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)可以强制让分形对齐,但这只是临时的补丁,无法从根源解决问题。
问题根源
- 变换顺序错误:P5JS的变换是累积式的,先旋转、再缩放、最后平移的顺序,会导致平移操作是在已旋转缩放的坐标系中进行,而之前的计算是基于原始坐标系,两者不匹配。
- 多余的平移操作:代码中额外添加的
translate(0,50)属于冗余操作,干扰了坐标对齐。 - 坐标计算未结合分形实际位置:分形在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
相关产品推荐
相关产品推荐

