171 lines
5.0 KiB
JavaScript
171 lines
5.0 KiB
JavaScript
/* 落日文字画 · 28 × 44 网格
|
||
构图全部由下方数组描述;空白格自动用「天」铺满。
|
||
修改元素只需调整数组中的坐标与尺寸,行列范围:x 0–27,y 0–43。 */
|
||
|
||
(() => {
|
||
const COLS = 28;
|
||
const ROWS = 44;
|
||
|
||
/* 云:x/y 为最宽一行的左上角,三行 3/5/3;上下拉开分布 */
|
||
const clouds = [
|
||
{ x: 10, y: 2, tone: 'pink' },
|
||
{ x: 18, y: 6, tone: 'violet' },
|
||
{ x: 1, y: 10, tone: 'orange' },
|
||
{ x: 23, y: 13, tone: 'lilac' },
|
||
{ x: 4, y: 16, tone: 'rose' },
|
||
];
|
||
|
||
/* 落日:整体下移贴近楼群,形成日落;
|
||
色调按数组顺序,顶部偏红 → 底部偏黄 */
|
||
const sun = [
|
||
{ y: 18, x: 13, w: 2 },
|
||
{ y: 19, x: 10, w: 8 },
|
||
{ y: 20, x: 9, w: 10 },
|
||
{ y: 21, x: 8, w: 12 },
|
||
{ y: 22, x: 7, w: 14 },
|
||
{ y: 23, x: 7, w: 14 },
|
||
{ y: 24, x: 6, w: 16 },
|
||
{ y: 25, x: 6, w: 16 },
|
||
{ y: 26, x: 6, w: 16 },
|
||
{ y: 27, x: 6, w: 16 },
|
||
{ y: 28, x: 6, w: 16 },
|
||
{ y: 29, x: 7, w: 14 },
|
||
{ y: 30, x: 7, w: 14 },
|
||
{ y: 31, x: 8, w: 12 },
|
||
{ y: 32, x: 9, w: 10 },
|
||
{ y: 33, x: 10, w: 8 },
|
||
{ y: 34, x: 13, w: 2 },
|
||
];
|
||
|
||
/* 飞鸟:数量减少,间距不等,仅少量成对;上下拉开分布 */
|
||
const birds = [
|
||
{ x: 25, y: 2, kind: 'magpie' },
|
||
{ x: 1, y: 4, kind: 'sparrow' },
|
||
{ x: 3, y: 8, kind: 'crow' },
|
||
{ x: 9, y: 13, kind: 'magpie' },
|
||
{ x: 9, y: 15, kind: 'sparrow' },
|
||
{ x: 26, y: 5, kind: 'crow' },
|
||
{ x: 17, y: 9, kind: 'sparrow' },
|
||
{ x: 21, y: 12, kind: 'magpie' },
|
||
{ x: 19, y: 15, kind: 'crow' },
|
||
];
|
||
|
||
/* 天线:单列「线」,从 y 向下叠 h 格;最右楼顶另加一根深色 */
|
||
const antennas = [
|
||
{ x: 7, y: 28, h: 3 },
|
||
{ x: 22, y: 27, h: 4 },
|
||
{ x: 26, y: 34, h: 3, cls: 'ant ant-dark' },
|
||
];
|
||
|
||
/* 楼群:n 对应 .bld-n,最终按 x/y/w/h 铺满「楼」(高度裁到第 44 行) */
|
||
const buildings = [
|
||
{ n: 1, x: 0, y: 35, w: 3, h: 9 },
|
||
{ n: 2, x: 3, y: 39, w: 3, h: 5 },
|
||
{ n: 3, x: 6, y: 31, w: 4, h: 13 },
|
||
{ n: 4, x: 10, y: 36, w: 3, h: 8 },
|
||
{ n: 5, x: 13, y: 30, w: 4, h: 14 },
|
||
{ n: 6, x: 17, y: 34, w: 3, h: 10 },
|
||
{ n: 7, x: 20, y: 31, w: 4, h: 13 },
|
||
{ n: 8, x: 24, y: 37, w: 4, h: 7 },
|
||
];
|
||
|
||
/* 标题区:整格扣出的居中矩形,左右各留 3 格 */
|
||
const TITLE = {
|
||
text: '关于「甲子」的虚构故事',
|
||
emphasis: '甲子',
|
||
top: 14,
|
||
height: 5,
|
||
side: 3,
|
||
};
|
||
|
||
const BIRD_WORDS = { sparrow: '麻雀', magpie: '喜鹊', crow: '乌鸦' };
|
||
|
||
const grid = Array.from({ length: ROWS }, () =>
|
||
Array.from({ length: COLS }, () => ({ ch: '天', cls: 'sky' }))
|
||
);
|
||
|
||
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 [a, b] = BIRD_WORDS[kind];
|
||
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 (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 += `<span class="${row[i].cls}">${text}</span>`;
|
||
i = j;
|
||
}
|
||
return `<div class="line">${line}</div>`;
|
||
})
|
||
.join('');
|
||
|
||
const scene = document.querySelector('.scene');
|
||
scene.innerHTML = html;
|
||
|
||
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 title = document.createElement('h1');
|
||
title.className = 'title';
|
||
|
||
const [before, after] = TITLE.text.split(TITLE.emphasis);
|
||
if (after === undefined) {
|
||
title.textContent = TITLE.text;
|
||
} else {
|
||
const em = document.createElement('span');
|
||
em.className = 'title-em';
|
||
em.textContent = TITLE.emphasis;
|
||
title.append(before, em, after);
|
||
}
|
||
|
||
panel.append(title);
|
||
scene.append(panel);
|
||
})();
|