Conversation
Overlapping nodes of one color read as a single blob: nothing marks where one disc ends and the next begins. The common remedy is a thin outline in a slightly darker or lighter version of each node's own fill, which separates neighbours without introducing a second color. The existing outline feature (`outlinedPointIndices`) is a different thing — a selection ring in one uniform color drawn outside the point on a scaled-up sprite — so this adds a rendering-level stroke every point gets, derived from its own color. Contract: with `pointStrokeWidth > 0`, every point whose on-screen diameter is at least the stroke width gets an inset band of that width along its edge, in a color a fixed perceptual lightness step away from its fill; with the default width of `0` rendering is byte-identical to before. - The stroke is a second coverage band read off the same signed distance as the fill, converted to device pixels (`sd * shapeSize / 2`), so it anti-aliases like the fill and a 0.5 px stroke means 0.5 px at any point size and zoom, including with `scalePointsOnZoom`. Every shape SDF has unit gradient for that conversion to be exact; the diamond's L1 form was the one exception and is normalised (its edge does not move). - The band is inset (`d ∈ [-w, 0]`) so the point never grows and the sprite needs no padding; the fill's edge sits half a pixel inside the nominal boundary so the anti-alias band cannot cross the sprite bounds. - Points narrower than the stroke are drawn without one — they would be nothing but stroke. - The stroke color is a step in OKLab lightness (`pointStrokeIntensity`), not a mix toward black or white: a fixed-fraction mix has no headroom on bright saturated fills (a yellow moved ~1% where a blue moved ~10%), while an OKLab step reads as the same contrast on every hue. `'auto'` picks the direction per point from its own lightness so the step always has room; hue is preserved by reducing chroma only where the shifted color leaves the sRGB gamut. The conversions live in a reusable luma `ShaderModule` (`Shared/oklab-module.ts`) and run once per point in the vertex stage, on the greyed color, so greyed points get a matching stroke. - When the stroke is on, the fill's edge is also anti-aliased in pixel space (a constant 1 px band): a pixel-width stroke is invisible inside a size-proportional blur on large points. With the stroke off the original edge code runs unchanged. Storybook: Examples/Points → Point Stroke, with a static size grid and a simulated set of overlapping clusters plus controls for width, lightness step, direction and background. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Nikita Rokotyan <nikita@rokotyan.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an optional per-point edge stroke: a thin inset band along every point's edge, in a shade of the point's own color a fixed OKLab lightness step away from the fill. Off by default (
pointStrokeWidth: 0), so existing renders are unchanged.New config keys (
config.ts, defaults invariables.ts, documented inconfiguration.mdx):pointStrokeWidth0pointStrokeIntensityL(0..1) between fill and stroke.0.1pointStrokeMode'auto'(per point: darken light points, lighten dark ones),'darken','lighten'.'auto'Why OKLab
A fixed-fraction mix toward black/white has no headroom on bright saturated fills: at the same setting a blue's channels moved ~0.1 while a yellow's moved ~0.01, giving ~30% vs ~5% luminance contrast — one cluster visibly outlined, the other not. A step in OKLab
Lreads as the same contrast on every hue. The conversions live in a reusable lumaShaderModule(src/modules/Shared/oklab-module.ts: sRGB ⇄ linear ⇄ OKLab plus a chroma-reducing gamut map that preserves hue) and run once per point in the vertex stage.Rendering
Story
Examples/Points → Point Stroke: a static size grid (exact-pixel sizes, sub-pixel to ~28 px, all eight shapes) and a simulated set of overlapping single-color clusters framed at 2× fit, with controls for width, lightness step, direction and light/dark background.History entry:
history/2026/2026-09-15-point-stroke.md(commit hash left as TODO until merge).Screenshots
Stroke width 0.75 px, lightness step 0.10,
'auto'direction, dark background.Size grid — all eight shapes, sizes from sub-pixel to ~28 px. Points narrower than the stroke (far left) get none.
Overlapping clusters — eight single-color piles after the simulation settles, framed at 2× fit.
Why OKLab, up close — at the same 0.10 step,
'auto'darkens the yellow pile and lightens the blue one, and both outlines read at the same strength. With the earlier fixed-fraction mix toward white the yellow had almost no visible stroke.Testing
npm run lintandnpm run buildpass.'auto'darkens the yellow cluster and lightens the blue one at the same step; explicit modes work; the small-point cutoff shows at the grid's left edge; width0restores the original rendering; no console errors.🤖 Generated with Claude Code