javascript 我怎样才能让这个文本布局算法正确地将文本缩放到“字体大小”选项(用SVG路径制作的文本)?

izkcnapc  于 2023-04-19  发布在  Java
关注(0)|答案(1)|浏览(158)

我有这个JS中的完整可运行代码在这篇文章的底部,根据“拉伸”文本设计显示希伯来语文本的布局,如here所述。它看起来像这样在几个不同的字体大小:

32

它在这个“比例”下看起来最好(你可以编辑下面代码片段顶部的16,它写着render(words, 16))。

16

它在这个规模上崩溃了,它在几个方面没有正确地布局。

下面是一个演示,展示了一个SVG图像上的边界框(理论上SVG是每行生成的,但请注意,它们不在一行上)。

问题

我如何让这个布局在不同的“字体大小”正确?(字体大小是您可以更改的16设置)。为什么我不能在SVG的每一行上说<svg width="100%" height={fontSize}>,它只会占用整个父宽度?这样,如果屏幕是800px,字体大小是16,它将是长行的小文本,而在48px,这将是800px宽的“窄”行(因为字体大得多),和更多的行。我不明白为什么SVG不像我期望的那样工作。
主要的问题似乎归结为字母位置(在X轴上)没有被正确计算,那么如何解决这个问题呢?像RLETTER_OFFSET这样的值我不确定这些“神奇值”是从哪里来的。
P.S.我从多年前的JavaScript代码中“移植”了这个文本布局代码到TypeScript,我不确定是我最初写的还是其他人写的。我似乎在网上找不到任何地方可以参考它,所以我不确定。

可运行示例

这里是完整的代码和演示数据。Here是更多的chapter数据,如果你想在更多的文本上测试它(并不都符合StackOverflow的字符限制)。我认为LETTER_OFFSETSPACE_OFFSET需要基于父宽度以某种方式动态计算,而不是硬编码。注意,如果更改这些硬编码值,字母有时会由于某种原因重叠,这使我相信字母布局计算(在x轴上)没有正确计算。

// this is the main drawing function,
// called at the very end.
function draw() {
  const words = []

  chapter.verses.forEach(v => {
    words.push(
      ...String(v.value[0].text)
        .split(/\s+/)
        .map(word => {
          return [...word].filter(c => ALPHABET.includes(c)).join("")
        })
    )
  })

  const lines16 = render(words, 16)
  lines16.forEach(line => document.querySelector(".container-16")?.appendChild(line))
  
  const lines32 = render(words, 32)
  lines32.forEach(line => document.querySelector(".container-32")?.appendChild(line))
}

const chapter = {
  verses: [
    {
      index: 0,
      type: "verse",
      value: [
        {
          language: "hebrew",
          script: "hebrew",
          text: "ויהיו חיי שרה מאה שנה ועשרים שנה ושבע שנים שני חיי שרה׃"
        }
      ]
    },
    {
      index: 1,
      type: "verse",
      value: [
        {
          language: "hebrew",
          script: "hebrew",
          text:
            "ותמת שרה בקרית ארבע הוא חברון בארץ כנען ויבא אברהם לספד לשרה ולבכתה׃"
        }
      ]
    }
  ]
}

let measurer

function addToMeasurer(el) {
  initializeMeasurer()

  if (!measurer) {
    return
  }

  measurer.appendChild(el)
}

function initializeMeasurer() {
  if (measurer) {
    return
  }

  measurer = document.createElement("span")
  measurer.setAttribute("id", "measurer")

  measurer.style.position = "absolute"
  measurer.style.height = "auto"
  measurer.style.width = "auto"
  measurer.style.whiteSpace = "nowrap"
  document.body.appendChild(measurer)
}

function removeFromMeasurer(el) {
  if (!measurer) {
    return
  }

  el.parentNode?.removeChild(el)
}

function setMeasurerFont({ family, size }) {
  if (!measurer) {
    return
  }

  if (family) {
    measurer.style.fontFamily = family
  }

  if (size) {
    measurer.style.fontSize = typeof size === "number" ? `${size}px` : size
  }
}

