project-darkwood

Saturday, February 21, 2026
Marian
Marian 2:56 AM
tldr
Cedric
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 — getSdfAsMask was clobbering the active GL program/framebuffer mid-render.
Marian
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
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 mask sourceHandle.
Marian
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
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
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
Cedric 3:05 AM
The compiler returns 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
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.
Marian
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
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
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
Cedric 3:09 AM
Now update the bitmap evaluator to set uconnected* uniforms:
Cedric
Cedric 3:09 AM
Now update the maskBlend node to use auto-detection:
Cedric
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 ${connected:inputName} template any node can use going forward.
Cedric
Cedric 3:09 AM
Building and deploying. Should be live in about a minute.
Marian
Marian 3:10 AM
Good. can we put some background for the transparency to show through? Like the iconic photoshop checkers?
Cedric
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
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
Marian 10:03 AM
SDF arrow sucks. fix it. add more params
image.png image.png
Cedric
Cedric 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
Cedric 10:05 AM
Wait, the glslHelpers has ${helpers} without the backslash escape. Let me fix:
Cedric
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.
Marian
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
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
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 nodessdfAnnular, 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
Cedric 10:52 AM
Good — 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
Cedric 10:52 AM
The 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
Cedric 10:52 AM
These are all generators. The seed param is additive: node's own seed + uBase_seed. Correct.

The only fix needed: exclude colorConstant from universal params.
Cedric
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: 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/sdfIconcustomRender + 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
Marian 10:56 AM
are you sure no operators are sensitive to position/rotation, etc?
Cedric
Cedric 10:56 AM
Good catch. Let me think about this...
Cedric
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 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-dependent
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:

pixelate — grid alignment is position-sensitive
tile — tiling origin matters
halftone — wait, that's already a generator
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?
Marian
Marian 11:16 AM
Good, fix all
Cedric
Cedric 11:18 AM
Let me check exactly which operators are missing position/rotation controls:
Cedric
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 pixelate and tile — these could use offset for grid alignment:
Cedric
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
Cedric 11:18 AM
Now pixelate (add offset) and tile (add offset):
Cedric
Cedric 11:18 AM
Done. Here's what got added:


| 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
Marian 2:00 PM
good, make sure basic params are moddable by params as well
Cedric
Cedric 2:02 PM
Let me check how mod inputs work on operators currently:
Cedric
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
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
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.
Marian
Marian 2:43 PM
arrow still looks very very shitty
Cedric
Cedric 2:45 PM
Let me look at it myself.
Cedric
Cedric 2:45 PM
Let me add an SDF Arrow node to see it:
Cedric
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
Cedric 2:45 PM
I see the problem — the defaults produce a weirdly proportioned arrow. 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
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.