brandmark-choreography

star 0

Scroll-driven journey for the v7 brandmark (continuous transform model, ADR-013). The brandmark is a single evolving point cloud whose position, scale, rotation, density, and dispersion are continuous functions of scrollY. One painter end-to-end. Activates on edits to `lib/brandmark/journey.ts`, `lib/stores/brandmarkJourneyStore.ts`, `components/landing/v7/hooks/useBrandmarkJourney.ts`, the keyframe table, the substrate-window math, the journey transform schema, or any change that touches the brandmark's per-frame state.

thoughtform-co By thoughtform-co schedule Updated 6/7/2026

name: brandmark-choreography description: > Scroll-driven journey for the v7 brandmark (continuous transform model, ADR-013). The brandmark is a single evolving point cloud whose position, scale, rotation, density, and dispersion are continuous functions of scrollY. One painter end-to-end. Activates on edits to lib/brandmark/journey.ts, lib/stores/brandmarkJourneyStore.ts, components/landing/v7/hooks/useBrandmarkJourney.ts, the keyframe table, the substrate-window math, the journey transform schema, or any change that touches the brandmark's per-frame state.

Brandmark journey (continuous transform model)

The v7 brandmark journey is a single continuous transform computed every scroll frame:

scrollY  →  computeBrandmarkTransform(scrollY, keyframes, ctx)  →  BrandmarkTransform

Four painters read the transform every frame, all subscribing imperatively to the same store:

  • BrandmarkVectorActor (ADR-015) paints the BRANDMARK SHAPE as crisp inline SVG at sigil (Thoughtform) rest only. Two stacked glyphs (full + ring) crossfade via transform.shapeBlend. Rotation is honest CSS perspective() rotateY(). Fades out geometrically as silhouetteMorph ramps (ADR-019).
  • BrandmarkSilhouettePoints (ADR-019) paints the brandmark as a silhouette point cloud from the sigil → miss transit onward, and stays at every park from Diagnostic through Orbit. Gated by transform.silhouetteMorph; suppressed inside the substrate window.
  • SubstrateMorphPoints (ADR-017) paints the brandmark → sphere morph inside the substrate window. Lives in the intelligence-layer R3F canvas, not the global one.
  • BrandmarkParticleStation (atmosphere field) paints luminous gold dust around the active mark painter. Damped while silhouetteMorph > 0 so the silhouette reads cleanly.

The R3F intelligence-layer scene (OrbitField) reads the same transform for its side-orbit emerge envelopes; the new CelestialLinework overlay adds hairline guide ring + bearing ticks + cardinal diamonds driven by --ilayer-progress. In SVG-fallback mode (reduced motion or no WebGL), useBrandmarkJourney pins the legacy BrandmarkActor to the transform's rect and writes data-brand-on-*="parked" attributes so native dock SVGs paint via CSS gates.

Canonical records:

  • ADR-015 — vector-first split (current default for sigil).
  • ADR-017substrateMorph channel + substrate-sphere morph mesh.
  • ADR-018 — depth corridor + brandmark accretion shell (shell-into-corridor: inside-out reconstruction of the intelligence-layer shell artifact around the travelling mark).
  • ADR-019silhouetteMorph channel + global silhouette point cloud (Diagnostic onward).

Predecessor (journey contract retained): ADR-013. Related (rendering): brandmark-particle. Related (compositing): ADR-008, landing-v7-compositing skill.


Five design principles (load-bearing — every change is reviewed against these)

  1. The brandmark is a single CONTINUOUS artifact that EVOLVES. Position, scale, rotation, density, dispersion are continuous functions of scrollY.
  2. No opacity fades for the brandmark cloud mid-journey. Every transition is geometric. Hero entry + post-orbit exit are the only opacity bookends.
  3. No crossfades between painters. One painter end-to-end. Boundary swaps are forbidden by construction. (ADR-017 corollary: when a renderer swap is unavoidable — vector → particle morph at substrate engage — it is an INSTANT visibility cut under matching particle cover, never an opacity ramp. The particles must already paint the same silhouette at the same screen position the moment the cut fires.)
  4. Decorations EMERGE geometrically, not via opacity. Encode ring, sub-orbits, halo dots all use group.scale.setScalar(splitEmerge(progress)) — material opacity stays constant.
  5. Hero entrance and post-orbit exit are the only bookends. They may use opacity ramps because there is nothing to evolve from / into.

If a fix tempts you to add an opacity fade mid-journey, an attribute swap, or a separate painter for a special case — re-read the principles and find a geometric solution instead.


Keyframe schema

Five keyframes, declared in lib/brandmark/journey.ts:

sigil → miss → substrate → rail → orbit

Each keyframe:

| Field | Purpose | | ------------- | ----------------------------------------------------------------------------------------- | ------ | ----------- | ------ | --------- | | id | Stable identifier ("sigil" | "miss" | "substrate" | "rail" | "orbit") | | resolveRect | Closure that returns the live DOMRect for this keyframe's anchor (re-read every frame) | | parkFracIn | Fraction of the inbound segment that counts as "parked at this keyframe" (default 0.32) | | parkFracOut | Fraction of the outbound segment that counts as "still parked here" (default 0.32) | | parked | { density, dispersion, ringsActive? } — what the painter reads while parked | | transitIn | Per-arrival override for dispersionBump (null = no bump) and easing |

