@clypra/engine 1.1.2 → 1.2.1

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/dist/index.d.cts CHANGED
@@ -462,7 +462,7 @@ interface CompositionPreset {
462
462
  description?: string;
463
463
  }
464
464
  declare const COMPOSITION_PRESETS: CompositionPreset[];
465
- /** Soft-wrap paragraphs to fit safe width */
465
+ /** Soft-wrap paragraphs to fit safe width character-by-character */
466
466
  declare function wrapTextToWidth(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, text: string, maxWidth: number, letterSpacing: number): string[];
467
467
  declare function measureTextFits(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, cfg: TextEffectConfig, fontSize: number, lines: string[]): boolean;
468
468
  declare function computeAutoFitFontSize(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, cfg: TextEffectConfig, wrappedLines: string[]): number;
package/dist/index.d.ts CHANGED
@@ -462,7 +462,7 @@ interface CompositionPreset {
462
462
  description?: string;
463
463
  }
464
464
  declare const COMPOSITION_PRESETS: CompositionPreset[];
465
- /** Soft-wrap paragraphs to fit safe width */
465
+ /** Soft-wrap paragraphs to fit safe width character-by-character */
466
466
  declare function wrapTextToWidth(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, text: string, maxWidth: number, letterSpacing: number): string[];
467
467
  declare function measureTextFits(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, cfg: TextEffectConfig, fontSize: number, lines: string[]): boolean;
468
468
  declare function computeAutoFitFontSize(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, cfg: TextEffectConfig, wrappedLines: string[]): number;
