daily image rendering script
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
const { createCanvas, loadImage } = (() => { try { return require('canvas') } catch { return {} } })()
|
||||
const http = require('http')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const W = 2560, H = 1440
|
||||
const PRESETS = [
|
||||
{ id: 'glyph', name: 'Glyph', desc: '5x5', tag: 'VERY EASY', icon: 'feather' },
|
||||
{ id: 'rune', name: 'Rune', desc: '5x5', tag: 'EASY', icon: 'diamond' },
|
||||
{ id: 'scroll', name: 'Scroll', desc: '9x9', tag: 'MEDIUM', icon: 'compass' },
|
||||
{ id: 'codex', name: 'Codex', desc: '15x15', tag: 'HARD', icon: 'flame' },
|
||||
{ id: 'grimoire', name: 'Grimoire', desc: '21x21', tag: 'EXPERT', icon: 'meteor' },
|
||||
]
|
||||
|
||||
function fetchJSON(urlPath) {
|
||||
return new Promise((resolve) => {
|
||||
http.get(`http://127.0.0.1:8080${urlPath}`, res => {
|
||||
if (res.statusCode !== 200) { resolve(null); res.resume(); return }
|
||||
let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)) } catch { resolve(null) } })
|
||||
}).on('error', () => resolve(null))
|
||||
})
|
||||
}
|
||||
|
||||
async function loadIcon(name, size) {
|
||||
const dataDir = process.env.DATA_DIR || '/app/data'
|
||||
const svgPath = path.join(dataDir, 'icons', `${name}.svg`)
|
||||
try {
|
||||
let svg = fs.readFileSync(svgPath, 'utf8')
|
||||
// set viewBox and size for rendering at target size
|
||||
svg = svg.replace(/width="24"/, `width="${size}"`)
|
||||
svg = svg.replace(/height="24"/, `height="${size}"`)
|
||||
const buf = Buffer.from(svg)
|
||||
return await loadImage(buf)
|
||||
} catch (e) {
|
||||
console.log(`could not load icon ${name}: ${e.message}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('daily image generator starting')
|
||||
if (!createCanvas) { console.log('canvas module not available, skipping'); process.exit(0) }
|
||||
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
console.log('generating for', today)
|
||||
const dateDisplay = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }).toUpperCase()
|
||||
|
||||
const grids = await Promise.all(PRESETS.map(async p => {
|
||||
const res = await fetchJSON(`/api/puzzles/daily/${today}/${p.id}`)
|
||||
return res ? { width: res.puzzle.width, height: res.puzzle.height, cells: res.puzzle.cells } : null
|
||||
}))
|
||||
|
||||
// preload icons
|
||||
const iconSize = 56
|
||||
const icons = await Promise.all(PRESETS.map(p => loadIcon(p.icon, iconSize)))
|
||||
|
||||
const canvas = createCanvas(W, H)
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
ctx.fillStyle = '#111111'
|
||||
ctx.fillRect(0, 0, W, H)
|
||||
|
||||
const cardW = 400, cardGap = 32, headerH = 240, gridPad = 24, bottomPad = 24
|
||||
const gridAreaW = cardW - gridPad * 2, maxGridH = 600
|
||||
|
||||
let tallestCard = headerH + bottomPad
|
||||
for (const grid of grids) {
|
||||
if (grid) {
|
||||
const cellSize = Math.min(gridAreaW / grid.width, maxGridH / grid.height)
|
||||
tallestCard = Math.max(tallestCard, headerH + cellSize * grid.height + bottomPad)
|
||||
}
|
||||
}
|
||||
|
||||
const totalCardsW = 5 * cardW + 4 * cardGap
|
||||
const startX = (W - totalCardsW) / 2
|
||||
|
||||
const topRuleH = 3, nameH = 190, dateH = 120, bottomRuleH = 3
|
||||
const gapAfterRule = 80, todaysH = 52, gapBeforeCards = 40
|
||||
const accentGap = 50, accentH = 3
|
||||
|
||||
const totalContentH = topRuleH + nameH + dateH + bottomRuleH + gapAfterRule + todaysH + gapBeforeCards + tallestCard + accentGap + accentH
|
||||
let y = Math.max(0, (H - totalContentH) / 2)
|
||||
|
||||
// top rule
|
||||
ctx.fillStyle = '#ededed'
|
||||
ctx.fillRect(140, y, W - 280, topRuleH)
|
||||
y += topRuleH
|
||||
|
||||
// title
|
||||
ctx.fillStyle = '#ededed'
|
||||
ctx.font = 'bold 180px serif'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText('CRUCIVERB', W / 2, y + nameH)
|
||||
y += nameH
|
||||
|
||||
// date
|
||||
ctx.fillStyle = '#ababab'
|
||||
ctx.font = '32px monospace'
|
||||
ctx.fillText(dateDisplay, W / 2, y + 55)
|
||||
y += dateH
|
||||
|
||||
// bottom rule
|
||||
ctx.fillStyle = '#ededed'
|
||||
ctx.fillRect(140, y, W - 280, bottomRuleH)
|
||||
y += bottomRuleH + gapAfterRule
|
||||
|
||||
// subtitle
|
||||
ctx.fillStyle = '#b5b5b5'
|
||||
ctx.font = 'italic 52px serif'
|
||||
ctx.fillText("Today's puzzles", W / 2, y + todaysH * 0.75)
|
||||
y += todaysH + gapBeforeCards
|
||||
|
||||
const cardY = y
|
||||
let maxCardBottom = cardY
|
||||
|
||||
// cards
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const p = PRESETS[i], grid = grids[i]
|
||||
const cx = startX + i * (cardW + cardGap)
|
||||
|
||||
let cardH = headerH + bottomPad
|
||||
let gridDrawH = 0
|
||||
if (grid) {
|
||||
const cellSize = Math.min(gridAreaW / grid.width, maxGridH / grid.height)
|
||||
gridDrawH = cellSize * grid.height
|
||||
cardH = headerH + gridDrawH + bottomPad
|
||||
}
|
||||
|
||||
maxCardBottom = Math.max(maxCardBottom, cardY + cardH)
|
||||
|
||||
// card background
|
||||
ctx.fillStyle = '#1a1a1a'
|
||||
ctx.fillRect(cx, cardY, cardW, cardH)
|
||||
ctx.strokeStyle = '#2a2a2a'
|
||||
ctx.lineWidth = 1
|
||||
ctx.strokeRect(cx, cardY, cardW, cardH)
|
||||
|
||||
// icon
|
||||
if (icons[i]) {
|
||||
ctx.drawImage(icons[i], cx + cardW / 2 - iconSize / 2, cardY + 28, iconSize, iconSize)
|
||||
}
|
||||
|
||||
// preset name
|
||||
ctx.fillStyle = '#ededed'
|
||||
ctx.font = 'bold 56px serif'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText(p.name, cx + cardW / 2, cardY + 150)
|
||||
|
||||
// size and tag
|
||||
ctx.fillStyle = '#ababab'
|
||||
ctx.font = '20px monospace'
|
||||
ctx.fillText(`${p.desc} ${p.tag}`, cx + cardW / 2, cardY + 185)
|
||||
|
||||
// grid
|
||||
if (grid) {
|
||||
const cellSize = Math.min(gridAreaW / grid.width, maxGridH / grid.height)
|
||||
const gridW = cellSize * grid.width, gridH = cellSize * grid.height
|
||||
const ox = cx + gridPad + (gridAreaW - gridW) / 2
|
||||
const oy = cardY + headerH
|
||||
for (let r = 0; r < grid.height; r++) {
|
||||
for (let c = 0; c < grid.width; c++) {
|
||||
const px = ox + c * cellSize, py = oy + r * cellSize
|
||||
ctx.fillStyle = grid.cells[r][c] === 'black' ? '#090909' : '#1e1e1e'
|
||||
ctx.fillRect(px, py, cellSize, cellSize)
|
||||
ctx.strokeStyle = '#555555'
|
||||
ctx.lineWidth = 0.5
|
||||
ctx.strokeRect(px, py, cellSize, cellSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// accent line
|
||||
ctx.fillStyle = '#d4432f'
|
||||
ctx.fillRect(140, maxCardBottom + accentGap, W - 280, accentH)
|
||||
|
||||
// save with today's date and as canonical name
|
||||
const buf = canvas.toBuffer('image/jpeg', { quality: 0.85 })
|
||||
const dataDir = process.env.DATA_DIR || '/app/data'
|
||||
fs.writeFileSync(`${dataDir}/daily-image-${today}.jpg`, buf)
|
||||
fs.writeFileSync(`${dataDir}/daily-image.jpg`, buf)
|
||||
console.log(`saved daily image for ${today} (${buf.length} bytes)`)
|
||||
}
|
||||
|
||||
main().catch(e => { console.error(e); process.exit(1) })
|
||||
Reference in New Issue
Block a user