How to add click to shaped in loop? - javascript
I'm passing text arrays to my circleCreate function, which creates a wedge for each text. What I'm trying to do is add a click event to each wedge, so when the user clicks on a wedge, it throws an alert with each wedges text.
But it's not working. Only the outer circle is alerting text. And it always says the same text. Both inner circles alert undefined.
http://jsfiddle.net/Yushell/9f7JN/
var layer = new Kinetic.Layer();
function circleCreate(vangle, vradius, vcolor, vtext) {
startAngle = 0;
endAngle = 0;
for (var i = 0; i < vangle.length; i++) {
// WEDGE
startAngle = endAngle;
endAngle = startAngle + vangle[i];
var wedge = new Kinetic.Wedge({
x: stage.getWidth() / 2,
y: stage.getHeight() / 2,
radius: vradius,
angleDeg: vangle[i],
fill: vcolor,
stroke: 'black',
strokeWidth: 1,
rotationDeg: startAngle
});
/* CLICK NOT WORKING
wedge.on('click', function() {
alert(vtext[i]);
});*/
layer.add(wedge);
}
stage.add(layer);
}
This is a typical problem you'll run into with asynchronous JavaScript code such as event handlers. The for loop in your circleCreate() function uses a variable i which it increments for each wedge. This is fine where you use i to create the wedge:
angleDeg: vangle[i],
But it fails where you use it inside the click event handler:
alert(vtext[i]);
Why is that?
When you create the wedge using the new Kinetic.Wedge() call, this is done directly inside the loop. This code runs synchronously; it uses the value of i as it exists at the very moment that this particular iteration of the loop is run.
But the click event handler doesn't run at that time. It may not run at all, if you never click. When you do click a wedge, its event handler is called at that time, long after the original loop has finished running.
So, what is the value of i when the event handler does run? It's whatever value the code left in it when it ran originally. This for loop exits when i equals vangle.length—so in other words, i is past the end of the array, and therefore vangle[i] is undefined.
You can fix this easily with a closure, by simply calling a function for each loop iteration:
var layer = new Kinetic.Layer();
function circleCreate(vangle, vradius, vcolor, vtext) {
startAngle = 0;
endAngle = 0;
for (var i = 0; i < vangle.length; i++) {
addWedge( i );
}
stage.add(layer);
function addWedge( i ) {
startAngle = endAngle;
endAngle = startAngle + vangle[i];
var wedge = new Kinetic.Wedge({
x: stage.getWidth() / 2,
y: stage.getHeight() / 2,
radius: vradius,
angleDeg: vangle[i],
fill: vcolor,
stroke: 'black',
strokeWidth: 1,
rotationDeg: startAngle
});
wedge.on('click', function() {
alert(vtext[i]);
});
layer.add(wedge);
}
}
What happens now is that calling the addWedge() function captures the value of i individually for each loop iteration. As you know, every function can have its own local variables/parameters, and the i inside addWedge() is local to that function—and specifically, local to each individual invocation of that function. (Note that because addWedge() is a function of its own, the i inside that function is not the same as the i in the outer circleCreate() function. If this is confusing, it's fine to give it a different name.)
Updated fiddle
A better way
This said, I recommend a different approach to structuring your data. As I was reading your code, the angle and text arrays caught my eye:
var anglesParents = [120, 120, 120];
var parentTextArray = ['Parent1', 'Parent2', 'Parent3'];
There are similar but lengthier pairs of arrays for children and grandchildren.
You use the values from these arrays with the vtext[i] and vangle[i] references in circleCreate().
In general, unless there's a specific reason to use parallel arrays like this, your code will become cleaner if you combine them into a single array of objects:
[
{ angle: 120, text: 'Parent1' },
{ angle: 120, text: 'Parent2' },
{ angle: 120, text: 'Parent3' }
]
For your nested cirles, we can take this a step further and combine all three rings into a single large array of objects that describes the entire set of nested rings. Where you have these arrays:
var anglesParents = [120, 120, 120];
var anglesChildren = [120, 60, 60, 60, 60];
var anglesGrandchildren = [
33.33, 20, 23.33, 43.33, 22.10, 25.26,
12.63, 28, 32, 33, 27, 36, 14.4, 9.6
];
var grandchildrenTextArray = [
'GrandCHild1', 'GrandCHild2', 'GrandCHild3', 'GrandCHild4',
'GrandCHild5', 'GrandCHild6', 'GrandCHild7', 'GrandCHild8',
'GrandCHild9', 'GrandCHild10', 'GrandCHild11', 'GrandCHild12',
'GrandCHild13', 'GrandCHild14', 'GrandCHild15', 'GrandCHild16'
];
var childrenTextArray = [
'Child1', 'Child2', 'Child3', 'Child4', 'Child5'
];
var parentTextArray = ['Parent1', 'Parent2', 'Parent3'];
It would be:
var rings = [
{
radius: 200,
color: 'grey',
slices: [
{ angle: 33.33, text: 'GrandChild1' },
{ angle: 20, text: 'GrandChild2' },
{ angle: 23.33, text: 'GrandChild3' },
{ angle: 43.33, text: 'GrandChild4' },
{ angle: 22.10, text: 'GrandChild5' },
{ angle: 25.26, text: 'GrandChild6' },
{ angle: 12.63, text: 'GrandChild7' },
{ angle: 28, text: 'GrandChild8' },
{ angle: 32, text: 'GrandChild9' },
{ angle: 33, text: 'GrandChild10' },
{ angle: 27, text: 'GrandChild10' },
{ angle: 36, text: 'GrandChild12' },
{ angle: 14.4, text: 'GrandChild13' },
{ angle: 9.6, text: 'GrandChild14' }
]
},
{
radius: 150,
color: 'darkgrey',
slices: [
{ angle: 120, text: 'Child1' },
{ angle: 60, text: 'Child2' },
{ angle: 60, text: 'Child3' },
{ angle: 60, text: 'Child4' },
{ angle: 60, text: 'Child5' }
]
},
{
radius: 100,
color: 'lightgrey',
slices: [
{ angle: 120, text: 'Parent1' },
{ angle: 120, text: 'Parent2' },
{ angle: 120, text: 'Parent3' }
]
}
];
Now this is longer than the original, what with all the angle: and text: property names, but that stuff compresses out very nicely with the gzip compression that servers and browsers use.
More importantly, it helps simplify and clarify the code and avoid errors. Did you happen to notice that your anglesGrandchildren and grandchildrenTextArray are not the same length? :-)
Using a single array of objects instead of parallel arrays prevents an error like that.
To use this data, remove the circleCreate() function and these calls to it:
circleCreate(anglesGrandchildren, 200, "grey", grandchildrenTextArray);
circleCreate(anglesChildren, 150, "darkgrey", childrenTextArray);
circleCreate(anglesParents, 100, "lightgrey", parentTextArray);
and replace them with:
function createRings( rings ) {
var startAngle = 0, endAngle = 0,
x = stage.getWidth() / 2,
y = stage.getHeight() / 2;
rings.forEach( function( ring ) {
ring.slices.forEach( function( slice ) {
startAngle = endAngle;
endAngle = startAngle + slice.angle;
var wedge = new Kinetic.Wedge({
x: x,
y: y,
radius: ring.radius,
angleDeg: slice.angle,
fill: ring.color,
stroke: 'black',
strokeWidth: 1,
rotationDeg: startAngle
});
wedge.on('click', function() {
alert(slice.text);
});
layer.add(wedge);
});
});
stage.add(layer);
}
createRings( rings );
Now this code isn't really any shorter than the original, but some of the details are more clear: slice.angle and slice.text show clearly that the angle and text belong to the same slice object, where with the original vangle[i] and vtext[i] we're left hoping that the vangle and vtext arrays are the correct matching arrays and are properly lined up with each other.
I also used .forEach() instead of a for loop; since you're using Canvas we know you are on a modern browser. One nice thing is that forEach() uses a function call, so it automatically gives you a closure.
Also, I moved the calculations of x and y outside the loop since they are the same for every wedge.
Here's the latest fiddle with this updated code and data.
because each anonymous function you define as an event handler with each loop iteration will share the same scope, each function will reference the same var (i) as the array address for the text you are trying to display. Because your are redefining the var i with each loop, you will always see the last text message in your message array displayed on each click event because the last value assigned to i will have been the length of your array.
here is the solution:
var layer = new Kinetic.Layer();
function circleCreate(vangle, vradius, vcolor, vtext) {
startAngle = 0;
endAngle = 0;
for (var i = 0; i < vangle.length; i++) {
// WEDGE
startAngle = endAngle;
endAngle = startAngle + vangle[i];
var wedge = new Kinetic.Wedge({
x: stage.getWidth() / 2,
y: stage.getHeight() / 2,
radius: vradius,
angleDeg: vangle[i],
fill: vcolor,
stroke: 'black',
strokeWidth: 1,
rotationDeg: startAngle
});
(function(index) {
wedge.on('click', function() {
alert(vtext[i]);
});
})(i)
layer.add(wedge);
}
stage.add(layer);
}
Your problem is with your loop index. Try this:
(function(j) {
wedge.on('click', function() {
alert(vtext[j]);
});
})(i);
See here
The problem is that when your click handler gets called, i has the value that it had at the end of your loop, so vtext[i] is obviously undefined. By wrapping it in a closure, you can save the value of the loop index at the time the loop ran for your click handler.
Related
Countdown animation with EaselJs
I am trying to simulate a countdown animation using easeljs. I have got an example of what it should look like. http://jsfiddle.net/eguneys/AeK28/ But it looks like a hack, is there a proper/better/flexible way to do this? To put it other way, how can i define a path, and draw that path with easeljs. This looks ugly: createjs.Tween.get(bar, {override: true}).to({x: 400}, 1500, createjs.linear) .call(function() { createjs.Tween.get(bar, {override: true}).to({y: 400}, 1500, createjs.linear) .call(function() { createjs.Tween.get(bar, {override: true}).to({ x: 10 }, 1500, createjs.linear) .call(function() { createjs.Tween.get(bar, {override: true}).to({ y: 10 }, 1500, createjs.linear); }) }); });
You can use the TweenJS MotionGuidePlugin to tween along a path instead of having multiple tweens. createjs.MotionGuidePlugin.install(); createjs.Tween.get(bar).to({ guide: {path: [10,10, 10,10,400,10, 400,10,400,400, 400,400,10,400, 10,400,10,10]} }, 6000, createjs.linear); The path array is basically the set of coordinates for a moveTo call followed by multiple curveTo calls. The coordinates will be interpolated along the path resulting from those calls. A more modular way to to specify your path array would be to have a function generating it using a set of points you declared. function getMotionPathFromPoints (points) { var i, motionPath; for (i = 0, motionPath = []; i < points.length; ++i) { if (i === 0) { motionPath.push(points[i].x, points[i].y); } else { motionPath.push(points[i - 1].x, points[i - 1].y, points[i].x, points[i].y); } } return motionPath; } var points = [ new createjs.Point(10, 10), new createjs.Point(400, 10), new createjs.Point(400, 400), new createjs.Point(10, 400), new createjs.Point(10, 10) ]; createjs.MotionGuidePlugin.install(); createjs.Tween.get(bar).to({ guide: {path: getMotionPathFromPoints(points)} }, 6000, createjs.linear); FIDDLE
How can I make a Kinetic.Line fire an event like the other Kinetic shapes?
You can interact with this code here on jsFiddle In the fiddle you can see that I have made a flag (Kinetic.Rect) on a flagpole (Kinetic.Line). I desire to fire an event when the user moves the mouse over any portion of the flag or flagpole. In prior attempts I have attached event handlers to each shape individually, only to learn that Kinetic.Line does not fire events. In this latest attempt I added the shapes to a group and attached the handler to the group thinking this would solve the issue: it does not. Is there any way to achieve the desired behavior? Thank you, and remember to press F12 to see the handler's console messages. var handler = function(e) { console.log("Event fired."); }; var stage = new Kinetic.Stage({ container: 'testBlock', width: 200, height: 200 }); var layer = new Kinetic.Layer(); var group = new Kinetic.Group(); var rect = new Kinetic.Rect({ x: 75, y: 10, width: 50, height: 50, fill: 'silver', }); line = new Kinetic.Line({ points: [ {x: 125, y: 10}, {x: 125, y: 160}, ], stroke: 'black', strokeWidth: 1 }); // add the shapes to the group group.add(rect); group.add(line); // event handler for the group group.on("mouseover", handler); // add the group to the layer layer.add(group); // add the layer to the stage stage.add(layer);
Kinetic.Line's have trouble with events when the stroke is too small, you can see this evident with any line with stroke < 3px. This was the response I got from Eric Rowell (creator of KineticJS): yep, KineticJS ignores the anti-aliased pixels. If you're drawing a 1px diagonal line, and you want it to be detectable, you need to create a custom hit function to define the hit region. You probably will want to create a hit region that's a line which is about 5px thick or so. Here's an example on creating custom hit regions: http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-custom-hit-function-tutorial/ So in addition to Ani's answer, you can also use the drawHitFunc property to make a custom hit region for the line that is thicker than the actual line: line = new Kinetic.Line({ points: [ {x: 125, y: 10}, {x: 125, y: 160}, ], stroke: 'black', strokeWidth: 1, drawHitFunc: function(canvas) { var x1=this.getPoints()[0].x; var x2=this.getPoints()[1].x; var y1=this.getPoints()[0].y; var y2=this.getPoints()[1].y; var ctx = canvas.getContext(); ctx.beginPath(); ctx.lineWidth = 1; ctx.moveTo(x1-3,y1-3); ctx.lineTo(x1-3,y2+3); ctx.lineTo(x2+3,y2+3); ctx.lineTo(x2+3,y1-3); ctx.closePath(); canvas.fillStroke(this); } }); jsfiddle
Try this fiddle I'm using Kinetic.Rect with width=1 and height= y2-y1 of your line. line = new Kinetic.Rect({ x: 125, y: 10, width: 1, height: 150, fill: 'black', });
Kineticjs - Fixed fill pattern of shape being rotated
I use kineticjs to work with shapes and transitions. For now I have made next code example: http://jsfiddle.net/z6LaH/2/ hexagon = new Kinetic.RegularPolygon({ x: stage.getWidth() / 2, y: stage.getHeight() / 2, sides: 6, radius: hexRadius, cornerRadius: 0, fillPatternImage: img, fillPatternOffset: [150, -150], //fill: 'white', stroke: 'black', strokeWidth: 0 }); hexagon.on('mouseover touchstart', function() { this.transitionTo({ cornerRadius: transRadius, rotation: Math.PI / 2, scale: {x: 0.75, y: 0.75}, easing: 'ease-in', duration: duration, callback: function() { hexagon.transitionTo({ scale: {x: 1.1, y: 1.1}, duration: duration * 7, easing: 'elastic-ease-out' }); } }); }); As you can see, fill pattern is rotating with shape. I need it to be fixed. So my question is: Is it posible to make fixed fill pattern, while shape is rotating, and how? UPDATE: I got next approach: rotate fill pattern in opposite direction. http://jsfiddle.net/z6LaH/3/ Is there any more elegant way to do the same?
Eric has just added the ability to save a user-defined clipping function to layers and groups. First, you define a function that draws a clipping region on a layer or group var layer = new Kinetic.Layer({ clipFunc: function(canvas) { var context = canvas.getContext(); context.rect(0, 0, 400, 100); } }); Then you call the .clip() function to apply the clip. Here is Kinetic’s clip() function in the source code: _clip: function(container) { var context = this.getContext(); context.save(); this._applyAncestorTransforms(container); context.beginPath(); container.getClipFunc()(this); context.clip(); context.setTransform(1, 0, 0, 1, 0, 0); } The clip() function applies existing transforms before doing the clip. If you don’t like the transform part of the Kinetic function, you can always use “container.getClipFunc()” and then build your own myClipWithoutTransform() based on the _clip function above.
kinetic js canvas adding event listener to multiple points within for loop
Say i have the following code the event only seems to be firing every now and again take the two following examples. First example this works great and the event fires everytime as expected for (var i = 0; i < items; i++) { var rect = new Kinetic.Rect({ x: 0, y: 1+i, width: 100, height: 2, fill: 'green' }); rect.on('mouseover', function(evt) { $('#console').text(evt.shape.index); }); layer.add(rect); } view in action http://jsfiddle.net/6aTNn/5/ Here is my issue when adding a rotate value on the event doesn't seem to be firing correctly. for (var i = 0; i < items; i++) { var rect = new Kinetic.Rect({ x: stage.getWidth() / 2, y: stage.getHeight() / 2, width: 100, height: 1, fill: 'green' }); rect.on('mouseover', function(evt) { console.log(evt.shape.index); $('#console').text(evt.shape.index); }); rect.rotate(i * angularSpeed / items); // add the shape to the layer layer.add(rect); } view in action http://jsfiddle.net/6aTNn/8/ Any help on this would be great been at this for hours and cant find a solution that works?
The nodes are too 'thin' and too overlapped for the event to fire properly. Try increasing the height of each node and adding fewer of them. For example, replace the i++ with i += 3 and increase the height to 2. for (var i = 0; i < 800; i += 3) { var rect = new Kinetic.Rect({ x: stage.getWidth() / 2, y: stage.getHeight() / 2, width: 100, height: 2, fill: 'green' }); // ... }
Dynamically draw marker on last point in highcharts
I want to draw a marker on the last point. Data source is dynamic. Have a look at following code $(function() { $("#btn").click(function() { var l = chart.series[0].points.length; var p = chart.series[0].points[l - 1]; p.marker = { symbol: 'square', fillColor: "#A0F", lineColor: "A0F0", radius: 5 }; a = 1; chart.series[0].points[l - 1] = p; chart.redraw(false); }); var ix = 13; var a = 0; var chart = new Highcharts.Chart({ chart: { renderTo: 'container', events: { load: function() { var series = this.series[0]; setInterval(function() { ix++; var vv = 500 + Math.round(Math.random() * 40); chart.series[0].data[0].remove(); var v; if (a == 1) v = { y: vv, x: ix, marker: { symbol: 'square', fillColor: "#A0F", lineColor: "A0F0", radius: 5 } } else v = { y: vv, x: ix } a = 0; series.addPoint(v); }, 1500); } } }, plotOptions: { series: {} }, series: [{ data: [500, 510, 540, 537, 510, 540, 537, 500, 510, 540, 537, 510, 540, 537]}] }); }); http://jsfiddle.net/9zNUP/ On button click event I am trying to draw marker on last point which is already added to chart. Is there a way to do that??
$("#btn").click(function() { var l = chart.series[0].points.length; var p = chart.series[0].points[l - 1]; p.update({ marker: { symbol: 'square', fillColor: "#A0F", lineColor: "A0F0", radius: 5 } }); a = 1; }); solution # http://jsfiddle.net/jugal/zJZSx/ Also tidied up your code a little, removed the removal of point before adding one at the end, highcharts supports it inbuilt with the third param to addPoint as true, which denotes shift series, which removes first point and then adds the given point. I didn't really understand what the a vv etc were, but well i didn't bother much either. I think this is enough based on what you asked for.