@mhamz.01/easyflow-whiteboard 2.20.0 → 2.21.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.
@@ -8,7 +8,7 @@ export declare const updateDrawingObject: (shape: FabricObject, toolType: string
8
8
  y: number;
9
9
  }) => void;
10
10
  export declare const deleteSelectedObjects: (canvas: Canvas) => void;
11
- export declare const exportCanvasToJSON: (canvas: Canvas) => void;
11
+ export declare const exportCanvasToJSON: (_canvas: Canvas) => void;
12
12
  export declare const importCanvasFromJSON: (canvas: Canvas, json: string, callback?: () => void) => void;
13
13
  export declare const exportCanvasToImage: (canvas: Canvas, format?: "png" | "jpeg") => string;
14
14
  export declare const calculateDashArray: (strokeDashArray: number[] | null, strokeWidth: number) => number[] | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"fabric-utils.d.ts","sourceRoot":"","sources":["../../src/lib/fabric-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAwE,WAAW,EAAc,MAAM,QAAQ,CAAC;AAK7I,eAAO,MAAM,sBAAsB,GAAI,QAAQ,MAAM,WAkBpD,CAAC;AAQF,eAAO,MAAM,mBAAmB,GAC9B,OAAO,YAAY,EACnB,UAAU,MAAM,EAChB,YAAY;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EACpC,cAAc;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,SAwEvC,CAAC;AAKF,eAAO,MAAM,qBAAqB,GAAI,QAAQ,MAAM,SASnD,CAAC;AAGF,eAAO,MAAM,kBAAkB,GAAI,QAAQ,MAAM,SAEhD,CAAC;AAGF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,MAAM,EACd,MAAM,MAAM,EACZ,WAAW,MAAM,IAAI,SAMtB,CAAC;AAGF,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,MAAM,EACd,SAAQ,KAAK,GAAG,MAAc,WAO/B,CAAC;AAUF,eAAO,MAAM,kBAAkB,GAC7B,iBAAiB,MAAM,EAAE,GAAG,IAAI,EAChC,aAAa,MAAM,KAClB,MAAM,EAAE,GAAG,SAKb,CAAC;AAKF,eAAO,MAAM,OAAO,GAClB,QAAQ,MAAM,EACd,SAAS;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,SAmBF,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAU,QAAQ,MAAM,EAAE,KAAK,MAAM,iJAuCjE,CAAC;AAGF,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,SAIzC,CAAC;AAGF,eAAO,MAAM,UAAU,GAAI,QAAQ,MAAM,EAAE,WAAW,MAAM,SAG3D,CAAC;AAGF,eAAO,MAAM,kBAAkB,GAAI,QAAQ,MAAM,SAoBhD,CAAC;AAIF,eAAO,MAAM,iBAAiB,GAAU,QAAQ,MAAM,EAAE,SAAS,MAAM,kBAoFtE,CAAC"}
