@bendyline/squisq 2.13.3 → 2.13.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/{Doc-BPhdJFS_.d.ts → Doc-BQUVDooL.d.ts} +16 -0
  2. package/dist/{ImageEditDoc-DDx-sw_S.d.ts → ImageEditDoc-glA5UUau.d.ts} +1 -1
  3. package/dist/{chunk-D3JQMZUT.js → chunk-CPD3MFX7.js} +1 -1
  4. package/dist/{chunk-POZCKPCO.js → chunk-CZHWFQ7C.js} +1 -1
  5. package/dist/{chunk-VX7MPZOK.js → chunk-F5WTRHWL.js} +4 -4
  6. package/dist/{chunk-JFJZ4NS6.js → chunk-FJNTG3VZ.js} +2 -2
  7. package/dist/{chunk-JOBMPMY5.js → chunk-FSZ6HB7S.js} +1 -1
  8. package/dist/{chunk-F3LRTRST.js → chunk-K75GTKSN.js} +5 -5
  9. package/dist/{chunk-6NWVLLMI.js → chunk-KVEOD57Z.js} +5 -0
  10. package/dist/{chunk-ISVJ42I3.js → chunk-SG3HXPMF.js} +3 -0
  11. package/dist/{chunk-5FUBK3DK.js → chunk-ZF4NWRZU.js} +384 -159
  12. package/dist/doc/index.d.ts +51 -7
  13. package/dist/doc/index.js +8 -6
  14. package/dist/fence/index.d.ts +1 -1
  15. package/dist/generate/index.d.ts +1 -1
  16. package/dist/imageEdit/index.d.ts +3 -3
  17. package/dist/index.d.ts +6 -6
  18. package/dist/index.js +13 -9
  19. package/dist/jsonForm/index.d.ts +1 -1
  20. package/dist/jsonForm/index.js +1 -1
  21. package/dist/markdown/index.d.ts +6 -1
  22. package/dist/markdown/index.js +5 -3
  23. package/dist/{materializePageSection-DnQi-0TA.d.ts → materializePageSection-_ftXlwvB.d.ts} +1 -1
  24. package/dist/mediaEdit/index.d.ts +2 -2
  25. package/dist/narration/index.d.ts +1 -1
  26. package/dist/narration/index.js +4 -4
  27. package/dist/recommend/index.js +2 -2
  28. package/dist/schemas/index.d.ts +4 -4
  29. package/dist/schemas/index.js +1 -1
  30. package/dist/{themeLibrary-DM7eSK1w.d.ts → themeLibrary-D6GK29l3.d.ts} +1 -1
  31. package/dist/timing/index.d.ts +1 -1
  32. package/dist/transform/index.d.ts +2 -2
  33. package/dist/transform/index.js +5 -5
  34. package/package.json +1 -1
@@ -32,7 +32,7 @@ import {
32
32
  profileBlockContents,
33
33
  renderMarkdownBlocksHtml,
34
34
  treeToTemplateData
35
- } from "./chunk-POZCKPCO.js";
35
+ } from "./chunk-CZHWFQ7C.js";
36
36
  import {
37
37
  anchorPoint,
38
38
  base64ToUtf8,
@@ -81,7 +81,7 @@ import {
81
81
  parseVideoEmbedUrl,
82
82
  readFrontmatterThemeId,
83
83
  walkMarkdownTree
84
- } from "./chunk-6NWVLLMI.js";
84
+ } from "./chunk-KVEOD57Z.js";
85
85
  import {
86
86
  resolveBlockTransition
87
87
  } from "./chunk-P53Z4RYC.js";
@@ -1289,6 +1289,131 @@ function sectionHeader(input, context) {
1289
1289
  return layers;
1290
1290
  }
1291
1291
 
