How to test if a point is inside an SVG closed path - javascript

I'm trying to put together a drag & drop interface which allows a user to drag a div on a page which is constrained to the inside of an irregular closed SVG path.
Here's an example - the orange square is my draggable element, the gray SVG path is what I want to constrain it to on drop:
<div class="drag-parent">
<svg xmlns="http://www.w3.org/2000/svg" width="141.019" height="74.065" viewBox="0 0 141.019 74.065">
<defs>
<style>
.target {
fill: #333;
}
</style>
</defs>
<path id="Path_4569" data-name="Path 4569" class="target" d="M0,0H141.018V74.065h-24.27V37.033H10.88V12.971H0Z"/>
</svg>
<div class="draggable" style="width:20px;height:20px;background:orange;cursor:pointer;"></div>
</div>
What I'd like to do is check the draggable div as it's dragged, to make sure it's completely inside the closed path of my SVG.
I'm using GSAP Draggable to take care of actually dragging the element, but I'm stumped on how to test if it's inside that path or not.
So far I've tried isPointInFill however this seems to return true in chrome nomatter what I give it.
I've also tried using mouseenter / mouseleave events on the path which is a great starting point; but when you're dragging something those events don't fire since the mouse pointer is "ontop" of the dragged item rather than the SVG path.
What would be a good way to enforce bounds of an SVG path - or, is there a much simpler way to enforce irregular bounds on dragged items?

The main idea is to check if all 4 corners of the orange div are over the path. Maybe I've oversimplified the things since the drag_parent has `margin 0; padding:0;``
I hope this is what you were asking.
let D = false,// if D ids true you can drag
m = {},// the mouse position
thePath = document.querySelector("#Path_4569"),
draggable = document.querySelector("#draggable");
draggable.w = draggable.getBoundingClientRect().width;
draggable.h = draggable.getBoundingClientRect().height;
draggable.p0s = [[], [], [], []];//one array for every corner
draggable.delta = {};// distance between the click point and the left upper corner
draggable.addEventListener("mousedown", e => {
D = true;
draggable.delta = oMousePos(draggable, e);
});
drag_parent.addEventListener("mousemove", e => {
if (D == true) {
let counter = 0;// how many corners are in path
m = oMousePos(drag_parent, e);
draggablePoints(m);
draggable.style.left = draggable.p0s[0][0] + 1 + "px";
draggable.style.top = draggable.p0s[0][1] + 1 + "px";
draggable.p0s.map(p => {
if (document.elementFromPoint(p[0], p[1]) && document.elementFromPoint(p[0], p[1]).id == "Path_4569") {
counter++;
}
});
if (counter == 4) {// if all 4 corners are in path
thePath.setAttributeNS(null, "fill", "#777");
} else {
thePath.setAttributeNS(null, "fill", "black");
}
}
});
drag_parent.addEventListener("mouseup", e => {
D = false;
});
drag_parent.addEventListener("mouseleave", e => {
D = false;
});
function oMousePos(elmt, evt) {
var ClientRect = elmt.getBoundingClientRect();
return {
//objeto
x: Math.round(evt.clientX - ClientRect.left),
y: Math.round(evt.clientY - ClientRect.top)
};
}
function draggablePoints(m) {
//top left
draggable.p0s[0][0] = m.x - draggable.delta.x - 1;
draggable.p0s[0][1] = m.y - draggable.delta.y - 1;
//top right
draggable.p0s[1][0] = m.x - draggable.delta.x + draggable.w + 1;
draggable.p0s[1][1] = m.y - draggable.delta.y + 1;
//bottom right
draggable.p0s[2][0] = m.x - draggable.delta.x + draggable.w + 1;
draggable.p0s[2][1] = m.y - draggable.delta.y + draggable.h + 1;
//bottom left
draggable.p0s[3][0] = m.x - draggable.delta.x + 1;
draggable.p0s[3][1] = m.y - draggable.delta.y + draggable.h + 1;
}
*{margin:0;padding:0}
svg {
outline: 1px solid;
}
#drag_parent {
outline: 1px solid;
min-height: 100vh;
position:relative;
}
#draggable {
position: absolute;
width: 20px;
height: 20px;
background: orange;
cursor: pointer;
}
<div id="drag_parent">
<svg xmlns="http://www.w3.org/2000/svg" width="141.019" height="74.065" viewBox="0 0 141.019 74.065">
<path id="Path_4569" data-name="Path 4569" class="target" d="M0,0H141.018V74.065h-24.27V37.033H10.88V12.971H0Z"/>
</svg>
<div id="draggable"></div>
</div>

