hyperframes 0.6.96 → 0.6.98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/beat-analyzer.global.js +326 -0
- package/dist/cli.js +3058 -1947
- package/dist/commands/layout-audit.browser.js +86 -0
- package/dist/hyperframe-runtime.js +20 -20
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +20 -20
- package/dist/skills/hyperframes-cli/SKILL.md +67 -103
- package/dist/skills/hyperframes-cli/references/doctor-browser.md +45 -0
- package/dist/skills/hyperframes-cli/references/init-and-scaffold.md +51 -0
- package/dist/skills/hyperframes-cli/references/lambda.md +132 -0
- package/dist/skills/hyperframes-cli/references/lint-validate-inspect.md +93 -0
- package/dist/skills/hyperframes-cli/references/preview-render.md +107 -0
- package/dist/skills/hyperframes-cli/references/upgrade-info-misc.md +75 -0
- package/dist/studio/assets/hyperframes-player-DgsMQSvV.js +418 -0
- package/dist/studio/assets/index-B62bDCQv.css +1 -0
- package/dist/studio/assets/index-Ce3pBm_I.js +252 -0
- package/dist/studio/assets/{index-BWFaypdT.js → index-D-ET9M0b.js} +1 -1
- package/dist/studio/assets/index-D-bS9Dxx.js +1 -0
- package/dist/studio/index.html +2 -2
- package/dist/templates/_shared/AGENTS.md +46 -21
- package/dist/templates/_shared/CLAUDE.md +16 -14
- package/package.json +3 -2
- package/dist/pngDecodeBlitWorker.js +0 -239
- package/dist/skills/gsap/SKILL.md +0 -240
- package/dist/skills/gsap/references/effects.md +0 -297
- package/dist/skills/gsap/scripts/extract-audio-data.py +0 -188
- package/dist/skills/hyperframes/SKILL.md +0 -491
- package/dist/skills/hyperframes/data-in-motion.md +0 -19
- package/dist/skills/hyperframes/house-style.md +0 -73
- package/dist/skills/hyperframes/palettes/bold-energetic.md +0 -14
- package/dist/skills/hyperframes/palettes/clean-corporate.md +0 -14
- package/dist/skills/hyperframes/palettes/dark-premium.md +0 -14
- package/dist/skills/hyperframes/palettes/jewel-rich.md +0 -14
- package/dist/skills/hyperframes/palettes/monochrome.md +0 -14
- package/dist/skills/hyperframes/palettes/nature-earth.md +0 -14
- package/dist/skills/hyperframes/palettes/neon-electric.md +0 -14
- package/dist/skills/hyperframes/palettes/pastel-soft.md +0 -14
- package/dist/skills/hyperframes/palettes/warm-editorial.md +0 -14
- package/dist/skills/hyperframes/patterns.md +0 -191
- package/dist/skills/hyperframes/references/audio-reactive.md +0 -76
- package/dist/skills/hyperframes/references/beat-direction.md +0 -171
- package/dist/skills/hyperframes/references/captions.md +0 -163
- package/dist/skills/hyperframes/references/css-patterns.md +0 -373
- package/dist/skills/hyperframes/references/design-picker.md +0 -117
- package/dist/skills/hyperframes/references/dynamic-techniques.md +0 -102
- package/dist/skills/hyperframes/references/html-in-canvas-patterns.md +0 -507
- package/dist/skills/hyperframes/references/motion-principles.md +0 -150
- package/dist/skills/hyperframes/references/narration.md +0 -92
- package/dist/skills/hyperframes/references/prompt-expansion.md +0 -68
- package/dist/skills/hyperframes/references/techniques.md +0 -525
- package/dist/skills/hyperframes/references/text-effects.md +0 -64
- package/dist/skills/hyperframes/references/transcript-guide.md +0 -107
- package/dist/skills/hyperframes/references/transitions/catalog.md +0 -117
- package/dist/skills/hyperframes/references/transitions/css-3d.md +0 -12
- package/dist/skills/hyperframes/references/transitions/css-blur.md +0 -51
- package/dist/skills/hyperframes/references/transitions/css-cover.md +0 -43
- package/dist/skills/hyperframes/references/transitions/css-destruction.md +0 -95
- package/dist/skills/hyperframes/references/transitions/css-dissolve.md +0 -66
- package/dist/skills/hyperframes/references/transitions/css-distortion.md +0 -45
- package/dist/skills/hyperframes/references/transitions/css-grid.md +0 -10
- package/dist/skills/hyperframes/references/transitions/css-light.md +0 -49
- package/dist/skills/hyperframes/references/transitions/css-mechanical.md +0 -30
- package/dist/skills/hyperframes/references/transitions/css-other.md +0 -25
- package/dist/skills/hyperframes/references/transitions/css-push.md +0 -41
- package/dist/skills/hyperframes/references/transitions/css-radial.md +0 -37
- package/dist/skills/hyperframes/references/transitions/css-scale.md +0 -24
- package/dist/skills/hyperframes/references/transitions.md +0 -138
- package/dist/skills/hyperframes/references/typography.md +0 -175
- package/dist/skills/hyperframes/references/video-composition.md +0 -62
- package/dist/skills/hyperframes/scripts/animation-map.mjs +0 -601
- package/dist/skills/hyperframes/scripts/contrast-report.mjs +0 -348
- package/dist/skills/hyperframes/scripts/package-loader.mjs +0 -269
- package/dist/skills/hyperframes/templates/design-picker.html +0 -1432
- package/dist/skills/hyperframes/visual-styles.md +0 -443
- package/dist/studio/assets/hyperframes-player-0esDKGRk.js +0 -418
- package/dist/studio/assets/index-B0twsRu0.css +0 -1
- package/dist/studio/assets/index-BA979yF1.js +0 -251
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{g as P}from"./index-Ce3pBm_I.js";function j(c,d){for(var s=0;s<d.length;s++){const a=d[s];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in c)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(c,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}))}var v={},w;function k(){if(w)return v;w=1,Object.defineProperty(v,"__esModule",{value:!0}),v.default=d;var c=window.OfflineAudioContext||window.webkitOfflineAudioContext;function d(e){var r=a(e);return r.start(0),[i,y,O(e.sampleRate),s].reduce(function(t,o){return o(t)},r.buffer.getChannelData(0))}function s(e){return e.sort(function(r,t){return t.count-r.count}).splice(0,5)[0].tempo}function a(e){var r=e.length,t=e.numberOfChannels,o=e.sampleRate,n=new c(t,r,o),u=n.createBufferSource();u.buffer=e;var f=n.createBiquadFilter();return f.type="lowpass",u.connect(f),f.connect(n.destination),u}function i(e){for(var r=[],t=.9,o=.3,n=15;r.length<n&&t>=o;)r=l(e,t),t-=.05;if(r.length<n)throw new Error("Could not find enough samples for a reliable detection.");return r}function l(e,r){for(var t=[],o=0,n=e.length;o<n;o+=1)e[o]>r&&(t.push(o),o+=1e4);return t}function y(e){var r=[];return e.forEach(function(t,o){for(var n=function(x){var g=e[o+x]-t,_=r.some(function(h){if(h.interval===g)return h.count+=1});_||r.push({interval:g,count:1})},u=0;u<10;u+=1)n(u)}),r}function O(e){return function(r){var t=[];return r.forEach(function(o){if(o.interval!==0){for(var n=60/(o.interval/e);n<90;)n*=2;for(;n>180;)n/=2;n=Math.round(n);var u=t.some(function(f){if(f.tempo===n)return f.count+=o.count});u||t.push({tempo:n,count:o.count})}}),t}}return v}var p,b;function q(){return b||(b=1,p=k().default),p}var m=q();const A=P(m),D=j({__proto__:null,default:A},[m]);export{D as i};
|
package/dist/studio/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>HyperFrames Studio</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-Ce3pBm_I.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B62bDCQv.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
# HyperFrames Composition Project
|
|
2
2
|
|
|
3
|
-
## Skills
|
|
3
|
+
## Skills — USE THESE FIRST
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Always invoke the relevant skill before writing or modifying compositions.** Skills encode framework-specific patterns (e.g., `window.__timelines` registration, `data-*` attribute semantics, shader-compatible CSS rules) that are NOT in generic web docs. Skipping them produces broken compositions.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
**Doing anything with HyperFrames?** Start at `/hyperframes-read-first` — it tells you what HyperFrames can do and which skill or workflow handles your intent (make a video, TTS / BGM, prep footage, author / animate, render, install blocks), and routes every "make me a video" request to the right workflow. Read it first, especially when there's no project context to orient you. The video workflows it routes to:
|
|
8
|
+
|
|
9
|
+
- `/product-launch-video` — a **product** URL or brief / script → 60-90s product launch / SaaS / promo video.
|
|
10
|
+
- `/website-to-video` — a **general** website / URL → a video _of_ the site (tour / showcase / social clip from captured visuals); a product **launch / promo** is `/product-launch-video`.
|
|
11
|
+
- `/faceless-explainer` — arbitrary text (topic / article / notes), **no URL, no website capture** → 60-90s faceless explainer.
|
|
12
|
+
- `/embedded-captions` — an existing talking-head video (MP4) → the same footage with captions / subtitles added (rail + embed, or pure-cinematic embed); the footage itself is untouched.
|
|
13
|
+
- `/graphic-overlays` — an existing talking-head / interview / podcast video (MP4) → the same footage **packaged with designed graphic overlays** (kinetic titles, lower-thirds, data callouts, pull-quotes, side panels, pip) synced to the transcript; the clip plays unchanged underneath. (Plain captions/subtitles → `/embedded-captions`.)
|
|
14
|
+
- `/pr-to-video` — a GitHub PR (URL / `owner/repo#N` / "this PR") → 30-90s code-change explainer (changelog / feature reveal / fix / refactor).
|
|
15
|
+
- `/motion-graphics` — a short (typically under 10s) design-led **motion graphic**, motion-is-the-message, no narration: kinetic type, a stat / number count-up, a chart, a logo sting, a lower-third / overlay, or an animated tweet / headline / captured-page highlight; rendered to MP4 or a transparent overlay. Longer / narrated / custom → `/general-video`.
|
|
16
|
+
- `/general-video` — fallback for any other video (title card, longer brand / sizzle reel, multi-scene montage, static loop, custom composition); the original hyperframes authoring flow, any length.
|
|
17
|
+
|
|
18
|
+
**Porting an existing composition?** `/remotion-to-hyperframes` translates a Remotion (React) composition into HyperFrames HTML — a source migration, separate from the creation workflows above.
|
|
19
|
+
|
|
20
|
+
The domain skills (`/hyperframes-core`, `/hyperframes-animation`, `/hyperframes-creative`, `/hyperframes-cli`, `/hyperframes-media`, `/hyperframes-registry`) and the full capability map live inside `/hyperframes-read-first` — it is the single source of truth for which skill handles which intent.
|
|
21
|
+
|
|
22
|
+
> **Tailwind v4 projects** (`hyperframes init --tailwind`): see `/hyperframes-core` → `references/tailwind.md`.
|
|
10
23
|
|
|
11
|
-
Skills
|
|
24
|
+
> **Skills not available?** Ask the user to run `npx hyperframes skills` and restart their
|
|
25
|
+
> agent session, or install manually: `npx skills add heygen-com/hyperframes`.
|
|
12
26
|
|
|
13
27
|
## Commands
|
|
14
28
|
|
|
@@ -17,46 +31,57 @@ npm run dev # start the preview server (long-running — keep it alive
|
|
|
17
31
|
npm run check # lint + validate + inspect
|
|
18
32
|
npm run render # render to MP4
|
|
19
33
|
npm run publish # publish and get a shareable link
|
|
34
|
+
npx hyperframes lint --verbose # include info-level findings
|
|
35
|
+
npx hyperframes lint --json # machine-readable output for CI
|
|
20
36
|
npx hyperframes docs <topic> # reference docs in terminal
|
|
21
37
|
```
|
|
22
38
|
|
|
23
39
|
> **`npm run dev` is a long-running server, not a one-shot command.** It blocks until stopped.
|
|
24
|
-
>
|
|
25
|
-
>
|
|
40
|
+
> In Claude Code, always run it with `run_in_background: true`. Never run it as a foreground
|
|
41
|
+
> command — it will time out and the server will die, breaking the browser preview.
|
|
42
|
+
|
|
43
|
+
## Documentation
|
|
44
|
+
|
|
45
|
+
**For quick reference**, use the local CLI docs command (no network required):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx hyperframes docs <topic>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Topics: `data-attributes`, `gsap`, `compositions`, `rendering`, `examples`, `troubleshooting`
|
|
52
|
+
|
|
53
|
+
**For full documentation**, discover pages via the machine-readable index — do NOT guess URLs:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
https://hyperframes.heygen.com/llms.txt
|
|
57
|
+
```
|
|
26
58
|
|
|
27
59
|
## Project Structure
|
|
28
60
|
|
|
29
61
|
- `index.html` — main composition (root timeline)
|
|
30
62
|
- `compositions/` — sub-compositions referenced via `data-composition-src`
|
|
31
|
-
- `assets/` — media files (video, audio, images)
|
|
32
63
|
- `meta.json` — project metadata (id, name)
|
|
33
64
|
- `transcript.json` — whisper word-level transcript (if generated)
|
|
34
65
|
|
|
35
|
-
## Linting —
|
|
66
|
+
## Linting — ALWAYS RUN AFTER CHANGES
|
|
36
67
|
|
|
37
|
-
After creating or editing any `.html` composition, run the full check before considering the task complete:
|
|
68
|
+
After creating or editing any `.html` composition, **always** run the full check before considering the task complete:
|
|
38
69
|
|
|
39
70
|
```bash
|
|
40
71
|
npm run check
|
|
41
72
|
```
|
|
42
73
|
|
|
43
|
-
Fix all errors before presenting the result.
|
|
74
|
+
Fix all errors before presenting the result. Inspect warnings should be reviewed before rendering.
|
|
44
75
|
|
|
45
76
|
## Key Rules
|
|
46
77
|
|
|
47
78
|
1. Every timed element needs `data-start`, `data-duration`, and `data-track-index`
|
|
48
|
-
2.
|
|
49
|
-
3.
|
|
79
|
+
2. Elements with timing **MUST** have `class="clip"` — the framework uses this for visibility control
|
|
80
|
+
3. Timelines must be paused and registered on `window.__timelines`:
|
|
50
81
|
```js
|
|
51
82
|
window.__timelines = window.__timelines || {};
|
|
52
83
|
window.__timelines["composition-id"] = gsap.timeline({ paused: true });
|
|
53
84
|
```
|
|
54
85
|
4. Videos use `muted` with a separate `<audio>` element for the audio track
|
|
55
|
-
5. Sub-compositions use `data-composition-src="compositions/file.html"`
|
|
86
|
+
5. Sub-compositions use `data-composition-src="compositions/file.html"` to reference other HTML files
|
|
56
87
|
6. Only deterministic logic — no `Date.now()`, no `Math.random()`, no network fetches
|
|
57
|
-
|
|
58
|
-
## Documentation
|
|
59
|
-
|
|
60
|
-
Full docs: https://hyperframes.heygen.com/introduction
|
|
61
|
-
|
|
62
|
-
Machine-readable index for AI tools: https://hyperframes.heygen.com/llms.txt
|
|
@@ -4,20 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
**Always invoke the relevant skill before writing or modifying compositions.** Skills encode framework-specific patterns (e.g., `window.__timelines` registration, `data-*` attribute semantics, shader-compatible CSS rules) that are NOT in generic web docs. Skipping them produces broken compositions.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
7
|
+
**Doing anything with HyperFrames?** Start at `/hyperframes-read-first` — it tells you what HyperFrames can do and which skill or workflow handles your intent (make a video, TTS / BGM, prep footage, author / animate, render, install blocks), and routes every "make me a video" request to the right workflow. Read it first, especially when there's no project context to orient you. The video workflows it routes to:
|
|
8
|
+
|
|
9
|
+
- `/product-launch-video` — a **product** URL or brief / script → 60-90s product launch / SaaS / promo video.
|
|
10
|
+
- `/website-to-video` — a **general** website / URL → a video _of_ the site (tour / showcase / social clip from captured visuals); a product **launch / promo** is `/product-launch-video`.
|
|
11
|
+
- `/faceless-explainer` — arbitrary text (topic / article / notes), **no URL, no website capture** → 60-90s faceless explainer.
|
|
12
|
+
- `/embedded-captions` — an existing talking-head video (MP4) → the same footage with captions / subtitles added (rail + embed, or pure-cinematic embed); the footage itself is untouched.
|
|
13
|
+
- `/graphic-overlays` — an existing talking-head / interview / podcast video (MP4) → the same footage **packaged with designed graphic overlays** (kinetic titles, lower-thirds, data callouts, pull-quotes, side panels, pip) synced to the transcript; the clip plays unchanged underneath. (Plain captions/subtitles → `/embedded-captions`.)
|
|
14
|
+
- `/pr-to-video` — a GitHub PR (URL / `owner/repo#N` / "this PR") → 30-90s code-change explainer (changelog / feature reveal / fix / refactor).
|
|
15
|
+
- `/motion-graphics` — a short (typically under 10s) design-led **motion graphic**, motion-is-the-message, no narration: kinetic type, a stat / number count-up, a chart, a logo sting, a lower-third / overlay, or an animated tweet / headline / captured-page highlight; rendered to MP4 or a transparent overlay. Longer / narrated / custom → `/general-video`.
|
|
16
|
+
- `/general-video` — fallback for any other video (title card, longer brand / sizzle reel, multi-scene montage, static loop, custom composition); the original hyperframes authoring flow, any length.
|
|
17
|
+
|
|
18
|
+
**Porting an existing composition?** `/remotion-to-hyperframes` translates a Remotion (React) composition into HyperFrames HTML — a source migration, separate from the creation workflows above.
|
|
19
|
+
|
|
20
|
+
The domain skills (`/hyperframes-core`, `/hyperframes-animation`, `/hyperframes-creative`, `/hyperframes-cli`, `/hyperframes-media`, `/hyperframes-registry`) and the full capability map live inside `/hyperframes-read-first` — it is the single source of truth for which skill handles which intent.
|
|
21
|
+
|
|
22
|
+
> **Tailwind v4 projects** (`hyperframes init --tailwind`): see `/hyperframes-core` → `references/tailwind.md`.
|
|
21
23
|
|
|
22
24
|
> **Skills not available?** Ask the user to run `npx hyperframes skills` and restart their
|
|
23
25
|
> agent session, or install manually: `npx skills add heygen-com/hyperframes`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hyperframes",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.98",
|
|
4
4
|
"description": "HyperFrames CLI — create, preview, and render HTML video compositions",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"test": "vitest run",
|
|
19
19
|
"dev": "tsx src/cli.ts",
|
|
20
|
-
"build": "bun run build:fonts && tsup && bun run build:runtime && bun run build:copy",
|
|
20
|
+
"build": "bun run build:fonts && tsup && bun run build:runtime && bun run build:beat-analyzer && bun run build:copy",
|
|
21
21
|
"build:fonts": "node scripts/build-fonts.mjs",
|
|
22
22
|
"build:runtime": "tsx scripts/build-runtime.ts",
|
|
23
|
+
"build:beat-analyzer": "node scripts/build-beat-analyzer.mjs",
|
|
23
24
|
"build:copy": "node scripts/build-copy.mjs",
|
|
24
25
|
"typecheck": "tsc --noEmit"
|
|
25
26
|
},
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { createRequire as __hf_createRequire } from "node:module";
|
|
2
|
-
import { fileURLToPath as __hf_fileURLToPath } from "node:url";
|
|
3
|
-
import { dirname as __hf_dirname } from "node:path";
|
|
4
|
-
var require = __hf_createRequire(import.meta.url);
|
|
5
|
-
var __filename = __hf_fileURLToPath(import.meta.url);
|
|
6
|
-
var __dirname = __hf_dirname(__filename);
|
|
7
|
-
|
|
8
|
-
// ../producer/src/services/pngDecodeBlitWorker.ts
|
|
9
|
-
import { parentPort } from "worker_threads";
|
|
10
|
-
|
|
11
|
-
// ../engine/src/utils/alphaBlit.ts
|
|
12
|
-
import { inflateSync } from "zlib";
|
|
13
|
-
function paeth(a, b, c) {
|
|
14
|
-
const p = a + b - c;
|
|
15
|
-
const pa = Math.abs(p - a);
|
|
16
|
-
const pb = Math.abs(p - b);
|
|
17
|
-
const pc = Math.abs(p - c);
|
|
18
|
-
if (pa <= pb && pa <= pc) return a;
|
|
19
|
-
if (pb <= pc) return b;
|
|
20
|
-
return c;
|
|
21
|
-
}
|
|
22
|
-
function decodePngRaw(buf, caller) {
|
|
23
|
-
if (buf[0] !== 137 || buf[1] !== 80 || buf[2] !== 78 || buf[3] !== 71 || buf[4] !== 13 || buf[5] !== 10 || buf[6] !== 26 || buf[7] !== 10) {
|
|
24
|
-
throw new Error(`${caller}: not a PNG file`);
|
|
25
|
-
}
|
|
26
|
-
let pos = 8;
|
|
27
|
-
let width = 0;
|
|
28
|
-
let height = 0;
|
|
29
|
-
let bitDepth = 0;
|
|
30
|
-
let colorType = 0;
|
|
31
|
-
let interlace = 0;
|
|
32
|
-
let sawIhdr = false;
|
|
33
|
-
const idatChunks = [];
|
|
34
|
-
while (pos + 12 <= buf.length) {
|
|
35
|
-
const chunkLen = buf.readUInt32BE(pos);
|
|
36
|
-
const chunkType = buf.toString("ascii", pos + 4, pos + 8);
|
|
37
|
-
const chunkData = buf.subarray(pos + 8, pos + 8 + chunkLen);
|
|
38
|
-
if (chunkType === "IHDR") {
|
|
39
|
-
width = chunkData.readUInt32BE(0);
|
|
40
|
-
height = chunkData.readUInt32BE(4);
|
|
41
|
-
bitDepth = chunkData[8] ?? 0;
|
|
42
|
-
colorType = chunkData[9] ?? 0;
|
|
43
|
-
interlace = chunkData[12] ?? 0;
|
|
44
|
-
sawIhdr = true;
|
|
45
|
-
} else if (chunkType === "IDAT") {
|
|
46
|
-
idatChunks.push(Buffer.from(chunkData));
|
|
47
|
-
} else if (chunkType === "IEND") {
|
|
48
|
-
break;
|
|
49
|
-
}
|
|
50
|
-
pos += 12 + chunkLen;
|
|
51
|
-
}
|
|
52
|
-
if (!sawIhdr) {
|
|
53
|
-
throw new Error(`${caller}: PNG missing IHDR chunk`);
|
|
54
|
-
}
|
|
55
|
-
if (colorType !== 2 && colorType !== 6) {
|
|
56
|
-
throw new Error(`${caller}: unsupported color type ${colorType} (expected 2=RGB or 6=RGBA)`);
|
|
57
|
-
}
|
|
58
|
-
if (interlace !== 0) {
|
|
59
|
-
throw new Error(
|
|
60
|
-
`${caller}: Adam7-interlaced PNGs are not supported (interlace method ${interlace})`
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
const channels = colorType === 6 ? 4 : 3;
|
|
64
|
-
const bpp = channels * (bitDepth / 8);
|
|
65
|
-
const stride = width * bpp;
|
|
66
|
-
const compressed = Buffer.concat(idatChunks);
|
|
67
|
-
const decompressed = inflateSync(compressed);
|
|
68
|
-
const rawPixels = Buffer.allocUnsafe(height * stride);
|
|
69
|
-
const prevRow = new Uint8Array(stride);
|
|
70
|
-
const currRow = new Uint8Array(stride);
|
|
71
|
-
let srcPos = 0;
|
|
72
|
-
for (let y = 0; y < height; y++) {
|
|
73
|
-
const filterType = decompressed[srcPos++] ?? 0;
|
|
74
|
-
const rawRow = decompressed.subarray(srcPos, srcPos + stride);
|
|
75
|
-
srcPos += stride;
|
|
76
|
-
switch (filterType) {
|
|
77
|
-
case 0:
|
|
78
|
-
currRow.set(rawRow);
|
|
79
|
-
break;
|
|
80
|
-
case 1:
|
|
81
|
-
for (let x = 0; x < stride; x++) {
|
|
82
|
-
currRow[x] = (rawRow[x] ?? 0) + (x >= bpp ? currRow[x - bpp] ?? 0 : 0) & 255;
|
|
83
|
-
}
|
|
84
|
-
break;
|
|
85
|
-
case 2:
|
|
86
|
-
for (let x = 0; x < stride; x++) {
|
|
87
|
-
currRow[x] = (rawRow[x] ?? 0) + (prevRow[x] ?? 0) & 255;
|
|
88
|
-
}
|
|
89
|
-
break;
|
|
90
|
-
case 3:
|
|
91
|
-
for (let x = 0; x < stride; x++) {
|
|
92
|
-
const left = x >= bpp ? currRow[x - bpp] ?? 0 : 0;
|
|
93
|
-
const up = prevRow[x] ?? 0;
|
|
94
|
-
currRow[x] = (rawRow[x] ?? 0) + Math.floor((left + up) / 2) & 255;
|
|
95
|
-
}
|
|
96
|
-
break;
|
|
97
|
-
case 4:
|
|
98
|
-
for (let x = 0; x < stride; x++) {
|
|
99
|
-
const left = x >= bpp ? currRow[x - bpp] ?? 0 : 0;
|
|
100
|
-
const up = prevRow[x] ?? 0;
|
|
101
|
-
const upLeft = x >= bpp ? prevRow[x - bpp] ?? 0 : 0;
|
|
102
|
-
currRow[x] = (rawRow[x] ?? 0) + paeth(left, up, upLeft) & 255;
|
|
103
|
-
}
|
|
104
|
-
break;
|
|
105
|
-
default:
|
|
106
|
-
throw new Error(`${caller}: unknown filter type ${filterType} at row ${y}`);
|
|
107
|
-
}
|
|
108
|
-
rawPixels.set(currRow, y * stride);
|
|
109
|
-
prevRow.set(currRow);
|
|
110
|
-
}
|
|
111
|
-
return { width, height, bitDepth, colorType, rawPixels };
|
|
112
|
-
}
|
|
113
|
-
function decodePng(buf) {
|
|
114
|
-
const { width, height, bitDepth, colorType, rawPixels } = decodePngRaw(buf, "decodePng");
|
|
115
|
-
if (bitDepth !== 8) {
|
|
116
|
-
throw new Error(`decodePng: unsupported bit depth ${bitDepth} (expected 8)`);
|
|
117
|
-
}
|
|
118
|
-
const output = new Uint8Array(width * height * 4);
|
|
119
|
-
if (colorType === 6) {
|
|
120
|
-
output.set(rawPixels);
|
|
121
|
-
} else {
|
|
122
|
-
for (let i = 0; i < width * height; i++) {
|
|
123
|
-
output[i * 4 + 0] = rawPixels[i * 3 + 0] ?? 0;
|
|
124
|
-
output[i * 4 + 1] = rawPixels[i * 3 + 1] ?? 0;
|
|
125
|
-
output[i * 4 + 2] = rawPixels[i * 3 + 2] ?? 0;
|
|
126
|
-
output[i * 4 + 3] = 255;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return { width, height, data: output };
|
|
130
|
-
}
|
|
131
|
-
function buildSrgbToSignalLut(transfer) {
|
|
132
|
-
const lut = new Uint16Array(256);
|
|
133
|
-
const hlgA = 0.17883277;
|
|
134
|
-
const hlgB = 1 - 4 * hlgA;
|
|
135
|
-
const hlgC = 0.5 - hlgA * Math.log(4 * hlgA);
|
|
136
|
-
const pqM1 = 0.1593017578125;
|
|
137
|
-
const pqM2 = 78.84375;
|
|
138
|
-
const pqC1 = 0.8359375;
|
|
139
|
-
const pqC2 = 18.8515625;
|
|
140
|
-
const pqC3 = 18.6875;
|
|
141
|
-
const pqMaxNits = 1e4;
|
|
142
|
-
const sdrNits = 203;
|
|
143
|
-
for (let i = 0; i < 256; i++) {
|
|
144
|
-
if (transfer === "srgb") {
|
|
145
|
-
lut[i] = i * 257;
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
const v = i / 255;
|
|
149
|
-
const linear = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
150
|
-
let signal;
|
|
151
|
-
if (transfer === "hlg") {
|
|
152
|
-
signal = linear <= 1 / 12 ? Math.sqrt(3 * linear) : hlgA * Math.log(12 * linear - hlgB) + hlgC;
|
|
153
|
-
} else {
|
|
154
|
-
const Lp = Math.max(0, linear * sdrNits / pqMaxNits);
|
|
155
|
-
const Lm1 = Math.pow(Lp, pqM1);
|
|
156
|
-
signal = Math.pow((pqC1 + pqC2 * Lm1) / (1 + pqC3 * Lm1), pqM2);
|
|
157
|
-
}
|
|
158
|
-
lut[i] = Math.min(65535, Math.round(signal * 65535));
|
|
159
|
-
}
|
|
160
|
-
return lut;
|
|
161
|
-
}
|
|
162
|
-
var SRGB_TO_SRGB_16 = buildSrgbToSignalLut("srgb");
|
|
163
|
-
var SRGB_TO_HLG = buildSrgbToSignalLut("hlg");
|
|
164
|
-
var SRGB_TO_PQ = buildSrgbToSignalLut("pq");
|
|
165
|
-
function getSrgbToSignalLut(transfer) {
|
|
166
|
-
if (transfer === "pq") return SRGB_TO_PQ;
|
|
167
|
-
if (transfer === "hlg") return SRGB_TO_HLG;
|
|
168
|
-
return SRGB_TO_SRGB_16;
|
|
169
|
-
}
|
|
170
|
-
function blitRgba8OverRgb48le(domRgba, canvas, width, height, transfer = "hlg") {
|
|
171
|
-
const pixelCount = width * height;
|
|
172
|
-
const lut = getSrgbToSignalLut(transfer);
|
|
173
|
-
for (let i = 0; i < pixelCount; i++) {
|
|
174
|
-
const da = domRgba[i * 4 + 3] ?? 0;
|
|
175
|
-
if (da === 0) {
|
|
176
|
-
continue;
|
|
177
|
-
} else if (da === 255) {
|
|
178
|
-
const r16 = lut[domRgba[i * 4 + 0] ?? 0] ?? 0;
|
|
179
|
-
const g16 = lut[domRgba[i * 4 + 1] ?? 0] ?? 0;
|
|
180
|
-
const b16 = lut[domRgba[i * 4 + 2] ?? 0] ?? 0;
|
|
181
|
-
canvas.writeUInt16LE(r16, i * 6);
|
|
182
|
-
canvas.writeUInt16LE(g16, i * 6 + 2);
|
|
183
|
-
canvas.writeUInt16LE(b16, i * 6 + 4);
|
|
184
|
-
} else {
|
|
185
|
-
const alpha = da / 255;
|
|
186
|
-
const invAlpha = 1 - alpha;
|
|
187
|
-
const hdrR = (canvas[i * 6 + 0] ?? 0) | (canvas[i * 6 + 1] ?? 0) << 8;
|
|
188
|
-
const hdrG = (canvas[i * 6 + 2] ?? 0) | (canvas[i * 6 + 3] ?? 0) << 8;
|
|
189
|
-
const hdrB = (canvas[i * 6 + 4] ?? 0) | (canvas[i * 6 + 5] ?? 0) << 8;
|
|
190
|
-
const domR = lut[domRgba[i * 4 + 0] ?? 0] ?? 0;
|
|
191
|
-
const domG = lut[domRgba[i * 4 + 1] ?? 0] ?? 0;
|
|
192
|
-
const domB = lut[domRgba[i * 4 + 2] ?? 0] ?? 0;
|
|
193
|
-
canvas.writeUInt16LE(Math.round(domR * alpha + hdrR * invAlpha), i * 6);
|
|
194
|
-
canvas.writeUInt16LE(Math.round(domG * alpha + hdrG * invAlpha), i * 6 + 2);
|
|
195
|
-
canvas.writeUInt16LE(Math.round(domB * alpha + hdrB * invAlpha), i * 6 + 4);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ../producer/src/services/pngDecodeBlitWorker.ts
|
|
201
|
-
if (!parentPort) {
|
|
202
|
-
console.warn("[pngDecodeBlitWorker] no parentPort; module loaded on main thread");
|
|
203
|
-
} else {
|
|
204
|
-
parentPort.on("message", (msg) => {
|
|
205
|
-
const { png, pngOffset, pngLength, dest, destOffset, destLength, width, height, transfer } = msg;
|
|
206
|
-
const pngBuf = Buffer.from(png, pngOffset, pngLength);
|
|
207
|
-
const destBuf = Buffer.from(dest, destOffset, destLength);
|
|
208
|
-
try {
|
|
209
|
-
const decodeStart = Date.now();
|
|
210
|
-
const { data: rgba } = decodePng(pngBuf);
|
|
211
|
-
const decodeMs = Date.now() - decodeStart;
|
|
212
|
-
const blitStart = Date.now();
|
|
213
|
-
blitRgba8OverRgb48le(
|
|
214
|
-
rgba,
|
|
215
|
-
destBuf,
|
|
216
|
-
width,
|
|
217
|
-
height,
|
|
218
|
-
transfer
|
|
219
|
-
);
|
|
220
|
-
const blitMs = Date.now() - blitStart;
|
|
221
|
-
const reply = {
|
|
222
|
-
ok: true,
|
|
223
|
-
png,
|
|
224
|
-
dest,
|
|
225
|
-
decodeMs,
|
|
226
|
-
blitMs
|
|
227
|
-
};
|
|
228
|
-
parentPort.postMessage(reply, [png, dest]);
|
|
229
|
-
} catch (err) {
|
|
230
|
-
const reply = {
|
|
231
|
-
ok: false,
|
|
232
|
-
error: err instanceof Error ? err.message : String(err),
|
|
233
|
-
png,
|
|
234
|
-
dest
|
|
235
|
-
};
|
|
236
|
-
parentPort.postMessage(reply, [png, dest]);
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
}
|