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

P5JS分形变位偏差问询:无限缩放Fractal分支匹配像素偏移

P5JS无限缩放分形的像素偏移问题解决

问题背景

说明:不确定这个问题属于编程还是数学范畴,若分类错误请告知正确发布位置。

我正在用P5JS实现无限缩放分形效果,思路是将分形绘制到graphics对象上,通过变换让其中一个分支与原始分形匹配,重复该过程实现无限深入的动画。但计算变换参数时,总是出现几个像素的偏移——理论计算应该完全匹配,实际偏移约4像素。

基础分形结构:一条50px的分支,递归旋转45°(PI/4弧度)并缩放0.67倍。

原始分形效果:
原始分形

预期实现效果:将变换后的分形与原始分形的分支对齐
预期效果

变换思路与问题分析

由于P5JS的参考点始终为左上角,我需要做以下变换:

  • 应用与分支相同角度的旋转
  • 应用分支缩放比例的倒数(1/0.67)进行缩放
  • 计算平移量让分支对齐

核心目标是将变换后的分形上的绿点移动到原始分形的蓝点位置,我给出的计算逻辑是:
y轴平移量 = -height + X,其中:

  • X = Y - Z
  • Y = height * 0.67 * sin(PI/4)
  • Z = width / 2 * 0.67 * cos(PI/4)

但实际运行后存在明显偏移,且x轴偏移量是y轴的两倍,怀疑与画布宽高比(宽度为高度的两倍)有关。

当前代码中,取消注释translate(4,-2)可以强制让分形对齐,但这只是临时的补丁,无法从根源解决问题。

问题根源

  1. 变换顺序错误:P5JS的变换是累积式的,先旋转、再缩放、最后平移的顺序,会导致平移操作是在已旋转缩放的坐标系中进行,而之前的计算是基于原始坐标系,两者不匹配。
  2. 多余的平移操作:代码中额外添加的translate(0,50)属于冗余操作,干扰了坐标对齐。
  3. 坐标计算未结合分形实际位置:分形在graphics画布中的根部实际位于(width/2, height),但计算时未正确结合这个基准点。

修正方案

重新推导变换公式,结合P5JS的变换顺序,计算正确的平移量:

假设:

  • 缩放因子s = 0.67,逆缩放为1/s
  • 旋转角度theta = PI/4
  • 分支长度L = 50
  • 画布宽W = width,高H = height

我们需要让变换后的分形分支端点(在graphics中坐标为(W/2, H-L))对齐到原始分形的根部(画布中坐标为(W/2, H))。根据P5JS的变换顺序(旋转→缩放→平移),推导得到平移量公式:

const cosTheta = cos(theta);
const sinTheta = sin(theta);
const xInGraphics = W/2;
const yInGraphics = H - L;

const tx = W/2 - (xInGraphics * cosTheta - yInGraphics * sinTheta) / s;
const ty = H - (xInGraphics * sinTheta + yInGraphics * cosTheta) / s;

修正后的完整代码

<script src="https://cdn.jsdelivr.net/npm/p5@1.11.0/lib/p5.min.js"></script>
let fractal;
let graphics;
let angleSlider;
let scaleSlider;

function setup() {
  createCanvas(640, 360);
  angleSlider = createSlider(0, TWO_PI, PI / 4, 0.01);
  scaleSlider = createSlider(0, 0.9, 0.67, 0.01);
  graphics = createGraphics(width, height);
  fractal = {
    render: function(graphics) {
      graphics.line(0, 0, 0, -50);
    },
    prepare: function(graphics) {
      graphics.stroke("red");
      graphics.strokeWeight(2);
      graphics.translate(width * 0.5, height);
    },
    anchors: [{
        rotation: angleSlider.value(),
        scale: 0.67,
        translation: {
          x: 0,
          y: -50
        },
      },
      {
        rotation: -angleSlider.value(),
        scale: 0.67,
        translation: {
          x: 0,
          y: -50
        },
      }
    ],
  };

  // 绘制画布边框用于调试
  graphics.stroke("red");
  graphics.strokeWeight(4);
  graphics.line(0, 0, 0, height);
  graphics.line(0, 0, width, 0);
  graphics.line(0, height, width, height);
  graphics.line(width, 0, width, height);

  graphics.stroke("blue");
  renderFractal(fractal, graphics);
}

function draw() {
  // 调试用的缩放和平移,可根据需要注释
  scale(0.5)
  translate(width / 2, height / 2)

  background(255);
  image(graphics, 0, 0); // 绘制原始小分形

  const s = 0.67;
  const theta = angleSlider.value();
  const W = width;
  const H = height;
  const L = 50;
  
  push();
  const cosTheta = cos(theta);
  const sinTheta = sin(theta);
  const xInGraphics = W/2;
  const yInGraphics = H - L;
  
  // 计算正确的平移量
  const tx = W/2 - (xInGraphics * cosTheta - yInGraphics * sinTheta) / s;
  const ty = H - (xInGraphics * sinTheta + yInGraphics * cosTheta) / s;
  
  rotate(theta);
  scale(1/s);
  translate(tx, ty);
  
  image(graphics, 0, 0); // 绘制变换后的大分形
  pop();
}

function renderFractal(fractal, graphics, iterationNumber = 1) {
  if (iterationNumber === 1) {
    fractal.prepare(graphics);
  }
  fractal.render(graphics);
  if (iterationNumber < 10) {
    for (let i = 0; i < fractal.anchors.length; i++) {
      let anchor = fractal.anchors[i];
      graphics.push();
      graphics.translate(anchor.translation.x, anchor.translation.y);
      graphics.rotate(anchor.rotation);
      graphics.scale(anchor.scale);
      renderFractal(fractal, graphics, iterationNumber + 1);
      graphics.pop();
    }
  }
}

效果验证

修正后的代码无需添加translate(4,-2)这类补丁即可实现分形分支的精准对齐,解决了像素偏移问题,能够为后续的无限缩放动画提供正确的变换基础。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 23:33:09