Leaflet: Fading in/out layer groups efficiently - javascript

I've got a Leaflet map with a single tile layer and then a LayerGroup (densityLayer) consisting of many (typically a few hundred) Rectangle layers, each of which is a semitransparent filled overlay with its fillColor based on population density for a particular year.
Based on a user action, the contents of densityLayer change. Pretty trivial to just run densityLayer.clearLayers() then generate all the new Rectangle layers and densityLayer.addLayer(aRectangle) for each of them.
What I want to do, though, is to animate a fade from the old to the new data: i.e., generate all the new Rectangle layers and put them in a new LayerGroup (newDensityLayer), and simultaneously fade out the original oldDensityLayer and fade in the newDensityLayer, and when the fade is complete, then clear out and remove oldDensityLayer and replace it with newDensityLayer.
My current solution is hideously inefficient:
var oldDensityLayer = densityLayer
var newDensityLayer = {...create new density layer here, add polygons, etc...}
oldDensityLayer.eachLayer(function(l) {
$(l._path).fadeOut(1000) // 1000ms animation time
})
setTimeout(function() {
oldDensityLayer.clearLayers()
myLeafletMap.removeLayer(oldDensityLayer)
oldDensityLayer = null
}, 1000)
myLeafletMap.addLayer(newDensityLayer)
// now fade in all the new polygons
newDensityLayer.eachLayer(function(l) {
$(l._path).hide() // so they start out invisible
$(l._path).fadeIn(1000)
})
densityLayer = newDensityLayer
This basically works, but gets pretty choppy and slow on anything but a very fast machine.
Is there some way to fade in/out an entire LayerGroup, or perhaps some option I haven't considered...?
This is critical functionality, so if adding another js library would help, that's fine. Also, SVG-specific answers are fine, as that's what I've got Leaflet using for its drawing functions, and cross-browser compatibility isn't a concern in this application.

Is there some way to fade in/out an entire LayerGroup, or perhaps some option I haven't considered...?
There is, in fact, an option which you haven't considered: manipulate the L.Renderer which actually draws the geometries as an HTML element. This means manipulating the actual <canvas> of a L.Canvas, or the actual <svg> of a L.SVG.
Remember any subclass of L.Path (Polygons, Polylines and such) can have its own renderer. Leaflet, by default, creates just one instance of L.Renderer and reuses it in all L.Paths unless told otherwise - this means less HTML elements and (in 99% of use cases) better performance.
So it should look something like:
var rendererA = L.canvas();
var rendererB = L.canvas();
var groupA = L.layerGroup().addTo(map);
var layerA1 = L.polygon(…, {renderer: rendererA}).addTo(groupA);
var layerA2 = L.polygon(…, {renderer: rendererA}).addTo(groupA);
var groupB = L.layerGroup().addTo(map);
var layerB1 = L.polygon(…, {renderer: rendererB}).addTo(groupB);
var layerB2 = L.polygon(…, {renderer: rendererB}).addTo(groupB);
// Here comes the magic - using the *undocumented*, *private* _container
// property of L.Canvas to access the <canvas> HTML element itself
rendererA._container.style.opacity = 0.5;
The code is obviously incomplete, but it should illustrate the idea properly.
That will create two different <canvas> in the browser, and changing the opacity of the HTML element itself will bypass re-rendering the features. There should be an analogous solution using L.SVG instead, but I'm not sure how browsers composite the opacity of SVG containers.
There are obvious disadvantages for this method - like losing any z-ordering (bringToFront, etc) if geometries from both groups should be intertwined.
Also, please remember: Using undocumented, private properties of leaflet objects is not recommended unless you really really really know what you're doing and are willing to see your code break in API changes or rare circumstances.

