Repository navigation
Conversation
|
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 |
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` the band is never mixed in and rendering is unchanged. - The stroke is a second coverage band read off the same signed edge distance as the fill ramp — the device-pixel distance the edge ramps already use (radius for circles, field over its per-pixel gradient for polygons) — so it anti-aliases exactly like the fill and a 0.5 px stroke means 0.5 px at any point size and zoom, including with `scalePointsOnZoom`. - The band is inset (`d ∈ [-w, 0]`) so the point never grows and the sprite needs no room for it. - 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. 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>
rokotyan
force-pushed
the
feat/point-stroke
branch
from
September 21, 2026 18:39
819bd26 to
43500a0
Compare
…come defaults "Stroke" promises a color you can set, and the first cut fixed it by rule (`pointStrokeMode` / `pointStrokeIntensity`). Making the promise true means the stroke is a per-point channel like color and size: a flat typed array aligned to the point index space, a config default behind it, and `NaN` resolving to that default at read time — the rule colors and sizes already follow. So a few points can carry an explicit stroke while the rest keep the derived shade, in one array. - `setPointStrokeColors(Float32Array)` (RGBA) and `setPointStrokeWidths(Float32Array)` (CSS px), with `NaN` = default. The channels ride the existing transitions: colors join `PointColors`, widths join `PointSizes`, each with a source/target buffer pair. - The config keys are named as the defaults they are, mirroring `pointDefaultColor` / `setPointColors`: `pointDefaultStrokeWidth`, `pointDefaultStrokeColor` — a color used as is, or one of the derivation rules `'auto'` | `'darken'` | `'lighten'` — and `pointStrokeContrast`, the OKLab lightness step the rules use (contrast says what intensity measured). The explicit default color is parsed once per config change in the store, not per frame. - Resolution moves fully into the vertex stage: an explicit per-point color wins, else an explicit default color, else the shade derived from the fill; the resolved color and the width in device pixels reach the fragment as varyings, so the fragment uniform goes away. A greyed-out point always gets the derived shade of its greyed fill, explicit color or not, so the stroke fades with the point instead of staying a bright rim. Story: a per-point overrides toggle strokes one group white and gives another a 2 px stroke through the two setters, leaving the rest on the defaults. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Nikita Rokotyan <nikita@rokotyan.com>
…ange replayed the fill's Toggling per-point stroke overrides flashed the fills of several clusters through old colors. `setPointStrokeColors` queued the shared `PointColors` transition, which turns on `animateColors` for the fill too — and the fill's source buffer holds the colors before the fill's *last* change (the previous layout's), not the current ones, so the fill animated from stale colors to its unchanged target. The same would have happened to strokes on any fill change. Invariant: a transition property is started only by the channel whose source buffer it interpolates. Stroke colors and widths get their own properties (`PointStrokeColors`, `PointStrokeWidths`) and their own animate flags through `setTransitionProgress` and the vertex uniforms; the shader mixes each channel only under its own flag. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Nikita Rokotyan <nikita@rokotyan.com>
…rence the two in the docs The inset stroke and the selection rings (`outlinedPointIndices`, the hover ring, the focused point) are both outline-shaped, so a reader meeting one looks for the other. The new story puts them on one canvas: six overlapping clumps with the stroke on every point, rings on a chosen set in one uniform color, and a highlight toggle that shows the difference under greyout — the greyed points keep a greyed stroke while their ring is skipped. The `outlinedPointIndices` doc now says what it is (a selection mark drawn outside the body in one color) and points to the stroke for a per-point edge color. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Nikita Rokotyan <nikita@rokotyan.com>
This branch has not been deployed
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 edge stroke: a thin inset band along every point's edge, by default in a shade of the point's own color a fixed OKLab lightness step away from the fill, or an explicit color and width per point. Off by default (
pointDefaultStrokeWidth: 0), so existing renders are unchanged.New config keys (
config.ts, defaults invariables.ts, documented inconfiguration.mdx):pointDefaultStrokeWidth0pointDefaultStrokeColor'auto'(per point: darken light points, lighten dark ones),'darken','lighten'.'auto'pointStrokeContrastL(0..1) between fill and derived stroke.0.1Per-point channels, following the
pointDefaultColor/setPointColorspattern (NaN= use the default):Each channel has its own transition property (
PointStrokeColors,PointStrokeWidths), so a stroke change never restarts the fill's transition from its stale source buffer and vice versa.Greyed-out points always get the derived shade of their greyed fill, explicit color or not, so the stroke fades with the point.
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 the default width, contrast and color rule, a light/dark background toggle, and a per-point overrides toggle (one group stroked white, another at 2 px, through the two setters).History entry:
history/2026/2026-09-15-point-stroke.md(commit hash left as TODO until merge).Examples/Points → Stroke & Outline Rings: the stroke next to the existing selection rings (outlinedPointIndices, hover, focus) on six overlapping clumps, with a highlight toggle showing greyed points keeping a greyed stroke while their ring is skipped. TheoutlinedPointIndicesdoc now cross-references the stroke.Screenshots
Default stroke width 0.75 px, contrast 0.10,
'auto'color rule, 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 rules work; per-point overrides (white color on one group, 2 px width on another) render with the rest on defaults; the small-point cutoff shows at the grid's left edge; width0restores the original rendering; no console errors.🤖 Generated with Claude Code