Export template source from e33714a5ca38429d1bdb77a961a14461697944f5:examples/local-first/packages/language-reader-presentation

This commit is contained in:
Quixos Template Publisher
2000-01-01 00:00:00 +00:00
commit 8f327c9ac8
63 changed files with 10474 additions and 0 deletions
+245
View File
@@ -0,0 +1,245 @@
import { layoutText } from "../src/canvas/layout.js";
import { canvasMeasure } from "../src/canvas/measure.js";
import { rectIntersects, type Annotation, type Font } from "../src/canvas/types.js";
import { drawText, drawUnderline } from "../src/canvas/draw.js";
import type { AnnotationBox, UnderlinePattern } from "../src/canvas/types.js";
export function sourceGeometry(
text: string,
width: number,
font: Font = { size: 23, family: '"Noto Serif CJK SC", "Songti SC", Georgia, serif' },
) {
const context = document.createElement("canvas").getContext("2d")!;
return layoutText({
text,
width,
font,
measure: canvasMeasure(context),
}).lines.flatMap((line) => line.glyphs);
}
export function verifyBrowserMetrics() {
const canvas = document.createElement("canvas"),
context = canvas.getContext("2d")!;
const text = " 阅读中文,👩🏽‍💻 café é。\n\n另一段阅读材料。";
let cases = 0;
for (const width of [30, 60, 120, 320, 640])
for (const size of [18, 23, 34])
for (const count of [2, 8, 30]) {
const annotations: Annotation[] = Array.from({ length: count }, (_, i) => ({
id: String(i),
feature: "same",
start: i % 5,
end: 6 + (i % 10),
style: "overhead",
text: i % 2 ? "Some long and overlapping explanatory text" : "阅读注解",
color: "#53796c",
fontSize: 10 + (i % 4) * 7,
}));
const layout = layoutText({
text,
width,
font: { size, family: '"Noto Serif CJK SC",Georgia,serif' },
annotations,
measure: canvasMeasure(context),
});
const boxes = layout.lines.flatMap((line) => line.overhead);
for (const line of layout.lines)
if (new Set(line.overhead.map((box) => box.baseline)).size > 1)
throw Error("One annotation source was split into multiple rows");
for (const box of boxes) {
if (box.x < 0 || box.x + box.width > width + 0.01) throw Error("Overhead box outside browser viewport");
for (const other of boxes)
if (box !== other && rectIntersects(box, other))
throw Error("Overhead text overlap with real browser metrics");
for (const line of layout.lines)
for (const glyph of line.glyphs)
if (rectIntersects(box, glyph)) throw Error("Overhead text overlaps source with browser metrics");
}
cases++;
}
const pinyin = ["cháng", "xiǎoyǔ", "zǒuchū", "dìtiě", "zhàn"].map(
(text, i): Annotation => ({
id: String(i),
feature: "pinyin",
start: i * 2,
end: i * 2 + 2,
style: "overhead",
text,
color: "green",
}),
);
const adjacent = layoutText({
text: "一场小雨走出地铁车站",
width: 640,
annotations: pinyin,
measure: canvasMeasure(context),
});
if (
adjacent.lines[0].overhead.length !== pinyin.length ||
new Set(adjacent.lines[0].overhead.map((box) => box.baseline)).size !== 1
)
throw Error("Adjacent pinyin zig-zags with real browser metrics");
const long =
"This is a very long sentence translation that should share source flow across several physical text lines.";
const twoSources = layoutText({
text: "一场小雨。",
width: 120,
annotations: [
...pinyin.slice(0, 2),
{
id: "sentence",
feature: "translation",
start: 0,
end: 5,
style: "overhead",
text: long,
color: "red",
priority: 1,
},
],
measure: canvasMeasure(context),
});
const sentence = twoSources.lines
.flatMap((line) => line.overhead)
.filter((box) => box.annotation.feature === "translation");
if (sentence.length < 3 || sentence.flatMap((box) => box.lines).join("") !== long)
throw Error("Long translation did not flow intact across text lines");
for (const line of twoSources.lines)
for (const feature of ["pinyin", "translation"])
if (
new Set(line.overhead.filter((box) => box.annotation.feature === feature).map((box) => box.baseline)).size > 1
)
throw Error("A word/sentence source gained a collision row");
return { cases, fixedRows: true, font: context.font };
}
import { readingPages } from "../src/canvas/pages.js";
export function sourcePages(text: string, width: number, height: number) {
const context = document.createElement("canvas").getContext("2d")!;
return readingPages(
layoutText({
text,
width,
font: { size: 23, family: '"Noto Serif CJK SC", "Songti SC", Georgia, serif' },
measure: canvasMeasure(context),
}),
height,
);
}
/** Exercise actual canvas rasterization: patterned strokes must stay in their
* band, include gaps/waves, and leave layout and selection geometry unchanged. */
export function verifyUnderlineStyles() {
const patterns: UnderlinePattern[] = ["solid", "dashed", "squiggly"];
const canvas = document.createElement("canvas");
canvas.width = 100;
canvas.height = 40;
const context = canvas.getContext("2d")!;
for (const pattern of patterns) {
context.clearRect(0, 0, canvas.width, canvas.height);
const box: AnnotationBox = {
x: 10,
y: 10,
width: 72,
height: 3,
layer: 0,
baseline: 0,
lines: [],
font: { family: "serif", size: 23 },
annotation: {
id: pattern,
feature: "test",
start: 0,
end: 1,
text: "",
style: "underline",
color: "#53796c",
underlinePattern: pattern,
},
};
drawUnderline(context, box);
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
const columns = new Set<number>(),
rows = new Set<number>();
for (let y = 0; y < canvas.height; y++)
for (let x = 0; x < canvas.width; x++)
if (pixels[(y * canvas.width + x) * 4 + 3]) {
if (x < 10 || x >= 82 || y < 10 || y >= 13) throw Error("Underline escapes its decoration band");
columns.add(x);
rows.add(y);
}
if (columns.size < 30) throw Error("Underline was not drawn");
if (pattern === "solid" && columns.size !== 72) throw Error("Solid underline has gaps");
if (pattern === "dashed" && columns.size >= 72) throw Error("Dashed underline has no gaps");
if (pattern === "squiggly" && rows.size !== 3) throw Error("Squiggly underline is flat");
if (context.getLineDash().length) throw Error("Underline leaked its dash state");
}
const text = " Several words wrapping onto another line.\n\tA new paragraph.";
let geometry = "";
for (const pattern of patterns) {
const layout = layoutText({
text,
width: 120,
measure: canvasMeasure(context),
annotations: [
{
id: "underlined",
feature: "test",
start: 0,
end: text.length,
text: "",
style: "underline",
color: "#53796c",
underlinePattern: pattern,
},
],
});
const current = JSON.stringify(
layout.lines.map(({ glyphs, y, height, decorations }) => ({
glyphs,
y,
height,
decorations: decorations.map(({ x, y, width, height }) => ({ x, y, width, height })),
})),
);
if (geometry && current !== geometry) throw Error("Underline pattern changes layout");
geometry = current;
for (const line of layout.lines)
if (/^\s/u.test(line.glyphs[0]?.text ?? "")) throw Error("Rendered line begins with a space");
}
return { patterns: patterns.length, clipped: true, stableGeometry: true };
}
export function underlinePreview() {
const canvas = document.createElement("canvas");
canvas.width = 440;
canvas.height = 200;
const context = canvas.getContext("2d")!;
context.fillStyle = "#fafaf6";
context.fillRect(0, 0, canvas.width, canvas.height);
context.translate(20, 10);
const text = "Solid underline\nDashed underline\nSquiggly underline";
const starts = [0, text.indexOf("Dashed"), text.indexOf("Squiggly")];
const annotations = (["solid", "dashed", "squiggly"] as const).map(
(pattern, i): Annotation => ({
id: pattern,
feature: pattern,
start: starts[i],
end: i === 2 ? text.length : starts[i + 1] - 1,
text: "",
style: "underline",
color: "#53796c",
underlinePattern: pattern,
}),
);
drawText(
context,
layoutText({
text,
width: 400,
font: { family: "Georgia, serif", size: 23 },
measure: canvasMeasure(context),
annotations,
}),
);
return canvas.toDataURL();
}