Ga direct naar de hoofdinhoud
Klasklaar.nl
Home
Blog
Gr. 1/2
Inloopactiviteiten
Thema's
Thema Herfst
Thema Ruimte
Thema Sinterklaas
Thema Boerderij
Thema Vlinder
Thema Vervoer
Thema Bouwen
Thema Koningsspelen
Thema Gezondheid
Motoriek 1/2
Rekenen 1/2
Lezen 1/2
Verklanken
Voorleesteksten
SEO 1/2
Energizers
dagritmekaarten
Gr. 3
Thema's
Rekenen 3
Bussommen
Dobbelsteen sommen
Contextsommen
Lezen 3
Teksten op thema
Sinterklaas
Kerst
Carnaval
Feest
Koningsspelen
Beroepen
De bakker
De bouw
De hulpdiensten
De post
De bibliotheek
De boerderij
De speeltuin
De tuin
Dierentuin
Het bos
het restaurant
Strand
Het zwembad
Thuis
School
Supermarkt
Verkeer
Het vliegtuig
De trein
Winter
Lente
Zomer
Herfst
Piraten
Sprookjes
Familie
Huisdieren
Kleding
Gezondheid
Gezond eten
Sport
Insecten
Teksten B3
Teksten M3
Teksten E3
Lees en kleur
Luister, lees en kies
Lees en zoek
Waar of niet waar
SEO 3
Creatief
dagritmekaarten
Gr. 4
Rekenen 4
Getallen en bewerkingen
Verhaaltjessommen
Werkbladen
Rekenspelletjes
Meten
Verhaaltjessommen
Werkbladen
Tijd
Verhaaltjessommen
Werkbladen
Geld
Verhaaltjessommen
Werkbladen
Som bij het plaatje
Steunkaarten
Taal 4
Woordbenoemen
Zinsontleding
Taal Algemeen
Steunkaarten
Spelling 4
Steunkaarten
Spellingwerkbladen
Spellen
Lezen 4
Teksten B4
Teksten M4
Teksten E4
Leeslessen 4
Begrijpend Lezen 4
SEO 4
Overig
Geschiedenis 4
dagritmekaarten
Gr. 5
Rekenen 5
Getallen en bewerkingen
Verhaalsommen
Werkbladen
Rekenspelletjes
Meten
Werkbladen
Verhaalsommen
Tijd
Verhaalsommen
Werkbladen
Geld
Verhaalsommen
Werkbladen
Breuken
Verhaalsommen
Werkbladen
Steunkaarten
Taal 5
Woordbenoemen
Zinsontleding
Taal Algemeen
Spelling 5
Spellingwerkbladen
Spellen
Lezen 5
Teksten B5
Teksten M5
Teksten E5
Leeslessen 5
Begrijpend Lezen 5
SEO 5
Geschiedenis 5
Gr. 6
Rekenen 6
Getallen en Bewerkingen
Verhaalsommen
Werkbladen
Meten
Verhaalsommen
Werkbladen
Tijd
Verhaalsommen
Werkbladen
Geld
Verhaalsommen
Werkbladen
Breuken
Verhaalsommen
Werkbladen
Procenten
Verhaalsommen
Werkbladen
Steunkaarten
Taal 6
Woordbenoemen
Zinsontleding
Taal Algemeen
Spelling 6
Spellingwerkbladen
Spellen
Werkwoordspelling
Lezen 6
Teksten B6
Teksten M6
Teksten E6
Leeslessen 6
Begrijpend Lezen 6
SEO 6
Gr. 7
Rekenen 7
Getallen en Bewerkingen
Werkbladen
Verhaalsommen
Meten
Werkbladen
Verhaalsommen
Tijd
Werkbladen
Verhaalsommen
Geld
Werkbladen
Verhaalsommen
Breuken
Verhaalsommen
Werkbladen
Procenten
Werkbladen
Verhaalsommen
Verhoudingen
Steunkaarten
Taal 7
Woordbenoemen
Zinsontleding
Taal Algemeen
Spelling 7
Spellingwerkbladen
Spellen
Lezen 7
Teksten B7
Teksten M7
Teksten E7
Leeslessen 7
Begrijpend Lezen 7
SEO 7
Gr. 8
Rekenen 8
Getallen en bewerkingen
Verhaalsommen
Werkbladen
Meten
Verhaalsommen
Werkbladen
Tijd
Verhaalsommen
Werkbladen
Geld
Verhaalsommen
Werkbladen
Breuken
Verhaalsommen
Werkbladen
Procenten
Verhaalsommen
Werkbladen
Verhoudingen
Steunkaarten
Taal 8
Woordbenoemen
Zinsontleding
Taal Algemeen
Spelling 8
Spellingwerkbladen
Spellen
Lezen 8
Leeslessen
Lezen B8
Begrijpend Lezen
SEO 8
Afscheid
Didactiek
Sommenmaker
Taalmaker
Woordbenoemen generator
Woorden generator
Zinnen generator
NT2
Lezen
Pictokaarten
Taal
Portfolio
Dit ben ik
Mijn werk in beeld
Doeboekjes
Prentenboeken
Vlam en het feest
Blaadje en de storm
Kooltje en steenkolen
Wolkje in de lucht
Tok en Tik - De taart
Tok en Tik - We gaan je missen
Tok en Tik - De storm
Tok en Tik - Het ruim
Groepsvorming
Gouden Weken
Kinderliedjes
Digitale Geletterdheid
B-bot
Techniek
Kleurplaten
Sinterklaas
Kerst
Pasen
Carnaval
Koningsdag
Koningsspelen
Oud en Nieuw
Verjaardag
Moederdag
Vaderdag
Halloween
Valentijn
Lente
Zomer
Herfst
Winter
Regen
Sneeuw
Strand
Bloemen
Paddenstoelen
Weer
Bakker
Bouw
Kunstenaar
Politie
Brandweer
Dokter
Tandarts
Boer
Kapper
Post
Winkel
Auto
Tractor
Bus
Vliegtuig
Boot
Fiets
Trein
Vrachtwagen
Graafmachine
Brandweerauto
Politieauto
Voetbal
Hockey
Korfbal
Zwemmen
Dansen
Turnen
Sportdag
Speeltuin
Speelgoed
Spelletjes
Sprookjes
Draken
Ridders
Piraten
Prinsessen
Zeemeerminnen
Heksen
Tovenaars
Eenhoorns
Monsters
Natuur
Bos
Boerderij
Ruimte
Verkeer
Gezondheid
Lichaam
Eten en drinken
Techniek
Recycling
Boerderijdieren
Dierentuin
Huisdieren
Bosdieren
Zeedieren
Insecten
Vogels
Dino's
Paarden
Honden
Katten
Ballet
Timer
Sommenflitser
Woordenflitser
Keuzerad
Dagstarter
Praatplaten
Naamplaatjes
Splitsmachine
Bussommen
Rekenrek
AR Aquarium
Woordzoeker maker
Sudoku Maker
Doolhof maker
Online spellen
Blikgooien (tafels)
Eendjes vangen (tafels)
Ringwerpen (tafels)
Roodkapje (splitsen)
Hans en Grietje (plus en min)
Zeven kleine geitjes (plus via de 10)
Verliefde harten
Ruimtemissie (automatiseren)
Locomotief (automatiseren)
contact
Zoeken
Doolhof maken
Kies de vorm, tekenstijl, afmeting en gangbreedte. Iedere puzzel wordt automatisch gecontroleerd.
Werkblad
Antwoordblad
Doolhof wordt gemaakt
De preview verschijnt automatisch.
(function(global){ 'use strict'; const DIRS=[[-1,0],[0,1],[1,0],[0,-1]],OPP=[2,3,0,1]; const GLYPHS={ A:[".###.","#...#","#...#","#####","#...#","#...#","#...#"],B:["####.","#...#","#...#","####.","#...#","#...#","####."],C:[".####","#....","#....","#....","#....","#....",".####"],D:["####.","#...#","#...#","#...#","#...#","#...#","####."],E:["#####","#....","#....","####.","#....","#....","#####"],F:["#####","#....","#....","####.","#....","#....","#...."],G:[".####","#....","#....","#.###","#...#","#...#",".###."],H:["#...#","#...#","#...#","#####","#...#","#...#","#...#"],I:["#####","..#..","..#..","..#..","..#..","..#..","#####"],J:["..###","...#.","...#.","...#.","...#.","#..#.",".##.."],K:["#...#","#..#.","#.#..","##...","#.#..","#..#.","#...#"],L:["#....","#....","#....","#....","#....","#....","#####"],M:["#...#","##.##","#.#.#","#...#","#...#","#...#","#...#"],N:["#...#","##..#","#.#.#","#..##","#...#","#...#","#...#"],O:[".###.","#...#","#...#","#...#","#...#","#...#",".###."],P:["####.","#...#","#...#","####.","#....","#....","#...."],Q:[".###.","#...#","#...#","#...#","#.#.#","#..#.",".##.#"],R:["####.","#...#","#...#","####.","#.#..","#..#.","#...#"],S:[".####","#....","#....",".###.","....#","....#","####."],T:["#####","..#..","..#..","..#..","..#..","..#..","..#.."],U:["#...#","#...#","#...#","#...#","#...#","#...#",".###."],V:["#...#","#...#","#...#","#...#","#...#",".#.#.","..#.."],W:["#...#","#...#","#...#","#...#","#.#.#","##.##","#...#"],X:["#...#",".#.#.","..#..","..#..","..#..",".#.#.","#...#"],Y:["#...#",".#.#.","..#..","..#..","..#..","..#..","..#.."],Z:["#####","....#","...#.","..#..",".#...","#....","#####"], "0":[".###.","#...#","#..##","#.#.#","##..#","#...#",".###."],"1":["..#..",".##..","..#..","..#..","..#..","..#..",".###."],"2":[".###.","#...#","....#","...#.","..#..",".#...","#####"],"3":["####.","....#","....#",".###.","....#","....#","####."],"4":["#...#","#...#","#...#","#####","....#","....#","....#"],"5":["#####","#....","#....","####.","....#","....#","####."],"6":[".###.","#....","#....","####.","#...#","#...#",".###."],"7":["#####","....#","...#.","..#..",".#...",".#...",".#..."],"8":[".###.","#...#","#...#",".###.","#...#","#...#",".###."],"9":[".###.","#...#","#...#",".####","....#","....#",".###."]}; function hashSeed(seed){let x=Number(seed);if(!Number.isFinite(x)){x=2166136261;for(const ch of String(seed)){x^=ch.charCodeAt(0);x=Math.imul(x,16777619)}}return (x>>>0)||0x9e3779b9} function createRng(seed){let a=hashSeed(seed);return function(){a|=0;a=(a+0x6D2B79F5)|0;let t=Math.imul(a^(a>>>15),1|a);t=(t+Math.imul(t^(t>>>7),61|t))^t;return ((t^(t>>>14))>>>0)/4294967296}} function ri(rng,n){return Math.floor(rng()*n)}function shuffle(a,rng){a=a.slice();for(let i=a.length-1;i>0;i--){const j=ri(rng,i+1);[a[i],a[j]]=[a[j],a[i]]}return a} function id(r,c,cols){return r*cols+c}function rc(i,cols){return [Math.floor(i/cols),i%cols]}function inb(r,c,rows,cols){return r>=0&&c>=0&&r<rows&&c<cols} function countMask(active){let n=0;for(const x of active)n+=x?1:0;return n} function maskComponents(rows,cols,active){const seen=new Uint8Array(active.length),comps=[];for(let s=0;s<active.length;s++){if(!active[s]||seen[s])continue;const q=[s],comp=[];seen[s]=1;for(let h=0;h<q.length;h++){const a=q[h];comp.push(a);const [r,c]=rc(a,cols);for(const [dr,dc] of DIRS){const nr=r+dr,nc=c+dc;if(!inb(nr,nc,rows,cols))continue;const b=id(nr,nc,cols);if(active[b]&&!seen[b]){seen[b]=1;q.push(b)}}}comps.push(comp)}return comps} function connectMask(rows,cols,active){active=new Uint8Array(active);for(let guard=0;guard<20;guard++){const comps=maskComponents(rows,cols,active);if(comps.length<=1)return active;comps.sort((a,b)=>b.length-a.length);const A=comps[0];let best=null;for(let k=1;k<comps.length;k++){const B=comps[k];for(const a of A){const [ar,ac]=rc(a,cols);for(const b of B){const [br,bc]=rc(b,cols),d=Math.abs(ar-br)+Math.abs(ac-bc);if(!best||d<best.d)best={a,b,d};if(d<=2)break}if(best&&best.d<=2)break}if(best&&best.d<=2)break}if(!best)break;let [r,c]=rc(best.a,cols),[tr,tc]=rc(best.b,cols);while(c!==tc){c+=c<tc?1:-1;active[id(r,c,cols)]=1}while(r!==tr){r+=r<tr?1:-1;active[id(r,c,cols)]=1}}return active} const FIGURE_POLYGONS={star:[[0.0000,0.9400],[-0.2527,0.3479],[-0.8940,0.2905],[-0.4090,-0.1329],[-0.5525,-0.7605],[-0.0000,-0.4300],[0.5525,-0.7605],[0.4090,-0.1329],[0.8940,0.2905],[0.2527,0.3479]],lightning:[[-0.1200,0.9500],[0.5800,0.9500],[0.1800,0.2000],[0.6300,0.2000],[-0.4200,-0.9600],[-0.1200,-0.1800],[-0.5600,-0.1800]],shield:[[-0.7800,0.7200],[0.0000,0.9400],[0.7800,0.7200],[0.6800,-0.2000],[0.0000,-0.9400],[-0.6800,-0.2000]],crown:[[-0.9200,-0.6600],[-0.8000,0.5500],[-0.3800,0.0800],[-0.1000,0.8200],[0.1800,0.0800],[0.5600,0.6400],[0.8200,0.1200],[0.9200,-0.6600]],rocket:[[0.0000,0.9800],[0.4200,0.4600],[0.4200,-0.2800],[0.7800,-0.7800],[0.3000,-0.6600],[0.0000,-0.9600],[-0.3000,-0.6600],[-0.7800,-0.7800],[-0.4200,-0.2800],[-0.4200,0.4600]],tree:[[0.0000,0.9800],[-0.7800,0.1800],[-0.3500,0.1800],[-0.8600,-0.4000],[-0.2800,-0.4000],[-0.2800,-0.9400],[0.2800,-0.9400],[0.2800,-0.4000],[0.8600,-0.4000],[0.3500,0.1800],[0.7800,0.1800]],house:[[-0.8800,-0.8600],[-0.8800,0.0800],[0.0000,0.9200],[0.8800,0.0800],[0.8800,-0.8600]],pencil:[[-0.9400,-0.2800],[0.4200,0.8200],[0.8600,0.3800],[-0.5000,-0.7200],[-0.9200,-0.8800]],drop:[[0.0000,0.9800],[-0.6800,0.1200],[-0.6200,-0.4000],[0.0000,-0.9000],[0.6200,-0.4000],[0.6800,0.1200]]}; function pointInPoly(x,y,pts){let inside=false;for(let i=0,j=pts.length-1;i<pts.length;j=i++){const xi=pts[i][0],yi=pts[i][1],xj=pts[j][0],yj=pts[j][1];if(((yi>y)!==(yj>y))&&(x<(xj-xi)*(y-yi)/(yj-yi||1e-12)+xi))inside=!inside}return inside} function ellipse(x,y,cx,cy,rx,ry){const dx=(x-cx)/rx,dy=(y-cy)/ry;return dx*dx+dy*dy<=1} function figureInside(name,x,y){const r=Math.hypot(x,y),a=Math.atan2(y,x);if(FIGURE_POLYGONS[name])return pointInPoly(x,y,FIGURE_POLYGONS[name]);switch(name){ case'heart':{const X=x*1.12,Y=(y+.12)*1.05,v=X*X+Y*Y-1;return v*v*v-X*X*Y*Y*Y<=0} case'moon':return r<=.92&&!ellipse(x,y,.34,.18,.68,.76); case'cloud':return ellipse(x,y,-.48,.02,.46,.42)||ellipse(x,y,-.06,.30,.50,.50)||ellipse(x,y,.45,.08,.45,.40)||(x>- .78&&x<.78&&y>-.48&&y<.18); case'sun':return r<=.56||(r<=.91&&Math.cos(8*a)>.48); case'mushroom':return (ellipse(x,y,0,.30,.88,.56)&&y>-.02)||(Math.abs(x)<.30&&y<.22&&y>-.88); case'apple':return ellipse(x,y,-.25,.02,.58,.72)||ellipse(x,y,.25,.02,.58,.72)||(Math.abs(x)<.13&&y>.62&&y<.94); case'flower':{if(r<=.34)return true;for(let k=0;k<6;k++){const t=k*Math.PI/3,cx=.48*Math.cos(t),cy=.48*Math.sin(t);if(ellipse(x,y,cx,cy,.34,.28))return true}return false} case'butterfly':return ellipse(x,y,-.45,.33,.43,.48)||ellipse(x,y,.45,.33,.43,.48)||ellipse(x,y,-.42,-.34,.38,.36)||ellipse(x,y,.42,-.34,.38,.36)||(Math.abs(x)<.14&&Math.abs(y)<.78); case'fish':return ellipse(x,y,-.10,0,.72,.55)||pointInPoly(x,y,[[.48,0],[.94,.62],[.94,-.62]]); case'turtle':return ellipse(x,y,-.08,0,.68,.52)||ellipse(x,y,.70,.02,.27,.25)||ellipse(x,y,-.52,.48,.22,.18)||ellipse(x,y,.35,.45,.20,.17)||ellipse(x,y,-.52,-.48,.22,.18)||ellipse(x,y,.35,-.45,.20,.17); case'snail':return ellipse(x,y,-.35,.20,.52,.52)||ellipse(x,y,.22,-.24,.70,.25)||ellipse(x,y,.54,.02,.24,.42); case'cat':return ellipse(x,y,0,-.05,.74,.70)||pointInPoly(x,y,[[-.70,.42],[-.62,.96],[-.18,.63]])||pointInPoly(x,y,[[.70,.42],[.62,.96],[.18,.63]]); case'rabbit':return ellipse(x,y,0,-.28,.65,.58)||ellipse(x,y,-.30,.52,.24,.54)||ellipse(x,y,.30,.52,.24,.54); case'gift':return (Math.abs(x)<.78&&y<.52&&y>-.88)||ellipse(x,y,-.22,.62,.27,.22)||ellipse(x,y,.22,.62,.27,.22); case'puzzle':return (Math.abs(x)<.72&&Math.abs(y)<.72)||ellipse(x,y,0,.72,.25,.28)||ellipse(x,y,.72,-.10,.28,.25); case'leaf':{const X=(x+y)*.72,Y=(y-x)*.72;return ellipse(X,Y,0,0,.48,.96)} case'ghost':return ellipse(x,y,0,.22,.72,.72)||(Math.abs(x)<.72&&y<.25&&y>-.72)||(y<-.60&&((x<-.35)||Math.abs(x)<.14||x>.35)); case'pumpkin':return ellipse(x,y,0,-.08,.88,.68)||(Math.abs(x)<.14&&y>.50&&y<.92); default:return r<=.85}} function figureMask(name,density){const n=density==='wide'?15:density==='narrow'?29:21,rows=n,cols=n,active=new Uint8Array(rows*cols);for(let r=0;r<rows;r++)for(let c=0;c<cols;c++){const x=((c+.5)/cols)*2-1,y=1-((r+.5)/rows)*2;if(figureInside(name,x,y))active[id(r,c,cols)]=1}return {rows,cols,active:connectMask(rows,cols,active)}} function glyphMask(ch,density){const pat=GLYPHS[ch]||GLYPHS.A,scale=density==='wide'?2:density==='narrow'?4:3,rows=7*scale,cols=5*scale,active=new Uint8Array(rows*cols);for(let pr=0;pr<7;pr++)for(let pc=0;pc<5;pc++)if(pat[pr][pc]==='#')for(let rr=0;rr<scale;rr++)for(let cc=0;cc<scale;cc++)active[id(pr*scale+rr,pc*scale+cc,cols)]=1;return {rows,cols,active:connectMask(rows,cols,active)}} function makeTopology(shape,density,glyph,figure){let rows,cols,active;if(shape==='letter'||shape==='digit'){const g=glyphMask(glyph,density);rows=g.rows;cols=g.cols;active=g.active}else if(shape==='figure'){const g=figureMask(figure||'star',density);rows=g.rows;cols=g.cols;active=g.active}else{const maps={square:{wide:[9,9],regular:[14,14],narrow:[20,20]},portrait:{wide:[12,8],regular:[18,12],narrow:[24,16]},landscape:{wide:[8,12],regular:[12,18],narrow:[16,24]},diagonal:{wide:[9,9],regular:[14,14],narrow:[20,20]},round:{wide:[13,13],regular:[19,19],narrow:[25,25]}};[rows,cols]=(maps[shape]||maps.square)[density]||maps.square.regular;active=new Uint8Array(rows*cols);if(shape==='round'){const cy=(rows-1)/2,cx=(cols-1)/2,ry=rows/2-.55,rx=cols/2-.55;for(let r=0;r<rows;r++)for(let c=0;c<cols;c++){const y=(r-cy)/ry,x=(c-cx)/rx;if(x*x+y*y<=1)active[id(r,c,cols)]=1}}else active.fill(1)}active=connectMask(rows,cols,active);const topo={shape,density,glyph:glyph||'',figure:figure||'',rows,cols,active,count:countMask(active)};if(topo.count<12)throw new Error('TOPOLOGY_TOO_SMALL');return topo} function neighborsTopo(i,topo){const [r,c]=rc(i,topo.cols),out=[];for(let d=0;d<4;d++){const nr=r+DIRS[d][0],nc=c+DIRS[d][1];if(inb(nr,nc,topo.rows,topo.cols)){const j=id(nr,nc,topo.cols);if(topo.active[j])out.push([j,d])}}return out} function addEdge(adj,a,b,d){adj[a]|=1<<d;adj[b]|=1<<OPP[d]}function hasEdge(adj,a,d){return !!(adj[a]&(1<<d))} function activeList(topo){const a=[];for(let i=0;i<topo.active.length;i++)if(topo.active[i])a.push(i);return a} function generateDFS(topo,rng){const adj=new Uint8Array(topo.active.length),act=activeList(topo),vis=new Uint8Array(topo.active.length),stack=[act[ri(rng,act.length)]];vis[stack[0]]=1;while(stack.length){const cur=stack[stack.length-1],opts=shuffle(neighborsTopo(cur,topo).filter(([j])=>!vis[j]),rng);if(!opts.length){stack.pop();continue}const [j,d]=opts[0];addEdge(adj,cur,j,d);vis[j]=1;stack.push(j)}return adj} function generatePrim(topo,rng){const adj=new Uint8Array(topo.active.length),act=activeList(topo),vis=new Uint8Array(topo.active.length),front=[],start=act[ri(rng,act.length)];vis[start]=1;for(const [j,d] of neighborsTopo(start,topo))front.push([start,j,d]);while(front.length){const k=ri(rng,front.length),[a,b,d]=front[k];front[k]=front[front.length-1];front.pop();if(vis[b])continue;addEdge(adj,a,b,d);vis[b]=1;for(const [j,dd] of neighborsTopo(b,topo))if(!vis[j])front.push([b,j,dd])}return adj} function generateKruskal(topo,rng){const adj=new Uint8Array(topo.active.length),parent=Int32Array.from({length:topo.active.length},(_,i)=>i),rank=new Uint8Array(topo.active.length);const find=x=>{while(parent[x]!==x){parent[x]=parent[parent[x]];x=parent[x]}return x},union=(a,b)=>{a=find(a);b=find(b);if(a===b)return false;if(rank[a]<rank[b])[a,b]=[b,a];parent[b]=a;if(rank[a]===rank[b])rank[a]++;return true};let edges=[];for(const a of activeList(topo)){const [r,c]=rc(a,topo.cols);if(c+1<topo.cols){const b=id(r,c+1,topo.cols);if(topo.active[b])edges.push([a,b,1])}if(r+1<topo.rows){const b=id(r+1,c,topo.cols);if(topo.active[b])edges.push([a,b,2])}}edges=shuffle(edges,rng);for(const [a,b,d] of edges)if(union(a,b))addEdge(adj,a,b,d);return adj} function exteriorMask(topo){const ext=new Uint8Array(topo.active.length),q=[];for(let r=0;r<topo.rows;r++)for(let c=0;c<topo.cols;c++){if(r!==0&&c!==0&&r!==topo.rows-1&&c!==topo.cols-1)continue;const i=id(r,c,topo.cols);if(!topo.active[i]&&!ext[i]){ext[i]=1;q.push(i)}}for(let h=0;h<q.length;h++){const a=q[h],[r,c]=rc(a,topo.cols);for(const [dr,dc] of DIRS){const nr=r+dr,nc=c+dc;if(inb(nr,nc,topo.rows,topo.cols)){const b=id(nr,nc,topo.cols);if(!topo.active[b]&&!ext[b]){ext[b]=1;q.push(b)}}}}return ext} function openingSides(i,topo,ext){const [r,c]=rc(i,topo.cols),out=[];for(let d=0;d<4;d++){const nr=r+DIRS[d][0],nc=c+DIRS[d][1];if(!inb(nr,nc,topo.rows,topo.cols)){out.push(d);continue}const j=id(nr,nc,topo.cols);if(!topo.active[j]&&ext[j])out.push(d)}return out} function bfs(adj,topo,start){const dist=new Int32Array(topo.active.length);dist.fill(-1);const par=new Int32Array(topo.active.length);par.fill(-1);const q=new Int32Array(topo.active.length);let h=0,t=0;q[t++]=start;dist[start]=0;while(h<t){const a=q[h++],[r,c]=rc(a,topo.cols);for(let d=0;d<4;d++)if(hasEdge(adj,a,d)){const nr=r+DIRS[d][0],nc=c+DIRS[d][1];if(!inb(nr,nc,topo.rows,topo.cols))continue;const b=id(nr,nc,topo.cols);if(topo.active[b]&&dist[b]<0){dist[b]=dist[a]+1;par[b]=a;q[t++]=b}}}return {dist,par}} function chooseSide(i,sides,topo){if(sides.length===1)return sides[0];const [r,c]=rc(i,topo.cols),cy=(topo.rows-1)/2,cx=(topo.cols-1)/2,vy=r-cy,vx=c-cx;let best=sides[0],bestDot=-Infinity;for(const d of sides){const [dy,dx]=DIRS[d],dot=dx*vx+dy*vy;if(dot>bestDot){bestDot=dot;best=d}}return best} function chooseEndpoints(adj,topo,rng){const ext=exteriorMask(topo),boundary=[];let minR=Infinity,maxR=-Infinity,minC=Infinity,maxC=-Infinity;for(const i of activeList(topo)){const [r,c]=rc(i,topo.cols);minR=Math.min(minR,r);maxR=Math.max(maxR,r);minC=Math.min(minC,c);maxC=Math.max(maxC,c)}for(const i of activeList(topo)){const [r,c]=rc(i,topo.cols),sides=openingSides(i,topo,ext);if(sides.length)boundary.push({i,sides})}if(boundary.length<2)throw new Error('NO_BOUNDARY_ENDPOINTS');const envelope=boundary.map(x=>({i:x.i,sides:x.sides.filter(d=>{const [r,c]=rc(x.i,topo.cols);return d===0?r===minR:d===2?r===maxR:d===3?c===minC:c===maxC})})).filter(x=>x.sides.length);const poolBoundary=envelope.length>=2?envelope:boundary;let a=poolBoundary[ri(rng,poolBoundary.length)].i;for(let sweep=0;sweep<2;sweep++){const {dist}=bfs(adj,topo,a),sorted=poolBoundary.filter(x=>dist[x.i]>=0&&x.i!==a).sort((x,y)=>dist[y.i]-dist[x.i]);if(!sorted.length)throw new Error('NO_ENDPOINT_PAIR');const pool=sorted.slice(0,Math.max(1,Math.ceil(sorted.length*.12)));a=pool[ri(rng,pool.length)].i}const start=a,{dist}=bfs(adj,topo,start),sorted=poolBoundary.filter(x=>x.i!==start&&dist[x.i]>=0).sort((x,y)=>dist[y.i]-dist[x.i]),pool=sorted.slice(0,Math.max(1,Math.ceil(sorted.length*.15))),endObj=pool[ri(rng,pool.length)],end=endObj.i,startObj=poolBoundary.find(x=>x.i===start);return {start,end,startSide:chooseSide(start,startObj.sides,topo),endSide:chooseSide(end,endObj.sides,topo)}} function route(adj,topo,start,end){const {par,dist}=bfs(adj,topo,start);if(dist[end]<0)return null;const p=[];for(let x=end;x!==-1;x=par[x]){p.push(x);if(x===start)break}p.reverse();return p[0]===start?p:null} function branchDepth(adj,topo,pathSet,from,parent){let max=0,stack=[[from,parent,1]];while(stack.length){const [a,p,dep]=stack.pop();max=Math.max(max,dep);const [r,c]=rc(a,topo.cols);for(let d=0;d<4;d++)if(hasEdge(adj,a,d)){const nr=r+DIRS[d][0],nc=c+DIRS[d][1],b=id(nr,nc,topo.cols);if(b!==p&&!pathSet.has(b))stack.push([b,a,dep+1])}}return max} function metrics(adj,topo,start,end,path){const V=topo.count,pathSet=new Set(path);let decisions=0,branches=0,depthSum=0,maxDepth=0,turns=0,prevDir=-1;for(let k=0;k<path.length;k++){const a=path[k],prev=k?path[k-1]:-1,next=k+1<path.length?path[k+1]:-1,[r,c]=rc(a,topo.cols);for(let d=0;d<4;d++)if(hasEdge(adj,a,d)){const b=id(r+DIRS[d][0],c+DIRS[d][1],topo.cols);if(b!==prev&&b!==next&&!pathSet.has(b)){decisions++;branches++;const bd=branchDepth(adj,topo,pathSet,b,a);depthSum+=bd;maxDepth=Math.max(maxDepth,bd)}}if(k+1<path.length){const [r2,c2]=rc(path[k+1],topo.cols),d=r2>r?2:r2<r?0:c2>c?1:3;if(prevDir>=0&&d!==prevDir)turns++;prevDir=d}}const [sr,sc]=rc(start,topo.cols),[er,ec]=rc(end,topo.cols),euclid=Math.hypot(sr-er,sc-ec),routeRatio=(path.length-1)/Math.max(1,V-1),decisionRate=decisions/Math.max(1,path.length-2),avgDepth=branches?depthSum/branches:0,turnRate=turns/Math.max(1,path.length-2),deception=(path.length-1)/Math.max(1,euclid),scale=Math.sqrt(V);const score=100*(.30*Math.min(1,routeRatio*2.1)+.25*Math.min(1,decisionRate*1.9)+.20*Math.min(1,avgDepth/(scale*1.45))+.15*Math.min(1,turnRate*1.35)+.10*Math.min(1,(deception-1)/4));return {score:+score.toFixed(2),pathLength:path.length-1,routeRatio,decisions,avgBranchDepth:+avgDepth.toFixed(2),maxBranchDepth:maxDepth,turns,deception:+deception.toFixed(2)}} function validateTopology(topo){if(!topo||topo.count<1)return {valid:false,reason:'empty'};const comps=maskComponents(topo.rows,topo.cols,topo.active);return comps.length===1&&comps[0].length===topo.count?{valid:true}:{valid:false,reason:'mask_disconnected'}} function validateGraph(result){const {topo,adj,start,end,path}=result,vt=validateTopology(topo);if(!vt.valid)return vt;if(!Number.isInteger(start)||!Number.isInteger(end)||start===end||!topo.active[start]||!topo.active[end])return {valid:false,reason:'bad_endpoints'};if(!Number.isInteger(result.startSide)||!Number.isInteger(result.endSide)||result.startSide<0||result.startSide>3||result.endSide<0||result.endSide>3)return {valid:false,reason:'bad_opening_side'};let edges=0;for(const a of activeList(topo)){const [r,c]=rc(a,topo.cols);for(let d=0;d<4;d++)if(hasEdge(adj,a,d)){const nr=r+DIRS[d][0],nc=c+DIRS[d][1];if(!inb(nr,nc,topo.rows,topo.cols))return {valid:false,reason:'edge_outside'};const b=id(nr,nc,topo.cols);if(!topo.active[b]||!hasEdge(adj,b,OPP[d]))return {valid:false,reason:'edge_invalid'};if(a<b)edges++}}if(edges!==topo.count-1)return {valid:false,reason:'not_tree_edges'};const {dist}=bfs(adj,topo,start);let seen=0;for(const i of activeList(topo))if(dist[i]>=0)seen++;if(seen!==topo.count||dist[end]<0)return {valid:false,reason:'not_connected'};const p=route(adj,topo,start,end);if(!p||!path||p.length!==path.length||p.some((v,i)=>v!==path[i]))return {valid:false,reason:'path_mismatch'};const ext=exteriorMask(topo);if(!openingSides(start,topo,ext).includes(result.startSide)||!openingSides(end,topo,ext).includes(result.endSide))return {valid:false,reason:'bad_opening'};return {valid:true,edges,vertices:topo.count}} function independentValidate(result){const {topo,adj}=result;if(!topo||!adj||!Number.isInteger(result.start)||!Number.isInteger(result.end)||result.start===result.end||!topo.active[result.start]||!topo.active[result.end])return {valid:false,reason:'bad_endpoints'};const graph=new Map();for(const a of activeList(topo)){const ns=[],[r,c]=rc(a,topo.cols);for(let d=0;d<4;d++)if((adj[a]&(1<<d))!==0){const nr=r+DIRS[d][0],nc=c+DIRS[d][1];if(!inb(nr,nc,topo.rows,topo.cols))return {valid:false,reason:'outside'};const b=id(nr,nc,topo.cols);if(!topo.active[b])return {valid:false,reason:'inactive'};if((adj[b]&(1<<OPP[d]))===0)return {valid:false,reason:'nonreciprocal'};ns.push(b)}graph.set(a,ns)}const seen=new Set(),parent=new Map(),stack=[result.start];parent.set(result.start,-1);let cycle=false;while(stack.length){const a=stack.pop();if(seen.has(a))continue;seen.add(a);for(const b of graph.get(a)||[]){if(!seen.has(b)){if(!parent.has(b))parent.set(b,a);stack.push(b)}else if(parent.get(a)!==b)cycle=true}}if(seen.size!==topo.count||cycle)return {valid:false,reason:cycle?'cycle':'disconnected'};const q=[result.start],par=new Map([[result.start,-1]]);for(let h=0;h<q.length;h++){const a=q[h];for(const b of graph.get(a)||[])if(!par.has(b)){par.set(b,a);q.push(b)}}if(!par.has(result.end))return {valid:false,reason:'no_route'};const p=[];for(let x=result.end;x!==-1;x=par.get(x)){p.push(x);if(x===result.start)break}p.reverse();if(p.length!==result.path.length||p.some((v,i)=>v!==result.path[i]))return {valid:false,reason:'different_route'};return {valid:true,path:p}} function generateCandidate(topo,seed,algo){const rng=createRng(seed),adj=algo===0?generateDFS(topo,rng):algo===1?generatePrim(topo,rng):generateKruskal(topo,rng),ep=chooseEndpoints(adj,topo,rng),path=route(adj,topo,ep.start,ep.end),m=metrics(adj,topo,ep.start,ep.end,path),out={topo,adj,start:ep.start,end:ep.end,startSide:ep.startSide,endSide:ep.endSide,path,metrics:m,seed:hashSeed(seed),algorithm:['dfs','prim','kruskal'][algo]};const a=validateGraph(out),b=independentValidate(out);if(!a.valid||!b.valid)throw new Error('INVALID_GENERATED_'+(a.reason||b.reason));return out} function generateMaze(opts){opts=opts||{};const shape=opts.shape||'square',density=opts.density||'regular',difficulty=opts.difficulty||'normal',glyph=opts.glyph||'A',figure=opts.figure||'star',topo=makeTopology(shape,density,glyph,figure),seed=hashSeed(opts.seed==null?Date.now():opts.seed),count=Math.max(15,Number(opts.candidates)||30),arr=[];for(let i=0;i<count;i++){const s=(seed+Math.imul(i+1,0x9e3779b1))>>>0;arr.push(generateCandidate(topo,s,i%3))}arr.sort((a,b)=>a.metrics.score-b.metrics.score);const q=difficulty==='easy'?.14:difficulty==='hard'?.88:.51,idx=Math.max(0,Math.min(arr.length-1,Math.round((arr.length-1)*q))),chosen=arr[idx];chosen.requestedDifficulty=difficulty;chosen.difficultyPercentile=arr.length>1?idx/(arr.length-1):0;chosen.measuredDifficulty=chosen.difficultyPercentile<.33?'easy':chosen.difficultyPercentile<.70?'normal':'hard';chosen.scoreRange=[arr[0].metrics.score,arr[arr.length-1].metrics.score];chosen.shape=shape;chosen.densityKey=density;chosen.glyph=glyph;chosen.figure=figure;return chosen} function wallSegments(result){const {topo,adj}=result,out=[];for(const a of activeList(topo)){const [r,c]=rc(a,topo.cols);for(let d=0;d<4;d++){if(hasEdge(adj,a,d))continue;const nr=r+DIRS[d][0],nc=c+DIRS[d][1],inside=inb(nr,nc,topo.rows,topo.cols),b=inside?id(nr,nc,topo.cols):-1,neighborActive=inside&&topo.active[b];if(neighborActive&&a>b)continue;if((a===result.start&&d===result.startSide)||(a===result.end&&d===result.endSide))continue;let p1,p2;if(d===0){p1=[c,r];p2=[c+1,r]}else if(d===1){p1=[c+1,r];p2=[c+1,r+1]}else if(d===2){p1=[c,r+1];p2=[c+1,r+1]}else{p1=[c,r];p2=[c,r+1]}const k1=p1[0]+','+p1[1],k2=p2[0]+','+p2[1];out.push({a:p1,b:p2,key1:k1,key2:k2})}}return out} function openingPoint(result,which){const i=which==='start'?result.start:result.end,d=which==='start'?result.startSide:result.endSide,[r,c]=rc(i,result.topo.cols);return d===0?[c+.5,r]:d===1?[c+1,r+.5]:d===2?[c+.5,r+1]:[c,r+.5]} function outsidePoint(result,which,amount=.55){const p=openingPoint(result,which),d=which==='start'?result.startSide:result.endSide;return [p[0]+DIRS[d][1]*amount,p[1]+DIRS[d][0]*amount]} function pathCenters(result){return result.path.map(i=>{const [r,c]=rc(i,result.topo.cols);return [c+.5,r+.5]})} global.KlasklaarMazeEngine={DIRS,OPP,GLYPHS,hashSeed,createRng,makeTopology,figureMask,generateMaze,validateGraph,independentValidate,wallSegments,openingPoint,outsidePoint,pathCenters,rc,activeList,maskComponents}; })(typeof window!=='undefined'?window:globalThis); (function(){ 'use strict'; const root=document.getElementById('kkmz-maze-root');if(!root||root.dataset.kkmzInitialized==='1')return;root.dataset.kkmzInitialized='1'; const E=window.KlasklaarMazeEngine;if(!E)throw new Error('MAZE_ENGINE_MISSING'); window.__KKMZ_LOGO_DATA='data:image/png;base64,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'; const q=s=>root.querySelector(s),qa=s=>Array.from(root.querySelectorAll(s)),els={title:q('#kkmz-title'),letterWrap:q('#kkmz-letterWrap'),digitWrap:q('#kkmz-digitWrap'),figureWrap:q('#kkmz-figureWrap'),letter:q('#kkmz-letterSelect'),digit:q('#kkmz-digitSelect'),figure:q('#kkmz-figureSelect'),showName:q('#kkmz-showName'),showGroup:q('#kkmz-showGroup'),showDate:q('#kkmz-showDate'),includeSolution:q('#kkmz-includeSolution'),status:q('#kkmz-statusBox'),newBtn:q('#kkmz-newBtn'),print:q('#kkmz-printBtn'),stage:q('#kkmz-sheetStage'),meta:q('#kkmz-previewMeta')}; const LABELS={portrait:'Lang',landscape:'Breed',square:'Vierkant',diagonal:'Schuin',round:'Rond',letter:'Letter',digit:'Cijfer',figure:'Vorm',normal:'Normaal',organic:'Organisch',winding:'Kronkel',easy:'Makkelijk',hard:'Moeilijk',small:'Klein',medium:'Normaal',large:'Groot'}; const DENSITY_LABELS={wide:'Breed',regular:'Normaal',narrow:'Smal'}; const FIGURE_LABELS={star:'Ster',heart:'Hart',moon:'Maan',cloud:'Wolk',sun:'Zon',drop:'Druppel',lightning:'Bliksem',shield:'Schild',crown:'Kroon',rocket:'Raket',tree:'Boom',mushroom:'Paddenstoel',apple:'Appel',flower:'Bloem',butterfly:'Vlinder',fish:'Vis',turtle:'Schildpad',snail:'Slak',cat:'Kattenkop',rabbit:'Konijnenkop',house:'Huis',gift:'Cadeau',puzzle:'Puzzelstuk',pencil:'Potlood',leaf:'Blad',ghost:'Spook',pumpkin:'Pompoen'}; const state={shape:'square',style:'normal',difficulty:'normal',size:'medium',density:'regular',seed:makeSeed(),results:null,activeTab:'student',generationId:0,generating:false}; function makeSeed(){if(window.crypto&&crypto.getRandomValues){const a=new Uint32Array(1);crypto.getRandomValues(a);return a[0]}return (Date.now()^Math.floor(Math.random()*0xffffffff))>>>0} function esc(s){return String(s).replace(/[&<>"']/g,ch=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]))} function selectButtons(sel,key,val){qa(sel).forEach(b=>b.classList.toggle('active',String(b.dataset[key])===String(val)))} function setStatus(kind,title,text){els.status.className='status '+kind;els.status.innerHTML='<strong>'+esc(title)+'</strong><span>'+esc(text)+'</span>'} function setGenerating(v){state.generating=v;els.newBtn.disabled=v;els.print.disabled=v} function glyphValue(){return state.shape==='letter'?els.letter.value:state.shape==='digit'?els.digit.value:''} function figureValue(){return state.shape==='figure'?els.figure.value:''} function updateShapeExtra(){els.letterWrap.classList.toggle('hidden',state.shape!=='letter');els.digitWrap.classList.toggle('hidden',state.shape!=='digit');els.figureWrap.classList.toggle('hidden',state.shape!=='figure')} function shapeDisplay(){const extra=glyphValue()||(figureValue()?FIGURE_LABELS[figureValue()]: '');return LABELS[state.shape]+(extra?' · '+extra:'')} function updateMeta(){els.meta.textContent=shapeDisplay()+' · '+LABELS[state.style]+' · '+LABELS[state.size]+' · gangen '+DENSITY_LABELS[state.density]+' · '+LABELS[state.difficulty]} function generate(){const id=++state.generationId;setGenerating(true);setStatus('info','Doolhof wordt gemaakt','De unieke route, START/FINISH en moeilijkheid worden gecontroleerd.');els.stage.innerHTML='<div class="empty-preview"><div><strong>Doolhof wordt gecontroleerd</strong>De preview verschijnt automatisch.</div></div>';setTimeout(()=>{try{const r=E.generateMaze({shape:state.shape,density:state.density,difficulty:state.difficulty,glyph:glyphValue()||'A',figure:figureValue()||'star',seed:state.seed,candidates:30}),v1=E.validateGraph(r),v2=E.independentValidate(r);if(!v1.valid||!v2.valid||r.measuredDifficulty!==state.difficulty)throw new Error('FINAL_VALIDATION_FAILED');if(id!==state.generationId)return;state.results=[r];setStatus('success','Doolhof gecontroleerd','Unieke route, veilige START/FINISH en het gekozen moeilijkheidsniveau zijn gecontroleerd.');renderPreview()}catch(err){console.error(err);if(id!==state.generationId)return;state.results=null;setStatus('error','Deze versie kon niet veilig worden gemaakt','Klik op Nieuwe versie. Er wordt nooit een onbewezen doolhof getoond.');els.stage.innerHTML='<div class="empty-preview"><div><strong>Geen doolhof weergegeven</strong>De eindcontrole heeft deze versie tegengehouden.</div></div>'}finally{if(id===state.generationId)setGenerating(false)}},20)} function baseTransform(result,p){const [x,y]=p;if(result.shape==='diagonal')return [x-y,x+y];return [x,y]} function activeBounds(result){const t=result.topo;let minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity;for(const i of E.activeList(t)){const [r,c]=E.rc(i,t.cols);for(const p of [[c,r],[c+1,r],[c,r+1],[c+1,r+1]]){const z=baseTransform(result,p);minX=Math.min(minX,z[0]);minY=Math.min(minY,z[1]);maxX=Math.max(maxX,z[0]);maxY=Math.max(maxY,z[1])}}return {minX,minY,maxX,maxY}} function makeMapper(result,U=1100,pad=105){const b=activeBounds(result),w=Math.max(.001,b.maxX-b.minX),h=Math.max(.001,b.maxY-b.minY),scale=Math.min((U-2*pad)/w,(U-2*pad)/h),ox=(U-w*scale)/2-b.minX*scale,oy=(U-h*scale)/2-b.minY*scale;return p=>{const z=baseTransform(result,p);return [ox+z[0]*scale,oy+z[1]*scale]}} function rngFor(result,salt){return E.createRng((result.seed^salt)>>>0)} function keyPoint(k){const a=k.split(',').map(Number);return [a[0],a[1]]} function wallVertexKeys(walls){const s=new Set();walls.forEach(w=>{s.add(w.key1);s.add(w.key2)});return Array.from(s)} function jitterVertices(walls,rng,intensity){const map=new Map();for(const k of wallVertexKeys(walls)){const p=keyPoint(k),jx=(rng()*2-1)*intensity,jy=(rng()*2-1)*intensity;map.set(k,[p[0]+jx,p[1]+jy])}return map} function qSample(p0,c,p1,n=5){const out=[];for(let i=0;i<=n;i++){const t=i/n,u=1-t;out.push([u*u*p0[0]+2*u*t*c[0]+t*t*p1[0],u*u*p0[1]+2*u*t*c[1]+t*t*p1[1]])}return out} function cSample(p0,c1,c2,p1,n=6){const out=[];for(let i=0;i<=n;i++){const t=i/n,u=1-t;out.push([u*u*u*p0[0]+3*u*u*t*c1[0]+3*u*t*t*c2[0]+t*t*t*p1[0],u*u*u*p0[1]+3*u*u*t*c1[1]+3*u*t*t*c2[1]+t*t*t*p1[1]])}return out} function orient(a,b,c){return (b[0]-a[0])*(c[1]-a[1])-(b[1]-a[1])*(c[0]-a[0])}function onSeg(a,b,p){return Math.min(a[0],b[0])-1e-7<=p[0]&&p[0]<=Math.max(a[0],b[0])+1e-7&&Math.min(a[1],b[1])-1e-7<=p[1]&&p[1]<=Math.max(a[1],b[1])+1e-7} function segIntersect(a,b,c,d){const o1=orient(a,b,c),o2=orient(a,b,d),o3=orient(c,d,a),o4=orient(c,d,b);if(((o1>1e-7&&o2<-1e-7)||(o1<-1e-7&&o2>1e-7))&&((o3>1e-7&&o4<-1e-7)||(o3<-1e-7&&o4>1e-7)))return true;if(Math.abs(o1)<=1e-7&&onSeg(a,b,c))return true;if(Math.abs(o2)<=1e-7&&onSeg(a,b,d))return true;if(Math.abs(o3)<=1e-7&&onSeg(c,d,a))return true;if(Math.abs(o4)<=1e-7&&onSeg(c,d,b))return true;return false} function bbox(poly){let x1=Infinity,y1=Infinity,x2=-Infinity,y2=-Infinity;for(const p of poly){x1=Math.min(x1,p[0]);y1=Math.min(y1,p[1]);x2=Math.max(x2,p[0]);y2=Math.max(y2,p[1])}return [x1,y1,x2,y2]}function boxesOverlap(a,b,m=0){return !(a[2]+m<b[0]-m||b[2]+m<a[0]-m||a[3]+m<b[1]-m||b[3]+m<a[1]-m)} function pointSegDist(p,a,b){const dx=b[0]-a[0],dy=b[1]-a[1],l2=dx*dx+dy*dy;if(l2<1e-12)return Math.hypot(p[0]-a[0],p[1]-a[1]);let t=((p[0]-a[0])*dx+(p[1]-a[1])*dy)/l2;t=Math.max(0,Math.min(1,t));return Math.hypot(p[0]-(a[0]+t*dx),p[1]-(a[1]+t*dy))} function segDist(a,b,c,d){if(segIntersect(a,b,c,d))return 0;return Math.min(pointSegDist(a,c,d),pointSegDist(b,c,d),pointSegDist(c,a,b),pointSegDist(d,a,b))} function validateCurves(curves,minClear=.22){const boxes=curves.map(c=>bbox(c.samples));for(let i=0;i<curves.length;i++){const P=curves[i].samples;for(let a=0;a<P.length-1;a++)for(let b=a+2;b<P.length-1;b++){if(a===0&&b===P.length-2&&Math.hypot(P[0][0]-P[P.length-1][0],P[0][1]-P[P.length-1][1])<1e-5)continue;if(segIntersect(P[a],P[a+1],P[b],P[b+1]))return false}}for(let i=0;i<curves.length;i++)for(let j=i+1;j<curves.length;j++){const A=curves[i],B=curves[j];if(A.keys.some(k=>B.keys.includes(k)))continue;if(!boxesOverlap(boxes[i],boxes[j],minClear))continue;for(let a=0;a<A.samples.length-1;a++)for(let b=0;b<B.samples.length-1;b++)if(segDist(A.samples[a],A.samples[a+1],B.samples[b],B.samples[b+1])<minClear)return false}return true} function organicGeometry(result){const walls=E.wallSegments(result);for(const intensity of [.105,.075,.045,.025]){const rng=rngFor(result,0xA53C9E17),verts=jitterVertices(walls,rng,intensity),curves=[];for(const w of walls){const a=verts.get(w.key1),b=verts.get(w.key2),mx=(a[0]+b[0])/2,my=(a[1]+b[1])/2,dx=b[0]-a[0],dy=b[1]-a[1],len=Math.hypot(dx,dy)||1,off=(rng()*2-1)*intensity*.65,c=[mx-dy/len*off,my+dx/len*off];curves.push({a,b,c,keys:[w.key1,w.key2],samples:qSample(a,c,b,5)})}if(validateCurves(curves,.27))return {type:'organic',curves,verts,intensity}}return null} function wallChains(walls){const inc=new Map();walls.forEach((w,i)=>{for(const k of [w.key1,w.key2]){if(!inc.has(k))inc.set(k,[]);inc.get(k).push(i)}});const used=new Uint8Array(walls.length),chains=[];function other(e,k){const w=walls[e];return w.key1===k?w.key2:w.key1}function walk(start,e0){const keys=[start],edges=[],start0=start;let cur=start,e=e0,guard=0;while(e!=null&&!used[e]&&guard++<walls.length+5){used[e]=1;edges.push(e);const nxt=other(e,cur);keys.push(nxt);if(nxt===start0)break;const list=inc.get(nxt)||[];if(list.length!==2)break;const ne=list[0]===e?list[1]:list[0];if(used[ne])break;cur=nxt;e=ne}return {keys,edges}} for(const [k,list] of inc)if(list.length!==2)for(const e of list)if(!used[e])chains.push(walk(k,e));for(let e=0;e<walls.length;e++)if(!used[e])chains.push(walk(walls[e].key1,e));return chains} function catmullSegments(points,tension=.82){const segs=[];if(points.length<2)return segs;for(let i=0;i<points.length-1;i++){const p0=points[i-1]||points[i],p1=points[i],p2=points[i+1],p3=points[i+2]||p2,c1=[p1[0]+(p2[0]-p0[0])*tension/6,p1[1]+(p2[1]-p0[1])*tension/6],c2=[p2[0]-(p3[0]-p1[0])*tension/6,p2[1]-(p3[1]-p1[1])*tension/6];segs.push({p0:p1,c1,c2,p1:p2})}return segs} function windingGeometry(result){const walls=E.wallSegments(result),chains=wallChains(walls);for(const intensity of [.32,.27,.22,.17,.125,.09]){const rng=rngFor(result,0xC7A1D23B),verts=jitterVertices(walls,rng,intensity*.28),curves=[];for(const ch of chains){let pts=[];for(let z=0;z<ch.edges.length;z++){const k1=ch.keys[z],k2=ch.keys[z+1],a=verts.get(k1),b=verts.get(k2);if(z===0)pts.push(a);const dx=b[0]-a[0],dy=b[1]-a[1],len=Math.hypot(dx,dy)||1,off=(rng()*2-1)*intensity,mid=[(a[0]+b[0])/2-dy/len*off,(a[1]+b[1])/2+dx/len*off];pts.push(mid,b)}const segs=catmullSegments(pts,.98),samples=[];for(const sg of segs){const part=cSample(sg.p0,sg.c1,sg.c2,sg.p1,7);if(samples.length)part.shift();samples.push(...part)}curves.push({segs,samples,keys:ch.keys})}if(validateCurves(curves,.32))return {type:'winding',curves,verts,intensity}}return null} function fmt(p){return p[0].toFixed(2)+' '+p[1].toFixed(2)} function svgPathOrganic(curve,map){const a=map(curve.a),c=map(curve.c),b=map(curve.b);return 'M '+fmt(a)+' Q '+fmt(c)+' '+fmt(b)} function svgPathWinding(curve,map){if(!curve.segs.length)return '';let d='M '+fmt(map(curve.segs[0].p0));for(const s of curve.segs)d+=' C '+fmt(map(s.c1))+' '+fmt(map(s.c2))+' '+fmt(map(s.p1));return d} function openingOut(result,which,map){const p=E.openingPoint(result,which),o=E.outsidePoint(result,which,.7),mp=map(p),mo=map(o),dx=mo[0]-mp[0],dy=mo[1]-mp[1],l=Math.hypot(dx,dy)||1;return {p:mp,dir:[dx/l,dy/l]}} function smoothMappedPath(points){if(points.length<2)return '';const segs=catmullSegments(points,.75);let d='M '+fmt(points[0]);for(const s of segs)d+=' C '+fmt(s.c1)+' '+fmt(s.c2)+' '+fmt(s.p1);return d} function mazeSvg(result,solutionMode,physicalMm){const U=1100,map=makeMapper(result,U,110),walls=E.wallSegments(result),style=state.style,sizeComp=150/physicalMm,normalLW=(result.topo.count<90?4.3:result.topo.count<220?3.4:2.7)*sizeComp;let geometry=null;if(style==='organic')geometry=organicGeometry(result);if(style==='winding')geometry=windingGeometry(result);if(style!=='normal'&&!geometry)throw new Error('GEOMETRY_VALIDATION_FAILED');let s='<svg class="kkmz-maze-svg" role="img" aria-label="Doolhof" viewBox="0 0 '+U+' '+U+'" style="width:'+physicalMm+'mm;height:'+physicalMm+'mm"><rect width="1100" height="1100" fill="white"/>'; if(style==='normal'){for(const w of walls){const a=map(w.a),b=map(w.b);s+='<line x1="'+a[0].toFixed(2)+'" y1="'+a[1].toFixed(2)+'" x2="'+b[0].toFixed(2)+'" y2="'+b[1].toFixed(2)+'" stroke="#111" stroke-width="'+normalLW+'" stroke-linecap="round"/>'}}else if(style==='organic'){for(const c of geometry.curves)s+='<path d="'+svgPathOrganic(c,map)+'" fill="none" stroke="#111" stroke-width="'+normalLW+'" stroke-linecap="round" stroke-linejoin="round"/>'}else{for(const c of geometry.curves)s+='<path d="'+svgPathWinding(c,map)+'" fill="none" stroke="#111" stroke-width="'+normalLW+'" stroke-linecap="round" stroke-linejoin="round"/>'} if(solutionMode){let pts=E.pathCenters(result).map(map),st=openingOut(result,'start',map),en=openingOut(result,'end',map);pts.unshift(st.p);pts.push(en.p);const sw=Math.max(9,820/Math.sqrt(result.topo.count)*.32);s+='<polyline points="'+pts.map(p=>p[0].toFixed(1)+','+p[1].toFixed(1)).join(' ')+'" fill="none" stroke="#555" stroke-width="'+sw+'" stroke-linecap="round" stroke-linejoin="round" opacity=".76"/>'} const A=openingOut(result,'start',map),B=openingOut(result,'end',map);function lab(o,text){const x=o.p[0]+o.dir[0]*62,y=o.p[1]+o.dir[1]*62;return '<text x="'+x+'" y="'+y+'" text-anchor="middle" dominant-baseline="central" font-family="Arial,Helvetica,sans-serif" font-size="23" font-weight="800" fill="#111">'+text+'</text>'}s+=lab(A,'START')+lab(B,'FINISH')+'</svg>';return s} function studentFieldsHtml(){const a=[];if(els.showName.checked)a.push(['Naam',66]);if(els.showGroup.checked)a.push(['Groep',40]);if(els.showDate.checked)a.push(['Datum',46]);if(!a.length)return '';return '<div class="kkmz-print-student-row">'+a.map(([x,w])=>'<div class="kkmz-print-student-field"><span class="kkmz-print-student-label">'+x+'</span><span class="kkmz-print-student-line" style="width:'+w+'mm"></span></div>').join('')+'</div>'} function puzzleBlock(result,solutionMode){const mm=state.size==='small'?120:state.size==='large'?166:146;return '<div class="kkmz-print-puzzle">'+mazeSvg(result,solutionMode,mm)+'</div>'} function sheetHtml(solutionMode){if(!state.results)return '';const title=esc(els.title.value.trim()||'Doolhof'),r=state.results[0],body='<div class="kkmz-print-puzzles one">'+puzzleBlock(r,solutionMode)+'</div>',info=solutionMode?'Antwoordblad · '+shapeDisplay()+' · '+LABELS[state.style]:'Vind de weg van START naar FINISH.';return '<section class="kkmz-print-page"><div class="kkmz-print-content"><header class="kkmz-print-header"><div class="kkmz-print-heading"><div class="kkmz-print-kicker">Doolhof</div><h1 class="kkmz-print-title">'+title+'</h1></div><div class="kkmz-print-logo-box"><img class="kkmz-print-logo" src="'+window.__KKMZ_LOGO_DATA+'" alt="Klasklaar"></div></header>'+studentFieldsHtml()+'<p class="kkmz-print-instruction">'+esc(info)+'</p>'+body+'<div class="kkmz-print-footer">Klasklaar.nl</div></div></section>'} function renderPreview(){if(!state.results)return;if(state.activeTab==='solution'&&!els.includeSolution.checked)state.activeTab='student';qa('.tab').forEach(t=>t.classList.toggle('active',t.dataset.tab===state.activeTab));const solTab=q('.tab[data-tab="solution"]');if(solTab)solTab.style.display=els.includeSolution.checked?'':'none';try{els.stage.innerHTML='<div class="kkmz-preview-page-wrap kkmz-doc" id="kkmz-previewPageWrap">'+sheetHtml(state.activeTab==='solution')+'</div>';requestAnimationFrame(fitSheet)}catch(err){console.error(err);state.results=null;setStatus('error','Deze vormgeving is tegengehouden','De geometrische controle vond een onveilige kruising. Klik op Nieuwe versie.');els.stage.innerHTML='<div class="empty-preview"><div><strong>Geen doolhof weergegeven</strong>De vormgeving was niet veilig genoeg.</div></div>'}} function fitSheet(){const wrap=q('#kkmz-previewPageWrap');if(!wrap)return;const page=wrap.querySelector('.kkmz-print-page');if(!page)return;wrap.style.transform='none';const stageW=els.stage.clientWidth,baseW=page.getBoundingClientRect().width;if(!stageW||!baseW)return;const scale=Math.min(1.42,Math.max(.25,(stageW-8)/baseW));wrap.style.transform='scale('+scale+')';wrap.style.left=Math.max(0,(stageW-baseW*scale)/2)+'px';wrap.style.top='0';els.stage.style.height=Math.ceil(page.getBoundingClientRect().height*scale+8)+'px'} qa('.shape-card').forEach(b=>b.addEventListener('click',()=>{state.shape=b.dataset.shape;selectButtons('.shape-card','shape',state.shape);updateShapeExtra();state.seed=makeSeed();updateMeta();generate()})); qa('.style-card').forEach(b=>b.addEventListener('click',()=>{state.style=b.dataset.style;selectButtons('.style-card','style',state.style);updateMeta();renderPreview()})); qa('.sizes .choice').forEach(b=>b.addEventListener('click',()=>{state.size=b.dataset.size;selectButtons('.sizes .choice','size',state.size);updateMeta();state.results&&renderPreview()})); qa('.corridors .choice').forEach(b=>b.addEventListener('click',()=>{state.density=b.dataset.density;selectButtons('.corridors .choice','density',state.density);state.seed=makeSeed();updateMeta();generate()})); qa('.levels .choice').forEach(b=>b.addEventListener('click',()=>{state.difficulty=b.dataset.difficulty;selectButtons('.levels .choice','difficulty',state.difficulty);state.seed=makeSeed();updateMeta();generate()})); [els.letter,els.digit,els.figure].forEach(x=>x.addEventListener('change',()=>{state.seed=makeSeed();updateMeta();generate()})); els.title.addEventListener('input',()=>state.results&&renderPreview());[els.showName,els.showGroup,els.showDate].forEach(x=>x.addEventListener('change',()=>state.results&&renderPreview()));els.includeSolution.addEventListener('change',()=>{if(!els.includeSolution.checked&&state.activeTab==='solution')state.activeTab='student';state.results&&renderPreview()});els.newBtn.addEventListener('click',()=>{state.seed=makeSeed();generate()});qa('.tab').forEach(t=>t.addEventListener('click',()=>{if(t.dataset.tab==='solution'&&!els.includeSolution.checked)return;state.activeTab=t.dataset.tab;renderPreview()}));window.addEventListener('resize',()=>requestAnimationFrame(fitSheet)); function printWorksheet(){if(!state.results||state.generating)return;let pages;try{pages=[sheetHtml(false)];if(els.includeSolution.checked)pages.push(sheetHtml(true))}catch(err){console.error(err);setStatus('error','Printen is tegengehouden','De geometrische eindcontrole kon deze versie niet bevestigen.');return}const printLayer=document.createElement('div');printLayer.id='kkmz-print-sandbox';printLayer.className='kkmz-doc';printLayer.innerHTML=pages.join('');const sourceStyle=document.getElementById('kkmz-maze-style'),tempStyle=document.createElement('style');tempStyle.id='kkmz-print-temp-style';tempStyle.textContent=(sourceStyle?sourceStyle.textContent:'')+'\n@page{size:A4 portrait;margin:0}html,body{margin:0!important;padding:0!important;background:#fff!important;width:210mm!important;min-width:210mm!important;max-width:210mm!important}#kkmz-print-sandbox{display:block!important;margin:0!important;padding:0!important;width:210mm!important;background:#fff!important}#kkmz-print-sandbox .kkmz-print-page{display:block!important;width:210mm!important;height:297mm!important;box-shadow:none!important;margin:0!important;overflow:hidden!important}';document.head.appendChild(tempStyle);const body=document.body,oldClass=body.className,oldStyle=body.getAttribute('style'),oldTitle=document.title,stash=document.createDocumentFragment();Array.from(body.childNodes).forEach(node=>stash.appendChild(node));body.className='kkmz-print-isolated';body.removeAttribute('style');document.title='';body.appendChild(printLayer);let cleaned=false;const cleanup=()=>{if(cleaned)return;cleaned=true;try{printLayer.remove()}catch(_e){}while(body.firstChild)body.removeChild(body.firstChild);body.appendChild(stash);body.className=oldClass;document.title=oldTitle;if(oldStyle===null)body.removeAttribute('style');else body.setAttribute('style',oldStyle);tempStyle.remove();requestAnimationFrame(fitSheet)};window.addEventListener('afterprint',cleanup,{once:true});setTimeout(()=>{try{window.print()}catch(err){console.error(err)}finally{setTimeout(cleanup,0)}},80)}els.print.addEventListener('click',printWorksheet); selectButtons('.shape-card','shape',state.shape);selectButtons('.style-card','style',state.style);selectButtons('.levels .choice','difficulty',state.difficulty);selectButtons('.sizes .choice','size',state.size);selectButtons('.corridors .choice','density',state.density);updateShapeExtra();updateMeta();window.__KKMZ_DEBUG={state,E,mazeSvg,organicGeometry,windingGeometry,generateNow:generate,renderPreview};generate(); })();
${body}
${SITE_TEXT}
${rows}