const GLYPHS = {
  // horiz-adv-x="646"
  "\u05c6": `M62.5 425q7.5 70 40 139.5t72.5 151t110 223.5l-184 1q30 211 0 418l148 -1q3 40 -8 75.5t-21.5 62.5t-40.5 89h-23l6 60h74l3 -60h-23q27 -37 37.5 -63.5t22 -62.5t19.5 -101l35 7l-3 274h-23l4 60h76l4 -60h-23l4 -280h25q13 76 26.5 109t17 50t36.5 55h-24l4 70h73 l6 -70h-23q-25 -57 -34.5 -85.5t-20 -57.5t-17.5 -75l160 -1q-20 -241 -8 -425l-204 10q-70 -154 -83.5 -198t-33.5 -128t-13 -138t53 -54h321q1 -210 32 -420h-303q-223 30 -263.5 119t-40.5 162.5t7.5 143.5z`,
  א: `M1110 0q-118 13 -157 59.5t-156 191.5l-521 541q-16 -93 -6 -277v-187q306 5 306 -98l-10 -84q-26 -73 -42 -104.5t-84 -181.5l-50 -14q59 124 18 144l-118 20q-71 1 -82 -67l-22 6l-2 67v45l1 78q0 35 10.5 146.5t0.5 199.5q-5 164 19 367l-155 187l33 426l-16 129
l36 -10l32 -152q52 -24 573 -628q101 87 159 168l-248 258l172 247l15 127l24 -4l10 -151l13 -19q199 -164 189 -278q-10 -140 -51 -161l-224 -243l152 -168l173 -150l50 83l19 -15l-35 -98z`,
  ב: (p = 0) => `
M ${1242 + p} 421
L ${1218 + p} 0
H 69
L 83 211
L 77 422
L ${1032 + p} 424
L ${1015 + p} 906
L 67 908
L 69 1352
L 49 1464
L 86 1454
L 117 1354
L 179 1355
L 178 1624
H 153
V 1693
L 225 1694
V 1624
H 208
L 223 1358
H ${1113 + p}
L ${1143 + p} 1428
H ${1178 + p}
L ${1153 + p} 1358
L ${1148 + p} 936
L ${1142 + p} 420
L ${1202 + p} 421
L ${1237 + p} 511
L ${1270 + p} 501
Z
`,
  ג: `M686 248q0 -248 -29 -248q-20 0 -22 27q-5 47 -63 232q-54 169 -102 389q-50 116 -91 229l-22 71l-262 -4q22 205 4 409l157 1q-8 73 -18 102.5t-17 53.5t-34 74h-25l5 50h74l4 -50h-27q36 -56 40 -79.5t15 -53.5t20 -93h39l-2 280h-24l6 60h68l8 -60h-24l6 -280h37
q16 76 27.5 101.5t22 49.5t40.5 69h-24l6 60h66l3 -60h-17q-29 -50 -43.5 -77.5t-24.5 -53.5t-19 -89h172q-30 -203 0 -407l-172 -3l60 -127q20 -59 57.5 -113.5t79.5 -145.5q45 -223 45 -314zM497 550q-13 -404 -68 -459.5t-150 -86.5l-104 -1l-170 -3q40 224 60 488
q65 -64 155 -58t179 57z`,
  ד: (p = 0) => `
M ${1255 + p} 935
L ${1117 + p} 928
L ${1156 + p} 613
L ${1230 + p} 356
Q ${1265 + p} 142 ${1209 + p} 71
T ${1137 + p} 0
Q ${1090 + p} 0 ${1090 + p} 43
L ${1007 + p} 642
L ${988 + p} 930
L 109 926
Q 136 1102 116 1353
L 106 1444
H 134
L 159 1354
L 240 1359
L 242 1624
H 220
V 1684
L 300 1683
V 1622
L 274 1624
L 281 1358
H ${1225 + p}
L ${1260 + p} 1458
L ${1292 + p} 1448
L ${1274 + p} 1358
Q ${1230 + p} 1178 ${1255 + p} 935
Z
`,
  ה: (p = 0) => `
M ${1260 + p} 1168
L ${1257 + p} 930
L ${1127 + p} 928
L ${1185 + p} 645
Q ${1242 + p} 393 ${1214 + p} 164
Q ${1215 + p} 142 ${1159 + p} 71
T ${1087 + p} 0
Q ${1050 + p} 0 ${1050 + p} 43
Q ${1050 + p} 219 ${987 + p} 926
L 128 930
L 126 1353
L 106 1454
H 144
L 172 1354
H 249
L 247 1602
L 226 1604
V 1680
L 313 1690
L 315 1604
H 290
L 291 1358
L ${1211 + p} 1356
L ${1250 + p} 1458
H ${1292 + p}
L ${1254 + p} 1358
Z
M 578 327
L 486 0
Q 317 20 237 73
T 138 331
Q 140 400 151 439
T 177 541.5
A 451.24 451.24 0 0 0 223 660
L 250 658
Q 242 587 246.5 548.5
T 282.5 457
Q 314 404 578 327
Z
`,
  ו: `M567 164q0 -23 -36.5 -93.5t-62.5 -70.5q-44 0 -44 43l9 768q0 117 -87 117l-240 -6q2 255 -23 426l282 10q200 0 200 -297l11 -448q21 -205 -9 -449z`,
  ז: `M637 941l-218 -3q41 -101 87 -280.5t36 -278.5q0 -113 -24 -249q-23 -130 -53 -130q-52 0 -52 23q0 -2 -3.5 49t-3.5 88q0 21 -30 177t-40 312q-10 73 -5 134t-11 161l-217 -1q7 184 -11 416l157 5q2 64 -3.5 90t-25.5 68.5t-40 71.5h-12l-2 60h67l4 -60h-16
q19 -26 34 -64.5t23.5 -70t16.5 -96.5l49 -2l10 288h-14l3 50h63l-4 -50h-14l3 -291l49 1q14 60 29.5 105.5t25 65t44.5 69.5h-15l5 60h55l2 -60l-17 1q-29 -55 -40 -72.5t-26 -65.5t-15 -102h149q-10 -283 0 -419z`,
  ח: (p = 0) => `
M1279 928h-178q47 -387 48.5 -476t-11.5 -199.5t-54 -181.5t-57 -71q-57 0 -57 43q-30 176 -26.5 353.5t59.5 531.5h-177l-17 296l-118 232l-78 -198l-12 -326l-193 -5q42 -341 37 -479.5t-11.5 -293t-67.5 -154.5q-39 0 -39 41q-73 258 -71.5 415.5t44.5 471.5l-186 4
q12 190 -4 424q6 118 4 118h20l40 -116l438 6l99 252l111 -258h462q-14 -160 -5 -430z
`,
  ט: `M1163.5 314.5q-109.5 -254.5 -293.5 -314.5h-235h-339l-64 956l-117 3q22 186 -6 393l125 2q-5 69 -9 98.5t-10 62.5t-35 79h-14l-1 60h64l3 -60h-17q29 -45 33 -73.5t9 -61.5t4 -105l40 -1l2 290l-14 -1l-4 60h65l3 -60h-17l2 -290h28q4 84 7.5 100t8.5 47.5t35 90.5
l-18 -1l-3 67l72 4l-1 -70l-14 -1q-26 -55 -31 -78t-11.5 -59.5t-0.5 -99.5h97q-5 -179 7 -399l-163 -4l49 -539l501 -20q147 0 185 90.5t53 205t-54 159.5q-19 95 -152 110t-241 -32q-44 -38 -57 -70t-49 -37l64 484q86 56 254.5 48t228 -100.5t91 -186.5t41.5 -166.5
t9 -199t-110.5 -381z`,
  י: `M580 1031q0 -147 -80 -392q-51 -191 -112 -191q-28 0 -33 15v32q50 146 65.5 247.5t15.5 98.5q0 85 -136 87h-148l-25 -108l-16 2q-9 250 -9 371l-10 401h30l40 -236h149q267 0 269 -327z`,
  ך: `M1250 1158q5 -227 5 -230l32 -1513l-97 13q-79 1366 -146 1433t-185 67h-684q21 194 11 310t-12 116l941 4q59 0 97 -47t38 -153z`,
  כ: `M1198 678q0 -392 -168 -544q-148 -134 -500 -134h-501q22 210 4 420h669q171 0 285 54q78 118 78 214q0 240 -514 240t-514 4q22 210 12 316t-13 106l699 4q246 0 352 -150q111 -156 111 -530z`,
  ל: (p = 0) => `
M ${1228 + p} 782
Q ${1228 + p} 560 ${1183.5 + p} 410.5
T ${1083.5 + p} 172
Q ${1028 + p} 83 ${953 + p} 41
T ${778 + p} -5
L ${530 + p} 7
Q ${549 + p} 187 ${536 + p} 393
L ${887 + p} 401
Q ${984 + p} 407 ${1027.5 + p} 441
T ${1086.5 + p} 540.5
Q ${1102 + p} 606 ${1106 + p} 701
Q ${1098 + p} 788 ${1083 + p} 818
T ${1025 + p} 887
Q ${982 + p} 926 ${889 + p} 926
L 264 936
Q 255 1020 255 1077.5
T 225 1396
L 213 1786
Q 213 1821 145 1818
L 101 1729
H 95
Q 113 1910 99 2039
L 90 2136
L 98 2142
L 128 2043
H 152
L 189 2163
H 199
L 202 2045
L 269 2032
Q 305 2023 311 2012
Q 325 1997 325 1941
A 827.14 827.14 0 0 0 320 1854
L 360 1359
L ${671 + p} 1358
Q ${833 + p} 1358 ${943 + p} 1294.5
T ${1116.5 + p} 1114
Q ${1180 + p} 997 ${1228 + p} 782
Z
`,
  ם: `M378 924l8 -504h707l-7 397q-2 69 -39 92q-30 19 -106 19zM1231 0h-994l-11 928q-83 3 -101 7q14 190 4 304.5t-13 114.5l836 4q279 0 279 -297v-1061z`,
  מ: `M1103 0h-585l-5 420h514q37 83 37 151q30 146 -63 252.5t-288 106.5q-208 30 -227 -110t-39 -277q0 -6 -21.5 -85t-41.5 -139q-50 -197 -179 -319q-39 0 -39 41l195 859q5 45 -1.5 63t-50.5 46l-196 57q29 182 44 418q145 -18 282 -45q71 -24 96 -61t40 -101.5t10 -119.5
q-71 -189 5 -146q65 102 80 141t21.5 108t6.5 162l153 -4q43 0 129 -44q89 -50 136 -123q113 -153 113 -447q-1 -75 -1 -184.5t-125 -619.5z`,
  ן: `M567 905l-192 5q20 -151 35 -246l50 -479q-6 -269 -12.5 -444.5t-28 -245.5t-37.5 -70q-44 0 -44 43q-80 847 -75 1055t-11 389l-172 1q13 216 -1 426l139 3q2 49 -2 74t-9 48.5t-29 87.5h-26l3 70h79l-2 -69l-25 -1q33 -63 36.5 -84.5t7.5 -49.5t13 -79l31 -2l12 281
h-25l3 80l80 -1l-2 -78h-24l1 -279l32 1q11 60 15 78t13.5 49.5t43.5 83.5h-25v71h81l3 -70h-27q-29 -52 -38 -75t-17.5 -50.5t-8.5 -83.5l157 1q-12 -203 -2 -440z`,
  נ: `M621.5 119q-40.5 -89 -263.5 -119h-303q31 210 32 420h321q46 0 53 54t-13 138t-33.5 128t-83.5 198l-204 -10q12 184 -8 425l160 1q-7 46 -17.5 75t-20 57.5t-34.5 85.5h-23l6 70h73l4 -70h-24q33 -38 36.5 -55t17 -50t26.5 -109h25l4 280h-23l4 60h76l4 -60h-23
l-3 -274l35 -7q8 65 19.5 101t22 62.5t37.5 63.5h-23l3 60h74l6 -60h-23q-30 -62 -40.5 -89t-21.5 -62.5t-8 -75.5l148 1q-30 -207 0 -418l-184 -1q70 -142 110 -223.5t72.5 -151t40 -139.5t7.5 -143.5t-40.5 -162.5z`,
  ס: `M513 928l-125 -5q9 -149 9 -173q-10 -174 63 -260q97 -81 113 -81h119q172 0 213 35q100 52 125 108t35 126q0 156 -54 183q-83 77 -289 67h-209zM1198 678q0 -312 -127 -495q-145 -210 -444 -210q-105 0 -232 172q-114 152 -144 418q0 293 -8 365l-139 4q32 210 17 316
t-17 106l621 4q238 0 357 -178q116 -174 116 -502z`,
  ע: `M1150 1083q0 -66 -221 -583q-35 -67 -84 -146.5t-96 -146.5t-122.5 -147t-558.5 -170l15 174l-15 174l320 63l-144 631l-143 2q24 196 4 429l144 -1q-20 48 -24 76t-8 59.5t-42 84.5h-15l2 50h68l-1 -50h-16q31 -24 34.5 -54.5t12.5 -54.5t13 -108l32 -2l5 270l-16 1
l4 50l62 -2l-2 -50l-19 -2l-1 -270l25 4q10 104 25 129.5t18 45.5t22 44h-16l3 53l68 -1l-2 -52h-16q-22 -43 -30.5 -69.5t-21.5 -70t-16 -76.5l116 -2q-13 -206 4 -428l-190 -6l145 -609q267 106 323.5 192t160.5 296q9 37 -26 62l-223 120q113 188 159 391l87 -56
q197 -73 197 -244z`,
  ף: `M1193 -655q-55 0 -105 43l-30 1503q0 37 -38 37h-782l1 -342l405 -5q5 -41 5 -97q0 -85 -6 -155l-47 -4h-262q-37 4 -64.5 14t-39 28.5t-41.5 65.5t-62 330v584l-21 137l32 10l45 -146l93 10h655q288 0 289 -297z`,
  פ: `M740 449q-447 -35 -515.5 30.5t-93.5 141t-42 212t-17 191.5l4 319l-3 141l22 30l35 -166l914 10l49 -156q72 -135 92 -522l-10 -477q-75 -196 -138 -259.5t-127 -78.5t-303 -55l-557 -10l18 440h899l75 1l13 201l-98 10q-10 319 -11 513l-771 3l1 -202l555 -5
q-5 -102 9 -312z`,
  ץ: `M563 945l-172 -5q40 -181 97 -574q157 88 225 184t103 161.5t17.5 142t-183.5 183.5q84 149 150 354l5 139l21 -3l24 -141l150 -96q91 -68 106 -111.5t16.5 -74t-16.5 -82.5q-53 -142 -79.5 -186.5t-117.5 -190t-169 -213t-239 -152.5l37 -316q3 -204 -5 -331
t-28.5 -197.5t-76.5 -70.5q-17 0 -68 435l-72 1140l-179 -1q13 232 6 410l149 3q7 70 0 100t-13.5 59.5t-40.5 79.5l-16 1v60l70 -1l-1 -59h-16q35 -49 42.5 -78.5t10 -59t7.5 -98.5l32 1l8 290h-13l1 49h69l-2 -49h-17l-2 -290l30 -1q7 64 14.5 94.5t16.5 60.5t33 75l-16 1
l-1 60h76l-1 -60l-19 -1q-26 -48 -33.5 -77t-15.5 -57.5t-13 -96.5h111q-12 -173 -2 -410z`,
  צ: `M1134 288q0 -61 -34 -135.5t-212 -154.5h-788q33 215 25 420h667l-497 518h-174q2 204 -9 426h141q-3 62 -14 93.5t-17.5 62.5t-28.5 93l-20 -3l2 51l75 6l-2 -50l-18 -3q24 -61 31 -92t14 -61.5t11 -92.5l29 -2l7 300h-15l1 42l67 2l-4 -44h-16l4 -300l29 2
q12 73 18.5 104t12.5 62.5t29 83.5h-17l2 46l63 1v-47h-17q-20 -52 -24.5 -83.5t-9.5 -62.5t-15 -104h103q-20 -205 2 -428l-105 -1l331 -233q138 143 139.5 166.5t-2.5 42.5t-128 147q150 189 214 308l31 153l4 27l14 -23l5 -152q141 -121 158.5 -166t13.5 -72.5t-9 -66
t-66.5 -135.5t-296.5 -287q178 -147 246 -209q50 -52 50 -149z`,
  ק: `M1179 628q-5 -130 -13 -307.5t-150 -250t-224 -71.5h-99l-2 417h110l153 8q92 28 103 84.5t11 146.5q0 180 -38.5 211.5t-80 46.5t-144.5 15l-708 4q22 190 2 422h107l1 267l-32 1l1 69l96 -1l-1 -68h-33l5 -264h607q165 0 237 -101q77 -135 87 -317t5 -312zM477 262
h-140q34 -347 60.5 -462t19.5 -475h-55q-159 600 -113 934l-98 10l2 322h324v-329z`,
  ר: (p = 0) => `
M ${1232 + p} 30
L ${1190 + p} 0
L ${1147 + p} 30
L ${1055 + p} 811
Q ${1055 + p} 928 ${876 + p} 928
H 244
Q 135 928 104 932
Q 126 1122 116 1238
T 104 1354
L ${960 + p} 1358
Q ${1238 + p} 1358 ${1239 + p} 1061
Z
`,
  ש: `M1321 1095q20 -103 -67 -346q-36 -31 -108.5 -173t-153.5 -265t-223.5 -182t-469.5 -129l-132 938l-101 2q12 104 8 421l97 1q-14 66 -21 93.5t-14.5 55.5t-21.5 73h-17v50h72l2 -49l-17 -1q14 -45 21 -73t14 -55.5t21 -93.5l30 -3l5 280l-20 -2l2 51l58 1l1 -50h-19
l3 -280l30 2q14 68 21.5 96.5t14.5 57.5t21 76h-18l1 48l71 2l2 -48l-18 -1q-14 -48 -20.5 -76.5t-13 -57.5t-19.5 -96h97l3 -297v-122l-159 -6l85 -387q86 30 157 95.5t124.5 135.5t60 98t3.5 30.5t-15 17t-115 36.5l-49 10q28 162 1 379l4 142l12 10l34 -148
q147 -17 178 -34t67.5 -51.5t56.5 -115t5 -116.5t-33 -83t-61.5 -116.5t-127 -182.5t-271.5 -207l81 -50q438 -85 563.5 175.5t-1.5 308.5l-86 31l27 160l3 247l68 -6q129 -37 157.5 -58.5t72.5 -49.5q37 -57 37 -113z`,
  ת: (p = 0) =>
    `
M ${1255 + p} 194
Q ${1188 + p} 0 ${1173 + p} 0
Q ${1118 + p} 0 ${1118 + p} 43
Q ${1118 + p} 59 ${1075 + p} 400
T ${1032 + p} 811
L ${1022 + p} 928
H 310
Q 294 826 321 732
T 415 495
Q 482 352 472 286
Q 472 135 426 65
Q 383 0 317 0
H 23
L 82 420
H 204
Q 265 444 254 504
T 229 632
Q 215 700 208 765
T 206 939
L 106 943
Q 138 1133 123 1244
T 106 1355
L ${1195 + p} 1359
Q ${1201 + p} 1359 ${1201 + p} 1251
T ${1225 + p} 937
L ${1158 + p} 931
L ${1162 + p} 885
L ${1176 + p} 839
L ${1210 + p} 615
L ${1234 + p} 390
A 1715.2 1715.2 0 0 0 ${1258 + p} 196
Z
`
}

