Skip to content

Filter

Serialized node types that include FilterAttributes — today rect, ellipse, path, iconfont, and property overrides on ref — accept a filter string, similar in spirit to the CSS filter property. The engine parses the string into an internal effect list, rasterizes the shape’s bounds when needed, and runs GPU post-processing passes.

For the rendering pipeline and render-graph context, see Lesson 30 - Post-processing and render graph.

filter on nodes

Set filter on serialized nodes the same way you set fill or stroke. Multiple functions can appear in one string, separated by whitespace. Order matches application order.

Examples:

ts
api.updateNodes([
    {
        ...node,
        filter: 'blur(4px) brightness(-0.1)',
    },
]);

Anything not covered in the subsections below is ignored by parseEffect (no matching branch). Source of truth: parseEffect / formatFilter in @infinite-canvas-tutorial/ecs (utils/filter.ts).

Supported filter functions

Each ### heading is one function name in the filter string. Internal type is Effect['type'] where it differs. Unless noted, comma-separated lists use parts[i] after splitting on commas and trimming (see each branch in filter.ts).

formatFilter round-trip: only implemented cases are written back. Saturate-only adjustmentsaturate(…); other adjustment fields are dropped.

blur

FieldParsed fromDefault / clamp
Internal typeblur
valueparseFloat on the whole parenthesis content (e.g. 4px4)NaN → effectively NaN until GPU path clamps; prefer finite px values

Example: blur(6px)

brightness

FieldParsed fromDefault / clamp
Internal typebrightness
valueparseFloat(params.trim()) then clampGlfxBrightnessClamped to [-1, 1]; 0 = no change. Not CSS brightness() multiplier (see comment above clampGlfxBrightness).

Example: brightness(-0.15)

contrast

FieldParsed fromDefault / clamp
Internal typecontrast
valueparseFloat(params.trim()) then clampGlfxContrastSame base clamp as brightness, then positive branch capped at 0.999 to avoid (1 - contrast) → 0 in the shader path.

Example: contrast(0.2)

saturate

FieldParsed fromDefault / clamp
Internal typeadjustment (merged with ADJUSTMENT_DEFAULTS)
saturationparseCssFilterScalar(params)If the token ends with %, value is parseFloat / 100; otherwise parseFloat as-is. Other adjustment channels stay at defaults (gamma, contrast, brightness, RGB, alpha = 1).

Example: saturate(1.2) or saturate(50%)0.5 saturation field.

hue-rotate

FieldParsed fromDefault / clamp
Internal typehueSaturation
huedegreesToGlfxHue(parseHueRotateDegrees(params))parseHueRotateDegrees: supports deg / rad / turn / grad substrings, else treated as degrees. Mapped to glfx hue [-1, 1] (±1 ≈ ±180°).
saturationAlways 0 for this token alone (saturation comes from a separate saturate() token).

Example: hue-rotate(90deg)

drop-shadow

Whitespace-split tokens; leading numeric tokens are parseFloat (so 5px works); remainder is joined as color (default black if empty).

FieldSourceNotes
x, y, blur, spread1st–4th numeric tokensMissing trailing numbers default to 0.
colorAll tokens after the last leading numeric runCan be multi-word (rgb(...), etc.).

Examples: drop-shadow(2px 4px 6px red) — spread 0. drop-shadow(1px 1px 2px 3px rgba(0,0,0,.4))

noise

FieldParsed fromNotes
Internal typenoise
valueparseFloat on full paramsSingle scalar

pixelate

FieldParsed fromNotes
Internal typepixelate
sizeparsePixelBlockSizeStrips px (case-insensitive), parseFloat; if non-finite or ≤ 0, defaults to 1.

Example: pixelate(12px)

dot

Comma-separated; each part trimmed. Missing entries use parser defaults via parseFloat / > 0.5 for the grayscale flag.

IndexPropertyDefault (from Pixi-style defaults in parser)
0scale1
1angle (radians)5
2grayscale1 if parsed > 0.5 else 0

Example: dot(1, 5, 1)

color-halftone

BranchArguments → fields
4+ numeric parts(centerX, centerY, angle, size) → all set; size must be > 0 or replaced with 4.
2 parts(size, angle) — no center (shader uses texture center at upload).
1 part(size)angle defaults to 0.

