fix: herstel gum en kleurplaatlagen (v0.4.72-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 36s

- 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
This commit is contained in:
Ramon 2026-07-29 14:49:34 +02:00
parent 724eb6a33c
commit 03bfb9feda
5 changed files with 45 additions and 44 deletions

View file

@ -1 +1 @@
0.4.71-beta
0.4.72-beta

View file

@ -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}

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.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",

View file

@ -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;

View file

@ -133,7 +133,7 @@ test('elk leerjaar heeft zeven herkenbare kleurplaten met diverse themas 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[^>]+><\/canvas><div class="coloring-ink" aria-hidden="true"><\/div>/);
assert.match(source,/coloringInkOverlay\(svg,"coloring-ink-halo"\)/);
assert.match(source,/coloringInkOverlay\(svg,"coloring-ink-overlay"\)/);
assert.match(source,/coloringInkOverlay\(svg\)/);
assert.match(source,/const overlay=svg\.cloneNode\(true\)/);
assert.match(source,/document\.createElementNS\(svgNS,"feColorMatrix"\)/);
assert.match(source,/document\.createElementNS\(svgNS,"feMorphology"\)/);
assert.match(source,/spread\.setAttribute\("operator","dilate"\)/);
assert.match(source,/flood\.setAttribute\("flood-color",className==="coloring-ink-halo"\?"#ffffff":"#1c293d"\)/);
assert.match(source,/overlay\.querySelectorAll\("\.coloring-detail,\.coloring-recognition"\)\.forEach\(node=>node\.remove\(\)\)/);
assert.match(source,/node\.removeAttribute\("data-fillable"\)/);
assert.match(source,/node\.setAttribute\("fill","#fff"\)/);
assert.match(source,/drawing\.setAttribute\("filter",`url\(#\$\{filterId\}\)`\)/);
assert.match(source,/inkHost\.replaceChildren\(coloringInkOverlay\(svg,"coloring-ink-halo"\),coloringInkOverlay\(svg,"coloring-ink-overlay"\)\)/);
assert.match(source,/inkHost\.replaceChildren\(coloringInkOverlay\(svg\)\)/);
assert.doesNotMatch(source,/coloring-ink-halo|feMorphology|feColorMatrix/);
assert.match(source,/class="coloring-region" data-fillable="true"/);
assert.match(source,/class="coloring-sheet"[^>]*preserveAspectRatio="xMidYMid meet"/);
assert.match(source,/fill-rule="evenodd" clip-rule="evenodd"[^>]*stroke-width="5\.6"[^>]*stroke-miterlimit="2"/);
assert.match(css,/\.coloring-svg-lines\{shape-rendering:geometricPrecision\}/);
assert.match(css,/\.coloring-region\[data-role="backdrop"\]\{stroke-width:3\.2\}/);
assert.match(css,/\.coloring-art\{z-index:1\}\.coloring-paper canvas\{z-index:2\}\.coloring-ink\{z-index:3;pointer-events:none\}/);
assert.match(css,/\.coloring-ink-svg \.coloring-region\{fill:#fff!important;stroke:#000;stroke-width:5\.6;stroke-opacity:1\}/);
assert.match(css,/\.coloring-ink-svg \.coloring-detail\{fill:none!important;stroke:#000!important;stroke-width:2\.9;stroke-opacity:1\}/);
assert.match(css,/\.coloring-ink-svg \.coloring-ink-dot\{fill:#000!important;stroke:none!important\}/);
assert.match(css,/\.coloring-svg-lines \.coloring-recognition\{display:none\}/);
assert.match(css,/\.coloring-ink-overlay\{pointer-events:none;shape-rendering:geometricPrecision;mix-blend-mode:multiply\}/);
assert.match(css,/\.coloring-ink-svg \.coloring-region\{fill:#fff!important;stroke:var\(--coloring-ink\);stroke-width:5\.6;stroke-opacity:1\}/);
assert.doesNotMatch(css,/coloring-ink-halo|\.coloring-ink-svg \.coloring-detail/);
assert.match(css,/\.coloring-app\[data-tool="bucket"\] \.coloring-paper canvas\{pointer-events:none\}/);
assert.match(source,/canvas\.style\.pointerEvents=tool==="bucket"\?"none":"auto";art\.style\.pointerEvents=tool==="bucket"\?"auto":"none"/);
assert.match(source,/className="coloring-tool-label"/);
@ -172,11 +169,15 @@ test('kwast reageert op pendruk en transparante inktlagen houden lijnen zichtbaa
assert.match(css,/\.coloring-tool-buttons button\{width:auto;min-width:72px/);
});
test('verfemmer vult een SVG-vak in één keer en state bewaart kwast én vlakkleuren',async()=>{
test('verfemmer vult een SVG-vak en de gum wist zowel kwast als opgeslagen vlakkleur',async()=>{
const {source}=await coloringContext();
assert.match(source,/event\.target\.closest\("\[data-fillable\]\[data-part\]\"\)/);
assert.match(source,/part\.setAttribute\("fill",color\)/);
assert.match(source,/fills\[part\.dataset\.part\]=color/);
assert.match(source,/document\.elementFromPoint\(event\.clientX,event\.clientY\)/);
assert.match(source,/Object\.prototype\.hasOwnProperty\.call\(fills,part\.dataset\.part\)/);
assert.match(source,/delete fills\[part\.dataset\.part\];part\.setAttribute\("fill","#fff"\)/);
assert.match(source,/if\(erase\)eraseFillAt\(event\)/);
assert.match(source,/canvas\.toDataURL\("image\/png"\)/);
assert.match(source,/return\{theme,pageId,fills:\{\.\.\.fills\},ink,tool,color,size,completed,yearGroup,level/);
assert.match(source,/opts\.onProgress/);