const ALPHABET = [
  "\u05d0",
  "\u05d1",
  "\u05d2",
  "\u05d3",
  "\u05d4",
  "\u05d5",
  "\u05d6",
  "\u05d7",
  "\u05d8",
  "\u05d9",
  "\u05da",
  "\u05db",
  "\u05dc",
  "\u05dd",
  "\u05de",
  "\u05df",
  "\u05e0",
  "\u05e1",
  "\u05e2",
  "\u05e3",
  "\u05e4",
  "\u05e5",
  "\u05e6",
  "\u05e7",
  "\u05e8",
  "\u05e9",
  "\u05ea",
  "\u05c6"
]

const scalePriority = {
  ד: 1,
  ה: 3,
  //'ב': '3.5',
  // ל: 2.5,
  ר: 2,
  ת: 4
}

const X = 2785 / 56
const Y = 3030
const R = X / Y
const LETTER_OFFSET = 1300
const SPACE_OFFSET = 0

function computeLines(words, state) {
  let line = []
  const lines = [line]

  let i = 0

  while (i < words.length) {
    var word = words[i++]
    var list = line.concat(word)
    //if (list.join('').length > 28) {
    //  line = [word.text]
    //  lines.push(line)
    //} else {
    var text = list.join("")
    var size = sumLine(
      text.split("").map(x => {
        return { adjust: 0, symbol: x }
      }),
      state
    )

    if (size > state.maxWidth) {
      line = [word]
      lines.push(line)
    } else {
      line.push(word)
    }
    //}
  }

  return lines
}

function getOffset(glyph, state) {
  const idx = state.alphabet.indexOf(glyph.symbol)
  const bounds = state.bounds[idx]
  const width = bounds.width + glyph.adjust
  return R * width + LETTER_OFFSET
}

function initializeState(fontSize, maxWidth) {
  const alphabet = ALPHABET
  const bounds = measureAllSVGGlyphs(alphabet, fontSize)
  return { alphabet, bounds, fontSize, lineOffset: 0, maxWidth }
}

