Verbeter grafische kwaliteit van kleurplaten (v0.4.67-beta) #5

Open
bes-r wants to merge 208 commits from bes-r/coloring-quality-v0.4.67 into main AGit
4 changed files with 323 additions and 16 deletions
Showing only changes of commit 5cdc854e69 - Show all commits

View file

@ -1 +1 @@
0.4.17-beta
0.4.18-beta

View file

@ -1733,6 +1733,25 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2
.world-answers .tbtn.wrong{background:color-mix(in srgb,var(--red) 14%,var(--surface));color:var(--red);border-color:var(--red)}.world-clue{width:min(100%,560px);padding:18px 20px;border:1px solid color-mix(in srgb,var(--accent) 35%,var(--line));border-radius:18px;background:linear-gradient(135deg,var(--accent-soft),var(--surface));color:var(--accent-ink);font-size:clamp(16px,3.2cqi,21px);font-weight:850;line-height:1.5;text-align:center}.world-clue-latin{font-family:Georgia,serif;font-size:clamp(23px,4.6cqi,34px);font-style:italic;font-weight:700}
.world-photo-answers{width:min(100%,640px);display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.world-photo-choice{position:relative;min-width:0;margin:0;overflow:hidden;border:3px solid transparent;border-radius:18px;background:#152432;box-shadow:0 8px 18px rgba(20,36,52,.16);transition:border-color .15s,transform .15s,box-shadow .15s}.world-image-button{display:block;width:100%;margin:0;padding:0;border:0;background:#152432;cursor:pointer}.world-image-button .world-picture{transition:transform .18s,filter .18s}.world-photo-choice:hover{transform:translateY(-2px);box-shadow:0 12px 22px rgba(20,36,52,.2)}.world-image-button:hover .world-picture{transform:scale(1.025)}.world-image-button:focus-visible{outline:4px solid var(--accent);outline-offset:-4px}.world-choice-number{position:absolute;left:8px;top:8px;display:grid;width:28px;height:28px;place-items:center;border:2px solid rgba(255,255,255,.7);border-radius:50%;background:rgba(10,27,42,.86);color:#fff;font-size:13px;font-weight:950;pointer-events:none}.world-photo-choice.correct{border-color:var(--green);box-shadow:0 0 0 3px color-mix(in srgb,var(--green) 25%,transparent)}.world-photo-choice.wrong{border-color:var(--red)}.world-photo-choice.wrong .world-picture{filter:saturate(.65) brightness(.82)}
.world-gallery{width:100%;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.world-specimen{min-width:0;display:flex;flex-direction:column;align-items:center;gap:9px;padding:11px;border:1px solid var(--line);border-radius:20px;background:linear-gradient(155deg,var(--surface),var(--surface-2));box-shadow:0 8px 22px rgba(20,36,52,.08)}.world-specimen .world-visual{width:100%}.world-specimen p{margin:0;padding:0 5px 3px;color:var(--muted);font-size:12px;font-weight:720;line-height:1.4;text-align:center}
/* flitskaarten: zelfde opbouw als flits.js (naam kort tonen, dan zelftoets),
hier met de foto/kaart altijd zichtbaar en de naam geflitst i.p.v. andersom */
.world-flash-card{width:100%;display:flex;flex-direction:column;align-items:center;gap:10px}
.world-flash-name{font-size:clamp(24px,5vw,34px);font-weight:900;letter-spacing:1px;color:var(--blue);min-height:44px;display:flex;align-items:center}
.world-flash-name.dim{color:var(--muted)}
.world-flash-hint{margin:0;color:var(--muted);font-weight:800;text-align:center}
.world-flash-reflect{width:100%;padding:10px 14px;border-radius:14px;background:var(--surface-2);text-align:center}
.world-flash-reflect>p{margin:0 0 8px;font-weight:900}
/* letterblokken: hergebruikt de blok/vak/dots-stijl van letters.js/de rekenwidgets */
.world-spell-hint{margin:0;color:var(--muted);font-weight:800;text-align:center}
.world-spell-answer{width:100%;display:flex;flex-direction:column;align-items:center;gap:12px}
.world-spell-word{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:6px;max-width:100%}
.world-spell-gap{width:14px;flex:none}
.world-letter-slot.slot.sm,.world-letter-block.blok.sm{width:40px;height:60px;padding-top:3px}
.world-letter-slot.slot.sm .ghost{height:24px;font-size:19px}
.world-letter-slot.slot.sm .slot-dots{margin-bottom:4px}
.world-letter-block.blok.sm .bl{height:26px;font-size:22px}
.world-letter-block.blok.sm .blok-dots{margin-bottom:4px}
.world-spell-tray.lg-tray{max-width:min(100%,480px)}
.topo-card{width:min(100%,740px)}.topo-map-wrap{width:100%;display:flex;flex-direction:column;align-items:center;gap:7px;overflow:hidden;padding:10px;border:1px solid var(--line);border-radius:20px;background:linear-gradient(155deg,#edf8fd,#dff2fb)}.topo-map{display:block;width:min(100%,510px);height:auto;max-height:590px;touch-action:manipulation;filter:drop-shadow(0 9px 13px rgba(29,60,76,.13))}.topo-water{fill:#dff3fb}.topo-province{stroke:#fff;stroke-width:4.5;stroke-linejoin:round;vector-effect:non-scaling-stroke;transition:filter .15s,opacity .15s,fill .15s}.topo-province[role=button]{cursor:pointer}.topo-province[role=button]:hover,.topo-province[role=button]:focus{filter:brightness(.88) saturate(1.12);outline:none}.topo-province[role=button]:focus-visible{stroke:var(--navy);stroke-width:8}.topo-province.picked{fill:var(--red)!important}.topo-province.target{fill:var(--green)!important}.topo-label,.topo-city-label{fill:#18324e;paint-order:stroke;stroke:#fff;stroke-width:5px;stroke-linejoin:round;font-size:18px;font-weight:950;text-anchor:middle;pointer-events:none}.topo-city-label{text-anchor:start;font-size:16px}.topo-city-label.left{text-anchor:end}.topo-marker{cursor:pointer}.topo-hit{fill:transparent}.topo-dot{fill:var(--navy);stroke:#fff;stroke-width:4;transition:fill .15s,stroke-width .15s}.topo-marker:hover .topo-dot,.topo-marker:focus .topo-dot{fill:var(--orange);outline:none}.topo-marker:focus-visible .topo-dot{stroke:var(--navy);stroke-width:8}.topo-marker.picked .topo-dot{fill:var(--red)}.topo-marker.target .topo-dot{fill:var(--green);stroke-width:7}.topo-source{align-self:flex-end;color:#3b657b;font-size:9px;font-weight:750;text-decoration:none}.topo-source:hover,.topo-source:focus-visible{text-decoration:underline}
.topo-province.inactive{opacity:.32}.topo-region[role=button],.topo-river[role=button],.world-continent[role=button]{cursor:pointer}.topo-region.picked .topo-province{fill:var(--red)!important}.topo-region.target .topo-province{fill:var(--green)!important}.topo-region:hover .topo-province,.topo-region:focus .topo-province{filter:brightness(.88) saturate(1.12)}.topo-region:focus-visible .topo-province{stroke:var(--navy);stroke-width:8}.topo-region-label{font-size:20px}
.topo-river.inactive{opacity:.22}.topo-river-hit,.topo-river-line{fill:none;stroke-linecap:round;stroke-linejoin:round}.topo-river-hit{stroke:transparent;stroke-width:38;pointer-events:stroke}.topo-river-line{stroke:var(--river-color);stroke-width:10;pointer-events:none;transition:stroke .15s,stroke-width .15s}.topo-river:hover .topo-river-line,.topo-river:focus .topo-river-line{stroke-width:15}.topo-river:focus-visible .topo-river-line{stroke:var(--navy);stroke-width:18}.topo-river.picked .topo-river-line{stroke:var(--red)}.topo-river.target .topo-river-line{stroke:var(--green);stroke-width:16}.topo-river-label{fill:#18324e;paint-order:stroke;stroke:#fff;stroke-width:5px;stroke-linejoin:round;font-size:20px;font-style:italic;font-weight:950;text-anchor:middle;pointer-events:none}

View file

@ -2,7 +2,7 @@
"use strict";
/* version shown until /api/version resolves (or if the fetch fails, e.g. offline).
Kept in sync by hand with the VERSION file at the repo root on every release. */
const VERSION = "0.4.17-beta";
const VERSION = "0.4.18-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;
@ -124,7 +124,8 @@ const I18N = {
wg_birds:"Vogels herkennen", wg_birds_d:"Leer achttien Nederlandse vogels met vier foto-, kenmerk- en naamoefeningen.",
wg_plants:"Wilde planten", wg_plants_d:"Herken twaalf algemene wilde planten aan echte foto's en duidelijke veldkenmerken.",
wg_topography:"Topografie", wg_topography_d:"Oefen Nederland én de wereld: provincies, plaatsen, landsdelen, rivieren en werelddelen op nauwkeurige kaarten.",
worldActivity:"Werkvorm", worldLearn:"Leren", worldQuiz:"Quiz", worldExercise:"Oefening", worldLevel:"Niveau", worldTreeView:"Wat zie je?", worldTreeWhole:"Hele boom", worldTreeLeaf:"Blad", worldTreeMixed:"Boom en blad",
worldActivity:"Werkvorm", worldLearn:"Leren", worldQuiz:"Quiz", worldFlash:"Flitskaarten", worldSpell:"Letterblokken", worldExercise:"Oefening", worldLevel:"Niveau", worldTreeView:"Wat zie je?", worldTreeWhole:"Hele boom", worldTreeLeaf:"Blad", worldTreeMixed:"Boom en blad",
worldSpellHint:"Sleep de letters naar de vakken en spel de naam.", worldSpellComplete:"Goed gespeld! 🌟",
worldExerciseRecognize:"Herken de soort", worldExercisePair:"Koppel boom en blad", worldExerciseClue:"Raad met kenmerken", worldExercisePhoto:"Kies de juiste foto", worldExerciseLatin:"Wetenschappelijke naam",
worldScore:"Score: {score} · reeks: {streak}", worldExploreHint:"Bekijk de kenmerken en vergelijk de soorten.", worldQuestionTree:"Welke boom zie je?", worldQuestionLeaf:"Bij welke boom hoort dit blad?", worldQuestionBird:"Welke vogel zie je?", worldQuestionPlant:"Welke wilde plant zie je?",
worldQuestionPair:"Bij welke boom horen deze boom en dit blad?", worldQuestionClue:"Welke soort hoort bij deze kenmerken?", worldQuestionLatin:"Welke soort hoort bij deze wetenschappelijke naam?", worldFindTreePhoto:"Tik op de foto van de {name}.", worldFindBirdPhoto:"Tik op de foto van de {name}.", worldFindPlantPhoto:"Tik op de foto van de {name}.",
@ -431,7 +432,8 @@ const I18N = {
wg_birds:"Bird identification", wg_birds_d:"Learn eighteen Dutch birds with four photo, feature and naming exercises.",
wg_plants:"Wild plants", wg_plants_d:"Recognise twelve common wild plants from real photographs and clear field marks.",
wg_topography:"Topography", wg_topography_d:"Practise the Netherlands and the world: provinces, places, regions, rivers and continents on accurate maps.",
worldActivity:"Activity", worldLearn:"Learn", worldQuiz:"Quiz", worldExercise:"Exercise", worldLevel:"Level", worldTreeView:"What do you see?", worldTreeWhole:"Whole tree", worldTreeLeaf:"Leaf", worldTreeMixed:"Tree and leaf",
worldActivity:"Activity", worldLearn:"Learn", worldQuiz:"Quiz", worldFlash:"Flashcards", worldSpell:"Letter blocks", worldExercise:"Exercise", worldLevel:"Level", worldTreeView:"What do you see?", worldTreeWhole:"Whole tree", worldTreeLeaf:"Leaf", worldTreeMixed:"Tree and leaf",
worldSpellHint:"Drag the letters into the slots to spell the name.", worldSpellComplete:"Well spelled! 🌟",
worldExerciseRecognize:"Recognise the species", worldExercisePair:"Match tree and leaf", worldExerciseClue:"Guess from features", worldExercisePhoto:"Choose the correct photo", worldExerciseLatin:"Scientific name",
worldScore:"Score: {score} · streak: {streak}", worldExploreHint:"Study the features and compare the species.", worldQuestionTree:"Which tree do you see?", worldQuestionLeaf:"Which tree does this leaf belong to?", worldQuestionBird:"Which bird do you see?", worldQuestionPlant:"Which wild plant do you see?",
worldQuestionPair:"Which species do this tree and leaf belong to?", worldQuestionClue:"Which species matches these features?", worldQuestionLatin:"Which species matches this scientific name?", worldFindTreePhoto:"Tap the photograph of the {name}.", worldFindBirdPhoto:"Tap the photograph of the {name}.", worldFindPlantPhoto:"Tap the photograph of the {name}.",

View file

@ -323,6 +323,182 @@ function worldIdentity(item){
return identity;
}
/* ---- flitskaarten: naam kort tonen, dan het beeld + zelftoets, zoals
flits.js - hier gevoed door de wereldoriëntatie-content i.p.v. woordkaarten.
Hergebruikt de generieke fl*-teksten (zelfde vraag/knoppen als flits.js). ---- */
function worldFlashCard(card,name,visual,S,redraw,onNext,onAssess){
const wrap=document.createElement("div");
wrap.className="world-flash-card";
wrap.append(visual);
const nameZone=document.createElement("div");
nameZone.className="world-flash-name"+(S.flashRevealed?"":" dim");
nameZone.textContent=S.flashRevealed?name:"• • •";
wrap.append(nameZone);
card.append(wrap);
if(!S.flashRevealed){
const hint=document.createElement("p");
hint.className="world-flash-hint";
hint.textContent=T("flMission");
card.append(hint);
const actions=document.createElement("div");
actions.className="world-actions";
actions.append(btn(T("flShow"),()=>{S.flashRevealed=true;redraw()}));
actions.append(btn(T("worldNext"),onNext,"ghost"));
card.append(actions);
}else if(!S.solved){
const reflect=document.createElement("div");
reflect.className="world-flash-reflect";
const q=document.createElement("p");
q.textContent=T("flQuestion");
reflect.append(q);
const row=document.createElement("div");
row.className="world-actions";
row.append(btn(T("flGotIt"),()=>onAssess(true)));
row.append(btn(T("flAgain"),()=>onAssess(false),"ghost"));
reflect.append(row);
card.append(reflect);
}
}
/* ---- letterblokken: spel de naam met dezelfde sleep-blokken-mechaniek als
letters.js, hier gevoed door de naam van de soort/plaats. Elk vak toont
meteen het puntenpatroon (en de letter) van het juiste antwoord - bewust
de makkelijkste ondersteuning, want deze namen zijn voor de meeste
kinderen onbekende spelling; het quiz/foto/latijn-werk hierboven test
het herkennen al veel strenger. */
const worldIsLetter=ch=>/\p{L}/u.test(ch);
const worldSpellPool=name=>[...name.toLowerCase()].filter(worldIsLetter);
function worldSpellNeedsNewTray(S,name){
const letters=worldSpellPool(name);
return !Array.isArray(S.answerLetters)||S.answerLetters.length!==letters.length||!Array.isArray(S.trayLetters);
}
function worldSpellNewTray(S,name){
const letters=worldSpellPool(name);
const distractPool="abcdefghijklmnopqrstuvwxyz".split("").filter(c=>!letters.includes(c));
S.trayLetters=worldShuffle([...letters,...worldShuffle(distractPool).slice(0,4)]);
S.answerLetters=letters.map(()=>null);
S.spellSolved=false;
}
function worldLetterBlock(ch,trayIndex,letters,S,redraw){
const b=document.createElement("div");
b.className="blok sm world-letter-block "+(VOWELS.includes(ch)?"vowel":"cons");
b.dataset.letter=ch;
const label=document.createElement("div");
label.className="bl";
label.textContent=ch;
b.append(label);
const dg=dotGrid(ch);
dg.classList.add("blok-dots");
b.append(dg);
b.setAttribute("role","button");
b.setAttribute("aria-label",T("clockDigit").replace("{digit}",ch));
b.tabIndex=0;
const settle=idx=>{S.answerLetters[idx]=trayIndex;sndGood();redraw()};
b.onkeydown=e=>{
if((e.key==="Enter"||e.key===" ")&&!S.spellSolved){
e.preventDefault();
const idx=letters.findIndex((need,i)=>need===ch&&S.answerLetters[i]===null);
if(idx>=0)settle(idx);
}
};
b.onpointerdown=e=>{
if(S.spellSolved||(e.pointerType==="mouse"&&e.button!==0))return;
e.preventDefault();
try{b.releasePointerCapture(e.pointerId)}catch(_){}
const clone=b.cloneNode(true);
clone.id="dragClone";
document.body.append(clone);
b.classList.add("dragging");
const moveTo=ev=>{const zoom=VZ();clone.style.left=ev.clientX/zoom+"px";clone.style.top=ev.clientY/zoom+"px"};
moveTo(e);
let hovered=null;
const hit=ev=>{
let found=null;
document.querySelectorAll(".world-letter-slot:not(.filled)").forEach(slot=>{
const r=slot.getBoundingClientRect(),pad=8;
if(ev.clientX>=r.left-pad&&ev.clientX<=r.right+pad&&ev.clientY>=r.top-pad&&ev.clientY<=r.bottom+pad)found=slot;
});
return found;
};
const move=ev=>{
moveTo(ev);
const target=hit(ev);
if(hovered&&hovered!==target)hovered.classList.remove("droptarget");
hovered=target;
if(hovered)hovered.classList.add("droptarget");
};
const clean=()=>{
document.removeEventListener("pointermove",move);
document.removeEventListener("pointerup",up);
document.removeEventListener("pointercancel",cancel);
clone.remove();
b.classList.remove("dragging");
if(hovered)hovered.classList.remove("droptarget");
};
const up=ev=>{
const target=hit(ev);
clean();
if(target){
const idx=+target.dataset.index;
if(letters[idx]===ch)settle(idx);
else{target.classList.add("shake");setTimeout(()=>target.classList.remove("shake"),400);sndBad()}
}
};
const cancel=clean;
document.addEventListener("pointermove",move);
document.addEventListener("pointerup",up);
document.addEventListener("pointercancel",cancel);
};
return b;
}
function worldSpellAnswer(name,S,redraw){
if(worldSpellNeedsNewTray(S,name))worldSpellNewTray(S,name);
const letters=worldSpellPool(name);
const wrap=document.createElement("div");
wrap.className="world-spell-answer";
const wordRow=document.createElement("div");
wordRow.className="world-spell-word";
let slotIdx=0;
[...name.toLowerCase()].forEach(ch=>{
if(!worldIsLetter(ch)){
const gap=document.createElement("span");
gap.className="world-spell-gap";
wordRow.append(gap);
return;
}
const idx=slotIdx++;
const slot=document.createElement("div");
slot.className="slot sm world-letter-slot"+(S.answerLetters[idx]!==null?" filled":"");
slot.dataset.index=idx;
const ghost=document.createElement("div");
ghost.className="ghost";
ghost.textContent=letters[idx];
const dots=dotGrid(letters[idx]);
dots.classList.add("slot-dots");
slot.append(ghost,dots);
if(S.answerLetters[idx]!==null){
const trayIndex=S.answerLetters[idx];
const placed=worldLetterBlock(S.trayLetters[trayIndex],trayIndex,letters,S,redraw);
placed.classList.add("in-slot");
placed.tabIndex=-1;
placed.onpointerdown=null;
placed.onkeydown=null;
slot.append(placed);
slot.onclick=()=>{if(!S.spellSolved){S.answerLetters[idx]=null;redraw()}};
}
wordRow.append(slot);
});
wrap.append(wordRow);
const tray=document.createElement("div");
tray.className="lg-tray world-spell-tray";
S.trayLetters.forEach((ch,trayIndex)=>{
if(S.answerLetters.includes(trayIndex))return;
tray.append(worldLetterBlock(ch,trayIndex,letters,S,redraw));
});
wrap.append(tray);
return wrap;
}
function mountWorldQuiz(root,init,config){
const exerciseOptions=config.type==="trees"
?[["recognize","worldExerciseRecognize"],["pair","worldExercisePair"],["clue","worldExerciseClue"],["photo","worldExercisePhoto"],["latin","worldExerciseLatin"]]
@ -330,10 +506,11 @@ function mountWorldQuiz(root,init,config){
const S=Object.assign({
activity:"quiz",exercise:"recognize",level:1,view:config.type==="trees"?"mixed":config.type==="birds"?"bird":"plant",
score:0,streak:0,target:"",taskView:"",options:[],picked:"",solved:false,feedback:"",
flashRevealed:false,answerLetters:null,trayLetters:null,spellSolved:false,
},init||{});
S.level=Math.max(1,Math.min(3,+S.level||1));
if(!exerciseOptions.some(([id])=>id===S.exercise))S.exercise="recognize";
const activity=worldSelect(S.activity,[["learn","worldLearn"],["quiz","worldQuiz"]]);
const activity=worldSelect(S.activity,[["learn","worldLearn"],["quiz","worldQuiz"],["flash","worldFlash"],["spell","worldSpell"]]);
const exercise=worldSelect(S.exercise,exerciseOptions);
const level=worldSelect(String(S.level),[["1","lvl1"],["2","lvl2"],["3","lvl3"]]);
const stage=document.createElement("div");
@ -359,6 +536,10 @@ function mountWorldQuiz(root,init,config){
S.picked="";
S.solved=false;
S.feedback="";
S.flashRevealed=false;
S.answerLetters=null;
S.trayLetters=null;
S.spellSolved=false;
draw();
};
const answer=id=>{
@ -371,6 +552,14 @@ function mountWorldQuiz(root,init,config){
}
draw();
};
const assessFlash=ok=>{
if(S.solved)return;
S.solved=true;
if(ok){S.feedback="good";S.score++;S.streak++;sndGood()}
else{S.feedback="bad";S.streak=0;sndBad()}
draw();
};
const worldViewFor=()=>config.type==="trees"?(S.view==="mixed"?"tree":S.view):config.type==="birds"?"bird":"plant";
const gallery=()=>{
const grid=document.createElement("div");
grid.className="world-gallery";
@ -390,7 +579,7 @@ function mountWorldQuiz(root,init,config){
S.exercise=exercise.value;
S.level=+level.value;
if(view)S.view=view.value;
exerciseSetting.hidden=S.activity==="learn";
exerciseSetting.hidden=S.activity!=="quiz";
if(viewSetting)viewSetting.hidden=S.activity==="quiz"&&!["recognize","photo"].includes(S.exercise);
stage.className="world-app";
stage.innerHTML="";
@ -411,6 +600,60 @@ function mountWorldQuiz(root,init,config){
}
const item=current(),card=document.createElement("section");
card.className="world-task-card";
if(S.activity==="flash"){
worldFlashCard(card,worldName(item),worldVisual(item,config.type,worldViewFor()),S,draw,()=>{S.flashRevealed=true;draw()},assessFlash);
if(S.solved){
const fact=document.createElement("div");
fact.className="world-fact";
fact.append(worldIdentity(item));
const factHint=document.createElement("p");
factHint.textContent=worldHint(item);
fact.append(factHint);
card.append(fact);
}
const flashFeedback=document.createElement("p");
flashFeedback.className="world-feedback "+(S.feedback==="good"?"ok":S.feedback?"bad":"");
flashFeedback.setAttribute("role","status");
if(S.feedback)flashFeedback.textContent=T(S.feedback==="good"?"worldCorrect":"worldTryAgain");
card.append(flashFeedback);
const flashActions=document.createElement("div");
flashActions.className="world-actions";
flashActions.append(btn(T("worldNext"),newTask,"ghost"));
card.append(flashActions);
stage.append(card);
return;
}
if(S.activity==="spell"){
card.append(worldVisual(item,config.type,worldViewFor()));
const spellHint=document.createElement("p");
spellHint.className="world-spell-hint";
spellHint.textContent=T("worldSpellHint");
card.append(spellHint);
card.append(worldSpellAnswer(worldName(item),S,draw));
if(!S.spellSolved&&S.answerLetters.every(v=>v!==null)){
S.spellSolved=true;S.score++;S.streak++;sndGood();
}
const spellFeedback=document.createElement("p");
spellFeedback.className="world-feedback "+(S.spellSolved?"ok":"");
spellFeedback.setAttribute("role","status");
if(S.spellSolved)spellFeedback.textContent=T("worldSpellComplete");
card.append(spellFeedback);
if(S.spellSolved){
const fact=document.createElement("div");
fact.className="world-fact";
fact.append(worldIdentity(item));
const factHint=document.createElement("p");
factHint.textContent=worldHint(item);
fact.append(factHint);
card.append(fact);
}
const spellActions=document.createElement("div");
spellActions.className="world-actions";
spellActions.append(btn(T("worldNext"),newTask,"ghost"));
card.append(spellActions);
stage.append(card);
return;
}
const question=document.createElement("h3");
if(S.exercise==="photo"){
const photoQuestion=config.type==="birds"?"worldFindBirdPhoto":config.type==="plants"?"worldFindPlantPhoto":"worldFindTreePhoto";
@ -477,12 +720,13 @@ function mountWorldQuiz(root,init,config){
}
stage.append(card);
}
activity.onchange=()=>{S.activity=activity.value;if(S.activity==="quiz")newTask();else draw()};
exercise.onchange=()=>{S.exercise=exercise.value;if(S.activity==="quiz")newTask();else draw()};
level.onchange=()=>{S.level=+level.value;if(S.activity==="quiz")newTask();else draw()};
if(view)view.onchange=()=>{S.view=view.value;if(S.activity==="quiz")newTask();else draw()};
const isTaskActivity=()=>["quiz","flash","spell"].includes(S.activity);
activity.onchange=()=>{S.activity=activity.value;if(isTaskActivity())newTask();else draw()};
exercise.onchange=()=>{S.exercise=exercise.value;if(isTaskActivity())newTask();else draw()};
level.onchange=()=>{S.level=+level.value;if(isTaskActivity())newTask();else draw()};
if(view)view.onchange=()=>{S.view=view.value;if(isTaskActivity())newTask();else draw()};
learningShell(root,settings,stage);
if(S.activity==="quiz")newTask();else draw();
if(isTaskActivity())newTask();else draw();
return{getState:()=>({...S,options:[...S.options]})};
}
@ -673,10 +917,11 @@ function mountTopography(root,init){
const S=Object.assign({
activity:"quiz",mode:"provinces",level:1,score:0,streak:0,
target:"",picked:"",solved:false,feedback:"",
flashRevealed:false,answerLetters:null,trayLetters:null,spellSolved:false,
},init||{});
S.level=Math.max(1,Math.min(3,+S.level||1));
if(!modeOptions.some(([id])=>id===S.mode))S.mode="provinces";
const activity=worldSelect(S.activity,[["learn","worldLearn"],["quiz","worldQuiz"]]);
const activity=worldSelect(S.activity,[["learn","worldLearn"],["quiz","worldQuiz"],["flash","worldFlash"],["spell","worldSpell"]]);
const mode=worldSelect(S.mode,modeOptions);
const level=worldSelect(String(S.level),[["1","lvl1"],["2","lvl2"],["3","lvl3"]]);
const stage=document.createElement("div");
@ -696,6 +941,7 @@ function mountTopography(root,init){
let target=items[Math.floor(Math.random()*items.length)];
if(items.length>1)while(target.id===previous)target=items[Math.floor(Math.random()*items.length)];
S.target=target.id;S.picked="";S.solved=false;S.feedback="";
S.flashRevealed=false;S.answerLetters=null;S.trayLetters=null;S.spellSolved=false;
draw();
};
const answer=id=>{
@ -708,6 +954,13 @@ function mountTopography(root,init){
}
draw();
};
const assessFlash=ok=>{
if(S.solved)return;
S.solved=true;
if(ok){S.feedback="good";S.score++;S.streak++;sndGood()}
else{S.feedback="bad";S.streak=0;sndBad()}
draw();
};
function draw(){
S.activity=activity.value;S.mode=mode.value;S.level=+level.value;
levelSetting.hidden=S.mode==="regions";
@ -731,6 +984,38 @@ function mountTopography(root,init){
intro.className="world-intro";
intro.textContent=T(S.mode==="regions"?"topoExploreRegions":S.mode==="rivers"?"topoExploreRivers":S.mode==="continents"?"topoExploreContinents":"topoExploreHint");
card.append(intro,renderMap(null,true,"",""));
}else if(S.activity==="flash"){
const item=current();
worldFlashCard(card,worldName(item),renderMap(null,S.flashRevealed,"",S.flashRevealed?S.target:""),S,draw,()=>{S.flashRevealed=true;draw()},assessFlash);
const flashFeedback=document.createElement("p");
flashFeedback.className="world-feedback "+(S.feedback==="good"?"ok":S.feedback?"bad":"");
flashFeedback.setAttribute("role","status");
if(S.feedback)flashFeedback.textContent=T(S.feedback==="good"?"worldCorrect":"worldTryAgain");
card.append(flashFeedback);
const flashActions=document.createElement("div");
flashActions.className="world-actions";
flashActions.append(btn(T("worldNext"),newTask,"ghost"));
card.append(flashActions);
}else if(S.activity==="spell"){
const item=current();
card.append(renderMap(null,true,"",S.target));
const spellHint=document.createElement("p");
spellHint.className="world-spell-hint";
spellHint.textContent=T("worldSpellHint");
card.append(spellHint);
card.append(worldSpellAnswer(worldName(item),S,draw));
if(!S.spellSolved&&S.answerLetters.every(v=>v!==null)){
S.spellSolved=true;S.score++;S.streak++;sndGood();
}
const spellFeedback=document.createElement("p");
spellFeedback.className="world-feedback "+(S.spellSolved?"ok":"");
spellFeedback.setAttribute("role","status");
if(S.spellSolved)spellFeedback.textContent=T("worldSpellComplete");
card.append(spellFeedback);
const spellActions=document.createElement("div");
spellActions.className="world-actions";
spellActions.append(btn(T("worldNext"),newTask,"ghost"));
card.append(spellActions);
}else{
const item=current(),question=document.createElement("h3");
const questionKey=S.mode==="provinces"?"topoFindProvince"
@ -752,14 +1037,15 @@ function mountTopography(root,init){
}
stage.append(card);
}
activity.onchange=()=>{S.activity=activity.value;if(S.activity==="quiz")newTask();else draw()};
mode.onchange=()=>{S.mode=mode.value;if(S.activity==="quiz")newTask();else draw()};
level.onchange=()=>{S.level=+level.value;if(S.activity==="quiz")newTask();else draw()};
const isTaskActivity=()=>["quiz","flash","spell"].includes(S.activity);
activity.onchange=()=>{S.activity=activity.value;if(isTaskActivity())newTask();else draw()};
mode.onchange=()=>{S.mode=mode.value;if(isTaskActivity())newTask();else draw()};
level.onchange=()=>{S.level=+level.value;if(isTaskActivity())newTask();else draw()};
learningShell(root,[
field(T("worldActivity"),activity),
field(T("topoMode"),mode),
levelSetting,
],stage);
if(S.activity==="quiz")newTask();else draw();
if(isTaskActivity())newTask();else draw();
return{getState:()=>({...S})};
}