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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Needs `git`, `ffmpeg`/`ffprobe` with libx264, and `python3` with Pillow and nump

After building, `bun run serve` serves `/demos/head-turn/` from the isolated output. The integration entry uses `<base href="./bundle/">`, with separate `site-ui.css` / `site-ui.js` for its single preview caption and Controls toggle. Below 800 CSS pixels the controls start closed; desktop keeps them available. Loading status belongs to the original application and remains visible until it finishes. The preview makes no shipped-package or version claim.

`demos/head-turn/bundle/` preserves all ten approved producer files byte for byte: `index.html`, `app.js`, `aperture.js`, `runtime.js`, `look.png`, `depth.jpg`, `scene.json`, `scene.bin.gz`, `README.md`, `THIRD_PARTY_LICENSES.txt`. The adjacent `bundle-manifest.json` records their path-sorted lengths and SHA-256 values; the build checks its pinned digest before fetching registry data. Verification checks the whole copied `demos/` tree and the complete deployment manifest. The original depth logic, renderer, geometry and licences are unchanged.
`demos/head-turn/bundle/` contains the approved ten-file preview inventory. The 2026-10-26 depth revision replaces `depth.jpg` and re-bakes `scene.json` / `scene.bin.gz` from that same confirmed atlas; the bundle README records the input and output hashes. The field reader, scene extractor and exact tuple deduplicator first reproduced the previous fields and complete gzip bytes as a control. The revision preserves ordered triangle-expanded neutral positions and UVs, all 224,144 triangles, draw order, eye aperture/core data and controller parameters. Depth-dependent deduplication now stores 132,540 vertices instead of 132,534. `index.html`, `app.js`, `aperture.js`, `runtime.js`, `look.png` and `THIRD_PARTY_LICENSES.txt` retain their previous bytes, as do the integration UI and separate curvature page. The adjacent `bundle-manifest.json` pins the revised inventory's path-sorted lengths and SHA-256 values; the build checks its digest before fetching registry data. Verification checks the whole copied `demos/` tree and the complete deployment manifest.

`demos/head-turn/curvature/` is a separate static page linked from the preview's caption: one WebGL fragment shader samples `original.webp` at each pixel's position plus depth × camera offset (pointer on desktop, drag on touch), with depth gamma, contrast and bias and a Show depth view. It reads the depth map and is not the head-turn feature. `depth.webp` is the supplied 1359×2000 depth map resampled into the painting's 1368×2000 frame by `x_depth = 0.99908·x − 2.939` (pixel indices, y unchanged), fitted to the silhouette edges of both images; a uniform 1359/1368 stretch left a median edge residual of 2.77 px, this fit 0.17 px. `verifyCurvaturePage` (in `src/head-turn.ts`) checks its five files, caption, back link, that every reference is relative and resolves, and the preview's link to it.

Expand Down
18 changes: 9 additions & 9 deletions demos/head-turn/bundle-manifest.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"schema": 1,
"sha256": "46477ab4b95d2f6aa2f804d2114f3f9dbc90d280c85c9c7aa58546dc018a5a2d",
"sha256": "515c89f1307308e5e41dcfe5a0fc7c69e5751539863ef7cb7c14e1cf6ffa38e2",
"files": [
{
"path": "README.md",
"bytes": 1832,
"sha256": "445bc45882d8b71c2826daeeccf7490377617107bd0b1aff591bfe6e90e057f3"
"bytes": 3979,
"sha256": "4b0cb0bc44ccbb56674d3cfd2b149b79b10722989fb964fc273345574d3b79d7"
},
{
"path": "THIRD_PARTY_LICENSES.txt",
Expand All @@ -24,8 +24,8 @@
},
{
"path": "depth.jpg",
"bytes": 242570,
"sha256": "72b50699ac18c9421d19142c2ddb7bfc8a59113f6ff45826153c16ef7c02cd09"
"bytes": 244996,
"sha256": "c814960c7e40dd1d5573a5e6d6e5d97eefeb309d015dea924359dc87f8e998a0"
},
{
"path": "index.html",
Expand All @@ -44,13 +44,13 @@
},
{
"path": "scene.bin.gz",
"bytes": 3409263,
"sha256": "af763aa40b3c80597c2cdf91d83713230efd1eef6625eafa1f0ba04222c3e168"
"bytes": 3405993,
"sha256": "4e52da5a46e86e200ca82a4ff5a1befbe1d5daa62ddd442f6aa3038e3a0b3e80"
},
{
"path": "scene.json",
"bytes": 3731,
"sha256": "82422ef7184e9b9c4f3999c510e2fde68c6dbf6c0739376ce0b79e7f6f34b806"
"bytes": 3730,
"sha256": "51a5bfdf5c2687871b9290fd2dcec9004b892c165e1ab098f1d008aa78b89f2a"
}
]
}
21 changes: 21 additions & 0 deletions demos/head-turn/bundle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,24 @@ Files:
The gzip payload is explicitly decoded by the page, so static hosting does not need automatic compression. Serve JavaScript as text/javascript, JSON as application/json, PNG/JPEG as images, and scene.bin.gz as application/gzip (or application/octet-stream). An absent Content-Encoding header is supported; a correctly auto-decoded response is also supported. Never label raw bytes with a compression header.

