pixelfx

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

pixelfx

pixelfx

Pixel art effects as code. Each effect is one file,
src/fx/<category>/<name>.js
, that draws into two buffers: color (pixels: opaque, dithered or truly translucent) and light (additive glow). No images, so an effect can be played live, recolored, mirrored, reseeded and rendered to a sprite sheet for any engine.
The method is Hugo Duprez's (How to make (good) pixel art effects with Claude): code is clay, trust your eyes, feed your references. One-shot prompts make mediocre effects. Drafts you render, look at and refine make good ones.
用代码实现像素艺术特效。每个特效都是一个文件,
src/fx/<category>/<name>.js
,会绘制到两个缓冲区: color(像素:不透明、抖动或真正的半透明)和 light(叠加式发光)。无需使用图片,因此一个特效 可以实时播放、重新着色、镜像、重新设定随机种子,并渲染为适用于任意引擎的 Sprite Sheet。
这种方法来自 Hugo Duprez 的文章(How to make (good) pixel art effects with Claude):代码如同黏土,要相信自己的眼睛,并提供参考素材。一次性提示词制作出来的效果只能算平庸。先渲染草稿、观察并打磨,才能做出优秀的效果。

Before anything

开始之前

  1. Work in a pixelfx checkout. If the working directory has no
    tools/render.mjs
    , clone one next to the project:
    git clone https://github.com/patch-gg/pixelfx
    . No install step: the tools use only Node.
  2. Check it runs:
    node tools/render.mjs fire/torch
    writes
    records/fire/torch/contact.png
    . Open that PNG with your image-reading tool. That contact sheet is how you see every effect from now on.
  1. 在 pixelfx 仓库中工作。如果当前工作目录没有
    tools/render.mjs
    ,就在项目旁克隆一个:
    git clone https://github.com/patch-gg/pixelfx
    。无需安装步骤:这些工具只使用 Node。
  2. 确认它能运行:
    node tools/render.mjs fire/torch
    会写入
    records/fire/torch/contact.png
    。使用图像读取工具打开该 PNG。此后,你将通过这张 contact sheet 查看每个特效。

1. Look for an existing effect first

1. 先查找已有特效

node tools/list.mjs <words>
searches ids, names, tags and descriptions (
node tools/list.mjs frost
,
node tools/list.mjs slash loop
).
docs/CATALOG.md
lists everything by category. Render the candidates and look at them before choosing. Most requests are an existing effect with different options:
  • palette
    : the main ramp, dark to bright, as hex strings. Any
    PAL
    ramp in
    src/core/color.js
    (fire ember lava smoke dust ice water electric volt poison holy heal arcane shadow blood earth stone gold leaf wood metal bone neon cyber matrix candy night) or your own. A blue fire is
    {"palette": PAL.ice}
    .
  • seed
    for a different but equivalent take;
    flip
    to face left;
    angle
    to rotate;
    light
    (0..1+) for glow strength; plus each effect's own knobs in its
    meta.options
    (size, count, radius, text, height...).
  • Try options without editing code:
    node tools/render.mjs fire/torch --opts '{"palette":["#102","#215","#43a","#8cf","#fff"]}'
    .
node tools/list.mjs <words>
可搜索 ID、名称、标签和描述(
node tools/list.mjs frost
、
node tools/list.mjs slash loop
)。
docs/CATALOG.md
按分类列出全部内容。渲染候选效果并逐一查看,再做选择。大多数需求都可以通过为已有效果调整选项来满足:
  • palette
    :主色阶,从暗到亮,以十六进制字符串表示。可使用
    src/core/color.js
    中的任意
    PAL
    色阶(fire ember lava smoke dust ice water electric volt poison holy heal arcane shadow blood earth stone gold leaf wood metal bone neon cyber matrix candy night),也可以使用自己的色阶。蓝色火焰就是
    {"palette": PAL.ice}
    。
  • seed
    可获得不同但等效的版本;
    flip
    可向左朝向;
    angle
    可旋转;
    light
    (0..1+)控制发光强度;此外还有每个特效在
    meta.options
    中自己的调节项(大小、数量、半径、文本、高度……)。
  • 不改代码也可以尝试选项:
    node tools/render.mjs fire/torch --opts '{"palette":["#102","#215","#43a","#8cf","#fff"]}'
    。

