volito 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/browser.js +2 -2
- package/dist/mcp.js +19 -1
- package/dist/player.js +2 -2
- package/dist/registry.json +1 -1
- package/examples/volito-intro/item.json +1 -1
- package/package.json +1 -1
package/dist/registry.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"format":1,"package":"volito","version":"0.3.0","packs":[{"format":1,"id":"essentials","name":"MONARC Essentials","version":"1.0.0","description":"The graphics every video needs — name plates, titles, calls to action, stats, lists, quotes, end screens — in any brand, any aspect.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/essentials","style":"ember","shared":"shared.js","items":["lower-third","title-card","headline","subscribe","social","callout","stat","bar-chart","steps","quote","banner","end-card","logo-sting","wipe"],"entries":[{"id":"lower-third","name":"Lower third","description":"A name plate over footage: an accent spine grows, the plate wipes in, the name rises, the role slides in.","category":"lower-third","tags":["name","speaker","intro","interview","plate"],"placement":"overlay","duration":{"default":5,"min":2.5,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"name":{"type":"text","label":"Name","default":"Priya Sharma","maxLength":32},"role":{"type":"text","label":"Role or title","default":"Product designer","maxLength":44},"side":{"type":"choice","label":"Side","options":["left","right"],"default":"left"},"look":{"type":"choice","label":"Look","description":"plate: a card behind the text · minimal: a spine and the text on a soft scrim","options":["plate","minimal"],"default":"plate"}},"beats":{"in":0.15},"parts":{"scrim":"the soft dark wash behind it","spine":"the accent bar","plate":"the card","name":"the name","role":"the role line and its dot"},"pack":"essentials","ref":"essentials/lower-third","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n // A 9:16 frame is seen on a phone: the plate reads bigger there.\n const k = $.k * (A.tall ? 1.2 : 1);\n const tin = $.beat(\"in\", 0.15);\n const right = p.side === \"right\";\n const side = right ? \"right\" : \"left\";\n const name = $.case(p.name), role = String(p.role).toUpperCase();\n const tName = landOn($, p.name, tin);\n const tRole = landOn($, p.role, tName + 0.45, tName);\n // Sizes: the name in the display face, the role as a tracked-out mono label — both cut to fit the safe width.\n const maxW = A.w * (A.tall ? 0.86 : 0.62);\n const nf = fitFont($, name, \"display\", 88 * k, maxW - 100 * k, { ls: -0.02 });\n const rf = fitFont($, role, \"mono\", 30 * k, maxW - 100 * k, { ls: 0.14 });\n const pad = 46 * k;\n const w = Math.max($.measure(name, nf), $.measure(role, rf) + 38 * k) + pad * 2;\n const h = nf.size * 1.06 + rf.size * 1.25 + pad * 1.35;\n const y = A.tall ? H * 0.6 : A.square ? H * 0.66 : H * 0.7;\n const x = right ? W - A.x - w : A.x;\n const plate = p.look !== \"minimal\";\n const tx = right ? x + w - pad - 10 * k : x + pad + 10 * k;\n const anchor = right ? \"end\" : \"start\";\n // Everything leaves the way it came: back to its side.\n const leave = { out: \"slide\", outFrom: side, distance: 30 };\n\n let s = M.part(\"scrim\", { at: tin - 0.1, in: \"fade\", dur: 0.45, out: \"fade\", vary: false },\n $.scrim(0, y - 260 * k, W, H - y + 260 * k, { to: \"bottom\", a: plate ? 0.45 : 0.62 }));\n s += M.part(\"spine\", Object.assign({ at: tin, in: \"grow-y\", origin: \"top\", ease: \"expo\", dur: 0.45, box: { x: right ? x + w - 10 * k : x, y: y, w: 10 * k, h: h } }, leave),\n $.rect(right ? x + w - 10 * k : x, y, 10 * k, h, { r: 5 * k, fill: c.accent }));\n if (plate) s += M.part(\"plate\", Object.assign({ at: tin + 0.06, in: \"wipe\", from: side, dur: 0.5, box: { x: right ? x : x + 10 * k, y: y, w: w - 10 * k, h: h } }, leave),\n $.card(right ? x : x + 10 * k, y, w - 10 * k, h, { r: $.style.radius * 0.4, shadow: \"md\" }));\n s += M.text(\"name\", Object.assign({ at: tName + 0.16, in: \"rise\", distance: 26 }, leave, nf, { x: tx, y: y + pad * 0.62, anchor: anchor, fill: plate ? c.fg : \"#FFFFFF\" }), name);\n const ry = y + pad * 0.62 + nf.size * 1.06;\n const rw = $.measure(role, rf) + 26 * k;\n s += M.part(\"role\", Object.assign({ at: tRole, in: \"slide\", from: side, distance: 10, box: { x: right ? tx - rw : tx, y: ry, w: rw, h: rf.size } }, leave),\n $.circle(right ? tx - 7 * k : tx + 7 * k, ry + rf.size * 0.62, 7 * k, { fill: c.accent }),\n $.text(role, Object.assign({}, rf, { x: right ? tx - 26 * k : tx + 26 * k, y: ry, anchor: anchor, fill: plate ? c.mute : \"rgba(255,255,255,0.78)\" })));\n return s;\n}\n","libs":[]},{"id":"title-card","name":"Title card","description":"A full-frame chapter or opening title: a kicker chip, the title built letter by letter, a brush underline, a subtitle — over the brand's ground with an outlined number drifting behind.","category":"title","tags":["chapter","section","intro","opening","title"],"placement":"full","duration":{"default":4,"min":2,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"kicker":{"type":"text","label":"Kicker","description":"a small label above the title (empty for none)","default":"Chapter two","maxLength":28},"title":{"type":"text","label":"Title","default":"Depth over reach","maxLength":48},"subtitle":{"type":"text","label":"Subtitle","default":"Why the best channels say less","maxLength":80},"number":{"type":"text","label":"Number behind","description":"a big outlined number behind the title (\"02\"; empty for none)","default":"02","maxLength":3},"align":{"type":"choice","label":"Align","options":["center","left"],"default":"center"}},"beats":{"in":0.05},"parts":{"ground":"the brand's ground behind it all","number":"the big outlined number","kicker":"the small label above","title":"the title, line by line","underline":"the brush underline","subtitle":"the line under the title"},"pack":"essentials","ref":"essentials/title-card","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.05);\n const left = p.align === \"left\";\n const x = left ? A.x : W / 2;\n const anchor = left ? \"start\" : \"middle\";\n const title = $.case(p.title);\n const tTitle = landOn($, p.title, tin + 0.3);\n // The title as big as fits: one line when it can, up to three.\n const fit = fitLines($, title, Object.assign({}, $.font(\"display\"), { ls: -0.035 }), A.w * (left ? 0.86 : 0.94), Math.min(230 * k, A.h * 0.34), 70 * k, 3);\n const lh = fit.size * 1.02;\n const sub = p.subtitle ? $.case(p.subtitle) : \"\";\n const subF = Object.assign({}, $.font(\"text\"), { size: Math.max(34 * k, Math.min(52 * k, fit.size * 0.26)) });\n const subLines = sub ? $.wrap(sub, A.w * 0.8, subF) : [];\n const kick = p.kicker ? $.case(p.kicker) : \"\";\n const chip = kick ? $.chip.size(kick, { size: 30 * k }) : { w: 0, h: 0 };\n const chipH = kick ? chip.h + 40 * k : 0;\n const total = chipH + fit.lines.length * lh + (subLines.length ? 50 * k + subLines.length * subF.size * 1.25 : 0);\n let y = H / 2 - total / 2;\n\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [W * 0.72, H * 0.3] }));\n if (p.number) {\n const nf = Object.assign({}, $.font(\"display\"), { size: Math.min(H * 0.62, W * 0.5) });\n const nx = (left ? W * 0.78 : W / 2) + ($.wave(t, 6) - 0.5) * 40 * k;\n const nw = $.measure(String(p.number), nf);\n s += M.part(\"number\", { at: tin, in: \"drop\", distance: 90, box: { x: nx - nw / 2, y: H / 2 - nf.size * 0.55, w: nw, h: nf.size } },\n $.text(String(p.number), Object.assign({}, nf, { x: nx, y: H / 2 - nf.size * 0.55, anchor: \"middle\", fill: \"none\", stroke: $.rgba(c.fg, 0.13), sw: 3 * k })));\n }\n if (kick) {\n s += M.part(\"kicker\", { at: tin + 0.14, in: \"rise\", distance: 18, box: { x: left ? x : x - chip.w / 2, y: y, w: chip.w, h: chip.h } }, $.chip(kick, x, y, { size: 30 * k, anchor: anchor, dot: true }));\n y += chipH;\n }\n // Each line built letter by letter (word by word in scripts that join), rising on a spring.\n fit.lines.forEach(function (line, i) {\n s += M.text(\"title\", Object.assign({ at: tTitle + i * 0.16, in: \"rise\", split: \"letters\", distance: fit.size * 0.6 / k, ease: \"spring(2.4, 7)\", stagger: Math.min(0.04, 0.45 / Math.max(1, line.length)) }, fit.font, { x: x, y: y + i * lh, anchor: anchor }), line);\n });\n const lastLine = fit.lines[fit.lines.length - 1];\n const lastW = $.measure(lastLine, fit.font);\n const uy = y + (fit.lines.length - 1) * lh + fit.size * 1.0;\n const ux = left ? x : x - lastW / 2;\n // The underline draws on once the last letter has landed.\n const tUnder = tTitle + (fit.lines.length - 1) * 0.16 + Math.min(0.04, 0.45 / Math.max(1, lastLine.length)) * lastLine.length + 0.15;\n const sw = Math.max(8 * k, fit.size * 0.07);\n s += M.part(\"underline\", { at: tUnder, in: \"wipe\", from: \"left\", dur: 0.4, box: { x: ux - sw, y: uy - 14 * k, w: lastW + sw * 2, h: 28 * k } }, $.underline(1, 0, ux, uy, lastW, { sw: sw, dur: 0.01 }));\n y += fit.lines.length * lh + 50 * k;\n if (subLines.length) s += M.text(\"subtitle\", Object.assign({ at: tUnder + 0.1, in: \"rise\", distance: 20, split: \"lines\", stagger: 0.08 }, subF, { x: x, y: y, anchor: anchor, fill: c.mute, lh: 1.25, maxW: A.w * 0.8 }), sub);\n return s;\n}\n","libs":[]},{"id":"headline","name":"Kinetic headline","description":"Big words that pop in one by one — on the voice when you give it a transcript — with a marker swiped under the *starred* words.","category":"text","tags":["kinetic typography","words","captions","hook","headline","text","emphasis"],"placement":"overlay","duration":{"default":3.5,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"text":{"type":"longtext","label":"Text","description":"wrap words in *stars* to mark them","default":"Stop editing. *Start directing.*","maxLength":90},"ground":{"type":"choice","label":"Behind the words","description":"none: straight on the footage (with an outline) · scrim: a soft dark wash · panel: the brand's ground, full frame","options":["scrim","none","panel"],"default":"scrim"},"position":{"type":"choice","label":"Position","options":["center","top","bottom"],"default":"center"},"pace":{"type":"number","label":"Seconds per word","description":"without a transcript","default":0.16,"min":0.04,"max":1,"step":0.01}},"beats":{"in":0.1},"parts":{"ground":"the scrim or panel behind the words","words":"the words","marker":"the marker swiped under starred words"},"pack":"essentials","ref":"essentials/headline","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n const raw = $.case(String(p.text || \"\"));\n const plain = raw.replace(/\\*/g, \"\");\n const face = $.font(\"display\");\n const fit = fitLines($, plain, Object.assign({}, face, { ls: -0.03 }), A.w * 0.94, Math.min(150 * k, A.h * 0.16), 52 * k, A.tall ? 5 : 3);\n const lh = 1.12;\n const blockH = fit.size * (1 + (fit.lines.length - 1) * lh);\n const y = p.position === \"top\" ? A.y : p.position === \"bottom\" ? A.y + A.h - blockH : H / 2 - blockH / 2;\n // Words, each with its time (on the voice when there is a transcript, else one every `pace`) and whether it is starred.\n const timed = $.sync(markedRows(raw, fit.lines), tin).reduce(function (a, row) { return a.concat(row); }, []);\n const at = timed.map(function (w, i) { return $.words.length ? w.at : tin + i * p.pace; });\n const hl = timed.map(function (w) { return w.hl; });\n const font = Object.assign({}, fit.font, { x: W / 2, y: y, anchor: \"middle\", lh: lh, maxW: A.w * 0.94 });\n const lay = M.layout(plain, font, \"words\");\n const onFootage = p.ground !== \"panel\";\n let s = \"\";\n if (p.ground === \"panel\") s += M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n else if (p.ground === \"scrim\") {\n const to = p.position === \"top\" ? \"top\" : \"bottom\";\n s += M.part(\"ground\", { at: tin - 0.1, in: \"fade\", dur: 0.4, out: \"fade\", vary: false }, p.position === \"center\" ? $.rect(0, 0, W, H, { fill: \"#000000\", op: 0.38 }) : $.scrim(0, to === \"top\" ? 0 : y - 200 * k, W, to === \"top\" ? y + blockH + 200 * k : H - y + 200 * k, { to: to, a: 0.62 }));\n }\n // The marker: one swipe behind each run of starred words, once the run's last word is said.\n let i = 0;\n while (i < lay.units.length) {\n if (!hl[i]) { i++; continue; }\n let j = i;\n while (j + 1 < lay.units.length && hl[j + 1] && lay.units[j + 1].line === lay.units[i].line) j++;\n const a = lay.units[i], b = lay.units[j];\n const box = { x: a.x - fit.size * 0.1, y: a.y + fit.size * 0.18, w: b.x + b.w - a.x + fit.size * 0.2, h: fit.size * 0.86 };\n s += M.part(\"marker\", { at: (at[j] || tin) + 0.08, in: \"grow-x\", origin: \"left\", ease: \"out\", dur: 0.32, box: box }, $.rect(box.x, box.y, box.w, box.h, { r: fit.size * 0.14, fill: c.accent, transform: $.tf({ r: -1.2, ox: box.x, oy: box.y + box.h / 2 }) }));\n i = j + 1;\n }\n s += M.text(\"words\", Object.assign({ split: \"words\", in: \"rise\", distance: fit.size * 0.4 / k, ease: \"spring(2.4, 7.5)\", unitAt: function (u) { return at[u.index] != null ? at[u.index] : tin; }, fillFor: function () { return onFootage ? \"#FFFFFF\" : c.fg; } }, font), plain);\n if (p.ground === \"none\") s = $.g({ extra: 'filter=\"url(#mg-md)\"' }, s);\n return s;\n}\n","libs":[]},{"id":"subscribe","name":"Subscribe","description":"A call to action over footage: the channel card springs in, a cursor glides onto the button and presses it, the bell rings.","category":"cta","tags":["subscribe","follow","youtube","cta","button","bell","channel"],"placement":"overlay","duration":{"default":4.5,"min":3,"max":15},"aspects":["16:9","9:16","1:1","4:5"],"props":{"channel":{"type":"text","label":"Channel","default":"MONARC Studio","maxLength":26},"detail":{"type":"text","label":"Line under it","default":"1.2M subscribers","maxLength":30},"button":{"type":"text","label":"Button","default":"Subscribe","maxLength":14},"done":{"type":"text","label":"After the press","default":"Subscribed","maxLength":14},"avatar":{"type":"image","label":"Avatar","description":"a square picture for the channel (a play mark when empty)","default":""},"position":{"type":"choice","label":"Position","options":["auto","bottom-left","bottom-right","center"],"default":"auto"},"cursor":{"type":"boolean","label":"Cursor","default":true},"bell":{"type":"boolean","label":"Bell","default":true}},"beats":{"in":0.1},"parts":{"card":"the channel card","cursor":"the cursor that presses the button"},"pack":"essentials","ref":"essentials/subscribe","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n // A 16:9 frame has room for a bigger card; a 9:16 one is seen on a phone.\n const k = $.k * (A.wide ? 1.2 : A.tall ? 1.3 : 1.1);\n const press = landOn($, p.button, Math.min(tin + 1.1, M.outAt - 0.8), tin);\n const name = $.case(p.channel);\n const nf = fitFont($, name, \"display\", 42 * k, 430 * k);\n const cw = Math.max(560 * k, 160 * k + $.measure(name, nf) + 60 * k), ch = 300 * k;\n const pos = p.position === \"auto\" ? (A.wide ? \"bottom-right\" : \"center\") : p.position;\n const x = pos === \"center\" ? (W - cw) / 2 : pos === \"bottom-left\" ? A.x : W - A.x - cw;\n const y = pos === \"center\" ? (A.tall ? H * 0.58 : (H - ch) / 2) : A.y + A.h - ch;\n\n let card = $.card(x, y, cw, ch, { shadow: \"lg\" });\n if (p.avatar) card += $.image(\"avatar\", x + 38 * k, y + 40 * k, 80 * k, 80 * k, { r: 40 * k });\n else {\n card += $.circle(x + 78 * k, y + 80 * k, 40 * k, { fill: c.accent });\n card += $.icon(\"play\", x + 60 * k, y + 62 * k, 36 * k, { color: $.on(c.accent), fill: true });\n }\n card += $.text(name, Object.assign({}, nf, { x: x + 140 * k, y: y + 46 * k }));\n if (p.detail) card += $.text(p.detail, Object.assign({}, $.font(\"mono\"), { size: 24 * k, x: x + 140 * k, y: y + 98 * k, fill: c.mute }));\n const bx = x + 44 * k, by = y + 168 * k;\n card += $.button(t, $.case(p.button), bx, by, { size: 36 * k, icon: p.bell ? \"bell\" : undefined, pressAt: press, done: $.case(p.done) });\n if (p.bell) {\n const ring = $.bump(t, press + 0.15, 0.05, 0.6);\n const bellX = x + cw - 92 * k, bellY = y + 176 * k;\n card += $.g({ transform: $.tf({ r: Math.sin((t - press) * 40) * 18 * ring, ox: bellX + 28 * k, oy: bellY }) }, $.icon(\"bell\", bellX, bellY, 56 * k, { color: ring > 0.05 ? c.accent : c.mute }));\n }\n let s = M.part(\"card\", { at: tin, in: \"rise\", distance: 70, out: \"rise\", box: { x: x, y: y, w: cw, h: ch } }, card);\n // The cursor glides in from below and presses the button.\n if (p.cursor && t > press - 0.8 && t < press + 0.9) {\n const glide = $.E.inOut3($.seg(t, press - 0.75, press - 0.04));\n const px = $.lerp(x + cw + 180 * k, bx + 150 * k, glide), py = $.lerp(Math.min(H, y + ch + 220 * k), by + 52 * k, glide);\n s += M.part(\"cursor\", { at: press - 0.8, in: \"fade\", dur: 0.2, outAt: press + 0.6, out: \"fade\", outDur: 0.3 }, $.cursor(px, py, { press: $.bump(t, press, 0.05, 0.2), scale: 2 * k }));\n }\n return s;\n}\n","libs":[]},{"id":"social","name":"Social handle","description":"Your handle in a corner: a pill with the platform's icon slides in, a follow button presses, it tucks away.","category":"social","tags":["handle","follow","instagram","tiktok","youtube","x","linkedin","username","social"],"placement":"overlay","duration":{"default":4,"min":2.5,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"handle":{"type":"text","label":"Handle","default":"@monarcstudio","maxLength":30},"platform":{"type":"choice","label":"Platform","description":"picks the icon and the line under the handle","options":["youtube","instagram","tiktok","x","linkedin","threads","website"],"default":"instagram"},"action":{"type":"text","label":"Button","description":"empty for no button","default":"Follow","maxLength":12},"done":{"type":"text","label":"After the press","default":"Following","maxLength":12},"corner":{"type":"choice","label":"Corner","options":["bottom-left","bottom-right","top-left","top-right","bottom-center"],"default":"bottom-left"}},"beats":{"in":0.15},"parts":{"pill":"the pill behind","icon":"the platform icon","handle":"the handle and platform line","button":"the follow button"},"pack":"essentials","ref":"essentials/social","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nvar PLATFORMS = {\n youtube: { icon: \"play\", line: \"YouTube\" },\n instagram: { icon: \"camera\", line: \"Instagram\" },\n tiktok: { icon: \"reel\", line: \"TikTok\" },\n x: { icon: \"x\", line: \"X\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\" },\n threads: { icon: \"chat\", line: \"Threads\" },\n website: { icon: \"globe\", line: \"Website\" },\n};\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k * 1.05, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.15);\n const pl = PLATFORMS[p.platform] || PLATFORMS.instagram;\n const hf = fitFont($, p.handle, \"display\", 40 * k, A.w * 0.5);\n const lf = Object.assign({}, $.font(\"mono\"), { size: 20 * k, ls: 0.14 });\n const hasBtn = !!String(p.action || \"\").trim();\n const b1 = hasBtn ? $.button.size($.case(p.action), { size: 26 * k }) : { w: 0, h: 0 };\n const b2 = hasBtn ? $.button.size($.case(p.done || p.action), { size: 26 * k, icon: \"check\" }) : { w: 0, h: 0 };\n const bw = Math.max(b1.w, b2.w);\n const ph = 112 * k;\n const pw = ph + Math.max($.measure(p.handle, hf), $.measure(pl.line.toUpperCase(), lf)) + 34 * k + (hasBtn ? bw + 30 * k : 10 * k);\n const corner = p.corner;\n const x = corner === \"bottom-center\" ? (W - pw) / 2 : corner.indexOf(\"left\") >= 0 ? A.x : W - A.x - pw;\n const y = corner.indexOf(\"top\") === 0 ? A.y : A.y + A.h - ph;\n const side = corner.indexOf(\"right\") >= 0 ? \"right\" : corner === \"bottom-center\" ? \"down\" : \"left\";\n const press = landOn($, p.action, tin + 0.9, tin);\n // Every part leaves the way the pill came in, off its own side.\n const leave = { out: \"slide\", outFrom: side, distance: 50 };\n\n let s = M.part(\"pill\", Object.assign({ at: tin, in: \"slide\", from: side, box: { x: x, y: y, w: pw, h: ph } }, leave), $.rect(x, y, pw, ph, { r: ph / 2, fill: c.surface, stroke: c.line, sw: 2 * k, shadow: \"lg\" }));\n s += M.part(\"icon\", Object.assign({ at: tin + 0.1, in: \"spin\", intensity: 0.12, box: { x: x + ph * 0.14, y: y + ph * 0.14, w: ph * 0.72, h: ph * 0.72 } }, leave),\n $.circle(x + ph / 2, y + ph / 2, ph * 0.36, { fill: c.accent }) +\n $.icon(pl.icon, x + ph / 2 - ph * 0.17, y + ph / 2 - ph * 0.17, ph * 0.34, { color: $.on(c.accent), sw: 2.4 }));\n s += M.part(\"handle\", Object.assign({ at: tin + 0.2, in: \"slide\", from: \"right\", distance: 8, box: { x: x + ph, y: y + ph * 0.2, w: pw - ph, h: ph * 0.6 } }, leave),\n $.text(p.handle, Object.assign({}, hf, { x: x + ph + 4 * k, y: y + ph * 0.2 })) +\n $.text(pl.line.toUpperCase(), Object.assign({}, lf, { x: x + ph + 6 * k, y: y + ph * 0.2 + hf.size * 1.08, fill: c.mute })));\n if (hasBtn) s += M.part(\"button\", Object.assign({ at: tin + 0.35, in: \"pop\", box: { x: x + pw - 22 * k - bw, y: y + (ph - b1.h) / 2, w: bw, h: b1.h } }, leave),\n $.button(t, $.case(p.action), x + pw - 22 * k - bw, y + (ph - b1.h) / 2, { size: 26 * k, pressAt: press, done: $.case(p.done || p.action) }));\n return s;\n}\n","libs":[]},{"id":"callout","name":"Callout","description":"Points at something in the shot: a ring pulses on the spot, a line draws out to a label card that pops in beside it.","category":"callout","tags":["annotation","pointer","arrow","label","highlight","tutorial","product"],"placement":"overlay","duration":{"default":4,"min":2,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"label":{"type":"text","label":"Label","default":"Export in one click","maxLength":40},"detail":{"type":"text","label":"Second line","default":"ProRes, WebM or MP4","maxLength":48},"x":{"type":"number","label":"Spot across","description":"where the spot is, 0 (left edge) to 1 (right edge)","default":0.62,"min":0,"max":1,"step":0.01},"y":{"type":"number","label":"Spot down","description":"where the spot is, 0 (top) to 1 (bottom)","default":0.44,"min":0,"max":1,"step":0.01},"side":{"type":"choice","label":"Label side","options":["auto","left","right","above","below"],"default":"auto"},"icon":{"type":"icon","label":"Icon","description":"an icon name (volito api lists them; empty for none)","default":"sparkle"}},"beats":{"in":0.1},"parts":{"pulse":"the ring pulsing on the spot","dot":"the spot","line":"the line out to the card","card":"the label card"},"pack":"essentials","ref":"essentials/callout","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n const sx = W * p.x, sy = H * p.y;\n const label = $.case(p.label);\n const lf = fitFont($, label, \"display\", 46 * k, A.w * 0.5);\n const df = Object.assign({}, $.font(\"text\"), { size: 30 * k });\n const hasIcon = !!p.icon && $.icons.indexOf(p.icon) >= 0;\n const iw = hasIcon ? 64 * k : 0;\n const cw = Math.max($.measure(label, lf), p.detail ? $.measure(p.detail, df) : 0) + iw + 72 * k;\n const ch = lf.size * 1.15 + (p.detail ? df.size * 1.35 : 0) + 56 * k;\n // The label sits where there is room: away from the nearer edge.\n let side = p.side;\n if (side === \"auto\") side = A.wide ? (p.x > 0.5 ? \"left\" : \"right\") : p.y > 0.5 ? \"above\" : \"below\";\n const gap = 150 * k;\n let cx = side === \"left\" ? sx - gap - cw : side === \"right\" ? sx + gap : sx - cw / 2;\n let cy = side === \"above\" ? sy - gap - ch : side === \"below\" ? sy + gap : sy - ch / 2;\n cx = Math.max(A.x, Math.min(W - A.x - cw, cx));\n cy = Math.max(A.y, Math.min(A.y + A.h - ch, cy));\n // The line runs from the spot to the card's nearest edge.\n const ex = side === \"left\" ? cx + cw : side === \"right\" ? cx : Math.max(cx + 30 * k, Math.min(cx + cw - 30 * k, sx));\n const ey = side === \"above\" ? cy + ch : side === \"below\" ? cy : Math.max(cy + 20 * k, Math.min(cy + ch - 20 * k, sy));\n const dl = Math.hypot(ex - sx, ey - sy) || 1;\n const r0 = 26 * k;\n const lx = sx + ((ex - sx) / dl) * r0, ly = sy + ((ey - sy) / dl) * r0;\n // The line draws out from the spot: a wipe from the spot's side.\n const lineFrom = Math.abs(ex - sx) > Math.abs(ey - sy) ? (ex > sx ? \"left\" : \"right\") : ey > sy ? \"up\" : \"down\";\n\n const pulse = ((t - tin) % 1.4) / 1.4;\n let s = M.part(\"pulse\", { at: tin, in: \"fade\", dur: 0.2, box: { x: sx - 70 * k, y: sy - 70 * k, w: 140 * k, h: 140 * k } },\n t > tin ? $.circle(sx, sy, (18 + 50 * $.E.out3(pulse)) * k, { stroke: c.accent, sw: 4 * k * (1 - pulse), op: 1 - pulse }) : \"\");\n s += M.part(\"dot\", { at: tin, in: \"pop\", box: { x: sx - 21 * k, y: sy - 21 * k, w: 42 * k, h: 42 * k } }, $.circle(sx, sy, 16 * k, { fill: c.accent, stroke: \"#FFFFFF\", sw: 5 * k, shadow: \"sm\" }));\n s += M.part(\"line\", { at: tin + 0.25, in: \"wipe\", from: lineFrom, ease: \"inOut\", dur: 0.35, box: { x: Math.min(lx, ex) - 4 * k, y: Math.min(ly, ey) - 4 * k, w: Math.abs(ex - lx) + 8 * k, h: Math.abs(ey - ly) + 8 * k } },\n $.line(lx, ly, ex, ey, { stroke: \"#FFFFFF\", sw: 4 * k }));\n let card = $.card(cx, cy, cw, ch, { shadow: \"lg\" });\n if (hasIcon) card += $.rect(cx + 28 * k, cy + 28 * k, 48 * k, 48 * k, { r: 14 * k, fill: c.tint }) + $.icon(p.icon, cx + 36 * k, cy + 36 * k, 32 * k, { color: c.accent });\n card += $.text(label, Object.assign({}, lf, { x: cx + 36 * k + iw, y: cy + 28 * k }));\n if (p.detail) card += $.text(p.detail, Object.assign({}, df, { x: cx + 36 * k + iw, y: cy + 28 * k + lf.size * 1.2, fill: c.mute }));\n s += M.part(\"card\", { at: tin + 0.55, in: \"pop\", box: { x: cx, y: cy, w: cw, h: ch }, ox: ex, oy: ey }, card);\n return s;\n}\n","libs":[]},{"id":"stat","name":"Stat","description":"One number that matters, full frame: it counts up as it lands, the label rises under it, a ring fills to the value or bars grow to it.","category":"stat","tags":["number","statistic","count","percent","growth","kpi","data"],"placement":"full","duration":{"default":4,"min":2,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"value":{"type":"number","label":"Value","default":3.2},"decimals":{"type":"number","label":"Decimals","default":1,"min":0,"max":3,"step":1},"prefix":{"type":"text","label":"Before the number","default":"","maxLength":4},"suffix":{"type":"text","label":"After the number","default":"×","maxLength":6},"label":{"type":"text","label":"Label","default":"more reach in 90 days","maxLength":48},"context":{"type":"text","label":"Small print","description":"a source or a comparison, in the mono face","default":"Across 1,200 creator channels","maxLength":60},"chart":{"type":"choice","label":"Chart","options":["bars","ring","none"],"default":"bars"},"max":{"type":"number","label":"Ring full at","description":"the value a full ring stands for (100 for a percent)","default":100,"when":{"prop":"chart","is":["ring"]}},"series":{"type":"list","label":"Bars","description":"the bars' values, the last one is the stat (empty: four steps up to it)","of":"number","default":[],"maxItems":8,"when":{"prop":"chart","is":["bars"]}}},"beats":{"in":0.1},"parts":{"ground":"the brand's ground","number":"the big number","label":"the label under it","context":"the small print","chart":"the bars or ring"},"pack":"essentials","ref":"essentials/stat","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n const tNum = landOn($, String(p.value), tin + 0.15);\n const tLabel = landOn($, p.label, tNum + 0.55, tNum);\n const dec = Math.max(0, Math.min(3, Math.round(p.decimals)));\n // The widest the counter gets (what $.counter draws at the end), so the size never jumps.\n const final = (p.prefix || \"\") + (dec ? Number(p.value).toFixed(dec) : String(Math.round(Number(p.value))).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\")) + (p.suffix || \"\");\n const chart = p.chart;\n const side = chart !== \"none\" && A.wide;\n // The number takes the room it owns: the left half beside a chart on 16:9, the width otherwise.\n const room = side ? A.w * 0.5 : A.w * 0.9;\n const big = fitFont($, final, \"display\", Math.min(340 * k, A.h * (chart !== \"none\" && !side ? 0.26 : 0.42)), room, { ls: -0.05 });\n const label = $.case(p.label);\n const lf = fitFont($, label, \"text\", Math.max(40 * k, big.size * 0.2), room);\n const cf = Object.assign({}, $.font(\"mono\"), { size: 26 * k, ls: 0.06 });\n const blockH = big.size * 1.02 + lf.size * 1.3 + (p.context ? cf.size * 2.2 : 0);\n const chartH = chart === \"none\" || side ? 0 : A.h * 0.34;\n const top = A.y + (A.h - blockH - chartH - (chartH ? 60 * k : 0)) / 2;\n const x = side ? A.x + A.w * 0.02 : W / 2;\n const anchor = side ? \"start\" : \"middle\";\n\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n // The number counts up over the time its entrance takes (at least 0.9 s).\n const np = M.progress(\"number\", { at: tNum, in: \"rise\", distance: 40, dur: 0.9 });\n const counting = $.counter(t, np.at, np.at + Math.max(0.9, np.end - np.at), Number(p.value), { dec: dec, prefix: p.prefix || \"\", suffix: p.suffix || \"\" });\n s += M.text(\"number\", Object.assign({ at: tNum, in: \"rise\", distance: 40 }, big, { x: x, y: top, anchor: anchor, fill: c.accent }), counting);\n s += M.text(\"label\", Object.assign({ at: tLabel, in: \"rise\", distance: 20 }, lf, { x: x, y: top + big.size * 1.04, anchor: anchor }), label);\n if (p.context) s += M.text(\"context\", Object.assign({ at: tLabel + 0.25, in: \"fade\", dur: 0.4 }, cf, { x: x, y: top + big.size * 1.04 + lf.size * 1.45, anchor: anchor, fill: c.mute, maxW: room }), p.context);\n\n // The chart: beside the number on 16:9, under it elsewhere.\n const box = side ? { x: A.x + A.w * 0.58, y: A.y + A.h * 0.12, w: A.w * 0.4, h: A.h * 0.76 } : { x: W / 2 - Math.min(A.w * 0.8, chartH * 1.6) / 2, y: top + blockH + 60 * k, w: Math.min(A.w * 0.8, chartH * 1.6), h: chartH };\n if (chart === \"ring\") {\n const r = Math.min(box.w, box.h) * 0.4, cx = box.x + box.w / 2, cy = box.y + box.h / 2;\n const fill = Math.max(0, Math.min(1, Number(p.value) / (Number(p.max) || 100)));\n s += M.part(\"chart\", { at: tNum - 0.05, in: \"pop\", box: { x: cx - r * 1.1, y: cy - r * 1.1, w: r * 2.2, h: r * 2.2 } },\n $.ring(cx, cy, r, fill * $.E.out3($.seg(t, tNum, tNum + 0.9)), { sw: r * 0.16 }),\n $.text(Math.round(fill * 100) + \"%\", Object.assign({}, $.font(\"mono\"), { size: r * 0.34, x: cx, y: cy - r * 0.2, anchor: \"middle\", fill: c.fg, op: $.fadeIn(t, tNum + 0.6, 0.3) })));\n } else if (chart === \"bars\") {\n const v = Number(p.value) || 1;\n const series = Array.isArray(p.series) && p.series.length >= 2 ? p.series.map(Number) : [v * 0.34, v * 0.52, v * 0.71, v];\n s += M.part(\"chart\", { at: tNum - 0.1, in: \"fade\", dur: 0.2, box: box },\n $.bars(t, tNum + 0.05, box.x, box.y, box.w, box.h, series, { highlight: series.length - 1, stagger: 0.1 }),\n $.line(box.x - 10 * k, box.y + box.h, box.x + box.w + 10 * k, box.y + box.h, { stroke: c.line, sw: 3 * k, p: $.E.out3($.seg(t, tNum - 0.1, tNum + 0.4)) }));\n }\n return s;\n}\n","libs":[]},{"id":"bar-chart","name":"Bar chart","description":"A ranked bar chart, full frame: each bar grows in turn with its value counting up beside it; one row can stand out in the accent.","category":"chart","tags":["chart","data","ranking","comparison","bars","graph","statistics"],"placement":"full","duration":{"default":6,"min":3,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"title":{"type":"text","label":"Heading","default":"Where viewers drop off","maxLength":48},"rows":{"type":"list","label":"Rows","description":"label and value per row (\"Intro: 92\")","of":{"label":"text","value":"number"},"default":[{"label":"First 3 seconds","value":92},{"label":"First 30 seconds","value":64},{"label":"Halfway","value":41},{"label":"The end","value":23}],"minItems":2,"maxItems":7},"prefix":{"type":"text","label":"Before each value","default":"","maxLength":3},"suffix":{"type":"text","label":"After each value","default":"%","maxLength":6},"decimals":{"type":"number","label":"Decimals","default":0,"min":0,"max":2,"step":1},"highlight":{"type":"number","label":"Row in the accent","description":"1 for the first row; 0 for the largest value; -1 for none","default":0,"min":-1,"max":7,"step":1},"source":{"type":"text","label":"Source","default":"Retention across 300 uploads","maxLength":60}},"beats":{"in":0.1},"parts":{"ground":"the brand's ground","title":"the heading","rows":"each row (label, bar, value)","source":"the source line"},"pack":"essentials","ref":"essentials/bar-chart","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n const rows = (p.rows || []).slice(0, 7);\n const n = Math.max(1, rows.length);\n const max = Math.max.apply(null, rows.map(function (r) { return Number(r.value) || 0; }).concat([1e-9]));\n let hi = Math.round(p.highlight);\n if (hi === 0) hi = rows.reduce(function (b, r, i) { return (Number(r.value) || 0) > (Number(rows[b].value) || 0) ? i : b; }, 0);\n else hi = hi - 1;\n const title = $.case(p.title);\n const tf = fitLines($, title, Object.assign({}, $.font(\"display\"), { ls: -0.03 }), A.w, Math.min((A.tall ? 120 : 100) * k, A.h * 0.1), 44 * k, 2);\n const headH = tf.lines.length * tf.size * 1.05 + 60 * k;\n const footH = p.source ? 70 * k : 0;\n const gapY = 26 * k;\n const rh = Math.min((A.tall ? 210 : A.wide ? 150 : 185) * k, (A.h - headH - footH - gapY * (n - 1)) / n);\n const top = A.y + (A.h - headH - footH - n * rh - (n - 1) * gapY) / 2;\n const w = A.wide ? Math.min(A.w, 1500 * k) : A.w;\n const x = (W - w) / 2;\n const fmt = function (v) { return (p.prefix || \"\") + (p.decimals ? v.toFixed(p.decimals) : String(Math.round(v)).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\")) + (p.suffix || \"\"); };\n // Labels sit above their bar on tall frames, beside it on wide ones.\n const lf = Object.assign({}, $.font(\"text\"), { size: Math.min((A.tall ? 46 : 40) * k, rh * (A.wide ? 0.4 : 0.3)) });\n const labels = rows.map(function (r) { return $.case(r.label); });\n const labelW = A.wide ? Math.min(w * 0.34, widest($, labels, lf) + 44 * k) : 0;\n const vf = Object.assign({}, $.font(\"mono\"), { size: Math.min(44 * k, rh * 0.36) });\n const valueW = $.measure(fmt(max), vf) + 30 * k;\n const barX = x + labelW;\n const barW = w - labelW - valueW;\n\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n tf.lines.forEach(function (line, i) {\n s += M.text(\"title\", Object.assign({ at: tin + i * 0.1, in: \"rise\", distance: 26 }, tf.font, { x: x, y: top + i * tf.size * 1.05 }), line);\n });\n const start = tin + 0.4;\n rows.forEach(function (r, i) {\n const at = landOn($, r.label, start + i * 0.18, start);\n const ry = top + headH + i * (rh + gapY);\n const v = Number(r.value) || 0;\n // The bar grows (and its value counts) over the row's entrance, at least 0.8 s.\n const rp = M.progress(\"rows\", { at: at - 0.05, in: \"fade\", dur: 0.25 });\n const grow = $.E.out3($.seg(t, rp.at, rp.at + 0.8));\n const bh = A.wide ? rh * 0.66 : rh * 0.48;\n const by = A.wide ? ry + (rh - bh) / 2 : ry + rh - bh;\n const col = hi === i ? c.accent : $.rgba(c.fg, 0.28);\n let row = \"\";\n if (A.wide) row += $.text(labels[i], Object.assign({}, lf, { x: x, y: ry + (rh - lf.size) / 2, fill: hi === i ? c.fg : c.mute, maxW: labelW - 20 * k }));\n else row += $.text(labels[i], Object.assign({}, lf, { x: x, y: ry, fill: hi === i ? c.fg : c.mute }));\n row += $.rect(barX, by, barW, bh, { r: bh * 0.22, fill: $.rgba(c.fg, 0.06) });\n row += $.rect(barX, by, Math.max(bh * 0.44, barW * (v / max) * grow), bh, { r: bh * 0.22, fill: col, shadow: hi === i ? \"glow\" : null });\n row += $.text(fmt(v * grow), Object.assign({}, vf, { x: barX + barW + 24 * k, y: by + (bh - vf.size) / 2 - vf.size * 0.05, fill: hi === i ? c.accent : c.fg }));\n s += M.part(\"rows\", { at: at - 0.05, in: \"fade\", dur: 0.25, box: { x: x, y: ry, w: w, h: rh } }, row);\n });\n if (p.source) s += M.text(\"source\", Object.assign({ at: start + n * 0.18 + 0.4, in: \"fade\", dur: 0.4 }, $.font(\"mono\"), { size: 24 * k, x: x, y: top + headH + n * rh + (n - 1) * gapY + 34 * k, fill: c.mute, ls: 0.06 }), p.source);\n return s;\n}\n","libs":[]},{"id":"steps","name":"Steps","description":"A numbered list or checklist, full frame: the heading lands, then each step slides in on its own card — numbered, or ticked off one by one.","category":"list","tags":["list","steps","how to","process","checklist","agenda","tips","framework"],"placement":"full","duration":{"default":6,"min":3,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"kicker":{"type":"text","label":"Kicker","default":"The framework","maxLength":28},"title":{"type":"text","label":"Heading","default":"Three steps to a better hook","maxLength":48},"steps":{"type":"list","label":"Steps","of":"text","default":["Open on the payoff","Cut every pause","Land one idea per shot"],"minItems":1,"maxItems":6},"marker":{"type":"choice","label":"Marker","options":["numbers","checks"],"default":"numbers"},"gap":{"type":"number","label":"Seconds between steps","default":0.35,"min":0.1,"max":5,"step":0.05}},"beats":{"in":0.1},"parts":{"ground":"the brand's ground","kicker":"the small label","title":"the heading","steps":"each step's card"},"pack":"essentials","ref":"essentials/steps","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n const rows = (p.steps || []).slice(0, 6);\n const n = Math.max(1, rows.length);\n const kick = p.kicker ? $.case(p.kicker) : \"\";\n const title = $.case(p.title);\n const tf = fitLines($, title, Object.assign({}, $.font(\"display\"), { ls: -0.03 }), A.w, Math.min((A.tall ? 140 : 110) * k, A.h * 0.11), 48 * k, 2);\n const chip = kick ? $.chip.size(kick, { size: 28 * k }) : { w: 0, h: 0 };\n const chipH = kick ? chip.h + 28 * k : 0;\n const headH = chipH + tf.lines.length * tf.size * 1.05 + 56 * k;\n // Rows share the room under the heading, with a gap.\n const room = A.h - headH;\n const gapY = 22 * k;\n const rh = Math.min((A.tall ? 200 : A.wide ? 150 : 180) * k, (room - gapY * (n - 1)) / n);\n const total = headH + n * rh + (n - 1) * gapY;\n let y = A.y + (A.h - total) / 2;\n const w = A.tall ? A.w : Math.min(A.w, 1500 * k);\n const ox = A.wide ? (W - w) / 2 : A.x;\n\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [W * 0.8, H * 0.2] }));\n if (kick) {\n s += M.part(\"kicker\", { at: tin, in: \"rise\", distance: 16, box: { x: ox, y: y, w: chip.w, h: chip.h } }, $.chip(kick, ox, y, { size: 28 * k, dot: true }));\n y += chipH;\n }\n tf.lines.forEach(function (line, i) {\n s += M.text(\"title\", Object.assign({ at: tin + 0.12 + i * 0.1, in: \"rise\", distance: 28 }, tf.font, { x: ox, y: y + i * tf.size * 1.05 }), line);\n });\n y += tf.lines.length * tf.size * 1.05 + 56 * k;\n const first = landOn($, rows[0], tin + 0.55);\n rows.forEach(function (row, i) {\n const at = landOn($, row, first + i * p.gap, first + (i - 1) * p.gap);\n const ry = y + i * (rh + gapY);\n const badge = rh * 0.62;\n const label = $.case(row);\n const lf = fitFont($, label, \"display\", Math.min(60 * k, rh * 0.42), w - badge - rh * 0.9);\n let card = $.card(ox, ry, w, rh, { r: Math.min($.style.radius, rh * 0.3), shadow: \"md\" });\n const bx = ox + (rh - badge) / 2, by = ry + (rh - badge) / 2;\n if (p.marker === \"checks\") {\n const done = $.E.out3($.seg(t, at + 0.25, at + 0.55));\n card += $.rect(bx, by, badge, badge, { r: badge * 0.28, fill: done > 0.5 ? c.accent : \"none\", stroke: c.accent, sw: 4 * k });\n card += $.path(\"M\" + (bx + badge * 0.24) + \" \" + (by + badge * 0.52) + \"L\" + (bx + badge * 0.43) + \" \" + (by + badge * 0.7) + \"L\" + (bx + badge * 0.78) + \" \" + (by + badge * 0.32), { stroke: $.on(c.accent), sw: badge * 0.11, draw: done });\n } else {\n card += $.rect(bx, by, badge, badge, { r: badge * 0.28, fill: c.accent });\n card += $.text(String(i + 1).padStart(2, \"0\"), Object.assign({}, $.font(\"mono\"), { size: badge * 0.42, x: bx + badge / 2, y: by + badge * 0.27, anchor: \"middle\", fill: $.on(c.accent) }));\n }\n card += $.text(label, Object.assign({}, lf, { x: ox + rh * 0.95, y: ry + (rh - lf.size) / 2 - lf.size * 0.04 }));\n s += M.part(\"steps\", { at: at, in: \"slide\", from: \"left\", distance: 33, box: { x: ox, y: ry, w: w, h: rh } }, card);\n });\n return s;\n}\n","libs":[]},{"id":"quote","name":"Quote","description":"A pull quote, full frame: a big quotation mark pops, the words set in line by line, the speaker signs it off under an accent rule.","category":"quote","tags":["quote","testimonial","review","citation","words"],"placement":"full","duration":{"default":6,"min":3,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"quote":{"type":"longtext","label":"Quote","default":"The best tools disappear. You stop thinking about the editor and start thinking about the story.","maxLength":220},"author":{"type":"text","label":"Who said it","default":"Maya Chen","maxLength":32},"role":{"type":"text","label":"Their role","default":"Documentary editor","maxLength":44},"face":{"type":"choice","label":"Typeface","description":"serif: the brand's accent face (an italic serif) · display: its headline face","options":["serif","display"],"default":"serif"}},"beats":{"in":0.1},"parts":{"ground":"the brand's ground","mark":"the quotation mark","quote":"the quote, line by line","rule":"the short accent rule","author":"who said it","role":"their role"},"pack":"essentials","ref":"essentials/quote","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n const face = p.face === \"display\" ? $.font(\"display\") : $.font(\"accent\");\n const text = String(p.quote).replace(/^[\"“”]+|[\"“”]+$/g, \"\").trim();\n const maxW = A.w * (A.wide ? 0.78 : 0.88);\n const fit = fitLines($, text, Object.assign({}, face, { ls: p.face === \"display\" ? -0.02 : 0 }), maxW, Math.min(96 * k, A.h * 0.12), 38 * k, A.tall ? 8 : 5);\n const lh = 1.22;\n const markSize = Math.max(160 * k, fit.size * 2.4);\n const af = Object.assign({}, $.font(\"display\"), { size: Math.max(34 * k, fit.size * 0.5) });\n const rf = Object.assign({}, $.font(\"mono\"), { size: Math.max(22 * k, fit.size * 0.3), ls: 0.12 });\n const blockH = markSize * 0.55 + fit.lines.length * fit.size * lh + 70 * k + af.size * 1.2 + (p.role ? rf.size * 1.6 : 0);\n const x = A.wide ? (W - maxW) / 2 : A.x;\n let y = A.y + (A.h - blockH) / 2;\n const tWords = landOn($, text, tin + 0.35);\n const leave = { out: \"drop\", distance: 24, outDur: 0.4 };\n\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [W * 0.2, H * 0.25] }));\n // The quotation mark, in the accent, springs in first.\n s += M.part(\"mark\", Object.assign({ at: tin, in: \"spin\", intensity: 0.05, box: { x: x - markSize * 0.04, y: y - markSize * 0.18, w: markSize * 0.6, h: markSize * 0.9 } }, leave),\n $.text(\"“\", Object.assign({}, $.font(\"accent\"), { size: markSize, x: x - markSize * 0.04, y: y - markSize * 0.18, fill: c.accent })));\n y += markSize * 0.55;\n s += M.text(\"quote\", Object.assign({ at: tWords, in: \"rise\", distance: 24, split: \"lines\", stagger: 0.14 }, leave, fit.font, { x: x, y: y, lh: lh, maxW: maxW }), text);\n y += fit.lines.length * fit.size * lh + 40 * k;\n const tSign = tWords + fit.lines.length * 0.14 + 0.25;\n s += M.part(\"rule\", Object.assign({ at: tSign, in: \"grow-x\", origin: \"left\", dur: 0.4, box: { x: x, y: y, w: 90 * k, h: 6 * k } }, leave), $.rect(x, y, 90 * k, 6 * k, { r: 3 * k, fill: c.accent }));\n y += 30 * k;\n s += M.text(\"author\", Object.assign({ at: tSign + 0.1, in: \"slide\", from: \"left\", distance: 8 }, leave, af, { x: x, y: y }), $.case(p.author));\n if (p.role) s += M.text(\"role\", Object.assign({ at: tSign + 0.22, in: \"slide\", from: \"left\", distance: 8 }, leave, rf, { x: x, y: y + af.size * 1.2, fill: c.mute }), String(p.role).toUpperCase());\n return s;\n}\n","libs":[]},{"id":"banner","name":"News banner","description":"A broadcast strap over footage: the label block slams in, the headline bar wipes across, a ticker scrolls under it.","category":"banner","tags":["breaking news","live","strap","ticker","chyron","broadcast","headline"],"placement":"overlay","duration":{"default":8,"min":3,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"label":{"type":"text","label":"Label","default":"Breaking","maxLength":14},"headline":{"type":"text","label":"Headline","default":"AI editors now cut a reel in under a minute","maxLength":70},"ticker":{"type":"longtext","label":"Ticker","description":"items separated by \" · \" scroll under the headline (empty for none)","default":"Creators report 3× more output · Open motion packs land on npm · Agents can now render graphics on their own","maxLength":280},"live":{"type":"boolean","label":"Live dot","default":true},"speed":{"type":"number","label":"Ticker speed","description":"px a second at 1080 px","default":140,"min":30,"max":600}},"beats":{"in":0.1},"parts":{"label":"the label block","bar":"the headline bar and ticker strip","headline":"the headline text","ticker":"the scrolling ticker text"},"pack":"essentials","ref":"essentials/banner","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.15 : 1);\n const tin = $.beat(\"in\", 0.1);\n const label = String(p.label || \"\").toUpperCase();\n const lf = Object.assign({}, $.font(\"display\"), { size: 40 * k, ls: 0.06 });\n const hf = fitFont($, $.case(p.headline), \"display\", 48 * k, A.w * (A.wide ? 0.74 : 0.92) - 60 * k);\n const tf = Object.assign({}, $.font(\"mono\"), { size: 26 * k, ls: 0.04 });\n const hasTicker = !!String(p.ticker || \"\").trim();\n const bh = 96 * k, th = hasTicker ? 52 * k : 0;\n // On 9:16 the label sits on its own row above the headline (the strap is too narrow for both).\n const stack = !A.wide;\n const lw = $.measure(label, lf) + (p.live ? 70 * k : 46 * k);\n const x = stack ? A.x : A.x * 0.6;\n const w = W - 2 * x;\n const yH = (stack ? H * 0.64 : H * 0.78) + (stack ? 70 * k : 0);\n const yL = stack ? yH - 70 * k : yH;\n const hx = stack ? x : x + lw;\n const bw = w - (hx - x);\n const leave = { out: \"rise\", distance: 30, outDur: 0.35 };\n\n // The headline bar wipes out from behind the label; the ticker strip under it.\n let s = M.part(\"bar\", Object.assign({ at: tin + 0.18, in: \"wipe\", from: \"left\", dur: 0.45, box: { x: hx, y: yH, w: bw, h: bh + th } }, leave),\n $.rect(hx, yH, bw, bh, { fill: c.surface, shadow: \"md\" }),\n hasTicker ? $.rect(hx, yH + bh, bw, th, { fill: c.ink }) : \"\");\n s += $.clip(hx, yH, bw, bh, 0, M.text(\"headline\", Object.assign({ at: tin + 0.35, in: \"rise\", distance: 24 }, leave, hf, { x: hx + 30 * k, y: yH + (bh - hf.size) / 2 - hf.size * 0.04, within: { x: hx, y: yH, w: bw, h: bh } }), $.case(p.headline)));\n if (hasTicker) {\n const items = String(p.ticker).replace(/\\s+/g, \" \").trim();\n const loop = items + \" · \";\n const lw2 = $.measure(loop, tf);\n const off = (Math.max(0, t - tin - 0.35) * p.speed * k) % lw2;\n let run = \"\";\n for (let i = 0; i < Math.ceil((w + lw2) / lw2) + 1; i++) run += loop;\n s += $.clip(hx, yH + bh, bw, th, 0, M.part(\"ticker\", Object.assign({ at: tin + 0.55, in: \"fade\", dur: 0.3, box: { x: hx, y: yH + bh, w: bw, h: th } }, leave),\n $.text(run, Object.assign({}, tf, { x: hx + 30 * k - off, y: yH + bh + (th - tf.size) / 2 - tf.size * 0.06, fill: \"#FFFFFF\", within: { x: -1e6, y: 0, w: 2e6, h: H } }))));\n }\n // The label block slams in with a jolt.\n const lh = stack ? 70 * k : bh;\n let lb = $.rect(x, yL, lw, lh, { fill: c.accent, shadow: \"md\" });\n if (p.live) lb += $.circle(x + 30 * k, yL + lh / 2, 9 * k, { fill: $.on(c.accent), op: 0.55 + 0.45 * $.wave(t, 1.1) });\n lb += $.text(label, Object.assign({}, lf, { x: x + (p.live ? 52 * k : 23 * k), y: yL + (lh - lf.size) / 2 - lf.size * 0.03, fill: $.on(c.accent) }));\n s += M.part(\"label\", Object.assign({ at: tin, in: \"slam\", intensity: 0.12, emphasis: [{ fx: \"shake\", at: tin + 0.08 }], box: { x: x, y: yL, w: lw, h: lh }, origin: \"left\" }, leave), lb);\n return s;\n}\n","libs":[]},{"id":"end-card","name":"End card","description":"An end screen, full frame: \"watch next\" slots pop in (for YouTube's end-screen elements or your own thumbnails), the subscribe button presses, the handle signs off.","category":"end-card","tags":["end screen","outro","watch next","youtube","subscribe","thumbnail"],"placement":"full","duration":{"default":10,"min":4,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"title":{"type":"text","label":"Heading","default":"Watch next","maxLength":28},"first":{"type":"text","label":"First slot","default":"How we edit a reel in 10 minutes","maxLength":48},"second":{"type":"text","label":"Second slot","default":"The hook formula","maxLength":48},"firstImage":{"type":"image","label":"First thumbnail","description":"a 16:9 picture (an empty slot when none)","default":""},"secondImage":{"type":"image","label":"Second thumbnail","default":""},"handle":{"type":"text","label":"Handle","default":"@monarcstudio","maxLength":30},"button":{"type":"text","label":"Button","default":"Subscribe","maxLength":14},"done":{"type":"text","label":"After the press","default":"Subscribed","maxLength":14}},"beats":{"in":0.1},"holdFromEnd":0.6,"parts":{"ground":"the brand's ground","title":"the heading","slots":"each watch-next slot","footer":"the button and handle"},"pack":"essentials","ref":"essentials/end-card","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n const title = $.case(p.title);\n const hf = fitFont($, title, \"display\", Math.min(110 * k, A.h * 0.1), A.w, { ls: -0.03 });\n const slotF = Object.assign({}, $.font(\"text\"), { size: 32 * k });\n // Two 16:9 slots side by side on wide frames, stacked on tall ones.\n const stack = !A.wide;\n const sw = stack ? Math.min(A.w, (A.h * (A.tall ? 0.62 : 0.72)) / 2 * (16 / 9) - 40 * k) : (A.w - 60 * k) / 2;\n const sh = (sw * 9) / 16;\n const labelH = slotF.size * 2.6;\n const headH = hf.size * 1.3;\n const footH = 130 * k;\n const gridH = stack ? 2 * (sh + labelH) + 30 * k : sh + labelH;\n const top = A.y + (A.h - headH - gridH - footH) / 2;\n const left = stack ? (W - sw) / 2 : A.x;\n\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n s += M.text(\"title\", Object.assign({ at: tin, in: \"rise\", distance: 26 }, hf, { x: W / 2, y: top, anchor: \"middle\" }), title);\n [[p.first, p.firstImage ? \"firstImage\" : \"\"], [p.second, p.secondImage ? \"secondImage\" : \"\"]].forEach(function (slot, i) {\n const sx = stack ? left : left + i * (sw + 60 * k);\n const sy = top + headH + (stack ? i * (sh + labelH + 30 * k) : 0);\n const ccx = sx + sw / 2, ccy = sy + sh / 2;\n let el = $.rect(sx, sy, sw, sh, { r: $.style.radius * 0.6, fill: $.rgba(c.fg, 0.06), stroke: $.rgba(c.fg, 0.22), sw: 3 * k, dash: slot[1] ? null : 14 * k + \" \" + 10 * k });\n if (slot[1]) el += $.image(slot[1], sx, sy, sw, sh, { r: $.style.radius * 0.6 });\n else {\n const pr = Math.min(sw, sh) * 0.14 * (1 + 0.06 * $.wave(t, 1.6, i * 0.3));\n el += $.circle(ccx, ccy, pr, { fill: c.accent, shadow: \"md\" }) + $.icon(\"play\", ccx - pr * 0.42, ccy - pr * 0.5, pr, { color: $.on(c.accent), fill: true });\n }\n $.wrap($.case(slot[0] || \"\"), sw, slotF).slice(0, 2).forEach(function (line, j) { el += $.text(line, Object.assign({}, slotF, { x: sx, y: sy + sh + 18 * k + j * slotF.size * 1.2 })); });\n s += M.part(\"slots\", { at: tin + 0.25 + i * 0.15, in: \"rise\", distance: 40, box: { x: sx, y: sy, w: sw, h: sh + labelH } }, el);\n });\n // The footer: the button presses, the handle beside it.\n const fy = top + headH + gridH + 40 * k;\n const press = tin + 1.3;\n const b1 = $.button.size($.case(p.button), { size: 34 * k, icon: \"bell\" }), b2 = $.button.size($.case(p.done), { size: 34 * k, icon: \"check\" });\n const btn = { w: Math.max(b1.w, b2.w), h: b1.h };\n const hfnt = Object.assign({}, $.font(\"mono\"), { size: 30 * k });\n const hw = p.handle ? $.measure(p.handle, hfnt) + 36 * k : 0;\n const fx = W / 2 - (btn.w + hw) / 2;\n s += M.part(\"footer\", { at: tin + 0.6, in: \"rise\", distance: 24, box: { x: fx, y: fy, w: btn.w + hw, h: btn.h } },\n $.button(t, $.case(p.button), fx, fy, { size: 34 * k, icon: \"bell\", pressAt: press, done: $.case(p.done) }),\n p.handle ? $.text(p.handle, Object.assign({}, hfnt, { x: fx + btn.w + 36 * k, y: fy + btn.h / 2 - hfnt.size * 0.55, fill: c.mute })) : \"\");\n return s;\n}\n","libs":[]},{"id":"logo-sting","name":"Logo sting","description":"A brand reveal, full frame: rings burst from the centre, the logo pops, the wordmark lands letter by letter with a light sweep, the tagline spaces out under it.","category":"logo","tags":["logo","intro","outro","brand","reveal","wordmark","sting","bumper"],"placement":"full","duration":{"default":3.5,"min":2,"max":10},"aspects":["16:9","9:16","1:1","4:5"],"props":{"wordmark":{"type":"text","label":"Wordmark","default":"MONARC","maxLength":20},"tagline":{"type":"text","label":"Tagline","default":"Made with motion","maxLength":40},"logo":{"type":"image","label":"Logo","description":"a square mark (PNG with alpha or SVG) above the wordmark; none for the wordmark alone","default":""},"burst":{"type":"boolean","label":"Burst","default":true}},"beats":{"in":0.1},"holdFromEnd":0.5,"parts":{"ground":"the brand's ground","rings":"the rings that burst out","logo":"the logo picture","wordmark":"the wordmark","tagline":"the tagline"},"pack":"essentials","ref":"essentials/logo-sting","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n const word = $.case(p.wordmark);\n const wf = fitFont($, word, \"display\", Math.min(220 * k, A.h * 0.26), A.w * 0.86, { ls: 0.02 });\n const tag = p.tagline ? String(p.tagline).toUpperCase() : \"\";\n const tfnt = fitFont($, tag, \"mono\", 30 * k, A.w * 0.8, { ls: 0.32 });\n const logo = p.logo ? Math.min(220 * k, A.h * 0.22) : 0;\n const total = logo + (logo ? 50 * k : 0) + wf.size + (tag ? 40 * k + tfnt.size : 0);\n const cx = W / 2, cy = H / 2;\n let y = cy - total / 2;\n const tWord = tin + (logo ? 0.55 : 0.3);\n const leave = { out: \"push\", intensity: 0.06 };\n\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [cx, cy] }));\n if (p.burst) {\n let rings = \"\";\n for (let i = 0; i < 3; i++) {\n const a = tin + i * 0.12;\n const q = $.E.out3($.seg(t, a, a + 0.9));\n if (q > 0 && q < 1) rings += $.circle(cx, cy, (60 + 700 * q) * k, { stroke: i === 1 ? c.accent2 : c.accent, sw: (14 - i * 3) * k * (1 - q), op: 1 - q });\n }\n rings += $.burst(t, tWord, cx, cy, { n: 18, r: Math.max(W, H) * 0.32, colors: [c.accent, c.accent2, c.fg], size: 10 * k });\n s += M.part(\"rings\", { at: tin, in: \"none\", out: \"fade\", vary: false }, rings);\n }\n if (logo) {\n s += M.part(\"logo\", Object.assign({ at: tin + 0.15, in: \"pop\", box: { x: cx - logo / 2, y: y, w: logo, h: logo } }, leave), $.image(\"logo\", cx - logo / 2, y, logo, logo, { fit: \"contain\" }));\n y += logo + 50 * k;\n }\n const ww = $.measure(word, wf);\n // The wordmark lands letter by letter (word by word in scripts that join), then a light sweeps across it.\n s += M.text(\"wordmark\", Object.assign({ at: tWord, in: \"rise\", split: \"letters\", distance: wf.size * 0.6 / k, ease: \"spring(2.4, 7)\", stagger: Math.min(0.06, 0.5 / Math.max(1, word.length)) }, leave, wf, { x: cx, y: y, anchor: \"middle\" }), word);\n const sweep = $.seg(t, tWord + 0.5, tWord + 1.2);\n if (sweep > 0 && sweep < 1) {\n const sx = cx - ww / 2 - 200 * k + (ww + 400 * k) * $.E.inOut2(sweep);\n s += $.g({ extra: 'style=\"mix-blend-mode:screen\"' }, $.clip(cx - ww / 2 - 20 * k, y - 10 * k, ww + 40 * k, wf.size * 1.1, 0,\n $.rect(sx - 60 * k, y - 20 * k, 120 * k, wf.size * 1.3, { fill: $.gradient([[0, \"#FFFFFF\", 0], [0.5, \"#FFFFFF\", 0.55], [1, \"#FFFFFF\", 0]], { angle: 90 }), transform: \"translate(\" + $.fx(sx) + \" \" + $.fx(y + wf.size / 2) + \") skewX(-18) translate(\" + $.fx(-sx) + \" \" + $.fx(-(y + wf.size / 2)) + \")\" })));\n }\n y += wf.size + 40 * k;\n if (tag) {\n // The tagline spaces out as it fades in.\n const tp = M.progress(\"tagline\", { at: tWord + 0.45, in: \"fade\", dur: 0.65, ease: \"out\" });\n s += M.text(\"tagline\", Object.assign({ at: tWord + 0.45, in: \"fade\", dur: 0.65, ease: \"out\" }, leave, tfnt, { x: cx, y: y, anchor: \"middle\", fill: c.mute, ls: 0.14 + 0.18 * Math.min(1, tp.in) }), tag);\n }\n return s;\n}\n","libs":[]},{"id":"wipe","name":"Brand wipe","description":"A transition in the brand's colours: panels sweep in to cover the frame at the midpoint — cut your clips there — and sweep out to reveal the next shot.","category":"transition","tags":["transition","wipe","swipe","cut","bars","circle","reveal"],"placement":"overlay","duration":{"default":1.2,"min":0.6,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"shape":{"type":"choice","label":"Shape","options":["bars","diagonal","circle"],"default":"bars"},"bars":{"type":"number","label":"Bars","default":5,"min":2,"max":12,"step":1,"when":{"prop":"shape","is":["bars"]}},"label":{"type":"text","label":"Word at the cut","description":"shown while the frame is covered (empty for none)","default":"","maxLength":24},"direction":{"type":"choice","label":"Direction","options":["right","left","up","down"],"default":"right"}},"parts":{"panels":"the panels that cover the frame","label":"the word at the cut"},"pack":"essentials","ref":"essentials/wipe","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const mid = $.dur / 2;\n // 0 → 1 covering until the midpoint, 1 → 2 uncovering after it.\n const cover = $.E.inOut3($.seg(t, 0, mid));\n const uncover = $.E.inOut3($.seg(t, mid, $.dur));\n const colors = [c.accent, c.accent2, c.ink];\n const dir = p.direction;\n const horiz = dir === \"right\" || dir === \"left\";\n const sign = dir === \"right\" || dir === \"down\" ? 1 : -1;\n const L = horiz ? W : H;\n let s = \"\";\n if (p.shape === \"circle\") {\n const R = Math.hypot(W, H) * 0.56;\n const cx = W / 2, cy = H / 2;\n if (t < mid) s += $.circle(cx, cy, R * cover, { fill: c.accent }) + $.circle(cx, cy, R * Math.max(0, cover * 1.12 - 0.12), { fill: c.ink });\n else s += $.path(\"M0 0H\" + W + \"V\" + H + \"H0Z M\" + (cx + R * uncover) + \" \" + cy + \"A\" + R * uncover + \" \" + R * uncover + \" 0 1 0 \" + (cx - R * uncover) + \" \" + cy + \"A\" + R * uncover + \" \" + R * uncover + \" 0 1 0 \" + (cx + R * uncover) + \" \" + cy + \"Z\", { fill: c.ink, extra: 'fill-rule=\"evenodd\"' });\n } else if (p.shape === \"diagonal\") {\n const skew = 0.35 * (horiz ? H : W);\n const pos = t < mid ? -L - skew + (L + skew) * cover : (L + skew) * uncover;\n [[c.accent, -0.12], [c.accent2, -0.06], [c.ink, 0]].forEach(function (lay) {\n const a = pos + (t < mid ? lay[1] : -lay[1] * 0.5) * L;\n const d = horiz\n ? \"M\" + a + \" 0H\" + (a + L + skew) + \"L\" + (a + L) + \" \" + H + \"H\" + (a - skew) + \"Z\"\n : \"M0 \" + a + \"V\" + (a + L + skew) + \"L\" + W + \" \" + (a + L) + \"V\" + (a - skew) + \"Z\";\n s += $.path(d, { fill: lay[0], transform: sign < 0 ? (horiz ? \"translate(\" + W + \" 0) scale(-1 1)\" : \"translate(0 \" + H + \") scale(1 -1)\") : undefined });\n });\n } else {\n const n = Math.max(2, Math.round(p.bars));\n const band = (horiz ? H : W) / n;\n for (let i = 0; i < n; i++) {\n const lag = (i / (n - 1)) * 0.35;\n const ci = $.E.inOut3($.clamp(($.seg(t, 0, mid) - lag) / (1 - lag)));\n const ui = $.E.inOut3($.clamp(($.seg(t, mid, $.dur) - lag) / (1 - lag)));\n const len = L * (t < mid ? ci : 1 - ui);\n const start = t < mid ? (sign > 0 ? 0 : L - len) : sign > 0 ? L - len : 0;\n s += horiz ? $.rect(start, i * band - 1, len, band + 2, { fill: colors[i % colors.length] }) : $.rect(i * band - 1, start, band + 2, len, { fill: colors[i % colors.length] });\n }\n }\n // The panels are the transition; they move only when the creator says so.\n s = M.part(\"panels\", { in: \"none\", out: \"none\", vary: false }, s);\n if (p.label) {\n const lf = fitFont($, $.case(p.label), \"display\", 150 * k, W * 0.8, { ls: -0.03 });\n s += M.text(\"label\", Object.assign({ at: mid - mid * 0.5, in: \"pop\", outAt: mid + mid * 0.25, out: \"fade\", outDur: mid * 0.3 }, lf, { x: W / 2, y: H / 2 - lf.size * 0.55, anchor: \"middle\", fill: c.paper }), $.case(p.label));\n }\n return s;\n}\n\n/** Air as the wipe covers the frame, a soft drop at the cut (the midpoint), air going as it uncovers. */\nfunction sound($) {\n const mid = $.dur / 2;\n return {\n cues: [\n { at: Math.max(0, mid - 0.4), kind: \"whoosh\", dur: Math.min(0.45, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.5 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.45, $.dur - mid), level: 0.55, pitch: -3 },\n ],\n auto: true,\n };\n}\n","libs":[]}]},{"format":1,"id":"broadcast","name":"MONARC Broadcast","version":"1.0.0","description":"Lower thirds in five looks, a news ticker, a live badge, a score bug, a location pin and a countdown — broadcast graphics in any brand, any aspect.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/broadcast","style":"ember","items":["lt-bars","lt-reveal","lt-kicker","lt-pill","lt-news","ticker","live-badge","score-bug","location","countdown"],"entries":[{"id":"lt-bars","name":"Lower third — two bars","description":"A name bar wipes in from one side and a role bar in the accent from the other, the words rising inside their bars.","category":"lower-third","tags":["lower-third","overlay","bold","interview"],"placement":"overlay","duration":{"default":5,"min":2.5,"max":30},"props":{"name":{"type":"text","label":"Name","default":"Priya Sharma","maxLength":32},"role":{"type":"text","label":"Role","default":"Product designer","maxLength":40},"side":{"type":"choice","label":"Side","options":["left","right"],"default":"left"}},"parts":{"scrim":"the soft wash behind it","nameBar":"the bar behind the name","name":"the name","roleBar":"the accent bar behind the role","role":"the role"},"beats":{"in":0.15,"out":-0.45},"pack":"broadcast","ref":"broadcast/lt-bars","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.2 : 1);\n const tin = $.beat(\"in\", 0.15);\n const right = p.side === \"right\";\n const side = right ? \"right\" : \"left\", other = right ? \"left\" : \"right\";\n const name = $.case(p.name), role = String(p.role || \"\").toUpperCase();\n const tName = landOn($, p.name, tin);\n const tRole = landOn($, p.role, tName + 0.3, tName);\n const maxW = A.w * (A.tall ? 0.9 : 0.6);\n const nf = fitFont($, name, \"display\", 78 * k, maxW - 80 * k, { ls: -0.01 });\n const rf = fitFont($, role, \"mono\", 30 * k, maxW - 80 * k, { ls: 0.12 });\n const pad = 34 * k;\n const nw = $.measure(name, nf) + pad * 2, nh = nf.size * 1.34;\n const rw = role ? $.measure(role, rf) + pad * 1.6 : 0, rh = rf.size * 1.95;\n const y = A.tall ? H * 0.6 : A.square ? H * 0.66 : H * 0.7;\n const x = right ? W - A.x - nw : A.x;\n const inset = 36 * k;\n const rx = right ? W - A.x - inset - rw : A.x + inset;\n const leave = { out: \"wipe\", outFrom: side, outDur: 0.35 };\n let s = M.part(\"scrim\", { at: tin - 0.1, in: \"fade\", dur: 0.4, out: \"fade\", vary: false }, $.scrim(0, y - 240 * k, W, H - y + 240 * k, { to: \"bottom\", a: 0.4 }));\n s += M.part(\"nameBar\", Object.assign({ at: tin, in: \"wipe\", from: side, dur: 0.45, ease: \"expo\", box: { x: x, y: y, w: nw, h: nh } }, leave), $.rect(x, y, nw, nh, { fill: c.surface, shadow: \"md\" }));\n s += $.clip(x, y, nw, nh, 0, M.text(\"name\", Object.assign({ at: tName + 0.18, in: \"rise\", distance: 40 }, leave, nf, { x: right ? x + nw - pad : x + pad, y: y + (nh - nf.size) / 2 - nf.size * 0.02, anchor: right ? \"end\" : \"start\", fill: c.fg }), name));\n if (role) {\n s += M.part(\"roleBar\", Object.assign({ at: tRole, in: \"wipe\", from: other, dur: 0.4, ease: \"expo\", box: { x: rx, y: y + nh, w: rw, h: rh } }, leave), $.rect(rx, y + nh, rw, rh, { fill: c.accent }));\n s += $.clip(rx, y + nh, rw, rh, 0, M.text(\"role\", Object.assign({ at: tRole + 0.15, in: \"rise\", distance: 24 }, leave, rf, { x: rx + rw / 2, y: y + nh + (rh - rf.size) / 2 - rf.size * 0.05, anchor: \"middle\", fill: $.on(c.accent) }), role));\n }\n return s;\n}\n","libs":[]},{"id":"lt-reveal","name":"Lower third — mask reveal","description":"Cardless: an accent sweep crosses the frame and leaves the name behind it, then the role fades up. Made for footage.","category":"lower-third","tags":["lower-third","overlay","cardless","minimal"],"placement":"overlay","duration":{"default":5,"min":2.5,"max":30},"props":{"name":{"type":"text","label":"Name","default":"Maya Chen","maxLength":32},"role":{"type":"text","label":"Role","default":"Documentary editor","maxLength":44},"side":{"type":"choice","label":"Side","options":["left","right"],"default":"left"}},"parts":{"scrim":"the soft wash behind it","sweep":"the accent bar that crosses","name":"the name","role":"the role"},"beats":{"in":0.15,"out":-0.45},"pack":"broadcast","ref":"broadcast/lt-reveal","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.2 : 1);\n const tin = landOn($, p.name, $.beat(\"in\", 0.15));\n const right = p.side === \"right\";\n const name = $.case(p.name), role = String(p.role || \"\");\n const maxW = A.w * (A.tall ? 0.92 : 0.66);\n const nf = fitFont($, name, \"display\", 104 * k, maxW, { ls: -0.02 });\n const rf = fitFont($, role, \"text\", 36 * k, maxW);\n const nw = $.measure(name, nf);\n const y = A.tall ? H * 0.6 : A.square ? H * 0.64 : H * 0.68;\n const x = right ? W - A.x - nw : A.x;\n // The sweep runs across the name; the name shows behind its trailing edge.\n const sw = Math.max(26 * k, nf.size * 0.32), sh = nf.size * 1.16;\n const run = $.E.inOut3($.seg(t, tin, tin + 0.7));\n const lead = right ? x + nw + sw - (nw + sw * 2) * run : x - sw + (nw + sw * 2) * run;\n const shown = right ? { x: lead, w: x + nw + 20 * k - lead } : { x: x - 20 * k, w: lead - x + 20 * k };\n let s = M.part(\"scrim\", { at: tin - 0.1, in: \"fade\", dur: 0.45, out: \"fade\", vary: false }, $.scrim(0, y - 280 * k, W, H - y + 280 * k, { to: \"bottom\", a: 0.55 }));\n if (t >= tin && shown.w > 0) s += $.clip(shown.x, y - sh, Math.max(0, shown.w), sh * 2.2, 0, M.text(\"name\", { at: tin, in: \"none\", out: \"fade\", ...nf, x: x, y: y, fill: \"#FFFFFF\", shadow: \"md\" }, name));\n if (run > 0 && run < 1) s += M.part(\"sweep\", { at: tin, in: \"none\", out: \"none\", vary: false }, $.rect(right ? lead : lead - sw, y - sh * 0.08, sw, sh, { fill: c.accent }));\n if (role) s += M.text(\"role\", { at: tin + 0.55, in: \"rise\", distance: 18, ...rf, x: right ? x + nw : x, y: y + nf.size * 1.12, anchor: right ? \"end\" : \"start\", fill: \"rgba(255,255,255,0.82)\", shadow: \"sm\" }, role);\n return s;\n}\n","libs":[]},{"id":"lt-kicker","name":"Lower third — kicker","description":"A kicker tag in the accent, a heavy name and a baseline that draws under it, the role beneath. Cardless, for footage.","category":"lower-third","tags":["lower-third","overlay","podcast","bold"],"placement":"overlay","duration":{"default":5,"min":2.5,"max":30},"props":{"kicker":{"type":"text","label":"Kicker","default":"Guest","maxLength":18},"name":{"type":"text","label":"Name","default":"Jordan Okafor","maxLength":30},"role":{"type":"text","label":"Role","default":"Founder, Northwind Studio","maxLength":44},"side":{"type":"choice","label":"Side","options":["left","right"],"default":"left"}},"parts":{"scrim":"the soft wash behind it","kicker":"the tag above the name","name":"the name","rule":"the baseline","role":"the role"},"beats":{"in":0.15,"out":-0.45},"pack":"broadcast","ref":"broadcast/lt-kicker","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.2 : 1);\n const tin = $.beat(\"in\", 0.15);\n const right = p.side === \"right\";\n const side = right ? \"right\" : \"left\";\n const anchor = right ? \"end\" : \"start\";\n const name = $.case(p.name), role = String(p.role || \"\"), kick = String(p.kicker || \"\").toUpperCase();\n const tName = landOn($, p.name, tin + 0.15);\n const maxW = A.w * (A.tall ? 0.92 : 0.62);\n const nf = fitFont($, name, \"display\", 92 * k, maxW, { ls: -0.02 });\n const rf = fitFont($, role, \"text\", 34 * k, maxW);\n const nw = $.measure(name, nf);\n const kf = { ...$.font(\"mono\"), size: 26 * k, ls: 0.14 };\n const chip = kick ? { w: $.measure(kick, kf) + 30 * k, h: kf.size * 1.75 } : { w: 0, h: 0 };\n const total = chip.h + 18 * k + nf.size * 1.05 + 22 * k + rf.size * 1.3;\n const y = (A.tall ? H * 0.62 : A.square ? H * 0.68 : H * 0.72) - total / 2 + 40 * k;\n const x = right ? W - A.x : A.x;\n const leave = { out: \"slide\", outFrom: side, distance: 24 };\n let s = M.part(\"scrim\", { at: tin - 0.1, in: \"fade\", dur: 0.45, out: \"fade\", vary: false }, $.scrim(0, y - 220 * k, W, H - y + 220 * k, { to: \"bottom\", a: 0.55 }));\n let yy = y;\n if (kick) {\n const cx = right ? x - chip.w : x;\n s += M.part(\"kicker\", Object.assign({ at: tin, in: \"pop\", origin: side, box: { x: cx, y: yy, w: chip.w, h: chip.h } }, leave), $.rect(cx, yy, chip.w, chip.h, { r: chip.h * 0.18, fill: c.accent }) + $.text(kick, { ...kf, x: cx + chip.w / 2, y: yy + (chip.h - kf.size) / 2 - kf.size * 0.06, anchor: \"middle\", fill: $.on(c.accent) }));\n yy += chip.h + 18 * k;\n }\n s += M.text(\"name\", Object.assign({ at: tName, in: \"rise\", distance: 30, split: \"words\", stagger: 0.07 }, leave, nf, { x: x, y: yy, anchor: anchor, fill: \"#FFFFFF\", shadow: \"md\" }), name);\n yy += nf.size * 1.05;\n const rx = right ? x - nw : x;\n s += M.part(\"rule\", Object.assign({ at: tName + 0.3, in: \"grow-x\", origin: side, dur: 0.45, ease: \"expo\", box: { x: rx, y: yy, w: nw, h: 6 * k } }, leave), $.rect(rx, yy, nw, 6 * k, { r: 3 * k, fill: c.accent }));\n yy += 22 * k;\n if (role) s += M.text(\"role\", Object.assign({ at: tName + 0.5, in: \"fade\", dur: 0.4 }, leave, rf, { x: x, y: yy, anchor: anchor, fill: \"rgba(255,255,255,0.85)\", shadow: \"sm\" }), role);\n return s;\n}\n","libs":[]},{"id":"lt-pill","name":"Lower third — pill","description":"A rounded pill with a picture or initials, a status dot, the name and the role. Pops in; good for podcasts and calls.","category":"lower-third","tags":["lower-third","overlay","podcast","minimal","avatar"],"placement":"overlay","duration":{"default":5,"min":2.5,"max":30},"props":{"name":{"type":"text","label":"Name","default":"Alex Rivera","maxLength":28},"role":{"type":"text","label":"Role","default":"Host · The Long Cut","maxLength":40},"photo":{"type":"image","label":"Photo","description":"A square picture of the person; initials when empty"},"status":{"type":"choice","label":"Status dot","options":["live","online","none"],"default":"live"},"side":{"type":"choice","label":"Side","options":["left","right"],"default":"left"}},"parts":{"pill":"the pill","avatar":"the picture or initials","dot":"the status dot","name":"the name","role":"the role"},"beats":{"in":0.15,"out":-0.45},"pack":"broadcast","ref":"broadcast/lt-pill","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.3 : 1.15);\n const tin = landOn($, p.name, $.beat(\"in\", 0.15));\n const right = p.side === \"right\";\n const side = right ? \"right\" : \"left\";\n const name = $.case(p.name), role = String(p.role || \"\");\n const maxW = A.w * (A.tall ? 0.92 : 0.5);\n const h = 128 * k, r = h / 2, av = h * 0.38;\n const nf = fitFont($, name, \"display\", 48 * k, maxW - h - 60 * k);\n const rf = fitFont($, role, \"text\", 30 * k, maxW - h - 60 * k);\n const w = h + Math.max($.measure(name, nf), role ? $.measure(role, rf) : 0) + 56 * k;\n const y = A.tall ? H * 0.62 : A.square ? H * 0.7 : H * 0.74;\n const x = right ? W - A.x - w : A.x;\n const ax = x + r, ay = y + r;\n const dotCol = p.status === \"live\" ? c.red : p.status === \"online\" ? c.green : null;\n const leave = { out: \"pop\" };\n let s = M.part(\"pill\", Object.assign({ at: tin, in: \"pop\", origin: side, box: { x: x, y: y, w: w, h: h } }, leave), $.rect(x, y, w, h, { r: r, fill: c.surface, stroke: c.line, sw: 2 * k, shadow: \"lg\" }));\n s += M.part(\"avatar\", Object.assign({ at: tin + 0.12, in: \"pop\", box: { x: ax - av, y: ay - av, w: av * 2, h: av * 2 } }, leave), avatar($, \"photo\", ax, ay, av, { name: p.name, fill: c.accent }));\n if (dotCol) s += M.part(\"dot\", Object.assign({ at: tin + 0.35, in: \"pop\", idle: p.status === \"live\" ? \"pulse\" : \"none\", box: { x: ax + av * 0.5, y: ay + av * 0.5, w: av * 0.6, h: av * 0.6 } }, leave), $.circle(ax + av * 0.72, ay + av * 0.72, av * 0.24, { fill: dotCol, stroke: c.surface, sw: 4 * k }));\n const tx = x + h + 4 * k;\n const ty = role ? y + h / 2 - (nf.size * 1.08 + rf.size) / 2 : y + (h - nf.size) / 2;\n s += M.text(\"name\", Object.assign({ at: tin + 0.18, in: \"rise\", distance: 14 }, leave, nf, { x: tx, y: ty, fill: c.fg }), name);\n if (role) s += M.text(\"role\", Object.assign({ at: tin + 0.3, in: \"fade\", dur: 0.35 }, leave, rf, { x: tx, y: ty + nf.size * 1.08, fill: c.mute }), role);\n return s;\n}\n","libs":[]},{"id":"lt-news","name":"Lower third — news boxes","description":"Broadcast style: a tight headline box with a hard accent shadow, a sub-line box beneath and an optional tag above.","category":"lower-third","tags":["lower-third","overlay","news","broadcast"],"placement":"overlay","duration":{"default":6,"min":3,"max":30},"props":{"tag":{"type":"text","label":"Tag","default":"Exclusive","maxLength":16},"headline":{"type":"text","label":"Headline","default":"Creators switch to AI editing","maxLength":48},"sub":{"type":"text","label":"Sub-line","default":"Studios report three times the output","maxLength":56}},"parts":{"tag":"the tag above","headline":"the headline box and its words","sub":"the sub-line box and its words"},"beats":{"in":0.15,"out":-0.45},"pack":"broadcast","ref":"broadcast/lt-news","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.2 : 1);\n const tin = landOn($, p.headline, $.beat(\"in\", 0.15));\n const head = $.case(p.headline), sub = String(p.sub || \"\"), tag = String(p.tag || \"\").toUpperCase();\n const maxW = A.w * (A.tall ? 0.94 : 0.72);\n const hf = fitFont($, head, \"display\", 64 * k, maxW - 50 * k, { ls: -0.01 });\n const sf = fitFont($, sub, \"text\", 38 * k, maxW - 40 * k);\n const tf = { ...$.font(\"mono\"), size: 28 * k, ls: 0.16 };\n const pad = 24 * k, sh = 9 * k;\n const hw = $.measure(head, hf) + pad * 2, hh = hf.size * 1.36;\n const sw = sub ? $.measure(sub, sf) + pad * 1.6 : 0, shh = sf.size * 1.6;\n const tw = tag ? $.measure(tag, tf) + pad * 1.4 : 0, th = tf.size * 1.8;\n const blockH = (tag ? th : 0) + hh + (sub ? shh : 0);\n const y0 = (A.tall ? H * 0.64 : A.square ? H * 0.7 : H * 0.74) - blockH / 2;\n const x = A.x;\n let y = y0;\n let s = \"\";\n if (tag) {\n s += M.part(\"tag\", { at: tin, in: \"pop\", origin: \"bottom-left\", out: \"fade\", box: { x: x, y: y, w: tw, h: th } }, $.rect(x, y, tw, th, { fill: c.ink }) + $.text(tag, { ...tf, x: x + tw / 2, y: y + (th - tf.size) / 2 - tf.size * 0.05, anchor: \"middle\", fill: \"#FFFFFF\" }));\n y += th;\n }\n s += M.part(\"headline\", { at: tin + 0.1, in: \"wipe\", from: \"left\", dur: 0.45, ease: \"expo\", out: \"wipe\", outFrom: \"right\", box: { x: x, y: y, w: hw + sh, h: hh + sh } },\n $.rect(x + sh, y + sh, hw, hh, { fill: c.accent }), $.rect(x, y, hw, hh, { fill: \"#FFFFFF\" }), $.text(head, { ...hf, x: x + pad, y: y + (hh - hf.size) / 2 - hf.size * 0.03, fill: \"#111318\" }));\n if (sub) s += M.part(\"sub\", { at: tin + 0.4, in: \"wipe\", from: \"left\", dur: 0.4, ease: \"expo\", out: \"wipe\", outFrom: \"right\", box: { x: x, y: y + hh, w: sw, h: shh } },\n $.rect(x, y + hh, sw, shh, { fill: c.accent }), $.text(sub, { ...sf, x: x + pad * 0.8, y: y + hh + (shh - sf.size) / 2 - sf.size * 0.06, fill: $.on(c.accent) }));\n return s;\n}\n","libs":[]},{"id":"ticker","name":"News ticker","description":"A crawl along the bottom: a label block in the accent and a line of items scrolling left without a break.","category":"banner","tags":["ticker","crawl","news","overlay","broadcast"],"placement":"overlay","duration":{"default":10,"min":3,"max":120},"props":{"label":{"type":"text","label":"Label","default":"Latest","maxLength":14},"items":{"type":"list","label":"Items","default":["Volito ships 96 graphics","3D text renders without a GPU","Agents preview a graphic in 0.2 s","Every pack works in any brand"],"maxItems":12,"of":"text"},"speed":{"type":"number","label":"Speed (px/s at 1080p)","default":170,"min":40,"max":600},"position":{"type":"choice","label":"Position","options":["bottom","top"],"default":"bottom"}},"parts":{"bar":"the strip","label":"the label block","items":"the scrolling words"},"beats":{"in":0.1,"out":-0.4},"pack":"broadcast","ref":"broadcast/ticker","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.15 : 1);\n const tin = $.beat(\"in\", 0.1);\n const bh = 74 * k;\n const top = p.position === \"top\";\n const y = top ? A.y * 0.6 : A.tall ? A.y + A.h - bh : H - bh - H * 0.05;\n const label = String(p.label || \"\").toUpperCase();\n const lf = { ...$.font(\"display\"), size: 32 * k, ls: 0.06 };\n const lw = label ? $.measure(label, lf) + 56 * k : 0;\n const tf = { ...$.font(\"text\"), size: 32 * k };\n const items = (p.items || []).map(function (s) { return String(s); }).filter(Boolean);\n const loop = items.length ? items.join(\" • \") + \" • \" : \"\";\n const loopW = loop ? $.measure(loop, tf) : 1;\n const leave = { out: \"slide\", outFrom: top ? \"up\" : \"down\", distance: 40 };\n let s = M.part(\"bar\", Object.assign({ at: tin, in: \"slide\", from: top ? \"up\" : \"down\", distance: 40, box: { x: 0, y: y, w: W, h: bh } }, leave), $.rect(0, y, W, bh, { fill: c.ink }), $.rect(0, top ? y + bh - 4 * k : y, W, 4 * k, { fill: c.accent }));\n // The crawl: copies of the loop, moving left at `speed` from when the bar lands.\n const run = Math.max(0, t - tin - 0.3) * p.speed * k;\n const off = run % loopW;\n let row = \"\";\n const x0 = lw + 30 * k - off;\n for (let i = 0; x0 + i * loopW < W + loopW && i < 40; i++) row += $.text(loop, { ...tf, x: x0 + i * loopW, y: y + (bh - tf.size) / 2 - tf.size * 0.04, fill: \"#FFFFFF\", within: { x: -1e6, y: 0, w: 2e6, h: H } });\n s += $.clip(lw, y, W - lw, bh, 0, M.part(\"items\", Object.assign({ at: tin + 0.3, in: \"fade\", dur: 0.4 }, leave), row));\n if (label) s += M.part(\"label\", Object.assign({ at: tin + 0.1, in: \"wipe\", from: \"left\", dur: 0.4, ease: \"expo\", box: { x: 0, y: y, w: lw, h: bh } }, leave), $.rect(0, y, lw, bh, { fill: c.accent }), $.text(label, { ...lf, x: lw / 2, y: y + (bh - lf.size) / 2 - lf.size * 0.03, anchor: \"middle\", fill: $.on(c.accent) }));\n return s;\n}\n","libs":[]},{"id":"live-badge","name":"Live badge","description":"A LIVE pill with a pulsing dot, a viewer count that counts up and keeps ticking, and an optional running clock.","category":"social","tags":["live","badge","stream","overlay","hud"],"placement":"overlay","duration":{"default":6,"min":2,"max":600},"props":{"label":{"type":"text","label":"Label","default":"Live","maxLength":10},"viewers":{"type":"number","label":"Viewers","default":12480,"min":0,"max":99999999},"clock":{"type":"boolean","label":"Running clock","default":true},"elapsed":{"type":"number","label":"Clock starts at (s)","default":2534,"min":0,"max":359999},"corner":{"type":"choice","label":"Corner","options":["top-left","top-right"],"default":"top-left"}},"parts":{"live":"the LIVE pill","viewers":"the viewer count","clock":"the running clock"},"beats":{"in":0.1,"out":-0.35},"pack":"broadcast","ref":"broadcast/live-badge","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.3 : 1.2);\n const tin = $.beat(\"in\", 0.1);\n const right = p.corner === \"top-right\";\n const h = 64 * k;\n const lf = { ...$.font(\"display\"), size: 30 * k, ls: 0.08 };\n const nf = { ...$.font(\"mono\"), size: 28 * k, weight: 700 };\n const label = String(p.label || \"LIVE\").toUpperCase();\n const liveW = h * 0.62 + $.measure(label, lf) + 30 * k;\n // Viewers: up to the number in 1.2 s, then a slow, fixed drift so the count keeps moving.\n const up = $.E.out3($.seg(t, tin + 0.3, tin + 1.5));\n const drift = Math.floor(Math.max(0, t - tin - 1.5) * 1.7);\n let v = Math.round(p.viewers * up);\n for (let i = 0; i < drift; i++) v += Math.floor($.hash(i * 7.31) * 9) - 2;\n const vText = $.count(Math.max(0, v));\n const vw = h * 0.95 + Math.max($.measure(\"88.8K\", nf), $.measure(vText, nf)) + 24 * k;\n const secs = Math.floor(p.elapsed + Math.max(0, t - tin));\n const two = function (n) { return (n < 10 ? \"0\" : \"\") + n; };\n const cText = Math.floor(secs / 3600) + \":\" + two(Math.floor(secs / 60) % 60) + \":\" + two(secs % 60);\n const cw = p.clock ? $.measure(\"00:00:00\", nf) + 40 * k : 0;\n const gap = 12 * k;\n const total = liveW + gap + vw + (p.clock ? gap + cw : 0);\n let x = right ? W - A.x - total : A.x;\n const y = A.y;\n const leave = { out: \"fade\", outDur: 0.3 };\n let s = M.part(\"live\", Object.assign({ at: tin, in: \"pop\", origin: right ? \"right\" : \"left\", box: { x: x, y: y, w: liveW, h: h } }, leave),\n $.rect(x, y, liveW, h, { r: 10 * k, fill: c.red, shadow: \"md\" }),\n $.circle(x + h * 0.42, y + h / 2, 9 * k, { fill: \"#FFFFFF\", op: 0.45 + 0.55 * $.wave(t, 1.1) }),\n $.text(label, { ...lf, x: x + h * 0.66, y: y + (h - lf.size) / 2 - lf.size * 0.03, fill: \"#FFFFFF\" }));\n x += liveW + gap;\n s += M.part(\"viewers\", Object.assign({ at: tin + 0.15, in: \"slide\", from: right ? \"right\" : \"left\", distance: 16, box: { x: x, y: y, w: vw, h: h } }, leave),\n $.rect(x, y, vw, h, { r: 10 * k, fill: \"rgba(10,12,16,0.72)\" }),\n $.icon(\"users\", x + 18 * k, y + (h - 30 * k) / 2, 30 * k, { color: \"#FFFFFF\", sw: 2.2 }),\n $.text(vText, { ...nf, x: x + h * 0.85, y: y + (h - nf.size) / 2 - nf.size * 0.05, fill: \"#FFFFFF\" }));\n x += vw + gap;\n if (p.clock) s += M.part(\"clock\", Object.assign({ at: tin + 0.3, in: \"slide\", from: right ? \"right\" : \"left\", distance: 16, box: { x: x, y: y, w: cw, h: h } }, leave),\n $.rect(x, y, cw, h, { r: 10 * k, fill: \"rgba(10,12,16,0.72)\" }),\n $.text(cText, { ...nf, x: x + cw / 2, y: y + (h - nf.size) / 2 - nf.size * 0.05, anchor: \"middle\", fill: \"#FFFFFF\" }));\n return s;\n}\n","libs":[]},{"id":"score-bug","name":"Score bug","description":"A sports score bug: two teams with their colours and scores, the period and a running clock. A goal adds a point with a flash.","category":"banner","tags":["sports","score","scoreboard","overlay","broadcast"],"placement":"overlay","duration":{"default":8,"min":2,"max":600},"props":{"home":{"type":"text","label":"Home team","default":"MON","maxLength":4},"away":{"type":"text","label":"Away team","default":"NWD","maxLength":4},"homeScore":{"type":"number","label":"Home score","default":2,"min":0,"max":999},"awayScore":{"type":"number","label":"Away score","default":1,"min":0,"max":999},"homeColor":{"type":"color","label":"Home colour","default":"#FF6A3D"},"awayColor":{"type":"color","label":"Away colour","default":"#3DD6FF"},"period":{"type":"text","label":"Period","default":"2nd","maxLength":6},"clock":{"type":"text","label":"Clock (mm:ss)","default":"67:12","maxLength":6},"clockRuns":{"type":"choice","label":"Clock","options":["up","down","stopped"],"default":"up"},"goal":{"type":"choice","label":"Goal","options":["none","home","away"],"default":"home"},"goalAt":{"type":"number","label":"Goal at (s)","default":3.5,"min":0,"max":600}},"parts":{"bug":"the board","home":"the home row","away":"the away row","clock":"the period and clock"},"beats":{"in":0.1,"out":-0.35},"pack":"broadcast","ref":"broadcast/score-bug","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.3 : 1.15);\n const tin = $.beat(\"in\", 0.1);\n const rowH = 56 * k, sw = 12 * k;\n const nf = { ...$.font(\"display\"), size: 30 * k, ls: 0.04 };\n const sf = { ...$.font(\"mono\"), size: 32 * k, weight: 700 };\n const teamW = Math.max($.measure(\"WWW\", nf), $.measure(String(p.home).toUpperCase(), nf), $.measure(String(p.away).toUpperCase(), nf)) + 30 * k;\n const scoreW = $.measure(\"888\", sf) + 24 * k;\n const w = sw + teamW + scoreW;\n const clockW = $.measure(\"888:88\", sf) + 30 * k;\n const x = A.x, y = A.y;\n const scored = p.goal !== \"none\" && t >= p.goalAt;\n const hs = p.homeScore + (scored && p.goal === \"home\" ? 1 : 0), as = p.awayScore + (scored && p.goal === \"away\" ? 1 : 0);\n const flash = p.goal !== \"none\" ? $.bump(t, p.goalAt, 0.05, 0.6) : 0;\n // The clock: mm:ss running up or down from its start, or held.\n const m = /^(\\d+):(\\d\\d)$/.exec(String(p.clock || \"0:00\"));\n let secs = m ? Number(m[1]) * 60 + Number(m[2]) : 0;\n const ran = Math.floor(Math.max(0, t - tin));\n secs = p.clockRuns === \"up\" ? secs + ran : p.clockRuns === \"down\" ? Math.max(0, secs - ran) : secs;\n const clock = Math.floor(secs / 60) + \":\" + (secs % 60 < 10 ? \"0\" : \"\") + (secs % 60);\n const leave = { out: \"slide\", outFrom: \"left\", distance: 30 };\n let s = M.part(\"bug\", Object.assign({ at: tin, in: \"wipe\", from: \"left\", dur: 0.4, ease: \"expo\", box: { x: x, y: y, w: w + clockW, h: rowH * 2 } }, leave),\n $.rect(x, y, w, rowH * 2, { fill: c.ink, shadow: \"md\" }), $.rect(x + w, y, clockW, rowH * 2, { fill: c.surface }));\n const row = function (name, team, score, col, ry, hit) {\n return M.part(name, Object.assign({ at: tin + (name === \"home\" ? 0.2 : 0.3), in: \"slide\", from: \"left\", distance: 20, box: { x: x, y: ry, w: w, h: rowH } }, leave),\n $.rect(x, ry, sw, rowH, { fill: col }),\n $.text(String(team).toUpperCase(), { ...nf, x: x + sw + 16 * k, y: ry + (rowH - nf.size) / 2 - nf.size * 0.03, fill: \"#FFFFFF\" }),\n hit > 0.01 ? $.rect(x + sw + teamW, ry, scoreW, rowH, { fill: col, op: hit * 0.9 }) : \"\",\n $.g({ transform: $.tf({ s: 1 + 0.35 * hit, ox: x + sw + teamW + scoreW / 2, oy: ry + rowH / 2 }) }, $.text(String(score), { ...sf, x: x + sw + teamW + scoreW / 2, y: ry + (rowH - sf.size) / 2 - sf.size * 0.05, anchor: \"middle\", fill: \"#FFFFFF\" })));\n };\n s += row(\"home\", p.home, hs, p.homeColor, y, p.goal === \"home\" ? flash : 0);\n s += row(\"away\", p.away, as, p.awayColor, y + rowH, p.goal === \"away\" ? flash : 0);\n s += M.part(\"clock\", Object.assign({ at: tin + 0.4, in: \"fade\", dur: 0.3, box: { x: x + w, y: y, w: clockW, h: rowH * 2 } }, leave),\n $.text(String(p.period || \"\").toUpperCase(), { ...$.font(\"mono\"), size: 22 * k, ls: 0.12, x: x + w + clockW / 2, y: y + rowH * 0.42, anchor: \"middle\", fill: c.mute }),\n $.text(clock, { ...sf, x: x + w + clockW / 2, y: y + rowH * 1.02, anchor: \"middle\", fill: c.fg }));\n return s;\n}\n","libs":[]},{"id":"location","name":"Location pin","description":"A map pin drops and bounces with a ripple, and a card beside it names the place, its region and its coordinates.","category":"callout","tags":["location","map","pin","place","overlay"],"placement":"overlay","duration":{"default":5,"min":2.5,"max":30},"props":{"place":{"type":"text","label":"Place","default":"Lisbon","maxLength":26},"region":{"type":"text","label":"Region","default":"Portugal","maxLength":32},"coords":{"type":"text","label":"Coordinates","default":"38.72° N, 9.14° W","maxLength":28},"x":{"type":"number","label":"Pin across (0–1)","default":0.3,"min":0.05,"max":0.95},"y":{"type":"number","label":"Pin down (0–1)","default":0.62,"min":0.1,"max":0.9}},"parts":{"pin":"the pin","ripple":"the ring under it","card":"the place card"},"beats":{"in":0.1,"out":-0.4},"pack":"broadcast","ref":"broadcast/location","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.2 : 1);\n const tin = landOn($, p.place, $.beat(\"in\", 0.1));\n const px = W * p.x, py = Math.max(A.y + 140 * k, Math.min(A.y + A.h, H * p.y));\n const pw = 64 * k, ph = 88 * k;\n // A teardrop pin whose point sits on (px, py).\n const pin = \"M\" + $.fx(px) + \" \" + $.fx(py) + \" C\" + $.fx(px - pw * 0.2) + \" \" + $.fx(py - ph * 0.35) + \" \" + $.fx(px - pw / 2) + \" \" + $.fx(py - ph * 0.5) + \" \" + $.fx(px - pw / 2) + \" \" + $.fx(py - ph * 0.68) + \" A\" + $.fx(pw / 2) + \" \" + $.fx(pw / 2) + \" 0 1 1 \" + $.fx(px + pw / 2) + \" \" + $.fx(py - ph * 0.68) + \" C\" + $.fx(px + pw / 2) + \" \" + $.fx(py - ph * 0.5) + \" \" + $.fx(px + pw * 0.2) + \" \" + $.fx(py - ph * 0.35) + \" \" + $.fx(px) + \" \" + $.fx(py) + \"Z\";\n const ring = $.seg(t, tin + 0.45, tin + 1.6);\n let s = \"\";\n s += M.part(\"ripple\", { at: tin + 0.4, in: \"fade\", dur: 0.1, out: \"fade\", box: { x: px - 90 * k, y: py - 30 * k, w: 180 * k, h: 60 * k } },\n $.path(\"M\" + $.fx(px - 60 * k) + \" \" + $.fx(py) + \"a\" + $.fx(60 * k) + \" \" + $.fx(18 * k) + \" 0 1 0 \" + $.fx(120 * k) + \" 0a\" + $.fx(60 * k) + \" \" + $.fx(18 * k) + \" 0 1 0 \" + $.fx(-120 * k) + \" 0\", { fill: \"rgba(0,0,0,0.35)\" }),\n ring > 0 && ring < 1 ? $.path(\"M\" + $.fx(px - 60 * k * (0.4 + ring)) + \" \" + $.fx(py) + \"a\" + $.fx(60 * k * (0.4 + ring)) + \" \" + $.fx(18 * k * (0.4 + ring)) + \" 0 1 0 \" + $.fx(120 * k * (0.4 + ring)) + \" 0a\" + $.fx(60 * k * (0.4 + ring)) + \" \" + $.fx(18 * k * (0.4 + ring)) + \" 0 1 0 \" + $.fx(-120 * k * (0.4 + ring)) + \" 0\", { fill: \"none\", stroke: c.accent, sw: 4 * k * (1 - ring) }) : \"\");\n s += M.part(\"pin\", { at: tin, in: \"bounce\", origin: \"bottom\", out: \"rise\", box: { x: px - pw / 2, y: py - ph, w: pw, h: ph } },\n $.path(pin, { fill: c.accent, stroke: \"rgba(0,0,0,0.25)\", sw: 2 * k }), $.circle(px, py - ph * 0.68, pw * 0.2, { fill: \"#FFFFFF\" }));\n // The card sits on the side with more room.\n const left = p.x > 0.55;\n const nf = fitFont($, $.case(p.place), \"display\", 54 * k, A.w * 0.5);\n const rf = { ...$.font(\"text\"), size: 30 * k };\n const cf = { ...$.font(\"mono\"), size: 22 * k, ls: 0.06 };\n const cw = Math.max($.measure($.case(p.place), nf), $.measure(String(p.region || \"\"), rf), $.measure(String(p.coords || \"\"), cf)) + 64 * k;\n const ch = nf.size * 1.2 + (p.region ? rf.size * 1.35 : 0) + (p.coords ? cf.size * 1.6 : 0) + 44 * k;\n const cx = left ? Math.max(A.x, px - pw - 30 * k - cw) : Math.min(W - A.x - cw, px + pw + 30 * k);\n const cy = Math.max(A.y, py - ph - ch * 0.3);\n let yy = cy + 22 * k;\n let card = $.card(cx, cy, cw, ch, { shadow: \"lg\" });\n card += $.text($.case(p.place), { ...nf, x: cx + 32 * k, y: yy, fill: c.fg });\n yy += nf.size * 1.2;\n if (p.region) { card += $.text(String(p.region), { ...rf, x: cx + 32 * k, y: yy, fill: c.mute }); yy += rf.size * 1.35; }\n if (p.coords) card += $.text(String(p.coords), { ...cf, x: cx + 32 * k, y: yy + 4 * k, fill: c.accent });\n s += M.part(\"card\", { at: tin + 0.55, in: \"pop\", origin: left ? \"right\" : \"left\", out: \"fade\", box: { x: cx, y: cy, w: cw, h: ch } }, card);\n return s;\n}\n","libs":[]},{"id":"countdown","name":"Countdown","description":"A big number counting down second by second inside a ring that empties, then the closing words pop.","category":"timer","tags":["countdown","timer","start","overlay"],"placement":"overlay","duration":{"default":6,"min":2,"max":120},"props":{"label":{"type":"text","label":"Label","default":"Starting in","maxLength":24},"from":{"type":"number","label":"Count from","default":5,"min":1,"max":99},"end":{"type":"text","label":"Closing words","default":"Let's go","maxLength":16}},"parts":{"ring":"the ring","number":"the number","label":"the label","end":"the closing words"},"beats":{"in":0.1,"out":-0.35},"pack":"broadcast","ref":"broadcast/countdown","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.1);\n const n = Math.max(1, Math.round(p.from));\n // The count fills the time before the closing words (the last ~0.9 s).\n const span = Math.max(0.5, $.dur - tin - 1.2);\n const step = span / n;\n const el = t - tin;\n const left = Math.max(0, n - Math.floor(el / step));\n const done = el >= span;\n const cx = W / 2, cy = A.y + A.h * (A.tall ? 0.45 : 0.5);\n const r = Math.min(A.w, A.h) * 0.24;\n const tick = el >= 0 && !done ? (el % step) / step : 0;\n let s = M.part(\"ring\", { at: tin, in: \"grow\", dur: 0.5, out: \"fade\", box: { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r } },\n $.circle(cx, cy, r, { fill: \"rgba(10,12,16,0.55)\" }),\n $.ring(cx, cy, r * 0.92, done ? 0 : 1 - (el + 0) / span, { sw: r * 0.09, color: c.accent }));\n const label = $.case(String(p.label || \"\"));\n if (label) s += M.text(\"label\", { at: tin + 0.15, in: \"rise\", distance: 16, out: \"fade\", ...fitFont($, label, \"text\", 40 * k, A.w), x: cx, y: cy - r - 76 * k, anchor: \"middle\", fill: \"#FFFFFF\", shadow: \"md\" }, label);\n if (!done) {\n // Each number pops in on its second.\n const pop = 1 + 0.25 * Math.exp(-9 * tick * step) * Math.cos(tick * step * 14);\n const nf = { ...$.font(\"display\"), size: r * 0.95 };\n s += M.part(\"number\", { at: tin + 0.1, in: \"fade\", dur: 0.2, out: \"none\", box: { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r } },\n $.g({ transform: $.tf({ s: pop, ox: cx, oy: cy }) }, $.text(String(left), { ...nf, x: cx, y: cy - nf.size * 0.52, anchor: \"middle\", fill: \"#FFFFFF\" })));\n } else {\n const end = $.case(String(p.end || \"\"));\n const ef = fitFont($, end, \"display\", r * 0.46, r * 1.45);\n s += M.text(\"end\", { at: tin + span, in: \"slam\", intensity: 0.4, out: \"fade\", ...ef, x: cx, y: cy - ef.size * 0.55, anchor: \"middle\", fill: \"#FFFFFF\" }, end);\n }\n return s;\n}\n","libs":[]}]},{"format":1,"id":"social","name":"MONARC Social","version":"1.0.0","description":"Follow cards, notifications, comments, chat threads, posts, now playing and ratings — the social and app UI that videos borrow, in any brand, any aspect.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/social","style":"studio","shared":"shared.js","items":["follow-card","notification","notifications","comments","chat","post","now-playing","rating"],"entries":[{"id":"follow-card","name":"Follow card","description":"A profile card: picture or initials, name with a verified check, handle, a follower count that counts up, a line of bio and a Follow button a cursor presses.","category":"social","tags":["social","follow","profile","cta","overlay"],"placement":"overlay","duration":{"default":5,"min":3,"max":30},"props":{"name":{"type":"text","label":"Name","default":"MONARC Studio","maxLength":26},"handle":{"type":"text","label":"Handle","default":"@monarcstudio","maxLength":28},"followers":{"type":"number","label":"Followers","default":248000,"min":0,"max":999999999},"bio":{"type":"text","label":"Bio","default":"Edits that feel directed, not assembled.","maxLength":60},"platform":{"type":"choice","label":"Platform","options":["instagram","tiktok","youtube","x","linkedin","threads","website"],"default":"instagram"},"photo":{"type":"image","label":"Photo"},"verified":{"type":"boolean","label":"Verified","default":true},"action":{"type":"text","label":"Button","default":"Follow","maxLength":14},"done":{"type":"text","label":"Button after","default":"Following","maxLength":14},"position":{"type":"choice","label":"Position","options":["auto","center","bottom-left","bottom-right"],"default":"auto"}},"parts":{"card":"the card","avatar":"the picture","name":"the name and handle","stats":"the follower count","bio":"the bio","button":"the button","cursor":"the cursor"},"beats":{"in":0.15,"out":-0.45},"pack":"social","ref":"social/follow-card","code":"// MONARC Social — helpers this pack's items share.\n\n/** A platform's icon and how it names its numbers (the platforms' own marks are theirs; these are plain icons). */\nvar PLATFORM = {\n instagram: { icon: \"camera\", line: \"Instagram\", count: \"followers\" },\n tiktok: { icon: \"reel\", line: \"TikTok\", count: \"followers\" },\n youtube: { icon: \"play\", line: \"YouTube\", count: \"subscribers\" },\n x: { icon: \"x\", line: \"X\", count: \"followers\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\", count: \"followers\" },\n threads: { icon: \"chat\", line: \"Threads\", count: \"followers\" },\n website: { icon: \"globe\", line: \"Website\", count: \"readers\" },\n};\n\n/** A small round check in the accent: verified. */\nfunction verified($, cx, cy, r) {\n return $.circle(cx, cy, r, { fill: $.c.accent }) + $.path(\"M\" + $.fx(cx - r * 0.45) + \" \" + $.fx(cy + r * 0.02) + \"L\" + $.fx(cx - r * 0.1) + \" \" + $.fx(cy + r * 0.36) + \"L\" + $.fx(cx + r * 0.48) + \" \" + $.fx(cy - r * 0.32), { stroke: $.on($.c.accent), sw: r * 0.28 });\n}\n\n/** A colour for a name, from the brand's two accents and fixed hues — the same name, the same colour. */\nfunction nameColor($, name) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\"];\n var h = 0, s = String(name || \"\");\n for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;\n return list[h % list.length];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.wide ? 1.15 : A.tall ? 1.35 : 1.15);\n const tin = $.beat(\"in\", 0.15);\n const pl = PLATFORM[p.platform] || PLATFORM.instagram;\n const cw = Math.min(A.w, 620 * k), pad = 40 * k, av = 62 * k;\n const nf = fitFont($, p.name, \"display\", 42 * k, cw - pad * 2 - av * 2 - 70 * k);\n const hf = { ...$.font(\"mono\"), size: 24 * k };\n const bf = fitFont($, p.bio, \"text\", 28 * k, cw - pad * 2);\n const btn = { size: 30 * k };\n const bh = $.button.size($.case(p.action), btn).h;\n const ch = pad + av * 2 + 28 * k + 36 * k * 1.4 + (p.bio ? bf.size * 1.5 : 0) + 24 * k + bh + pad;\n const pos = p.position === \"auto\" ? (A.wide ? \"bottom-right\" : \"center\") : p.position;\n const x = pos === \"center\" ? (W - cw) / 2 : pos === \"bottom-left\" ? A.x : W - A.x - cw;\n const y = pos === \"center\" ? A.y + (A.h - ch) * (A.tall ? 0.62 : 0.5) : A.y + A.h - ch;\n const press = landOn($, p.action, Math.min(tin + 1.5, $.beat(\"out\", $.dur - 0.45) - 0.9), tin);\n const leave = { out: \"rise\", distance: 40 };\n let s = M.part(\"card\", Object.assign({ at: tin, in: \"rise\", distance: 70, box: { x: x, y: y, w: cw, h: ch } }, leave), $.card(x, y, cw, ch, { shadow: \"lg\" }));\n const ax = x + pad + av, ay = y + pad + av;\n s += M.part(\"avatar\", Object.assign({ at: tin + 0.1, in: \"pop\", box: { x: ax - av, y: ay - av, w: av * 2, h: av * 2 } }, leave), avatar($, \"photo\", ax, ay, av, { name: p.name, fill: c.accent, ring: c.surface }));\n // The platform's icon sits on the picture's edge.\n s += M.part(\"avatar\", Object.assign({ at: tin + 0.25, in: \"pop\", box: { x: ax + av * 0.4, y: ay + av * 0.4, w: av * 0.8, h: av * 0.8 } }, leave), $.circle(ax + av * 0.72, ay + av * 0.72, av * 0.34, { fill: c.surface }), $.icon(pl.icon, ax + av * 0.72 - av * 0.2, ay + av * 0.72 - av * 0.2, av * 0.4, { color: c.fg, sw: 2.2 }));\n const tx = ax + av + 26 * k;\n const nw = $.measure(p.name, nf);\n s += M.part(\"name\", Object.assign({ at: tin + 0.2, in: \"rise\", distance: 16, box: { x: tx, y: ay - av * 0.75, w: cw - (tx - x) - pad, h: av * 1.4 } }, leave),\n $.text(p.name, { ...nf, x: tx, y: ay - nf.size * 0.95, fill: c.fg }),\n p.verified ? verified($, tx + nw + nf.size * 0.5, ay - nf.size * 0.42, nf.size * 0.36) : \"\",\n $.text(String(p.handle || \"\"), { ...hf, x: tx, y: ay + 8 * k, fill: c.mute }));\n let yy = y + pad + av * 2 + 28 * k;\n // The count on its own row: a big number and what it counts.\n const count = $.counter(t, tin + 0.4, tin + 1.6, p.followers, { short: true });\n const cf = { ...$.font(\"display\"), size: 36 * k };\n s += M.part(\"stats\", Object.assign({ at: tin + 0.35, in: \"fade\", dur: 0.3, box: { x: x + pad, y: yy, w: cw - pad * 2, h: cf.size * 1.2 } }, leave),\n $.text(count, { ...cf, x: x + pad, y: yy, fill: c.fg }),\n $.text(pl.count, { ...$.font(\"text\"), size: 26 * k, x: x + pad + $.measure($.count(p.followers), cf) + 14 * k, y: yy + cf.size * 0.22, fill: c.mute }));\n yy += cf.size * 1.4;\n if (p.bio) {\n s += M.text(\"bio\", Object.assign({ at: tin + 0.45, in: \"fade\", dur: 0.35 }, leave, bf, { x: x + pad, y: yy, fill: c.fg }), String(p.bio));\n yy += bf.size * 1.5;\n }\n yy += 24 * k;\n s += M.part(\"button\", Object.assign({ at: tin + 0.55, in: \"rise\", distance: 14, box: { x: x + pad, y: yy, w: cw - pad * 2, h: bh } }, leave),\n $.button(t, $.case(p.action), x + cw / 2, yy, { size: 30 * k, anchor: \"middle\", pressAt: press, done: $.case(p.done) }));\n if (t > press - 0.8 && t < press + 0.9) {\n const g = $.E.inOut3($.seg(t, press - 0.75, press - 0.04));\n const px = $.lerp(x + cw + 160 * k, x + cw * 0.55, g), py = $.lerp(Math.min(H, y + ch + 200 * k), yy + bh * 0.6, g);\n s += M.part(\"cursor\", { at: press - 0.8, in: \"fade\", dur: 0.2, outAt: press + 0.6, out: \"fade\", outDur: 0.3 }, $.cursor(px, py, { press: $.bump(t, press, 0.05, 0.2), scale: 2 * k }));\n }\n return s;\n}\n","libs":[]},{"id":"notification","name":"Notification","description":"A phone or desktop notification slides in: the app's icon and name, the time, a bold title and the message.","category":"social","tags":["notification","phone","ui","overlay","alert"],"placement":"overlay","duration":{"default":4.5,"min":2,"max":30},"props":{"app":{"type":"text","label":"App","default":"MONARC","maxLength":18},"icon":{"type":"icon","label":"Icon","default":"sparkle"},"title":{"type":"text","label":"Title","default":"Your cut is ready","maxLength":40},"message":{"type":"text","label":"Message","default":"Three versions exported in 4K. Tap to review before you post.","maxLength":110},"time":{"type":"text","label":"Time","default":"now","maxLength":10},"look":{"type":"choice","label":"Look","options":["phone","desktop"],"default":"phone"}},"parts":{"banner":"the notification"},"beats":{"in":0.15,"out":-0.45},"pack":"social","ref":"social/notification","code":"// MONARC Social — helpers this pack's items share.\n\n/** A platform's icon and how it names its numbers (the platforms' own marks are theirs; these are plain icons). */\nvar PLATFORM = {\n instagram: { icon: \"camera\", line: \"Instagram\", count: \"followers\" },\n tiktok: { icon: \"reel\", line: \"TikTok\", count: \"followers\" },\n youtube: { icon: \"play\", line: \"YouTube\", count: \"subscribers\" },\n x: { icon: \"x\", line: \"X\", count: \"followers\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\", count: \"followers\" },\n threads: { icon: \"chat\", line: \"Threads\", count: \"followers\" },\n website: { icon: \"globe\", line: \"Website\", count: \"readers\" },\n};\n\n/** A small round check in the accent: verified. */\nfunction verified($, cx, cy, r) {\n return $.circle(cx, cy, r, { fill: $.c.accent }) + $.path(\"M\" + $.fx(cx - r * 0.45) + \" \" + $.fx(cy + r * 0.02) + \"L\" + $.fx(cx - r * 0.1) + \" \" + $.fx(cy + r * 0.36) + \"L\" + $.fx(cx + r * 0.48) + \" \" + $.fx(cy - r * 0.32), { stroke: $.on($.c.accent), sw: r * 0.28 });\n}\n\n/** A colour for a name, from the brand's two accents and fixed hues — the same name, the same colour. */\nfunction nameColor($, name) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\"];\n var h = 0, s = String(name || \"\");\n for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;\n return list[h % list.length];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.3 : 1.15);\n const tin = landOn($, p.title, $.beat(\"in\", 0.15));\n const desk = p.look === \"desktop\";\n const w = desk ? Math.min(A.w * 0.42, 720 * k) : Math.min(A.w, A.wide ? 760 * k : A.w);\n const pad = 26 * k, ic = 64 * k;\n const af = { ...$.font(\"text\"), size: 22 * k, ls: 0.06 };\n const tf = fitFont($, p.title, \"display\", 32 * k, w - pad * 2 - ic - 20 * k);\n const mf = { ...$.font(\"text\"), size: 27 * k, lh: 1.3 };\n const lines = $.wrap(String(p.message || \"\"), w - pad * 2 - ic - 20 * k, mf).slice(0, 3);\n const h = pad * 2 + af.size * 1.6 + tf.size * 1.25 + lines.length * mf.size * 1.3;\n const x = desk ? W - A.x - w : (W - w) / 2;\n const y = desk ? A.y : A.y * (A.tall ? 0.55 : 0.8);\n let b = $.rect(x, y, w, h, { r: (desk ? 18 : 34) * k, fill: c.surface, stroke: c.line, sw: 1.5 * k, shadow: \"lg\" });\n b += appIcon($, p.icon, x + pad, y + pad, ic, { fill: c.accent });\n const tx = x + pad + ic + 20 * k;\n b += $.text(String(p.app || \"\").toUpperCase(), { ...af, x: tx, y: y + pad - 2 * k, fill: c.mute });\n b += $.text(String(p.time || \"\"), { ...af, x: x + w - pad, y: y + pad - 2 * k, anchor: \"end\", fill: c.mute });\n let yy = y + pad + af.size * 1.6;\n b += $.text(String(p.title), { ...tf, x: tx, y: yy, fill: c.fg });\n yy += tf.size * 1.25;\n lines.forEach(function (l, i) { b += $.text(l, { ...mf, x: tx, y: yy + i * mf.size * 1.3, fill: c.fg, op: 0.82 }); });\n return M.part(\"banner\", { at: tin, in: desk ? \"slide\" : \"drop\", from: desk ? \"right\" : \"up\", distance: desk ? 60 : 90, ease: \"bouncy\", out: desk ? \"slide\" : \"drop\", outFrom: desk ? \"right\" : \"up\", box: { x: x, y: y, w: w, h: h } }, b);\n}\n","libs":[]},{"id":"notifications","name":"Notification stack","description":"Notifications land one after another and stack up, each new one pushing the pile down.","category":"social","tags":["notification","stack","cascade","ui","overlay"],"placement":"overlay","duration":{"default":6,"min":3,"max":30},"props":{"items":{"type":"list","label":"Notifications (App: message)","default":[{"app":"Sales","text":"New order — $1,240"},{"app":"Mail","text":"The client signed off on the cut"},{"app":"Calendar","text":"Launch call moved to 3 pm"},{"app":"Bank","text":"Payment received from Northwind"}],"maxItems":6,"minItems":1,"of":{"app":"text","text":"text"}},"gap":{"type":"number","label":"Seconds between","default":0.75,"min":0.2,"max":5},"icon":{"type":"icon","label":"Icon","default":"bell"}},"parts":{"items":"each notification"},"beats":{"in":0.2,"out":-0.45},"pack":"social","ref":"social/notifications","code":"// MONARC Social — helpers this pack's items share.\n\n/** A platform's icon and how it names its numbers (the platforms' own marks are theirs; these are plain icons). */\nvar PLATFORM = {\n instagram: { icon: \"camera\", line: \"Instagram\", count: \"followers\" },\n tiktok: { icon: \"reel\", line: \"TikTok\", count: \"followers\" },\n youtube: { icon: \"play\", line: \"YouTube\", count: \"subscribers\" },\n x: { icon: \"x\", line: \"X\", count: \"followers\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\", count: \"followers\" },\n threads: { icon: \"chat\", line: \"Threads\", count: \"followers\" },\n website: { icon: \"globe\", line: \"Website\", count: \"readers\" },\n};\n\n/** A small round check in the accent: verified. */\nfunction verified($, cx, cy, r) {\n return $.circle(cx, cy, r, { fill: $.c.accent }) + $.path(\"M\" + $.fx(cx - r * 0.45) + \" \" + $.fx(cy + r * 0.02) + \"L\" + $.fx(cx - r * 0.1) + \" \" + $.fx(cy + r * 0.36) + \"L\" + $.fx(cx + r * 0.48) + \" \" + $.fx(cy - r * 0.32), { stroke: $.on($.c.accent), sw: r * 0.28 });\n}\n\n/** A colour for a name, from the brand's two accents and fixed hues — the same name, the same colour. */\nfunction nameColor($, name) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\"];\n var h = 0, s = String(name || \"\");\n for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;\n return list[h % list.length];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.25 : 1.1);\n const tin = $.beat(\"in\", 0.2);\n const rows = p.items || [];\n const w = Math.min(A.w, A.wide ? 720 * k : A.w);\n const x = A.wide ? W - A.x - w : (W - w) / 2;\n const pad = 22 * k, ic = 52 * k, h = 112 * k, gap = 16 * k;\n const af = { ...$.font(\"text\"), size: 21 * k, ls: 0.06 };\n let s = \"\";\n rows.forEach(function (r, i) {\n const at = tin + i * p.gap;\n // Later arrivals push this one down by a row each, on a spring.\n let push = 0;\n for (let j = i + 1; j < rows.length; j++) push += $.spring(Math.max(0, t - (tin + j * p.gap)), 2.4, 8);\n const y = A.y + push * (h + gap);\n const tf = fitFont($, String(r.text), \"display\", 30 * k, w - pad * 2 - ic - 20 * k);\n let b = $.rect(x, y, w, h, { r: 26 * k, fill: c.surface, stroke: c.line, sw: 1.5 * k, shadow: \"md\" });\n b += appIcon($, p.icon, x + pad, y + (h - ic) / 2, ic, { fill: nameColor($, r.app) });\n b += $.text(String(r.app).toUpperCase(), { ...af, x: x + pad + ic + 18 * k, y: y + pad - 2 * k, fill: c.mute });\n b += $.text(\"now\", { ...af, x: x + w - pad, y: y + pad - 2 * k, anchor: \"end\", fill: c.mute });\n b += $.text(String(r.text), { ...tf, x: x + pad + ic + 18 * k, y: y + pad + af.size * 1.45, fill: c.fg });\n s = M.part(\"items\", { at: at, in: \"drop\", distance: 70, ease: \"bouncy\", out: \"fade\", box: { x: x, y: y, w: w, h: h } }, b) + s;\n });\n return s;\n}\n","libs":[]},{"id":"comments","name":"Comments","description":"Comment cards appear one by one: a picture or initials, the name, the time, the words typing on and a like count.","category":"social","tags":["comments","social","replies","typing","overlay"],"placement":"overlay","duration":{"default":7,"min":3,"max":30},"props":{"items":{"type":"list","label":"Comments (Name: words)","default":[{"name":"maya.edits","text":"This transition is unreal. How long did it take?"},{"name":"jordan_k","text":"Saving this for my next edit"},{"name":"alex.rivera","text":"The pacing in the first 3 seconds 🔥"}],"maxItems":4,"minItems":1,"of":{"name":"text","text":"text"}},"likes":{"type":"number","label":"Likes on the first","default":1240,"min":0,"max":9999999},"gap":{"type":"number","label":"Seconds between","default":1.6,"min":0.4,"max":6}},"parts":{"items":"each comment"},"beats":{"in":0.2,"out":-0.45},"pack":"social","ref":"social/comments","code":"// MONARC Social — helpers this pack's items share.\n\n/** A platform's icon and how it names its numbers (the platforms' own marks are theirs; these are plain icons). */\nvar PLATFORM = {\n instagram: { icon: \"camera\", line: \"Instagram\", count: \"followers\" },\n tiktok: { icon: \"reel\", line: \"TikTok\", count: \"followers\" },\n youtube: { icon: \"play\", line: \"YouTube\", count: \"subscribers\" },\n x: { icon: \"x\", line: \"X\", count: \"followers\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\", count: \"followers\" },\n threads: { icon: \"chat\", line: \"Threads\", count: \"followers\" },\n website: { icon: \"globe\", line: \"Website\", count: \"readers\" },\n};\n\n/** A small round check in the accent: verified. */\nfunction verified($, cx, cy, r) {\n return $.circle(cx, cy, r, { fill: $.c.accent }) + $.path(\"M\" + $.fx(cx - r * 0.45) + \" \" + $.fx(cy + r * 0.02) + \"L\" + $.fx(cx - r * 0.1) + \" \" + $.fx(cy + r * 0.36) + \"L\" + $.fx(cx + r * 0.48) + \" \" + $.fx(cy - r * 0.32), { stroke: $.on($.c.accent), sw: r * 0.28 });\n}\n\n/** A colour for a name, from the brand's two accents and fixed hues — the same name, the same colour. */\nfunction nameColor($, name) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\"];\n var h = 0, s = String(name || \"\");\n for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;\n return list[h % list.length];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.3 : 1.12);\n const tin = $.beat(\"in\", 0.2);\n const rows = p.items || [];\n const w = Math.min(A.w, A.wide ? 700 * k : A.w);\n const pad = 24 * k, av = 30 * k;\n const nf = { ...$.font(\"display\"), size: 26 * k };\n const tf = { ...$.font(\"text\"), size: 28 * k, lh: 1.3 };\n const meta = { ...$.font(\"text\"), size: 22 * k };\n // Heights first, so the stack can sit at the bottom of the safe area.\n const cards = rows.map(function (r, i) {\n const lines = $.wrap(String(r.text), w - pad * 2 - av * 2 - 20 * k, tf).slice(0, 3);\n return { r: r, lines: lines, h: pad * 2 + nf.size * 1.3 + lines.length * tf.size * 1.3 + meta.size * 1.4, at: tin + i * p.gap };\n });\n const gap = 16 * k;\n const total = cards.reduce(function (a, c2) { return a + c2.h; }, 0) + gap * Math.max(0, cards.length - 1);\n const x = A.wide ? A.x : (W - w) / 2;\n let y = A.wide ? A.y + A.h - total : A.y + A.h * 0.55 - total / 2;\n let s = \"\";\n cards.forEach(function (cd, i) {\n const r = cd.r;\n let b = $.rect(x, y, w, cd.h, { r: 24 * k, fill: c.surface, stroke: c.line, sw: 1.5 * k, shadow: \"md\" });\n b += avatar($, null, x + pad + av, y + pad + av, av, { name: r.name, fill: nameColor($, r.name) });\n const tx = x + pad + av * 2 + 18 * k;\n b += $.text(String(r.name), { ...nf, x: tx, y: y + pad, fill: c.fg });\n b += $.text((i + 1) * 2 + \"m\", { ...meta, x: tx + $.measure(String(r.name), nf) + 14 * k, y: y + pad + 3 * k, fill: c.mute });\n // The words type on, line by line.\n let left = Math.max(0, Math.floor((t - cd.at - 0.25) * 42));\n cd.lines.forEach(function (l, j) {\n const n = Math.min(Array.from(l).length, left);\n left -= n;\n if (n > 0) b += $.text(Array.from(l).slice(0, n).join(\"\"), { ...tf, x: tx, y: y + pad + nf.size * 1.35 + j * tf.size * 1.3, fill: c.fg });\n });\n const ly = y + cd.h - pad - meta.size * 0.9;\n const likes = i === 0 ? $.count(Math.round(p.likes * $.E.out3($.seg(t, cd.at + 0.6, cd.at + 1.6)))) : String(Math.round(p.likes / (6 + i * 5)));\n b += $.icon(\"heart\", tx, ly - 2 * k, meta.size * 1.05, { color: i === 0 && t > cd.at + 0.6 ? c.red : c.mute, fill: i === 0 && t > cd.at + 0.6 });\n b += $.text(likes, { ...meta, x: tx + meta.size * 1.5, y: ly, fill: c.mute });\n b += $.text(\"Reply\", { ...meta, x: tx + meta.size * 5.5, y: ly, fill: c.mute });\n s += M.part(\"items\", { at: cd.at, in: \"rise\", distance: 40, ease: \"bouncy\", out: \"fade\", box: { x: x, y: y, w: w, h: cd.h } }, b);\n y += cd.h + gap;\n });\n return s;\n}\n","libs":[]},{"id":"chat","name":"Chat thread","description":"A message thread plays out: their bubbles arrive after a typing indicator, yours on the right in the accent.","category":"social","tags":["chat","messages","dm","texting","ui"],"placement":"overlay","duration":{"default":8,"min":3,"max":60},"props":{"contact":{"type":"text","label":"Contact","default":"Sam · Producer","maxLength":28},"messages":{"type":"list","label":"Messages (me: … / them: …)","default":[{"who":"them","text":"Did the new cut land?"},{"who":"me","text":"Rendering now — 3 versions"},{"who":"them","text":"Send the vertical one first"},{"who":"me","text":"On its way 🚀"}],"maxItems":8,"minItems":1,"of":{"who":"text","text":"text"}},"gap":{"type":"number","label":"Seconds between","default":1.4,"min":0.4,"max":6}},"parts":{"header":"the contact bar","messages":"each bubble","typing":"the typing dots"},"beats":{"in":0.2,"out":-0.45},"pack":"social","ref":"social/chat","code":"// MONARC Social — helpers this pack's items share.\n\n/** A platform's icon and how it names its numbers (the platforms' own marks are theirs; these are plain icons). */\nvar PLATFORM = {\n instagram: { icon: \"camera\", line: \"Instagram\", count: \"followers\" },\n tiktok: { icon: \"reel\", line: \"TikTok\", count: \"followers\" },\n youtube: { icon: \"play\", line: \"YouTube\", count: \"subscribers\" },\n x: { icon: \"x\", line: \"X\", count: \"followers\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\", count: \"followers\" },\n threads: { icon: \"chat\", line: \"Threads\", count: \"followers\" },\n website: { icon: \"globe\", line: \"Website\", count: \"readers\" },\n};\n\n/** A small round check in the accent: verified. */\nfunction verified($, cx, cy, r) {\n return $.circle(cx, cy, r, { fill: $.c.accent }) + $.path(\"M\" + $.fx(cx - r * 0.45) + \" \" + $.fx(cy + r * 0.02) + \"L\" + $.fx(cx - r * 0.1) + \" \" + $.fx(cy + r * 0.36) + \"L\" + $.fx(cx + r * 0.48) + \" \" + $.fx(cy - r * 0.32), { stroke: $.on($.c.accent), sw: r * 0.28 });\n}\n\n/** A colour for a name, from the brand's two accents and fixed hues — the same name, the same colour. */\nfunction nameColor($, name) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\"];\n var h = 0, s = String(name || \"\");\n for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;\n return list[h % list.length];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.3 : 1.1);\n const tin = $.beat(\"in\", 0.2);\n const msgs = p.messages || [];\n const w = Math.min(A.w, A.wide ? 760 * k : A.w);\n const x = (W - w) / 2;\n const tf = { ...$.font(\"text\"), size: 30 * k, lh: 1.28 };\n const bp = 22 * k, maxB = w * 0.74, gap = 14 * k;\n // Each bubble's lines and time: theirs wait 0.7 s behind typing dots.\n let at = tin + 0.5;\n const bubbles = msgs.map(function (m) {\n const mine = /^me$/i.test(String(m.who).trim());\n const lines = $.wrap(String(m.text), maxB - bp * 2, tf).slice(0, 4);\n const bw = Math.max.apply(null, lines.map(function (l) { return $.measure(l, tf); })) + bp * 2;\n const bh = lines.length * tf.size * 1.28 + bp * 1.4;\n const b = { mine: mine, lines: lines, w: bw, h: bh, typing: mine ? null : at, at: mine ? at : at + 0.7 };\n at = b.at + p.gap;\n return b;\n });\n const hh = 96 * k;\n // The thread scrolls up when it outgrows the space under the header.\n const room = A.h - hh - 20 * k;\n let used = 0;\n const shown = bubbles.filter(function (b) { return t >= b.at; });\n shown.forEach(function (b) { used += b.h + gap; });\n const typingNow = bubbles.some(function (b) { return b.typing !== null && t >= b.typing && t < b.at; });\n if (typingNow) used += 70 * k + gap;\n const scroll = Math.max(0, used - room);\n const top = A.y + hh + 20 * k;\n let s = M.part(\"header\", { at: tin, in: \"drop\", distance: 30, out: \"fade\", vary: false, box: { x: x, y: A.y, w: w, h: hh } },\n $.rect(x, A.y, w, hh, { r: 24 * k, fill: c.surface, stroke: c.line, sw: 1.5 * k, shadow: \"md\" }),\n avatar($, null, x + 30 * k + 28 * k, A.y + hh / 2, 28 * k, { name: p.contact, fill: c.accent }),\n $.text(String(p.contact), { ...$.font(\"display\"), size: 30 * k, x: x + 30 * k + 72 * k, y: A.y + hh / 2 - 17 * k, fill: c.fg }));\n let y = top - scroll;\n let body = \"\";\n bubbles.forEach(function (b, i) {\n if (b.typing !== null && t >= b.typing && t < b.at) {\n const dx = x, dw = 120 * k, dh = 66 * k;\n let dots = $.rect(dx, y, dw, dh, { r: dh / 2, fill: c.surface, stroke: c.line, sw: 1.5 * k });\n for (let d = 0; d < 3; d++) dots += $.circle(dx + dw * (0.3 + d * 0.2), y + dh / 2 - 6 * k * Math.max(0, Math.sin((t - b.typing) * 9 - d * 0.9)), 8 * k, { fill: c.mute });\n body += M.part(\"typing\", { at: b.typing, in: \"pop\", origin: \"left\", out: \"none\", box: { x: dx, y: y, w: dw, h: dh } }, dots);\n }\n if (t < b.at) return;\n const bx = b.mine ? x + w - b.w : x;\n let bub = $.rect(bx, y, b.w, b.h, { r: 28 * k, fill: b.mine ? c.accent : c.surface, stroke: b.mine ? \"none\" : c.line, sw: 1.5 * k });\n b.lines.forEach(function (l, j) { bub += $.text(l, { ...tf, x: bx + bp, y: y + bp * 0.7 + j * tf.size * 1.28, fill: b.mine ? $.on(c.accent) : c.fg }); });\n body += M.part(\"messages\", { at: b.at, in: \"pop\", origin: b.mine ? \"bottom-right\" : \"bottom-left\", ease: \"bouncy\", out: \"fade\", box: { x: bx, y: y, w: b.w, h: b.h } }, bub);\n y += b.h + gap;\n });\n return s + $.clip(x - 20 * k, top, w + 40 * k, A.y + A.h - top, 0, body);\n}\n","libs":[]},{"id":"post","name":"Social post","description":"A post card: picture or initials, name, handle and check, the words, and likes, reposts and replies counting up as a heart pops.","category":"social","tags":["post","tweet","social","quote","card"],"placement":"overlay","duration":{"default":6,"min":3,"max":30},"props":{"name":{"type":"text","label":"Name","default":"Maya Chen","maxLength":26},"handle":{"type":"text","label":"Handle","default":"@mayaedits","maxLength":26},"text":{"type":"longtext","label":"Post","default":"Stopped editing timelines by hand this year. I describe the cut, the agent builds it, I direct. Shipping three times as much.","maxLength":280},"photo":{"type":"image","label":"Photo"},"verified":{"type":"boolean","label":"Verified","default":true},"likes":{"type":"number","label":"Likes","default":18400,"min":0,"max":999999999},"reposts":{"type":"number","label":"Reposts","default":2310,"min":0,"max":999999999},"replies":{"type":"number","label":"Replies","default":412,"min":0,"max":999999999},"time":{"type":"text","label":"Time","default":"2h","maxLength":12}},"parts":{"card":"the card","author":"the name line","text":"the words","stats":"the counts","heart":"the heart"},"beats":{"in":0.15,"out":-0.45},"pack":"social","ref":"social/post","code":"// MONARC Social — helpers this pack's items share.\n\n/** A platform's icon and how it names its numbers (the platforms' own marks are theirs; these are plain icons). */\nvar PLATFORM = {\n instagram: { icon: \"camera\", line: \"Instagram\", count: \"followers\" },\n tiktok: { icon: \"reel\", line: \"TikTok\", count: \"followers\" },\n youtube: { icon: \"play\", line: \"YouTube\", count: \"subscribers\" },\n x: { icon: \"x\", line: \"X\", count: \"followers\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\", count: \"followers\" },\n threads: { icon: \"chat\", line: \"Threads\", count: \"followers\" },\n website: { icon: \"globe\", line: \"Website\", count: \"readers\" },\n};\n\n/** A small round check in the accent: verified. */\nfunction verified($, cx, cy, r) {\n return $.circle(cx, cy, r, { fill: $.c.accent }) + $.path(\"M\" + $.fx(cx - r * 0.45) + \" \" + $.fx(cy + r * 0.02) + \"L\" + $.fx(cx - r * 0.1) + \" \" + $.fx(cy + r * 0.36) + \"L\" + $.fx(cx + r * 0.48) + \" \" + $.fx(cy - r * 0.32), { stroke: $.on($.c.accent), sw: r * 0.28 });\n}\n\n/** A colour for a name, from the brand's two accents and fixed hues — the same name, the same colour. */\nfunction nameColor($, name) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\"];\n var h = 0, s = String(name || \"\");\n for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;\n return list[h % list.length];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.3 : 1.1);\n const tin = $.beat(\"in\", 0.15);\n const cw = Math.min(A.w, A.wide ? 900 * k : A.w), pad = 40 * k, av = 40 * k;\n const nf = fitFont($, p.name, \"display\", 32 * k, cw * 0.5);\n const mf = { ...$.font(\"text\"), size: 26 * k };\n const tf = { ...$.font(\"text\"), size: 36 * k, lh: 1.32 };\n const lines = $.wrap(String(p.text || \"\"), cw - pad * 2, tf).slice(0, 7);\n const ch = pad + av * 2 + 24 * k + lines.length * tf.size * 1.32 + 30 * k + 40 * k + pad;\n const x = (W - cw) / 2, y = A.y + (A.h - ch) * (A.tall ? 0.48 : 0.5);\n const leave = { out: \"rise\", distance: 40 };\n let s = M.part(\"card\", Object.assign({ at: tin, in: \"rise\", distance: 60, box: { x: x, y: y, w: cw, h: ch } }, leave), $.card(x, y, cw, ch, { shadow: \"lg\" }));\n const ay = y + pad + av;\n const tx = x + pad + av * 2 + 20 * k;\n const nw = $.measure(String(p.name), nf);\n s += M.part(\"author\", Object.assign({ at: tin + 0.12, in: \"fade\", dur: 0.3, box: { x: x + pad, y: y + pad, w: cw - pad * 2, h: av * 2 } }, leave),\n avatar($, \"photo\", x + pad + av, ay, av, { name: p.name, fill: c.accent }),\n $.text(String(p.name), { ...nf, x: tx, y: ay - nf.size * 1.0, fill: c.fg }),\n p.verified ? verified($, tx + nw + nf.size * 0.55, ay - nf.size * 0.45, nf.size * 0.38) : \"\",\n $.text(String(p.handle || \"\") + (p.time ? \" · \" + p.time : \"\"), { ...mf, x: tx, y: ay + 6 * k, fill: c.mute }));\n s += M.text(\"text\", Object.assign({ at: tin + 0.3, in: \"rise\", distance: 16, split: \"lines\", stagger: 0.08 }, leave, tf, { x: x + pad, y: y + pad + av * 2 + 24 * k, fill: c.fg, maxW: cw - pad * 2 }), String(p.text || \"\"));\n // Counts climb after the words land; the heart pops red as it starts.\n const sy = y + ch - pad - 30 * k, ts = tin + 0.6 + lines.length * 0.08;\n const cnt = function (n) { return $.count(Math.round(n * $.E.out3($.seg(t, ts, ts + 1.6)))); };\n const col = (cw - pad * 2) / 3;\n const statsRow = [[\"chat\", cnt(p.replies)], [\"loop\", cnt(p.reposts)], [\"heart\", cnt(p.likes)]];\n let row = \"\";\n statsRow.forEach(function (st, i) {\n const sx = x + pad + i * col;\n if (st[0] !== \"heart\") row += $.icon(st[0], sx, sy - 2 * k, 32 * k, { color: c.mute });\n row += $.text(st[1], { ...mf, x: sx + 46 * k, y: sy, fill: st[0] === \"heart\" && t > ts ? c.red : c.mute });\n });\n s += M.part(\"stats\", Object.assign({ at: ts - 0.2, in: \"fade\", dur: 0.3, box: { x: x + pad, y: sy - 10 * k, w: cw - pad * 2, h: 50 * k } }, leave), row);\n const hx = x + pad + 2 * col;\n const liked = t > ts;\n s += M.part(\"heart\", Object.assign({ at: ts - 0.2, in: \"fade\", dur: 0.2, emphasis: [{ fx: \"pop\", at: ts }], box: { x: hx, y: sy - 4 * k, w: 34 * k, h: 34 * k } }, leave), $.icon(\"heart\", hx, sy - 2 * k, 32 * k, { color: liked ? c.red : c.mute, fill: liked }));\n return s;\n}\n","libs":[]},{"id":"now-playing","name":"Now playing","description":"A music card: the cover (or a generated one), the title and artist, a progress bar moving with the clip and an equaliser.","category":"social","tags":["music","now playing","song","audio","card"],"placement":"overlay","duration":{"default":6,"min":2,"max":60},"props":{"title":{"type":"text","label":"Song","default":"Midnight Edit","maxLength":32},"artist":{"type":"text","label":"Artist","default":"The Long Cut","maxLength":32},"cover":{"type":"image","label":"Cover"},"length":{"type":"text","label":"Length (m:ss)","default":"3:42","maxLength":6},"start":{"type":"number","label":"Starts at (0–1)","default":0.34,"min":0,"max":1},"position":{"type":"choice","label":"Position","options":["auto","center","bottom-left","bottom-right"],"default":"auto"}},"parts":{"card":"the card","cover":"the cover","title":"the song and artist","progress":"the progress bar","eq":"the equaliser"},"beats":{"in":0.15,"out":-0.45},"pack":"social","ref":"social/now-playing","code":"// MONARC Social — helpers this pack's items share.\n\n/** A platform's icon and how it names its numbers (the platforms' own marks are theirs; these are plain icons). */\nvar PLATFORM = {\n instagram: { icon: \"camera\", line: \"Instagram\", count: \"followers\" },\n tiktok: { icon: \"reel\", line: \"TikTok\", count: \"followers\" },\n youtube: { icon: \"play\", line: \"YouTube\", count: \"subscribers\" },\n x: { icon: \"x\", line: \"X\", count: \"followers\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\", count: \"followers\" },\n threads: { icon: \"chat\", line: \"Threads\", count: \"followers\" },\n website: { icon: \"globe\", line: \"Website\", count: \"readers\" },\n};\n\n/** A small round check in the accent: verified. */\nfunction verified($, cx, cy, r) {\n return $.circle(cx, cy, r, { fill: $.c.accent }) + $.path(\"M\" + $.fx(cx - r * 0.45) + \" \" + $.fx(cy + r * 0.02) + \"L\" + $.fx(cx - r * 0.1) + \" \" + $.fx(cy + r * 0.36) + \"L\" + $.fx(cx + r * 0.48) + \" \" + $.fx(cy - r * 0.32), { stroke: $.on($.c.accent), sw: r * 0.28 });\n}\n\n/** A colour for a name, from the brand's two accents and fixed hues — the same name, the same colour. */\nfunction nameColor($, name) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\"];\n var h = 0, s = String(name || \"\");\n for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;\n return list[h % list.length];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.3 : 1.1);\n const tin = $.beat(\"in\", 0.15);\n const cw = Math.min(A.w, 680 * k), pad = 28 * k, art = 150 * k;\n const ch = art + pad * 2 + 70 * k;\n const pos = p.position === \"auto\" ? (A.wide ? \"bottom-left\" : \"center\") : p.position;\n const x = pos === \"center\" ? (W - cw) / 2 : pos === \"bottom-left\" ? A.x : W - A.x - cw;\n const y = pos === \"center\" ? A.y + (A.h - ch) * 0.6 : A.y + A.h - ch;\n const leave = { out: \"slide\", outFrom: pos === \"bottom-right\" ? \"right\" : \"left\", distance: 40 };\n let s = M.part(\"card\", Object.assign({ at: tin, in: \"slide\", from: pos === \"bottom-right\" ? \"right\" : \"left\", distance: 60, box: { x: x, y: y, w: cw, h: ch } }, leave), $.card(x, y, cw, ch, { shadow: \"lg\" }));\n // The cover: the picture, or a gradient in the brand's accents with a disc on it.\n let cover;\n if ($.media.cover) cover = $.image(\"cover\", x + pad, y + pad, art, art, { r: 18 * k });\n else {\n const gid = $.uid(\"np\");\n $.def('<linearGradient id=\"' + gid + '\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"' + c.accent + '\"/><stop offset=\"1\" stop-color=\"' + c.accent2 + '\"/></linearGradient>');\n cover = $.rect(x + pad, y + pad, art, art, { r: 18 * k, fill: \"url(#\" + gid + \")\" }) + $.circle(x + pad + art / 2, y + pad + art / 2, art * 0.3, { fill: \"rgba(0,0,0,0.25)\" }) + $.circle(x + pad + art / 2, y + pad + art / 2, art * 0.06, { fill: \"#FFFFFF\", op: 0.8 });\n }\n s += M.part(\"cover\", Object.assign({ at: tin + 0.1, in: \"pop\", box: { x: x + pad, y: y + pad, w: art, h: art } }, leave), cover);\n const tx = x + pad + art + 26 * k;\n const tw = cw - (tx - x) - pad;\n const nf = fitFont($, p.title, \"display\", 38 * k, tw - 80 * k);\n const af = fitFont($, p.artist, \"text\", 28 * k, tw);\n s += M.part(\"title\", Object.assign({ at: tin + 0.2, in: \"rise\", distance: 14, box: { x: tx, y: y + pad, w: tw, h: art * 0.7 } }, leave),\n $.text(String(p.title), { ...nf, x: tx, y: y + pad + 10 * k, fill: c.fg }),\n $.text(String(p.artist), { ...af, x: tx, y: y + pad + 10 * k + nf.size * 1.25, fill: c.mute }));\n // An equaliser beside the title: bars bouncing at fixed, different rates.\n let eq = \"\";\n for (let i = 0; i < 4; i++) {\n const hgt = (0.3 + 0.7 * Math.abs(Math.sin(t * (5 + i * 1.7) + i * 1.3))) * 34 * k;\n eq += $.rect(x + cw - pad - (4 - i) * 12 * k, y + pad + 44 * k - hgt, 7 * k, hgt, { r: 3 * k, fill: c.accent });\n }\n s += M.part(\"eq\", Object.assign({ at: tin + 0.3, in: \"fade\", dur: 0.3, box: { x: x + cw - pad - 50 * k, y: y + pad, w: 50 * k, h: 48 * k } }, leave), eq);\n // Progress: from `start` of the song, moving in real time.\n const m = /^(\\d+):(\\d\\d)$/.exec(String(p.length || \"3:00\"));\n const len = m ? Number(m[1]) * 60 + Number(m[2]) : 180;\n const now = Math.min(len, p.start * len + Math.max(0, t - tin));\n const fmt = function (v) { v = Math.floor(v); return Math.floor(v / 60) + \":\" + (v % 60 < 10 ? \"0\" : \"\") + (v % 60); };\n const py = y + pad + art + 30 * k, pw = cw - pad * 2;\n const mf = { ...$.font(\"mono\"), size: 20 * k };\n s += M.part(\"progress\", Object.assign({ at: tin + 0.35, in: \"grow-x\", origin: \"left\", dur: 0.4, box: { x: x + pad, y: py - 6 * k, w: pw, h: 40 * k } }, leave),\n $.rect(x + pad, py, pw, 8 * k, { r: 4 * k, fill: $.rgba(c.fg, 0.15) }),\n $.rect(x + pad, py, pw * now / len, 8 * k, { r: 4 * k, fill: c.accent }),\n $.circle(x + pad + pw * now / len, py + 4 * k, 10 * k, { fill: c.fg }),\n $.text(fmt(now), { ...mf, x: x + pad, y: py + 18 * k, fill: c.mute }),\n $.text(\"-\" + fmt(len - now), { ...mf, x: x + pad + pw, y: py + 18 * k, anchor: \"end\", fill: c.mute }));\n return s;\n}\n","libs":[]},{"id":"rating","name":"Rating","description":"Stars fill one by one to the score, the score counts up, then a review and its author fade in.","category":"social","tags":["rating","review","stars","testimonial","proof"],"placement":"full","duration":{"default":6,"min":3,"max":30},"props":{"score":{"type":"number","label":"Score (out of 5)","default":4.8,"min":0,"max":5,"decimals":1},"count":{"type":"text","label":"Count line","default":"from 2,480 reviews","maxLength":40},"quote":{"type":"longtext","label":"Review","default":"The fastest way we've found to get from raw footage to something we're proud to post.","maxLength":200},"author":{"type":"text","label":"Author","default":"Priya S., creative director","maxLength":40}},"parts":{"ground":"the ground","stars":"the stars","score":"the score and count","quote":"the review","author":"who wrote it"},"beats":{"in":0.15,"out":-0.45},"pack":"social","ref":"social/rating","code":"// MONARC Social — helpers this pack's items share.\n\n/** A platform's icon and how it names its numbers (the platforms' own marks are theirs; these are plain icons). */\nvar PLATFORM = {\n instagram: { icon: \"camera\", line: \"Instagram\", count: \"followers\" },\n tiktok: { icon: \"reel\", line: \"TikTok\", count: \"followers\" },\n youtube: { icon: \"play\", line: \"YouTube\", count: \"subscribers\" },\n x: { icon: \"x\", line: \"X\", count: \"followers\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\", count: \"followers\" },\n threads: { icon: \"chat\", line: \"Threads\", count: \"followers\" },\n website: { icon: \"globe\", line: \"Website\", count: \"readers\" },\n};\n\n/** A small round check in the accent: verified. */\nfunction verified($, cx, cy, r) {\n return $.circle(cx, cy, r, { fill: $.c.accent }) + $.path(\"M\" + $.fx(cx - r * 0.45) + \" \" + $.fx(cy + r * 0.02) + \"L\" + $.fx(cx - r * 0.1) + \" \" + $.fx(cy + r * 0.36) + \"L\" + $.fx(cx + r * 0.48) + \" \" + $.fx(cy - r * 0.32), { stroke: $.on($.c.accent), sw: r * 0.28 });\n}\n\n/** A colour for a name, from the brand's two accents and fixed hues — the same name, the same colour. */\nfunction nameColor($, name) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\"];\n var h = 0, s = String(name || \"\");\n for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;\n return list[h % list.length];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n const star = function (cx, cy, r, fill) {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + i * Math.PI / 5, rr = i % 2 ? r * 0.45 : r;\n d += (i ? \"L\" : \"M\") + $.fx(cx + rr * Math.cos(a)) + \" \" + $.fx(cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n };\n const sr = Math.min(A.w / 13, 64 * k);\n const sg = sr * 2.4;\n const sx0 = W / 2 - sg * 2;\n const total = sr * 2 + 40 * k + 150 * k + 60 * k + 200 * k;\n let y = A.y + (A.h - total) / 2 + sr;\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n // Each star fills in turn up to the score; the last one partly.\n let stars = \"\";\n for (let i = 0; i < 5; i++) {\n const cx = sx0 + i * sg;\n const at = tin + 0.2 + i * 0.16;\n const f = Math.max(0, Math.min(1, p.score - i)) * $.E.out3($.seg(t, at, at + 0.3));\n const pop = 1 + 0.25 * $.bump(t, at + 0.2, 0.06, 0.25);\n const id = $.uid(\"rs\");\n $.def('<clipPath id=\"' + id + '\"><rect x=\"' + $.fx(cx - sr) + '\" y=\"' + $.fx(y - sr) + '\" width=\"' + $.fx(2 * sr * f) + '\" height=\"' + $.fx(2 * sr) + '\"/></clipPath>');\n stars += $.g({ transform: $.tf({ s: pop, ox: cx, oy: y }) }, $.path(star(cx, y, sr), { fill: $.rgba(c.fg, 0.12) }), '<g clip-path=\"url(#' + id + ')\">' + $.path(star(cx, y, sr), { fill: c.accent2 }) + \"</g>\");\n }\n s += M.part(\"stars\", { at: tin, in: \"fade\", dur: 0.2, box: { x: sx0 - sr, y: y - sr, w: sg * 4 + 2 * sr, h: 2 * sr } }, stars);\n y += sr + 40 * k;\n const nf = { ...$.font(\"display\"), size: 120 * k };\n const score = $.counter(t, tin + 0.2, tin + 1.1, p.score, { dec: 1 });\n s += M.part(\"score\", { at: tin + 0.2, in: \"rise\", distance: 20, box: { x: A.x, y: y, w: A.w, h: nf.size * 1.6 } },\n $.text(score, { ...nf, x: W / 2, y: y, anchor: \"middle\", fill: c.fg }),\n $.text(String(p.count || \"\"), { ...$.font(\"text\"), size: 30 * k, x: W / 2, y: y + nf.size * 1.05, anchor: \"middle\", fill: c.mute }));\n y += nf.size * 1.05 + 30 * k + 60 * k;\n const qf = fitLines($, \"“\" + String(p.quote || \"\") + \"”\", { ...$.font(\"accent\"), ls: 0 }, A.w * (A.wide ? 0.7 : 0.92), 64 * k, 30 * k, 4);\n s += M.text(\"quote\", { at: tin + 1.2, in: \"rise\", distance: 16, split: \"lines\", stagger: 0.1, ...qf.font, x: W / 2, y: y, anchor: \"middle\", fill: c.fg, lh: 1.25, maxW: A.w * (A.wide ? 0.7 : 0.92) }, qf.lines.join(\"\\n\"));\n y += qf.lines.length * qf.size * 1.25 + 30 * k;\n s += M.text(\"author\", { at: tin + 1.6, in: \"fade\", dur: 0.4, ...$.font(\"mono\"), size: 24 * k, ls: 0.1, x: W / 2, y: y, anchor: \"middle\", fill: c.mute }, String(p.author || \"\").toUpperCase());\n return s;\n}\n","libs":[]}]},{"format":1,"id":"data","name":"MONARC Data","version":"1.0.0","description":"Line charts, donuts, bar races, goal meters, versus splits, timelines, funnels, world maps and flight routes — data that moves, in any brand, any aspect.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/data","style":"cream","shared":"shared.js","items":["line-chart","donut","bar-race","goals","versus","timeline","funnel","world-map","flight"],"entries":[{"id":"line-chart","name":"Line chart","description":"A line draws on through its points over a soft area, each point popping as the line reaches it, the last value called out, against an optional target.","category":"chart","tags":["chart","line","trend","growth","data"],"placement":"full","duration":{"default":6,"min":3,"max":30},"props":{"title":{"type":"text","label":"Title","default":"Weekly views since the switch","maxLength":56},"rows":{"type":"list","label":"Rows (Label: value)","default":[{"label":"W1","value":12},{"label":"W2","value":18},{"label":"W3","value":16},{"label":"W4","value":27},{"label":"W5","value":34},{"label":"W6","value":31},{"label":"W7","value":46},{"label":"W8","value":58}],"maxItems":12,"minItems":2,"of":{"label":"text","value":"number"}},"prefix":{"type":"text","label":"Before numbers","default":"","maxLength":3},"suffix":{"type":"text","label":"After numbers","default":"K","maxLength":6},"decimals":{"type":"number","label":"Decimals","default":0,"min":0,"max":3},"target":{"type":"number","label":"Target line (0 = none)","default":40,"min":0,"max":1000000000000},"targetLabel":{"type":"text","label":"Target label","default":"Goal","maxLength":16}},"parts":{"ground":"the ground","title":"the heading","grid":"the gridlines and axis labels","area":"the area under the line","line":"the line","dots":"the points","label":"the last value's callout","target":"the target line"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/line-chart","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const head = chartHead($, M, t, tin, p.title, A, k);\n s += head.s;\n const rows = p.rows || [];\n const n = rows.length;\n const lf = { ...$.font(\"text\"), size: Math.min(28 * k, (A.w / n) * 0.3) };\n const vf = { ...$.font(\"mono\"), size: 22 * k };\n const maxV = Math.max.apply(null, rows.map(function (r) { return r.value; }).concat([p.target || 0, 1e-9]));\n const step = niceStep(maxV * 1.1, 4);\n const top = step * Math.ceil((maxV * 1.08) / step);\n const axisW = $.measure(fmtNum(top, 0, p.prefix, p.suffix), vf) + 18 * k;\n const box = { x: A.x + axisW, y: head.y + 30 * k, w: A.w - axisW - 70 * k, h: A.y + A.h - head.y - 30 * k - lf.size * 2.2 };\n const X = function (i) { return box.x + (n > 1 ? (i / (n - 1)) * box.w : box.w / 2); };\n const Y = function (v) { return box.y + box.h - (v / top) * box.h; };\n let grid = \"\";\n for (let v = 0; v <= top + 1e-9; v += step) {\n grid += $.line(box.x, Y(v), box.x + box.w, Y(v), { stroke: $.rgba(c.fg, v === 0 ? 0.3 : 0.1), sw: 2 * k });\n grid += $.text(fmtNum(v, 0, p.prefix, p.suffix), { ...vf, x: box.x - 16 * k, y: Y(v) - vf.size * 0.55, anchor: \"end\", fill: c.mute });\n }\n rows.forEach(function (r, i) { if (n <= 8 || i % Math.ceil(n / 8) === 0 || i === n - 1) grid += $.text(String(r.label), { ...lf, x: X(i), y: box.y + box.h + lf.size * 0.6, anchor: \"middle\", fill: c.mute }); });\n s += M.part(\"grid\", { at: tin + 0.1, in: \"fade\", dur: 0.4, out: \"fade\", vary: false }, grid);\n // The line draws on from the left; each point pops when the line reaches it.\n const t0 = tin + 0.5, t1 = t0 + 1.8;\n const run = $.E.inOut2($.seg(t, t0, t1));\n const pts = rows.map(function (r, i) { return [X(i), Y(r.value)]; });\n const d = pts.map(function (q, i) { return (i ? \"L\" : \"M\") + $.fx(q[0]) + \" \" + $.fx(q[1]); }).join(\"\");\n const reach = box.x + box.w * run;\n if (p.target > 0) {\n const ty = Y(p.target);\n s += M.part(\"target\", { at: tin + 0.3, in: \"grow-x\", origin: \"left\", dur: 0.6, out: \"fade\", box: { x: box.x, y: ty - 4 * k, w: box.w, h: 8 * k } },\n $.line(box.x, ty, box.x + box.w, ty, { stroke: c.accent2, sw: 3 * k, dash: 12 * k + \" \" + 10 * k }),\n $.text(String(p.targetLabel || \"\") + \" \" + fmtNum(p.target, p.decimals, p.prefix, p.suffix), { ...vf, x: box.x + box.w + 12 * k, y: ty - vf.size * 0.55, fill: c.accent2 }));\n }\n const gid = $.uid(\"lg\");\n $.def('<linearGradient id=\"' + gid + '\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"' + c.accent + '\" stop-opacity=\"0.32\"/><stop offset=\"1\" stop-color=\"' + c.accent + '\" stop-opacity=\"0\"/></linearGradient>');\n s += $.clip(box.x - 10 * k, box.y - 40 * k, reach - box.x + 10 * k, box.h + 50 * k, 0,\n M.part(\"area\", { at: t0, in: \"fade\", dur: 0.4, out: \"fade\", vary: false }, $.path(d + \"L\" + $.fx(pts[n - 1][0]) + \" \" + $.fx(box.y + box.h) + \"L\" + $.fx(pts[0][0]) + \" \" + $.fx(box.y + box.h) + \"Z\", { fill: \"url(#\" + gid + \")\" })) +\n M.part(\"line\", { at: t0, in: \"none\", out: \"fade\" }, $.path(d, { stroke: c.accent, sw: 6 * k, fill: \"none\" })));\n let dots = \"\";\n pts.forEach(function (q, i) {\n if (q[0] > reach + 1) return;\n const at = t0 + (q[0] - box.x) / box.w * (t1 - t0);\n const pop = $.pop(t, at);\n dots += $.circle(q[0], q[1], 9 * k * Math.max(0, pop), { fill: c.bg, stroke: c.accent, sw: 4 * k });\n });\n s += M.part(\"dots\", { at: t0, in: \"none\", out: \"fade\" }, dots);\n const last = rows[n - 1], lp = pts[n - 1];\n const callout = fmtNum(last.value * $.E.out3($.seg(t, t1 - 0.2, t1 + 0.4)), p.decimals, p.prefix, p.suffix);\n const cfnt = { ...$.font(\"display\"), size: 40 * k };\n const cw2 = $.measure(fmtNum(last.value, p.decimals, p.prefix, p.suffix), cfnt) + 36 * k, chh = cfnt.size * 1.5;\n const cx = Math.min(lp[0] - cw2 / 2, A.x + A.w - cw2), cy = lp[1] - chh - 22 * k;\n s += M.part(\"label\", { at: t1 - 0.1, in: \"pop\", origin: \"bottom\", out: \"fade\", box: { x: cx, y: cy, w: cw2, h: chh } },\n $.rect(cx, cy, cw2, chh, { r: chh / 2, fill: c.accent }), $.text(callout, { ...cfnt, x: cx + cw2 / 2, y: cy + (chh - cfnt.size) / 2 - cfnt.size * 0.04, anchor: \"middle\", fill: $.on(c.accent) }));\n return s;\n}\n","libs":[]},{"id":"donut","name":"Donut chart","description":"Segments sweep round a ring one after another, the centre counts up to the total (or the first share), and a legend lists each part.","category":"chart","tags":["chart","donut","pie","share","data"],"placement":"full","duration":{"default":6,"min":3,"max":30},"props":{"title":{"type":"text","label":"Title","default":"Where the hours went","maxLength":50},"rows":{"type":"list","label":"Rows (Label: value)","default":[{"label":"Editing","value":46},{"label":"Rendering","value":12},{"label":"Review","value":18},{"label":"Publishing","value":9}],"maxItems":12,"minItems":2,"of":{"label":"text","value":"number"}},"center":{"type":"choice","label":"Centre shows","options":["total","first share"],"default":"first share"},"centerLabel":{"type":"text","label":"Centre label","default":"on editing","maxLength":20},"prefix":{"type":"text","label":"Before numbers","default":"","maxLength":3},"suffix":{"type":"text","label":"After numbers","default":" h","maxLength":6},"decimals":{"type":"number","label":"Decimals","default":0,"min":0,"max":3}},"parts":{"ground":"the ground","title":"the heading","ring":"the segments","center":"the number in the middle","legend":"the legend"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/donut","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const head = chartHead($, M, t, tin, p.title, A, k);\n s += head.s;\n const rows = p.rows || [];\n const total = rows.reduce(function (a, r) { return a + Math.max(0, r.value); }, 0) || 1;\n const room = { x: A.x, y: head.y, w: A.w, h: A.y + A.h - head.y };\n const side = A.wide;\n const R = side ? Math.min(room.h * 0.46, room.w * 0.24) : Math.min(room.w * 0.36, room.h * 0.26);\n const cx = side ? room.x + R + 20 * k : W / 2, cy = side ? room.y + room.h / 2 : room.y + R + 10 * k;\n const r0 = R * 0.62;\n // Segments sweep in turn, together taking 1.4 s.\n const t0 = tin + 0.4;\n let a = 0, ring = \"\";\n rows.forEach(function (r, i) {\n const share = Math.max(0, r.value) / total;\n const st = t0 + (a / (Math.PI * 2)) * 1.4;\n const grow = $.E.out3($.seg(t, st, st + share * 1.4 + 0.15));\n if (grow > 0) ring += $.path(arcPath(cx, cy, r0, R, a + 0.012, a + Math.max(0.0001, share * Math.PI * 2 * grow) - 0.012), { fill: seriesColor($, i) });\n a += share * Math.PI * 2;\n });\n s += M.part(\"ring\", { at: t0 - 0.1, in: \"fade\", dur: 0.2, out: \"fade\", box: { x: cx - R, y: cy - R, w: 2 * R, h: 2 * R } }, $.circle(cx, cy, (R + r0) / 2, { stroke: $.rgba(c.fg, 0.07), sw: R - r0, fill: \"none\" }), ring);\n const first = rows[0] || { value: 0 };\n const big = p.center === \"total\" ? fmtNum(total * $.E.out3($.seg(t, t0, t0 + 1.4)), p.decimals, p.prefix, p.suffix) : Math.round((first.value / total) * 100 * $.E.out3($.seg(t, t0, t0 + 1.4))) + \"%\";\n const bf = fitFont($, p.center === \"total\" ? fmtNum(total, p.decimals, p.prefix, p.suffix) : \"100%\", \"display\", r0 * 0.62, r0 * 1.5);\n s += M.part(\"center\", { at: t0 + 0.2, in: \"pop\", out: \"fade\", box: { x: cx - r0, y: cy - r0, w: 2 * r0, h: 2 * r0 } },\n $.text(big, { ...bf, x: cx, y: cy - bf.size * 0.62, anchor: \"middle\", fill: c.fg }),\n $.text(String(p.centerLabel || \"\"), { ...$.font(\"text\"), size: Math.min(28 * k, r0 * 0.2), x: cx, y: cy + bf.size * 0.45, anchor: \"middle\", fill: c.mute }));\n // The legend beside the ring on wide frames, under it elsewhere.\n const lx = side ? cx + R + 80 * k : A.x, ly0 = side ? cy - (rows.length * 70 * k) / 2 : cy + R + 50 * k;\n const lw = side ? Math.min(A.x + A.w - lx, 680 * k) : A.w;\n const rowH = Math.min(70 * k, (A.y + A.h - ly0) / Math.max(1, rows.length));\n const lf = { ...$.font(\"text\"), size: Math.min(34 * k, rowH * 0.5) };\n rows.forEach(function (r, i) {\n const y = ly0 + i * rowH;\n s += M.part(\"legend\", { at: t0 + 0.3 + i * 0.12, in: \"slide\", from: \"left\", distance: 16, out: \"fade\", box: { x: lx, y: y, w: lw, h: rowH } },\n $.rect(lx, y + rowH * 0.25, rowH * 0.5, rowH * 0.5, { r: rowH * 0.12, fill: seriesColor($, i) }),\n $.text(String(r.label), { ...lf, x: lx + rowH * 0.8, y: y + (rowH - lf.size) / 2 - lf.size * 0.05, fill: c.fg }),\n $.text(fmtNum(r.value, p.decimals, p.prefix, p.suffix) + \" \" + Math.round((r.value / total) * 100) + \"%\", { ...$.font(\"mono\"), size: lf.size * 0.82, x: lx + lw, y: y + (rowH - lf.size * 0.82) / 2 - lf.size * 0.05, anchor: \"end\", fill: c.mute }));\n });\n return s;\n}\n","libs":[]},{"id":"bar-race","name":"Bar chart race","description":"Ranked bars grow and overtake each other as the periods advance; the axis rescales and the period counts up in the corner.","category":"chart","tags":["chart","bar race","ranking","time series","data"],"placement":"full","duration":{"default":8,"min":4,"max":60},"props":{"title":{"type":"text","label":"Title","default":"Most-used editing tools","maxLength":50},"periods":{"type":"list","label":"Periods","default":["2022","2023","2024","2025","2026"],"maxItems":12,"minItems":2,"of":"text"},"rows":{"type":"list","label":"Rows (Name: v1, v2, …)","default":[{"label":"MONARC","values":"4, 14, 38, 72, 118"},{"label":"Studio A","values":"60, 66, 70, 71, 72"},{"label":"Cutter","values":"40, 48, 52, 50, 46"},{"label":"Frame.io","values":"22, 30, 41, 47, 51"},{"label":"Clipmate","values":"35, 30, 24, 20, 18"},{"label":"Reelsy","values":"8, 20, 33, 44, 49"}],"maxItems":12,"minItems":2,"of":{"label":"text","values":"text"}},"show":{"type":"number","label":"Bars shown","default":6,"min":3,"max":10},"prefix":{"type":"text","label":"Before numbers","default":"","maxLength":3},"suffix":{"type":"text","label":"After numbers","default":"K","maxLength":6},"decimals":{"type":"number","label":"Decimals","default":0,"min":0,"max":3}},"parts":{"ground":"the ground","title":"the heading","bars":"the bars","period":"the period in the corner"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/bar-race","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const head = chartHead($, M, t, tin, p.title, A, k);\n s += head.s;\n const periods = p.periods || [];\n const np = periods.length;\n const rows = (p.rows || []).map(function (r, i) { return { label: String(r.label), i: i, v: String(r.values).split(/[,\\s]+/).filter(Boolean).map(Number).concat(new Array(np).fill(0)).slice(0, np) }; });\n // The race runs from 0.6 s to 0.8 s before the end; each period gets an equal share.\n const t0 = tin + 0.6, t1 = Math.max(t0 + 1, $.beat(\"out\", $.dur - 0.45) - 0.4);\n const pos = Math.max(0, Math.min(np - 1, ((t - t0) / (t1 - t0)) * (np - 1)));\n const i0 = Math.min(np - 2, Math.floor(pos)), f = pos - i0, fe = $.E.inOut2(f);\n const valAt = function (r) { return r.v[i0] + (r.v[i0 + 1] - r.v[i0]) * f; };\n const rankAt = function (j) { const sorted = rows.slice().sort(function (a, b) { return b.v[j] - a.v[j] || a.i - b.i; }); const m = {}; sorted.forEach(function (r, ix) { m[r.i] = ix; }); return m; };\n const ra = rankAt(i0), rb = rankAt(i0 + 1);\n const show = Math.min(rows.length, Math.round(p.show));\n const room = { y: head.y, h: A.y + A.h - head.y - 20 * k };\n // On wide frames the names sit left of the bars; elsewhere each bar gets a name row above it.\n const slot = room.h / show, bh = slot * (A.wide ? 0.72 : 0.44);\n const maxNow = Math.max.apply(null, rows.map(valAt).concat([1e-9]));\n const lf = { ...$.font(\"display\"), size: Math.min(34 * k, A.wide ? bh * 0.5 : slot * 0.3) };\n const vf = { ...$.font(\"mono\"), size: Math.min(28 * k, bh * 0.42), weight: 700 };\n const labelW = A.wide ? Math.min(A.w * 0.22, Math.max.apply(null, rows.map(function (r) { return $.measure(r.label, lf); })) + 24 * k) : 0;\n const bx = A.x + labelW, bw = A.w - labelW - $.measure(fmtNum(maxNow, p.decimals, p.prefix, p.suffix), vf) - 30 * k;\n let bars = \"\";\n rows.forEach(function (r) {\n const rank = ra[r.i] + (rb[r.i] - ra[r.i]) * fe;\n if (rank > show - 0.05) return;\n const y = A.wide ? room.y + rank * slot + (slot - bh) / 2 : room.y + rank * slot + slot * 0.42;\n const v = valAt(r), w = Math.max(bh * 0.3, (v / maxNow) * bw);\n const col = seriesColor($, r.i);\n bars += $.rect(bx, y, w, bh, { r: bh * 0.18, fill: col });\n if (A.wide) bars += $.text(r.label, { ...lf, x: bx - 18 * k, y: y + (bh - lf.size) / 2 - lf.size * 0.04, anchor: \"end\", fill: c.fg });\n else bars += $.text(r.label, { ...lf, x: bx, y: y - lf.size * 1.25, fill: c.fg });\n bars += $.text(fmtNum(v, p.decimals, p.prefix, p.suffix), { ...vf, x: bx + w + 14 * k, y: y + (bh - vf.size) / 2 - vf.size * 0.05, fill: c.fg });\n });\n s += M.part(\"bars\", { at: tin + 0.3, in: \"fade\", dur: 0.4, out: \"fade\" }, bars);\n const pf = { ...$.font(\"display\"), size: Math.min(A.h * 0.18, 200 * k) };\n const label = String(periods[Math.round(pos)] || \"\");\n s += M.part(\"period\", { at: tin + 0.4, in: \"fade\", dur: 0.4, out: \"fade\", vary: false }, $.text(label, { ...pf, x: A.x + A.w, y: A.y + A.h - pf.size * 1.05, anchor: \"end\", fill: $.rgba(c.fg, 0.16) }));\n return s;\n}\n","libs":[]},{"id":"goals","name":"Goal meters","description":"Rows of capsule meters fill to their share of a goal, each with its label and a percentage counting up.","category":"chart","tags":["chart","progress","goal","percent","data"],"placement":"full","duration":{"default":5,"min":3,"max":30},"props":{"title":{"type":"text","label":"Title","default":"Launch checklist","maxLength":50},"rows":{"type":"list","label":"Rows (Label: percent)","default":[{"label":"Footage cut","value":100},{"label":"Captions","value":85},{"label":"Colour grade","value":60},{"label":"Sound mix","value":35}],"maxItems":8,"minItems":1,"of":{"label":"text","value":"number"}}},"parts":{"ground":"the ground","title":"the heading","rows":"each meter"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/goals","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const head = chartHead($, M, t, tin, p.title, A, k);\n s += head.s;\n const rows = p.rows || [];\n const room = A.y + A.h - head.y;\n const rowH = Math.min((A.tall ? 190 : 150) * k, room / Math.max(1, rows.length));\n const total = rows.length * rowH;\n const w = A.wide ? Math.min(A.w, 1500 * k) : A.w;\n const x = A.wide ? (W - w) / 2 : A.x;\n let y = head.y + Math.max(0, (room - total) / 2);\n const lf = { ...$.font(\"text\"), size: Math.min(36 * k, rowH * 0.26) };\n const vf = { ...$.font(\"display\"), size: Math.min(40 * k, rowH * 0.28) };\n rows.forEach(function (r, i) {\n const at = tin + 0.35 + i * 0.18;\n const pct = Math.max(0, Math.min(100, r.value));\n const g = $.E.out3($.seg(t, at, at + 1));\n const th = rowH * 0.3, ty = y + rowH * 0.42;\n const done = pct >= 100 && g >= 1;\n let row = $.text(String(r.label), { ...lf, x: x, y: y + rowH * 0.06, fill: c.fg });\n row += $.text(Math.round(pct * g) + \"%\", { ...vf, x: x + w, y: y + rowH * 0.04, anchor: \"end\", fill: done ? c.green : c.fg });\n row += $.rect(x, ty, w, th, { r: th / 2, fill: $.rgba(c.fg, 0.1) });\n row += $.rect(x, ty, Math.max(th, w * (pct / 100) * g), th, { r: th / 2, fill: done ? c.green : seriesColor($, 0) });\n if (done) row += $.icon(\"check\", x + w - th * 1.05, ty + th * 0.12, th * 0.76, { color: $.on(c.green), sw: 3 });\n s += M.part(\"rows\", { at: at - 0.1, in: \"rise\", distance: 20, out: \"fade\", box: { x: x, y: y, w: w, h: rowH } }, row);\n y += rowH;\n });\n return s;\n}\n","libs":[]},{"id":"versus","name":"Versus","description":"Two sides face off: a label and a big number counting up on each, a VS badge popping between them and a split bar settling to their ratio.","category":"chart","tags":["comparison","versus","vs","split","data"],"placement":"full","duration":{"default":5,"min":3,"max":30},"props":{"title":{"type":"text","label":"Title","default":"Hours to publish one video","maxLength":50},"left":{"type":"text","label":"Left","default":"Before","maxLength":20},"leftValue":{"type":"number","label":"Left value","default":9.5,"min":0,"max":1000000000000},"right":{"type":"text","label":"Right","default":"With MONARC","maxLength":20},"rightValue":{"type":"number","label":"Right value","default":1.5,"min":0,"max":1000000000000},"prefix":{"type":"text","label":"Before numbers","default":"","maxLength":3},"suffix":{"type":"text","label":"After numbers","default":" h","maxLength":6},"decimals":{"type":"number","label":"Decimals","default":1,"min":0,"max":3}},"parts":{"ground":"the ground","title":"the heading","left":"the left side","right":"the right side","badge":"the VS badge","split":"the split bar"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/versus","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const head = chartHead($, M, t, tin, p.title, A, k);\n s += head.s;\n // Side by side on wide and square frames, one above the other on tall ones.\n const stack = A.tall;\n const room = { x: A.x, y: head.y, w: A.w, h: A.y + A.h - head.y - 110 * k };\n const half = stack ? { w: room.w, h: room.h / 2 } : { w: room.w / 2, h: room.h };\n const vfmt = function (v) { return fmtNum(v, p.decimals, p.prefix, p.suffix); };\n const side = function (name, label, value, col, i) {\n const bx = stack ? room.x : room.x + i * half.w, by = stack ? room.y + i * half.h : room.y;\n const nf = fitFont($, vfmt(value), \"display\", Math.min(half.h * 0.42, 220 * k), half.w * 0.86, { ls: -0.03 });\n const lf = fitFont($, $.case(label), \"text\", 40 * k, half.w * 0.86);\n const cx = bx + half.w / 2, cy = by + half.h / 2;\n const num = vfmt(value * $.E.out3($.seg(t, tin + 0.5, tin + 1.6)));\n return M.part(name, { at: tin + 0.25 + i * 0.15, in: \"rise\", distance: 30, out: \"fade\", box: { x: bx, y: by, w: half.w, h: half.h } },\n $.text($.case(label), { ...lf, x: cx, y: cy - nf.size * 0.62 - lf.size * 1.4, anchor: \"middle\", fill: c.mute }),\n $.text(num, { ...nf, x: cx, y: cy - nf.size * 0.55, anchor: \"middle\", fill: col }));\n };\n s += side(\"left\", p.left, p.leftValue, c.fg, 0);\n s += side(\"right\", p.right, p.rightValue, c.accent, 1);\n const bcx = stack ? W / 2 : room.x + room.w / 2, bcy = stack ? room.y + room.h / 2 : room.y + room.h / 2;\n const br = 54 * k;\n s += M.part(\"badge\", { at: tin + 0.7, in: \"slam\", intensity: 0.5, out: \"fade\", box: { x: bcx - br, y: bcy - br, w: 2 * br, h: 2 * br } },\n stack ? \"\" : $.line(bcx, room.y + 40 * k, bcx, room.y + room.h - 40 * k, { stroke: $.rgba(c.fg, 0.14), sw: 3 * k }),\n $.circle(bcx, bcy, br, { fill: c.accent2 }), $.text(\"VS\", { ...$.font(\"display\"), size: br * 0.78, x: bcx, y: bcy - br * 0.45, anchor: \"middle\", fill: $.on(c.accent2) }));\n // The split bar: 50/50, then the true ratio.\n const tot = (p.leftValue + p.rightValue) || 1;\n const f = 0.5 + (p.leftValue / tot - 0.5) * $.E.inOut3($.seg(t, tin + 1.1, tin + 2.1));\n const sy = A.y + A.h - 60 * k, sh = 26 * k;\n s += M.part(\"split\", { at: tin + 0.9, in: \"grow-x\", origin: \"center\", dur: 0.4, out: \"fade\", box: { x: A.x, y: sy, w: A.w, h: sh } },\n $.rect(A.x, sy, A.w * f - 4 * k, sh, { r: sh / 2, fill: c.fg, op: 0.85 }), $.rect(A.x + A.w * f + 4 * k, sy, A.w * (1 - f) - 4 * k, sh, { r: sh / 2, fill: c.accent }));\n return s;\n}\n","libs":[]},{"id":"timeline","name":"Timeline","description":"Milestones along a line that draws itself: each date's dot pops as the line reaches it and its words rise beside it.","category":"list","tags":["timeline","history","milestones","roadmap","data"],"placement":"full","duration":{"default":7,"min":3,"max":40},"props":{"title":{"type":"text","label":"Title","default":"How we got here","maxLength":50},"rows":{"type":"list","label":"Milestones (When: what)","default":[{"when":"2019","what":"First edit in a garage"},{"when":"2021","what":"Studio opens in Lisbon"},{"when":"2023","what":"100 creators on board"},{"when":"2025","what":"The AI editor ships"},{"when":"2026","what":"Volito goes open"}],"maxItems":8,"minItems":2,"of":{"when":"text","what":"text"}}},"parts":{"ground":"the ground","title":"the heading","line":"the line","dots":"the dots","labels":"the dates and words"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/timeline","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const head = chartHead($, M, t, tin, p.title, A, k);\n s += head.s;\n const rows = p.rows || [];\n const n = rows.length;\n const horiz = A.wide;\n const t0 = tin + 0.5, t1 = Math.min(t0 + 0.6 * n + 0.8, $.beat(\"out\", $.dur - 0.45) - 0.5);\n const run = $.E.inOut2($.seg(t, t0, t1));\n const wf = { ...$.font(\"display\"), size: (horiz ? 44 : 46) * k };\n const tf = { ...$.font(\"text\"), size: (horiz ? 28 : 32) * k, lh: 1.25 };\n let line = \"\", dots = \"\", labels = \"\";\n if (horiz) {\n const ly = head.y + (A.y + A.h - head.y) / 2;\n const x0 = A.x + 40 * k, x1 = A.x + A.w - 40 * k;\n line = $.line(x0, ly, x1, ly, { stroke: $.rgba(c.fg, 0.14), sw: 6 * k }) + $.line(x0, ly, x0 + (x1 - x0) * run, ly, { stroke: c.accent, sw: 6 * k });\n const colW = (x1 - x0) / Math.max(1, n - 1);\n rows.forEach(function (r, i) {\n const x = n > 1 ? x0 + i * colW : (x0 + x1) / 2;\n const at = t0 + (n > 1 ? i / (n - 1) : 0) * (t1 - t0);\n const up = i % 2 === 0;\n dots += $.circle(x, ly, 16 * k * Math.max(0, $.pop(t, at)), { fill: c.bg, stroke: c.accent, sw: 6 * k });\n const lines = $.wrap(String(r.what), colW * 1.6, tf).slice(0, 2);\n const blockH = wf.size * 1.15 + lines.length * tf.size * 1.25;\n const by = up ? ly - 50 * k - blockH : ly + 50 * k;\n // Kept inside the safe area: a label near an edge slides in from its dot.\n const bw = Math.max($.measure(String(r.when), wf), Math.max.apply(null, lines.map(function (l) { return $.measure(l, tf); })));\n const lx = Math.max(A.x + bw / 2, Math.min(A.x + A.w - bw / 2, x));\n labels += M.part(\"labels\", { at: at, in: \"rise\", distance: up ? -20 : 20, out: \"fade\", box: { x: lx - bw / 2, y: by, w: bw, h: blockH } },\n $.text(String(r.when), { ...wf, x: lx, y: by, anchor: \"middle\", fill: c.accent }),\n lines.map(function (l, j) { return $.text(l, { ...tf, x: lx, y: by + wf.size * 1.15 + j * tf.size * 1.25, anchor: \"middle\", fill: c.fg }); }).join(\"\"));\n });\n } else {\n const lx = A.x + 30 * k;\n const y0 = head.y + 20 * k, y1 = A.y + A.h - 20 * k;\n line = $.line(lx, y0, lx, y1, { stroke: $.rgba(c.fg, 0.14), sw: 6 * k }) + $.line(lx, y0, lx, y0 + (y1 - y0) * run, { stroke: c.accent, sw: 6 * k });\n const rowH = (y1 - y0) / Math.max(1, n);\n rows.forEach(function (r, i) {\n const y = y0 + i * rowH + 10 * k;\n const at = t0 + (i / Math.max(1, n)) * (t1 - t0);\n dots += $.circle(lx, y + wf.size * 0.5, 16 * k * Math.max(0, $.pop(t, at)), { fill: c.bg, stroke: c.accent, sw: 6 * k });\n const lines = $.wrap(String(r.what), A.w - 90 * k, tf).slice(0, 2);\n labels += M.part(\"labels\", { at: at, in: \"slide\", from: \"left\", distance: 20, out: \"fade\", box: { x: lx + 50 * k, y: y, w: A.w - 90 * k, h: rowH } },\n $.text(String(r.when), { ...wf, x: lx + 50 * k, y: y, fill: c.accent }),\n lines.map(function (l, j) { return $.text(l, { ...tf, x: lx + 50 * k, y: y + wf.size * 1.12 + j * tf.size * 1.25, fill: c.fg }); }).join(\"\"));\n });\n }\n s += M.part(\"line\", { at: t0, in: \"none\", out: \"fade\", vary: false }, line);\n s += M.part(\"dots\", { at: t0, in: \"none\", out: \"fade\" }, dots);\n return s + labels;\n}\n","libs":[]},{"id":"funnel","name":"Funnel","description":"Stages stack from widest to narrowest, each dropping in with its count, and the share that carries on to the next stage between them.","category":"chart","tags":["funnel","conversion","pipeline","data"],"placement":"full","duration":{"default":6,"min":3,"max":30},"props":{"title":{"type":"text","label":"Title","default":"From view to sign-up","maxLength":50},"rows":{"type":"list","label":"Stages (Label: count)","default":[{"label":"Watched","value":120000},{"label":"Clicked","value":18400},{"label":"Tried it","value":6200},{"label":"Signed up","value":2480}],"maxItems":7,"minItems":2,"of":{"label":"text","value":"number"}},"prefix":{"type":"text","label":"Before numbers","default":"","maxLength":3},"suffix":{"type":"text","label":"After numbers","default":"","maxLength":6},"decimals":{"type":"number","label":"Decimals","default":0,"min":0,"max":3}},"parts":{"ground":"the ground","title":"the heading","stages":"each stage","rates":"the share between stages"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/funnel","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const head = chartHead($, M, t, tin, p.title, A, k);\n s += head.s;\n const rows = p.rows || [];\n const n = rows.length;\n const room = A.y + A.h - head.y;\n const gap = 40 * k;\n const sh = Math.min((A.tall ? 200 : 130) * k, (room - gap * (n - 1)) / n);\n const maxW = A.wide ? A.w * 0.6 : A.w * 0.9, minW = maxW * 0.22;\n const top = rows[0] ? rows[0].value || 1 : 1;\n const cx = A.wide ? A.x + maxW / 2 + 20 * k : W / 2;\n let y = head.y + Math.max(0, (room - (n * sh + (n - 1) * gap)) / 2);\n const lf = { ...$.font(\"display\"), size: Math.min(38 * k, sh * 0.3) };\n const vf = { ...$.font(\"mono\"), size: Math.min(32 * k, sh * 0.26), weight: 700 };\n rows.forEach(function (r, i) {\n const at = tin + 0.4 + i * 0.25;\n const w1 = minW + (maxW - minW) * Math.sqrt(r.value / top);\n const next = rows[i + 1];\n const w2 = next ? minW + (maxW - minW) * Math.sqrt(next.value / top) : w1 * 0.92;\n const d = \"M\" + $.fx(cx - w1 / 2) + \" \" + $.fx(y) + \"L\" + $.fx(cx + w1 / 2) + \" \" + $.fx(y) + \"L\" + $.fx(cx + w2 / 2) + \" \" + $.fx(y + sh) + \"L\" + $.fx(cx - w2 / 2) + \" \" + $.fx(y + sh) + \"Z\";\n // Deeper stages mix toward the ground; their words take whichever ink reads on them.\n const col = $.mix(seriesColor($, 0), c.bg, i * (0.4 / Math.max(1, n - 1)));\n let st = $.path(d, { fill: col });\n const labelInside = A.wide;\n const val = fmtNum(r.value * $.E.out3($.seg(t, at, at + 0.8)), p.decimals, p.prefix, p.suffix);\n if (labelInside) {\n st += $.text(String(r.label), { ...lf, x: cx, y: y + sh / 2 - lf.size * 0.85, anchor: \"middle\", fill: $.on(col) });\n st += $.text(val, { ...vf, x: cx, y: y + sh / 2 + lf.size * 0.25, anchor: \"middle\", fill: $.on(col) });\n } else {\n // The label and its final count, sized to fit the stage's narrow end.\n const line = String(r.label) + \" \" + fmtNum(r.value, p.decimals, p.prefix, p.suffix);\n const ff = fitFont($, line, \"display\", Math.min(lf.size, sh * 0.28), w2 * 0.86);\n st += $.text(String(r.label) + \" \" + val, { ...ff, x: cx, y: y + sh / 2 - ff.size * 0.55, anchor: \"middle\", fill: $.on(col) });\n }\n s += M.part(\"stages\", { at: at, in: \"drop\", distance: 30, ease: \"bouncy\", out: \"fade\", box: { x: cx - w1 / 2, y: y, w: w1, h: sh } }, st);\n if (next) {\n const rate = Math.round((next.value / (r.value || 1)) * 1000) / 10;\n const rx = A.wide ? cx + maxW / 2 + 60 * k : cx;\n const ry = y + sh + gap / 2;\n s += M.part(\"rates\", { at: at + 0.3, in: \"fade\", dur: 0.3, out: \"fade\", box: { x: rx - 80 * k, y: ry - 20 * k, w: 160 * k, h: 40 * k } },\n A.wide ? $.text(\"↓ \" + rate + \"%\", { ...vf, x: rx, y: ry - vf.size * 0.55, fill: c.mute }) : $.text(rate + \"% carry on\", { ...$.font(\"mono\"), size: 22 * k, x: rx, y: ry - 11 * k, anchor: \"middle\", fill: c.mute }));\n }\n y += sh + gap;\n });\n return s;\n}\n","libs":[]},{"id":"world-map","name":"World map","description":"A world map in the brand's colours: chosen countries fill in one by one (shaded by their values when given) and the top ones are labelled. Zooms to a region.","category":"chart","tags":["map","world","countries","choropleth","geo"],"placement":"full","libs":["world"],"duration":{"default":6,"min":3,"max":30},"props":{"title":{"type":"text","label":"Title","default":"Where our viewers are","maxLength":50},"countries":{"type":"list","label":"Countries (Name: value)","default":[{"name":"India","value":42},{"name":"United States","value":31},{"name":"Brazil","value":18},{"name":"United Kingdom","value":12},{"name":"Indonesia","value":11},{"name":"Germany","value":9},{"name":"Nigeria","value":7},{"name":"Japan","value":6}],"maxItems":40,"minItems":1,"of":{"name":"text","value":"number"}},"region":{"type":"choice","label":"Region","options":["world","americas","europe","africa","asia","middle east","oceania","india"],"default":"world"},"labels":{"type":"number","label":"Countries labelled","default":4,"min":0,"max":12},"prefix":{"type":"text","label":"Before numbers","default":"","maxLength":3},"suffix":{"type":"text","label":"After numbers","default":"%","maxLength":6},"decimals":{"type":"number","label":"Decimals","default":0,"min":0,"max":3}},"parts":{"ground":"the ground","title":"the heading","map":"the map","highlights":"the chosen countries","labels":"the labels"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/world-map","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\nvar REGIONS = { world: [10, 12, 1], americas: [-82, 8, 1.75], europe: [14, 53, 4.2], africa: [18, 2, 2.1], asia: [92, 32, 1.9], \"middle east\": [44, 28, 4.4], oceania: [142, -26, 2.8], india: [80, 22, 5] };\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const head = chartHead($, M, t, tin, p.title, A, k);\n s += head.s;\n let rg = REGIONS[p.region] || REGIONS.world;\n // On narrow frames the whole world is a thin strip: frame the chosen countries instead.\n if (p.region === \"world\" && !A.wide) {\n const pts = (p.countries || []).map(function (r) { return worldFind(r.name); }).filter(Boolean);\n if (pts.length) {\n let sx = 0, sy = 0, lat = 0;\n pts.forEach(function (cn) { sx += Math.cos(cn.c[0] * Math.PI / 180); sy += Math.sin(cn.c[0] * Math.PI / 180); lat += cn.c[1]; });\n rg = [Math.atan2(sy, sx) * 180 / Math.PI, lat / pts.length, A.tall ? 1.9 : 1.3];\n }\n }\n // A slow push in while the countries fill.\n const zoom = rg[2] * (1 + 0.05 * $.E.inOut2($.seg(t, tin, $.dur)));\n const box = { x: A.x, y: head.y, w: A.w, h: A.y + A.h - head.y };\n const m = worldMap($, { x: box.x, y: box.y, w: box.w, h: box.h, center: [rg[0], rg[1]], zoom: zoom });\n const picked = (p.countries || []).map(function (r) { return { r: r, c: worldFind(r.name) }; }).filter(function (q) { return q.c; });\n const maxV = Math.max.apply(null, picked.map(function (q) { return q.r.value; }).concat([1e-9]));\n const base = $.rgba(c.fg, 0.13);\n let land = \"\";\n m.countries.forEach(function (cn) { land += $.path(m.path(cn), { fill: base, stroke: c.bg, sw: 0.8 * k }); });\n const clipId = m.clipId();\n const mapBox = '<clipPath id=\"mmbox\"><rect x=\"' + box.x + '\" y=\"' + box.y + '\" width=\"' + box.w + '\" height=\"' + box.h + '\"/></clipPath>';\n $.def(mapBox);\n s += '<g clip-path=\"url(#mmbox)\">' + M.part(\"map\", { at: tin + 0.1, in: \"fade\", dur: 0.6, out: \"fade\", vary: false }, '<g clip-path=\"url(#' + clipId + ')\">' + land + \"</g>\");\n // Countries fill in order of value, each shaded by its share of the largest.\n const order = picked.slice().sort(function (a, b) { return b.r.value - a.r.value; });\n let hi = \"\";\n order.forEach(function (q, i) {\n const at = tin + 0.6 + i * Math.min(0.18, 1.6 / order.length);\n const f = $.E.out3($.seg(t, at, at + 0.4));\n if (f <= 0) return;\n const shade = 0.35 + 0.65 * (q.r.value / maxV);\n hi += $.path(m.path(q.c), { fill: c.accent, op: shade * f, stroke: c.bg, sw: 0.8 * k });\n });\n s += M.part(\"highlights\", { at: tin + 0.5, in: \"none\", out: \"fade\" }, '<g clip-path=\"url(#' + clipId + ')\">' + hi + \"</g>\");\n // Labels for the top countries, at their label points, kept inside the map box.\n let labs = \"\";\n const lf = { ...$.font(\"display\"), size: 26 * k }, vf = { ...$.font(\"mono\"), size: 22 * k, weight: 700 };\n order.slice(0, Math.round(p.labels)).forEach(function (q, i) {\n const at = tin + 1.2 + i * 0.2;\n const pt = m.project(q.c.c[0], q.c.c[1]);\n if (pt[0] < box.x || pt[0] > box.x + box.w || pt[1] < box.y || pt[1] > box.y + box.h) return;\n const name = String(q.r.name), val = fmtNum(q.r.value, p.decimals, p.prefix, p.suffix);\n const w = Math.max($.measure(name, lf), $.measure(val, vf)) + 28 * k, h = lf.size * 1.25 + vf.size * 1.3 + 16 * k;\n const lx = Math.max(box.x, Math.min(box.x + box.w - w, pt[0] + 14 * k)), ly = Math.max(box.y, Math.min(box.y + box.h - h, pt[1] - h - 14 * k));\n labs += M.part(\"labels\", { at: at, in: \"pop\", origin: \"bottom-left\", out: \"fade\", box: { x: lx, y: ly, w: w, h: h } },\n $.circle(pt[0], pt[1], 7 * k, { fill: c.fg, stroke: c.bg, sw: 3 * k }),\n $.rect(lx, ly, w, h, { r: 10 * k, fill: c.surface, shadow: \"md\" }),\n $.text(name, { ...lf, x: lx + 14 * k, y: ly + 8 * k, fill: c.fg }),\n $.text(val, { ...vf, x: lx + 14 * k, y: ly + 8 * k + lf.size * 1.25, fill: c.accent }));\n });\n return s + labs + \"</g>\";\n}\n"},{"id":"flight","name":"Flight route","description":"A map frames two places; a route arcs from one to the other along the great circle while a plane flies it, and both ends are pinned and named.","category":"chart","tags":["map","travel","route","flight","geo"],"placement":"full","libs":["world"],"duration":{"default":6,"min":3,"max":30},"props":{"from":{"type":"text","label":"From (name)","default":"Lisbon","maxLength":24},"fromAt":{"type":"text","label":"From (lat, lon or country)","default":"38.72, -9.14","maxLength":32},"to":{"type":"text","label":"To (name)","default":"Mumbai","maxLength":24},"toAt":{"type":"text","label":"To (lat, lon or country)","default":"19.08, 72.88","maxLength":32},"note":{"type":"text","label":"Note","default":"8,154 km · 9 h 40 m","maxLength":32}},"parts":{"ground":"the ground","map":"the map","route":"the route","plane":"the plane","pins":"the two pins and names","note":"the note"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/flight","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\n/** \"38.7, -9.1\" → [lon, lat]; a country name → its label point. */\nfunction placeAt(s) {\n const m = /^\\s*(-?\\d+(?:\\.\\d+)?)\\s*[, ]\\s*(-?\\d+(?:\\.\\d+)?)\\s*$/.exec(String(s || \"\"));\n if (m) return [Number(m[2]), Number(m[1])];\n const cn = worldFind(s);\n return cn ? cn.c : [0, 0];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const a = placeAt(p.fromAt), b = placeAt(p.toAt);\n // The great circle between them, as points (slerp on the unit sphere).\n const v = function (q) { const l = q[0] * Math.PI / 180, f = q[1] * Math.PI / 180; return [Math.cos(f) * Math.cos(l), Math.cos(f) * Math.sin(l), Math.sin(f)]; };\n const va = v(a), vb = v(b);\n const om = Math.acos(Math.max(-1, Math.min(1, va[0] * vb[0] + va[1] * vb[1] + va[2] * vb[2]))) || 1e-6;\n const gc = [];\n for (let i = 0; i <= 64; i++) {\n const f = i / 64, s1 = Math.sin((1 - f) * om) / Math.sin(om), s2 = Math.sin(f * om) / Math.sin(om);\n const x = s1 * va[0] + s2 * vb[0], y = s1 * va[1] + s2 * vb[1], z = s1 * va[2] + s2 * vb[2];\n gc.push([Math.atan2(y, x) * 180 / Math.PI, Math.asin(Math.max(-1, Math.min(1, z))) * 180 / Math.PI]);\n }\n // Frame both ends: centre between them (longitudes unwrapped), zoom to their spread.\n let lb = b[0];\n while (lb - a[0] > 180) lb -= 360;\n while (a[0] - lb > 180) lb += 360;\n const cen = [(a[0] + lb) / 2, (a[1] + b[1]) / 2 + 6];\n const box = { x: 0, y: 0, w: W, h: H };\n // Fit: project both ends at zoom 1, then zoom until they span 56% of the width or 45% of the height.\n const m1 = worldMap($, { x: 0, y: 0, w: W, h: H, center: cen, zoom: 1 });\n const pa = m1.project(a[0], a[1]), pb = m1.project(b[0], b[1]);\n const fit = Math.min((W * 0.56) / Math.max(1, Math.abs(pa[0] - pb[0])), (H * 0.45) / Math.max(1, Math.abs(pa[1] - pb[1])));\n const zoom = Math.min(8, Math.max(1, fit)) * (1 + 0.04 * $.E.inOut2($.seg(t, tin, $.dur)));\n const m = worldMap($, { x: box.x, y: box.y, w: box.w, h: box.h, center: cen, zoom: zoom });\n let land = \"\";\n m.countries.forEach(function (cn) { land += $.path(m.path(cn), { fill: $.rgba(c.fg, 0.14), stroke: c.bg, sw: 0.8 * k }); });\n s += M.part(\"map\", { at: tin, in: \"fade\", dur: 0.6, out: \"fade\", vary: false }, '<g clip-path=\"url(#' + m.clipId() + ')\">' + land + \"</g>\");\n // The route: projected points, lifted into an arc, drawn on as the plane flies.\n const pts = m.ring(gc);\n const P = pts.map(function (q, i) { const f = i / (pts.length - 1); return [q[0], q[1] - Math.sin(f * Math.PI) * Math.min(220 * k, Math.hypot(pts[pts.length - 1][0] - pts[0][0], pts[pts.length - 1][1] - pts[0][1]) * 0.22)]; });\n const t0 = tin + 0.9, t1 = $.beat(\"out\", $.dur - 0.45) - 0.9;\n const run = $.E.inOut2($.seg(t, t0, t1));\n const upto = run * (P.length - 1), iu = Math.floor(upto), fu = upto - iu;\n const drawn = P.slice(0, iu + 1);\n if (iu < P.length - 1) drawn.push([P[iu][0] + (P[iu + 1][0] - P[iu][0]) * fu, P[iu][1] + (P[iu + 1][1] - P[iu][1]) * fu]);\n const dd = drawn.map(function (q, i) { return (i ? \"L\" : \"M\") + $.fx(q[0]) + \" \" + $.fx(q[1]); }).join(\"\");\n const full = P.map(function (q, i) { return (i ? \"L\" : \"M\") + $.fx(q[0]) + \" \" + $.fx(q[1]); }).join(\"\");\n s += M.part(\"route\", { at: t0, in: \"none\", out: \"fade\" }, $.path(full, { stroke: $.rgba(c.fg, 0.2), sw: 3 * k, dash: 10 * k + \" \" + 10 * k, fill: \"none\" }), drawn.length > 1 ? $.path(dd, { stroke: c.accent, sw: 6 * k, fill: \"none\" }) : \"\");\n // The plane at the tip, pointing along the route.\n if (run > 0 && run < 1) {\n const tip = drawn[drawn.length - 1], prev = drawn[Math.max(0, drawn.length - 2)];\n const ang = Math.atan2(tip[1] - prev[1], tip[0] - prev[0]) * 180 / Math.PI;\n const sz = 30 * k;\n const plane = \"M\" + sz + \" 0L\" + -sz * 0.6 + \" \" + -sz * 0.75 + \"L\" + -sz * 0.35 + \" 0L\" + -sz * 0.6 + \" \" + sz * 0.75 + \"Z\";\n s += M.part(\"plane\", { at: t0, in: \"none\", out: \"fade\" }, $.g({ transform: \"translate(\" + $.fx(tip[0]) + \" \" + $.fx(tip[1]) + \") rotate(\" + $.fx(ang) + \")\" }, $.path(plane, { fill: c.fg, stroke: c.bg, sw: 2 * k })));\n }\n // Pins and names at both ends.\n const nf = { ...$.font(\"display\"), size: 40 * k };\n const pin = function (q, name, at, right) {\n const pr = 13 * k;\n return M.part(\"pins\", { at: at, in: \"pop\", out: \"fade\", box: { x: q[0] - pr, y: q[1] - pr, w: 2 * pr, h: 2 * pr } },\n $.circle(q[0], q[1], pr, { fill: c.accent, stroke: c.bg, sw: 4 * k }),\n $.text($.case(String(name)), { ...nf, x: q[0] + (right ? 1 : -1) * 26 * k, y: q[1] - nf.size * 0.55, anchor: right ? \"start\" : \"end\", fill: c.fg, shadow: \"sm\" }));\n };\n const ra = P[0], rb = P[P.length - 1];\n // Names face inward, toward the middle of the frame.\n s += pin(ra, p.from, tin + 0.5, ra[0] < W / 2);\n s += pin(rb, p.to, t1 - 0.1, rb[0] < W / 2);\n if (p.note) s += M.text(\"note\", { at: t1, in: \"rise\", distance: 16, out: \"fade\", ...$.font(\"mono\"), size: 30 * k, ls: 0.06, x: W / 2, y: A.y + A.h - 40 * k, anchor: \"middle\", fill: c.mute }, String(p.note));\n return s;\n}\n"}]},{"format":1,"id":"3d","name":"MONARC 3D","version":"1.0.0","description":"Extruded logos, titles and numbers, a card that flips, a word cube, a 3D bar chart and a globe — real depth, light and camera moves, rendered without a GPU, in any brand.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/3d","style":"ember","shared":"shared.js","items":["logo-3d","title-3d","number-3d","card-flip","cube","bars-3d","globe"],"entries":[{"id":"logo-3d","name":"3D logo","description":"An extruded wordmark swings in on a spring and settles as a light sweeps across its faces; the tagline rises beneath. Real depth, light and perspective.","category":"logo","tags":["3d","logo","sting","intro","brand","extruded"],"placement":"full","libs":["3d"],"duration":{"default":4,"min":2.5,"max":20},"props":{"wordmark":{"type":"text","label":"Wordmark","default":"MONARC","maxLength":16},"tagline":{"type":"text","label":"Tagline","default":"Made with motion","maxLength":40},"depth":{"type":"number","label":"Depth (share of the height)","default":0.3,"min":0.05,"max":1}},"parts":{"ground":"the ground","wordmark":"the 3D wordmark","tagline":"the tagline"},"beats":{"in":0.1,"out":-0.45},"pack":"3d","ref":"3d/logo-3d","code":"// MONARC 3D — helpers this pack's items share. The scenes are built with scene3d (the 3D kit).\n\n/** The focal length a scene3d camera with this field of view has on this canvas. */\nfunction focalFor($, fov) {\n return ($.H / 2) / Math.tan(((fov || 35) * Math.PI) / 360);\n}\n\n/** Lines of 3D text no wider than maxW (px at the target depth), by words. */\nfunction wrap3d(S, str, o, maxW, maxLines) {\n var words = String(str || \"\").split(/\\s+/).filter(Boolean), lines = [], cur = \"\";\n words.forEach(function (w) {\n var next = cur ? cur + \" \" + w : w;\n if (cur && S.measure(next, o) > maxW) { lines.push(cur); cur = w; } else cur = next;\n });\n if (cur) lines.push(cur);\n if (maxLines && lines.length > maxLines) { var rest = lines.slice(maxLines - 1).join(\" \"); lines = lines.slice(0, maxLines - 1).concat([rest]); }\n return lines;\n}\n\n/** The biggest size (≤ max) at which str fits maxW on one line, in 3D text. */\nfunction fit3d(S, str, o, maxW, max) {\n var w = S.measure(str, Object.assign({}, o, { size: 100 }));\n return w > 0 ? Math.min(max, (maxW / w) * 100) : max;\n}\n\n/** A soft contact shadow on the floor under a point (2D, drawn before the scene). */\nfunction floorShadow($, S, p, rx, op) {\n var q = S.project(p);\n if (!q) return \"\";\n var id = $.uid(\"fs\");\n $.def('<radialGradient id=\"' + id + '\"><stop offset=\"0\" stop-color=\"#000\" stop-opacity=\"' + (op || 0.45) + '\"/><stop offset=\"1\" stop-color=\"#000\" stop-opacity=\"0\"/></radialGradient>');\n return '<ellipse cx=\"' + $.fx(q.x) + '\" cy=\"' + $.fx(q.y) + '\" rx=\"' + $.fx(rx * q.s) + '\" ry=\"' + $.fx(rx * q.s * 0.18) + '\" fill=\"url(#' + id + ')\"/>';\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.1);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [W / 2, H / 2] }));\n const word = $.case(String(p.wordmark || \"\"));\n const face = $.font(\"display\");\n const cy = A.y + A.h * 0.46;\n const e = $.spring(Math.max(0, t - tin), 2.1, 6.2);\n // The light sweeps right across the letters as they settle.\n const sweep = $.seg(t, tin + 0.45, tin + 1.5);\n const lx = -1.4 + 2.8 * $.E.inOut2(sweep);\n const S = scene3d($, { camera: { fov: 30, yaw: -7 + 14 * $.seg(t, tin, $.dur), pitch: -5, y: cy }, light: { dir: [lx, 0.4, 0.85], spec: 0.2 + 0.7 * Math.sin(Math.PI * sweep), shine: 14 } });\n const size = fit3d(S, word, { face: face }, A.w * 0.82, Math.min(A.h * 0.3, 300 * k));\n const mesh = S.text(word, { face: face, size: size, depth: size * p.depth, color: c.fg, side: c.accent });\n S.add(mesh, { rotate: [10 * (1 - e), -78 * (1 - e), 0], scale: 0.88 + 0.12 * e });\n const shadow = floorShadow($, S, [0, size * 0.62, 0], mesh.w * 0.55, 0.4 * Math.min(1, e));\n s += M.part(\"wordmark\", { at: tin, in: \"fade\", dur: 0.2, out: \"zoom\", box: { x: W / 2 - mesh.w / 2, y: cy - size, w: mesh.w, h: size * 2 } }, shadow + S.render());\n if (p.tagline) {\n const tf = fitFont($, String(p.tagline).toUpperCase(), \"mono\", 34 * k, A.w * 0.8, { ls: 0.24 });\n s += M.text(\"tagline\", { at: tin + 1.1, in: \"rise\", distance: 18, out: \"fade\", ...tf, x: W / 2, y: cy + size * 0.75, anchor: \"middle\", fill: c.mute }, String(p.tagline).toUpperCase());\n }\n return s;\n}\n"},{"id":"title-3d","name":"3D title","description":"Extruded letters flip up one by one on springs while the camera eases in; the subtitle rises beneath.","category":"title","tags":["3d","title","chapter","kinetic","extruded"],"placement":"full","libs":["3d"],"duration":{"default":4.5,"min":2.5,"max":20},"props":{"title":{"type":"text","label":"Title","default":"Depth over reach","maxLength":28},"subtitle":{"type":"text","label":"Subtitle","default":"Chapter two","maxLength":40},"depth":{"type":"number","label":"Depth (share of the height)","default":0.22,"min":0.05,"max":1}},"parts":{"ground":"the ground","title":"the 3D title","subtitle":"the subtitle"},"beats":{"in":0.1,"out":-0.45},"pack":"3d","ref":"3d/title-3d","code":"// MONARC 3D — helpers this pack's items share. The scenes are built with scene3d (the 3D kit).\n\n/** The focal length a scene3d camera with this field of view has on this canvas. */\nfunction focalFor($, fov) {\n return ($.H / 2) / Math.tan(((fov || 35) * Math.PI) / 360);\n}\n\n/** Lines of 3D text no wider than maxW (px at the target depth), by words. */\nfunction wrap3d(S, str, o, maxW, maxLines) {\n var words = String(str || \"\").split(/\\s+/).filter(Boolean), lines = [], cur = \"\";\n words.forEach(function (w) {\n var next = cur ? cur + \" \" + w : w;\n if (cur && S.measure(next, o) > maxW) { lines.push(cur); cur = w; } else cur = next;\n });\n if (cur) lines.push(cur);\n if (maxLines && lines.length > maxLines) { var rest = lines.slice(maxLines - 1).join(\" \"); lines = lines.slice(0, maxLines - 1).concat([rest]); }\n return lines;\n}\n\n/** The biggest size (≤ max) at which str fits maxW on one line, in 3D text. */\nfunction fit3d(S, str, o, maxW, max) {\n var w = S.measure(str, Object.assign({}, o, { size: 100 }));\n return w > 0 ? Math.min(max, (maxW / w) * 100) : max;\n}\n\n/** A soft contact shadow on the floor under a point (2D, drawn before the scene). */\nfunction floorShadow($, S, p, rx, op) {\n var q = S.project(p);\n if (!q) return \"\";\n var id = $.uid(\"fs\");\n $.def('<radialGradient id=\"' + id + '\"><stop offset=\"0\" stop-color=\"#000\" stop-opacity=\"' + (op || 0.45) + '\"/><stop offset=\"1\" stop-color=\"#000\" stop-opacity=\"0\"/></radialGradient>');\n return '<ellipse cx=\"' + $.fx(q.x) + '\" cy=\"' + $.fx(q.y) + '\" rx=\"' + $.fx(rx * q.s) + '\" ry=\"' + $.fx(rx * q.s * 0.18) + '\" fill=\"url(#' + id + ')\"/>';\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.1);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [W * 0.3, H * 0.3] }));\n const face = $.font(\"display\");\n const title = $.case(String(p.title || \"\"));\n const cy = A.y + A.h * 0.45;\n const fov = 32, f = focalFor($, fov);\n const S = scene3d($, { camera: { fov: fov, distance: f * (1.18 - 0.18 * $.E.out3($.seg(t, tin, $.dur))), pitch: -8, y: cy }, light: { spec: 0.35 } });\n // One line when it holds at a good size, else two.\n let lines = [title];\n let size = fit3d(S, title, { face: face }, A.w * 0.86, Math.min(A.h * 0.24, 260 * k));\n if (size < Math.min(A.h * 0.24, 260 * k) * 0.62) {\n lines = wrap3d(S, title, { face: face, size: 100 }, (A.w * 0.86 * 100) / (Math.min(A.h * 0.2, 220 * k)), 2);\n size = Math.min.apply(null, lines.map(function (l) { return fit3d(S, l, { face: face }, A.w * 0.86, Math.min(A.h * 0.2, 220 * k)); }));\n }\n let n0 = 0;\n lines.forEach(function (line, li) {\n const base = n0;\n const mesh = S.text(line, { face: face, size: size, depth: size * p.depth, color: c.fg, side: c.accent, each: function (i) {\n const at = tin + 0.25 + (base + i) * 0.055;\n if (t < at) return { hide: true };\n const e = $.spring(t - at, 2.4, 6.8);\n return { rotate: [-100 * (1 - e), 0, 0], offset: [0, size * 0.25 * (1 - Math.min(1, e)), 0] };\n } });\n S.add(mesh, { position: [0, (li - (lines.length - 1) / 2) * size * 1.12, 0] });\n n0 += Array.from(line).length;\n });\n s += M.part(\"title\", { at: tin, in: \"fade\", dur: 0.2, out: \"blur\", box: { x: A.x, y: cy - size * lines.length, w: A.w, h: size * lines.length * 2 } }, S.render());\n if (p.subtitle) {\n const sf = fitFont($, $.case(String(p.subtitle)), \"text\", 46 * k, A.w * 0.8);\n s += M.text(\"subtitle\", { at: tin + 0.4 + n0 * 0.055, in: \"rise\", distance: 20, out: \"fade\", ...sf, x: W / 2, y: cy + size * (lines.length * 0.56 + 0.35), anchor: \"middle\", fill: c.mute }, $.case(String(p.subtitle)));\n }\n return s;\n}\n"},{"id":"number-3d","name":"3D number","description":"A big extruded number counts up, tilted toward the camera, with a light sweep when it lands and the label beneath.","category":"stat","tags":["3d","number","stat","counter","extruded"],"placement":"full","libs":["3d"],"duration":{"default":4,"min":2.5,"max":20},"props":{"value":{"type":"number","label":"Value","default":3.2,"min":-1000000000000,"max":1000000000000},"decimals":{"type":"number","label":"Decimals","default":1,"min":0,"max":3},"prefix":{"type":"text","label":"Before","default":"","maxLength":3},"suffix":{"type":"text","label":"After","default":"×","maxLength":4},"label":{"type":"text","label":"Label","default":"more videos shipped","maxLength":48}},"parts":{"ground":"the ground","number":"the 3D number","label":"the label"},"beats":{"in":0.1,"out":-0.45},"pack":"3d","ref":"3d/number-3d","code":"// MONARC 3D — helpers this pack's items share. The scenes are built with scene3d (the 3D kit).\n\n/** The focal length a scene3d camera with this field of view has on this canvas. */\nfunction focalFor($, fov) {\n return ($.H / 2) / Math.tan(((fov || 35) * Math.PI) / 360);\n}\n\n/** Lines of 3D text no wider than maxW (px at the target depth), by words. */\nfunction wrap3d(S, str, o, maxW, maxLines) {\n var words = String(str || \"\").split(/\\s+/).filter(Boolean), lines = [], cur = \"\";\n words.forEach(function (w) {\n var next = cur ? cur + \" \" + w : w;\n if (cur && S.measure(next, o) > maxW) { lines.push(cur); cur = w; } else cur = next;\n });\n if (cur) lines.push(cur);\n if (maxLines && lines.length > maxLines) { var rest = lines.slice(maxLines - 1).join(\" \"); lines = lines.slice(0, maxLines - 1).concat([rest]); }\n return lines;\n}\n\n/** The biggest size (≤ max) at which str fits maxW on one line, in 3D text. */\nfunction fit3d(S, str, o, maxW, max) {\n var w = S.measure(str, Object.assign({}, o, { size: 100 }));\n return w > 0 ? Math.min(max, (maxW / w) * 100) : max;\n}\n\n/** A soft contact shadow on the floor under a point (2D, drawn before the scene). */\nfunction floorShadow($, S, p, rx, op) {\n var q = S.project(p);\n if (!q) return \"\";\n var id = $.uid(\"fs\");\n $.def('<radialGradient id=\"' + id + '\"><stop offset=\"0\" stop-color=\"#000\" stop-opacity=\"' + (op || 0.45) + '\"/><stop offset=\"1\" stop-color=\"#000\" stop-opacity=\"0\"/></radialGradient>');\n return '<ellipse cx=\"' + $.fx(q.x) + '\" cy=\"' + $.fx(q.y) + '\" rx=\"' + $.fx(rx * q.s) + '\" ry=\"' + $.fx(rx * q.s * 0.18) + '\" fill=\"url(#' + id + ')\"/>';\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.1);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [W / 2, H * 0.45] }));\n const dec = Math.max(0, Math.min(3, Math.round(p.decimals)));\n const fmt = function (v) { const s2 = v.toFixed(dec); const parts = s2.split(\".\"); return (p.prefix || \"\") + parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + (parts[1] ? \".\" + parts[1] : \"\") + (p.suffix || \"\"); };\n const face = $.font(\"display\");\n const cy = A.y + A.h * 0.42;\n const land = tin + 1.4;\n const sweep = $.seg(t, land, land + 0.9);\n const S = scene3d($, { camera: { fov: 30, yaw: -16 + 10 * $.E.out3($.seg(t, tin, $.dur)), pitch: -10, y: cy }, light: { dir: [-1.2 + 2.4 * sweep, 0.4, 0.85], spec: 0.2 + 0.6 * Math.sin(Math.PI * sweep), shine: 14 } });\n const final = fmt(p.value);\n const size = fit3d(S, final, { face: face }, A.w * 0.8, Math.min(A.h * 0.36, 380 * k));\n const v = p.value * $.E.out3($.seg(t, tin + 0.2, land));\n const e = $.spring(Math.max(0, t - tin), 2, 7);\n S.add(S.text(fmt(v), { face: face, size: size, depth: size * 0.28, color: c.accent, side: $.mix(c.accent, c.bg, 0.45) }), { rotate: [14 * (1 - e) + 8, 0, 0], scale: 0.8 + 0.2 * e });\n s += M.part(\"number\", { at: tin, in: \"fade\", dur: 0.2, out: \"zoom\", box: { x: A.x, y: cy - size, w: A.w, h: size * 2 } }, floorShadow($, S, [0, size * 0.6, 0], S.measure(final, { face: face, size: size }) * 0.5, 0.35) + S.render());\n if (p.label) {\n const lf = fitFont($, $.case(String(p.label)), \"text\", 50 * k, A.w * 0.86);\n s += M.text(\"label\", { at: land - 0.2, in: \"rise\", distance: 20, out: \"fade\", ...lf, x: W / 2, y: cy + size * 0.78, anchor: \"middle\", fill: c.fg }, $.case(String(p.label)));\n }\n return s;\n}\n"},{"id":"card-flip","name":"3D card flip","description":"A card with real thickness tilts in, holds its front, then flips over to its back. Good for before/after, question/answer, myth/fact.","category":"scene","tags":["3d","card","flip","reveal","before after"],"placement":"full","libs":["3d"],"faces3d":["display","text"],"duration":{"default":5,"min":3,"max":20},"props":{"frontTitle":{"type":"text","label":"Front","default":"Before","maxLength":24},"frontText":{"type":"text","label":"Front line","default":"9 hours in the timeline","maxLength":60},"backTitle":{"type":"text","label":"Back","default":"After","maxLength":24},"backText":{"type":"text","label":"Back line","default":"One sentence to the agent","maxLength":60},"flipAt":{"type":"number","label":"Flip at (s)","default":2.1,"min":0.5,"max":20}},"parts":{"ground":"the ground","card":"the card"},"beats":{"in":0.1,"out":-0.45},"pack":"3d","ref":"3d/card-flip","code":"// MONARC 3D — helpers this pack's items share. The scenes are built with scene3d (the 3D kit).\n\n/** The focal length a scene3d camera with this field of view has on this canvas. */\nfunction focalFor($, fov) {\n return ($.H / 2) / Math.tan(((fov || 35) * Math.PI) / 360);\n}\n\n/** Lines of 3D text no wider than maxW (px at the target depth), by words. */\nfunction wrap3d(S, str, o, maxW, maxLines) {\n var words = String(str || \"\").split(/\\s+/).filter(Boolean), lines = [], cur = \"\";\n words.forEach(function (w) {\n var next = cur ? cur + \" \" + w : w;\n if (cur && S.measure(next, o) > maxW) { lines.push(cur); cur = w; } else cur = next;\n });\n if (cur) lines.push(cur);\n if (maxLines && lines.length > maxLines) { var rest = lines.slice(maxLines - 1).join(\" \"); lines = lines.slice(0, maxLines - 1).concat([rest]); }\n return lines;\n}\n\n/** The biggest size (≤ max) at which str fits maxW on one line, in 3D text. */\nfunction fit3d(S, str, o, maxW, max) {\n var w = S.measure(str, Object.assign({}, o, { size: 100 }));\n return w > 0 ? Math.min(max, (maxW / w) * 100) : max;\n}\n\n/** A soft contact shadow on the floor under a point (2D, drawn before the scene). */\nfunction floorShadow($, S, p, rx, op) {\n var q = S.project(p);\n if (!q) return \"\";\n var id = $.uid(\"fs\");\n $.def('<radialGradient id=\"' + id + '\"><stop offset=\"0\" stop-color=\"#000\" stop-opacity=\"' + (op || 0.45) + '\"/><stop offset=\"1\" stop-color=\"#000\" stop-opacity=\"0\"/></radialGradient>');\n return '<ellipse cx=\"' + $.fx(q.x) + '\" cy=\"' + $.fx(q.y) + '\" rx=\"' + $.fx(rx * q.s) + '\" ry=\"' + $.fx(rx * q.s * 0.18) + '\" fill=\"url(#' + id + ')\"/>';\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.1);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [W / 2, H * 0.4] }));\n const cw = A.wide ? A.w * 0.58 : A.tall ? A.w * 0.92 : A.w * 0.78;\n const ch = A.wide ? cw * 0.56 : A.tall ? cw * 1.05 : cw * 0.8;\n const d = 18 * k;\n const cy = A.y + A.h * 0.5;\n const S = scene3d($, { camera: { fov: 30, pitch: -6, y: cy }, light: { spec: 0.3 } });\n // In: tilted back, settling on a spring. Flip: a half turn with a lift toward the camera.\n const e = $.spring(Math.max(0, t - tin), 2, 6.5);\n const fl = $.E.inOut3($.seg(t, p.flipAt, p.flipAt + 0.9));\n const card = { rotate: [42 * (1 - e) + 12 * Math.sin(Math.PI * fl), 180 * fl - 14 * (1 - e), 0], position: [0, 80 * k * (1 - e), -140 * k * Math.sin(Math.PI * fl)] };\n S.add(S.card(cw, ch, { depth: d, radius: 36 * k, color: c.surface, back: c.accent, side: $.mix(c.accent, c.bg, 0.35) }), card);\n const side = function (title, line, back) {\n const ink = back ? $.on(c.accent) : c.fg;\n const tface = $.font(\"display\"), lface = $.font(\"text\");\n const ts = fit3d(S, title, { face: tface }, cw * 0.8, ch * 0.32);\n const ls = Math.min(ts * 0.34, ch * 0.09);\n const lines = wrap3d(S, line, { face: lface, size: ls }, cw * 0.8, 2);\n const z = back ? d / 2 + 1.5 : -d / 2 - 1.5;\n const ry = back ? 180 : 0;\n const total = ts + (lines.length ? ts * 0.35 + lines.length * ls * 1.3 : 0);\n let y = -total / 2 + ts / 2;\n S.add(S.text(title, { face: tface, size: ts, depth: 0, color: ink }), { position: [0, y, z], rotate: [0, ry, 0], parent: card, flat: true, decal: true });\n y += ts / 2 + ts * 0.35 + ls / 2;\n lines.forEach(function (l, i) { S.add(S.text(l, { face: lface, size: ls, depth: 0, color: ink }), { position: [0, y + i * ls * 1.3, z], rotate: [0, ry, 0], parent: card, flat: true, decal: true }); });\n };\n side($.case(String(p.frontTitle || \"\")), String(p.frontText || \"\"), false);\n side($.case(String(p.backTitle || \"\")), String(p.backText || \"\"), true);\n s += M.part(\"card\", { at: tin, in: \"fade\", dur: 0.25, out: \"zoom\", box: { x: W / 2 - cw / 2, y: cy - ch / 2, w: cw, h: ch } }, floorShadow($, S, [0, ch * 0.62, 0], cw * 0.5, 0.35) + S.render());\n return s;\n}\n"},{"id":"cube","name":"3D word cube","description":"A cube turns a quarter at a time on a spring, each side showing the next word — a kinetic way through a list of 2 to 4 words.","category":"text","tags":["3d","cube","words","kinetic","rotate"],"placement":"full","libs":["3d"],"duration":{"default":5,"min":2.5,"max":30},"props":{"lead":{"type":"text","label":"Lead-in","default":"Edit","maxLength":20},"words":{"type":"list","label":"Words (2–4)","default":["faster","smarter","together","anywhere"],"maxItems":4,"minItems":2,"of":"text"}},"parts":{"ground":"the ground","lead":"the lead-in","cube":"the cube"},"beats":{"in":0.1,"out":-0.45},"pack":"3d","ref":"3d/cube","code":"// MONARC 3D — helpers this pack's items share. The scenes are built with scene3d (the 3D kit).\n\n/** The focal length a scene3d camera with this field of view has on this canvas. */\nfunction focalFor($, fov) {\n return ($.H / 2) / Math.tan(((fov || 35) * Math.PI) / 360);\n}\n\n/** Lines of 3D text no wider than maxW (px at the target depth), by words. */\nfunction wrap3d(S, str, o, maxW, maxLines) {\n var words = String(str || \"\").split(/\\s+/).filter(Boolean), lines = [], cur = \"\";\n words.forEach(function (w) {\n var next = cur ? cur + \" \" + w : w;\n if (cur && S.measure(next, o) > maxW) { lines.push(cur); cur = w; } else cur = next;\n });\n if (cur) lines.push(cur);\n if (maxLines && lines.length > maxLines) { var rest = lines.slice(maxLines - 1).join(\" \"); lines = lines.slice(0, maxLines - 1).concat([rest]); }\n return lines;\n}\n\n/** The biggest size (≤ max) at which str fits maxW on one line, in 3D text. */\nfunction fit3d(S, str, o, maxW, max) {\n var w = S.measure(str, Object.assign({}, o, { size: 100 }));\n return w > 0 ? Math.min(max, (maxW / w) * 100) : max;\n}\n\n/** A soft contact shadow on the floor under a point (2D, drawn before the scene). */\nfunction floorShadow($, S, p, rx, op) {\n var q = S.project(p);\n if (!q) return \"\";\n var id = $.uid(\"fs\");\n $.def('<radialGradient id=\"' + id + '\"><stop offset=\"0\" stop-color=\"#000\" stop-opacity=\"' + (op || 0.45) + '\"/><stop offset=\"1\" stop-color=\"#000\" stop-opacity=\"0\"/></radialGradient>');\n return '<ellipse cx=\"' + $.fx(q.x) + '\" cy=\"' + $.fx(q.y) + '\" rx=\"' + $.fx(rx * q.s) + '\" ry=\"' + $.fx(rx * q.s * 0.18) + '\" fill=\"url(#' + id + ')\"/>';\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.1);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const words = (p.words || []).map(function (w) { return $.case(String(w)); });\n const n = Math.max(2, words.length);\n const face = $.font(\"display\");\n const lead = $.case(String(p.lead || \"\"));\n // A long box that rolls about its horizontal axis: four faces as wide as the longest word.\n const probe = scene3d($, {});\n const box = A.wide ? Math.min(A.h * 0.36, 360 * k) : Math.min(A.w * 0.42, 380 * k);\n const cw = Math.min(A.w * (A.wide && lead ? 0.6 : 0.88), Math.max.apply(null, words.map(function (w) { return probe.measure(w, { face: face, size: box * 0.6 }); })) + box * 0.5);\n const lf = fitFont($, lead, \"display\", box * 0.5, A.w * 0.9);\n const lw = lead ? $.measure(lead, lf) : 0;\n const stack = !A.wide;\n const cx = stack ? W / 2 : W / 2 + (lw + 30 * k) / 2;\n const cy = A.y + A.h * (stack ? 0.55 : 0.5);\n const S = scene3d($, { camera: { fov: 30, pitch: -6, yaw: -16, x: cx, y: cy }, light: { spec: 0.25 } });\n // A quarter roll for each word after the first, on springs, evenly through the hold.\n const span = Math.max(0.8, $.beat(\"out\", $.dur - 0.45) - tin - 0.8);\n const step = span / n;\n let turn = 0;\n for (let i = 1; i < n; i++) turn += Math.min(1.08, $.spring(Math.max(0, t - (tin + 0.5 + i * step)), 2.2, 7));\n const cube = { rotate: [90 * turn, 0, 0], position: [0, 0, 0] };\n const hw = cw / 2, hh = box / 2, hd = box / 2;\n const quad = function (pts, nrm, col) { return { loops: [pts], n: nrm, color: col, role: \"side\" }; };\n const faceCol = function (i) { return i % 2 ? c.accent : c.surface; };\n const faces = [\n quad([[-hw, -hh, -hd], [hw, -hh, -hd], [hw, hh, -hd], [-hw, hh, -hd]], [0, 0, -1], faceCol(0)),\n quad([[-hw, -hh, hd], [hw, -hh, hd], [hw, -hh, -hd], [-hw, -hh, -hd]], [0, -1, 0], faceCol(1)),\n quad([[hw, -hh, hd], [-hw, -hh, hd], [-hw, hh, hd], [hw, hh, hd]], [0, 0, 1], faceCol(2)),\n quad([[-hw, hh, -hd], [hw, hh, -hd], [hw, hh, hd], [-hw, hh, hd]], [0, 1, 0], faceCol(3)),\n quad([[hw, -hh, -hd], [hw, -hh, hd], [hw, hh, hd], [hw, hh, -hd]], [1, 0, 0], $.mix(c.surface, c.bg, 0.45)),\n quad([[-hw, -hh, hd], [-hw, -hh, -hd], [-hw, hh, -hd], [-hw, hh, hd]], [-1, 0, 0], $.mix(c.surface, c.bg, 0.45)),\n ];\n S.add({ faces: faces }, cube);\n // Word i sits on the face that rolls to the front after i quarter turns: the front face turned back by i quarters.\n words.slice(0, 4).forEach(function (w, i) {\n const size = fit3d(S, w, { face: face }, cw * 0.84, box * 0.56);\n const ink = i % 2 ? $.on(c.accent) : c.fg;\n S.add(S.text(w, { face: face, size: size, depth: 0, color: ink }), { position: [0, 0, -hd - 1.5], parent: { rotate: [-90 * i, 0, 0], parent: cube }, flat: true, decal: true });\n });\n s += M.part(\"cube\", { at: tin, in: \"pop\", out: \"zoom\", box: { x: cx - hw, y: cy - hh, w: cw, h: box } }, S.render());\n if (lead) s += M.text(\"lead\", { at: tin + 0.15, in: \"rise\", distance: 20, out: \"fade\", ...lf, x: stack ? W / 2 : cx - hw - 40 * k, y: stack ? cy - hh - lf.size * 1.6 : cy - lf.size * 0.55, anchor: stack ? \"middle\" : \"end\", fill: c.fg }, lead);\n return s;\n}\n"},{"id":"bars-3d","name":"3D bar chart","description":"Bars rise from a floor one after another while the camera orbits; values count up on their tops and the leader takes the accent.","category":"chart","tags":["3d","chart","bars","data","orbit"],"placement":"full","libs":["3d"],"duration":{"default":6,"min":3,"max":30},"props":{"title":{"type":"text","label":"Title","default":"Videos shipped per week","maxLength":50},"rows":{"type":"list","label":"Rows (Label: value)","default":[{"label":"Jan","value":4},{"label":"Feb","value":6},{"label":"Mar","value":9},{"label":"Apr","value":13},{"label":"May","value":21}],"maxItems":8,"minItems":2,"of":{"label":"text","value":"number"}},"suffix":{"type":"text","label":"After numbers","default":"","maxLength":6}},"parts":{"ground":"the ground","title":"the heading","bars":"the bars and floor","labels":"the names and values"},"beats":{"in":0.1,"out":-0.45},"pack":"3d","ref":"3d/bars-3d","code":"// MONARC 3D — helpers this pack's items share. The scenes are built with scene3d (the 3D kit).\n\n/** The focal length a scene3d camera with this field of view has on this canvas. */\nfunction focalFor($, fov) {\n return ($.H / 2) / Math.tan(((fov || 35) * Math.PI) / 360);\n}\n\n/** Lines of 3D text no wider than maxW (px at the target depth), by words. */\nfunction wrap3d(S, str, o, maxW, maxLines) {\n var words = String(str || \"\").split(/\\s+/).filter(Boolean), lines = [], cur = \"\";\n words.forEach(function (w) {\n var next = cur ? cur + \" \" + w : w;\n if (cur && S.measure(next, o) > maxW) { lines.push(cur); cur = w; } else cur = next;\n });\n if (cur) lines.push(cur);\n if (maxLines && lines.length > maxLines) { var rest = lines.slice(maxLines - 1).join(\" \"); lines = lines.slice(0, maxLines - 1).concat([rest]); }\n return lines;\n}\n\n/** The biggest size (≤ max) at which str fits maxW on one line, in 3D text. */\nfunction fit3d(S, str, o, maxW, max) {\n var w = S.measure(str, Object.assign({}, o, { size: 100 }));\n return w > 0 ? Math.min(max, (maxW / w) * 100) : max;\n}\n\n/** A soft contact shadow on the floor under a point (2D, drawn before the scene). */\nfunction floorShadow($, S, p, rx, op) {\n var q = S.project(p);\n if (!q) return \"\";\n var id = $.uid(\"fs\");\n $.def('<radialGradient id=\"' + id + '\"><stop offset=\"0\" stop-color=\"#000\" stop-opacity=\"' + (op || 0.45) + '\"/><stop offset=\"1\" stop-color=\"#000\" stop-opacity=\"0\"/></radialGradient>');\n return '<ellipse cx=\"' + $.fx(q.x) + '\" cy=\"' + $.fx(q.y) + '\" rx=\"' + $.fx(rx * q.s) + '\" ry=\"' + $.fx(rx * q.s * 0.18) + '\" fill=\"url(#' + id + ')\"/>';\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.1);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const title = $.case(String(p.title || \"\"));\n let top = A.y;\n if (title) {\n const tf = fitLines($, title, { ...$.font(\"display\"), ls: -0.02 }, A.w, Math.min(84 * k, A.h * 0.09), 40 * k, 2);\n tf.lines.forEach(function (l, i) { s += M.text(\"title\", { at: tin + i * 0.1, in: \"rise\", distance: 20, out: \"fade\", ...tf.font, x: A.x, y: A.y + i * tf.size * 1.05, fill: c.fg }, l); });\n top = A.y + tf.lines.length * tf.size * 1.05 + 30 * k;\n }\n const rows = p.rows || [];\n const n = rows.length;\n const maxV = Math.max.apply(null, rows.map(function (r) { return r.value; }).concat([1e-9]));\n const span = A.w * (A.wide ? 0.78 : 0.9);\n const gap = span / n, bw = gap * 0.56;\n const maxH = (A.y + A.h - top) * 0.62;\n const cy = top + (A.y + A.h - top) * 0.52;\n const floorY = maxH / 2;\n const S = scene3d($, { camera: { fov: 32, yaw: -26 + 14 * $.E.inOut2($.seg(t, tin, $.dur)), pitch: -20, y: cy }, light: { spec: 0.22 } });\n // The floor: a thin slab under the bars.\n S.add(S.box(span + gap * 0.4, 6 * k, gap * 1.4, { color: $.mix(c.bg, c.fg, 0.1) }), { position: [0, floorY + 3 * k, 0], rotate: [0, 0, 0] });\n let lab = \"\";\n rows.forEach(function (r, i) {\n const at = tin + 0.4 + i * 0.16;\n const e = $.spring(Math.max(0, t - at), 2.2, 7.5);\n const h = Math.max(2 * k, maxH * (r.value / maxV) * e);\n const x = -span / 2 + gap * (i + 0.5);\n const lead = r.value === maxV;\n S.add(S.box(bw, h, bw, { color: lead ? c.accent : $.mix(c.fg, c.bg, 0.45) }), { position: [x, floorY - h / 2, 0] });\n S.at([x, floorY + 18 * k, -bw / 2], function (px, py, sc) { return $.text(String(r.label), { ...$.font(\"text\"), size: 30 * k, x: px, y: py, anchor: \"middle\", fill: c.mute }); });\n if (t > at) S.at([x, floorY - h - 14 * k, 0], function (px, py, sc) { return $.text(Math.round(r.value * Math.min(1, e)) + (p.suffix || \"\"), { ...$.font(\"display\"), size: 36 * k, x: px, y: py - 40 * k, anchor: \"middle\", fill: lead ? c.accent : c.fg }); }, { onTop: true });\n });\n s += M.part(\"bars\", { at: tin + 0.2, in: \"fade\", dur: 0.3, out: \"fade\", box: { x: A.x, y: top, w: A.w, h: A.y + A.h - top } }, S.render());\n return s + lab;\n}\n"},{"id":"globe","name":"3D globe","description":"A lit globe spins to a country, which lights up in the accent; routes arc in from other countries and a pin names the place.","category":"scene","tags":["3d","globe","map","world","geo","travel"],"placement":"full","libs":["3d","world"],"duration":{"default":6,"min":3,"max":30},"props":{"target":{"type":"text","label":"Country","default":"India","maxLength":40},"label":{"type":"text","label":"Label","default":"Mumbai studio","maxLength":28},"from":{"type":"list","label":"Routes from","default":["United States","Brazil","United Kingdom","Australia"],"maxItems":8,"of":"text"},"title":{"type":"text","label":"Title","default":"","maxLength":50}},"parts":{"ground":"the ground","title":"the heading","globe":"the globe","routes":"the routes","pin":"the pin and label"},"beats":{"in":0.1,"out":-0.45},"pack":"3d","ref":"3d/globe","code":"// MONARC 3D — helpers this pack's items share. The scenes are built with scene3d (the 3D kit).\n\n/** The focal length a scene3d camera with this field of view has on this canvas. */\nfunction focalFor($, fov) {\n return ($.H / 2) / Math.tan(((fov || 35) * Math.PI) / 360);\n}\n\n/** Lines of 3D text no wider than maxW (px at the target depth), by words. */\nfunction wrap3d(S, str, o, maxW, maxLines) {\n var words = String(str || \"\").split(/\\s+/).filter(Boolean), lines = [], cur = \"\";\n words.forEach(function (w) {\n var next = cur ? cur + \" \" + w : w;\n if (cur && S.measure(next, o) > maxW) { lines.push(cur); cur = w; } else cur = next;\n });\n if (cur) lines.push(cur);\n if (maxLines && lines.length > maxLines) { var rest = lines.slice(maxLines - 1).join(\" \"); lines = lines.slice(0, maxLines - 1).concat([rest]); }\n return lines;\n}\n\n/** The biggest size (≤ max) at which str fits maxW on one line, in 3D text. */\nfunction fit3d(S, str, o, maxW, max) {\n var w = S.measure(str, Object.assign({}, o, { size: 100 }));\n return w > 0 ? Math.min(max, (maxW / w) * 100) : max;\n}\n\n/** A soft contact shadow on the floor under a point (2D, drawn before the scene). */\nfunction floorShadow($, S, p, rx, op) {\n var q = S.project(p);\n if (!q) return \"\";\n var id = $.uid(\"fs\");\n $.def('<radialGradient id=\"' + id + '\"><stop offset=\"0\" stop-color=\"#000\" stop-opacity=\"' + (op || 0.45) + '\"/><stop offset=\"1\" stop-color=\"#000\" stop-opacity=\"0\"/></radialGradient>');\n return '<ellipse cx=\"' + $.fx(q.x) + '\" cy=\"' + $.fx(q.y) + '\" rx=\"' + $.fx(rx * q.s) + '\" ry=\"' + $.fx(rx * q.s * 0.18) + '\" fill=\"url(#' + id + ')\"/>';\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.1);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [W / 2, H / 2] }));\n let top = A.y;\n if (p.title) {\n const tf = fitFont($, $.case(String(p.title)), \"display\", 72 * k, A.w);\n s += M.text(\"title\", { at: tin, in: \"rise\", distance: 20, out: \"fade\", ...tf, x: W / 2, y: A.y, anchor: \"middle\", fill: c.fg }, $.case(String(p.title)));\n top = A.y + tf.size * 1.4;\n }\n const target = worldFind(p.target) || worldFind(\"India\");\n const R = Math.min(A.w, A.y + A.h - top) * 0.4;\n const cy = top + (A.y + A.h - top) / 2;\n const S = scene3d($, { camera: { fov: 28, y: cy }, light: { dir: [0.45, 0.35, 0.82], spec: 0.15 } });\n // Spin from well behind to the country, easing in; tilt toward its latitude.\n const arrive = tin + 2.2;\n const g = $.E.inOut3($.seg(t, tin, arrive));\n const spin = -target.c[0] - 150 * (1 - g) - 6 * $.seg(t, arrive, $.dur);\n const tilt = Math.max(-35, Math.min(35, target.c[1])) * g + 12 * (1 - g);\n S.add(S.sphere(R, { color: $.mix(c.bg, c.accent, 0.1) }), { position: [0, 0, 0] });\n const lit = $.seg(t, arrive - 0.3, arrive + 0.3);\n worldGlobe(S, { r: R, spin: spin, tilt: tilt, colorOf: function (cn) { return cn === target ? $.mix($.mix(c.fg, c.bg, 0.55), c.accent, lit) : $.mix(c.fg, c.bg, 0.55); } });\n // Routes: great circles from each country to the target, lifted off the surface as they travel.\n const tp = target.c;\n (p.from || []).forEach(function (name, i) {\n const src = worldFind(name);\n if (!src || src === target) return;\n const at = arrive + 0.1 + i * 0.25;\n const run = $.E.inOut2($.seg(t, at, at + 1));\n if (run <= 0) return;\n const v = function (q) { const l = q[0] * Math.PI / 180, f = q[1] * Math.PI / 180; return [Math.cos(f) * Math.cos(l), Math.cos(f) * Math.sin(l), Math.sin(f)]; };\n const va = v(src.c), vb = v(tp);\n const om = Math.acos(Math.max(-1, Math.min(1, va[0] * vb[0] + va[1] * vb[1] + va[2] * vb[2]))) || 1e-6;\n const pts = [];\n const steps = 40;\n for (let j = 0; j <= Math.round(steps * run); j++) {\n const f = j / steps, s1 = Math.sin((1 - f) * om) / Math.sin(om), s2 = Math.sin(f * om) / Math.sin(om);\n const x = s1 * va[0] + s2 * vb[0], y = s1 * va[1] + s2 * vb[1], z = s1 * va[2] + s2 * vb[2];\n const lon = Math.atan2(y, x) * 180 / Math.PI, lat = Math.asin(Math.max(-1, Math.min(1, z))) * 180 / Math.PI;\n pts.push(worldOnSphere(lon, lat, R * (1.01 + 0.05 * Math.sin(Math.PI * f) * Math.min(1, om)), spin, tilt));\n }\n // Only the stretch on the near side of the globe: a route round the back would cross the picture.\n const near = pts.filter(function (q) { return q[2] < -R * 0.08; });\n if (near.length > 1) S.line(near, { stroke: $.style.mode === \"light\" ? c.accent : c.accent2, width: 4 * k, opacity: 0.9 });\n });\n s += M.part(\"globe\", { at: tin, in: \"zoom\", dur: 0.8, out: \"fade\", box: { x: W / 2 - R, y: cy - R, w: 2 * R, h: 2 * R } }, S.render());\n // The pin: where the country's label point sits on the turned globe.\n if (t > arrive) {\n const q = S.project(worldOnSphere(tp[0], tp[1], R * 1.01, spin, tilt));\n if (q) {\n const lf = fitFont($, $.case(String(p.label || target.name)), \"display\", 40 * k, A.w * 0.6);\n const lw = $.measure($.case(String(p.label || target.name)), lf) + 40 * k, lh = lf.size * 1.6;\n const right = q.x < W / 2 + R * 0.4;\n const bx = right ? q.x + 30 * k : q.x - 30 * k - lw;\n s += M.part(\"pin\", { at: arrive, in: \"pop\", out: \"fade\", box: { x: Math.min(bx, q.x) - 20 * k, y: q.y - lh - 30 * k, w: lw + 60 * k, h: lh + 50 * k } },\n $.line(q.x, q.y, right ? bx : bx + lw, q.y - lh / 2 - 20 * k, { stroke: c.fg, sw: 3 * k }),\n $.circle(q.x, q.y, 10 * k, { fill: c.accent, stroke: c.fg, sw: 3 * k }),\n $.rect(bx, q.y - lh - 20 * k, lw, lh, { r: lh / 2, fill: c.surface, shadow: \"md\" }),\n $.text($.case(String(p.label || target.name)), { ...lf, x: bx + lw / 2, y: q.y - lh - 20 * k + (lh - lf.size) / 2 - lf.size * 0.04, anchor: \"middle\", fill: c.fg }));\n }\n }\n return s;\n}\n"}]},{"format":1,"id":"transitions","name":"MONARC Transitions","version":"1.0.0","description":"Eleven transitions that cover the frame by the cut and reveal the next shot — iris, blinds, doors, glitch, ink, clock, Bauhaus tiles, pixels, stripes, slices and a whip pan — in any brand's colours.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/transitions","style":"cobalt","shared":"shared.js","items":["iris","blinds","doors","glitch","ink","clock","tiles","pixels","stripes","slices","whip"],"entries":[{"id":"iris","name":"Iris","description":"A camera iris: the frame closes down to a point through a ring of the accent, then opens on the next shot. Circle, diamond, star or square, from any point.","category":"transition","tags":["transition","iris","circle","close","open","shape"],"placement":"overlay","duration":{"default":1.2,"min":0.5,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"shape":{"type":"choice","label":"Shape","options":["circle","diamond","star","square"],"default":"circle"},"x":{"type":"number","label":"Centre across (%)","default":50,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Centre down (%)","default":50,"min":0,"max":100,"step":1},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/iris","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const cx = W * p.x / 100, cy = H * p.y / 100;\n const far = Math.hypot(Math.max(cx, W - cx), Math.max(cy, H - cy));\n // How far the shape must open to clear the frame.\n const R = far * (p.shape === \"star\" ? 2.6 : p.shape === \"diamond\" ? 1.45 : p.shape === \"square\" ? 1.02 : 1.02);\n const e = ph.before ? 1 - $.E.inOut3(ph.cover) : $.E.inOut3(ph.uncover);\n if (e < 1) {\n const band = 34 * k * e + (1 - e) * 0;\n s += frameWithHole($, shapePath(p.shape, cx, cy, R * e), col.lead);\n s += frameWithHole($, shapePath(p.shape, cx, cy, R * e + band * 0.5), col.lead2);\n s += frameWithHole($, shapePath(p.shape, cx, cy, R * e + band), col.cover);\n }\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]},{"id":"blinds","name":"Blinds","description":"Venetian blinds: slats swing shut one after another in alternating colours, then open on the next shot.","category":"transition","tags":["transition","blinds","slats","stripes","venetian"],"placement":"overlay","duration":{"default":1.2,"min":0.5,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"slats":{"type":"number","label":"Slats","default":8,"min":3,"max":20,"step":1},"direction":{"type":"choice","label":"Slats run","options":["across","down"],"default":"across"},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/blinds","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const n = Math.max(3, Math.round(p.slats)), across = p.direction === \"across\";\n const L = across ? H : W, band = L / n;\n for (let i = 0; i < n; i++) {\n const lag = (i / (n - 1)) * 0.4;\n const ci = $.E.inOut3($.clamp((ph.cover - lag) / (1 - lag)));\n const ui = $.E.inOut3($.clamp((ph.uncover - lag) / (1 - lag)));\n const f = ph.before ? ci : 1 - ui;\n if (f <= 0) continue;\n const h = band * f + 1;\n // Closing they grow down from the slat's top; opening they shrink to its bottom.\n const a = i * band + (ph.before ? 0 : band - h);\n const fill = i % 2 ? col.lead : col.cover;\n s += across ? $.rect(0, a, W, h, { fill: fill }) : $.rect(a, 0, h, H, { fill: fill });\n }\n // Once shut, the cover colour fills the frame for the word.\n if (Math.abs(t - ph.mid) < 0.06 * $.dur && p.label) s += $.rect(0, 0, W, H, { fill: col.cover });\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]},{"id":"doors","name":"Doors","description":"Two doors slide shut and a Bauhaus disc locks them, then they slide open on the next shot. Across on wide frames, top and bottom on tall ones.","category":"transition","tags":["transition","doors","split","close","open","bauhaus"],"placement":"overlay","duration":{"default":1.4,"min":0.6,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"disc":{"type":"boolean","label":"Disc at the seam","default":true},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/doors","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const tall = H > W * 1.2;\n const L = tall ? H : W, half = L / 2;\n const e = ph.before ? $.E.out3(ph.cover) : 1 - $.E.in3(ph.uncover);\n const off = half * (1 - e);\n const R = Math.min(W, H) * 0.17;\n const doorA = tall ? $.rect(0, -off, W, half + 1, { fill: col.cover }) + $.rect(0, half - off - 10 * k, W, 10 * k, { fill: col.lead }) : $.rect(-off, 0, half + 1, H, { fill: col.cover }) + $.rect(half - off - 10 * k, 0, 10 * k, H, { fill: col.lead });\n const doorB = tall ? $.rect(0, half + off, W, half, { fill: col.cover }) + $.rect(0, half + off, W, 10 * k, { fill: col.lead2 }) : $.rect(half + off, 0, half, H, { fill: col.cover }) + $.rect(half + off, 0, 10 * k, H, { fill: col.lead2 });\n s += doorA + doorB;\n if (p.disc && !p.label) {\n // Each door carries half a disc; shut, they make one.\n const cx = W / 2, cy = H / 2;\n const spin = ph.before ? (1 - e) * -90 : (1 - e) * 90;\n if (tall) {\n s += $.path(\"M\" + (cx - R) + \" \" + (cy - off) + \"A\" + R + \" \" + R + \" 0 0 1 \" + (cx + R) + \" \" + (cy - off) + \"Z\", { fill: col.lead, transform: \"rotate(\" + spin + \" \" + cx + \" \" + (cy - off) + \")\" });\n s += $.path(\"M\" + (cx - R) + \" \" + (cy + off) + \"A\" + R + \" \" + R + \" 0 0 0 \" + (cx + R) + \" \" + (cy + off) + \"Z\", { fill: col.lead2, transform: \"rotate(\" + spin + \" \" + cx + \" \" + (cy + off) + \")\" });\n } else {\n s += $.path(\"M\" + (cx - off) + \" \" + (cy - R) + \"A\" + R + \" \" + R + \" 0 0 0 \" + (cx - off) + \" \" + (cy + R) + \"Z\", { fill: col.lead, transform: \"rotate(\" + spin + \" \" + (cx - off) + \" \" + cy + \")\" });\n s += $.path(\"M\" + (cx + off) + \" \" + (cy - R) + \"A\" + R + \" \" + R + \" 0 0 1 \" + (cx + off) + \" \" + (cy + R) + \"Z\", { fill: col.lead2, transform: \"rotate(\" + spin + \" \" + (cx + off) + \" \" + cy + \")\" });\n }\n }\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]},{"id":"glitch","name":"Glitch","description":"A digital glitch: torn slices and RGB-split blocks flicker in, the frame breaks fully at the cut, then glitches out to the next shot.","category":"transition","tags":["transition","glitch","digital","rgb","noise","tech"],"placement":"overlay","duration":{"default":1,"min":0.4,"max":3},"aspects":["16:9","9:16","1:1","4:5"],"props":{"intensity":{"type":"number","label":"Intensity","default":1,"min":0.3,"max":1.5,"step":0.1},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/glitch","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const fr = Math.floor(t * 24);\n // Strength rises to full at the cut and falls after it.\n const g = Math.min(1, (ph.before ? $.E.in2(ph.cover) : 1 - $.E.out2(ph.uncover)) * p.intensity * 1.15);\n const near = Math.abs(t - ph.mid) < Math.max(0.05, $.dur * 0.07);\n const pal = [col.lead, col.lead2, col.cover, c.fg, \"#00F0FF\", \"#FF2E88\"];\n if (near) s += $.rect(0, 0, W, H, { fill: col.cover });\n const n = 22;\n for (let i = 0; i < n; i++) {\n const h1 = $.hash(i * 7.3 + fr * 1.91), h2 = $.hash(i * 3.1 + fr * 0.77), h3 = $.hash(i * 11.7 + fr * 2.3);\n if (h1 > g) continue;\n const y = h2 * H, hh = (0.015 + 0.12 * h3 * h3) * H;\n const x = (h3 - 0.5) * W * 0.5 * g, w = W * (0.35 + 0.65 * $.hash(i + fr * 5.1));\n s += $.rect(x + (h1 > 0.5 ? W - w : 0) * (1 - g), y, w, hh, { fill: pal[Math.floor(h2 * 997) % pal.length] });\n }\n // Pixel blocks.\n for (let j = 0; j < 14; j++) {\n const b1 = $.hash(j * 5.7 + fr * 3.3);\n if (b1 > g * 0.8) continue;\n const sz = (20 + 60 * $.hash(j + fr)) * k;\n s += $.rect($.hash(j * 2.2 + fr) * W, $.hash(j * 9.9 + fr) * H, sz * 2.2, sz, { fill: pal[(j + fr) % pal.length] });\n }\n // Scanlines over it all while it is breaking.\n if (g > 0.2) {\n const id = $.uid(\"sl\");\n $.def('<pattern id=\"' + id + '\" width=\"8\" height=\"' + 6 * k + '\" patternUnits=\"userSpaceOnUse\"><rect width=\"8\" height=\"' + 2 * k + '\" fill=\"rgba(0,0,0,0.22)\"/></pattern>');\n s += $.rect(0, 0, W, H, { fill: \"url(#\" + id + \")\", op: g });\n }\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n if (p.label) {\n // The word with an RGB split that jitters.\n const word = cutWord(M, $, t, p.label, col.cover);\n const j = (($.hash(fr * 1.3) - 0.5) * 18) * k;\n s += word ? '<g opacity=\"0.75\" transform=\"translate(' + j + ' 0)\" style=\"mix-blend-mode:screen\">' + word.replace(/fill=\"[^\"]*\"/g, 'fill=\"#00F0FF\"') + \"</g>\" + '<g opacity=\"0.75\" transform=\"translate(' + -j + ' 0)\">' + word.replace(/fill=\"[^\"]*\"/g, 'fill=\"#FF2E88\"') + \"</g>\" + word : \"\";\n }\n return s;\n}\n\n/** A glitch cuts on static: zaps into the cut, on it, and one more as it clears. */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.3), kind: \"zap\", level: 0.8 },\n { at: mid, kind: \"zap\", level: 1.1, pitch: 4 },\n { at: mid, kind: \"sub\", level: 0.4 },\n { at: Math.min($.dur - 0.1, mid + 0.14), kind: \"zap\", level: 0.6, pitch: -3 },\n ],\n auto: true,\n };\n}\n","libs":[]},{"id":"ink","name":"Ink bleed","description":"Ink spreads from a point in wobbly blooms of the accents until it floods the frame, then a hole blooms open on the next shot.","category":"transition","tags":["transition","ink","paint","blob","organic","liquid","splash"],"placement":"overlay","duration":{"default":1.6,"min":0.6,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"x":{"type":"number","label":"From across (%)","default":30,"min":0,"max":100,"step":1},"y":{"type":"number","label":"From down (%)","default":60,"min":0,"max":100,"step":1},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/ink","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const cx = W * p.x / 100, cy = H * p.y / 100;\n const far = Math.hypot(Math.max(cx, W - cx), Math.max(cy, H - cy)) * 1.25;\n if (ph.before) {\n const lags = [0, 0.12, 0.24], fills = [col.lead, col.lead2, col.cover];\n lags.forEach(function (lag, i) {\n const e = $.E.inOut3($.clamp((ph.cover - lag) / (1 - lag)));\n if (e > 0) s += $.path(blobPath(cx, cy, far * e, 1.7 + i * 2.1, t), { fill: fills[i] });\n });\n } else {\n // The hole opens from the frame's centre, the cover first, the accents trailing as rims.\n const lags = [0.2, 0.1, 0], fills = [col.lead, col.lead2, col.cover];\n const hx = W / 2, hy = H / 2, far2 = Math.hypot(W, H) * 0.7;\n lags.forEach(function (lag, i) {\n const e = $.E.inOut3($.clamp((ph.uncover - lag) / (1 - lag)));\n if (e < 1) s += frameWithHole($, e > 0 ? blobPath(hx, hy, far2 * e, 4.2 + i, t) : \"\", fills[i]);\n });\n }\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]},{"id":"clock","name":"Clock wipe","description":"A clock-hand sweep: the next colour wipes round the frame like a second hand with an accent edge, then sweeps on round to reveal the next shot.","category":"transition","tags":["transition","clock","radial","sweep","pie","wipe"],"placement":"overlay","duration":{"default":1.2,"min":0.5,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"direction":{"type":"choice","label":"Turns","options":["clockwise","anticlockwise"],"default":"clockwise"},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/clock","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const cx = W / 2, cy = H / 2, R = Math.hypot(W, H);\n const dirn = p.direction === \"anticlockwise\" ? -1 : 1;\n const pt = function (deg) { const a = (deg - 90) * Math.PI / 180; return [cx + R * Math.cos(a) * 1, cy + R * Math.sin(a)]; };\n // A wedge from angle a to angle b (degrees from 12 o'clock, in the turning direction).\n const wedge = function (a, b, fill) {\n if (b - a <= 0.2) return \"\";\n if (b - a >= 359.8) return $.rect(0, 0, W, H, { fill: fill });\n const steps = Math.ceil((b - a) / 30), d = [\"M\" + cx + \" \" + cy];\n for (let i = 0; i <= steps; i++) { const q = pt(dirn * (a + ((b - a) * i) / steps)); d.push(\"L\" + q[0] + \" \" + q[1]); }\n return $.path(d.join(\"\") + \"Z\", { fill: fill });\n };\n if (ph.before) {\n const e = $.E.inOut2(ph.cover) * 360;\n s += wedge(0, e, col.cover) + wedge(Math.max(0, e - 14), e, col.lead);\n } else {\n const e = $.E.inOut2(ph.uncover) * 360;\n s += wedge(e, 360, col.cover) + wedge(e, Math.min(360, e + 14), col.lead2);\n }\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]},{"id":"tiles","name":"Bauhaus tiles","description":"A grid of Bauhaus shapes — discs, quarter circles, squares — pops in tile by tile in a diagonal wave to cover the frame, then shrinks away.","category":"transition","tags":["transition","tiles","bauhaus","shapes","grid","geometric","mosaic"],"placement":"overlay","duration":{"default":1.4,"min":0.6,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"size":{"type":"number","label":"Tile size","default":4,"min":2,"max":9,"step":1,"description":"tiles across the short side"},"order":{"type":"choice","label":"Order","options":["diagonal","random","center"],"default":"diagonal"},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/tiles","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const n = Math.max(2, Math.round(p.size)), cell = Math.min(W, H) / n;\n const cols = Math.ceil(W / cell), rows = Math.ceil(H / cell);\n const ox = (W - cols * cell) / 2, oy = (H - rows * cell) / 2;\n const pal = [col.cover, col.lead, col.lead2, col.cover];\n for (let r = 0; r < rows; r++) for (let q = 0; q < cols; q++) {\n const h = $.hash(r * 17.3 + q * 5.1 + 0.4);\n const rank = p.order === \"random\" ? h : p.order === \"center\" ? Math.hypot(q - (cols - 1) / 2, r - (rows - 1) / 2) / Math.hypot(cols / 2, rows / 2) : (q + r) / (cols + rows - 2);\n const lag = rank * 0.5;\n const e = ph.before ? $.E.outBack($.clamp((ph.cover - lag) / 0.5)) : 1 - $.E.in3($.clamp((ph.uncover - lag) / 0.5));\n if (e <= 0) continue;\n const x = ox + q * cell, y = oy + r * cell, cx = x + cell / 2, cy = y + cell / 2;\n const fill = pal[Math.floor(h * 4) % 4], kind = Math.floor($.hash(r * 3.3 + q * 9.7) * 3);\n let shape;\n if (kind === 0) shape = $.circle(cx, cy, cell * 0.72 * e, { fill: fill });\n else if (kind === 1) { const rr = cell * 1.42 * e, rot = Math.floor(h * 4) * 90; shape = $.g({ transform: \"rotate(\" + rot + \" \" + cx + \" \" + cy + \")\" }, $.path(\"M\" + (cx - cell / 2) + \" \" + (cy - cell / 2) + \"h\" + rr + \"A\" + rr + \" \" + rr + \" 0 0 1 \" + (cx - cell / 2) + \" \" + (cy - cell / 2 + rr) + \"Z\", { fill: fill })); }\n else shape = $.rect(cx - cell * 0.51 * e, cy - cell * 0.51 * e, cell * 1.02 * e, cell * 1.02 * e, { fill: fill });\n s += $.clip(x - 0.5, y - 0.5, cell + 1, cell + 1, 0, shape);\n }\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n if (p.label) {\n const str = $.case(p.label), f = fitFont($, str, \"display\", 120 * k, Math.min(W, H * 1.4) * 0.66, { ls: -0.03 });\n const w = $.measure(str, f) + 80 * k, hh = f.size * 1.5;\n const a = Math.max(0, ph.mid - Math.min(0.3, ph.mid * 0.5));\n s += M.part(\"label\", { at: a, in: \"pop\", outAt: ph.mid + ($.dur - ph.mid) * 0.2, out: \"pop\", outDur: 0.2, box: { x: W / 2 - w / 2, y: H / 2 - hh / 2, w: w, h: hh } },\n $.rect(W / 2 - w / 2, H / 2 - hh / 2, w, hh, { r: hh / 2, fill: col.cover }) + $.text(str, Object.assign({}, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(col.cover) })));\n }\n return s;\n}\n","libs":[]},{"id":"pixels","name":"Pixel dissolve","description":"The frame dissolves into square pixels in random order — a few in the accents — until it is covered, then dissolves away to the next shot.","category":"transition","tags":["transition","pixels","dissolve","mosaic","retro","digital"],"placement":"overlay","duration":{"default":1.2,"min":0.5,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"size":{"type":"number","label":"Pixel size","default":48,"min":16,"max":160,"step":4},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/pixels","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const px = Math.max(12, p.size * k), cols = Math.ceil(W / px), rows = Math.ceil(H / px);\n const cover = ph.before ? $.E.inOut2(ph.cover) : 1 - $.E.inOut2(ph.uncover);\n let d = \"\", dl = \"\", dl2 = \"\";\n for (let r = 0; r < rows; r++) for (let q = 0; q < cols; q++) {\n const h = $.hash(r * 31.7 + q * 7.9 + (ph.before ? 0 : 3.3));\n if (h >= cover) continue;\n const cell = \"M\" + q * px + \" \" + r * px + \"h\" + (px + 0.5) + \"v\" + (px + 0.5) + \"h\" + -(px + 0.5) + \"z\";\n // The last pixels to land are the accents.\n if (cover < 0.98 && h > cover - 0.08) { if ((q + r) % 2) dl += cell; else dl2 += cell; } else d += cell;\n }\n s += (d ? $.path(d, { fill: col.cover }) : \"\") + (dl ? $.path(dl, { fill: col.lead }) : \"\") + (dl2 ? $.path(dl2, { fill: col.lead2 }) : \"\");\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]},{"id":"stripes","name":"Speed stripes","description":"Diagonal stripes in the brand's colours race across in a stagger to cover the frame, then race off to reveal the next shot.","category":"transition","tags":["transition","stripes","diagonal","speed","sport","swipe"],"placement":"overlay","duration":{"default":1.1,"min":0.5,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"stripes":{"type":"number","label":"Stripes","default":7,"min":3,"max":16,"step":1},"direction":{"type":"choice","label":"Direction","options":["right","left"],"default":"right"},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/stripes","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const n = Math.max(3, Math.round(p.stripes)), D = Math.hypot(W, H), band = D / n + 2;\n const ang = -24, sign = p.direction === \"left\" ? -1 : 1;\n const pal = [col.lead, col.lead2, col.cover];\n let g = \"\";\n for (let i = 0; i < n; i++) {\n const lag = (i / (n - 1)) * 0.35;\n const ci = $.E.inOut3($.clamp((ph.cover - lag) / (1 - lag))), ui = $.E.inOut3($.clamp((ph.uncover - lag) / (1 - lag)));\n // Each stripe slides in from one side, then out the other.\n const x0 = ph.before ? -D + D * ci : D * ui;\n if (ph.before ? ci <= 0 : ui >= 1) continue;\n g += $.rect(x0 - D / 2, -D / 2 + i * (D / n), D * 1.02, band, { fill: i === n - 1 || i % 3 === 2 ? col.cover : pal[i % 3] });\n }\n if (ph.before && ph.cover > 0.98) g += $.rect(-D, -D, D * 2, D * 2, { fill: col.cover });\n s += '<g transform=\"translate(' + W / 2 + \" \" + H / 2 + \") scale(\" + sign + \" 1) rotate(\" + ang + ')\">' + g + \"</g>\";\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]},{"id":"slices","name":"Slices","description":"The frame is sliced into bands that shoot in from alternate sides, each with an accent edge, then shoot out the far side to reveal the next shot.","category":"transition","tags":["transition","slices","bands","slide","split"],"placement":"overlay","duration":{"default":1.2,"min":0.5,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"slices":{"type":"number","label":"Slices","default":5,"min":2,"max":12,"step":1},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/slices","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const n = Math.max(2, Math.round(p.slices)), band = H / n, edge = 12 * k;\n for (let i = 0; i < n; i++) {\n const lag = $.hash(i * 4.4 + 1) * 0.3, dir = i % 2 ? -1 : 1;\n const ci = $.E.out3($.clamp((ph.cover - lag) / (1 - lag))), ui = $.E.in3($.clamp((ph.uncover - lag) / (1 - lag)));\n const x = ph.before ? -dir * W * (1 - ci) : dir * W * ui;\n if (ph.before ? ci <= 0 : ui >= 1) continue;\n s += $.rect(x, i * band - 0.5, W, band + 1, { fill: col.cover });\n // The leading edge in an accent.\n const ex = dir > 0 ? (ph.before ? x + W - edge : x) : (ph.before ? x : x + W - edge);\n s += $.rect(ex, i * band - 0.5, edge, band + 1, { fill: i % 2 ? col.lead2 : col.lead });\n }\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]},{"id":"whip","name":"Whip pan","description":"A whip pan: speed streaks blur across, thicken into a smear of the brand's colours at the cut, then streak away the same way onto the next shot.","category":"transition","tags":["transition","whip","pan","speed","blur","streak","swish"],"placement":"overlay","duration":{"default":0.8,"min":0.3,"max":3},"aspects":["16:9","9:16","1:1","4:5"],"props":{"direction":{"type":"choice","label":"Direction","options":["left","right","up","down"],"default":"left"},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/whip","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const horiz = p.direction === \"left\" || p.direction === \"right\";\n const L = horiz ? W : H, A2 = horiz ? H : W;\n const g = ph.before ? $.E.in3(ph.cover) : 1 - $.E.out3(ph.uncover);\n const n = 46;\n let lines = \"\";\n for (let i = 0; i < n; i++) {\n const h1 = $.hash(i * 3.7), h2 = $.hash(i * 9.1 + 2), h3 = $.hash(i * 5.3 + 7);\n if (h3 > g * 1.3) continue;\n const pos = h1 * A2, th = (4 + 40 * h2 * h2) * k * (0.4 + g);\n const len = L * (0.3 + 0.9 * h2) * (0.4 + g), travel = (t * (2.2 + h3 * 2) + h1) % 1;\n const a = -len + (L + len) * travel;\n const fill = i % 5 === 0 ? col.lead : i % 5 === 1 ? col.lead2 : col.cover;\n lines += horiz ? $.rect(a, pos, len, th, { fill: fill, r: th / 2 }) : $.rect(pos, a, th, len, { fill: fill, r: th / 2 });\n }\n // The smear: the cover colour washing in at the cut.\n const wash = $.clamp((g - 0.55) / 0.35);\n if (wash > 0) lines = $.rect(0, 0, W, H, { fill: col.cover, op: wash }) + lines;\n const flip = p.direction === \"left\" ? \"translate(\" + W + \" 0) scale(-1 1)\" : p.direction === \"up\" ? \"translate(0 \" + H + \") scale(1 -1)\" : \"\";\n s += flip ? '<g transform=\"' + flip + '\">' + lines + \"</g>\" : lines;\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]}]},{"format":1,"id":"annotate","name":"MONARC Annotate","version":"1.0.0","description":"Point things out in footage: hand-drawn arrows and circles, a spotlight, focus brackets, highlighter marks, a rubber stamp, live reactions, a tech pointer, emphasis marks and numbered markers.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/annotate","style":"candy","shared":"shared.js","items":["arrow","circle","spotlight","focus","highlight","stamp","reactions","pointer","emphasis","markers"],"entries":[{"id":"arrow","name":"Arrow","description":"A hand-drawn (or clean) arrow sweeps from a label to the thing you point at, with a pulse on the spot.","category":"callout","tags":["annotation","arrow","point","look","here","hand-drawn"],"placement":"overlay","duration":{"default":3,"min":1.5,"max":12},"aspects":["16:9","9:16","1:1","4:5"],"props":{"label":{"type":"text","label":"Label","default":"Look here","maxLength":32},"x":{"type":"number","label":"Point across (%)","default":62,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Point down (%)","default":45,"min":0,"max":100,"step":1},"side":{"type":"choice","label":"Label side","options":["auto","left","right","above","below"],"default":"auto"},"look":{"type":"choice","label":"Look","options":["marker","clean"],"default":"marker"},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"label":"the label","arrow":"the arrow","spot":"the pulse on the spot"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/arrow","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const tx = W * p.x / 100, ty = H * p.y / 100;\n const side = p.side === \"auto\" ? roomy($, tx, ty) : p.side;\n const v = { left: [-1, 0], right: [1, 0], above: [0, -1], below: [0, 1] }[side] || [1, 0];\n const gap = Math.min(W, H) * 0.34;\n const tg = tagAt($, p.label, tx + v[0] * gap * 1.3, ty + v[1] * gap - (v[0] ? gap * 0.35 : 0), col, 44);\n const b = tg.box, lcx = b.x + b.w / 2, lcy = b.y + b.h / 2;\n // From the label's edge facing the spot, to just short of the spot.\n const sx = v[0] ? (v[0] > 0 ? b.x - 14 * k : b.x + b.w + 14 * k) : lcx, sy = v[1] ? (v[1] > 0 ? b.y - 14 * k : b.y + b.h + 14 * k) : lcy;\n const d = Math.hypot(tx - sx, ty - sy) || 1, ex = tx - ((tx - sx) / d) * 26 * k, ey = ty - ((ty - sy) / d) * 26 * k;\n const ta = tin + 0.25;\n let arrow;\n if (p.look === \"clean\") {\n const bend = 0.25, mx = (sx + ex) / 2 - (ey - sy) * bend, my = (sy + ey) / 2 + (ex - sx) * bend;\n const pr = $.E.inOut2($.seg(t, ta, ta + 0.55));\n const ang = Math.atan2(ey - my, ex - mx), hl = 26 * k;\n arrow = $.path(\"M\" + sx + \" \" + sy + \"Q\" + mx + \" \" + my + \" \" + ex + \" \" + ey, { stroke: col, sw: 7 * k, draw: pr });\n if (pr > 0.95) arrow += $.path(\"M\" + (ex - hl * Math.cos(ang - 0.5)) + \" \" + (ey - hl * Math.sin(ang - 0.5)) + \"L\" + ex + \" \" + ey + \"L\" + (ex - hl * Math.cos(ang + 0.5)) + \" \" + (ey - hl * Math.sin(ang + 0.5)), { stroke: col, sw: 7 * k });\n } else arrow = $.ink(t, ta, $.hand.arrow(sx, sy, ex, ey, { bend: 0.22, seed: 3, rough: 3 * k }), { color: col, sw: 9 * k, dur: 0.55 });\n s += M.part(\"arrow\", { at: ta, in: \"none\" }, arrow);\n s += M.part(\"spot\", { at: ta + 0.55, in: \"pop\", box: { x: tx - 20 * k, y: ty - 20 * k, w: 40 * k, h: 40 * k } }, $.circle(tx, ty, 9 * k, { fill: col }) + pulse($, t, ta + 0.55, tx, ty, 12 * k, col));\n s += M.part(\"label\", { at: tin, in: \"pop\", box: b }, tg.markup);\n return s;\n}\n","libs":[]},{"id":"circle","name":"Circle it","description":"A marker loop drawn round the thing that matters — once, twice, or a clean ring — with an optional label.","category":"callout","tags":["annotation","circle","loop","ring","marker","hand-drawn"],"placement":"overlay","duration":{"default":3,"min":1.5,"max":12},"aspects":["16:9","9:16","1:1","4:5"],"props":{"x":{"type":"number","label":"Box left (%)","default":40,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Box top (%)","default":30,"min":0,"max":100,"step":1},"w":{"type":"number","label":"Box width (%)","default":22,"min":2,"max":100,"step":1},"h":{"type":"number","label":"Box height (%)","default":30,"min":2,"max":100,"step":1},"look":{"type":"choice","label":"Look","options":["marker","double","clean"],"default":"marker"},"label":{"type":"text","label":"Label","default":"","maxLength":28,"description":"a tag by the loop (empty for none)"},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"loop":"the loop","label":"the label"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/circle","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const b = boxOf($, p.x, p.y, p.w, p.h);\n const cx = b.x + b.w / 2, cy = b.y + b.h / 2, rx = b.w * 0.62 + 14 * k, ry = b.h * 0.62 + 14 * k;\n let loop;\n if (p.look === \"clean\") {\n const d = \"M\" + (cx + rx) + \" \" + cy + \"A\" + rx + \" \" + ry + \" 0 1 1 \" + (cx - rx) + \" \" + cy + \"A\" + rx + \" \" + ry + \" 0 1 1 \" + (cx + rx) + \" \" + cy;\n loop = $.path(d, { stroke: col, sw: 7 * k, draw: $.E.inOut2($.seg(t, tin, tin + 0.6)), transform: \"rotate(-90 \" + cx + \" \" + cy + \")\" });\n } else {\n loop = $.ink(t, tin, $.hand.circle(cx, cy, rx, ry, { turns: 1.12, seed: 2, rough: 4 * k }), { color: col, sw: 9 * k, dur: 0.6 });\n if (p.look === \"double\") loop += $.ink(t, tin + 0.35, $.hand.circle(cx, cy, rx * 1.06, ry * 0.96, { turns: 1.05, seed: 9, rough: 5 * k }), { color: col, sw: 6 * k, dur: 0.5 });\n }\n s += M.part(\"loop\", { at: tin, in: \"none\" }, loop);\n if (p.label) {\n const tg = tagAt($, p.label, cx + rx * 0.75, cy - ry - 34 * k, col, 36);\n s += M.part(\"label\", { at: tin + 0.5, in: \"pop\", box: tg.box }, tg.markup);\n }\n return s;\n}\n","libs":[]},{"id":"spotlight","name":"Spotlight","description":"Everything but one area dims; a feathered spotlight closes onto it with an accent ring, and a label sits by it.","category":"callout","tags":["annotation","spotlight","focus","dim","highlight","isolate"],"placement":"overlay","duration":{"default":4,"min":1.5,"max":15},"aspects":["16:9","9:16","1:1","4:5"],"props":{"x":{"type":"number","label":"Box left (%)","default":38,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Box top (%)","default":28,"min":0,"max":100,"step":1},"w":{"type":"number","label":"Box width (%)","default":24,"min":2,"max":100,"step":1},"h":{"type":"number","label":"Box height (%)","default":34,"min":2,"max":100,"step":1},"shape":{"type":"choice","label":"Shape","options":["circle","box"],"default":"circle"},"dim":{"type":"number","label":"Dim (%)","default":70,"min":10,"max":95,"step":5},"label":{"type":"text","label":"Label","default":"This part","maxLength":28,"description":"empty for none"},"note":{"type":"text","label":"Note","default":"","maxLength":60,"description":"a line under the label"},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"dim":"the dimmed frame","ring":"the ring","label":"the label and note"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/spotlight","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const b = boxOf($, p.x, p.y, p.w, p.h);\n const cx = b.x + b.w / 2, cy = b.y + b.h / 2;\n const e = $.E.inOut3($.seg(t, tin, tin + 0.9));\n const circ = p.shape !== \"box\";\n const rx1 = circ ? b.w * 0.62 + 18 * k : b.w / 2 + 18 * k, ry1 = circ ? b.h * 0.62 + 18 * k : b.h / 2 + 18 * k;\n const rx = $.lerp(Math.hypot(W, H), rx1, e), ry = $.lerp(Math.hypot(W, H), ry1, e);\n const rr = circ ? 0 : Math.min(rx, ry, 28 * k);\n const hole = circ\n ? \"M\" + (cx + rx) + \" \" + cy + \"A\" + rx + \" \" + ry + \" 0 1 0 \" + (cx - rx) + \" \" + cy + \"A\" + rx + \" \" + ry + \" 0 1 0 \" + (cx + rx) + \" \" + cy + \"Z\"\n : \"M\" + (cx - rx + rr) + \" \" + (cy - ry) + \"H\" + (cx + rx - rr) + \"Q\" + (cx + rx) + \" \" + (cy - ry) + \" \" + (cx + rx) + \" \" + (cy - ry + rr) + \"V\" + (cy + ry - rr) + \"Q\" + (cx + rx) + \" \" + (cy + ry) + \" \" + (cx + rx - rr) + \" \" + (cy + ry) + \"H\" + (cx - rx + rr) + \"Q\" + (cx - rx) + \" \" + (cy + ry) + \" \" + (cx - rx) + \" \" + (cy + ry - rr) + \"V\" + (cy - ry + rr) + \"Q\" + (cx - rx) + \" \" + (cy - ry) + \" \" + (cx - rx + rr) + \" \" + (cy - ry) + \"Z\";\n const fid = $.uid(\"fb\"), m = 80 * k;\n $.def('<filter id=\"' + fid + '\" x=\"-5%\" y=\"-5%\" width=\"110%\" height=\"110%\"><feGaussianBlur stdDeviation=\"' + 16 * k + '\"/></filter>');\n s += M.part(\"dim\", { at: tin, in: \"fade\", dur: 0.4, vary: false }, $.path(\"M\" + -m + \" \" + -m + \"H\" + (W + m) + \"V\" + (H + m) + \"H\" + -m + \"Z\" + hole, { fill: \"rgba(0,0,0,\" + p.dim / 100 + \")\", extra: 'fill-rule=\"evenodd\" filter=\"url(#' + fid + ')\"' }));\n const ringP = $.E.inOut2($.seg(t, tin + 0.8, tin + 1.3));\n if (ringP > 0) s += M.part(\"ring\", { at: tin + 0.8, in: \"none\" }, $.path(hole, { stroke: col, sw: 5 * k, draw: ringP }));\n if (p.label) {\n const below = cy + ry1 + 150 * k < H;\n const ly = below ? cy + ry1 + 30 * k : cy - ry1 - 30 * k - 56 * k;\n const tg = tagAt($, p.label, cx, ly + 28 * k, col, 40);\n let lab = tg.markup;\n if (p.note) {\n const nf = Object.assign({}, $.font(\"text\"), { size: 32 * k });\n const nw = Math.min(W * 0.8, $.measure(p.note, nf));\n const nx = Math.min(W - 30 * k - nw / 2, Math.max(30 * k + nw / 2, cx));\n lab += $.text(p.note, Object.assign({}, nf, { x: nx, y: below ? tg.box.y + tg.box.h + 16 * k : tg.box.y - 16 * k - 40 * k, anchor: \"middle\", fill: \"#FFFFFF\", maxW: W * 0.8 }));\n }\n s += M.part(\"label\", { at: tin + 1.0, in: \"rise\", distance: 16, box: tg.box }, lab);\n }\n return s;\n}\n","libs":[]},{"id":"focus","name":"Focus lock","description":"Camera focus brackets hunt in from the frame's corners, snap onto the subject with a blink, and lock in the accent with a label and crosshair.","category":"callout","tags":["annotation","focus","brackets","camera","lock","target","track"],"placement":"overlay","duration":{"default":3,"min":1.5,"max":12},"aspects":["16:9","9:16","1:1","4:5"],"props":{"x":{"type":"number","label":"Box left (%)","default":36,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Box top (%)","default":26,"min":0,"max":100,"step":1},"w":{"type":"number","label":"Box width (%)","default":28,"min":2,"max":100,"step":1},"h":{"type":"number","label":"Box height (%)","default":40,"min":2,"max":100,"step":1},"label":{"type":"text","label":"Label","default":"Subject","maxLength":28,"description":"empty for none"},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"brackets":"the corner brackets","cross":"the crosshair","label":"the label"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/focus","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const b = boxOf($, p.x, p.y, p.w, p.h), m = Math.min(W, H) * 0.08;\n const q = $.clamp($.spring(Math.max(0, t - tin), 2.2, 6.5), 0, 1.15);\n const bx = $.lerp(m, b.x, q), by = $.lerp(m, b.y, q), bw = $.lerp(W - 2 * m, b.w, q), bh = $.lerp(H - 2 * m, b.h, q);\n const lock = tin + 0.7, locked = t >= lock;\n const blink = locked && t < lock + 0.3 && Math.floor((t - lock) * 14) % 2 === 1;\n const stroke = locked ? col : \"#FFFFFF\", L = Math.min(bw, bh) * 0.22, sw = 6 * k;\n const corner = function (x, y, dx, dy) { return $.path(\"M\" + x + \" \" + (y + dy * L) + \"V\" + y + \"H\" + (x + dx * L), { stroke: stroke, sw: sw, cap: \"square\", join: \"miter\" }); };\n const br = corner(bx, by, 1, 1) + corner(bx + bw, by, -1, 1) + corner(bx, by + bh, 1, -1) + corner(bx + bw, by + bh, -1, -1);\n s += M.part(\"brackets\", { at: tin, in: \"fade\", dur: 0.2 }, blink ? \"\" : br);\n const cx = bx + bw / 2, cy = by + bh / 2, cr = 18 * k;\n s += M.part(\"cross\", { at: lock, in: \"pop\", box: { x: cx - cr, y: cy - cr, w: 2 * cr, h: 2 * cr } }, $.path(\"M\" + (cx - cr) + \" \" + cy + \"H\" + (cx - 6 * k) + \"M\" + (cx + 6 * k) + \" \" + cy + \"H\" + (cx + cr) + \"M\" + cx + \" \" + (cy - cr) + \"V\" + (cy - 6 * k) + \"M\" + cx + \" \" + (cy + 6 * k) + \"V\" + (cy + cr), { stroke: col, sw: 3 * k }));\n if (p.label) {\n const txt = $.case(p.label), f = Object.assign({}, $.font(\"mono\"), { size: 30 * k });\n const w = $.measure(txt, f) + 28 * k, h = 48 * k;\n const lx = Math.min(W - w - 20 * k, Math.max(20 * k, b.x)), ly = b.y - h - 14 * k > 20 * k ? b.y - h - 14 * k : b.y + b.h + 14 * k;\n s += M.part(\"label\", { at: lock + 0.1, in: \"wipe\", from: \"left\", box: { x: lx, y: ly, w: w, h: h } }, $.rect(lx, ly, w, h, { fill: col }) + $.text(txt, Object.assign({}, f, { x: lx + 14 * k, y: ly + (h - f.size) / 2 - 2 * k, fill: $.on(col) })));\n }\n return s;\n}\n","libs":[]},{"id":"highlight","name":"Highlight","description":"A highlighter swipes across a word or area — or a marker underline, box or strike-through drawn on by hand.","category":"callout","tags":["annotation","highlight","marker","underline","strike","emphasis"],"placement":"overlay","duration":{"default":2.5,"min":1,"max":12},"aspects":["16:9","9:16","1:1","4:5"],"props":{"x":{"type":"number","label":"Box left (%)","default":30,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Box top (%)","default":44,"min":0,"max":100,"step":1},"w":{"type":"number","label":"Box width (%)","default":40,"min":2,"max":100,"step":1},"h":{"type":"number","label":"Box height (%)","default":10,"min":1,"max":100,"step":1},"mode":{"type":"choice","label":"Mark","options":["highlight","underline","box","strike"],"default":"highlight"},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"mark":"the mark"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/highlight","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const b = boxOf($, p.x, p.y, p.w, p.h);\n // A highlighter is yellow unless told otherwise; pens are the accent.\n const hi = p.color || c.accent2;\n let mark;\n if (p.mode === \"highlight\") {\n // A marker band with ragged ends, swiped on left to right.\n const j = function (i) { return ($.hash(i * 3.7) - 0.5) * 10 * k; };\n const d = \"M\" + (b.x - 10 * k + j(1)) + \" \" + (b.y + j(2)) + \"L\" + (b.x + b.w + 8 * k + j(3)) + \" \" + (b.y - 4 * k + j(4)) + \"L\" + (b.x + b.w + 14 * k + j(5)) + \" \" + (b.y + b.h + j(6)) + \"L\" + (b.x - 4 * k + j(7)) + \" \" + (b.y + b.h + 4 * k + j(8)) + \"Z\";\n mark = M.part(\"mark\", { at: tin, in: \"wipe\", from: \"left\", dur: 0.45, box: { x: b.x - 20 * k, y: b.y - 12 * k, w: b.w + 40 * k, h: b.h + 24 * k } }, $.path(d, { fill: $.rgba(hi, 0.5) }));\n } else {\n const shape = p.mode === \"box\" ? $.hand.rect(b.x - 12 * k, b.y - 10 * k, b.w + 24 * k, b.h + 20 * k, { seed: 4, rough: 4 * k })\n : p.mode === \"strike\" ? $.hand.line(b.x - 10 * k, b.y + b.h * 0.55, b.x + b.w + 10 * k, b.y + b.h * 0.45, { seed: 5, rough: 3 * k })\n : $.hand.curve(b.x - 6 * k, b.y + b.h + 12 * k, b.x + b.w + 6 * k, b.y + b.h + 6 * k, { bend: 0.04, seed: 6, rough: 3 * k });\n mark = M.part(\"mark\", { at: tin, in: \"none\" }, $.ink(t, tin, shape, { color: col, sw: (p.mode === \"box\" ? 7 : 12) * k, dur: 0.5 }));\n }\n return s + mark;\n}\n","libs":[]},{"id":"stamp","name":"Stamp","description":"A rubber stamp slams down at an angle — APPROVED, SOLD OUT, NEW — with a rough inked double border and a jolt.","category":"callout","tags":["annotation","stamp","approved","sold","rubber","badge","seal"],"placement":"overlay","duration":{"default":2.5,"min":1,"max":12},"aspects":["16:9","9:16","1:1","4:5"],"props":{"text":{"type":"text","label":"Words","default":"Approved","maxLength":18},"x":{"type":"number","label":"Centre across (%)","default":50,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Centre down (%)","default":50,"min":0,"max":100,"step":1},"angle":{"type":"number","label":"Angle","default":-12,"min":-45,"max":45,"step":1},"size":{"type":"number","label":"Size","default":100,"min":40,"max":240,"step":5},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"stamp":"the stamp"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/stamp","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const cx = W * p.x / 100, cy = H * p.y / 100, txt = String(p.text || \"\").toUpperCase();\n const f = fitFont($, txt, \"display\", p.size * k, W * 0.7, { ls: 0.04 });\n const tw = $.measure(txt, f), pad = f.size * 0.42, w = tw + pad * 2, h = f.size * 1.5;\n // Ink that did not take: a fixed noise mask so the stamp looks pressed, not printed.\n const nid = $.uid(\"sn\"), mid = $.uid(\"sm\");\n $.def('<filter id=\"' + nid + '\" x=\"0\" y=\"0\" width=\"100%\" height=\"100%\"><feTurbulence type=\"fractalNoise\" baseFrequency=\"' + (0.9 / k).toFixed(3) + '\" numOctaves=\"2\" seed=\"7\"/><feColorMatrix type=\"matrix\" values=\"0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 -2.4 1.75\"/></filter>');\n $.def('<mask id=\"' + mid + '\" maskUnits=\"userSpaceOnUse\" x=\"' + (cx - w) + '\" y=\"' + (cy - h) + '\" width=\"' + 2 * w + '\" height=\"' + 2 * h + '\"><rect x=\"' + (cx - w) + '\" y=\"' + (cy - h) + '\" width=\"' + 2 * w + '\" height=\"' + 2 * h + '\" fill=\"#fff\" filter=\"url(#' + nid + ')\"/></mask>');\n const body = $.rect(cx - w / 2, cy - h / 2, w, h, { r: 14 * k, stroke: col, sw: 9 * k }) + $.rect(cx - w / 2 + 16 * k, cy - h / 2 + 16 * k, w - 32 * k, h - 32 * k, { r: 8 * k, stroke: col, sw: 4 * k }) +\n $.text(txt, Object.assign({}, f, { x: cx, y: cy - f.size * 0.56, anchor: \"middle\", fill: col }));\n const inked = '<g mask=\"url(#' + mid + ')\" transform=\"rotate(' + p.angle + \" \" + cx + \" \" + cy + ')\">' + body + \"</g>\";\n s += M.part(\"stamp\", { at: tin, in: \"slam\", box: { x: cx - w / 2, y: cy - h / 2, w: w, h: h } }, inked);\n return s;\n}\n","libs":[]},{"id":"reactions","name":"Reactions","description":"Live reactions float up in a stream — hearts, likes, fire, laughs — swaying, popping in and fading out, like a live stream.","category":"social","tags":["annotation","reactions","hearts","likes","live","emoji","float"],"placement":"overlay","duration":{"default":5,"min":2,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"kind":{"type":"choice","label":"Reactions","options":["mixed","hearts","likes","fire","laughs","stars"],"default":"mixed"},"x":{"type":"number","label":"Stream across (%)","default":86,"min":0,"max":100,"step":1},"rate":{"type":"number","label":"Per second","default":4,"min":1,"max":12,"step":1},"size":{"type":"number","label":"Size","default":88,"min":36,"max":160,"step":4},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"stream":"the reactions"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/reactions","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const KINDS = { hearts: [\"heart\", \"#FF3B5C\"], likes: [\"thumbsUp\", \"#2F7BFF\"], fire: [\"fire\", \"#FF7A1A\"], laughs: [\"smile\", \"#FFC21A\"], stars: [\"star\", \"#B55CFF\"] };\n const names = p.kind === \"mixed\" ? [\"hearts\", \"likes\", \"fire\", \"laughs\", \"hearts\", \"stars\"] : [p.kind];\n const x0 = W * p.x / 100, life = 2.6, rise = H * 0.62, n = Math.ceil(($.dur - tin) * p.rate);\n const end = M.outAt;\n let out = \"\";\n for (let i = 0; i < n; i++) {\n const born = tin + i / p.rate + ($.hash(i * 1.7) - 0.5) * 0.15, age = t - born;\n if (age < 0 || age > life || born > end) continue;\n const kd = KINDS[names[Math.floor($.hash(i * 4.1) * names.length) % names.length]];\n const q = age / life, r = p.size * k * 0.5 * (0.8 + 0.4 * $.hash(i * 2.9));\n const x = x0 + Math.sin(age * 2.4 + i) * 34 * k + ($.hash(i * 6.3) - 0.5) * 60 * k;\n const y = H * 0.9 - rise * $.E.out2(q);\n const sc = Math.min(1, $.E.outBack($.clamp(age / 0.3))), op = q > 0.7 ? 1 - (q - 0.7) / 0.3 : 1;\n out += '<g opacity=\"' + op.toFixed(3) + '\" transform=\"translate(' + x + \" \" + y + \") scale(\" + sc + ')\">' + $.circle(0, 0, r, { fill: kd[1] }) + $.icon(kd[0], -r * 0.55, -r * 0.55, r * 1.1, { color: \"#FFFFFF\", fill: \"#FFFFFF\", sw: 2 }) + \"</g>\";\n }\n return s + M.part(\"stream\", { in: \"none\", out: \"none\", vary: false }, out);\n}\n\n/** A small pop as each reaction is born, panned with it and pitched a little differently. */\nfunction sound($) {\n const p = $.data, tin = $.beat(\"in\", 0.1), end = motion($, 0).outAt, n = Math.ceil(($.dur - tin) * p.rate);\n const out = [];\n for (let i = 0; i < n; i++) {\n const born = tin + i / p.rate + ($.hash(i * 1.7) - 0.5) * 0.15;\n if (born > end) continue;\n out.push({ at: Math.max(0, born), kind: \"pop\", level: 0.45, pitch: Math.round(($.hash(i * 3.3) - 0.5) * 8), pan: (p.x / 50 - 1) * 0.6 });\n }\n return out;\n}\n","libs":[]},{"id":"pointer","name":"Pointer","description":"A tech callout: a pulsing dot on the spot, a leader line that draws out at an angle, and a card with a title and a detail.","category":"callout","tags":["annotation","pointer","callout","leader","label","spec","tech"],"placement":"overlay","duration":{"default":4,"min":1.5,"max":15},"aspects":["16:9","9:16","1:1","4:5"],"props":{"title":{"type":"text","label":"Title","default":"48 MP sensor","maxLength":32},"detail":{"type":"text","label":"Detail","default":"Larger pixels, better low light","maxLength":64},"x":{"type":"number","label":"Point across (%)","default":42,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Point down (%)","default":55,"min":0,"max":100,"step":1},"side":{"type":"choice","label":"Card side","options":["auto","left","right"],"default":"auto"},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"dot":"the dot on the spot","line":"the leader line","card":"the card"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/pointer","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const tx = W * p.x / 100, ty = H * p.y / 100;\n const right = p.side === \"right\" || (p.side === \"auto\" && tx < W / 2);\n const up = ty > H * 0.4, dir = right ? 1 : -1;\n const tf = Object.assign({}, $.font(\"display\"), { size: 46 * k }), df = Object.assign({}, $.font(\"text\"), { size: 28 * k });\n const title = $.case(p.title), maxW = Math.min(W * 0.4, 560 * k);\n const dl = p.detail ? $.wrap(p.detail, maxW, df).slice(0, 3) : [];\n const cw = Math.min(maxW, Math.max($.measure(title, tf), widest($, dl, df))) + 56 * k, ch = 40 * k + tf.size * 1.1 + (dl.length ? 14 * k + dl.length * df.size * 1.3 : 0) + 4 * k;\n // An elbow: out at 45° for d1, then level to the card.\n const d1 = Math.min(W, H) * 0.14, ex = tx + dir * d1, ey = ty + (up ? -d1 : d1);\n let cx = right ? ex + Math.min(W, H) * 0.08 : ex - Math.min(W, H) * 0.08 - cw;\n cx = Math.min(W - cw - 24 * k, Math.max(24 * k, cx));\n const lineEnd = right ? cx : cx + cw, cy = Math.min(H - ch - 24 * k, Math.max(24 * k, ey - ch / 2));\n const lp = $.E.inOut2($.seg(t, tin + 0.3, tin + 0.8));\n s += M.part(\"dot\", { at: tin, in: \"pop\", box: { x: tx - 16 * k, y: ty - 16 * k, w: 32 * k, h: 32 * k } }, $.circle(tx, ty, 10 * k, { fill: col, stroke: \"#FFFFFF\", sw: 3 * k }) + pulse($, t, tin + 0.2, tx, ty, 12 * k, col));\n if (lp > 0) s += M.part(\"line\", { at: tin + 0.3, in: \"none\" }, $.path(\"M\" + tx + \" \" + ty + \"L\" + ex + \" \" + ey + \"L\" + lineEnd + \" \" + ey, { stroke: col, sw: 4 * k, draw: lp, cap: \"round\" }));\n let card = $.card(cx, cy, cw, ch, { t: t }) + $.rect(right ? cx : cx + cw - 8 * k, cy, 8 * k, ch, { fill: col });\n card += $.text(title, Object.assign({}, tf, { x: cx + 28 * k, y: cy + 22 * k, fill: c.fg }));\n dl.forEach(function (l, i) { card += $.text(l, Object.assign({}, df, { x: cx + 28 * k, y: cy + 22 * k + tf.size * 1.1 + 14 * k + i * df.size * 1.3, fill: c.mute })); });\n s += M.part(\"card\", { at: tin + 0.75, in: \"slide\", from: right ? \"left\" : \"right\", distance: 20, box: { x: cx, y: cy, w: cw, h: ch } }, card);\n return s;\n}\n","libs":[]},{"id":"emphasis","name":"Emphasis marks","description":"Comic emphasis round a point: action lines burst out and sparkles twinkle — for a punchline, a reveal or a wow.","category":"callout","tags":["annotation","emphasis","burst","sparkle","wow","lines","comic"],"placement":"overlay","duration":{"default":2.5,"min":1,"max":12},"aspects":["16:9","9:16","1:1","4:5"],"props":{"x":{"type":"number","label":"Centre across (%)","default":50,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Centre down (%)","default":45,"min":0,"max":100,"step":1},"size":{"type":"number","label":"Size","default":100,"min":40,"max":260,"step":5},"kind":{"type":"choice","label":"Marks","options":["both","lines","sparkles"],"default":"both"},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"lines":"the action lines","sparkles":"the sparkles"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/emphasis","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const cx = W * p.x / 100, cy = H * p.y / 100, R = p.size * k * 1.6;\n if (p.kind !== \"sparkles\") {\n let lines = \"\";\n for (let i = 0; i < 9; i++) {\n const a = -Math.PI * 0.95 + (i / 8) * Math.PI * 0.9 + ($.hash(i * 2.1) - 0.5) * 0.12;\n const r0 = R * (0.62 + 0.1 * $.hash(i * 5.5)), r1 = R * (0.95 + 0.25 * $.hash(i * 7.7));\n const pr = $.E.out3($.seg(t, tin + i * 0.03, tin + i * 0.03 + 0.25));\n const wob = 1 + 0.06 * Math.sin((t - tin) * 9 + i);\n lines += $.line(cx + Math.cos(a) * r0 * wob, cy + Math.sin(a) * r0 * wob, cx + Math.cos(a) * r1 * wob, cy + Math.sin(a) * r1 * wob, { p: pr, stroke: col, sw: (7 + 5 * $.hash(i)) * k });\n }\n s += M.part(\"lines\", { at: tin, in: \"none\" }, lines);\n }\n if (p.kind !== \"lines\") {\n let sp = \"\";\n const spots = [[-1.05, -0.55, 1], [1.0, -0.75, 0.7], [0.8, 0.45, 0.55], [-0.75, 0.6, 0.45]];\n spots.forEach(function (q, i) {\n const a = tin + 0.15 + i * 0.12, sc = $.E.outBack($.clamp((t - a) / 0.3)) * (1 + 0.18 * Math.sin((t - a) * 7 + i * 2)) * q[2];\n if (sc <= 0) return;\n const x = cx + q[0] * R, y = cy + q[1] * R, r = 34 * k * sc, n = r * 0.22;\n sp += $.path(\"M\" + x + \" \" + (y - r) + \"Q\" + (x + n) + \" \" + (y - n) + \" \" + (x + r) + \" \" + y + \"Q\" + (x + n) + \" \" + (y + n) + \" \" + x + \" \" + (y + r) + \"Q\" + (x - n) + \" \" + (y + n) + \" \" + (x - r) + \" \" + y + \"Q\" + (x - n) + \" \" + (y - n) + \" \" + x + \" \" + (y - r) + \"Z\", { fill: i % 2 ? c.accent2 : col });\n });\n s += M.part(\"sparkles\", { at: tin + 0.15, in: \"none\" }, sp);\n }\n return s;\n}\n\n/** A pop as the lines burst out, a shimmer as the sparkles turn up. */\nfunction sound($) {\n const p = $.data, tin = $.beat(\"in\", 0.1), out = [];\n if (p.kind !== \"sparkles\") out.push({ at: tin, kind: \"pop\", level: 0.9, pan: (p.x / 50 - 1) * 0.6 });\n if (p.kind !== \"lines\") out.push({ at: tin + 0.15, kind: \"shimmer\", dur: 0.7, level: 0.6, pan: (p.x / 50 - 1) * 0.6 });\n return out;\n}\n","libs":[]},{"id":"markers","name":"Numbered markers","description":"Numbered dots pop onto the frame one after another, each with a ring and an optional label — for walking through the parts of a shot.","category":"callout","tags":["annotation","markers","numbers","steps","points","parts","hotspots"],"placement":"overlay","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"points":{"type":"list","label":"Points","of":"text","default":["22 38 Lens","52 62 Grip","78 30 Screen"],"maxItems":8,"description":"one per line: across% down% label"},"gap":{"type":"number","label":"Seconds apart","default":0.45,"min":0.1,"max":3,"step":0.05},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"markers":"the numbered dots","labels":"their labels"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/markers","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const pts = (p.points || []).map(function (row) {\n const m = /^\\s*(-?[\\d.]+)[\\s,]+(-?[\\d.]+)\\s*(.*)$/.exec(String(row));\n return m ? { x: W * $.clamp(Number(m[1]) / 100), y: H * $.clamp(Number(m[2]) / 100), label: m[3] } : null;\n }).filter(Boolean);\n const r = 34 * k, nf = Object.assign({}, $.font(\"display\"), { size: 36 * k });\n let dots = \"\", labels = \"\";\n pts.forEach(function (q, i) {\n const a = tin + i * p.gap;\n dots += M.part(\"markers\", { at: a, in: \"pop\", box: { x: q.x - r, y: q.y - r, w: 2 * r, h: 2 * r } },\n $.circle(q.x, q.y, r, { fill: col, stroke: \"#FFFFFF\", sw: 4 * k, shadow: \"md\" }) + $.text(String(i + 1), Object.assign({}, nf, { x: q.x, y: q.y - nf.size * 0.56, anchor: \"middle\", fill: $.on(col) })) + pulse($, t, a + 0.2, q.x, q.y, r, col));\n if (q.label) {\n const tg = tagAt($, q.label, q.x + (q.x < W * 0.7 ? 1 : -1) * (r + 20 * k + $.chip.size($.case(q.label), { size: 32 * k }).w / 2), q.y, c.surface, 32);\n labels += M.part(\"labels\", { at: a + 0.15, in: \"rise\", distance: 12, box: tg.box }, tg.markup);\n }\n });\n return s + dots + labels;\n}\n","libs":[]}]},{"format":1,"id":"type","name":"MONARC Type","version":"1.0.0","description":"Kinetic typography on the brand's ground: typewriter, word swap, split-flap board, marquee bands, echo stack, liquid fill, decode, word by word, key phrase and an editorial stack.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/type","style":"tomato","shared":"shared.js","items":["typewriter","word-swap","split-flap","marquee","echo","liquid-fill","decode","word-by-word","key-phrase","stack"],"entries":[{"id":"typewriter","name":"Typewriter","description":"Words type on letter by letter with a blinking caret — in the brand's mono, text or display face.","category":"text","tags":["kinetic type","text","typewriter","typing","type on","caret","terminal"],"placement":"full","duration":{"default":5,"min":1.5,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"text":{"type":"longtext","label":"Text","default":"Every great edit starts with a single cut.","maxLength":200},"face":{"type":"choice","label":"Face","options":["mono","display","text"],"default":"mono"},"speed":{"type":"number","label":"Letters a second","default":18,"min":4,"max":60,"step":1}},"parts":{"ground":"the brand's ground","text":"the typed text"},"beats":{"in":0.2},"pack":"type","ref":"type/typewriter","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const face = p.face, txt = face === \"display\" ? $.case(p.text) : String(p.text || \"\");\n const font = Object.assign({}, $.font(face), { ls: face === \"mono\" ? 0 : -0.015 });\n const fit = fitLines($, txt, font, A.w * 0.86, Math.min(A.h * 0.2, 140 * k), 34 * k, 6);\n const w = widest($, fit.lines, fit.font), lh = face === \"display\" ? 1.08 : 1.3;\n const y0 = H / 2 - (fit.lines.length * fit.size * lh) / 2;\n s += M.text(\"text\", Object.assign({ at: tin, in: \"typewriter\", split: \"letters\", stagger: 1 / p.speed }, fit.font, { x: W / 2 - w / 2, y: y0, lh: lh, maxW: A.w * 0.86, fill: c.fg }), fit.lines.join(\"\\n\"));\n return s;\n}\n","libs":[]},{"id":"word-swap","name":"Word swap","description":"A fixed lead and a word that keeps changing: each new word rolls up into an accent box that stretches to fit it — “We make videos / ads / stories”.","category":"text","tags":["kinetic type","text","word swap","rotating","changing word","slot","carousel","hero"],"placement":"full","duration":{"default":6,"min":2,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"lead":{"type":"text","label":"Lead","default":"We make","maxLength":24},"words":{"type":"list","label":"Words","of":"text","default":["videos","ads","stories","brands"],"maxItems":8},"end":{"type":"text","label":"After","default":"","maxLength":12,"description":"words after the box (empty for none)"}},"parts":{"ground":"the brand's ground","lead":"the fixed words","box":"the box and its word"},"beats":{"in":0.2},"pack":"type","ref":"type/word-swap","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const lead = $.case(p.lead), end = p.end ? $.case(p.end) : \"\";\n const words = (p.words || []).map(function (w) { return $.case(String(w)); }).filter(Boolean);\n if (!words.length) words.push(\"…\");\n const tall = A.tall || A.square;\n const base = Object.assign({}, $.font(\"display\"), { ls: -0.03 });\n const pad = 0.28;\n const wid = function (sz, str) { return $.measure(str, Object.assign({}, base, { size: sz })); };\n // One size for every word: the widest must fit with the lead (side by side on wide frames, stacked on tall).\n const widestWord = words.reduce(function (a, w) { return wid(100, w) > wid(100, a) ? w : a; }, words[0]);\n const row = function (sz) { return tall ? Math.max(wid(sz, lead), wid(sz, widestWord) + sz * pad * 2, end ? wid(sz, end) : 0) : wid(sz, lead) + sz * 0.3 + wid(sz, widestWord) + sz * pad * 2 + (end ? sz * 0.3 + wid(sz, end) : 0); };\n let size = Math.min(150 * k, A.h * (tall ? 0.16 : 0.28));\n if (row(size) > A.w * 0.94) size *= (A.w * 0.94) / row(size);\n const f = Object.assign({}, base, { size: size });\n const per = Math.max(0.6, (M.outAt - tin - 0.3) / words.length);\n const rel = t - tin - 0.3, idx = $.clamp(Math.floor(rel / per), 0, words.length - 1), loc = rel - idx * per;\n const sw = $.E.out3($.clamp(loc / 0.4)), cur = words[idx], prev = idx > 0 ? words[idx - 1] : null;\n const bwOf = function (w) { return wid(size, w) + size * pad * 2; };\n const bw = prev && idx > 0 ? $.lerp(bwOf(prev), bwOf(cur), sw) : bwOf(cur), bh = size * 1.32;\n const lw = wid(size, lead), ew = end ? wid(size, end) : 0, gap = size * 0.3;\n let lx, bx, ex, ly, by;\n if (tall) {\n const h = bh * 2.1 + (end ? size * 1.15 : 0);\n ly = H / 2 - h / 2; by = ly + size * 1.25;\n lx = W / 2 - lw / 2; bx = W / 2 - bw / 2; ex = W / 2 - ew / 2;\n } else {\n const total = lw + gap + bw + (end ? gap + ew : 0);\n lx = W / 2 - total / 2; bx = lx + lw + gap; ex = bx + bw + gap;\n by = H / 2 - bh / 2; ly = by + (bh - size) / 2 - size * 0.04;\n }\n s += M.text(\"lead\", Object.assign({ at: tin, in: \"rise\" }, f, { x: lx, y: ly, fill: c.fg }), lead);\n if (end) s += M.text(\"lead\", Object.assign({ at: tin, in: \"rise\" }, f, { x: ex, y: tall ? by + bh + size * 0.1 : ly, fill: c.fg }), end);\n // The words roll through the box: the old one up and out, the new one up from below.\n const ty = by + (bh - size) / 2 - size * 0.04, roll = bh;\n let inner = $.text(cur, Object.assign({}, f, { x: bx + bw / 2, y: ty + (idx > 0 ? (1 - sw) * roll : 0), anchor: \"middle\", fill: $.on(c.accent) }));\n if (prev && sw < 1) inner += $.text(prev, Object.assign({}, f, { x: bx + bw / 2, y: ty - sw * roll, anchor: \"middle\", fill: $.on(c.accent) }));\n s += M.part(\"box\", { at: tin + 0.2, in: \"pop\", box: { x: bx, y: by, w: bw, h: bh } }, $.rect(bx, by, bw, bh, { r: Math.min($.style.radius, bh / 2), fill: c.accent }) + $.clip(bx, by, bw, bh, 0, inner));\n return s;\n}\n","libs":[]},{"id":"split-flap","name":"Split-flap board","description":"An airport split-flap board: every cell flutters through letters and clacks onto the message, left to right.","category":"text","tags":["kinetic type","text","split flap","flap","board","airport","departures","solari"],"placement":"full","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"text":{"type":"longtext","label":"Message","default":"NOW BOARDING\nGATE 7","maxLength":60,"description":"up to three lines; new line for a new row"}},"parts":{"ground":"the brand's ground","board":"the board"},"beats":{"in":0.2},"pack":"type","ref":"type/split-flap","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const POOL = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let rows = String(p.text || \"\").toUpperCase().split(\"\\n\").map(function (r) { return r.trim(); }).filter(Boolean).slice(0, 3);\n if (!rows.length) rows.push(\"HELLO\");\n // Tall frames: long rows break between words so the cells stay big.\n if (A.tall || A.square) {\n const max = A.tall ? 8 : 11, out = [];\n rows.forEach(function (r) {\n let cur = \"\";\n r.split(/\\s+/).forEach(function (w) { if (cur && (cur + \" \" + w).length > max) { out.push(cur); cur = w; } else cur = cur ? cur + \" \" + w : w; });\n if (cur) out.push(cur);\n });\n rows = out.slice(0, 5);\n }\n const cols = Math.max.apply(null, rows.map(function (r) { return r.length; }));\n // A board is black with white letters whatever the brand; the brand shows in the landing flash.\n const cellFill = \"#151517\", ink = \"#F4F1EA\";\n const cw = Math.min((A.w * 0.96) / cols, (A.h * 0.7) / rows.length / 1.36) * 0.94, ch = cw * 1.36, gap = cw * 0.07;\n const bw = cols * (cw + gap) - gap, bh = rows.length * (ch + gap * 2) - gap * 2;\n const x0 = W / 2 - bw / 2, y0 = H / 2 - bh / 2;\n const f = Object.assign({}, $.font(\"mono\"), { size: ch * 0.62, weight: 700 });\n let out = \"\";\n rows.forEach(function (row, r) {\n const padL = Math.floor((cols - row.length) / 2);\n for (let q = 0; q < cols; q++) {\n const target = row.charAt(q - padL) || \" \";\n const x = x0 + q * (cw + gap), y = y0 + r * (ch + gap * 2);\n const start = tin + q * 0.045 + r * 0.12, flips = target === \" \" ? 0 : 5 + Math.floor($.hash(q * 3.1 + r * 7.7) * 9), fd = 0.065;\n const n = Math.floor((t - start) / fd), fr = ((t - start) / fd) % 1;\n const done = t < start ? false : n >= flips;\n const ch2 = t < start ? \" \" : done ? target : POOL.charAt(Math.floor($.hash(n * 1.37 + q + r * 9) * POOL.length));\n out += $.rect(x, y, cw, ch, { r: cw * 0.1, fill: cellFill });\n if (ch2 !== \" \") out += $.text(ch2, Object.assign({}, f, { x: x + cw / 2, y: y + ch / 2 - f.size * 0.56, anchor: \"middle\", fill: done && t - start - flips * fd < 0.25 ? c.accent : ink }));\n // The flap falling: a shade over the top half shrinking to the hinge, then over the bottom half growing.\n if (t >= start && !done) {\n const half = ch / 2;\n if (fr < 0.5) out += $.rect(x, y + half * 2 * fr, cw, half * (1 - 2 * fr), { fill: $.rgba(cellFill, 0.85) });\n else out += $.rect(x, y + half, cw, half * (2 * fr - 1), { fill: $.rgba(\"#000000\", 0.35) });\n }\n out += $.rect(x, y + ch / 2 - Math.max(1, cw * 0.018), cw, Math.max(2, cw * 0.036), { fill: $.rgba(\"#000000\", 0.45) });\n }\n });\n s += M.part(\"board\", { at: tin, in: \"fade\", dur: 0.25 }, out);\n return s;\n}\n","libs":[]},{"id":"marquee","name":"Marquee bands","description":"Big words repeat along tilted bands that scroll in opposite directions — solid on accent, outlined on the ground — like a poster in motion.","category":"text","tags":["kinetic type","text","marquee","ticker","scrolling","bands","poster","repeat"],"placement":"full","duration":{"default":6,"min":2,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"text":{"type":"text","label":"Words","default":"New drop","maxLength":28},"bands":{"type":"number","label":"Bands","default":3,"min":1,"max":5,"step":1},"angle":{"type":"number","label":"Tilt","default":-8,"min":-30,"max":30,"step":1},"speed":{"type":"number","label":"Speed","default":1,"min":0.2,"max":3,"step":0.1}},"parts":{"ground":"the brand's ground","bands":"the bands"},"beats":{"in":0.2},"pack":"type","ref":"type/marquee","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const str = $.case(p.text), n = Math.max(1, Math.round(p.bands));\n const D = Math.hypot(W, H), bandH = Math.min(H, W) * (n === 1 ? 0.3 : 0.62 / n + 0.06);\n const f = Object.assign({}, $.font(\"display\"), { size: bandH * 0.64, ls: -0.02 });\n const sep = \" • \", unit = $.measure(str + sep, f);\n let out = \"\";\n for (let i = 0; i < n; i++) {\n const y = (i - (n - 1) / 2) * bandH * 1.12, solid = i % 2 === 0;\n const dir = i % 2 ? -1 : 1, off = (((t * 140 * k * p.speed * dir) % unit) + unit) % unit;\n let row = \"\";\n for (let x = -D / 2 - unit + off; x < D / 2 + unit; x += unit) row += $.text(str + sep, Object.assign({}, f, { within: { x: -1e5, y: -1e5, w: 2e5, h: 2e5 }, x: x, y: y - f.size * 0.56, fill: solid ? $.on(c.accent) : \"none\", stroke: solid ? undefined : c.fg, sw: solid ? undefined : 2.6 * k }));\n const band = (solid ? $.rect(-D / 2, y - bandH / 2, D, bandH, { fill: c.accent }) : \"\") + row;\n out += M.part(\"bands\", { at: tin + i * 0.12, in: \"slide\", from: i % 2 ? \"right\" : \"left\", distance: 160, box: { x: -D / 2, y: y - bandH / 2, w: D, h: bandH } }, band);\n }\n s += '<g transform=\"translate(' + W / 2 + \" \" + H / 2 + \") rotate(\" + p.angle + ')\">' + out + \"</g>\";\n return s;\n}\n","libs":[]},{"id":"echo","name":"Echo stack","description":"One word with outlined echoes fanning out above and below it, rippling in a slow wave — a poster headline that breathes.","category":"text","tags":["kinetic type","text","echo","repeat","outline","stack","poster","wave"],"placement":"full","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"word":{"type":"text","label":"Word","default":"Echo","maxLength":16},"copies":{"type":"number","label":"Echoes each side","default":4,"min":1,"max":7,"step":1}},"parts":{"ground":"the brand's ground","word":"the solid word","echoes":"the outlined echoes"},"beats":{"in":0.2},"pack":"type","ref":"type/echo","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const str = $.case(p.word), n = Math.max(1, Math.round(p.copies));\n const f = fitFont($, str, \"display\", Math.min(A.h / (2 * n + 1) * 1.45, 260 * k), A.w * 0.92, { ls: -0.03 });\n const step = f.size * 0.74, cy = H / 2 - f.size * 0.56;\n let ech = \"\";\n for (let j = n; j >= 1; j--) {\n const open = $.E.outBack($.clamp((t - tin - 0.25 - j * 0.07) / 0.6));\n for (const sgn of [-1, 1]) {\n const wob = Math.sin((t - tin) * 2.2 - j * 0.55) * 16 * k * j / n;\n ech += $.text(str, Object.assign({}, f, { x: W / 2 + wob * sgn, y: cy + sgn * j * step * open, anchor: \"middle\", fill: \"none\", stroke: j % 2 ? c.accent : c.accent2, sw: 2.6 * k, op: Math.max(0.15, 1 - j * 0.16) }));\n }\n }\n s += M.part(\"echoes\", { at: tin + 0.2, in: \"fade\", dur: 0.3 }, ech);\n s += M.text(\"word\", Object.assign({ at: tin, in: \"pop\" }, f, { x: W / 2, y: cy, anchor: \"middle\", fill: c.fg }), str);\n return s;\n}\n","libs":[]},{"id":"liquid-fill","name":"Liquid fill","description":"A big outlined word fills from the bottom with a wavy liquid in the brand's accents, then a tagline rises under it.","category":"text","tags":["kinetic type","text","liquid","fill","outline","wave","water","pour"],"placement":"full","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"word":{"type":"text","label":"Word","default":"Fresh","maxLength":16},"tagline":{"type":"text","label":"Tagline","default":"Squeezed this morning","maxLength":48,"description":"empty for none"}},"parts":{"ground":"the brand's ground","outline":"the outlined word","fill":"the liquid","tagline":"the tagline"},"beats":{"in":0.2},"pack":"type","ref":"type/liquid-fill","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const str = $.case(p.word);\n const f = fitFont($, str, \"display\", Math.min(A.h * 0.42, 320 * k), A.w * 0.92, { ls: -0.03 });\n const tw = $.measure(str, f), x0 = W / 2 - tw / 2, y = H / 2 - f.size * (p.tagline ? 0.72 : 0.56);\n const top = y + f.size * 0.12, bot = y + f.size * 0.96;\n const word = function (fill, stroke) { return $.text(str, Object.assign({}, f, { x: W / 2, y: y, anchor: \"middle\", fill: fill, stroke: stroke, sw: stroke ? 3.2 * k : undefined })); };\n const lvl = $.E.inOut2($.seg(t, tin + 0.4, tin + 2.2));\n const wave = function (level, amp, ph, id) {\n let d = \"M\" + (x0 - 20 * k) + \" \" + (bot + 20 * k);\n for (let i = 0; i <= 30; i++) { const x = x0 - 20 * k + ((tw + 40 * k) * i) / 30; d += \"L\" + x + \" \" + (level + Math.sin(i * 0.65 + (t - tin) * 4 + ph) * amp); }\n d += \"L\" + (x0 + tw + 20 * k) + \" \" + (bot + 20 * k) + \"Z\";\n $.def('<clipPath id=\"' + id + '\"><path d=\"' + d + '\"/></clipPath>');\n return 'clip-path=\"url(#' + id + ')\"';\n };\n const amp = f.size * 0.045 * (1 - 0.6 * lvl), L = $.lerp(bot + amp, top - amp * 2, lvl);\n s += M.part(\"outline\", { at: tin, in: \"fade\" }, word(\"none\", c.fg));\n if (lvl > 0) s += M.part(\"fill\", { at: tin + 0.4, in: \"none\" }, \"<g \" + wave(L - f.size * 0.05, amp, 1.8, $.uid(\"lq\")) + \">\" + word(c.accent2) + \"</g><g \" + wave(L, amp, 0, $.uid(\"lq\")) + \">\" + word(c.accent) + \"</g>\");\n if (p.tagline) {\n const tf = Object.assign({}, $.font(\"text\"), { size: Math.max(32 * k, f.size * 0.16) });\n s += M.text(\"tagline\", Object.assign({ at: tin + 2, in: \"rise\", distance: 18 }, tf, { x: W / 2, y: bot + f.size * 0.18, anchor: \"middle\", fill: c.mute, maxW: A.w }), $.case(p.tagline));\n }\n return s;\n}\n","libs":[]},{"id":"decode","name":"Decode","description":"A title decodes letter by letter out of scrambled glyphs between brackets, then a mono line types on under it.","category":"text","tags":["kinetic type","text","decode","scramble","hacker","glitch","reveal","tech"],"placement":"full","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"title":{"type":"text","label":"Title","default":"Access granted","maxLength":28},"line":{"type":"text","label":"Line","default":"loading your workspace","maxLength":60,"description":"empty for none"}},"parts":{"ground":"the brand's ground","brackets":"the brackets","title":"the title","line":"the mono line"},"beats":{"in":0.2},"pack":"type","ref":"type/decode","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const str = $.case(p.title);\n const fit = fitLines($, str, Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w * 0.8, Math.min(170 * k, A.h * 0.3), 60 * k, 2);\n const lh = fit.size * 1.05, h = fit.lines.length * lh, y0 = H / 2 - h / 2 - (p.line ? 40 * k : 0);\n const w = widest($, fit.lines, fit.font);\n fit.lines.forEach(function (l, i) {\n s += M.text(\"title\", Object.assign({ at: tin + 0.2 + i * 0.25, in: \"scramble\", split: \"letters\", dur: 0.7, stagger: Math.min(0.06, 0.9 / l.length) }, fit.font, { x: W / 2, y: y0 + i * lh, anchor: \"middle\", fill: c.fg }), l);\n });\n // Brackets open out from the centre to frame the title.\n const bo = $.E.out3($.seg(t, tin, tin + 0.5)), bx = (w / 2 + 50 * k) * bo, bh = h + 30 * k, by = y0 - 18 * k, arm = 34 * k;\n const br = function (x, dir) { return $.path(\"M\" + (x + dir * arm) + \" \" + by + \"H\" + x + \"V\" + (by + bh) + \"H\" + (x + dir * arm), { stroke: c.accent, sw: 7 * k, cap: \"square\", join: \"miter\" }); };\n s += M.part(\"brackets\", { at: tin, in: \"fade\", dur: 0.2 }, br(W / 2 - bx, 1) + br(W / 2 + bx, -1));\n if (p.line) {\n const mf = Object.assign({}, $.font(\"mono\"), { size: Math.max(28 * k, fit.size * 0.22) });\n const txt = \"> \" + p.line, mw = $.measure(txt, mf);\n s += M.text(\"line\", Object.assign({ at: tin + 1.1, in: \"typewriter\", split: \"letters\", stagger: 0.04 }, mf, { x: W / 2 - Math.min(mw, A.w) / 2, y: by + bh + 40 * k, fill: c.accent, maxW: A.w }), txt);\n }\n return s;\n}\n","libs":[]},{"id":"word-by-word","name":"Word by word","description":"A sentence one word at a time, each word slammed full-frame on a new ground — the brand's, the accent, the second accent — cut to a steady pace. *Marked* words go big.","category":"text","tags":["kinetic type","text","word by word","kinetic","fast","punchy","quote","beat"],"placement":"full","duration":{"default":5,"min":1.5,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"text":{"type":"longtext","label":"Text","default":"Stop scrolling. *This* changes everything.","maxLength":160,"description":"wrap a word in *stars* to make it the accent"},"pace":{"type":"number","label":"Seconds a word","default":0.42,"min":0.15,"max":2,"step":0.01}},"parts":{"ground":"the brand's ground","words":"the words"},"beats":{"in":0.2},"pack":"type","ref":"type/word-by-word","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const ws = markedWords($.case(p.text));\n if (!ws.length) return s;\n const pace = Math.max(0.15, Math.min(p.pace, (M.outAt - tin) / ws.length));\n const i = $.clamp(Math.floor((t - tin) / pace), 0, ws.length - 1), loc = t - tin - i * pace;\n if (t < tin) return s;\n const w = ws[i], grounds = [null, c.accent, c.fg, null, c.accent2];\n const g = w.marked ? c.accent : grounds[i % grounds.length];\n let fr = g ? $.rect(0, 0, W, H, { fill: g }) : \"\";\n const ink = g ? $.on(g) : c.fg;\n const font = w.marked ? Object.assign({}, $.font(\"accent\")) : Object.assign({}, $.font(\"display\"), { ls: -0.035 });\n const f = fitFont($, w.word, w.marked ? \"accent\" : \"display\", Math.min(A.h * 0.5, (w.marked ? 340 : 280) * k), A.w * 0.94, { ls: w.marked ? -0.01 : -0.035 });\n const pop = 1 + 0.22 * (1 - $.E.out3($.clamp(loc / 0.14))), rot = (i % 2 ? 1 : -1) * 2.5 * (1 - $.E.out3($.clamp(loc / 0.2)));\n fr += '<g transform=\"translate(' + W / 2 + \" \" + H / 2 + \") rotate(\" + rot + \") scale(\" + pop + \") translate(\" + -W / 2 + \" \" + -H / 2 + ')\">' + $.text(w.word, Object.assign({}, f, { x: W / 2, y: H / 2 - f.size * 0.56, anchor: \"middle\", fill: ink })) + \"</g>\";\n s += M.part(\"words\", { at: tin, in: \"none\" }, fr);\n return s;\n}\n\n/** A tick for every word as it lands, a hit for a marked one. */\nfunction sound($) {\n const p = $.data, tin = $.beat(\"in\", 0.2), ws = markedWords($.case(p.text));\n if (!ws.length) return [];\n const pace = Math.max(0.15, Math.min(p.pace, (motion($, 0).outAt - tin) / ws.length));\n return ws.map(function (w, i) {\n return w.marked ? { at: tin + i * pace, kind: \"hit\", level: 0.8 } : { at: tin + i * pace, kind: \"tick\", level: 0.9, pitch: (i % 3) - 1 };\n });\n}\n","libs":[]},{"id":"key-phrase","name":"Key phrase","description":"A sentence sets in big type, then its *marked* words get a marker swipe one after another — the line people should remember.","category":"text","tags":["kinetic type","text","key phrase","highlight","marker","quote","emphasis","sentence"],"placement":"full","duration":{"default":5,"min":1.5,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"text":{"type":"longtext","label":"Text","default":"Good editing is *invisible* until it is *missing*.","maxLength":140,"description":"wrap words in *stars* to mark them"}},"parts":{"ground":"the brand's ground","text":"the sentence","marks":"the marker swipes"},"beats":{"in":0.2},"pack":"type","ref":"type/key-phrase","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const ws = markedWords($.case(p.text)), plain = ws.map(function (w) { return w.word; }).join(\" \");\n const fit = fitLines($, plain, Object.assign({}, $.font(\"display\"), { ls: -0.025 }), A.w * 0.9, Math.min(150 * k, A.h * 0.22), 48 * k, 5);\n const lh = fit.size * 1.18, y0 = H / 2 - (fit.lines.length * lh) / 2;\n let wi = 0, marks = \"\", nMark = 0;\n const flags = [], boxes = [];\n fit.lines.forEach(function (line, li) {\n const lay = M.layout(line, Object.assign({}, fit.font, { x: W / 2, y: y0 + li * lh, anchor: \"middle\" }), \"words\");\n lay.units.forEach(function (u) { flags.push(ws[wi] && ws[wi].marked); boxes.push(u); wi++; });\n });\n const tMarks = tin + 0.35 + fit.lines.length * 0.12, markAt = [];\n boxes.forEach(function (u, j) {\n if (!flags[j]) return;\n const a = tMarks + nMark * 0.35;\n markAt[j] = a;\n const pr = $.E.out3($.seg(t, a, a + 0.32)), pad = fit.size * 0.1;\n if (pr > 0) marks += $.path(\"M\" + (u.x - pad) + \" \" + (u.y + fit.size * 0.18) + \"h\" + (u.w + 2 * pad) * pr + \"l\" + -pad * 0.4 + \" \" + fit.size * 0.86 + \"h\" + -(u.w + 2 * pad) * pr + \"Z\", { fill: c.accent2 });\n nMark++;\n });\n s += M.part(\"marks\", { at: tMarks, in: \"none\" }, marks);\n let base = 0;\n fit.lines.forEach(function (line, li) {\n const off = base;\n s += M.text(\"text\", Object.assign({ at: tin + li * 0.12, in: \"rise\", split: \"words\", stagger: 0.05, fillFor: function (u) { const j = off + u.index; return flags[j] && t >= markAt[j] + 0.1 ? $.on(c.accent2) : c.fg; } }, fit.font, { x: W / 2, y: y0 + li * lh, anchor: \"middle\", fill: c.fg }), line);\n base += line.split(/\\s+/).filter(Boolean).length;\n });\n return s;\n}\n","libs":[]},{"id":"stack","name":"Editorial stack","description":"Lines stack like a magazine cover — heavy capitals alternating with *italic* accent lines — sliding in from alternate sides between hairline rules.","category":"text","tags":["kinetic type","text","stack","editorial","magazine","serif","italic","headline"],"placement":"full","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"lines":{"type":"list","label":"Lines","of":"text","default":["Make","*things*","people","*remember*"],"maxItems":6,"description":"wrap a line in *stars* for the italic accent face"}},"parts":{"ground":"the brand's ground","lines":"the lines","rules":"the hairlines"},"beats":{"in":0.2},"pack":"type","ref":"type/stack","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const rows = (p.lines || []).map(function (l) { const m = /^\\*(.*)\\*$/.exec(String(l).trim()); return m ? { text: m[1], acc: true } : { text: $.case(String(l).trim()), acc: false }; }).filter(function (r) { return r.text; });\n if (!rows.length) return s;\n const maxH = (A.h * 0.92) / rows.length;\n const sized = rows.map(function (r) { return fitFont($, r.text, r.acc ? \"accent\" : \"display\", maxH / 1.0, A.w * 0.92, { ls: r.acc ? -0.01 : -0.04 }); });\n const total = sized.reduce(function (a, f) { return a + f.size * 0.98; }, 0);\n let y = H / 2 - total / 2;\n rows.forEach(function (r, i) {\n const f = sized[i];\n s += M.text(\"lines\", Object.assign({ at: tin + i * 0.16, in: \"slide\", from: i % 2 ? \"right\" : \"left\", distance: 60 }, f, { x: W / 2, y: y - f.size * 0.06, anchor: \"middle\", fill: r.acc ? c.accent : c.fg }), r.text);\n y += f.size * 0.98;\n if (i < rows.length - 1) s += M.part(\"rules\", { at: tin + i * 0.16 + 0.25, in: \"grow-x\", origin: \"center\", box: { x: A.x, y: y - 2 * k, w: A.w, h: 4 * k } }, $.rect(A.x + A.w * 0.2, y - 1.5 * k, A.w * 0.6, 3 * k, { fill: $.rgba(c.fg, 0.35) }));\n });\n return s;\n}\n","libs":[]}]},{"format":1,"id":"backdrops","name":"MONARC Backdrops","version":"1.0.0","description":"Grounds that move, to set titles, products or footage on: any of sixteen patterns, aurora, a Bauhaus field, a synthwave grid, lava blobs, a colour sweep, a starfield, bokeh and topographic lines.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/backdrops","style":"aurora","shared":"shared.js","items":["pattern","aurora","tiles","retro-grid","blobs","gradient","starfield","bokeh","topo"],"entries":[{"id":"pattern","name":"Pattern","description":"Any of the sixteen animated patterns as a full ground — mesh, halftone, dither, Bauhaus tiles, waves, rings, dots, confetti, pills, sunburst… — in the brand's colours or your own.","category":"scene","tags":["backdrop","background","loop","pattern","texture","motion background"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"kind":{"type":"choice","label":"Pattern","options":["mesh","halftone","dither","tiles","bauhaus","waves","rings","dots","plus","graph","hatch","stripes","checker","confetti","pills","sunburst"],"default":"mesh"},"strength":{"type":"number","label":"Strength","default":1,"min":0.2,"max":2,"step":0.1},"scale":{"type":"number","label":"Scale","default":1,"min":0.4,"max":3,"step":0.1},"speed":{"type":"number","label":"Speed","default":1,"min":0,"max":4,"step":0.1},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"},"color2":{"type":"color","label":"Second colour","default":"","description":"empty: the brand's colour"},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the ground","pattern":"the pattern"},"pack":"backdrops","ref":"backdrops/pattern","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, plain($, t));\n s += M.part(\"pattern\", { in: \"fade\", out: \"none\", dur: 0.6, vary: false }, $.pattern(t, p.kind, { a: p.strength, scale: p.scale, speed: p.speed, color: p.color || undefined, color2: p.color2 || undefined }));\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n","libs":[]},{"id":"aurora","name":"Aurora","description":"Soft blooms of the brand's colours drift and melt into each other under a fine grain — a calm, premium ground for titles.","category":"scene","tags":["backdrop","background","loop","aurora","mesh","gradient","glow","soft","premium"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"intensity":{"type":"number","label":"Intensity","default":1.2,"min":0.4,"max":2,"step":0.1},"speed":{"type":"number","label":"Speed","default":1,"min":0,"max":3,"step":0.1},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the ground","light":"the drifting light"},"pack":"backdrops","ref":"backdrops/aurora","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, plain($, t));\n let l = $.pattern(t, \"mesh\", { a: p.intensity, speed: p.speed * 1.4, scale: 1 });\n // A second, smaller bloom in the second accent crossing the frame.\n const x = W * (0.5 + 0.35 * Math.sin(t * 0.21 * p.speed)), y = H * (0.5 + 0.3 * Math.cos(t * 0.17 * p.speed));\n l += $.circle(x, y, Math.max(W, H) * 0.35, { fill: $.gradient([[0, c.accent2, 0.35 * p.intensity], [1, c.accent2, 0]], { radial: true }) });\n s += M.part(\"light\", { in: \"fade\", out: \"none\", dur: 0.8, vary: false }, l) + $.grain(t, { op: 0.06 });\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n","libs":[]},{"id":"tiles","name":"Bauhaus field","description":"A full field of Bauhaus tiles — quarter circles, half discs, discs, diamonds — turning in a diagonal wave, in the brand's colours.","category":"scene","tags":["backdrop","background","loop","bauhaus","tiles","geometric","shapes","grid","swiss"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"size":{"type":"number","label":"Tile size","default":1.4,"min":0.5,"max":3,"step":0.1},"speed":{"type":"number","label":"Speed","default":1,"min":0,"max":3,"step":0.1},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the ground","tiles":"the tiles"},"pack":"backdrops","ref":"backdrops/tiles","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, plain($, t));\n s += M.part(\"tiles\", { in: \"fade\", out: \"none\", dur: 0.5, vary: false }, $.pattern(t, \"tiles\", { a: 1, scale: p.size, speed: p.speed }));\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n","libs":[]},{"id":"retro-grid","name":"Retro grid","description":"Synthwave: a neon grid floor rushing toward you under a striped sun and a glowing horizon, stars twinkling above.","category":"scene","tags":["backdrop","background","loop","synthwave","retro","grid","80s","neon","vaporwave","sun"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"speed":{"type":"number","label":"Speed","default":1,"min":0,"max":3,"step":0.1},"sun":{"type":"boolean","label":"Sun","default":true},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the sky","sun":"the sun","floor":"the grid floor"},"pack":"backdrops","ref":"backdrops/retro-grid","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, plain($, t));\n const hz = H * 0.58, cx = W / 2;\n // Sky: the ground darkening upward, a glow on the horizon, stars.\n let sky = $.rect(0, 0, W, hz, { fill: $.gradient([[0, \"#000000\", 0.35], [1, c.accent, 0.28]], { angle: 180 }) });\n for (let i = 0; i < 60; i++) {\n const x = $.hash(i * 3.1) * W, y = $.hash(i * 7.3) * hz * 0.85, tw = 0.4 + 0.6 * Math.abs(Math.sin(t * (1 + $.hash(i)) + i));\n sky += $.circle(x, y, (1 + 2 * $.hash(i * 5.5)) * k, { fill: c.fg, op: 0.6 * tw });\n }\n s += M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, sky);\n if (p.sun) {\n const R = Math.min(W, H) * 0.26, sy = hz - R * 0.35, id = $.uid(\"sun\");\n let cuts = \"\";\n for (let i = 0; i < 6; i++) { const yy = sy + R * (0.05 + i * 0.16) + ((t * 20 * k * p.speed) % (R * 0.16)), hh = R * (0.02 + i * 0.018); cuts += '<rect x=\"' + (cx - R) + '\" y=\"' + yy + '\" width=\"' + 2 * R + '\" height=\"' + hh + '\" fill=\"#000\"/>'; }\n $.def('<mask id=\"' + id + '\" maskUnits=\"userSpaceOnUse\" x=\"' + (cx - R) + '\" y=\"' + (sy - R) + '\" width=\"' + 2 * R + '\" height=\"' + 2 * R + '\"><rect x=\"' + (cx - R) + '\" y=\"' + (sy - R) + '\" width=\"' + 2 * R + '\" height=\"' + 2 * R + '\" fill=\"#fff\"/>' + cuts + \"</mask>\");\n s += M.part(\"sun\", { in: \"rise\", distance: 60, box: { x: cx - R, y: sy - R, w: 2 * R, h: 2 * R } }, $.circle(cx, sy, R * 1.35, { fill: $.gradient([[0, c.accent2, 0.35], [1, c.accent2, 0]], { radial: true }) }) + '<g mask=\"url(#' + id + ')\">' + $.circle(cx, sy, R, { fill: $.gradient([c.accent2, c.accent], { angle: 180 }) }) + \"</g>\");\n }\n // The floor: lines converging on the centre of the horizon; cross lines rushing toward the viewer.\n let floor = $.rect(0, hz, W, H - hz, { fill: $.gradient([[0, c.accent, 0.25], [1, \"#000000\", 0.4]], { angle: 180 }) });\n const fh = H - hz, sw = 2.5 * k;\n for (let i = -16; i <= 16; i++) floor += $.line(cx + i * W * 0.012, hz, cx + i * W * 0.16, H + fh, { stroke: c.accent, sw: sw, op: 0.8 });\n const ph = (t * 0.55 * p.speed) % 1;\n for (let j = 0; j < 14; j++) {\n const z = (j + 1 - ph) / 14, y = hz + fh * Math.pow(z, 2.6);\n if (y > hz) floor += $.line(0, y, W, y, { stroke: c.accent, sw: sw * (0.4 + z), op: 0.25 + 0.7 * z });\n }\n floor += $.rect(0, hz - 2 * k, W, 4 * k, { fill: c.accent2 });\n s += M.part(\"floor\", { in: \"none\", out: \"none\", vary: false }, floor);\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n","libs":[]},{"id":"blobs","name":"Lava blobs","description":"Gooey blobs in the accents drift, merge and pull apart like a lava lamp.","category":"scene","tags":["backdrop","background","loop","blobs","lava","goo","liquid","metaballs","organic"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"count":{"type":"number","label":"Blobs","default":7,"min":3,"max":14,"step":1},"speed":{"type":"number","label":"Speed","default":1,"min":0,"max":3,"step":0.1},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the ground","blobs":"the blobs"},"pack":"backdrops","ref":"backdrops/blobs","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, plain($, t));\n const n = Math.round(p.count), R = Math.min(W, H) * 0.13, fid = $.uid(\"goo\");\n $.def('<filter id=\"' + fid + '\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"' + 30 * k + '\"/><feColorMatrix type=\"matrix\" values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 26 -11\"/></filter>');\n const layer = function (col, off) {\n let g = \"\";\n for (let i = 0; i < n; i++) {\n const sp = (0.15 + 0.2 * $.hash(i * 2.7 + off)) * p.speed, ph = $.hash(i * 9.1 + off) * 6.28;\n const x = W * (0.5 + 0.42 * Math.sin(t * sp + ph)), y = H * (0.5 + 0.4 * Math.sin(t * sp * 1.3 + ph * 1.7));\n g += $.circle(x, y, R * (0.6 + 0.8 * $.hash(i * 4.4 + off)), { fill: col });\n }\n return '<g filter=\"url(#' + fid + ')\">' + g + \"</g>\";\n };\n s += M.part(\"blobs\", { in: \"fade\", out: \"none\", dur: 0.6, vary: false }, layer(c.accent2, 3) + layer(c.accent, 0));\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n","libs":[]},{"id":"gradient","name":"Colour sweep","description":"A slow, rich gradient through the brand's colours that turns and shifts, with a light grain — the simplest premium ground.","category":"scene","tags":["backdrop","background","loop","gradient","sweep","colour","smooth","duotone"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"speed":{"type":"number","label":"Speed","default":1,"min":0,"max":3,"step":0.1},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the gradient"},"pack":"backdrops","ref":"backdrops/gradient","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const M = motion($, t);\n const a = 120 + t * 9 * p.speed, sh = 0.08 * Math.sin(t * 0.4 * p.speed);\n let g = $.rect(0, 0, W, H, { fill: $.gradient([[0, c.bg], [0.3 + sh, c.accent], [0.62 - sh, c.accent2], [1, c.bg]], { angle: a }) });\n g += $.rect(0, 0, W, H, { fill: $.gradient([[0, c.bg, 0.55], [0.5, c.bg, 0], [1, c.bg, 0.5]], { angle: a + 90 }) });\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, g) + $.grain(t, { op: 0.07 });\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n\n/** A backdrop has no moments: just soft air as it comes up. */\nfunction sound($) {\n return [{ at: 0, kind: \"swell\", level: 0.6 }];\n}\n","libs":[]},{"id":"starfield","name":"Starfield","description":"Stars stream toward you out of the dark — warp speed or a slow drift — some in the accents.","category":"scene","tags":["backdrop","background","loop","stars","space","warp","hyperspace","particles"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"speed":{"type":"number","label":"Speed","default":1,"min":0.1,"max":4,"step":0.1},"streaks":{"type":"boolean","label":"Streaks","default":true},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the ground","stars":"the stars"},"pack":"backdrops","ref":"backdrops/starfield","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, plain($, t));\n const n = 220, cx = W / 2, cy = H / 2, D = Math.hypot(W, H) * 0.6;\n let st = \"\";\n for (let i = 0; i < n; i++) {\n const a = $.hash(i * 1.37) * 6.2832, r0 = 0.02 + $.hash(i * 7.1) * 0.98;\n const z = ((r0 + t * 0.18 * p.speed * (0.6 + $.hash(i * 3.3))) % 1);\n const d = Math.pow(z, 2.2) * D, d0 = Math.pow(Math.max(0, z - 0.04 * p.speed), 2.2) * D;\n const x = cx + Math.cos(a) * d, y = cy + Math.sin(a) * d, col = i % 9 === 0 ? c.accent : i % 13 === 0 ? c.accent2 : c.fg;\n const sw = (0.8 + 3.2 * z) * k, op = Math.min(1, z * 2.4);\n st += p.streaks ? $.line(cx + Math.cos(a) * d0, cy + Math.sin(a) * d0, x, y, { stroke: col, sw: sw, op: op }) : $.circle(x, y, sw, { fill: col, op: op });\n }\n s += M.part(\"stars\", { in: \"fade\", out: \"none\", dur: 0.4, vary: false }, st);\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n","libs":[]},{"id":"bokeh","name":"Bokeh","description":"Soft out-of-focus lights in the brand's colours drift and breathe — a warm, filmic ground.","category":"scene","tags":["backdrop","background","loop","bokeh","lights","blur","soft","particles","film"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"count":{"type":"number","label":"Lights","default":26,"min":6,"max":60,"step":1},"speed":{"type":"number","label":"Speed","default":1,"min":0,"max":3,"step":0.1},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the ground","lights":"the lights"},"pack":"backdrops","ref":"backdrops/bokeh","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, plain($, t));\n let l = \"\";\n const pal = [c.accent, c.accent2, c.fg];\n for (let i = 0; i < Math.round(p.count); i++) {\n const r = Math.min(W, H) * (0.03 + 0.12 * Math.pow($.hash(i * 2.3), 2));\n const x = ($.hash(i * 5.1) * 1.2 - 0.1) * W + Math.sin(t * 0.3 * p.speed + i) * 40 * k, y = ($.hash(i * 8.7) * 1.2 - 0.1) * H - ((t * 14 * k * p.speed * (0.5 + $.hash(i))) % (H * 1.4)) + H * 0.2;\n const yy = ((y % (H * 1.4)) + H * 1.4) % (H * 1.4) - H * 0.2;\n const a = (0.18 + 0.35 * $.hash(i * 3.9)) * (0.75 + 0.25 * Math.sin(t * 1.3 + i * 2));\n const col = pal[i % 3];\n l += $.circle(x, yy, r, { fill: $.gradient([[0, col, a], [0.75, col, a * 0.8], [1, col, 0]], { radial: true }) });\n }\n s += M.part(\"lights\", { in: \"fade\", out: \"none\", dur: 0.6, vary: false }, l);\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n","libs":[]},{"id":"topo","name":"Topographic","description":"Contour lines like a topographic map, slowly shifting as if the land were breathing.","category":"scene","tags":["backdrop","background","loop","topographic","contour","lines","map","terrain","organic"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"lines":{"type":"number","label":"Lines","default":14,"min":6,"max":40,"step":1},"speed":{"type":"number","label":"Speed","default":1,"min":0,"max":3,"step":0.1},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the ground","lines":"the contour lines"},"pack":"backdrops","ref":"backdrops/topo","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, plain($, t));\n const n = Math.round(p.lines), peaks = [[0.3, 0.38, 0], [0.76, 0.66, 2.1]];\n const D = Math.hypot(W, H);\n let l = \"\";\n peaks.forEach(function (pk, pi) {\n const cx = W * pk[0], cy = H * pk[1];\n for (let j = 1; j <= n; j++) {\n const r = (j / n) * D * 0.55, pts = [];\n for (let q = 0; q < 40; q++) {\n const a = (q / 40) * 6.2832;\n const w = 1 + 0.16 * Math.sin(3 * a + pk[2] + t * 0.25 * p.speed) + 0.08 * Math.sin(5 * a - j * 0.3 + t * 0.4 * p.speed) + 0.05 * Math.sin(9 * a + j * 0.5);\n pts.push([cx + Math.cos(a) * r * w, cy + Math.sin(a) * r * w * 0.8]);\n }\n let d = \"M\" + (pts[0][0] + pts[39][0]) / 2 + \" \" + (pts[0][1] + pts[39][1]) / 2;\n for (let q = 0; q < 40; q++) { const a1 = pts[q], b1 = pts[(q + 1) % 40]; d += \"Q\" + a1[0] + \" \" + a1[1] + \" \" + (a1[0] + b1[0]) / 2 + \" \" + (a1[1] + b1[1]) / 2; }\n l += $.path(d + \"Z\", { stroke: j % 5 === 0 ? c.accent : $.rgba(c.fg, 0.18), sw: (j % 5 === 0 ? 3 : 1.8) * k, op: pi === 0 ? 1 : 0.65 });\n }\n });\n s += M.part(\"lines\", { in: \"fade\", out: \"none\", dur: 0.6, vary: false }, l);\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n","libs":[]}]},{"format":1,"id":"promo","name":"MONARC Promo","version":"1.0.0","description":"Offers that sell: a sale starburst, a price card that counts down, a coupon with a code to copy, an offer countdown, a feature checklist, a call to action with a click, a launch title and crossing tape ribbons.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/promo","style":"candy","shared":"shared.js","items":["sale-badge","price-card","coupon","offer-countdown","features","cta","launch","ribbon"],"entries":[{"id":"sale-badge","name":"Sale badge","description":"A starburst badge slams in spinning — 50% OFF — then wobbles while a dashed ring turns inside it.","category":"cta","tags":["promo","sale","offer","badge","starburst","discount","percent","off","sticker"],"placement":"overlay","duration":{"default":3,"min":1,"max":15},"aspects":["16:9","9:16","1:1","4:5"],"props":{"big":{"type":"text","label":"Big","default":"50%","maxLength":6},"small":{"type":"text","label":"Small","default":"Off","maxLength":12},"x":{"type":"number","label":"Centre across (%)","default":78,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Centre down (%)","default":30,"min":0,"max":100,"step":1},"size":{"type":"number","label":"Size","default":100,"min":40,"max":200,"step":5}},"parts":{"badge":"the starburst","words":"the words"},"beats":{"in":0.1},"pack":"promo","ref":"promo/sale-badge","code":"// Promo pieces: offers, prices, codes and calls to action, in the brand's surface (bold styles get ink\n// outlines and hard shadows).\n/** True when the brand draws ink outlines and hard shadows. */\nfunction boldSurface($) { return $.style.surface === \"bold\"; }\n/** A filled shape path with the brand's outline and hard shadow when it has them. */\nfunction shapeFill($, d, fill) {\n const k = $.k;\n if (!boldSurface($)) return $.path(d, { fill: fill, shadow: \"md\" });\n return '<g transform=\"translate(' + 8 * k + \" \" + 8 * k + ')\">' + $.path(d, { fill: $.c.ink }) + \"</g>\" + $.path(d, { fill: fill, stroke: $.c.ink, sw: 4 * k });\n}\n/** Money: \"$1,299\" or \"1,299 €\" (currency after when it is a word or ends with a space). */\nfunction money(cur, n, dec) {\n const v = Number(n).toLocaleString(\"en-US\", { minimumFractionDigits: dec, maximumFractionDigits: dec });\n return /^[A-Za-z]{2,}$/.test(cur) ? v + \" \" + cur : cur + v;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n let s = \"\";\n const R = Math.min(W, H) * 0.17 * p.size / 100, cx = Math.min(W - R * 1.1, Math.max(R * 1.1, W * p.x / 100)), cy = Math.min(H - R * 1.1, Math.max(R * 1.1, H * p.y / 100));\n let d = \"\";\n for (let i = 0; i < 36; i++) { const a = (i / 36) * 6.2832, r = i % 2 ? R * 0.86 : R; d += (i ? \"L\" : \"M\") + (cx + Math.cos(a) * r) + \" \" + (cy + Math.sin(a) * r); }\n const wob = Math.sin((t - tin) * 3) * 4, spin = (t - tin) * 18;\n let badge = shapeFill($, d + \"Z\", c.accent);\n badge += $.circle(cx, cy, R * 0.7, { stroke: $.on(c.accent), sw: 3 * k, dash: 10 * k + \" \" + 8 * k, transform: \"rotate(\" + spin + \" \" + cx + \" \" + cy + \")\", op: 0.6 });\n const big = $.case(p.big), small = $.case(p.small);\n const bf = fitFont($, big, \"display\", R * 0.66, R * 1.3, { ls: -0.04 }), sf = fitFont($, small, \"display\", R * 0.3, R * 1.1, { ls: 0.02 });\n const words = $.text(big, Object.assign({}, bf, { x: cx, y: cy - bf.size * 0.7, anchor: \"middle\", fill: $.on(c.accent) })) + (small ? $.text(small, Object.assign({}, sf, { x: cx, y: cy + bf.size * 0.32, anchor: \"middle\", fill: $.on(c.accent) })) : \"\");\n const box = { x: cx - R, y: cy - R, w: 2 * R, h: 2 * R };\n s += '<g transform=\"rotate(' + wob + \" \" + cx + \" \" + cy + ')\">' + M.part(\"badge\", { at: tin, in: \"spin\", box: box }, badge) + M.part(\"words\", { at: tin + 0.3, in: \"pop\", box: box }, words) + \"</g>\";\n return s;\n}\n","libs":[]},{"id":"price-card","name":"Price card","description":"A product card: the old price is struck through, the new price counts down to it, a saving chip pops and the button gets pressed.","category":"cta","tags":["promo","sale","offer","price","product","card","discount","shop","ecommerce"],"placement":"overlay","duration":{"default":5,"min":2,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"kicker":{"type":"text","label":"Kicker","default":"Weekend deal","maxLength":24},"name":{"type":"text","label":"Product","default":"Studio headphones","maxLength":32},"was":{"type":"number","label":"Was","default":199,"min":0,"max":10000000},"now":{"type":"number","label":"Now","default":149,"min":0,"max":10000000},"currency":{"type":"text","label":"Currency","default":"$","maxLength":4},"decimals":{"type":"number","label":"Decimals","default":0,"min":0,"max":2,"step":1},"cta":{"type":"text","label":"Button","default":"Shop now","maxLength":16},"added":{"type":"text","label":"After the click","default":"Added","maxLength":16},"position":{"type":"choice","label":"Position","options":["center","left","right"],"default":"center"}},"parts":{"card":"the card","price":"the prices","save":"the saving chip","button":"the button and cursor"},"beats":{"in":0.1},"pack":"promo","ref":"promo/price-card","code":"// Promo pieces: offers, prices, codes and calls to action, in the brand's surface (bold styles get ink\n// outlines and hard shadows).\n/** True when the brand draws ink outlines and hard shadows. */\nfunction boldSurface($) { return $.style.surface === \"bold\"; }\n/** A filled shape path with the brand's outline and hard shadow when it has them. */\nfunction shapeFill($, d, fill) {\n const k = $.k;\n if (!boldSurface($)) return $.path(d, { fill: fill, shadow: \"md\" });\n return '<g transform=\"translate(' + 8 * k + \" \" + 8 * k + ')\">' + $.path(d, { fill: $.c.ink }) + \"</g>\" + $.path(d, { fill: fill, stroke: $.c.ink, sw: 4 * k });\n}\n/** Money: \"$1,299\" or \"1,299 €\" (currency after when it is a word or ends with a space). */\nfunction money(cur, n, dec) {\n const v = Number(n).toLocaleString(\"en-US\", { minimumFractionDigits: dec, maximumFractionDigits: dec });\n return /^[A-Za-z]{2,}$/.test(cur) ? v + \" \" + cur : cur + v;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n let s = \"\";\n const cw = Math.min(A.w * (A.wide ? 0.42 : 0.9), 760 * k), pad = 44 * k;\n const nf = Object.assign({}, $.font(\"display\"), { size: 58 * k, ls: -0.02 });\n const nameLines = $.wrap($.case(p.name), cw - pad * 2, nf).slice(0, 2);\n const pf = Object.assign({}, $.font(\"display\"), { size: 120 * k, ls: -0.03 }), of = Object.assign({}, $.font(\"text\"), { size: 44 * k });\n const ch = pad * 2 + 40 * k + 20 * k + nameLines.length * nf.size * 1.08 + 24 * k + pf.size * 1.05 + 30 * k + 96 * k;\n const cx = p.position === \"left\" ? A.x : p.position === \"right\" ? A.x + A.w - cw : W / 2 - cw / 2, cy = H / 2 - ch / 2;\n let y = cy + pad;\n let card = $.card(cx, cy, cw, ch, { t: t });\n card += $.chip($.case(p.kicker), cx + pad, y, { size: 26 * k, fill: c.accent2, color: $.on(c.accent2) });\n y += 60 * k;\n nameLines.forEach(function (l, i) { card += $.text(l, Object.assign({}, nf, { x: cx + pad, y: y + i * nf.size * 1.08, fill: c.fg })); });\n y += nameLines.length * nf.size * 1.08 + 24 * k;\n s += M.part(\"card\", { at: tin, in: \"rise\", distance: 40, box: { x: cx, y: cy, w: cw, h: ch } }, card);\n // The price counts down from the old one to the new.\n const tc = tin + 0.7, q = $.E.out3($.seg(t, tc, tc + 0.9));\n const nowV = $.lerp(p.was, p.now, q), nowS = money(p.currency, p.decimals > 0 ? nowV : Math.round(nowV), p.decimals);\n const wasS = money(p.currency, p.was, p.decimals), ww = $.measure(wasS, of);\n let price = $.text(nowS, Object.assign({}, pf, { x: cx + pad, y: y, fill: c.fg }));\n const px = cx + pad + $.measure(money(p.currency, p.now, p.decimals), pf) + 26 * k, py = y + pf.size * 0.5 - of.size * 0.5;\n price += $.text(wasS, Object.assign({}, of, { x: px, y: py, fill: c.mute }));\n price += $.strike(t, tc, px - 4 * k, py + of.size * 0.55, px + ww + 4 * k, py + of.size * 0.5, { color: c.red, w: 6 * k });\n s += M.part(\"price\", { at: tin + 0.3, in: \"fade\" }, price);\n const save = p.was > 0 ? Math.round((1 - p.now / p.was) * 100) : 0;\n if (save > 0) {\n // The saving sits in the card's top-right corner, clear of both prices.\n const st = \"-\" + save + \"%\", sz = $.chip.size(st, { size: 34 * k }), sx = cx + cw - pad - sz.w, sy = cy + pad - 4 * k;\n s += M.part(\"save\", { at: tc + 0.9, in: \"pop\", box: { x: sx, y: sy, w: sz.w, h: sz.h } }, $.chip(st, sx, sy, { size: 34 * k, fill: c.accent, color: $.on(c.accent) }));\n }\n y += pf.size * 1.05 + 30 * k;\n const press = tc + 1.9;\n let btn = $.button(t, $.case(p.cta), cx + pad, y, { size: 38 * k, fill: c.accent, icon: \"cart\", pressAt: press, done: $.case(p.added || \"Added\"), anchor: \"start\" });\n const bx = cx + pad + 160 * k, by = y + 40 * k;\n const glide = $.E.inOut3($.seg(t, press - 0.8, press - 0.05));\n btn += $.cursor($.lerp(W * 0.9, bx, glide), $.lerp(H * 1.05, by, glide), { press: $.bump(t, press, 0.05, 0.2), ripple: $.seg(t, press, press + 0.5) });\n s += M.part(\"button\", { at: tin + 0.5, in: \"rise\", distance: 20, box: { x: cx + pad, y: y, w: cw - 2 * pad, h: 96 * k } }, btn);\n return s;\n}\n","libs":[]},{"id":"coupon","name":"Coupon","description":"A ticket with a perforated tear line tears in: the deal on the stub, the code in a dashed box — then a click copies it.","category":"cta","tags":["promo","sale","offer","coupon","code","voucher","ticket","promo code","discount"],"placement":"overlay","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"deal":{"type":"text","label":"Deal","default":"20% off","maxLength":12},"note":{"type":"text","label":"Note","default":"your first order","maxLength":32},"code":{"type":"text","label":"Code","default":"SAVE20","maxLength":16},"copied":{"type":"text","label":"After the click","default":"Copied","maxLength":12}},"parts":{"ticket":"the ticket","code":"the code box","copied":"the copied chip"},"beats":{"in":0.1},"pack":"promo","ref":"promo/coupon","code":"// Promo pieces: offers, prices, codes and calls to action, in the brand's surface (bold styles get ink\n// outlines and hard shadows).\n/** True when the brand draws ink outlines and hard shadows. */\nfunction boldSurface($) { return $.style.surface === \"bold\"; }\n/** A filled shape path with the brand's outline and hard shadow when it has them. */\nfunction shapeFill($, d, fill) {\n const k = $.k;\n if (!boldSurface($)) return $.path(d, { fill: fill, shadow: \"md\" });\n return '<g transform=\"translate(' + 8 * k + \" \" + 8 * k + ')\">' + $.path(d, { fill: $.c.ink }) + \"</g>\" + $.path(d, { fill: fill, stroke: $.c.ink, sw: 4 * k });\n}\n/** Money: \"$1,299\" or \"1,299 €\" (currency after when it is a word or ends with a space). */\nfunction money(cur, n, dec) {\n const v = Number(n).toLocaleString(\"en-US\", { minimumFractionDigits: dec, maximumFractionDigits: dec });\n return /^[A-Za-z]{2,}$/.test(cur) ? v + \" \" + cur : cur + v;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n let s = \"\";\n const tw = Math.min(A.w * 0.94, 1200 * k), th = Math.min(A.h * 0.42, tw * 0.36), x = W / 2 - tw / 2, y = H / 2 - th / 2;\n const stub = tw * 0.42, n = 22 * k, r = Math.min(24 * k, th * 0.08);\n // The ticket: rounded corners, a half-round notch top and bottom at the tear line.\n const sx = x + stub;\n const d = \"M\" + (x + r) + \" \" + y + \"H\" + (sx - n) + \"A\" + n + \" \" + n + \" 0 0 0 \" + (sx + n) + \" \" + y + \"H\" + (x + tw - r) + \"Q\" + (x + tw) + \" \" + y + \" \" + (x + tw) + \" \" + (y + r) + \"V\" + (y + th - r) + \"Q\" + (x + tw) + \" \" + (y + th) + \" \" + (x + tw - r) + \" \" + (y + th) + \"H\" + (sx + n) + \"A\" + n + \" \" + n + \" 0 0 0 \" + (sx - n) + \" \" + (y + th) + \"H\" + (x + r) + \"Q\" + x + \" \" + (y + th) + \" \" + x + \" \" + (y + th - r) + \"V\" + (y + r) + \"Q\" + x + \" \" + y + \" \" + (x + r) + \" \" + y + \"Z\";\n let tk = shapeFill($, d, c.surface) + $.clip(x, y, stub, th, 0, $.path(d, { fill: c.accent }));\n tk += $.line(sx, y + n + 8 * k, sx, y + th - n - 8 * k, { stroke: $.rgba(c.fg, 0.35), sw: 3 * k, dash: 10 * k + \" \" + 10 * k, cap: \"butt\" });\n const deal = $.case(p.deal), df = fitFont($, deal, \"display\", th * 0.36, stub * 0.84, { ls: -0.03 });\n const nf = fitFont($, p.note, \"text\", Math.max(26 * k, th * 0.1), stub * 0.84, {});\n tk += $.text(deal, Object.assign({}, df, { x: x + stub / 2, y: y + th / 2 - df.size * 0.72, anchor: \"middle\", fill: $.on(c.accent) }));\n if (p.note) tk += $.text(p.note, Object.assign({}, nf, { x: x + stub / 2, y: y + th / 2 + df.size * 0.38, anchor: \"middle\", fill: $.rgba($.on(c.accent), 0.8) }));\n s += M.part(\"ticket\", { at: tin, in: \"stretch\", box: { x: x, y: y, w: tw, h: th } }, tk);\n // The code in a dashed box on the right.\n const code = String(p.code || \"\").toUpperCase(), cf = fitFont($, code, \"mono\", th * 0.22, (tw - stub) * 0.7, { ls: 0.08 });\n const lf = Object.assign({}, $.font(\"text\"), { size: Math.max(24 * k, th * 0.085) });\n const rx = sx + (tw - stub) / 2, bw = $.measure(code, cf) + 70 * k, bh = cf.size * 1.7, by = y + th / 2 - bh / 2 + lf.size * 0.6;\n const copyAt = tin + 1.5;\n const lit = t >= copyAt;\n let cd = $.text(\"Use code\", Object.assign({}, lf, { x: rx, y: by - lf.size * 1.6, anchor: \"middle\", fill: c.mute }));\n cd += $.rect(rx - bw / 2, by, bw, bh, { r: 12 * k, fill: lit ? $.rgba(c.accent, 0.14) : \"none\", stroke: c.accent, sw: 4 * k, dash: 14 * k + \" \" + 9 * k });\n cd += $.text(code, Object.assign({}, cf, { x: rx, y: by + (bh - cf.size) / 2 - cf.size * 0.06, anchor: \"middle\", fill: c.fg }));\n const glide = $.E.inOut3($.seg(t, copyAt - 0.7, copyAt - 0.05));\n cd += $.cursor($.lerp(x + tw + 60 * k, rx + bw * 0.2, glide), $.lerp(y + th + 80 * k, by + bh * 0.6, glide), { press: $.bump(t, copyAt, 0.05, 0.2), ripple: $.seg(t, copyAt, copyAt + 0.5) });\n s += M.part(\"code\", { at: tin + 0.4, in: \"fade\" }, cd);\n if (p.copied) {\n const ct = $.case(p.copied), co = { size: 28 * k, fill: c.green, color: $.on(c.green), anchor: \"middle\", icon: \"check\" }, cz = $.chip.size(ct, co);\n s += M.part(\"copied\", { at: copyAt + 0.1, in: \"pop\", box: { x: rx - cz.w / 2, y: by + bh + 18 * k, w: cz.w, h: cz.h } }, $.chip(ct, rx, by + bh + 18 * k, co));\n }\n return s;\n}\n","libs":[]},{"id":"offer-countdown","name":"Offer countdown","description":"Offer ends in — days, hours, minutes and seconds in tiles, the seconds rolling down live. Sits low in the frame.","category":"timer","tags":["promo","sale","offer","countdown","timer","deadline","urgency","ends","limited"],"placement":"overlay","duration":{"default":5,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"label":{"type":"text","label":"Label","default":"Offer ends in","maxLength":28},"hours":{"type":"number","label":"Hours left","default":26,"min":0,"max":9999},"minutes":{"type":"number","label":"And minutes","default":41,"min":0,"max":59,"step":1},"days":{"type":"boolean","label":"Show days","default":true}},"parts":{"panel":"the panel","digits":"the digit tiles"},"beats":{"in":0.1},"pack":"promo","ref":"promo/offer-countdown","code":"// Promo pieces: offers, prices, codes and calls to action, in the brand's surface (bold styles get ink\n// outlines and hard shadows).\n/** True when the brand draws ink outlines and hard shadows. */\nfunction boldSurface($) { return $.style.surface === \"bold\"; }\n/** A filled shape path with the brand's outline and hard shadow when it has them. */\nfunction shapeFill($, d, fill) {\n const k = $.k;\n if (!boldSurface($)) return $.path(d, { fill: fill, shadow: \"md\" });\n return '<g transform=\"translate(' + 8 * k + \" \" + 8 * k + ')\">' + $.path(d, { fill: $.c.ink }) + \"</g>\" + $.path(d, { fill: fill, stroke: $.c.ink, sw: 4 * k });\n}\n/** Money: \"$1,299\" or \"1,299 €\" (currency after when it is a word or ends with a space). */\nfunction money(cur, n, dec) {\n const v = Number(n).toLocaleString(\"en-US\", { minimumFractionDigits: dec, maximumFractionDigits: dec });\n return /^[A-Za-z]{2,}$/.test(cur) ? v + \" \" + cur : cur + v;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n let s = \"\";\n const start = (p.hours * 3600 + p.minutes * 60 + 17) - Math.max(0, t - tin);\n const left = Math.max(0, start);\n const parts = p.days ? [[Math.floor(left / 86400), \"days\"], [Math.floor(left / 3600) % 24, \"hrs\"], [Math.floor(left / 60) % 60, \"min\"], [Math.floor(left) % 60, \"sec\"]] : [[Math.floor(left / 3600), \"hrs\"], [Math.floor(left / 60) % 60, \"min\"], [Math.floor(left) % 60, \"sec\"]];\n const n = parts.length, lab = $.case(p.label);\n const tileW = Math.min(A.w * 0.9 / (n * 1.25), 190 * k), tileH = tileW * 1.05, gap = tileW * 0.25;\n const lf = Object.assign({}, $.font(\"display\"), { size: tileW * 0.3 }), df = Object.assign({}, $.font(\"mono\"), { size: tileW * 0.58, weight: 700 }), uf = Object.assign({}, $.font(\"text\"), { size: tileW * 0.16 });\n const rowW = n * tileW + (n - 1) * gap, pw = Math.max(rowW, $.measure(lab, lf)) + 80 * k, ph = 40 * k + lf.size * 1.3 + tileH + uf.size * 1.8 + 30 * k;\n const px = W / 2 - pw / 2, py = A.y + A.h - ph;\n s += M.part(\"panel\", { at: tin, in: \"rise\", distance: 40, box: { x: px, y: py, w: pw, h: ph } }, $.card(px, py, pw, ph, { t: t }) + $.text(lab, Object.assign({}, lf, { x: W / 2, y: py + 34 * k, anchor: \"middle\", fill: c.fg })));\n const ty = py + 34 * k + lf.size * 1.3;\n let digits = \"\";\n parts.forEach(function (q, i) {\n const x = W / 2 - rowW / 2 + i * (tileW + gap), last = i === n - 1;\n const v = String(q[0]).padStart(2, \"0\");\n // The seconds roll: the new value drops in over the first quarter of each second.\n const fr = (start % 1 + 1) % 1, roll = last ? $.E.out3($.clamp((1 - fr) / 0.25)) : 1;\n let inner = $.text(v, Object.assign({}, df, { x: x + tileW / 2, y: ty + (tileH - df.size) / 2 - df.size * 0.08 - (1 - roll) * tileH * 0.5, anchor: \"middle\", fill: last ? $.on(c.accent) : c.fg, op: roll }));\n digits += $.rect(x, ty, tileW, tileH, { r: 14 * k, fill: last ? c.accent : $.rgba(c.fg, 0.08) }) + $.clip(x, ty, tileW, tileH, 14 * k, inner);\n digits += $.text(q[1], Object.assign({}, uf, { x: x + tileW / 2, y: ty + tileH + uf.size * 0.5, anchor: \"middle\", fill: c.mute }));\n if (i < n - 1) digits += $.text(\":\", Object.assign({}, df, { x: x + tileW + gap / 2, y: ty + (tileH - df.size) / 2 - df.size * 0.1, anchor: \"middle\", fill: c.mute }));\n });\n s += M.part(\"digits\", { at: tin + 0.25, in: \"rise\", split: \"none\", distance: 20, box: { x: W / 2 - rowW / 2, y: ty, w: rowW, h: tileH } }, digits);\n return s;\n}\n","libs":[]},{"id":"features","name":"Feature list","description":"A title, then features tick in one by one — a check draws in its circle and the line slides across.","category":"list","tags":["promo","sale","offer","features","checklist","benefits","list","ticks","included"],"placement":"full","duration":{"default":5,"min":2,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"title":{"type":"text","label":"Title","default":"Everything you need","maxLength":40},"features":{"type":"list","label":"Features","of":"text","default":["Unlimited projects","4K exports","Brand styles","Team sharing"],"maxItems":6}},"parts":{"ground":"the ground","title":"the title","rows":"the feature rows"},"beats":{"in":0.1},"pack":"promo","ref":"promo/features","code":"// Promo pieces: offers, prices, codes and calls to action, in the brand's surface (bold styles get ink\n// outlines and hard shadows).\n/** True when the brand draws ink outlines and hard shadows. */\nfunction boldSurface($) { return $.style.surface === \"bold\"; }\n/** A filled shape path with the brand's outline and hard shadow when it has them. */\nfunction shapeFill($, d, fill) {\n const k = $.k;\n if (!boldSurface($)) return $.path(d, { fill: fill, shadow: \"md\" });\n return '<g transform=\"translate(' + 8 * k + \" \" + 8 * k + ')\">' + $.path(d, { fill: $.c.ink }) + \"</g>\" + $.path(d, { fill: fill, stroke: $.c.ink, sw: 4 * k });\n}\n/** Money: \"$1,299\" or \"1,299 €\" (currency after when it is a word or ends with a space). */\nfunction money(cur, n, dec) {\n const v = Number(n).toLocaleString(\"en-US\", { minimumFractionDigits: dec, maximumFractionDigits: dec });\n return /^[A-Za-z]{2,}$/.test(cur) ? v + \" \" + cur : cur + v;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n let s = \"\";\n s += M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const rows = (p.features || []).filter(Boolean).slice(0, 6);\n const tf = fitLines($, $.case(p.title), Object.assign({}, $.font(\"display\"), { ls: -0.03 }), A.w * 0.9, Math.min(120 * k, A.h * 0.16), 50 * k, 2);\n const rf = Object.assign({}, $.font(\"text\"), { size: Math.min(56 * k, (A.h * 0.62) / Math.max(1, rows.length) / 1.9), weight: 600 });\n const rowH = rf.size * 1.9, listW = Math.min(A.w * 0.9, widest($, rows, rf) + rf.size * 2.2);\n const total = tf.lines.length * tf.size * 1.05 + 50 * k + rows.length * rowH;\n let y = H / 2 - total / 2;\n tf.lines.forEach(function (l, i) { s += M.text(\"title\", Object.assign({ at: tin + i * 0.1, in: \"rise\" }, tf.font, { x: W / 2, y: y + i * tf.size * 1.05, anchor: \"middle\", fill: c.fg }), l); });\n y += tf.lines.length * tf.size * 1.05 + 50 * k;\n const x0 = W / 2 - listW / 2, r = rf.size * 0.62;\n rows.forEach(function (row, i) {\n const a = tin + 0.5 + i * 0.32, cy = y + i * rowH + rowH / 2;\n const ck = $.E.out3($.seg(t, a + 0.15, a + 0.45));\n let g = $.circle(x0 + r, cy, r, { fill: c.accent }) + (ck > 0 ? $.path(\"M\" + (x0 + r * 0.55) + \" \" + cy + \"L\" + (x0 + r * 0.88) + \" \" + (cy + r * 0.32) + \"L\" + (x0 + r * 1.48) + \" \" + (cy - r * 0.36), { stroke: $.on(c.accent), sw: r * 0.24, draw: ck }) : \"\");\n g += $.text(row, Object.assign({}, rf, { x: x0 + r * 2 + rf.size * 0.5, y: cy - rf.size * 0.56, fill: c.fg, maxW: listW - r * 3 }));\n if (i < rows.length - 1) g += $.rect(x0, y + (i + 1) * rowH - 1 * k, listW, 2 * k, { fill: c.line });\n s += M.part(\"rows\", { at: a, in: \"slide\", from: \"left\", distance: 30, box: { x: x0, y: cy - rowH / 2, w: listW, h: rowH } }, g);\n });\n return s;\n}\n","libs":[]},{"id":"cta","name":"Call to action","description":"A short line, then a big button — a cursor glides on, presses it with a ripple, and it flips to the done label.","category":"cta","tags":["promo","sale","offer","cta","button","click","shop now","sign up","download","link in bio"],"placement":"overlay","duration":{"default":4,"min":1.5,"max":15},"aspects":["16:9","9:16","1:1","4:5"],"props":{"line":{"type":"text","label":"Line","default":"Ready when you are","maxLength":40,"description":"empty for none"},"button":{"type":"text","label":"Button","default":"Get started","maxLength":20},"done":{"type":"text","label":"After the click","default":"You're in","maxLength":20},"icon":{"type":"icon","label":"Icon","default":"arrowRight"},"position":{"type":"choice","label":"Position","options":["center","bottom"],"default":"center"}},"parts":{"line":"the line","button":"the button","cursor":"the cursor"},"beats":{"in":0.1},"pack":"promo","ref":"promo/cta","code":"// Promo pieces: offers, prices, codes and calls to action, in the brand's surface (bold styles get ink\n// outlines and hard shadows).\n/** True when the brand draws ink outlines and hard shadows. */\nfunction boldSurface($) { return $.style.surface === \"bold\"; }\n/** A filled shape path with the brand's outline and hard shadow when it has them. */\nfunction shapeFill($, d, fill) {\n const k = $.k;\n if (!boldSurface($)) return $.path(d, { fill: fill, shadow: \"md\" });\n return '<g transform=\"translate(' + 8 * k + \" \" + 8 * k + ')\">' + $.path(d, { fill: $.c.ink }) + \"</g>\" + $.path(d, { fill: fill, stroke: $.c.ink, sw: 4 * k });\n}\n/** Money: \"$1,299\" or \"1,299 €\" (currency after when it is a word or ends with a space). */\nfunction money(cur, n, dec) {\n const v = Number(n).toLocaleString(\"en-US\", { minimumFractionDigits: dec, maximumFractionDigits: dec });\n return /^[A-Za-z]{2,}$/.test(cur) ? v + \" \" + cur : cur + v;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n let s = \"\";\n const size = Math.min(64 * k, A.w / 14);\n const by = p.position === \"bottom\" ? A.y + A.h - size * 2.4 : H / 2 + (p.line ? size * 0.4 : -size);\n if (p.line) {\n const lf = fitFont($, $.case(p.line), \"display\", size * 1.5, A.w * 0.9, { ls: -0.03 });\n s += M.text(\"line\", Object.assign({ at: tin, in: \"rise\", split: \"words\" }, lf, { x: W / 2, y: by - lf.size * 1.6, anchor: \"middle\", fill: c.fg }), $.case(p.line));\n }\n const press = tin + 1.5;\n s += M.part(\"button\", { at: tin + 0.3, in: \"pop\", box: { x: W / 2 - size * 4, y: by, w: size * 8, h: size * 2 } }, $.button(t, $.case(p.button), W / 2, by, { size: size, fill: c.accent, icon: p.icon || \"arrowRight\", pressAt: press, done: $.case(p.done || p.button), anchor: \"middle\" }));\n const glide = $.E.inOut3($.seg(t, press - 0.75, press - 0.05)), bx = W / 2 + size * 1.2, byy = by + size * 1.1;\n s += M.part(\"cursor\", { at: press - 0.8, in: \"fade\", dur: 0.2 }, $.cursor($.lerp(W * 0.86, bx, glide), $.lerp(H * 0.98, byy, glide), { press: $.bump(t, press, 0.05, 0.2), ripple: $.seg(t, press, press + 0.5), scale: 2.6 * k }));\n return s;\n}\n","libs":[]},{"id":"launch","name":"Launch","description":"A launch title: a NEW sticker spins on, the product name lands huge over the brand's pattern, then the tagline and when it is out.","category":"title","tags":["promo","sale","offer","launch","new","product","reveal","announcement","drop"],"placement":"full","duration":{"default":5,"min":2,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"badge":{"type":"text","label":"Sticker","default":"New","maxLength":8},"name":{"type":"text","label":"Name","default":"Aero Pro","maxLength":28},"tagline":{"type":"text","label":"Tagline","default":"Lighter than anything we've made","maxLength":64},"when":{"type":"text","label":"When","default":"Available now","maxLength":28,"description":"empty for none"}},"parts":{"ground":"the ground","badge":"the sticker","name":"the name","tagline":"the tagline","when":"the availability chip"},"beats":{"in":0.1},"pack":"promo","ref":"promo/launch","code":"// Promo pieces: offers, prices, codes and calls to action, in the brand's surface (bold styles get ink\n// outlines and hard shadows).\n/** True when the brand draws ink outlines and hard shadows. */\nfunction boldSurface($) { return $.style.surface === \"bold\"; }\n/** A filled shape path with the brand's outline and hard shadow when it has them. */\nfunction shapeFill($, d, fill) {\n const k = $.k;\n if (!boldSurface($)) return $.path(d, { fill: fill, shadow: \"md\" });\n return '<g transform=\"translate(' + 8 * k + \" \" + 8 * k + ')\">' + $.path(d, { fill: $.c.ink }) + \"</g>\" + $.path(d, { fill: fill, stroke: $.c.ink, sw: 4 * k });\n}\n/** Money: \"$1,299\" or \"1,299 €\" (currency after when it is a word or ends with a space). */\nfunction money(cur, n, dec) {\n const v = Number(n).toLocaleString(\"en-US\", { minimumFractionDigits: dec, maximumFractionDigits: dec });\n return /^[A-Za-z]{2,}$/.test(cur) ? v + \" \" + cur : cur + v;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n let s = \"\";\n s += M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const fit = fitLines($, $.case(p.name), Object.assign({}, $.font(\"display\"), { ls: -0.045 }), A.w * 0.94, Math.min(A.h * 0.36, 300 * k), 80 * k, 2);\n const tf = Object.assign({}, $.font(\"text\"), { size: Math.max(30 * k, Math.min(48 * k, fit.size * 0.2)) });\n const tl = p.tagline ? $.wrap(p.tagline, A.w * 0.8, tf).slice(0, 2) : [];\n const total = fit.lines.length * fit.size * 0.98 + (tl.length ? 34 * k + tl.length * tf.size * 1.3 : 0) + (p.when ? 90 * k : 0);\n let y = H / 2 - total / 2;\n const w = widest($, fit.lines, fit.font);\n fit.lines.forEach(function (l, i) { s += M.text(\"name\", Object.assign({ at: tin + 0.35 + i * 0.12, in: \"push\", split: \"letters\", stagger: 0.03 }, fit.font, { x: W / 2, y: y + i * fit.size * 0.98, anchor: \"middle\", fill: c.fg }), l); });\n // The sticker sits on the name's top-right corner.\n const R = Math.max(56 * k, fit.size * 0.42), bx = Math.min(W - R - 20 * k, W / 2 + w / 2 + R * 0.2), by = Math.max(R + 20 * k, y - R * 0.1);\n const bt = $.case(p.badge), bf = fitFont($, bt, \"display\", R * 0.6, R * 1.5, { ls: 0.02 });\n s += M.part(\"badge\", { at: tin, in: \"spin\", box: { x: bx - R, y: by - R, w: 2 * R, h: 2 * R } }, '<g transform=\"rotate(-14 ' + bx + \" \" + by + ')\">' + $.circle(bx, by, R, { fill: c.accent2, stroke: boldSurface($) ? c.ink : undefined, sw: 4 * k, shadow: boldSurface($) ? \"hard\" : \"md\" }) + $.text(bt, Object.assign({}, bf, { x: bx, y: by - bf.size * 0.56, anchor: \"middle\", fill: $.on(c.accent2) })) + \"</g>\");\n y += fit.lines.length * fit.size * 0.98 + 34 * k;\n if (tl.length) s += M.text(\"tagline\", Object.assign({ at: tin + 1.0, in: \"rise\", split: \"lines\", distance: 16 }, tf, { x: W / 2, y: y, anchor: \"middle\", fill: c.mute, lh: 1.3, maxW: A.w * 0.8 }), tl.join(\"\\n\"));\n y += tl.length * tf.size * 1.3 + 30 * k;\n if (p.when) {\n const wt = $.case(p.when), sz = $.chip.size(wt, { size: 30 * k, dot: true });\n s += M.part(\"when\", { at: tin + 1.3, in: \"pop\", box: { x: W / 2 - sz.w / 2, y: y, w: sz.w, h: sz.h } }, $.chip(wt, W / 2, y, { size: 30 * k, dot: true, anchor: \"middle\", fill: c.accent, color: $.on(c.accent) }));\n }\n return s;\n}\n","libs":[]},{"id":"ribbon","name":"Tape ribbons","description":"Two tapes cross the frame like caution tape, their words scrolling — FREE SHIPPING • LIMITED DROP — over your footage.","category":"banner","tags":["promo","sale","offer","ribbon","tape","banner","caution","scrolling","free shipping"],"placement":"overlay","duration":{"default":5,"min":2,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"text":{"type":"text","label":"First tape","default":"Free shipping","maxLength":28},"text2":{"type":"text","label":"Second tape","default":"Limited drop","maxLength":28,"description":"empty for one tape"},"speed":{"type":"number","label":"Speed","default":1,"min":0.2,"max":3,"step":0.1}},"parts":{"tapes":"the tapes"},"beats":{"in":0.1},"pack":"promo","ref":"promo/ribbon","code":"// Promo pieces: offers, prices, codes and calls to action, in the brand's surface (bold styles get ink\n// outlines and hard shadows).\n/** True when the brand draws ink outlines and hard shadows. */\nfunction boldSurface($) { return $.style.surface === \"bold\"; }\n/** A filled shape path with the brand's outline and hard shadow when it has them. */\nfunction shapeFill($, d, fill) {\n const k = $.k;\n if (!boldSurface($)) return $.path(d, { fill: fill, shadow: \"md\" });\n return '<g transform=\"translate(' + 8 * k + \" \" + 8 * k + ')\">' + $.path(d, { fill: $.c.ink }) + \"</g>\" + $.path(d, { fill: fill, stroke: $.c.ink, sw: 4 * k });\n}\n/** Money: \"$1,299\" or \"1,299 €\" (currency after when it is a word or ends with a space). */\nfunction money(cur, n, dec) {\n const v = Number(n).toLocaleString(\"en-US\", { minimumFractionDigits: dec, maximumFractionDigits: dec });\n return /^[A-Za-z]{2,}$/.test(cur) ? v + \" \" + cur : cur + v;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n let s = \"\";\n const D = Math.hypot(W, H), th = Math.min(W, H) * 0.1;\n const tapes = [[p.text, -9, c.accent, 0.42], [p.text2, 7, c.accent2, 0.62]].filter(function (q) { return q[0]; });\n tapes.forEach(function (q, i) {\n const str = $.case(q[0]), f = Object.assign({}, $.font(\"display\"), { size: th * 0.5, ls: 0.02 });\n const sep = \" • \", unit = $.measure(str + sep, f), dir = i ? -1 : 1, off = (((t * 160 * k * p.speed * dir) % unit) + unit) % unit;\n let row = $.rect(-D / 2, -th / 2, D, th, { fill: q[2] }) + $.rect(-D / 2, -th / 2 + 6 * k, D, 3 * k, { fill: $.rgba($.on(q[2]), 0.5) }) + $.rect(-D / 2, th / 2 - 9 * k, D, 3 * k, { fill: $.rgba($.on(q[2]), 0.5) });\n for (let x = -D / 2 - unit + off; x < D / 2 + unit; x += unit) row += $.text(str + sep, Object.assign({}, f, { within: { x: -1e5, y: -1e5, w: 2e5, h: 2e5 }, x: x, y: -f.size * 0.56, fill: $.on(q[2]) }));\n const cy = H * q[3];\n s += M.part(\"tapes\", { at: tin + i * 0.2, in: \"wipe\", from: i ? \"right\" : \"left\", dur: 0.5, box: { x: 0, y: cy - th, w: W, h: th * 2 } }, '<g transform=\"translate(' + W / 2 + \" \" + cy + \") rotate(\" + q[1] + ')\">' + row + \"</g>\");\n });\n return s;\n}\n","libs":[]}]},{"format":1,"id":"ui","name":"MONARC UI","version":"1.0.0","description":"Product micro-interactions that play live in a page and render to video: a button that hovers, presses, loads and lands on a check, a toggle, a like with a count, a loader that succeeds or fails, and a notification bell. Each declares its states, so <volito-player> plays them on hover and click with no setup.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/ui","style":"studio","shared":"shared.js","items":["button","toggle","like","loader","bell"],"entries":[{"id":"button","name":"Button","description":"A product button with every state: it springs in, breathes while idle, lifts on hover, presses, spins while it loads and lands on a check with the done label. In a page, <volito-player> plays each state on hover and click.","category":"cta","tags":["ui","button","hover","press","loading","success","interactive","micro-interaction","product","web"],"placement":"overlay","duration":{"default":4.6,"min":4.6,"max":4.6},"aspects":["16:9","9:16","1:1","4:5"],"props":{"label":{"type":"text","label":"Label","default":"Get started","maxLength":22},"done":{"type":"text","label":"Done label","default":"You're in","maxLength":22},"icon":{"type":"icon","label":"Icon","default":"arrowRight"}},"beats":{"idle":0.6,"hoverIn":1.6,"hoverOut":1.9,"press":2.2,"loading":2.6,"done":3.6},"states":{"enter":{"from":"start","to":"idle","play":"once","next":"idle"},"idle":{"from":"idle","to":"hoverIn","play":"loop"},"hover":{"from":"hoverIn","to":"hoverOut","play":"once"},"unhover":{"from":"hoverOut","to":"press","play":"once","next":"idle"},"press":{"from":"press","to":"loading","play":"once","next":"loading"},"loading":{"from":"loading","to":"done","play":"loop"},"done":{"from":"done","to":"end","play":"once"}},"on":"pointerenter(idle|unhover): hover; pointerleave(hover): unhover; click(idle|hover|unhover): press","pack":"ui","ref":"ui/button","code":"// UI pieces: product micro-interactions whose timeline is cut into states at named beats (enter, idle,\n// hover, press, loading, done…). Loops are periodic so a looped state repeats without a seam, and every\n// state ends on a pose the next one starts from.\n\n/** 0→1 through a loop of [a, b): the frame at b is the frame at a. */\nfunction loopP(t, a, b) {\n var L = b - a;\n if (L <= 0) return 0;\n var x = (t - a) % L;\n return (x < 0 ? x + L : x) / L;\n}\n\n/** A size as a share of the frame's short side, so a control reads the same at every aspect. */\nfunction unit($, share) {\n return Math.min($.W, $.H) * share;\n}\n\n/** Fixed-precision number for markup. */\nfunction n2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A rounded box in the brand's surface: bold brands get an ink outline and a hard shadow. */\nfunction uiBox($, x, y, w, h, r, fill, shadow) {\n var k = $.k;\n if ($.style.surface === \"bold\") return $.rect(x + 6 * k, y + 6 * k, w, h, { r: r, fill: $.c.ink }) + $.rect(x, y, w, h, { r: r, fill: fill, stroke: $.c.ink, sw: 3.5 * k });\n return $.rect(x, y, w, h, { r: r, fill: fill, shadow: $.style.surface === \"flat\" ? null : shadow || \"md\" });\n}\n\n/** A spinner arc at (cx, cy) turned `turn` (0–1) of the way round. */\nfunction spinnerArc(cx, cy, r, turn, color, sw, op) {\n return '<g transform=\"rotate(' + n2(turn * 360) + \" \" + n2(cx) + \" \" + n2(cy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(op) + '\"' : \"\") + '><circle cx=\"' + n2(cx) + '\" cy=\"' + n2(cy) + '\" r=\"' + n2(r) + '\" fill=\"none\" stroke=\"' + color + '\" stroke-width=\"' + n2(sw) + '\" stroke-linecap=\"round\" pathLength=\"100\" stroke-dasharray=\"30 70\"/></g>';\n}\n\n/** A check mark centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction checkMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var d = \"M\" + n2(cx - s * 0.5) + \" \" + n2(cy + s * 0.02) + \" L\" + n2(cx - s * 0.14) + \" \" + n2(cy + s * 0.36) + \" L\" + n2(cx + s * 0.52) + \" \" + n2(cy - s * 0.34);\n return $.path(d, { stroke: color, sw: sw, draw: Math.min(1, p) });\n}\n\n/** A cross centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction crossMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var h = s * 0.4;\n var a = $.path(\"M\" + n2(cx - h) + \" \" + n2(cy - h) + \" L\" + n2(cx + h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, p * 2) });\n var b = p > 0.5 ? $.path(\"M\" + n2(cx + h) + \" \" + n2(cy - h) + \" L\" + n2(cx - h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, (p - 0.5) * 2) }) : \"\";\n return a + b;\n}\n\n/** A group scaled about (ox, oy) and moved by (dx, dy), at an opacity. */\nfunction posed(markup, ox, oy, s, dx, dy, op) {\n return '<g transform=\"translate(' + n2(dx || 0) + \" \" + n2(dy || 0) + \") translate(\" + n2(ox) + \" \" + n2(oy) + \") scale(\" + n2(s) + \") translate(\" + n2(-ox) + \" \" + n2(-oy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(Math.max(0, op)) + '\"' : \"\") + \">\" + markup + \"</g>\";\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n var bIdle = $.beat(\"idle\", 0.6), bHi = $.beat(\"hoverIn\", 1.6), bHo = $.beat(\"hoverOut\", 1.9), bPress = $.beat(\"press\", 2.2), bLoad = $.beat(\"loading\", 2.6), bDone = $.beat(\"done\", 3.6);\n var tf = $.font(\"text\");\n var size = unit($, 0.07);\n var font = { size: size, weight: Math.max(700, tf.weight), family: tf.family };\n var label = $.case(p.label), done = $.case(p.done || p.label);\n var iconS = size * 1.0, gap = size * 0.55, padX = size * 1.25, h = size * 2.4;\n var lw = Math.max($.measure(label, font), $.measure(done, font) + iconS + gap);\n var w = padX * 2 + lw + (p.icon ? iconS + gap : 0);\n var cx = W / 2, cy = H / 2;\n var r = Math.min(h / 2, Math.max(8 * k, $.style.radius));\n\n // Each stretch of the timeline is a state: enter, idle (loops), hover, unhover, press, loading (loops), done.\n var appear = $.E.outBack($.seg(t, 0, bIdle * 0.85));\n var breathe = t >= bIdle && t < bHi ? Math.sin(loopP(t, bIdle, bHi) * Math.PI * 2) : 0;\n var hover = t < bHi ? 0 : t < bHo ? $.E.out3($.seg(t, bHi, bHi + 0.22)) : t < bPress ? 1 - $.E.inOut2($.seg(t, bHo, bHo + 0.25)) : 0;\n var press = $.bump(t, bPress + 0.02, 0.06, 0.2);\n var collapse = t < bDone ? $.E.inOut3($.seg(t, bPress + 0.14, bLoad)) : 1 - $.E.out3($.seg(t, bDone, bDone + 0.35));\n var curW = $.lerp(w, h, collapse);\n var curR = $.lerp(r, h / 2, collapse);\n var lift = (hover * 6 + breathe * 1.5) * k;\n var scale = (0.6 + 0.4 * appear) * (1 - 0.06 * press) * (1 + 0.03 * hover);\n var fill = $.mix(c.accent, c.fg, 0.1 * hover);\n var fg = $.on(c.accent);\n var x = cx - curW / 2, y = cy - h / 2;\n\n var s = \"\";\n // The press ripple, outside the scaled button.\n var rp = $.seg(t, bPress, bPress + 0.55);\n if (rp > 0 && rp < 1) s += $.rect(cx - w / 2 - 26 * rp * k, y - 26 * rp * k, w + 52 * rp * k, h + 52 * rp * k, { r: r + 26 * rp * k, stroke: c.accent, sw: 5 * k * (1 - rp), op: 1 - rp });\n\n var b = uiBox($, x, y, curW, h, curR, fill, hover > 0.5 ? \"lg\" : \"md\");\n // The label and icon, until the button collapses into a circle.\n var labelOp = t < bDone ? 1 - $.seg(collapse, 0, 0.35) : 0;\n if (labelOp > 0) {\n var tx = cx - w / 2 + padX;\n b += $.text(label, Object.assign({}, font, { x: tx, y: cy - size * 0.55, fill: fg, op: labelOp }));\n if (p.icon) b += $.icon(p.icon, cx + w / 2 - padX - iconS + hover * 5 * k, cy - iconS / 2, iconS, { color: fg, sw: 2.4, op: labelOp });\n }\n // Loading: a spinner turning once a second, seamless across the loop.\n var spinOp = t >= bPress && t < bDone + 0.15 ? $.seg(t, bLoad - 0.15, bLoad) * (1 - $.seg(t, bDone, bDone + 0.12)) : 0;\n if (spinOp > 0) b += spinnerArc(cx, cy, h * 0.27, t >= bLoad ? loopP(t, bLoad, bDone) * Math.round(bDone - bLoad) : 0, fg, size * 0.22, spinOp);\n // Done: the check draws on, then the done label slides in as the button opens out again.\n if (t >= bDone) {\n var open = $.E.out3($.seg(t, bDone, bDone + 0.35));\n var ck = $.seg(t, bDone + 0.08, bDone + 0.42);\n var ckX = $.lerp(cx, cx - w / 2 + padX + iconS / 2, open);\n b += checkMark($, ckX, cy, iconS * 0.9, ck, fg, size * 0.2);\n var dOp = $.seg(t, bDone + 0.22, bDone + 0.5);\n if (dOp > 0) b += $.text(done, Object.assign({}, font, { x: cx - w / 2 + padX + iconS + gap + (1 - dOp) * 10 * k, y: cy - size * 0.55, fill: fg, op: dOp }));\n }\n s += posed(b, cx, cy, scale, 0, -lift, Math.min(1, appear * 1.4));\n return s;\n}\n","libs":[]},{"id":"toggle","name":"Toggle","description":"A settings switch with its label: the knob stretches as it slides, the track fills with the accent, and it slides back. In a page, a click turns it on and off.","category":"cta","tags":["ui","toggle","switch","settings","on off","interactive","micro-interaction","product","web"],"placement":"overlay","duration":{"default":2.4,"min":2.4,"max":2.4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"label":{"type":"text","label":"Label","default":"Notifications","maxLength":24},"hint":{"type":"text","label":"Hint","default":"Get a ping when a render finishes","maxLength":44,"description":"empty for none"}},"beats":{"off":0.4,"turnOn":0.7,"turnOff":1.7},"states":{"enter":{"from":"start","to":"off","play":"once","next":"off"},"off":{"from":"off","play":"hold"},"on":{"from":"turnOn","to":"turnOff","play":"once"},"turning-off":{"from":"turnOff","to":"end","play":"once","next":"off"}},"on":"click(off|turning-off): on; click(on): turning-off","pack":"ui","ref":"ui/toggle","code":"// UI pieces: product micro-interactions whose timeline is cut into states at named beats (enter, idle,\n// hover, press, loading, done…). Loops are periodic so a looped state repeats without a seam, and every\n// state ends on a pose the next one starts from.\n\n/** 0→1 through a loop of [a, b): the frame at b is the frame at a. */\nfunction loopP(t, a, b) {\n var L = b - a;\n if (L <= 0) return 0;\n var x = (t - a) % L;\n return (x < 0 ? x + L : x) / L;\n}\n\n/** A size as a share of the frame's short side, so a control reads the same at every aspect. */\nfunction unit($, share) {\n return Math.min($.W, $.H) * share;\n}\n\n/** Fixed-precision number for markup. */\nfunction n2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A rounded box in the brand's surface: bold brands get an ink outline and a hard shadow. */\nfunction uiBox($, x, y, w, h, r, fill, shadow) {\n var k = $.k;\n if ($.style.surface === \"bold\") return $.rect(x + 6 * k, y + 6 * k, w, h, { r: r, fill: $.c.ink }) + $.rect(x, y, w, h, { r: r, fill: fill, stroke: $.c.ink, sw: 3.5 * k });\n return $.rect(x, y, w, h, { r: r, fill: fill, shadow: $.style.surface === \"flat\" ? null : shadow || \"md\" });\n}\n\n/** A spinner arc at (cx, cy) turned `turn` (0–1) of the way round. */\nfunction spinnerArc(cx, cy, r, turn, color, sw, op) {\n return '<g transform=\"rotate(' + n2(turn * 360) + \" \" + n2(cx) + \" \" + n2(cy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(op) + '\"' : \"\") + '><circle cx=\"' + n2(cx) + '\" cy=\"' + n2(cy) + '\" r=\"' + n2(r) + '\" fill=\"none\" stroke=\"' + color + '\" stroke-width=\"' + n2(sw) + '\" stroke-linecap=\"round\" pathLength=\"100\" stroke-dasharray=\"30 70\"/></g>';\n}\n\n/** A check mark centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction checkMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var d = \"M\" + n2(cx - s * 0.5) + \" \" + n2(cy + s * 0.02) + \" L\" + n2(cx - s * 0.14) + \" \" + n2(cy + s * 0.36) + \" L\" + n2(cx + s * 0.52) + \" \" + n2(cy - s * 0.34);\n return $.path(d, { stroke: color, sw: sw, draw: Math.min(1, p) });\n}\n\n/** A cross centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction crossMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var h = s * 0.4;\n var a = $.path(\"M\" + n2(cx - h) + \" \" + n2(cy - h) + \" L\" + n2(cx + h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, p * 2) });\n var b = p > 0.5 ? $.path(\"M\" + n2(cx + h) + \" \" + n2(cy - h) + \" L\" + n2(cx - h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, (p - 0.5) * 2) }) : \"\";\n return a + b;\n}\n\n/** A group scaled about (ox, oy) and moved by (dx, dy), at an opacity. */\nfunction posed(markup, ox, oy, s, dx, dy, op) {\n return '<g transform=\"translate(' + n2(dx || 0) + \" \" + n2(dy || 0) + \") translate(\" + n2(ox) + \" \" + n2(oy) + \") scale(\" + n2(s) + \") translate(\" + n2(-ox) + \" \" + n2(-oy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(Math.max(0, op)) + '\"' : \"\") + \">\" + markup + \"</g>\";\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n var bOff = $.beat(\"off\", 0.4), bOn = $.beat(\"turnOn\", 0.7), bBack = $.beat(\"turnOff\", 1.7);\n var tf = $.font(\"text\");\n var size = unit($, 0.06);\n var th = size * 1.9, tw = th * 1.8, kr = th * 0.5 - 5 * k;\n var label = $.case(p.label), hint = p.hint ? p.hint : \"\";\n var lf = { size: size, weight: Math.max(650, tf.weight), family: tf.family };\n var hf = { size: size * 0.62, weight: 500, family: tf.family };\n var textW = Math.max($.measure(label, lf), hint ? $.measure(hint, hf) : 0);\n var gap = size * 1.4, total = textW + gap + tw;\n var x0 = W / 2 - total / 2, tx = x0 + textW + gap, ty = H / 2 - th / 2;\n\n var appear = $.E.outBack($.seg(t, 0, bOff * 0.9));\n // How far on: slides on at turnOn, back off at turnOff; the knob stretches while it moves.\n var on = t < bOn ? 0 : t < bBack ? $.E.inOut3($.seg(t, bOn, bOn + 0.38)) : 1 - $.E.inOut3($.seg(t, bBack, bBack + 0.38));\n var moving = t < bBack ? $.bump(t, bOn + 0.05, 0.12, 0.25) : $.bump(t, bBack + 0.05, 0.12, 0.25);\n var track = $.mix($.mix(c.surface, c.fg, 0.22), c.accent, on);\n var padX = size * 1.0, padY = size * 0.8, ch = Math.max(th, hint ? size * 1.9 : size) + padY * 2;\n var s = uiBox($, x0 - padX, H / 2 - ch / 2, total + padX * 2, ch, Math.min(ch / 2, Math.max(10 * k, $.style.radius)), c.surface, \"md\");\n s += $.text(label, Object.assign({}, lf, { x: x0, y: H / 2 - (hint ? size * 1.05 : size * 0.55), fill: c.fg }));\n if (hint) s += $.text(hint, Object.assign({}, hf, { x: x0, y: H / 2 + size * 0.25, fill: $.mix(c.fg, c.surface, 0.4) }));\n var g = uiBox($, tx, ty, tw, th, th / 2, track, null);\n var kx = $.lerp(tx + th / 2, tx + tw - th / 2, on);\n var stretch = 1 + 0.35 * moving;\n g += '<ellipse cx=\"' + n2(kx) + '\" cy=\"' + n2(H / 2) + '\" rx=\"' + n2(kr * stretch) + '\" ry=\"' + n2(kr * (1 - 0.08 * moving)) + '\" fill=\"' + $.on(c.accent) + '\"/>';\n if (on > 0.6) g += checkMark($, kx, H / 2, kr * 0.9, (on - 0.6) / 0.4, c.accent, size * 0.16);\n s += g;\n return posed(s, W / 2, H / 2, 0.85 + 0.15 * appear, 0, (1 - appear) * 20 * k, Math.min(1, appear * 1.5));\n}\n","libs":[]},{"id":"like","name":"Like","description":"A heart and its count: it breathes while idle, pops full with a burst when liked as the count rolls up by one, and empties again when unliked. In a page, a click likes and unlikes.","category":"social","tags":["ui","like","heart","favourite","count","interactive","micro-interaction","social","web"],"placement":"overlay","duration":{"default":3.2,"min":3.2,"max":3.2},"aspects":["16:9","9:16","1:1","4:5"],"props":{"count":{"type":"number","label":"Likes before","default":128,"min":0,"max":9999999,"step":1}},"beats":{"idle":0.5,"like":1.5,"unlike":2.6},"states":{"enter":{"from":"start","to":"idle","play":"once","next":"idle"},"idle":{"from":"idle","to":"like","play":"loop"},"like":{"from":"like","to":"unlike","play":"once"},"unlike":{"from":"unlike","to":"end","play":"once","next":"idle"}},"on":"click(idle|unlike): like; click(like): unlike","pack":"ui","ref":"ui/like","code":"// UI pieces: product micro-interactions whose timeline is cut into states at named beats (enter, idle,\n// hover, press, loading, done…). Loops are periodic so a looped state repeats without a seam, and every\n// state ends on a pose the next one starts from.\n\n/** 0→1 through a loop of [a, b): the frame at b is the frame at a. */\nfunction loopP(t, a, b) {\n var L = b - a;\n if (L <= 0) return 0;\n var x = (t - a) % L;\n return (x < 0 ? x + L : x) / L;\n}\n\n/** A size as a share of the frame's short side, so a control reads the same at every aspect. */\nfunction unit($, share) {\n return Math.min($.W, $.H) * share;\n}\n\n/** Fixed-precision number for markup. */\nfunction n2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A rounded box in the brand's surface: bold brands get an ink outline and a hard shadow. */\nfunction uiBox($, x, y, w, h, r, fill, shadow) {\n var k = $.k;\n if ($.style.surface === \"bold\") return $.rect(x + 6 * k, y + 6 * k, w, h, { r: r, fill: $.c.ink }) + $.rect(x, y, w, h, { r: r, fill: fill, stroke: $.c.ink, sw: 3.5 * k });\n return $.rect(x, y, w, h, { r: r, fill: fill, shadow: $.style.surface === \"flat\" ? null : shadow || \"md\" });\n}\n\n/** A spinner arc at (cx, cy) turned `turn` (0–1) of the way round. */\nfunction spinnerArc(cx, cy, r, turn, color, sw, op) {\n return '<g transform=\"rotate(' + n2(turn * 360) + \" \" + n2(cx) + \" \" + n2(cy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(op) + '\"' : \"\") + '><circle cx=\"' + n2(cx) + '\" cy=\"' + n2(cy) + '\" r=\"' + n2(r) + '\" fill=\"none\" stroke=\"' + color + '\" stroke-width=\"' + n2(sw) + '\" stroke-linecap=\"round\" pathLength=\"100\" stroke-dasharray=\"30 70\"/></g>';\n}\n\n/** A check mark centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction checkMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var d = \"M\" + n2(cx - s * 0.5) + \" \" + n2(cy + s * 0.02) + \" L\" + n2(cx - s * 0.14) + \" \" + n2(cy + s * 0.36) + \" L\" + n2(cx + s * 0.52) + \" \" + n2(cy - s * 0.34);\n return $.path(d, { stroke: color, sw: sw, draw: Math.min(1, p) });\n}\n\n/** A cross centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction crossMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var h = s * 0.4;\n var a = $.path(\"M\" + n2(cx - h) + \" \" + n2(cy - h) + \" L\" + n2(cx + h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, p * 2) });\n var b = p > 0.5 ? $.path(\"M\" + n2(cx + h) + \" \" + n2(cy - h) + \" L\" + n2(cx - h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, (p - 0.5) * 2) }) : \"\";\n return a + b;\n}\n\n/** A group scaled about (ox, oy) and moved by (dx, dy), at an opacity. */\nfunction posed(markup, ox, oy, s, dx, dy, op) {\n return '<g transform=\"translate(' + n2(dx || 0) + \" \" + n2(dy || 0) + \") translate(\" + n2(ox) + \" \" + n2(oy) + \") scale(\" + n2(s) + \") translate(\" + n2(-ox) + \" \" + n2(-oy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(Math.max(0, op)) + '\"' : \"\") + \">\" + markup + \"</g>\";\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n var bIdle = $.beat(\"idle\", 0.5), bLike = $.beat(\"like\", 1.5), bUn = $.beat(\"unlike\", 2.6);\n var tf = $.font(\"text\");\n var hs = unit($, 0.17);\n var base = Math.max(0, Math.round(Number(p.count) || 0));\n var cf = { size: hs * 0.5, weight: Math.max(700, tf.weight), family: tf.family };\n var after = $.count(base + 1), before = $.count(base);\n var cw = Math.max($.measure(after, cf), $.measure(before, cf));\n var gap = hs * 0.3, total = hs + gap + cw;\n var hx = W / 2 - total / 2, hy = H / 2 - hs / 2, cx = hx + hs / 2, cy = H / 2;\n\n var appear = $.E.outBack($.seg(t, 0, bIdle * 0.9));\n var breathe = t >= bIdle && t < bLike ? Math.sin(loopP(t, bIdle, bLike) * Math.PI * 2) : 0;\n var liked = t < bLike ? 0 : t < bUn ? 1 : 1 - $.E.inOut2($.seg(t, bUn, bUn + 0.3));\n var popS = t >= bLike && t < bUn ? 1 + 0.45 * $.bump(t, bLike, 0.1, 0.35) : 1;\n var squash = t >= bUn ? $.bump(t, bUn, 0.08, 0.25) : 0;\n var pad = hs * 0.32, ph = hs + pad * 2;\n var s = uiBox($, hx - pad * 1.2, cy - ph / 2, total + pad * 2.6, ph, ph / 2, c.surface, \"md\");\n if (t >= bLike && t < bUn) s += $.burst(t, bLike + 0.05, cx, cy, { n: 12, r: hs * 1.2, colors: [c.accent, c.accent2], size: hs * 0.08, dur: 0.7 });\n // The heart: an outline that fills with the accent when liked.\n var scale = (0.7 + 0.3 * appear) * (1 + 0.04 * breathe) * (liked > 0 && t < bUn ? popS : 1) * (1 - 0.15 * squash);\n var heart = $.icon(\"heart\", hx, hy, hs, { color: $.mix(c.fg, c.accent, liked), sw: 2.2 });\n if (liked > 0.01) heart += '<g opacity=\"' + n2(liked) + '\">' + $.icon(\"heart\", hx, hy, hs, { color: c.accent, fill: true }) + \"</g>\";\n s += posed(heart, cx, cy, scale, 0, 0, Math.min(1, appear * 1.5));\n // The count rolls: the old number slides up and out as the new one slides in.\n var roll = t < bLike ? 0 : t < bUn ? $.E.out3($.seg(t, bLike + 0.05, bLike + 0.4)) : 1 - $.E.out3($.seg(t, bUn, bUn + 0.35));\n var tx = hx + hs + gap, ty = cy - cf.size * 0.55, dy = cf.size * 0.9;\n var clip = $.uid();\n $.def('<clipPath id=\"' + clip + '\"><rect x=\"' + n2(tx - 4 * k) + '\" y=\"' + n2(cy - cf.size * 0.8) + '\" width=\"' + n2(cw + 8 * k) + '\" height=\"' + n2(cf.size * 1.6) + '\"/></clipPath>');\n var nums = $.text(before, Object.assign({}, cf, { x: tx, y: ty - dy * roll, fill: c.fg, op: Math.max(0, 1 - roll * 2) })) + $.text(after, Object.assign({}, cf, { x: tx, y: ty + dy * (1 - roll), fill: liked > 0.5 ? c.accent : c.fg, op: Math.max(0, roll * 2 - 1) }));\n s += '<g clip-path=\"url(#' + clip + ')\"' + (appear < 1 ? ' opacity=\"' + n2(Math.min(1, appear * 1.5)) + '\"' : \"\") + \">\" + nums + \"</g>\";\n return s;\n}\n","libs":[]},{"id":"loader","name":"Loader","description":"A status card that waits, then ends one of two ways: a spinner turns while the line says what is happening, then it closes into a ring with a check, or turns into a cross with a shake. In a page, your code sets the state when the work finishes.","category":"callout","tags":["ui","loader","spinner","loading","progress","success","error","status","interactive","micro-interaction","web"],"placement":"overlay","duration":{"default":3.6,"min":3.6,"max":3.6},"aspects":["16:9","9:16","1:1","4:5"],"props":{"loading":{"type":"text","label":"While it works","default":"Uploading your video","maxLength":32},"success":{"type":"text","label":"When it works","default":"Uploaded","maxLength":32},"failure":{"type":"text","label":"When it fails","default":"Upload failed — try again","maxLength":32}},"beats":{"loading":0.4,"success":1.4,"failure":2.5},"states":{"enter":{"from":"start","to":"loading","play":"once","next":"loading"},"loading":{"from":"loading","to":"success","play":"loop"},"success":{"from":"success","to":"failure","play":"once"},"failure":{"from":"failure","to":"end","play":"once"}},"on":"click(success|failure): loading","pack":"ui","ref":"ui/loader","code":"// UI pieces: product micro-interactions whose timeline is cut into states at named beats (enter, idle,\n// hover, press, loading, done…). Loops are periodic so a looped state repeats without a seam, and every\n// state ends on a pose the next one starts from.\n\n/** 0→1 through a loop of [a, b): the frame at b is the frame at a. */\nfunction loopP(t, a, b) {\n var L = b - a;\n if (L <= 0) return 0;\n var x = (t - a) % L;\n return (x < 0 ? x + L : x) / L;\n}\n\n/** A size as a share of the frame's short side, so a control reads the same at every aspect. */\nfunction unit($, share) {\n return Math.min($.W, $.H) * share;\n}\n\n/** Fixed-precision number for markup. */\nfunction n2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A rounded box in the brand's surface: bold brands get an ink outline and a hard shadow. */\nfunction uiBox($, x, y, w, h, r, fill, shadow) {\n var k = $.k;\n if ($.style.surface === \"bold\") return $.rect(x + 6 * k, y + 6 * k, w, h, { r: r, fill: $.c.ink }) + $.rect(x, y, w, h, { r: r, fill: fill, stroke: $.c.ink, sw: 3.5 * k });\n return $.rect(x, y, w, h, { r: r, fill: fill, shadow: $.style.surface === \"flat\" ? null : shadow || \"md\" });\n}\n\n/** A spinner arc at (cx, cy) turned `turn` (0–1) of the way round. */\nfunction spinnerArc(cx, cy, r, turn, color, sw, op) {\n return '<g transform=\"rotate(' + n2(turn * 360) + \" \" + n2(cx) + \" \" + n2(cy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(op) + '\"' : \"\") + '><circle cx=\"' + n2(cx) + '\" cy=\"' + n2(cy) + '\" r=\"' + n2(r) + '\" fill=\"none\" stroke=\"' + color + '\" stroke-width=\"' + n2(sw) + '\" stroke-linecap=\"round\" pathLength=\"100\" stroke-dasharray=\"30 70\"/></g>';\n}\n\n/** A check mark centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction checkMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var d = \"M\" + n2(cx - s * 0.5) + \" \" + n2(cy + s * 0.02) + \" L\" + n2(cx - s * 0.14) + \" \" + n2(cy + s * 0.36) + \" L\" + n2(cx + s * 0.52) + \" \" + n2(cy - s * 0.34);\n return $.path(d, { stroke: color, sw: sw, draw: Math.min(1, p) });\n}\n\n/** A cross centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction crossMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var h = s * 0.4;\n var a = $.path(\"M\" + n2(cx - h) + \" \" + n2(cy - h) + \" L\" + n2(cx + h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, p * 2) });\n var b = p > 0.5 ? $.path(\"M\" + n2(cx + h) + \" \" + n2(cy - h) + \" L\" + n2(cx - h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, (p - 0.5) * 2) }) : \"\";\n return a + b;\n}\n\n/** A group scaled about (ox, oy) and moved by (dx, dy), at an opacity. */\nfunction posed(markup, ox, oy, s, dx, dy, op) {\n return '<g transform=\"translate(' + n2(dx || 0) + \" \" + n2(dy || 0) + \") translate(\" + n2(ox) + \" \" + n2(oy) + \") scale(\" + n2(s) + \") translate(\" + n2(-ox) + \" \" + n2(-oy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(Math.max(0, op)) + '\"' : \"\") + \">\" + markup + \"</g>\";\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n var bLoad = $.beat(\"loading\", 0.4), bOk = $.beat(\"success\", 1.4), bFail = $.beat(\"failure\", 2.5);\n var tf = $.font(\"text\");\n var size = unit($, 0.055);\n var font = { size: size, weight: Math.max(600, tf.weight), family: tf.family };\n var lines = [$.case(p.loading), $.case(p.success), $.case(p.failure)];\n var iconR = size * 0.85, padX = size * 1.1, padY = size * 0.85, gap = size * 0.75;\n var tw = Math.max($.measure(lines[0], font), $.measure(lines[1], font), $.measure(lines[2], font));\n var w = padX * 2 + iconR * 2 + gap + tw, h = padY * 2 + iconR * 2;\n var x = W / 2 - w / 2, y = H / 2 - h / 2, icx = x + padX + iconR, icy = H / 2, tx = icx + iconR + gap;\n\n var appear = $.E.outBack($.seg(t, 0, bLoad * 0.9));\n var phase = t < bOk ? 0 : t < bFail ? 1 : 2;\n var ok = phase === 1 ? $.E.out3($.seg(t, bOk, bOk + 0.4)) : 0;\n var bad = phase === 2 ? $.E.out3($.seg(t, bFail, bFail + 0.4)) : 0;\n var shake = phase === 2 ? $.shake(t, bFail + 0.15, 9 * k, 0.4)[0] : 0;\n var tone = phase === 2 ? c.red || c.accent2 : c.accent;\n var s = uiBox($, x, y, w, h, Math.min(h / 2, Math.max(10 * k, $.style.radius)), c.surface, \"md\");\n // The icon: a spinner while it works (one turn a second, seamless), then a full ring with a check or a cross.\n var sw = size * 0.2;\n if (phase === 0) s += '<circle cx=\"' + n2(icx) + '\" cy=\"' + n2(icy) + '\" r=\"' + n2(iconR) + '\" fill=\"none\" stroke=\"' + $.mix(c.surface, c.fg, 0.12) + '\" stroke-width=\"' + n2(sw) + '\"/>' + spinnerArc(icx, icy, iconR, t >= bLoad ? loopP(t, bLoad, bOk) * Math.round(bOk - bLoad) : 0, tone, sw, 1);\n else {\n var done = phase === 1 ? ok : bad;\n s += $.ring(icx, icy, iconR, done, { color: tone, sw: sw, bg: $.mix(c.surface, c.fg, 0.12) });\n if (phase === 1) s += checkMark($, icx, icy, iconR * 1.0, $.seg(t, bOk + 0.15, bOk + 0.5), tone, sw);\n else s += crossMark($, icx, icy, iconR * 1.0, $.seg(t, bFail + 0.15, bFail + 0.5), tone, sw);\n }\n // The line: the new one rises in as the old one fades.\n var swap = phase === 0 ? 1 : $.E.out3($.seg(t, phase === 1 ? bOk + 0.1 : bFail + 0.1, (phase === 1 ? bOk : bFail) + 0.45));\n if (phase > 0 && swap < 1) s += $.text(lines[phase - 1], Object.assign({}, font, { x: tx, y: icy - size * 0.55 - swap * size * 0.6, fill: c.fg, op: 1 - swap }));\n s += $.text(lines[phase], Object.assign({}, font, { x: tx, y: icy - size * 0.55 + (1 - swap) * size * 0.6, fill: c.fg, op: swap }));\n return posed(s, W / 2, H / 2, 0.85 + 0.15 * appear, shake, (1 - appear) * 20 * k, Math.min(1, appear * 1.5));\n}\n","libs":[]},{"id":"bell","name":"Notification bell","description":"A bell that rings: it swings on its hook, a badge pops in with the count, and it clears when read. In a page, hovering rings it and a click marks it read.","category":"social","tags":["ui","notification","bell","badge","alert","inbox","interactive","micro-interaction","web"],"placement":"overlay","duration":{"default":3,"min":3,"max":3},"aspects":["16:9","9:16","1:1","4:5"],"props":{"count":{"type":"number","label":"New","default":3,"min":1,"max":99,"step":1}},"beats":{"idle":0.5,"ring":1.5,"clear":2.5},"states":{"enter":{"from":"start","to":"idle","play":"once","next":"idle"},"idle":{"from":"idle","to":"ring","play":"loop"},"ring":{"from":"ring","to":"clear","play":"once"},"clear":{"from":"clear","to":"end","play":"once","next":"idle"}},"on":"pointerenter(idle): ring; click(ring): clear","pack":"ui","ref":"ui/bell","code":"// UI pieces: product micro-interactions whose timeline is cut into states at named beats (enter, idle,\n// hover, press, loading, done…). Loops are periodic so a looped state repeats without a seam, and every\n// state ends on a pose the next one starts from.\n\n/** 0→1 through a loop of [a, b): the frame at b is the frame at a. */\nfunction loopP(t, a, b) {\n var L = b - a;\n if (L <= 0) return 0;\n var x = (t - a) % L;\n return (x < 0 ? x + L : x) / L;\n}\n\n/** A size as a share of the frame's short side, so a control reads the same at every aspect. */\nfunction unit($, share) {\n return Math.min($.W, $.H) * share;\n}\n\n/** Fixed-precision number for markup. */\nfunction n2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A rounded box in the brand's surface: bold brands get an ink outline and a hard shadow. */\nfunction uiBox($, x, y, w, h, r, fill, shadow) {\n var k = $.k;\n if ($.style.surface === \"bold\") return $.rect(x + 6 * k, y + 6 * k, w, h, { r: r, fill: $.c.ink }) + $.rect(x, y, w, h, { r: r, fill: fill, stroke: $.c.ink, sw: 3.5 * k });\n return $.rect(x, y, w, h, { r: r, fill: fill, shadow: $.style.surface === \"flat\" ? null : shadow || \"md\" });\n}\n\n/** A spinner arc at (cx, cy) turned `turn` (0–1) of the way round. */\nfunction spinnerArc(cx, cy, r, turn, color, sw, op) {\n return '<g transform=\"rotate(' + n2(turn * 360) + \" \" + n2(cx) + \" \" + n2(cy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(op) + '\"' : \"\") + '><circle cx=\"' + n2(cx) + '\" cy=\"' + n2(cy) + '\" r=\"' + n2(r) + '\" fill=\"none\" stroke=\"' + color + '\" stroke-width=\"' + n2(sw) + '\" stroke-linecap=\"round\" pathLength=\"100\" stroke-dasharray=\"30 70\"/></g>';\n}\n\n/** A check mark centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction checkMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var d = \"M\" + n2(cx - s * 0.5) + \" \" + n2(cy + s * 0.02) + \" L\" + n2(cx - s * 0.14) + \" \" + n2(cy + s * 0.36) + \" L\" + n2(cx + s * 0.52) + \" \" + n2(cy - s * 0.34);\n return $.path(d, { stroke: color, sw: sw, draw: Math.min(1, p) });\n}\n\n/** A cross centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction crossMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var h = s * 0.4;\n var a = $.path(\"M\" + n2(cx - h) + \" \" + n2(cy - h) + \" L\" + n2(cx + h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, p * 2) });\n var b = p > 0.5 ? $.path(\"M\" + n2(cx + h) + \" \" + n2(cy - h) + \" L\" + n2(cx - h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, (p - 0.5) * 2) }) : \"\";\n return a + b;\n}\n\n/** A group scaled about (ox, oy) and moved by (dx, dy), at an opacity. */\nfunction posed(markup, ox, oy, s, dx, dy, op) {\n return '<g transform=\"translate(' + n2(dx || 0) + \" \" + n2(dy || 0) + \") translate(\" + n2(ox) + \" \" + n2(oy) + \") scale(\" + n2(s) + \") translate(\" + n2(-ox) + \" \" + n2(-oy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(Math.max(0, op)) + '\"' : \"\") + \">\" + markup + \"</g>\";\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n var bIdle = $.beat(\"idle\", 0.5), bRing = $.beat(\"ring\", 1.5), bClear = $.beat(\"clear\", 2.5);\n var tf = $.font(\"text\");\n var bs = unit($, 0.18);\n var cx = W / 2, cy = H / 2, bx = cx - bs / 2, by = cy - bs / 2;\n var appear = $.E.outBack($.seg(t, 0, bIdle * 0.9));\n var sway = t >= bIdle && t < bRing ? Math.sin(loopP(t, bIdle, bRing) * Math.PI * 2) * 2.5 : 0;\n // Ringing: a damped swing about the hook at the top.\n var rt = t - bRing;\n var swing = t >= bRing && t < bClear ? 22 * Math.exp(-rt * 3.2) * Math.sin(rt * 26) : 0;\n var disc = bs * 1.7;\n var s = uiBox($, cx - disc / 2, cy - disc / 2, disc, disc, disc / 2, c.surface, \"md\");\n s += '<g transform=\"rotate(' + n2(sway + swing) + \" \" + n2(cx) + \" \" + n2(by + bs * 0.08) + ')\">' + $.icon(\"bell\", bx, by, bs, { color: c.fg, sw: 2.2 }) + \"</g>\";\n // The badge pops in when it rings and shrinks away when read.\n var show = t < bRing ? 0 : t < bClear ? $.pop(t, bRing + 0.08, 2.6, 6) : 1 - $.E.in3($.seg(t, bClear, bClear + 0.3));\n if (show > 0.01) {\n var n = String(Math.max(1, Math.round(Number(p.count) || 1)));\n var r = bs * 0.24, bcx = cx + bs * 0.32, bcy = by + bs * 0.18;\n var badge = $.circle(bcx, bcy, r, { fill: c.accent, stroke: c.surface, sw: 4 * k }) + $.text(n, { x: bcx, y: bcy - r * 0.62, size: r * 1.15, weight: Math.max(700, tf.weight), family: tf.family, fill: $.on(c.accent), anchor: \"middle\" });\n s += posed(badge, bcx, bcy, show, 0, 0, Math.min(1, show * 2));\n }\n return posed(s, cx, cy, 0.7 + 0.3 * appear, 0, 0, Math.min(1, appear * 1.5));\n}\n","libs":[]}]},{"format":1,"id":"effects","name":"MONARC Effects","version":"1.0.0","description":"Looks made from SVG filters, so they render the same in Node and the browser with no GPU: film grain, a light leak, a frosted glass card, a chromatic split title, a liquid warp title and a neon sign, in the brand's colours.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/effects","style":"cobalt","shared":"shared.js","items":["grain","light-leak","glass","chromatic","warp","neon"],"entries":[{"id":"grain","name":"Film grain","description":"Moving film grain and a soft vignette over footage: the grain re-rolls on every film frame, so it reads as real stock, not a still texture.","category":"scene","tags":["effect","film grain","grain","noise","vignette","film look","texture","overlay"],"placement":"overlay","duration":{"default":6,"min":1,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"amount":{"type":"number","label":"Grain","default":0.35,"min":0,"max":1,"step":0.05},"size":{"type":"choice","label":"Grain size","options":["fine","medium","coarse"],"default":"medium"},"vignette":{"type":"number","label":"Vignette","default":0.4,"min":0,"max":1,"step":0.05}},"beats":{},"pack":"effects","ref":"effects/grain","code":"// Effects: looks built from SVG filters (turbulence, displacement, blur, colour matrices), which resvg and\n// browsers both draw, so a render and the live player match. Every filter is a pure function of t.\n\n/** Fixed-precision number for markup. */\nfunction f2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A glow filter (a blur of the shape under the shape itself); returns its id. */\nfunction glowFilter($, std, strength) {\n var id = $.uid();\n var copies = \"\";\n for (var i = 0; i < (strength || 2); i++) copies += '<feMergeNode in=\"b\"/>';\n $.def('<filter id=\"' + id + '\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\"><feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"' + f2(std) + '\" result=\"b\"/><feMerge>' + copies + '<feMergeNode in=\"SourceGraphic\"/></feMerge></filter>');\n return id;\n}\n\n/** A title sized to fit the safe area's width, centred: { font, x, y, lines }. */\nfunction fitTitle($, str, share, maxSize) {\n var A = safeArea($);\n var f = fitFont($, str, \"display\", Math.min(maxSize, $.W * share), A.w * 0.9, { ls: -0.03 });\n return { font: f, x: $.W / 2, y: $.H / 2 - f.size * 0.56 };\n}\n\nfunction frame(t, $) {\n var p = $.data, W = $.W, H = $.H;\n var amount = $.clamp(Number(p.amount)), vig = $.clamp(Number(p.vignette));\n var freq = { fine: 0.95, medium: 0.7, coarse: 0.45 }[p.size] || 0.7;\n var s = \"\";\n if (amount > 0) {\n // Grey noise, pushed light and dark around the middle, at a low opacity: specks lighter and darker than the\n // picture under them. A new seed on every film frame (24 a second) moves it the way film stock does.\n var seed = Math.floor($.step(t, 24) * 24) % 997;\n var id = $.uid();\n $.def('<filter id=\"' + id + '\" x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" color-interpolation-filters=\"sRGB\"><feTurbulence type=\"fractalNoise\" baseFrequency=\"' + f2(freq / Math.max(1, $.k)) + '\" numOctaves=\"2\" seed=\"' + seed + '\" result=\"n\"/><feColorMatrix in=\"n\" type=\"matrix\" values=\"2.4 0 0 0 -0.7 2.4 0 0 0 -0.7 2.4 0 0 0 -0.7 0 0 0 0 ' + f2(amount * 0.42) + '\"/></filter>');\n s += '<rect x=\"0\" y=\"0\" width=\"' + W + '\" height=\"' + H + '\" filter=\"url(#' + id + ')\"/>';\n }\n if (vig > 0) {\n var g = $.uid();\n $.def('<radialGradient id=\"' + g + '\" cx=\"50%\" cy=\"50%\" r=\"72%\"><stop offset=\"55%\" stop-color=\"#000\" stop-opacity=\"0\"/><stop offset=\"100%\" stop-color=\"#000\" stop-opacity=\"' + f2(vig * 0.85) + '\"/></radialGradient>');\n s += '<rect x=\"0\" y=\"0\" width=\"' + W + '\" height=\"' + H + '\" fill=\"url(#' + g + ')\"/>';\n }\n return s;\n}\n","libs":[]},{"id":"light-leak","name":"Light leak","description":"Warm light bleeding in from the edge of the frame and drifting across it, in the brand's colours: for openings, transitions and a film feel over footage.","category":"scene","tags":["effect","light leak","film","glow","flare","warm","overlay","transition"],"placement":"overlay","duration":{"default":4,"min":1,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"from":{"type":"choice","label":"From","options":["left","right","top","corner"],"default":"left"},"strength":{"type":"number","label":"Strength","default":0.7,"min":0.1,"max":1,"step":0.05}},"beats":{},"pack":"effects","ref":"effects/light-leak","code":"// Effects: looks built from SVG filters (turbulence, displacement, blur, colour matrices), which resvg and\n// browsers both draw, so a render and the live player match. Every filter is a pure function of t.\n\n/** Fixed-precision number for markup. */\nfunction f2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A glow filter (a blur of the shape under the shape itself); returns its id. */\nfunction glowFilter($, std, strength) {\n var id = $.uid();\n var copies = \"\";\n for (var i = 0; i < (strength || 2); i++) copies += '<feMergeNode in=\"b\"/>';\n $.def('<filter id=\"' + id + '\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\"><feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"' + f2(std) + '\" result=\"b\"/><feMerge>' + copies + '<feMergeNode in=\"SourceGraphic\"/></feMerge></filter>');\n return id;\n}\n\n/** A title sized to fit the safe area's width, centred: { font, x, y, lines }. */\nfunction fitTitle($, str, share, maxSize) {\n var A = safeArea($);\n var f = fitFont($, str, \"display\", Math.min(maxSize, $.W * share), A.w * 0.9, { ls: -0.03 });\n return { font: f, x: $.W / 2, y: $.H / 2 - f.size * 0.56 };\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, W = $.W, H = $.H, D = $.dur;\n var strength = $.clamp(Number(p.strength) || 0.7, 0.1, 1);\n // It swells in, drifts, and fades out by the end.\n var life = $.E.out3($.seg(t, 0, Math.min(1, D * 0.3))) * (1 - $.E.in2($.seg(t, D - Math.min(1, D * 0.3), D)));\n var u = D > 0 ? t / D : 0;\n var origin = { left: [0, 0.45], right: [1, 0.55], top: [0.5, 0], corner: [0, 0] }[p.from] || [0, 0.45];\n var dir = { left: [1, 0.15], right: [-1, -0.1], top: [0.1, 1], corner: [0.8, 0.6] }[p.from] || [1, 0.15];\n var blobs = [\n { c: c.accent, r: 0.75, d: 0.18, a: 0.55 },\n { c: c.accent2 || c.accent, r: 0.5, d: 0.32, a: 0.45 },\n { c: $.mix(c.accent2 || c.accent, \"#FFFFFF\", 0.55), r: 0.3, d: 0.45, a: 0.5 },\n ];\n var s = \"\";\n var L = Math.max(W, H);\n for (var i = 0; i < blobs.length; i++) {\n var b = blobs[i], id = $.uid();\n var x = (origin[0] + dir[0] * b.d * u) * W + Math.sin(t * 0.7 + i * 2.1) * L * 0.03;\n var y = (origin[1] + dir[1] * b.d * u) * H + Math.cos(t * 0.5 + i * 1.3) * L * 0.03;\n $.def('<radialGradient id=\"' + id + '\"><stop offset=\"0%\" stop-color=\"' + b.c + '\" stop-opacity=\"' + f2(b.a * strength * life) + '\"/><stop offset=\"60%\" stop-color=\"' + b.c + '\" stop-opacity=\"' + f2(b.a * 0.35 * strength * life) + '\"/><stop offset=\"100%\" stop-color=\"' + b.c + '\" stop-opacity=\"0\"/></radialGradient>');\n s += '<circle cx=\"' + f2(x) + '\" cy=\"' + f2(y) + '\" r=\"' + f2(L * b.r) + '\" fill=\"url(#' + id + ')\"/>';\n }\n return s;\n}\n","libs":[]},{"id":"glass","name":"Glass card","description":"A frosted glass card over soft moving colour: the ground blurs through the glass, a highlight runs along its edge, and the title sets on it.","category":"title","tags":["effect","glass","glassmorphism","frosted","blur","title","card","gradient"],"placement":"full","duration":{"default":5,"min":2,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"kicker":{"type":"text","label":"Kicker","default":"New","maxLength":20,"description":"empty for none"},"title":{"type":"text","label":"Title","default":"Made for agents","maxLength":28},"subtitle":{"type":"text","label":"Subtitle","default":"Motion graphics as code","maxLength":44,"description":"empty for none"}},"beats":{"in":0.2},"pack":"effects","ref":"effects/glass","code":"// Effects: looks built from SVG filters (turbulence, displacement, blur, colour matrices), which resvg and\n// browsers both draw, so a render and the live player match. Every filter is a pure function of t.\n\n/** Fixed-precision number for markup. */\nfunction f2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A glow filter (a blur of the shape under the shape itself); returns its id. */\nfunction glowFilter($, std, strength) {\n var id = $.uid();\n var copies = \"\";\n for (var i = 0; i < (strength || 2); i++) copies += '<feMergeNode in=\"b\"/>';\n $.def('<filter id=\"' + id + '\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\"><feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"' + f2(std) + '\" result=\"b\"/><feMerge>' + copies + '<feMergeNode in=\"SourceGraphic\"/></feMerge></filter>');\n return id;\n}\n\n/** A title sized to fit the safe area's width, centred: { font, x, y, lines }. */\nfunction fitTitle($, str, share, maxSize) {\n var A = safeArea($);\n var f = fitFont($, str, \"display\", Math.min(maxSize, $.W * share), A.w * 0.9, { ls: -0.03 });\n return { font: f, x: $.W / 2, y: $.H / 2 - f.size * 0.56 };\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n var tin = $.beat(\"in\", 0.2);\n var L = Math.min(W, H);\n // The ground: the brand's ground with three soft colour fields drifting on slow loops.\n var fields = [\n { col: c.accent, x: 0.3, y: 0.35, r: 0.42, sp: 0.21 },\n { col: c.accent2 || c.accent, x: 0.72, y: 0.62, r: 0.36, sp: 0.17 },\n { col: $.mix(c.accent, c.accent2 || c.accent, 0.5), x: 0.55, y: 0.2, r: 0.28, sp: 0.27 },\n ];\n var blobs = \"\";\n for (var i = 0; i < fields.length; i++) {\n var f = fields[i];\n var x = (f.x + Math.sin(t * f.sp * 2 + i) * 0.06) * W, y = (f.y + Math.cos(t * f.sp * 2 + i * 1.7) * 0.06) * H;\n blobs += '<circle cx=\"' + f2(x) + '\" cy=\"' + f2(y) + '\" r=\"' + f2(L * f.r) + '\" fill=\"' + f.col + '\"/>';\n }\n var soft = $.uid();\n $.def('<filter id=\"' + soft + '\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\"><feGaussianBlur stdDeviation=\"' + f2(L * 0.08) + '\"/></filter>');\n var s = '<rect x=\"0\" y=\"0\" width=\"' + W + '\" height=\"' + H + '\" fill=\"' + c.bg + '\"/><g filter=\"url(#' + soft + ')\">' + blobs + \"</g>\";\n\n // The glass: the same fields blurred harder, clipped to the card, a frost over them and a lit edge.\n var A = safeArea($);\n var title = $.case(p.title), sub = p.subtitle ? $.case(p.subtitle) : \"\", kick = p.kicker ? $.case(p.kicker) : \"\";\n var tf = fitFont($, title, \"display\", L * 0.11, A.w * 0.72, { ls: -0.03 });\n var sf = { size: tf.size * 0.32, weight: 500, family: $.font(\"text\").family };\n var cw = Math.min(A.w, Math.max($.measure(title, tf), sub ? $.measure(sub, sf) : 0) + tf.size * 1.6);\n var ch = tf.size * (sub ? 2.4 : 1.8) + (kick ? tf.size * 0.7 : 0);\n var rise = $.E.out3($.seg(t, tin, tin + 0.7));\n var cx = W / 2 - cw / 2, cy = H / 2 - ch / 2 + (1 - rise) * 40 * k;\n var r = Math.min(ch * 0.25, Math.max(14 * k, $.style.radius));\n var clip = $.uid(), frost = $.uid(), edge = $.uid();\n $.def('<clipPath id=\"' + clip + '\"><rect x=\"' + f2(cx) + '\" y=\"' + f2(cy) + '\" width=\"' + f2(cw) + '\" height=\"' + f2(ch) + '\" rx=\"' + f2(r) + '\"/></clipPath>');\n $.def('<filter id=\"' + frost + '\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\"><feGaussianBlur stdDeviation=\"' + f2(L * 0.14) + '\"/></filter>');\n var sweep = $.seg(t, tin + 0.4, tin + 1.6);\n $.def('<linearGradient id=\"' + edge + '\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"' + f2(Math.max(0, sweep - 0.25)) + '\" stop-color=\"#FFFFFF\" stop-opacity=\"0.15\"/><stop offset=\"' + f2(sweep) + '\" stop-color=\"#FFFFFF\" stop-opacity=\"0.85\"/><stop offset=\"' + f2(Math.min(1, sweep + 0.25)) + '\" stop-color=\"#FFFFFF\" stop-opacity=\"0.15\"/></linearGradient>');\n var glass = '<g clip-path=\"url(#' + clip + ')\"><rect x=\"0\" y=\"0\" width=\"' + W + '\" height=\"' + H + '\" fill=\"' + c.bg + '\"/><g filter=\"url(#' + frost + ')\">' + blobs + '</g><rect x=\"' + f2(cx) + '\" y=\"' + f2(cy) + '\" width=\"' + f2(cw) + '\" height=\"' + f2(ch) + '\" fill=\"#FFFFFF\" fill-opacity=\"0.16\"/></g>';\n glass += '<rect x=\"' + f2(cx) + '\" y=\"' + f2(cy) + '\" width=\"' + f2(cw) + '\" height=\"' + f2(ch) + '\" rx=\"' + f2(r) + '\" fill=\"none\" stroke=\"url(#' + edge + ')\" stroke-width=\"' + f2(2.5 * k) + '\"/>';\n var ty = cy + ch / 2 - (sub ? tf.size * 0.85 : tf.size * 0.56) + (kick ? tf.size * 0.35 : 0);\n if (kick) glass += $.chip(kick, W / 2, cy + tf.size * 0.35, { size: tf.size * 0.26, anchor: \"middle\", fill: c.accent, color: $.on(c.accent) });\n glass += $.text(title, Object.assign({}, tf, { x: W / 2, y: ty, anchor: \"middle\", fill: c.fg }));\n if (sub) glass += $.text(sub, Object.assign({}, sf, { x: W / 2, y: ty + tf.size * 1.15, anchor: \"middle\", fill: c.fg, op: 0.82 }));\n s += '<g opacity=\"' + f2(Math.min(1, rise * 1.4)) + '\">' + glass + \"</g>\";\n return s;\n}\n","libs":[]},{"id":"chromatic","name":"Chromatic split","description":"A title whose colour channels arrive apart and snap together, then split again on a hit: a lens-fringe look in the brand's two accents.","category":"text","tags":["effect","chromatic aberration","rgb split","glitch","title","text effect","fringe"],"placement":"overlay","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"title":{"type":"text","label":"Title","default":"Signal found","maxLength":24},"hits":{"type":"number","label":"Split again (times)","default":2,"min":0,"max":6,"step":1}},"beats":{"in":0.1},"pack":"effects","ref":"effects/chromatic","code":"// Effects: looks built from SVG filters (turbulence, displacement, blur, colour matrices), which resvg and\n// browsers both draw, so a render and the live player match. Every filter is a pure function of t.\n\n/** Fixed-precision number for markup. */\nfunction f2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A glow filter (a blur of the shape under the shape itself); returns its id. */\nfunction glowFilter($, std, strength) {\n var id = $.uid();\n var copies = \"\";\n for (var i = 0; i < (strength || 2); i++) copies += '<feMergeNode in=\"b\"/>';\n $.def('<filter id=\"' + id + '\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\"><feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"' + f2(std) + '\" result=\"b\"/><feMerge>' + copies + '<feMergeNode in=\"SourceGraphic\"/></feMerge></filter>');\n return id;\n}\n\n/** A title sized to fit the safe area's width, centred: { font, x, y, lines }. */\nfunction fitTitle($, str, share, maxSize) {\n var A = safeArea($);\n var f = fitFont($, str, \"display\", Math.min(maxSize, $.W * share), A.w * 0.9, { ls: -0.03 });\n return { font: f, x: $.W / 2, y: $.H / 2 - f.size * 0.56 };\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, D = $.dur;\n var tin = $.beat(\"in\", 0.1);\n var str = $.case(p.title);\n var T = fitTitle($, str, 0.14, 220 * k);\n var settle = $.E.out3($.seg(t, tin, tin + 0.6));\n var split = (1 - settle) * T.font.size * 0.35;\n // Hits: the channels jolt apart and back, spread evenly through the hold.\n var n = Math.max(0, Math.round(Number(p.hits) || 0));\n for (var i = 1; i <= n; i++) split += $.bump(t, tin + 0.8 + ((D - tin - 1.2) * i) / (n + 1), 0.04, 0.22) * T.font.size * 0.12;\n var op = $.fadeIn(t, tin, 0.15) * $.fadeOut(t, D, 0.3);\n var base = Object.assign({}, T.font, { y: T.y, anchor: \"middle\" });\n var s = $.text(str, Object.assign({}, base, { x: T.x - split, fill: c.accent, op: 0.9 * op }));\n s += $.text(str, Object.assign({}, base, { x: T.x + split, fill: c.accent2 || c.accent, op: 0.9 * op }));\n s += $.text(str, Object.assign({}, base, { x: T.x, fill: c.fg, op: op }));\n return s;\n}\n","libs":[]},{"id":"warp","name":"Liquid warp","description":"A title that settles out of a liquid ripple: rolling noise bends the letters, eases off until they read clean, then keeps a slow shimmer.","category":"text","tags":["effect","liquid","warp","distortion","ripple","title","text effect","heat haze"],"placement":"overlay","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"title":{"type":"text","label":"Title","default":"Fluid by design","maxLength":24},"shimmer":{"type":"boolean","label":"Keep a shimmer","default":true}},"beats":{"in":0.1},"pack":"effects","ref":"effects/warp","code":"// Effects: looks built from SVG filters (turbulence, displacement, blur, colour matrices), which resvg and\n// browsers both draw, so a render and the live player match. Every filter is a pure function of t.\n\n/** Fixed-precision number for markup. */\nfunction f2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A glow filter (a blur of the shape under the shape itself); returns its id. */\nfunction glowFilter($, std, strength) {\n var id = $.uid();\n var copies = \"\";\n for (var i = 0; i < (strength || 2); i++) copies += '<feMergeNode in=\"b\"/>';\n $.def('<filter id=\"' + id + '\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\"><feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"' + f2(std) + '\" result=\"b\"/><feMerge>' + copies + '<feMergeNode in=\"SourceGraphic\"/></feMerge></filter>');\n return id;\n}\n\n/** A title sized to fit the safe area's width, centred: { font, x, y, lines }. */\nfunction fitTitle($, str, share, maxSize) {\n var A = safeArea($);\n var f = fitFont($, str, \"display\", Math.min(maxSize, $.W * share), A.w * 0.9, { ls: -0.03 });\n return { font: f, x: $.W / 2, y: $.H / 2 - f.size * 0.56 };\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, D = $.dur;\n var tin = $.beat(\"in\", 0.1);\n var str = $.case(p.title);\n var T = fitTitle($, str, 0.13, 200 * k);\n var settle = $.E.out3($.seg(t, tin, tin + 1.1));\n var amp = (1 - settle) * T.font.size * 0.5 + (p.shimmer ? T.font.size * 0.025 : 0);\n var id = $.uid();\n // The noise drifts with time (its offset moves), so the ripple rolls rather than flickers.\n $.def('<filter id=\"' + id + '\" x=\"-20%\" y=\"-60%\" width=\"140%\" height=\"220%\"><feTurbulence type=\"turbulence\" baseFrequency=\"' + f2(0.012 / Math.max(0.5, k)) + \" \" + f2(0.045 / Math.max(0.5, k)) + '\" numOctaves=\"2\" seed=\"7\" result=\"n\"/><feOffset in=\"n\" dx=\"' + f2(t * 60 * k) + '\" dy=\"0\" result=\"m\"/><feDisplacementMap in=\"SourceGraphic\" in2=\"m\" scale=\"' + f2(amp) + '\" xChannelSelector=\"R\" yChannelSelector=\"G\"/></filter>');\n var op = $.fadeIn(t, tin, 0.3) * $.fadeOut(t, D, 0.3);\n return '<g filter=\"url(#' + id + ')\">' + $.text(str, Object.assign({}, T.font, { x: T.x, y: T.y, anchor: \"middle\", fill: c.fg, op: op })) + \"</g>\";\n}\n","libs":[]},{"id":"neon","name":"Neon sign","description":"A neon sign that flickers on: glass tubes of light in the accent with a glow around them, a buzz of flicker as it strikes, then a steady hum.","category":"title","tags":["effect","neon","glow","sign","flicker","night","title","retro"],"placement":"full","duration":{"default":4,"min":2,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"title":{"type":"text","label":"Sign","default":"Open late","maxLength":20},"subtitle":{"type":"text","label":"Under it","default":"since 2026","maxLength":30,"description":"empty for none"}},"beats":{"on":0.3},"pack":"effects","ref":"effects/neon","code":"// Effects: looks built from SVG filters (turbulence, displacement, blur, colour matrices), which resvg and\n// browsers both draw, so a render and the live player match. Every filter is a pure function of t.\n\n/** Fixed-precision number for markup. */\nfunction f2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A glow filter (a blur of the shape under the shape itself); returns its id. */\nfunction glowFilter($, std, strength) {\n var id = $.uid();\n var copies = \"\";\n for (var i = 0; i < (strength || 2); i++) copies += '<feMergeNode in=\"b\"/>';\n $.def('<filter id=\"' + id + '\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\"><feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"' + f2(std) + '\" result=\"b\"/><feMerge>' + copies + '<feMergeNode in=\"SourceGraphic\"/></feMerge></filter>');\n return id;\n}\n\n/** A title sized to fit the safe area's width, centred: { font, x, y, lines }. */\nfunction fitTitle($, str, share, maxSize) {\n var A = safeArea($);\n var f = fitFont($, str, \"display\", Math.min(maxSize, $.W * share), A.w * 0.9, { ls: -0.03 });\n return { font: f, x: $.W / 2, y: $.H / 2 - f.size * 0.56 };\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n var on = $.beat(\"on\", 0.3);\n var str = $.case(p.title), sub = p.subtitle ? $.case(p.subtitle) : \"\";\n var T = fitTitle($, str, 0.14, 210 * k);\n var tube = c.accent, tube2 = c.accent2 || c.accent;\n // Strikes on with a few hard flickers, then hums (a slight shimmer of brightness).\n var flick = [0, 0.06, 0.1, 0.16, 0.22, 0.3, 0.34];\n var lit = t < on ? 0 : 1;\n for (var i = 0; i < flick.length; i += 2) if (t >= on + flick[i] && t < on + flick[i + 1]) lit = 0.15;\n var hum = 0.93 + 0.07 * Math.sin(t * 47) * Math.sin(t * 13);\n var b = lit * (t > on + 0.4 ? hum : 1);\n var glow = glowFilter($, T.font.size * 0.12, 2);\n var dark = $.mix(c.ink || \"#000000\", \"#000000\", 0.35);\n var s = '<rect x=\"0\" y=\"0\" width=\"' + W + '\" height=\"' + H + '\" fill=\"' + dark + '\"/>';\n // The unlit tubes stay faintly visible; the lit ones glow.\n var base = Object.assign({}, T.font, { x: T.x, y: T.y - (sub ? T.font.size * 0.3 : 0), anchor: \"middle\", fill: \"none\", stroke: $.mix(dark, tube, 0.25), sw: T.font.size * 0.045 });\n s += $.text(str, base);\n if (b > 0) s += '<g filter=\"url(#' + glow + ')\" opacity=\"' + f2(b) + '\">' + $.text(str, Object.assign({}, base, { stroke: tube })) + $.text(str, Object.assign({}, base, { stroke: $.mix(tube, \"#FFFFFF\", 0.6), sw: T.font.size * 0.015 })) + \"</g>\";\n if (sub) {\n var sf = { size: T.font.size * 0.3, weight: 600, family: $.font(\"text\").family };\n var sb = $.seg(t, on + 0.5, on + 0.6) * hum;\n var sy = T.y + T.font.size * 1.0;\n s += $.text(sub, Object.assign({}, sf, { x: T.x, y: sy, anchor: \"middle\", fill: \"none\", stroke: $.mix(dark, tube2, 0.25), sw: sf.size * 0.05 }));\n if (sb > 0) s += '<g filter=\"url(#' + glow + ')\" opacity=\"' + f2(sb) + '\">' + $.text(sub, Object.assign({}, sf, { x: T.x, y: sy, anchor: \"middle\", fill: \"none\", stroke: tube2, sw: sf.size * 0.06 })) + \"</g>\";\n }\n return s;\n}\n","libs":[]}]}],"libs":{"world":"// ———————— Volito world ————————\nvar MM_WORLD = [[\"Afghanistan\",4,66.1,33.9,\"qh@kV_@FOEE@EGK?EIGGIB@DE@@NGF]Oa@@CDbAJHFEPHHAFDDR?GJJDFJ?JFDT?@DN?HJ@NXFLABDJCRB^IQQ@MNAF]IIHCMe@SDOACIYGCOQCAGOD\"],[\"Albania\",8,20,41.1,\"cLoX?BDB@DFHNM@GCSBIGIABEAGFATGD\"],[\"Algeria\",12,2.6,28.2,\"lDcP?[_@Oe@GEIWGAMIAIGYCCGDCF_@FMSIUC_@MaAEIBSGi@BBHCPDNJJAN]VMt@@x@FFKL?HGHICOHGJbAd@x@f@ZFT@@KRGDIpEgC\"],[\"Angola\",24,17.5,-12.3,\"cG~AFBBLB@DOMKGF uFxBUCy@?KXKPSEIBGOKAACI?@FW?CR?VEF?VYCG?AD?`@h@??~@YZb@Fn@ALIrA@JGLATF@KKm@IYQUAM?KN]EKHa@HKAC\"],[\"Antarctica\",10,31.2,-77.3,\"l]vo@g@C]BYHGJCP~@JnAFn@?XGCGi@EQEm@_@ lh@dq@wA@YOSFJRtAAVEJE dm@pk@c@AEIAWIIQAKFSVGPHNl@DX?IGl@DNE?GUG |~@|k@KCkAFUAKHzA?RCHG rkA`m@CGq@FWC\\\\JZARE pnA|l@MEs@L`AG heBbp@KGe@Ba@JEHd@@XEXM noB|s@QKc@F]G[F]Gy@C{@LqDNy@EuABo@DkBKCGnCENGt@CCIMMBIlAQ}AA]DcAKOGFGt@IrBEv@UDU[FaACOF_@AiAO]A?GFGEEYCKDu@Gu@AiAM]@[A[Bu@Cw@BqB?k@IYBm@I_@RSEWFq@Fu@EuAFIGVMVAJGJU{ADSDGF[?mAGSBYAQMOFUBYAOFiAFWOSF[Aa@JYAi@ImAGQCKEEIBGV]?GAGOOCEDQGGu@YGGWKS?cAWOAKBFFZHNCN@ZHHD@F?FID^Db@TBFQL_@H[\\\\EFAPMV@HVLXBTLjBPNFbC@EFs@HKDTD`@AZD@NWDCFWFk@@cBPaBFgAL]N{AQkAKq@?yAFKG[Es@A_ESTM?EtBBDGAOIC}@GaAS_CKcBSc@KEGTCGGMG}@MYQMCW?IFW??GIGU@EFW@q@EU?IFUEkBO[KOBSAYNWCGGSCY?GFOGcAAk@BUJyACaBOOEUQSBGDQBS?[JUEEGg@Ii@Eo@MQ@_@KQ?QCAGQEu@Ge@BOBAH[JW@]JS?a@Kw@Hg@?OT?B@Hb@JAFW?BFRNODW@UCQOWKOOiAGOOMGiAUoAAMCIMONQ@yA?_@Dy@ES@}@_@[Fg@Pe@@i@E]Ke@Cg@NU?e@HW@UA[KQAg@Bc@Ac@Bg@GoACEQKDCFMNQBoBCo@@MDBFKDk@HiAHU?MEq@No@BIFe@JqBB_AJMD@Fb@^X@JFXBd@TJL@PWTe@@GHfBFPHBHPLYFIFi@LgBPIHy@BQFw@CoAH\"],[\"Argentina\",32,-65.1,-35.2,\"zi@z_@OVYL[BFHR?HE^??m@ ~b@zQHb@?RDD@TYPBLMF@HPX\\\\Hf@BTACJBNCHJDTBPGFDARMDIGEJPDNJF\\\\P?NJDLSLSBFPVJJVPFFFETKJhAGFK?ML?DE@UOGEM@IIOEY@KIC@GHCEGFGDUICBYIe@KEDS?QMM?OKS?QDCFa@KU@QEQMSMKDGCE?a@UIES@EOQYDKLGOW?g@d@O@k@VCFR\\\\i@FOAQOCQICIJ?N^Rn@l@\"],[\"Armenia\",51,45,40.2,\"a\\\\gWF@FOF?^OAGBG[AKFBBIFDDQF?L\"],[\"Australia\",36,134.4,-25.6,\"i{AnXK@AVFD@NDELLPAJO@MJO?Ia@FYE ymAbSd@NHP\\\\@PCZBJHD?RJZA^O?KMEAUNk@?IFSFGBOLWIFFQOL?IN[GY@KGMALGMi@UM?e@M_@EY]AQMQGPICFIEIIBAMOQIC?EG@?CSEYTW@BKIOGC@EGKKEI@QC?INEKCWLOBEAKDWEIHLPD?AFLRADw@b@ODCDQDMEGSG[BYES@EG[GEEHAJC@AFEHARELMEON@FCPAHE@EP@HELo@Z@BKLGRGCGHCCCTg@b@CN?VINLdAHRLFLXPh@@RJFT?PFTNZKAIHBPLz@OJKFYHGRAEIBMHLPBKKAKGI@MNPJDFNNG?KTUCCz@Ub@@n@LRA\"],[\"Austria\",40,14.1,47.6,\"sIa]@FJ?CBHNP?HDj@GBERB@@^EAGEAIDAE_@AOB?OEAGIMDSGMDIAIB?H\"],[\"Azerbaijan\",31,47.7,40.3,\"_\\\\eYE@MJG?OKSVI@EBN@FVDB?HD?JIGIDE^N?MPGEEHGCCJGCCYDAAHIEC y[eWJCLOQBGN\"],[\"Bahamas\",44,-77.9,24.5,\"jp@wOW??BTB@G ro@{OOFBLBAAIHI zo@wNE?GP?HD@BKFECK\"],[\"Bangladesh\",50,90.3,23.8,\"}x@wL?LFCANJ]FKP?AFDJHC@@LCH]CKJECGKELKGKOFG?ALq@DHPF?DJIHCKC?K^\"],[\"Belarus\",112,28,53.5,\"sPcb@SDCBIASBAHBDSN@BMBEBFDR?IRP@DB?HhBKb@D?GDIKCDWQ?UGEIOG@Ga@K\"],[\"Belgium\",56,4.6,50.7,\"{Bw^BLB?@JPIH@VQF?BEq@GWL\"],[\"Belize\",84,-88.7,17.2,\"tv@cJACC@EKGBDb@HJD?Ae@\"],[\"Benin\",204,2.3,9.6,\"u@}BNBDMAm@BC@IJMAIEACGIAKKE?OHCRBFABNRBJ?j@\"],[\"Bhutan\",64,90.5,27.4,\"ix@kPGD@L^ALBPG?CMMIEYDGB\"],[\"Bolivia\",68,-64.6,-16.7,\"lj@zEW?EE[OYA@b@UPU@GFUHK?KDCXB?EVg@?BJAFKDCJHVAJDB?EPIt@BHPF`@@CV?FNJMXENPLBRq@GSJG@MHMKUFOCG@EGI?_@CEN_@\"],[\"Bosnia and Herz.\",70,17.8,44.2,\"sJsYPGVSNOEGEBCCI?o@DDHIF@FNF@J\"],[\"Botswana\",72,23.8,-22.1,\"kQxLZLPNJRJ@BNLBN?PGHBBHRLL@BEAKJSDA?{@S??eAm@GGFKGUCQ^UTG@ERO@KH\"],[\"Brazil\",76,-53.1,-10.8,\"j`@bTBKGIJMb@UF?ROJ@o@m@_@S?OHKHBEa@FCLBF_@LEFBRC?[DIEC@KIWBKJE@GCKf@?DWC?BYJEJ?TIFGTATQAc@X@ZNDDj@??]NJP?FKJ?CIJKFQEC?GKE@IEOi@SQ?I_ABKFE?MMAAGJA?Ke@?GEGPCCKJOAAEWGAGME?CPA@YFEC?]DECc@IGG@EI?CB@FEBEFDFBNGRKHI?WKY@?MQ?SFEEGDI?EGQa@G?Ol@IBALNNEDc@@?ROKy@RIHBJWEe@H]?]NYTMDQ?GFIb@F`@d@f@Zf@D@DLAd@Fj@DFBXRX@RPHBJT?\\\\FNFTDTNPR@LAJF\\\\LHTb@^VFRLJ\"],[\"Brunei\",96,114.9,4.7,\"egAkBBTF?BDHIYQ\"],[\"Bulgaria\",100,25.2,42.8,\"eMsZCFu@@_@I[HJFDLEJPARD?HP@LGNDL?@MHECKGEHG@GEC\"],[\"Burkina Faso\",854,-1.8,12.3,\"jBoE@KEG?EOOAMEEIBGCUUQGKACBM??PKL?FWD?LDDH@BFD@jA?AZLEH?DDTO\"],[\"Burundi\",108,29.9,-3.4,\"aRn@ERRTH??WDIK@EKK@\"],[\"Cambodia\",116,104.9,12.7,\"c_AsFDWMOYCQ@OFIMQFCL@V`@LGJR@PFNAP_@\"],[\"Cameroon\",120,12.6,5.7,\"aHaGGL?XKPX?BHIHIBGTP\\\\?\\\\QZID?X^I|AACOFKHCFKESGQEAKIGAKHKGIYKGOi@OOCIFG?EEA\"],[\"Canada\",124,-101.6,57.7,\"vkAs]v@[b@GJQCKXIBOVM?III?K`@M`@c@\\\\QJIf@P\\\\URER??{DyANi@M]@}@MMFMCEIM@_@P[MANWCGEW@mANo@@YHZHc@BgAESJUIRGKGi@Ac@NUAa@H}@CBKQC_@F?PKOO?ISl@SAUWMY@SH[TPHe@B?RYOWLDLSLSMMQAUy@BYHAHNJMH@Hd@LZ@TE\\\\b@VLZ@NF@JTBXNRTFN@V]@Q`@[Ce@Fa@Pm@Lu@B@NERMT]ROEIUH]LK_@GUMKM@MLQVMWUHQD]MEu@FQCi@PEFc@??PEVQBMJ]KSUKG_A`AFLk@T_@DMDGNMBGDAVZL`@DVP`@@j@Cp@@NLXFr@l@OC_@Yi@O]AQHPJKd@YHa@CSU?LMFVJ~@RVNLA@Qc@Ot@BLK?[FELBDCNLLVLB@DbA?^RDFd@?H@EJXHTBTHLES_@Ha@RIACFABGH@DIdA[PD\\\\ENBt@KDABKF??FfP? ns@af@OGY??BVHL?BE zp@ol@TIAGGAm@@c@J?DdA? dq@ye@GEKBFJFABG ny@{m@JFr@EII[CYJ ry@mo@n@ABCg@?MB@@ n{@gp@WFBD\\\\DNEHG?Gc@@ `v@om@tAGHURGh@ATGGGg@@UDg@?QDDFc@Hw@@_@CiA?SFEFLBZDXC`BB pdA{o@[BDDd@DZEOG[A jdAgp@YBv@B?CSEI@ va@a_@V\\\\MEKBDDQDGESFDLMCIVHNTACOBCVPJ?MIRCz@?@EKGFCOKS_@MKOEI?BD ls@ug@m@J?HOAMFPD\\\\EJGn@RFIX@OIIa@M?CHICKD fp@gl@SGgARAHe@ESLo@FQFQPb@Hm@J_@D]P]?DL`@VVG^SX@@Jw@XKRDLjASq@ZAFt@Gh@KVIEEx@S?Dx@BPGMMmACDEGIWSJO\\\\Ih@EMCTMP?NGHDb@BrCMNGSGZ?DSOQSGs@ENJOLQOs@Ia@TBJg@E `z@im@oAB\\\\PVBTNTAJO?IIISC xkAqn@kA[y@A@LNFvAJZC |qAw`@SAFTQNF?\\\\W@OIB |`Aqp@}AHUP~@Gl@ASEVC@G dlAi]H@`@GDGNEBETCDI?Es@HQNSFGJ |jAom@mABi@LhARXL?Fr@HHGl@Kg@c@RKcAA jbAkn@e@CCHJHbA@p@H\\\\?BEi@IxABZC[SQEw@Fc@Ja@?ZQQGS@OP paAul@UFQ`@eAR@F^@KFDDfAGz@FtADJIXEP@VOaBGd@CfA@HGm@G^?`@E]Uu@KSBHFm@EYHWIQFOPIGLSQAS@ v}@ml@TMUGWBc@CEDRH_@FBP`@FPALEp@O?Ei@@ raA_m@Y?OBPJ\\\\MEA p|@}n@OF?JHL^@TC?I^?@MU?]E[?AA ~z@kq@MESAFCm@AYJaAFOJWFZDb@LlA?TIAEOEd@?TEJIMG fx@er@{AG]GW@UDOK}@CgA?y@AkCBmAF?BrBPk@?nAP`@Nt@Fz@@[@LBOHp@JHFZDCBa@??Bt@Jr@Ex@BbAC@Ic@CHMMAs@HXM`@COGc@CEEZGFKeAB_@GrB?b@Gf@MBE ~m@ci@LDT?BIGIQCOD@J d{@mj@MFLDXEN@ZG_@Me@J hg@e^_@BUF?Bb@EPI ~f@k\\\\EHa@@HFF?VGDEGE\"],[\"Central African Rep.\",140,20.4,6.5,\"cPgB`@APFFCTHHAFL\\\\EZMRN?LZCJHHP@MHEP[?O?MOWOACEGDYGQM@GY?QIMUIGMAAFKJ@T_@V?DUREJMDCF\"],[\"Chad\",148,18.6,15.3,\"{MgKAnAPALZCBDDAFFNG?CPGD?DL@HFLTPHX?AFPLXFFEBDN@AEFUHCHICIY?JQ?YFMAGJA?KHGIYWQAYKq@NQB_@SI}CjA\"],[\"Chile\",152,-71.7,-37.3,\"zi@z_@?l@_@?DFNDx@Il@UZWgAXIIEMSGM@ nj@~IILALKFFRSp@MCADDRTH?`@BDEFLJLRDPAPJTG`@EB?PJR?NLL?PERJDHd@CXHBETGFDFIBAFHBAJDXHNAHDLNFATEDM??LGJaADP?XJBPF@VGl@WFMEKHK@a@GSUO\\\\EQOEa@WFIi@LEDXJCKaAIKFi@GAc@}AD]EO@WKYO{BD}@ICEI\"],[\"China\",156,103.9,36.6,\"mcAkJNE@QIGa@ECFFFDJNH eq@oY@IMEPa@q@KMc@g@DIIASO?OMGACLQH[FMNFVEFs@DWJKBIPIJ_ABYCSB]JW?IDUI_@G]?WG[OHOKK_@DSI]GMMMEm@?AEPMNGNFRCJBBGWc@WF[K?IQSIG?IHEOIWA[Am@L[h@GPa@FWLGP]?QG_@EPXDTLRVCPDEPBVH??HJKFJ\\\\FAHP?FELLd@VZDNFRDIIBGOKJIf@RJJR@HHKJOB?HOBUMQFM?AJZBHJPHHLSHGRY`@?NJBCJKDF^J@n@fAv@`@T@JHFEJFZHRBFTH?DMEGx@GFGAKPCHGNJb@@RFATH?BKLBVIEOJCBSTBAWSO?_@HCDK`@AGGHMLHNEh@ZN@FCXEHDLL@OJBl@E^KDIJCRKNEFBn@YBUM@?IFIAOTW^GDOPM@SJCF@BQEEBCSIMCSBGMYAEG_@ICE\"],[\"Colombia\",170,-73.1,3.9,\"xh@YBBFQFDd@??JK@@FL@?LGDCJH~@HIDAMSNGJ?FAHBNAJSZWD@PKD@NADGVK@EGA@IEEIAOUFECKBQCCBQFIAIG@CEDKACK@MMGACWKIK?ACO@_@WG?EDBDLBPTFXI@GL?RGFe@?MPa@CGBFP@LIVHHKJEP\"],[\"Congo\",178,15.1,-0.8,\"qJeA@JHVDh@@DTRFR?NZ\\\\FC@EH?DFLILJNSOKFKSGAIIHQ@GW@MHKIWDCL@DIAIW@_@HAKIQKI[B\"],[\"Costa Rica\",188,-84.2,10,\"pr@_EF@?FCBDLHCBEAGRIFIADBBJE@CAMBAEEMBI?IDCCEHQN\"],[\"Côte d'Ivoire\",384,-5.6,7.6,\"~CkEW@AEIACFKEUNEEI?MDCZFNBTGP@F`@CVBd@JAYROCG@IAEA??KIEHWAECA\"],[\"Croatia\",191,16.6,45,\"kIa\\\\SHQBEAKLFDFC^AH?BBDCDFg@b@QFB@n@SNMCAHG?GJADFDE?GM?CAM@?CGAAGOE\"],[\"Cuba\",192,-79,21.6,\"lr@oMa@@QDGFSAc@XQB@DO?MF@BLBv@?MIFEJ?FEBKJ?VGXCFCGCRALHF?BDP?WQ]G\"],[\"Cyprus\",196,33,34.9,\"mS}TY@RFHABGG?\"],[\"Czechia\",203,15.3,49.8,\"kH}^IDM@?DIBCEM@?FO?IHLBDFN@@B`@GRFb@QDOi@OM?\"],[\"Dem. Rep. Congo\",180,23.6,-2.8,\"iQxAEf@KJIVFA^DDLEFDj@QJECATNALQNCBIJDNAFIT??ENAXB?WDG?WBSV?AGH?@BJ@FNHCRDJQJYx@?TB@ECACMGCE@EGI?ADGB[]?OGSUSAEEi@IWAYSO[L]DGMI@UIMBCEGAa@@KNG@YEEFOL?VG@LJJRDd@DDDVEH?V\"],[\"Denmark\",208,9.3,56.2,\"eEka@JBNCFI?SGKQAWI@HBDADI@BFDANLEH wFab@EHJNTK@G]E\"],[\"Djibouti\",262,42.5,11.8,\"oYyFG?ECCD?FJDGBDJBCN?@IMQ\"],[\"Dominican Rep.\",214,-70.5,18.9,\"xk@gJ?EBECCAU_@DCDK?@DI?IFFFHCN?HBBCD@FLDC?C\"],[\"Ecuador\",218,-78.4,-1.5,\"bn@BCLDLTRVFLP@JJHFINA@EEE@GIMBIFHJICC@QECKY@GWKWJEFO@EAQJ\"],[\"Egypt\",818,29.8,26.5,\"aVwLlF??oCDOEMBGGIY?o@LWKQAMBEHCG_@DICK`@L`@DBNOL]@@c@hA_@j@B@?L[T\"],[\"El Salvador\",222,-88.9,13.7,\"zv@_HQJGCGBBLJA^IMM\"],[\"Eq. Guinea\",226,10.4,1.6,\"_Em@a@??Vb@@BCEU\"],[\"Eritrea\",232,38.7,15.4,\"wU_H@GKk@EEMAIKQh@e@Ze@b@DBF?\\\\_@PGL?BCJBJIDNVC\"],[\"Estonia\",233,25.8,58.6,\"oPed@ABLJEPFDN?XIPBAKD@LE@Ks@Gi@@\"],[\"eSwatini\",748,31.4,-26.5,\"aStOBHJ@JK?EEMO@EP\"],[\"Ethiopia\",231,39.6,8.7,\"{\\\\_Dv@z@X?PLL@BDL?FGPHDF\\\\CVOL?FG?IHCJUHCJQJAEKI?Ik@GEKWKIIc@WBEOKHKCCBM?QFMJORLPAHO?CBBDUZ_AVQ?\"],[\"Falkland Is.\",238,-59.4,-51.7,\"fe@j_@WIOBMGOFDFZDFGPHHI\"],[\"Fiji\",242,178,-17.8,\"inB|IECED@JL@HA@IGEG@ noB`ICA@HZH@G[I\"],[\"Finland\",246,26.2,64.5,\"{Pej@BL_@LPNUTLPQLFJ]LFHz@d@b@@bAJJKRECSFQGKQK{@Y@GZIDG@]x@WKEWJYASDSIIM]GYFFL\"],[\"Fr. S. Antarctic Lands\",260,69.5,-49.3,\"aj@j]MDQBABDF^@CW\"],[\"France\",250,2.3,46.6,\"h_@sAP`@DFH?FEDDFCIYFK@MIQUFUNAF {B}]ID[BHL@LDBHAABNJ?FIAEF?DEFFDELKBBFPJd@E\\\\D@JVBTIFBb@GFGIKCg@b@_@\\\\G@MYCa@DDWQHm@OEOQCCDG?WPIAUJEA mDsYMGCNFNFCDMCE\"],[\"Gabon\",266,11.7,-0.6,\"aFm@a@?ERMAEBHVIJALFVPAHI@HRFGJNJ`@e@JSMi@c@A?W\"],[\"Gambia\",270,-15.4,13.5,\"lIoGU?IEGDMCCDHBNCNFPAAG\"],[\"Georgia\",268,43.5,42.2,\"_XcZACm@F]JKASBEFKBDBIHJ@NE^DRIR@AIBK\\\\O\"],[\"Germany\",276,10.3,51.1,\"yGs`@EJDBEFEH@FGJL?h@NENUJFHD@?NNC^@@DHEZCBBN?AMIMZCHE?GBCCMBUK?CEEQBGCCO?CBMIDSOBKC?FSB?F_@I_@L\"],[\"Ghana\",288,-1.2,7.9,\"?{E?DGHA\\\\CFBPAHIR|@VPEAGFQCUGODw@y@?\"],[\"Greece\",300,22.6,39.3,\"mOaU@D\\\\@?CVCCGID_@?@BKA kMyXM?OEMFQA?IIBDLB@VAVBMJT@HIBBEJIHFBSN?HPCEHJ@GPL?NGJ]PYGIAEEC?Cg@I\"],[\"Greenland\",304,-41.5,74.8,\"f\\\\sr@cAKeA?YEeF?}BNd@FvD@IBeAA}@De@EOFTHsCM{@BKFzAR~@@m@@f@X?VWJ\\\\@`@De@HCPT?YPj@@WFFDv@BYL?Ff@GHD[BYJEN`@Bf@QEJVJqA@jB^pAFRFZRj@LbAHPL?LHJ^NGNRb@Z@\\\\Qf@?PILS`@WHMBQZQGMLGSW]EGGCOr@JVE@MGI{@Br@QR@NEUQr@i@XG?Gt@KnC?x@Q{AGrACl@GAGuCSGGt@EQGcAM]AFIkBG}@?UDs@IuBLn@IAG\"],[\"Guatemala\",320,-90.4,15.7,\"rx@aHAK@CIOW?AETQI??Ie@?@d@SBRJAFDDLLTCRK\"],[\"Guinea\",324,-11.1,10.4,\"pG{FWDSA?DIAOFUIMPBJE??DEDB@@DIVHD?JF?BFD?BCAGHKLB@SHMN?HBNPZY@IFEGISE?O\"],[\"Guinea-Bissau\",624,-15.1,12,\"lIwFWCc@??NRDFHJIF?JQ\"],[\"Guyana\",328,-59,4.8,\"hb@e@N?VJH?JIFSCOEGDGDCAGBCH?LOEC@IQEDG?GOKMFKL?HG?SNBPJD@LGNE?CHKP\"],[\"Haiti\",332,-72.7,18.9,\"xk@iK?PBBCD?Db@CFBJEAGa@DGEHG?GJACE]B\"],[\"Honduras\",340,-86.6,14.8,\"|r@kHXFHCPRDCBBH?AHJDBEDAAGBAJ@PKEE@GYOS@QCS?KBYN\"],[\"Hungary\",348,19.4,47.2,\"yLg]IBADJBTZZ@TFPCZQEKBCK?AGQDm@IKEUDCA\"],[\"Iceland\",352,-18.8,65.1,\"`Hqh@BLULXLjAPpAISGj@Ic@C?Eh@CKM_@A]L]KYD_@Ia@?\"],[\"India\",356,79.6,22.9,\"y{@uPAFDB?JLCXLAJJN?HHNNC?TBDAFHDJ_@B?BJHIEKG?IQp@E@MF?NGFJMJJDBFKDBJI\\\\@Fd@B?NHHZLVV`@X?Fd@LDNEh@FP?`@J@FNEDPDFJFDPQPo@N[He@P[L_ABk@ZJLAXWGGDETQKMk@?BOHI@OLGUUW@s@}@?MQKNIN]IG_@BUCQOUV@NGH?HLACTo@XJHFP_AZ[BKHw@D?]KCARQFMC_@@AMFEOAi@[ODMIILFFU@\"],[\"Indonesia\",360,114,-0.3,\"cwAr@?`CPORCBDX?GOMEDUHOd@QN?\\\\SDHF@BONIUEM?@E\\\\?FKPCFI[CIE_@FGf@UJOUUKQ?_@LUB cmApDAHLLND@CIQSG {rAhC@OGMCD?HHL uhAqAJPMP@HUPV@DL?PPL@RF\\\\@ETFFKLAHEVFFI\\\\A@YHEHO@QAQKMALMHMCK@KIIAQDOCI[GGGUg@B {oAv@UDENNIh@?CKY? inAjALCBGSACFDD }nAk@AJK@AF@NHA@LGHD@FKDYCOEG ykAQW?SMABNRLBRCp@BBLQPKGc@G?HHCFJPFSXBDQV?JJDFEIORFBEAGLKASLDAr@JBFEEQBSF?DMGMKm@CGQMODW@ ejAlEXMQCQJ@BF@ yjAhD_@G@J\\\\BZA?GOCKD }hAfDKACFn@DGIIAEEGD ybA~BAFe@@EGc@FGL]BWJTFTIf@?h@KT@b@GBIP?MSW@WF g`ATIVM@ILBV@^T?NQXOV[Xi@NQL_@PMHOb@a@@Ik@D}@|@U?OLKNMHFNSF\"],[\"Iran\",364,54.3,32.5,\"k]uQJK?ID?AMHOVILSEMIG@KLEVg@CEDUMEMNKBg@QEDFHKHE?ELQBMH]B]EACQCMKc@?UHO@WPM?APLd@IBHHG\\\\O@ALPPQTQFAPGBAFZJFTpAKFYHAb@JVEROREZk@H@LEDF\"],[\"Iraq\",368,43.8,33,\"oWcSFWk@SGW@O]SU@GDICKVMDAJHFDLMRWHIN@LE??HKJXCLRd@Av@g@\\\\MVE\"],[\"Ireland\",372,-8,53.2,\"zBu`@CLNPb@JZAOUHSi@WCHBH[B\"],[\"Israel\",376,35,31.5,\"iUmSBDDABJC@BB@BIA?FH^Ja@EEI_@MC@J\"],[\"Italy\",380,12.2,43.5,\"oEi\\\\M@AASCCD[D@HCFNAND?NEHQHINUNO?EDDBq@TABBFJINAFJMF@HH@HNF@CQCCLSb@WLA`@ULIDSXI\\\\LCGJCDMGEDG?EIBI?IGCBI?CGM@IC?G gHyVMADNADBHt@SCIk@? mDqXIEKL@XH?FDFE?WBKI@\"],[\"Jamaica\",388,-77.3,18.1,\"no@qJM@IBCDRBJCHEAEK?\"],[\"Japan\",392,136.9,36,\"uwAoWPR?TFNCHHLXFb@@ZTLE?Ob@BTHV?SNL^JHHGCQJEFMSEIKSKMKg@EUBSc@MHg@[MWBWGKUCIZ?N kyAoZMGCTZDPR^MHTT?BSIMUAKk@UT]H wqA}SIKKBGIMDCDJJFEHBDJJE?I\"],[\"Jordan\",400,36.8,31.2,\"eUgSCEUFg@UGVj@LSRHHN@JLTC@AI_@AY\"],[\"Kazakhstan\",398,67.3,48.2,\"su@w]NLN?@RHHf@ELb@p@JQ`@LDAHVIdA?ZGJB@J`@EJ@BFd@PHLD?DGXABOH?ASVMx@BPQr@Wr@JAhAH?LMLEVBFDCKBGVEFQJE?ES@?MQCO@CQBMR@NCh@JHCAIJKN?NMIOBCOUSJAMi@U]?cATUG_@AWJEE[?EI^MQGBESELMGEkAGICq@GQGc@BETSEYF@HS?q@SFDYNk@p@KIYJ]EIBIJMBGFYAKJ\"],[\"Kenya\",404,37.8,0.6,\"oW|AZS@KjAi@?UUc@Ha@HMYWIB?HGFM?WN]BEGQIGFM?PT?hAKNLFDFD@L`@FF\"],[\"Kosovo\",0,20.9,42.6,\"{KeY@EFGIKCASJBHRD\"],[\"Kuwait\",414,47.6,29.3,\"_]wQGZL@BIPAMSM@\"],[\"Kyrgyzstan\",417,74.6,41.5,\"kk@mYCGKAa@DAKKC[FeA?WHBD^HDFX@FLRC`@LCBDDd@@XER@AKSBGEO@YOVILDNGQMDA\"],[\"Laos\",418,103.7,18.4,\"cbA{GPGHLNGEGAQNO@SLONABDJ@DCRJ@QEUL?@KFEUSABI?@UICS`@W?GPJDDDWJ]h@MLELBR\"],[\"Latvia\",428,24.8,56.8,\"aP}b@IDGR`@JRIHA@Ct@@TD?OIKQGONO?COQCYHO?\"],[\"Lebanon\",422,35.9,33.9,\"kUySLBGOIMG?CFHFDH\"],[\"Lesotho\",426,28.2,-29.6,\"cQbQEDHNH@BDD@NMUSGEIF\"],[\"Liberia\",430,-9.4,6.4,\"fDyC?NBFSN@XXGn@g@EKQSGAIJ@FCBE?CGE?\"],[\"Libya\",434,18,27,\"sNwL?f@V??F|CkAb@PHI^IFKNIHBFI?IJMGG?_@CQFYIC@Q]SAMWDW@YFIPm@JUHSMDOEGOIKA[BEFa@DEDFHCFDLEN?nC\"],[\"Lithuania\",440,23.9,55.3,\"qOwa@AFNFDHTFP?DEHAAK\\\\EBOUEu@AABI@SH\"],[\"Luxembourg\",442,6,49.8,\"wBi^CB?FH?AKC?\"],[\"Macedonia\",807,21.7,41.6,\"_MmYIDALf@HFE@GEK_@C\"],[\"Madagascar\",450,46.7,-19.4,\"}]xFKTAVEF@HBDDKBDCN@FDB@Pl@hC`@LZKLk@?MAOIAKc@HU?QGWe@G[WEI@GG@IMAKEIEH\"],[\"Malawi\",454,34.2,-13.2,\"oSvDQBKNEXDNEXE?GDGLAXHBDLJKA_@FCD@TOEQEGBOGUDOFI\"],[\"Malaysia\",458,114.7,3.5,\"q}@aCCAOFAHKAEGQLGJAh@GDEVL@h@_@@IJK@QFIAMBI uhAqAf@CFTFFHZNBPEH@JHJALBLI@MMDOCCO_@GW[IHCEG?CUMMIOG?IHAF]J@FL?CHLF\"],[\"Mali\",466,-3.5,17.3,\"dFwF@MDEDWGCAKSDSCCCoA?CMBARmD_@?eCvAEHSFAJUA?l@JL@Jj@DFDX@BCJ@PFTTFBHCDD@LNN?DDFAJJDBGH@@DVADE?ED?CKLQTHNGH@?E\"],[\"Mauritania\",478,-10.3,20.2,\"tIcLEEmA?B]CISA@q@aA??]kAn@^?SlDC@BLnA?BBRBRE@JFBV[VKRBHAFF@KEICQD]AINQ\"],[\"Mexico\",484,-102.6,23.9,\"dhAiSo@C@BkAVw@??Ia@?]VIRKDODKOO?MF]d@IT]JGAJ^Bd@Mb@Yb@UBGH{@OKGIa@c@I[?EB?HJHBLC@FVDCF?DJBA@Bd@??HH?UP@DV?HNAB@J`@[NEd@HrA]TM^GFGTKHKBIEA@ECE?EL[n@o@PG@CAKTOBMJ?RURc@AG`@K@FCV[ZCFA?CJWTQd@?HI?OP?BJFDKd@Y@YVO@BPIJKI?GG?ILMJE^y@\"],[\"Moldova\",498,28.4,47.2,\"sOc]QEWFGD?DEBKPT??@LN@G?QVYDA\"],[\"Mongolia\",496,102.9,46.8,\"{u@y]SCcAYQDS?MHq@DQMFKUSUFi@DCN[Fk@ES@_@Rk@Bk@G]MUFWAVb@CFKCSBOGOFQL@Dl@?LDLL\\\\FRH^EJJINZNVF\\\\?^FTHHEV?\\\\KRCXB~@CHKHQJCVKr@EDGGWLOZGPIBM\"],[\"Montenegro\",499,19.3,42.8,\"qKsYD@@CFHADRKEMIEUJBD\"],[\"Morocco\",504,-8.4,29.9,\"j@_UGLG^EBBFXBHFH@@LVFDHd@F^N@`@J?DB`@?DND@HVZTDZJNj@@?IMO?EEMQOEUGMKEMSIGQAi@c@BYI[KKc@QSa@M?KHSA[B\"],[\"Mozambique\",508,35.5,-17.2,\"sTdFMAWDSAEEK?UGOKCHAbACHLZJJh@Pr@j@@LINCNC?BZCB@FHFf@PFDAFC@@HL?DWAULi@SWIOAgA^OP?@U{@OKHEAGB@^KJEMIC@YFMFED?DYEO\"],[\"Myanmar\",104,96.5,21,\"q}@wKTJL@HTFB]j@FPDBQXARGPRf@@OEMFKAUFIHq@FOb@VVEEYBQNWCEJCLO@OGB?MIE@GCE?UOBIO?IKO@KYMMB?KEC@GK?EJIB?^RN@VUCCRKBDNWHMCAFTR\"],[\"N. Cyprus\",0,33.6,35.3,\"mS}TCEO?QELHA@X?\"],[\"Namibia\",516,17.2,-22.1,\"mKnN?hAPHH?TCBGFEHHLOFMN_A@c@PWNe@NQ@OUGM@KFsAAMHo@@s@KUD\\\\LFGl@F?dAR??z@\"],[\"Nepal\",524,84,28.2,\"av@mP?\\\\P?d@EJIZC~@[GQSMo@TEH_@Jm@D\"],[\"Netherlands\",528,5.5,52.3,\"iCm`@CFDPBDJ?CTVMRBL?IEQ][GO?\"],[\"New Caledonia\",540,165.5,-21.3,\"sfBdLYTFBHEZSNSI?YR\"],[\"New Zealand\",554,170.5,-44,\"qmB`XPVNFHGIODKTGAGMEA[FQVYHMG?KHMDEPMRAMGDCN[FKGI@H\\\\NADDAH@B aiBfZ[UIMGEAIMIGNOGCF?H\\\\^GHN?NDP`@XLP?JETABEIOYS[I\"],[\"Nicaragua\",558,-85,12.8,\"hs@yEBBNGB@LC@@f@c@ACCBGAEE@II?CCEBQSIBYGBDAFDL?VFRCH\"],[\"Niger\",562,9.3,17.3,\"iHiMC^OPJp@@XVPHXIF?JK@FNB?LUB?NJXGP@JFH@VIHBH?FGRGRBDBFP@PNID?DD?MVE?GJM?QKAGEk@EAKKM?m@[Gy@g@cAe@_@HIHOG\"],[\"Nigeria\",566,8,9.5,\"u@}B?k@CKOS@CCGBKAYMUSCSFGFI?ICWHIAKGQAYFOKC?MTKFBHNNNh@JFHXJFJIF@JHD@L^b@JNAPYLM^?\"],[\"North Korea\",408,127.2,40.1,\"spAoYCBFALL?LTLTF@JSJBBT@FHH@HCFB@CHCIK@AEKTIOMUIMMGDQ?@I]GGKKJ\"],[\"Norway\",578,14.2,64.5,\"mHyp@GE]AyATp@FHNPBHNV?h@KQE\\\\Ef@QNOw@EID[? mRoj@p@HGMXG\\\\FHLRHREX@VKVD@Ld@CDJP?~@~@EFDFRALRAXKHDVXVJKf@VZBXIFUDm@QKu@Qe@SuAaAwAg@k@I_@@]Oe@?c@C}@LXDUJ cPaq@\\\\Jx@@x@CBCZATG}@E[BSEyAJ mNuo@j@Hb@EMCJGi@CGF[B\"],[\"Oman\",512,56.1,20.6,\"oa@eMEWICBCAKI?GLKDYDMPGB?BXd@HABD@XH?JDDLJ?FD?FHDJATDTm@{@SMg@HM eb@aOBGEEA@BJ\"],[\"Pakistan\",586,69.4,30,\"so@eUPNTB^CHFO\\\\OHPJ?Lr@|@VATTMFANIHCNj@?JLNCDOLM`BDGU[K@GFC@QPGPU_@HSCKBCEM@YGAOIKO?AEU?GE?KGKKEFKS?EE@GIIDQIGcAKMFEN_@F\"],[\"Palestine\",275,35.3,31.9,\"cUuRH@ACCCBACKE@@P\"],[\"Panama\",591,-80.1,8.5,\"jo@mD@BEJBDFA@HDEBKCCPMH@BDJD@BIJPDBK@BDABGTC?BBAEMBC?GGAEF?BI?EB]IEEK?QFMH\"],[\"Papua New Guinea\",598,144.3,-6.6,\"cwAr@gAXWRCJ_@JEHP@CLQJKTK??FMBDBUF@DL@BEd@EXWJOXIZJANND^C?aC k~AhAGDAHDDFS`@UCCYLGH q}ArBTHH?ZKAE[@EIAHK?OM@KKACB?JDJH@BB w_BhBWV@DD@FGHKBOCAAD\"],[\"Paraguay\",600,-58.4,-23.2,\"jc@rKEH?ZSBGCMDG^MCGBHr@PNN@h@GS]BGj@WNAd@a@Ga@IQu@CQH?D\"],[\"Peru\",604,-74.4,-9.2,\"tj@tAP?h@RDNAHJD?FDBGPKJBHK?GJQ?OK?\\\\G@KAO^BD?^FHADBFGNP^HBRI@Gd@Sr@c@FOAEdA}B\\\\UGGHSEMOKAFDDADO@GHKIAKMQWGUSEMBMEA[VKRO@ICG@K?OFLRE@IH\"],[\"Philippines\",608,121.5,15.8,\"ojA}FHOQ@EDBPHI skAgECEAKK?BJOQ@PRXJMCE _nAgDAVDPFSFHENDFVKDMEIJGDFHALJBEGOWMEHOECIM?@OOHCP aiAyDXPc@g@ISCNVX mkAkJ@FELBNJFBNCNS?YH@JEB@HNIFKBFLKPBHEAGECDE@FLS?SGFA_@EQK?KDEEAD gkAcF@IKDK??FTN?S mmAsFEVNEEPHD?MDABKK@?GJOQ@ED\"],[\"Poland\",616,19.3,52.1,\"uMu`@EVJBOb@@DH@PNEHVIV@JBHGFBHKN??GLABDHC?ELAHEFKAGDIDGECDKeAUSBADoA@I@ED\"],[\"Portugal\",620,-8.1,39.6,\"rDeYMGEHYAEHHDBZF@GJDJGDHL?DFDHCH@A[HABEAMGGCSBU\"],[\"Puerto Rico\",630,-66.5,18.2,\"lh@qJMDBBZ@?IAAO?\"],[\"Qatar\",634,51.2,25.3,\"w^oN@MEIEAED?JBJD?DC\"],[\"Romania\",642,24.9,45.9,\"sPm[[B?DHBDABVZI^Ht@AFKCCBAFBHE@GJC@EJGOCU[SGY@IDa@IE@WX?PAF\"],[\"Russia\",643,99.9,61.7,\"sy@sq@i@Eg@Jm@PDPh@@v@C`@GNMXCs@M g_Aqp@s@JDFpBFe@Y]A wuAqn@u@?gAHNNhA?^Bf@MIKYC s{Aan@q@BTF^Ad@GEEc@B mvA{l@QGYA[FADhAC _[kq@eACCD]G]BlALTCKGl@? eM}`@z@ACI[E]D@J m`@cm@o@ODIoBWaAAa@Gg@AMFLDbCP|@R|@d@CPe@NjAADGb@E@ISC@Kg@OPC ixAq`@GR@P]v@\\\\EJZQT?LNKJLBO?i@CMA[JQA[QIFGICCL wpAkYBMI?CWDQQEWBMSEUQY^DPF\\\\?FQVM`@GFQZi@NG\\\\Er@BNHID?HHFPR?HZJVGV@TG\\\\Lj@Fj@C^SRAj@DZGBOh@ETGTRGJPLp@ELIR?PEbAXZDJKX@FGLCHKHC\\\\DXKJHj@q@XOGEp@RR?AIXGRDDUb@CPFp@FHBjAFFDMLRDCDPF_@LDHZ?DDVK^@TFbAU\\\\?h@T@LRKNTCBHNOLO?KJ@HIBHJPBRRQP@LUVNJF?LKPEDGRCJ@\\\\Kl@G@BXQVGNIMCOOHG[G?EPBAGIEQACEBKGI?Eh@EJINBVGFKNA@EECJIf@BHSS?GEDCLCACROCE@IRCH@BCREFSHEGEDQMK@CUIRI{@e@GI\\\\MGKPMMQTUQO^MCMeAOa@Ju@BkAVOF?LTH^DxAOLB_@LA\\\\i@HAGJIMEo@JOEJMm@SQ@SDIMNKIKLKw@FIHV@?JMD_@CCKoB[O@RJW@MEe@A]GUJWKTKKGy@FcBZMIRI@EVAGGJO?Ec@QMQMCu@BCJRNMDELBZUJHLd@ZU@GEUEEIQIJIGMTABIOSXOc@MBMIAIJFRU@HMa@Gi@?c@JPQ@Sa@E{A?NKWKU?g@Is@CECu@AOBk@Ie@?CISGo@Ga@DZDm@@CHSCy@?k@HOFBHxATu@FQCILIE_@C_ABCHsA@AMiA@_@JIJJFWN_@FSS_@Fa@Ee@FMEa@@NQ[GsDJQJs@NoACg@@OF@NWD[CgA@e@Ac@PWENMIG}@Di@Ay@HmAVs@R@JMDDOu@Bg@PRH^@@PFBP?h@KBIRAV@HECGVBIHJFXHXAQHKNIDAFDDb@ChANz@ZFFZMr@NHGPHZCDLVRAFUB@ZR@FNGF`@JFT\\\\BDTZPFMRiAI[QK?G_@EeAk@c@OO]V@JPr@TNYr@Fp@^OLhAD?M\\\\CXHz@A`ADlCtA_@@IJSBKIU@[TAPNR@VF^\\\\ZFL`Ax@ZJJ?JIZL@F noBuk@SA[DTFn@@BEYG {Sw[ECSBOJSABFTBXHJCCGREUKBA\"],[\"Rwanda\",646,29.9,-2,\"_RTGJ?JP?DJJAEWEEC@KE\"],[\"S. Sudan\",728,30.2,7.3,\"gReANMDGXDFANWLEDKTS?EVOKEK[MAMNE@EAM?CBS??CICAEGCOJKASY@KBEK??CI?@NALIFE^BDH?DJK@IHAFIBKTd@`@\\\\BLCFD\"],[\"Saudi Arabia\",682,44.5,24.1,\"{TkQUBKMOAIIRSk@MWD]Lw@f@w@BCHMAGP[R?TKNEBE?K^{@HCCILLf@z@Rx@FPFLTHBDG\\\\AZ@FEDHAFFFHWHEJQDOLMFALS?YJURKJYb@q@H?GY\"],[\"Senegal\",686,-14.5,14.4,\"lIoGFOHEGCU_@I@SCWJWZEVEDALR@LEl@?VB@OQ@OGOBICBELBFEHDT?\"],[\"Serbia\",688,20.8,44.2,\"wJu[OEK@KFADKBAFIDGCC@BBCBDBAFIFFDBDA@PDCIRKHHTMEAAGHGEIF?GEJM\"],[\"Sierra Leone\",694,-11.8,8.5,\"fGqDOQICO?ILAREAPRDJRIHIDU\"],[\"Slovakia\",703,19.5,48.7,\"cMu]LNTEJDl@HPE@GCEOAMKMAIFKCWASF\"],[\"Slovenia\",705,14.9,46.1,\"sGa\\\\O@_@IGFND@FF@?BLAB@L?CABGAI\"],[\"Solomon Is.\",90,159.1,-7.9,\"idBpEEDL?FKOD adB~DBBLQBKE?OX qcBdERADCAGMBIH wbB~CEH`@UCAWL eaBjCGDJAFG?CKF\"],[\"Somalia\",706,45.7,4.8,\"_Y`@JO?iAU[MAQMY?mAyA?e@k@K@ZFZf@lAf@n@dAr@\\\\f@\"],[\"Somaliland\",0,46.2,9.8,\"q]cF?d@T\\\\P?~@WT[IQSTq@Gm@K\"],[\"South Africa\",710,25.1,-29,\"eIzPIIGDCFUBI?QI?iAE@KR@JCDMASMCIICQFO?MCCOKAKSQO[Mc@BMh@@ZNADL?DKJKACIM?Jh@PJV`@d@`@LF^H@BJAHBTCR@d@HJFH?FEDAHG?@@OFMEC?Md@}@ cQbQHGFDTROLEACEIAIODE\"],[\"South Korea\",410,127.8,36.4,\"{mAqVIAGIUACCOVEJ?VFHPBNFP@@ICOHSOCLO\"],[\"Spain\",724,-3.6,40.3,\"tCeV?EIMFEEKFKGAC[IEDIX@DILF?MFG[MiADo@?GFc@FGCUHWC?JPLXBT`@GJJFBLLBLLl@?RNHAJQRC\"],[\"Sri Lanka\",144,80.7,7.7,\"cr@uCBRXHFOB[G_@MHSb@\"],[\"Sudan\",729,29.9,16,\"kNcDTOAUJK@MFEBQF?GO@GEEBCM[Q@@wAW??g@mF?ERBBARGVQJHJL@DDJj@AF@NFRJHJVFDDR?PD_@HG@MAOH??BJ?CDAJRXJ@NKFB@DHB?BR?BCL?D@DALOL@JZJDMF\"],[\"Suriname\",740,-55.9,4.1,\"`a@m@JCP??LHAJQBID?FOAMKECQWBACO?SBHPALGJHX\"],[\"Sweden\",752,16.6,62.8,\"{Eyc@YWEWJI@YMSS@EGDG_A_AQ?EKe@BAMK?y@VA\\\\EF`@DRLCJfA`@LZMLSHPTTDF`@HPVAJNV?DQ^s@\"],[\"Switzerland\",756,8.1,46.8,\"_Eu\\\\@FQB?FHBLABFH?BCHFH?NKH@?GOK@C_@ACCUD\"],[\"Syria\",760,38.5,35,\"iUmSGUIGBGF?@OOQ?IGBUEYBWG_@AHJHBANFVrAh@TG\"],[\"Taiwan\",158,121,23.7,\"ckAgNTn@HO@OKQOOIDBJ\"],[\"Tajikistan\",762,71,38.6,\"ki@eVKUBMNCEIO@OW[EBHADG?FDRC@JSAYDe@ACPGAKBAR`@A\\\\NFGAODAAEHCFFDHJ?DFDANDDA\"],[\"Tanzania\",834,34.8,-6.3,\"eTRkAh@AJ[RHVAJKDB`@O^EBNJz@NVEL@DYJOPCh@QHWJKDg@I?SUDSEA?KFKeAA\"],[\"Thailand\",764,101,15,\"w`A}GPAXBLNEVPGP?COP?@TNl@?NM?Kb@IHKBIHDFJ@@INGB@FEBIRUBJBII_@Sg@FQ@SPYECGQ\\\\k@GCIUMAUKGDAJM?DTAPSKEBKACEO@MNARON@PDF\"],[\"Timor-Leste\",626,126,-8.8,\"cmApDACOEUAE@j@R@I\"],[\"Togo\",768,1,8.4,\"Q{E@HKLAHCB@l@ELNBHS@ICQBG@]FI?EQ?\"],[\"Trinidad and Tobago\",780,-61.3,10.4,\"pe@wEOA?NT?EE?G\"],[\"Tunisia\",788,9.5,34.2,\"}D}QFc@\\\\W@OKKEOBQCIUIMB?HOGABHH?HEB@PJHAHK?CHGB@L\\\\RAPHB\"],[\"Turkey\",792,35.4,39,\"_[gVHBFETAh@DVFXCTDFC?HJHDIGGJ@PCLJ\\\\@NITABFLBRKT?JSLIIOJIUQ]AGMe@@WKWEa@?}@Ti@AWISASHCF@FUJLDETBDKN iOcYSEQ@AFQDBBV@VNDKCAEMHC\"],[\"Turkmenistan\",795,59.3,39.1,\"y_@cYGEWCMDML_@?BIa@S[JANGBSAGBIRc@Vm@P?JNE@FPBBNXFBHN@RE@QL?VQNATIb@?LJPBBMCUNEEML?CQSDOGLKDINB@NDM\"],[\"Uganda\",800,32.4,1.3,\"eTR|@?RFBAEe@KSMKFA?WGEMB]CKIILI`@Tb@?T\"],[\"Ukraine\",804,31.2,49.1,\"{Rq_@g@CKHDBADO@GJWFOCKHi@D?DFHCJBDP@HD@FN@JDR@ND?JXIDB`@E?GR@TXFCHBHCMO?AU?JQDC?EFEVGr@NHEXAFB@EHCIMC@DIQOIAAEHQc@EiBJ?IECQA\"],[\"United Arab Emirates\",784,54.2,23.9,\"g_@cNCA?DOC[@i@g@CFANH?@JCBHB?FHRz@IFW\"],[\"United Kingdom\",826,-2.7,53.9,\"zBu`@ZCCIBIQASJHL |@k`@CKLKVCDEGGDEJH?SJIGSOOg@?TRi@ABNPPS@SXM@Q^WB@JJDGHPHX?`@DHCJHPALDJC]SQC^CDGUEJICM]@\"],[\"United States of America\",840,-99.1,39.5,\"vkAs]gP??GG?CJE@u@JOC]DQEeAZEHIACFG@@BSHI`@R\\\\GFkAWDKIAe@?EG_@ScA?AEMCMWOMEBMCGD?ZOP|@TLN?JEHG?@EEB@Dx@FND]CEBZDL??CDDE@BLLN@EHEGL?FNVCMHGBO@FCJLAMD?PE@EXLNRDLJH?HF@DTLRTBLCLO\\\\?HGT?TDJD@HA@GFEVg@CMRYFCRHXOf@?VBGPB@FAFBL?LKP@LC\\\\Dn@`@DF?LENF@\\\\KHU\\\\e@LGN?JNNEJEHS\\\\W`@??Hv@?jAWACn@BBIJKHA@Eh@K@KRSN[?CVWBOHICO?QDOGQCc@B[JYAC]FIPEEH_@ b`BqKKJPJBCBKCKIB n`B_LFBDEM@ vaBqLEFF?BEEA rbB{L@DDCGA `gBwd@O@AFJ@XGSC v~Agc@K@IDd@PHEBI]I bwAqj@?zDS?SD]Tg@QKH]Pa@b@a@L?JHH\\\\MBQXOJQp@AVEh@St@KZ@|@QTBCLhANBIISUEDEXJLJZLMHPNh@LDF\\\\HDHVFL?v@P`@D@C}@YWAIG[KSKAMKKVDDCJFJIDDFIRFJ?@KCEJGXB\\\\M@KLGGKOIEI]?OGO?MEBGHCMG`@BDDLE\\\\BZEFGXKgAQO?@Hi@?NKVG`@QZGKIc@?WIEISGy@KQ@]K]DOFGCa@@@B_@BSA_BHYCs@H hjB{f@{@HNDVGP@BAAG\"],[\"Uruguay\",858,-56,-32.8,\"~b@zQKASNG?c@TKLFHCJFJTJNCH@PIL@JKOiA\"],[\"Uzbekistan\",860,63.2,41.7,\"_b@yX@iAs@Ks@VQPy@CWL@RI?CNY@EFE?IMe@QE@PLOFMEWHXNVA@ECIZDNVNADHOBCLJTXE?Kl@Qb@WHSFCR@FC@OZK`@RCHT?\"],[\"Vanuatu\",548,166.9,-15.2,\"ogB|HKJD@DG?E ggBxHBUIDCNH?\"],[\"Venezuela\",862,-66.2,7.2,\"|d@gBADFFb@HDB\\\\EB?GDAXQ@?BLD@FVF@DN@JKDQJKIIHWAMGQFC`@BLQd@?FG?SFMHAGYQUMC@DH@EF?JHHGNG?EOFE?OWG@GGGELM@MH?Dg@?IFO@KE?Co@ANDEHO?OHANI?GBNJ?FEFPDAHDBMN\"],[\"Vietnam\",704,106.3,16.7,\"e`AqEQGSAFKa@MAWBMCSDMLM\\\\i@VKEEKEFQV?Ra@ICc@AOKIFQB@JGFSBXPNPBLa@n@QJKNGd@@`@f@Xf@b@FKEMNK\"],[\"W. Sahara\",732,-12.1,24.3,\"lDiP?b@`A?Ap@R@BHC\\\\lA?DDAGk@AKOE[[UIWEAEOa@?ECK??KA?\"],[\"Yemen\",887,47.5,15.9,\"o_@{JUl@LDBNr@PPLN?HFb@DJJ\\\\@DKAKL[CA?SGG@GEIGD[A]@EFICMUQGy@G\"],[\"Zambia\",894,27.7,-13.4,\"eRdDi@PGHENFTCNDFDPIDz@NALN@JF@FD@\\\\Z`@CJELANBX[?_Ai@?@g@G@?DU?GHO@KECHOBMPO@@UDBPKEk@DGEMEAa@A\"],[\"Zimbabwe\",716,29.8,-18.9,\"oR|Lb@CJINADSFATUP_@a@B][EAAGKGOA?FQ?_@N@fAHNRV\"]];\nvar MM_WORLD_ALIASES = {\"us\":\"United States of America\",\"usa\":\"United States of America\",\"united states\":\"United States of America\",\"america\":\"United States of America\",\"uk\":\"United Kingdom\",\"great britain\":\"United Kingdom\",\"britain\":\"United Kingdom\",\"england\":\"United Kingdom\",\"uae\":\"United Arab Emirates\",\"south korea\":\"South Korea\",\"korea\":\"South Korea\",\"north korea\":\"North Korea\",\"drc\":\"Dem. Rep. Congo\",\"dr congo\":\"Dem. Rep. Congo\",\"democratic republic of the congo\":\"Dem. Rep. Congo\",\"republic of the congo\":\"Congo\",\"czech republic\":\"Czechia\",\"dominican republic\":\"Dominican Rep.\",\"ivory coast\":\"Côte d'Ivoire\",\"cote d'ivoire\":\"Côte d'Ivoire\",\"eswatini\":\"eSwatini\",\"swaziland\":\"eSwatini\",\"east timor\":\"Timor-Leste\",\"north macedonia\":\"Macedonia\",\"bosnia\":\"Bosnia and Herz.\",\"bosnia and herzegovina\":\"Bosnia and Herz.\",\"western sahara\":\"W. Sahara\",\"central african republic\":\"Central African Rep.\",\"south sudan\":\"S. Sudan\",\"equatorial guinea\":\"Eq. Guinea\",\"solomon islands\":\"Solomon Is.\",\"falkland islands\":\"Falkland Is.\",\"holland\":\"Netherlands\",\"burma\":\"Myanmar\",\"persia\":\"Iran\",\"viet nam\":\"Vietnam\",\"lao pdr\":\"Laos\"};\nvar MM_WORLD_CACHE = null;\nfunction mmWorldDecode(s) {\n var pts = [], i = 0, x = 0, y = 0;\n while (i < s.length) {\n var v = 0, sh = 0, b;\n do { b = s.charCodeAt(i++) - 63; v |= (b & 31) << sh; sh += 5; } while (b >= 32);\n x += v & 1 ? ~(v >> 1) : v >> 1;\n v = 0; sh = 0;\n do { b = s.charCodeAt(i++) - 63; v |= (b & 31) << sh; sh += 5; } while (b >= 32);\n y += v & 1 ? ~(v >> 1) : v >> 1;\n pts.push([x / 10, y / 10]);\n }\n return pts;\n}\n/** Every country: {name, id (ISO 3166 numeric), c: [lon, lat] label point, rings: [[[lon, lat], …], …]}. */\nfunction worldShapes() {\n if (!MM_WORLD_CACHE) MM_WORLD_CACHE = MM_WORLD.map(function (r) { return { name: r[0], id: r[1], c: [r[2], r[3]], rings: r[4].split(\" \").map(mmWorldDecode) }; });\n return MM_WORLD_CACHE;\n}\n/** A country by name (\"India\"), a common short name (\"UK\", \"USA\") or ISO numeric id (356). */\nfunction worldFind(q) {\n var all = worldShapes(), s = String(q == null ? \"\" : q).trim().toLowerCase();\n if (!s) return null;\n var target = MM_WORLD_ALIASES[s] ? MM_WORLD_ALIASES[s].toLowerCase() : s;\n for (var i = 0; i < all.length; i++) if (all[i].name.toLowerCase() === target || String(all[i].id) === s) return all[i];\n for (var j = 0; j < all.length; j++) if (all[j].name.toLowerCase().indexOf(target) === 0) return all[j];\n return null;\n}\n/** Equal Earth: [lon, lat] degrees → unit coordinates (x ±2.71, y ±1.32, y up). */\nfunction mmEqualEarth(lon, lat) {\n var A1 = 1.340264, A2 = -0.081106, A3 = 0.000893, A4 = 0.003796, l = lon * Math.PI / 180, p = lat * Math.PI / 180;\n var th = Math.asin(Math.sqrt(3) / 2 * Math.sin(p)), t2 = th * th, t6 = t2 * t2 * t2;\n return [2 * Math.sqrt(3) * l * Math.cos(th) / (3 * (9 * A4 * t6 * t2 + 7 * A3 * t6 + 3 * A2 * t2 + A1)), A4 * t6 * t2 * th + A3 * t6 * th + A2 * t2 * th + A1 * th];\n}\n/**\n * A flat world map fitted into a box: o = {x, y, w, h, center: [lon, lat], zoom (1 = whole world),\n * antarctica (false)}. Returns project(lon, lat) → [x, y], path(country or name) → SVG path data,\n * outline (the map's edge as path data, to clip or fill the sea), clipId(), and the countries.\n */\nfunction worldMap($, o) {\n o = o || {};\n var bx = o.x || 0, by = o.y || 0, bw = o.w || $.W, bh = o.h || $.H;\n var center = o.center || [0, 0], zoom = o.zoom || 1;\n var fit = Math.min(bw / (2 * 2.7066), bh / (2 * 1.3173)) * zoom;\n var cy0 = mmEqualEarth(0, center[1])[1];\n function rotLon(lon) { var l = lon - center[0]; while (l > 180) l -= 360; while (l < -180) l += 360; return l; }\n function P(lon, lat) { var u = mmEqualEarth(lon, lat); return [bx + bw / 2 + u[0] * fit, by + bh / 2 - (u[1] - cy0) * fit]; }\n var api = { countries: worldShapes().filter(function (c) { return o.antarctica || c.name !== \"Antarctica\"; }) };\n api.project = function (lon, lat) { return P(rotLon(lon), lat); };\n api.ring = function (ring) {\n var out = [], prev = null;\n for (var i = 0; i < ring.length; i++) {\n var l = rotLon(ring[i][0]);\n if (prev !== null) { while (l - prev > 180) l -= 360; while (prev - l > 180) l += 360; }\n prev = l;\n out.push(P(l, ring[i][1]));\n }\n return out;\n };\n api.path = function (c) {\n if (typeof c === \"string\" || typeof c === \"number\") c = worldFind(c);\n if (!c) return \"\";\n return c.rings.map(function (r) { return \"M\" + api.ring(r).map(function (p) { return $.fx(p[0]) + \" \" + $.fx(p[1]); }).join(\"L\") + \"Z\"; }).join(\"\");\n };\n var edge = [];\n for (var lat = -90; lat <= 90; lat += 3) edge.push(P(180, lat));\n for (var lat2 = 90; lat2 >= -90; lat2 -= 3) edge.push(P(-180, lat2));\n api.outline = \"M\" + edge.map(function (p) { return $.fx(p[0]) + \" \" + $.fx(p[1]); }).join(\"L\") + \"Z\";\n var clip = null;\n api.clipId = function () { if (!clip) { clip = $.uid(\"mmw\"); $.def('<clipPath id=\"' + clip + '\"><path d=\"' + api.outline + '\"/></clipPath>'); } return clip; };\n return api;\n}\n/** [lon, lat] on a sphere of radius r turned by spin (deg) and tilted by tilt: a 3D point for scene3d. */\nfunction worldOnSphere(lon, lat, r, spin, tilt) {\n var l = (lon + (spin || 0)) * Math.PI / 180, p = lat * Math.PI / 180;\n var x = r * Math.cos(p) * Math.sin(l), y = -r * Math.sin(p), z = -r * Math.cos(p) * Math.cos(l);\n var t = (tilt || 0) * Math.PI / 180, c = Math.cos(t), s = Math.sin(t);\n return [x, y * c - z * s, y * s + z * c];\n}\n/**\n * Countries on a globe in a scene3d: o = {r, center [x, y, z], spin, tilt, color, colorOf(country) → colour,\n * lift (how far above the sea, 0.004)}. Faces beyond the horizon are clipped away.\n */\nfunction worldGlobe(S, o) {\n o = o || {};\n var r = o.r || 300, C = o.center || [0, 0, 0], lift = 1 + (o.lift != null ? o.lift : 0.004);\n var view = [S.camPos[0] - C[0], S.camPos[1] - C[1], S.camPos[2] - C[2]];\n var vl = Math.sqrt(view[0] * view[0] + view[1] * view[1] + view[2] * view[2]) || 1;\n var vn = [view[0] / vl, view[1] / vl, view[2] / vl];\n // The visible cap ends where the line from the camera grazes the sphere, a little short of the centre plane.\n var cut = (r * r) / vl;\n var plane = [C[0] + vn[0] * cut, C[1] + vn[1] * cut, C[2] + vn[2] * cut];\n var faces = [];\n worldShapes().forEach(function (c) {\n if (c.name === \"Antarctica\" && !o.antarctica) return;\n var col = o.colorOf ? o.colorOf(c) : o.color || \"#3a4150\";\n if (!col) return;\n c.rings.forEach(function (ring) {\n var pts = ring.map(function (p) { var q = worldOnSphere(p[0], p[1], r * lift, o.spin, o.tilt); return [q[0] + C[0], q[1] + C[1], q[2] + C[2]]; });\n var vis = S.clip(pts, vn, plane);\n if (vis.length < 3) return;\n var m = [0, 0, 0];\n vis.forEach(function (q) { m[0] += q[0] - C[0]; m[1] += q[1] - C[1]; m[2] += q[2] - C[2]; });\n var ml = Math.sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]) || 1;\n faces.push({ loops: [vis], n: [m[0] / ml, m[1] / ml, m[2] / ml], color: col, role: \"surface\" });\n });\n });\n S.add({ faces: faces }, { twoSided: true, spec: 0.12 });\n return S;\n}\n"}}
|
|
1
|
+
{"format":1,"package":"volito","version":"0.3.1","packs":[{"format":1,"id":"essentials","name":"MONARC Essentials","version":"1.0.0","description":"The graphics every video needs — name plates, titles, calls to action, stats, lists, quotes, end screens — in any brand, any aspect.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/essentials","style":"ember","shared":"shared.js","items":["lower-third","title-card","headline","subscribe","social","callout","stat","bar-chart","steps","quote","banner","end-card","logo-sting","wipe"],"entries":[{"id":"lower-third","name":"Lower third","description":"A name plate over footage: an accent spine grows, the plate wipes in, the name rises, the role slides in.","category":"lower-third","tags":["name","speaker","intro","interview","plate"],"placement":"overlay","duration":{"default":5,"min":2.5,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"name":{"type":"text","label":"Name","default":"Priya Sharma","maxLength":32},"role":{"type":"text","label":"Role or title","default":"Product designer","maxLength":44},"side":{"type":"choice","label":"Side","options":["left","right"],"default":"left"},"look":{"type":"choice","label":"Look","description":"plate: a card behind the text · minimal: a spine and the text on a soft scrim","options":["plate","minimal"],"default":"plate"}},"beats":{"in":0.15},"parts":{"scrim":"the soft dark wash behind it","spine":"the accent bar","plate":"the card","name":"the name","role":"the role line and its dot"},"pack":"essentials","ref":"essentials/lower-third","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n // A 9:16 frame is seen on a phone: the plate reads bigger there.\n const k = $.k * (A.tall ? 1.2 : 1);\n const tin = $.beat(\"in\", 0.15);\n const right = p.side === \"right\";\n const side = right ? \"right\" : \"left\";\n const name = $.case(p.name), role = String(p.role).toUpperCase();\n const tName = landOn($, p.name, tin);\n const tRole = landOn($, p.role, tName + 0.45, tName);\n // Sizes: the name in the display face, the role as a tracked-out mono label — both cut to fit the safe width.\n const maxW = A.w * (A.tall ? 0.86 : 0.62);\n const nf = fitFont($, name, \"display\", 88 * k, maxW - 100 * k, { ls: -0.02 });\n const rf = fitFont($, role, \"mono\", 30 * k, maxW - 100 * k, { ls: 0.14 });\n const pad = 46 * k;\n const w = Math.max($.measure(name, nf), $.measure(role, rf) + 38 * k) + pad * 2;\n const h = nf.size * 1.06 + rf.size * 1.25 + pad * 1.35;\n const y = A.tall ? H * 0.6 : A.square ? H * 0.66 : H * 0.7;\n const x = right ? W - A.x - w : A.x;\n const plate = p.look !== \"minimal\";\n const tx = right ? x + w - pad - 10 * k : x + pad + 10 * k;\n const anchor = right ? \"end\" : \"start\";\n // Everything leaves the way it came: back to its side.\n const leave = { out: \"slide\", outFrom: side, distance: 30 };\n\n let s = M.part(\"scrim\", { at: tin - 0.1, in: \"fade\", dur: 0.45, out: \"fade\", vary: false },\n $.scrim(0, y - 260 * k, W, H - y + 260 * k, { to: \"bottom\", a: plate ? 0.45 : 0.62 }));\n s += M.part(\"spine\", Object.assign({ at: tin, in: \"grow-y\", origin: \"top\", ease: \"expo\", dur: 0.45, box: { x: right ? x + w - 10 * k : x, y: y, w: 10 * k, h: h } }, leave),\n $.rect(right ? x + w - 10 * k : x, y, 10 * k, h, { r: 5 * k, fill: c.accent }));\n if (plate) s += M.part(\"plate\", Object.assign({ at: tin + 0.06, in: \"wipe\", from: side, dur: 0.5, box: { x: right ? x : x + 10 * k, y: y, w: w - 10 * k, h: h } }, leave),\n $.card(right ? x : x + 10 * k, y, w - 10 * k, h, { r: $.style.radius * 0.4, shadow: \"md\" }));\n s += M.text(\"name\", Object.assign({ at: tName + 0.16, in: \"rise\", distance: 26 }, leave, nf, { x: tx, y: y + pad * 0.62, anchor: anchor, fill: plate ? c.fg : \"#FFFFFF\" }), name);\n const ry = y + pad * 0.62 + nf.size * 1.06;\n const rw = $.measure(role, rf) + 26 * k;\n s += M.part(\"role\", Object.assign({ at: tRole, in: \"slide\", from: side, distance: 10, box: { x: right ? tx - rw : tx, y: ry, w: rw, h: rf.size } }, leave),\n $.circle(right ? tx - 7 * k : tx + 7 * k, ry + rf.size * 0.62, 7 * k, { fill: c.accent }),\n $.text(role, Object.assign({}, rf, { x: right ? tx - 26 * k : tx + 26 * k, y: ry, anchor: anchor, fill: plate ? c.mute : \"rgba(255,255,255,0.78)\" })));\n return s;\n}\n","libs":[]},{"id":"title-card","name":"Title card","description":"A full-frame chapter or opening title: a kicker chip, the title built letter by letter, a brush underline, a subtitle — over the brand's ground with an outlined number drifting behind.","category":"title","tags":["chapter","section","intro","opening","title"],"placement":"full","duration":{"default":4,"min":2,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"kicker":{"type":"text","label":"Kicker","description":"a small label above the title (empty for none)","default":"Chapter two","maxLength":28},"title":{"type":"text","label":"Title","default":"Depth over reach","maxLength":48},"subtitle":{"type":"text","label":"Subtitle","default":"Why the best channels say less","maxLength":80},"number":{"type":"text","label":"Number behind","description":"a big outlined number behind the title (\"02\"; empty for none)","default":"02","maxLength":3},"align":{"type":"choice","label":"Align","options":["center","left"],"default":"center"}},"beats":{"in":0.05},"parts":{"ground":"the brand's ground behind it all","number":"the big outlined number","kicker":"the small label above","title":"the title, line by line","underline":"the brush underline","subtitle":"the line under the title"},"pack":"essentials","ref":"essentials/title-card","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.05);\n const left = p.align === \"left\";\n const x = left ? A.x : W / 2;\n const anchor = left ? \"start\" : \"middle\";\n const title = $.case(p.title);\n const tTitle = landOn($, p.title, tin + 0.3);\n // The title as big as fits: one line when it can, up to three.\n const fit = fitLines($, title, Object.assign({}, $.font(\"display\"), { ls: -0.035 }), A.w * (left ? 0.86 : 0.94), Math.min(230 * k, A.h * 0.34), 70 * k, 3);\n const lh = fit.size * 1.02;\n const sub = p.subtitle ? $.case(p.subtitle) : \"\";\n const subF = Object.assign({}, $.font(\"text\"), { size: Math.max(34 * k, Math.min(52 * k, fit.size * 0.26)) });\n const subLines = sub ? $.wrap(sub, A.w * 0.8, subF) : [];\n const kick = p.kicker ? $.case(p.kicker) : \"\";\n const chip = kick ? $.chip.size(kick, { size: 30 * k }) : { w: 0, h: 0 };\n const chipH = kick ? chip.h + 40 * k : 0;\n const total = chipH + fit.lines.length * lh + (subLines.length ? 50 * k + subLines.length * subF.size * 1.25 : 0);\n let y = H / 2 - total / 2;\n\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [W * 0.72, H * 0.3] }));\n if (p.number) {\n const nf = Object.assign({}, $.font(\"display\"), { size: Math.min(H * 0.62, W * 0.5) });\n const nx = (left ? W * 0.78 : W / 2) + ($.wave(t, 6) - 0.5) * 40 * k;\n const nw = $.measure(String(p.number), nf);\n s += M.part(\"number\", { at: tin, in: \"drop\", distance: 90, box: { x: nx - nw / 2, y: H / 2 - nf.size * 0.55, w: nw, h: nf.size } },\n $.text(String(p.number), Object.assign({}, nf, { x: nx, y: H / 2 - nf.size * 0.55, anchor: \"middle\", fill: \"none\", stroke: $.rgba(c.fg, 0.13), sw: 3 * k })));\n }\n if (kick) {\n s += M.part(\"kicker\", { at: tin + 0.14, in: \"rise\", distance: 18, box: { x: left ? x : x - chip.w / 2, y: y, w: chip.w, h: chip.h } }, $.chip(kick, x, y, { size: 30 * k, anchor: anchor, dot: true }));\n y += chipH;\n }\n // Each line built letter by letter (word by word in scripts that join), rising on a spring.\n fit.lines.forEach(function (line, i) {\n s += M.text(\"title\", Object.assign({ at: tTitle + i * 0.16, in: \"rise\", split: \"letters\", distance: fit.size * 0.6 / k, ease: \"spring(2.4, 7)\", stagger: Math.min(0.04, 0.45 / Math.max(1, line.length)) }, fit.font, { x: x, y: y + i * lh, anchor: anchor }), line);\n });\n const lastLine = fit.lines[fit.lines.length - 1];\n const lastW = $.measure(lastLine, fit.font);\n const uy = y + (fit.lines.length - 1) * lh + fit.size * 1.0;\n const ux = left ? x : x - lastW / 2;\n // The underline draws on once the last letter has landed.\n const tUnder = tTitle + (fit.lines.length - 1) * 0.16 + Math.min(0.04, 0.45 / Math.max(1, lastLine.length)) * lastLine.length + 0.15;\n const sw = Math.max(8 * k, fit.size * 0.07);\n s += M.part(\"underline\", { at: tUnder, in: \"wipe\", from: \"left\", dur: 0.4, box: { x: ux - sw, y: uy - 14 * k, w: lastW + sw * 2, h: 28 * k } }, $.underline(1, 0, ux, uy, lastW, { sw: sw, dur: 0.01 }));\n y += fit.lines.length * lh + 50 * k;\n if (subLines.length) s += M.text(\"subtitle\", Object.assign({ at: tUnder + 0.1, in: \"rise\", distance: 20, split: \"lines\", stagger: 0.08 }, subF, { x: x, y: y, anchor: anchor, fill: c.mute, lh: 1.25, maxW: A.w * 0.8 }), sub);\n return s;\n}\n","libs":[]},{"id":"headline","name":"Kinetic headline","description":"Big words that pop in one by one — on the voice when you give it a transcript — with a marker swiped under the *starred* words.","category":"text","tags":["kinetic typography","words","captions","hook","headline","text","emphasis"],"placement":"overlay","duration":{"default":3.5,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"text":{"type":"longtext","label":"Text","description":"wrap words in *stars* to mark them","default":"Stop editing. *Start directing.*","maxLength":90},"ground":{"type":"choice","label":"Behind the words","description":"none: straight on the footage (with an outline) · scrim: a soft dark wash · panel: the brand's ground, full frame","options":["scrim","none","panel"],"default":"scrim"},"position":{"type":"choice","label":"Position","options":["center","top","bottom"],"default":"center"},"pace":{"type":"number","label":"Seconds per word","description":"without a transcript","default":0.16,"min":0.04,"max":1,"step":0.01}},"beats":{"in":0.1},"parts":{"ground":"the scrim or panel behind the words","words":"the words","marker":"the marker swiped under starred words"},"pack":"essentials","ref":"essentials/headline","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n const raw = $.case(String(p.text || \"\"));\n const plain = raw.replace(/\\*/g, \"\");\n const face = $.font(\"display\");\n const fit = fitLines($, plain, Object.assign({}, face, { ls: -0.03 }), A.w * 0.94, Math.min(150 * k, A.h * 0.16), 52 * k, A.tall ? 5 : 3);\n const lh = 1.12;\n const blockH = fit.size * (1 + (fit.lines.length - 1) * lh);\n const y = p.position === \"top\" ? A.y : p.position === \"bottom\" ? A.y + A.h - blockH : H / 2 - blockH / 2;\n // Words, each with its time (on the voice when there is a transcript, else one every `pace`) and whether it is starred.\n const timed = $.sync(markedRows(raw, fit.lines), tin).reduce(function (a, row) { return a.concat(row); }, []);\n const at = timed.map(function (w, i) { return $.words.length ? w.at : tin + i * p.pace; });\n const hl = timed.map(function (w) { return w.hl; });\n const font = Object.assign({}, fit.font, { x: W / 2, y: y, anchor: \"middle\", lh: lh, maxW: A.w * 0.94 });\n const lay = M.layout(plain, font, \"words\");\n const onFootage = p.ground !== \"panel\";\n let s = \"\";\n if (p.ground === \"panel\") s += M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n else if (p.ground === \"scrim\") {\n const to = p.position === \"top\" ? \"top\" : \"bottom\";\n s += M.part(\"ground\", { at: tin - 0.1, in: \"fade\", dur: 0.4, out: \"fade\", vary: false }, p.position === \"center\" ? $.rect(0, 0, W, H, { fill: \"#000000\", op: 0.38 }) : $.scrim(0, to === \"top\" ? 0 : y - 200 * k, W, to === \"top\" ? y + blockH + 200 * k : H - y + 200 * k, { to: to, a: 0.62 }));\n }\n // The marker: one swipe behind each run of starred words, once the run's last word is said.\n let i = 0;\n while (i < lay.units.length) {\n if (!hl[i]) { i++; continue; }\n let j = i;\n while (j + 1 < lay.units.length && hl[j + 1] && lay.units[j + 1].line === lay.units[i].line) j++;\n const a = lay.units[i], b = lay.units[j];\n const box = { x: a.x - fit.size * 0.1, y: a.y + fit.size * 0.18, w: b.x + b.w - a.x + fit.size * 0.2, h: fit.size * 0.86 };\n s += M.part(\"marker\", { at: (at[j] || tin) + 0.08, in: \"grow-x\", origin: \"left\", ease: \"out\", dur: 0.32, box: box }, $.rect(box.x, box.y, box.w, box.h, { r: fit.size * 0.14, fill: c.accent, transform: $.tf({ r: -1.2, ox: box.x, oy: box.y + box.h / 2 }) }));\n i = j + 1;\n }\n s += M.text(\"words\", Object.assign({ split: \"words\", in: \"rise\", distance: fit.size * 0.4 / k, ease: \"spring(2.4, 7.5)\", unitAt: function (u) { return at[u.index] != null ? at[u.index] : tin; }, fillFor: function () { return onFootage ? \"#FFFFFF\" : c.fg; } }, font), plain);\n if (p.ground === \"none\") s = $.g({ extra: 'filter=\"url(#mg-md)\"' }, s);\n return s;\n}\n","libs":[]},{"id":"subscribe","name":"Subscribe","description":"A call to action over footage: the channel card springs in, a cursor glides onto the button and presses it, the bell rings.","category":"cta","tags":["subscribe","follow","youtube","cta","button","bell","channel"],"placement":"overlay","duration":{"default":4.5,"min":3,"max":15},"aspects":["16:9","9:16","1:1","4:5"],"props":{"channel":{"type":"text","label":"Channel","default":"MONARC Studio","maxLength":26},"detail":{"type":"text","label":"Line under it","default":"1.2M subscribers","maxLength":30},"button":{"type":"text","label":"Button","default":"Subscribe","maxLength":14},"done":{"type":"text","label":"After the press","default":"Subscribed","maxLength":14},"avatar":{"type":"image","label":"Avatar","description":"a square picture for the channel (a play mark when empty)","default":""},"position":{"type":"choice","label":"Position","options":["auto","bottom-left","bottom-right","center"],"default":"auto"},"cursor":{"type":"boolean","label":"Cursor","default":true},"bell":{"type":"boolean","label":"Bell","default":true}},"beats":{"in":0.1},"parts":{"card":"the channel card","cursor":"the cursor that presses the button"},"pack":"essentials","ref":"essentials/subscribe","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n // A 16:9 frame has room for a bigger card; a 9:16 one is seen on a phone.\n const k = $.k * (A.wide ? 1.2 : A.tall ? 1.3 : 1.1);\n const press = landOn($, p.button, Math.min(tin + 1.1, M.outAt - 0.8), tin);\n const name = $.case(p.channel);\n const nf = fitFont($, name, \"display\", 42 * k, 430 * k);\n const cw = Math.max(560 * k, 160 * k + $.measure(name, nf) + 60 * k), ch = 300 * k;\n const pos = p.position === \"auto\" ? (A.wide ? \"bottom-right\" : \"center\") : p.position;\n const x = pos === \"center\" ? (W - cw) / 2 : pos === \"bottom-left\" ? A.x : W - A.x - cw;\n const y = pos === \"center\" ? (A.tall ? H * 0.58 : (H - ch) / 2) : A.y + A.h - ch;\n\n let card = $.card(x, y, cw, ch, { shadow: \"lg\" });\n if (p.avatar) card += $.image(\"avatar\", x + 38 * k, y + 40 * k, 80 * k, 80 * k, { r: 40 * k });\n else {\n card += $.circle(x + 78 * k, y + 80 * k, 40 * k, { fill: c.accent });\n card += $.icon(\"play\", x + 60 * k, y + 62 * k, 36 * k, { color: $.on(c.accent), fill: true });\n }\n card += $.text(name, Object.assign({}, nf, { x: x + 140 * k, y: y + 46 * k }));\n if (p.detail) card += $.text(p.detail, Object.assign({}, $.font(\"mono\"), { size: 24 * k, x: x + 140 * k, y: y + 98 * k, fill: c.mute }));\n const bx = x + 44 * k, by = y + 168 * k;\n card += $.button(t, $.case(p.button), bx, by, { size: 36 * k, icon: p.bell ? \"bell\" : undefined, pressAt: press, done: $.case(p.done) });\n if (p.bell) {\n const ring = $.bump(t, press + 0.15, 0.05, 0.6);\n const bellX = x + cw - 92 * k, bellY = y + 176 * k;\n card += $.g({ transform: $.tf({ r: Math.sin((t - press) * 40) * 18 * ring, ox: bellX + 28 * k, oy: bellY }) }, $.icon(\"bell\", bellX, bellY, 56 * k, { color: ring > 0.05 ? c.accent : c.mute }));\n }\n let s = M.part(\"card\", { at: tin, in: \"rise\", distance: 70, out: \"rise\", box: { x: x, y: y, w: cw, h: ch } }, card);\n // The cursor glides in from below and presses the button.\n if (p.cursor && t > press - 0.8 && t < press + 0.9) {\n const glide = $.E.inOut3($.seg(t, press - 0.75, press - 0.04));\n const px = $.lerp(x + cw + 180 * k, bx + 150 * k, glide), py = $.lerp(Math.min(H, y + ch + 220 * k), by + 52 * k, glide);\n s += M.part(\"cursor\", { at: press - 0.8, in: \"fade\", dur: 0.2, outAt: press + 0.6, out: \"fade\", outDur: 0.3 }, $.cursor(px, py, { press: $.bump(t, press, 0.05, 0.2), scale: 2 * k }));\n }\n return s;\n}\n","libs":[]},{"id":"social","name":"Social handle","description":"Your handle in a corner: a pill with the platform's icon slides in, a follow button presses, it tucks away.","category":"social","tags":["handle","follow","instagram","tiktok","youtube","x","linkedin","username","social"],"placement":"overlay","duration":{"default":4,"min":2.5,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"handle":{"type":"text","label":"Handle","default":"@monarcstudio","maxLength":30},"platform":{"type":"choice","label":"Platform","description":"picks the icon and the line under the handle","options":["youtube","instagram","tiktok","x","linkedin","threads","website"],"default":"instagram"},"action":{"type":"text","label":"Button","description":"empty for no button","default":"Follow","maxLength":12},"done":{"type":"text","label":"After the press","default":"Following","maxLength":12},"corner":{"type":"choice","label":"Corner","options":["bottom-left","bottom-right","top-left","top-right","bottom-center"],"default":"bottom-left"}},"beats":{"in":0.15},"parts":{"pill":"the pill behind","icon":"the platform icon","handle":"the handle and platform line","button":"the follow button"},"pack":"essentials","ref":"essentials/social","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nvar PLATFORMS = {\n youtube: { icon: \"play\", line: \"YouTube\" },\n instagram: { icon: \"camera\", line: \"Instagram\" },\n tiktok: { icon: \"reel\", line: \"TikTok\" },\n x: { icon: \"x\", line: \"X\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\" },\n threads: { icon: \"chat\", line: \"Threads\" },\n website: { icon: \"globe\", line: \"Website\" },\n};\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k * 1.05, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.15);\n const pl = PLATFORMS[p.platform] || PLATFORMS.instagram;\n const hf = fitFont($, p.handle, \"display\", 40 * k, A.w * 0.5);\n const lf = Object.assign({}, $.font(\"mono\"), { size: 20 * k, ls: 0.14 });\n const hasBtn = !!String(p.action || \"\").trim();\n const b1 = hasBtn ? $.button.size($.case(p.action), { size: 26 * k }) : { w: 0, h: 0 };\n const b2 = hasBtn ? $.button.size($.case(p.done || p.action), { size: 26 * k, icon: \"check\" }) : { w: 0, h: 0 };\n const bw = Math.max(b1.w, b2.w);\n const ph = 112 * k;\n const pw = ph + Math.max($.measure(p.handle, hf), $.measure(pl.line.toUpperCase(), lf)) + 34 * k + (hasBtn ? bw + 30 * k : 10 * k);\n const corner = p.corner;\n const x = corner === \"bottom-center\" ? (W - pw) / 2 : corner.indexOf(\"left\") >= 0 ? A.x : W - A.x - pw;\n const y = corner.indexOf(\"top\") === 0 ? A.y : A.y + A.h - ph;\n const side = corner.indexOf(\"right\") >= 0 ? \"right\" : corner === \"bottom-center\" ? \"down\" : \"left\";\n const press = landOn($, p.action, tin + 0.9, tin);\n // Every part leaves the way the pill came in, off its own side.\n const leave = { out: \"slide\", outFrom: side, distance: 50 };\n\n let s = M.part(\"pill\", Object.assign({ at: tin, in: \"slide\", from: side, box: { x: x, y: y, w: pw, h: ph } }, leave), $.rect(x, y, pw, ph, { r: ph / 2, fill: c.surface, stroke: c.line, sw: 2 * k, shadow: \"lg\" }));\n s += M.part(\"icon\", Object.assign({ at: tin + 0.1, in: \"spin\", intensity: 0.12, box: { x: x + ph * 0.14, y: y + ph * 0.14, w: ph * 0.72, h: ph * 0.72 } }, leave),\n $.circle(x + ph / 2, y + ph / 2, ph * 0.36, { fill: c.accent }) +\n $.icon(pl.icon, x + ph / 2 - ph * 0.17, y + ph / 2 - ph * 0.17, ph * 0.34, { color: $.on(c.accent), sw: 2.4 }));\n s += M.part(\"handle\", Object.assign({ at: tin + 0.2, in: \"slide\", from: \"right\", distance: 8, box: { x: x + ph, y: y + ph * 0.2, w: pw - ph, h: ph * 0.6 } }, leave),\n $.text(p.handle, Object.assign({}, hf, { x: x + ph + 4 * k, y: y + ph * 0.2 })) +\n $.text(pl.line.toUpperCase(), Object.assign({}, lf, { x: x + ph + 6 * k, y: y + ph * 0.2 + hf.size * 1.08, fill: c.mute })));\n if (hasBtn) s += M.part(\"button\", Object.assign({ at: tin + 0.35, in: \"pop\", box: { x: x + pw - 22 * k - bw, y: y + (ph - b1.h) / 2, w: bw, h: b1.h } }, leave),\n $.button(t, $.case(p.action), x + pw - 22 * k - bw, y + (ph - b1.h) / 2, { size: 26 * k, pressAt: press, done: $.case(p.done || p.action) }));\n return s;\n}\n","libs":[]},{"id":"callout","name":"Callout","description":"Points at something in the shot: a ring pulses on the spot, a line draws out to a label card that pops in beside it.","category":"callout","tags":["annotation","pointer","arrow","label","highlight","tutorial","product"],"placement":"overlay","duration":{"default":4,"min":2,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"label":{"type":"text","label":"Label","default":"Export in one click","maxLength":40},"detail":{"type":"text","label":"Second line","default":"ProRes, WebM or MP4","maxLength":48},"x":{"type":"number","label":"Spot across","description":"where the spot is, 0 (left edge) to 1 (right edge)","default":0.62,"min":0,"max":1,"step":0.01},"y":{"type":"number","label":"Spot down","description":"where the spot is, 0 (top) to 1 (bottom)","default":0.44,"min":0,"max":1,"step":0.01},"side":{"type":"choice","label":"Label side","options":["auto","left","right","above","below"],"default":"auto"},"icon":{"type":"icon","label":"Icon","description":"an icon name (volito api lists them; empty for none)","default":"sparkle"}},"beats":{"in":0.1},"parts":{"pulse":"the ring pulsing on the spot","dot":"the spot","line":"the line out to the card","card":"the label card"},"pack":"essentials","ref":"essentials/callout","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n const sx = W * p.x, sy = H * p.y;\n const label = $.case(p.label);\n const lf = fitFont($, label, \"display\", 46 * k, A.w * 0.5);\n const df = Object.assign({}, $.font(\"text\"), { size: 30 * k });\n const hasIcon = !!p.icon && $.icons.indexOf(p.icon) >= 0;\n const iw = hasIcon ? 64 * k : 0;\n const cw = Math.max($.measure(label, lf), p.detail ? $.measure(p.detail, df) : 0) + iw + 72 * k;\n const ch = lf.size * 1.15 + (p.detail ? df.size * 1.35 : 0) + 56 * k;\n // The label sits where there is room: away from the nearer edge.\n let side = p.side;\n if (side === \"auto\") side = A.wide ? (p.x > 0.5 ? \"left\" : \"right\") : p.y > 0.5 ? \"above\" : \"below\";\n const gap = 150 * k;\n let cx = side === \"left\" ? sx - gap - cw : side === \"right\" ? sx + gap : sx - cw / 2;\n let cy = side === \"above\" ? sy - gap - ch : side === \"below\" ? sy + gap : sy - ch / 2;\n cx = Math.max(A.x, Math.min(W - A.x - cw, cx));\n cy = Math.max(A.y, Math.min(A.y + A.h - ch, cy));\n // The line runs from the spot to the card's nearest edge.\n const ex = side === \"left\" ? cx + cw : side === \"right\" ? cx : Math.max(cx + 30 * k, Math.min(cx + cw - 30 * k, sx));\n const ey = side === \"above\" ? cy + ch : side === \"below\" ? cy : Math.max(cy + 20 * k, Math.min(cy + ch - 20 * k, sy));\n const dl = Math.hypot(ex - sx, ey - sy) || 1;\n const r0 = 26 * k;\n const lx = sx + ((ex - sx) / dl) * r0, ly = sy + ((ey - sy) / dl) * r0;\n // The line draws out from the spot: a wipe from the spot's side.\n const lineFrom = Math.abs(ex - sx) > Math.abs(ey - sy) ? (ex > sx ? \"left\" : \"right\") : ey > sy ? \"up\" : \"down\";\n\n const pulse = ((t - tin) % 1.4) / 1.4;\n let s = M.part(\"pulse\", { at: tin, in: \"fade\", dur: 0.2, box: { x: sx - 70 * k, y: sy - 70 * k, w: 140 * k, h: 140 * k } },\n t > tin ? $.circle(sx, sy, (18 + 50 * $.E.out3(pulse)) * k, { stroke: c.accent, sw: 4 * k * (1 - pulse), op: 1 - pulse }) : \"\");\n s += M.part(\"dot\", { at: tin, in: \"pop\", box: { x: sx - 21 * k, y: sy - 21 * k, w: 42 * k, h: 42 * k } }, $.circle(sx, sy, 16 * k, { fill: c.accent, stroke: \"#FFFFFF\", sw: 5 * k, shadow: \"sm\" }));\n s += M.part(\"line\", { at: tin + 0.25, in: \"wipe\", from: lineFrom, ease: \"inOut\", dur: 0.35, box: { x: Math.min(lx, ex) - 4 * k, y: Math.min(ly, ey) - 4 * k, w: Math.abs(ex - lx) + 8 * k, h: Math.abs(ey - ly) + 8 * k } },\n $.line(lx, ly, ex, ey, { stroke: \"#FFFFFF\", sw: 4 * k }));\n let card = $.card(cx, cy, cw, ch, { shadow: \"lg\" });\n if (hasIcon) card += $.rect(cx + 28 * k, cy + 28 * k, 48 * k, 48 * k, { r: 14 * k, fill: c.tint }) + $.icon(p.icon, cx + 36 * k, cy + 36 * k, 32 * k, { color: c.accent });\n card += $.text(label, Object.assign({}, lf, { x: cx + 36 * k + iw, y: cy + 28 * k }));\n if (p.detail) card += $.text(p.detail, Object.assign({}, df, { x: cx + 36 * k + iw, y: cy + 28 * k + lf.size * 1.2, fill: c.mute }));\n s += M.part(\"card\", { at: tin + 0.55, in: \"pop\", box: { x: cx, y: cy, w: cw, h: ch }, ox: ex, oy: ey }, card);\n return s;\n}\n","libs":[]},{"id":"stat","name":"Stat","description":"One number that matters, full frame: it counts up as it lands, the label rises under it, a ring fills to the value or bars grow to it.","category":"stat","tags":["number","statistic","count","percent","growth","kpi","data"],"placement":"full","duration":{"default":4,"min":2,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"value":{"type":"number","label":"Value","default":3.2},"decimals":{"type":"number","label":"Decimals","default":1,"min":0,"max":3,"step":1},"prefix":{"type":"text","label":"Before the number","default":"","maxLength":4},"suffix":{"type":"text","label":"After the number","default":"×","maxLength":6},"label":{"type":"text","label":"Label","default":"more reach in 90 days","maxLength":48},"context":{"type":"text","label":"Small print","description":"a source or a comparison, in the mono face","default":"Across 1,200 creator channels","maxLength":60},"chart":{"type":"choice","label":"Chart","options":["bars","ring","none"],"default":"bars"},"max":{"type":"number","label":"Ring full at","description":"the value a full ring stands for (100 for a percent)","default":100,"when":{"prop":"chart","is":["ring"]}},"series":{"type":"list","label":"Bars","description":"the bars' values, the last one is the stat (empty: four steps up to it)","of":"number","default":[],"maxItems":8,"when":{"prop":"chart","is":["bars"]}}},"beats":{"in":0.1},"parts":{"ground":"the brand's ground","number":"the big number","label":"the label under it","context":"the small print","chart":"the bars or ring"},"pack":"essentials","ref":"essentials/stat","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n const tNum = landOn($, String(p.value), tin + 0.15);\n const tLabel = landOn($, p.label, tNum + 0.55, tNum);\n const dec = Math.max(0, Math.min(3, Math.round(p.decimals)));\n // The widest the counter gets (what $.counter draws at the end), so the size never jumps.\n const final = (p.prefix || \"\") + (dec ? Number(p.value).toFixed(dec) : String(Math.round(Number(p.value))).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\")) + (p.suffix || \"\");\n const chart = p.chart;\n const side = chart !== \"none\" && A.wide;\n // The number takes the room it owns: the left half beside a chart on 16:9, the width otherwise.\n const room = side ? A.w * 0.5 : A.w * 0.9;\n const big = fitFont($, final, \"display\", Math.min(340 * k, A.h * (chart !== \"none\" && !side ? 0.26 : 0.42)), room, { ls: -0.05 });\n const label = $.case(p.label);\n const lf = fitFont($, label, \"text\", Math.max(40 * k, big.size * 0.2), room);\n const cf = Object.assign({}, $.font(\"mono\"), { size: 26 * k, ls: 0.06 });\n const blockH = big.size * 1.02 + lf.size * 1.3 + (p.context ? cf.size * 2.2 : 0);\n const chartH = chart === \"none\" || side ? 0 : A.h * 0.34;\n const top = A.y + (A.h - blockH - chartH - (chartH ? 60 * k : 0)) / 2;\n const x = side ? A.x + A.w * 0.02 : W / 2;\n const anchor = side ? \"start\" : \"middle\";\n\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n // The number counts up over the time its entrance takes (at least 0.9 s).\n const np = M.progress(\"number\", { at: tNum, in: \"rise\", distance: 40, dur: 0.9 });\n const counting = $.counter(t, np.at, np.at + Math.max(0.9, np.end - np.at), Number(p.value), { dec: dec, prefix: p.prefix || \"\", suffix: p.suffix || \"\" });\n s += M.text(\"number\", Object.assign({ at: tNum, in: \"rise\", distance: 40 }, big, { x: x, y: top, anchor: anchor, fill: c.accent }), counting);\n s += M.text(\"label\", Object.assign({ at: tLabel, in: \"rise\", distance: 20 }, lf, { x: x, y: top + big.size * 1.04, anchor: anchor }), label);\n if (p.context) s += M.text(\"context\", Object.assign({ at: tLabel + 0.25, in: \"fade\", dur: 0.4 }, cf, { x: x, y: top + big.size * 1.04 + lf.size * 1.45, anchor: anchor, fill: c.mute, maxW: room }), p.context);\n\n // The chart: beside the number on 16:9, under it elsewhere.\n const box = side ? { x: A.x + A.w * 0.58, y: A.y + A.h * 0.12, w: A.w * 0.4, h: A.h * 0.76 } : { x: W / 2 - Math.min(A.w * 0.8, chartH * 1.6) / 2, y: top + blockH + 60 * k, w: Math.min(A.w * 0.8, chartH * 1.6), h: chartH };\n if (chart === \"ring\") {\n const r = Math.min(box.w, box.h) * 0.4, cx = box.x + box.w / 2, cy = box.y + box.h / 2;\n const fill = Math.max(0, Math.min(1, Number(p.value) / (Number(p.max) || 100)));\n s += M.part(\"chart\", { at: tNum - 0.05, in: \"pop\", box: { x: cx - r * 1.1, y: cy - r * 1.1, w: r * 2.2, h: r * 2.2 } },\n $.ring(cx, cy, r, fill * $.E.out3($.seg(t, tNum, tNum + 0.9)), { sw: r * 0.16 }),\n $.text(Math.round(fill * 100) + \"%\", Object.assign({}, $.font(\"mono\"), { size: r * 0.34, x: cx, y: cy - r * 0.2, anchor: \"middle\", fill: c.fg, op: $.fadeIn(t, tNum + 0.6, 0.3) })));\n } else if (chart === \"bars\") {\n const v = Number(p.value) || 1;\n const series = Array.isArray(p.series) && p.series.length >= 2 ? p.series.map(Number) : [v * 0.34, v * 0.52, v * 0.71, v];\n s += M.part(\"chart\", { at: tNum - 0.1, in: \"fade\", dur: 0.2, box: box },\n $.bars(t, tNum + 0.05, box.x, box.y, box.w, box.h, series, { highlight: series.length - 1, stagger: 0.1 }),\n $.line(box.x - 10 * k, box.y + box.h, box.x + box.w + 10 * k, box.y + box.h, { stroke: c.line, sw: 3 * k, p: $.E.out3($.seg(t, tNum - 0.1, tNum + 0.4)) }));\n }\n return s;\n}\n","libs":[]},{"id":"bar-chart","name":"Bar chart","description":"A ranked bar chart, full frame: each bar grows in turn with its value counting up beside it; one row can stand out in the accent.","category":"chart","tags":["chart","data","ranking","comparison","bars","graph","statistics"],"placement":"full","duration":{"default":6,"min":3,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"title":{"type":"text","label":"Heading","default":"Where viewers drop off","maxLength":48},"rows":{"type":"list","label":"Rows","description":"label and value per row (\"Intro: 92\")","of":{"label":"text","value":"number"},"default":[{"label":"First 3 seconds","value":92},{"label":"First 30 seconds","value":64},{"label":"Halfway","value":41},{"label":"The end","value":23}],"minItems":2,"maxItems":7},"prefix":{"type":"text","label":"Before each value","default":"","maxLength":3},"suffix":{"type":"text","label":"After each value","default":"%","maxLength":6},"decimals":{"type":"number","label":"Decimals","default":0,"min":0,"max":2,"step":1},"highlight":{"type":"number","label":"Row in the accent","description":"1 for the first row; 0 for the largest value; -1 for none","default":0,"min":-1,"max":7,"step":1},"source":{"type":"text","label":"Source","default":"Retention across 300 uploads","maxLength":60}},"beats":{"in":0.1},"parts":{"ground":"the brand's ground","title":"the heading","rows":"each row (label, bar, value)","source":"the source line"},"pack":"essentials","ref":"essentials/bar-chart","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n const rows = (p.rows || []).slice(0, 7);\n const n = Math.max(1, rows.length);\n const max = Math.max.apply(null, rows.map(function (r) { return Number(r.value) || 0; }).concat([1e-9]));\n let hi = Math.round(p.highlight);\n if (hi === 0) hi = rows.reduce(function (b, r, i) { return (Number(r.value) || 0) > (Number(rows[b].value) || 0) ? i : b; }, 0);\n else hi = hi - 1;\n const title = $.case(p.title);\n const tf = fitLines($, title, Object.assign({}, $.font(\"display\"), { ls: -0.03 }), A.w, Math.min((A.tall ? 120 : 100) * k, A.h * 0.1), 44 * k, 2);\n const headH = tf.lines.length * tf.size * 1.05 + 60 * k;\n const footH = p.source ? 70 * k : 0;\n const gapY = 26 * k;\n const rh = Math.min((A.tall ? 210 : A.wide ? 150 : 185) * k, (A.h - headH - footH - gapY * (n - 1)) / n);\n const top = A.y + (A.h - headH - footH - n * rh - (n - 1) * gapY) / 2;\n const w = A.wide ? Math.min(A.w, 1500 * k) : A.w;\n const x = (W - w) / 2;\n const fmt = function (v) { return (p.prefix || \"\") + (p.decimals ? v.toFixed(p.decimals) : String(Math.round(v)).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\")) + (p.suffix || \"\"); };\n // Labels sit above their bar on tall frames, beside it on wide ones.\n const lf = Object.assign({}, $.font(\"text\"), { size: Math.min((A.tall ? 46 : 40) * k, rh * (A.wide ? 0.4 : 0.3)) });\n const labels = rows.map(function (r) { return $.case(r.label); });\n const labelW = A.wide ? Math.min(w * 0.34, widest($, labels, lf) + 44 * k) : 0;\n const vf = Object.assign({}, $.font(\"mono\"), { size: Math.min(44 * k, rh * 0.36) });\n const valueW = $.measure(fmt(max), vf) + 30 * k;\n const barX = x + labelW;\n const barW = w - labelW - valueW;\n\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n tf.lines.forEach(function (line, i) {\n s += M.text(\"title\", Object.assign({ at: tin + i * 0.1, in: \"rise\", distance: 26 }, tf.font, { x: x, y: top + i * tf.size * 1.05 }), line);\n });\n const start = tin + 0.4;\n rows.forEach(function (r, i) {\n const at = landOn($, r.label, start + i * 0.18, start);\n const ry = top + headH + i * (rh + gapY);\n const v = Number(r.value) || 0;\n // The bar grows (and its value counts) over the row's entrance, at least 0.8 s.\n const rp = M.progress(\"rows\", { at: at - 0.05, in: \"fade\", dur: 0.25 });\n const grow = $.E.out3($.seg(t, rp.at, rp.at + 0.8));\n const bh = A.wide ? rh * 0.66 : rh * 0.48;\n const by = A.wide ? ry + (rh - bh) / 2 : ry + rh - bh;\n const col = hi === i ? c.accent : $.rgba(c.fg, 0.28);\n let row = \"\";\n if (A.wide) row += $.text(labels[i], Object.assign({}, lf, { x: x, y: ry + (rh - lf.size) / 2, fill: hi === i ? c.fg : c.mute, maxW: labelW - 20 * k }));\n else row += $.text(labels[i], Object.assign({}, lf, { x: x, y: ry, fill: hi === i ? c.fg : c.mute }));\n row += $.rect(barX, by, barW, bh, { r: bh * 0.22, fill: $.rgba(c.fg, 0.06) });\n row += $.rect(barX, by, Math.max(bh * 0.44, barW * (v / max) * grow), bh, { r: bh * 0.22, fill: col, shadow: hi === i ? \"glow\" : null });\n row += $.text(fmt(v * grow), Object.assign({}, vf, { x: barX + barW + 24 * k, y: by + (bh - vf.size) / 2 - vf.size * 0.05, fill: hi === i ? c.accent : c.fg }));\n s += M.part(\"rows\", { at: at - 0.05, in: \"fade\", dur: 0.25, box: { x: x, y: ry, w: w, h: rh } }, row);\n });\n if (p.source) s += M.text(\"source\", Object.assign({ at: start + n * 0.18 + 0.4, in: \"fade\", dur: 0.4 }, $.font(\"mono\"), { size: 24 * k, x: x, y: top + headH + n * rh + (n - 1) * gapY + 34 * k, fill: c.mute, ls: 0.06 }), p.source);\n return s;\n}\n","libs":[]},{"id":"steps","name":"Steps","description":"A numbered list or checklist, full frame: the heading lands, then each step slides in on its own card — numbered, or ticked off one by one.","category":"list","tags":["list","steps","how to","process","checklist","agenda","tips","framework"],"placement":"full","duration":{"default":6,"min":3,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"kicker":{"type":"text","label":"Kicker","default":"The framework","maxLength":28},"title":{"type":"text","label":"Heading","default":"Three steps to a better hook","maxLength":48},"steps":{"type":"list","label":"Steps","of":"text","default":["Open on the payoff","Cut every pause","Land one idea per shot"],"minItems":1,"maxItems":6},"marker":{"type":"choice","label":"Marker","options":["numbers","checks"],"default":"numbers"},"gap":{"type":"number","label":"Seconds between steps","default":0.35,"min":0.1,"max":5,"step":0.05}},"beats":{"in":0.1},"parts":{"ground":"the brand's ground","kicker":"the small label","title":"the heading","steps":"each step's card"},"pack":"essentials","ref":"essentials/steps","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n const rows = (p.steps || []).slice(0, 6);\n const n = Math.max(1, rows.length);\n const kick = p.kicker ? $.case(p.kicker) : \"\";\n const title = $.case(p.title);\n const tf = fitLines($, title, Object.assign({}, $.font(\"display\"), { ls: -0.03 }), A.w, Math.min((A.tall ? 140 : 110) * k, A.h * 0.11), 48 * k, 2);\n const chip = kick ? $.chip.size(kick, { size: 28 * k }) : { w: 0, h: 0 };\n const chipH = kick ? chip.h + 28 * k : 0;\n const headH = chipH + tf.lines.length * tf.size * 1.05 + 56 * k;\n // Rows share the room under the heading, with a gap.\n const room = A.h - headH;\n const gapY = 22 * k;\n const rh = Math.min((A.tall ? 200 : A.wide ? 150 : 180) * k, (room - gapY * (n - 1)) / n);\n const total = headH + n * rh + (n - 1) * gapY;\n let y = A.y + (A.h - total) / 2;\n const w = A.tall ? A.w : Math.min(A.w, 1500 * k);\n const ox = A.wide ? (W - w) / 2 : A.x;\n\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [W * 0.8, H * 0.2] }));\n if (kick) {\n s += M.part(\"kicker\", { at: tin, in: \"rise\", distance: 16, box: { x: ox, y: y, w: chip.w, h: chip.h } }, $.chip(kick, ox, y, { size: 28 * k, dot: true }));\n y += chipH;\n }\n tf.lines.forEach(function (line, i) {\n s += M.text(\"title\", Object.assign({ at: tin + 0.12 + i * 0.1, in: \"rise\", distance: 28 }, tf.font, { x: ox, y: y + i * tf.size * 1.05 }), line);\n });\n y += tf.lines.length * tf.size * 1.05 + 56 * k;\n const first = landOn($, rows[0], tin + 0.55);\n rows.forEach(function (row, i) {\n const at = landOn($, row, first + i * p.gap, first + (i - 1) * p.gap);\n const ry = y + i * (rh + gapY);\n const badge = rh * 0.62;\n const label = $.case(row);\n const lf = fitFont($, label, \"display\", Math.min(60 * k, rh * 0.42), w - badge - rh * 0.9);\n let card = $.card(ox, ry, w, rh, { r: Math.min($.style.radius, rh * 0.3), shadow: \"md\" });\n const bx = ox + (rh - badge) / 2, by = ry + (rh - badge) / 2;\n if (p.marker === \"checks\") {\n const done = $.E.out3($.seg(t, at + 0.25, at + 0.55));\n card += $.rect(bx, by, badge, badge, { r: badge * 0.28, fill: done > 0.5 ? c.accent : \"none\", stroke: c.accent, sw: 4 * k });\n card += $.path(\"M\" + (bx + badge * 0.24) + \" \" + (by + badge * 0.52) + \"L\" + (bx + badge * 0.43) + \" \" + (by + badge * 0.7) + \"L\" + (bx + badge * 0.78) + \" \" + (by + badge * 0.32), { stroke: $.on(c.accent), sw: badge * 0.11, draw: done });\n } else {\n card += $.rect(bx, by, badge, badge, { r: badge * 0.28, fill: c.accent });\n card += $.text(String(i + 1).padStart(2, \"0\"), Object.assign({}, $.font(\"mono\"), { size: badge * 0.42, x: bx + badge / 2, y: by + badge * 0.27, anchor: \"middle\", fill: $.on(c.accent) }));\n }\n card += $.text(label, Object.assign({}, lf, { x: ox + rh * 0.95, y: ry + (rh - lf.size) / 2 - lf.size * 0.04 }));\n s += M.part(\"steps\", { at: at, in: \"slide\", from: \"left\", distance: 33, box: { x: ox, y: ry, w: w, h: rh } }, card);\n });\n return s;\n}\n","libs":[]},{"id":"quote","name":"Quote","description":"A pull quote, full frame: a big quotation mark pops, the words set in line by line, the speaker signs it off under an accent rule.","category":"quote","tags":["quote","testimonial","review","citation","words"],"placement":"full","duration":{"default":6,"min":3,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"quote":{"type":"longtext","label":"Quote","default":"The best tools disappear. You stop thinking about the editor and start thinking about the story.","maxLength":220},"author":{"type":"text","label":"Who said it","default":"Maya Chen","maxLength":32},"role":{"type":"text","label":"Their role","default":"Documentary editor","maxLength":44},"face":{"type":"choice","label":"Typeface","description":"serif: the brand's accent face (an italic serif) · display: its headline face","options":["serif","display"],"default":"serif"}},"beats":{"in":0.1},"parts":{"ground":"the brand's ground","mark":"the quotation mark","quote":"the quote, line by line","rule":"the short accent rule","author":"who said it","role":"their role"},"pack":"essentials","ref":"essentials/quote","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n const face = p.face === \"display\" ? $.font(\"display\") : $.font(\"accent\");\n const text = String(p.quote).replace(/^[\"“”]+|[\"“”]+$/g, \"\").trim();\n const maxW = A.w * (A.wide ? 0.78 : 0.88);\n const fit = fitLines($, text, Object.assign({}, face, { ls: p.face === \"display\" ? -0.02 : 0 }), maxW, Math.min(96 * k, A.h * 0.12), 38 * k, A.tall ? 8 : 5);\n const lh = 1.22;\n const markSize = Math.max(160 * k, fit.size * 2.4);\n const af = Object.assign({}, $.font(\"display\"), { size: Math.max(34 * k, fit.size * 0.5) });\n const rf = Object.assign({}, $.font(\"mono\"), { size: Math.max(22 * k, fit.size * 0.3), ls: 0.12 });\n const blockH = markSize * 0.55 + fit.lines.length * fit.size * lh + 70 * k + af.size * 1.2 + (p.role ? rf.size * 1.6 : 0);\n const x = A.wide ? (W - maxW) / 2 : A.x;\n let y = A.y + (A.h - blockH) / 2;\n const tWords = landOn($, text, tin + 0.35);\n const leave = { out: \"drop\", distance: 24, outDur: 0.4 };\n\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [W * 0.2, H * 0.25] }));\n // The quotation mark, in the accent, springs in first.\n s += M.part(\"mark\", Object.assign({ at: tin, in: \"spin\", intensity: 0.05, box: { x: x - markSize * 0.04, y: y - markSize * 0.18, w: markSize * 0.6, h: markSize * 0.9 } }, leave),\n $.text(\"“\", Object.assign({}, $.font(\"accent\"), { size: markSize, x: x - markSize * 0.04, y: y - markSize * 0.18, fill: c.accent })));\n y += markSize * 0.55;\n s += M.text(\"quote\", Object.assign({ at: tWords, in: \"rise\", distance: 24, split: \"lines\", stagger: 0.14 }, leave, fit.font, { x: x, y: y, lh: lh, maxW: maxW }), text);\n y += fit.lines.length * fit.size * lh + 40 * k;\n const tSign = tWords + fit.lines.length * 0.14 + 0.25;\n s += M.part(\"rule\", Object.assign({ at: tSign, in: \"grow-x\", origin: \"left\", dur: 0.4, box: { x: x, y: y, w: 90 * k, h: 6 * k } }, leave), $.rect(x, y, 90 * k, 6 * k, { r: 3 * k, fill: c.accent }));\n y += 30 * k;\n s += M.text(\"author\", Object.assign({ at: tSign + 0.1, in: \"slide\", from: \"left\", distance: 8 }, leave, af, { x: x, y: y }), $.case(p.author));\n if (p.role) s += M.text(\"role\", Object.assign({ at: tSign + 0.22, in: \"slide\", from: \"left\", distance: 8 }, leave, rf, { x: x, y: y + af.size * 1.2, fill: c.mute }), String(p.role).toUpperCase());\n return s;\n}\n","libs":[]},{"id":"banner","name":"News banner","description":"A broadcast strap over footage: the label block slams in, the headline bar wipes across, a ticker scrolls under it.","category":"banner","tags":["breaking news","live","strap","ticker","chyron","broadcast","headline"],"placement":"overlay","duration":{"default":8,"min":3,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"label":{"type":"text","label":"Label","default":"Breaking","maxLength":14},"headline":{"type":"text","label":"Headline","default":"AI editors now cut a reel in under a minute","maxLength":70},"ticker":{"type":"longtext","label":"Ticker","description":"items separated by \" · \" scroll under the headline (empty for none)","default":"Creators report 3× more output · Open motion packs land on npm · Agents can now render graphics on their own","maxLength":280},"live":{"type":"boolean","label":"Live dot","default":true},"speed":{"type":"number","label":"Ticker speed","description":"px a second at 1080 px","default":140,"min":30,"max":600}},"beats":{"in":0.1},"parts":{"label":"the label block","bar":"the headline bar and ticker strip","headline":"the headline text","ticker":"the scrolling ticker text"},"pack":"essentials","ref":"essentials/banner","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.15 : 1);\n const tin = $.beat(\"in\", 0.1);\n const label = String(p.label || \"\").toUpperCase();\n const lf = Object.assign({}, $.font(\"display\"), { size: 40 * k, ls: 0.06 });\n const hf = fitFont($, $.case(p.headline), \"display\", 48 * k, A.w * (A.wide ? 0.74 : 0.92) - 60 * k);\n const tf = Object.assign({}, $.font(\"mono\"), { size: 26 * k, ls: 0.04 });\n const hasTicker = !!String(p.ticker || \"\").trim();\n const bh = 96 * k, th = hasTicker ? 52 * k : 0;\n // On 9:16 the label sits on its own row above the headline (the strap is too narrow for both).\n const stack = !A.wide;\n const lw = $.measure(label, lf) + (p.live ? 70 * k : 46 * k);\n const x = stack ? A.x : A.x * 0.6;\n const w = W - 2 * x;\n const yH = (stack ? H * 0.64 : H * 0.78) + (stack ? 70 * k : 0);\n const yL = stack ? yH - 70 * k : yH;\n const hx = stack ? x : x + lw;\n const bw = w - (hx - x);\n const leave = { out: \"rise\", distance: 30, outDur: 0.35 };\n\n // The headline bar wipes out from behind the label; the ticker strip under it.\n let s = M.part(\"bar\", Object.assign({ at: tin + 0.18, in: \"wipe\", from: \"left\", dur: 0.45, box: { x: hx, y: yH, w: bw, h: bh + th } }, leave),\n $.rect(hx, yH, bw, bh, { fill: c.surface, shadow: \"md\" }),\n hasTicker ? $.rect(hx, yH + bh, bw, th, { fill: c.ink }) : \"\");\n s += $.clip(hx, yH, bw, bh, 0, M.text(\"headline\", Object.assign({ at: tin + 0.35, in: \"rise\", distance: 24 }, leave, hf, { x: hx + 30 * k, y: yH + (bh - hf.size) / 2 - hf.size * 0.04, within: { x: hx, y: yH, w: bw, h: bh } }), $.case(p.headline)));\n if (hasTicker) {\n const items = String(p.ticker).replace(/\\s+/g, \" \").trim();\n const loop = items + \" · \";\n const lw2 = $.measure(loop, tf);\n const off = (Math.max(0, t - tin - 0.35) * p.speed * k) % lw2;\n let run = \"\";\n for (let i = 0; i < Math.ceil((w + lw2) / lw2) + 1; i++) run += loop;\n s += $.clip(hx, yH + bh, bw, th, 0, M.part(\"ticker\", Object.assign({ at: tin + 0.55, in: \"fade\", dur: 0.3, box: { x: hx, y: yH + bh, w: bw, h: th } }, leave),\n $.text(run, Object.assign({}, tf, { x: hx + 30 * k - off, y: yH + bh + (th - tf.size) / 2 - tf.size * 0.06, fill: \"#FFFFFF\", within: { x: -1e6, y: 0, w: 2e6, h: H } }))));\n }\n // The label block slams in with a jolt.\n const lh = stack ? 70 * k : bh;\n let lb = $.rect(x, yL, lw, lh, { fill: c.accent, shadow: \"md\" });\n if (p.live) lb += $.circle(x + 30 * k, yL + lh / 2, 9 * k, { fill: $.on(c.accent), op: 0.55 + 0.45 * $.wave(t, 1.1) });\n lb += $.text(label, Object.assign({}, lf, { x: x + (p.live ? 52 * k : 23 * k), y: yL + (lh - lf.size) / 2 - lf.size * 0.03, fill: $.on(c.accent) }));\n s += M.part(\"label\", Object.assign({ at: tin, in: \"slam\", intensity: 0.12, emphasis: [{ fx: \"shake\", at: tin + 0.08 }], box: { x: x, y: yL, w: lw, h: lh }, origin: \"left\" }, leave), lb);\n return s;\n}\n","libs":[]},{"id":"end-card","name":"End card","description":"An end screen, full frame: \"watch next\" slots pop in (for YouTube's end-screen elements or your own thumbnails), the subscribe button presses, the handle signs off.","category":"end-card","tags":["end screen","outro","watch next","youtube","subscribe","thumbnail"],"placement":"full","duration":{"default":10,"min":4,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"title":{"type":"text","label":"Heading","default":"Watch next","maxLength":28},"first":{"type":"text","label":"First slot","default":"How we edit a reel in 10 minutes","maxLength":48},"second":{"type":"text","label":"Second slot","default":"The hook formula","maxLength":48},"firstImage":{"type":"image","label":"First thumbnail","description":"a 16:9 picture (an empty slot when none)","default":""},"secondImage":{"type":"image","label":"Second thumbnail","default":""},"handle":{"type":"text","label":"Handle","default":"@monarcstudio","maxLength":30},"button":{"type":"text","label":"Button","default":"Subscribe","maxLength":14},"done":{"type":"text","label":"After the press","default":"Subscribed","maxLength":14}},"beats":{"in":0.1},"holdFromEnd":0.6,"parts":{"ground":"the brand's ground","title":"the heading","slots":"each watch-next slot","footer":"the button and handle"},"pack":"essentials","ref":"essentials/end-card","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n const title = $.case(p.title);\n const hf = fitFont($, title, \"display\", Math.min(110 * k, A.h * 0.1), A.w, { ls: -0.03 });\n const slotF = Object.assign({}, $.font(\"text\"), { size: 32 * k });\n // Two 16:9 slots side by side on wide frames, stacked on tall ones.\n const stack = !A.wide;\n const sw = stack ? Math.min(A.w, (A.h * (A.tall ? 0.62 : 0.72)) / 2 * (16 / 9) - 40 * k) : (A.w - 60 * k) / 2;\n const sh = (sw * 9) / 16;\n const labelH = slotF.size * 2.6;\n const headH = hf.size * 1.3;\n const footH = 130 * k;\n const gridH = stack ? 2 * (sh + labelH) + 30 * k : sh + labelH;\n const top = A.y + (A.h - headH - gridH - footH) / 2;\n const left = stack ? (W - sw) / 2 : A.x;\n\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n s += M.text(\"title\", Object.assign({ at: tin, in: \"rise\", distance: 26 }, hf, { x: W / 2, y: top, anchor: \"middle\" }), title);\n [[p.first, p.firstImage ? \"firstImage\" : \"\"], [p.second, p.secondImage ? \"secondImage\" : \"\"]].forEach(function (slot, i) {\n const sx = stack ? left : left + i * (sw + 60 * k);\n const sy = top + headH + (stack ? i * (sh + labelH + 30 * k) : 0);\n const ccx = sx + sw / 2, ccy = sy + sh / 2;\n let el = $.rect(sx, sy, sw, sh, { r: $.style.radius * 0.6, fill: $.rgba(c.fg, 0.06), stroke: $.rgba(c.fg, 0.22), sw: 3 * k, dash: slot[1] ? null : 14 * k + \" \" + 10 * k });\n if (slot[1]) el += $.image(slot[1], sx, sy, sw, sh, { r: $.style.radius * 0.6 });\n else {\n const pr = Math.min(sw, sh) * 0.14 * (1 + 0.06 * $.wave(t, 1.6, i * 0.3));\n el += $.circle(ccx, ccy, pr, { fill: c.accent, shadow: \"md\" }) + $.icon(\"play\", ccx - pr * 0.42, ccy - pr * 0.5, pr, { color: $.on(c.accent), fill: true });\n }\n $.wrap($.case(slot[0] || \"\"), sw, slotF).slice(0, 2).forEach(function (line, j) { el += $.text(line, Object.assign({}, slotF, { x: sx, y: sy + sh + 18 * k + j * slotF.size * 1.2 })); });\n s += M.part(\"slots\", { at: tin + 0.25 + i * 0.15, in: \"rise\", distance: 40, box: { x: sx, y: sy, w: sw, h: sh + labelH } }, el);\n });\n // The footer: the button presses, the handle beside it.\n const fy = top + headH + gridH + 40 * k;\n const press = tin + 1.3;\n const b1 = $.button.size($.case(p.button), { size: 34 * k, icon: \"bell\" }), b2 = $.button.size($.case(p.done), { size: 34 * k, icon: \"check\" });\n const btn = { w: Math.max(b1.w, b2.w), h: b1.h };\n const hfnt = Object.assign({}, $.font(\"mono\"), { size: 30 * k });\n const hw = p.handle ? $.measure(p.handle, hfnt) + 36 * k : 0;\n const fx = W / 2 - (btn.w + hw) / 2;\n s += M.part(\"footer\", { at: tin + 0.6, in: \"rise\", distance: 24, box: { x: fx, y: fy, w: btn.w + hw, h: btn.h } },\n $.button(t, $.case(p.button), fx, fy, { size: 34 * k, icon: \"bell\", pressAt: press, done: $.case(p.done) }),\n p.handle ? $.text(p.handle, Object.assign({}, hfnt, { x: fx + btn.w + 36 * k, y: fy + btn.h / 2 - hfnt.size * 0.55, fill: c.mute })) : \"\");\n return s;\n}\n","libs":[]},{"id":"logo-sting","name":"Logo sting","description":"A brand reveal, full frame: rings burst from the centre, the logo pops, the wordmark lands letter by letter with a light sweep, the tagline spaces out under it.","category":"logo","tags":["logo","intro","outro","brand","reveal","wordmark","sting","bumper"],"placement":"full","duration":{"default":3.5,"min":2,"max":10},"aspects":["16:9","9:16","1:1","4:5"],"props":{"wordmark":{"type":"text","label":"Wordmark","default":"MONARC","maxLength":20},"tagline":{"type":"text","label":"Tagline","default":"Made with motion","maxLength":40},"logo":{"type":"image","label":"Logo","description":"a square mark (PNG with alpha or SVG) above the wordmark; none for the wordmark alone","default":""},"burst":{"type":"boolean","label":"Burst","default":true}},"beats":{"in":0.1},"holdFromEnd":0.5,"parts":{"ground":"the brand's ground","rings":"the rings that burst out","logo":"the logo picture","wordmark":"the wordmark","tagline":"the tagline"},"pack":"essentials","ref":"essentials/logo-sting","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n const word = $.case(p.wordmark);\n const wf = fitFont($, word, \"display\", Math.min(220 * k, A.h * 0.26), A.w * 0.86, { ls: 0.02 });\n const tag = p.tagline ? String(p.tagline).toUpperCase() : \"\";\n const tfnt = fitFont($, tag, \"mono\", 30 * k, A.w * 0.8, { ls: 0.32 });\n const logo = p.logo ? Math.min(220 * k, A.h * 0.22) : 0;\n const total = logo + (logo ? 50 * k : 0) + wf.size + (tag ? 40 * k + tfnt.size : 0);\n const cx = W / 2, cy = H / 2;\n let y = cy - total / 2;\n const tWord = tin + (logo ? 0.55 : 0.3);\n const leave = { out: \"push\", intensity: 0.06 };\n\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [cx, cy] }));\n if (p.burst) {\n let rings = \"\";\n for (let i = 0; i < 3; i++) {\n const a = tin + i * 0.12;\n const q = $.E.out3($.seg(t, a, a + 0.9));\n if (q > 0 && q < 1) rings += $.circle(cx, cy, (60 + 700 * q) * k, { stroke: i === 1 ? c.accent2 : c.accent, sw: (14 - i * 3) * k * (1 - q), op: 1 - q });\n }\n rings += $.burst(t, tWord, cx, cy, { n: 18, r: Math.max(W, H) * 0.32, colors: [c.accent, c.accent2, c.fg], size: 10 * k });\n s += M.part(\"rings\", { at: tin, in: \"none\", out: \"fade\", vary: false }, rings);\n }\n if (logo) {\n s += M.part(\"logo\", Object.assign({ at: tin + 0.15, in: \"pop\", box: { x: cx - logo / 2, y: y, w: logo, h: logo } }, leave), $.image(\"logo\", cx - logo / 2, y, logo, logo, { fit: \"contain\" }));\n y += logo + 50 * k;\n }\n const ww = $.measure(word, wf);\n // The wordmark lands letter by letter (word by word in scripts that join), then a light sweeps across it.\n s += M.text(\"wordmark\", Object.assign({ at: tWord, in: \"rise\", split: \"letters\", distance: wf.size * 0.6 / k, ease: \"spring(2.4, 7)\", stagger: Math.min(0.06, 0.5 / Math.max(1, word.length)) }, leave, wf, { x: cx, y: y, anchor: \"middle\" }), word);\n const sweep = $.seg(t, tWord + 0.5, tWord + 1.2);\n if (sweep > 0 && sweep < 1) {\n const sx = cx - ww / 2 - 200 * k + (ww + 400 * k) * $.E.inOut2(sweep);\n s += $.g({ extra: 'style=\"mix-blend-mode:screen\"' }, $.clip(cx - ww / 2 - 20 * k, y - 10 * k, ww + 40 * k, wf.size * 1.1, 0,\n $.rect(sx - 60 * k, y - 20 * k, 120 * k, wf.size * 1.3, { fill: $.gradient([[0, \"#FFFFFF\", 0], [0.5, \"#FFFFFF\", 0.55], [1, \"#FFFFFF\", 0]], { angle: 90 }), transform: \"translate(\" + $.fx(sx) + \" \" + $.fx(y + wf.size / 2) + \") skewX(-18) translate(\" + $.fx(-sx) + \" \" + $.fx(-(y + wf.size / 2)) + \")\" })));\n }\n y += wf.size + 40 * k;\n if (tag) {\n // The tagline spaces out as it fades in.\n const tp = M.progress(\"tagline\", { at: tWord + 0.45, in: \"fade\", dur: 0.65, ease: \"out\" });\n s += M.text(\"tagline\", Object.assign({ at: tWord + 0.45, in: \"fade\", dur: 0.65, ease: \"out\" }, leave, tfnt, { x: cx, y: y, anchor: \"middle\", fill: c.mute, ls: 0.14 + 0.18 * Math.min(1, tp.in) }), tag);\n }\n return s;\n}\n","libs":[]},{"id":"wipe","name":"Brand wipe","description":"A transition in the brand's colours: panels sweep in to cover the frame at the midpoint — cut your clips there — and sweep out to reveal the next shot.","category":"transition","tags":["transition","wipe","swipe","cut","bars","circle","reveal"],"placement":"overlay","duration":{"default":1.2,"min":0.6,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"shape":{"type":"choice","label":"Shape","options":["bars","diagonal","circle"],"default":"bars"},"bars":{"type":"number","label":"Bars","default":5,"min":2,"max":12,"step":1,"when":{"prop":"shape","is":["bars"]}},"label":{"type":"text","label":"Word at the cut","description":"shown while the frame is covered (empty for none)","default":"","maxLength":24},"direction":{"type":"choice","label":"Direction","options":["right","left","up","down"],"default":"right"}},"parts":{"panels":"the panels that cover the frame","label":"the word at the cut"},"pack":"essentials","ref":"essentials/wipe","code":"// MONARC Essentials — helpers this pack's items share (prepended to each item's code). The layout\n// helpers (safeArea, fitFont, fitLines, landOn, widest) live in the motion kit.\n\n/** Words of `text` (with *marks*) laid into `lines` (the same words without marks), for $.sync / $.headline. */\nfunction markedRows(text, lines) {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const rows = [];\n let i = 0;\n for (let l = 0; l < lines.length; l++) {\n const n = String(lines[l]).split(/\\s+/).filter(Boolean).length;\n rows.push(words.slice(i, i + n).join(\" \"));\n i += n;\n }\n if (i < words.length && rows.length) rows[rows.length - 1] += \" \" + words.slice(i).join(\" \");\n return rows;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const mid = $.dur / 2;\n // 0 → 1 covering until the midpoint, 1 → 2 uncovering after it.\n const cover = $.E.inOut3($.seg(t, 0, mid));\n const uncover = $.E.inOut3($.seg(t, mid, $.dur));\n const colors = [c.accent, c.accent2, c.ink];\n const dir = p.direction;\n const horiz = dir === \"right\" || dir === \"left\";\n const sign = dir === \"right\" || dir === \"down\" ? 1 : -1;\n const L = horiz ? W : H;\n let s = \"\";\n if (p.shape === \"circle\") {\n const R = Math.hypot(W, H) * 0.56;\n const cx = W / 2, cy = H / 2;\n if (t < mid) s += $.circle(cx, cy, R * cover, { fill: c.accent }) + $.circle(cx, cy, R * Math.max(0, cover * 1.12 - 0.12), { fill: c.ink });\n else s += $.path(\"M0 0H\" + W + \"V\" + H + \"H0Z M\" + (cx + R * uncover) + \" \" + cy + \"A\" + R * uncover + \" \" + R * uncover + \" 0 1 0 \" + (cx - R * uncover) + \" \" + cy + \"A\" + R * uncover + \" \" + R * uncover + \" 0 1 0 \" + (cx + R * uncover) + \" \" + cy + \"Z\", { fill: c.ink, extra: 'fill-rule=\"evenodd\"' });\n } else if (p.shape === \"diagonal\") {\n const skew = 0.35 * (horiz ? H : W);\n const pos = t < mid ? -L - skew + (L + skew) * cover : (L + skew) * uncover;\n [[c.accent, -0.12], [c.accent2, -0.06], [c.ink, 0]].forEach(function (lay) {\n const a = pos + (t < mid ? lay[1] : -lay[1] * 0.5) * L;\n const d = horiz\n ? \"M\" + a + \" 0H\" + (a + L + skew) + \"L\" + (a + L) + \" \" + H + \"H\" + (a - skew) + \"Z\"\n : \"M0 \" + a + \"V\" + (a + L + skew) + \"L\" + W + \" \" + (a + L) + \"V\" + (a - skew) + \"Z\";\n s += $.path(d, { fill: lay[0], transform: sign < 0 ? (horiz ? \"translate(\" + W + \" 0) scale(-1 1)\" : \"translate(0 \" + H + \") scale(1 -1)\") : undefined });\n });\n } else {\n const n = Math.max(2, Math.round(p.bars));\n const band = (horiz ? H : W) / n;\n for (let i = 0; i < n; i++) {\n const lag = (i / (n - 1)) * 0.35;\n const ci = $.E.inOut3($.clamp(($.seg(t, 0, mid) - lag) / (1 - lag)));\n const ui = $.E.inOut3($.clamp(($.seg(t, mid, $.dur) - lag) / (1 - lag)));\n const len = L * (t < mid ? ci : 1 - ui);\n const start = t < mid ? (sign > 0 ? 0 : L - len) : sign > 0 ? L - len : 0;\n s += horiz ? $.rect(start, i * band - 1, len, band + 2, { fill: colors[i % colors.length] }) : $.rect(i * band - 1, start, band + 2, len, { fill: colors[i % colors.length] });\n }\n }\n // The panels are the transition; they move only when the creator says so.\n s = M.part(\"panels\", { in: \"none\", out: \"none\", vary: false }, s);\n if (p.label) {\n const lf = fitFont($, $.case(p.label), \"display\", 150 * k, W * 0.8, { ls: -0.03 });\n s += M.text(\"label\", Object.assign({ at: mid - mid * 0.5, in: \"pop\", outAt: mid + mid * 0.25, out: \"fade\", outDur: mid * 0.3 }, lf, { x: W / 2, y: H / 2 - lf.size * 0.55, anchor: \"middle\", fill: c.paper }), $.case(p.label));\n }\n return s;\n}\n\n/** Air as the wipe covers the frame, a soft drop at the cut (the midpoint), air going as it uncovers. */\nfunction sound($) {\n const mid = $.dur / 2;\n return {\n cues: [\n { at: Math.max(0, mid - 0.4), kind: \"whoosh\", dur: Math.min(0.45, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.5 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.45, $.dur - mid), level: 0.55, pitch: -3 },\n ],\n auto: true,\n };\n}\n","libs":[]}]},{"format":1,"id":"broadcast","name":"MONARC Broadcast","version":"1.0.0","description":"Lower thirds in five looks, a news ticker, a live badge, a score bug, a location pin and a countdown — broadcast graphics in any brand, any aspect.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/broadcast","style":"ember","items":["lt-bars","lt-reveal","lt-kicker","lt-pill","lt-news","ticker","live-badge","score-bug","location","countdown"],"entries":[{"id":"lt-bars","name":"Lower third — two bars","description":"A name bar wipes in from one side and a role bar in the accent from the other, the words rising inside their bars.","category":"lower-third","tags":["lower-third","overlay","bold","interview"],"placement":"overlay","duration":{"default":5,"min":2.5,"max":30},"props":{"name":{"type":"text","label":"Name","default":"Priya Sharma","maxLength":32},"role":{"type":"text","label":"Role","default":"Product designer","maxLength":40},"side":{"type":"choice","label":"Side","options":["left","right"],"default":"left"}},"parts":{"scrim":"the soft wash behind it","nameBar":"the bar behind the name","name":"the name","roleBar":"the accent bar behind the role","role":"the role"},"beats":{"in":0.15,"out":-0.45},"pack":"broadcast","ref":"broadcast/lt-bars","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.2 : 1);\n const tin = $.beat(\"in\", 0.15);\n const right = p.side === \"right\";\n const side = right ? \"right\" : \"left\", other = right ? \"left\" : \"right\";\n const name = $.case(p.name), role = String(p.role || \"\").toUpperCase();\n const tName = landOn($, p.name, tin);\n const tRole = landOn($, p.role, tName + 0.3, tName);\n const maxW = A.w * (A.tall ? 0.9 : 0.6);\n const nf = fitFont($, name, \"display\", 78 * k, maxW - 80 * k, { ls: -0.01 });\n const rf = fitFont($, role, \"mono\", 30 * k, maxW - 80 * k, { ls: 0.12 });\n const pad = 34 * k;\n const nw = $.measure(name, nf) + pad * 2, nh = nf.size * 1.34;\n const rw = role ? $.measure(role, rf) + pad * 1.6 : 0, rh = rf.size * 1.95;\n const y = A.tall ? H * 0.6 : A.square ? H * 0.66 : H * 0.7;\n const x = right ? W - A.x - nw : A.x;\n const inset = 36 * k;\n const rx = right ? W - A.x - inset - rw : A.x + inset;\n const leave = { out: \"wipe\", outFrom: side, outDur: 0.35 };\n let s = M.part(\"scrim\", { at: tin - 0.1, in: \"fade\", dur: 0.4, out: \"fade\", vary: false }, $.scrim(0, y - 240 * k, W, H - y + 240 * k, { to: \"bottom\", a: 0.4 }));\n s += M.part(\"nameBar\", Object.assign({ at: tin, in: \"wipe\", from: side, dur: 0.45, ease: \"expo\", box: { x: x, y: y, w: nw, h: nh } }, leave), $.rect(x, y, nw, nh, { fill: c.surface, shadow: \"md\" }));\n s += $.clip(x, y, nw, nh, 0, M.text(\"name\", Object.assign({ at: tName + 0.18, in: \"rise\", distance: 40 }, leave, nf, { x: right ? x + nw - pad : x + pad, y: y + (nh - nf.size) / 2 - nf.size * 0.02, anchor: right ? \"end\" : \"start\", fill: c.fg }), name));\n if (role) {\n s += M.part(\"roleBar\", Object.assign({ at: tRole, in: \"wipe\", from: other, dur: 0.4, ease: \"expo\", box: { x: rx, y: y + nh, w: rw, h: rh } }, leave), $.rect(rx, y + nh, rw, rh, { fill: c.accent }));\n s += $.clip(rx, y + nh, rw, rh, 0, M.text(\"role\", Object.assign({ at: tRole + 0.15, in: \"rise\", distance: 24 }, leave, rf, { x: rx + rw / 2, y: y + nh + (rh - rf.size) / 2 - rf.size * 0.05, anchor: \"middle\", fill: $.on(c.accent) }), role));\n }\n return s;\n}\n","libs":[]},{"id":"lt-reveal","name":"Lower third — mask reveal","description":"Cardless: an accent sweep crosses the frame and leaves the name behind it, then the role fades up. Made for footage.","category":"lower-third","tags":["lower-third","overlay","cardless","minimal"],"placement":"overlay","duration":{"default":5,"min":2.5,"max":30},"props":{"name":{"type":"text","label":"Name","default":"Maya Chen","maxLength":32},"role":{"type":"text","label":"Role","default":"Documentary editor","maxLength":44},"side":{"type":"choice","label":"Side","options":["left","right"],"default":"left"}},"parts":{"scrim":"the soft wash behind it","sweep":"the accent bar that crosses","name":"the name","role":"the role"},"beats":{"in":0.15,"out":-0.45},"pack":"broadcast","ref":"broadcast/lt-reveal","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.2 : 1);\n const tin = landOn($, p.name, $.beat(\"in\", 0.15));\n const right = p.side === \"right\";\n const name = $.case(p.name), role = String(p.role || \"\");\n const maxW = A.w * (A.tall ? 0.92 : 0.66);\n const nf = fitFont($, name, \"display\", 104 * k, maxW, { ls: -0.02 });\n const rf = fitFont($, role, \"text\", 36 * k, maxW);\n const nw = $.measure(name, nf);\n const y = A.tall ? H * 0.6 : A.square ? H * 0.64 : H * 0.68;\n const x = right ? W - A.x - nw : A.x;\n // The sweep runs across the name; the name shows behind its trailing edge.\n const sw = Math.max(26 * k, nf.size * 0.32), sh = nf.size * 1.16;\n const run = $.E.inOut3($.seg(t, tin, tin + 0.7));\n const lead = right ? x + nw + sw - (nw + sw * 2) * run : x - sw + (nw + sw * 2) * run;\n const shown = right ? { x: lead, w: x + nw + 20 * k - lead } : { x: x - 20 * k, w: lead - x + 20 * k };\n let s = M.part(\"scrim\", { at: tin - 0.1, in: \"fade\", dur: 0.45, out: \"fade\", vary: false }, $.scrim(0, y - 280 * k, W, H - y + 280 * k, { to: \"bottom\", a: 0.55 }));\n if (t >= tin && shown.w > 0) s += $.clip(shown.x, y - sh, Math.max(0, shown.w), sh * 2.2, 0, M.text(\"name\", { at: tin, in: \"none\", out: \"fade\", ...nf, x: x, y: y, fill: \"#FFFFFF\", shadow: \"md\" }, name));\n if (run > 0 && run < 1) s += M.part(\"sweep\", { at: tin, in: \"none\", out: \"none\", vary: false }, $.rect(right ? lead : lead - sw, y - sh * 0.08, sw, sh, { fill: c.accent }));\n if (role) s += M.text(\"role\", { at: tin + 0.55, in: \"rise\", distance: 18, ...rf, x: right ? x + nw : x, y: y + nf.size * 1.12, anchor: right ? \"end\" : \"start\", fill: \"rgba(255,255,255,0.82)\", shadow: \"sm\" }, role);\n return s;\n}\n","libs":[]},{"id":"lt-kicker","name":"Lower third — kicker","description":"A kicker tag in the accent, a heavy name and a baseline that draws under it, the role beneath. Cardless, for footage.","category":"lower-third","tags":["lower-third","overlay","podcast","bold"],"placement":"overlay","duration":{"default":5,"min":2.5,"max":30},"props":{"kicker":{"type":"text","label":"Kicker","default":"Guest","maxLength":18},"name":{"type":"text","label":"Name","default":"Jordan Okafor","maxLength":30},"role":{"type":"text","label":"Role","default":"Founder, Northwind Studio","maxLength":44},"side":{"type":"choice","label":"Side","options":["left","right"],"default":"left"}},"parts":{"scrim":"the soft wash behind it","kicker":"the tag above the name","name":"the name","rule":"the baseline","role":"the role"},"beats":{"in":0.15,"out":-0.45},"pack":"broadcast","ref":"broadcast/lt-kicker","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.2 : 1);\n const tin = $.beat(\"in\", 0.15);\n const right = p.side === \"right\";\n const side = right ? \"right\" : \"left\";\n const anchor = right ? \"end\" : \"start\";\n const name = $.case(p.name), role = String(p.role || \"\"), kick = String(p.kicker || \"\").toUpperCase();\n const tName = landOn($, p.name, tin + 0.15);\n const maxW = A.w * (A.tall ? 0.92 : 0.62);\n const nf = fitFont($, name, \"display\", 92 * k, maxW, { ls: -0.02 });\n const rf = fitFont($, role, \"text\", 34 * k, maxW);\n const nw = $.measure(name, nf);\n const kf = { ...$.font(\"mono\"), size: 26 * k, ls: 0.14 };\n const chip = kick ? { w: $.measure(kick, kf) + 30 * k, h: kf.size * 1.75 } : { w: 0, h: 0 };\n const total = chip.h + 18 * k + nf.size * 1.05 + 22 * k + rf.size * 1.3;\n const y = (A.tall ? H * 0.62 : A.square ? H * 0.68 : H * 0.72) - total / 2 + 40 * k;\n const x = right ? W - A.x : A.x;\n const leave = { out: \"slide\", outFrom: side, distance: 24 };\n let s = M.part(\"scrim\", { at: tin - 0.1, in: \"fade\", dur: 0.45, out: \"fade\", vary: false }, $.scrim(0, y - 220 * k, W, H - y + 220 * k, { to: \"bottom\", a: 0.55 }));\n let yy = y;\n if (kick) {\n const cx = right ? x - chip.w : x;\n s += M.part(\"kicker\", Object.assign({ at: tin, in: \"pop\", origin: side, box: { x: cx, y: yy, w: chip.w, h: chip.h } }, leave), $.rect(cx, yy, chip.w, chip.h, { r: chip.h * 0.18, fill: c.accent }) + $.text(kick, { ...kf, x: cx + chip.w / 2, y: yy + (chip.h - kf.size) / 2 - kf.size * 0.06, anchor: \"middle\", fill: $.on(c.accent) }));\n yy += chip.h + 18 * k;\n }\n s += M.text(\"name\", Object.assign({ at: tName, in: \"rise\", distance: 30, split: \"words\", stagger: 0.07 }, leave, nf, { x: x, y: yy, anchor: anchor, fill: \"#FFFFFF\", shadow: \"md\" }), name);\n yy += nf.size * 1.05;\n const rx = right ? x - nw : x;\n s += M.part(\"rule\", Object.assign({ at: tName + 0.3, in: \"grow-x\", origin: side, dur: 0.45, ease: \"expo\", box: { x: rx, y: yy, w: nw, h: 6 * k } }, leave), $.rect(rx, yy, nw, 6 * k, { r: 3 * k, fill: c.accent }));\n yy += 22 * k;\n if (role) s += M.text(\"role\", Object.assign({ at: tName + 0.5, in: \"fade\", dur: 0.4 }, leave, rf, { x: x, y: yy, anchor: anchor, fill: \"rgba(255,255,255,0.85)\", shadow: \"sm\" }), role);\n return s;\n}\n","libs":[]},{"id":"lt-pill","name":"Lower third — pill","description":"A rounded pill with a picture or initials, a status dot, the name and the role. Pops in; good for podcasts and calls.","category":"lower-third","tags":["lower-third","overlay","podcast","minimal","avatar"],"placement":"overlay","duration":{"default":5,"min":2.5,"max":30},"props":{"name":{"type":"text","label":"Name","default":"Alex Rivera","maxLength":28},"role":{"type":"text","label":"Role","default":"Host · The Long Cut","maxLength":40},"photo":{"type":"image","label":"Photo","description":"A square picture of the person; initials when empty"},"status":{"type":"choice","label":"Status dot","options":["live","online","none"],"default":"live"},"side":{"type":"choice","label":"Side","options":["left","right"],"default":"left"}},"parts":{"pill":"the pill","avatar":"the picture or initials","dot":"the status dot","name":"the name","role":"the role"},"beats":{"in":0.15,"out":-0.45},"pack":"broadcast","ref":"broadcast/lt-pill","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.3 : 1.15);\n const tin = landOn($, p.name, $.beat(\"in\", 0.15));\n const right = p.side === \"right\";\n const side = right ? \"right\" : \"left\";\n const name = $.case(p.name), role = String(p.role || \"\");\n const maxW = A.w * (A.tall ? 0.92 : 0.5);\n const h = 128 * k, r = h / 2, av = h * 0.38;\n const nf = fitFont($, name, \"display\", 48 * k, maxW - h - 60 * k);\n const rf = fitFont($, role, \"text\", 30 * k, maxW - h - 60 * k);\n const w = h + Math.max($.measure(name, nf), role ? $.measure(role, rf) : 0) + 56 * k;\n const y = A.tall ? H * 0.62 : A.square ? H * 0.7 : H * 0.74;\n const x = right ? W - A.x - w : A.x;\n const ax = x + r, ay = y + r;\n const dotCol = p.status === \"live\" ? c.red : p.status === \"online\" ? c.green : null;\n const leave = { out: \"pop\" };\n let s = M.part(\"pill\", Object.assign({ at: tin, in: \"pop\", origin: side, box: { x: x, y: y, w: w, h: h } }, leave), $.rect(x, y, w, h, { r: r, fill: c.surface, stroke: c.line, sw: 2 * k, shadow: \"lg\" }));\n s += M.part(\"avatar\", Object.assign({ at: tin + 0.12, in: \"pop\", box: { x: ax - av, y: ay - av, w: av * 2, h: av * 2 } }, leave), avatar($, \"photo\", ax, ay, av, { name: p.name, fill: c.accent }));\n if (dotCol) s += M.part(\"dot\", Object.assign({ at: tin + 0.35, in: \"pop\", idle: p.status === \"live\" ? \"pulse\" : \"none\", box: { x: ax + av * 0.5, y: ay + av * 0.5, w: av * 0.6, h: av * 0.6 } }, leave), $.circle(ax + av * 0.72, ay + av * 0.72, av * 0.24, { fill: dotCol, stroke: c.surface, sw: 4 * k }));\n const tx = x + h + 4 * k;\n const ty = role ? y + h / 2 - (nf.size * 1.08 + rf.size) / 2 : y + (h - nf.size) / 2;\n s += M.text(\"name\", Object.assign({ at: tin + 0.18, in: \"rise\", distance: 14 }, leave, nf, { x: tx, y: ty, fill: c.fg }), name);\n if (role) s += M.text(\"role\", Object.assign({ at: tin + 0.3, in: \"fade\", dur: 0.35 }, leave, rf, { x: tx, y: ty + nf.size * 1.08, fill: c.mute }), role);\n return s;\n}\n","libs":[]},{"id":"lt-news","name":"Lower third — news boxes","description":"Broadcast style: a tight headline box with a hard accent shadow, a sub-line box beneath and an optional tag above.","category":"lower-third","tags":["lower-third","overlay","news","broadcast"],"placement":"overlay","duration":{"default":6,"min":3,"max":30},"props":{"tag":{"type":"text","label":"Tag","default":"Exclusive","maxLength":16},"headline":{"type":"text","label":"Headline","default":"Creators switch to AI editing","maxLength":48},"sub":{"type":"text","label":"Sub-line","default":"Studios report three times the output","maxLength":56}},"parts":{"tag":"the tag above","headline":"the headline box and its words","sub":"the sub-line box and its words"},"beats":{"in":0.15,"out":-0.45},"pack":"broadcast","ref":"broadcast/lt-news","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.2 : 1);\n const tin = landOn($, p.headline, $.beat(\"in\", 0.15));\n const head = $.case(p.headline), sub = String(p.sub || \"\"), tag = String(p.tag || \"\").toUpperCase();\n const maxW = A.w * (A.tall ? 0.94 : 0.72);\n const hf = fitFont($, head, \"display\", 64 * k, maxW - 50 * k, { ls: -0.01 });\n const sf = fitFont($, sub, \"text\", 38 * k, maxW - 40 * k);\n const tf = { ...$.font(\"mono\"), size: 28 * k, ls: 0.16 };\n const pad = 24 * k, sh = 9 * k;\n const hw = $.measure(head, hf) + pad * 2, hh = hf.size * 1.36;\n const sw = sub ? $.measure(sub, sf) + pad * 1.6 : 0, shh = sf.size * 1.6;\n const tw = tag ? $.measure(tag, tf) + pad * 1.4 : 0, th = tf.size * 1.8;\n const blockH = (tag ? th : 0) + hh + (sub ? shh : 0);\n const y0 = (A.tall ? H * 0.64 : A.square ? H * 0.7 : H * 0.74) - blockH / 2;\n const x = A.x;\n let y = y0;\n let s = \"\";\n if (tag) {\n s += M.part(\"tag\", { at: tin, in: \"pop\", origin: \"bottom-left\", out: \"fade\", box: { x: x, y: y, w: tw, h: th } }, $.rect(x, y, tw, th, { fill: c.ink }) + $.text(tag, { ...tf, x: x + tw / 2, y: y + (th - tf.size) / 2 - tf.size * 0.05, anchor: \"middle\", fill: \"#FFFFFF\" }));\n y += th;\n }\n s += M.part(\"headline\", { at: tin + 0.1, in: \"wipe\", from: \"left\", dur: 0.45, ease: \"expo\", out: \"wipe\", outFrom: \"right\", box: { x: x, y: y, w: hw + sh, h: hh + sh } },\n $.rect(x + sh, y + sh, hw, hh, { fill: c.accent }), $.rect(x, y, hw, hh, { fill: \"#FFFFFF\" }), $.text(head, { ...hf, x: x + pad, y: y + (hh - hf.size) / 2 - hf.size * 0.03, fill: \"#111318\" }));\n if (sub) s += M.part(\"sub\", { at: tin + 0.4, in: \"wipe\", from: \"left\", dur: 0.4, ease: \"expo\", out: \"wipe\", outFrom: \"right\", box: { x: x, y: y + hh, w: sw, h: shh } },\n $.rect(x, y + hh, sw, shh, { fill: c.accent }), $.text(sub, { ...sf, x: x + pad * 0.8, y: y + hh + (shh - sf.size) / 2 - sf.size * 0.06, fill: $.on(c.accent) }));\n return s;\n}\n","libs":[]},{"id":"ticker","name":"News ticker","description":"A crawl along the bottom: a label block in the accent and a line of items scrolling left without a break.","category":"banner","tags":["ticker","crawl","news","overlay","broadcast"],"placement":"overlay","duration":{"default":10,"min":3,"max":120},"props":{"label":{"type":"text","label":"Label","default":"Latest","maxLength":14},"items":{"type":"list","label":"Items","default":["Volito ships 96 graphics","3D text renders without a GPU","Agents preview a graphic in 0.2 s","Every pack works in any brand"],"maxItems":12,"of":"text"},"speed":{"type":"number","label":"Speed (px/s at 1080p)","default":170,"min":40,"max":600},"position":{"type":"choice","label":"Position","options":["bottom","top"],"default":"bottom"}},"parts":{"bar":"the strip","label":"the label block","items":"the scrolling words"},"beats":{"in":0.1,"out":-0.4},"pack":"broadcast","ref":"broadcast/ticker","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.15 : 1);\n const tin = $.beat(\"in\", 0.1);\n const bh = 74 * k;\n const top = p.position === \"top\";\n const y = top ? A.y * 0.6 : A.tall ? A.y + A.h - bh : H - bh - H * 0.05;\n const label = String(p.label || \"\").toUpperCase();\n const lf = { ...$.font(\"display\"), size: 32 * k, ls: 0.06 };\n const lw = label ? $.measure(label, lf) + 56 * k : 0;\n const tf = { ...$.font(\"text\"), size: 32 * k };\n const items = (p.items || []).map(function (s) { return String(s); }).filter(Boolean);\n const loop = items.length ? items.join(\" • \") + \" • \" : \"\";\n const loopW = loop ? $.measure(loop, tf) : 1;\n const leave = { out: \"slide\", outFrom: top ? \"up\" : \"down\", distance: 40 };\n let s = M.part(\"bar\", Object.assign({ at: tin, in: \"slide\", from: top ? \"up\" : \"down\", distance: 40, box: { x: 0, y: y, w: W, h: bh } }, leave), $.rect(0, y, W, bh, { fill: c.ink }), $.rect(0, top ? y + bh - 4 * k : y, W, 4 * k, { fill: c.accent }));\n // The crawl: copies of the loop, moving left at `speed` from when the bar lands.\n const run = Math.max(0, t - tin - 0.3) * p.speed * k;\n const off = run % loopW;\n let row = \"\";\n const x0 = lw + 30 * k - off;\n for (let i = 0; x0 + i * loopW < W + loopW && i < 40; i++) row += $.text(loop, { ...tf, x: x0 + i * loopW, y: y + (bh - tf.size) / 2 - tf.size * 0.04, fill: \"#FFFFFF\", within: { x: -1e6, y: 0, w: 2e6, h: H } });\n s += $.clip(lw, y, W - lw, bh, 0, M.part(\"items\", Object.assign({ at: tin + 0.3, in: \"fade\", dur: 0.4 }, leave), row));\n if (label) s += M.part(\"label\", Object.assign({ at: tin + 0.1, in: \"wipe\", from: \"left\", dur: 0.4, ease: \"expo\", box: { x: 0, y: y, w: lw, h: bh } }, leave), $.rect(0, y, lw, bh, { fill: c.accent }), $.text(label, { ...lf, x: lw / 2, y: y + (bh - lf.size) / 2 - lf.size * 0.03, anchor: \"middle\", fill: $.on(c.accent) }));\n return s;\n}\n","libs":[]},{"id":"live-badge","name":"Live badge","description":"A LIVE pill with a pulsing dot, a viewer count that counts up and keeps ticking, and an optional running clock.","category":"social","tags":["live","badge","stream","overlay","hud"],"placement":"overlay","duration":{"default":6,"min":2,"max":600},"props":{"label":{"type":"text","label":"Label","default":"Live","maxLength":10},"viewers":{"type":"number","label":"Viewers","default":12480,"min":0,"max":99999999},"clock":{"type":"boolean","label":"Running clock","default":true},"elapsed":{"type":"number","label":"Clock starts at (s)","default":2534,"min":0,"max":359999},"corner":{"type":"choice","label":"Corner","options":["top-left","top-right"],"default":"top-left"}},"parts":{"live":"the LIVE pill","viewers":"the viewer count","clock":"the running clock"},"beats":{"in":0.1,"out":-0.35},"pack":"broadcast","ref":"broadcast/live-badge","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.3 : 1.2);\n const tin = $.beat(\"in\", 0.1);\n const right = p.corner === \"top-right\";\n const h = 64 * k;\n const lf = { ...$.font(\"display\"), size: 30 * k, ls: 0.08 };\n const nf = { ...$.font(\"mono\"), size: 28 * k, weight: 700 };\n const label = String(p.label || \"LIVE\").toUpperCase();\n const liveW = h * 0.62 + $.measure(label, lf) + 30 * k;\n // Viewers: up to the number in 1.2 s, then a slow, fixed drift so the count keeps moving.\n const up = $.E.out3($.seg(t, tin + 0.3, tin + 1.5));\n const drift = Math.floor(Math.max(0, t - tin - 1.5) * 1.7);\n let v = Math.round(p.viewers * up);\n for (let i = 0; i < drift; i++) v += Math.floor($.hash(i * 7.31) * 9) - 2;\n const vText = $.count(Math.max(0, v));\n const vw = h * 0.95 + Math.max($.measure(\"88.8K\", nf), $.measure(vText, nf)) + 24 * k;\n const secs = Math.floor(p.elapsed + Math.max(0, t - tin));\n const two = function (n) { return (n < 10 ? \"0\" : \"\") + n; };\n const cText = Math.floor(secs / 3600) + \":\" + two(Math.floor(secs / 60) % 60) + \":\" + two(secs % 60);\n const cw = p.clock ? $.measure(\"00:00:00\", nf) + 40 * k : 0;\n const gap = 12 * k;\n const total = liveW + gap + vw + (p.clock ? gap + cw : 0);\n let x = right ? W - A.x - total : A.x;\n const y = A.y;\n const leave = { out: \"fade\", outDur: 0.3 };\n let s = M.part(\"live\", Object.assign({ at: tin, in: \"pop\", origin: right ? \"right\" : \"left\", box: { x: x, y: y, w: liveW, h: h } }, leave),\n $.rect(x, y, liveW, h, { r: 10 * k, fill: c.red, shadow: \"md\" }),\n $.circle(x + h * 0.42, y + h / 2, 9 * k, { fill: \"#FFFFFF\", op: 0.45 + 0.55 * $.wave(t, 1.1) }),\n $.text(label, { ...lf, x: x + h * 0.66, y: y + (h - lf.size) / 2 - lf.size * 0.03, fill: \"#FFFFFF\" }));\n x += liveW + gap;\n s += M.part(\"viewers\", Object.assign({ at: tin + 0.15, in: \"slide\", from: right ? \"right\" : \"left\", distance: 16, box: { x: x, y: y, w: vw, h: h } }, leave),\n $.rect(x, y, vw, h, { r: 10 * k, fill: \"rgba(10,12,16,0.72)\" }),\n $.icon(\"users\", x + 18 * k, y + (h - 30 * k) / 2, 30 * k, { color: \"#FFFFFF\", sw: 2.2 }),\n $.text(vText, { ...nf, x: x + h * 0.85, y: y + (h - nf.size) / 2 - nf.size * 0.05, fill: \"#FFFFFF\" }));\n x += vw + gap;\n if (p.clock) s += M.part(\"clock\", Object.assign({ at: tin + 0.3, in: \"slide\", from: right ? \"right\" : \"left\", distance: 16, box: { x: x, y: y, w: cw, h: h } }, leave),\n $.rect(x, y, cw, h, { r: 10 * k, fill: \"rgba(10,12,16,0.72)\" }),\n $.text(cText, { ...nf, x: x + cw / 2, y: y + (h - nf.size) / 2 - nf.size * 0.05, anchor: \"middle\", fill: \"#FFFFFF\" }));\n return s;\n}\n","libs":[]},{"id":"score-bug","name":"Score bug","description":"A sports score bug: two teams with their colours and scores, the period and a running clock. A goal adds a point with a flash.","category":"banner","tags":["sports","score","scoreboard","overlay","broadcast"],"placement":"overlay","duration":{"default":8,"min":2,"max":600},"props":{"home":{"type":"text","label":"Home team","default":"MON","maxLength":4},"away":{"type":"text","label":"Away team","default":"NWD","maxLength":4},"homeScore":{"type":"number","label":"Home score","default":2,"min":0,"max":999},"awayScore":{"type":"number","label":"Away score","default":1,"min":0,"max":999},"homeColor":{"type":"color","label":"Home colour","default":"#FF6A3D"},"awayColor":{"type":"color","label":"Away colour","default":"#3DD6FF"},"period":{"type":"text","label":"Period","default":"2nd","maxLength":6},"clock":{"type":"text","label":"Clock (mm:ss)","default":"67:12","maxLength":6},"clockRuns":{"type":"choice","label":"Clock","options":["up","down","stopped"],"default":"up"},"goal":{"type":"choice","label":"Goal","options":["none","home","away"],"default":"home"},"goalAt":{"type":"number","label":"Goal at (s)","default":3.5,"min":0,"max":600}},"parts":{"bug":"the board","home":"the home row","away":"the away row","clock":"the period and clock"},"beats":{"in":0.1,"out":-0.35},"pack":"broadcast","ref":"broadcast/score-bug","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.3 : 1.15);\n const tin = $.beat(\"in\", 0.1);\n const rowH = 56 * k, sw = 12 * k;\n const nf = { ...$.font(\"display\"), size: 30 * k, ls: 0.04 };\n const sf = { ...$.font(\"mono\"), size: 32 * k, weight: 700 };\n const teamW = Math.max($.measure(\"WWW\", nf), $.measure(String(p.home).toUpperCase(), nf), $.measure(String(p.away).toUpperCase(), nf)) + 30 * k;\n const scoreW = $.measure(\"888\", sf) + 24 * k;\n const w = sw + teamW + scoreW;\n const clockW = $.measure(\"888:88\", sf) + 30 * k;\n const x = A.x, y = A.y;\n const scored = p.goal !== \"none\" && t >= p.goalAt;\n const hs = p.homeScore + (scored && p.goal === \"home\" ? 1 : 0), as = p.awayScore + (scored && p.goal === \"away\" ? 1 : 0);\n const flash = p.goal !== \"none\" ? $.bump(t, p.goalAt, 0.05, 0.6) : 0;\n // The clock: mm:ss running up or down from its start, or held.\n const m = /^(\\d+):(\\d\\d)$/.exec(String(p.clock || \"0:00\"));\n let secs = m ? Number(m[1]) * 60 + Number(m[2]) : 0;\n const ran = Math.floor(Math.max(0, t - tin));\n secs = p.clockRuns === \"up\" ? secs + ran : p.clockRuns === \"down\" ? Math.max(0, secs - ran) : secs;\n const clock = Math.floor(secs / 60) + \":\" + (secs % 60 < 10 ? \"0\" : \"\") + (secs % 60);\n const leave = { out: \"slide\", outFrom: \"left\", distance: 30 };\n let s = M.part(\"bug\", Object.assign({ at: tin, in: \"wipe\", from: \"left\", dur: 0.4, ease: \"expo\", box: { x: x, y: y, w: w + clockW, h: rowH * 2 } }, leave),\n $.rect(x, y, w, rowH * 2, { fill: c.ink, shadow: \"md\" }), $.rect(x + w, y, clockW, rowH * 2, { fill: c.surface }));\n const row = function (name, team, score, col, ry, hit) {\n return M.part(name, Object.assign({ at: tin + (name === \"home\" ? 0.2 : 0.3), in: \"slide\", from: \"left\", distance: 20, box: { x: x, y: ry, w: w, h: rowH } }, leave),\n $.rect(x, ry, sw, rowH, { fill: col }),\n $.text(String(team).toUpperCase(), { ...nf, x: x + sw + 16 * k, y: ry + (rowH - nf.size) / 2 - nf.size * 0.03, fill: \"#FFFFFF\" }),\n hit > 0.01 ? $.rect(x + sw + teamW, ry, scoreW, rowH, { fill: col, op: hit * 0.9 }) : \"\",\n $.g({ transform: $.tf({ s: 1 + 0.35 * hit, ox: x + sw + teamW + scoreW / 2, oy: ry + rowH / 2 }) }, $.text(String(score), { ...sf, x: x + sw + teamW + scoreW / 2, y: ry + (rowH - sf.size) / 2 - sf.size * 0.05, anchor: \"middle\", fill: \"#FFFFFF\" })));\n };\n s += row(\"home\", p.home, hs, p.homeColor, y, p.goal === \"home\" ? flash : 0);\n s += row(\"away\", p.away, as, p.awayColor, y + rowH, p.goal === \"away\" ? flash : 0);\n s += M.part(\"clock\", Object.assign({ at: tin + 0.4, in: \"fade\", dur: 0.3, box: { x: x + w, y: y, w: clockW, h: rowH * 2 } }, leave),\n $.text(String(p.period || \"\").toUpperCase(), { ...$.font(\"mono\"), size: 22 * k, ls: 0.12, x: x + w + clockW / 2, y: y + rowH * 0.42, anchor: \"middle\", fill: c.mute }),\n $.text(clock, { ...sf, x: x + w + clockW / 2, y: y + rowH * 1.02, anchor: \"middle\", fill: c.fg }));\n return s;\n}\n","libs":[]},{"id":"location","name":"Location pin","description":"A map pin drops and bounces with a ripple, and a card beside it names the place, its region and its coordinates.","category":"callout","tags":["location","map","pin","place","overlay"],"placement":"overlay","duration":{"default":5,"min":2.5,"max":30},"props":{"place":{"type":"text","label":"Place","default":"Lisbon","maxLength":26},"region":{"type":"text","label":"Region","default":"Portugal","maxLength":32},"coords":{"type":"text","label":"Coordinates","default":"38.72° N, 9.14° W","maxLength":28},"x":{"type":"number","label":"Pin across (0–1)","default":0.3,"min":0.05,"max":0.95},"y":{"type":"number","label":"Pin down (0–1)","default":0.62,"min":0.1,"max":0.9}},"parts":{"pin":"the pin","ripple":"the ring under it","card":"the place card"},"beats":{"in":0.1,"out":-0.4},"pack":"broadcast","ref":"broadcast/location","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.2 : 1);\n const tin = landOn($, p.place, $.beat(\"in\", 0.1));\n const px = W * p.x, py = Math.max(A.y + 140 * k, Math.min(A.y + A.h, H * p.y));\n const pw = 64 * k, ph = 88 * k;\n // A teardrop pin whose point sits on (px, py).\n const pin = \"M\" + $.fx(px) + \" \" + $.fx(py) + \" C\" + $.fx(px - pw * 0.2) + \" \" + $.fx(py - ph * 0.35) + \" \" + $.fx(px - pw / 2) + \" \" + $.fx(py - ph * 0.5) + \" \" + $.fx(px - pw / 2) + \" \" + $.fx(py - ph * 0.68) + \" A\" + $.fx(pw / 2) + \" \" + $.fx(pw / 2) + \" 0 1 1 \" + $.fx(px + pw / 2) + \" \" + $.fx(py - ph * 0.68) + \" C\" + $.fx(px + pw / 2) + \" \" + $.fx(py - ph * 0.5) + \" \" + $.fx(px + pw * 0.2) + \" \" + $.fx(py - ph * 0.35) + \" \" + $.fx(px) + \" \" + $.fx(py) + \"Z\";\n const ring = $.seg(t, tin + 0.45, tin + 1.6);\n let s = \"\";\n s += M.part(\"ripple\", { at: tin + 0.4, in: \"fade\", dur: 0.1, out: \"fade\", box: { x: px - 90 * k, y: py - 30 * k, w: 180 * k, h: 60 * k } },\n $.path(\"M\" + $.fx(px - 60 * k) + \" \" + $.fx(py) + \"a\" + $.fx(60 * k) + \" \" + $.fx(18 * k) + \" 0 1 0 \" + $.fx(120 * k) + \" 0a\" + $.fx(60 * k) + \" \" + $.fx(18 * k) + \" 0 1 0 \" + $.fx(-120 * k) + \" 0\", { fill: \"rgba(0,0,0,0.35)\" }),\n ring > 0 && ring < 1 ? $.path(\"M\" + $.fx(px - 60 * k * (0.4 + ring)) + \" \" + $.fx(py) + \"a\" + $.fx(60 * k * (0.4 + ring)) + \" \" + $.fx(18 * k * (0.4 + ring)) + \" 0 1 0 \" + $.fx(120 * k * (0.4 + ring)) + \" 0a\" + $.fx(60 * k * (0.4 + ring)) + \" \" + $.fx(18 * k * (0.4 + ring)) + \" 0 1 0 \" + $.fx(-120 * k * (0.4 + ring)) + \" 0\", { fill: \"none\", stroke: c.accent, sw: 4 * k * (1 - ring) }) : \"\");\n s += M.part(\"pin\", { at: tin, in: \"bounce\", origin: \"bottom\", out: \"rise\", box: { x: px - pw / 2, y: py - ph, w: pw, h: ph } },\n $.path(pin, { fill: c.accent, stroke: \"rgba(0,0,0,0.25)\", sw: 2 * k }), $.circle(px, py - ph * 0.68, pw * 0.2, { fill: \"#FFFFFF\" }));\n // The card sits on the side with more room.\n const left = p.x > 0.55;\n const nf = fitFont($, $.case(p.place), \"display\", 54 * k, A.w * 0.5);\n const rf = { ...$.font(\"text\"), size: 30 * k };\n const cf = { ...$.font(\"mono\"), size: 22 * k, ls: 0.06 };\n const cw = Math.max($.measure($.case(p.place), nf), $.measure(String(p.region || \"\"), rf), $.measure(String(p.coords || \"\"), cf)) + 64 * k;\n const ch = nf.size * 1.2 + (p.region ? rf.size * 1.35 : 0) + (p.coords ? cf.size * 1.6 : 0) + 44 * k;\n const cx = left ? Math.max(A.x, px - pw - 30 * k - cw) : Math.min(W - A.x - cw, px + pw + 30 * k);\n const cy = Math.max(A.y, py - ph - ch * 0.3);\n let yy = cy + 22 * k;\n let card = $.card(cx, cy, cw, ch, { shadow: \"lg\" });\n card += $.text($.case(p.place), { ...nf, x: cx + 32 * k, y: yy, fill: c.fg });\n yy += nf.size * 1.2;\n if (p.region) { card += $.text(String(p.region), { ...rf, x: cx + 32 * k, y: yy, fill: c.mute }); yy += rf.size * 1.35; }\n if (p.coords) card += $.text(String(p.coords), { ...cf, x: cx + 32 * k, y: yy + 4 * k, fill: c.accent });\n s += M.part(\"card\", { at: tin + 0.55, in: \"pop\", origin: left ? \"right\" : \"left\", out: \"fade\", box: { x: cx, y: cy, w: cw, h: ch } }, card);\n return s;\n}\n","libs":[]},{"id":"countdown","name":"Countdown","description":"A big number counting down second by second inside a ring that empties, then the closing words pop.","category":"timer","tags":["countdown","timer","start","overlay"],"placement":"overlay","duration":{"default":6,"min":2,"max":120},"props":{"label":{"type":"text","label":"Label","default":"Starting in","maxLength":24},"from":{"type":"number","label":"Count from","default":5,"min":1,"max":99},"end":{"type":"text","label":"Closing words","default":"Let's go","maxLength":16}},"parts":{"ring":"the ring","number":"the number","label":"the label","end":"the closing words"},"beats":{"in":0.1,"out":-0.35},"pack":"broadcast","ref":"broadcast/countdown","code":"function frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.1);\n const n = Math.max(1, Math.round(p.from));\n // The count fills the time before the closing words (the last ~0.9 s).\n const span = Math.max(0.5, $.dur - tin - 1.2);\n const step = span / n;\n const el = t - tin;\n const left = Math.max(0, n - Math.floor(el / step));\n const done = el >= span;\n const cx = W / 2, cy = A.y + A.h * (A.tall ? 0.45 : 0.5);\n const r = Math.min(A.w, A.h) * 0.24;\n const tick = el >= 0 && !done ? (el % step) / step : 0;\n let s = M.part(\"ring\", { at: tin, in: \"grow\", dur: 0.5, out: \"fade\", box: { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r } },\n $.circle(cx, cy, r, { fill: \"rgba(10,12,16,0.55)\" }),\n $.ring(cx, cy, r * 0.92, done ? 0 : 1 - (el + 0) / span, { sw: r * 0.09, color: c.accent }));\n const label = $.case(String(p.label || \"\"));\n if (label) s += M.text(\"label\", { at: tin + 0.15, in: \"rise\", distance: 16, out: \"fade\", ...fitFont($, label, \"text\", 40 * k, A.w), x: cx, y: cy - r - 76 * k, anchor: \"middle\", fill: \"#FFFFFF\", shadow: \"md\" }, label);\n if (!done) {\n // Each number pops in on its second.\n const pop = 1 + 0.25 * Math.exp(-9 * tick * step) * Math.cos(tick * step * 14);\n const nf = { ...$.font(\"display\"), size: r * 0.95 };\n s += M.part(\"number\", { at: tin + 0.1, in: \"fade\", dur: 0.2, out: \"none\", box: { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r } },\n $.g({ transform: $.tf({ s: pop, ox: cx, oy: cy }) }, $.text(String(left), { ...nf, x: cx, y: cy - nf.size * 0.52, anchor: \"middle\", fill: \"#FFFFFF\" })));\n } else {\n const end = $.case(String(p.end || \"\"));\n const ef = fitFont($, end, \"display\", r * 0.46, r * 1.45);\n s += M.text(\"end\", { at: tin + span, in: \"slam\", intensity: 0.4, out: \"fade\", ...ef, x: cx, y: cy - ef.size * 0.55, anchor: \"middle\", fill: \"#FFFFFF\" }, end);\n }\n return s;\n}\n","libs":[]}]},{"format":1,"id":"social","name":"MONARC Social","version":"1.0.0","description":"Follow cards, notifications, comments, chat threads, posts, now playing and ratings — the social and app UI that videos borrow, in any brand, any aspect.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/social","style":"studio","shared":"shared.js","items":["follow-card","notification","notifications","comments","chat","post","now-playing","rating"],"entries":[{"id":"follow-card","name":"Follow card","description":"A profile card: picture or initials, name with a verified check, handle, a follower count that counts up, a line of bio and a Follow button a cursor presses.","category":"social","tags":["social","follow","profile","cta","overlay"],"placement":"overlay","duration":{"default":5,"min":3,"max":30},"props":{"name":{"type":"text","label":"Name","default":"MONARC Studio","maxLength":26},"handle":{"type":"text","label":"Handle","default":"@monarcstudio","maxLength":28},"followers":{"type":"number","label":"Followers","default":248000,"min":0,"max":999999999},"bio":{"type":"text","label":"Bio","default":"Edits that feel directed, not assembled.","maxLength":60},"platform":{"type":"choice","label":"Platform","options":["instagram","tiktok","youtube","x","linkedin","threads","website"],"default":"instagram"},"photo":{"type":"image","label":"Photo"},"verified":{"type":"boolean","label":"Verified","default":true},"action":{"type":"text","label":"Button","default":"Follow","maxLength":14},"done":{"type":"text","label":"Button after","default":"Following","maxLength":14},"position":{"type":"choice","label":"Position","options":["auto","center","bottom-left","bottom-right"],"default":"auto"}},"parts":{"card":"the card","avatar":"the picture","name":"the name and handle","stats":"the follower count","bio":"the bio","button":"the button","cursor":"the cursor"},"beats":{"in":0.15,"out":-0.45},"pack":"social","ref":"social/follow-card","code":"// MONARC Social — helpers this pack's items share.\n\n/** A platform's icon and how it names its numbers (the platforms' own marks are theirs; these are plain icons). */\nvar PLATFORM = {\n instagram: { icon: \"camera\", line: \"Instagram\", count: \"followers\" },\n tiktok: { icon: \"reel\", line: \"TikTok\", count: \"followers\" },\n youtube: { icon: \"play\", line: \"YouTube\", count: \"subscribers\" },\n x: { icon: \"x\", line: \"X\", count: \"followers\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\", count: \"followers\" },\n threads: { icon: \"chat\", line: \"Threads\", count: \"followers\" },\n website: { icon: \"globe\", line: \"Website\", count: \"readers\" },\n};\n\n/** A small round check in the accent: verified. */\nfunction verified($, cx, cy, r) {\n return $.circle(cx, cy, r, { fill: $.c.accent }) + $.path(\"M\" + $.fx(cx - r * 0.45) + \" \" + $.fx(cy + r * 0.02) + \"L\" + $.fx(cx - r * 0.1) + \" \" + $.fx(cy + r * 0.36) + \"L\" + $.fx(cx + r * 0.48) + \" \" + $.fx(cy - r * 0.32), { stroke: $.on($.c.accent), sw: r * 0.28 });\n}\n\n/** A colour for a name, from the brand's two accents and fixed hues — the same name, the same colour. */\nfunction nameColor($, name) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\"];\n var h = 0, s = String(name || \"\");\n for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;\n return list[h % list.length];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.wide ? 1.15 : A.tall ? 1.35 : 1.15);\n const tin = $.beat(\"in\", 0.15);\n const pl = PLATFORM[p.platform] || PLATFORM.instagram;\n const cw = Math.min(A.w, 620 * k), pad = 40 * k, av = 62 * k;\n const nf = fitFont($, p.name, \"display\", 42 * k, cw - pad * 2 - av * 2 - 70 * k);\n const hf = { ...$.font(\"mono\"), size: 24 * k };\n const bf = fitFont($, p.bio, \"text\", 28 * k, cw - pad * 2);\n const btn = { size: 30 * k };\n const bh = $.button.size($.case(p.action), btn).h;\n const ch = pad + av * 2 + 28 * k + 36 * k * 1.4 + (p.bio ? bf.size * 1.5 : 0) + 24 * k + bh + pad;\n const pos = p.position === \"auto\" ? (A.wide ? \"bottom-right\" : \"center\") : p.position;\n const x = pos === \"center\" ? (W - cw) / 2 : pos === \"bottom-left\" ? A.x : W - A.x - cw;\n const y = pos === \"center\" ? A.y + (A.h - ch) * (A.tall ? 0.62 : 0.5) : A.y + A.h - ch;\n const press = landOn($, p.action, Math.min(tin + 1.5, $.beat(\"out\", $.dur - 0.45) - 0.9), tin);\n const leave = { out: \"rise\", distance: 40 };\n let s = M.part(\"card\", Object.assign({ at: tin, in: \"rise\", distance: 70, box: { x: x, y: y, w: cw, h: ch } }, leave), $.card(x, y, cw, ch, { shadow: \"lg\" }));\n const ax = x + pad + av, ay = y + pad + av;\n s += M.part(\"avatar\", Object.assign({ at: tin + 0.1, in: \"pop\", box: { x: ax - av, y: ay - av, w: av * 2, h: av * 2 } }, leave), avatar($, \"photo\", ax, ay, av, { name: p.name, fill: c.accent, ring: c.surface }));\n // The platform's icon sits on the picture's edge.\n s += M.part(\"avatar\", Object.assign({ at: tin + 0.25, in: \"pop\", box: { x: ax + av * 0.4, y: ay + av * 0.4, w: av * 0.8, h: av * 0.8 } }, leave), $.circle(ax + av * 0.72, ay + av * 0.72, av * 0.34, { fill: c.surface }), $.icon(pl.icon, ax + av * 0.72 - av * 0.2, ay + av * 0.72 - av * 0.2, av * 0.4, { color: c.fg, sw: 2.2 }));\n const tx = ax + av + 26 * k;\n const nw = $.measure(p.name, nf);\n s += M.part(\"name\", Object.assign({ at: tin + 0.2, in: \"rise\", distance: 16, box: { x: tx, y: ay - av * 0.75, w: cw - (tx - x) - pad, h: av * 1.4 } }, leave),\n $.text(p.name, { ...nf, x: tx, y: ay - nf.size * 0.95, fill: c.fg }),\n p.verified ? verified($, tx + nw + nf.size * 0.5, ay - nf.size * 0.42, nf.size * 0.36) : \"\",\n $.text(String(p.handle || \"\"), { ...hf, x: tx, y: ay + 8 * k, fill: c.mute }));\n let yy = y + pad + av * 2 + 28 * k;\n // The count on its own row: a big number and what it counts.\n const count = $.counter(t, tin + 0.4, tin + 1.6, p.followers, { short: true });\n const cf = { ...$.font(\"display\"), size: 36 * k };\n s += M.part(\"stats\", Object.assign({ at: tin + 0.35, in: \"fade\", dur: 0.3, box: { x: x + pad, y: yy, w: cw - pad * 2, h: cf.size * 1.2 } }, leave),\n $.text(count, { ...cf, x: x + pad, y: yy, fill: c.fg }),\n $.text(pl.count, { ...$.font(\"text\"), size: 26 * k, x: x + pad + $.measure($.count(p.followers), cf) + 14 * k, y: yy + cf.size * 0.22, fill: c.mute }));\n yy += cf.size * 1.4;\n if (p.bio) {\n s += M.text(\"bio\", Object.assign({ at: tin + 0.45, in: \"fade\", dur: 0.35 }, leave, bf, { x: x + pad, y: yy, fill: c.fg }), String(p.bio));\n yy += bf.size * 1.5;\n }\n yy += 24 * k;\n s += M.part(\"button\", Object.assign({ at: tin + 0.55, in: \"rise\", distance: 14, box: { x: x + pad, y: yy, w: cw - pad * 2, h: bh } }, leave),\n $.button(t, $.case(p.action), x + cw / 2, yy, { size: 30 * k, anchor: \"middle\", pressAt: press, done: $.case(p.done) }));\n if (t > press - 0.8 && t < press + 0.9) {\n const g = $.E.inOut3($.seg(t, press - 0.75, press - 0.04));\n const px = $.lerp(x + cw + 160 * k, x + cw * 0.55, g), py = $.lerp(Math.min(H, y + ch + 200 * k), yy + bh * 0.6, g);\n s += M.part(\"cursor\", { at: press - 0.8, in: \"fade\", dur: 0.2, outAt: press + 0.6, out: \"fade\", outDur: 0.3 }, $.cursor(px, py, { press: $.bump(t, press, 0.05, 0.2), scale: 2 * k }));\n }\n return s;\n}\n","libs":[]},{"id":"notification","name":"Notification","description":"A phone or desktop notification slides in: the app's icon and name, the time, a bold title and the message.","category":"social","tags":["notification","phone","ui","overlay","alert"],"placement":"overlay","duration":{"default":4.5,"min":2,"max":30},"props":{"app":{"type":"text","label":"App","default":"MONARC","maxLength":18},"icon":{"type":"icon","label":"Icon","default":"sparkle"},"title":{"type":"text","label":"Title","default":"Your cut is ready","maxLength":40},"message":{"type":"text","label":"Message","default":"Three versions exported in 4K. Tap to review before you post.","maxLength":110},"time":{"type":"text","label":"Time","default":"now","maxLength":10},"look":{"type":"choice","label":"Look","options":["phone","desktop"],"default":"phone"}},"parts":{"banner":"the notification"},"beats":{"in":0.15,"out":-0.45},"pack":"social","ref":"social/notification","code":"// MONARC Social — helpers this pack's items share.\n\n/** A platform's icon and how it names its numbers (the platforms' own marks are theirs; these are plain icons). */\nvar PLATFORM = {\n instagram: { icon: \"camera\", line: \"Instagram\", count: \"followers\" },\n tiktok: { icon: \"reel\", line: \"TikTok\", count: \"followers\" },\n youtube: { icon: \"play\", line: \"YouTube\", count: \"subscribers\" },\n x: { icon: \"x\", line: \"X\", count: \"followers\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\", count: \"followers\" },\n threads: { icon: \"chat\", line: \"Threads\", count: \"followers\" },\n website: { icon: \"globe\", line: \"Website\", count: \"readers\" },\n};\n\n/** A small round check in the accent: verified. */\nfunction verified($, cx, cy, r) {\n return $.circle(cx, cy, r, { fill: $.c.accent }) + $.path(\"M\" + $.fx(cx - r * 0.45) + \" \" + $.fx(cy + r * 0.02) + \"L\" + $.fx(cx - r * 0.1) + \" \" + $.fx(cy + r * 0.36) + \"L\" + $.fx(cx + r * 0.48) + \" \" + $.fx(cy - r * 0.32), { stroke: $.on($.c.accent), sw: r * 0.28 });\n}\n\n/** A colour for a name, from the brand's two accents and fixed hues — the same name, the same colour. */\nfunction nameColor($, name) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\"];\n var h = 0, s = String(name || \"\");\n for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;\n return list[h % list.length];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.3 : 1.15);\n const tin = landOn($, p.title, $.beat(\"in\", 0.15));\n const desk = p.look === \"desktop\";\n const w = desk ? Math.min(A.w * 0.42, 720 * k) : Math.min(A.w, A.wide ? 760 * k : A.w);\n const pad = 26 * k, ic = 64 * k;\n const af = { ...$.font(\"text\"), size: 22 * k, ls: 0.06 };\n const tf = fitFont($, p.title, \"display\", 32 * k, w - pad * 2 - ic - 20 * k);\n const mf = { ...$.font(\"text\"), size: 27 * k, lh: 1.3 };\n const lines = $.wrap(String(p.message || \"\"), w - pad * 2 - ic - 20 * k, mf).slice(0, 3);\n const h = pad * 2 + af.size * 1.6 + tf.size * 1.25 + lines.length * mf.size * 1.3;\n const x = desk ? W - A.x - w : (W - w) / 2;\n const y = desk ? A.y : A.y * (A.tall ? 0.55 : 0.8);\n let b = $.rect(x, y, w, h, { r: (desk ? 18 : 34) * k, fill: c.surface, stroke: c.line, sw: 1.5 * k, shadow: \"lg\" });\n b += appIcon($, p.icon, x + pad, y + pad, ic, { fill: c.accent });\n const tx = x + pad + ic + 20 * k;\n b += $.text(String(p.app || \"\").toUpperCase(), { ...af, x: tx, y: y + pad - 2 * k, fill: c.mute });\n b += $.text(String(p.time || \"\"), { ...af, x: x + w - pad, y: y + pad - 2 * k, anchor: \"end\", fill: c.mute });\n let yy = y + pad + af.size * 1.6;\n b += $.text(String(p.title), { ...tf, x: tx, y: yy, fill: c.fg });\n yy += tf.size * 1.25;\n lines.forEach(function (l, i) { b += $.text(l, { ...mf, x: tx, y: yy + i * mf.size * 1.3, fill: c.fg, op: 0.82 }); });\n return M.part(\"banner\", { at: tin, in: desk ? \"slide\" : \"drop\", from: desk ? \"right\" : \"up\", distance: desk ? 60 : 90, ease: \"bouncy\", out: desk ? \"slide\" : \"drop\", outFrom: desk ? \"right\" : \"up\", box: { x: x, y: y, w: w, h: h } }, b);\n}\n","libs":[]},{"id":"notifications","name":"Notification stack","description":"Notifications land one after another and stack up, each new one pushing the pile down.","category":"social","tags":["notification","stack","cascade","ui","overlay"],"placement":"overlay","duration":{"default":6,"min":3,"max":30},"props":{"items":{"type":"list","label":"Notifications (App: message)","default":[{"app":"Sales","text":"New order — $1,240"},{"app":"Mail","text":"The client signed off on the cut"},{"app":"Calendar","text":"Launch call moved to 3 pm"},{"app":"Bank","text":"Payment received from Northwind"}],"maxItems":6,"minItems":1,"of":{"app":"text","text":"text"}},"gap":{"type":"number","label":"Seconds between","default":0.75,"min":0.2,"max":5},"icon":{"type":"icon","label":"Icon","default":"bell"}},"parts":{"items":"each notification"},"beats":{"in":0.2,"out":-0.45},"pack":"social","ref":"social/notifications","code":"// MONARC Social — helpers this pack's items share.\n\n/** A platform's icon and how it names its numbers (the platforms' own marks are theirs; these are plain icons). */\nvar PLATFORM = {\n instagram: { icon: \"camera\", line: \"Instagram\", count: \"followers\" },\n tiktok: { icon: \"reel\", line: \"TikTok\", count: \"followers\" },\n youtube: { icon: \"play\", line: \"YouTube\", count: \"subscribers\" },\n x: { icon: \"x\", line: \"X\", count: \"followers\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\", count: \"followers\" },\n threads: { icon: \"chat\", line: \"Threads\", count: \"followers\" },\n website: { icon: \"globe\", line: \"Website\", count: \"readers\" },\n};\n\n/** A small round check in the accent: verified. */\nfunction verified($, cx, cy, r) {\n return $.circle(cx, cy, r, { fill: $.c.accent }) + $.path(\"M\" + $.fx(cx - r * 0.45) + \" \" + $.fx(cy + r * 0.02) + \"L\" + $.fx(cx - r * 0.1) + \" \" + $.fx(cy + r * 0.36) + \"L\" + $.fx(cx + r * 0.48) + \" \" + $.fx(cy - r * 0.32), { stroke: $.on($.c.accent), sw: r * 0.28 });\n}\n\n/** A colour for a name, from the brand's two accents and fixed hues — the same name, the same colour. */\nfunction nameColor($, name) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\"];\n var h = 0, s = String(name || \"\");\n for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;\n return list[h % list.length];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.25 : 1.1);\n const tin = $.beat(\"in\", 0.2);\n const rows = p.items || [];\n const w = Math.min(A.w, A.wide ? 720 * k : A.w);\n const x = A.wide ? W - A.x - w : (W - w) / 2;\n const pad = 22 * k, ic = 52 * k, h = 112 * k, gap = 16 * k;\n const af = { ...$.font(\"text\"), size: 21 * k, ls: 0.06 };\n let s = \"\";\n rows.forEach(function (r, i) {\n const at = tin + i * p.gap;\n // Later arrivals push this one down by a row each, on a spring.\n let push = 0;\n for (let j = i + 1; j < rows.length; j++) push += $.spring(Math.max(0, t - (tin + j * p.gap)), 2.4, 8);\n const y = A.y + push * (h + gap);\n const tf = fitFont($, String(r.text), \"display\", 30 * k, w - pad * 2 - ic - 20 * k);\n let b = $.rect(x, y, w, h, { r: 26 * k, fill: c.surface, stroke: c.line, sw: 1.5 * k, shadow: \"md\" });\n b += appIcon($, p.icon, x + pad, y + (h - ic) / 2, ic, { fill: nameColor($, r.app) });\n b += $.text(String(r.app).toUpperCase(), { ...af, x: x + pad + ic + 18 * k, y: y + pad - 2 * k, fill: c.mute });\n b += $.text(\"now\", { ...af, x: x + w - pad, y: y + pad - 2 * k, anchor: \"end\", fill: c.mute });\n b += $.text(String(r.text), { ...tf, x: x + pad + ic + 18 * k, y: y + pad + af.size * 1.45, fill: c.fg });\n s = M.part(\"items\", { at: at, in: \"drop\", distance: 70, ease: \"bouncy\", out: \"fade\", box: { x: x, y: y, w: w, h: h } }, b) + s;\n });\n return s;\n}\n","libs":[]},{"id":"comments","name":"Comments","description":"Comment cards appear one by one: a picture or initials, the name, the time, the words typing on and a like count.","category":"social","tags":["comments","social","replies","typing","overlay"],"placement":"overlay","duration":{"default":7,"min":3,"max":30},"props":{"items":{"type":"list","label":"Comments (Name: words)","default":[{"name":"maya.edits","text":"This transition is unreal. How long did it take?"},{"name":"jordan_k","text":"Saving this for my next edit"},{"name":"alex.rivera","text":"The pacing in the first 3 seconds 🔥"}],"maxItems":4,"minItems":1,"of":{"name":"text","text":"text"}},"likes":{"type":"number","label":"Likes on the first","default":1240,"min":0,"max":9999999},"gap":{"type":"number","label":"Seconds between","default":1.6,"min":0.4,"max":6}},"parts":{"items":"each comment"},"beats":{"in":0.2,"out":-0.45},"pack":"social","ref":"social/comments","code":"// MONARC Social — helpers this pack's items share.\n\n/** A platform's icon and how it names its numbers (the platforms' own marks are theirs; these are plain icons). */\nvar PLATFORM = {\n instagram: { icon: \"camera\", line: \"Instagram\", count: \"followers\" },\n tiktok: { icon: \"reel\", line: \"TikTok\", count: \"followers\" },\n youtube: { icon: \"play\", line: \"YouTube\", count: \"subscribers\" },\n x: { icon: \"x\", line: \"X\", count: \"followers\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\", count: \"followers\" },\n threads: { icon: \"chat\", line: \"Threads\", count: \"followers\" },\n website: { icon: \"globe\", line: \"Website\", count: \"readers\" },\n};\n\n/** A small round check in the accent: verified. */\nfunction verified($, cx, cy, r) {\n return $.circle(cx, cy, r, { fill: $.c.accent }) + $.path(\"M\" + $.fx(cx - r * 0.45) + \" \" + $.fx(cy + r * 0.02) + \"L\" + $.fx(cx - r * 0.1) + \" \" + $.fx(cy + r * 0.36) + \"L\" + $.fx(cx + r * 0.48) + \" \" + $.fx(cy - r * 0.32), { stroke: $.on($.c.accent), sw: r * 0.28 });\n}\n\n/** A colour for a name, from the brand's two accents and fixed hues — the same name, the same colour. */\nfunction nameColor($, name) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\"];\n var h = 0, s = String(name || \"\");\n for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;\n return list[h % list.length];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.3 : 1.12);\n const tin = $.beat(\"in\", 0.2);\n const rows = p.items || [];\n const w = Math.min(A.w, A.wide ? 700 * k : A.w);\n const pad = 24 * k, av = 30 * k;\n const nf = { ...$.font(\"display\"), size: 26 * k };\n const tf = { ...$.font(\"text\"), size: 28 * k, lh: 1.3 };\n const meta = { ...$.font(\"text\"), size: 22 * k };\n // Heights first, so the stack can sit at the bottom of the safe area.\n const cards = rows.map(function (r, i) {\n const lines = $.wrap(String(r.text), w - pad * 2 - av * 2 - 20 * k, tf).slice(0, 3);\n return { r: r, lines: lines, h: pad * 2 + nf.size * 1.3 + lines.length * tf.size * 1.3 + meta.size * 1.4, at: tin + i * p.gap };\n });\n const gap = 16 * k;\n const total = cards.reduce(function (a, c2) { return a + c2.h; }, 0) + gap * Math.max(0, cards.length - 1);\n const x = A.wide ? A.x : (W - w) / 2;\n let y = A.wide ? A.y + A.h - total : A.y + A.h * 0.55 - total / 2;\n let s = \"\";\n cards.forEach(function (cd, i) {\n const r = cd.r;\n let b = $.rect(x, y, w, cd.h, { r: 24 * k, fill: c.surface, stroke: c.line, sw: 1.5 * k, shadow: \"md\" });\n b += avatar($, null, x + pad + av, y + pad + av, av, { name: r.name, fill: nameColor($, r.name) });\n const tx = x + pad + av * 2 + 18 * k;\n b += $.text(String(r.name), { ...nf, x: tx, y: y + pad, fill: c.fg });\n b += $.text((i + 1) * 2 + \"m\", { ...meta, x: tx + $.measure(String(r.name), nf) + 14 * k, y: y + pad + 3 * k, fill: c.mute });\n // The words type on, line by line.\n let left = Math.max(0, Math.floor((t - cd.at - 0.25) * 42));\n cd.lines.forEach(function (l, j) {\n const n = Math.min(Array.from(l).length, left);\n left -= n;\n if (n > 0) b += $.text(Array.from(l).slice(0, n).join(\"\"), { ...tf, x: tx, y: y + pad + nf.size * 1.35 + j * tf.size * 1.3, fill: c.fg });\n });\n const ly = y + cd.h - pad - meta.size * 0.9;\n const likes = i === 0 ? $.count(Math.round(p.likes * $.E.out3($.seg(t, cd.at + 0.6, cd.at + 1.6)))) : String(Math.round(p.likes / (6 + i * 5)));\n b += $.icon(\"heart\", tx, ly - 2 * k, meta.size * 1.05, { color: i === 0 && t > cd.at + 0.6 ? c.red : c.mute, fill: i === 0 && t > cd.at + 0.6 });\n b += $.text(likes, { ...meta, x: tx + meta.size * 1.5, y: ly, fill: c.mute });\n b += $.text(\"Reply\", { ...meta, x: tx + meta.size * 5.5, y: ly, fill: c.mute });\n s += M.part(\"items\", { at: cd.at, in: \"rise\", distance: 40, ease: \"bouncy\", out: \"fade\", box: { x: x, y: y, w: w, h: cd.h } }, b);\n y += cd.h + gap;\n });\n return s;\n}\n","libs":[]},{"id":"chat","name":"Chat thread","description":"A message thread plays out: their bubbles arrive after a typing indicator, yours on the right in the accent.","category":"social","tags":["chat","messages","dm","texting","ui"],"placement":"overlay","duration":{"default":8,"min":3,"max":60},"props":{"contact":{"type":"text","label":"Contact","default":"Sam · Producer","maxLength":28},"messages":{"type":"list","label":"Messages (me: … / them: …)","default":[{"who":"them","text":"Did the new cut land?"},{"who":"me","text":"Rendering now — 3 versions"},{"who":"them","text":"Send the vertical one first"},{"who":"me","text":"On its way 🚀"}],"maxItems":8,"minItems":1,"of":{"who":"text","text":"text"}},"gap":{"type":"number","label":"Seconds between","default":1.4,"min":0.4,"max":6}},"parts":{"header":"the contact bar","messages":"each bubble","typing":"the typing dots"},"beats":{"in":0.2,"out":-0.45},"pack":"social","ref":"social/chat","code":"// MONARC Social — helpers this pack's items share.\n\n/** A platform's icon and how it names its numbers (the platforms' own marks are theirs; these are plain icons). */\nvar PLATFORM = {\n instagram: { icon: \"camera\", line: \"Instagram\", count: \"followers\" },\n tiktok: { icon: \"reel\", line: \"TikTok\", count: \"followers\" },\n youtube: { icon: \"play\", line: \"YouTube\", count: \"subscribers\" },\n x: { icon: \"x\", line: \"X\", count: \"followers\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\", count: \"followers\" },\n threads: { icon: \"chat\", line: \"Threads\", count: \"followers\" },\n website: { icon: \"globe\", line: \"Website\", count: \"readers\" },\n};\n\n/** A small round check in the accent: verified. */\nfunction verified($, cx, cy, r) {\n return $.circle(cx, cy, r, { fill: $.c.accent }) + $.path(\"M\" + $.fx(cx - r * 0.45) + \" \" + $.fx(cy + r * 0.02) + \"L\" + $.fx(cx - r * 0.1) + \" \" + $.fx(cy + r * 0.36) + \"L\" + $.fx(cx + r * 0.48) + \" \" + $.fx(cy - r * 0.32), { stroke: $.on($.c.accent), sw: r * 0.28 });\n}\n\n/** A colour for a name, from the brand's two accents and fixed hues — the same name, the same colour. */\nfunction nameColor($, name) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\"];\n var h = 0, s = String(name || \"\");\n for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;\n return list[h % list.length];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.3 : 1.1);\n const tin = $.beat(\"in\", 0.2);\n const msgs = p.messages || [];\n const w = Math.min(A.w, A.wide ? 760 * k : A.w);\n const x = (W - w) / 2;\n const tf = { ...$.font(\"text\"), size: 30 * k, lh: 1.28 };\n const bp = 22 * k, maxB = w * 0.74, gap = 14 * k;\n // Each bubble's lines and time: theirs wait 0.7 s behind typing dots.\n let at = tin + 0.5;\n const bubbles = msgs.map(function (m) {\n const mine = /^me$/i.test(String(m.who).trim());\n const lines = $.wrap(String(m.text), maxB - bp * 2, tf).slice(0, 4);\n const bw = Math.max.apply(null, lines.map(function (l) { return $.measure(l, tf); })) + bp * 2;\n const bh = lines.length * tf.size * 1.28 + bp * 1.4;\n const b = { mine: mine, lines: lines, w: bw, h: bh, typing: mine ? null : at, at: mine ? at : at + 0.7 };\n at = b.at + p.gap;\n return b;\n });\n const hh = 96 * k;\n // The thread scrolls up when it outgrows the space under the header.\n const room = A.h - hh - 20 * k;\n let used = 0;\n const shown = bubbles.filter(function (b) { return t >= b.at; });\n shown.forEach(function (b) { used += b.h + gap; });\n const typingNow = bubbles.some(function (b) { return b.typing !== null && t >= b.typing && t < b.at; });\n if (typingNow) used += 70 * k + gap;\n const scroll = Math.max(0, used - room);\n const top = A.y + hh + 20 * k;\n let s = M.part(\"header\", { at: tin, in: \"drop\", distance: 30, out: \"fade\", vary: false, box: { x: x, y: A.y, w: w, h: hh } },\n $.rect(x, A.y, w, hh, { r: 24 * k, fill: c.surface, stroke: c.line, sw: 1.5 * k, shadow: \"md\" }),\n avatar($, null, x + 30 * k + 28 * k, A.y + hh / 2, 28 * k, { name: p.contact, fill: c.accent }),\n $.text(String(p.contact), { ...$.font(\"display\"), size: 30 * k, x: x + 30 * k + 72 * k, y: A.y + hh / 2 - 17 * k, fill: c.fg }));\n let y = top - scroll;\n let body = \"\";\n bubbles.forEach(function (b, i) {\n if (b.typing !== null && t >= b.typing && t < b.at) {\n const dx = x, dw = 120 * k, dh = 66 * k;\n let dots = $.rect(dx, y, dw, dh, { r: dh / 2, fill: c.surface, stroke: c.line, sw: 1.5 * k });\n for (let d = 0; d < 3; d++) dots += $.circle(dx + dw * (0.3 + d * 0.2), y + dh / 2 - 6 * k * Math.max(0, Math.sin((t - b.typing) * 9 - d * 0.9)), 8 * k, { fill: c.mute });\n body += M.part(\"typing\", { at: b.typing, in: \"pop\", origin: \"left\", out: \"none\", box: { x: dx, y: y, w: dw, h: dh } }, dots);\n }\n if (t < b.at) return;\n const bx = b.mine ? x + w - b.w : x;\n let bub = $.rect(bx, y, b.w, b.h, { r: 28 * k, fill: b.mine ? c.accent : c.surface, stroke: b.mine ? \"none\" : c.line, sw: 1.5 * k });\n b.lines.forEach(function (l, j) { bub += $.text(l, { ...tf, x: bx + bp, y: y + bp * 0.7 + j * tf.size * 1.28, fill: b.mine ? $.on(c.accent) : c.fg }); });\n body += M.part(\"messages\", { at: b.at, in: \"pop\", origin: b.mine ? \"bottom-right\" : \"bottom-left\", ease: \"bouncy\", out: \"fade\", box: { x: bx, y: y, w: b.w, h: b.h } }, bub);\n y += b.h + gap;\n });\n return s + $.clip(x - 20 * k, top, w + 40 * k, A.y + A.h - top, 0, body);\n}\n","libs":[]},{"id":"post","name":"Social post","description":"A post card: picture or initials, name, handle and check, the words, and likes, reposts and replies counting up as a heart pops.","category":"social","tags":["post","tweet","social","quote","card"],"placement":"overlay","duration":{"default":6,"min":3,"max":30},"props":{"name":{"type":"text","label":"Name","default":"Maya Chen","maxLength":26},"handle":{"type":"text","label":"Handle","default":"@mayaedits","maxLength":26},"text":{"type":"longtext","label":"Post","default":"Stopped editing timelines by hand this year. I describe the cut, the agent builds it, I direct. Shipping three times as much.","maxLength":280},"photo":{"type":"image","label":"Photo"},"verified":{"type":"boolean","label":"Verified","default":true},"likes":{"type":"number","label":"Likes","default":18400,"min":0,"max":999999999},"reposts":{"type":"number","label":"Reposts","default":2310,"min":0,"max":999999999},"replies":{"type":"number","label":"Replies","default":412,"min":0,"max":999999999},"time":{"type":"text","label":"Time","default":"2h","maxLength":12}},"parts":{"card":"the card","author":"the name line","text":"the words","stats":"the counts","heart":"the heart"},"beats":{"in":0.15,"out":-0.45},"pack":"social","ref":"social/post","code":"// MONARC Social — helpers this pack's items share.\n\n/** A platform's icon and how it names its numbers (the platforms' own marks are theirs; these are plain icons). */\nvar PLATFORM = {\n instagram: { icon: \"camera\", line: \"Instagram\", count: \"followers\" },\n tiktok: { icon: \"reel\", line: \"TikTok\", count: \"followers\" },\n youtube: { icon: \"play\", line: \"YouTube\", count: \"subscribers\" },\n x: { icon: \"x\", line: \"X\", count: \"followers\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\", count: \"followers\" },\n threads: { icon: \"chat\", line: \"Threads\", count: \"followers\" },\n website: { icon: \"globe\", line: \"Website\", count: \"readers\" },\n};\n\n/** A small round check in the accent: verified. */\nfunction verified($, cx, cy, r) {\n return $.circle(cx, cy, r, { fill: $.c.accent }) + $.path(\"M\" + $.fx(cx - r * 0.45) + \" \" + $.fx(cy + r * 0.02) + \"L\" + $.fx(cx - r * 0.1) + \" \" + $.fx(cy + r * 0.36) + \"L\" + $.fx(cx + r * 0.48) + \" \" + $.fx(cy - r * 0.32), { stroke: $.on($.c.accent), sw: r * 0.28 });\n}\n\n/** A colour for a name, from the brand's two accents and fixed hues — the same name, the same colour. */\nfunction nameColor($, name) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\"];\n var h = 0, s = String(name || \"\");\n for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;\n return list[h % list.length];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.3 : 1.1);\n const tin = $.beat(\"in\", 0.15);\n const cw = Math.min(A.w, A.wide ? 900 * k : A.w), pad = 40 * k, av = 40 * k;\n const nf = fitFont($, p.name, \"display\", 32 * k, cw * 0.5);\n const mf = { ...$.font(\"text\"), size: 26 * k };\n const tf = { ...$.font(\"text\"), size: 36 * k, lh: 1.32 };\n const lines = $.wrap(String(p.text || \"\"), cw - pad * 2, tf).slice(0, 7);\n const ch = pad + av * 2 + 24 * k + lines.length * tf.size * 1.32 + 30 * k + 40 * k + pad;\n const x = (W - cw) / 2, y = A.y + (A.h - ch) * (A.tall ? 0.48 : 0.5);\n const leave = { out: \"rise\", distance: 40 };\n let s = M.part(\"card\", Object.assign({ at: tin, in: \"rise\", distance: 60, box: { x: x, y: y, w: cw, h: ch } }, leave), $.card(x, y, cw, ch, { shadow: \"lg\" }));\n const ay = y + pad + av;\n const tx = x + pad + av * 2 + 20 * k;\n const nw = $.measure(String(p.name), nf);\n s += M.part(\"author\", Object.assign({ at: tin + 0.12, in: \"fade\", dur: 0.3, box: { x: x + pad, y: y + pad, w: cw - pad * 2, h: av * 2 } }, leave),\n avatar($, \"photo\", x + pad + av, ay, av, { name: p.name, fill: c.accent }),\n $.text(String(p.name), { ...nf, x: tx, y: ay - nf.size * 1.0, fill: c.fg }),\n p.verified ? verified($, tx + nw + nf.size * 0.55, ay - nf.size * 0.45, nf.size * 0.38) : \"\",\n $.text(String(p.handle || \"\") + (p.time ? \" · \" + p.time : \"\"), { ...mf, x: tx, y: ay + 6 * k, fill: c.mute }));\n s += M.text(\"text\", Object.assign({ at: tin + 0.3, in: \"rise\", distance: 16, split: \"lines\", stagger: 0.08 }, leave, tf, { x: x + pad, y: y + pad + av * 2 + 24 * k, fill: c.fg, maxW: cw - pad * 2 }), String(p.text || \"\"));\n // Counts climb after the words land; the heart pops red as it starts.\n const sy = y + ch - pad - 30 * k, ts = tin + 0.6 + lines.length * 0.08;\n const cnt = function (n) { return $.count(Math.round(n * $.E.out3($.seg(t, ts, ts + 1.6)))); };\n const col = (cw - pad * 2) / 3;\n const statsRow = [[\"chat\", cnt(p.replies)], [\"loop\", cnt(p.reposts)], [\"heart\", cnt(p.likes)]];\n let row = \"\";\n statsRow.forEach(function (st, i) {\n const sx = x + pad + i * col;\n if (st[0] !== \"heart\") row += $.icon(st[0], sx, sy - 2 * k, 32 * k, { color: c.mute });\n row += $.text(st[1], { ...mf, x: sx + 46 * k, y: sy, fill: st[0] === \"heart\" && t > ts ? c.red : c.mute });\n });\n s += M.part(\"stats\", Object.assign({ at: ts - 0.2, in: \"fade\", dur: 0.3, box: { x: x + pad, y: sy - 10 * k, w: cw - pad * 2, h: 50 * k } }, leave), row);\n const hx = x + pad + 2 * col;\n const liked = t > ts;\n s += M.part(\"heart\", Object.assign({ at: ts - 0.2, in: \"fade\", dur: 0.2, emphasis: [{ fx: \"pop\", at: ts }], box: { x: hx, y: sy - 4 * k, w: 34 * k, h: 34 * k } }, leave), $.icon(\"heart\", hx, sy - 2 * k, 32 * k, { color: liked ? c.red : c.mute, fill: liked }));\n return s;\n}\n","libs":[]},{"id":"now-playing","name":"Now playing","description":"A music card: the cover (or a generated one), the title and artist, a progress bar moving with the clip and an equaliser.","category":"social","tags":["music","now playing","song","audio","card"],"placement":"overlay","duration":{"default":6,"min":2,"max":60},"props":{"title":{"type":"text","label":"Song","default":"Midnight Edit","maxLength":32},"artist":{"type":"text","label":"Artist","default":"The Long Cut","maxLength":32},"cover":{"type":"image","label":"Cover"},"length":{"type":"text","label":"Length (m:ss)","default":"3:42","maxLength":6},"start":{"type":"number","label":"Starts at (0–1)","default":0.34,"min":0,"max":1},"position":{"type":"choice","label":"Position","options":["auto","center","bottom-left","bottom-right"],"default":"auto"}},"parts":{"card":"the card","cover":"the cover","title":"the song and artist","progress":"the progress bar","eq":"the equaliser"},"beats":{"in":0.15,"out":-0.45},"pack":"social","ref":"social/now-playing","code":"// MONARC Social — helpers this pack's items share.\n\n/** A platform's icon and how it names its numbers (the platforms' own marks are theirs; these are plain icons). */\nvar PLATFORM = {\n instagram: { icon: \"camera\", line: \"Instagram\", count: \"followers\" },\n tiktok: { icon: \"reel\", line: \"TikTok\", count: \"followers\" },\n youtube: { icon: \"play\", line: \"YouTube\", count: \"subscribers\" },\n x: { icon: \"x\", line: \"X\", count: \"followers\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\", count: \"followers\" },\n threads: { icon: \"chat\", line: \"Threads\", count: \"followers\" },\n website: { icon: \"globe\", line: \"Website\", count: \"readers\" },\n};\n\n/** A small round check in the accent: verified. */\nfunction verified($, cx, cy, r) {\n return $.circle(cx, cy, r, { fill: $.c.accent }) + $.path(\"M\" + $.fx(cx - r * 0.45) + \" \" + $.fx(cy + r * 0.02) + \"L\" + $.fx(cx - r * 0.1) + \" \" + $.fx(cy + r * 0.36) + \"L\" + $.fx(cx + r * 0.48) + \" \" + $.fx(cy - r * 0.32), { stroke: $.on($.c.accent), sw: r * 0.28 });\n}\n\n/** A colour for a name, from the brand's two accents and fixed hues — the same name, the same colour. */\nfunction nameColor($, name) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\"];\n var h = 0, s = String(name || \"\");\n for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;\n return list[h % list.length];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k * (A.tall ? 1.3 : 1.1);\n const tin = $.beat(\"in\", 0.15);\n const cw = Math.min(A.w, 680 * k), pad = 28 * k, art = 150 * k;\n const ch = art + pad * 2 + 70 * k;\n const pos = p.position === \"auto\" ? (A.wide ? \"bottom-left\" : \"center\") : p.position;\n const x = pos === \"center\" ? (W - cw) / 2 : pos === \"bottom-left\" ? A.x : W - A.x - cw;\n const y = pos === \"center\" ? A.y + (A.h - ch) * 0.6 : A.y + A.h - ch;\n const leave = { out: \"slide\", outFrom: pos === \"bottom-right\" ? \"right\" : \"left\", distance: 40 };\n let s = M.part(\"card\", Object.assign({ at: tin, in: \"slide\", from: pos === \"bottom-right\" ? \"right\" : \"left\", distance: 60, box: { x: x, y: y, w: cw, h: ch } }, leave), $.card(x, y, cw, ch, { shadow: \"lg\" }));\n // The cover: the picture, or a gradient in the brand's accents with a disc on it.\n let cover;\n if ($.media.cover) cover = $.image(\"cover\", x + pad, y + pad, art, art, { r: 18 * k });\n else {\n const gid = $.uid(\"np\");\n $.def('<linearGradient id=\"' + gid + '\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"' + c.accent + '\"/><stop offset=\"1\" stop-color=\"' + c.accent2 + '\"/></linearGradient>');\n cover = $.rect(x + pad, y + pad, art, art, { r: 18 * k, fill: \"url(#\" + gid + \")\" }) + $.circle(x + pad + art / 2, y + pad + art / 2, art * 0.3, { fill: \"rgba(0,0,0,0.25)\" }) + $.circle(x + pad + art / 2, y + pad + art / 2, art * 0.06, { fill: \"#FFFFFF\", op: 0.8 });\n }\n s += M.part(\"cover\", Object.assign({ at: tin + 0.1, in: \"pop\", box: { x: x + pad, y: y + pad, w: art, h: art } }, leave), cover);\n const tx = x + pad + art + 26 * k;\n const tw = cw - (tx - x) - pad;\n const nf = fitFont($, p.title, \"display\", 38 * k, tw - 80 * k);\n const af = fitFont($, p.artist, \"text\", 28 * k, tw);\n s += M.part(\"title\", Object.assign({ at: tin + 0.2, in: \"rise\", distance: 14, box: { x: tx, y: y + pad, w: tw, h: art * 0.7 } }, leave),\n $.text(String(p.title), { ...nf, x: tx, y: y + pad + 10 * k, fill: c.fg }),\n $.text(String(p.artist), { ...af, x: tx, y: y + pad + 10 * k + nf.size * 1.25, fill: c.mute }));\n // An equaliser beside the title: bars bouncing at fixed, different rates.\n let eq = \"\";\n for (let i = 0; i < 4; i++) {\n const hgt = (0.3 + 0.7 * Math.abs(Math.sin(t * (5 + i * 1.7) + i * 1.3))) * 34 * k;\n eq += $.rect(x + cw - pad - (4 - i) * 12 * k, y + pad + 44 * k - hgt, 7 * k, hgt, { r: 3 * k, fill: c.accent });\n }\n s += M.part(\"eq\", Object.assign({ at: tin + 0.3, in: \"fade\", dur: 0.3, box: { x: x + cw - pad - 50 * k, y: y + pad, w: 50 * k, h: 48 * k } }, leave), eq);\n // Progress: from `start` of the song, moving in real time.\n const m = /^(\\d+):(\\d\\d)$/.exec(String(p.length || \"3:00\"));\n const len = m ? Number(m[1]) * 60 + Number(m[2]) : 180;\n const now = Math.min(len, p.start * len + Math.max(0, t - tin));\n const fmt = function (v) { v = Math.floor(v); return Math.floor(v / 60) + \":\" + (v % 60 < 10 ? \"0\" : \"\") + (v % 60); };\n const py = y + pad + art + 30 * k, pw = cw - pad * 2;\n const mf = { ...$.font(\"mono\"), size: 20 * k };\n s += M.part(\"progress\", Object.assign({ at: tin + 0.35, in: \"grow-x\", origin: \"left\", dur: 0.4, box: { x: x + pad, y: py - 6 * k, w: pw, h: 40 * k } }, leave),\n $.rect(x + pad, py, pw, 8 * k, { r: 4 * k, fill: $.rgba(c.fg, 0.15) }),\n $.rect(x + pad, py, pw * now / len, 8 * k, { r: 4 * k, fill: c.accent }),\n $.circle(x + pad + pw * now / len, py + 4 * k, 10 * k, { fill: c.fg }),\n $.text(fmt(now), { ...mf, x: x + pad, y: py + 18 * k, fill: c.mute }),\n $.text(\"-\" + fmt(len - now), { ...mf, x: x + pad + pw, y: py + 18 * k, anchor: \"end\", fill: c.mute }));\n return s;\n}\n","libs":[]},{"id":"rating","name":"Rating","description":"Stars fill one by one to the score, the score counts up, then a review and its author fade in.","category":"social","tags":["rating","review","stars","testimonial","proof"],"placement":"full","duration":{"default":6,"min":3,"max":30},"props":{"score":{"type":"number","label":"Score (out of 5)","default":4.8,"min":0,"max":5,"decimals":1},"count":{"type":"text","label":"Count line","default":"from 2,480 reviews","maxLength":40},"quote":{"type":"longtext","label":"Review","default":"The fastest way we've found to get from raw footage to something we're proud to post.","maxLength":200},"author":{"type":"text","label":"Author","default":"Priya S., creative director","maxLength":40}},"parts":{"ground":"the ground","stars":"the stars","score":"the score and count","quote":"the review","author":"who wrote it"},"beats":{"in":0.15,"out":-0.45},"pack":"social","ref":"social/rating","code":"// MONARC Social — helpers this pack's items share.\n\n/** A platform's icon and how it names its numbers (the platforms' own marks are theirs; these are plain icons). */\nvar PLATFORM = {\n instagram: { icon: \"camera\", line: \"Instagram\", count: \"followers\" },\n tiktok: { icon: \"reel\", line: \"TikTok\", count: \"followers\" },\n youtube: { icon: \"play\", line: \"YouTube\", count: \"subscribers\" },\n x: { icon: \"x\", line: \"X\", count: \"followers\" },\n linkedin: { icon: \"users\", line: \"LinkedIn\", count: \"followers\" },\n threads: { icon: \"chat\", line: \"Threads\", count: \"followers\" },\n website: { icon: \"globe\", line: \"Website\", count: \"readers\" },\n};\n\n/** A small round check in the accent: verified. */\nfunction verified($, cx, cy, r) {\n return $.circle(cx, cy, r, { fill: $.c.accent }) + $.path(\"M\" + $.fx(cx - r * 0.45) + \" \" + $.fx(cy + r * 0.02) + \"L\" + $.fx(cx - r * 0.1) + \" \" + $.fx(cy + r * 0.36) + \"L\" + $.fx(cx + r * 0.48) + \" \" + $.fx(cy - r * 0.32), { stroke: $.on($.c.accent), sw: r * 0.28 });\n}\n\n/** A colour for a name, from the brand's two accents and fixed hues — the same name, the same colour. */\nfunction nameColor($, name) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\"];\n var h = 0, s = String(name || \"\");\n for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;\n return list[h % list.length];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n const star = function (cx, cy, r, fill) {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + i * Math.PI / 5, rr = i % 2 ? r * 0.45 : r;\n d += (i ? \"L\" : \"M\") + $.fx(cx + rr * Math.cos(a)) + \" \" + $.fx(cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n };\n const sr = Math.min(A.w / 13, 64 * k);\n const sg = sr * 2.4;\n const sx0 = W / 2 - sg * 2;\n const total = sr * 2 + 40 * k + 150 * k + 60 * k + 200 * k;\n let y = A.y + (A.h - total) / 2 + sr;\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n // Each star fills in turn up to the score; the last one partly.\n let stars = \"\";\n for (let i = 0; i < 5; i++) {\n const cx = sx0 + i * sg;\n const at = tin + 0.2 + i * 0.16;\n const f = Math.max(0, Math.min(1, p.score - i)) * $.E.out3($.seg(t, at, at + 0.3));\n const pop = 1 + 0.25 * $.bump(t, at + 0.2, 0.06, 0.25);\n const id = $.uid(\"rs\");\n $.def('<clipPath id=\"' + id + '\"><rect x=\"' + $.fx(cx - sr) + '\" y=\"' + $.fx(y - sr) + '\" width=\"' + $.fx(2 * sr * f) + '\" height=\"' + $.fx(2 * sr) + '\"/></clipPath>');\n stars += $.g({ transform: $.tf({ s: pop, ox: cx, oy: y }) }, $.path(star(cx, y, sr), { fill: $.rgba(c.fg, 0.12) }), '<g clip-path=\"url(#' + id + ')\">' + $.path(star(cx, y, sr), { fill: c.accent2 }) + \"</g>\");\n }\n s += M.part(\"stars\", { at: tin, in: \"fade\", dur: 0.2, box: { x: sx0 - sr, y: y - sr, w: sg * 4 + 2 * sr, h: 2 * sr } }, stars);\n y += sr + 40 * k;\n const nf = { ...$.font(\"display\"), size: 120 * k };\n const score = $.counter(t, tin + 0.2, tin + 1.1, p.score, { dec: 1 });\n s += M.part(\"score\", { at: tin + 0.2, in: \"rise\", distance: 20, box: { x: A.x, y: y, w: A.w, h: nf.size * 1.6 } },\n $.text(score, { ...nf, x: W / 2, y: y, anchor: \"middle\", fill: c.fg }),\n $.text(String(p.count || \"\"), { ...$.font(\"text\"), size: 30 * k, x: W / 2, y: y + nf.size * 1.05, anchor: \"middle\", fill: c.mute }));\n y += nf.size * 1.05 + 30 * k + 60 * k;\n const qf = fitLines($, \"“\" + String(p.quote || \"\") + \"”\", { ...$.font(\"accent\"), ls: 0 }, A.w * (A.wide ? 0.7 : 0.92), 64 * k, 30 * k, 4);\n s += M.text(\"quote\", { at: tin + 1.2, in: \"rise\", distance: 16, split: \"lines\", stagger: 0.1, ...qf.font, x: W / 2, y: y, anchor: \"middle\", fill: c.fg, lh: 1.25, maxW: A.w * (A.wide ? 0.7 : 0.92) }, qf.lines.join(\"\\n\"));\n y += qf.lines.length * qf.size * 1.25 + 30 * k;\n s += M.text(\"author\", { at: tin + 1.6, in: \"fade\", dur: 0.4, ...$.font(\"mono\"), size: 24 * k, ls: 0.1, x: W / 2, y: y, anchor: \"middle\", fill: c.mute }, String(p.author || \"\").toUpperCase());\n return s;\n}\n","libs":[]}]},{"format":1,"id":"data","name":"MONARC Data","version":"1.0.0","description":"Line charts, donuts, bar races, goal meters, versus splits, timelines, funnels, world maps and flight routes — data that moves, in any brand, any aspect.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/data","style":"cream","shared":"shared.js","items":["line-chart","donut","bar-race","goals","versus","timeline","funnel","world-map","flight"],"entries":[{"id":"line-chart","name":"Line chart","description":"A line draws on through its points over a soft area, each point popping as the line reaches it, the last value called out, against an optional target.","category":"chart","tags":["chart","line","trend","growth","data"],"placement":"full","duration":{"default":6,"min":3,"max":30},"props":{"title":{"type":"text","label":"Title","default":"Weekly views since the switch","maxLength":56},"rows":{"type":"list","label":"Rows (Label: value)","default":[{"label":"W1","value":12},{"label":"W2","value":18},{"label":"W3","value":16},{"label":"W4","value":27},{"label":"W5","value":34},{"label":"W6","value":31},{"label":"W7","value":46},{"label":"W8","value":58}],"maxItems":12,"minItems":2,"of":{"label":"text","value":"number"}},"prefix":{"type":"text","label":"Before numbers","default":"","maxLength":3},"suffix":{"type":"text","label":"After numbers","default":"K","maxLength":6},"decimals":{"type":"number","label":"Decimals","default":0,"min":0,"max":3},"target":{"type":"number","label":"Target line (0 = none)","default":40,"min":0,"max":1000000000000},"targetLabel":{"type":"text","label":"Target label","default":"Goal","maxLength":16}},"parts":{"ground":"the ground","title":"the heading","grid":"the gridlines and axis labels","area":"the area under the line","line":"the line","dots":"the points","label":"the last value's callout","target":"the target line"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/line-chart","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const head = chartHead($, M, t, tin, p.title, A, k);\n s += head.s;\n const rows = p.rows || [];\n const n = rows.length;\n const lf = { ...$.font(\"text\"), size: Math.min(28 * k, (A.w / n) * 0.3) };\n const vf = { ...$.font(\"mono\"), size: 22 * k };\n const maxV = Math.max.apply(null, rows.map(function (r) { return r.value; }).concat([p.target || 0, 1e-9]));\n const step = niceStep(maxV * 1.1, 4);\n const top = step * Math.ceil((maxV * 1.08) / step);\n const axisW = $.measure(fmtNum(top, 0, p.prefix, p.suffix), vf) + 18 * k;\n const box = { x: A.x + axisW, y: head.y + 30 * k, w: A.w - axisW - 70 * k, h: A.y + A.h - head.y - 30 * k - lf.size * 2.2 };\n const X = function (i) { return box.x + (n > 1 ? (i / (n - 1)) * box.w : box.w / 2); };\n const Y = function (v) { return box.y + box.h - (v / top) * box.h; };\n let grid = \"\";\n for (let v = 0; v <= top + 1e-9; v += step) {\n grid += $.line(box.x, Y(v), box.x + box.w, Y(v), { stroke: $.rgba(c.fg, v === 0 ? 0.3 : 0.1), sw: 2 * k });\n grid += $.text(fmtNum(v, 0, p.prefix, p.suffix), { ...vf, x: box.x - 16 * k, y: Y(v) - vf.size * 0.55, anchor: \"end\", fill: c.mute });\n }\n rows.forEach(function (r, i) { if (n <= 8 || i % Math.ceil(n / 8) === 0 || i === n - 1) grid += $.text(String(r.label), { ...lf, x: X(i), y: box.y + box.h + lf.size * 0.6, anchor: \"middle\", fill: c.mute }); });\n s += M.part(\"grid\", { at: tin + 0.1, in: \"fade\", dur: 0.4, out: \"fade\", vary: false }, grid);\n // The line draws on from the left; each point pops when the line reaches it.\n const t0 = tin + 0.5, t1 = t0 + 1.8;\n const run = $.E.inOut2($.seg(t, t0, t1));\n const pts = rows.map(function (r, i) { return [X(i), Y(r.value)]; });\n const d = pts.map(function (q, i) { return (i ? \"L\" : \"M\") + $.fx(q[0]) + \" \" + $.fx(q[1]); }).join(\"\");\n const reach = box.x + box.w * run;\n if (p.target > 0) {\n const ty = Y(p.target);\n s += M.part(\"target\", { at: tin + 0.3, in: \"grow-x\", origin: \"left\", dur: 0.6, out: \"fade\", box: { x: box.x, y: ty - 4 * k, w: box.w, h: 8 * k } },\n $.line(box.x, ty, box.x + box.w, ty, { stroke: c.accent2, sw: 3 * k, dash: 12 * k + \" \" + 10 * k }),\n $.text(String(p.targetLabel || \"\") + \" \" + fmtNum(p.target, p.decimals, p.prefix, p.suffix), { ...vf, x: box.x + box.w + 12 * k, y: ty - vf.size * 0.55, fill: c.accent2 }));\n }\n const gid = $.uid(\"lg\");\n $.def('<linearGradient id=\"' + gid + '\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"' + c.accent + '\" stop-opacity=\"0.32\"/><stop offset=\"1\" stop-color=\"' + c.accent + '\" stop-opacity=\"0\"/></linearGradient>');\n s += $.clip(box.x - 10 * k, box.y - 40 * k, reach - box.x + 10 * k, box.h + 50 * k, 0,\n M.part(\"area\", { at: t0, in: \"fade\", dur: 0.4, out: \"fade\", vary: false }, $.path(d + \"L\" + $.fx(pts[n - 1][0]) + \" \" + $.fx(box.y + box.h) + \"L\" + $.fx(pts[0][0]) + \" \" + $.fx(box.y + box.h) + \"Z\", { fill: \"url(#\" + gid + \")\" })) +\n M.part(\"line\", { at: t0, in: \"none\", out: \"fade\" }, $.path(d, { stroke: c.accent, sw: 6 * k, fill: \"none\" })));\n let dots = \"\";\n pts.forEach(function (q, i) {\n if (q[0] > reach + 1) return;\n const at = t0 + (q[0] - box.x) / box.w * (t1 - t0);\n const pop = $.pop(t, at);\n dots += $.circle(q[0], q[1], 9 * k * Math.max(0, pop), { fill: c.bg, stroke: c.accent, sw: 4 * k });\n });\n s += M.part(\"dots\", { at: t0, in: \"none\", out: \"fade\" }, dots);\n const last = rows[n - 1], lp = pts[n - 1];\n const callout = fmtNum(last.value * $.E.out3($.seg(t, t1 - 0.2, t1 + 0.4)), p.decimals, p.prefix, p.suffix);\n const cfnt = { ...$.font(\"display\"), size: 40 * k };\n const cw2 = $.measure(fmtNum(last.value, p.decimals, p.prefix, p.suffix), cfnt) + 36 * k, chh = cfnt.size * 1.5;\n const cx = Math.min(lp[0] - cw2 / 2, A.x + A.w - cw2), cy = lp[1] - chh - 22 * k;\n s += M.part(\"label\", { at: t1 - 0.1, in: \"pop\", origin: \"bottom\", out: \"fade\", box: { x: cx, y: cy, w: cw2, h: chh } },\n $.rect(cx, cy, cw2, chh, { r: chh / 2, fill: c.accent }), $.text(callout, { ...cfnt, x: cx + cw2 / 2, y: cy + (chh - cfnt.size) / 2 - cfnt.size * 0.04, anchor: \"middle\", fill: $.on(c.accent) }));\n return s;\n}\n","libs":[]},{"id":"donut","name":"Donut chart","description":"Segments sweep round a ring one after another, the centre counts up to the total (or the first share), and a legend lists each part.","category":"chart","tags":["chart","donut","pie","share","data"],"placement":"full","duration":{"default":6,"min":3,"max":30},"props":{"title":{"type":"text","label":"Title","default":"Where the hours went","maxLength":50},"rows":{"type":"list","label":"Rows (Label: value)","default":[{"label":"Editing","value":46},{"label":"Rendering","value":12},{"label":"Review","value":18},{"label":"Publishing","value":9}],"maxItems":12,"minItems":2,"of":{"label":"text","value":"number"}},"center":{"type":"choice","label":"Centre shows","options":["total","first share"],"default":"first share"},"centerLabel":{"type":"text","label":"Centre label","default":"on editing","maxLength":20},"prefix":{"type":"text","label":"Before numbers","default":"","maxLength":3},"suffix":{"type":"text","label":"After numbers","default":" h","maxLength":6},"decimals":{"type":"number","label":"Decimals","default":0,"min":0,"max":3}},"parts":{"ground":"the ground","title":"the heading","ring":"the segments","center":"the number in the middle","legend":"the legend"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/donut","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const head = chartHead($, M, t, tin, p.title, A, k);\n s += head.s;\n const rows = p.rows || [];\n const total = rows.reduce(function (a, r) { return a + Math.max(0, r.value); }, 0) || 1;\n const room = { x: A.x, y: head.y, w: A.w, h: A.y + A.h - head.y };\n const side = A.wide;\n const R = side ? Math.min(room.h * 0.46, room.w * 0.24) : Math.min(room.w * 0.36, room.h * 0.26);\n const cx = side ? room.x + R + 20 * k : W / 2, cy = side ? room.y + room.h / 2 : room.y + R + 10 * k;\n const r0 = R * 0.62;\n // Segments sweep in turn, together taking 1.4 s.\n const t0 = tin + 0.4;\n let a = 0, ring = \"\";\n rows.forEach(function (r, i) {\n const share = Math.max(0, r.value) / total;\n const st = t0 + (a / (Math.PI * 2)) * 1.4;\n const grow = $.E.out3($.seg(t, st, st + share * 1.4 + 0.15));\n if (grow > 0) ring += $.path(arcPath(cx, cy, r0, R, a + 0.012, a + Math.max(0.0001, share * Math.PI * 2 * grow) - 0.012), { fill: seriesColor($, i) });\n a += share * Math.PI * 2;\n });\n s += M.part(\"ring\", { at: t0 - 0.1, in: \"fade\", dur: 0.2, out: \"fade\", box: { x: cx - R, y: cy - R, w: 2 * R, h: 2 * R } }, $.circle(cx, cy, (R + r0) / 2, { stroke: $.rgba(c.fg, 0.07), sw: R - r0, fill: \"none\" }), ring);\n const first = rows[0] || { value: 0 };\n const big = p.center === \"total\" ? fmtNum(total * $.E.out3($.seg(t, t0, t0 + 1.4)), p.decimals, p.prefix, p.suffix) : Math.round((first.value / total) * 100 * $.E.out3($.seg(t, t0, t0 + 1.4))) + \"%\";\n const bf = fitFont($, p.center === \"total\" ? fmtNum(total, p.decimals, p.prefix, p.suffix) : \"100%\", \"display\", r0 * 0.62, r0 * 1.5);\n s += M.part(\"center\", { at: t0 + 0.2, in: \"pop\", out: \"fade\", box: { x: cx - r0, y: cy - r0, w: 2 * r0, h: 2 * r0 } },\n $.text(big, { ...bf, x: cx, y: cy - bf.size * 0.62, anchor: \"middle\", fill: c.fg }),\n $.text(String(p.centerLabel || \"\"), { ...$.font(\"text\"), size: Math.min(28 * k, r0 * 0.2), x: cx, y: cy + bf.size * 0.45, anchor: \"middle\", fill: c.mute }));\n // The legend beside the ring on wide frames, under it elsewhere.\n const lx = side ? cx + R + 80 * k : A.x, ly0 = side ? cy - (rows.length * 70 * k) / 2 : cy + R + 50 * k;\n const lw = side ? Math.min(A.x + A.w - lx, 680 * k) : A.w;\n const rowH = Math.min(70 * k, (A.y + A.h - ly0) / Math.max(1, rows.length));\n const lf = { ...$.font(\"text\"), size: Math.min(34 * k, rowH * 0.5) };\n rows.forEach(function (r, i) {\n const y = ly0 + i * rowH;\n s += M.part(\"legend\", { at: t0 + 0.3 + i * 0.12, in: \"slide\", from: \"left\", distance: 16, out: \"fade\", box: { x: lx, y: y, w: lw, h: rowH } },\n $.rect(lx, y + rowH * 0.25, rowH * 0.5, rowH * 0.5, { r: rowH * 0.12, fill: seriesColor($, i) }),\n $.text(String(r.label), { ...lf, x: lx + rowH * 0.8, y: y + (rowH - lf.size) / 2 - lf.size * 0.05, fill: c.fg }),\n $.text(fmtNum(r.value, p.decimals, p.prefix, p.suffix) + \" \" + Math.round((r.value / total) * 100) + \"%\", { ...$.font(\"mono\"), size: lf.size * 0.82, x: lx + lw, y: y + (rowH - lf.size * 0.82) / 2 - lf.size * 0.05, anchor: \"end\", fill: c.mute }));\n });\n return s;\n}\n","libs":[]},{"id":"bar-race","name":"Bar chart race","description":"Ranked bars grow and overtake each other as the periods advance; the axis rescales and the period counts up in the corner.","category":"chart","tags":["chart","bar race","ranking","time series","data"],"placement":"full","duration":{"default":8,"min":4,"max":60},"props":{"title":{"type":"text","label":"Title","default":"Most-used editing tools","maxLength":50},"periods":{"type":"list","label":"Periods","default":["2022","2023","2024","2025","2026"],"maxItems":12,"minItems":2,"of":"text"},"rows":{"type":"list","label":"Rows (Name: v1, v2, …)","default":[{"label":"MONARC","values":"4, 14, 38, 72, 118"},{"label":"Studio A","values":"60, 66, 70, 71, 72"},{"label":"Cutter","values":"40, 48, 52, 50, 46"},{"label":"Frame.io","values":"22, 30, 41, 47, 51"},{"label":"Clipmate","values":"35, 30, 24, 20, 18"},{"label":"Reelsy","values":"8, 20, 33, 44, 49"}],"maxItems":12,"minItems":2,"of":{"label":"text","values":"text"}},"show":{"type":"number","label":"Bars shown","default":6,"min":3,"max":10},"prefix":{"type":"text","label":"Before numbers","default":"","maxLength":3},"suffix":{"type":"text","label":"After numbers","default":"K","maxLength":6},"decimals":{"type":"number","label":"Decimals","default":0,"min":0,"max":3}},"parts":{"ground":"the ground","title":"the heading","bars":"the bars","period":"the period in the corner"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/bar-race","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const head = chartHead($, M, t, tin, p.title, A, k);\n s += head.s;\n const periods = p.periods || [];\n const np = periods.length;\n const rows = (p.rows || []).map(function (r, i) { return { label: String(r.label), i: i, v: String(r.values).split(/[,\\s]+/).filter(Boolean).map(Number).concat(new Array(np).fill(0)).slice(0, np) }; });\n // The race runs from 0.6 s to 0.8 s before the end; each period gets an equal share.\n const t0 = tin + 0.6, t1 = Math.max(t0 + 1, $.beat(\"out\", $.dur - 0.45) - 0.4);\n const pos = Math.max(0, Math.min(np - 1, ((t - t0) / (t1 - t0)) * (np - 1)));\n const i0 = Math.min(np - 2, Math.floor(pos)), f = pos - i0, fe = $.E.inOut2(f);\n const valAt = function (r) { return r.v[i0] + (r.v[i0 + 1] - r.v[i0]) * f; };\n const rankAt = function (j) { const sorted = rows.slice().sort(function (a, b) { return b.v[j] - a.v[j] || a.i - b.i; }); const m = {}; sorted.forEach(function (r, ix) { m[r.i] = ix; }); return m; };\n const ra = rankAt(i0), rb = rankAt(i0 + 1);\n const show = Math.min(rows.length, Math.round(p.show));\n const room = { y: head.y, h: A.y + A.h - head.y - 20 * k };\n // On wide frames the names sit left of the bars; elsewhere each bar gets a name row above it.\n const slot = room.h / show, bh = slot * (A.wide ? 0.72 : 0.44);\n const maxNow = Math.max.apply(null, rows.map(valAt).concat([1e-9]));\n const lf = { ...$.font(\"display\"), size: Math.min(34 * k, A.wide ? bh * 0.5 : slot * 0.3) };\n const vf = { ...$.font(\"mono\"), size: Math.min(28 * k, bh * 0.42), weight: 700 };\n const labelW = A.wide ? Math.min(A.w * 0.22, Math.max.apply(null, rows.map(function (r) { return $.measure(r.label, lf); })) + 24 * k) : 0;\n const bx = A.x + labelW, bw = A.w - labelW - $.measure(fmtNum(maxNow, p.decimals, p.prefix, p.suffix), vf) - 30 * k;\n let bars = \"\";\n rows.forEach(function (r) {\n const rank = ra[r.i] + (rb[r.i] - ra[r.i]) * fe;\n if (rank > show - 0.05) return;\n const y = A.wide ? room.y + rank * slot + (slot - bh) / 2 : room.y + rank * slot + slot * 0.42;\n const v = valAt(r), w = Math.max(bh * 0.3, (v / maxNow) * bw);\n const col = seriesColor($, r.i);\n bars += $.rect(bx, y, w, bh, { r: bh * 0.18, fill: col });\n if (A.wide) bars += $.text(r.label, { ...lf, x: bx - 18 * k, y: y + (bh - lf.size) / 2 - lf.size * 0.04, anchor: \"end\", fill: c.fg });\n else bars += $.text(r.label, { ...lf, x: bx, y: y - lf.size * 1.25, fill: c.fg });\n bars += $.text(fmtNum(v, p.decimals, p.prefix, p.suffix), { ...vf, x: bx + w + 14 * k, y: y + (bh - vf.size) / 2 - vf.size * 0.05, fill: c.fg });\n });\n s += M.part(\"bars\", { at: tin + 0.3, in: \"fade\", dur: 0.4, out: \"fade\" }, bars);\n const pf = { ...$.font(\"display\"), size: Math.min(A.h * 0.18, 200 * k) };\n const label = String(periods[Math.round(pos)] || \"\");\n s += M.part(\"period\", { at: tin + 0.4, in: \"fade\", dur: 0.4, out: \"fade\", vary: false }, $.text(label, { ...pf, x: A.x + A.w, y: A.y + A.h - pf.size * 1.05, anchor: \"end\", fill: $.rgba(c.fg, 0.16) }));\n return s;\n}\n","libs":[]},{"id":"goals","name":"Goal meters","description":"Rows of capsule meters fill to their share of a goal, each with its label and a percentage counting up.","category":"chart","tags":["chart","progress","goal","percent","data"],"placement":"full","duration":{"default":5,"min":3,"max":30},"props":{"title":{"type":"text","label":"Title","default":"Launch checklist","maxLength":50},"rows":{"type":"list","label":"Rows (Label: percent)","default":[{"label":"Footage cut","value":100},{"label":"Captions","value":85},{"label":"Colour grade","value":60},{"label":"Sound mix","value":35}],"maxItems":8,"minItems":1,"of":{"label":"text","value":"number"}}},"parts":{"ground":"the ground","title":"the heading","rows":"each meter"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/goals","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const head = chartHead($, M, t, tin, p.title, A, k);\n s += head.s;\n const rows = p.rows || [];\n const room = A.y + A.h - head.y;\n const rowH = Math.min((A.tall ? 190 : 150) * k, room / Math.max(1, rows.length));\n const total = rows.length * rowH;\n const w = A.wide ? Math.min(A.w, 1500 * k) : A.w;\n const x = A.wide ? (W - w) / 2 : A.x;\n let y = head.y + Math.max(0, (room - total) / 2);\n const lf = { ...$.font(\"text\"), size: Math.min(36 * k, rowH * 0.26) };\n const vf = { ...$.font(\"display\"), size: Math.min(40 * k, rowH * 0.28) };\n rows.forEach(function (r, i) {\n const at = tin + 0.35 + i * 0.18;\n const pct = Math.max(0, Math.min(100, r.value));\n const g = $.E.out3($.seg(t, at, at + 1));\n const th = rowH * 0.3, ty = y + rowH * 0.42;\n const done = pct >= 100 && g >= 1;\n let row = $.text(String(r.label), { ...lf, x: x, y: y + rowH * 0.06, fill: c.fg });\n row += $.text(Math.round(pct * g) + \"%\", { ...vf, x: x + w, y: y + rowH * 0.04, anchor: \"end\", fill: done ? c.green : c.fg });\n row += $.rect(x, ty, w, th, { r: th / 2, fill: $.rgba(c.fg, 0.1) });\n row += $.rect(x, ty, Math.max(th, w * (pct / 100) * g), th, { r: th / 2, fill: done ? c.green : seriesColor($, 0) });\n if (done) row += $.icon(\"check\", x + w - th * 1.05, ty + th * 0.12, th * 0.76, { color: $.on(c.green), sw: 3 });\n s += M.part(\"rows\", { at: at - 0.1, in: \"rise\", distance: 20, out: \"fade\", box: { x: x, y: y, w: w, h: rowH } }, row);\n y += rowH;\n });\n return s;\n}\n","libs":[]},{"id":"versus","name":"Versus","description":"Two sides face off: a label and a big number counting up on each, a VS badge popping between them and a split bar settling to their ratio.","category":"chart","tags":["comparison","versus","vs","split","data"],"placement":"full","duration":{"default":5,"min":3,"max":30},"props":{"title":{"type":"text","label":"Title","default":"Hours to publish one video","maxLength":50},"left":{"type":"text","label":"Left","default":"Before","maxLength":20},"leftValue":{"type":"number","label":"Left value","default":9.5,"min":0,"max":1000000000000},"right":{"type":"text","label":"Right","default":"With MONARC","maxLength":20},"rightValue":{"type":"number","label":"Right value","default":1.5,"min":0,"max":1000000000000},"prefix":{"type":"text","label":"Before numbers","default":"","maxLength":3},"suffix":{"type":"text","label":"After numbers","default":" h","maxLength":6},"decimals":{"type":"number","label":"Decimals","default":1,"min":0,"max":3}},"parts":{"ground":"the ground","title":"the heading","left":"the left side","right":"the right side","badge":"the VS badge","split":"the split bar"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/versus","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const head = chartHead($, M, t, tin, p.title, A, k);\n s += head.s;\n // Side by side on wide and square frames, one above the other on tall ones.\n const stack = A.tall;\n const room = { x: A.x, y: head.y, w: A.w, h: A.y + A.h - head.y - 110 * k };\n const half = stack ? { w: room.w, h: room.h / 2 } : { w: room.w / 2, h: room.h };\n const vfmt = function (v) { return fmtNum(v, p.decimals, p.prefix, p.suffix); };\n const side = function (name, label, value, col, i) {\n const bx = stack ? room.x : room.x + i * half.w, by = stack ? room.y + i * half.h : room.y;\n const nf = fitFont($, vfmt(value), \"display\", Math.min(half.h * 0.42, 220 * k), half.w * 0.86, { ls: -0.03 });\n const lf = fitFont($, $.case(label), \"text\", 40 * k, half.w * 0.86);\n const cx = bx + half.w / 2, cy = by + half.h / 2;\n const num = vfmt(value * $.E.out3($.seg(t, tin + 0.5, tin + 1.6)));\n return M.part(name, { at: tin + 0.25 + i * 0.15, in: \"rise\", distance: 30, out: \"fade\", box: { x: bx, y: by, w: half.w, h: half.h } },\n $.text($.case(label), { ...lf, x: cx, y: cy - nf.size * 0.62 - lf.size * 1.4, anchor: \"middle\", fill: c.mute }),\n $.text(num, { ...nf, x: cx, y: cy - nf.size * 0.55, anchor: \"middle\", fill: col }));\n };\n s += side(\"left\", p.left, p.leftValue, c.fg, 0);\n s += side(\"right\", p.right, p.rightValue, c.accent, 1);\n const bcx = stack ? W / 2 : room.x + room.w / 2, bcy = stack ? room.y + room.h / 2 : room.y + room.h / 2;\n const br = 54 * k;\n s += M.part(\"badge\", { at: tin + 0.7, in: \"slam\", intensity: 0.5, out: \"fade\", box: { x: bcx - br, y: bcy - br, w: 2 * br, h: 2 * br } },\n stack ? \"\" : $.line(bcx, room.y + 40 * k, bcx, room.y + room.h - 40 * k, { stroke: $.rgba(c.fg, 0.14), sw: 3 * k }),\n $.circle(bcx, bcy, br, { fill: c.accent2 }), $.text(\"VS\", { ...$.font(\"display\"), size: br * 0.78, x: bcx, y: bcy - br * 0.45, anchor: \"middle\", fill: $.on(c.accent2) }));\n // The split bar: 50/50, then the true ratio.\n const tot = (p.leftValue + p.rightValue) || 1;\n const f = 0.5 + (p.leftValue / tot - 0.5) * $.E.inOut3($.seg(t, tin + 1.1, tin + 2.1));\n const sy = A.y + A.h - 60 * k, sh = 26 * k;\n s += M.part(\"split\", { at: tin + 0.9, in: \"grow-x\", origin: \"center\", dur: 0.4, out: \"fade\", box: { x: A.x, y: sy, w: A.w, h: sh } },\n $.rect(A.x, sy, A.w * f - 4 * k, sh, { r: sh / 2, fill: c.fg, op: 0.85 }), $.rect(A.x + A.w * f + 4 * k, sy, A.w * (1 - f) - 4 * k, sh, { r: sh / 2, fill: c.accent }));\n return s;\n}\n","libs":[]},{"id":"timeline","name":"Timeline","description":"Milestones along a line that draws itself: each date's dot pops as the line reaches it and its words rise beside it.","category":"list","tags":["timeline","history","milestones","roadmap","data"],"placement":"full","duration":{"default":7,"min":3,"max":40},"props":{"title":{"type":"text","label":"Title","default":"How we got here","maxLength":50},"rows":{"type":"list","label":"Milestones (When: what)","default":[{"when":"2019","what":"First edit in a garage"},{"when":"2021","what":"Studio opens in Lisbon"},{"when":"2023","what":"100 creators on board"},{"when":"2025","what":"The AI editor ships"},{"when":"2026","what":"Volito goes open"}],"maxItems":8,"minItems":2,"of":{"when":"text","what":"text"}}},"parts":{"ground":"the ground","title":"the heading","line":"the line","dots":"the dots","labels":"the dates and words"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/timeline","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const head = chartHead($, M, t, tin, p.title, A, k);\n s += head.s;\n const rows = p.rows || [];\n const n = rows.length;\n const horiz = A.wide;\n const t0 = tin + 0.5, t1 = Math.min(t0 + 0.6 * n + 0.8, $.beat(\"out\", $.dur - 0.45) - 0.5);\n const run = $.E.inOut2($.seg(t, t0, t1));\n const wf = { ...$.font(\"display\"), size: (horiz ? 44 : 46) * k };\n const tf = { ...$.font(\"text\"), size: (horiz ? 28 : 32) * k, lh: 1.25 };\n let line = \"\", dots = \"\", labels = \"\";\n if (horiz) {\n const ly = head.y + (A.y + A.h - head.y) / 2;\n const x0 = A.x + 40 * k, x1 = A.x + A.w - 40 * k;\n line = $.line(x0, ly, x1, ly, { stroke: $.rgba(c.fg, 0.14), sw: 6 * k }) + $.line(x0, ly, x0 + (x1 - x0) * run, ly, { stroke: c.accent, sw: 6 * k });\n const colW = (x1 - x0) / Math.max(1, n - 1);\n rows.forEach(function (r, i) {\n const x = n > 1 ? x0 + i * colW : (x0 + x1) / 2;\n const at = t0 + (n > 1 ? i / (n - 1) : 0) * (t1 - t0);\n const up = i % 2 === 0;\n dots += $.circle(x, ly, 16 * k * Math.max(0, $.pop(t, at)), { fill: c.bg, stroke: c.accent, sw: 6 * k });\n const lines = $.wrap(String(r.what), colW * 1.6, tf).slice(0, 2);\n const blockH = wf.size * 1.15 + lines.length * tf.size * 1.25;\n const by = up ? ly - 50 * k - blockH : ly + 50 * k;\n // Kept inside the safe area: a label near an edge slides in from its dot.\n const bw = Math.max($.measure(String(r.when), wf), Math.max.apply(null, lines.map(function (l) { return $.measure(l, tf); })));\n const lx = Math.max(A.x + bw / 2, Math.min(A.x + A.w - bw / 2, x));\n labels += M.part(\"labels\", { at: at, in: \"rise\", distance: up ? -20 : 20, out: \"fade\", box: { x: lx - bw / 2, y: by, w: bw, h: blockH } },\n $.text(String(r.when), { ...wf, x: lx, y: by, anchor: \"middle\", fill: c.accent }),\n lines.map(function (l, j) { return $.text(l, { ...tf, x: lx, y: by + wf.size * 1.15 + j * tf.size * 1.25, anchor: \"middle\", fill: c.fg }); }).join(\"\"));\n });\n } else {\n const lx = A.x + 30 * k;\n const y0 = head.y + 20 * k, y1 = A.y + A.h - 20 * k;\n line = $.line(lx, y0, lx, y1, { stroke: $.rgba(c.fg, 0.14), sw: 6 * k }) + $.line(lx, y0, lx, y0 + (y1 - y0) * run, { stroke: c.accent, sw: 6 * k });\n const rowH = (y1 - y0) / Math.max(1, n);\n rows.forEach(function (r, i) {\n const y = y0 + i * rowH + 10 * k;\n const at = t0 + (i / Math.max(1, n)) * (t1 - t0);\n dots += $.circle(lx, y + wf.size * 0.5, 16 * k * Math.max(0, $.pop(t, at)), { fill: c.bg, stroke: c.accent, sw: 6 * k });\n const lines = $.wrap(String(r.what), A.w - 90 * k, tf).slice(0, 2);\n labels += M.part(\"labels\", { at: at, in: \"slide\", from: \"left\", distance: 20, out: \"fade\", box: { x: lx + 50 * k, y: y, w: A.w - 90 * k, h: rowH } },\n $.text(String(r.when), { ...wf, x: lx + 50 * k, y: y, fill: c.accent }),\n lines.map(function (l, j) { return $.text(l, { ...tf, x: lx + 50 * k, y: y + wf.size * 1.12 + j * tf.size * 1.25, fill: c.fg }); }).join(\"\"));\n });\n }\n s += M.part(\"line\", { at: t0, in: \"none\", out: \"fade\", vary: false }, line);\n s += M.part(\"dots\", { at: t0, in: \"none\", out: \"fade\" }, dots);\n return s + labels;\n}\n","libs":[]},{"id":"funnel","name":"Funnel","description":"Stages stack from widest to narrowest, each dropping in with its count, and the share that carries on to the next stage between them.","category":"chart","tags":["funnel","conversion","pipeline","data"],"placement":"full","duration":{"default":6,"min":3,"max":30},"props":{"title":{"type":"text","label":"Title","default":"From view to sign-up","maxLength":50},"rows":{"type":"list","label":"Stages (Label: count)","default":[{"label":"Watched","value":120000},{"label":"Clicked","value":18400},{"label":"Tried it","value":6200},{"label":"Signed up","value":2480}],"maxItems":7,"minItems":2,"of":{"label":"text","value":"number"}},"prefix":{"type":"text","label":"Before numbers","default":"","maxLength":3},"suffix":{"type":"text","label":"After numbers","default":"","maxLength":6},"decimals":{"type":"number","label":"Decimals","default":0,"min":0,"max":3}},"parts":{"ground":"the ground","title":"the heading","stages":"each stage","rates":"the share between stages"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/funnel","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const head = chartHead($, M, t, tin, p.title, A, k);\n s += head.s;\n const rows = p.rows || [];\n const n = rows.length;\n const room = A.y + A.h - head.y;\n const gap = 40 * k;\n const sh = Math.min((A.tall ? 200 : 130) * k, (room - gap * (n - 1)) / n);\n const maxW = A.wide ? A.w * 0.6 : A.w * 0.9, minW = maxW * 0.22;\n const top = rows[0] ? rows[0].value || 1 : 1;\n const cx = A.wide ? A.x + maxW / 2 + 20 * k : W / 2;\n let y = head.y + Math.max(0, (room - (n * sh + (n - 1) * gap)) / 2);\n const lf = { ...$.font(\"display\"), size: Math.min(38 * k, sh * 0.3) };\n const vf = { ...$.font(\"mono\"), size: Math.min(32 * k, sh * 0.26), weight: 700 };\n rows.forEach(function (r, i) {\n const at = tin + 0.4 + i * 0.25;\n const w1 = minW + (maxW - minW) * Math.sqrt(r.value / top);\n const next = rows[i + 1];\n const w2 = next ? minW + (maxW - minW) * Math.sqrt(next.value / top) : w1 * 0.92;\n const d = \"M\" + $.fx(cx - w1 / 2) + \" \" + $.fx(y) + \"L\" + $.fx(cx + w1 / 2) + \" \" + $.fx(y) + \"L\" + $.fx(cx + w2 / 2) + \" \" + $.fx(y + sh) + \"L\" + $.fx(cx - w2 / 2) + \" \" + $.fx(y + sh) + \"Z\";\n // Deeper stages mix toward the ground; their words take whichever ink reads on them.\n const col = $.mix(seriesColor($, 0), c.bg, i * (0.4 / Math.max(1, n - 1)));\n let st = $.path(d, { fill: col });\n const labelInside = A.wide;\n const val = fmtNum(r.value * $.E.out3($.seg(t, at, at + 0.8)), p.decimals, p.prefix, p.suffix);\n if (labelInside) {\n st += $.text(String(r.label), { ...lf, x: cx, y: y + sh / 2 - lf.size * 0.85, anchor: \"middle\", fill: $.on(col) });\n st += $.text(val, { ...vf, x: cx, y: y + sh / 2 + lf.size * 0.25, anchor: \"middle\", fill: $.on(col) });\n } else {\n // The label and its final count, sized to fit the stage's narrow end.\n const line = String(r.label) + \" \" + fmtNum(r.value, p.decimals, p.prefix, p.suffix);\n const ff = fitFont($, line, \"display\", Math.min(lf.size, sh * 0.28), w2 * 0.86);\n st += $.text(String(r.label) + \" \" + val, { ...ff, x: cx, y: y + sh / 2 - ff.size * 0.55, anchor: \"middle\", fill: $.on(col) });\n }\n s += M.part(\"stages\", { at: at, in: \"drop\", distance: 30, ease: \"bouncy\", out: \"fade\", box: { x: cx - w1 / 2, y: y, w: w1, h: sh } }, st);\n if (next) {\n const rate = Math.round((next.value / (r.value || 1)) * 1000) / 10;\n const rx = A.wide ? cx + maxW / 2 + 60 * k : cx;\n const ry = y + sh + gap / 2;\n s += M.part(\"rates\", { at: at + 0.3, in: \"fade\", dur: 0.3, out: \"fade\", box: { x: rx - 80 * k, y: ry - 20 * k, w: 160 * k, h: 40 * k } },\n A.wide ? $.text(\"↓ \" + rate + \"%\", { ...vf, x: rx, y: ry - vf.size * 0.55, fill: c.mute }) : $.text(rate + \"% carry on\", { ...$.font(\"mono\"), size: 22 * k, x: rx, y: ry - 11 * k, anchor: \"middle\", fill: c.mute }));\n }\n y += sh + gap;\n });\n return s;\n}\n","libs":[]},{"id":"world-map","name":"World map","description":"A world map in the brand's colours: chosen countries fill in one by one (shaded by their values when given) and the top ones are labelled. Zooms to a region.","category":"chart","tags":["map","world","countries","choropleth","geo"],"placement":"full","libs":["world"],"duration":{"default":6,"min":3,"max":30},"props":{"title":{"type":"text","label":"Title","default":"Where our viewers are","maxLength":50},"countries":{"type":"list","label":"Countries (Name: value)","default":[{"name":"India","value":42},{"name":"United States","value":31},{"name":"Brazil","value":18},{"name":"United Kingdom","value":12},{"name":"Indonesia","value":11},{"name":"Germany","value":9},{"name":"Nigeria","value":7},{"name":"Japan","value":6}],"maxItems":40,"minItems":1,"of":{"name":"text","value":"number"}},"region":{"type":"choice","label":"Region","options":["world","americas","europe","africa","asia","middle east","oceania","india"],"default":"world"},"labels":{"type":"number","label":"Countries labelled","default":4,"min":0,"max":12},"prefix":{"type":"text","label":"Before numbers","default":"","maxLength":3},"suffix":{"type":"text","label":"After numbers","default":"%","maxLength":6},"decimals":{"type":"number","label":"Decimals","default":0,"min":0,"max":3}},"parts":{"ground":"the ground","title":"the heading","map":"the map","highlights":"the chosen countries","labels":"the labels"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/world-map","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\nvar REGIONS = { world: [10, 12, 1], americas: [-82, 8, 1.75], europe: [14, 53, 4.2], africa: [18, 2, 2.1], asia: [92, 32, 1.9], \"middle east\": [44, 28, 4.4], oceania: [142, -26, 2.8], india: [80, 22, 5] };\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const head = chartHead($, M, t, tin, p.title, A, k);\n s += head.s;\n let rg = REGIONS[p.region] || REGIONS.world;\n // On narrow frames the whole world is a thin strip: frame the chosen countries instead.\n if (p.region === \"world\" && !A.wide) {\n const pts = (p.countries || []).map(function (r) { return worldFind(r.name); }).filter(Boolean);\n if (pts.length) {\n let sx = 0, sy = 0, lat = 0;\n pts.forEach(function (cn) { sx += Math.cos(cn.c[0] * Math.PI / 180); sy += Math.sin(cn.c[0] * Math.PI / 180); lat += cn.c[1]; });\n rg = [Math.atan2(sy, sx) * 180 / Math.PI, lat / pts.length, A.tall ? 1.9 : 1.3];\n }\n }\n // A slow push in while the countries fill.\n const zoom = rg[2] * (1 + 0.05 * $.E.inOut2($.seg(t, tin, $.dur)));\n const box = { x: A.x, y: head.y, w: A.w, h: A.y + A.h - head.y };\n const m = worldMap($, { x: box.x, y: box.y, w: box.w, h: box.h, center: [rg[0], rg[1]], zoom: zoom });\n const picked = (p.countries || []).map(function (r) { return { r: r, c: worldFind(r.name) }; }).filter(function (q) { return q.c; });\n const maxV = Math.max.apply(null, picked.map(function (q) { return q.r.value; }).concat([1e-9]));\n const base = $.rgba(c.fg, 0.13);\n let land = \"\";\n m.countries.forEach(function (cn) { land += $.path(m.path(cn), { fill: base, stroke: c.bg, sw: 0.8 * k }); });\n const clipId = m.clipId();\n const mapBox = '<clipPath id=\"mmbox\"><rect x=\"' + box.x + '\" y=\"' + box.y + '\" width=\"' + box.w + '\" height=\"' + box.h + '\"/></clipPath>';\n $.def(mapBox);\n s += '<g clip-path=\"url(#mmbox)\">' + M.part(\"map\", { at: tin + 0.1, in: \"fade\", dur: 0.6, out: \"fade\", vary: false }, '<g clip-path=\"url(#' + clipId + ')\">' + land + \"</g>\");\n // Countries fill in order of value, each shaded by its share of the largest.\n const order = picked.slice().sort(function (a, b) { return b.r.value - a.r.value; });\n let hi = \"\";\n order.forEach(function (q, i) {\n const at = tin + 0.6 + i * Math.min(0.18, 1.6 / order.length);\n const f = $.E.out3($.seg(t, at, at + 0.4));\n if (f <= 0) return;\n const shade = 0.35 + 0.65 * (q.r.value / maxV);\n hi += $.path(m.path(q.c), { fill: c.accent, op: shade * f, stroke: c.bg, sw: 0.8 * k });\n });\n s += M.part(\"highlights\", { at: tin + 0.5, in: \"none\", out: \"fade\" }, '<g clip-path=\"url(#' + clipId + ')\">' + hi + \"</g>\");\n // Labels for the top countries, at their label points, kept inside the map box.\n let labs = \"\";\n const lf = { ...$.font(\"display\"), size: 26 * k }, vf = { ...$.font(\"mono\"), size: 22 * k, weight: 700 };\n order.slice(0, Math.round(p.labels)).forEach(function (q, i) {\n const at = tin + 1.2 + i * 0.2;\n const pt = m.project(q.c.c[0], q.c.c[1]);\n if (pt[0] < box.x || pt[0] > box.x + box.w || pt[1] < box.y || pt[1] > box.y + box.h) return;\n const name = String(q.r.name), val = fmtNum(q.r.value, p.decimals, p.prefix, p.suffix);\n const w = Math.max($.measure(name, lf), $.measure(val, vf)) + 28 * k, h = lf.size * 1.25 + vf.size * 1.3 + 16 * k;\n const lx = Math.max(box.x, Math.min(box.x + box.w - w, pt[0] + 14 * k)), ly = Math.max(box.y, Math.min(box.y + box.h - h, pt[1] - h - 14 * k));\n labs += M.part(\"labels\", { at: at, in: \"pop\", origin: \"bottom-left\", out: \"fade\", box: { x: lx, y: ly, w: w, h: h } },\n $.circle(pt[0], pt[1], 7 * k, { fill: c.fg, stroke: c.bg, sw: 3 * k }),\n $.rect(lx, ly, w, h, { r: 10 * k, fill: c.surface, shadow: \"md\" }),\n $.text(name, { ...lf, x: lx + 14 * k, y: ly + 8 * k, fill: c.fg }),\n $.text(val, { ...vf, x: lx + 14 * k, y: ly + 8 * k + lf.size * 1.25, fill: c.accent }));\n });\n return s + labs + \"</g>\";\n}\n"},{"id":"flight","name":"Flight route","description":"A map frames two places; a route arcs from one to the other along the great circle while a plane flies it, and both ends are pinned and named.","category":"chart","tags":["map","travel","route","flight","geo"],"placement":"full","libs":["world"],"duration":{"default":6,"min":3,"max":30},"props":{"from":{"type":"text","label":"From (name)","default":"Lisbon","maxLength":24},"fromAt":{"type":"text","label":"From (lat, lon or country)","default":"38.72, -9.14","maxLength":32},"to":{"type":"text","label":"To (name)","default":"Mumbai","maxLength":24},"toAt":{"type":"text","label":"To (lat, lon or country)","default":"19.08, 72.88","maxLength":32},"note":{"type":"text","label":"Note","default":"8,154 km · 9 h 40 m","maxLength":32}},"parts":{"ground":"the ground","map":"the map","route":"the route","plane":"the plane","pins":"the two pins and names","note":"the note"},"beats":{"in":0.15,"out":-0.45},"pack":"data","ref":"data/flight","code":"// MONARC Data — helpers this pack's charts share.\n\n/** Series colours: the brand's two accents first, then fixed hues that sit well beside them. */\nfunction seriesColor($, i) {\n var list = [$.c.accent, $.c.accent2, \"#3DD6FF\", \"#7C5CFF\", \"#22C55E\", \"#F59E0B\", \"#EC4899\", \"#94A3B8\"];\n return list[i % list.length];\n}\n\n/** A number as a reader expects it: thousands separators, fixed decimals, a prefix and a suffix. */\nfunction fmtNum(v, dec, prefix, suffix) {\n var n = Number(v) || 0, d = Math.max(0, Math.min(3, Math.round(dec || 0)));\n var s = n.toFixed(d).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n if (d) { var parts = n.toFixed(d).split(\".\"); s = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + \".\" + parts[1]; }\n return (prefix || \"\") + s + (suffix || \"\");\n}\n\n/** Round \"nice\" steps for an axis that reaches max: 1, 2 or 5 × a power of ten. */\nfunction niceStep(max, count) {\n var raw = (max || 1) / Math.max(1, count || 4), pow = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), m = raw / pow;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\n\n/** A heading (and kicker) at the top of the safe area; returns the y below it. */\nfunction chartHead($, M, t, at, title, A, k) {\n if (!title) return { s: \"\", y: A.y };\n var tf = fitLines($, $.case(title), Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w, Math.min((A.tall ? 96 : 80) * k, A.h * 0.1), 40 * k, 2);\n var s = \"\";\n tf.lines.forEach(function (line, i) { s += M.text(\"title\", Object.assign({ at: at + i * 0.1, in: \"rise\", distance: 22 }, tf.font, { x: A.x, y: A.y + i * tf.size * 1.05, fill: $.c.fg }), line); });\n return { s: s, y: A.y + tf.lines.length * tf.size * 1.05 + 40 * k };\n}\n\n/** A donut or ring segment from angle a0 to a1 (radians, 0 = up, clockwise). */\nfunction arcPath(cx, cy, r0, r1, a0, a1) {\n var big = a1 - a0 > Math.PI ? 1 : 0;\n var p = function (r, a) { return (cx + r * Math.sin(a)).toFixed(2) + \" \" + (cy - r * Math.cos(a)).toFixed(2); };\n if (a1 - a0 >= Math.PI * 2 - 1e-4) a1 = a0 + Math.PI * 2 - 1e-4;\n return \"M\" + p(r1, a0) + \"A\" + r1 + \" \" + r1 + \" 0 \" + big + \" 1 \" + p(r1, a1) + \"L\" + p(r0, a1) + \"A\" + r0 + \" \" + r0 + \" 0 \" + big + \" 0 \" + p(r0, a0) + \"Z\";\n}\n\n/** \"38.7, -9.1\" → [lon, lat]; a country name → its label point. */\nfunction placeAt(s) {\n const m = /^\\s*(-?\\d+(?:\\.\\d+)?)\\s*[, ]\\s*(-?\\d+(?:\\.\\d+)?)\\s*$/.exec(String(s || \"\"));\n if (m) return [Number(m[2]), Number(m[1])];\n const cn = worldFind(s);\n return cn ? cn.c : [0, 0];\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.15);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const a = placeAt(p.fromAt), b = placeAt(p.toAt);\n // The great circle between them, as points (slerp on the unit sphere).\n const v = function (q) { const l = q[0] * Math.PI / 180, f = q[1] * Math.PI / 180; return [Math.cos(f) * Math.cos(l), Math.cos(f) * Math.sin(l), Math.sin(f)]; };\n const va = v(a), vb = v(b);\n const om = Math.acos(Math.max(-1, Math.min(1, va[0] * vb[0] + va[1] * vb[1] + va[2] * vb[2]))) || 1e-6;\n const gc = [];\n for (let i = 0; i <= 64; i++) {\n const f = i / 64, s1 = Math.sin((1 - f) * om) / Math.sin(om), s2 = Math.sin(f * om) / Math.sin(om);\n const x = s1 * va[0] + s2 * vb[0], y = s1 * va[1] + s2 * vb[1], z = s1 * va[2] + s2 * vb[2];\n gc.push([Math.atan2(y, x) * 180 / Math.PI, Math.asin(Math.max(-1, Math.min(1, z))) * 180 / Math.PI]);\n }\n // Frame both ends: centre between them (longitudes unwrapped), zoom to their spread.\n let lb = b[0];\n while (lb - a[0] > 180) lb -= 360;\n while (a[0] - lb > 180) lb += 360;\n const cen = [(a[0] + lb) / 2, (a[1] + b[1]) / 2 + 6];\n const box = { x: 0, y: 0, w: W, h: H };\n // Fit: project both ends at zoom 1, then zoom until they span 56% of the width or 45% of the height.\n const m1 = worldMap($, { x: 0, y: 0, w: W, h: H, center: cen, zoom: 1 });\n const pa = m1.project(a[0], a[1]), pb = m1.project(b[0], b[1]);\n const fit = Math.min((W * 0.56) / Math.max(1, Math.abs(pa[0] - pb[0])), (H * 0.45) / Math.max(1, Math.abs(pa[1] - pb[1])));\n const zoom = Math.min(8, Math.max(1, fit)) * (1 + 0.04 * $.E.inOut2($.seg(t, tin, $.dur)));\n const m = worldMap($, { x: box.x, y: box.y, w: box.w, h: box.h, center: cen, zoom: zoom });\n let land = \"\";\n m.countries.forEach(function (cn) { land += $.path(m.path(cn), { fill: $.rgba(c.fg, 0.14), stroke: c.bg, sw: 0.8 * k }); });\n s += M.part(\"map\", { at: tin, in: \"fade\", dur: 0.6, out: \"fade\", vary: false }, '<g clip-path=\"url(#' + m.clipId() + ')\">' + land + \"</g>\");\n // The route: projected points, lifted into an arc, drawn on as the plane flies.\n const pts = m.ring(gc);\n const P = pts.map(function (q, i) { const f = i / (pts.length - 1); return [q[0], q[1] - Math.sin(f * Math.PI) * Math.min(220 * k, Math.hypot(pts[pts.length - 1][0] - pts[0][0], pts[pts.length - 1][1] - pts[0][1]) * 0.22)]; });\n const t0 = tin + 0.9, t1 = $.beat(\"out\", $.dur - 0.45) - 0.9;\n const run = $.E.inOut2($.seg(t, t0, t1));\n const upto = run * (P.length - 1), iu = Math.floor(upto), fu = upto - iu;\n const drawn = P.slice(0, iu + 1);\n if (iu < P.length - 1) drawn.push([P[iu][0] + (P[iu + 1][0] - P[iu][0]) * fu, P[iu][1] + (P[iu + 1][1] - P[iu][1]) * fu]);\n const dd = drawn.map(function (q, i) { return (i ? \"L\" : \"M\") + $.fx(q[0]) + \" \" + $.fx(q[1]); }).join(\"\");\n const full = P.map(function (q, i) { return (i ? \"L\" : \"M\") + $.fx(q[0]) + \" \" + $.fx(q[1]); }).join(\"\");\n s += M.part(\"route\", { at: t0, in: \"none\", out: \"fade\" }, $.path(full, { stroke: $.rgba(c.fg, 0.2), sw: 3 * k, dash: 10 * k + \" \" + 10 * k, fill: \"none\" }), drawn.length > 1 ? $.path(dd, { stroke: c.accent, sw: 6 * k, fill: \"none\" }) : \"\");\n // The plane at the tip, pointing along the route.\n if (run > 0 && run < 1) {\n const tip = drawn[drawn.length - 1], prev = drawn[Math.max(0, drawn.length - 2)];\n const ang = Math.atan2(tip[1] - prev[1], tip[0] - prev[0]) * 180 / Math.PI;\n const sz = 30 * k;\n const plane = \"M\" + sz + \" 0L\" + -sz * 0.6 + \" \" + -sz * 0.75 + \"L\" + -sz * 0.35 + \" 0L\" + -sz * 0.6 + \" \" + sz * 0.75 + \"Z\";\n s += M.part(\"plane\", { at: t0, in: \"none\", out: \"fade\" }, $.g({ transform: \"translate(\" + $.fx(tip[0]) + \" \" + $.fx(tip[1]) + \") rotate(\" + $.fx(ang) + \")\" }, $.path(plane, { fill: c.fg, stroke: c.bg, sw: 2 * k })));\n }\n // Pins and names at both ends.\n const nf = { ...$.font(\"display\"), size: 40 * k };\n const pin = function (q, name, at, right) {\n const pr = 13 * k;\n return M.part(\"pins\", { at: at, in: \"pop\", out: \"fade\", box: { x: q[0] - pr, y: q[1] - pr, w: 2 * pr, h: 2 * pr } },\n $.circle(q[0], q[1], pr, { fill: c.accent, stroke: c.bg, sw: 4 * k }),\n $.text($.case(String(name)), { ...nf, x: q[0] + (right ? 1 : -1) * 26 * k, y: q[1] - nf.size * 0.55, anchor: right ? \"start\" : \"end\", fill: c.fg, shadow: \"sm\" }));\n };\n const ra = P[0], rb = P[P.length - 1];\n // Names face inward, toward the middle of the frame.\n s += pin(ra, p.from, tin + 0.5, ra[0] < W / 2);\n s += pin(rb, p.to, t1 - 0.1, rb[0] < W / 2);\n if (p.note) s += M.text(\"note\", { at: t1, in: \"rise\", distance: 16, out: \"fade\", ...$.font(\"mono\"), size: 30 * k, ls: 0.06, x: W / 2, y: A.y + A.h - 40 * k, anchor: \"middle\", fill: c.mute }, String(p.note));\n return s;\n}\n"}]},{"format":1,"id":"3d","name":"MONARC 3D","version":"1.0.0","description":"Extruded logos, titles and numbers, a card that flips, a word cube, a 3D bar chart and a globe — real depth, light and camera moves, rendered without a GPU, in any brand.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/3d","style":"ember","shared":"shared.js","items":["logo-3d","title-3d","number-3d","card-flip","cube","bars-3d","globe"],"entries":[{"id":"logo-3d","name":"3D logo","description":"An extruded wordmark swings in on a spring and settles as a light sweeps across its faces; the tagline rises beneath. Real depth, light and perspective.","category":"logo","tags":["3d","logo","sting","intro","brand","extruded"],"placement":"full","libs":["3d"],"duration":{"default":4,"min":2.5,"max":20},"props":{"wordmark":{"type":"text","label":"Wordmark","default":"MONARC","maxLength":16},"tagline":{"type":"text","label":"Tagline","default":"Made with motion","maxLength":40},"depth":{"type":"number","label":"Depth (share of the height)","default":0.3,"min":0.05,"max":1}},"parts":{"ground":"the ground","wordmark":"the 3D wordmark","tagline":"the tagline"},"beats":{"in":0.1,"out":-0.45},"pack":"3d","ref":"3d/logo-3d","code":"// MONARC 3D — helpers this pack's items share. The scenes are built with scene3d (the 3D kit).\n\n/** The focal length a scene3d camera with this field of view has on this canvas. */\nfunction focalFor($, fov) {\n return ($.H / 2) / Math.tan(((fov || 35) * Math.PI) / 360);\n}\n\n/** Lines of 3D text no wider than maxW (px at the target depth), by words. */\nfunction wrap3d(S, str, o, maxW, maxLines) {\n var words = String(str || \"\").split(/\\s+/).filter(Boolean), lines = [], cur = \"\";\n words.forEach(function (w) {\n var next = cur ? cur + \" \" + w : w;\n if (cur && S.measure(next, o) > maxW) { lines.push(cur); cur = w; } else cur = next;\n });\n if (cur) lines.push(cur);\n if (maxLines && lines.length > maxLines) { var rest = lines.slice(maxLines - 1).join(\" \"); lines = lines.slice(0, maxLines - 1).concat([rest]); }\n return lines;\n}\n\n/** The biggest size (≤ max) at which str fits maxW on one line, in 3D text. */\nfunction fit3d(S, str, o, maxW, max) {\n var w = S.measure(str, Object.assign({}, o, { size: 100 }));\n return w > 0 ? Math.min(max, (maxW / w) * 100) : max;\n}\n\n/** A soft contact shadow on the floor under a point (2D, drawn before the scene). */\nfunction floorShadow($, S, p, rx, op) {\n var q = S.project(p);\n if (!q) return \"\";\n var id = $.uid(\"fs\");\n $.def('<radialGradient id=\"' + id + '\"><stop offset=\"0\" stop-color=\"#000\" stop-opacity=\"' + (op || 0.45) + '\"/><stop offset=\"1\" stop-color=\"#000\" stop-opacity=\"0\"/></radialGradient>');\n return '<ellipse cx=\"' + $.fx(q.x) + '\" cy=\"' + $.fx(q.y) + '\" rx=\"' + $.fx(rx * q.s) + '\" ry=\"' + $.fx(rx * q.s * 0.18) + '\" fill=\"url(#' + id + ')\"/>';\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.1);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [W / 2, H / 2] }));\n const word = $.case(String(p.wordmark || \"\"));\n const face = $.font(\"display\");\n const cy = A.y + A.h * 0.46;\n const e = $.spring(Math.max(0, t - tin), 2.1, 6.2);\n // The light sweeps right across the letters as they settle.\n const sweep = $.seg(t, tin + 0.45, tin + 1.5);\n const lx = -1.4 + 2.8 * $.E.inOut2(sweep);\n const S = scene3d($, { camera: { fov: 30, yaw: -7 + 14 * $.seg(t, tin, $.dur), pitch: -5, y: cy }, light: { dir: [lx, 0.4, 0.85], spec: 0.2 + 0.7 * Math.sin(Math.PI * sweep), shine: 14 } });\n const size = fit3d(S, word, { face: face }, A.w * 0.82, Math.min(A.h * 0.3, 300 * k));\n const mesh = S.text(word, { face: face, size: size, depth: size * p.depth, color: c.fg, side: c.accent });\n S.add(mesh, { rotate: [10 * (1 - e), -78 * (1 - e), 0], scale: 0.88 + 0.12 * e });\n const shadow = floorShadow($, S, [0, size * 0.62, 0], mesh.w * 0.55, 0.4 * Math.min(1, e));\n s += M.part(\"wordmark\", { at: tin, in: \"fade\", dur: 0.2, out: \"zoom\", box: { x: W / 2 - mesh.w / 2, y: cy - size, w: mesh.w, h: size * 2 } }, shadow + S.render());\n if (p.tagline) {\n const tf = fitFont($, String(p.tagline).toUpperCase(), \"mono\", 34 * k, A.w * 0.8, { ls: 0.24 });\n s += M.text(\"tagline\", { at: tin + 1.1, in: \"rise\", distance: 18, out: \"fade\", ...tf, x: W / 2, y: cy + size * 0.75, anchor: \"middle\", fill: c.mute }, String(p.tagline).toUpperCase());\n }\n return s;\n}\n"},{"id":"title-3d","name":"3D title","description":"Extruded letters flip up one by one on springs while the camera eases in; the subtitle rises beneath.","category":"title","tags":["3d","title","chapter","kinetic","extruded"],"placement":"full","libs":["3d"],"duration":{"default":4.5,"min":2.5,"max":20},"props":{"title":{"type":"text","label":"Title","default":"Depth over reach","maxLength":28},"subtitle":{"type":"text","label":"Subtitle","default":"Chapter two","maxLength":40},"depth":{"type":"number","label":"Depth (share of the height)","default":0.22,"min":0.05,"max":1}},"parts":{"ground":"the ground","title":"the 3D title","subtitle":"the subtitle"},"beats":{"in":0.1,"out":-0.45},"pack":"3d","ref":"3d/title-3d","code":"// MONARC 3D — helpers this pack's items share. The scenes are built with scene3d (the 3D kit).\n\n/** The focal length a scene3d camera with this field of view has on this canvas. */\nfunction focalFor($, fov) {\n return ($.H / 2) / Math.tan(((fov || 35) * Math.PI) / 360);\n}\n\n/** Lines of 3D text no wider than maxW (px at the target depth), by words. */\nfunction wrap3d(S, str, o, maxW, maxLines) {\n var words = String(str || \"\").split(/\\s+/).filter(Boolean), lines = [], cur = \"\";\n words.forEach(function (w) {\n var next = cur ? cur + \" \" + w : w;\n if (cur && S.measure(next, o) > maxW) { lines.push(cur); cur = w; } else cur = next;\n });\n if (cur) lines.push(cur);\n if (maxLines && lines.length > maxLines) { var rest = lines.slice(maxLines - 1).join(\" \"); lines = lines.slice(0, maxLines - 1).concat([rest]); }\n return lines;\n}\n\n/** The biggest size (≤ max) at which str fits maxW on one line, in 3D text. */\nfunction fit3d(S, str, o, maxW, max) {\n var w = S.measure(str, Object.assign({}, o, { size: 100 }));\n return w > 0 ? Math.min(max, (maxW / w) * 100) : max;\n}\n\n/** A soft contact shadow on the floor under a point (2D, drawn before the scene). */\nfunction floorShadow($, S, p, rx, op) {\n var q = S.project(p);\n if (!q) return \"\";\n var id = $.uid(\"fs\");\n $.def('<radialGradient id=\"' + id + '\"><stop offset=\"0\" stop-color=\"#000\" stop-opacity=\"' + (op || 0.45) + '\"/><stop offset=\"1\" stop-color=\"#000\" stop-opacity=\"0\"/></radialGradient>');\n return '<ellipse cx=\"' + $.fx(q.x) + '\" cy=\"' + $.fx(q.y) + '\" rx=\"' + $.fx(rx * q.s) + '\" ry=\"' + $.fx(rx * q.s * 0.18) + '\" fill=\"url(#' + id + ')\"/>';\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.1);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [W * 0.3, H * 0.3] }));\n const face = $.font(\"display\");\n const title = $.case(String(p.title || \"\"));\n const cy = A.y + A.h * 0.45;\n const fov = 32, f = focalFor($, fov);\n const S = scene3d($, { camera: { fov: fov, distance: f * (1.18 - 0.18 * $.E.out3($.seg(t, tin, $.dur))), pitch: -8, y: cy }, light: { spec: 0.35 } });\n // One line when it holds at a good size, else two.\n let lines = [title];\n let size = fit3d(S, title, { face: face }, A.w * 0.86, Math.min(A.h * 0.24, 260 * k));\n if (size < Math.min(A.h * 0.24, 260 * k) * 0.62) {\n lines = wrap3d(S, title, { face: face, size: 100 }, (A.w * 0.86 * 100) / (Math.min(A.h * 0.2, 220 * k)), 2);\n size = Math.min.apply(null, lines.map(function (l) { return fit3d(S, l, { face: face }, A.w * 0.86, Math.min(A.h * 0.2, 220 * k)); }));\n }\n let n0 = 0;\n lines.forEach(function (line, li) {\n const base = n0;\n const mesh = S.text(line, { face: face, size: size, depth: size * p.depth, color: c.fg, side: c.accent, each: function (i) {\n const at = tin + 0.25 + (base + i) * 0.055;\n if (t < at) return { hide: true };\n const e = $.spring(t - at, 2.4, 6.8);\n return { rotate: [-100 * (1 - e), 0, 0], offset: [0, size * 0.25 * (1 - Math.min(1, e)), 0] };\n } });\n S.add(mesh, { position: [0, (li - (lines.length - 1) / 2) * size * 1.12, 0] });\n n0 += Array.from(line).length;\n });\n s += M.part(\"title\", { at: tin, in: \"fade\", dur: 0.2, out: \"blur\", box: { x: A.x, y: cy - size * lines.length, w: A.w, h: size * lines.length * 2 } }, S.render());\n if (p.subtitle) {\n const sf = fitFont($, $.case(String(p.subtitle)), \"text\", 46 * k, A.w * 0.8);\n s += M.text(\"subtitle\", { at: tin + 0.4 + n0 * 0.055, in: \"rise\", distance: 20, out: \"fade\", ...sf, x: W / 2, y: cy + size * (lines.length * 0.56 + 0.35), anchor: \"middle\", fill: c.mute }, $.case(String(p.subtitle)));\n }\n return s;\n}\n"},{"id":"number-3d","name":"3D number","description":"A big extruded number counts up, tilted toward the camera, with a light sweep when it lands and the label beneath.","category":"stat","tags":["3d","number","stat","counter","extruded"],"placement":"full","libs":["3d"],"duration":{"default":4,"min":2.5,"max":20},"props":{"value":{"type":"number","label":"Value","default":3.2,"min":-1000000000000,"max":1000000000000},"decimals":{"type":"number","label":"Decimals","default":1,"min":0,"max":3},"prefix":{"type":"text","label":"Before","default":"","maxLength":3},"suffix":{"type":"text","label":"After","default":"×","maxLength":4},"label":{"type":"text","label":"Label","default":"more videos shipped","maxLength":48}},"parts":{"ground":"the ground","number":"the 3D number","label":"the label"},"beats":{"in":0.1,"out":-0.45},"pack":"3d","ref":"3d/number-3d","code":"// MONARC 3D — helpers this pack's items share. The scenes are built with scene3d (the 3D kit).\n\n/** The focal length a scene3d camera with this field of view has on this canvas. */\nfunction focalFor($, fov) {\n return ($.H / 2) / Math.tan(((fov || 35) * Math.PI) / 360);\n}\n\n/** Lines of 3D text no wider than maxW (px at the target depth), by words. */\nfunction wrap3d(S, str, o, maxW, maxLines) {\n var words = String(str || \"\").split(/\\s+/).filter(Boolean), lines = [], cur = \"\";\n words.forEach(function (w) {\n var next = cur ? cur + \" \" + w : w;\n if (cur && S.measure(next, o) > maxW) { lines.push(cur); cur = w; } else cur = next;\n });\n if (cur) lines.push(cur);\n if (maxLines && lines.length > maxLines) { var rest = lines.slice(maxLines - 1).join(\" \"); lines = lines.slice(0, maxLines - 1).concat([rest]); }\n return lines;\n}\n\n/** The biggest size (≤ max) at which str fits maxW on one line, in 3D text. */\nfunction fit3d(S, str, o, maxW, max) {\n var w = S.measure(str, Object.assign({}, o, { size: 100 }));\n return w > 0 ? Math.min(max, (maxW / w) * 100) : max;\n}\n\n/** A soft contact shadow on the floor under a point (2D, drawn before the scene). */\nfunction floorShadow($, S, p, rx, op) {\n var q = S.project(p);\n if (!q) return \"\";\n var id = $.uid(\"fs\");\n $.def('<radialGradient id=\"' + id + '\"><stop offset=\"0\" stop-color=\"#000\" stop-opacity=\"' + (op || 0.45) + '\"/><stop offset=\"1\" stop-color=\"#000\" stop-opacity=\"0\"/></radialGradient>');\n return '<ellipse cx=\"' + $.fx(q.x) + '\" cy=\"' + $.fx(q.y) + '\" rx=\"' + $.fx(rx * q.s) + '\" ry=\"' + $.fx(rx * q.s * 0.18) + '\" fill=\"url(#' + id + ')\"/>';\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.1);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [W / 2, H * 0.45] }));\n const dec = Math.max(0, Math.min(3, Math.round(p.decimals)));\n const fmt = function (v) { const s2 = v.toFixed(dec); const parts = s2.split(\".\"); return (p.prefix || \"\") + parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\") + (parts[1] ? \".\" + parts[1] : \"\") + (p.suffix || \"\"); };\n const face = $.font(\"display\");\n const cy = A.y + A.h * 0.42;\n const land = tin + 1.4;\n const sweep = $.seg(t, land, land + 0.9);\n const S = scene3d($, { camera: { fov: 30, yaw: -16 + 10 * $.E.out3($.seg(t, tin, $.dur)), pitch: -10, y: cy }, light: { dir: [-1.2 + 2.4 * sweep, 0.4, 0.85], spec: 0.2 + 0.6 * Math.sin(Math.PI * sweep), shine: 14 } });\n const final = fmt(p.value);\n const size = fit3d(S, final, { face: face }, A.w * 0.8, Math.min(A.h * 0.36, 380 * k));\n const v = p.value * $.E.out3($.seg(t, tin + 0.2, land));\n const e = $.spring(Math.max(0, t - tin), 2, 7);\n S.add(S.text(fmt(v), { face: face, size: size, depth: size * 0.28, color: c.accent, side: $.mix(c.accent, c.bg, 0.45) }), { rotate: [14 * (1 - e) + 8, 0, 0], scale: 0.8 + 0.2 * e });\n s += M.part(\"number\", { at: tin, in: \"fade\", dur: 0.2, out: \"zoom\", box: { x: A.x, y: cy - size, w: A.w, h: size * 2 } }, floorShadow($, S, [0, size * 0.6, 0], S.measure(final, { face: face, size: size }) * 0.5, 0.35) + S.render());\n if (p.label) {\n const lf = fitFont($, $.case(String(p.label)), \"text\", 50 * k, A.w * 0.86);\n s += M.text(\"label\", { at: land - 0.2, in: \"rise\", distance: 20, out: \"fade\", ...lf, x: W / 2, y: cy + size * 0.78, anchor: \"middle\", fill: c.fg }, $.case(String(p.label)));\n }\n return s;\n}\n"},{"id":"card-flip","name":"3D card flip","description":"A card with real thickness tilts in, holds its front, then flips over to its back. Good for before/after, question/answer, myth/fact.","category":"scene","tags":["3d","card","flip","reveal","before after"],"placement":"full","libs":["3d"],"faces3d":["display","text"],"duration":{"default":5,"min":3,"max":20},"props":{"frontTitle":{"type":"text","label":"Front","default":"Before","maxLength":24},"frontText":{"type":"text","label":"Front line","default":"9 hours in the timeline","maxLength":60},"backTitle":{"type":"text","label":"Back","default":"After","maxLength":24},"backText":{"type":"text","label":"Back line","default":"One sentence to the agent","maxLength":60},"flipAt":{"type":"number","label":"Flip at (s)","default":2.1,"min":0.5,"max":20}},"parts":{"ground":"the ground","card":"the card"},"beats":{"in":0.1,"out":-0.45},"pack":"3d","ref":"3d/card-flip","code":"// MONARC 3D — helpers this pack's items share. The scenes are built with scene3d (the 3D kit).\n\n/** The focal length a scene3d camera with this field of view has on this canvas. */\nfunction focalFor($, fov) {\n return ($.H / 2) / Math.tan(((fov || 35) * Math.PI) / 360);\n}\n\n/** Lines of 3D text no wider than maxW (px at the target depth), by words. */\nfunction wrap3d(S, str, o, maxW, maxLines) {\n var words = String(str || \"\").split(/\\s+/).filter(Boolean), lines = [], cur = \"\";\n words.forEach(function (w) {\n var next = cur ? cur + \" \" + w : w;\n if (cur && S.measure(next, o) > maxW) { lines.push(cur); cur = w; } else cur = next;\n });\n if (cur) lines.push(cur);\n if (maxLines && lines.length > maxLines) { var rest = lines.slice(maxLines - 1).join(\" \"); lines = lines.slice(0, maxLines - 1).concat([rest]); }\n return lines;\n}\n\n/** The biggest size (≤ max) at which str fits maxW on one line, in 3D text. */\nfunction fit3d(S, str, o, maxW, max) {\n var w = S.measure(str, Object.assign({}, o, { size: 100 }));\n return w > 0 ? Math.min(max, (maxW / w) * 100) : max;\n}\n\n/** A soft contact shadow on the floor under a point (2D, drawn before the scene). */\nfunction floorShadow($, S, p, rx, op) {\n var q = S.project(p);\n if (!q) return \"\";\n var id = $.uid(\"fs\");\n $.def('<radialGradient id=\"' + id + '\"><stop offset=\"0\" stop-color=\"#000\" stop-opacity=\"' + (op || 0.45) + '\"/><stop offset=\"1\" stop-color=\"#000\" stop-opacity=\"0\"/></radialGradient>');\n return '<ellipse cx=\"' + $.fx(q.x) + '\" cy=\"' + $.fx(q.y) + '\" rx=\"' + $.fx(rx * q.s) + '\" ry=\"' + $.fx(rx * q.s * 0.18) + '\" fill=\"url(#' + id + ')\"/>';\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.1);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [W / 2, H * 0.4] }));\n const cw = A.wide ? A.w * 0.58 : A.tall ? A.w * 0.92 : A.w * 0.78;\n const ch = A.wide ? cw * 0.56 : A.tall ? cw * 1.05 : cw * 0.8;\n const d = 18 * k;\n const cy = A.y + A.h * 0.5;\n const S = scene3d($, { camera: { fov: 30, pitch: -6, y: cy }, light: { spec: 0.3 } });\n // In: tilted back, settling on a spring. Flip: a half turn with a lift toward the camera.\n const e = $.spring(Math.max(0, t - tin), 2, 6.5);\n const fl = $.E.inOut3($.seg(t, p.flipAt, p.flipAt + 0.9));\n const card = { rotate: [42 * (1 - e) + 12 * Math.sin(Math.PI * fl), 180 * fl - 14 * (1 - e), 0], position: [0, 80 * k * (1 - e), -140 * k * Math.sin(Math.PI * fl)] };\n S.add(S.card(cw, ch, { depth: d, radius: 36 * k, color: c.surface, back: c.accent, side: $.mix(c.accent, c.bg, 0.35) }), card);\n const side = function (title, line, back) {\n const ink = back ? $.on(c.accent) : c.fg;\n const tface = $.font(\"display\"), lface = $.font(\"text\");\n const ts = fit3d(S, title, { face: tface }, cw * 0.8, ch * 0.32);\n const ls = Math.min(ts * 0.34, ch * 0.09);\n const lines = wrap3d(S, line, { face: lface, size: ls }, cw * 0.8, 2);\n const z = back ? d / 2 + 1.5 : -d / 2 - 1.5;\n const ry = back ? 180 : 0;\n const total = ts + (lines.length ? ts * 0.35 + lines.length * ls * 1.3 : 0);\n let y = -total / 2 + ts / 2;\n S.add(S.text(title, { face: tface, size: ts, depth: 0, color: ink }), { position: [0, y, z], rotate: [0, ry, 0], parent: card, flat: true, decal: true });\n y += ts / 2 + ts * 0.35 + ls / 2;\n lines.forEach(function (l, i) { S.add(S.text(l, { face: lface, size: ls, depth: 0, color: ink }), { position: [0, y + i * ls * 1.3, z], rotate: [0, ry, 0], parent: card, flat: true, decal: true }); });\n };\n side($.case(String(p.frontTitle || \"\")), String(p.frontText || \"\"), false);\n side($.case(String(p.backTitle || \"\")), String(p.backText || \"\"), true);\n s += M.part(\"card\", { at: tin, in: \"fade\", dur: 0.25, out: \"zoom\", box: { x: W / 2 - cw / 2, y: cy - ch / 2, w: cw, h: ch } }, floorShadow($, S, [0, ch * 0.62, 0], cw * 0.5, 0.35) + S.render());\n return s;\n}\n"},{"id":"cube","name":"3D word cube","description":"A cube turns a quarter at a time on a spring, each side showing the next word — a kinetic way through a list of 2 to 4 words.","category":"text","tags":["3d","cube","words","kinetic","rotate"],"placement":"full","libs":["3d"],"duration":{"default":5,"min":2.5,"max":30},"props":{"lead":{"type":"text","label":"Lead-in","default":"Edit","maxLength":20},"words":{"type":"list","label":"Words (2–4)","default":["faster","smarter","together","anywhere"],"maxItems":4,"minItems":2,"of":"text"}},"parts":{"ground":"the ground","lead":"the lead-in","cube":"the cube"},"beats":{"in":0.1,"out":-0.45},"pack":"3d","ref":"3d/cube","code":"// MONARC 3D — helpers this pack's items share. The scenes are built with scene3d (the 3D kit).\n\n/** The focal length a scene3d camera with this field of view has on this canvas. */\nfunction focalFor($, fov) {\n return ($.H / 2) / Math.tan(((fov || 35) * Math.PI) / 360);\n}\n\n/** Lines of 3D text no wider than maxW (px at the target depth), by words. */\nfunction wrap3d(S, str, o, maxW, maxLines) {\n var words = String(str || \"\").split(/\\s+/).filter(Boolean), lines = [], cur = \"\";\n words.forEach(function (w) {\n var next = cur ? cur + \" \" + w : w;\n if (cur && S.measure(next, o) > maxW) { lines.push(cur); cur = w; } else cur = next;\n });\n if (cur) lines.push(cur);\n if (maxLines && lines.length > maxLines) { var rest = lines.slice(maxLines - 1).join(\" \"); lines = lines.slice(0, maxLines - 1).concat([rest]); }\n return lines;\n}\n\n/** The biggest size (≤ max) at which str fits maxW on one line, in 3D text. */\nfunction fit3d(S, str, o, maxW, max) {\n var w = S.measure(str, Object.assign({}, o, { size: 100 }));\n return w > 0 ? Math.min(max, (maxW / w) * 100) : max;\n}\n\n/** A soft contact shadow on the floor under a point (2D, drawn before the scene). */\nfunction floorShadow($, S, p, rx, op) {\n var q = S.project(p);\n if (!q) return \"\";\n var id = $.uid(\"fs\");\n $.def('<radialGradient id=\"' + id + '\"><stop offset=\"0\" stop-color=\"#000\" stop-opacity=\"' + (op || 0.45) + '\"/><stop offset=\"1\" stop-color=\"#000\" stop-opacity=\"0\"/></radialGradient>');\n return '<ellipse cx=\"' + $.fx(q.x) + '\" cy=\"' + $.fx(q.y) + '\" rx=\"' + $.fx(rx * q.s) + '\" ry=\"' + $.fx(rx * q.s * 0.18) + '\" fill=\"url(#' + id + ')\"/>';\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.1);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const words = (p.words || []).map(function (w) { return $.case(String(w)); });\n const n = Math.max(2, words.length);\n const face = $.font(\"display\");\n const lead = $.case(String(p.lead || \"\"));\n // A long box that rolls about its horizontal axis: four faces as wide as the longest word.\n const probe = scene3d($, {});\n const box = A.wide ? Math.min(A.h * 0.36, 360 * k) : Math.min(A.w * 0.42, 380 * k);\n const cw = Math.min(A.w * (A.wide && lead ? 0.6 : 0.88), Math.max.apply(null, words.map(function (w) { return probe.measure(w, { face: face, size: box * 0.6 }); })) + box * 0.5);\n const lf = fitFont($, lead, \"display\", box * 0.5, A.w * 0.9);\n const lw = lead ? $.measure(lead, lf) : 0;\n const stack = !A.wide;\n const cx = stack ? W / 2 : W / 2 + (lw + 30 * k) / 2;\n const cy = A.y + A.h * (stack ? 0.55 : 0.5);\n const S = scene3d($, { camera: { fov: 30, pitch: -6, yaw: -16, x: cx, y: cy }, light: { spec: 0.25 } });\n // A quarter roll for each word after the first, on springs, evenly through the hold.\n const span = Math.max(0.8, $.beat(\"out\", $.dur - 0.45) - tin - 0.8);\n const step = span / n;\n let turn = 0;\n for (let i = 1; i < n; i++) turn += Math.min(1.08, $.spring(Math.max(0, t - (tin + 0.5 + i * step)), 2.2, 7));\n const cube = { rotate: [90 * turn, 0, 0], position: [0, 0, 0] };\n const hw = cw / 2, hh = box / 2, hd = box / 2;\n const quad = function (pts, nrm, col) { return { loops: [pts], n: nrm, color: col, role: \"side\" }; };\n const faceCol = function (i) { return i % 2 ? c.accent : c.surface; };\n const faces = [\n quad([[-hw, -hh, -hd], [hw, -hh, -hd], [hw, hh, -hd], [-hw, hh, -hd]], [0, 0, -1], faceCol(0)),\n quad([[-hw, -hh, hd], [hw, -hh, hd], [hw, -hh, -hd], [-hw, -hh, -hd]], [0, -1, 0], faceCol(1)),\n quad([[hw, -hh, hd], [-hw, -hh, hd], [-hw, hh, hd], [hw, hh, hd]], [0, 0, 1], faceCol(2)),\n quad([[-hw, hh, -hd], [hw, hh, -hd], [hw, hh, hd], [-hw, hh, hd]], [0, 1, 0], faceCol(3)),\n quad([[hw, -hh, -hd], [hw, -hh, hd], [hw, hh, hd], [hw, hh, -hd]], [1, 0, 0], $.mix(c.surface, c.bg, 0.45)),\n quad([[-hw, -hh, hd], [-hw, -hh, -hd], [-hw, hh, -hd], [-hw, hh, hd]], [-1, 0, 0], $.mix(c.surface, c.bg, 0.45)),\n ];\n S.add({ faces: faces }, cube);\n // Word i sits on the face that rolls to the front after i quarter turns: the front face turned back by i quarters.\n words.slice(0, 4).forEach(function (w, i) {\n const size = fit3d(S, w, { face: face }, cw * 0.84, box * 0.56);\n const ink = i % 2 ? $.on(c.accent) : c.fg;\n S.add(S.text(w, { face: face, size: size, depth: 0, color: ink }), { position: [0, 0, -hd - 1.5], parent: { rotate: [-90 * i, 0, 0], parent: cube }, flat: true, decal: true });\n });\n s += M.part(\"cube\", { at: tin, in: \"pop\", out: \"zoom\", box: { x: cx - hw, y: cy - hh, w: cw, h: box } }, S.render());\n if (lead) s += M.text(\"lead\", { at: tin + 0.15, in: \"rise\", distance: 20, out: \"fade\", ...lf, x: stack ? W / 2 : cx - hw - 40 * k, y: stack ? cy - hh - lf.size * 1.6 : cy - lf.size * 0.55, anchor: stack ? \"middle\" : \"end\", fill: c.fg }, lead);\n return s;\n}\n"},{"id":"bars-3d","name":"3D bar chart","description":"Bars rise from a floor one after another while the camera orbits; values count up on their tops and the leader takes the accent.","category":"chart","tags":["3d","chart","bars","data","orbit"],"placement":"full","libs":["3d"],"duration":{"default":6,"min":3,"max":30},"props":{"title":{"type":"text","label":"Title","default":"Videos shipped per week","maxLength":50},"rows":{"type":"list","label":"Rows (Label: value)","default":[{"label":"Jan","value":4},{"label":"Feb","value":6},{"label":"Mar","value":9},{"label":"Apr","value":13},{"label":"May","value":21}],"maxItems":8,"minItems":2,"of":{"label":"text","value":"number"}},"suffix":{"type":"text","label":"After numbers","default":"","maxLength":6}},"parts":{"ground":"the ground","title":"the heading","bars":"the bars and floor","labels":"the names and values"},"beats":{"in":0.1,"out":-0.45},"pack":"3d","ref":"3d/bars-3d","code":"// MONARC 3D — helpers this pack's items share. The scenes are built with scene3d (the 3D kit).\n\n/** The focal length a scene3d camera with this field of view has on this canvas. */\nfunction focalFor($, fov) {\n return ($.H / 2) / Math.tan(((fov || 35) * Math.PI) / 360);\n}\n\n/** Lines of 3D text no wider than maxW (px at the target depth), by words. */\nfunction wrap3d(S, str, o, maxW, maxLines) {\n var words = String(str || \"\").split(/\\s+/).filter(Boolean), lines = [], cur = \"\";\n words.forEach(function (w) {\n var next = cur ? cur + \" \" + w : w;\n if (cur && S.measure(next, o) > maxW) { lines.push(cur); cur = w; } else cur = next;\n });\n if (cur) lines.push(cur);\n if (maxLines && lines.length > maxLines) { var rest = lines.slice(maxLines - 1).join(\" \"); lines = lines.slice(0, maxLines - 1).concat([rest]); }\n return lines;\n}\n\n/** The biggest size (≤ max) at which str fits maxW on one line, in 3D text. */\nfunction fit3d(S, str, o, maxW, max) {\n var w = S.measure(str, Object.assign({}, o, { size: 100 }));\n return w > 0 ? Math.min(max, (maxW / w) * 100) : max;\n}\n\n/** A soft contact shadow on the floor under a point (2D, drawn before the scene). */\nfunction floorShadow($, S, p, rx, op) {\n var q = S.project(p);\n if (!q) return \"\";\n var id = $.uid(\"fs\");\n $.def('<radialGradient id=\"' + id + '\"><stop offset=\"0\" stop-color=\"#000\" stop-opacity=\"' + (op || 0.45) + '\"/><stop offset=\"1\" stop-color=\"#000\" stop-opacity=\"0\"/></radialGradient>');\n return '<ellipse cx=\"' + $.fx(q.x) + '\" cy=\"' + $.fx(q.y) + '\" rx=\"' + $.fx(rx * q.s) + '\" ry=\"' + $.fx(rx * q.s * 0.18) + '\" fill=\"url(#' + id + ')\"/>';\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.1);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const title = $.case(String(p.title || \"\"));\n let top = A.y;\n if (title) {\n const tf = fitLines($, title, { ...$.font(\"display\"), ls: -0.02 }, A.w, Math.min(84 * k, A.h * 0.09), 40 * k, 2);\n tf.lines.forEach(function (l, i) { s += M.text(\"title\", { at: tin + i * 0.1, in: \"rise\", distance: 20, out: \"fade\", ...tf.font, x: A.x, y: A.y + i * tf.size * 1.05, fill: c.fg }, l); });\n top = A.y + tf.lines.length * tf.size * 1.05 + 30 * k;\n }\n const rows = p.rows || [];\n const n = rows.length;\n const maxV = Math.max.apply(null, rows.map(function (r) { return r.value; }).concat([1e-9]));\n const span = A.w * (A.wide ? 0.78 : 0.9);\n const gap = span / n, bw = gap * 0.56;\n const maxH = (A.y + A.h - top) * 0.62;\n const cy = top + (A.y + A.h - top) * 0.52;\n const floorY = maxH / 2;\n const S = scene3d($, { camera: { fov: 32, yaw: -26 + 14 * $.E.inOut2($.seg(t, tin, $.dur)), pitch: -20, y: cy }, light: { spec: 0.22 } });\n // The floor: a thin slab under the bars.\n S.add(S.box(span + gap * 0.4, 6 * k, gap * 1.4, { color: $.mix(c.bg, c.fg, 0.1) }), { position: [0, floorY + 3 * k, 0], rotate: [0, 0, 0] });\n let lab = \"\";\n rows.forEach(function (r, i) {\n const at = tin + 0.4 + i * 0.16;\n const e = $.spring(Math.max(0, t - at), 2.2, 7.5);\n const h = Math.max(2 * k, maxH * (r.value / maxV) * e);\n const x = -span / 2 + gap * (i + 0.5);\n const lead = r.value === maxV;\n S.add(S.box(bw, h, bw, { color: lead ? c.accent : $.mix(c.fg, c.bg, 0.45) }), { position: [x, floorY - h / 2, 0] });\n S.at([x, floorY + 18 * k, -bw / 2], function (px, py, sc) { return $.text(String(r.label), { ...$.font(\"text\"), size: 30 * k, x: px, y: py, anchor: \"middle\", fill: c.mute }); });\n if (t > at) S.at([x, floorY - h - 14 * k, 0], function (px, py, sc) { return $.text(Math.round(r.value * Math.min(1, e)) + (p.suffix || \"\"), { ...$.font(\"display\"), size: 36 * k, x: px, y: py - 40 * k, anchor: \"middle\", fill: lead ? c.accent : c.fg }); }, { onTop: true });\n });\n s += M.part(\"bars\", { at: tin + 0.2, in: \"fade\", dur: 0.3, out: \"fade\", box: { x: A.x, y: top, w: A.w, h: A.y + A.h - top } }, S.render());\n return s + lab;\n}\n"},{"id":"globe","name":"3D globe","description":"A lit globe spins to a country, which lights up in the accent; routes arc in from other countries and a pin names the place.","category":"scene","tags":["3d","globe","map","world","geo","travel"],"placement":"full","libs":["3d","world"],"duration":{"default":6,"min":3,"max":30},"props":{"target":{"type":"text","label":"Country","default":"India","maxLength":40},"label":{"type":"text","label":"Label","default":"Mumbai studio","maxLength":28},"from":{"type":"list","label":"Routes from","default":["United States","Brazil","United Kingdom","Australia"],"maxItems":8,"of":"text"},"title":{"type":"text","label":"Title","default":"","maxLength":50}},"parts":{"ground":"the ground","title":"the heading","globe":"the globe","routes":"the routes","pin":"the pin and label"},"beats":{"in":0.1,"out":-0.45},"pack":"3d","ref":"3d/globe","code":"// MONARC 3D — helpers this pack's items share. The scenes are built with scene3d (the 3D kit).\n\n/** The focal length a scene3d camera with this field of view has on this canvas. */\nfunction focalFor($, fov) {\n return ($.H / 2) / Math.tan(((fov || 35) * Math.PI) / 360);\n}\n\n/** Lines of 3D text no wider than maxW (px at the target depth), by words. */\nfunction wrap3d(S, str, o, maxW, maxLines) {\n var words = String(str || \"\").split(/\\s+/).filter(Boolean), lines = [], cur = \"\";\n words.forEach(function (w) {\n var next = cur ? cur + \" \" + w : w;\n if (cur && S.measure(next, o) > maxW) { lines.push(cur); cur = w; } else cur = next;\n });\n if (cur) lines.push(cur);\n if (maxLines && lines.length > maxLines) { var rest = lines.slice(maxLines - 1).join(\" \"); lines = lines.slice(0, maxLines - 1).concat([rest]); }\n return lines;\n}\n\n/** The biggest size (≤ max) at which str fits maxW on one line, in 3D text. */\nfunction fit3d(S, str, o, maxW, max) {\n var w = S.measure(str, Object.assign({}, o, { size: 100 }));\n return w > 0 ? Math.min(max, (maxW / w) * 100) : max;\n}\n\n/** A soft contact shadow on the floor under a point (2D, drawn before the scene). */\nfunction floorShadow($, S, p, rx, op) {\n var q = S.project(p);\n if (!q) return \"\";\n var id = $.uid(\"fs\");\n $.def('<radialGradient id=\"' + id + '\"><stop offset=\"0\" stop-color=\"#000\" stop-opacity=\"' + (op || 0.45) + '\"/><stop offset=\"1\" stop-color=\"#000\" stop-opacity=\"0\"/></radialGradient>');\n return '<ellipse cx=\"' + $.fx(q.x) + '\" cy=\"' + $.fx(q.y) + '\" rx=\"' + $.fx(rx * q.s) + '\" ry=\"' + $.fx(rx * q.s * 0.18) + '\" fill=\"url(#' + id + ')\"/>';\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const A = safeArea($);\n const M = motion($, t);\n const k = $.k;\n const tin = $.beat(\"in\", 0.1);\n let s = M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H }, glow: [W / 2, H / 2] }));\n let top = A.y;\n if (p.title) {\n const tf = fitFont($, $.case(String(p.title)), \"display\", 72 * k, A.w);\n s += M.text(\"title\", { at: tin, in: \"rise\", distance: 20, out: \"fade\", ...tf, x: W / 2, y: A.y, anchor: \"middle\", fill: c.fg }, $.case(String(p.title)));\n top = A.y + tf.size * 1.4;\n }\n const target = worldFind(p.target) || worldFind(\"India\");\n const R = Math.min(A.w, A.y + A.h - top) * 0.4;\n const cy = top + (A.y + A.h - top) / 2;\n const S = scene3d($, { camera: { fov: 28, y: cy }, light: { dir: [0.45, 0.35, 0.82], spec: 0.15 } });\n // Spin from well behind to the country, easing in; tilt toward its latitude.\n const arrive = tin + 2.2;\n const g = $.E.inOut3($.seg(t, tin, arrive));\n const spin = -target.c[0] - 150 * (1 - g) - 6 * $.seg(t, arrive, $.dur);\n const tilt = Math.max(-35, Math.min(35, target.c[1])) * g + 12 * (1 - g);\n S.add(S.sphere(R, { color: $.mix(c.bg, c.accent, 0.1) }), { position: [0, 0, 0] });\n const lit = $.seg(t, arrive - 0.3, arrive + 0.3);\n worldGlobe(S, { r: R, spin: spin, tilt: tilt, colorOf: function (cn) { return cn === target ? $.mix($.mix(c.fg, c.bg, 0.55), c.accent, lit) : $.mix(c.fg, c.bg, 0.55); } });\n // Routes: great circles from each country to the target, lifted off the surface as they travel.\n const tp = target.c;\n (p.from || []).forEach(function (name, i) {\n const src = worldFind(name);\n if (!src || src === target) return;\n const at = arrive + 0.1 + i * 0.25;\n const run = $.E.inOut2($.seg(t, at, at + 1));\n if (run <= 0) return;\n const v = function (q) { const l = q[0] * Math.PI / 180, f = q[1] * Math.PI / 180; return [Math.cos(f) * Math.cos(l), Math.cos(f) * Math.sin(l), Math.sin(f)]; };\n const va = v(src.c), vb = v(tp);\n const om = Math.acos(Math.max(-1, Math.min(1, va[0] * vb[0] + va[1] * vb[1] + va[2] * vb[2]))) || 1e-6;\n const pts = [];\n const steps = 40;\n for (let j = 0; j <= Math.round(steps * run); j++) {\n const f = j / steps, s1 = Math.sin((1 - f) * om) / Math.sin(om), s2 = Math.sin(f * om) / Math.sin(om);\n const x = s1 * va[0] + s2 * vb[0], y = s1 * va[1] + s2 * vb[1], z = s1 * va[2] + s2 * vb[2];\n const lon = Math.atan2(y, x) * 180 / Math.PI, lat = Math.asin(Math.max(-1, Math.min(1, z))) * 180 / Math.PI;\n pts.push(worldOnSphere(lon, lat, R * (1.01 + 0.05 * Math.sin(Math.PI * f) * Math.min(1, om)), spin, tilt));\n }\n // Only the stretch on the near side of the globe: a route round the back would cross the picture.\n const near = pts.filter(function (q) { return q[2] < -R * 0.08; });\n if (near.length > 1) S.line(near, { stroke: $.style.mode === \"light\" ? c.accent : c.accent2, width: 4 * k, opacity: 0.9 });\n });\n s += M.part(\"globe\", { at: tin, in: \"zoom\", dur: 0.8, out: \"fade\", box: { x: W / 2 - R, y: cy - R, w: 2 * R, h: 2 * R } }, S.render());\n // The pin: where the country's label point sits on the turned globe.\n if (t > arrive) {\n const q = S.project(worldOnSphere(tp[0], tp[1], R * 1.01, spin, tilt));\n if (q) {\n const lf = fitFont($, $.case(String(p.label || target.name)), \"display\", 40 * k, A.w * 0.6);\n const lw = $.measure($.case(String(p.label || target.name)), lf) + 40 * k, lh = lf.size * 1.6;\n const right = q.x < W / 2 + R * 0.4;\n const bx = right ? q.x + 30 * k : q.x - 30 * k - lw;\n s += M.part(\"pin\", { at: arrive, in: \"pop\", out: \"fade\", box: { x: Math.min(bx, q.x) - 20 * k, y: q.y - lh - 30 * k, w: lw + 60 * k, h: lh + 50 * k } },\n $.line(q.x, q.y, right ? bx : bx + lw, q.y - lh / 2 - 20 * k, { stroke: c.fg, sw: 3 * k }),\n $.circle(q.x, q.y, 10 * k, { fill: c.accent, stroke: c.fg, sw: 3 * k }),\n $.rect(bx, q.y - lh - 20 * k, lw, lh, { r: lh / 2, fill: c.surface, shadow: \"md\" }),\n $.text($.case(String(p.label || target.name)), { ...lf, x: bx + lw / 2, y: q.y - lh - 20 * k + (lh - lf.size) / 2 - lf.size * 0.04, anchor: \"middle\", fill: c.fg }));\n }\n }\n return s;\n}\n"}]},{"format":1,"id":"transitions","name":"MONARC Transitions","version":"1.0.0","description":"Eleven transitions that cover the frame by the cut and reveal the next shot — iris, blinds, doors, glitch, ink, clock, Bauhaus tiles, pixels, stripes, slices and a whip pan — in any brand's colours.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/transitions","style":"cobalt","shared":"shared.js","items":["iris","blinds","doors","glitch","ink","clock","tiles","pixels","stripes","slices","whip"],"entries":[{"id":"iris","name":"Iris","description":"A camera iris: the frame closes down to a point through a ring of the accent, then opens on the next shot. Circle, diamond, star or square, from any point.","category":"transition","tags":["transition","iris","circle","close","open","shape"],"placement":"overlay","duration":{"default":1.2,"min":0.5,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"shape":{"type":"choice","label":"Shape","options":["circle","diamond","star","square"],"default":"circle"},"x":{"type":"number","label":"Centre across (%)","default":50,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Centre down (%)","default":50,"min":0,"max":100,"step":1},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/iris","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const cx = W * p.x / 100, cy = H * p.y / 100;\n const far = Math.hypot(Math.max(cx, W - cx), Math.max(cy, H - cy));\n // How far the shape must open to clear the frame.\n const R = far * (p.shape === \"star\" ? 2.6 : p.shape === \"diamond\" ? 1.45 : p.shape === \"square\" ? 1.02 : 1.02);\n const e = ph.before ? 1 - $.E.inOut3(ph.cover) : $.E.inOut3(ph.uncover);\n if (e < 1) {\n const band = 34 * k * e + (1 - e) * 0;\n s += frameWithHole($, shapePath(p.shape, cx, cy, R * e), col.lead);\n s += frameWithHole($, shapePath(p.shape, cx, cy, R * e + band * 0.5), col.lead2);\n s += frameWithHole($, shapePath(p.shape, cx, cy, R * e + band), col.cover);\n }\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]},{"id":"blinds","name":"Blinds","description":"Venetian blinds: slats swing shut one after another in alternating colours, then open on the next shot.","category":"transition","tags":["transition","blinds","slats","stripes","venetian"],"placement":"overlay","duration":{"default":1.2,"min":0.5,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"slats":{"type":"number","label":"Slats","default":8,"min":3,"max":20,"step":1},"direction":{"type":"choice","label":"Slats run","options":["across","down"],"default":"across"},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/blinds","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const n = Math.max(3, Math.round(p.slats)), across = p.direction === \"across\";\n const L = across ? H : W, band = L / n;\n for (let i = 0; i < n; i++) {\n const lag = (i / (n - 1)) * 0.4;\n const ci = $.E.inOut3($.clamp((ph.cover - lag) / (1 - lag)));\n const ui = $.E.inOut3($.clamp((ph.uncover - lag) / (1 - lag)));\n const f = ph.before ? ci : 1 - ui;\n if (f <= 0) continue;\n const h = band * f + 1;\n // Closing they grow down from the slat's top; opening they shrink to its bottom.\n const a = i * band + (ph.before ? 0 : band - h);\n const fill = i % 2 ? col.lead : col.cover;\n s += across ? $.rect(0, a, W, h, { fill: fill }) : $.rect(a, 0, h, H, { fill: fill });\n }\n // Once shut, the cover colour fills the frame for the word.\n if (Math.abs(t - ph.mid) < 0.06 * $.dur && p.label) s += $.rect(0, 0, W, H, { fill: col.cover });\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]},{"id":"doors","name":"Doors","description":"Two doors slide shut and a Bauhaus disc locks them, then they slide open on the next shot. Across on wide frames, top and bottom on tall ones.","category":"transition","tags":["transition","doors","split","close","open","bauhaus"],"placement":"overlay","duration":{"default":1.4,"min":0.6,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"disc":{"type":"boolean","label":"Disc at the seam","default":true},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/doors","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const tall = H > W * 1.2;\n const L = tall ? H : W, half = L / 2;\n const e = ph.before ? $.E.out3(ph.cover) : 1 - $.E.in3(ph.uncover);\n const off = half * (1 - e);\n const R = Math.min(W, H) * 0.17;\n const doorA = tall ? $.rect(0, -off, W, half + 1, { fill: col.cover }) + $.rect(0, half - off - 10 * k, W, 10 * k, { fill: col.lead }) : $.rect(-off, 0, half + 1, H, { fill: col.cover }) + $.rect(half - off - 10 * k, 0, 10 * k, H, { fill: col.lead });\n const doorB = tall ? $.rect(0, half + off, W, half, { fill: col.cover }) + $.rect(0, half + off, W, 10 * k, { fill: col.lead2 }) : $.rect(half + off, 0, half, H, { fill: col.cover }) + $.rect(half + off, 0, 10 * k, H, { fill: col.lead2 });\n s += doorA + doorB;\n if (p.disc && !p.label) {\n // Each door carries half a disc; shut, they make one.\n const cx = W / 2, cy = H / 2;\n const spin = ph.before ? (1 - e) * -90 : (1 - e) * 90;\n if (tall) {\n s += $.path(\"M\" + (cx - R) + \" \" + (cy - off) + \"A\" + R + \" \" + R + \" 0 0 1 \" + (cx + R) + \" \" + (cy - off) + \"Z\", { fill: col.lead, transform: \"rotate(\" + spin + \" \" + cx + \" \" + (cy - off) + \")\" });\n s += $.path(\"M\" + (cx - R) + \" \" + (cy + off) + \"A\" + R + \" \" + R + \" 0 0 0 \" + (cx + R) + \" \" + (cy + off) + \"Z\", { fill: col.lead2, transform: \"rotate(\" + spin + \" \" + cx + \" \" + (cy + off) + \")\" });\n } else {\n s += $.path(\"M\" + (cx - off) + \" \" + (cy - R) + \"A\" + R + \" \" + R + \" 0 0 0 \" + (cx - off) + \" \" + (cy + R) + \"Z\", { fill: col.lead, transform: \"rotate(\" + spin + \" \" + (cx - off) + \" \" + cy + \")\" });\n s += $.path(\"M\" + (cx + off) + \" \" + (cy - R) + \"A\" + R + \" \" + R + \" 0 0 1 \" + (cx + off) + \" \" + (cy + R) + \"Z\", { fill: col.lead2, transform: \"rotate(\" + spin + \" \" + (cx + off) + \" \" + cy + \")\" });\n }\n }\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]},{"id":"glitch","name":"Glitch","description":"A digital glitch: torn slices and RGB-split blocks flicker in, the frame breaks fully at the cut, then glitches out to the next shot.","category":"transition","tags":["transition","glitch","digital","rgb","noise","tech"],"placement":"overlay","duration":{"default":1,"min":0.4,"max":3},"aspects":["16:9","9:16","1:1","4:5"],"props":{"intensity":{"type":"number","label":"Intensity","default":1,"min":0.3,"max":1.5,"step":0.1},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/glitch","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const fr = Math.floor(t * 24);\n // Strength rises to full at the cut and falls after it.\n const g = Math.min(1, (ph.before ? $.E.in2(ph.cover) : 1 - $.E.out2(ph.uncover)) * p.intensity * 1.15);\n const near = Math.abs(t - ph.mid) < Math.max(0.05, $.dur * 0.07);\n const pal = [col.lead, col.lead2, col.cover, c.fg, \"#00F0FF\", \"#FF2E88\"];\n if (near) s += $.rect(0, 0, W, H, { fill: col.cover });\n const n = 22;\n for (let i = 0; i < n; i++) {\n const h1 = $.hash(i * 7.3 + fr * 1.91), h2 = $.hash(i * 3.1 + fr * 0.77), h3 = $.hash(i * 11.7 + fr * 2.3);\n if (h1 > g) continue;\n const y = h2 * H, hh = (0.015 + 0.12 * h3 * h3) * H;\n const x = (h3 - 0.5) * W * 0.5 * g, w = W * (0.35 + 0.65 * $.hash(i + fr * 5.1));\n s += $.rect(x + (h1 > 0.5 ? W - w : 0) * (1 - g), y, w, hh, { fill: pal[Math.floor(h2 * 997) % pal.length] });\n }\n // Pixel blocks.\n for (let j = 0; j < 14; j++) {\n const b1 = $.hash(j * 5.7 + fr * 3.3);\n if (b1 > g * 0.8) continue;\n const sz = (20 + 60 * $.hash(j + fr)) * k;\n s += $.rect($.hash(j * 2.2 + fr) * W, $.hash(j * 9.9 + fr) * H, sz * 2.2, sz, { fill: pal[(j + fr) % pal.length] });\n }\n // Scanlines over it all while it is breaking.\n if (g > 0.2) {\n const id = $.uid(\"sl\");\n $.def('<pattern id=\"' + id + '\" width=\"8\" height=\"' + 6 * k + '\" patternUnits=\"userSpaceOnUse\"><rect width=\"8\" height=\"' + 2 * k + '\" fill=\"rgba(0,0,0,0.22)\"/></pattern>');\n s += $.rect(0, 0, W, H, { fill: \"url(#\" + id + \")\", op: g });\n }\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n if (p.label) {\n // The word with an RGB split that jitters.\n const word = cutWord(M, $, t, p.label, col.cover);\n const j = (($.hash(fr * 1.3) - 0.5) * 18) * k;\n s += word ? '<g opacity=\"0.75\" transform=\"translate(' + j + ' 0)\" style=\"mix-blend-mode:screen\">' + word.replace(/fill=\"[^\"]*\"/g, 'fill=\"#00F0FF\"') + \"</g>\" + '<g opacity=\"0.75\" transform=\"translate(' + -j + ' 0)\">' + word.replace(/fill=\"[^\"]*\"/g, 'fill=\"#FF2E88\"') + \"</g>\" + word : \"\";\n }\n return s;\n}\n\n/** A glitch cuts on static: zaps into the cut, on it, and one more as it clears. */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.3), kind: \"zap\", level: 0.8 },\n { at: mid, kind: \"zap\", level: 1.1, pitch: 4 },\n { at: mid, kind: \"sub\", level: 0.4 },\n { at: Math.min($.dur - 0.1, mid + 0.14), kind: \"zap\", level: 0.6, pitch: -3 },\n ],\n auto: true,\n };\n}\n","libs":[]},{"id":"ink","name":"Ink bleed","description":"Ink spreads from a point in wobbly blooms of the accents until it floods the frame, then a hole blooms open on the next shot.","category":"transition","tags":["transition","ink","paint","blob","organic","liquid","splash"],"placement":"overlay","duration":{"default":1.6,"min":0.6,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"x":{"type":"number","label":"From across (%)","default":30,"min":0,"max":100,"step":1},"y":{"type":"number","label":"From down (%)","default":60,"min":0,"max":100,"step":1},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/ink","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const cx = W * p.x / 100, cy = H * p.y / 100;\n const far = Math.hypot(Math.max(cx, W - cx), Math.max(cy, H - cy)) * 1.25;\n if (ph.before) {\n const lags = [0, 0.12, 0.24], fills = [col.lead, col.lead2, col.cover];\n lags.forEach(function (lag, i) {\n const e = $.E.inOut3($.clamp((ph.cover - lag) / (1 - lag)));\n if (e > 0) s += $.path(blobPath(cx, cy, far * e, 1.7 + i * 2.1, t), { fill: fills[i] });\n });\n } else {\n // The hole opens from the frame's centre, the cover first, the accents trailing as rims.\n const lags = [0.2, 0.1, 0], fills = [col.lead, col.lead2, col.cover];\n const hx = W / 2, hy = H / 2, far2 = Math.hypot(W, H) * 0.7;\n lags.forEach(function (lag, i) {\n const e = $.E.inOut3($.clamp((ph.uncover - lag) / (1 - lag)));\n if (e < 1) s += frameWithHole($, e > 0 ? blobPath(hx, hy, far2 * e, 4.2 + i, t) : \"\", fills[i]);\n });\n }\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]},{"id":"clock","name":"Clock wipe","description":"A clock-hand sweep: the next colour wipes round the frame like a second hand with an accent edge, then sweeps on round to reveal the next shot.","category":"transition","tags":["transition","clock","radial","sweep","pie","wipe"],"placement":"overlay","duration":{"default":1.2,"min":0.5,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"direction":{"type":"choice","label":"Turns","options":["clockwise","anticlockwise"],"default":"clockwise"},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/clock","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const cx = W / 2, cy = H / 2, R = Math.hypot(W, H);\n const dirn = p.direction === \"anticlockwise\" ? -1 : 1;\n const pt = function (deg) { const a = (deg - 90) * Math.PI / 180; return [cx + R * Math.cos(a) * 1, cy + R * Math.sin(a)]; };\n // A wedge from angle a to angle b (degrees from 12 o'clock, in the turning direction).\n const wedge = function (a, b, fill) {\n if (b - a <= 0.2) return \"\";\n if (b - a >= 359.8) return $.rect(0, 0, W, H, { fill: fill });\n const steps = Math.ceil((b - a) / 30), d = [\"M\" + cx + \" \" + cy];\n for (let i = 0; i <= steps; i++) { const q = pt(dirn * (a + ((b - a) * i) / steps)); d.push(\"L\" + q[0] + \" \" + q[1]); }\n return $.path(d.join(\"\") + \"Z\", { fill: fill });\n };\n if (ph.before) {\n const e = $.E.inOut2(ph.cover) * 360;\n s += wedge(0, e, col.cover) + wedge(Math.max(0, e - 14), e, col.lead);\n } else {\n const e = $.E.inOut2(ph.uncover) * 360;\n s += wedge(e, 360, col.cover) + wedge(e, Math.min(360, e + 14), col.lead2);\n }\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]},{"id":"tiles","name":"Bauhaus tiles","description":"A grid of Bauhaus shapes — discs, quarter circles, squares — pops in tile by tile in a diagonal wave to cover the frame, then shrinks away.","category":"transition","tags":["transition","tiles","bauhaus","shapes","grid","geometric","mosaic"],"placement":"overlay","duration":{"default":1.4,"min":0.6,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"size":{"type":"number","label":"Tile size","default":4,"min":2,"max":9,"step":1,"description":"tiles across the short side"},"order":{"type":"choice","label":"Order","options":["diagonal","random","center"],"default":"diagonal"},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/tiles","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const n = Math.max(2, Math.round(p.size)), cell = Math.min(W, H) / n;\n const cols = Math.ceil(W / cell), rows = Math.ceil(H / cell);\n const ox = (W - cols * cell) / 2, oy = (H - rows * cell) / 2;\n const pal = [col.cover, col.lead, col.lead2, col.cover];\n for (let r = 0; r < rows; r++) for (let q = 0; q < cols; q++) {\n const h = $.hash(r * 17.3 + q * 5.1 + 0.4);\n const rank = p.order === \"random\" ? h : p.order === \"center\" ? Math.hypot(q - (cols - 1) / 2, r - (rows - 1) / 2) / Math.hypot(cols / 2, rows / 2) : (q + r) / (cols + rows - 2);\n const lag = rank * 0.5;\n const e = ph.before ? $.E.outBack($.clamp((ph.cover - lag) / 0.5)) : 1 - $.E.in3($.clamp((ph.uncover - lag) / 0.5));\n if (e <= 0) continue;\n const x = ox + q * cell, y = oy + r * cell, cx = x + cell / 2, cy = y + cell / 2;\n const fill = pal[Math.floor(h * 4) % 4], kind = Math.floor($.hash(r * 3.3 + q * 9.7) * 3);\n let shape;\n if (kind === 0) shape = $.circle(cx, cy, cell * 0.72 * e, { fill: fill });\n else if (kind === 1) { const rr = cell * 1.42 * e, rot = Math.floor(h * 4) * 90; shape = $.g({ transform: \"rotate(\" + rot + \" \" + cx + \" \" + cy + \")\" }, $.path(\"M\" + (cx - cell / 2) + \" \" + (cy - cell / 2) + \"h\" + rr + \"A\" + rr + \" \" + rr + \" 0 0 1 \" + (cx - cell / 2) + \" \" + (cy - cell / 2 + rr) + \"Z\", { fill: fill })); }\n else shape = $.rect(cx - cell * 0.51 * e, cy - cell * 0.51 * e, cell * 1.02 * e, cell * 1.02 * e, { fill: fill });\n s += $.clip(x - 0.5, y - 0.5, cell + 1, cell + 1, 0, shape);\n }\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n if (p.label) {\n const str = $.case(p.label), f = fitFont($, str, \"display\", 120 * k, Math.min(W, H * 1.4) * 0.66, { ls: -0.03 });\n const w = $.measure(str, f) + 80 * k, hh = f.size * 1.5;\n const a = Math.max(0, ph.mid - Math.min(0.3, ph.mid * 0.5));\n s += M.part(\"label\", { at: a, in: \"pop\", outAt: ph.mid + ($.dur - ph.mid) * 0.2, out: \"pop\", outDur: 0.2, box: { x: W / 2 - w / 2, y: H / 2 - hh / 2, w: w, h: hh } },\n $.rect(W / 2 - w / 2, H / 2 - hh / 2, w, hh, { r: hh / 2, fill: col.cover }) + $.text(str, Object.assign({}, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(col.cover) })));\n }\n return s;\n}\n","libs":[]},{"id":"pixels","name":"Pixel dissolve","description":"The frame dissolves into square pixels in random order — a few in the accents — until it is covered, then dissolves away to the next shot.","category":"transition","tags":["transition","pixels","dissolve","mosaic","retro","digital"],"placement":"overlay","duration":{"default":1.2,"min":0.5,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"size":{"type":"number","label":"Pixel size","default":48,"min":16,"max":160,"step":4},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/pixels","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const px = Math.max(12, p.size * k), cols = Math.ceil(W / px), rows = Math.ceil(H / px);\n const cover = ph.before ? $.E.inOut2(ph.cover) : 1 - $.E.inOut2(ph.uncover);\n let d = \"\", dl = \"\", dl2 = \"\";\n for (let r = 0; r < rows; r++) for (let q = 0; q < cols; q++) {\n const h = $.hash(r * 31.7 + q * 7.9 + (ph.before ? 0 : 3.3));\n if (h >= cover) continue;\n const cell = \"M\" + q * px + \" \" + r * px + \"h\" + (px + 0.5) + \"v\" + (px + 0.5) + \"h\" + -(px + 0.5) + \"z\";\n // The last pixels to land are the accents.\n if (cover < 0.98 && h > cover - 0.08) { if ((q + r) % 2) dl += cell; else dl2 += cell; } else d += cell;\n }\n s += (d ? $.path(d, { fill: col.cover }) : \"\") + (dl ? $.path(dl, { fill: col.lead }) : \"\") + (dl2 ? $.path(dl2, { fill: col.lead2 }) : \"\");\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]},{"id":"stripes","name":"Speed stripes","description":"Diagonal stripes in the brand's colours race across in a stagger to cover the frame, then race off to reveal the next shot.","category":"transition","tags":["transition","stripes","diagonal","speed","sport","swipe"],"placement":"overlay","duration":{"default":1.1,"min":0.5,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"stripes":{"type":"number","label":"Stripes","default":7,"min":3,"max":16,"step":1},"direction":{"type":"choice","label":"Direction","options":["right","left"],"default":"right"},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/stripes","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const n = Math.max(3, Math.round(p.stripes)), D = Math.hypot(W, H), band = D / n + 2;\n const ang = -24, sign = p.direction === \"left\" ? -1 : 1;\n const pal = [col.lead, col.lead2, col.cover];\n let g = \"\";\n for (let i = 0; i < n; i++) {\n const lag = (i / (n - 1)) * 0.35;\n const ci = $.E.inOut3($.clamp((ph.cover - lag) / (1 - lag))), ui = $.E.inOut3($.clamp((ph.uncover - lag) / (1 - lag)));\n // Each stripe slides in from one side, then out the other.\n const x0 = ph.before ? -D + D * ci : D * ui;\n if (ph.before ? ci <= 0 : ui >= 1) continue;\n g += $.rect(x0 - D / 2, -D / 2 + i * (D / n), D * 1.02, band, { fill: i === n - 1 || i % 3 === 2 ? col.cover : pal[i % 3] });\n }\n if (ph.before && ph.cover > 0.98) g += $.rect(-D, -D, D * 2, D * 2, { fill: col.cover });\n s += '<g transform=\"translate(' + W / 2 + \" \" + H / 2 + \") scale(\" + sign + \" 1) rotate(\" + ang + ')\">' + g + \"</g>\";\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]},{"id":"slices","name":"Slices","description":"The frame is sliced into bands that shoot in from alternate sides, each with an accent edge, then shoot out the far side to reveal the next shot.","category":"transition","tags":["transition","slices","bands","slide","split"],"placement":"overlay","duration":{"default":1.2,"min":0.5,"max":4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"slices":{"type":"number","label":"Slices","default":5,"min":2,"max":12,"step":1},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/slices","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const n = Math.max(2, Math.round(p.slices)), band = H / n, edge = 12 * k;\n for (let i = 0; i < n; i++) {\n const lag = $.hash(i * 4.4 + 1) * 0.3, dir = i % 2 ? -1 : 1;\n const ci = $.E.out3($.clamp((ph.cover - lag) / (1 - lag))), ui = $.E.in3($.clamp((ph.uncover - lag) / (1 - lag)));\n const x = ph.before ? -dir * W * (1 - ci) : dir * W * ui;\n if (ph.before ? ci <= 0 : ui >= 1) continue;\n s += $.rect(x, i * band - 0.5, W, band + 1, { fill: col.cover });\n // The leading edge in an accent.\n const ex = dir > 0 ? (ph.before ? x + W - edge : x) : (ph.before ? x : x + W - edge);\n s += $.rect(ex, i * band - 0.5, edge, band + 1, { fill: i % 2 ? col.lead2 : col.lead });\n }\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]},{"id":"whip","name":"Whip pan","description":"A whip pan: speed streaks blur across, thicken into a smear of the brand's colours at the cut, then streak away the same way onto the next shot.","category":"transition","tags":["transition","whip","pan","speed","blur","streak","swish"],"placement":"overlay","duration":{"default":0.8,"min":0.3,"max":3},"aspects":["16:9","9:16","1:1","4:5"],"props":{"direction":{"type":"choice","label":"Direction","options":["left","right","up","down"],"default":"left"},"colors":{"type":"choice","label":"Colours","options":["brand","accent","black","white"],"default":"brand","description":"brand: the accents lead and the brand's ground covers"},"label":{"type":"text","label":"Word at the cut","default":"","maxLength":24,"description":"shown while the frame is covered (empty for none)"}},"parts":{"cover":"the shapes that cover the frame","label":"the word at the cut"},"pack":"transitions","ref":"transitions/whip","code":"// Transitions cover the frame by the cut (the midpoint, or the \"cut\" beat) and uncover after it: cut your\n// two clips there. Everything is transparent at the first and last frame.\nfunction cutAt($) { return $.clamp($.beat(\"cut\", $.dur / 2), 0.1, $.dur - 0.1); }\n/** Progress before and after the cut: cover 0→1, then uncover 0→1. */\nfunction phases($, t) {\n const mid = cutAt($);\n return { mid: mid, cover: $.clamp(t / mid), uncover: $.clamp((t - mid) / ($.dur - mid)), before: t < mid };\n}\n/** The sound of a cut: air as the cover arrives, a soft low drop on the cut, air going as it opens (and the label's pop). */\nfunction sound($) {\n const mid = cutAt($);\n return {\n cues: [\n { at: Math.max(0, mid - 0.45), kind: \"whoosh\", dur: Math.min(0.5, mid), level: 0.9 },\n { at: mid, kind: \"sub\", level: 0.55 },\n { at: mid + 0.04, kind: \"whoosh\", dur: Math.min(0.5, $.dur - mid), level: 0.55, pitch: -3, pan: 0.2 },\n ],\n auto: true,\n };\n}\n/** The paints: lead (the edge that arrives first), lead2, and cover (what fills the frame at the cut). */\nfunction paints($, scheme) {\n const c = $.c;\n if (scheme === \"accent\") return { lead: c.accent2, lead2: c.bg, cover: c.accent };\n if (scheme === \"black\") return { lead: c.accent, lead2: c.accent2, cover: \"#0A0A0C\" };\n if (scheme === \"white\") return { lead: c.accent, lead2: c.accent2, cover: \"#FFFFFF\" };\n return { lead: c.accent, lead2: c.accent2, cover: c.bg };\n}\n/** The word shown while the frame is covered (on the cover colour). */\nfunction cutWord(M, $, t, label, bg) {\n if (!label) return \"\";\n const mid = cutAt($), k = $.k, W = $.W, H = $.H, after = $.dur - mid;\n const str = $.case(label);\n const f = fitFont($, str, \"display\", 150 * k, Math.min(W, H * 1.4) * 0.74, { ls: -0.03 });\n return M.text(\"label\", Object.assign({ at: Math.max(0, mid - Math.min(0.3, mid * 0.5)), in: \"pop\", outAt: mid + after * 0.25, out: \"fade\", outDur: after * 0.3 }, f, { x: W / 2, y: H / 2 - f.size * 0.55, anchor: \"middle\", fill: $.on(bg) }), str);\n}\n/** The whole frame with a hole cut in it (hole: path data). */\nfunction frameWithHole($, hole, fill) {\n return $.path(\"M0 0H\" + $.W + \"V\" + $.H + \"H0Z\" + hole, { fill: fill, extra: 'fill-rule=\"evenodd\"' });\n}\n/** A closed shape as path data: circle, diamond, star or square, radius r about (cx, cy). */\nfunction shapePath(shape, cx, cy, r) {\n if (r <= 0.01) return \"\";\n if (shape === \"diamond\") return \"M\" + cx + \" \" + (cy - r) + \"L\" + (cx + r) + \" \" + cy + \"L\" + cx + \" \" + (cy + r) + \"L\" + (cx - r) + \" \" + cy + \"Z\";\n if (shape === \"square\") return \"M\" + (cx - r) + \" \" + (cy - r) + \"H\" + (cx + r) + \"V\" + (cy + r) + \"H\" + (cx - r) + \"Z\";\n if (shape === \"star\") {\n let d = \"\";\n for (let i = 0; i < 10; i++) {\n const a = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.5 : r;\n d += (i ? \"L\" : \"M\") + (cx + rr * Math.cos(a)) + \" \" + (cy + rr * Math.sin(a));\n }\n return d + \"Z\";\n }\n return \"M\" + (cx + r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx - r) + \" \" + cy + \"A\" + r + \" \" + r + \" 0 1 0 \" + (cx + r) + \" \" + cy + \"Z\";\n}\n/** A wobbly closed blob through n points (radius r, lumps seeded), as smooth path data. */\nfunction blobPath(cx, cy, r, seed, t) {\n const n = 48, pts = [];\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2;\n const q = 1 + 0.1 * Math.sin(3 * a + seed) + 0.06 * Math.sin(7 * a + seed * 2.3 + t * 2) + 0.035 * Math.sin(13 * a + seed * 4.1 - t * 3);\n pts.push([cx + Math.cos(a) * r * q, cy + Math.sin(a) * r * q]);\n }\n let d = \"M\" + ((pts[0][0] + pts[n - 1][0]) / 2) + \" \" + ((pts[0][1] + pts[n - 1][1]) / 2);\n for (let i = 0; i < n; i++) {\n const p = pts[i], q2 = pts[(i + 1) % n];\n d += \"Q\" + p[0] + \" \" + p[1] + \" \" + (p[0] + q2[0]) / 2 + \" \" + (p[1] + q2[1]) / 2;\n }\n return d + \"Z\";\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const ph = phases($, t), col = paints($, p.colors);\n let s = \"\";\n const horiz = p.direction === \"left\" || p.direction === \"right\";\n const L = horiz ? W : H, A2 = horiz ? H : W;\n const g = ph.before ? $.E.in3(ph.cover) : 1 - $.E.out3(ph.uncover);\n const n = 46;\n let lines = \"\";\n for (let i = 0; i < n; i++) {\n const h1 = $.hash(i * 3.7), h2 = $.hash(i * 9.1 + 2), h3 = $.hash(i * 5.3 + 7);\n if (h3 > g * 1.3) continue;\n const pos = h1 * A2, th = (4 + 40 * h2 * h2) * k * (0.4 + g);\n const len = L * (0.3 + 0.9 * h2) * (0.4 + g), travel = (t * (2.2 + h3 * 2) + h1) % 1;\n const a = -len + (L + len) * travel;\n const fill = i % 5 === 0 ? col.lead : i % 5 === 1 ? col.lead2 : col.cover;\n lines += horiz ? $.rect(a, pos, len, th, { fill: fill, r: th / 2 }) : $.rect(pos, a, th, len, { fill: fill, r: th / 2 });\n }\n // The smear: the cover colour washing in at the cut.\n const wash = $.clamp((g - 0.55) / 0.35);\n if (wash > 0) lines = $.rect(0, 0, W, H, { fill: col.cover, op: wash }) + lines;\n const flip = p.direction === \"left\" ? \"translate(\" + W + \" 0) scale(-1 1)\" : p.direction === \"up\" ? \"translate(0 \" + H + \") scale(1 -1)\" : \"\";\n s += flip ? '<g transform=\"' + flip + '\">' + lines + \"</g>\" : lines;\n s = M.part(\"cover\", { in: \"none\", out: \"none\", vary: false }, s);\n return s + cutWord(M, $, t, p.label, col.cover);\n}\n","libs":[]}]},{"format":1,"id":"annotate","name":"MONARC Annotate","version":"1.0.0","description":"Point things out in footage: hand-drawn arrows and circles, a spotlight, focus brackets, highlighter marks, a rubber stamp, live reactions, a tech pointer, emphasis marks and numbered markers.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/annotate","style":"candy","shared":"shared.js","items":["arrow","circle","spotlight","focus","highlight","stamp","reactions","pointer","emphasis","markers"],"entries":[{"id":"arrow","name":"Arrow","description":"A hand-drawn (or clean) arrow sweeps from a label to the thing you point at, with a pulse on the spot.","category":"callout","tags":["annotation","arrow","point","look","here","hand-drawn"],"placement":"overlay","duration":{"default":3,"min":1.5,"max":12},"aspects":["16:9","9:16","1:1","4:5"],"props":{"label":{"type":"text","label":"Label","default":"Look here","maxLength":32},"x":{"type":"number","label":"Point across (%)","default":62,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Point down (%)","default":45,"min":0,"max":100,"step":1},"side":{"type":"choice","label":"Label side","options":["auto","left","right","above","below"],"default":"auto"},"look":{"type":"choice","label":"Look","options":["marker","clean"],"default":"marker"},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"label":"the label","arrow":"the arrow","spot":"the pulse on the spot"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/arrow","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const tx = W * p.x / 100, ty = H * p.y / 100;\n const side = p.side === \"auto\" ? roomy($, tx, ty) : p.side;\n const v = { left: [-1, 0], right: [1, 0], above: [0, -1], below: [0, 1] }[side] || [1, 0];\n const gap = Math.min(W, H) * 0.34;\n const tg = tagAt($, p.label, tx + v[0] * gap * 1.3, ty + v[1] * gap - (v[0] ? gap * 0.35 : 0), col, 44);\n const b = tg.box, lcx = b.x + b.w / 2, lcy = b.y + b.h / 2;\n // From the label's edge facing the spot, to just short of the spot.\n const sx = v[0] ? (v[0] > 0 ? b.x - 14 * k : b.x + b.w + 14 * k) : lcx, sy = v[1] ? (v[1] > 0 ? b.y - 14 * k : b.y + b.h + 14 * k) : lcy;\n const d = Math.hypot(tx - sx, ty - sy) || 1, ex = tx - ((tx - sx) / d) * 26 * k, ey = ty - ((ty - sy) / d) * 26 * k;\n const ta = tin + 0.25;\n let arrow;\n if (p.look === \"clean\") {\n const bend = 0.25, mx = (sx + ex) / 2 - (ey - sy) * bend, my = (sy + ey) / 2 + (ex - sx) * bend;\n const pr = $.E.inOut2($.seg(t, ta, ta + 0.55));\n const ang = Math.atan2(ey - my, ex - mx), hl = 26 * k;\n arrow = $.path(\"M\" + sx + \" \" + sy + \"Q\" + mx + \" \" + my + \" \" + ex + \" \" + ey, { stroke: col, sw: 7 * k, draw: pr });\n if (pr > 0.95) arrow += $.path(\"M\" + (ex - hl * Math.cos(ang - 0.5)) + \" \" + (ey - hl * Math.sin(ang - 0.5)) + \"L\" + ex + \" \" + ey + \"L\" + (ex - hl * Math.cos(ang + 0.5)) + \" \" + (ey - hl * Math.sin(ang + 0.5)), { stroke: col, sw: 7 * k });\n } else arrow = $.ink(t, ta, $.hand.arrow(sx, sy, ex, ey, { bend: 0.22, seed: 3, rough: 3 * k }), { color: col, sw: 9 * k, dur: 0.55 });\n s += M.part(\"arrow\", { at: ta, in: \"none\" }, arrow);\n s += M.part(\"spot\", { at: ta + 0.55, in: \"pop\", box: { x: tx - 20 * k, y: ty - 20 * k, w: 40 * k, h: 40 * k } }, $.circle(tx, ty, 9 * k, { fill: col }) + pulse($, t, ta + 0.55, tx, ty, 12 * k, col));\n s += M.part(\"label\", { at: tin, in: \"pop\", box: b }, tg.markup);\n return s;\n}\n","libs":[]},{"id":"circle","name":"Circle it","description":"A marker loop drawn round the thing that matters — once, twice, or a clean ring — with an optional label.","category":"callout","tags":["annotation","circle","loop","ring","marker","hand-drawn"],"placement":"overlay","duration":{"default":3,"min":1.5,"max":12},"aspects":["16:9","9:16","1:1","4:5"],"props":{"x":{"type":"number","label":"Box left (%)","default":40,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Box top (%)","default":30,"min":0,"max":100,"step":1},"w":{"type":"number","label":"Box width (%)","default":22,"min":2,"max":100,"step":1},"h":{"type":"number","label":"Box height (%)","default":30,"min":2,"max":100,"step":1},"look":{"type":"choice","label":"Look","options":["marker","double","clean"],"default":"marker"},"label":{"type":"text","label":"Label","default":"","maxLength":28,"description":"a tag by the loop (empty for none)"},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"loop":"the loop","label":"the label"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/circle","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const b = boxOf($, p.x, p.y, p.w, p.h);\n const cx = b.x + b.w / 2, cy = b.y + b.h / 2, rx = b.w * 0.62 + 14 * k, ry = b.h * 0.62 + 14 * k;\n let loop;\n if (p.look === \"clean\") {\n const d = \"M\" + (cx + rx) + \" \" + cy + \"A\" + rx + \" \" + ry + \" 0 1 1 \" + (cx - rx) + \" \" + cy + \"A\" + rx + \" \" + ry + \" 0 1 1 \" + (cx + rx) + \" \" + cy;\n loop = $.path(d, { stroke: col, sw: 7 * k, draw: $.E.inOut2($.seg(t, tin, tin + 0.6)), transform: \"rotate(-90 \" + cx + \" \" + cy + \")\" });\n } else {\n loop = $.ink(t, tin, $.hand.circle(cx, cy, rx, ry, { turns: 1.12, seed: 2, rough: 4 * k }), { color: col, sw: 9 * k, dur: 0.6 });\n if (p.look === \"double\") loop += $.ink(t, tin + 0.35, $.hand.circle(cx, cy, rx * 1.06, ry * 0.96, { turns: 1.05, seed: 9, rough: 5 * k }), { color: col, sw: 6 * k, dur: 0.5 });\n }\n s += M.part(\"loop\", { at: tin, in: \"none\" }, loop);\n if (p.label) {\n const tg = tagAt($, p.label, cx + rx * 0.75, cy - ry - 34 * k, col, 36);\n s += M.part(\"label\", { at: tin + 0.5, in: \"pop\", box: tg.box }, tg.markup);\n }\n return s;\n}\n","libs":[]},{"id":"spotlight","name":"Spotlight","description":"Everything but one area dims; a feathered spotlight closes onto it with an accent ring, and a label sits by it.","category":"callout","tags":["annotation","spotlight","focus","dim","highlight","isolate"],"placement":"overlay","duration":{"default":4,"min":1.5,"max":15},"aspects":["16:9","9:16","1:1","4:5"],"props":{"x":{"type":"number","label":"Box left (%)","default":38,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Box top (%)","default":28,"min":0,"max":100,"step":1},"w":{"type":"number","label":"Box width (%)","default":24,"min":2,"max":100,"step":1},"h":{"type":"number","label":"Box height (%)","default":34,"min":2,"max":100,"step":1},"shape":{"type":"choice","label":"Shape","options":["circle","box"],"default":"circle"},"dim":{"type":"number","label":"Dim (%)","default":70,"min":10,"max":95,"step":5},"label":{"type":"text","label":"Label","default":"This part","maxLength":28,"description":"empty for none"},"note":{"type":"text","label":"Note","default":"","maxLength":60,"description":"a line under the label"},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"dim":"the dimmed frame","ring":"the ring","label":"the label and note"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/spotlight","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const b = boxOf($, p.x, p.y, p.w, p.h);\n const cx = b.x + b.w / 2, cy = b.y + b.h / 2;\n const e = $.E.inOut3($.seg(t, tin, tin + 0.9));\n const circ = p.shape !== \"box\";\n const rx1 = circ ? b.w * 0.62 + 18 * k : b.w / 2 + 18 * k, ry1 = circ ? b.h * 0.62 + 18 * k : b.h / 2 + 18 * k;\n const rx = $.lerp(Math.hypot(W, H), rx1, e), ry = $.lerp(Math.hypot(W, H), ry1, e);\n const rr = circ ? 0 : Math.min(rx, ry, 28 * k);\n const hole = circ\n ? \"M\" + (cx + rx) + \" \" + cy + \"A\" + rx + \" \" + ry + \" 0 1 0 \" + (cx - rx) + \" \" + cy + \"A\" + rx + \" \" + ry + \" 0 1 0 \" + (cx + rx) + \" \" + cy + \"Z\"\n : \"M\" + (cx - rx + rr) + \" \" + (cy - ry) + \"H\" + (cx + rx - rr) + \"Q\" + (cx + rx) + \" \" + (cy - ry) + \" \" + (cx + rx) + \" \" + (cy - ry + rr) + \"V\" + (cy + ry - rr) + \"Q\" + (cx + rx) + \" \" + (cy + ry) + \" \" + (cx + rx - rr) + \" \" + (cy + ry) + \"H\" + (cx - rx + rr) + \"Q\" + (cx - rx) + \" \" + (cy + ry) + \" \" + (cx - rx) + \" \" + (cy + ry - rr) + \"V\" + (cy - ry + rr) + \"Q\" + (cx - rx) + \" \" + (cy - ry) + \" \" + (cx - rx + rr) + \" \" + (cy - ry) + \"Z\";\n const fid = $.uid(\"fb\"), m = 80 * k;\n $.def('<filter id=\"' + fid + '\" x=\"-5%\" y=\"-5%\" width=\"110%\" height=\"110%\"><feGaussianBlur stdDeviation=\"' + 16 * k + '\"/></filter>');\n s += M.part(\"dim\", { at: tin, in: \"fade\", dur: 0.4, vary: false }, $.path(\"M\" + -m + \" \" + -m + \"H\" + (W + m) + \"V\" + (H + m) + \"H\" + -m + \"Z\" + hole, { fill: \"rgba(0,0,0,\" + p.dim / 100 + \")\", extra: 'fill-rule=\"evenodd\" filter=\"url(#' + fid + ')\"' }));\n const ringP = $.E.inOut2($.seg(t, tin + 0.8, tin + 1.3));\n if (ringP > 0) s += M.part(\"ring\", { at: tin + 0.8, in: \"none\" }, $.path(hole, { stroke: col, sw: 5 * k, draw: ringP }));\n if (p.label) {\n const below = cy + ry1 + 150 * k < H;\n const ly = below ? cy + ry1 + 30 * k : cy - ry1 - 30 * k - 56 * k;\n const tg = tagAt($, p.label, cx, ly + 28 * k, col, 40);\n let lab = tg.markup;\n if (p.note) {\n const nf = Object.assign({}, $.font(\"text\"), { size: 32 * k });\n const nw = Math.min(W * 0.8, $.measure(p.note, nf));\n const nx = Math.min(W - 30 * k - nw / 2, Math.max(30 * k + nw / 2, cx));\n lab += $.text(p.note, Object.assign({}, nf, { x: nx, y: below ? tg.box.y + tg.box.h + 16 * k : tg.box.y - 16 * k - 40 * k, anchor: \"middle\", fill: \"#FFFFFF\", maxW: W * 0.8 }));\n }\n s += M.part(\"label\", { at: tin + 1.0, in: \"rise\", distance: 16, box: tg.box }, lab);\n }\n return s;\n}\n","libs":[]},{"id":"focus","name":"Focus lock","description":"Camera focus brackets hunt in from the frame's corners, snap onto the subject with a blink, and lock in the accent with a label and crosshair.","category":"callout","tags":["annotation","focus","brackets","camera","lock","target","track"],"placement":"overlay","duration":{"default":3,"min":1.5,"max":12},"aspects":["16:9","9:16","1:1","4:5"],"props":{"x":{"type":"number","label":"Box left (%)","default":36,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Box top (%)","default":26,"min":0,"max":100,"step":1},"w":{"type":"number","label":"Box width (%)","default":28,"min":2,"max":100,"step":1},"h":{"type":"number","label":"Box height (%)","default":40,"min":2,"max":100,"step":1},"label":{"type":"text","label":"Label","default":"Subject","maxLength":28,"description":"empty for none"},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"brackets":"the corner brackets","cross":"the crosshair","label":"the label"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/focus","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const b = boxOf($, p.x, p.y, p.w, p.h), m = Math.min(W, H) * 0.08;\n const q = $.clamp($.spring(Math.max(0, t - tin), 2.2, 6.5), 0, 1.15);\n const bx = $.lerp(m, b.x, q), by = $.lerp(m, b.y, q), bw = $.lerp(W - 2 * m, b.w, q), bh = $.lerp(H - 2 * m, b.h, q);\n const lock = tin + 0.7, locked = t >= lock;\n const blink = locked && t < lock + 0.3 && Math.floor((t - lock) * 14) % 2 === 1;\n const stroke = locked ? col : \"#FFFFFF\", L = Math.min(bw, bh) * 0.22, sw = 6 * k;\n const corner = function (x, y, dx, dy) { return $.path(\"M\" + x + \" \" + (y + dy * L) + \"V\" + y + \"H\" + (x + dx * L), { stroke: stroke, sw: sw, cap: \"square\", join: \"miter\" }); };\n const br = corner(bx, by, 1, 1) + corner(bx + bw, by, -1, 1) + corner(bx, by + bh, 1, -1) + corner(bx + bw, by + bh, -1, -1);\n s += M.part(\"brackets\", { at: tin, in: \"fade\", dur: 0.2 }, blink ? \"\" : br);\n const cx = bx + bw / 2, cy = by + bh / 2, cr = 18 * k;\n s += M.part(\"cross\", { at: lock, in: \"pop\", box: { x: cx - cr, y: cy - cr, w: 2 * cr, h: 2 * cr } }, $.path(\"M\" + (cx - cr) + \" \" + cy + \"H\" + (cx - 6 * k) + \"M\" + (cx + 6 * k) + \" \" + cy + \"H\" + (cx + cr) + \"M\" + cx + \" \" + (cy - cr) + \"V\" + (cy - 6 * k) + \"M\" + cx + \" \" + (cy + 6 * k) + \"V\" + (cy + cr), { stroke: col, sw: 3 * k }));\n if (p.label) {\n const txt = $.case(p.label), f = Object.assign({}, $.font(\"mono\"), { size: 30 * k });\n const w = $.measure(txt, f) + 28 * k, h = 48 * k;\n const lx = Math.min(W - w - 20 * k, Math.max(20 * k, b.x)), ly = b.y - h - 14 * k > 20 * k ? b.y - h - 14 * k : b.y + b.h + 14 * k;\n s += M.part(\"label\", { at: lock + 0.1, in: \"wipe\", from: \"left\", box: { x: lx, y: ly, w: w, h: h } }, $.rect(lx, ly, w, h, { fill: col }) + $.text(txt, Object.assign({}, f, { x: lx + 14 * k, y: ly + (h - f.size) / 2 - 2 * k, fill: $.on(col) })));\n }\n return s;\n}\n","libs":[]},{"id":"highlight","name":"Highlight","description":"A highlighter swipes across a word or area — or a marker underline, box or strike-through drawn on by hand.","category":"callout","tags":["annotation","highlight","marker","underline","strike","emphasis"],"placement":"overlay","duration":{"default":2.5,"min":1,"max":12},"aspects":["16:9","9:16","1:1","4:5"],"props":{"x":{"type":"number","label":"Box left (%)","default":30,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Box top (%)","default":44,"min":0,"max":100,"step":1},"w":{"type":"number","label":"Box width (%)","default":40,"min":2,"max":100,"step":1},"h":{"type":"number","label":"Box height (%)","default":10,"min":1,"max":100,"step":1},"mode":{"type":"choice","label":"Mark","options":["highlight","underline","box","strike"],"default":"highlight"},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"mark":"the mark"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/highlight","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const b = boxOf($, p.x, p.y, p.w, p.h);\n // A highlighter is yellow unless told otherwise; pens are the accent.\n const hi = p.color || c.accent2;\n let mark;\n if (p.mode === \"highlight\") {\n // A marker band with ragged ends, swiped on left to right.\n const j = function (i) { return ($.hash(i * 3.7) - 0.5) * 10 * k; };\n const d = \"M\" + (b.x - 10 * k + j(1)) + \" \" + (b.y + j(2)) + \"L\" + (b.x + b.w + 8 * k + j(3)) + \" \" + (b.y - 4 * k + j(4)) + \"L\" + (b.x + b.w + 14 * k + j(5)) + \" \" + (b.y + b.h + j(6)) + \"L\" + (b.x - 4 * k + j(7)) + \" \" + (b.y + b.h + 4 * k + j(8)) + \"Z\";\n mark = M.part(\"mark\", { at: tin, in: \"wipe\", from: \"left\", dur: 0.45, box: { x: b.x - 20 * k, y: b.y - 12 * k, w: b.w + 40 * k, h: b.h + 24 * k } }, $.path(d, { fill: $.rgba(hi, 0.5) }));\n } else {\n const shape = p.mode === \"box\" ? $.hand.rect(b.x - 12 * k, b.y - 10 * k, b.w + 24 * k, b.h + 20 * k, { seed: 4, rough: 4 * k })\n : p.mode === \"strike\" ? $.hand.line(b.x - 10 * k, b.y + b.h * 0.55, b.x + b.w + 10 * k, b.y + b.h * 0.45, { seed: 5, rough: 3 * k })\n : $.hand.curve(b.x - 6 * k, b.y + b.h + 12 * k, b.x + b.w + 6 * k, b.y + b.h + 6 * k, { bend: 0.04, seed: 6, rough: 3 * k });\n mark = M.part(\"mark\", { at: tin, in: \"none\" }, $.ink(t, tin, shape, { color: col, sw: (p.mode === \"box\" ? 7 : 12) * k, dur: 0.5 }));\n }\n return s + mark;\n}\n","libs":[]},{"id":"stamp","name":"Stamp","description":"A rubber stamp slams down at an angle — APPROVED, SOLD OUT, NEW — with a rough inked double border and a jolt.","category":"callout","tags":["annotation","stamp","approved","sold","rubber","badge","seal"],"placement":"overlay","duration":{"default":2.5,"min":1,"max":12},"aspects":["16:9","9:16","1:1","4:5"],"props":{"text":{"type":"text","label":"Words","default":"Approved","maxLength":18},"x":{"type":"number","label":"Centre across (%)","default":50,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Centre down (%)","default":50,"min":0,"max":100,"step":1},"angle":{"type":"number","label":"Angle","default":-12,"min":-45,"max":45,"step":1},"size":{"type":"number","label":"Size","default":100,"min":40,"max":240,"step":5},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"stamp":"the stamp"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/stamp","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const cx = W * p.x / 100, cy = H * p.y / 100, txt = String(p.text || \"\").toUpperCase();\n const f = fitFont($, txt, \"display\", p.size * k, W * 0.7, { ls: 0.04 });\n const tw = $.measure(txt, f), pad = f.size * 0.42, w = tw + pad * 2, h = f.size * 1.5;\n // Ink that did not take: a fixed noise mask so the stamp looks pressed, not printed.\n const nid = $.uid(\"sn\"), mid = $.uid(\"sm\");\n $.def('<filter id=\"' + nid + '\" x=\"0\" y=\"0\" width=\"100%\" height=\"100%\"><feTurbulence type=\"fractalNoise\" baseFrequency=\"' + (0.9 / k).toFixed(3) + '\" numOctaves=\"2\" seed=\"7\"/><feColorMatrix type=\"matrix\" values=\"0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 -2.4 1.75\"/></filter>');\n $.def('<mask id=\"' + mid + '\" maskUnits=\"userSpaceOnUse\" x=\"' + (cx - w) + '\" y=\"' + (cy - h) + '\" width=\"' + 2 * w + '\" height=\"' + 2 * h + '\"><rect x=\"' + (cx - w) + '\" y=\"' + (cy - h) + '\" width=\"' + 2 * w + '\" height=\"' + 2 * h + '\" fill=\"#fff\" filter=\"url(#' + nid + ')\"/></mask>');\n const body = $.rect(cx - w / 2, cy - h / 2, w, h, { r: 14 * k, stroke: col, sw: 9 * k }) + $.rect(cx - w / 2 + 16 * k, cy - h / 2 + 16 * k, w - 32 * k, h - 32 * k, { r: 8 * k, stroke: col, sw: 4 * k }) +\n $.text(txt, Object.assign({}, f, { x: cx, y: cy - f.size * 0.56, anchor: \"middle\", fill: col }));\n const inked = '<g mask=\"url(#' + mid + ')\" transform=\"rotate(' + p.angle + \" \" + cx + \" \" + cy + ')\">' + body + \"</g>\";\n s += M.part(\"stamp\", { at: tin, in: \"slam\", box: { x: cx - w / 2, y: cy - h / 2, w: w, h: h } }, inked);\n return s;\n}\n","libs":[]},{"id":"reactions","name":"Reactions","description":"Live reactions float up in a stream — hearts, likes, fire, laughs — swaying, popping in and fading out, like a live stream.","category":"social","tags":["annotation","reactions","hearts","likes","live","emoji","float"],"placement":"overlay","duration":{"default":5,"min":2,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"kind":{"type":"choice","label":"Reactions","options":["mixed","hearts","likes","fire","laughs","stars"],"default":"mixed"},"x":{"type":"number","label":"Stream across (%)","default":86,"min":0,"max":100,"step":1},"rate":{"type":"number","label":"Per second","default":4,"min":1,"max":12,"step":1},"size":{"type":"number","label":"Size","default":88,"min":36,"max":160,"step":4},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"stream":"the reactions"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/reactions","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const KINDS = { hearts: [\"heart\", \"#FF3B5C\"], likes: [\"thumbsUp\", \"#2F7BFF\"], fire: [\"fire\", \"#FF7A1A\"], laughs: [\"smile\", \"#FFC21A\"], stars: [\"star\", \"#B55CFF\"] };\n const names = p.kind === \"mixed\" ? [\"hearts\", \"likes\", \"fire\", \"laughs\", \"hearts\", \"stars\"] : [p.kind];\n const x0 = W * p.x / 100, life = 2.6, rise = H * 0.62, n = Math.ceil(($.dur - tin) * p.rate);\n const end = M.outAt;\n let out = \"\";\n for (let i = 0; i < n; i++) {\n const born = tin + i / p.rate + ($.hash(i * 1.7) - 0.5) * 0.15, age = t - born;\n if (age < 0 || age > life || born > end) continue;\n const kd = KINDS[names[Math.floor($.hash(i * 4.1) * names.length) % names.length]];\n const q = age / life, r = p.size * k * 0.5 * (0.8 + 0.4 * $.hash(i * 2.9));\n const x = x0 + Math.sin(age * 2.4 + i) * 34 * k + ($.hash(i * 6.3) - 0.5) * 60 * k;\n const y = H * 0.9 - rise * $.E.out2(q);\n const sc = Math.min(1, $.E.outBack($.clamp(age / 0.3))), op = q > 0.7 ? 1 - (q - 0.7) / 0.3 : 1;\n out += '<g opacity=\"' + op.toFixed(3) + '\" transform=\"translate(' + x + \" \" + y + \") scale(\" + sc + ')\">' + $.circle(0, 0, r, { fill: kd[1] }) + $.icon(kd[0], -r * 0.55, -r * 0.55, r * 1.1, { color: \"#FFFFFF\", fill: \"#FFFFFF\", sw: 2 }) + \"</g>\";\n }\n return s + M.part(\"stream\", { in: \"none\", out: \"none\", vary: false }, out);\n}\n\n/** A small pop as each reaction is born, panned with it and pitched a little differently. */\nfunction sound($) {\n const p = $.data, tin = $.beat(\"in\", 0.1), end = motion($, 0).outAt, n = Math.ceil(($.dur - tin) * p.rate);\n const out = [];\n for (let i = 0; i < n; i++) {\n const born = tin + i / p.rate + ($.hash(i * 1.7) - 0.5) * 0.15;\n if (born > end) continue;\n out.push({ at: Math.max(0, born), kind: \"pop\", level: 0.45, pitch: Math.round(($.hash(i * 3.3) - 0.5) * 8), pan: (p.x / 50 - 1) * 0.6 });\n }\n return out;\n}\n","libs":[]},{"id":"pointer","name":"Pointer","description":"A tech callout: a pulsing dot on the spot, a leader line that draws out at an angle, and a card with a title and a detail.","category":"callout","tags":["annotation","pointer","callout","leader","label","spec","tech"],"placement":"overlay","duration":{"default":4,"min":1.5,"max":15},"aspects":["16:9","9:16","1:1","4:5"],"props":{"title":{"type":"text","label":"Title","default":"48 MP sensor","maxLength":32},"detail":{"type":"text","label":"Detail","default":"Larger pixels, better low light","maxLength":64},"x":{"type":"number","label":"Point across (%)","default":42,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Point down (%)","default":55,"min":0,"max":100,"step":1},"side":{"type":"choice","label":"Card side","options":["auto","left","right"],"default":"auto"},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"dot":"the dot on the spot","line":"the leader line","card":"the card"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/pointer","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const tx = W * p.x / 100, ty = H * p.y / 100;\n const right = p.side === \"right\" || (p.side === \"auto\" && tx < W / 2);\n const up = ty > H * 0.4, dir = right ? 1 : -1;\n const tf = Object.assign({}, $.font(\"display\"), { size: 46 * k }), df = Object.assign({}, $.font(\"text\"), { size: 28 * k });\n const title = $.case(p.title), maxW = Math.min(W * 0.4, 560 * k);\n const dl = p.detail ? $.wrap(p.detail, maxW, df).slice(0, 3) : [];\n const cw = Math.min(maxW, Math.max($.measure(title, tf), widest($, dl, df))) + 56 * k, ch = 40 * k + tf.size * 1.1 + (dl.length ? 14 * k + dl.length * df.size * 1.3 : 0) + 4 * k;\n // An elbow: out at 45° for d1, then level to the card.\n const d1 = Math.min(W, H) * 0.14, ex = tx + dir * d1, ey = ty + (up ? -d1 : d1);\n let cx = right ? ex + Math.min(W, H) * 0.08 : ex - Math.min(W, H) * 0.08 - cw;\n cx = Math.min(W - cw - 24 * k, Math.max(24 * k, cx));\n const lineEnd = right ? cx : cx + cw, cy = Math.min(H - ch - 24 * k, Math.max(24 * k, ey - ch / 2));\n const lp = $.E.inOut2($.seg(t, tin + 0.3, tin + 0.8));\n s += M.part(\"dot\", { at: tin, in: \"pop\", box: { x: tx - 16 * k, y: ty - 16 * k, w: 32 * k, h: 32 * k } }, $.circle(tx, ty, 10 * k, { fill: col, stroke: \"#FFFFFF\", sw: 3 * k }) + pulse($, t, tin + 0.2, tx, ty, 12 * k, col));\n if (lp > 0) s += M.part(\"line\", { at: tin + 0.3, in: \"none\" }, $.path(\"M\" + tx + \" \" + ty + \"L\" + ex + \" \" + ey + \"L\" + lineEnd + \" \" + ey, { stroke: col, sw: 4 * k, draw: lp, cap: \"round\" }));\n let card = $.card(cx, cy, cw, ch, { t: t }) + $.rect(right ? cx : cx + cw - 8 * k, cy, 8 * k, ch, { fill: col });\n card += $.text(title, Object.assign({}, tf, { x: cx + 28 * k, y: cy + 22 * k, fill: c.fg }));\n dl.forEach(function (l, i) { card += $.text(l, Object.assign({}, df, { x: cx + 28 * k, y: cy + 22 * k + tf.size * 1.1 + 14 * k + i * df.size * 1.3, fill: c.mute })); });\n s += M.part(\"card\", { at: tin + 0.75, in: \"slide\", from: right ? \"left\" : \"right\", distance: 20, box: { x: cx, y: cy, w: cw, h: ch } }, card);\n return s;\n}\n","libs":[]},{"id":"emphasis","name":"Emphasis marks","description":"Comic emphasis round a point: action lines burst out and sparkles twinkle — for a punchline, a reveal or a wow.","category":"callout","tags":["annotation","emphasis","burst","sparkle","wow","lines","comic"],"placement":"overlay","duration":{"default":2.5,"min":1,"max":12},"aspects":["16:9","9:16","1:1","4:5"],"props":{"x":{"type":"number","label":"Centre across (%)","default":50,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Centre down (%)","default":45,"min":0,"max":100,"step":1},"size":{"type":"number","label":"Size","default":100,"min":40,"max":260,"step":5},"kind":{"type":"choice","label":"Marks","options":["both","lines","sparkles"],"default":"both"},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"lines":"the action lines","sparkles":"the sparkles"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/emphasis","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const cx = W * p.x / 100, cy = H * p.y / 100, R = p.size * k * 1.6;\n if (p.kind !== \"sparkles\") {\n let lines = \"\";\n for (let i = 0; i < 9; i++) {\n const a = -Math.PI * 0.95 + (i / 8) * Math.PI * 0.9 + ($.hash(i * 2.1) - 0.5) * 0.12;\n const r0 = R * (0.62 + 0.1 * $.hash(i * 5.5)), r1 = R * (0.95 + 0.25 * $.hash(i * 7.7));\n const pr = $.E.out3($.seg(t, tin + i * 0.03, tin + i * 0.03 + 0.25));\n const wob = 1 + 0.06 * Math.sin((t - tin) * 9 + i);\n lines += $.line(cx + Math.cos(a) * r0 * wob, cy + Math.sin(a) * r0 * wob, cx + Math.cos(a) * r1 * wob, cy + Math.sin(a) * r1 * wob, { p: pr, stroke: col, sw: (7 + 5 * $.hash(i)) * k });\n }\n s += M.part(\"lines\", { at: tin, in: \"none\" }, lines);\n }\n if (p.kind !== \"lines\") {\n let sp = \"\";\n const spots = [[-1.05, -0.55, 1], [1.0, -0.75, 0.7], [0.8, 0.45, 0.55], [-0.75, 0.6, 0.45]];\n spots.forEach(function (q, i) {\n const a = tin + 0.15 + i * 0.12, sc = $.E.outBack($.clamp((t - a) / 0.3)) * (1 + 0.18 * Math.sin((t - a) * 7 + i * 2)) * q[2];\n if (sc <= 0) return;\n const x = cx + q[0] * R, y = cy + q[1] * R, r = 34 * k * sc, n = r * 0.22;\n sp += $.path(\"M\" + x + \" \" + (y - r) + \"Q\" + (x + n) + \" \" + (y - n) + \" \" + (x + r) + \" \" + y + \"Q\" + (x + n) + \" \" + (y + n) + \" \" + x + \" \" + (y + r) + \"Q\" + (x - n) + \" \" + (y + n) + \" \" + (x - r) + \" \" + y + \"Q\" + (x - n) + \" \" + (y - n) + \" \" + x + \" \" + (y - r) + \"Z\", { fill: i % 2 ? c.accent2 : col });\n });\n s += M.part(\"sparkles\", { at: tin + 0.15, in: \"none\" }, sp);\n }\n return s;\n}\n\n/** A pop as the lines burst out, a shimmer as the sparkles turn up. */\nfunction sound($) {\n const p = $.data, tin = $.beat(\"in\", 0.1), out = [];\n if (p.kind !== \"sparkles\") out.push({ at: tin, kind: \"pop\", level: 0.9, pan: (p.x / 50 - 1) * 0.6 });\n if (p.kind !== \"lines\") out.push({ at: tin + 0.15, kind: \"shimmer\", dur: 0.7, level: 0.6, pan: (p.x / 50 - 1) * 0.6 });\n return out;\n}\n","libs":[]},{"id":"markers","name":"Numbered markers","description":"Numbered dots pop onto the frame one after another, each with a ring and an optional label — for walking through the parts of a shot.","category":"callout","tags":["annotation","markers","numbers","steps","points","parts","hotspots"],"placement":"overlay","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"points":{"type":"list","label":"Points","of":"text","default":["22 38 Lens","52 62 Grip","78 30 Screen"],"maxItems":8,"description":"one per line: across% down% label"},"gap":{"type":"number","label":"Seconds apart","default":0.45,"min":0.1,"max":3,"step":0.05},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"}},"parts":{"markers":"the numbered dots","labels":"their labels"},"beats":{"in":0.1},"pack":"annotate","ref":"annotate/markers","code":"// Annotations point at things in the footage: every place is a percentage of the frame (0–100).\nfunction spot($, x, y) { return [$.W * $.clamp(x / 100), $.H * $.clamp(y / 100)]; }\nfunction boxOf($, x, y, w, h) { return { x: $.W * x / 100, y: $.H * y / 100, w: $.W * w / 100, h: $.H * h / 100 }; }\n/** The annotation colour: the prop when given, else the accent. */\nfunction inkOf($, col) { return col || $.c.accent; }\n/** Which side of a point has the most room for a label. */\nfunction roomy($, x, y) {\n const room = { left: x, right: $.W - x, above: y, below: $.H - y };\n const wide = $.W >= $.H;\n let best = wide ? (x < $.W / 2 ? \"right\" : \"left\") : (y < $.H / 2 ? \"below\" : \"above\");\n if (room[best] < Math.min($.W, $.H) * 0.3) for (const s in room) if (room[s] > room[best]) best = s;\n return best;\n}\n/** A label tag (the brand's chip) centred on (cx, cy), kept inside the frame: { markup, box }. */\nfunction tagAt($, str, cx, cy, fill, size) {\n const txt = $.case(str), o = { size: (size || 40) * $.k, fill: fill, color: $.on(fill) };\n const sz = $.chip.size(txt, o), m = 24 * $.k;\n const x = Math.min($.W - m - sz.w, Math.max(m, cx - sz.w / 2)), y = Math.min($.H - m - sz.h, Math.max(m, cy - sz.h / 2));\n return { markup: $.chip(txt, x, y, o), box: { x: x, y: y, w: sz.w, h: sz.h } };\n}\n/** Pulsing rings on a point (from at). */\nfunction pulse($, t, at, x, y, r, col) {\n let s = \"\";\n for (let i = 0; i < 2; i++) {\n const q = ((t - at - i * 0.6) % 1.2) / 1.2;\n if (t - at - i * 0.6 < 0) continue;\n s += $.circle(x, y, r * (1 + q * 2.2), { stroke: col, sw: 4 * $.k, op: 0.8 * (1 - q) });\n }\n return s;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n const tin = $.beat(\"in\", 0.1), col = inkOf($, p.color);\n let s = \"\";\n const pts = (p.points || []).map(function (row) {\n const m = /^\\s*(-?[\\d.]+)[\\s,]+(-?[\\d.]+)\\s*(.*)$/.exec(String(row));\n return m ? { x: W * $.clamp(Number(m[1]) / 100), y: H * $.clamp(Number(m[2]) / 100), label: m[3] } : null;\n }).filter(Boolean);\n const r = 34 * k, nf = Object.assign({}, $.font(\"display\"), { size: 36 * k });\n let dots = \"\", labels = \"\";\n pts.forEach(function (q, i) {\n const a = tin + i * p.gap;\n dots += M.part(\"markers\", { at: a, in: \"pop\", box: { x: q.x - r, y: q.y - r, w: 2 * r, h: 2 * r } },\n $.circle(q.x, q.y, r, { fill: col, stroke: \"#FFFFFF\", sw: 4 * k, shadow: \"md\" }) + $.text(String(i + 1), Object.assign({}, nf, { x: q.x, y: q.y - nf.size * 0.56, anchor: \"middle\", fill: $.on(col) })) + pulse($, t, a + 0.2, q.x, q.y, r, col));\n if (q.label) {\n const tg = tagAt($, q.label, q.x + (q.x < W * 0.7 ? 1 : -1) * (r + 20 * k + $.chip.size($.case(q.label), { size: 32 * k }).w / 2), q.y, c.surface, 32);\n labels += M.part(\"labels\", { at: a + 0.15, in: \"rise\", distance: 12, box: tg.box }, tg.markup);\n }\n });\n return s + dots + labels;\n}\n","libs":[]}]},{"format":1,"id":"type","name":"MONARC Type","version":"1.0.0","description":"Kinetic typography on the brand's ground: typewriter, word swap, split-flap board, marquee bands, echo stack, liquid fill, decode, word by word, key phrase and an editorial stack.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/type","style":"tomato","shared":"shared.js","items":["typewriter","word-swap","split-flap","marquee","echo","liquid-fill","decode","word-by-word","key-phrase","stack"],"entries":[{"id":"typewriter","name":"Typewriter","description":"Words type on letter by letter with a blinking caret — in the brand's mono, text or display face.","category":"text","tags":["kinetic type","text","typewriter","typing","type on","caret","terminal"],"placement":"full","duration":{"default":5,"min":1.5,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"text":{"type":"longtext","label":"Text","default":"Every great edit starts with a single cut.","maxLength":200},"face":{"type":"choice","label":"Face","options":["mono","display","text"],"default":"mono"},"speed":{"type":"number","label":"Letters a second","default":18,"min":4,"max":60,"step":1}},"parts":{"ground":"the brand's ground","text":"the typed text"},"beats":{"in":0.2},"pack":"type","ref":"type/typewriter","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const face = p.face, txt = face === \"display\" ? $.case(p.text) : String(p.text || \"\");\n const font = Object.assign({}, $.font(face), { ls: face === \"mono\" ? 0 : -0.015 });\n const fit = fitLines($, txt, font, A.w * 0.86, Math.min(A.h * 0.2, 140 * k), 34 * k, 6);\n const w = widest($, fit.lines, fit.font), lh = face === \"display\" ? 1.08 : 1.3;\n const y0 = H / 2 - (fit.lines.length * fit.size * lh) / 2;\n s += M.text(\"text\", Object.assign({ at: tin, in: \"typewriter\", split: \"letters\", stagger: 1 / p.speed }, fit.font, { x: W / 2 - w / 2, y: y0, lh: lh, maxW: A.w * 0.86, fill: c.fg }), fit.lines.join(\"\\n\"));\n return s;\n}\n","libs":[]},{"id":"word-swap","name":"Word swap","description":"A fixed lead and a word that keeps changing: each new word rolls up into an accent box that stretches to fit it — “We make videos / ads / stories”.","category":"text","tags":["kinetic type","text","word swap","rotating","changing word","slot","carousel","hero"],"placement":"full","duration":{"default":6,"min":2,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"lead":{"type":"text","label":"Lead","default":"We make","maxLength":24},"words":{"type":"list","label":"Words","of":"text","default":["videos","ads","stories","brands"],"maxItems":8},"end":{"type":"text","label":"After","default":"","maxLength":12,"description":"words after the box (empty for none)"}},"parts":{"ground":"the brand's ground","lead":"the fixed words","box":"the box and its word"},"beats":{"in":0.2},"pack":"type","ref":"type/word-swap","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const lead = $.case(p.lead), end = p.end ? $.case(p.end) : \"\";\n const words = (p.words || []).map(function (w) { return $.case(String(w)); }).filter(Boolean);\n if (!words.length) words.push(\"…\");\n const tall = A.tall || A.square;\n const base = Object.assign({}, $.font(\"display\"), { ls: -0.03 });\n const pad = 0.28;\n const wid = function (sz, str) { return $.measure(str, Object.assign({}, base, { size: sz })); };\n // One size for every word: the widest must fit with the lead (side by side on wide frames, stacked on tall).\n const widestWord = words.reduce(function (a, w) { return wid(100, w) > wid(100, a) ? w : a; }, words[0]);\n const row = function (sz) { return tall ? Math.max(wid(sz, lead), wid(sz, widestWord) + sz * pad * 2, end ? wid(sz, end) : 0) : wid(sz, lead) + sz * 0.3 + wid(sz, widestWord) + sz * pad * 2 + (end ? sz * 0.3 + wid(sz, end) : 0); };\n let size = Math.min(150 * k, A.h * (tall ? 0.16 : 0.28));\n if (row(size) > A.w * 0.94) size *= (A.w * 0.94) / row(size);\n const f = Object.assign({}, base, { size: size });\n const per = Math.max(0.6, (M.outAt - tin - 0.3) / words.length);\n const rel = t - tin - 0.3, idx = $.clamp(Math.floor(rel / per), 0, words.length - 1), loc = rel - idx * per;\n const sw = $.E.out3($.clamp(loc / 0.4)), cur = words[idx], prev = idx > 0 ? words[idx - 1] : null;\n const bwOf = function (w) { return wid(size, w) + size * pad * 2; };\n const bw = prev && idx > 0 ? $.lerp(bwOf(prev), bwOf(cur), sw) : bwOf(cur), bh = size * 1.32;\n const lw = wid(size, lead), ew = end ? wid(size, end) : 0, gap = size * 0.3;\n let lx, bx, ex, ly, by;\n if (tall) {\n const h = bh * 2.1 + (end ? size * 1.15 : 0);\n ly = H / 2 - h / 2; by = ly + size * 1.25;\n lx = W / 2 - lw / 2; bx = W / 2 - bw / 2; ex = W / 2 - ew / 2;\n } else {\n const total = lw + gap + bw + (end ? gap + ew : 0);\n lx = W / 2 - total / 2; bx = lx + lw + gap; ex = bx + bw + gap;\n by = H / 2 - bh / 2; ly = by + (bh - size) / 2 - size * 0.04;\n }\n s += M.text(\"lead\", Object.assign({ at: tin, in: \"rise\" }, f, { x: lx, y: ly, fill: c.fg }), lead);\n if (end) s += M.text(\"lead\", Object.assign({ at: tin, in: \"rise\" }, f, { x: ex, y: tall ? by + bh + size * 0.1 : ly, fill: c.fg }), end);\n // The words roll through the box: the old one up and out, the new one up from below.\n const ty = by + (bh - size) / 2 - size * 0.04, roll = bh;\n let inner = $.text(cur, Object.assign({}, f, { x: bx + bw / 2, y: ty + (idx > 0 ? (1 - sw) * roll : 0), anchor: \"middle\", fill: $.on(c.accent) }));\n if (prev && sw < 1) inner += $.text(prev, Object.assign({}, f, { x: bx + bw / 2, y: ty - sw * roll, anchor: \"middle\", fill: $.on(c.accent) }));\n s += M.part(\"box\", { at: tin + 0.2, in: \"pop\", box: { x: bx, y: by, w: bw, h: bh } }, $.rect(bx, by, bw, bh, { r: Math.min($.style.radius, bh / 2), fill: c.accent }) + $.clip(bx, by, bw, bh, 0, inner));\n return s;\n}\n","libs":[]},{"id":"split-flap","name":"Split-flap board","description":"An airport split-flap board: every cell flutters through letters and clacks onto the message, left to right.","category":"text","tags":["kinetic type","text","split flap","flap","board","airport","departures","solari"],"placement":"full","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"text":{"type":"longtext","label":"Message","default":"NOW BOARDING\nGATE 7","maxLength":60,"description":"up to three lines; new line for a new row"}},"parts":{"ground":"the brand's ground","board":"the board"},"beats":{"in":0.2},"pack":"type","ref":"type/split-flap","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const POOL = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let rows = String(p.text || \"\").toUpperCase().split(\"\\n\").map(function (r) { return r.trim(); }).filter(Boolean).slice(0, 3);\n if (!rows.length) rows.push(\"HELLO\");\n // Tall frames: long rows break between words so the cells stay big.\n if (A.tall || A.square) {\n const max = A.tall ? 8 : 11, out = [];\n rows.forEach(function (r) {\n let cur = \"\";\n r.split(/\\s+/).forEach(function (w) { if (cur && (cur + \" \" + w).length > max) { out.push(cur); cur = w; } else cur = cur ? cur + \" \" + w : w; });\n if (cur) out.push(cur);\n });\n rows = out.slice(0, 5);\n }\n const cols = Math.max.apply(null, rows.map(function (r) { return r.length; }));\n // A board is black with white letters whatever the brand; the brand shows in the landing flash.\n const cellFill = \"#151517\", ink = \"#F4F1EA\";\n const cw = Math.min((A.w * 0.96) / cols, (A.h * 0.7) / rows.length / 1.36) * 0.94, ch = cw * 1.36, gap = cw * 0.07;\n const bw = cols * (cw + gap) - gap, bh = rows.length * (ch + gap * 2) - gap * 2;\n const x0 = W / 2 - bw / 2, y0 = H / 2 - bh / 2;\n const f = Object.assign({}, $.font(\"mono\"), { size: ch * 0.62, weight: 700 });\n let out = \"\";\n rows.forEach(function (row, r) {\n const padL = Math.floor((cols - row.length) / 2);\n for (let q = 0; q < cols; q++) {\n const target = row.charAt(q - padL) || \" \";\n const x = x0 + q * (cw + gap), y = y0 + r * (ch + gap * 2);\n const start = tin + q * 0.045 + r * 0.12, flips = target === \" \" ? 0 : 5 + Math.floor($.hash(q * 3.1 + r * 7.7) * 9), fd = 0.065;\n const n = Math.floor((t - start) / fd), fr = ((t - start) / fd) % 1;\n const done = t < start ? false : n >= flips;\n const ch2 = t < start ? \" \" : done ? target : POOL.charAt(Math.floor($.hash(n * 1.37 + q + r * 9) * POOL.length));\n out += $.rect(x, y, cw, ch, { r: cw * 0.1, fill: cellFill });\n if (ch2 !== \" \") out += $.text(ch2, Object.assign({}, f, { x: x + cw / 2, y: y + ch / 2 - f.size * 0.56, anchor: \"middle\", fill: done && t - start - flips * fd < 0.25 ? c.accent : ink }));\n // The flap falling: a shade over the top half shrinking to the hinge, then over the bottom half growing.\n if (t >= start && !done) {\n const half = ch / 2;\n if (fr < 0.5) out += $.rect(x, y + half * 2 * fr, cw, half * (1 - 2 * fr), { fill: $.rgba(cellFill, 0.85) });\n else out += $.rect(x, y + half, cw, half * (2 * fr - 1), { fill: $.rgba(\"#000000\", 0.35) });\n }\n out += $.rect(x, y + ch / 2 - Math.max(1, cw * 0.018), cw, Math.max(2, cw * 0.036), { fill: $.rgba(\"#000000\", 0.45) });\n }\n });\n s += M.part(\"board\", { at: tin, in: \"fade\", dur: 0.25 }, out);\n return s;\n}\n","libs":[]},{"id":"marquee","name":"Marquee bands","description":"Big words repeat along tilted bands that scroll in opposite directions — solid on accent, outlined on the ground — like a poster in motion.","category":"text","tags":["kinetic type","text","marquee","ticker","scrolling","bands","poster","repeat"],"placement":"full","duration":{"default":6,"min":2,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"text":{"type":"text","label":"Words","default":"New drop","maxLength":28},"bands":{"type":"number","label":"Bands","default":3,"min":1,"max":5,"step":1},"angle":{"type":"number","label":"Tilt","default":-8,"min":-30,"max":30,"step":1},"speed":{"type":"number","label":"Speed","default":1,"min":0.2,"max":3,"step":0.1}},"parts":{"ground":"the brand's ground","bands":"the bands"},"beats":{"in":0.2},"pack":"type","ref":"type/marquee","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const str = $.case(p.text), n = Math.max(1, Math.round(p.bands));\n const D = Math.hypot(W, H), bandH = Math.min(H, W) * (n === 1 ? 0.3 : 0.62 / n + 0.06);\n const f = Object.assign({}, $.font(\"display\"), { size: bandH * 0.64, ls: -0.02 });\n const sep = \" • \", unit = $.measure(str + sep, f);\n let out = \"\";\n for (let i = 0; i < n; i++) {\n const y = (i - (n - 1) / 2) * bandH * 1.12, solid = i % 2 === 0;\n const dir = i % 2 ? -1 : 1, off = (((t * 140 * k * p.speed * dir) % unit) + unit) % unit;\n let row = \"\";\n for (let x = -D / 2 - unit + off; x < D / 2 + unit; x += unit) row += $.text(str + sep, Object.assign({}, f, { within: { x: -1e5, y: -1e5, w: 2e5, h: 2e5 }, x: x, y: y - f.size * 0.56, fill: solid ? $.on(c.accent) : \"none\", stroke: solid ? undefined : c.fg, sw: solid ? undefined : 2.6 * k }));\n const band = (solid ? $.rect(-D / 2, y - bandH / 2, D, bandH, { fill: c.accent }) : \"\") + row;\n out += M.part(\"bands\", { at: tin + i * 0.12, in: \"slide\", from: i % 2 ? \"right\" : \"left\", distance: 160, box: { x: -D / 2, y: y - bandH / 2, w: D, h: bandH } }, band);\n }\n s += '<g transform=\"translate(' + W / 2 + \" \" + H / 2 + \") rotate(\" + p.angle + ')\">' + out + \"</g>\";\n return s;\n}\n","libs":[]},{"id":"echo","name":"Echo stack","description":"One word with outlined echoes fanning out above and below it, rippling in a slow wave — a poster headline that breathes.","category":"text","tags":["kinetic type","text","echo","repeat","outline","stack","poster","wave"],"placement":"full","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"word":{"type":"text","label":"Word","default":"Echo","maxLength":16},"copies":{"type":"number","label":"Echoes each side","default":4,"min":1,"max":7,"step":1}},"parts":{"ground":"the brand's ground","word":"the solid word","echoes":"the outlined echoes"},"beats":{"in":0.2},"pack":"type","ref":"type/echo","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const str = $.case(p.word), n = Math.max(1, Math.round(p.copies));\n const f = fitFont($, str, \"display\", Math.min(A.h / (2 * n + 1) * 1.45, 260 * k), A.w * 0.92, { ls: -0.03 });\n const step = f.size * 0.74, cy = H / 2 - f.size * 0.56;\n let ech = \"\";\n for (let j = n; j >= 1; j--) {\n const open = $.E.outBack($.clamp((t - tin - 0.25 - j * 0.07) / 0.6));\n for (const sgn of [-1, 1]) {\n const wob = Math.sin((t - tin) * 2.2 - j * 0.55) * 16 * k * j / n;\n ech += $.text(str, Object.assign({}, f, { x: W / 2 + wob * sgn, y: cy + sgn * j * step * open, anchor: \"middle\", fill: \"none\", stroke: j % 2 ? c.accent : c.accent2, sw: 2.6 * k, op: Math.max(0.15, 1 - j * 0.16) }));\n }\n }\n s += M.part(\"echoes\", { at: tin + 0.2, in: \"fade\", dur: 0.3 }, ech);\n s += M.text(\"word\", Object.assign({ at: tin, in: \"pop\" }, f, { x: W / 2, y: cy, anchor: \"middle\", fill: c.fg }), str);\n return s;\n}\n","libs":[]},{"id":"liquid-fill","name":"Liquid fill","description":"A big outlined word fills from the bottom with a wavy liquid in the brand's accents, then a tagline rises under it.","category":"text","tags":["kinetic type","text","liquid","fill","outline","wave","water","pour"],"placement":"full","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"word":{"type":"text","label":"Word","default":"Fresh","maxLength":16},"tagline":{"type":"text","label":"Tagline","default":"Squeezed this morning","maxLength":48,"description":"empty for none"}},"parts":{"ground":"the brand's ground","outline":"the outlined word","fill":"the liquid","tagline":"the tagline"},"beats":{"in":0.2},"pack":"type","ref":"type/liquid-fill","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const str = $.case(p.word);\n const f = fitFont($, str, \"display\", Math.min(A.h * 0.42, 320 * k), A.w * 0.92, { ls: -0.03 });\n const tw = $.measure(str, f), x0 = W / 2 - tw / 2, y = H / 2 - f.size * (p.tagline ? 0.72 : 0.56);\n const top = y + f.size * 0.12, bot = y + f.size * 0.96;\n const word = function (fill, stroke) { return $.text(str, Object.assign({}, f, { x: W / 2, y: y, anchor: \"middle\", fill: fill, stroke: stroke, sw: stroke ? 3.2 * k : undefined })); };\n const lvl = $.E.inOut2($.seg(t, tin + 0.4, tin + 2.2));\n const wave = function (level, amp, ph, id) {\n let d = \"M\" + (x0 - 20 * k) + \" \" + (bot + 20 * k);\n for (let i = 0; i <= 30; i++) { const x = x0 - 20 * k + ((tw + 40 * k) * i) / 30; d += \"L\" + x + \" \" + (level + Math.sin(i * 0.65 + (t - tin) * 4 + ph) * amp); }\n d += \"L\" + (x0 + tw + 20 * k) + \" \" + (bot + 20 * k) + \"Z\";\n $.def('<clipPath id=\"' + id + '\"><path d=\"' + d + '\"/></clipPath>');\n return 'clip-path=\"url(#' + id + ')\"';\n };\n const amp = f.size * 0.045 * (1 - 0.6 * lvl), L = $.lerp(bot + amp, top - amp * 2, lvl);\n s += M.part(\"outline\", { at: tin, in: \"fade\" }, word(\"none\", c.fg));\n if (lvl > 0) s += M.part(\"fill\", { at: tin + 0.4, in: \"none\" }, \"<g \" + wave(L - f.size * 0.05, amp, 1.8, $.uid(\"lq\")) + \">\" + word(c.accent2) + \"</g><g \" + wave(L, amp, 0, $.uid(\"lq\")) + \">\" + word(c.accent) + \"</g>\");\n if (p.tagline) {\n const tf = Object.assign({}, $.font(\"text\"), { size: Math.max(32 * k, f.size * 0.16) });\n s += M.text(\"tagline\", Object.assign({ at: tin + 2, in: \"rise\", distance: 18 }, tf, { x: W / 2, y: bot + f.size * 0.18, anchor: \"middle\", fill: c.mute, maxW: A.w }), $.case(p.tagline));\n }\n return s;\n}\n","libs":[]},{"id":"decode","name":"Decode","description":"A title decodes letter by letter out of scrambled glyphs between brackets, then a mono line types on under it.","category":"text","tags":["kinetic type","text","decode","scramble","hacker","glitch","reveal","tech"],"placement":"full","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"title":{"type":"text","label":"Title","default":"Access granted","maxLength":28},"line":{"type":"text","label":"Line","default":"loading your workspace","maxLength":60,"description":"empty for none"}},"parts":{"ground":"the brand's ground","brackets":"the brackets","title":"the title","line":"the mono line"},"beats":{"in":0.2},"pack":"type","ref":"type/decode","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const str = $.case(p.title);\n const fit = fitLines($, str, Object.assign({}, $.font(\"display\"), { ls: -0.02 }), A.w * 0.8, Math.min(170 * k, A.h * 0.3), 60 * k, 2);\n const lh = fit.size * 1.05, h = fit.lines.length * lh, y0 = H / 2 - h / 2 - (p.line ? 40 * k : 0);\n const w = widest($, fit.lines, fit.font);\n fit.lines.forEach(function (l, i) {\n s += M.text(\"title\", Object.assign({ at: tin + 0.2 + i * 0.25, in: \"scramble\", split: \"letters\", dur: 0.7, stagger: Math.min(0.06, 0.9 / l.length) }, fit.font, { x: W / 2, y: y0 + i * lh, anchor: \"middle\", fill: c.fg }), l);\n });\n // Brackets open out from the centre to frame the title.\n const bo = $.E.out3($.seg(t, tin, tin + 0.5)), bx = (w / 2 + 50 * k) * bo, bh = h + 30 * k, by = y0 - 18 * k, arm = 34 * k;\n const br = function (x, dir) { return $.path(\"M\" + (x + dir * arm) + \" \" + by + \"H\" + x + \"V\" + (by + bh) + \"H\" + (x + dir * arm), { stroke: c.accent, sw: 7 * k, cap: \"square\", join: \"miter\" }); };\n s += M.part(\"brackets\", { at: tin, in: \"fade\", dur: 0.2 }, br(W / 2 - bx, 1) + br(W / 2 + bx, -1));\n if (p.line) {\n const mf = Object.assign({}, $.font(\"mono\"), { size: Math.max(28 * k, fit.size * 0.22) });\n const txt = \"> \" + p.line, mw = $.measure(txt, mf);\n s += M.text(\"line\", Object.assign({ at: tin + 1.1, in: \"typewriter\", split: \"letters\", stagger: 0.04 }, mf, { x: W / 2 - Math.min(mw, A.w) / 2, y: by + bh + 40 * k, fill: c.accent, maxW: A.w }), txt);\n }\n return s;\n}\n","libs":[]},{"id":"word-by-word","name":"Word by word","description":"A sentence one word at a time, each word slammed full-frame on a new ground — the brand's, the accent, the second accent — cut to a steady pace. *Marked* words go big.","category":"text","tags":["kinetic type","text","word by word","kinetic","fast","punchy","quote","beat"],"placement":"full","duration":{"default":5,"min":1.5,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"text":{"type":"longtext","label":"Text","default":"Stop scrolling. *This* changes everything.","maxLength":160,"description":"wrap a word in *stars* to make it the accent"},"pace":{"type":"number","label":"Seconds a word","default":0.42,"min":0.15,"max":2,"step":0.01}},"parts":{"ground":"the brand's ground","words":"the words"},"beats":{"in":0.2},"pack":"type","ref":"type/word-by-word","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const ws = markedWords($.case(p.text));\n if (!ws.length) return s;\n const pace = Math.max(0.15, Math.min(p.pace, (M.outAt - tin) / ws.length));\n const i = $.clamp(Math.floor((t - tin) / pace), 0, ws.length - 1), loc = t - tin - i * pace;\n if (t < tin) return s;\n const w = ws[i], grounds = [null, c.accent, c.fg, null, c.accent2];\n const g = w.marked ? c.accent : grounds[i % grounds.length];\n let fr = g ? $.rect(0, 0, W, H, { fill: g }) : \"\";\n const ink = g ? $.on(g) : c.fg;\n const font = w.marked ? Object.assign({}, $.font(\"accent\")) : Object.assign({}, $.font(\"display\"), { ls: -0.035 });\n const f = fitFont($, w.word, w.marked ? \"accent\" : \"display\", Math.min(A.h * 0.5, (w.marked ? 340 : 280) * k), A.w * 0.94, { ls: w.marked ? -0.01 : -0.035 });\n const pop = 1 + 0.22 * (1 - $.E.out3($.clamp(loc / 0.14))), rot = (i % 2 ? 1 : -1) * 2.5 * (1 - $.E.out3($.clamp(loc / 0.2)));\n fr += '<g transform=\"translate(' + W / 2 + \" \" + H / 2 + \") rotate(\" + rot + \") scale(\" + pop + \") translate(\" + -W / 2 + \" \" + -H / 2 + ')\">' + $.text(w.word, Object.assign({}, f, { x: W / 2, y: H / 2 - f.size * 0.56, anchor: \"middle\", fill: ink })) + \"</g>\";\n s += M.part(\"words\", { at: tin, in: \"none\" }, fr);\n return s;\n}\n\n/** A tick for every word as it lands, a hit for a marked one. */\nfunction sound($) {\n const p = $.data, tin = $.beat(\"in\", 0.2), ws = markedWords($.case(p.text));\n if (!ws.length) return [];\n const pace = Math.max(0.15, Math.min(p.pace, (motion($, 0).outAt - tin) / ws.length));\n return ws.map(function (w, i) {\n return w.marked ? { at: tin + i * pace, kind: \"hit\", level: 0.8 } : { at: tin + i * pace, kind: \"tick\", level: 0.9, pitch: (i % 3) - 1 };\n });\n}\n","libs":[]},{"id":"key-phrase","name":"Key phrase","description":"A sentence sets in big type, then its *marked* words get a marker swipe one after another — the line people should remember.","category":"text","tags":["kinetic type","text","key phrase","highlight","marker","quote","emphasis","sentence"],"placement":"full","duration":{"default":5,"min":1.5,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"text":{"type":"longtext","label":"Text","default":"Good editing is *invisible* until it is *missing*.","maxLength":140,"description":"wrap words in *stars* to mark them"}},"parts":{"ground":"the brand's ground","text":"the sentence","marks":"the marker swipes"},"beats":{"in":0.2},"pack":"type","ref":"type/key-phrase","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const ws = markedWords($.case(p.text)), plain = ws.map(function (w) { return w.word; }).join(\" \");\n const fit = fitLines($, plain, Object.assign({}, $.font(\"display\"), { ls: -0.025 }), A.w * 0.9, Math.min(150 * k, A.h * 0.22), 48 * k, 5);\n const lh = fit.size * 1.18, y0 = H / 2 - (fit.lines.length * lh) / 2;\n let wi = 0, marks = \"\", nMark = 0;\n const flags = [], boxes = [];\n fit.lines.forEach(function (line, li) {\n const lay = M.layout(line, Object.assign({}, fit.font, { x: W / 2, y: y0 + li * lh, anchor: \"middle\" }), \"words\");\n lay.units.forEach(function (u) { flags.push(ws[wi] && ws[wi].marked); boxes.push(u); wi++; });\n });\n const tMarks = tin + 0.35 + fit.lines.length * 0.12, markAt = [];\n boxes.forEach(function (u, j) {\n if (!flags[j]) return;\n const a = tMarks + nMark * 0.35;\n markAt[j] = a;\n const pr = $.E.out3($.seg(t, a, a + 0.32)), pad = fit.size * 0.1;\n if (pr > 0) marks += $.path(\"M\" + (u.x - pad) + \" \" + (u.y + fit.size * 0.18) + \"h\" + (u.w + 2 * pad) * pr + \"l\" + -pad * 0.4 + \" \" + fit.size * 0.86 + \"h\" + -(u.w + 2 * pad) * pr + \"Z\", { fill: c.accent2 });\n nMark++;\n });\n s += M.part(\"marks\", { at: tMarks, in: \"none\" }, marks);\n let base = 0;\n fit.lines.forEach(function (line, li) {\n const off = base;\n s += M.text(\"text\", Object.assign({ at: tin + li * 0.12, in: \"rise\", split: \"words\", stagger: 0.05, fillFor: function (u) { const j = off + u.index; return flags[j] && t >= markAt[j] + 0.1 ? $.on(c.accent2) : c.fg; } }, fit.font, { x: W / 2, y: y0 + li * lh, anchor: \"middle\", fill: c.fg }), line);\n base += line.split(/\\s+/).filter(Boolean).length;\n });\n return s;\n}\n","libs":[]},{"id":"stack","name":"Editorial stack","description":"Lines stack like a magazine cover — heavy capitals alternating with *italic* accent lines — sliding in from alternate sides between hairline rules.","category":"text","tags":["kinetic type","text","stack","editorial","magazine","serif","italic","headline"],"placement":"full","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"lines":{"type":"list","label":"Lines","of":"text","default":["Make","*things*","people","*remember*"],"maxItems":6,"description":"wrap a line in *stars* for the italic accent face"}},"parts":{"ground":"the brand's ground","lines":"the lines","rules":"the hairlines"},"beats":{"in":0.2},"pack":"type","ref":"type/stack","code":"// Kinetic type on the brand's ground (and its pattern). Text sits inside the title-safe area.\nfunction ground(M, $, t) {\n return M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }));\n}\n/** Words with *marks* → [{ word, marked }]. */\nfunction markedWords(str) {\n const out = [];\n String(str || \"\").split(/\\s+/).filter(Boolean).forEach(function (w) {\n const m = /^\\*(.*)\\*([.,!?;:]*)$/.exec(w);\n out.push(m ? { word: m[1] + m[2], marked: true } : { word: w.replace(/\\*/g, \"\"), marked: false });\n });\n return out;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.2);\n let s = ground(M, $, t);\n const rows = (p.lines || []).map(function (l) { const m = /^\\*(.*)\\*$/.exec(String(l).trim()); return m ? { text: m[1], acc: true } : { text: $.case(String(l).trim()), acc: false }; }).filter(function (r) { return r.text; });\n if (!rows.length) return s;\n const maxH = (A.h * 0.92) / rows.length;\n const sized = rows.map(function (r) { return fitFont($, r.text, r.acc ? \"accent\" : \"display\", maxH / 1.0, A.w * 0.92, { ls: r.acc ? -0.01 : -0.04 }); });\n const total = sized.reduce(function (a, f) { return a + f.size * 0.98; }, 0);\n let y = H / 2 - total / 2;\n rows.forEach(function (r, i) {\n const f = sized[i];\n s += M.text(\"lines\", Object.assign({ at: tin + i * 0.16, in: \"slide\", from: i % 2 ? \"right\" : \"left\", distance: 60 }, f, { x: W / 2, y: y - f.size * 0.06, anchor: \"middle\", fill: r.acc ? c.accent : c.fg }), r.text);\n y += f.size * 0.98;\n if (i < rows.length - 1) s += M.part(\"rules\", { at: tin + i * 0.16 + 0.25, in: \"grow-x\", origin: \"center\", box: { x: A.x, y: y - 2 * k, w: A.w, h: 4 * k } }, $.rect(A.x + A.w * 0.2, y - 1.5 * k, A.w * 0.6, 3 * k, { fill: $.rgba(c.fg, 0.35) }));\n });\n return s;\n}\n","libs":[]}]},{"format":1,"id":"backdrops","name":"MONARC Backdrops","version":"1.0.0","description":"Grounds that move, to set titles, products or footage on: any of sixteen patterns, aurora, a Bauhaus field, a synthwave grid, lava blobs, a colour sweep, a starfield, bokeh and topographic lines.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/backdrops","style":"aurora","shared":"shared.js","items":["pattern","aurora","tiles","retro-grid","blobs","gradient","starfield","bokeh","topo"],"entries":[{"id":"pattern","name":"Pattern","description":"Any of the sixteen animated patterns as a full ground — mesh, halftone, dither, Bauhaus tiles, waves, rings, dots, confetti, pills, sunburst… — in the brand's colours or your own.","category":"scene","tags":["backdrop","background","loop","pattern","texture","motion background"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"kind":{"type":"choice","label":"Pattern","options":["mesh","halftone","dither","tiles","bauhaus","waves","rings","dots","plus","graph","hatch","stripes","checker","confetti","pills","sunburst"],"default":"mesh"},"strength":{"type":"number","label":"Strength","default":1,"min":0.2,"max":2,"step":0.1},"scale":{"type":"number","label":"Scale","default":1,"min":0.4,"max":3,"step":0.1},"speed":{"type":"number","label":"Speed","default":1,"min":0,"max":4,"step":0.1},"color":{"type":"color","label":"Colour","default":"","description":"empty: the brand's colour"},"color2":{"type":"color","label":"Second colour","default":"","description":"empty: the brand's colour"},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the ground","pattern":"the pattern"},"pack":"backdrops","ref":"backdrops/pattern","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, plain($, t));\n s += M.part(\"pattern\", { in: \"fade\", out: \"none\", dur: 0.6, vary: false }, $.pattern(t, p.kind, { a: p.strength, scale: p.scale, speed: p.speed, color: p.color || undefined, color2: p.color2 || undefined }));\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n","libs":[]},{"id":"aurora","name":"Aurora","description":"Soft blooms of the brand's colours drift and melt into each other under a fine grain — a calm, premium ground for titles.","category":"scene","tags":["backdrop","background","loop","aurora","mesh","gradient","glow","soft","premium"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"intensity":{"type":"number","label":"Intensity","default":1.2,"min":0.4,"max":2,"step":0.1},"speed":{"type":"number","label":"Speed","default":1,"min":0,"max":3,"step":0.1},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the ground","light":"the drifting light"},"pack":"backdrops","ref":"backdrops/aurora","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, plain($, t));\n let l = $.pattern(t, \"mesh\", { a: p.intensity, speed: p.speed * 1.4, scale: 1 });\n // A second, smaller bloom in the second accent crossing the frame.\n const x = W * (0.5 + 0.35 * Math.sin(t * 0.21 * p.speed)), y = H * (0.5 + 0.3 * Math.cos(t * 0.17 * p.speed));\n l += $.circle(x, y, Math.max(W, H) * 0.35, { fill: $.gradient([[0, c.accent2, 0.35 * p.intensity], [1, c.accent2, 0]], { radial: true }) });\n s += M.part(\"light\", { in: \"fade\", out: \"none\", dur: 0.8, vary: false }, l) + $.grain(t, { op: 0.06 });\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n","libs":[]},{"id":"tiles","name":"Bauhaus field","description":"A full field of Bauhaus tiles — quarter circles, half discs, discs, diamonds — turning in a diagonal wave, in the brand's colours.","category":"scene","tags":["backdrop","background","loop","bauhaus","tiles","geometric","shapes","grid","swiss"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"size":{"type":"number","label":"Tile size","default":1.4,"min":0.5,"max":3,"step":0.1},"speed":{"type":"number","label":"Speed","default":1,"min":0,"max":3,"step":0.1},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the ground","tiles":"the tiles"},"pack":"backdrops","ref":"backdrops/tiles","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, plain($, t));\n s += M.part(\"tiles\", { in: \"fade\", out: \"none\", dur: 0.5, vary: false }, $.pattern(t, \"tiles\", { a: 1, scale: p.size, speed: p.speed }));\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n","libs":[]},{"id":"retro-grid","name":"Retro grid","description":"Synthwave: a neon grid floor rushing toward you under a striped sun and a glowing horizon, stars twinkling above.","category":"scene","tags":["backdrop","background","loop","synthwave","retro","grid","80s","neon","vaporwave","sun"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"speed":{"type":"number","label":"Speed","default":1,"min":0,"max":3,"step":0.1},"sun":{"type":"boolean","label":"Sun","default":true},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the sky","sun":"the sun","floor":"the grid floor"},"pack":"backdrops","ref":"backdrops/retro-grid","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, plain($, t));\n const hz = H * 0.58, cx = W / 2;\n // Sky: the ground darkening upward, a glow on the horizon, stars.\n let sky = $.rect(0, 0, W, hz, { fill: $.gradient([[0, \"#000000\", 0.35], [1, c.accent, 0.28]], { angle: 180 }) });\n for (let i = 0; i < 60; i++) {\n const x = $.hash(i * 3.1) * W, y = $.hash(i * 7.3) * hz * 0.85, tw = 0.4 + 0.6 * Math.abs(Math.sin(t * (1 + $.hash(i)) + i));\n sky += $.circle(x, y, (1 + 2 * $.hash(i * 5.5)) * k, { fill: c.fg, op: 0.6 * tw });\n }\n s += M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, sky);\n if (p.sun) {\n const R = Math.min(W, H) * 0.26, sy = hz - R * 0.35, id = $.uid(\"sun\");\n let cuts = \"\";\n for (let i = 0; i < 6; i++) { const yy = sy + R * (0.05 + i * 0.16) + ((t * 20 * k * p.speed) % (R * 0.16)), hh = R * (0.02 + i * 0.018); cuts += '<rect x=\"' + (cx - R) + '\" y=\"' + yy + '\" width=\"' + 2 * R + '\" height=\"' + hh + '\" fill=\"#000\"/>'; }\n $.def('<mask id=\"' + id + '\" maskUnits=\"userSpaceOnUse\" x=\"' + (cx - R) + '\" y=\"' + (sy - R) + '\" width=\"' + 2 * R + '\" height=\"' + 2 * R + '\"><rect x=\"' + (cx - R) + '\" y=\"' + (sy - R) + '\" width=\"' + 2 * R + '\" height=\"' + 2 * R + '\" fill=\"#fff\"/>' + cuts + \"</mask>\");\n s += M.part(\"sun\", { in: \"rise\", distance: 60, box: { x: cx - R, y: sy - R, w: 2 * R, h: 2 * R } }, $.circle(cx, sy, R * 1.35, { fill: $.gradient([[0, c.accent2, 0.35], [1, c.accent2, 0]], { radial: true }) }) + '<g mask=\"url(#' + id + ')\">' + $.circle(cx, sy, R, { fill: $.gradient([c.accent2, c.accent], { angle: 180 }) }) + \"</g>\");\n }\n // The floor: lines converging on the centre of the horizon; cross lines rushing toward the viewer.\n let floor = $.rect(0, hz, W, H - hz, { fill: $.gradient([[0, c.accent, 0.25], [1, \"#000000\", 0.4]], { angle: 180 }) });\n const fh = H - hz, sw = 2.5 * k;\n for (let i = -16; i <= 16; i++) floor += $.line(cx + i * W * 0.012, hz, cx + i * W * 0.16, H + fh, { stroke: c.accent, sw: sw, op: 0.8 });\n const ph = (t * 0.55 * p.speed) % 1;\n for (let j = 0; j < 14; j++) {\n const z = (j + 1 - ph) / 14, y = hz + fh * Math.pow(z, 2.6);\n if (y > hz) floor += $.line(0, y, W, y, { stroke: c.accent, sw: sw * (0.4 + z), op: 0.25 + 0.7 * z });\n }\n floor += $.rect(0, hz - 2 * k, W, 4 * k, { fill: c.accent2 });\n s += M.part(\"floor\", { in: \"none\", out: \"none\", vary: false }, floor);\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n","libs":[]},{"id":"blobs","name":"Lava blobs","description":"Gooey blobs in the accents drift, merge and pull apart like a lava lamp.","category":"scene","tags":["backdrop","background","loop","blobs","lava","goo","liquid","metaballs","organic"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"count":{"type":"number","label":"Blobs","default":7,"min":3,"max":14,"step":1},"speed":{"type":"number","label":"Speed","default":1,"min":0,"max":3,"step":0.1},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the ground","blobs":"the blobs"},"pack":"backdrops","ref":"backdrops/blobs","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, plain($, t));\n const n = Math.round(p.count), R = Math.min(W, H) * 0.13, fid = $.uid(\"goo\");\n $.def('<filter id=\"' + fid + '\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"' + 30 * k + '\"/><feColorMatrix type=\"matrix\" values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 26 -11\"/></filter>');\n const layer = function (col, off) {\n let g = \"\";\n for (let i = 0; i < n; i++) {\n const sp = (0.15 + 0.2 * $.hash(i * 2.7 + off)) * p.speed, ph = $.hash(i * 9.1 + off) * 6.28;\n const x = W * (0.5 + 0.42 * Math.sin(t * sp + ph)), y = H * (0.5 + 0.4 * Math.sin(t * sp * 1.3 + ph * 1.7));\n g += $.circle(x, y, R * (0.6 + 0.8 * $.hash(i * 4.4 + off)), { fill: col });\n }\n return '<g filter=\"url(#' + fid + ')\">' + g + \"</g>\";\n };\n s += M.part(\"blobs\", { in: \"fade\", out: \"none\", dur: 0.6, vary: false }, layer(c.accent2, 3) + layer(c.accent, 0));\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n","libs":[]},{"id":"gradient","name":"Colour sweep","description":"A slow, rich gradient through the brand's colours that turns and shifts, with a light grain — the simplest premium ground.","category":"scene","tags":["backdrop","background","loop","gradient","sweep","colour","smooth","duotone"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"speed":{"type":"number","label":"Speed","default":1,"min":0,"max":3,"step":0.1},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the gradient"},"pack":"backdrops","ref":"backdrops/gradient","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, W = $.W, H = $.H;\n const M = motion($, t);\n const a = 120 + t * 9 * p.speed, sh = 0.08 * Math.sin(t * 0.4 * p.speed);\n let g = $.rect(0, 0, W, H, { fill: $.gradient([[0, c.bg], [0.3 + sh, c.accent], [0.62 - sh, c.accent2], [1, c.bg]], { angle: a }) });\n g += $.rect(0, 0, W, H, { fill: $.gradient([[0, c.bg, 0.55], [0.5, c.bg, 0], [1, c.bg, 0.5]], { angle: a + 90 }) });\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, g) + $.grain(t, { op: 0.07 });\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n\n/** A backdrop has no moments: just soft air as it comes up. */\nfunction sound($) {\n return [{ at: 0, kind: \"swell\", level: 0.6 }];\n}\n","libs":[]},{"id":"starfield","name":"Starfield","description":"Stars stream toward you out of the dark — warp speed or a slow drift — some in the accents.","category":"scene","tags":["backdrop","background","loop","stars","space","warp","hyperspace","particles"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"speed":{"type":"number","label":"Speed","default":1,"min":0.1,"max":4,"step":0.1},"streaks":{"type":"boolean","label":"Streaks","default":true},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the ground","stars":"the stars"},"pack":"backdrops","ref":"backdrops/starfield","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, plain($, t));\n const n = 220, cx = W / 2, cy = H / 2, D = Math.hypot(W, H) * 0.6;\n let st = \"\";\n for (let i = 0; i < n; i++) {\n const a = $.hash(i * 1.37) * 6.2832, r0 = 0.02 + $.hash(i * 7.1) * 0.98;\n const z = ((r0 + t * 0.18 * p.speed * (0.6 + $.hash(i * 3.3))) % 1);\n const d = Math.pow(z, 2.2) * D, d0 = Math.pow(Math.max(0, z - 0.04 * p.speed), 2.2) * D;\n const x = cx + Math.cos(a) * d, y = cy + Math.sin(a) * d, col = i % 9 === 0 ? c.accent : i % 13 === 0 ? c.accent2 : c.fg;\n const sw = (0.8 + 3.2 * z) * k, op = Math.min(1, z * 2.4);\n st += p.streaks ? $.line(cx + Math.cos(a) * d0, cy + Math.sin(a) * d0, x, y, { stroke: col, sw: sw, op: op }) : $.circle(x, y, sw, { fill: col, op: op });\n }\n s += M.part(\"stars\", { in: \"fade\", out: \"none\", dur: 0.4, vary: false }, st);\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n","libs":[]},{"id":"bokeh","name":"Bokeh","description":"Soft out-of-focus lights in the brand's colours drift and breathe — a warm, filmic ground.","category":"scene","tags":["backdrop","background","loop","bokeh","lights","blur","soft","particles","film"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"count":{"type":"number","label":"Lights","default":26,"min":6,"max":60,"step":1},"speed":{"type":"number","label":"Speed","default":1,"min":0,"max":3,"step":0.1},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the ground","lights":"the lights"},"pack":"backdrops","ref":"backdrops/bokeh","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, plain($, t));\n let l = \"\";\n const pal = [c.accent, c.accent2, c.fg];\n for (let i = 0; i < Math.round(p.count); i++) {\n const r = Math.min(W, H) * (0.03 + 0.12 * Math.pow($.hash(i * 2.3), 2));\n const x = ($.hash(i * 5.1) * 1.2 - 0.1) * W + Math.sin(t * 0.3 * p.speed + i) * 40 * k, y = ($.hash(i * 8.7) * 1.2 - 0.1) * H - ((t * 14 * k * p.speed * (0.5 + $.hash(i))) % (H * 1.4)) + H * 0.2;\n const yy = ((y % (H * 1.4)) + H * 1.4) % (H * 1.4) - H * 0.2;\n const a = (0.18 + 0.35 * $.hash(i * 3.9)) * (0.75 + 0.25 * Math.sin(t * 1.3 + i * 2));\n const col = pal[i % 3];\n l += $.circle(x, yy, r, { fill: $.gradient([[0, col, a], [0.75, col, a * 0.8], [1, col, 0]], { radial: true }) });\n }\n s += M.part(\"lights\", { in: \"fade\", out: \"none\", dur: 0.6, vary: false }, l);\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n","libs":[]},{"id":"topo","name":"Topographic","description":"Contour lines like a topographic map, slowly shifting as if the land were breathing.","category":"scene","tags":["backdrop","background","loop","topographic","contour","lines","map","terrain","organic"],"placement":"full","duration":{"default":8,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"lines":{"type":"number","label":"Lines","default":14,"min":6,"max":40,"step":1},"speed":{"type":"number","label":"Speed","default":1,"min":0,"max":3,"step":0.1},"vignette":{"type":"boolean","label":"Dark edges","default":false}},"parts":{"ground":"the ground","lines":"the contour lines"},"pack":"backdrops","ref":"backdrops/topo","code":"// Backdrops are grounds that move: put titles, products or footage over them. They loop smoothly.\n/** The brand's plain ground (no texture of its own: the backdrop is the texture). */\nfunction plain($, t) { return $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H }, grid: false, glow: false, grain: false, texture: false }); }\n/** A soft dark edge round the frame (strength 0–1). */\nfunction vignetteOf($, a) {\n return $.rect(0, 0, $.W, $.H, { fill: $.gradient([[0, \"#000000\", 0], [0.6, \"#000000\", 0], [1, \"#000000\", a]], { radial: true, r: 0.75 }) });\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const M = motion($, t);\n let s = M.part(\"ground\", { in: \"none\", out: \"none\", vary: false }, plain($, t));\n const n = Math.round(p.lines), peaks = [[0.3, 0.38, 0], [0.76, 0.66, 2.1]];\n const D = Math.hypot(W, H);\n let l = \"\";\n peaks.forEach(function (pk, pi) {\n const cx = W * pk[0], cy = H * pk[1];\n for (let j = 1; j <= n; j++) {\n const r = (j / n) * D * 0.55, pts = [];\n for (let q = 0; q < 40; q++) {\n const a = (q / 40) * 6.2832;\n const w = 1 + 0.16 * Math.sin(3 * a + pk[2] + t * 0.25 * p.speed) + 0.08 * Math.sin(5 * a - j * 0.3 + t * 0.4 * p.speed) + 0.05 * Math.sin(9 * a + j * 0.5);\n pts.push([cx + Math.cos(a) * r * w, cy + Math.sin(a) * r * w * 0.8]);\n }\n let d = \"M\" + (pts[0][0] + pts[39][0]) / 2 + \" \" + (pts[0][1] + pts[39][1]) / 2;\n for (let q = 0; q < 40; q++) { const a1 = pts[q], b1 = pts[(q + 1) % 40]; d += \"Q\" + a1[0] + \" \" + a1[1] + \" \" + (a1[0] + b1[0]) / 2 + \" \" + (a1[1] + b1[1]) / 2; }\n l += $.path(d + \"Z\", { stroke: j % 5 === 0 ? c.accent : $.rgba(c.fg, 0.18), sw: (j % 5 === 0 ? 3 : 1.8) * k, op: pi === 0 ? 1 : 0.65 });\n }\n });\n s += M.part(\"lines\", { in: \"fade\", out: \"none\", dur: 0.6, vary: false }, l);\n if (p.vignette) s += vignetteOf($, 0.5);\n return s;\n}\n","libs":[]}]},{"format":1,"id":"promo","name":"MONARC Promo","version":"1.0.0","description":"Offers that sell: a sale starburst, a price card that counts down, a coupon with a code to copy, an offer countdown, a feature checklist, a call to action with a click, a launch title and crossing tape ribbons.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/promo","style":"candy","shared":"shared.js","items":["sale-badge","price-card","coupon","offer-countdown","features","cta","launch","ribbon"],"entries":[{"id":"sale-badge","name":"Sale badge","description":"A starburst badge slams in spinning — 50% OFF — then wobbles while a dashed ring turns inside it.","category":"cta","tags":["promo","sale","offer","badge","starburst","discount","percent","off","sticker"],"placement":"overlay","duration":{"default":3,"min":1,"max":15},"aspects":["16:9","9:16","1:1","4:5"],"props":{"big":{"type":"text","label":"Big","default":"50%","maxLength":6},"small":{"type":"text","label":"Small","default":"Off","maxLength":12},"x":{"type":"number","label":"Centre across (%)","default":78,"min":0,"max":100,"step":1},"y":{"type":"number","label":"Centre down (%)","default":30,"min":0,"max":100,"step":1},"size":{"type":"number","label":"Size","default":100,"min":40,"max":200,"step":5}},"parts":{"badge":"the starburst","words":"the words"},"beats":{"in":0.1},"pack":"promo","ref":"promo/sale-badge","code":"// Promo pieces: offers, prices, codes and calls to action, in the brand's surface (bold styles get ink\n// outlines and hard shadows).\n/** True when the brand draws ink outlines and hard shadows. */\nfunction boldSurface($) { return $.style.surface === \"bold\"; }\n/** A filled shape path with the brand's outline and hard shadow when it has them. */\nfunction shapeFill($, d, fill) {\n const k = $.k;\n if (!boldSurface($)) return $.path(d, { fill: fill, shadow: \"md\" });\n return '<g transform=\"translate(' + 8 * k + \" \" + 8 * k + ')\">' + $.path(d, { fill: $.c.ink }) + \"</g>\" + $.path(d, { fill: fill, stroke: $.c.ink, sw: 4 * k });\n}\n/** Money: \"$1,299\" or \"1,299 €\" (currency after when it is a word or ends with a space). */\nfunction money(cur, n, dec) {\n const v = Number(n).toLocaleString(\"en-US\", { minimumFractionDigits: dec, maximumFractionDigits: dec });\n return /^[A-Za-z]{2,}$/.test(cur) ? v + \" \" + cur : cur + v;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n let s = \"\";\n const R = Math.min(W, H) * 0.17 * p.size / 100, cx = Math.min(W - R * 1.1, Math.max(R * 1.1, W * p.x / 100)), cy = Math.min(H - R * 1.1, Math.max(R * 1.1, H * p.y / 100));\n let d = \"\";\n for (let i = 0; i < 36; i++) { const a = (i / 36) * 6.2832, r = i % 2 ? R * 0.86 : R; d += (i ? \"L\" : \"M\") + (cx + Math.cos(a) * r) + \" \" + (cy + Math.sin(a) * r); }\n const wob = Math.sin((t - tin) * 3) * 4, spin = (t - tin) * 18;\n let badge = shapeFill($, d + \"Z\", c.accent);\n badge += $.circle(cx, cy, R * 0.7, { stroke: $.on(c.accent), sw: 3 * k, dash: 10 * k + \" \" + 8 * k, transform: \"rotate(\" + spin + \" \" + cx + \" \" + cy + \")\", op: 0.6 });\n const big = $.case(p.big), small = $.case(p.small);\n const bf = fitFont($, big, \"display\", R * 0.66, R * 1.3, { ls: -0.04 }), sf = fitFont($, small, \"display\", R * 0.3, R * 1.1, { ls: 0.02 });\n const words = $.text(big, Object.assign({}, bf, { x: cx, y: cy - bf.size * 0.7, anchor: \"middle\", fill: $.on(c.accent) })) + (small ? $.text(small, Object.assign({}, sf, { x: cx, y: cy + bf.size * 0.32, anchor: \"middle\", fill: $.on(c.accent) })) : \"\");\n const box = { x: cx - R, y: cy - R, w: 2 * R, h: 2 * R };\n s += '<g transform=\"rotate(' + wob + \" \" + cx + \" \" + cy + ')\">' + M.part(\"badge\", { at: tin, in: \"spin\", box: box }, badge) + M.part(\"words\", { at: tin + 0.3, in: \"pop\", box: box }, words) + \"</g>\";\n return s;\n}\n","libs":[]},{"id":"price-card","name":"Price card","description":"A product card: the old price is struck through, the new price counts down to it, a saving chip pops and the button gets pressed.","category":"cta","tags":["promo","sale","offer","price","product","card","discount","shop","ecommerce"],"placement":"overlay","duration":{"default":5,"min":2,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"kicker":{"type":"text","label":"Kicker","default":"Weekend deal","maxLength":24},"name":{"type":"text","label":"Product","default":"Studio headphones","maxLength":32},"was":{"type":"number","label":"Was","default":199,"min":0,"max":10000000},"now":{"type":"number","label":"Now","default":149,"min":0,"max":10000000},"currency":{"type":"text","label":"Currency","default":"$","maxLength":4},"decimals":{"type":"number","label":"Decimals","default":0,"min":0,"max":2,"step":1},"cta":{"type":"text","label":"Button","default":"Shop now","maxLength":16},"added":{"type":"text","label":"After the click","default":"Added","maxLength":16},"position":{"type":"choice","label":"Position","options":["center","left","right"],"default":"center"}},"parts":{"card":"the card","price":"the prices","save":"the saving chip","button":"the button and cursor"},"beats":{"in":0.1},"pack":"promo","ref":"promo/price-card","code":"// Promo pieces: offers, prices, codes and calls to action, in the brand's surface (bold styles get ink\n// outlines and hard shadows).\n/** True when the brand draws ink outlines and hard shadows. */\nfunction boldSurface($) { return $.style.surface === \"bold\"; }\n/** A filled shape path with the brand's outline and hard shadow when it has them. */\nfunction shapeFill($, d, fill) {\n const k = $.k;\n if (!boldSurface($)) return $.path(d, { fill: fill, shadow: \"md\" });\n return '<g transform=\"translate(' + 8 * k + \" \" + 8 * k + ')\">' + $.path(d, { fill: $.c.ink }) + \"</g>\" + $.path(d, { fill: fill, stroke: $.c.ink, sw: 4 * k });\n}\n/** Money: \"$1,299\" or \"1,299 €\" (currency after when it is a word or ends with a space). */\nfunction money(cur, n, dec) {\n const v = Number(n).toLocaleString(\"en-US\", { minimumFractionDigits: dec, maximumFractionDigits: dec });\n return /^[A-Za-z]{2,}$/.test(cur) ? v + \" \" + cur : cur + v;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n let s = \"\";\n const cw = Math.min(A.w * (A.wide ? 0.42 : 0.9), 760 * k), pad = 44 * k;\n const nf = Object.assign({}, $.font(\"display\"), { size: 58 * k, ls: -0.02 });\n const nameLines = $.wrap($.case(p.name), cw - pad * 2, nf).slice(0, 2);\n const pf = Object.assign({}, $.font(\"display\"), { size: 120 * k, ls: -0.03 }), of = Object.assign({}, $.font(\"text\"), { size: 44 * k });\n const ch = pad * 2 + 40 * k + 20 * k + nameLines.length * nf.size * 1.08 + 24 * k + pf.size * 1.05 + 30 * k + 96 * k;\n const cx = p.position === \"left\" ? A.x : p.position === \"right\" ? A.x + A.w - cw : W / 2 - cw / 2, cy = H / 2 - ch / 2;\n let y = cy + pad;\n let card = $.card(cx, cy, cw, ch, { t: t });\n card += $.chip($.case(p.kicker), cx + pad, y, { size: 26 * k, fill: c.accent2, color: $.on(c.accent2) });\n y += 60 * k;\n nameLines.forEach(function (l, i) { card += $.text(l, Object.assign({}, nf, { x: cx + pad, y: y + i * nf.size * 1.08, fill: c.fg })); });\n y += nameLines.length * nf.size * 1.08 + 24 * k;\n s += M.part(\"card\", { at: tin, in: \"rise\", distance: 40, box: { x: cx, y: cy, w: cw, h: ch } }, card);\n // The price counts down from the old one to the new.\n const tc = tin + 0.7, q = $.E.out3($.seg(t, tc, tc + 0.9));\n const nowV = $.lerp(p.was, p.now, q), nowS = money(p.currency, p.decimals > 0 ? nowV : Math.round(nowV), p.decimals);\n const wasS = money(p.currency, p.was, p.decimals), ww = $.measure(wasS, of);\n let price = $.text(nowS, Object.assign({}, pf, { x: cx + pad, y: y, fill: c.fg }));\n const px = cx + pad + $.measure(money(p.currency, p.now, p.decimals), pf) + 26 * k, py = y + pf.size * 0.5 - of.size * 0.5;\n price += $.text(wasS, Object.assign({}, of, { x: px, y: py, fill: c.mute }));\n price += $.strike(t, tc, px - 4 * k, py + of.size * 0.55, px + ww + 4 * k, py + of.size * 0.5, { color: c.red, w: 6 * k });\n s += M.part(\"price\", { at: tin + 0.3, in: \"fade\" }, price);\n const save = p.was > 0 ? Math.round((1 - p.now / p.was) * 100) : 0;\n if (save > 0) {\n // The saving sits in the card's top-right corner, clear of both prices.\n const st = \"-\" + save + \"%\", sz = $.chip.size(st, { size: 34 * k }), sx = cx + cw - pad - sz.w, sy = cy + pad - 4 * k;\n s += M.part(\"save\", { at: tc + 0.9, in: \"pop\", box: { x: sx, y: sy, w: sz.w, h: sz.h } }, $.chip(st, sx, sy, { size: 34 * k, fill: c.accent, color: $.on(c.accent) }));\n }\n y += pf.size * 1.05 + 30 * k;\n const press = tc + 1.9;\n let btn = $.button(t, $.case(p.cta), cx + pad, y, { size: 38 * k, fill: c.accent, icon: \"cart\", pressAt: press, done: $.case(p.added || \"Added\"), anchor: \"start\" });\n const bx = cx + pad + 160 * k, by = y + 40 * k;\n const glide = $.E.inOut3($.seg(t, press - 0.8, press - 0.05));\n btn += $.cursor($.lerp(W * 0.9, bx, glide), $.lerp(H * 1.05, by, glide), { press: $.bump(t, press, 0.05, 0.2), ripple: $.seg(t, press, press + 0.5) });\n s += M.part(\"button\", { at: tin + 0.5, in: \"rise\", distance: 20, box: { x: cx + pad, y: y, w: cw - 2 * pad, h: 96 * k } }, btn);\n return s;\n}\n","libs":[]},{"id":"coupon","name":"Coupon","description":"A ticket with a perforated tear line tears in: the deal on the stub, the code in a dashed box — then a click copies it.","category":"cta","tags":["promo","sale","offer","coupon","code","voucher","ticket","promo code","discount"],"placement":"overlay","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"deal":{"type":"text","label":"Deal","default":"20% off","maxLength":12},"note":{"type":"text","label":"Note","default":"your first order","maxLength":32},"code":{"type":"text","label":"Code","default":"SAVE20","maxLength":16},"copied":{"type":"text","label":"After the click","default":"Copied","maxLength":12}},"parts":{"ticket":"the ticket","code":"the code box","copied":"the copied chip"},"beats":{"in":0.1},"pack":"promo","ref":"promo/coupon","code":"// Promo pieces: offers, prices, codes and calls to action, in the brand's surface (bold styles get ink\n// outlines and hard shadows).\n/** True when the brand draws ink outlines and hard shadows. */\nfunction boldSurface($) { return $.style.surface === \"bold\"; }\n/** A filled shape path with the brand's outline and hard shadow when it has them. */\nfunction shapeFill($, d, fill) {\n const k = $.k;\n if (!boldSurface($)) return $.path(d, { fill: fill, shadow: \"md\" });\n return '<g transform=\"translate(' + 8 * k + \" \" + 8 * k + ')\">' + $.path(d, { fill: $.c.ink }) + \"</g>\" + $.path(d, { fill: fill, stroke: $.c.ink, sw: 4 * k });\n}\n/** Money: \"$1,299\" or \"1,299 €\" (currency after when it is a word or ends with a space). */\nfunction money(cur, n, dec) {\n const v = Number(n).toLocaleString(\"en-US\", { minimumFractionDigits: dec, maximumFractionDigits: dec });\n return /^[A-Za-z]{2,}$/.test(cur) ? v + \" \" + cur : cur + v;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n let s = \"\";\n const tw = Math.min(A.w * 0.94, 1200 * k), th = Math.min(A.h * 0.42, tw * 0.36), x = W / 2 - tw / 2, y = H / 2 - th / 2;\n const stub = tw * 0.42, n = 22 * k, r = Math.min(24 * k, th * 0.08);\n // The ticket: rounded corners, a half-round notch top and bottom at the tear line.\n const sx = x + stub;\n const d = \"M\" + (x + r) + \" \" + y + \"H\" + (sx - n) + \"A\" + n + \" \" + n + \" 0 0 0 \" + (sx + n) + \" \" + y + \"H\" + (x + tw - r) + \"Q\" + (x + tw) + \" \" + y + \" \" + (x + tw) + \" \" + (y + r) + \"V\" + (y + th - r) + \"Q\" + (x + tw) + \" \" + (y + th) + \" \" + (x + tw - r) + \" \" + (y + th) + \"H\" + (sx + n) + \"A\" + n + \" \" + n + \" 0 0 0 \" + (sx - n) + \" \" + (y + th) + \"H\" + (x + r) + \"Q\" + x + \" \" + (y + th) + \" \" + x + \" \" + (y + th - r) + \"V\" + (y + r) + \"Q\" + x + \" \" + y + \" \" + (x + r) + \" \" + y + \"Z\";\n let tk = shapeFill($, d, c.surface) + $.clip(x, y, stub, th, 0, $.path(d, { fill: c.accent }));\n tk += $.line(sx, y + n + 8 * k, sx, y + th - n - 8 * k, { stroke: $.rgba(c.fg, 0.35), sw: 3 * k, dash: 10 * k + \" \" + 10 * k, cap: \"butt\" });\n const deal = $.case(p.deal), df = fitFont($, deal, \"display\", th * 0.36, stub * 0.84, { ls: -0.03 });\n const nf = fitFont($, p.note, \"text\", Math.max(26 * k, th * 0.1), stub * 0.84, {});\n tk += $.text(deal, Object.assign({}, df, { x: x + stub / 2, y: y + th / 2 - df.size * 0.72, anchor: \"middle\", fill: $.on(c.accent) }));\n if (p.note) tk += $.text(p.note, Object.assign({}, nf, { x: x + stub / 2, y: y + th / 2 + df.size * 0.38, anchor: \"middle\", fill: $.rgba($.on(c.accent), 0.8) }));\n s += M.part(\"ticket\", { at: tin, in: \"stretch\", box: { x: x, y: y, w: tw, h: th } }, tk);\n // The code in a dashed box on the right.\n const code = String(p.code || \"\").toUpperCase(), cf = fitFont($, code, \"mono\", th * 0.22, (tw - stub) * 0.7, { ls: 0.08 });\n const lf = Object.assign({}, $.font(\"text\"), { size: Math.max(24 * k, th * 0.085) });\n const rx = sx + (tw - stub) / 2, bw = $.measure(code, cf) + 70 * k, bh = cf.size * 1.7, by = y + th / 2 - bh / 2 + lf.size * 0.6;\n const copyAt = tin + 1.5;\n const lit = t >= copyAt;\n let cd = $.text(\"Use code\", Object.assign({}, lf, { x: rx, y: by - lf.size * 1.6, anchor: \"middle\", fill: c.mute }));\n cd += $.rect(rx - bw / 2, by, bw, bh, { r: 12 * k, fill: lit ? $.rgba(c.accent, 0.14) : \"none\", stroke: c.accent, sw: 4 * k, dash: 14 * k + \" \" + 9 * k });\n cd += $.text(code, Object.assign({}, cf, { x: rx, y: by + (bh - cf.size) / 2 - cf.size * 0.06, anchor: \"middle\", fill: c.fg }));\n const glide = $.E.inOut3($.seg(t, copyAt - 0.7, copyAt - 0.05));\n cd += $.cursor($.lerp(x + tw + 60 * k, rx + bw * 0.2, glide), $.lerp(y + th + 80 * k, by + bh * 0.6, glide), { press: $.bump(t, copyAt, 0.05, 0.2), ripple: $.seg(t, copyAt, copyAt + 0.5) });\n s += M.part(\"code\", { at: tin + 0.4, in: \"fade\" }, cd);\n if (p.copied) {\n const ct = $.case(p.copied), co = { size: 28 * k, fill: c.green, color: $.on(c.green), anchor: \"middle\", icon: \"check\" }, cz = $.chip.size(ct, co);\n s += M.part(\"copied\", { at: copyAt + 0.1, in: \"pop\", box: { x: rx - cz.w / 2, y: by + bh + 18 * k, w: cz.w, h: cz.h } }, $.chip(ct, rx, by + bh + 18 * k, co));\n }\n return s;\n}\n","libs":[]},{"id":"offer-countdown","name":"Offer countdown","description":"Offer ends in — days, hours, minutes and seconds in tiles, the seconds rolling down live. Sits low in the frame.","category":"timer","tags":["promo","sale","offer","countdown","timer","deadline","urgency","ends","limited"],"placement":"overlay","duration":{"default":5,"min":2,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"label":{"type":"text","label":"Label","default":"Offer ends in","maxLength":28},"hours":{"type":"number","label":"Hours left","default":26,"min":0,"max":9999},"minutes":{"type":"number","label":"And minutes","default":41,"min":0,"max":59,"step":1},"days":{"type":"boolean","label":"Show days","default":true}},"parts":{"panel":"the panel","digits":"the digit tiles"},"beats":{"in":0.1},"pack":"promo","ref":"promo/offer-countdown","code":"// Promo pieces: offers, prices, codes and calls to action, in the brand's surface (bold styles get ink\n// outlines and hard shadows).\n/** True when the brand draws ink outlines and hard shadows. */\nfunction boldSurface($) { return $.style.surface === \"bold\"; }\n/** A filled shape path with the brand's outline and hard shadow when it has them. */\nfunction shapeFill($, d, fill) {\n const k = $.k;\n if (!boldSurface($)) return $.path(d, { fill: fill, shadow: \"md\" });\n return '<g transform=\"translate(' + 8 * k + \" \" + 8 * k + ')\">' + $.path(d, { fill: $.c.ink }) + \"</g>\" + $.path(d, { fill: fill, stroke: $.c.ink, sw: 4 * k });\n}\n/** Money: \"$1,299\" or \"1,299 €\" (currency after when it is a word or ends with a space). */\nfunction money(cur, n, dec) {\n const v = Number(n).toLocaleString(\"en-US\", { minimumFractionDigits: dec, maximumFractionDigits: dec });\n return /^[A-Za-z]{2,}$/.test(cur) ? v + \" \" + cur : cur + v;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n let s = \"\";\n const start = (p.hours * 3600 + p.minutes * 60 + 17) - Math.max(0, t - tin);\n const left = Math.max(0, start);\n const parts = p.days ? [[Math.floor(left / 86400), \"days\"], [Math.floor(left / 3600) % 24, \"hrs\"], [Math.floor(left / 60) % 60, \"min\"], [Math.floor(left) % 60, \"sec\"]] : [[Math.floor(left / 3600), \"hrs\"], [Math.floor(left / 60) % 60, \"min\"], [Math.floor(left) % 60, \"sec\"]];\n const n = parts.length, lab = $.case(p.label);\n const tileW = Math.min(A.w * 0.9 / (n * 1.25), 190 * k), tileH = tileW * 1.05, gap = tileW * 0.25;\n const lf = Object.assign({}, $.font(\"display\"), { size: tileW * 0.3 }), df = Object.assign({}, $.font(\"mono\"), { size: tileW * 0.58, weight: 700 }), uf = Object.assign({}, $.font(\"text\"), { size: tileW * 0.16 });\n const rowW = n * tileW + (n - 1) * gap, pw = Math.max(rowW, $.measure(lab, lf)) + 80 * k, ph = 40 * k + lf.size * 1.3 + tileH + uf.size * 1.8 + 30 * k;\n const px = W / 2 - pw / 2, py = A.y + A.h - ph;\n s += M.part(\"panel\", { at: tin, in: \"rise\", distance: 40, box: { x: px, y: py, w: pw, h: ph } }, $.card(px, py, pw, ph, { t: t }) + $.text(lab, Object.assign({}, lf, { x: W / 2, y: py + 34 * k, anchor: \"middle\", fill: c.fg })));\n const ty = py + 34 * k + lf.size * 1.3;\n let digits = \"\";\n parts.forEach(function (q, i) {\n const x = W / 2 - rowW / 2 + i * (tileW + gap), last = i === n - 1;\n const v = String(q[0]).padStart(2, \"0\");\n // The seconds roll: the new value drops in over the first quarter of each second.\n const fr = (start % 1 + 1) % 1, roll = last ? $.E.out3($.clamp((1 - fr) / 0.25)) : 1;\n let inner = $.text(v, Object.assign({}, df, { x: x + tileW / 2, y: ty + (tileH - df.size) / 2 - df.size * 0.08 - (1 - roll) * tileH * 0.5, anchor: \"middle\", fill: last ? $.on(c.accent) : c.fg, op: roll }));\n digits += $.rect(x, ty, tileW, tileH, { r: 14 * k, fill: last ? c.accent : $.rgba(c.fg, 0.08) }) + $.clip(x, ty, tileW, tileH, 14 * k, inner);\n digits += $.text(q[1], Object.assign({}, uf, { x: x + tileW / 2, y: ty + tileH + uf.size * 0.5, anchor: \"middle\", fill: c.mute }));\n if (i < n - 1) digits += $.text(\":\", Object.assign({}, df, { x: x + tileW + gap / 2, y: ty + (tileH - df.size) / 2 - df.size * 0.1, anchor: \"middle\", fill: c.mute }));\n });\n s += M.part(\"digits\", { at: tin + 0.25, in: \"rise\", split: \"none\", distance: 20, box: { x: W / 2 - rowW / 2, y: ty, w: rowW, h: tileH } }, digits);\n return s;\n}\n","libs":[]},{"id":"features","name":"Feature list","description":"A title, then features tick in one by one — a check draws in its circle and the line slides across.","category":"list","tags":["promo","sale","offer","features","checklist","benefits","list","ticks","included"],"placement":"full","duration":{"default":5,"min":2,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"title":{"type":"text","label":"Title","default":"Everything you need","maxLength":40},"features":{"type":"list","label":"Features","of":"text","default":["Unlimited projects","4K exports","Brand styles","Team sharing"],"maxItems":6}},"parts":{"ground":"the ground","title":"the title","rows":"the feature rows"},"beats":{"in":0.1},"pack":"promo","ref":"promo/features","code":"// Promo pieces: offers, prices, codes and calls to action, in the brand's surface (bold styles get ink\n// outlines and hard shadows).\n/** True when the brand draws ink outlines and hard shadows. */\nfunction boldSurface($) { return $.style.surface === \"bold\"; }\n/** A filled shape path with the brand's outline and hard shadow when it has them. */\nfunction shapeFill($, d, fill) {\n const k = $.k;\n if (!boldSurface($)) return $.path(d, { fill: fill, shadow: \"md\" });\n return '<g transform=\"translate(' + 8 * k + \" \" + 8 * k + ')\">' + $.path(d, { fill: $.c.ink }) + \"</g>\" + $.path(d, { fill: fill, stroke: $.c.ink, sw: 4 * k });\n}\n/** Money: \"$1,299\" or \"1,299 €\" (currency after when it is a word or ends with a space). */\nfunction money(cur, n, dec) {\n const v = Number(n).toLocaleString(\"en-US\", { minimumFractionDigits: dec, maximumFractionDigits: dec });\n return /^[A-Za-z]{2,}$/.test(cur) ? v + \" \" + cur : cur + v;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n let s = \"\";\n s += M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const rows = (p.features || []).filter(Boolean).slice(0, 6);\n const tf = fitLines($, $.case(p.title), Object.assign({}, $.font(\"display\"), { ls: -0.03 }), A.w * 0.9, Math.min(120 * k, A.h * 0.16), 50 * k, 2);\n const rf = Object.assign({}, $.font(\"text\"), { size: Math.min(56 * k, (A.h * 0.62) / Math.max(1, rows.length) / 1.9), weight: 600 });\n const rowH = rf.size * 1.9, listW = Math.min(A.w * 0.9, widest($, rows, rf) + rf.size * 2.2);\n const total = tf.lines.length * tf.size * 1.05 + 50 * k + rows.length * rowH;\n let y = H / 2 - total / 2;\n tf.lines.forEach(function (l, i) { s += M.text(\"title\", Object.assign({ at: tin + i * 0.1, in: \"rise\" }, tf.font, { x: W / 2, y: y + i * tf.size * 1.05, anchor: \"middle\", fill: c.fg }), l); });\n y += tf.lines.length * tf.size * 1.05 + 50 * k;\n const x0 = W / 2 - listW / 2, r = rf.size * 0.62;\n rows.forEach(function (row, i) {\n const a = tin + 0.5 + i * 0.32, cy = y + i * rowH + rowH / 2;\n const ck = $.E.out3($.seg(t, a + 0.15, a + 0.45));\n let g = $.circle(x0 + r, cy, r, { fill: c.accent }) + (ck > 0 ? $.path(\"M\" + (x0 + r * 0.55) + \" \" + cy + \"L\" + (x0 + r * 0.88) + \" \" + (cy + r * 0.32) + \"L\" + (x0 + r * 1.48) + \" \" + (cy - r * 0.36), { stroke: $.on(c.accent), sw: r * 0.24, draw: ck }) : \"\");\n g += $.text(row, Object.assign({}, rf, { x: x0 + r * 2 + rf.size * 0.5, y: cy - rf.size * 0.56, fill: c.fg, maxW: listW - r * 3 }));\n if (i < rows.length - 1) g += $.rect(x0, y + (i + 1) * rowH - 1 * k, listW, 2 * k, { fill: c.line });\n s += M.part(\"rows\", { at: a, in: \"slide\", from: \"left\", distance: 30, box: { x: x0, y: cy - rowH / 2, w: listW, h: rowH } }, g);\n });\n return s;\n}\n","libs":[]},{"id":"cta","name":"Call to action","description":"A short line, then a big button — a cursor glides on, presses it with a ripple, and it flips to the done label.","category":"cta","tags":["promo","sale","offer","cta","button","click","shop now","sign up","download","link in bio"],"placement":"overlay","duration":{"default":4,"min":1.5,"max":15},"aspects":["16:9","9:16","1:1","4:5"],"props":{"line":{"type":"text","label":"Line","default":"Ready when you are","maxLength":40,"description":"empty for none"},"button":{"type":"text","label":"Button","default":"Get started","maxLength":20},"done":{"type":"text","label":"After the click","default":"You're in","maxLength":20},"icon":{"type":"icon","label":"Icon","default":"arrowRight"},"position":{"type":"choice","label":"Position","options":["center","bottom"],"default":"center"}},"parts":{"line":"the line","button":"the button","cursor":"the cursor"},"beats":{"in":0.1},"pack":"promo","ref":"promo/cta","code":"// Promo pieces: offers, prices, codes and calls to action, in the brand's surface (bold styles get ink\n// outlines and hard shadows).\n/** True when the brand draws ink outlines and hard shadows. */\nfunction boldSurface($) { return $.style.surface === \"bold\"; }\n/** A filled shape path with the brand's outline and hard shadow when it has them. */\nfunction shapeFill($, d, fill) {\n const k = $.k;\n if (!boldSurface($)) return $.path(d, { fill: fill, shadow: \"md\" });\n return '<g transform=\"translate(' + 8 * k + \" \" + 8 * k + ')\">' + $.path(d, { fill: $.c.ink }) + \"</g>\" + $.path(d, { fill: fill, stroke: $.c.ink, sw: 4 * k });\n}\n/** Money: \"$1,299\" or \"1,299 €\" (currency after when it is a word or ends with a space). */\nfunction money(cur, n, dec) {\n const v = Number(n).toLocaleString(\"en-US\", { minimumFractionDigits: dec, maximumFractionDigits: dec });\n return /^[A-Za-z]{2,}$/.test(cur) ? v + \" \" + cur : cur + v;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n let s = \"\";\n const size = Math.min(64 * k, A.w / 14);\n const by = p.position === \"bottom\" ? A.y + A.h - size * 2.4 : H / 2 + (p.line ? size * 0.4 : -size);\n if (p.line) {\n const lf = fitFont($, $.case(p.line), \"display\", size * 1.5, A.w * 0.9, { ls: -0.03 });\n s += M.text(\"line\", Object.assign({ at: tin, in: \"rise\", split: \"words\" }, lf, { x: W / 2, y: by - lf.size * 1.6, anchor: \"middle\", fill: c.fg }), $.case(p.line));\n }\n const press = tin + 1.5;\n s += M.part(\"button\", { at: tin + 0.3, in: \"pop\", box: { x: W / 2 - size * 4, y: by, w: size * 8, h: size * 2 } }, $.button(t, $.case(p.button), W / 2, by, { size: size, fill: c.accent, icon: p.icon || \"arrowRight\", pressAt: press, done: $.case(p.done || p.button), anchor: \"middle\" }));\n const glide = $.E.inOut3($.seg(t, press - 0.75, press - 0.05)), bx = W / 2 + size * 1.2, byy = by + size * 1.1;\n s += M.part(\"cursor\", { at: press - 0.8, in: \"fade\", dur: 0.2 }, $.cursor($.lerp(W * 0.86, bx, glide), $.lerp(H * 0.98, byy, glide), { press: $.bump(t, press, 0.05, 0.2), ripple: $.seg(t, press, press + 0.5), scale: 2.6 * k }));\n return s;\n}\n","libs":[]},{"id":"launch","name":"Launch","description":"A launch title: a NEW sticker spins on, the product name lands huge over the brand's pattern, then the tagline and when it is out.","category":"title","tags":["promo","sale","offer","launch","new","product","reveal","announcement","drop"],"placement":"full","duration":{"default":5,"min":2,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"badge":{"type":"text","label":"Sticker","default":"New","maxLength":8},"name":{"type":"text","label":"Name","default":"Aero Pro","maxLength":28},"tagline":{"type":"text","label":"Tagline","default":"Lighter than anything we've made","maxLength":64},"when":{"type":"text","label":"When","default":"Available now","maxLength":28,"description":"empty for none"}},"parts":{"ground":"the ground","badge":"the sticker","name":"the name","tagline":"the tagline","when":"the availability chip"},"beats":{"in":0.1},"pack":"promo","ref":"promo/launch","code":"// Promo pieces: offers, prices, codes and calls to action, in the brand's surface (bold styles get ink\n// outlines and hard shadows).\n/** True when the brand draws ink outlines and hard shadows. */\nfunction boldSurface($) { return $.style.surface === \"bold\"; }\n/** A filled shape path with the brand's outline and hard shadow when it has them. */\nfunction shapeFill($, d, fill) {\n const k = $.k;\n if (!boldSurface($)) return $.path(d, { fill: fill, shadow: \"md\" });\n return '<g transform=\"translate(' + 8 * k + \" \" + 8 * k + ')\">' + $.path(d, { fill: $.c.ink }) + \"</g>\" + $.path(d, { fill: fill, stroke: $.c.ink, sw: 4 * k });\n}\n/** Money: \"$1,299\" or \"1,299 €\" (currency after when it is a word or ends with a space). */\nfunction money(cur, n, dec) {\n const v = Number(n).toLocaleString(\"en-US\", { minimumFractionDigits: dec, maximumFractionDigits: dec });\n return /^[A-Za-z]{2,}$/.test(cur) ? v + \" \" + cur : cur + v;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n let s = \"\";\n s += M.part(\"ground\", { in: \"none\", out: \"fade\", vary: false }, $.panel(t, { box: { x: 0, y: 0, w: W, h: H } }));\n const fit = fitLines($, $.case(p.name), Object.assign({}, $.font(\"display\"), { ls: -0.045 }), A.w * 0.94, Math.min(A.h * 0.36, 300 * k), 80 * k, 2);\n const tf = Object.assign({}, $.font(\"text\"), { size: Math.max(30 * k, Math.min(48 * k, fit.size * 0.2)) });\n const tl = p.tagline ? $.wrap(p.tagline, A.w * 0.8, tf).slice(0, 2) : [];\n const total = fit.lines.length * fit.size * 0.98 + (tl.length ? 34 * k + tl.length * tf.size * 1.3 : 0) + (p.when ? 90 * k : 0);\n let y = H / 2 - total / 2;\n const w = widest($, fit.lines, fit.font);\n fit.lines.forEach(function (l, i) { s += M.text(\"name\", Object.assign({ at: tin + 0.35 + i * 0.12, in: \"push\", split: \"letters\", stagger: 0.03 }, fit.font, { x: W / 2, y: y + i * fit.size * 0.98, anchor: \"middle\", fill: c.fg }), l); });\n // The sticker sits on the name's top-right corner.\n const R = Math.max(56 * k, fit.size * 0.42), bx = Math.min(W - R - 20 * k, W / 2 + w / 2 + R * 0.2), by = Math.max(R + 20 * k, y - R * 0.1);\n const bt = $.case(p.badge), bf = fitFont($, bt, \"display\", R * 0.6, R * 1.5, { ls: 0.02 });\n s += M.part(\"badge\", { at: tin, in: \"spin\", box: { x: bx - R, y: by - R, w: 2 * R, h: 2 * R } }, '<g transform=\"rotate(-14 ' + bx + \" \" + by + ')\">' + $.circle(bx, by, R, { fill: c.accent2, stroke: boldSurface($) ? c.ink : undefined, sw: 4 * k, shadow: boldSurface($) ? \"hard\" : \"md\" }) + $.text(bt, Object.assign({}, bf, { x: bx, y: by - bf.size * 0.56, anchor: \"middle\", fill: $.on(c.accent2) })) + \"</g>\");\n y += fit.lines.length * fit.size * 0.98 + 34 * k;\n if (tl.length) s += M.text(\"tagline\", Object.assign({ at: tin + 1.0, in: \"rise\", split: \"lines\", distance: 16 }, tf, { x: W / 2, y: y, anchor: \"middle\", fill: c.mute, lh: 1.3, maxW: A.w * 0.8 }), tl.join(\"\\n\"));\n y += tl.length * tf.size * 1.3 + 30 * k;\n if (p.when) {\n const wt = $.case(p.when), sz = $.chip.size(wt, { size: 30 * k, dot: true });\n s += M.part(\"when\", { at: tin + 1.3, in: \"pop\", box: { x: W / 2 - sz.w / 2, y: y, w: sz.w, h: sz.h } }, $.chip(wt, W / 2, y, { size: 30 * k, dot: true, anchor: \"middle\", fill: c.accent, color: $.on(c.accent) }));\n }\n return s;\n}\n","libs":[]},{"id":"ribbon","name":"Tape ribbons","description":"Two tapes cross the frame like caution tape, their words scrolling — FREE SHIPPING • LIMITED DROP — over your footage.","category":"banner","tags":["promo","sale","offer","ribbon","tape","banner","caution","scrolling","free shipping"],"placement":"overlay","duration":{"default":5,"min":2,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"text":{"type":"text","label":"First tape","default":"Free shipping","maxLength":28},"text2":{"type":"text","label":"Second tape","default":"Limited drop","maxLength":28,"description":"empty for one tape"},"speed":{"type":"number","label":"Speed","default":1,"min":0.2,"max":3,"step":0.1}},"parts":{"tapes":"the tapes"},"beats":{"in":0.1},"pack":"promo","ref":"promo/ribbon","code":"// Promo pieces: offers, prices, codes and calls to action, in the brand's surface (bold styles get ink\n// outlines and hard shadows).\n/** True when the brand draws ink outlines and hard shadows. */\nfunction boldSurface($) { return $.style.surface === \"bold\"; }\n/** A filled shape path with the brand's outline and hard shadow when it has them. */\nfunction shapeFill($, d, fill) {\n const k = $.k;\n if (!boldSurface($)) return $.path(d, { fill: fill, shadow: \"md\" });\n return '<g transform=\"translate(' + 8 * k + \" \" + 8 * k + ')\">' + $.path(d, { fill: $.c.ink }) + \"</g>\" + $.path(d, { fill: fill, stroke: $.c.ink, sw: 4 * k });\n}\n/** Money: \"$1,299\" or \"1,299 €\" (currency after when it is a word or ends with a space). */\nfunction money(cur, n, dec) {\n const v = Number(n).toLocaleString(\"en-US\", { minimumFractionDigits: dec, maximumFractionDigits: dec });\n return /^[A-Za-z]{2,}$/.test(cur) ? v + \" \" + cur : cur + v;\n}\n\nfunction frame(t, $) {\n const p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n const A = safeArea($), M = motion($, t);\n const tin = $.beat(\"in\", 0.1);\n let s = \"\";\n const D = Math.hypot(W, H), th = Math.min(W, H) * 0.1;\n const tapes = [[p.text, -9, c.accent, 0.42], [p.text2, 7, c.accent2, 0.62]].filter(function (q) { return q[0]; });\n tapes.forEach(function (q, i) {\n const str = $.case(q[0]), f = Object.assign({}, $.font(\"display\"), { size: th * 0.5, ls: 0.02 });\n const sep = \" • \", unit = $.measure(str + sep, f), dir = i ? -1 : 1, off = (((t * 160 * k * p.speed * dir) % unit) + unit) % unit;\n let row = $.rect(-D / 2, -th / 2, D, th, { fill: q[2] }) + $.rect(-D / 2, -th / 2 + 6 * k, D, 3 * k, { fill: $.rgba($.on(q[2]), 0.5) }) + $.rect(-D / 2, th / 2 - 9 * k, D, 3 * k, { fill: $.rgba($.on(q[2]), 0.5) });\n for (let x = -D / 2 - unit + off; x < D / 2 + unit; x += unit) row += $.text(str + sep, Object.assign({}, f, { within: { x: -1e5, y: -1e5, w: 2e5, h: 2e5 }, x: x, y: -f.size * 0.56, fill: $.on(q[2]) }));\n const cy = H * q[3];\n s += M.part(\"tapes\", { at: tin + i * 0.2, in: \"wipe\", from: i ? \"right\" : \"left\", dur: 0.5, box: { x: 0, y: cy - th, w: W, h: th * 2 } }, '<g transform=\"translate(' + W / 2 + \" \" + cy + \") rotate(\" + q[1] + ')\">' + row + \"</g>\");\n });\n return s;\n}\n","libs":[]}]},{"format":1,"id":"ui","name":"MONARC UI","version":"1.0.0","description":"Product micro-interactions that play live in a page and render to video: a button that hovers, presses, loads and lands on a check, a toggle, a like with a count, a loader that succeeds or fails, and a notification bell. Each declares its states, so <volito-player> plays them on hover and click with no setup.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/ui","style":"studio","shared":"shared.js","items":["button","toggle","like","loader","bell"],"entries":[{"id":"button","name":"Button","description":"A product button with every state: it springs in, breathes while idle, lifts on hover, presses, spins while it loads and lands on a check with the done label. In a page, <volito-player> plays each state on hover and click.","category":"cta","tags":["ui","button","hover","press","loading","success","interactive","micro-interaction","product","web"],"placement":"overlay","duration":{"default":4.6,"min":4.6,"max":4.6},"aspects":["16:9","9:16","1:1","4:5"],"props":{"label":{"type":"text","label":"Label","default":"Get started","maxLength":22},"done":{"type":"text","label":"Done label","default":"You're in","maxLength":22},"icon":{"type":"icon","label":"Icon","default":"arrowRight"}},"beats":{"idle":0.6,"hoverIn":1.6,"hoverOut":1.9,"press":2.2,"loading":2.6,"done":3.6},"states":{"enter":{"from":"start","to":"idle","play":"once","next":"idle"},"idle":{"from":"idle","to":"hoverIn","play":"loop"},"hover":{"from":"hoverIn","to":"hoverOut","play":"once"},"unhover":{"from":"hoverOut","to":"press","play":"once","next":"idle"},"press":{"from":"press","to":"loading","play":"once","next":"loading"},"loading":{"from":"loading","to":"done","play":"loop"},"done":{"from":"done","to":"end","play":"once"}},"on":"pointerenter(idle|unhover): hover; pointerleave(hover): unhover; click(idle|hover|unhover): press","pack":"ui","ref":"ui/button","code":"// UI pieces: product micro-interactions whose timeline is cut into states at named beats (enter, idle,\n// hover, press, loading, done…). Loops are periodic so a looped state repeats without a seam, and every\n// state ends on a pose the next one starts from.\n\n/** 0→1 through a loop of [a, b): the frame at b is the frame at a. */\nfunction loopP(t, a, b) {\n var L = b - a;\n if (L <= 0) return 0;\n var x = (t - a) % L;\n return (x < 0 ? x + L : x) / L;\n}\n\n/** A size as a share of the frame's short side, so a control reads the same at every aspect. */\nfunction unit($, share) {\n return Math.min($.W, $.H) * share;\n}\n\n/** Fixed-precision number for markup. */\nfunction n2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A rounded box in the brand's surface: bold brands get an ink outline and a hard shadow. */\nfunction uiBox($, x, y, w, h, r, fill, shadow) {\n var k = $.k;\n if ($.style.surface === \"bold\") return $.rect(x + 6 * k, y + 6 * k, w, h, { r: r, fill: $.c.ink }) + $.rect(x, y, w, h, { r: r, fill: fill, stroke: $.c.ink, sw: 3.5 * k });\n return $.rect(x, y, w, h, { r: r, fill: fill, shadow: $.style.surface === \"flat\" ? null : shadow || \"md\" });\n}\n\n/** A spinner arc at (cx, cy) turned `turn` (0–1) of the way round. */\nfunction spinnerArc(cx, cy, r, turn, color, sw, op) {\n return '<g transform=\"rotate(' + n2(turn * 360) + \" \" + n2(cx) + \" \" + n2(cy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(op) + '\"' : \"\") + '><circle cx=\"' + n2(cx) + '\" cy=\"' + n2(cy) + '\" r=\"' + n2(r) + '\" fill=\"none\" stroke=\"' + color + '\" stroke-width=\"' + n2(sw) + '\" stroke-linecap=\"round\" pathLength=\"100\" stroke-dasharray=\"30 70\"/></g>';\n}\n\n/** A check mark centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction checkMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var d = \"M\" + n2(cx - s * 0.5) + \" \" + n2(cy + s * 0.02) + \" L\" + n2(cx - s * 0.14) + \" \" + n2(cy + s * 0.36) + \" L\" + n2(cx + s * 0.52) + \" \" + n2(cy - s * 0.34);\n return $.path(d, { stroke: color, sw: sw, draw: Math.min(1, p) });\n}\n\n/** A cross centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction crossMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var h = s * 0.4;\n var a = $.path(\"M\" + n2(cx - h) + \" \" + n2(cy - h) + \" L\" + n2(cx + h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, p * 2) });\n var b = p > 0.5 ? $.path(\"M\" + n2(cx + h) + \" \" + n2(cy - h) + \" L\" + n2(cx - h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, (p - 0.5) * 2) }) : \"\";\n return a + b;\n}\n\n/** A group scaled about (ox, oy) and moved by (dx, dy), at an opacity. */\nfunction posed(markup, ox, oy, s, dx, dy, op) {\n return '<g transform=\"translate(' + n2(dx || 0) + \" \" + n2(dy || 0) + \") translate(\" + n2(ox) + \" \" + n2(oy) + \") scale(\" + n2(s) + \") translate(\" + n2(-ox) + \" \" + n2(-oy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(Math.max(0, op)) + '\"' : \"\") + \">\" + markup + \"</g>\";\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n var bIdle = $.beat(\"idle\", 0.6), bHi = $.beat(\"hoverIn\", 1.6), bHo = $.beat(\"hoverOut\", 1.9), bPress = $.beat(\"press\", 2.2), bLoad = $.beat(\"loading\", 2.6), bDone = $.beat(\"done\", 3.6);\n var tf = $.font(\"text\");\n var size = unit($, 0.07);\n var font = { size: size, weight: Math.max(700, tf.weight), family: tf.family };\n var label = $.case(p.label), done = $.case(p.done || p.label);\n var iconS = size * 1.0, gap = size * 0.55, padX = size * 1.25, h = size * 2.4;\n var lw = Math.max($.measure(label, font), $.measure(done, font) + iconS + gap);\n var w = padX * 2 + lw + (p.icon ? iconS + gap : 0);\n var cx = W / 2, cy = H / 2;\n var r = Math.min(h / 2, Math.max(8 * k, $.style.radius));\n\n // Each stretch of the timeline is a state: enter, idle (loops), hover, unhover, press, loading (loops), done.\n var appear = $.E.outBack($.seg(t, 0, bIdle * 0.85));\n var breathe = t >= bIdle && t < bHi ? Math.sin(loopP(t, bIdle, bHi) * Math.PI * 2) : 0;\n var hover = t < bHi ? 0 : t < bHo ? $.E.out3($.seg(t, bHi, bHi + 0.22)) : t < bPress ? 1 - $.E.inOut2($.seg(t, bHo, bHo + 0.25)) : 0;\n var press = $.bump(t, bPress + 0.02, 0.06, 0.2);\n var collapse = t < bDone ? $.E.inOut3($.seg(t, bPress + 0.14, bLoad)) : 1 - $.E.out3($.seg(t, bDone, bDone + 0.35));\n var curW = $.lerp(w, h, collapse);\n var curR = $.lerp(r, h / 2, collapse);\n var lift = (hover * 6 + breathe * 1.5) * k;\n var scale = (0.6 + 0.4 * appear) * (1 - 0.06 * press) * (1 + 0.03 * hover);\n var fill = $.mix(c.accent, c.fg, 0.1 * hover);\n var fg = $.on(c.accent);\n var x = cx - curW / 2, y = cy - h / 2;\n\n var s = \"\";\n // The press ripple, outside the scaled button.\n var rp = $.seg(t, bPress, bPress + 0.55);\n if (rp > 0 && rp < 1) s += $.rect(cx - w / 2 - 26 * rp * k, y - 26 * rp * k, w + 52 * rp * k, h + 52 * rp * k, { r: r + 26 * rp * k, stroke: c.accent, sw: 5 * k * (1 - rp), op: 1 - rp });\n\n var b = uiBox($, x, y, curW, h, curR, fill, hover > 0.5 ? \"lg\" : \"md\");\n // The label and icon, until the button collapses into a circle.\n var labelOp = t < bDone ? 1 - $.seg(collapse, 0, 0.35) : 0;\n if (labelOp > 0) {\n var tx = cx - w / 2 + padX;\n b += $.text(label, Object.assign({}, font, { x: tx, y: cy - size * 0.55, fill: fg, op: labelOp }));\n if (p.icon) b += $.icon(p.icon, cx + w / 2 - padX - iconS + hover * 5 * k, cy - iconS / 2, iconS, { color: fg, sw: 2.4, op: labelOp });\n }\n // Loading: a spinner turning once a second, seamless across the loop.\n var spinOp = t >= bPress && t < bDone + 0.15 ? $.seg(t, bLoad - 0.15, bLoad) * (1 - $.seg(t, bDone, bDone + 0.12)) : 0;\n if (spinOp > 0) b += spinnerArc(cx, cy, h * 0.27, t >= bLoad ? loopP(t, bLoad, bDone) * Math.round(bDone - bLoad) : 0, fg, size * 0.22, spinOp);\n // Done: the check draws on, then the done label slides in as the button opens out again.\n if (t >= bDone) {\n var open = $.E.out3($.seg(t, bDone, bDone + 0.35));\n var ck = $.seg(t, bDone + 0.08, bDone + 0.42);\n var ckX = $.lerp(cx, cx - w / 2 + padX + iconS / 2, open);\n b += checkMark($, ckX, cy, iconS * 0.9, ck, fg, size * 0.2);\n var dOp = $.seg(t, bDone + 0.22, bDone + 0.5);\n if (dOp > 0) b += $.text(done, Object.assign({}, font, { x: cx - w / 2 + padX + iconS + gap + (1 - dOp) * 10 * k, y: cy - size * 0.55, fill: fg, op: dOp }));\n }\n s += posed(b, cx, cy, scale, 0, -lift, Math.min(1, appear * 1.4));\n return s;\n}\n","libs":[]},{"id":"toggle","name":"Toggle","description":"A settings switch with its label: the knob stretches as it slides, the track fills with the accent, and it slides back. In a page, a click turns it on and off.","category":"cta","tags":["ui","toggle","switch","settings","on off","interactive","micro-interaction","product","web"],"placement":"overlay","duration":{"default":2.4,"min":2.4,"max":2.4},"aspects":["16:9","9:16","1:1","4:5"],"props":{"label":{"type":"text","label":"Label","default":"Notifications","maxLength":24},"hint":{"type":"text","label":"Hint","default":"Get a ping when a render finishes","maxLength":44,"description":"empty for none"}},"beats":{"off":0.4,"turnOn":0.7,"turnOff":1.7},"states":{"enter":{"from":"start","to":"off","play":"once","next":"off"},"off":{"from":"off","play":"hold"},"on":{"from":"turnOn","to":"turnOff","play":"once"},"turning-off":{"from":"turnOff","to":"end","play":"once","next":"off"}},"on":"click(off|turning-off): on; click(on): turning-off","pack":"ui","ref":"ui/toggle","code":"// UI pieces: product micro-interactions whose timeline is cut into states at named beats (enter, idle,\n// hover, press, loading, done…). Loops are periodic so a looped state repeats without a seam, and every\n// state ends on a pose the next one starts from.\n\n/** 0→1 through a loop of [a, b): the frame at b is the frame at a. */\nfunction loopP(t, a, b) {\n var L = b - a;\n if (L <= 0) return 0;\n var x = (t - a) % L;\n return (x < 0 ? x + L : x) / L;\n}\n\n/** A size as a share of the frame's short side, so a control reads the same at every aspect. */\nfunction unit($, share) {\n return Math.min($.W, $.H) * share;\n}\n\n/** Fixed-precision number for markup. */\nfunction n2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A rounded box in the brand's surface: bold brands get an ink outline and a hard shadow. */\nfunction uiBox($, x, y, w, h, r, fill, shadow) {\n var k = $.k;\n if ($.style.surface === \"bold\") return $.rect(x + 6 * k, y + 6 * k, w, h, { r: r, fill: $.c.ink }) + $.rect(x, y, w, h, { r: r, fill: fill, stroke: $.c.ink, sw: 3.5 * k });\n return $.rect(x, y, w, h, { r: r, fill: fill, shadow: $.style.surface === \"flat\" ? null : shadow || \"md\" });\n}\n\n/** A spinner arc at (cx, cy) turned `turn` (0–1) of the way round. */\nfunction spinnerArc(cx, cy, r, turn, color, sw, op) {\n return '<g transform=\"rotate(' + n2(turn * 360) + \" \" + n2(cx) + \" \" + n2(cy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(op) + '\"' : \"\") + '><circle cx=\"' + n2(cx) + '\" cy=\"' + n2(cy) + '\" r=\"' + n2(r) + '\" fill=\"none\" stroke=\"' + color + '\" stroke-width=\"' + n2(sw) + '\" stroke-linecap=\"round\" pathLength=\"100\" stroke-dasharray=\"30 70\"/></g>';\n}\n\n/** A check mark centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction checkMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var d = \"M\" + n2(cx - s * 0.5) + \" \" + n2(cy + s * 0.02) + \" L\" + n2(cx - s * 0.14) + \" \" + n2(cy + s * 0.36) + \" L\" + n2(cx + s * 0.52) + \" \" + n2(cy - s * 0.34);\n return $.path(d, { stroke: color, sw: sw, draw: Math.min(1, p) });\n}\n\n/** A cross centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction crossMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var h = s * 0.4;\n var a = $.path(\"M\" + n2(cx - h) + \" \" + n2(cy - h) + \" L\" + n2(cx + h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, p * 2) });\n var b = p > 0.5 ? $.path(\"M\" + n2(cx + h) + \" \" + n2(cy - h) + \" L\" + n2(cx - h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, (p - 0.5) * 2) }) : \"\";\n return a + b;\n}\n\n/** A group scaled about (ox, oy) and moved by (dx, dy), at an opacity. */\nfunction posed(markup, ox, oy, s, dx, dy, op) {\n return '<g transform=\"translate(' + n2(dx || 0) + \" \" + n2(dy || 0) + \") translate(\" + n2(ox) + \" \" + n2(oy) + \") scale(\" + n2(s) + \") translate(\" + n2(-ox) + \" \" + n2(-oy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(Math.max(0, op)) + '\"' : \"\") + \">\" + markup + \"</g>\";\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n var bOff = $.beat(\"off\", 0.4), bOn = $.beat(\"turnOn\", 0.7), bBack = $.beat(\"turnOff\", 1.7);\n var tf = $.font(\"text\");\n var size = unit($, 0.06);\n var th = size * 1.9, tw = th * 1.8, kr = th * 0.5 - 5 * k;\n var label = $.case(p.label), hint = p.hint ? p.hint : \"\";\n var lf = { size: size, weight: Math.max(650, tf.weight), family: tf.family };\n var hf = { size: size * 0.62, weight: 500, family: tf.family };\n var textW = Math.max($.measure(label, lf), hint ? $.measure(hint, hf) : 0);\n var gap = size * 1.4, total = textW + gap + tw;\n var x0 = W / 2 - total / 2, tx = x0 + textW + gap, ty = H / 2 - th / 2;\n\n var appear = $.E.outBack($.seg(t, 0, bOff * 0.9));\n // How far on: slides on at turnOn, back off at turnOff; the knob stretches while it moves.\n var on = t < bOn ? 0 : t < bBack ? $.E.inOut3($.seg(t, bOn, bOn + 0.38)) : 1 - $.E.inOut3($.seg(t, bBack, bBack + 0.38));\n var moving = t < bBack ? $.bump(t, bOn + 0.05, 0.12, 0.25) : $.bump(t, bBack + 0.05, 0.12, 0.25);\n var track = $.mix($.mix(c.surface, c.fg, 0.22), c.accent, on);\n var padX = size * 1.0, padY = size * 0.8, ch = Math.max(th, hint ? size * 1.9 : size) + padY * 2;\n var s = uiBox($, x0 - padX, H / 2 - ch / 2, total + padX * 2, ch, Math.min(ch / 2, Math.max(10 * k, $.style.radius)), c.surface, \"md\");\n s += $.text(label, Object.assign({}, lf, { x: x0, y: H / 2 - (hint ? size * 1.05 : size * 0.55), fill: c.fg }));\n if (hint) s += $.text(hint, Object.assign({}, hf, { x: x0, y: H / 2 + size * 0.25, fill: $.mix(c.fg, c.surface, 0.4) }));\n var g = uiBox($, tx, ty, tw, th, th / 2, track, null);\n var kx = $.lerp(tx + th / 2, tx + tw - th / 2, on);\n var stretch = 1 + 0.35 * moving;\n g += '<ellipse cx=\"' + n2(kx) + '\" cy=\"' + n2(H / 2) + '\" rx=\"' + n2(kr * stretch) + '\" ry=\"' + n2(kr * (1 - 0.08 * moving)) + '\" fill=\"' + $.on(c.accent) + '\"/>';\n if (on > 0.6) g += checkMark($, kx, H / 2, kr * 0.9, (on - 0.6) / 0.4, c.accent, size * 0.16);\n s += g;\n return posed(s, W / 2, H / 2, 0.85 + 0.15 * appear, 0, (1 - appear) * 20 * k, Math.min(1, appear * 1.5));\n}\n","libs":[]},{"id":"like","name":"Like","description":"A heart and its count: it breathes while idle, pops full with a burst when liked as the count rolls up by one, and empties again when unliked. In a page, a click likes and unlikes.","category":"social","tags":["ui","like","heart","favourite","count","interactive","micro-interaction","social","web"],"placement":"overlay","duration":{"default":3.2,"min":3.2,"max":3.2},"aspects":["16:9","9:16","1:1","4:5"],"props":{"count":{"type":"number","label":"Likes before","default":128,"min":0,"max":9999999,"step":1}},"beats":{"idle":0.5,"like":1.5,"unlike":2.6},"states":{"enter":{"from":"start","to":"idle","play":"once","next":"idle"},"idle":{"from":"idle","to":"like","play":"loop"},"like":{"from":"like","to":"unlike","play":"once"},"unlike":{"from":"unlike","to":"end","play":"once","next":"idle"}},"on":"click(idle|unlike): like; click(like): unlike","pack":"ui","ref":"ui/like","code":"// UI pieces: product micro-interactions whose timeline is cut into states at named beats (enter, idle,\n// hover, press, loading, done…). Loops are periodic so a looped state repeats without a seam, and every\n// state ends on a pose the next one starts from.\n\n/** 0→1 through a loop of [a, b): the frame at b is the frame at a. */\nfunction loopP(t, a, b) {\n var L = b - a;\n if (L <= 0) return 0;\n var x = (t - a) % L;\n return (x < 0 ? x + L : x) / L;\n}\n\n/** A size as a share of the frame's short side, so a control reads the same at every aspect. */\nfunction unit($, share) {\n return Math.min($.W, $.H) * share;\n}\n\n/** Fixed-precision number for markup. */\nfunction n2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A rounded box in the brand's surface: bold brands get an ink outline and a hard shadow. */\nfunction uiBox($, x, y, w, h, r, fill, shadow) {\n var k = $.k;\n if ($.style.surface === \"bold\") return $.rect(x + 6 * k, y + 6 * k, w, h, { r: r, fill: $.c.ink }) + $.rect(x, y, w, h, { r: r, fill: fill, stroke: $.c.ink, sw: 3.5 * k });\n return $.rect(x, y, w, h, { r: r, fill: fill, shadow: $.style.surface === \"flat\" ? null : shadow || \"md\" });\n}\n\n/** A spinner arc at (cx, cy) turned `turn` (0–1) of the way round. */\nfunction spinnerArc(cx, cy, r, turn, color, sw, op) {\n return '<g transform=\"rotate(' + n2(turn * 360) + \" \" + n2(cx) + \" \" + n2(cy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(op) + '\"' : \"\") + '><circle cx=\"' + n2(cx) + '\" cy=\"' + n2(cy) + '\" r=\"' + n2(r) + '\" fill=\"none\" stroke=\"' + color + '\" stroke-width=\"' + n2(sw) + '\" stroke-linecap=\"round\" pathLength=\"100\" stroke-dasharray=\"30 70\"/></g>';\n}\n\n/** A check mark centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction checkMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var d = \"M\" + n2(cx - s * 0.5) + \" \" + n2(cy + s * 0.02) + \" L\" + n2(cx - s * 0.14) + \" \" + n2(cy + s * 0.36) + \" L\" + n2(cx + s * 0.52) + \" \" + n2(cy - s * 0.34);\n return $.path(d, { stroke: color, sw: sw, draw: Math.min(1, p) });\n}\n\n/** A cross centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction crossMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var h = s * 0.4;\n var a = $.path(\"M\" + n2(cx - h) + \" \" + n2(cy - h) + \" L\" + n2(cx + h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, p * 2) });\n var b = p > 0.5 ? $.path(\"M\" + n2(cx + h) + \" \" + n2(cy - h) + \" L\" + n2(cx - h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, (p - 0.5) * 2) }) : \"\";\n return a + b;\n}\n\n/** A group scaled about (ox, oy) and moved by (dx, dy), at an opacity. */\nfunction posed(markup, ox, oy, s, dx, dy, op) {\n return '<g transform=\"translate(' + n2(dx || 0) + \" \" + n2(dy || 0) + \") translate(\" + n2(ox) + \" \" + n2(oy) + \") scale(\" + n2(s) + \") translate(\" + n2(-ox) + \" \" + n2(-oy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(Math.max(0, op)) + '\"' : \"\") + \">\" + markup + \"</g>\";\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n var bIdle = $.beat(\"idle\", 0.5), bLike = $.beat(\"like\", 1.5), bUn = $.beat(\"unlike\", 2.6);\n var tf = $.font(\"text\");\n var hs = unit($, 0.17);\n var base = Math.max(0, Math.round(Number(p.count) || 0));\n var cf = { size: hs * 0.5, weight: Math.max(700, tf.weight), family: tf.family };\n var after = $.count(base + 1), before = $.count(base);\n var cw = Math.max($.measure(after, cf), $.measure(before, cf));\n var gap = hs * 0.3, total = hs + gap + cw;\n var hx = W / 2 - total / 2, hy = H / 2 - hs / 2, cx = hx + hs / 2, cy = H / 2;\n\n var appear = $.E.outBack($.seg(t, 0, bIdle * 0.9));\n var breathe = t >= bIdle && t < bLike ? Math.sin(loopP(t, bIdle, bLike) * Math.PI * 2) : 0;\n var liked = t < bLike ? 0 : t < bUn ? 1 : 1 - $.E.inOut2($.seg(t, bUn, bUn + 0.3));\n var popS = t >= bLike && t < bUn ? 1 + 0.45 * $.bump(t, bLike, 0.1, 0.35) : 1;\n var squash = t >= bUn ? $.bump(t, bUn, 0.08, 0.25) : 0;\n var pad = hs * 0.32, ph = hs + pad * 2;\n var s = uiBox($, hx - pad * 1.2, cy - ph / 2, total + pad * 2.6, ph, ph / 2, c.surface, \"md\");\n if (t >= bLike && t < bUn) s += $.burst(t, bLike + 0.05, cx, cy, { n: 12, r: hs * 1.2, colors: [c.accent, c.accent2], size: hs * 0.08, dur: 0.7 });\n // The heart: an outline that fills with the accent when liked.\n var scale = (0.7 + 0.3 * appear) * (1 + 0.04 * breathe) * (liked > 0 && t < bUn ? popS : 1) * (1 - 0.15 * squash);\n var heart = $.icon(\"heart\", hx, hy, hs, { color: $.mix(c.fg, c.accent, liked), sw: 2.2 });\n if (liked > 0.01) heart += '<g opacity=\"' + n2(liked) + '\">' + $.icon(\"heart\", hx, hy, hs, { color: c.accent, fill: true }) + \"</g>\";\n s += posed(heart, cx, cy, scale, 0, 0, Math.min(1, appear * 1.5));\n // The count rolls: the old number slides up and out as the new one slides in.\n var roll = t < bLike ? 0 : t < bUn ? $.E.out3($.seg(t, bLike + 0.05, bLike + 0.4)) : 1 - $.E.out3($.seg(t, bUn, bUn + 0.35));\n var tx = hx + hs + gap, ty = cy - cf.size * 0.55, dy = cf.size * 0.9;\n var clip = $.uid();\n $.def('<clipPath id=\"' + clip + '\"><rect x=\"' + n2(tx - 4 * k) + '\" y=\"' + n2(cy - cf.size * 0.8) + '\" width=\"' + n2(cw + 8 * k) + '\" height=\"' + n2(cf.size * 1.6) + '\"/></clipPath>');\n var nums = $.text(before, Object.assign({}, cf, { x: tx, y: ty - dy * roll, fill: c.fg, op: Math.max(0, 1 - roll * 2) })) + $.text(after, Object.assign({}, cf, { x: tx, y: ty + dy * (1 - roll), fill: liked > 0.5 ? c.accent : c.fg, op: Math.max(0, roll * 2 - 1) }));\n s += '<g clip-path=\"url(#' + clip + ')\"' + (appear < 1 ? ' opacity=\"' + n2(Math.min(1, appear * 1.5)) + '\"' : \"\") + \">\" + nums + \"</g>\";\n return s;\n}\n","libs":[]},{"id":"loader","name":"Loader","description":"A status card that waits, then ends one of two ways: a spinner turns while the line says what is happening, then it closes into a ring with a check, or turns into a cross with a shake. In a page, your code sets the state when the work finishes.","category":"callout","tags":["ui","loader","spinner","loading","progress","success","error","status","interactive","micro-interaction","web"],"placement":"overlay","duration":{"default":3.6,"min":3.6,"max":3.6},"aspects":["16:9","9:16","1:1","4:5"],"props":{"loading":{"type":"text","label":"While it works","default":"Uploading your video","maxLength":32},"success":{"type":"text","label":"When it works","default":"Uploaded","maxLength":32},"failure":{"type":"text","label":"When it fails","default":"Upload failed — try again","maxLength":32}},"beats":{"loading":0.4,"success":1.4,"failure":2.5},"states":{"enter":{"from":"start","to":"loading","play":"once","next":"loading"},"loading":{"from":"loading","to":"success","play":"loop"},"success":{"from":"success","to":"failure","play":"once"},"failure":{"from":"failure","to":"end","play":"once"}},"on":"click(success|failure): loading","pack":"ui","ref":"ui/loader","code":"// UI pieces: product micro-interactions whose timeline is cut into states at named beats (enter, idle,\n// hover, press, loading, done…). Loops are periodic so a looped state repeats without a seam, and every\n// state ends on a pose the next one starts from.\n\n/** 0→1 through a loop of [a, b): the frame at b is the frame at a. */\nfunction loopP(t, a, b) {\n var L = b - a;\n if (L <= 0) return 0;\n var x = (t - a) % L;\n return (x < 0 ? x + L : x) / L;\n}\n\n/** A size as a share of the frame's short side, so a control reads the same at every aspect. */\nfunction unit($, share) {\n return Math.min($.W, $.H) * share;\n}\n\n/** Fixed-precision number for markup. */\nfunction n2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A rounded box in the brand's surface: bold brands get an ink outline and a hard shadow. */\nfunction uiBox($, x, y, w, h, r, fill, shadow) {\n var k = $.k;\n if ($.style.surface === \"bold\") return $.rect(x + 6 * k, y + 6 * k, w, h, { r: r, fill: $.c.ink }) + $.rect(x, y, w, h, { r: r, fill: fill, stroke: $.c.ink, sw: 3.5 * k });\n return $.rect(x, y, w, h, { r: r, fill: fill, shadow: $.style.surface === \"flat\" ? null : shadow || \"md\" });\n}\n\n/** A spinner arc at (cx, cy) turned `turn` (0–1) of the way round. */\nfunction spinnerArc(cx, cy, r, turn, color, sw, op) {\n return '<g transform=\"rotate(' + n2(turn * 360) + \" \" + n2(cx) + \" \" + n2(cy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(op) + '\"' : \"\") + '><circle cx=\"' + n2(cx) + '\" cy=\"' + n2(cy) + '\" r=\"' + n2(r) + '\" fill=\"none\" stroke=\"' + color + '\" stroke-width=\"' + n2(sw) + '\" stroke-linecap=\"round\" pathLength=\"100\" stroke-dasharray=\"30 70\"/></g>';\n}\n\n/** A check mark centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction checkMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var d = \"M\" + n2(cx - s * 0.5) + \" \" + n2(cy + s * 0.02) + \" L\" + n2(cx - s * 0.14) + \" \" + n2(cy + s * 0.36) + \" L\" + n2(cx + s * 0.52) + \" \" + n2(cy - s * 0.34);\n return $.path(d, { stroke: color, sw: sw, draw: Math.min(1, p) });\n}\n\n/** A cross centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction crossMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var h = s * 0.4;\n var a = $.path(\"M\" + n2(cx - h) + \" \" + n2(cy - h) + \" L\" + n2(cx + h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, p * 2) });\n var b = p > 0.5 ? $.path(\"M\" + n2(cx + h) + \" \" + n2(cy - h) + \" L\" + n2(cx - h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, (p - 0.5) * 2) }) : \"\";\n return a + b;\n}\n\n/** A group scaled about (ox, oy) and moved by (dx, dy), at an opacity. */\nfunction posed(markup, ox, oy, s, dx, dy, op) {\n return '<g transform=\"translate(' + n2(dx || 0) + \" \" + n2(dy || 0) + \") translate(\" + n2(ox) + \" \" + n2(oy) + \") scale(\" + n2(s) + \") translate(\" + n2(-ox) + \" \" + n2(-oy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(Math.max(0, op)) + '\"' : \"\") + \">\" + markup + \"</g>\";\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n var bLoad = $.beat(\"loading\", 0.4), bOk = $.beat(\"success\", 1.4), bFail = $.beat(\"failure\", 2.5);\n var tf = $.font(\"text\");\n var size = unit($, 0.055);\n var font = { size: size, weight: Math.max(600, tf.weight), family: tf.family };\n var lines = [$.case(p.loading), $.case(p.success), $.case(p.failure)];\n var iconR = size * 0.85, padX = size * 1.1, padY = size * 0.85, gap = size * 0.75;\n var tw = Math.max($.measure(lines[0], font), $.measure(lines[1], font), $.measure(lines[2], font));\n var w = padX * 2 + iconR * 2 + gap + tw, h = padY * 2 + iconR * 2;\n var x = W / 2 - w / 2, y = H / 2 - h / 2, icx = x + padX + iconR, icy = H / 2, tx = icx + iconR + gap;\n\n var appear = $.E.outBack($.seg(t, 0, bLoad * 0.9));\n var phase = t < bOk ? 0 : t < bFail ? 1 : 2;\n var ok = phase === 1 ? $.E.out3($.seg(t, bOk, bOk + 0.4)) : 0;\n var bad = phase === 2 ? $.E.out3($.seg(t, bFail, bFail + 0.4)) : 0;\n var shake = phase === 2 ? $.shake(t, bFail + 0.15, 9 * k, 0.4)[0] : 0;\n var tone = phase === 2 ? c.red || c.accent2 : c.accent;\n var s = uiBox($, x, y, w, h, Math.min(h / 2, Math.max(10 * k, $.style.radius)), c.surface, \"md\");\n // The icon: a spinner while it works (one turn a second, seamless), then a full ring with a check or a cross.\n var sw = size * 0.2;\n if (phase === 0) s += '<circle cx=\"' + n2(icx) + '\" cy=\"' + n2(icy) + '\" r=\"' + n2(iconR) + '\" fill=\"none\" stroke=\"' + $.mix(c.surface, c.fg, 0.12) + '\" stroke-width=\"' + n2(sw) + '\"/>' + spinnerArc(icx, icy, iconR, t >= bLoad ? loopP(t, bLoad, bOk) * Math.round(bOk - bLoad) : 0, tone, sw, 1);\n else {\n var done = phase === 1 ? ok : bad;\n s += $.ring(icx, icy, iconR, done, { color: tone, sw: sw, bg: $.mix(c.surface, c.fg, 0.12) });\n if (phase === 1) s += checkMark($, icx, icy, iconR * 1.0, $.seg(t, bOk + 0.15, bOk + 0.5), tone, sw);\n else s += crossMark($, icx, icy, iconR * 1.0, $.seg(t, bFail + 0.15, bFail + 0.5), tone, sw);\n }\n // The line: the new one rises in as the old one fades.\n var swap = phase === 0 ? 1 : $.E.out3($.seg(t, phase === 1 ? bOk + 0.1 : bFail + 0.1, (phase === 1 ? bOk : bFail) + 0.45));\n if (phase > 0 && swap < 1) s += $.text(lines[phase - 1], Object.assign({}, font, { x: tx, y: icy - size * 0.55 - swap * size * 0.6, fill: c.fg, op: 1 - swap }));\n s += $.text(lines[phase], Object.assign({}, font, { x: tx, y: icy - size * 0.55 + (1 - swap) * size * 0.6, fill: c.fg, op: swap }));\n return posed(s, W / 2, H / 2, 0.85 + 0.15 * appear, shake, (1 - appear) * 20 * k, Math.min(1, appear * 1.5));\n}\n","libs":[]},{"id":"bell","name":"Notification bell","description":"A bell that rings: it swings on its hook, a badge pops in with the count, and it clears when read. In a page, hovering rings it and a click marks it read.","category":"social","tags":["ui","notification","bell","badge","alert","inbox","interactive","micro-interaction","web"],"placement":"overlay","duration":{"default":3,"min":3,"max":3},"aspects":["16:9","9:16","1:1","4:5"],"props":{"count":{"type":"number","label":"New","default":3,"min":1,"max":99,"step":1}},"beats":{"idle":0.5,"ring":1.5,"clear":2.5},"states":{"enter":{"from":"start","to":"idle","play":"once","next":"idle"},"idle":{"from":"idle","to":"ring","play":"loop"},"ring":{"from":"ring","to":"clear","play":"once"},"clear":{"from":"clear","to":"end","play":"once","next":"idle"}},"on":"pointerenter(idle): ring; click(ring): clear","pack":"ui","ref":"ui/bell","code":"// UI pieces: product micro-interactions whose timeline is cut into states at named beats (enter, idle,\n// hover, press, loading, done…). Loops are periodic so a looped state repeats without a seam, and every\n// state ends on a pose the next one starts from.\n\n/** 0→1 through a loop of [a, b): the frame at b is the frame at a. */\nfunction loopP(t, a, b) {\n var L = b - a;\n if (L <= 0) return 0;\n var x = (t - a) % L;\n return (x < 0 ? x + L : x) / L;\n}\n\n/** A size as a share of the frame's short side, so a control reads the same at every aspect. */\nfunction unit($, share) {\n return Math.min($.W, $.H) * share;\n}\n\n/** Fixed-precision number for markup. */\nfunction n2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A rounded box in the brand's surface: bold brands get an ink outline and a hard shadow. */\nfunction uiBox($, x, y, w, h, r, fill, shadow) {\n var k = $.k;\n if ($.style.surface === \"bold\") return $.rect(x + 6 * k, y + 6 * k, w, h, { r: r, fill: $.c.ink }) + $.rect(x, y, w, h, { r: r, fill: fill, stroke: $.c.ink, sw: 3.5 * k });\n return $.rect(x, y, w, h, { r: r, fill: fill, shadow: $.style.surface === \"flat\" ? null : shadow || \"md\" });\n}\n\n/** A spinner arc at (cx, cy) turned `turn` (0–1) of the way round. */\nfunction spinnerArc(cx, cy, r, turn, color, sw, op) {\n return '<g transform=\"rotate(' + n2(turn * 360) + \" \" + n2(cx) + \" \" + n2(cy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(op) + '\"' : \"\") + '><circle cx=\"' + n2(cx) + '\" cy=\"' + n2(cy) + '\" r=\"' + n2(r) + '\" fill=\"none\" stroke=\"' + color + '\" stroke-width=\"' + n2(sw) + '\" stroke-linecap=\"round\" pathLength=\"100\" stroke-dasharray=\"30 70\"/></g>';\n}\n\n/** A check mark centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction checkMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var d = \"M\" + n2(cx - s * 0.5) + \" \" + n2(cy + s * 0.02) + \" L\" + n2(cx - s * 0.14) + \" \" + n2(cy + s * 0.36) + \" L\" + n2(cx + s * 0.52) + \" \" + n2(cy - s * 0.34);\n return $.path(d, { stroke: color, sw: sw, draw: Math.min(1, p) });\n}\n\n/** A cross centred at (cx, cy), `s` wide, drawn on as p goes 0→1. */\nfunction crossMark($, cx, cy, s, p, color, sw) {\n if (p <= 0) return \"\";\n var h = s * 0.4;\n var a = $.path(\"M\" + n2(cx - h) + \" \" + n2(cy - h) + \" L\" + n2(cx + h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, p * 2) });\n var b = p > 0.5 ? $.path(\"M\" + n2(cx + h) + \" \" + n2(cy - h) + \" L\" + n2(cx - h) + \" \" + n2(cy + h), { stroke: color, sw: sw, draw: Math.min(1, (p - 0.5) * 2) }) : \"\";\n return a + b;\n}\n\n/** A group scaled about (ox, oy) and moved by (dx, dy), at an opacity. */\nfunction posed(markup, ox, oy, s, dx, dy, op) {\n return '<g transform=\"translate(' + n2(dx || 0) + \" \" + n2(dy || 0) + \") translate(\" + n2(ox) + \" \" + n2(oy) + \") scale(\" + n2(s) + \") translate(\" + n2(-ox) + \" \" + n2(-oy) + ')\"' + (op != null && op < 1 ? ' opacity=\"' + n2(Math.max(0, op)) + '\"' : \"\") + \">\" + markup + \"</g>\";\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n var bIdle = $.beat(\"idle\", 0.5), bRing = $.beat(\"ring\", 1.5), bClear = $.beat(\"clear\", 2.5);\n var tf = $.font(\"text\");\n var bs = unit($, 0.18);\n var cx = W / 2, cy = H / 2, bx = cx - bs / 2, by = cy - bs / 2;\n var appear = $.E.outBack($.seg(t, 0, bIdle * 0.9));\n var sway = t >= bIdle && t < bRing ? Math.sin(loopP(t, bIdle, bRing) * Math.PI * 2) * 2.5 : 0;\n // Ringing: a damped swing about the hook at the top.\n var rt = t - bRing;\n var swing = t >= bRing && t < bClear ? 22 * Math.exp(-rt * 3.2) * Math.sin(rt * 26) : 0;\n var disc = bs * 1.7;\n var s = uiBox($, cx - disc / 2, cy - disc / 2, disc, disc, disc / 2, c.surface, \"md\");\n s += '<g transform=\"rotate(' + n2(sway + swing) + \" \" + n2(cx) + \" \" + n2(by + bs * 0.08) + ')\">' + $.icon(\"bell\", bx, by, bs, { color: c.fg, sw: 2.2 }) + \"</g>\";\n // The badge pops in when it rings and shrinks away when read.\n var show = t < bRing ? 0 : t < bClear ? $.pop(t, bRing + 0.08, 2.6, 6) : 1 - $.E.in3($.seg(t, bClear, bClear + 0.3));\n if (show > 0.01) {\n var n = String(Math.max(1, Math.round(Number(p.count) || 1)));\n var r = bs * 0.24, bcx = cx + bs * 0.32, bcy = by + bs * 0.18;\n var badge = $.circle(bcx, bcy, r, { fill: c.accent, stroke: c.surface, sw: 4 * k }) + $.text(n, { x: bcx, y: bcy - r * 0.62, size: r * 1.15, weight: Math.max(700, tf.weight), family: tf.family, fill: $.on(c.accent), anchor: \"middle\" });\n s += posed(badge, bcx, bcy, show, 0, 0, Math.min(1, show * 2));\n }\n return posed(s, cx, cy, 0.7 + 0.3 * appear, 0, 0, Math.min(1, appear * 1.5));\n}\n","libs":[]}]},{"format":1,"id":"effects","name":"MONARC Effects","version":"1.0.0","description":"Looks made from SVG filters, so they render the same in Node and the browser with no GPU: film grain, a light leak, a frosted glass card, a chromatic split title, a liquid warp title and a neon sign, in the brand's colours.","author":"MONARC STUDIOS","license":"MONARC Packs License 1.0 (free to use in any video; not for resale as templates)","homepage":"https://volito.dev/docs/packs/effects","style":"cobalt","shared":"shared.js","items":["grain","light-leak","glass","chromatic","warp","neon"],"entries":[{"id":"grain","name":"Film grain","description":"Moving film grain and a soft vignette over footage: the grain re-rolls on every film frame, so it reads as real stock, not a still texture.","category":"scene","tags":["effect","film grain","grain","noise","vignette","film look","texture","overlay"],"placement":"overlay","duration":{"default":6,"min":1,"max":60},"aspects":["16:9","9:16","1:1","4:5"],"props":{"amount":{"type":"number","label":"Grain","default":0.35,"min":0,"max":1,"step":0.05},"size":{"type":"choice","label":"Grain size","options":["fine","medium","coarse"],"default":"medium"},"vignette":{"type":"number","label":"Vignette","default":0.4,"min":0,"max":1,"step":0.05}},"beats":{},"pack":"effects","ref":"effects/grain","code":"// Effects: looks built from SVG filters (turbulence, displacement, blur, colour matrices), which resvg and\n// browsers both draw, so a render and the live player match. Every filter is a pure function of t.\n\n/** Fixed-precision number for markup. */\nfunction f2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A glow filter (a blur of the shape under the shape itself); returns its id. */\nfunction glowFilter($, std, strength) {\n var id = $.uid();\n var copies = \"\";\n for (var i = 0; i < (strength || 2); i++) copies += '<feMergeNode in=\"b\"/>';\n $.def('<filter id=\"' + id + '\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\"><feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"' + f2(std) + '\" result=\"b\"/><feMerge>' + copies + '<feMergeNode in=\"SourceGraphic\"/></feMerge></filter>');\n return id;\n}\n\n/** A title sized to fit the safe area's width, centred: { font, x, y, lines }. */\nfunction fitTitle($, str, share, maxSize) {\n var A = safeArea($);\n var f = fitFont($, str, \"display\", Math.min(maxSize, $.W * share), A.w * 0.9, { ls: -0.03 });\n return { font: f, x: $.W / 2, y: $.H / 2 - f.size * 0.56 };\n}\n\nfunction frame(t, $) {\n var p = $.data, W = $.W, H = $.H;\n var amount = $.clamp(Number(p.amount)), vig = $.clamp(Number(p.vignette));\n var freq = { fine: 0.95, medium: 0.7, coarse: 0.45 }[p.size] || 0.7;\n var s = \"\";\n if (amount > 0) {\n // Grey noise, pushed light and dark around the middle, at a low opacity: specks lighter and darker than the\n // picture under them. A new seed on every film frame (24 a second) moves it the way film stock does.\n var seed = Math.floor($.step(t, 24) * 24) % 997;\n var id = $.uid();\n $.def('<filter id=\"' + id + '\" x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" color-interpolation-filters=\"sRGB\"><feTurbulence type=\"fractalNoise\" baseFrequency=\"' + f2(freq / Math.max(1, $.k)) + '\" numOctaves=\"2\" seed=\"' + seed + '\" result=\"n\"/><feColorMatrix in=\"n\" type=\"matrix\" values=\"2.4 0 0 0 -0.7 2.4 0 0 0 -0.7 2.4 0 0 0 -0.7 0 0 0 0 ' + f2(amount * 0.42) + '\"/></filter>');\n s += '<rect x=\"0\" y=\"0\" width=\"' + W + '\" height=\"' + H + '\" filter=\"url(#' + id + ')\"/>';\n }\n if (vig > 0) {\n var g = $.uid();\n $.def('<radialGradient id=\"' + g + '\" cx=\"50%\" cy=\"50%\" r=\"72%\"><stop offset=\"55%\" stop-color=\"#000\" stop-opacity=\"0\"/><stop offset=\"100%\" stop-color=\"#000\" stop-opacity=\"' + f2(vig * 0.85) + '\"/></radialGradient>');\n s += '<rect x=\"0\" y=\"0\" width=\"' + W + '\" height=\"' + H + '\" fill=\"url(#' + g + ')\"/>';\n }\n return s;\n}\n","libs":[]},{"id":"light-leak","name":"Light leak","description":"Warm light bleeding in from the edge of the frame and drifting across it, in the brand's colours: for openings, transitions and a film feel over footage.","category":"scene","tags":["effect","light leak","film","glow","flare","warm","overlay","transition"],"placement":"overlay","duration":{"default":4,"min":1,"max":30},"aspects":["16:9","9:16","1:1","4:5"],"props":{"from":{"type":"choice","label":"From","options":["left","right","top","corner"],"default":"left"},"strength":{"type":"number","label":"Strength","default":0.7,"min":0.1,"max":1,"step":0.05}},"beats":{},"pack":"effects","ref":"effects/light-leak","code":"// Effects: looks built from SVG filters (turbulence, displacement, blur, colour matrices), which resvg and\n// browsers both draw, so a render and the live player match. Every filter is a pure function of t.\n\n/** Fixed-precision number for markup. */\nfunction f2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A glow filter (a blur of the shape under the shape itself); returns its id. */\nfunction glowFilter($, std, strength) {\n var id = $.uid();\n var copies = \"\";\n for (var i = 0; i < (strength || 2); i++) copies += '<feMergeNode in=\"b\"/>';\n $.def('<filter id=\"' + id + '\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\"><feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"' + f2(std) + '\" result=\"b\"/><feMerge>' + copies + '<feMergeNode in=\"SourceGraphic\"/></feMerge></filter>');\n return id;\n}\n\n/** A title sized to fit the safe area's width, centred: { font, x, y, lines }. */\nfunction fitTitle($, str, share, maxSize) {\n var A = safeArea($);\n var f = fitFont($, str, \"display\", Math.min(maxSize, $.W * share), A.w * 0.9, { ls: -0.03 });\n return { font: f, x: $.W / 2, y: $.H / 2 - f.size * 0.56 };\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, W = $.W, H = $.H, D = $.dur;\n var strength = $.clamp(Number(p.strength) || 0.7, 0.1, 1);\n // It swells in, drifts, and fades out by the end.\n var life = $.E.out3($.seg(t, 0, Math.min(1, D * 0.3))) * (1 - $.E.in2($.seg(t, D - Math.min(1, D * 0.3), D)));\n var u = D > 0 ? t / D : 0;\n var origin = { left: [0, 0.45], right: [1, 0.55], top: [0.5, 0], corner: [0, 0] }[p.from] || [0, 0.45];\n var dir = { left: [1, 0.15], right: [-1, -0.1], top: [0.1, 1], corner: [0.8, 0.6] }[p.from] || [1, 0.15];\n var blobs = [\n { c: c.accent, r: 0.75, d: 0.18, a: 0.55 },\n { c: c.accent2 || c.accent, r: 0.5, d: 0.32, a: 0.45 },\n { c: $.mix(c.accent2 || c.accent, \"#FFFFFF\", 0.55), r: 0.3, d: 0.45, a: 0.5 },\n ];\n var s = \"\";\n var L = Math.max(W, H);\n for (var i = 0; i < blobs.length; i++) {\n var b = blobs[i], id = $.uid();\n var x = (origin[0] + dir[0] * b.d * u) * W + Math.sin(t * 0.7 + i * 2.1) * L * 0.03;\n var y = (origin[1] + dir[1] * b.d * u) * H + Math.cos(t * 0.5 + i * 1.3) * L * 0.03;\n $.def('<radialGradient id=\"' + id + '\"><stop offset=\"0%\" stop-color=\"' + b.c + '\" stop-opacity=\"' + f2(b.a * strength * life) + '\"/><stop offset=\"60%\" stop-color=\"' + b.c + '\" stop-opacity=\"' + f2(b.a * 0.35 * strength * life) + '\"/><stop offset=\"100%\" stop-color=\"' + b.c + '\" stop-opacity=\"0\"/></radialGradient>');\n s += '<circle cx=\"' + f2(x) + '\" cy=\"' + f2(y) + '\" r=\"' + f2(L * b.r) + '\" fill=\"url(#' + id + ')\"/>';\n }\n return s;\n}\n","libs":[]},{"id":"glass","name":"Glass card","description":"A frosted glass card over soft moving colour: the ground blurs through the glass, a highlight runs along its edge, and the title sets on it.","category":"title","tags":["effect","glass","glassmorphism","frosted","blur","title","card","gradient"],"placement":"full","duration":{"default":5,"min":2,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"kicker":{"type":"text","label":"Kicker","default":"New","maxLength":20,"description":"empty for none"},"title":{"type":"text","label":"Title","default":"Made for agents","maxLength":28},"subtitle":{"type":"text","label":"Subtitle","default":"Motion graphics as code","maxLength":44,"description":"empty for none"}},"beats":{"in":0.2},"pack":"effects","ref":"effects/glass","code":"// Effects: looks built from SVG filters (turbulence, displacement, blur, colour matrices), which resvg and\n// browsers both draw, so a render and the live player match. Every filter is a pure function of t.\n\n/** Fixed-precision number for markup. */\nfunction f2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A glow filter (a blur of the shape under the shape itself); returns its id. */\nfunction glowFilter($, std, strength) {\n var id = $.uid();\n var copies = \"\";\n for (var i = 0; i < (strength || 2); i++) copies += '<feMergeNode in=\"b\"/>';\n $.def('<filter id=\"' + id + '\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\"><feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"' + f2(std) + '\" result=\"b\"/><feMerge>' + copies + '<feMergeNode in=\"SourceGraphic\"/></feMerge></filter>');\n return id;\n}\n\n/** A title sized to fit the safe area's width, centred: { font, x, y, lines }. */\nfunction fitTitle($, str, share, maxSize) {\n var A = safeArea($);\n var f = fitFont($, str, \"display\", Math.min(maxSize, $.W * share), A.w * 0.9, { ls: -0.03 });\n return { font: f, x: $.W / 2, y: $.H / 2 - f.size * 0.56 };\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n var tin = $.beat(\"in\", 0.2);\n var L = Math.min(W, H);\n // The ground: the brand's ground with three soft colour fields drifting on slow loops.\n var fields = [\n { col: c.accent, x: 0.3, y: 0.35, r: 0.42, sp: 0.21 },\n { col: c.accent2 || c.accent, x: 0.72, y: 0.62, r: 0.36, sp: 0.17 },\n { col: $.mix(c.accent, c.accent2 || c.accent, 0.5), x: 0.55, y: 0.2, r: 0.28, sp: 0.27 },\n ];\n var blobs = \"\";\n for (var i = 0; i < fields.length; i++) {\n var f = fields[i];\n var x = (f.x + Math.sin(t * f.sp * 2 + i) * 0.06) * W, y = (f.y + Math.cos(t * f.sp * 2 + i * 1.7) * 0.06) * H;\n blobs += '<circle cx=\"' + f2(x) + '\" cy=\"' + f2(y) + '\" r=\"' + f2(L * f.r) + '\" fill=\"' + f.col + '\"/>';\n }\n var soft = $.uid();\n $.def('<filter id=\"' + soft + '\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\"><feGaussianBlur stdDeviation=\"' + f2(L * 0.08) + '\"/></filter>');\n var s = '<rect x=\"0\" y=\"0\" width=\"' + W + '\" height=\"' + H + '\" fill=\"' + c.bg + '\"/><g filter=\"url(#' + soft + ')\">' + blobs + \"</g>\";\n\n // The glass: the same fields blurred harder, clipped to the card, a frost over them and a lit edge.\n var A = safeArea($);\n var title = $.case(p.title), sub = p.subtitle ? $.case(p.subtitle) : \"\", kick = p.kicker ? $.case(p.kicker) : \"\";\n var tf = fitFont($, title, \"display\", L * 0.11, A.w * 0.72, { ls: -0.03 });\n var sf = { size: tf.size * 0.32, weight: 500, family: $.font(\"text\").family };\n var cw = Math.min(A.w, Math.max($.measure(title, tf), sub ? $.measure(sub, sf) : 0) + tf.size * 1.6);\n var ch = tf.size * (sub ? 2.4 : 1.8) + (kick ? tf.size * 0.7 : 0);\n var rise = $.E.out3($.seg(t, tin, tin + 0.7));\n var cx = W / 2 - cw / 2, cy = H / 2 - ch / 2 + (1 - rise) * 40 * k;\n var r = Math.min(ch * 0.25, Math.max(14 * k, $.style.radius));\n var clip = $.uid(), frost = $.uid(), edge = $.uid();\n $.def('<clipPath id=\"' + clip + '\"><rect x=\"' + f2(cx) + '\" y=\"' + f2(cy) + '\" width=\"' + f2(cw) + '\" height=\"' + f2(ch) + '\" rx=\"' + f2(r) + '\"/></clipPath>');\n $.def('<filter id=\"' + frost + '\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\"><feGaussianBlur stdDeviation=\"' + f2(L * 0.14) + '\"/></filter>');\n var sweep = $.seg(t, tin + 0.4, tin + 1.6);\n $.def('<linearGradient id=\"' + edge + '\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"' + f2(Math.max(0, sweep - 0.25)) + '\" stop-color=\"#FFFFFF\" stop-opacity=\"0.15\"/><stop offset=\"' + f2(sweep) + '\" stop-color=\"#FFFFFF\" stop-opacity=\"0.85\"/><stop offset=\"' + f2(Math.min(1, sweep + 0.25)) + '\" stop-color=\"#FFFFFF\" stop-opacity=\"0.15\"/></linearGradient>');\n var glass = '<g clip-path=\"url(#' + clip + ')\"><rect x=\"0\" y=\"0\" width=\"' + W + '\" height=\"' + H + '\" fill=\"' + c.bg + '\"/><g filter=\"url(#' + frost + ')\">' + blobs + '</g><rect x=\"' + f2(cx) + '\" y=\"' + f2(cy) + '\" width=\"' + f2(cw) + '\" height=\"' + f2(ch) + '\" fill=\"#FFFFFF\" fill-opacity=\"0.16\"/></g>';\n glass += '<rect x=\"' + f2(cx) + '\" y=\"' + f2(cy) + '\" width=\"' + f2(cw) + '\" height=\"' + f2(ch) + '\" rx=\"' + f2(r) + '\" fill=\"none\" stroke=\"url(#' + edge + ')\" stroke-width=\"' + f2(2.5 * k) + '\"/>';\n var ty = cy + ch / 2 - (sub ? tf.size * 0.85 : tf.size * 0.56) + (kick ? tf.size * 0.35 : 0);\n if (kick) glass += $.chip(kick, W / 2, cy + tf.size * 0.35, { size: tf.size * 0.26, anchor: \"middle\", fill: c.accent, color: $.on(c.accent) });\n glass += $.text(title, Object.assign({}, tf, { x: W / 2, y: ty, anchor: \"middle\", fill: c.fg }));\n if (sub) glass += $.text(sub, Object.assign({}, sf, { x: W / 2, y: ty + tf.size * 1.15, anchor: \"middle\", fill: c.fg, op: 0.82 }));\n s += '<g opacity=\"' + f2(Math.min(1, rise * 1.4)) + '\">' + glass + \"</g>\";\n return s;\n}\n","libs":[]},{"id":"chromatic","name":"Chromatic split","description":"A title whose colour channels arrive apart and snap together, then split again on a hit: a lens-fringe look in the brand's two accents.","category":"text","tags":["effect","chromatic aberration","rgb split","glitch","title","text effect","fringe"],"placement":"overlay","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"title":{"type":"text","label":"Title","default":"Signal found","maxLength":24},"hits":{"type":"number","label":"Split again (times)","default":2,"min":0,"max":6,"step":1}},"beats":{"in":0.1},"pack":"effects","ref":"effects/chromatic","code":"// Effects: looks built from SVG filters (turbulence, displacement, blur, colour matrices), which resvg and\n// browsers both draw, so a render and the live player match. Every filter is a pure function of t.\n\n/** Fixed-precision number for markup. */\nfunction f2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A glow filter (a blur of the shape under the shape itself); returns its id. */\nfunction glowFilter($, std, strength) {\n var id = $.uid();\n var copies = \"\";\n for (var i = 0; i < (strength || 2); i++) copies += '<feMergeNode in=\"b\"/>';\n $.def('<filter id=\"' + id + '\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\"><feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"' + f2(std) + '\" result=\"b\"/><feMerge>' + copies + '<feMergeNode in=\"SourceGraphic\"/></feMerge></filter>');\n return id;\n}\n\n/** A title sized to fit the safe area's width, centred: { font, x, y, lines }. */\nfunction fitTitle($, str, share, maxSize) {\n var A = safeArea($);\n var f = fitFont($, str, \"display\", Math.min(maxSize, $.W * share), A.w * 0.9, { ls: -0.03 });\n return { font: f, x: $.W / 2, y: $.H / 2 - f.size * 0.56 };\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, D = $.dur;\n var tin = $.beat(\"in\", 0.1);\n var str = $.case(p.title);\n var T = fitTitle($, str, 0.14, 220 * k);\n var settle = $.E.out3($.seg(t, tin, tin + 0.6));\n var split = (1 - settle) * T.font.size * 0.35;\n // Hits: the channels jolt apart and back, spread evenly through the hold.\n var n = Math.max(0, Math.round(Number(p.hits) || 0));\n for (var i = 1; i <= n; i++) split += $.bump(t, tin + 0.8 + ((D - tin - 1.2) * i) / (n + 1), 0.04, 0.22) * T.font.size * 0.12;\n var op = $.fadeIn(t, tin, 0.15) * $.fadeOut(t, D, 0.3);\n var base = Object.assign({}, T.font, { y: T.y, anchor: \"middle\" });\n var s = $.text(str, Object.assign({}, base, { x: T.x - split, fill: c.accent, op: 0.9 * op }));\n s += $.text(str, Object.assign({}, base, { x: T.x + split, fill: c.accent2 || c.accent, op: 0.9 * op }));\n s += $.text(str, Object.assign({}, base, { x: T.x, fill: c.fg, op: op }));\n return s;\n}\n","libs":[]},{"id":"warp","name":"Liquid warp","description":"A title that settles out of a liquid ripple: rolling noise bends the letters, eases off until they read clean, then keeps a slow shimmer.","category":"text","tags":["effect","liquid","warp","distortion","ripple","title","text effect","heat haze"],"placement":"overlay","duration":{"default":4,"min":1.5,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"title":{"type":"text","label":"Title","default":"Fluid by design","maxLength":24},"shimmer":{"type":"boolean","label":"Keep a shimmer","default":true}},"beats":{"in":0.1},"pack":"effects","ref":"effects/warp","code":"// Effects: looks built from SVG filters (turbulence, displacement, blur, colour matrices), which resvg and\n// browsers both draw, so a render and the live player match. Every filter is a pure function of t.\n\n/** Fixed-precision number for markup. */\nfunction f2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A glow filter (a blur of the shape under the shape itself); returns its id. */\nfunction glowFilter($, std, strength) {\n var id = $.uid();\n var copies = \"\";\n for (var i = 0; i < (strength || 2); i++) copies += '<feMergeNode in=\"b\"/>';\n $.def('<filter id=\"' + id + '\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\"><feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"' + f2(std) + '\" result=\"b\"/><feMerge>' + copies + '<feMergeNode in=\"SourceGraphic\"/></feMerge></filter>');\n return id;\n}\n\n/** A title sized to fit the safe area's width, centred: { font, x, y, lines }. */\nfunction fitTitle($, str, share, maxSize) {\n var A = safeArea($);\n var f = fitFont($, str, \"display\", Math.min(maxSize, $.W * share), A.w * 0.9, { ls: -0.03 });\n return { font: f, x: $.W / 2, y: $.H / 2 - f.size * 0.56 };\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, D = $.dur;\n var tin = $.beat(\"in\", 0.1);\n var str = $.case(p.title);\n var T = fitTitle($, str, 0.13, 200 * k);\n var settle = $.E.out3($.seg(t, tin, tin + 1.1));\n var amp = (1 - settle) * T.font.size * 0.5 + (p.shimmer ? T.font.size * 0.025 : 0);\n var id = $.uid();\n // The noise drifts with time (its offset moves), so the ripple rolls rather than flickers.\n $.def('<filter id=\"' + id + '\" x=\"-20%\" y=\"-60%\" width=\"140%\" height=\"220%\"><feTurbulence type=\"turbulence\" baseFrequency=\"' + f2(0.012 / Math.max(0.5, k)) + \" \" + f2(0.045 / Math.max(0.5, k)) + '\" numOctaves=\"2\" seed=\"7\" result=\"n\"/><feOffset in=\"n\" dx=\"' + f2(t * 60 * k) + '\" dy=\"0\" result=\"m\"/><feDisplacementMap in=\"SourceGraphic\" in2=\"m\" scale=\"' + f2(amp) + '\" xChannelSelector=\"R\" yChannelSelector=\"G\"/></filter>');\n var op = $.fadeIn(t, tin, 0.3) * $.fadeOut(t, D, 0.3);\n return '<g filter=\"url(#' + id + ')\">' + $.text(str, Object.assign({}, T.font, { x: T.x, y: T.y, anchor: \"middle\", fill: c.fg, op: op })) + \"</g>\";\n}\n","libs":[]},{"id":"neon","name":"Neon sign","description":"A neon sign that flickers on: glass tubes of light in the accent with a glow around them, a buzz of flicker as it strikes, then a steady hum.","category":"title","tags":["effect","neon","glow","sign","flicker","night","title","retro"],"placement":"full","duration":{"default":4,"min":2,"max":20},"aspects":["16:9","9:16","1:1","4:5"],"props":{"title":{"type":"text","label":"Sign","default":"Open late","maxLength":20},"subtitle":{"type":"text","label":"Under it","default":"since 2026","maxLength":30,"description":"empty for none"}},"beats":{"on":0.3},"pack":"effects","ref":"effects/neon","code":"// Effects: looks built from SVG filters (turbulence, displacement, blur, colour matrices), which resvg and\n// browsers both draw, so a render and the live player match. Every filter is a pure function of t.\n\n/** Fixed-precision number for markup. */\nfunction f2(v) {\n return (Math.round(v * 100) / 100).toString();\n}\n\n/** A glow filter (a blur of the shape under the shape itself); returns its id. */\nfunction glowFilter($, std, strength) {\n var id = $.uid();\n var copies = \"\";\n for (var i = 0; i < (strength || 2); i++) copies += '<feMergeNode in=\"b\"/>';\n $.def('<filter id=\"' + id + '\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\"><feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"' + f2(std) + '\" result=\"b\"/><feMerge>' + copies + '<feMergeNode in=\"SourceGraphic\"/></feMerge></filter>');\n return id;\n}\n\n/** A title sized to fit the safe area's width, centred: { font, x, y, lines }. */\nfunction fitTitle($, str, share, maxSize) {\n var A = safeArea($);\n var f = fitFont($, str, \"display\", Math.min(maxSize, $.W * share), A.w * 0.9, { ls: -0.03 });\n return { font: f, x: $.W / 2, y: $.H / 2 - f.size * 0.56 };\n}\n\nfunction frame(t, $) {\n var p = $.data, c = $.c, k = $.k, W = $.W, H = $.H;\n var on = $.beat(\"on\", 0.3);\n var str = $.case(p.title), sub = p.subtitle ? $.case(p.subtitle) : \"\";\n var T = fitTitle($, str, 0.14, 210 * k);\n var tube = c.accent, tube2 = c.accent2 || c.accent;\n // Strikes on with a few hard flickers, then hums (a slight shimmer of brightness).\n var flick = [0, 0.06, 0.1, 0.16, 0.22, 0.3, 0.34];\n var lit = t < on ? 0 : 1;\n for (var i = 0; i < flick.length; i += 2) if (t >= on + flick[i] && t < on + flick[i + 1]) lit = 0.15;\n var hum = 0.93 + 0.07 * Math.sin(t * 47) * Math.sin(t * 13);\n var b = lit * (t > on + 0.4 ? hum : 1);\n var glow = glowFilter($, T.font.size * 0.12, 2);\n var dark = $.mix(c.ink || \"#000000\", \"#000000\", 0.35);\n var s = '<rect x=\"0\" y=\"0\" width=\"' + W + '\" height=\"' + H + '\" fill=\"' + dark + '\"/>';\n // The unlit tubes stay faintly visible; the lit ones glow.\n var base = Object.assign({}, T.font, { x: T.x, y: T.y - (sub ? T.font.size * 0.3 : 0), anchor: \"middle\", fill: \"none\", stroke: $.mix(dark, tube, 0.25), sw: T.font.size * 0.045 });\n s += $.text(str, base);\n if (b > 0) s += '<g filter=\"url(#' + glow + ')\" opacity=\"' + f2(b) + '\">' + $.text(str, Object.assign({}, base, { stroke: tube })) + $.text(str, Object.assign({}, base, { stroke: $.mix(tube, \"#FFFFFF\", 0.6), sw: T.font.size * 0.015 })) + \"</g>\";\n if (sub) {\n var sf = { size: T.font.size * 0.3, weight: 600, family: $.font(\"text\").family };\n var sb = $.seg(t, on + 0.5, on + 0.6) * hum;\n var sy = T.y + T.font.size * 1.0;\n s += $.text(sub, Object.assign({}, sf, { x: T.x, y: sy, anchor: \"middle\", fill: \"none\", stroke: $.mix(dark, tube2, 0.25), sw: sf.size * 0.05 }));\n if (sb > 0) s += '<g filter=\"url(#' + glow + ')\" opacity=\"' + f2(sb) + '\">' + $.text(sub, Object.assign({}, sf, { x: T.x, y: sy, anchor: \"middle\", fill: \"none\", stroke: tube2, sw: sf.size * 0.06 })) + \"</g>\";\n }\n return s;\n}\n","libs":[]}]}],"libs":{"world":"// ———————— Volito world ————————\nvar MM_WORLD = [[\"Afghanistan\",4,66.1,33.9,\"qh@kV_@FOEE@EGK?EIGGIB@DE@@NGF]Oa@@CDbAJHFEPHHAFDDR?GJJDFJ?JFDT?@DN?HJ@NXFLABDJCRB^IQQ@MNAF]IIHCMe@SDOACIYGCOQCAGOD\"],[\"Albania\",8,20,41.1,\"cLoX?BDB@DFHNM@GCSBIGIABEAGFATGD\"],[\"Algeria\",12,2.6,28.2,\"lDcP?[_@Oe@GEIWGAMIAIGYCCGDCF_@FMSIUC_@MaAEIBSGi@BBHCPDNJJAN]VMt@@x@FFKL?HGHICOHGJbAd@x@f@ZFT@@KRGDIpEgC\"],[\"Angola\",24,17.5,-12.3,\"cG~AFBBLB@DOMKGF uFxBUCy@?KXKPSEIBGOKAACI?@FW?CR?VEF?VYCG?AD?`@h@??~@YZb@Fn@ALIrA@JGLATF@KKm@IYQUAM?KN]EKHa@HKAC\"],[\"Antarctica\",10,31.2,-77.3,\"l]vo@g@C]BYHGJCP~@JnAFn@?XGCGi@EQEm@_@ lh@dq@wA@YOSFJRtAAVEJE dm@pk@c@AEIAWIIQAKFSVGPHNl@DX?IGl@DNE?GUG |~@|k@KCkAFUAKHzA?RCHG rkA`m@CGq@FWC\\\\JZARE pnA|l@MEs@L`AG heBbp@KGe@Ba@JEHd@@XEXM noB|s@QKc@F]G[F]Gy@C{@LqDNy@EuABo@DkBKCGnCENGt@CCIMMBIlAQ}AA]DcAKOGFGt@IrBEv@UDU[FaACOF_@AiAO]A?GFGEEYCKDu@Gu@AiAM]@[A[Bu@Cw@BqB?k@IYBm@I_@RSEWFq@Fu@EuAFIGVMVAJGJU{ADSDGF[?mAGSBYAQMOFUBYAOFiAFWOSF[Aa@JYAi@ImAGQCKEEIBGV]?GAGOOCEDQGGu@YGGWKS?cAWOAKBFFZHNCN@ZHHD@F?FID^Db@TBFQL_@H[\\\\EFAPMV@HVLXBTLjBPNFbC@EFs@HKDTD`@AZD@NWDCFWFk@@cBPaBFgAL]N{AQkAKq@?yAFKG[Es@A_ESTM?EtBBDGAOIC}@GaAS_CKcBSc@KEGTCGGMG}@MYQMCW?IFW??GIGU@EFW@q@EU?IFUEkBO[KOBSAYNWCGGSCY?GFOGcAAk@BUJyACaBOOEUQSBGDQBS?[JUEEGg@Ii@Eo@MQ@_@KQ?QCAGQEu@Ge@BOBAH[JW@]JS?a@Kw@Hg@?OT?B@Hb@JAFW?BFRNODW@UCQOWKOOiAGOOMGiAUoAAMCIMONQ@yA?_@Dy@ES@}@_@[Fg@Pe@@i@E]Ke@Cg@NU?e@HW@UA[KQAg@Bc@Ac@Bg@GoACEQKDCFMNQBoBCo@@MDBFKDk@HiAHU?MEq@No@BIFe@JqBB_AJMD@Fb@^X@JFXBd@TJL@PWTe@@GHfBFPHBHPLYFIFi@LgBPIHy@BQFw@CoAH\"],[\"Argentina\",32,-65.1,-35.2,\"zi@z_@OVYL[BFHR?HE^??m@ ~b@zQHb@?RDD@TYPBLMF@HPX\\\\Hf@BTACJBNCHJDTBPGFDARMDIGEJPDNJF\\\\P?NJDLSLSBFPVJJVPFFFETKJhAGFK?ML?DE@UOGEM@IIOEY@KIC@GHCEGFGDUICBYIe@KEDS?QMM?OKS?QDCFa@KU@QEQMSMKDGCE?a@UIES@EOQYDKLGOW?g@d@O@k@VCFR\\\\i@FOAQOCQICIJ?N^Rn@l@\"],[\"Armenia\",51,45,40.2,\"a\\\\gWF@FOF?^OAGBG[AKFBBIFDDQF?L\"],[\"Australia\",36,134.4,-25.6,\"i{AnXK@AVFD@NDELLPAJO@MJO?Ia@FYE ymAbSd@NHP\\\\@PCZBJHD?RJZA^O?KMEAUNk@?IFSFGBOLWIFFQOL?IN[GY@KGMALGMi@UM?e@M_@EY]AQMQGPICFIEIIBAMOQIC?EG@?CSEYTW@BKIOGC@EGKKEI@QC?INEKCWLOBEAKDWEIHLPD?AFLRADw@b@ODCDQDMEGSG[BYES@EG[GEEHAJC@AFEHARELMEON@FCPAHE@EP@HELo@Z@BKLGRGCGHCCCTg@b@CN?VINLdAHRLFLXPh@@RJFT?PFTNZKAIHBPLz@OJKFYHGRAEIBMHLPBKKAKGI@MNPJDFNNG?KTUCCz@Ub@@n@LRA\"],[\"Austria\",40,14.1,47.6,\"sIa]@FJ?CBHNP?HDj@GBERB@@^EAGEAIDAE_@AOB?OEAGIMDSGMDIAIB?H\"],[\"Azerbaijan\",31,47.7,40.3,\"_\\\\eYE@MJG?OKSVI@EBN@FVDB?HD?JIGIDE^N?MPGEEHGCCJGCCYDAAHIEC y[eWJCLOQBGN\"],[\"Bahamas\",44,-77.9,24.5,\"jp@wOW??BTB@G ro@{OOFBLBAAIHI zo@wNE?GP?HD@BKFECK\"],[\"Bangladesh\",50,90.3,23.8,\"}x@wL?LFCANJ]FKP?AFDJHC@@LCH]CKJECGKELKGKOFG?ALq@DHPF?DJIHCKC?K^\"],[\"Belarus\",112,28,53.5,\"sPcb@SDCBIASBAHBDSN@BMBEBFDR?IRP@DB?HhBKb@D?GDIKCDWQ?UGEIOG@Ga@K\"],[\"Belgium\",56,4.6,50.7,\"{Bw^BLB?@JPIH@VQF?BEq@GWL\"],[\"Belize\",84,-88.7,17.2,\"tv@cJACC@EKGBDb@HJD?Ae@\"],[\"Benin\",204,2.3,9.6,\"u@}BNBDMAm@BC@IJMAIEACGIAKKE?OHCRBFABNRBJ?j@\"],[\"Bhutan\",64,90.5,27.4,\"ix@kPGD@L^ALBPG?CMMIEYDGB\"],[\"Bolivia\",68,-64.6,-16.7,\"lj@zEW?EE[OYA@b@UPU@GFUHK?KDCXB?EVg@?BJAFKDCJHVAJDB?EPIt@BHPF`@@CV?FNJMXENPLBRq@GSJG@MHMKUFOCG@EGI?_@CEN_@\"],[\"Bosnia and Herz.\",70,17.8,44.2,\"sJsYPGVSNOEGEBCCI?o@DDHIF@FNF@J\"],[\"Botswana\",72,23.8,-22.1,\"kQxLZLPNJRJ@BNLBN?PGHBBHRLL@BEAKJSDA?{@S??eAm@GGFKGUCQ^UTG@ERO@KH\"],[\"Brazil\",76,-53.1,-10.8,\"j`@bTBKGIJMb@UF?ROJ@o@m@_@S?OHKHBEa@FCLBF_@LEFBRC?[DIEC@KIWBKJE@GCKf@?DWC?BYJEJ?TIFGTATQAc@X@ZNDDj@??]NJP?FKJ?CIJKFQEC?GKE@IEOi@SQ?I_ABKFE?MMAAGJA?Ke@?GEGPCCKJOAAEWGAGME?CPA@YFEC?]DECc@IGG@EI?CB@FEBEFDFBNGRKHI?WKY@?MQ?SFEEGDI?EGQa@G?Ol@IBALNNEDc@@?ROKy@RIHBJWEe@H]?]NYTMDQ?GFIb@F`@d@f@Zf@D@DLAd@Fj@DFBXRX@RPHBJT?\\\\FNFTDTNPR@LAJF\\\\LHTb@^VFRLJ\"],[\"Brunei\",96,114.9,4.7,\"egAkBBTF?BDHIYQ\"],[\"Bulgaria\",100,25.2,42.8,\"eMsZCFu@@_@I[HJFDLEJPARD?HP@LGNDL?@MHECKGEHG@GEC\"],[\"Burkina Faso\",854,-1.8,12.3,\"jBoE@KEG?EOOAMEEIBGCUUQGKACBM??PKL?FWD?LDDH@BFD@jA?AZLEH?DDTO\"],[\"Burundi\",108,29.9,-3.4,\"aRn@ERRTH??WDIK@EKK@\"],[\"Cambodia\",116,104.9,12.7,\"c_AsFDWMOYCQ@OFIMQFCL@V`@LGJR@PFNAP_@\"],[\"Cameroon\",120,12.6,5.7,\"aHaGGL?XKPX?BHIHIBGTP\\\\?\\\\QZID?X^I|AACOFKHCFKESGQEAKIGAKHKGIYKGOi@OOCIFG?EEA\"],[\"Canada\",124,-101.6,57.7,\"vkAs]v@[b@GJQCKXIBOVM?III?K`@M`@c@\\\\QJIf@P\\\\URER??{DyANi@M]@}@MMFMCEIM@_@P[MANWCGEW@mANo@@YHZHc@BgAESJUIRGKGi@Ac@NUAa@H}@CBKQC_@F?PKOO?ISl@SAUWMY@SH[TPHe@B?RYOWLDLSLSMMQAUy@BYHAHNJMH@Hd@LZ@TE\\\\b@VLZ@NF@JTBXNRTFN@V]@Q`@[Ce@Fa@Pm@Lu@B@NERMT]ROEIUH]LK_@GUMKM@MLQVMWUHQD]MEu@FQCi@PEFc@??PEVQBMJ]KSUKG_A`AFLk@T_@DMDGNMBGDAVZL`@DVP`@@j@Cp@@NLXFr@l@OC_@Yi@O]AQHPJKd@YHa@CSU?LMFVJ~@RVNLA@Qc@Ot@BLK?[FELBDCNLLVLB@DbA?^RDFd@?H@EJXHTBTHLES_@Ha@RIACFABGH@DIdA[PD\\\\ENBt@KDABKF??FfP? ns@af@OGY??BVHL?BE zp@ol@TIAGGAm@@c@J?DdA? dq@ye@GEKBFJFABG ny@{m@JFr@EII[CYJ ry@mo@n@ABCg@?MB@@ n{@gp@WFBD\\\\DNEHG?Gc@@ `v@om@tAGHURGh@ATGGGg@@UDg@?QDDFc@Hw@@_@CiA?SFEFLBZDXC`BB pdA{o@[BDDd@DZEOG[A jdAgp@YBv@B?CSEI@ va@a_@V\\\\MEKBDDQDGESFDLMCIVHNTACOBCVPJ?MIRCz@?@EKGFCOKS_@MKOEI?BD ls@ug@m@J?HOAMFPD\\\\EJGn@RFIX@OIIa@M?CHICKD fp@gl@SGgARAHe@ESLo@FQFQPb@Hm@J_@D]P]?DL`@VVG^SX@@Jw@XKRDLjASq@ZAFt@Gh@KVIEEx@S?Dx@BPGMMmACDEGIWSJO\\\\Ih@EMCTMP?NGHDb@BrCMNGSGZ?DSOQSGs@ENJOLQOs@Ia@TBJg@E `z@im@oAB\\\\PVBTNTAJO?IIISC xkAqn@kA[y@A@LNFvAJZC |qAw`@SAFTQNF?\\\\W@OIB |`Aqp@}AHUP~@Gl@ASEVC@G dlAi]H@`@GDGNEBETCDI?Es@HQNSFGJ |jAom@mABi@LhARXL?Fr@HHGl@Kg@c@RKcAA jbAkn@e@CCHJHbA@p@H\\\\?BEi@IxABZC[SQEw@Fc@Ja@?ZQQGS@OP paAul@UFQ`@eAR@F^@KFDDfAGz@FtADJIXEP@VOaBGd@CfA@HGm@G^?`@E]Uu@KSBHFm@EYHWIQFOPIGLSQAS@ v}@ml@TMUGWBc@CEDRH_@FBP`@FPALEp@O?Ei@@ raA_m@Y?OBPJ\\\\MEA p|@}n@OF?JHL^@TC?I^?@MU?]E[?AA ~z@kq@MESAFCm@AYJaAFOJWFZDb@LlA?TIAEOEd@?TEJIMG fx@er@{AG]GW@UDOK}@CgA?y@AkCBmAF?BrBPk@?nAP`@Nt@Fz@@[@LBOHp@JHFZDCBa@??Bt@Jr@Ex@BbAC@Ic@CHMMAs@HXM`@COGc@CEEZGFKeAB_@GrB?b@Gf@MBE ~m@ci@LDT?BIGIQCOD@J d{@mj@MFLDXEN@ZG_@Me@J hg@e^_@BUF?Bb@EPI ~f@k\\\\EHa@@HFF?VGDEGE\"],[\"Central African Rep.\",140,20.4,6.5,\"cPgB`@APFFCTHHAFL\\\\EZMRN?LZCJHHP@MHEP[?O?MOWOACEGDYGQM@GY?QIMUIGMAAFKJ@T_@V?DUREJMDCF\"],[\"Chad\",148,18.6,15.3,\"{MgKAnAPALZCBDDAFFNG?CPGD?DL@HFLTPHX?AFPLXFFEBDN@AEFUHCHICIY?JQ?YFMAGJA?KHGIYWQAYKq@NQB_@SI}CjA\"],[\"Chile\",152,-71.7,-37.3,\"zi@z_@?l@_@?DFNDx@Il@UZWgAXIIEMSGM@ nj@~IILALKFFRSp@MCADDRTH?`@BDEFLJLRDPAPJTG`@EB?PJR?NLL?PERJDHd@CXHBETGFDFIBAFHBAJDXHNAHDLNFATEDM??LGJaADP?XJBPF@VGl@WFMEKHK@a@GSUO\\\\EQOEa@WFIi@LEDXJCKaAIKFi@GAc@}AD]EO@WKYO{BD}@ICEI\"],[\"China\",156,103.9,36.6,\"mcAkJNE@QIGa@ECFFFDJNH eq@oY@IMEPa@q@KMc@g@DIIASO?OMGACLQH[FMNFVEFs@DWJKBIPIJ_ABYCSB]JW?IDUI_@G]?WG[OHOKK_@DSI]GMMMEm@?AEPMNGNFRCJBBGWc@WF[K?IQSIG?IHEOIWA[Am@L[h@GPa@FWLGP]?QG_@EPXDTLRVCPDEPBVH??HJKFJ\\\\FAHP?FELLd@VZDNFRDIIBGOKJIf@RJJR@HHKJOB?HOBUMQFM?AJZBHJPHHLSHGRY`@?NJBCJKDF^J@n@fAv@`@T@JHFEJFZHRBFTH?DMEGx@GFGAKPCHGNJb@@RFATH?BKLBVIEOJCBSTBAWSO?_@HCDK`@AGGHMLHNEh@ZN@FCXEHDLL@OJBl@E^KDIJCRKNEFBn@YBUM@?IFIAOTW^GDOPM@SJCF@BQEEBCSIMCSBGMYAEG_@ICE\"],[\"Colombia\",170,-73.1,3.9,\"xh@YBBFQFDd@??JK@@FL@?LGDCJH~@HIDAMSNGJ?FAHBNAJSZWD@PKD@NADGVK@EGA@IEEIAOUFECKBQCCBQFIAIG@CEDKACK@MMGACWKIK?ACO@_@WG?EDBDLBPTFXI@GL?RGFe@?MPa@CGBFP@LIVHHKJEP\"],[\"Congo\",178,15.1,-0.8,\"qJeA@JHVDh@@DTRFR?NZ\\\\FC@EH?DFLILJNSOKFKSGAIIHQ@GW@MHKIWDCL@DIAIW@_@HAKIQKI[B\"],[\"Costa Rica\",188,-84.2,10,\"pr@_EF@?FCBDLHCBEAGRIFIADBBJE@CAMBAEEMBI?IDCCEHQN\"],[\"Côte d'Ivoire\",384,-5.6,7.6,\"~CkEW@AEIACFKEUNEEI?MDCZFNBTGP@F`@CVBd@JAYROCG@IAEA??KIEHWAECA\"],[\"Croatia\",191,16.6,45,\"kIa\\\\SHQBEAKLFDFC^AH?BBDCDFg@b@QFB@n@SNMCAHG?GJADFDE?GM?CAM@?CGAAGOE\"],[\"Cuba\",192,-79,21.6,\"lr@oMa@@QDGFSAc@XQB@DO?MF@BLBv@?MIFEJ?FEBKJ?VGXCFCGCRALHF?BDP?WQ]G\"],[\"Cyprus\",196,33,34.9,\"mS}TY@RFHABGG?\"],[\"Czechia\",203,15.3,49.8,\"kH}^IDM@?DIBCEM@?FO?IHLBDFN@@B`@GRFb@QDOi@OM?\"],[\"Dem. Rep. Congo\",180,23.6,-2.8,\"iQxAEf@KJIVFA^DDLEFDj@QJECATNALQNCBIJDNAFIT??ENAXB?WDG?WBSV?AGH?@BJ@FNHCRDJQJYx@?TB@ECACMGCE@EGI?ADGB[]?OGSUSAEEi@IWAYSO[L]DGMI@UIMBCEGAa@@KNG@YEEFOL?VG@LJJRDd@DDDVEH?V\"],[\"Denmark\",208,9.3,56.2,\"eEka@JBNCFI?SGKQAWI@HBDADI@BFDANLEH wFab@EHJNTK@G]E\"],[\"Djibouti\",262,42.5,11.8,\"oYyFG?ECCD?FJDGBDJBCN?@IMQ\"],[\"Dominican Rep.\",214,-70.5,18.9,\"xk@gJ?EBECCAU_@DCDK?@DI?IFFFHCN?HBBCD@FLDC?C\"],[\"Ecuador\",218,-78.4,-1.5,\"bn@BCLDLTRVFLP@JJHFINA@EEE@GIMBIFHJICC@QECKY@GWKWJEFO@EAQJ\"],[\"Egypt\",818,29.8,26.5,\"aVwLlF??oCDOEMBGGIY?o@LWKQAMBEHCG_@DICK`@L`@DBNOL]@@c@hA_@j@B@?L[T\"],[\"El Salvador\",222,-88.9,13.7,\"zv@_HQJGCGBBLJA^IMM\"],[\"Eq. Guinea\",226,10.4,1.6,\"_Em@a@??Vb@@BCEU\"],[\"Eritrea\",232,38.7,15.4,\"wU_H@GKk@EEMAIKQh@e@Ze@b@DBF?\\\\_@PGL?BCJBJIDNVC\"],[\"Estonia\",233,25.8,58.6,\"oPed@ABLJEPFDN?XIPBAKD@LE@Ks@Gi@@\"],[\"eSwatini\",748,31.4,-26.5,\"aStOBHJ@JK?EEMO@EP\"],[\"Ethiopia\",231,39.6,8.7,\"{\\\\_Dv@z@X?PLL@BDL?FGPHDF\\\\CVOL?FG?IHCJUHCJQJAEKI?Ik@GEKWKIIc@WBEOKHKCCBM?QFMJORLPAHO?CBBDUZ_AVQ?\"],[\"Falkland Is.\",238,-59.4,-51.7,\"fe@j_@WIOBMGOFDFZDFGPHHI\"],[\"Fiji\",242,178,-17.8,\"inB|IECED@JL@HA@IGEG@ noB`ICA@HZH@G[I\"],[\"Finland\",246,26.2,64.5,\"{Pej@BL_@LPNUTLPQLFJ]LFHz@d@b@@bAJJKRECSFQGKQK{@Y@GZIDG@]x@WKEWJYASDSIIM]GYFFL\"],[\"Fr. S. Antarctic Lands\",260,69.5,-49.3,\"aj@j]MDQBABDF^@CW\"],[\"France\",250,2.3,46.6,\"h_@sAP`@DFH?FEDDFCIYFK@MIQUFUNAF {B}]ID[BHL@LDBHAABNJ?FIAEF?DEFFDELKBBFPJd@E\\\\D@JVBTIFBb@GFGIKCg@b@_@\\\\G@MYCa@DDWQHm@OEOQCCDG?WPIAUJEA mDsYMGCNFNFCDMCE\"],[\"Gabon\",266,11.7,-0.6,\"aFm@a@?ERMAEBHVIJALFVPAHI@HRFGJNJ`@e@JSMi@c@A?W\"],[\"Gambia\",270,-15.4,13.5,\"lIoGU?IEGDMCCDHBNCNFPAAG\"],[\"Georgia\",268,43.5,42.2,\"_XcZACm@F]JKASBEFKBDBIHJ@NE^DRIR@AIBK\\\\O\"],[\"Germany\",276,10.3,51.1,\"yGs`@EJDBEFEH@FGJL?h@NENUJFHD@?NNC^@@DHEZCBBN?AMIMZCHE?GBCCMBUK?CEEQBGCCO?CBMIDSOBKC?FSB?F_@I_@L\"],[\"Ghana\",288,-1.2,7.9,\"?{E?DGHA\\\\CFBPAHIR|@VPEAGFQCUGODw@y@?\"],[\"Greece\",300,22.6,39.3,\"mOaU@D\\\\@?CVCCGID_@?@BKA kMyXM?OEMFQA?IIBDLB@VAVBMJT@HIBBEJIHFBSN?HPCEHJ@GPL?NGJ]PYGIAEEC?Cg@I\"],[\"Greenland\",304,-41.5,74.8,\"f\\\\sr@cAKeA?YEeF?}BNd@FvD@IBeAA}@De@EOFTHsCM{@BKFzAR~@@m@@f@X?VWJ\\\\@`@De@HCPT?YPj@@WFFDv@BYL?Ff@GHD[BYJEN`@Bf@QEJVJqA@jB^pAFRFZRj@LbAHPL?LHJ^NGNRb@Z@\\\\Qf@?PILS`@WHMBQZQGMLGSW]EGGCOr@JVE@MGI{@Br@QR@NEUQr@i@XG?Gt@KnC?x@Q{AGrACl@GAGuCSGGt@EQGcAM]AFIkBG}@?UDs@IuBLn@IAG\"],[\"Guatemala\",320,-90.4,15.7,\"rx@aHAK@CIOW?AETQI??Ie@?@d@SBRJAFDDLLTCRK\"],[\"Guinea\",324,-11.1,10.4,\"pG{FWDSA?DIAOFUIMPBJE??DEDB@@DIVHD?JF?BFD?BCAGHKLB@SHMN?HBNPZY@IFEGISE?O\"],[\"Guinea-Bissau\",624,-15.1,12,\"lIwFWCc@??NRDFHJIF?JQ\"],[\"Guyana\",328,-59,4.8,\"hb@e@N?VJH?JIFSCOEGDGDCAGBCH?LOEC@IQEDG?GOKMFKL?HG?SNBPJD@LGNE?CHKP\"],[\"Haiti\",332,-72.7,18.9,\"xk@iK?PBBCD?Db@CFBJEAGa@DGEHG?GJACE]B\"],[\"Honduras\",340,-86.6,14.8,\"|r@kHXFHCPRDCBBH?AHJDBEDAAGBAJ@PKEE@GYOS@QCS?KBYN\"],[\"Hungary\",348,19.4,47.2,\"yLg]IBADJBTZZ@TFPCZQEKBCK?AGQDm@IKEUDCA\"],[\"Iceland\",352,-18.8,65.1,\"`Hqh@BLULXLjAPpAISGj@Ic@C?Eh@CKM_@A]L]KYD_@Ia@?\"],[\"India\",356,79.6,22.9,\"y{@uPAFDB?JLCXLAJJN?HHNNC?TBDAFHDJ_@B?BJHIEKG?IQp@E@MF?NGFJMJJDBFKDBJI\\\\@Fd@B?NHHZLVV`@X?Fd@LDNEh@FP?`@J@FNEDPDFJFDPQPo@N[He@P[L_ABk@ZJLAXWGGDETQKMk@?BOHI@OLGUUW@s@}@?MQKNIN]IG_@BUCQOUV@NGH?HLACTo@XJHFP_AZ[BKHw@D?]KCARQFMC_@@AMFEOAi@[ODMIILFFU@\"],[\"Indonesia\",360,114,-0.3,\"cwAr@?`CPORCBDX?GOMEDUHOd@QN?\\\\SDHF@BONIUEM?@E\\\\?FKPCFI[CIE_@FGf@UJOUUKQ?_@LUB cmApDAHLLND@CIQSG {rAhC@OGMCD?HHL uhAqAJPMP@HUPV@DL?PPL@RF\\\\@ETFFKLAHEVFFI\\\\A@YHEHO@QAQKMALMHMCK@KIIAQDOCI[GGGUg@B {oAv@UDENNIh@?CKY? inAjALCBGSACFDD }nAk@AJK@AF@NHA@LGHD@FKDYCOEG ykAQW?SMABNRLBRCp@BBLQPKGc@G?HHCFJPFSXBDQV?JJDFEIORFBEAGLKASLDAr@JBFEEQBSF?DMGMKm@CGQMODW@ ejAlEXMQCQJ@BF@ yjAhD_@G@J\\\\BZA?GOCKD }hAfDKACFn@DGIIAEEGD ybA~BAFe@@EGc@FGL]BWJTFTIf@?h@KT@b@GBIP?MSW@WF g`ATIVM@ILBV@^T?NQXOV[Xi@NQL_@PMHOb@a@@Ik@D}@|@U?OLKNMHFNSF\"],[\"Iran\",364,54.3,32.5,\"k]uQJK?ID?AMHOVILSEMIG@KLEVg@CEDUMEMNKBg@QEDFHKHE?ELQBMH]B]EACQCMKc@?UHO@WPM?APLd@IBHHG\\\\O@ALPPQTQFAPGBAFZJFTpAKFYHAb@JVEROREZk@H@LEDF\"],[\"Iraq\",368,43.8,33,\"oWcSFWk@SGW@O]SU@GDICKVMDAJHFDLMRWHIN@LE??HKJXCLRd@Av@g@\\\\MVE\"],[\"Ireland\",372,-8,53.2,\"zBu`@CLNPb@JZAOUHSi@WCHBH[B\"],[\"Israel\",376,35,31.5,\"iUmSBDDABJC@BB@BIA?FH^Ja@EEI_@MC@J\"],[\"Italy\",380,12.2,43.5,\"oEi\\\\M@AASCCD[D@HCFNAND?NEHQHINUNO?EDDBq@TABBFJINAFJMF@HH@HNF@CQCCLSb@WLA`@ULIDSXI\\\\LCGJCDMGEDG?EIBI?IGCBI?CGM@IC?G gHyVMADNADBHt@SCIk@? mDqXIEKL@XH?FDFE?WBKI@\"],[\"Jamaica\",388,-77.3,18.1,\"no@qJM@IBCDRBJCHEAEK?\"],[\"Japan\",392,136.9,36,\"uwAoWPR?TFNCHHLXFb@@ZTLE?Ob@BTHV?SNL^JHHGCQJEFMSEIKSKMKg@EUBSc@MHg@[MWBWGKUCIZ?N kyAoZMGCTZDPR^MHTT?BSIMUAKk@UT]H wqA}SIKKBGIMDCDJJFEHBDJJE?I\"],[\"Jordan\",400,36.8,31.2,\"eUgSCEUFg@UGVj@LSRHHN@JLTC@AI_@AY\"],[\"Kazakhstan\",398,67.3,48.2,\"su@w]NLN?@RHHf@ELb@p@JQ`@LDAHVIdA?ZGJB@J`@EJ@BFd@PHLD?DGXABOH?ASVMx@BPQr@Wr@JAhAH?LMLEVBFDCKBGVEFQJE?ES@?MQCO@CQBMR@NCh@JHCAIJKN?NMIOBCOUSJAMi@U]?cATUG_@AWJEE[?EI^MQGBESELMGEkAGICq@GQGc@BETSEYF@HS?q@SFDYNk@p@KIYJ]EIBIJMBGFYAKJ\"],[\"Kenya\",404,37.8,0.6,\"oW|AZS@KjAi@?UUc@Ha@HMYWIB?HGFM?WN]BEGQIGFM?PT?hAKNLFDFD@L`@FF\"],[\"Kosovo\",0,20.9,42.6,\"{KeY@EFGIKCASJBHRD\"],[\"Kuwait\",414,47.6,29.3,\"_]wQGZL@BIPAMSM@\"],[\"Kyrgyzstan\",417,74.6,41.5,\"kk@mYCGKAa@DAKKC[FeA?WHBD^HDFX@FLRC`@LCBDDd@@XER@AKSBGEO@YOVILDNGQMDA\"],[\"Laos\",418,103.7,18.4,\"cbA{GPGHLNGEGAQNO@SLONABDJ@DCRJ@QEUL?@KFEUSABI?@UICS`@W?GPJDDDWJ]h@MLELBR\"],[\"Latvia\",428,24.8,56.8,\"aP}b@IDGR`@JRIHA@Ct@@TD?OIKQGONO?COQCYHO?\"],[\"Lebanon\",422,35.9,33.9,\"kUySLBGOIMG?CFHFDH\"],[\"Lesotho\",426,28.2,-29.6,\"cQbQEDHNH@BDD@NMUSGEIF\"],[\"Liberia\",430,-9.4,6.4,\"fDyC?NBFSN@XXGn@g@EKQSGAIJ@FCBE?CGE?\"],[\"Libya\",434,18,27,\"sNwL?f@V??F|CkAb@PHI^IFKNIHBFI?IJMGG?_@CQFYIC@Q]SAMWDW@YFIPm@JUHSMDOEGOIKA[BEFa@DEDFHCFDLEN?nC\"],[\"Lithuania\",440,23.9,55.3,\"qOwa@AFNFDHTFP?DEHAAK\\\\EBOUEu@AABI@SH\"],[\"Luxembourg\",442,6,49.8,\"wBi^CB?FH?AKC?\"],[\"Macedonia\",807,21.7,41.6,\"_MmYIDALf@HFE@GEK_@C\"],[\"Madagascar\",450,46.7,-19.4,\"}]xFKTAVEF@HBDDKBDCN@FDB@Pl@hC`@LZKLk@?MAOIAKc@HU?QGWe@G[WEI@GG@IMAKEIEH\"],[\"Malawi\",454,34.2,-13.2,\"oSvDQBKNEXDNEXE?GDGLAXHBDLJKA_@FCD@TOEQEGBOGUDOFI\"],[\"Malaysia\",458,114.7,3.5,\"q}@aCCAOFAHKAEGQLGJAh@GDEVL@h@_@@IJK@QFIAMBI uhAqAf@CFTFFHZNBPEH@JHJALBLI@MMDOCCO_@GW[IHCEG?CUMMIOG?IHAF]J@FL?CHLF\"],[\"Mali\",466,-3.5,17.3,\"dFwF@MDEDWGCAKSDSCCCoA?CMBARmD_@?eCvAEHSFAJUA?l@JL@Jj@DFDX@BCJ@PFTTFBHCDD@LNN?DDFAJJDBGH@@DVADE?ED?CKLQTHNGH@?E\"],[\"Mauritania\",478,-10.3,20.2,\"tIcLEEmA?B]CISA@q@aA??]kAn@^?SlDC@BLnA?BBRBRE@JFBV[VKRBHAFF@KEICQD]AINQ\"],[\"Mexico\",484,-102.6,23.9,\"dhAiSo@C@BkAVw@??Ia@?]VIRKDODKOO?MF]d@IT]JGAJ^Bd@Mb@Yb@UBGH{@OKGIa@c@I[?EB?HJHBLC@FVDCF?DJBA@Bd@??HH?UP@DV?HNAB@J`@[NEd@HrA]TM^GFGTKHKBIEA@ECE?EL[n@o@PG@CAKTOBMJ?RURc@AG`@K@FCV[ZCFA?CJWTQd@?HI?OP?BJFDKd@Y@YVO@BPIJKI?GG?ILMJE^y@\"],[\"Moldova\",498,28.4,47.2,\"sOc]QEWFGD?DEBKPT??@LN@G?QVYDA\"],[\"Mongolia\",496,102.9,46.8,\"{u@y]SCcAYQDS?MHq@DQMFKUSUFi@DCN[Fk@ES@_@Rk@Bk@G]MUFWAVb@CFKCSBOGOFQL@Dl@?LDLL\\\\FRH^EJJINZNVF\\\\?^FTHHEV?\\\\KRCXB~@CHKHQJCVKr@EDGGWLOZGPIBM\"],[\"Montenegro\",499,19.3,42.8,\"qKsYD@@CFHADRKEMIEUJBD\"],[\"Morocco\",504,-8.4,29.9,\"j@_UGLG^EBBFXBHFH@@LVFDHd@F^N@`@J?DB`@?DND@HVZTDZJNj@@?IMO?EEMQOEUGMKEMSIGQAi@c@BYI[KKc@QSa@M?KHSA[B\"],[\"Mozambique\",508,35.5,-17.2,\"sTdFMAWDSAEEK?UGOKCHAbACHLZJJh@Pr@j@@LINCNC?BZCB@FHFf@PFDAFC@@HL?DWAULi@SWIOAgA^OP?@U{@OKHEAGB@^KJEMIC@YFMFED?DYEO\"],[\"Myanmar\",104,96.5,21,\"q}@wKTJL@HTFB]j@FPDBQXARGPRf@@OEMFKAUFIHq@FOb@VVEEYBQNWCEJCLO@OGB?MIE@GCE?UOBIO?IKO@KYMMB?KEC@GK?EJIB?^RN@VUCCRKBDNWHMCAFTR\"],[\"N. Cyprus\",0,33.6,35.3,\"mS}TCEO?QELHA@X?\"],[\"Namibia\",516,17.2,-22.1,\"mKnN?hAPHH?TCBGFEHHLOFMN_A@c@PWNe@NQ@OUGM@KFsAAMHo@@s@KUD\\\\LFGl@F?dAR??z@\"],[\"Nepal\",524,84,28.2,\"av@mP?\\\\P?d@EJIZC~@[GQSMo@TEH_@Jm@D\"],[\"Netherlands\",528,5.5,52.3,\"iCm`@CFDPBDJ?CTVMRBL?IEQ][GO?\"],[\"New Caledonia\",540,165.5,-21.3,\"sfBdLYTFBHEZSNSI?YR\"],[\"New Zealand\",554,170.5,-44,\"qmB`XPVNFHGIODKTGAGMEA[FQVYHMG?KHMDEPMRAMGDCN[FKGI@H\\\\NADDAH@B aiBfZ[UIMGEAIMIGNOGCF?H\\\\^GHN?NDP`@XLP?JETABEIOYS[I\"],[\"Nicaragua\",558,-85,12.8,\"hs@yEBBNGB@LC@@f@c@ACCBGAEE@II?CCEBQSIBYGBDAFDL?VFRCH\"],[\"Niger\",562,9.3,17.3,\"iHiMC^OPJp@@XVPHXIF?JK@FNB?LUB?NJXGP@JFH@VIHBH?FGRGRBDBFP@PNID?DD?MVE?GJM?QKAGEk@EAKKM?m@[Gy@g@cAe@_@HIHOG\"],[\"Nigeria\",566,8,9.5,\"u@}B?k@CKOS@CCGBKAYMUSCSFGFI?ICWHIAKGQAYFOKC?MTKFBHNNNh@JFHXJFJIF@JHD@L^b@JNAPYLM^?\"],[\"North Korea\",408,127.2,40.1,\"spAoYCBFALL?LTLTF@JSJBBT@FHH@HCFB@CHCIK@AEKTIOMUIMMGDQ?@I]GGKKJ\"],[\"Norway\",578,14.2,64.5,\"mHyp@GE]AyATp@FHNPBHNV?h@KQE\\\\Ef@QNOw@EID[? mRoj@p@HGMXG\\\\FHLRHREX@VKVD@Ld@CDJP?~@~@EFDFRALRAXKHDVXVJKf@VZBXIFUDm@QKu@Qe@SuAaAwAg@k@I_@@]Oe@?c@C}@LXDUJ cPaq@\\\\Jx@@x@CBCZATG}@E[BSEyAJ mNuo@j@Hb@EMCJGi@CGF[B\"],[\"Oman\",512,56.1,20.6,\"oa@eMEWICBCAKI?GLKDYDMPGB?BXd@HABD@XH?JDDLJ?FD?FHDJATDTm@{@SMg@HM eb@aOBGEEA@BJ\"],[\"Pakistan\",586,69.4,30,\"so@eUPNTB^CHFO\\\\OHPJ?Lr@|@VATTMFANIHCNj@?JLNCDOLM`BDGU[K@GFC@QPGPU_@HSCKBCEM@YGAOIKO?AEU?GE?KGKKEFKS?EE@GIIDQIGcAKMFEN_@F\"],[\"Palestine\",275,35.3,31.9,\"cUuRH@ACCCBACKE@@P\"],[\"Panama\",591,-80.1,8.5,\"jo@mD@BEJBDFA@HDEBKCCPMH@BDJD@BIJPDBK@BDABGTC?BBAEMBC?GGAEF?BI?EB]IEEK?QFMH\"],[\"Papua New Guinea\",598,144.3,-6.6,\"cwAr@gAXWRCJ_@JEHP@CLQJKTK??FMBDBUF@DL@BEd@EXWJOXIZJANND^C?aC k~AhAGDAHDDFS`@UCCYLGH q}ArBTHH?ZKAE[@EIAHK?OM@KKACB?JDJH@BB w_BhBWV@DD@FGHKBOCAAD\"],[\"Paraguay\",600,-58.4,-23.2,\"jc@rKEH?ZSBGCMDG^MCGBHr@PNN@h@GS]BGj@WNAd@a@Ga@IQu@CQH?D\"],[\"Peru\",604,-74.4,-9.2,\"tj@tAP?h@RDNAHJD?FDBGPKJBHK?GJQ?OK?\\\\G@KAO^BD?^FHADBFGNP^HBRI@Gd@Sr@c@FOAEdA}B\\\\UGGHSEMOKAFDDADO@GHKIAKMQWGUSEMBMEA[VKRO@ICG@K?OFLRE@IH\"],[\"Philippines\",608,121.5,15.8,\"ojA}FHOQ@EDBPHI skAgECEAKK?BJOQ@PRXJMCE _nAgDAVDPFSFHENDFVKDMEIJGDFHALJBEGOWMEHOECIM?@OOHCP aiAyDXPc@g@ISCNVX mkAkJ@FELBNJFBNCNS?YH@JEB@HNIFKBFLKPBHEAGECDE@FLS?SGFA_@EQK?KDEEAD gkAcF@IKDK??FTN?S mmAsFEVNEEPHD?MDABKK@?GJOQ@ED\"],[\"Poland\",616,19.3,52.1,\"uMu`@EVJBOb@@DH@PNEHVIV@JBHGFBHKN??GLABDHC?ELAHEFKAGDIDGECDKeAUSBADoA@I@ED\"],[\"Portugal\",620,-8.1,39.6,\"rDeYMGEHYAEHHDBZF@GJDJGDHL?DFDHCH@A[HABEAMGGCSBU\"],[\"Puerto Rico\",630,-66.5,18.2,\"lh@qJMDBBZ@?IAAO?\"],[\"Qatar\",634,51.2,25.3,\"w^oN@MEIEAED?JBJD?DC\"],[\"Romania\",642,24.9,45.9,\"sPm[[B?DHBDABVZI^Ht@AFKCCBAFBHE@GJC@EJGOCU[SGY@IDa@IE@WX?PAF\"],[\"Russia\",643,99.9,61.7,\"sy@sq@i@Eg@Jm@PDPh@@v@C`@GNMXCs@M g_Aqp@s@JDFpBFe@Y]A wuAqn@u@?gAHNNhA?^Bf@MIKYC s{Aan@q@BTF^Ad@GEEc@B mvA{l@QGYA[FADhAC _[kq@eACCD]G]BlALTCKGl@? eM}`@z@ACI[E]D@J m`@cm@o@ODIoBWaAAa@Gg@AMFLDbCP|@R|@d@CPe@NjAADGb@E@ISC@Kg@OPC ixAq`@GR@P]v@\\\\EJZQT?LNKJLBO?i@CMA[JQA[QIFGICCL wpAkYBMI?CWDQQEWBMSEUQY^DPF\\\\?FQVM`@GFQZi@NG\\\\Er@BNHID?HHFPR?HZJVGV@TG\\\\Lj@Fj@C^SRAj@DZGBOh@ETGTRGJPLp@ELIR?PEbAXZDJKX@FGLCHKHC\\\\DXKJHj@q@XOGEp@RR?AIXGRDDUb@CPFp@FHBjAFFDMLRDCDPF_@LDHZ?DDVK^@TFbAU\\\\?h@T@LRKNTCBHNOLO?KJ@HIBHJPBRRQP@LUVNJF?LKPEDGRCJ@\\\\Kl@G@BXQVGNIMCOOHG[G?EPBAGIEQACEBKGI?Eh@EJINBVGFKNA@EECJIf@BHSS?GEDCLCACROCE@IRCH@BCREFSHEGEDQMK@CUIRI{@e@GI\\\\MGKPMMQTUQO^MCMeAOa@Ju@BkAVOF?LTH^DxAOLB_@LA\\\\i@HAGJIMEo@JOEJMm@SQ@SDIMNKIKLKw@FIHV@?JMD_@CCKoB[O@RJW@MEe@A]GUJWKTKKGy@FcBZMIRI@EVAGGJO?Ec@QMQMCu@BCJRNMDELBZUJHLd@ZU@GEUEEIQIJIGMTABIOSXOc@MBMIAIJFRU@HMa@Gi@?c@JPQ@Sa@E{A?NKWKU?g@Is@CECu@AOBk@Ie@?CISGo@Ga@DZDm@@CHSCy@?k@HOFBHxATu@FQCILIE_@C_ABCHsA@AMiA@_@JIJJFWN_@FSS_@Fa@Ee@FMEa@@NQ[GsDJQJs@NoACg@@OF@NWD[CgA@e@Ac@PWENMIG}@Di@Ay@HmAVs@R@JMDDOu@Bg@PRH^@@PFBP?h@KBIRAV@HECGVBIHJFXHXAQHKNIDAFDDb@ChANz@ZFFZMr@NHGPHZCDLVRAFUB@ZR@FNGF`@JFT\\\\BDTZPFMRiAI[QK?G_@EeAk@c@OO]V@JPr@TNYr@Fp@^OLhAD?M\\\\CXHz@A`ADlCtA_@@IJSBKIU@[TAPNR@VF^\\\\ZFL`Ax@ZJJ?JIZL@F noBuk@SA[DTFn@@BEYG {Sw[ECSBOJSABFTBXHJCCGREUKBA\"],[\"Rwanda\",646,29.9,-2,\"_RTGJ?JP?DJJAEWEEC@KE\"],[\"S. Sudan\",728,30.2,7.3,\"gReANMDGXDFANWLEDKTS?EVOKEK[MAMNE@EAM?CBS??CICAEGCOJKASY@KBEK??CI?@NALIFE^BDH?DJK@IHAFIBKTd@`@\\\\BLCFD\"],[\"Saudi Arabia\",682,44.5,24.1,\"{TkQUBKMOAIIRSk@MWD]Lw@f@w@BCHMAGP[R?TKNEBE?K^{@HCCILLf@z@Rx@FPFLTHBDG\\\\AZ@FEDHAFFFHWHEJQDOLMFALS?YJURKJYb@q@H?GY\"],[\"Senegal\",686,-14.5,14.4,\"lIoGFOHEGCU_@I@SCWJWZEVEDALR@LEl@?VB@OQ@OGOBICBELBFEHDT?\"],[\"Serbia\",688,20.8,44.2,\"wJu[OEK@KFADKBAFIDGCC@BBCBDBAFIFFDBDA@PDCIRKHHTMEAAGHGEIF?GEJM\"],[\"Sierra Leone\",694,-11.8,8.5,\"fGqDOQICO?ILAREAPRDJRIHIDU\"],[\"Slovakia\",703,19.5,48.7,\"cMu]LNTEJDl@HPE@GCEOAMKMAIFKCWASF\"],[\"Slovenia\",705,14.9,46.1,\"sGa\\\\O@_@IGFND@FF@?BLAB@L?CABGAI\"],[\"Solomon Is.\",90,159.1,-7.9,\"idBpEEDL?FKOD adB~DBBLQBKE?OX qcBdERADCAGMBIH wbB~CEH`@UCAWL eaBjCGDJAFG?CKF\"],[\"Somalia\",706,45.7,4.8,\"_Y`@JO?iAU[MAQMY?mAyA?e@k@K@ZFZf@lAf@n@dAr@\\\\f@\"],[\"Somaliland\",0,46.2,9.8,\"q]cF?d@T\\\\P?~@WT[IQSTq@Gm@K\"],[\"South Africa\",710,25.1,-29,\"eIzPIIGDCFUBI?QI?iAE@KR@JCDMASMCIICQFO?MCCOKAKSQO[Mc@BMh@@ZNADL?DKJKACIM?Jh@PJV`@d@`@LF^H@BJAHBTCR@d@HJFH?FEDAHG?@@OFMEC?Md@}@ cQbQHGFDTROLEACEIAIODE\"],[\"South Korea\",410,127.8,36.4,\"{mAqVIAGIUACCOVEJ?VFHPBNFP@@ICOHSOCLO\"],[\"Spain\",724,-3.6,40.3,\"tCeV?EIMFEEKFKGAC[IEDIX@DILF?MFG[MiADo@?GFc@FGCUHWC?JPLXBT`@GJJFBLLBLLl@?RNHAJQRC\"],[\"Sri Lanka\",144,80.7,7.7,\"cr@uCBRXHFOB[G_@MHSb@\"],[\"Sudan\",729,29.9,16,\"kNcDTOAUJK@MFEBQF?GO@GEEBCM[Q@@wAW??g@mF?ERBBARGVQJHJL@DDJj@AF@NFRJHJVFDDR?PD_@HG@MAOH??BJ?CDAJRXJ@NKFB@DHB?BR?BCL?D@DALOL@JZJDMF\"],[\"Suriname\",740,-55.9,4.1,\"`a@m@JCP??LHAJQBID?FOAMKECQWBACO?SBHPALGJHX\"],[\"Sweden\",752,16.6,62.8,\"{Eyc@YWEWJI@YMSS@EGDG_A_AQ?EKe@BAMK?y@VA\\\\EF`@DRLCJfA`@LZMLSHPTTDF`@HPVAJNV?DQ^s@\"],[\"Switzerland\",756,8.1,46.8,\"_Eu\\\\@FQB?FHBLABFH?BCHFH?NKH@?GOK@C_@ACCUD\"],[\"Syria\",760,38.5,35,\"iUmSGUIGBGF?@OOQ?IGBUEYBWG_@AHJHBANFVrAh@TG\"],[\"Taiwan\",158,121,23.7,\"ckAgNTn@HO@OKQOOIDBJ\"],[\"Tajikistan\",762,71,38.6,\"ki@eVKUBMNCEIO@OW[EBHADG?FDRC@JSAYDe@ACPGAKBAR`@A\\\\NFGAODAAEHCFFDHJ?DFDANDDA\"],[\"Tanzania\",834,34.8,-6.3,\"eTRkAh@AJ[RHVAJKDB`@O^EBNJz@NVEL@DYJOPCh@QHWJKDg@I?SUDSEA?KFKeAA\"],[\"Thailand\",764,101,15,\"w`A}GPAXBLNEVPGP?COP?@TNl@?NM?Kb@IHKBIHDFJ@@INGB@FEBIRUBJBII_@Sg@FQ@SPYECGQ\\\\k@GCIUMAUKGDAJM?DTAPSKEBKACEO@MNARON@PDF\"],[\"Timor-Leste\",626,126,-8.8,\"cmApDACOEUAE@j@R@I\"],[\"Togo\",768,1,8.4,\"Q{E@HKLAHCB@l@ELNBHS@ICQBG@]FI?EQ?\"],[\"Trinidad and Tobago\",780,-61.3,10.4,\"pe@wEOA?NT?EE?G\"],[\"Tunisia\",788,9.5,34.2,\"}D}QFc@\\\\W@OKKEOBQCIUIMB?HOGABHH?HEB@PJHAHK?CHGB@L\\\\RAPHB\"],[\"Turkey\",792,35.4,39,\"_[gVHBFETAh@DVFXCTDFC?HJHDIGGJ@PCLJ\\\\@NITABFLBRKT?JSLIIOJIUQ]AGMe@@WKWEa@?}@Ti@AWISASHCF@FUJLDETBDKN iOcYSEQ@AFQDBBV@VNDKCAEMHC\"],[\"Turkmenistan\",795,59.3,39.1,\"y_@cYGEWCMDML_@?BIa@S[JANGBSAGBIRc@Vm@P?JNE@FPBBNXFBHN@RE@QL?VQNATIb@?LJPBBMCUNEEML?CQSDOGLKDINB@NDM\"],[\"Uganda\",800,32.4,1.3,\"eTR|@?RFBAEe@KSMKFA?WGEMB]CKIILI`@Tb@?T\"],[\"Ukraine\",804,31.2,49.1,\"{Rq_@g@CKHDBADO@GJWFOCKHi@D?DFHCJBDP@HD@FN@JDR@ND?JXIDB`@E?GR@TXFCHBHCMO?AU?JQDC?EFEVGr@NHEXAFB@EHCIMC@DIQOIAAEHQc@EiBJ?IECQA\"],[\"United Arab Emirates\",784,54.2,23.9,\"g_@cNCA?DOC[@i@g@CFANH?@JCBHB?FHRz@IFW\"],[\"United Kingdom\",826,-2.7,53.9,\"zBu`@ZCCIBIQASJHL |@k`@CKLKVCDEGGDEJH?SJIGSOOg@?TRi@ABNPPS@SXM@Q^WB@JJDGHPHX?`@DHCJHPALDJC]SQC^CDGUEJICM]@\"],[\"United States of America\",840,-99.1,39.5,\"vkAs]gP??GG?CJE@u@JOC]DQEeAZEHIACFG@@BSHI`@R\\\\GFkAWDKIAe@?EG_@ScA?AEMCMWOMEBMCGD?ZOP|@TLN?JEHG?@EEB@Dx@FND]CEBZDL??CDDE@BLLN@EHEGL?FNVCMHGBO@FCJLAMD?PE@EXLNRDLJH?HF@DTLRTBLCLO\\\\?HGT?TDJD@HA@GFEVg@CMRYFCRHXOf@?VBGPB@FAFBL?LKP@LC\\\\Dn@`@DF?LENF@\\\\KHU\\\\e@LGN?JNNEJEHS\\\\W`@??Hv@?jAWACn@BBIJKHA@Eh@K@KRSN[?CVWBOHICO?QDOGQCc@B[JYAC]FIPEEH_@ b`BqKKJPJBCBKCKIB n`B_LFBDEM@ vaBqLEFF?BEEA rbB{L@DDCGA `gBwd@O@AFJ@XGSC v~Agc@K@IDd@PHEBI]I bwAqj@?zDS?SD]Tg@QKH]Pa@b@a@L?JHH\\\\MBQXOJQp@AVEh@St@KZ@|@QTBCLhANBIISUEDEXJLJZLMHPNh@LDF\\\\HDHVFL?v@P`@D@C}@YWAIG[KSKAMKKVDDCJFJIDDFIRFJ?@KCEJGXB\\\\M@KLGGKOIEI]?OGO?MEBGHCMG`@BDDLE\\\\BZEFGXKgAQO?@Hi@?NKVG`@QZGKIc@?WIEISGy@KQ@]K]DOFGCa@@@B_@BSA_BHYCs@H hjB{f@{@HNDVGP@BAAG\"],[\"Uruguay\",858,-56,-32.8,\"~b@zQKASNG?c@TKLFHCJFJTJNCH@PIL@JKOiA\"],[\"Uzbekistan\",860,63.2,41.7,\"_b@yX@iAs@Ks@VQPy@CWL@RI?CNY@EFE?IMe@QE@PLOFMEWHXNVA@ECIZDNVNADHOBCLJTXE?Kl@Qb@WHSFCR@FC@OZK`@RCHT?\"],[\"Vanuatu\",548,166.9,-15.2,\"ogB|HKJD@DG?E ggBxHBUIDCNH?\"],[\"Venezuela\",862,-66.2,7.2,\"|d@gBADFFb@HDB\\\\EB?GDAXQ@?BLD@FVF@DN@JKDQJKIIHWAMGQFC`@BLQd@?FG?SFMHAGYQUMC@DH@EF?JHHGNG?EOFE?OWG@GGGELM@MH?Dg@?IFO@KE?Co@ANDEHO?OHANI?GBNJ?FEFPDAHDBMN\"],[\"Vietnam\",704,106.3,16.7,\"e`AqEQGSAFKa@MAWBMCSDMLM\\\\i@VKEEKEFQV?Ra@ICc@AOKIFQB@JGFSBXPNPBLa@n@QJKNGd@@`@f@Xf@b@FKEMNK\"],[\"W. Sahara\",732,-12.1,24.3,\"lDiP?b@`A?Ap@R@BHC\\\\lA?DDAGk@AKOE[[UIWEAEOa@?ECK??KA?\"],[\"Yemen\",887,47.5,15.9,\"o_@{JUl@LDBNr@PPLN?HFb@DJJ\\\\@DKAKL[CA?SGG@GEIGD[A]@EFICMUQGy@G\"],[\"Zambia\",894,27.7,-13.4,\"eRdDi@PGHENFTCNDFDPIDz@NALN@JF@FD@\\\\Z`@CJELANBX[?_Ai@?@g@G@?DU?GHO@KECHOBMPO@@UDBPKEk@DGEMEAa@A\"],[\"Zimbabwe\",716,29.8,-18.9,\"oR|Lb@CJINADSFATUP_@a@B][EAAGKGOA?FQ?_@N@fAHNRV\"]];\nvar MM_WORLD_ALIASES = {\"us\":\"United States of America\",\"usa\":\"United States of America\",\"united states\":\"United States of America\",\"america\":\"United States of America\",\"uk\":\"United Kingdom\",\"great britain\":\"United Kingdom\",\"britain\":\"United Kingdom\",\"england\":\"United Kingdom\",\"uae\":\"United Arab Emirates\",\"south korea\":\"South Korea\",\"korea\":\"South Korea\",\"north korea\":\"North Korea\",\"drc\":\"Dem. Rep. Congo\",\"dr congo\":\"Dem. Rep. Congo\",\"democratic republic of the congo\":\"Dem. Rep. Congo\",\"republic of the congo\":\"Congo\",\"czech republic\":\"Czechia\",\"dominican republic\":\"Dominican Rep.\",\"ivory coast\":\"Côte d'Ivoire\",\"cote d'ivoire\":\"Côte d'Ivoire\",\"eswatini\":\"eSwatini\",\"swaziland\":\"eSwatini\",\"east timor\":\"Timor-Leste\",\"north macedonia\":\"Macedonia\",\"bosnia\":\"Bosnia and Herz.\",\"bosnia and herzegovina\":\"Bosnia and Herz.\",\"western sahara\":\"W. Sahara\",\"central african republic\":\"Central African Rep.\",\"south sudan\":\"S. Sudan\",\"equatorial guinea\":\"Eq. Guinea\",\"solomon islands\":\"Solomon Is.\",\"falkland islands\":\"Falkland Is.\",\"holland\":\"Netherlands\",\"burma\":\"Myanmar\",\"persia\":\"Iran\",\"viet nam\":\"Vietnam\",\"lao pdr\":\"Laos\"};\nvar MM_WORLD_CACHE = null;\nfunction mmWorldDecode(s) {\n var pts = [], i = 0, x = 0, y = 0;\n while (i < s.length) {\n var v = 0, sh = 0, b;\n do { b = s.charCodeAt(i++) - 63; v |= (b & 31) << sh; sh += 5; } while (b >= 32);\n x += v & 1 ? ~(v >> 1) : v >> 1;\n v = 0; sh = 0;\n do { b = s.charCodeAt(i++) - 63; v |= (b & 31) << sh; sh += 5; } while (b >= 32);\n y += v & 1 ? ~(v >> 1) : v >> 1;\n pts.push([x / 10, y / 10]);\n }\n return pts;\n}\n/** Every country: {name, id (ISO 3166 numeric), c: [lon, lat] label point, rings: [[[lon, lat], …], …]}. */\nfunction worldShapes() {\n if (!MM_WORLD_CACHE) MM_WORLD_CACHE = MM_WORLD.map(function (r) { return { name: r[0], id: r[1], c: [r[2], r[3]], rings: r[4].split(\" \").map(mmWorldDecode) }; });\n return MM_WORLD_CACHE;\n}\n/** A country by name (\"India\"), a common short name (\"UK\", \"USA\") or ISO numeric id (356). */\nfunction worldFind(q) {\n var all = worldShapes(), s = String(q == null ? \"\" : q).trim().toLowerCase();\n if (!s) return null;\n var target = MM_WORLD_ALIASES[s] ? MM_WORLD_ALIASES[s].toLowerCase() : s;\n for (var i = 0; i < all.length; i++) if (all[i].name.toLowerCase() === target || String(all[i].id) === s) return all[i];\n for (var j = 0; j < all.length; j++) if (all[j].name.toLowerCase().indexOf(target) === 0) return all[j];\n return null;\n}\n/** Equal Earth: [lon, lat] degrees → unit coordinates (x ±2.71, y ±1.32, y up). */\nfunction mmEqualEarth(lon, lat) {\n var A1 = 1.340264, A2 = -0.081106, A3 = 0.000893, A4 = 0.003796, l = lon * Math.PI / 180, p = lat * Math.PI / 180;\n var th = Math.asin(Math.sqrt(3) / 2 * Math.sin(p)), t2 = th * th, t6 = t2 * t2 * t2;\n return [2 * Math.sqrt(3) * l * Math.cos(th) / (3 * (9 * A4 * t6 * t2 + 7 * A3 * t6 + 3 * A2 * t2 + A1)), A4 * t6 * t2 * th + A3 * t6 * th + A2 * t2 * th + A1 * th];\n}\n/**\n * A flat world map fitted into a box: o = {x, y, w, h, center: [lon, lat], zoom (1 = whole world),\n * antarctica (false)}. Returns project(lon, lat) → [x, y], path(country or name) → SVG path data,\n * outline (the map's edge as path data, to clip or fill the sea), clipId(), and the countries.\n */\nfunction worldMap($, o) {\n o = o || {};\n var bx = o.x || 0, by = o.y || 0, bw = o.w || $.W, bh = o.h || $.H;\n var center = o.center || [0, 0], zoom = o.zoom || 1;\n var fit = Math.min(bw / (2 * 2.7066), bh / (2 * 1.3173)) * zoom;\n var cy0 = mmEqualEarth(0, center[1])[1];\n function rotLon(lon) { var l = lon - center[0]; while (l > 180) l -= 360; while (l < -180) l += 360; return l; }\n function P(lon, lat) { var u = mmEqualEarth(lon, lat); return [bx + bw / 2 + u[0] * fit, by + bh / 2 - (u[1] - cy0) * fit]; }\n var api = { countries: worldShapes().filter(function (c) { return o.antarctica || c.name !== \"Antarctica\"; }) };\n api.project = function (lon, lat) { return P(rotLon(lon), lat); };\n api.ring = function (ring) {\n var out = [], prev = null;\n for (var i = 0; i < ring.length; i++) {\n var l = rotLon(ring[i][0]);\n if (prev !== null) { while (l - prev > 180) l -= 360; while (prev - l > 180) l += 360; }\n prev = l;\n out.push(P(l, ring[i][1]));\n }\n return out;\n };\n api.path = function (c) {\n if (typeof c === \"string\" || typeof c === \"number\") c = worldFind(c);\n if (!c) return \"\";\n return c.rings.map(function (r) { return \"M\" + api.ring(r).map(function (p) { return $.fx(p[0]) + \" \" + $.fx(p[1]); }).join(\"L\") + \"Z\"; }).join(\"\");\n };\n var edge = [];\n for (var lat = -90; lat <= 90; lat += 3) edge.push(P(180, lat));\n for (var lat2 = 90; lat2 >= -90; lat2 -= 3) edge.push(P(-180, lat2));\n api.outline = \"M\" + edge.map(function (p) { return $.fx(p[0]) + \" \" + $.fx(p[1]); }).join(\"L\") + \"Z\";\n var clip = null;\n api.clipId = function () { if (!clip) { clip = $.uid(\"mmw\"); $.def('<clipPath id=\"' + clip + '\"><path d=\"' + api.outline + '\"/></clipPath>'); } return clip; };\n return api;\n}\n/** [lon, lat] on a sphere of radius r turned by spin (deg) and tilted by tilt: a 3D point for scene3d. */\nfunction worldOnSphere(lon, lat, r, spin, tilt) {\n var l = (lon + (spin || 0)) * Math.PI / 180, p = lat * Math.PI / 180;\n var x = r * Math.cos(p) * Math.sin(l), y = -r * Math.sin(p), z = -r * Math.cos(p) * Math.cos(l);\n var t = (tilt || 0) * Math.PI / 180, c = Math.cos(t), s = Math.sin(t);\n return [x, y * c - z * s, y * s + z * c];\n}\n/**\n * Countries on a globe in a scene3d: o = {r, center [x, y, z], spin, tilt, color, colorOf(country) → colour,\n * lift (how far above the sea, 0.004)}. Faces beyond the horizon are clipped away.\n */\nfunction worldGlobe(S, o) {\n o = o || {};\n var r = o.r || 300, C = o.center || [0, 0, 0], lift = 1 + (o.lift != null ? o.lift : 0.004);\n var view = [S.camPos[0] - C[0], S.camPos[1] - C[1], S.camPos[2] - C[2]];\n var vl = Math.sqrt(view[0] * view[0] + view[1] * view[1] + view[2] * view[2]) || 1;\n var vn = [view[0] / vl, view[1] / vl, view[2] / vl];\n // The visible cap ends where the line from the camera grazes the sphere, a little short of the centre plane.\n var cut = (r * r) / vl;\n var plane = [C[0] + vn[0] * cut, C[1] + vn[1] * cut, C[2] + vn[2] * cut];\n var faces = [];\n worldShapes().forEach(function (c) {\n if (c.name === \"Antarctica\" && !o.antarctica) return;\n var col = o.colorOf ? o.colorOf(c) : o.color || \"#3a4150\";\n if (!col) return;\n c.rings.forEach(function (ring) {\n var pts = ring.map(function (p) { var q = worldOnSphere(p[0], p[1], r * lift, o.spin, o.tilt); return [q[0] + C[0], q[1] + C[1], q[2] + C[2]]; });\n var vis = S.clip(pts, vn, plane);\n if (vis.length < 3) return;\n var m = [0, 0, 0];\n vis.forEach(function (q) { m[0] += q[0] - C[0]; m[1] += q[1] - C[1]; m[2] += q[2] - C[2]; });\n var ml = Math.sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]) || 1;\n faces.push({ loops: [vis], n: [m[0] / ml, m[1] / ml, m[2] / ml], color: col, role: \"surface\" });\n });\n });\n S.add({ faces: faces }, { twoSided: true, spec: 0.12 });\n return S;\n}\n"}}
|