function formatLine(line, state) {
  const glyphs = line
    .join(" ")
    .split("")
    .map(x => {
      return { adjust: 0, symbol: x }
    })

  if (glyphs[glyphs.length - 1].symbol == " ") {
    glyphs.pop()
  }
  if (glyphs[0].symbol == " ") {
    glyphs.shift()
  }

  let initialSize = Math.ceil(sumLine(glyphs, state))

  const idealWidth = state.maxWidth
  if (initialSize === idealWidth) {
    return glyphs
  }

  let remaining = idealWidth - initialSize

  const stretchA = []
  const stretchB = []
  const stretchC = []
  const stretchD = []

  glyphs.forEach((br, i) => {
    if (!glyphs[i + 1]) {
      if (scalePriority[br.symbol]) {
        // stretchA.push({
        //   glyph: br,
        //   scalePriority: scalePriority[br.symbol],
        // })
      }
    } else if (glyphs[i + 1].symbol == " ") {
      // last symbol
      if (scalePriority[br.symbol]) {
        stretchA.push({
          glyph: br,
          scalePriority: scalePriority[br.symbol]
        })
      }
    } else if (!glyphs[i + 2]) {
      if (scalePriority[br.symbol]) {
        stretchB.push({
          glyph: br,
          scalePriority: scalePriority[br.symbol]
        })
      }
    } else if (glyphs[i + 2].symbol == " ") {
      // last symbol
      if (scalePriority[br.symbol]) {
        stretchB.push({
          glyph: br,
          scalePriority: scalePriority[br.symbol]
        })
      }
    } else if (!glyphs[i + 3]) {
      if (scalePriority[br.symbol]) {
        stretchC.push({
          glyph: br,
          scalePriority: scalePriority[br.symbol]
        })
      }
    } else if (
      glyphs
        .slice(i, i + 3)
        .map(x => x.symbol)
        .join()
        .match(/^\w+$/)
    ) {
      // last symbol
      if (scalePriority[br.symbol]) {
        stretchC.push({
          glyph: br,
          scalePriority: scalePriority[br.symbol]
        })
      }
    } else if (
      glyphs
        .slice(i, i + 4)
        .map(x => x.symbol)
        .join()
        .match(/^\w+$/)
    ) {
      // last symbol
      if (scalePriority[br.symbol]) {
        stretchD.push({
          glyph: br,
          scalePriority: scalePriority[br.symbol]
        })
      }
    } else if (glyphs[i - 1] && glyphs[i - 1].symbol == " ") {
      // last symbol
      if (scalePriority[br.symbol]) {
        stretchD.push({
          glyph: br,
          scalePriority: scalePriority[br.symbol]
        })
      }
    }
  })

  stretchA.sort((a, b) => a.scalePriority - b.scalePriority)
  stretchB.sort((a, b) => a.scalePriority - b.scalePriority)
  stretchC.sort((a, b) => a.scalePriority - b.scalePriority)
  stretchD.sort((a, b) => a.scalePriority - b.scalePriority)

  // var spectrum = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

  var spectrum = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]

  var spectrum2 = []

  if (stretchA.length) {
    spectrum2.push(1.0)
  } else {
    spectrum2.push(0.0)
  }
  if (stretchB.length) {
    spectrum2.push(1.0)
  } else {
    spectrum2.push(0.0)
  }
  if (stretchC.length) {
    spectrum2.push(1.0)
  } else {
    spectrum2.push(0.0)
  }
  if (stretchD.length) {
    spectrum2.push(1.0)
  } else {
    spectrum2.push(0.0)
  }

  var totals = splitSpectrum(remaining, spectrum2)

  var adjustmentsA = stretchA.length
    ? splitSpectrum(totals[0], spectrum.slice(0, stretchA.length))
    : []
  var adjustmentsB = stretchB.length
    ? splitSpectrum(totals[1], spectrum.slice(0, stretchB.length))
    : []
  var adjustmentsC = stretchC.length
    ? splitSpectrum(totals[2], spectrum.slice(0, stretchC.length))
    : []
  var adjustmentsD = stretchD.length
    ? splitSpectrum(totals[3], spectrum.slice(0, stretchD.length))
    : []

  while (adjustmentsA.length) {
    var i = getRandomInteger(0, adjustmentsA.length - 1)
    var y = stretchA[i]
    var x = adjustmentsA[i]
    adjustmentsA.splice(i, 1)
    stretchA.splice(i, 1)
    remaining -= x
    y.glyph.adjust = x
  }

  //if (remaining) {
  while (remaining && adjustmentsB.length) {
    var i = getRandomInteger(0, adjustmentsB.length - 1)
    var y = stretchB[i]
    var x = adjustmentsB[i]
    adjustmentsB.splice(i, 1)
    stretchB.splice(i, 1)
    remaining -= x
    y.glyph.adjust = x
  }
  //}

  //if (remaining) {
  while (remaining && adjustmentsC.length) {
    var i = getRandomInteger(0, adjustmentsC.length - 1)
    var y = stretchC[i]
    var x = adjustmentsC[i]
    adjustmentsC.splice(i, 1)
    stretchC.splice(i, 1)
    remaining -= x
    y.glyph.adjust = x
  }
  //}

  //if (remaining) {
  while (remaining && adjustmentsD.length) {
    var i = getRandomInteger(0, adjustmentsD.length - 1)
    var y = stretchD[i]
    var x = adjustmentsD[i]
    adjustmentsD.splice(i, 1)
    stretchD.splice(i, 1)
    remaining -= x
    y.glyph.adjust = x
  }
  //}

  return glyphs
}

function createDynamicPath(index, path, symbol, scale, state) {
  const i = state.alphabet.indexOf(symbol)
  const bounds = state.bounds[i]
  const width = bounds.width

  state.lineOffset += R * width + scale + LETTER_OFFSET

  const transform = `translate(${state.maxWidth -
    state.lineOffset}, 0) scale(${1},-${1})`

  let el = document.createElementNS("http://www.w3.org/2000/svg", "path")
  el.setAttribute("d", path.replace(/\s+/g, " "))
  el.setAttribute("transform", transform)

  return el
}

function layout(lines, state) {
  const svgs = []

  let I = 0

  lines.forEach((line, i) => {
    state.lineOffset = 0

    const glyphs =
      i != lines.length - 1
        ? formatLine(line, state)
        : line
            .join(" ")
            .split("")
            .map(x => {
              return { adjust: 0, symbol: x }
            })

    const paths = []

    glyphs.forEach(glyph => {
      const g = GLYPHS[glyph.symbol]
      const text = typeof g == "function" ? g(glyph.adjust) : g

      if (glyph.symbol == " " || glyph.symbol == "\u00a0") {
        state.lineOffset += SPACE_OFFSET
      } else {
        paths.push(
          createDynamicPath(I, text, glyph.symbol, glyph.adjust, state)
        )

        I++
      }
    })

    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
    svg.setAttribute("version", "1.1")
    svg.setAttribute("viewBox", `0 -1900 ${state.maxWidth} 3030`)
    svg.setAttribute("height", `${state.fontSize}`)
    paths.forEach(path => svg.appendChild(path))

    svgs.push(svg)

    state.lineOffset = 0
  })

  return svgs
}

function measureAllSVGGlyphs(glyphs, fontSize) {
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
  svg.setAttribute("viewBox", "0 -900 52000 3030")
  svg.setAttribute("version", "1.1")
  svg.setAttribute("height", `${fontSize}`)

  addToMeasurer(svg)

  const measurements = glyphs.map(symbol =>
    measureSVGGlyph(svg, GLYPHS[symbol])
  )

  removeFromMeasurer(svg)

  return measurements
}

function measureSVGGlyph(svg, path) {
  // document.body.style['line-height'] = LINE_HEIGHT + 'px'
  // document.body.style['font-size'] = FONT_SIZE + 'px'

  const el = document.createElementNS("http://www.w3.org/2000/svg", "path")

  el.setAttribute(
    "d",
    (typeof path == "function" ? path() : path).replace(/\s+/g, " ")
  )

  svg.appendChild(el)

  const rect = el.getBoundingClientRect()

  svg.removeChild(el)

  return rect
  // return svg.querySelector('path').getBoundingClientRect()
}

function render(words, fontSize, maxWidth = 49000) {
  const state = initializeState(fontSize, maxWidth)
  const lines = computeLines(words, state)
  console.log(lines)
  const svgs = layout(lines, state)
  return svgs
}

function getRandomInteger(min, max) {
  min = Math.ceil(min)
  max = Math.floor(max)
  return Math.floor(Math.random() * (max - min)) + min
}

function sumLine(glyphs, state) {
  let sum = 0
  glyphs.forEach(glyph => {
    if (glyph.symbol == " " || glyph.symbol == "\u00a0") {
      sum += SPACE_OFFSET
    } else {
      sum += getOffset(glyph, state)
    }
  })
  return sum
}

function splitSpectrum(total, spectrum) {
  var smallSum = 0
  spectrum.forEach(x => (smallSum += x))
  var ratios = spectrum.map(x => x / smallSum)
  return ratios.map(x => x * total)
}

