nuno 已修改 2 weeks ago. 還原成這個修訂版本
1 file changed, 84 insertions
gistfile1.txt(檔案已創建)
| @@ -0,0 +1,84 @@ | |||
| 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 | + | }; | |
上一頁
下一頁