diff --git a/VERSION b/VERSION index c33f5b4..0ba71ab 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.69-beta +0.4.70-beta diff --git a/public/css/coloring.css b/public/css/coloring.css index cdd21c2..d795172 100644 --- a/public/css/coloring.css +++ b/public/css/coloring.css @@ -32,22 +32,24 @@ .coloring-svg-lines .coloring-detail{stroke:var(--coloring-detail);stroke-width:3.6;stroke-opacity:.94;pointer-events:none} .coloring-svg-lines .coloring-detail[data-level="3"]{stroke-width:3;stroke-opacity:.88} .coloring-svg-lines .coloring-recognition{pointer-events:none} -.coloring-ink-halo,.coloring-ink-overlay{pointer-events:none;shape-rendering:geometricPrecision}.coloring-ink-halo{mix-blend-mode:screen}.coloring-ink-overlay{mix-blend-mode:multiply} -.coloring-ink-halo .coloring-region{fill:#000!important;stroke:#fff;stroke-width:10.25;stroke-opacity:.96} +.coloring-ink-halo,.coloring-ink-overlay{pointer-events:none;shape-rendering:geometricPrecision} +.coloring-ink-halo .coloring-region{fill:none!important;stroke:#fff;stroke-width:10.25;stroke-opacity:.96} .coloring-ink-halo .coloring-region[data-role="backdrop"]{stroke-width:7.25} .coloring-ink-halo .coloring-region[data-level="2"]{stroke-width:9.65} .coloring-ink-halo .coloring-region[data-level="3"]{stroke-width:8.85} .coloring-ink-halo .coloring-detail{fill:none!important;stroke:#fff!important;stroke-width:7.2;stroke-opacity:.96} .coloring-ink-halo .coloring-detail[data-level="3"]{stroke-width:6.35} .coloring-ink-halo .coloring-ink-dot{fill:#fff!important;stroke:#fff!important;stroke-width:4.5} -.coloring-ink-overlay .coloring-region{fill:#fff!important;stroke:var(--coloring-ink);stroke-width:6.5;stroke-opacity:1} +.coloring-ink-overlay .coloring-region{fill:none!important;stroke:var(--coloring-ink);stroke-width:6.5;stroke-opacity:1} .coloring-ink-overlay .coloring-region[data-role="backdrop"]{stroke-width:4.25} .coloring-ink-overlay .coloring-region[data-level="2"]{stroke-width:5.9} .coloring-ink-overlay .coloring-region[data-level="3"]{stroke-width:5.15} .coloring-ink-overlay .coloring-detail{fill:none!important;stroke:var(--coloring-detail)!important;stroke-width:3.8;stroke-opacity:1} .coloring-ink-overlay .coloring-detail[data-level="3"]{stroke-width:3.15} .coloring-ink-overlay .coloring-ink-dot{fill:var(--coloring-ink)!important;stroke:none!important} -.coloring-paper canvas{touch-action:none;cursor:crosshair;mix-blend-mode:multiply} +.coloring-paper canvas{touch-action:none;cursor:crosshair} +.coloring-app[data-tool="brush"] .coloring-paper canvas{cursor:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Ctext y='25' font-size='24'%3E%F0%9F%96%8C%EF%B8%8F%3C/text%3E%3C/svg%3E") 5 25,crosshair} +.coloring-app[data-tool="bucket"] .coloring-paper canvas{pointer-events:none} .coloring-app[data-tool="bucket"] .coloring-art{cursor:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Ctext y='24' font-size='23'%3E%F0%9F%AA%A3%3C/text%3E%3C/svg%3E") 6 24,pointer} .coloring-app[data-tool="bucket"] .coloring-art [data-fillable]{pointer-events:all} .coloring-app[data-tool="bucket"] .coloring-art [data-fillable]:hover{fill-opacity:.82} diff --git a/public/js/core.js b/public/js/core.js index 07c97f4..1567e95 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -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.69-beta"; +const VERSION = "0.4.70-beta"; /* Safari en oudere mobiele browsers behandelen 100vh/100% soms als de hoogte achter hun adres- en navigatiebalk. visualViewport geeft de werkelijk zichtbare hoogte; de CSS gebruikt deze pixelwaarde als betrouwbare bron en @@ -49,11 +49,11 @@ const I18N = { wg_notes:"Notities", wg_notes_d:"Een vel om op te schrijven.", wg_timer:"Timer", wg_timer_d:"Afteltimer met kijkklok voor de klas.", wg_write:"Schrijfblad", wg_write_d:"Schrijven en rekenen: pen, gum en papier met schrijflijnen of rekenblokjes.", - wg_coloring:"Kleurplaten", wg_coloring_d:"Kies thematische kleurplaten op leerjaar en niveau; kleur met een echt werkend kleurpotlood of vul een vak met de verfemmer.", + wg_coloring:"Kleurplaten", wg_coloring_d:"Kies thematische kleurplaten op leerjaar en niveau; schilder met een kwast of vul een vak met de verfemmer.", colorTheme:"Thema", colorPage:"Kleurplaat", colorProfile:"Groep {year} · niveau {level}", colorThemeAll:"Alle thema's", colorThemeAnimals:"Dieren", colorThemeSeasons:"Seizoenen", colorThemeNature:"Natuur", colorThemeScience:"Wetenschap & techniek", colorThemeCulture:"Cultuur & omgeving", - colorToolPencil:"Kleurpotlood", colorToolBucket:"Verfemmer", - colorPencilHint:"Teken meerdere keren over dezelfde plek om de kleur donkerder te maken, net als met een echt kleurpotlood.", colorBucketHint:"Tik op een omsloten vak om het in één keer te kleuren.", colorEraserHint:"Veeg alleen de potloodlaag weg; ingekleurde vakken blijven staan.", + colorToolBrush:"Kwast", colorToolBucket:"Verfemmer", + colorBrushHint:"Schilder vloeiende streken met de kwast; de contourlijnen blijven altijd zichtbaar.", colorBucketHint:"Tik op een omsloten vak om het in één keer te kleuren.", colorEraserHint:"Veeg alleen de kwastlaag weg; ingekleurde vakken blijven staan.", colorReset:"Kleurplaat leegmaken", colorDone:"Ik ben klaar", colorFinished:"Kleurplaat klaar!", bgBlank:"Leeg blad", bgLines:"Lijntjes", bgWrite:"Schrijflijnen", bgGridS:"Kleine rekenblokjes", bgGridB:"Grote rekenblokjes", @@ -479,11 +479,11 @@ const I18N = { wg_notes:"Notes", wg_notes_d:"A sheet to write on.", wg_timer:"Timer", wg_timer_d:"Countdown timer with a visual clock for class.", wg_write:"Writing pad", wg_write_d:"Writing and maths: pen, eraser and paper with handwriting lines or maths squares.", - wg_coloring:"Colouring pages", wg_coloring_d:"Choose themed colouring pages by year and level; use a realistic coloured pencil or fill an area with the paint bucket.", + wg_coloring:"Colouring pages", wg_coloring_d:"Choose themed colouring pages by year and level; paint with a brush or fill an area with the paint bucket.", colorTheme:"Theme", colorPage:"Colouring page", colorProfile:"Year {year} · level {level}", colorThemeAll:"All themes", colorThemeAnimals:"Animals", colorThemeSeasons:"Seasons", colorThemeNature:"Nature", colorThemeScience:"Science & technology", colorThemeCulture:"Culture & surroundings", - colorToolPencil:"Coloured pencil", colorToolBucket:"Paint bucket", - colorPencilHint:"Draw over the same spot to make the colour darker, just like a real coloured pencil.", colorBucketHint:"Tap an enclosed area to colour it in one go.", colorEraserHint:"Erase only the pencil layer; filled areas stay in place.", + colorToolBrush:"Brush", colorToolBucket:"Paint bucket", + colorBrushHint:"Paint smooth strokes with the brush; the outlines always stay visible.", colorBucketHint:"Tap an enclosed area to colour it in one go.", colorEraserHint:"Erase only the brush layer; filled areas stay in place.", colorReset:"Clear colouring page", colorDone:"I'm finished", colorFinished:"Colouring page finished!", bgBlank:"Blank sheet", bgLines:"Lines", bgWrite:"Handwriting lines", bgGridS:"Small maths squares", bgGridB:"Big maths squares", diff --git a/public/js/widgets/coloring.js b/public/js/widgets/coloring.js index 8ae5c8f..fa9c20c 100644 --- a/public/js/widgets/coloring.js +++ b/public/js/widgets/coloring.js @@ -1,4 +1,4 @@ -/* teach - thematische kleurplaten met gekleurd potlood en vlakvulling */ +/* teach - thematische kleurplaten met kwast en vlakvulling */ "use strict"; const COLORING_THEME_KEYS=Object.freeze({ @@ -346,7 +346,7 @@ function coloringPagesFor(yearGroup,theme="all"){ const year=Math.max(1,Math.min(8,Number(yearGroup)||4)); return COLORING_PAGES.filter(page=>year>=page.years[0]&&year<=page.years[1]&&(theme==="all"||page.theme===theme)); } -function coloringPencilWidth(size,pressure,pointerType){ +function coloringBrushWidth(size,pressure,pointerType){ const base=Math.max(2,Math.min(18,Number(size)||6)); return pointerType==="pen"&&pressure>0?base*(.45+pressure*1.2):base; } @@ -365,7 +365,8 @@ function coloringInkOverlay(svg,className){ function mountColoring(root,initState,opts={}){ const yearGroup=Math.max(1,Math.min(8,Number(initState&&initState.yearGroup)||4)); const level=Math.max(1,Math.min(3,Number(initState&&initState.level)||2)); - let tool=["pencil","bucket","eraser"].includes(initState&&initState.tool)?initState.tool:"pencil"; + const savedTool=initState&&initState.tool==="pencil"?"brush":initState&&initState.tool; + let tool=["brush","bucket","eraser"].includes(savedTool)?savedTool:"brush"; let color=coloringSafeColor(initState&&initState.color); let size=[4,7,12].includes(Number(initState&&initState.size))?Number(initState.size):7; let theme=COLORING_THEME_KEYS[initState&&initState.theme]?initState.theme:"all"; @@ -394,12 +395,12 @@ function mountColoring(root,initState,opts={}){ const art=root.querySelector(".coloring-art"),inkHost=root.querySelector(".coloring-ink"),paper=root.querySelector(".coloring-paper"),canvas=root.querySelector("canvas"),ctx=canvas.getContext("2d"); const toolHost=root.querySelector(".coloring-tool-buttons"),palette=root.querySelector(".coloring-palette"),sizes=root.querySelector(".coloring-sizes"); const custom=root.querySelector(".coloring-custom input"),reset=root.querySelector(".coloring-reset"),done=root.querySelector(".coloring-done"),hint=root.querySelector(".coloring-hint"); - const TOOLS=[["pencil","✏️","colorToolPencil"],["bucket","🪣","colorToolBucket"],["eraser","🧽","eraser"]]; + const TOOLS=[["brush","🖌️","colorToolBrush"],["bucket","🪣","colorToolBucket"],["eraser","🧽","eraser"]]; TOOLS.forEach(([id,icon])=>{const button=document.createElement("button");button.type="button";button.dataset.tool=id;button.textContent=icon;button.onclick=()=>{tool=id;refreshTools()};toolHost.appendChild(button)}); - COLORING_COLORS.forEach(value=>{const button=document.createElement("button");button.type="button";button.className="coloring-color";button.dataset.color=value;const dot=document.createElement("i");dot.style.background=value;button.appendChild(dot);button.onclick=()=>{color=value;custom.value=value;if(tool==="eraser")tool="pencil";refreshTools()};palette.appendChild(button)}); + COLORING_COLORS.forEach(value=>{const button=document.createElement("button");button.type="button";button.className="coloring-color";button.dataset.color=value;const dot=document.createElement("i");dot.style.background=value;button.appendChild(dot);button.onclick=()=>{color=value;custom.value=value;if(tool==="eraser")tool="brush";refreshTools()};palette.appendChild(button)}); [4,7,12].forEach(value=>{const button=document.createElement("button");button.type="button";button.className="coloring-size";button.dataset.size=value;const dot=document.createElement("i");dot.style.width=dot.style.height=(value+5)+"px";button.appendChild(dot);button.onclick=()=>{size=value;refreshTools()};sizes.appendChild(button)}); - custom.value=color;custom.oninput=()=>{color=coloringSafeColor(custom.value,color);if(tool==="eraser")tool="pencil";refreshTools()}; + custom.value=color;custom.oninput=()=>{color=coloringSafeColor(custom.value,color);if(tool==="eraser")tool="brush";refreshTools()}; function pageTitle(page){return page.title[LANG]||page.title.nl} function eligibleThemes(){return [...new Set(coloringPagesFor(yearGroup).map(page=>page.theme))]} @@ -441,7 +442,7 @@ function mountColoring(root,initState,opts={}){ custom.parentElement.querySelector("span").textContent=T("customColor"); reset.title=T("colorReset");reset.setAttribute("aria-label",T("colorReset")); done.textContent=completed?"✓ "+T("colorFinished"):T("colorDone"); - hint.textContent=T(tool==="bucket"?"colorBucketHint":tool==="eraser"?"colorEraserHint":"colorPencilHint"); + hint.textContent=T(tool==="bucket"?"colorBucketHint":tool==="eraser"?"colorEraserHint":"colorBrushHint"); const selected=pageId;rebuildPageOptions(selected); } function refreshTools(){ @@ -449,7 +450,7 @@ function mountColoring(root,initState,opts={}){ palette.querySelectorAll("button").forEach(button=>button.classList.toggle("on",button.dataset.color===color&&tool!=="eraser")); sizes.querySelectorAll("button").forEach(button=>{button.classList.toggle("on",Number(button.dataset.size)===size);button.title=T("penSize")}); app.dataset.tool=tool;canvas.style.pointerEvents=tool==="bucket"?"none":"auto";art.style.pointerEvents=tool==="bucket"?"auto":"none"; - hint.textContent=T(tool==="bucket"?"colorBucketHint":tool==="eraser"?"colorEraserHint":"colorPencilHint"); + hint.textContent=T(tool==="bucket"?"colorBucketHint":tool==="eraser"?"colorEraserHint":"colorBrushHint"); } themeSel.onchange=()=>{theme=themeSel.value;rebuildPageOptions("");renderPage(false)}; pageSel.onchange=()=>{pageId=pageSel.value;renderPage(false)}; @@ -460,15 +461,14 @@ function mountColoring(root,initState,opts={}){ const point=event=>{const rect=canvas.getBoundingClientRect();return{x:(event.clientX-rect.left)*canvas.width/rect.width,y:(event.clientY-rect.top)*canvas.height/rect.height}}; let drawing=false,last=null,mid=null; - function pencilSegment(next,event){ - const width=coloringPencilWidth(size,event.pressure,event.pointerType),erase=tool==="eraser"; - ctx.save();ctx.globalCompositeOperation=erase?"destination-out":"source-over";ctx.strokeStyle=color;ctx.globalAlpha=erase?1:.27;ctx.lineCap="round";ctx.lineJoin="round";ctx.lineWidth=erase?width*3.5:width; + function brushSegment(next,event){ + const width=coloringBrushWidth(size,event.pressure,event.pointerType),erase=tool==="eraser"; + ctx.save();ctx.globalCompositeOperation=erase?"destination-out":"source-over";ctx.strokeStyle=color;ctx.globalAlpha=erase?1:.72;ctx.lineCap="round";ctx.lineJoin="round";ctx.lineWidth=erase?width*3.5:width; const nextMid={x:(last.x+next.x)/2,y:(last.y+next.y)/2};ctx.beginPath();ctx.moveTo(mid?mid.x:last.x,mid?mid.y:last.y);ctx.quadraticCurveTo(last.x,last.y,nextMid.x,nextMid.y);ctx.stroke(); - if(!erase){for(let n=0;n<2;n++){const jitter=(Math.random()-.5)*width*.65;ctx.globalAlpha=.07;ctx.lineWidth=Math.max(1,width*.28);ctx.beginPath();ctx.moveTo(last.x+jitter,last.y-jitter);ctx.lineTo(next.x-jitter,next.y+jitter);ctx.stroke()}} ctx.restore();last=next;mid=nextMid; } - canvas.addEventListener("pointerdown",event=>{if(tool==="bucket"||(event.pointerType==="mouse"&&event.button!==0))return;event.preventDefault();canvas.setPointerCapture(event.pointerId);drawing=true;last=point(event);mid=null;pencilSegment({x:last.x+.01,y:last.y+.01},event);completed=false;updateTexts()}); - canvas.addEventListener("pointermove",event=>{if(!drawing)return;(event.getCoalescedEvents?event.getCoalescedEvents():[event]).forEach(sample=>pencilSegment(point(sample),sample))}); + canvas.addEventListener("pointerdown",event=>{if(tool==="bucket"||(event.pointerType==="mouse"&&event.button!==0))return;event.preventDefault();canvas.setPointerCapture(event.pointerId);drawing=true;last=point(event);mid=null;brushSegment({x:last.x+.01,y:last.y+.01},event);completed=false;updateTexts()}); + canvas.addEventListener("pointermove",event=>{if(!drawing)return;(event.getCoalescedEvents?event.getCoalescedEvents():[event]).forEach(sample=>brushSegment(point(sample),sample))}); const stop=()=>{drawing=false;last=mid=null};canvas.addEventListener("pointerup",stop);canvas.addEventListener("pointercancel",stop); reset.onclick=()=>{fills={};completed=false;clearInk();renderPage(false)}; done.onclick=()=>{completed=true;updateTexts();if(typeof opts.onProgress==="function")opts.onProgress({attempts:1,correct:1,stars:1,detail:{question:pageTitle(COLORING_PAGES.find(page=>page.id===pageId)),answer:T("colorFinished"),correct:true}})}; diff --git a/test/coloring.test.js b/test/coloring.test.js index 9b3fa7d..7646bef 100644 --- a/test/coloring.test.js +++ b/test/coloring.test.js @@ -13,7 +13,7 @@ async function coloringContext(){ pages:COLORING_PAGES, themes:COLORING_THEME_KEYS, pagesFor:coloringPagesFor, - pencilWidth:coloringPencilWidth, + brushWidth:coloringBrushWidth, safeColor:coloringSafeColor }`,context); return{source,api:context.__coloring}; @@ -34,7 +34,7 @@ test('kleurplatenwidget laadt vóór het bord en is als creatieve leerlingwidget assert.match(pupil,/PUPIL_WIDGET_CATS\s*=\s*\[[^\]]*"creatief"/); assert.match(cast,/CAST_WIDGET_CATEGORIES=new Set\(\[[^\]]*"creatief"/); assert.match(css,/\.coloring-paper[^}]*aspect-ratio:4\/3/); - for(const key of ['wg_coloring','wg_coloring_d','catCreative','colorToolPencil','colorToolBucket','colorBucketHint','colorPencilHint']){ + for(const key of ['wg_coloring','wg_coloring_d','catCreative','colorToolBrush','colorToolBucket','colorBucketHint','colorBrushHint']){ assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key); } }); @@ -96,15 +96,17 @@ test('elk leerjaar heeft zeven herkenbare kleurplaten met diverse thema’s en d assert.equal(api.pagesFor(3,'science').length,1); }); -test('potlood reageert op echte pendruk en de bovenste inktlaag houdt lijnen zichtbaar',async()=>{ +test('kwast reageert op pendruk en transparante inktlagen houden lijnen zichtbaar',async()=>{ const {source,api}=await coloringContext(); - assert.ok(api.pencilWidth(7,.9,'pen')>api.pencilWidth(7,.2,'pen')); - assert.equal(api.pencilWidth(7,0,'mouse'),7); + assert.ok(api.brushWidth(7,.9,'pen')>api.brushWidth(7,.2,'pen')); + assert.equal(api.brushWidth(7,0,'mouse'),7); assert.equal(api.safeColor('#AABBCC'),'#aabbcc'); assert.equal(api.safeColor('red'),'#e0554d'); - for(const feature of ['getCoalescedEvents','event.pressure','erase?1:.27','quadraticCurveTo','Math.random()'])assert.ok(source.includes(feature),feature); + for(const feature of ['getCoalescedEvents','event.pressure','erase?1:.72','quadraticCurveTo'])assert.ok(source.includes(feature),feature); + assert.match(source,/const savedTool=initState&&initState\.tool==="pencil"\?"brush"/); + assert.match(source,/\["brush","🖌️","colorToolBrush"\]/); const css=await readFile('public/css/coloring.css','utf8'); - assert.match(css,/mix-blend-mode:multiply/); + assert.doesNotMatch(css,/mix-blend-mode/); assert.match(source,/class="coloring-art"><\/div>]+><\/canvas>