Keyframe configuration today (ADR-015 — atmosphere-field tuning + ADR-017 — substrate morph):

Keyframe Parked density Parked dispersion Rings substrateMorph transitIn.dispersionBump transitIn.easing
sigil 0 0 off 0 (no inbound segment — first keyframe)
miss 0 0 off 0 default sin(πt) * 0.45 (atmospheric same-size) TRAVEL_EASE
substrate 0.15 0.35 on 0 → 1 → 0 (sym.) sin(πt) * 0.35 exhaust MORPH_EASE
rail 0 0 off 0 sin(πt) * 0.35 exhaust MORPH_EASE
orbit 0 0 off 0 sin(πt) * 0.20 exhaust TRAVEL_EASE

substrateMorph (ADR-017) is the symmetric trapezoid envelope (MORPH_EASE, SUBSTRATE_MORPH_FRAC = 0.35) that drives the substrate-sphere R3F point cloud's morph from brandmark shape → Fibonacci sphere → brandmark shape across the substrate scroll window. The vector actor + portal'd substrate dock glyphs are visibility-cut OFF whenever substrateMorph > 0.001 (instant — particles cover the same silhouette).

The vector actor owns the brandmark shape at every keyframe — these densities tune the ATMOSPHERE FIELD around it. Full-mark stations (sigil / miss / rail / orbit) get density 0 so the vector mark sits alone, crisp, no halo. The substrate hold beat gets ambient dust + cardinal diamonds + hairline guide ring (via CelestialLinework) for the celestial-editor read. Transit dispersion bumps are RESTORED on every leg as exhaust around the moving vector — this is the visual story now, no longer a coherence threat as it was under ADR-013.

Easing semantics: TRAVEL_EASE is smoothstep(t) — a gentle S-curve with brisk mid-range velocity, so the brandmark visibly leaves each dock instead of hanging on the flat tail of power3.inOut. MORPH_EASE is smootherstep(t) — even gentler ends, used on size-changing arrivals so the rect grow / shrink reads as an elegant settle. Same-size translations (sigil → miss, rail → orbit) get TRAVEL_EASE; size-changing arrivals (miss → substrate, substrate → rail) get MORPH_EASE.

Per-leg travel windows: sigil → miss is gated on #definition's reading-zone exit (the brandmark stays section-locked while the visitor is reading the Thoughtform definition). All other legs use centre-to-centre + parkFrac. The rail → orbit leg further re-bases its span on practice.top for the sticky special case. Helper: resolveLegTravelWindow in lib/brandmark/journey.ts.

Substrate shape-blend: SHAPE_BLEND_FRAC = 0.30 (was 0.18 before the speed-ramp pass) so the full → ring morph spans the first 30% of the substrate window, holds through the read beat, and retracts in the last 30%. The blend curve uses MORPH_EASE so the morph reads as a continuous evolve rather than a linear flip.


The journey transform

interface BrandmarkTransform {
  rect: { left: number; top: number; width: number; height: number };
  opacity: number; // 0 only at hero / post-orbit bookends (Principle 2)
  density: number; // continuous
  dispersion: number; // continuous (+ per-arrival bump if defined)
  rotationY: number; // radians — non-zero only inside substrate window
  ringsActive: boolean; // true only while parked at substrate
  ringProgress: number; // 0..1 inside substrate window; drives R3F envelopes
  shapeBlend: number; // 0..1 — full → ring topology blend (substrate window)
  vectorOpacity: number; // 0..1 — legacy HANDOFF ramp (under particle cover post-ADR-017)
  substrateMorph: number; // 0..1 (ADR-017) — substrate-sphere point cloud morph
  visible: boolean; // false only at hero / post-orbit-fade-end
  parkedAt: KeyframeId | null; // current parked station; null in transit
}

opacity is the contract that enforces Principle 2: in the painter it's the uOpacity uniform; in any code reviewing this skill, an opacity change at any value of scrollY between c[sigil] + 0 and c[orbit] - 0 is a regression. The painter MUST keep the brandmark cloud at full opacity throughout.


Substrate window — single source of truth for R3F

The substrate-parked scroll window is computed by computeSubstrateRange(keyframes, centres):

engageY = c[miss] + (1 - parkFracIn-substrate) * (c[substrate] - c[miss])
exitY   = c[substrate] + parkFracOut-substrate * (c[rail] - c[substrate])

Inside [engageY, exitY]:

  • transform.ringsActive = true
  • transform.ringProgress = (scrollY - engageY) / (exitY - engageY)
  • transform.rotationY = splitRotation(ringProgress) — drives BOTH the global painter's 2D squash AND the R3F parent group's true 3D rotation.y

At ringProgress = 0 and ringProgress = 1 the rotation is 0 (axis-aligned). Decorations are at scale 0 (geometrically absent). Both endpoints are clean visual swaps from the surrounding transit beats.


Pre-merge checklist (regression invariants)

