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.
- package/README.md +437 -47
- package/dist/cjs/Canvas/ApexPainter.d.ts +122 -78
- package/dist/cjs/Canvas/ApexPainter.d.ts.map +1 -1
- package/dist/cjs/Canvas/ApexPainter.js +461 -352
- package/dist/cjs/Canvas/ApexPainter.js.map +1 -1
- package/dist/cjs/Canvas/utils/Background/bg.d.ts +23 -11
- package/dist/cjs/Canvas/utils/Background/bg.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Background/bg.js +174 -107
- package/dist/cjs/Canvas/utils/Background/bg.js.map +1 -1
- package/dist/cjs/Canvas/utils/Custom/customLines.js +2 -2
- package/dist/cjs/Canvas/utils/Custom/customLines.js.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageFilters.d.ts +11 -0
- package/dist/cjs/Canvas/utils/Image/imageFilters.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Image/imageFilters.js +307 -0
- package/dist/cjs/Canvas/utils/Image/imageFilters.js.map +1 -0
- package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts +47 -112
- package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageProperties.js +229 -560
- package/dist/cjs/Canvas/utils/Image/imageProperties.js.map +1 -1
- package/dist/cjs/Canvas/utils/Image/professionalImageFilters.d.ts +11 -0
- package/dist/cjs/Canvas/utils/Image/professionalImageFilters.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Image/professionalImageFilters.js +351 -0
- package/dist/cjs/Canvas/utils/Image/professionalImageFilters.js.map +1 -0
- package/dist/cjs/Canvas/utils/Image/simpleProfessionalFilters.d.ts +11 -0
- package/dist/cjs/Canvas/utils/Image/simpleProfessionalFilters.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Image/simpleProfessionalFilters.js +215 -0
- package/dist/cjs/Canvas/utils/Image/simpleProfessionalFilters.js.map +1 -0
- package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts +71 -0
- package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js +392 -0
- package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -0
- package/dist/cjs/Canvas/utils/Shapes/shapes.d.ts +29 -0
- package/dist/cjs/Canvas/utils/Shapes/shapes.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Shapes/shapes.js +334 -0
- package/dist/cjs/Canvas/utils/Shapes/shapes.js.map +1 -0
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts +127 -0
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js +365 -0
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -0
- package/dist/cjs/Canvas/utils/types.d.ts +227 -131
- package/dist/cjs/Canvas/utils/types.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/types.js +0 -1
- package/dist/cjs/Canvas/utils/types.js.map +1 -1
- package/dist/cjs/Canvas/utils/utils.d.ts +7 -4
- package/dist/cjs/Canvas/utils/utils.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/utils.js +17 -7
- package/dist/cjs/Canvas/utils/utils.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/Canvas/ApexPainter.d.ts +122 -78
- package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
- package/dist/esm/Canvas/ApexPainter.js +461 -352
- package/dist/esm/Canvas/ApexPainter.js.map +1 -1
- package/dist/esm/Canvas/utils/Background/bg.d.ts +23 -11
- package/dist/esm/Canvas/utils/Background/bg.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Background/bg.js +174 -107
- package/dist/esm/Canvas/utils/Background/bg.js.map +1 -1
- package/dist/esm/Canvas/utils/Custom/customLines.js +2 -2
- package/dist/esm/Canvas/utils/Custom/customLines.js.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageFilters.d.ts +11 -0
- package/dist/esm/Canvas/utils/Image/imageFilters.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Image/imageFilters.js +307 -0
- package/dist/esm/Canvas/utils/Image/imageFilters.js.map +1 -0
- package/dist/esm/Canvas/utils/Image/imageProperties.d.ts +47 -112
- package/dist/esm/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageProperties.js +229 -560
- package/dist/esm/Canvas/utils/Image/imageProperties.js.map +1 -1
- package/dist/esm/Canvas/utils/Image/professionalImageFilters.d.ts +11 -0
- package/dist/esm/Canvas/utils/Image/professionalImageFilters.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Image/professionalImageFilters.js +351 -0
- package/dist/esm/Canvas/utils/Image/professionalImageFilters.js.map +1 -0
- package/dist/esm/Canvas/utils/Image/simpleProfessionalFilters.d.ts +11 -0
- package/dist/esm/Canvas/utils/Image/simpleProfessionalFilters.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Image/simpleProfessionalFilters.js +215 -0
- package/dist/esm/Canvas/utils/Image/simpleProfessionalFilters.js.map +1 -0
- package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts +71 -0
- package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js +392 -0
- package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -0
- package/dist/esm/Canvas/utils/Shapes/shapes.d.ts +29 -0
- package/dist/esm/Canvas/utils/Shapes/shapes.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Shapes/shapes.js +334 -0
- package/dist/esm/Canvas/utils/Shapes/shapes.js.map +1 -0
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts +127 -0
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js +365 -0
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -0
- package/dist/esm/Canvas/utils/types.d.ts +227 -131
- package/dist/esm/Canvas/utils/types.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/types.js +0 -1
- package/dist/esm/Canvas/utils/types.js.map +1 -1
- package/dist/esm/Canvas/utils/utils.d.ts +7 -4
- package/dist/esm/Canvas/utils/utils.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/utils.js +17 -7
- package/dist/esm/Canvas/utils/utils.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/lib/Canvas/ApexPainter.ts +1325 -1218
- package/lib/Canvas/utils/Background/bg.ts +247 -173
- package/lib/Canvas/utils/Custom/customLines.ts +3 -3
- package/lib/Canvas/utils/Image/imageFilters.ts +356 -0
- package/lib/Canvas/utils/Image/imageProperties.ts +322 -775
- package/lib/Canvas/utils/Image/professionalImageFilters.ts +391 -0
- package/lib/Canvas/utils/Image/simpleProfessionalFilters.ts +229 -0
- package/lib/Canvas/utils/Patterns/enhancedPatternRenderer.ts +444 -0
- package/lib/Canvas/utils/Shapes/shapes.ts +528 -0
- package/lib/Canvas/utils/Texts/enhancedTextRenderer.ts +478 -0
- package/lib/Canvas/utils/types.ts +301 -117
- package/lib/Canvas/utils/utils.ts +85 -72
- package/package.json +106 -188
|
@@ -1,835 +1,382 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
6
|
+
x: number, y: number, w: number, h: number,
|
|
7
|
+
radius: number | "circular" = 0,
|
|
8
|
+
borderPos: borderPosition = "all"
|
|
62
9
|
): void {
|
|
63
|
-
|
|
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 (
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
19
|
+
if (!radius || radius <= 0) {
|
|
20
|
+
ctx.rect(x, y, w, h);
|
|
21
|
+
ctx.closePath();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
108
24
|
|
|
109
|
-
|
|
110
|
-
const
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
126
|
+
// utils/imageMath.ts
|
|
127
|
+
import type { AlignMode, FitMode } from "../types";
|
|
228
128
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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 (
|
|
301
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
190
|
+
/* ---------------------------------------------
|
|
191
|
+
SHADOW — overloaded to support both call styles
|
|
192
|
+
--------------------------------------------- */
|
|
429
193
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
ctx
|
|
433
|
-
|
|
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
|
-
|
|
436
|
-
|
|
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
|
-
|
|
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
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
457
|
-
|
|
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.
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
);
|
|
481
|
-
}
|
|
270
|
+
/* ---------------------------------------------
|
|
271
|
+
STROKE — overloaded to support both call styles
|
|
272
|
+
--------------------------------------------- */
|
|
482
273
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
//
|
|
737
|
-
|
|
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
|
-
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
|
|
316
|
+
if (!stroke) return;
|
|
744
317
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
+
}
|