林泊百科开放中!

提交修改前请务必备份您的编辑内容。如有站点问题反馈,请发送邮件至account@pigeon-games.com

模块:纸张

来自Limbo Wiki
晨茗biu~​(留言 | 贡献)2026年10月23日 (五) 00:51的版本 (纸张模拟模块)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)

此模块的文档可以在模块:纸张/doc创建

local p = {}

local TYPES = { ['咖啡']='st1',['墨']='st2',['水']='st3',['茶']='st4',['霉']='st5',['血迹']='st6' }
local RING = { st1=true, st3=true, st4=true }

local OFF = { {5,-6,0.66}, {-6,7,0.58}, {9,6,0.50}, {-5,-8,0.44} }

local WEARPTS = {
  {10,14,1.30,1.00},{72,30,1.70,1.20},{38,66,1.55,1.10},{84,72,1.20,0.95},
  {20,86,1.85,1.30},{56,8,1.10,0.85},{90,44,1.00,0.80},{6,52,1.40,1.05},
}

local CORNERCLS = { ['左上']='tl',['右上']='tr',['左下']='bl',['右下']='br' }
local CORNERPOS = { tl='0% 0%', tr='100% 0%', bl='0% 100%', br='100% 100%' }

local function trim(s) if not s then return '' end s=s:gsub('^%s+','') s=s:gsub('%s+$','') return s end
local function clamp(v,lo,hi) if v<lo then return lo end if v>hi then return hi end return v end
local function lcg(n) return (n*1103515245+12345) % 2147483648 end

local function pageSeed()
  local t = mw.title.getCurrentTitle().text or ''
  local h = 5381
  for i = 1, #t do h = (h*33 + t:byte(i)) % 100003 end
  return h
end
local function normOpacity(s)
  local o=tonumber(s) if not o then return nil end
  if o>1 then o=o/100 end
  if o<0 then o=0 end
  if o>1 then o=1 end
  return o
end
local function opStr(o) if not o then return '' end return 'opacity:'..string.format('%.2f',o)..';' end
local function mix(n, salt)
  local v = (n + salt*7919) % 2147483647
  v = (v * 1103515245 + 12345) % 2147483647
  v = (v * 1103515245 + 12345) % 2147483647
  return v
end
local function jitterR(h,i)
  local a = mix(h+i, 1) % 33 - 16
  local b = mix(h+i, 2) % 33 - 16
  local c = mix(h+i, 3) % 33 - 16
  local d = mix(h+i, 4) % 33 - 16
  return string.format('%d%% %d%% %d%% %d%%',50+a,50+b,50+c,50+d)
end

local function strhash(s)
  if not s or s == '' then return 0 end
  local n = #s
  local step = 1
  if n > 400 then step = math.floor(n / 400) end
  local h = 5381
  for i = 1, n, step do h = (h*31 + s:byte(i)) % 1000033 end
  return h
end
local function contentSeed(content, seed, typ)
  return (pageSeed() + strhash(content or '')*389 + strhash(seed or '')*701 + strhash(typ or '')*47 + 17) % 1000003
end
local function instSeed(frame)
  return contentSeed(frame.args[1], frame.args[2], frame.args[3])
end

