Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ sm64config.txt

# Deterministic local prepare/build output
build/generated/
build/oracles/
build/package-inputs/
build/release/
build/reports/
Expand Down
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"dev": "vite --host 127.0.0.1",
"dev:3dpipes": "vite --config src/adapters/3dpipes/vite.config.mjs --host 127.0.0.1",
"dev:flowerbox": "vite --config src/adapters/flowerbox/vite.config.mjs --host 127.0.0.1",
"dev:cityflow": "vite --config src/adapters/cityflow/vite.config.mjs --host 127.0.0.1",
"dev:gears": "vite --config src/adapters/gears/vite.config.mjs --host 127.0.0.1",
"dev:gravitywell": "vite --config src/adapters/gravitywell/vite.config.mjs --host 127.0.0.1",
"dev:electropaint": "vite --config src/adapters/electropaint/vite.config.mjs --host 127.0.0.1",
Expand All @@ -50,6 +51,8 @@
"build:3dpipes": "vite build --config src/adapters/3dpipes/vite.config.mjs",
"build:3dpipes:full": "pnpm prepare:3dpipes && pnpm build:3dpipes",
"build:flowerbox": "vite build --config src/adapters/flowerbox/vite.config.mjs",
"build:cityflow": "vite build --config src/adapters/cityflow/vite.config.mjs",
"build:cityflow:full": "pnpm prepare:cityflow && pnpm build:cityflow",
"build:flowerbox:full": "pnpm prepare:flowerbox:artifact && pnpm build:flowerbox",
"build:gears": "vite build --config src/adapters/gears/vite.config.mjs",
"build:gears:full": "pnpm prepare:gears:source && pnpm build:gears",
Expand All @@ -73,6 +76,8 @@
"prepare:3dpipes": "node src/adapters/3dpipes/tools/prepare-csspipes.mjs",
"prepare:flowerbox:artifact": "node scripts/prepare/flowerbox-product-bank.mjs",
"prepare:flowerbox:source": "node src/adapters/flowerbox/tools/prepare-cssflower.mjs && node src/adapters/flowerbox/tools/prepare-polycss-snapshot.mjs",
"prepare:cityflow": "node src/adapters/cityflow/tools/prepare-csscityflow.mjs",
"prepare:cityflow:check": "node src/adapters/cityflow/tools/check-prepare-determinism.mjs",
"prepare:gears:artifact": "node scripts/prepare/cssgears-product-bank.mjs",
"prepare:gears:source": "node src/adapters/gears/tools/prepare-cssgears.mjs && node src/adapters/gears/tools/prepare-polycss-snapshot.mjs",
"prepare:gravitywell:artifact": "node scripts/prepare/cssgravitywell-product-bank.mjs",
Expand All @@ -98,6 +103,10 @@
"verify:flowerbox:runtime": "node src/adapters/flowerbox/tools/audit-runtime-surface.mjs",
"test:flowerbox:lighting-publication": "node --test src/adapters/flowerbox/test/visible-lighting-publication.test.mjs",
"test:flowerbox:browser": "node src/adapters/flowerbox/tools/smoke-browser.mjs",
"test:cityflow": "node --test src/adapters/cityflow/test/*.test.mjs",
"test:cityflow:browser": "node src/adapters/cityflow/tools/smoke-browser.mjs",
"oracle:cityflow:native": "node src/adapters/cityflow/tools/run-native-oracle.mjs",
"oracle:cityflow:visual": "node src/adapters/cityflow/tools/oracle/run-visual-oracle.mjs",
"test:flowerbox:deploy": "node src/adapters/flowerbox/tools/smoke-browser.mjs --deploy",
"verify:gears:bank": "node src/adapters/gears/tools/verify-product-bank.mjs",
"test:gears": "node --test src/adapters/gears/test/cssgears-route-state.test.mjs src/adapters/gears/test/cssgears-shell.test.mjs src/adapters/gears/test/cssgears-source-profile.test.mjs",
Expand Down
10 changes: 10 additions & 0 deletions src/adapters/cityflow/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Cityflow adapter contract

