From 03bfb9fedaf72de6752d0bc713e6237d3cf2501a Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 29 Jul 2026 14:49:34 +0200 Subject: [PATCH] fix: herstel gum en kleurplaatlagen (v0.4.72-beta) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - laat de gum ook verfpotvullingen bij tikken en slepen verwijderen - vervang dubbele filterlagen door één gemaskeerde contourlaag zonder witte halo - verberg overlappende herkenningshulplijnen en dek achterliggende contouren correct af - voeg regressietests toe voor gum, vulranden en lijnlagen --- VERSION | 2 +- public/css/coloring.css | 9 +++----- public/js/core.js | 6 ++--- public/js/widgets/coloring.js | 43 +++++++++++++++++++---------------- test/coloring.test.js | 29 +++++++++++------------ 5 files changed, 45 insertions(+), 44 deletions(-) diff --git a/VERSION b/VERSION index d0f1f76..4efd0fa 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.71-beta +0.4.72-beta diff --git a/public/css/coloring.css b/public/css/coloring.css index 331a205..db1750e 100644 --- a/public/css/coloring.css +++ b/public/css/coloring.css @@ -33,15 +33,12 @@ .coloring-svg-lines .coloring-region[data-level="3"]{stroke-width:4.55} .coloring-svg-lines .coloring-detail{stroke:var(--coloring-detail);stroke-width:2.9;stroke-opacity:.94;pointer-events:none} .coloring-svg-lines .coloring-detail[data-level="3"]{stroke-width:2.45;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-svg .coloring-region{fill:#fff!important;stroke:#000;stroke-width:5.6;stroke-opacity:1} +.coloring-svg-lines .coloring-recognition{display:none} +.coloring-ink-overlay{pointer-events:none;shape-rendering:geometricPrecision;mix-blend-mode:multiply} +.coloring-ink-svg .coloring-region{fill:#fff!important;stroke:var(--coloring-ink);stroke-width:5.6;stroke-opacity:1} .coloring-ink-svg .coloring-region[data-role="backdrop"]{stroke-width:3.2} .coloring-ink-svg .coloring-region[data-level="2"]{stroke-width:5.1} .coloring-ink-svg .coloring-region[data-level="3"]{stroke-width:4.55} -.coloring-ink-svg .coloring-detail{fill:none!important;stroke:#000!important;stroke-width:2.9;stroke-opacity:1} -.coloring-ink-svg .coloring-detail[data-level="3"]{stroke-width:2.45} -.coloring-ink-svg .coloring-ink-dot{fill:#000!important;stroke:none!important} .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} diff --git a/public/js/core.js b/public/js/core.js index f7b4347..963c496 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.71-beta"; +const VERSION = "0.4.72-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 @@ -53,7 +53,7 @@ const I18N = { 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", 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.", + 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 kwaststreken weg of tik en sleep over een gevuld vak om ook de verfpotkleur te wissen.", colorReset:"Kleurplaat leegmaken", colorDone:"Ik ben klaar", colorFinished:"Kleurplaat klaar!", bgBlank:"Leeg blad", bgLines:"Lijntjes", bgWrite:"Schrijflijnen", bgGridS:"Kleine rekenblokjes", bgGridB:"Grote rekenblokjes", @@ -483,7 +483,7 @@ const I18N = { 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", 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.", + 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 brush strokes, or tap and drag across a filled area to remove its paint-bucket colour too.", 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 fb182c1..4a30742 100644 --- a/public/js/widgets/coloring.js +++ b/public/js/widgets/coloring.js @@ -358,28 +358,12 @@ function coloringBrushWidth(size,pressure,pointerType){ function coloringSafeColor(value,fallback=COLORING_COLORS[1]){ return /^#[0-9a-f]{6}$/i.test(String(value||""))?String(value).toLowerCase():fallback; } -let COLORING_INK_ID=0; -function coloringInkOverlay(svg,className){ - const overlay=svg.cloneNode(true);overlay.setAttribute("class",`coloring-ink-svg ${className}`); - const filterId=`coloring-ink-filter-${++COLORING_INK_ID}`,svgNS="http://www.w3.org/2000/svg"; - const defs=document.createElementNS(svgNS,"defs"),filter=document.createElementNS(svgNS,"filter"); - filter.setAttribute("id",filterId);filter.setAttribute("x","-4%");filter.setAttribute("y","-4%");filter.setAttribute("width","108%");filter.setAttribute("height","108%");filter.setAttribute("color-interpolation-filters","sRGB"); - const alpha=document.createElementNS(svgNS,"feColorMatrix"); - alpha.setAttribute("in","SourceGraphic");alpha.setAttribute("result","ink-alpha");alpha.setAttribute("type","matrix"); - alpha.setAttribute("values","0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -.2126 -.7152 -.0722 1 0");filter.appendChild(alpha); - let mask="ink-alpha"; - if(className==="coloring-ink-halo"){ - const spread=document.createElementNS(svgNS,"feMorphology");spread.setAttribute("in",mask);spread.setAttribute("operator","dilate");spread.setAttribute("radius","1.8");spread.setAttribute("result","ink-spread");filter.appendChild(spread);mask="ink-spread"; - } - const flood=document.createElementNS(svgNS,"feFlood"),composite=document.createElementNS(svgNS,"feComposite"); - flood.setAttribute("flood-color",className==="coloring-ink-halo"?"#ffffff":"#1c293d");flood.setAttribute("result","ink-color"); - composite.setAttribute("in","ink-color");composite.setAttribute("in2",mask);composite.setAttribute("operator","in"); - filter.append(flood,composite);defs.appendChild(filter);overlay.prepend(defs); - overlay.querySelectorAll(".coloring-detail[stroke=\"none\"]").forEach(node=>node.classList.add("coloring-ink-dot")); +function coloringInkOverlay(svg){ + const overlay=svg.cloneNode(true);overlay.setAttribute("class","coloring-ink-svg coloring-ink-overlay"); + overlay.querySelectorAll(".coloring-detail,.coloring-recognition").forEach(node=>node.remove()); overlay.querySelectorAll("[data-fillable]").forEach(node=>{ node.removeAttribute("data-fillable");node.removeAttribute("data-part");node.setAttribute("fill","#fff"); }); - const drawing=overlay.querySelector(".coloring-svg-lines");if(drawing)drawing.setAttribute("filter",`url(#${filterId})`); return overlay; } @@ -451,7 +435,7 @@ function mountColoring(root,initState,opts={}){ svg.querySelectorAll("[data-fillable][data-part]").forEach(node=>{ const saved=coloringSafeColor(fills[node.dataset.part],"");if(saved)node.setAttribute("fill",saved); }); - inkHost.replaceChildren(coloringInkOverlay(svg,"coloring-ink-halo"),coloringInkOverlay(svg,"coloring-ink-overlay")); + inkHost.replaceChildren(coloringInkOverlay(svg)); if(keepState&&restoreInk){loadInk(restoreInk);restoreInk=null} refreshTools();updateTexts(); } @@ -482,10 +466,29 @@ function mountColoring(root,initState,opts={}){ part.setAttribute("fill",color);fills[part.dataset.part]=color;completed=false;updateTexts(); }); + function partBelow(event){ + const canvasEvents=canvas.style.pointerEvents,artEvents=art.style.pointerEvents; + let part=null; + try{ + canvas.style.pointerEvents="none";art.style.pointerEvents="auto"; + const target=document.elementFromPoint(event.clientX,event.clientY); + part=target&&typeof target.closest==="function"?target.closest("[data-fillable][data-part]"):null; + }finally{ + canvas.style.pointerEvents=canvasEvents;art.style.pointerEvents=artEvents; + } + return part&&art.contains(part)&&!part.hasAttribute("hidden")?part:null; + } + function eraseFillAt(event){ + if(tool!=="eraser")return false; + const part=partBelow(event);if(!part||!Object.prototype.hasOwnProperty.call(fills,part.dataset.part))return false; + delete fills[part.dataset.part];part.setAttribute("fill","#fff");completed=false;return true; + } + 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 brushSegment(next,event){ const width=coloringBrushWidth(size,event.pressure,event.pointerType),erase=tool==="eraser"; + if(erase)eraseFillAt(event); 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(); ctx.restore();last=next;mid=nextMid; diff --git a/test/coloring.test.js b/test/coloring.test.js index 22a18e8..88f5ed1 100644 --- a/test/coloring.test.js +++ b/test/coloring.test.js @@ -133,7 +133,7 @@ test('elk leerjaar heeft zeven herkenbare kleurplaten met diverse thema’s en d assert.equal(api.pagesFor(3,'science').length,1); }); -test('kwast reageert op pendruk en transparante inktlagen houden lijnen zichtbaar',async()=>{ +test('kwast reageert op pendruk en één gemaskeerde contourlaag houdt alleen buitenlijnen zichtbaar',async()=>{ const {source,api}=await coloringContext(); assert.ok(api.brushWidth(7,.9,'pen')>api.brushWidth(7,.2,'pen')); assert.equal(api.brushWidth(7,0,'mouse'),7); @@ -143,28 +143,25 @@ test('kwast reageert op pendruk en transparante inktlagen houden lijnen zichtbaa 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.doesNotMatch(css,/mix-blend-mode/); + assert.match(css,/mix-blend-mode:multiply/); assert.match(source,/class="coloring-art"><\/div>]+><\/canvas>