1292
+ // src/doc/utils/textMetrics.ts
1293
+ var AVERAGE_CHAR_WIDTH_EM = 0.62;
1294
+ var BOLD_CHAR_WIDTH_EM = 0.66;
1295
+ function estimateTextWidth(text, fontSize, charWidthEm = AVERAGE_CHAR_WIDTH_EM) {
1296
+ return text.length * fontSize * charWidthEm;
1297
+ }
1298
+ var DISPLAY_CHAR_EM = {
1299
+ space: 0.27,
1300
+ narrowLower: 0.36,
1301
+ wideLower: 0.86,
1302
+ lower: 0.57,
1303
+ narrowUpper: 0.4,
1304
+ wideUpper: 0.95,
1305
+ upper: 0.7,
1306
+ digit: 0.57,
1307
+ punct: 0.34,
1308
+ other: 0.62
1309
+ };
1310
+ function displayCharEm(ch) {
1311
+ if (ch === " ") return DISPLAY_CHAR_EM.space;
1312
+ if ("fijlrt".includes(ch)) return DISPLAY_CHAR_EM.narrowLower;
1313
+ if (ch === "m" || ch === "w") return DISPLAY_CHAR_EM.wideLower;
1314
+ if (ch >= "a" && ch <= "z") return DISPLAY_CHAR_EM.lower;
1315
+ if (ch === "I" || ch === "J") return DISPLAY_CHAR_EM.narrowUpper;
1316
+ if (ch === "M" || ch === "W") return DISPLAY_CHAR_EM.wideUpper;
1317
+ if (ch >= "A" && ch <= "Z") return DISPLAY_CHAR_EM.upper;
1318
+ if (ch >= "0" && ch <= "9") return DISPLAY_CHAR_EM.digit;
1319
+ if (".,:;-/'\u2019!?()".includes(ch)) return DISPLAY_CHAR_EM.punct;
1320
+ return DISPLAY_CHAR_EM.other;
1321
+ }
1322
+ function estimateDisplayTextWidth(text, fontSize) {
1323
+ let em = 0;
1324
+ for (const ch of text.normalize("NFD").replace(/\p{M}/gu, "")) em += displayCharEm(ch);
1325
+ return em * fontSize;
1326
+ }
1327
+
1328
+ // src/doc/templates/frostedPlate.ts
1329
+ var FROSTED_PAD_X_EM = 0.45;
1330
+ var FROSTED_PAD_Y_EM = 0.3;
1331
+ var FROSTED_RADIUS_EM = 0.3;
1332
+ var FROSTED_BLUR_SHARE = 0.0167;
1333
+ var FROSTED_TINT_UNDER_LIGHT_TEXT = "rgba(12, 12, 14, 0.26)";
1334
+ var FROSTED_TINT_UNDER_DARK_TEXT = "rgba(250, 250, 250, 0.5)";
1335
+ var FROSTED_BACKDROP_CONTRAST = 0.65;
1336
+ function frostedUnderLightText(context) {
1337
+ return relativeLuminance(context.theme.colors.text) > 0.5;
1338
+ }
1339
+ function frostedBlurPx(context) {
1340
+ const { width, height } = context.viewport;
1341
+ return Math.round(FROSTED_BLUR_SHARE * Math.min(width, height));
1342
+ }
1343
+ function pct(value, total) {
1344
+ return `${Math.round(value / total * 1e4) / 100}%`;
1345
+ }
1346
+ function frostedPlateLayer(id, plate, context) {
1347
+ const { viewport } = context;
1348
+ const lightText = frostedUnderLightText(context);
1349
+ return {
1350
+ type: "shape",
1351
+ id,
1352
+ content: {
1353
+ shape: "rect",
1354
+ fill: lightText ? FROSTED_TINT_UNDER_LIGHT_TEXT : FROSTED_TINT_UNDER_DARK_TEXT,
1355
+ borderRadius: Math.round(plate.radius),
1356
+ backdropBlur: frostedBlurPx(context),
1357
+ ...lightText ? { backdropContrast: FROSTED_BACKDROP_CONTRAST } : {}
1358
+ },
1359
+ position: {
1360
+ x: pct(plate.x, viewport.width),
1361
+ y: pct(plate.y, viewport.height),
1362
+ width: pct(plate.width, viewport.width),
1363
+ height: pct(plate.height, viewport.height)
1364
+ }
1365
+ };
1366
+ }
1367
+ var INK_EM = {
1368
+ xHeight: 0.45,
1369
+ capital: 0.67,
1370
+ ascent: 0.71,
1371
+ accentedCapital: 0.8,
1372
+ descent: 0.2,
1373
+ comma: 0.155
1374
+ };
1375
+ function opticalCentreShiftEm(lines) {
1376
+ const first = (lines[0] ?? "").normalize("NFD");
1377
+ const last = lines[lines.length - 1] ?? "";
1378
+ const top = /[A-Z]\p{M}/u.test(first) ? INK_EM.accentedCapital : /[bdfhklt]|[a-z]\p{M}/u.test(first) ? INK_EM.ascent : /[A-Z0-9]/.test(first) ? INK_EM.capital : INK_EM.xHeight;
1379
+ const bottom = /[gjpqyQ]/.test(last) ? INK_EM.descent : /[,;]/.test(last) ? INK_EM.comma : 0;
1380
+ return Math.round(((top - bottom) / 2 - INK_EM.xHeight / 2) * 1e3) / 1e3;
1381
+ }
1382
+ function wrapDisplayLine(line, fontSize, maxWidth) {
1383
+ const lines = [];
1384
+ let current = "";
1385
+ for (const word of line.trim().split(/\s+/).filter(Boolean)) {
1386
+ const candidate = current ? `${current} ${word}` : word;
1387
+ if (!current || estimateDisplayTextWidth(candidate, fontSize) <= maxWidth) {
1388
+ current = candidate;
1389
+ } else {
1390
+ lines.push(current);
1391
+ current = word;
1392
+ }
1393
+ }
1394
+ if (current) lines.push(current);
1395
+ return lines.length > 0 ? lines : [""];
1396
+ }
1397
+ function balancedDisplayLines(line, fontSize, maxWidth) {
1398
+ const greedy = wrapDisplayLine(line, fontSize, maxWidth);
1399
+ if (greedy.length < 2) return greedy;
1400
+ let lo = Math.max(
1401
+ ...line.split(/\s+/).filter(Boolean).map((word) => estimateDisplayTextWidth(word, fontSize))
1402
+ );
1403
+ let hi = maxWidth;
1404
+ if (lo >= hi) return greedy;
1405
+ while (hi - lo > 0.5) {
1406
+ const mid = (lo + hi) / 2;
1407
+ if (wrapDisplayLine(line, fontSize, mid).length <= greedy.length) hi = mid;
1408
+ else lo = mid;
1409
+ }
1410
+ return wrapDisplayLine(line, fontSize, hi);
1411
+ }
1412
+ function frostedTitleBoxWidth(lines, fontSize, textWidth) {
1413
+ const longestChars = Math.max(...lines.map((line) => line.length));
1414
+ return Math.max(textWidth, longestChars * fontSize * 0.5 + 1);
1415
+ }
1416
+
1292
1417
  // src/doc/templates/bigText.ts
