You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在JS图形编程中围绕轴心旋转对象组

实现Container子对象围绕轴心旋转的解决方案

我正在用JavaScript开发一款基于HTML Canvas的轻量简易2D引擎,目前在实现继承机制。参考PixiJS设计了entity类和container类,二者都包含draw函数,接收graphics类引用、旋转偏移量、x和y坐标作为参数。container类用于让多组entity对象协同运动,现在需要实现子对象围绕container的轴心旋转的效果(即容器旋转时,子对象保持相对容器的位置,绕容器中心转动)。

现有代码

Container类

//Container Class
class container {
    //List of objects (children)
    objects = [];
    //Position and rotation
    x = 0;
    y = 0;
    rot = 0;
    //Layer
    layer = 0;

    //Add a child to the container
    addChild = function(child) {
        //Add reference to array
        this.objects.push(child);
        //Sort by layer
        this.objects = this.objects.sort((a, b) => { if (a.layer > b.layer) { return 1 } return -1})
    }

    //Draw
    draw = function(gfx, rot, x, y) {
        //Loop through all children
        for(let i=0; i<this.objects.length; i++) {
            let ent = this.objects[i]
            //Call draw function from child and offset rotation, x, and y, by current x and y.
            //Basically, the x and y from the input of the function offsets the container, and then we
            //offset the children by the container's x and y.
            //Allows for endless inheritance and offsets
            ent.draw(gfx, this.rot + rot, this.x + x, this.y + y);
        }
    }
}

Entity类

//Entity class
class entity {
    //Sprite
    sprite = null;
    //Position and rotation
    x = 0;
    y = 0;
    rot = 0;
    //Layer
    layer = 0;

    //Create new sprite on creation (don't worry about this)
    constructor() {
        this.sprite = new sprite();
    }

    //Draw function
    draw = function(gfx, rot, x, y) {
        //Draws the sprite at this entities position offsetted by the input position and rotation
        gfx.drawspr(this.sprite, this.x + x, this.y + y, this.rot + rot);
    }
}

Sprite类

//Sprite class
class sprite {
    //Image
    img = null;
    width = 0;
    height = 0;
    //Loads image
    load = async function(src) {
        //New image object
        this.img = new Image();
        //Sets source to src
        this.img.src = src
        //Waits for image to load
        await new Promise((resolve, reject) => {
            setTimeout(() => {
              resolve("timeout");
            }, 10000);
            this.img.onload = function() {
                resolve();
            }
        });
        //Sets the width to the image
        this.img.onload = null;
        this.width = this.img.width;
        this.height = this.img.height;
    }
}

drawspr函数

//Draws sprite. (spr - sprite object, x - x position on screen to render, y - y position on screen to render
//rot - rotation to render the sprite
drawspr = function(spr, x, y, rot) {
    //Save current 2d context transform
    this.ctx.save();
    if(rot!=0) {
        //Rotate the screen to draw the image if its rotation is bigger than 0
        this.ctx.rotate(rot*Math.PI/180);
    }
    //Draw the image at the x and y, and reset 2d context transform
    this.ctx.drawImage(spr.img, x, y, spr.width, spr.height)
    this.ctx.restore();
}

问题分析

现有代码的核心问题是仅通过数值叠加坐标和旋转角度,没有利用Canvas上下文的变换机制。这种方式无法实现子对象围绕容器轴心旋转的效果——容器旋转时,子对象只会跟着平移并自身旋转,不会绕容器中心做圆周运动。

修改方案

1. 重构Container的draw方法

利用Canvas上下文的translate和rotate实现整体变换,让子对象自动继承容器的旋转和位置:

// 修改后的Container Class
class container {
    objects = [];
    x = 0;
    y = 0;
    rot = 0;
    layer = 0;

    addChild = function(child) {
        this.objects.push(child);
        // 简化排序逻辑
        this.objects = this.objects.sort((a, b) => a.layer - b.layer);
    }

    draw = function(gfx, parentRot = 0, parentX = 0, parentY = 0) {
        // 计算容器的世界坐标与总旋转角度
        const worldX = parentX + this.x;
        const worldY = parentY + this.y;
        const totalRot = parentRot + this.rot;

        // 保存当前上下文状态,避免影响其他元素
        gfx.save();
        // 平移到容器的世界位置
        gfx.translate(worldX, worldY);
        // 应用容器的总旋转角度(转为弧度)
        gfx.rotate(totalRot * Math.PI / 180);

        // 绘制子对象:此时上下文已完成容器的变换,子对象只需使用本地坐标
        for(let i = 0; i < this.objects.length; i++) {
            const ent = this.objects[i];
            // 父级变换已由上下文处理,传递0即可
            ent.draw(gfx, 0, 0, 0);
        }

        // 恢复上下文状态
        gfx.restore();
    }
}

2. 调整Entity的draw方法

实体的坐标是相对于父容器的本地坐标,只需处理自身的相对变换:

// 修改后的Entity class
class entity {
    sprite = null;
    x = 0;
    y = 0;
    rot = 0;
    layer = 0;

    constructor() {
        this.sprite = new sprite();
    }

    draw = function(gfx, parentRot = 0, parentX = 0, parentY = 0) {
        // 计算实体的相对旋转与位置
        const localRot = parentRot + this.rot;
        const localX = parentX + this.x;
        const localY = parentY + this.y;

        gfx.drawspr(this.sprite, localX, localY, localRot);
    }
}

3. 修正drawspr函数的旋转逻辑

原来的旋转基于Canvas原点,需改为围绕精灵自身中心旋转(若需要):

// 修改后的drawspr函数
drawspr = function(spr, x, y, rot) {
    this.ctx.save();
    // 平移到精灵的目标位置
    this.ctx.translate(x, y);
    if(rot !== 0) {
        // 应用旋转角度(转为弧度)
        this.ctx.rotate(rot * Math.PI / 180);
    }
    // 平移到精灵中心(若不需要围绕中心旋转,可删除此行)
    this.ctx.translate(-spr.width / 2, -spr.height / 2);
    // 绘制精灵,此时原点在精灵的旋转中心(或左上角)
    this.ctx.drawImage(spr.img, 0, 0, spr.width, spr.height);
    this.ctx.restore();
}

效果说明

修改后,当你调整Container的rot属性时,所有子Entity会自动围绕Container的轴心(即Container的x/y位置)旋转,同时保持自身相对容器的位置不变。例如:

  • 创建一个Container,设置x = canvas.width/2、y = canvas.height/2(画布中心)
  • 添加几个Entity,分别设置x为-50、0、50,y为0
  • 不断增加Container的rot值,就能看到子Entity绕画布中心旋转的效果

内容的提问来源于stack exchange,提问作者Vinny

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 18:40:24