draw()
.container {
  width: 800px;
}
<h2>16 Font Size</h2>
<div class="container container-16"></div>
<h2>32 Font Size</h2>
<div class="container container-32"></div>

关于x轴上的布局不正确,请参阅32字体大小示例中第二行的第一个字符(从右边开始),如下所示:

这是“第一节”的结尾,有这两个字:חיי שרה,所以单词没有作为一个单元保存。如果没有拉伸字母,它应该看起来更像这样:

e4eetjau

e4eetjau1#

也许一个务实的方法是可以接受的:
为什么不简单地使用CSS scale并根据设备大小使用CSS * 自定义属性 * 将render(words, 32)版本扩展到所需的大小?
您仍然需要注意适当的 Package ,但HTML引擎在呈现缩放内容方面做得很好。
我调整了你的JS,只创建了一组大小为render(words, 32)的行,并创建了一个简单的范围滑块,用于调整CSS * 自定义属性 * --scale,以相对于32px的所需比例显示文本。

// this is the main drawing function,
// called at the very end.
function draw() {
  const words = []

  chapter.verses.forEach(v => {
    words.push(
      ...String(v.value[0].text)
        .split(/\s+/)
        .map(word => {
          return [...word].filter(c => ALPHABET.includes(c)).join("")
        })
    )
  })

  // NEW
  const lines = render(words, 32)
  lines.forEach(line => document.querySelector(".container")?.appendChild(line))
  //
}

const chapter = {
  verses: [
    {
      index: 0,
      type: "verse",
      value: [
        {
          language: "hebrew",
          script: "hebrew",
          text: "ויהיו חיי שרה מאה שנה ועשרים שנה ושבע שנים שני חיי שרה׃"
        }
      ]
    },
    {
      index: 1,
      type: "verse",
      value: [
        {
          language: "hebrew",
          script: "hebrew",
          text:
            "ותמת שרה בקרית ארבע הוא חברון בארץ כנען ויבא אברהם לספד לשרה ולבכתה׃"
        }
      ]
    }
  ]
}

let measurer

function addToMeasurer(el) {
  initializeMeasurer()

  if (!measurer) {
    return
  }

  measurer.appendChild(el)
}

function initializeMeasurer() {
  if (measurer) {
    return
  }

  measurer = document.createElement("span")
  measurer.setAttribute("id", "measurer")

  measurer.style.position = "absolute"
  measurer.style.height = "auto"
  measurer.style.width = "auto"
  measurer.style.whiteSpace = "nowrap"
  document.body.appendChild(measurer)
}

function removeFromMeasurer(el) {
  if (!measurer) {
    return
  }

  el.parentNode?.removeChild(el)
}

function setMeasurerFont({ family, size }) {
  if (!measurer) {
    return
  }

  if (family) {
    measurer.style.fontFamily = family
  }

  if (size) {
    measurer.style.fontSize = typeof size === "number" ? `${size}px` : size
  }
}

