javascript - Rotating a group of objects while keeping their orientation intact -
basically have container object 'children' modified relative parent, , want rotate of objects changing parent's rotation value, while keeping orientation of individual children stable. (as in, rotate whole object) feel i'm not explaining well, here 2 examples. physicsjs: http://wellcaffeinated.net/physicsjs/ (see first example, 0.7 , balls -- notice how when 0 or 7 rotated after collision overall shape of object maintained. same goes example in phaserjs (http://phaser.io/examples/v2/groups/group-transform-rotate) robot. now, see if could, tried duplicate aforementioned physicsjs example own library -- https://jsfiddle.net/khanfused/r4lgl5y9/ (simplified brevity)
art.prototype.modules.display.rectangle.prototype.draw = function() { // initialize variables. var g = art.prototype.modules.display.rectangle.core.graphics, t = this; // execute drawing commands. g.save(); g.translate(t.parent.x ? t.parent.x + t.x : t.x, t.parent.y ? t.parent.y + t.y : t.y); /* point of interest. */ g.rotate(t.parent.rotation ? t.rotation : t.rotation); g.scale(t.scale.x, t.scale.y); g.globalalpha = t.opacity === 'super' ? t.parent.opacity : t.opacity; g.linewidth = t.linewidth === 'super' ? t.parent.linewidth : t.linewidth; g.fillstyle = t.fill === 'super' ? t.parent.fill : t.fill; g.strokestyle = t.stroke === 'super' ? t.parent.stroke : t.stroke; g.beginpath(); g.rect(t.width / -2, t.height / -2, t.width, t.height); g.closepath(); if (t.fill) { g.fill(); } if (t.stroke) { g.stroke(); } g.restore(); return this; };
refer labeled point of interest -- that's rotate canvas. if object has parent, it's rotated parent's value plus object's value -- otherwise, object's value. i've tried different combinations, like...
• parent - object
• object - parent
...and looked through physicsjs , phaser's sources kind of clue in right direction, no avail.
how rotate group not change layout?
nested transform
to transform group of objects surround group transform wish apply members of group , render each member own transform. before each member transformed local transform need save current transform can used next group member. @ end of rendering each group member must restore transform state group above it.
the data structure
group = { origin : { x : 100, y : 100}, rotate : 2, scale : { x : 1, y : 1}, render : function(){ // function draws canvas ctx.strokerect(-50,-50,100,100); }, groups : [ // array of groups { origin : { x : 100, y : 100}, rotate : 2, scale : { x : 1, y : 1}, render : function(){... }// draw groups : [] // have more members }], // objects rendered }
recursive rendering
rendering nested transformations best done via recursion rendergroup function checks sub groups , calls render group. makes easy have complex nested objects minimum of code. tree simple example of recursion terminating condition reaching last node. can go wrong if allow nested group members reference other members within tree. result in javascript blocking page , crash.
function rendergroup(group){ ctx.save(); // important order of transforms correct ctx.translate(group.origin.x, group.origin.y); ctx.scale(group.scale.x, group.scale.y); ctx.rotate(group.rotate); // draw needed if(group.render !== undefined){ group.render(); } // draw each member of group.groups ( var = 0 ; < group.groups.length; ++){ // warning recursive having member of group reference // member within nested group object result in // infinite recursion , computers don't have memory or // speed complete impossible rendergroup(group.groups[i]); // recursive call }; // , restore original transform ctx.restore(); }
that how nest transforms , how w3c has intended render used. never way. killer of frame rate due need use save , restore, because ctx.gettransform support limited (only chrome). can not transform must mirror in code, needless there many optimisations can applied if maintaining matrix. may 1000 sprites in realtime using settransform , little math, doing way on canvas quarters or worse frame rate.
demo
running example safe recursion.
draws nested objects centered on mouse is.
the demo recursive render taken other code have , cut suit demo. extends recursive render allow animation , render order. note scales non uniform there skewing deeper iterations go.
// adapted quickrunjs environment. //=========================================================================== // simple mouse //=========================================================================== var mouse = (function(){ function preventdefault(e) { e.preventdefault(); } var mouse = { x : 0, y : 0, buttonraw : 0, bm : [1, 2, 4, 6, 5, 3], // masks setting , clearing button raw bits; mouseevents : "mousemove,mousedown,mouseup".split(",") }; function mousemove(e) { var t = e.type, m = mouse; m.x = e.offsetx; m.y = e.offsety; if (m.x === undefined) { m.x = e.clientx; m.y = e.clienty; } if (t === "mousedown") { m.buttonraw |= m.bm[e.which-1]; } else if (t === "mouseup") { m.buttonraw &= m.bm[e.which + 2];} e.preventdefault(); } mouse.start = function(element, blockcontextmenu){ if(mouse.element !== undefined){ mouse.removemouse();} mouse.element = element; mouse.mouseevents.foreach(n => { element.addeventlistener(n, mousemove); } ); if(blockcontextmenu === true){ element.addeventlistener("contextmenu", preventdefault, false); mouse.contextmenublocked = true; } } mouse.remove = function(){ if(mouse.element !== undefined){ mouse.mouseevents.foreach(n => { mouse.element.removeeventlistener(n, mousemove); } ); if(mouse.contextmenublocked === true){ mouse.element.removeeventlistener("contextmenu", preventdefault);} mouse.contextmenublocked = undefined; mouse.element = undefined; } } return mouse; })(); //=========================================================================== // fullscreen canvas //=========================================================================== // delete needed quickrunjs environment function removecanvas(){ if(canvas !== undefined){ document.body.removechild(canvas); } canvas = undefined; } // create onscreen, background, , pixelate canvas function createcanvas(){ canvas = document.createelement("canvas"); canvas.style.position = "absolute"; canvas.style.left = "0px"; canvas.style.top = "0px"; canvas.style.zindex = 1000; document.body.appendchild(canvas); } function resizecanvas(){ if(canvas === undefined){ createcanvas(); } canvas.width = window.innerwidth; canvas.height = window.innerheight; ctx = canvas.ctx = canvas.getcontext("2d"); } //=========================================================================== // general set //=========================================================================== var canvas,ctx; canvas = undefined; // create , size canvas resizecanvas(); // start mouse listening canvas mouse.start(canvas,true); // flag context needs blocked // listen resize window.addeventlistener("resize",resizecanvas); var holdexit = 0; // stop in quickrunjs environment var font = "18px arial"; //=========================================================================== // following function creating render nodes. //=========================================================================== // render functions // adds box render node; function addboxtonode(node,when,stroke,fill,lwidth,w,h){ function drawbox(){ ctx.strokestyle = this.sstyle; ctx.fillstyle = this.fstyle; ctx.linewidth = this.lwidth; ctx.fillrect(-this.w/2,-this.h/2,this.w,this.h); ctx.strokerect(-this.w/2,-this.h/2,this.w,this.h); } var rendernode = { render : drawbox, sstyle : stroke, fstyle : fill, lwidth : lwidth, w : w, h : h, } node[when].push(rendernode); return node; } // adds text render node function addtexttonode(node,when,text,x,y,fill){ function drawtext(){ ctx.textalign = "center"; ctx.textbaseline = "middle"; ctx.fillstyle = this.fstyle ctx.filltext(this.text,this.x,this.y); } var rendernode = { render : drawtext, text : text, fstyle : fill, x : x, y : y, } node[when].push(rendernode); // binds node return node; } // renders node function rendernode(renderlist){ var i,len = renderlist.length; for(i = 0; < len; += 1){ renderlist[i].render(); } } //--------------------------------------------------------------------------- // animation functions // add rotator node. rotates node function addrotatortonode(node,speed){ function rotator(){ this.transform.rot += this.rotspeed; } node.animations.push(rotator.bind(node)) node.rotspeed = speed; } // addd wobbla nod. wobbles node function addwobblatonode(node,amount){ function wobbla(){ this.transform.sx = 1 - ((math.cos(this.transform.rot) + 1) / 2) * this.scaleamount ; this.transform.sy = 1 - ((math.sin(this.transform.rot) + 1) / 2) * this.scaleamount ; } node.animations.push(wobbla.bind(node)) node.scaleamount = amount; } // add groover node. move funcky thang. function addgroovertonode(node,amount){ function wobbla(){ this.transform.x += math.cos(this.transform.rot) * this.translatedist ; this.transform.y += math.sin(this.transform.rot*3) * this.translatedist ; } node.animations.push(wobbla.bind(node)) node.translatedist = amount; } // function animate , set transform function settransform(){ var i, len = this.animations.length; for(i = 0; < len; ++){ // animtions on node this.animations[i](); } // set transfomr ctx.scale(this.transform.sx, this.transform.sy); ctx.translate(this.transform.x, this.transform.y); ctx.rotate(this.transform.rot); } //--------------------------------------------------------------------------- // node creation // creats node , returns function createnode(){ return { transform : undefined, settransform : settransform, // function apply current transform animations : [], // animation functions render : rendernode, // render main function prerenders : [], // render done befor child nodes rendered postrenders : [], // render done after child nodes rendered nodes : [], itterationcounter : 0, // important counts iteration depth }; } function addnodetonode(node,child){ node.nodes.push(child); } // adds transform node , returns transform function createnodetransform(node,x,y,sx,sy,rot){ return node.transform = { x : x, // translate y : y, sx : sx, //scale sy : sy, rot : rot, //rotate }; } // 1 top node var nodetree = createnode(); // no details yet // add transform top node , keep ref moving var toptransform = createnodetransform(nodetree,0,0,1,1,0); // top node has no render var boxnode = createnode(); createnodetransform(boxnode,0,0,0.9,0.9,0.1) addrotatortonode(boxnode,-0.02) addwobblatonode(boxnode,0.2) addboxtonode(boxnode,"prerenders","blue","rgba(0,255,0,0.2)",3,100,100) addtexttonode(boxnode,"postrenders","first",0,0,"red") addtexttonode(boxnode,"postrenders","text on top",0,20,"red") addnodetonode(nodetree,boxnode) function addnode(node,x,y,scale,rot,text,anrot,ansc,antr){ var boxnode1 = createnode(); createnodetransform(boxnode1,x,y,scale,scale,rot) addrotatortonode(boxnode1,anrot) addwobblatonode(boxnode1,ansc) addgroovertonode(boxnode1,antr) addboxtonode(boxnode1,"prerenders","black","rgba(0,255,255,0.2)",3,100,100) addtexttonode(boxnode1,"postrenders",text,0,0,"black") addnodetonode(node,boxnode1) // add boxes coners var boxnode2 = createnode(); createnodetransform(boxnode2,50,-50,0.8,0.8,0.1) addrotatortonode(boxnode2,0.2) addboxtonode(boxnode2,"postrenders","black","rgba(0,255,255,0.2)",3,20,20) addnodetonode(boxnode1,boxnode2) var boxnode2 = createnode(); createnodetransform(boxnode2,-50,-50,0.8,0.8,0.1) addrotatortonode(boxnode2,0.2) addboxtonode(boxnode2,"postrenders","black","rgba(0,255,255,0.2)",3,20,20) addnodetonode(boxnode1,boxnode2) var boxnode2 = createnode(); createnodetransform(boxnode2,-50,50,0.8,0.8,0.1) addrotatortonode(boxnode2,0.2) addboxtonode(boxnode2,"postrenders","black","rgba(0,255,255,0.2)",3,20,20) addnodetonode(boxnode1,boxnode2) var boxnode2 = createnode(); createnodetransform(boxnode2,50,50,0.8,0.8,0.1) addrotatortonode(boxnode2,0.2) addboxtonode(boxnode2,"postrenders","black","rgba(0,255,255,0.2)",3,20,20) addnodetonode(boxnode1,boxnode2) } addnode(boxnode,50,50,0.9,2,"bot right",-0.01,0.1,0); addnode(boxnode,50,-50,0.9,2,"top right",-0.02,0.2,0); addnode(boxnode,-50,-50,0.9,2,"top left",0.01,0.1,0); addnode(boxnode,-50,50,0.9,2,"bot left",-0.02,0.2,0); //=========================================================================== // recursive node render //=========================================================================== // safety var must have not used recursion var recursioncount = 0; // number of nodes const max_recusion = 30; // max number of nodes itterate // safe recursive global recursion count limit nodes reandered function rendernodetree(node){ var i,len; // safty net if((recursioncount ++) > max_recusion){ return; } ctx.save(); // save context state node.settransform(); // animate , set transform // pre render node.render(node.prerenders); // render each child node len = node.nodes.length; for(i = 0; < len; += 1){ rendernodetree(node.nodes[i]); } // post renders node.render(node.postrenders); ctx.restore(); // restore context state } //=========================================================================== // recursive node render //=========================================================================== ctx.font = font; function update(time){ ctx.settransform(1,0,0,1,0,0); // reset top transform ctx.clearrect(0,0,canvas.width,canvas.height); // set top transform mouse position toptransform.x = mouse.x; toptransform.y = mouse.y; recursioncount = 0; rendernodetree(nodetree); requestanimationframe(update); } requestanimationframe(update);
Comments
Post a Comment