180 lines
5.9 KiB
JavaScript
180 lines
5.9 KiB
JavaScript
/* 第 3 页场景数据
|
|
前景:演职员表;背景:电线、麻雀、草。
|
|
本页不铺「天」,空白格保持为空。 */
|
|
|
|
(() => {
|
|
/* 固定种子的噪声,保证每次渲染一致 */
|
|
const noise = (x, y) => {
|
|
let h = Math.imul(x, 374761393) + Math.imul(y, 668265263);
|
|
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
|
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
|
|
};
|
|
|
|
/* 火山:占据下半屏,下 2/3 为山体、上 1/3 为熔岩棋盘格 */
|
|
const volcano = [];
|
|
const VOLCANO_TOP = 21;
|
|
const VOLCANO_BOTTOM = 42;
|
|
|
|
/* 每列的熔岩下沿,形成不规则的熔岩流 */
|
|
const lavaBottom = Array.from({ length: 28 }, (_, x) => {
|
|
return VOLCANO_TOP + 6 + Math.floor(noise(x, 3) * 3);
|
|
});
|
|
[5, 13, 21].forEach((x) => {
|
|
lavaBottom[x] += 4 + Math.floor(noise(x, 9) * 3);
|
|
});
|
|
|
|
/* 两层熔岩的分界:按列小幅随机起伏 */
|
|
const lavaSplit = Array.from({ length: 28 }, (_, x) => {
|
|
const offset = Math.round((noise(x, 13) - 0.5) * 4);
|
|
return Math.max(
|
|
VOLCANO_TOP + 2,
|
|
Math.min(VOLCANO_TOP + 5, VOLCANO_TOP + 3 + offset)
|
|
);
|
|
});
|
|
|
|
/* 主熔岩区以下为山体,两侧做对称平滑处理 */
|
|
const ROCK_START = VOLCANO_TOP + 7;
|
|
|
|
for (let y = VOLCANO_TOP; y <= VOLCANO_BOTTOM; y++) {
|
|
/* 距底部 5 格起铺满整幅宽度,避免山体过陡 */
|
|
const fullWidthAt = VOLCANO_BOTTOM - 5;
|
|
const t = (y - VOLCANO_TOP) / (fullWidthAt - VOLCANO_TOP);
|
|
const baseWidth =
|
|
y >= fullWidthAt ? 28 : Math.round(10 + 18 * Math.min(1, Math.max(0, t)));
|
|
|
|
let width;
|
|
let left;
|
|
|
|
if (y < ROCK_START) {
|
|
/* 熔岩区保持原有随机形状 */
|
|
const jitter =
|
|
y >= fullWidthAt ? 0 : Math.round((noise(0, y) - 0.5) * 2);
|
|
width = Math.max(4, Math.min(28, baseWidth + jitter));
|
|
left = Math.floor((28 - width) / 2);
|
|
left += Math.round((noise(1, y) - 0.5) * 2);
|
|
left = Math.max(0, Math.min(28 - width, left));
|
|
if (y === VOLCANO_TOP + 1) left = Math.max(0, left - 1);
|
|
} else {
|
|
/* 山体左右对称,宽度取偶数和,保留低频小幅随机 */
|
|
const wobble = Math.round((noise(7, Math.floor(y / 3)) - 0.5) * 2);
|
|
let w = y >= fullWidthAt ? 28 : Math.min(28, baseWidth + wobble);
|
|
w -= w % 2;
|
|
width = Math.max(4, w);
|
|
left = (28 - width) / 2;
|
|
}
|
|
|
|
/* 顶部火山口:仅两行凹下,第一行缺口更宽 */
|
|
const craterGap = y < ROCK_START ? [4, 2][y - VOLCANO_TOP] || 0 : 0;
|
|
const gapStart = craterGap ? Math.floor((width - craterGap) / 2) : 0;
|
|
|
|
for (let i = 0; i < width; i++) {
|
|
if (craterGap && i >= gapStart && i < gapStart + craterGap) continue;
|
|
|
|
const x = left + i;
|
|
const isLava = y <= lavaBottom[x];
|
|
const isUpperLava = isLava && y <= lavaSplit[x];
|
|
|
|
volcano.push({
|
|
text: isLava ? ((x + y) % 2 === 0 ? '熔' : '岩') : '石',
|
|
x,
|
|
y,
|
|
cls: isLava ? (isUpperLava ? 'lava-a' : 'lava-b') : 'rock',
|
|
});
|
|
}
|
|
}
|
|
|
|
/* 第十行左侧补一个「熔」 */
|
|
const row10 = volcano.filter((cell) => cell.y === VOLCANO_TOP + 9);
|
|
const row10Left = Math.min(...row10.map((cell) => cell.x));
|
|
volcano.push({
|
|
text: '熔',
|
|
x: row10Left - 1,
|
|
y: VOLCANO_TOP + 9,
|
|
cls: 'lava-b',
|
|
});
|
|
|
|
TextScene.render({
|
|
cols: 28,
|
|
rows: 44,
|
|
skyChar: '',
|
|
glyphs: [
|
|
/* 电线:整行贯穿页面 */
|
|
{ text: '电线'.repeat(14), x: 0, y: 8, cls: 'wire' },
|
|
/* 麻雀:竖排于从左到右第四格,落在电线上方 */
|
|
{ text: '麻雀', x: 3, y: 6, cls: 'sparrow', vertical: true },
|
|
/* 火山铺在底部背景,草、消防栓在其上层 */
|
|
...volcano,
|
|
/* 草:最底部一排 */
|
|
{ text: '草'.repeat(28), x: 0, y: 43, cls: 'grass' },
|
|
/* 消防栓:最后三行分散竖排九根,左侧留 2 格 */
|
|
...[2, 5, 8, 10, 13, 16, 19, 23, 25].map((x) => ({
|
|
text: '消防栓',
|
|
x,
|
|
y: 41,
|
|
cls: 'hydrant',
|
|
vertical: true,
|
|
})),
|
|
],
|
|
});
|
|
|
|
const scene = document.querySelector('.scene');
|
|
const credits = document.createElement('h1');
|
|
credits.className = 'credits';
|
|
credits.textContent = '演职员表';
|
|
scene.append(credits);
|
|
|
|
/* 35 个姓名:4 列 × 9 行,放在一个半透明大区块上 */
|
|
const NAMES = [
|
|
'郑滕飞', '秦雪莹', '关宸宇', '王天晓', '谢雨舟', '储明威', '庞熙若',
|
|
'刘婉莹', '陈彦淇', '古雅文', '周胤伯', '郑佳欣', '娄敬庭', '张加弛',
|
|
'李昀芷', '吴红霓', '鲍馨仪', '刘润泽', '李若熙', '马希予', '袁立桓',
|
|
'蒋子模', '李有睿', '王海心', '朱丽烨', '朱彦阳', '汪楚钧', '吴玥',
|
|
'张梓轩', '罗午阳', '郭柯言', '王之饶', '应玥', '湛茜茜', '黄钰涵',
|
|
];
|
|
|
|
const TAG = {
|
|
cols: 4,
|
|
rows: 9,
|
|
width: 3,
|
|
height: 1,
|
|
gapX: 1,
|
|
gapY: 1,
|
|
padding: 1,
|
|
left: 2,
|
|
top: 12,
|
|
};
|
|
|
|
const blockWidth =
|
|
TAG.padding * 2 + TAG.cols * TAG.width + (TAG.cols - 1) * TAG.gapX;
|
|
const blockHeight =
|
|
TAG.padding * 2 + TAG.rows * TAG.height + (TAG.rows - 1) * TAG.gapY;
|
|
|
|
const block = document.createElement('div');
|
|
block.className = 'nametag-block';
|
|
block.style.left = `${TAG.left}em`;
|
|
block.style.top = `${TAG.top}em`;
|
|
block.style.width = `${blockWidth}em`;
|
|
block.style.height = `${blockHeight}em`;
|
|
block.style.padding = `${TAG.padding}em`;
|
|
block.style.gridTemplateColumns = `repeat(${TAG.cols}, ${TAG.width}em)`;
|
|
block.style.gridAutoRows = `${TAG.height}em`;
|
|
block.style.columnGap = `${TAG.gapX}em`;
|
|
block.style.rowGap = `${TAG.gapY}em`;
|
|
scene.append(block);
|
|
|
|
let nameIndex = 0;
|
|
for (let row = 0; row < TAG.rows; row++) {
|
|
for (let col = 0; col < TAG.cols; col++) {
|
|
if (row === TAG.rows - 1 && col === TAG.cols - 1) continue;
|
|
|
|
const name = NAMES[nameIndex++];
|
|
if (!name) break;
|
|
|
|
const label = document.createElement('div');
|
|
label.className = 'nametag-name';
|
|
label.textContent = name;
|
|
block.append(label);
|
|
}
|
|
}
|
|
})();
|