/** * 生成统一线性图标集 → client/src/static/icons/ * 用法: NODE_PATH=/server/node_modules node scripts/gen-icons.mjs * 规格: 48x48 (显示 22-24px @2x), 2.5px 线宽, 圆角端点 */ import { createRequire } from 'node:module' import { mkdirSync, writeFileSync } from 'node:fs' import { join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' const CANVAS_PATH = process.env.CANVAS_PATH || '/home/dev2-ubuntu/workspace/ai-art/server/node_modules/@napi-rs/canvas/index.js' const { createCanvas } = createRequire(import.meta.url)(CANVAS_PATH) const __dirname = dirname(fileURLToPath(import.meta.url)) const OUT = join(__dirname, '../client/src/static/icons') mkdirSync(OUT, { recursive: true }) const BRAND = '#5b5ff8' const GRAY = '#8e93a8' const WHITE = '#ffffff' const GREEN = '#22c55e' const S = 96 const LW = 2.6 const SCALE = 2 async function make(draw, color = BRAND) { const c = createCanvas(S, S) const ctx = c.getContext('2d') ctx.scale(SCALE, SCALE) ctx.strokeStyle = color ctx.fillStyle = color ctx.lineWidth = LW ctx.lineCap = 'round' ctx.lineJoin = 'round' draw(ctx) return await c.encode('png') } // —— 图标绘制函数(几何线条,风格统一)—— const shapes = { // 铃铛 notice: (g) => { g.beginPath(); g.arc(24, 18, 9, Math.PI, 0); g.stroke() g.beginPath(); g.moveTo(15, 18); g.lineTo(13, 30); g.lineTo(35, 30); g.lineTo(33, 18); g.stroke() g.beginPath(); g.arc(24, 34, 3, Math.PI, 0, true); g.stroke() }, // 用户 user: (g) => { g.beginPath(); g.arc(24, 17, 7, 0, Math.PI * 2); g.stroke() g.beginPath(); g.arc(24, 40, 12, Math.PI * 1.15, Math.PI * 1.85); g.stroke() }, // 搜索 search: (g) => { g.beginPath(); g.arc(21, 21, 10, 0, Math.PI * 2); g.stroke() g.beginPath(); g.moveTo(29, 29); g.lineTo(38, 38); g.stroke() }, // 收藏星(空心/实心两版) star: (g, fill) => { const pts = [] for (let i = 0; i < 10; i++) { const r = i % 2 === 0 ? 14 : 6 const a = -Math.PI / 2 + (i * Math.PI) / 5 pts.push([24 + r * Math.cos(a), 24 + r * Math.sin(a)]) } g.beginPath(); pts.forEach(([x, y], i) => (i ? g.lineTo(x, y) : g.moveTo(x, y))); g.closePath() if (fill) { g.save(); g.lineWidth = LW / 2; g.fill(); g.restore() } g.stroke() }, // 火焰(streak) flame: (g) => { g.beginPath() g.moveTo(24, 10) g.bezierCurveTo(30, 17, 34, 22, 34, 29) g.arc(24, 29, 10, 0, Math.PI, false) g.bezierCurveTo(14, 22, 18, 17, 24, 10) g.stroke() g.beginPath(); g.arc(24, 31, 4, 0, Math.PI * 2); g.stroke() }, // 星球(品牌) planet: (g) => { g.beginPath(); g.arc(22, 22, 11, 0, Math.PI * 2); g.stroke() g.beginPath(); g.ellipse(24, 24, 17, 6.5, -0.45, 0, Math.PI * 2); g.stroke() }, // 对话气泡 chat: (g) => { g.beginPath() g.moveTo(10, 14); g.lineTo(38, 14); g.quadraticCurveTo(41, 14, 41, 17) g.lineTo(41, 29); g.quadraticCurveTo(41, 32, 38, 32); g.lineTo(22, 32) g.lineTo(14, 39); g.lineTo(14, 32); g.quadraticCurveTo(11, 32, 11, 29) g.lineTo(11, 17); g.quadraticCurveTo(11, 14, 14, 14); g.closePath(); g.stroke() }, // 分享 share: (g) => { g.beginPath(); g.arc(34, 12, 5, 0, Math.PI * 2); g.stroke() g.beginPath(); g.arc(34, 36, 5, 0, Math.PI * 2); g.stroke() g.beginPath(); g.arc(14, 24, 5, 0, Math.PI * 2); g.stroke() g.beginPath(); g.moveTo(19.5, 21.5); g.lineTo(28.5, 14.5); g.stroke() g.beginPath(); g.moveTo(19.5, 26.5); g.lineTo(28.5, 33.5); g.stroke() }, // 设置齿轮 settings: (g) => { g.beginPath(); g.arc(24, 24, 6.5, 0, Math.PI * 2); g.stroke() for (let i = 0; i < 8; i++) { const a = (i * Math.PI) / 4 g.beginPath() g.moveTo(24 + 10.5 * Math.cos(a), 24 + 10.5 * Math.sin(a)) g.lineTo(24 + 14.5 * Math.cos(a), 24 + 14.5 * Math.sin(a)) g.stroke() } }, // 反馈(对话+点) feedback: (g) => { g.beginPath() g.moveTo(10, 13); g.lineTo(38, 13); g.quadraticCurveTo(41, 13, 41, 16) g.lineTo(41, 28); g.quadraticCurveTo(41, 31, 38, 31); g.lineTo(20, 31) g.lineTo(13, 38); g.lineTo(13, 31); g.quadraticCurveTo(10, 31, 10, 28) g.lineTo(10, 16); g.quadraticCurveTo(10, 13, 13, 13); g.closePath(); g.stroke() g.beginPath(); g.arc(17, 22, 1.6, 0, Math.PI * 2); g.fill() g.beginPath(); g.arc(25, 22, 1.6, 0, Math.PI * 2); g.fill() g.beginPath(); g.arc(33, 22, 1.6, 0, Math.PI * 2); g.fill() }, // 奖杯 trophy: (g) => { g.beginPath(); g.moveTo(16, 11); g.lineTo(32, 11); g.lineTo(31, 24); g.quadraticCurveTo(30, 30, 24, 30); g.quadraticCurveTo(18, 30, 17, 24); g.closePath(); g.stroke() g.beginPath(); g.moveTo(24, 30); g.lineTo(24, 36); g.stroke() g.beginPath(); g.moveTo(17, 38); g.lineTo(31, 38); g.stroke() g.beginPath(); g.moveTo(16, 14); g.lineTo(10, 16); g.quadraticCurveTo(10, 25, 16, 26); g.stroke() g.beginPath(); g.moveTo(32, 14); g.lineTo(38, 16); g.quadraticCurveTo(38, 25, 32, 26); g.stroke() }, // 火箭(趋势) rocket: (g) => { g.beginPath() g.moveTo(24, 8) g.bezierCurveTo(31, 13, 33, 20, 33, 27) g.lineTo(24, 33); g.lineTo(15, 27) g.bezierCurveTo(15, 20, 17, 13, 24, 8) g.closePath(); g.stroke() g.beginPath(); g.arc(24, 20, 3.5, 0, Math.PI * 2); g.stroke() g.beginPath(); g.moveTo(15, 27); g.lineTo(10, 36); g.lineTo(17, 33); g.stroke() g.beginPath(); g.moveTo(33, 27); g.lineTo(38, 36); g.lineTo(31, 33); g.stroke() }, // 书本(知识) book: (g) => { g.beginPath(); g.moveTo(24, 13); g.quadraticCurveTo(17, 9, 9, 11); g.lineTo(9, 36); g.quadraticCurveTo(17, 34, 24, 38); g.quadraticCurveTo(31, 34, 39, 36); g.lineTo(39, 11); g.quadraticCurveTo(31, 9, 24, 13); g.closePath(); g.stroke() g.beginPath(); g.moveTo(24, 13); g.lineTo(24, 38); g.stroke() }, // 灯泡(提示/AI) bulb: (g) => { g.beginPath(); g.arc(24, 19, 9.5, Math.PI * 0.95, Math.PI * 0.05, false); g.stroke() g.beginPath(); g.moveTo(19, 27); g.lineTo(19, 33); g.lineTo(29, 33); g.lineTo(29, 27); g.stroke() g.beginPath(); g.moveTo(20, 37); g.lineTo(28, 37); g.stroke() }, // 勾 check: (g) => { g.beginPath(); g.moveTo(13, 25); g.lineTo(21, 33); g.lineTo(35, 16); g.stroke() }, // 锁 lock: (g) => { g.beginPath(); g.moveTo(14, 23); g.lineTo(14, 37); g.lineTo(34, 37); g.lineTo(34, 23); g.closePath(); g.stroke() g.beginPath(); g.arc(24, 23, 6.5, Math.PI, 0); g.stroke() }, // 返回箭头 back: (g) => { g.beginPath(); g.moveTo(27, 12); g.lineTo(18, 24); g.lineTo(27, 36); g.stroke() }, // 右箭头 arrow: (g) => { g.beginPath(); g.moveTo(20, 12); g.lineTo(30, 24); g.lineTo(20, 36); g.stroke() }, // 发送 send: (g) => { g.beginPath(); g.moveTo(10, 24); g.lineTo(38, 12); g.lineTo(30, 36); g.lineTo(24, 27); g.closePath(); g.stroke() g.beginPath(); g.moveTo(24, 27); g.lineTo(38, 12); g.stroke() }, // 礼物 gift: (g) => { g.beginPath(); g.moveTo(10, 18); g.lineTo(38, 18); g.lineTo(38, 38); g.lineTo(10, 38); g.closePath(); g.stroke() g.beginPath(); g.moveTo(24, 18); g.lineTo(24, 38); g.stroke() g.beginPath(); g.moveTo(10, 24); g.lineTo(38, 24); g.stroke() g.beginPath(); g.arc(19.5, 13, 4.5, Math.PI * 0.6, Math.PI * 1.7); g.stroke() g.beginPath(); g.arc(28.5, 13, 4.5, Math.PI * 1.3, Math.PI * 0.4); g.stroke() }, // 时钟(继续探索) clock: (g) => { g.beginPath(); g.arc(24, 24, 12, 0, Math.PI * 2); g.stroke() g.beginPath(); g.moveTo(24, 16); g.lineTo(24, 24); g.lineTo(30, 28); g.stroke() }, // 闪烁(点亮/正确反馈) sparkle: (g) => { g.beginPath() g.moveTo(24, 8); g.quadraticCurveTo(26, 20, 38, 24); g.quadraticCurveTo(26, 28, 24, 40); g.quadraticCurveTo(22, 28, 10, 24); g.quadraticCurveTo(22, 20, 24, 8) g.closePath(); g.stroke() g.beginPath(); g.arc(37, 11, 1.8, 0, Math.PI * 2); g.fill() g.beginPath(); g.arc(11, 35, 1.4, 0, Math.PI * 2); g.fill() }, } const jobs = [] for (const [name, draw] of Object.entries(shapes)) { jobs.push([name, await make(draw, BRAND)]) jobs.push([`${name}-gray`, await make(draw, GRAY)]) jobs.push([`${name}-white`, await make(draw, WHITE)]) } // 特例:实心星(品牌色 + 灰) jobs.push(['star-fill', await make((g) => shapes.star(g, true), BRAND)]) jobs.push(['star-fill-gray', await make((g) => shapes.star(g, true), GRAY)]) jobs.push(['star-fill-white', await make((g) => shapes.star(g, true), WHITE)]) jobs.push(['check-green', await make(shapes.check, GREEN)]) for (const [name, buf] of jobs) writeFileSync(join(OUT, `${name}.png`), buf) console.log(`生成 ${jobs.length} 个图标 → ${OUT}`)