Skip to content
Open
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ pnpm build:3dpipes:full
pnpm dev:3dpipes
```

### Flower Box

[`src/adapters/flowerbox`](src/adapters/flowerbox) is an independent PolyCSS
reconstruction of the classic Flower Box. Its rounded prepared cycle uses
1,200 stable retained triangle leaves, prepared Morph transforms, and a
hash-bound q60 space-texel lighting atlas.

```sh
pnpm prepare:flowerbox:artifact
pnpm build:flowerbox
pnpm dev:flowerbox
```

Microsoft source, binaries, captures, and oracle packets are not included.

## License

cssGraphics source code is [MIT licensed](LICENSE). Third-party models retain
Expand Down
5 changes: 5 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@
to = "/pipes/"
status = 302
force = true

[[headers]]
for = "/cssflower/assets/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,29 @@
"scripts": {
"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",
"build": "pnpm build:site && pnpm build:lib",
"build:site": "vite build",
"build:lib": "vite build --mode library && node scripts/build-cli.mjs",
"build:3dpipes": "vite build --config src/adapters/3dpipes/vite.config.mjs",
"build:3dpipes:full": "pnpm prepare:3dpipes && pnpm build:3dpipes",
"build:deploy": "pnpm exec playwright install chromium && pnpm prepare:3dpipes && CSSPIPES_DEPLOY_BUILD=1 pnpm build:3dpipes",
"build:flowerbox": "vite build --config src/adapters/flowerbox/vite.config.mjs",
"build:flowerbox:full": "pnpm prepare:flowerbox:artifact && pnpm build:flowerbox",
"build:deploy": "pnpm exec playwright install chromium && pnpm prepare:3dpipes && pnpm prepare:flowerbox:artifact && CSSPIPES_DEPLOY_BUILD=1 pnpm build:3dpipes && CSSFLOWER_DEPLOY_BUILD=1 pnpm build:flowerbox",
"prepack": "pnpm build:lib",
"preview": "vite preview --host 127.0.0.1",
"prepare:super-mario-64": "pnpm build:lib && node scripts/prepare/super-mario-64.mjs",
"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:catalog": "pnpm build:lib && node scripts/prepare/catalog.mjs",
"prepare:site-models": "node scripts/prepare/site-models.mjs",
"prepare:site-previews": "pnpm prepare:site-models && node scripts/prepare/site-previews.mjs",
"verify:source-only": "node scripts/verify-source-only.mjs --strict",
"verify:flowerbox:bank": "node src/adapters/flowerbox/tools/verify-product-bank.mjs",
"verify:flowerbox:runtime": "node src/adapters/flowerbox/tools/audit-runtime-surface.mjs",
"test:flowerbox:browser": "node src/adapters/flowerbox/tools/smoke-browser.mjs",
"test:flowerbox:deploy": "node src/adapters/flowerbox/tools/smoke-browser.mjs --deploy",
"typecheck": "tsc --noEmit"
},
"dependencies": {
Expand Down
101 changes: 101 additions & 0 deletions scripts/prepare/flowerbox-product-bank.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env node

import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { inspectFlowerboxProductBank } from "../../src/adapters/flowerbox/tools/productBank.mjs";

const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
const lock = JSON.parse(await readFile(
join(repositoryRoot, "src/adapters/flowerbox/prepared-bank.lock.json"),
"utf8",
));
if (lock.schema !== "cssflower-prepared-bank-lock@3" ||
lock.retainedTriangleLeafCount !== 1_200 || lock.retainedRotationRootCount !== 1 ||
lock.timelineStateCount !== 360 || lock.geometryStateCount !== 46 ||
lock.transformBlockCount !== 3 || lock.lightingAssetCount !== 1 ||
lock.lightingQuality !== 60 || lock.visibilityMinimumOwnedPixels !== 8) {
throw new Error("Flower Box prepared-bank lock does not bind the rounded retained Morph product");
}
const generatedRoot = resolve(
process.env.CSSFLOWER_GENERATED_ROOT ?? join(repositoryRoot, "build", "generated"),
);
const publicRoot = join(generatedRoot, "public");
const targetRoot = join(publicRoot, "cssflower");

try {
const current = await inspectFlowerboxProductBank(targetRoot);
const descriptorBytes = await readFile(join(targetRoot, "product-bank.json"));
if (current.closureSha256 === lock.productClosureSha256 &&
current.closureBytes === lock.productClosureBytes &&
current.fileCount === lock.productFileCount &&
descriptorBytes.length === lock.productDescriptorByteLength &&
sha256(descriptorBytes) === lock.productDescriptorSha256) {
process.stdout.write(`${JSON.stringify({ status: "ready", source: "existing", ...current }, null, 2)}\n`);
process.exit(0);
}
} catch {
// Missing or stale output is replaced only after the downloaded bank verifies.
}

const localArchive = process.env.CSSFLOWER_PRODUCT_BANK_ARCHIVE;
const cacheRoot = join(repositoryRoot, ".local", "downloads", "cssflower");
const archivePath = localArchive
? resolve(localArchive)
: join(cacheRoot, `${lock.archiveSha256}.tar.gz`);
let archiveBytes;
try {
archiveBytes = await readFile(archivePath);
} catch (error) {
if (localArchive || error?.code !== "ENOENT") throw error;
const response = await fetch(lock.url, { redirect: "follow" });
if (!response.ok) throw new Error(`Flower Box product bank download failed: ${response.status}`);
archiveBytes = Buffer.from(await response.arrayBuffer());
await mkdir(cacheRoot, { recursive: true });
await writeFile(archivePath, archiveBytes);
}
if (archiveBytes.length !== lock.archiveByteLength || sha256(archiveBytes) !== lock.archiveSha256) {
throw new Error("Flower Box product bank archive identity mismatch");
}

const listing = spawnSync("tar", ["-tzf", archivePath], { encoding: "utf8" });
if (listing.error) throw listing.error;
if (listing.status !== 0) throw new Error(`Unable to inspect Flower Box product bank:\n${listing.stderr}`);
const entries = listing.stdout.trim().split("\n").filter(Boolean);
if (entries.length === 0 || entries.some((entry) =>
entry.startsWith("/") || entry.split("/").includes("..") ||
!(entry === "cssflower" || entry === "cssflower/" || entry.startsWith("cssflower/")))) {
throw new Error("Flower Box product bank archive contains an unsafe path");
}

const stagingRoot = join(generatedRoot, `.flowerbox-product-${process.pid}`);
await rm(stagingRoot, { recursive: true, force: true });
await mkdir(stagingRoot, { recursive: true });
const extraction = spawnSync("tar", ["-xzf", archivePath, "-C", stagingRoot], { encoding: "utf8" });
if (extraction.error) throw extraction.error;
if (extraction.status !== 0) throw new Error(`Unable to extract Flower Box product bank:\n${extraction.stderr}`);
try {
const stagedProduct = join(stagingRoot, "cssflower");
const summary = await inspectFlowerboxProductBank(stagedProduct);
const descriptorBytes = await readFile(join(stagedProduct, "product-bank.json"));
if (summary.closureSha256 !== lock.productClosureSha256 || summary.closureBytes !== lock.productClosureBytes ||
summary.fileCount !== lock.productFileCount) {
throw new Error("Flower Box unpacked product bank identity mismatch");
}
if (descriptorBytes.length !== lock.productDescriptorByteLength ||
sha256(descriptorBytes) !== lock.productDescriptorSha256) {
throw new Error("Flower Box product-bank descriptor identity mismatch");
}
await mkdir(publicRoot, { recursive: true });
await rm(targetRoot, { recursive: true, force: true });
await rename(stagedProduct, targetRoot);
process.stdout.write(`${JSON.stringify({ status: "ready", source: localArchive ? "local-archive" : "release", ...summary }, null, 2)}\n`);
} finally {
await rm(stagingRoot, { recursive: true, force: true });
}

function sha256(bytes) {
return createHash("sha256").update(bytes).digest("hex");
}
7 changes: 6 additions & 1 deletion scripts/verify-source-only.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const STRICT_SOURCE_RENDERER_PATTERNS = Object.freeze([
["wasm-runtime", /WebAssembly\.(?:compile|instantiate)|instantiateStreaming\(|\.wasm(?:\b|["'])/iu],
["emulator-runtime", /(?:from|import\s*)[\s\S]{0,80}["'][^"']*(?:dosbox|mupen64|retroarch|emulator)[^"']*["']/iu],
]);
const STRICT_AUDITOR_PATHS = new Set();
const STRICT_AUDITOR_PATHS = new Set([
"src/adapters/flowerbox/tools/audit-runtime-surface.mjs",
]);
const STRICT_PATH_GUARD_PATHS = new Set([
"scripts/verify-source-only.mjs",
]);
Expand All @@ -34,9 +36,12 @@ const DISTRIBUTION_PREFIXES = Object.freeze([
"site/public/previews/",
]);
const README_MEDIA_PATHS = new Set([
"site/public/favicon.ico",
"site/readme/animated-morph-sphere.gif",
"site/readme/cube-to-sphere.gif",
"site/readme/pipes.gif",
"src/adapters/3dpipes/public/pipes-social.png",
"src/adapters/flowerbox/public/flower-social.png",
]);
const SHA256 = /^[a-f0-9]{64}$/u;

Expand Down
3 changes: 3 additions & 0 deletions site/public/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@
<url>
<loc>https://css.graphics/pipes/</loc>
</url>
<url>
<loc>https://css.graphics/flower/</loc>
</url>
</urlset>
38 changes: 38 additions & 0 deletions src/adapters/flowerbox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Flower Box

An independently authored PolyCSS reconstruction of the classic 1995 Flower
Box. The rounded default-cube bloom and complete rotation cycle are rendered
through 1,200 stable retained HTML triangle leaves and one retained rotation
root.

The browser loads one prepared PolyCSS snapshot, three prepared matrix3d
blocks, and one q60 prepared space-texel lighting grid. PolyCSS Morph writes
only selected prepared leaf transforms; a prepared source-camera visibility
schedule suppresses cells owning fewer than eight pixels. The runtime does not
construct geometry, project vertices, calculate normals or lighting,
rasterize, or grow the DOM. The canonical page is the responsive `/` route;
there is no separate presentation or oracle mode.

From the repository root:

```sh
pnpm prepare:flowerbox:artifact
pnpm build:flowerbox
pnpm dev:flowerbox
```

The public rounded q60 product bank is a content-addressed build artifact. Its
lock binds the archive and unpacked closure; generated output remains ignored
under `build/generated/`. A full source preparation is available separately
through `pnpm prepare:flowerbox:source` when the pinned `avifenc` is supplied.

The source profile is bound to `DigitalMars/dmc` revision
`9478d25a677f70dbe4fc0ed317cc5a5e5050ef8b`. Exact native-state qualification
was performed locally across 9,331 ticks. That evidence and the owned native
inputs are not part of the product bank; native/browser pixel parity is not
claimed.

The implementation is covered by the repository's [MIT license](../../../LICENSE).
Microsoft source, binaries, native captures, and oracle packets are not
included or downloaded. This independent experiment is not affiliated with or
endorsed by Microsoft.
23 changes: 23 additions & 0 deletions src/adapters/flowerbox/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flower Box — HTML and CSS experiment</title>
<meta name="description" content="The classic Flower Box reconstructed as 1,200 retained HTML and CSS triangles with PolyCSS." />
<link rel="canonical" href="https://css.graphics/flower/" />
<meta property="og:title" content="Flower Box — HTML and CSS experiment" />
<meta property="og:description" content="A complete prepared Flower Box cycle rendered with retained HTML and CSS triangles." />
<meta property="og:url" content="https://css.graphics/flower/" />
<meta property="og:image" content="https://css.graphics/flower/flower-social.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Flower Box — HTML and CSS experiment" />
<meta name="twitter:description" content="A complete prepared Flower Box cycle rendered with retained HTML and CSS triangles." />
<meta name="twitter:image" content="https://css.graphics/flower/flower-social.png" />
<link rel="stylesheet" href="/src/cssflower/styles.css" />
<script type="module" src="/src/main.mjs"></script>
</head>
<body></body>
</html>
30 changes: 30 additions & 0 deletions src/adapters/flowerbox/prepared-bank.lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"schema": "cssflower-prepared-bank-lock@3",
"tag": "cssflower-product-rounded-q60-v1",
"asset": "cssflower-product-rounded-q60-v1.tar.gz",
"url": "https://github.com/layoutit/cssGraphics/releases/download/cssflower-product-rounded-q60-v1/cssflower-product-rounded-q60-v1.tar.gz",
"archiveByteLength": 5312404,
"archiveSha256": "8ad850187c297ab8f1671a29f656f4eb098d8370219183e4b37f9e6c4b1fe9e5",
"productClosureBytes": 5324102,
"productClosureSha256": "f7e8f9144b859fc34ccbc74a980e07180154ece23eb0783f85f92d6470655b83",
"productFileCount": 7,
"productDescriptorByteLength": 1886,
"productDescriptorSha256": "2418737362c5c50f2f245dabef45571b002fc279ca165863e352d2f91d517758",
"retainedTriangleLeafCount": 1200,
"retainedRotationRootCount": 1,
"timelineStateCount": 360,
"geometryStateCount": 46,
"retainedSourceOracleTimelineStateCount": 9331,
"transformBlockCount": 3,
"transformAssetBytes": 1347149,
"lightingAssetCount": 1,
"lightingAssetBytes": 3848891,
"lightingQuality": 60,
"visibilityMinimumOwnedPixels": 8,
"publicBoundary": {
"microsoftSourceIncluded": false,
"microsoftBinaryIncluded": false,
"nativeCaptureIncluded": false,
"oraclePacketIncluded": false
}
}
Binary file added src/adapters/flowerbox/public/flower-social.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 86 additions & 0 deletions src/adapters/flowerbox/src/cssflower/client.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
loadPreparedManifest,
loadPreparedScene,
} from "./manifestClient.mjs";
import { mountPreparedPolycssSnapshot } from "./polycssScene.mjs";
import { createCssflowerPreparedPlayer } from "./preparedPlayback.mjs";
import { createRouteState } from "./routeState.mjs";
import { installCssflowerDebugApi } from "./debugApi.mjs";
import { installCssflowerStagePresentation } from "./stagePresentation.mjs";

export function mountCssflowerClient(host) {
if (!(host instanceof HTMLElement)) throw new TypeError("cssFlower scene host is missing");
const state = {
ready: false,
status: "loading",
route: null,
manifest: null,
sceneData: null,
mount: null,
errors: [],
};
installCssflowerDebugApi(state);

window.addEventListener("error", (event) => {
recordError(state, host, event.message || String(event.error || "error"));
});
window.addEventListener("unhandledrejection", (event) => {
recordError(state, host, String(event.reason?.message || event.reason || "unhandled rejection"));
});

main().catch((error) => {
recordError(state, host, error.stack || error.message || String(error));
});

async function main() {
const route = createRouteState();
state.route = route;

const manifest = await loadPreparedManifest(route);
state.manifest = manifest;

const { entry, sceneData, snapshotHtml, preparedAssets } = await loadPreparedScene(manifest, route);
state.sceneData = sceneData;

const snapshot = mountPreparedPolycssSnapshot({ host, sceneData, snapshotHtml, preparedAssets });
const presentation = installCssflowerStagePresentation({ host, camera: snapshot.camera });
const player = await createCssflowerPreparedPlayer({
playback: sceneData.playback,
lighting: sceneData.lighting,
rotationRoot: snapshot.rotationRoot,
mesh: snapshot.mesh,
leaves: snapshot.leaves,
transformBlocks: preparedAssets.transformBlocks,
lightingPages: preparedAssets.lightingPages,
});
state.mount = Object.freeze({
...snapshot,
player,
stats() {
return Object.freeze({
...player.stats(),
...snapshot.stats(),
...presentation.stats(),
});
},
destroy() {
presentation.destroy();
player.destroy();
preparedAssets.transformBlocks.destroy();
preparedAssets.lightingPages.destroy();
snapshot.destroy();
},
});
state.ready = true;
state.status = "ready";
host.classList.add("r");
requestAnimationFrame(() => player.resume());
}
}

function recordError(state, host, message) {
state.errors.push(message);
state.status = "error";
host.classList.remove("r");
host.classList.add("e");
}
Loading