gistfile1.txt
· 3.7 KiB · Text
Brut
/**
* TextAlign — approximate column alignment for Discord embeds using
* invisible filler characters (ㅤ U+3164 Hangul Filler).
*
* Discord embed text is NOT monospace, so naive character-count padding
* misaligns whenever strings contain narrow characters (punctuation,
* symbols like »«) mixed with regular letters. This module estimates
* VISUAL width per character (not raw count) for better alignment.
*
* EXPERIMENTAL — width weights are approximations tuned by visual
* comparison in actual Discord clients, not measured font metrics.
* Iterate WIDTH_TABLE based on screenshots.
*
* Usage:
* import { TextAlign } from "@ui/text-align";
*
* const width = TextAlign.estimateWidth("»Flash«");
* const padded = TextAlign.pad("»Flash«", maxWidth);
* const maxWidth = TextAlign.maxWidth(["»Flash«", "XefronYokuda", ...]);
*/
import { Logger } from "../systems/logger";
const log = Logger.for("TextAlign");
const FILLER = "\u2009"; // Thin Space — finer-grained than Hangul Filler, better precision
// ─── Width estimation ──────────────────────────────────────────────────────
// Flat character width (1.0) for ALL characters, including guillemets.
// IMPORTANT: guillemet narrowness is already implicitly absorbed into the
// FILLERS_PER_CHAR calibration below (calibrated using names that included
// guillemets). Adding a SEPARATE per-character guillemet correction here
// double-counts the effect and causes overcorrection. Do not reintroduce
// without recalibrating FILLERS_PER_CHAR from scratch using guillemet-free
// reference names only.
function charWidth(ch: string): number {
return 1.0;
}
// Estimated width of the invisible filler character itself, relative to
// one standard letter, SPECIFICALLY INSIDE EMBEDS using Thin Space (U+2009).
// CALIBRATED via live embed testing:
// «Keira» (7) vs XefronYokuda (12), diff 5 chars — exactly 14 thin fillers
// »No.1« (6) vs «MonkeyHunter» (14), diff 8 chars — exactly 23 thin fillers
// Combined: 37 fillers per 13 missing chars => 1 char ≈ 2.846 fillers
const FILLERS_PER_CHAR = 37 / 13; // ≈ 2.846
const FILLER_WIDTH = 1 / FILLERS_PER_CHAR; // ≈ 0.351 (letter-widths per filler)
// ─── Namespace ────────────────────────────────────────────────────────────────
export const TextAlign = {
/**
* Estimate the visual width of a string in "units" (1.0 = one average letter).
*/
estimateWidth(text: string): number {
return text.split("").reduce((sum, ch) => sum + charWidth(ch), 0);
},
/**
* Get the max estimated width across a list of strings.
*/
maxWidth(texts: string[]): number {
return Math.max(...texts.map((t) => TextAlign.estimateWidth(t)), 0);
},
/**
* Pad a string with invisible filler characters to reach a target width.
*/
pad(text: string, targetWidth: number): string {
const current = TextAlign.estimateWidth(text);
const diff = targetWidth - current;
if (diff <= 0) return text;
const fillerCount = Math.round(diff / FILLER_WIDTH);
log.info(`[TextAlign] "${text}": estimatedWidth=${current.toFixed(3)} target=${targetWidth.toFixed(3)} diff=${diff.toFixed(3)} fillerCount=${fillerCount}`);
return text + FILLER.repeat(Math.max(0, fillerCount));
},
/**
* Pad a string to match the widest string in a list (convenience).
*/
padToMax(text: string, allTexts: string[]): string {
return TextAlign.pad(text, TextAlign.maxWidth(allTexts));
},
};
| 1 | /** |
| 2 | * TextAlign — approximate column alignment for Discord embeds using |
| 3 | * invisible filler characters (ㅤ U+3164 Hangul Filler). |
| 4 | * |
| 5 | * Discord embed text is NOT monospace, so naive character-count padding |
| 6 | * misaligns whenever strings contain narrow characters (punctuation, |
| 7 | * symbols like »«) mixed with regular letters. This module estimates |
| 8 | * VISUAL width per character (not raw count) for better alignment. |
| 9 | * |
| 10 | * EXPERIMENTAL — width weights are approximations tuned by visual |
| 11 | * comparison in actual Discord clients, not measured font metrics. |
| 12 | * Iterate WIDTH_TABLE based on screenshots. |
| 13 | * |
| 14 | * Usage: |
| 15 | * import { TextAlign } from "@ui/text-align"; |
| 16 | * |
| 17 | * const width = TextAlign.estimateWidth("»Flash«"); |
| 18 | * const padded = TextAlign.pad("»Flash«", maxWidth); |
| 19 | * const maxWidth = TextAlign.maxWidth(["»Flash«", "XefronYokuda", ...]); |
| 20 | */ |
| 21 | |
| 22 | import { Logger } from "../systems/logger"; |
| 23 | |
| 24 | const log = Logger.for("TextAlign"); |
| 25 | |
| 26 | const FILLER = "\u2009"; // Thin Space — finer-grained than Hangul Filler, better precision |
| 27 | |
| 28 | // ─── Width estimation ────────────────────────────────────────────────────── |
| 29 | // Flat character width (1.0) for ALL characters, including guillemets. |
| 30 | // IMPORTANT: guillemet narrowness is already implicitly absorbed into the |
| 31 | // FILLERS_PER_CHAR calibration below (calibrated using names that included |
| 32 | // guillemets). Adding a SEPARATE per-character guillemet correction here |
| 33 | // double-counts the effect and causes overcorrection. Do not reintroduce |
| 34 | // without recalibrating FILLERS_PER_CHAR from scratch using guillemet-free |
| 35 | // reference names only. |
| 36 | function charWidth(ch: string): number { |
| 37 | return 1.0; |
| 38 | } |
| 39 | |
| 40 | // Estimated width of the invisible filler character itself, relative to |
| 41 | // one standard letter, SPECIFICALLY INSIDE EMBEDS using Thin Space (U+2009). |
| 42 | // CALIBRATED via live embed testing: |
| 43 | // «Keira» (7) vs XefronYokuda (12), diff 5 chars — exactly 14 thin fillers |
| 44 | // »No.1« (6) vs «MonkeyHunter» (14), diff 8 chars — exactly 23 thin fillers |
| 45 | // Combined: 37 fillers per 13 missing chars => 1 char ≈ 2.846 fillers |
| 46 | const FILLERS_PER_CHAR = 37 / 13; // ≈ 2.846 |
| 47 | const FILLER_WIDTH = 1 / FILLERS_PER_CHAR; // ≈ 0.351 (letter-widths per filler) |
| 48 | |
| 49 | // ─── Namespace ──────────────────────────────────────────────────────────────── |
| 50 | |
| 51 | export const TextAlign = { |
| 52 | /** |
| 53 | * Estimate the visual width of a string in "units" (1.0 = one average letter). |
| 54 | */ |
| 55 | estimateWidth(text: string): number { |
| 56 | return text.split("").reduce((sum, ch) => sum + charWidth(ch), 0); |
| 57 | }, |
| 58 | |
| 59 | /** |
| 60 | * Get the max estimated width across a list of strings. |
| 61 | */ |
| 62 | maxWidth(texts: string[]): number { |
| 63 | return Math.max(...texts.map((t) => TextAlign.estimateWidth(t)), 0); |
| 64 | }, |
| 65 | |
| 66 | /** |
| 67 | * Pad a string with invisible filler characters to reach a target width. |
| 68 | */ |
| 69 | pad(text: string, targetWidth: number): string { |
| 70 | const current = TextAlign.estimateWidth(text); |
| 71 | const diff = targetWidth - current; |
| 72 | if (diff <= 0) return text; |
| 73 | const fillerCount = Math.round(diff / FILLER_WIDTH); |
| 74 | log.info(`[TextAlign] "${text}": estimatedWidth=${current.toFixed(3)} target=${targetWidth.toFixed(3)} diff=${diff.toFixed(3)} fillerCount=${fillerCount}`); |
| 75 | return text + FILLER.repeat(Math.max(0, fillerCount)); |
| 76 | }, |
| 77 | |
| 78 | /** |
| 79 | * Pad a string to match the widest string in a list (convenience). |
| 80 | */ |
| 81 | padToMax(text: string, allTexts: string[]): string { |
| 82 | return TextAlign.pad(text, TextAlign.maxWidth(allTexts)); |
| 83 | }, |
| 84 | }; |