我所指的Sefer Torah布局在here中描述,它看起来像this(我无法快速找到更好的图像):
基本上,您会注意到 some “带有水平条的字母”沿条形部分水平拉伸。这有助于它使文本完美地填充该列。
在给定任何旧希伯来语字体的情况下,如何在 JavaScript 中完成此操作?
就好像你需要分割字形并只在中间扩展它,根本不需要拉伸垂直部分。我不确定这是否简单、容易,甚至可能。您将如何以一种实用的方式(即不需要大量研究或复杂的博士算法)在高层次上实现这一点?有没有什么技巧可以用字体做到这一点?
我找到了一些旧代码来解决它,至少是部分解决。我不确定我从哪里得到代码或者我是否编写了代码(JavaScript,现在是 TypeScript)。但它将 SVG 路径转换为模板化字符串,您可以在正确的位置缩放(大部分),它本身一定已经花费了很多人的工作,我不认为我这样做了,我一定是在某个地方找到了它一些点。然后它使用一些我不完全理解的有趣算法来计算字母的优先级以缩放它们。总的来说,我会给解决方案一个 B-/B,它已经到达那里,但还不完美。如果您可以改进它,或者知道更好的东西,请继续分享!
这是没有字形和数据的代码(仍然在 Codepen 上),它太大了所以无法拥有所有内容。
export 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",
];
export type MeasurementSpreadParamsType = {
glyph: GlyphWithAdjustmentType;
scalePriority: number;
};
const scalePriority: Record<string, number> = {
ד: 1,
ה: 3,
//'ב': '3.5',
// ל: 2.5,
ר: 2,
ת: 4,
};
export type MeasurementStateType = {
alphabet: Array<string>;
bounds: Array<DOMRect>;
fontSize: number;
lineOffset: number;
maxWidth: number;
};
const X = 2785 / 56;
const Y = 3030;
const R = X / Y;
const LETTER_OFFSET = 1300;
const SPACE_OFFSET = 0;
export function computeLines(
words: Array<string>,
state: MeasurementStateType
) {
let line: Array<string> = [];
const lines: Array<Array<string>> = [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;
}
export function getOffset(
glyph: GlyphWithAdjustmentType,
state: MeasurementStateType
) {
const idx = state.alphabet.indexOf(glyph.symbol);
const bounds = state.bounds[idx];
const width = bounds.width + glyph.adjust;
return R * width + LETTER_OFFSET;
}
export function initializeState(
fontSize: number,
maxWidth: number
): MeasurementStateType {
const alphabet = ALPHABET;
const bounds = measureAllSVGGlyphs(alphabet, fontSize);
return { alphabet, bounds, fontSize, lineOffset: 0, maxWidth };
}
function formatLine(line: Array<string>, state: MeasurementStateType) {
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: Array<MeasurementSpreadParamsType> = [];
const stretchB: Array<MeasurementSpreadParamsType> = [];
const stretchC: Array<MeasurementSpreadParamsType> = [];
const stretchD: Array<MeasurementSpreadParamsType> = [];
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: Array<number> = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
var spectrum2: Array<number> = [];
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: number,
path: string,
symbol: string,
scale: number,
state: MeasurementStateType
) {
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})`;
return (
<path key={index} d={path.replace(/\s+/g, " ")} transform={transform} />
);
}
function createSpace(index: number, state: MeasurementStateType) {
const el = (
<rect
key={index}
width={SPACE_OFFSET}
height={0}
transform={`translate(${state.lineOffset},0)`}
/>
);
state.lineOffset += SPACE_OFFSET;
return el;
}
export function layout(
lines: Array<Array<string>>,
state: MeasurementStateType
) {
const svgs: Array<React.ReactNode> = [];
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: Array<React.ReactNode> = [];
glyphs.forEach((glyph) => {
const g = GLYPHS[glyph.symbol];
const text = typeof g == "function" ? g(glyph.adjust) : g;
if (glyph.symbol == " " || glyph.symbol == "\u00a0") {
// paths.push(createSpace(I, state))
state.lineOffset += SPACE_OFFSET;
// I++
} else {
paths.push(
createDynamicPath(I, text, glyph.symbol, glyph.adjust, state)
);
I++;
// paths.push(createSpace(I, state))
// state.lineOffset += SPACE_OFFSET
I++;
}
});
svgs.push(
<svg
key={i}
xmlns="http://www.w3.org/2000/svg"
version="1.1"
viewBox={`0 -1900 ${state.maxWidth} 3030`}
height={`${state.fontSize}`}
>
{paths}
</svg>
);
state.lineOffset = 0;
});
return svgs;
}
export function measureAllSVGGlyphs(glyphs: Array<string>, fontSize: number) {
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;
}
export function measureSVGGlyph(svg: Node, path: GlyphType) {
// 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: Array<string>, fontSize: number, 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: number, max: number) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
}
function sumLine(
glyphs: Array<GlyphWithAdjustmentType>,
state: MeasurementStateType
) {
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: number, spectrum: Array<number>) {
var smallSum = 0;
spectrum.forEach((x) => (smallSum += x));
var ratios = spectrum.map((x) => x / smallSum);
return ratios.map((x) => x * total);
}
const App = () => {
const [sefer, setSefer] = React.useState<React.ReactNode>([]);
React.useEffect(() => {
const words: Array<string> = [];
chapter.verses.forEach((v) => {
words.push(
...String(v.value[0].text)
.split(/\s+/)
.map((word) => {
return [...word].filter((c) => ALPHABET.includes(c)).join("");
})
);
});
setSefer(render(words, 16));
}, [chapter]);
return (
<div style={{ width: 800 }}>
<h2>sefer torah demo</h2>
<div>{sefer}</div>
</div>
);
};
ReactDOM.render(<App />, document.querySelector(".container"));