Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
verify:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

# Must come before setup-node: the pnpm cache below needs the store to exist.
# The version is read from the "packageManager" field in package.json.
- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm

- run: pnpm install --frozen-lockfile

- run: pnpm check

- run: pnpm lint

- run: pnpm format:check

- run: pnpm build
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
2 changes: 1 addition & 1 deletion .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"printWidth": 120,
"trailingComma": "all",
"sortPackageJson": false,
"ignorePatterns": []
"ignorePatterns": ["src/assets/fonts/*.typeface.json"]
}
123 changes: 123 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# CLAUDE.md

Guidance for Claude Code when working in this repository. See [README.md](README.md) for
setup, scripts and architecture; this file only covers what changes how you should work.

## Package manager

**pnpm only.** There is a single `pnpm-lock.yaml` and the version is pinned via the
`packageManager` field. Never run `npm` or `yarn` here — it will produce a competing
lockfile and CI installs with `--frozen-lockfile`.

## Toolchain

**oxlint + oxfmt, not ESLint and not Prettier.** This is deliberate. Do not install
`eslint` or `prettier`, and do not create their config files. Rules live in
`.oxlintrc.json`; formatting in `.oxfmtrc.json`.

Formatting: single quotes, semicolons, 120-column width, trailing commas everywhere,
2-space indent, LF, final newline.

Before calling a task done:

```bash
pnpm check && pnpm lint && pnpm format:check && pnpm build
```

## This is a single-page static site

`output: 'static'` with no adapter. Do not reach for SSR-only APIs (`Astro.request`,
runtime endpoints) without changing `output` and installing an adapter first.

`build.inlineStylesheets: 'always'` and `vite.build.cssCodeSplit: false` are chosen
specifically because there is one page. If real routes get added, revisit both.

## The hero animation

Lives in `src/scripts/hero-animation.ts`.

