apexify.js 4.9.26 → 4.9.28

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.
Files changed (108) hide show
  1. package/README.md +437 -47
  2. package/dist/cjs/Canvas/ApexPainter.d.ts +122 -78
  3. package/dist/cjs/Canvas/ApexPainter.d.ts.map +1 -1
  4. package/dist/cjs/Canvas/ApexPainter.js +461 -352
  5. package/dist/cjs/Canvas/ApexPainter.js.map +1 -1
  6. package/dist/cjs/Canvas/utils/Background/bg.d.ts +23 -11
  7. package/dist/cjs/Canvas/utils/Background/bg.d.ts.map +1 -1
  8. package/dist/cjs/Canvas/utils/Background/bg.js +174 -107
  9. package/dist/cjs/Canvas/utils/Background/bg.js.map +1 -1
  10. package/dist/cjs/Canvas/utils/Custom/customLines.js +2 -2
  11. package/dist/cjs/Canvas/utils/Custom/customLines.js.map +1 -1
  12. package/dist/cjs/Canvas/utils/Image/imageFilters.d.ts +11 -0
  13. package/dist/cjs/Canvas/utils/Image/imageFilters.d.ts.map +1 -0
  14. package/dist/cjs/Canvas/utils/Image/imageFilters.js +307 -0
  15. package/dist/cjs/Canvas/utils/Image/imageFilters.js.map +1 -0
  16. package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts +47 -112
  17. package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
  18. package/dist/cjs/Canvas/utils/Image/imageProperties.js +229 -560
  19. package/dist/cjs/Canvas/utils/Image/imageProperties.js.map +1 -1
  20. package/dist/cjs/Canvas/utils/Image/professionalImageFilters.d.ts +11 -0
  21. package/dist/cjs/Canvas/utils/Image/professionalImageFilters.d.ts.map +1 -0
  22. package/dist/cjs/Canvas/utils/Image/professionalImageFilters.js +351 -0
  23. package/dist/cjs/Canvas/utils/Image/professionalImageFilters.js.map +1 -0
  24. package/dist/cjs/Canvas/utils/Image/simpleProfessionalFilters.d.ts +11 -0
  25. package/dist/cjs/Canvas/utils/Image/simpleProfessionalFilters.d.ts.map +1 -0
  26. package/dist/cjs/Canvas/utils/Image/simpleProfessionalFilters.js +215 -0
  27. package/dist/cjs/Canvas/utils/Image/simpleProfessionalFilters.js.map +1 -0
  28. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts +71 -0
  29. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -0
  30. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js +392 -0
  31. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -0
  32. package/dist/cjs/Canvas/utils/Shapes/shapes.d.ts +29 -0
  33. package/dist/cjs/Canvas/utils/Shapes/shapes.d.ts.map +1 -0
  34. package/dist/cjs/Canvas/utils/Shapes/shapes.js +334 -0
  35. package/dist/cjs/Canvas/utils/Shapes/shapes.js.map +1 -0
  36. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts +127 -0
  37. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -0
  38. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js +365 -0
  39. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -0
  40. package/dist/cjs/Canvas/utils/types.d.ts +227 -131
  41. package/dist/cjs/Canvas/utils/types.d.ts.map +1 -1
  42. package/dist/cjs/Canvas/utils/types.js +0 -1
  43. package/dist/cjs/Canvas/utils/types.js.map +1 -1
  44. package/dist/cjs/Canvas/utils/utils.d.ts +7 -4
  45. package/dist/cjs/Canvas/utils/utils.d.ts.map +1 -1
  46. package/dist/cjs/Canvas/utils/utils.js +17 -7
  47. package/dist/cjs/Canvas/utils/utils.js.map +1 -1
  48. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  49. package/dist/esm/Canvas/ApexPainter.d.ts +122 -78
  50. package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
  51. package/dist/esm/Canvas/ApexPainter.js +461 -352
  52. package/dist/esm/Canvas/ApexPainter.js.map +1 -1
  53. package/dist/esm/Canvas/utils/Background/bg.d.ts +23 -11
  54. package/dist/esm/Canvas/utils/Background/bg.d.ts.map +1 -1
  55. package/dist/esm/Canvas/utils/Background/bg.js +174 -107
  56. package/dist/esm/Canvas/utils/Background/bg.js.map +1 -1
  57. package/dist/esm/Canvas/utils/Custom/customLines.js +2 -2
  58. package/dist/esm/Canvas/utils/Custom/customLines.js.map +1 -1
  59. package/dist/esm/Canvas/utils/Image/imageFilters.d.ts +11 -0
  60. package/dist/esm/Canvas/utils/Image/imageFilters.d.ts.map +1 -0
  61. package/dist/esm/Canvas/utils/Image/imageFilters.js +307 -0
  62. package/dist/esm/Canvas/utils/Image/imageFilters.js.map +1 -0
  63. package/dist/esm/Canvas/utils/Image/imageProperties.d.ts +47 -112
  64. package/dist/esm/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
  65. package/dist/esm/Canvas/utils/Image/imageProperties.js +229 -560
  66. package/dist/esm/Canvas/utils/Image/imageProperties.js.map +1 -1
  67. package/dist/esm/Canvas/utils/Image/professionalImageFilters.d.ts +11 -0
  68. package/dist/esm/Canvas/utils/Image/professionalImageFilters.d.ts.map +1 -0
  69. package/dist/esm/Canvas/utils/Image/professionalImageFilters.js +351 -0
  70. package/dist/esm/Canvas/utils/Image/professionalImageFilters.js.map +1 -0
  71. package/dist/esm/Canvas/utils/Image/simpleProfessionalFilters.d.ts +11 -0
  72. package/dist/esm/Canvas/utils/Image/simpleProfessionalFilters.d.ts.map +1 -0
  73. package/dist/esm/Canvas/utils/Image/simpleProfessionalFilters.js +215 -0
  74. package/dist/esm/Canvas/utils/Image/simpleProfessionalFilters.js.map +1 -0
  75. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts +71 -0
  76. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -0
  77. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js +392 -0
  78. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -0
  79. package/dist/esm/Canvas/utils/Shapes/shapes.d.ts +29 -0
  80. package/dist/esm/Canvas/utils/Shapes/shapes.d.ts.map +1 -0
  81. package/dist/esm/Canvas/utils/Shapes/shapes.js +334 -0
  82. package/dist/esm/Canvas/utils/Shapes/shapes.js.map +1 -0
  83. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts +127 -0
  84. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -0
  85. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js +365 -0
  86. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -0
  87. package/dist/esm/Canvas/utils/types.d.ts +227 -131
  88. package/dist/esm/Canvas/utils/types.d.ts.map +1 -1
  89. package/dist/esm/Canvas/utils/types.js +0 -1
  90. package/dist/esm/Canvas/utils/types.js.map +1 -1
  91. package/dist/esm/Canvas/utils/utils.d.ts +7 -4
  92. package/dist/esm/Canvas/utils/utils.d.ts.map +1 -1
  93. package/dist/esm/Canvas/utils/utils.js +17 -7
  94. package/dist/esm/Canvas/utils/utils.js.map +1 -1
  95. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  96. package/lib/Canvas/ApexPainter.ts +1325 -1218
  97. package/lib/Canvas/utils/Background/bg.ts +247 -173
  98. package/lib/Canvas/utils/Custom/customLines.ts +3 -3
  99. package/lib/Canvas/utils/Image/imageFilters.ts +356 -0
  100. package/lib/Canvas/utils/Image/imageProperties.ts +322 -775
  101. package/lib/Canvas/utils/Image/professionalImageFilters.ts +391 -0
  102. package/lib/Canvas/utils/Image/simpleProfessionalFilters.ts +229 -0
  103. package/lib/Canvas/utils/Patterns/enhancedPatternRenderer.ts +444 -0
  104. package/lib/Canvas/utils/Shapes/shapes.ts +528 -0
  105. package/lib/Canvas/utils/Texts/enhancedTextRenderer.ts +478 -0
  106. package/lib/Canvas/utils/types.ts +301 -117
  107. package/lib/Canvas/utils/utils.ts +85 -72
  108. package/package.json +106 -188