const GLYPHS = {
  // horiz-adv-x="646"
  "\u05c6": `M62.5 425q7.5 70 40 139.5t72.5 151t110 223.5l-184 1q30 211 0 418l148 -1q3 40 -8 75.5t-21.5 62.5t-40.5 89h-23l6 60h74l3 -60h-23q27 -37 37.5 -63.5t22 -62.5t19.5 -101l35 7l-3 274h-23l4 60h76l4 -60h-23l4 -280h25q13 76 26.5 109t17 50t36.5 55h-24l4 70h73 l6 -70h-23q-25 -57 -34.5 -85.5t-20 -57.5t-17.5 -75l160 -1q-20 -241 -8 -425l-204 10q-70 -154 -83.5 -198t-33.5 -128t-13 -138t53 -54h321q1 -210 32 -420h-303q-223 30 -263.5 119t-40.5 162.5t7.5 143.5z`,
  א: `M1110 0q-118 13 -157 59.5t-156 191.5l-521 541q-16 -93 -6 -277v-187q306 5 306 -98l-10 -84q-26 -73 -42 -104.5t-84 -181.5l-50 -14q59 124 18 144l-118 20q-71 1 -82 -67l-22 6l-2 67v45l1 78q0 35 10.5 146.5t0.5 199.5q-5 164 19 367l-155 187l33 426l-16 129
l36 -10l32 -152q52 -24 573 -628q101 87 159 168l-248 258l172 247l15 127l24 -4l10 -151l13 -19q199 -164 189 -278q-10 -140 -51 -161l-224 -243l152 -168l173 -150l50 83l19 -15l-35 -98z`,
  ב: (p = 0) => `
M ${1242 + p} 421
L ${1218 + p} 0
H 69
L 83 211
L 77 422
L ${1032 + p} 424
L ${1015 + p} 906
L 67 908
L 69 1352
L 49 1464
L 86 1454
L 117 1354
L 179 1355
L 178 1624
H 153
V 1693
L 225 1694
V 1624
H 208
L 223 1358
H ${1113 + p}
L ${1143 + p} 1428
H ${1178 + p}
L ${1153 + p} 1358
L ${1148 + p} 936
L ${1142 + p} 420
L ${1202 + p} 421
L ${1237 + p} 511
L ${1270 + p} 501
Z
`,
  ג: `M686 248q0 -248 -29 -248q-20 0 -22 27q-5 47 -63 232q-54 169 -102 389q-50 116 -91 229l-22 71l-262 -4q22 205 4 409l157 1q-8 73 -18 102.5t-17 53.5t-34 74h-25l5 50h74l4 -50h-27q36 -56 40 -79.5t15 -53.5t20 -93h39l-2 280h-24l6 60h68l8 -60h-24l6 -280h37
q16 76 27.5 101.5t22 49.5t40.5 69h-24l6 60h66l3 -60h-17q-29 -50 -43.5 -77.5t-24.5 -53.5t-19 -89h172q-30 -203 0 -407l-172 -3l60 -127q20 -59 57.5 -113.5t79.5 -145.5q45 -223 45 -314zM497 550q-13 -404 -68 -459.5t-150 -86.5l-104 -1l-170 -3q40 224 60 488
q65 -64 155 -58t179 57z`,
  ד: (p = 0) => `
M ${1255 + p} 935
L ${1117 + p} 928
L ${1156 + p} 613
L ${1230 + p} 356
Q ${1265 + p} 142 ${1209 + p} 71
T ${1137 + p} 0
Q ${1090 + p} 0 ${1090 + p} 43
L ${1007 + p} 642
L ${988 + p} 930
L 109 926
Q 136 1102 116 1353
L 106 1444
H 134
L 159 1354
L 240 1359
L 242 1624
H 220
V 1684
L 300 1683
V 1622
L 274 1624
L 281 1358
H ${1225 + p}
L ${1260 + p} 1458
L ${1292 + p} 1448
L ${1274 + p} 1358
Q ${1230 + p} 1178 ${1255 + p} 935
Z
`,
  ה: (p = 0) => `
M ${1260 + p} 1168
L ${1257 + p} 930
L ${1127 + p} 928
L ${1185 + p} 645
Q ${1242 + p} 393 ${1214 + p} 164
Q ${1215 + p} 142 ${1159 + p} 71
T ${1087 + p} 0
Q ${1050 + p} 0 ${1050 + p} 43
Q ${1050 + p} 219 ${987 + p} 926
L 128 930
L 126 1353
L 106 1454
H 144
L 172 1354
H 249
L 247 1602
L 226 1604
V 1680
L 313 1690
L 315 1604
H 290
L 291 1358
L ${1211 + p} 1356
L ${1250 + p} 1458
H ${1292 + p}
L ${1254 + p} 1358
Z
M 578 327
L 486 0
Q 317 20 237 73
T 138 331
Q 140 400 151 439
T 177 541.5
A 451.24 451.24 0 0 0 223 660
L 250 658
Q 242 587 246.5 548.5
T 282.5 457
Q 314 404 578 327
Z
`,
  ו: `M567 164q0 -23 -36.5 -93.5t-62.5 -70.5q-44 0 -44 43l9 768q0 117 -87 117l-240 -6q2 255 -23 426l282 10q200 0 200 -297l11 -448q21 -205 -9 -449z`,
  ז: `M637 941l-218 -3q41 -101 87 -280.5t36 -278.5q0 -113 -24 -249q-23 -130 -53 -130q-52 0 -52 23q0 -2 -3.5 49t-3.5 88q0 21 -30 177t-40 312q-10 73 -5 134t-11 161l-217 -1q7 184 -11 416l157 5q2 64 -3.5 90t-25.5 68.5t-40 71.5h-12l-2 60h67l4 -60h-16
q19 -26 34 -64.5t23.5 -70t16.5 -96.5l49 -2l10 288h-14l3 50h63l-4 -50h-14l3 -291l49 1q14 60 29.5 105.5t25 65t44.5 69.5h-15l5 60h55l2 -60l-17 1q-29 -55 -40 -72.5t-26 -65.5t-15 -102h149q-10 -283 0 -419z`,
  ח: (p = 0) => `
M1279 928h-178q47 -387 48.5 -476t-11.5 -199.5t-54 -181.5t-57 -71q-57 0 -57 43q-30 176 -26.5 353.5t59.5 531.5h-177l-17 296l-118 232l-78 -198l-12 -326l-193 -5q42 -341 37 -479.5t-11.5 -293t-67.5 -154.5q-39 0 -39 41q-73 258 -71.5 415.5t44.5 471.5l-186 4
q12 190 -4 424q6 118 4 118h20l40 -116l438 6l99 252l111 -258h462q-14 -160 -5 -430z
`,
  ט: `M1163.5 314.5q-109.5 -254.5 -293.5 -314.5h-235h-339l-64 956l-117 3q22 186 -6 393l125 2q-5 69 -9 98.5t-10 62.5t-35 79h-14l-1 60h64l3 -60h-17q29 -45 33 -73.5t9 -61.5t4 -105l40 -1l2 290l-14 -1l-4 60h65l3 -60h-17l2 -290h28q4 84 7.5 100t8.5 47.5t35 90.5
l-18 -1l-3 67l72 4l-1 -70l-14 -1q-26 -55 -31 -78t-11.5 -59.5t-0.5 -99.5h97q-5 -179 7 -399l-163 -4l49 -539l501 -20q147 0 185 90.5t53 205t-54 159.5q-19 95 -152 110t-241 -32q-44 -38 -57 -70t-49 -37l64 484q86 56 254.5 48t228 -100.5t91 -186.5t41.5 -166.5
t9 -199t-110.5 -381z`,
  י: `M580 1031q0 -147 -80 -392q-51 -191 -112 -191q-28 0 -33 15v32q50 146 65.5 247.5t15.5 98.5q0 85 -136 87h-148l-25 -108l-16 2q-9 250 -9 371l-10 401h30l40 -236h149q267 0 269 -327z`,
  ך: `M1250 1158q5 -227 5 -230l32 -1513l-97 13q-79 1366 -146 1433t-185 67h-684q21 194 11 310t-12 116l941 4q59 0 97 -47t38 -153z`,
  כ: `M1198 678q0 -392 -168 -544q-148 -134 -500 -134h-501q22 210 4 420h669q171 0 285 54q78 118 78 214q0 240 -514 240t-514 4q22 210 12 316t-13 106l699 4q246 0 352 -150q111 -156 111 -530z`,
  ל: (p = 0) => `
M ${1228 + p} 782
Q ${1228 + p} 560 ${1183.5 + p} 410.5
T ${1083.5 + p} 172
Q ${1028 + p} 83 ${953 + p} 41
T ${778 + p} -5
L ${530 + p} 7
Q ${549 + p} 187 ${536 + p} 393
L ${887 + p} 401
Q ${984 + p} 407 ${1027.5 + p} 441
T ${1086.5 + p} 540.5
Q ${1102 + p} 606 ${1106 + p} 701
Q ${1098 + p} 788 ${1083 + p} 818
T ${1025 + p} 887
Q ${982 + p} 926 ${889 + p} 926
L 264 936
Q 255 1020 255 1077.5
T 225 1396
L 213 1786
Q 213 1821 145 1818
L 101 1729
H 95
Q 113 1910 99 2039
L 90 2136
L 98 2142
L 128 2043
H 152
L 189 2163
H 199
L 202 2045
L 269 2032
Q 305 2023 311 2012
Q 325 1997 325 1941
A 827.14 827.14 0 0 0 320 1854
L 360 1359
L ${671 + p} 1358
Q ${833 + p} 1358 ${943 + p} 1294.5
T ${1116.5 + p} 1114
Q ${1180 + p} 997 ${1228 + p} 782
Z
`,
  ם: `M378 924l8 -504h707l-7 397q-2 69 -39 92q-30 19 -106 19zM1231 0h-994l-11 928q-83 3 -101 7q14 190 4 304.5t-13 114.5l836 4q279 0 279 -297v-1061z`,
  מ: `M1103 0h-585l-5 420h514q37 83 37 151q30 146 -63 252.5t-288 106.5q-208 30 -227 -110t-39 -277q0 -6 -21.5 -85t-41.5 -139q-50 -197 -179 -319q-39 0 -39 41l195 859q5 45 -1.5 63t-50.5 46l-196 57q29 182 44 418q145 -18 282 -45q71 -24 96 -61t40 -101.5t10 -119.5
q-71 -189 5 -146q65 102 80 141t21.5 108t6.5 162l153 -4q43 0 129 -44q89 -50 136 -123q113 -153 113 -447q-1 -75 -1 -184.5t-125 -619.5z`,
  ן: `M567 905l-192 5q20 -151 35 -246l50 -479q-6 -269 -12.5 -444.5t-28 -245.5t-37.5 -70q-44 0 -44 43q-80 847 -75 1055t-11 389l-172 1q13 216 -1 426l139 3q2 49 -2 74t-9 48.5t-29 87.5h-26l3 70h79l-2 -69l-25 -1q33 -63 36.5 -84.5t7.5 -49.5t13 -79l31 -2l12 281
h-25l3 80l80 -1l-2 -78h-24l1 -279l32 1q11 60 15 78t13.5 49.5t43.5 83.5h-25v71h81l3 -70h-27q-29 -52 -38 -75t-17.5 -50.5t-8.5 -83.5l157 1q-12 -203 -2 -440z`,
  נ: `M621.5 119q-40.5 -89 -263.5 -119h-303q31 210 32 420h321q46 0 53 54t-13 138t-33.5 128t-83.5 198l-204 -10q12 184 -8 425l160 1q-7 46 -17.5 75t-20 57.5t-34.5 85.5h-23l6 70h73l4 -70h-24q33 -38 36.5 -55t17 -50t26.5 -109h25l4 280h-23l4 60h76l4 -60h-23
l-3 -274l35 -7q8 65 19.5 101t22 62.5t37.5 63.5h-23l3 60h74l6 -60h-23q-30 -62 -40.5 -89t-21.5 -62.5t-8 -75.5l148 1q-30 -207 0 -418l-184 -1q70 -142 110 -223.5t72.5 -151t40 -139.5t7.5 -143.5t-40.5 -162.5z`,
  ס: `M513 928l-125 -5q9 -149 9 -173q-10 -174 63 -260q97 -81 113 -81h119q172 0 213 35q100 52 125 108t35 126q0 156 -54 183q-83 77 -289 67h-209zM1198 678q0 -312 -127 -495q-145 -210 -444 -210q-105 0 -232 172q-114 152 -144 418q0 293 -8 365l-139 4q32 210 17 316
t-17 106l621 4q238 0 357 -178q116 -174 116 -502z`,
  ע: `M1150 1083q0 -66 -221 -583q-35 -67 -84 -146.5t-96 -146.5t-122.5 -147t-558.5 -170l15 174l-15 174l320 63l-144 631l-143 2q24 196 4 429l144 -1q-20 48 -24 76t-8 59.5t-42 84.5h-15l2 50h68l-1 -50h-16q31 -24 34.5 -54.5t12.5 -54.5t13 -108l32 -2l5 270l-16 1
l4 50l62 -2l-2 -50l-19 -2l-1 -270l25 4q10 104 25 129.5t18 45.5t22 44h-16l3 53l68 -1l-2 -52h-16q-22 -43 -30.5 -69.5t-21.5 -70t-16 -76.5l116 -2q-13 -206 4 -428l-190 -6l145 -609q267 106 323.5 192t160.5 296q9 37 -26 62l-223 120q113 188 159 391l87 -56
q197 -73 197 -244z`,
  ף: `M1193 -655q-55 0 -105 43l-30 1503q0 37 -38 37h-782l1 -342l405 -5q5 -41 5 -97q0 -85 -6 -155l-47 -4h-262q-37 4 -64.5 14t-39 28.5t-41.5 65.5t-62 330v584l-21 137l32 10l45 -146l93 10h655q288 0 289 -297z`,
  פ: `M740 449q-447 -35 -515.5 30.5t-93.5 141t-42 212t-17 191.5l4 319l-3 141l22 30l35 -166l914 10l49 -156q72 -135 92 -522l-10 -477q-75 -196 -138 -259.5t-127 -78.5t-303 -55l-557 -10l18 440h899l75 1l13 201l-98 10q-10 319 -11 513l-771 3l1 -202l555 -5
q-5 -102 9 -312z`,
  ץ: `M563 945l-172 -5q40 -181 97 -574q157 88 225 184t103 161.5t17.5 142t-183.5 183.5q84 149 150 354l5 139l21 -3l24 -141l150 -96q91 -68 106 -111.5t16.5 -74t-16.5 -82.5q-53 -142 -79.5 -186.5t-117.5 -190t-169 -213t-239 -152.5l37 -316q3 -204 -5 -331
t-28.5 -197.5t-76.5 -70.5q-17 0 -68 435l-72 1140l-179 -1q13 232 6 410l149 3q7 70 0 100t-13.5 59.5t-40.5 79.5l-16 1v60l70 -1l-1 -59h-16q35 -49 42.5 -78.5t10 -59t7.5 -98.5l32 1l8 290h-13l1 49h69l-2 -49h-17l-2 -290l30 -1q7 64 14.5 94.5t16.5 60.5t33 75l-16 1
l-1 60h76l-1 -60l-19 -1q-26 -48 -33.5 -77t-15.5 -57.5t-13 -96.5h111q-12 -173 -2 -410z`,
  צ: `M1134 288q0 -61 -34 -135.5t-212 -154.5h-788q33 215 25 420h667l-497 518h-174q2 204 -9 426h141q-3 62 -14 93.5t-17.5 62.5t-28.5 93l-20 -3l2 51l75 6l-2 -50l-18 -3q24 -61 31 -92t14 -61.5t11 -92.5l29 -2l7 300h-15l1 42l67 2l-4 -44h-16l4 -300l29 2
q12 73 18.5 104t12.5 62.5t29 83.5h-17l2 46l63 1v-47h-17q-20 -52 -24.5 -83.5t-9.5 -62.5t-15 -104h103q-20 -205 2 -428l-105 -1l331 -233q138 143 139.5 166.5t-2.5 42.5t-128 147q150 189 214 308l31 153l4 27l14 -23l5 -152q141 -121 158.5 -166t13.5 -72.5t-9 -66
t-66.5 -135.5t-296.5 -287q178 -147 246 -209q50 -52 50 -149z`,
  ק: `M1179 628q-5 -130 -13 -307.5t-150 -250t-224 -71.5h-99l-2 417h110l153 8q92 28 103 84.5t11 146.5q0 180 -38.5 211.5t-80 46.5t-144.5 15l-708 4q22 190 2 422h107l1 267l-32 1l1 69l96 -1l-1 -68h-33l5 -264h607q165 0 237 -101q77 -135 87 -317t5 -312zM477 262
h-140q34 -347 60.5 -462t19.5 -475h-55q-159 600 -113 934l-98 10l2 322h324v-329z`,
  ר: (p = 0) => `
M ${1232 + p} 30
L ${1190 + p} 0
L ${1147 + p} 30
L ${1055 + p} 811
Q ${1055 + p} 928 ${876 + p} 928
H 244
Q 135 928 104 932
Q 126 1122 116 1238
T 104 1354
L ${960 + p} 1358
Q ${1238 + p} 1358 ${1239 + p} 1061
Z
`,
  ש: `M1321 1095q20 -103 -67 -346q-36 -31 -108.5 -173t-153.5 -265t-223.5 -182t-469.5 -129l-132 938l-101 2q12 104 8 421l97 1q-14 66 -21 93.5t-14.5 55.5t-21.5 73h-17v50h72l2 -49l-17 -1q14 -45 21 -73t14 -55.5t21 -93.5l30 -3l5 280l-20 -2l2 51l58 1l1 -50h-19
l3 -280l30 2q14 68 21.5 96.5t14.5 57.5t21 76h-18l1 48l71 2l2 -48l-18 -1q-14 -48 -20.5 -76.5t-13 -57.5t-19.5 -96h97l3 -297v-122l-159 -6l85 -387q86 30 157 95.5t124.5 135.5t60 98t3.5 30.5t-15 17t-115 36.5l-49 10q28 162 1 379l4 142l12 10l34 -148
q147 -17 178 -34t67.5 -51.5t56.5 -115t5 -116.5t-33 -83t-61.5 -116.5t-127 -182.5t-271.5 -207l81 -50q438 -85 563.5 175.5t-1.5 308.5l-86 31l27 160l3 247l68 -6q129 -37 157.5 -58.5t72.5 -49.5q37 -57 37 -113z`,
  ת: (p = 0) =>
    `
M ${1255 + p} 194
Q ${1188 + p} 0 ${1173 + p} 0
Q ${1118 + p} 0 ${1118 + p} 43
Q ${1118 + p} 59 ${1075 + p} 400
T ${1032 + p} 811
L ${1022 + p} 928
H 310
Q 294 826 321 732
T 415 495
Q 482 352 472 286
Q 472 135 426 65
Q 383 0 317 0
H 23
L 82 420
H 204
Q 265 444 254 504
T 229 632
Q 215 700 208 765
T 206 939
L 106 943
Q 138 1133 123 1244
T 106 1355
L ${1195 + p} 1359
Q ${1201 + p} 1359 ${1201 + p} 1251
T ${1225 + p} 937
L ${1158 + p} 931
L ${1162 + p} 885
L ${1176 + p} 839
L ${1210 + p} 615
L ${1234 + p} 390
A 1715.2 1715.2 0 0 0 ${1258 + p} 196
Z
`
}

