-
Notifications
You must be signed in to change notification settings - Fork 0
ColorTheme Enhancement Roadmap
Tracking document for closing the feature gap between Fracturing Fog's colour system and comparable fractal / procedural-colour tools (Ultra Fractal, Apophysis / Chaotica, Kalles Fraktaler, Mandelbulber, matplotlib colormaps, IQ-style shader palettes).
Status legend: ☐ not started · ◐ in progress · ☑ shipped
The colour system has three surfaces. Any enhancement must state which it touches.
| Surface | Code | Interpolation today |
|---|---|---|
| JSON data-driven themes |
ColorThemeData → DataDrivenColorThemes → GradientColorMap
|
Linear sRGB only, hardcoded |
| ColorGen DSL (CPU + HLSL) |
ColorGenEmitter, ColorGenHlslPrelude, ColorMap.template.cs
|
palette() = linear sRGB cyclic lerp |
| Palette-extraction preview / PDF | Imaging/PaletteExtraction/GradientInterpolation.cs |
sRGB / Lab / OkLab — NOT wired to render |
Key injection points referenced throughout this doc:
-
Engine/Models/ColorUtils.cs-
GradientColorMap.SampleStops()— per-stop segment lerp (feeds the 256-entry LUT). -
GradientColorMap.BuildLut()— builds the LUT once per theme instance. -
GradientColorMap.Map()— computest = smooth / maxIter, clamps, →MapNormalized. -
CyclingGradientColorMap.Map()—t = (smooth * CycleSpeed) % 1. -
MapNormalized()— final LUT sample + lerp → packed ARGB.
-
-
Engine/Models/ColorThemeData.cs— JSON DTO. All new theme fields land here (nullable = back-compat). -
Engine/Models/DataDrivenColorThemes.cs— plumbs DTO fields into the runtime maps. -
Abstractions/Models/ColorStopData.cs— per-stop DTO (position + RGB). -
ColorGen/Parser/ColorGenAst.cs—CgFunctions.Table(builtin registry). -
ColorGen/Emitters/ColorGenEmitter.cs— CPU C# emission (EmitCall). -
ColorGen/Emitters/ColorGenHlslPrelude.cs— GPU HLSL helper prelude. -
ColorGen/Templates/ColorMap.template.cs—Cg3runtime struct (CPU palette/mix/gamma helpers). -
Imaging/PaletteExtraction/GradientInterpolation.cs+ColorSpaces— existing OkLab math (reusable).
Two structural facts constrain the specs:
- The LUT is built once per theme instance (256 entries, byte precision). Interpolation-space and curve changes that only affect LUT construction are effectively free at render time — the per-pixel hot path is unchanged.
-
Map()has no pixel coordinates. Anything needing screen x/y (spatial dithering) requires a signature change across theIColorMapsurface — that is why dithering is ranked high-risk.
Each spec: what, surfaces, data model, algorithm, injection points, back-compat, test.
- What: choose the colour space the gradient blends in. OkLab kills muddy mid-tones between distant hues; HSV-arc gives rainbow sweeps.
-
Surfaces: JSON themes (all four kinds inherit
GradientColorMap). -
Data model:
ColorThemeData.InterpolationSpace(enumSrgb|OkLab|Lab|Hsv, defaultSrgb). -
Algorithm: in
SampleStops, convert the two bracketing stops into the chosen space, lerp, convert back. ReuseColorSpaces.RgbToOkLab/OkLabToRgbfromGradientInterpolation. HSV-arc lerps hue along the shorter arc. -
Injection:
GradientColorMapgains aprotected GradientInterpolationSpace Space(default Srgb);SampleStopsbranches on it;DataDriven*ctors set it from the DTO. LUT already caches the result → zero per-pixel cost. -
Gap to close first:
GradientInterpolation.Mixcurrently fakesLabby delegating to OkLab (see comment atGradientInterpolation.cs:66). Either implement a real Lab inverse inColorSpacesor dropLabfrom the enum for v1. -
Back-compat: null/absent ⇒
Srgb⇒ byte-identical to today. -
Test: golden-image compare; unit test that OkLab midpoint of
#000↔#fffdiffers from sRGB midpoint.
- What: shape of the blend within a segment. Cosine = smooth ease at both stops (classic demo-scene look); cubic (Catmull-Rom / monotone) = spline through stops; step = hard bands.
- Surfaces: JSON themes.
-
Data model:
ColorThemeData.InterpolationCurve(enumLinear|Cosine|CubicMonotone|Step, defaultLinear). -
Algorithm: remap the segment parameter
u:- cosine:
u' = 0.5 - 0.5*cos(pi*u) - step:
u' = 0(hold low stop) - cubic: needs the neighbouring stops (4-point Catmull-Rom), so compute in
BuildLutacross the global stop list, not per-segment.
- cosine:
-
Injection:
SampleStopsfor linear/cosine/step;BuildLutfor cubic (it already walks all 256 samples). -
Back-compat: null/absent ⇒
Linear. -
Test: unit test each curve's
u=0.5output; cubic C1-continuity spot check.
-
What: remap
tbefore palette lookup (Ultra Fractal "transfer function"). Compresses/expands where colour detail lands.logspreads deep detail,sqrtlifts shadows,powis a general knob. - Surfaces: JSON themes (Gradient + Cycling + 3D albedo).
-
Data model:
ColorThemeData.TransferFunction(enumLinear|Sqrt|Cubic|Log|Sine, defaultLinear) +TransferStrength(float, default 1.0). -
Algorithm: apply to
tinGradientColorMap.Map/CyclingGradientColorMap.MapbeforeMapNormalized:- sqrt
t^0.5, cubict^3, loglog(1+k·t)/log(1+k), sine0.5-0.5cos(pi·t).TransferStrengthblends identity↔curve.
- sqrt
-
Injection: the two
Mapoverrides inColorUtils.cs. Also mirror into the ColorGen template'sin_tif desired (optional — DSL users can already write it). -
Back-compat: null/absent ⇒
Linear, strength ignored. -
Test: unit test monotonicity + endpoints (
f(0)=0, f(1)=1) for every curve.
- What: rotate the palette along the iteration axis (offset/phase) and scale how many cycles fit (density). Ultra Fractal "rotation" + "density".
- Surfaces: JSON Cycling / Phong3D / Pbr3D.
-
Data model:
ColorThemeData.ColorOffset(float[0,1), default 0) +ColorDensity(float, default 1.0). -
Algorithm:
t = ((smooth * CycleSpeed * ColorDensity) + ColorOffset) mod 1. (Density multiplies frequency; offset is an additive phase — distinct knobs, both cheap.) -
Injection:
CyclingGradientColorMap.Map(single line change) +DataDriven*ctors. - Back-compat: offset 0 + density 1 ⇒ identical.
- Note: exposes as animatable params later (ties into the scene-engine global-track work).
- Test: offset 0.5 shifts LUT index by 128; density 2 doubles band count.
- What: ping-pong (mirror) removes the hard seam where a cycling palette wraps 1→0. Clamp holds the endpoints.
- Surfaces: JSON Cycling.
-
Data model:
ColorThemeData.WrapMode(enumRepeat|PingPong|Clamp, defaultRepeat). -
Algorithm: after computing raw cyclic
t: pingpong =1 - |1 - 2·frac(t/2)·? |→ use triangle wavetri(t) = 1 - abs(1 - 2*frac(t))... implement asabs(((t mod 2) ) - 1)mapped to [0,1]; clamp =saturate. -
Injection:
CyclingGradientColorMap.Map. -
Back-compat: default
Repeat. - Test: ping-pong at t and (1-t) equal; continuity at seam.
- What: per-theme gamma alongside brightness/contrast/adaptive.
- Surfaces: JSON themes (all) + host post-FX pipeline + UI slider.
-
Data model:
ColorThemeData.Gamma(int?on the same [-100,100] slider scale, orfloat?gamma value — pick one; slider-scale keeps JSON consistent with the other three). ExtendIThemePostFxwithThemeGamma. -
Algorithm:
out = pow(in, 1/gamma)on the linearised RGB in the existing post-FX stage. -
Injection: find the post-FX apply site (search
IThemePostFxconsumers /Adaptiveslider handler inUI.Avalonia), add a gamma stage + slider + lock checkbox mirroring brightness/contrast/adaptive. - Back-compat: null ⇒ no gamma stage.
- Test: gamma-neutral (value 1.0 / slider 0) is identity; round-trip export/import.
- What: Photoshop-style midpoint: move the 50 % blend point within a segment without adding a stop.
- Surfaces: JSON themes.
-
Data model:
ColorStopData.Midpoint(float[0,1], default 0.5) — the bias applied to the segment ending at this stop (or starting; pick and document). -
Algorithm: remap segment
uby a bias/gain curve:u' = u^(log(0.5)/log(mid))(power bias) sou=mid → 0.5. -
Injection:
SampleStops(needs the stop's midpoint;ColorStopvalue type must carry it — add field toColorStopinColorUtils.csandColorStopData). - Back-compat: default 0.5 ⇒ linear.
-
Test:
mid=0.25puts halfway colour at u=0.25.
-
What:
cosine(t, a, b, c, d)(Inigo Quilez 4-vector cosine palette) andpalette_cos/palette_oklabvariants of the existing stop palette. - Surfaces: ColorGen CPU + HLSL.
-
Data model: new builtins in
CgFunctions.Table:-
cosine— 4 Vec3 args (a,b,c,d) + scalar t → Vec3:a + b*cos(tau*(c*t + d)). -
palette_cos,palette_oklab— same variadic signature aspalette.
-
-
Injection:
-
ColorGenAst.cs— register signatures. -
ColorGenParser.cs— same variadic validation branch aspalette. -
ColorGenEmitter.cs— emitCg3.CosinePalette(...)/Cg3.PaletteCos(...). -
ColorMap.template.cs— add theCg3helper methods. -
ColorGenHlslPrelude.cs+ColorGenHlslEmitter.cs— GPU parity helpers (track arity likepalette).
-
- Back-compat: purely additive; existing programs unaffected.
-
Test: CPU/GPU parity harness (the existing multikernel/golden compare pattern);
cosinematches the IQ reference at sampled t.
-
What:
oklab(L,a,b),oklch(L,C,h)constructors +mix_oklab(va,vb,t). - Surfaces: ColorGen CPU + HLSL.
-
Data model: builtins in
CgFunctions.Table(Vec3 ctors + polymorphic mix). -
Injection: same five sites as F8. Port the OkLab matrices from
Imaging/PaletteExtraction/ColorSpacesinto theCg3helper block and the HLSL prelude. - Back-compat: additive.
-
Test:
oklab→RGB round-trips againstColorSpaces; CPU/GPU parity.
- What: RGBA stops; honour interior transparency (docs currently say "transparent not honored").
-
Surfaces: JSON themes + the whole compositing path (
Mapreturns packed ARGB with A=0xFF everywhere today; in-set is always opaque). -
Data model:
ColorStopData.A(byte, default 255) +InSetColorData.A. -
Algorithm: carry alpha through the LUT (LUT would need a 4th lane) and
stop forcing
0xFFinMapNormalized/PackArgb. -
Injection:
ColorUtils.csLUT + pack,IColorMap.InSetColor, and every downstream consumer that assumes opaque (image capture, video, compositor). - Back-compat: default 255 preserves opacity, but the render/export pipeline must be audited for premultiply / opaque assumptions — this is why it is high-risk.
- Test: alpha=128 stop composites over a known background; PNG export keeps alpha.
Audit 2026-07-17 (feature/ui-overhaul). The opaque-ARGB assumption is confirmed baked at two hard sites in
ColorUtils.cs—PackArgb(0xFF000000 | …) andMapNormalized(returns0xFF000000 | …) — plus the two 3D pack points (GradientPhong3DBase,PbrGradient3DBase). The same0xFF/opaque force recurs across ~104 files (grep0xFF000000/PixelFormat/Bgra8888): everyColorSchemes3D/*andColorSchemes/*theme, every calculator, all three GPU renderers (Rendering.D3D,Rendering.Silk,Rendering.Skia), and the whole export/capture/video chain (ImageExport,PosterRenderer,BatchRenderer,SceneVideoRenderer,PngSequenceWriter,FractalOverlayCompositor). Alpha is not a "carry one lane" change — it is a pipeline-wide compositing contract change. Real risk is subtle premultiply / over-composite bugs at any consumer that blends onto a background (overlay compositor, watermark, video frame accumulation) plus PNG-vs-video alpha-handling divergence. Prerequisite before any F10 code: a--colorprobegolden gate so a composited alpha result can be regression- checked across the option matrix. Estimate revised 3 → 5+ ideal-days.
- What: ordered/blue-noise dither on the final 8-bit quantise to kill visible bands in smooth gradients and deep zooms.
- Surfaces: JSON + ColorGen output (final pack), host.
-
Data model: global toggle + optional per-theme
DitherStrength. -
Algorithm: add a threshold from a 8×8 Bayer / blue-noise texture at
(x mod 8, y mod 8)before rounding to byte. -
Injection: requires pixel coordinates in the colour path.
IColorMap.Maphas none — either (a) dither in a post-pass over the rendered ARGB buffer (cleaner; no interface change; where the post-FX stage already iterates pixels), or (b) thread x/y throughMap(invasive). Prefer (a). - Back-compat: off by default.
- Test: flat gradient histogram shows dither noise; SSIM vs undithered stays high.
Audit 2026-07-17 — route (a) is wrong; it cannot deband. Banding is born at exactly one place:
MapNormalized(ColorUtils.cs:449-453) lerps the LUT to a float RGB, then(int)rgb.GetElement(0)truncates to byte. That truncation is the only quantise step, and the float sub-byte precision exists only there. The buffer handed to the post-FX upload pass (FractalRenderHost.UploadProcessedBuffer) is already 8-bit — Map already truncated. Ordered dither on an already-quantised integer is a no-op:floor(V + threshold)withV ∈ ℤandthreshold ∈ [0,1)returnsV. So a post-pass over the rendered ARGB adds patterned noise but recovers zero band detail — the information is gone before the post-pass runs. Route (a) is dropped.To actually kill bands the dither threshold must be added to the float value before the
(int)cast, i.e. inside the colour path. Coordinate delivery without breakingIColorMap.Map's signature:
- (b1) thread-static dither offset — render loops set a
[ThreadStatic]GradientColorMap.DitherOffset = bayer8x8[x&7, y&7] − 0.5fimmediately before eachMapcall;MapNormalizedadds it pre-cast. No interface change, thread-safe (each worker sets its own before use), CPU-only. Contained:MapNormalized+ the two 3D pack points + the CPU render loops.- (b2) explicit x/y overload — new
Map(..., int x, int y); invasive across every calculator + the interface. Rejected (blast radius ≈ F10).- GPU parity is separate. On the GPU path
Mapis never called — colour is packed in HLSL. GPU dither needs acg_dither(col, uv)in the HLSL prelude wired into every GPU final-pack site (Rendering.D3D,Rendering.Silk, ColorGen HLSL emitter). Deep-zoom banding is worst on GPU, so a complete F11 spans CPU and GPU; they can ship in that order.Revised plan: F11a = CPU deband via (b1) (contained, real payoff, off by default), F11b = GPU HLSL dither (separate, wider). A
--colorprobegate should assert histogram-spread on a flat gradient and SSIM parity.
- What: one-click random palette in the Theme Editor (golden-ratio hue walk, or IQ random cosine coefficients) with a reproducible seed.
-
Surfaces: UI only (Theme Editor) — emits ordinary stops or a ColorGen
cosineprogram. -
Data model: none persisted beyond the generated stops (optionally store seed in
Description). -
Injection:
UI.AvaloniaTheme Editor + ColorGen editor "Randomize" button. - Back-compat: additive UI.
- Test: same seed ⇒ same palette.
ROI = user-visible payoff × breadth of themes affected. Risk = blast radius × math/precision/compat hazard. Effort in ideal-days, rough.
| ID | Feature | ROI | Risk | Effort | Score (ROI−Risk) |
|---|---|---|---|---|---|
| F1 | Stop interpolation space (OkLab) | High | Low | 1.5 | ★★★★★ |
| F8 | ColorGen cosine / IQ palette | High | Low | 2 | ★★★★★ |
| F4 | Colour offset/phase + density | High | Low | 1 | ★★★★★ |
| F3 | Transfer function | High | Low-Med | 1.5 | ★★★★☆ |
| F2 | Interpolation curve (cosine/step) | Med-High | Low | 1.5 | ★★★★☆ |
| F5 | Wrap mode (ping-pong) | Med | Low | 0.5 | ★★★★☆ |
| F6 | Palette gamma post-FX | Med | Low-Med | 1 | ★★★☆☆ |
| F9 | ColorGen OkLab/OkLCh | Med | Low-Med | 2 | ★★★☆☆ |
| F7 | Per-stop midpoint/bias | Med | Low | 1 | ★★★☆☆ |
| F12 | Randomize/seed generator | Med | Low | 1 | ★★★☆☆ |
| F10 | Per-stop alpha | Med | High | 3+ | ★★☆☆☆ |
| F11 | Dithering | Med | High | 3+ | ★★☆☆☆ |
Rationale for the two high-risk items:
-
F10 (alpha) touches the opaque-ARGB assumption baked into the LUT, the
packer,
IColorMap.InSetColor, and every export/capture/video consumer. Wide blast radius, easy to ship subtle premultiply bugs. Audit confirmed ~104 files carry the assumption; estimate raised to 5+ days (see F10 note). -
F11 (dither) must add the dither threshold before the float→byte
truncate in
MapNormalized— the post-pass route (a) was found unable to deband (8-bit in = 8-bit out; see F11 note). The contained route is a thread-static offset set by the CPU render loops (F11a); GPU HLSL dither (F11b) is a separate, wider follow-up. F11a reranks toward the middle; full CPU+GPU stays high.
Phase A — cheap high-ROI, no interface changes (target first): ☑ SHIPPED (2026-07-17) ☑ F1 · ☑ F4 · ☑ F5 · ☑ F8
Phase A landed the render-engine + DSL plumbing (no editor UI yet):
-
F1 —
ColorThemeData.InterpolationSpace(Srgb/OkLab/Hsv); OkLab+HSV blend implemented self-contained inGradientColorSpaces(Engine, no PaletteExtraction dependency). Applied at LUT-build inGradientColorMap.SampleStops→ zero per-pixel cost.Labdeferred (needs real inverse). -
F4/F5 —
ColorOffset/ColorDensity/WrapModeonColorThemeData; sharedGradientColorMap.CyclicT(smooth, cycleSpeed)helper consumed byCyclingGradientColorMapand both 3D lit bases (GradientPhong3DBase,PbrGradient3DBase) so every cycling kind honours them. Defaults collapse to the historical((smooth*speed) mod 1)— byte-identical. -
F8 — ColorGen
cosine(t, a, b, c, d)IQ palette builtin: AST signature, CPU emitter (Cg3.Cosine), HLSL emitter (native vector ops, no prelude helper), template runtime helper. Verified: codegen exit 0 + generated C# Roslyn-compiles against Engine + HLSLcos()emitted. - Round-trip: all four JSON fields export via
GradientColorMap.Export*accessors so user themes persist the options. -
Not yet done (follow-up): editor UI controls in
UI.Avalonia/, user-doc worked examples, and a golden-image--colorprobegate.
Rationale: F1/F4/F5 are additive nullable DTO fields + LUT-build or one-line
Map edits, byte-identical when defaulted. F8 is purely additive ColorGen
builtins. All four are independently shippable and each visibly upgrades output.
Phase B — small-surface curves & knobs: ☑ SHIPPED (2026-07-17) ☑ F2 · ☑ F3 · ☑ F7
-
F2 —
ColorThemeData.InterpolationCurve(Linear/Cosine/Cubic/Step). Cosine/Step remap the segment parameter inSampleStops;Cubicis a Catmull-Rom spline through the 4 neighbouring stops in sRGB (SampleCubic). LUT-baked → zero per-pixel cost. -
F3 —
TransferFunction(Linear/Sqrt/Cubic/Log/Sine) +TransferStrength. SharedGradientColorMap.ApplyTransfer(t)called fromGradientColorMap.MapandCyclingGradientColorMap.Map. Every curve fixes f(0)=0/f(1)=1 so cycling seams stay continuous. Not applied to 3D albedo (would move PBR material bands); theInterpCurveLUT effect still applies to 3D albedo. -
F7 —
ColorStopData.Midpoint(+ColorStop.Midpointruntime field, + interop). Power-bias remap inSampleStops(ApplyMidpoint); 0/out-of-range ⇒ 0.5 = linear, so legacy stops are unaffected. - Round-trip via
Export*accessors;FromColorStopnormalises legacy 0→0.5. - Verified: Engine + WinExe build clean; 12/12 runtime invariant checks pass (transfer endpoints, step/cosine/cubic curves, midpoint bias, OkLab≠sRGB, ping-pong seam continuity).
- Not yet done (follow-up): editor UI, user worked-examples.
Phase C — DSL depth + post-FX: ☑ SHIPPED (2026-07-17) ☑ F9 · ☑ F6 · ☑ F12
-
F9 (commit 92c4178) — ColorGen
oklab(L,a,b),oklch(L,C,h)(h in radians),mix_oklab(va,vb,t). Ottosson matrices in theCg3template block- always-emitted HLSL prelude (
sign*pow(abs,1/3)for the missingcbrt). Additive builtins, generic parser path. Verified: generated theme Roslyn-compiles vs Engine + HLSL prelude self-contained with parity call sites.
- always-emitted HLSL prelude (
-
F6 — palette gamma, split in two:
-
Part 1, theme-baked (commit b9a13da):
ColorThemeData.PaletteGamma(float, default 1) baked into the gradient LUT at build (out=in^(1/gamma), all four kinds), editor Gamma slider, Export round-trip. Zero per-pixel cost. Verified: probe 0.5→63 / 1.0→127 / 2.0→180 at mid-grey, round-trips. -
Part 2, live host slider (commit d7b9e36):
ViewState.Gamma[-100,100] + a Post-FX Gamma slider (FloatingMenu → Main → RepaintWithPostFx). 256- entry byte gamma LUT in the upload pass; SIMD fast path kept for the no- gamma case, scalar path when gamma is active.2^(slider/100)exponent. No lock / no theme default (themes use PaletteGamma); the two gammas compound.
-
Part 1, theme-baked (commit b9a13da):
- F12 (commit 5604a85) — editor "Random" button: golden-ratio hue walk, 5 stops, jittered S/V, seed recorded in Description. UI-only, additive.
Editor UI (cross-cutting): all Phase A/B fields wired into the Avalonia Color Theme Editor in commit e33d05a (interp space/curve/transfer + strength, offset/density/wrap, per-stop midpoint), plus the F6/F12 controls above.
Phase D — structural (gate behind explicit sign-off):
☑ --colorprobe gate · ☐ F11a (CPU deband) · ☐ F11b (GPU dither) · ☐ F10 (alpha)
Pipeline audit done 2026-07-17 (see the F10 / F11 notes above); it overturned two assumptions the original phasing rested on, so the plan is re-sequenced:
-
Prerequisite —
--colorprobegolden gate. ☑ SHIPPED 2026-07-17. Mirrors--kifsprobe/--inputprobebut is a true GATE (non-zero exit on drift, for CI).Engine/Diagnostics/ColorProbe.cs: sweeps the 21-config Gradient+Cycling option matrix (F1-F9/F12) throughDataDrivenColorThemes.Create→IColorMap.Map, SHA-256 over sampled ARGB, compares to an embedded golden digest.--colorprobe(gate) /--colorprobe regen(re-pin after an intended change) /--colorprobe verbose. Per-config table dumped tocolorprobe.outto localise drift. 3D (needs normals) + ColorGen (separate codegen) out of scope — the shared quantise point (MapNormalized) is fully exercised via Gradient/Cycling. Nothing structural lands without this, because both remaining features change pixel values in ways only a golden diff catches. -
F11a — CPU deband (lowest risk, real payoff). Add the ordered-dither
threshold before the
(int)truncate inMapNormalized, coord delivered by a[ThreadStatic]offset the CPU render loops set per pixel. NoIColorMapchange. Off by default; per-themeDitherStrength+ global toggle. The roadmap's original "cheap post-pass over the ARGB buffer" route is rejected — it is provably a no-op on already-8-bit data. -
F11b — GPU HLSL dither (separate, wider).
cg_dither(col, uv)in the HLSL prelude wired into every GPU final-pack site. Deep-zoom banding is worst on the GPU path, so F11 is only "done" once this ships, but it is a distinct unit of work behind its own sign-off. -
F10 — per-stop alpha (highest blast radius, do last). ~104 files carry
the opaque-ARGB assumption; it is a compositing-contract change, not a
lane-add. Needs the
--colorprobegate plus a premultiply audit of every export/capture/video consumer first.
-
Editor UI: each JSON field needs a control in the Avalonia Color Theme
Editor (
UI.Avalonia/). Per project rules, all new UI goes to Avalonia only; WinForms editor stays frozen. -
JSON schema doc: update
Docs/User/ColorThemeEditor-Guide.md§14 andDocs/User/ColorGen-UserGuide.md§2.6 / reference card as features land. - GPU parity: every ColorGen builtin (F8/F9) must emit both CPU C# and HLSL and pass the existing CPU/GPU golden-compare harness.
- Tunable-params preference: matches the project's standing preference to expose hardcoded constants as fields — these specs follow it.
-
Colourblind default: any new error/validation UI in the editor uses
#FFCC00, not red.
Color Theme Enhancement Roadmap · Fracturing Fog · created 2026-07-17