@@ -1,835 +1,382 @@
1
- import { Image, SKRSContext2D } from "@napi-rs/canvas";
2
- import { ImageProperties } from "../utils";
1
+ import type { SKRSContext2D } from "@napi-rs/canvas";
2
+ import type { borderPosition } from "../types";
3
3
 
4
- /**
5
- * Applies shadow to the canvas context.
6
- * @param ctx The canvas rendering context.
7
- * @param shadow The shadow properties.
8
- * @param x The x-coordinate of the shape.
9
- * @param y The y-coordinate of the shape.
10
- * @param width The width of the shape.
11
- * @param height The height of the shape.
12
- */
13
- export function applyShadow(
14
- ctx: SKRSContext2D,
15
- shadow: ImageProperties['shadow'],
16
- x: number,
17
- y: number,
18
- width: number,
19
- height: number
20
- ): void {
21
- ctx.save();
22
-
23
- if (shadow) {
24
- ctx.globalAlpha = shadow.opacity ?? 1;
25
- ctx.filter = `blur(${shadow.blur ?? 0}px)`;
26
-
27
- const shadowX = x + (shadow.offsetX ?? 0);
28
- const shadowY = y + (shadow.offsetY ?? 0);
29
-
30
- objectRadius(
31
- ctx,
32
- shadowX,
33
- shadowY,
34
- width,
35
- height,
36
- shadow.borderRadius ?? 2,
37
- );
38
-
39
- ctx.fillStyle = shadow.gradient
40
- ? createGradient(
41
- ctx, shadow.gradient,
42
- shadowX, shadowY,
43
- shadowX + width, shadowY + height
44
- )
45
- : shadow.color || "transparent";
46
- ctx.fill();
47
- }
48
-
49
- ctx.filter = "none";
50
- ctx.globalAlpha = 1;
51
- ctx.restore();
52
- }
53
-
54
- /**
55
- * Applies zoom (scaling) to the canvas context around a specified point.
56
- * @param ctx The canvas rendering context.
57
- * @param zoom An object with scale (zoom factor) and the x/y coordinates that act as the zoom origin.
58
- */
59
- export function applyZoom(
4
+ export function buildPath(
60
5
  ctx: SKRSContext2D,
61
- zoom?: { scale?: number; x?: number; y?: number }
6
+ x: number, y: number, w: number, h: number,
7
+ radius: number | "circular" = 0,
8
+ borderPos: borderPosition = "all"
62
9
  ): void {
63
- if (!zoom) return;
64
- const scale = zoom.scale ?? 1;
65
- const zoomX = zoom.x ?? 0;
66
- const zoomY = zoom.y ?? 0;
67
-
68
- ctx.translate(zoomX, zoomY);
69
- ctx.scale(scale, scale);
70
- ctx.translate(-zoomX, -zoomY);
71
- }
72
-
73
-
74
- /**
75
- * Applies stroke to the canvas context with support for selective side strokes.
76
- * Supports optional blur effect on the stroke.
77
- *
78
- * @param ctx The canvas rendering context.
79
- * @param stroke The stroke properties.
80
- * @param x The x-coordinate of the shape.
81
- * @param y The y-coordinate of the shape.
82
- * @param width The width of the shape.
83
- * @param height The height of the shape.
84
- * @param blur Optional blur effect on the stroke.
85
- */
86
- export function applyStroke(
87
- ctx: SKRSContext2D,
88
- stroke: ImageProperties["stroke"],
89
- x: number,
90
- y: number,
91
- width: number,
92
- height: number,
93
- shapeName?: string,
94
- ): void {
95
- if (!stroke) return;
96
-
97
- ctx.save();
10
+ ctx.beginPath();
98
11
 
99
- if (stroke.blur && stroke.blur > 0) {
100
- ctx.filter = `blur(${stroke.blur}px)`;
12
+ if (radius === "circular") {
13
+ const r = Math.min(w, h) / 2;
14
+ ctx.arc(x + w / 2, y + h / 2, r, 0, Math.PI * 2);
15
+ ctx.closePath();
16
+ return;
101
17
  }
102
18
 
103
- ctx.strokeStyle = stroke.gradient
104
- ? createGradient(ctx, stroke.gradient, x, y, x + width, y + height)
105
- : stroke.color || "transparent";
106
-
107
- ctx.lineWidth = stroke.width && stroke.width > 0 ? stroke.width : 2;
19
+ if (!radius || radius <= 0) {
20
+ ctx.rect(x, y, w, h);
21
+ ctx.closePath();
22
+ return;
23
+ }
108
24
 
109
- ctx.beginPath();
110
- const borderPos = stroke.position || 0;
25
+ const br = Math.min(radius, w / 2, h / 2);
26
+ const sel = new Set(borderPos.toLowerCase().split(",").map(s => s.trim()));
27
+
28
+ const has = (name: string) =>
29
+ sel.has("all") || sel.has(name) ||
30
+ (name === "top-left" && (sel.has("top") || sel.has("left"))) ||
31
+ (name === "top-right" && (sel.has("top") || sel.has("right"))) ||
32
+ (name === "bottom-right" && (sel.has("bottom") || sel.has("right"))) ||
33
+ (name === "bottom-left" && (sel.has("bottom") || sel.has("left")));
34
+
35
+ const tl = has("top-left") ? br : 0;
36
+ const tr = has("top-right") ? br : 0;
37
+ const brR= has("bottom-right") ? br : 0;
38
+ const bl = has("bottom-left") ? br : 0;
39
+
40
+ ctx.moveTo(x + tl, y);
41
+ ctx.lineTo(x + w - tr, y);
42
+ if (tr) ctx.arcTo(x + w, y, x + w, y + tr, tr);
43
+ ctx.lineTo(x + w, y + h - brR);
44
+ if (brR) ctx.arcTo(x + w, y + h, x + w - brR, y + h, brR);
45
+ ctx.lineTo(x + bl, y + h);
46
+ if (bl) ctx.arcTo(x, y + h, x, y + h - bl, bl);
47
+ ctx.lineTo(x, y + tl);
48
+ if (tl) ctx.arcTo(x, y, x + tl, y, tl);
111
49
 
112
- // Adjust the bounding box by borderPos
113
- // - Positive borderPos moves stroke outward
114
- // - Negative borderPos moves stroke inward
115
- const offsetX = x - borderPos;
116
- const offsetY = y - borderPos;
117
- const offsetW = width + 2 * borderPos;
118
- const offsetH = height + 2 * borderPos;
50
+ ctx.closePath();
51
+ }
119
52
 
120
- ctx.beginPath();
53
+ export function applyRotation(
54
+ ctx: SKRSContext2D,
55
+ deg: number | undefined,
56
+ x: number, y: number, w: number, h: number
57
+ ) {
58
+ if (!deg) return;
59
+ const cx = x + w / 2, cy = y + h / 2;
60
+ ctx.translate(cx, cy);
61
+ ctx.rotate((deg * Math.PI) / 180);
62
+ ctx.translate(-cx, -cy);
63
+ }
121
64
 
122
- // For known shapes (circle, star, etc.), re-draw with adjusted bounding box
123
- if (
124
- [
125
- "heart", "arrow", "circle", "star",
126
- "pentagon", "hexagon", "heptagon", "octagon",
127
- "diamond", "trapezoid", "kite",
128
- ].includes(shapeName as string)
129
- ) {
130
- switch (shapeName) {
131
- case "circle":
132
- // Circle centered in the new bounding box
133
- ctx.arc(
134
- offsetX + offsetW / 2,
135
- offsetY + offsetH / 2,
136
- offsetW / 2, // radius
137
- 0,
138
- Math.PI * 2
139
- );
140
- break;
141
-
142
- case "star":
143
- drawStar(ctx, offsetX, offsetY, offsetW, offsetH);
144
- break;
145
-
146
- case "arrow":
147
- drawArrow(ctx, offsetX, offsetY, offsetW, offsetH);
148
- break;
149
-
150
- case "pentagon":
151
- case "hexagon":
152
- case "heptagon":
153
- case "octagon": {
154
- const sides = parseInt(shapeName.replace(/\D/g, ""), 10);
155
- drawPolygon(ctx, offsetX, offsetY, offsetW, offsetH, sides);
156
- break;
157
- }
158
-
159
- case "diamond":
160
- ctx.moveTo(offsetX + offsetW / 2, offsetY);
161
- ctx.lineTo(offsetX + offsetW, offsetY + offsetH / 2);
162
- ctx.lineTo(offsetX + offsetW / 2, offsetY + offsetH);
163
- ctx.lineTo(offsetX, offsetY + offsetH / 2);
164
- ctx.closePath();
165
- break;
166
-
167
- case "trapezoid": {
168
- const topWidth = offsetW * 0.6;
169
- const offsetVal = (offsetW - topWidth) / 2;
170
- ctx.moveTo(offsetX + offsetVal, offsetY);
171
- ctx.lineTo(offsetX + offsetVal + topWidth, offsetY);
172
- ctx.lineTo(offsetX + offsetW, offsetY + offsetH);
173
- ctx.lineTo(offsetX, offsetY + offsetH);
174
- ctx.closePath();
175
- break;
176
- }
177
-
178
- case "heart":
179
- drawHeart(ctx, offsetX, offsetY, offsetW, offsetH);
180
- break;
181
-
182
- case "kite":
183
- ctx.moveTo(offsetX + offsetW / 2, offsetY);
184
- ctx.lineTo(offsetX + offsetW, offsetY + offsetH / 2);
185
- ctx.lineTo(offsetX + offsetW / 2, offsetY + offsetH);
186
- ctx.lineTo(offsetX, offsetY + offsetH / 2);
187
- ctx.closePath();
188
- break;
189
- }
190
- } else {
191
- ctx.globalCompositeOperation = "source-atop";
192
- objectRadius(ctx, offsetX, offsetY, offsetW, offsetH, stroke.borderRadius || 2, stroke.borderPosition);
193
- }
194
65
 
195
- ctx.stroke();
196
- ctx.filter = "none";
197
- ctx.restore();
66
+ function rotatePoint(
67
+ x: number, y: number, px: number, py: number, deg = 0
68
+ ): [number, number] {
69
+ if (!deg) return [x, y];
70
+ const a = (deg * Math.PI) / 180;
71
+ const dx = x - px, dy = y - py;
72
+ return [px + dx * Math.cos(a) - dy * Math.sin(a),
73
+ py + dx * Math.sin(a) + dy * Math.cos(a)];
198
74
  }
199
75
 
76
+ /**
77
+ * Build a gradient in **rect-local coordinates**:
78
+ * - Defaults for coords use rect {w,h}
79
+ * - Rotation pivot defaults to rect center
80
+ * - Offsets are applied by adding rect.x/rect.y to all points
81
+ */
82
+ export function createGradientFill(
83
+ ctx: SKRSContext2D,
84
+ g: gradient,
85
+ rect: { x: number; y: number; w: number; h: number }
86
+ ): CanvasGradient {
87
+ const { x, y, w, h } = rect;
88
+
89
+ if (g.type === "linear") {
90
+ const {
91
+ startX = 0, startY = 0,
92
+ endX = w, endY = 0,
93
+ rotate = 0,
94
+ pivotX = w / 2, pivotY = h / 2,
95
+ colors
96
+ } = g;
97
+
98
+ const [sx, sy] = rotatePoint(startX, startY, pivotX, pivotY, rotate);
99
+ const [ex, ey] = rotatePoint(endX, endY, pivotX, pivotY, rotate);
100
+
101
+ const grad = ctx.createLinearGradient(x + sx, y + sy, x + ex, y + ey);
102
+ colors.forEach(cs => grad.addColorStop(cs.stop, cs.color));
103
+ return grad;
104
+ }
200
105
 
201
-
202
- export function drawShape(ctx: SKRSContext2D, shapeSettings: any): void {
106
+ // radial
203
107
  const {
204
- source,
205
- x,
206
- y,
207
- width,
208
- height,
209
- opacity,
210
- rotation = 0,
211
- borderRadius = 0,
212
- borderPosition = 'all',
213
- stroke,
214
- shadow,
215
- isFilled = false,
216
- color = "transparent",
217
- gradient,
218
- filling,
219
- } = shapeSettings;
220
-
221
- const shapeName = source.toLowerCase();
222
- ctx.save();
223
-
224
- applyRotation(ctx, rotation, x, y, width, height);
225
- applyShadow(ctx, shadow, x, y, width, height);
108
+ startX = w / 2, startY = h / 2, startRadius = 0,
109
+ endX = w / 2, endY = h / 2, endRadius = Math.max(w, h) / 2,
110
+ rotate = 0,
111
+ pivotX = w / 2, pivotY = h / 2,
112
+ colors
113
+ } = g;
114
+
115
+ const [sx, sy] = rotatePoint(startX, startY, pivotX, pivotY, rotate);
116
+ const [ex, ey] = rotatePoint(endX, endY, pivotX, pivotY, rotate);
117
+
118
+ const grad = ctx.createRadialGradient(
119
+ x + sx, y + sy, startRadius,
120
+ x + ex, y + ey, endRadius
121
+ );
122
+ colors.forEach(cs => grad.addColorStop(cs.stop, cs.color));
123
+ return grad;
124
+ }
226
125
 
227
- ctx.beginPath();
126
+ // utils/imageMath.ts
127
+ import type { AlignMode, FitMode } from "../types";
228
128
 
229
- ctx.globalAlpha = opacity as number;
230
-
231
- switch (shapeName) {
232
- case 'circle': {
233
- ctx.arc(x + width / 2, y + height / 2, width / 2, 0, Math.PI * 2);
234
- break;
235
- }
236
- case 'square': {
237
- ctx.rect(x, y, width, height);
238
- break;
239
- }
240
- case 'triangle': {
241
- ctx.moveTo(x + width / 2, y);
242
- ctx.lineTo(x, y + height);
243
- ctx.lineTo(x + width, y + height);
244
- ctx.closePath();
245
- break;
246
- }
247
- case 'pentagon': case 'hexagon': case 'heptagon': case 'octagon': {
248
- const sides = parseInt(shapeName.replace(/\D/g, ""), 10);
249
- drawPolygon(ctx, x, y, width, height, sides);
250
- break;
251
- }
252
- case 'star': {
253
- drawStar(ctx, x, y, width, height);
254
- break;
255
- }
256
- case 'kite': {
257
- ctx.moveTo(x + width / 2, y);
258
- ctx.lineTo(x + width, y + height / 2);
259
- ctx.lineTo(x + width / 2, y + height);
260
- ctx.lineTo(x, y + height / 2);
261
- ctx.closePath();
262
- break;
263
- }
264
- case 'oval': {
265
- ctx.ellipse(x + width / 2, y + height / 2, width / 2, height / 2, 0, 0, Math.PI * 2);
266
- break;
267
- }
268
- case 'arrow': {
269
- drawArrow(ctx, x, y, width, height);
270
- break;
271
- }
272
- case 'heart': {
273
- drawHeart(ctx, x, y, width, height);
274
- break;
275
- }
276
- case 'diamond': {
277
- ctx.moveTo(x + width / 2, y);
278
- ctx.lineTo(x + width, y + height / 2);
279
- ctx.lineTo(x + width / 2, y + height);
280
- ctx.lineTo(x, y + height / 2);
281
- ctx.closePath();
282
- break;
283
- }
284
- case 'trapezoid': {
285
- const topWidth = width * 0.6;
286
- const offset = (width - topWidth) / 2;
287
- ctx.moveTo(x + offset, y);
288
- ctx.lineTo(x + offset + topWidth, y);
289
- ctx.lineTo(x + width, y + height);
290
- ctx.lineTo(x, y + height);
291
- ctx.closePath();
292
- break;
293
- }
294
- default: {
295
- ctx.restore();
296
- throw new Error(`Unsupported shape: ${shapeName}`);
297
- }
298
- }
129
+ export function fitInto(
130
+ boxX: number, boxY: number, boxW: number, boxH: number,
131
+ imgW: number, imgH: number,
132
+ fit: FitMode = "fill",
133
+ align: AlignMode = "center"
134
+ ) {
135
+ let dx = boxX, dy = boxY, dw = boxW, dh = boxH, sx = 0, sy = 0, sw = imgW, sh = imgH;
299
136
 
300
- if (isFilled) {
301
- if (borderRadius && shapeName !== 'circle' && shapeName !== 'oval') {
302
- objectRadius(ctx, x, y, width, height, borderRadius, borderPosition);
303
- }
304
-
305
- let fillStyle: string | CanvasGradient = color;
306
-
307
- if (gradient) {
308
- fillStyle = createGradient(ctx, gradient, x, y, x + width, y + height);
309
- }
310
-
311
- if (filling && filling.percentage <= 100) {
312
- ctx.save();
313
-
314
- const isCustomShape = ["heart", "arrow", "star", "pentagon", "hexagon", "heptagon", "octagon", "diamond", "trapezoid", "kite", "oval", "circle"].includes(shapeName);
315
-
316
- if (isCustomShape) {
317
- ctx.clip();
318
- }
319
-
320
- let fillX = x;
321
- let fillY = y;
322
- let fillWidth = width;
323
- let fillHeight = height;
324
-
325
- switch (filling.rotation) {
326
- case 0:
327
- fillHeight = (filling.percentage / 100) * height;
328
- fillY = y + height - fillHeight;
329
- break;
330
-
331
- case 180:
332
- fillHeight = (filling.percentage / 100) * height;
333
- break;
334
-
335
- case 90:
336
- fillWidth = (filling.percentage / 100) * width;
337
- break;
338
-
339
- case 270:
340
- fillWidth = (filling.percentage / 100) * width;
341
- fillX = x + width - fillWidth;
342
- break;
343
-
344
- default:
345
- console.warn(`Unsupported filling rotation: ${filling.rotation}, defaulting to 0 (Bottom to Top).`);
346
- fillHeight = (filling.percentage / 100) * height;
347
- fillY = y + height - fillHeight;
348
- }
349
-
350
- ctx.beginPath();
351
- ctx.rect(fillX, fillY, fillWidth, fillHeight);
352
- ctx.fillStyle = fillStyle;
353
- ctx.fill();
354
-
355
- ctx.restore();
356
- } else {
357
- ctx.fillStyle = fillStyle;
358
- ctx.fill();
359
- }
137
+ if (fit === "fill") {
138
+ return { dx, dy, dw, dh, sx, sy, sw, sh };
360
139
  }
361
140
 
362
- ctx.globalAlpha = 1.0;
363
-
364
- if (stroke) {
365
- if (stroke?.opacity) ctx.globalAlpha = stroke.opacity as number;
366
- applyStroke(ctx, stroke, x, y, width, height, shapeName);
367
- ctx.globalAlpha = 1.0;
141
+ const s = fit === "contain"
142
+ ? Math.min(boxW / imgW, boxH / imgH)
143
+ : Math.max(boxW / imgW, boxH / imgH);
144
+
145
+ dw = imgW * s;
146
+ dh = imgH * s;
147
+
148
+ const cx = boxX + (boxW - dw) / 2;
149
+ const cy = boxY + (boxH - dh) / 2;
150
+
151
+ switch (align) {
152
+ case "top-left": dx = boxX; dy = boxY; break;
153
+ case "top": dx = cx; dy = boxY; break;
154
+ case "top-right": dx = boxX + boxW - dw; dy = boxY; break;
155
+ case "left": dx = boxX; dy = cy; break;
156
+ case "center": dx = cx; dy = cy; break;
157
+ case "right": dx = boxX + boxW - dw; dy = cy; break;
158
+ case "bottom-left": dx = boxX; dy = boxY + boxH - dh; break;
159
+ case "bottom": dx = cx; dy = boxY + boxH - dh; break;
160
+ case "bottom-right": dx = boxX + boxW - dw; dy = boxY + boxH - dh; break;
161
+ default: dx = cx; dy = cy; break;
368
162
  }
369
163
 
370
- ctx.restore();
164
+ return { dx, dy, dw, dh, sx, sy, sw, sh };
371
165
  }
372
166
 
373
- function drawHeart(ctx: SKRSContext2D, x: number, y: number, width: number, height: number): void {
374
- ctx.beginPath();
375
-
376
- ctx.moveTo(x + width / 2, y + height * 0.9);
377
-
378
- ctx.bezierCurveTo(
379
- x + (width * 35) / 100, y + (height * 60) / 100,
380
- x + (width * 10) / 100, y + (height * 55) / 100,
381
- x + (width * 10) / 100, y + (height * 33.33) / 100
382
- );
383
167
 
384
- ctx.bezierCurveTo(
385
- x + (width * 10) / 100, y + (height * 10) / 100,
386
- x + (width * 50) / 100, y + (height * 5) / 100,
387
- x + (width * 50) / 100, y + (height * 33.33) / 100
388
- );
389
-
390
- ctx.bezierCurveTo(
391
- x + (width * 50) / 100, y + (height * 5) / 100,
392
- x + (width * 90) / 100, y + (height * 10) / 100,
393
- x + (width * 90) / 100, y + (height * 33.33) / 100
394
- );
168
+ // utils/imageCache.ts
169
+ import { loadImage, type Image } from "@napi-rs/canvas";
170
+ import path from "path";
395
171
 
396
- ctx.bezierCurveTo(
397
- x + (width * 90) / 100, y + (height * 55) / 100,
398
- x + (width * 65) / 100, y + (height * 60) / 100,
399
- x + width / 2, y + height * 0.9
400
- );
172
+ const cache = new Map<string, Promise<Image>>();
401
173
 
402
- ctx.closePath();
174
+ export function loadImageCached(src: string | Buffer): Promise<Image> {
175
+ if (Buffer.isBuffer(src)) return loadImage(src);
176
+ const key = src.startsWith("http") ? src : path.resolve(process.cwd(), src);
177
+ if (!cache.has(key)) cache.set(key, loadImage(key));
178
+ return cache.get(key)!;
403
179
  }
404
180
 
405
181
 
406
- /** Draws a polygon with a given number of sides */
407
- function drawPolygon(ctx: SKRSContext2D, x: number, y: number, width: number, height: number, sides: number): void {
408
- const cx = x + width / 2;
409
- const cy = y + height / 2;
410
- const radius = Math.min(width, height) / 2;
411
- ctx.moveTo(cx + radius, cy);
412
- for (let i = 1; i <= sides; i++) {
413
- const angle = (Math.PI * 2 * i) / sides;
414
- ctx.lineTo(cx + radius * Math.cos(angle), cy + radius * Math.sin(angle));
415
- }
416
- ctx.closePath();
417
- }
182
+ // utils/drawPasses.ts
418
183
 
419
- function drawArrow(ctx: SKRSContext2D, x: number, y: number, width: number, height: number) {
420
- const shaftWidth = width * 0.25;
421
- const headWidth = width * 0.5;
422
- const headHeight = height * 0.6;
184
+ import type { BoxBackground, ShadowOptions, StrokeOptions, gradient } from "../types";
423
185
 
424
- ctx.beginPath();
186
+ /** Shadow pass (independent) — supports solid color or gradient fill */
187
+ // Shared rect type
188
+ type Rect = { x: number; y: number; w: number; h: number };
425
189
 
426
- ctx.moveTo(x, y + height / 2 - shaftWidth / 2);
427
- ctx.lineTo(x + width - headWidth, y + height / 2 - shaftWidth / 2);
428
- ctx.lineTo(x + width - headWidth, y);
190
+ /* ---------------------------------------------
191
+ SHADOW overloaded to support both call styles
192
+ --------------------------------------------- */
429
193
 
430
- ctx.lineTo(x + width, y + height / 2);
431
-
432
- ctx.lineTo(x + width - headWidth, y + height);
433
- ctx.lineTo(x + width - headWidth, y + height / 2 + shaftWidth / 2);
194
+ // Overload 1: rect-first (new style)
195
+ export function applyShadow(
196
+ ctx: SKRSContext2D,
197
+ rect: Rect,
198
+ shadow?: ShadowOptions
199
+ ): void;
434
200
 
435
- ctx.lineTo(x, y + height / 2 + shaftWidth / 2);
436
- ctx.closePath();
437
- }
201
+ // Overload 2: positional (legacy createCanvas style)
202
+ export function applyShadow(
203
+ ctx: SKRSContext2D,
204
+ shadow: ShadowOptions | undefined,
205
+ x: number, y: number, width: number, height: number,
206
+ borderRadius?: number | "circular",
207
+ borderPosition?: borderPosition
208
+ ): void;
438
209
 
210
+ // Single implementation handling both
211
+ export function applyShadow(
212
+ ctx: SKRSContext2D,
213
+ a: any,
214
+ b?: any,
215
+ c?: any, d?: any, e?: any, f?: any, g?: any
216
+ ): void {
217
+ let rect: Rect;
218
+ let shadow: ShadowOptions | undefined;
219
+ let radius: number | "circular" | undefined;
220
+ let borderPos: borderPosition | undefined;
221
+
222
+ // Detect which overload we’re in
223
+ if (typeof a === "object" && "x" in a && "w" in a) {
224
+ // (ctx, rect, shadow)
225
+ rect = a as Rect;
226
+ shadow = b as ShadowOptions | undefined;
227
+ radius = shadow?.borderRadius ?? 0;
228
+ borderPos = shadow?.borderPosition ?? "all";
229
+ } else {
230
+ // (ctx, shadow, x, y, w, h, radius?, borderPos?)
231
+ shadow = a as ShadowOptions | undefined;
232
+ rect = { x: b as number, y: c as number, w: d as number, h: e as number };
233
+ radius = (f as number | "circular") ?? shadow?.borderRadius ?? 0;
234
+ borderPos = (g as borderPosition) ?? shadow?.borderPosition ?? "all";
235
+ }
439
236
 
440
- function drawStar(ctx: SKRSContext2D, x: number, y: number, width: number, height: number): void {
441
- const cx = x + width / 2;
442
- const cy = y + height / 2;
443
-
444
- const size = Math.min(width, height);
237
+ if (!shadow) return;
445
238
 
446
- const outerRadius = size / 2;
447
- const innerRadius = outerRadius * 0.5;
448
- const rotationOffset = -Math.PI / 2;
239
+ const {
240
+ color = "rgba(0,0,0,1)",
241
+ gradient,
242
+ opacity = 0.4,
243
+ offsetX = 0,
244
+ offsetY = 0,
245
+ blur = 20
246
+ } = shadow;
449
247
 
450
- ctx.beginPath();
248
+ const r = { x: rect.x + offsetX, y: rect.y + offsetY, w: rect.w, h: rect.h };
249
+
250
+ ctx.save();
251
+ ctx.globalAlpha = opacity;
252
+ if (blur > 0) ctx.filter = `blur(${blur}px)`;
451
253
 
452
- for (let i = 0; i < 5; i++) {
453
- let angle = (i * (Math.PI * 2)) / 5 + rotationOffset;
454
- ctx.lineTo(cx + outerRadius * Math.cos(angle), cy + outerRadius * Math.sin(angle));
254
+ buildPath(ctx, r.x, r.y, r.w, r.h, radius!, borderPos!);
455
255
 
456
- angle += Math.PI / 5;
457
- ctx.lineTo(cx + innerRadius * Math.cos(angle), cy + innerRadius * Math.sin(angle));
256
+ if (gradient) {
257
+ const gfill = createGradientFill(ctx, gradient, r);
258
+ ctx.fillStyle = gfill;
259
+ } else {
260
+ ctx.fillStyle = color;
458
261
  }
262
+ ctx.fill();
459
263
 
460
- ctx.closePath();
264
+ ctx.filter = "none";
265
+ ctx.globalAlpha = 1;
266
+ ctx.restore();
461
267
  }
462
268
 
463
- export function createGradient(
464
- ctx: any,
465
- gradientOptions: any,
466
- startX: number,
467
- startY: number,
468
- endX: number,
469
- endY: number
470
- ) {
471
- if (!gradientOptions || !gradientOptions.type || !gradientOptions.colors) {
472
- throw new Error(
473
- "Invalid gradient options. Provide a valid object with type and colors properties."
474
- );
475
- }
476
269
 
477
- if (!Array.isArray(gradientOptions.colors)) {
478
- throw new Error(
479
- "Invalid gradient options. The colors property should be an array of color stops."
480
- );
481
- }
270
+ /* ---------------------------------------------
271
+ STROKE overloaded to support both call styles
272
+ --------------------------------------------- */
482
273
 
483
- if (gradientOptions.type === "linear") {
484
- if (
485
- typeof startX !== "number" ||
486
- typeof startY !== "number" ||
487
- typeof endX !== "number" ||
488
- typeof endY !== "number"
489
- ) {
490
- throw new Error(
491
- "Invalid gradient options for linear gradient. Numeric values are required for startX, startY, endX, and endY."
492
- );
493
- }
494
-
495
- if (typeof gradientOptions.rotate === "number") {
496
- const centerX = (startX + endX) / 2;
497
- const centerY = (startY + endY) / 2;
498
- const dx = endX - startX;
499
- const dy = endY - startY;
500
- const length = Math.sqrt(dx * dx + dy * dy);
501
- const angleRad = (gradientOptions.rotate * Math.PI) / 180;
502
-
503
- startX = centerX - (length / 2) * Math.cos(angleRad);
504
- startY = centerY - (length / 2) * Math.sin(angleRad);
505
- endX = centerX + (length / 2) * Math.cos(angleRad);
506
- endY = centerY + (length / 2) * Math.sin(angleRad);
507
- }
508
-
509
- const gradient = ctx.createLinearGradient(startX, startY, endX, endY);
510
-
511
- for (const colorStop of gradientOptions.colors) {
512
- if (
513
- typeof colorStop.stop !== "number" ||
514
- typeof colorStop.color !== "string"
515
- ) {
516
- throw new Error(
517
- "Invalid color stop. Each color stop should have a numeric stop value and a color string."
518
- );
519
- }
520
- gradient.addColorStop(colorStop.stop, colorStop.color);
521
- }
522
-
523
- return gradient;
524
- } else if (gradientOptions.type === "radial") {
525
- if (
526
- typeof gradientOptions.startX !== "number" ||
527
- typeof gradientOptions.startY !== "number" ||
528
- typeof gradientOptions.startRadius !== "number" ||
529
- typeof gradientOptions.endX !== "number" ||
530
- typeof gradientOptions.endY !== "number" ||
531
- typeof gradientOptions.endRadius !== "number"
532
- ) {
533
- throw new Error(
534
- "Invalid gradient options for radial gradient. Numeric values are required for startX, startY, startRadius, endX, endY, and endRadius."
535
- );
536
- }
537
-
538
- const gradient = ctx.createRadialGradient(
539
- gradientOptions.startX,
540
- gradientOptions.startY,
541
- gradientOptions.startRadius,
542
- gradientOptions.endX,
543
- gradientOptions.endY,
544
- gradientOptions.endRadius
545
- );
546
-
547
- for (const colorStop of gradientOptions.colors) {
548
- if (
549
- typeof colorStop.stop !== "number" ||
550
- typeof colorStop.color !== "string"
551
- ) {
552
- throw new Error(
553
- "Invalid color stop. Each color stop should have a numeric stop value and a color string."
554
- );
555
- }
556
- gradient.addColorStop(colorStop.stop, colorStop.color);
557
- }
558
-
559
- return gradient;
560
- } else {
561
- throw new Error('Unsupported gradient type. Use "linear" or "radial".');
562
- }
563
- }
274
+ // Overload 1: rect-first (new style)
275
+ export function applyStroke(
276
+ ctx: SKRSContext2D,
277
+ rect: Rect,
278
+ stroke?: StrokeOptions
279
+ ): void;
564
280
 
281
+ // Overload 2: positional (legacy createCanvas style)
282
+ export function applyStroke(
283
+ ctx: SKRSContext2D,
284
+ stroke: StrokeOptions | undefined,
285
+ x: number, y: number, width: number, height: number,
286
+ borderRadius?: number | "circular",
287
+ borderPosition?: borderPosition
288
+ ): void;
565
289
 
566
- /**
567
- * Applies rotation to the canvas context.
568
- * @param ctx The canvas rendering context.
569
- * @param rotation The rotation angle in degrees.
570
- * @param x The x-coordinate of the center of rotation.
571
- * @param y The y-coordinate of the center of rotation.
572
- * @param width The width of the shape.
573
- * @param height The height of the shape.
574
- */
575
- export function applyRotation(
576
- ctx: SKRSContext2D,
577
- rotation: number,
578
- x: number,
579
- y: number,
580
- width: number,
581
- height: number
582
- ): void {
583
- const rotationX = x + width / 2;
584
- const rotationY = y + height / 2;
585
- ctx.translate(rotationX, rotationY);
586
- ctx.rotate((rotation * Math.PI) / 180);
587
- ctx.translate(-rotationX, -rotationY);
588
- }
589
-
590
- /**
591
- * Applies border radius to the canvas context with selective corner support.
592
- *
593
- * @param ctx The canvas rendering context.
594
- * @param image The image to be drawn (will be drawn after clipping).
595
- * @param x The x-coordinate of the shape.
596
- * @param y The y-coordinate of the shape.
597
- * @param width The width of the shape.
598
- * @param height The height of the shape.
599
- * @param borderRadius The border radius value (number or "circular").
600
- * @param borderPosition The sides or corners to round.
601
- * Valid values include:
602
- * - "all"
603
- * - "top", "bottom", "left", "right"
604
- * - "top-left", "top-right", "bottom-left", "bottom-right"
605
- * - Or a comma‑separated list (e.g., "top, left, bottom")
606
- */
607
- export function imageRadius(
608
- ctx: SKRSContext2D,
609
- image: any,
610
- x: number,
611
- y: number,
612
- width: number,
613
- height: number,
614
- borderRadius: number | "circular",
615
- borderPosition: string = "all"
616
- ): void {
617
- ctx.save();
618
- ctx.beginPath();
619
-
620
- if (borderRadius === "circular") {
621
- const circleRadius = Math.min(width, height) / 2;
622
- ctx.arc(x + width / 2, y + height / 2, circleRadius, 0, 2 * Math.PI);
623
- ctx.clip();
624
- } else if (typeof borderRadius === "number" && borderRadius > 0) {
625
- const br: number = Math.min(borderRadius, width / 2, height / 2);
626
- const selectedPositions = new Set(borderPosition.toLowerCase().split(",").map((s) => s.trim()));
627
-
628
- const roundTopLeft = selectedPositions.has("all") || selectedPositions.has("top-left") || (selectedPositions.has("top") && selectedPositions.has("left"));
629
- const roundTopRight = selectedPositions.has("all") || selectedPositions.has("top-right") || (selectedPositions.has("top") && selectedPositions.has("right"));
630
- const roundBottomRight = selectedPositions.has("all") || selectedPositions.has("bottom-right") || (selectedPositions.has("bottom") && selectedPositions.has("right"));
631
- const roundBottomLeft = selectedPositions.has("all") || selectedPositions.has("bottom-left") || (selectedPositions.has("bottom") && selectedPositions.has("left"));
632
-
633
- const tl = roundTopLeft ? br : 0;
634
- const tr = roundTopRight ? br : 0;
635
- const brR = roundBottomRight ? br : 0;
636
- const bl = roundBottomLeft ? br : 0;
637
-
638
- ctx.moveTo(x + tl, y);
639
- ctx.lineTo(x + width - tr, y);
640
- if (tr > 0) ctx.arc(x + width - tr, y + tr, tr, -Math.PI / 2, 0, false);
641
- ctx.lineTo(x + width, y + height - brR);
642
- if (brR > 0) ctx.arc(x + width - brR, y + height - brR, brR, 0, Math.PI / 2, false);
643
- ctx.lineTo(x + bl, y + height);
644
- if (bl > 0) ctx.arc(x + bl, y + height - bl, bl, Math.PI / 2, Math.PI, false);
645
- ctx.lineTo(x, y + tl);
646
- if (tl > 0) ctx.arc(x + tl, y + tl, tl, Math.PI, -Math.PI / 2, false);
647
-
648
- ctx.closePath();
649
- ctx.clip();
650
- } else {
651
- ctx.rect(x, y, width, height);
652
- ctx.clip();
653
- }
654
-
655
- ctx.drawImage(image, x, y, width, height);
656
-
657
- ctx.restore();
658
- }
659
-
660
-
661
-
662
- /**
663
- * Creates a rounded rectangle (or circular) path on the canvas context.
664
- *
665
- * @param ctx The canvas rendering context.
666
- * @param x The x-coordinate of the rectangle.
667
- * @param y The y-coordinate of the rectangle.
668
- * @param width The width of the rectangle.
669
- * @param height The height of the rectangle.
670
- * @param borderRadius The radius for rounding. Use a number (or string "circular" for a circle).
671
- * @param borderPosition Which sides/corners to round. Valid values include:
672
- * - "all" (default)
673
- * - "top", "bottom", "left", "right"
674
- * - "top-left", "top-right", "bottom-left", "bottom-right"
675
- * - Or a comma-separated list, e.g. "top-left, bottom-right" or "top, left, bottom"
676
- */
677
- export function objectRadius(
290
+ // Single implementation handling both
291
+ export function applyStroke(
678
292
  ctx: SKRSContext2D,
679
- x: number,
680
- y: number,
681
- width: number,
682
- height: number,
683
- borderRadius: number | "circular" = 0.1,
684
- borderPosition: string = "all"
293
+ a: any,
294
+ b?: any,
295
+ c?: any, d?: any, e?: any, f?: any, g?: any
685
296
  ): void {
686
- ctx.beginPath();
687
-
688
- if (borderRadius === "circular") {
689
- // Draw a circular shape
690
- const circleRadius = Math.min(width, height) / 2;
691
- ctx.arc(x + width / 2, y + height / 2, circleRadius, 0, 2 * Math.PI);
692
- } else if (borderRadius > 0) {
693
- const br: number = Math.min(borderRadius, width / 2, height / 2);
694
- const selectedPositions = new Set(
695
- borderPosition.toLowerCase().split(",").map((s) => s.trim())
696
- );
697
-
698
- // **Correct Grouping**
699
- const roundTopLeft = selectedPositions.has("all") ||
700
- selectedPositions.has("top-left") ||
701
- selectedPositions.has("top") ||
702
- selectedPositions.has("left");
703
-
704
- const roundTopRight = selectedPositions.has("all") ||
705
- selectedPositions.has("top-right") ||
706
- selectedPositions.has("top") ||
707
- selectedPositions.has("right");
708
-
709
- const roundBottomRight = selectedPositions.has("all") ||
710
- selectedPositions.has("bottom-right") ||
711
- selectedPositions.has("bottom") ||
712
- selectedPositions.has("right");
713
-
714
- const roundBottomLeft = selectedPositions.has("all") ||
715
- selectedPositions.has("bottom-left") ||
716
- selectedPositions.has("bottom") ||
717
- selectedPositions.has("left");
718
-
719
- // **Assign correct radii**
720
- const tl = roundTopLeft ? br : 0;
721
- const tr = roundTopRight ? br : 0;
722
- const brR = roundBottomRight ? br : 0;
723
- const bl = roundBottomLeft ? br : 0;
724
-
725
- // **Draw rounded rectangle**
726
- ctx.moveTo(x + tl, y);
727
- ctx.lineTo(x + width - tr, y);
728
- if (tr > 0) ctx.arc(x + width - tr, y + tr, tr, -Math.PI / 2, 0, false);
729
- ctx.lineTo(x + width, y + height - brR);
730
- if (brR > 0) ctx.arc(x + width - brR, y + height - brR, brR, 0, Math.PI / 2, false);
731
- ctx.lineTo(x + bl, y + height);
732
- if (bl > 0) ctx.arc(x + bl, y + height - bl, bl, Math.PI / 2, Math.PI, false);
733
- ctx.lineTo(x, y + tl);
734
- if (tl > 0) ctx.arc(x + tl, y + tl, tl, Math.PI, -Math.PI / 2, false);
297
+ let rect: Rect;
298
+ let stroke: StrokeOptions | undefined;
299
+ let radius: number | "circular" | undefined;
300
+ let borderPos: borderPosition | undefined;
301
+
302
+ if (typeof a === "object" && "x" in a && "w" in a) {
303
+ // (ctx, rect, stroke)
304
+ rect = a as Rect;
305
+ stroke = b as StrokeOptions | undefined;
306
+ radius = stroke?.borderRadius ?? 0;
307
+ borderPos = stroke?.borderPosition ?? "all";
735
308
  } else {
736
- // Default: Draw normal rectangle
737
- ctx.rect(x, y, width, height);
309
+ // (ctx, stroke, x, y, w, h, radius?, borderPos?)
310
+ stroke = a as StrokeOptions | undefined;
311
+ rect = { x: b as number, y: c as number, w: d as number, h: e as number };
312
+ radius = (f as number | "circular") ?? stroke?.borderRadius ?? 0;
313
+ borderPos = (g as borderPosition) ?? stroke?.borderPosition ?? "all";
738
314
  }
739
315
 
740
- ctx.closePath();
741
- }
742
-
743
-
316
+ if (!stroke) return;
744
317
 
745
- /**
746
- * Performs bilinear interpolation on four corners.
747
- * @param corners The four corners (topLeft, topRight, bottomRight, bottomLeft).
748
- * @param t Horizontal interpolation factor (0 to 1).
749
- * @param u Vertical interpolation factor (0 to 1).
750
- * @returns The interpolated point.
751
- */
752
- function bilinearInterpolate(corners: [{ x: number, y: number }, { x: number, y: number }, { x: number, y: number }, { x: number, y: number }], t: number, u: number): { x: number, y: number } {
753
- const top: { x: number, y: number } = {
754
- x: corners[0].x * (1 - t) + corners[1].x * t,
755
- y: corners[0].y * (1 - t) + corners[1].y * t,
756
- };
757
- const bottom: { x: number, y: number } = {
758
- x: corners[3].x * (1 - t) + corners[2].x * t,
759
- y: corners[3].y * (1 - t) + corners[2].y * t,
760
- };
761
- return {
762
- x: top.x * (1 - u) + bottom.x * u,
763
- y: top.y * (1 - u) + bottom.y * u,
318
+ const {
319
+ color = "#000",
320
+ gradient,
321
+ width = 2,
322
+ position = 0,
323
+ blur = 0,
324
+ opacity = 1
325
+ } = stroke;
326
+
327
+ // expand/shrink by `position`
328
+ const r = {
329
+ x: rect.x - position,
330
+ y: rect.y - position,
331
+ w: rect.w + position * 2,
332
+ h: rect.h + position * 2
764
333
  };
334
+
335
+ ctx.save();
336
+ if (blur > 0) ctx.filter = `blur(${blur}px)`;
337
+ ctx.globalAlpha = opacity;
338
+
339
+ buildPath(ctx, r.x, r.y, r.w, r.h, radius!, borderPos!);
340
+
341
+ ctx.lineWidth = width;
342
+
343
+ if (gradient) {
344
+ const gstroke = createGradientFill(ctx, gradient, r);
345
+ ctx.strokeStyle = gstroke as any;
346
+ } else {
347
+ ctx.strokeStyle = color;
348
+ }
349
+ ctx.stroke();
350
+
351
+ ctx.filter = "none";
352
+ ctx.globalAlpha = 1;
353
+ ctx.restore();
765
354
  }
766
355
 
767
- /**
768
- * Applies a perspective warp to the given image by subdividing the source rectangle
769
- * and drawing each cell with an affine transform approximating the local perspective.
770
- *
771
- * @param ctx The canvas rendering context.
772
- * @param image The source image.
773
- * @param x The x-coordinate where the image is drawn.
774
- * @param y The y-coordinate where the image is drawn.
775
- * @param width The width of the region to draw.
776
- * @param height The height of the region to draw.
777
- * @param perspective An object containing four destination corners:
778
- * { topLeft, topRight, bottomRight, bottomLeft }.
779
- * @param gridCols Number of columns to subdivide (default: 10).
780
- * @param gridRows Number of rows to subdivide (default: 10).
781
- */
782
- export async function applyPerspective(
356
+ /** Optional “box background” under the bitmap, inside the image clip */
357
+ export function drawBoxBackground(
783
358
  ctx: SKRSContext2D,
784
- image: Image,
785
- x: number,
786
- y: number,
787
- width: number,
788
- height: number,
789
- perspective: { topLeft: { x: number, y: number }; topRight: { x: number, y: number }; bottomRight: { x: number, y: number }; bottomLeft: { x: number, y: number }; },
790
- gridCols: number = 10,
791
- gridRows: number = 10
792
- ): Promise<void> {
793
- const cellWidth = width / gridCols;
794
- const cellHeight = height / gridRows;
795
-
796
- for (let row = 0; row < gridRows; row++) {
797
- for (let col = 0; col < gridCols; col++) {
798
- const sx = x + col * cellWidth;
799
- const sy = y + row * cellHeight;
800
-
801
- const t0 = col / gridCols;
802
- const t1 = (col + 1) / gridCols;
803
- const u0 = row / gridRows;
804
- const u1 = (row + 1) / gridRows;
805
-
806
- const destTL = bilinearInterpolate(
807
- [perspective.topLeft, perspective.topRight, perspective.bottomRight, perspective.bottomLeft],
808
- t0,
809
- u0
810
- );
811
- const destTR = bilinearInterpolate(
812
- [perspective.topLeft, perspective.topRight, perspective.bottomRight, perspective.bottomLeft],
813
- t1,
814
- u0
815
- );
816
- const destBL = bilinearInterpolate(
817
- [perspective.topLeft, perspective.topRight, perspective.bottomRight, perspective.bottomLeft],
818
- t0,
819
- u1
820
- );
821
-
822
- const a = (destTR.x - destTL.x) / cellWidth;
823
- const b = (destTR.y - destTL.y) / cellWidth;
824
- const c = (destBL.x - destTL.x) / cellHeight;
825
- const d = (destBL.y - destTL.y) / cellHeight;
826
- const e = destTL.x;
827
- const f = destTL.y;
828
-
829
- ctx.save();
830
- ctx.setTransform(a, b, c, d, e, f);
831
- ctx.drawImage(image, sx, sy, cellWidth, cellHeight, 0, 0, cellWidth, cellHeight);
832
- ctx.restore();
833
- }
359
+ rect: { x: number; y: number; w: number; h: number },
360
+ boxBg?: BoxBackground,
361
+ borderRadius?: number | "circular",
362
+ borderPosition?: string
363
+ ) {
364
+ if (!boxBg) return;
365
+ const { color, gradient } = boxBg;
366
+
367
+ // clip to the box radius, then fill
368
+ ctx.save();
369
+ buildPath(ctx, rect.x, rect.y, rect.w, rect.h, borderRadius ?? 0, borderPosition ?? "all");
370
+ ctx.clip();
371
+
372
+ if (gradient) {
373
+ const g = createGradientFill(ctx, gradient, rect);
374
+ ctx.fillStyle = g as any;
375
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
376
+ } else if (color && color !== "transparent") {
377
+ ctx.fillStyle = color;
378
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
834
379
  }
835
- }
380
+
381
+ ctx.restore();
382
+ }