Match each item to the principle it enforces. Run the dev parity log ([brandmarkJourney] console.debug every 30 frames) and scroll through the page.

  • Hero (scrollY < 4)transform.visible === false. No painter renders.
  • Sigil entrance — opacity ramps 0 → 1 across FADE_IN_FRAC * vh. Hero bookend; only opacity write outside the orbit fade-out (Principle 5).
  • Sigil parkedparkedAt === "sigil"; rect matches sigil dock; no rotation; no rings.
  • Sigil → miss transitparkedAt === null; rect lerps; dispersion ramps up (default bump = atmosphere — sigil → miss is same-size and the bump IS the visual story).
  • Miss parkedparkedAt === "miss"; rect matches the centre of the 4-card grid.
  • Miss → substrate transit (CRITICAL) — rect lerps from ~144 px to ~280 px+; dispersion stays at 0 throughout (Principle 2: cloud must stay coherent through growth). NEVER allow a dispersion bump on this leg.
  • Substrate parkedparkedAt === "substrate"; ringsActive === true; ringProgress ramps 0 → 1 over the parked scroll window. Rotation envelope plays per splitRotation. Decorations (encode ring, sub-orbits, halo) emerge via geometric scale 0 → 1 across ringProgress ∈ [0, 0.08] (Principle 4 — NEVER opacity).
  • Substrate → rail transit (CRITICAL) — rect lerps from ~280 px+ down to ~56 px; dispersion stays at 0. Same coherence requirement as miss → substrate.
  • Rail parkedparkedAt === "rail"; rect matches the rail dock.
  • Rail → orbit transit — uses the practice.top non-sticky reference inside computeBaseTransform to handle sticky-orbit math.
  • Orbit parkedparkedAt === "orbit"; rect matches the orbit dock.
  • Post-orbit fade-out — opacity ramps 1 → 0 across FADE_OUT_FRAC * vh. Post-orbit bookend; only opacity write outside the sigil entrance (Principle 5).
  • Singleton checkbrandmarkSingletonCheck reports at most one painter visible at any scroll position.

Common edits and where they live

Want to change File
Add a new keyframe / station lib/brandmark/journey.ts (buildKeyframes) + the painter's CSS dock anchor (if SVG fallback)
Tune park dwell on a keyframe lib/brandmark/journey.ts (parkFracIn / parkFracOut on that keyframe)
Tune dispersion bump on an arrival lib/brandmark/journey.ts (transitIn.dispersionBump on the destination keyframe)
Tune substrate rotation envelope components/landing/v7/intelligence-layer/intelligenceLayerGeom.ts (splitRotation)
Tune ring extrude envelope same file (splitExtrude)
Tune decoration emerge envelope same file (splitEmerge) — geometric scale 0 → 1, NOT opacity
Tune entrance / fade-out band widths lib/brandmark/journey.ts (FADE_IN_FRAC / FADE_OUT_FRAC)
Change the 2D squash math (shader rotation) components/brand/BrandmarkParticleField/shaders.ts (uRotationY block, SHEAR_SCALE)
Add per-frame side effects (CSS gates etc.) components/landing/v7/hooks/useBrandmarkJourney.ts — SVG-mode block (particle mode is silent)

Home-v2 corridor (ADR-018) — accretion shell

The depth corridor's brandmark accretes the intelligence-layer shell artifact around it inside-out as it travels Navigate → Encode → Build. The shell lives in components/landing/home-v2/DepthGatewayScene/shell/ and is composed by BrandmarkAccretionShell (which tracks getBrandmarkWorldPosition(paintProgress) per frame so the whole shell follows the mark).

⚠️ Current handoff model — hybrid SVG-rest + matched-pixel handoff (2026-06-25, ADR-023). The corridor brandmark is two painters with an invisible seam, not one. ProjectedBrandmarkActor owns the rest band [0, BRANDMARK_CORE_HANDOFF_PROGRESS] (0.30) at full opacity — instant display: block, never an opacity fade. The instant progress crosses the threshold from below, the SVG cuts to display: none in the SAME frame BrandmarkPhysicsCoreActor rasterises the SVG's live screen rect (rasterizeBrandmarkToWorldPositions + worldPositionsToLocal + sim.reseed + sim.setHomePositions) into world positions that reproject to those exact CSS pixels. The eye sees no swap because the particles ARE the SVG at frame N+1. From there the vertex shader interpolates each particle from its matched-pixel seed to its paired aTarget3D (the GLB volumetric wireframe sampled from /models/brandmark/brandmark.glb via sampleBrandmark3D — the SAME mesh the #services hologram uses) with per-particle staggered flowT, +Z recede peaking mid-flow, and low-amplitude tangent + asymmetric XY drift. By uDepth = 1 (at substrate wrap peak, progress = 0.42) every particle has settled exactly on its wireframe home inside the substrate sphere. The two painters share a brandmarkScreenRectRef module — the SVG writes its rect every paint frame; the physics-core actor reads it at the swap frame. The coverMorph and uStream machinery in the shader are retired (the actor pins them to identity values); the wind-blown shader morph subsumes both. The paragraphs below describe SUPERSEDED single-painter / async-flow / cover-in / renderer-ownership / DOLLY_HOLD_END models — read them as HISTORY only; the live contract is this hybrid note + ADR-023 Invariants 1, 3, 13, 14.

