apexify.js 4.5.56 → 4.5.57

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 (42) hide show
  1. package/dist/cjs/canvas/ApexPainter.d.ts +7 -1
  2. package/dist/cjs/canvas/ApexPainter.d.ts.map +1 -1
  3. package/dist/cjs/canvas/ApexPainter.js +56 -52
  4. package/dist/cjs/canvas/ApexPainter.js.map +1 -1
  5. package/dist/cjs/canvas/utils/Background/radius.d.ts.map +1 -1
  6. package/dist/cjs/canvas/utils/Background/radius.js +0 -11
  7. package/dist/cjs/canvas/utils/Background/radius.js.map +1 -1
  8. package/dist/cjs/canvas/utils/Image/imageProperties.d.ts +50 -10
  9. package/dist/cjs/canvas/utils/Image/imageProperties.d.ts.map +1 -1
  10. package/dist/cjs/canvas/utils/Image/imageProperties.js +256 -153
  11. package/dist/cjs/canvas/utils/Image/imageProperties.js.map +1 -1
  12. package/dist/cjs/canvas/utils/types.d.ts +33 -2
  13. package/dist/cjs/canvas/utils/types.d.ts.map +1 -1
  14. package/dist/cjs/canvas/utils/types.js.map +1 -1
  15. package/dist/cjs/canvas/utils/utils.d.ts +2 -2
  16. package/dist/cjs/canvas/utils/utils.d.ts.map +1 -1
  17. package/dist/cjs/canvas/utils/utils.js +2 -1
  18. package/dist/cjs/canvas/utils/utils.js.map +1 -1
  19. package/dist/esm/canvas/ApexPainter.d.ts +7 -1
  20. package/dist/esm/canvas/ApexPainter.d.ts.map +1 -1
  21. package/dist/esm/canvas/ApexPainter.js +56 -52
  22. package/dist/esm/canvas/ApexPainter.js.map +1 -1
  23. package/dist/esm/canvas/utils/Background/radius.d.ts.map +1 -1
  24. package/dist/esm/canvas/utils/Background/radius.js +0 -11
  25. package/dist/esm/canvas/utils/Background/radius.js.map +1 -1
  26. package/dist/esm/canvas/utils/Image/imageProperties.d.ts +50 -10
  27. package/dist/esm/canvas/utils/Image/imageProperties.d.ts.map +1 -1
  28. package/dist/esm/canvas/utils/Image/imageProperties.js +256 -153
  29. package/dist/esm/canvas/utils/Image/imageProperties.js.map +1 -1
  30. package/dist/esm/canvas/utils/types.d.ts +33 -2
  31. package/dist/esm/canvas/utils/types.d.ts.map +1 -1
  32. package/dist/esm/canvas/utils/types.js.map +1 -1
  33. package/dist/esm/canvas/utils/utils.d.ts +2 -2
  34. package/dist/esm/canvas/utils/utils.d.ts.map +1 -1
  35. package/dist/esm/canvas/utils/utils.js +2 -1
  36. package/dist/esm/canvas/utils/utils.js.map +1 -1
  37. package/lib/canvas/ApexPainter.ts +122 -83
  38. package/lib/canvas/utils/Background/radius.ts +0 -11
  39. package/lib/canvas/utils/Image/imageProperties.ts +363 -215
  40. package/lib/canvas/utils/types.ts +18 -2
  41. package/lib/canvas/utils/utils.ts +3 -2
  42. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { SKRSContext2D } from "@napi-rs/canvas";
1
+ import { Image, SKRSContext2D } from "@napi-rs/canvas";
2
2
  import { ImageProperties } from "../utils";
3
3
 