You could include an SVG layer populated with your shapes. Below in an example that fades out 500 svg shapes, then builds a new bunch of SVG shapes and fades them in. (Edited to zoom shapes at their locations)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Fade Out/In SVG Elements in Leaflet World Map</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src='https://api.tiles.mapbox.com/mapbox.js/v2.1.5/mapbox.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox.js/v2.1.5/mapbox.css' rel='stylesheet' />
</head>
<body style='font-family:arial'>
<center><h4>Fade Out/In SVG Elements in Leaflet World Map</h4>
<div style='width:90%;background-color:gainsboro;text-align:justify;padding:10px;border-radius:6px;'>
This adds 500 svg elements(circles, ellipses, rects, polygons) to the SVG layer in the world map. The map's mouse wheel zoom remains smooth in IE/CH/FF. Each element has a lat/lng value, converted to the needed x,y values to translate each symbol to the desired point.
During map zoom level changes, the symbols are automatically scaled and translated, maintaining their original size and position.
</div>
<br />
<table border=1>
<tr>
<td>
<b>Scenerio:</b><br />
1). The map is placed into its DIV (width:800px, height:400px).<br />
2). The map is centered at Lat/Lng (0,0) at zoom level 1.<br />
3.) The SVG element is added via <b>initPathRoot.</b><br />
4.) 500 SVG elements are added, randomly place about the world.<br />
5.) The svg <b>viewBox</b> is computed, used to create x,y values for the symbols.<br />
6.)Each element is translated/scaled when the map is zoomed, using the <b>viewreset</b> event.<br /> This calls the map method <b>latLngToLayerPoint(MyLatLng)</b> to accomplish this.
<br>7.) Select <button>fade out/in</button> to fade out the current elements, build a new group, then fade In new group
</td>
</tr>
</table>
<div style='width:800px;height:400px' id='MyMap'></div>
<button onClick=fadeOutIn()>fade out/in</button>
<br />Javascript:<br />
<textarea spellcheck=false id=jsValue style='border-radius:26px;font-size:110%;font-weight:bold;color:midnightblue;padding:16px;background-color:beige;border-width:0px;font-size:100%;font-family:lucida console;width:90%;height:400px'></textarea>
</center>
<script id=myScript>
L.mapbox.accessToken = 'pk.eyJ1IjoiZmhlbXNoZXIiLCJhIjoiODQ5MW9WayJ9.px2P6wVMFucfXHE1zmDA1A';
MyMap = L.mapbox.map('MyMap', 'mapbox.streets', { zoomControl:false,center: new L.latLng(0,0),zoom:1,minZoom:1});
//---zooming the map---
MyMap.on("viewreset", adjustSVGSymbols);
var MySVG
var SymbolG //---<g> element containing all symbols---
var VBw
var VBh
var NS="http://www.w3.org/2000/svg"
//---body onload---
function initSVG()
{
MyMap._initPathRoot() //---creates an svg layer---
MySVG=document.querySelector("svg") //---access svg element---
//---place symbols in here---
SymbolG=document.createElementNS(NS,"g")
SymbolG.setAttribute("id","symbolG")
MySVG.appendChild(SymbolG)
//---create random svg elements, place in SymbolG--
getViewBox()//---used to place svg random elements
//---create 500 symbols at size 10 pixels--
svgGLOB(500,10)
}
//--- on map zoom - fired via map event: viewreset---
function adjustSVGSymbols()
{
var symbols=SymbolG.childNodes
for(var k=0;k<symbols.length;k++)
{
var symbol=symbols.item(k)
//---initial lat/lng for symbol---
var lat=parseFloat(symbol.getAttribute("lat"))
var lng=parseFloat(symbol.getAttribute("lng"))
var latLng= new L.latLng(lat, lng)
var transX=MyMap.latLngToLayerPoint(latLng).x
var transY=MyMap.latLngToLayerPoint(latLng).y
//---scale---
var initZoom=parseFloat(symbol.getAttribute("initZoom"))
var scale = (Math.pow(2, MyMap.getZoom())/2)/(Math.pow(2, initZoom)/2);
//---trash previous transform---
symbol.setAttribute("transform","")
symbol.removeAttribute("transform")
var transformRequestObj=MySVG.createSVGTransform()
var animTransformList=symbol.transform
//---get baseVal to access/place object transforms
var transformList=animTransformList.baseVal
//---translate----
transformRequestObj.setTranslate(transX,transY)
transformList.appendItem(transformRequestObj)
transformList.consolidate()
//---scale---
transformRequestObj.setScale(scale,scale)
transformList.appendItem(transformRequestObj)
transformList.consolidate()
}
}
//---needed for random symbol placement: create x,y values---
function getViewBox()
{
vb=MySVG.viewBox.baseVal
VBw=vb.width
VBh=vb.height
}
//---compute svg elems: circles, rects, ellipses, polygons---
function svgGLOB(elems,elemSize)
{
//---note: each browser creates a different sized svg layer---
var svgWidth=VBw
var svgHeight=VBh
//---obtain a random whole number from a thru b---
function rdm(a,b)
{
return a + Math.floor(Math.random()*(b-a+1));
}
function randomPoints(elems,svgWidth,svgHeight,elemSize)
{
//--return format:[ [x,y],[x,y],,, ]
//---Generate random points---
function times(n, fn)
{
var a = [], i;
for (i = 0; i < n; i++)
{
a.push(fn(i));
}
return a;
}
var width=svgWidth-2*elemSize //---offset from edge---
var height=svgHeight-2*elemSize //---offset from edge---
return RandomPnts = times(elems, function() { return [Math.floor(width * Math.random()) + elemSize, Math.floor(height * Math.random()) + elemSize] });
}
//---random color---
function rcolor()
{
var letters = '0123456789ABCDEF'.split('');
var color = '#';
for (var i = 0; i < 6; i++ )
{
color += letters[Math.round(Math.random() * 15)];
}
return color;
}
function polygon(vCnt,radius,centerX,centerY)
{
var myPoints=[]
var polyXPts = Array(vCnt);
var polyYPts = Array(vCnt);
var vertexAngle = 360/vCnt;
//---init polygon points processor---
for(var v=0; v<vCnt; v++)
{
theAngle = (v*vertexAngle)*Math.PI/180;
polyXPts[v] = radius*Math.cos(theAngle);
polyYPts[v] = -radius*Math.sin(theAngle);
}
//--note points are CCW---
for(var v=0;v<vCnt; v++)
{
var point=[centerX+polyXPts[v],centerY+polyYPts[v]]
myPoints.push(point)
}
return myPoints
}
var Points=randomPoints(elems,svgWidth,svgHeight,elemSize)
var n=Points.length
var circleCnt=0
var ellipseCnt=0
var rectCnt=0
var polygonCnt=0
var RandomElems=[]
RandomElems[0]="circle"
RandomElems[1]="rect"
RandomElems[2]="ellipse"
RandomElems[3]="polygon_3"
RandomElems[4]="polygon_4"
RandomElems[5]="polygon_5"
RandomElems[6]="polygon_6"
RandomElems[7]="polygon_7"
RandomElems[8]="polygon_8"
RandomElems[9]="polygon_9"
RandomElems[10]="polygon_10"
RandomElems[11]="polygon_11"
RandomElems[12]="polygon_12"
//---create all at center(0,0), then translate---
for(var k=0;k<n;k++)
{
var rand=rdm(0,12)
var elemStr=RandomElems[rand]
if(!elemStr.indexOf("_"))
var elemSt=elemStr
else
var elemSt=elemStr.split("_")[0]
//var elem=document.createElementNS(NS,elemSt)
var x=Points[k][0]
var y=Points[k][1]
var lng=((x * 360 / VBw) - 180)
var lat= (90 - (y * 180 / VBh))
var id="symbol"+k
var fill=rcolor()
var elem=document.createElementNS(NS,elemSt)
elem.setAttribute("id",id)
elem.setAttribute("cursor","default")
elem.setAttribute("fill",fill)
elem.setAttribute("lat",lat)
elem.setAttribute("lng",lng)
if(elemSt=="circle")
{
var r=elemSize
elem.setAttribute("r",r)
}
else if(elemSt=="ellipse")
{
var rx=elemSize
var ry=elemSize/2
elem.setAttribute("rx",rx)
elem.setAttribute("ry",ry)
}
else if(elemSt=="rect")
{
var width=elemSize
var height=elemSize
//---center at 0,0---
var x=-elemSize/2
var y=-elemSize/2
elem.setAttribute("width",width)
elem.setAttribute("height",height)
elem.setAttribute("x",x)
elem.setAttribute("y",y)
}
else if(elemSt=="polygon")
{
var pgonSides=parseInt(elemStr.split("_")[1])
var pgonPnts=polygon(pgonSides,elemSize,0,0)
var points=pgonPnts.join()
elem.setAttribute("points",points)
}
elem.setAttribute("initZoom",1)
SymbolG.appendChild(elem)
}
//---initialize locations---
adjustSVGSymbols()
}
//---fade out/In button----
function fadeOutIn()
{
$("#symbolG").fadeOut(1000, function(){
createAnotherGlob()
});
}
function createAnotherGlob()
{
var symbols=SymbolG.childNodes
for(var k=symbols.length-1;k>=0;k--)
SymbolG.removeChild(symbols.item(k))
svgGLOB(500,10,true)
$("#symbolG").fadeIn(1500)
}
</script>
<script>
document.addEventListener("onload",init(),false)
function init()
{
jsValue.value=myScript.text
initSVG()
}
</script>
</body>
</html>