1293
1418
  var BIG_TEXT_LINE_HEIGHT = 1.05;
1294
1419
  var BIG_TEXT_WIDTH_FRACTION = 0.92;
@@ -1338,6 +1463,65 @@ function fitBigTextSize(title, viewport, heightFraction = BIG_TEXT_HEIGHT_FRACTI
1338
1463
  }
1339
1464
  return Math.floor(low);
1340
1465
  }
1466
+ var BIG_TEXT_FROSTED_WIDTH_FRACTION = 0.9;
1467
+ var BIG_TEXT_FROSTED_HEIGHT_SHARE = 0.45;
1468
+ function frostedBigTextLayout(displayTitle, viewport, band = BIG_TEXT_BANDS.center) {
1469
+ const maxPlateWidth = viewport.width * BIG_TEXT_FROSTED_WIDTH_FRACTION;
1470
+ const maxPlateHeight = Math.min(
1471
+ viewport.height * band.height,
1472
+ Math.min(viewport.width, viewport.height) * BIG_TEXT_FROSTED_HEIGHT_SHARE
1473
+ );
1474
+ const wrapAt = (size) => {
1475
+ const column2 = maxPlateWidth - 2 * FROSTED_PAD_X_EM * size;
1476
+ const lines2 = displayTitle.split("\n").flatMap((line) => balancedDisplayLines(line, size, Math.max(1, column2)));
1477
+ const width2 = Math.max(...lines2.map((line) => estimateDisplayTextWidth(line, size)));
1478
+ return { lines: lines2, width: width2, column: column2 };
1479
+ };
1480
+ const fits = (size) => {
1481
+ const { lines: lines2, width: width2, column: column2 } = wrapAt(size);
1482
+ const height = (lines2.length * BIG_TEXT_LINE_HEIGHT + 2 * FROSTED_PAD_Y_EM) * size;
1483
+ return width2 <= column2 && height <= maxPlateHeight;
1484
+ };
1485
+ let low = BIG_TEXT_MIN_PX;
1486
+ let high = maxPlateHeight / (BIG_TEXT_LINE_HEIGHT + 2 * FROSTED_PAD_Y_EM);
1487
+ if (high > low && fits(low)) {
1488
+ for (let i = 0; i < 24; i += 1) {
1489
+ const mid = (low + high) / 2;
1490
+ if (fits(mid)) low = mid;
1491
+ else high = mid;
1492
+ }
1493
+ }
1494
+ const fontSize = Math.floor(low);
1495
+ const { lines, width, column } = wrapAt(fontSize);
1496
+ const textWidth = Math.min(width, column);
1497
+ const textHeight = lines.length * fontSize * BIG_TEXT_LINE_HEIGHT;
1498
+ const padX = FROSTED_PAD_X_EM * fontSize;
1499
+ const padY = FROSTED_PAD_Y_EM * fontSize;
1500
+ const plateWidth = textWidth + 2 * padX;
1501
+ const plateHeight = textHeight + 2 * padY;
1502
+ const cx = viewport.width / 2;
1503
+ const cy = viewport.height * band.y;
1504
+ return {
1505
+ fontSize,
1506
+ lines,
1507
+ plate: {
1508
+ x: cx - plateWidth / 2,
1509
+ y: cy - plateHeight / 2,
1510
+ width: plateWidth,
1511
+ height: plateHeight,
1512
+ radius: FROSTED_RADIUS_EM * fontSize
1513
+ },
1514
+ // As on the frosted cover, the box spans the padding: shrink-to-fit measures the
1515
+ // font's whole ascent and descent, more than a line of display type.
1516
+ title: {
1517
+ cx,
1518
+ cy,
1519
+ width: frostedTitleBoxWidth(lines, fontSize, textWidth),
1520
+ height: plateHeight
1521
+ },
1522
+ baselineShift: opticalCentreShiftEm(lines)
1523
+ };
1524
+ }
1341
1525
  function bigTextBloom(background, centerY = 0.5) {
1342
1526
  return `radial-gradient(ellipse at 50% ${Math.round(centerY * 100)}%, ${withAlpha(background, 0.92)} 0%, ${withAlpha(background, 0.78)} 38%, ${withAlpha(background, 0.32)} 66%, ${withAlpha(background, 0)} 88%)`;
1343
1527
  }
