@sliiu/present-renderer 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COPYRIGHT +5 -0
- package/LICENSE +661 -0
- package/THIRD-PARTY-NOTICES.md +221 -0
- package/dist/cjs/arrow-geom.js +38 -0
- package/dist/cjs/canvas-backend.js +1368 -0
- package/dist/cjs/cell-borders.js +25 -0
- package/dist/cjs/chart-canvas.js +1059 -0
- package/dist/cjs/chart-ex-render.js +275 -0
- package/dist/cjs/chart-geom.js +313 -0
- package/dist/cjs/chart-svg.js +1110 -0
- package/dist/cjs/compose.js +74 -0
- package/dist/cjs/effect-clip.js +652 -0
- package/dist/cjs/entrance-hide.js +63 -0
- package/dist/cjs/font-stack.js +103 -0
- package/dist/cjs/hit-geom.js +50 -0
- package/dist/cjs/hybrid-backend.js +93 -0
- package/dist/cjs/hybrid-segments.js +54 -0
- package/dist/cjs/index.js +70 -0
- package/dist/cjs/linked-src.js +116 -0
- package/dist/cjs/metafile-preview.js +142 -0
- package/dist/cjs/model3d-frame.js +199 -0
- package/dist/cjs/model3d-render.js +78 -0
- package/dist/cjs/morph-transition.js +57 -0
- package/dist/cjs/node-affine.js +79 -0
- package/dist/cjs/ole-render.js +291 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/paint-canvas.js +701 -0
- package/dist/cjs/paint-svg.js +674 -0
- package/dist/cjs/scene-node.js +762 -0
- package/dist/cjs/selection.js +130 -0
- package/dist/cjs/shape-effects.js +66 -0
- package/dist/cjs/shape-front.js +38 -0
- package/dist/cjs/stage-chrome.js +15 -0
- package/dist/cjs/svg-backend.js +2172 -0
- package/dist/cjs/text-autofit.js +48 -0
- package/dist/cjs/text-hit.js +284 -0
- package/dist/cjs/text-layout.js +408 -0
- package/dist/cjs/text-shared.js +238 -0
- package/dist/cjs/text-stacked.js +82 -0
- package/dist/cjs/text-warp.js +377 -0
- package/dist/cjs/text-wordart.js +33 -0
- package/dist/cjs/video-frame.js +113 -0
- package/dist/cjs/webgl-model3d-backend.js +285 -0
- package/dist/cjs/webgl-shape-backend.js +360 -0
- package/dist/esm/arrow-geom.js +33 -0
- package/dist/esm/canvas-backend.js +1363 -0
- package/dist/esm/cell-borders.js +21 -0
- package/dist/esm/chart-canvas.js +1056 -0
- package/dist/esm/chart-ex-render.js +261 -0
- package/dist/esm/chart-geom.js +296 -0
- package/dist/esm/chart-svg.js +1107 -0
- package/dist/esm/compose.js +71 -0
- package/dist/esm/effect-clip.js +648 -0
- package/dist/esm/entrance-hide.js +59 -0
- package/dist/esm/font-stack.js +100 -0
- package/dist/esm/hit-geom.js +45 -0
- package/dist/esm/hybrid-backend.js +89 -0
- package/dist/esm/hybrid-segments.js +51 -0
- package/dist/esm/index.js +24 -0
- package/dist/esm/linked-src.js +109 -0
- package/dist/esm/metafile-preview.js +135 -0
- package/dist/esm/model3d-frame.js +158 -0
- package/dist/esm/model3d-render.js +73 -0
- package/dist/esm/morph-transition.js +54 -0
- package/dist/esm/node-affine.js +71 -0
- package/dist/esm/ole-render.js +286 -0
- package/dist/esm/paint-canvas.js +693 -0
- package/dist/esm/paint-svg.js +670 -0
- package/dist/esm/scene-node.js +750 -0
- package/dist/esm/selection.js +122 -0
- package/dist/esm/shape-effects.js +60 -0
- package/dist/esm/shape-front.js +35 -0
- package/dist/esm/stage-chrome.js +12 -0
- package/dist/esm/svg-backend.js +2168 -0
- package/dist/esm/text-autofit.js +44 -0
- package/dist/esm/text-hit.js +280 -0
- package/dist/esm/text-layout.js +400 -0
- package/dist/esm/text-shared.js +221 -0
- package/dist/esm/text-stacked.js +78 -0
- package/dist/esm/text-warp.js +371 -0
- package/dist/esm/text-wordart.js +30 -0
- package/dist/esm/video-frame.js +109 -0
- package/dist/esm/webgl-model3d-backend.js +248 -0
- package/dist/esm/webgl-shape-backend.js +323 -0
- package/dist/types/arrow-geom.d.ts +10 -0
- package/dist/types/canvas-backend.d.ts +55 -0
- package/dist/types/cell-borders.d.ts +9 -0
- package/dist/types/chart-canvas.d.ts +2 -0
- package/dist/types/chart-ex-render.d.ts +79 -0
- package/dist/types/chart-geom.d.ts +112 -0
- package/dist/types/chart-svg.d.ts +2 -0
- package/dist/types/compose.d.ts +2 -0
- package/dist/types/effect-clip.d.ts +10 -0
- package/dist/types/entrance-hide.d.ts +16 -0
- package/dist/types/font-stack.d.ts +9 -0
- package/dist/types/hit-geom.d.ts +15 -0
- package/dist/types/hybrid-backend.d.ts +21 -0
- package/dist/types/hybrid-segments.d.ts +11 -0
- package/dist/types/index.d.ts +24 -0
- package/dist/types/linked-src.d.ts +13 -0
- package/dist/types/metafile-preview.d.ts +11 -0
- package/dist/types/model3d-frame.d.ts +8 -0
- package/dist/types/model3d-render.d.ts +4 -0
- package/dist/types/morph-transition.d.ts +15 -0
- package/dist/types/node-affine.d.ts +22 -0
- package/dist/types/ole-render.d.ts +7 -0
- package/dist/types/paint-canvas.d.ts +22 -0
- package/dist/types/paint-svg.d.ts +19 -0
- package/dist/types/scene-node.d.ts +128 -0
- package/dist/types/selection.d.ts +22 -0
- package/dist/types/shape-effects.d.ts +13 -0
- package/dist/types/shape-front.d.ts +4 -0
- package/dist/types/stage-chrome.d.ts +8 -0
- package/dist/types/svg-backend.d.ts +88 -0
- package/dist/types/text-autofit.d.ts +7 -0
- package/dist/types/text-hit.d.ts +7 -0
- package/dist/types/text-layout.d.ts +35 -0
- package/dist/types/text-shared.d.ts +86 -0
- package/dist/types/text-stacked.d.ts +11 -0
- package/dist/types/text-warp.d.ts +37 -0
- package/dist/types/text-wordart.d.ts +18 -0
- package/dist/types/video-frame.d.ts +2 -0
- package/dist/types/webgl-model3d-backend.d.ts +25 -0
- package/dist/types/webgl-shape-backend.d.ts +33 -0
- package/package.json +33 -4
- package/README.md +0 -3
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { applyModel3DCameraPose, applyModel3DTransform, loadGltfScene } from "./model3d-frame.js";
|
|
3
|
+
let sharedRenderer = null;
|
|
4
|
+
let sharedRendererFailed = false;
|
|
5
|
+
function getSharedRenderer() {
|
|
6
|
+
if (sharedRenderer)
|
|
7
|
+
return sharedRenderer;
|
|
8
|
+
if (sharedRendererFailed)
|
|
9
|
+
return null;
|
|
10
|
+
try {
|
|
11
|
+
const r = new THREE.WebGLRenderer({ alpha: true, antialias: true, preserveDrawingBuffer: true });
|
|
12
|
+
r.setPixelRatio(1);
|
|
13
|
+
r.setClearColor(0x000000, 0);
|
|
14
|
+
r.outputColorSpace = THREE.SRGBColorSpace;
|
|
15
|
+
sharedRenderer = r;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
sharedRendererFailed = true;
|
|
19
|
+
}
|
|
20
|
+
return sharedRenderer;
|
|
21
|
+
}
|
|
22
|
+
export class WebGLModel3DBackend {
|
|
23
|
+
space;
|
|
24
|
+
managed = [];
|
|
25
|
+
dispose = [];
|
|
26
|
+
k = 1;
|
|
27
|
+
rafId = 0;
|
|
28
|
+
readyResolvers = [];
|
|
29
|
+
constructor(scene, container, opts = {}) {
|
|
30
|
+
const shapeFilter = opts.shapeFilter ?? (() => true);
|
|
31
|
+
const interactive = opts.interactive === true;
|
|
32
|
+
this.space = document.createElement("div");
|
|
33
|
+
this.space.style.cssText = "position:absolute;left:0;top:0;transform-origin:0 0;pointer-events:none";
|
|
34
|
+
this.space.style.width = `${scene.width}px`;
|
|
35
|
+
this.space.style.height = `${scene.height}px`;
|
|
36
|
+
container.appendChild(this.space);
|
|
37
|
+
scene.root.walk((n) => {
|
|
38
|
+
if (n.shape.kind !== "model3d")
|
|
39
|
+
return;
|
|
40
|
+
const s = n.shape;
|
|
41
|
+
if (!shapeFilter(s))
|
|
42
|
+
return;
|
|
43
|
+
if (!s.glbSrc)
|
|
44
|
+
return;
|
|
45
|
+
this.add(n, s, interactive);
|
|
46
|
+
});
|
|
47
|
+
const updateScale = () => {
|
|
48
|
+
const w = container.clientWidth || scene.width;
|
|
49
|
+
const k = w / scene.width;
|
|
50
|
+
if (!Number.isFinite(k) || k <= 0)
|
|
51
|
+
return;
|
|
52
|
+
this.k = k;
|
|
53
|
+
this.space.style.transform = `scale(${k})`;
|
|
54
|
+
this.scheduleRender();
|
|
55
|
+
};
|
|
56
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
57
|
+
const ro = new ResizeObserver(updateScale);
|
|
58
|
+
ro.observe(container);
|
|
59
|
+
this.dispose.push(() => ro.disconnect());
|
|
60
|
+
}
|
|
61
|
+
updateScale();
|
|
62
|
+
this.scheduleRender();
|
|
63
|
+
}
|
|
64
|
+
add(node, s, interactive) {
|
|
65
|
+
const wrapper = document.createElement("div");
|
|
66
|
+
wrapper.style.position = "absolute";
|
|
67
|
+
wrapper.style.overflow = "hidden";
|
|
68
|
+
const canvas = document.createElement("canvas");
|
|
69
|
+
canvas.style.width = "100%";
|
|
70
|
+
canvas.style.height = "100%";
|
|
71
|
+
canvas.style.display = "block";
|
|
72
|
+
wrapper.appendChild(canvas);
|
|
73
|
+
this.space.appendChild(wrapper);
|
|
74
|
+
const m = {
|
|
75
|
+
node,
|
|
76
|
+
bbox: { ...s.bbox },
|
|
77
|
+
wrapper,
|
|
78
|
+
canvas,
|
|
79
|
+
ctx: canvas.getContext("2d"),
|
|
80
|
+
scene: null,
|
|
81
|
+
camera: null,
|
|
82
|
+
lat: s.camera?.rot?.lat ?? 15,
|
|
83
|
+
lon: s.camera?.rot?.lon ?? 25,
|
|
84
|
+
lastW: 0,
|
|
85
|
+
lastH: 0,
|
|
86
|
+
};
|
|
87
|
+
this.managed.push(m);
|
|
88
|
+
if (interactive)
|
|
89
|
+
this.bindOrbit(m);
|
|
90
|
+
void loadGltfScene(s.glbSrc).then((root) => {
|
|
91
|
+
const sc = new THREE.Scene();
|
|
92
|
+
const clone = root.clone(true);
|
|
93
|
+
applyModel3DTransform(clone, s);
|
|
94
|
+
sc.add(clone);
|
|
95
|
+
const light = new THREE.DirectionalLight(0xffffff, 1.1);
|
|
96
|
+
light.position.set(2, 3, 4);
|
|
97
|
+
sc.add(light);
|
|
98
|
+
sc.add(new THREE.AmbientLight(0xffffff, 0.45));
|
|
99
|
+
const cam = new THREE.PerspectiveCamera(35, 1, 0.01, 100);
|
|
100
|
+
applyModel3DCameraPose(cam, m.lat, m.lon);
|
|
101
|
+
m.scene = sc;
|
|
102
|
+
m.camera = cam;
|
|
103
|
+
this.syncWrapper(m);
|
|
104
|
+
this.scheduleRender();
|
|
105
|
+
this.resolveReady();
|
|
106
|
+
}).catch(() => this.resolveReady());
|
|
107
|
+
const off = node.on(() => this.syncWrapper(m));
|
|
108
|
+
this.dispose.push(off);
|
|
109
|
+
}
|
|
110
|
+
bindOrbit(m) {
|
|
111
|
+
const el = m.wrapper;
|
|
112
|
+
el.style.pointerEvents = "auto";
|
|
113
|
+
el.style.cursor = "grab";
|
|
114
|
+
el.style.touchAction = "none";
|
|
115
|
+
let dragging = false;
|
|
116
|
+
let lastX = 0;
|
|
117
|
+
let lastY = 0;
|
|
118
|
+
const stop = (e) => e.stopPropagation();
|
|
119
|
+
el.addEventListener("pointerdown", (e) => {
|
|
120
|
+
if (e.button !== 0)
|
|
121
|
+
return;
|
|
122
|
+
e.stopPropagation();
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
dragging = true;
|
|
125
|
+
lastX = e.clientX;
|
|
126
|
+
lastY = e.clientY;
|
|
127
|
+
try {
|
|
128
|
+
el.setPointerCapture(e.pointerId);
|
|
129
|
+
}
|
|
130
|
+
catch { }
|
|
131
|
+
el.style.cursor = "grabbing";
|
|
132
|
+
});
|
|
133
|
+
el.addEventListener("pointermove", (e) => {
|
|
134
|
+
if (!dragging)
|
|
135
|
+
return;
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
const dx = e.clientX - lastX;
|
|
138
|
+
const dy = e.clientY - lastY;
|
|
139
|
+
lastX = e.clientX;
|
|
140
|
+
lastY = e.clientY;
|
|
141
|
+
this.orbitModel3D(m.node.shape.id, -dx * 0.45, dy * 0.45);
|
|
142
|
+
});
|
|
143
|
+
const end = (e) => {
|
|
144
|
+
if (!dragging)
|
|
145
|
+
return;
|
|
146
|
+
dragging = false;
|
|
147
|
+
el.style.cursor = "grab";
|
|
148
|
+
try {
|
|
149
|
+
el.releasePointerCapture(e.pointerId);
|
|
150
|
+
}
|
|
151
|
+
catch { }
|
|
152
|
+
e.stopPropagation();
|
|
153
|
+
};
|
|
154
|
+
el.addEventListener("pointerup", end);
|
|
155
|
+
el.addEventListener("pointercancel", end);
|
|
156
|
+
el.addEventListener("mousedown", stop);
|
|
157
|
+
el.addEventListener("click", stop);
|
|
158
|
+
el.addEventListener("dblclick", stop);
|
|
159
|
+
el.addEventListener("touchstart", stop, { passive: true });
|
|
160
|
+
}
|
|
161
|
+
syncWrapper(m) {
|
|
162
|
+
const n = m.node;
|
|
163
|
+
const s = n.shape;
|
|
164
|
+
const b = s.bbox;
|
|
165
|
+
m.bbox = { ...b };
|
|
166
|
+
m.wrapper.style.left = `${b.x}px`;
|
|
167
|
+
m.wrapper.style.top = `${b.y}px`;
|
|
168
|
+
m.wrapper.style.width = `${b.w}px`;
|
|
169
|
+
m.wrapper.style.height = `${b.h}px`;
|
|
170
|
+
m.wrapper.style.opacity = String(n.opacity * ((n.visibleOverride ?? s.visible) !== false ? 1 : 0));
|
|
171
|
+
m.wrapper.style.transform = `translate(${n.tx}px,${n.ty}px) rotate(${n.rotation}deg) scale(${n.scale})`;
|
|
172
|
+
}
|
|
173
|
+
scheduleRender() {
|
|
174
|
+
if (this.rafId)
|
|
175
|
+
return;
|
|
176
|
+
this.rafId = requestAnimationFrame(() => {
|
|
177
|
+
this.rafId = 0;
|
|
178
|
+
this.renderAll();
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
renderAll() {
|
|
182
|
+
const renderer = getSharedRenderer();
|
|
183
|
+
if (!renderer)
|
|
184
|
+
return;
|
|
185
|
+
const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
|
|
186
|
+
for (const m of this.managed) {
|
|
187
|
+
if (!m.scene || !m.camera || !m.ctx)
|
|
188
|
+
continue;
|
|
189
|
+
const w = Math.max(1, Math.ceil(m.bbox.w * this.k * dpr));
|
|
190
|
+
const h = Math.max(1, Math.ceil(m.bbox.h * this.k * dpr));
|
|
191
|
+
m.lastW = w;
|
|
192
|
+
m.lastH = h;
|
|
193
|
+
if (m.canvas.width !== w)
|
|
194
|
+
m.canvas.width = w;
|
|
195
|
+
if (m.canvas.height !== h)
|
|
196
|
+
m.canvas.height = h;
|
|
197
|
+
m.camera.aspect = w / h;
|
|
198
|
+
m.camera.updateProjectionMatrix();
|
|
199
|
+
renderer.setSize(w, h, false);
|
|
200
|
+
renderer.render(m.scene, m.camera);
|
|
201
|
+
m.ctx.clearRect(0, 0, w, h);
|
|
202
|
+
m.ctx.drawImage(renderer.domElement, 0, 0, w, h);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
resolveReady() {
|
|
206
|
+
const rs = this.readyResolvers.splice(0);
|
|
207
|
+
for (const r of rs)
|
|
208
|
+
r();
|
|
209
|
+
}
|
|
210
|
+
orbitModel3D(shapeId, dLon, dLat) {
|
|
211
|
+
const m = this.managed.find((x) => x.node.shape.id === shapeId);
|
|
212
|
+
if (!m)
|
|
213
|
+
return false;
|
|
214
|
+
if (dLon === 0 && dLat === 0)
|
|
215
|
+
return true;
|
|
216
|
+
m.lon += dLon;
|
|
217
|
+
m.lat = Math.max(-85, Math.min(85, m.lat + dLat));
|
|
218
|
+
if (m.camera) {
|
|
219
|
+
applyModel3DCameraPose(m.camera, m.lat, m.lon);
|
|
220
|
+
this.scheduleRender();
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
flush() {
|
|
225
|
+
if (this.rafId) {
|
|
226
|
+
cancelAnimationFrame(this.rafId);
|
|
227
|
+
this.rafId = 0;
|
|
228
|
+
}
|
|
229
|
+
for (const m of this.managed)
|
|
230
|
+
this.syncWrapper(m);
|
|
231
|
+
this.renderAll();
|
|
232
|
+
}
|
|
233
|
+
whenReady() {
|
|
234
|
+
return new Promise((res) => {
|
|
235
|
+
if (this.managed.every((m) => m.scene || !m.node.shape.glbSrc))
|
|
236
|
+
res();
|
|
237
|
+
else
|
|
238
|
+
this.readyResolvers.push(res);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
destroy() {
|
|
242
|
+
for (const d of this.dispose)
|
|
243
|
+
d();
|
|
244
|
+
this.dispose = [];
|
|
245
|
+
this.space.remove();
|
|
246
|
+
this.managed = [];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { applyTextureCrop, buildThreeScene, createSceneCamera, getCachedImageTexture, loadImageTexture, needs3DRender, resolveScene3D, shape3DInfo, shape3DPad, shape3DPathD, shapeSupports3D, } from "@sliiu/present-three-bake";
|
|
3
|
+
import { accumulatedAffine, chainHidden, chainOpacity, cssAffineForBox } from "./node-affine.js";
|
|
4
|
+
import { shapeFrontImageSrc } from "./shape-front.js";
|
|
5
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
6
|
+
// Live WebGL 3D backend —— player / editor 的 hybrid 视图里,每个需要 3D 的
|
|
7
|
+
// image / 矩形 / 椭圆 / 路径 / 多边形用一块定位 canvas 实时渲染 three.js 3D
|
|
8
|
+
// (透视 / 倒角 / 光照 / 材质)。好处:
|
|
9
|
+
// - 缩放清晰(按显示像素重渲,不再是固定分辨率位图);
|
|
10
|
+
// - 动画(entrance/emphasis 的 tx/ty/scale/opacity)过程中 3D 形体保持立体、光照正确。
|
|
11
|
+
//
|
|
12
|
+
// 设计:
|
|
13
|
+
// - 一个 "slide-space" 容器(slide 像素尺寸)用 transform:scale(k) 适配 layer 实际像素,
|
|
14
|
+
// ResizeObserver 跟随更新;每个 shape 的 wrapper 用 slide 单位定位。
|
|
15
|
+
// - 共享一个 WebGLRenderer(单 GL context),逐 shape 渲染到其 backing 尺寸再 drawImage
|
|
16
|
+
// 到该 shape 的 2D 展示 canvas,避免 per-shape GL context 累积。
|
|
17
|
+
// - 几何/材质/相机全部复用 @sliiu/present-three-bake 的 buildThreeScene / createSceneCamera,
|
|
18
|
+
// 与离屏光栅(缩略图/截图)完全一致。
|
|
19
|
+
// - 每帧 2D 变换(tx/ty/rotation/scale/opacity)经 node.on() 同步到 wrapper CSS,
|
|
20
|
+
// 不重渲 GL(3D 相机不随动画变);仅 resize / 贴图加载完成时重渲 GL。
|
|
21
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
22
|
+
// ── 共享 WebGLRenderer(单 GL context,跨 backend 实例复用)──────────────────────
|
|
23
|
+
// 编辑器拖动时 drawSlide 每帧重建整个 backend(含 WebGLShapeBackend)。若每个实例都
|
|
24
|
+
// 自建/销毁一个 WebGLRenderer,会以 ~60fps 频繁创建 + forceContextLoss GL context:
|
|
25
|
+
// 浏览器对活动 context 数量有上限、create/loss 自身也有开销,会引发掉帧甚至闪烁。
|
|
26
|
+
// 这里改用一个 module 级单例 renderer,所有 WebGLShapeBackend 复用它(逐 shape
|
|
27
|
+
// setSize→render→drawImage 是同步串行的,复用安全)。backend.destroy() 不再销毁它。
|
|
28
|
+
let sharedRenderer = null;
|
|
29
|
+
let sharedRendererFailed = false;
|
|
30
|
+
function getSharedRenderer() {
|
|
31
|
+
if (sharedRenderer)
|
|
32
|
+
return sharedRenderer;
|
|
33
|
+
if (sharedRendererFailed)
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
const r = new THREE.WebGLRenderer({
|
|
37
|
+
alpha: true,
|
|
38
|
+
antialias: true,
|
|
39
|
+
premultipliedAlpha: true,
|
|
40
|
+
preserveDrawingBuffer: true,
|
|
41
|
+
});
|
|
42
|
+
r.setPixelRatio(1);
|
|
43
|
+
r.setClearColor(0x000000, 0);
|
|
44
|
+
r.outputColorSpace = THREE.SRGBColorSpace;
|
|
45
|
+
sharedRenderer = r;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
sharedRendererFailed = true;
|
|
49
|
+
sharedRenderer = null;
|
|
50
|
+
}
|
|
51
|
+
return sharedRenderer;
|
|
52
|
+
}
|
|
53
|
+
function paddedRect(bbox, pad) {
|
|
54
|
+
const padW = Math.ceil(bbox.w * (1 + pad));
|
|
55
|
+
const padH = Math.ceil(bbox.h * (1 + pad));
|
|
56
|
+
return {
|
|
57
|
+
x: bbox.x - (padW - bbox.w) / 2,
|
|
58
|
+
y: bbox.y - (padH - bbox.h) / 2,
|
|
59
|
+
w: padW,
|
|
60
|
+
h: padH,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export class WebGLShapeBackend {
|
|
64
|
+
scene;
|
|
65
|
+
space;
|
|
66
|
+
managed = [];
|
|
67
|
+
dispose = [];
|
|
68
|
+
dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
|
|
69
|
+
k = 1;
|
|
70
|
+
rafId = 0;
|
|
71
|
+
ro = null;
|
|
72
|
+
readyResolvers = [];
|
|
73
|
+
constructor(scene, container, opts = {}) {
|
|
74
|
+
this.scene = scene;
|
|
75
|
+
const filter = opts.shapeFilter ?? (() => true);
|
|
76
|
+
this.space = document.createElement("div");
|
|
77
|
+
this.space.style.position = "absolute";
|
|
78
|
+
this.space.style.left = "0";
|
|
79
|
+
this.space.style.top = "0";
|
|
80
|
+
this.space.style.width = `${scene.width}px`;
|
|
81
|
+
this.space.style.height = `${scene.height}px`;
|
|
82
|
+
this.space.style.transformOrigin = "0 0";
|
|
83
|
+
this.space.style.pointerEvents = "none";
|
|
84
|
+
container.appendChild(this.space);
|
|
85
|
+
scene.root.walk((n) => {
|
|
86
|
+
const s = n.shape;
|
|
87
|
+
if (!shapeSupports3D(s))
|
|
88
|
+
return;
|
|
89
|
+
if (!filter(s))
|
|
90
|
+
return;
|
|
91
|
+
if (!needs3DRender(shape3DInfo(s)))
|
|
92
|
+
return;
|
|
93
|
+
this.add(n);
|
|
94
|
+
});
|
|
95
|
+
// 适配 layer 实际像素:k = 实际宽 / slide 宽。
|
|
96
|
+
const updateScale = () => {
|
|
97
|
+
const w = container.clientWidth || scene.width;
|
|
98
|
+
const k = w / scene.width;
|
|
99
|
+
if (!Number.isFinite(k) || k <= 0)
|
|
100
|
+
return;
|
|
101
|
+
if (Math.abs(k - this.k) < 1e-4)
|
|
102
|
+
return;
|
|
103
|
+
this.k = k;
|
|
104
|
+
this.space.style.transform = `scale(${k})`;
|
|
105
|
+
this.scheduleGlRender();
|
|
106
|
+
};
|
|
107
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
108
|
+
this.ro = new ResizeObserver(updateScale);
|
|
109
|
+
this.ro.observe(container);
|
|
110
|
+
}
|
|
111
|
+
updateScale();
|
|
112
|
+
this.scheduleSync();
|
|
113
|
+
this.scheduleGlRender();
|
|
114
|
+
}
|
|
115
|
+
add(node) {
|
|
116
|
+
const s = node.shape;
|
|
117
|
+
const meta = shape3DInfo(s);
|
|
118
|
+
if (!meta)
|
|
119
|
+
return;
|
|
120
|
+
const pad = shape3DPad(s.kind);
|
|
121
|
+
const baseBBox = { x: s.bbox.x, y: s.bbox.y, w: s.bbox.w, h: s.bbox.h };
|
|
122
|
+
const paddedBBox = paddedRect(baseBBox, pad);
|
|
123
|
+
const resolved = resolveScene3D(meta);
|
|
124
|
+
const wrapper = document.createElement("div");
|
|
125
|
+
wrapper.style.position = "absolute";
|
|
126
|
+
wrapper.style.left = `${paddedBBox.x}px`;
|
|
127
|
+
wrapper.style.top = `${paddedBBox.y}px`;
|
|
128
|
+
wrapper.style.width = `${paddedBBox.w}px`;
|
|
129
|
+
wrapper.style.height = `${paddedBBox.h}px`;
|
|
130
|
+
wrapper.style.pointerEvents = "none";
|
|
131
|
+
const canvas = document.createElement("canvas");
|
|
132
|
+
canvas.style.width = "100%";
|
|
133
|
+
canvas.style.height = "100%";
|
|
134
|
+
canvas.style.display = "block";
|
|
135
|
+
wrapper.appendChild(canvas);
|
|
136
|
+
this.space.appendChild(wrapper);
|
|
137
|
+
const m = {
|
|
138
|
+
node,
|
|
139
|
+
baseBBox,
|
|
140
|
+
paddedBBox,
|
|
141
|
+
pad,
|
|
142
|
+
resolved,
|
|
143
|
+
wrapper,
|
|
144
|
+
canvas,
|
|
145
|
+
ctx: canvas.getContext("2d"),
|
|
146
|
+
built: null,
|
|
147
|
+
camera: null,
|
|
148
|
+
texture: null,
|
|
149
|
+
lastBackingW: 0,
|
|
150
|
+
lastBackingH: 0,
|
|
151
|
+
disposed: false,
|
|
152
|
+
};
|
|
153
|
+
this.managed.push(m);
|
|
154
|
+
const src = s.kind === "image" ? s.src : s.kind === "text" ? shapeFrontImageSrc(this.scene, s) : undefined;
|
|
155
|
+
// 贴图此前已解码过(典型:初次加载已经走过一遍)→ 同步构建场景并 built 就绪,
|
|
156
|
+
// 这样紧随其后的 flush() 能当帧首渲,drawSlide 双缓冲不会出现 3D 空一帧的闪屏。
|
|
157
|
+
const cachedTex = src ? getCachedImageTexture(src) : null;
|
|
158
|
+
if (src ? cachedTex !== null : true) {
|
|
159
|
+
this.buildManaged(m, s, cachedTex);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// 首次(未缓存)走异步解码;解码完成后会缓存,后续重建即走上面的同步路径。
|
|
163
|
+
void (async () => {
|
|
164
|
+
const texture = src ? await loadImageTexture(src) : null;
|
|
165
|
+
if (m.disposed) {
|
|
166
|
+
texture?.dispose();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.buildManaged(m, s, texture);
|
|
170
|
+
})();
|
|
171
|
+
}
|
|
172
|
+
let cur = node;
|
|
173
|
+
while (cur) {
|
|
174
|
+
const off = cur.on(() => this.scheduleSync());
|
|
175
|
+
this.dispose.push(off);
|
|
176
|
+
cur = cur.parent;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** 用(可能为空的)贴图构建 three 场景 + 相机,写回 managed 并触发首渲。 */
|
|
180
|
+
buildManaged(m, s, texture) {
|
|
181
|
+
if (m.disposed) {
|
|
182
|
+
texture?.dispose();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const isPicture = s.kind === "image" || (s.kind === "text" && !!texture);
|
|
186
|
+
if (s.kind === "image" && texture)
|
|
187
|
+
applyTextureCrop(texture, s.cropRect);
|
|
188
|
+
m.texture = texture;
|
|
189
|
+
m.built = buildThreeScene({
|
|
190
|
+
baseW: Math.max(8, m.baseBBox.w),
|
|
191
|
+
baseH: Math.max(8, m.baseBBox.h),
|
|
192
|
+
pathD: isPicture ? undefined : shape3DPathD(s),
|
|
193
|
+
fill: s.fill,
|
|
194
|
+
stroke: s.stroke,
|
|
195
|
+
resolved: m.resolved,
|
|
196
|
+
texture: isPicture ? texture : null,
|
|
197
|
+
isImage: isPicture,
|
|
198
|
+
});
|
|
199
|
+
m.camera = createSceneCamera(m.resolved.camera, m.built.geoW, m.built.geoH, m.paddedBBox.w, m.paddedBBox.h, m.pad, m.resolved.backdrop);
|
|
200
|
+
this.scheduleGlRender();
|
|
201
|
+
this.checkReady();
|
|
202
|
+
}
|
|
203
|
+
getRenderer() {
|
|
204
|
+
return getSharedRenderer();
|
|
205
|
+
}
|
|
206
|
+
// ── 每帧 2D 变换同步(CSS,不重渲 GL)─────────────────────────────────────────
|
|
207
|
+
scheduleSync() {
|
|
208
|
+
if (this.rafId)
|
|
209
|
+
return;
|
|
210
|
+
this.rafId = requestAnimationFrame(() => {
|
|
211
|
+
this.rafId = 0;
|
|
212
|
+
this.syncTransforms();
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
syncTransforms() {
|
|
216
|
+
for (const m of this.managed)
|
|
217
|
+
this.syncOne(m);
|
|
218
|
+
}
|
|
219
|
+
syncOne(m) {
|
|
220
|
+
const node = m.node;
|
|
221
|
+
const visible = !chainHidden(node);
|
|
222
|
+
m.wrapper.style.display = visible ? "block" : "none";
|
|
223
|
+
if (!visible)
|
|
224
|
+
return;
|
|
225
|
+
// wrapper 放在 paddedBBox。CSS 原点取左上角,矩阵里已经包含祖先组合的旋转。
|
|
226
|
+
const css = cssAffineForBox(accumulatedAffine(node), m.paddedBBox.x, m.paddedBBox.y);
|
|
227
|
+
m.wrapper.style.transformOrigin = "0 0";
|
|
228
|
+
m.wrapper.style.transform = `matrix(${css.a}, ${css.b}, ${css.c}, ${css.d}, ${css.e}, ${css.f})`;
|
|
229
|
+
let alpha = chainOpacity(node);
|
|
230
|
+
const clip = node.clip;
|
|
231
|
+
if (clip && clip.kind === "effect" && clip.filter === "fade") {
|
|
232
|
+
const tr = clip.transition ?? "none";
|
|
233
|
+
alpha *= tr === "none" ? 1 : Math.max(0, Math.min(1, clip.progress ?? 0));
|
|
234
|
+
}
|
|
235
|
+
m.wrapper.style.opacity = String(alpha);
|
|
236
|
+
}
|
|
237
|
+
// ── GL 重渲(仅 resize / 贴图加载完成时)──────────────────────────────────────
|
|
238
|
+
scheduleGlRender() {
|
|
239
|
+
if (typeof requestAnimationFrame === "undefined") {
|
|
240
|
+
this.glRenderAll();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
requestAnimationFrame(() => this.glRenderAll());
|
|
244
|
+
}
|
|
245
|
+
glRenderAll() {
|
|
246
|
+
const renderer = this.getRenderer();
|
|
247
|
+
if (!renderer)
|
|
248
|
+
return;
|
|
249
|
+
for (const m of this.managed)
|
|
250
|
+
this.glRenderOne(renderer, m);
|
|
251
|
+
}
|
|
252
|
+
glRenderOne(renderer, m) {
|
|
253
|
+
if (m.disposed || !m.built || !m.camera || !m.ctx)
|
|
254
|
+
return;
|
|
255
|
+
const backingW = Math.max(1, Math.ceil(m.paddedBBox.w * this.k * this.dpr));
|
|
256
|
+
const backingH = Math.max(1, Math.ceil(m.paddedBBox.h * this.k * this.dpr));
|
|
257
|
+
if (backingW === m.lastBackingW && backingH === m.lastBackingH && m.canvas.width === backingW) {
|
|
258
|
+
// 尺寸没变也重渲一次(贴图刚加载完会走到这里);但避免无意义的频繁循环。
|
|
259
|
+
}
|
|
260
|
+
renderer.setSize(backingW, backingH, false);
|
|
261
|
+
renderer.render(m.built.scene, m.camera);
|
|
262
|
+
if (m.canvas.width !== backingW)
|
|
263
|
+
m.canvas.width = backingW;
|
|
264
|
+
if (m.canvas.height !== backingH)
|
|
265
|
+
m.canvas.height = backingH;
|
|
266
|
+
m.ctx.clearRect(0, 0, backingW, backingH);
|
|
267
|
+
try {
|
|
268
|
+
m.ctx.drawImage(renderer.domElement, 0, 0, backingW, backingH);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// drawImage 偶发失败(context lost)忽略,下次 resize 再试。
|
|
272
|
+
}
|
|
273
|
+
m.lastBackingW = backingW;
|
|
274
|
+
m.lastBackingH = backingH;
|
|
275
|
+
}
|
|
276
|
+
flush() {
|
|
277
|
+
if (this.rafId) {
|
|
278
|
+
cancelAnimationFrame(this.rafId);
|
|
279
|
+
this.rafId = 0;
|
|
280
|
+
}
|
|
281
|
+
this.syncTransforms();
|
|
282
|
+
this.glRenderAll();
|
|
283
|
+
}
|
|
284
|
+
/** 所有 3D shape 的贴图都加载完、场景已构建并首渲后 resolve(截图前等待用)。 */
|
|
285
|
+
whenReady() {
|
|
286
|
+
if (this.isReady()) {
|
|
287
|
+
this.flush();
|
|
288
|
+
return Promise.resolve();
|
|
289
|
+
}
|
|
290
|
+
return new Promise((res) => this.readyResolvers.push(res));
|
|
291
|
+
}
|
|
292
|
+
isReady() {
|
|
293
|
+
return this.managed.every((m) => m.built || m.disposed);
|
|
294
|
+
}
|
|
295
|
+
checkReady() {
|
|
296
|
+
if (!this.isReady())
|
|
297
|
+
return;
|
|
298
|
+
this.flush();
|
|
299
|
+
const rs = this.readyResolvers;
|
|
300
|
+
this.readyResolvers = [];
|
|
301
|
+
for (const r of rs)
|
|
302
|
+
r();
|
|
303
|
+
}
|
|
304
|
+
destroy() {
|
|
305
|
+
if (this.rafId)
|
|
306
|
+
cancelAnimationFrame(this.rafId);
|
|
307
|
+
this.rafId = 0;
|
|
308
|
+
for (const off of this.dispose)
|
|
309
|
+
off();
|
|
310
|
+
this.dispose = [];
|
|
311
|
+
this.ro?.disconnect();
|
|
312
|
+
this.ro = null;
|
|
313
|
+
for (const m of this.managed) {
|
|
314
|
+
m.disposed = true;
|
|
315
|
+
m.built?.dispose();
|
|
316
|
+
m.texture?.dispose();
|
|
317
|
+
}
|
|
318
|
+
this.managed = [];
|
|
319
|
+
// 不销毁 sharedRenderer:它是跨 backend 实例复用的单例 GL context(见顶部注释)。
|
|
320
|
+
// 编辑器拖动每帧重建 backend,这里若 forceContextLoss 会造成 context 抖动 / 闪屏。
|
|
321
|
+
this.space.remove();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ArrowKind } from "@sliiu/present-core";
|
|
2
|
+
export declare function arrowSize(spec: {
|
|
3
|
+
w?: "sm" | "med" | "lg";
|
|
4
|
+
len?: "sm" | "med" | "lg";
|
|
5
|
+
}): {
|
|
6
|
+
w: number;
|
|
7
|
+
l: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function arrowPathD(kind: ArrowKind, w: number, l: number, side: "start" | "end"): string;
|
|
10
|
+
export declare function arrowRefX(kind: ArrowKind, side: "start" | "end", l: number): number;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Shape } from "@sliiu/present-core";
|
|
2
|
+
import { type Scene } from "./scene-node.js";
|
|
3
|
+
export interface CanvasBackendOptions {
|
|
4
|
+
shapeFilter?: (s: Shape) => boolean;
|
|
5
|
+
transparent?: boolean;
|
|
6
|
+
/** Offscreen export: override `devicePixelRatio` (e.g. PDF at 150 DPI → 150/96). */
|
|
7
|
+
pixelRatio?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class CanvasBackend {
|
|
10
|
+
readonly canvas: HTMLCanvasElement;
|
|
11
|
+
private ctx;
|
|
12
|
+
private scene;
|
|
13
|
+
private dpr;
|
|
14
|
+
private rafId;
|
|
15
|
+
private dispose;
|
|
16
|
+
private shapeFilter;
|
|
17
|
+
private transparent;
|
|
18
|
+
/** 给文字 3D 正面贴图用:画字形,不把已经烘好的 PNG 再画一遍。 */
|
|
19
|
+
private drawingFront;
|
|
20
|
+
private arrowProbe?;
|
|
21
|
+
constructor(scene: Scene, container: HTMLElement, opts?: CanvasBackendOptions);
|
|
22
|
+
resize(w: number, h: number): void;
|
|
23
|
+
private scheduleRedraw;
|
|
24
|
+
/** 同步刷新到 canvas,并取消 pending rAF。详见 SvgBackend.flush。 */
|
|
25
|
+
flush(): void;
|
|
26
|
+
private redraw;
|
|
27
|
+
private drawNode;
|
|
28
|
+
private applyClip;
|
|
29
|
+
private drawShape;
|
|
30
|
+
/** Canvas 导出 / 缩略图:audio 无 DOM;video 无 poster 且第一帧未就绪时画占位图标。 */
|
|
31
|
+
private drawMediaPlaceholder;
|
|
32
|
+
private drawArrowheads;
|
|
33
|
+
private drawOneArrow;
|
|
34
|
+
private measurePathEnds;
|
|
35
|
+
private drawText;
|
|
36
|
+
/**
|
|
37
|
+
* 单行艺术字的墨迹框。多段、项目符号、竖排不在这里估,那些仍走 SVG 的 getBBox。
|
|
38
|
+
* 估算必须和 drawTextHorizontal 的顶对齐 / 居中公式一致,否则拉伸会把字拽偏。
|
|
39
|
+
*/
|
|
40
|
+
private measureCanvasWordArt;
|
|
41
|
+
private drawTextWarped;
|
|
42
|
+
private drawTextHorizontal;
|
|
43
|
+
private drawWrappedParagraphCanvas;
|
|
44
|
+
private drawTextStacked;
|
|
45
|
+
private runFillStyle;
|
|
46
|
+
private fillTextWithOutline;
|
|
47
|
+
private drawBullet;
|
|
48
|
+
private drawRunsWithCharOverrides;
|
|
49
|
+
private roundRect;
|
|
50
|
+
/** 按未旋转的 bbox 把这个 shape 画到一张独立画布,左上角对齐 bbox 原点。 */
|
|
51
|
+
renderShapeFront(shapeId: string): HTMLCanvasElement | null;
|
|
52
|
+
destroy(): void;
|
|
53
|
+
}
|
|
54
|
+
/** 离屏把一个 shape 的正面画成画布。调用方负责不再使用返回的 canvas 之后的生命周期。 */
|
|
55
|
+
export declare function rasterizeShapeFront(scene: Scene, shapeId: string): HTMLCanvasElement | null;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Rect, Stroke, TableCellBorders, TextShape } from "@sliiu/present-core";
|
|
2
|
+
export declare function cellHasPerSideBorders(s: TextShape): boolean;
|
|
3
|
+
export declare function cellBorderSegments(bbox: Rect, borders: TableCellBorders): Array<{
|
|
4
|
+
x1: number;
|
|
5
|
+
y1: number;
|
|
6
|
+
x2: number;
|
|
7
|
+
y2: number;
|
|
8
|
+
stroke: Stroke;
|
|
9
|
+
}>;
|