@spectratools/graphic-designer-cli 0.8.0 → 0.10.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/dist/renderer.js CHANGED
@@ -473,6 +473,38 @@ function renderFlowNode(ctx, node, bounds, theme) {
473
473
  ctx.shadowOffsetX = 0;
474
474
  ctx.shadowOffsetY = 0;
475
475
  }
476
+ if (node.accentColor) {
477
+ const barWidth = node.accentBarWidth ?? 3;
478
+ const effectiveRadius = node.shape === "box" ? 0 : cornerRadius;
479
+ ctx.save();
480
+ ctx.beginPath();
481
+ ctx.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, effectiveRadius);
482
+ ctx.clip();
483
+ ctx.fillStyle = node.accentColor;
484
+ ctx.fillRect(bounds.x, bounds.y, barWidth, bounds.height);
485
+ ctx.restore();
486
+ }
487
+ if (node.glowColor) {
488
+ const glowW = node.glowWidth ?? 16;
489
+ const glowOp = node.glowOpacity ?? 0.15;
490
+ ctx.save();
491
+ ctx.beginPath();
492
+ ctx.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, cornerRadius);
493
+ ctx.clip();
494
+ const barOffset = node.accentColor ? node.accentBarWidth ?? 3 : 0;
495
+ const gradient = ctx.createLinearGradient(
496
+ bounds.x + barOffset,
497
+ bounds.y,
498
+ bounds.x + barOffset + glowW,
499
+ bounds.y
500
+ );
501
+ gradient.addColorStop(0, node.glowColor);
502
+ gradient.addColorStop(1, "rgba(0,0,0,0)");
503
+ ctx.globalAlpha = glowOp;
504
+ ctx.fillStyle = gradient;
505
+ ctx.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
506
+ ctx.restore();
507
+ }
476
508
  const headingFont = resolveFont(theme.fonts.heading, "heading");
477
509
  const bodyFont = resolveFont(theme.fonts.body, "body");
478
510
  const monoFont = resolveFont(theme.fonts.mono, "mono");
@@ -1976,16 +2008,6 @@ function drawBezier(ctx, points, style) {
1976
2008
  ctx.quadraticCurveTo(penultimate.x, penultimate.y, last.x, last.y);
1977
2009
  ctx.stroke();
1978
2010
  }
1979
- function drawOrthogonalPath(ctx, from, to, style) {
1980
- const midX = (from.x + to.x) / 2;
1981
- applyLineStyle(ctx, style);
1982
- ctx.beginPath();
1983
- ctx.moveTo(from.x, from.y);
1984
- ctx.lineTo(midX, from.y);
1985
- ctx.lineTo(midX, to.y);
1986
- ctx.lineTo(to.x, to.y);
1987
- ctx.stroke();
1988
- }
1989
2011
 
1990
2012
  // src/renderers/connection.ts
1991
2013
  var ELLIPSE_KAPPA = 4 * (Math.sqrt(2) - 1) / 3;
@@ -2225,11 +2247,36 @@ function pointAlongPolyline(points, t) {
2225
2247
  }
2226
2248
  return points[points.length - 1];
2227
2249
  }