Related

Box-shadow responds to mouse position

I'm trying to create an animation in a webpage where an element's box-shadow responds to the mouse's position, i.e. (red X = mouse):
I already found a function that tracks the mouse movement, but I don't know how to apply this to the object. This is my code:
$(document).ready(function() {
function shadowAnimation() {
var objectToAnimate = $("#shadow-test");
document.onmousemove = handleMouseMove;
function handleMouseMove(event) {
var eventDoc, doc, body;
event = event || window.event;
if (event.pageX == null && event.clientX != null) {
eventDoc = (event.target && event.target.ownerDocument) || document;
doc = eventDoc.documentElement;
body = eventDoc.body;
event.pageX = event.clientX +
(doc && doc.scrollLeft || body && body.scrollLeft || 0) -
(doc && doc.clientLeft || body && body.clientLeft || 0);
event.pageY = event.clientY +
(doc && doc.scrollTop || body && body.scrollTop || 0) -
(doc && doc.clientTop || body && body.clientTop || 0);
}
console.log(event.pageX + " " + event.pageY);
}
}
});
#shadow-test {
box-shadow: -10px -10px red;
border: 1px solid white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<p id="shadow-test">This is a shadow test</p>
Your logic to capture the mouse position is much more complicated than it needs to be. You only need to track pageX and pageY of the mousemove event.
To move the shadow you simply need to work out the distance from the mouse to the mid-point of each dimension of the target element. Then you can apply that distance, limited by the available height of the element, to the size of the box-shadow, something like this:
jQuery($ => {
let $shadow = $('#shadow-test');
let shadowMax = $shadow.height();
let shadowMin = shadowMax * -1;
let shadowMidPoints = [
$shadow.offset().left + $shadow.width() / 2,
$shadow.offset().top + $shadow.height() / 2
];
$(document).on('mousemove', e => {
let shadowX = Math.min(shadowMax, Math.max(shadowMin, shadowMidPoints[0] - e.pageX));
let shadowY = Math.min(shadowMax, Math.max(shadowMin, shadowMidPoints[1] - e.pageY));
$shadow.css('box-shadow', `${shadowX}px ${shadowY}px red`);
});
});
body {
height: 2000px;
}
#shadow-test {
box-shadow: -10px -10px red;
border: 1px solid white;
margin: 100px;
background-color: #CCC;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<p id="shadow-test">This is a shadow test</p>
Please take a look at the following examples(sans jquery).
const p = document.querySelector('#shadow-test');
const clamp = (a, m, n) => {
const max = Math.max(m, n);
const min = Math.min(m, n);
return Math.max(min, Math.min(max, a));
};
const MAX_SHADOW_OFFSET = 30;
const paint = (x, y) => {
const r = p.getBoundingClientRect();
const o = Math.min(r.width, r.height, MAX_SHADOW_OFFSET); // compute max shadow offset
const mx = clamp(x, r.left - o, r.right + o); // clamp mouse coordinates within the shadow projection bounding box.
const my = clamp(y, r.top - o, r.bottom + o);
const px = r.right - r.width / 2; // compute element bb midpoints.
const py = r.bottom - r.height / 2;
const nx = (mx - px) / (r.right - r.left + 2 * o); // project mouse position relative to the bounding box to [-.5, .5];
const ny = (my - py) / (r.bottom - r.top + 2 * o);
requestAnimationFrame(() => {
p.style.boxShadow = `${-1 * nx * o}px ${-1 * ny * o}px var(--shadow-color)`;
});
};
document.addEventListener('mousemove', (e) => paint(e.clientX, e.clientY), {
passive: true
});
:root {
--primary-color: black;
--secondary-color: white;
--shadow-color: red;
--button-color: blue;
--color: white;
}
body {
background-color: var(--primary-color);
padding: 1rem;
}
.center {
text-align: center;
}
button {
background-color: var(--button-color);
color: var(--color);
border: 0;
border-radius: .25rem;
padding: .375rem 1rem;
}
#shadow-test {
border: 1px solid var(--secondary-color);
color: var(--secondary-color);
}
<div class="center">
<button>
More
</button>
</div>
<p id="shadow-test">This is a shadow test</p>
Its definitely a mouthful compared to the other solutions but its not all that complex. The algorithm can be broken down into a couple of steps.
We figure out the bounding box of the element. We need this to figure out the max offset for our box shadow as well as projections to our normalize coordinate space later on.
We clamp the mouse coordinates to our new bounding box. This is used so that if you move your mouse far away from the element it doesn't continue moving. We also will need the mouse coordinates to project our to our normalized coordinate space.
We find the midpoint of our element. Anything left of the midpoint will be negative, anything right will be positive and so on. We can use the sign values to figure what side the box shadow should be. A mouse on the left side of the midpoint will give use a negative value, on the right side it will be positive.
Finally we project our mouse coordinates to our normalized coordinate space of [-0.5, .5]. The normalized coordinates makes it super easy to compute offsets by just simple multiplication. Think of (nx,ny) as just scale values. Given a max shadow offset value, how much should we apply to the style? and what direction?
Other notes
Im updating the style in an request animation frame. This is typically better for performance. You should also note the css variables. This is done so i don't have to hard code the color value of the box-shadow in code. The theme lives completely in css.
As I understand you implemented tracking of mouse.
In your screenshot and description its not clear when box-shadow property changes according to mouse position.
Once you catch mouse position and can calculate position of where mouse pointer is then just use this js code to update box-shadow property of your text input.
three case
$("#shadow-test").css('box-shadow', '-10px -10px red');
$("#shadow-test").css('box-shadow', '10px 10px red');
$("#shadow-test").css('box-shadow', '0px 10px red');

