apexify.js 4.9.28 → 4.9.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +672 -456
  2. package/dist/cjs/Canvas/ApexPainter.d.ts.map +1 -1
  3. package/dist/cjs/Canvas/ApexPainter.js +169 -2
  4. package/dist/cjs/Canvas/ApexPainter.js.map +1 -1
  5. package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
  6. package/dist/cjs/Canvas/utils/Image/imageProperties.js +181 -2
  7. package/dist/cjs/Canvas/utils/Image/imageProperties.js.map +1 -1
  8. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts +33 -0
  9. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
  10. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js +237 -32
  11. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
  12. package/dist/cjs/Canvas/utils/types.d.ts +50 -10
  13. package/dist/cjs/Canvas/utils/types.d.ts.map +1 -1
  14. package/dist/cjs/Canvas/utils/types.js.map +1 -1
  15. package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
  16. package/dist/esm/Canvas/ApexPainter.js +169 -2
  17. package/dist/esm/Canvas/ApexPainter.js.map +1 -1
  18. package/dist/esm/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
  19. package/dist/esm/Canvas/utils/Image/imageProperties.js +181 -2
  20. package/dist/esm/Canvas/utils/Image/imageProperties.js.map +1 -1
  21. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts +33 -0
  22. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
  23. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js +237 -32
  24. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
  25. package/dist/esm/Canvas/utils/types.d.ts +50 -10
  26. package/dist/esm/Canvas/utils/types.d.ts.map +1 -1
  27. package/dist/esm/Canvas/utils/types.js.map +1 -1
  28. package/lib/Canvas/ApexPainter.ts +192 -2
  29. package/lib/Canvas/utils/Image/imageProperties.ts +207 -2
  30. package/lib/Canvas/utils/Texts/enhancedTextRenderer.ts +274 -36
  31. package/lib/Canvas/utils/types.ts +52 -10
  32. package/package.json +1 -1
@@ -321,7 +321,8 @@ export function applyStroke(
321
321
  width = 2,
322
322
  position = 0,
323
323
  blur = 0,
324
- opacity = 1
324
+ opacity = 1,
325
+ style = 'solid'
325
326
  } = stroke;
326
327
 
327
328
  // expand/shrink by `position`
@@ -346,7 +347,16 @@ export function applyStroke(
346
347
  } else {
347
348
  ctx.strokeStyle = color;
348
349
  }
349
- ctx.stroke();
350
+
351
+ // Apply stroke style
352
+ applyStrokeStyle(ctx, style, width);
353
+
354
+ // Handle complex stroke styles that require multiple passes
355
+ if (style === 'groove' || style === 'ridge' || style === 'double') {
356
+ applyComplexStrokeStyle(ctx, style, width, color, gradient, r);
357
+ } else {
358
+ ctx.stroke();
359
+ }
350
360
 
351
361
  ctx.filter = "none";
352
362
  ctx.globalAlpha = 1;
@@ -380,3 +390,198 @@ export function drawBoxBackground(
380
390
 
381
391
  ctx.restore();
382
392
  }
