Export: fall back to VP9/AV1 when H.264 unavailable and warn about software encoding
Firefox on Linux exposes VideoEncoder but reports every avc1.* config as
unsupported (OpenH264 is decode-only), so pickVideoConfig returned null and
export threw "no supported H.264 configuration found". Probe VP9/AV1 families
after H.264 and pick the first profile that isConfigSupported and does not
reorder (B-frame) — same 12-frame ordering test. Map the chosen family to
mp4-muxer's video codec (avc/vp9/av1).
Before any rendering, probe the codec for the chosen preset and show a
modal when we fell off H.264 onto a software VP9/AV1 path — that's what the
user felt as "super slow". The modal names the codec, explains render vs
encode cost, suggests Chromium/Chrome for hardware H.264 (or 720p/lower
preset), and offers Continue anyway / Cancel (Esc/backdrop also cancel) plus
"Don't warn again this session". Injected pick is threaded through
Exporter.export({ videoPick }) and exportSegment so we don't probe twice.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b2c9497bae
commit
e5484c9c38
@ -21,6 +21,18 @@
|
||||
</div>
|
||||
<div id="hud" hidden></div>
|
||||
<div id="toast" hidden></div>
|
||||
<div id="export-warn" hidden>
|
||||
<div class="ew-backdrop"></div>
|
||||
<div class="ew-box" role="dialog" aria-modal="true" aria-labelledby="ew-title">
|
||||
<div id="ew-title" class="ew-title">Software encoding — export will be slow</div>
|
||||
<div id="ew-body" class="ew-body"></div>
|
||||
<div class="ew-actions">
|
||||
<button id="ew-cancel">Cancel</button>
|
||||
<button id="ew-continue" class="primary">Continue anyway</button>
|
||||
</div>
|
||||
<label class="ew-opt"><input type="checkbox" id="ew-dontask"> Don't warn again this session</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="transport">
|
||||
|
||||
64
flow-state/package-lock.json
generated
64
flow-state/package-lock.json
generated
@ -13,6 +13,7 @@
|
||||
"three": "^0.181.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.62.1",
|
||||
"vite": "^7.2.2"
|
||||
}
|
||||
},
|
||||
@ -475,6 +476,22 @@
|
||||
"node": "^22.20 || ^24.12 || >=25"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz",
|
||||
"integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.62.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz",
|
||||
@ -969,6 +986,53 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
|
||||
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.62.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
||||
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.25",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
},
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.62.1",
|
||||
"vite": "^7.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@ -85,43 +85,88 @@ async function emitsInPresentationOrder(config) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe for a codec configuration the browser will actually accept — and will
|
||||
* encode in presentation order.
|
||||
*
|
||||
* The candidates run high profile first for compression efficiency, down to
|
||||
* baseline last. Baseline forbids B-slices outright, so it is the profile that
|
||||
* cannot reorder; the earlier entries are tried first because when a browser
|
||||
* does not reorder there is no reason to give up their quality.
|
||||
* Families in preference order — H.264 first where available (most compatible),
|
||||
* then VP9, then AV1. Each family maps to one mp4-muxer codec ("avc"/"vp9"/"av1").
|
||||
* Inside a family we walk high→low profiles; the first that does not reorder wins.
|
||||
*/
|
||||
async function pickVideoConfig(width, height, bitrate, fps) {
|
||||
const candidates = [
|
||||
'avc1.640034', 'avc1.640033', 'avc1.4d0034', 'avc1.42003e',
|
||||
const VIDEO_FAMILIES = [
|
||||
{
|
||||
muxerCodec: 'avc',
|
||||
candidates: [
|
||||
{ codec: 'avc1.640034', avc: { format: 'avc' } },
|
||||
{ codec: 'avc1.640033', avc: { format: 'avc' } },
|
||||
{ codec: 'avc1.4d0034', avc: { format: 'avc' } },
|
||||
{ codec: 'avc1.42003e', avc: { format: 'avc' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
muxerCodec: 'vp9',
|
||||
candidates: [
|
||||
{ codec: 'vp09.00.10.08' },
|
||||
{ codec: 'vp8' },
|
||||
],
|
||||
},
|
||||
{
|
||||
muxerCodec: 'av1',
|
||||
candidates: [
|
||||
{ codec: 'av01.0.04M.08' },
|
||||
{ codec: 'av01.0.05M.08' },
|
||||
],
|
||||
},
|
||||
];
|
||||
const supported = [];
|
||||
for (const codec of candidates) {
|
||||
const config = {
|
||||
codec, width, height, bitrate, framerate: fps,
|
||||
avc: { format: 'avc' },
|
||||
};
|
||||
|
||||
async function pickVideoConfig(width, height, bitrate, fps) {
|
||||
const attempted = [];
|
||||
const reorderFailures = [];
|
||||
for (const family of VIDEO_FAMILIES) {
|
||||
for (const cand of family.candidates) {
|
||||
const config = { codec: cand.codec, width, height, bitrate, framerate: fps };
|
||||
if (cand.avc) config.avc = cand.avc;
|
||||
let supported = false;
|
||||
let negotiatedHardware = '';
|
||||
try {
|
||||
const support = await VideoEncoder.isConfigSupported(config);
|
||||
if (!support.supported) continue;
|
||||
} catch { continue; }
|
||||
supported.push(config);
|
||||
if (await emitsInPresentationOrder(config)) return config;
|
||||
const sup = await VideoEncoder.isConfigSupported(config);
|
||||
supported = !!sup.supported;
|
||||
negotiatedHardware = sup.config ? sup.config.hardwareAcceleration || '' : '';
|
||||
} catch { supported = false; }
|
||||
attempted.push(`${family.muxerCodec}:${cand.codec}=${supported ? 'ok' : 'no'}`);
|
||||
if (!supported) continue;
|
||||
// Found a supported profile — does it reorder? Baseline AVC never does;
|
||||
// VP9/AV1 may, so we enforce the same test.
|
||||
if (await emitsInPresentationOrder(config)) {
|
||||
return { config, muxerCodec: family.muxerCodec, hardwareAcceleration: negotiatedHardware };
|
||||
}
|
||||
// Every supported profile reorders. Refuse rather than write a file that
|
||||
// silently loses most of its frames — see emitsInPresentationOrder.
|
||||
if (supported.length) {
|
||||
reorderFailures.push(`${cand.codec}`);
|
||||
}
|
||||
}
|
||||
if (reorderFailures.length) {
|
||||
throw new Error(
|
||||
'every supported H.264 profile emits frames out of order on this browser ' +
|
||||
`(tried ${supported.map((c) => c.codec).join(', ')}), which this exporter ` +
|
||||
'every supported codec profile emits frames out of order on this browser ' +
|
||||
`(tried ${reorderFailures.join(', ')} — all reordered), which this exporter ` +
|
||||
'cannot mux correctly',
|
||||
);
|
||||
}
|
||||
// Nothing supported at all — surface what we tried so the caller can explain.
|
||||
console.warn('[export] no supported video config — tried', attempted.join(' '));
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* What codec would an export at this size actually use, without rendering.
|
||||
* Used for the "software encoding will be slow" warning — the choice depends on
|
||||
* the browser + resolution, so it is not a constant.
|
||||
*/
|
||||
export async function probeExportCodec(width, height, bitrate, fps = 60) {
|
||||
const picked = await pickVideoConfig(width, height, bitrate, fps);
|
||||
if (!picked) return null;
|
||||
return {
|
||||
codec: picked.config.codec,
|
||||
muxerCodec: picked.muxerCodec,
|
||||
hardwareAcceleration: picked.hardwareAcceleration || '',
|
||||
isSoftwareFallback: picked.muxerCodec !== 'avc',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick an audio codec the browser can actually encode.
|
||||
*
|
||||
@ -222,7 +267,7 @@ export class Exporter {
|
||||
* @param {(progress: {frame, total, fraction, stage}) => void} [options.onProgress]
|
||||
* @returns {Promise<Blob>}
|
||||
*/
|
||||
async export({ preset = '1080p', frameRange = null, onProgress = null } = {}) {
|
||||
async export({ preset = '1080p', frameRange = null, onProgress = null, videoPick = null } = {}) {
|
||||
if (!isSupported()) {
|
||||
throw new Error('WebCodecs VideoEncoder is unavailable in this browser');
|
||||
}
|
||||
@ -235,8 +280,16 @@ export class Exporter {
|
||||
const [startFrame, endFrame] = frameRange || [0, show.frameCount];
|
||||
const total = Math.max(1, endFrame - startFrame);
|
||||
|
||||
const videoConfig = await pickVideoConfig(width, height, bitrate, fps);
|
||||
if (!videoConfig) throw new Error('no supported H.264 configuration found');
|
||||
const picked = videoPick || await pickVideoConfig(width, height, bitrate, fps);
|
||||
if (!picked) {
|
||||
throw new Error(
|
||||
'no supported video encoder found — this Firefox build reports H.264 (avc1.*) as unsupported; ' +
|
||||
'VP9/AV1 were also unavailable. Try Chromium/Chrome, or paste the console output of ' +
|
||||
'VideoEncoder.isConfigSupported probes so a codec string can be added'
|
||||
);
|
||||
}
|
||||
const videoConfig = picked.config;
|
||||
const muxerVideoCodec = picked.muxerCodec;
|
||||
|
||||
const channels = show.audioBuffer ? Math.min(2, show.audioBuffer.numberOfChannels) : 0;
|
||||
const audioConfig = show.audioBuffer && typeof AudioEncoder !== 'undefined'
|
||||
@ -258,7 +311,7 @@ export class Exporter {
|
||||
|
||||
const muxer = new Muxer({
|
||||
target: new ArrayBufferTarget(),
|
||||
video: { codec: 'avc', width, height, frameRate: fps },
|
||||
video: { codec: muxerVideoCodec, width, height, frameRate: fps },
|
||||
...(hasAudio ? {
|
||||
audio: {
|
||||
codec: audioConfig.muxerCodec,
|
||||
@ -454,12 +507,12 @@ export class Exporter {
|
||||
|
||||
/** Render a short range around a frame — the "test render" bridge before a full export. */
|
||||
export async function exportSegment(show, centreFrame,
|
||||
{ seconds = 20, preset = '1080p', onProgress, exporter = null } = {}) {
|
||||
{ seconds = 20, preset = '1080p', onProgress, exporter = null, videoPick = null } = {}) {
|
||||
const half = Math.round((seconds * show.fps) / 2);
|
||||
const start = Math.max(0, centreFrame - half);
|
||||
const end = Math.min(show.frameCount, centreFrame + half);
|
||||
// Accept a caller-supplied Exporter so the caller can read `warnings` and cancel.
|
||||
return (exporter || new Exporter(show)).export({ preset, frameRange: [start, end], onProgress });
|
||||
return (exporter || new Exporter(show)).export({ preset, frameRange: [start, end], onProgress, videoPick });
|
||||
}
|
||||
|
||||
export function downloadBlob(blob, filename) {
|
||||
|
||||
@ -6,7 +6,7 @@ import { describeLook } from './look/LookGenerator.js';
|
||||
import { toHex } from './look/palette.js';
|
||||
import { applyGrainToPost, describeGrain, GRAIN_MASKS, GRAIN_MODES } from './look/grain.js';
|
||||
import { renderClickTrack, audioBufferToWavBlob } from './audio/metronome.js';
|
||||
import { Exporter, exportSegment, downloadBlob, PRESETS, isSupported } from './export/Exporter.js';
|
||||
import { Exporter, exportSegment, downloadBlob, PRESETS, isSupported, probeExportCodec } from './export/Exporter.js';
|
||||
import { subjectOf, groundOf, overlaysOf, stackCoverage } from './look/stack.js';
|
||||
import { coverageOf as sceneCoverage } from './scenes/surface.js';
|
||||
|
||||
@ -37,6 +37,11 @@ const dom = {
|
||||
thStep: document.getElementById('th-step'),
|
||||
thFill: document.getElementById('th-fill'),
|
||||
changeTrack: document.getElementById('btn-change-track'),
|
||||
exportWarn: document.getElementById('export-warn'),
|
||||
exportWarnBody: document.getElementById('ew-body'),
|
||||
exportWarnContinue: document.getElementById('ew-continue'),
|
||||
exportWarnCancel: document.getElementById('ew-cancel'),
|
||||
exportWarnDontAsk: document.getElementById('ew-dontask'),
|
||||
};
|
||||
|
||||
const state = {
|
||||
@ -549,6 +554,56 @@ function grainRows(grain) {
|
||||
|
||||
// ---------------------------------------------------------------- export
|
||||
|
||||
const exportWarnState = { dismissedForSession: false };
|
||||
|
||||
function maybeWarnSoftwareEncoder(pick, preset) {
|
||||
// Only warn when we fell off H.264 onto a software VP9/AV1 path.
|
||||
// H.264 is taken as hardware (or at least the fast path) on the machines
|
||||
// we care about; a VP9 software encode is what the user just felt as "super slow".
|
||||
if (!pick || !pick.isSoftwareFallback || exportWarnState.dismissedForSession) return Promise.resolve(true);
|
||||
const isFirefox = /Firefox\//.test(navigator.userAgent);
|
||||
const codecLabel = pick.muxerCodec === 'vp9' ? 'VP9' : pick.muxerCodec === 'av1' ? 'AV1' : pick.codec;
|
||||
const lines = [
|
||||
`This browser will encode as ${codecLabel} (${pick.codec}) — H.264 (hardware) is not available here, so encoding is in software and will be much slower.`,
|
||||
'',
|
||||
`Preset ${preset}: the render itself is the same cost everywhere; the difference is how fast frames are encoded afterwards.`,
|
||||
isFirefox
|
||||
? 'Fastest fix: open this page in Chromium or Chrome (H.264 hardware) and export there. On Firefox, VP9 is the best available.'
|
||||
: 'Try Chromium/Chrome for hardware H.264, or lower the preset (720p) for a faster encode.',
|
||||
'You can also enable hardware encoding if your browser/OS offers it and then reload.',
|
||||
];
|
||||
return showExportWarning(lines.join('\n'));
|
||||
}
|
||||
|
||||
function showExportWarning(message) {
|
||||
const warn = dom.exportWarn;
|
||||
const body = dom.exportWarnBody;
|
||||
if (!warn || !body) return Promise.resolve(true);
|
||||
return new Promise((resolve) => {
|
||||
body.textContent = message;
|
||||
warn.hidden = false;
|
||||
const cleanup = (proceed) => {
|
||||
warn.hidden = true;
|
||||
if (dom.exportWarnDontAsk && dom.exportWarnDontAsk.checked) {
|
||||
exportWarnState.dismissedForSession = true;
|
||||
}
|
||||
dom.exportWarnContinue.removeEventListener('click', onContinue);
|
||||
dom.exportWarnCancel.removeEventListener('click', onCancel);
|
||||
warn.removeEventListener('click', onBackdrop);
|
||||
document.removeEventListener('keydown', onKey);
|
||||
resolve(proceed);
|
||||
};
|
||||
const onContinue = () => cleanup(true);
|
||||
const onCancel = () => cleanup(false);
|
||||
const onBackdrop = (e) => { if (e.target.classList.contains('ew-backdrop')) cleanup(false); };
|
||||
const onKey = (e) => { if (e.key === 'Escape') cleanup(false); };
|
||||
dom.exportWarnContinue.addEventListener('click', onContinue);
|
||||
dom.exportWarnCancel.addEventListener('click', onCancel);
|
||||
warn.addEventListener('click', onBackdrop);
|
||||
document.addEventListener('keydown', onKey);
|
||||
});
|
||||
}
|
||||
|
||||
function currentPreset() {
|
||||
const el = document.getElementById('sel-preset');
|
||||
return el ? el.value : '1080p';
|
||||
@ -590,11 +645,21 @@ async function runExport(segment) {
|
||||
if (dom.toast) dom.toast.hidden = true;
|
||||
try {
|
||||
const preset = currentPreset();
|
||||
const { width, height, bitrate } = PRESETS[preset] || PRESETS['1080p'];
|
||||
// Probe codec before any rendering so the warning is cheap and can be
|
||||
// dismissed without paying for a warm-up.
|
||||
const pick = await probeExportCodec(width, height, bitrate, state.show.fps);
|
||||
const proceed = await maybeWarnSoftwareEncoder(pick, preset);
|
||||
if (!proceed) {
|
||||
const status = document.getElementById('export-status');
|
||||
if (status) status.textContent = 'export cancelled — software encoding warning dismissed';
|
||||
return;
|
||||
}
|
||||
const exporter = new Exporter(state.show);
|
||||
const blob = segment
|
||||
? await exportSegment(state.show, state.show.timeline.frame,
|
||||
{ seconds: 20, preset, onProgress: exportProgress, exporter })
|
||||
: await exporter.export({ preset, onProgress: exportProgress });
|
||||
: await exporter.export({ preset, onProgress: exportProgress, videoPick: pick });
|
||||
|
||||
const suffix = segment ? `-segment-${state.show.timeline.frame}` : '';
|
||||
downloadBlob(blob, `${state.show.fileName || 'flow-state'}${suffix}-${preset}.mp4`);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"fingerprint": "ab24b461",
|
||||
"fingerprint": "6a922435",
|
||||
"schema": 5,
|
||||
"measured": "2026-08-20",
|
||||
"contexts": [
|
||||
@ -8306,6 +8306,251 @@
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"Pylon Field 3D": {
|
||||
"coverage": 0.014,
|
||||
"bedCoverage": 0,
|
||||
"bedCoverageMean": 0.006,
|
||||
"variety": 0.132,
|
||||
"blocks": {
|
||||
"scale": 0.009,
|
||||
"orient": 0.154,
|
||||
"layout": 0.17,
|
||||
"region": 0.312,
|
||||
"texture": 0.055,
|
||||
"motion": 0.095,
|
||||
"colour": 0.531
|
||||
},
|
||||
"readsHistory": false,
|
||||
"profile": {
|
||||
"scale": [
|
||||
0.2393,
|
||||
0.1761,
|
||||
0.1808,
|
||||
0.1881,
|
||||
0.2157
|
||||
],
|
||||
"orient": [
|
||||
0.0911,
|
||||
0.0988,
|
||||
0.0465,
|
||||
0.0758,
|
||||
0.0445,
|
||||
0.0532
|
||||
],
|
||||
"layout": [
|
||||
0.0147,
|
||||
0.0183,
|
||||
0.0129,
|
||||
0.0039,
|
||||
0.074,
|
||||
0.195,
|
||||
0.1856,
|
||||
0.0717,
|
||||
0.0741,
|
||||
0.1143,
|
||||
0.111,
|
||||
0.0649,
|
||||
0.0108,
|
||||
0.0223,
|
||||
0.0224,
|
||||
0.0042
|
||||
],
|
||||
"region": [
|
||||
-0.6079,
|
||||
-0.011,
|
||||
0.837,
|
||||
-0.1399,
|
||||
-0.2597,
|
||||
-0.3173,
|
||||
-0.6353,
|
||||
-0.1502,
|
||||
0.1133,
|
||||
0.5919,
|
||||
0.0501,
|
||||
0.3099,
|
||||
1.5848,
|
||||
0.1487,
|
||||
1.2557,
|
||||
0.5846,
|
||||
0.1699,
|
||||
0.2694,
|
||||
-0.5945,
|
||||
-0.0476,
|
||||
-0.8741,
|
||||
-0.2272,
|
||||
-0.0582,
|
||||
-0.6888,
|
||||
-0.5566,
|
||||
0.158,
|
||||
-0.9051
|
||||
],
|
||||
"texture": [
|
||||
0.4569,
|
||||
0.0467,
|
||||
0.1254,
|
||||
0.0372,
|
||||
0.1422,
|
||||
0.1648,
|
||||
0.0537
|
||||
],
|
||||
"colour": [
|
||||
0.2171,
|
||||
0.1161,
|
||||
0.4889,
|
||||
0.1754,
|
||||
0.0009,
|
||||
0.0016,
|
||||
0.5458,
|
||||
0.0331
|
||||
],
|
||||
"motion": [
|
||||
0.3918,
|
||||
0.2137,
|
||||
0.1319,
|
||||
0.1273,
|
||||
0.1352,
|
||||
0.0081,
|
||||
0.0268,
|
||||
0.0143,
|
||||
0.001,
|
||||
0.0521,
|
||||
0.1993,
|
||||
0.1588,
|
||||
0.0503,
|
||||
0.0876,
|
||||
0.1811,
|
||||
0.1411,
|
||||
0.0471,
|
||||
0.0062,
|
||||
0.0097,
|
||||
0.016,
|
||||
0.0006
|
||||
]
|
||||
}
|
||||
},
|
||||
"Synthwave Corridor": {
|
||||
"coverage": 0.012,
|
||||
"bedCoverage": 0,
|
||||
"bedCoverageMean": 0.01,
|
||||
"variety": 0.12,
|
||||
"blocks": {
|
||||
"scale": 0.015,
|
||||
"orient": 0.084,
|
||||
"layout": 0.16,
|
||||
"region": 0.288,
|
||||
"texture": 0.064,
|
||||
"motion": 0.11,
|
||||
"colour": 0.541
|
||||
},
|
||||
"readsHistory": false,
|
||||
"profile": {
|
||||
"scale": [
|
||||
0.205,
|
||||
0.1834,
|
||||
0.2427,
|
||||
0.2296,
|
||||
0.1392
|
||||
],
|
||||
"orient": [
|
||||
0.1676,
|
||||
0.1722,
|
||||
0.077,
|
||||
0.2031,
|
||||
0.0636,
|
||||
0.1073
|
||||
],
|
||||
"layout": [
|
||||
0,
|
||||
0.0111,
|
||||
0.005,
|
||||
0,
|
||||
0.0272,
|
||||
0.2547,
|
||||
0.2295,
|
||||
0.0002,
|
||||
0.0262,
|
||||
0.2076,
|
||||
0.1523,
|
||||
0,
|
||||
0,
|
||||
0.046,
|
||||
0.0401,
|
||||
0
|
||||
],
|
||||
"region": [
|
||||
-0.9498,
|
||||
-0.0083,
|
||||
-0.9038,
|
||||
0.3037,
|
||||
-0.0735,
|
||||
-0.119,
|
||||
-1,
|
||||
0.0332,
|
||||
-1,
|
||||
-0.2529,
|
||||
-0.4383,
|
||||
1.1061,
|
||||
4.4222,
|
||||
0.0955,
|
||||
3.8589,
|
||||
-0.9753,
|
||||
0.0933,
|
||||
-0.6853,
|
||||
-1,
|
||||
0.0332,
|
||||
-1,
|
||||
0.452,
|
||||
0.2317,
|
||||
-0.2569,
|
||||
-1,
|
||||
0.0332,
|
||||
-1
|
||||
],
|
||||
"texture": [
|
||||
0.1016,
|
||||
0.0213,
|
||||
0.0519,
|
||||
0.0285,
|
||||
0.2572,
|
||||
0.2294,
|
||||
0.0577
|
||||
],
|
||||
"colour": [
|
||||
0.1347,
|
||||
0.164,
|
||||
0.4911,
|
||||
0.1878,
|
||||
0.0218,
|
||||
0.0006,
|
||||
0.0566,
|
||||
0.0056
|
||||
],
|
||||
"motion": [
|
||||
0.328,
|
||||
0.2303,
|
||||
0.1743,
|
||||
0.1662,
|
||||
0.1012,
|
||||
0.0001,
|
||||
0.0171,
|
||||
0.0258,
|
||||
0,
|
||||
0.0084,
|
||||
0.2525,
|
||||
0.2127,
|
||||
0,
|
||||
0.0313,
|
||||
0.3059,
|
||||
0.1374,
|
||||
0,
|
||||
0,
|
||||
0.0024,
|
||||
0.0062,
|
||||
0
|
||||
]
|
||||
},
|
||||
"error": "renders nothing — luminance 0.0012, variance 0.0057. A failed shader compile scores 0.000 on every block; check the console for GLSL errors."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,6 +108,17 @@ body {
|
||||
color: var(--dim); font-size: 14px;
|
||||
}
|
||||
|
||||
#export-warn[hidden] { display: none !important; }
|
||||
#export-warn { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 40; }
|
||||
#export-warn .ew-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.62); backdrop-filter: blur(2px); }
|
||||
#export-warn .ew-box { position: relative; max-width: 560px; margin: 16px; background: var(--panel); border: 1px solid var(--line); border-left: 3px solid #f59e0b; padding: 16px 18px 12px; box-shadow: 0 20px 40px rgba(0,0,0,0.55); }
|
||||
#export-warn .ew-title { font-weight: 700; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; margin-bottom: 8px; }
|
||||
#export-warn .ew-body { font-size: 12px; line-height: 1.65; color: var(--text); white-space: pre-wrap; }
|
||||
#export-warn .ew-body code { background: #171a22; border: 1px solid var(--line); padding: 1px 4px; font-size: 11px; }
|
||||
#export-warn .ew-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 14px; }
|
||||
#export-warn .ew-opt { display: flex; align-items: center; gap: 6px; margin-top: 10px; font-size: 11px; color: var(--dim); cursor: pointer; }
|
||||
#export-warn .ew-opt input { accent-color: var(--accent); }
|
||||
|
||||
#transport { grid-area: transport; background: var(--panel); }
|
||||
#timeline { height: 46px; }
|
||||
#timeline-canvas { width: 100%; height: 46px; display: block; cursor: pointer; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user