Trying to get this smoother and more natural in behavior

My implementation,
http://kodhus.com/kodnest/land/PpNFTgp
I am curious, as I am not able for some reason to figure this out, how to get my JavaScript to make my slider behave more natural and smoother, if someone knows, how to, or can make this, feel free. I'd be happy to understand.
JavaScript:
const thumb = document.querySelector('.thumb');
const thumbIndicator = document.querySelector('.thumb .thumb-indicator');
const sliderContainer = document.querySelector('.slider-container');
const trackProgress = document.querySelector('.track-progress');
const sliderContainerStart = sliderContainer.offsetLeft;
const sliderContainerWidth = sliderContainer.offsetWidth;
var translate;
var dragging = false;
var percentage = 14;
document.addEventListener('mousedown', function(e) {
if (e.target.classList.contains('thumb-indicator')) {
dragging = true;
thumbIndicator.classList.add('focus');
}
});
document.addEventListener('mousemove', function(e) {
if (dragging) {
console.log('moving', e)
if (e.clientX < sliderContainerStart) {
translate = 0;
} else if (e.clientX > sliderContainerWidth + sliderContainerStart) {
translate = sliderContainerWidth;
} else {
translate = e.clientX - sliderContainer.offsetLeft;
}
thumb.style.transform = 'translate(-50%) translate(' + translate + 'px)';
trackProgress.style.transform = 'scaleX(' + translate / sliderContainerWidth + ')'
}
});
function setPercentage() {
thumb.style.transform = 'translate(-50%) translate(' + percentage/100 * sliderContainerWidth + 'px)';
trackProgress.style.transform = 'scaleX(' + percentage/100 + ')';
}
function init() {
setPercentage();
}
init();
document.addEventListener('mouseup', function(e) {
dragging = false;
thumbIndicator.classList.remove('focus');
});
EDIT: Is there a way to smoothly and naturally increment by one for every slow move?
Is it possible to make to behave as if, like when one clicks the progress bar so that it jumps there?
The kodhus site is very janky in my browser, so I can't tell if your code lacks responsiveness or whether it's the site itself. I feel that your code is a bit convoluted: translate and width / height are mixed unnecessarily; no need to use a dragging boolean when that information is always stored in the classlist. The following slider performs nicely, and has a few considerations I don't see in yours:
stopPropagation when clicking the .thumb element
drag stops if window loses focus
pointer-events: none; applied to every part of the slider but the .thumb element
let applySliderFeel = (slider, valueChangeCallback=()=>{}) => {
// Now `thumb`, `bar` and `slider` are the elements that concern us
let [ thumb, bar ] = [ '.thumb', '.bar' ].map(v => slider.querySelector(v));
let changed = amt => {
thumb.style.left = `${amt * 100}%`;
bar.style.width = `${amt * 100}%`;
valueChangeCallback(amt);
};
// Pressing down on `thumb` activates dragging
thumb.addEventListener('mousedown', evt => {
thumb.classList.add('active');
evt.preventDefault();
evt.stopPropagation();
});
// Releasing the mouse button (anywhere) deactivates dragging
document.addEventListener('mouseup', evt => thumb.classList.remove('active'));
// If the window loses focus dragging also stops - this can be a very
// nice quality of life improvement!
window.addEventListener('blur', evt => thumb.classList.remove('active'));
// Now we have to act when the mouse moves...
document.addEventListener('mousemove', evt => {
// If the drag isn't active do nothing!
if (!thumb.classList.contains('active')) return;
// Compute `xRelSlider`, which is the mouse position relative to the
// left side of the slider bar. Note that *client*X is compatible with
// getBounding*Client*Rect, and using these two values we can quickly
// get the relative x position.
let { width, left } = slider.getBoundingClientRect();
// Consider mouse x, subtract left offset of slider, and subtract half
// the width of the thumb (so drags position the center of the thumb,
// not its left side):
let xRelSlider = evt.clientX - left - (thumb.getBoundingClientRect().width >> 1);
// Clamp `xRelSlider` between 0 and the slider's width
if (xRelSlider < 0) xRelSlider = 0;
if (xRelSlider > width) xRelSlider = width;
// Apply styling (using percents is more robust!)
changed(xRelSlider / width);
evt.preventDefault();
evt.stopPropagation();
});
slider.addEventListener('mousedown', evt => {
let { width, left } = slider.getBoundingClientRect();
// Clicking the slider also activates a drag
thumb.classList.add('active');
// Consider mouse x, subtract left offset of slider, and subtract half
// the width of the thumb (so drags position the center of the thumb,
// not its left side):
let xRelSlider = evt.clientX - left - (thumb.getBoundingClientRect().width >> 1);
// Apply styling (using percents is more robust!)
changed(xRelSlider / width);
evt.preventDefault();
evt.stopPropagation();
});
changed(0);
};
let valElem = document.querySelector('.value');
applySliderFeel(document.querySelector('.slider'), amt => valElem.innerHTML = amt.toFixed(3));
.slider {
position: absolute;
width: 80%; height: 4px; background-color: rgba(0, 0, 0, 0.3);
left: 10%; top: 50%; margin-top: -2px;
}
.slider > .bar {
position: absolute;
left: 0; top: 0; width: 0; height: 100%;
background-color: #000;
pointer-events: none;
}
.slider > .thumb {
position: absolute;
width: 20px; height: 20px; background-color: #000; border-radius: 100%;
left: 0; top: 50%; margin-top: -10px;
}
.slider > .thumb.active {
box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.5);
}
<div class="slider">
<div class="bar"></div>
<div class="thumb"></div>
</div>
<div class="value"></div>