2. Put it in the game

2. 放入游戏

Canvas or WebGL games in JS or TS (plain canvas, Phaser, PixiJS, Kaplay, three.js): use the code directly. It has no dependencies; copy
src/core
and the effect files you need, or depend on the repo.
js
import { createFrame, clearFrame, createFx, composite } from "pixelfx";   // src/core/index.js
import * as frostNova from "pixelfx/fx/ice/frost-nova";                   // src/fx/ice/frost-nova.js

const frame = createFrame(320, 180);            // the game's pixel resolution; ox, oy scroll it with the camera
const img = new ImageData(320, 180);
const fx = createFx(frostNova, { x: hero.x, y: hero.y, flip: hero.facing < 0 });

// every tick
fx.update(dt);
clearFrame(frame);
fx.draw(frame, "back");    // what goes behind characters
drawCharacters();          // your own sprites, or draw them into frame.color yourself
fx.draw(frame, "front");
composite(frame, img.data, background);   // color over the background plus light; or null for transparent
ctx.putImageData(img, 0, 0);              // then scale the canvas up with image-rendering: pixelated
if (fx.done()) removeIt();               // one-shots end on their own; fx.stop() fades a loop out
Several effects can share one frame. In a WebGL engine, upload the composited RGBA to a texture each frame, or render the effect to a sprite sheet once (below) and play it like any other animation.
Any engine (Godot, Unity, GameMaker, Defold, LÖVE, Bevy, Construct): export sprite sheets.
node tools/render.mjs ice/frost-nova --sheet
writes
records/ice/frost-nova/frost-nova.png
(transparent frames in a grid) and
frost-nova.json
(Aseprite-style frames, so Phaser's atlas loader, Godot and Unity Aseprite importers and most tools read it;
meta.pixelfx
has fps, loop, frame size, columns and the pivot). Set the pivot to
meta.pixelfx.pivot
(the effect's origin, e.g. the feet), import with nearest-neighbour filtering and no compression, and play at
meta.pixelfx.fps
. Glow is baked into the sheet as color with alpha, which reads as additive over dark scenes; for bright scenes, render a recolored version or keep the effect on a dark layer.
For the full library:
node tools/render.mjs <category> --sheet
does a whole category.
A hit needs a body behind it. An effect alone looks like a sticker. Effects with a character carry an
act
: when the body winds up, when to freeze, how the target reacts. Use it whichever way the game is built:
  • With the stock character:
    createAct(slash, { x, y })
    instead of
    createFx
    .
    act.update(dt)
    ,
    act.draw(frame)
    , and throw the picture by
    act.shake
    . It holds still by itself during a freeze.
    node tools/act.mjs <id> --sheet
    bakes the lot into a sprite sheet.
  • With the game's own character: read the timing,
    slash.act
    or
    meta.pixelfx.act
    in a sheet's JSON (
    windup
    ,
    recovery
    ,
    hits
    with
    t
    and
    freeze
    ). Play the character's attack animation
    windup
    seconds before the effect, and at each hit stop updating everything, effect and characters both, for
    freeze
    seconds. Host-side hit-stop works on any effect, with or without an act.
src/fx/moves/
has whole attacks chained from the effects, wind-up to recovery: start there when someone asks for a move, not an effect.
使用 JS 或 TS 的 Canvas 或 WebGL 游戏(原生 Canvas、Phaser、PixiJS、Kaplay、three.js):可直接使用代码。它没有依赖项;复制
src/core
和所需特效文件,或者直接依赖该仓库。
js
import { createFrame, clearFrame, createFx, composite } from "pixelfx";   // src/core/index.js
import * as frostNova from "pixelfx/fx/ice/frost-nova";                   // src/fx/ice/frost-nova.js

const frame = createFrame(320, 180);            // the game's pixel resolution; ox, oy scroll it with the camera
const img = new ImageData(320, 180);
const fx = createFx(frostNova, { x: hero.x, y: hero.y, flip: hero.facing < 0 });

