prototype-coarse-to-fine-transition.html 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="utf-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1" />
  6. <title>原型 · 低精度到高精度的交接</title>
  7. <style>
  8. :root { color-scheme: light; --ink: #1c2430; --muted: #5c6775; --line: #ddd4c6; --paper: #f6f3ec; --card: #fffdf8; --accent: #1f6f8b; --bad: #8c3a2f; --ok: #2d6a4f; }
  9. * { box-sizing: border-box; }
  10. body { margin: 0; font: 16px/1.5 "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; color: var(--ink); background: var(--paper); }
  11. main { max-width: 980px; margin: 0 auto; padding: 32px 20px 80px; }
  12. h1 { font-size: 28px; line-height: 1.25; margin: 0 0 8px; }
  13. h2 { font-size: 18px; margin: 28px 0 10px; }
  14. p { margin: 0 0 12px; }
  15. .eyebrow { color: var(--accent); font-weight: 700; letter-spacing: 0.04em; }
  16. .question, .assume, .panel, .walk { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 16px 18px; }
  17. .assume { border-color: #e2c48a; background: #fff8ea; }
  18. .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 10px; }
  19. .field { border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; background: #fff; }
  20. .field b { display: block; font-size: 13px; color: var(--muted); font-weight: 600; }
  21. .field.changed { border-color: var(--accent); box-shadow: inset 3px 0 0 var(--accent); }
  22. .groups { display: grid; gap: 14px; }
  23. .group { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
  24. .group span { min-width: 4.5em; color: var(--muted); font-size: 14px; }
  25. button { font: inherit; border: 1px solid #b9c7cf; background: #fff; color: var(--ink); border-radius: 999px; padding: 7px 12px; cursor: pointer; }
  26. button:hover { border-color: var(--accent); }
  27. button:disabled { opacity: 0.45; cursor: default; }
  28. button.primary { background: var(--accent); color: white; border-color: var(--accent); }
  29. .tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
  30. .tabs button[aria-selected="true"] { background: var(--ink); color: white; border-color: var(--ink); }
  31. .steps { display: grid; gap: 8px; margin-top: 12px; }
  32. .note { color: var(--muted); }
  33. .bad { color: var(--bad); }
  34. .ok { color: var(--ok); }
  35. code { font-family: ui-monospace, Consolas, monospace; font-size: 0.92em; }
  36. </style>
  37. </head>
  38. <body>
  39. <main>
  40. <p class="eyebrow">可丢弃原型 · 双击即可打开 · 不写入正式地图</p>
  41. <h1>低精度到高精度,边缘什么时候可以出现</h1>
  42. <p>用按钮把「全球 → 进入县城 → 锁定 → 返回 / 取消」走一遍,看精细影像会不会早于遮挡出现。</p>
  43. <section class="question">
  44. <p><strong>要回答的问题:</strong>在已确认的「先藏精细影像、雾先挡住、再渐显;返回时反过来」下,取消、雾失败、瓦片早晚和过期操作,会不会让矩形边缘先露出来。</p>
  45. <p class="note">这是逻辑原型,不是地球画面。它不证明肉眼看不到方块,也不证明帧耗时。保护是否成立由你显式标记,不能从「雾强度已经拉满」推出来。</p>
  46. </section>
  47. <h2>还没经你确认的假设</h2>
  48. <section class="assume">
  49. <p>下面这些只为让按钮能跑,点出来不对就改,不要当成验收通过:</p>
  50. <p>渐显和收起各分四下,只是按钮粒度,不是时长。亮起前除了保护成立,还要等「目标视野可显示」;瓦片失败时保持隐藏、继续用粗底图。返回收到看不见之后才回到全球。矩形内部换级不会被整层透明度修掉。</p>
  51. </section>
  52. <h2>当前状态</h2>
  53. <section id="state" class="panel"></section>
  54. <p id="delta" class="note"></p>
  55. <p id="refusal"></p>
  56. <h2>自由试</h2>
  57. <div id="free" class="groups"></div>
  58. <h2>对照走查</h2>
  59. <section class="walk">
  60. <div id="tabs" class="tabs"></div>
  61. <p id="blurb"></p>
  62. <div id="steps" class="steps"></div>
  63. </section>
  64. </main>
  65. <script>
  66. const Transition = (() => {
  67. const STEP = 0.25
  68. function createState() {
  69. return {
  70. phase: "global",
  71. intent: "idle",
  72. generation: 0,
  73. fogSupported: true,
  74. fogReady: false,
  75. fogStrength: 0,
  76. fogFailure: null,
  77. protection: false,
  78. alpha: 0,
  79. reveal: "hidden",
  80. prefetch: "idle",
  81. promise: "none",
  82. edgeExposed: false,
  83. internalSwap: false,
  84. notice: "全球和引导阶段只看粗底图,精细影像藏着。",
  85. refusal: null
  86. }
  87. }
  88. function next(state, patch) {
  89. return Object.assign({}, state, patch, { refusal: null })
  90. }
  91. function refuse(state, reason) {
  92. return Object.assign({}, state, { refusal: reason })
  93. }
  94. function fogExpected(state) {
  95. return state.fogSupported && !state.fogFailure
  96. }
  97. function beginPrefetch(state) {
  98. if (state.prefetch === "ready" || state.prefetch === "fetching") return state.prefetch
  99. return "fetching"
  100. }
  101. function syncReveal(state) {
  102. if (state.intent === "hide") {
  103. return state.alpha > 0 ? "concealing" : "hidden"
  104. }
  105. if (state.intent !== "show") return state.alpha > 0 ? "shown" : "hidden"
  106. if (state.alpha >= 1) return "shown"
  107. if (state.alpha > 0) return "revealing"
  108. return "hidden"
  109. }
  110. function promiseFor(state) {
  111. if (state.alpha <= 0) return "none"
  112. if (state.protection) return "covered"
  113. return "degraded"
  114. }
  115. function enter(state, phase) {
  116. const showing = state.alpha > 0
  117. return next(state, {
  118. phase: phase,
  119. intent: "show",
  120. generation: state.generation + 1,
  121. prefetch: beginPrefetch(state),
  122. reveal: syncReveal({ intent: "show", alpha: state.alpha }),
  123. promise: promiseFor(state),
  124. notice: showing
  125. ? "最新目标改成进入。过期的收起作废,精细层停在当前透明度。"
  126. : "已进入交接。精细层仍不可见,并开始预取瓦片。"
  127. })
  128. }
  129. function leave(state, label) {
  130. return next(state, {
  131. phase: "returning",
  132. intent: "hide",
  133. generation: state.generation + 1,
  134. reveal: state.alpha > 0 ? "concealing" : "hidden",
  135. notice: state.alpha > 0
  136. ? label + "立刻开始收起精细层,不把正在变亮的过程放完。"
  137. : label + "时精细层本来就看不见。"
  138. })
  139. }
  140. function reduce(state, action) {
  141. switch (action.type) {
  142. case "prepareFog":
  143. if (!state.fogSupported || state.fogFailure) return refuse(state, "雾当前不可用,准备不会变成保护。")
  144. return next(state, { fogReady: true, notice: "雾处理级可执行。强度仍是 0,边缘还没被挡住。" })
  145. case "raiseFog":
  146. if (!state.fogReady || !fogExpected(state)) return refuse(state, "雾还不能执行,不能把强度拉满。")
  147. return next(state, { fogStrength: 1, notice: "雾强度已满。这仍不等于矩形边缘已经被挡住。" })
  148. case "markProtection":
  149. if (!(state.fogReady && state.fogStrength >= 1 && fogExpected(state))) {
  150. return refuse(state, "保护不能由准备完成或强度单独成立。")
  151. }
  152. return next(state, { protection: true, promise: state.alpha > 0 ? "covered" : state.promise, notice: "已标记保护成立。精细层不会因此自动亮起。" })
  153. case "loseProtection":
  154. return next(state, {
  155. protection: false,
  156. promise: state.alpha > 0 ? "degraded" : "none",
  157. edgeExposed: state.edgeExposed || (state.alpha > 0 && fogExpected(state)),
  158. notice: state.alpha > 0 ? "保护消失时精细层还看得到。" : "保护已取消,精细层当时看不见。"
  159. })
  160. case "fogFailed":
  161. return next(state, {
  162. fogSupported: false,
  163. fogReady: false,
  164. fogStrength: 0,
  165. fogFailure: "failed",
  166. protection: false,
  167. promise: state.alpha > 0 ? "degraded" : "none",
  168. edgeExposed: state.edgeExposed || state.alpha > 0,
  169. notice: state.alpha > 0
  170. ? "雾已关闭并提示。精细层不突然藏起来,但不再承诺挡住边缘。"
  171. : "雾已关闭并提示。精细层当时看不见,业务不无限等待。"
  172. })
  173. case "fogRecovered":
  174. return next(state, {
  175. fogSupported: true,
  176. fogFailure: null,
  177. notice: "雾恢复为可准备。不会自动标成已经挡住边缘。"
  178. })
  179. case "enter":
  180. if (state.phase === "locked" && state.intent === "show") return refuse(state, "已经锁定在主场景,不用再次进入。")
  181. return enter(state, "entering")
  182. case "arrive":
  183. if (state.intent !== "show") return refuse(state, "当前目标不是进入,不能锁定主场景。")
  184. return next(state, { phase: "locked", notice: "相机已到主场景。精细层是否可见仍看保护和瓦片,不看锁定本身。" })
  185. case "cancel":
  186. if (state.phase !== "entering") return refuse(state, "只有正在进入时可以取消。已经锁定请用返回全球。")
  187. return leave(state, "取消进入")
  188. case "returnGlobal":
  189. if (state.phase === "global" && state.alpha === 0) return refuse(state, "已经在全球,精细层也看不见。")
  190. return leave(state, "返回全球")
  191. case "tilesReady":
  192. if (state.prefetch === "failed" && state.intent !== "show") return refuse(state, "这次加载已失败。要重试请先再次进入。")
  193. return next(state, { prefetch: "ready", notice: "目标视野现在可显示。是否亮起仍要看当前目标和保护。" })
  194. case "tilesFailed":
  195. if (state.alpha > 0) {
  196. return next(state, { prefetch: "failed", internalSwap: true, notice: "新的一级没到,已经显示的精细层保持原样。整层透明度修不了内部换挡。" })
  197. }
  198. return next(state, { prefetch: "failed", reveal: "hidden", notice: "瓦片失败。精细层继续隐藏,画面留在粗底图。" })
  199. case "revealStep": {
  200. if (state.intent !== "show") return refuse(state, "当前目标不是亮起精细层,过期的变亮无效。")
  201. if (state.prefetch !== "ready") return refuse(state, "目标视野还不能显示,先不亮,避免空白后突然跳出方块。")
  202. if (!state.protection && fogExpected(state)) return refuse(state, "雾仍被指望遮住边缘,但保护还没成立,不能亮起精细层。")
  203. if (state.alpha >= 1) return refuse(state, "精细层已经完全可见。")
  204. const alpha = Math.min(1, Math.round((state.alpha + STEP) * 100) / 100)
  205. const exposed = state.edgeExposed || !state.protection
  206. return next(state, {
  207. alpha: alpha,
  208. reveal: alpha >= 1 ? "shown" : "revealing",
  209. promise: state.protection ? "covered" : "degraded",
  210. edgeExposed: exposed,
  211. notice: state.protection ? "在保护成立后渐显一档。" : "无雾降级:精细层仍渐显,但不承诺挡住矩形边。"
  212. })
  213. }
  214. case "concealStep": {
  215. if (state.intent !== "hide") return refuse(state, "当前目标不是收起,过期的变暗无效。")
  216. if (state.alpha <= 0) return refuse(state, "精细层已经看不见。")
  217. const alpha = Math.max(0, Math.round((state.alpha - STEP) * 100) / 100)
  218. const done = alpha === 0
  219. return next(state, {
  220. alpha: alpha,
  221. reveal: done ? "hidden" : "concealing",
  222. promise: done ? "none" : promiseFor(Object.assign({}, state, { alpha: alpha })),
  223. phase: done ? "global" : "returning",
  224. intent: done ? "idle" : "hide",
  225. notice: done ? "精细层已收起,可以再撤雾。现在回到全球。" : "精细层再收起一档。雾先留着。"
  226. })
  227. }
  228. case "dropFog":
  229. if (state.alpha > 0) {
  230. return next(state, {
  231. fogStrength: 0,
  232. protection: false,
  233. promise: "degraded",
  234. edgeExposed: true,
  235. notice: "精细层还在时雾被撤掉,矩形边缘可能露出。这不是合格的返回顺序。"
  236. })
  237. }
  238. return next(state, { fogStrength: 0, protection: false, fogReady: state.fogReady, notice: "精细层已经看不见,撤雾不会露出矩形边缘。" })
  239. case "levelSwap":
  240. return next(state, {
  241. internalSwap: true,
  242. notice: state.alpha > 0 ? "矩形内部换了一级。整层透明度不能只淡新到的那一级。" : "换级发生时精细层不可见,画面上还没有跳变。"
  243. })
  244. default:
  245. return refuse(state, "没有这个操作。")
  246. }
  247. }
  248. return { createState: createState, reduce: reduce }
  249. })()
  250. const PHASE = { global: "全球", entering: "正在进入", locked: "已锁定主场景", returning: "正在返回" }
  251. const REVEAL = { hidden: "隐藏", revealing: "正在亮起", shown: "已显示", concealing: "正在收起" }
  252. const PREFETCH = { idle: "未请求", fetching: "预取中,仍不可见", ready: "目标视野可显示", failed: "加载失败" }
  253. const PROMISE = { none: "无", covered: "有保护", degraded: "已降级,不承诺遮边" }
  254. const FIELDS = [
  255. ["phase", "阶段", (s) => PHASE[s.phase]],
  256. ["intent", "当前目标", (s) => ({ idle: "闲置", show: "亮起精细层", hide: "收起精细层" }[s.intent])],
  257. ["generation", "只认最新操作", (s) => "第 " + s.generation + " 次"],
  258. ["fogSupported", "雾是否可用", (s) => s.fogSupported ? "可用" : "已关闭"],
  259. ["fogReady", "雾是否可执行", (s) => s.fogReady ? "可执行" : "还不能执行"],
  260. ["fogStrength", "雾强度", (s) => Math.round(s.fogStrength * 100) + "%"],
  261. ["protection", "保护是否成立", (s) => s.protection ? "成立" : "未成立"],
  262. ["alpha", "精细层透明度", (s) => Math.round(s.alpha * 100) + "%"],
  263. ["reveal", "精细层显示", (s) => REVEAL[s.reveal]],
  264. ["prefetch", "瓦片", (s) => PREFETCH[s.prefetch]],
  265. ["promise", "遮缝承诺", (s) => PROMISE[s.promise]],
  266. ["edgeExposed", "边缘是否可能已露出", (s) => s.edgeExposed ? "是" : "否"],
  267. ["internalSwap", "内部换级", (s) => s.internalSwap ? "发生过,整层透明度治不了" : "还没有"]
  268. ]
  269. const FREE = [
  270. ["雾", [
  271. ["准备雾", { type: "prepareFog" }],
  272. ["把雾强度拉满", { type: "raiseFog" }],
  273. ["标记保护已成立", { type: "markProtection" }],
  274. ["标记保护消失", { type: "loseProtection" }],
  275. ["雾运行失败", { type: "fogFailed" }],
  276. ["雾恢复可准备", { type: "fogRecovered" }],
  277. ["撤掉雾", { type: "dropFog" }]
  278. ]],
  279. ["进出", [
  280. ["点击进入系统", { type: "enter" }],
  281. ["飞到并锁定", { type: "arrive" }],
  282. ["取消进入", { type: "cancel" }],
  283. ["返回全球", { type: "returnGlobal" }]
  284. ]],
  285. ["影像", [
  286. ["目标视野可显示", { type: "tilesReady" }],
  287. ["瓦片加载失败", { type: "tilesFailed" }],
  288. ["精细层再亮一档", { type: "revealStep" }],
  289. ["精细层再暗一档", { type: "concealStep" }],
  290. ["矩形内部换了一级", { type: "levelSwap" }]
  291. ]]
  292. ]
  293. const SCENARIOS = [
  294. { id: "forward", title: "正向进入", blurb: "看精细层会不会在保护成立前亮起。强度拉满之后仍应不能亮。", steps: [
  295. ["点击进入系统", { type: "enter" }], ["准备雾", { type: "prepareFog" }], ["把雾强度拉满", { type: "raiseFog" }],
  296. ["目标视野可显示", { type: "tilesReady" }], ["精细层再亮一档", { type: "revealStep" }], ["标记保护已成立", { type: "markProtection" }],
  297. ["精细层再亮一档", { type: "revealStep" }], ["精细层再亮一档", { type: "revealStep" }], ["精细层再亮一档", { type: "revealStep" }],
  298. ["精细层再亮一档", { type: "revealStep" }], ["飞到并锁定", { type: "arrive" }]
  299. ]},
  300. { id: "strength", title: "只有强度", blurb: "复现「雾看起来满了,但还没挡住边缘」。这一下变亮必须被拒绝。", steps: [
  301. ["点击进入系统", { type: "enter" }], ["准备雾", { type: "prepareFog" }], ["把雾强度拉满", { type: "raiseFog" }],
  302. ["目标视野可显示", { type: "tilesReady" }], ["精细层再亮一档", { type: "revealStep" }]
  303. ]},
  304. { id: "back", title: "返回先收影像", blurb: "先把精细层收到看不见,再撤雾。走完后边缘应仍是「否」。", steps: [
  305. ["点击进入系统", { type: "enter" }], ["准备雾", { type: "prepareFog" }], ["把雾强度拉满", { type: "raiseFog" }],
  306. ["标记保护已成立", { type: "markProtection" }], ["目标视野可显示", { type: "tilesReady" }],
  307. ["精细层再亮一档", { type: "revealStep" }], ["精细层再亮一档", { type: "revealStep" }], ["精细层再亮一档", { type: "revealStep" }], ["精细层再亮一档", { type: "revealStep" }],
  308. ["返回全球", { type: "returnGlobal" }],
  309. ["精细层再暗一档", { type: "concealStep" }], ["精细层再暗一档", { type: "concealStep" }], ["精细层再暗一档", { type: "concealStep" }], ["精细层再暗一档", { type: "concealStep" }],
  310. ["撤掉雾", { type: "dropFog" }]
  311. ]},
  312. { id: "earlyFog", title: "返回时先撤雾", blurb: "不合格顺序:精细层还在就撤雾。边缘应变成「是」。", steps: [
  313. ["点击进入系统", { type: "enter" }], ["准备雾", { type: "prepareFog" }], ["把雾强度拉满", { type: "raiseFog" }],
  314. ["标记保护已成立", { type: "markProtection" }], ["目标视野可显示", { type: "tilesReady" }],
  315. ["精细层再亮一档", { type: "revealStep" }], ["精细层再亮一档", { type: "revealStep" }], ["精细层再亮一档", { type: "revealStep" }], ["精细层再亮一档", { type: "revealStep" }],
  316. ["返回全球", { type: "returnGlobal" }], ["撤掉雾", { type: "dropFog" }]
  317. ]},
  318. { id: "cancel", title: "取消正在变亮", blurb: "亮到一半改取消。下一档变亮应失效,接着只应能收起。", steps: [
  319. ["点击进入系统", { type: "enter" }], ["准备雾", { type: "prepareFog" }], ["把雾强度拉满", { type: "raiseFog" }],
  320. ["标记保护已成立", { type: "markProtection" }], ["目标视野可显示", { type: "tilesReady" }],
  321. ["精细层再亮一档", { type: "revealStep" }], ["精细层再亮一档", { type: "revealStep" }],
  322. ["取消进入", { type: "cancel" }], ["精细层再亮一档", { type: "revealStep" }], ["精细层再暗一档", { type: "concealStep" }]
  323. ]},
  324. { id: "reenter", title: "返回途中再进入", blurb: "收起还没完成就再次进入。过期的变暗应失效,精细层停在当前透明度。", steps: [
  325. ["点击进入系统", { type: "enter" }], ["准备雾", { type: "prepareFog" }], ["把雾强度拉满", { type: "raiseFog" }],
  326. ["标记保护已成立", { type: "markProtection" }], ["目标视野可显示", { type: "tilesReady" }],
  327. ["精细层再亮一档", { type: "revealStep" }], ["精细层再亮一档", { type: "revealStep" }], ["精细层再亮一档", { type: "revealStep" }], ["精细层再亮一档", { type: "revealStep" }],
  328. ["返回全球", { type: "returnGlobal" }], ["精细层再暗一档", { type: "concealStep" }], ["精细层再暗一档", { type: "concealStep" }],
  329. ["点击进入系统", { type: "enter" }], ["精细层再暗一档", { type: "concealStep" }], ["精细层再亮一档", { type: "revealStep" }]
  330. ]},
  331. { id: "nofog", title: "雾失败仍要能看", blurb: "业务可用优先:仍渐显,但承诺改成不保证遮边,并记下边缘可能露出。", steps: [
  332. ["点击进入系统", { type: "enter" }], ["雾运行失败", { type: "fogFailed" }], ["目标视野可显示", { type: "tilesReady" }],
  333. ["精细层再亮一档", { type: "revealStep" }]
  334. ]},
  335. { id: "tiles", title: "瓦片失败", blurb: "保护已经成立,但目标视野没有影像。应继续隐藏,不要亮出空白再跳变。", steps: [
  336. ["点击进入系统", { type: "enter" }], ["准备雾", { type: "prepareFog" }], ["把雾强度拉满", { type: "raiseFog" }],
  337. ["标记保护已成立", { type: "markProtection" }], ["瓦片加载失败", { type: "tilesFailed" }], ["精细层再亮一档", { type: "revealStep" }]
  338. ]},
  339. { id: "swap", title: "内部换级", blurb: "精细层完全可见后,地形又换了一级影像。确认整层透明度没有把它淡掉。", steps: [
  340. ["点击进入系统", { type: "enter" }], ["准备雾", { type: "prepareFog" }], ["把雾强度拉满", { type: "raiseFog" }],
  341. ["标记保护已成立", { type: "markProtection" }], ["目标视野可显示", { type: "tilesReady" }],
  342. ["精细层再亮一档", { type: "revealStep" }], ["精细层再亮一档", { type: "revealStep" }], ["精细层再亮一档", { type: "revealStep" }], ["精细层再亮一档", { type: "revealStep" }],
  343. ["矩形内部换了一级", { type: "levelSwap" }]
  344. ]}
  345. ]
  346. let state = Transition.createState()
  347. let previous = null
  348. let activeScenario = null
  349. let stepIndex = 0
  350. function dispatch(action) {
  351. previous = state
  352. state = Transition.reduce(state, action)
  353. render()
  354. }
  355. function render() {
  356. const root = document.querySelector("#state")
  357. root.replaceChildren()
  358. const grid = document.createElement("div")
  359. grid.className = "grid"
  360. FIELDS.forEach(([key, label, format]) => {
  361. const card = document.createElement("div")
  362. card.className = "field" + (previous && previous[key] !== state[key] ? " changed" : "")
  363. const name = document.createElement("b")
  364. name.textContent = label
  365. card.append(name, document.createTextNode(format(state)))
  366. grid.append(card)
  367. })
  368. const notice = document.createElement("p")
  369. notice.textContent = state.notice
  370. root.append(grid, notice)
  371. const changed = previous ? FIELDS.filter(([key]) => previous[key] !== state[key]).map(([, label]) => label) : []
  372. document.querySelector("#delta").textContent = changed.length ? "这一下变了:" + changed.join("、") : "还没有操作。"
  373. const refusal = document.querySelector("#refusal")
  374. refusal.textContent = state.refusal ? "这一下被拒绝:" + state.refusal : ""
  375. refusal.className = state.refusal ? "bad" : ""
  376. renderWalk()
  377. }
  378. function renderFree() {
  379. const root = document.querySelector("#free")
  380. FREE.forEach(([title, actions]) => {
  381. const row = document.createElement("div")
  382. row.className = "group"
  383. const label = document.createElement("span")
  384. label.textContent = title
  385. row.append(label)
  386. actions.forEach(([text, action]) => {
  387. const button = document.createElement("button")
  388. button.type = "button"
  389. button.textContent = text
  390. button.addEventListener("click", () => { activeScenario = null; dispatch(action) })
  391. row.append(button)
  392. })
  393. root.append(row)
  394. })
  395. const reset = document.createElement("button")
  396. reset.type = "button"
  397. reset.textContent = "回到初始全球"
  398. reset.addEventListener("click", () => { activeScenario = null; previous = state; state = Transition.createState(); render() })
  399. root.append(reset)
  400. }
  401. function renderWalk() {
  402. const tabs = document.querySelector("#tabs")
  403. tabs.replaceChildren()
  404. SCENARIOS.forEach((scenario) => {
  405. const button = document.createElement("button")
  406. button.type = "button"
  407. button.textContent = scenario.title
  408. button.setAttribute("aria-selected", String(activeScenario === scenario.id))
  409. button.addEventListener("click", () => startScenario(scenario.id))
  410. tabs.append(button)
  411. })
  412. const scenario = SCENARIOS.find((item) => item.id === activeScenario)
  413. document.querySelector("#blurb").textContent = scenario ? scenario.blurb : "选一个走查会先回到全球初始,再按顺序点。"
  414. const steps = document.querySelector("#steps")
  415. steps.replaceChildren()
  416. if (!scenario) return
  417. scenario.steps.forEach(([label], index) => {
  418. const button = document.createElement("button")
  419. button.type = "button"
  420. button.textContent = (index + 1) + ". " + label
  421. button.disabled = index !== stepIndex
  422. if (index === stepIndex) button.className = "primary"
  423. button.addEventListener("click", () => {
  424. dispatch(scenario.steps[index][1])
  425. stepIndex += 1
  426. render()
  427. })
  428. steps.append(button)
  429. })
  430. if (stepIndex >= scenario.steps.length) {
  431. const done = document.createElement("p")
  432. done.className = "ok"
  433. done.textContent = "这一条走完了。拒绝也是结果:看上面有没有「被拒绝」,以及边缘是否可能已露出。"
  434. steps.append(done)
  435. }
  436. }
  437. function startScenario(id) {
  438. activeScenario = id
  439. stepIndex = 0
  440. previous = null
  441. state = Transition.createState()
  442. render()
  443. }
  444. renderFree()
  445. render()
  446. </script>
  447. </body>
  448. </html>