spawn & drag of SVG elements - approach

I am on my learning curve for Javascript/SVG combo (animating and making interactive SVGs).
I wanted to create a code snippet where menu elements ("inventory") can be dragged over to the main screen ("canvas") while the originating element would remain in its place (as if one would move a copy of it off the original element).
Here I crafted the code snippet as best as I could:
http://codepen.io/cmer41k/pen/f2b5eea274cdde29b0b2dc8a2424a645
So I sort of managed to do something but its buggy:
I could deal with 1 copy and making it draggable, but then I don't know how to deal with IDs for all those spawning elements, which causes dragging issues
I fail to understand how to make it work indefinitely (so that it can spawn any amount of circles draggable to canvas)
Draggable elements in canvas often overlap and I fail to attach the listeners the way they don't overlap so that the listener on the element I am dragging would propagate "through" whatever other elements there;(
Question is basically - can someone suggest logic that I should put into this snippet so that it was not as cumbersome. I am pretty sure I am missing something here;( (e.g. it should not be that hard is it not?)
HTML:
<body>
<svg id="svg"
height="800"
width="480"
viewbox="0 0 480 800"
preserveAspectRatio="xMinYMin meet"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<rect id="canvasBackground" width="480" height="480" x="0" y="0"/>
<rect id="inventoryBackground" width="480" height="100" x="0" y="480"/>
<g id="inventory">
<path id="curve4" class="inventory" d="M60,530 A35,35 0 1,1 60,531" />
<path id="curve3" class="inventory" d="M150,530 A35,35 0 1,1 150,531" />
<path id="curve2" class="inventory" d="M240,530 A35,35 0 1,1 240,531" />
<path id="curve1" class="inventory" d="M330,530 A35,35 0 1,1 330,531" />
</g>
<g id="canvas">
</g>
</svg>
</body>
Javascript:
// define meta objects
var drag = null;
// this stores all "curves"-circles
var curves = {};
var canvas = {};
var inventory = {};
window.onload = function() {
// creates the curve-circles in the object and at their initial x,y coords
curves.curve1 = document.getElementById("curve1");
curves.curve1.x = 0;
curves.curve1.y = 0;
curves.curve2 = document.getElementById("curve2");
curves.curve2.x = 0;
curves.curve2.y = 0;
curves.curve3 = document.getElementById("curve3");
curves.curve3.x = 0;
curves.curve3.y = 0;
curves.curve4 = document.getElementById("curve4");
curves.curve4.x = 0;
curves.curve4.y = 0;
canvas = document.getElementById("canvas");
inventory = document.getElementById("inventory");
// attach events listeners
AttachListeners();
}
function AttachListeners() {
var ttt = document.getElementsByClassName('inventory'), i;
for (i = 0; i < ttt.length; i++) {
document.getElementsByClassName("inventory")[i].onmousedown=Drag;
document.getElementsByClassName("inventory")[i].onmousemove=Drag;
document.getElementsByClassName("inventory")[i].onmouseup=Drag;
}
}
// Drag function that needs to be modified;//
function Drag(e) {
e.stopPropagation();
var t = e.target, id = t.id, et = e.type; m = MousePos(e);
if (!drag && (et == "mousedown")) {
if (t.className.baseVal=="inventory") { //if its inventory class item, this should get cloned into draggable?
copy = t.cloneNode(true);
copy.onmousedown=copy.onmousemove=onmouseup=Drag;
inventory.insertBefore(copy, inventory.firstChild);
drag = t;
dPoint = m;
}
if (t.className.baseVal=="draggable") { //if its just draggable class - it can be dragged around
drag = t;
dPoint = m;
}
}
// drag the spawned/copied draggable element now
if (drag && (et == "mousemove")) {
curves[id].x += m.x - dPoint.x;
curves[id].y += m.y - dPoint.y;
dPoint = m;
curves[id].setAttribute("transform", "translate(" +curves[id].x+","+curves[id].y+")");
}
// stop drag
if (drag && (et == "mouseup")) {
t.className.baseVal="draggable";
drag = null;
}
}
// adjust mouse position to the matrix of SVG & screen size
function MousePos(event) {
var p = svg.createSVGPoint();
p.x = event.clientX;
p.y = event.clientY;
var matrix = svg.getScreenCTM();
p = p.matrixTransform(matrix.inverse());
return {
x: p.x,
y: p.y
}
}
You were close. You had a couple of bugs. Eg.
copy.onmousedown=copy.onmousemove=onmouseup=Drag;
should have been:
copy.onmousedown=copy.onmousemove=copy.onmouseup=Drag;
And drag = t should have been drag = copy (?)
Also you were appending the clones to the inventory section, when I think you intended to append them to the "canvas" section.
But there were also also some less-obvious mistakes that were contributing to the unreliableness. For example, if you attach the mousemove and mouseup events to the inventory and clone shapes, then you will won't get the events if you drag too fast. The mouse will get outside the shape, and the events won't be passed to the shapes. The fix is to move those event handlers to the root SVG.
Another change I made was to store the x and y positions in the DOM for the clone as _x and _y. This makes it easier than trying to keep them in a separate array.
Anyway, here's my modified version of your example which works a lot more reliably.
// define meta objects
var drag = null;
var canvas = {};
var inventory = {};
window.onload = function() {
canvas = document.getElementById("canvas");
inventory = document.getElementById("inventory");
// attach events listeners
AttachListeners();
}
function AttachListeners() {
var ttt = document.getElementsByClassName('inventory'), i;
for (i = 0; i < ttt.length; i++) {
document.getElementsByClassName("inventory")[i].onmousedown=Drag;
}
document.getElementById("svg").onmousemove=Drag;
document.getElementById("svg").onmouseup=Drag;
}
// Drag function that needs to be modified;//
function Drag(e) {
var t = e.target, id = t.id, et = e.type; m = MousePos(e);
if (!drag && (et == "mousedown")) {
if (t.className.baseVal=="inventory") { //if its inventory class item, this should get cloned into draggable?
copy = t.cloneNode(true);
copy.onmousedown = Drag;
copy.removeAttribute("id");
copy._x = 0;
copy._y = 0;
canvas.appendChild(copy);
drag = copy;
dPoint = m;
}
else if (t.className.baseVal=="draggable") { //if its just draggable class - it can be dragged around
drag = t;
dPoint = m;
}
}
// drag the spawned/copied draggable element now
if (drag && (et == "mousemove")) {
drag._x += m.x - dPoint.x;
drag._y += m.y - dPoint.y;
dPoint = m;
drag.setAttribute("transform", "translate(" +drag._x+","+drag._y+")");
}
// stop drag
if (drag && (et == "mouseup")) {
drag.className.baseVal="draggable";
drag = null;
}
}
// adjust mouse position to the matrix of SVG & screen size
function MousePos(event) {
var p = svg.createSVGPoint();
p.x = event.clientX;
p.y = event.clientY;
var matrix = svg.getScreenCTM();
p = p.matrixTransform(matrix.inverse());
return {
x: p.x,
y: p.y
}
}
/* SVG styles */
path
{
stroke-width: 4;
stroke: #000;
stroke-linecap: round;
}
path.fill
{
fill: #3ff;
}
html, body {
margin: 0;
padding: 0;
border: 0;
overflow:hidden;
background-color: #fff;
}
body {
-ms-touch-action: none;
}
#canvasBackground {
fill: lightgrey;
}
#inventoryBackground {
fill: grey;
}
.inventory {
fill: red;
}
.draggable {
fill: green;
}
svg {
position: fixed;
top:0%;
left:0%;
width:100%;
height:100%;
}
<svg id="svg"
height="800"
width="480"
viewbox="0 0 480 800"
preserveAspectRatio="xMinYMin meet"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<rect id="canvasBackground" width="480" height="480" x="0" y="0"/>
<rect id="inventoryBackground" width="480" height="100" x="0" y="480"/>
<g id="inventory">
<path id="curve4" class="inventory" d="M60,530 A35,35 0 1,1 60,531" />
<path id="curve3" class="inventory" d="M150,530 A35,35 0 1,1 150,531" />
<path id="curve2" class="inventory" d="M240,530 A35,35 0 1,1 240,531" />
<path id="curve1" class="inventory" d="M330,530 A35,35 0 1,1 330,531" />
</g>
<g id="canvas">
</g>
</svg>

