// cycle-glass-node-morph — 单主体缩入机制图,循环标签随后被上升的玻璃节点接管。 // 运动顺序:上下文主体 → 对角擦除 → 循环箭头与标签 → 连续推近 → 节点托起 → 诊断标记。 import React from 'react'; import { Easing, interpolate, useCurrentFrame } from 'remotion'; import { DesignStage, E, lerp } from '../../_fixtures/Motion'; export const CYCLE_GLASS_NODE_MORPH_DURATION = 257; const CLAMP = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const; const frameSeg = (frame: number, start: number, end: number, ease: (t: number) => number = E.linear) => ease(Math.min(1, Math.max(0, (frame - start) / Math.max(1, end - start)))); // Monotone cubic Hermite interpolation keeps the recorded camera key states while // sharing one non-zero velocity at each interior keyframe. This avoids the visible // stop/restart that three separately eased interpolate() calls would introduce. const smoothCamera = (frame: number, values: readonly number[]) => { const frames = [176, 180, 190, 198] as const; if (frame <= frames[0]) return values[0]; if (frame >= frames[frames.length - 1]) return values[values.length - 1]; const widths = frames.slice(1).map((value, index) => value - frames[index]); const secants = widths.map((width, index) => (values[index + 1] - values[index]) / width); const tangents = [secants[0]]; for (let index = 1; index < values.length - 1; index++) { const before = secants[index - 1]; const after = secants[index]; if (before * after <= 0) { tangents.push(0); continue; } const beforeWidth = widths[index - 1]; const afterWidth = widths[index]; const w1 = 2 * afterWidth + beforeWidth; const w2 = afterWidth + 2 * beforeWidth; tangents.push((w1 + w2) / (w1 / before + w2 / after)); } tangents.push(secants[secants.length - 1]); const segment = frame <= frames[1] ? 0 : frame <= frames[2] ? 1 : 2; const width = widths[segment]; const t = (frame - frames[segment]) / width; const t2 = t * t; const t3 = t2 * t; const h00 = 2 * t3 - 3 * t2 + 1; const h10 = t3 - 2 * t2 + t; const h01 = -2 * t3 + 3 * t2; const h11 = t3 - t2; return ( h00 * values[segment] + h10 * width * tangents[segment] + h01 * values[segment + 1] + h11 * width * tangents[segment + 1] ); }; const quadraticPoint = ( from: readonly [number, number], control: readonly [number, number], to: readonly [number, number], t: number, ) => { const oneMinus = 1 - t; const x = oneMinus * oneMinus * from[0] + 2 * oneMinus * t * control[0] + t * t * to[0]; const y = oneMinus * oneMinus * from[1] + 2 * oneMinus * t * control[1] + t * t * to[1]; const dx = 2 * (oneMinus * (control[0] - from[0]) + t * (to[0] - control[0])); const dy = 2 * (oneMinus * (control[1] - from[1]) + t * (to[1] - control[1])); return { x, y, angle: (Math.atan2(dy, dx) * 180) / Math.PI }; }; const cycleLabelOpacity = (frame: number, start: number) => { const local = frame - start; if (local < 0) return 0; if (local === 0 || local === 3) return 1; if (local === 1 || local === 2) return 0; if (local === 4 || local === 6) return 0.42; return 1; }; const GlassNode: React.FC<{ frame: number; start: number; end: number; left: number; top: number; label: string; }> = ({ frame, start, end, left, top, label }) => { const k = frameSeg(frame, start, end, E.outBack); const opacity = frameSeg(frame, start, start + 2, E.outQuad); return (
{label}
); }; const Marker: React.FC<{ frame: number; start: number; end: number; left: number; top: number; rotate: number }> = ({ frame, start, end, left, top, rotate, }) => { const k = frameSeg(frame, start, end, E.outBack); return (
); }; export const CycleGlassNodeMorph: React.FC = () => { const frame = useCurrentFrame(); const wipe = frameSeg(frame, 66, 90, E.inOutCubic); const vehicle = frameSeg(frame, 62, 94, E.inOutCubic); const disk = frameSeg(frame, 66, 110, E.outCubic); const title = frameSeg(frame, 84, 90, E.outCubic); const arrows = frameSeg(frame, 127, 176, E.inOutCubic); const camScale = smoothCamera(frame, [0.61, 0.659, 0.935, 1]); const camX = smoothCamera(frame, [6, 5.5, 1, 0]); const camY = smoothCamera(frame, [-51, -44, -9, 0]); const topArrow = quadraticPoint([68, 98], [185, 4], [302, 98], arrows); const bottomArrow = quadraticPoint([302, 100], [185, 190], [68, 100], arrows); const wipeStop = interpolate(wipe, [0, 1], [-40, 140]); const labelData = [ { label: 'SENSE', left: 147, top: 92, start: 124 }, { label: 'MODEL', left: 240, top: 63, start: 129 }, { label: 'ACT', left: 333, top: 92, start: 134 }, ]; return (
CONTEXT
SYSTEM LOOP
SUBJECT
{labelData.map((item, i) => { const opacity = cycleLabelOpacity(frame, item.start); const drift = frameSeg(frame, item.start, 160, E.outCubic); const nodeTarget = frameSeg(frame, 176, 198, E.inOutCubic); const nodeTakeover = frameSeg(frame, 184 + i * 8, 192 + i * 8, E.outCubic); return (
{item.label}
); })}
); };