upgrade texte
This commit is contained in:
parent
b56af2906e
commit
a433f58604
2 changed files with 232 additions and 63 deletions
|
|
@ -2,56 +2,228 @@ export async function drawText(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log(filePath, "\n");
|
// New implementation: word-based wrapping, DPR-aware canvas resize,
|
||||||
const fileContent = await fetch(filePath)
|
// simple inline formatting markers: *bold*, ~italic~, #rrggbb{color}...
|
||||||
.then(response => response.text());
|
const raw = await fetch(filePath).then(r => r.text());
|
||||||
const size = 30;
|
|
||||||
const dl = 5;
|
|
||||||
const ll = 20;
|
|
||||||
const xx = 10;
|
|
||||||
const defaultColor = "#ffffff";
|
|
||||||
ctx.fillStyle = defaultColor;
|
|
||||||
ctx.font = size + "px Garamond";
|
|
||||||
|
|
||||||
let readingColor = false;
|
const DEFAULT_COLOR = "#ffffff";
|
||||||
let color = "";
|
const BASE_FONT_SIZE = 30; // in CSS px
|
||||||
let gras = "";
|
const FONT_FAMILY = "Garamond, serif";
|
||||||
let italic = "";
|
const MARGIN_X = 10;
|
||||||
|
const MARGIN_Y = 10;
|
||||||
|
const LINE_SPACING = 1.25; // multiplier
|
||||||
|
|
||||||
let line = 1;
|
const canvas = ctx.canvas;
|
||||||
let x = 0;
|
const dpr = (window.devicePixelRatio || 1);
|
||||||
for (var c of fileContent) {
|
|
||||||
if (c === '*') {
|
// Helper to build font string for canvas context
|
||||||
if (gras === "") {
|
const fontFor = (size: number, bold: boolean, italic: boolean) => {
|
||||||
gras = "bold ";
|
return `${italic ? 'italic ' : ''}${bold ? 'bold ' : ''}${size}px ${FONT_FAMILY}`;
|
||||||
} else {
|
};
|
||||||
gras = "";
|
|
||||||
|
// Tokenize raw text into words/newlines while preserving inline styles
|
||||||
|
type Style = { bold: boolean; italic: boolean; color?: string };
|
||||||
|
type Token = { text: string; style: Style };
|
||||||
|
|
||||||
|
function tokenize(input: string): Token[] {
|
||||||
|
const tokens: Token[] = [];
|
||||||
|
let i = 0;
|
||||||
|
let currentStyle: Style = { bold: false, italic: false };
|
||||||
|
while (i < input.length) {
|
||||||
|
const ch = input[i];
|
||||||
|
if (ch === '*') { currentStyle = { ...currentStyle, bold: !currentStyle.bold }; i++; continue; }
|
||||||
|
if (ch === '~') { currentStyle = { ...currentStyle, italic: !currentStyle.italic }; i++; continue; }
|
||||||
|
if (ch === '#') {
|
||||||
|
// read hex up to '{'
|
||||||
|
let hex = '#';
|
||||||
|
i++;
|
||||||
|
while (i < input.length && input[i] !== '{') { hex += input[i++]; }
|
||||||
|
if (i < input.length && input[i] === '{') {
|
||||||
|
// consume '{'
|
||||||
|
i++;
|
||||||
|
// read until closing '}' or newline
|
||||||
|
let inner = '';
|
||||||
|
while (i < input.length && input[i] !== '}') { inner += input[i++]; }
|
||||||
|
// consume '}' if present
|
||||||
|
if (i < input.length && input[i] === '}') i++;
|
||||||
|
// push inner text as a token with color hex
|
||||||
|
if (inner.length > 0) {
|
||||||
|
tokens.push({ text: inner, style: { ...currentStyle, color: hex } });
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// fallback: just treat '#' as normal char
|
||||||
|
tokens.push({ text: '#', style: { ...currentStyle } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ctx.font = italic + gras + size + "px Garamond";
|
|
||||||
} else if (c === '~') {
|
if (ch === '\n') {
|
||||||
if (italic === "") {
|
tokens.push({ text: '\n', style: { ...currentStyle } });
|
||||||
italic = "italic ";
|
i++; continue;
|
||||||
} else {
|
|
||||||
italic = "";
|
|
||||||
}
|
}
|
||||||
ctx.font = italic + gras + size + "px Garamond";
|
|
||||||
} else if (c === '#') {
|
// read a word (until whitespace or newline)
|
||||||
color = "#";
|
if (ch === ' ' || ch === '\t' || ch === '\r') {
|
||||||
readingColor = true;
|
// collapse sequences of spaces into single space token
|
||||||
} else if (c === '{') {
|
let spaces = '';
|
||||||
readingColor = false;
|
while (i < input.length && (input[i] === ' ' || input[i] === '\t' || input[i] === '\r')) { spaces += input[i++]; }
|
||||||
ctx.fillStyle = color;
|
tokens.push({ text: ' ', style: { ...currentStyle } });
|
||||||
} else if (c === '}') {
|
continue;
|
||||||
color = "";
|
}
|
||||||
ctx.fillStyle = defaultColor;
|
|
||||||
} else if (readingColor) {
|
// regular word
|
||||||
color += c;
|
let w = '';
|
||||||
} else if (c === '\n') {
|
while (i < input.length && input[i] !== ' ' && input[i] !== '\n' && input[i] !== '\t' && input[i] !== '\r') {
|
||||||
line++;
|
// handle formatting markers inside words by breaking
|
||||||
x = 0;
|
if (input[i] === '*' || input[i] === '~' || input[i] === '#') break;
|
||||||
|
w += input[i++];
|
||||||
|
}
|
||||||
|
if (w.length > 0) tokens.push({ text: w, style: { ...currentStyle } });
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = tokenize(raw);
|
||||||
|
|
||||||
|
// Use an offscreen canvas to measure text widths
|
||||||
|
const measureCanvas = document.createElement('canvas');
|
||||||
|
const mctx = measureCanvas.getContext('2d') as CanvasRenderingContext2D;
|
||||||
|
|
||||||
|
// Determine max allowed width (allow text to expand but cap to viewport)
|
||||||
|
const MAX_ALLOWED_CSS_WIDTH = Math.max(200, Math.min(window.innerWidth - 40, 1050));
|
||||||
|
|
||||||
|
// Build lines by measuring tokens and wrapping
|
||||||
|
const lines: Token[][] = [];
|
||||||
|
let currentLine: Token[] = [];
|
||||||
|
let currentLineWidth = 0;
|
||||||
|
let measuredMaxLineWidth = 0;
|
||||||
|
|
||||||
|
const spaceWidthCache = new Map<string, number>();
|
||||||
|
|
||||||
|
function measureTokenWidth(tok: Token) {
|
||||||
|
const font = fontFor(BASE_FONT_SIZE, tok.style.bold, tok.style.italic);
|
||||||
|
mctx.font = font;
|
||||||
|
return mctx.measureText(tok.text).width;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
const tok = tokens[i];
|
||||||
|
if (tok.text === '\n') {
|
||||||
|
// push current line and start a new one
|
||||||
|
lines.push(currentLine);
|
||||||
|
measuredMaxLineWidth = Math.max(measuredMaxLineWidth, currentLineWidth);
|
||||||
|
currentLine = [];
|
||||||
|
currentLineWidth = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// measure token width
|
||||||
|
let w = measureTokenWidth(tok);
|
||||||
|
|
||||||
|
// treat spaces as small width
|
||||||
|
if (tok.text === ' ') {
|
||||||
|
currentLine.push(tok);
|
||||||
|
currentLineWidth += w;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If token is wider than the allowed width, try to break it into smaller tokens (characters)
|
||||||
|
const availableWidth = MAX_ALLOWED_CSS_WIDTH - 2 * MARGIN_X;
|
||||||
|
if (w > availableWidth) {
|
||||||
|
// break the token into single-character tokens (preserve style)
|
||||||
|
for (const ch of tok.text) {
|
||||||
|
const charTok: Token = { text: ch, style: { ...tok.style } };
|
||||||
|
const cw = measureTokenWidth(charTok);
|
||||||
|
if (currentLine.length > 0 && (currentLineWidth + cw) > availableWidth) {
|
||||||
|
lines.push(currentLine);
|
||||||
|
measuredMaxLineWidth = Math.max(measuredMaxLineWidth, currentLineWidth);
|
||||||
|
currentLine = [charTok];
|
||||||
|
currentLineWidth = cw;
|
||||||
|
} else {
|
||||||
|
currentLine.push(charTok);
|
||||||
|
currentLineWidth += cw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If adding this word would exceed max width, wrap
|
||||||
|
if (currentLine.length > 0 && (currentLineWidth + w) > availableWidth) {
|
||||||
|
lines.push(currentLine);
|
||||||
|
measuredMaxLineWidth = Math.max(measuredMaxLineWidth, currentLineWidth);
|
||||||
|
currentLine = [tok];
|
||||||
|
currentLineWidth = w;
|
||||||
} else {
|
} else {
|
||||||
ctx.fillText(c, xx + x, ll + line * (size + dl));
|
currentLine.push(tok);
|
||||||
x += ctx.measureText(c).width;
|
currentLineWidth += w;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// push last line
|
||||||
|
if (currentLine.length > 0) {
|
||||||
|
lines.push(currentLine);
|
||||||
|
measuredMaxLineWidth = Math.max(measuredMaxLineWidth, currentLineWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssWidth = Math.min(MAX_ALLOWED_CSS_WIDTH, Math.ceil(measuredMaxLineWidth + 2 * MARGIN_X));
|
||||||
|
const lineHeight = Math.ceil(BASE_FONT_SIZE * LINE_SPACING);
|
||||||
|
const cssHeight = Math.ceil(lines.length * lineHeight + 2 * MARGIN_Y);
|
||||||
|
|
||||||
|
// Resize canvas with DPR awareness. Re-get context after setting width/height (context is reset).
|
||||||
|
canvas.style.width = cssWidth + 'px';
|
||||||
|
canvas.style.height = cssHeight + 'px';
|
||||||
|
canvas.width = Math.max(1, Math.round(cssWidth * dpr));
|
||||||
|
canvas.height = Math.max(1, Math.round(cssHeight * dpr));
|
||||||
|
|
||||||
|
const drawCtx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||||
|
// reset transform and scale for DPR
|
||||||
|
drawCtx.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
|
drawCtx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
// Do not clear background here — caller may have drawn background. But
|
||||||
|
// to avoid artifacts on increased canvas size, we can clear the interior area.
|
||||||
|
drawCtx.clearRect(0, 0, cssWidth, cssHeight);
|
||||||
|
|
||||||
|
// Draw lines
|
||||||
|
let y = MARGIN_Y + BASE_FONT_SIZE; // baseline for first line
|
||||||
|
for (const lineTokens of lines) {
|
||||||
|
let x = MARGIN_X;
|
||||||
|
for (const tok of lineTokens) {
|
||||||
|
if (tok.text === ' ') {
|
||||||
|
// measure and advance
|
||||||
|
drawCtx.font = fontFor(BASE_FONT_SIZE, tok.style.bold, tok.style.italic);
|
||||||
|
const sw = drawCtx.measureText(' ').width;
|
||||||
|
x += sw;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
drawCtx.font = fontFor(BASE_FONT_SIZE, tok.style.bold, tok.style.italic);
|
||||||
|
drawCtx.fillStyle = tok.style.color || DEFAULT_COLOR;
|
||||||
|
drawCtx.fillText(tok.text, x, y);
|
||||||
|
const w = drawCtx.measureText(tok.text).width;
|
||||||
|
x += w;
|
||||||
|
}
|
||||||
|
y += lineHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update an offscreen accessible copy for screen readers
|
||||||
|
try {
|
||||||
|
let htmlCopy = document.getElementById('textHtmlCopy');
|
||||||
|
if (!htmlCopy) {
|
||||||
|
htmlCopy = document.createElement('div');
|
||||||
|
htmlCopy.id = 'textHtmlCopy';
|
||||||
|
htmlCopy.style.position = 'absolute';
|
||||||
|
htmlCopy.style.left = '-9999px';
|
||||||
|
htmlCopy.style.top = 'auto';
|
||||||
|
htmlCopy.style.width = '1px';
|
||||||
|
htmlCopy.style.height = '1px';
|
||||||
|
htmlCopy.style.overflow = 'hidden';
|
||||||
|
document.body.appendChild(htmlCopy);
|
||||||
|
}
|
||||||
|
// plain text variant: strip formatting markers
|
||||||
|
const plain = raw.replace(/\*|~/g, '').replace(/#([0-9a-fA-F]+)\{([^}]*)\}/g, '$2');
|
||||||
|
htmlCopy.textContent = plain;
|
||||||
|
} catch (e) {
|
||||||
|
// non-fatal
|
||||||
|
console.warn('Could not create accessible text copy', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,27 @@
|
||||||
~Introduction~
|
~Introduction~
|
||||||
|
|
||||||
Bienvenue sur ce site dédié au *Mahjong* !
|
Bienvenue sur ce site dédié au *Riichi Mahjong* !
|
||||||
|
|
||||||
Ce site est principalement destiné aux personnes découvrant ce
|
Ce site s'adresse principalement aux personnes découvrant ce
|
||||||
formidable jeu.
|
formidable jeu.
|
||||||
|
|
||||||
Le nombre de chapitre de chaque section peut faire peur aux
|
Le nombre de chapitres par section peut sembler impressionnant
|
||||||
premiers abords, mais chacun deux n'est pas plus long que ce
|
au premier abord, mais chacun d'eux reste court et facilement
|
||||||
même texte
|
abordable.
|
||||||
|
|
||||||
Cependant, de nombreux services y sont proposés:
|
Vous trouverez notamment :
|
||||||
|
|
||||||
- *Initiation riichi majhong* dans l'onglet #ff00ff{Riichi Mahjong}
|
- *Initiation au Riichi Mahjong* dans l'onglet #ff00ff{Riichi Mahjong}.
|
||||||
|
|
||||||
- *Explication d'une partie en physique* dans l'onglet #ff00ff{Partie
|
- *Explication d'une partie réelle* dans l'onglet #ff00ff{Partie réelle}
|
||||||
réelle} pour comprendre comment y jouer dans la vraie vie
|
pour apprendre à jouer en conditions réelles.
|
||||||
|
|
||||||
- *Introduction à une variante* dans l'onglet #ff00ff{Riichi à trois}
|
- *Présentation d'une variante* dans l'onglet #ff00ff{Riichi à trois}.
|
||||||
|
|
||||||
- *Un entrainement aux différents Yakus* dans l'onglet #ff00ff{Yaku
|
- *Entraînement aux différents Yakus* dans l'onglet #ff00ff{Yaku trainer}.
|
||||||
trainer}
|
|
||||||
|
|
||||||
- *Une introduction au MCR*, la variante chinoise dans l'onglet
|
- *Introduction au MCR* (variante chinoise) dans l'onglet #ff00ff{MCR}.
|
||||||
#ff00ff{MCR}
|
|
||||||
|
|
||||||
Evidemment, ce site n'est pas encore fini, et possède encore
|
Ce site est en développement — il contient encore des pages incomplètes
|
||||||
beaucoup de problème. Pour le moindre retour, ne pas hésiter
|
et des améliorations à apporter. N'hésitez pas à poster vos retours
|
||||||
à le faire savoir directement sur le Github pour pour aider
|
sur le dépôt GitHub pour contribuer à son amélioration.
|
||||||
à l'améliorer.
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue