Module:ItemBox: Difference between revisions

From The Deadlock Wiki
Jump to navigation Jump to search
Vergir (talk | contribs)
m comments update
Vergir (talk | contribs)
m Use Module:TextFit instead of own implementation
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



Revision as of 18:15, 1 August 2026

Documentation for this module may be created at Module:ItemBox/doc

local p = {}
local ustring = mw.ustring
local textfit = require "Module:TextFit"

-- ============================================================================
--  Name box geometry, in em of .itembox's font-size (1em = 10px at size=1).
--  Mirrors Template:ItemBox/styles.css:
--    .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 NAME_W  = 7.2
local NAME_H  = 3.5
local LINE_H  = 1.2
local MAX_EM  = 1.5
local MIN_EM  = 0.8
local SNAP_EM = 0.05   -- keep emitted sizes on a tidy grid

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

    local pct = textfit.fit(name, {
        aspect     = NAME_H / NAME_W,
        lineHeight = LINE_H,
        max        = MAX_EM / NAME_W * 100,
        min        = MIN_EM / NAME_W * 100,
        bold       = true,   -- .itembox-name is font-weight: bold
    })

    -- 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

-- User for stable per-item texture variety
local function hash(s)
    local h = 5381
    for i = 1, #s do
        h = (h * 33 + s:byte(i)) % 2147483647
    end
    return h
end

function p.variants(frame)
    local h = hash(frame.args[1] or "")
    local icon = (h % 3) + 1
    local wear = (math.floor(h / 3) % 4) + 1
    local tex  = (math.floor(h / 12) % 3) + 1
    return string.format("itembox-icon-v%d itembox-wear-v%d itembox-texture-v%d",
        icon, wear, tex)
end

return p