feat: 新增 page3 演职员表并拆分多页资源
- 新增 page3.html:无「天」背景的演职员表与火山场景 - 重构为页面专属 CSS/JS 与共用的 text-scene 渲染引擎 - PDF 导出支持 --page 参数
This commit is contained in:
1 parent
b9ed01d91b
commit
319ba1890a
11 files changed
+538
-257
No files matched your search
@@ -1,6 +1,8 @@
|
|||||||
# 《关于「甲子」的虚构故事》公众号推送 Demo
|
# 《关于「甲子」的虚构故事》公众号推送 Demo
|
||||||
|
|
||||||
一个 28 列 × 44 行的汉字文字画单页。黑底、每格一个汉字,空白处由淡色「天」铺满;画面包含晚霞云、落日、飞鸟、天线与城市楼群,上方以整格扣出的黑色标题框居中呈现「关于「甲子」的虚构故事」。
|
一个 28 列 × 44 行的汉字文字画 Demo。黑底、每格一个汉字,空白处由淡色「天」铺满;画面包含晚霞云、落日、飞鸟、天线与城市楼群,上方以整格扣出的黑色标题框居中呈现「关于「甲子」的虚构故事」。
|
||||||
|
|
||||||
|
项目规划为三张类似页面,当前实现第 1 页(`page1.html`)与第 3 页(`page3.html`)。
|
||||||
|
|
||||||
## 效果概述
|
## 效果概述
|
||||||
|
|
||||||
@@ -14,33 +16,56 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├─ index.html # 页面骨架
|
├─ page1.html # 第 1 页入口
|
||||||
|
├─ page3.html # 第 3 页入口
|
||||||
├─ pyproject.toml # uv 环境与导出依赖
|
├─ pyproject.toml # uv 环境与导出依赖
|
||||||
├─ uv.lock # 依赖锁定
|
├─ uv.lock # 依赖锁定
|
||||||
├─ scripts/export_pdf.py # PDF 导出脚本
|
├─ scripts/export_pdf.py # PDF 导出脚本
|
||||||
├─ assets/
|
├─ assets/
|
||||||
│ ├─ css/style.css # 布局、字号与各元素配色
|
│ ├─ css/
|
||||||
│ ├─ js/scene.js # 场景数据与网格渲染
|
│ │ ├─ common.css # 三页共用:字体、骨架、网格、标题框
|
||||||
|
│ │ ├─ page1.css # 第 1 页配色
|
||||||
|
│ │ └─ page3.css # 第 3 页配色与前景标题
|
||||||
|
│ ├─ js/
|
||||||
|
│ │ ├─ text-scene.js # 共享文字画渲染引擎
|
||||||
|
│ │ ├─ page1.js # 第 1 页场景数据
|
||||||
|
│ │ └─ page3.js # 第 3 页场景数据
|
||||||
│ └─ fonts/zpix.ttf # Zpix 像素字体
|
│ └─ fonts/zpix.ttf # Zpix 像素字体
|
||||||
└─ references/a.html # 原始参考页
|
└─ references/a.html # 原始参考页
|
||||||
```
|
```
|
||||||
|
|
||||||
## 运行
|
## 运行
|
||||||
|
|
||||||
直接打开 `index.html` 即可。若需通过本地服务器预览:
|
直接打开 `page1.html` 即可。若需通过本地服务器预览:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m http.server 8000
|
python -m http.server 8000
|
||||||
# 浏览器访问 http://localhost:8000
|
# 第 1 页 http://localhost:8000/page1.html
|
||||||
|
# 第 3 页 http://localhost:8000/page3.html
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 多页结构
|
||||||
|
|
||||||
|
现有页面:
|
||||||
|
|
||||||
|
- `page1.html`:完整日落城市文字画,空白处铺淡色「天」。
|
||||||
|
- `page3.html`:无「天」背景的演职员表;前景标题「演职员表」,背景为电线、麻雀、枪、草、消防栓与下半屏火山。
|
||||||
|
|
||||||
|
共用与专属:
|
||||||
|
|
||||||
|
- `common.css`、`text-scene.js`:三页共用的页面骨架、网格、标题框与渲染引擎。
|
||||||
|
- `pageN.html`、`pageN.css`、`pageN.js`:每一页的入口、配色与场景数据。
|
||||||
|
|
||||||
|
新增页面时复制 `page1.*` 三件套,修改 `pageN.js` 中的数据与 `pageN.css` 配色,并按顺序引入 `common.css`、`pageN.css`、`text-scene.js`、`pageN.js`。
|
||||||
|
|
||||||
## 导出 PDF
|
## 导出 PDF
|
||||||
|
|
||||||
使用 Playwright(由 uv 管理)导出,页面尺寸为 30em × 46em:28 × 44 格的画面,四周各留恰好 1em。
|
使用 Playwright(由 uv 管理)导出,页面尺寸为 30em × 46em:28 × 44 格的画面,四周各留恰好 1em。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv sync
|
uv sync
|
||||||
uv run python scripts/export_pdf.py # 默认 1em = 24px,输出 dist/封面demo.pdf
|
uv run python scripts/export_pdf.py # 默认导出 page1.html 到 dist/封面demo.pdf
|
||||||
|
uv run python scripts/export_pdf.py --page page3.html -o dist/page3.pdf
|
||||||
uv run python scripts/export_pdf.py --em 16 -o dist/封面demo-16.pdf
|
uv run python scripts/export_pdf.py --em 16 -o dist/封面demo-16.pdf
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -54,7 +79,7 @@ uv run playwright install chromium
|
|||||||
|
|
||||||
## 调整指南
|
## 调整指南
|
||||||
|
|
||||||
### 构图(assets/js/scene.js)
|
### 构图(assets/js/page1.js)
|
||||||
|
|
||||||
| 数据 | 含义 |
|
| 数据 | 含义 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -63,24 +88,33 @@ uv run playwright install chromium
|
|||||||
| `birds` | 飞鸟:每只占 2 格(麻雀 / 喜鹊 / 乌鸦) |
|
| `birds` | 飞鸟:每只占 2 格(麻雀 / 喜鹊 / 乌鸦) |
|
||||||
| `antennas` | 天线:单列「线」,从 y 向下叠 h 格 |
|
| `antennas` | 天线:单列「线」,从 y 向下叠 h 格 |
|
||||||
| `buildings` | 楼群:x/y/w/h,n 对应 `.bld-n` |
|
| `buildings` | 楼群:x/y/w/h,n 对应 `.bld-n` |
|
||||||
| `TITLE` | 标题文案、起始行、行高、左右留格数 |
|
| `title` | 标题文案、高亮词、起始行、行高、左右留格数 |
|
||||||
|
|
||||||
坐标为 0–27 列、0–43 行;越界会直接抛错提示。
|
坐标为 0–27 列、0–43 行;越界会直接抛错提示。
|
||||||
|
|
||||||
### 样式(assets/css/style.css)
|
### 样式
|
||||||
|
|
||||||
|
共用样式(`assets/css/common.css`):
|
||||||
|
|
||||||
- 整体字号:`.scene` 的 `font-size`,所有元素统一继承。
|
- 整体字号:`.scene` 的 `font-size`,所有元素统一继承。
|
||||||
|
- 标题:`.title-panel`(黑色矩形)与 `.title-panel .title`(文字样式)。
|
||||||
|
|
||||||
|
页面样式(`assets/css/page1.css`):
|
||||||
|
|
||||||
- 背景「天」:`.sky`。
|
- 背景「天」:`.sky`。
|
||||||
- 云:`.cloud` 的明度与 `.cloud-*` 的色相。
|
- 云:`.cloud` 的明度与 `.cloud-*` 的色相。
|
||||||
- 落日:`.sun` 的整体明度(当前为原色 90%),`.sun-t1` 至 `.sun-t17` 为逐行色阶。
|
- 落日:`.sun` 的整体明度(当前为原色 80%),`.sun-t1` 至 `.sun-t17` 为逐行色阶。
|
||||||
- 飞鸟:`.bird-sparrow`、`.bird-magpie`、`.bird-crow`。
|
- 飞鸟:`.bird-sparrow`、`.bird-magpie`、`.bird-crow`。
|
||||||
- 天线:`.ant`(深色款为 `.ant-dark`)。
|
- 天线:`.ant`(深色款为 `.ant-dark`)。
|
||||||
- 楼群:`.bld` 与 `.bld-1` 至 `.bld-8`。
|
- 楼群:`.bld` 与 `.bld-1` 至 `.bld-8`。
|
||||||
- 标题:`.title-panel`(黑色矩形)与 `.title-panel .title`(白色文字及字号)。
|
|
||||||
|
### 第 3 页(assets/js/page3.js)
|
||||||
|
|
||||||
|
通过 `glyphs` 数组放置任意背景字:`{ text, x, y, cls, vertical }`,坐标仍按格对齐。前景标题 `.credits` 由页面脚本追加,可在 `page3.css` 中调整字号与位置。
|
||||||
|
|
||||||
## 实现说明
|
## 实现说明
|
||||||
|
|
||||||
- `scene.js` 先建立 28×44 网格并全部填「天」,再按数组覆盖元素;同 class 的连续字符合并为单个 `span` 输出。
|
- `text-scene.js` 先建立 28×44 网格并全部填「天」,再按页面数据覆盖元素;同 class 的连续字符合并为单个 `span` 输出。
|
||||||
- 标题区把对应格子清空,并在同一位置叠放绝对定位的黑色面板;边缘与字符整格对齐,不会出现半个字。
|
- 标题区把对应格子清空,并在同一位置叠放绝对定位的黑色面板;边缘与字符整格对齐,不会出现半个字。
|
||||||
- Zpix 单字步进为 13/12 em,因此 `.line` 使用 `letter-spacing: -0.083333em` 补偿,保证 28 列恰好落在 28em 宽内。
|
- Zpix 单字步进为 13/12 em,因此 `.line` 使用 `letter-spacing: -0.083333em` 补偿,保证 28 列恰好落在 28em 宽内。
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/* 三页共用:字体、页面骨架、文字网格与标题框 */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Zpix";
|
||||||
|
src: url("../fonts/zpix.ttf") format("truetype");
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100dvh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: "Zpix", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
|
||||||
|
"Noto Sans SC", "Source Han Sans SC", sans-serif;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 28 列 × 44 行文字网格,1em = 1 个汉字;
|
||||||
|
字号统一在此控制,所有元素(含天线)字号一致 */
|
||||||
|
.scene {
|
||||||
|
position: relative;
|
||||||
|
font-size: min(3.4vw, calc(100dvh / 44));
|
||||||
|
line-height: 1em;
|
||||||
|
width: 28em;
|
||||||
|
height: 44em;
|
||||||
|
user-select: none;
|
||||||
|
font-kerning: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
display: block;
|
||||||
|
height: 1em;
|
||||||
|
line-height: 1em;
|
||||||
|
white-space: pre;
|
||||||
|
/* Zpix 单字步进为 13/12 em,补偿回 1em,保证 28 列正好落在 28em 内 */
|
||||||
|
letter-spacing: -0.083333em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题框:按整格扣出的黑色矩形,位置与尺寸由页面数据决定 */
|
||||||
|
.title-panel {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-panel .title {
|
||||||
|
margin: 0;
|
||||||
|
color: #e6e6e6;
|
||||||
|
font-size: 1.8em;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-panel .title-em { color: #fff; }
|
||||||
@@ -1,70 +1,4 @@
|
|||||||
@font-face {
|
/* 第 1 页:天空、云、落日、飞鸟、天线与楼群配色 */
|
||||||
font-family: "Zpix";
|
|
||||||
src: url("../fonts/zpix.ttf") format("truetype");
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
min-height: 100dvh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
font-family: "Zpix", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
|
|
||||||
"Noto Sans SC", "Source Han Sans SC", sans-serif;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 28 列 × 44 行文字网格,1em = 1 个汉字;
|
|
||||||
字号在这里统一控制,所有元素(含天线)字号完全一致 */
|
|
||||||
.scene {
|
|
||||||
position: relative;
|
|
||||||
font-size: min(3.4vw, calc(100dvh / 44));
|
|
||||||
line-height: 1em;
|
|
||||||
width: 28em;
|
|
||||||
height: 44em;
|
|
||||||
user-select: none;
|
|
||||||
font-kerning: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line {
|
|
||||||
display: block;
|
|
||||||
height: 1em;
|
|
||||||
line-height: 1em;
|
|
||||||
white-space: pre;
|
|
||||||
/* Zpix 单字步进为 13/12 em,补偿回 1em,保证 28 列正好落在 28em 内 */
|
|
||||||
letter-spacing: -0.083333em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— 标题区:按整格扣出的黑色矩形,左右各留 3 格 —— */
|
|
||||||
.title-panel {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-panel .title {
|
|
||||||
margin: 0;
|
|
||||||
color: #e6e6e6;
|
|
||||||
font-size: 1.8em;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-panel .title-em { color: #fff; }
|
|
||||||
|
|
||||||
/* —— 背景:空白格里的淡色「天」 —— */
|
/* —— 背景:空白格里的淡色「天」 —— */
|
||||||
.sky { color: rgba(190, 200, 240, 0.15); }
|
.sky { color: rgba(190, 200, 240, 0.15); }
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/* 第 3 页:无「天」背景;只保留电线、麻雀、枪、草与前景标题 */
|
||||||
|
|
||||||
|
.wire { color: #8794a5; }
|
||||||
|
.sparrow { color: #ff7a3f; }
|
||||||
|
.gun { color: #8b6f55; }
|
||||||
|
.grass { color: #4fc353; }
|
||||||
|
.hydrant { color: #a93b43; }
|
||||||
|
/* 火山岩 / 熔岩参考色,按暗度 70% 压暗(保留 30% 明度) */
|
||||||
|
.rock { color: color-mix(in srgb, #d07a38 30%, #000); }
|
||||||
|
.lava-a { color: color-mix(in srgb, #ff8f3f 30%, #000); }
|
||||||
|
.lava-b { color: color-mix(in srgb, #ff5a2a 30%, #000); }
|
||||||
|
|
||||||
|
/* 前景标题:不参与网格,字号与位置自由;
|
||||||
|
top 的 em 相对自身 1.6em 字号,3.125em = 距顶部 5 个背景格 */
|
||||||
|
.credits {
|
||||||
|
position: absolute;
|
||||||
|
top: 3.125em;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
margin: 0;
|
||||||
|
color: #f2f2f2;
|
||||||
|
font-size: 1.6em;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/* 第 1 页场景数据
|
||||||
|
渲染引擎见 text-scene.js;坐标范围 x 0–27,y 0–43。 */
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
/* 云: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 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/* 标题区:整格扣出的居中矩形,左右各留 2 格 */
|
||||||
|
const title = {
|
||||||
|
text: '关于「甲子」的虚构故事',
|
||||||
|
emphasis: '甲子',
|
||||||
|
top: 14,
|
||||||
|
height: 4,
|
||||||
|
side: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
TextScene.render({
|
||||||
|
cols: 28,
|
||||||
|
rows: 44,
|
||||||
|
clouds,
|
||||||
|
sun,
|
||||||
|
birds,
|
||||||
|
antennas,
|
||||||
|
buildings,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/* 第 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,
|
||||||
|
/* 枪:多个「枪」拼出枪形,占最右 4 列,枪口指向左上方的麻雀 */
|
||||||
|
{ text: '枪', x: 24, y: 20, cls: 'gun' },
|
||||||
|
{ text: '枪', x: 25, y: 20, cls: 'gun' },
|
||||||
|
{ text: '枪', x: 25, y: 21, cls: 'gun' },
|
||||||
|
{ text: '枪', x: 26, y: 21, cls: 'gun' },
|
||||||
|
{ text: '枪', x: 26, y: 22, cls: 'gun' },
|
||||||
|
{ text: '枪', x: 27, y: 22, cls: 'gun' },
|
||||||
|
{ text: '枪', x: 27, y: 23, cls: 'gun' },
|
||||||
|
{ text: '枪', x: 27, y: 24, cls: 'gun' },
|
||||||
|
/* 草:最底部一排 */
|
||||||
|
{ 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);
|
||||||
|
})();
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
/* 落日文字画 · 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 },
|
|
||||||
];
|
|
||||||
|
|
||||||
/* 标题区:整格扣出的居中矩形,左右各留 2 格 */
|
|
||||||
const TITLE = {
|
|
||||||
text: '关于「甲子」的虚构故事',
|
|
||||||
emphasis: '甲子',
|
|
||||||
top: 14,
|
|
||||||
height: 4,
|
|
||||||
side: 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
})();
|
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
/* 文字画共享渲染引擎
|
||||||
|
页面脚本调用 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 += `<span class="${row[i].cls}">${text}</span>`;
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
|
return `<div class="line">${line}</div>`;
|
||||||
|
})
|
||||||
|
.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 };
|
||||||
|
})();
|
||||||
@@ -5,8 +5,10 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<meta name="color-scheme" content="dark">
|
<meta name="color-scheme" content="dark">
|
||||||
<title>封面</title>
|
<title>封面</title>
|
||||||
<link rel="stylesheet" href="assets/css/style.css">
|
<link rel="stylesheet" href="assets/css/common.css">
|
||||||
<script src="assets/js/scene.js" defer></script>
|
<link rel="stylesheet" href="assets/css/page1.css">
|
||||||
|
<script src="assets/js/text-scene.js" defer></script>
|
||||||
|
<script src="assets/js/page1.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<meta name="color-scheme" content="dark">
|
||||||
|
<title>演职员表</title>
|
||||||
|
<link rel="stylesheet" href="assets/css/common.css">
|
||||||
|
<link rel="stylesheet" href="assets/css/page3.css">
|
||||||
|
<script src="assets/js/text-scene.js" defer></script>
|
||||||
|
<script src="assets/js/page3.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="scene"></div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+13
-5
@@ -1,10 +1,11 @@
|
|||||||
"""将 index.html 导出为 PDF,画面四边各留恰好 1em。
|
"""将文字画页面导出为 PDF,画面四边各留恰好 1em。
|
||||||
|
|
||||||
页面为 28 × 44 格文字画,1em 即一个汉字格。导出页尺寸为 30em × 46em,
|
页面为 28 × 44 格文字画,1em 即一个汉字格。导出页尺寸为 30em × 46em,
|
||||||
场景四周各留 1em;底色铺满整页。
|
场景四周各留 1em;底色铺满整页。
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
uv run python scripts/export_pdf.py
|
uv run python scripts/export_pdf.py
|
||||||
|
uv run python scripts/export_pdf.py --page page2.html -o dist/page2.pdf
|
||||||
uv run python scripts/export_pdf.py --em 16 -o dist/封面demo-16.pdf
|
uv run python scripts/export_pdf.py --em 16 -o dist/封面demo-16.pdf
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -16,7 +17,6 @@ import pathlib
|
|||||||
from playwright.sync_api import sync_playwright
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
HTML = ROOT / "index.html"
|
|
||||||
|
|
||||||
COLS = 28
|
COLS = 28
|
||||||
ROWS = 44
|
ROWS = 44
|
||||||
@@ -71,7 +71,7 @@ def launch_browser(pw):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def export(em: float, out: pathlib.Path) -> None:
|
def export(em: float, out: pathlib.Path, html: pathlib.Path) -> None:
|
||||||
css, page_w, page_h = export_css(em)
|
css, page_w, page_h = export_css(em)
|
||||||
|
|
||||||
with sync_playwright() as pw:
|
with sync_playwright() as pw:
|
||||||
@@ -80,7 +80,7 @@ def export(em: float, out: pathlib.Path) -> None:
|
|||||||
viewport={"width": round(page_w), "height": round(page_h)},
|
viewport={"width": round(page_w), "height": round(page_h)},
|
||||||
device_scale_factor=1,
|
device_scale_factor=1,
|
||||||
)
|
)
|
||||||
page.goto(HTML.as_uri(), wait_until="load")
|
page.goto(html.as_uri(), wait_until="load")
|
||||||
page.evaluate("() => document.fonts.ready")
|
page.evaluate("() => document.fonts.ready")
|
||||||
if not page.evaluate("document.fonts.check('16px Zpix')"):
|
if not page.evaluate("document.fonts.check('16px Zpix')"):
|
||||||
raise SystemExit("Zpix 字体未加载,无法导出。")
|
raise SystemExit("Zpix 字体未加载,无法导出。")
|
||||||
@@ -128,6 +128,11 @@ def export(em: float, out: pathlib.Path) -> None:
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser(description="将文字画页面导出为 PDF(四周各留 1em)")
|
parser = argparse.ArgumentParser(description="将文字画页面导出为 PDF(四周各留 1em)")
|
||||||
|
parser.add_argument(
|
||||||
|
"--page",
|
||||||
|
default="page1.html",
|
||||||
|
help="要导出的 HTML 文件,默认 page1.html",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--em",
|
"--em",
|
||||||
type=float,
|
type=float,
|
||||||
@@ -144,7 +149,10 @@ def main() -> None:
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.em <= 0:
|
if args.em <= 0:
|
||||||
parser.error("--em 必须大于 0")
|
parser.error("--em 必须大于 0")
|
||||||
export(args.em, args.out.resolve())
|
html = (ROOT / args.page).resolve()
|
||||||
|
if not html.is_file():
|
||||||
|
parser.error(f"页面不存在:{args.page}")
|
||||||
|
export(args.em, args.out.resolve(), html)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in new issue
Block a user