2228
- function drawCubicInterpolatedPath(ctx, points, style) {
2250
+ function createConnectionGradient(ctx, start, end, fromColor, baseColor, toColor) {
2251
+ const gradient = ctx.createLinearGradient(start.x, start.y, end.x, end.y);
2252
+ gradient.addColorStop(0, fromColor);
2253
+ gradient.addColorStop(0.5, baseColor);
2254
+ gradient.addColorStop(1, toColor);
2255
+ return gradient;
2256
+ }
2257
+ function resolveConnectionStroke(ctx, start, end, fromColor, baseColor, toColor) {
2258
+ if (!fromColor || !toColor) {
2259
+ return baseColor;
2260
+ }
2261
+ return createConnectionGradient(ctx, start, end, fromColor, baseColor, toColor);
2262
+ }
2263
+ function drawOrthogonalPathWithStroke(ctx, from, to, style, stroke) {
2264
+ const midX = (from.x + to.x) / 2;
2265
+ ctx.strokeStyle = stroke;
2266
+ ctx.lineWidth = style.width;
2267
+ ctx.setLineDash(style.dash ?? []);
2268
+ ctx.beginPath();
2269
+ ctx.moveTo(from.x, from.y);
2270
+ ctx.lineTo(midX, from.y);
2271
+ ctx.lineTo(midX, to.y);
2272
+ ctx.lineTo(to.x, to.y);
2273
+ ctx.stroke();
2274
+ }
2275
+ function drawCubicInterpolatedPath(ctx, points, style, stroke) {
2229
2276
  if (points.length < 2) {
2230
2277
  return;
2231
2278
  }
2232
- ctx.strokeStyle = style.color;
2279
+ ctx.strokeStyle = stroke;
2233
2280
  ctx.lineWidth = style.width;
2234
2281
  ctx.setLineDash(style.dash ?? []);
2235
2282
  ctx.beginPath();
@@ -2300,7 +2347,8 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
2300
2347
  conn.fromAnchor,
2301
2348
  conn.toAnchor
2302
2349
  );
2303
- ctx.strokeStyle = style.color;
2350
+ const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
2351
+ ctx.strokeStyle = stroke;
2304
2352
  ctx.lineWidth = style.width;
2305
2353
  ctx.setLineDash(style.dash ?? []);
2306
2354
  ctx.beginPath();
@@ -2342,7 +2390,8 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
2342
2390
  );
2343
2391
  const [p0, cp1, cp2, pMid] = first;
2344
2392
  const [, cp3, cp4, p3] = second;
2345
- ctx.strokeStyle = style.color;
2393
+ const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
2394
+ ctx.strokeStyle = stroke;
2346
2395
  ctx.lineWidth = style.width;
2347
2396
  ctx.setLineDash(style.dash ?? []);
2348
2397
  ctx.beginPath();
@@ -2385,10 +2434,18 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
2385
2434
  endPoint = linePoints[linePoints.length - 1] ?? linePoints[0];
2386
2435
  startAngle = Math.atan2(startSegment.y - linePoints[0].y, startSegment.x - linePoints[0].x) + Math.PI;
2387
2436
  endAngle = Math.atan2(endPoint.y - endStart.y, endPoint.x - endStart.x);
2437
+ const stroke = resolveConnectionStroke(
2438
+ ctx,
2439
+ startPoint,
2440
+ endPoint,
2441
+ conn.fromColor,
2442
+ style.color,
2443
+ conn.toColor
2444
+ );
2388
2445
  if (useElkRoute) {
2389
- drawCubicInterpolatedPath(ctx, linePoints, style);
2446
+ drawCubicInterpolatedPath(ctx, linePoints, style, stroke);
2390
2447
  } else {
2391
- drawOrthogonalPath(ctx, startPoint, endPoint, style);
2448
+ drawOrthogonalPathWithStroke(ctx, startPoint, endPoint, style, stroke);
2392
2449
  }
2393
2450
  labelPoint = pointAlongPolyline(linePoints, labelT);
2394
2451
  }
@@ -2693,6 +2750,9 @@ function measureTextBounds(ctx, options) {
2693
2750
  function angleBetween(from, to) {
2694
2751
  return Math.atan2(to.y - from.y, to.x - from.x);
2695
2752
  }
2753
+ function degreesToRadians(angle) {
2754
+ return angle * Math.PI / 180;
2755
+ }
2696
2756
  function pathBounds(operations) {
2697
2757
  let minX = Number.POSITIVE_INFINITY;
2698
2758
  let minY = Number.POSITIVE_INFINITY;
@@ -2930,6 +2990,34 @@ function renderDrawCommands(ctx, commands, theme) {
2930
2990
  });
2931
2991
  break;
2932
2992
  }