How do i animate the fill of an inline svg to change with mouse position (in parent div)

Is it possible to animate the fill of a inline svg path, to change as the mouse moves around the page ?
In the example in the code snippet below, the x and y positions of the mouse are monitored using the "mousemove" event and the screenX and screenY properties of the event object. Red and green colors values are set depending on those coordinates, and an rgb fill color string is constructed and imposed on an SVG path element.
var path = document.querySelector("path");
document.body.addEventListener("mousemove", function(evt) {
var xRatio = Math.min(1, evt.clientX / document.body.offsetWidth );
var yRatio = Math.min(1, evt.clientY / document.body.offsetHeight);
var red = parseInt(255 * xRatio);
var green = parseInt(255 * yRatio);
var color = "rgb(" + red + ", " + green + ", 128)";
document.querySelector("path").setAttribute("fill", color);
});
body {
border: solid black 1px;
padding: 0.5em;
}
p {
margin: 0;
}
<body>
<p>Move your mouse over the image to see the colour-change effect.</p>
<svg width="400" height="150">
<path d="M10,75 C200,-100 300,120 390,75 200,250 100,30 10,75" stroke="black" stroke-width="4" fill="none" />
</svg>
</body>

scroll while dragging element with d3.js

I am using d3's drag behavior on an svg element that might need to be dropped outside of the visible area. I have it setup within two divs with overflow set to auto to enable scrolling. I have this only working with some browsers but not all.
The issue is that in some browsers, you will be able to scroll while dragging, but in others the window will not scroll while dragging. I have as of yet been unable to find a way to make this work consistently.
for a working example see the fiddle: http://jsfiddle.net/glanotte/qd5Td/1/
This is working as expected on:
chrome - mac/windows
safari - mac
But not working on
Firefox - mac/windows
IE - windows
html:
<div id="outerFrame">
<div id="innerFrame">
<svg width="600" height="600">
</svg>
</div>
</div>
css:
#outerFrame{
width: 300px;
height: 300px;
border: 1px solid red;
overflow: auto;
}
#innerFrame{
width: 600px;
height: 600px;
background-color: lightgrey;
}
javascript:
var drag = d3.behavior.drag()
.on("dragstart", dragstart)
.on("drag", dragmove)
.on("dragend", dragend);
function dragstart() {
d3.select(this).style("border", "1px solid red");
}
function dragmove(d) {
var coordinates = d3.mouse(d3.select("svg").node());
d3.select(this).attr({
x: coordinates[0] - 50,
y: coordinates[1] - 25
})
}
function dragend() {
d3.select(this).style("border", null);
}
d3.select("svg")
.append("rect")
.attr({x: 100, y: 100, width: 100, height: 50})
.call(drag);
You have unfortunately struck upon a bug in Firefox which has been noticed before by mbostock and marked as WONT-FIX.
As per suggestion in the bug report, you can make it work, but only by scrolling the container manually: http://jsfiddle.net/62CYD/
The implementation is pretty simple and can be improved by:
Using animations
Taking into account width of scroll bars, like done in DOMUtilityService in ng-grid.
Taking current mouse position into account to avoid snapping of the dragged item and smoother scrolling.
Use setTimeout to continue scrolling even if dragging stops
function dragmove(d) {
var svg = d3.select("svg").node(),
$parent = $('#outerFrame'),
w = $parent.width(), h = $parent.height(),
sL = $parent.scrollLeft(), sT = $parent.scrollTop();
var coordinates = d3.mouse(svg),
x = coordinates[0],
y = coordinates[1];
if (x > w + sL) {
$parent.scrollLeft(x - w);
} else if (x < sL) {
$parent.scrollLeft(x);
}
if (y > h + sT) {
$parent.scrollTop(y - h);
} else if (y < sT) {
$parent.scrollTop(y);
}
d3.select(this).attr({
x: x - 50,
y: y - 25
})
}

Categories

Resources