first commit
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import zlib from 'node:zlib'
|
||||
|
||||
const OUT = path.resolve('public/icons')
|
||||
fs.mkdirSync(OUT, { recursive: true })
|
||||
|
||||
const CRC_TABLE = (() => {
|
||||
const table = new Uint32Array(256)
|
||||
for (let n = 0; n < 256; n++) {
|
||||
let c = n
|
||||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
|
||||
table[n] = c >>> 0
|
||||
}
|
||||
return table
|
||||
})()
|
||||
|
||||
function crc32(buf) {
|
||||
let c = 0xffffffff
|
||||
for (const b of buf) c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8)
|
||||
return (c ^ 0xffffffff) >>> 0
|
||||
}
|
||||
|
||||
function chunk(type, data) {
|
||||
const len = Buffer.alloc(4)
|
||||
len.writeUInt32BE(data.length, 0)
|
||||
const t = Buffer.from(type, 'ascii')
|
||||
const crc = Buffer.alloc(4)
|
||||
crc.writeUInt32BE(crc32(Buffer.concat([t, data])), 0)
|
||||
return Buffer.concat([len, t, data, crc])
|
||||
}
|
||||
|
||||
function encodePng(width, height, rgba) {
|
||||
const ihdr = Buffer.alloc(13)
|
||||
ihdr.writeUInt32BE(width, 0)
|
||||
ihdr.writeUInt32BE(height, 4)
|
||||
ihdr[8] = 8
|
||||
ihdr[9] = 6
|
||||
ihdr[10] = 0
|
||||
ihdr[11] = 0
|
||||
ihdr[12] = 0
|
||||
const raw = Buffer.alloc((width * 4 + 1) * height)
|
||||
for (let y = 0; y < height; y++) {
|
||||
raw[y * (width * 4 + 1)] = 0
|
||||
rgba.copy(raw, y * (width * 4 + 1) + 1, y * width * 4, (y + 1) * width * 4)
|
||||
}
|
||||
const idat = zlib.deflateSync(raw, { level: 9 })
|
||||
return Buffer.concat([
|
||||
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
||||
chunk('IHDR', ihdr),
|
||||
chunk('IDAT', idat),
|
||||
chunk('IEND', Buffer.alloc(0))
|
||||
])
|
||||
}
|
||||
|
||||
function clamp01(v) {
|
||||
return v < 0 ? 0 : v > 1 ? 1 : v
|
||||
}
|
||||
|
||||
function mix(a, b, t) {
|
||||
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]
|
||||
}
|
||||
|
||||
function roundedRectInside(x, y, cx, cy, hw, hh, r) {
|
||||
const dx = Math.abs(x - cx) - (hw - r)
|
||||
const dy = Math.abs(y - cy) - (hh - r)
|
||||
const ox = Math.max(dx, 0)
|
||||
const oy = Math.max(dy, 0)
|
||||
return Math.hypot(ox, oy) + Math.min(Math.max(dx, dy), 0) - r <= 0
|
||||
}
|
||||
|
||||
function segmentDistance(px, py, ax, ay, bx, by) {
|
||||
const dx = bx - ax
|
||||
const dy = by - ay
|
||||
const len2 = dx * dx + dy * dy || 1
|
||||
let t = ((px - ax) * dx + (py - ay) * dy) / len2
|
||||
t = t < 0 ? 0 : t > 1 ? 1 : t
|
||||
return Math.hypot(px - (ax + dx * t), py - (ay + dy * t))
|
||||
}
|
||||
|
||||
function ringInside(px, py, cx, cy, r, width) {
|
||||
return Math.abs(Math.hypot(px - cx, py - cy) - r) <= width / 2
|
||||
}
|
||||
|
||||
function arcInside(px, py, cx, cy, r, width, a0, a1) {
|
||||
if (!ringInside(px, py, cx, cy, r, width)) return false
|
||||
let a = Math.atan2(py - cy, px - cx)
|
||||
while (a < a0) a += Math.PI * 2
|
||||
return a <= a1
|
||||
}
|
||||
|
||||
function samplePlan(x, y, pad) {
|
||||
const sx = (x - pad) / (1 - pad * 2)
|
||||
const sy = (y - pad) / (1 - pad * 2)
|
||||
const line = 0.048
|
||||
const l = 0.022
|
||||
// outer wall
|
||||
if (segmentDistance(sx, sy, 0.2, 0.3, 0.8, 0.3) < line / 2) return true
|
||||
if (segmentDistance(sx, sy, 0.8, 0.3, 0.8, 0.7) < line / 2) return true
|
||||
if (segmentDistance(sx, sy, 0.8, 0.7, 0.2, 0.7) < line / 2) return true
|
||||
if (segmentDistance(sx, sy, 0.2, 0.7, 0.2, 0.3) < line / 2) return true
|
||||
// inner walls
|
||||
if (segmentDistance(sx, sy, 0.52, 0.3, 0.52, 0.7) < l) return true
|
||||
if (segmentDistance(sx, sy, 0.52, 0.5, 0.8, 0.5) < l) return true
|
||||
// door swing
|
||||
if (arcInside(sx, sy, 0.52, 0.5, 0.15, l * 1.2, -Math.PI / 2, 0)) return true
|
||||
if (segmentDistance(sx, sy, 0.52, 0.5, 0.52, 0.65) < l * 0.9) return true
|
||||
// round table
|
||||
if (ringInside(sx, sy, 0.34, 0.42, 0.075, l * 1.3)) return true
|
||||
if (ringInside(sx, sy, 0.34, 0.42, 0.028, l)) return true
|
||||
// bed
|
||||
if (segmentDistance(sx, sy, 0.58, 0.57, 0.74, 0.57) < l * 0.9) return true
|
||||
if (segmentDistance(sx, sy, 0.74, 0.57, 0.74, 0.66) < l * 0.9) return true
|
||||
if (segmentDistance(sx, sy, 0.74, 0.66, 0.58, 0.66) < l * 0.9) return true
|
||||
if (segmentDistance(sx, sy, 0.58, 0.66, 0.58, 0.57) < l * 0.9) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function render(size, { maskable = false, fullSquare = false } = {}) {
|
||||
const aa = 4
|
||||
const w = size * aa
|
||||
const rgba = Buffer.alloc(size * size * 4)
|
||||
const pad = maskable ? 0.2 : 0.14
|
||||
const bgTop = [96, 156, 255]
|
||||
const bgBottom = [29, 78, 216]
|
||||
const bgDeep = [12, 22, 44]
|
||||
for (let py = 0; py < size; py++) {
|
||||
for (let px = 0; px < size; px++) {
|
||||
let sr = 0
|
||||
let sg = 0
|
||||
let sb = 0
|
||||
let sa = 0
|
||||
for (let oy = 0; oy < aa; oy++) {
|
||||
for (let ox = 0; ox < aa; ox++) {
|
||||
const x = (px + (ox + 0.5) / aa) / size
|
||||
const y = (py + (oy + 0.5) / aa) / size
|
||||
const insideBg = fullSquare || roundedRectInside(x, y, 0.5, 0.5, 0.5, 0.5, 0.235)
|
||||
if (!insideBg) continue
|
||||
const t = clamp01((x + y) / 2.2)
|
||||
let color = mix(bgTop, bgBottom, t)
|
||||
const vignette = clamp01(Math.hypot(x - 0.3, y - 0.26) * 1.35)
|
||||
color = mix(color, bgDeep, vignette * 0.24)
|
||||
if (samplePlan(x, y, pad)) {
|
||||
color = mix([255, 255, 255], [192, 219, 255], clamp01((x + y) / 2.4) * 0.45)
|
||||
}
|
||||
sr += color[0]
|
||||
sg += color[1]
|
||||
sb += color[2]
|
||||
sa += 1
|
||||
}
|
||||
}
|
||||
const total = aa * aa
|
||||
const i = (py * size + px) * 4
|
||||
if (sa === 0) {
|
||||
rgba[i] = 0
|
||||
rgba[i + 1] = 0
|
||||
rgba[i + 2] = 0
|
||||
rgba[i + 3] = 0
|
||||
} else {
|
||||
rgba[i] = Math.round(sr / sa)
|
||||
rgba[i + 1] = Math.round(sg / sa)
|
||||
rgba[i + 2] = Math.round(sb / sa)
|
||||
rgba[i + 3] = Math.round((sa / total) * 255)
|
||||
}
|
||||
}
|
||||
}
|
||||
return encodePng(size, size, rgba)
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(OUT, 'icon-192.png'), render(192))
|
||||
fs.writeFileSync(path.join(OUT, 'icon-512.png'), render(512))
|
||||
fs.writeFileSync(path.join(OUT, 'apple-touch-icon.png'), render(180, { fullSquare: true }))
|
||||
fs.writeFileSync(path.join(OUT, 'icon-maskable-512.png'), render(512, { maskable: true, fullSquare: true }))
|
||||
console.log('icons written to', OUT)
|
||||
Reference in New Issue
Block a user