@inkra/react 0.3.0 → 0.4.3
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 +15 -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 +126 -31
- package/dist/calendar/index.d.ts +4 -1
- package/dist/calendar/index.js +31 -12
- package/dist/carousel/index.js +13 -6
- package/dist/choice-grid/index.d.ts +6 -1
- package/dist/choice-grid/index.js +61 -33
- package/dist/color-picker/GradientEditor.d.ts +39 -0
- package/dist/color-picker/GradientEditor.js +63 -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 +134 -32
- 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 +12 -3
- package/dist/context-menu/index.js +1 -1
- package/dist/data-table/index.d.ts +7 -1
- package/dist/data-table/index.js +64 -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 +10 -9
- package/dist/day-agenda/index.d.ts +6 -1
- package/dist/day-agenda/index.js +46 -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 +5 -5
- package/dist/drawer/index.js +4 -3
- package/dist/editable/index.d.ts +24 -2
- package/dist/editable/index.js +142 -37
- 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/Textarea.d.ts +3 -1
- package/dist/form/Textarea.js +6 -4
- package/dist/hotkey/index.d.ts +1 -0
- package/dist/hotkey/index.js +6 -4
- package/dist/hover-card/index.js +1 -1
- package/dist/image-cropper/index.js +4 -2
- package/dist/image-viewer/VideoResource.d.ts +21 -0
- package/dist/image-viewer/VideoResource.js +194 -0
- package/dist/image-viewer/index.d.ts +14 -0
- package/dist/image-viewer/index.js +118 -65
- package/dist/image-viewer/lucideGeometry.d.ts +3 -0
- package/dist/image-viewer/lucideGeometry.js +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/input-number/index.js +9 -4
- 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 +2 -0
- package/dist/internal/textRenderer.js +3 -3
- package/dist/internal/usePencilSurface.js +17 -1
- package/dist/internal/wash.js +4 -3
- package/dist/journey-map/terrain.js +10 -5
- package/dist/layout/index.js +1 -1
- package/dist/log/index.js +29 -22
- 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 +175 -0
- package/dist/node-graph/index.d.ts +2 -0
- package/dist/node-graph/index.js +49 -25
- package/dist/number-ticker/precision.js +9 -2
- package/dist/overflow-list/index.js +5 -2
- package/dist/overlay/Overlay.js +1 -1
- 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 +6 -4
- package/dist/pan-zoom/index.d.ts +2 -0
- package/dist/pan-zoom/index.js +7 -6
- 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 +40 -20
- package/dist/rating/index.d.ts +4 -0
- package/dist/rating/index.js +128 -24
- package/dist/resizable/index.js +1 -1
- 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 +61 -6
- package/dist/select/Select.d.ts +1 -0
- package/dist/select/Select.js +3 -1
- package/dist/select/StepperSelect.d.ts +29 -0
- package/dist/select/StepperSelect.js +119 -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 +6 -2
- package/dist/sparkline/index.js +17 -2
- package/dist/sparkline/model.js +11 -4
- 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 +557 -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 +2094 -421
- package/dist/tabs/index.d.ts +9 -0
- package/dist/tabs/index.js +66 -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 +70 -26
- package/dist/time-picker/index.js +38 -20
- package/dist/toast/index.d.ts +19 -11
- package/dist/toast/index.js +48 -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 +93 -29
- 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 +5 -5
- package/dist/tree-select/index.js +67 -30
- 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 +30 -16
- package/dist/virtual-list/index.js +7 -5
- package/package.json +413 -3
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { __iconData as chevronLeftIcon } from 'lucide-react/dist/esm/icons/chevron-left.mjs';
|
|
3
3
|
import { __iconData as chevronRightIcon } from 'lucide-react/dist/esm/icons/chevron-right.mjs';
|
|
4
4
|
import { __iconData as imageOffIcon } from 'lucide-react/dist/esm/icons/image-off.mjs';
|
|
5
|
+
import { __iconData as playIcon } from 'lucide-react/dist/esm/icons/play.mjs';
|
|
5
6
|
import { __iconData as rotateIcon } from 'lucide-react/dist/esm/icons/rotate-cw.mjs';
|
|
6
7
|
import { __iconData as zoomInIcon } from 'lucide-react/dist/esm/icons/zoom-in.mjs';
|
|
7
8
|
import { __iconData as zoomOutIcon } from 'lucide-react/dist/esm/icons/zoom-out.mjs';
|
|
@@ -11,14 +12,17 @@ import { PencilIcon } from '../PencilIcon.js';
|
|
|
11
12
|
import { PencilSurface } from '../PencilSurface.js';
|
|
12
13
|
import { PencilText } from '../PencilText.js';
|
|
13
14
|
import { PanZoom } from '../pan-zoom/index.js';
|
|
14
|
-
import { OverlayBackdrop, OverlayContent, OverlayDescription,
|
|
15
|
+
import { OverlayBackdrop, OverlayCloseButton, OverlayContent, OverlayDescription, OverlayPortal, OverlayRoot, OverlayTitle, OverlayTrigger, } from '../overlay/index.js';
|
|
16
|
+
import { lucideNodesToPencilPaths } from './lucideGeometry.js';
|
|
15
17
|
import { nextImageViewerIndex, resolveImageViewerSelection } from './model.js';
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
18
|
+
import { VideoResource } from './VideoResource.js';
|
|
19
|
+
const chevronLeftPaths = lucideNodesToPencilPaths(chevronLeftIcon.node);
|
|
20
|
+
const chevronRightPaths = lucideNodesToPencilPaths(chevronRightIcon.node);
|
|
21
|
+
const imageOffPaths = lucideNodesToPencilPaths(imageOffIcon.node);
|
|
22
|
+
const playPaths = lucideNodesToPencilPaths(playIcon.node);
|
|
23
|
+
const rotatePaths = lucideNodesToPencilPaths(rotateIcon.node);
|
|
24
|
+
const zoomInPaths = lucideNodesToPencilPaths(zoomInIcon.node);
|
|
25
|
+
const zoomOutPaths = lucideNodesToPencilPaths(zoomOutIcon.node);
|
|
22
26
|
const ImageViewerContext = createContext(null);
|
|
23
27
|
function useImageViewerContext() {
|
|
24
28
|
const context = useContext(ImageViewerContext);
|
|
@@ -73,9 +77,10 @@ export function ImageViewerRoot({ activeId, defaultActiveId, defaultOpen = false
|
|
|
73
77
|
activeItem: selection.index < 0 ? null : items[selection.index],
|
|
74
78
|
activeIndex: selection.index,
|
|
75
79
|
loop,
|
|
80
|
+
open: openControlled ? Boolean(open) : internalOpen,
|
|
76
81
|
selectIndex,
|
|
77
82
|
step,
|
|
78
|
-
}), [items, loop, selectIndex, selection.index, step]);
|
|
83
|
+
}), [internalOpen, items, loop, open, openControlled, selectIndex, selection.index, step]);
|
|
79
84
|
return (_jsx(ImageViewerContext.Provider, { value: context, children: _jsx(OverlayRoot, { ...props, open: openControlled ? open : internalOpen, onOpenChange: (nextOpen) => {
|
|
80
85
|
if (!openControlled)
|
|
81
86
|
setInternalOpen(nextOpen);
|
|
@@ -108,23 +113,46 @@ const defaultLabels = {
|
|
|
108
113
|
function directionFor(node) {
|
|
109
114
|
return getComputedStyle(node).direction === 'rtl' ? 'rtl' : 'ltr';
|
|
110
115
|
}
|
|
111
|
-
export const ImageViewerContent = forwardRef(function ImageViewerContent({ className, emptyState, labels: labelOverrides, onOpenAutoFocus, renderImage, rotatable = false, showThumbnails = true, zoomable = true, minScale = 0.5, maxScale = 6, wheel = 'modifier', ...props }, forwardedRef) {
|
|
112
|
-
const { activeIndex, activeItem, items, loop, selectIndex, step } = useImageViewerContext();
|
|
116
|
+
export const ImageViewerContent = forwardRef(function ImageViewerContent({ className, emptyState, labels: labelOverrides, onOpenAutoFocus, renderImage, rotatable = false, showThumbnails = true, zoomable = true, minScale = 0.5, maxScale = 6, videoLabels, wheel = 'modifier', ...props }, forwardedRef) {
|
|
117
|
+
const { activeIndex, activeItem, items, loop, open, selectIndex, step } = useImageViewerContext();
|
|
113
118
|
const labels = { ...defaultLabels, ...labelOverrides };
|
|
114
119
|
const stageRef = useRef(null);
|
|
120
|
+
const thumbnailsRef = useRef(null);
|
|
115
121
|
const thumbnailRefs = useRef(new Map());
|
|
116
122
|
const panZoomRef = useRef(null);
|
|
117
|
-
const
|
|
123
|
+
const focusFrameRef = useRef(null);
|
|
124
|
+
const fitFrameRef = useRef(null);
|
|
125
|
+
useEffect(() => () => {
|
|
126
|
+
if (focusFrameRef.current !== null)
|
|
127
|
+
cancelAnimationFrame(focusFrameRef.current);
|
|
128
|
+
if (fitFrameRef.current !== null)
|
|
129
|
+
cancelAnimationFrame(fitFrameRef.current);
|
|
130
|
+
}, []);
|
|
131
|
+
const [failedResources, setFailedResources] = useState(() => new Set());
|
|
118
132
|
const [attempts, setAttempts] = useState({});
|
|
119
133
|
const [rotation, setRotation] = useState(0);
|
|
120
134
|
const total = items.length;
|
|
121
135
|
const atStart = activeIndex <= 0;
|
|
122
136
|
const atEnd = activeIndex >= total - 1;
|
|
137
|
+
const activeItemType = activeItem?.type ?? 'image';
|
|
138
|
+
const activeItemKey = activeItem ? JSON.stringify([activeItemType, activeItem.id, activeItem.src]) : undefined;
|
|
139
|
+
const activeResourceRef = useRef(activeItemKey);
|
|
140
|
+
activeResourceRef.current = activeItemKey;
|
|
123
141
|
useEffect(() => {
|
|
124
|
-
|
|
125
|
-
|
|
142
|
+
const thumbnail = activeItem ? thumbnailRefs.current.get(activeItem.id) : undefined;
|
|
143
|
+
const viewport = thumbnailsRef.current;
|
|
144
|
+
if (thumbnail && viewport) {
|
|
145
|
+
const item = thumbnail.getBoundingClientRect();
|
|
146
|
+
const clip = viewport.getBoundingClientRect();
|
|
147
|
+
if (item.left < clip.left)
|
|
148
|
+
viewport.scrollLeft -= clip.left - item.left;
|
|
149
|
+
else if (item.right > clip.right)
|
|
150
|
+
viewport.scrollLeft += item.right - clip.right;
|
|
151
|
+
}
|
|
126
152
|
setRotation(0);
|
|
127
|
-
|
|
153
|
+
setFailedResources(current => activeItemKey && current.has(activeItemKey) ? new Set([activeItemKey]) : new Set());
|
|
154
|
+
setAttempts(current => activeItemKey && current[activeItemKey] !== undefined ? { [activeItemKey]: current[activeItemKey] } : {});
|
|
155
|
+
}, [activeItemKey]);
|
|
128
156
|
const handleStageKeyDown = (event) => {
|
|
129
157
|
if (event.target !== event.currentTarget)
|
|
130
158
|
return;
|
|
@@ -149,72 +177,97 @@ export const ImageViewerContent = forwardRef(function ImageViewerContent({ class
|
|
|
149
177
|
const selectThumbnail = (index, reason = 'thumbnail') => {
|
|
150
178
|
selectIndex(index, reason);
|
|
151
179
|
const item = items[index];
|
|
152
|
-
if (
|
|
153
|
-
|
|
180
|
+
if (focusFrameRef.current !== null)
|
|
181
|
+
cancelAnimationFrame(focusFrameRef.current);
|
|
182
|
+
focusFrameRef.current = item ? requestAnimationFrame(() => {
|
|
183
|
+
focusFrameRef.current = null;
|
|
184
|
+
thumbnailRefs.current.get(item.id)?.focus();
|
|
185
|
+
}) : null;
|
|
154
186
|
};
|
|
155
187
|
const renderDefaultImage = (item, { key, ...imageProps }) => (_jsx("img", { ...imageProps, draggable: false }, key));
|
|
156
|
-
const failed =
|
|
157
|
-
const imageProps = activeItem ? {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
188
|
+
const failed = activeItemKey ? failedResources.has(activeItemKey) : false;
|
|
189
|
+
const imageProps = activeItem && activeItemKey && activeItemType === 'image' ? (() => {
|
|
190
|
+
const resourceKey = activeItemKey;
|
|
191
|
+
return {
|
|
192
|
+
alt: activeItem.alt,
|
|
193
|
+
className: 'inkra-image-viewer__image',
|
|
194
|
+
height: activeItem.height,
|
|
195
|
+
key: `${resourceKey}:${attempts[resourceKey] ?? 0}`,
|
|
196
|
+
onError: () => setFailedResources(current => new Set(current).add(resourceKey)),
|
|
197
|
+
onLoad: () => setFailedResources(current => {
|
|
198
|
+
if (!current.has(resourceKey))
|
|
199
|
+
return current;
|
|
200
|
+
const next = new Set(current);
|
|
201
|
+
next.delete(resourceKey);
|
|
202
|
+
return next;
|
|
203
|
+
}),
|
|
204
|
+
src: activeItem.src,
|
|
205
|
+
width: activeItem.width,
|
|
206
|
+
};
|
|
207
|
+
})() : null;
|
|
173
208
|
return (_jsxs(OverlayPortal, { children: [_jsx(OverlayBackdrop, {}), _jsxs(OverlayContent, { ...props, className: ['inkra-image-viewer', className].filter(Boolean).join(' '), layout: "sections", onOpenAutoFocus: (event) => {
|
|
174
209
|
onOpenAutoFocus?.(event);
|
|
175
210
|
if (event.defaultPrevented)
|
|
176
211
|
return;
|
|
177
212
|
event.preventDefault();
|
|
178
213
|
stageRef.current?.focus();
|
|
179
|
-
}, ref: forwardedRef, surfaceRadius:
|
|
180
|
-
|
|
181
|
-
|
|
214
|
+
}, ref: forwardedRef, surfaceRadius: 0, surfaceVariant: "ghost", children: [_jsx(OverlayTitle, { className: "inkra-image-viewer__accessible-label", children: labels.title }), _jsx(OverlayDescription, { className: "inkra-image-viewer__accessible-label", children: labels.description }), _jsxs("div", { className: "inkra-image-viewer__close-control", children: [_jsx(PencilSurface, { "aria-hidden": "true", className: "inkra-image-viewer__close-paper", radius: 8, seed: "image-viewer-close-paper", variant: "paper" }), _jsx(OverlayCloseButton, { className: "inkra-image-viewer__close", label: labels.close })] }), _jsx("div", { className: "inkra-image-viewer__body", children: activeItem && activeItemKey ? _jsx(_Fragment, { children: _jsxs("div", { "aria-label": activeItem.alt, className: "inkra-image-viewer__stage", onKeyDown: handleStageKeyDown, ref: stageRef, tabIndex: 0, children: [_jsx(PencilSurface, { "aria-hidden": "true", className: "inkra-image-viewer__stage-focus", radius: 6, seed: `image-viewer-stage-${activeItem.id}`, variant: "outline" }), failed ? _jsxs("div", { className: "inkra-image-viewer__state", role: "alert", children: [_jsx(PencilIcon, { decorative: true, paths: imageOffPaths, size: 48, sourceSize: 24 }), _jsx(PencilText, { as: "h3", children: labels.failedTitle }), _jsx(PencilText, { as: "p", children: labels.failedDescription(activeItem) }), _jsx(Button, { variant: "outline", onClick: () => {
|
|
215
|
+
if (!activeItemKey)
|
|
216
|
+
return;
|
|
217
|
+
const failedKey = activeItemKey;
|
|
218
|
+
setFailedResources(current => {
|
|
182
219
|
const next = new Set(current);
|
|
183
|
-
next.delete(
|
|
220
|
+
next.delete(failedKey);
|
|
184
221
|
return next;
|
|
185
222
|
});
|
|
186
|
-
setAttempts(current => ({ ...current, [
|
|
187
|
-
}, children: labels.retry })] }) :
|
|
223
|
+
setAttempts(current => ({ ...current, [failedKey]: (current[failedKey] ?? 0) + 1 }));
|
|
224
|
+
}, children: labels.retry })] }) : activeItemType === 'video' && open ? _jsx(VideoResource, { alt: activeItem.alt, labels: videoLabels, onError: () => setFailedResources(current => new Set(current).add(activeItemKey)), onLoad: () => setFailedResources(current => {
|
|
225
|
+
if (!current.has(activeItemKey))
|
|
226
|
+
return current;
|
|
227
|
+
const next = new Set(current);
|
|
228
|
+
next.delete(activeItemKey);
|
|
229
|
+
return next;
|
|
230
|
+
}), poster: activeItem.poster, src: activeItem.src }, activeItemKey) : imageProps ? _jsx(PanZoom, { "aria-label": labels.panZoom(activeItem), centerOnInit: true, className: "inkra-image-viewer__pan-zoom", fitOnInit: "contain", keyboard: zoomable, maxScale: maxScale, minScale: minScale, pan: zoomable, pinch: zoomable, readOnly: !zoomable, ref: panZoomRef, wheel: zoomable ? wheel : 'disabled', children: _jsx("div", { className: "inkra-image-viewer__image-transform", style: { transform: `rotate(${rotation}deg)` }, children: (renderImage ?? renderDefaultImage)(activeItem, imageProps) }) }, activeItemKey) : null, total > 1 && activeItemType === 'image' ? _jsxs("div", { className: "inkra-image-viewer__navigation", children: [_jsx(Button, { "aria-label": labels.previous, className: "inkra-image-viewer__previous", disabled: !loop && atStart, onClick: () => step(-1), size: "icon", variant: "secondary", children: _jsx(PencilIcon, { decorative: true, paths: chevronLeftPaths, size: 30, sourceSize: 24 }) }), _jsx(Button, { "aria-label": labels.next, className: "inkra-image-viewer__next", disabled: !loop && atEnd, onClick: () => step(1), size: "icon", variant: "secondary", children: _jsx(PencilIcon, { decorative: true, paths: chevronRightPaths, size: 30, sourceSize: 24 }) })] }) : null] }) }) : _jsxs("div", { className: "inkra-image-viewer__state", ref: stageRef, tabIndex: 0, children: [_jsx(PencilSurface, { "aria-hidden": "true", className: "inkra-image-viewer__stage-focus", radius: 6, seed: "image-viewer-empty-stage", variant: "outline" }), emptyState ?? _jsxs(_Fragment, { children: [_jsx(PencilIcon, { decorative: true, paths: imageOffPaths, size: 48, sourceSize: 24 }), _jsx(PencilText, { as: "h3", children: labels.emptyTitle }), _jsx(PencilText, { as: "p", children: labels.emptyDescription })] })] }) }), (activeItem || (showThumbnails && total > 1)) ? _jsxs("div", { className: "inkra-image-viewer__bottom", children: [_jsxs("div", { className: "inkra-image-viewer__bottom-row", children: [total > 0 ? _jsx(PencilText, { as: "span", className: "inkra-image-viewer__counter", dir: "auto", children: labels.imagePosition(activeIndex + 1, total) }) : null, activeItem && activeItemType === 'image' && !failed && (zoomable || rotatable) ? _jsxs("div", { className: "inkra-image-viewer__tools", role: "group", "aria-label": labels.panZoom(activeItem), "data-pan-zoom-no-drag": true, children: [_jsx(PencilSurface, { "aria-hidden": "true", className: "inkra-image-viewer__tools-paper", radius: 8, seed: `image-viewer-tools-${activeItem.id}`, variant: "paper" }), zoomable ? _jsxs(_Fragment, { children: [_jsx(Button, { "aria-label": labels.zoomOut, onClick: () => void panZoomRef.current?.zoomOut(), size: "icon-sm", variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: zoomOutPaths, size: 24, sourceSize: 24 }) }), _jsx(Button, { "aria-label": labels.resetZoom, onClick: () => void panZoomRef.current?.reset(), size: "sm", variant: "outline", children: "100%" }), _jsx(Button, { "aria-label": labels.zoomIn, onClick: () => void panZoomRef.current?.zoomIn(), size: "icon-sm", variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: zoomInPaths, size: 24, sourceSize: 24 }) })] }) : null, rotatable ? _jsx(Button, { "aria-label": labels.rotate, onClick: () => {
|
|
188
231
|
setRotation(current => (current + 90) % 360);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
232
|
+
if (fitFrameRef.current !== null)
|
|
233
|
+
cancelAnimationFrame(fitFrameRef.current);
|
|
234
|
+
const resourceKey = activeItemKey;
|
|
235
|
+
fitFrameRef.current = requestAnimationFrame(() => {
|
|
236
|
+
fitFrameRef.current = null;
|
|
237
|
+
if (activeResourceRef.current === resourceKey)
|
|
238
|
+
void panZoomRef.current?.fit({ mode: 'contain' });
|
|
239
|
+
});
|
|
240
|
+
}, size: "icon-sm", variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: rotatePaths, size: 24, sourceSize: 24 }) }) : null] }) : null, activeItem && activeItemType === 'video' && total > 1 ? _jsxs("div", { className: "inkra-image-viewer__video-navigation", role: "group", "aria-label": labels.thumbnails, children: [_jsx(Button, { "aria-label": labels.previous, disabled: !loop && atStart, onClick: () => step(-1), size: "icon-sm", variant: "secondary", children: _jsx(PencilIcon, { decorative: true, paths: chevronLeftPaths, size: 24, sourceSize: 24 }) }), _jsx(Button, { "aria-label": labels.next, disabled: !loop && atEnd, onClick: () => step(1), size: "icon-sm", variant: "secondary", children: _jsx(PencilIcon, { decorative: true, paths: chevronRightPaths, size: 24, sourceSize: 24 }) })] }) : null] }), showThumbnails && total > 1 ? _jsx("div", { "aria-label": labels.thumbnails, className: "inkra-image-viewer__thumbnails", ref: thumbnailsRef, role: "listbox", children: items.map((item, index) => {
|
|
241
|
+
const selected = index === activeIndex;
|
|
242
|
+
return _jsxs("button", { "aria-label": labels.thumbnail(item, index + 1, total), "aria-selected": selected, className: "inkra-image-viewer__thumbnail", onClick: () => selectIndex(index, 'thumbnail'), onKeyDown: (event) => {
|
|
243
|
+
const rtl = directionFor(event.currentTarget) === 'rtl';
|
|
244
|
+
let nextIndex = index;
|
|
245
|
+
if (event.key === 'ArrowLeft')
|
|
246
|
+
nextIndex = nextImageViewerIndex(total, index, rtl ? 1 : -1, loop);
|
|
247
|
+
else if (event.key === 'ArrowRight')
|
|
248
|
+
nextIndex = nextImageViewerIndex(total, index, rtl ? -1 : 1, loop);
|
|
249
|
+
else if (event.key === 'Home')
|
|
250
|
+
nextIndex = 0;
|
|
251
|
+
else if (event.key === 'End')
|
|
252
|
+
nextIndex = total - 1;
|
|
253
|
+
else
|
|
254
|
+
return;
|
|
255
|
+
event.preventDefault();
|
|
256
|
+
selectThumbnail(nextIndex, event.key === 'Home' ? 'home' : event.key === 'End' ? 'end' : 'thumbnail');
|
|
257
|
+
}, ref: (node) => {
|
|
258
|
+
if (node)
|
|
259
|
+
thumbnailRefs.current.set(item.id, node);
|
|
260
|
+
else
|
|
261
|
+
thumbnailRefs.current.delete(item.id);
|
|
262
|
+
}, role: "option", tabIndex: selected ? 0 : -1, type: "button", children: [_jsx(PencilSurface, { as: "span", className: "inkra-image-viewer__thumbnail-surface", radius: 6, variant: selected ? 'selection' : 'outline', children: (item.thumbnailSrc ?? item.poster ?? (item.type === 'video' ? undefined : item.src))
|
|
263
|
+
? _jsx("img", { alt: "", className: "inkra-image-viewer__thumbnail-image", draggable: false, src: item.thumbnailSrc ?? item.poster ?? item.src })
|
|
264
|
+
: _jsx(PencilIcon, { decorative: true, paths: item.type === 'video' ? playPaths : imageOffPaths, size: 30, sourceSize: 24 }) }), item.type === 'video' ? _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-image-viewer__thumbnail-video", radius: 6, variant: "paper", children: _jsx(PencilIcon, { decorative: true, paths: playPaths, size: 18, sourceSize: 24 }) }) : null, _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-image-viewer__thumbnail-focus", radius: 6, variant: "outline" })] }, item.id);
|
|
265
|
+
}) }) : null] }) : null] })] }));
|
|
214
266
|
});
|
|
215
|
-
export const ImageViewer = Object.freeze({
|
|
267
|
+
export const ImageViewer = /* @__PURE__ */ Object.freeze({
|
|
216
268
|
Root: ImageViewerRoot,
|
|
217
269
|
Trigger: ImageViewerTrigger,
|
|
218
270
|
Content: ImageViewerContent,
|
|
219
271
|
});
|
|
272
|
+
export const MediaViewer = ImageViewer;
|
|
220
273
|
export { nextImageViewerIndex, resolveImageViewerSelection } from './model.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function number(attributes, name) {
|
|
2
|
+
const value = Number(attributes[name]);
|
|
3
|
+
return Number.isFinite(value) ? value : 0;
|
|
4
|
+
}
|
|
5
|
+
export function lucideNodesToPencilPaths(nodes) {
|
|
6
|
+
return nodes.flatMap(([tag, attributes]) => {
|
|
7
|
+
if (tag === 'path' && attributes.d)
|
|
8
|
+
return [{ d: attributes.d }];
|
|
9
|
+
if (tag === 'line')
|
|
10
|
+
return [{ d: `M${number(attributes, 'x1')} ${number(attributes, 'y1')}L${number(attributes, 'x2')} ${number(attributes, 'y2')}` }];
|
|
11
|
+
if (tag === 'circle') {
|
|
12
|
+
const cx = number(attributes, 'cx');
|
|
13
|
+
const cy = number(attributes, 'cy');
|
|
14
|
+
const radius = number(attributes, 'r');
|
|
15
|
+
return [{ d: `M${cx - radius} ${cy}A${radius} ${radius} 0 1 0 ${cx + radius} ${cy}A${radius} ${radius} 0 1 0 ${cx - radius} ${cy}` }];
|
|
16
|
+
}
|
|
17
|
+
return [];
|
|
18
|
+
});
|
|
19
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export * from './audio-player/index.js';
|
|
|
38
38
|
export * from './carousel/index.js';
|
|
39
39
|
export * from './choice-grid/index.js';
|
|
40
40
|
export * from './color-picker/index.js';
|
|
41
|
+
export * from './color-swatch/index.js';
|
|
41
42
|
export * from './context-menu/index.js';
|
|
42
43
|
export * from './dockable-panel/index.js';
|
|
43
44
|
export * from './editable/index.js';
|
|
@@ -62,6 +63,7 @@ export * from './transfer/index.js';
|
|
|
62
63
|
export * from './tree-select/index.js';
|
|
63
64
|
export * from './typewriter/index.js';
|
|
64
65
|
export * from './virtual-list/index.js';
|
|
66
|
+
export * from './markdown/index.js';
|
|
65
67
|
export type { ButtonGroupOrientation, ButtonAlign, ButtonLoadingPosition, ButtonGroupProps, ButtonLinkProps, ButtonProps, ButtonSize, ButtonVariant, } from './Button.js';
|
|
66
68
|
export { PencilLine } from './PencilLine.js';
|
|
67
69
|
export type { PencilLineOrientation, PencilLineProps } from './PencilLine.js';
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ export * from './audio-player/index.js';
|
|
|
38
38
|
export * from './carousel/index.js';
|
|
39
39
|
export * from './choice-grid/index.js';
|
|
40
40
|
export * from './color-picker/index.js';
|
|
41
|
+
export * from './color-swatch/index.js';
|
|
41
42
|
export * from './context-menu/index.js';
|
|
42
43
|
export * from './dockable-panel/index.js';
|
|
43
44
|
export * from './editable/index.js';
|
|
@@ -62,6 +63,7 @@ export * from './transfer/index.js';
|
|
|
62
63
|
export * from './tree-select/index.js';
|
|
63
64
|
export * from './typewriter/index.js';
|
|
64
65
|
export * from './virtual-list/index.js';
|
|
66
|
+
export * from './markdown/index.js';
|
|
65
67
|
export { PencilLine } from './PencilLine.js';
|
|
66
68
|
export { PencilSurface } from './PencilSurface.js';
|
|
67
69
|
export { PencilText } from './PencilText.js';
|
|
@@ -25,9 +25,12 @@ export const InputNumber = forwardRef(function InputNumber({ classNames, control
|
|
|
25
25
|
const holdRef = useRef(null);
|
|
26
26
|
const timerRef = useRef(undefined);
|
|
27
27
|
const suppressClickRef = useRef(false);
|
|
28
|
+
const removeHoldListeners = useRef(undefined);
|
|
28
29
|
const stopHolding = useCallback(() => {
|
|
29
30
|
clearTimeout(timerRef.current);
|
|
30
31
|
timerRef.current = undefined;
|
|
32
|
+
removeHoldListeners.current?.();
|
|
33
|
+
removeHoldListeners.current = undefined;
|
|
31
34
|
const hold = holdRef.current;
|
|
32
35
|
holdRef.current = null;
|
|
33
36
|
if (hold?.button.hasPointerCapture?.(hold.pointerId)) {
|
|
@@ -37,7 +40,8 @@ export const InputNumber = forwardRef(function InputNumber({ classNames, control
|
|
|
37
40
|
catch { /* The pointer may already be inactive. */ }
|
|
38
41
|
}
|
|
39
42
|
}, []);
|
|
40
|
-
|
|
43
|
+
// Idle controls do not need global pointer, keyboard or visibility listeners.
|
|
44
|
+
const listenWhileHolding = () => {
|
|
41
45
|
const onVisibilityChange = () => { if (document.hidden)
|
|
42
46
|
stopHolding(); };
|
|
43
47
|
const onPointerEnd = (event) => {
|
|
@@ -56,8 +60,7 @@ export const InputNumber = forwardRef(function InputNumber({ classNames, control
|
|
|
56
60
|
window.addEventListener('keydown', updateModifiers);
|
|
57
61
|
window.addEventListener('keyup', updateModifiers);
|
|
58
62
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
59
|
-
|
|
60
|
-
stopHolding();
|
|
63
|
+
removeHoldListeners.current = () => {
|
|
61
64
|
window.removeEventListener('blur', stopHolding);
|
|
62
65
|
window.removeEventListener('pointerup', onPointerEnd);
|
|
63
66
|
window.removeEventListener('pointercancel', onPointerEnd);
|
|
@@ -65,7 +68,8 @@ export const InputNumber = forwardRef(function InputNumber({ classNames, control
|
|
|
65
68
|
window.removeEventListener('keyup', updateModifiers);
|
|
66
69
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
67
70
|
};
|
|
68
|
-
}
|
|
71
|
+
};
|
|
72
|
+
useEffect(() => stopHolding, [stopHolding]);
|
|
69
73
|
useBrowserLayoutEffect(() => {
|
|
70
74
|
if (isControlled && currentValue !== finiteValue(value))
|
|
71
75
|
setDraft(format(value));
|
|
@@ -136,6 +140,7 @@ export const InputNumber = forwardRef(function InputNumber({ classNames, control
|
|
|
136
140
|
if (!button.isConnected || button.disabled)
|
|
137
141
|
return;
|
|
138
142
|
holdRef.current = { button, pointerId: event.pointerId, direction, modifiers };
|
|
143
|
+
listenWhileHolding();
|
|
139
144
|
try {
|
|
140
145
|
button.setPointerCapture(event.pointerId);
|
|
141
146
|
}
|
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));
|