ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

p5.js坐标系完全指南:从原点拆到变换,一次讲透

p5.js坐标系完全指南:从原点拆到变换,一次讲透 如果你刚开始写 p5.js八成会遇到这样一种“灵异事件”明明画了一个圆它却不在你脑海中的位置给图形加了个旋转它却像脱缰野马一样飞出了屏幕照着教程敲完了代码画面却和教程截图完全对不上。遇到这些别急着怀疑自己写错了代码很可能是坐标系这关还没过。JavaScript 的 p5.js 库坐标系和你在数学课上学到的二维直角坐标系有几个关键差异。这篇就把坐标系从原点拆到变换从网格画到旋转一次讲透。这篇内容适合刚学 p5.js 的新手也适合那些用了很久但总觉得“哪里不对”的爱好者。坐标系是 p5.js 所有图形操作的公共底盘translate、rotate、scale、push/pop甚至后续接触 3D 模式和 shader全都建立在这套规则之上。把这层地基打牢后面写什么都会顺手很多。1. p5.js 坐标系和数学坐标系有三个关键区别1.1 原点在左上角、y 轴朝下坐标单位是像素p5.js 默认的 2D 坐标系规则其实非常简单就三条原点 (0,0) 固定在画布左上角。x 轴正方向朝右y 轴正方向朝下。坐标单位就是像素一个单位对应画布上的一个逻辑像素。数学课上的笛卡尔坐标系通常原点在图形中心y 轴向上数值往上是正的。p5.js 里完全相反往右 x 变大往下 y 变大。也就是说在一个 400×400 的画布上四个角的坐标是左上角 (0,0)、右上角 (400,0)、左下角 (0,400)、右下角 (400,400)。而画布正中心是 (200,200)。很多新手第一次画 circle(width/2, height/2, 100) 时还不太理解为什么这个圆正好在正中间其实这正是因为 width/2 和 height/2 分别对应 x 和 y 的中间值。换个角度说p5.js 里的坐标系就是在告诉你一句话越往右下坐标越大这个直觉一旦建立后面看很多问题都会清晰很多。1.2 为什么 p5.js 要“反着来”屏幕绘图坐标系的底层习惯你可能会问为什么 p5.js 不跟数学书保持一致这不是设计者故意找麻烦而是因为 p5.js 的祖先 Processing 从诞生起就面向视觉与互动艺术目标平台就是屏幕而屏幕绘制的行业约定本身就建立在“左上原点、y 向下”的基础上。从早期 CRT 显示器的扫描线顺序到操作系统里窗口的坐标约定再到 HTML 的 CSS 盒模型、浏览器 canvas 2D API全都遵循同一套规则光栅化时从左上角开始逐行铺像素。如果 p5.js 强行改用数学坐标系那任何绘图操作底层都得先做一次翻转白白增加计算量也会让“在窗口里越往下数值越大”这种天然直觉被破坏。所以这不是 bug而是一个深思熟虑的设计决定。这里可以做一个生活化类比写字时我们习惯从左往右、从上往下写屏幕绘制像素也是这个顺序坐标跟着书写方向走反而更自然。1.3 坐标系和画布范围坐标真的只有画布那么大吗还有一个容易被忽略的点p5.js 的坐标系范围并不局限于画布内部。你可以画一个圆在圆周位置为 (-50, -50)只要它在屏幕内看不到代码也不会报错你也可以画一个正方形让它的 x 坐标超过 800它依然会被渲染只是超出画布的部分被裁切了看不见而已。理解这一点很重要因为很多“图形突然消失”的问题根源其实是坐标跑到画布外面去了而不是程序真的出了逻辑错误。后面第五部分我会专门讲怎么排查这类问题。2. 坐标系变换三板斧translate、rotate、scale 到底转了什么2.1 translate把原点“搬”到你想要的位置translate(x, y) 做的事情是把当前坐标系的原点移动到 (x, y)。调用之后后续所有绘图操作都基于新原点来计算位置。我见过很多新手不理解“原点”是什么意思这里换个说法你画一个圆 circle(0, 0, 50)它画在哪里完全取决于当时坐标系原点在哪。如果原点在默认的 (0,0)圆就在左上角如果你先用 translate(200, 150)再画 circle(0, 0, 50)这个圆就跑到了屏幕坐标 (200,150) 处。直接看这段代码function draw() { background(20); circle(0, 0, 50); // 画在画布左上角 translate(200, 150); circle(0, 0, 50); // 画在 (200,150) 处 translate(50, 50); circle(0, 0, 50); // 画在 (250,200) 处因为上一个 translate 被累积了 }注意translate 是会累积的第二次 translate(50,50) 是在第一次的基础上又移动了 50。如果不希望累积就需要 push 和 pop 配合这个下面第 2.4 节细说。translate 的意义在于它把“固定画布上的绝对坐标”变成了“相对于某个点的局部坐标”。绘图时与其每次手算绝对位置不如把原点搬到图形的中心然后直接用局部坐标绘制这样代码可读性高也方便复用。2.2 rotate旋转永远绕原点而不是绕图形自己rotate(angle) 的用法看起来简单但坑非常大。首先angle 的单位是弧度制不是角度制。一圈是 2π半圈是 π。很多新手直接写 rotate(90)以为转了 90 度实际转的是 90 弧度相当于转了超过 14 圈视觉效果就是图形疯狂乱转。正确写法应该是 rotate(radians(90)) 或 rotate(PI/2)。更重要的是rotate 永远围绕当前坐标系的原点旋转而不是围绕某个图形的中心。这一点是无数人踩坑的根源。function draw() { background(20); // 错误理解以为 rotate 是绕矩形中心转 rotate(radians(30)); rectMode(CENTER); rect(100, 100, 60, 60); }这段代码里的矩形并不会在原地旋转 30 度而是会以一个很大的半径绕着画布左上角 (0,0) 划弧线。因为 rotate 改变了整个坐标系的朝向原本坐标为 (100,100) 的点在旋转后的坐标系里位置也跟着变了视觉上就是“整个矩形被甩了出去”。正确的绕自身中心旋转要先 translate 到图形中心再 rotatefunction draw() { background(20); translate(100, 100); // 第一步把原点搬到矩形中心 rotate(radians(30)); // 第二步绕新的原点旋转 rectMode(CENTER); rect(0, 0, 60, 60); // 矩形此时在自己的中心旋转 }这里的顺序极其关键一定是先平移、后旋转。如果反过来先 rotate 再 translatetranslate 的偏移方向会被旋转影响效果完全不同。2.3 scale缩放时会连“线宽”一起放大还能翻转 y 轴scale(sx, sy) 按比例缩放坐标系。scale(2, 2) 会让后续绘制的所有图形坐标和尺寸都翻倍比如一个 50×50 的矩形画出来变成 100×100 的视觉效果。这很容易理解但有个细节经常被忽略线宽 strokeWeight 也会跟着缩放。如果你设置了 strokeWeight(2)然后在 scale(2,2) 环境下绘制线的视觉宽度会变成 4。如果不想线宽被缩放影响你需要在绘制时手动除以当前缩放系数或者统一在 push/pop 中处理。这类“环境属性连带变化”的问题在实际项目中非常常见。scale 还有另一个妙用scale(1, -1) 可以把 y 轴翻转让坐标系变回数学课上“y 轴向上”的样子。这个技巧在很多数据可视化和数学图形绘制场景中非常有用但有一个明显的副作用文字也会跟着倒过来。第 5 节我会专门讲怎么处理。scale 同样会累积也和 translate、rotate 一样受到 push/pop 的约束。2.4 push 和 pop坐标系现场的“存档”与“读档”坐标系变换是累积的如果不加控制你会发现自己陷入“越画越乱”的泥潭。push() 会保存当前坐标系状态包括 translate、rotate、scale 的所有累积结果pop() 则把最近一次 push 保存的状态恢复出来。它们的本质是操作一个状态栈可以嵌套使用。一个非常实用的习惯每次做坐标系变换之前先 push()做完立刻 pop()。这样可以保证变换只影响当前这一段绘图不会污染后续代码。function draw() { background(20); push(); // 存档当前坐标系 translate(100, 100); // 在这个局部环境里随便折腾 rotate(radians(45)); rect(0, 0, 50, 50); pop(); // 恢复存档坐标系回到平移之前 circle(0, 0, 30); // 这还是在画布左上角画圆没有被之前的变换影响 }可以把这个机制理解成游戏里的存档读档进入一个副本前存个档打完副本读档回来外面的世界一点没变。递归和循环里尤其要注意 push/pop 要成对出现不然坐标系会越转越歪。3. 图文并茂一套坐标网格工具加三个坐标系实战案例3.1 先造一个坐标系图解工具网格与箭头纸上谈兵没用我建议你在自己的项目里加一个辅助函数把坐标系画出来。这样每次运行你都能直接“看见”原点、x 轴、y 轴的位置调试起来一目了然。function drawGrid(spacing) { strokeWeight(1); stroke(60); for (let x 0; x width; x spacing) { line(x, 0, x, height); } for (let y 0; y height; y spacing) { line(0, y, width, y); } // x 轴标红 stroke(255, 90, 90); strokeWeight(2); line(0, 0, width, 0); // y 轴标蓝 stroke(90, 180, 255); line(0, 0, 0, height); }再配一个画箭头的辅助函数用来标明轴的正方向function drawArrow(ox, oy, tx, ty) { push(); stroke(255); strokeWeight(2); line(ox, oy, tx, ty); let angle atan2(ty - oy, tx - ox); let len 10; line(tx, ty, tx - len * cos(angle - PI / 4), ty - len * sin(angle - PI / 4)); line(tx, ty, tx - len * cos(angle PI / 4), ty - len * sin(angle PI / 4)); pop(); }然后这样调用function setup() { createCanvas(400, 400); } function draw() { background(20); drawGrid(40); drawArrow(0, 0, 360, 0); // x 轴方向 drawArrow(0, 0, 0, 360); // y 轴方向 fill(255); circle(200, 200, 8); // 画一个参考点标出中心 }你会直观地看到原点在左上x 向右y 向下中心坐标是 (200,200)。这套“可视化坐标系”的习惯建议长期保留排查问题时非常管用。3.2 实战一让图形绕自己中心旋转绕自身中心旋转是动画里最常用的需求比如旋转的风车、转动的齿轮、加载中的圆形图标。正确做法是三步走先 translate再 rotate最后在原点附近绘制。let angle 0; function setup() { createCanvas(400, 400); rectMode(CENTER); } function draw() { background(30); angle 0.03; // 错误版直接 rotate矩形会绕着画布左上角甩来甩去 push(); rotate(angle); fill(255, 120, 120); rect(100, 100, 80, 80); pop(); // 正确版先把原点搬到矩形中心 push(); translate(300, 200); rotate(angle); fill(255, 200, 80); rect(0, 0, 80, 80); pop(); }运行这段代码你会看到左边的红色矩形沿着一个弧形轨道乱跑右边的橙色矩形稳稳地在原地旋转。两条代码只差了一个 translate效果天差地别。这个对比足够说明坐标系变换顺序的重要性。3.3 实战二父子坐标系的“行星轨道”动画坐标系变换的另一个经典应用是构建父子层级关系。“地球绕太阳转”这个动画如果不用坐标系变换你需要用 sin、cos 实时算地球的坐标用了坐标系变换逻辑会变得非常简洁。function setup() { createCanvas(400, 400); } function draw() { background(20); // 把原点搬到画布中心 translate(width / 2, height / 2); // 太阳 noStroke(); fill(255, 200, 40); circle(0, 0, 60); // 地球绕太阳公转 push(); rotate(frameCount * 0.02); // 公转 translate(140, 0); // 在旋转后的坐标系中偏移 140 像素 fill(60, 150, 255); circle(0, 0, 30); pop(); }这里的关键在于rotate 让坐标系旋转translate(140, 0) 的位置会随着旋转而改变所以地球在每一帧看起来都“指向”不同的方向自然就形成了圆周运动。你完全不需要手动计算 sin、cos。再加一层“月亮绕地球转”的嵌套父子层级的效果会更明显push(); rotate(frameCount * 0.02); translate(140, 0); fill(60, 150, 255); circle(0, 0, 30); // 月亮在“地球”的局部坐标系里再转 push(); rotate(frameCount * 0.2); translate(40, 0); fill(220); circle(0, 0, 12); pop(); pop();这个嵌套结构用一句话概括就是先让世界动起来再让物体在自己的局部世界里做动作。这也是创意编程里非常核心的思维方式。3.4 实战三用 scale(1,-1) 建立数学式直角坐标系如果你要画函数曲线、散点图或者做物理模拟可能更希望 y 轴朝上。这时可以用 scale(1, -1) 把坐标系翻转过来function setup() { createCanvas(400, 400); } function draw() { background(20); // 翻转 y 轴让正方向朝上 scale(1, -1); // 注意平移的 y 也要取负才能把原点放到画布中心 translate(width / 2, -height / 2); // 画 x 轴和 y 轴 stroke(100); strokeWeight(2); line(-width / 2, 0, width / 2, 0); line(0, -height / 2, 0, height / 2); // 画一条抛物线 y 0.01 * x^2 noStroke(); fill(255, 120, 60); for (let x -150; x 150; x 3) { let y 0.01 * x * x; circle(x, y, 5); } }scale(1, -1) 的本质是让所有 y 坐标取反所以原本坐标为 (x, 100) 的点绘制时会变成 (x, -100)呈现出来的效果就是“向上”。平移时为什么 y 要取负因为原点现在也被翻转了如果你写 translate(width/2, height/2)原点会跑到画布下面去所以必须写成 -height/2。这个技巧很实用但注意一个副作用文字 text() 也会被上下颠倒。解决办法是绘制文字时把坐标系单独再翻转回来或者手动创建一个不翻转的“文字层”。4. 坐标系在真实项目中怎么用三个典型场景4.1 数据可视化数据值到像素坐标的映射p5.js 经常被用来画图表、仪表盘和动态数据看板。真实数据往往不是像素值比如温度可能是 -10℃ 到 40℃时间可能是 0 点到 24 点。这时就需要做“数据坐标→屏幕坐标”的映射。最简单的工具是 map() 函数// 把气温值映射到条形图的像素高度 let barHeight map(temp, -10, 40, 20, 280);map(value, min1, max1, min2, max2) 做的事情是线性映射。举个例子当 temp -10 时barHeight 是 20temp 40 时barHeight 是 280中间值按比例插值。这比手写 “(temp - (-10)) / (40 - (-10)) * (280 - 20) 20” 要直观得多。但这里有一个容易出错的地方数据纵轴的方向默认是“越大越靠上”而屏幕 y 轴是“越大越靠下”。如果你直接用 map(temp, -10, 40, height, 0)那么高温会对应屏幕上方低温对应下方。很多新手写反了映射区间图表就倒过来了。记住纵轴映射时要考虑方向的翻转。4.2 创意编程递归分形里的局部坐标系坐标系变换在递归分形中几乎不可或缺。比如经典的分形树每画一节树枝就在枝头建立一个新的局部坐标系再在这个局部坐标系里继续画下一节。这样写的代码极其简洁因为每一层递归都“从头开始”画自己的分支。function setup() { createCanvas(400, 400); } function draw() { background(20); translate(width / 2, height); branch(100); } function branch(len) { strokeWeight(map(len, 4, 100, 1, 6)); stroke(200); line(0, 0, 0, -len); translate(0, -len); // 移动到枝头 if (len 4) { push(); rotate(PI / 6); branch(len * 0.7); pop(); push(); rotate(-PI / 6); branch(len * 0.7); pop(); } }这段代码里最值得留意的是 push 和 pop 的配对使用。每次分支前 push分支完成后 pop才能保证左右两个分支的旋转角度不会互相影响。如果少了 pop旋转会不断累积最终树枝会朝一个方向越偏越远。4.3 游戏与物理圆周排列与物体围绕另一个高频需求是“多个物体均匀分布在圆周上”比如雷达扫描点、粒子环绕效果、技能冷却图标。用坐标系变换你甚至不用计算任何三角函数function draw() { background(20); translate(width / 2, height / 2); for (let i 0; i 6; i) { push(); rotate(i * TWO_PI / 6); // 每次旋转 60 度 translate(150, 0); // 在旋转后的坐标系中偏移 rectMode(CENTER); fill(255, 120, 80); rect(0, 0, 30, 30); pop(); } }这 6 个方块会均匀分布在半径为 150 的圆周上而且都“面向”圆心。如果用绝对坐标来手算每帧都要算 sin 和 cos代码又长又容易写错。用旋转坐标系整个问题被降维成“先转角度再往前走固定距离”逻辑很干净。物理模拟中也经常用到“世界坐标系”和“局部坐标系”分离的思路。比如物体的自旋用局部坐标系物体的平移用世界坐标系。坐标系变换像是一层一层的俄罗斯套娃每一层管理自己的局部状态代码就不会乱成一锅粥。5. 坐标系实战排坑5 个高频问题的排查思路5.1 旋转方向看起来反了p5.js 的 y 轴向下所以“正角度”在视觉上表现为顺时针旋转而数学坐标系中正角度是逆时针。如果你习惯数学中的角度比如认为正 30 度是逆时针到了 p5.js 里就会看到完全相反的旋转效果。解决办法很简单如果需要逆时针效果直接传负角度或者用 -radians(angle)。角度制换弧度制用 radians()这个函数建议常备。5.2 旋转后图形“飞”出屏幕这是一个非常高频的问题。原因在于 rotate 永远绕当前坐标系原点旋转而默认原点在左上角。如果你的图形原本在 (100,100)一 rotate 它就开始绕着左上角做圆周运动自然就“飞”走了。解决办法就是先 translate 再 rotatepush(); translate(cx, cy); // 先把原点搬到目标中心 rotate(angle); rect(0, 0, w, h); // 在原点附近绘制 pop();如果图形依然乱飞检查一下是不是忘了把 rectMode 设成 CENTER或者绘制时用了绝对坐标而不是 (0,0) 附近的相对坐标。5.3 push/pop 配错对变换越积越乱变换是累积的如果你在循环里调用 rotate 但没在循环内 push/pop那么每一帧的旋转都会叠加到上一帧之上结果图形会越来越快地旋转或者位置越偏越远。我在实际项目里见过最典型的错误是在 draw() 开头忘记 restore或者循环内部 push 了但没 pop导致内存里的坐标系状态越来越多。建议养成一个习惯凡是涉及 translate / rotate / scale一律先 push() 后操作操作完立刻 pop()。这样即使代码改动频繁坐标系也不会悄悄乱掉。5.4 坐标系翻转过文字跟着倒立当你使用 scale(1, -1) 或 scale(-1, 1) 翻转坐标系后用 text() 绘制的文字也会被镜像或倒置。这在数据可视化和数学绘图里几乎必然遇到。解决办法是给文字单独建立“未翻转”的坐标系push(); scale(1, -1); // 当前坐标系 y 轴朝上 // 画图形…… // 画文字时临时恢复 push(); scale(1, -1); // 再翻转回来这样文字方向正常 text(label, x, y); pop(); pop();更彻底的方案是把文字放在另一个循环里单独用一个不翻转的坐标系绘制。这样图形和文字互不干扰逻辑也清晰。5.5 排查速查表现象、原因、解法一条龙我把常见的坐标系问题整理成一个速查表方便你以后遇到问题直接对照现象可能原因解决办法图形不在预期位置忘了 translate或 translate 顺序错误先 translate 到目标点再绘制图形旋转后跑出屏幕rotate 绕原点旋转不是绕图形中心先 translate 到图形中心再 rotate旋转方向与预期相反y 轴向下正角度是顺时针用负角度或 -radians(angle)角度传了数值旋转效果诡异弧度制与角度制混淆用 radians(90) 或 PI/2文字倒立或镜像scale 翻转了坐标系单独建立临时翻转坐标系画文字线宽变粗或变细scale 会连带缩放 strokeWeight手动除以缩放系数或用 push/pop 隔离图形画了但看不到坐标超出画布范围被裁切打印坐标检查是否在 width/height 范围内变换越积越乱越画越歪没有成对使用 push/pop养成变换前后 push/pop 的习惯在实际调试时我的经验是先在 draw() 开头加一个临时网格也就是第 3 节的那个 drawGrid() 函数把坐标系可视化出来再对着现象排查。绝大多数坐标系问题在网格上画几笔就能看出端倪。带过不少学 p5.js 的朋友之后我发现坐标系这关一旦通了后面学粒子系统、分形、物理引擎乃至 WebGL 模式下的 3D 坐标都会顺畅很多。因为底层逻辑是同一套平移、旋转、缩放不过是不同维度上的矩阵变换罢了。如果你现在正被坐标系绕晕别急着背结论打开一个空画布打上网格把原点、方向、变换一个个亲手画出来亲眼看到它们运动变化比记一百条注意事项都管用。
返回列表