javascript - 旋转一组对象,同时保持其方向不变

标签 javascript html canvas rotation

基本上,我有一个带有“子对象”的容器对象,这些对象相对于其父对象进行了修改,并且我想通过更改父对象的旋转值来旋转所有对象,同时保持各个子对象的方向稳定。 (如旋转整个对象)我觉得我没有很好地解释这一点,所以这里有两个例子。物理JS: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;

};

引用标记的兴趣点——这是我旋转 Canvas 的地方。如果对象有父对象,则按父对象的值加上对象的值进行旋转 - 否则,仅旋转对象的值。我尝试了一些不同的组合,例如...

• 父级 - 对象
• 对象 - 父级

...我查看了PhysicsJS和Phaser的来源,寻找某种正确方向的线索,但没有成功。

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

最佳答案

嵌套变换

要使用您希望应用于该组的所有成员的变换来变换该组周围的一组对象,然后仅使用其自己的变换来渲染每个成员。在每个成员通过其本地变换进行变换之前,您需要保存当前变换,以便它可以用于下一个组成员。在渲染每个组成员结束时,您必须将变换恢复到其上方组的状态。

数据结构

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 打算如何使用渲染。但我绝不会这样做。由于需要使用保存和恢复,它是帧速率的 killer ,这是因为 ctx.getTransform 支持非常有限(仅限 Chrome)。由于您无法获得必须在代码中镜像的变换,因此不必担心,因为如果您维护矩阵,则可以应用许多优化。您可以使用 setTransform 和一点数学来实时获得 1000 个 Sprite ,在 Canvas 四分之一或更糟糕的帧速率上这样做。

演示

使用安全递归运行示例。

以鼠标所在位置为中心绘制嵌套对象。

该演示只是从我拥有的其他一些代码中获取的递归渲染,并进行了剪切以适合该演示。它扩展了递归渲染以允许动画和渲染顺序。请注意,尺度不均匀,因此迭代越深入,就会出现一些偏差。

// 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);

关于javascript - 旋转一组对象,同时保持其方向不变,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36708977/

相关文章:

javascript - 如何分配 html 格式的 javascript 变量?

javascript - 加载tinyMCE时的默认html字符

javascript - 如何向 Kinetic JS 舞台或图层添加背景颜色?

javascript - D3 简单折线图 : Error: <path> attribute d: Expected moveto path command ('M' or 'm' ), "[object Object]"

javascript - 如何从嵌套对象中获取数据

javascript - 通过无限滚动回到相同的位置

xpath 后的 PHP DomXPath 编码问题

javascript - 如何让 GSAP 时间线识别在其中创建的元素?

html - Canvas (Kinetic.JS): multiple layers vs single layer approach

javascript - Jest : select an HTML element through innerHTML