@johnhenry/canvas-fx
v0.0.1
Published
Pixel effects for images, video, canvases, and live HTML, written as HTML: <pixel-canvas effects="mosaic(4) palette(gameboy)">. Custom elements shipped as source, no build step.
Downloads
325
Maintainers
Readme
canvas-fx
Full documentation: opensource.johnhenry.me/canvas-fx
Pixel effects for any image, video, or canvas, written as HTML. Put the
image in a <pixel-canvas> and list the effects, like CSS filter:
<script type="module" src="https://esm.sh/@johnhenry/canvas-fx/global.mjs"></script>
<pixel-canvas width="160" effects="adjust(contrast 1.3) palette(gameboy, ordered)">
<img src="photo.jpg" alt="Our cat" />
</pixel-canvas>Effects run left to right: here the contrast is raised, then the photo is
reduced to the Game Boy's four greens. Before the script loads, or if the
image can't be read, the plain <img> shows, so nothing is lost.
Effects work on images, video, canvases, pixel art you write as text, your own drawing elements (a game, a visualization), and, experimentally, live HTML. Custom elements, shipped as source: no build step, no dependencies.
Contents
- Install
- Effects
- Writing effects
- Your own effects
- GPU effects
- Live HTML (experimental)
- Ideas
- Notes
- Adding a new effect
- Family
- License
Install
npm install @johnhenry/canvas-fxOr straight from a CDN, with no install at all:
<script type="module" src="https://esm.sh/@johnhenry/canvas-fx/global.mjs"></script>global.mjs registers every element; pixel-canvas/global.mjs alone is
enough for the effects attribute; @johnhenry/canvas-fx exports every
class unregistered, plus the effect toolkit (definePixelEffect,
definePixelShader, …). Each element's guide is in its directory
(src/<element>/readme.md); the generated
element reference lists every attribute, property,
event, and CSS part.
Provenance. canvas-fx was developed inside
@johnhenry/domkit (as
src/pixelable/, and before that experimental/pixel-shader and the
pixelshader.component that domkit 0.0.4 shipped, from johnhenry/lib) and
split out into this package with its history. This family was never
published to npm under any name: 0.0.0 is its first version.
Effects
| Function | Element | What it does |
|---|---|---|
| mosaic(size) | <pixel-mosaic> | Pixelates into blocks of one color |
| palette(colors, dither, count) | <pixel-palette> | Limits the colors to a palette (gameboy, pico-8, 1bit, …, any CSS colors, or auto from the image), with dithering |
| grid(size, color, line) | <pixel-grid> | Grid lines between cells |
| adjust(brightness, contrast, saturation, hue) | <pixel-adjust> | Tone and color, like the CSS filter functions |
| halftone(size, angle, ink, paper) | <pixel-halftone> | Printed dots |
| outline(threshold, ink, paper) | <pixel-outline> | Line art from edges |
| crt(scanlines, mask, glow) | <pixel-crt> | An old screen: scanlines and a color stripe mask |
| chroma-key(color, tolerance, softness) | <pixel-chroma-key> | Makes a color transparent (green screen) |
| glitch(amount, rate) | <pixel-glitch> | Animated digital breakup |
| wave(amplitude, wavelength, speed) | <pixel-wave> | Rows rippling along a moving sine wave |
| lens(radius, zoom) | <pixel-lens> | A magnifying glass that follows the pointer |
| spotlight(radius, softness, dim) | <pixel-spotlight> | Light around the pointer, the rest dimmed |
| glyphs(cell, chars, font, mode, color, background) | <pixel-glyphs> | The image as text characters (ASCII art), by brightness or by shape; toText() gives it back as text |
The <pixel-canvas> draws the result. Its
source can be an <img>, a <video>, a <canvas>, or a
<pixel-sprite>: pixel art written as text,
with animation frames, or another <pixel-canvas> (they chain). With
html (experimental), it draws its own HTML content, live and still
interactive, where the browser has
HTML-in-canvas.
It also runs whole chains on the GPU (gpu), animates between settings
(transition="400ms"), follows the pointer, and records itself as an
animated GIF (toGIF()), a video (record()), or text (toText()); its
guide has each.
global.mjs here registers it and every effect element; for the
effects attribute alone, pixel-canvas/global.mjs is enough.
Writing effects
As a function, parameters fill in order, or by name: palette(gameboy,
ordered) and palette(dither ordered, colors gameboy) are the same.
Values are as you'd write them in CSS (grid(8, rgb(0 0 0 / 0.5))).
An effect can appear more than once.
As elements wrapped around the source, each effect's parameters are attributes, and the effects apply from the inside out:
<pixel-canvas width="160">
<pixel-palette colors="gameboy" dither="ordered"> <!-- 2. then this -->
<pixel-adjust contrast="1.3"> <!-- 1. this first -->
<img src="photo.jpg" alt="Our cat" />
</pixel-adjust>
</pixel-palette>
</pixel-canvas>Elements earn their keep when you want to switch one effect on and off
(disabled, say from a checkbox) or drive it from script. Both forms can
be mixed: the elements run first (they're inside), then the attribute's
list.
Your own effects
definePixelEffect() registers an effect for both forms:
import { definePixelEffect, number } from "@johnhenry/canvas-fx/effects.mjs";
definePixelEffect(
"posterize",
(image, params) => {
const levels = number(params.levels, 4, { min: 2 });
const step = 255 / (levels - 1);
for (let i = 0; i < image.data.length; i += 4) {
for (let c = 0; c < 3; c++) image.data[i + c] = Math.round(image.data[i + c] / step) * step;
}
return image;
},
{ params: ["levels"] },
);<pixel-canvas effects="posterize(3)">…</pixel-canvas>
<pixel-canvas><pixel-posterize levels="3">…</pixel-posterize></pixel-canvas>The function gets the ImageData and the parameters as strings, by name
(number() and parseColor() help read them), and returns an
ImageData. A third argument, { time, frame }, is for effects that
change over time: time is seconds on the canvas's clock, and frame
counts redraws. random(seed) gives repeatable randomness, so a paused
canvas doesn't flicker. A canvas already showing an effect that wasn't defined yet
redraws once it is. For an effect with its own state, extend PixelEffect
and override apply(image); call invalidate() after a change that isn't
an attribute. Any element with an apply(image) method works.
GPU effects
<pixel-canvas gpu> runs the whole chain on WebGL2 when every effect in it
can (most built-ins, every shader): upload once, a shader per effect,
nothing read back. A chain with a CPU-only step (glitch, palette(auto),
Floyd–Steinberg, a JavaScript effect) runs on the CPU; renderer says
which ran. Your own effect gets a GPU version with
definePixelEffect(name, apply, { gpu: { fragment, uniforms } }) (see
gpu.mjs).
<pixel-shader> runs a GLSL fragment shader
you write in a <script type="x-shader/x-fragment"> child, on the GPU,
with the image, its size, and the clock provided, and its u_ uniforms
set from attributes. definePixelShader(name, code) (in shader.mjs)
makes a shader a named effect for both forms.
Live HTML (experimental)
<pixel-canvas html> draws its own HTML content, live and still
interactive, through the effects, using the proposed
HTML-in-canvas API; where the API
is missing, the content shows as it is.
<pixel-canvas>'s guide
has the details.
Ideas
- Retro art: any photo in Game Boy greens, PICO-8 colors, 1-bit dither, or as halftone print.
- A live camera effect: a
<video>showinggetUserMedia()is redrawn every frame. Addchroma-key(lime)and a CSS background on the<pixel-canvas>for a virtual backdrop. - Privacy:
mosaic(16)makes faces or screenshots unrecognizable, on the client. - Creative coding: a
<pixel-shader>over a camera, withfpsandu_timefor motion. - Icons and game art with no image files:
<pixel-sprite>, animated withfps, run throughcrt()orpalette(gameboy). - Theme from a picture:
<pixel-canvas swatches="3" swatches-target="html">sets--pixel-swatch-1…--pixel-swatch-3from an album cover or photo, for the page's CSS to use. - Previews: what an image looks like on an e-ink panel
(
palette(#000 #fff, floyd-steinberg)) or an old TV (crt()).
Notes
- Without
gpu, effects run on the CPU, once per redraw. For video, usegpu, or keep the working size small (<pixel-canvas width="160">) and scale the result up with CSS. It's drawn withimage-rendering: pixelated, so it stays crisp.
Adding a new effect
grid() cutting gaps (grid(8, transparent), see the CHANGELOG) is the best
small worked example: one registered function, picked up by both forms.
Smallest: a new parameter on an existing effect. Every effect reads its
parameters by name (resolveParams), so a new one is a new name in the
effect's params list and a branch in its function. No new element, no new
tag. Prefer this whenever the new behavior is a variant of an existing look.
A genuinely new effect is one function and four touchpoints:
src/pixel-<name>/effect.mjs:definePixelEffect("<name>", (image, params, { time, frame }) => …, { params: [...] }). This registers<name>(…)for theeffectsattribute and a<pixel-<name>>element for the wrapping form.src/pixel-<name>/index.mjsandglobal.mjs: the element class (aPixelEffectsubclass with JSDoc for the manifest:@tag,@attrper parameter) and its registration.src/builtins.mjs(andsrc/global.mjs,src/index.mjs): load it with the built-ins, register and export it.- The one part that isn't boilerplate: the pixel math. Work on the
ImageDatain place and return it; userandom(seed)rather thanMath.random()so a paused canvas doesn't flicker; readtimeonly if the effect changes over time, andpointeronly if it follows the pointer (then pass{ pointer: true }). For<pixel-canvas gpu>, give it agpu: { fragment, uniforms }version too (built withgpuShader()fromgpu.mjs), and a test that the two draw the same pixels (test/browser/expansions.spec.mjsdoes this for every built-in).
Then: a test in test/browser/pixel-canvas.spec.mjs that checks actual pixels
(not just that the function ran), a row in the Effects table, a
readme.md for the element, npm run manifest, and a CHANGELOG entry.
Family
@johnhenry/domkit, where this family started: its<frame-timer>is the clock that animated sources (games, sprites) usually run on, and its<hot-key>,<gamepad-input>, and<swipe-input>drive whatever is drawn. canvas-fx doesn't import domkit; they meet in the page.@johnhenry/data-plot, plots written as HTML: any element can be one of its marks, so a<pixel-sprite>can be a point on a plot. Neither imports the other.forsnakenis a consumer: its<forsnaken-game>is a custom source (it exposes acanvasand firesframechange), shown through<pixel-canvas effects="grid(…)">.htmlbuilderreads this package'scustom-elements.json(throughpackage.json'scustomElements), so every effect is in its palette with typed fields.
License
MIT