- It is a faithful port of zadvorsky's "PIECE BY PIECE" pen
(https://codepen.io/zadvorsky/pen/GZmKYX): shards fly along cubic beziers and collapse
into the origin, each vertex of a face gets its own `delay + Math.random()` jitter
(that per-vertex smear is the signature look), and a click-drag scrubber pauses/seeks
the tween. Keep the port exact — magnitudes, timings and easings are the original's
absolute values, tied to `TEXT_SIZE` 14.
- Framing is responsive, unlike the pen: the camera distance is derived from the h1
fallback's computed font-size so canvas and fallback render the same type size, the
text reflows onto two centred lines when that size drops below
`MIN_SINGLE_LINE_EM_PX`, and the bezier arcs scale with `uArcScale` (= z / 1400) so
the debris always fills the stage. Mind `REST_SCALE`: the original shader draws the
resting wordmark at exactly 2× its geometry (`position + cubicBezier(t=0)`), so every
camera-fit formula sizes against twice the measured bounds.
- The motion is entirely in the vertex shader. The render loop only advances one `uTime`
uniform. Do not add CPU-side per-frame animation — change the attributes or the GLSL.
- GSAP, THREE.BAS and PNLTRI are deliberately not used; the BAS material is ~15 lines of
inlined GLSL, the GSAP tween/scrubber is a few easing formulas in the loop, and modern
three's earcut triangulation makes PNLTRI redundant. Do not reintroduce them.
- `TRACKING` widens every glyph advance at runtime because the bevel expands each
outline by `bevelSize` per side — without it adjacent Archivo Bold glyphs touch.
- The `webgl-ready` class must go on `<html>` *before* `createHeroAnimation` runs — it is
what gives the canvas its layout box, and the renderer needs real dimensions. It is
removed again if initialisation throws.
- Booting is deliberately deferred and chunked, and this is what keeps Total Blocking Time
down — three.js is ~550KB and init is ~200ms of main thread on a throttled phone:
- `Hero.astro` `import()`s the module at idle after `load`, never statically. A static
import puts the bundle in the preload scanner's queue, where it competes with the font
for the connection and pushes FCP/LCP out.
- It also waits for `document.hidden` to clear first. `requestIdleCallback` never fires
in a hidden document — not even on timeout — so without this a page opened in a
background tab never starts the animation at all.
- `createHeroAnimation` is **async** and `await yieldToMain()`s at every phase boundary
(each `TextGeometry`, the merge, the GL context, the first render) plus on a time
budget inside the shard-attribute loop. No single phase exceeds ~45ms; adjacent phases
left in one task is what makes tasks "long". Adding synchronous work between these, or
dropping a yield, silently regresses TBT.
- The line count is measured *before* the first build. Building single-line and letting
`resize()` discard it triangulated the wordmark twice on every phone-width viewport.
- The geometry drops its `normal` and `uv` attributes: the shader reads neither, and
they are ~30% of a ~130k-vertex upload.
- The `<h1>` fallback is not decoration. Keep it in the DOM, visible by default, and
readable on its own.

## The 3D font

`src/assets/fonts/archivo-bold.typeface.json` is a **generated, committed artifact**. It
is subsetted to exactly the glyphs the hero renders.

If the hero copy or the typeface changes, update `CHARSET` in
`scripts/fonts/build-typeface.mjs` and re-run `pnpm fonts:typeface`. Adding a character
without regenerating will make three.js fail to find the glyph at runtime.

The file is listed in `ignorePatterns` in `.oxfmtrc.json` so oxfmt does not expand the
minified output.

## Brand assets

`public/logo.svg` is the designer's full lock-up. **Do not edit it.**

`public/favicon.svg` is derived from it: children 0–4 (the dotted X mark, without the
wordmark), wrapped in `translate(-90 0)` with `viewBox="0 0 120 120"`. If `logo.svg` is
replaced, re-cut the mark rather than hand-editing the favicon.

The lock-up's wordmark is not used on the page — the centre wordmark is 3D geometry built
from Archivo.

## Script targets are hardcoded

`lint`, `lint:fix`, `format` and `format:check` all target `src scripts astro.config.mjs`
explicitly. If you add a new top-level directory containing code, add it to all four or
it will silently escape every check.

## Commits

Conventional Commits (`feat:`, `fix:`, `chore:`, …), matching the existing history.

## License

Proprietary, all rights reserved. Do not copy third-party code into this repository
without checking license compatibility first.

## Not yet written

`.gitignore` reserves `.screenshots/` for a planned `scripts/dev/shot.ts` concept-review
screenshot tool. It does not exist yet.
143 changes: 141 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,141 @@
# website
Alexis Technologies Website
# Alexis Technologies — Website

![Astro](https://img.shields.io/badge/Astro-7-BC52EE)
![TypeScript](https://img.shields.io/badge/TypeScript-6-3178C6)
![three.js](https://img.shields.io/badge/three.js-r185-000000)
![pnpm](https://img.shields.io/badge/pnpm-10.34-F69220)
![License](https://img.shields.io/badge/License-Proprietary-red)

The main website for Alexis Technologies. It will eventually showcase the company's
services, projects and contact information.

Right now it is a single-page placeholder: an animated wordmark that assembles itself
out of shards, with a "coming soon" line beneath it.

Production: <https://alexis.dev>

## Tech stack

| | |
|---|---|
| Framework | [Astro](https://astro.build) 7, static output — no adapter, no SSR |
| Language | TypeScript 6 |
| Graphics | [three.js](https://threejs.org) with a hand-written GLSL vertex shader |
| Lint / format | [oxlint](https://oxc.rs) + [oxfmt](https://oxc.rs) — **not** ESLint or Prettier |
| Typography | [Archivo](https://fonts.google.com/specimen/Archivo) via Fontsource |
| SEO | `@astrojs/sitemap` |
| Telemetry | Vercel Analytics + Speed Insights |
| Hosting | Vercel (static) |

## Prerequisites

- **Node** ≥ 22.12 — the repo pins 24 in [`.nvmrc`](.nvmrc)
- **pnpm** 10.34.5 — pinned via the `packageManager` field, so Corepack picks it up automatically

```bash
corepack enable
```

## Getting started

```bash
pnpm install
```

```bash
pnpm dev
```

The dev server runs at <http://localhost:4321>.

## Scripts

| Script | What it does |
|---|---|
| `pnpm dev` | Start the Astro dev server |
| `pnpm build` | Build the static site into `dist/` |
| `pnpm preview` | Serve the built output locally |
| `pnpm check` | Type-check `.astro` and `.ts` files (`astro check`) |
| `pnpm lint` | Lint with oxlint |
| `pnpm lint:fix` | Lint and apply fixable rules |
| `pnpm format` | Format with oxfmt |
| `pnpm format:check` | Verify formatting without writing — what CI runs |
| `pnpm fonts:typeface` | Regenerate the subsetted 3D font. **One-off** — the output is committed |

CI runs `check`, `lint`, `format:check` and `build` on every push to `main` and every
pull request.

## Project structure

```
.github/workflows/ CI
public/ Served as-is
logo.svg Full brand lock-up (source of truth, not used on the page)
favicon.svg The dotted X mark, cut from logo.svg — favicon and corner mark
scripts/
fonts/ Offline font conversion (not part of the build)
src/
assets/fonts/ Generated typeface.json consumed by three.js
components/ LogoMark, Hero, SiteFooter
layouts/ BaseLayout — head, meta, telemetry
pages/ index.astro — the only route
scripts/ hero-animation.ts — the WebGL scene
styles/ global.css
```

## How the hero animation works

`TextGeometry` extrudes the wordmark into a bevelled, **non-indexed** geometry, so every
triangle owns its three vertices and can be moved independently. Each triangle is then
given its own scatter offset, two bezier control points, a delay and a duration, stored
as vertex attributes.

The vertex shader walks each triangle along that cubic bezier from "scattered" to
"assembled", easing it and growing it out of its own centroid on the way in. Nothing is
animated on the CPU — the render loop only advances a single `uTime` uniform, which
ping-pongs so the wordmark repeatedly assembles and blows apart.

Scatter magnitudes are expressed as multiples of the wordmark's cap height rather than in
absolute units, so the debris field stays proportional to the text at any viewport size.

**Fallbacks.** The real `<h1>` is always in the DOM and visible by default; the canvas
replaces it only once WebGL has actually initialised. That covers no-JS, no-WebGL and
crawlers for free. When `prefers-reduced-motion: reduce` is set, WebGL never boots at all
and the plain heading stays.

## Brand assets

`public/logo.svg` is the full vertical lock-up delivered by design — mark, "Alexis"
wordmark and "TECHNOLOGIES" tagline. Treat it as read-only.

`public/favicon.svg` is the dotted X mark cut out of it (`viewBox="0 0 120 120"`). It is
used both as the favicon and as the corner mark on the page, so the browser fetches one
file for both. If `logo.svg` ever changes, re-cut it.

## Code style

Enforced by oxfmt and `.editorconfig`:

- 2-space indent, LF endings, final newline
- single quotes, semicolons, trailing commas everywhere
- 120-column print width

Lint rules live in [`.oxlintrc.json`](.oxlintrc.json). This project deliberately does not
use ESLint or Prettier — do not add them.

Commits follow [Conventional Commits](https://www.conventionalcommits.org/).

## Deployment

`pnpm build` emits a fully static site to `dist/`. Vercel picks it up directly; there is
no Astro adapter and no server runtime.

The three.js bundle is roughly 139 KB gzipped — the dominant asset on the page, and
inherent to shipping a WebGL renderer.

## License

Proprietary. Copyright © Alexis Technologies. All rights reserved.

See [LICENSE](LICENSE) — no part of this repository may be copied, modified or
distributed without prior written permission.
18 changes: 18 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import sitemap from '@astrojs/sitemap';
import { defineConfig } from 'astro/config';

export default defineConfig({
site: 'https://alexis.dev',
output: 'static',
integrations: [sitemap()],
build: {
inlineStylesheets: 'always',
},
vite: {
build: {
// The whole site is a single page; splitting CSS across chunks only adds
// requests to the critical path.
cssCodeSplit: false,
},
},
});
26 changes: 20 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,47 @@
"preview": "astro preview",
"check": "astro check",
"lint": "oxlint src scripts astro.config.mjs",
"lint:fix": "oxlint --fix src scripts astro.config.mjs",
"format": "oxfmt src scripts astro.config.mjs",
"format:check": "oxfmt --check src scripts astro.config.mjs"
"format:check": "oxfmt --check src scripts astro.config.mjs",
"fonts:typeface": "node scripts/fonts/build-typeface.mjs"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Alexis-Technologies/website.git"
},
"keywords": [],
"keywords": [
"alexis-technologies",
"astro",
"three.js",
"webgl",
"website"
],
"author": "Alex Dolid <dolid.sasha@gmail.com>",
"bugs": {
"url": "https://github.com/Alexis-Technologies/website/issues"
},
"homepage": "https://alexis.dev/",
"dependencies": {
"@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0"
"@vercel/speed-insights": "^2.0.0",
"three": "^0.185.1"
},
"devDependencies": {
"@astrojs/check": "^0.9.10",
"@astrojs/sitemap": "^3.7.3",
"@fontsource-variable/archivo": "^5.3.0",
"@fontsource-variable/inter": "^5.3.0",
"@fontsource-variable/jetbrains-mono": "^5.3.0",
"@fontsource/archivo": "^5.3.0",
"@types/three": "^0.185.3",
"astro": "^7.1.6",
"opentype.js": "^2.0.0",
"oxfmt": "^0.58.0",
"oxlint": "^1.73.0",
"typescript": "^6.0.3"
"typescript": "^6.0.3",
"wawoff2": "^2.0.1"
},
"engines": {
"node": ">=22.12.0"
},
"packageManager": "pnpm@10.34.5",
"pnpm": {
Expand Down
Loading
Loading