4
4
  /**
@@ -37,7 +37,10 @@ export function applyShadow(
37
37
  shadow.borderPosition
38
38
  );
39
39
 
40
- ctx.fillStyle = shadow.color || "transparent";
40
+ ctx.fillStyle = shadow.gradient
41
+ ? createGradient(ctx, shadow.gradient, x, y, x + width, y + height)
42
+ : shadow.color || "transparent";
43
+
41
44
  ctx.fill();
42
45
  }
43
46
 
@@ -60,7 +63,6 @@ export function applyZoom(
60
63
  const zoomX = zoom.x ?? 0;
61
64
  const zoomY = zoom.y ?? 0;
62
65
 
63
- // Translate to the zoom origin, apply the scaling, then translate back.
64
66
  ctx.translate(zoomX, zoomY);
65
67
  ctx.scale(scale, scale);
66
68
  ctx.translate(-zoomX, -zoomY);
@@ -90,26 +92,21 @@ export function applyZoom(
90
92
 
91
93
  ctx.save();
92
94
 
93
- // Set stroke style: use gradient if provided, otherwise solid color.
94
95
  ctx.strokeStyle = stroke.gradient
95
96
  ? createGradient(ctx, stroke.gradient, x, y, x + width, y + height)
96
97
  : stroke.color || "transparent";
97
98
 
98
- // Set the stroke width.
99
99
  ctx.lineWidth = stroke.width ?? 0;
100
100
 
101
- // Calculate offset: stroke.position plus half the stroke width ensures the stroke is drawn outside.
102
101
  const positionOffset = stroke.position ?? 0;
103
102
  const halfStroke = ctx.lineWidth / 2;
104
103
  const totalOffset = positionOffset + halfStroke;
105
104
 
106
- // Adjust the drawing rectangle to accommodate the stroke.
107
105
  const adjustedX = x - totalOffset;
108
106
  const adjustedY = y - totalOffset;
109
107
  const adjustedWidth = width + totalOffset * 2;
110
108
  const adjustedHeight = height + totalOffset * 2;
111
109
 
112
- // If the border position is "all" (or not provided), draw a complete rounded rectangle.
113
110
  if (!stroke.borderPosition || stroke.borderPosition.trim().toLowerCase() === "all") {
114
111
  objectRadius(
115
112
  ctx,
@@ -125,21 +122,16 @@ export function applyZoom(
125
122
  return;
126
123
  }
127
124
 
128
- // Otherwise, draw only the segments corresponding to the specified sides/corners.
129
125
  const bp = stroke.borderPosition.toLowerCase();
130
126
  const positions = bp.split(',').map(s => s.trim());
131
- // For convenience, if a simple keyword (like "top") is used, it indicates the entire edge.
132
- // We'll allow individual corners as well.
133
127
 
134
128
  ctx.beginPath();
135
129
 
136
- // --- Top Edge ---
137
130
  if (
138
131
  positions.includes("top") ||
139
132
  positions.includes("top-left") ||
140
133
  positions.includes("top-right")
141
134
  ) {
142
- // Determine rounding for the top-left and top-right corners.
143
135
  const tl = positions.includes("top-left") || positions.includes("top")
144
136
  ? (stroke.borderRadius ? +stroke.borderRadius : 0)
145
137
  : 0;
@@ -153,7 +145,6 @@ export function applyZoom(
153
145
  }
154
146
  }
155
147
 
156
- // --- Right Edge ---
157
148
  if (
158
149
  positions.includes("right") ||
159
150
  positions.includes("top-right") ||
@@ -172,7 +163,6 @@ export function applyZoom(
172
163
  }
173
164
  }
174
165
 
175
- // --- Bottom Edge ---
176
166
  if (
177
167
  positions.includes("bottom") ||
178
168
  positions.includes("bottom-left") ||
@@ -191,7 +181,6 @@ export function applyZoom(
191
181
  }
192
182
  }
193
183
 
194
- // --- Left Edge ---
195
184
  if (
196
185
  positions.includes("left") ||
197
186
  positions.includes("top-left") ||
@@ -215,172 +204,206 @@ export function applyZoom(
215
204
  }
216
205
 
217
206
  /**
218
- * Draws a shape on the canvas context.
219
- * @param ctx The canvas rendering context.
220
- * @param shapeSettings The settings for the shape.
207
+ * Draws a shape on the canvas context based on the provided settings.
208
+ * Supports built‑in shapes as well as a custom polygon.
209
+ * @param ctx - The canvas rendering context.
210
+ * @param shapeSettings - The settings for the shape.
221
211
  */