Seam verification (mandatory before any merge that touches the handoff): scrub progress by 0.005 across [BRANDMARK_CORE_HANDOFF_PROGRESS - 0.02, BRANDMARK_CORE_HANDOFF_PROGRESS + 0.02] (lab via preview_eval window.scrollY = ... to put the corridor at that paint-progress). Screenshot each step; successive frames in the silhouette region must differ by <3% pixel intensity. If any frame shows a dimmer, sparser, or misaligned mark, the matched-pixel rasterise is producing wrong world coords (most likely a stale brandmarkScreenRectRef or the group's transform not yet matched at the swap frame — see BrandmarkPhysicsCoreActor swap block, which sets group position/scale BEFORE worldToLocal). Layout above the corridor lazy-loads, so re-read stageTop and confirm stable right before each screenshot.

⚠️ Harmonization with #services (2026-06-25 follow-up). The SETTLED in-sphere mark reads as a crisp, airy, Services-colored wireframe (not soft orange dust) so the corridor→Services transition is continuation, not a swap. Driven by wireCrisp = smoothstep(0.7, 1.0, uDepth) (vertex→vWireCrisp): tightens dots (max(uCleanField, vWireCrisp) on the edge), stills twinkle/pulse, shrinks specks (*= mix(1.0, 0.82, vWireCrisp)), lerps body+accent from the flat SVG gold toward uLandedColor/uLandedAccent (#b08b42/#dcc176 = ServicesStage), and trims the additive alpha. wireCrisp = 0 at the flat rest + flight keeps the matched-pixel handoff color-seamless and the fly-in luminous. Production #services is frontal (restTiltX/Y = 0 in ServicesStage.tsx) to match the head-on in-sphere mark; the lab keeps the 3/4 default. Keep both frontal (the substrate sphere's vertical gimbal orbit is rolled to the mark's frontal sword via BRANDMARK_SWORD_TILT_RAD). New BrandmarkPhysicsCore props landedColor/landedAccent default to color/accentColor, so the lab + other consumers are unchanged. See ADR-023 harmonization follow-up + ADR-025 Update 7.

SVG → physics core handoff (2026-06-16, ADR-023): the corridor brandmark is a luminous 3D GPGPU particle core for the entire span Navigate → Encode → Build. The crisp DOM SVG (ProjectedBrandmarkActor) holds at the section-2 Thoughtform rest, then MORPHS into BrandmarkPhysicsCoreActor. MORPH, not crossfade (2026-06-17, supersedes the earlier swirl-and-crossfade model): the SVG and the particle core are the SAME mark — so the swap is a morph, never an opacity crossfade between two different-looking things. Two channels, both anchored at DOLLY_HOLD_END: (1) a near-instant cut (CORRIDOR_HANDOFF_CUT_WIDTH ≈ 0.012, imported by BOTH actors from corridorMap) where the SVG fades to 0 while the core fades in held FLAT (uDepth ≈ 0 paints the EXACT same 2D silhouette at the same screen position — the swap is invisible); then (2) a wider extrude (DEPTH_MORPH_WIDTH ≈ 0.05) where depth ramps 0 → 1 and the core's uDepth uniform extrudes the flat silhouette into the 3D dome. No swirl: igniteRef is pinned to ASSEMBLED_IGNITE (1) and BrandmarkPhysicsCore is seedAtHome, so the cloud is the brandmark silhouette from frame one — the dome/jitter only ever live in Z, so the XY silhouette is preserved at every uDepth (the morph is Z-only). The brief explicitly rejected both the mismatched crossfade AND a swirl behind the present 2D mark; do not reintroduce either. The core sits inside the accretion shell as the bright centre of the intelligence-layer artifact; it scales each frame to the same base sphere half-extent as the shell linework that wraps it. Reveal brightness + speck size: CORE_OPACITY (≈0.95, solid so the flat silhouette reads as densely as the SVG) reveals over the cut; CSS point size eases CORE_POINT_SIZE_FLAT → CORE_POINT_SIZE_3D (≈3.0 → 4.0px) with depth — driven via the opacityRef / pointSizeRef / depthRef props on BrandmarkPhysicsCore. The DEFAULT_OPACITY / DEFAULT_POINT_SIZE_PX constants are the lab baseline only. Epilogue planet-grow sync (2026-06-16): while non-docked epilogue is particle-owned, BrandmarkPhysicsCoreActor multiplies the core scale by getEpiloguePlanetScale(getSmoothedEpilogueProgress()) — the exact clock/multiplier used by BrandmarkAccretionShell for the sphere. Do not remove this: otherwise the core appears slightly delayed / left behind as the sphere grows into the "Everyone is racing..." title section. Corridor → epilogue handoff (2026-06-16): the welded DOM SVG must stay OFF during non-docked epilogue / Build exit. The previous corridorFade = 1 branch at epilogueProgress > 1e-3 made the 2D mark appear over the particle artifact as the visitor exited Build; the follow-up cross-dissolve still showed the SVG too early. Current rule: ProjectedBrandmarkActor returns corridorFade = 0 for useEpilogueOverride && !docked, and BrandmarkPhysicsCoreActor keeps the core opacity at its fly-in value until t.docked. The SVG returns only once the later dock / #services handoff owns the mark (docked ? 1 : 0), preserving Services re-centre + CorridorSeamPixelField while keeping the Build exit purely particle/canvas-owned. Mobile / no-WebGL2 fall back to a static home-position render of the same 3D point cloud (no compute pass); reduced-motion / corridorCapable() === false keeps the existing static-text fallback (canvas not mounted). Single-painter rule: outside the cut band and before dock, exactly one of {SVG, particle core} is visible — the particle core. See ADR-023.

This reverses the earlier 2026-06-06 "stay 2D SVG throughout" decision recorded in ProjectedBrandmarkActor.tsx — that decision was correct in removing a Build-only SVG → particle SPHERE morph, but it left the corridor without a visceral "you are flying into something alive" cue. The current handoff keeps the mark a MARK throughout, only changing its medium.

Subtle matrix glitch on the handoff (2026-06-17): a small, in-palette glitch accompanies the 2D → 3D extrude — the soft-halo gold look is otherwise unchanged (an LED-pixel restyle was prototyped and rejected; the user kept the original soft-halo particles). BrandmarkPhysicsCoreActor drives a uGlitch BELL (Math.sin(t·π)) across [DOLLY_HOLD_END, DOLLY_HOLD_END + GLITCH_BAND_WIDTH] (0.05, in corridorMap.ts, aligned with DEPTH_MORPH_WIDTH). The bell is exactly 0 at both ends, so the cloud is byte-stable outside the handoff and peaks mid-band as the mark gains depth. The shader (components/brand/BrandmarkPhysicsCore/shaders.ts) gates two gentle effects on uGlitch: a VERTEX scanline-band horizontal tear (0.022 normalised, ~10% of bands amplified as "tear" bands) and an in-palette FRAGMENT hue warble toward the dawn accent + brightness flicker. Keep amplitudes small — "subtle and integrated", not a harsh tear. Precision gotcha: uGlitch + uTime are shared between vertex and fragment; both are declared mediump in the vertex to match the fragment's precision mediump float. A precision mismatch silently fails to link the WebGL2 program and the canvas goes blank — do not add a shared float uniform at differing precision. See ADR-023 Invariants 8-9.

Z-stream momentum on corridor entry (2026-06-17): so the mark doesn't just "float" as you enter the corridor, particles stream toward the background (local −Z, toward the substrate sphere) for a flying-into-depth momentum. A vertex-only uStream uniform pushes particles back: a base component (whole silhouette) + a pow(aLuma, 1.3) seed-varied component (comet-tail per particle). BrandmarkPhysicsCoreActor drives uStream from a VELOCITY-modulated envelope gated to [DOLLY_HOLD_END, SIZE_MERGE_END] (entry → Navigate park): faster scroll trails further (momentum), with a STREAM_VEL_BASE baseline so it reads on a slow scroll, eased on wall-clock time. It fades to 0 by the park so the silhouette settles clean inside the sphere. Critical: the actor NEVER moves the group center (always getBrandmarkWorldPosition) — the stream is purely in-shader Z, so the brandmark stays aligned with the sphere/shell at every frame. Tune knobs: STREAM_MAX, STREAM_VEL_SCALE, STREAM_VEL_BASE in the actor. Calm-down (2026-06-24 "crosshair unfurls" pass): STREAM_VEL_BASE was dropped 0.45 → 0.10. With the late handoff the bold mark is on-screen and COHERENT through the wrap; the old high baseline smeared a settled/slow-scroll view into a radial comet-burst (it was only ever masked by the on-clock sphere). The velocity term is intact, so a fast scroll still trails. See ADR-023 Invariant 10.

Services centerpiece treatment (2026-06-22, ADR-023): at the parked #services centerpiece (recT → 1) the WebGL core is the sole DESKTOP mark — the 2D ServicesBrandmarkField is the mobile / reduced-motion fallback, hidden on desktop-with-motion by a services.css media query (no desktop double-paint). There the core is tuned as a calm, airy, 3D background element, all gated by the existing uCleanField / recT dial (0 across the whole corridor + sphere → byte-identical there). Knobs: CLEAN_FIELD_FORCE_FLOOR (BrandmarkPhysicsCore.tsx — damps the sim's turbulence + flow to 0 while returnStrength holds home, killing the per-particle wobble; the sim is NEVER paused there); CLEAN_FIELD_KEEP (shaders.ts vertex — a uCleanField-gated aLuma rank-clip thinning the DRAW ~40% for the airy "fine points, clear gaps" read WITHOUT touching the 3600 count); depthRef = depth (actor — keeps the 3D dome, no longer flattened); CENTER_DRIFT_AMP_* / CENTER_DRIFT_PERIOD_* (actor — a gentle ≤ ~12° sinusoidal tilt that reveals the dome, never a full edge-on spin); CENTER_OPACITY_MUL (actor — recedes the mark behind the copy). Density baseline is 3600 (free GPU compute — the sim already allocates a 64×64 texture). Tune the centerpiece dot size / falloff via the uCleanField mix(...) literals in shaders.ts. See ADR-023 Invariant 11.

Lab-match composition (2026-06-05): the substrate cage is a clean gold geodesic icosphere (buildGeodesicEdges(SUBSTRATE_CAGE_RADIUS, 1) = 80 fine triangular faces) + a fainter dawn inner geodesic, matching the standalone NestedShellSphere's outer + inner shells exactly. It emerges as ONE CLEAN BODY via splitEmerge(reveal) (no per-face petals — they would have read busy at the corridor's parked viewing distance). The previous dodecahedron + per-face petal decomposition was removed.