angle is in radians on the effect; size is dot diameter in pixels (scale = π / size on GPU per glfx).

halftone-dots

Comma-separated. Indices map to HalftoneDotsEffect; see HALFTONE_DOTS_DEFAULTS.

IndexPropertyParser notes
0size0–1 clamp
1radius0–2 clamp
2contrast0–1 clamp
3gridhex / 11, else numeric > 0.51 (hex grid), else 0 (square)
4dotStyleclassic/gooey/holes/soft or integer 0–3
5originalColorsfalse/0/nofalse; true/1/yestrue; else numeric > 0.5true

fluted-glass

Internal type: flutedGlass. 17 comma-separated numbers (all parsed with parseFloat / finite fallbacks to FLUTED_GLASS_DEFAULTS):

IndexPropertyDefaultGPU clamp (high level)
0size0.50–1
1shadows0.60–1
2angle (deg)450–180
3stretch0.20–1
4shape1integer 1–5 (GlassGridShapes)
5distortion0.50–1
6highlights0.40–1
7distortionShape1integer 1–5 (GlassDistortionShapes)
8shift0-1–1
9blur0.150–1
10edges0.30–1
1114marginLeftmarginBottom0each 0–1
15grainMixer00–1
16grainOverlay00–1

tsunami

Internal type: tsunami. 11 comma-separated numbers → TSUNAMI_DEFAULTS fallbacks; stripeCount clamped 1–256 on GPU path; stripeAngle degrees → radians, clamped ±180°; blend parsed as > 0.51 else 0; drift [-1, 1].

IndexPropertyDefault
0stripeCount45
1stripeAngle (deg)0
2distortion0.32
3reflection0.17
4disturbance0.03
5contortion0.13
6blend0
7dispersion0.22
8drift0
9shadowIntensity0.5
10offset0

burn

Internal type: burn. Comma-separated; indices 0–4 are numeric via parseFloat / defaults; colors and flags:

IndexPropertyRule
0burndefault 0.5, shader clamp 0–1
1densitydefault 1, min 0.01 on GPU
2softnessdefault 0.2
3dispersiondefault 0.1
4distortiondefault 0.3
5edgeColorCSS color; used if parts.length ≥ 7
6maskColorCSS color; used if parts.length ≥ 7
7invertMaskif present: parseFloat > 0.5
8transparentif present: parseFloat > 0.5

crt

Internal type: crt. Comma-separated. Let n = parts.length. timeIdx = n >= 11 ? 7 : 4. Token at timeIdx: if auto / engine (case-insensitive) → useEngineTime: true; else numeric time.

IndexPropertyDefault (CRT_DEFAULTS)
0curvature1
1lineWidth1 (≥ 0 on GPU)
2lineContrast0.25
3verticalLine0 (> 0.5 → vertical scanlines)
timeIdxtime / engine0 or engine clock

Layouts: 5 args → time at index 4. 11+ args → legacy Pixi layout, time at index 7.

vignette

Internal type: vignette. Two numbers; GPU clamps size, amount to [0, 1].

IndexPropertyDefault
0size0.5
1amount0.5

ascii

Internal type: ascii.

IndexPropertyRule
0sizeparseFloat, max 1..min(textureW, textureH) on GPU
1replaceColorif present: parseFloat > 0.5
2+colorjoined with commas, trimmed; default #ffffff

glitch

Internal type: glitch. Important: formatFilter order is jitter, rgbSplit, time, blocks — same as comma index order below.

IndexPropertyRule
0jitterdefault 0.17 if omitted
1rgbSplitdefault 0.24 if omitted
2time / engineIf parts.length < 3useEngineTime: true. Else token auto/engine → engine time; else numeric time.
3blocksif parts.length ≥ 4, else default 0.2

liquid-glass

Internal type: liquidGlass. 17 comma-separated numbers → LIQUID_GLASS_DEFAULTS:

IndexPropertyDefault
0powerFactor4
1fPower3
2noise0.1
3glowWeight0.3
4glowBias0
5glowEdge00.06
6glowEdge10
710a, b, c, d0.7, 2.3, 5.2, 6.9
11centerX0.5
12centerY0.5
13scaleX1
14scaleY1
15ellipseSizeX1
16ellipseSizeY1

