videoShotcraft
Nested detail page

模板与 PageCam 深度解析

这页只看 template 如何把真实页面截图转成可复用 2.5D 产品片镜头。

§ 00 · IMPORT CHAIN

导入链与契约

模板子页证据 01:src/aifl/live/PageCam.tsx:4、src/aifl/live/PageCam.tsx:29、src/aifl/live/PageCam.tsx:42、src/aifl/live/PageCam.tsx:50、src/aifl/live/PageCam.tsx:62。这些行展示 PageCam 和主模板不是泛用动画玩具,而是围绕真实页面纹理、页面坐标、绝对帧和可复现渲染建立的契约。只要素材三件套替换正确,镜头能保持原来的运动语法和构图质量。

src/aifl/live/PageCam.tsx · L29-52TSX
29  src: string; // staticFile path under textures/live/
30  pageH: number; // CSS page height
31  keys: CamKey[];
32  children?: React.ReactNode; // page-space overlays (positioned in CSS px)
33  blur?: number;
34  saturate?: number;
35  ease?: (t: number) => number;
36  dof?: { focusY: number; strength: number };
37}> = ({ src, pageH, keys, children, blur = 0, saturate = 1, ease = Easing.bezier(0.33, 0, 0.15, 1), dof }) => {
38  const frame = useCurrentFrame();
39  // find segment
40  let a = keys[0], b = keys[keys.length - 1];
41  for (let i = 0; i < keys.length - 1; i++) {
42    if (frame >= keys[i].frame && frame <= keys[i + 1].frame) { a = keys[i]; b = keys[i + 1]; break; }
43  }
44  const t = a.frame === b.frame ? 1 : interpolate(frame, [a.frame, b.frame], [0, 1], {
45    extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: ease,
46  });
47  const cx = lerp(a.cx, b.cx, t);
48  const cy = lerp(a.cy, b.cy, t);
49  const zoom = lerp(a.zoom, b.zoom, t);
50
51  const filters: string[] = [];
52  if (blur > 0) filters.push(`blur(${blur}px)`);
§ 01 · CORE IMPL

核心实现,逐段拆

▸flat mode 保持像素等价

模板子页证据 02:src/aifl/live/PageCam.tsx:64、src/aifl/live/PageCam.tsx:70、src/aifl/live/PageCam.tsx:75、src/aifl/live/PageCam.tsx:82、src/aifl/live/PageCam.tsx:95。这些行展示 PageCam 和主模板不是泛用动画玩具,而是围绕真实页面纹理、页面坐标、绝对帧和可复现渲染建立的契约。只要素材三件套替换正确,镜头能保持原来的运动语法和构图质量。

src/aifl/live/PageCam.tsx · L61-80TSX
61        <div
62          style={{
63            position: 'absolute', width: 1920, height: pageH,
64            transform: `translate(${960 - cx * zoom}px, ${540 - cy * zoom}px) scale(${zoom})`,
65            transformOrigin: '0 0',
66            filter: filters.length ? filters.join(' ') : undefined,
67          }}
68        >
69          <Img src={staticFile(src)} style={{ position: 'absolute', width: 1920, height: pageH }} />
70          {children}
71        </div>
72      </AbsoluteFill>
73    );
74  }
75
76  // 3D mode: pivot rotation/scale about the focal page-point (cx, cy) so it
77  // stays centered in the viewport. With rotX=rotZ=0 this reduces to the flat
78  // transform (proven identical: (960,540) + zoom*(p - (cx,cy))).
79  const rotX = lerp(a.rotX ?? 0, b.rotX ?? 0, t);
80  const rotY = lerp(a.rotY ?? 0, b.rotY ?? 0, t);

▸3D mode 的布局级放大

模板子页证据 03:src/aifl/live/PageCam.tsx:99、src/aifl/live/PageCam.tsx:119、src/aifl/live/PageCam.tsx:120、src/aifl/live/PageCam.tsx:126、src/aifl/live/PageCam.tsx:133。这些行展示 PageCam 和主模板不是泛用动画玩具,而是围绕真实页面纹理、页面坐标、绝对帧和可复现渲染建立的契约。只要素材三件套替换正确,镜头能保持原来的运动语法和构图质量。