// every tick
fx.update(dt);
clearFrame(frame);
fx.draw(frame, "back");    // what goes behind characters
drawCharacters();          // your own sprites, or draw them into frame.color yourself
fx.draw(frame, "front");
composite(frame, img.data, background);   // color over the background plus light; or null for transparent
ctx.putImageData(img, 0, 0);              // then scale the canvas up with image-rendering: pixelated
if (fx.done()) removeIt();               // one-shots end on their own; fx.stop() fades a loop out
多个特效可以共用同一帧。在 WebGL 引擎中,每帧将合成后的 RGBA 上传到纹理,或者只渲染一次特效 Sprite Sheet(见下文),再像播放其他动画一样播放它。
任意引擎(Godot、Unity、GameMaker、Defold、LÖVE、Bevy、Construct):导出 Sprite Sheet。
node tools/render.mjs ice/frost-nova --sheet
会写入
records/ice/frost-nova/frost-nova.png
(网格中排列的透明帧)和
frost-nova.json
(Aseprite 风格帧信息,因此 Phaser 的 atlas loader、Godot 和 Unity 的 Aseprite importer 以及大多数工具都能读取;
meta.pixelfx
包含 fps、loop、帧大小、列数和 pivot)。将 pivot 设置为
meta.pixelfx.pivot
(特效原点,例如脚部),导入时使用 nearest-neighbour filtering(最近邻过滤)且不压缩,并以
meta.pixelfx.fps
播放。辉光会作为带 alpha 的颜色烘焙进 Sprite Sheet,在暗场景中会呈现叠加效果;对于明亮场景,请渲染一个重新着色的版本,或将特效保留在单独的暗色图层上。
要处理完整库:
node tools/render.mjs <category> --sheet
会渲染整个分类。
命中特效的背后需要角色身体。 单独看特效,就像一张贴纸。带角色的特效包含一个
act
:何时让身体蓄力、何时定格、目标如何反应。无论游戏采用哪种实现方式,都可以这样使用:
  • 使用内置角色:用
    createAct(slash, { x, y })
    代替
    createFx
    。调用
    act.update(dt)
    、
    act.draw(frame)
    ,并通过
    act.shake
    抖动画面。定格期间它会自行保持静止。
    node tools/act.mjs <id> --sheet
    会把整套动作烘焙成 Sprite Sheet。
  • 使用游戏自己的角色:读取时序信息,即 Sprite Sheet JSON 中的
    slash.act
    或
    meta.pixelfx.act
    (
    windup
    、
    recovery
    ,以及带有
    t
    和
    freeze
    的
    hits
    )。在特效开始前
    windup
    秒播放角色攻击动画;每次命中时,让所有内容——特效和角色——停止更新
    freeze
    秒。宿主端 hit-stop 对任何特效都适用,无论它是否包含 act。
src/fx/moves/
包含由特效串联而成的完整攻击,从蓄力一直到恢复:如果有人要的是一套动作而非单个特效,应从那里开始。

3. Make a new one

3. 制作新特效

Read
docs/AUTHORING.md
first: it is the contract (meta,
draw(p, t, o)
for pure effects or
create(o)
for simulated ones, local coordinates, seamless loops, ending empty), the toolkit, the painter API and the quality bar. Then:
  1. Say it in one line with art direction: what it is, how it moves, its palette, its timing. "A frost nova: crystals erupt in a ring from the feet outward, a white flash at the center, shards spin off, the floor stays frosted and fades."
  2. Find references in the library:
    node tools/list.mjs <words>
    , then read the two or three closest effect files and borrow their techniques (
    Particles
    and
    Bits
    in
    src/core/sim.js
    for sparks, smoke and debris,
    p.disc
    for translucent volumes,
    cycle
    for seamless particles,
    tileFbm
    scrolls for flames and water). Look at the closest of Hugo Duprez's effects too: they are the quality bar. If his has more detail, light or life at the same size on screen, yours isn't done.
  3. Draft.
    node tools/new.mjs <category>/<name>
    (add
    --loop
    for a loop) gives a working file. When the direction is open, write 3 to 5 genuinely different drafts (
    <name>-a.js
    ..
    -e.js
    ), render them all (
    node tools/render.mjs "<category>/<name>-*"
    ) and look at every contact sheet before choosing. Show the user the strongest two or three if they're around (
    --gif
    makes animations to share).
  4. Look, judge, fix. Render, open
    records/<id>/contact.png
    , and judge like a picky pixel artist. Fix the worst thing, render again. Three rounds minimum; a showpiece takes more.
    --all
    shows every frame,
    --mp4
    shows motion at full speed.
  5. Act it out if a character is involved: add
    export const act
    (see "Acts" in
    docs/AUTHORING.md
    ), with a wind-up before t = 0, a pose change on the release frame, a freeze on contact and a recovery.
    node tools/act.mjs <id>
    shows consecutive 12 fps steps: judge motion from those and from
    --mp4
    , not from a contact sheet's samples.
  6. Check.
    node tools/check.mjs <id>
    catches drawing outside the canvas, one-shots that don't end empty, loop seams, unseeded randomness and slow frames, and in an act: keys out of order, unknown pose fields and hands that teleport. Fix until it passes clean. Delete the drafts you didn't keep.