liquid-metal

Internal type: liquidMetal. Indices 0–7 numeric (shape floored to 0–4). Then:

IndexPropertyRule
8useImageif parts.length > 8: parseFloat(parts[8]) > 0.5
9colorBackif parts.length === 10 or ≥ 11, read string
10colorTintif parts.length ≥ 11
11time / engineif parts.length ≥ 12: auto/engineuseEngineTime; else numeric time. If < 12 and LIQUID_METAL_DEFAULTS.useEngineTime → engine time.
12usePoissonif parts.length > 12: parseFloat > 0.5

Defaults: repetition 2 (clamped 1–10 on GPU), shape 3, colorBack transparent, colorTint #ffffff, useEngineTime default true in defaults object.

heat-map / heatmap

Internal type: heatmap. Same parser for heat-map and heatmap.

IndexPropertyRule
04contour, angle, noise, innerGlow, outerGlownumeric / defaults
5useImageif parts.length > 5
6usePreprocessif parts.length > 6
7time / engineif parts.length > 7: auto/engine or numeric
8colorBackif parts.length > 8
917colors[]up to 9 extra comma fields (loop c < 18), appended as gradient stops

Default palette: see HEATMAP_DEFAULTS.colors (7 stops).

gem-smoke / gemSmoke

Internal type: gemSmoke. Indices 0–6 numeric; 7 shape floored 0–4.

IndexPropertyRule
8useImageif parts.length > 8
9usePoissonif parts.length > 9
10time / engineif parts.length > 10
11colorBackif parts.length > 11
12colorInnerif parts.length > 12
1318colors[]up to 6 gradient tokens (c < 13 + 6)

lut / LUT

Internal type: lut. Params parsed by parseLutFilterParams:

FormlutKeystrength
lut(url("…"), 0.5)String inside quotesAfter closing ), first comma segment, clamped [0, 1], default 1
lut(name("…"), 1)Inner quoted namesame
lut(fuji, 1)Identifier ^[a-zA-Z_][a-zA-Z0-9_-]*$second segment
lut("my-key", 1)Quoted first tokensecond segment

Registration: 3D LUT.

fxaa

Internal type: fxaa. No parameters required; fxaa() adds a pass. Parser ignores unknown params.

Parsing and formatting

Use these helpers when you build tooling or migrate stored data:

ts
import { parseEffect, formatFilter } from '@infinite-canvas-tutorial/ecs';

const effects = parseEffect('blur(2px) lut(fuji, 0.65)');
const again = formatFilter(effects);
  • parseEffect(filter: string) returns a typed Effect[] (empty array if the string is missing or invalid).
  • formatFilter(effects: Effect[]) serializes back to a filter string LUT segments preserve lut(url("…"), strength) when the key is not a simple identifier.

3D LUT (.cube)

The lut(…) segment applies a 3D color cube. Sampling matches three.js LUTPass conventions (half-texel inset and intensity mix).

Register the cube once per WebGPU Device and logical name:

ts
import { registerCubeLutFromText } from '@infinite-canvas-tutorial/ecs';

const text = await fetch('/luts/grade_sRGB.cube').then((r) => r.text());
registerCubeLutFromText(device, 'fuji', text);

Then reference it from a node:

ts
filter: 'lut(fuji, 1)',

Alternative spellings understood by the parser:

  • Named key: lut(myKey, 0.8) — must match the string passed to registerCubeLutFromText.
  • URL-style key: lut(url("./grade.cube"), 1) — the key is the path inside url("…") (must still be registered under that same key, or you register after resolving the path).

Use listRegisteredCubeLutKeys(device) to debug which keys exist; missing keys produce a one-time console warning in development.

Optional RegisterCubeLutOptions.atlasFormat ('u8' | 'f16' | 'f32') controls GPU volume texel format for HDR or wide DOMAIN cubes.

Engine-time animation

Some filters (for example crt, glitch) can read engine time for animation. The package exports filterStringUsesEngineTimeCrt, filterStringUsesEngineTimeGlitch, and filterStringUsesEngineTimePost so you can detect that before exporting video. See Export image for WEBM / GIF options.

Released under the MIT License.