The compiled data is a rendering payload, not an editable skeleton or producer configuration. The runtime notices do not relicense the demo artwork.

## Confirmed depth revision (2026-10-26)

The linked depth image and compiled displacement values come from the same confirmed atlas. The field reader, dense scene extractor and exact tuple deduplicator are unchanged. Before this revision, an old-atlas control regenerated the previous fields and reproduced the complete previous gzip and scene JSON bytes, including gzip headers. This establishes the re-bake chain; the new depth intentionally changes turned poses.

SHA-256 provenance:

| Input or output | SHA-256 |
| --- | --- |
| Confirmed `depth.jpg` | `c814960c7e40dd1d5573a5e6d6e5d97eefeb309d015dea924359dc87f8e998a0` |
| Regenerated depth fields | `27e1ad3bf4f0a9b5ac983e786eda04fd33c33d5b168befac31f989c488e28af9` |
| Frozen field reader | `450b69019ee9cef8a92e53bb8d4ba94027418a60bd4d251fb5db2bd49ee5324e` |
| Frozen scene extractor | `55f81d766f1c71a663faa166464091678523321dd6dc83a2eaff3ce7f7b3cfbf` |
| Frozen tuple deduplicator | `e3f7f116b29932bfd1d9cfb429f505849a77b70f312e7bd22d073e21a9f417fb` |
| Decoded geometry payload | `0106351273baf144f276895e986c1f6e95c013bf3de64071d8561d0c678f3e19` |
| `scene.bin.gz` | `4e52da5a46e86e200ca82a4ff5a1befbe1d5daa62ddd442f6aa3038e3a0b3e80` |
| `scene.json` | `51a5bfdf5c2687871b9290fd2dcec9004b892c165e1ab098f1d008aa78b89f2a` |

The gzip recipe is `gzip.compress(raw, compresslevel=9, mtime=0)`, reproduced with Python 3.13.1 and zlib 1.2.12 (compile and runtime). This is a reproduced invocation, not a recovered historical command log. The scene finalization changes only its `buffer` name from `scene.bin` to `scene.bin.gz`; gzip headers are not patched. The decoded payload is 6,294,257 bytes; its raw gzip file is 3,405,993 bytes.