- Read `notes/provenance-bible.md` before changing source simulation, box geometry, colors, lighting, camera, timing, or parity claims.
- Preserve the XScreenSaver simulation contract: six waves, 25 wave speed, 256 radius, 12-degree skew, 20 ms cadence, the source-supported `count` control set to 200, and only the source-emitted top, front, and right faces. The source default remains 800; do not describe 200 as that default.
- Preserve the seeded initialization, wave equation, palette, lighting, camera, and cadence. Cityflow has one prepared product bank and no runtime bank selection.
- Preparation owns the exact seeded `ya_random` initialization, palette, box census, wave phases, source lighting, native far-plane cutoff, retained Morph package, canonical transform dictionary, and typed transform/color frame tables.
- Runtime may mount the prepared graph, derive static face lighting from each prepared box transform, and apply changed prepared transform indices and palette classes. Playback must follow the merged DOMFORMAT `polycss-playback@0` precedent: timer wait, one paint-aligned callback when due, and `elapsed` catch-up. It must not calculate geometry, rasterize assets, grow the DOM, or rebuild topology during playback.
- Keep every box as one retained rigid root with exactly three solid `<b>` leaves. No Canvas, WebGL, WebGPU, SVG scene geometry, masks, clip paths, runtime atlas work, or paint-heavy CSS effects.
- Native state claims require the pinned headless C source harness at revision `906693799e4fb7581436590cf84ecb2d3c9186ba`. Native visual claims require the independent CGL frame-sequence oracle and calibrated native/browser comparison.
- Keep source checkouts, native binaries, captures, traces, and generated packages ignored.
27 changes: 27 additions & 0 deletions src/adapters/cityflow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# css.graphics/cityflow

Source-backed XScreenSaver Cityflow rendered as retained PolyCSS Morph boxes.
The single product bank uses the source's own `count` control at 200 boxes;
the XScreenSaver default is 800. Every box retains the source-emitted top,
front, and right faces.

```bash
export CSSCITYFLOW_SOURCE_ROOT=/path/to/xscreensaver
pnpm prepare:cityflow
pnpm dev:cityflow
pnpm test:cityflow
pnpm prepare:cityflow:check
pnpm test:cityflow:browser
pnpm oracle:cityflow:native
pnpm oracle:cityflow:visual
pnpm build:cityflow
```

Preparation owns the seeded source initialization, smooth color map, box
geometry, source-light factors, OpenGL far-plane cutoff, Morph packages, and
source-cadence transform/color tables. Duplicate transforms are removed during
preparation. Runtime mounts one retained graph, derives its static face-light
factors from the prepared transforms, and sparsely publishes changed prepared
indices with the merged DOMFORMAT timer/rAF `elapsed` scheduler. Runtime does
no geometry calculation, atlas rasterization, topology rebuilding, or DOM
growth.
57 changes: 57 additions & 0 deletions src/adapters/cityflow/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta
name="description"
content="XScreenSaver Cityflow rendered entirely with retained HTML and CSS using PolyCSS."
>
<meta name="robots" content="index, follow">
<meta name="theme-color" content="#000000">
<link rel="canonical" href="https://css.graphics/cityflow/">
<link rel="icon" href="/favicon.ico" sizes="any">
<meta property="og:type" content="website">
<meta property="og:site_name" content="css.graphics">
<meta property="og:title" content="Cityflow - Powered by PolyCSS">
<meta
property="og:description"
content="XScreenSaver Cityflow rendered entirely with retained HTML and CSS using PolyCSS."
>
<meta property="og:url" content="https://css.graphics/cityflow/">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Cityflow - Powered by PolyCSS">
<meta
name="twitter:description"
content="XScreenSaver Cityflow rendered entirely with retained HTML and CSS using PolyCSS."
>
<title>Cityflow - Powered by PolyCSS</title>
</head>
<body class="loading">
<header class="site-header">
<h1 class="site-wordmark-heading">
<a class="site-wordmark" href="/">
<svg class="site-wordmark-svg" viewBox="0 0 218 30">
<text x="0" y="23"><tspan class="site-wordmark-css">css.</tspan><tspan class="site-wordmark-graphics">graphics</tspan><tspan class="site-wordmark-path">/cityflow</tspan></text>
</svg>
</a>
</h1>
<a
class="site-action-icon-only"
href="https://github.com/layoutit/cssGraphics"
target="_blank"
rel="noopener noreferrer"
aria-label="View cssGraphics on GitHub"
title="View cssGraphics on GitHub"
>
<svg class="site-action-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path class="site-action-icon-outline" d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3.3-.4 6.8-1.6 6.8-7A5.5 5.5 0 0 0 19.3 3.7 5.1 5.1 0 0 0 19.1 0S17.9-.4 15 1.5a13.4 13.4 0 0 0-6 0C6.1-.4 4.9 0 4.9 0a5.1 5.1 0 0 0-.2 3.7 5.5 5.5 0 0 0-1.5 3.8c0 5.4 3.5 6.6 6.8 7A4.8 4.8 0 0 0 9 18v4"></path>
<path class="site-action-icon-outline" d="M9 19c-3 .9-3-1.5-4.2-2"></path>
<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3.3-.4 6.8-1.6 6.8-7A5.5 5.5 0 0 0 19.3 3.7 5.1 5.1 0 0 0 19.1 0S17.9-.4 15 1.5a13.4 13.4 0 0 0-6 0C6.1-.4 4.9 0 4.9 0a5.1 5.1 0 0 0-.2 3.7 5.5 5.5 0 0 0-1.5 3.8c0 5.4 3.5 6.6 6.8 7A4.8 4.8 0 0 0 9 18v4"></path>
<path d="M9 19c-3 .9-3-1.5-4.2-2"></path>
</svg>
</a>
</header>
<script type="module" src="/src/main.mjs"></script>
</body>
</html>
66 changes: 66 additions & 0 deletions src/adapters/cityflow/notes/provenance-bible.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# cssCityflow provenance

