@twick/canvas 0.14.2 → 0.14.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 CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  A React-based canvas library built with Fabric.js for video and image manipulation.
4
4
 
5
+ ## Overview
6
+
7
+ This package provides a comprehensive canvas solution for video and image editing, built on top of Fabric.js with React integration. It offers advanced manipulation capabilities for creating professional video editing applications.
8
+
5
9
  ## Requirements
6
10
 
7
11
  - Browser environment with Canvas and Video support
@@ -11,10 +15,12 @@ A React-based canvas library built with Fabric.js for video and image manipulati
11
15
  ## Installation
12
16
 
13
17
  ```bash
14
- pnpm install @twick/canvas
18
+ npm install @twick/canvas
19
+ # or
20
+ pnpm add @twick/canvas
15
21
  ```
16
22
 
17
- ## Usage
23
+ ## Quick Start
18
24
 
19
25
  ### Basic Canvas Setup
20
26
 
@@ -116,6 +122,8 @@ addElementToCanvas({ element: textElement, index: 1 });
116
122
  - `CanvasElementProps`: Canvas element props interface
117
123
  - `CaptionProps`: Caption configuration props
118
124
 
125
+ For complete API documentation, refer to the generated documentation.
126
+
119
127
  ## Browser Support
120
128
 
121
129
  This library requires a browser environment with support for:
@@ -125,6 +133,18 @@ This library requires a browser environment with support for:
125
133
 
126
134
  The library will throw appropriate errors if used in an unsupported environment.
127
135
 
136
+ ## Documentation
137
+
138
+ For complete documentation, refer to the project documentation site.
139
+
128
140
  ## License
129
141
 
130
- Apache-2.0
142
+ This package is licensed under the **Sustainable Use License (SUL) Version 1.0**.
143
+
144
+ - Free for use in commercial and non-commercial apps
145
+ - Can be modified and self-hosted
146
+ - Cannot be sold, rebranded, or distributed as a standalone SDK
147
+
148
+ For commercial licensing inquiries, contact: contact@kifferai.com
149
+
150
+ For full license terms, see the main LICENSE.md file in the project root.
package/dist/index.js CHANGED
@@ -3,36 +3,61 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const fabric = require("fabric");
4
4
  const react = require("react");
5
5
  const DEFAULT_TEXT_PROPS = {
6
+ /** Font family for text elements */
6
7
  family: "Poppins",
8
+ /** Font size in pixels */
7
9
  size: 48,
10
+ /** Text fill color */
8
11
  fill: "#FFFFFF",
12
+ /** Text stroke color */
9
13
  stroke: "#000000",
14
+ /** Stroke line width */
10
15
  lineWidth: 0
11
16
  };
12
17
  const DEFAULT_CAPTION_PROPS = {
18
+ /** Font family for caption elements */
13
19
  family: "Poppins",
20
+ /** Font size in pixels */
14
21
  size: 48,
22
+ /** Text fill color */
15
23
  fill: "#FFFFFF",
24
+ /** Font weight */
16
25
  fontWeight: 600,
26
+ /** Text stroke color */
17
27
  stroke: "#000000",
28
+ /** Stroke line width */
18
29
  lineWidth: 0.2,
30
+ /** Shadow color */
19
31
  shadowColor: "#000000",
32
+ /** Shadow blur radius */
20
33
  shadowBlur: 2,
34
+ /** Shadow offset [x, y] */
21
35
  shadowOffset: [0, 0]
22
36
  };
23
37
  const CANVAS_OPERATIONS = {
38
+ /** An item has been selected on the canvas */
24
39
  ITEM_SELECTED: "ITEM_SELECTED",
40
+ /** An item has been updated/modified on the canvas */
25
41
  ITEM_UPDATED: "ITEM_UPDATED",
42
+ /** An item has been deleted from the canvas */
26
43
  ITEM_DELETED: "ITEM_DELETED",
44
+ /** A new item has been added to the canvas */
27
45
  ITEM_ADDED: "ITEM_ADDED",
46
+ /** Items have been grouped together */
28
47
  ITEM_GROUPED: "ITEM_GROUPED",
48
+ /** Items have been ungrouped */
29
49
  ITEM_UNGROUPED: "ITEM_UNGROUPED"
30
50
  };