Current production substrate (2026-06-07, ADR-018): the Navigate substrate is the migrated Thoughtform compass — 4 concentric rings (gold/dawn + dash) sized to the opening-beat compass, bearing crosshair + ticks, cardinal markers, and atmosphere orbit dots. The whole compass is flat / camera-facing with a slow Z breath. It deploys via an organic staggered unfold (per-part foldEmerge + petalStagger, outer ring → inner → reticle) at full opacity, not an opacity fade. The earlier eight-ball horizon / gimbal-tilt attitude read was removed (the tilting ellipses competed with the flat compass). The gold geodesic icosphere (2026-06-06) is retired. Opening beat (ThoughtformCompassGate) now renders nested square portal loops instead of circles — same flythrough behaviour.

Gyroscope substrate (PRODUCTION since 2026-06-08): the 3D gimbal globe ShellSubstrateGyro is the live Navigate/Encode/Build substrate — gyroLabStore.enabled defaults to true, so BrandmarkAccretionShell mounts the gyro on production home + /test/home-v2; /test/navigate-gyroscope adds the GyroLabPanel tuning overlay. (The flat ShellSubstrate compass remains in code as a fallback — set enabled = false to restore it.) It renders a wireframe attitude globe (meridians + parallels + equator), three gimbal rings, a dotted surface shell, and a smoky occluder core. Constants: SUBSTRATE_GYRO_* in shell/shellGeom.ts; tuning panel home-v2/lab/GyroLabPanel.tsx. The particle haze is a substrate-layer decoration (not a brandmark silhouette painter; does not count against the brandmark-particle cap).

