模板与 PageCam 深度解析
这页只看 template 如何把真实页面截图转成可复用 2.5D 产品片镜头。
导入链与契约
模板子页证据 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 和主模板不是泛用动画玩具,而是围绕真实页面纹理、页面坐标、绝对帧和可复现渲染建立的契约。只要素材三件套替换正确,镜头能保持原来的运动语法和构图质量。
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)`);
核心实现,逐段拆
▸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 和主模板不是泛用动画玩具,而是围绕真实页面纹理、页面坐标、绝对帧和可复现渲染建立的契约。只要素材三件套替换正确,镜头能保持原来的运动语法和构图质量。
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 和主模板不是泛用动画玩具,而是围绕真实页面纹理、页面坐标、绝对帧和可复现渲染建立的契约。只要素材三件套替换正确,镜头能保持原来的运动语法和构图质量。
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={{
关键流程
▸从 layout 到镜头
模板子页证据 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 和主模板不是泛用动画玩具,而是围绕真实页面纹理、页面坐标、绝对帧和可复现渲染建立的契约。只要素材三件套替换正确,镜头能保持原来的运动语法和构图质量。
边界 / 失败模式
- 如果 transform scale 替代 CSS zoom,3D 放大时文字可能先低分辨率栅格化再被 GPU 放大。
- 如果 children 不按 page-space 坐标定位,元素飞入无法落进真实页面槽位。
- 如果跳过静帧验收,PageCam 的 DOF 和透视很容易把可读文字变糊。