## Authority

- Project: XScreenSaver Cityflow
- Revision: `906693799e4fb7581436590cf84ecb2d3c9186ba`
- Primary source: `hacks/glx/cityflow.c`
- Primary SHA-256: `9113c9f3214ba6c1f350b3863c306e6015e47a856a17bbe24d597f909dfa027b`
- Configuration: `hacks/config/cityflow.xml`
- Configuration SHA-256: `0453c12e39e4b7d32e66a7780d69979aaaa42d0632a3b6757df9c1cee6c7c919`

The primary file grants permission to use, copy, modify, distribute, and sell
the software and documentation provided its copyright and permission notices
are preserved. Cityflow has no external model or texture data.

## Source contract

The default mode initializes 800 randomly positioned and skewed boxes, six
moving interference sources, a radius of 256, wave speed 25, skew 12, and a
20 ms delay. Although each box is a solid, the source deliberately emits only
its top, front, and right quads; the left and back faces are compiled out and
there is no bottom face. The default retained census is therefore 800 roots
and 2,400 leaves.

The product uses the source-supported `count` option at 200 boxes in one
prepared bank. The source default remains 800, and the source's
`1.8 / sqrt(count)` box scale is preserved for the selected count.

The deterministic prepared seed is product authoring input because the native
screen saver normally seeds `ya_random` from process and wall-clock state. The
source equations, random stream, smooth color-map construction, draw cadence,
lighting, and camera remain authoritative.

## Runtime boundary

Preparation writes one static Morph package plus source-cadence typed transform
and color-index tables. Every box retains three solid quad leaves.
Because CSS perspective has no OpenGL far plane, preparation derives the side
face cutoff from the exact source model-view sequence and clips the source
`bottom = 5` faces conservatively at eye-space `z = -50`. Each prepared box
matrix carries real initial geometry rather than adapter metadata. Runtime
derives the exact static source-light factors from that matrix, then applies
only changed prepared transform indices and palette classes. Scheduling follows
the merged DOMFORMAT `polycss-playback@0` precedent: a timer waits for the next
deadline, one paint-aligned callback publishes the due state, and `elapsed`
catch-up collapses missed source ticks. There is no runtime geometry,
rasterization, topology rebuilding, or DOM growth.

## Proof state

