Site: hero model of six apps, roads, beat-timed growth

This commit is contained in:
ZacharyZhang-NY committed 2026-10-08 23:29:21 -04:00
1 parent b991d59b21
commit 3b75617599
9 files changed
+199 -16

No files matched your search

+19
View File
@@ -11,3 +11,22 @@
.face.right {
fill: color-mix(in oklab, var(--tone), black 20%);
}
/* Each block drops one step and lands on its beat. */
.grow .block {
animation: land 0.2s cubic-bezier(0.2, 0.8, 0.3, 1) both;
animation-delay: calc(var(--land) - 0.2s);
}
@keyframes land {
from {
transform: translateY(-28px);
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.grow .block {
animation: none;
}
}
+23 -8
View File
@@ -1,15 +1,30 @@
import "./style.css";
import "./iso.css";
import { block, stage } from "./iso.ts";
import { model } from "./model.ts";
/** 112 BPM, the film's tempo; blocks land on sixteenths. */
const BEAT = 60 / 112;
const STEP = BEAT / 4;
const app = document.querySelector<HTMLElement>("#app");
if (!app) throw new Error("index.html has no #app");
app.append(
stage([
block({ x: 0, y: 0, w: 12, d: 12, h: 0.5, tone: "#e9e9e6" }),
block({ x: 2, y: 2, z: 0.5, w: 3, d: 3, h: 4, tone: "#f4f4f2" }),
block({ x: 7, y: 2, z: 0.5, w: 3, d: 3, h: 2, tone: "#f4f4f2" }),
block({ x: 2, y: 7, z: 0.5, w: 3, d: 3, h: 1, tone: "#3d7fe5" }),
]),
);
const blocks: SVGGElement[] = [];
let beat = 1;
for (const part of model) {
part.blocks.forEach((spec, i) => {
const g = block(spec);
g.style.setProperty("--land", `${(beat * BEAT + i * STEP).toFixed(3)}s`);
blocks.push(g);
});
beat += Math.ceil(part.blocks.length / 4);
}
const title = document.createElement("h1");
title.textContent = "Ely Office";
const model3d = stage(blocks);
model3d.classList.add("grow");
model3d.setAttribute("role", "img");
model3d.setAttribute("aria-label", model.slice(1, -1).map((p) => p.name).join(", "));
app.append(title, model3d);
+61
View File
@@ -0,0 +1,61 @@
// The desk model: six apps, each its own object.
import type { Block } from "./iso.ts";
const PAPER = "#f4f4f2";
const DESK = "#e6e6e2";
/** The one accent: roads and the selected cell. */
export const ACCENT = "#3d7fe5";
const TOP = 0.6;
/** One part of the model, grown together. */
export interface Part {
name: string;
blocks: Required<Block>[];
}
const on = (x: number, y: number, w: number, d: number, h: number, z = 0, tone = PAPER): Required<Block> => ({
x, y, z: TOP + z, w, d, h, tone,
});
/** A stack of pages. */
const doc = [0, 1, 2, 3].map((i) => on(1 + i * 0.08, 1 - i * 0.08, 2.6, 3, 0.25, i * 0.25));
/** A three by three sheet; one cell is selected. */
const heights = [1.2, 2.4, 1.6, 0.8, 1.8, 1, 0.5, 1.4, 0.7];
const table = heights.map((h, i) =>
on(5 + (i % 3), 1 + Math.floor(i / 3), 0.9, 0.9, h, 0, i === 4 ? ACCENT : PAPER),
);
/** A slide on its stand. */
const show = [on(9.4, 1, 3.2, 0.25, 2.3), on(9.4, 1.25, 3.2, 1.6, 0.25)];
/** A page with its corner folded down. */
const pdf = [on(13, 3.2, 2.4, 2.4, 1), on(13, 5.6, 1.6, 0.8, 1), on(14.6, 5.6, 0.8, 0.8, 0.3)];
/** A trunk and a branch, joined by a merge. */
const tree = [on(6.5, 7.6, 0.7, 0.7, 2.8), on(8.7, 7.6, 0.7, 0.7, 1.8), on(7.2, 7.6, 1.5, 0.7, 0.3, 1.2)];
/** Headings stepping down. */
const mark = [0, 1, 2].map((i) => on(1, 7 + i, 3, 1, 2 - i * 0.6));
/** Roads: every document exports to PDF; Markdown goes to Git. */
const road = (x: number, y: number, w: number, d: number) => on(x, y, w, d, 0.08, 0, ACCENT);
const roads = [
road(2, 4, 0.6, 0.6),
road(6, 4, 0.6, 0.6),
road(10.7, 2.85, 0.6, 1.75),
road(2, 4.6, 11, 0.6),
road(4, 7.6, 2.5, 0.6),
];
/** Growth order: desk, the six apps, then roads. */
export const model: Part[] = [
{ name: "Desk", blocks: [{ x: 0, y: 0, z: 0, w: 16, d: 11, h: TOP, tone: DESK }] },
{ name: "ElyDoc", blocks: doc },
{ name: "ElyTable", blocks: table },
{ name: "ElyShow", blocks: show },
{ name: "ElyPDF", blocks: pdf },
{ name: "ElyTree", blocks: tree },
{ name: "ElyMark", blocks: mark },
{ name: "Roads", blocks: roads },
];
+17 -1
View File
@@ -12,5 +12,21 @@ body {
#app {
min-height: 100vh;
display: grid;
place-items: center;
place-content: center;
justify-items: center;
gap: 6rem;
padding: 9rem 1.5rem;
box-sizing: border-box;
}
h1 {
margin: 0;
font-size: clamp(3rem, 8vw, 6rem);
font-weight: 700;
letter-spacing: -0.03em;
}
.stage {
max-width: 100%;
height: auto;
}
+27
View File
@@ -0,0 +1,27 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { order } from "../src/iso.ts";
import { ACCENT, model } from "../src/model.ts";
const all = model.flatMap((p) => p.blocks);
const [desk] = model[0].blocks;
test("the model draws without intersections or cycles", () => {
assert.equal(order(all).length, all.length);
});
test("every block stands on the desk", () => {
for (const b of all.slice(1)) {
assert.ok(b.x >= 0 && b.y >= 0 && b.x + b.w <= desk.w && b.y + b.d <= desk.d, JSON.stringify(b));
assert.ok(b.z >= desk.h);
}
});
test("the accent covers at most a tenth of the visible tops", () => {
const area = (list: typeof all) => list.reduce((s, b) => s + b.w * b.d, 0);
assert.ok(area(all.filter((b) => b.tone === ACCENT)) / area([desk]) <= 0.1);
});
test("six apps between the desk and the roads", () => {
assert.deepEqual(model.slice(1, -1).map((p) => p.name), ["ElyDoc", "ElyTable", "ElyShow", "ElyPDF", "ElyTree", "ElyMark"]);
});
+6 -6
View File
@@ -256,15 +256,15 @@ climbs to the lighthouse and comes back through the dunes.
## Route at a glance
| Section | Distance | Time |
| --- | --- | --- |
| Sea wall | 1.2 km | 15 min |
| Lighthouse climb | 0.8 km | 20 min |
| Dune path | 2.1 km | 30 min |
| Section | Distance | Time |
| ---------------- | -------- | ------ |
| Sea wall | 1.2 km | 15 min |
| Lighthouse climb | 0.8 km | 20 min |
| Dune path | 2.1 km | 30 min |
## Pace
Walking time is distance over pace, plus a minute for every ten metres of climb:
Walking time is distance over pace, plus a minute for every ten meters of climb:
$$
t = \frac{d}{v} + \frac{h}{10}
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Builds demo content, then records all six apps.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
bash -c "source '$here/lib.sh'; trap - EXIT; content"
for app in doc table show pdf tree mark; do
bash "$here/$app.sh"
done
+3 -1
View File
@@ -42,8 +42,10 @@ launch() {
rm -rf "$home"
local data="$home/Library/Application Support/ElyOffice/$app"
mkdir -p "$data"
local panels=
[ -z "${PANEL_LEFT:-}" ] || panels=",\"panels\":{\"left\":$PANEL_LEFT}"
printf '{"version":1,"window":{"mode":"windowed","x":%s,"y":%s,"width":%s,"height":%s}%s}\n' \
"$win_x" "$win_y" "$win_w" "$win_h" "${PANEL_LEFT:+,\"panels\":{\"left\":$PANEL_LEFT}}" >"$data/settings.json"
"$win_x" "$win_y" "$win_w" "$win_h" "$panels" >"$data/settings.json"
env HOME="$home" ELY_APPEARANCE=light ELY_LOCALE=en ELY_NO_DIALOG=1 ELY_LOG=info ELY_REDUCE_MOTION=0 \
${EXTRA_ENV:-} "$root/target/release/$app" "$@" >"$out/logs/$app.out" 2>"$out/logs/$app.err" &
app_pid=$!
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# ElyMark: visual typing, formula and diagram, source and back.
source "$(dirname "$0")/lib.sh"
notes=/Users/Shared/Notes
mkdir -p "$notes"
cp "$demo/harbor-loop.md" "$notes/"
us_input
launch ely-mark "$notes/harbor-loop.md"
wait_log 'mark: shown as "visual"' 60
front
sleep 3
x=$win_x
y=$win_y
record_start mark
pause 0.8
mark "click end of the intro paragraph"
mouse click $((x + 1000)) $((y + 235))
pause 0.4
mark "typing"
typed " Allow two hours in winter."
pause 0.6
mark "scroll to formula and Mermaid diagram"
mouse move $((x + 1000)) $((y + 520)) 0.5
mouse scroll -1000 1.8
pause 1.8
mark "Cmd+2: source mode"
keys 'keystroke "2" using command down'
pause 2.2
mark "Cmd+1: back to visual"
keys 'keystroke "1" using command down'
pause 2.2
record_stop mark
quit_app
rm -rf "$notes"