222
- export function drawShape(ctx: any, shapeSettings: any) {
223
- const { source, x, y, width, height, rotation, borderRadius, borderPosition, stroke, shadow, isFilled, color, gradient } = shapeSettings;
212
+ export function drawShape(ctx: SKRSContext2D, shapeSettings: any): void {
213
+ const {
214
+ source,
215
+ x,
216
+ y,
217
+ width,
218
+ height,
219
+ rotation = 0,
220
+ borderRadius = 0,
221
+ borderPosition = 'all',
222
+ stroke,
223
+ shadow,
224
+ isFilled = false,
225
+ color = "transparent",
226
+ gradient,
227
+ } = shapeSettings;
224
228
 
225
229
  const shapeName = source.toLowerCase();
226
230
 
231
+ ctx.save();
232
+
233
+ applyRotation(ctx, rotation, x, y, width, height);
234
+ applyShadow(ctx, shadow, x, y, width, height);
235
+
236
+ ctx.beginPath();
237
+
227
238
  switch (shapeName) {
228
- case 'circle':
229
- ctx.save();
230
- applyShadow(ctx, shadow, x, y, width, height);
231
- applyRotation(ctx, rotation, x, y, width, height);
232
- ctx.beginPath();
233
- ctx.arc(x + width / 2, y + height / 2, width / 2, 0, Math.PI * 2);
234
- break;
235
- case 'square':
236
- ctx.save();
237
- applyRotation(ctx, rotation, x, y, width, height);
238
- applyShadow(ctx, shadow, x, y, width, height);
239
- break;
240
- case 'triangle':
241
- ctx.save();
242
- applyRotation(ctx, rotation, x, y, width, height);
243
- applyShadow(ctx, shadow, x, y, width, height);
244
- ctx.beginPath();
245
- ctx.moveTo(x, y + height);
246
- ctx.lineTo(x + width / 2, y);
247
- ctx.lineTo(x + width, y + height);
248
- ctx.closePath();
249
- break;
250
- case 'pentagon':
251
- ctx.save();
252
- applyRotation(ctx, rotation, x, y, width, height);
253
- applyShadow(ctx, shadow, x, y, width, height);
254
- ctx.beginPath();
255
- for (let i = 0; i < 5; i++) {
256
- ctx.lineTo(x + width / 2 + width / 2 * Math.sin(i * 2 * Math.PI / 5),
257
- y + height / 2 - height / 2 * Math.cos(i * 2 * Math.PI / 5));
258
- }
259
- ctx.closePath();
260
- break;
261
- case 'hexagon':
262
- ctx.save();
263
- applyRotation(ctx, rotation, x, y, width, height);
264
- applyShadow(ctx, shadow, x, y, width, height);
265
- ctx.beginPath();
266
- for (let i = 0; i < 6; i++) {
267
- ctx.lineTo(x + width / 2 + width / 2 * Math.sin(i * 2 * Math.PI / 6),
268
- y + height / 2 - height / 2 * Math.cos(i * 2 * Math.PI / 6));
269
- }
270
- ctx.closePath();
271
- break;
272
- case 'heptagon':
273
- ctx.save();
274
- applyRotation(ctx, rotation, x, y, width, height);
275
- applyShadow(ctx, shadow, x, y, width, height);
276
- ctx.beginPath();
277
- for (let i = 0; i < 7; i++) {
278
- ctx.lineTo(x + width / 2 + width / 2 * Math.sin(i * 2 * Math.PI / 7),
279
- y + height / 2 - height / 2 * Math.cos(i * 2 * Math.PI / 7));
280
- }
281
- ctx.closePath();
282
- break;
283
- case 'octagon':
284
- ctx.save();
285
- applyRotation(ctx, rotation, x, y, width, height);
286
- applyShadow(ctx, shadow, x, y, width, height);
287
- ctx.beginPath();
288
- for (let i = 0; i < 8; i++) {
289
- ctx.lineTo(x + width / 2 + width / 2 * Math.sin(i * 2 * Math.PI / 8),
290
- y + height / 2 - height / 2 * Math.cos(i * 2 * Math.PI / 8));
291
- }
292
- ctx.closePath();
293
- case 'star':
294
- ctx.save();
295
- applyRotation(ctx, rotation, x, y, width, height);
296
- applyShadow(ctx, shadow, x, y, width, height);
297
- ctx.beginPath();
298
- const numPoints = 5;
299
- const outerRadius = Math.min(width, height) / 2;
300
- const innerRadius = outerRadius / 2;
301
- for (let i = 0; i < numPoints * 2; i++) {
302
- const radius = i % 2 === 0 ? outerRadius : innerRadius;
303
- const angle = Math.PI / numPoints * i;
304
- ctx.lineTo(x + width / 2 + radius * Math.sin(angle),
305
- y + height / 2 - radius * Math.cos(angle));
239
+ case 'circle': {
240
+ ctx.arc(x + width / 2, y + height / 2, width / 2, 0, Math.PI * 2);
241
+ break;
242
+ }
243
+ case 'square': {
244
+ ctx.rect(x, y, width, height);
245
+ break;
246
+ }
247
+ case 'triangle': {
248
+ ctx.moveTo(x + width / 2, y);
249
+ ctx.lineTo(x, y + height);
250
+ ctx.lineTo(x + width, y + height);
251
+ ctx.closePath();
252
+ break;
253
+ }
254
+ case 'pentagon': {
255
+ drawPolygon(ctx, x, y, width, height, 5);
256
+ break;
257
+ }
258
+ case 'hexagon': {
259
+ drawPolygon(ctx, x, y, width, height, 6);
260
+ break;
261
+ }
262
+ case 'heptagon': {
263
+ drawPolygon(ctx, x, y, width, height, 7);
264
+ break;
265
+ }
266
+ case 'octagon': {
267
+ drawPolygon(ctx, x, y, width, height, 8);
268
+ break;
269
+ }
270
+ case 'star': {
271
+ {
272
+ const cx = x + width / 2;
273
+ const cy = y + height / 2;
274
+ const outerRadius = Math.min(width, height) / 2;
275
+ const innerRadius = outerRadius / 2;
276
+ const step = Math.PI / 5;
277
+ for (let i = 0; i < 10; i++) {
278
+ const r = i % 2 === 0 ? outerRadius : innerRadius;
279
+ const angle = i * step - Math.PI / 2;
280
+ if (i === 0) {
281
+ ctx.moveTo(cx + r * Math.cos(angle), cy + r * Math.sin(angle));
282
+ } else {
283
+ ctx.lineTo(cx + r * Math.cos(angle), cy + r * Math.sin(angle));
306
284
  }
307
- ctx.closePath();
308
- break;
309
- case 'oval':
310
- ctx.save();
311
- applyRotation(ctx, rotation, x, y, width, height);
312
- applyShadow(ctx, shadow, x, y, width, height);
313
-
314
- ctx.beginPath();
315
- ctx.ellipse(x + width / 2, y + height / 2, width / 2, height / 2, 0, 0, Math.PI * 2);
316
- ctx.closePath();
317
- if (isFilled) {
318
- ctx.fillStyle = color;
319
- ctx.fill();
320
- } else {
321
- applyStroke(ctx, stroke, x, y, width, height);
322
- }
323
- ctx.restore();
324
- break;
325
- default:
326
- throw new Error(`Unsupported shape: ${shapeName}`);
327
- }
328
- if (isFilled) {
329
- if (borderRadius) {
330
- objectRadius(ctx, x, y, width, height, borderRadius, borderPosition);
331
- if (gradient) {
332
- const gradientFill = createGradient(
333
- ctx,
334
- gradient,
335
- x,
336
- y,
337
- x + width,
338
- y + height,
339
- );
340
- ctx.fillStyle = gradientFill;
341
- } else {
342
- ctx.fillStyle = color || "transparent";
343
285
  }
344
- ctx.fill();
345
- } else {
346
- if (gradient) {
347
- const gradientFill = createGradient(
348
- ctx,
349
- gradient,
350
- x,
351
- y,
352
- x + width,
353
- y + height,
354
- );
355
- ctx.fillStyle = gradientFill;
356
- } else {
357
- ctx.fillStyle = color || "transparent";
286
+ ctx.closePath();
287
+ }
288
+ break;
358
289
  }
359
- if (shapeName === 'square') {
360
- ctx.fillRect(x, y, width, height);
361
- } else if (shapeName === 'circle') {
362
- ctx.fill();
363
- } else {
364
- ctx.fill()
365
- }
290
+ case 'kite': {
291
+ ctx.moveTo(x + width / 2, y);
292
+ ctx.lineTo(x + width, y + height / 2);
293
+ ctx.lineTo(x + width / 2, y + height);
294
+ ctx.lineTo(x, y + height / 2);
295
+ ctx.closePath();
296
+ break;
297
+ }
298
+ case 'oval': {
299
+ ctx.ellipse(x + width / 2, y + height / 2, width / 2, height / 2, 0, 0, Math.PI * 2);
300
+ break;
301
+ }
302
+ case 'heart': {
303
+ ctx.moveTo(x + width / 2, y + height * 0.75);
304
+ ctx.bezierCurveTo(
305
+ x + width * 0.1, y + height * 0.5,
306
+ x, y + height * 0.2,
307
+ x + width / 2, y + height * 0.3
308
+ );
309
+ ctx.bezierCurveTo(
310
+ x + width, y + height * 0.2,
311
+ x + width * 0.9, y + height * 0.5,
312
+ x + width / 2, y + height * 0.75
313
+ );
314
+ ctx.closePath();
315
+ break;
316
+ }
317
+ case 'arrow': {
318
+ const arrowHeadWidth = width * 0.3;
319
+ ctx.moveTo(x, y + height / 2);
320
+ ctx.lineTo(x + width - arrowHeadWidth, y + height / 2);
321
+ ctx.lineTo(x + width - arrowHeadWidth, y);
322
+ ctx.lineTo(x + width, y + height / 2);
323
+ ctx.lineTo(x + width - arrowHeadWidth, y + height);
324
+ ctx.lineTo(x + width - arrowHeadWidth, y + height / 2);
325
+ ctx.lineTo(x, y + height / 2);
326
+ ctx.closePath();
327
+ break;
328
+ }
329
+ case 'diamond': {
330
+ ctx.moveTo(x + width / 2, y);
331
+ ctx.lineTo(x + width, y + height / 2);
332
+ ctx.lineTo(x + width / 2, y + height);
333
+ ctx.lineTo(x, y + height / 2);
334
+ ctx.closePath();
335
+ break;
336
+ }
337
+ case 'trapezoid': {
338
+ const topWidth = width * 0.6;
339
+ const offset = (width - topWidth) / 2;
340
+ ctx.moveTo(x + offset, y);
341
+ ctx.lineTo(x + offset + topWidth, y);
342
+ ctx.lineTo(x + width, y + height);
343
+ ctx.lineTo(x, y + height);
344
+ ctx.closePath();
345
+ break;
346
+ }
347
+ case 'cloud': {
348
+ const radius = width / 5;
349
+ ctx.moveTo(x + radius, y + height / 2);
350
+ ctx.arc(x + radius, y + height / 2, radius, Math.PI * 0.5, Math.PI * 1.5);
351
+ ctx.arc(x + width / 2, y + height / 2 - radius, radius, Math.PI, 0);
352
+ ctx.arc(x + width - radius, y + height / 2, radius, Math.PI * 1.5, Math.PI * 0.5);
353
+ ctx.lineTo(x + width, y + height);
354
+ ctx.lineTo(x, y + height);
355
+ ctx.closePath();
356
+ break;
357
+ }
358
+ default: {
359
+ ctx.restore();
360
+ throw new Error(`Unsupported shape: ${shapeName}`);
361
+ }
362
+ }
363
+
364
+ if (isFilled) {
365
+ if (borderRadius && shapeName !== 'circle' && shapeName !== 'oval') {
366
+ objectRadius(ctx, x, y, width, height, borderRadius, borderPosition);
366
367
  }
367
- applyStroke(ctx, stroke, x, y, width, height);
368
- } else {
369
368
  if (gradient) {
370
- const gradientFill = createGradient(
371
- ctx,
372
- gradient,
373
- x,
374
- y,
375
- x + width,
376
- y + height,
377
- );
378
- ctx.fillStyle = gradientFill;
369
+ const gradFill = createGradient(ctx, gradient, x, y, x + width, y + height);
370
+ ctx.fillStyle = gradFill;
371
+ } else {
372
+ ctx.fillStyle = color;
379
373
  }
374
+ ctx.fill();
375
+ }
376
+
377
+ if (stroke) {
380
378
  applyStroke(ctx, stroke, x, y, width, height);
379
+ }
380
+
381
+ ctx.restore();
381
382
  }
382
383
 
383
- ctx.restore();
384
+ /**
385
+ * Helper function to draw a regular polygon.
386
+ * Inscribes a polygon with a given number of sides inside the rectangle defined by (x, y, width, height).
387
+ * @param ctx - The canvas rendering context.
388
+ * @param x - The x-coordinate of the bounding rectangle.
389
+ * @param y - The y-coordinate of the bounding rectangle.
390
+ * @param width - The width of the bounding rectangle.
391
+ * @param height - The height of the bounding rectangle.
392
+ * @param sides - The number of sides (≥ 3).
393
+ */
394
+ function drawPolygon(ctx: SKRSContext2D, x: number, y: number, width: number, height: number, sides: number): void {
395
+ const cx = x + width / 2;
396
+ const cy = y + height / 2;
397
+ const radius = Math.min(width, height) / 2;
398
+ for (let i = 0; i < sides; i++) {
399
+ const angle = (2 * Math.PI * i) / sides - Math.PI / 2;
400
+ if (i === 0) {
401
+ ctx.moveTo(cx + radius * Math.cos(angle), cy + radius * Math.sin(angle));
402
+ } else {
403
+ ctx.lineTo(cx + radius * Math.cos(angle), cy + radius * Math.sin(angle));
404
+ }
405
+ }
406
+ ctx.closePath();
384
407
  }
385
408
 
386
409
  export function createGradient(ctx: any, gradientOptions: any, startX: number, startY: number, endX: number, endY: number) {
@@ -464,7 +487,6 @@ export function applyRotation(
464
487
  width: number,
465
488
  height: number
466
489
  ): void {
467
- // Check is not really necessary since 0 is valid.
468
490
  const rotationX = x + width / 2;
469
491
  const rotationY = y + height / 2;
470
492
  ctx.translate(rotationX, rotationY);
@@ -472,49 +494,95 @@ export function applyRotation(
472
494
  ctx.translate(-rotationX, -rotationY);
473
495
  }
474
496
 
475
-
476
- /**
477
- * Applies border radius to the canvas context.
478
- * @param ctx The canvas rendering context.
479
- * @param image The image properties containing the border radius.
480
- * @param x The x-coordinate of the shape.
481
- * @param y The y-coordinate of the shape.
482
- * @param width The width of the shape.
483
- * @param height The height of the shape.
484
- * @param borderRadius The border radius value.
485
- * @param borderPosition The border radius position to be applied on.
486
- */
487
- export function imageRadius(ctx: any, image: any, x: number, y: number, width: number, height: number, borderRadius: any, borderPosition: string = 'all'): void {
497
+ /**
498
+ * Applies border radius to the canvas context with selective corner support.
499
+ *
500
+ * @param ctx The canvas rendering context.
501
+ * @param image The image to be drawn (will be drawn after clipping).
502
+ * @param x The x-coordinate of the shape.
503
+ * @param y The y-coordinate of the shape.
504
+ * @param width The width of the shape.
505
+ * @param height The height of the shape.
506
+ * @param borderRadius The border radius value (number or "circular").
507
+ * @param borderPosition The sides or corners to round.
508
+ * Valid values include:
509
+ * - "all"
510
+ * - "top", "bottom", "left", "right"
511
+ * - "top-left", "top-right", "bottom-left", "bottom-right"
512
+ * - Or a comma‑separated list (e.g., "top, left, bottom")
513
+ */
514
+ export function imageRadius(
515
+ ctx: SKRSContext2D,
516
+ image: any,
517
+ x: number,
518
+ y: number,
519
+ width: number,
520
+ height: number,
521
+ borderRadius: number | "circular",
522
+ borderPosition: string = "all"
523
+ ): void {
488
524
  ctx.save();
489
525
  ctx.beginPath();
490
-
526
+
491
527
  if (borderRadius === "circular") {
492
528
  const circleRadius = Math.min(width, height) / 2;
493
529
  ctx.arc(x + width / 2, y + height / 2, circleRadius, 0, 2 * Math.PI);
530
+ ctx.closePath();
494
531
  } else {
495
- ctx.moveTo(x + borderRadius, y);
496
- ctx.lineTo(x + width - borderRadius, y);
497
- ctx.quadraticCurveTo(x + width, y, x + width, y + borderRadius);
498
- ctx.lineTo(x + width, y + height - borderRadius);
499
- ctx.quadraticCurveTo(
500
- x + width,
501
- y + height,
502
- x + width - borderRadius,
503
- y + height,
504
- );
505
- ctx.lineTo(x + borderRadius, y + height);
506
- ctx.quadraticCurveTo(x, y + height, x, y + height - borderRadius);
507
- ctx.lineTo(x, y + borderRadius);
508
- ctx.quadraticCurveTo(x, y, x + borderRadius, y);
532
+ const br: number = typeof borderRadius === "number" ? borderRadius : 0;
533
+ const bp = borderPosition.trim().toLowerCase();
534
+
535
+ const selectedCorners = new Set(bp.split(",").map((s) => s.trim()));
536
+
537
+ const roundTopLeft = selectedCorners.has("all") || selectedCorners.has("top-left") || (selectedCorners.has("top") && selectedCorners.has("left"));
538
+ const roundTopRight = selectedCorners.has("all") || selectedCorners.has("top-right") || (selectedCorners.has("top") && selectedCorners.has("right"));
539
+ const roundBottomRight = selectedCorners.has("all") || selectedCorners.has("bottom-right") || (selectedCorners.has("bottom") && selectedCorners.has("right"));
540
+ const roundBottomLeft = selectedCorners.has("all") || selectedCorners.has("bottom-left") || (selectedCorners.has("bottom") && selectedCorners.has("left"));
541
+
542
+ const tl = roundTopLeft ? br : 0;
543
+ const tr = roundTopRight ? br : 0;
544
+ const brR = roundBottomRight ? br : 0;
545
+ const bl = roundBottomLeft ? br : 0;
546
+
547
+ ctx.moveTo(x + tl, y);
548
+
549
+ if (roundTopLeft && roundTopRight) {
550
+ ctx.arcTo(x, y, x + width, y, tl);
551
+ } else if (roundTopLeft) {
552
+ ctx.arcTo(x, y, x + width, y, tl);
553
+ } else {
554
+ ctx.lineTo(x, y);
555
+ ctx.lineTo(x + width, y);
556
+ }
557
+
558
+ if (tr > 0) {
559
+ ctx.arcTo(x + width, y, x + width, y + height, tr);
560
+ }
561
+
562
+ ctx.lineTo(x + width, y + height - brR);
563
+ if (brR > 0) {
564
+ ctx.arcTo(x + width, y + height, x, y + height, brR);
565
+ }
566
+
567
+ ctx.lineTo(x + bl, y + height);
568
+ if (bl > 0) {
569
+ ctx.arcTo(x, y + height, x, y, bl);
570
+ }
571
+
572
+ ctx.lineTo(x, y + tl);
573
+ if (tl > 0) {
574
+ ctx.arcTo(x, y, x + tl, y, tl);
575
+ }
576
+
577
+ ctx.closePath();
509
578
  }
510
-
511
- ctx.closePath();
579
+
512
580
  ctx.clip();
513
581
  ctx.drawImage(image, x, y, width, height);
514
582
  ctx.restore();
515
- }
516
-
517
-
583
+ }
584
+
585
+
518
586
  /**
519
587
  * Creates a rounded rectangle (or circular) path on the canvas context.
520
588
  *
@@ -540,13 +608,11 @@ export function objectRadius(
540
608
  borderPosition: string = 'all'
541
609
  ): void {
542
610
  if (borderRadius === "circular") {
543
- // Draw a circular path based on the smallest dimension.
544
611
  const circleRadius = Math.min(width, height) / 2;
545
612
  ctx.beginPath();
546
613
  ctx.arc(x + width / 2, y + height / 2, circleRadius, 0, 2 * Math.PI);
547
614
  ctx.closePath();
548
615
  } else if (borderRadius) {
549
- // Determine which corners to round.
550
616
  let roundTopLeft = false;
551
617
  let roundTopRight = false;
552
618
  let roundBottomRight = false;
@@ -554,7 +620,6 @@ export function objectRadius(
554
620
 
555
621
  const bp = borderPosition.trim().toLowerCase();
556
622
 
557
- // If a simple keyword is provided, handle those cases:
558
623
  if (bp === 'all') {
559
624
  roundTopLeft = roundTopRight = roundBottomRight = roundBottomLeft = true;
560
625
  } else if (bp === 'top') {
@@ -574,7 +639,6 @@ export function objectRadius(
574
639
  } else if (bp === 'bottom-right') {
575
640
  roundBottomRight = true;
576
641
  } else {
577
- // For a comma-separated list of values.
578
642
  const positions = bp.split(',').map(s => s.trim());
579
643
  roundTopLeft = positions.includes('top-left') || (positions.includes('top') && positions.includes('left'));
580
644
  roundTopRight = positions.includes('top-right') || (positions.includes('top') && positions.includes('right'));
@@ -582,38 +646,31 @@ export function objectRadius(
582
646
  roundBottomLeft = positions.includes('bottom-left') || (positions.includes('bottom') && positions.includes('left'));
583
647
  }
584
648
 
585
- // Determine the radius for each corner.
586
649
  const tl = roundTopLeft ? +borderRadius : 0;
587
650
  const tr = roundTopRight ? +borderRadius : 0;
588
651
  const br = roundBottomRight ? +borderRadius : 0;
589
652
  const bl = roundBottomLeft ? +borderRadius : 0;
590
653
 
591
- // Construct the path.
592
654
  ctx.beginPath();
593
- // Start at top-left (with offset for rounding)
594
655
  ctx.moveTo(x + tl, y);
595
- // Top edge to top-right corner.
596
656
  ctx.lineTo(x + width - tr, y);
597
657
  if (tr > 0) {
598
658
  ctx.quadraticCurveTo(x + width, y, x + width, y + tr);
599
659
  } else {
600
660
  ctx.lineTo(x + width, y);
601
661
  }
602
- // Right edge to bottom-right corner.
603
662
  ctx.lineTo(x + width, y + height - br);
604
663
  if (br > 0) {
605
664
  ctx.quadraticCurveTo(x + width, y + height, x + width - br, y + height);
606
665
  } else {
607
666
  ctx.lineTo(x + width, y + height);
608
667
  }
609
- // Bottom edge to bottom-left corner.
610
668
  ctx.lineTo(x + bl, y + height);
611
669
  if (bl > 0) {
612
670
  ctx.quadraticCurveTo(x, y + height, x, y + height - bl);
613
671
  } else {
614
672
  ctx.lineTo(x, y + height);
615
673
  }
616
- // Left edge back to top-left.
617
674
  ctx.lineTo(x, y + tl);
618
675
  if (tl > 0) {
619
676
  ctx.quadraticCurveTo(x, y, x + tl, y);
@@ -622,10 +679,101 @@ export function objectRadius(
622
679
  }
623
680
  ctx.closePath();
624
681
  } else {
625
- // If no borderRadius is provided, simply use a rectangle.
626
682
  ctx.beginPath();
627
683
  ctx.rect(x, y, width, height);
628
684
  ctx.closePath();
629
685
  }
630
686
  }
631
-
687
+
688
+
689
+ /**
690
+ * Performs bilinear interpolation on four corners.
691
+ * @param corners The four corners (topLeft, topRight, bottomRight, bottomLeft).
692
+ * @param t Horizontal interpolation factor (0 to 1).
693
+ * @param u Vertical interpolation factor (0 to 1).
694
+ * @returns The interpolated point.
695
+ */
696
+ 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 } {
697
+ const top: { x: number, y: number } = {
698
+ x: corners[0].x * (1 - t) + corners[1].x * t,
699
+ y: corners[0].y * (1 - t) + corners[1].y * t,
700
+ };
701
+ const bottom: { x: number, y: number } = {
702
+ x: corners[3].x * (1 - t) + corners[2].x * t,
703
+ y: corners[3].y * (1 - t) + corners[2].y * t,
704
+ };
705
+ return {
706
+ x: top.x * (1 - u) + bottom.x * u,
707
+ y: top.y * (1 - u) + bottom.y * u,
708
+ };
709
+ }
710
+
711
+ /**
712
+ * Applies a perspective warp to the given image by subdividing the source rectangle
713
+ * and drawing each cell with an affine transform approximating the local perspective.
714
+ *
715
+ * @param ctx The canvas rendering context.
716
+ * @param image The source image.
717
+ * @param x The x-coordinate where the image is drawn.
718
+ * @param y The y-coordinate where the image is drawn.
719
+ * @param width The width of the region to draw.
720
+ * @param height The height of the region to draw.
721
+ * @param perspective An object containing four destination corners:
722
+ * { topLeft, topRight, bottomRight, bottomLeft }.
723
+ * @param gridCols Number of columns to subdivide (default: 10).
724
+ * @param gridRows Number of rows to subdivide (default: 10).
725
+ */
726
+ export async function applyPerspective(
727
+ ctx: SKRSContext2D,
728
+ image: Image,
729
+ x: number,
730
+ y: number,
731
+ width: number,
732
+ height: number,
733
+ perspective: { topLeft: { x: number, y: number }; topRight: { x: number, y: number }; bottomRight: { x: number, y: number }; bottomLeft: { x: number, y: number }; },
734
+ gridCols: number = 10,
735
+ gridRows: number = 10
736
+ ): Promise<void> {
737
+ const cellWidth = width / gridCols;
738
+ const cellHeight = height / gridRows;
739
+
740
+ for (let row = 0; row < gridRows; row++) {
741
+ for (let col = 0; col < gridCols; col++) {
742
+ const sx = x + col * cellWidth;
743
+ const sy = y + row * cellHeight;
744
+
745
+ const t0 = col / gridCols;
746
+ const t1 = (col + 1) / gridCols;
747
+ const u0 = row / gridRows;
748
+ const u1 = (row + 1) / gridRows;
749
+
750
+ const destTL = bilinearInterpolate(
751
+ [perspective.topLeft, perspective.topRight, perspective.bottomRight, perspective.bottomLeft],
752
+ t0,
753
+ u0
754
+ );
755
+ const destTR = bilinearInterpolate(
756
+ [perspective.topLeft, perspective.topRight, perspective.bottomRight, perspective.bottomLeft],
757
+ t1,
758
+ u0
759
+ );
760
+ const destBL = bilinearInterpolate(
761
+ [perspective.topLeft, perspective.topRight, perspective.bottomRight, perspective.bottomLeft],
762
+ t0,
763
+ u1
764
+ );
765
+
766
+ const a = (destTR.x - destTL.x) / cellWidth;
767
+ const b = (destTR.y - destTL.y) / cellWidth;
768
+ const c = (destBL.x - destTL.x) / cellHeight;
769
+ const d = (destBL.y - destTL.y) / cellHeight;
770
+ const e = destTL.x;
771
+ const f = destTL.y;
772
+
773
+ ctx.save();
774
+ ctx.setTransform(a, b, c, d, e, f);
775
+ ctx.drawImage(image, sx, sy, cellWidth, cellHeight, 0, 0, cellWidth, cellHeight);
776
+ ctx.restore();
777
+ }
778
+ }
779
+ }