// ===== demos/data/chart-live-moves/AxisRescaleShockV2.tsx ===== // axis-rescale-shock-v2 —— 轴爆表重标 v2(批次 6 "改改再看" 重做) // 相对 v1 的加码:爆表点冲出卡片顶 80→220px(真的冲进标题字区域)、冲出段折线 // 加粗 10px 且变琥珀;重标瞬间"哗"——旧刻度数字向下飞出淡出、新刻度从上滑入, // 网格 4→8 根同帧加密;真图表语境:真标题 "Monthly revenue"、真轴刻度 // $25k/$50k/$75k/$100k → $100k/$200k/$300k/$400k、真月份 x 轴、端点弹真值 // 标签 "$340k";卡片震动 3→8px。收尾 f120 后真静止 40f。 // 帧确定性:数据硬编码,全部 frame 派生,无 Math.random / Date.now。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { G, TitleBlock } from '../../_fixtures/Fixtures'; const AMBER = '#b45309'; const CARD_W = 1060; const CARD_H = 600; const CX = (1920 - CARD_W) / 2; const CY = (1080 - CARD_H) / 2 + 40; const PAD = 52; const AXIS_W = 96; // 左侧 $ 刻度位 const PLOT_W = CARD_W - PAD * 2 - AXIS_W; const PLOT_H = 360; const PLOT_X = PAD + AXIS_W; const PLOT_Y = 130; // 历史数据($k,0–100 量程内温和爬升),最后一点爆表 340 const DATA = [22, 30, 26, 38, 35, 47, 44, 58, 55, 66, 72, 340]; const N = DATA.length; const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const HOLD = 12; const DRAW_END = HOLD + 34; // f46:历史段画完 const SHOCK_END = DRAW_END + 16; // f62:爆表点冲顶 const BEAT = SHOCK_END + 16; // f78:停半拍(悬在标题区) const RESCALE_END = BEAT + 12; // f90:重标完成 const MARK_END = RESCALE_END + 8; // f98:端点标记弹出 const VAL_END = MARK_END + 10; // f108:真值标签弹出 const easeDraw = Easing.inOut(Easing.cubic); export const AxisRescaleShockV2: React.FC = () => { const frame = useCurrentFrame(); // 量程:0–100 → 0–400,重标 12f out-cubic const range = interpolate(frame, [BEAT, RESCALE_END], [100, 400], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const yOf = (v: number): number => PLOT_H - (v / range) * PLOT_H; const drawT = interpolate(frame, [HOLD, DRAW_END], [0, N - 2], { easing: easeDraw, extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const shockT = interpolate(frame, [DRAW_END + 2, SHOCK_END], [0, 1], { easing: Easing.in(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // 冲顶停位:卡片上沿之上 220px(PLOT_Y=130 → 相对绘图区顶 -350),真冲进标题字区 const SHOCK_Y = -(PLOT_Y + 220); const rescaleP = interpolate(frame, [BEAT, RESCALE_END], [0, 1], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const xOf = (i: number): number => (i / (N - 1)) * PLOT_W; // 历史段点集 const basePts: string[] = []; const upto = Math.min(drawT, N - 2); for (let i = 0; i <= Math.floor(upto); i++) basePts.push(`${xOf(i).toFixed(2)},${yOf(DATA[i]).toFixed(2)}`); if (upto < N - 2 && upto > Math.floor(upto)) { const i = Math.floor(upto); const f = upto - i; const x = xOf(i) + (xOf(i + 1) - xOf(i)) * f; const y = yOf(DATA[i]) + (yOf(DATA[i + 1]) - yOf(DATA[i])) * f; basePts.push(`${x.toFixed(2)},${y.toFixed(2)}`); } // 爆表段单独一根(琥珀 + 10px 加粗),起点 = 历史最后一点 let headX = basePts.length ? Number(basePts[basePts.length - 1].split(',')[0]) : 0; let headY = basePts.length ? Number(basePts[basePts.length - 1].split(',')[1]) : 0; let shockSeg: string[] = []; if (shockT > 0) { const x0 = xOf(N - 2); const y0 = yOf(DATA[N - 2]); const x = x0 + (xOf(N - 1) - x0) * shockT; const yEnd = SHOCK_Y + (yOf(DATA[N - 1]) - SHOCK_Y) * rescaleP; const y = y0 + (yEnd - y0) * shockT; shockSeg = [`${x0.toFixed(2)},${y0.toFixed(2)}`, `${x.toFixed(2)},${y.toFixed(2)}`]; headX = x; headY = y; } // 重标"哗":旧刻度向下飞出淡出、新刻度从上滑入 const swap = interpolate(frame, [BEAT, BEAT + 10], [0, 1], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const OLD_TICKS = ['$25k', '$50k', '$75k', '$100k']; const NEW_TICKS = ['$100k', '$200k', '$300k', '$400k']; // 网格密度 4→8:新增的 4 根细网格随 swap 浮现 const denseOp = swap; const markS = interpolate(frame, [RESCALE_END, MARK_END], [0, 1], { easing: Easing.out(Easing.back(2.2)), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const valS = interpolate(frame, [MARK_END, VAL_END], [0, 1], { easing: Easing.out(Easing.back(1.8)), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // 爆表瞬间卡片震(±8px,8f 内衰减归零) const kick = frame >= SHOCK_END && frame < SHOCK_END + 8 ? 8 * (1 - (frame - SHOCK_END) / 8) * Math.sin((frame - SHOCK_END) * 2.6) : 0; const shockOn = frame >= DRAW_END; // 爆表段样式生效期 return (
{/* 真卡头 */}
Monthly revenue
FY2026 · all products · USD
{/* 基础网格 4 根 + 重标同帧加密出的 4 根 */} {[0, 1, 2, 3, 4].map((i) => (
))} {[1, 3, 5, 7].map((i) => (
))} {/* 刻度:同一格位,旧值下飞淡出 / 新值上方滑入 */} {OLD_TICKS.map((v, i) => { const y = (PLOT_H / 4) * (3 - i); return (
{v}
{NEW_TICKS[i]}
); })}
$0
{/* 图表上沿强调线 */}
{/* 历史段 */} {/* 爆表段:琥珀 + 10px,重标落回后收敛回常规 */} {shockSeg.length > 0 && ( )} {/* 端点标记 */} {markS > 0 && ( <> )} {/* 真值标签 "$340k"(端点旁弹出) */} {valS > 0 && (
$340k
)} {/* x 轴真月份 */} {MONTHS.map((m, i) => (
{m}
))}
); }; // ===== demos/data/chart-live-moves/OscilloscopeStreamV2.tsx ===== // oscilloscope-stream-v2 —— 示波流线 v2(批次 6 "改改再看" 重做) // 相对 v1 的加码:流速 5.5→8px/f;写入点亮点 1.5×(7→10.5)、余辉 90→160px; // 流动中途插入一次突发尖峰(振幅 2.2×、持续 ~20 帧写入后随流带走),尖峰经过写入点时 // 卡头实时读数跳大并变琥珀;真图表语境:真标题/真 y 轴刻度/真时间轴/实时读数。 // 刹停逻辑保留:f100–112 out-cubic 刹停,f112 后真静止 48f。 // 帧确定性:波形与尖峰包络都是纯 worldX 函数,无 Math.random / Date.now。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { G, TitleBlock } from '../../_fixtures/Fixtures'; const AMBER = '#b45309'; const CARD_W = 1080; const CARD_H = 560; const CX = (1920 - CARD_W) / 2; const CY = (1080 - CARD_H) / 2 + 40; const PAD = 46; const AXIS_W = 64; // 左侧 y 轴刻度位 const PLOT_X = PAD + AXIS_W; const PLOT_W = CARD_W - PAD * 2 - AXIS_W; // 924 const PLOT_H = 300; const PLOT_Y = 158; const HOLD = 12; const FREEZE_START = 100; const FREEZE_END = 112; const SPEED = 8; // px/frame(v1 5.5 → 8,~240px/s) const AMP = 0.72; // 基础振幅占半高比例(给尖峰留出头部空间) // 突发尖峰:worldX ∈ [288, 448](写入发生在 f48–68,之后随流向左带走) const SPIKE_X0 = 288; const SPIKE_W = 160; const SPIKE_GAIN = 1.2; // 峰值 = 1 + 1.2 = 2.2× const env = (x: number): number => { if (x <= SPIKE_X0 || x >= SPIKE_X0 + SPIKE_W) return 1; const p = (x - SPIKE_X0) / SPIKE_W; return 1 + SPIKE_GAIN * (0.5 - 0.5 * Math.cos(p * Math.PI * 2)); }; // 有效时间:HOLD 前为 0,之后线性流动,FREEZE 区间 easeOut 刹停为常数 const effTime = (frame: number): number => { const t = (f: number) => Math.max(f - HOLD, 0) * SPEED; if (frame <= FREEZE_START) return t(frame); const brakeDist = (t(FREEZE_END) - t(FREEZE_START)) * 0.45; return ( t(FREEZE_START) + interpolate(frame, [FREEZE_START, FREEZE_END], [0, brakeDist], { easing: Easing.out(Easing.cubic), extrapolateRight: 'clamp', }) ); }; // 波形:四个正弦叠加,x 单位 = 世界像素 const wave = (x: number): number => 0.34 * Math.sin(x * 0.021) + 0.27 * Math.sin(x * 0.052 + 1.7) + 0.18 * Math.sin(x * 0.013 + 4.2) + 0.12 * Math.sin(x * 0.087 + 2.3); const signal = (x: number): number => wave(x) * env(x) * AMP; // ∈ ~[-1.6, 1.6],常态 [-0.65, 0.65] const yOf = (x: number): number => PLOT_H / 2 - signal(x) * (PLOT_H / 2); // 读数映射:绘图区中线 = 1,000 req/s,上下沿 = 2,000 / 0 const valueOf = (x: number): number => Math.round(1000 + signal(x) * 1000); const fmt = (n: number): string => n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); const Y_TICKS = ['2.0k', '1.5k', '1.0k', '0.5k', '0']; const X_TICKS = ['-60s', '-50s', '-40s', '-30s', '-20s', '-10s', 'now']; export const OscilloscopeStreamV2: React.FC = () => { const frame = useCurrentFrame(); const T = effTime(frame); const N = 270; const pts: string[] = []; for (let i = 0; i <= N; i++) { const px = (i / N) * PLOT_W; const worldX = T - (PLOT_W - px); pts.push(`${px.toFixed(2)},${yOf(worldX).toFixed(2)}`); } const headY = yOf(T); const frozen = frame >= FREEZE_END; const glowOp = interpolate(frame, [FREEZE_START, FREEZE_END], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // 余辉段:头部往回 160px(v1 90 → 160) const TAIL = 160; const tailPts: string[] = []; if (!frozen) { for (let i = 0; i <= 40; i++) { const px = PLOT_W - TAIL + (i / 40) * TAIL; const worldX = T - (PLOT_W - px); tailPts.push(`${px.toFixed(2)},${yOf(worldX).toFixed(2)}`); } } // 实时读数:尖峰经过写入点时跳大 + 变琥珀 const spikeK = Math.min(Math.max((env(T) - 1) / SPIKE_GAIN, 0), 1); const hot = spikeK > 0.22; const readout = fmt(valueOf(T)); const readScale = 1 + 0.32 * spikeK; const dotColor = spikeK > 0.15 ? AMBER : G.ink; return (
{/* 真卡头:标题 + 副题 + 实时读数 */}
Requests per second
api-gateway · production · last 60 s
{readout}
req/s · live
{/* y 轴真刻度 */} {Y_TICKS.map((t, i) => (
{t}
))}
{/* 网格 */} {[0, 1, 2, 3, 4].map((i) => (
))} {[0, 1, 2, 3, 4, 5, 6].map((i) => (
))} {!frozen && glowOp > 0 && ( )} {/* 写入点亮点(1.5×) */} {!frozen && glowOp > 0 && ( <> 0.15 ? AMBER : '#8f8f8d'} opacity={0.35 * glowOp} style={{ filter: 'blur(5px)' }} /> )}
{/* x 轴真时间刻度 */}
{X_TICKS.map((t, i) => (
{t}
))}
); }; // ===== demos/data/chart-live-moves/UnitDotSwarmRegroupV2.tsx ===== // unit-dot-swarm-regroup-v2 —— 单位点阵重组 v2(批次 6 "改改再看" 重做) // 相对 v1 的加码:点数 200→320、点径 7→9;真实叙事语境:图例 "Each dot ≈ 40 customers", // 聚簇时每簇上方浮真标签(Free · 7,210 / Pro · 4,102 / Enterprise · 1,535,Pro 琥珀主角色), // 列队成柱时柱底浮真轴标 + 基线;终幕点阵聚成 "12,847"(1/2/,/8/4/7 位图字形), // 下方浮 "Total customers"。阶段间 hold 更短(14/38/36f 间隔)、迁徙 spring 更冲 // (stiffness 110→150、DUR 26→20)。收尾 f126 后真静止 44f。 // 帧确定性:伪随机全用 sin 散列,无 Math.random / Date.now。 import React from 'react'; import { useCurrentFrame, spring, interpolate } from 'remotion'; import { G, TitleBlock } from '../../_fixtures/Fixtures'; const AMBER = '#b45309'; const FPS = 30; const N = 320; const DOT_R = 9; const M1 = 12; // 聚簇 const M2 = 48; // 列队成柱 const M3 = 84; // 收拢成 12,847 const DUR = 20; const STAG = 8; // 帧确定伪随机 const rnd = (i: number, salt: number): number => { const x = Math.sin(i * 12.9898 + salt * 78.233) * 43758.5453; return x - Math.floor(x); }; // —— 阶段 0:散布(星群) —— const scatter = (i: number): [number, number] => [ 300 + rnd(i, 1) * 1320, 330 + rnd(i, 2) * 620, ]; // —— 分组:Free 180 / Pro 102 / Enterprise 38(每点 ≈ 40 customers) —— const groupOf = (i: number): number => (i < 180 ? 0 : i < 282 ? 1 : 2); const idxInGroup = (i: number): number => (i < 180 ? i : i < 282 ? i - 180 : i - 282); const GROUP_N = [180, 102, 38]; const GROUP_LABEL = ['Free · 7,210', 'Pro · 4,102', 'Enterprise · 1,535']; // —— 阶段 1:三簇(圆盘散布,sqrt 半径均匀) —— const CLUSTER_C: [number, number][] = [ [520, 620], [980, 570], [1400, 640], ]; const CLUSTER_R = GROUP_N.map((n) => 58 + n * 0.46); // 141 / 105 / 75 const cluster = (i: number): [number, number] => { const g = groupOf(i); const r = Math.sqrt(rnd(i, 3)) * CLUSTER_R[g]; const a = rnd(i, 4) * Math.PI * 2; return [CLUSTER_C[g][0] + r * Math.cos(a), CLUSTER_C[g][1] + r * Math.sin(a)]; }; // —— 阶段 2:三根柱(8 点宽网格自底向上码放,底边对齐) —— const BAR_BASE = 840; const BAR_X = [520, 980, 1400]; const SPACING = 20; const bar = (i: number): [number, number] => { const g = groupOf(i); const j = idxInGroup(i); const col = j % 8; const row = Math.floor(j / 8); return [BAR_X[g] + (col - 3.5) * SPACING, BAR_BASE - row * SPACING]; }; // —— 阶段 3:数字 "12,847" 点阵(5×7 位图 × 2×2 上采样 + 逗号) —— const ONE = ['00100', '01100', '00100', '00100', '00100', '00100', '01110']; const TWO = ['01110', '10001', '00001', '00010', '00100', '01000', '11111']; const EIGHT = ['01110', '10001', '10001', '01110', '10001', '10001', '01110']; const FOUR = ['00110', '01010', '10010', '11111', '00010', '00010', '00010']; const SEVEN = ['11111', '00001', '00010', '00100', '00100', '00100', '00100']; const CELL = 40; const SUB = 20; const Y0 = 390; const cellPts = (x: number, y: number): [number, number][] => [ [x, y], [x + SUB, y], [x, y + SUB], [x + SUB, y + SUB], ]; const buildDigit = (bitmap: string[], x0: number): [number, number][] => { const out: [number, number][] = []; bitmap.forEach((rowStr, r) => { rowStr.split('').forEach((c, col) => { if (c === '1') out.push(...cellPts(x0 + col * CELL, Y0 + r * CELL)); }); }); return out; }; const DIGIT_PTS: [number, number][] = [ ...buildDigit(ONE, 350), ...buildDigit(TWO, 590), // 逗号:基线处两格,微斜的小尾巴 ...cellPts(845, Y0 + 5.4 * CELL), ...cellPts(836, Y0 + 6.3 * CELL), ...buildDigit(EIGHT, 920), ...buildDigit(FOUR, 1160), ...buildDigit(SEVEN, 1400), ]; const digit = (i: number): [number, number] => { const p = DIGIT_PTS[i % DIGIT_PTS.length]; const jx = (rnd(i, 5) - 0.5) * 7; const jy = (rnd(i, 6) - 0.5) * 7; return [p[0] + jx, p[1] + jy]; }; const lerp = (a: [number, number], b: [number, number], t: number): [number, number] => [ a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, ]; const fade = (frame: number, inA: number, inB: number, outA?: number, outB?: number): number => { const fi = interpolate(frame, [inA, inB], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }); if (outA === undefined || outB === undefined) return fi; const fo = interpolate(frame, [outA, outB], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }); return Math.min(fi, fo); }; const LABEL_FONT = 'Helvetica, Arial, sans-serif'; export const UnitDotSwarmRegroupV2: React.FC = () => { const frame = useCurrentFrame(); const dots = Array.from({ length: N }, (_, i) => { const stag = rnd(i, 7) * STAG; const mig = (start: number) => spring({ frame: frame - start - stag, fps: FPS, config: { damping: 11.5, stiffness: 150, mass: 0.8 }, durationInFrames: DUR, durationRestThreshold: 0.0001, }); let p = scatter(i); p = lerp(p, cluster(i), mig(M1)); p = lerp(p, bar(i), mig(M2)); p = lerp(p, digit(i), mig(M3)); return p; }); // 各阶段标签透明度(迁徙完成后浮现,下一次迁徙启动即撤) const clusterLabelOp = fade(frame, 38, 46, M2, M2 + 8); const barLabelOp = fade(frame, 72, 80, M3, M3 + 8); const captionOp = fade(frame, 114, 126); return (
{/* 图例:每点的含义(全程挂角) */}
Each dot ≈ 40 customers
{dots.map((p, i) => ( ))} {/* 阶段 1:簇标签(真人数) */} {clusterLabelOp > 0 && CLUSTER_C.map((c, g) => (
{GROUP_LABEL[g]}
))} {/* 阶段 2:柱底基线 + 真轴标 */} {barLabelOp > 0 && ( <>
{BAR_X.map((x, g) => (
{['Free', 'Pro', 'Enterprise'][g]}
))} )} {/* 终幕:数字下方真文案 */} {captionOp > 0 && (
Total customers
)}
); };