import React, {useEffect, useLayoutEffect, useRef, useState} from 'react'; import {AbsoluteFill, Composition, OffthreadVideo, Sequence, continueRender, delayRender, getInputProps, registerRoot, staticFile, useCurrentFrame} from 'remotion'; import {blenderLabels} from './scene-plan.mjs'; const clamp = (n) => Math.max(0, Math.min(1, n)); const ease = (n) => n * n * (3 - 2 * n); const ochre = (p, i) => p[i] > p[i + 1] + 8 && p[i + 1] > 150 && p[i + 1] > p[i + 2] + 12; const loaded = new Map(); function loadImage(name) { if (!loaded.has(name)) loaded.set(name, new Promise((resolve, reject) => { const im = new Image(); im.onload = () => resolve(im); im.onerror = reject; im.src = staticFile(name); })); return loaded.get(name); } function Motion({clip}) { const frame = useCurrentFrame(); const ref = useRef(null); const [pair, setPair] = useState(null); const [handle] = useState(() => delayRender('load actual start/end PNG')); useEffect(() => { Promise.all([loadImage(clip.a), loadImage(clip.b)]).then(([a, b]) => { const c = document.createElement('canvas'); c.width = 945; c.height = 1680; const ctx = c.getContext('2d', {willReadFrequently: true}); ctx.drawImage(a, 0, 0); const pa = ctx.getImageData(0, 0, 945, 1680); ctx.clearRect(0, 0, 945, 1680); ctx.drawImage(b, 0, 0); const pb = ctx.getImageData(0, 0, 945, 1680); const pixels = []; for (let i = 0; i < pa.data.length; i += 4) { if (ochre(pa.data, i) || ochre(pb.data, i)) pixels.push(i); } setPair({a, b, pa, pb, pixels}); continueRender(handle); }).catch((e) => {throw e;}); }, [clip.a, clip.b, handle]); useLayoutEffect(() => { if (!pair || !ref.current) return; const ctx = ref.current.getContext('2d'); const p = ease(clamp((frame * 20 - (clip.plan.delayMs ?? 0)) / (clip.frames * 20 - (clip.plan.delayMs ?? 0)) * 1.35)); ctx.clearRect(0, 0, 945, 1680); if (p >= 1 || frame === clip.frames - 1) {ctx.drawImage(pair.b, 0, 0); return;} ctx.drawImage(pair.a, 0, 0); if (!p || clip.plan.kind === 'hold') return; let annotationPixels; if (clip.plan.kind === 'annotation' || clip.plan.kind === 'deform') { // Only ochre explanatory pixels change. All source atoms/internal bonds remain intact. const out = new ImageData(new Uint8ClampedArray(pair.pa.data), 945, 1680); for (const i of pair.pixels) { const coordinate = clip.plan.direction === 'x' ? (i / 4 % 945) / 945 : clip.plan.direction === 'y' ? Math.floor(i / 4 / 945) / 1680 : 0; const t = clamp(p * (clip.plan.direction ? 1.4 : 1) - coordinate * 0.4); for (let k = 0; k < 3; k++) out.data[i + k] = pair.pa.data[i + k] * (1 - t) + pair.pb.data[i + k] * t; } ctx.putImageData(out, 0, 0); annotationPixels = out.data; } if (clip.plan.kind === 'deform') { const [a, b] = clip.boxes; const box = a.map((v, i) => v + (b[i] - v) * p); ctx.fillStyle = `rgb(${clip.background.join(',')})`; // Clear all of the original contour, including non-elliptical antialiasing. ctx.fillRect(a[0] - 3, a[1] - 3, a[2] + 6, a[3] + 6); ctx.save(); ctx.beginPath(); ctx.ellipse(box[0] + box[2] / 2, box[1] + box[3] / 2, box[2] / 2, box[3] / 2, 0, 0, Math.PI * 2); ctx.clip(); ctx.drawImage(pair.a, ...a, ...box); ctx.globalAlpha = p; ctx.drawImage(pair.b, ...b, ...box); ctx.restore(); // Preserve external energy/arrow pixels if their callout edge overlaps the body bounding box. for (const i of pair.pixels) { ctx.fillStyle = `rgb(${annotationPixels[i]},${annotationPixels[i + 1]},${annotationPixels[i + 2]})`; ctx.fillRect(i / 4 % 945, Math.floor(i / 4 / 945), 1, 1); } } if (clip.plan.regions?.length) { ctx.save(); ctx.beginPath(); for (const r of clip.plan.regions) ctx.rect(...r); ctx.clip(); ctx.globalAlpha = p; ctx.drawImage(pair.b, 0, 0); ctx.restore(); } }, [frame, pair, clip]); return ; } function TextLayer({data}) { const frame = useCurrentFrame(), ms = frame * 20; const caption = data.captions.find((c) => ms >= c.start_ms && ms < c.end_ms); const clip = data.clips.find((c) => ms >= c.start_ms && ms < c.end_ms); const blender = data.visual_engine === 'Blender'; const labels = blender ? blenderLabels(clip, frame) : data.labels.filter((l) => ms >= l.start && ms < l.end); return {ms < 7920 &&
{data.title}
} {blender && {labels.filter((l) => l.leader).map((l, i) => )} } {labels.map((l, i) =>
{l.text}
)} {caption &&
{caption.text}
}
; } function Video({data, composed = false}) { return {data.clips.map((clip) => {composed ? : } )} {composed && } ; } function Root() { const {data} = getInputProps(); return <> ; } registerRoot(Root);