All 19 parts retain their ordered triangle-expanded Float64 base positions and UVs, 224,144 ordered triangles, common/iris drivers, eye masks and core data, bounds, basis and gain. Exact depth-dependent tuple deduplication increases the stored vertex count from 132,534 to 132,540. Packed indices and offsets may consequently differ. The renderer, gaze controller, colour atlas, original entry and runtime licence notices retain their previous bytes.
Binary file modified demos/head-turn/bundle/depth.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demos/head-turn/bundle/scene.bin.gz
Binary file not shown.
2 changes: 1 addition & 1 deletion demos/head-turn/bundle/scene.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"format":1,"buffer":"scene.bin.gz","bounds":{"x":-351.74672513788,"y":-3.850272288221205,"width":705.4742411296379,"height":1188.3414032307276},"basis":{"a":1.0039057916469938,"b":0.0137497879953016,"c":-0.013695170810545318,"d":0.9999062174544701},"depthGain":109.86819166092819,"parts":[{"base":[8,85958],"uv":[687672,85958],"tri":[1375336,214980],"indexWidth":2,"depth":{"values":[1805296,42979]},"common":2,"iris":-1},{"base":[2149128,11840],"uv":[2243848,11840],"tri":[2338568,28602],"indexWidth":2,"depth":{"values":[2395776,5920]},"common":1,"iris":-1},{"base":[2443136,1262],"uv":[2453232,1262],"tri":[2463328,3246],"indexWidth":2,"depth":{"constant":0},"common":0,"iris":-1},{"base":[2469824,6368],"uv":[2520768,6368],"tri":[2571712,15576],"indexWidth":2,"depth":{"constant":0},"common":0,"iris":-1},{"base":[2602864,71188],"uv":[3172368,71188],"tri":[3741872,178944],"indexWidth":2,"depth":{"constant":0},"common":0,"iris":-1},{"base":[4099760,1948],"uv":[4115344,1948],"tri":[4130928,5166],"indexWidth":2,"depth":{"constant":-0.7280316441945862},"common":1,"iris":-1},{"base":[4141264,1760],"uv":[4155344,1760],"tri":[4169424,4674],"indexWidth":2,"depth":{"constant":-0.9176215887663812},"common":1,"iris":-1},{"base":[4178776,35240],"uv":[4460696,35240],"tri":[4742616,97002],"indexWidth":2,"depth":{"values":[4936624,17620]},"common":1,"iris":-1},{"base":[5077584,488],"uv":[5081488,488],"tri":[5085392,1158],"indexWidth":2,"depth":{"constant":0.1813717910250977},"common":1,"iris":-1},{"base":[5087712,590],"uv":[5092432,590],"tri":[5097152,1386],"indexWidth":2,"depth":{"values":[5099928,295]},"common":1,"iris":-1},{"base":[5102288,1518],"uv":[5114432,1518],"tri":[5126576,3846],"indexWidth":2,"depth":{"constant":0.0001567105774445965},"common":1,"iris":-1},{"base":[5134272,1512],"uv":[5146368,1512],"tri":[5158464,3984],"indexWidth":2,"depth":{"constant":0.005297318078439361},"common":1,"iris":-1},{"base":[5166432,1132],"uv":[5175488,1132],"tri":[5184544,2880],"indexWidth":2,"depth":{"constant":0.0001567105774445965},"common":1,"iris":1},{"base":[5190304,1078],"uv":[5198928,1078],"tri":[5207552,2754],"indexWidth":2,"depth":{"constant":0.005297318078439361},"common":1,"iris":0},{"base":[5213064,2096],"uv":[5229832,2096],"tri":[5246600,5532],"indexWidth":2,"depth":{"constant":0.0001567105774445965},"common":1,"iris":-1},{"base":[5257664,2184],"uv":[5275136,2184],"tri":[5292608,5814],"indexWidth":2,"depth":{"constant":0.005297318078439361},"common":1,"iris":-1},{"base":[5304240,762],"uv":[5310336,762],"tri":[5316432,1818],"indexWidth":2,"depth":{"values":[5320072,381]},"common":1,"iris":-1},{"base":[5323120,37360],"uv":[5622000,37360],"tri":[5920880,93252],"indexWidth":2,"depth":{"values":[6107384,18680]},"common":1,"iris":-1},{"base":[6256824,784],"uv":[6263096,784],"tri":[6269368,1818],"indexWidth":2,"depth":{"values":[6273008,392]},"common":1,"iris":-1}],"eyes":[{"iris":13,"unit":11,"frame":[1156.0000040531158,612.000016361475,111.00001502037048,84.00000676512718],"base":[1.0039056560116206,0.013695168958304608,-0.01374976359122096,0.999906137031654,64.1697197614219,-769.1880035427055],"gazeBasis":[1.0039056560116206,-0.01374976359122096,-0.013695168958304608,-0.999906137031654],"anchor":[48.50001118755654,41.0000182729259],"size":[111,84],"core":[6276144,9324]},{"iris":12,"unit":10,"frame":[1.999999936670065,766.9999826550484,103.00000211969018,83.00002086162567],"base":[1.0039057711988868,0.013695170529676426,-0.013749761242656288,0.9999059662400712,-153.71908642328503,-769.1601367694789],"gazeBasis":[1.0039057711988868,-0.013749761242656288,-0.013695170529676426,-0.9999059662400712],"anchor":[58.000026449172424,39.999992758437884],"size":[103,83],"core":[6285468,8549]}]}
{"format":1,"buffer":"scene.bin.gz","bounds":{"x":-351.74672513788,"y":-3.850272288221205,"width":705.4742411296379,"height":1188.3414032307276},"basis":{"a":1.0039057916469938,"b":0.0137497879953016,"c":-0.013695170810545318,"d":0.9999062174544701},"depthGain":109.86819166092819,"parts":[{"base":[8,85966],"uv":[687736,85966],"tri":[1375464,214980],"indexWidth":2,"depth":{"values":[1805424,42983]},"common":2,"iris":-1},{"base":[2149288,11842],"uv":[2244024,11842],"tri":[2338760,28602],"indexWidth":2,"depth":{"values":[2395968,5921]},"common":1,"iris":-1},{"base":[2443336,1262],"uv":[2453432,1262],"tri":[2463528,3246],"indexWidth":2,"depth":{"constant":0},"common":0,"iris":-1},{"base":[2470024,6368],"uv":[2520968,6368],"tri":[2571912,15576],"indexWidth":2,"depth":{"constant":0},"common":0,"iris":-1},{"base":[2603064,71188],"uv":[3172568,71188],"tri":[3742072,178944],"indexWidth":2,"depth":{"constant":0},"common":0,"iris":-1},{"base":[4099960,1948],"uv":[4115544,1948],"tri":[4131128,5166],"indexWidth":2,"depth":{"constant":-0.3422005872291952},"common":1,"iris":-1},{"base":[4141464,1760],"uv":[4155544,1760],"tri":[4169624,4674],"indexWidth":2,"depth":{"constant":-0.43582416241717215},"common":1,"iris":-1},{"base":[4178976,35242],"uv":[4460912,35242],"tri":[4742848,97002],"indexWidth":2,"depth":{"values":[4936856,17621]},"common":1,"iris":-1},{"base":[5077824,488],"uv":[5081728,488],"tri":[5085632,1158],"indexWidth":2,"depth":{"constant":0.17680768859630025},"common":1,"iris":-1},{"base":[5087952,590],"uv":[5092672,590],"tri":[5097392,1386],"indexWidth":2,"depth":{"values":[5100168,295]},"common":1,"iris":-1},{"base":[5102528,1518],"uv":[5114672,1518],"tri":[5126816,3846],"indexWidth":2,"depth":{"constant":-0.06940427590312848},"common":1,"iris":-1},{"base":[5134512,1512],"uv":[5146608,1512],"tri":[5158704,3984],"indexWidth":2,"depth":{"constant":-0.06562773119764409},"common":1,"iris":-1},{"base":[5166672,1132],"uv":[5175728,1132],"tri":[5184784,2880],"indexWidth":2,"depth":{"constant":-0.06940427590312848},"common":1,"iris":1},{"base":[5190544,1078],"uv":[5199168,1078],"tri":[5207792,2754],"indexWidth":2,"depth":{"constant":-0.06562773119764409},"common":1,"iris":0},{"base":[5213304,2096],"uv":[5230072,2096],"tri":[5246840,5532],"indexWidth":2,"depth":{"constant":-0.06940427590312848},"common":1,"iris":-1},{"base":[5257904,2184],"uv":[5275376,2184],"tri":[5292848,5814],"indexWidth":2,"depth":{"constant":-0.06562773119764409},"common":1,"iris":-1},{"base":[5304480,762],"uv":[5310576,762],"tri":[5316672,1818],"indexWidth":2,"depth":{"values":[5320312,381]},"common":1,"iris":-1},{"base":[5323360,37360],"uv":[5622240,37360],"tri":[5921120,93252],"indexWidth":2,"depth":{"values":[6107624,18680]},"common":1,"iris":-1},{"base":[6257064,784],"uv":[6263336,784],"tri":[6269608,1818],"indexWidth":2,"depth":{"values":[6273248,392]},"common":1,"iris":-1}],"eyes":[{"iris":13,"unit":11,"frame":[1156.0000040531158,612.000016361475,111.00001502037048,84.00000676512718],"base":[1.0039056560116206,0.013695168958304608,-0.01374976359122096,0.999906137031654,64.1697197614219,-769.1880035427055],"gazeBasis":[1.0039056560116206,-0.01374976359122096,-0.013695168958304608,-0.999906137031654],"anchor":[48.50001118755654,41.0000182729259],"size":[111,84],"core":[6276384,9324]},{"iris":12,"unit":10,"frame":[1.999999936670065,766.9999826550484,103.00000211969018,83.00002086162567],"base":[1.0039057711988868,0.013695170529676426,-0.013749761242656288,0.9999059662400712,-153.71908642328503,-769.1601367694789],"gazeBasis":[1.0039057711988868,-0.013749761242656288,-0.013695170529676426,-0.9999059662400712],"anchor":[58.000026449172424,39.999992758437884],"size":[103,83],"core":[6285708,8549]}]}
39 changes: 38 additions & 1 deletion src/head-turn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,52 @@ import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:f
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { ROOT_DIR } from './output.ts';
import { artifactManifest } from './deployment.ts';
import { CURVATURE_PATH, HEAD_TURN_PATH, verifyCurvaturePage, verifyHeadTurnBundle } from './head-turn.ts';
const read = (path: string) => readFileSync(join(ROOT_DIR, path), 'utf8');

