apexify.js 4.9.28 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +727 -456
- package/dist/cjs/Canvas/ApexPainter.d.ts +96 -145
- package/dist/cjs/Canvas/ApexPainter.d.ts.map +1 -1
- package/dist/cjs/Canvas/ApexPainter.js +1416 -420
- package/dist/cjs/Canvas/ApexPainter.js.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/charts.d.ts +7 -2
- package/dist/cjs/Canvas/utils/Charts/charts.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/charts.js +3 -1
- package/dist/cjs/Canvas/utils/Charts/charts.js.map +1 -1
- package/dist/cjs/Canvas/utils/Custom/advancedLines.d.ts +75 -0
- package/dist/cjs/Canvas/utils/Custom/advancedLines.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Custom/advancedLines.js +263 -0
- package/dist/cjs/Canvas/utils/Custom/advancedLines.js.map +1 -0
- package/dist/cjs/Canvas/utils/Custom/customLines.d.ts +2 -1
- package/dist/cjs/Canvas/utils/Custom/customLines.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Custom/customLines.js +73 -6
- package/dist/cjs/Canvas/utils/Custom/customLines.js.map +1 -1
- package/dist/cjs/Canvas/utils/General/batchOperations.d.ts +17 -0
- package/dist/cjs/Canvas/utils/General/batchOperations.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/General/batchOperations.js +88 -0
- package/dist/cjs/Canvas/utils/General/batchOperations.js.map +1 -0
- package/dist/cjs/Canvas/utils/General/general functions.d.ts +25 -3
- package/dist/cjs/Canvas/utils/General/general functions.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/General/general functions.js +37 -9
- package/dist/cjs/Canvas/utils/General/general functions.js.map +1 -1
- package/dist/cjs/Canvas/utils/General/imageCompression.d.ts +19 -0
- package/dist/cjs/Canvas/utils/General/imageCompression.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/General/imageCompression.js +262 -0
- package/dist/cjs/Canvas/utils/General/imageCompression.js.map +1 -0
- package/dist/cjs/Canvas/utils/General/imageStitching.d.ts +20 -0
- package/dist/cjs/Canvas/utils/General/imageStitching.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/General/imageStitching.js +227 -0
- package/dist/cjs/Canvas/utils/General/imageStitching.js.map +1 -0
- package/dist/cjs/Canvas/utils/Image/imageEffects.d.ts +37 -0
- package/dist/cjs/Canvas/utils/Image/imageEffects.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Image/imageEffects.js +128 -0
- package/dist/cjs/Canvas/utils/Image/imageEffects.js.map +1 -0
- package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts +67 -0
- package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Image/imageMasking.js +276 -0
- package/dist/cjs/Canvas/utils/Image/imageMasking.js.map +1 -0
- package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageProperties.js +181 -2
- package/dist/cjs/Canvas/utils/Image/imageProperties.js.map +1 -1
- package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js +16 -8
- package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts +33 -0
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js +237 -32
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/textPathRenderer.d.ts +17 -0
- package/dist/cjs/Canvas/utils/Texts/textPathRenderer.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Texts/textPathRenderer.js +233 -0
- package/dist/cjs/Canvas/utils/Texts/textPathRenderer.js.map +1 -0
- package/dist/cjs/Canvas/utils/types.d.ts +171 -10
- package/dist/cjs/Canvas/utils/types.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/types.js.map +1 -1
- package/dist/cjs/Canvas/utils/utils.d.ts +9 -2
- package/dist/cjs/Canvas/utils/utils.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/utils.js +32 -1
- 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 +96 -145
- package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
- package/dist/esm/Canvas/ApexPainter.js +1416 -420
- package/dist/esm/Canvas/ApexPainter.js.map +1 -1
- package/dist/esm/Canvas/utils/Charts/charts.d.ts +7 -2
- package/dist/esm/Canvas/utils/Charts/charts.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Charts/charts.js +3 -1
- package/dist/esm/Canvas/utils/Charts/charts.js.map +1 -1
- package/dist/esm/Canvas/utils/Custom/advancedLines.d.ts +75 -0
- package/dist/esm/Canvas/utils/Custom/advancedLines.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Custom/advancedLines.js +263 -0
- package/dist/esm/Canvas/utils/Custom/advancedLines.js.map +1 -0
- package/dist/esm/Canvas/utils/Custom/customLines.d.ts +2 -1
- package/dist/esm/Canvas/utils/Custom/customLines.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Custom/customLines.js +73 -6
- package/dist/esm/Canvas/utils/Custom/customLines.js.map +1 -1
- package/dist/esm/Canvas/utils/General/batchOperations.d.ts +17 -0
- package/dist/esm/Canvas/utils/General/batchOperations.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/General/batchOperations.js +88 -0
- package/dist/esm/Canvas/utils/General/batchOperations.js.map +1 -0
- package/dist/esm/Canvas/utils/General/general functions.d.ts +25 -3
- package/dist/esm/Canvas/utils/General/general functions.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/General/general functions.js +37 -9
- package/dist/esm/Canvas/utils/General/general functions.js.map +1 -1
- package/dist/esm/Canvas/utils/General/imageCompression.d.ts +19 -0
- package/dist/esm/Canvas/utils/General/imageCompression.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/General/imageCompression.js +262 -0
- package/dist/esm/Canvas/utils/General/imageCompression.js.map +1 -0
- package/dist/esm/Canvas/utils/General/imageStitching.d.ts +20 -0
- package/dist/esm/Canvas/utils/General/imageStitching.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/General/imageStitching.js +227 -0
- package/dist/esm/Canvas/utils/General/imageStitching.js.map +1 -0
- package/dist/esm/Canvas/utils/Image/imageEffects.d.ts +37 -0
- package/dist/esm/Canvas/utils/Image/imageEffects.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Image/imageEffects.js +128 -0
- package/dist/esm/Canvas/utils/Image/imageEffects.js.map +1 -0
- package/dist/esm/Canvas/utils/Image/imageMasking.d.ts +67 -0
- package/dist/esm/Canvas/utils/Image/imageMasking.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Image/imageMasking.js +276 -0
- package/dist/esm/Canvas/utils/Image/imageMasking.js.map +1 -0
- package/dist/esm/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageProperties.js +181 -2
- package/dist/esm/Canvas/utils/Image/imageProperties.js.map +1 -1
- package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js +16 -8
- package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -1
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts +33 -0
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js +237 -32
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
- package/dist/esm/Canvas/utils/Texts/textPathRenderer.d.ts +17 -0
- package/dist/esm/Canvas/utils/Texts/textPathRenderer.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Texts/textPathRenderer.js +233 -0
- package/dist/esm/Canvas/utils/Texts/textPathRenderer.js.map +1 -0
- package/dist/esm/Canvas/utils/types.d.ts +171 -10
- package/dist/esm/Canvas/utils/types.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/types.js.map +1 -1
- package/dist/esm/Canvas/utils/utils.d.ts +9 -2
- package/dist/esm/Canvas/utils/utils.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/utils.js +32 -1
- package/dist/esm/Canvas/utils/utils.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/lib/Canvas/ApexPainter.ts +1309 -267
- package/lib/Canvas/utils/Charts/charts.ts +16 -7
- package/lib/Canvas/utils/Custom/advancedLines.ts +335 -0
- package/lib/Canvas/utils/Custom/customLines.ts +84 -9
- package/lib/Canvas/utils/General/batchOperations.ts +103 -0
- package/lib/Canvas/utils/General/general functions.ts +85 -41
- package/lib/Canvas/utils/General/imageCompression.ts +316 -0
- package/lib/Canvas/utils/General/imageStitching.ts +252 -0
- package/lib/Canvas/utils/Image/imageEffects.ts +175 -0
- package/lib/Canvas/utils/Image/imageMasking.ts +335 -0
- package/lib/Canvas/utils/Image/imageProperties.ts +207 -2
- package/lib/Canvas/utils/Patterns/enhancedPatternRenderer.ts +455 -444
- package/lib/Canvas/utils/Texts/enhancedTextRenderer.ts +274 -36
- package/lib/Canvas/utils/Texts/textPathRenderer.ts +320 -0
- package/lib/Canvas/utils/types.ts +173 -10
- package/lib/Canvas/utils/utils.ts +49 -2
- package/package.json +69 -34
|
@@ -46,7 +46,13 @@ class ApexPainter {
|
|
|
46
46
|
* @param textProps - Text properties
|
|
47
47
|
*/
|
|
48
48
|
async #renderEnhancedText(ctx, textProps) {
|
|
49
|
-
|
|
49
|
+
// Check if text should be rendered on a path
|
|
50
|
+
if (textProps.path && textProps.textOnPath) {
|
|
51
|
+
(0, utils_1.renderTextOnPath)(ctx, textProps.text, textProps.path, textProps.path.offset ?? 0);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
await enhancedTextRenderer_1.EnhancedTextRenderer.renderText(ctx, textProps);
|
|
55
|
+
}
|
|
50
56
|
}
|
|
51
57
|
/**
|
|
52
58
|
* Creates a canvas with the given configuration.
|
|
@@ -79,75 +85,162 @@ class ApexPainter {
|
|
|
79
85
|
* const buffer = result.buffer;
|
|
80
86
|
* ```
|
|
81
87
|
*/
|
|
88
|
+
/**
|
|
89
|
+
* Validates canvas configuration.
|
|
90
|
+
* @private
|
|
91
|
+
* @param canvas - Canvas configuration to validate
|
|
92
|
+
*/
|
|
93
|
+
#validateCanvasConfig(canvas) {
|
|
94
|
+
if (!canvas) {
|
|
95
|
+
throw new Error("createCanvas: canvas configuration is required.");
|
|
96
|
+
}
|
|
97
|
+
if (canvas.width !== undefined && (typeof canvas.width !== 'number' || canvas.width <= 0)) {
|
|
98
|
+
throw new Error("createCanvas: width must be a positive number.");
|
|
99
|
+
}
|
|
100
|
+
if (canvas.height !== undefined && (typeof canvas.height !== 'number' || canvas.height <= 0)) {
|
|
101
|
+
throw new Error("createCanvas: height must be a positive number.");
|
|
102
|
+
}
|
|
103
|
+
if (canvas.opacity !== undefined && (typeof canvas.opacity !== 'number' || canvas.opacity < 0 || canvas.opacity > 1)) {
|
|
104
|
+
throw new Error("createCanvas: opacity must be a number between 0 and 1.");
|
|
105
|
+
}
|
|
106
|
+
if (canvas.zoom?.scale !== undefined && (typeof canvas.zoom.scale !== 'number' || canvas.zoom.scale <= 0)) {
|
|
107
|
+
throw new Error("createCanvas: zoom.scale must be a positive number.");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
82
110
|
async createCanvas(canvas) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// Apply shadow effect
|
|
137
|
-
if (shadow) {
|
|
111
|
+
try {
|
|
112
|
+
// Validate canvas configuration
|
|
113
|
+
this.#validateCanvasConfig(canvas);
|
|
114
|
+
// Handle inherit sizing
|
|
115
|
+
if (canvas.customBg?.inherit) {
|
|
116
|
+
let p = canvas.customBg.source;
|
|
117
|
+
if (!/^https?:\/\//i.test(p))
|
|
118
|
+
p = path_1.default.join(process.cwd(), p);
|
|
119
|
+
try {
|
|
120
|
+
const img = await (0, canvas_1.loadImage)(p);
|
|
121
|
+
canvas.width = img.width;
|
|
122
|
+
canvas.height = img.height;
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
126
|
+
throw new Error(`createCanvas: Failed to load image for inherit sizing: ${errorMessage}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Handle video background inherit sizing
|
|
130
|
+
if (canvas.videoBg) {
|
|
131
|
+
try {
|
|
132
|
+
const frameBuffer = await this.#extractVideoFrame(canvas.videoBg.source, canvas.videoBg.frame ?? 0);
|
|
133
|
+
if (frameBuffer) {
|
|
134
|
+
const img = await (0, canvas_1.loadImage)(frameBuffer);
|
|
135
|
+
if (!canvas.width)
|
|
136
|
+
canvas.width = img.width;
|
|
137
|
+
if (!canvas.height)
|
|
138
|
+
canvas.height = img.height;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
console.warn('createCanvas: Failed to extract video frame for sizing, using defaults');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// 2) Use final width/height after inherit
|
|
146
|
+
const width = canvas.width ?? 500;
|
|
147
|
+
const height = canvas.height ?? 500;
|
|
148
|
+
const { x = 0, y = 0, rotation = 0, borderRadius = 0, borderPosition = 'all', opacity = 1, colorBg, customBg, gradientBg, videoBg, patternBg, noiseBg, blendMode, zoom, stroke, shadow, blur } = canvas;
|
|
149
|
+
// Validate background configuration
|
|
150
|
+
const bgSources = [
|
|
151
|
+
canvas.colorBg ? 'colorBg' : null,
|
|
152
|
+
canvas.gradientBg ? 'gradientBg' : null,
|
|
153
|
+
canvas.customBg ? 'customBg' : null
|
|
154
|
+
].filter(Boolean);
|
|
155
|
+
if (bgSources.length > 1) {
|
|
156
|
+
throw new Error(`createCanvas: only one of colorBg, gradientBg, or customBg can be used. You provided: ${bgSources.join(', ')}`);
|
|
157
|
+
}
|
|
158
|
+
const cv = (0, canvas_1.createCanvas)(width, height);
|
|
159
|
+
const ctx = cv.getContext('2d');
|
|
160
|
+
if (!ctx)
|
|
161
|
+
throw new Error('Unable to get 2D context');
|
|
162
|
+
ctx.globalAlpha = opacity;
|
|
163
|
+
// ---- BACKGROUND (clipped) ----
|
|
138
164
|
ctx.save();
|
|
165
|
+
(0, utils_1.applyRotation)(ctx, rotation, x, y, width, height);
|
|
139
166
|
(0, utils_1.buildPath)(ctx, x, y, width, height, borderRadius, borderPosition);
|
|
140
|
-
|
|
167
|
+
ctx.clip();
|
|
168
|
+
(0, utils_1.applyCanvasZoom)(ctx, width, height, zoom);
|
|
169
|
+
ctx.translate(x, y);
|
|
170
|
+
if (typeof blendMode === 'string') {
|
|
171
|
+
ctx.globalCompositeOperation = blendMode;
|
|
172
|
+
}
|
|
173
|
+
// Draw video background if specified
|
|
174
|
+
if (videoBg) {
|
|
175
|
+
try {
|
|
176
|
+
const frameBuffer = await this.#extractVideoFrame(videoBg.source, videoBg.frame ?? 0);
|
|
177
|
+
if (frameBuffer) {
|
|
178
|
+
const videoImg = await (0, canvas_1.loadImage)(frameBuffer);
|
|
179
|
+
ctx.globalAlpha = videoBg.opacity ?? 1;
|
|
180
|
+
ctx.drawImage(videoImg, 0, 0, width, height);
|
|
181
|
+
ctx.globalAlpha = opacity;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
console.warn('createCanvas: Failed to load video background frame');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Draw custom background with filters and opacity support
|
|
189
|
+
if (customBg) {
|
|
190
|
+
await (0, utils_1.customBackground)(ctx, { ...canvas, blur });
|
|
191
|
+
// Apply filters to background if specified
|
|
192
|
+
if (customBg.filters && customBg.filters.length > 0) {
|
|
193
|
+
const tempCanvas = (0, canvas_1.createCanvas)(width, height);
|
|
194
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
195
|
+
if (tempCtx) {
|
|
196
|
+
tempCtx.drawImage(cv, 0, 0);
|
|
197
|
+
await (0, utils_1.applySimpleProfessionalFilters)(tempCtx, customBg.filters, width, height);
|
|
198
|
+
ctx.clearRect(0, 0, width, height);
|
|
199
|
+
ctx.globalAlpha = customBg.opacity ?? 1;
|
|
200
|
+
ctx.drawImage(tempCanvas, 0, 0);
|
|
201
|
+
ctx.globalAlpha = opacity;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (customBg.opacity !== undefined && customBg.opacity !== 1) {
|
|
205
|
+
ctx.globalAlpha = customBg.opacity;
|
|
206
|
+
await (0, utils_1.customBackground)(ctx, { ...canvas, blur });
|
|
207
|
+
ctx.globalAlpha = opacity;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
await (0, utils_1.customBackground)(ctx, { ...canvas, blur });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else if (gradientBg) {
|
|
214
|
+
await (0, utils_1.drawBackgroundGradient)(ctx, { ...canvas, blur });
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
await (0, utils_1.drawBackgroundColor)(ctx, { ...canvas, blur, colorBg: colorBg ?? '#000' });
|
|
218
|
+
}
|
|
219
|
+
if (patternBg)
|
|
220
|
+
await enhancedPatternRenderer_1.EnhancedPatternRenderer.renderPattern(ctx, cv, patternBg);
|
|
221
|
+
if (noiseBg)
|
|
222
|
+
(0, utils_1.applyNoise)(ctx, width, height, noiseBg.intensity ?? 0.05);
|
|
141
223
|
ctx.restore();
|
|
224
|
+
// Apply shadow effect
|
|
225
|
+
if (shadow) {
|
|
226
|
+
ctx.save();
|
|
227
|
+
(0, utils_1.buildPath)(ctx, x, y, width, height, borderRadius, borderPosition);
|
|
228
|
+
(0, utils_1.applyShadow)(ctx, shadow, x, y, width, height);
|
|
229
|
+
ctx.restore();
|
|
230
|
+
}
|
|
231
|
+
// Apply stroke effect
|
|
232
|
+
if (stroke) {
|
|
233
|
+
ctx.save();
|
|
234
|
+
(0, utils_1.buildPath)(ctx, x, y, width, height, borderRadius, borderPosition);
|
|
235
|
+
(0, utils_1.applyStroke)(ctx, stroke, x, y, width, height);
|
|
236
|
+
ctx.restore();
|
|
237
|
+
}
|
|
238
|
+
return { buffer: cv.toBuffer('image/png'), canvas };
|
|
142
239
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
(0, utils_1.buildPath)(ctx, x, y, width, height, borderRadius, borderPosition);
|
|
147
|
-
(0, utils_1.applyStroke)(ctx, stroke, x, y, width, height);
|
|
148
|
-
ctx.restore();
|
|
240
|
+
catch (error) {
|
|
241
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
242
|
+
throw new Error(`createCanvas failed: ${errorMessage}`);
|
|
149
243
|
}
|
|
150
|
-
return { buffer: cv.toBuffer('image/png'), canvas };
|
|
151
244
|
}
|
|
152
245
|
/**
|
|
153
246
|
* Draws one or more images (or shapes) on an existing canvas buffer.
|
|
@@ -191,24 +284,49 @@ class ApexPainter {
|
|
|
191
284
|
* ], canvasBuffer);
|
|
192
285
|
* ```
|
|
193
286
|
*/
|
|
194
|
-
|
|
287
|
+
/**
|
|
288
|
+
* Validates image/shape properties array.
|
|
289
|
+
* @private
|
|
290
|
+
* @param images - Image properties to validate
|
|
291
|
+
*/
|
|
292
|
+
#validateImageArray(images) {
|
|
195
293
|
const list = Array.isArray(images) ? images : [images];
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
: await (0, canvas_1.loadImage)(canvasBuffer.buffer);
|
|
200
|
-
const cv = (0, canvas_1.createCanvas)(base.width, base.height);
|
|
201
|
-
const ctx = cv.getContext("2d");
|
|
202
|
-
if (!ctx)
|
|
203
|
-
throw new Error("Unable to get 2D rendering context");
|
|
204
|
-
// Paint bg
|
|
205
|
-
ctx.drawImage(base, 0, 0);
|
|
206
|
-
// Draw each image/shape on canvas
|
|
294
|
+
if (list.length === 0) {
|
|
295
|
+
throw new Error("createImage: At least one image/shape is required.");
|
|
296
|
+
}
|
|
207
297
|
for (const ip of list) {
|
|
208
|
-
|
|
298
|
+
this.#validateImageProperties(ip);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async createImage(images, canvasBuffer) {
|
|
302
|
+
try {
|
|
303
|
+
// Validate inputs
|
|
304
|
+
if (!canvasBuffer) {
|
|
305
|
+
throw new Error("createImage: canvasBuffer is required.");
|
|
306
|
+
}
|
|
307
|
+
this.#validateImageArray(images);
|
|
308
|
+
const list = Array.isArray(images) ? images : [images];
|
|
309
|
+
// Load base canvas buffer
|
|
310
|
+
const base = Buffer.isBuffer(canvasBuffer)
|
|
311
|
+
? await (0, canvas_1.loadImage)(canvasBuffer)
|
|
312
|
+
: await (0, canvas_1.loadImage)(canvasBuffer.buffer);
|
|
313
|
+
const cv = (0, canvas_1.createCanvas)(base.width, base.height);
|
|
314
|
+
const ctx = cv.getContext("2d");
|
|
315
|
+
if (!ctx)
|
|
316
|
+
throw new Error("Unable to get 2D rendering context");
|
|
317
|
+
// Paint bg
|
|
318
|
+
ctx.drawImage(base, 0, 0);
|
|
319
|
+
// Draw each image/shape on canvas
|
|
320
|
+
for (const ip of list) {
|
|
321
|
+
await this.#drawImageBitmap(ctx, ip);
|
|
322
|
+
}
|
|
323
|
+
// Return updated buffer
|
|
324
|
+
return cv.toBuffer("image/png");
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
328
|
+
throw new Error(`createImage failed: ${errorMessage}`);
|
|
209
329
|
}
|
|
210
|
-
// Return updated buffer
|
|
211
|
-
return cv.toBuffer("image/png");
|
|
212
330
|
}
|
|
213
331
|
/**
|
|
214
332
|
* Draws a single bitmap or shape with independent shadow & stroke.
|
|
@@ -217,7 +335,7 @@ class ApexPainter {
|
|
|
217
335
|
* @param ip - Image properties
|
|
218
336
|
*/
|
|
219
337
|
async #drawImageBitmap(ctx, ip) {
|
|
220
|
-
const { source, x, y, width, height, inherit, fit = "fill", align = "center", rotation = 0, opacity = 1, blur = 0, borderRadius = 0, borderPosition = "all", shadow, stroke, boxBackground, shape, filters } = ip;
|
|
338
|
+
const { source, x, y, width, height, inherit, fit = "fill", align = "center", rotation = 0, opacity = 1, blur = 0, borderRadius = 0, borderPosition = "all", shadow, stroke, boxBackground, shape, filters, filterIntensity = 1, filterOrder = 'post', mask, clipPath, distortion, meshWarp, effects } = ip;
|
|
221
339
|
this.#validateImageProperties(ip);
|
|
222
340
|
// Check if source is a shape
|
|
223
341
|
if ((0, utils_1.isShapeSource)(source)) {
|
|
@@ -248,23 +366,126 @@ class ApexPainter {
|
|
|
248
366
|
(0, utils_1.applyShadow)(ctx, box, shadow);
|
|
249
367
|
// 2) Optional box background (under bitmap, inside clip) — color or gradient
|
|
250
368
|
(0, utils_1.drawBoxBackground)(ctx, box, boxBackground, borderRadius, borderPosition);
|
|
251
|
-
// 3) Clip to image border radius, then draw the bitmap with blur/opacity and fit/align
|
|
369
|
+
// 3) Clip to image border radius or custom clip path, then draw the bitmap with blur/opacity and fit/align
|
|
252
370
|
ctx.save();
|
|
253
|
-
|
|
254
|
-
|
|
371
|
+
if (clipPath && clipPath.length >= 3) {
|
|
372
|
+
(0, utils_1.applyClipPath)(ctx, clipPath);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
(0, utils_1.buildPath)(ctx, box.x, box.y, box.w, box.h, borderRadius, borderPosition);
|
|
376
|
+
ctx.clip();
|
|
377
|
+
}
|
|
255
378
|
const { dx, dy, dw, dh, sx, sy, sw, sh } = (0, utils_1.fitInto)(box.x, box.y, box.w, box.h, img.width, img.height, fit, align);
|
|
256
379
|
const prevAlpha = ctx.globalAlpha;
|
|
257
380
|
ctx.globalAlpha = opacity ?? 1;
|
|
258
381
|
if ((blur ?? 0) > 0)
|
|
259
382
|
ctx.filter = `blur(${blur}px)`;
|
|
260
|
-
// Apply professional image filters BEFORE drawing
|
|
261
|
-
if (filters && filters.length > 0) {
|
|
262
|
-
|
|
383
|
+
// Apply professional image filters BEFORE drawing if filterOrder is 'pre'
|
|
384
|
+
if (filters && filters.length > 0 && filterOrder === 'pre') {
|
|
385
|
+
const adjustedFilters = filters.map(f => ({
|
|
386
|
+
...f,
|
|
387
|
+
intensity: f.intensity !== undefined ? f.intensity * filterIntensity : (f.intensity ?? 1) * filterIntensity,
|
|
388
|
+
value: f.value !== undefined ? f.value * filterIntensity : f.value,
|
|
389
|
+
radius: f.radius !== undefined ? f.radius * filterIntensity : f.radius
|
|
390
|
+
}));
|
|
391
|
+
await (0, utils_1.applySimpleProfessionalFilters)(ctx, adjustedFilters, dw, dh);
|
|
392
|
+
}
|
|
393
|
+
// Apply distortion if specified (before drawing)
|
|
394
|
+
if (distortion) {
|
|
395
|
+
if (distortion.type === 'perspective' && distortion.points && distortion.points.length === 4) {
|
|
396
|
+
(0, utils_1.applyPerspectiveDistortion)(ctx, img, distortion.points, dx, dy, dw, dh);
|
|
397
|
+
ctx.filter = "none";
|
|
398
|
+
ctx.globalAlpha = prevAlpha;
|
|
399
|
+
ctx.restore();
|
|
400
|
+
ctx.restore();
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
else if (distortion.type === 'bulge' || distortion.type === 'pinch') {
|
|
404
|
+
const centerX = dx + dw / 2;
|
|
405
|
+
const centerY = dy + dh / 2;
|
|
406
|
+
const radius = Math.min(dw, dh) / 2;
|
|
407
|
+
const intensity = (distortion.intensity ?? 0.5) * (distortion.type === 'pinch' ? -1 : 1);
|
|
408
|
+
(0, utils_1.applyBulgeDistortion)(ctx, img, centerX, centerY, radius, intensity, dx, dy, dw, dh);
|
|
409
|
+
ctx.filter = "none";
|
|
410
|
+
ctx.globalAlpha = prevAlpha;
|
|
411
|
+
ctx.restore();
|
|
412
|
+
ctx.restore();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// Apply mesh warp if specified
|
|
417
|
+
if (meshWarp && meshWarp.controlPoints) {
|
|
418
|
+
(0, utils_1.applyMeshWarp)(ctx, img, meshWarp.gridX ?? 10, meshWarp.gridY ?? 10, meshWarp.controlPoints, dx, dy, dw, dh);
|
|
419
|
+
ctx.filter = "none";
|
|
420
|
+
ctx.globalAlpha = prevAlpha;
|
|
421
|
+
ctx.restore();
|
|
422
|
+
ctx.restore();
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
// Draw image with or without masking
|
|
426
|
+
if (mask) {
|
|
427
|
+
await (0, utils_1.applyImageMask)(ctx, img, mask.source, mask.mode ?? 'alpha', dx, dy, dw, dh);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
|
|
263
431
|
}
|
|
264
|
-
ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
|
|
265
432
|
ctx.filter = "none";
|
|
266
433
|
ctx.globalAlpha = prevAlpha;
|
|
267
434
|
ctx.restore();
|
|
435
|
+
// Apply professional image filters AFTER drawing if filterOrder is 'post'
|
|
436
|
+
if (filters && filters.length > 0 && filterOrder === 'post') {
|
|
437
|
+
ctx.save();
|
|
438
|
+
const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
|
|
439
|
+
const tempCanvas = (0, canvas_1.createCanvas)(box.w, box.h);
|
|
440
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
441
|
+
if (tempCtx) {
|
|
442
|
+
tempCtx.putImageData(imageData, 0, 0);
|
|
443
|
+
const adjustedFilters = filters.map(f => ({
|
|
444
|
+
...f,
|
|
445
|
+
intensity: f.intensity !== undefined ? f.intensity * filterIntensity : (f.intensity ?? 1) * filterIntensity,
|
|
446
|
+
value: f.value !== undefined ? f.value * filterIntensity : f.value,
|
|
447
|
+
radius: f.radius !== undefined ? f.radius * filterIntensity : f.radius
|
|
448
|
+
}));
|
|
449
|
+
await (0, utils_1.applySimpleProfessionalFilters)(tempCtx, adjustedFilters, box.w, box.h);
|
|
450
|
+
ctx.clearRect(box.x, box.y, box.w, box.h);
|
|
451
|
+
ctx.drawImage(tempCanvas, box.x, box.y);
|
|
452
|
+
}
|
|
453
|
+
ctx.restore();
|
|
454
|
+
}
|
|
455
|
+
// Apply effects stack
|
|
456
|
+
if (effects) {
|
|
457
|
+
ctx.save();
|
|
458
|
+
const effectsCtx = ctx;
|
|
459
|
+
if (effects.vignette) {
|
|
460
|
+
(0, utils_1.applyVignette)(effectsCtx, effects.vignette.intensity, effects.vignette.size, box.w, box.h);
|
|
461
|
+
}
|
|
462
|
+
if (effects.lensFlare) {
|
|
463
|
+
(0, utils_1.applyLensFlare)(effectsCtx, box.x + effects.lensFlare.x, box.y + effects.lensFlare.y, effects.lensFlare.intensity, box.w, box.h);
|
|
464
|
+
}
|
|
465
|
+
if (effects.chromaticAberration) {
|
|
466
|
+
const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
|
|
467
|
+
const tempCanvas = (0, canvas_1.createCanvas)(box.w, box.h);
|
|
468
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
469
|
+
if (tempCtx) {
|
|
470
|
+
tempCtx.putImageData(imageData, 0, 0);
|
|
471
|
+
(0, utils_1.applyChromaticAberration)(tempCtx, effects.chromaticAberration.intensity, box.w, box.h);
|
|
472
|
+
ctx.clearRect(box.x, box.y, box.w, box.h);
|
|
473
|
+
ctx.drawImage(tempCanvas, box.x, box.y);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (effects.filmGrain) {
|
|
477
|
+
const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
|
|
478
|
+
const tempCanvas = (0, canvas_1.createCanvas)(box.w, box.h);
|
|
479
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
480
|
+
if (tempCtx) {
|
|
481
|
+
tempCtx.putImageData(imageData, 0, 0);
|
|
482
|
+
(0, utils_1.applyFilmGrain)(tempCtx, effects.filmGrain.intensity, box.w, box.h);
|
|
483
|
+
ctx.clearRect(box.x, box.y, box.w, box.h);
|
|
484
|
+
ctx.drawImage(tempCanvas, box.x, box.y);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
ctx.restore();
|
|
488
|
+
}
|
|
268
489
|
// 4) Stroke (independent) — supports gradient or color
|
|
269
490
|
(0, utils_1.applyStroke)(ctx, box, stroke);
|
|
270
491
|
ctx.restore();
|
|
@@ -405,7 +626,7 @@ class ApexPainter {
|
|
|
405
626
|
* @param shapeOptions - Shape-specific options
|
|
406
627
|
*/
|
|
407
628
|
#applyShapeStroke(ctx, shapeType, x, y, width, height, stroke, shapeProps) {
|
|
408
|
-
const { color = "#000", gradient, width: strokeWidth = 2, position = 0, blur = 0, opacity = 1 } = stroke;
|
|
629
|
+
const { color = "#000", gradient, width: strokeWidth = 2, position = 0, blur = 0, opacity = 1, style = 'solid' } = stroke;
|
|
409
630
|
ctx.save();
|
|
410
631
|
if (blur > 0)
|
|
411
632
|
ctx.filter = `blur(${blur}px)`;
|
|
@@ -419,9 +640,17 @@ class ApexPainter {
|
|
|
419
640
|
ctx.strokeStyle = color;
|
|
420
641
|
}
|
|
421
642
|
ctx.lineWidth = strokeWidth;
|
|
643
|
+
// Apply stroke style
|
|
644
|
+
this.#applyShapeStrokeStyle(ctx, style, strokeWidth);
|
|
422
645
|
// Create stroke path
|
|
423
646
|
(0, utils_1.createShapePath)(ctx, shapeType, x, y, width, height, shapeProps);
|
|
424
|
-
|
|
647
|
+
// Handle complex stroke styles
|
|
648
|
+
if (style === 'groove' || style === 'ridge' || style === 'double') {
|
|
649
|
+
this.#applyComplexShapeStroke(ctx, style, strokeWidth, color, gradient);
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
ctx.stroke();
|
|
653
|
+
}
|
|
425
654
|
ctx.filter = "none";
|
|
426
655
|
ctx.globalAlpha = 1;
|
|
427
656
|
ctx.restore();
|
|
@@ -480,14 +709,29 @@ class ApexPainter {
|
|
|
480
709
|
* ], canvasBuffer);
|
|
481
710
|
* ```
|
|
482
711
|
*/
|
|
712
|
+
/**
|
|
713
|
+
* Validates text properties array.
|
|
714
|
+
* @private
|
|
715
|
+
* @param textArray - Text properties to validate
|
|
716
|
+
*/
|
|
717
|
+
#validateTextArray(textArray) {
|
|
718
|
+
const textList = Array.isArray(textArray) ? textArray : [textArray];
|
|
719
|
+
if (textList.length === 0) {
|
|
720
|
+
throw new Error("createText: At least one text object is required.");
|
|
721
|
+
}
|
|
722
|
+
for (const textProps of textList) {
|
|
723
|
+
this.#validateTextProperties(textProps);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
483
726
|
async createText(textArray, canvasBuffer) {
|
|
484
727
|
try {
|
|
728
|
+
// Validate inputs
|
|
729
|
+
if (!canvasBuffer) {
|
|
730
|
+
throw new Error("createText: canvasBuffer is required.");
|
|
731
|
+
}
|
|
732
|
+
this.#validateTextArray(textArray);
|
|
485
733
|
// Ensure textArray is an array
|
|
486
734
|
const textList = Array.isArray(textArray) ? textArray : [textArray];
|
|
487
|
-
// Validate each text object
|
|
488
|
-
for (const textProps of textList) {
|
|
489
|
-
this.#validateTextProperties(textProps);
|
|
490
|
-
}
|
|
491
735
|
// Load existing canvas buffer
|
|
492
736
|
let existingImage;
|
|
493
737
|
if (Buffer.isBuffer(canvasBuffer)) {
|
|
@@ -517,15 +761,37 @@ class ApexPainter {
|
|
|
517
761
|
return canvas.toBuffer("image/png");
|
|
518
762
|
}
|
|
519
763
|
catch (error) {
|
|
520
|
-
|
|
521
|
-
throw new Error(
|
|
764
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
765
|
+
throw new Error(`createText failed: ${errorMessage}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Validates custom line options.
|
|
770
|
+
* @private
|
|
771
|
+
* @param options - Custom options to validate
|
|
772
|
+
*/
|
|
773
|
+
#validateCustomOptions(options) {
|
|
774
|
+
const opts = Array.isArray(options) ? options : [options];
|
|
775
|
+
if (opts.length === 0) {
|
|
776
|
+
throw new Error("createCustom: At least one custom option is required.");
|
|
777
|
+
}
|
|
778
|
+
for (const opt of opts) {
|
|
779
|
+
if (!opt.startCoordinates || typeof opt.startCoordinates.x !== 'number' || typeof opt.startCoordinates.y !== 'number') {
|
|
780
|
+
throw new Error("createCustom: startCoordinates with valid x and y are required.");
|
|
781
|
+
}
|
|
782
|
+
if (!opt.endCoordinates || typeof opt.endCoordinates.x !== 'number' || typeof opt.endCoordinates.y !== 'number') {
|
|
783
|
+
throw new Error("createCustom: endCoordinates with valid x and y are required.");
|
|
784
|
+
}
|
|
522
785
|
}
|
|
523
786
|
}
|
|
524
787
|
async createCustom(options, buffer) {
|
|
525
788
|
try {
|
|
526
|
-
|
|
527
|
-
|
|
789
|
+
// Validate inputs
|
|
790
|
+
if (!buffer) {
|
|
791
|
+
throw new Error("createCustom: buffer is required.");
|
|
528
792
|
}
|
|
793
|
+
this.#validateCustomOptions(options);
|
|
794
|
+
const opts = Array.isArray(options) ? options : [options];
|
|
529
795
|
let existingImage;
|
|
530
796
|
if (Buffer.isBuffer(buffer)) {
|
|
531
797
|
existingImage = await (0, canvas_1.loadImage)(buffer);
|
|
@@ -542,65 +808,68 @@ class ApexPainter {
|
|
|
542
808
|
const canvas = (0, canvas_1.createCanvas)(existingImage.width, existingImage.height);
|
|
543
809
|
const ctx = canvas.getContext("2d");
|
|
544
810
|
ctx.drawImage(existingImage, 0, 0);
|
|
545
|
-
(0, utils_1.customLines)(ctx,
|
|
811
|
+
await (0, utils_1.customLines)(ctx, opts);
|
|
546
812
|
return canvas.toBuffer("image/png");
|
|
547
813
|
}
|
|
548
814
|
catch (error) {
|
|
549
|
-
|
|
550
|
-
throw new Error(
|
|
815
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
816
|
+
throw new Error(`createCustom failed: ${errorMessage}`);
|
|
551
817
|
}
|
|
552
818
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
function createBufferStream() {
|
|
564
|
-
const bufferStream = new stream_1.PassThrough();
|
|
565
|
-
const chunks = [];
|
|
566
|
-
bufferStream.on('data', (chunk) => {
|
|
567
|
-
chunks.push(chunk);
|
|
568
|
-
});
|
|
569
|
-
return {
|
|
570
|
-
...bufferStream,
|
|
571
|
-
getBuffer: function () {
|
|
572
|
-
return Buffer.concat(chunks);
|
|
573
|
-
}
|
|
574
|
-
};
|
|
819
|
+
/**
|
|
820
|
+
* Validates GIF options and frames.
|
|
821
|
+
* @private
|
|
822
|
+
* @param gifFrames - GIF frames to validate
|
|
823
|
+
* @param options - GIF options to validate
|
|
824
|
+
*/
|
|
825
|
+
#validateGIFOptions(gifFrames, options) {
|
|
826
|
+
if (!gifFrames || gifFrames.length === 0) {
|
|
827
|
+
throw new Error("createGIF: At least one frame is required.");
|
|
575
828
|
}
|
|
576
|
-
|
|
577
|
-
if (
|
|
578
|
-
throw new Error("
|
|
829
|
+
for (const frame of gifFrames) {
|
|
830
|
+
if (!frame.background) {
|
|
831
|
+
throw new Error("createGIF: Each frame must have a background property.");
|
|
579
832
|
}
|
|
580
|
-
if (
|
|
581
|
-
throw new Error("
|
|
833
|
+
if (typeof frame.duration !== 'number' || frame.duration < 0) {
|
|
834
|
+
throw new Error("createGIF: Each frame duration must be a non-negative number.");
|
|
582
835
|
}
|
|
583
|
-
|
|
584
|
-
|
|
836
|
+
}
|
|
837
|
+
if (options.outputFormat === "file" && !options.outputFile) {
|
|
838
|
+
throw new Error("createGIF: outputFile is required when outputFormat is 'file'.");
|
|
839
|
+
}
|
|
840
|
+
if (options.repeat !== undefined && (typeof options.repeat !== "number" || options.repeat < 0)) {
|
|
841
|
+
throw new Error("createGIF: repeat must be a non-negative number or undefined.");
|
|
842
|
+
}
|
|
843
|
+
if (options.quality !== undefined && (typeof options.quality !== "number" || options.quality < 1 || options.quality > 20)) {
|
|
844
|
+
throw new Error("createGIF: quality must be a number between 1 and 20 or undefined.");
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async createGIF(gifFrames, options) {
|
|
848
|
+
try {
|
|
849
|
+
this.#validateGIFOptions(gifFrames, options);
|
|
850
|
+
async function resizeImage(image, targetWidth, targetHeight) {
|
|
851
|
+
const canvas = (0, canvas_1.createCanvas)(targetWidth, targetHeight);
|
|
852
|
+
const ctx = canvas.getContext("2d");
|
|
853
|
+
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
|
|
854
|
+
return canvas;
|
|
585
855
|
}
|
|
586
|
-
|
|
587
|
-
|
|
856
|
+
function createOutputStream(outputFile) {
|
|
857
|
+
return fs_1.default.createWriteStream(outputFile);
|
|
588
858
|
}
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
859
|
+
function createBufferStream() {
|
|
860
|
+
const bufferStream = new stream_1.PassThrough();
|
|
861
|
+
const chunks = [];
|
|
862
|
+
bufferStream.on('data', (chunk) => {
|
|
863
|
+
chunks.push(chunk);
|
|
864
|
+
});
|
|
865
|
+
return {
|
|
866
|
+
...bufferStream,
|
|
867
|
+
getBuffer: function () {
|
|
868
|
+
return Buffer.concat(chunks);
|
|
869
|
+
}
|
|
870
|
+
};
|
|
600
871
|
}
|
|
601
|
-
|
|
602
|
-
try {
|
|
603
|
-
validateOptions(options);
|
|
872
|
+
// Validation is done in #validateGIFOptions
|
|
604
873
|
const canvasWidth = options.width || 1200;
|
|
605
874
|
const canvasHeight = options.height || 1200;
|
|
606
875
|
const encoder = new gifencoder_1.default(canvasWidth, canvasHeight);
|
|
@@ -611,6 +880,8 @@ class ApexPainter {
|
|
|
611
880
|
encoder.setQuality(options.quality || 10);
|
|
612
881
|
const canvas = (0, canvas_1.createCanvas)(canvasWidth, canvasHeight);
|
|
613
882
|
const ctx = canvas.getContext("2d");
|
|
883
|
+
if (!ctx)
|
|
884
|
+
throw new Error("Unable to get 2D context");
|
|
614
885
|
for (const frame of gifFrames) {
|
|
615
886
|
const image = await (0, canvas_1.loadImage)(frame.background);
|
|
616
887
|
const resizedImage = await resizeImage(image, canvasWidth, canvasHeight);
|
|
@@ -631,12 +902,13 @@ class ApexPainter {
|
|
|
631
902
|
encoder.finish();
|
|
632
903
|
outputStream.end();
|
|
633
904
|
if (options.outputFormat === "file") {
|
|
634
|
-
await new Promise((resolve) => outputStream.on("finish", resolve));
|
|
905
|
+
await new Promise((resolve) => outputStream.on("finish", () => resolve()));
|
|
635
906
|
}
|
|
636
907
|
else if (options.outputFormat === "base64") {
|
|
637
908
|
if ('getBuffer' in outputStream) {
|
|
638
909
|
return outputStream.getBuffer().toString("base64");
|
|
639
910
|
}
|
|
911
|
+
throw new Error("createGIF: Unable to get buffer for base64 output.");
|
|
640
912
|
}
|
|
641
913
|
else if (options.outputFormat === "attachment") {
|
|
642
914
|
const gifStream = encoder.createReadStream();
|
|
@@ -646,42 +918,204 @@ class ApexPainter {
|
|
|
646
918
|
if ('getBuffer' in outputStream) {
|
|
647
919
|
return outputStream.getBuffer();
|
|
648
920
|
}
|
|
921
|
+
throw new Error("createGIF: Unable to get buffer for buffer output.");
|
|
649
922
|
}
|
|
650
923
|
else {
|
|
651
924
|
throw new Error("Invalid output format. Supported formats are 'file', 'base64', 'attachment', and 'buffer'.");
|
|
652
925
|
}
|
|
653
926
|
}
|
|
654
|
-
catch (
|
|
655
|
-
|
|
656
|
-
throw
|
|
927
|
+
catch (error) {
|
|
928
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
929
|
+
throw new Error(`createGIF failed: ${errorMessage}`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Validates resize options.
|
|
934
|
+
* @private
|
|
935
|
+
* @param options - Resize options to validate
|
|
936
|
+
*/
|
|
937
|
+
#validateResizeOptions(options) {
|
|
938
|
+
if (!options || !options.imagePath) {
|
|
939
|
+
throw new Error("resize: imagePath is required.");
|
|
940
|
+
}
|
|
941
|
+
if (options.size) {
|
|
942
|
+
if (options.size.width !== undefined && (typeof options.size.width !== 'number' || options.size.width <= 0)) {
|
|
943
|
+
throw new Error("resize: size.width must be a positive number.");
|
|
944
|
+
}
|
|
945
|
+
if (options.size.height !== undefined && (typeof options.size.height !== 'number' || options.size.height <= 0)) {
|
|
946
|
+
throw new Error("resize: size.height must be a positive number.");
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (options.quality !== undefined && (typeof options.quality !== 'number' || options.quality < 0 || options.quality > 100)) {
|
|
950
|
+
throw new Error("resize: quality must be a number between 0 and 100.");
|
|
657
951
|
}
|
|
658
952
|
}
|
|
659
953
|
async resize(resizeOptions) {
|
|
660
|
-
|
|
954
|
+
try {
|
|
955
|
+
this.#validateResizeOptions(resizeOptions);
|
|
956
|
+
return await (0, utils_1.resizingImg)(resizeOptions);
|
|
957
|
+
}
|
|
958
|
+
catch (error) {
|
|
959
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
960
|
+
throw new Error(`resize failed: ${errorMessage}`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Validates image converter inputs.
|
|
965
|
+
* @private
|
|
966
|
+
* @param source - Image source to validate
|
|
967
|
+
* @param newExtension - Extension to validate
|
|
968
|
+
*/
|
|
969
|
+
#validateConverterInputs(source, newExtension) {
|
|
970
|
+
if (!source) {
|
|
971
|
+
throw new Error("imgConverter: source is required.");
|
|
972
|
+
}
|
|
973
|
+
if (!newExtension) {
|
|
974
|
+
throw new Error("imgConverter: newExtension is required.");
|
|
975
|
+
}
|
|
976
|
+
const validExtensions = ['jpeg', 'png', 'webp', 'tiff', 'gif', 'avif', 'heif', 'raw', 'pdf', 'svg'];
|
|
977
|
+
if (!validExtensions.includes(newExtension.toLowerCase())) {
|
|
978
|
+
throw new Error(`imgConverter: Invalid extension. Supported: ${validExtensions.join(', ')}`);
|
|
979
|
+
}
|
|
661
980
|
}
|
|
662
981
|
async imgConverter(source, newExtension) {
|
|
663
|
-
|
|
982
|
+
try {
|
|
983
|
+
this.#validateConverterInputs(source, newExtension);
|
|
984
|
+
return await (0, utils_1.converter)(source, newExtension);
|
|
985
|
+
}
|
|
986
|
+
catch (error) {
|
|
987
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
988
|
+
throw new Error(`imgConverter failed: ${errorMessage}`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Validates effects inputs.
|
|
993
|
+
* @private
|
|
994
|
+
* @param source - Image source to validate
|
|
995
|
+
* @param filters - Filters array to validate
|
|
996
|
+
*/
|
|
997
|
+
#validateEffectsInputs(source, filters) {
|
|
998
|
+
if (!source) {
|
|
999
|
+
throw new Error("effects: source is required.");
|
|
1000
|
+
}
|
|
1001
|
+
if (!filters || !Array.isArray(filters) || filters.length === 0) {
|
|
1002
|
+
throw new Error("effects: filters array with at least one filter is required.");
|
|
1003
|
+
}
|
|
664
1004
|
}
|
|
665
1005
|
async effects(source, filters) {
|
|
666
|
-
|
|
1006
|
+
try {
|
|
1007
|
+
this.#validateEffectsInputs(source, filters);
|
|
1008
|
+
return await (0, utils_1.imgEffects)(source, filters);
|
|
1009
|
+
}
|
|
1010
|
+
catch (error) {
|
|
1011
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1012
|
+
throw new Error(`effects failed: ${errorMessage}`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Validates color filter inputs.
|
|
1017
|
+
* @private
|
|
1018
|
+
* @param source - Image source to validate
|
|
1019
|
+
* @param opacity - Opacity to validate
|
|
1020
|
+
*/
|
|
1021
|
+
#validateColorFilterInputs(source, opacity) {
|
|
1022
|
+
if (!source) {
|
|
1023
|
+
throw new Error("colorsFilter: source is required.");
|
|
1024
|
+
}
|
|
1025
|
+
if (opacity !== undefined && (typeof opacity !== 'number' || opacity < 0 || opacity > 1)) {
|
|
1026
|
+
throw new Error("colorsFilter: opacity must be a number between 0 and 1.");
|
|
1027
|
+
}
|
|
667
1028
|
}
|
|
668
1029
|
async colorsFilter(source, filterColor, opacity) {
|
|
669
|
-
|
|
1030
|
+
try {
|
|
1031
|
+
this.#validateColorFilterInputs(source, opacity);
|
|
1032
|
+
return await (0, utils_1.applyColorFilters)(source, filterColor, opacity);
|
|
1033
|
+
}
|
|
1034
|
+
catch (error) {
|
|
1035
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1036
|
+
throw new Error(`colorsFilter failed: ${errorMessage}`);
|
|
1037
|
+
}
|
|
670
1038
|
}
|
|
671
1039
|
async colorAnalysis(source) {
|
|
672
|
-
|
|
1040
|
+
try {
|
|
1041
|
+
if (!source) {
|
|
1042
|
+
throw new Error("colorAnalysis: source is required.");
|
|
1043
|
+
}
|
|
1044
|
+
return await (0, utils_1.detectColors)(source);
|
|
1045
|
+
}
|
|
1046
|
+
catch (error) {
|
|
1047
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1048
|
+
throw new Error(`colorAnalysis failed: ${errorMessage}`);
|
|
1049
|
+
}
|
|
673
1050
|
}
|
|
674
1051
|
async colorsRemover(source, colorToRemove) {
|
|
675
|
-
|
|
1052
|
+
try {
|
|
1053
|
+
if (!source) {
|
|
1054
|
+
throw new Error("colorsRemover: source is required.");
|
|
1055
|
+
}
|
|
1056
|
+
if (!colorToRemove || typeof colorToRemove.red !== 'number' || typeof colorToRemove.green !== 'number' || typeof colorToRemove.blue !== 'number') {
|
|
1057
|
+
throw new Error("colorsRemover: colorToRemove must be an object with red, green, and blue properties (0-255).");
|
|
1058
|
+
}
|
|
1059
|
+
if (colorToRemove.red < 0 || colorToRemove.red > 255 ||
|
|
1060
|
+
colorToRemove.green < 0 || colorToRemove.green > 255 ||
|
|
1061
|
+
colorToRemove.blue < 0 || colorToRemove.blue > 255) {
|
|
1062
|
+
throw new Error("colorsRemover: colorToRemove RGB values must be between 0 and 255.");
|
|
1063
|
+
}
|
|
1064
|
+
return await (0, utils_1.removeColor)(source, colorToRemove);
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1068
|
+
throw new Error(`colorsRemover failed: ${errorMessage}`);
|
|
1069
|
+
}
|
|
676
1070
|
}
|
|
677
1071
|
async removeBackground(imageURL, apiKey) {
|
|
678
|
-
|
|
1072
|
+
try {
|
|
1073
|
+
if (!imageURL) {
|
|
1074
|
+
throw new Error("removeBackground: imageURL is required.");
|
|
1075
|
+
}
|
|
1076
|
+
if (!apiKey) {
|
|
1077
|
+
throw new Error("removeBackground: apiKey is required.");
|
|
1078
|
+
}
|
|
1079
|
+
return await (0, utils_1.bgRemoval)(imageURL, apiKey);
|
|
1080
|
+
}
|
|
1081
|
+
catch (error) {
|
|
1082
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1083
|
+
throw new Error(`removeBackground failed: ${errorMessage}`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Validates blend inputs.
|
|
1088
|
+
* @private
|
|
1089
|
+
* @param layers - Layers to validate
|
|
1090
|
+
* @param baseImageBuffer - Base image buffer to validate
|
|
1091
|
+
*/
|
|
1092
|
+
#validateBlendInputs(layers, baseImageBuffer) {
|
|
1093
|
+
if (!baseImageBuffer || !Buffer.isBuffer(baseImageBuffer)) {
|
|
1094
|
+
throw new Error("blend: baseImageBuffer must be a valid Buffer.");
|
|
1095
|
+
}
|
|
1096
|
+
if (!layers || !Array.isArray(layers) || layers.length === 0) {
|
|
1097
|
+
throw new Error("blend: layers array with at least one layer is required.");
|
|
1098
|
+
}
|
|
1099
|
+
for (const layer of layers) {
|
|
1100
|
+
if (!layer.image) {
|
|
1101
|
+
throw new Error("blend: Each layer must have an image property.");
|
|
1102
|
+
}
|
|
1103
|
+
if (!layer.blendMode) {
|
|
1104
|
+
throw new Error("blend: Each layer must have a blendMode property.");
|
|
1105
|
+
}
|
|
1106
|
+
if (layer.opacity !== undefined && (typeof layer.opacity !== 'number' || layer.opacity < 0 || layer.opacity > 1)) {
|
|
1107
|
+
throw new Error("blend: Layer opacity must be a number between 0 and 1.");
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
679
1110
|
}
|
|
680
1111
|
async blend(layers, baseImageBuffer, defaultBlendMode = 'source-over') {
|
|
681
1112
|
try {
|
|
1113
|
+
this.#validateBlendInputs(layers, baseImageBuffer);
|
|
682
1114
|
const baseImage = await (0, canvas_1.loadImage)(baseImageBuffer);
|
|
683
1115
|
const canvas = (0, canvas_1.createCanvas)(baseImage.width, baseImage.height);
|
|
684
1116
|
const ctx = canvas.getContext('2d');
|
|
1117
|
+
if (!ctx)
|
|
1118
|
+
throw new Error("Unable to get 2D context");
|
|
685
1119
|
ctx.globalCompositeOperation = defaultBlendMode;
|
|
686
1120
|
ctx.drawImage(baseImage, 0, 0);
|
|
687
1121
|
for (const layer of layers) {
|
|
@@ -695,322 +1129,884 @@ class ApexPainter {
|
|
|
695
1129
|
return canvas.toBuffer('image/png');
|
|
696
1130
|
}
|
|
697
1131
|
catch (error) {
|
|
698
|
-
|
|
699
|
-
throw new Error(
|
|
1132
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1133
|
+
throw new Error(`blend failed: ${errorMessage}`);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Validates chart inputs.
|
|
1138
|
+
* @private
|
|
1139
|
+
* @param data - Chart data to validate
|
|
1140
|
+
* @param type - Chart type configuration to validate
|
|
1141
|
+
*/
|
|
1142
|
+
#validateChartInputs(data, type) {
|
|
1143
|
+
if (!data || typeof data !== 'object' || Object.keys(data).length === 0) {
|
|
1144
|
+
throw new Error("createChart: data object with datasets is required.");
|
|
1145
|
+
}
|
|
1146
|
+
if (!type || typeof type !== 'object') {
|
|
1147
|
+
throw new Error("createChart: type configuration object is required.");
|
|
1148
|
+
}
|
|
1149
|
+
if (!type.chartType || typeof type.chartType !== 'string') {
|
|
1150
|
+
throw new Error("createChart: type.chartType must be a string.");
|
|
1151
|
+
}
|
|
1152
|
+
if (typeof type.chartNumber !== 'number' || type.chartNumber < 1) {
|
|
1153
|
+
throw new Error("createChart: type.chartNumber must be a positive number.");
|
|
1154
|
+
}
|
|
1155
|
+
const validChartTypes = ['bar', 'line', 'pie'];
|
|
1156
|
+
if (!validChartTypes.includes(type.chartType.toLowerCase())) {
|
|
1157
|
+
throw new Error(`createChart: Invalid chartType. Supported: ${validChartTypes.join(', ')}`);
|
|
700
1158
|
}
|
|
701
1159
|
}
|
|
702
1160
|
async createChart(data, type) {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1161
|
+
try {
|
|
1162
|
+
this.#validateChartInputs(data, type);
|
|
1163
|
+
const { chartType, chartNumber } = type;
|
|
1164
|
+
switch (chartType.toLowerCase()) {
|
|
1165
|
+
case 'bar':
|
|
1166
|
+
switch (chartNumber) {
|
|
1167
|
+
case 1:
|
|
1168
|
+
const barResult = await (0, utils_1.verticalBarChart)(data);
|
|
1169
|
+
if (!barResult) {
|
|
1170
|
+
throw new Error("createChart: Failed to generate bar chart.");
|
|
1171
|
+
}
|
|
1172
|
+
return barResult;
|
|
1173
|
+
case 2:
|
|
1174
|
+
throw new Error('Type 2 is still under development.');
|
|
1175
|
+
default:
|
|
1176
|
+
throw new Error('Invalid chart number for chart type "bar".');
|
|
1177
|
+
}
|
|
1178
|
+
case 'line':
|
|
1179
|
+
switch (chartNumber) {
|
|
1180
|
+
case 1:
|
|
1181
|
+
// LineChart expects DataPoint[][] where DataPoint has { label: string; y: number }
|
|
1182
|
+
// Type assertion needed because there are two different DataPoint interfaces
|
|
1183
|
+
return await (0, utils_1.lineChart)(data);
|
|
1184
|
+
case 2:
|
|
1185
|
+
throw new Error('Type 2 is still under development.');
|
|
1186
|
+
default:
|
|
1187
|
+
throw new Error('Invalid chart number for chart type "line".');
|
|
1188
|
+
}
|
|
1189
|
+
case 'pie':
|
|
1190
|
+
switch (chartNumber) {
|
|
1191
|
+
case 1:
|
|
1192
|
+
return await (0, utils_1.pieChart)(data);
|
|
1193
|
+
case 2:
|
|
1194
|
+
throw new Error('Type 2 is still under development.');
|
|
1195
|
+
default:
|
|
1196
|
+
throw new Error('Invalid chart number for chart type "pie".');
|
|
1197
|
+
}
|
|
1198
|
+
default:
|
|
1199
|
+
throw new Error(`Unsupported chart type "${chartType}".`);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
catch (error) {
|
|
1203
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1204
|
+
throw new Error(`createChart failed: ${errorMessage}`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Validates crop options.
|
|
1209
|
+
* @private
|
|
1210
|
+
* @param options - Crop options to validate
|
|
1211
|
+
*/
|
|
1212
|
+
#validateCropOptions(options) {
|
|
1213
|
+
if (!options) {
|
|
1214
|
+
throw new Error("cropImage: options object is required.");
|
|
1215
|
+
}
|
|
1216
|
+
if (!options.imageSource) {
|
|
1217
|
+
throw new Error("cropImage: imageSource is required.");
|
|
1218
|
+
}
|
|
1219
|
+
if (!options.coordinates || !Array.isArray(options.coordinates) || options.coordinates.length < 3) {
|
|
1220
|
+
throw new Error("cropImage: coordinates array with at least 3 points is required.");
|
|
1221
|
+
}
|
|
1222
|
+
if (options.crop !== 'inner' && options.crop !== 'outer') {
|
|
1223
|
+
throw new Error("cropImage: crop must be either 'inner' or 'outer'.");
|
|
740
1224
|
}
|
|
741
1225
|
}
|
|
742
1226
|
async cropImage(options) {
|
|
743
1227
|
try {
|
|
744
|
-
|
|
745
|
-
throw new Error('The "imageSource" option is needed. Please provide the path to the image to crop.');
|
|
746
|
-
if (!options.coordinates || options.coordinates.length < 3)
|
|
747
|
-
throw new Error('The "coordinates" option is needed. Please provide coordinates to crop the image.');
|
|
1228
|
+
this.#validateCropOptions(options);
|
|
748
1229
|
if (options.crop === 'outer') {
|
|
749
1230
|
return await (0, utils_1.cropOuter)(options);
|
|
750
1231
|
}
|
|
751
|
-
else
|
|
1232
|
+
else {
|
|
752
1233
|
return await (0, utils_1.cropInner)(options);
|
|
753
1234
|
}
|
|
1235
|
+
}
|
|
1236
|
+
catch (error) {
|
|
1237
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1238
|
+
throw new Error(`cropImage failed: ${errorMessage}`);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Extracts a single frame from a video
|
|
1243
|
+
* @private
|
|
1244
|
+
* @param videoSource - Video source (path, URL, or Buffer)
|
|
1245
|
+
* @param frameNumber - Frame number to extract (default: 0)
|
|
1246
|
+
* @returns Buffer containing the frame image
|
|
1247
|
+
*/
|
|
1248
|
+
async #extractVideoFrame(videoSource, frameNumber = 0) {
|
|
1249
|
+
try {
|
|
1250
|
+
const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
|
|
1251
|
+
if (!fs_1.default.existsSync(frameDir)) {
|
|
1252
|
+
fs_1.default.mkdirSync(frameDir, { recursive: true });
|
|
1253
|
+
}
|
|
1254
|
+
const tempVideoPath = path_1.default.join(frameDir, `temp-video-${Date.now()}.mp4`);
|
|
1255
|
+
const frameOutputPath = path_1.default.join(frameDir, `frame-${Date.now()}.jpg`);
|
|
1256
|
+
// Handle video source
|
|
1257
|
+
if (Buffer.isBuffer(videoSource)) {
|
|
1258
|
+
fs_1.default.writeFileSync(tempVideoPath, videoSource);
|
|
1259
|
+
}
|
|
1260
|
+
else if (typeof videoSource === 'string' && videoSource.startsWith('http')) {
|
|
1261
|
+
const response = await (0, axios_1.default)({
|
|
1262
|
+
method: 'get',
|
|
1263
|
+
url: videoSource,
|
|
1264
|
+
responseType: 'arraybuffer'
|
|
1265
|
+
});
|
|
1266
|
+
fs_1.default.writeFileSync(tempVideoPath, Buffer.from(response.data));
|
|
1267
|
+
}
|
|
754
1268
|
else {
|
|
755
|
-
|
|
1269
|
+
// Local file path
|
|
1270
|
+
if (!fs_1.default.existsSync(videoSource)) {
|
|
1271
|
+
throw new Error(`Video file not found: ${videoSource}`);
|
|
1272
|
+
}
|
|
1273
|
+
// Use the existing path
|
|
1274
|
+
return await new Promise((resolve, reject) => {
|
|
1275
|
+
(0, fluent_ffmpeg_1.default)(videoSource)
|
|
1276
|
+
.seekInput(frameNumber / 1000) // Convert frame to seconds (approximate)
|
|
1277
|
+
.frames(1)
|
|
1278
|
+
.output(frameOutputPath)
|
|
1279
|
+
.on('end', () => {
|
|
1280
|
+
try {
|
|
1281
|
+
const buffer = fs_1.default.readFileSync(frameOutputPath);
|
|
1282
|
+
// Cleanup
|
|
1283
|
+
if (fs_1.default.existsSync(frameOutputPath))
|
|
1284
|
+
fs_1.default.unlinkSync(frameOutputPath);
|
|
1285
|
+
resolve(buffer);
|
|
1286
|
+
}
|
|
1287
|
+
catch (e) {
|
|
1288
|
+
resolve(null);
|
|
1289
|
+
}
|
|
1290
|
+
})
|
|
1291
|
+
.on('error', (err) => {
|
|
1292
|
+
reject(err);
|
|
1293
|
+
})
|
|
1294
|
+
.run();
|
|
1295
|
+
});
|
|
756
1296
|
}
|
|
1297
|
+
// Extract frame from temp video
|
|
1298
|
+
return await new Promise((resolve, reject) => {
|
|
1299
|
+
(0, fluent_ffmpeg_1.default)(tempVideoPath)
|
|
1300
|
+
.seekInput(frameNumber / 1000)
|
|
1301
|
+
.frames(1)
|
|
1302
|
+
.output(frameOutputPath)
|
|
1303
|
+
.on('end', () => {
|
|
1304
|
+
try {
|
|
1305
|
+
const buffer = fs_1.default.readFileSync(frameOutputPath);
|
|
1306
|
+
// Cleanup
|
|
1307
|
+
if (fs_1.default.existsSync(tempVideoPath))
|
|
1308
|
+
fs_1.default.unlinkSync(tempVideoPath);
|
|
1309
|
+
if (fs_1.default.existsSync(frameOutputPath))
|
|
1310
|
+
fs_1.default.unlinkSync(frameOutputPath);
|
|
1311
|
+
resolve(buffer);
|
|
1312
|
+
}
|
|
1313
|
+
catch (e) {
|
|
1314
|
+
resolve(null);
|
|
1315
|
+
}
|
|
1316
|
+
})
|
|
1317
|
+
.on('error', (err) => {
|
|
1318
|
+
// Cleanup on error
|
|
1319
|
+
if (fs_1.default.existsSync(tempVideoPath))
|
|
1320
|
+
fs_1.default.unlinkSync(tempVideoPath);
|
|
1321
|
+
if (fs_1.default.existsSync(frameOutputPath))
|
|
1322
|
+
fs_1.default.unlinkSync(frameOutputPath);
|
|
1323
|
+
reject(err);
|
|
1324
|
+
})
|
|
1325
|
+
.run();
|
|
1326
|
+
});
|
|
757
1327
|
}
|
|
758
1328
|
catch (error) {
|
|
759
|
-
console.error('
|
|
760
|
-
|
|
1329
|
+
console.error('Error extracting video frame:', error);
|
|
1330
|
+
return null;
|
|
761
1331
|
}
|
|
762
1332
|
}
|
|
763
|
-
|
|
1333
|
+
/**
|
|
1334
|
+
* Validates extract frames inputs.
|
|
1335
|
+
* @private
|
|
1336
|
+
* @param videoSource - Video source to validate
|
|
1337
|
+
* @param options - Extract frames options to validate
|
|
1338
|
+
*/
|
|
1339
|
+
#validateExtractFramesInputs(videoSource, options) {
|
|
1340
|
+
if (!videoSource) {
|
|
1341
|
+
throw new Error("extractFrames: videoSource is required.");
|
|
1342
|
+
}
|
|
1343
|
+
if (!options || typeof options !== 'object') {
|
|
1344
|
+
throw new Error("extractFrames: options object is required.");
|
|
1345
|
+
}
|
|
1346
|
+
if (typeof options.interval !== 'number' || options.interval <= 0) {
|
|
1347
|
+
throw new Error("extractFrames: options.interval must be a positive number (milliseconds).");
|
|
1348
|
+
}
|
|
1349
|
+
if (options.outputFormat && !['jpg', 'png'].includes(options.outputFormat)) {
|
|
1350
|
+
throw new Error("extractFrames: outputFormat must be 'jpg' or 'png'.");
|
|
1351
|
+
}
|
|
764
1352
|
}
|
|
765
1353
|
async extractFrames(videoSource, options) {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
if (typeof duration !== "number") {
|
|
811
|
-
return reject(new Error("Video duration not found in metadata."));
|
|
812
|
-
}
|
|
813
|
-
const totalFrames = Math.floor(duration * 1000 / options.interval);
|
|
814
|
-
for (let i = 0; i < totalFrames; i++) {
|
|
815
|
-
if (options.frameSelection && (i < (options.frameSelection.start || 0) || i > (options.frameSelection.end || totalFrames - 1))) {
|
|
816
|
-
continue;
|
|
1354
|
+
try {
|
|
1355
|
+
this.#validateExtractFramesInputs(videoSource, options);
|
|
1356
|
+
const frames = [];
|
|
1357
|
+
const frameDir = path_1.default.join(__dirname, 'frames');
|
|
1358
|
+
if (!fs_1.default.existsSync(frameDir)) {
|
|
1359
|
+
fs_1.default.mkdirSync(frameDir);
|
|
1360
|
+
}
|
|
1361
|
+
const videoPath = typeof videoSource === 'string' ? videoSource : path_1.default.join(frameDir, 'temp-video.mp4');
|
|
1362
|
+
if (Buffer.isBuffer(videoSource)) {
|
|
1363
|
+
fs_1.default.writeFileSync(videoPath, videoSource);
|
|
1364
|
+
}
|
|
1365
|
+
else if (videoSource.startsWith('http')) {
|
|
1366
|
+
await (0, axios_1.default)({
|
|
1367
|
+
method: 'get',
|
|
1368
|
+
url: videoSource,
|
|
1369
|
+
responseType: 'arraybuffer'
|
|
1370
|
+
})
|
|
1371
|
+
.then((response) => {
|
|
1372
|
+
fs_1.default.writeFileSync(videoPath, response.data);
|
|
1373
|
+
})
|
|
1374
|
+
.catch(err => {
|
|
1375
|
+
throw new Error(`Error downloading video: ${err.message}`);
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
else if (!fs_1.default.existsSync(videoPath)) {
|
|
1379
|
+
throw new Error("Video file not found at specified path.");
|
|
1380
|
+
}
|
|
1381
|
+
function processVideoExtraction(videoPath, frames, options, resolve, reject) {
|
|
1382
|
+
const outputFormat = options.outputFormat || 'jpg';
|
|
1383
|
+
const outputFileTemplate = `frame-%03d.${outputFormat}`;
|
|
1384
|
+
(0, fluent_ffmpeg_1.default)(videoPath)
|
|
1385
|
+
.on('end', () => {
|
|
1386
|
+
console.log('Frames extracted successfully.');
|
|
1387
|
+
resolve(frames);
|
|
1388
|
+
})
|
|
1389
|
+
.on('error', (err) => {
|
|
1390
|
+
console.error('Error extracting frames:', err.message);
|
|
1391
|
+
reject(err);
|
|
1392
|
+
})
|
|
1393
|
+
.outputOptions([`-vf fps=1/${options.interval / 1000}`, `-q:v 2`])
|
|
1394
|
+
.saveToFile(path_1.default.join(frameDir, outputFileTemplate));
|
|
1395
|
+
fluent_ffmpeg_1.default.ffprobe(videoPath, (err, metadata) => {
|
|
1396
|
+
if (err) {
|
|
1397
|
+
return reject(err);
|
|
817
1398
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
}
|
|
822
|
-
|
|
1399
|
+
const duration = metadata?.format?.duration;
|
|
1400
|
+
if (typeof duration !== "number") {
|
|
1401
|
+
return reject(new Error("Video duration not found in metadata."));
|
|
1402
|
+
}
|
|
1403
|
+
const totalFrames = Math.floor(duration * 1000 / options.interval);
|
|
1404
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
1405
|
+
if (options.frameSelection && (i < (options.frameSelection.start || 0) || i > (options.frameSelection.end || totalFrames - 1))) {
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
frames.push({
|
|
1409
|
+
source: path_1.default.join(frameDir, `frame-${String(i).padStart(3, '0')}.${outputFormat}`),
|
|
1410
|
+
isRemote: false
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
return new Promise((resolve, reject) => {
|
|
1416
|
+
processVideoExtraction(videoPath, frames, options, resolve, reject);
|
|
823
1417
|
});
|
|
824
1418
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1419
|
+
catch (error) {
|
|
1420
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1421
|
+
throw new Error(`extractFrames failed: ${errorMessage}`);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Validates masking inputs.
|
|
1426
|
+
* @private
|
|
1427
|
+
* @param source - Source image to validate
|
|
1428
|
+
* @param maskSource - Mask image to validate
|
|
1429
|
+
* @param options - Mask options to validate
|
|
1430
|
+
*/
|
|
1431
|
+
#validateMaskingInputs(source, maskSource, options) {
|
|
1432
|
+
if (!source) {
|
|
1433
|
+
throw new Error("masking: source is required.");
|
|
1434
|
+
}
|
|
1435
|
+
if (!maskSource) {
|
|
1436
|
+
throw new Error("masking: maskSource is required.");
|
|
1437
|
+
}
|
|
1438
|
+
if (options.type && !['alpha', 'grayscale', 'color'].includes(options.type)) {
|
|
1439
|
+
throw new Error("masking: type must be 'alpha', 'grayscale', or 'color'.");
|
|
1440
|
+
}
|
|
1441
|
+
if (options.type === 'color' && !options.colorKey) {
|
|
1442
|
+
throw new Error("masking: colorKey is required when type is 'color'.");
|
|
1443
|
+
}
|
|
1444
|
+
if (options.threshold !== undefined && (typeof options.threshold !== 'number' || options.threshold < 0 || options.threshold > 255)) {
|
|
1445
|
+
throw new Error("masking: threshold must be a number between 0 and 255.");
|
|
1446
|
+
}
|
|
828
1447
|
}
|
|
829
1448
|
async masking(source, maskSource, options = { type: "alpha" }) {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
maskData.data[i
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1449
|
+
try {
|
|
1450
|
+
this.#validateMaskingInputs(source, maskSource, options);
|
|
1451
|
+
const img = await (0, canvas_1.loadImage)(source);
|
|
1452
|
+
const mask = await (0, canvas_1.loadImage)(maskSource);
|
|
1453
|
+
const canvas = (0, canvas_1.createCanvas)(img.width, img.height);
|
|
1454
|
+
const ctx = canvas.getContext("2d");
|
|
1455
|
+
ctx.drawImage(img, 0, 0, img.width, img.height);
|
|
1456
|
+
const maskCanvas = (0, canvas_1.createCanvas)(img.width, img.height);
|
|
1457
|
+
const maskCtx = maskCanvas.getContext("2d");
|
|
1458
|
+
maskCtx.drawImage(mask, 0, 0, img.width, img.height);
|
|
1459
|
+
const maskData = maskCtx.getImageData(0, 0, img.width, img.height);
|
|
1460
|
+
const imgData = ctx.getImageData(0, 0, img.width, img.height);
|
|
1461
|
+
for (let i = 0; i < maskData.data.length; i += 4) {
|
|
1462
|
+
let alphaValue = 255;
|
|
1463
|
+
if (options.type === "grayscale") {
|
|
1464
|
+
const grayscale = maskData.data[i] * 0.3 + maskData.data[i + 1] * 0.59 + maskData.data[i + 2] * 0.11;
|
|
1465
|
+
alphaValue = grayscale >= (options.threshold ?? 128) ? 255 : 0;
|
|
1466
|
+
}
|
|
1467
|
+
else if (options.type === "alpha") {
|
|
1468
|
+
alphaValue = maskData.data[i + 3];
|
|
1469
|
+
}
|
|
1470
|
+
else if (options.type === "color" && options.colorKey) {
|
|
1471
|
+
const colorMatch = maskData.data[i] === parseInt(options.colorKey.slice(1, 3), 16) &&
|
|
1472
|
+
maskData.data[i + 1] === parseInt(options.colorKey.slice(3, 5), 16) &&
|
|
1473
|
+
maskData.data[i + 2] === parseInt(options.colorKey.slice(5, 7), 16);
|
|
1474
|
+
alphaValue = colorMatch ? 0 : 255;
|
|
1475
|
+
}
|
|
1476
|
+
if (options.invert)
|
|
1477
|
+
alphaValue = 255 - alphaValue;
|
|
1478
|
+
imgData.data[i + 3] = alphaValue;
|
|
1479
|
+
}
|
|
1480
|
+
ctx.putImageData(imgData, 0, 0);
|
|
1481
|
+
return canvas.toBuffer("image/png");
|
|
1482
|
+
}
|
|
1483
|
+
catch (error) {
|
|
1484
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1485
|
+
throw new Error(`masking failed: ${errorMessage}`);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Validates gradient blend inputs.
|
|
1490
|
+
* @private
|
|
1491
|
+
* @param source - Source image to validate
|
|
1492
|
+
* @param options - Blend options to validate
|
|
1493
|
+
*/
|
|
1494
|
+
#validateGradientBlendInputs(source, options) {
|
|
1495
|
+
if (!source) {
|
|
1496
|
+
throw new Error("gradientBlend: source is required.");
|
|
1497
|
+
}
|
|
1498
|
+
if (!options || typeof options !== 'object') {
|
|
1499
|
+
throw new Error("gradientBlend: options object is required.");
|
|
1500
|
+
}
|
|
1501
|
+
if (!options.colors || !Array.isArray(options.colors) || options.colors.length === 0) {
|
|
1502
|
+
throw new Error("gradientBlend: options.colors array with at least one color stop is required.");
|
|
1503
|
+
}
|
|
1504
|
+
if (options.type && !['linear', 'radial', 'conic'].includes(options.type)) {
|
|
1505
|
+
throw new Error("gradientBlend: type must be 'linear', 'radial', or 'conic'.");
|
|
1506
|
+
}
|
|
1507
|
+
for (const colorStop of options.colors) {
|
|
1508
|
+
if (typeof colorStop.stop !== 'number' || colorStop.stop < 0 || colorStop.stop > 1) {
|
|
1509
|
+
throw new Error("gradientBlend: Each color stop must have a stop value between 0 and 1.");
|
|
1510
|
+
}
|
|
1511
|
+
if (!colorStop.color || typeof colorStop.color !== 'string') {
|
|
1512
|
+
throw new Error("gradientBlend: Each color stop must have a valid color string.");
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
861
1515
|
}
|
|
862
1516
|
async gradientBlend(source, options) {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1517
|
+
try {
|
|
1518
|
+
this.#validateGradientBlendInputs(source, options);
|
|
1519
|
+
const img = await (0, canvas_1.loadImage)(source);
|
|
1520
|
+
const canvas = (0, canvas_1.createCanvas)(img.width, img.height);
|
|
1521
|
+
const ctx = canvas.getContext("2d");
|
|
1522
|
+
if (!ctx)
|
|
1523
|
+
throw new Error("Unable to get 2D context");
|
|
1524
|
+
ctx.drawImage(img, 0, 0, img.width, img.height);
|
|
1525
|
+
let gradient;
|
|
1526
|
+
if (options.type === "linear") {
|
|
1527
|
+
const angle = options.angle ?? 0;
|
|
1528
|
+
const radians = (angle * Math.PI) / 180;
|
|
1529
|
+
const x1 = img.width / 2 - (Math.cos(radians) * img.width) / 2;
|
|
1530
|
+
const y1 = img.height / 2 - (Math.sin(radians) * img.height) / 2;
|
|
1531
|
+
const x2 = img.width / 2 + (Math.cos(radians) * img.width) / 2;
|
|
1532
|
+
const y2 = img.height / 2 + (Math.sin(radians) * img.height) / 2;
|
|
1533
|
+
gradient = ctx.createLinearGradient(x1, y1, x2, y2);
|
|
1534
|
+
}
|
|
1535
|
+
else if (options.type === "radial") {
|
|
1536
|
+
gradient = ctx.createRadialGradient(img.width / 2, img.height / 2, 0, img.width / 2, img.height / 2, Math.max(img.width, img.height));
|
|
1537
|
+
}
|
|
1538
|
+
else {
|
|
1539
|
+
gradient = ctx.createConicGradient(Math.PI, img.width / 2, img.height / 2);
|
|
1540
|
+
}
|
|
1541
|
+
options.colors.forEach(({ stop, color }) => gradient.addColorStop(stop, color));
|
|
1542
|
+
ctx.fillStyle = gradient;
|
|
1543
|
+
ctx.globalCompositeOperation = options.blendMode ?? "multiply";
|
|
1544
|
+
ctx.fillRect(0, 0, img.width, img.height);
|
|
1545
|
+
if (options.maskSource) {
|
|
1546
|
+
const mask = await (0, canvas_1.loadImage)(options.maskSource);
|
|
1547
|
+
ctx.globalCompositeOperation = "destination-in";
|
|
1548
|
+
ctx.drawImage(mask, 0, 0, img.width, img.height);
|
|
1549
|
+
}
|
|
1550
|
+
ctx.globalCompositeOperation = "source-over";
|
|
1551
|
+
return canvas.toBuffer("image/png");
|
|
879
1552
|
}
|
|
880
|
-
|
|
881
|
-
|
|
1553
|
+
catch (error) {
|
|
1554
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1555
|
+
throw new Error(`gradientBlend failed: ${errorMessage}`);
|
|
882
1556
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Validates animate inputs.
|
|
1560
|
+
* @private
|
|
1561
|
+
* @param frames - Animation frames to validate
|
|
1562
|
+
* @param defaultDuration - Default duration to validate
|
|
1563
|
+
* @param defaultWidth - Default width to validate
|
|
1564
|
+
* @param defaultHeight - Default height to validate
|
|
1565
|
+
* @param options - Animation options to validate
|
|
1566
|
+
*/
|
|
1567
|
+
#validateAnimateInputs(frames, defaultDuration, defaultWidth, defaultHeight, options) {
|
|
1568
|
+
if (!frames || !Array.isArray(frames) || frames.length === 0) {
|
|
1569
|
+
throw new Error("animate: frames array with at least one frame is required.");
|
|
1570
|
+
}
|
|
1571
|
+
if (typeof defaultDuration !== 'number' || defaultDuration < 0) {
|
|
1572
|
+
throw new Error("animate: defaultDuration must be a non-negative number.");
|
|
1573
|
+
}
|
|
1574
|
+
if (typeof defaultWidth !== 'number' || defaultWidth <= 0) {
|
|
1575
|
+
throw new Error("animate: defaultWidth must be a positive number.");
|
|
1576
|
+
}
|
|
1577
|
+
if (typeof defaultHeight !== 'number' || defaultHeight <= 0) {
|
|
1578
|
+
throw new Error("animate: defaultHeight must be a positive number.");
|
|
1579
|
+
}
|
|
1580
|
+
if (options?.gif && !options.gifPath) {
|
|
1581
|
+
throw new Error("animate: gifPath is required when gif is enabled.");
|
|
891
1582
|
}
|
|
892
|
-
ctx.globalCompositeOperation = "source-over";
|
|
893
|
-
return canvas.toBuffer("image/png");
|
|
894
1583
|
}
|
|
895
1584
|
async animate(frames, defaultDuration, defaultWidth = 800, defaultHeight = 600, options) {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
const frame = frames[i];
|
|
915
|
-
const width = frame.width || defaultWidth;
|
|
916
|
-
const height = frame.height || defaultHeight;
|
|
917
|
-
const canvas = (0, canvas_1.createCanvas)(width, height);
|
|
918
|
-
const ctx = canvas.getContext('2d');
|
|
919
|
-
if (!isNode) {
|
|
920
|
-
canvas.width = width;
|
|
921
|
-
canvas.height = height;
|
|
922
|
-
document.body.appendChild(canvas);
|
|
923
|
-
}
|
|
924
|
-
ctx.clearRect(0, 0, width, height);
|
|
925
|
-
if (frame.transformations) {
|
|
926
|
-
const { scaleX = 1, scaleY = 1, rotate = 0, translateX = 0, translateY = 0 } = frame.transformations;
|
|
927
|
-
ctx.save();
|
|
928
|
-
ctx.translate(translateX, translateY);
|
|
929
|
-
ctx.rotate((rotate * Math.PI) / 180);
|
|
930
|
-
ctx.scale(scaleX, scaleY);
|
|
931
|
-
}
|
|
932
|
-
let fillStyle = null;
|
|
933
|
-
if (frame.gradient) {
|
|
934
|
-
const { type, startX, startY, endX, endY, startRadius, endRadius, colors } = frame.gradient;
|
|
935
|
-
let gradient = null;
|
|
936
|
-
if (type === 'linear') {
|
|
937
|
-
gradient = ctx.createLinearGradient(startX || 0, startY || 0, endX || width, endY || height);
|
|
938
|
-
}
|
|
939
|
-
else if (type === 'radial') {
|
|
940
|
-
gradient = ctx.createRadialGradient(startX || width / 2, startY || height / 2, startRadius || 0, endX || width / 2, endY || height / 2, endRadius || Math.max(width, height));
|
|
941
|
-
}
|
|
942
|
-
colors.forEach((colorStop) => {
|
|
943
|
-
if (gradient)
|
|
944
|
-
gradient.addColorStop(colorStop.stop, colorStop.color);
|
|
945
|
-
});
|
|
946
|
-
fillStyle = gradient;
|
|
1585
|
+
try {
|
|
1586
|
+
this.#validateAnimateInputs(frames, defaultDuration, defaultWidth, defaultHeight, options);
|
|
1587
|
+
const buffers = [];
|
|
1588
|
+
const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
1589
|
+
if (options?.onStart)
|
|
1590
|
+
options.onStart();
|
|
1591
|
+
let encoder = null;
|
|
1592
|
+
let gifStream = null;
|
|
1593
|
+
if (options?.gif) {
|
|
1594
|
+
if (!options.gifPath) {
|
|
1595
|
+
throw new Error("animate: gifPath is required when gif is enabled.");
|
|
1596
|
+
}
|
|
1597
|
+
encoder = new gifencoder_1.default(defaultWidth, defaultHeight);
|
|
1598
|
+
gifStream = fs_1.default.createWriteStream(options.gifPath);
|
|
1599
|
+
encoder.createReadStream().pipe(gifStream);
|
|
1600
|
+
encoder.start();
|
|
1601
|
+
encoder.setRepeat(0);
|
|
1602
|
+
encoder.setQuality(10);
|
|
947
1603
|
}
|
|
948
|
-
|
|
949
|
-
const
|
|
950
|
-
const
|
|
951
|
-
|
|
1604
|
+
for (let i = 0; i < frames.length; i++) {
|
|
1605
|
+
const frame = frames[i];
|
|
1606
|
+
const width = frame.width || defaultWidth;
|
|
1607
|
+
const height = frame.height || defaultHeight;
|
|
1608
|
+
const canvas = (0, canvas_1.createCanvas)(width, height);
|
|
1609
|
+
const ctx = canvas.getContext('2d');
|
|
1610
|
+
if (!isNode) {
|
|
1611
|
+
canvas.width = width;
|
|
1612
|
+
canvas.height = height;
|
|
1613
|
+
document.body.appendChild(canvas);
|
|
1614
|
+
}
|
|
1615
|
+
ctx.clearRect(0, 0, width, height);
|
|
1616
|
+
if (frame.transformations) {
|
|
1617
|
+
const { scaleX = 1, scaleY = 1, rotate = 0, translateX = 0, translateY = 0 } = frame.transformations;
|
|
1618
|
+
ctx.save();
|
|
1619
|
+
ctx.translate(translateX, translateY);
|
|
1620
|
+
ctx.rotate((rotate * Math.PI) / 180);
|
|
1621
|
+
ctx.scale(scaleX, scaleY);
|
|
1622
|
+
}
|
|
1623
|
+
let fillStyle = null;
|
|
1624
|
+
if (frame.gradient) {
|
|
1625
|
+
const { type, startX, startY, endX, endY, startRadius, endRadius, colors } = frame.gradient;
|
|
1626
|
+
let gradient = null;
|
|
1627
|
+
if (type === 'linear') {
|
|
1628
|
+
gradient = ctx.createLinearGradient(startX || 0, startY || 0, endX || width, endY || height);
|
|
1629
|
+
}
|
|
1630
|
+
else if (type === 'radial') {
|
|
1631
|
+
gradient = ctx.createRadialGradient(startX || width / 2, startY || height / 2, startRadius || 0, endX || width / 2, endY || height / 2, endRadius || Math.max(width, height));
|
|
1632
|
+
}
|
|
1633
|
+
colors.forEach((colorStop) => {
|
|
1634
|
+
if (gradient)
|
|
1635
|
+
gradient.addColorStop(colorStop.stop, colorStop.color);
|
|
1636
|
+
});
|
|
1637
|
+
fillStyle = gradient;
|
|
1638
|
+
}
|
|
1639
|
+
if (frame.pattern) {
|
|
1640
|
+
const patternImage = await (0, canvas_1.loadImage)(frame.pattern.source);
|
|
1641
|
+
const pattern = ctx.createPattern(patternImage, frame.pattern.repeat || 'repeat');
|
|
1642
|
+
fillStyle = pattern;
|
|
1643
|
+
}
|
|
1644
|
+
if (!fillStyle && frame.backgroundColor) {
|
|
1645
|
+
fillStyle = frame.backgroundColor;
|
|
1646
|
+
}
|
|
1647
|
+
if (fillStyle) {
|
|
1648
|
+
ctx.fillStyle = fillStyle;
|
|
1649
|
+
ctx.fillRect(0, 0, width, height);
|
|
1650
|
+
}
|
|
1651
|
+
if (frame.source) {
|
|
1652
|
+
const image = await (0, canvas_1.loadImage)(frame.source);
|
|
1653
|
+
ctx.globalCompositeOperation = frame.blendMode || 'source-over';
|
|
1654
|
+
ctx.drawImage(image, 0, 0, width, height);
|
|
1655
|
+
}
|
|
1656
|
+
if (frame.onDrawCustom) {
|
|
1657
|
+
frame.onDrawCustom(ctx, canvas);
|
|
1658
|
+
}
|
|
1659
|
+
if (frame.transformations) {
|
|
1660
|
+
ctx.restore();
|
|
1661
|
+
}
|
|
1662
|
+
const buffer = canvas.toBuffer('image/png');
|
|
1663
|
+
buffers.push(buffer);
|
|
1664
|
+
if (encoder) {
|
|
1665
|
+
const frameDuration = frame.duration || defaultDuration;
|
|
1666
|
+
encoder.setDelay(frameDuration);
|
|
1667
|
+
encoder.addFrame(ctx);
|
|
1668
|
+
}
|
|
1669
|
+
if (options?.onFrame)
|
|
1670
|
+
options.onFrame(i);
|
|
1671
|
+
await new Promise(resolve => setTimeout(resolve, frame.duration || defaultDuration));
|
|
952
1672
|
}
|
|
953
|
-
if (
|
|
954
|
-
|
|
1673
|
+
if (encoder) {
|
|
1674
|
+
encoder.finish();
|
|
955
1675
|
}
|
|
956
|
-
if (
|
|
957
|
-
|
|
958
|
-
|
|
1676
|
+
if (options?.onEnd)
|
|
1677
|
+
options.onEnd();
|
|
1678
|
+
return options?.gif ? undefined : buffers;
|
|
1679
|
+
}
|
|
1680
|
+
catch (error) {
|
|
1681
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1682
|
+
throw new Error(`animate failed: ${errorMessage}`);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Processes multiple operations in parallel
|
|
1687
|
+
* @param operations - Array of operations to process
|
|
1688
|
+
* @returns Array of result buffers
|
|
1689
|
+
*/
|
|
1690
|
+
async batch(operations) {
|
|
1691
|
+
try {
|
|
1692
|
+
return await (0, utils_1.batchOperations)(this, operations);
|
|
1693
|
+
}
|
|
1694
|
+
catch (error) {
|
|
1695
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1696
|
+
throw new Error(`batch failed: ${errorMessage}`);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Chains multiple operations sequentially
|
|
1701
|
+
* @param operations - Array of operations to chain
|
|
1702
|
+
* @returns Final result buffer
|
|
1703
|
+
*/
|
|
1704
|
+
async chain(operations) {
|
|
1705
|
+
try {
|
|
1706
|
+
return await (0, utils_1.chainOperations)(this, operations);
|
|
1707
|
+
}
|
|
1708
|
+
catch (error) {
|
|
1709
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1710
|
+
throw new Error(`chain failed: ${errorMessage}`);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Stitches multiple images together
|
|
1715
|
+
* @param images - Array of image sources
|
|
1716
|
+
* @param options - Stitching options
|
|
1717
|
+
* @returns Stitched image buffer
|
|
1718
|
+
*/
|
|
1719
|
+
async stitchImages(images, options) {
|
|
1720
|
+
try {
|
|
1721
|
+
if (!images || images.length === 0) {
|
|
1722
|
+
throw new Error("stitchImages: images array is required");
|
|
959
1723
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1724
|
+
return await (0, utils_1.stitchImages)(images, options);
|
|
1725
|
+
}
|
|
1726
|
+
catch (error) {
|
|
1727
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1728
|
+
throw new Error(`stitchImages failed: ${errorMessage}`);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Creates an image collage
|
|
1733
|
+
* @param images - Array of image sources with optional dimensions
|
|
1734
|
+
* @param layout - Collage layout configuration
|
|
1735
|
+
* @returns Collage image buffer
|
|
1736
|
+
*/
|
|
1737
|
+
async createCollage(images, layout) {
|
|
1738
|
+
try {
|
|
1739
|
+
if (!images || images.length === 0) {
|
|
1740
|
+
throw new Error("createCollage: images array is required");
|
|
964
1741
|
}
|
|
965
|
-
if (
|
|
966
|
-
|
|
1742
|
+
if (!layout) {
|
|
1743
|
+
throw new Error("createCollage: layout configuration is required");
|
|
967
1744
|
}
|
|
968
|
-
|
|
969
|
-
|
|
1745
|
+
return await (0, utils_1.createCollage)(images, layout);
|
|
1746
|
+
}
|
|
1747
|
+
catch (error) {
|
|
1748
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1749
|
+
throw new Error(`createCollage failed: ${errorMessage}`);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Compresses an image with quality control
|
|
1754
|
+
* @param image - Image source (path, URL, or Buffer)
|
|
1755
|
+
* @param options - Compression options
|
|
1756
|
+
* @returns Compressed image buffer
|
|
1757
|
+
*/
|
|
1758
|
+
async compress(image, options) {
|
|
1759
|
+
try {
|
|
1760
|
+
if (!image) {
|
|
1761
|
+
throw new Error("compress: image is required");
|
|
970
1762
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1763
|
+
return await (0, utils_1.compressImage)(image, options);
|
|
1764
|
+
}
|
|
1765
|
+
catch (error) {
|
|
1766
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1767
|
+
throw new Error(`compress failed: ${errorMessage}`);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Extracts color palette from an image
|
|
1772
|
+
* @param image - Image source (path, URL, or Buffer)
|
|
1773
|
+
* @param options - Palette extraction options
|
|
1774
|
+
* @returns Array of colors with percentages
|
|
1775
|
+
*/
|
|
1776
|
+
async extractPalette(image, options) {
|
|
1777
|
+
try {
|
|
1778
|
+
if (!image) {
|
|
1779
|
+
throw new Error("extractPalette: image is required");
|
|
977
1780
|
}
|
|
978
|
-
|
|
979
|
-
options.onFrame(i);
|
|
980
|
-
await new Promise(resolve => setTimeout(resolve, frame.duration || defaultDuration));
|
|
1781
|
+
return await (0, utils_1.extractPalette)(image, options);
|
|
981
1782
|
}
|
|
982
|
-
|
|
983
|
-
|
|
1783
|
+
catch (error) {
|
|
1784
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1785
|
+
throw new Error(`extractPalette failed: ${errorMessage}`);
|
|
984
1786
|
}
|
|
985
|
-
if (options?.onEnd)
|
|
986
|
-
options.onEnd();
|
|
987
|
-
return options?.gif ? undefined : buffers;
|
|
988
1787
|
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Validates a hexadecimal color string.
|
|
1790
|
+
* @param hexColor - Hexadecimal color string to validate (format: #RRGGBB)
|
|
1791
|
+
* @returns True if the color is valid
|
|
1792
|
+
* @throws Error if the color format is invalid
|
|
1793
|
+
*
|
|
1794
|
+
* @example
|
|
1795
|
+
* ```typescript
|
|
1796
|
+
* painter.validHex('#ff0000'); // true
|
|
1797
|
+
* painter.validHex('#FF00FF'); // true
|
|
1798
|
+
* painter.validHex('invalid'); // throws Error
|
|
1799
|
+
* ```
|
|
1800
|
+
*/
|
|
989
1801
|
validHex(hexColor) {
|
|
1802
|
+
if (typeof hexColor !== 'string') {
|
|
1803
|
+
throw new Error("validHex: hexColor must be a string.");
|
|
1804
|
+
}
|
|
990
1805
|
const hexPattern = /^#[0-9a-fA-F]{6}$/;
|
|
991
1806
|
if (!hexPattern.test(hexColor)) {
|
|
992
|
-
throw new Error("Invalid hexadecimal color format. It should be in the format '#RRGGBB'.");
|
|
1807
|
+
throw new Error("validHex: Invalid hexadecimal color format. It should be in the format '#RRGGBB'.");
|
|
993
1808
|
}
|
|
994
1809
|
return true;
|
|
995
1810
|
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Converts results to the configured output format.
|
|
1813
|
+
* @param results - Buffer or result to convert
|
|
1814
|
+
* @returns Converted result in the configured format
|
|
1815
|
+
* @throws Error if format is unsupported or conversion fails
|
|
1816
|
+
*
|
|
1817
|
+
* @example
|
|
1818
|
+
* ```typescript
|
|
1819
|
+
* const painter = new ApexPainter({ type: 'base64' });
|
|
1820
|
+
* const result = await painter.createCanvas({ width: 100, height: 100 });
|
|
1821
|
+
* const base64String = await painter.outPut(result.buffer); // Returns base64 string
|
|
1822
|
+
* ```
|
|
1823
|
+
*/
|
|
996
1824
|
async outPut(results) {
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1825
|
+
try {
|
|
1826
|
+
if (!Buffer.isBuffer(results)) {
|
|
1827
|
+
throw new Error("outPut: results must be a Buffer.");
|
|
1828
|
+
}
|
|
1829
|
+
const formatType = this.format?.type || 'buffer';
|
|
1830
|
+
switch (formatType) {
|
|
1831
|
+
case 'buffer':
|
|
1832
|
+
return results;
|
|
1833
|
+
case 'url':
|
|
1834
|
+
return await (0, utils_1.url)(results);
|
|
1835
|
+
case 'dataURL':
|
|
1836
|
+
return (0, utils_1.dataURL)(results);
|
|
1837
|
+
case 'blob':
|
|
1838
|
+
return (0, utils_1.blob)(results);
|
|
1839
|
+
case 'base64':
|
|
1840
|
+
return (0, utils_1.base64)(results);
|
|
1841
|
+
case 'arraybuffer':
|
|
1842
|
+
return (0, utils_1.arrayBuffer)(results);
|
|
1843
|
+
default:
|
|
1844
|
+
throw new Error(`outPut: Unsupported format '${formatType}'. Supported: buffer, url, dataURL, blob, base64, arraybuffer`);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
catch (error) {
|
|
1848
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1849
|
+
throw new Error(`outPut failed: ${errorMessage}`);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Applies stroke style to shape context
|
|
1854
|
+
* @private
|
|
1855
|
+
* @param ctx - Canvas 2D context
|
|
1856
|
+
* @param style - Stroke style type
|
|
1857
|
+
* @param width - Stroke width for calculating dash patterns
|
|
1858
|
+
*/
|
|
1859
|
+
#applyShapeStrokeStyle(ctx, style, width) {
|
|
1860
|
+
switch (style) {
|
|
1861
|
+
case 'solid':
|
|
1862
|
+
ctx.setLineDash([]);
|
|
1863
|
+
ctx.lineCap = 'butt';
|
|
1864
|
+
ctx.lineJoin = 'miter';
|
|
1865
|
+
break;
|
|
1866
|
+
case 'dashed':
|
|
1867
|
+
ctx.setLineDash([width * 3, width * 2]);
|
|
1868
|
+
ctx.lineCap = 'butt';
|
|
1869
|
+
ctx.lineJoin = 'miter';
|
|
1870
|
+
break;
|
|
1871
|
+
case 'dotted':
|
|
1872
|
+
ctx.setLineDash([width, width]);
|
|
1873
|
+
ctx.lineCap = 'round';
|
|
1874
|
+
ctx.lineJoin = 'round';
|
|
1875
|
+
break;
|
|
1876
|
+
case 'groove':
|
|
1877
|
+
case 'ridge':
|
|
1878
|
+
case 'double':
|
|
1879
|
+
ctx.setLineDash([]);
|
|
1880
|
+
ctx.lineCap = 'butt';
|
|
1881
|
+
ctx.lineJoin = 'miter';
|
|
1882
|
+
break;
|
|
1011
1883
|
default:
|
|
1012
|
-
|
|
1884
|
+
ctx.setLineDash([]);
|
|
1885
|
+
ctx.lineCap = 'butt';
|
|
1886
|
+
ctx.lineJoin = 'miter';
|
|
1887
|
+
break;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Applies complex shape stroke styles that require multiple passes
|
|
1892
|
+
* @private
|
|
1893
|
+
* @param ctx - Canvas 2D context
|
|
1894
|
+
* @param style - Complex stroke style type
|
|
1895
|
+
* @param width - Stroke width
|
|
1896
|
+
* @param color - Base stroke color
|
|
1897
|
+
* @param gradient - Optional gradient
|
|
1898
|
+
*/
|
|
1899
|
+
#applyComplexShapeStroke(ctx, style, width, color, gradient) {
|
|
1900
|
+
const halfWidth = width / 2;
|
|
1901
|
+
switch (style) {
|
|
1902
|
+
case 'groove':
|
|
1903
|
+
// Groove: dark outer, light inner
|
|
1904
|
+
ctx.lineWidth = halfWidth;
|
|
1905
|
+
// Outer dark stroke
|
|
1906
|
+
if (gradient) {
|
|
1907
|
+
const gstroke = (0, utils_1.createGradientFill)(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
1908
|
+
ctx.strokeStyle = gstroke;
|
|
1909
|
+
}
|
|
1910
|
+
else {
|
|
1911
|
+
ctx.strokeStyle = this.#darkenColor(color, 0.3);
|
|
1912
|
+
}
|
|
1913
|
+
ctx.stroke();
|
|
1914
|
+
// Inner light stroke
|
|
1915
|
+
ctx.lineWidth = halfWidth;
|
|
1916
|
+
if (gradient) {
|
|
1917
|
+
const gstroke = (0, utils_1.createGradientFill)(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
1918
|
+
ctx.strokeStyle = gstroke;
|
|
1919
|
+
}
|
|
1920
|
+
else {
|
|
1921
|
+
ctx.strokeStyle = this.#lightenColor(color, 0.3);
|
|
1922
|
+
}
|
|
1923
|
+
ctx.stroke();
|
|
1924
|
+
break;
|
|
1925
|
+
case 'ridge':
|
|
1926
|
+
// Ridge: light outer, dark inner
|
|
1927
|
+
ctx.lineWidth = halfWidth;
|
|
1928
|
+
// Outer light stroke
|
|
1929
|
+
if (gradient) {
|
|
1930
|
+
const gstroke = (0, utils_1.createGradientFill)(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
1931
|
+
ctx.strokeStyle = gstroke;
|
|
1932
|
+
}
|
|
1933
|
+
else {
|
|
1934
|
+
ctx.strokeStyle = this.#lightenColor(color, 0.3);
|
|
1935
|
+
}
|
|
1936
|
+
ctx.stroke();
|
|
1937
|
+
// Inner dark stroke
|
|
1938
|
+
ctx.lineWidth = halfWidth;
|
|
1939
|
+
if (gradient) {
|
|
1940
|
+
const gstroke = (0, utils_1.createGradientFill)(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
1941
|
+
ctx.strokeStyle = gstroke;
|
|
1942
|
+
}
|
|
1943
|
+
else {
|
|
1944
|
+
ctx.strokeStyle = this.#darkenColor(color, 0.3);
|
|
1945
|
+
}
|
|
1946
|
+
ctx.stroke();
|
|
1947
|
+
break;
|
|
1948
|
+
case 'double':
|
|
1949
|
+
// Double: two parallel strokes
|
|
1950
|
+
ctx.lineWidth = halfWidth;
|
|
1951
|
+
// First stroke (outer)
|
|
1952
|
+
if (gradient) {
|
|
1953
|
+
const gstroke = (0, utils_1.createGradientFill)(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
1954
|
+
ctx.strokeStyle = gstroke;
|
|
1955
|
+
}
|
|
1956
|
+
else {
|
|
1957
|
+
ctx.strokeStyle = color;
|
|
1958
|
+
}
|
|
1959
|
+
ctx.stroke();
|
|
1960
|
+
// Second stroke (inner)
|
|
1961
|
+
ctx.lineWidth = halfWidth;
|
|
1962
|
+
if (gradient) {
|
|
1963
|
+
const gstroke = (0, utils_1.createGradientFill)(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
1964
|
+
ctx.strokeStyle = gstroke;
|
|
1965
|
+
}
|
|
1966
|
+
else {
|
|
1967
|
+
ctx.strokeStyle = color;
|
|
1968
|
+
}
|
|
1969
|
+
ctx.stroke();
|
|
1970
|
+
break;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Darkens a color by a factor
|
|
1975
|
+
* @private
|
|
1976
|
+
* @param color - Color string
|
|
1977
|
+
* @param factor - Darkening factor (0-1)
|
|
1978
|
+
* @returns Darkened color string
|
|
1979
|
+
*/
|
|
1980
|
+
#darkenColor(color, factor) {
|
|
1981
|
+
// Simple darkening for hex colors
|
|
1982
|
+
if (color.startsWith('#')) {
|
|
1983
|
+
const hex = color.slice(1);
|
|
1984
|
+
const num = parseInt(hex, 16);
|
|
1985
|
+
const r = Math.max(0, Math.floor((num >> 16) * (1 - factor)));
|
|
1986
|
+
const g = Math.max(0, Math.floor(((num >> 8) & 0x00FF) * (1 - factor)));
|
|
1987
|
+
const b = Math.max(0, Math.floor((num & 0x0000FF) * (1 - factor)));
|
|
1988
|
+
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
|
|
1989
|
+
}
|
|
1990
|
+
return color; // Return original for non-hex colors
|
|
1991
|
+
}
|
|
1992
|
+
/**
|
|
1993
|
+
* Lightens a color by a factor
|
|
1994
|
+
* @private
|
|
1995
|
+
* @param color - Color string
|
|
1996
|
+
* @param factor - Lightening factor (0-1)
|
|
1997
|
+
* @returns Lightened color string
|
|
1998
|
+
*/
|
|
1999
|
+
#lightenColor(color, factor) {
|
|
2000
|
+
// Simple lightening for hex colors
|
|
2001
|
+
if (color.startsWith('#')) {
|
|
2002
|
+
const hex = color.slice(1);
|
|
2003
|
+
const num = parseInt(hex, 16);
|
|
2004
|
+
const r = Math.min(255, Math.floor((num >> 16) + (255 - (num >> 16)) * factor));
|
|
2005
|
+
const g = Math.min(255, Math.floor(((num >> 8) & 0x00FF) + (255 - ((num >> 8) & 0x00FF)) * factor));
|
|
2006
|
+
const b = Math.min(255, Math.floor((num & 0x0000FF) + (255 - (num & 0x0000FF)) * factor));
|
|
2007
|
+
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
|
|
1013
2008
|
}
|
|
2009
|
+
return color; // Return original for non-hex colors
|
|
1014
2010
|
}
|
|
1015
2011
|
}
|
|
1016
2012
|
exports.ApexPainter = ApexPainter;
|