const ALPHABET = [
  "\u05d0",
  "\u05d1",
  "\u05d2",
  "\u05d3",
  "\u05d4",
  "\u05d5",
  "\u05d6",
  "\u05d7",
  "\u05d8",
  "\u05d9",
  "\u05da",
  "\u05db",
  "\u05dc",
  "\u05dd",
  "\u05de",
  "\u05df",
  "\u05e0",
  "\u05e1",
  "\u05e2",
  "\u05e3",
  "\u05e4",
  "\u05e5",
  "\u05e6",
  "\u05e7",
  "\u05e8",
  "\u05e9",
  "\u05ea",
  "\u05c6"
]

const scalePriority = {
  ד: 1,
  ה: 3,
  //'ב': '3.5',
  // ל: 2.5,
  ר: 2,
  ת: 4
}

const X = 2785 / 56
const Y = 3030
const R = X / Y
const LETTER_OFFSET = 1300
const SPACE_OFFSET = 0

function computeLines(words, state) {
  let line = []
  const lines = [line]

  let i = 0

  while (i < words.length) {
    var word = words[i++]
    var list = line.concat(word)
    //if (list.join('').length > 28) {
    //  line = [word.text]
    //  lines.push(line)
    //} else {
    var text = list.join("")
    var size = sumLine(
      text.split("").map(x => {
        return { adjust: 0, symbol: x }
      }),
      state
    )

    if (size > state.maxWidth) {
      line = [word]
      lines.push(line)
    } else {
      line.push(word)
    }
    //}
  }

  return lines
}

function getOffset(glyph, state) {
  const idx = state.alphabet.indexOf(glyph.symbol)
  const bounds = state.bounds[idx]
  const width = bounds.width + glyph.adjust
  return R * width + LETTER_OFFSET
}

function initializeState(fontSize, maxWidth) {
  const alphabet = ALPHABET
  const bounds = measureAllSVGGlyphs(alphabet, fontSize)
  return { alphabet, bounds, fontSize, lineOffset: 0, maxWidth }
}

