@widgetic/canvas 0.5.4
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 +45 -0
- package/dist/canvas/Canvas.svelte +10678 -0
- package/dist/canvas/Canvas.svelte.d.ts +147 -0
- package/dist/canvas/CanvasToolbar.svelte +1422 -0
- package/dist/canvas/CanvasToolbar.svelte.d.ts +54 -0
- package/dist/canvas/ContextMenu.svelte +279 -0
- package/dist/canvas/ContextMenu.svelte.d.ts +36 -0
- package/dist/canvas/PanZoomPanel.svelte +315 -0
- package/dist/canvas/PanZoomPanel.svelte.d.ts +32 -0
- package/dist/canvas/canvasLogger.d.ts +5 -0
- package/dist/canvas/canvasLogger.js +19 -0
- package/dist/canvas/index.d.ts +13 -0
- package/dist/canvas/index.js +12 -0
- package/dist/canvas/props-panel/PropsPanel.svelte +1902 -0
- package/dist/canvas/props-panel/PropsPanel.svelte.d.ts +73 -0
- package/dist/canvas/props-panel/TextPropsICSection.svelte +238 -0
- package/dist/canvas/props-panel/TextPropsICSection.svelte.d.ts +36 -0
- package/dist/canvas/shapes/FrameShape.d.ts +97 -0
- package/dist/canvas/shapes/FrameShape.js +951 -0
- package/dist/canvas/shapes/ImageShape.d.ts +38 -0
- package/dist/canvas/shapes/ImageShape.js +245 -0
- package/dist/canvas/shapes/ShapeLibrary.d.ts +64 -0
- package/dist/canvas/shapes/ShapeLibrary.js +526 -0
- package/dist/canvas/shapes/WidgetShape.d.ts +21 -0
- package/dist/canvas/shapes/WidgetShape.js +132 -0
- package/dist/canvas/types.d.ts +26 -0
- package/dist/canvas/types.js +5 -0
- package/dist/components/Tooltip.svelte +179 -0
- package/dist/components/Tooltip.svelte.d.ts +21 -0
- package/dist/components/index.d.ts +11 -0
- package/dist/components/index.js +13 -0
- package/dist/components/input-controls/ColorIC.svelte +388 -0
- package/dist/components/input-controls/ColorIC.svelte.d.ts +22 -0
- package/dist/components/input-controls/FontSelectorIC.svelte +69 -0
- package/dist/components/input-controls/FontSelectorIC.svelte.d.ts +17 -0
- package/dist/components/input-controls/SliderUnitIC.svelte +217 -0
- package/dist/components/input-controls/SliderUnitIC.svelte.d.ts +24 -0
- package/dist/components/input-controls/TextAlignIC.svelte +84 -0
- package/dist/components/input-controls/TextAlignIC.svelte.d.ts +17 -0
- package/dist/components/input-controls/TextStyleIC.svelte +85 -0
- package/dist/components/input-controls/TextStyleIC.svelte.d.ts +21 -0
- package/dist/icons/arrow.svg +3 -0
- package/dist/icons/checkmark.svg +3 -0
- package/dist/icons/draw.svg +22 -0
- package/dist/icons/eraser.svg +23 -0
- package/dist/icons/hand.svg +6 -0
- package/dist/icons/select.svg +3 -0
- package/dist/icons/text.svg +5 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/package.json +101 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageShape — custom Rect subclass for image placeholders on canvas.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - When _hasImage = false → renders placeholder UI (icon + "Drop image" text + dashed border)
|
|
6
|
+
* - When _hasImage = true → renders the loaded image (clipped, with optional corner radius)
|
|
7
|
+
*
|
|
8
|
+
* This is the base class for images on canvas. For widget-specific behavior
|
|
9
|
+
* (linked to a widget via _widgetId), see WidgetShape which extends this class.
|
|
10
|
+
*
|
|
11
|
+
* Fabric serialises as type:'ImageShape'. Old saves with type:'Rect' + _customType:'image'
|
|
12
|
+
* are handled by upgradeToImageShape() which swaps the prototype chain.
|
|
13
|
+
*/
|
|
14
|
+
import { Rect } from 'fabric';
|
|
15
|
+
export declare const IMAGE_SHAPE_CUSTOM_PROPS: string[];
|
|
16
|
+
export declare class ImageShape extends Rect {
|
|
17
|
+
static type: string;
|
|
18
|
+
_customType: string;
|
|
19
|
+
_hasImage: boolean;
|
|
20
|
+
_imageElement: HTMLImageElement | null;
|
|
21
|
+
_imageUrl: string | null;
|
|
22
|
+
_originalFilename: string | null;
|
|
23
|
+
_originalAspectRatio: number | null;
|
|
24
|
+
_cornerRoundness: number;
|
|
25
|
+
_cornerRoundnessPixels: number;
|
|
26
|
+
_objectId: string | null;
|
|
27
|
+
_render(ctx: CanvasRenderingContext2D): void;
|
|
28
|
+
private _renderImage;
|
|
29
|
+
private _renderPlaceholderOverlay;
|
|
30
|
+
toObject(propertiesToInclude?: any[]): any;
|
|
31
|
+
static fromObject(object: Record<string, unknown>, options?: Record<string, unknown>): Promise<ImageShape>;
|
|
32
|
+
static reloadImage(shape: ImageShape): void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Upgrades a plain Rect with _customType:'image' to an ImageShape instance by
|
|
36
|
+
* swapping the prototype chain. No object re-creation needed.
|
|
37
|
+
*/
|
|
38
|
+
export declare function upgradeToImageShape(obj: any): ImageShape | null;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageShape — custom Rect subclass for image placeholders on canvas.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - When _hasImage = false → renders placeholder UI (icon + "Drop image" text + dashed border)
|
|
6
|
+
* - When _hasImage = true → renders the loaded image (clipped, with optional corner radius)
|
|
7
|
+
*
|
|
8
|
+
* This is the base class for images on canvas. For widget-specific behavior
|
|
9
|
+
* (linked to a widget via _widgetId), see WidgetShape which extends this class.
|
|
10
|
+
*
|
|
11
|
+
* Fabric serialises as type:'ImageShape'. Old saves with type:'Rect' + _customType:'image'
|
|
12
|
+
* are handled by upgradeToImageShape() which swaps the prototype chain.
|
|
13
|
+
*/
|
|
14
|
+
import { Rect, classRegistry } from 'fabric';
|
|
15
|
+
import { canvasLog, canvasWarn } from '../canvasLogger';
|
|
16
|
+
// ── Custom property names for base ImageShape ─────────────────────────────────
|
|
17
|
+
export const IMAGE_SHAPE_CUSTOM_PROPS = [
|
|
18
|
+
'_customType',
|
|
19
|
+
'_hasImage',
|
|
20
|
+
'_imageUrl',
|
|
21
|
+
'_originalFilename',
|
|
22
|
+
'_originalAspectRatio',
|
|
23
|
+
'_cornerRoundness',
|
|
24
|
+
'_cornerRoundnessPixels',
|
|
25
|
+
'_objectId',
|
|
26
|
+
];
|
|
27
|
+
export class ImageShape extends Rect {
|
|
28
|
+
static type = 'ImageShape';
|
|
29
|
+
// Custom fields — typed for IDE support
|
|
30
|
+
_customType = 'image';
|
|
31
|
+
_hasImage = false;
|
|
32
|
+
_imageElement = null;
|
|
33
|
+
_imageUrl = null;
|
|
34
|
+
_originalFilename = null;
|
|
35
|
+
_originalAspectRatio = null;
|
|
36
|
+
_cornerRoundness = 0;
|
|
37
|
+
_cornerRoundnessPixels = 0;
|
|
38
|
+
_objectId = null;
|
|
39
|
+
// ── Placeholder rendering ─────────────────────────────────────────────────
|
|
40
|
+
_render(ctx) {
|
|
41
|
+
const hasImage = this._hasImage && this._imageElement;
|
|
42
|
+
if (hasImage) {
|
|
43
|
+
this._renderImage(ctx);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
super._render(ctx);
|
|
47
|
+
this._renderPlaceholderOverlay(ctx);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// ── Image rendering ───────────────────────────────────────────────────────
|
|
51
|
+
_renderImage(ctx) {
|
|
52
|
+
const w = this.width || 100;
|
|
53
|
+
const h = this.height || 100;
|
|
54
|
+
const r = this._cornerRoundnessPixels || 0;
|
|
55
|
+
ctx.save();
|
|
56
|
+
ctx.beginPath();
|
|
57
|
+
if (r > 0) {
|
|
58
|
+
const minDim = Math.min(w, h);
|
|
59
|
+
if (r >= minDim / 2) {
|
|
60
|
+
if (w === h) {
|
|
61
|
+
ctx.arc(0, 0, w / 2, 0, Math.PI * 2);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
ctx.ellipse(0, 0, w / 2, h / 2, 0, 0, Math.PI * 2);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
ctx.moveTo(-w / 2 + r, -h / 2);
|
|
69
|
+
ctx.arcTo(w / 2, -h / 2, w / 2, h / 2, r);
|
|
70
|
+
ctx.arcTo(w / 2, h / 2, -w / 2, h / 2, r);
|
|
71
|
+
ctx.arcTo(-w / 2, h / 2, -w / 2, -h / 2, r);
|
|
72
|
+
ctx.arcTo(-w / 2, -h / 2, w / 2, -h / 2, r);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
ctx.rect(-w / 2, -h / 2, w, h);
|
|
77
|
+
}
|
|
78
|
+
ctx.closePath();
|
|
79
|
+
ctx.clip();
|
|
80
|
+
const img = this._imageElement;
|
|
81
|
+
const imgReady = img.complete && img.naturalWidth > 0;
|
|
82
|
+
if (imgReady) {
|
|
83
|
+
if (this._isWidgetImage) {
|
|
84
|
+
const imgW = img.naturalWidth;
|
|
85
|
+
const imgH = img.naturalHeight;
|
|
86
|
+
const scale = Math.min(w / imgW, h / imgH);
|
|
87
|
+
const dw = imgW * scale;
|
|
88
|
+
const dh = imgH * scale;
|
|
89
|
+
ctx.drawImage(img, -dw / 2, -dh / 2, dw, dh);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
ctx.drawImage(img, -w / 2, -h / 2, w, h);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
ctx.restore();
|
|
96
|
+
if (this.stroke && (this.strokeWidth ?? 0) > 0) {
|
|
97
|
+
ctx.save();
|
|
98
|
+
ctx.strokeStyle = this.stroke;
|
|
99
|
+
ctx.lineWidth = this.strokeWidth ?? 1;
|
|
100
|
+
if (this.strokeDashArray)
|
|
101
|
+
ctx.setLineDash(this.strokeDashArray);
|
|
102
|
+
ctx.beginPath();
|
|
103
|
+
if (r > 0) {
|
|
104
|
+
const minDim = Math.min(w, h);
|
|
105
|
+
if (r >= minDim / 2) {
|
|
106
|
+
if (w === h) {
|
|
107
|
+
ctx.arc(0, 0, w / 2, 0, Math.PI * 2);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
ctx.ellipse(0, 0, w / 2, h / 2, 0, 0, Math.PI * 2);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
ctx.moveTo(-w / 2 + r, -h / 2);
|
|
115
|
+
ctx.arcTo(w / 2, -h / 2, w / 2, h / 2, r);
|
|
116
|
+
ctx.arcTo(w / 2, h / 2, -w / 2, h / 2, r);
|
|
117
|
+
ctx.arcTo(-w / 2, h / 2, -w / 2, -h / 2, r);
|
|
118
|
+
ctx.arcTo(-w / 2, -h / 2, w / 2, -h / 2, r);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
ctx.rect(-w / 2, -h / 2, w, h);
|
|
123
|
+
}
|
|
124
|
+
ctx.closePath();
|
|
125
|
+
ctx.stroke();
|
|
126
|
+
ctx.restore();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ── Placeholder icon + text overlay ──────────────────────────────────────
|
|
130
|
+
_renderPlaceholderOverlay(ctx) {
|
|
131
|
+
const availableW = this.width || 100;
|
|
132
|
+
const availableH = this.height || 100;
|
|
133
|
+
const contentHeight = 76;
|
|
134
|
+
const contentScale = Math.min(1, availableH / contentHeight, availableW / 100);
|
|
135
|
+
ctx.fillStyle = '#94a3b8';
|
|
136
|
+
ctx.textAlign = 'center';
|
|
137
|
+
ctx.textBaseline = 'middle';
|
|
138
|
+
if (contentScale < 0.3) {
|
|
139
|
+
const tinyIconSize = Math.max(8, 24 * contentScale);
|
|
140
|
+
ctx.font = `${tinyIconSize}px Arial`;
|
|
141
|
+
ctx.fillText('🖼️', 0, 0);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
ctx.save();
|
|
145
|
+
ctx.scale(contentScale, contentScale);
|
|
146
|
+
ctx.font = '24px Arial';
|
|
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();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// ── Serialisation ─────────────────────────────────────────────────────────
|
|
157
|
+
toObject(propertiesToInclude) {
|
|
158
|
+
const extras = IMAGE_SHAPE_CUSTOM_PROPS.concat(propertiesToInclude ?? []);
|
|
159
|
+
const base = super.toObject(extras);
|
|
160
|
+
return {
|
|
161
|
+
...base,
|
|
162
|
+
type: this.constructor.type,
|
|
163
|
+
_imageElement: undefined,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// ── Deserialisation ───────────────────────────────────────────────────────
|
|
167
|
+
static fromObject(object, options) {
|
|
168
|
+
return Rect.fromObject({ ...object, type: 'rect' }, options).then((rect) => {
|
|
169
|
+
Object.setPrototypeOf(rect, ImageShape.prototype);
|
|
170
|
+
const shape = rect;
|
|
171
|
+
for (const prop of IMAGE_SHAPE_CUSTOM_PROPS) {
|
|
172
|
+
if (prop in object && shape[prop] === undefined || shape[prop] !== object[prop]) {
|
|
173
|
+
shape[prop] = object[prop];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (shape._imageUrl) {
|
|
177
|
+
ImageShape.reloadImage(shape);
|
|
178
|
+
}
|
|
179
|
+
canvasLog('ImageShape: fromObject — restored', shape._objectId || 'shape', 'hasImage:', shape._hasImage);
|
|
180
|
+
return shape;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// ── Image loading ─────────────────────────────────────────────────────────
|
|
184
|
+
static reloadImage(shape) {
|
|
185
|
+
if (!shape._imageUrl)
|
|
186
|
+
return;
|
|
187
|
+
const img = new Image();
|
|
188
|
+
img.crossOrigin = 'anonymous';
|
|
189
|
+
img.onload = () => {
|
|
190
|
+
shape._imageElement = img;
|
|
191
|
+
shape._hasImage = true;
|
|
192
|
+
shape.dirty = true;
|
|
193
|
+
const parentGroup = shape.group;
|
|
194
|
+
if (parentGroup) {
|
|
195
|
+
parentGroup.dirty = true;
|
|
196
|
+
}
|
|
197
|
+
const canvasRef = shape.canvas;
|
|
198
|
+
if (canvasRef) {
|
|
199
|
+
canvasRef.requestRenderAll();
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
requestAnimationFrame(() => {
|
|
203
|
+
const c = shape.canvas;
|
|
204
|
+
if (c) {
|
|
205
|
+
const pg = shape.group;
|
|
206
|
+
if (pg)
|
|
207
|
+
pg.dirty = true;
|
|
208
|
+
c.requestRenderAll();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
canvasLog('ImageShape: image reloaded from URL', shape._imageUrl?.substring(0, 60));
|
|
213
|
+
};
|
|
214
|
+
img.onerror = () => {
|
|
215
|
+
canvasWarn('ImageShape: failed to reload image', shape._imageUrl?.substring(0, 60));
|
|
216
|
+
};
|
|
217
|
+
img.src = shape._imageUrl;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Register so loadFromJSON can resolve type:'ImageShape'
|
|
221
|
+
classRegistry.setClass(ImageShape);
|
|
222
|
+
// ── Upgrade helper for backward-compat (old saves: type:'Rect' + _customType:'image') ─
|
|
223
|
+
/**
|
|
224
|
+
* Upgrades a plain Rect with _customType:'image' to an ImageShape instance by
|
|
225
|
+
* swapping the prototype chain. No object re-creation needed.
|
|
226
|
+
*/
|
|
227
|
+
export function upgradeToImageShape(obj) {
|
|
228
|
+
if (!obj || obj._customType !== 'image')
|
|
229
|
+
return null;
|
|
230
|
+
if (obj instanceof ImageShape)
|
|
231
|
+
return obj;
|
|
232
|
+
try {
|
|
233
|
+
Object.setPrototypeOf(obj, ImageShape.prototype);
|
|
234
|
+
const shape = obj;
|
|
235
|
+
if (shape._imageUrl && !shape._imageElement) {
|
|
236
|
+
ImageShape.reloadImage(shape);
|
|
237
|
+
}
|
|
238
|
+
canvasLog('ImageShape: upgraded Rect → ImageShape for', shape._objectId || 'shape');
|
|
239
|
+
return shape;
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
canvasWarn('ImageShape: upgradeToImageShape failed', err);
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShapeLibrary.ts
|
|
3
|
+
*
|
|
4
|
+
* Pre-defined SVG path data for special shapes (media controls, UI icons, etc.).
|
|
5
|
+
* All paths are normalized to a 100×100 viewBox so Fabric.js scales them uniformly.
|
|
6
|
+
*
|
|
7
|
+
* cornerRadiusMode per shape:
|
|
8
|
+
* 'none' – shape already curved (circle, heart) → hide Corner Radius slider
|
|
9
|
+
* 'clipPath' – clip bounding-box rect to rounded rect (arrows, …)
|
|
10
|
+
* 'strokeRound' – apply round linecap / linejoin (line-only: check, X, +, -)
|
|
11
|
+
* 'pathRegen' – regenerate sub-shape path data with rounded corners (rects & polygons)
|
|
12
|
+
*/
|
|
13
|
+
export type CornerRadiusMode = 'none' | 'clipPath' | 'strokeRound' | 'pathRegen';
|
|
14
|
+
export type RegenSubShape = {
|
|
15
|
+
type: 'rect';
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
w: number;
|
|
19
|
+
h: number;
|
|
20
|
+
} | {
|
|
21
|
+
type: 'polygon';
|
|
22
|
+
points: [number, number][];
|
|
23
|
+
} | {
|
|
24
|
+
type: 'fixed';
|
|
25
|
+
path: string;
|
|
26
|
+
};
|
|
27
|
+
export interface LibraryShape {
|
|
28
|
+
key: string;
|
|
29
|
+
label: string;
|
|
30
|
+
category: string;
|
|
31
|
+
pathData: string;
|
|
32
|
+
iconSvg: string;
|
|
33
|
+
cornerRadiusMode: CornerRadiusMode;
|
|
34
|
+
/** Sub-shapes used by 'pathRegen' mode to regenerate path with rounded corners */
|
|
35
|
+
regenData?: RegenSubShape[];
|
|
36
|
+
/**
|
|
37
|
+
* Per-shape override for proportional resize lock.
|
|
38
|
+
* When undefined, the category-based rule applies (Symbol Shapes / Media Icons / UI Icons = locked).
|
|
39
|
+
* Set to `false` to explicitly opt out for shapes that are intentionally non-square (e.g. speech bubble).
|
|
40
|
+
*/
|
|
41
|
+
lockAspectRatio?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** Generate SVG path data for a rounded rectangle. */
|
|
44
|
+
export declare function roundedRectPath(x: number, y: number, w: number, h: number, r: number): string;
|
|
45
|
+
/**
|
|
46
|
+
* Generate SVG path data for a convex polygon with rounded corners.
|
|
47
|
+
* @param points – vertices in order (any winding)
|
|
48
|
+
* @param r – corner radius in path units
|
|
49
|
+
*/
|
|
50
|
+
export declare function roundedPolygonPath(points: [number, number][], r: number): string;
|
|
51
|
+
/**
|
|
52
|
+
* Regenerate the full SVG path string for a library shape at the given roundness (0-100 %).
|
|
53
|
+
* Returns null for shapes that don't use pathRegen.
|
|
54
|
+
*/
|
|
55
|
+
export declare function generatePathForShape(shape: LibraryShape, roundnessPercent: number): string | null;
|
|
56
|
+
/** All library shapes, grouped by category (Geometric first, then Arrows) */
|
|
57
|
+
export declare const SHAPE_LIBRARY: LibraryShape[];
|
|
58
|
+
/**
|
|
59
|
+
* Look up a library shape definition by its stable key.
|
|
60
|
+
* Returns undefined for unknown keys (dynamic/legacy shape keys).
|
|
61
|
+
*/
|
|
62
|
+
export declare function getShapeByKey(key: string | null | undefined): LibraryShape | undefined;
|
|
63
|
+
/** Shapes grouped by category for rendering in the UI */
|
|
64
|
+
export declare function getShapesByCategory(): Map<string, LibraryShape[]>;
|