旋转一组物体,同时保持其方向完好无损

时间:2016-04-19 05:02:23

标签: javascript html5 canvas rotation

基本上我有一个带有孩子的容器对象'相对于其父级修改的,我想通过更改父级的旋转值来旋转所有对象,同时保持各个子项的方向稳定。 (如旋转整个物体)我觉得我没有很好地解释这一点,所以这里有两个例子。 PhysicsJS:http://wellcaffeinated.net/PhysicsJS/(参见第一个例子,0.7和球 - 请注意在碰撞后零或七旋转时如何保持对象的整体形状。在PhaserJS中这个例子也是如此( http://phaser.io/examples/v2/groups/group-transform-rotate)与机器人。现在,只是为了看看我是否可以,我试图用我自己的库复制前面提到的PhysicsJS示例 - https://jsfiddle.net/khanfused/r4LgL5y9/(简化为简洁)

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;

};

请参阅标记的兴趣点 - 旋转画布的位置。如果对象具有父对象,则它将按父对象的值加上对象的值旋转 - 否则,只是对象的值。我尝试了一些不同的组合,比如......

•父 - 对象
•object - parent

......我查看了PhysicsJS和Phaser的消息来源,寻找正确方向的某种线索,但无济于事。

如何旋转组但不更改其布局?

1 个答案:

答案 0 :(得分:2)

嵌套转换

要使用要应用于组的所有成员的变换转换组周围的对象,然后使用自己的变换渲染每个成员。在通过局部变换转换每个成员之前,您需要保存当前变换,以便它可以用于下一个组成员。在渲染每个组成员的最后,您必须将转换恢复回其上方组的状态。

数据结构

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
}

递归渲染

渲染嵌套转换最好通过递归来完成,其中renderGroup函数检查任何子组并调用自身来呈现该组。这使得使用最少的代码很容易拥有复杂的嵌套对象。树是递归的简单示例,其中终止条件到达最后一个节点。但是,如果允许嵌套组成员引用树中的其他成员,则很容易出错。这将导致Javascript阻止页面崩溃。

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();
}

这就是如何嵌套转换以及W3C如何使用渲染。但我永远不会这样做。由于需要使用保存和恢复,这是帧速率的杀手,这是因为ctx.getTransform支持非常有限(仅限Chrome)。由于无法获得转换,因此必须使用镜像进行转换,因为如果要维护矩阵,可以应用许多优化。你可以使用setTransform和一些小数学实时获得1000个精灵,在画布上使用这种方式或者更差的帧速率。

<强>演示

使用安全递归运行示例。

以鼠标所在的位置绘制嵌套对象。

该演示只是一个递归渲染,取自我所拥有的其他代码并剪切以适应此演示。它扩展了递归渲染以允许动画和渲染顺序。请注意,比例是不均匀的,因此迭代的深度会有一些偏差。

&#13;
&#13;
// 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);
&#13;
&#13;
&#13;