The static front/back occlusion needed real motion to read as 3D.
Now the helix actually rotates around its long axis — one full turn
every 24 s by default.
Mechanics
- A 2D projection of a 3D helix face-on is `x = R * sin(k*y + φ)`.
Animating φ via requestAnimationFrame is exactly equivalent to
spinning the helix around its vertical axis.
- All derived geometry (strand front/back splits, decorative rungs,
per-project base-pair rungs, slot strandAx/Bx) now reads `phase`
so they recompute every frame.
- buildStrandSplit's crossing snap also takes phase: y_c moves down
the helix as it spins, so the seamless front/back joints track it.
Performance
- Path resampled at H/1.8 (~310 points / strand at 3 projects) and
redrawn 60×/s on a modern device. Coords are .toFixed(3) to keep
diff-friendly strings short.
- dt is clamped to 100 ms so tab-resumes don't jump.
Interaction
- A 280 px-wide transparent hover strip down the centre catches
pointerenter/leave on the helix without stealing clicks from the
cards. While hovered, `paused = true` freezes phase so users can
read the labels comfortably.
- prefers-reduced-motion disables the rAF loop entirely — phase
stays at 0, helix is a still 3D portrait.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The biggest visual deficit was that both strands were drawn fully on
top of each other with a Gaussian-blur filter applied directly to the
sharp lines — so they read flat and slightly fuzzy.
What changed
- buildStrandSplit() carves each strand into front/back segments
partitioned at every sin(ky)=0 crossing. Crossings are snapped to
the exact y_c = n * PERIOD/2 so consecutive front/back segments
meet seamlessly at (CX, y_c) with no visible gap.
- Render order is now strictly back → mid (rungs, per-project rungs,
card connectors) → front, so front-strand portions occlude back-
strand portions where they cross. Real 3D illusion.
- Glow is a separate blurred copy *underneath* the sharp path
(stdDeviation 3.5, stroke-width 6-7). Sharp lines now stay crisp.
- Back portions render dimmed (opacity 0.62) for depth.
- Subtle white highlight stroke (stroke-width 0.9, alpha .55) is
added on top of the front sharp strands — wet-ribbon specular feel.
- Path resolution doubled (steps ≈ H/1.8, was H/4). Coords use
.toFixed(3) instead of .2. shape-rendering="geometricPrecision"
on every load-bearing path.
- Decorative DNA-texture rungs were folded into a single pass that
varies stroke-width and opacity by sin-depth, brighter for the
"front side" rungs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Side now flips strictly by slot index (even=left, odd=right) instead
of by strand. With strand-driven side assignment, several consecutive
same-strand projects would stack on one side and overlap vertically.
Belt: SLOT_HEIGHT trimmed to 140 (was 180). Same-side cards are now
2×140 = 280 px apart centre-to-centre. Suspenders: card .slide gets
max-height: 230 px so a runaway title can never grow past that
budget. Together they guarantee a ≥50 px gap on same-side cards.
Strand identity remains visible via stripe colour, badge, and node
fill — the L/R position is no longer a strand signal.
Helix section for 3 seeded projects now renders at 560 px (was 960).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each project's node now sits at the rung centre (x = CX) instead of at
its strand's instantaneous extreme. The "line in between" the strands
becomes the natural place for the project marker — true to the
base-pair metaphor — and frees Y positions from having to coincide
with strand-peak intervals.
What this unlocks
- Slot Y is decoupled from the wave period. SLOT_HEIGHT drops from
240 → 180 and the wave period from SLOT_HEIGHT*2 → a fixed 320,
giving the helix a denser twist without re-spacing the cards.
- Cards pack tighter (TOP/BOTTOM padding 120 → 80, summary clamp
3 → 2 lines, padding shaved). 3 projects now fit in ~620 px of
helix section instead of ~960 px — empty space goes away.
Visual additions
- Per-project base-pair: a prominent strand-coloured line at each
node's Y, drawn from strand A's x to strand B's x. This is the
rung the node sits on.
- Connector dash line continues from the node centre outward to the
card edge.
Side mapping
- Strand identity is now purely the card's side + stripe + badge:
A → left, B → right. (Previously the side followed the strand's
instantaneous extreme, which gave inconsistent groupings.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Identity refresh aligned to Waterschap Brabantse Delta.
Brand
- HELIX → R&D-lab everywhere user-facing (SITE.name; literal "HELIX"s
swept across routes, app.html, login, error messages, seed).
- New tagline: "Projects, innovations, and every strand between."
- Site description updated.
Palette (sourced from the official WSBD-logo.svg)
- Primary #0d4f9e, secondary #1fa0db, accent #bed137 — added as
--wbd-blue / --wbd-cyan / --wbd-lime CSS vars and as wbd.* in
tailwind.config.js. helix.* aliases now point to the WBD palette.
- Strand A (Projects) → #1fa0db cyan. Strand B (Innovations) → #bed137 lime.
- Body vignette + scroll-bar + legend dots repainted accordingly.
Composite logo
- New 24px nav glyph + favicon.svg: WBD-style tilted-square mark in WBD
blues at the centre, helix strands (lime + cyan) wrapping it, lime
"active site" dot at the crossing. Says "R&D-lab × Brabantse Delta"
in one mark.
Lab-slide cards (VerticalHelix)
- Frosted-glass surface (backdrop-filter blur+saturate).
- Thick 5px strand-coloured stripe on the helix-facing edge (gradient,
glowing shadow). Slide rounds the stripe corners; the rest is square.
- Slide header has the strand badge and a monospace serial number
(01/03 etc) — lab-specimen feel.
- Dashed footer rule + "Open detail →" CTA.
- Inline link chips (Gitea / Dashboard / Demo / Docs / Paper / Video)
with inline SVG icons + short labels. Hover lights up in the strand
colour. Capped at 5 visible, "+N" overflow indicator.
- Real <a> chips inside the card without nested <a>: overlay-link
pattern (transparent slide-link absolute fills the card, chips sit on
z-index: 2 above it).
Server load
- + Page now fetches each project's links in one Drizzle relational
query (db.query.projects.findMany with: { links }), capped at 12.
- + Form: strand picker (Project / Innovation radios) reads + persists
the new column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reshape the landing so each project anchors to a slot on one of the two
helix strands. Strand A = Projects (durable, in-production), strand B =
Innovations (experiments, prototypes). Cards alternate L/R based on the
strand's instantaneous position at the slot — strand identity is shown
by colour + badge, not by side.
Schema
- ALTER projects ADD strand TEXT NOT NULL DEFAULT 'A' CHECK ('A','B').
Generated as drizzle/0001_*.sql.
Component
- New VerticalHelix.svelte replaces Helix.svelte on the landing.
- Slot Y = 120 + i * 240; period = 480, so each strand is at an extreme
at every slot. Node sits at the strand's actual x; card hugs that side.
- Pulsing node halos and animated gradient stops give "alive" feeling
without moving the strand geometry — projects stay anchored.
- prefers-reduced-motion disables the pulse.
- <760px: SVG hides, cards stack full-width.
Authoring
- /projects/new form gets a Project / Innovation radio picker.
- Seed adds an "DNA Scout" example on strand B so the helix renders with
both strands populated on first boot.
Landing
- Hero shrinks (60vh, no helix backdrop) and gains a strand legend.
- Top 12 projects bind to the helix; "See all N →" appears if there's
more.
- Posts band becomes compact (3 most recent), below the helix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Dockerfile: npm ci (uses package-lock for reproducible installs)
- CMD now: migrate → seed (idempotent) → start. Gated by SEED_ON_BOOT.
- docker-compose: name: helix, healthcheck on /, OAuth env defaults to empty
so `docker compose up` works without a .env (public pages render; sign-in
fails until OAuth is configured).
- README: explicit "Run it locally — two ways" section. Docker first
(production-like), native Node second. Documents port-conflict workaround
and Gitea OAuth setup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>