更新技术美术明亮教具室版:字体、指路、教学与庆祝效果
This commit is contained in:
@@ -1,39 +1,60 @@
|
||||
import {CLEAR_DURATION,clearProgress,drawClearCelebration} from './bright-classroom/celebration.mjs';
|
||||
import {stopAudio,audioDiagnostics} from './audio.mjs';
|
||||
import { mainCanvas, boardCanvas, document, window, PACK } from './platform.mjs';
|
||||
import {renderLesson,hintView} from './upstream/ui.mjs';
|
||||
import { mainCanvas, boardCanvas, document, window, PACK, setViewInvalidator } from './platform.mjs';
|
||||
import {renderLesson,hintView,refreshBrightBoard,brightOverlay,brightAnimating} from './upstream/ui.mjs';
|
||||
import {drawHint} from './hints.mjs';
|
||||
import {lessonKeys,makeLesson,lessonFrame} from './lessons.mjs';
|
||||
import {lessonKeys,makeLesson,lessonFrame,loopLessonTime} from './lessons.mjs';
|
||||
import {tutorials} from './copy.mjs';
|
||||
import {C,textFont,label,panel,fontDiagnostics} from './bright-classroom/theme.mjs';
|
||||
import {background,buttonArt,stage,statusPill} from './bright-classroom/ui.mjs';
|
||||
import {preferences} from './bright-classroom/board-renderer.mjs';
|
||||
const lessonCanvas=wx.createCanvas();let lesson=null,lessonStarted=0,lessonState=null,lessonCache=new Map();
|
||||
const $ = id => document.getElementById(id), ctx = mainCanvas.getContext('2d');
|
||||
let width = 375, height = 812, top = 60, bottom = 24, page = 0, buttons = [], pointer = null, visible = true, raf = 0;
|
||||
let rulePage=0,ruleKey='';
|
||||
const hintCanvas=wx.createCanvas(),hintContext=hintCanvas.getContext('2d');
|
||||
let hintAnimation={key:'',started:0};
|
||||
let settingsOpen=false,pressedLabel='',pressedUntil=0;
|
||||
let renderedFrames=0,clearCelebration=null;
|
||||
function requestPaint(){if(visible&&!raf)raf=requestAnimationFrame(draw);}
|
||||
setViewInvalidator(requestPaint);
|
||||
try{preferences.reducedMotion=wx.getStorageSync('number-swipe:reduced-motion')==='1';}catch{}
|
||||
const ordered = PACK.playOrder.map(id => PACK.levels.find(l => l.id === id));
|
||||
function resize() { const info = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync(); width = info.windowWidth; height = info.windowHeight; top = Math.max(info.safeArea?.top || 0, wx.getMenuButtonBoundingClientRect?.().bottom || 0) + 12; bottom = Math.max(16, height - (info.safeArea?.bottom || height)); window.devicePixelRatio = Math.min(info.pixelRatio || 1, 3); mainCanvas.width = width * window.devicePixelRatio; mainCanvas.height = height * window.devicePixelRatio; $('board').clientWidth = width; $('board').clientHeight = Math.max(140, height - top - bottom - 218); window.handlers.resize?.(); }
|
||||
function text(value, x, y, size = 16, color = '#23243A') { ctx.fillStyle = color; ctx.font = size + 'px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(String(value), x, y); }
|
||||
function splitLines(value,maxWidth,size=16){ctx.font=size+'px sans-serif';const lines=[];for(const paragraph of String(value).split('\n')){let line='';for(const c of paragraph){if(line&&ctx.measureText(line+c).width>maxWidth){lines.push(line);line='';}line+=c;}lines.push(line);}return lines;}
|
||||
function resize() { const info = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync(); width = info.windowWidth; height = info.windowHeight; top = Math.max(info.safeArea?.top || 0, wx.getMenuButtonBoundingClientRect?.().bottom || 0) + 12; bottom = Math.max(16, height - (info.safeArea?.bottom || height)); window.devicePixelRatio = Math.min(info.pixelRatio || 1, 3); mainCanvas.width = width * window.devicePixelRatio; mainCanvas.height = height * window.devicePixelRatio; $('board').clientWidth = width; $('board').clientHeight = Math.max(140, height - top - bottom - 218); window.handlers.resize?.(); requestPaint(); }
|
||||
function text(value, x, y, size = 16, color = C.ink) { label(ctx,value,x,y,size,color); }
|
||||
function splitLines(value,maxWidth,size=16){textFont(ctx,size);const lines=[];for(const paragraph of String(value).split('\n')){let line='';for(const c of paragraph){if(line&&ctx.measureText(line+c).width>maxWidth){lines.push(line);line='';}line+=c;}lines.push(line);}return lines;}
|
||||
function wrapped(value,x,y,maxWidth,size=16){const lines=splitLines(value,maxWidth,size);lines.forEach((line,i)=>text(line,x,y+i*(size+9),size));return lines.length;}
|
||||
function button(label, x, y, w, h, run, { enabled = true, primary = false } = {}) { ctx.fillStyle = enabled ? (primary ? '#6558F5' : '#ECECF3') : '#F4F4F6'; ctx.fillRect(x, y, w, h); text(label, x + w / 2, y + h / 2, 15, enabled ? (primary ? '#FFFFFF' : '#23243A') : '#AAAAAA'); buttons.push({ label, x, y, w, h, run, enabled }); }
|
||||
function button(label, x, y, w, h, run, { enabled = true, primary = false } = {}) { buttonArt(ctx,label,x,y,w,h,{enabled,primary,pressed:!preferences.reducedMotion&&pressedLabel===label&&Date.now()<pressedUntil}); buttons.push({ label, x, y, w, h, run, enabled }); }
|
||||
function click(id) { if(id==='btn-rules')ruleKey=''; $('board').handlers.pointerup?.(); pointer = null; $(id).click(); }
|
||||
function levelButtons() { return $('pack-list').children.flatMap(section => section.children[1].children); }
|
||||
function draw() {
|
||||
raf=0;
|
||||
if (!visible)
|
||||
return;
|
||||
renderedFrames++;
|
||||
const d = window.devicePixelRatio;
|
||||
ctx.setTransform(d, 0, 0, d, 0, 0);
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
const state=window.variantSnapshot();
|
||||
if(!state.cleared||state.screen!=='play')clearCelebration=null;
|
||||
else if(!clearCelebration||clearCelebration.key!==state.level)clearCelebration={key:state.level,started:Date.now(),requested:audioDiagnostics.completeRequestedAt};
|
||||
if(clearCelebration&&state.soundEnabled&&audioDiagnostics.completeRequestedAt===clearCelebration.requested){
|
||||
const actual=audioDiagnostics.completeStartedAt;
|
||||
clearCelebration.started=actual??(Date.now()-clearCelebration.requested<500?null:clearCelebration.requested);
|
||||
}
|
||||
const currentLevel=ordered.find(l=>l.id===state.level);
|
||||
const dense=(currentLevel.features||[]).length>=3||state.cellViews?.length>=30;
|
||||
background(ctx,width,height,!$('screen-play').className.includes('hidden')&&dense);
|
||||
buttons = [];
|
||||
const select = $('screen-play').className.includes('hidden');
|
||||
if (select) {
|
||||
text('数字一笔', width / 2, top + 24, 30);
|
||||
text('200关 · 评分修订版', width / 2, top + 61, 16);
|
||||
text($('progress').textContent, width / 2, top + 90, 14);
|
||||
text('数字一笔', width / 2, top + 24, 30,C.primary);
|
||||
text('每块一次 · 数字对步数', width / 2, top + 61, 16);
|
||||
text('已完成 '+$('progress').textContent, width / 2, top + 90, 14,C.inkSecondary);
|
||||
button('设置',width-72,top+4,56,34,()=>settingsOpen=true);
|
||||
const rows = Math.max(1, Math.floor((height - top - bottom - 242) / 57)), count = rows * 4, pages = Math.ceil(200 / count);
|
||||
page = Math.max(0, Math.min(pages - 1, page));
|
||||
const levels = levelButtons();
|
||||
panel(ctx,12,top+110,width-24,rows*57+4,24,C.stage,3);
|
||||
for (let j = 0; j < count && page * count + j < levels.length; j++) {
|
||||
const index = page * count + j, node = levels[index];
|
||||
button(node.textContent + (node.className.includes('cleared') ? ' ✓' : ''), 18 + j % 4 * (width - 30) / 4, top + 120 + Math.floor(j / 4) * 57, (width - 48) / 4 - 4, 45, () => node.click(), { primary: node.className.includes('current') });
|
||||
@@ -46,9 +67,12 @@ function draw() {
|
||||
}
|
||||
else {
|
||||
button('选关', 16, top, 62, 36, () => click('btn-back'));
|
||||
text('第 ' + window.variantSnapshot().displayOrder + ' 关', width / 2, top + 18, 20);
|
||||
panel(ctx,width/2-60,top+2,120,32,16,C.coral,3);
|
||||
text('第 ' + window.variantSnapshot().displayOrder + ' 关', width / 2, top + 18, 18,C.onPrimary);
|
||||
button($('btn-sound').textContent, width - 94, top, 78, 36, () => click('btn-sound'));
|
||||
text($('level-sub').textContent, width / 2, top + 59, 14);
|
||||
statusPill(ctx,84,top+43,width-100,31,state.path.length,currentLevel.cells.length-state.path.length);
|
||||
stage(ctx,width,top+82,$('board').clientHeight,dense);
|
||||
refreshBrightBoard(Date.now());
|
||||
ctx.drawImage(boardCanvas, 0, top + 82, width, $('board').clientHeight);
|
||||
const hint=hintView(),boardState=window.variantSnapshot();
|
||||
if(hint){
|
||||
@@ -60,29 +84,30 @@ function draw() {
|
||||
hintContext.setTransform(d,0,0,d,0,0);hintContext.clearRect(0,0,width,h);
|
||||
drawHint(hintContext,hint,boardState.centers,boardState.unit,Date.now()-hintAnimation.started,boardState.path);
|
||||
ctx.drawImage(hintCanvas,0,top+82,width,h);
|
||||
ctx.save();ctx.translate(0,top+82);brightOverlay(ctx);ctx.restore();
|
||||
}else hintAnimation={key:'',started:0};
|
||||
const foot = height - bottom - 102;
|
||||
const cleared=window.variantSnapshot().cleared;
|
||||
const message=cleared?(window.variantSnapshot().displayOrder===ordered.length?'200 关全部完成!':'完成!准备好就进入下一关。'):($('status').className.includes('show')?$('status').textContent:'按住滑动连线 · 沿来时路逐格回退');
|
||||
const message=cleared?(window.variantSnapshot().displayOrder===ordered.length?'200 关全部完成!':'完成!准备好就进入下一关。'):($('status').className.includes('show')&&$('status').textContent?$('status').textContent:'按住滑动连线 · 沿来时路逐格回退');
|
||||
const messageLines=splitLines(message,width-36,13);
|
||||
messageLines.slice(0,2).forEach((line,i)=>text(i===1&&messageLines.length>2?line.slice(0,-1)+'…':line,width/2,foot-12+i*22,13,cleared?'#6558F5':'#777789'));
|
||||
messageLines.slice(0,2).forEach((line,i)=>text(i===1&&messageLines.length>2?line.slice(0,-1)+'…':line,width/2,foot-12+i*22,13,cleared?C.success:C.inkSecondary));
|
||||
const actionHeight=height<700?54:70;
|
||||
if(cleared){
|
||||
button(!$('next').hidden?'下一关 →':'返回选关',16,foot+22,width-32,44,()=>click(!$('next').hidden?'next':'btn-back'),{primary:true});
|
||||
button(!$('next').hidden?'下一关 →':'返回选关',16,foot+22,width-32,actionHeight,()=>click(!$('next').hidden?'next':'btn-back'),{primary:true});
|
||||
}else{
|
||||
const bw=(width-48)/3;
|
||||
button('重开',16,foot+22,bw,44,()=>click('restart'));
|
||||
button('撤销',24+bw,foot+22,bw,44,()=>click('undo'),{enabled:!$('undo').disabled});
|
||||
button('指路',32+2*bw,foot+22,bw,44,()=>click('hint'));
|
||||
button('重开',16,foot+22,bw,actionHeight,()=>click('restart'));
|
||||
button('撤销',24+bw,foot+22,bw,actionHeight,()=>click('undo'),{enabled:!$('undo').disabled});
|
||||
button('指路',32+2*bw,foot+22,bw,actionHeight,()=>click('hint'),{primary:true});
|
||||
}
|
||||
button('规则', 16, top + 43, 56, 30, () => click('btn-rules'));
|
||||
text('剩余 ' + $('remaining-n').textContent, width - 52, top + 58, 14);
|
||||
}
|
||||
if(clearCelebration&&clearCelebration.started!==null&&!preferences.reducedMotion&&!settingsOpen&&$('rules-sheet').hidden)drawClearCelebration(ctx,width,top+82,$('board').clientHeight,Date.now()-clearCelebration.started);
|
||||
if (!$('rules-sheet').hidden) {
|
||||
ctx.fillStyle = 'rgba(25,24,44,.55)';
|
||||
ctx.fillStyle = 'rgba(67,56,47,.32)';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
const panelTop=top+12,panelHeight=Math.max(240,height-panelTop-bottom-20);
|
||||
ctx.fillRect(16,panelTop,width-32,panelHeight);
|
||||
panel(ctx,16,panelTop,width-32,panelHeight,24,C.surface,5);
|
||||
buttons=[];
|
||||
text('怎么玩',width/2,panelTop+28,23);
|
||||
const key=window.variantSnapshot().level+':'+$('rule-text').textContent;
|
||||
@@ -94,20 +119,31 @@ function draw() {
|
||||
rulePage=Math.max(0,Math.min(pages.length-1,rulePage));const part=pages[rulePage];
|
||||
const lessonKey=key+':'+rulePage;
|
||||
if(!lesson||lesson.id!==lessonKey){if(!lessonCache.has(part.key))lessonCache.set(part.key,makeLesson(PACK,part.key));lesson={...lessonCache.get(part.key),id:lessonKey};lessonStarted=Date.now();}
|
||||
lessonState=lessonFrame(lesson,Date.now()-lessonStarted);
|
||||
lessonState=lessonFrame(lesson,preferences.reducedMotion?lesson.duration:loopLessonTime(lesson,Date.now()-lessonStarted));
|
||||
const area={x:32,y:panelTop+49,w:width-64,h:visualHeight};
|
||||
const view=renderLesson(lessonCanvas,lesson.level,lessonState.progress<1?lessonState.previous:lessonState.route,area.w,area.h);
|
||||
ctx.drawImage(lessonCanvas,area.x,area.y,area.w,area.h);
|
||||
if(lessonState.blocked){const [a,b]=lessonState.blocked.map(id=>view.centers[id]);ctx.beginPath();ctx.moveTo(area.x+a[0],area.y+a[1]);ctx.lineTo(area.x+b[0],area.y+b[1]);ctx.setLineDash([5,5]);ctx.strokeStyle='#E5484D';ctx.lineWidth=4;ctx.stroke();ctx.setLineDash([]);text('×',area.x+(a[0]+b[0])/2,area.y+(a[1]+b[1])/2,26,'#E5484D');}
|
||||
if(lessonState.blocked){const [a,b]=lessonState.blocked.map(id=>view.centers[id]);ctx.beginPath();ctx.moveTo(area.x+a[0],area.y+a[1]);ctx.lineTo(area.x+b[0],area.y+b[1]);ctx.setLineDash([5,5]);ctx.strokeStyle=C.danger;ctx.lineWidth=4;ctx.stroke();ctx.setLineDash([]);text('×',area.x+(a[0]+b[0])/2,area.y+(a[1]+b[1])/2,26,C.danger);}
|
||||
const last=lessonState.route.at(-1),prev=lessonState.previous.at(-1),end=view.centers[last],start=view.centers[prev]||end;
|
||||
if(end){const t=lessonState.progress,x=area.x+start[0]+(end[0]-start[0])*t,y=area.y+start[1]+(end[1]-start[1])*t;if(start!==end){ctx.beginPath();ctx.moveTo(area.x+start[0],area.y+start[1]);ctx.lineTo(x,y);ctx.strokeStyle='#6558F5';ctx.lineWidth=6;ctx.lineCap='round';ctx.stroke();}ctx.beginPath();ctx.arc(x,y,10,0,Math.PI*2);ctx.strokeStyle='#F5A524';ctx.lineWidth=3;ctx.stroke();}
|
||||
const msg=splitLines(lessonState.message,width-76,14);msg.forEach((line,i)=>text(line,width/2,area.y+area.h+16+i*20,14,'#6558F5'));
|
||||
if(end){const t=lessonState.progress,x=area.x+start[0]+(end[0]-start[0])*t,y=area.y+start[1]+(end[1]-start[1])*t;if(start!==end){ctx.beginPath();ctx.moveTo(area.x+start[0],area.y+start[1]);ctx.lineTo(x,y);ctx.strokeStyle=C.primary;ctx.lineWidth=6;ctx.lineCap='round';ctx.stroke();}ctx.beginPath();ctx.arc(x,y,10,0,Math.PI*2);ctx.strokeStyle=C.hint;ctx.lineWidth=3;ctx.stroke();}
|
||||
const msg=splitLines(lessonState.message,width-76,14);msg.forEach((line,i)=>text(line,width/2,area.y+area.h+16+i*20,14,C.primary));
|
||||
part.lines.forEach((line,i)=>text(line,width/2,area.y+area.h+65+i*23,15));
|
||||
button('重播',width/2-32,panelTop+panelHeight-144,64,30,()=>{lessonStarted=Date.now();});
|
||||
if(pages.length>1){button('上一段',38,panelTop+panelHeight-102,80,30,()=>{rulePage--;lesson=null;},{enabled:rulePage>0});text((rulePage+1)+' / '+pages.length,width/2,panelTop+panelHeight-87,14);button('下一段',width-118,panelTop+panelHeight-102,80,30,()=>{rulePage++;lesson=null;},{enabled:rulePage<pages.length-1});}
|
||||
button('返回游戏',40,panelTop+panelHeight-60,width-80,42,()=>click('close-rules'),{primary:true});
|
||||
}
|
||||
raf = requestAnimationFrame(draw);
|
||||
if(settingsOpen){
|
||||
ctx.fillStyle='rgba(67,56,47,.32)';ctx.fillRect(0,0,width,height);buttons=[];
|
||||
const y=Math.max(top,height/2-150);panel(ctx,24,y,width-48,280,24,C.surface,5);
|
||||
text('设置',width/2,y+32,24);
|
||||
button($('btn-sound').textContent,44,y+65,width-88,48,()=>click('btn-sound'));
|
||||
button('减少动态:'+(preferences.reducedMotion?'开':'关'),44,y+125,width-88,48,()=>{preferences.reducedMotion=!preferences.reducedMotion;wx.setStorageSync('number-swipe:reduced-motion',preferences.reducedMotion?'1':'0');});
|
||||
button('完成',44,y+205,width-88,48,()=>settingsOpen=false,{primary:true});
|
||||
}
|
||||
const liveBoard=!settingsOpen&&$('rules-sheet').hidden&&state.screen==='play';
|
||||
const liveLesson=!settingsOpen&&!$('rules-sheet').hidden&&!preferences.reducedMotion&&Boolean(lesson);
|
||||
const liveClear=liveBoard&&clearCelebration&&!preferences.reducedMotion&&(clearCelebration.started===null||Date.now()-clearCelebration.started<CLEAR_DURATION);
|
||||
if(!preferences.reducedMotion&&Date.now()<pressedUntil||liveLesson||liveClear||liveBoard&&(brightAnimating()||!preferences.reducedMotion&&Boolean(hintView())))requestPaint();
|
||||
}
|
||||
function feedback() { wx.showModal({ title: '试玩反馈', editable: true, placeholderText: '记录有趣、太难或规则不清楚的关卡', content: $('reasoning').value || '', success: r => { if (r.confirm) {
|
||||
$('reasoning').value = r.content || '';
|
||||
@@ -119,29 +155,31 @@ function event(p) { return { clientX: p.clientX, clientY: p.clientY - top - 82,
|
||||
wx.onTouchStart(e => { if (pointer !== null)
|
||||
return; const p = e.changedTouches[0]; if (!p)
|
||||
return; const b = buttons.find(b => p.clientX >= b.x && p.clientX <= b.x + b.w && p.clientY >= b.y && p.clientY <= b.y + b.h); if (b) {
|
||||
if (b.enabled)
|
||||
if (b.enabled){pressedLabel=b.label;pressedUntil=Date.now()+120;
|
||||
b.run();
|
||||
requestPaint();
|
||||
}
|
||||
return;
|
||||
} if (!$('rules-sheet').hidden || $('screen-play').className.includes('hidden'))
|
||||
} if (settingsOpen || !$('rules-sheet').hidden || $('screen-play').className.includes('hidden'))
|
||||
return; if (p.clientY < top + 82 || p.clientY > top + 82 + $('board').clientHeight)
|
||||
return; pointer = p.identifier; $('board').handlers.pointerdown?.(event(p)); });
|
||||
wx.onTouchMove(e => { const p = e.touches.find(p => p.identifier === pointer); if (p)
|
||||
$('board').handlers.pointermove?.(event(p)); });
|
||||
return; pointer = p.identifier; $('board').handlers.pointerdown?.(event(p)); requestPaint(); });
|
||||
wx.onTouchMove(e => { const p = e.touches.find(p => p.identifier === pointer); if (p){
|
||||
$('board').handlers.pointermove?.(event(p));requestPaint();} });
|
||||
const release = e => { if (e.changedTouches.some(p => p.identifier === pointer)) {
|
||||
$('board').handlers.pointerup?.();
|
||||
pointer = null;
|
||||
} };
|
||||
wx.onTouchEnd(release);
|
||||
wx.onTouchCancel(release);
|
||||
wx.onHide(() => { visible = false; lessonStarted=Date.now(); stopAudio(); cancelAnimationFrame(raf); pointer = null; $('board').handlers.pointercancel?.(); window.handlers.pagehide?.(); });
|
||||
wx.onHide(() => { visible = false; lessonStarted=Date.now(); stopAudio(); cancelAnimationFrame(raf);raf=0; pointer = null; $('board').handlers.pointercancel?.(); window.handlers.pagehide?.(); });
|
||||
wx.onShow(() => { if (!visible) {
|
||||
visible = true;lessonStarted=Date.now();
|
||||
window.handlers.resize?.();
|
||||
raf = requestAnimationFrame(draw);
|
||||
resize();
|
||||
requestPaint();
|
||||
} });
|
||||
wx.onAudioInterruptionBegin?.(stopAudio);
|
||||
wx.onWindowResize?.(resize);
|
||||
// 只读状态,供微信开发者工具和发布包自动验收定位问题。
|
||||
GameGlobal.numberSwipeR2 = { snapshot: () => ({...window.variantSnapshot(),hint:hintView(),audio:{...audioDiagnostics},rules: $('rule-text').textContent,lesson:!$('rules-sheet').hidden&&lessonState?{key:lesson?.key,path:[...lessonState.route],message:lessonState.message}:null}), buttons: () => buttons.map(({ label, x, y, w, h, enabled }) => ({ label, x, y, w, h, enabled })) };
|
||||
GameGlobal.numberSwipeR2 = { snapshot: () => ({...window.variantSnapshot(),theme:'bright-classroom',fonts:{...fontDiagnostics},renderedFrames,reducedMotion:preferences.reducedMotion,boardRect:{x:0,y:top+82,w:width,h:$('board').clientHeight},hint:hintView(),audio:{...audioDiagnostics},rules: $('rule-text').textContent,lesson:!$('rules-sheet').hidden&&lessonState?{key:lesson?.key,finished:lessonState.finished,duration:lesson?.duration,path:[...lessonState.route],message:lessonState.message}:null}), buttons: () => buttons.map(({ label, x, y, w, h, enabled }) => ({ label, x, y, w, h, enabled })) };
|
||||
resize();
|
||||
raf = requestAnimationFrame(draw);
|
||||
requestPaint();
|
||||
|
||||
Reference in New Issue
Block a user