@@ -1347,27 +1531,28 @@ function bigText(input, context) {
1347
1531
  const band = BIG_TEXT_BANDS[input.placement ?? "center"] ?? BIG_TEXT_BANDS.center;
1348
1532
  const treatment = themedImageTreatment(context, input.imageTreatment);
1349
1533
  const displayTitle = title.toUpperCase();
1534
+ const frosted = input.backdrop === "frosted" && !!imageSrc;
1350
1535
  const titleFontSize = fitBigTextSize(displayTitle, viewport, band.height);
1351
1536
  const layers = [];
1352
1537
  if (imageSrc) {
1353
- layers.push(
1354
- {
1355
- type: "image",
1356
- id: "bigtext-image",
1357
- content: {
1358
- src: imageSrc,
1359
- alt: imageAlt || title,
1360
- fit: "cover",
1361
- ...treatment ? { treatment } : {}
1362
- },
1363
- position: { x: 0, y: 0, width: "100%", height: "100%" },
1364
- animation: mapAmbientMotion(ambientMotion, 8) ?? {
1365
- type: "slowZoom",
1366
- duration: 8,
1367
- direction: "in"
1368
- }
1538
+ layers.push({
1539
+ type: "image",
1540
+ id: "bigtext-image",
1541
+ content: {
1542
+ src: imageSrc,
1543
+ alt: imageAlt || title,
1544
+ fit: "cover",
1545
+ ...treatment ? { treatment } : {}
1369
1546
  },
1370
- {
1547
+ position: { x: 0, y: 0, width: "100%", height: "100%" },
1548
+ animation: mapAmbientMotion(ambientMotion, 8) ?? {
1549
+ type: "slowZoom",
1550
+ duration: 8,
1551
+ direction: "in"
1552
+ }
1553
+ });
1554
+ if (!frosted) {
1555
+ layers.push({
1371
1556
  type: "shape",
1372
1557
  id: "bigtext-bloom",
1373
1558
  content: {
@@ -1375,8 +1560,8 @@ function bigText(input, context) {
1375
1560
  fill: bigTextBloom(theme.colors.background, band.y)
1376
1561
  },
1377
1562
  position: { x: 0, y: 0, width: "100%", height: "100%" }
1378
- }
1379
- );
1563
+ });
1564
+ }
1380
1565
  } else {
1381
1566
  layers.push(
1382
1567
  {
@@ -1399,6 +1584,38 @@ function bigText(input, context) {
1399
1584
  }
1400
1585
  );
1401
1586
  }
1587
+ if (frosted) {
1588
+ const layout = frostedBigTextLayout(displayTitle, viewport, band);
1589
+ const lightText = frostedUnderLightText(context);
1590
+ layers.push(frostedPlateLayer("bigtext-plate", layout.plate, context), {
1591
+ type: "text",
1592
+ id: "bigtext-title",
1593
+ content: {
1594
+ text: layout.lines.join("\n"),
1595
+ style: {
1596
+ fontSize: layout.fontSize,
1597
+ fontFamily: getThemeFont(context, "title"),
1598
+ fontWeight: "bold",
1599
+ color: theme.colors.text,
1600
+ textAlign: "center",
1601
+ verticalAlign: "middle",
1602
+ lineHeight: BIG_TEXT_LINE_HEIGHT,
1603
+ baselineShift: layout.baselineShift,
1604
+ shadow: lightText,
1605
+ shrinkToFit: true
1606
+ }
1607
+ },
1608
+ position: {
1609
+ x: "50%",
1610
+ y: pct(layout.title.cy, viewport.height),
1611
+ anchor: "center",
1612
+ width: pct(layout.title.width, viewport.width),
1613
+ height: pct(layout.title.height, viewport.height)
1614
+ },
1615
+ animation: motionEntrance(context, { type: "fadeIn", duration: 1 })
1616
+ });
1617
+ return layers;
1618
+ }
1402
1619
  layers.push({
1403
1620
  type: "text",
1404
1621
  id: "bigtext-title",
@@ -7042,42 +7259,6 @@ function treeBlock(input, context) {
7042
7259
  return layers;
7043
7260
  }
7044
7261
 
7045
- // src/doc/utils/textMetrics.ts
7046
- var AVERAGE_CHAR_WIDTH_EM = 0.62;
7047
- var BOLD_CHAR_WIDTH_EM = 0.66;
7048
- function estimateTextWidth(text, fontSize, charWidthEm = AVERAGE_CHAR_WIDTH_EM) {
7049
- return text.length * fontSize * charWidthEm;
7050
- }
7051
- var DISPLAY_CHAR_EM = {
7052
- space: 0.27,
7053
- narrowLower: 0.36,
7054
- wideLower: 0.86,
7055
- lower: 0.57,
7056
- narrowUpper: 0.4,
7057
- wideUpper: 0.95,
7058
- upper: 0.7,
7059
- digit: 0.57,
7060
- punct: 0.34,
7061
- other: 0.62
7062
- };
7063
- function displayCharEm(ch) {
7064
- if (ch === " ") return DISPLAY_CHAR_EM.space;
7065
- if ("fijlrt".includes(ch)) return DISPLAY_CHAR_EM.narrowLower;
7066
- if (ch === "m" || ch === "w") return DISPLAY_CHAR_EM.wideLower;
7067
- if (ch >= "a" && ch <= "z") return DISPLAY_CHAR_EM.lower;
7068
- if (ch === "I" || ch === "J") return DISPLAY_CHAR_EM.narrowUpper;
7069
- if (ch === "M" || ch === "W") return DISPLAY_CHAR_EM.wideUpper;
7070
- if (ch >= "A" && ch <= "Z") return DISPLAY_CHAR_EM.upper;
7071
- if (ch >= "0" && ch <= "9") return DISPLAY_CHAR_EM.digit;
7072
- if (".,:;-/'\u2019!?()".includes(ch)) return DISPLAY_CHAR_EM.punct;
7073
- return DISPLAY_CHAR_EM.other;
7074
- }
7075
- function estimateDisplayTextWidth(text, fontSize) {
7076
- let em = 0;
7077
- for (const ch of text.normalize("NFD").replace(/\p{M}/gu, "")) em += displayCharEm(ch);
7078
- return em * fontSize;
7079
- }
7080
-
7081
7262
  // src/doc/templates/timelineBlock.ts
7082
7263
  var clamp01 = (value) => Math.max(0, Math.min(1, value));
7083
7264
  var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
@@ -10153,78 +10334,125 @@ function coverScrimGradient(background) {
10153
10334
  const stops = COVER_SCRIM_STOPS.map((s) => `${withAlpha(background, s.alpha)} ${s.pos}%`);
10154
10335
  return `linear-gradient(0deg, ${stops.join(", ")})`;
10155
10336
  }
10337
+ function fitFrostedText(text, size, minSize, maxPlateWidth, maxLines) {
10338
+ const wrapAt = (s) => {
10339
+ const column = maxPlateWidth - 2 * FROSTED_PAD_X_EM * s;
10340
+ const lines = text.split("\n").flatMap((line) => balancedDisplayLines(line, s, column));
10341
+ const width = Math.max(...lines.map((line) => estimateDisplayTextWidth(line, s)));
10342
+ return { lines, width, column };
10343
+ };
10344
+ let fontSize = size;
10345
+ let wrapped = wrapAt(fontSize);
10346
+ while ((wrapped.lines.length > maxLines || wrapped.width > wrapped.column) && fontSize > minSize) {
10347
+ fontSize = Math.max(minSize, Math.floor(fontSize * 0.95));
10348
+ wrapped = wrapAt(fontSize);
10349
+ }
10350
+ return {
10351
+ fontSize,
10352
+ lines: wrapped.lines,
10353
+ column: wrapped.column,
10354
+ textWidth: Math.min(wrapped.width, wrapped.column)
10355
+ };
10356
+ }
10156
10357
  var FROSTED_LINE_HEIGHT = 1.12;
10157
10358
  var FROSTED_MAX_LINES = 3;
10158
10359
  var FROSTED_SQUARE_SHARE = 0.9;
10159
- var FROSTED_PAD_X_EM = 0.45;
10160
- var FROSTED_PAD_Y_EM = 0.3;
10161
- var FROSTED_RADIUS_EM = 0.3;
10162
- var FROSTED_BLUR_SHARE = 0.0167;
10163
10360
  var FROSTED_BOTTOM_MARGIN_PCT = { wide: 7, tall: 17 };
10164
- var FROSTED_TINT_UNDER_LIGHT_TEXT = "rgba(12, 12, 14, 0.26)";
10165
- var FROSTED_TINT_UNDER_DARK_TEXT = "rgba(250, 250, 250, 0.5)";
10166
- var FROSTED_BACKDROP_CONTRAST = 0.65;
10361
+ var FROSTED_TOP_MARGIN_PCT = FROSTED_BOTTOM_MARGIN_PCT;
10167
10362
  var COVER_SUBTITLE_BASE_PX = 40;
10168
- var INK_EM = { xHeight: 0.45, ascent: 0.71, accentedCapital: 0.8, descent: 0.165 };
10169
- function opticalCentreShiftEm(lines) {
10170
- const first = (lines[0] ?? "").normalize("NFD");
10171
- const last = lines[lines.length - 1] ?? "";
10172
- const top = /[A-Z]\p{M}/u.test(first) ? INK_EM.accentedCapital : /[A-Zbdfhklt0-9]|[a-z]\p{M}/u.test(first) ? INK_EM.ascent : INK_EM.xHeight;
10173
- const bottom = /[gjpqyQ,;]/.test(last) ? INK_EM.descent : 0;
10174
- return Math.round(((top - bottom) / 2 - INK_EM.xHeight / 2) * 1e3) / 1e3;
10175
- }
10176
- function wrapDisplayLine(line, fontSize, maxWidth) {
10177
- const lines = [];
10178
- let current = "";
10179
- for (const word of line.trim().split(/\s+/).filter(Boolean)) {
10180
- const candidate = current ? `${current} ${word}` : word;
10181
- if (!current || estimateDisplayTextWidth(candidate, fontSize) <= maxWidth) {
10182
- current = candidate;
10183
- } else {
10184
- lines.push(current);
10185
- current = word;
10186
- }
10187
- }
10188
- if (current) lines.push(current);
10189
- return lines.length > 0 ? lines : [""];
10190
- }
10191
- function balancedDisplayLines(line, fontSize, maxWidth) {
10192
- const greedy = wrapDisplayLine(line, fontSize, maxWidth);
10193
- if (greedy.length < 2) return greedy;
10194
- let lo = Math.max(
10195
- ...line.split(/\s+/).filter(Boolean).map((word) => estimateDisplayTextWidth(word, fontSize))
10196
- );
10197
- let hi = maxWidth;
10198
- if (lo >= hi) return greedy;
10199
- while (hi - lo > 0.5) {
10200
- const mid = (lo + hi) / 2;
10201
- if (wrapDisplayLine(line, fontSize, mid).length <= greedy.length) hi = mid;
10202
- else lo = mid;
10203
- }
10204
- return wrapDisplayLine(line, fontSize, hi);
10205
- }
10206
- function frostedCoverLayout(title, subtitle, context) {
10363
+ var COVER_DECK_BASE_PX = 114;
10364
+ var FROSTED_SPLIT_TITLE_SCALE = 1.5;
10365
+ var FROSTED_DECK_MAX_LINES = 3;
10366
+ var FROSTED_SPLIT_MIN_GAP_EM = 0.5;
10367
+ function frostedCoverLayout(title, subtitle, context, coverLayout = "stacked") {
10207
10368
  const { width: frameW, height: frameH } = context.viewport;
10208
10369
  const shortSide = Math.min(frameW, frameH);
10209
10370
  const maxPlateWidth = FROSTED_SQUARE_SHARE * shortSide;
10210
10371
  const minSize = themedFontSize(COVER_TITLE_MIN_BASE_PX, context, true);
10211
- const wrapAt = (size) => {
10212
- const column2 = maxPlateWidth - 2 * FROSTED_PAD_X_EM * size;
10213
- const lines2 = title.split("\n").flatMap((line) => balancedDisplayLines(line, size, column2));
10214
- const width = Math.max(...lines2.map((line) => estimateDisplayTextWidth(line, size)));
10215
- return { lines: lines2, width, column: column2 };
10372
+ const tall = frameH > frameW;
10373
+ const marginPct = tall ? FROSTED_BOTTOM_MARGIN_PCT.tall : FROSTED_BOTTOM_MARGIN_PCT.wide;
10374
+ const plateBottom = frameH * (1 - marginPct / 100);
10375
+ const cx = frameW / 2;
10376
+ const fitTitle = (scale) => {
10377
+ const fit = fitFrostedText(
10378
+ title,
10379
+ Math.round(scale * themedFontSize(fitCoverTitleSize(title), context, true)),
10380
+ minSize,
10381
+ maxPlateWidth,
10382
+ FROSTED_MAX_LINES
10383
+ );
10384
+ const textHeight2 = fit.lines.length * fit.fontSize * FROSTED_LINE_HEIGHT;
10385
+ const padX2 = FROSTED_PAD_X_EM * fit.fontSize;
10386
+ const padY2 = FROSTED_PAD_Y_EM * fit.fontSize;
10387
+ return {
10388
+ ...fit,
10389
+ textHeight: textHeight2,
10390
+ padX: padX2,
10391
+ padY: padY2,
10392
+ radius: FROSTED_RADIUS_EM * fit.fontSize,
10393
+ // The box spans the plate's padding too: shrink-to-fit measures the font's whole
10394
+ // ascent and descent (about 1.3 em), which overflows a box of 1.12 em lines and
10395
+ // would scale every title down by up to 14%.
10396
+ box: (plateY2) => ({
10397
+ cx,
10398
+ cy: plateY2 + padY2 + textHeight2 / 2,
10399
+ width: frostedTitleBoxWidth(fit.lines, fit.fontSize, fit.textWidth),
10400
+ height: textHeight2 + 2 * padY2
10401
+ })
10402
+ };
10216
10403
  };
10217
- let fontSize = themedFontSize(fitCoverTitleSize(title), context, true);
10218
- let wrapped = wrapAt(fontSize);
10219
- while ((wrapped.lines.length > FROSTED_MAX_LINES || wrapped.width > wrapped.column) && fontSize > minSize) {
10220
- fontSize = Math.max(minSize, Math.floor(fontSize * 0.95));
10221
- wrapped = wrapAt(fontSize);
10404
+ if (coverLayout === "split" && subtitle?.trim()) {
10405
+ const { fontSize: fontSize2, lines: lines2, textWidth: textWidth2, textHeight: textHeight2, padX: padX2, padY: padY2, radius: radius2, box: box2 } = fitTitle(FROSTED_SPLIT_TITLE_SCALE);
10406
+ const deckMin = themedFontSize(COVER_SUBTITLE_BASE_PX, context, false);
10407
+ const deck = fitFrostedText(
10408
+ subtitle.trim(),
10409
+ Math.max(deckMin, themedFontSize(COVER_DECK_BASE_PX, context, true)),
10410
+ deckMin,
10411
+ maxPlateWidth,
10412
+ FROSTED_DECK_MAX_LINES
10413
+ );
10414
+ const deckPadX = FROSTED_PAD_X_EM * deck.fontSize;
10415
+ const deckHeight = deck.lines.length * deck.fontSize * FROSTED_LINE_HEIGHT + 2 * FROSTED_PAD_Y_EM * deck.fontSize;
10416
+ const deckWidth = deck.textWidth + 2 * deckPadX;
10417
+ const topPct = tall ? FROSTED_TOP_MARGIN_PCT.tall : FROSTED_TOP_MARGIN_PCT.wide;
10418
+ const titlePlate = {
10419
+ x: cx - (textWidth2 + 2 * padX2) / 2,
10420
+ y: frameH * topPct / 100,
10421
+ width: textWidth2 + 2 * padX2,
10422
+ height: textHeight2 + 2 * padY2,
10423
+ radius: radius2
10424
+ };
10425
+ const deckPlate = {
10426
+ x: cx - deckWidth / 2,
10427
+ y: plateBottom - deckHeight,
10428
+ width: deckWidth,
10429
+ height: deckHeight,
10430
+ // The title's corners, so the two plates read as a pair.
10431
+ radius: Math.min(radius2, deckHeight / 2)
10432
+ };
10433
+ const gap = deckPlate.y - (titlePlate.y + titlePlate.height);
10434
+ if (gap >= FROSTED_SPLIT_MIN_GAP_EM * fontSize2) {
10435
+ return {
10436
+ fontSize: fontSize2,
10437
+ lines: lines2,
10438
+ plate: titlePlate,
10439
+ title: box2(titlePlate.y),
10440
+ subtitle: {
10441
+ cx,
10442
+ cy: deckPlate.y + deckHeight / 2,
10443
+ width: frostedTitleBoxWidth(deck.lines, deck.fontSize, deck.textWidth),
10444
+ height: deckHeight,
10445
+ fontSize: deck.fontSize,
10446
+ lines: deck.lines,
10447
+ baselineShift: opticalCentreShiftEm(deck.lines)
10448
+ },
10449
+ subtitlePlate: deckPlate,
10450
+ baselineShift: opticalCentreShiftEm(lines2),
10451
+ blur: frostedBlurPx(context)
10452
+ };
10453
+ }
10222
10454
  }
10223
- const { lines, column } = wrapped;
10224
- const textWidth = Math.min(wrapped.width, column);
10225
- const textHeight = lines.length * fontSize * FROSTED_LINE_HEIGHT;
10226
- const padX = FROSTED_PAD_X_EM * fontSize;
10227
- const padY = FROSTED_PAD_Y_EM * fontSize;
10455
+ const { fontSize, lines, column, textWidth, textHeight, padX, padY, radius, box } = fitTitle(1);
10228
10456
  let sub;
10229
10457
  if (subtitle?.trim()) {
10230
10458
  const subSize = themedFontSize(COVER_SUBTITLE_BASE_PX, context, false);
@@ -10242,12 +10470,7 @@ function frostedCoverLayout(title, subtitle, context) {
10242
10470
  }
10243
10471
  const plateWidth = Math.max(textWidth, sub?.width ?? 0) + 2 * padX;
10244
10472
  const plateHeight = textHeight + (sub ? sub.gap + sub.height : 0) + 2 * padY;
10245
- const marginPct = frameH > frameW ? FROSTED_BOTTOM_MARGIN_PCT.tall : FROSTED_BOTTOM_MARGIN_PCT.wide;
10246
- const plateBottom = frameH * (1 - marginPct / 100);
10247
10473
  const plateY = plateBottom - plateHeight;
10248
- const cx = frameW / 2;
10249
- const longestChars = Math.max(...lines.map((line) => line.length));
10250
- const titleBoxWidth = Math.max(textWidth, longestChars * fontSize * 0.5 + 1);
10251
10474
  return {
10252
10475
  fontSize,
10253
10476
  lines,
@@ -10256,17 +10479,9 @@ function frostedCoverLayout(title, subtitle, context) {
10256
10479
  y: plateY,
10257
10480
  width: plateWidth,
10258
10481
  height: plateHeight,
10259
- radius: FROSTED_RADIUS_EM * fontSize
10260
- },
10261
- // The box spans the plate's padding too: shrink-to-fit measures the font's whole
10262
- // ascent and descent (about 1.3 em), which overflows a box of 1.12 em lines and
10263
- // would scale every title down by up to 14%.
10264
- title: {
10265
- cx,
10266
- cy: plateY + padY + textHeight / 2,
10267
- width: titleBoxWidth,
10268
- height: textHeight + 2 * padY
10482
+ radius
10269
10483
  },
10484
+ title: box(plateY),
10270
10485
  ...sub ? {
10271
10486
  subtitle: {
10272
10487
  cx,
@@ -10281,35 +10496,15 @@ function frostedCoverLayout(title, subtitle, context) {
10281
10496
  }
10282
10497
  } : {},
10283
10498
  baselineShift: opticalCentreShiftEm(lines),
10284
- blur: Math.round(FROSTED_BLUR_SHARE * shortSide)
10499
+ blur: frostedBlurPx(context)
10285
10500
  };
10286
10501
  }
10287
- function pct(value, total) {
10288
- return `${Math.round(value / total * 1e4) / 100}%`;
10289
- }
10290
10502
  function frostedCoverLayers(input, context) {
10291
10503
  const { theme, viewport } = context;
10292
- const layout = frostedCoverLayout(input.title, input.subtitle, context);
10293
- const lightText = relativeLuminance(theme.colors.text) > 0.5;
10294
- const { plate } = layout;
10504
+ const layout = frostedCoverLayout(input.title, input.subtitle, context, input.coverLayout);
10505
+ const lightText = frostedUnderLightText(context);
10295
10506
  const layers = [
10296
- {
10297
- type: "shape",
10298
- id: "cover-plate",
10299
- content: {
10300
- shape: "rect",
10301
- fill: lightText ? FROSTED_TINT_UNDER_LIGHT_TEXT : FROSTED_TINT_UNDER_DARK_TEXT,
10302
- borderRadius: Math.round(plate.radius),
10303
- backdropBlur: layout.blur,
10304
- ...lightText ? { backdropContrast: FROSTED_BACKDROP_CONTRAST } : {}
10305
- },
10306
- position: {
10307
- x: pct(plate.x, viewport.width),
10308
- y: pct(plate.y, viewport.height),
10309
- width: pct(plate.width, viewport.width),
10310
- height: pct(plate.height, viewport.height)
10311
- }
10312
- },
10507
+ frostedPlateLayer("cover-plate", layout.plate, context),
10313
10508
  {
10314
10509
  type: "text",
10315
10510
  id: "cover-title",
@@ -10338,7 +10533,35 @@ function frostedCoverLayers(input, context) {
10338
10533
  animation: getMotionProfile(context).coverWordReveal ? { type: "wordReveal", duration: 0.5, delay: 0.1, stagger: 0.12 } : { type: "fadeIn", duration: 0.8 }
10339
10534
  }
10340
10535
  ];
10341
- if (layout.subtitle) {
10536
+ if (layout.subtitle && layout.subtitlePlate) {
10537
+ layers.push(frostedPlateLayer("cover-subtitle-plate", layout.subtitlePlate, context), {
10538
+ type: "text",
10539
+ id: "cover-subtitle",
10540
+ content: {
10541
+ text: layout.subtitle.lines.join("\n"),
10542
+ style: {
10543
+ fontSize: layout.subtitle.fontSize,
10544
+ fontFamily: getThemeFont(context, "title"),
10545
+ fontWeight: "bold",
10546
+ color: theme.colors.text,
10547
+ textAlign: "center",
10548
+ verticalAlign: "middle",
10549
+ lineHeight: FROSTED_LINE_HEIGHT,
10550
+ baselineShift: layout.subtitle.baselineShift,
10551
+ shadow: lightText,
10552
+ shrinkToFit: true
10553
+ }
10554
+ },
10555
+ position: {
10556
+ x: "50%",
10557
+ y: pct(layout.subtitle.cy, viewport.height),
10558
+ anchor: "center",
10559
+ width: pct(layout.subtitle.width, viewport.width),
10560
+ height: pct(layout.subtitle.height, viewport.height)
10561
+ },
10562
+ animation: motionEntrance(context, { type: "fadeIn", duration: 0.8, delay: 0.2 })
10563
+ });
10564
+ } else if (layout.subtitle) {
10342
10565
  layers.push({
10343
10566
  type: "text",
10344
10567
  id: "cover-subtitle",
@@ -10530,7 +10753,8 @@ function startBlockToCoverInput(config) {
10530
10753
  ambientMotion: config.ambientMotion,
10531
10754
  heroCredit: config.heroCredit,
10532
10755
  heroLicense: config.heroLicense,
10533
- ...config.coverStyle ? { coverStyle: config.coverStyle } : {}
10756
+ ...config.coverStyle ? { coverStyle: config.coverStyle } : {},
10757
+ ...config.coverLayout ? { coverLayout: config.coverLayout } : {}
10534
10758
  };
10535
10759
  }
10536
10760
  function expandCoverBlock(config, context, template = "cover") {
@@ -12338,6 +12562,7 @@ export {
12338
12562
  adjustY,
12339
12563
  sectionHeader,
12340
12564
  fitBigTextSize,
12565
+ frostedBigTextLayout,
12341
12566
  bigText,
12342
12567
  contentBlock,
12343
12568
  parseStatNumber,