Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585// Reusable, client-owned side chrome for Captutor deliveries.//// Captutor owns the behavior (responsive placement, inward-facing lockups,// slow drift, tight separation shadow). A client theme owns the asset and// tuning. Nothing in this module knows what Fuser—or any future client—looks// like.
import { execFileSync } from "node:child_process";import { existsSync, mkdirSync } from "node:fs";import { basename, dirname, extname, join, resolve } from "node:path";
const FFMPEG = process.env.FFMPEG || "ffmpeg";const FFPROBE = process.env.FFPROBE || "ffprobe";
const defaults = Object.freeze({ opacity: 0.96, periodSec: 18, driftPx: 5, bobPx: 3, longSideFraction: 0.155, edgeFraction: 0.055, leftCenterY: 0.78, rightCenterY: 0.22, markColor: "#ffffff", labelColor: "#ffffff", shadow: { color: "#000000", opacity: 78, blur: 2, x: 2, y: 3 },});
function finite(value, fallback) { return Number.isFinite(Number(value)) ? Number(value) : fallback;}
function clamp(value, min, max) { return Math.max(min, Math.min(max, value));}
export function layoutBrandChrome(theme, { width, height, format = "docs", fps:frameRate = "30",}) { if (!theme || typeof theme !== "object") throw new Error("brand chrome needs a theme object"); if (!theme.asset && !((theme.markAsset || theme.markVideoAsset) && (theme.label || theme.labelAsset))) { throw new Error(`brand theme "${theme.id || "unnamed"}" needs an asset or markAsset + label/labelAsset`); } if (!(width > 0 && height > 0)) throw new Error(`invalid brand frame ${width}x${height}`);
const override = theme.formats?.[format] || {}; const merged = { ...defaults, ...theme, ...override }; const fpsParts = String(frameRate).split("/").map(Number); const fps = fpsParts[0] / (fpsParts[1] || 1); const short = Math.min(width, height); const longSide = Math.round(short * clamp( finite(merged.longSideFraction, defaults.longSideFraction), 0.06, 0.32, )); return { id: theme.id || "client", asset: theme.asset ? resolve(String(theme.asset)) : null, markAsset: theme.markAsset ? resolve(String(theme.markAsset)) : null, markVideoAsset: theme.markVideoAsset ? resolve(String(theme.markVideoAsset)) : null, markPreserveColor: Boolean(merged.markPreserveColor), markOrientation: merged.markOrientation === "upright" ? "upright" : "side", label: theme.label || null, labelAsset: theme.labelAsset ? resolve(String(theme.labelAsset)) : null, labelAssetCrop: theme.labelAssetCrop || null, labelCharacterCuts: Array.isArray(theme.labelCharacterCuts) ? theme.labelCharacterCuts : null, font: theme.font ? resolve(String(theme.font)) : null, markColor: String(merged.markColor || defaults.markColor), labelColor: String(merged.labelColor || defaults.labelColor), labelWeight: Math.round(clamp(finite(merged.labelWeight, 500), 100, 900)), labelStrokeFraction: clamp(finite(merged.labelStrokeFraction, 0.03), 0, 0.08), labelShadow: merged.labelShadow !== false, sectionLabel: Boolean(merged.sectionLabel), unifiedSectionStamp: Boolean(merged.unifiedSectionStamp), sectionLabelPx: Math.round(short * finite(merged.sectionLabelPxFraction, 0.014)), sectionLabelGapPx: Math.round(short * finite(merged.sectionLabelGapFraction, 0.018)), sectionLabelOpacity: clamp(finite(merged.sectionLabelOpacity, 0.84), 0.1, 1), opacity: clamp(finite(merged.opacity, defaults.opacity), 0.1, 1), periodSec: clamp(finite(merged.periodSec, defaults.periodSec), 6, 90), driftPx: Math.round(short * finite(merged.driftFraction, 0)) || Math.round(finite(merged.driftPx, defaults.driftPx)), bobPx: Math.round(short * finite(merged.bobFraction, 0)) || Math.round(finite(merged.bobPx, defaults.bobPx)), longSide, edgePx: Math.round(width * clamp( finite(merged.edgeFraction, defaults.edgeFraction), 0.015, 0.18, )), leftCenterY: clamp(finite(merged.leftCenterY, defaults.leftCenterY), 0.08, 0.92), rightCenterY: clamp(finite(merged.rightCenterY, defaults.rightCenterY), 0.08, 0.92), shadow: { ...defaults.shadow, ...(theme.shadow || {}), ...(override.shadow || {}) }, transparentColors: override.transparentColors || theme.transparentColors || [], markSide: Math.round(short * finite(merged.markSideFraction, 0.105)), labelPx: Math.round(short * finite(merged.labelPxFraction, 0.048)), leftMarkCenterY: finite(merged.leftMarkCenterY, 0.68), leftLabelCenterY: finite(merged.leftLabelCenterY, 0.82), rightMarkCenterY: finite(merged.rightMarkCenterY, 0.32), rightLabelCenterY: finite(merged.rightLabelCenterY, 0.18), fps: Number.isFinite(fps) && fps > 0 ? fps : 30, markPerspective: merged.markPerspective && typeof merged.markPerspective === "object" ? { amount: clamp(finite(merged.markPerspective.amount, 0.08), 0.02, 0.16), periodSec: clamp(finite(merged.markPerspective.periodSec, merged.periodSec), 4, 30), } : null, characterMotion: merged.characterMotion && typeof merged.characterMotion === "object" ? { driftPx: Math.max(0, Math.round(short * finite(merged.characterMotion.driftFraction, 0.0014))), periodSec: clamp(finite(merged.characterMotion.periodSec, 3.2), 1.2, 12), shimmerPeriodSec: clamp(finite(merged.characterMotion.shimmerPeriodSec, 2.4), 1, 12), shimmerAmount: clamp(finite(merged.characterMotion.shimmerAmount, 0.18), 0, 0.35), } : null, };}
function probeVideo(input) { const data = JSON.parse(execFileSync(FFPROBE, [ "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height,r_frame_rate:format=duration", "-of", "json", input, ], { encoding: "utf8" })); const stream = data.streams?.[0]; if (!stream) throw new Error(`no video stream: ${input}`); return { width: Number(stream.width), height: Number(stream.height), fps: stream.r_frame_rate || "30", duration: Number(data.format?.duration), };}
function renderLockups(layout, workDir) { if (!existsSync(layout.asset)) throw new Error(`brand asset does not exist: ${layout.asset}`); mkdirSync(workDir, { recursive: true }); const stem = `${layout.id}-${layout.longSide}`.replace(/[^a-z0-9_.-]+/gi, "-"); const raster = join(workDir, `${stem}-source.png`); const transparentRaster = join(workDir, `${stem}-transparent.png`); const styled = join(workDir, `${stem}-styled.png`); const left = join(workDir, `${stem}-left.png`); const right = join(workDir, `${stem}-right.png`);
const ext = extname(layout.asset).toLowerCase(); if (ext === ".svg") { execFileSync("rsvg-convert", [ "-w", String(layout.longSide * 2), "-o", raster, layout.asset, ], { stdio: "pipe" }); } else { execFileSync("magick", [ layout.asset, "-resize", `${layout.longSide * 2}x${layout.longSide * 2}>`, raster, ], { stdio: "pipe" }); }
// Some client lockup masters are designed as thumbnail badges. Themes may // key those flat backing colors away while retaining the authored glyph, // wordmark, alpha glow, and our compact separation shadow. let styledSource = raster; if (Array.isArray(layout.transparentColors) && layout.transparentColors.length) { const keyArgs = [raster]; for (const entry of layout.transparentColors) { const color = typeof entry === "string" ? entry : entry.color; const fuzz = typeof entry === "string" ? 8 : finite(entry.fuzz, 8); if (!color) continue; keyArgs.push("-fuzz", `${clamp(fuzz, 0, 35)}%`, "-transparent", color); } keyArgs.push(transparentRaster); execFileSync("magick", keyArgs, { stdio:"pipe" }); styledSource = transparentRaster; }
// A compact, high-opacity shadow separates glowing lockups without producing // the broad gray fog that a large blur/spread creates over product UI. const shadow = layout.shadow; execFileSync("magick", [ styledSource, "(", "+clone", "-channel", "A", "-evaluate", "multiply", String(clamp(finite(shadow.opacity, 78), 1, 100) / 100), "+channel", "-background", String(shadow.color || defaults.shadow.color), "-shadow", `${clamp(finite(shadow.opacity, 78), 1, 100)}x${clamp(finite(shadow.blur, 2), 0, 12)}+${finite(shadow.x, 2)}+${finite(shadow.y, 3)}`, ")", "+swap", "-background", "none", "-layers", "merge", "+repage", "-resize", `${layout.longSide}x${layout.longSide}>`, styled, ], { stdio: "pipe" }); execFileSync("magick", [styled, "-background", "none", "-rotate", "90", left], { stdio: "pipe" }); execFileSync("magick", [styled, "-background", "none", "-rotate", "-90", right], { stdio: "pipe" }); return { left, right };}
function sharpShadow(input, output, shadow) { execFileSync("magick", [ input, "(", "+clone", "-channel", "A", "-evaluate", "multiply", String(clamp(finite(shadow.opacity, 88), 1, 100) / 100), "+channel", "-background", String(shadow.color || defaults.shadow.color), "-shadow", `${clamp(finite(shadow.opacity, 88), 1, 100)}x${clamp(finite(shadow.blur, 1.4), 0, 8)}+${finite(shadow.x, 2)}+${finite(shadow.y, 3)}`, ")", "+swap", "-background", "none", "-layers", "merge", "+repage", output, ], { stdio:"pipe" });}
function renderSeparateElements(layout, workDir) { const animatedMark = Boolean(layout.markVideoAsset); const markSource = layout.markVideoAsset || layout.markAsset; if (!existsSync(markSource)) throw new Error(`brand mark does not exist: ${markSource}`); if (layout.labelAsset && !existsSync(layout.labelAsset)) { throw new Error(`brand wordmark does not exist: ${layout.labelAsset}`); } if (!layout.labelAsset && (!layout.font || !existsSync(layout.font))) { throw new Error(`brand font does not exist: ${layout.font}`); } mkdirSync(workDir, { recursive:true }); const stem = `${layout.id}-${layout.markSide}-${layout.labelPx}`.replace(/[^a-z0-9_.-]+/gi, "-"); const markMaster = join(workDir, `${stem}-mark-master.png`); const mark = join(workDir, `${stem}-mark.png`); const markShadow = join(workDir, `${stem}-mark-shadow.png`); const label = join(workDir, `${stem}-label.png`); const labelShadow = join(workDir, `${stem}-label-shadow.png`); if (animatedMark) { // The checked-in mark movie contains unique 60 fps ray-marched views of // the real 3D surface. Keep it upright when the client contract asks for // an emblem beside vertical type; never fake depth by warping a still. } else if (extname(layout.markAsset).toLowerCase() === ".svg") { execFileSync("rsvg-convert", [ "-w", String(layout.markSide * 2), "-h", String(layout.markSide * 2), "-o", markMaster, layout.markAsset, ], { stdio:"pipe" }); } else { execFileSync("magick", [ layout.markAsset, "-resize", `${layout.markSide * 2}x${layout.markSide * 2}!`, markMaster, ], { stdio:"pipe" }); } // Rasterize at 2× for clean edges, then return to the contracted size. A // dimensional client mark may preserve its authored light and material; // flat glyph assets continue to receive the theme's mark color. if (!animatedMark) { const markArgs = [markMaster, "-resize", `${layout.markSide}x${layout.markSide}!`]; if (!layout.markPreserveColor) { markArgs.push("-fill", layout.markColor, "-colorize", "100"); } markArgs.push(mark); execFileSync("magick", markArgs, { stdio:"pipe" }); } if (layout.labelAsset) { const crop = layout.labelAssetCrop || { x:0, y:0, width:1, height:1 }; const fullHeight = Math.max(64, Math.round((layout.labelPx * 2) / finite(crop.height, 1))); const fullWidth = Math.round(fullHeight * finite(crop.aspect, 202 / 161)); const wordmarkMaster = join(workDir, `${stem}-wordmark-master.png`); execFileSync("rsvg-convert", [ "-w", String(fullWidth), "-h", String(fullHeight), "-o", wordmarkMaster, layout.labelAsset, ], { stdio:"pipe" }); const x = Math.round(fullWidth * finite(crop.x, 0)); const y = Math.round(fullHeight * finite(crop.y, 0)); const width = Math.max(1, Math.round(fullWidth * finite(crop.width, 1))); const height = Math.max(1, Math.round(fullHeight * finite(crop.height, 1))); execFileSync("magick", [ wordmarkMaster, "-crop", `${width}x${height}+${x}+${y}`, "+repage", "-colorspace", "gray", "-level", "20%,100%", "-alpha", "copy", "-fill", layout.labelColor, "-colorize", "100", "-resize", `x${layout.labelPx}`, label, ], { stdio:"pipe" }); } else { const labelArgs = [ "-background", "none", "-fill", layout.labelColor, ]; if (layout.labelStrokeFraction > 0) labelArgs.push( "-stroke", layout.shadow.color || defaults.shadow.color, "-strokewidth", String(Math.max(1, Math.round(layout.labelPx * layout.labelStrokeFraction))), ); labelArgs.push( "-weight", String(layout.labelWeight), "-font", layout.font, "-pointsize", String(layout.labelPx * 2), `label:${layout.label}`, "-resize", "50%", label, ); execFileSync("magick", labelArgs, { stdio:"pipe" }); } if (!animatedMark) sharpShadow(mark, markShadow, layout.shadow); if (layout.labelShadow) sharpShadow(label, labelShadow, layout.shadow); const rotate = (source, suffix, degrees) => { const output = join(workDir, `${stem}-${suffix}.png`); execFileSync("magick", [source, "-background", "none", "-rotate", String(degrees), output], { stdio:"pipe" }); return output; }; const labelSource = layout.labelShadow ? labelShadow : label; const result = { kind:"separate", labelLeft:rotate(labelSource, "label-left", 90), labelRight:rotate(labelSource, "label-right", -90), }; if (animatedMark) { const animated = (suffix, transpose) => { const output = join(workDir, `${stem}-${suffix}.mov`); const orient = layout.markOrientation === "upright" ? "" : `,transpose=${transpose}`; execFileSync(FFMPEG, [ "-y", "-i", layout.markVideoAsset, "-vf", `scale=${layout.markSide}:${layout.markSide}:flags=lanczos${orient},format=argb`, "-an", "-c:v", "qtrle", output, ], { stdio:["ignore", "ignore", "pipe"] }); return output; }; result.markLeft = animated("mark-left", "clock"); result.markRight = animated("mark-right", "cclock"); result.animatedMark = true; } else { result.markLeft = rotate(markShadow, "mark-left", 90); result.markRight = rotate(markShadow, "mark-right", -90); }
// Canonical client wordmarks can opt into the /pop-style traveling-letter // treatment without falling back to an approximate font. Each interval is a // normalized horizontal slice of the authored wordmark. The slices remain on // the original transparent canvas, so their kerning is preserved exactly. if (layout.characterMotion && layout.labelCharacterCuts?.length) { const [labelWidth, labelHeight] = execFileSync("identify", [ "-format", "%w %h", label, ], { encoding:"utf8" }).trim().split(/\s+/).map(Number); result.labelLeftChars = []; result.labelRightChars = []; for (const [index, interval] of layout.labelCharacterCuts.entries()) { const from = clamp(finite(interval?.[0], 0), 0, 1); const to = clamp(finite(interval?.[1], 1), from, 1); const x = Math.round(labelWidth * from); const x2 = Math.max(x + 1, Math.round(labelWidth * to)); const charCrop = join(workDir, `${stem}-char-${index}-crop.png`); const charCanvas = join(workDir, `${stem}-char-${index}.png`); const charShadow = join(workDir, `${stem}-char-${index}-shadow.png`); execFileSync("magick", [ label, "-crop", `${x2 - x}x${labelHeight}+${x}+0`, "+repage", charCrop, ], { stdio:"pipe" }); execFileSync("magick", [ "-size", `${labelWidth}x${labelHeight}`, "canvas:none", charCrop, "-geometry", `+${x}+0`, "-composite", charCanvas, ], { stdio:"pipe" }); sharpShadow(charCanvas, charShadow, layout.shadow); result.labelLeftChars.push(rotate(charShadow, `char-${index}-left`, 90)); result.labelRightChars.push(rotate(charShadow, `char-${index}-right`, -90)); } } return result;}
export function brandChromeFilter(layout) { const phase = "2*PI*t/" + layout.periodSec.toFixed(3); const leftX = `${layout.edgePx}-overlay_w/2+${layout.driftPx}*sin(${phase})`; const rightX = `main_w-${layout.edgePx}-overlay_w/2-${layout.driftPx}*sin(${phase})`; const leftY = `main_h*${layout.leftCenterY.toFixed(5)}-overlay_h/2+${layout.bobPx}*sin(${phase}+1.3)`; const rightY = `main_h*${layout.rightCenterY.toFixed(5)}-overlay_h/2-${layout.bobPx}*sin(${phase}+1.3)`; return [ `[1:v]format=rgba,colorchannelmixer=aa=${layout.opacity.toFixed(3)}[left]`, `[2:v]format=rgba,colorchannelmixer=aa=${layout.opacity.toFixed(3)}[right]`, `[0:v][left]overlay=x='${leftX}':y='${leftY}':eval=frame:shortest=1[one]`, `[one][right]overlay=x='${rightX}':y='${rightY}':eval=frame:shortest=1[outv]`, ].join(";");}
export function separateBrandChromeFilter(layout, characterCount = 0, outputLabel = "outv") { const phase = "2*PI*t/" + layout.periodSec.toFixed(3); const edge = layout.edgePx; const leftX = `${edge}-overlay_w/2`; const rightX = `main_w-${edge}-overlay_w/2`; const layered = (sign = 1, offset = 0) => { const s = sign < 0 ? "-" : "+"; return `${s}${layout.driftPx}*sin(${phase}+${offset})${s}${(layout.driftPx * 0.35).toFixed(2)}*sin(${phase}*.43+${offset + 1.1})`; }; const y = (fraction, sign, offset) => `main_h*${fraction.toFixed(5)}-overlay_h/2${sign}${offset}*sin(${phase})`; const markPlate = (input, name, phaseOffset = 0) => { if (!layout.markPerspective) { return `[${input}:v]format=rgba,colorchannelmixer=aa=${layout.opacity.toFixed(3)}[${name}]`; } const amount = layout.markPerspective.amount.toFixed(4); const frames = Math.max(1, Math.round(layout.fps * layout.markPerspective.periodSec)); const offsetFrames = Math.round(frames * phaseOffset); const turn = `2*PI*(in+${offsetFrames})/${frames}`; return ( `[${input}:v]format=rgba,pad=iw*1.55:ih*1.55:(ow-iw)/2:(oh-ih)/2:color=black@0,` + `perspective=` + `x0='0.08*W+${amount}*W*sin(${turn})':y0='0.08*H-${amount}*H*cos(${turn})':` + `x1='0.92*W-${amount}*W*sin(${turn})':y1='0.08*H+${amount}*H*cos(${turn})':` + `x2='0.08*W-${amount}*W*sin(${turn})':y2='0.92*H-${amount}*H*cos(${turn})':` + `x3='0.92*W+${amount}*W*sin(${turn})':y3='0.92*H+${amount}*H*cos(${turn})':` + `sense=destination:eval=frame:interpolation=cubic,` + `colorchannelmixer=aa=${layout.opacity.toFixed(3)}[${name}]` ); }; if (characterCount > 0 && layout.characterMotion) { const filters = []; const leftMarkIndex = 1; const leftCharStart = 2; const rightMarkIndex = leftCharStart + characterCount; const rightCharStart = rightMarkIndex + 1; filters.push(`[${leftMarkIndex}:v]format=rgba,colorchannelmixer=aa=${layout.opacity.toFixed(3)}[ml]`); filters.push(`[${rightMarkIndex}:v]format=rgba,colorchannelmixer=aa=${layout.opacity.toFixed(3)}[mr]`); const motion = layout.characterMotion; for (let i = 0; i < characterCount; i += 1) { const shimmerPhase = (i * 0.72).toFixed(3); const shimmer = `${(1 - motion.shimmerAmount).toFixed(3)}+${motion.shimmerAmount.toFixed(3)}*sin(2*PI*T/${motion.shimmerPeriodSec.toFixed(3)}-${shimmerPhase})`; for (const [input, name] of [ [leftCharStart + i, `cl${i}`], [rightCharStart + i, `cr${i}`], ]) { filters.push( `[${input}:v]format=rgba,geq=r='r(X,Y)':g='g(X,Y)':b='b(X,Y)':a='alpha(X,Y)*${layout.opacity.toFixed(3)}*(${shimmer})'[${name}]`, ); } } const leftPairY = y(layout.leftMarkCenterY, "+", layout.bobPx); const rightPairY = y(layout.rightMarkCenterY, "-", layout.bobPx); filters.push(`[0:v][ml]overlay=x='${leftX}${layered(1, 0)}':y='${leftPairY}':eval=frame:shortest=1[v0]`); let previous = "v0"; for (let i = 0; i < characterCount; i += 1) { const drift = `${motion.driftPx}*sin(2*PI*t/${motion.periodSec.toFixed(3)}-${(i * 0.8).toFixed(3)})`; const next = `vl${i}`; filters.push(`[${previous}][cl${i}]overlay=x='${leftX}${layered(1, 0)}+${drift}':y='main_h*${layout.leftLabelCenterY.toFixed(5)}-overlay_h/2+${layout.bobPx}*sin(${phase})':eval=frame:shortest=1[${next}]`); previous = next; } filters.push(`[${previous}][mr]overlay=x='${rightX}${layered(-1, 0.6)}':y='${rightPairY}':eval=frame:shortest=1[vr0]`); previous = "vr0"; for (let i = 0; i < characterCount; i += 1) { const drift = `${motion.driftPx}*sin(2*PI*t/${motion.periodSec.toFixed(3)}-${(i * 0.8 + 0.6).toFixed(3)})`; const next = i === characterCount - 1 ? outputLabel : `vr${i + 1}`; filters.push(`[${previous}][cr${i}]overlay=x='${rightX}${layered(-1, 0.6)}-${drift}':y='main_h*${layout.rightLabelCenterY.toFixed(5)}-overlay_h/2-${layout.bobPx}*sin(${phase})':eval=frame:shortest=1[${next}]`); previous = next; } return filters.join(";"); } return [ markPlate(1, "ml", 0), `[2:v]format=rgba,colorchannelmixer=aa=${layout.opacity.toFixed(3)}[tl]`, markPlate(3, "mr", 0.5), `[4:v]format=rgba,colorchannelmixer=aa=${layout.opacity.toFixed(3)}[tr]`, `[0:v][ml]overlay=x='${leftX}+${layout.driftPx}*sin(${phase})':y='${y(layout.leftMarkCenterY, "+", layout.bobPx)}':eval=frame:shortest=1[a]`, `[a][tl]overlay=x='${leftX}-${layout.driftPx}*sin(${phase}+1.7)':y='${y(layout.leftLabelCenterY, "-", layout.bobPx)}':eval=frame:shortest=1[b]`, `[b][mr]overlay=x='${rightX}-${layout.driftPx}*sin(${phase}+.6)':y='${y(layout.rightMarkCenterY, "-", layout.bobPx)}':eval=frame:shortest=1[c]`, `[c][tr]overlay=x='${rightX}+${layout.driftPx}*sin(${phase}+2.1)':y='${y(layout.rightLabelCenterY, "+", layout.bobPx)}':eval=frame:shortest=1[${outputLabel}]`, ].join(";");}
export function unifiedSectionStampBaseFilter(layout, outputLabel = "brandbase") { const phase = "2*PI*t/" + layout.periodSec.toFixed(3); const leftX = `${layout.edgePx}-overlay_w/2`; const rightX = `main_w-${layout.edgePx}-overlay_w/2`; const leftY = `main_h*${layout.leftMarkCenterY.toFixed(5)}-overlay_h/2+${layout.bobPx}*sin(${phase})`; const rightY = `main_h*${layout.rightMarkCenterY.toFixed(5)}-overlay_h/2-${layout.bobPx}*sin(${phase})`; return [ `[1:v]format=rgba,colorchannelmixer=aa=${layout.opacity.toFixed(3)}[ml]`, `[2:v]format=rgba,colorchannelmixer=aa=${layout.opacity.toFixed(3)}[mr]`, `[0:v][ml]overlay=x='${leftX}+${layout.driftPx}*sin(${phase})':y='${leftY}':eval=frame:shortest=1[a]`, `[a][mr]overlay=x='${rightX}-${layout.driftPx}*sin(${phase}+.6)':y='${rightY}':eval=frame:shortest=1[${outputLabel}]`, ].join(";");}
function mixSectionColor(base, tint, amount = 0.46) { const parse = (value, fallback) => /^#[0-9a-f]{6}$/i.test(String(value || "")) ? [1, 3, 5].map((at) => parseInt(String(value).slice(at, at + 2), 16)) : fallback; const from = parse(base, [79, 61, 89]); const to = parse(tint, from); return `#${from.map((part, index) => Math.round(part + (to[index] - part) * amount) .toString(16).padStart(2, "0")).join("")}`;}
function renderSectionLabels(layout, { title, chapters, duration }, workDir) { if (!layout.sectionLabel || !title || !Array.isArray(chapters) || !chapters.length) return []; const ordered = chapters .map((chapter) => ({ ...chapter, startSec:Number(chapter.startSec) })) .filter((chapter) => Number.isFinite(chapter.startSec) && chapter.startSec >= 0) .sort((a, b) => a.startSec - b.startSec); return ordered.map((chapter, index) => { const stem = `section-${String(index).padStart(2, "0")}`; const flat = join(workDir, `${stem}.png`); const left = join(workDir, `${stem}-left.png`); const right = join(workDir, `${stem}-right.png`); const sectionTitle = chapter.shortTitle || chapter.title || `Chapter ${index + 1}`; const copy = layout.unifiedSectionStamp ? `${layout.label}: ${title} - ${sectionTitle}` : `: ${title} · ${sectionTitle}`; const color = mixSectionColor( layout.labelColor, chapter.wallpaperColor || chapter.color || layout.labelColor, layout.unifiedSectionStamp ? 0.78 : 0.46, ); execFileSync("magick", [ "-background", "none", "-fill", color, "-font", layout.font, "-pointsize", String((layout.unifiedSectionStamp ? layout.labelPx : layout.sectionLabelPx) * 2), "-weight", String(layout.labelWeight), `label:${copy}`, "-resize", "50%", flat, ], { stdio:"pipe" }); execFileSync("magick", [flat, "-background", "none", "-rotate", "90", left], { stdio:"pipe" }); execFileSync("magick", [flat, "-background", "none", "-rotate", "-90", right], { stdio:"pipe" }); return { left, right, startSec:chapter.startSec, endSec:Number(ordered[index + 1]?.startSec ?? duration), color, copy, }; });}
function sectionLabelFilter(layout, sections, firstInput, inputLabel) { if (!sections.length) return ""; const phase = `2*PI*t/${layout.periodSec.toFixed(3)}`; const leftX = `${layout.edgePx + layout.sectionLabelGapPx}-overlay_w/2`; const rightX = `main_w-${layout.edgePx + layout.sectionLabelGapPx}-overlay_w/2`; const filters = []; let previous = inputLabel; sections.forEach((section, index) => { const leftInput = firstInput + index * 2; const rightInput = leftInput + 1; const leftPlate = `section-left-${index}`; const rightPlate = `section-right-${index}`; const leftOut = `section-left-out-${index}`; const rightOut = index === sections.length - 1 ? "outv" : `section-right-out-${index}`; const enable = `gte(t,${section.startSec.toFixed(3)})*lt(t,${section.endSec.toFixed(3)})`; filters.push( `[${leftInput}:v]format=rgba,colorchannelmixer=aa=${layout.sectionLabelOpacity.toFixed(3)}[${leftPlate}]`, `[${rightInput}:v]format=rgba,colorchannelmixer=aa=${layout.sectionLabelOpacity.toFixed(3)}[${rightPlate}]`, `[${previous}][${leftPlate}]overlay=x='${leftX}':` + `y='main_h*${layout.leftLabelCenterY.toFixed(5)}-overlay_h/2+${layout.bobPx}*sin(${phase})':` + `enable='${enable}':eval=frame:shortest=1[${leftOut}]`, `[${leftOut}][${rightPlate}]overlay=x='${rightX}':` + `y='main_h*${layout.rightLabelCenterY.toFixed(5)}-overlay_h/2-${layout.bobPx}*sin(${phase})':` + `enable='${enable}':eval=frame:shortest=1[${rightOut}]`, ); previous = rightOut; }); return filters.join(";");}
export function applyBrandChrome({ input, out, theme, workDir, format = "docs", context = null,}) { if (!existsSync(input)) throw new Error(`brand input does not exist: ${input}`); const media = probeVideo(input); const layout = layoutBrandChrome(theme, { ...media, format }); const assetsDir = join(workDir || dirname(out), "brand-chrome", layout.id); const lockups = (layout.markAsset || layout.markVideoAsset) && (layout.label || layout.labelAsset) ? renderSeparateElements(layout, assetsDir) : renderLockups(layout, assetsDir); const sectionLabels = lockups.kind === "separate" ? renderSectionLabels(layout, { title:context?.title, chapters:context?.chapters, duration:media.duration, }, assetsDir) : []; const unifiedSectionStamp = Boolean(layout.unifiedSectionStamp && sectionLabels.length); mkdirSync(dirname(out), { recursive: true }); const baseOverlayInputs = lockups.kind === "separate" ? unifiedSectionStamp ? [lockups.markLeft, lockups.markRight] : lockups.labelLeftChars?.length ? [lockups.markLeft, ...lockups.labelLeftChars, lockups.markRight, ...lockups.labelRightChars] : [lockups.markLeft, lockups.labelLeft, lockups.markRight, lockups.labelRight] : [lockups.left, lockups.right]; const overlayInputs = [ ...baseOverlayInputs, ...sectionLabels.flatMap((section) => [section.left, section.right]), ]; const animatedMarkInputs = unifiedSectionStamp ? [0, 1] : [0, 2]; const ffmpegInputs = overlayInputs.flatMap((path, index) => lockups.animatedMark && animatedMarkInputs.includes(index) ? ["-stream_loop", "-1", "-i", path] : ["-loop", "1", "-i", path]); execFileSync(FFMPEG, [ "-y", "-i", input, ...ffmpegInputs, "-filter_complex_threads", "1", "-filter_complex", lockups.kind === "separate" ? [ unifiedSectionStamp ? unifiedSectionStampBaseFilter(layout, "brandbase") : separateBrandChromeFilter( layout, lockups.labelLeftChars?.length || 0, sectionLabels.length ? "brandbase" : "outv", ), sectionLabelFilter(layout, sectionLabels, 1 + baseOverlayInputs.length, "brandbase"), ].filter(Boolean).join(";") : brandChromeFilter(layout), "-map", "[outv]", "-map", "0:a?", "-t", media.duration.toFixed(3), "-r", media.fps, "-c:v", "libx264", "-preset", "medium", "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "copy", "-movflags", "+faststart", out, ], { stdio: ["ignore", "ignore", "pipe"] }); return { out, layout, lockups, sectionLabels, source:basename(input) };}