57 lines
8.8 KiB
HTML
57 lines
8.8 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>数字一笔 · 明亮教具室试玩</title>
|
||
<style>
|
||
@font-face{font-family:NumberSwipeSans500;src:url(../fonts/NumberSwipeSans500.ttf)}
|
||
@font-face{font-family:NumberSwipeSans600;src:url(../fonts/NumberSwipeSans600.ttf)}
|
||
@font-face{font-family:NumberSwipeSans700;src:url(../fonts/NumberSwipeSans700.ttf)}
|
||
@font-face{font-family:NumberSwipeDigits800;src:url(../fonts/NumberSwipeDigits800.ttf)}
|
||
@font-face{font-family:NumberSwipePlayful;src:url(../fonts/NumberSwipePlayful.ttf)}
|
||
*{box-sizing:border-box}body{margin:0;background:#fff6df;color:#43382f;font:15px NumberSwipeSans500,sans-serif}header{padding:28px 32px 20px;border-bottom:1px solid #e5d7ba}h1{margin:0 0 8px;font:25px NumberSwipeSans700}p{margin:8px 0;line-height:1.7;color:#71604f}main{display:flex;gap:32px;padding:28px 32px;align-items:flex-start}.tools{width:270px;position:sticky;top:24px}.tools h2{font-size:17px;margin:24px 0 10px}.tools button,select{font:14px NumberSwipeSans600;cursor:pointer;border:1px solid #d8c4a0;background:#fffbf2;color:#43382f;border-radius:12px;padding:12px;margin:0 6px 8px 0}.tools button:hover{border-color:#6552d9;color:#5140b7}.tools select{width:100%}.phone{border:1px solid #d8c4a0;border-radius:24px;overflow:hidden;box-shadow:0 12px 34px #43382f14;background:#ffe69a}canvas{display:block;touch-action:none}.note{font-size:13px}#status{background:#ece7ff;color:#5140b7;border-radius:12px;padding:12px;margin-top:16px}a{color:#5140b7}@media(max-width:750px){main{padding:16px;flex-direction:column}.tools{width:100%;position:static}.phone{align-self:center}header{padding:20px}}
|
||
</style>
|
||
<header><h1>数字一笔 · 明亮教具室</h1><p>200关实际游戏包的 Canvas 预览。全部画面由程序绘制,字体随包提供。</p></header>
|
||
<main><aside class="tools"><label for="size">屏幕尺寸</label><select id="size"><option>375×812</option><option>320×568</option><option>375×667</option><option>390×844</option><option>414×896</option></select><h2>真实关卡样张</h2><div id="samples"></div><h2>指定关卡</h2><select id="level"></select><button id="partial">画前几步</button><button id="hint">显示指路</button><button id="clear">完成本关</button><button id="select">返回选关</button><p class="note">在右侧直接点击或滑动。减少动态与音效开关在游戏选关页的“设置”中。辅助演示按钮仅属于此验收页。</p><div id="status">加载字体与游戏包…</div><p class="note" id="audio-status" role="status">音效:点击游戏后播放</p><p class="note">这是浏览器预览,微信手机上的字体、触摸与性能仍需真机验收。</p><p><a href="hints.html" target="_blank">运行200关提示像素检查</a></p></aside><div class="phone" id="phone"></div></main>
|
||
<script type="module">
|
||
import {solve} from '../src/upstream/rules.mjs';
|
||
import {browserAudio} from './browser-audio.mjs';
|
||
await Promise.all(['NumberSwipeSans500','NumberSwipeSans600','NumberSwipeSans700','NumberSwipeDigits800','NumberSwipePlayful'].map(f=>document.fonts.load('16px '+f)));
|
||
const source=await(await fetch('../game.js')).text(),pack=await(await fetch('../src/upstream/levels.json')).json();
|
||
const levels=pack.playOrder.map(id=>pack.levels.find(l=>l.id===id));
|
||
const size=document.querySelector('#size'),levelSelect=document.querySelector('#level'),status=document.querySelector('#status'),phone=document.querySelector('#phone');
|
||
let runtime;
|
||
for(const [i,l] of levels.entries()){const o=document.createElement('option');o.value=i;o.textContent=`第 ${i+1} 关 · ${l.title}`;levelSelect.append(o);}
|
||
const samples=[['基础大块',3],['异形拼块',levels.findIndex(l=>l.cells.some(c=>c.shape)&&l.cells.length>=12)],['通道与数字',levels.findIndex(l=>l.features.includes('direction')&&l.cells.some(c=>c.clue&&c.direction))],['双色与奇偶',levels.findIndex(l=>l.features.includes('alternation')&&l.features.includes('parity'))],['节奏门',levels.findIndex(l=>l.doors?.length)],['六边墙',levels.findIndex(l=>l.geometry==='hex'&&l.walls?.length)],['密集36块',levels.findIndex(l=>l.id==='r2-163')]];
|
||
for(const [name,index] of samples){const b=document.createElement('button');b.textContent=name;b.onclick=()=>show(index);document.querySelector('#samples').append(b);}
|
||
function boot(){
|
||
const [width,height]=size.value.split('×').map(Number),handlers={},frames=new Map(),canvases=[],store=new Map();let frame=0,now=100000;
|
||
const timers=new Set(),sound=browserAudio(message=>{document.querySelector('#audio-status').textContent=message;});
|
||
const later=(callback,delay)=>{const timer=setTimeout(()=>{timers.delete(timer);callback();},delay);timers.add(timer);return timer;};
|
||
const cancel=timer=>{clearTimeout(timer);timers.delete(timer);};
|
||
class Clock extends Date{static now(){return now;}}
|
||
const wx={createCanvas(){const c=document.createElement('canvas');if(!canvases.length)canvases.push(c);return c;},loadFont:p=>p.split('/').at(-1).replace('.ttf',''),getWindowInfo:()=>({windowWidth:width,windowHeight:height,pixelRatio:2,safeArea:{top:24,bottom:height-24}}),getMenuButtonBoundingClientRect:()=>({bottom:42}),getPerformance:()=>({now:()=>now}),getStorageSync:k=>store.get(k)||'',setStorageSync:(k,v)=>store.set(k,v),removeStorageSync:k=>store.delete(k),setInnerAudioOption:sound.setInnerAudioOption,createInnerAudioContext:sound.createInnerAudioContext,showModal(){},env:{USER_DATA_PATH:'/preview'}};
|
||
for(const n of ['TouchStart','TouchMove','TouchEnd','TouchCancel','Hide','Show','WindowResize'])wx['on'+n]=cb=>handlers[n]=cb;
|
||
const global={};new Function('wx','GameGlobal','requestAnimationFrame','cancelAnimationFrame','setTimeout','clearTimeout','Date',source)(wx,global,fn=>{frames.set(++frame,fn);return frame;},id=>frames.delete(id),later,cancel,Clock);
|
||
const api=global.numberSwipeR2,canvas=canvases[0];canvas.style.width=width+'px';canvas.style.height=height+'px';phone.style.width=width+'px';phone.replaceChildren(canvas);
|
||
function tick(ms=16){now+=ms;const entry=frames.entries().next().value;if(entry){frames.delete(entry[0]);entry[1]();}}
|
||
function touch(kind,x,y){handlers[kind]({changedTouches:[{identifier:1,clientX:x,clientY:y}],touches:[{identifier:1,clientX:x,clientY:y}]});}
|
||
function tap(x,y){touch('TouchStart',x,y);touch('TouchEnd',x,y);tick();}
|
||
function button(name){const b=api.buttons().find(b=>b.label===name&&b.enabled);if(!b)throw Error('缺少按钮 '+name);tap(b.x+b.w/2,b.y+b.h/2);}
|
||
function cell(id){const s=api.snapshot(),p=s.centers[id];tap(p[0],p[1]+s.boardRect.y);}
|
||
function select(index){if(api.buttons().some(b=>b.label==='返回游戏'))button('返回游戏');if(api.snapshot().screen!=='select')button('选关');let n=0;while(!api.buttons().some(b=>b.label===String(index+1)||b.label===`${index+1} ✓`)){button(api.snapshot().selection.chapter>Math.floor(index/10)+1?'上一页':'下一页');if(++n>30)throw Error('分页失败');}button(api.buttons().some(b=>b.label===String(index+1))?String(index+1):`${index+1} ✓`);if(api.buttons().some(b=>b.label==='返回游戏'))button('返回游戏');tick(300);}
|
||
let down=false;
|
||
const coords=e=>{const r=canvas.getBoundingClientRect();return [(e.clientX-r.left)*width/r.width,(e.clientY-r.top)*height/r.height];};
|
||
canvas.onpointerdown=e=>{down=true;canvas.setPointerCapture(e.pointerId);touch('TouchStart',...coords(e));tick();};
|
||
canvas.onpointermove=e=>{if(down){touch('TouchMove',...coords(e));tick();}};
|
||
canvas.onpointerup=e=>{down=false;touch('TouchEnd',...coords(e));tick();};canvas.onpointercancel=e=>{down=false;touch('TouchCancel',...coords(e));tick();};
|
||
tick();return {tick,api,button,cell,select,dispose(){sound.dispose();for(const timer of timers)clearTimeout(timer);timers.clear();}};
|
||
}
|
||
function show(index){runtime?.dispose();runtime=boot();runtime.select(index);levelSelect.value=index;update();}
|
||
function update(){const s=runtime.api.snapshot();status.textContent=`第 ${s.displayOrder} 关 · ${s.path.length} / ${levels.find(l=>l.id===s.level).cells.length} 步 · ${s.theme}`;}
|
||
levelSelect.onchange=()=>show(Number(levelSelect.value));size.onchange=()=>show(Number(levelSelect.value));
|
||
document.querySelector('#partial').onclick=()=>{const l=levels.find(l=>l.id===runtime.api.snapshot().level);runtime.button('重开');for(const id of solve(l)[0].slice(0,Math.min(5,l.cells.length-1)))runtime.cell(id);runtime.tick(300);update();};
|
||
document.querySelector('#hint').onclick=()=>{runtime.button('指路');update();};
|
||
document.querySelector('#clear').onclick=()=>{const l=levels.find(l=>l.id===runtime.api.snapshot().level);runtime.button('重开');for(const id of solve(l)[0])runtime.cell(id);runtime.tick(600);update();};
|
||
document.querySelector('#select').onclick=()=>{runtime.button('选关');update();};
|
||
show(3);function pump(){runtime.tick();requestAnimationFrame(pump);}requestAnimationFrame(pump);
|
||
</script></html>
|