31
51
  const ELEMENT_TYPES = {
52
+ /** Text element type */
32
53
  TEXT: "text",
54
+ /** Caption element type */
33
55
  CAPTION: "caption",
56
+ /** Image element type */
34
57
  IMAGE: "image",
58
+ /** Video element type */
35
59
  VIDEO: "video",
60
+ /** Rectangle element type */
36
61
  RECT: "rect"
37
62
  };
38
63
  const isBrowser = typeof window !== "undefined";
@@ -106,10 +131,19 @@ const reorderElementsByZIndex = (canvas) => {
106
131
  objects.forEach((obj) => canvas.add(obj));
107
132
  canvas.renderAll();
108
133
  };
134
+ const getCanvasContext = (canvas) => {
135
+ var _a, _b, _c, _d;
136
+ if (!canvas || !((_b = (_a = canvas.elements) == null ? void 0 : _a.lower) == null ? void 0 : _b.ctx)) return;
137
+ return (_d = (_c = canvas.elements) == null ? void 0 : _c.lower) == null ? void 0 : _d.ctx;
138
+ };
109
139
  const clearCanvas = (canvas) => {
110
- if (!canvas) return;
111
- canvas.clear();
112
- canvas.renderAll();
140
+ try {
141
+ if (!canvas || !getCanvasContext(canvas)) return;
142
+ canvas.clear();
143
+ canvas.renderAll();
144
+ } catch (error) {
145
+ console.warn("Error clearing canvas:", error);
146
+ }
113
147
  };