Related

Layering mesh behind panorama

I would like to know if we can have an equirectangular panorama on the foreground and a mesh in the background so when users move across the panorama, the mouse would be able to pickup the plane information from behind.
I have tried loading a panorama using the below code but I couldn't find any way to layer a mesh in the background.
var sphereBufferGeometry = new THREE.SphereGeometry(500,60,40);
sphereBufferGeometry.scale( -1, 1, 1 );
var material = new THREE.MeshBasicMaterial( {
map: new THREE.TextureLoader().load( [panorama image])});
var mesh = new THREE.Mesh( sphereBufferGeometry, material );
scene.add( mesh );
I have also tried loading the mesh(.obj) using the below code but the variable 'object' is of type THREE.Group and the variable 'child' returns multiple objects of type THREE.MESH. So, I am not sure how we can layer a panorama in the foreground.
objLoader.load( [objfile], function ( object ) {
object.traverse( function ( child ) {
});
});
Any help would be appreciated.
Thanks
Put the image on the page and add an image map to it. Do whatever you want with any cell that fires an event. For proof-of-concept, I simply logged the coordinates - top-left-corner ordered pair and bottom-right-corner ordered pair - for cells firing click events.
You could replace click with mouseover/-out calls. If you reduce the cell heights and width to 1px, you could track exact position as the mouse crosses the image...WARNING - lots of event-handling there.
<html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<script type="text/javascript"">
// convenience reference
function get(eId) {return document.getElementById(eId);};
// add area elements to an existing image map
function paintMapGrid(imageId, gridId, cellHeight, cellWidth) {
var imgHeight = get(imageId).height;
var imgWidth = get(imageId).width;
var lastJ = Math.floor(imgWidth/cellWidth);
var lastI = Math.floor(imgHeight/cellHeight);
var grid = '';
for (var i = 0; i < lastI; i++) {
for (var j = 0; j < lastJ; j++) {
var coords= (cellWidth*j) + ',' + (cellHeight*i) + ',' +(cellWidth*(j+1) + ',' + (cellHeight*(i + 1)));
grid += '<area onclick="logCellCoords(this);" shape="rect" coords="' + coords + '">';
}
}
get('mesh').innerHTML = grid;
};
function logCellCoords(cRef) {
console.log(cRef.coords);
};
window.onload = function() {
// paint an image map on the image with id='meshImage', adding the
// area elements to the map with id="mesh"; cells are 16px wide
// and 16px tall
paintMapGrid('meshImage', 'mesh', 16, 16);
};
</script>
</head>
<body>
<div>
<img src="yourPanoramicImage.jpg" alt="some 1024x668 panorama"
width="1024" height="668" usemap="#grid" id="meshImage"
/>
<map name="grid" id="mesh"></map>
</div>
</body>
</html>
Afterthoughts...prob'ly less resource intensive to create each area element individually in the inner for loop and append it to its map parent rather than creating the entire HTML string for all and adding it all at once. Also, not sure how tolerant browsers would be for 1px areas, e.g., 1024x668 would need 684,032 area tags. My POC required 64x41 = 2624 area tags.
I was able to find a solution. The idea is to have both spheregeometry and mesh in the same scene and make any initial position/rotation/scale changes to the mesh so that it aligns with the panorama. Once this is the done, any changes to the camera would affect both the objects in exactly same way giving the user an impression that these two objects are always aligned.