Vertical orbit ↔ brandmark spine alignment (2026-06-17): the gimbal cage's VERTICAL orbit (SUBSTRATE_GYRO_GIMBAL_RINGS[2], the ring whose great circle projects to an on-screen vertical line) is rolled by BRANDMARK_SWORD_TILT_RAD off true vertical and held STATIC (spin: 0) so it traces the brandmark core's vertical "sword" stroke — the sphere then reads as a 3D extrusion of the 2D mark. BRANDMARK_SWORD_TILT_RAD (≈10.4°) is sourced from the canonical SVG endpoints (top vertex (254.35, 0) → bottom (179.17, 408.81)) in shellGeom.ts, so it stays correct if the mark is re-exported. The other two rings keep counter-rotating for instrument life. The alignment holds at the parked beats (assembly bank ≈ 0); during banked fly-through the static ring and the (un-banked) core can diverge slightly — acceptable, since the read that matters is the parked composition.

Trim-path draw-on (2026-06-09): the substrate gimbal sphere now reveals via per-element geometry.setDrawRange sweeps inside ShellSubstrateGyro, not via material opacity (Principle 4: decorations emerge geometrically). Closed circles use <line> (LINE_STRIP) instead of <lineLoop> so the trim shows; the geometries already have first-vertex == last-vertex so the visual closure at full draw is identical. Each ring rides its gyroRingUnfold(reveal, idx).tiltT so the line draws on as it tilts open; meridians + parallels each ride their own petalStagger (overlap 0.7) for cascade; per-ring graduations and tick stripes ride the same per-ring stagger; pivot diamonds geometric-scale-in over the last 30% of their ring's tiltT. gyroAssemblyUnfold(...).presence was shrunk from 0.4 → 0.08 of the reveal — opacity is anti-pop only; the trim does the storytelling.

Corridor entry-fly gate on pointer bank (2026-06-09): BrandmarkAccretionShell multiplies pointer / drift / static-tilt all by smoothstep(DOLLY_HOLD_END, DOLLY_HOLD_END + 0.06, paintProgress) before writing gyroTilt. The brandmark and the canvas assembly both stay axis-aligned at the parked Thoughtform read (section 2) and only acquire mouse/idle bank once the camera is actually flying through the 3D corridor. Stacks compose with the existing mouseCalm (Encode floor) and planetCalm (epilogue APPROACH).

<threeLine> r3f registration (2026-06-10): the trim-path lines render through <threeLine> (the r3f TypeScript alias for THREE.Line, used to dodge the <line> SVG-intrinsic collision). The auto-extend(THREE) inside <Canvas> registers THREE under its own keys (Line), not under r3f's typed alias (ThreeLine), so the runtime threw R3F: ThreeLine is not part of the THREE namespace whenever the gimbal first revealed. ShellSubstrateGyro now calls extend({ ThreeLine: THREE.Line }) once at module load. Don't remove this — every closed line in the substrate sphere depends on it.

