@efectoapp/mcp-server 0.1.19 → 0.1.21

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.
@@ -85,7 +85,7 @@ function getDefaultAnimationParams(type) {
85
85
  const base = {
86
86
  type,
87
87
  enabled: true,
88
- speed: 1.0,
88
+ speed: 0.2,
89
89
  amplitude: 50,
90
90
  staggerDelay: 150,
91
91
  staggerMode: 'sequential',
@@ -289,8 +289,118 @@ function createDefaultBackgroundLayer(backgroundColor) {
289
289
  },
290
290
  contentType: 'solid',
291
291
  solidColor: backgroundColor,
292
+ fill: {
293
+ type: 'solid',
294
+ color: backgroundColor,
295
+ opacity: 1,
296
+ },
297
+ };
298
+ }
299
+ const DEFAULT_LINEAR_GRADIENT_FILL = {
300
+ type: 'linear',
301
+ angle: 135,
302
+ stops: [
303
+ { color: '#7c3aed', opacity: 1, position: 0 },
304
+ { color: '#22d3ee', opacity: 1, position: 1 },
305
+ ],
306
+ };
307
+ const DEFAULT_RADIAL_GRADIENT_FILL = {
308
+ type: 'radial',
309
+ center: { x: 0.5, y: 0.5 },
310
+ radius: 0.8,
311
+ stops: [
312
+ { color: '#a78bfa', opacity: 1, position: 0 },
313
+ { color: '#0f172a', opacity: 1, position: 1 },
314
+ ],
315
+ };
316
+ const MIN_GRADIENT_STOPS = 2;
317
+ const MAX_GRADIENT_STOPS = 8;
318
+ function clamp01(value) {
319
+ if (!Number.isFinite(value))
320
+ return 0;
321
+ return Math.min(1, Math.max(0, value));
322
+ }
323
+ function normalizeAngle(value) {
324
+ if (!Number.isFinite(value))
325
+ return 0;
326
+ const normalized = value % 360;
327
+ return normalized < 0 ? normalized + 360 : normalized;
328
+ }
329
+ function clampRadius(value) {
330
+ if (!Number.isFinite(value))
331
+ return DEFAULT_RADIAL_GRADIENT_FILL.radius;
332
+ return Math.min(2, Math.max(0.05, value));
333
+ }
334
+ function isHexColor(value) {
335
+ return /^#[0-9a-fA-F]{3}$/.test(value) || /^#[0-9a-fA-F]{6}$/.test(value);
336
+ }
337
+ function normalizeGradientStop(stop, fallback) {
338
+ return {
339
+ color: isHexColor(stop.color) ? stop.color : fallback.color,
340
+ opacity: clamp01(stop.opacity),
341
+ position: clamp01(stop.position),
342
+ };
343
+ }
344
+ function normalizeGradientStops(stops, fallbackStops) {
345
+ const fallbackStart = fallbackStops[0];
346
+ const fallbackEnd = fallbackStops[fallbackStops.length - 1] ?? fallbackStart;
347
+ const input = Array.isArray(stops) ? stops : [];
348
+ const normalized = input
349
+ .map((stop, index) => normalizeGradientStop(stop, fallbackStops[Math.min(index, fallbackStops.length - 1)] ?? fallbackEnd))
350
+ .slice(0, MAX_GRADIENT_STOPS);
351
+ if (normalized.length < MIN_GRADIENT_STOPS) {
352
+ if (normalized.length === 0) {
353
+ normalized.push(normalizeGradientStop(fallbackStart, fallbackStart), normalizeGradientStop(fallbackEnd, fallbackEnd));
354
+ }
355
+ else if (normalized.length === 1) {
356
+ normalized.push(normalizeGradientStop(fallbackEnd, fallbackEnd));
357
+ }
358
+ }
359
+ return normalized
360
+ .map((stop, index) => ({ stop, index }))
361
+ .sort((a, b) => a.stop.position - b.stop.position || a.index - b.index)
362
+ .map(({ stop }) => stop);
363
+ }
364
+ function normalizeLinearGradientFill(fill) {
365
+ const fallback = DEFAULT_LINEAR_GRADIENT_FILL;
366
+ return {
367
+ type: 'linear',
368
+ angle: normalizeAngle(fill.angle),
369
+ stops: normalizeGradientStops(fill.stops, fallback.stops),
292
370
  };
293
371
  }
