Reputation: 411
Basically I have a container object with 'children' that are modified relative to their parent, and I want to rotate all of the objects by changing the parent's rotation value, while keeping the orientation of the individual children stable. (as in, rotate the whole object) I feel like I'm not explaining this very well, so here are two examples. PhysicsJS: http://wellcaffeinated.net/PhysicsJS/ (see the first example, with the 0.7 and the balls -- notice how when the zero or seven are rotated after a collision the overall shape of the object is maintained. Same goes for this example in PhaserJS (http://phaser.io/examples/v2/groups/group-transform-rotate) with the robot. Now, just to see if I could, I tried to duplicate the aforementioned PhysicsJS example with my own library -- https://jsfiddle.net/khanfused/r4LgL5y9/ (simplified for brevity)
Art.prototype.modules.display.rectangle.prototype.draw = function() {
// Initialize variables.
var g = Art.prototype.modules.display.rectangle.core.graphics,
t = this;
// Execute the 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 to the labeled point of interest -- that's where I rotate the canvas. If the object has a parent, it's rotated by the parent's value plus the object's value -- otherwise, just the object's value. I've tried some different combinations, like...
• parent - object
• object - parent
...and I looked through PhysicsJS and Phaser's sources for some kind of clue in the right direction, to no avail.
How do I rotate a group but not change its layout?
Upvotes: 0
Views: 1496
Reputation: 54079
To transform a group of objects surround the group with the transform you wish to apply to all the members of the group and then just render each member with its own transform. Before each member is transformed by its local transform you need to save the current transform so it can be used for the next group member. At the end of rendering each group member you must restore the transform back to the state for the group above it.
The data structure
group = {
origin : { x : 100, y : 100},
rotate : 2,
scale : { x : 1, y : 1},
render : function(){ // the function that draws to the 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 something
groups : [] // could have more members
}], // the objects to be rendered
}
Recursive rendering
Rendering nested transformations is best done via recursion where the renderGroup function checks for any sub groups and calls itself to render that group. This makes it very easy to have complex nested objects with the minimum of code. A tree is a simple example of recursion where the terminating condition is reaching the last node. But this can easily go wrong if you allow nested group members to reference other members within the tree. This will result in Javascript blocking the page and a crash.
function renderGroup(group){
ctx.save();
// it is important that the order of transforms us correct
ctx.translate(group.origin.x, group.origin.y);
ctx.scale(group.scale.x, group.scale.y);
ctx.rotate(group.rotate);
// draw what is needed
if(group.render !== undefined){
group.render();
}
// now draw each member of this group.groups
for ( var i = 0 ; i < group.groups.length; i ++){
// WARNING this is recursive having any member of a group reference
// another member within the nested group object will result in an
// infinite recursion and computers just don't have the memory or
// speed to complete the impossible
renderGroup(group.groups[i]); // recursive call
};
// and finally restore the original transform
ctx.restore();
}
That is how to nest transforms and how the W3C has intended for the render to be used. But I would never do it this way. It is a killer of frame rate due to the need to use save and restore, this is because ctx.getTransform support is very limited (only Chrome). As you can not get the transform you must mirror is in code, needless as there are many optimisations that can be applied if you are maintaining the matrix. Where you may get 1000 sprites in realtime using setTransform and a little math, doing it this way on canvas quarters or worse the frame rate.
Demo
Running example with safe recursion.
Draws nested objects centered on where the mouse is.
The demo is simply a recursive render taken from some other code I have and cut to suit this demo. It extends the recursive render to allow for animation and render order. Note that the scales are non uniform thus there will be some skewing the deeper the iterations go.
// adapted from 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 for setting and 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 for my QuickRunJS environment
function removeCanvas(){
if(canvas !== undefined){
document.body.removeChild(canvas);
}
canvas = undefined;
}
// create onscreen, background, and 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 up
//===========================================================================
var canvas,ctx;
canvas = undefined;
// create and size canvas
resizeCanvas();
// start mouse listening to canvas
mouse.start(canvas,true); // flag that context needs to be blocked
// listen to resize
window.addEventListener("resize",resizeCanvas);
var holdExit = 0; // To stop in QuickRunJS environment
var font = "18px arial";
//===========================================================================
// The following function are for creating render nodes.
//===========================================================================
// render functions
// adds a box render to a 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 a text render to a 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 to this node
return node;
}
// renders a node
function renderNode(renderList){
var i,len = renderList.length;
for(i = 0; i < len; i += 1){
renderList[i].render();
}
}
//---------------------------------------------------------------------------
// animation functions
// add a rotator to a node. Rotates the node
function addRotatorToNode(node,speed){
function rotator(){
this.transform.rot += this.rotSpeed;
}
node.animations.push(rotator.bind(node))
node.rotSpeed = speed;
}
// addd a wobbla to a nod. Wobbles the 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 a groover to a node. Move that 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 to animate and set a transform
function setTransform(){
var i, len = this.animations.length;
for(i = 0; i < len; i ++){ // do any animtions that are on this node
this.animations[i]();
}
// set the 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 a node and returns it
function createNode(){
return {
transform : undefined,
setTransform : setTransform, // function to apply the current transform
animations : [], // animation functions
render : renderNode, // render main function
preRenders : [], // render to be done befor child nodes are rendered
postRenders : [], // render to be done after child nodes are rendered
nodes : [],
itterationCounter : 0, // important counts iteration depth
};
}
function addNodeToNode(node,child){
node.nodes.push(child);
}
// adds a transform to a node and returns the 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
};
}
// only one top node
var nodeTree = createNode(); // no details as yet
// add a transform to the top node and keep a ref for 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 to 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 for those not used to recursion
var recursionCount = 0; // number of nodes
const MAX_RECUSION = 30; // max number of nodes to itterate
// safe recursive as global recursion count will limit nodes reandered
function renderNodeTree(node){
var i,len;
// safty net
if((recursionCount ++) > MAX_RECUSION){
return;
}
ctx.save(); // save context state
node.setTransform(); // animate and set transform
// do pre render
node.render(node.preRenders);
// render each child node
len = node.nodes.length;
for(i = 0; i < len; i += 1){
renderNodeTree(node.nodes[i]);
}
// do 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 the top transform to the mouse position
topTransform.x = mouse.x;
topTransform.y = mouse.y;
recursionCount = 0;
renderNodeTree(nodeTree);
requestAnimationFrame(update);
}
requestAnimationFrame(update);
Upvotes: 2