Draw on top of an image using javascript

For a certain image I have a list containing the pixel coordinates of all the points in a polygon segmenting all the objects it contains (look at the image below).
For instance, for the person I have a list l1 = [x0,y0,x1,y1,...,xn,yn], for the cat a list l2 = [x0',y0',x1',y1',...,xk',yk'], and similarly for all the objects.
I have 2 questions:
What is the best javascript library to use to draw on top of an image? Given the raw image I would like to obtain the result seen below.
I would like each segmentation to be visualized only when the mouse hovers on top of it. For this I believe I should bind this drawing function to the mouse position.
I'm thinking at something with the structure below but don't know how to fill the gaps, could you please give me some indication?
$(.container).hover( function(e) {
//get coordinates of mouse
//if mouse is over one object
//draw on top of image the segmentation for that object
});
container is the class of the div containing the image so I should be able to get the coordinates of the mouse since the image starts at the top left corner of the container div.
Simply rebuild the polygons from each array and do a hit test using the mouse position.
First: If you have many arrays defining the shapes it could be smarter to approach it in a more general way instead of using variables for each array as this can soon be hard to maintain. Better yet, an object holding the array and for example id could be better.
Using an object you could do - example:
function Shape(id, points, color) {
this.id = id;
this.points = points;
this.color = color;
}
// this will build the path for this shape and do hit-testing:
Shape.prototype.hitTest = function(ctx, x, y) {
ctx.beginPath();
// start point
ctx.moveTo(this.points[0], this.points[1]);
// build path
for(var i = 2, l = this.points.length; i < l; i += 2) {
ctx.lineTo(this.points[i], this.points[i+1]);
}
ctx.closePath();
return ctx.isPointInPath(x, y);
};
Now you can create new instances with the various point arrays like this:
var shapes = [];
shapes.push(new Shape("Cat", [x0,y0,x1,y1, ...], "rgba(255,0,0,0.5)");
shapes.push(new Shape("Woman", [x0,y0,x1,y1, ...], "rgba(0,255,0,0.5)"));
...
When you get a mouse position simply hit-test each shape:
$(".container").hover( function(e) {
//get corrected coordinates of mouse to x/y
// redraw canvas without shapes highlighted
for(var i = 0, shape; shape = shapes[i]; i++) { // get a shape from array
if (shape.hitTest(ctx, x, y)) { // is x/y inside shape?
ctx.fillStyle = shape.color; // we already have a path
ctx.fill(); // when testing so just fill
// other tasks here...
break;
}
}
});
check this Link it might be slow your problem.
Include necessary javascript library files
jquery.min.js,raphael.min.js,json2.min.js,raphael.sketchpad.js
To create an editor
<div id="editor"></div>
<form action="save.php" method="post">
<input type="hidden" name="data" />
<input type="submit" value="Save" />
</form>
<script type="text/javascript">
var sketchpad = Raphael.sketchpad("editor", {
width: 400,
height: 400,
editing: true
});
// When the sketchpad changes, update the input field.
sketchpad.change(function() {
$("#data").val(sketchpad.json());
});
</script>

Drawing a timeline from div to div with canvas

I have the following div markup
<div data-x="-1000" data-y="-1500">
// content
</div>
<div data-x="0" data-y="-1500">
// content
</div>
And I have many of this divs with different data-x and data-y value depending on their position.
What I want to achieve here is to draw something like a timeline between the divs so div1 line to div2 line to div 3 etc,,
I want this to be done automatically So I am trying to make a loop for it but my javascript/jquery knowledge is not that good. Could someone point me in the good direction?
what I have now is
function drawTimeline() {
var divs = document.getElementsByTagName('div');
var canvas = document.getElementById('timeline');
// Make sure we don't execute when canvas isn't supported
if (canvas.getContext){
// use getContext to use the canvas for drawing
var ctx = canvas.getContext('2d');
var prevCoord = {};
for (var i = -1; div = divs[++i]; ) {
if (div.dataset.x && div.dataset.y) {
var x = parseInt(div.dataset.x);
var y = parseInt(div.dataset.y);
if ({} !== prevCoord) {
ctx.beginPath();
ctx.lineWidth="5";
ctx.strokeStyle="purple"; // Purple path
ctx.moveTo(prevCoord.x, prevCoord.y);
ctx.lineTo(x, y);
ctx.closePath();
ctx.stroke()
}
prevCoord.x = x;
prevCoord.y = y;
}
}
} else {
alert('You need Safari or Firefox 1.5+ to see this.');
}
}
unfortunately the line is not correct its like a linear line and thats it.. can someone help me out with this?
You have the right idea, but your problem is that canvas does not draw at negative coordinates.
Therefore, you must map all your data-x and data-y into positive coordinates.
Here's a function that maps your negative values into positive values:
function mapRange(value, sourceLow, sourceHigh, mappedLow, mappedHigh) {
return mappedLow + (mappedHigh - mappedLow) * (value - sourceLow) / (sourceHigh - sourceLow);
}
So in your example to map a data-x value of -300 into an onscreen range of 0-1000 can do this:
var x = mapRange(-300, -1500,0, 0,1000); // The mapped x is 466.66.
You must reposition your divs to the mapped x,y coordinates which you can do with CSS.
An alternative is to use SVG which creates actual DOM elements that can be positioned at negative coordinates.

Diagram creater using KineticJS

I'm working on a simple diagram editor using KineticJS. I would like to use two separate canvases for the palette area (that contains a number of Kinetic.Groups that represent the different nodes of the network I might create), and the diagramming area where I can add nodes from the palette via drag and drop, and then add connections between the various nodes as specific anchor points. I'm having trouble figuring out the drag and drop process from the palette canvas of (stationary) Kinetic.Groups over to the other canvas containing diagramming area. I'm guessing that I need to fire off of the dragstart event for the palette objects (although I don't want these themselves to be draggable), and then do something like create an opague copy of the palette object that can be dragged around, and finally dropped into the diagramming area (with full opacity).
Can groups be dragged outside of a the staging canvases boundaries? Maybe I need to generate an image when I start to drag from the palette, drag that image over, and then create another group when dropping into the diagramming area.
Does any one know of any examples that might point me in the right direction, or who can offer some insight (even code) into the required process. I've searched the KineticJS examples but can't quite find enough to get me going.
Here’s one way to drag nodes from a source palette into a destination group:
A Fiddle: http://jsfiddle.net/m1erickson/xtVyL/
Network nodes are represented by small icons (which are really small kinetic image objects).
The user can drag any icon from the source palette to any destination group.
The groups are just defined areas on the canvas, but could be Kinetc.Groups for more flexibility.
During the dragend event, a new duplicate copy of the dragged icon is created in the destination group.
After the dragend event is complete, the original palette icon is automatically moved from
The newly created duplicate icon can be dragged around the destination group (but not outside that group).
Here is code and a Fiddle: http://jsfiddle.net/m1erickson/xtVyL/
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Prototype</title>
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<script src="http://d3lp1msu2r81bx.cloudfront.net/kjs/js/lib/kinetic-v4.5.5.min.js"></script>
<style>
#container{
border:solid 1px #ccc;
margin-top: 10px;
width:350px;
height:350px;
}
</style>
<script>
$(function(){
var stage = new Kinetic.Stage({
container: 'container',
width: 350,
height: 350
});
var layer = new Kinetic.Layer();
stage.add(layer);
// image loader
var imageURLs=[];
var imagesOK=0;
var imgs=[];
imageURLs.push("https://dl.dropboxusercontent.com/u/139992952/stackoverflow/tempPC.png");
imageURLs.push("https://dl.dropboxusercontent.com/u/139992952/stackoverflow/tempServer.png");
imageURLs.push("https://dl.dropboxusercontent.com/u/139992952/stackoverflow/tempRouter.png");
loadAllImages();
function loadAllImages(callback){
for (var i = 0; i < imageURLs.length; i++) {
var img = new Image();
imgs.push(img);
img.onload = function(){
imagesOK++;
if (imagesOK==imageURLs.length ) {
start();
}
};
img.src = imageURLs[i];
}
}
// top icon positions
var nextIconX=20;
var nextIconY=20;
// define groups
var groups=[];
groups.push({x:0,y:100,w:175,h:250,fill:"skyblue"});
groups.push({x:175,y:100,w:175,h:250,fill:"cornsilk"});
// add boundary info to each group
// draw colored rect to show group area
for(var i=0;i<groups.length;i++){
var g=groups[i];
g.left=g.x;
g.right=g.x+g.w;
g.top=g.y;
g.bottom=g.y+g.h;
var rect=new Kinetic.Rect({
x:g.x,
y:g.y,
width:g.w,
height:g.h,
fill:g.fill,
stroke:"gray"
});
layer.add(rect);
}
// hittest for each group
function groupHit(x,y){
for(var i=0;i<groups.length;i++){
var g=groups[i];
if(x>g.left && x<g.right && y>g.top && y<g.bottom){return(i);}
}
return(-1);
}
function start(){
makePaletteIcon(imgs[0]);
makePaletteIcon(imgs[1]);
makePaletteIcon(imgs[2]);
layer.draw();
}
function makePaletteIcon(img){
// make an icon that stays in the pallette tray
var fixedIcon=newImage(nextIconX,nextIconY,img,false);
layer.add(fixedIcon);
// make an icon that is dragged from the tray to a group
var dragIcon=makeDraggableIcon(nextIconX,nextIconY,img);
layer.add(dragIcon);
// calc the next icon position
nextIconX+=(img.width+20);
}
function makeDraggableIcon(x,y,img){
var i=newImage(x,y,img,true);
//
i.trayX=x;
i.trayY=y;
//
i.setOpacity(0.50);
i.on("dragend",function(){
var x=this.getX();
var y=this.getY();
// if this pallette icon was not dropped in a group
// put the icon back in the tray and return
var hit=groupHit(x,y);
if(hit==-1){
this.setPosition(this.trayX,this.trayY);
return;
}
// add a copy of this icon to the drop group
var component=newImage(x,y,this.getImage(),true);
// set drag limits
var group=groups[hit];
component.maxDragLeft=group.left;
component.maxDragRight=group.right;
component.maxDragTop=group.top;
component.maxDragBottom=group.bottom;
// limit component dragging to inside the assigned group
component.setDragBoundFunc(function(pos) {
var xx=pos.x;
var yy=pos.y;
var w=this.getWidth();
var h=this.getHeight();
if(pos.x<this.maxDragLeft){xx=this.maxDragLeft;}
if(pos.x+w>this.maxDragRight){xx=this.maxDragRight-w;}
if(pos.y<this.maxDragTop){yy=this.maxDragTop;}
if(pos.y+h>this.maxDragBottom){yy=this.maxDragBottom-h;}
return{ x:xx, y:yy };
});
layer.add(component);
// move the dragIcon back into the pallette tray
this.setPosition(this.trayX,this.trayY);
layer.draw();
});
return(i);
}
// make a new Kinetic.Image
function newImage(x,y,img,isDraggable){
var i=new Kinetic.Image({
image:img,
x: x,
y: y,
width: img.width,
height: img.height,
draggable:isDraggable
});
return(i);
}
}); // end $(function(){});
</script>
</head>
<body>
<p>Drag any icon from top into blue or yellow group</p>
<div id="container"></div>
</body>
</html>