393
+
394
+ /**
395
+ * Applies stroke style to canvas context
396
+ * @param ctx - Canvas 2D context
397
+ * @param style - Stroke style type
398
+ * @param width - Stroke width for calculating dash patterns
399
+ */
400
+ function applyStrokeStyle(
401
+ ctx: SKRSContext2D,
402
+ style: 'solid' | 'dashed' | 'dotted' | 'groove' | 'ridge' | 'double',
403
+ width: number
404
+ ): void {
405
+ switch (style) {
406
+ case 'solid':
407
+ ctx.setLineDash([]);
408
+ ctx.lineCap = 'butt';
409
+ ctx.lineJoin = 'miter';
410
+ break;
411
+
412
+ case 'dashed':
413
+ ctx.setLineDash([width * 3, width * 2]);
414
+ ctx.lineCap = 'butt';
415
+ ctx.lineJoin = 'miter';
416
+ break;
417
+
418
+ case 'dotted':
419
+ ctx.setLineDash([width, width]);
420
+ ctx.lineCap = 'round';
421
+ ctx.lineJoin = 'round';
422
+ break;
423
+
424
+ case 'groove':
425
+ // Groove effect: draw multiple strokes with different colors/opacity
426
+ ctx.setLineDash([]);
427
+ ctx.lineCap = 'butt';
428
+ ctx.lineJoin = 'miter';
429
+ // Note: Groove effect requires multiple passes - handled in main stroke function
430
+ break;
431
+
432
+ case 'ridge':
433
+ // Ridge effect: draw multiple strokes with different colors/opacity
434
+ ctx.setLineDash([]);
435
+ ctx.lineCap = 'butt';
436
+ ctx.lineJoin = 'miter';
437
+ // Note: Ridge effect requires multiple passes - handled in main stroke function
438
+ break;
439
+
440
+ case 'double':
441
+ // Double effect: draw multiple strokes
442
+ ctx.setLineDash([]);
443
+ ctx.lineCap = 'butt';
444
+ ctx.lineJoin = 'miter';
445
+ // Note: Double effect requires multiple passes - handled in main stroke function
446
+ break;
447
+
448
+ default:
449
+ ctx.setLineDash([]);
450
+ ctx.lineCap = 'butt';
451
+ ctx.lineJoin = 'miter';
452
+ break;
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Applies complex stroke styles that require multiple passes
458
+ * @param ctx - Canvas 2D context
459
+ * @param style - Complex stroke style type
460
+ * @param width - Stroke width
461
+ * @param color - Base stroke color
462
+ * @param gradient - Optional gradient
463
+ * @param rect - Rectangle dimensions
464
+ */
465
+ function applyComplexStrokeStyle(
466
+ ctx: SKRSContext2D,
467
+ style: 'groove' | 'ridge' | 'double',
468
+ width: number,
469
+ color: string,
470
+ gradient: any,
471
+ rect: { x: number; y: number; w: number; h: number }
472
+ ): void {
473
+ const halfWidth = width / 2;
474
+
475
+ switch (style) {
476
+ case 'groove':
477
+ // Groove: dark outer, light inner
478
+ ctx.lineWidth = halfWidth;
479
+
480
+ // Outer dark stroke
481
+ if (gradient) {
482
+ const gstroke = createGradientFill(ctx, gradient, rect);
483
+ ctx.strokeStyle = gstroke as any;
484
+ } else {
485
+ ctx.strokeStyle = darkenColor(color, 0.3);
486
+ }
487
+ ctx.stroke();
488
+
489
+ // Inner light stroke
490
+ ctx.lineWidth = halfWidth;
491
+ if (gradient) {
492
+ const gstroke = createGradientFill(ctx, gradient, rect);
493
+ ctx.strokeStyle = gstroke as any;
494
+ } else {
495
+ ctx.strokeStyle = lightenColor(color, 0.3);
496
+ }
497
+ ctx.stroke();
498
+ break;
499
+
500
+ case 'ridge':
501
+ // Ridge: light outer, dark inner
502
+ ctx.lineWidth = halfWidth;
503
+
504
+ // Outer light stroke
505
+ if (gradient) {
506
+ const gstroke = createGradientFill(ctx, gradient, rect);
507
+ ctx.strokeStyle = gstroke as any;
508
+ } else {
509
+ ctx.strokeStyle = lightenColor(color, 0.3);
510
+ }
511
+ ctx.stroke();
512
+
513
+ // Inner dark stroke
514
+ ctx.lineWidth = halfWidth;
515
+ if (gradient) {
516
+ const gstroke = createGradientFill(ctx, gradient, rect);
517
+ ctx.strokeStyle = gstroke as any;
518
+ } else {
519
+ ctx.strokeStyle = darkenColor(color, 0.3);
520
+ }
521
+ ctx.stroke();
522
+ break;
523
+
524
+ case 'double':
525
+ // Double: two parallel strokes
526
+ const gap = Math.max(1, width / 4);
527
+
528
+ // First stroke (outer)
529
+ ctx.lineWidth = halfWidth;
530
+ if (gradient) {
531
+ const gstroke = createGradientFill(ctx, gradient, rect);
532
+ ctx.strokeStyle = gstroke as any;
533
+ } else {
534
+ ctx.strokeStyle = color;
535
+ }
536
+ ctx.stroke();
537
+
538
+ // Second stroke (inner)
539
+ ctx.lineWidth = halfWidth;
540
+ if (gradient) {
541
+ const gstroke = createGradientFill(ctx, gradient, rect);
542
+ ctx.strokeStyle = gstroke as any;
543
+ } else {
544
+ ctx.strokeStyle = color;
545
+ }
546
+ ctx.stroke();
547
+ break;
548
+ }
549
+ }
550
+
551
+ /**
552
+ * Darkens a color by a factor
553
+ * @param color - Color string
554
+ * @param factor - Darkening factor (0-1)
555
+ * @returns Darkened color string
556
+ */
557
+ function darkenColor(color: string, factor: number): string {
558
+ // Simple darkening for hex colors
559
+ if (color.startsWith('#')) {
560
+ const hex = color.slice(1);
561
+ const num = parseInt(hex, 16);
562
+ const r = Math.max(0, Math.floor((num >> 16) * (1 - factor)));
563
+ const g = Math.max(0, Math.floor(((num >> 8) & 0x00FF) * (1 - factor)));
564
+ const b = Math.max(0, Math.floor((num & 0x0000FF) * (1 - factor)));
565
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
566
+ }
567
+ return color; // Return original for non-hex colors
568
+ }
569
+
570
+ /**
571
+ * Lightens a color by a factor
572
+ * @param color - Color string
573
+ * @param factor - Lightening factor (0-1)
574
+ * @returns Lightened color string
575
+ */
576
+ function lightenColor(color: string, factor: number): string {
577
+ // Simple lightening for hex colors
578
+ if (color.startsWith('#')) {
579
+ const hex = color.slice(1);
580
+ const num = parseInt(hex, 16);
581
+ const r = Math.min(255, Math.floor((num >> 16) + (255 - (num >> 16)) * factor));
582
+ const g = Math.min(255, Math.floor(((num >> 8) & 0x00FF) + (255 - ((num >> 8) & 0x00FF)) * factor));
583
+ const b = Math.min(255, Math.floor((num & 0x0000FF) + (255 - (num & 0x0000FF)) * factor));
584
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
585
+ }
586
+ return color; // Return original for non-hex colors
587
+ }
@@ -16,9 +16,12 @@ export class EnhancedTextRenderer {
16
16
  ctx.save();
17
17
 
18
18
  try {
19
- // 1. Register custom font if provided
20
- if (textProps.fontPath) {
21
- await this.registerCustomFont(textProps.fontPath, textProps.fontName || 'customFont');
19
+ // 1. Register custom font if provided (support both new and legacy properties)
20
+ const fontPath = textProps.font?.path || textProps.fontPath;
21
+ const fontName = textProps.font?.name || textProps.fontName;
22
+
23
+ if (fontPath) {
24
+ await this.registerCustomFont(fontPath, fontName || 'customFont');
22
25
  }
23
26
 
24
27
  // 2. Apply transformations
@@ -81,8 +84,9 @@ export class EnhancedTextRenderer {
81
84
  * @param textProps - Text properties
82
85
  */
83
86
  private static setupFont(ctx: SKRSContext2D, textProps: TextProperties): void {
84
- const fontSize = textProps.fontSize || 16;
85
- const fontFamily = textProps.fontName || textProps.fontFamily || 'Arial';
87
+ // Support both new font object and legacy properties
88
+ const fontSize = textProps.font?.size || textProps.fontSize || 16;
89
+ const fontFamily = textProps.font?.name || textProps.fontName || textProps.font?.family || textProps.fontFamily || 'Arial';
86
90
 
87
91
  // Build font string with decorations
88
92
  let fontString = '';
@@ -121,7 +125,7 @@ export class EnhancedTextRenderer {
121
125
  * @param textProps - Text properties
122
126
  */
123
127
  private static async renderWrappedText(ctx: SKRSContext2D, textProps: TextProperties): Promise<void> {
124
- const fontSize = textProps.fontSize || 16;
128
+ const fontSize = textProps.font?.size || textProps.fontSize || 16;
125
129
  const lineHeight = (textProps.lineHeight || 1.4) * fontSize;
126
130
  const maxHeight = textProps.maxHeight;
127
131
  const maxLines = maxHeight ? Math.floor(maxHeight / lineHeight) : Infinity;
@@ -186,7 +190,7 @@ export class EnhancedTextRenderer {
186
190
  // Calculate text dimensions
187
191
  const metrics = ctx.measureText(text);
188
192
  const textWidth = metrics.width;
189
- const fontSize = textProps.fontSize || 16;
193
+ const fontSize = textProps.font?.size || textProps.fontSize || 16;
190
194
  const textHeight = fontSize;
191
195
 
192
196
  // Apply highlight background
@@ -231,13 +235,19 @@ export class EnhancedTextRenderer {
231
235
  y: number,
232
236
  width: number,
233
237
  height: number,
234
- highlight: { color?: string; opacity?: number }
238
+ highlight: { color?: string; gradient?: gradient; opacity?: number }
235
239
  ): void {
236
240
  ctx.save();
237
241
 
238
242
  const opacity = highlight.opacity !== undefined ? highlight.opacity : 0.3;
239
243
  ctx.globalAlpha = opacity;
240
- ctx.fillStyle = highlight.color || '#ffff00';
244
+
245
+ // Set fill style (gradient or color)
246
+ if (highlight.gradient) {
247
+ ctx.fillStyle = this.createGradient(ctx, highlight.gradient, x, y, x + width, y + height);
248
+ } else {
249
+ ctx.fillStyle = highlight.color || '#ffff00';
250
+ }
241
251
 
242
252
  // Adjust highlight position based on text baseline
243
253
  const highlightY = y - height * 0.8; // Adjust for different baselines
@@ -259,18 +269,33 @@ export class EnhancedTextRenderer {
259
269
  text: string,
260
270
  x: number,
261
271
  y: number,
262
- glow: { color?: string; intensity?: number; opacity?: number }
272
+ glow: { color?: string; gradient?: gradient; intensity?: number; opacity?: number }
263
273
  ): void {
264
274
  ctx.save();
265
275
 
266
276
  const intensity = glow.intensity || 10;
267
277
  const opacity = glow.opacity !== undefined ? glow.opacity : 0.8;
268
278
 
269
- ctx.shadowColor = glow.color || '#ffffff';
270
- ctx.shadowBlur = intensity;
271
- ctx.globalAlpha = opacity;
272
-
273
- ctx.fillText(text, x, y);
279
+ // For glow, we need to use shadowColor which only supports solid colors
280
+ // So we'll render the glow with the base color and then overlay with gradient if needed
281
+ if (glow.gradient) {
282
+ // First render with shadow for glow effect
283
+ ctx.shadowColor = '#ffffff'; // Use white as base for glow
284
+ ctx.shadowBlur = intensity;
285
+ ctx.globalAlpha = opacity;
286
+ ctx.fillText(text, x, y);
287
+
288
+ // Then overlay with gradient
289
+ ctx.shadowColor = 'transparent';
290
+ ctx.shadowBlur = 0;
291
+ ctx.fillStyle = this.createGradient(ctx, glow.gradient, x, y, x + ctx.measureText(text).width, y);
292
+ ctx.fillText(text, x, y);
293
+ } else {
294
+ ctx.shadowColor = glow.color || '#ffffff';
295
+ ctx.shadowBlur = intensity;
296
+ ctx.globalAlpha = opacity;
297
+ ctx.fillText(text, x, y);
298
+ }
274
299
 
275
300
  ctx.restore();
276
301
  }
@@ -319,11 +344,14 @@ export class EnhancedTextRenderer {
319
344
  text: string,
320
345
  x: number,
321
346
  y: number,
322
- stroke: { color?: string; width?: number; gradient?: gradient; opacity?: number }
347
+ stroke: { color?: string; width?: number; gradient?: gradient; opacity?: number; style?: 'solid' | 'dashed' | 'dotted' | 'groove' | 'ridge' | 'double' }
323
348
  ): void {
324
349
  ctx.save();
325
350
 
326
- ctx.lineWidth = stroke.width || 1;
351
+ const strokeWidth = stroke.width || 1;
352
+ const strokeStyle = stroke.style || 'solid';
353
+
354
+ ctx.lineWidth = strokeWidth;
327
355
 
328
356
  if (stroke.gradient) {
329
357
  ctx.strokeStyle = this.createGradient(ctx, stroke.gradient, x, y, x + ctx.measureText(text).width, y);
@@ -335,7 +363,15 @@ export class EnhancedTextRenderer {
335
363
  ctx.globalAlpha = stroke.opacity;
336
364
  }
337
365
 
338
- ctx.strokeText(text, x, y);
366
+ // Apply stroke style
367
+ this.applyTextStrokeStyle(ctx, strokeStyle, strokeWidth);
368
+
369
+ // Handle complex stroke styles
370
+ if (strokeStyle === 'groove' || strokeStyle === 'ridge' || strokeStyle === 'double') {
371
+ this.renderComplexTextStroke(ctx, text, x, y, strokeStyle, strokeWidth, stroke.color, stroke.gradient);
372
+ } else {
373
+ ctx.strokeText(text, x, y);
374
+ }
339
375
 
340
376
  ctx.restore();
341
377
  }
@@ -387,44 +423,69 @@ export class EnhancedTextRenderer {
387
423
  height: number,
388
424
  textProps: TextProperties
389
425
  ): void {
390
- if (!textProps.underline && !textProps.overline && !textProps.strikethrough) {
426
+ const hasDecorations = textProps.underline || textProps.overline || textProps.strikethrough;
427
+ if (!hasDecorations) {
391
428
  return;
392
429
  }
393
430
 
394
431
  ctx.save();
395
432
 
396
- const fontSize = textProps.fontSize || 16;
397
- const decorationColor = textProps.color || '#000000';
398
- const decorationWidth = Math.max(1, fontSize * 0.05); // 5% of font size
433
+ const fontSize = textProps.font?.size || textProps.fontSize || 16;
434
+ const defaultColor = textProps.color || '#000000';
399
435
 
400
- ctx.strokeStyle = decorationColor;
401
- ctx.lineWidth = decorationWidth;
436
+ // Helper function to render a decoration line
437
+ const renderDecorationLine = (
438
+ decorationY: number,
439
+ decoration: boolean | { color?: string; gradient?: gradient; width?: number } | undefined,
440
+ lineName: string
441
+ ) => {
442
+ if (!decoration) return;
443
+
444
+ ctx.save();
445
+
446
+ let decorationColor = defaultColor;
447
+ let decorationWidth = Math.max(1, fontSize * 0.05); // 5% of font size
448
+
449
+ if (typeof decoration === 'object') {
450
+ decorationColor = decoration.color || defaultColor;
451
+ decorationWidth = decoration.width || decorationWidth;
452
+
453
+ // Set stroke style (gradient or color)
454
+ if (decoration.gradient) {
455
+ ctx.strokeStyle = this.createGradient(ctx, decoration.gradient, x, decorationY, x + width, decorationY);
456
+ } else {
457
+ ctx.strokeStyle = decorationColor;
458
+ }
459
+ } else {
460
+ ctx.strokeStyle = decorationColor;
461
+ }
462
+
463
+ ctx.lineWidth = decorationWidth;
464
+
465
+ ctx.beginPath();
466
+ ctx.moveTo(x, decorationY);
467
+ ctx.lineTo(x + width, decorationY);
468
+ ctx.stroke();
469
+
470
+ ctx.restore();
471
+ };
402
472
 
403
473
  // Underline
404
474
  if (textProps.underline) {
405
475
  const underlineY = y + fontSize * 0.1;
406
- ctx.beginPath();
407
- ctx.moveTo(x, underlineY);
408
- ctx.lineTo(x + width, underlineY);
409
- ctx.stroke();
476
+ renderDecorationLine(underlineY, textProps.underline, 'underline');
410
477
  }
411
478
 
412
479
  // Overline
413
480
  if (textProps.overline) {
414
481
  const overlineY = y - fontSize * 0.8;
415
- ctx.beginPath();
416
- ctx.moveTo(x, overlineY);
417
- ctx.lineTo(x + width, overlineY);
418
- ctx.stroke();
482
+ renderDecorationLine(overlineY, textProps.overline, 'overline');
419
483
  }
420
484
 
421
485
  // Strikethrough
422
486
  if (textProps.strikethrough) {
423
487
  const strikethroughY = y - fontSize * 0.3;
424
- ctx.beginPath();
425
- ctx.moveTo(x, strikethroughY);
426
- ctx.lineTo(x + width, strikethroughY);
427
- ctx.stroke();
488
+ renderDecorationLine(strikethroughY, textProps.strikethrough, 'strikethrough');
428
489
  }
429
490
 
430
491
  ctx.restore();
@@ -475,4 +536,181 @@ export class EnhancedTextRenderer {
475
536
 
476
537
  return gradient;
477
538
  }
539
+
540
+ /**
541
+ * Applies stroke style to text context
542
+ * @param ctx - Canvas 2D context
543
+ * @param style - Stroke style type
544
+ * @param width - Stroke width for calculating dash patterns
545
+ */
546
+ private static applyTextStrokeStyle(
547
+ ctx: SKRSContext2D,
548
+ style: 'solid' | 'dashed' | 'dotted' | 'groove' | 'ridge' | 'double',
549
+ width: number
550
+ ): void {
551
+ switch (style) {
552
+ case 'solid':
553
+ ctx.setLineDash([]);
554
+ ctx.lineCap = 'butt';
555
+ ctx.lineJoin = 'miter';
556
+ break;
557
+
558
+ case 'dashed':
559
+ ctx.setLineDash([width * 3, width * 2]);
560
+ ctx.lineCap = 'butt';
561
+ ctx.lineJoin = 'miter';
562
+ break;
563
+
564
+ case 'dotted':
565
+ ctx.setLineDash([width, width]);
566
+ ctx.lineCap = 'round';
567
+ ctx.lineJoin = 'round';
568
+ break;
569
+
570
+ case 'groove':
571
+ case 'ridge':
572
+ case 'double':
573
+ ctx.setLineDash([]);
574
+ ctx.lineCap = 'butt';
575
+ ctx.lineJoin = 'miter';
576
+ break;
577
+
578
+ default:
579
+ ctx.setLineDash([]);
580
+ ctx.lineCap = 'butt';
581
+ ctx.lineJoin = 'miter';
582
+ break;
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Renders complex text stroke styles that require multiple passes
588
+ * @param ctx - Canvas 2D context
589
+ * @param text - Text to render
590
+ * @param x - X position
591
+ * @param y - Y position
592
+ * @param style - Complex stroke style type
593
+ * @param width - Stroke width
594
+ * @param color - Base stroke color
595
+ * @param gradient - Optional gradient
596
+ */
597
+ private static renderComplexTextStroke(
598
+ ctx: SKRSContext2D,
599
+ text: string,
600
+ x: number,
601
+ y: number,
602
+ style: 'groove' | 'ridge' | 'double',
603
+ width: number,
604
+ color?: string,
605
+ gradient?: gradient
606
+ ): void {
607
+ const halfWidth = width / 2;
608
+ const textWidth = ctx.measureText(text).width;
609
+
610
+ switch (style) {
611
+ case 'groove':
612
+ // Groove: dark outer, light inner
613
+ ctx.lineWidth = halfWidth;
614
+
615
+ // Outer dark stroke
616
+ if (gradient) {
617
+ ctx.strokeStyle = this.createGradient(ctx, gradient, x, y, x + textWidth, y);
618
+ } else {
619
+ ctx.strokeStyle = this.darkenColor(color || '#000000', 0.3);
620
+ }
621
+ ctx.strokeText(text, x, y);
622
+
623
+ // Inner light stroke
624
+ ctx.lineWidth = halfWidth;
625
+ if (gradient) {
626
+ ctx.strokeStyle = this.createGradient(ctx, gradient, x, y, x + textWidth, y);
627
+ } else {
628
+ ctx.strokeStyle = this.lightenColor(color || '#000000', 0.3);
629
+ }
630
+ ctx.strokeText(text, x, y);
631
+ break;
632
+
633
+ case 'ridge':
634
+ // Ridge: light outer, dark inner
635
+ ctx.lineWidth = halfWidth;
636
+
637
+ // Outer light stroke
638
+ if (gradient) {
639
+ ctx.strokeStyle = this.createGradient(ctx, gradient, x, y, x + textWidth, y);
640
+ } else {
641
+ ctx.strokeStyle = this.lightenColor(color || '#000000', 0.3);
642
+ }
643
+ ctx.strokeText(text, x, y);
644
+
645
+ // Inner dark stroke
646
+ ctx.lineWidth = halfWidth;
647
+ if (gradient) {
648
+ ctx.strokeStyle = this.createGradient(ctx, gradient, x, y, x + textWidth, y);
649
+ } else {
650
+ ctx.strokeStyle = this.darkenColor(color || '#000000', 0.3);
651
+ }
652
+ ctx.strokeText(text, x, y);
653
+ break;
654
+
655
+ case 'double':
656
+ // Double: two parallel strokes
657
+ ctx.lineWidth = halfWidth;
658
+
659
+ // First stroke (outer)
660
+ if (gradient) {
661
+ ctx.strokeStyle = this.createGradient(ctx, gradient, x, y, x + textWidth, y);
662
+ } else {
663
+ ctx.strokeStyle = color || '#000000';
664
+ }
665
+ ctx.strokeText(text, x, y);
666
+
667
+ // Second stroke (inner)
668
+ ctx.lineWidth = halfWidth;
669
+ if (gradient) {
670
+ ctx.strokeStyle = this.createGradient(ctx, gradient, x, y, x + textWidth, y);
671
+ } else {
672
+ ctx.strokeStyle = color || '#000000';
673
+ }
674
+ ctx.strokeText(text, x, y);
675
+ break;
676
+ }
677
+ }
678
+
679
+ /**
680
+ * Darkens a color by a factor
681
+ * @param color - Color string
682
+ * @param factor - Darkening factor (0-1)
683
+ * @returns Darkened color string
684
+ */
685
+ private static darkenColor(color: string, factor: number): string {
686
+ // Simple darkening for hex colors
687
+ if (color.startsWith('#')) {
688
+ const hex = color.slice(1);
689
+ const num = parseInt(hex, 16);
690
+ const r = Math.max(0, Math.floor((num >> 16) * (1 - factor)));
691
+ const g = Math.max(0, Math.floor(((num >> 8) & 0x00FF) * (1 - factor)));
692
+ const b = Math.max(0, Math.floor((num & 0x0000FF) * (1 - factor)));
693
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
694
+ }
695
+ return color; // Return original for non-hex colors
696
+ }
697
+
698
+ /**
699
+ * Lightens a color by a factor
700
+ * @param color - Color string
701
+ * @param factor - Lightening factor (0-1)
702
+ * @returns Lightened color string
703
+ */
704
+ private static lightenColor(color: string, factor: number): string {
705
+ // Simple lightening for hex colors
706
+ if (color.startsWith('#')) {
707
+ const hex = color.slice(1);
708
+ const num = parseInt(hex, 16);
709
+ const r = Math.min(255, Math.floor((num >> 16) + (255 - (num >> 16)) * factor));
710
+ const g = Math.min(255, Math.floor(((num >> 8) & 0x00FF) + (255 - ((num >> 8) & 0x00FF)) * factor));
711
+ const b = Math.min(255, Math.floor((num & 0x0000FF) + (255 - (num & 0x0000FF)) * factor));
712
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
713
+ }
714
+ return color; // Return original for non-hex colors
715
+ }
478
716
  }