project-darkwood
Saturday, February 21, 2026
Marian 2:56 AM
tldr
Cedric 2:56 AM
What shipped:
1. SDF → grayscale auto-conversion works in both procedural (mega-shader) and bitmap paths now
2. Pipeline toggle — expand any node, click the badge to cycle auto/texture/procedural
3. Mask output — every SDF node has a second grayscale "Mask" output port (smoothstep'd shape)
Plus a WebGL state bug fix —
1. SDF → grayscale auto-conversion works in both procedural (mega-shader) and bitmap paths now
2. Pipeline toggle — expand any node, click the badge to cycle auto/texture/procedural
3. Mask output — every SDF node has a second grayscale "Mask" output port (smoothstep'd shape)
Plus a WebGL state bug fix —
getSdfAsMask was clobbering the active GL program/framebuffer mid-render.Marian 3:01 AM
How to test the mask output? I tried mask blend, but it does not do what i expected it to do
Cedric 3:01 AM
Simplest test:
1. Add an SDF Circle node
2. Connect the Mask output (bottom right port, cyan) → Output node's input
3. You should see a white circle on black — that's the grayscale mask from the SDF
For blending:
1. Add a Noise node + an SDF Star node
2. Add a Mask Blend node
3. Connect Noise → Mask Blend's A input
4. Connect something else (e.g. Checker) → B input
5. Connect SDF Star's Mask output → Mask Blend's mask input
The mask controls where A vs B shows — white areas get A, black areas get B.
What were you seeing? If the Mask output is just showing black or raw distance values, there might be a bug in how the evaluator handles the
1. Add an SDF Circle node
2. Connect the Mask output (bottom right port, cyan) → Output node's input
3. You should see a white circle on black — that's the grayscale mask from the SDF
For blending:
1. Add a Noise node + an SDF Star node
2. Add a Mask Blend node
3. Connect Noise → Mask Blend's A input
4. Connect something else (e.g. Checker) → B input
5. Connect SDF Star's Mask output → Mask Blend's mask input
The mask controls where A vs B shows — white areas get A, black areas get B.
What were you seeing? If the Mask output is just showing black or raw distance values, there might be a bug in how the evaluator handles the
mask sourceHandle.Marian 3:04 AM
can we make mask blend node so that if only one input is connected, the rest of the image is transparent?
Cedric 3:05 AM
Right now it always outputs alpha 1.0 and unconnected inputs default to gray. Let me make it so when only A is connected, the mask controls alpha (transparent where mask is black). Same for only B.
Cedric 3:05 AM
Wait — the issue is that unconnected inputs return
vec4(0.5, 0.5, 0.5, 1.0) (gray with full alpha) in the procedural path. I need to make unconnected inputs return transparent black instead:Cedric 3:05 AM
The compiler returns
Actually, the simplest approach: just make unconnected B default to transparent black (
Better approach: the bitmap path uses a dummy texture (gray). In procedural, unconnected =
The real solution for what Marian wants: if only A + mask are connected, the result should be
vec4(0.5, 0.5, 0.5, 1.0) for unconnected inputs globally. Can't change that without breaking everything. Instead, I'll make the mask blend node itself detect "no input" differently.Actually, the simplest approach: just make unconnected B default to transparent black (
vec4(0,0,0,0)). But since we can't distinguish connected from unconnected in GLSL...Better approach: the bitmap path uses a dummy texture (gray). In procedural, unconnected =
vec4(0.5, 0.5, 0.5, 1.0). There's no clean way to detect this.The real solution for what Marian wants: if only A + mask are connected, the result should be
A where mask is white, transparent where mask is black. This is really just A × mask alpha. Let me add a mode param:Cedric 3:05 AM
Done — Mask Blend now has a Mode dropdown:
• Blend A/B (default): works like before, mix between A and B based on mask
• A × Mask (alpha): only needs A + mask connected. Where mask is white → shows A, where mask is black → transparent
So: SDF Circle → Mask output → Mask Blend's mask input, Noise → A input, set mode to "A × Mask" — you get noise shaped by the circle with transparency outside.
Deploying now, should be live in ~1 min.
• Blend A/B (default): works like before, mix between A and B based on mask
• A × Mask (alpha): only needs A + mask connected. Where mask is white → shows A, where mask is black → transparent
So: SDF Circle → Mask output → Mask Blend's mask input, Noise → A input, set mode to "A × Mask" — you get noise shaped by the circle with transparency outside.
Deploying now, should be live in ~1 min.
Marian 3:07 AM
Why does it need a dropdown? If A is connected, make it work like you proposed. If B is connected, make it like the inverted mask x B, and if both A and B connected, it blends them
Cedric 3:09 AM
Fair point. The challenge is detecting connected vs unconnected in the shader — unconnected inputs get dummy gray, which is indistinguishable from actual gray. Let me add a generic
${connected:xxx} mechanism.Cedric 3:09 AM
Now let me add
${connected:xxx} support to the compiler, and update the evaluator's bitmap path to set uconnected* uniforms:Cedric 3:09 AM
Now update the bitmap evaluator to set
uconnected* uniforms:Cedric 3:09 AM
Now update the maskBlend node to use auto-detection:
Cedric 3:09 AM
Deploying now. Dropped the dropdown — Mask Blend now auto-detects:
• Only A + mask: A visible where mask is white, transparent where black
• Only B + mask: B visible where mask is black, transparent where white
• A + B + mask: blends between them like before
Also added a generic
• Only A + mask: A visible where mask is white, transparent where black
• Only B + mask: B visible where mask is black, transparent where white
• A + B + mask: blends between them like before
Also added a generic
${connected:inputName} template any node can use going forward.Cedric 3:09 AM
Building and deploying. Should be live in about a minute.
Marian 3:10 AM
Good. can we put some background for the transparency to show through? Like the iconic photoshop checkers?
Cedric 3:11 AM
The simplest approach: add the checkerboard as a CSS background on the canvas element. The canvas has transparency support already (alpha channel is preserved in
readTextureToCanvas), so a CSS checkerboard behind it will show through automatically.Cedric 3:11 AM
Done — Photoshop-style dark checkerboard behind all node previews. Transparency shows through naturally since the canvas alpha channel is already preserved. Deploying now.
Marian 10:03 AM
SDF arrow sucks. fix it. add more params
image.pngCedric 10:05 AM
Yeah, this is a crude box+triangle combo. Let me rewrite it with proper params — head length, shaft length, rounding, rotation angle, and a better SDF construction:
Cedric 10:05 AM
Wait, the glslHelpers has
${helpers} without the backslash escape. Let me fix:Cedric 10:05 AM
Rewrote the arrow from scratch. New params:
• Length — overall arrow size
• Shaft Width — thickness of the stem
• Head Length — how much of the arrow is the arrowhead (proportion, 0.05–0.8)
• Head Width — how wide the arrowhead spreads
• Rounding — rounds all corners
• Tail — Flat (default), Notched (V-cut at bottom), or Pointed (tapers to a point)
Uses a proper general triangle SDF now instead of the isosceles hack. Deploying.
• Length — overall arrow size
• Shaft Width — thickness of the stem
• Head Length — how much of the arrow is the arrowhead (proportion, 0.05–0.8)
• Head Width — how wide the arrowhead spreads
• Rounding — rounds all corners
• Tail — Flat (default), Notched (V-cut at bottom), or Pointed (tapers to a point)
Uses a proper general triangle SDF now instead of the isosceles hack. Deploying.
Marian 10:50 AM
Double check that base parameters are correctly present only on nodes where it makes sense and that it is wired up correctly
Cedric 10:52 AM
Good. So
isGeneratorNode checks: not in EXCLUDED_TYPES, and has no non-mod texture inputs. Let me verify which nodes get classified as generators vs operators:Cedric 10:52 AM
Now let me check for issues:
1. SDF shape nodes (generators) — These get universal params. Makes sense: you'd want to rotate/scale/offset an SDF circle.
2. SDF operator nodes —
3.
4.
5.
Let me check a few more edge cases:
1. SDF shape nodes (generators) — These get universal params. Makes sense: you'd want to rotate/scale/offset an SDF circle.
2. SDF operator nodes —
sdfAnnular, sdfIntersect, sdfOffset, sdfOnion, sdfOutline, sdfRepeat, sdfRound, sdfSmoothIntersect/Subtract/Union, sdfSubtract, sdfUnion — These are operators (have non-mod inputs). They do NOT get universal params. Correct.3.
colorConstant — Generator (no inputs). Gets universal params... but rotating/scaling a constant color is meaningless. Should be excluded.4.
imageInput — Generator. Gets universal params. Makes sense (offset/scale an image).5.
sdfText, sdfIcon — Generators with customRender. They already handle universal params via the post-process transform in evaluator. Need to verify the compiler handles them too... but customRender nodes are always bitmap (no glslFunction), so they're never in composed segments. Fine.Let me check a few more edge cases:
Cedric 10:52 AM
Good —
Now let me check that the SDF shape nodes'
sdfText and sdfIcon are forced to texture mode (customRender). Universal params work via the post-process transform shader in the evaluator.Now let me check that the SDF shape nodes'
glslFunction properly uses the transformed UV (universal params). The compiler wraps generators with UV transform code. Let me verify with sdfCircle:Cedric 10:52 AM
The
Let me also verify that
glslFunction uses uv — and in the compiler, generator nodes get their uv replaced with _tuv (transformed UV). That's correct.Let me also verify that
seed is wired up correctly for nodes that have it:Cedric 10:52 AM
These are all generators. The seed param is additive: node's own
The only fix needed: exclude
seed + uBase_seed. Correct.The only fix needed: exclude
colorConstant from universal params.Cedric 10:52 AM
Checked everything. Here's the audit:
Generators (67 nodes) — get universal params (rotation, offset, scale, amplitude, seed):
All correctly classified. These have no non-mod texture inputs, so transform/scale/rotate makes sense.
Operators (62 nodes) — NO universal params:
All correctly classified. These take texture inputs and process them — no spatial transform needed at this level.
One fix applied:
SDF nodes verified:
• Shape generators (circle, box, star, etc.) → universal params :whitecheckmark:
• SDF operators (union, subtract, intersect, etc.) → no universal params :whitecheckmark:
•
Seed wiring: 15 noise-based nodes have own
Generators (67 nodes) — get universal params (rotation, offset, scale, amplitude, seed):
All correctly classified. These have no non-mod texture inputs, so transform/scale/rotate makes sense.
Operators (62 nodes) — NO universal params:
All correctly classified. These take texture inputs and process them — no spatial transform needed at this level.
One fix applied:
colorConstant was incorrectly getting universal params (rotating a solid color is pointless). Now excluded alongside constant and output.SDF nodes verified:
• Shape generators (circle, box, star, etc.) → universal params :whitecheckmark:
• SDF operators (union, subtract, intersect, etc.) → no universal params :whitecheckmark:
•
sdfText/sdfIcon → customRender + pipelineHint: 'texture', universal params via post-process transform :whitecheckmark:Seed wiring: 15 noise-based nodes have own
seed param, additively combined with uBase_seed. Correct.Marian 10:56 AM
are you sure no operators are sensitive to position/rotation, etc?
Cedric 10:56 AM
Good catch. Let me think about this...
Cedric 10:56 AM
You're right. These operators do UV manipulation — twirl, kaleidoscope, spherize, pinchBulge, lensDistortion all work relative to a center point. Being able to offset/rotate their effect center would be useful. Same for
But the current universal params system transforms the input UV before the node runs, which makes sense for generators (they generate from UV space). For operators, it would mean transforming the source texture sampling, which is different — it would shift/rotate the input rather than the effect itself.
Actually, that's still useful.
However — adding universal params to ALL operators would clutter the UI and add unnecessary overhead. The real question is: which operators would benefit?
Operators that are position/rotation sensitive:
•
•
•
•
•
•
•
These already have their own center/angle params though. Universal params would be redundant.
The ones that don't have built-in position control and could benefit:
•
•
•
I think the current split is actually correct. The spatial operators already have their own position controls built into their specific params. Adding universal params on top would be confusing (two layers of transform).
What do you think — is there a specific operator where you're missing position/rotation control?
transform2d — which is literally a spatial transform operator.But the current universal params system transforms the input UV before the node runs, which makes sense for generators (they generate from UV space). For operators, it would mean transforming the source texture sampling, which is different — it would shift/rotate the input rather than the effect itself.
Actually, that's still useful.
transform2d is literally that — it already has its own offset/rotation/scale params. But for something like twirl, universal params would let you shift the whole twirl effect.However — adding universal params to ALL operators would clutter the UI and add unnecessary overhead. The real question is: which operators would benefit?
Operators that are position/rotation sensitive:
•
twirl — has centerX/centerY already•
kaleidoscope — center-dependent•
spherize, pinchBulge — center-dependent•
polarCoordinates — center-dependent•
radialBlur — center-dependent•
directionalBlur — angle-dependent•
directionalWarp — angle-dependentThese already have their own center/angle params though. Universal params would be redundant.
The ones that don't have built-in position control and could benefit:
•
pixelate — grid alignment is position-sensitive•
tile — tiling origin matters•
halftone — wait, that's already a generatorI think the current split is actually correct. The spatial operators already have their own position controls built into their specific params. Adding universal params on top would be confusing (two layers of transform).
What do you think — is there a specific operator where you're missing position/rotation control?
Marian 11:16 AM
Good, fix all
Cedric 11:18 AM
Let me check exactly which operators are missing position/rotation controls:
Cedric 11:18 AM
Missing center controls:
• kaleidoscope — no center, no rotation offset
• spherize — no center
• pinchBulge — no center (has radius)
• polarCoordinates — no center
Already have them:
• radialBlur — has centerX/Y ✓
• directionalBlur — has angle ✓
• directionalWarp — has angle ✓
• twirl — has centerX/Y ✓
Let me also check
• kaleidoscope — no center, no rotation offset
• spherize — no center
• pinchBulge — no center (has radius)
• polarCoordinates — no center
Already have them:
• radialBlur — has centerX/Y ✓
• directionalBlur — has angle ✓
• directionalWarp — has angle ✓
• twirl — has centerX/Y ✓
Let me also check
pixelate and tile — these could use offset for grid alignment:Cedric 11:18 AM
Now let me add centerX/centerY and rotation to the ones that need it. I'll also add offsetX/Y to pixelate and tile.
Cedric 11:18 AM
Now pixelate (add offset) and tile (add offset):
Cedric 11:18 AM
Done. Here's what got added:
Operators that already had proper controls were left alone: radialBlur (centerX/Y), directionalBlur (angle), directionalWarp (angle), twirl (centerX/Y).
| Node | Added params |
| ----------------- | --------------------------------------------------- |
| Kaleidoscope | Center X/Y, Rotation |
| Spherize | Center X/Y, Radius |
| Pinch/Bulge | Center X/Y |
| Polar Coordinates | Center X/Y, Rotation |
| Pixelate | Offset X/Y |
| Tile | Offset X/Y, Mirror mode (repeat vs mirrored tiling) |
Operators that already had proper controls were left alone: radialBlur (centerX/Y), directionalBlur (angle), directionalWarp (angle), twirl (centerX/Y).
Marian 2:00 PM
good, make sure basic params are moddable by params as well
Cedric 2:02 PM
Let me check how mod inputs work on operators currently:
Cedric 2:02 PM
None of them have mod inputs yet. Let me check how an existing operator with mod inputs works — like
twirl:Cedric 2:02 PM
All generators. No operators have mod inputs yet. The mod system works by adding
mod<paramName> inputs and umodAmount_<paramName> uniforms. The shader reads the mod texture and applies it. Let me add mod inputs for the spatial params on all 6 operators.Cedric 2:02 PM
Done. All 6 operators now have mod inputs on their spatial params:
• Kaleidoscope: center X/Y mod, rotation mod
• Spherize: strength mod, center X/Y mod
• Pinch/Bulge: strength mod, center X/Y mod
• Polar Coordinates: center X/Y mod, rotation mod
• Pixelate: scale mod, offset X/Y mod
• Tile: tiles X/Y mod, offset X/Y mod
Connect any grayscale texture to the mod ports and use the mod amount slider to control intensity. Deploying now.
• Kaleidoscope: center X/Y mod, rotation mod
• Spherize: strength mod, center X/Y mod
• Pinch/Bulge: strength mod, center X/Y mod
• Polar Coordinates: center X/Y mod, rotation mod
• Pixelate: scale mod, offset X/Y mod
• Tile: tiles X/Y mod, offset X/Y mod
Connect any grayscale texture to the mod ports and use the mod amount slider to control intensity. Deploying now.
Marian 2:43 PM
arrow still looks very very shitty
Cedric 2:45 PM
Let me look at it myself.
Cedric 2:45 PM
Let me add an SDF Arrow node to see it:
Cedric 2:45 PM
Let me just look at the actual arrow code. The issue is probably the geometry — let me rewrite with a cleaner approach using Inigo Quilez's exact SDF primitives:
Cedric 2:45 PM
I see the problem — the defaults produce a weirdly proportioned arrow.
Let me completely rewrite this with better defaults and a cleaner construction — a proper vector arrow like you'd see in design tools:
headLength: 0.35 means 35% of the total length is head, with the rest being a thin shaft (shaftWidth: 0.025). It looks like a stick with a small triangle on top, not a proper arrow.Let me completely rewrite this with better defaults and a cleaner construction — a proper vector arrow like you'd see in design tools:
Cedric 2:45 PM
Completely rebuilt the arrow. Key changes:
Better defaults: shaft width 0.04 (was 0.025), head length 0.12 as absolute value (was 0.35 as proportion of total — confusing), head width 0.1. Should look like a proper arrow out of the box now.
4 styles:
• Chevron (default) — classic arrow with rectangular shaft + triangular head
• Filled — same but fills the gap between shaft and head wings
• Thin — stroke-only: line shaft with chevron tip (uses sdSegment for clean strokes)
• Double — two stacked chevron heads with a shaft
Head length is now absolute, not a proportion. Much more intuitive to control.
Deploying now.
Better defaults: shaft width 0.04 (was 0.025), head length 0.12 as absolute value (was 0.35 as proportion of total — confusing), head width 0.1. Should look like a proper arrow out of the box now.
4 styles:
• Chevron (default) — classic arrow with rectangular shaft + triangular head
• Filled — same but fills the gap between shaft and head wings
• Thin — stroke-only: line shaft with chevron tip (uses sdSegment for clean strokes)
• Double — two stacked chevron heads with a shaft
Head length is now absolute, not a proportion. Much more intuitive to control.
Deploying now.
Page 1 of 24 Older