Dynamic Canvas DrawImage Function

I've got a problem filling 25 canvas elements automatically in a for loop. They are numbered like so: can01 to can25.
I've tried all I knew to draw different images on the canvas and I have spent a lot of time in searching a few articles which are about this problem but I haven't found any.
This is my working code to fill all canvas elements with the same image:
var imageGrass = new Image();
imageGrass.src = 'recources/imagesBG/grass.jpg';
imageGrass.onload = function() {
for (var i = 1; i < 26; i++)
{
if( i < 10 )
{
var task = "can0" + i + "_ctx.drawImage(imageGrass, 0, 0);";
eval(task);
}
else
{
var task = "can" + i + "_ctx.drawImage(imageGrass, 0, 0);";
eval(task);
}
}
}
But I really don't know how to make the imageGrass.src dynamic. For example, the canvas element no. 5 (can05) in this case shall look like stone texture.
I´m really looking forward to read your ideas. I just don't get it.
Here’s how to impliment Dave’s good idea of using arrays to organize your canvases:
Create an array that will hold references to all your 25 canvases (do the same for 25 contexts)
var canvases=[];
var contexts=[];
Next, fill the array with all your canvases and contexts:
for(var i=0;i<25;i++){
var canvas=document.getElementById("can"+(i<10?"0":""));
var context=canvas.getContext("2d");
canvases[i]=canvas;
contexts[i]=context;
}
If you haven't seen it before: i<10?"0":"" is an inline if/else used here to add a leading zero to your lower-numbered canvases.
Then you can fetch your “can05” canvas like this:
var canvas=canvases[4];
Why 4 and not 5? Arrays are zero based, so canvases[0] holds can01. Therefore array element 4 contains your 5th canvas “can05”.
So you can fetch the drawing context for your “can05” like this:
var context=contexts[4];
As Dave says, “evals are evil” so here’s how to fetch the context for “can05” and draw the stone image on it.
var context=contexts[4];
context.drawImage(stoneImage,0,0);
This stone drawing can be shortened to:
contexts[4].drawImage(stoneImage,0,0);
You can even put this shortened code into a function for easy reuse and modification:
function reImage( canvasIndex, newImage ){
contexts[ canvasIndex ].drawImage( newImage,0,0 );
}
Then you can change the image on any of your canvases by calling the function:
reimage( 4,stoneImage );
That’s it!
The evil-evals have been vanquished (warning: never invite them to your computer again!)
Here is example code and a Fiddle: http://jsfiddle.net/m1erickson/ZuU2e/
This code creates 25 canvases dynamically rather than hard-coding 25 html canvas elements.
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
body{ background-color: ivory; padding:0px; margin:0px;border:0px; }
canvas{vertical-align: top; }
</style>
<script>
$(function(){
var canvases=[];
var contexts=[];
var grass=new Image();
grass.onload=function(){
// the grass is loaded
// now make 25 canvases and fill them with grass
// ALSO !!!
// keep track of them in an array
// so we can use them later!
make25CanvasesFilledWithGrass()
// just a test
// fill canvas#3 with gold
draw(3,"gold");
// fill canvas#14 with red
draw(14,"red");
}
//grass.src="https://dl.dropboxusercontent.com/u/139992952/stackoverflow/grass.jpg";
//grass.src="grass.jpg";
function make25CanvasesFilledWithGrass(){
// get the div that we will fill with 25 canvases
var container=document.getElementById("canvasContainer");
for(var i=0;i<25;i++){
// create a new html canvas element
var canvas=document.createElement("canvas");
// assign the new canvas an id, width and height
canvas.id="can"+(i<10?"0":"")+i;
canvas.width=grass.width;
canvas.height=grass.height;
// get the context for this new canvas
var ctx=canvas.getContext("2d");
// draw the grass image in the new canvas
ctx.drawImage(grass,0,0);
// add this new canvas to the web page
container.appendChild(canvas);
// add this new canvas to the canvases array
canvases[i]=canvas;
// add the context for this new canvas to the contexts array
contexts[i]=ctx;
}
}
// test -- just fill the specified canvas with the specified color
function draw(canvasIndex,newColor){
var canvas=canvases[canvasIndex];
var ctx=contexts[canvasIndex];
ctx.beginPath();
ctx.fillStyle=newColor;
ctx.rect(0,0,canvas.width,canvas.height);
ctx.fill();
}
}); // end $(function(){});
</script>
</head>
<body>
<div id="canvasContainer"></div>
</body>
</html>

Categories