The procedural-glyph-engine dep pointed at a non-portable file:/private/tmp/orby-pkg
path, breaking npm ci in Docker and every main deploy since v0.20.0 (the build
cache masked it until it busted ~Aug 5). Vendor Orby v5.0.0 into web/vendor/,
switch the dep to file:../vendor, and use npm install in the web Dockerfile
(file: deps need install, not ci). Cherry-picked from 3cd4cf9.
83 lines
2.2 KiB
JavaScript
83 lines
2.2 KiB
JavaScript
const DEFAULT_PREVIEW_PROGRESS = 0.48;
|
|
|
|
function clamp01(value) {
|
|
return Math.min(1, Math.max(0, Number(value) || 0));
|
|
}
|
|
|
|
function representativeTime(recipe) {
|
|
const phase = clamp01(recipe?.reducedMotion?.phase ?? 0.25);
|
|
const cycleMs =
|
|
Number(recipe?.timeline?.macroCycleMs) ||
|
|
Number(recipe?.timeline?.durationMs) ||
|
|
4000;
|
|
return (Math.max(1, cycleMs) / 1000) * phase;
|
|
}
|
|
|
|
/**
|
|
* One-shot recipes may advance to a different state while their card is active.
|
|
* Atlas previews retain the requested recipe so their identity and resting frame
|
|
* always match the card label.
|
|
*/
|
|
export function createThumbnailRecipe(recipe) {
|
|
if (!recipe?.timeline?.next) return recipe;
|
|
return {
|
|
...recipe,
|
|
timeline: {
|
|
...recipe.timeline,
|
|
next: null,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function shouldAnimateThumbnail(
|
|
id,
|
|
{
|
|
selectedId = null,
|
|
mainRunning = false,
|
|
hoveredIds = new Set(),
|
|
focusedIds = new Set(),
|
|
} = {},
|
|
) {
|
|
return (
|
|
(id === selectedId && mainRunning) ||
|
|
hoveredIds.has(id) ||
|
|
focusedIds.has(id)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Render a deterministic, authored representative phase without leaving the
|
|
* preview in reduced-motion mode. JoanGlyphEngine maps time zero to each
|
|
* recipe's reducedMotion.phase while reduced motion is enabled.
|
|
*/
|
|
export function renderThumbnailRestFrame(
|
|
preview,
|
|
{ progress = DEFAULT_PREVIEW_PROGRESS } = {},
|
|
) {
|
|
if (!preview || preview.destroyed) return null;
|
|
|
|
const nextProgress = clamp01(progress);
|
|
if (Math.abs(preview.progress - nextProgress) > 1e-6) {
|
|
preview.setProgress(nextProgress);
|
|
}
|
|
|
|
// Neighbor-propagation previews otherwise retain their previous gate buffer,
|
|
// making the supposedly static pose depend on how long the card was hovered.
|
|
preview.stateElapsed = representativeTime(preview.currentRecipe);
|
|
preview.gates?.fill(0);
|
|
preview.neighborBuffer?.fill(0);
|
|
preview.dwell?.fill(0);
|
|
|
|
const previousReducedMotion = preview.reducedMotion;
|
|
preview.reducedMotion = true;
|
|
let stats;
|
|
try {
|
|
stats = preview.renderOnce(0);
|
|
} finally {
|
|
preview.reducedMotion = previousReducedMotion;
|
|
}
|
|
|
|
if (preview.canvas?.dataset) preview.canvas.dataset.previewState = "ready";
|
|
return stats;
|
|
}
|