test('approved producer bundle is byte exact, including original entry and raw gzip', () => {
test('approved depth revision bundle is byte exact, including original entry and raw gzip', () => {
verifyHeadTurnBundle(ROOT_DIR);
const gzip = readFileSync(join(ROOT_DIR, HEAD_TURN_PATH, 'bundle/scene.bin.gz'));
expect([...gzip.subarray(0, 2)]).toEqual([31, 139]);
});

test('changed depth image or compiled depth payload fails the approved inventory', () => {
const copy = mkdtempSync(join(tmpdir(), 'head-turn-depth-'));
try {
const demo = join(copy, HEAD_TURN_PATH);
cpSync(join(ROOT_DIR, HEAD_TURN_PATH, 'bundle'), join(demo, 'bundle'), { recursive: true });
cpSync(join(ROOT_DIR, HEAD_TURN_PATH, 'bundle-manifest.json'), join(demo, 'bundle-manifest.json'));
for (const name of ['depth.jpg', 'scene.bin.gz']) {
const path = join(demo, 'bundle', name);
const original = readFileSync(path);
const changed = Buffer.from(original);
changed[changed.length - 1] ^= 1;
writeFileSync(path, changed);
expect(() => verifyHeadTurnBundle(copy)).toThrow('approved ten-file inventory');
writeFileSync(path, original);
}
verifyHeadTurnBundle(copy);
} finally {
rmSync(copy, { recursive: true, force: true });
}
});

test('a self-consistent replacement manifest cannot retarget the approved depth pin', () => {
const copy = mkdtempSync(join(tmpdir(), 'head-turn-pin-'));
try {
const demo = join(copy, HEAD_TURN_PATH);
cpSync(join(ROOT_DIR, HEAD_TURN_PATH, 'bundle'), join(demo, 'bundle'), { recursive: true });
writeFileSync(join(demo, 'bundle', 'depth.jpg'), 'unapproved replacement depth');
// This manifest correctly hashes its files; only the independent approval pin rejects it.
const replacement = artifactManifest(join(demo, 'bundle'));
writeFileSync(join(demo, 'bundle-manifest.json'), JSON.stringify(replacement));
expect(() => verifyHeadTurnBundle(copy)).toThrow('approved ten-file inventory');
} finally {
rmSync(copy, { recursive: true, force: true });
}
});

test('site entry changes only caption and integration UI; original resources still use bundle base', () => {
const original = read(`${HEAD_TURN_PATH}/bundle/index.html`);
const entry = read(`${HEAD_TURN_PATH}/index.html`);
Expand Down
6 changes: 3 additions & 3 deletions src/head-turn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { join } from 'node:path';
import { artifactManifest, parseManifest } from './deployment.ts';

export const HEAD_TURN_PATH = 'demos/head-turn';
// Approved ten-file producer inventory; changes require a new producer handoff.
export const HEAD_TURN_BUNDLE_SHA256 = '46477ab4b95d2f6aa2f804d2114f3f9dbc90d280c85c9c7aa58546dc018a5a2d';
// Approved ten-file preview inventory, including the confirmed depth revision.
export const HEAD_TURN_BUNDLE_SHA256 = '515c89f1307308e5e41dcfe5a0fc7c69e5751539863ef7cb7c14e1cf6ffa38e2';
export function verifyHeadTurnBundle(root: string): void {
const demo = join(root, HEAD_TURN_PATH);
const expected = parseManifest(readFileSync(join(demo, 'bundle-manifest.json'), 'utf8'));
if (expected.sha256 !== HEAD_TURN_BUNDLE_SHA256 || expected.files.length !== 10
|| artifactManifest(join(demo, 'bundle')).sha256 !== expected.sha256) {
throw new Error('Head-turn producer bundle differs from the approved ten-file inventory');
throw new Error('Head-turn bundle differs from the approved ten-file inventory');
}
}

Expand Down
Loading