package/dist/index.js CHANGED
@@ -73,35 +73,26 @@ function wrapTextToWidth(ctx, text, maxWidth, letterSpacing) {
73
73
  const paragraphs = text.split("\n");
74
74
  const lines = [];
75
75
  for (const para of paragraphs) {
76
- if (!para.trim()) {
76
+ if (para === "") {
77
77
  lines.push("");
78
78
  continue;
79
79
  }
80
- const words = para.split(/\s+/).filter(Boolean);
81
80
  let current = "";
82
- for (const word of words) {
83
- const candidate = current ? `${current} ${word}` : word;
84
- if (measureLine(ctx, candidate, letterSpacing) <= maxWidth) {
85
- current = candidate;
81
+ for (let i = 0; i < para.length; i++) {
82
+ const char = para[i];
83
+ const tryLine = current + char;
84
+ if (measureLine(ctx, tryLine, letterSpacing) <= maxWidth) {
85
+ current = tryLine;
86
86
  } else {
87
- if (current) lines.push(current);
88
- if (measureLine(ctx, word, letterSpacing) > maxWidth) {
89
- let chunk = "";
90
- for (const ch of word) {
91
- const tryChunk = chunk + ch;
92
- if (measureLine(ctx, tryChunk, letterSpacing) <= maxWidth) chunk = tryChunk;
93
- else {
94
- if (chunk) lines.push(chunk);
95
- chunk = ch;
96
- }
97
- }
98
- current = chunk;
99
- } else {
100
- current = word;
87
+ if (current) {
88
+ lines.push(current);
101
89
  }
90
+ current = char;
102
91
  }
103
92
  }
104
- if (current) lines.push(current);
93
+ if (current) {
94
+ lines.push(current);
95
+ }
105
96
  }
106
97
  return lines.length > 0 ? lines : [""];
107
98
  }
@@ -127,17 +118,17 @@ function layoutWithFontSize(ctx, cfg, fontSize, lines) {
127
118
  } else {
128
119
  startX = safe.x + safe.width / 2;
129
120
  }
130
- let startY = safe.y + (safe.height - textBlockHeight) / 2 + fontSize * 0.82;
121
+ let startY = safe.y + (safe.height - textBlockHeight) / 2 + fontSize * 0.85;
131
122
  if (cfg.textPosY === "top") {
132
- startY = safe.y + fontSize * 0.82;
123
+ startY = safe.y + fontSize * 0.85;
133
124
  } else if (cfg.textPosY === "bottom") {
134
- startY = safe.y + safe.height - textBlockHeight + fontSize * 0.82;
125
+ startY = safe.y + safe.height - textBlockHeight + fontSize * 0.85;
135
126
  }
136
127
  let xMin = startX;
137
128
  if (align === "center") xMin = startX - maxLineWidth / 2;
138
129
  else if (align === "right") xMin = startX - maxLineWidth;
139
130
  const yMin = startY - fontSize * 0.85;
140
- const yMax = startY + (lines.length - 1) * fontSize * lineHeight + fontSize * 0.25;
131
+ const yMax = startY + (lines.length - 1) * fontSize * lineHeight + fontSize * 0.15;
141
132
  return {
142
133
  lines,
143
134
  fontSize,
@@ -576,11 +567,11 @@ var InkBrushEngine = class {
576
567
  if (letterSpacing !== 0) {
577
568
  ctx.letterSpacing = `${letterSpacing}px`;
578
569
  }
579
- let startY = (height - textBlockHeight) / 2 + fontSize * 0.8;
570
+ let startY = (height - textBlockHeight) / 2 + fontSize * 0.85;
580
571
  if (textPosY === "top") {
581
- startY = 40 + fontSize * 0.8;
572
+ startY = 40 + fontSize * 0.85;
582
573
  } else if (textPosY === "bottom") {
583
- startY = height - 40 - textBlockHeight + fontSize * 0.8;
574
+ startY = height - 40 - textBlockHeight + fontSize * 0.85;
584
575
  }
585
576
  ctx.save();
586
577
  if (skewX !== 0) {
@@ -728,11 +719,11 @@ var InkBrushEngine = class {
728
719
  if (letterSpacing !== 0) {
729
720
  ctx.letterSpacing = `${letterSpacing}px`;
730
721
  }
731
- let startY = (height - textBlockHeight) / 2 + fontSize * 0.8;
722
+ let startY = (height - textBlockHeight) / 2 + fontSize * 0.85;
732
723
  if (textPosY === "top") {
733
- startY = 40 + fontSize * 0.8;
724
+ startY = 40 + fontSize * 0.85;
734
725
  } else if (textPosY === "bottom") {
735
- startY = height - 40 - textBlockHeight + fontSize * 0.8;
726
+ startY = height - 40 - textBlockHeight + fontSize * 0.85;
736
727
  }
737
728
  ctx.save();
738
729
  if (skewX !== 0) {
@@ -817,6 +808,17 @@ function restoreLetterSpacing(ctx, saved) {
817
808
  function getCanvas2DContext2(canvas) {
818
809
  return canvas.getContext("2d");
819
810
  }
811
+ function ctxSupportsFilter(ctx) {
812
+ try {
813
+ const prev = ctx.filter;
814
+ ctx.filter = "blur(4px)";
815
+ const ok = typeof ctx.filter === "string" && ctx.filter.includes("blur");
816
+ ctx.filter = prev;
817
+ return ok;
818
+ } catch {
819
+ return false;
820
+ }
821
+ }
820
822
  function renderTextEffectCore(ctx, cfg) {
821
823
  if (cfg.customRenderer === "InkBrushEngine") {
822
824
  const engine = new InkBrushEngine(cfg);
@@ -1126,19 +1128,31 @@ function renderTextEffectCore(ctx, cfg) {
1126
1128
  const vpy = cHeight / 2 + (bevelVanishingPointY !== void 0 ? bevelVanishingPointY : 80) / 100 * (cHeight / 2);
1127
1129
  const fl = Math.max(100, bevelFocalLength !== void 0 ? bevelFocalLength : 400);
1128
1130
  if (bevelBlur && bevelBlur > 0) {
1129
- ctx.save();
1130
- ctx.filter = `blur(${bevelBlur}px)`;
1131
1131
  const blurColor = bevelBlurColor || bevelShadow || "#000000";
1132
- for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1133
- const scale = fl / (fl + i);
1132
+ if (ctxSupportsFilter(ctx)) {
1134
1133
  ctx.save();
1135
- ctx.translate(vpx, vpy);
1136
- ctx.scale(scale, scale);
1137
- ctx.translate(-vpx, -vpy);
1138
- renderLines("fill", blurColor);
1134
+ ctx.filter = `blur(${bevelBlur}px)`;
1135
+ for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1136
+ const scale = fl / (fl + i);
1137
+ ctx.save();
1138
+ ctx.translate(vpx, vpy);
1139
+ ctx.scale(scale, scale);
1140
+ ctx.translate(-vpx, -vpy);
1141
+ renderLines("fill", blurColor);
1142
+ ctx.restore();
1143
+ }
1139
1144
  ctx.restore();
1145
+ } else {
1146
+ for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1147
+ const scale = fl / (fl + i);
1148
+ ctx.save();
1149
+ ctx.translate(vpx, vpy);
1150
+ ctx.scale(scale, scale);
1151
+ ctx.translate(-vpx, -vpy);
1152
+ renderWithShadowTrick("fill", blurColor, bevelBlur, 0, 0, 100);
1153
+ ctx.restore();
1154
+ }
1140
1155
  }
1141
- ctx.restore();
1142
1156
  }
1143
1157
  ctx.save();
1144
1158
  for (let i = bevelDepth; i > 0; i--) {
@@ -1185,14 +1199,21 @@ function renderTextEffectCore(ctx, cfg) {
1185
1199
  return { dx: i, dy: i };
1186
1200
  };
1187
1201
  if (bevelBlur && bevelBlur > 0) {
1188
- ctx.save();
1189
- ctx.filter = `blur(${bevelBlur}px)`;
1190
1202
  const blurColor = bevelBlurColor || bevelShadow || "#000000";
1191
- for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1192
- const { dx, dy } = getDirOffset(i);
1193
- renderLines("fill", blurColor, dx, dy);
1203
+ if (ctxSupportsFilter(ctx)) {
1204
+ ctx.save();
1205
+ ctx.filter = `blur(${bevelBlur}px)`;
1206
+ for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1207
+ const { dx, dy } = getDirOffset(i);
1208
+ renderLines("fill", blurColor, dx, dy);
1209
+ }
1210
+ ctx.restore();
1211
+ } else {
1212
+ for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1213
+ const { dx, dy } = getDirOffset(i);
1214
+ renderWithShadowTrick("fill", blurColor, bevelBlur, dx, dy, 100);
1215
+ }
1194
1216
  }
1195
- ctx.restore();
1196
1217
  }
1197
1218
  ctx.save();
1198
1219
  for (let i = bevelDepth; i > 0; i--) {
@@ -1246,24 +1267,51 @@ function renderTextEffectCore(ctx, cfg) {
1246
1267
  customStrokeStyle = grad;
1247
1268
  }
1248
1269
  const drawStrokeLayer = (color, width, blurAmount, opacity, position) => {
1249
- ctx.save();
1250
- ctx.globalAlpha = opacity / 100;
1251
- ctx.strokeStyle = color;
1252
- if (blurAmount > 0) {
1270
+ if (blurAmount > 0 && ctxSupportsFilter(ctx)) {
1271
+ ctx.save();
1272
+ ctx.globalAlpha = opacity / 100;
1273
+ ctx.strokeStyle = color;
1253
1274
  ctx.filter = `blur(${blurAmount}px)`;
1275
+ if (position === "outside") {
1276
+ ctx.lineWidth = width * 2;
1277
+ renderLines("stroke");
1278
+ } else if (position === "center") {
1279
+ ctx.lineWidth = width;
1280
+ renderLines("stroke");
1281
+ } else if (position === "inside") {
1282
+ ctx.globalCompositeOperation = "source-atop";
1283
+ ctx.lineWidth = width * 2;
1284
+ renderLines("stroke");
1285
+ }
1286
+ ctx.restore();
1287
+ } else if (blurAmount > 0) {
1288
+ const colorStr = typeof color === "string" ? color : strokeColor;
1289
+ const spread = position === "center" ? width / 2 : width;
1290
+ if (position === "inside") {
1291
+ ctx.save();
1292
+ ctx.globalCompositeOperation = "source-atop";
1293
+ renderWithShadowTrick("stroke", colorStr, blurAmount, 0, 0, opacity, void 0, spread);
1294
+ ctx.restore();
1295
+ } else {
1296
+ renderWithShadowTrick("stroke", colorStr, blurAmount, 0, 0, opacity, void 0, spread);
1297
+ }
1298
+ } else {
1299
+ ctx.save();
1300
+ ctx.globalAlpha = opacity / 100;
1301
+ ctx.strokeStyle = color;
1302
+ if (position === "outside") {
1303
+ ctx.lineWidth = width * 2;
1304
+ renderLines("stroke");
1305
+ } else if (position === "center") {
1306
+ ctx.lineWidth = width;
1307
+ renderLines("stroke");
1308
+ } else if (position === "inside") {
1309
+ ctx.globalCompositeOperation = "source-atop";
1310
+ ctx.lineWidth = width * 2;
1311
+ renderLines("stroke");
1312
+ }
1313
+ ctx.restore();
1254
1314
  }
1255
- if (position === "outside") {
1256
- ctx.lineWidth = width * 2;
1257
- renderLines("stroke");
1258
- } else if (position === "center") {
1259
- ctx.lineWidth = width;
1260
- renderLines("stroke");
1261
- } else if (position === "inside") {
1262
- ctx.globalCompositeOperation = "source-atop";
1263
- ctx.lineWidth = width * 2;
1264
- renderLines("stroke");
1265
- }
1266
- ctx.restore();
1267
1315
  };
1268
1316
  if (sType === "double") {
1269
1317
  const outerWidth = strokeWidth + sWidthSecondary;