Navigate apparent-size parity (2026-06-10): the camera parks 7.9 world units from the brandmark at Navigate and ~6.1 at Encode, so the same gimbal sphere reads ~29% smaller on screen at Navigate. getNavigateApparentSizeBoost(paintProgress) (in sceneGeom.ts) returns a `1.30multiplier across[0.30, 0.52]of paint progress, applied multiplicatively to the gyro assembly scale inBrandmarkAccretionShellAND mirrored ingyroAssemblyWorldPositionso DOM cardinal/group labels stay welded. Returns 1.0 outside the window — Encode/Build are byte-identical. Combined withsubstrate.peakAt` 0.48 → 0.42 the unfold is essentially complete at the Navigate park.

Cardinal label depth cue (2026-06-10): gateEncodePrimitive in sceneGeom.ts computes each cardinal's rotated Z (via rotateGyroLocalOffset on the same local offset the canvas anchor uses) and dims/scales the chip when the cardinal swings to the back of the sphere. The new label markup in CopyAnchors.tsx is marker + leader + frame instead of a heavy black box — the diamond marker pins the label to the cardinal node, the thin gold leader bridges to the hairline-frame chip. Stack item labels in the same file are chip + leader with a numeric index prefix (01_, 02_, …), tinted to their stream. Cardinals = wayfinding (unboxed visual class); stack items = inventory (boxed, indexed). Don't unify the two visual classes.

