@widgetic/canvas 0.5.6 → 0.5.9
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/dist/canvas/Canvas.svelte +353 -49
- package/dist/canvas/Canvas.svelte.d.ts +24 -0
- package/dist/canvas/CanvasToolbar.svelte +113 -26
- package/dist/canvas/CanvasToolbar.svelte.d.ts +7 -1
- package/dist/canvas/PanZoomPanel.svelte +136 -6
- package/dist/canvas/PanZoomPanel.svelte.d.ts +1 -0
- package/dist/canvas/props-panel/PropsPanel.svelte +40 -11
- package/dist/canvas/shapes/ImageShape.d.ts +9 -0
- package/dist/canvas/shapes/ImageShape.js +115 -24
- package/dist/components/Tooltip.svelte +10 -2
- package/package.json +1 -1
|
@@ -122,9 +122,8 @@
|
|
|
122
122
|
$: isFrame = customType === 'frame' || objectType === 'frame';
|
|
123
123
|
// WidgetShape is a Rect subclass with _customType === 'image'
|
|
124
124
|
// isImagePlaceholder: when no image loaded yet (_hasImage is false)
|
|
125
|
-
$: isImagePlaceholder = customType === 'image' && !selectedObject?._hasImage;
|
|
126
|
-
|
|
127
|
-
$: isRect = objectType === 'rect' && !isFrame && customType !== 'image';
|
|
125
|
+
$: isImagePlaceholder = (customType === 'image' || customType === 'video') && !selectedObject?._hasImage;
|
|
126
|
+
$: isRect = objectType === 'rect' && !isFrame && customType !== 'image' && customType !== 'video';
|
|
128
127
|
$: isTextbox = objectType === 'textbox';
|
|
129
128
|
$: isPath = objectType === 'path' && !customType;
|
|
130
129
|
$: isArrow = objectType === 'path' && customType === 'arrow';
|
|
@@ -141,7 +140,7 @@
|
|
|
141
140
|
$: isMultiSelect = (isGroup && !isFrame) || isActiveSelection;
|
|
142
141
|
$: isPlainGroup = isGroup && !isFrame;
|
|
143
142
|
$: isLine = objectType === 'line';
|
|
144
|
-
$: isImage = objectType === 'image' || customType === 'image';
|
|
143
|
+
$: isImage = objectType === 'image' || customType === 'image' || customType === 'video';
|
|
145
144
|
// Widget link: true when the selected object has already been converted to a widget
|
|
146
145
|
$: hasWidgetId = !!selectedObject?._widgetId;
|
|
147
146
|
$: widgetId = selectedObject?._widgetId ?? null;
|
|
@@ -195,6 +194,7 @@
|
|
|
195
194
|
if (obj._customType === 'frame') return 'Frame';
|
|
196
195
|
if (obj._customType === 'arrow') return 'Arrow';
|
|
197
196
|
if (obj._customType === 'triangle') return 'Triangle';
|
|
197
|
+
if (obj._customType === 'video') return 'Video';
|
|
198
198
|
if (obj._customType === 'image' || obj.type === 'image') return 'Image';
|
|
199
199
|
// Library shapes: derive a readable name from _shapeKey (e.g. 'arrow-right' → 'Arrow Right')
|
|
200
200
|
// Fall back to 'Custom Shape (Drawing)' if _shapeKey is missing (old saves).
|
|
@@ -226,7 +226,13 @@
|
|
|
226
226
|
// Get current values from selected object
|
|
227
227
|
// For Frames (Group-based), read fill/stroke from custom properties (_frameFill, _frameStroke)
|
|
228
228
|
// because the actual fill/stroke is on the background Rect child, not the Group itself
|
|
229
|
-
$:
|
|
229
|
+
$: usesPathBackgroundFill = selectedObject?._cornerRadiusMode === 'strokeRound'
|
|
230
|
+
|| ['cross', 'plus', 'minus', 'check'].includes(selectedObject?._shapeKey);
|
|
231
|
+
$: currentFill = isFrame
|
|
232
|
+
? (selectedObject?._frameFill ?? '#FFFFFF')
|
|
233
|
+
: (usesPathBackgroundFill
|
|
234
|
+
? (selectedObject?._iconFill || selectedObject?.backgroundColor || '#FFFFFF')
|
|
235
|
+
: (selectedObject?.fill ?? '#FFFFFF'));
|
|
230
236
|
$: currentFillOpacity = Math.round((selectedObject?._fillOpacity ?? 1) * 100);
|
|
231
237
|
$: currentStroke = isFrame ? (selectedObject?._frameStroke ?? '#000000') : (selectedObject?.stroke ?? '#000000');
|
|
232
238
|
$: currentStrokeWidth = isFrame ? (selectedObject?._frameStrokeWidth ?? 1) : (selectedObject?.strokeWidth ?? 1);
|
|
@@ -568,7 +574,7 @@
|
|
|
568
574
|
<span class="prop-label">Convert to Widget</span>
|
|
569
575
|
<div class="capture-section">
|
|
570
576
|
<button class="convert-widget-btn" onclick={onConvertToWidget}>
|
|
571
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
577
|
+
<svg class="convert-widget-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
572
578
|
<rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"/>
|
|
573
579
|
<line x1="7" y1="2" x2="7" y2="22"/>
|
|
574
580
|
<line x1="17" y1="2" x2="17" y2="22"/>
|
|
@@ -1305,6 +1311,16 @@
|
|
|
1305
1311
|
border-radius: 8px;
|
|
1306
1312
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
1307
1313
|
}
|
|
1314
|
+
|
|
1315
|
+
.props-panel-el :global(button:focus),
|
|
1316
|
+
.props-panel-el :global(button:focus-visible),
|
|
1317
|
+
.props-panel-el :global(a:focus),
|
|
1318
|
+
.props-panel-el :global(a:focus-visible),
|
|
1319
|
+
.props-panel-el :global(input:focus),
|
|
1320
|
+
.props-panel-el :global(input:focus-visible) {
|
|
1321
|
+
outline: none !important;
|
|
1322
|
+
box-shadow: none !important;
|
|
1323
|
+
}
|
|
1308
1324
|
|
|
1309
1325
|
/* Sticky header: stays at top, never scrolls */
|
|
1310
1326
|
.props-panel-sticky-header {
|
|
@@ -1550,9 +1566,14 @@
|
|
|
1550
1566
|
box-shadow: 0 2px 4px rgba(49, 79, 47, 0.3);
|
|
1551
1567
|
}
|
|
1552
1568
|
|
|
1553
|
-
|
|
1569
|
+
/* Host apps (site / DS) leak global svg fill/stroke; keep this icon white on the green button. */
|
|
1570
|
+
.convert-widget-btn :global(svg),
|
|
1571
|
+
.convert-widget-btn :global(svg *) {
|
|
1554
1572
|
width: 16px;
|
|
1555
1573
|
height: 16px;
|
|
1574
|
+
color: #ffffff !important;
|
|
1575
|
+
fill: none !important;
|
|
1576
|
+
stroke: currentColor !important;
|
|
1556
1577
|
}
|
|
1557
1578
|
|
|
1558
1579
|
.widget-linked-section {
|
|
@@ -1669,7 +1690,9 @@
|
|
|
1669
1690
|
border-radius: 4px;
|
|
1670
1691
|
cursor: pointer;
|
|
1671
1692
|
transition: all 0.15s;
|
|
1672
|
-
width:
|
|
1693
|
+
width: 100%;
|
|
1694
|
+
box-sizing: border-box;
|
|
1695
|
+
justify-content: center;
|
|
1673
1696
|
}
|
|
1674
1697
|
.refresh-screenshot-btn:hover {
|
|
1675
1698
|
color: #166534;
|
|
@@ -1695,14 +1718,18 @@
|
|
|
1695
1718
|
border: 1px solid #fecaca;
|
|
1696
1719
|
border-radius: 4px;
|
|
1697
1720
|
cursor: pointer;
|
|
1698
|
-
width:
|
|
1721
|
+
width: 100%;
|
|
1722
|
+
box-sizing: border-box;
|
|
1723
|
+
justify-content: center;
|
|
1699
1724
|
}
|
|
1700
1725
|
.set-primary-btn:hover {
|
|
1701
1726
|
background: #fee2e2;
|
|
1702
1727
|
border-color: #f87171;
|
|
1703
1728
|
}
|
|
1704
1729
|
.primary-widget-badge {
|
|
1705
|
-
display:
|
|
1730
|
+
display: flex;
|
|
1731
|
+
align-items: center;
|
|
1732
|
+
justify-content: center;
|
|
1706
1733
|
margin-top: 4px;
|
|
1707
1734
|
padding: 3px 8px;
|
|
1708
1735
|
font-size: 11px;
|
|
@@ -1711,7 +1738,9 @@
|
|
|
1711
1738
|
background: #fef2f2;
|
|
1712
1739
|
border: 1px solid #fecaca;
|
|
1713
1740
|
border-radius: 4px;
|
|
1714
|
-
width:
|
|
1741
|
+
width: 100%;
|
|
1742
|
+
box-sizing: border-box;
|
|
1743
|
+
text-align: center;
|
|
1715
1744
|
}
|
|
1716
1745
|
|
|
1717
1746
|
.widget-orphan-notice {
|
|
@@ -24,8 +24,17 @@ export declare class ImageShape extends Rect {
|
|
|
24
24
|
_cornerRoundness: number;
|
|
25
25
|
_cornerRoundnessPixels: number;
|
|
26
26
|
_objectId: string | null;
|
|
27
|
+
_videoElement: HTMLVideoElement | null;
|
|
28
|
+
_videoObjectUrl: string | null;
|
|
27
29
|
_render(ctx: CanvasRenderingContext2D): void;
|
|
28
30
|
private _renderImage;
|
|
31
|
+
getVideoPlayBadgeRadius(): number;
|
|
32
|
+
isVideoPlayBadgeHit(scenePoint: {
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
}): boolean;
|
|
36
|
+
toggleVideoPlayback(): Promise<boolean>;
|
|
37
|
+
private _renderVideoChrome;
|
|
29
38
|
private _renderPlaceholderOverlay;
|
|
30
39
|
toObject(propertiesToInclude?: any[]): any;
|
|
31
40
|
static fromObject(object: Record<string, unknown>, options?: Record<string, unknown>): Promise<ImageShape>;
|
|
@@ -36,9 +36,12 @@ export class ImageShape extends Rect {
|
|
|
36
36
|
_cornerRoundness = 0;
|
|
37
37
|
_cornerRoundnessPixels = 0;
|
|
38
38
|
_objectId = null;
|
|
39
|
+
_videoElement = null;
|
|
40
|
+
_videoObjectUrl = null;
|
|
39
41
|
// ── Placeholder rendering ─────────────────────────────────────────────────
|
|
40
42
|
_render(ctx) {
|
|
41
|
-
const
|
|
43
|
+
const hasVideoFrame = this._customType === 'video' && !!this._videoElement && this._videoElement.readyState >= 2;
|
|
44
|
+
const hasImage = (this._hasImage && this._imageElement) || hasVideoFrame;
|
|
42
45
|
if (hasImage) {
|
|
43
46
|
this._renderImage(ctx);
|
|
44
47
|
}
|
|
@@ -78,9 +81,12 @@ export class ImageShape extends Rect {
|
|
|
78
81
|
ctx.closePath();
|
|
79
82
|
ctx.clip();
|
|
80
83
|
const img = this._imageElement;
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
const video = this._customType === 'video' ? this._videoElement : null;
|
|
85
|
+
const useVideo = !!(video && video.readyState >= 2);
|
|
86
|
+
const source = useVideo ? video : (img && img.complete && img.naturalWidth > 0 ? img : null);
|
|
87
|
+
const imgReady = !!source;
|
|
88
|
+
if (source) {
|
|
89
|
+
if (this._isWidgetImage && img && img.complete && img.naturalWidth > 0 && !useVideo) {
|
|
84
90
|
const imgW = img.naturalWidth;
|
|
85
91
|
const imgH = img.naturalHeight;
|
|
86
92
|
const scale = Math.min(w / imgW, h / imgH);
|
|
@@ -89,10 +95,13 @@ export class ImageShape extends Rect {
|
|
|
89
95
|
ctx.drawImage(img, -dw / 2, -dh / 2, dw, dh);
|
|
90
96
|
}
|
|
91
97
|
else {
|
|
92
|
-
ctx.drawImage(
|
|
98
|
+
ctx.drawImage(source, -w / 2, -h / 2, w, h);
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
ctx.restore();
|
|
102
|
+
if (this._customType === 'video' && imgReady) {
|
|
103
|
+
this._renderVideoChrome(ctx, w, h);
|
|
104
|
+
}
|
|
96
105
|
if (this.stroke && (this.strokeWidth ?? 0) > 0) {
|
|
97
106
|
ctx.save();
|
|
98
107
|
ctx.strokeStyle = this.stroke;
|
|
@@ -126,32 +135,114 @@ export class ImageShape extends Rect {
|
|
|
126
135
|
ctx.restore();
|
|
127
136
|
}
|
|
128
137
|
}
|
|
138
|
+
getVideoPlayBadgeRadius() {
|
|
139
|
+
const w = this.width || 100;
|
|
140
|
+
const h = this.height || 100;
|
|
141
|
+
const size = Math.max(18, Math.min(w, h) * 0.22);
|
|
142
|
+
return (size / 2) * Math.max(this.scaleX || 1, this.scaleY || 1);
|
|
143
|
+
}
|
|
144
|
+
isVideoPlayBadgeHit(scenePoint) {
|
|
145
|
+
if (this._customType !== 'video' || !this._videoElement)
|
|
146
|
+
return false;
|
|
147
|
+
const center = this.getCenterPoint();
|
|
148
|
+
const radius = this.getVideoPlayBadgeRadius();
|
|
149
|
+
const dx = scenePoint.x - center.x;
|
|
150
|
+
const dy = scenePoint.y - center.y;
|
|
151
|
+
return dx * dx + dy * dy <= radius * radius;
|
|
152
|
+
}
|
|
153
|
+
async toggleVideoPlayback() {
|
|
154
|
+
const video = this._videoElement;
|
|
155
|
+
if (!video) {
|
|
156
|
+
canvasWarn('ImageShape: no video element to play');
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
if (video.paused) {
|
|
161
|
+
await video.play();
|
|
162
|
+
canvasLog('ImageShape: video playing');
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
video.pause();
|
|
166
|
+
canvasLog('ImageShape: video paused');
|
|
167
|
+
}
|
|
168
|
+
this.dirty = true;
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
canvasWarn('ImageShape: video play/pause failed', err);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
_renderVideoChrome(ctx, w, h) {
|
|
177
|
+
const video = this._videoElement;
|
|
178
|
+
const playing = !!(video && !video.paused && !video.ended);
|
|
179
|
+
const size = Math.max(18, Math.min(w, h) * 0.22);
|
|
180
|
+
ctx.save();
|
|
181
|
+
ctx.beginPath();
|
|
182
|
+
ctx.arc(0, 0, size / 2, 0, Math.PI * 2);
|
|
183
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';
|
|
184
|
+
ctx.fill();
|
|
185
|
+
ctx.fillStyle = '#ffffff';
|
|
186
|
+
if (playing) {
|
|
187
|
+
const barW = size * 0.08;
|
|
188
|
+
const barH = size * 0.28;
|
|
189
|
+
const gap = size * 0.08;
|
|
190
|
+
ctx.fillRect(-gap / 2 - barW, -barH / 2, barW, barH);
|
|
191
|
+
ctx.fillRect(gap / 2, -barH / 2, barW, barH);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
ctx.beginPath();
|
|
195
|
+
const tri = size * 0.18;
|
|
196
|
+
ctx.moveTo(-tri * 0.35, -tri);
|
|
197
|
+
ctx.lineTo(tri * 0.85, 0);
|
|
198
|
+
ctx.lineTo(-tri * 0.35, tri);
|
|
199
|
+
ctx.closePath();
|
|
200
|
+
ctx.fill();
|
|
201
|
+
}
|
|
202
|
+
if (video && Number.isFinite(video.duration) && video.duration > 0) {
|
|
203
|
+
const barH = Math.max(4, Math.min(8, h * 0.06));
|
|
204
|
+
const progress = Math.max(0, Math.min(1, video.currentTime / video.duration));
|
|
205
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';
|
|
206
|
+
ctx.fillRect(-w / 2, h / 2 - barH, w, barH);
|
|
207
|
+
ctx.fillStyle = '#ffffff';
|
|
208
|
+
ctx.fillRect(-w / 2, h / 2 - barH, w * progress, barH);
|
|
209
|
+
}
|
|
210
|
+
ctx.restore();
|
|
211
|
+
}
|
|
129
212
|
// ── Placeholder icon + text overlay ──────────────────────────────────────
|
|
130
213
|
_renderPlaceholderOverlay(ctx) {
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
214
|
+
const scaleX = Math.abs(this.scaleX || 1) || 1;
|
|
215
|
+
const scaleY = Math.abs(this.scaleY || 1) || 1;
|
|
216
|
+
ctx.save();
|
|
217
|
+
ctx.scale(1 / scaleX, 1 / scaleY);
|
|
218
|
+
const visualW = Math.abs((this.width || 100) * scaleX);
|
|
219
|
+
const visualH = Math.abs((this.height || 100) * scaleY);
|
|
220
|
+
const iconSize = 36;
|
|
221
|
+
const fontSize = 16;
|
|
222
|
+
const lineGap = fontSize + 6;
|
|
223
|
+
const contentWidth = 168;
|
|
224
|
+
const contentHeight = iconSize + 12 + lineGap * 2;
|
|
225
|
+
const contentScale = Math.max(0.7, Math.min((visualW * 0.88) / contentWidth, (visualH * 0.7) / contentHeight));
|
|
135
226
|
ctx.fillStyle = '#94a3b8';
|
|
136
227
|
ctx.textAlign = 'center';
|
|
137
228
|
ctx.textBaseline = 'middle';
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
229
|
+
ctx.save();
|
|
230
|
+
ctx.scale(contentScale, contentScale);
|
|
231
|
+
ctx.font = `${iconSize}px Arial`;
|
|
232
|
+
ctx.fillText(this._customType === 'video' ? '🎬' : '🖼️', 0, -28);
|
|
233
|
+
ctx.font = `${fontSize}px Arial`;
|
|
234
|
+
if (this._customType === 'video') {
|
|
235
|
+
ctx.fillText('Drop video', 0, 8);
|
|
236
|
+
ctx.fillText('or', 0, 8 + lineGap);
|
|
237
|
+
ctx.fillText('double click', 0, 8 + lineGap * 2);
|
|
142
238
|
}
|
|
143
239
|
else {
|
|
144
|
-
ctx.
|
|
145
|
-
ctx.
|
|
146
|
-
ctx.
|
|
147
|
-
ctx.fillText('🖼️', 0, -28);
|
|
148
|
-
const fontSize = 12;
|
|
149
|
-
ctx.font = `${fontSize}px Arial`;
|
|
150
|
-
ctx.fillText('Drop image', 0, 0);
|
|
151
|
-
ctx.fillText('or', 0, fontSize + 4);
|
|
152
|
-
ctx.fillText('double click', 0, (fontSize + 4) * 2);
|
|
153
|
-
ctx.restore();
|
|
240
|
+
ctx.fillText('Drop image', 0, 8);
|
|
241
|
+
ctx.fillText('or', 0, 8 + lineGap);
|
|
242
|
+
ctx.fillText('double click', 0, 8 + lineGap * 2);
|
|
154
243
|
}
|
|
244
|
+
ctx.restore();
|
|
245
|
+
ctx.restore();
|
|
155
246
|
}
|
|
156
247
|
// ── Serialisation ─────────────────────────────────────────────────────────
|
|
157
248
|
toObject(propertiesToInclude) {
|
|
@@ -225,7 +316,7 @@ classRegistry.setClass(ImageShape);
|
|
|
225
316
|
* swapping the prototype chain. No object re-creation needed.
|
|
226
317
|
*/
|
|
227
318
|
export function upgradeToImageShape(obj) {
|
|
228
|
-
if (!obj || obj._customType !== 'image')
|
|
319
|
+
if (!obj || (obj._customType !== 'image' && obj._customType !== 'video'))
|
|
229
320
|
return null;
|
|
230
321
|
if (obj instanceof ImageShape)
|
|
231
322
|
return obj;
|
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
|
|
26
26
|
let triggerEl: HTMLElement | null = null;
|
|
27
27
|
|
|
28
|
+
$: if (text) refreshVisibleTooltip();
|
|
29
|
+
|
|
30
|
+
function refreshVisibleTooltip() {
|
|
31
|
+
if (tooltipBodyEl && tooltipBodyEl.style.display === 'block') {
|
|
32
|
+
show();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
// Lazily-created body-level elements
|
|
29
37
|
let tooltipBodyEl: HTMLElement | null = null;
|
|
30
38
|
let arrowBodyEl: HTMLElement | null = null;
|
|
@@ -48,8 +56,8 @@
|
|
|
48
56
|
lineHeight: '1.4',
|
|
49
57
|
padding: '5px 10px',
|
|
50
58
|
borderRadius: '6px',
|
|
51
|
-
whiteSpace: '
|
|
52
|
-
maxWidth: '
|
|
59
|
+
whiteSpace: 'nowrap',
|
|
60
|
+
maxWidth: 'none',
|
|
53
61
|
textAlign: 'center',
|
|
54
62
|
boxShadow: '0 2px 8px rgba(0,0,0,0.25)',
|
|
55
63
|
userSelect: 'none',
|