What to look for, and the usual fix:
you seefix
it reads as mush at 1xfewer colors, a clear silhouette, one focal point, bigger shapes
speckle: lone noise pixels inside shapesthreshold noise less, quantize with
at()
, no noise on small shapes
floaty, no punchfast in, slow out (
ease.outQuart
/
outExpo
), a 1 to 2 frame white hero frame, shorter total time
stiff or mechanicalvary every particle (size, speed, delay, life), add secondary motion (sparks, smoke, debris)
washed outless glow (k 0.2 to 0.5), darker edge steps, keep white for the hottest core only
flatlight from one side: brighter top-left, darker bottom-right step; a darker ramp step at the base
smooth gradients, vector lookhard ramp steps in color; softness only in light and in translucent smoke
a checkerboard where smoke should be
SMOKE
,
VAPOUR
or
DUST
particles, or
p.disc
: truly translucent, lit on top
seam when a loop wraps
cycle
,
loopNoise(phase)
, whole-cell
tileNoise
scrolls,
sin(phase * TAU * n)
a one-shot that pops offlet it shrink, dither out or fall away before t = duration
a great effect on a dead charactergive it an act: wind-up, release pose, freeze on contact, a target that reacts, recovery
the act feels weightlessa wind-up the opposite way to the blow, a longer freeze (up to 0.2 s), a bigger silhouette change, shake
the hit lands out of syncmove the hit key to the effect's own constant, not to a time you eyeballed
he holds the weapon badlykeep a bend in the elbow, shafts in front of the chest; on a swing the blade lags the hand; arms reach about 15 px and don't stretch, so step the body in
his arms cross or the pose looks flathe faces three-quarters towards us: for open arms the near hand goes left (
fx < 0
), far hand right, with
turn: 1
;
yaw
really turns him
Directional effects face right, ground effects stand on y = 0, actor effects wrap
o.actor
(a 24 x 38 stock character's mask and pixels), and nothing uses
Math.random
. Generic motifs are fine; never recreate a specific game's sprites, characters or logos.
先阅读
docs/AUTHORING.md
:它是规范文档,涵盖
meta
、纯特效所用的
draw(p, t, o)
或模拟特效所用的
create(o)
、局部坐标、无缝循环、空帧结束、工具包、Painter API 和质量标准。 然后:
  1. 用一行描述并给出美术方向:它是什么、如何运动、使用什么调色板、节奏如何。“一个冰霜新星:水晶从脚边向外呈环形爆发,中心出现一道白色闪光,碎片旋转飞散,地面保持结霜并逐渐消退。”
  2. 在库中寻找参考:
    node tools/list.mjs <words>
    ,然后阅读最接近的两三个特效文件并借鉴其技法(
    src/core/sim.js
    中的
    Particles
    和
    Bits
    可用于火花、烟雾和碎屑,
    p.disc
    用于半透明体积,
    cycle
    用于无缝粒子,滚动的
    tileFbm
    可用于火焰和水)。再查看 Hugo Duprez 的特效中最接近本需求的作品:Hugo Duprez's effects。这里是质量基准。如果他的作品在同一屏幕尺寸下细节、光照或生命力更丰富,你的作品就还没完成。
  3. 草稿。
    node tools/new.mjs <category>/<name>
    (添加
    --loop
    可创建循环)会生成一个可用文件。如果方向尚未明确,请编写 3 到 5 个真正不同的草稿(
    <name>-a.js
    ..
    -e.js
    ),全部渲染(
    node tools/render.mjs "<category>/<name>-*"
    ),并在选择前查看每张 contact sheet。如果用户在场,展示其中最强的两三个(
    --gif
    可生成便于分享的动画)。
  4. 观察、评判、修复。 渲染,打开
    records/<id>/contact.png
    ,并像严格的像素艺术家一样评判。修复最糟糕的问题,再次渲染。至少进行三轮;展示级作品需要更多轮。
    --all
    显示每一帧,
    --mp4
    以全速显示运动。
  5. 如果涉及角色,将动作演示出来:添加
    export const act
    (参见
    docs/AUTHORING.md
    中的 “Acts”),其中包含 t = 0 之前的蓄力、释放帧上的姿态变化、命中时的定格以及恢复阶段。
    node tools/act.mjs <id>
    会显示连续的 12 fps 步骤:应根据这些步骤和
    --mp4
    判断动作,而不要根据 contact sheet 中的采样帧判断。
  6. 检查。
    node tools/check.mjs <id>
    会检测绘制超出画布、一次性特效未以空画面结束、循环接缝、未设定种子的随机数、帧耗时过长;在 act 中还会检测键顺序错误、未知姿态字段和手部瞬移等问题。修复到检查干净通过为止。删除未保留的草稿。
需要观察什么,以及常见修复方法:
你看到的问题修复方法
在 1x 下像一团糊状物减少颜色,塑造清晰轮廓,设置一个视觉焦点,增大形状
零散噪点:形状内部出现孤立噪点降低噪声阈值,使用
at()
量化;小形状上不要加噪声
漂浮、缺乏冲击感快进慢出(
ease.outQuart
/
outExpo
),加入 1 到 2 帧白色高光主帧,缩短总时长
僵硬或机械让每个粒子都有变化(大小、速度、延迟、寿命),加入次级运动(火花、烟雾、碎屑)
色彩发白减少辉光(k 0.2 到 0.5),使用更暗的边缘色阶,只在最热的核心处保留白色
平淡让光从一侧照来:左上更亮,右下使用更暗的色阶;底部增加一层更暗的色阶
平滑渐变、矢量感色彩使用硬色阶;只让 light 和半透明烟雾表现柔和
烟雾位置出现棋盘格使用
SMOKE
、
VAPOUR
或
DUST
particles,或使用
p.disc
:真正半透明,并在顶部受光
循环回绕时出现接缝
cycle
、
loopNoise(phase)
、整格
tileNoise
滚动、
sin(phase * TAU * n)
一次性特效突然消失在 t = duration 前让它收缩、抖动消散或坠落
特效很棒,但角色像死了为它添加 act:蓄力、释放姿态、命中定格、目标反应和恢复阶段
动作显得没有重量蓄力方向与挥击方向相反,延长定格(最长 0.2 s),加大轮廓变化,添加 shake
命中不同步把命中键移到特效自身的常量上,而不是凭肉眼估算的时间
武器姿势不自然肘部保持弯曲,杆状武器置于胸前前方;挥击时刀刃滞后于手;手臂伸展约 15 px 且不要拉伸,因此让身体移动来配合
双臂交叉或姿态扁平角色以四分之三侧身朝向观察者:张开双臂时,近手向左(
fx < 0
),远手向右,并设置
turn: 1
;
yaw
才会真正转动角色
定向特效朝右,地面特效立在 y = 0 上,角色特效包裹
o.actor
(一个 24 x 38 内置角色的遮罩和像素),并且任何内容都不使用
Math.random
。可以使用通用母题,但绝不复制特定游戏的 Sprite、角色或 Logo。

4. Native code instead of JS

4. 使用原生代码而非 JS

If the game can't run JS and a sprite sheet isn't enough (an effect that follows a moving target, or needs per-instance colors at runtime), port the effect. Pure effects are a function of time; simulated ones step the particles in
src/core/sim.js
. Keep the two-buffer idea, the 4x4 Bayer threshold for dithered alpha and the palette ramp lookups. Render the JS original and your port side by side to check they match.
如果游戏无法运行 JS,而 Sprite Sheet 也不够用(例如特效需要跟随移动目标,或需要在运行时为每个实例设置颜色),就移植该特效。纯特效是时间函数;模拟特效在
src/core/sim.js
中推进粒子。保留双缓冲区思路、用于抖动 alpha 的 4x4 Bayer threshold,以及调色板色阶查找。并排渲染 JS 原版和移植版,检查二者是否匹配。