372
+ function normalizeRadialGradientFill(fill) {
373
+ const fallback = DEFAULT_RADIAL_GRADIENT_FILL;
374
+ return {
375
+ type: 'radial',
376
+ center: {
377
+ x: clamp01(fill.center?.x ?? fallback.center.x),
378
+ y: clamp01(fill.center?.y ?? fallback.center.y),
379
+ },
380
+ radius: clampRadius(fill.radius),
381
+ stops: normalizeGradientStops(fill.stops, fallback.stops),
382
+ };
383
+ }
384
+ function parseGradientStopsInput(input, fallbackStops) {
385
+ if (!Array.isArray(input))
386
+ return null;
387
+ const parsed = input
388
+ .map((item, index) => {
389
+ if (!item || typeof item !== 'object')
390
+ return null;
391
+ const candidate = item;
392
+ const fallback = fallbackStops[Math.min(index, fallbackStops.length - 1)] ?? fallbackStops[0];
393
+ const opacityRaw = typeof candidate.opacity === 'number' ? candidate.opacity : Number.parseFloat(String(candidate.opacity ?? ''));
394
+ const positionRaw = typeof candidate.position === 'number' ? candidate.position : Number.parseFloat(String(candidate.position ?? ''));
395
+ return {
396
+ color: typeof candidate.color === 'string' && isHexColor(candidate.color) ? candidate.color : fallback.color,
397
+ opacity: clamp01(Number.isFinite(opacityRaw) ? opacityRaw : fallback.opacity),
398
+ position: clamp01(Number.isFinite(positionRaw) ? positionRaw : fallback.position),
399
+ };
400
+ })
401
+ .filter((stop) => stop !== null);
402
+ return parsed.length >= MIN_GRADIENT_STOPS ? parsed : null;
403
+ }
294
404
  // Tool definitions
