@appinternalleads/ui 0.4.1 → 0.4.2

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 (70) hide show
  1. package/README.md +92 -0
  2. package/dist/cjs/motion/typesurface/TypeSurfaceCanvas.js +25 -2
  3. package/dist/cjs/motion/typesurface/TypeSurfaceCanvas.js.map +1 -1
  4. package/dist/cjs/motion/typesurface/config.js +21 -1
  5. package/dist/cjs/motion/typesurface/config.js.map +1 -1
  6. package/dist/cjs/motion/typesurface/construct.js +875 -0
  7. package/dist/cjs/motion/typesurface/construct.js.map +1 -0
  8. package/dist/cjs/motion/typesurface/engine.js +28 -4
  9. package/dist/cjs/motion/typesurface/engine.js.map +1 -1
  10. package/dist/cjs/motion/typesurface/index.js +9 -1
  11. package/dist/cjs/motion/typesurface/index.js.map +1 -1
  12. package/dist/cjs/motion/typesurface/presets.js +69 -2
  13. package/dist/cjs/motion/typesurface/presets.js.map +1 -1
  14. package/dist/cjs/motion/typesurface/renderer.js +81 -15
  15. package/dist/cjs/motion/typesurface/renderer.js.map +1 -1
  16. package/dist/cjs/motion/typesurface/shaders.js +23 -1
  17. package/dist/cjs/motion/typesurface/shaders.js.map +1 -1
  18. package/dist/cjs/motion/typesurface/surfaces.js +69 -0
  19. package/dist/cjs/motion/typesurface/surfaces.js.map +1 -1
  20. package/dist/esm/motion/typesurface/TypeSurfaceCanvas.d.ts +11 -1
  21. package/dist/esm/motion/typesurface/TypeSurfaceCanvas.d.ts.map +1 -1
  22. package/dist/esm/motion/typesurface/TypeSurfaceCanvas.js +25 -2
  23. package/dist/esm/motion/typesurface/TypeSurfaceCanvas.js.map +1 -1
  24. package/dist/esm/motion/typesurface/config.d.ts +13 -2
  25. package/dist/esm/motion/typesurface/config.d.ts.map +1 -1
  26. package/dist/esm/motion/typesurface/config.js +21 -1
  27. package/dist/esm/motion/typesurface/config.js.map +1 -1
  28. package/dist/esm/motion/typesurface/construct.d.ts +284 -0
  29. package/dist/esm/motion/typesurface/construct.d.ts.map +1 -0
  30. package/dist/esm/motion/typesurface/construct.js +866 -0
  31. package/dist/esm/motion/typesurface/construct.js.map +1 -0
  32. package/dist/esm/motion/typesurface/engine.d.ts +8 -0
  33. package/dist/esm/motion/typesurface/engine.d.ts.map +1 -1
  34. package/dist/esm/motion/typesurface/engine.js +28 -4
  35. package/dist/esm/motion/typesurface/engine.js.map +1 -1
  36. package/dist/esm/motion/typesurface/index.d.ts +7 -3
  37. package/dist/esm/motion/typesurface/index.d.ts.map +1 -1
  38. package/dist/esm/motion/typesurface/index.js +6 -2
  39. package/dist/esm/motion/typesurface/index.js.map +1 -1
  40. package/dist/esm/motion/typesurface/presets.d.ts +18 -0
  41. package/dist/esm/motion/typesurface/presets.d.ts.map +1 -1
  42. package/dist/esm/motion/typesurface/presets.js +68 -3
  43. package/dist/esm/motion/typesurface/presets.js.map +1 -1
  44. package/dist/esm/motion/typesurface/renderer.d.ts +10 -0
  45. package/dist/esm/motion/typesurface/renderer.d.ts.map +1 -1
  46. package/dist/esm/motion/typesurface/renderer.js +82 -16
  47. package/dist/esm/motion/typesurface/renderer.js.map +1 -1
  48. package/dist/esm/motion/typesurface/shaders.d.ts +1 -1
  49. package/dist/esm/motion/typesurface/shaders.d.ts.map +1 -1
  50. package/dist/esm/motion/typesurface/shaders.js +23 -1
  51. package/dist/esm/motion/typesurface/shaders.js.map +1 -1
  52. package/dist/esm/motion/typesurface/surfaces.d.ts +28 -1
  53. package/dist/esm/motion/typesurface/surfaces.d.ts.map +1 -1
  54. package/dist/esm/motion/typesurface/surfaces.js +67 -0
  55. package/dist/esm/motion/typesurface/surfaces.js.map +1 -1
  56. package/docs/TYPE-SURFACE.md +183 -0
  57. package/package.json +4 -2
  58. package/src/motion/typesurface/TypeSurfaceCanvas.tsx +30 -2
  59. package/src/motion/typesurface/config.ts +29 -3
  60. package/src/motion/typesurface/construct.ts +828 -0
  61. package/src/motion/typesurface/engine.ts +31 -5
  62. package/src/motion/typesurface/index.ts +7 -3
  63. package/src/motion/typesurface/presets.ts +75 -3
  64. package/src/motion/typesurface/renderer.ts +77 -17
  65. package/src/motion/typesurface/shaders.ts +23 -1
  66. package/src/motion/typesurface/surfaces.ts +78 -1
  67. package/tools/ask/dist/components.mjs +1138 -57
  68. package/tools/ask/dist/neon.js +69 -0
  69. package/tools/ask/mcp.mjs +90 -5
  70. package/tools/ask/neon-render.mjs +143 -0