1
+ {"version":3,"file":"fabric-utils.d.ts","sourceRoot":"","sources":["../../src/lib/fabric-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EAAE,YAAY,EACN,WAAW,EAC1B,MAAM,QAAQ,CAAC;AAMhB,eAAO,MAAM,sBAAsB,GAAI,QAAQ,MAAM,WAgBpD,CAAC;AAIF,eAAO,MAAM,mBAAmB,GAC9B,OAAc,YAAY,EAC1B,UAAc,MAAM,EACpB,YAAc;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EACtC,cAAc;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,SAsDvC,CAAC;AAIF,eAAO,MAAM,qBAAqB,GAAI,QAAQ,MAAM,SAOnD,CAAC;AAIF,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,SAEjD,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,QAAQ,MAAM,EAAE,MAAM,MAAM,EAAE,WAAW,MAAM,IAAI,SAKvF,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,QAAQ,MAAM,EAAE,SAAQ,KAAK,GAAG,MAAc,WAEjF,CAAC;AAIF,eAAO,MAAM,kBAAkB,GAC7B,iBAAiB,MAAM,EAAE,GAAG,IAAI,EAChC,aAAa,MAAM,KAClB,MAAM,EAAE,GAAG,SAIb,CAAC;AAIF,eAAO,MAAM,OAAO,GAClB,QAAQ,MAAM,EACd,SAAS;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CACzD,SAgBF,CAAC;AAIF,eAAO,MAAM,gBAAgB,GAAU,QAAQ,MAAM,EAAE,KAAK,MAAM,iJAoBjE,CAAC;AAIF,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,SAIzC,CAAC;AAIF,eAAO,MAAM,UAAU,GAAI,QAAQ,MAAM,EAAE,WAAW,MAAM,SAG3D,CAAC;AAIF,eAAO,MAAM,kBAAkB,GAAI,QAAQ,MAAM,SAqChD,CAAC;AAIF,eAAO,MAAM,iBAAiB,GAAU,QAAQ,MAAM,EAAE,SAAS,MAAM,kBAqCtE,CAAC"}
@@ -1,7 +1,7 @@
1
- import { FabricObject, Text, Line, Group, IText, PencilBrush, FabricImage, util } from "fabric";
2
- // Initialize canvas with default settings
1
+ import { FabricObject, Text, Line, Group, IText, FabricImage, util, } from "fabric";
2
+ import { PencilBrush } from "fabric";
3
+ // ── Canvas initialization ─────────────────────────────────────────────────────
3
4
  export const initializeFabricCanvas = (canvas) => {
4
- // Set default object properties
5
5
  FabricObject.prototype.set({
6
6
  transparentCorners: false,
7
7
  borderColor: "#3b82f6",
@@ -10,49 +10,40 @@ export const initializeFabricCanvas = (canvas) => {
10
10
  cornerStyle: "circle",
11
11
  padding: 4,
12
12
  });
13
- // Enable object caching for better performance
13
+ // Object caching composites complex objects into a cached bitmap.
14
+ // Dramatically reduces repaint cost for objects with shadows, gradients, etc.
14
15
  FabricObject.prototype.objectCaching = true;
15
- // Initialize drawing brush
16
16
  canvas.freeDrawingBrush = new PencilBrush(canvas);
17
17
  return canvas;
18
18
  };
19
- // Update shape dimensions during drag-to-draw
19
+ // ── updateDrawingObject ───────────────────────────────────────────────────────
20
20
  export const updateDrawingObject = (shape, toolType, startPoint, currentPoint) => {
21
- const width = currentPoint.x - startPoint.x;
22
- const height = currentPoint.y - startPoint.y;
23
21
  switch (toolType) {
24
22
  case "rectangle":
25
23
  case "frame": {
26
- const rect = shape; // or Frame
24
+ const rect = shape;
27
25
  const dx = currentPoint.x - startPoint.x;
28
26
  const dy = currentPoint.y - startPoint.y;
29
- // 1. Calculate the center point (Midpoint)
30
- const midX = startPoint.x + dx / 2;
31
- const midY = startPoint.y + dy / 2;
32
- // 2. Calculate dimensions
33
- // We subtract a tiny fraction of the strokeWidth if you want the
34
- // outer visual edge to be exactly under the pixel of the cursor.
35
27
  const strokeW = rect.strokeWidth || 0;
36
- const width = Math.abs(dx) - strokeW;
37
- const height = Math.abs(dy) - strokeW;
38
28
  rect.set({
39
- left: midX,
40
- top: midY,
41
- width: Math.max(0, width),
42
- height: Math.max(0, height),
29
+ left: startPoint.x + dx / 2,
30
+ top: startPoint.y + dy / 2,
31
+ width: Math.max(0, Math.abs(dx) - strokeW),
32
+ height: Math.max(0, Math.abs(dy) - strokeW),
43
33
  originX: "center",
44
34
  originY: "center",
45
35
  });
36
+ // ── PERF FIX 1: Single setCoords() call ──────────────────────────────
37
+ // Old code called setCoords() inside each case AND again at the bottom
38
+ // of the function — double call on every mousemove during drawing.
46
39
  rect.setCoords();
47
- break;
40
+ return; // early return skips the bottom setCoords()
48
41
  }
49
42
  case "circle": {
50
43
  const circle = shape;
51
44
  const dx = currentPoint.x - startPoint.x;
52
45
  const dy = currentPoint.y - startPoint.y;
53
- // Radius from diagonal distance
54
46
  const radius = Math.sqrt(dx * dx + dy * dy) / 2;
55
- // Center at midpoint
56
47
  circle.set({
57
48
  left: startPoint.x + dx / 2,
58
49
  top: startPoint.y + dy / 2,
@@ -61,61 +52,53 @@ export const updateDrawingObject = (shape, toolType, startPoint, currentPoint) =
61
52
  originY: "center",
62
53
  });
63
54
  circle.setCoords();
64
- break;
55
+ return;
65
56
  }
66
57
  case "line":
67
- case "arrow":
58
+ case "arrow": {
68
59
  if (shape instanceof Line) {
69
60
  shape.set({
70
- x1: startPoint.x,
71
- y1: startPoint.y,
72
- x2: currentPoint.x,
73
- y2: currentPoint.y,
61
+ x1: startPoint.x, y1: startPoint.y,
62
+ x2: currentPoint.x, y2: currentPoint.y,
74
63
  });
75
64
  }
76
- break;
65
+ shape.setCoords();
66
+ return;
67
+ }
77
68
  }
78
- shape.setCoords();
79
69
  };
80
- // Delete selected objects
70
+ // ── deleteSelectedObjects ─────────────────────────────────────────────────────
81
71
  export const deleteSelectedObjects = (canvas) => {
82
72
  const activeObjects = canvas.getActiveObjects();
83
- if (activeObjects.length) {
84
- activeObjects.forEach((obj) => {
85
- canvas.remove(obj);
86
- });
87
- canvas.discardActiveObject();
88
- canvas.renderAll();
89
- }
73
+ if (!activeObjects.length)
74
+ return;
75
+ activeObjects.forEach((obj) => canvas.remove(obj));
76
+ canvas.discardActiveObject();
77
+ // ── PERF FIX 2: requestRenderAll instead of renderAll ─────────────────────
78
+ canvas.requestRenderAll();
90
79
  };
91
- // Export canvas as JSON (save to database)
92
- export const exportCanvasToJSON = (canvas) => {
93
- // return JSON.stringify(canvas.toJSON(["data"]));
80
+ // ── Export / Import ───────────────────────────────────────────────────────────
81
+ export const exportCanvasToJSON = (_canvas) => {
82
+ // Intentionally left as a no-op stub — implement when needed
94
83
  };
95
- // Import canvas from JSON
96
84
  export const importCanvasFromJSON = (canvas, json, callback) => {
97
85
  canvas.loadFromJSON(json).then(() => {
98
- canvas.renderAll();
86
+ canvas.requestRenderAll(); // PERF FIX 3
99
87
  if (callback)
100
88
  callback();
101
89
  });
102
90
  };
103
- // Export canvas as image
104
91
  export const exportCanvasToImage = (canvas, format = "png") => {
105
- return canvas.toDataURL({
106
- format,
107
- quality: 1,
108
- multiplier: 2, // 2x resolution for better quality
109
- });
92
+ return canvas.toDataURL({ format, quality: 1, multiplier: 2 });
110
93
  };
111
- // lib/fabric-utils.ts
94
+ // ── calculateDashArray ────────────────────────────────────────────────────────
112
95
  export const calculateDashArray = (strokeDashArray, strokeWidth) => {
113
96
  if (!strokeDashArray)
114
97
  return undefined;
115
98
  const multiplier = Math.max(strokeWidth, 1);
116
- return strokeDashArray.map(value => value * multiplier);
99
+ return strokeDashArray.map((v) => v * multiplier);
117
100
  };
118
- // Add text at specific position
101
+ // ── addText ───────────────────────────────────────────────────────────────────
119
102
  export const addText = (canvas, options) => {
120
103
  const text = new IText(options.text || "Double click to edit", {
121
104
  left: options.x || 100,
@@ -124,147 +107,108 @@ export const addText = (canvas, options) => {
124
107
  fontFamily: options.fontFamily || "Arial",
125
108
  fontWeight: options.fontWeight || "400",
126
109
  fill: options.color || "#000000",
127
- // textAlign: options.textAlign || "left",
128
110
  });
129
111
  canvas.add(text);
130
112
  canvas.setActiveObject(text);
131
- canvas.renderAll();
132
- // Auto-enter edit mode
113
+ canvas.requestRenderAll(); // PERF FIX 4
133
114
  text.enterEditing();
134
115
  text.selectAll();
135
116
  };
117
+ // ── addImageToCanvas ──────────────────────────────────────────────────────────
136
118
  export const addImageToCanvas = async (canvas, url) => {
137
119
  if (!canvas)
138
120
  return;
139
- // 1. Get the current Viewport Transform (the Pan and Zoom matrix)
140
121
  const vpt = canvas.viewportTransform;
141
122
  const zoom = canvas.getZoom();
142
- // 2. Calculate the exact Scene coordinates for the center
143
- // Formula: (-PanOffset / Zoom) + (CanvasDimension / 2 / Zoom)
144
123
  const centerX = (-vpt[4] + canvas.width / 2) / zoom;
145
124
  const centerY = (-vpt[5] + canvas.height / 2) / zoom;
146
125
  try {
147
126
  const img = await FabricImage.fromURL(url);
148
- // 3. Dynamic Scaling (Make it fit roughly 40% of the screen height)
149
127
  const targetHeight = (canvas.height / zoom) * 0.4;
150
128
  img.scaleToHeight(targetHeight);
151
- img.set({
152
- left: centerX,
153
- top: centerY,
154
- originX: "center",
155
- originY: "center",
156
- // cornerStyle: "circle",
157
- // transparentCorners: false,
158
- // cornerColor: "#3b82f6", // Nice modern blue
159
- // cornerStrokeColor: "#ffffff",
160
- padding: 10
161
- });
129
+ img.set({ left: centerX, top: centerY, originX: "center", originY: "center", padding: 10 });
162
130
  canvas.add(img);
163
131
  canvas.setActiveObject(img);
164
- canvas.renderAll();
132
+ canvas.requestRenderAll(); // PERF FIX 5
165
133
  return img;
166
134
  }
167
135
  catch (error) {
168
136
  console.error("Image load failed", error);
169
137
  }
170
138
  };
171
- // Clear entire canvas
139
+ // ── clearCanvas ───────────────────────────────────────────────────────────────
172
140
  export const clearCanvas = (canvas) => {
173
141
  canvas.clear();
174
142
  canvas.backgroundColor = "transparent";
175
- canvas.renderAll();
143
+ canvas.requestRenderAll(); // PERF FIX 6
176
144
  };
177
- // Zoom canvas
145
+ // ── zoomCanvas ────────────────────────────────────────────────────────────────
178
146
  export const zoomCanvas = (canvas, zoomLevel) => {
179
147
  canvas.setZoom(zoomLevel);
180
- canvas.renderAll();
148
+ canvas.requestRenderAll(); // PERF FIX 7
181
149
  };
182
- // Reset view to fit all objects
150
+ // ── fitCanvasToObjects ────────────────────────────────────────────────────────
183
151
  export const fitCanvasToObjects = (canvas) => {
184
152
  const objects = canvas.getObjects();
185
- if (objects.length === 0)
153
+ if (!objects.length)
186
154
  return;
187
- const group = new Group(objects);
188
- const groupWidth = group.width || 0;
189
- const groupHeight = group.height || 0;
190
- const canvasWidth = canvas.getWidth();
191
- const canvasHeight = canvas.getHeight();
192
- const zoom = Math.min(canvasWidth / (groupWidth + 100), canvasHeight / (groupHeight + 100));
155
+ // ── PERF FIX 8: Use getBoundingRect instead of creating a Group ───────────
156
+ // Old code: new Group(objects) — instantiates a full Fabric Group object
157
+ // with layout recalculation for every object just to get bounds. O(n) object
158
+ // creation + layout for a measurement-only operation.
159
+ // getBoundingRect() computes bounds from existing coords — no allocation.
160
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
161
+ for (const obj of objects) {
162
+ const bounds = obj.getBoundingRect();
163
+ if (bounds.left < minX)
164
+ minX = bounds.left;
165
+ if (bounds.top < minY)
166
+ minY = bounds.top;
167
+ if (bounds.left + bounds.width > maxX)
168
+ maxX = bounds.left + bounds.width;
169
+ if (bounds.top + bounds.height > maxY)
170
+ maxY = bounds.top + bounds.height;
171
+ }
172
+ const contentW = maxX - minX;
173
+ const contentH = maxY - minY;
174
+ const padding = 100;
175
+ const zoom = Math.min(canvas.getWidth() / (contentW + padding), canvas.getHeight() / (contentH + padding));
193
176
  canvas.setZoom(zoom);
194
- canvas.viewportCenterObject(group);
195
- // group.destroy();
196
- canvas.renderAll();
177
+ // Center the content
178
+ const vpt = canvas.viewportTransform;
179
+ if (vpt) {
180
+ vpt[4] = (canvas.getWidth() - contentW * zoom) / 2 - minX * zoom;
181
+ vpt[5] = (canvas.getHeight() - contentH * zoom) / 2 - minY * zoom;
182
+ }
183
+ canvas.requestRenderAll();
197
184
  };
185
+ // ── addWelcomeContent ─────────────────────────────────────────────────────────
198
186
  export const addWelcomeContent = async (canvas, logoUrl) => {
199
187
  const centerX = canvas.getWidth() / 2;
200
188
  const centerY = canvas.getHeight() / 2;
201
- // 1. Load the Logo Image
202
- const logoImg = await FabricImage.fromURL(logoUrl, {
203
- crossOrigin: 'anonymous'
204
- });
205
- // Scale logo to fit (e.g., 80px width)
189
+ const logoImg = await FabricImage.fromURL(logoUrl, { crossOrigin: "anonymous" });
206
190
  const scale = 60 / logoImg.width;
207
- logoImg.set({
208
- scaleX: scale,
209
- scaleY: scale,
210
- originX: "center",
211
- originY: "center",
212
- top: -120, // Relative to group center
213
- });
214
- // 2. Creative Background "Aura" (The Glow)
215
- // const aura = new Circle({
216
- // radius: 100,
217
- // fill: "radial-gradient(circle, rgba(99, 102, 241, 0.15) 0%, rgba(255, 255, 255, 0) 70%)",
218
- // originX: "center",
219
- // originY: "center",
220
- // top: -120,
221
- // selectable: false,
222
- // evented: false,
223
- // });
224
- // 3. High-End Typography
191
+ logoImg.set({ scaleX: scale, scaleY: scale, originX: "center", originY: "center", top: -120 });
225
192
  const mainTitle = new Text("EasyFlow", {
226
- top: -20,
227
- fontSize: 84,
228
- fontFamily: "'cursive', 'Segoe UI', Roboto, sans-serif",
229
- fontWeight: "700",
230
- fill: "#029AFF",
231
- charSpacing: -20, // Tight kerning for a modern look
232
- originX: "center",
193
+ top: -20, fontSize: 84, fontFamily: "'cursive', 'Segoe UI', Roboto, sans-serif",
194
+ fontWeight: "700", fill: "#029AFF", charSpacing: -20, originX: "center",
233
195
  });
234
196
  const subTitle = new Text("Whiteboard", {
235
- top: 55,
236
- fontSize: 38,
237
- fontFamily: "'Inter', 'Segoe UI', Roboto, sans-serif",
238
- fontWeight: "300", // Thin contrast against the bold "EasyFlow"
239
- fill: "#fff",
240
- originX: "center",
197
+ top: 55, fontSize: 38, fontFamily: "'Inter', 'Segoe UI', Roboto, sans-serif",
198
+ fontWeight: "300", fill: "#fff", originX: "center",
241
199
  });
242
200
  const hint = new Text("SELECT A TOOL TO START CREATING", {
243
- top: 110,
244
- fontSize: 12,
245
- fontFamily: "monospace", // Techy/Creative feel
246
- fontWeight: "600",
247
- fill: "#9ca3af",
248
- charSpacing: 200, // Wide spacing for the hint
249
- originX: "center",
201
+ top: 110, fontSize: 12, fontFamily: "monospace",
202
+ fontWeight: "600", fill: "#9ca3af", charSpacing: 200, originX: "center",
250
203
  });
251
- // 4. Create Master Hero Group
252
204
  const heroGroup = new Group([logoImg, mainTitle, subTitle, hint], {
253
- left: centerX,
254
- top: centerY + 20, // Start slightly lower for "float-up" animation
255
- originX: "center",
256
- originY: "center",
257
- selectable: false,
258
- evented: false,
259
- opacity: 0,
205
+ left: centerX, top: centerY + 20,
206
+ originX: "center", originY: "center",
207
+ selectable: false, evented: false, opacity: 0,
260
208
  });
261
209
  heroGroup.id = "welcome-hero";
262
210
  canvas.add(heroGroup);
263
- // 5. "Float & Fade" Animation
264
- heroGroup.animate({
265
- opacity: 1,
266
- top: centerY // Moves up 20px while fading in
267
- }, {
211
+ heroGroup.animate({ opacity: 1, top: centerY }, {
268
212
  duration: 1200,
269
213
  easing: util.ease.easeOutQuart,
270
214
  onChange: () => canvas.requestRenderAll(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhamz.01/easyflow-whiteboard",
3
- "version": "2.20.0",
3
+ "version": "2.21.0",
4
4
  "description": "A feature-rich whiteboard component built with Fabric.js and React",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",