Module:ItemBox: Difference between revisions

Jump to navigation Jump to search
(it's vergir) better sizing for non-latin fonts
Vergir (talk | contribs)
Rewrite getFontSize: measured Retail Demo advance-width table + fit logic, output em so name scales with the card (with help from vergir-bot LLM)
Line 1: Line 1:
local p = {}
local p = {}
local ustring = mw.ustring


local function charWidth(cp)
-- ============================================================================
    -- Fullwidth & Wide ranges (CJK, Hangul, etc.) → 2 units
-- Retail Demo (bold) glyph advance widths, in em of the font size.
    if
-- unitsPerEm = 1000. Measured 2026-07-20 with fontTools from
        (cp >= 0x1100 and cp <= 0x115F)  or  -- Hangul Jamo
-- File:Retaildemo-bold.woff2 ( /images/d/d5/Retaildemo-bold.woff2 ),
        (cp >= 0x2E80 and cp <= 0x303E)  or -- CJK radicals, Kangxi
-- the bold face used by .modName.
        (cp >= 0x3041  and cp <= 0x33BF)  or -- Hiragana, Katakana, CJK compat
--
        (cp >= 0x33FF  and cp <= 0xA4CF)  or -- CJK unified
-- THIS TABLE IS FONT-SPECIFIC. If that file is re-uploaded with different
        (cp >= 0xA960 and cp <= 0xA97F)  or -- Hangul Jamo Extended
-- metrics, name fitting will drift; regenerate the table from the new file's
        (cp >= 0xAC00  and cp <= 0xD7FF)  or  -- Hangul syllables
-- hmtx / unitsPerEm.
        (cp >= 0xF900  and cp <= 0xFAFF)  or  -- CJK compatibility ideographs
-- ============================================================================
        (cp >= 0xFE10  and cp <= 0xFE1F)  or  -- Vertical forms
local WIDTH = {
        (cp >= 0xFE30  and cp <= 0xFE6F)  or  -- CJK compatibility forms
    A=0.677, B=0.590, C=0.621, D=0.693, E=0.552, F=0.530, G=0.686, H=0.702,
        (cp >= 0xFF01  and cp <= 0xFF60)  or  -- Fullwidth Latin/punctuation
    I=0.276, J=0.301, K=0.637, L=0.491, M=0.904, N=0.695, O=0.741, P=0.585,
        (cp >= 0xFFE0  and cp <= 0xFFE6)  or  -- Fullwidth signs
    Q=0.740, R=0.628, S=0.555, T=0.555, U=0.694, V=0.668, W=0.913, X=0.667,
        (cp >= 0x1B000 and cp <= 0x1B0FF) or  -- Kana supplement
    Y=0.635, Z=0.666,
        (cp >= 0x1F300 and cp <= 0x1F9FF) or  -- Emoji block
    a=0.573, b=0.608, c=0.473, d=0.604, e=0.570, f=0.382, g=0.558, h=0.608,
        (cp >= 0x20000 and cp <= 0x2FFFD) or  -- CJK Extension B–F
    i=0.269, j=0.266, k=0.584, l=0.268, m=0.907, n=0.602, o=0.611, p=0.604,
        (cp >= 0x30000 and cp <= 0x3FFFD)    -- CJK Extension G+
    q=0.605, r=0.392, s=0.496, t=0.404, u=0.602, v=0.549, w=0.776, x=0.571,
     then
    y=0.550, z=0.561,
        return 2
    ["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,
}


    -- Narrow Latin specifics
-- Characters absent from Retail Demo (hyphen, apostrophe, ...) are drawn by the
    elseif cp == 0x69 or cp == 0x6C or  -- i, l
-- browser in the fallback sans-serif; this is a reasonable stand-in width.
          cp == 0x49 or                -- I
local FALLBACK = 0.60
          cp == 0x31 or                -- 1
          cp == 0x21 or cp == 0x7C    -- !, |
    then
        return 0.5


    -- Wide Latin
-- Card geometry in px at the reference scale where 1em = 10px. The card is
    elseif cp == 0x6D or cp == 0x77 or  -- m, w
-- scaled through font-size on .CitadelShopMod (the |size= parameter) and the
          cp == 0x4D or cp == 0x57   -- M, W
-- value returned here is em, so it scales together with everything else.
     then
local USABLE_W = 72    -- 80px card minus 4px padding on each side
        return 1.5
local NAME_H  = 42    -- vertical room the wrapped name may occupy
local LINE_H  = 1.2  -- must match .modName line-height
local MAX_PX  = 15   -- full size  (1.50em)
local MIN_PX  = 8     -- hard floor (0.80em)


    -- Cyrillic and other "ambiguous" scripts — slightly wider than Latin
local function charWidth(ch)
    elseif cp >= 0x0400 and cp <= 0x04FF then
    local w = WIDTH[ch]
        -- Wide Cyrillic (1.5)
    if w then return w end
            local wide = {
    local cp = ustring.codepoint(ch) or 0
                [0x0416]=true, [0x0436]=true, -- Ж ж
    -- CJK / full-width ranges render roughly twice as wide as Latin
                [0x041C]=true, [0x043C]=true, -- М м
    if (cp >= 0x1100 and cp <= 0x115F) or (cp >= 0x2E80 and cp <= 0xA4CF)
                [0x0424]=true, [0x0444]=true, -- Ф ф
        or (cp >= 0xAC00 and cp <= 0xD7A3) or (cp >= 0xF900 and cp <= 0xFAFF)
                [0x0428]=true, [0x0448]=true, -- Ш ш
        or (cp >= 0xFF00 and cp <= 0xFF60) or (cp >= 0xFFE0 and cp <= 0xFFE6) then
                [0x0429]=true, [0x0449]=true, -- Щ щ
        return 2.0
                [0x042E]=true, [0x044E]=true, -- Ю ю
                [0x042B]=true, [0x044B]=true, -- Ы ы
            }
            -- Narrow Cyrillic (0.5)
            local narrow = {
                [0x0406]=true, [0x0456]=true, -- І і (Ukrainian I)
                [0x0407]=true, [0x0457]=true, -- Ї ї
            }
 
            if wide[cp] then
                return 1.5
            elseif narrow[cp] then
                return 0.5
            else
                return 1.1
            end
 
    -- Default for everything else (Arabic, Hebrew, Thai, etc.)
    else
        return 1
     end
     end
    return FALLBACK
end
end


local function wordWidth(word)
local function wordWidth(word)
     local width = 0
     local total = 0
     for i = 1, mw.ustring.len(word) do
     for ch in ustring.gmatch(word, ".") do
    local cp = mw.ustring.codepoint(word, i, i)
        total = total + charWidth(ch)
    width = width + charWidth(cp)
    end
end
     return total
     return math.ceil(width)
end
end


function p.getFontSize(frame)
-- Greedy line count when filling USABLE_W at font size `px`. Close enough to
     local name = frame.args[1] or ""
-- text-wrap:balance for sizing, since both keep every word intact.
     local words = {}
local function lineCount(words, px)
     for word in name:gmatch("%S+") do
     local space = charWidth(" ") * px
         table.insert(words, word)
     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
     end
      
     return lines
    local longestLine = 0
end


     if #words == 1 then
-- Returns the name font size as an em string (scales with the card).
        longestLine = wordWidth(words[1])
function p.getFontSize(frame)
    local name = ustring.gsub(frame.args[1] or "", "^%s*(.-)%s*$", "%1")
     if name == "" then return string.format("%.2f", MAX_PX / 10) end


     elseif #words == 2 then
     local words = {}
        longestLine = math.max(wordWidth(words[1]), wordWidth(words[2]))
    for w in ustring.gmatch(name, "%S+") do words[#words + 1] = w end


     elseif #words == 3 then
     local widest = 0
        local first = wordWidth(words[1])
    for _, w in ipairs(words) do
         local mid  = wordWidth(words[2])
         local ww = wordWidth(w)
         local last  = wordWidth(words[3])
         if ww > widest then widest = ww end
    end


        local shorterOuter = math.min(first, last)
    local px = USABLE_W / widest
        local longerOuter  = math.max(first, last)
    if px > MAX_PX then px = MAX_PX end


        local combined = mid + 1 + shorterOuter
    while px > MIN_PX and lineCount(words, px) * LINE_H * px > NAME_H do
 
        px = px - 0.25
        longestLine = math.max(combined, longerOuter)
    else
    -- 4+ words: take the max of all individual word widths
    for _, word in ipairs(words) do
        longestLine = math.max(longestLine, wordWidth(word))
    end
     end
     end
    if px < MIN_PX then px = MIN_PX end


local fullSize = 15
    local snapped = math.floor(px * 2 + 0.5) / 2
    local fontSize
     if lineCount(words, snapped) * LINE_H * snapped > NAME_H then
 
         snapped = math.floor(px * 2) / 2
     if longestLine <= 8 then
         fontSize = fullSize
    else
        fontSize = fullSize - (longestLine - 8) * 1
     end
     end
   
    fontSize = math.max(6, fontSize)


     return fontSize
     return string.format("%.2f", snapped / 10)
end
end


return p
return p