function p.stain(frame)
  local src    = trim(frame.args[1] or '')
  local qtyG   = clamp(math.floor(tonumber(trim(frame.args[2] or '')) or 1),1,5)
  local opaG   = normOpacity(trim(frame.args[3] or ''))
  local spread = clamp(math.floor(tonumber(trim(frame.args[4] or '')) or 0),0,5)

  src = src:gsub(',',','):gsub(';',';'):gsub('@','@'):gsub(' ',' ')
  src = trim(src)
  local srch = 7
  for i = 1, #src do srch = (srch*31 + src:byte(i)) % 100003 end
  local h = (contentSeed(frame.args[5], frame.args[6], frame.args[7]) + srch * 131) % 1000003

  local list = {}
  for entry in mw.text.gsplit(src,';') do
    entry = trim(entry)
    if entry ~= '' and entry ~= '无' then
      local at = entry:find('@',1,true)
      local tname, rest
      if at then tname=trim(entry:sub(1,at-1)) rest=entry:sub(at+1) else tname=entry rest='' end
      local cls = TYPES[tname]
      if cls then
        local x,y,color,splash = nil,nil,'',''
        local opa,qty = opaG,qtyG
        if rest ~= '' then
          local segs = mw.text.split(rest,',',true)
          local px = tonumber(trim(segs[1] or '') or '')
          local py = tonumber(trim(segs[2] or '') or '')
          if px then x = clamp(px,0,95) end
          if py then y = clamp(py,0,95) end
          color = trim(segs[3] or '')
          local sp = trim(segs[4] or '')
          if sp=='飞溅' or sp=='splash' then splash=' limbo-paper-splash' end
          local o5 = normOpacity(trim(segs[5] or '')) if o5 then opa=o5 end
          local q6 = tonumber(trim(segs[6] or '') or '') if q6 then qty=clamp(math.floor(q6),1,5) end
        end
        list[#list+1] = { cls=cls, x=x, y=y, color=color, splash=splash, opa=opa, qty=qty }
      end
    end
  end

  local out, idx = {}, 0
  for _, it in ipairs(list) do
    idx = idx + 1
    local x = it.x
    local y = it.y
    if not x then x = 6 + (mix(h, idx*37+1) % 87) end
    if not y then y = 6 + (mix(h, idx*37+2) % 80) end
    local colStyle = it.color ~= '' and ('--limbo-paper-stain:'..it.color..';') or ''
    local shape = 'border-radius:'..jitterR(h,idx)..';'
    local spin = string.format('transform:rotate(%.1fdeg) scale(%.2f);', ((mix(h, idx*9+1) % 4000)/100.0) - 20, 0.68 + (mix(h, idx*9+2) % 78)/100.0)
    local bwid = ''
    if RING[it.cls] then
      local bbase = (it.cls == 'st3') and 0.18 or 0.24
      bwid = string.format('border-width:%.2fem;', bbase * (0.60 + (mix(h, idx*29+3) % 91)/100.0))
    end
    out[#out+1] = string.format('<span class="limbo-paper-stain limbo-paper-%s%s" style="left:%d%%;top:%d%%;%s%s%s%s%s"></span>',
      it.cls, it.splash, math.floor(x), math.floor(y), colStyle, shape, bwid, spin, opStr(it.opa))
    for i = 1, it.qty-1 do
      local d = OFF[i]
      if d then
        local ring = RING[it.cls]
        local k = ring and (2.2 + (mix(h, idx*13+i) % 141)/100.0) or (0.60 + (mix(h, idx*13+i) % 86)/100.0)
        local rot = ((mix(h, idx*17+i) % 4800)/100.0) - 24
        local fs = d[3] * (0.70 + (mix(h, idx*31+i) % 61)/100.0)
        local dx, dy = d[1]*k, d[2]*k
        if ring then
          local ang = math.rad(((mix(h, idx*43+i) % 4400)/100.0) - 22)
          local ca, sa = math.cos(ang), math.sin(ang)
          dx, dy = dx*ca - dy*sa, dx*sa + dy*ca
        end
        out[#out+1] = string.format('<span class="limbo-paper-stain limbo-paper-%s" style="left:%d%%;top:%d%%;font-size:%.2fem;transform:rotate(%.1fdeg);%s%s%s"></span>',
          it.cls, math.floor(clamp(x+dx,0,94)), math.floor(clamp(y+dy,0,94)), fs, rot,
          colStyle, 'border-radius:'..jitterR(h,idx*7+i)..';', opStr(it.opa))
      end
    end
  end

  if spread > 0 and #list > 0 then
    local total = spread * 6
    local cols = math.ceil(math.sqrt(total*1.4))
    local rows = math.ceil(total/cols)
    local cw = 90.0/cols
    local ch = 86.0/rows
    for i = 1, total do
      local it = list[(i-1) % #list + 1]
      local col = (i-1) % cols
      local row = math.floor((i-1)/cols)
      local jx = lcg(i*7919) % math.max(1, math.floor(cw*0.55))
      local jy = lcg(i*104729+17) % math.max(1, math.floor(ch*0.55))
      local x = math.floor(4 + col*cw + jx)
      local y = math.floor(4 + row*ch + jy)
      local sc = 0.12 + (lcg(i*31337) % 84) / 100.0
      local rot = (mix(h + i, 9) % 12000) / 100.0 - 60
      local colStyle = it.color ~= '' and ('--limbo-paper-stain:'..it.color..';') or ''
      out[#out+1] = string.format(
        '<span class="limbo-paper-stain limbo-paper-%s" style="left:%d%%;top:%d%%;font-size:%.2fem;transform:rotate(%.1fdeg);%s%s%s"></span>',
        it.cls, x, y, sc, rot, colStyle, 'border-radius:'..jitterR(h,i*11+3)..';', opStr(it.opa))
    end
  end
  return table.concat(out)
end

function p.corner(frame)
  local src = trim(frame.args[1] or '')
  src = src:gsub(',',','):gsub('、',','):gsub(';',';'):gsub(' ',' ')
  local out, seen = {}, {}
  for entry in mw.text.gsplit(src,'[,;]') do
    local c = CORNERCLS[trim(entry)]
    if c and not seen[c] then
      seen[c] = true
      out[#out+1] = '<span class="limbo-paper-deco limbo-paper-corner limbo-paper-cn-'..c..'"></span>'
    end
  end
  return table.concat(out)
end

function p.damage(frame)
  local n = clamp(math.floor(tonumber(trim(frame.args[1] or '')) or 0),0,8)
  if n < 1 then return '' end
  local h = contentSeed(frame.args[2], frame.args[3], frame.args[4])
  local grow = 0.55 + 0.11*(n-1)
  local out = {}
  for i = 1, n do
    local w = WEARPTS[i]
    if w then
      local jx = ((h+i*97)%23)-11
      local jy = ((h+i*131)%23)-11
      local sz = 0.65 + (mix(h+i,7)%81)/100.0
      out[#out+1] = string.format('<span class="limbo-paper-wear" style="left:%d%%;top:%d%%;width:%.2fem;height:%.2fem;border-radius:%s"></span>',
        clamp(w[1]+jx,0,92), clamp(w[2]+jy,0,90), w[3]*grow*sz, w[4]*grow*sz, jitterR(h,i*5))
    end
  end
  return table.concat(out)
end

-- 裁切:每个图层都铺满纸张,仅在目标处留透明洞(配合 mask-composite:intersect)
-- 折角:45° 线性渐变,缺口两条直角边与折痕完全贴合
-- 缺口:椭圆簇切出指定长宽/位置的不规则孔洞
local CORNERGRAD = { tl=135, tr=225, br=315, bl=45 }
local CORNERLEG = 1.33
local function gapCores(src, h, base)
  local list = {}
  src = (src or ''):gsub(';',';'):gsub(',',','):gsub(' ',' ')
  local gi = 0
  for entry in mw.text.gsplit(src,';') do
    entry = trim(entry)
    if entry ~= '' then
      local body, pos = entry:match('^(.-)@(.*)$')
      if not body then body = entry end
      body = body:gsub('×','x'):gsub('%*','x')
      local gw, gh = body:match('([%d%.]+)%s*[x,]%s*([%d%.]+)')
      if gw and gh then
        gi = gi + 1
        local w2, h2 = tonumber(gw), tonumber(gh)
        local gx, gy = 50, 50
        if pos then
          local px, py = pos:match('([%d%.%-]+)%s*,%s*([%d%.%-]+)')
          gx = tonumber(px) or 50  gy = tonumber(py) or 50
        end
        local seed = h + base + gi * 3571
        local n = 1 + (mix(seed, 51) % 3)
        local cores = {}
        for i = 1, n do
          local s = seed + i * 97
          local rx, ry, xe, ye
          if n == 1 then
            rx = (w2 / 2) * (0.95 + (mix(s, 56) % 11) / 100.0)
            ry = (h2 / 2) * (0.95 + (mix(s, 57) % 11) / 100.0)
            xe, ye = 0, 0
          else
            local ang = (mix(s, 54) % 6283) / 1000.0
            local off = 0.14 + (mix(s, 55) % 13) / 100.0
            xe = math.cos(ang) * (w2 / 2) * off
            ye = math.sin(ang) * (h2 / 2) * off
            rx = (w2 / 2) * (0.62 + (mix(s, 56) % 19) / 100.0)
            ry = (h2 / 2) * (0.62 + (mix(s, 57) % 19) / 100.0)
          end
          cores[#cores+1] = { rx=rx, ry=ry, xp=gx, xe=xe, yp=gy, ye=ye }
        end
        list[#list+1] = cores
      end
    end
  end
  return list
end
function p.cut(frame)
  local corners = trim(frame.args[1] or '')
  local dmg = clamp(math.floor(tonumber(trim(frame.args[2] or '')) or 0),0,8)
  local gaps = trim(frame.args[3] or '')
  local h = contentSeed(frame.args[4], frame.args[5], frame.args[6])
  local layers = {}
  local function calcMix(pct, em)
    if em >= 0 then return string.format('calc(%.2f%% + %.2fem)', pct, em) end
    return string.format('calc(%.2f%% - %.2fem)', pct, -em)
  end

  corners = corners:gsub(',',','):gsub('、',','):gsub(';',';')
  local seen = {}
  for entry in mw.text.gsplit(corners,'[,;]') do
    local c = CORNERCLS[trim(entry)]
    if c and not seen[c] then
      seen[c] = true
      layers[#layers+1] = string.format('linear-gradient(%ddeg,transparent 0 %.2fem,#000 %.2fem)',
        CORNERGRAD[c], CORNERLEG, CORNERLEG + 0.02)
    end
  end

  local grow = 0.55 + 0.11*(dmg-1)
  for i = 1, dmg do
    local w = WEARPTS[i]
    if w then
      local jx = ((h+i*97)%23)-11
      local jy = ((h+i*131)%23)-11
      local sz = 0.65 + (mix(h+i,7)%81)/100.0
      local r = w[3]*grow*sz*0.52
      layers[#layers+1] = string.format('radial-gradient(circle at %d%% %d%%,transparent %.2fem,#000 %.2fem)',
        clamp(w[1]+jx,0,92), clamp(w[2]+jy,0,90), r, r*1.35)
    end
  end

  for _, cores in ipairs(gapCores(frame.args[3], h, 0)) do
    for _, c in ipairs(cores) do
      layers[#layers+1] = string.format('radial-gradient(ellipse %.2fem %.2fem at %s %s,transparent 0 96%%,#000 100%%)',
        c.rx, c.ry, calcMix(c.xp, c.xe), calcMix(c.yp, c.ye))
    end
  end
  for _, cores in ipairs(gapCores(frame.args[7], h, 777)) do
    for _, c in ipairs(cores) do
      layers[#layers+1] = string.format('radial-gradient(ellipse %.2fem %.2fem at %s %s,transparent 0 96%%,#000 100%%)',
        c.rx, c.ry, calcMix(c.xp, c.xe), calcMix(c.yp, c.ye))
    end
  end

  if #layers == 0 then return '' end
  return 'mask-image:'..table.concat(layers,',')..';'
end

function p.burnholes(frame)
  local h = contentSeed(frame.args[2], frame.args[3], frame.args[4])
  local out, idx = {}, 0
  local function calc(pct, em)
    if em >= 0 then return string.format('calc(%.2f%% + %.2fem)', pct, em) end
    return string.format('calc(%.2f%% - %.2fem)', pct, -em)
  end
  for _, cores in ipairs(gapCores(frame.args[1], h, 777)) do
    for _, c in ipairs(cores) do
      idx = idx + 1
      local function v(k) return 48 + ((mix(h + idx, 90 + k) % 9) - 4) end
      local radius = string.format('%d%% %d%% %d%% %d%%', v(1), v(2), v(3), v(4))
      out[#out+1] = string.format('<span class="limbo-paper-burnhole" style="left:%s;top:%s;width:%.2fem;height:%.2fem;border-radius:%s"></span>',
        calc(c.xp, c.xe), calc(c.yp, c.ye), (c.rx + 0.30) * 2, (c.ry + 0.30) * 2, radius)
    end
  end
  return table.concat(out)
end

function p.charb(frame)
  local h = instSeed(frame)
  local out = {}
  local function rad(seed, base)
    local function v(k) return 50 + ((mix(h, seed*7+k) % 33) - 16) end
    return string.format('%d%% %d%% %d%% %d%%', v(1), v(2), v(3), v(4))
  end
  local function pos(basePct, em)
    if em >= 0 then return string.format('calc(%.1f%% + %.2fem)', basePct, em) end
    return string.format('calc(%.1f%% - %.2fem)', basePct, -em)
  end
  local function push(x, y, wem, hhem, seed, a)
    out[#out+1] = string.format(
      '<span class="limbo-paper-charb" style="left:%.1f%%;top:%.1f%%;width:%.2fem;height:%.2fem;border-radius:%s;background-color:rgba(28,15,4,%.2f);transform:translate(-50%%,-50%%)"></span>',
      clamp(x,-4,104), clamp(y,-4,104), wem, hhem, rad(seed, math.max(0.18, hhem)), a)
  end
  local function pushAt(xstr, ystr, wem, hem, seed, a)
    out[#out+1] = string.format(
      '<span class="limbo-paper-charb" style="left:%s;top:%s;width:%.2fem;height:%.2fem;border-radius:%s;background-color:rgba(28,15,4,%.2f);transform:translate(-50%%,-50%%)"></span>',
      xstr, ystr, wem, hem, rad(seed, math.max(0.20, math.min(wem, hem))), a)
  end
  local function pushW(x, ystr, wpct, hhem, seed, a)
    out[#out+1] = string.format(
      '<span class="limbo-paper-charb" style="left:%.1f%%;top:%s;width:%.1f%%;height:%.2fem;border-radius:%s;background-color:rgba(28,15,4,%.2f);transform:translate(-50%%,-50%%)"></span>',
      clamp(x,-4,104), ystr, wpct, hhem, rad(seed, math.max(0.20, hhem)), a)
  end
  local function pushH(xstr, y, wem, hpct, seed, a)
    out[#out+1] = string.format(
      '<span class="limbo-paper-charb" style="left:%s;top:%.1f%%;width:%.2fem;height:%.1f%%;border-radius:%s;background-color:rgba(28,15,4,%.2f);transform:translate(-50%%,-50%%)"></span>',
      xstr, clamp(y,-4,104), wem, hpct, rad(seed, math.max(0.20, wem)), a)
  end
  local function edgeH(y, salt)
    local per = 26 + (mix(h+salt, 5) % 14)
    local step = 100.0/(per-1)
    for i = 0, per-1 do
      local t = i/(per-1)
      local x = t*100 + (((mix(h+i, 30+salt) % 100)/100.0) - 0.5) * step * 0.9
      local off = ((mix(h+i, 40+salt) % 350)/1000.0) - 0.10
      local ystr
      if y == 0 then ystr = pos(0, off) else ystr = pos(100, -off) end
      local pick = mix(h+i, 55+salt) % 10
      local w = step * (1.7 + (mix(h+i, 50+salt) % 90)/100.0)
      local hh
      if pick < 8 then
        hh = 0.30 + (mix(h+i, 60+salt) % 55)/100.0
      else
        hh = 0.35 + (mix(h+i, 60+salt) % 85)/100.0
      end
      pushW(x, ystr, w, hh, i*4+salt, 1)
    end
  end
  local function edgeV(x0, salt)
    local per = 26 + (mix(h+salt, 6) % 14)
    local step = 100.0/(per-1)
    for i = 0, per-1 do
      local t = i/(per-1)
      local y = t*100 + (((mix(h+i, 31+salt) % 100)/100.0) - 0.5) * step * 0.9
      local off = ((mix(h+i, 41+salt) % 350)/1000.0) - 0.10
      local xstr
      if x0 == 0 then xstr = pos(0, off) else xstr = pos(100, -off) end
      local pick = mix(h+i, 56+salt) % 10
      local hh = step * (1.7 + (mix(h+i, 61+salt) % 90)/100.0)
      local w
      if pick < 8 then
        w = 0.30 + (mix(h+i, 51+salt) % 55)/100.0
      else
        w = 0.35 + (mix(h+i, 51+salt) % 85)/100.0
      end
      pushH(xstr, y, w, hh, i*4+salt, 1)
    end
  end
  edgeH(0, 10)
  edgeH(100, 20)
  edgeV(0, 30)
  edgeV(100, 40)
  for i = 1, 14 do
    local side = i % 4
    local t = (mix(h+i, 81) % 92) + 4
    local inset = 0.12 + (mix(h+i, 82) % 45)/100.0
    local sz = 0.10 + (mix(h+i, 83) % 30)/100.0
    local xstr, ystr
    if side == 0 then xstr = string.format('%.1f%%', t) ystr = pos(0, inset)
    elseif side == 1 then xstr = string.format('%.1f%%', t) ystr = pos(100, -inset)
    elseif side == 2 then xstr = pos(0, inset) ystr = string.format('%.1f%%', t)
    else xstr = pos(100, -inset) ystr = string.format('%.1f%%', t) end
    pushAt(xstr, ystr, sz*2.2, sz, i*7+3, 0.88)
  end
  return table.concat(out)
end

function p.charring(frame)
  local h = instSeed(frame)
  local n = 96
  local function walk(seed)
    local t = 0.33
    local out = {}
    for i = 1, n do
      local r = (mix(h + i, seed) % 1000) / 1000.0
      t = t + (r * 2 - 1) * 0.055
      if t < 0.18 then t = 0.18 end
      if t > 0.48 then t = 0.48 end
      if (mix(h + i, seed + 7) % 100) < 8 then t = t + 0.09 end
      if t > 0.58 then t = 0.58 end
      out[i] = t
    end
    for pass = 1, 3 do
      local sm = {}
      for i = 1, n do
        local a = out[math.max(1, i-1)]
        local b = out[i]
        local c = out[math.min(n, i+1)]
        sm[i] = a*0.25 + b*0.5 + c*0.25
      end
      out = sm
    end
    return out
  end
  local T, R, B, L = walk(101), walk(211), walk(307), walk(401)
  local pts = {}
  local function pct(v) return string.format('%.1f%%', v) end
  local function em(v) return string.format('%.2fem', v) end
  local function right(v) return string.format('calc(100%% - %.2fem)', v) end
  local function bottom(v) return string.format('calc(100%% - %.2fem)', v) end
  pts[#pts+1] = '0% 0%'
  pts[#pts+1] = '100% 0%'
  pts[#pts+1] = '100% 100%'
  pts[#pts+1] = '0% 100%'
  pts[#pts+1] = '0% 0%'
  pts[#pts+1] = em(L[1]) .. ' ' .. em(T[1])
  for i = 2, n do
    pts[#pts+1] = em(L[i]) .. ' ' .. pct((i-1)*100/(n-1))
  end
  for i = 1, n do
    pts[#pts+1] = pct((i-1)*100/(n-1)) .. ' ' .. bottom(B[i])
  end
  for i = n, 1, -1 do
    pts[#pts+1] = right(R[i]) .. ' ' .. pct((i-1)*100/(n-1))
  end
  for i = n, 1, -1 do
    pts[#pts+1] = pct((i-1)*100/(n-1)) .. ' ' .. em(T[i])
  end
  pts[#pts+1] = em(L[1]) .. ' ' .. em(T[1])
  return '<span class="limbo-paper-charring" style="clip-path:polygon(' .. table.concat(pts, ',') .. ')"></span>'
end

function p.charstyle(frame)
  local h = instSeed(frame)
  local function v(seed, lo, hi) return lo + (mix(h, seed) % (hi-lo+1)) end
  return string.format('style="background-position:%d%% %d%%,%d%% %d%%;background-size:%d%% %d%%,%d%% %d%%"',
    v(1,8,62), v(2,8,62), v(3,42,82), v(4,48,86),
    v(5,48,70), v(6,26,42), v(7,28,46), v(8,16,28))
end

function p.edge(frame)
  local kind = trim(frame.args[1] or '')
  local h = instSeed(frame)
  local ampT, ampS
  if kind == '撕边' then ampT, ampS = 0.16, 0.06 else ampT, ampS = 0.14, 0.07 end
  local per = 26
  local pts = {}
  local function rnd(salt, amp) return amp * ((mix(h+salt, 11) % 1000)/1000.0) end
  local function p(v) return string.format('%.1f%%', v) end
  local function e(v) return string.format('%.2fem', v) end
  local function topEdge(salt) return e(0.01 + rnd(salt, ampT)) end
  local function bottomEdge(salt) return 'calc(100% - '..e(0.01 + rnd(salt, ampT))..')' end
  local function leftEdge(salt) return e(0.01 + rnd(salt, ampS)) end
  local function rightEdge(salt) return 'calc(100% - '..e(0.01 + rnd(salt, ampS))..')' end
  for i = 0, per do pts[#pts+1] = p(i*100/per)..' '..topEdge(i*3+1) end
  for i = 1, per do pts[#pts+1] = rightEdge(i*3+2)..' '..p(i*100/per) end
  for i = 1, per do pts[#pts+1] = p(100 - i*100/per)..' '..bottomEdge(i*3+3) end
  for i = 1, per do pts[#pts+1] = leftEdge(i*3+4)..' '..p(100 - i*100/per) end
  return 'clip-path:polygon('..table.concat(pts,',')..');'
end

return p