@inkra/react 0.3.0 → 0.5.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 +113 -3
- package/dist/Button.js +3 -2
- package/dist/PencilIcon.d.ts +10 -0
- package/dist/PencilIcon.js +83 -39
- package/dist/PencilLine.js +9 -1
- package/dist/PencilText.js +15 -9
- package/dist/accordion/Accordion.js +65 -5
- package/dist/alert-dialog/index.js +14 -4
- package/dist/audio-player/index.d.ts +2 -0
- package/dist/audio-player/index.js +131 -34
- package/dist/breadcrumb/index.js +4 -1
- package/dist/calendar/index.d.ts +4 -1
- package/dist/calendar/index.js +37 -12
- package/dist/calendar/locales.d.ts +7 -0
- package/dist/calendar/locales.js +2 -0
- package/dist/carousel/index.js +20 -10
- package/dist/choice-grid/index.d.ts +6 -1
- package/dist/choice-grid/index.js +63 -33
- package/dist/color-picker/GradientEditor.d.ts +40 -0
- package/dist/color-picker/GradientEditor.js +64 -0
- package/dist/color-picker/gradient.d.ts +25 -0
- package/dist/color-picker/gradient.js +60 -0
- package/dist/color-picker/index.d.ts +27 -7
- package/dist/color-picker/index.js +144 -40
- package/dist/color-swatch/index.d.ts +10 -0
- package/dist/color-swatch/index.js +9 -0
- package/dist/command/index.js +27 -4
- package/dist/content/index.d.ts +5 -0
- package/dist/content/index.js +15 -3
- package/dist/context-menu/index.js +1 -1
- package/dist/data-table/index.d.ts +7 -1
- package/dist/data-table/index.js +67 -16
- package/dist/data-table/model.d.ts +2 -0
- package/dist/data-table/model.js +7 -0
- package/dist/date-picker/index.d.ts +2 -0
- package/dist/date-picker/index.js +20 -21
- package/dist/day-agenda/index.d.ts +6 -1
- package/dist/day-agenda/index.js +49 -21
- package/dist/day-agenda/layout.js +65 -9
- package/dist/display/ProgressRing.js +7 -3
- package/dist/display/index.d.ts +4 -1
- package/dist/display/index.js +3 -2
- package/dist/dockable-panel/index.js +15 -13
- package/dist/drawer/index.js +11 -5
- package/dist/editable/index.d.ts +24 -2
- package/dist/editable/index.js +148 -40
- package/dist/empty/illustrations.d.ts +8 -0
- package/dist/empty/illustrations.js +78 -0
- package/dist/empty/index.d.ts +6 -1
- package/dist/empty/index.js +11 -3
- package/dist/flip/index.js +28 -8
- package/dist/form/Field.js +11 -10
- package/dist/form/Input.js +4 -1
- package/dist/form/Textarea.d.ts +3 -1
- package/dist/form/Textarea.js +9 -5
- package/dist/heatmap/index.js +4 -1
- package/dist/hotkey/index.d.ts +1 -0
- package/dist/hotkey/index.js +9 -4
- package/dist/hover-card/index.js +1 -1
- package/dist/i18n/catalog.d.ts +5 -0
- package/dist/i18n/catalog.js +289 -0
- package/dist/i18n/index.d.ts +23 -0
- package/dist/i18n/index.js +50 -0
- package/dist/image-cropper/index.js +21 -17
- package/dist/image-viewer/VideoResource.d.ts +21 -0
- package/dist/image-viewer/VideoResource.js +196 -0
- package/dist/image-viewer/index.d.ts +14 -0
- package/dist/image-viewer/index.js +141 -86
- package/dist/image-viewer/lucideGeometry.d.ts +3 -0
- package/dist/image-viewer/lucideGeometry.js +19 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/input-number/index.js +13 -5
- package/dist/internal/cache.d.ts +4 -0
- package/dist/internal/cache.js +18 -1
- package/dist/internal/iconGeometry.d.ts +2 -0
- package/dist/internal/iconGeometry.js +32 -0
- package/dist/internal/renderTokens.d.ts +4 -0
- package/dist/internal/renderTokens.js +4 -0
- package/dist/internal/stroke.d.ts +7 -0
- package/dist/internal/stroke.js +63 -7
- package/dist/internal/surfaceBudget.d.ts +20 -0
- package/dist/internal/surfaceBudget.js +69 -0
- package/dist/internal/surfaceRenderer.js +87 -28
- package/dist/internal/surfaceVisibility.d.ts +3 -0
- package/dist/internal/surfaceVisibility.js +60 -0
- package/dist/internal/textRenderer.d.ts +3 -0
- package/dist/internal/textRenderer.js +13 -5
- package/dist/internal/usePencilSurface.js +17 -1
- package/dist/internal/wash.js +4 -3
- package/dist/journey-map/index.d.ts +1 -1
- package/dist/journey-map/index.js +6 -3
- package/dist/journey-map/terrain.js +10 -5
- package/dist/keypad/index.js +4 -1
- package/dist/layout/index.js +1 -1
- package/dist/log/index.js +42 -33
- package/dist/markdown/CodeBlock.d.ts +19 -0
- package/dist/markdown/CodeBlock.js +86 -0
- package/dist/markdown/Deletion.d.ts +2 -0
- package/dist/markdown/Deletion.js +69 -0
- package/dist/markdown/icons.d.ts +14 -0
- package/dist/markdown/icons.js +16 -0
- package/dist/markdown/index.d.ts +17 -0
- package/dist/markdown/index.js +177 -0
- package/dist/node-graph/index.d.ts +3 -1
- package/dist/node-graph/index.js +63 -28
- package/dist/number-ticker/index.js +4 -1
- package/dist/number-ticker/precision.js +9 -2
- package/dist/overflow-list/index.js +9 -6
- package/dist/overlay/Overlay.js +8 -3
- package/dist/overlay/Popover.js +3 -3
- package/dist/overlay/themeBridge.js +14 -2
- package/dist/pagination/index.d.ts +10 -1
- package/dist/pagination/index.js +13 -9
- package/dist/pan-zoom/index.d.ts +2 -0
- package/dist/pan-zoom/index.js +12 -8
- package/dist/pencil.d.ts +1 -0
- package/dist/pencil.js +4 -4
- package/dist/radar/index.d.ts +2 -0
- package/dist/radar/index.js +44 -21
- package/dist/range-slider/index.js +4 -3
- package/dist/rating/index.d.ts +4 -0
- package/dist/rating/index.js +131 -24
- package/dist/resizable/index.d.ts +1 -1
- package/dist/resizable/index.js +5 -2
- package/dist/scroll-area/index.d.ts +1 -1
- package/dist/scroll-area/index.js +5 -2
- package/dist/select/MultipleSelect.js +5 -4
- package/dist/select/SearchableSelect.js +64 -7
- package/dist/select/Select.d.ts +1 -0
- package/dist/select/Select.js +6 -2
- package/dist/select/StepperSelect.d.ts +29 -0
- package/dist/select/StepperSelect.js +122 -0
- package/dist/select/index.d.ts +2 -0
- package/dist/select/index.js +1 -0
- package/dist/select/stepperModel.d.ts +7 -0
- package/dist/select/stepperModel.js +29 -0
- package/dist/selection/Checkbox.js +4 -3
- package/dist/selection/RadioGroup.js +29 -13
- package/dist/selection/Switch.js +4 -3
- package/dist/selection/useNativeChecked.d.ts +2 -1
- package/dist/selection/useNativeChecked.js +26 -8
- package/dist/slider/index.js +50 -6
- package/dist/sortable/index.js +17 -11
- package/dist/sparkline/index.js +21 -6
- package/dist/sparkline/model.js +11 -4
- package/dist/steps/index.js +4 -1
- package/dist/styles/accordion.css +228 -0
- package/dist/styles/alert-dialog.css +139 -0
- package/dist/styles/audio-player.css +22 -0
- package/dist/styles/base.css +638 -0
- package/dist/styles/breadcrumb.css +7 -0
- package/dist/styles/calendar.css +62 -0
- package/dist/styles/carousel.css +173 -0
- package/dist/styles/choice-grid.css +226 -0
- package/dist/styles/color-picker.css +281 -0
- package/dist/styles/color-swatch.css +47 -0
- package/dist/styles/command.css +52 -0
- package/dist/styles/content.css +79 -0
- package/dist/styles/context-menu.css +35 -0
- package/dist/styles/data-table.css +31 -0
- package/dist/styles/date-picker.css +107 -0
- package/dist/styles/day-agenda.css +30 -0
- package/dist/styles/display.css +304 -0
- package/dist/styles/dockable-panel.css +110 -0
- package/dist/styles/drawer.css +133 -0
- package/dist/styles/dropdown-menu.css +68 -0
- package/dist/styles/editable.css +145 -0
- package/dist/styles/empty.css +108 -0
- package/dist/styles/flip.css +101 -0
- package/dist/styles/form.css +634 -0
- package/dist/styles/heatmap.css +256 -0
- package/dist/styles/hotkey.css +189 -0
- package/dist/styles/hover-card.css +27 -0
- package/dist/styles/image-cropper.css +205 -0
- package/dist/styles/image-viewer.css +422 -0
- package/dist/styles/input-number.css +21 -0
- package/dist/styles/journey-map.css +19 -0
- package/dist/styles/keypad.css +92 -0
- package/dist/styles/layout.css +72 -0
- package/dist/styles/leaflet.css +661 -0
- package/dist/styles/log.css +203 -0
- package/dist/styles/markdown.css +379 -0
- package/dist/styles/node-graph.css +142 -0
- package/dist/styles/number-ticker.css +83 -0
- package/dist/styles/overflow-list.css +51 -0
- package/dist/styles/overlay.css +284 -0
- package/dist/styles/pagination.css +21 -0
- package/dist/styles/pan-zoom.css +136 -0
- package/dist/styles/radar.css +1 -0
- package/dist/styles/rating.css +121 -0
- package/dist/styles/resizable.css +108 -0
- package/dist/styles/scroll-area.css +122 -0
- package/dist/styles/select.css +554 -0
- package/dist/styles/selection.css +264 -0
- package/dist/styles/slider.css +145 -0
- package/dist/styles/slot.css +109 -0
- package/dist/styles/sortable.css +159 -0
- package/dist/styles/sparkline.css +24 -0
- package/dist/styles/steps.css +27 -0
- package/dist/styles/tabs.css +79 -0
- package/dist/styles/tag-input.css +213 -0
- package/dist/styles/time-picker.css +66 -0
- package/dist/styles/timeline.css +24 -0
- package/dist/styles/toast.css +160 -0
- package/dist/styles/toggle-group.css +107 -0
- package/dist/styles/tooltip.css +78 -0
- package/dist/styles/transfer.css +148 -0
- package/dist/styles/tree-select.css +131 -0
- package/dist/styles/tree.css +59 -0
- package/dist/styles/typewriter.css +62 -0
- package/dist/styles/upload.css +60 -0
- package/dist/styles/virtual-list.css +112 -0
- package/dist/styles.css +2091 -421
- package/dist/tabs/index.d.ts +9 -0
- package/dist/tabs/index.js +69 -16
- package/dist/tabs/useOverflow.d.ts +20 -0
- package/dist/tabs/useOverflow.js +145 -0
- package/dist/tag-input/index.d.ts +7 -2
- package/dist/tag-input/index.js +81 -42
- package/dist/theme/index.d.ts +11 -0
- package/dist/theme/index.js +23 -0
- package/dist/time-picker/index.js +45 -27
- package/dist/timeline/index.js +4 -1
- package/dist/toast/index.d.ts +19 -11
- package/dist/toast/index.js +51 -8
- package/dist/toast/manager.d.ts +42 -0
- package/dist/toast/manager.js +85 -0
- package/dist/toast/useToastNavigation.d.ts +3 -0
- package/dist/toast/useToastNavigation.js +75 -0
- package/dist/tokens.css +4 -0
- package/dist/tooltip/Tooltip.js +3 -3
- package/dist/transfer/index.d.ts +5 -0
- package/dist/transfer/index.js +120 -53
- package/dist/transfer/model.d.ts +4 -0
- package/dist/transfer/model.js +37 -11
- package/dist/tree/index.d.ts +5 -1
- package/dist/tree/index.js +11 -6
- package/dist/tree-select/index.js +70 -31
- package/dist/tree-select/model.d.ts +11 -5
- package/dist/tree-select/model.js +111 -83
- package/dist/typewriter/index.js +10 -10
- package/dist/upload/index.js +50 -33
- package/dist/virtual-list/index.js +11 -6
- package/package.json +423 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useInkraLocale } from '../i18n/index.js';
|
|
2
3
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
3
4
|
import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
|
|
4
5
|
import { Button } from '../Button.js';
|
|
@@ -13,7 +14,9 @@ const minusPaths = [{ d: 'M5 12h14' }];
|
|
|
13
14
|
const finiteValue = (value) => typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
14
15
|
const format = (value) => String(finiteValue(value) ?? '');
|
|
15
16
|
const numberFromDraft = (draft) => draft === '' ? null : finiteValue(Number(draft));
|
|
16
|
-
export const InputNumber = forwardRef(function InputNumber(
|
|
17
|
+
export const InputNumber = forwardRef(function InputNumber(inkraProps, forwardedRef) {
|
|
18
|
+
const { t } = useInkraLocale();
|
|
19
|
+
const { classNames, controls = true, decrementButtonLabel = t('inputNumber.decrease'), defaultValue, disabled, incrementButtonLabel = t('inputNumber.increase'), inputMode = 'decimal', max, min, onChange, onBlur, onKeyDown, onValueChange, pressAndHold = true, readOnly, repeatDelay = 400, repeatInterval = 80, size = 'md', step = 1, stepModifiers, textAlign = 'center', value, ...props } = inkraProps;
|
|
17
20
|
const inputRef = useRef(null);
|
|
18
21
|
const isControlled = value !== undefined;
|
|
19
22
|
const [draft, setDraft] = useState(() => format(isControlled ? value : defaultValue));
|
|
@@ -25,9 +28,12 @@ export const InputNumber = forwardRef(function InputNumber({ classNames, control
|
|
|
25
28
|
const holdRef = useRef(null);
|
|
26
29
|
const timerRef = useRef(undefined);
|
|
27
30
|
const suppressClickRef = useRef(false);
|
|
31
|
+
const removeHoldListeners = useRef(undefined);
|
|
28
32
|
const stopHolding = useCallback(() => {
|
|
29
33
|
clearTimeout(timerRef.current);
|
|
30
34
|
timerRef.current = undefined;
|
|
35
|
+
removeHoldListeners.current?.();
|
|
36
|
+
removeHoldListeners.current = undefined;
|
|
31
37
|
const hold = holdRef.current;
|
|
32
38
|
holdRef.current = null;
|
|
33
39
|
if (hold?.button.hasPointerCapture?.(hold.pointerId)) {
|
|
@@ -37,7 +43,8 @@ export const InputNumber = forwardRef(function InputNumber({ classNames, control
|
|
|
37
43
|
catch { /* The pointer may already be inactive. */ }
|
|
38
44
|
}
|
|
39
45
|
}, []);
|
|
40
|
-
|
|
46
|
+
// Idle controls do not need global pointer, keyboard or visibility listeners.
|
|
47
|
+
const listenWhileHolding = () => {
|
|
41
48
|
const onVisibilityChange = () => { if (document.hidden)
|
|
42
49
|
stopHolding(); };
|
|
43
50
|
const onPointerEnd = (event) => {
|
|
@@ -56,8 +63,7 @@ export const InputNumber = forwardRef(function InputNumber({ classNames, control
|
|
|
56
63
|
window.addEventListener('keydown', updateModifiers);
|
|
57
64
|
window.addEventListener('keyup', updateModifiers);
|
|
58
65
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
59
|
-
|
|
60
|
-
stopHolding();
|
|
66
|
+
removeHoldListeners.current = () => {
|
|
61
67
|
window.removeEventListener('blur', stopHolding);
|
|
62
68
|
window.removeEventListener('pointerup', onPointerEnd);
|
|
63
69
|
window.removeEventListener('pointercancel', onPointerEnd);
|
|
@@ -65,7 +71,8 @@ export const InputNumber = forwardRef(function InputNumber({ classNames, control
|
|
|
65
71
|
window.removeEventListener('keyup', updateModifiers);
|
|
66
72
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
67
73
|
};
|
|
68
|
-
}
|
|
74
|
+
};
|
|
75
|
+
useEffect(() => stopHolding, [stopHolding]);
|
|
69
76
|
useBrowserLayoutEffect(() => {
|
|
70
77
|
if (isControlled && currentValue !== finiteValue(value))
|
|
71
78
|
setDraft(format(value));
|
|
@@ -136,6 +143,7 @@ export const InputNumber = forwardRef(function InputNumber({ classNames, control
|
|
|
136
143
|
if (!button.isConnected || button.disabled)
|
|
137
144
|
return;
|
|
138
145
|
holdRef.current = { button, pointerId: event.pointerId, direction, modifiers };
|
|
146
|
+
listenWhileHolding();
|
|
139
147
|
try {
|
|
140
148
|
button.setPointerCapture(event.pointerId);
|
|
141
149
|
}
|
package/dist/internal/cache.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export declare class BoundedCache<Value> {
|
|
|
9
9
|
set(key: string, value: Value, cost: number): Value;
|
|
10
10
|
delete(key: string): void;
|
|
11
11
|
}
|
|
12
|
+
export declare function boundedCanvasSize(width: number, height: number, dpr: number): {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
};
|
|
12
16
|
export declare function canvasByteCost(canvas: HTMLCanvasElement): number;
|
|
13
17
|
export declare function canvasIsUsable(canvas: HTMLCanvasElement): boolean;
|
|
14
18
|
export declare function effectiveDpr(limit?: number): number;
|
package/dist/internal/cache.js
CHANGED
|
@@ -15,6 +15,12 @@ export class BoundedCache {
|
|
|
15
15
|
return entry.value;
|
|
16
16
|
}
|
|
17
17
|
set(key, value, cost) {
|
|
18
|
+
// A one-off oversized image must not flush every useful cached control.
|
|
19
|
+
// Invalid accounting must never disable the byte budget (NaN) either.
|
|
20
|
+
if (!Number.isFinite(cost) || cost < 0 || cost > this.maxCost || this.maxEntries < 1) {
|
|
21
|
+
this.delete(key);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
18
24
|
const previous = this.entries.get(key);
|
|
19
25
|
if (previous) {
|
|
20
26
|
this.totalCost -= previous.cost;
|
|
@@ -24,7 +30,7 @@ export class BoundedCache {
|
|
|
24
30
|
this.totalCost += cost;
|
|
25
31
|
while (this.entries.size > this.maxEntries || this.totalCost > this.maxCost) {
|
|
26
32
|
const oldestKey = this.entries.keys().next().value;
|
|
27
|
-
if (
|
|
33
|
+
if (oldestKey === undefined)
|
|
28
34
|
break;
|
|
29
35
|
const oldest = this.entries.get(oldestKey);
|
|
30
36
|
this.entries.delete(oldestKey);
|
|
@@ -40,6 +46,17 @@ export class BoundedCache {
|
|
|
40
46
|
this.totalCost -= entry.cost;
|
|
41
47
|
}
|
|
42
48
|
}
|
|
49
|
+
// Cache limits apply after drawing. Check allocation limits before asking the
|
|
50
|
+
// browser for a backing store, including transient canvases that are not cached.
|
|
51
|
+
export function boundedCanvasSize(width, height, dpr) {
|
|
52
|
+
const pixelWidth = Math.ceil(width * dpr);
|
|
53
|
+
const pixelHeight = Math.ceil(height * dpr);
|
|
54
|
+
if (![width, height, dpr].every(value => Number.isFinite(value) && value > 0)
|
|
55
|
+
|| pixelWidth > 8192 || pixelHeight > 8192 || pixelWidth * pixelHeight > 4 * 1024 * 1024) {
|
|
56
|
+
throw new RangeError('Pencil canvas exceeds the bounded pixel budget');
|
|
57
|
+
}
|
|
58
|
+
return { width: pixelWidth, height: pixelHeight };
|
|
59
|
+
}
|
|
43
60
|
export function canvasByteCost(canvas) {
|
|
44
61
|
return canvas.width * canvas.height * 4;
|
|
45
62
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BoundedCache, PENCIL_RENDERER_VERSION } from './cache.js';
|
|
2
|
+
const geometryCache = new BoundedCache(256, 180000);
|
|
3
|
+
export function cachedPathParts(data) {
|
|
4
|
+
// Limit both parsing input and samples before allocating. Cache eviction only
|
|
5
|
+
// bounds retained geometry, not the work needed to construct a huge path.
|
|
6
|
+
if (data.length > 65536)
|
|
7
|
+
throw new RangeError('Pencil icon path exceeds the bounded input');
|
|
8
|
+
const key = `${PENCIL_RENDERER_VERSION}:${data}`;
|
|
9
|
+
const cached = geometryCache.get(key);
|
|
10
|
+
if (cached)
|
|
11
|
+
return cached;
|
|
12
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
13
|
+
path.setAttribute('d', data);
|
|
14
|
+
const length = path.getTotalLength();
|
|
15
|
+
const count = Math.max(1, Math.ceil(length / 0.25));
|
|
16
|
+
if (!Number.isFinite(length) || length < 0 || count > 16384) {
|
|
17
|
+
throw new RangeError('Pencil icon path exceeds the bounded sampling budget');
|
|
18
|
+
}
|
|
19
|
+
const parts = [[]];
|
|
20
|
+
for (let index = 0; index <= count; index += 1) {
|
|
21
|
+
const point = path.getPointAtLength(length * index / count);
|
|
22
|
+
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) {
|
|
23
|
+
throw new RangeError('Pencil icon path contains invalid coordinates');
|
|
24
|
+
}
|
|
25
|
+
const activePart = parts[parts.length - 1];
|
|
26
|
+
const previous = activePart[activePart.length - 1];
|
|
27
|
+
if (previous && Math.hypot(point.x - previous[0], point.y - previous[1]) > 0.5)
|
|
28
|
+
parts.push([]);
|
|
29
|
+
parts[parts.length - 1].push([point.x, point.y]);
|
|
30
|
+
}
|
|
31
|
+
return geometryCache.set(key, parts, parts.reduce((total, part) => total + part.length, 0));
|
|
32
|
+
}
|
|
@@ -2,6 +2,12 @@ export type PencilPoint = readonly [x: number, y: number];
|
|
|
2
2
|
export interface StrokeSettings {
|
|
3
3
|
variation?: number;
|
|
4
4
|
breaks?: number;
|
|
5
|
+
continuousBreaks?: boolean;
|
|
6
|
+
/** Maximum gaps per path; short paths reduce the actual count. */
|
|
7
|
+
breakCount?: number;
|
|
8
|
+
/** Display pixels per path unit, excluding device pixel ratio. */
|
|
9
|
+
breakScale?: number;
|
|
10
|
+
fitPressure?: boolean;
|
|
5
11
|
grain?: number;
|
|
6
12
|
taperLength?: number;
|
|
7
13
|
scale?: number;
|
|
@@ -20,5 +26,6 @@ export declare function strokeProfile(at: number, total: number, seed: number, s
|
|
|
20
26
|
contact: number;
|
|
21
27
|
deposit: number;
|
|
22
28
|
taper: number;
|
|
29
|
+
gapDistance: number;
|
|
23
30
|
};
|
|
24
31
|
export declare function pencilStroke(context: CanvasRenderingContext2D, points: readonly PencilPoint[], color: string, width: number, seed: number, amplitude: number, passes?: number, closed?: boolean, settings?: StrokeSettings): void;
|
package/dist/internal/stroke.js
CHANGED
|
@@ -14,14 +14,52 @@ export function strokeProfile(at, total, seed, settings = {}) {
|
|
|
14
14
|
const variation = settings.variation ?? 1;
|
|
15
15
|
const breaks = settings.breaks ?? 0.8;
|
|
16
16
|
const grainValue = settings.grain ?? 0.7;
|
|
17
|
-
|
|
17
|
+
let force = clamp(0.12 + noise(at / 19, seed + 17) * 0.97
|
|
18
18
|
+ (noise(at / 5.3, seed + 39) - 0.5) * 0.3);
|
|
19
|
+
if (settings.fitPressure && total > 0 && total < 48) {
|
|
20
|
+
// Short icon paths need a full pressure gesture, not a nearly flat slice
|
|
21
|
+
// of the long-stroke noise. Blend back to the original profile by 48 units.
|
|
22
|
+
const phase = (at / total + hash(0, seed + 61)) * Math.PI * 2;
|
|
23
|
+
const gesture = 0.25 + (1 - Math.cos(phase)) * 0.35
|
|
24
|
+
+ (noise(at / 5.3, seed + 39) - 0.5) * 0.08;
|
|
25
|
+
const blend = clamp((48 - total) / 24);
|
|
26
|
+
force = force * (1 - blend) + gesture * blend;
|
|
27
|
+
}
|
|
19
28
|
const pressure = clamp(0.72 * (1 - variation) + force * variation);
|
|
20
29
|
let contact = 1;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
let gapDistance = Infinity;
|
|
31
|
+
// Reserve at least 12 CSS pixels per gap, measured over the full path.
|
|
32
|
+
// Display size affects density, but device pixel ratio must not affect it.
|
|
33
|
+
const breakScale = Number.isFinite(settings.breakScale) && settings.breakScale > 0 ? settings.breakScale : 1;
|
|
34
|
+
const gapCount = Number.isFinite(settings.breakCount)
|
|
35
|
+
? Math.max(0, Math.min(12, Math.floor(settings.breakCount), Math.floor(total * breakScale / 12)))
|
|
36
|
+
: undefined;
|
|
37
|
+
const gapSpacing = settings.continuousBreaks
|
|
38
|
+
? gapCount ? total / gapCount : Math.min(37, Math.max(1, total))
|
|
39
|
+
: 37;
|
|
40
|
+
const cell = Math.floor(at / gapSpacing);
|
|
41
|
+
// A fixed iteration count also terminates for out-of-range numeric input.
|
|
42
|
+
for (let offset = -1; offset <= 1; offset += 1) {
|
|
43
|
+
const index = cell + offset;
|
|
44
|
+
if (!settings.continuousBreaks && hash(index, seed + 277) > breaks * 0.9)
|
|
45
|
+
continue;
|
|
46
|
+
if (settings.continuousBreaks) {
|
|
47
|
+
if (gapCount !== undefined && (index < 0 || index >= gapCount))
|
|
48
|
+
continue;
|
|
49
|
+
const amount = clamp(breaks);
|
|
50
|
+
if (!amount)
|
|
51
|
+
continue;
|
|
52
|
+
// Fixed locations with linearly growing gap and feather widths avoid
|
|
53
|
+
// switching an entire gap on when a slider crosses a random threshold.
|
|
54
|
+
const middle = gapSpacing * (index + (gapCount === undefined
|
|
55
|
+
? 0.2 + hash(index, seed + 283) * 0.6
|
|
56
|
+
: 0.3 + hash(index, seed + 283) * 0.4));
|
|
57
|
+
const gapScale = Math.min(1, gapSpacing / 24);
|
|
58
|
+
const halfGap = (1.1 + hash(index, seed + 293) * 3.6) * amount * gapScale;
|
|
59
|
+
gapDistance = Math.min(gapDistance, Math.abs(at - middle) - halfGap - 2.1 * amount * gapScale);
|
|
60
|
+
contact = Math.min(contact, clamp((Math.abs(at - middle) - halfGap) / (2.1 * amount * gapScale)));
|
|
24
61
|
continue;
|
|
62
|
+
}
|
|
25
63
|
const middle = index * 37 + 5 + hash(index, seed + 283) * 27;
|
|
26
64
|
const halfGap = (1.1 + hash(index, seed + 293) * 3.6) * breaks;
|
|
27
65
|
contact = Math.min(contact, clamp((Math.abs(at - middle) - halfGap) / 2.1));
|
|
@@ -29,13 +67,22 @@ export function strokeProfile(at, total, seed, settings = {}) {
|
|
|
29
67
|
const tooth = noise(at * 1.6, seed + 401);
|
|
30
68
|
const deposit = clamp((tooth - grainValue * 0.22) / Math.max(0.2, 1 - grainValue * 0.22));
|
|
31
69
|
const taper = 0.2 + 0.8 * clamp(Math.min(at, total - at) / (settings.taperLength ?? 3));
|
|
32
|
-
return { pressure, contact, deposit, taper };
|
|
70
|
+
return { pressure, contact, deposit, taper, gapDistance };
|
|
33
71
|
}
|
|
34
72
|
export function pencilStroke(context, points, color, width, seed, amplitude, passes = 2, closed = false, settings = {}) {
|
|
35
|
-
|
|
73
|
+
const pointCount = points.length;
|
|
74
|
+
if (pointCount < 2 || pointCount > 65536)
|
|
36
75
|
return;
|
|
76
|
+
// Validate the coordinates we retain, without scanning then cloning them again.
|
|
77
|
+
const line = [];
|
|
78
|
+
for (let index = 0; index < pointCount; index += 1) {
|
|
79
|
+
const point = points[index];
|
|
80
|
+
const x = point[0], y = point[1];
|
|
81
|
+
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
82
|
+
return;
|
|
83
|
+
line.push([x, y]);
|
|
84
|
+
}
|
|
37
85
|
const scale = Math.max(0.4, settings.scale ?? 1);
|
|
38
|
-
const line = points.map(point => [...point]);
|
|
39
86
|
const lastPoint = line[line.length - 1];
|
|
40
87
|
if (closed && Math.hypot(line[0][0] - lastPoint[0], line[0][1] - lastPoint[1]) > 0.01) {
|
|
41
88
|
line.push(line[0]);
|
|
@@ -56,6 +103,13 @@ export function pencilStroke(context, points, color, width, seed, amplitude, pas
|
|
|
56
103
|
const distanceOffset = settings.distanceOffset ?? 0;
|
|
57
104
|
const totalDistance = settings.totalDistance ?? distanceOffset + total;
|
|
58
105
|
const step = settings.step ?? (settings.detail === 'fill' ? 2.1 : 1.05);
|
|
106
|
+
// Stop invalid steps and huge geometry before entering the sampling loop.
|
|
107
|
+
if (!Number.isFinite(total) || !Number.isFinite(step) || step <= 0 || total / step > 65536
|
|
108
|
+
|| !Number.isFinite(distanceOffset) || Math.abs(distanceOffset) > Number.MAX_SAFE_INTEGER
|
|
109
|
+
|| !Number.isFinite(totalDistance) || Math.abs(totalDistance) > Number.MAX_SAFE_INTEGER
|
|
110
|
+
|| !Number.isFinite(passes) || passes < 0 || passes > 16
|
|
111
|
+
|| !Number.isFinite(settings.filaments ?? 3) || (settings.filaments ?? 3) > 16)
|
|
112
|
+
return;
|
|
59
113
|
const samples = [];
|
|
60
114
|
let segmentIndex = 0;
|
|
61
115
|
for (let at = 0;; at = Math.min(total, at + step)) {
|
|
@@ -103,6 +157,8 @@ export function pencilStroke(context, points, color, width, seed, amplitude, pas
|
|
|
103
157
|
if (opacity < 0.012)
|
|
104
158
|
continue;
|
|
105
159
|
const nib = width * (0.3 + pressure * 1.65) * Math.pow(scale, 0.32);
|
|
160
|
+
// Round segment caps must not paint back across intentional short gaps.
|
|
161
|
+
context.lineCap = Math.min(a.gapDistance, b.gapDistance) < nib / 2 + step ? 'butt' : 'round';
|
|
106
162
|
const drift = pass ? (noise(a.at / 6, seed + pass * 107) - 0.5) * width * 1.1 : 0;
|
|
107
163
|
context.globalAlpha = Math.min(1, inheritedAlpha * opacity * (pass ? 0.56 : 1.2)
|
|
108
164
|
* (settings.gain ?? 1));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const MAX_SURFACE_PHYSICAL_EDGE = 8192;
|
|
2
|
+
export declare const MAX_SURFACE_LAYER_PIXELS: number;
|
|
3
|
+
export declare const MAX_SURFACE_TOTAL_PIXELS: number;
|
|
4
|
+
export type SurfaceAllocationFailure = 'invalid-size' | 'edge-limit' | 'layer-budget' | 'surface-budget';
|
|
5
|
+
export type SurfaceAllocation = {
|
|
6
|
+
ok: true;
|
|
7
|
+
dpr: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
layerPixels: number;
|
|
11
|
+
totalPixels: number;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
reason: SurfaceAllocationFailure;
|
|
15
|
+
};
|
|
16
|
+
export declare function resolveSurfaceAllocation(width: number, height: number, dpr: number, bleed: number, layers: 1 | 2): SurfaceAllocation;
|
|
17
|
+
type CanvasBitmap = Pick<HTMLCanvasElement, 'width' | 'height'>;
|
|
18
|
+
export declare function resizeSurfaceBitmap(canvas: CanvasBitmap, width: number, height: number): void;
|
|
19
|
+
export declare function releaseSurfaceBitmaps(...canvases: Array<CanvasBitmap | null | undefined>): void;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// 1024x640 at DPR 2 is 2.62M pixels per layer and 5.24M with a clipped border layer.
|
|
2
|
+
export const MAX_SURFACE_PHYSICAL_EDGE = 8192;
|
|
3
|
+
export const MAX_SURFACE_LAYER_PIXELS = 4 * 1024 * 1024;
|
|
4
|
+
export const MAX_SURFACE_TOTAL_PIXELS = 6 * 1024 * 1024;
|
|
5
|
+
const DPR_PRECISION = 1024;
|
|
6
|
+
function allocationAtDpr(logicalWidth, logicalHeight, dpr, layers) {
|
|
7
|
+
const physicalWidth = Math.ceil(logicalWidth * dpr);
|
|
8
|
+
const physicalHeight = Math.ceil(logicalHeight * dpr);
|
|
9
|
+
if (!Number.isSafeInteger(physicalWidth) || !Number.isSafeInteger(physicalHeight)
|
|
10
|
+
|| physicalWidth < 1 || physicalHeight < 1) {
|
|
11
|
+
return { ok: false, reason: 'invalid-size' };
|
|
12
|
+
}
|
|
13
|
+
if (physicalWidth > MAX_SURFACE_PHYSICAL_EDGE || physicalHeight > MAX_SURFACE_PHYSICAL_EDGE) {
|
|
14
|
+
return { ok: false, reason: 'edge-limit' };
|
|
15
|
+
}
|
|
16
|
+
const layerPixels = physicalWidth * physicalHeight;
|
|
17
|
+
if (!Number.isSafeInteger(layerPixels) || layerPixels > MAX_SURFACE_LAYER_PIXELS) {
|
|
18
|
+
return { ok: false, reason: 'layer-budget' };
|
|
19
|
+
}
|
|
20
|
+
const totalPixels = layerPixels * layers;
|
|
21
|
+
if (!Number.isSafeInteger(totalPixels) || totalPixels > MAX_SURFACE_TOTAL_PIXELS) {
|
|
22
|
+
return { ok: false, reason: 'surface-budget' };
|
|
23
|
+
}
|
|
24
|
+
return { ok: true, dpr, width: physicalWidth, height: physicalHeight, layerPixels, totalPixels };
|
|
25
|
+
}
|
|
26
|
+
export function resolveSurfaceAllocation(width, height, dpr, bleed, layers) {
|
|
27
|
+
if (![width, height, dpr, bleed].every(Number.isFinite)
|
|
28
|
+
|| width <= 0 || height <= 0 || dpr < 1 || bleed < 0) {
|
|
29
|
+
return { ok: false, reason: 'invalid-size' };
|
|
30
|
+
}
|
|
31
|
+
const logicalWidth = width + bleed * 2;
|
|
32
|
+
const logicalHeight = height + bleed * 2;
|
|
33
|
+
const requested = allocationAtDpr(logicalWidth, logicalHeight, dpr, layers);
|
|
34
|
+
if (requested.ok)
|
|
35
|
+
return requested;
|
|
36
|
+
const minimum = allocationAtDpr(logicalWidth, logicalHeight, 1, layers);
|
|
37
|
+
if (!minimum.ok)
|
|
38
|
+
return minimum;
|
|
39
|
+
let lower = 1;
|
|
40
|
+
let upper = dpr;
|
|
41
|
+
for (let iteration = 0; iteration < 40; iteration += 1) {
|
|
42
|
+
const candidate = (lower + upper) / 2;
|
|
43
|
+
if (allocationAtDpr(logicalWidth, logicalHeight, candidate, layers).ok)
|
|
44
|
+
lower = candidate;
|
|
45
|
+
else
|
|
46
|
+
upper = candidate;
|
|
47
|
+
}
|
|
48
|
+
const resolvedDpr = Math.max(1, Math.floor(lower * DPR_PRECISION) / DPR_PRECISION);
|
|
49
|
+
const resolved = allocationAtDpr(logicalWidth, logicalHeight, resolvedDpr, layers);
|
|
50
|
+
return resolved.ok ? resolved : minimum;
|
|
51
|
+
}
|
|
52
|
+
export function resizeSurfaceBitmap(canvas, width, height) {
|
|
53
|
+
if (canvas.width === width && canvas.height === height) {
|
|
54
|
+
canvas.width = width;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
canvas.width = 0;
|
|
58
|
+
canvas.height = 0;
|
|
59
|
+
canvas.width = width;
|
|
60
|
+
canvas.height = height;
|
|
61
|
+
}
|
|
62
|
+
export function releaseSurfaceBitmaps(...canvases) {
|
|
63
|
+
for (const canvas of canvases) {
|
|
64
|
+
if (!canvas)
|
|
65
|
+
continue;
|
|
66
|
+
canvas.width = 0;
|
|
67
|
+
canvas.height = 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -2,6 +2,8 @@ import { pencilPaper } from './paper.js';
|
|
|
2
2
|
import { pencilStroke } from './stroke.js';
|
|
3
3
|
import { drawPencilWash } from './wash.js';
|
|
4
4
|
import { pencilRenderDefaults } from './renderTokens.js';
|
|
5
|
+
import { BoundedCache, PENCIL_RENDERER_VERSION, canvasByteCost, canvasIsUsable } from './cache.js';
|
|
6
|
+
import { releaseSurfaceBitmaps, resizeSurfaceBitmap, resolveSurfaceAllocation } from './surfaceBudget.js';
|
|
5
7
|
export const PENCIL_ACTION_WASH = Object.freeze({
|
|
6
8
|
lightStrength: 0.2,
|
|
7
9
|
darkStrength: 0.12,
|
|
@@ -71,19 +73,27 @@ function paperPattern(context, color, dark, dpr) {
|
|
|
71
73
|
}
|
|
72
74
|
return pattern;
|
|
73
75
|
}
|
|
74
|
-
|
|
76
|
+
function allocateSurface(canvas, borderCanvas, width, height, dpr, bleed) {
|
|
77
|
+
const allocation = resolveSurfaceAllocation(width, height, dpr, bleed, borderCanvas ? 2 : 1);
|
|
78
|
+
if (!allocation.ok) {
|
|
79
|
+
releaseSurfaceBitmaps(canvas, borderCanvas);
|
|
80
|
+
throw new RangeError(`PencilSurface allocation rejected: ${allocation.reason}`);
|
|
81
|
+
}
|
|
82
|
+
return allocation;
|
|
83
|
+
}
|
|
84
|
+
function paintPencilSurface(canvas, borderCanvas, width, height, variant, seed, allocation, theme, radius) {
|
|
75
85
|
const bleed = variant === 'wash' ? 12 : 0;
|
|
76
|
-
|
|
77
|
-
canvas.
|
|
86
|
+
const renderDpr = allocation.dpr;
|
|
87
|
+
resizeSurfaceBitmap(canvas, allocation.width, allocation.height);
|
|
78
88
|
const context = canvas.getContext('2d');
|
|
79
|
-
if (!context)
|
|
89
|
+
if (!context) {
|
|
90
|
+
releaseSurfaceBitmaps(canvas, borderCanvas);
|
|
80
91
|
throw new Error('Canvas 2D is unavailable');
|
|
92
|
+
}
|
|
93
|
+
if (borderCanvas)
|
|
94
|
+
resizeSurfaceBitmap(borderCanvas, allocation.width, allocation.height);
|
|
81
95
|
context.setTransform(canvas.width / (width + bleed * 2), 0, 0, canvas.height / (height + bleed * 2), 0, 0);
|
|
82
96
|
if (variant === 'wash') {
|
|
83
|
-
if (borderCanvas) {
|
|
84
|
-
borderCanvas.width = canvas.width;
|
|
85
|
-
borderCanvas.height = canvas.height;
|
|
86
|
-
}
|
|
87
97
|
context.translate(bleed, bleed);
|
|
88
98
|
drawPencilWash(context, { x: 0, y: 0, width, height }, theme.wash, seed, {
|
|
89
99
|
strength: theme.washSettings?.strength ?? pencilRenderDefaults.washStrength / 0.48,
|
|
@@ -93,40 +103,28 @@ export function drawPencilSurface(canvas, borderCanvas, width, height, variant,
|
|
|
93
103
|
inset: 2,
|
|
94
104
|
radius: 8,
|
|
95
105
|
edgeReach: 8,
|
|
96
|
-
dpr,
|
|
106
|
+
dpr: renderDpr,
|
|
97
107
|
});
|
|
98
108
|
return undefined;
|
|
99
109
|
}
|
|
100
110
|
if (variant === 'selection') {
|
|
101
|
-
if (borderCanvas) {
|
|
102
|
-
borderCanvas.width = canvas.width;
|
|
103
|
-
borderCanvas.height = canvas.height;
|
|
104
|
-
}
|
|
105
111
|
drawPencilWash(context, { x: 0, y: 0, width, height }, theme.wash, seed, {
|
|
106
112
|
strength: (theme.dark ? PENCIL_SELECTION_WASH.darkStrength : PENCIL_SELECTION_WASH.lightStrength) * (theme.washSettings?.strength ?? pencilRenderDefaults.washStrength / 0.48),
|
|
107
113
|
spacing: theme.washSettings?.spacing ?? PENCIL_SELECTION_WASH.spacing,
|
|
108
114
|
roughness: theme.washSettings?.roughness ?? PENCIL_SELECTION_WASH.roughness,
|
|
109
115
|
inset: PENCIL_SELECTION_WASH.inset,
|
|
110
|
-
radius: PENCIL_SELECTION_WASH.radius,
|
|
116
|
+
radius: radius !== undefined && Number.isFinite(radius) ? Math.max(0, Math.min(radius, 8)) : PENCIL_SELECTION_WASH.radius,
|
|
111
117
|
edgeReach: PENCIL_SELECTION_WASH.edgeReach,
|
|
112
118
|
angle: theme.washSettings?.angle ?? PENCIL_SELECTION_WASH.angle,
|
|
113
|
-
dpr,
|
|
119
|
+
dpr: renderDpr,
|
|
114
120
|
});
|
|
115
121
|
return undefined;
|
|
116
122
|
}
|
|
117
123
|
if (variant === 'ghost') {
|
|
118
|
-
if (borderCanvas) {
|
|
119
|
-
borderCanvas.width = canvas.width;
|
|
120
|
-
borderCanvas.height = canvas.height;
|
|
121
|
-
}
|
|
122
124
|
return undefined;
|
|
123
125
|
}
|
|
124
126
|
if (variant === 'full') {
|
|
125
|
-
|
|
126
|
-
borderCanvas.width = canvas.width;
|
|
127
|
-
borderCanvas.height = canvas.height;
|
|
128
|
-
}
|
|
129
|
-
context.fillStyle = paperPattern(context, theme.paper, theme.dark, dpr);
|
|
127
|
+
context.fillStyle = paperPattern(context, theme.paper, theme.dark, renderDpr);
|
|
130
128
|
context.fillRect(0, 0, width, height);
|
|
131
129
|
return undefined;
|
|
132
130
|
}
|
|
@@ -144,7 +142,7 @@ export function drawPencilSurface(canvas, borderCanvas, width, height, variant,
|
|
|
144
142
|
context.lineTo(...point);
|
|
145
143
|
context.closePath();
|
|
146
144
|
context.clip();
|
|
147
|
-
context.fillStyle = paperPattern(context, theme.paper, theme.dark,
|
|
145
|
+
context.fillStyle = paperPattern(context, theme.paper, theme.dark, renderDpr);
|
|
148
146
|
context.fillRect(0, 0, width, height);
|
|
149
147
|
if (strength) {
|
|
150
148
|
drawPencilWash(context, { x: 0, y: 0, width, height }, theme.wash, seed, {
|
|
@@ -152,15 +150,13 @@ export function drawPencilSurface(canvas, borderCanvas, width, height, variant,
|
|
|
152
150
|
...theme.washSettings,
|
|
153
151
|
strength: strength * (theme.washSettings?.strength ?? pencilRenderDefaults.washStrength / 0.48),
|
|
154
152
|
radius: resolvedRadius,
|
|
155
|
-
dpr,
|
|
153
|
+
dpr: renderDpr,
|
|
156
154
|
});
|
|
157
155
|
}
|
|
158
156
|
context.restore();
|
|
159
157
|
}
|
|
160
158
|
let borderContext = context;
|
|
161
159
|
if (borderCanvas) {
|
|
162
|
-
borderCanvas.width = canvas.width;
|
|
163
|
-
borderCanvas.height = canvas.height;
|
|
164
160
|
const separateContext = borderCanvas.getContext('2d');
|
|
165
161
|
if (separateContext) {
|
|
166
162
|
separateContext.setTransform(canvas.width / width, 0, 0, canvas.height / height, 0, 0);
|
|
@@ -176,3 +172,66 @@ export function drawPencilSurface(canvas, borderCanvas, width, height, variant,
|
|
|
176
172
|
});
|
|
177
173
|
return points;
|
|
178
174
|
}
|
|
175
|
+
const surfaceCache = new BoundedCache(64, 16 * 1024 * 1024);
|
|
176
|
+
// Admit only a repeat within the recent working set. Cold mounts and long scans
|
|
177
|
+
// must not pay for snapshots that will be evicted before they are ever reused.
|
|
178
|
+
const surfaceRequests = new BoundedCache(64, 64);
|
|
179
|
+
function copyCanvas(source, target) {
|
|
180
|
+
resizeSurfaceBitmap(target, source.width, source.height);
|
|
181
|
+
const context = target.getContext('2d');
|
|
182
|
+
if (!context || context.isContextLost?.())
|
|
183
|
+
throw new Error('Canvas 2D is unavailable');
|
|
184
|
+
context.drawImage(source, 0, 0);
|
|
185
|
+
}
|
|
186
|
+
export function drawPencilSurface(canvas, borderCanvas, width, height, variant, seed, dpr, theme, radius) {
|
|
187
|
+
// Transparent surfaces have no pixels to allocate or cache.
|
|
188
|
+
if (variant === 'ghost') {
|
|
189
|
+
canvas.width = canvas.height = 0;
|
|
190
|
+
if (borderCanvas)
|
|
191
|
+
borderCanvas.width = borderCanvas.height = 0;
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
const bleed = variant === 'wash' ? 12 : 0;
|
|
195
|
+
const allocation = allocateSurface(canvas, borderCanvas, width, height, dpr, bleed);
|
|
196
|
+
const key = JSON.stringify([
|
|
197
|
+
PENCIL_RENDERER_VERSION, variant, width, height, seed, allocation.dpr, Boolean(borderCanvas), radius,
|
|
198
|
+
theme.paper, theme.dark, theme.ink, theme.wash, theme.strokeWidth, theme.strokeRoughness,
|
|
199
|
+
theme.washSettings?.strength, theme.washSettings?.spacing,
|
|
200
|
+
theme.washSettings?.roughness, theme.washSettings?.angle,
|
|
201
|
+
]);
|
|
202
|
+
const cached = surfaceCache.get(key);
|
|
203
|
+
if (cached?.valid && canvasIsUsable(cached.ink) && (!cached.border || canvasIsUsable(cached.border))) {
|
|
204
|
+
copyCanvas(cached.ink, canvas);
|
|
205
|
+
if (cached.border && borderCanvas)
|
|
206
|
+
copyCanvas(cached.border, borderCanvas);
|
|
207
|
+
return cached.points?.map(([x, y]) => [x, y]);
|
|
208
|
+
}
|
|
209
|
+
if (cached)
|
|
210
|
+
surfaceCache.delete(key);
|
|
211
|
+
const points = paintPencilSurface(canvas, borderCanvas, width, height, variant, seed, allocation, theme, radius);
|
|
212
|
+
const cost = canvasByteCost(canvas) + (borderCanvas ? canvasByteCost(borderCanvas) : 0);
|
|
213
|
+
const repeated = surfaceRequests.get(key) === true;
|
|
214
|
+
surfaceRequests.set(key, true, 1);
|
|
215
|
+
// Keep the cache useful for repeated controls, not a single large panel.
|
|
216
|
+
if (repeated && cost <= 2 * 1024 * 1024) {
|
|
217
|
+
try {
|
|
218
|
+
const ink = document.createElement('canvas');
|
|
219
|
+
const border = borderCanvas ? document.createElement('canvas') : null;
|
|
220
|
+
copyCanvas(canvas, ink);
|
|
221
|
+
if (border && borderCanvas)
|
|
222
|
+
copyCanvas(borderCanvas, border);
|
|
223
|
+
const image = { ink, border, points: points?.map(([x, y]) => [x, y]), valid: true };
|
|
224
|
+
// A restored context can report healthy after losing all of its pixels.
|
|
225
|
+
// Keep that entry invalid even if loss and restoration precede the next hit.
|
|
226
|
+
const invalidate = () => { image.valid = false; };
|
|
227
|
+
ink.addEventListener?.('contextlost', invalidate, { once: true });
|
|
228
|
+
border?.addEventListener?.('contextlost', invalidate, { once: true });
|
|
229
|
+
surfaceCache.set(key, image, cost);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Caching is optional: a snapshot allocation failure must not discard a
|
|
233
|
+
// successfully painted live surface.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return points;
|
|
237
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function surfaceIsNearViewport(root: HTMLElement, margin?: number): boolean;
|
|
2
|
+
/** One observer per document, explicitly released after the final subscription. */
|
|
3
|
+
export declare function observeSurfaceViewport(root: HTMLElement, refresh: () => void): (() => void) | undefined;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Keep visible/prewarm surfaces synchronous. An offscreen surface retains DOM,
|
|
2
|
+
// geometry and any existing bitmap; only its decorative paint is deferred.
|
|
3
|
+
export function surfaceIsNearViewport(root, margin = 240) {
|
|
4
|
+
const view = root.ownerDocument?.defaultView;
|
|
5
|
+
if (!view || !Number.isFinite(view.innerWidth) || !Number.isFinite(view.innerHeight)
|
|
6
|
+
|| typeof root.getBoundingClientRect !== 'function')
|
|
7
|
+
return true;
|
|
8
|
+
const rect = root.getBoundingClientRect();
|
|
9
|
+
if (![rect.left, rect.right, rect.top, rect.bottom].every(Number.isFinite))
|
|
10
|
+
return true;
|
|
11
|
+
return rect.bottom >= -margin && rect.top <= view.innerHeight + margin
|
|
12
|
+
&& rect.right >= -margin && rect.left <= view.innerWidth + margin;
|
|
13
|
+
}
|
|
14
|
+
const documents = new WeakMap();
|
|
15
|
+
/** One observer per document, explicitly released after the final subscription. */
|
|
16
|
+
export function observeSurfaceViewport(root, refresh) {
|
|
17
|
+
if (typeof IntersectionObserver === 'undefined' || !root.ownerDocument)
|
|
18
|
+
return undefined;
|
|
19
|
+
const document = root.ownerDocument;
|
|
20
|
+
let state = documents.get(document);
|
|
21
|
+
if (!state) {
|
|
22
|
+
const callbacks = new Map();
|
|
23
|
+
try {
|
|
24
|
+
const observer = new IntersectionObserver(entries => {
|
|
25
|
+
for (const entry of entries)
|
|
26
|
+
if (entry.isIntersecting)
|
|
27
|
+
callbacks.get(entry.target)?.();
|
|
28
|
+
}, { rootMargin: '240px', threshold: 0 });
|
|
29
|
+
state = { observer, callbacks };
|
|
30
|
+
documents.set(document, state);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const current = state;
|
|
37
|
+
current.callbacks.set(root, refresh);
|
|
38
|
+
try {
|
|
39
|
+
current.observer.observe(root);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
current.callbacks.delete(root);
|
|
43
|
+
if (!current.callbacks.size) {
|
|
44
|
+
current.observer.disconnect();
|
|
45
|
+
documents.delete(document);
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return () => {
|
|
50
|
+
// Ignore a stale unsubscribe if a newer owner registered the same element.
|
|
51
|
+
if (current.callbacks.get(root) !== refresh)
|
|
52
|
+
return;
|
|
53
|
+
current.callbacks.delete(root);
|
|
54
|
+
current.observer.unobserve(root);
|
|
55
|
+
if (!current.callbacks.size) {
|
|
56
|
+
current.observer.disconnect();
|
|
57
|
+
documents.delete(document);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -16,9 +16,12 @@ export interface PencilTextSprite {
|
|
|
16
16
|
}
|
|
17
17
|
export declare const pencilLettering: PencilLetteringSettings;
|
|
18
18
|
export declare const pencilHandwriting = "\"Trip Map Hand\",\"HanziPen SC\",\"Hannotate SC\",\"Kaiti SC\",\"STKaiti\",cursive";
|
|
19
|
+
export declare function boundedTextResolution(width: number, height: number, dpr: number): number;
|
|
19
20
|
export declare function renderPencilText(text: string, options: {
|
|
20
21
|
size: number;
|
|
21
22
|
family?: string;
|
|
23
|
+
weight?: string;
|
|
24
|
+
style?: string;
|
|
22
25
|
color: string;
|
|
23
26
|
seed: number;
|
|
24
27
|
dpr: number;
|