/* 文字画共享渲染引擎 页面脚本调用 TextScene.render({...}) 提供数据即可,示例见 page1.js。 网格坐标:x 0–cols-1,y 0–rows-1;空白格自动填充天空字符。 */ (() => { const BIRD_WORDS = { sparrow: '麻雀', magpie: '喜鹊', crow: '乌鸦', }; function render({ cols, rows, skyChar = '天', skyClass = 'sky', clouds = [], sun = [], birds = [], antennas = [], buildings = [], glyphs = [], title, container = '.scene', }) { if (!cols || !rows) throw new Error('TextScene.render 需要 cols 与 rows'); // skyChar 传空字符串时不铺背景字,空格仍占满整格 const blank = skyChar ? { ch: skyChar, cls: skyClass } : { ch: ' ', cls: 'void' }; const grid = Array.from({ length: rows }, () => Array.from({ length: cols }, () => ({ ...blank })) ); const put = (x, y, ch, cls) => { if (x < 0 || x >= cols || y < 0 || y >= rows) { throw new Error(`元素超出网格范围:(${x}, ${y})`); } grid[y][x] = { ch, cls }; }; const putRow = (y, x, n, ch, cls) => { for (let i = 0; i < n; i++) put(x + i, y, ch, cls); }; for (const { x, y, tone } of clouds) { const cls = `cloud cloud-${tone}`; putRow(y, x + 1, 3, '云', cls); putRow(y + 1, x, 5, '云', cls); putRow(y + 2, x + 1, 3, '云', cls); } sun.forEach(({ y, x, w }, i) => { const tone = `sun-t${i + 1}`; for (let dx = 0; dx < w; dx++) put(x + dx, y, '日', `sun ${tone}`); }); for (const { x, y, kind } of birds) { const word = BIRD_WORDS[kind]; if (!word) throw new Error(`未知鸟种:${kind}`); const [a, b] = [...word]; put(x, y, a, `bird-${kind}`); put(x + 1, y, b, `bird-${kind}`); } for (const { x, y, h, cls = 'ant' } of antennas) { for (let i = 0; i < h; i++) put(x, y + i, '线', cls); } for (const { n, x, y, w, h } of buildings) { for (let j = 0; j < h; j++) putRow(y + j, x, w, '楼', `bld bld-${n}`); } for (const { text, x, y, cls = 'glyph', vertical = false } of glyphs) { [...text].forEach((ch, i) => { put(vertical ? x : x + i, vertical ? y + i : y, ch, cls); }); } if (title) { for (let y = title.top; y < title.top + title.height; y++) { for (let x = title.side; x < cols - title.side; x++) { grid[y][x] = { ch: ' ', cls: 'cut' }; } } } /* 把同 class 的连续字符合并成一个 span,减少 DOM 节点 */ const html = grid .map((row) => { let line = ''; for (let i = 0; i < cols; ) { let j = i + 1; while (j < cols && row[j].cls === row[i].cls) j++; const text = row.slice(i, j).map((cell) => cell.ch).join(''); line += `${text}`; i = j; } return `
${line}
`; }) .join(''); const scene = document.querySelector(container); if (!scene) throw new Error(`未找到容器:${container}`); scene.innerHTML = html; if (!title) return; const panel = document.createElement('div'); panel.className = 'title-panel'; panel.style.top = `${title.top}em`; panel.style.left = `${title.side}em`; panel.style.width = `${cols - title.side * 2}em`; panel.style.height = `${title.height}em`; const titleEl = document.createElement('h1'); titleEl.className = 'title'; const { text, emphasis } = title; if (emphasis && text.includes(emphasis)) { const [before, after] = text.split(emphasis); const em = document.createElement('span'); em.className = 'title-em'; em.textContent = emphasis; titleEl.append(before, em, after); } else { titleEl.textContent = text; } panel.append(titleEl); scene.append(panel); } window.TextScene = { render }; })();