Corridor-exit ride-out + Services re-centre (2026-06-16, ADR-021 follow-up): the home-v2 brandmark (ProjectedBrandmarkActor) used to FADE OUT across the epilogue APPROACH band so the core mark didn't sit inside the substrate as it grew into a planet. That fade is GONE. The mark is now WELDED to BRANDMARK_ANCHOR_INTELLIGENCE (sphere centre) via a private mirror camera that follows the SAME pose chain as FlyingCameraRig + EpilogueNewsTicker (getEpilogueCameraPose with docked-pose ease, then getCorridorExitCameraPose once the dissipate engages). The mark therefore rides the sphere off-screen during the BILLIONS landing tilt-up — geometric exit, Principle 2. Once transform.docked engages and the dissipate clock ramps, the welded screen position lerps toward the viewport centre across DOCK_RECENTRE_FRAC = 0.85 of the dissipate clock; the last 15% holds at centre while the planet finishes scattering. After the dock releases, a new data-services-brandmark CSS gate (written by useCorridorExitScroll) holds the mark fixed-centred in #services ("hold" state) and fades it via --services-brandmark (0..1) as #continuum.top crosses 0.5 → 0.1 vh ("fade" state). A NEW parallel rAF in ProjectedBrandmarkActor handles the post-active dock window (the `1vhof scroll where the corridor sticky cell has released but the dock is still engaged) — gated on the INVERSE of the tracker'spaintingclause so they never race. SharedcomputeWeldedRecthelper +EpilogueScratchcache power both the trackeronPaint` corridor-active path and the parallel rAF. The full revision (CSS rules, gate states, single-writer invariants) is documented in ADR-021 § "2026-06-16 Revision — Brandmark ride-out + Services re-centre".

Previous substrate (2026-06-06): one outer gold geodesic icosphere (buildGeodesicEdges(SUBSTRATE_CAGE_RADIUS, 1)) and NO inner dawn shell. Low-poly brain remains lab-only.

Lab-only brain exploration (2026-06-06): lib/brandmark/sampleBrain.ts exports buildLowPolyBrain for /test/intelligence-artifact variants. Do not treat it as the production home substrate unless a later ADR explicitly ports it back.

Stack dock (2026-06-07): at Build the layer (substrate + orbits) docks into the full stack via ShellStack — green trusted-source lanes from the left, dawn surface fan to the right (ported from the lab FUNNEL variant). The outer geodesic cage at Build is retired. Accretion keys: { substrate, orbits, stack }. Encode orbits are judgment, not sources.

Judgment cardinal primitives (Encode): four labeled compass axes in ShellEncode — Judgment (N), Taste (E), Way of working (S), Voice (W) — plus ~6 asymmetric captured notes that slide in and compare against a primitive. Staggered via petalStagger; framed DOM labels via encode.primitive.* anchors; persists through Build.

Per-station camera framing (parkDistance): each station can override how far the camera sits in front of it when parked (StationNode.parkDistance, defaults to GATE_PARK_DISTANCE 4.5). The three shell parks (Navigate / Encode / Build) use 6.2 so the assembled layer + stack read with oversight.

Corridor → Services seam exit (ADR-021 Phase 2, 2026-06-16): the centred SVG brandmark in #services is REPLACED by CorridorSeamPixelField — a 2D-canvas pixel cloud sampled from BRANDMARK_FULL_PATHS (same source as the SVG glyph + BrandmarkSilhouettePoints, ADR-014). Driven by transform.seamMorph (0..1) written by useCorridorExitScroll. The dissolve OPENS the moment the welded mark re-centres and shows itself — during the dock tail (markCentred = docked ? dissipate >= MARK_CENTRED_DOCK_PROGRESS (0.85) : rawDissipate >= 0.999), NOT only after the dock releases — and runs a long runway driven by #continuum's approach (SEAM_MORPH_START_VH 1.9SEAM_MORPH_END_VH 0.1, smoothstepped). Pixel field paints assembled at seamMorph = 0, then disperses outward + lifts upward with rank-stagger; alpha fully fades by seamMorph = 1. data-services-pixelate may co-exist with data-corridor-docked (the morph begins before dock release by design). This is OUTSIDE the corridor brandmark journey (its own clock, its own canvas, its own renderer) and OUTSIDE the brandmark painter cap — see the brandmark-particle skill's "Don't reintroduce" exception. The dispersal math lives in lib/home-v2/seamPixelize.ts and is unit-tested.

Want to change in the corridor accretion shell File
Tune which corridor progress each shell layer DEPLOYS at components/landing/home-v2/DepthGatewayScene/sceneGeom.tsCORRIDOR_TIMELINE.accretion (substrate / orbits / stack start + peakAt).
Tune plug cascade overlap shell/ShellEncode.tsxPRIMITIVE_OVERLAP / NOTE_OVERLAP in petalStagger.
Tune stack funnel extents / lane counts shell/shellGeom.tsSTACK_SOURCES_X, STACK_SURFACES_X, STACK_LANE_COUNT, STACK_FAN_COUNT, etc.
Tune shell layer radii (substrate / primitive table) shell/shellGeom.tsSUBSTRATE_CAGE_RADIUS (0.42), PLUG_INNER_R, PRIMITIVE_NODE_R, SHELL_PRIMITIVES, SHELL_NOTES.
Tune shell-park camera oversight lib/home-v2/corridorMap.tsStationNode.parkDistance (Navigate / Encode / Build use 6.2).
Add / re-tune a judgment primitive or note SHELL_PRIMITIVES / SHELL_NOTES in shell/shellGeom.ts
Change layer composition (mount order, group rotation) BrandmarkAccretionShell.tsx — mount order: ShellSubstrateShellEncodeShellStack.
Change a single layer's geometry / materials shell/ShellSubstrate.tsx, shell/ShellEncode.tsx, shell/ShellStack.tsx
Stack tier DOM labels (Sources / Surfaces) sceneGeom.ts COPY_ANCHORS + CopyAnchors.tsx + home-v2.css

The substrate geodesic emerges via shellWrapEmerge(reveal). Judgment primitives + notes and stack funnel emerge geometrically (trim + slide / foldEmerge + petalStagger). All three layers persist at the Build landing around the persistent DOM brandmark.

splitEmerge(reveal) is the alias of petalEmerge(reveal).scale — used by the substrate cage (uniform body) and the outer surfaces geodesic where per-element petals would have read busy.


When you touch CSS too

  • Particle mode — there is a SINGLE rule that hides every native dock SVG + the fixed actor: [data-brandmark-mode="particle"] [data-brand-anchor=...] :where(img, svg) { opacity: 0; visibility: hidden; } and [data-brandmark-mode="particle"] .tf-brandmark-actor { display: none; }. Do not reintroduce per-station data-brand-svg-dock or data-brand-particle-backdrop attribute gates.
  • SVG fallback — the [data-brand-on-missing="parked"] and [data-brand-on-rail="parked"] rules make the native dock SVGs visible at their parked positions. These survived the refactor. Keep them — they're the SVG-mode display story.
  • The brandmark cloud's opacity is owned by the shader uniform, not by CSS. Do not add opacity: 0 → 1 transitions to .tf-brandmark-particle-canvas — the canvas wrapper stays at opacity 1; the painter inside controls visibility via uOpacity.

Don't reintroduce

  • useSigilChoreography — replaced by useBrandmarkJourney. The old per-station snapshot model is gone; do not bring it back as a parallel hook.
  • brandmarkParticleStore — replaced by brandmarkJourneyStore. The single transform is the only state.
  • data-brand-svg-dock / data-brand-particle-backdrop — retired. The single data-brandmark-mode attribute (set once at init) is the entire CSS gate.
  • applyR3FDockMask / MutationObserver in IntelligenceLayerPortal — retired. The CSS [data-brandmark-mode="particle"] rule hides the substrate SVG dock declaratively.
  • BrandmarkActor.morphRects — dead API, deleted. The actor uses pinToRect only.
  • buildBrandmarkParticles / R3F <points> inside BrandmarkRingfield — retired. The R3F scene is rings-only; the brandmark cloud is the global painter's job, even inside the intelligence-layer section.

Debugging tip — the dev parity log

In development, useBrandmarkJourney writes a console.debug line every 30 frames:

[brandmarkJourney] scrollY=4321 parked=miss rect=842,521 144x144 density=1.00 disp=0.00 rotY=0.0deg rings=off ringP=0.00

Scan it as you scroll:

  • density should stay at 1.00 throughout (Principle 1 — continuous);
  • disp should be 0 at parks and during miss → substrate / substrate → rail / rail → orbit transits (Principle 2); only sigil → miss should show a bell-curve bump up to ~0.45.
  • rotY should be 0 outside the substrate window and ramp via splitRotation inside it.
  • rings=on should ONLY appear during the substrate window.
  • parked should match the current dock or report transit between them.
Install via CLI
npx skills add https://github.com/thoughtform-co/thoughtform --skill brandmark-choreography
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
thoughtform-co
thoughtform-co Explore all skills →