114
148
  const convertToCanvasPosition = (x, y, canvasMetadata) => {
115
149
  return {
@@ -135,14 +169,21 @@ const getCurrentFrameEffect = (item, seekTime) => {
135
169
  return currentFrameEffect;
136
170
  };
137
171
  const disabledControl = new fabric.Control({
172
+ /** X position offset */
138
173
  x: 0,
174
+ /** Y position offset */
139
175
  y: -0.5,
176
+ /** Additional Y offset */
140
177
  offsetY: 0,
178
+ /** Cursor style when hovering */
141
179
  cursorStyle: "pointer",
180
+ /** Action handler that does nothing */
142
181
  actionHandler: () => {
143
182
  return true;
144
183
  },
184
+ /** Name of the action */
145
185
  actionName: "scale",
186
+ /** Render function for the control */
146
187
  render: function(ctx, left, top) {
147
188
  const size = 0;
148
189
  ctx.save();
@@ -153,12 +194,19 @@ const disabledControl = new fabric.Control({
153
194
  }
154
195
  });
155
196
  const rotateControl = new fabric.Control({
197
+ /** X position offset */
156
198
  x: 0,
199
+ /** Y position offset */
157
200
  y: -0.5,
201
+ /** Additional Y offset for positioning */
158
202
  offsetY: -25,
203
+ /** Cursor style when hovering */
159
204
  cursorStyle: "crosshair",
205
+ /** Action handler with rotation and snapping */
160
206
  actionHandler: fabric.controlsUtils.rotationWithSnapping,
207
+ /** Name of the action */
161
208
  actionName: "rotate",
209
+ /** Whether to show connection line */
162
210
  withConnection: true
163
211
  });
164
212
  const getThumbnail = async (videoUrl, seekTime = 0.1, playbackRate = 1) => {
@@ -241,7 +289,7 @@ const getThumbnail = async (videoUrl, seekTime = 0.1, playbackRate = 1) => {
241
289
  timeoutId = window.setTimeout(() => {
242
290
  cleanup();
243
291
  reject(new Error("Video loading timed out"));
244
- }, 5e3);
292
+ }, 15e3);
245
293
  video.src = videoUrl;
246
294
  document.body.appendChild(video);
247
295
  });
@@ -292,7 +340,7 @@ const addTextElement = ({
292
340
  canvas,
293
341
  canvasMetadata
294
342
  }) => {
295
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v;
343
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w;
296
344
  const { x, y } = convertToCanvasPosition(
297
345
  ((_a = element.props) == null ? void 0 : _a.x) || 0,
298
346
  ((_b = element.props) == null ? void 0 : _b.y) || 0,
@@ -311,15 +359,16 @@ const addTextElement = ({
311
359
  fontStyle: ((_g = element.props) == null ? void 0 : _g.fontStyle) || "normal",
312
360
  fontWeight: ((_h = element.props) == null ? void 0 : _h.fontWeight) || "normal",
313
361
  fill: ((_i = element.props) == null ? void 0 : _i.fill) || DEFAULT_TEXT_PROPS.fill,
362
+ opacity: ((_j = element.props) == null ? void 0 : _j.opacity) ?? 1,
314
363
  skipWrapping: false,
315
- textAlign: ((_j = element.props) == null ? void 0 : _j.textAlign) || "center",
316
- stroke: ((_k = element.props) == null ? void 0 : _k.stroke) || DEFAULT_TEXT_PROPS.stroke,
317
- strokeWidth: ((_l = element.props) == null ? void 0 : _l.lineWidth) || DEFAULT_TEXT_PROPS.lineWidth,
318
- shadow: ((_m = element.props) == null ? void 0 : _m.shadowColor) ? new fabric.Shadow({
319
- offsetX: ((_o = (_n = element.props) == null ? void 0 : _n.shadowOffset) == null ? void 0 : _o.length) && ((_q = (_p = element.props) == null ? void 0 : _p.shadowOffset) == null ? void 0 : _q.length) > 1 ? element.props.shadowOffset[0] / 2 : 1,
320
- offsetY: ((_s = (_r = element.props) == null ? void 0 : _r.shadowOffset) == null ? void 0 : _s.length) && ((_t = element.props) == null ? void 0 : _t.shadowOffset.length) > 1 ? element.props.shadowOffset[1] / 2 : 1,
321
- blur: (((_u = element.props) == null ? void 0 : _u.shadowBlur) || 2) / 2,
322
- color: (_v = element.props) == null ? void 0 : _v.shadowColor
364
+ textAlign: ((_k = element.props) == null ? void 0 : _k.textAlign) || "center",
365
+ stroke: ((_l = element.props) == null ? void 0 : _l.stroke) || DEFAULT_TEXT_PROPS.stroke,
366
+ strokeWidth: ((_m = element.props) == null ? void 0 : _m.lineWidth) || DEFAULT_TEXT_PROPS.lineWidth,
367
+ shadow: ((_n = element.props) == null ? void 0 : _n.shadowColor) ? new fabric.Shadow({
368
+ offsetX: ((_p = (_o = element.props) == null ? void 0 : _o.shadowOffset) == null ? void 0 : _p.length) && ((_r = (_q = element.props) == null ? void 0 : _q.shadowOffset) == null ? void 0 : _r.length) > 1 ? element.props.shadowOffset[0] / 2 : 1,
369
+ offsetY: ((_t = (_s = element.props) == null ? void 0 : _s.shadowOffset) == null ? void 0 : _t.length) && ((_u = element.props) == null ? void 0 : _u.shadowOffset.length) > 1 ? element.props.shadowOffset[1] / 2 : 1,
370
+ blur: (((_v = element.props) == null ? void 0 : _v.shadowBlur) || 2) / 2,
371
+ color: (_w = element.props) == null ? void 0 : _w.shadowColor
323
372
  }) : void 0
324
373
  });
325
374
  text.set("id", element.id);
@@ -342,7 +391,7 @@ const setImageProps = ({
342
391
  index,
343
392
  canvasMetadata
344
393
  }) => {
345
- var _a, _b, _c, _d;
394
+ var _a, _b, _c, _d, _e;
346
395
  const width = (((_a = element.props) == null ? void 0 : _a.width) || 0) * canvasMetadata.scaleX || canvasMetadata.width;
347
396
  const height = (((_b = element.props) == null ? void 0 : _b.height) || 0) * canvasMetadata.scaleY || canvasMetadata.height;
348
397
  const { x, y } = convertToCanvasPosition(
@@ -357,6 +406,7 @@ const setImageProps = ({
357
406
  img.set("height", height);
358
407
  img.set("left", x);
359
408
  img.set("top", y);
409
+ img.set("opacity", ((_e = element.props) == null ? void 0 : _e.opacity) ?? 1);
360
410
  img.set("selectable", true);
361
411
  img.set("hasControls", true);
362
412
  img.set("touchAction", "all");
@@ -368,7 +418,7 @@ const addCaptionElement = ({
368
418
  captionProps,
369
419
  canvasMetadata
370
420
  }) => {
371
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
421
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A;
372
422
  const { x, y } = convertToCanvasPosition(
373
423
  ((_b = (_a = element.props) == null ? void 0 : _a.pos) == null ? void 0 : _b.x) || ((_c = captionProps == null ? void 0 : captionProps.pos) == null ? void 0 : _c.x) || 0,
374
424
  ((_e = (_d = element.props) == null ? void 0 : _d.pos) == null ? void 0 : _e.y) || ((_f = captionProps == null ? void 0 : captionProps.pos) == null ? void 0 : _f.y) || 0,
@@ -387,13 +437,14 @@ const addCaptionElement = ({
387
437
  fill: ((_o = element.props) == null ? void 0 : _o.fill) || ((_p = captionProps.color) == null ? void 0 : _p.text) || DEFAULT_CAPTION_PROPS.fill,
388
438
  fontWeight: DEFAULT_CAPTION_PROPS.fontWeight,
389
439
  stroke: ((_q = element.props) == null ? void 0 : _q.stroke) || DEFAULT_CAPTION_PROPS.stroke,
440
+ opacity: ((_r = element.props) == null ? void 0 : _r.opacity) ?? 1,
390
441
  shadow: new fabric.Shadow({
391
- offsetX: ((_s = (_r = element.props) == null ? void 0 : _r.shadowOffset) == null ? void 0 : _s[0]) || ((_t = DEFAULT_CAPTION_PROPS.shadowOffset) == null ? void 0 : _t[0]),
392
- offsetY: ((_v = (_u = element.props) == null ? void 0 : _u.shadowOffset) == null ? void 0 : _v[1]) || ((_w = DEFAULT_CAPTION_PROPS.shadowOffset) == null ? void 0 : _w[1]),
393
- blur: ((_x = element.props) == null ? void 0 : _x.shadowBlur) || DEFAULT_CAPTION_PROPS.shadowBlur,
394
- color: ((_y = element.props) == null ? void 0 : _y.shadowColor) || DEFAULT_CAPTION_PROPS.shadowColor
442
+ offsetX: ((_t = (_s = element.props) == null ? void 0 : _s.shadowOffset) == null ? void 0 : _t[0]) || ((_u = DEFAULT_CAPTION_PROPS.shadowOffset) == null ? void 0 : _u[0]),
443
+ offsetY: ((_w = (_v = element.props) == null ? void 0 : _v.shadowOffset) == null ? void 0 : _w[1]) || ((_x = DEFAULT_CAPTION_PROPS.shadowOffset) == null ? void 0 : _x[1]),
444
+ blur: ((_y = element.props) == null ? void 0 : _y.shadowBlur) || DEFAULT_CAPTION_PROPS.shadowBlur,
445
+ color: ((_z = element.props) == null ? void 0 : _z.shadowColor) || DEFAULT_CAPTION_PROPS.shadowColor
395
446
  }),
396
- strokeWidth: ((_z = element.props) == null ? void 0 : _z.lineWidth) || DEFAULT_CAPTION_PROPS.lineWidth
447
+ strokeWidth: ((_A = element.props) == null ? void 0 : _A.lineWidth) || DEFAULT_CAPTION_PROPS.lineWidth
397
448
  });
398
449
  caption.set("id", element.id);
399
450
  caption.set("zIndex", index);
@@ -484,7 +535,7 @@ const addMediaGroup = ({
484
535
  canvasMetadata,
485
536
  currentFrameEffect
486
537
  }) => {
487
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
538
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
488
539
  let frameSize;
489
540
  let angle;
490
541
  let framePosition;
@@ -539,7 +590,8 @@ const addMediaGroup = ({
539
590
  originX: "center",
540
591
  originY: "center",
541
592
  scaleX: newSize.width / img.width,
542
- scaleY: newSize.height / img.height
593
+ scaleY: newSize.height / img.height,
594
+ opacity: ((_n = element.props) == null ? void 0 : _n.opacity) ?? 1
543
595
  });
544
596
  const { x, y } = convertToCanvasPosition(
545
597
  (framePosition == null ? void 0 : framePosition.x) || 0,
@@ -659,6 +711,7 @@ const useTwickCanvas = ({
659
711
  const elementFrameMap = react.useRef({});
660
712
  const twickCanvasRef = react.useRef(null);
661
713
  const videoSizeRef = react.useRef({ width: 1, height: 1 });
714
+ const canvasResolutionRef = react.useRef({ width: 1, height: 1 });
662
715
  const canvasMetadataRef = react.useRef({
663
716
  width: 0,
664
717
  height: 0,
@@ -682,9 +735,13 @@ const useTwickCanvas = ({
682
735
  selectionLineWidth = 2,
683
736
  uniScaleTransform = true,
684
737
  enableRetinaScaling = true,
685
- touchZoomThreshold = 10
738
+ touchZoomThreshold = 10,
739
+ forceBuild = false
686
740
  }) => {
687
741
  if (!canvasRef) return;
742
+ if (!forceBuild && canvasResolutionRef.current.width === canvasSize.width && canvasResolutionRef.current.height === canvasSize.height) {
743
+ return;
744
+ }
688
745
  if (twickCanvasRef.current) {
689
746
  console.log("Destroying twickCanvas");
690
747
  twickCanvasRef.current.off("mouse:up", handleMouseUp);
@@ -704,6 +761,7 @@ const useTwickCanvas = ({
704
761
  canvasMetadataRef.current = canvasMetadata;
705
762
  videoSizeRef.current = videoSize;
706
763
  canvas == null ? void 0 : canvas.on("mouse:up", handleMouseUp);
764
+ canvasResolutionRef.current = canvasSize;
707
765
  setTwickCanvas(canvas);
708
766
  twickCanvasRef.current = canvas;
709
767
  if (onCanvasReady) {
@@ -844,13 +902,18 @@ const useTwickCanvas = ({
844
902
  captionProps,
845
903
  cleanAndAdd = false
846
904
  }) => {
847
- if (!twickCanvas) {
848
- console.warn("Canvas not initialized");
905
+ if (!twickCanvas || !getCanvasContext(twickCanvas)) {
906
+ console.warn("Canvas not properly initialized");
849
907
  return;
850
908
  }
851
909
  try {
852
- if (cleanAndAdd) {
910
+ if (cleanAndAdd && getCanvasContext(twickCanvas)) {
911
+ const backgroundColor = twickCanvas.backgroundColor;
853
912
  clearCanvas(twickCanvas);
913
+ if (backgroundColor) {
914
+ twickCanvas.backgroundColor = backgroundColor;
915
+ twickCanvas.renderAll();
916
+ }
854
917
  }
855
918
  await Promise.all(
856
919
  elements.map(async (element, index) => {