src/aifl/live/PageCam.tsx · L82-129TSX
82  const persp = lerp(a.persp ?? 1400, b.persp ?? 1400, t);
83
84  return (
85    <AbsoluteFill style={{ overflow: 'hidden', backgroundColor: '#faf7f2' }}>
86      <div
87        style={{
88          position: 'absolute', inset: 0,
89          perspective: `${persp * zoom}px`,
90          perspectiveOrigin: '960px 540px',
91        }}
92      >
93        {/* LAYOUT-SCALE zoom: instead of scale(zoom) in the transform chain (which
94            makes Chromium rasterize the 3D-composited layer at 1920-wide LAYOUT
95            size and then GPU-upscale by zoom — everything inside gets downsampled
96            before magnification, hence blurry text), we apply the magnification as
97            the CSS `zoom` property. `zoom` enlarges the layout box itself, so the
98            page + card textures rasterize at the ENLARGED device size and sample
99            down from their hi-res sources → sharp glyph edges under perspective.
100
101            Coordinate math: `zoom` scales this element's local coordinate space by
102            `zoom`, so a page point (cx,cy) renders at (cx*zoom, cy*zoom) device px
103            from the box origin, and a `translate(Tx px)` renders as Tx*zoom device
104            px. To land the focal point (cx,cy) at viewport centre (960,540):
105              cx*zoom + Tx*zoom = 960  ⟹  Tx = 960/zoom - cx  (likewise Ty).
106            Rotations pivot about transform-origin (cx,cy) = the focal point, so
107            they leave its screen position unchanged. With rot=0 this reduces to
108            translate(960/zoom - cx, 540/zoom - cy) under zoom — identical framing
109            to the old scale-based transform, just rasterized at layout scale. */}
110        <div
111          style={{
112            position: 'absolute', width: 1920, height: pageH,
113            zoom,
114            transform: `translate(${960 / zoom - cx}px, ${540 / zoom - cy}px) rotateY(${rotY}deg) rotateX(${rotX}deg) rotateZ(${rotZ}deg)`,
115            transformOrigin: `${cx}px ${cy}px`,
116            transformStyle: 'preserve-3d',
117            filter: filters.length ? filters.join(' ') : undefined,
118          }}
119        >
120          <Img src={staticFile(src)} style={{ position: 'absolute', width: 1920, height: pageH }} />
121          {children}
122        </div>
123      </div>
124
125      {/* Depth-of-field approximation: a top-band gradient blur (far part of a
126          tilted page reads soft). Screen-space, over the page. */}
127      {dof ? (
128        <div
129          style={{
§ 02 · KEY FLOW

关键流程

▸从 layout 到镜头

layout.json -> PageCam keys -> texture plane -> page-space children -> Sequence timing

模板子页证据 04:src/aifl/live/SceneOpen.tsx:34、src/aifl/live/SceneOpen.tsx:130、src/aifl/live/SceneOpen.tsx:135、src/aifl/live/SceneOpen.tsx:184、src/aifl/live/SceneOpen.tsx:201。这些行展示 PageCam 和主模板不是泛用动画玩具,而是围绕真实页面纹理、页面坐标、绝对帧和可复现渲染建立的契约。只要素材三件套替换正确,镜头能保持原来的运动语法和构图质量。

§ 03 · EDGE CASES

边界 / 失败模式

  • 如果 transform scale 替代 CSS zoom,3D 放大时文字可能先低分辨率栅格化再被 GPU 放大。
  • 如果 children 不按 page-space 坐标定位,元素飞入无法落进真实页面槽位。
  • 如果跳过静帧验收,PageCam 的 DOF 和透视很容易把可读文字变糊。
Generated · 2026-07-24Source · Vincentwei1021/video-shotcraft全部分析