- Source-state oracle: product ticks 0, 73, and 251 match the pinned
C implementation within explicit float tolerance.
- Browser retained-DOM smoke: desktop and mobile viewports mount the same
expected leaf census, keep DOM identity stable, publish prepared transforms,
and construct/redraw no atlases.
- Native visual capture: the headless macOS CGL harness executes the pinned
`cityflow.c` source directly; two 48-frame runs are byte-identical.
- Browser visual parity: 48 source-cadence frames pass the calibrated native /
retained-DOM comparison at mean delta `<= 1`, changed-pixel ratio `<= 0.05`,
and channel threshold `2`. The worst observed mean delta is `0.541659`; the
residual is confined to polygon-edge rasterization.
- FrameSleuth performance on the untouched 1280 x 720 product route records a
`19.728 ms` DrawFrame p50 and `34.213 ms` p95 against the source's `20 ms`
cadence. The only scheduled work is the DOMFORMAT-precedented timer /
paint-aligned callback; publications use prepared data and the retained graph
remains 200 roots / 600 leaves.
15 changes: 15 additions & 0 deletions src/adapters/cityflow/notes/references/source-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"schema": "csscityflow-source-lock@1",
"project": "XScreenSaver Cityflow",
"repository": "https://github.com/Zygo/xscreensaver",
"revision": "906693799e4fb7581436590cf84ecb2d3c9186ba",
"primary": {
"path": "hacks/glx/cityflow.c",
"sha256": "9113c9f3214ba6c1f350b3863c306e6015e47a856a17bbe24d597f909dfa027b"
},
"config": {
"path": "hacks/config/cityflow.xml",
"sha256": "0453c12e39e4b7d32e66a7780d69979aaaa42d0632a3b6757df9c1cee6c7c919"
},
"license": "permissive notice embedded in cityflow.c"
}
90 changes: 90 additions & 0 deletions src/adapters/cityflow/src/csscityflow/client.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { createPolyPerspectiveCamera } from "@layoutit/polycss";
import { loadPolyMorphPackage, mountPolyMorphModel } from "@layoutit/polycss-morph";
import {
createCityflowPreparedPlayer,
loadCityflowPreparedPlayback,
} from "./preparedPlayback.mjs";

export function mountCityflow(host) {
const state = { ready: false, errors: [], bankId: null, mounted: null, player: null };
globalThis.__csscityflow = state;
main().catch((error) => fail(error));

async function main() {
const bankId = "desktop";
const modelId = "cityflow";
const [loaded, playback] = await Promise.all([
loadPolyMorphPackage("/csscityflow/", { modelId }),
loadCityflowPreparedPlayback(`/csscityflow/${modelId}.playback.json`),
loadPreparedStylesheet(`/csscityflow/${modelId}.css`),
]);
const perspective = innerHeight / (2 * Math.tan(Math.PI / 12));
const mounted = mountPolyMorphModel(host, loaded.model, {
resources: loaded.resources,
camera: createPolyPerspectiveCamera({
perspective,
target: [0, 0, 0],
rotX: 0,
rotY: 0,
zoom: 50,
distance: -perspective,
}),
});
const shapeElements = [];
for (const [index, box] of loaded.model.render.shapes.entries()) {
const element = mounted.shapeElements.get(box.id);
if (!element) throw new Error(`Missing retained Cityflow box ${box.id}`);
const source = readBoxState(loaded.model, index);
element.classList.add("csscityflow-box");
shapeElements.push(element);
for (const [faceIndex, leaf] of [...element.children].entries()) {
leaf.style.setProperty("--csscityflow-light", source.lightFactors[faceIndex]);
}
}
const player = createCityflowPreparedPlayer({ playback, mounted, shapeElements });
state.ready = true;
state.bankId = bankId;
state.mounted = mounted;
state.player = player;
document.body.classList.replace("loading", "ready");
player.resume();
}

function fail(error) {
state.errors.push(String(error?.stack || error));
document.body.classList.remove("loading");
document.body.classList.add("error");
console.error(error);
}
return state;
}

function loadPreparedStylesheet(href) {
return new Promise((resolveLoad, rejectLoad) => {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = href;
link.addEventListener("load", resolveLoad, { once: true });
link.addEventListener("error", () => rejectLoad(new Error(`Cityflow stylesheet failed: ${href}`)), { once: true });
document.head.append(link);
});
}

function readBoxState(model, index) {
const shape = model.render.shapes[index];
const matrix = shape?.matrix;
if (!shape || !Array.isArray(matrix) || matrix.length !== 16) {
throw new Error("Prepared Cityflow box transform is missing");
}
const width = Math.hypot(matrix[0], matrix[1]);
const cth = matrix[0] / width;
const sth = -matrix[1] / width;
const lightLength = Math.hypot(0, 0.25, -1);
return Object.freeze({
lightFactors: Object.freeze([
0.4 + 1 / lightLength,
0.4 + Math.max(0, cth * 0.25 / lightLength),
0.4 + Math.max(0, -sth * 0.25 / lightLength),
]),
});
}
Loading