From 8bf939043fb44e05f6dd7caba81822ac5ef18334 Mon Sep 17 00:00:00 2001 From: vanceingalls Date: Fri, 24 Jul 2026 06:10:00 +0000 Subject: [PATCH] fix(registry): liberal emoji-pop brand colors, weight-shift fit fixes, 8192 clamp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - caption-emoji-pop: shadowForColor now builds its glow via color-mix() instead of hex-pair slicing, so the strict 6-digit brand-color gate is gone — any CSS color the sibling templates accept (#fff, rgb(), named) now renders instead of silently falling back to the default palette - caption-weight-shift: fitFontSize now sizes against the WIDEST split line rather than the joined group text (two-line groups no longer shrink unnecessarily), and avoidSingleWordGroups' merges re-check fitsInTwoLines like makeGroups' first pass does (merged groups can no longer overflow the split budget) - all 5: hfApplyStageConfig clamps resolution to the published validator's <=8192 bound (validator is optional pre-flight; unbounded stages OOM render workers), and fit floors carry a comment documenting that the minimum size is returned unverified by design Co-Authored-By: Claude Fable 5 --- .../caption-editorial-emphasis.html | 16 +++++- .../caption-emoji-pop/caption-emoji-pop.html | 52 +++++++++-------- .../caption-highlight/caption-highlight.html | 16 +++++- .../caption-pill-karaoke.html | 16 +++++- .../caption-weight-shift.html | 57 +++++++++++++++---- 5 files changed, 117 insertions(+), 40 deletions(-) diff --git a/registry/components/caption-editorial-emphasis/caption-editorial-emphasis.html b/registry/components/caption-editorial-emphasis/caption-editorial-emphasis.html index 3790946ee..2622b09a8 100644 --- a/registry/components/caption-editorial-emphasis/caption-editorial-emphasis.html +++ b/registry/components/caption-editorial-emphasis/caption-editorial-emphasis.html @@ -226,8 +226,17 @@ function hfApplyStageConfig(data, durationS) { var res = (data && data.resolution) || HF_DEFAULT_RESOLUTION; - var W = Math.max(1, Math.round(Number(res.width) || HF_DEFAULT_RESOLUTION.width)); - var H = Math.max(1, Math.round(Number(res.height) || HF_DEFAULT_RESOLUTION.height)); + // Mirror the published validator's <=8192 bound defensively — the + // validator is an optional pre-flight, and an unbounded stage size + // would OOM render workers. + var W = Math.min( + 8192, + Math.max(1, Math.round(Number(res.width) || HF_DEFAULT_RESOLUTION.width)), + ); + var H = Math.min( + 8192, + Math.max(1, Math.round(Number(res.height) || HF_DEFAULT_RESOLUTION.height)), + ); var root = document.getElementById(HF_ROOT_ID); [document.documentElement, document.body, root].forEach(function (el) { el.style.width = W + "px"; @@ -420,6 +429,9 @@ function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) { var size = baseFontSize; var minSize = Math.floor(baseFontSize * 0.45); + // minSize is a hard floor returned unverified below — self-heal + // accepts residual overflow at the floor (clipped by the stage) + // rather than shrinking below legibility. while (size > minSize) { _fitCtx.font = fontWeight + " " + size + "px " + fontFamily; if (_fitCtx.measureText(text).width <= maxWidth) return size; diff --git a/registry/components/caption-emoji-pop/caption-emoji-pop.html b/registry/components/caption-emoji-pop/caption-emoji-pop.html index 7c982e278..11d7c77f8 100644 --- a/registry/components/caption-emoji-pop/caption-emoji-pop.html +++ b/registry/components/caption-emoji-pop/caption-emoji-pop.html @@ -229,8 +229,17 @@ function hfApplyStageConfig(data, durationS) { var res = (data && data.resolution) || HF_DEFAULT_RESOLUTION; - var W = Math.max(1, Math.round(Number(res.width) || HF_DEFAULT_RESOLUTION.width)); - var H = Math.max(1, Math.round(Number(res.height) || HF_DEFAULT_RESOLUTION.height)); + // Mirror the published validator's <=8192 bound defensively — the + // validator is an optional pre-flight, and an unbounded stage size + // would OOM render workers. + var W = Math.min( + 8192, + Math.max(1, Math.round(Number(res.width) || HF_DEFAULT_RESOLUTION.width)), + ); + var H = Math.min( + 8192, + Math.max(1, Math.round(Number(res.height) || HF_DEFAULT_RESOLUTION.height)), + ); var root = document.getElementById(HF_ROOT_ID); [document.documentElement, document.body, root].forEach(function (el) { el.style.width = W + "px"; @@ -532,6 +541,9 @@ function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) { var size = baseFontSize; var minSize = Math.floor(baseFontSize * 0.45); + // minSize is a hard floor returned unverified below — self-heal + // accepts residual overflow at the floor (clipped by the stage) + // rather than shrinking below legibility. while (size > minSize) { _fitCtx.font = fontWeight + " " + size + "px " + fontFamily; if (_fitCtx.measureText(text).width <= maxWidth) return size; @@ -605,24 +617,16 @@ } function shadowForColor(color) { - var hex = color.replace("#", ""); - var r = parseInt(hex.slice(0, 2), 16); - var g = parseInt(hex.slice(2, 4), 16); - var b = parseInt(hex.slice(4, 6), 16); + // color-mix handles ANY CSS color (hex of either length, rgb()/hsl(), + // named colors) — the old hex-pair slicing was why brand colors were + // gated behind a strict 6-digit regex, silently dropping payloads the + // sibling templates accept. return ( - "0 4px 8px rgba(0,0,0,0.7), 0 0 2px rgba(" + - r + - "," + - g + - "," + - b + - ",1), 0 0 8px rgba(" + - r + - "," + - g + - "," + - b + - ",0.6)" + "0 4px 8px rgba(0,0,0,0.7), 0 0 2px " + + color + + ", 0 0 8px color-mix(in srgb, " + + color + + " 60%, transparent)" ); } @@ -689,12 +693,14 @@ stage.style.bottom = Math.round(80 * layout.scaleY) + "px"; var rootEl = document.getElementById(HF_ROOT_ID); + // Any non-empty CSS color is honored (mirrors caption-pill-karaoke) — + // shadowForColor no longer needs parseable hex, so the old 6-digit + // gate (which silently dropped #fff / rgb() / named colors that the + // sibling templates accept) is gone. var primary = getComputedStyle(rootEl).getPropertyValue("--hf-caption-primary").trim(); var accent = getComputedStyle(rootEl).getPropertyValue("--hf-caption-accent").trim(); - hfPrimaryColor = /^#[0-9a-f]{6}$/i.test(primary) ? primary : "#FFFFFF"; - hfAccentColors = /^#[0-9a-f]{6}$/i.test(accent) - ? [accent] - : ["#FF76FF", "#FF0002", "#B2F7FF"]; + hfPrimaryColor = primary || "#FFFFFF"; + hfAccentColors = accent ? [accent] : ["#FF76FF", "#FF0002", "#B2F7FF"]; var WORDS = normalizeWords(words); var GROUPS = makeGroups(WORDS); diff --git a/registry/components/caption-highlight/caption-highlight.html b/registry/components/caption-highlight/caption-highlight.html index a1d1b8be9..2156930ce 100644 --- a/registry/components/caption-highlight/caption-highlight.html +++ b/registry/components/caption-highlight/caption-highlight.html @@ -132,6 +132,9 @@ function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) { var size = baseFontSize; var minSize = Math.floor(baseFontSize * 0.45); + // minSize is a hard floor returned unverified below — self-heal + // accepts residual overflow at the floor (clipped by the stage) + // rather than shrinking below legibility. while (size > minSize) { _fitCtx.font = fontWeight + " " + size + "px " + fontFamily; if (_fitCtx.measureText(text).width <= maxWidth) return size; @@ -235,8 +238,17 @@ function hfApplyStageConfig(data, durationS) { var res = (data && data.resolution) || HF_DEFAULT_RESOLUTION; - var W = Math.max(1, Math.round(Number(res.width) || HF_DEFAULT_RESOLUTION.width)); - var H = Math.max(1, Math.round(Number(res.height) || HF_DEFAULT_RESOLUTION.height)); + // Mirror the published validator's <=8192 bound defensively — the + // validator is an optional pre-flight, and an unbounded stage size + // would OOM render workers. + var W = Math.min( + 8192, + Math.max(1, Math.round(Number(res.width) || HF_DEFAULT_RESOLUTION.width)), + ); + var H = Math.min( + 8192, + Math.max(1, Math.round(Number(res.height) || HF_DEFAULT_RESOLUTION.height)), + ); var root = document.getElementById(HF_ROOT_ID); [document.documentElement, document.body, root].forEach(function (el) { el.style.width = W + "px"; diff --git a/registry/components/caption-pill-karaoke/caption-pill-karaoke.html b/registry/components/caption-pill-karaoke/caption-pill-karaoke.html index c0025e262..5e19c8ed4 100644 --- a/registry/components/caption-pill-karaoke/caption-pill-karaoke.html +++ b/registry/components/caption-pill-karaoke/caption-pill-karaoke.html @@ -235,8 +235,17 @@ function hfApplyStageConfig(data, durationS) { var res = (data && data.resolution) || HF_DEFAULT_RESOLUTION; - var W = Math.max(1, Math.round(Number(res.width) || HF_DEFAULT_RESOLUTION.width)); - var H = Math.max(1, Math.round(Number(res.height) || HF_DEFAULT_RESOLUTION.height)); + // Mirror the published validator's <=8192 bound defensively — the + // validator is an optional pre-flight, and an unbounded stage size + // would OOM render workers. + var W = Math.min( + 8192, + Math.max(1, Math.round(Number(res.width) || HF_DEFAULT_RESOLUTION.width)), + ); + var H = Math.min( + 8192, + Math.max(1, Math.round(Number(res.height) || HF_DEFAULT_RESOLUTION.height)), + ); var root = document.getElementById(HF_ROOT_ID); [document.documentElement, document.body, root].forEach(function (el) { el.style.width = W + "px"; @@ -464,6 +473,9 @@ function fontSizeForGroup(words) { var size = BASE_FONT_SIZE; + // MIN_FONT_SIZE is a hard floor returned unverified below — self-heal + // accepts residual overflow at the floor (clipped by the stage) + // rather than shrinking below legibility. while (size > MIN_FONT_SIZE && !fitsInTwoLinesAtSize(words, size)) { size -= 2; } diff --git a/registry/components/caption-weight-shift/caption-weight-shift.html b/registry/components/caption-weight-shift/caption-weight-shift.html index d6340fd6a..b42c4e59a 100644 --- a/registry/components/caption-weight-shift/caption-weight-shift.html +++ b/registry/components/caption-weight-shift/caption-weight-shift.html @@ -221,8 +221,17 @@ function hfApplyStageConfig(data, durationS) { var res = (data && data.resolution) || HF_DEFAULT_RESOLUTION; - var W = Math.max(1, Math.round(Number(res.width) || HF_DEFAULT_RESOLUTION.width)); - var H = Math.max(1, Math.round(Number(res.height) || HF_DEFAULT_RESOLUTION.height)); + // Mirror the published validator's <=8192 bound defensively — the + // validator is an optional pre-flight, and an unbounded stage size + // would OOM render workers. + var W = Math.min( + 8192, + Math.max(1, Math.round(Number(res.width) || HF_DEFAULT_RESOLUTION.width)), + ); + var H = Math.min( + 8192, + Math.max(1, Math.round(Number(res.height) || HF_DEFAULT_RESOLUTION.height)), + ); var root = document.getElementById(HF_ROOT_ID); [document.documentElement, document.body, root].forEach(function (el) { el.style.width = W + "px"; @@ -345,6 +354,9 @@ function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) { var size = baseFontSize; var minSize = Math.floor(baseFontSize * 0.45); + // minSize is a hard floor returned unverified below — self-heal + // accepts residual overflow at the floor (clipped by the stage) + // rather than shrinking below legibility. while (size > minSize) { _fitCtx.font = fontWeight + " " + size + "px " + fontFamily; if (_fitCtx.measureText(text).width <= maxWidth) return size; @@ -401,7 +413,11 @@ if ( group.words.length === 1 && out.length > 0 && - out[out.length - 1].words.length < MAX_WORDS_PER_GROUP + out[out.length - 1].words.length < MAX_WORDS_PER_GROUP && + // makeGroups guards every addition with fitsInTwoLines; this + // second-pass merge must too, or it can produce a group that + // splitLines cannot fit within MAX_LINE_WIDTH. + fitsInTwoLines(out[out.length - 1].words.concat(group.words)) ) { out[out.length - 1] = makeGroup(out[out.length - 1].words.concat(group.words)); } else { @@ -411,10 +427,18 @@ for (var i = 0; i < out.length; i++) { if (out[i].words.length !== 1) continue; - if (i + 1 < out.length && out[i + 1].words.length < MAX_WORDS_PER_GROUP) { + if ( + i + 1 < out.length && + out[i + 1].words.length < MAX_WORDS_PER_GROUP && + fitsInTwoLines(out[i].words.concat(out[i + 1].words)) + ) { out[i] = makeGroup(out[i].words.concat(out[i + 1].words)); out.splice(i + 1, 1); - } else if (i > 0 && out[i - 1].words.length < MAX_WORDS_PER_GROUP) { + } else if ( + i > 0 && + out[i - 1].words.length < MAX_WORDS_PER_GROUP && + fitsInTwoLines(out[i - 1].words.concat(out[i].words)) + ) { out[i - 1] = makeGroup(out[i - 1].words.concat(out[i].words)); out.splice(i, 1); i--; @@ -464,13 +488,24 @@ function buildCaptions(groups) { var stage = document.getElementById("caption-stage"); groups.forEach(function (group, groupIndex) { - var groupText = group.words - .map(function (w) { - return w.text.toLowerCase(); - }) - .join(" "); + // Fit the WIDEST split line, not the joined group text: the group + // renders across up to two lines, so sizing against the joined + // string shrank two-line groups that already fit at full size. + var widestLineText = ""; + var widestLineWidth = -1; + group.lines.forEach(function (line) { + var lineWidth = measureLineWidth(line.words); + if (lineWidth > widestLineWidth) { + widestLineWidth = lineWidth; + widestLineText = line.words + .map(function (w) { + return w.text.toLowerCase(); + }) + .join(" "); + } + }); var computedSize = fitFontSize( - groupText, + widestLineText, CAPTION_FONT_SIZE, "700", "Montserrat",