@@ -1767,10 +1767,10 @@ function validateExportFormat(c) {
1767
1767
  if (!Array.isArray(c.formats)) return [{ path: "formats", message: "formats must be a list." }];
1768
1768
  const issues = [];
1769
1769
  const values = /* @__PURE__ */ new Set();
1770
- c.formats.forEach((f, i) => {
1771
- if (values.has(f.value)) issues.push({ path: `formats[${i}].value`, message: `The value \u201C${f.value}\u201D is used twice.` });
1772
- values.add(f.value);
1773
- if (!f.ext) issues.push({ path: `formats[${i}].ext`, message: "Every format needs an extension." });
1770
+ c.formats.forEach((f2, i) => {
1771
+ if (values.has(f2.value)) issues.push({ path: `formats[${i}].value`, message: `The value \u201C${f2.value}\u201D is used twice.` });
1772
+ values.add(f2.value);
1773
+ if (!f2.ext) issues.push({ path: `formats[${i}].ext`, message: "Every format needs an extension." });
1774
1774
  });
1775
1775
  if (c.formats.length && !values.has(c.defaultFormat)) issues.push({ path: "defaultFormat", message: `\u201C${c.defaultFormat}\u201D is not one of the formats.` });
1776
1776
  return issues;
@@ -6198,8 +6198,8 @@ var FILE_SHARED_PRESETS = presets_default25;
6198
6198
  function validateFileShared(c) {
6199
6199
  if (!Array.isArray(c.files)) return [{ path: "files", message: "files must be a list." }];
6200
6200
  const issues = [];
6201
- c.files.forEach((f, i) => {
6202
- if (!f || !f.name) issues.push({ path: `files[${i}].name`, message: "Every file needs a name." });
6201
+ c.files.forEach((f2, i) => {
6202
+ if (!f2 || !f2.name) issues.push({ path: `files[${i}].name`, message: "Every file needs a name." });
6203
6203
  });
6204
6204
  if (!(Number.isInteger(c.failIndex) && c.failIndex >= 0)) issues.push({ path: "failIndex", message: "failIndex is a whole number from 0." });
6205
6205
  return issues;
@@ -19839,13 +19839,13 @@ function validateReceiptPrinter(c) {
19839
19839
  });
19840
19840
  const keys = /* @__PURE__ */ new Set();
19841
19841
  if (fields.length > RECEIPT_MAX_FIELDS) issues.push({ path: "fields", message: `At most ${RECEIPT_MAX_FIELDS} fields \u2014 this has ${fields.length}.` });
19842
- else fields.forEach((f, i) => {
19843
- const key = f?.key;
19842
+ else fields.forEach((f2, i) => {
19843
+ const key = f2?.key;
19844
19844
  if (typeof key !== "string" || !FIELD_KEY.test(key)) issues.push({ path: `fields[${i}].key`, message: "A key starts with a letter and uses only letters, digits and _, like bookName." });
19845
19845
  else if (keys.has(key)) issues.push({ path: `fields[${i}].key`, message: `${key} is used twice.` });
19846
19846
  else keys.add(key);
19847
- if (!isText(f?.label)) issues.push({ path: `fields[${i}].label`, message: "Each field needs a label." });
19848
- if (f?.value !== void 0 && typeof f.value !== "string") issues.push({ path: `fields[${i}].value`, message: "A value is text." });
19847
+ if (!isText(f2?.label)) issues.push({ path: `fields[${i}].label`, message: "Each field needs a label." });
19848
+ if (f2?.value !== void 0 && typeof f2.value !== "string") issues.push({ path: `fields[${i}].value`, message: "A value is text." });
19849
19849
  });
19850
19850
  if (items.length > RECEIPT_MAX_ITEMS) issues.push({ path: "items", message: `A receipt prints at most ${RECEIPT_MAX_ITEMS} lines \u2014 this has ${items.length}.` });
19851
19851
  else items.forEach((it, i) => {
@@ -21143,8 +21143,8 @@ function validateThresholdFlow(doc) {
21143
21143
  if (typeof d.key !== "string" || !KEY.test(d.key)) issues.push({ code: "schema", message: "key must match ^[a-z][a-z0-9_]*$." });
21144
21144
  const p = d.props;
21145
21145
  if (!p || typeof p !== "object") return [...issues, { code: "schema", message: "props is required." }];
21146
- for (const f of ["title", "valueLabel"]) {
21147
- if (typeof p[f] !== "string" || !p[f]) issues.push({ code: "schema", message: `props.${f} is required.` });
21146
+ for (const f2 of ["title", "valueLabel"]) {
21147
+ if (typeof p[f2] !== "string" || !p[f2]) issues.push({ code: "schema", message: `props.${f2} is required.` });
21148
21148
  }
21149
21149
  if (!Array.isArray(p.defaultItems)) return [...issues, { code: "schema", message: "props.defaultItems must be an array." }];
21150
21150
  if (p.type !== void 0 && !DATA_TYPES.includes(p.type)) issues.push({ code: "unknown-type", message: `Unknown type "${String(p.type)}".` });
@@ -21254,8 +21254,8 @@ function checkValue(spec, v, path, c) {
21254
21254
  if (spec.maxItems !== void 0 && v.length > spec.maxItems) push(c, path, "MAX_ITEMS", layer, `${path} holds at most ${spec.maxItems} (has ${v.length}).`, `\u2264 ${spec.maxItems} items`);
21255
21255
  v.forEach((x, i) => checkValue(spec.item, x, `${path}[${i}]`, c));
21256
21256
  if (spec.item.type === "object") {
21257
- for (const [k, f] of Object.entries(spec.item.fields)) {
21258
- if (f.type !== "text" || !f.unique) continue;
21257
+ for (const [k, f2] of Object.entries(spec.item.fields)) {
21258
+ if (f2.type !== "text" || !f2.unique) continue;
21259
21259
  const seen = /* @__PURE__ */ new Set();
21260
21260
  v.forEach((x, i) => {
21261
21261
  const val = x?.[k];
@@ -21290,15 +21290,15 @@ function checkFields(fields, v, path, c) {
21290
21290
  for (const k of Object.keys(v)) {
21291
21291
  if (!(k in fields)) push(c, `${path}.${k}`, "UNKNOWN_KEY", "schema", `${path} has no "${k}". It takes: ${expected.join(", ") || "nothing"}.`, expected);
21292
21292
  }
21293
- for (const [k, f] of Object.entries(fields)) {
21294
- if (f.required && v[k] === void 0) push(c, `${path}.${k}`, "REQUIRED", c.section === "data" ? "data" : "configuration", `${path}.${k} is required: ${f.description}`);
21295
- else checkValue(f, v[k], `${path}.${k}`, c);
21293
+ for (const [k, f2] of Object.entries(fields)) {
21294
+ if (f2.required && v[k] === void 0) push(c, `${path}.${k}`, "REQUIRED", c.section === "data" ? "data" : "configuration", `${path}.${k} is required: ${f2.description}`);
21295
+ else checkValue(f2, v[k], `${path}.${k}`, c);
21296
21296
  }
21297
21297
  }
21298
21298
  function checkOwners(fields, v, path, c) {
21299
- for (const [k, f] of Object.entries(fields)) {
21300
- if (v[k] !== void 0 && f.owners && !f.owners.includes("agent")) {
21301
- push(c, `${path}.${k}`, "NOT_ALLOWED", "configuration", `${path}.${k} is set by the ${f.owners.join(" or ")}, not by an agent.`, f.owners);
21299
+ for (const [k, f2] of Object.entries(fields)) {
21300
+ if (v[k] !== void 0 && f2.owners && !f2.owners.includes("agent")) {
21301
+ push(c, `${path}.${k}`, "NOT_ALLOWED", "configuration", `${path}.${k} is set by the ${f2.owners.join(" or ")}, not by an agent.`, f2.owners);
21302
21302
  }
21303
21303
  }
21304
21304
  }
@@ -21315,22 +21315,22 @@ function valuesAt(root, ref) {
21315
21315
  return cur;
21316
21316
  }
21317
21317
  function checkRefs(fields, v, path, root, c) {
21318
- for (const [k, f] of Object.entries(fields)) visitRef(f, v?.[k], `${path}.${k}`, root, c);
21318
+ for (const [k, f2] of Object.entries(fields)) visitRef(f2, v?.[k], `${path}.${k}`, root, c);
21319
21319
  }
21320
- function visitRef(f, v, path, root, c) {
21320
+ function visitRef(f2, v, path, root, c) {
21321
21321
  if (v === void 0 || v === null) return;
21322
- if (f.ref && (typeof v === "string" || typeof v === "number")) {
21323
- const allowed = valuesAt(root, f.ref).filter((x) => typeof x === "string" || typeof x === "number");
21322
+ if (f2.ref && (typeof v === "string" || typeof v === "number")) {
21323
+ const allowed = valuesAt(root, f2.ref).filter((x) => typeof x === "string" || typeof x === "number");
21324
21324
  if (!allowed.includes(v)) {
21325
- push(c, path, "REFERENCE", c.section === "data" ? "data" : "configuration", `${path} must name one of ${f.ref} (got "${v}").`, allowed.map(String));
21325
+ push(c, path, "REFERENCE", c.section === "data" ? "data" : "configuration", `${path} must name one of ${f2.ref} (got "${v}").`, allowed.map(String));
21326
21326
  }
21327
21327
  }
21328
- if (f.type === "list" && Array.isArray(v)) v.forEach((x, i) => visitRef(f.item, x, `${path}[${i}]`, root, c));
21329
- if (f.type === "object" && v && typeof v === "object") for (const [k, g] of Object.entries(f.fields)) visitRef(g, v[k], `${path}.${k}`, root, c);
21328
+ if (f2.type === "list" && Array.isArray(v)) v.forEach((x, i) => visitRef(f2.item, x, `${path}[${i}]`, root, c));
21329
+ if (f2.type === "object" && v && typeof v === "object") for (const [k, g] of Object.entries(f2.fields)) visitRef(g, v[k], `${path}.${k}`, root, c);
21330
21330
  }
21331
21331
  function withDefaults(fields, v = {}) {
21332
21332
  const out = { ...v };
21333
- for (const [k, f] of Object.entries(fields)) if (out[k] === void 0 && f.default !== void 0) out[k] = structuredClone(f.default);
21333
+ for (const [k, f2] of Object.entries(fields)) if (out[k] === void 0 && f2.default !== void 0) out[k] = structuredClone(f2.default);
21334
21334
  return out;
21335
21335
  }
21336
21336
  var PLACEHOLDER = /\{(\w+)\}/g;
@@ -22724,8 +22724,8 @@ function check(value, shape, path, out) {
22724
22724
  if (k === "array" && shape.item) value.forEach((v, i) => check(v, shape.item, `${path}[${i}]`, out));
22725
22725
  if (k === "object" && shape.fields) {
22726
22726
  for (const [key, v] of Object.entries(value)) {
22727
- const f = shape.fields.get(key);
22728
- if (f) check(v, f, path ? `${path}.${key}` : key, out);
22727
+ const f2 = shape.fields.get(key);
22728
+ if (f2) check(v, f2, path ? `${path}.${key}` : key, out);
22729
22729
  }
22730
22730
  }
22731
22731
  }
@@ -22745,8 +22745,8 @@ var word = (v, fallback2) => typeof v === "string" && v.trim() ? v : fallback2;
22745
22745
  var blankItem = () => ({ name: "", quantity: "1", amount: "" });
22746
22746
  function fieldDrafts(c) {
22747
22747
  const defs = (Array.isArray(c.fields) ? c.fields : []).slice(0, RECEIPT_MAX_FIELDS);
22748
- const first = defs.findIndex((f) => f?.heading === true);
22749
- return defs.map((f, i) => ({ key: text2(f?.key), label: text2(f?.label), value: text2(f?.value), heading: i === first, keyTouched: true }));
22748
+ const first = defs.findIndex((f2) => f2?.heading === true);
22749
+ return defs.map((f2, i) => ({ key: text2(f2?.key), label: text2(f2?.label), value: text2(f2?.value), heading: i === first, keyTouched: true }));
22750
22750
  }
22751
22751
  function initialDraft(c) {
22752
22752
  const currencies = currenciesOf(c);
@@ -22775,15 +22775,15 @@ function formErrors(draft, c) {
22775
22775
  const m = messagesOf(c);
22776
22776
  if (draft.message.trim().length > RECEIPT_MAX_LENGTH.message) e.message = fill(m.tooLong, { max: RECEIPT_MAX_LENGTH.message });
22777
22777
  const keys = /* @__PURE__ */ new Set();
22778
- draft.fields.forEach((f, i) => {
22779
- const label = f.label.trim();
22778
+ draft.fields.forEach((f2, i) => {
22779
+ const label = f2.label.trim();
22780
22780
  if (!label) e[`fields.${i}.label`] = m.labelMissing;
22781
22781
  else if (label.length > RECEIPT_MAX_LENGTH.fieldLabel) e[`fields.${i}.label`] = fill(m.labelTooLong, { max: RECEIPT_MAX_LENGTH.fieldLabel });
22782
- if (!FIELD_KEY.test(f.key)) e[`fields.${i}.key`] = m.keyInvalid;
22783
- else if (keys.has(f.key)) e[`fields.${i}.key`] = fill(m.keyTaken, { key: f.key });
22784
- keys.add(f.key);
22785
- const v = f.value.trim();
22786
- const max = f.heading ? RECEIPT_MAX_LENGTH.heading : RECEIPT_MAX_LENGTH.fieldValue;
22782
+ if (!FIELD_KEY.test(f2.key)) e[`fields.${i}.key`] = m.keyInvalid;
22783
+ else if (keys.has(f2.key)) e[`fields.${i}.key`] = fill(m.keyTaken, { key: f2.key });
22784
+ keys.add(f2.key);
22785
+ const v = f2.value.trim();
22786
+ const max = f2.heading ? RECEIPT_MAX_LENGTH.heading : RECEIPT_MAX_LENGTH.fieldValue;
22787
22787
  if (!v) e[`fields.${i}.value`] = label ? fill(m.valueMissing, { label: label.toLowerCase() }) : m.valueMissingNoLabel;
22788
22788
  else if (v.length > max) e[`fields.${i}.value`] = fill(m.tooLong, { max });
22789
22789
  });
@@ -22810,7 +22810,7 @@ function formErrors(draft, c) {
22810
22810
  }
22811
22811
  function valuesOf(draft) {
22812
22812
  return {
22813
- fields: Object.fromEntries(draft.fields.map((f) => [f.key, f.value.trim()])),
22813
+ fields: Object.fromEntries(draft.fields.map((f2) => [f2.key, f2.value.trim()])),
22814
22814
  items: draft.items.map((it) => ({ name: it.name.trim(), quantity: Number(it.quantity.trim()), amount: Number(it.amount.trim()) })),
22815
22815
  currency: draft.currency,
22816
22816
  paymentMethod: draft.paymentMethod,
@@ -22833,14 +22833,14 @@ function receiptLines(v, c, defs = fieldDrafts(c)) {
22833
22833
  const minor = 10 ** (currency?.decimals ?? 2);
22834
22834
  const total = v.items.reduce((sum, it) => sum + Math.round(it.quantity * it.amount * minor), 0) / minor;
22835
22835
  const paid = word(c.paidLabel, "") ? ` (${c.paidLabel.toUpperCase()})` : "";
22836
- const heading = defs.find((f) => f.heading);
22836
+ const heading = defs.find((f2) => f2.heading);
22837
22837
  const title = heading ? v.fields[heading.key] ?? "" : "";
22838
22838
  return {
22839
22839
  title: title.toUpperCase(),
22840
22840
  badge: (title.match(/[\p{L}\p{N}]/u)?.[0] ?? "\u2022").toUpperCase(),
22841
22841
  total: formatMoney(total, currency),
22842
22842
  meta: `${receiptDate(v.date, c.monthNames?.length === 12 ? c.monthNames : RECEIPT_MONTHS)} | ${method.toUpperCase()}${paid}`,
22843
- details: defs.filter((f) => !f.heading).map((f) => ({ label: f.label, value: v.fields[f.key] ?? "" })),
22843
+ details: defs.filter((f2) => !f2.heading).map((f2) => ({ label: f2.label, value: v.fields[f2.key] ?? "" })),
22844
22844
  items: v.items.map((it) => ({ line: fill(messagesOf(c).itemLine, { quantity: it.quantity, name: it.name }), amount: formatMoney(it.quantity * it.amount, currency) })),
22845
22845
  paymentMethod: method,
22846
22846
  message: v.message.toUpperCase(),
@@ -25147,10 +25147,10 @@ function controlKit(groupOf3, easingNames) {
25147
25147
  ...o.unit ? { unit: o.unit } : {},
25148
25148
  ...o.step !== void 0 ? { step: o.step } : {}
25149
25149
  });
25150
- const num9 = (path, def, min, max, description, o = {}) => ({ path, group: groupOf3(path), type: "number", default: def, min, max, description, ...base4({ step: 0.01, ...o }) });
25150
+ const num10 = (path, def, min, max, description, o = {}) => ({ path, group: groupOf3(path), type: "number", default: def, min, max, description, ...base4({ step: 0.01, ...o }) });
25151
25151
  const int4 = (path, def, min, max, description, o = {}) => ({ path, group: groupOf3(path), type: "integer", default: def, min, max, description, ...base4({ step: 1, ...o }) });
25152
25152
  const k = {
25153
- num: num9,
25153
+ num: num10,
25154
25154
  int: int4,
25155
25155
  frames: (path, def, max, description, min = 0) => int4(path, def, min, max, description, { unit: "frames" }),
25156
25156
  bool: (path, def, description, o = {}) => ({ path, group: groupOf3(path), type: "boolean", default: def, description, ...base4(o) }),
@@ -25169,12 +25169,12 @@ function controlKit(groupOf3, easingNames) {
25169
25169
  }),
25170
25170
  easing: (path, def, description) => ({ path, group: groupOf3(path), type: "easing", default: def, options: easingNames, description, ...base4() }),
25171
25171
  asset: (path, description) => ({ path, group: groupOf3(path), type: "asset", default: "", maxLength: 2048, description, ...base4() }),
25172
- opacity: (path, def, description) => num9(path, def, 0, 1, description, { unit: "fraction" }),
25173
- du: (path, def, min, max, description) => num9(path, def, min, max, description, { step: 1, unit: "du" }),
25174
- pu: (path, def, min, max, description) => num9(path, def, min, max, description, { step: 1, unit: "pu" }),
25175
- norm: (path, def, min, max, description) => num9(path, def, min, max, description, { step: 5e-3, unit: "normalized" }),
25176
- deg: (path, def, min, max, description) => num9(path, def, min, max, description, { step: 0.5, unit: "deg" }),
25177
- factor: (path, def, min, max, description) => num9(path, def, min, max, description, { step: 0.01, unit: "factor" })
25172
+ opacity: (path, def, description) => num10(path, def, 0, 1, description, { unit: "fraction" }),
25173
+ du: (path, def, min, max, description) => num10(path, def, min, max, description, { step: 1, unit: "du" }),
25174
+ pu: (path, def, min, max, description) => num10(path, def, min, max, description, { step: 1, unit: "pu" }),
25175
+ norm: (path, def, min, max, description) => num10(path, def, min, max, description, { step: 5e-3, unit: "normalized" }),
25176
+ deg: (path, def, min, max, description) => num10(path, def, min, max, description, { step: 0.5, unit: "deg" }),
25177
+ factor: (path, def, min, max, description) => num10(path, def, min, max, description, { step: 0.01, unit: "factor" })
25178
25178
  };
25179
25179
  return k;
25180
25180
  }
@@ -29202,7 +29202,7 @@ var NOT_CONFIGURABLE = [
29202
29202
  ];
29203
29203
  function motionOutputFormats(t) {
29204
29204
  const { fps } = t.motion;
29205
- const canvases = (t.motion.formats ?? [{ id: "default", aspectRatio: "", width: t.motion.width, height: t.motion.height }]).map((f) => `${f.width}\xD7${f.height}${f.aspectRatio ? ` (${f.aspectRatio})` : ""}`).join(" or ");
29205
+ const canvases = (t.motion.formats ?? [{ id: "default", aspectRatio: "", width: t.motion.width, height: t.motion.height }]).map((f2) => `${f2.width}\xD7${f2.height}${f2.aspectRatio ? ` (${f2.aspectRatio})` : ""}`).join(" or ");
29206
29206
  const composition = t.id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
29207
29207
  return [
29208
29208
  {
@@ -29230,24 +29230,24 @@ function listMotionTemplates() {
29230
29230
  title: t.title,
29231
29231
  description: t.description,
29232
29232
  canvas: { width: t.motion.width, height: t.motion.height },
29233
- ...t.motion.formats ? { aspectRatios: t.motion.formats.map((f) => f.aspectRatio) } : {},
29233
+ ...t.motion.formats ? { aspectRatios: t.motion.formats.map((f2) => f2.aspectRatio) } : {},
29234
29234
  ...t.motion.sequence ? { kind: "sequence" } : {},
29235
29235
  seconds: seconds(t, t.motion.durationInFrames),
29236
29236
  slots: t.slots.length,
29237
- formats: motionOutputFormats(t).map((f) => f.format)
29237
+ formats: motionOutputFormats(t).map((f2) => f2.format)
29238
29238
  })).concat(Object.values(CONFIGURABLE_MOTION_TEMPLATES).map((t) => ({
29239
29239
  template: t.id,
29240
29240
  version: t.version,
29241
29241
  title: t.title,
29242
29242
  description: t.description,
29243
29243
  canvas: { width: t.formats[0].width, height: t.formats[0].height },
29244
- aspectRatios: t.formats.map((f) => f.aspectRatio),
29244
+ aspectRatios: t.formats.map((f2) => f2.aspectRatio),
29245
29245
  kind: "configurableMotion",
29246
29246
  renderer: t.renderer,
29247
29247
  seconds: Math.round(t.durationInFrames / t.fps * 100) / 100,
29248
29248
  slots: 0,
29249
29249
  controls: t.controls.length,
29250
- formats: configurableOutputFormats(t).map((f) => f.format)
29250
+ formats: configurableOutputFormats(t).map((f2) => f2.format)
29251
29251
  })));
29252
29252
  }
29253
29253
  function getConfigurableMotionTemplate(id) {
@@ -29260,7 +29260,7 @@ var CONFIG_API = {
29260
29260
  };
29261
29261
  var LIVE_PLAYER = { "promo-product-showcase": "PromoShowcasePlayer", "paper-fold-promo": "PaperFoldPlayer" };
29262
29262
  function configurableOutputFormats(t) {
29263
- const canvases = t.formats.map((f) => `${f.width}\xD7${f.height} (${f.aspectRatio})`).join(", ");
29263
+ const canvases = t.formats.map((f2) => `${f2.width}\xD7${f2.height} (${f2.aspectRatio})`).join(", ");
29264
29264
  if (t.renderer === "remotion") {
29265
29265
  return [
29266
29266
  { output: "live", format: "HTML + CSS (DOM)", detail: `Any size, ${t.fps} fps; drawn procedurally from the config by the package's renderer (${LIVE_PLAYER[t.id] ?? "its player"} on the web). Not a video file.` },
@@ -29377,7 +29377,7 @@ function describeDom(id) {
29377
29377
  }])),
29378
29378
  theme: Object.fromEntries(t.theme.map((c) => [`theme.${c.token}`, { type: "hex colour", default: c.default, description: c.description }])),
29379
29379
  optionalScenes: t.optionalScenes.map((id2) => ({ scene: id2, effect: `leave "${id2}" out of content (or set it to null) to ${t.motion.sequence ? "hide it" : "end the video before it"}` })),
29380
- ...t.motion.formats ? { aspectRatio: { field: "aspectRatio", values: t.motion.formats.map((f) => ({ value: f.aspectRatio, width: f.width, height: f.height })), default: t.motion.formats[0].aspectRatio, effect: "the same animation, laid out for that canvas" } } : {},
29380
+ ...t.motion.formats ? { aspectRatio: { field: "aspectRatio", values: t.motion.formats.map((f2) => ({ value: f2.aspectRatio, width: f2.width, height: f2.height })), default: t.motion.formats[0].aspectRatio, effect: "the same animation, laid out for that canvas" } } : {},
29381
29381
  ...t.motion.sequence ? { sequence: describeSequence(t) } : {},
29382
29382
  notConfigurable: NOT_CONFIGURABLE,
29383
29383
  example: { schema: MOTION_REQUEST_SCHEMA, template: t.id, content: t.exampleContent },
@@ -29429,6 +29429,1079 @@ function motionPreviewUrl(request, origin = "https://comppack.vercel.app") {
29429
29429
  return `${origin.replace(/\/+$/, "")}/animations/${encodeURIComponent(request.template)}?request=${b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
29430
29430
  }
29431
29431
 
29432
+ // src/motion/neon/config.ts
29433
+ var NEON_CONSTRUCTIONS = ["outline", "monoline"];
29434
+ var NEON_ALIGNS = ["start", "middle", "end"];
29435
+ var DEFAULT_NEON_CONFIG = {
29436
+ text: "CAFE",
29437
+ typography: {
29438
+ fontFamily: "Oswald, 'Arial Narrow', 'Helvetica Neue', Arial, sans-serif",
29439
+ fontSize: 220,
29440
+ fontWeight: 500,
29441
+ fontStyle: "normal",
29442
+ letterSpacing: 4,
29443
+ lineHeight: 1.15,
29444
+ strokeWidth: 8,
29445
+ construction: "outline",
29446
+ align: "middle"
29447
+ },
29448
+ neon: {
29449
+ color: "#FF4655",
29450
+ coreColor: "#FFF5F5",
29451
+ tubeColor: "#FF4655",
29452
+ glowColor: "#FF1838",
29453
+ bloomColor: "#D4002F",
29454
+ intensity: 1,
29455
+ bloomIntensity: 1,
29456
+ tubeBrightness: 1,
29457
+ coreBrightness: 1,
29458
+ glowRadius: 20,
29459
+ bloomRadius: 70,
29460
+ coreWidth: 0.47,
29461
+ glassOpacity: 0.16,
29462
+ shadow: { enabled: true, color: "#000000", opacity: 0.5, offsetX: 0.035, offsetY: 0.045, blur: 6 }
29463
+ },
29464
+ animation: {
29465
+ enabled: true,
29466
+ speed: 1,
29467
+ seed: 7356951,
29468
+ flickerEnabled: true,
29469
+ flickerIntensity: 0.15,
29470
+ flickerFrequency: 0.08,
29471
+ flickerSpeed: 1,
29472
+ flickerRandomness: 0.6,
29473
+ flickerRecovery: 0.7,
29474
+ flickerMinBrightness: 0.05,
29475
+ glitchEnabled: true,
29476
+ glitchIntensity: 0.15,
29477
+ glitchFrequency: 0.04,
29478
+ glitchDuration: 180,
29479
+ glitchDisplacement: 1,
29480
+ glitchRgbSplit: 1,
29481
+ glitchSlices: 1,
29482
+ glitchGlowSeparation: 1
29483
+ },
29484
+ background: null
29485
+ };
29486
+ var NEON_COLOR_PRESETS = {
29487
+ red: { color: "#FF3B4D", coreColor: "#FFF5F5", tubeColor: "#FF3B4D", glowColor: "#FF1744", bloomColor: "#C40030" },
29488
+ blue: { color: "#00D9FF", coreColor: "#F5FFFF", tubeColor: "#00D9FF", glowColor: "#009DFF", bloomColor: "#0055FF" },
29489
+ purple: { color: "#D66BFF", coreColor: "#FFF5FF", tubeColor: "#D66BFF", glowColor: "#9C27FF", bloomColor: "#5E00B8" },
29490
+ green: { color: "#39FF88", coreColor: "#F5FFF5", tubeColor: "#39FF88", glowColor: "#00D95F", bloomColor: "#008F3D" }
29491
+ };
29492
+ var NEON_TEXT_PRESETS = {
29493
+ cafe: { text: "CAFE", typography: { construction: "outline", fontFamily: DEFAULT_NEON_CONFIG.typography.fontFamily, fontWeight: 500, strokeWidth: 8, letterSpacing: 4 } },
29494
+ open: { text: "OPEN", typography: { construction: "outline", fontFamily: DEFAULT_NEON_CONFIG.typography.fontFamily, fontWeight: 500, strokeWidth: 8, letterSpacing: 10 } },
29495
+ night: { text: "NIGHT", typography: { construction: "outline", fontFamily: DEFAULT_NEON_CONFIG.typography.fontFamily, fontWeight: 500, strokeWidth: 8, letterSpacing: 6 } },
29496
+ "24/7": { text: "24/7", typography: { construction: "monoline", fontFamily: "Quicksand, 'Arial Rounded MT Bold', 'Helvetica Neue', Arial, sans-serif", fontWeight: 500, strokeWidth: 10, letterSpacing: 2 } },
29497
+ hello: { text: "Hello", typography: { construction: "monoline", fontFamily: "Sacramento, 'Snell Roundhand', 'Brush Script MT', cursive", fontWeight: 400, strokeWidth: 7, letterSpacing: 0 } }
29498
+ };
29499
+ var HEX7 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
29500
+ var isHexColor = (v) => typeof v === "string" && HEX7.test(v);
29501
+ function hexToRgb(hex) {
29502
+ const h = hex.length === 4 ? hex.slice(1).split("").map((c) => c + c).join("") : hex.slice(1);
29503
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
29504
+ }
29505
+ var rgbToHex = (rgb) => "#" + rgb.map((v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0")).join("").toUpperCase();
29506
+ function mixHex(a, b, t) {
29507
+ const x = hexToRgb(a);
29508
+ const y = hexToRgb(b);
29509
+ return rgbToHex(x.map((v, i) => v + (y[i] - v) * t));
29510
+ }
29511
+ function toHsl([r, g, b]) {
29512
+ const [R, G2, B] = [r / 255, g / 255, b / 255];
29513
+ const max = Math.max(R, G2, B);
29514
+ const min = Math.min(R, G2, B);
29515
+ const l = (max + min) / 2;
29516
+ if (max === min) return [0, 0, l];
29517
+ const d = max - min;
29518
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
29519
+ const h = max === R ? (G2 - B) / d + (G2 < B ? 6 : 0) : max === G2 ? (B - R) / d + 2 : (R - G2) / d + 4;
29520
+ return [h * 60, s, l];
29521
+ }
29522
+ function fromHsl(h, s, l) {
29523
+ const k = (n) => (n + h / 30) % 12;
29524
+ const a = s * Math.min(l, 1 - l);
29525
+ const f2 = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
29526
+ return rgbToHex([f2(0) * 255, f2(8) * 255, f2(4) * 255]);
29527
+ }
29528
+ function deriveNeonPalette(color4) {
29529
+ const base4 = isHexColor(color4) ? color4 : DEFAULT_NEON_CONFIG.neon.color;
29530
+ const [h, s, l] = toHsl(hexToRgb(base4));
29531
+ const drift = (deg3) => s < 0.08 ? h : (h + deg3 + 360) % 360;
29532
+ return {
29533
+ coreColor: mixHex(base4, "#FFFFFF", 0.94),
29534
+ tubeColor: rgbToHex(hexToRgb(base4)),
29535
+ glowColor: fromHsl(drift(-6), Math.min(1, s + 0.15), Math.max(0.3, l * 0.84)),
29536
+ bloomColor: fromHsl(drift(-10), Math.min(1, s + 0.2), Math.max(0.18, l * 0.6))
29537
+ };
29538
+ }
29539
+ var isObj5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
29540
+ var NEON_CONTROLS = [
29541
+ { path: "text", label: "Text", group: "Text", type: "text", description: "What the sign says. A new line (\\n) starts a second row." },
29542
+ { path: "typography.fontFamily", label: "Font", group: "Typography", type: "text", description: "A CSS font-family list; the page must load the font." },
29543
+ { path: "typography.fontSize", label: "Font size", group: "Typography", type: "number", min: 12, max: 1e3, step: 1, unit: "units", description: "The em size, in SVG user units." },
29544
+ { path: "typography.fontWeight", label: "Font weight", group: "Typography", type: "number", min: 100, max: 900, step: 100, description: "CSS font-weight." },
29545
+ { path: "typography.fontStyle", label: "Font style", group: "Typography", type: "enum", options: ["normal", "italic"], description: "Upright or italic." },
29546
+ { path: "typography.letterSpacing", label: "Letter spacing", group: "Typography", type: "number", min: -50, max: 200, step: 1, unit: "units", description: "Extra space between letters." },
29547
+ { path: "typography.lineHeight", label: "Line height", group: "Typography", type: "number", min: 0.6, max: 3, step: 0.05, unit: "\xD7 size", description: "Row pitch for multi-line text." },
29548
+ { path: "typography.strokeWidth", label: "Tube width", group: "Typography", type: "number", min: 0.5, max: 80, step: 0.5, unit: "units", description: "The glass tube. The reference is \u2248 0.033 \xD7 font size." },
29549
+ { path: "typography.construction", label: "Construction", group: "Typography", type: "enum", options: NEON_CONSTRUCTIONS, description: "outline: the tube traces each letter (CAFE). monoline: the letter is the tube (24/7)." },
29550
+ { path: "typography.align", label: "Align", group: "Typography", type: "enum", options: NEON_ALIGNS, description: "Row alignment for multi-line text." },
29551
+ { path: "neon.color", label: "Neon colour", group: "Colour", type: "color", description: "The primary colour; the others follow it unless set." },
29552
+ { path: "neon.tubeColor", label: "Tube colour", group: "Colour", type: "color-or-null", description: "The lit glass. null follows the neon colour." },
29553
+ { path: "neon.coreColor", label: "Core colour", group: "Colour", type: "color-or-null", description: "The hot centre line. null follows the neon colour." },
29554
+ { path: "neon.glowColor", label: "Glow colour", group: "Colour", type: "color-or-null", description: "The tight halo. null follows the neon colour." },
29555
+ { path: "neon.bloomColor", label: "Bloom colour", group: "Colour", type: "color-or-null", description: "The light on the wall. null follows the neon colour." },
29556
+ { path: "neon.intensity", label: "Glow intensity", group: "Light", type: "number", min: 0, max: 3, step: 0.05, description: "0 is a bare tube; 1 the reference." },
29557
+ { path: "neon.bloomIntensity", label: "Bloom intensity", group: "Light", type: "number", min: 0, max: 3, step: 0.05, description: "The wall bloom\u2019s own strength." },
29558
+ { path: "neon.tubeBrightness", label: "Tube brightness", group: "Light", type: "number", min: 0, max: 1.5, step: 0.01, description: "How bright the lit glass is." },
29559
+ { path: "neon.coreBrightness", label: "Core brightness", group: "Light", type: "number", min: 0, max: 1.5, step: 0.01, description: "How bright the hot centre line is." },
29560
+ { path: "neon.glowRadius", label: "Glow radius", group: "Light", type: "number", min: 0, max: 200, step: 1, unit: "units", description: "Reach of the tight halo." },
29561
+ { path: "neon.bloomRadius", label: "Bloom radius", group: "Light", type: "number", min: 0, max: 400, step: 1, unit: "units", description: "Reach of the wall bloom." },
29562
+ { path: "neon.coreWidth", label: "Core width", group: "Light", type: "number", min: 0, max: 1, step: 0.01, unit: "\xD7 tube", description: "The hot core, as a share of the tube." },
29563
+ { path: "neon.glassOpacity", label: "Unlit glass", group: "Light", type: "number", min: 0, max: 1, step: 0.01, description: "How much dark glass shows when the tube dims." },
29564
+ { path: "neon.shadow.enabled", label: "Shadow", group: "Light", type: "boolean", description: "The tube\u2019s dark cast shadow on the wall." },
29565
+ { path: "neon.shadow.color", label: "Shadow colour", group: "Light", type: "color", description: "Shadow colour." },
29566
+ { path: "neon.shadow.opacity", label: "Shadow opacity", group: "Light", type: "number", min: 0, max: 1, step: 0.01, description: "Shadow strength." },
29567
+ { path: "neon.shadow.offsetX", label: "Shadow x", group: "Light", type: "number", min: -0.5, max: 0.5, step: 5e-3, unit: "\xD7 size", description: "Shadow offset, across." },
29568
+ { path: "neon.shadow.offsetY", label: "Shadow y", group: "Light", type: "number", min: -0.5, max: 0.5, step: 5e-3, unit: "\xD7 size", description: "Shadow offset, down." },
29569
+ { path: "neon.shadow.blur", label: "Shadow blur", group: "Light", type: "number", min: 0, max: 100, step: 0.5, unit: "units", description: "Shadow softness." },
29570
+ { path: "animation.enabled", label: "Animation", group: "Animation", type: "boolean", description: "Flicker and glitch on or off." },
29571
+ { path: "animation.speed", label: "Animation speed", group: "Animation", type: "number", min: 0.1, max: 5, step: 0.05, unit: "\xD7", description: "Plays everything faster or slower." },
29572
+ { path: "animation.seed", label: "Seed", group: "Animation", type: "number", min: 0, max: 4294967295, step: 1, description: "Same seed, same performance." },
29573
+ { path: "animation.flickerEnabled", label: "Flicker", group: "Flicker", type: "boolean", description: "Brightness flicker on or off." },
29574
+ { path: "animation.flickerIntensity", label: "Flicker intensity", group: "Flicker", type: "number", min: 0, max: 1, step: 0.01, description: "0.25 plays the full hardware ranges." },
29575
+ { path: "animation.flickerFrequency", label: "Flicker frequency", group: "Flicker", type: "number", min: 0, max: 5, step: 0.01, unit: "/ s", description: "Noticeable flicker events per second." },
29576
+ { path: "animation.flickerSpeed", label: "Flicker speed", group: "Flicker", type: "number", min: 0.1, max: 5, step: 0.05, unit: "\xD7", description: "How fast each flicker event plays." },
29577
+ { path: "animation.flickerRandomness", label: "Randomness", group: "Flicker", type: "number", min: 0, max: 1, step: 0.01, description: "How much events vary in depth and length." },
29578
+ { path: "animation.flickerRecovery", label: "Recovery speed", group: "Flicker", type: "number", min: 0, max: 1, step: 0.01, description: "How fast the tube comes back after a dip or failure." },
29579
+ { path: "animation.flickerMinBrightness", label: "Minimum brightness", group: "Flicker", type: "number", min: 0, max: 1, step: 0.01, description: "The tube never falls below this." },
29580
+ { path: "animation.glitchEnabled", label: "Glitch", group: "Glitch", type: "boolean", description: "Occasional glitches on or off." },
29581
+ { path: "animation.glitchIntensity", label: "Glitch intensity", group: "Glitch", type: "number", min: 0, max: 1, step: 0.01, description: "How far slices shift and colours split." },
29582
+ { path: "animation.glitchFrequency", label: "Glitch frequency", group: "Glitch", type: "number", min: 0, max: 5, step: 0.01, unit: "/ s", description: "Glitches per second." },
29583
+ { path: "animation.glitchDuration", label: "Glitch duration", group: "Glitch", type: "number", min: 20, max: 2e3, step: 10, unit: "ms", description: "A glitch\u2019s typical length." },
29584
+ { path: "animation.glitchDisplacement", label: "Horizontal displacement", group: "Glitch", type: "number", min: 0, max: 2, step: 0.05, unit: "\xD7", description: "The whole-sign sideways jump." },
29585
+ { path: "animation.glitchRgbSplit", label: "RGB separation", group: "Glitch", type: "number", min: 0, max: 2, step: 0.05, unit: "\xD7", description: "The red/cyan split." },
29586
+ { path: "animation.glitchSlices", label: "Slice displacement", group: "Glitch", type: "number", min: 0, max: 2, step: 0.05, unit: "\xD7", description: "How far the sliced bands shift." },
29587
+ { path: "animation.glitchGlowSeparation", label: "Glow separation", group: "Glitch", type: "number", min: 0, max: 2, step: 0.05, unit: "\xD7", description: "How far the glow slips from the tube." },
29588
+ { path: "background", label: "Background", group: "Colour", type: "color-or-null", description: "A colour behind the sign, or null for transparent." }
29589
+ ];
29590
+ function getPath4(obj2, path) {
29591
+ return path.split(".").reduce((o, k) => isObj5(o) ? o[k] : void 0, obj2);
29592
+ }
29593
+ var MAX_NEON_TEXT_LENGTH = 200;
29594
+ function validateNeonConfig(config) {
29595
+ const errors = [];
29596
+ if (!isObj5(config)) return [{ path: "", message: "The config must be an object." }];
29597
+ for (const c of NEON_CONTROLS) {
29598
+ const v = getPath4(config, c.path);
29599
+ const where = c.path;
29600
+ if (c.type === "text") {
29601
+ if (typeof v !== "string") errors.push({ path: where, message: "Must be text." });
29602
+ else if (c.path === "text" && v.length > MAX_NEON_TEXT_LENGTH) errors.push({ path: where, message: `At most ${MAX_NEON_TEXT_LENGTH} characters.` });
29603
+ else if (c.path !== "text" && !v.trim()) errors.push({ path: where, message: "Must not be empty." });
29604
+ } else if (c.type === "number") {
29605
+ if (typeof v !== "number" || !Number.isFinite(v)) errors.push({ path: where, message: "Must be a number." });
29606
+ else if (c.min !== void 0 && v < c.min || c.max !== void 0 && v > c.max) errors.push({ path: where, message: `Must be between ${c.min} and ${c.max}.` });
29607
+ } else if (c.type === "boolean") {
29608
+ if (typeof v !== "boolean") errors.push({ path: where, message: "Must be true or false." });
29609
+ } else if (c.type === "enum") {
29610
+ if (!c.options.includes(v)) errors.push({ path: where, message: `Must be one of: ${c.options.join(", ")}.` });
29611
+ } else if (c.type === "color") {
29612
+ if (!isHexColor(v)) errors.push({ path: where, message: "Must be a hex colour like #FF3B4D." });
29613
+ } else if (v !== null && !isHexColor(v)) {
29614
+ errors.push({ path: where, message: "Must be a hex colour like #FF3B4D, or null." });
29615
+ }
29616
+ }
29617
+ return errors;
29618
+ }
29619
+
29620
+ // src/motion/neon/artwork.ts
29621
+ var SHAPES = /* @__PURE__ */ new Set(["path", "circle", "ellipse", "rect", "line", "polyline", "polygon"]);
29622
+ var CONTAINERS = /* @__PURE__ */ new Set(["g", "a", "switch"]);
29623
+ var SKIP = /* @__PURE__ */ new Set(["defs", "style", "script", "title", "desc", "metadata", "clippath", "mask", "pattern", "lineargradient", "radialgradient", "filter", "symbol", "marker", "text", "foreignobject", "image", "use"]);
29624
+ var ATTRS = /* @__PURE__ */ new Set(["d", "cx", "cy", "r", "rx", "ry", "x", "y", "width", "height", "x1", "y1", "x2", "y2", "points", "transform", "fill-rule"]);
29625
+ var SAFE_VALUE = /^[\s\d.,eE+\-MmLlHhVvCcSsQqTtAaZz()a-z%]*$/;
29626
+ var SAFE_TRANSFORM = /^\s*((matrix|translate|scale|rotate|skewX|skewY)\s*\([\d\s.,eE+-]*\)\s*,?\s*)*$/;
29627
+ function parseAttrs(src) {
29628
+ const out = {};
29629
+ for (const m of src.matchAll(/([a-zA-Z_:][\w:.-]*)\s*=\s*("([^"]*)"|'([^']*)')/g)) out[m[1].toLowerCase()] = m[3] ?? m[4] ?? "";
29630
+ return out;
29631
+ }
29632
+ var MAX_SVG_BYTES = 5e5;
29633
+ var MAX_NODES = 2e3;
29634
+ function parseSvgArtwork(svg) {
29635
+ if (typeof svg !== "string" || !svg.trim()) throw new Error("The SVG is empty.");
29636
+ if (svg.length > MAX_SVG_BYTES) throw new Error(`The SVG is larger than ${MAX_SVG_BYTES / 1e3} kB.`);
29637
+ const clean = svg.replace(/<!--[\s\S]*?-->/g, "").replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, "").replace(/<![^>]*>/g, "").replace(/<\?[\s\S]*?\?>/g, "");
29638
+ const root = { tag: "g", attrs: {}, children: [] };
29639
+ const stack = [{ node: root, tag: "#root" }];
29640
+ let viewBox = null;
29641
+ let count = 0;
29642
+ let shapes = 0;
29643
+ for (const m of clean.matchAll(/<(\/?)([a-zA-Z][\w:.-]*)((?:\s+[^\s=>/]+\s*=\s*(?:"[^"]*"|'[^']*'))*)\s*(\/?)>/g)) {
29644
+ const [, close, rawTag, rawAttrs, selfClose] = m;
29645
+ const tag = rawTag.toLowerCase().replace(/^svg:/, "");
29646
+ if (close) {
29647
+ while (stack.length > 1) {
29648
+ const top = stack.pop();
29649
+ if (top.tag === tag) break;
29650
+ }
29651
+ continue;
29652
+ }
29653
+ const parent = stack[stack.length - 1].node;
29654
+ const attrs = parseAttrs(rawAttrs ?? "");
29655
+ if (tag === "svg" && !viewBox) {
29656
+ const vb = (attrs.viewbox ?? "").trim().split(/[\s,]+/).map(Number);
29657
+ if (vb.length === 4 && vb.every(Number.isFinite) && vb[2] > 0 && vb[3] > 0) viewBox = vb;
29658
+ else {
29659
+ const w = parseFloat(attrs.width ?? "");
29660
+ const h = parseFloat(attrs.height ?? "");
29661
+ if (w > 0 && h > 0) viewBox = [0, 0, w, h];
29662
+ }
29663
+ }
29664
+ if (tag === "svg") {
29665
+ if (!selfClose) stack.push({ node: parent, tag });
29666
+ continue;
29667
+ }
29668
+ let node = null;
29669
+ if (parent && !SKIP.has(tag) && (SHAPES.has(tag) || CONTAINERS.has(tag)) && count < MAX_NODES) {
29670
+ const kept = {};
29671
+ for (const [k, v] of Object.entries(attrs)) {
29672
+ if (!ATTRS.has(k)) continue;
29673
+ if (k === "transform" ? !SAFE_TRANSFORM.test(v) : !SAFE_VALUE.test(v)) continue;
29674
+ if (CONTAINERS.has(tag) && k !== "transform") continue;
29675
+ kept[k] = v;
29676
+ }
29677
+ node = { tag: SHAPES.has(tag) ? tag : "g", attrs: kept, children: [] };
29678
+ parent.children.push(node);
29679
+ count++;
29680
+ if (SHAPES.has(tag)) shapes++;
29681
+ }
29682
+ if (!selfClose && !SHAPES.has(tag)) stack.push({ node: SKIP.has(tag) ? null : node, tag });
29683
+ else if (!selfClose && SHAPES.has(tag)) stack.push({ node: null, tag });
29684
+ }
29685
+ if (!shapes) throw new Error("The SVG has no shapes to bend into tubes (paths, circles, rects, lines or polygons).");
29686
+ return { viewBox: viewBox ?? [0, 0, 100, 100], nodes: root.children, shapes };
29687
+ }
29688
+ var NEON_ARTWORK_LIBRARY = {
29689
+ heart: { name: "Heart", svg: '<svg viewBox="0 0 100 100"><path d="M50 84 C22 64 8 48 12 32 C16 17 35 13 45 24 C48 27 49 30 50 33 C51 30 52 27 55 24 C65 13 84 17 88 32 C92 48 78 64 50 84 Z"/></svg>' },
29690
+ star: { name: "Star", svg: '<svg viewBox="0 0 100 100"><polygon points="50,9 61,37 91,38 67,57 76,87 50,70 24,87 33,57 9,38 39,37"/></svg>' },
29691
+ bolt: { name: "Lightning", svg: '<svg viewBox="0 0 100 100"><path d="M60 7 L26 55 L49 55 L40 93 L75 42 L52 42 Z"/></svg>' },
29692
+ moon: { name: "Moon", svg: '<svg viewBox="0 0 100 100"><path d="M64 12 C40 16 26 35 28 55 C30 76 50 90 72 87 C56 80 45 66 45 49 C45 33 52 20 64 12 Z"/></svg>' },
29693
+ sparkle: { name: "Sparkle", svg: '<svg viewBox="0 0 100 100"><path d="M50 8 C54 38 62 46 92 50 C62 54 54 62 50 92 C46 62 38 54 8 50 C38 46 46 38 50 8 Z"/></svg>' },
29694
+ note: { name: "Music note", svg: '<svg viewBox="0 0 100 100"><path d="M38 74 C38 83 21 86 19 77 C17 69 33 64 38 71 L38 20 L80 11 L80 64 C80 73 63 76 61 67 C59 59 75 54 80 61"/><path d="M38 31 L80 22"/></svg>' },
29695
+ arrow: { name: "Arrow", svg: '<svg viewBox="0 0 100 100"><path d="M10 50 L84 50"/><path d="M62 28 L86 50 L62 72"/></svg>' },
29696
+ cocktail: { name: "Cocktail", svg: '<svg viewBox="0 0 100 100"><path d="M18 18 L82 18 L50 54 Z"/><path d="M50 54 L50 86 M32 86 L68 86"/><circle cx="70" cy="12" r="7"/></svg>' },
29697
+ rocket: { name: "Rocket", svg: '<svg viewBox="0 0 100 100"><path d="M50 8 C66 22 70 44 64 66 L36 66 C30 44 34 22 50 8 Z"/><circle cx="50" cy="36" r="7"/><path d="M36 54 L24 72 L36 68 M64 54 L76 72 L64 68 M44 74 L50 90 L56 74"/></svg>' },
29698
+ crown: { name: "Crown", svg: '<svg viewBox="0 0 100 100"><path d="M14 74 L10 30 L32 50 L50 20 L68 50 L90 30 L86 74 Z"/><path d="M16 84 L84 84"/></svg>' }
29699
+ };
29700
+
29701
+ // src/motion/neon/fonts.ts
29702
+ var f = (id, name, category, fallback2, google, weight3, construction, stroke) => ({ id, name, category, family: google ? `'${name}', ${fallback2}` : fallback2, google, weight: weight3, construction, stroke });
29703
+ var NEON_FONTS = [
29704
+ f("tilt-neon", "Tilt Neon", "neon tube", "Quicksand, sans-serif", "Tilt+Neon", 400, "monoline", 0.04),
29705
+ f("bebas-neue", "Bebas Neue", "condensed", "Oswald, 'Arial Narrow', sans-serif", "Bebas+Neue", 400, "outline", 0.033),
29706
+ f("anton", "Anton", "bold", "'Arial Narrow', Impact, sans-serif", "Anton", 400, "outline", 0.03),
29707
+ f("oswald", "Oswald", "condensed", "'Arial Narrow', 'Helvetica Neue', Arial, sans-serif", "Oswald:wght@300;400;500;600;700", 500, "outline", 0.036),
29708
+ f("space-grotesk", "Space Grotesk", "geometric", "'Helvetica Neue', Arial, sans-serif", "Space+Grotesk:wght@400;500;700", 500, "outline", 0.033),
29709
+ f("orbitron", "Orbitron", "futuristic", "'Eurostile', sans-serif", "Orbitron:wght@400;600;800", 600, "outline", 0.03),
29710
+ f("audiowide", "Audiowide", "futuristic", "sans-serif", "Audiowide", 400, "outline", 0.03),
29711
+ f("russo-one", "Russo One", "signage", "Impact, 'Arial Black', sans-serif", "Russo+One", 400, "outline", 0.03),
29712
+ f("rajdhani", "Rajdhani", "industrial", "'Arial Narrow', sans-serif", "Rajdhani:wght@500;600;700", 600, "outline", 0.033),
29713
+ f("exo-2", "Exo 2", "futuristic", "sans-serif", "Exo+2:wght@300;500;700", 500, "outline", 0.033),
29714
+ f("montserrat", "Montserrat", "geometric", "'Helvetica Neue', Arial, sans-serif", "Montserrat:wght@300;500;700", 500, "outline", 0.033),
29715
+ f("inter", "Inter", "geometric", "system-ui, 'Helvetica Neue', Arial, sans-serif", "Inter:wght@300;500;700", 500, "outline", 0.033),
29716
+ f("archivo", "Archivo", "signage", "'Helvetica Neue', Arial, sans-serif", "Archivo:wght@400;600;800", 600, "outline", 0.033),
29717
+ f("barlow-condensed", "Barlow Condensed", "condensed", "'Arial Narrow', sans-serif", "Barlow+Condensed:wght@400;600", 600, "outline", 0.034),
29718
+ f("sora", "Sora", "geometric", "sans-serif", "Sora:wght@300;500;700", 500, "outline", 0.033),
29719
+ f("raleway", "Raleway", "thin", "'Helvetica Neue', sans-serif", "Raleway:wght@200;300;500", 300, "monoline", 0.03),
29720
+ f("press-start-2p", "Press Start 2P", "arcade", "'Courier New', monospace", "Press+Start+2P", 400, "outline", 0.03),
29721
+ f("pacifico", "Pacifico", "handwritten", "'Brush Script MT', cursive", "Pacifico", 400, "outline", 0.026),
29722
+ f("dm-sans", "DM Sans", "rounded", "'Helvetica Neue', Arial, sans-serif", "DM+Sans:wght@300;500;700", 500, "outline", 0.033),
29723
+ f("quicksand", "Quicksand", "rounded", "'Arial Rounded MT Bold', 'Helvetica Neue', Arial, sans-serif", "Quicksand:wght@300;400;500;600;700", 500, "monoline", 0.045),
29724
+ f("sacramento", "Sacramento", "handwritten", "'Snell Roundhand', 'Brush Script MT', cursive", "Sacramento", 400, "monoline", 0.03),
29725
+ f("righteous", "Righteous", "retro", "sans-serif", "Righteous", 400, "outline", 0.03),
29726
+ f("space-mono", "Space Mono", "monospace", "'Courier New', monospace", "Space+Mono:wght@400;700", 400, "monoline", 0.03),
29727
+ f("great-vibes", "Great Vibes", "elegant", "'Snell Roundhand', cursive", "Great+Vibes", 400, "monoline", 0.022),
29728
+ f("black-ops-one", "Black Ops One", "industrial", "Impact, sans-serif", "Black+Ops+One", 400, "outline", 0.026),
29729
+ f("monoton", "Monoton", "display", "sans-serif", "Monoton", 400, "monoline", 8e-3),
29730
+ f("system", "System", "system", "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif", null, 600, "outline", 0.033)
29731
+ ];
29732
+ var NEON_EMOJI_FONT = f("noto-emoji", "Noto Emoji", "display", "sans-serif", "Noto+Emoji:wght@400;500;700", 500, "outline", 0.03);
29733
+ function findNeonFont(query) {
29734
+ const q = query.trim().toLowerCase().replace(/[-_]+/g, " ");
29735
+ if (!q) return void 0;
29736
+ return NEON_FONTS.find((x) => x.id.replace(/-/g, " ") === q || x.name.toLowerCase() === q) ?? NEON_FONTS.find((x) => x.name.toLowerCase().startsWith(q) || x.family.toLowerCase().startsWith(`'${q}`)) ?? NEON_FONTS.find((x) => x.name.toLowerCase().includes(q));
29737
+ }
29738
+
29739
+ // src/motion/neon/material.ts
29740
+ function materialPad(neon, t) {
29741
+ const shadow = neon.shadow.enabled ? Math.hypot(neon.shadow.offsetX, neon.shadow.offsetY) * t.fontSize + neon.shadow.blur * 2 : 0;
29742
+ const light2 = neon.intensity > 0 ? Math.max(neon.bloomRadius * 1.25, neon.glowRadius * 1.6) : 0;
29743
+ return Math.ceil(Math.max(shadow, light2) + t.strokeWidth * 2 + t.fontSize * 0.04);
29744
+ }
29745
+
29746
+ // src/motion/neon/typography.ts
29747
+ var NARROW = /* @__PURE__ */ new Set([..."iIl.,:;!|'`jft1"]);
29748
+ var WIDE = /* @__PURE__ */ new Set([..."MWmw@%"]);
29749
+ var CAPS = /[A-Z0-9]/;
29750
+ var estimateTextWidth = (text6, t) => {
29751
+ const condensed = /oswald|bebas|narrow|condensed|compressed|league gothic|anton/i.test(t.fontFamily) ? 0.8 : 1;
29752
+ const weight3 = 0.92 + (t.fontWeight - 400) / 2500;
29753
+ let em = 0;
29754
+ for (const ch of text6) {
29755
+ if (ch === " ") em += 0.28;
29756
+ else if (NARROW.has(ch)) em += 0.3;
29757
+ else if (WIDE.has(ch)) em += 0.86;
29758
+ else if (ch === "/") em += 0.4;
29759
+ else if (CAPS.test(ch)) em += 0.64;
29760
+ else em += 0.54;
29761
+ }
29762
+ return em * condensed * weight3 * t.fontSize;
29763
+ };
29764
+ function textRows(text6) {
29765
+ const rows = String(text6 ?? "").replace(/\\n/g, "\n").replace(/\r\n?/g, "\n").replace(/\t/g, " ").split("\n");
29766
+ return rows.length && rows.some((r) => r.length) ? rows : [" "];
29767
+ }
29768
+ var CAP = 0.74;
29769
+ var DESCENT = 0.24;
29770
+ function layoutText(text6, t, pad, measure = estimateTextWidth) {
29771
+ const rows = textRows(text6);
29772
+ const pitch = t.fontSize * t.lineHeight;
29773
+ const measured = rows.map((r, i) => {
29774
+ const glyphs = [...r].length;
29775
+ let w;
29776
+ try {
29777
+ w = measure(r, t);
29778
+ } catch {
29779
+ w = estimateTextWidth(r, t);
29780
+ }
29781
+ if (!Number.isFinite(w) || w < 0) w = estimateTextWidth(r, t);
29782
+ return { text: r, y: i * pitch, width: w + t.letterSpacing * Math.max(0, glyphs - 1) };
29783
+ });
29784
+ const width = Math.max(t.fontSize * 0.3, ...measured.map((r) => r.width));
29785
+ const textAnchor = t.align;
29786
+ const anchorX = textAnchor === "start" ? -width / 2 : textAnchor === "end" ? width / 2 : 0;
29787
+ const top = -t.fontSize * CAP - t.strokeWidth;
29788
+ const bottom = (rows.length - 1) * pitch + t.fontSize * DESCENT + t.strokeWidth;
29789
+ const box = { x: -width / 2 - t.strokeWidth, y: top, width: width + 2 * t.strokeWidth, height: bottom - top };
29790
+ return {
29791
+ rows: measured,
29792
+ anchorX,
29793
+ textAnchor,
29794
+ box,
29795
+ viewBox: { x: box.x - pad, y: box.y - pad, width: box.width + 2 * pad, height: box.height + 2 * pad },
29796
+ pad
29797
+ };
29798
+ }
29799
+
29800
+ // src/motion/neon/wall.ts
29801
+ var DEFAULT_NEON_WALL = {
29802
+ enabled: true,
29803
+ seed: 7356951,
29804
+ brickWidth: 150,
29805
+ brickHeight: 52,
29806
+ mortar: 4,
29807
+ brickColor: "#0D1230",
29808
+ mortarColor: "#030409",
29809
+ variation: 0.5,
29810
+ texture: 0.5,
29811
+ ambientColor: "#1F2FD0",
29812
+ ambient: 0.55,
29813
+ vignette: 0.85,
29814
+ lightSpill: 1,
29815
+ spillRadius: 260,
29816
+ glitchAffectsWall: true
29817
+ };
29818
+
29819
+ // src/motion/neon/scene.ts
29820
+ var DEFAULT_NEON_AUDIO = {
29821
+ enabled: false,
29822
+ muted: false,
29823
+ master: 0.7,
29824
+ hum: 0.45,
29825
+ flicker: 0.45,
29826
+ dip: 0.55,
29827
+ failure: 0.6,
29828
+ glitch: 0.3,
29829
+ recovery: 0.45,
29830
+ scrub: true,
29831
+ scrubVolume: 0.5
29832
+ };
29833
+ var common = (id, seed) => ({
29834
+ id,
29835
+ x: 800,
29836
+ y: 500,
29837
+ scale: 1,
29838
+ rotation: 0,
29839
+ opacity: 1,
29840
+ visible: true,
29841
+ locked: false,
29842
+ neon: { ...DEFAULT_NEON_CONFIG.neon, shadow: { ...DEFAULT_NEON_CONFIG.neon.shadow } },
29843
+ animation: { ...DEFAULT_NEON_CONFIG.animation, seed }
29844
+ });
29845
+ var DEFAULT_NEON_ELEMENTS = {
29846
+ text: { ...common("text-1", 7356951), type: "text", text: "CAFE", typography: { ...DEFAULT_NEON_CONFIG.typography, fontSize: 300, strokeWidth: 10.5, letterSpacing: 6 } },
29847
+ emoji: { ...common("emoji-1", 7356952), type: "emoji", emoji: "\u2615", size: 240, strokeWidth: 8, construction: "outline", fontWeight: 500, fontFamily: NEON_EMOJI_FONT.family },
29848
+ artwork: { ...common("artwork-1", 7356953), type: "artwork", artwork: { kind: "library", id: "heart" }, size: 260, strokeWidth: 8, construction: "outline" }
29849
+ };
29850
+ var DEFAULT_NEON_SCENE = {
29851
+ type: "neonScene",
29852
+ version: 1,
29853
+ stage: { width: 1600, height: 1e3 },
29854
+ elements: [DEFAULT_NEON_ELEMENTS.text],
29855
+ wall: DEFAULT_NEON_WALL,
29856
+ audio: DEFAULT_NEON_AUDIO,
29857
+ timeline: { duration: 8e3, loop: true, triggers: [] }
29858
+ };
29859
+ var isObj6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
29860
+ function merge(base4, over) {
29861
+ if (!isObj6(base4) || !isObj6(over)) return over === void 0 ? base4 : over;
29862
+ const out = { ...base4 };
29863
+ for (const [k, v] of Object.entries(over)) {
29864
+ if (v === void 0) continue;
29865
+ out[k] = isObj6(out[k]) && isObj6(v) ? merge(out[k], v) : v;
29866
+ }
29867
+ return out;
29868
+ }
29869
+ function withDerivedColours(el, input) {
29870
+ const n = isObj6(input) && isObj6(input.neon) ? input.neon : void 0;
29871
+ const primary = n && typeof n.color === "string" ? n.color : n && typeof n.tubeColor === "string" ? n.tubeColor : void 0;
29872
+ if (!n || !isHexColor(primary)) return el;
29873
+ const d = deriveNeonPalette(primary);
29874
+ const neon = { ...el.neon, color: primary };
29875
+ for (const k of ["coreColor", "tubeColor", "glowColor", "bloomColor"]) if (n[k] === void 0) neon[k] = d[k];
29876
+ return { ...el, neon };
29877
+ }
29878
+ function resolveNeonElement(input, index = 0) {
29879
+ const type = isObj6(input) && (input.type === "emoji" || input.type === "artwork") ? input.type : "text";
29880
+ const base4 = DEFAULT_NEON_ELEMENTS[type];
29881
+ const seeded = { ...base4, id: `${type}-${index + 1}`, animation: { ...base4.animation, seed: base4.animation.seed + index * 101 } };
29882
+ const el = merge(seeded, isObj6(input) ? input : {});
29883
+ return withDerivedColours({ ...el, type }, input);
29884
+ }
29885
+ function resolveNeonScene(input) {
29886
+ const i = isObj6(input) ? input : {};
29887
+ const elements = Array.isArray(i.elements) ? i.elements.map((e, n) => resolveNeonElement(e, n)) : DEFAULT_NEON_SCENE.elements;
29888
+ return {
29889
+ ...DEFAULT_NEON_SCENE,
29890
+ stage: merge(DEFAULT_NEON_SCENE.stage, i.stage),
29891
+ wall: merge(DEFAULT_NEON_SCENE.wall, i.wall),
29892
+ audio: merge(DEFAULT_NEON_SCENE.audio, i.audio),
29893
+ timeline: merge(DEFAULT_NEON_SCENE.timeline, i.timeline),
29894
+ elements
29895
+ };
29896
+ }
29897
+ var num9 = (v, min, max) => typeof v === "number" && Number.isFinite(v) && v >= min && v <= max;
29898
+ var WALL_RANGES = [
29899
+ ["brickWidth", 20, 1e3],
29900
+ ["brickHeight", 10, 500],
29901
+ ["mortar", 0, 40],
29902
+ ["variation", 0, 1],
29903
+ ["texture", 0, 1],
29904
+ ["ambient", 0, 1],
29905
+ ["vignette", 0, 1],
29906
+ ["lightSpill", 0, 2],
29907
+ ["spillRadius", 0, 2e3],
29908
+ ["seed", 0, 4294967295]
29909
+ ];
29910
+ var AUDIO_KEYS = ["master", "hum", "flicker", "dip", "failure", "glitch", "recovery", "scrubVolume"];
29911
+ function textLike(el) {
29912
+ if (el.type === "text") return { text: el.text, typography: el.typography };
29913
+ return {
29914
+ text: emojiText(el.emoji),
29915
+ typography: { fontFamily: el.fontFamily, fontSize: el.size, fontWeight: el.fontWeight, fontStyle: "normal", letterSpacing: 0, lineHeight: 1, strokeWidth: el.strokeWidth, construction: el.construction, align: "middle" }
29916
+ };
29917
+ }
29918
+ var emojiText = (e) => `${String(e ?? "").replace(/️/g, "")}\uFE0E`;
29919
+ function validateNeonElement(el, at = "") {
29920
+ if (!isObj6(el)) return [{ path: at, message: "An element must be an object." }];
29921
+ const errors = [];
29922
+ const p = (k) => at ? `${at}.${k}` : k;
29923
+ if (!["text", "emoji", "artwork"].includes(el.type)) errors.push({ path: p("type"), message: "Must be text, emoji or artwork." });
29924
+ if (typeof el.id !== "string" || !el.id) errors.push({ path: p("id"), message: "Must be a non-empty id." });
29925
+ for (const [k, lo, hi] of [["x", -5e3, 1e4], ["y", -5e3, 1e4], ["scale", 0.01, 20], ["rotation", -360, 360], ["opacity", 0, 1]]) {
29926
+ if (!num9(el[k], lo, hi)) errors.push({ path: p(k), message: `Must be a number between ${lo} and ${hi}.` });
29927
+ }
29928
+ if (typeof el.visible !== "boolean") errors.push({ path: p("visible"), message: "Must be true or false." });
29929
+ if (typeof el.locked !== "boolean") errors.push({ path: p("locked"), message: "Must be true or false." });
29930
+ const asText = { ...DEFAULT_NEON_CONFIG, neon: el.neon, animation: el.animation };
29931
+ for (const e of validateNeonConfig(asText)) if (e.path.startsWith("neon") || e.path.startsWith("animation")) errors.push({ path: p(e.path), message: e.message });
29932
+ if (el.type === "text") {
29933
+ for (const e of validateNeonConfig({ ...DEFAULT_NEON_CONFIG, text: el.text, typography: el.typography })) if (e.path === "text" || e.path.startsWith("typography")) errors.push({ path: p(e.path), message: e.message });
29934
+ } else if (el.type === "emoji" || el.type === "artwork") {
29935
+ if (!num9(el.size, 8, 2e3)) errors.push({ path: p("size"), message: "Must be a number between 8 and 2000." });
29936
+ if (!num9(el.strokeWidth, 0.5, 80)) errors.push({ path: p("strokeWidth"), message: "Must be a number between 0.5 and 80." });
29937
+ if (el.construction !== "outline" && el.construction !== "monoline") errors.push({ path: p("construction"), message: "Must be outline or monoline." });
29938
+ if (el.type === "emoji") {
29939
+ if (typeof el.emoji !== "string" || !el.emoji.trim() || [...el.emoji].length > 12) errors.push({ path: p("emoji"), message: "Must be one emoji (or a few characters)." });
29940
+ if (!num9(el.fontWeight, 100, 900)) errors.push({ path: p("fontWeight"), message: "Must be between 100 and 900." });
29941
+ if (typeof el.fontFamily !== "string" || !el.fontFamily.trim()) errors.push({ path: p("fontFamily"), message: "Must be a font-family." });
29942
+ } else {
29943
+ const a = el.artwork;
29944
+ if (!isObj6(a)) errors.push({ path: p("artwork"), message: "Must be { kind: library | svg | raster, \u2026 }." });
29945
+ else if (a.kind === "library") {
29946
+ if (!NEON_ARTWORK_LIBRARY[a.id]) errors.push({ path: p("artwork.id"), message: `Must be one of: ${Object.keys(NEON_ARTWORK_LIBRARY).join(", ")}.` });
29947
+ } else if (a.kind === "svg") {
29948
+ try {
29949
+ parseSvgArtwork(a.svg);
29950
+ } catch (err) {
29951
+ errors.push({ path: p("artwork.svg"), message: err.message });
29952
+ }
29953
+ } else if (a.kind === "raster") {
29954
+ if (typeof a.src !== "string" || !/^(data:image\/(png|webp|jpeg|gif);base64,|https?:\/\/|\/)/.test(a.src)) errors.push({ path: p("artwork.src"), message: "Must be a PNG/WebP data URL or an image URL." });
29955
+ if (!num9(a.width, 1, 2e4) || !num9(a.height, 1, 2e4)) errors.push({ path: p("artwork.width"), message: "Width and height must be the image\u2019s pixel size." });
29956
+ if (a.mode !== "alpha" && a.mode !== "luminance") errors.push({ path: p("artwork.mode"), message: "Must be alpha or luminance." });
29957
+ if (!num9(a.threshold, 0, 1)) errors.push({ path: p("artwork.threshold"), message: "Must be between 0 and 1." });
29958
+ } else errors.push({ path: p("artwork.kind"), message: "Must be library, svg or raster." });
29959
+ }
29960
+ }
29961
+ return errors;
29962
+ }
29963
+ function validateNeonScene(scene) {
29964
+ if (!isObj6(scene)) return [{ path: "", message: "The scene must be an object." }];
29965
+ const errors = [];
29966
+ const st = scene.stage;
29967
+ if (!isObj6(st) || !num9(st.width, 100, 8e3) || !num9(st.height, 100, 8e3)) errors.push({ path: "stage", message: "width and height must be 100\u20268000." });
29968
+ if (!Array.isArray(scene.elements)) errors.push({ path: "elements", message: "Must be a list." });
29969
+ else {
29970
+ if (scene.elements.length > 40) errors.push({ path: "elements", message: "At most 40 elements." });
29971
+ const ids = /* @__PURE__ */ new Set();
29972
+ scene.elements.forEach((el, i) => {
29973
+ errors.push(...validateNeonElement(el, `elements.${i}`));
29974
+ if (isObj6(el) && typeof el.id === "string") {
29975
+ if (ids.has(el.id)) errors.push({ path: `elements.${i}.id`, message: "Ids must be unique." });
29976
+ ids.add(el.id);
29977
+ }
29978
+ });
29979
+ }
29980
+ const w = scene.wall;
29981
+ if (!isObj6(w)) errors.push({ path: "wall", message: "Must be an object." });
29982
+ else {
29983
+ if (typeof w.enabled !== "boolean") errors.push({ path: "wall.enabled", message: "Must be true or false." });
29984
+ for (const [k, lo, hi] of WALL_RANGES) if (!num9(w[k], lo, hi)) errors.push({ path: `wall.${k}`, message: `Must be a number between ${lo} and ${hi}.` });
29985
+ for (const k of ["brickColor", "mortarColor", "ambientColor"]) if (!isHexColor(w[k])) errors.push({ path: `wall.${k}`, message: "Must be a hex colour." });
29986
+ }
29987
+ const a = scene.audio;
29988
+ if (!isObj6(a)) errors.push({ path: "audio", message: "Must be an object." });
29989
+ else {
29990
+ for (const k of ["enabled", "muted", "scrub"]) if (typeof a[k] !== "boolean") errors.push({ path: `audio.${k}`, message: "Must be true or false." });
29991
+ for (const k of AUDIO_KEYS) if (!num9(a[k], 0, 1)) errors.push({ path: `audio.${k}`, message: "Must be a number between 0 and 1." });
29992
+ }
29993
+ const t = scene.timeline;
29994
+ if (!isObj6(t) || !num9(t.duration, 500, 6e5) || typeof t.loop !== "boolean" || !Array.isArray(t.triggers)) errors.push({ path: "timeline", message: "Needs duration (500\u2026600000 ms), loop and a triggers list." });
29995
+ else t.triggers.forEach((tr, i) => {
29996
+ if (!isObj6(tr) || !["sag", "flicker", "dip", "failure", "glitch", "normal"].includes(tr.kind) || !num9(tr.at, 0, 6e5)) errors.push({ path: `timeline.triggers.${i}`, message: "Needs kind (sag, flicker, dip, failure, glitch, normal) and at (ms)." });
29997
+ });
29998
+ return errors;
29999
+ }
30000
+ var r3 = (v) => Math.round(v * 1e3) / 1e3;
30001
+ function localNeon(neon, k) {
30002
+ return { ...neon, glowRadius: neon.glowRadius * k, bloomRadius: neon.bloomRadius * k, shadow: { ...neon.shadow, blur: neon.shadow.blur * k } };
30003
+ }
30004
+ function elementGeometry(el, measure = estimateTextWidth) {
30005
+ let shape;
30006
+ let box;
30007
+ let unit2 = 1;
30008
+ let em;
30009
+ let metrics;
30010
+ if (el.type === "text" || el.type === "emoji") {
30011
+ const { text: text6, typography } = textLike(el);
30012
+ const layout = layoutText(text6, typography, 0, el.type === "emoji" ? () => typography.fontSize * 1.05 : measure);
30013
+ shape = { kind: "text", layout, typography, text: text6 };
30014
+ box = layout.box;
30015
+ em = typography.fontSize;
30016
+ metrics = { fontSize: typography.fontSize, strokeWidth: typography.strokeWidth, construction: typography.construction };
30017
+ } else {
30018
+ let vb = [0, 0, 100, 100];
30019
+ const a = el.artwork;
30020
+ try {
30021
+ if (a.kind === "raster") {
30022
+ const w = Math.max(1, el.size);
30023
+ const h = w * a.height / Math.max(1, a.width);
30024
+ vb = [0, 0, a.width >= a.height ? w : w * a.width / a.height, a.width >= a.height ? h : w];
30025
+ shape = { kind: "raster", src: a.src, width: vb[2], height: vb[3], mode: a.mode, threshold: a.threshold };
30026
+ } else {
30027
+ const art = parseSvgArtwork(a.kind === "library" ? (NEON_ARTWORK_LIBRARY[a.id] ?? NEON_ARTWORK_LIBRARY.heart).svg : a.svg);
30028
+ vb = art.viewBox;
30029
+ shape = { kind: "svg", art };
30030
+ }
30031
+ } catch (err) {
30032
+ shape = { kind: "none", reason: err.message };
30033
+ }
30034
+ const side = Math.max(vb[2], vb[3]);
30035
+ unit2 = side / Math.max(1, el.size);
30036
+ box = { x: vb[0], y: vb[1], width: vb[2], height: vb[3] };
30037
+ em = side;
30038
+ metrics = { fontSize: side, strokeWidth: el.strokeWidth * unit2, construction: el.construction };
30039
+ }
30040
+ const neon = unit2 === 1 ? el.neon : localNeon(el.neon, unit2);
30041
+ const pad = materialPad(neon, metrics);
30042
+ const cx = box.x + box.width / 2;
30043
+ const cy = box.y + box.height / 2;
30044
+ const s = el.scale / unit2;
30045
+ const transform = `translate(${r3(el.x)} ${r3(el.y)}) rotate(${r3(el.rotation)}) scale(${r3(s)}) translate(${r3(-cx)} ${r3(-cy)})`;
30046
+ const hw = box.width / 2 * s;
30047
+ const hh = box.height / 2 * s;
30048
+ const rad = el.rotation * Math.PI / 180;
30049
+ const bw = Math.abs(hw * Math.cos(rad)) + Math.abs(hh * Math.sin(rad));
30050
+ const bh = Math.abs(hw * Math.sin(rad)) + Math.abs(hh * Math.cos(rad));
30051
+ return { shape, box, unit: unit2, em, metrics, neon, pad, transform, bounds: { x: el.x - bw, y: el.y - bh, width: 2 * bw, height: 2 * bh } };
30052
+ }
30053
+ var fontFor = (id) => NEON_FONTS.find((x) => x.id === id);
30054
+ function textEl(id, text6, fontId, color4, over = {}) {
30055
+ const font = fontFor(fontId);
30056
+ return {
30057
+ id,
30058
+ type: "text",
30059
+ text: text6,
30060
+ typography: { fontFamily: font.family, fontWeight: font.weight, construction: font.construction, fontSize: 300, strokeWidth: Math.round(300 * font.stroke * 2) / 2, ...over.typography ?? {} },
30061
+ neon: color4,
30062
+ ...Object.fromEntries(Object.entries(over).filter(([k]) => k !== "typography"))
30063
+ };
30064
+ }
30065
+ var anim = (o) => o;
30066
+ var NEON_SCENE_PRESETS = [
30067
+ { id: "cafe-red", name: "CAFE RED", description: "The reference: red outline CAFE on a navy brick wall.", scene: { elements: [textEl("text-1", "CAFE", "oswald", NEON_COLOR_PRESETS.red, { animation: anim({ flickerIntensity: 0.18, flickerFrequency: 0.25, glitchFrequency: 0.1 }) })] } },
30068
+ { id: "cafe-blue", name: "CAFE BLUE", description: "CAFE in ice blue.", scene: { elements: [textEl("text-1", "CAFE", "oswald", NEON_COLOR_PRESETS.blue)] } },
30069
+ { id: "cafe-purple", name: "CAFE PURPLE", description: "CAFE in violet.", scene: { elements: [textEl("text-1", "CAFE", "oswald", NEON_COLOR_PRESETS.purple)] } },
30070
+ { id: "cafe-green", name: "CAFE GREEN", description: "CAFE in acid green.", scene: { elements: [textEl("text-1", "CAFE", "oswald", NEON_COLOR_PRESETS.green)] } },
30071
+ {
30072
+ id: "retro-red",
30073
+ name: "RETRO RED",
30074
+ description: "A 1950s diner sign: Righteous, warm red, a tired transformer.",
30075
+ scene: {
30076
+ elements: [
30077
+ textEl("text-1", "DINER", "righteous", { color: "#FF3040" }, { y: 470, animation: anim({ flickerIntensity: 0.3, flickerFrequency: 0.45, flickerRandomness: 0.8, glitchEnabled: false }) }),
30078
+ { id: "artwork-1", type: "artwork", artwork: { kind: "library", id: "star" }, size: 170, x: 1370, y: 300, rotation: 12, neon: { color: "#FFC24A" }, animation: { seed: 91 } }
30079
+ ],
30080
+ wall: { ambientColor: "#3A1F7A", ambient: 0.45, lightSpill: 1.2 },
30081
+ audio: { hum: 0.55, failure: 0.7 }
30082
+ }
30083
+ },
30084
+ {
30085
+ id: "cyber-blue",
30086
+ name: "CYBER BLUE",
30087
+ description: "Orbitron in electric blue with frequent digital glitches.",
30088
+ scene: {
30089
+ elements: [textEl("text-1", "CYBER", "orbitron", NEON_COLOR_PRESETS.blue, { animation: anim({ flickerIntensity: 0.12, flickerFrequency: 0.15, glitchIntensity: 0.6, glitchFrequency: 0.35, glitchRgbSplit: 1.6 }) })],
30090
+ wall: { ambientColor: "#0D3DD8", ambient: 0.6 },
30091
+ audio: { glitch: 0.5 }
30092
+ }
30093
+ },
30094
+ {
30095
+ id: "purple-night",
30096
+ name: "PURPLE NIGHT",
30097
+ description: "A handwritten \u201Cnight\u201D under a neon moon.",
30098
+ scene: {
30099
+ elements: [
30100
+ textEl("text-1", "night", "sacramento", NEON_COLOR_PRESETS.purple, { y: 540, typography: { fontSize: 420, strokeWidth: 9 } }),
30101
+ { id: "emoji-1", type: "emoji", emoji: "\u{1F319}", size: 200, x: 1200, y: 290, neon: { color: "#E0B3FF" }, animation: { seed: 7356951 } }
30102
+ ],
30103
+ wall: { ambientColor: "#4A1FB8", ambient: 0.5 }
30104
+ }
30105
+ },
30106
+ {
30107
+ id: "electric-green",
30108
+ name: "ELECTRIC GREEN",
30109
+ description: "Audiowide \u201C24/7\u201D with a bolt, both on one circuit.",
30110
+ scene: {
30111
+ elements: [
30112
+ textEl("text-1", "24/7", "audiowide", NEON_COLOR_PRESETS.green, { x: 700, animation: anim({ seed: 4242, flickerIntensity: 0.22, flickerFrequency: 0.3 }) }),
30113
+ { id: "emoji-1", type: "emoji", emoji: "\u26A1", size: 230, x: 1300, y: 480, neon: { color: "#F4FF3A" }, animation: { seed: 4242, flickerIntensity: 0.22, flickerFrequency: 0.3 } }
30114
+ ],
30115
+ wall: { ambientColor: "#123A7A", ambient: 0.5 }
30116
+ }
30117
+ },
30118
+ {
30119
+ id: "warm-white",
30120
+ name: "WARM WHITE",
30121
+ description: "Tilt Neon \u201CHello\u201D in warm white; nearly steady.",
30122
+ scene: {
30123
+ elements: [textEl("text-1", "Hello", "tilt-neon", { color: "#FFE3B8", coreColor: "#FFFFFF", tubeColor: "#FFE3B8", glowColor: "#FFC98A", bloomColor: "#C98A3A" }, { typography: { fontSize: 340, strokeWidth: 12 }, animation: anim({ flickerIntensity: 0.06, flickerFrequency: 0.05, glitchEnabled: false }) })],
30124
+ wall: { ambientColor: "#2A2A6A", ambient: 0.4, lightSpill: 1.3 },
30125
+ audio: { hum: 0.35 }
30126
+ }
30127
+ }
30128
+ ];
30129
+ var NEON_NAMED_COLORS = {
30130
+ red: NEON_COLOR_PRESETS.red.color,
30131
+ blue: NEON_COLOR_PRESETS.blue.color,
30132
+ purple: NEON_COLOR_PRESETS.purple.color,
30133
+ green: NEON_COLOR_PRESETS.green.color,
30134
+ pink: "#FF4FB8",
30135
+ magenta: "#FF2BD6",
30136
+ cyan: "#2BF7FF",
30137
+ teal: "#1FFFD0",
30138
+ amber: "#FFB300",
30139
+ orange: "#FF7A1A",
30140
+ yellow: "#F4FF3A",
30141
+ white: "#F2F6FF",
30142
+ "warm white": "#FFE3B8",
30143
+ "cool white": "#DDEBFF",
30144
+ violet: "#9C5BFF",
30145
+ lime: "#B6FF2B"
30146
+ };
30147
+ var NEON_FLICKER_LEVELS = {
30148
+ none: { flickerEnabled: false, flickerIntensity: 0, flickerFrequency: 0 },
30149
+ subtle: { flickerEnabled: true, flickerIntensity: 0.1, flickerFrequency: 0.06 },
30150
+ medium: { flickerEnabled: true, flickerIntensity: 0.2, flickerFrequency: 0.2 },
30151
+ heavy: { flickerEnabled: true, flickerIntensity: 0.3, flickerFrequency: 0.5 },
30152
+ failing: { flickerEnabled: true, flickerIntensity: 0.6, flickerFrequency: 1.2 }
30153
+ };
30154
+ var NEON_GLITCH_LEVELS = {
30155
+ none: { glitchEnabled: false, glitchIntensity: 0, glitchFrequency: 0 },
30156
+ rare: { glitchEnabled: true, glitchIntensity: 0.15, glitchFrequency: 0.03 },
30157
+ occasional: { glitchEnabled: true, glitchIntensity: 0.3, glitchFrequency: 0.1 },
30158
+ frequent: { glitchEnabled: true, glitchIntensity: 0.5, glitchFrequency: 0.35 }
30159
+ };
30160
+ function neonColor(nameOrHex) {
30161
+ if (isHexColor(nameOrHex)) return nameOrHex.toUpperCase();
30162
+ return NEON_NAMED_COLORS[String(nameOrHex).trim().toLowerCase().replace(/[-_]+/g, " ")];
30163
+ }
30164
+ function neonElementFromSpec(spec, index = 0) {
30165
+ const type = spec.type ?? (spec.emoji ? "emoji" : spec.artwork ? "artwork" : "text");
30166
+ const out = { type, id: spec.id ?? `${type}-${index + 1}` };
30167
+ const color4 = spec.color ? neonColor(spec.color) : void 0;
30168
+ if (color4) out.neon = { color: color4 };
30169
+ const flicker = spec.flicker ? NEON_FLICKER_LEVELS[spec.flicker] : void 0;
30170
+ const glitch = spec.glitch ? NEON_GLITCH_LEVELS[spec.glitch] : void 0;
30171
+ if (flicker || glitch) out.animation = { ...flicker, ...glitch };
30172
+ if (type === "text") {
30173
+ out.text = spec.text ?? "OPEN";
30174
+ const font = spec.font ? findNeonFont(spec.font) : void 0;
30175
+ const size = spec.size ?? 300;
30176
+ out.typography = { fontSize: size, ...font ? { fontFamily: font.family, fontWeight: font.weight, construction: font.construction, strokeWidth: Math.max(2, Math.round(size * font.stroke * 2) / 2) } : {} };
30177
+ } else if (type === "emoji") {
30178
+ out.emoji = spec.emoji ?? "\u2B50";
30179
+ out.size = spec.size ?? 240;
30180
+ } else {
30181
+ const art = spec.artwork ?? "heart";
30182
+ out.artwork = NEON_ARTWORK_LIBRARY[art] ? { kind: "library", id: art } : { kind: "svg", svg: art };
30183
+ out.size = spec.size ?? 260;
30184
+ }
30185
+ return out;
30186
+ }
30187
+
30188
+ // tools/ask/neon.ts
30189
+ var NEON_TEMPLATE = "neon-sign";
30190
+ var NEON_RENDER_TOOL = "render_neon_sign";
30191
+ var NEON_FORMATS = ["mp4", "webm", "gif"];
30192
+ var STAGE_WIDTH = 1600;
30193
+ var DEFAULT_FONT = "oswald";
30194
+ var DEFAULT_SEED = 7356951;
30195
+ var COLOR_NAMES = Object.keys(NEON_NAMED_COLORS);
30196
+ var FONT_IDS = NEON_FONTS.map((f2) => f2.id);
30197
+ var FLICKER = Object.keys(NEON_FLICKER_LEVELS);
30198
+ var GLITCH = Object.keys(NEON_GLITCH_LEVELS);
30199
+ var ARTWORK = Object.keys(NEON_ARTWORK_LIBRARY);
30200
+ var TRIGGER_KINDS = ["sag", "flicker", "dip", "failure", "glitch", "normal"];
30201
+ var colorField = (what) => ({ type: "string", description: `${what} A hex colour (#FF4FB8) or a name: ${COLOR_NAMES.join(", ")}.` });
30202
+ var LOOK = {
30203
+ color: colorField("The neon colour; core, tube, glow and bloom derive from it unless given."),
30204
+ tubeColor: colorField("The lit glass."),
30205
+ coreColor: colorField("The hot centre line."),
30206
+ glowColor: colorField("The tight halo around the tube."),
30207
+ bloomColor: colorField("The wide light thrown on the wall."),
30208
+ font: { type: "string", enum: FONT_IDS, description: "A neon font by id." },
30209
+ fontSize: { type: "number", description: "Letter height in stage units (the stage is 1600 wide). Default 300. Signs wider than the stage are scaled to fit." },
30210
+ flicker: { type: "string", enum: FLICKER, description: "How much the tube flickers." },
30211
+ glitch: { type: "string", enum: GLITCH, description: "How often the sign glitches." }
30212
+ };
30213
+ var NEON_INPUT_SCHEMA = {
30214
+ type: "object",
30215
+ properties: {
30216
+ text: { type: "string", description: 'The sign, when it is one piece in one colour ("OPEN"; "\\n" starts a new line). For several pieces or colours use `elements`.' },
30217
+ elements: {
30218
+ type: "array",
30219
+ description: 'The pieces of the sign, in reading order. Each has its own colour, font and animation, so "EG in pink, LAB in green" is two text elements. Pieces without x/y are placed side by side (see `layout`, `gap`) and centred. The scene contains exactly these and nothing else.',
30220
+ items: {
30221
+ type: "object",
30222
+ properties: {
30223
+ type: { type: "string", enum: ["text", "emoji", "artwork"], description: "Default: text, or emoji / artwork when that field is given." },
30224
+ text: { type: "string", description: "The letters of a text element." },
30225
+ emoji: { type: "string", description: "One emoji, drawn as a neon outline." },
30226
+ artwork: { type: "string", description: `A library shape (${ARTWORK.join(", ")}) or SVG markup.` },
30227
+ ...LOOK,
30228
+ size: { type: "number", description: "Emoji or artwork size in stage units." },
30229
+ x: { type: "number", description: "Centre x in stage units (0\u20261600). Omit to place automatically." },
30230
+ y: { type: "number", description: "Centre y in stage units. Omit to place automatically." },
30231
+ scale: { type: "number" },
30232
+ rotation: { type: "number", description: "Degrees, clockwise." },
30233
+ opacity: { type: "number", description: "0\u20261." },
30234
+ id: { type: "string", description: "A name for `triggers[].target`." },
30235
+ neon: { type: "object", description: "Advanced material overrides: intensity (0\u20263), bloomIntensity, tubeBrightness, coreBrightness, glowRadius, bloomRadius, coreWidth, glassOpacity, shadow { enabled, color, opacity, offsetX, offsetY, blur }." },
30236
+ animation: { type: "object", description: "Advanced animation overrides: speed, seed, flickerEnabled, flickerIntensity (0\u20261), flickerFrequency (events per second), flickerSpeed, flickerRandomness, flickerRecovery, flickerMinBrightness, glitchEnabled, glitchIntensity (0\u20261), glitchFrequency, glitchDuration (ms), glitchDisplacement, glitchRgbSplit, glitchSlices, glitchGlowSeparation." },
30237
+ typography: { type: "object", description: "Advanced letter overrides: letterSpacing, lineHeight, strokeWidth, fontWeight, fontStyle (normal | italic), construction (outline | monoline), align (start | middle | end)." }
30238
+ }
30239
+ }
30240
+ },
30241
+ ...LOOK,
30242
+ layout: { type: "string", enum: ["row", "column"], description: "How automatically placed elements are arranged. Default row." },
30243
+ gap: { type: "number", description: "Space between neighbouring elements in stage units. Default 0 in a row: adjacent text elements sit a letter apart and read as one word; about 0.3 \xD7 fontSize is a word space." },
30244
+ sync: { type: "boolean", description: "true (default): every element flickers together, as one sign on one circuit. false: each flickers on its own." },
30245
+ seed: { type: "number", description: "Same seed, same performance. Change it for a different, equally natural one." },
30246
+ background: {
30247
+ type: "object",
30248
+ description: "What is behind the sign. Default: a dark navy brick wall lit by the neon.",
30249
+ properties: {
30250
+ type: { type: "string", enum: ["brick-wall", "none"], description: "none: a flat `color` and no wall." },
30251
+ color: { type: "string", description: "Hex. The flat background when type is none. Default #000000." },
30252
+ brickColor: { type: "string", description: "Hex. Darker reads as a darker wall." },
30253
+ mortarColor: { type: "string", description: "Hex." },
30254
+ ambientColor: { type: "string", description: "Hex. The room's own light on the wall." },
30255
+ ambient: { type: "number", description: "0\u20261. Lower is a darker room." },
30256
+ lightSpill: { type: "number", description: "0\u20262. How strongly the neon lights the wall." },
30257
+ spillRadius: { type: "number", description: "How far that light reaches, stage units." },
30258
+ vignette: { type: "number", description: "0\u20261. Edge darkening." },
30259
+ texture: { type: "number", description: "0\u20261. Surface grain." },
30260
+ variation: { type: "number", description: "0\u20261. Brick-to-brick shade variation." },
30261
+ brickWidth: { type: "number" },
30262
+ brickHeight: { type: "number" },
30263
+ mortar: { type: "number" },
30264
+ seed: { type: "number" }
30265
+ }
30266
+ },
30267
+ triggers: {
30268
+ type: "array",
30269
+ description: "Scripted moments on top of the natural flicker.",
30270
+ items: {
30271
+ type: "object",
30272
+ properties: {
30273
+ kind: { type: "string", enum: TRIGGER_KINDS },
30274
+ at: { type: "number", description: "Seconds from the start." },
30275
+ target: { type: "string", description: "An element id; omit for every element." }
30276
+ },
30277
+ required: ["kind", "at"]
30278
+ }
30279
+ },
30280
+ duration: { type: "number", description: "Seconds, 0.5\u202660. Default 6." },
30281
+ fps: { type: "number", description: "Frames per second, 1\u202660. Default 30." },
30282
+ format: { type: "string", enum: [...NEON_FORMATS], description: "Default mp4 (H.264)." },
30283
+ width: { type: "number", description: "Output width in pixels, 320\u20263840. Default 1920." },
30284
+ height: { type: "number", description: "Output height in pixels, 180\u20262160. Default 1080." }
30285
+ }
30286
+ };
30287
+ var TOP_KEYS = Object.keys(NEON_INPUT_SCHEMA.properties);
30288
+ var ELEMENT_KEYS = Object.keys(NEON_INPUT_SCHEMA.properties.elements.items.properties);
30289
+ var BACKGROUND_KEYS = Object.keys(NEON_INPUT_SCHEMA.properties.background.properties);
30290
+ var WALL_NUMBERS = ["ambient", "lightSpill", "spillRadius", "vignette", "texture", "variation", "brickWidth", "brickHeight", "mortar", "seed"];
30291
+ var WALL_COLORS = ["brickColor", "mortarColor", "ambientColor"];
30292
+ var COLOR_KEYS2 = ["color", "tubeColor", "coreColor", "glowColor", "bloomColor"];
30293
+ var isObj7 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
30294
+ var even = (n) => Math.max(2, Math.round(n / 2) * 2);
30295
+ var r32 = (v) => Math.round(v * 1e3) / 1e3;
30296
+ function elementInput(spec, defaults, i, at, sync, seed, errors) {
30297
+ const before = errors.length;
30298
+ const bad = (key, message) => errors.push({ path: `${at}.${key}`, message });
30299
+ for (const k of Object.keys(spec)) if (!ELEMENT_KEYS.includes(k)) bad(k, `Unknown key. An element takes: ${ELEMENT_KEYS.join(", ")}.`);
30300
+ const type = spec.type ?? (spec.emoji !== void 0 ? "emoji" : spec.artwork !== void 0 ? "artwork" : "text");
30301
+ if (!["text", "emoji", "artwork"].includes(type)) {
30302
+ bad("type", "Must be text, emoji or artwork.");
30303
+ return null;
30304
+ }
30305
+ const content = spec[type];
30306
+ if (typeof content !== "string" || !content.trim()) bad(type, type === "text" ? "A text element needs its text." : type === "emoji" ? "An emoji element needs its emoji." : `An artwork element needs a library shape (${ARTWORK.join(", ")}) or SVG markup.`);
30307
+ const pick = (k) => spec[k] ?? defaults[k];
30308
+ const colors = {};
30309
+ for (const k of COLOR_KEYS2) {
30310
+ const v = pick(k);
30311
+ if (v === void 0) continue;
30312
+ const hex = typeof v === "string" ? neonColor(v) : void 0;
30313
+ if (hex) colors[k] = hex;
30314
+ else errors.push({ path: `${spec[k] !== void 0 ? `${at}.` : ""}${k}`, message: `Must be a hex colour like #FF4FB8 or one of: ${COLOR_NAMES.join(", ")}.` });
30315
+ }
30316
+ const fontQuery = pick("font") ?? DEFAULT_FONT;
30317
+ const font = typeof fontQuery === "string" ? findNeonFont(fontQuery) : void 0;
30318
+ if (!font) errors.push({ path: `${spec.font !== void 0 ? `${at}.` : ""}font`, message: `Must be one of: ${FONT_IDS.join(", ")}.` });
30319
+ const flicker = pick("flicker");
30320
+ if (flicker !== void 0 && !FLICKER.includes(flicker)) errors.push({ path: `${spec.flicker !== void 0 ? `${at}.` : ""}flicker`, message: `Must be one of: ${FLICKER.join(", ")}.` });
30321
+ const glitch = pick("glitch");
30322
+ if (glitch !== void 0 && !GLITCH.includes(glitch)) errors.push({ path: `${spec.glitch !== void 0 ? `${at}.` : ""}glitch`, message: `Must be one of: ${GLITCH.join(", ")}.` });
30323
+ for (const k of ["size", "x", "y", "scale", "rotation", "opacity"]) if (spec[k] !== void 0 && (typeof spec[k] !== "number" || !Number.isFinite(spec[k]))) bad(k, "Must be a number.");
30324
+ const fontSize = pick("fontSize");
30325
+ if (fontSize !== void 0 && (typeof fontSize !== "number" || !Number.isFinite(fontSize))) errors.push({ path: `${spec.fontSize !== void 0 ? `${at}.` : ""}fontSize`, message: "Must be a number." });
30326
+ for (const k of ["neon", "animation", "typography"]) if (spec[k] !== void 0 && !isObj7(spec[k])) bad(k, "Must be an object.");
30327
+ if (spec.id !== void 0 && (typeof spec.id !== "string" || !spec.id)) bad("id", "Must be a non-empty id.");
30328
+ if (errors.length > before) return null;
30329
+ const base4 = neonElementFromSpec({
30330
+ type,
30331
+ [type]: content,
30332
+ id: spec.id,
30333
+ font: font.id,
30334
+ flicker,
30335
+ glitch,
30336
+ size: type === "text" ? fontSize : spec.size
30337
+ }, i);
30338
+ const out = { ...base4 };
30339
+ out.neon = { ...colors, ...spec.neon };
30340
+ out.animation = { ...base4.animation, ...sync ? { seed } : {}, ...spec.animation };
30341
+ if (type === "text") out.typography = { ...base4.typography, ...spec.typography };
30342
+ for (const k of ["x", "y", "scale", "rotation", "opacity"]) if (spec[k] !== void 0) out[k] = spec[k];
30343
+ return out;
30344
+ }
30345
+ function arrange(scene, auto, column, gap, measure) {
30346
+ const placed = scene.elements.filter((e) => auto.has(e.id));
30347
+ if (!placed.length) return scene;
30348
+ const { width: W, height: H } = scene.stage;
30349
+ const boxes = placed.map((e) => elementGeometry({ ...e, x: 0, y: 0 }, measure).bounds);
30350
+ const em = Math.max(...placed.map((e) => (e.type === "text" ? e.typography.fontSize : e.size) * e.scale));
30351
+ const g = gap ?? (column ? em * 0.12 : 0);
30352
+ const along = (b) => column ? b.height : b.width;
30353
+ const across = (b) => column ? b.width : b.height;
30354
+ const total = boxes.reduce((s, b) => s + along(b), 0) + g * (boxes.length - 1);
30355
+ const room = column ? H * 0.8 : W * 0.86;
30356
+ const roomAcross = column ? W * 0.86 : H * 0.72;
30357
+ const k = Math.min(1, room / Math.max(1, total), roomAcross / Math.max(1, ...boxes.map(across)));
30358
+ let cursor = ((column ? H : W) - total * k) / 2;
30359
+ const at = /* @__PURE__ */ new Map();
30360
+ placed.forEach((e, n) => {
30361
+ const centre = cursor + along(boxes[n]) * k / 2;
30362
+ cursor += (along(boxes[n]) + g) * k;
30363
+ at.set(e.id, column ? { x: W / 2, y: r32(centre), scale: r32(e.scale * k) } : { x: r32(centre), y: H / 2, scale: r32(e.scale * k) });
30364
+ });
30365
+ return { ...scene, elements: scene.elements.map((e) => at.has(e.id) ? { ...e, ...at.get(e.id) } : e) };
30366
+ }
30367
+ function buildNeonScene(content, measure = estimateTextWidth) {
30368
+ if (!isObj7(content)) return { valid: false, errors: [{ path: "", message: "Must be an object with `text` or `elements`." }] };
30369
+ const errors = [];
30370
+ for (const k of Object.keys(content)) if (!TOP_KEYS.includes(k)) errors.push({ path: k, message: `Unknown key. A neon sign takes: ${TOP_KEYS.join(", ")}.` });
30371
+ const specs = [];
30372
+ if (content.text !== void 0 && typeof content.text !== "string") errors.push({ path: "text", message: "Must be text." });
30373
+ else if (typeof content.text === "string" && content.text.trim()) specs.push({ spec: { text: content.text }, at: "text" });
30374
+ if (content.elements !== void 0 && !Array.isArray(content.elements)) errors.push({ path: "elements", message: "Must be a list." });
30375
+ else content.elements?.forEach((spec, i) => specs.push({ spec, at: `elements.${i}` }));
30376
+ if (!specs.length && !errors.length) errors.push({ path: "elements", message: "Nothing to draw: give `text`, or `elements` with at least one piece." });
30377
+ const num10 = (k, lo, hi, fallback2) => {
30378
+ const v = content[k];
30379
+ if (v === void 0) return fallback2;
30380
+ if (typeof v !== "number" || !Number.isFinite(v) || v < lo || v > hi) {
30381
+ errors.push({ path: k, message: `Must be a number between ${lo} and ${hi}.` });
30382
+ return fallback2;
30383
+ }
30384
+ return v;
30385
+ };
30386
+ const durationSeconds = num10("duration", 0.5, 60, 6);
30387
+ const fps = Math.round(num10("fps", 1, 60, 30));
30388
+ const width = even(num10("width", 320, 3840, 1920));
30389
+ const height = even(num10("height", 180, 2160, 1080));
30390
+ const seed = num10("seed", 0, 4294967295, DEFAULT_SEED);
30391
+ const gap = content.gap === void 0 ? void 0 : num10("gap", -2e3, 4e3, 0);
30392
+ const format = content.format ?? "mp4";
30393
+ if (!NEON_FORMATS.includes(format)) errors.push({ path: "format", message: `Must be one of: ${NEON_FORMATS.join(", ")}.` });
30394
+ if (content.layout !== void 0 && content.layout !== "row" && content.layout !== "column") errors.push({ path: "layout", message: "Must be row or column." });
30395
+ if (content.sync !== void 0 && typeof content.sync !== "boolean") errors.push({ path: "sync", message: "Must be true or false." });
30396
+ const wall = {};
30397
+ let backgroundColor = "#000000";
30398
+ const bg = content.background;
30399
+ if (bg !== void 0 && !isObj7(bg)) errors.push({ path: "background", message: "Must be an object." });
30400
+ else if (isObj7(bg)) {
30401
+ for (const k of Object.keys(bg)) if (!BACKGROUND_KEYS.includes(k)) errors.push({ path: `background.${k}`, message: `Unknown key. A background takes: ${BACKGROUND_KEYS.join(", ")}.` });
30402
+ if (bg.type !== void 0 && bg.type !== "brick-wall" && bg.type !== "none") errors.push({ path: "background.type", message: "Must be brick-wall or none." });
30403
+ wall.enabled = bg.type !== "none";
30404
+ if (bg.color !== void 0) {
30405
+ if (isHexColor(bg.color)) backgroundColor = bg.color;
30406
+ else errors.push({ path: "background.color", message: "Must be a hex colour." });
30407
+ }
30408
+ for (const k of [...WALL_COLORS, ...WALL_NUMBERS]) if (bg[k] !== void 0) wall[k] = bg[k];
30409
+ }
30410
+ const triggers = [];
30411
+ if (content.triggers !== void 0 && !Array.isArray(content.triggers)) errors.push({ path: "triggers", message: "Must be a list." });
30412
+ else content.triggers?.forEach((t, i) => {
30413
+ if (!isObj7(t) || !TRIGGER_KINDS.includes(t.kind) || typeof t.at !== "number" || t.at < 0 || t.at > durationSeconds) errors.push({ path: `triggers.${i}`, message: `Needs kind (${TRIGGER_KINDS.join(", ")}) and at (seconds, within the duration).` });
30414
+ else triggers.push({ kind: t.kind, at: Math.round(t.at * 1e3), ...typeof t.target === "string" ? { target: t.target } : {} });
30415
+ });
30416
+ const auto = /* @__PURE__ */ new Set();
30417
+ const inputs = [];
30418
+ specs.forEach(({ spec, at }, i) => {
30419
+ if (!isObj7(spec)) {
30420
+ errors.push({ path: at, message: "An element must be an object." });
30421
+ return;
30422
+ }
30423
+ const input = elementInput(spec, content, i, at, content.sync !== false, seed, errors);
30424
+ if (!input) return;
30425
+ if (spec.x === void 0 && spec.y === void 0) auto.add(String(input.id));
30426
+ inputs.push(input);
30427
+ });
30428
+ if (errors.length) return { valid: false, errors };
30429
+ const stage2 = { width: STAGE_WIDTH, height: Math.round(STAGE_WIDTH * height / width) };
30430
+ let scene = resolveNeonScene({ stage: stage2, elements: inputs, wall, timeline: { duration: Math.round(durationSeconds * 1e3), loop: false, triggers } });
30431
+ scene = arrange(scene, auto, content.layout === "column", gap, measure);
30432
+ const sceneErrors = validateNeonScene(scene).map((e) => ({ path: e.path.replace(/^wall\./, "background."), message: e.message }));
30433
+ if (sceneErrors.length) return { valid: false, errors: sceneErrors };
30434
+ return { valid: true, errors: [], scene, backgroundColor, output: { format, width, height, durationSeconds, fps, frameCount: Math.max(1, Math.round(durationSeconds * fps)) } };
30435
+ }
30436
+ var neonSceneSummary = (scene) => scene.elements.map((e) => ({
30437
+ id: e.id,
30438
+ type: e.type,
30439
+ content: e.type === "text" ? e.text : e.type === "emoji" ? e.emoji : e.artwork.kind === "library" ? e.artwork.id : "svg",
30440
+ color: e.neon.color,
30441
+ x: e.x,
30442
+ y: e.y,
30443
+ scale: e.scale
30444
+ }));
30445
+ var EXAMPLE = {
30446
+ elements: [{ text: "HELLO", color: "cyan" }, { artwork: "heart", color: "pink" }],
30447
+ font: "tilt-neon",
30448
+ flicker: "medium",
30449
+ glitch: "rare",
30450
+ gap: 60,
30451
+ background: { type: "brick-wall" },
30452
+ duration: 6,
30453
+ fps: 30,
30454
+ format: "mp4"
30455
+ };
30456
+ function listNeonAnimation() {
30457
+ return {
30458
+ template: NEON_TEMPLATE,
30459
+ version: 1,
30460
+ title: "Neon sign",
30461
+ description: `A neon sign on a brick wall: any text, emoji or artwork, each piece with its own colour and font, with natural flicker and glitches. Renders to a video file with ${NEON_RENDER_TOOL}.`,
30462
+ canvas: { width: 1920, height: 1080 },
30463
+ kind: "neonScene",
30464
+ renderer: NEON_RENDER_TOOL,
30465
+ seconds: 6,
30466
+ slots: 0,
30467
+ formats: ["MP4 (H.264)", "WebM (VP9)", "GIF", "JSON (motion/1)"]
30468
+ };
30469
+ }
30470
+ function describeNeonAnimation() {
30471
+ return {
30472
+ template: NEON_TEMPLATE,
30473
+ version: 1,
30474
+ kind: "neonScene",
30475
+ title: "Neon sign",
30476
+ description: listNeonAnimation().description,
30477
+ render: { tool: NEON_RENDER_TOOL, arguments: "The `content` below, plus an optional outputPath. Returns the video file's path." },
30478
+ request: { schema: "motion/1", template: NEON_TEMPLATE, content: "<object: see inputSchema>" },
30479
+ output: { width: 1920, height: 1080, fps: 30, seconds: 6, formats: [...NEON_FORMATS] },
30480
+ rules: [
30481
+ "The scene contains exactly the elements you send. Nothing is added.",
30482
+ "One colour per element: text in two colours is two text elements, in reading order.",
30483
+ "Elements without x/y are placed side by side and centred, a letter apart, so neighbouring text reads as one word; set `gap` to separate them.",
30484
+ "Sizes and positions are stage units on a stage 1600 wide, whatever the output resolution.",
30485
+ "Unknown keys, colours, fonts and levels are refused, never guessed."
30486
+ ],
30487
+ inputSchema: NEON_INPUT_SCHEMA,
30488
+ colors: NEON_NAMED_COLORS,
30489
+ fonts: NEON_FONTS.map((f2) => ({ id: f2.id, name: f2.name, category: f2.category })),
30490
+ flicker: FLICKER,
30491
+ glitch: GLITCH,
30492
+ artwork: ARTWORK,
30493
+ example: { schema: "motion/1", template: NEON_TEMPLATE, content: EXAMPLE }
30494
+ };
30495
+ }
30496
+ function validateNeonRequest(request) {
30497
+ if (!isObj7(request) || request.schema !== "motion/1" || request.template !== NEON_TEMPLATE) {
30498
+ return { valid: false, errors: [{ path: "", message: `Must be { schema: "motion/1", template: "${NEON_TEMPLATE}", content }.` }] };
30499
+ }
30500
+ const b = buildNeonScene(request.content);
30501
+ if (!b.valid) return { valid: false, errors: b.errors };
30502
+ return { valid: true, seconds: b.output.durationSeconds, output: b.output, elements: neonSceneSummary(b.scene), request };
30503
+ }
30504
+
29432
30505
  // tools/ask/components.ts
29433
30506
  function listComponents() {
29434
30507
  return [
@@ -29455,17 +30528,25 @@ function checkConfig(name, preset, config) {
29455
30528
  }
29456
30529
  export {
29457
30530
  AGENT_COMPONENTS,
30531
+ NEON_INPUT_SCHEMA,
30532
+ NEON_RENDER_TOOL,
30533
+ NEON_TEMPLATE,
29458
30534
  agentAvailability,
30535
+ buildNeonScene,
29459
30536
  checkConfig,
29460
30537
  describeComponent,
29461
30538
  describeMotionTemplate,
30539
+ describeNeonAnimation,
29462
30540
  isAgentComponent,
29463
30541
  isComponent,
29464
30542
  listAgentComponents,
29465
30543
  listComponents,
29466
30544
  listMotionTemplates,
30545
+ listNeonAnimation,
29467
30546
  motionPreviewUrl,
30547
+ neonSceneSummary,
29468
30548
  searchComponents,
29469
30549
  validateMotionRequest,
30550
+ validateNeonRequest,
29470
30551
  validateRenderRequest
29471
30552
  };