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

Rotating a group of objects while keeping their orientation intact

本文关键字:方向 对象 一组 旋转      更新时间:2023-09-26

基本上,我有一个容器对象,其中包含相对于其父对象修改的"子对象",我希望通过更改父对象的旋转值来旋转所有对象,同时保持单个子对象的方向稳定。(如,旋转整个对象)我觉得我没有很好地解释这一点,所以这里有两个例子。PhysicsJS:http://wellcaffeinated.net/PhysicsJS/(参见第一个例子,有0.7和球——注意当碰撞后旋转0或7时,物体的整体形状是如何保持的(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;
};

参考标记的兴趣点——这是我旋转画布的地方。如果对象有一个父对象,它将按父对象的值加上对象的值进行旋转——否则,只旋转对象的值。我试过一些不同的组合,比如。。。

•父对象
•对象-父

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

演示

使用安全递归运行示例。

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

这个演示只是从我的其他代码中截取的递归渲染,并根据这个演示进行了裁剪。它扩展了递归渲染以允许动画和渲染顺序。请注意,尺度是不均匀的,因此迭代越深入,就会有一些偏斜。

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