Rene De Ren cfdccb1b17 feat(helix): 3D rotation with rAF + hover-to-pause
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>
2026-05-20 15:35:58 +02:00

HELIX

EVOLV and every R&D strand, one helix.

The R&D showcase platform of Waterschap Brabantse Delta. EVOLV at its core, every innovation along the strands.

HELIX collects projects, innovations, and updates from across the R&D team in one place — with deep links to repos, dashboards, and demos. Sign-in is gated to the RnD Gitea organisation; viewing is open.

Stack

  • SvelteKit 2 + Svelte 5 + TypeScript
  • Tailwind v4 (CSS-first design tokens)
  • SQLite (single file) + Drizzle ORM
  • Gitea OAuth2 for authoring (no passwords stored)
  • Pure SVG + CSS helix animation — no WebGL

Run it locally — two ways

Mirrors how the EVOLV stack is exercised locally. One command, named volume for SQLite, healthcheck wired:

cd /path/to/helix
docker compose up -d --build         # build image + start container
docker compose logs -f helix         # tail logs
# → http://localhost:3000

If port 3000 is in use (Node-RED, another dev server, …) pick another:

HELIX_PORT=3030 \
  ORIGIN=http://localhost:3030 \
  GITEA_REDIRECT_URI=http://localhost:3030/auth/gitea/callback \
  docker compose up -d --build
# → http://localhost:3030

Useful commands:

docker compose ps                                 # status + health
docker inspect helix --format '{{.State.Health.Status}}'
docker compose exec helix sh                      # shell into the container
docker compose exec helix sqlite3 /data/helix.db  # inspect the DB
docker compose down                               # stop (keeps the volume)
docker compose down -v                            # stop + DELETE all data

The SQLite database lives in the helix_helix-data named volume at /data/helix.db. Migrations + the idempotent seed both run on every boot. Once you have real content, set SEED_ON_BOOT=false in your env to stop re-seeding the demo entries.

2. Native Node (faster dev loop, hot reload)

nvm use                # .nvmrc → Node 20 (Tailwind v3 also works on 18)
cp .env.example .env   # fill in Gitea OAuth — see below

npm install
npm run db:generate    # generate drizzle/0000_*.sql from the schema
npm run db:migrate     # apply migrations to ./helix.db
npm run db:seed        # idempotent demo content
npm run dev            # http://localhost:5173

Gitea OAuth setup

HELIX uses your existing Gitea identity. Create an OAuth2 application once:

  1. Open https://gitea.wbd-rd.nl/-/user/settings/applications
  2. Click Create new OAuth2 application
  3. Application name: HELIX (local) (or HELIX (production))
  4. Redirect URI:
    • local: http://localhost:3000/auth/gitea/callback
    • prod: https://<your-helix-host>/auth/gitea/callback
  5. Click Create application
  6. Copy the Client ID and Client Secret into your .env:
    GITEA_CLIENT_ID=...
    GITEA_CLIENT_SECRET=...
    

GITEA_ALLOWED_ORG=RnD restricts authoring (sign-in) to members of the RnD organisation. Leave it empty to allow any authenticated Gitea user.

Adding a dashboard embed allowlist host

src/lib/config.tsDASHBOARD_ALLOWED_HOSTS controls which Grafana / dashboard origins can be embedded inline on a project page. Add a hostname (no scheme, no path) and redeploy. Hosts not on the list render as a "Open in new tab" card instead — never blindly iframed.

Production deploy

Same compose file as local. On the production host:

cp .env.example .env
# Set: ORIGIN=https://your-host, GITEA_REDIRECT_URI=https://your-host/auth/gitea/callback,
#      GITEA_CLIENT_ID, GITEA_CLIENT_SECRET (from a separate prod OAuth app)
docker compose up -d --build

Put a TLS-terminating reverse proxy (nginx/caddy/traefik) in front of port 3000. Back up the volume periodically:

docker compose exec helix sqlite3 /data/helix.db .dump | gzip > helix-backup-$(date +%F).sql.gz

Project layout

helix/
├── src/
│   ├── lib/
│   │   ├── components/         # Helix.svelte, ProjectCard, PostCard, Nav, Footer, DashboardEmbed
│   │   ├── server/
│   │   │   ├── db/             # Drizzle schema + client (better-sqlite3)
│   │   │   ├── auth.ts         # session token + cookie helpers
│   │   │   └── gitea.ts        # OAuth2 dance + user fetch
│   │   ├── config.ts           # site name, tagline, dashboard allowlist
│   │   └── markdown.ts         # marked wrapper + slug/id helpers
│   ├── routes/
│   │   ├── +page.svelte        # landing (helix hero + recent projects/posts)
│   │   ├── projects/           # /projects, /[slug], /new
│   │   ├── posts/              # /posts, /[slug], /new
│   │   ├── login/              # sign-in page (kicks off Gitea OAuth)
│   │   ├── logout/+server.ts   # POST → invalidates session
│   │   └── auth/gitea/         # OAuth start + callback
│   ├── app.html
│   ├── app.css                 # Tailwind v4 entry + design tokens
│   ├── app.d.ts                # Locals types
│   └── hooks.server.ts         # session hydration
├── drizzle/                    # generated migrations
├── scripts/
│   ├── migrate.js              # run pending migrations
│   └── seed.js                 # idempotent example data
├── static/favicon.svg
├── Dockerfile
├── docker-compose.yml
├── drizzle.config.ts
├── svelte.config.js
├── vite.config.ts
├── tailwind.config.ts (none — Tailwind v4 uses @theme in app.css)
├── tsconfig.json
└── package.json

How to post

After signing in (top-right → Sign in → Gitea), use:

  • + New in the nav (or /projects/new) — a project: title, summary, markdown body, links
  • /posts/new — a shorter update / write-up

Both are auth-gated. Anyone in RnD on Gitea can post.

Contributing

This is internal R&D — open a PR against main on gitea.wbd-rd.nl/RnD/helix.

License

Internal — Waterschap Brabantse Delta R&D.

Description
landing page R&D projects
Readme 195 KiB
Languages
Svelte 64.2%
TypeScript 23.3%
JavaScript 6.9%
CSS 3.8%
Dockerfile 1.2%
Other 0.6%