function formatLine(line, state) {
  const glyphs = line
    .join(" ")
    .split("")
    .map(x => {
      return { adjust: 0, symbol: x }
    })

  if (glyphs[glyphs.length - 1].symbol == " ") {
    glyphs.pop()
  }
  if (glyphs[0].symbol == " ") {
    glyphs.shift()
  }

  let initialSize = Math.ceil(sumLine(glyphs, state))

  const idealWidth = state.maxWidth
  if (initialSize === idealWidth) {
    return glyphs
  }

  let remaining = idealWidth - initialSize

  const stretchA = []
  const stretchB = []
  const stretchC = []
  const stretchD = []

  glyphs.forEach((br, i) => {
    if (!glyphs[i + 1]) {
      if (scalePriority[br.symbol]) {
        // stretchA.push({
        //   glyph: br,
        //   scalePriority: scalePriority[br.symbol],
        // })
      }
    } else if (glyphs[i + 1].symbol == " ") {
      // last symbol
      if (scalePriority[br.symbol]) {
        stretchA.push({
          glyph: br,
          scalePriority: scalePriority[br.symbol]
        })
      }
    } else if (!glyphs[i + 2]) {
      if (scalePriority[br.symbol]) {
        stretchB.push({
          glyph: br,
          scalePriority: scalePriority[br.symbol]
        })
      }
    } else if (glyphs[i + 2].symbol == " ") {
      // last symbol
      if (scalePriority[br.symbol]) {
        stretchB.push({
          glyph: br,
          scalePriority: scalePriority[br.symbol]
        })
      }
    } else if (!glyphs[i + 3]) {
      if (scalePriority[br.symbol]) {
        stretchC.push({
          glyph: br,
          scalePriority: scalePriority[br.symbol]
        })
      }
    } else if (
      glyphs
        .slice(i, i + 3)
        .map(x => x.symbol)
        .join()
        .match(/^\w+$/)
    ) {
      // last symbol
      if (scalePriority[br.symbol]) {
        stretchC.push({
          glyph: br,
          scalePriority: scalePriority[br.symbol]
        })
      }
    } else if (
      glyphs
        .slice(i, i + 4)
        .map(x => x.symbol)
        .join()
        .match(/^\w+$/)
    ) {
      // last symbol
      if (scalePriority[br.symbol]) {
        stretchD.push({
          glyph: br,
          scalePriority: scalePriority[br.symbol]
        })
      }
    } else if (glyphs[i - 1] && glyphs[i - 1].symbol == " ") {
      // last symbol
      if (scalePriority[br.symbol]) {
        stretchD.push({
          glyph: br,
          scalePriority: scalePriority[br.symbol]
        })
      }
    }
  })

  stretchA.sort((a, b) => a.scalePriority - b.scalePriority)
  stretchB.sort((a, b) => a.scalePriority - b.scalePriority)
  stretchC.sort((a, b) => a.scalePriority - b.scalePriority)
  stretchD.sort((a, b) => a.scalePriority - b.scalePriority)

  // var spectrum = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

  var spectrum = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]

  var spectrum2 = []

  if (stretchA.length) {
    spectrum2.push(1.0)
  } else {
    spectrum2.push(0.0)
  }
  if (stretchB.length) {
    spectrum2.push(1.0)
  } else {
    spectrum2.push(0.0)
  }
  if (stretchC.length) {
    spectrum2.push(1.0)
  } else {
    spectrum2.push(0.0)
  }
  if (stretchD.length) {
    spectrum2.push(1.0)
  } else {
    spectrum2.push(0.0)
  }

  var totals = splitSpectrum(remaining, spectrum2)

  var adjustmentsA = stretchA.length
    ? splitSpectrum(totals[0], spectrum.slice(0, stretchA.length))
    : []
  var adjustmentsB = stretchB.length
    ? splitSpectrum(totals[1], spectrum.slice(0, stretchB.length))
    : []
  var adjustmentsC = stretchC.length
    ? splitSpectrum(totals[2], spectrum.slice(0, stretchC.length))
    : []
  var adjustmentsD = stretchD.length
    ? splitSpectrum(totals[3], spectrum.slice(0, stretchD.length))
    : []

  while (adjustmentsA.length) {
    var i = getRandomInteger(0, adjustmentsA.length - 1)
    var y = stretchA[i]
    var x = adjustmentsA[i]
    adjustmentsA.splice(i, 1)
    stretchA.splice(i, 1)
    remaining -= x
    y.glyph.adjust = x
  }

  //if (remaining) {
  while (remaining && adjustmentsB.length) {
    var i = getRandomInteger(0, adjustmentsB.length - 1)
    var y = stretchB[i]
    var x = adjustmentsB[i]
    adjustmentsB.splice(i, 1)
    stretchB.splice(i, 1)
    remaining -= x
    y.glyph.adjust = x
  }
  //}

  //if (remaining) {
  while (remaining && adjustmentsC.length) {
    var i = getRandomInteger(0, adjustmentsC.length - 1)
    var y = stretchC[i]
    var x = adjustmentsC[i]
    adjustmentsC.splice(i, 1)
    stretchC.splice(i, 1)
    remaining -= x
    y.glyph.adjust = x
  }
  //}

  //if (remaining) {
  while (remaining && adjustmentsD.length) {
    var i = getRandomInteger(0, adjustmentsD.length - 1)
    var y = stretchD[i]
    var x = adjustmentsD[i]
    adjustmentsD.splice(i, 1)
    stretchD.splice(i, 1)
    remaining -= x
    y.glyph.adjust = x
  }
  //}

  return glyphs
}

function createDynamicPath(index, path, symbol, scale, state) {
  const i = state.alphabet.indexOf(symbol)
  const bounds = state.bounds[i]
  const width = bounds.width

  state.lineOffset += R * width + scale + LETTER_OFFSET

  const transform = `translate(${state.maxWidth -
    state.lineOffset}, 0) scale(${1},-${1})`

  let el = document.createElementNS("http://www.w3.org/2000/svg", "path")
  el.setAttribute("d", path.replace(/\s+/g, " "))
  el.setAttribute("transform", transform)

  return el
}

function layout(lines, state) {
  const svgs = []

  let I = 0

  lines.forEach((line, i) => {
    state.lineOffset = 0

    const glyphs =
      i != lines.length - 1
        ? formatLine(line, state)
        : line
            .join(" ")
            .split("")
            .map(x => {
              return { adjust: 0, symbol: x }
            })

    const paths = []

    glyphs.forEach(glyph => {
      const g = GLYPHS[glyph.symbol]
      const text = typeof g == "function" ? g(glyph.adjust) : g

      if (glyph.symbol == " " || glyph.symbol == "\u00a0") {
        state.lineOffset += SPACE_OFFSET
      } else {
        paths.push(
          createDynamicPath(I, text, glyph.symbol, glyph.adjust, state)
        )

        I++
      }
    })

    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
    svg.setAttribute("version", "1.1")
    svg.setAttribute("viewBox", `0 -1900 ${state.maxWidth} 3030`)
    svg.setAttribute("height", `${state.fontSize}`)
    paths.forEach(path => svg.appendChild(path))

    svgs.push(svg)

    state.lineOffset = 0
  })

  return svgs
}

function measureAllSVGGlyphs(glyphs, fontSize) {
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
  svg.setAttribute("viewBox", "0 -900 52000 3030")
  svg.setAttribute("version", "1.1")
  svg.setAttribute("height", `${fontSize}`)

  addToMeasurer(svg)

  const measurements = glyphs.map(symbol =>
    measureSVGGlyph(svg, GLYPHS[symbol])
  )

  removeFromMeasurer(svg)

  return measurements
}

function measureSVGGlyph(svg, path) {
  // document.body.style['line-height'] = LINE_HEIGHT + 'px'
  // document.body.style['font-size'] = FONT_SIZE + 'px'

  const el = document.createElementNS("http://www.w3.org/2000/svg", "path")

  el.setAttribute(
    "d",
    (typeof path == "function" ? path() : path).replace(/\s+/g, " ")
  )

  svg.appendChild(el)

  const rect = el.getBoundingClientRect()

  svg.removeChild(el)

  return rect
  // return svg.querySelector('path').getBoundingClientRect()
}

function render(words, fontSize, maxWidth = 49000) {
  const state = initializeState(fontSize, maxWidth)
  const lines = computeLines(words, state)
  console.log(lines)
  const svgs = layout(lines, state)
  return svgs
}

function getRandomInteger(min, max) {
  min = Math.ceil(min)
  max = Math.floor(max)
  return Math.floor(Math.random() * (max - min)) + min
}

function sumLine(glyphs, state) {
  let sum = 0
  glyphs.forEach(glyph => {
    if (glyph.symbol == " " || glyph.symbol == "\u00a0") {
      sum += SPACE_OFFSET
    } else {
      sum += getOffset(glyph, state)
    }
  })
  return sum
}

function splitSpectrum(total, spectrum) {
  var smallSum = 0
  spectrum.forEach(x => (smallSum += x))
  var ratios = spectrum.map(x => x / smallSum)
  return ratios.map(x => x * total)
}

draw()
/* To show .container.margin needs to be adjusted to accomodate scale */
/* *:not(label,label>*) { outline: 1px dotted } /**/

* { box-sizing: border-box }

body { --scale: 0.5 }

.container {
    width: 800px;
    transform-origin: 0 0 0; /* default is 50% 50% 0 */
    scale: var(--scale);
}
[dir="rtl"] .container { transform-origin: 100% 0 0 }

/* just page makeup */
section {
    margin-top: 7rem;
}
label {
    position: fixed; top: 0; left: 0; z-index: 9999;
    display: inline-flex; flex-direction: column;
    width: 100%; padding: 1rem; font-size: 1rem;
    background-color: hsl(0,0%,96%,.95); cursor: pointer;
}
<label><span id="info-scale">Scale factor: 0.5 = 16px</span>
    <input type="range" min="0.1" max="3" value="0.5" step="0.01"
           oninput="document.body.style.setProperty('--scale', this.value);
                    document.getElementById('info-scale').innerHTML = 'Scale factor: ' + this.value + ' = ' + (this.value * 32) + 'px';">
</label>

<section dir="rtl">
    <h2>Hebrew Text</h2>
    <div class="container"></div>
</section>

相关问题