|
|
| Line 1: |
Line 1: |
| local p = {} | | local p = {} |
| local ustring = mw.ustring | | local ustring = mw.ustring |
| | local textfit = require "Module:TextFit" |
|
| |
|
| -- ============================================================================ | | -- ============================================================================ |
| -- Retail Demo (bold) glyph advance widths, in em of the font size. | | -- Name box geometry, in em of .itembox's font-size (1em = 10px at size=1). |
| -- unitsPerEm = 1000. Measured from /images/d/d5/Retaildemo-bold.woff2 | | -- Mirrors Template:ItemBox/styles.css: |
| -- Table can be regenerated with fontforge / fonttools | | -- .itembox width: 8em |
| | -- .itembox-name-area height: 4.5em; padding: 0.5em 0.4em (border-box) |
| | -- .itembox-name line-height: 1.2; font-size: 1.5em (overridden inline) |
| | -- so the name's content box is 8 - 2*0.4 = 7.2em wide and 4.5 - 2*0.5 = 3.5em |
| | -- tall. Keep these in step with that stylesheet. |
| | -- Glyph widths live in [[Module:TextFit]]. |
| -- ============================================================================ | | -- ============================================================================ |
| local WIDTH = { | | local NAME_W = 7.2 |
| A=0.677, B=0.590, C=0.621, D=0.693, E=0.552, F=0.530, G=0.686, H=0.702,
| | local NAME_H = 3.5 |
| I=0.276, J=0.301, K=0.637, L=0.491, M=0.904, N=0.695, O=0.741, P=0.585,
| | local LINE_H = 1.2 |
| Q=0.740, R=0.628, S=0.555, T=0.555, U=0.694, V=0.668, W=0.913, X=0.667,
| | local MAX_EM = 1.5 |
| Y=0.635, Z=0.666,
| | local MIN_EM = 0.8 |
| a=0.573, b=0.608, c=0.473, d=0.604, e=0.570, f=0.382, g=0.558, h=0.608,
| | local SNAP_EM = 0.05 -- keep emitted sizes on a tidy grid |
| i=0.269, j=0.266, k=0.584, l=0.268, m=0.907, n=0.602, o=0.611, p=0.604,
| |
| q=0.605, r=0.392, s=0.496, t=0.404, u=0.602, v=0.549, w=0.776, x=0.571,
| |
| y=0.550, z=0.561,
| |
| ["0"]=0.662, ["1"]=0.312, ["2"]=0.560, ["3"]=0.566, ["4"]=0.612,
| |
| ["5"]=0.562, ["6"]=0.601, ["7"]=0.520, ["8"]=0.583, ["9"]=0.598,
| |
| [" "]=0.240,
| |
| }
| |
| | |
| -- Characters absent from Retail Demo (hyphen, apostrophe, ...) are drawn by the
| |
| -- browser in the fallback sans-serif; this is a reasonable stand-in width.
| |
| local FALLBACK = 0.60
| |
| | |
| -- Card geometry in px at the reference scale where 1em = 10px. The card is
| |
| -- scaled through font-size on .itembox (the |size= parameter) and the value
| |
| -- returned here is em, so it scales together with everything else.
| |
| local USABLE_W = 72 -- 80px card minus 4px padding on each side
| |
| local NAME_H = 42 -- vertical room the wrapped name may occupy
| |
| local LINE_H = 1.2 -- must match .itembox-name line-height | |
| local MAX_PX = 15 -- full size (1.50em) | |
| local MIN_PX = 8 -- hard floor (0.80em) | |
| | |
| local function charWidth(ch) | |
| local w = WIDTH[ch]
| |
| if w then return w end
| |
| local cp = ustring.codepoint(ch) or 0
| |
| -- CJK / full-width ranges render roughly twice as wide as Latin
| |
| if (cp >= 0x1100 and cp <= 0x115F) or (cp >= 0x2E80 and cp <= 0xA4CF)
| |
| or (cp >= 0xAC00 and cp <= 0xD7A3) or (cp >= 0xF900 and cp <= 0xFAFF)
| |
| or (cp >= 0xFF00 and cp <= 0xFF60) or (cp >= 0xFFE0 and cp <= 0xFFE6) then
| |
| return 2.0
| |
| end
| |
| return FALLBACK
| |
| end
| |
| | |
| local function wordWidth(word)
| |
| local total = 0
| |
| for ch in ustring.gmatch(word, ".") do
| |
| total = total + charWidth(ch)
| |
| end
| |
| return total
| |
| end
| |
| | |
| -- Greedy line count when filling USABLE_W at font size `px`. Close enough to
| |
| -- text-wrap:balance for sizing, since both keep every word intact.
| |
| local function lineCount(words, px)
| |
| local space = charWidth(" ") * px
| |
| local cur, lines = 0, 1
| |
| for _, w in ipairs(words) do
| |
| local ww = wordWidth(w) * px
| |
| if cur == 0 then
| |
| cur = ww
| |
| elseif cur + space + ww <= USABLE_W then
| |
| cur = cur + space + ww
| |
| else
| |
| lines = lines + 1
| |
| cur = ww
| |
| end
| |
| end
| |
| return lines
| |
| end
| |
|
| |
|
| -- Returns the name font size as an em string (scales with the card). | | -- Returns the name font size as an em string (scales with the card). |
| function p.getFontSize(frame) | | function p.getFontSize(frame) |
| local name = ustring.gsub(frame.args[1] or "", "^%s*(.-)%s*$", "%1") | | local name = ustring.gsub(frame.args[1] or "", "^%s*(.-)%s*$", "%1") |
| if name == "" then return string.format("%.2f", MAX_PX / 10) end | | if name == "" then return string.format("%.2f", MAX_EM) end |
| | |
| local words = {}
| |
| for w in ustring.gmatch(name, "%S+") do words[#words + 1] = w end
| |
| | |
| local widest = 0
| |
| for _, w in ipairs(words) do
| |
| local ww = wordWidth(w)
| |
| if ww > widest then widest = ww end
| |
| end
| |
| | |
| local px = USABLE_W / widest
| |
| if px > MAX_PX then px = MAX_PX end
| |
| | |
| while px > MIN_PX and lineCount(words, px) * LINE_H * px > NAME_H do
| |
| px = px - 0.25
| |
| end
| |
| if px < MIN_PX then px = MIN_PX end
| |
|
| |
|
| local snapped = math.floor(px * 2 + 0.5) / 2 | | local pct = textfit.fit(name, { |
| if lineCount(words, snapped) * LINE_H * snapped > NAME_H then
| | aspect = NAME_H / NAME_W, |
| snapped = math.floor(px * 2) / 2 | | lineHeight = LINE_H, |
| end | | max = MAX_EM / NAME_W * 100, |
| | min = MIN_EM / NAME_W * 100, |
| | bold = true, -- .itembox-name is font-weight: bold |
| | }) |
|
| |
|
| return string.format("%.2f", snapped / 10) | | -- fit() returns a percentage of the usable width. Convert back to em and |
| | -- coarsen downward, so a snapped value still fits. The epsilon guards |
| | -- against binary representation: 1.45 / 0.05 is 28.999999999999996, which |
| | -- would otherwise floor a whole grid step too low. |
| | local em = math.floor(pct / 100 * NAME_W / SNAP_EM + 1e-9) * SNAP_EM |
| | return string.format("%.2f", math.max(em, MIN_EM)) |
| end | | end |
|
| |
|