2993
+ case "arc": {
2994
+ const startAngle = degreesToRadians(command.startAngle);
2995
+ const endAngle = degreesToRadians(command.endAngle);
2996
+ withOpacity(ctx, command.opacity, () => {
2997
+ applyDrawShadow(ctx, command.shadow);
2998
+ ctx.beginPath();
2999
+ ctx.setLineDash(command.dash ?? []);
3000
+ ctx.lineWidth = command.width;
3001
+ ctx.strokeStyle = command.color;
3002
+ ctx.arc(command.center.x, command.center.y, command.radius, startAngle, endAngle);
3003
+ ctx.stroke();
3004
+ });
3005
+ rendered.push({
3006
+ id,
3007
+ kind: "draw",
3008
+ bounds: expandRect(
3009
+ {
3010
+ x: command.center.x - command.radius,
3011
+ y: command.center.y - command.radius,
3012
+ width: command.radius * 2,
3013
+ height: command.radius * 2
3014
+ },
3015
+ command.width / 2
3016
+ ),
3017
+ foregroundColor: command.color
3018
+ });
3019
+ break;
3020
+ }
2933
3021
  case "bezier": {
2934
3022
  const points = command.points;
2935
3023
  withOpacity(ctx, command.opacity, () => {
@@ -3086,6 +3174,84 @@ function renderDrawCommands(ctx, commands, theme) {
3086
3174
  });
3087
3175
  break;
3088
3176
  }
3177
+ case "text-row": {
3178
+ const segments = command.segments;
3179
+ if (segments.length === 0) break;
3180
+ const resolveSegment = (seg) => ({
3181
+ text: seg.text,
3182
+ fontSize: seg.fontSize ?? command.defaultFontSize,
3183
+ fontWeight: seg.fontWeight ?? command.defaultFontWeight,
3184
+ fontFamily: resolveDrawFont(theme, seg.fontFamily ?? command.defaultFontFamily),
3185
+ color: seg.color ?? command.defaultColor
3186
+ });
3187
+ const measured = [];
3188
+ let totalWidth = 0;
3189
+ let maxAscent = 0;
3190
+ let maxDescent = 0;
3191
+ for (const seg of segments) {
3192
+ const resolved = resolveSegment(seg);
3193
+ applyFont(ctx, {
3194
+ size: resolved.fontSize,
3195
+ weight: resolved.fontWeight,
3196
+ family: resolved.fontFamily
3197
+ });
3198
+ const metrics = ctx.measureText(resolved.text);
3199
+ const width = metrics.width;
3200
+ const ascent = metrics.actualBoundingBoxAscent || 0;
3201
+ const descent = metrics.actualBoundingBoxDescent || 0;
3202
+ totalWidth += width;
3203
+ maxAscent = Math.max(maxAscent, ascent);
3204
+ maxDescent = Math.max(maxDescent, descent);
3205
+ measured.push({ width, resolved });
3206
+ }
3207
+ let cursorX;
3208
+ if (command.align === "center") {
3209
+ cursorX = command.x - totalWidth / 2;
3210
+ } else if (command.align === "right") {
3211
+ cursorX = command.x - totalWidth;
3212
+ } else {
3213
+ cursorX = command.x;
3214
+ }
3215
+ const startX = cursorX;
3216
+ withOpacity(ctx, command.opacity, () => {
3217
+ ctx.textBaseline = command.baseline;
3218
+ for (const { width, resolved } of measured) {
3219
+ applyFont(ctx, {
3220
+ size: resolved.fontSize,
3221
+ weight: resolved.fontWeight,
3222
+ family: resolved.fontFamily
3223
+ });
3224
+ ctx.fillStyle = resolved.color;
3225
+ ctx.textAlign = "left";
3226
+ ctx.fillText(resolved.text, cursorX, command.y);
3227
+ cursorX += width;
3228
+ }
3229
+ });
3230
+ const height = Math.max(1, maxAscent + maxDescent);
3231
+ let topY;
3232
+ if (command.baseline === "top") {
3233
+ topY = command.y;
3234
+ } else if (command.baseline === "middle") {
3235
+ topY = command.y - height / 2;
3236
+ } else if (command.baseline === "bottom") {
3237
+ topY = command.y - height;
3238
+ } else {
3239
+ topY = command.y - maxAscent;
3240
+ }
3241
+ rendered.push({
3242
+ id,
3243
+ kind: "draw",
3244
+ bounds: {
3245
+ x: startX,
3246
+ y: topY,
3247
+ width: Math.max(1, totalWidth),
3248
+ height
3249
+ },
3250
+ foregroundColor: command.defaultColor,
3251
+ backgroundColor: theme.background
3252
+ });
3253
+ break;
3254
+ }
3089
3255
  }
