fabricjs-document-engine 1.2.0 → 1.3.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/README.md +31 -0
- package/README.zh-CN.md +32 -0
- package/dist/commands/text-edit.cjs +223 -0
- package/dist/commands/text-edit.d.cts +23 -0
- package/dist/commands/text-edit.d.ts +23 -0
- package/dist/commands/text-edit.js +218 -0
- package/dist/fabric/object-registry.cjs +7 -1
- package/dist/fabric/object-registry.js +7 -1
- package/dist/filters/filter-worker.cjs +281 -0
- package/dist/filters/filter-worker.d.cts +26 -0
- package/dist/filters/filter-worker.d.ts +26 -0
- package/dist/filters/filter-worker.js +281 -0
- package/dist/filters/pipeline.cjs +91 -0
- package/dist/filters/pipeline.d.cts +35 -0
- package/dist/filters/pipeline.d.ts +35 -0
- package/dist/filters/pipeline.js +87 -0
- package/dist/filters/worker-entry.cjs +78 -0
- package/dist/filters/worker-entry.d.cts +29 -0
- package/dist/filters/worker-entry.d.ts +29 -0
- package/dist/filters/worker-entry.js +78 -0
- package/dist/filters.cjs +9 -0
- package/dist/filters.d.cts +3 -0
- package/dist/filters.d.ts +3 -0
- package/dist/filters.js +3 -0
- package/dist/import/svg-import.cjs +102 -0
- package/dist/import/svg-import.d.cts +1 -0
- package/dist/import/svg-import.d.ts +1 -0
- package/dist/import/svg-import.js +103 -1
- package/dist/index.cjs +7 -0
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/performance/batch.cjs +31 -0
- package/dist/performance/batch.d.cts +4 -0
- package/dist/performance/batch.d.ts +4 -0
- package/dist/performance/batch.js +31 -0
- package/dist/performance/dirty-regions.cjs +378 -0
- package/dist/performance/dirty-regions.d.cts +23 -0
- package/dist/performance/dirty-regions.d.ts +23 -0
- package/dist/performance/dirty-regions.js +378 -0
- package/dist/performance/monitor.cjs +60 -0
- package/dist/performance/monitor.d.cts +20 -0
- package/dist/performance/monitor.d.ts +20 -0
- package/dist/performance/monitor.js +60 -0
- package/dist/performance/spatial-index.cjs +78 -0
- package/dist/performance/spatial-index.d.cts +21 -0
- package/dist/performance/spatial-index.d.ts +21 -0
- package/dist/performance/spatial-index.js +78 -0
- package/dist/performance.cjs +9 -0
- package/dist/performance.d.cts +5 -0
- package/dist/performance.d.ts +5 -0
- package/dist/performance.js +5 -0
- package/dist/text/bounded-textbox.cjs +306 -0
- package/dist/text/bounded-textbox.d.cts +53 -0
- package/dist/text/bounded-textbox.d.ts +53 -0
- package/dist/text/bounded-textbox.js +306 -0
- package/dist/text/mobile-input.cjs +146 -0
- package/dist/text/mobile-input.d.cts +19 -0
- package/dist/text/mobile-input.d.ts +19 -0
- package/dist/text/mobile-input.js +145 -0
- package/dist/text/register.cjs +17 -0
- package/dist/text/register.d.cts +5 -0
- package/dist/text/register.d.ts +5 -0
- package/dist/text/register.js +16 -0
- package/dist/text/shaping.cjs +125 -0
- package/dist/text/shaping.d.cts +28 -0
- package/dist/text/shaping.d.ts +28 -0
- package/dist/text/shaping.js +121 -0
- package/dist/text/vertical-orientation.cjs +192 -0
- package/dist/text/vertical-orientation.d.cts +10 -0
- package/dist/text/vertical-orientation.d.ts +10 -0
- package/dist/text/vertical-orientation.js +190 -0
- package/dist/text/vertical-text.cjs +336 -0
- package/dist/text/vertical-text.d.cts +54 -0
- package/dist/text/vertical-text.d.ts +54 -0
- package/dist/text/vertical-text.js +336 -0
- package/dist/text.cjs +21 -0
- package/dist/text.d.cts +7 -0
- package/dist/text.d.ts +7 -0
- package/dist/text.js +7 -0
- package/package.json +31 -1
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { createSpatialIndex } from "./spatial-index.js";
|
|
2
|
+
//#region src/performance/dirty-regions.ts
|
|
3
|
+
const UNBOUNDED_COMPOSITES = /* @__PURE__ */ new Set([
|
|
4
|
+
"source-in",
|
|
5
|
+
"source-out",
|
|
6
|
+
"destination-in",
|
|
7
|
+
"destination-atop",
|
|
8
|
+
"copy"
|
|
9
|
+
]);
|
|
10
|
+
function union(first, second) {
|
|
11
|
+
const left = Math.min(first.left, second.left);
|
|
12
|
+
const top = Math.min(first.top, second.top);
|
|
13
|
+
return {
|
|
14
|
+
left,
|
|
15
|
+
top,
|
|
16
|
+
width: Math.max(first.left + first.width, second.left + second.width) - left,
|
|
17
|
+
height: Math.max(first.top + first.height, second.top + second.height) - top
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function intersects(first, second, gap = 0) {
|
|
21
|
+
return first.left <= second.left + second.width + gap && second.left <= first.left + first.width + gap && first.top <= second.top + second.height + gap && second.top <= first.top + first.height + gap;
|
|
22
|
+
}
|
|
23
|
+
const area = (rect) => Math.max(0, rect.width) * Math.max(0, rect.height);
|
|
24
|
+
function overhang(object) {
|
|
25
|
+
const self = object;
|
|
26
|
+
let x = 0;
|
|
27
|
+
let y = 0;
|
|
28
|
+
if (self.stroke && self.strokeWidth) x = y = self.strokeWidth * Math.max(1, self.strokeMiterLimit ?? 4) / 2;
|
|
29
|
+
if (typeof self.fontSize === "number" && typeof self.text === "string") {
|
|
30
|
+
x = Math.max(x, self.fontSize * .5);
|
|
31
|
+
y = Math.max(y, self.fontSize * .5);
|
|
32
|
+
}
|
|
33
|
+
for (const child of self._objects ?? []) {
|
|
34
|
+
const inner = overhang(child);
|
|
35
|
+
const childShadow = shadowReach(child);
|
|
36
|
+
x = Math.max(x, inner.x + childShadow.x);
|
|
37
|
+
y = Math.max(y, inner.y + childShadow.y);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
x,
|
|
41
|
+
y
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function shadowReach(object) {
|
|
45
|
+
const shadow = object.shadow;
|
|
46
|
+
if (!shadow) return {
|
|
47
|
+
x: 0,
|
|
48
|
+
y: 0
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
x: Math.abs(shadow.offsetX) + shadow.blur * 2,
|
|
52
|
+
y: Math.abs(shadow.offsetY) + shadow.blur * 2
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function drawnBounds(object) {
|
|
56
|
+
object.setCoords();
|
|
57
|
+
const box = object.getBoundingRect();
|
|
58
|
+
const scaling = object.getObjectScaling();
|
|
59
|
+
const scaleX = Math.abs(scaling.x);
|
|
60
|
+
const scaleY = Math.abs(scaling.y);
|
|
61
|
+
const reach = overhang(object);
|
|
62
|
+
const shadow = shadowReach(object);
|
|
63
|
+
const nonScaling = Boolean(object.shadow?.nonScaling);
|
|
64
|
+
const grow = Math.max(reach.x * scaleX, reach.y * scaleY) + Math.max(shadow.x, shadow.y) * (nonScaling ? 1 : Math.max(scaleX, scaleY));
|
|
65
|
+
let bounds = {
|
|
66
|
+
left: box.left - grow,
|
|
67
|
+
top: box.top - grow,
|
|
68
|
+
width: box.width + grow * 2,
|
|
69
|
+
height: box.height + grow * 2
|
|
70
|
+
};
|
|
71
|
+
for (const child of object._objects ?? []) bounds = union(bounds, drawnBounds(child));
|
|
72
|
+
return bounds;
|
|
73
|
+
}
|
|
74
|
+
function fingerprint(object, index, into = []) {
|
|
75
|
+
const self = object;
|
|
76
|
+
const matrix = object.calcTransformMatrix();
|
|
77
|
+
const shadow = self.shadow;
|
|
78
|
+
into.push(index, matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5], self.width, self.height, self.visible, self.opacity, self.strokeWidth, self.globalCompositeOperation, self._element, self.clipPath, shadow, shadow?.blur, shadow?.offsetX, shadow?.offsetY);
|
|
79
|
+
self._objects?.forEach((child, position) => fingerprint(child, position, into));
|
|
80
|
+
return into;
|
|
81
|
+
}
|
|
82
|
+
function isDirty(object) {
|
|
83
|
+
const self = object;
|
|
84
|
+
return Boolean(self.dirty) || (self._objects ?? []).some(isDirty);
|
|
85
|
+
}
|
|
86
|
+
function sameFingerprint(first, second) {
|
|
87
|
+
if (first.length !== second.length) return false;
|
|
88
|
+
for (let index = 0; index < first.length; index += 1) if (first[index] !== second[index]) return false;
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
function toViewport(rect, matrix) {
|
|
92
|
+
const [a, b, c, d, e, f] = matrix;
|
|
93
|
+
const xs = [];
|
|
94
|
+
const ys = [];
|
|
95
|
+
for (const [x, y] of [
|
|
96
|
+
[rect.left, rect.top],
|
|
97
|
+
[rect.left + rect.width, rect.top],
|
|
98
|
+
[rect.left, rect.top + rect.height],
|
|
99
|
+
[rect.left + rect.width, rect.top + rect.height]
|
|
100
|
+
]) {
|
|
101
|
+
xs.push(a * x + c * y + e);
|
|
102
|
+
ys.push(b * x + d * y + f);
|
|
103
|
+
}
|
|
104
|
+
const left = Math.min(...xs);
|
|
105
|
+
const top = Math.min(...ys);
|
|
106
|
+
return {
|
|
107
|
+
left,
|
|
108
|
+
top,
|
|
109
|
+
width: Math.max(...xs) - left,
|
|
110
|
+
height: Math.max(...ys) - top
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function toScene(rect, matrix) {
|
|
114
|
+
const [a, b, c, d, e, f] = matrix;
|
|
115
|
+
const determinant = a * d - b * c;
|
|
116
|
+
return toViewport(rect, [
|
|
117
|
+
d / determinant,
|
|
118
|
+
-b / determinant,
|
|
119
|
+
-c / determinant,
|
|
120
|
+
a / determinant,
|
|
121
|
+
(c * f - d * e) / determinant,
|
|
122
|
+
(b * e - a * f) / determinant
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
function controlsArea(canvas) {
|
|
126
|
+
const active = canvas.getActiveObject?.();
|
|
127
|
+
if (!active) return {
|
|
128
|
+
key: [],
|
|
129
|
+
rect: void 0
|
|
130
|
+
};
|
|
131
|
+
const self = active;
|
|
132
|
+
const points = Object.values(self.oCoords ?? {});
|
|
133
|
+
if (points.length === 0 || !self.hasControls && !self.hasBorders) return {
|
|
134
|
+
key: [active],
|
|
135
|
+
rect: void 0
|
|
136
|
+
};
|
|
137
|
+
const reach = Math.max(self.cornerSize ?? 13, self.touchCornerSize ?? 0) / 2 + (self.padding ?? 0) + 4;
|
|
138
|
+
let left = Infinity;
|
|
139
|
+
let top = Infinity;
|
|
140
|
+
let right = -Infinity;
|
|
141
|
+
let bottom = -Infinity;
|
|
142
|
+
const key = [
|
|
143
|
+
active,
|
|
144
|
+
self.hasControls,
|
|
145
|
+
self.hasBorders
|
|
146
|
+
];
|
|
147
|
+
for (const point of points) {
|
|
148
|
+
left = Math.min(left, point.x);
|
|
149
|
+
top = Math.min(top, point.y);
|
|
150
|
+
right = Math.max(right, point.x);
|
|
151
|
+
bottom = Math.max(bottom, point.y);
|
|
152
|
+
key.push(point.x, point.y);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
key,
|
|
156
|
+
rect: {
|
|
157
|
+
left: left - reach,
|
|
158
|
+
top: top - reach,
|
|
159
|
+
width: right - left + reach * 2,
|
|
160
|
+
height: bottom - top + reach * 2
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function mergeRects(rects, limit) {
|
|
165
|
+
if (rects.length > 64) return [rects.reduce(union)];
|
|
166
|
+
const merged = [...rects];
|
|
167
|
+
let changed = true;
|
|
168
|
+
while (changed) {
|
|
169
|
+
changed = false;
|
|
170
|
+
for (let first = 0; first < merged.length && !changed; first += 1) for (let second = first + 1; second < merged.length; second += 1) if (intersects(merged[first], merged[second], 8)) {
|
|
171
|
+
merged[first] = union(merged[first], merged[second]);
|
|
172
|
+
merged.splice(second, 1);
|
|
173
|
+
changed = true;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
while (merged.length > limit) {
|
|
178
|
+
let best = [0, 1];
|
|
179
|
+
let bestGrowth = Infinity;
|
|
180
|
+
for (let first = 0; first < merged.length; first += 1) for (let second = first + 1; second < merged.length; second += 1) {
|
|
181
|
+
const growth = area(union(merged[first], merged[second])) - area(merged[first]) - area(merged[second]);
|
|
182
|
+
if (growth < bestGrowth) {
|
|
183
|
+
bestGrowth = growth;
|
|
184
|
+
best = [first, second];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
merged[best[0]] = union(merged[best[0]], merged[best[1]]);
|
|
188
|
+
merged.splice(best[1], 1);
|
|
189
|
+
}
|
|
190
|
+
return merged;
|
|
191
|
+
}
|
|
192
|
+
function enableDirtyRegionRendering(canvas, options = {}) {
|
|
193
|
+
const self = canvas;
|
|
194
|
+
const fullRatio = options.fullRedrawRatio ?? .4;
|
|
195
|
+
const maxRegions = Math.max(1, options.maxRegions ?? 4);
|
|
196
|
+
const index = createSpatialIndex({ cellSize: options.cellSize ?? 256 });
|
|
197
|
+
const states = /* @__PURE__ */ new Map();
|
|
198
|
+
const unbounded = /* @__PURE__ */ new Set();
|
|
199
|
+
const original = self.renderCanvas;
|
|
200
|
+
const hadOwn = Object.prototype.hasOwnProperty.call(canvas, "renderCanvas");
|
|
201
|
+
const stats = {
|
|
202
|
+
frames: 0,
|
|
203
|
+
fullFrames: 0,
|
|
204
|
+
skippedFrames: 0,
|
|
205
|
+
lastObjectsDrawn: 0,
|
|
206
|
+
lastAreaRatio: 0,
|
|
207
|
+
lastRegions: []
|
|
208
|
+
};
|
|
209
|
+
let frame = 0;
|
|
210
|
+
let forceFull = true;
|
|
211
|
+
let lastPage = [];
|
|
212
|
+
let lastControls = {
|
|
213
|
+
key: [],
|
|
214
|
+
rect: void 0
|
|
215
|
+
};
|
|
216
|
+
const pageKey = () => [
|
|
217
|
+
...self.viewportTransform,
|
|
218
|
+
self.width,
|
|
219
|
+
self.height,
|
|
220
|
+
self.getRetinaScaling(),
|
|
221
|
+
self.backgroundColor,
|
|
222
|
+
self.backgroundImage,
|
|
223
|
+
self.backgroundImage?.dirty,
|
|
224
|
+
self.overlayColor,
|
|
225
|
+
self.overlayImage,
|
|
226
|
+
self.overlayImage?.dirty,
|
|
227
|
+
self.clipPath,
|
|
228
|
+
self.clipPath?.dirty
|
|
229
|
+
];
|
|
230
|
+
function track(object, position) {
|
|
231
|
+
const print = fingerprint(object, position);
|
|
232
|
+
const state = states.get(object);
|
|
233
|
+
if (state) {
|
|
234
|
+
state.seen = frame;
|
|
235
|
+
if (!isDirty(object) && sameFingerprint(state.fingerprint, print)) return [];
|
|
236
|
+
}
|
|
237
|
+
const bounds = drawnBounds(object);
|
|
238
|
+
states.set(object, {
|
|
239
|
+
fingerprint: print,
|
|
240
|
+
bounds,
|
|
241
|
+
seen: frame
|
|
242
|
+
});
|
|
243
|
+
index.set(object, bounds);
|
|
244
|
+
const composite = object.globalCompositeOperation ?? "source-over";
|
|
245
|
+
if (UNBOUNDED_COMPOSITES.has(composite)) unbounded.add(object);
|
|
246
|
+
else unbounded.delete(object);
|
|
247
|
+
return state ? [state.bounds, bounds] : [bounds];
|
|
248
|
+
}
|
|
249
|
+
function drawFull(ctx, objects) {
|
|
250
|
+
stats.fullFrames += 1;
|
|
251
|
+
stats.lastObjectsDrawn = objects.length;
|
|
252
|
+
stats.lastAreaRatio = 1;
|
|
253
|
+
stats.lastRegions = [];
|
|
254
|
+
original.call(canvas, ctx, objects);
|
|
255
|
+
}
|
|
256
|
+
function renderCanvas(ctx, objects) {
|
|
257
|
+
if (ctx !== self.getContext()) {
|
|
258
|
+
original.call(canvas, ctx, objects);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
frame += 1;
|
|
262
|
+
stats.frames += 1;
|
|
263
|
+
const changed = [];
|
|
264
|
+
self._objects.forEach((object, position) => changed.push(...track(object, position)));
|
|
265
|
+
for (const [object, state] of states) {
|
|
266
|
+
if (state.seen === frame) continue;
|
|
267
|
+
changed.push(state.bounds);
|
|
268
|
+
states.delete(object);
|
|
269
|
+
index.delete(object);
|
|
270
|
+
unbounded.delete(object);
|
|
271
|
+
}
|
|
272
|
+
const page = pageKey();
|
|
273
|
+
const controls = controlsArea(self);
|
|
274
|
+
const pageChanged = !sameFingerprint(page, lastPage) || page.length !== lastPage.length;
|
|
275
|
+
const controlsChanged = !sameFingerprint(controls.key, lastControls.key) || controls.key.length !== lastControls.key.length;
|
|
276
|
+
const viewportChanged = [];
|
|
277
|
+
if (controlsChanged) {
|
|
278
|
+
if (lastControls.rect) viewportChanged.push(lastControls.rect);
|
|
279
|
+
if (controls.rect) viewportChanged.push(controls.rect);
|
|
280
|
+
const before = lastControls.key[0];
|
|
281
|
+
const after = controls.key[0];
|
|
282
|
+
for (const object of [before, after]) {
|
|
283
|
+
const state = object ? states.get(object) : void 0;
|
|
284
|
+
if (state) changed.push(state.bounds);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
lastPage = page;
|
|
288
|
+
lastControls = controls;
|
|
289
|
+
if (forceFull || pageChanged) {
|
|
290
|
+
forceFull = false;
|
|
291
|
+
drawFull(ctx, objects);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const matrix = self.viewportTransform;
|
|
295
|
+
const retina = self.getRetinaScaling();
|
|
296
|
+
const canvasRect = {
|
|
297
|
+
left: 0,
|
|
298
|
+
top: 0,
|
|
299
|
+
width: self.width,
|
|
300
|
+
height: self.height
|
|
301
|
+
};
|
|
302
|
+
const regions = [];
|
|
303
|
+
for (const rect of [...changed.map((scene) => toViewport(scene, matrix)), ...viewportChanged]) {
|
|
304
|
+
const pad = 2 / retina;
|
|
305
|
+
const left = Math.max(0, Math.floor((rect.left - pad) * retina) / retina);
|
|
306
|
+
const top = Math.max(0, Math.floor((rect.top - pad) * retina) / retina);
|
|
307
|
+
const right = Math.min(self.width, Math.ceil((rect.left + rect.width + pad) * retina) / retina);
|
|
308
|
+
const bottom = Math.min(self.height, Math.ceil((rect.top + rect.height + pad) * retina) / retina);
|
|
309
|
+
if (right > left && bottom > top) regions.push({
|
|
310
|
+
left,
|
|
311
|
+
top,
|
|
312
|
+
width: right - left,
|
|
313
|
+
height: bottom - top
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (regions.length === 0) {
|
|
317
|
+
stats.skippedFrames += 1;
|
|
318
|
+
stats.lastObjectsDrawn = 0;
|
|
319
|
+
stats.lastAreaRatio = 0;
|
|
320
|
+
stats.lastRegions = [];
|
|
321
|
+
self.fire("before:render", { ctx });
|
|
322
|
+
self.fire("after:render", { ctx });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const merged = mergeRects(regions, maxRegions);
|
|
326
|
+
const drawnArea = merged.reduce((sum, rect) => sum + area(rect), 0);
|
|
327
|
+
if (drawnArea > area(canvasRect) * fullRatio) {
|
|
328
|
+
drawFull(ctx, objects);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const wanted = new Set(unbounded);
|
|
332
|
+
for (const rect of merged) {
|
|
333
|
+
const pad = 2 / retina;
|
|
334
|
+
const scene = toScene({
|
|
335
|
+
left: rect.left - pad,
|
|
336
|
+
top: rect.top - pad,
|
|
337
|
+
width: rect.width + pad * 2,
|
|
338
|
+
height: rect.height + pad * 2
|
|
339
|
+
}, matrix);
|
|
340
|
+
index.query(scene).forEach((object) => {
|
|
341
|
+
const bounds = states.get(object)?.bounds;
|
|
342
|
+
if (!bounds || intersects(bounds, scene)) wanted.add(object);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
const toDraw = objects.filter((object) => wanted.has(object));
|
|
346
|
+
stats.lastObjectsDrawn = toDraw.length;
|
|
347
|
+
stats.lastAreaRatio = drawnArea / area(canvasRect);
|
|
348
|
+
stats.lastRegions = merged;
|
|
349
|
+
ctx.save();
|
|
350
|
+
ctx.beginPath();
|
|
351
|
+
for (const rect of merged) ctx.rect(rect.left, rect.top, rect.width, rect.height);
|
|
352
|
+
ctx.clip();
|
|
353
|
+
try {
|
|
354
|
+
original.call(canvas, ctx, toDraw);
|
|
355
|
+
} finally {
|
|
356
|
+
ctx.restore();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
self.renderCanvas = renderCanvas;
|
|
360
|
+
return {
|
|
361
|
+
invalidate() {
|
|
362
|
+
forceFull = true;
|
|
363
|
+
},
|
|
364
|
+
stats: () => ({
|
|
365
|
+
...stats,
|
|
366
|
+
lastRegions: [...stats.lastRegions]
|
|
367
|
+
}),
|
|
368
|
+
disable() {
|
|
369
|
+
if (self.renderCanvas !== renderCanvas) return;
|
|
370
|
+
if (hadOwn) self.renderCanvas = original;
|
|
371
|
+
else delete canvas.renderCanvas;
|
|
372
|
+
index.clear();
|
|
373
|
+
states.clear();
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
//#endregion
|
|
378
|
+
export { enableDirtyRegionRendering };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/performance/monitor.ts
|
|
2
|
+
function createPerformanceMonitor(canvas, options = {}) {
|
|
3
|
+
const self = canvas;
|
|
4
|
+
const capacity = Math.max(1, options.samples ?? 120);
|
|
5
|
+
const times = new Float64Array(capacity);
|
|
6
|
+
let count = 0;
|
|
7
|
+
let next = 0;
|
|
8
|
+
let started = 0;
|
|
9
|
+
let objectsDrawn = 0;
|
|
10
|
+
let lastObjectsDrawn = 0;
|
|
11
|
+
const onBefore = ({ ctx }) => {
|
|
12
|
+
if (ctx !== self.getContext()) return;
|
|
13
|
+
started = performance.now();
|
|
14
|
+
objectsDrawn = 0;
|
|
15
|
+
};
|
|
16
|
+
const onAfter = ({ ctx }) => {
|
|
17
|
+
if (ctx !== self.getContext() || started === 0) return;
|
|
18
|
+
times[next] = performance.now() - started;
|
|
19
|
+
next = (next + 1) % capacity;
|
|
20
|
+
count = Math.min(count + 1, capacity);
|
|
21
|
+
lastObjectsDrawn = objectsDrawn;
|
|
22
|
+
started = 0;
|
|
23
|
+
};
|
|
24
|
+
const original = self._renderObjects;
|
|
25
|
+
const hadOwn = Object.prototype.hasOwnProperty.call(canvas, "_renderObjects");
|
|
26
|
+
function renderObjects(ctx, objects) {
|
|
27
|
+
if (ctx === self.getContext()) objectsDrawn += objects.length;
|
|
28
|
+
original.call(this, ctx, objects);
|
|
29
|
+
}
|
|
30
|
+
self._renderObjects = renderObjects;
|
|
31
|
+
self.on("before:render", onBefore);
|
|
32
|
+
self.on("after:render", onAfter);
|
|
33
|
+
return {
|
|
34
|
+
stats() {
|
|
35
|
+
const recorded = Array.from(times.subarray(0, count)).sort((first, second) => first - second);
|
|
36
|
+
const at = (share) => count === 0 ? 0 : recorded[Math.min(count - 1, Math.floor(share * count))];
|
|
37
|
+
return {
|
|
38
|
+
frames: count,
|
|
39
|
+
averageMs: count === 0 ? 0 : recorded.reduce((sum, time) => sum + time, 0) / count,
|
|
40
|
+
p50Ms: at(.5),
|
|
41
|
+
p95Ms: at(.95),
|
|
42
|
+
worstMs: count === 0 ? 0 : recorded[count - 1],
|
|
43
|
+
lastObjectsDrawn
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
reset() {
|
|
47
|
+
count = 0;
|
|
48
|
+
next = 0;
|
|
49
|
+
},
|
|
50
|
+
stop() {
|
|
51
|
+
self.off("before:render", onBefore);
|
|
52
|
+
self.off("after:render", onAfter);
|
|
53
|
+
if (self._renderObjects !== renderObjects) return;
|
|
54
|
+
if (hadOwn) self._renderObjects = original;
|
|
55
|
+
else delete canvas._renderObjects;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
exports.createPerformanceMonitor = createPerformanceMonitor;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { StaticCanvas } from "fabric";
|
|
2
|
+
//#region src/performance/monitor.d.ts
|
|
3
|
+
export interface PerformanceMonitorOptions {
|
|
4
|
+
samples?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface FrameStats {
|
|
7
|
+
frames: number;
|
|
8
|
+
averageMs: number;
|
|
9
|
+
p50Ms: number;
|
|
10
|
+
p95Ms: number;
|
|
11
|
+
worstMs: number;
|
|
12
|
+
lastObjectsDrawn: number;
|
|
13
|
+
}
|
|
14
|
+
export interface PerformanceMonitor {
|
|
15
|
+
stats(): FrameStats;
|
|
16
|
+
reset(): void;
|
|
17
|
+
stop(): void;
|
|
18
|
+
}
|
|
19
|
+
export declare function createPerformanceMonitor(canvas: StaticCanvas, options?: PerformanceMonitorOptions): PerformanceMonitor;
|
|
20
|
+
//#endregion
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { StaticCanvas } from "fabric";
|
|
2
|
+
//#region src/performance/monitor.d.ts
|
|
3
|
+
export interface PerformanceMonitorOptions {
|
|
4
|
+
samples?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface FrameStats {
|
|
7
|
+
frames: number;
|
|
8
|
+
averageMs: number;
|
|
9
|
+
p50Ms: number;
|
|
10
|
+
p95Ms: number;
|
|
11
|
+
worstMs: number;
|
|
12
|
+
lastObjectsDrawn: number;
|
|
13
|
+
}
|
|
14
|
+
export interface PerformanceMonitor {
|
|
15
|
+
stats(): FrameStats;
|
|
16
|
+
reset(): void;
|
|
17
|
+
stop(): void;
|
|
18
|
+
}
|
|
19
|
+
export declare function createPerformanceMonitor(canvas: StaticCanvas, options?: PerformanceMonitorOptions): PerformanceMonitor;
|
|
20
|
+
//#endregion
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/performance/monitor.ts
|
|
2
|
+
function createPerformanceMonitor(canvas, options = {}) {
|
|
3
|
+
const self = canvas;
|
|
4
|
+
const capacity = Math.max(1, options.samples ?? 120);
|
|
5
|
+
const times = new Float64Array(capacity);
|
|
6
|
+
let count = 0;
|
|
7
|
+
let next = 0;
|
|
8
|
+
let started = 0;
|
|
9
|
+
let objectsDrawn = 0;
|
|
10
|
+
let lastObjectsDrawn = 0;
|
|
11
|
+
const onBefore = ({ ctx }) => {
|
|
12
|
+
if (ctx !== self.getContext()) return;
|
|
13
|
+
started = performance.now();
|
|
14
|
+
objectsDrawn = 0;
|
|
15
|
+
};
|
|
16
|
+
const onAfter = ({ ctx }) => {
|
|
17
|
+
if (ctx !== self.getContext() || started === 0) return;
|
|
18
|
+
times[next] = performance.now() - started;
|
|
19
|
+
next = (next + 1) % capacity;
|
|
20
|
+
count = Math.min(count + 1, capacity);
|
|
21
|
+
lastObjectsDrawn = objectsDrawn;
|
|
22
|
+
started = 0;
|
|
23
|
+
};
|
|
24
|
+
const original = self._renderObjects;
|
|
25
|
+
const hadOwn = Object.prototype.hasOwnProperty.call(canvas, "_renderObjects");
|
|
26
|
+
function renderObjects(ctx, objects) {
|
|
27
|
+
if (ctx === self.getContext()) objectsDrawn += objects.length;
|
|
28
|
+
original.call(this, ctx, objects);
|
|
29
|
+
}
|
|
30
|
+
self._renderObjects = renderObjects;
|
|
31
|
+
self.on("before:render", onBefore);
|
|
32
|
+
self.on("after:render", onAfter);
|
|
33
|
+
return {
|
|
34
|
+
stats() {
|
|
35
|
+
const recorded = Array.from(times.subarray(0, count)).sort((first, second) => first - second);
|
|
36
|
+
const at = (share) => count === 0 ? 0 : recorded[Math.min(count - 1, Math.floor(share * count))];
|
|
37
|
+
return {
|
|
38
|
+
frames: count,
|
|
39
|
+
averageMs: count === 0 ? 0 : recorded.reduce((sum, time) => sum + time, 0) / count,
|
|
40
|
+
p50Ms: at(.5),
|
|
41
|
+
p95Ms: at(.95),
|
|
42
|
+
worstMs: count === 0 ? 0 : recorded[count - 1],
|
|
43
|
+
lastObjectsDrawn
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
reset() {
|
|
47
|
+
count = 0;
|
|
48
|
+
next = 0;
|
|
49
|
+
},
|
|
50
|
+
stop() {
|
|
51
|
+
self.off("before:render", onBefore);
|
|
52
|
+
self.off("after:render", onAfter);
|
|
53
|
+
if (self._renderObjects !== renderObjects) return;
|
|
54
|
+
if (hadOwn) self._renderObjects = original;
|
|
55
|
+
else delete canvas._renderObjects;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { createPerformanceMonitor };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//#region src/performance/spatial-index.ts
|
|
2
|
+
const OFFSET = 1 << 20;
|
|
3
|
+
const keyOf = (column, row) => (column + OFFSET) * 2 ** 21 + (row + OFFSET);
|
|
4
|
+
function createSpatialIndex(options = {}) {
|
|
5
|
+
const cellSize = options.cellSize ?? 256;
|
|
6
|
+
const maxCells = options.maxCellsPerItem ?? 64;
|
|
7
|
+
const cells = /* @__PURE__ */ new Map();
|
|
8
|
+
const placed = /* @__PURE__ */ new Map();
|
|
9
|
+
const large = /* @__PURE__ */ new Set();
|
|
10
|
+
const span = (bounds) => [
|
|
11
|
+
Math.floor(bounds.left / cellSize),
|
|
12
|
+
Math.floor(bounds.top / cellSize),
|
|
13
|
+
Math.floor((bounds.left + Math.max(0, bounds.width)) / cellSize),
|
|
14
|
+
Math.floor((bounds.top + Math.max(0, bounds.height)) / cellSize)
|
|
15
|
+
];
|
|
16
|
+
function remove(item) {
|
|
17
|
+
const where = placed.get(item);
|
|
18
|
+
if (where === void 0) return false;
|
|
19
|
+
placed.delete(item);
|
|
20
|
+
if (where === "large") {
|
|
21
|
+
large.delete(item);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
for (const key of where) {
|
|
25
|
+
const bucket = cells.get(key);
|
|
26
|
+
bucket.delete(item);
|
|
27
|
+
if (bucket.size === 0) cells.delete(key);
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
set(item, bounds) {
|
|
33
|
+
remove(item);
|
|
34
|
+
const [left, top, right, bottom] = span(bounds);
|
|
35
|
+
const count = (right - left + 1) * (bottom - top + 1);
|
|
36
|
+
if (!Number.isFinite(count) || count > maxCells) {
|
|
37
|
+
large.add(item);
|
|
38
|
+
placed.set(item, "large");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const keys = [];
|
|
42
|
+
for (let column = left; column <= right; column += 1) for (let row = top; row <= bottom; row += 1) {
|
|
43
|
+
const key = keyOf(column, row);
|
|
44
|
+
let bucket = cells.get(key);
|
|
45
|
+
if (!bucket) cells.set(key, bucket = /* @__PURE__ */ new Set());
|
|
46
|
+
bucket.add(item);
|
|
47
|
+
keys.push(key);
|
|
48
|
+
}
|
|
49
|
+
placed.set(item, keys);
|
|
50
|
+
},
|
|
51
|
+
delete: remove,
|
|
52
|
+
has: (item) => placed.has(item),
|
|
53
|
+
query(bounds) {
|
|
54
|
+
const found = new Set(large);
|
|
55
|
+
const [left, top, right, bottom] = span(bounds);
|
|
56
|
+
if ((right - left + 1) * (bottom - top + 1) > cells.size) {
|
|
57
|
+
for (const [key, bucket] of cells) {
|
|
58
|
+
const column = Math.floor(key / 2 ** 21) - OFFSET;
|
|
59
|
+
const row = key % 2 ** 21 - OFFSET;
|
|
60
|
+
if (column >= left && column <= right && row >= top && row <= bottom) bucket.forEach((item) => found.add(item));
|
|
61
|
+
}
|
|
62
|
+
return found;
|
|
63
|
+
}
|
|
64
|
+
for (let column = left; column <= right; column += 1) for (let row = top; row <= bottom; row += 1) cells.get(keyOf(column, row))?.forEach((item) => found.add(item));
|
|
65
|
+
return found;
|
|
66
|
+
},
|
|
67
|
+
clear() {
|
|
68
|
+
cells.clear();
|
|
69
|
+
placed.clear();
|
|
70
|
+
large.clear();
|
|
71
|
+
},
|
|
72
|
+
get size() {
|
|
73
|
+
return placed.size;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
exports.createSpatialIndex = createSpatialIndex;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/performance/spatial-index.d.ts
|
|
2
|
+
export interface Rect {
|
|
3
|
+
left: number;
|
|
4
|
+
top: number;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
export interface SpatialIndexOptions {
|
|
9
|
+
cellSize?: number;
|
|
10
|
+
maxCellsPerItem?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface SpatialIndex<Item> {
|
|
13
|
+
set(item: Item, bounds: Rect): void;
|
|
14
|
+
delete(item: Item): boolean;
|
|
15
|
+
has(item: Item): boolean;
|
|
16
|
+
query(bounds: Rect): Set<Item>;
|
|
17
|
+
clear(): void;
|
|
18
|
+
readonly size: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function createSpatialIndex<Item>(options?: SpatialIndexOptions): SpatialIndex<Item>;
|
|
21
|
+
//#endregion
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/performance/spatial-index.d.ts
|
|
2
|
+
export interface Rect {
|
|
3
|
+
left: number;
|
|
4
|
+
top: number;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
export interface SpatialIndexOptions {
|
|
9
|
+
cellSize?: number;
|
|
10
|
+
maxCellsPerItem?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface SpatialIndex<Item> {
|
|
13
|
+
set(item: Item, bounds: Rect): void;
|
|
14
|
+
delete(item: Item): boolean;
|
|
15
|
+
has(item: Item): boolean;
|
|
16
|
+
query(bounds: Rect): Set<Item>;
|
|
17
|
+
clear(): void;
|
|
18
|
+
readonly size: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function createSpatialIndex<Item>(options?: SpatialIndexOptions): SpatialIndex<Item>;
|
|
21
|
+
//#endregion
|