I have multiple SVG elements that are in separate groups. They overlap each other. Example:
<svg id="board" width="100%" height="80%">
<g id="terrain" class="layer">
<path d="M-32,-32L32,-32 32,32 -32,32Z" transform="translate(0, 0)" class="mote terrain hill"></path>
</g>
<g id="guy" class="layer">
<path d="M-21...Z" transform="translate(192, 448)" class="mote guy"></path>
</g>
</svg>
When an x, y position that matches both is clicked, I want to know all that both were clicked. If I bind each to the 'click' event, only the event handlers for one on top gets called. Which is reasonable, although not what I want here.
I'm thinking of creating a topmost layer and having that catch all clicks, then figure out which elements in the other layers should be notified. That's a lot of tracking that I'd like to avoid, if possible. Are there simpler approaches to this?
From The SVG spec
"By default, pointer-events must not be dispatched on the clipped (non-visible) regions of a shape. For example, a circle with a radius of 10 which is clipped to a circle with a radius of 5 will not receive 'click' events outside the smaller radius. Later versions of SVG may define new properties to enable fine-grained control over the interactions between hit testing and clipping."
However, there is a way of getting a list of svg shapes that intersect at a particular point. The "getIntersectionList" function returns a list of items.
I've created one of those jsfiddle things jsfiddle.net/uKVVg/1/ Click on the intersection of the circles to get a list of ID's. Manually send events to that list.
Javascript follows:
function s$(a) {
return document.getElementById(a);
}
var list
function hoverElement(evt) {
var root = s$("canvas");
var disp = s$("pointer");
disp.setAttribute("x",evt.clientX);
disp.setAttribute("y",evt.clientY);
rpos = root.createSVGRect();
rpos.x = evt.clientX;
rpos.y = evt.clientY;
rpos.width = rpos.height = 1;
list = root.getIntersectionList(rpos, null);
s = "clicked: "
$.each(list,function(item,val){
if (val.id != "pointer") {
s = s + (val.id) + " ";
}
})
alert(s);
}
var root = s$("canvas");
root.addEventListener("click", hoverElement, false);
There's some javascript that could probably be tidied up, but hopefully it answers your question.
Related
As I understand it, getBBox() is supposed to ignore the transformations applied to the element it's measuring.
eg: if I have: <g transform="scale(0.5, 0.5)"><rect width="100" height="100" /></g>, calling getBBox on <g> will always return width: 100, height: 100, no matter what the scale() args are. However, this does NOT appear to be the case when using a <text> element instead of <rect>. The size of <text> reported by getBBox() changes as the transformation changes.
Full example:
const gNode = document.querySelector("g");
const dimensionsBB = () => {
console.log(gNode.getBBox());
}
// trigger every time a change in size is detected
const resizeObserver = new ResizeObserver(dimensionsBB);
resizeObserver.observe(gNode);
// update transformation every second
setInterval(() => {
gNode.setAttribute("transform", `scale(${Math.random()}, ${Math.random()})`);
}, 1000);
<svg width="500" height="500">
<g>
<text>Text goes here</text>
</g>
</svg>
In this example, the resizeObserver is triggered each time the scale changes, and the dimensions reported by getBBox() are different each time (with slight variations).
Why? And is there a way to always get a consistent result?
I have a D3 graph that allows a user to click a button to take them to a specified node. The button looks like this:
<button type="button" class="btn btn-primary" ng-click="ctrl.panGraph(9)">Go to End</button>
This button will take the user from wherever they are in the svg at the time of click, to the x and y coordinates of the last node, with the id of 9. On click this function is called:
function panGraph (nodeId:any) {
svgWidth = parseInt(svg.style("width").replace(/px/, ""), 10);
svgHeight = parseInt(svg.style("height").replace(/px/, ""), 10);
for (var i = 0; i < renderedNodes.length; i++) {
if (nodeID === renderedNodes[i].id) {
ctrl.selectedNode = renderedNodes[i];
var translate = [svgWidth / 2 - renderedNodes[i].x, svgHeight / 2 - renderedNodes[i].y];
var scale = 1;
svg.transition().duration(4000).ease(d3.easeExpInOut).call(zoom.translate(translate).scale(scale).event);
}
}
}
In the above function I have all the rendered nodes that have been rendered on the page, once I find the matching id I use its x and y coordinates to center the specified node in the middle of the svg. That all works fine.
I am trying to use some animations during the time that the graph is translating to the specified node on button click. When the user clicks the button that takes him or her to the specified node, is it possible to animate the transition so that the transition initially starts slow, then speeds up, but then slows down again at the end as it gets close to the specified node? Thanks
UPDATE:
The above code with the "ease" incluided gives me this console error:
angular.js:13550 TypeError: Cannot read property 'indexOf' of undefined
at Object.d3.ease (d3.js:5844)
at Array.d3_transitionPrototype.ease (d3.js:8838)
at zoomOnNode (DiagramComponent.ts:1128)
at DiagramComponent.ts:1072
at Scope.$digest (angular.js:17073)
at Scope.$apply (angular.js:17337)
at HTMLButtonElement.<anonymous> (angular.js:25023)
at HTMLButtonElement.dispatch (jquery.js:4737)
at HTMLButtonElement.elemData.handle (jquery.js:4549)
Here is the v3 equivalent to Gerardo's post regarding v4:
svg.transition().duration(1000).ease("exp-in-out").call(zoom.translate(translate).scale(scale).event);
For a list of all the easing equivalents from v3 to v4 and other changes:
https://github.com/d3/d3/blob/master/CHANGES.md
One (out of several) solution is to use ease with d3.easeExpInOut, or d3.easePolyInOut.exponent(x) with a high exponent (like x=4 or x=5).
See this snippet. Click the circle to see it moving from left to right, starting slow, speeding up and then slowing down again:
d3.select("circle").on("click", function(){
d3.select(this).transition()
.duration(4000)
.ease(d3.easeExpInOut)
.attr("cx", 360)
});
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="400" height="200">
<circle cx="40" cy="100" r="30" fill="teal"></circle>
<line x1="40" x2="40" y1="100" y2="150" stroke="black" stroke-width="1"></line>
<line x1="360" x2="360" y1="100" y2="150" stroke="black" stroke-width="1"></line>
</svg>
I would like to let users re-draw over a svg shape (the number 8), and be able to detect if the shape is well completed or not. The user will have to re-draw over the borders of this svg, with his finger on mobile, to be abIe to continue through the website. I want to use Javascript.
My first approach was to use hidden divs and detect whenever the mouse passes over each one in the right order (see sketch), but there can be errors. And I would also like to highlight the drawn track in a way to show the filling progress.
Which approach should I use ?
Well, it depends on how you implemented your drawing canvas. Without the actual code, it's difficult to provide a good answer.
Regardless, you might try to use a second path that is hidden/transparent and overlaps the first one.
Then capture the mouseenter/mouseleave (or whatever events you're using to "draw") and based on the event coordinates, assess if the "shape" was correctly draw or not.
Basic example:
var foo = document.getElementById('foo'),
x = -1,
y = -1;
function between(n, en, err) {
return (n >= en - err && n <= en + err);
}
function fail() {
// user failed so do stuff accordingly
console.warn('User failed to draw properly');
// reset stuff
x = -1;
y = -1;
}
foo.addEventListener("mouseenter", function(event) {
console.log('enter: ' + event.x + ',' + event.y);
if (x === -1) {
// first enter, so check if user started in the correct position
// This is tied to the position of the canvas, if the user must start at the beginning
// of the path or he can start wherever...
// so you should implement your own logic
return;
}
// Check if drawing is complete
// Onde again, same as above
// check if after the first mouseleave, the mouseenter event was
// in an acceptable range
if (!between(x, event.x, 10) || !between(y, event.y, 10)) {
fail();
}
});
foo.addEventListener("mouseleave", function(event) {
console.log('exit: ' + event.x + ',' + event.y);
x = event.x;
y = event.y;
});
<svg>
<path id="foo" d="M100,100
L150,100
a50,25 0 0,0 150,100
q50,-50 70,-170
Z" stroke-width="20" style="stroke: red; fill: none;" />
<path d="M100,100
L150,100
a50,25 0 0,0 150,100
q50,-50 70,-170
Z" style="stroke: #006666; fill: none;" />
</svg>
For instance, in this example I accept an "error" of 10px. So I set the stroke of the hidden path to 20px.
When the user mouse leaves the accepted drawing area, if it not reenters in an acceptable range, it fails the drawing and resets.
This is just an example, but you can build from this
I've created several shapes in SVG and added an effect that changes the shapes color when hovered over.
But how can I make the shapes "pop up" slightly when hovered over?
Would appreciate any help.
You can modify the transform of each element. Probably something like this:
function onEnter (e) {
e.target.setAttribute('transform', 'scale(2,2)');
}
function onLeave (e) {
e.target.removeAttribute('transform');
}
But in fact you do not need to take the long slow way using the attributes, instead you can use the transform interface of SVG elements. The documentation therefore is on the page linked above, too. But dealing with this might be a bit »unintuitive« the first time, since each element has a transform list with several transform items.
I think it would be a good Idea to dive in the svg transform interface (and affine transformations in general), since it is very powerful, but it requires a »learning curve« before you are ready to use it.
If you want to short circuit this you can also use a helper framework like: svg.js, snapsvg or raphael which all have common methods to help you dealing with thing like that.
EDIT
The functions above are just examples for one could do it. The onEnter and onLeave methods should be called only if the mouse enters the svg element or when it leaves. So you could add a mousemove listener and check if the target is one of your desired elements:
var firstElement = document.getElementById('<yourelementsid>');
isOverFirstElement = false;
window.addEventListener('mousemove', function (e) {
if (e.target === firstElement && isOverFirstElement == false) {
isOverFirstElement = true;
onEnter(e);
} else if (e.target !== firstElement && isOverFirstElement == true) {
isOverFirstElement = false;
onLeave(e);
}
}, false);
With code like that you can detect a mouse enter or leave and react accordingly.
I've got it to do what I wanted by adding
target.setAttributeNS(null, 'transform', "translate(0 0)");
as such:
function unselectedColour(evt) {
var target = evt.target;
target.setAttributeNS(null, 'fill', 'white');
target.setAttributeNS(null, 'transform', "translate(0 0)");
}
function selectedColourBuilding(evt) {
var target = evt.target;
target.setAttributeNS(null, 'fill', 'purple');
target.setAttributeNS(null, 'transform', "translate(-3 -3)");
}
To make the 'pop out' stable, keeping it at it's current position when scaling, you can compute the center of its bounding box then translate the element's center to the origin, scale it, then translate back to its current location
e.g.
function onEnter(evt)
{
var target=evt.target
var bb=target.getBBox()
var bbx=bb.x
var bby=bb.y
var bbw=bb.width
var bbh=bb.height
var cx=bbx+.5*bbw
var cy=bby+.5*bbh
target.setAttribute("transform","translate("+(cx)+" "+(cy)+")scale(1.2 1.2)translate("+(-cx)+" "+(-cy)+")")
}
function onExit(evt)
{
var target=evt.target
target.removeAttribute("transform")
}
I am looking to build a simple photo collage using KineticJS and Canvas. In the canvas container, I have pre-defined image areas. I also have (in the DOM and outside of the canvas container), a strip of images which I want to drag and drop to the image wells on the canvas. I am able to drag an image and snap it to a (hard-coded) image well but while the image is moving on canvas I'm unable to get the x and y co-ordinates. I've tried using jQuery's drag/drag event but then it is blind to the canvas and I've tried to use use the KineticJS's dragover function but then I can't get the DOM image's x and y. The idea is that knowing the x,y of the image being dragged I can write some logic to figure out which image location it should get snapped to - as opposed to hard coding it's target.
Is it even possible to do this with KineticJS+jQuery - drag images from a DOM onto a canvas and have them snap into pre-defined image areas? Or, is there a simpler way?
as far as I understand your problem, I tried to reproduce it, but not seeing any difficulties
<div width="600" height="500" style="background-color:green;padding-left:60px" id="wrap">
<canvas id="a" width="300" height="225" style="background-color:yellow"></canvas>
</div>
<img id="dragMe" src="http://www.linuxmint.com/img/logo.png"/>
<div id="log">
<span class="a"></span>
<span class="b"></span>
<span class="c"></span>
<span class="d"></span>
</div>
$("#wrap").mousemove(function(e){
var pageCoords = "( " + e.pageX + ", " + e.pageY + " )";
var clientCoords = "( " + e.clientX + ", " + e.clientY + " )";
$("#log > span.a").text("( e.pageX, e.pageY ) : " + pageCoords);
$("#log > span.b").text("( e.clientX, e.clientY ) : " + clientCoords);
});
var p = $('#a').position();
$("#dragMe").draggable({
drag: function(event, ui) {
$("#log > span.c").text( ui.offset.left+':' +ui.offset.top);
$("#log > span.d").text( ( ui.offset.top-p.top)+':' +(ui.offset.left - p.left));
}
});
http://jsfiddle.net/agj3N/1/
When I asked you if you had a reason you had the strip of images outside of the KineticJS stage, you answered:
Only because when I toJSON it (for saving and calling up for a future edit), I just want the images on my photo collage and not the photos on the strip.
My answer to this is:
You don't have to use toJSON on the entire stage.
If you separate the Photo Strip (previously outside of the canvas) into a different layer from the Collage (previously inside the canvas, then you can have two layers and use toJSON on only one of those layers!
The result will be that toJSON will only serialize the objects from that given layer, which sounds like what you need.
jsfiddle - here is an example to illustrate what I mean, excuse my poor logic for snapping the image to the group. I have 2 layers: photoStripLayer and collageLayer
Click on the Collage toJSON button. Notice the console.log output does not include any of the images. This is because collageLayer only has a group inside it that has a child rectangle.
Drag the first yoda (top left, the rest don't work this is just an example) into the red box. Sorry you'll have to play around with this to get it to snap properly (I'm assuming you have your "snap" code already)
On dragend if the Yoda node is inside the red box, it will use KineticJS' moveTo function to move the yoda from the photoStripLayer --> collageLayer. You'll know it worked when the Yoda snaps to position (0,0) relative to the new group.
Now Click on the Collage toJSON button. Notice that the yoda image is now a part of the toJSON console.log output. The yoda was moved to the new group, which is a child of collageLayer. Now part of the collageLayer, Yoda is.
Here's the dragend code:
node.on('dragend', function () {
var pos = stage.getMousePosition();
var X = pos.x;
var Y = pos.y;
var minX = snap.getX();
var maxX = snap.getX() + snap.getWidth();
var minY = snap.getY();
var maxY = snap.getY() + snap.getHeight();
if (node.getX() < minX) {
node.moveTo(snap);
node.setX(0);
node.setY(0);
}
if (node.getX() > maxX) {
node.moveTo(snap);
node.setX(0);
node.setY(0);
}
if (node.getY() < minY) {
node.moveTo(snap);
node.setX(0);
node.setY(0);
}
if (node.getY() > maxY) {
node.moveTo(snap);
node.setX(0);
node.setY(0);
}
photoStripLayer.draw();
collageLayer.draw();
});
And the button click code to illustrate using toJSON:
function collageToJSON() {
var cjson = collageLayer.toJSON();
console.log(cjson);
/* To illustrate, you can also call toDataURL here. But in JSFiddle I think it throws a Security Error.
collageLayer.toDataURL({
callback: function(dataUrl) {
window.open(dataUrl);
}
});
*/
}
document.getElementById('CtoJSON').addEventListener('click', function () {
collageToJSON();
});
And there you have it! The issue is solved by letting KineticJS handle the entire process.