3090
3256
  }
3091
3257
  return rendered;
@@ -3435,6 +3601,21 @@ var drawLineSchema = z2.object({
3435
3601
  opacity: z2.number().min(0).max(1).default(1),
3436
3602
  shadow: drawShadowSchema.optional()
3437
3603
  }).strict();
3604
+ var drawArcSchema = z2.object({
3605
+ type: z2.literal("arc"),
3606
+ center: z2.object({
3607
+ x: z2.number(),
3608
+ y: z2.number()
3609
+ }).strict(),
3610
+ radius: z2.number().positive(),
3611
+ startAngle: z2.number(),
3612
+ endAngle: z2.number(),
3613
+ color: colorHexSchema2.default("#FFFFFF"),
3614
+ width: z2.number().min(0.5).max(32).default(2),
3615
+ dash: z2.array(z2.number()).max(6).optional(),
3616
+ opacity: z2.number().min(0).max(1).default(1),
3617
+ shadow: drawShadowSchema.optional()
3618
+ }).strict();
3438
3619
  var drawPointSchema = z2.object({
3439
3620
  x: z2.number(),
3440
3621
  y: z2.number()
@@ -3519,6 +3700,7 @@ var drawCommandSchema = z2.discriminatedUnion("type", [
3519
3700
  drawCircleSchema,
3520
3701
  drawTextSchema,
3521
3702
  drawLineSchema,
3703
+ drawArcSchema,
3522
3704
  drawBezierSchema,
3523
3705
  drawPathSchema,
3524
3706
  drawBadgeSchema,
@@ -3654,6 +3836,8 @@ var connectionElementSchema = z2.object({
3654
3836
  label: z2.string().min(1).max(200).optional(),
3655
3837
  labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
3656
3838
  color: colorHexSchema2.optional(),
3839
+ fromColor: colorHexSchema2.optional(),
3840
+ toColor: colorHexSchema2.optional(),
3657
3841
  width: z2.number().min(0.5).max(10).optional(),
3658
3842
  strokeWidth: z2.number().min(0.5).max(10).default(2),
3659
3843
  arrowSize: z2.number().min(4).max(32).optional(),
@@ -4039,6 +4223,18 @@ async function renderDesign(input, options = {}) {
4039
4223
  const specHash = computeSpecHash(spec);
4040
4224
  const generatorVersion = options.generatorVersion ?? DEFAULT_GENERATOR_VERSION;
4041
4225
  const renderedAt = options.renderedAt ?? (/* @__PURE__ */ new Date()).toISOString();
4226
+ const iteration = options.iteration;
4227
+ if (iteration) {
4228
+ if (!Number.isInteger(iteration.iteration) || iteration.iteration <= 0) {
4229
+ throw new Error("Iteration metadata requires iteration to be a positive integer.");
4230
+ }
4231
+ if (iteration.maxIterations != null && (!Number.isInteger(iteration.maxIterations) || iteration.maxIterations <= 0)) {
4232
+ throw new Error("Iteration metadata requires maxIterations to be a positive integer.");
4233
+ }
4234
+ if (iteration.maxIterations != null && iteration.maxIterations < iteration.iteration) {
4235
+ throw new Error("Iteration metadata requires maxIterations to be >= iteration.");
4236
+ }
4237
+ }
4042
4238
  const renderScale = resolveRenderScale(spec);
4043
4239
  const canvas = createCanvas(spec.canvas.width * renderScale, spec.canvas.height * renderScale);
4044
4240
  const ctx = canvas.getContext("2d");
@@ -4279,7 +4475,8 @@ async function renderDesign(input, options = {}) {
4279
4475
  layout: {
4280
4476
  safeFrame,
4281
4477
  elements
4282
- }
4478
+ },
4479
+ ...iteration ? { iteration } : {}
4283
4480
  };
4284
4481
  return {
4285
4482
  png: pngBuffer,