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,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
): Promise<void> {
|
||||
console.log(filePath, "\n");
|
||||
const fileContent = await fetch(filePath)
|
||||
.then(response => response.text());
|
||||
const size = 30;
|
||||
const dl = 5;
|
||||
const ll = 20;
|
||||
const xx = 10;
|
||||
const defaultColor = "#ffffff";
|
||||
ctx.fillStyle = defaultColor;
|
||||
ctx.font = size + "px Garamond";
|
||||
// New implementation: word-based wrapping, DPR-aware canvas resize,
|
||||
// simple inline formatting markers: *bold*, ~italic~, #rrggbb{color}...
|
||||
const raw = await fetch(filePath).then(r => r.text());
|
||||
|
||||
let readingColor = false;
|
||||
let color = "";
|
||||
let gras = "";
|
||||
let italic = "";
|
||||
const DEFAULT_COLOR = "#ffffff";
|
||||
const BASE_FONT_SIZE = 30; // in CSS px
|
||||
const FONT_FAMILY = "Garamond, serif";
|
||||
const MARGIN_X = 10;
|
||||
const MARGIN_Y = 10;
|
||||
const LINE_SPACING = 1.25; // multiplier
|
||||
|
||||
let line = 1;
|
||||
let x = 0;
|
||||
for (var c of fileContent) {
|
||||
if (c === '*') {
|
||||
if (gras === "") {
|
||||
gras = "bold ";
|
||||
} else {
|
||||
gras = "";
|
||||
const canvas = ctx.canvas;
|
||||
const dpr = (window.devicePixelRatio || 1);
|
||||
|
||||
// Helper to build font string for canvas context
|
||||
const fontFor = (size: number, bold: boolean, italic: boolean) => {
|
||||
return `${italic ? 'italic ' : ''}${bold ? 'bold ' : ''}${size}px ${FONT_FAMILY}`;
|
||||
};
|
||||
|
||||
// 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 (italic === "") {
|
||||
italic = "italic ";
|
||||
} else {
|
||||
italic = "";
|
||||
|
||||
if (ch === '\n') {
|
||||
tokens.push({ text: '\n', style: { ...currentStyle } });
|
||||
i++; continue;
|
||||
}
|
||||
ctx.font = italic + gras + size + "px Garamond";
|
||||
} else if (c === '#') {
|
||||
color = "#";
|
||||
readingColor = true;
|
||||
} else if (c === '{') {
|
||||
readingColor = false;
|
||||
ctx.fillStyle = color;
|
||||
} else if (c === '}') {
|
||||
color = "";
|
||||
ctx.fillStyle = defaultColor;
|
||||
} else if (readingColor) {
|
||||
color += c;
|
||||
} else if (c === '\n') {
|
||||
line++;
|
||||
x = 0;
|
||||
|
||||
// read a word (until whitespace or newline)
|
||||
if (ch === ' ' || ch === '\t' || ch === '\r') {
|
||||
// collapse sequences of spaces into single space token
|
||||
let spaces = '';
|
||||
while (i < input.length && (input[i] === ' ' || input[i] === '\t' || input[i] === '\r')) { spaces += input[i++]; }
|
||||
tokens.push({ text: ' ', style: { ...currentStyle } });
|
||||
continue;
|
||||
}
|
||||
|
||||
// regular word
|
||||
let w = '';
|
||||
while (i < input.length && input[i] !== ' ' && input[i] !== '\n' && input[i] !== '\t' && input[i] !== '\r') {
|
||||
// handle formatting markers inside words by breaking
|
||||
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 {
|
||||
ctx.fillText(c, xx + x, ll + line * (size + dl));
|
||||
x += ctx.measureText(c).width;
|
||||
currentLine.push(tok);
|
||||
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~
|
||||
|
||||
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.
|
||||
|
||||
Le nombre de chapitre de chaque section peut faire peur aux
|
||||
premiers abords, mais chacun deux n'est pas plus long que ce
|
||||
même texte
|
||||
Le nombre de chapitres par section peut sembler impressionnant
|
||||
au premier abord, mais chacun d'eux reste court et facilement
|
||||
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
|
||||
réelle} pour comprendre comment y jouer dans la vraie vie
|
||||
- *Explication d'une partie réelle* dans l'onglet #ff00ff{Partie réelle}
|
||||
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
|
||||
trainer}
|
||||
- *Entraînement aux différents Yakus* dans l'onglet #ff00ff{Yaku trainer}.
|
||||
|
||||
- *Une introduction au MCR*, la variante chinoise dans l'onglet
|
||||
#ff00ff{MCR}
|
||||
- *Introduction au MCR* (variante chinoise) dans l'onglet #ff00ff{MCR}.
|
||||
|
||||
Evidemment, ce site n'est pas encore fini, et possède encore
|
||||
beaucoup de problème. Pour le moindre retour, ne pas hésiter
|
||||
à le faire savoir directement sur le Github pour pour aider
|
||||
à l'améliorer.
|
||||
Ce site est en développement — il contient encore des pages incomplètes
|
||||
et des améliorations à apporter. N'hésitez pas à poster vos retours
|
||||
sur le dépôt GitHub pour contribuer à son amélioration.
|
||||
|
|
|
|||
Loading…
Reference in a new issue