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

Popular posts from this blog

Ansible - ERROR! the field 'hosts' is required but was not set -

customize file_field button ruby on rails -

SoapUI on windows 10 - high DPI/4K scaling issue -