295
405
  exports.stateTools = [
296
406
  {
@@ -301,7 +411,7 @@ exports.stateTools = [
301
411
  properties: {
302
412
  aspectRatio: {
303
413
  type: 'string',
304
- enum: ['16:9', '9:16', '1:1', '4:3', 'full'],
414
+ enum: ['16:9', '9:16', '1:1', '4:3', '3:4', 'full'],
305
415
  description: 'Canvas aspect ratio (default: 9:16 for portrait poster)',
306
416
  default: '9:16',
307
417
  },
@@ -316,13 +426,13 @@ exports.stateTools = [
316
426
  },
317
427
  {
318
428
  name: 'set_background',
319
- description: 'Configure the background layer (always at index 0). Can be solid color, image, or video.',
429
+ description: 'Configure the background layer (always at index 0). Can be solid fill, linear/radial gradient fill, image, or video.',
320
430
  inputSchema: {
321
431
  type: 'object',
322
432
  properties: {
323
433
  type: {
324
434
  type: 'string',
325
- enum: ['solid', 'image', 'video'],
435
+ enum: ['solid', 'gradient', 'image', 'video'],
326
436
  description: 'Background content type',
327
437
  default: 'solid',
328
438
  },
@@ -330,6 +440,50 @@ exports.stateTools = [
330
440
  type: 'string',
331
441
  description: 'Solid background color in hex (for type: solid)',
332
442
  },
443
+ gradientStartColor: {
444
+ type: 'string',
445
+ description: 'Gradient start color in hex (for type: gradient)',
446
+ },
447
+ gradientEndColor: {
448
+ type: 'string',
449
+ description: 'Gradient end color in hex (for type: gradient)',
450
+ },
451
+ gradientStops: {
452
+ type: 'array',
453
+ description: 'Optional gradient stops array (2-8 items). If provided, overrides start/end colors.',
454
+ items: {
455
+ type: 'object',
456
+ properties: {
457
+ color: { type: 'string', description: 'Stop color in hex' },
458
+ opacity: { type: 'number', description: 'Stop opacity (0-1)' },
459
+ position: { type: 'number', description: 'Stop position (0-1)' },
460
+ },
461
+ required: ['color', 'position'],
462
+ },
463
+ minItems: 2,
464
+ maxItems: 8,
465
+ },
466
+ gradientStyle: {
467
+ type: 'string',
468
+ enum: ['linear', 'radial'],
469
+ description: 'Gradient style (for type: gradient)',
470
+ },
471
+ gradientAngle: {
472
+ type: 'number',
473
+ description: 'Linear gradient angle in degrees (for linear style)',
474
+ },
475
+ gradientCenterX: {
476
+ type: 'number',
477
+ description: 'Radial center X (0-1, for radial style)',
478
+ },
479
+ gradientCenterY: {
480
+ type: 'number',
481
+ description: 'Radial center Y (0-1, for radial style)',
482
+ },
483
+ gradientRadius: {
484
+ type: 'number',
485
+ description: 'Radial radius scale (for radial style)',
486
+ },
333
487
  imageUrl: {
334
488
  type: 'string',
335
489
  description: 'Image URL (for type: image)',
@@ -369,13 +523,19 @@ exports.stateTools = [
369
523
  opacity: { type: 'number', description: 'Opacity 0-1', default: 1 },
370
524
  // Text layer properties
371
525
  content: { type: 'string', description: 'Text content (required for text layers)' },
526
+ textType: { type: 'string', enum: ['point', 'frame'], description: 'Text authoring mode: "frame" for layout copy with wrapping (recommended), "point" for scalable text', default: 'frame' },
372
527
  fontFamily: { type: 'string', description: 'Font family name', default: 'DM Sans' },
373
528
  fontSize: { type: 'number', description: 'Font size in pixels', default: 72 },
374
529
  fontWeight: { type: 'string', enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], description: 'Font weight', default: 'bold' },
375
530
  color: { type: 'string', description: 'Text color in hex', default: '#ffffff' },
376
531
  textAlign: { type: 'string', enum: ['left', 'center', 'right'], description: 'Text alignment', default: 'center' },
377
- letterSpacing: { type: 'number', description: 'Letter spacing in pixels', default: 0 },
378
- lineHeight: { type: 'number', description: 'Line height multiplier', default: 1.2 },
532
+ letterSpacing: { type: 'number', description: 'Letter spacing in pixels (negative for tighter, e.g., -1 to -2 for headlines)', default: 0 },
533
+ lineHeight: { type: 'number', description: 'Line height multiplier (0.9-1.0 for tight headlines, 1.3-1.5 for body)', default: 1.2 },
534
+ textTransformMode: { type: 'string', enum: ['scale', 'box'], description: 'Text sizing mode: "box" for natural wrapping (recommended), "scale" to stretch to fit', default: 'box' },
535
+ textBoxWidth: { type: 'number', description: 'Width in pixels for text wrapping when using box mode (e.g., 300-600 for readable lines)' },
536
+ frameWidthPx: { type: 'number', description: 'Frame width in CSS pixels for frame text (resizing changes this, not fontSize)' },
537
+ frameHeightPx: { type: 'number', description: 'Frame height in CSS pixels for frame text' },
538
+ autoSize: { type: 'string', enum: ['none', 'height', 'widthAndHeight'], description: 'Auto-size mode for frame text' },
379
539
  // Text animation properties (use list_text_animations to see all options)
380
540
  animationType: {
381
541
  type: 'string',
@@ -453,9 +613,72 @@ exports.stateTools = [
453
613
  required: ['type'],
454
614
  },
455
615
  },
616
+ {
617
+ name: 'add_group',
618
+ description: 'Add a flexbox container with child layers. Works like CSS flexbox: set direction (column/row), alignment, and gap. Children are positioned automatically by the layout engine — do NOT set child x/y. Use this for ALL text layout (heading + subheading, title + date, etc.).',
619
+ inputSchema: {
620
+ type: 'object',
621
+ properties: {
622
+ name: { type: 'string', description: 'Group name' },
623
+ layoutMode: { type: 'string', enum: ['row', 'column'], description: 'Flex direction: "column" stacks vertically (default, most common), "row" places side by side', default: 'column' },
624
+ widthMode: { type: 'string', enum: ['hug', 'fixed'], description: 'Horizontal sizing mode: hug contents or fixed width' },
625
+ heightMode: { type: 'string', enum: ['hug', 'fixed'], description: 'Vertical sizing mode: hug contents or fixed height' },
626
+ gap: { type: 'number', description: 'Space between children (like CSS gap). 0.02=small, 0.04=medium, 0.06=large', default: 0.02 },
627
+ justifyContent: { type: 'string', enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'], description: 'Main-axis alignment (like CSS justify-content)', default: 'start' },
628
+ alignItems: { type: 'string', enum: ['start', 'center', 'end'], description: 'Cross-axis alignment (like CSS align-items): "start"=left, "center"=centered, "end"=right', default: 'center' },
629
+ wrap: { type: 'boolean', description: 'Wrap children to next line on overflow (like CSS flex-wrap)', default: false },
630
+ padding: { type: 'number', description: 'Inner padding (like CSS padding)', default: 0 },
631
+ paddingTop: { type: 'number', description: 'Top padding override' },
632
+ paddingRight: { type: 'number', description: 'Right padding override' },
633
+ paddingBottom: { type: 'number', description: 'Bottom padding override' },
634
+ paddingLeft: { type: 'number', description: 'Left padding override' },
635
+ fillColor: { type: 'string', description: 'Group background color (hex)' },
636
+ fillOpacity: { type: 'number', description: 'Group background opacity (0-1)' },
637
+ cornerRadius: { type: 'number', description: 'Corner radius' },
638
+ x: { type: 'number', description: 'Group X position (-1 to 1, 0 is center)', default: 0 },
639
+ y: { type: 'number', description: 'Group Y position (-1 to 1, POSITIVE is UP/TOP). 0.15=upper, 0=center, -0.3=lower', default: 0 },
640
+ width: { type: 'number', description: 'Group width (0-1 relative to canvas)', default: 0.6 },
641
+ height: { type: 'number', description: 'Group height (0-1 relative to canvas)', default: 0.4 },
642
+ rotation: { type: 'number', description: 'Rotation in degrees', default: 0 },
643
+ opacity: { type: 'number', description: 'Opacity 0-1', default: 1 },
644
+ children: {
645
+ type: 'array',
646
+ description: 'Child layers to add inside the group',
647
+ items: {
648
+ type: 'object',
649
+ properties: {
650
+ type: { type: 'string', enum: ['text', 'image', 'video'], description: 'Child layer type' },
651
+ name: { type: 'string', description: 'Child layer name' },
652
+ content: { type: 'string', description: 'Text content' },
653
+ fontFamily: { type: 'string', description: 'Font family' },
654
+ fontSize: { type: 'number', description: 'Font size in pixels' },
655
+ fontWeight: { type: 'string', description: 'Font weight' },
656
+ color: { type: 'string', description: 'Text color in hex' },
657
+ textAlign: { type: 'string', enum: ['left', 'center', 'right'], description: 'Text alignment' },
658
+ letterSpacing: { type: 'number', description: 'Letter spacing' },
659
+ lineHeight: { type: 'number', description: 'Line height multiplier' },
660
+ textType: { type: 'string', enum: ['point', 'frame'], description: 'Text authoring mode' },
661
+ textTransformMode: { type: 'string', enum: ['scale', 'box'], description: 'Text sizing mode' },
662
+ textBoxWidth: { type: 'number', description: 'Width for text wrapping' },
663
+ frameWidthPx: { type: 'number', description: 'Frame width in CSS pixels' },
664
+ frameHeightPx: { type: 'number', description: 'Frame height in CSS pixels' },
665
+ autoSize: { type: 'string', enum: ['none', 'height', 'widthAndHeight'], description: 'Auto-size mode' },
666
+ mediaUrl: { type: 'string', description: 'Media URL (for image/video)' },
667
+ objectFit: { type: 'string', enum: ['cover', 'contain'], description: 'Object fit mode' },
668
+ width: { type: 'number', description: 'Child width (0-1 relative to group)' },
669
+ height: { type: 'number', description: 'Child height (0-1 relative to group)' },
670
+ opacity: { type: 'number', description: 'Opacity 0-1' },
671
+ },
672
+ required: ['type'],
673
+ },
674
+ },
675
+ },
676
+ required: ['children'],
677
+ },
678
+ },
456
679
  {
457
680
  name: 'modify_layer',
458
- description: 'Modify an existing layer by ID',
681
+ description: 'Modify an existing layer by ID. Supports text, image, video, and group properties.',
459
682
  inputSchema: {
460
683
  type: 'object',
461
684
  properties: {
@@ -472,6 +695,7 @@ exports.stateTools = [
472
695
  name: { type: 'string', description: 'Layer name' },
473
696
  // Text layer properties
474
697
  content: { type: 'string' },
698
+ textType: { type: 'string', enum: ['point', 'frame'], description: 'Text authoring mode' },
475
699
  fontFamily: { type: 'string' },
476
700
  fontSize: { type: 'number' },
477
701
  fontWeight: { type: 'string' },
@@ -479,6 +703,11 @@ exports.stateTools = [
479
703
  textAlign: { type: 'string' },
480
704
  letterSpacing: { type: 'number' },
481
705
  lineHeight: { type: 'number' },
706
+ textTransformMode: { type: 'string', enum: ['scale', 'box'] },
707
+ textBoxWidth: { type: 'number' },
708
+ frameWidthPx: { type: 'number' },
709
+ frameHeightPx: { type: 'number' },
710
+ autoSize: { type: 'string', enum: ['none', 'height', 'widthAndHeight'] },
482
711
  // Text animation properties (all type-specific params supported - see add_layer for descriptions)
483
712
  animationType: {
484
713
  type: 'string',
@@ -535,6 +764,22 @@ exports.stateTools = [
535
764
  // Media properties
536
765
  mediaUrl: { type: 'string' },
537
766
  objectFit: { type: 'string' },
767
+ // Group layout properties
768
+ layoutMode: { type: 'string', enum: ['row', 'column'], description: 'Group layout direction' },
769
+ widthMode: { type: 'string', enum: ['hug', 'fixed'], description: 'Group horizontal sizing mode' },
770
+ heightMode: { type: 'string', enum: ['hug', 'fixed'], description: 'Group vertical sizing mode' },
771
+ gap: { type: 'number', description: 'Gap between children (normalized 0-1)' },
772
+ justifyContent: { type: 'string', enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'], description: 'Group main-axis alignment' },
773
+ alignItems: { type: 'string', enum: ['start', 'center', 'end'], description: 'Group cross-axis alignment' },
774
+ wrap: { type: 'boolean', description: 'Group wrap children' },
775
+ padding: { type: 'number', description: 'Group uniform padding' },
776
+ paddingTop: { type: 'number', description: 'Group top padding' },
777
+ paddingRight: { type: 'number', description: 'Group right padding' },
778
+ paddingBottom: { type: 'number', description: 'Group bottom padding' },
779
+ paddingLeft: { type: 'number', description: 'Group left padding' },
780
+ fillColor: { type: 'string', description: 'Group background color' },
781
+ fillOpacity: { type: 'number', description: 'Group background opacity' },
782
+ cornerRadius: { type: 'number', description: 'Group corner radius' },
538
783
  },
539
784
  required: ['layerId'],
540
785
  },
@@ -765,13 +1010,79 @@ async function handleStateTool(name, args) {
765
1010
  content: [{ type: 'text', text: 'Error: Background layer not found at index 0' }],
766
1011
  };
767
1012
  }
768
- const contentType = params.type || 'solid';
1013
+ const requestedType = params.type || 'solid';
1014
+ const contentType = requestedType === 'gradient' ? 'solid' : requestedType;
769
1015
  bgLayer.contentType = contentType;
770
- if (contentType === 'solid') {
771
- if (params.color) {
772
- bgLayer.solidColor = params.color;
773
- currentPoster.canvas.backgroundColor = params.color;
774
- }
1016
+ if (requestedType === 'gradient') {
1017
+ const gradientStyle = params.gradientStyle === 'radial' ? 'radial' : 'linear';
1018
+ const explicitStops = parseGradientStopsInput(params.gradientStops, gradientStyle === 'radial' ? DEFAULT_RADIAL_GRADIENT_FILL.stops : DEFAULT_LINEAR_GRADIENT_FILL.stops);
1019
+ const gradient = gradientStyle === 'radial'
1020
+ ? normalizeRadialGradientFill({
1021
+ type: 'radial',
1022
+ center: {
1023
+ x: Number.isFinite(params.gradientCenterX)
1024
+ ? params.gradientCenterX
1025
+ : DEFAULT_RADIAL_GRADIENT_FILL.center.x,
1026
+ y: Number.isFinite(params.gradientCenterY)
1027
+ ? params.gradientCenterY
1028
+ : DEFAULT_RADIAL_GRADIENT_FILL.center.y,
1029
+ },
1030
+ radius: Number.isFinite(params.gradientRadius)
1031
+ ? params.gradientRadius
1032
+ : DEFAULT_RADIAL_GRADIENT_FILL.radius,
1033
+ stops: explicitStops ?? [
1034
+ {
1035
+ color: params.gradientStartColor || DEFAULT_RADIAL_GRADIENT_FILL.stops[0].color,
1036
+ opacity: DEFAULT_RADIAL_GRADIENT_FILL.stops[0].opacity,
1037
+ position: DEFAULT_RADIAL_GRADIENT_FILL.stops[0].position,
1038
+ },
1039
+ {
1040
+ color: params.gradientEndColor || DEFAULT_RADIAL_GRADIENT_FILL.stops[1].color,
1041
+ opacity: DEFAULT_RADIAL_GRADIENT_FILL.stops[1].opacity,
1042
+ position: DEFAULT_RADIAL_GRADIENT_FILL.stops[1].position,
1043
+ },
1044
+ ],
1045
+ })
1046
+ : normalizeLinearGradientFill({
1047
+ type: 'linear',
1048
+ angle: Number.isFinite(params.gradientAngle)
1049
+ ? params.gradientAngle
1050
+ : DEFAULT_LINEAR_GRADIENT_FILL.angle,
1051
+ stops: explicitStops ?? [
1052
+ {
1053
+ color: params.gradientStartColor || DEFAULT_LINEAR_GRADIENT_FILL.stops[0].color,
1054
+ opacity: DEFAULT_LINEAR_GRADIENT_FILL.stops[0].opacity,
1055
+ position: DEFAULT_LINEAR_GRADIENT_FILL.stops[0].position,
1056
+ },
1057
+ {
1058
+ color: params.gradientEndColor || DEFAULT_LINEAR_GRADIENT_FILL.stops[1].color,
1059
+ opacity: DEFAULT_LINEAR_GRADIENT_FILL.stops[1].opacity,
1060
+ position: DEFAULT_LINEAR_GRADIENT_FILL.stops[1].position,
1061
+ },
1062
+ ],
1063
+ });
1064
+ bgLayer.fill = gradient;
1065
+ bgLayer.solidColor = gradient.stops[0].color;
1066
+ currentPoster.canvas.backgroundColor = gradient.stops[0].color;
1067
+ delete bgLayer.inputMedia;
1068
+ }
1069
+ else if (contentType === 'solid') {
1070
+ const solidColor = params.color ||
1071
+ (bgLayer.fill?.type === 'solid'
1072
+ ? bgLayer.fill.color
1073
+ : bgLayer.fill
1074
+ ? bgLayer.fill.stops[0]?.color
1075
+ : undefined) ||
1076
+ bgLayer.solidColor ||
1077
+ currentPoster.canvas.backgroundColor ||
1078
+ '#5c5c5c';
1079
+ bgLayer.solidColor = solidColor;
1080
+ bgLayer.fill = {
1081
+ type: 'solid',
1082
+ color: solidColor,
1083
+ opacity: 1,
1084
+ };
1085
+ currentPoster.canvas.backgroundColor = solidColor;
775
1086
  delete bgLayer.inputMedia;
776
1087
  }
777
1088
  else if (contentType === 'image' && params.imageUrl) {
@@ -843,11 +1154,17 @@ async function handleStateTool(name, args) {
843
1154
  let layer;
844
1155
  if (layerType === 'text') {
845
1156
  const animation = buildAnimationSettings(params);
1157
+ // textType is canonical, textTransformMode is legacy. textType wins if both provided.
1158
+ const requestedTextType = params.textType;
1159
+ const requestedTransformMode = params.textTransformMode;
1160
+ const textType = requestedTextType ?? (requestedTransformMode === 'scale' ? 'point' : 'frame');
1161
+ const textTransformMode = textType === 'point' ? 'scale' : 'box';
846
1162
  // Use same defaults as app's DEFAULT_TEXT_LAYER to avoid encoding unnecessary params in URLs
847
1163
  layer = {
848
1164
  ...baseLayer,
849
1165
  type: 'text',
850
1166
  content: params.content || 'Text',
1167
+ textType,
851
1168
  fontFamily: normalizeFontFamily(params.fontFamily),
852
1169
  fontSize: params.fontSize || 72,
853
1170
  fontWeight: normalizeFontWeight(params.fontWeight),
@@ -855,7 +1172,12 @@ async function handleStateTool(name, args) {
855
1172
  textAlign: params.textAlign || 'center',
856
1173
  letterSpacing: params.letterSpacing ?? 0,
857
1174
  lineHeight: params.lineHeight ?? 1.2,
858
- ...(animation && { animation }),
1175
+ textTransformMode,
1176
+ ...(params.textBoxWidth ? { textBoxWidth: params.textBoxWidth } : {}),
1177
+ ...(params.frameWidthPx ? { frameWidthPx: params.frameWidthPx } : {}),
1178
+ ...(params.frameHeightPx ? { frameHeightPx: params.frameHeightPx } : {}),
1179
+ ...(params.autoSize ? { autoSize: params.autoSize } : {}),
1180
+ ...(animation ? { animation } : {}),
859
1181
  };
860
1182
  }
861
1183
  else if (layerType === 'image') {
@@ -910,6 +1232,154 @@ async function handleStateTool(name, args) {
910
1232
  ],
911
1233
  };
912
1234
  }
1235
+ case 'add_group': {
1236
+ if (!currentPoster) {
1237
+ return {
1238
+ content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
1239
+ };
1240
+ }
1241
+ const children = params.children;
1242
+ if (!children || !Array.isArray(children) || children.length === 0) {
1243
+ return {
1244
+ content: [{ type: 'text', text: 'Error: children array is required and must have at least 1 child' }],
1245
+ };
1246
+ }
1247
+ const groupId = generateId();
1248
+ const childLayers = [];
1249
+ for (const childParams of children) {
1250
+ const childType = childParams.type || 'text';
1251
+ const childId = generateId();
1252
+ const childBase = {
1253
+ id: childId,
1254
+ type: childType,
1255
+ name: childParams.name || `${childType.charAt(0).toUpperCase() + childType.slice(1)} Layer`,
1256
+ visible: true,
1257
+ locked: false,
1258
+ transform: {
1259
+ x: 0,
1260
+ y: 0,
1261
+ width: childParams.width ?? 1,
1262
+ height: childParams.height ?? 1,
1263
+ rotation: 0,
1264
+ opacity: childParams.opacity ?? 1,
1265
+ },
1266
+ };
1267
+ if (childType === 'text') {
1268
+ const requestedTextType = childParams.textType;
1269
+ const requestedTransformMode = childParams.textTransformMode;
1270
+ const textType = requestedTextType ?? (requestedTransformMode === 'scale' ? 'point' : 'frame');
1271
+ const textTransformMode = textType === 'point' ? 'scale' : 'box';
1272
+ childLayers.push({
1273
+ ...childBase,
1274
+ type: 'text',
1275
+ content: childParams.content || 'Text',
1276
+ textType,
1277
+ fontFamily: normalizeFontFamily(childParams.fontFamily),
1278
+ fontSize: childParams.fontSize || 72,
1279
+ fontWeight: normalizeFontWeight(childParams.fontWeight),
1280
+ color: childParams.color || '#ffffff',
1281
+ textAlign: childParams.textAlign || 'center',
1282
+ letterSpacing: childParams.letterSpacing ?? 0,
1283
+ lineHeight: childParams.lineHeight ?? 1.2,
1284
+ textTransformMode,
1285
+ ...(childParams.textBoxWidth ? { textBoxWidth: childParams.textBoxWidth } : {}),
1286
+ ...(childParams.frameWidthPx ? { frameWidthPx: childParams.frameWidthPx } : {}),
1287
+ ...(childParams.frameHeightPx ? { frameHeightPx: childParams.frameHeightPx } : {}),
1288
+ ...(childParams.autoSize ? { autoSize: childParams.autoSize } : {}),
1289
+ });
1290
+ }
1291
+ else if (childType === 'image') {
1292
+ if (!childParams.mediaUrl)
1293
+ continue;
1294
+ childLayers.push({
1295
+ ...childBase,
1296
+ type: 'image',
1297
+ mediaUrl: childParams.mediaUrl,
1298
+ objectFit: childParams.objectFit || 'cover',
1299
+ brightness: 1,
1300
+ contrast: 1,
1301
+ saturation: 1,
1302
+ });
1303
+ }
1304
+ else if (childType === 'video') {
1305
+ if (!childParams.mediaUrl)
1306
+ continue;
1307
+ childLayers.push({
1308
+ ...childBase,
1309
+ type: 'video',
1310
+ mediaUrl: childParams.mediaUrl,
1311
+ objectFit: childParams.objectFit || 'cover',
1312
+ brightness: 1,
1313
+ contrast: 1,
1314
+ saturation: 1,
1315
+ loop: true,
1316
+ playbackSpeed: 1,
1317
+ });
1318
+ }
1319
+ }
1320
+ if (childLayers.length === 0) {
1321
+ return {
1322
+ content: [{ type: 'text', text: 'Error: No valid child layers could be created' }],
1323
+ };
1324
+ }
1325
+ // Build padding
1326
+ const uniformPadding = params.padding ?? 0;
1327
+ const groupPadding = {
1328
+ top: params.paddingTop ?? uniformPadding,
1329
+ right: params.paddingRight ?? uniformPadding,
1330
+ bottom: params.paddingBottom ?? uniformPadding,
1331
+ left: params.paddingLeft ?? uniformPadding,
1332
+ };
1333
+ const layoutMode = params.layoutMode || 'column';
1334
+ const widthMode = params.widthMode ?? (params.width !== undefined ? 'fixed' : 'hug');
1335
+ const heightMode = params.heightMode ?? (params.height !== undefined ? 'fixed' : 'hug');
1336
+ const baseWidth = params.width ?? 0.6;
1337
+ const baseHeight = params.height ?? 0.4;
1338
+ const groupLayer = {
1339
+ id: groupId,
1340
+ type: 'group',
1341
+ name: params.name || 'Group',
1342
+ visible: true,
1343
+ locked: false,
1344
+ transform: {
1345
+ x: params.x ?? 0,
1346
+ y: params.y ?? 0,
1347
+ width: 1,
1348
+ height: 1,
1349
+ rotation: params.rotation ?? 0,
1350
+ opacity: params.opacity ?? 1,
1351
+ },
1352
+ children: childLayers,
1353
+ expanded: true,
1354
+ baseWidth,
1355
+ baseHeight,
1356
+ layoutMode,
1357
+ widthMode,
1358
+ heightMode,
1359
+ gap: params.gap ?? 0.02,
1360
+ justifyContent: params.justifyContent ?? 'start',
1361
+ alignItems: params.alignItems ?? 'center',
1362
+ wrap: params.wrap ?? false,
1363
+ padding: groupPadding,
1364
+ ...(params.fillColor ? { fillColor: params.fillColor } : {}),
1365
+ ...(params.fillOpacity !== undefined ? { fillOpacity: params.fillOpacity } : {}),
1366
+ ...(params.cornerRadius !== undefined ? { cornerRadius: params.cornerRadius } : {}),
1367
+ };
1368
+ currentPoster.layers.push(groupLayer);
1369
+ return {
1370
+ content: [
1371
+ {
1372
+ type: 'text',
1373
+ text: JSON.stringify({
1374
+ success: true,
1375
+ message: `Group added with ${childLayers.length} children (${groupLayer.layoutMode} layout)`,
1376
+ group: groupLayer,
1377
+ totalLayers: currentPoster.layers.length,
1378
+ }, null, 2),
1379
+ },
1380
+ ],
1381
+ };
1382
+ }
913
1383
  case 'modify_layer': {
914
1384
  if (!currentPoster) {
915
1385
  return {
@@ -962,6 +1432,22 @@ async function handleStateTool(name, args) {
962
1432
  textLayer.letterSpacing = params.letterSpacing;
963
1433
  if (params.lineHeight !== undefined)
964
1434
  textLayer.lineHeight = params.lineHeight;
1435
+ // Reconcile textType (canonical) vs textTransformMode (legacy). textType wins.
1436
+ if (params.textType !== undefined || params.textTransformMode !== undefined) {
1437
+ const resolvedTextType = params.textType !== undefined
1438
+ ? params.textType
1439
+ : (params.textTransformMode === 'scale' ? 'point' : 'frame');
1440
+ textLayer.textType = resolvedTextType;
1441
+ textLayer.textTransformMode = resolvedTextType === 'point' ? 'scale' : 'box';
1442
+ }
1443
+ if (params.textBoxWidth !== undefined)
1444
+ textLayer.textBoxWidth = params.textBoxWidth;
1445
+ if (params.frameWidthPx !== undefined)
1446
+ textLayer.frameWidthPx = params.frameWidthPx;
1447
+ if (params.frameHeightPx !== undefined)
1448
+ textLayer.frameHeightPx = params.frameHeightPx;
1449
+ if (params.autoSize !== undefined)
1450
+ textLayer.autoSize = params.autoSize;
965
1451
  // Update animation settings
966
1452
  if (params.animationType !== undefined) {
967
1453
  if (params.animationType === 'none') {
@@ -1095,6 +1581,54 @@ async function handleStateTool(name, args) {
1095
1581
  if (params.objectFit !== undefined)
1096
1582
  mediaLayer.objectFit = params.objectFit;
1097
1583
  }
1584
+ // Update group layout properties
1585
+ if (layer.type === 'group') {
1586
+ const groupLayer = layer;
1587
+ if (params.layoutMode !== undefined)
1588
+ groupLayer.layoutMode = params.layoutMode;
1589
+ if (params.widthMode !== undefined)
1590
+ groupLayer.widthMode = params.widthMode;
1591
+ if (params.heightMode !== undefined)
1592
+ groupLayer.heightMode = params.heightMode;
1593
+ if (params.gap !== undefined)
1594
+ groupLayer.gap = params.gap;
1595
+ if (params.justifyContent !== undefined)
1596
+ groupLayer.justifyContent = params.justifyContent;
1597
+ if (params.alignItems !== undefined)
1598
+ groupLayer.alignItems = params.alignItems;
1599
+ if (params.wrap !== undefined)
1600
+ groupLayer.wrap = params.wrap;
1601
+ if (params.fillColor !== undefined)
1602
+ groupLayer.fillColor = params.fillColor;
1603
+ if (params.fillOpacity !== undefined)
1604
+ groupLayer.fillOpacity = params.fillOpacity;
1605
+ if (params.cornerRadius !== undefined)
1606
+ groupLayer.cornerRadius = params.cornerRadius;
1607
+ // Padding: uniform or individual overrides
1608
+ if (params.padding !== undefined || params.paddingTop !== undefined || params.paddingRight !== undefined || params.paddingBottom !== undefined || params.paddingLeft !== undefined) {
1609
+ const existing = groupLayer.padding || { top: 0, right: 0, bottom: 0, left: 0 };
1610
+ const uniform = params.padding ?? undefined;
1611
+ groupLayer.padding = {
1612
+ top: params.paddingTop ?? uniform ?? existing.top,
1613
+ right: params.paddingRight ?? uniform ?? existing.right,
1614
+ bottom: params.paddingBottom ?? uniform ?? existing.bottom,
1615
+ left: params.paddingLeft ?? uniform ?? existing.left,
1616
+ };
1617
+ }
1618
+ // Allow updating baseWidth/baseHeight via width/height params
1619
+ if (params.width !== undefined) {
1620
+ groupLayer.baseWidth = params.width;
1621
+ if (params.widthMode === undefined) {
1622
+ groupLayer.widthMode = 'fixed';
1623
+ }
1624
+ }
1625
+ if (params.height !== undefined) {
1626
+ groupLayer.baseHeight = params.height;
1627
+ if (params.heightMode === undefined) {
1628
+ groupLayer.heightMode = 'fixed';
1629
+ }
1630
+ }
1631
+ }
1098
1632
  return {
1099
1633
  content: [
1100
1634
  {
@@ -1250,12 +1784,12 @@ async function handleStateTool(name, args) {
1250
1784
  }
1251
1785
  else if (settingsKey === 'chromatic') {
1252
1786
  effect.chromatic = {
1253
- shape: 'dot',
1787
+ shape: 'square',
1254
1788
  radius: 4,
1255
1789
  rotateR: 15,
1256
1790
  rotateG: 75,
1257
1791
  rotateB: 0,
1258
- scatter: 0,
1792
+ scatter: 1,
1259
1793
  blending: 1,
1260
1794
  blendingMode: 'linear',
1261
1795
  greyscale: false,
@@ -1273,12 +1807,12 @@ async function handleStateTool(name, args) {
1273
1807
  // Digital glitch defaults
1274
1808
  blockSize: 0.5,
1275
1809
  displacement: 0.5,
1276
- blockOpacity: 1.0,
1810
+ blockOpacity: 0,
1277
1811
  colorSplit: 0.5,
1278
1812
  lineTear: 0.5,
1279
1813
  pixelate: 0.3,
1280
1814
  // Weird glitch defaults
1281
- glitchChance: 0.2,
1815
+ glitchChance: 0.66,
1282
1816
  glitchSpeed: 7.0,
1283
1817
  sliceDensity: 14.0,
1284
1818
  sliceStrength: 0.38,