feat: herontwerp kleurplaten professioneel (v0.4.71-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 40s

Gewijzigde functionaliteiten:
- Vervang losse geometrische dierdelen door vloeiende Bézier-silhouetten met geloofwaardiger anatomie.
- Schilder poten, staarten, vinnen, vleugels, wielen en andere appendages achter het hoofdsilhouet voor aansluitende contouren.
- Voeg een SVG-occlusiefilter toe dat alleen werkelijk zichtbare lijnen boven kwast en verfpot toont.
- Verfijn lijngewichten, witte contrastrand, wielkasten, manen en objectcontouren.
- Geef kwast, verfpot en gum zichtbare labels en verbeter ARIA-groepering van gereedschappen en palet.
- Voeg regressietests toe voor organische contouren, schildervolgorde, gesloten kleurvakken en UX-bediening.

Tests:
- npm test (201 geslaagd)
This commit is contained in:
Ramon 2026-07-29 14:27:26 +02:00
parent 33398c8594
commit 724eb6a33c
6 changed files with 170 additions and 105 deletions

View file

@ -1 +1 @@
0.4.70-beta
0.4.71-beta

View file

@ -7,6 +7,8 @@
.coloring-tools{gap:7px;background:color-mix(in srgb,var(--surface) 96%,var(--orange))}
.coloring-tool-buttons,.coloring-palette,.coloring-sizes{display:flex;align-items:center;gap:4px;padding:4px;border:1px solid color-mix(in srgb,var(--line) 78%,transparent);border-radius:13px;background:linear-gradient(180deg,color-mix(in srgb,var(--surface-2) 82%,#fff),var(--surface-2));box-shadow:inset 0 1px 0 rgba(255,255,255,.72),0 2px 7px rgba(30,50,90,.06)}
.coloring-tools button{width:36px;height:36px;display:grid;place-items:center;border:0;border-radius:10px;background:transparent;color:var(--ink);font:inherit;font-size:18px;cursor:pointer;transition:transform .14s ease,background .14s ease,box-shadow .14s ease}
.coloring-tool-buttons button{width:auto;min-width:72px;grid-auto-flow:column;grid-template-columns:auto auto;gap:5px;padding:0 9px}
.coloring-tool-icon{display:grid;place-items:center;font-size:18px;line-height:1}.coloring-tool-label{font-size:10px;font-weight:850;line-height:1}
.coloring-tools button:is(:hover,:focus-visible){transform:translateY(-1px);background:var(--surface);box-shadow:0 4px 10px rgba(30,50,90,.14)}
.coloring-tools button.on{background:var(--surface);box-shadow:0 2px 7px rgba(30,50,90,.16)}
.coloring-tool-buttons button.on{outline:2px solid var(--orange);outline-offset:-2px}
@ -25,28 +27,21 @@
.coloring-art svg,.coloring-ink svg{width:100%;height:100%;display:block}.coloring-art svg{background:#fff}.coloring-ink svg{position:absolute;inset:0;overflow:visible}
.coloring-art [hidden],.coloring-ink [hidden]{display:none}
.coloring-svg-lines{shape-rendering:geometricPrecision}
.coloring-svg-lines .coloring-region{paint-order:fill stroke;stroke-opacity:.98}
.coloring-svg-lines .coloring-region[data-role="backdrop"]{stroke-width:4.25}
.coloring-svg-lines .coloring-region[data-level="2"]{stroke-width:5.9}
.coloring-svg-lines .coloring-region[data-level="3"]{stroke-width:5.15}
.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-region{paint-order:fill stroke;stroke-width:5.6;stroke-opacity:.98}
.coloring-svg-lines .coloring-region[data-role="backdrop"]{stroke-width:3.2}
.coloring-svg-lines .coloring-region[data-level="2"]{stroke-width:5.1}
.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-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: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-ink-svg .coloring-region{fill:#fff!important;stroke:#000;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}
@ -62,6 +57,7 @@
.coloring-choices select{width:100%;max-width:none;height:32px}
.coloring-fit{width:100%;margin:0;text-align:center}
.coloring-tools button{width:32px;height:32px;font-size:16px}
.coloring-tool-buttons button{min-width:36px;width:36px;padding:0}.coloring-tool-label{display:none}
.coloring-color i{width:18px;height:18px}
.coloring-custom span{display:none}.coloring-custom input{width:32px;height:32px}
.coloring-tools .coloring-reset{margin-left:0}

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.70-beta";
const VERSION = "0.4.71-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

View file

@ -9,16 +9,16 @@ function sceneFarmyard(){return coloringScene(
coloringPart("circle","barn-window",'cx="190" cy="210" r="42"',2)+
coloringPart("rect","barn-door-left",'x="115" y="330" width="75" height="150"')+
coloringPart("rect","barn-door-right",'x="190" y="330" width="75" height="150"')+
coloringPart("ellipse","cow-body",'cx="535" cy="390" rx="155" ry="95"')+
coloringPart("path","cow-head",'d="M 620 315 Q 720 300 738 380 Q 710 460 620 425 Z"')+
coloringPart("ellipse","cow-snout",'cx="700" cy="400" rx="58" ry="35"',2)+
coloringPart("path","cow-tail",'d="M 410 355 C 360 350 335 390 350 438 C 358 465 375 480 392 462 C 402 450 390 430 377 435 C 365 402 382 385 420 388 Z"',2)+
[420,495,575,635].map((x,i)=>coloringPart("path",`cow-leg-${i}`,`d="M ${x} 420 C ${x-4} 462 ${x-8} 505 ${x-5} 545 Q ${x+12} 558 ${x+42} 548 L ${x+38} 532 C ${x+32} 495 ${x+35} 455 ${x+30} 420 Z"`,i>1?2:1)).join("")+
coloringPart("path","cow-body",'d="M 390 380 C 395 320 455 285 535 292 C 610 295 655 330 665 390 C 670 450 620 485 535 485 C 450 485 385 450 390 380 Z"')+
coloringPart("path","cow-ear-left",'d="M 645 330 Q 600 285 580 330 Q 610 360 650 355 Z"',2)+
coloringPart("path","cow-ear-right",'d="M 710 325 Q 750 280 775 325 Q 755 360 715 355 Z"',2)+
coloringPart("path","cow-horn-left",'d="M 648 322 Q 635 278 665 292 Q 680 315 663 338 Z"',2)+
coloringPart("path","cow-horn-right",'d="M 710 320 Q 728 278 748 298 Q 748 325 723 340 Z"',2)+
coloringPart("path","cow-tail",'d="M 405 370 Q 350 390 368 458 Q 386 478 402 448 Q 375 408 420 390 Z"',2)+
coloringPart("path","cow-head",'d="M 615 325 C 645 300 710 300 740 330 C 768 358 758 420 725 445 C 690 468 635 445 620 415 C 605 385 600 345 615 325 Z"')+
coloringPart("path","cow-snout",'d="M 645 390 C 660 365 720 365 742 392 C 755 415 733 442 700 445 C 665 445 638 420 645 390 Z"',2)+
coloringPart("ellipse","cow-udder",'cx="565" cy="458" rx="44" ry="25"',3)+
[420,500,575,640].map((x,i)=>coloringPart("rect",`cow-leg-${i}`,`x="${x}" y="445" width="35" height="105" rx="14"`)).join("")+
[[480,365,42,28],[555,420,35,25],[590,350,30,22]].map(([x,y,rx,ry],i)=>coloringPart("ellipse",`cow-spot-${i}`,`cx="${x}" cy="${y}" rx="${rx}" ry="${ry}"`,2)).join("")+
coloringPart("circle","farm-sun",'cx="690" cy="95" r="54"')+
coloringLine("path",'d="M 115 330 L 265 480 M 265 330 L 115 480 M 148 210 H 232 M 190 168 V 252" fill="none"')+
@ -90,11 +90,12 @@ function sceneForestPicnic(){return coloringScene(
coloringPart("path","basket-handle",'d="M 350 390 Q 400 290 455 390 L 430 390 Q 400 335 375 390 Z"',2)+
coloringPart("circle","apple",'cx="520" cy="455" r="34"')+
coloringPart("path","sandwich",'d="M 560 520 L 650 455 L 705 535 Z"',2)+
coloringPart("ellipse","rabbit-body",'cx="655" cy="320" rx="80" ry="65"')+
coloringPart("circle","rabbit-head",'cx="690" cy="245" r="58"')+
coloringPart("ellipse","rabbit-ear-a",'cx="665" cy="160" rx="25" ry="75" transform="rotate(-15 665 160)"')+
coloringPart("ellipse","rabbit-ear-b",'cx="720" cy="160" rx="25" ry="75" transform="rotate(15 720 160)"')+
coloringPart("circle","rabbit-tail",'cx="575" cy="315" r="38"',2)+
coloringPart("path","rabbit-tail",'d="M 595 295 C 560 280 540 305 550 335 C 558 360 590 365 610 342 C 622 325 615 305 595 295 Z"',2)+
coloringPart("path","rabbit-ear-a",'d="M 665 218 C 640 175 635 92 662 78 C 692 88 695 170 684 222 Z"')+
coloringPart("path","rabbit-ear-b",'d="M 704 215 C 700 165 715 90 744 88 C 767 110 745 188 724 226 Z"')+
coloringPart("path","rabbit-body",'d="M 575 330 C 580 280 620 255 668 268 C 720 278 750 315 738 360 C 725 405 675 430 625 412 C 590 400 570 368 575 330 Z"')+
coloringPart("path","rabbit-hind-leg",'d="M 620 365 C 650 345 700 360 710 398 C 688 430 630 432 600 402 C 598 385 605 373 620 365 Z"',2)+
coloringPart("path","rabbit-head",'d="M 645 245 C 645 205 670 180 705 185 C 745 190 765 225 752 262 C 742 298 700 315 668 292 C 652 280 645 264 645 245 Z"')+
coloringLine("path",'d="M 135 150 Q 170 190 205 150 M 150 235 Q 180 265 210 235" fill="none"',2)+
coloringLine("path",'d="M 365 410 H 450 M 380 440 H 445 M 395 470 H 440" fill="none"',3)+
coloringLine("circle",'cx="705" cy="235" r="6" fill="#26344a" stroke="none"')+
@ -108,12 +109,12 @@ function sceneDinosaurValley(){return coloringScene(
coloringPart("path","volcano",'d="M 35 430 L 175 145 L 320 430 Z"')+
coloringPart("path","lava",'d="M 135 225 L 175 145 L 220 235 L 205 320 L 170 275 L 145 335 Z"',2)+
coloringPart("circle","smoke-a",'cx="145" cy="105" r="45"',2)+coloringPart("circle","smoke-b",'cx="205" cy="75" r="55"',3)+
coloringPart("ellipse","dino-body",'cx="500" cy="380" rx="170" ry="95"')+
coloringPart("path","dino-neck",'d="M 575 345 Q 610 170 690 155 Q 750 160 742 225 Q 690 255 660 235 Q 650 330 625 405 Z"')+
coloringPart("ellipse","dino-head",'cx="705" cy="190" rx="72" ry="52"')+
coloringPart("path","dino-tail",'d="M 350 360 Q 225 300 250 390 Q 290 445 370 425 Z"')+
[390,475,555,615].map((x,i)=>coloringPart("path",`dino-leg-${i}`,`d="M ${x} 425 L ${x-10} 545 H ${x+55} L ${x+35} 430 Z"`,i>1?2:1)).join("")+
coloringPart("path","dino-tail",'d="M 365 348 C 300 310 225 280 205 315 C 235 355 285 405 375 425 Z"')+
[390,475,555,615].map((x,i)=>coloringPart("path",`dino-leg-${i}`,`d="M ${x} 410 C ${x-4} 452 ${x-12} 500 ${x-8} 540 L ${x+2} 552 L ${x+58} 552 C ${x+62} 538 ${x+50} 528 ${x+35} 530 C ${x+38} 485 ${x+44} 448 ${x+32} 412 Z"`,i>1?2:1)).join("")+
[[385,320],[440,290],[500,280],[555,295]].map(([x,y],i)=>coloringPart("path",`dino-plate-${i}`,`d="M ${x-25} ${y+20} L ${x} ${y-35} L ${x+25} ${y+20} Z"`,i>1?2:1)).join("")+
coloringPart("path","dino-body",'d="M 340 375 C 350 315 415 275 500 280 C 585 285 642 330 650 390 C 655 450 595 485 505 480 C 415 480 350 445 340 375 Z"')+
coloringPart("path","dino-neck",'d="M 570 365 C 590 285 600 205 650 165 C 690 130 745 145 755 190 C 758 220 730 245 690 242 C 670 240 660 255 658 290 C 655 345 640 395 620 420 Z"')+
coloringPart("path","dino-head",'d="M 650 170 C 675 140 735 140 765 170 C 785 195 765 228 730 240 C 690 250 650 228 645 198 C 642 187 645 178 650 170 Z"')+
coloringPart("circle","dino-sun",'cx="690" cy="85" r="42"')+
coloringLine("circle",'cx="725" cy="180" r="6" fill="#26344a" stroke="none"')+
coloringLine("path",'d="M 715 210 Q 735 225 755 208" fill="none"')+
@ -128,11 +129,11 @@ function sceneSpringNest(){return coloringScene(
coloringPart("path","nest-branch",'d="M 45 410 Q 250 340 480 390 Q 620 420 755 345 L 770 405 Q 600 475 455 445 Q 220 405 50 475 Z"')+
coloringPart("path","nest-bowl",'d="M 250 350 Q 395 305 535 350 Q 500 470 390 475 Q 285 460 250 350 Z"')+
[315,390,465].map((x,i)=>coloringPart("ellipse",`nest-egg-${i}`,`cx="${x}" cy="350" rx="38" ry="55"`,i===2?2:1)).join("")+
coloringPart("ellipse","bird-body",'cx="590" cy="255" rx="105" ry="85"')+
coloringPart("circle","bird-head",'cx="655" cy="185" r="63"')+
coloringPart("path","bird-wing",'d="M 520 245 Q 595 185 625 300 Q 560 345 520 245 Z"',2)+
coloringPart("path","bird-beak",'d="M 710 180 L 780 205 L 710 225 Z"')+
coloringPart("path","bird-tail-a",'d="M 505 285 L 405 330 L 525 330 Z"',2)+coloringPart("path","bird-tail-b",'d="M 520 305 L 445 375 L 555 335 Z"',3)+
coloringPart("path","bird-body",'d="M 485 270 C 488 210 535 175 595 185 C 655 195 690 245 675 300 C 660 355 595 375 540 345 C 505 328 485 300 485 270 Z"')+
coloringPart("path","bird-wing",'d="M 520 245 C 555 205 625 220 630 280 C 612 325 555 340 520 300 C 508 282 508 260 520 245 Z"',2)+
coloringPart("path","bird-head",'d="M 610 190 C 610 145 640 118 680 125 C 720 132 740 175 725 210 C 712 245 670 258 640 235 C 620 223 610 207 610 190 Z"')+
coloringPart("path","bird-beak",'d="M 718 178 C 745 178 770 190 785 205 C 760 215 738 220 715 215 Z"')+
[[125,300,-20],[175,340,25],[665,390,-25],[705,350,20]].map(([x,y,a],i)=>coloringPart("path",`spring-leaf-${i}`,`d="M ${x-35} ${y} Q ${x} ${y-45} ${x+35} ${y} Q ${x} ${y+45} ${x-35} ${y} Z" transform="rotate(${a} ${x} ${y})"`,i>1?2:1)).join("")+
coloringPetals(135,165,"spring-nest-flower",3)+
coloringLine("circle",'cx="670" cy="175" r="6" fill="#26344a" stroke="none"')+
@ -150,14 +151,14 @@ function sceneFireStation(){return coloringScene(
coloringPart("circle","station-badge",'cx="210" cy="145" r="42"',2)+
coloringPart("rect","station-door-a",'x="80" y="255" width="115" height="200" rx="8"')+
coloringPart("rect","station-door-b",'x="225" y="255" width="115" height="200" rx="8"')+
coloringPart("path","engine-body",'d="M 360 350 H 640 L 730 425 V 500 H 355 Z"')+
coloringPart("path","engine-cab",'d="M 580 350 H 665 L 730 425 H 580 Z"')+
coloringPart("circle","engine-wheel-a",'cx="445" cy="500" r="55"')+coloringPart("circle","engine-wheel-b",'cx="665" cy="500" r="55"')+
coloringPart("path","engine-body",'d="M 360 350 H 630 C 672 350 705 378 730 415 C 738 430 738 465 730 500 H 720 C 715 465 695 445 665 445 C 635 445 615 465 610 500 H 500 C 495 465 475 445 445 445 C 415 445 395 465 390 500 H 355 V 385 C 355 370 355 358 360 350 Z"')+
coloringPart("path","engine-cab",'d="M 580 350 H 655 C 680 355 708 385 730 425 H 580 Z"')+
coloringPart("path","engine-window",'d="M 610 370 H 655 L 695 415 H 610 Z"',2)+
coloringPart("rect","engine-panel",'x="385" y="375" width="165" height="90" rx="12"',2)+
coloringPart("path","engine-ladder",'d="M 375 342 L 555 287 L 566 317 L 386 373 Z"',2)+
coloringPart("circle","engine-hose-reel",'cx="470" cy="420" r="34"',2)+
coloringPart("circle","engine-hose-hub",'cx="470" cy="420" r="12"',3)+
coloringPart("circle","engine-wheel-a",'cx="445" cy="500" r="55"')+coloringPart("circle","engine-wheel-b",'cx="665" cy="500" r="55"')+
coloringPart("circle","engine-hub-a",'cx="445" cy="500" r="22"',2)+coloringPart("circle","engine-hub-b",'cx="665" cy="500" r="22"',2)+
coloringPart("rect","engine-light",'x="470" y="315" width="55" height="35" rx="16"',2)+
coloringPart("rect","hydrant",'x="755" y="420" width="30" height="90" rx="10"',3)+
@ -171,12 +172,12 @@ function sceneFireStation(){return coloringScene(
function sceneBeeMeadow(){return coloringScene(
coloringPart("rect","bee-sky",'x="4" y="4" width="792" height="592" rx="28"')+
coloringPart("path","bee-grass",'d="M 4 420 Q 180 385 355 430 T 796 405 L 796 596 L 4 596 Z"')+
coloringPart("ellipse","bee-body",'cx="430" cy="250" rx="135" ry="82"')+
coloringPart("circle","bee-head",'cx="560" cy="235" r="68"')+
coloringPart("ellipse","bee-wing-a",'cx="385" cy="150" rx="75" ry="45" transform="rotate(-25 385 150)"')+
coloringPart("ellipse","bee-wing-b",'cx="475" cy="145" rx="75" ry="45" transform="rotate(25 475 145)"')+
coloringPart("path","bee-wing-a",'d="M 420 205 C 340 205 300 155 330 115 C 365 78 430 125 445 195 Z"')+
coloringPart("path","bee-wing-b",'d="M 430 195 C 445 115 510 82 540 125 C 565 165 515 210 445 210 Z"')+
coloringPart("path","bee-body",'d="M 292 250 C 300 185 355 155 430 165 C 500 172 545 205 550 250 C 545 300 500 330 430 335 C 355 340 300 315 292 250 Z"')+
coloringPart("path","bee-stripe-a",'d="M 350 188 Q 395 165 415 180 L 405 320 Q 365 315 335 285 Z"',2)+
coloringPart("path","bee-stripe-b",'d="M 445 170 Q 485 175 510 195 L 490 315 Q 455 328 430 320 Z"',2)+
coloringPart("path","bee-head",'d="M 500 235 C 500 195 525 168 563 168 C 602 168 630 198 628 238 C 625 278 595 305 558 302 C 520 300 498 273 500 235 Z"')+
coloringPart("path","hive",'d="M 610 385 Q 685 300 760 385 L 745 535 H 625 Z"')+
coloringPart("ellipse","hive-door",'cx="685" cy="475" rx="38" ry="52"',2)+
[[115,430],[220,460],[330,440]].map(([x,y],i)=>coloringPetals(x,y,`bee-flower-${i}`,i===2?3:2)).join("")+
@ -191,13 +192,15 @@ function sceneBeeMeadow(){return coloringScene(
function sceneCoralReef(){return coloringScene(
coloringPart("rect","reef-water",'x="4" y="4" width="792" height="592" rx="28"')+
coloringPart("path","reef-sand",'d="M 4 475 Q 175 425 345 490 T 796 455 L 796 596 L 4 596 Z"')+
coloringPart("ellipse","clownfish-body",'cx="355" cy="260" rx="155" ry="82"')+
coloringPart("path","clownfish-tail",'d="M 205 260 L 105 180 Q 130 260 105 340 Z"')+
coloringPart("path","clownfish-tail",'d="M 215 255 C 170 230 130 190 95 170 C 110 225 110 295 95 350 C 135 325 175 295 215 270 Z"')+
coloringPart("path","clownfish-fin-top",'d="M 295 185 C 330 125 390 135 420 195 C 380 180 340 178 295 185 Z"',2)+
coloringPart("path","clownfish-fin-bottom",'d="M 315 332 C 350 385 410 370 432 315 C 390 332 355 338 315 332 Z"',3)+
coloringPart("path","clownfish-body",'d="M 195 260 C 240 175 365 145 465 205 C 515 235 515 280 470 310 C 370 375 240 345 195 260 Z"')+
coloringPart("path","clownfish-stripe-a",'d="M 290 188 Q 330 178 355 185 L 350 338 Q 315 345 285 328 Q 320 260 290 188 Z"',2)+
coloringPart("path","clownfish-stripe-b",'d="M 405 190 Q 445 205 470 235 L 460 300 Q 435 325 400 333 Q 430 260 405 190 Z"',2)+
coloringPart("circle","clownfish-eye",'cx="445" cy="235" r="16"')+
coloringPart("path","seahorse-body",'d="M 610 220 Q 680 165 710 225 Q 660 270 675 345 Q 700 430 625 450 Q 570 440 600 390 Q 650 405 635 350 Q 590 285 610 220 Z"')+
coloringPart("path","seahorse-fin",'d="M 610 285 Q 540 300 585 360 Q 620 345 625 315 Z"',2)+
coloringPart("path","seahorse-body",'d="M 610 225 C 625 185 680 172 712 202 C 730 222 715 250 682 258 C 655 265 648 285 658 322 C 670 360 705 400 675 435 C 650 465 590 452 585 415 C 582 390 615 380 635 397 C 650 370 625 345 612 318 C 595 285 595 252 610 225 Z"')+
coloringPart("circle","seahorse-eye",'cx="670" cy="220" r="13"',2)+
coloringPart("path","coral-red",'d="M 95 535 Q 82 480 108 435 L 88 400 Q 95 375 118 405 L 128 360 Q 145 342 150 375 L 145 430 L 175 395 Q 198 390 188 420 L 158 468 L 200 452 Q 225 458 200 480 L 165 505 L 170 540 Z"')+
coloringPart("path","coral-fan",'d="M 475 540 Q 455 485 472 430 Q 480 395 496 430 L 510 472 L 520 405 Q 535 378 543 420 L 540 478 L 565 430 Q 590 408 580 448 L 556 492 L 592 468 Q 615 470 593 495 L 548 530 L 550 545 Z"',2)+
@ -244,16 +247,17 @@ function sceneMountainCamp(){return coloringScene(
function sceneRobotWorkshop(){return coloringScene(
coloringPart("rect","robot-wall",'x="4" y="4" width="792" height="592" rx="28"')+
coloringPart("path","robot-floor",'d="M 4 450 H 796 V 596 H 4 Z"')+
coloringPart("path","robot-arm-a",'d="M 270 285 C 220 292 175 310 140 335 L 165 402 C 210 390 245 375 280 355 Z"')+
coloringPart("path","robot-arm-b",'d="M 530 285 C 580 292 625 310 660 335 L 635 402 C 590 390 555 375 520 355 Z"')+
coloringPart("circle","robot-hand-a",'cx="135" cy="390" r="42"',2)+coloringPart("circle","robot-hand-b",'cx="665" cy="390" r="42"',2)+
coloringPart("path","robot-leg-a",'d="M 285 430 H 370 V 535 Q 350 558 305 548 Q 282 535 285 500 Z"')+coloringPart("path","robot-leg-b",'d="M 430 430 H 515 V 535 Q 495 558 450 548 Q 427 535 430 500 Z"')+
coloringPart("rect","robot-neck",'x="365" y="220" width="70" height="55" rx="16"',2)+
coloringPart("rect","robot-head",'x="285" y="95" width="230" height="145" rx="28"')+
coloringPart("rect","robot-face",'x="320" y="125" width="160" height="80" rx="18"',2)+
coloringPart("circle","robot-eye-a",'cx="360" cy="165" r="20"')+coloringPart("circle","robot-eye-b",'cx="440" cy="165" r="20"')+
coloringPart("rect","robot-body",'x="250" y="250" width="300" height="210" rx="25"')+
coloringPart("rect","robot-screen",'x="305" y="285" width="190" height="90" rx="15"',2)+
coloringPart("circle","robot-button-a",'cx="335" cy="415" r="18"',2)+coloringPart("circle","robot-button-b",'cx="400" cy="415" r="18"',2)+coloringPart("circle","robot-button-c",'cx="465" cy="415" r="18"',3)+
coloringPart("path","robot-arm-a",'d="M 250 285 L 140 330 L 165 395 L 265 360 Z"')+
coloringPart("path","robot-arm-b",'d="M 550 285 L 660 330 L 635 395 L 535 360 Z"')+
coloringPart("circle","robot-hand-a",'cx="135" cy="390" r="42"',2)+coloringPart("circle","robot-hand-b",'cx="665" cy="390" r="42"',2)+
coloringPart("rect","robot-leg-a",'x="285" y="460" width="85" height="95" rx="20"')+coloringPart("rect","robot-leg-b",'x="430" y="460" width="85" height="95" rx="20"')+
coloringPart("path","gear-a",'d="M 105 130 l 18 -28 l 28 18 l 30 -12 l 12 32 l 35 5 l -4 35 l 25 24 l -22 28 l 8 34 l -34 10 l -16 31 l -30 -18 l -33 12 l -11 -33 l -35 -6 l 5 -34 l -25 -25 l 22 -27 l -8 -35 l 34 -9 Z"',3)+
coloringPart("circle","gear-a-hole",'cx="145" cy="205" r="35"',3)+
coloringLine("path",'d="M 400 95 V 45 M 375 45 H 425" fill="none"')+
@ -313,20 +317,17 @@ function sceneSavannaWeb(){return coloringScene(
coloringPart("path","savanna-ground",'d="M 4 415 Q 180 380 360 425 T 796 400 L 796 596 L 4 596 Z"')+
coloringPart("path","acacia-trunk",'d="M 95 450 Q 145 300 135 185 H 205 Q 190 310 235 450 Z"')+
coloringPart("path","acacia-crown",'d="M 35 210 Q 65 95 165 125 Q 250 70 320 155 Q 365 235 270 260 H 90 Q 30 260 35 210 Z"')+
coloringPart("ellipse","lion-body",'cx="485" cy="390" rx="145" ry="80"')+
coloringPart("circle","lion-mane",'cx="625" cy="325" r="105"')+
coloringPart("path","lion-tail",'d="M 365 372 C 305 335 275 372 290 435 C 298 475 325 505 350 480 C 365 465 350 440 330 448 C 315 410 330 395 380 405 Z"',2)+
[390,470,540,590].map((x,i)=>coloringPart("path",`lion-leg-${i}`,`d="M ${x} 410 C ${x-4} 455 ${x-7} 505 ${x-2} 545 Q ${x+18} 558 ${x+48} 548 L ${x+43} 532 C ${x+35} 488 ${x+38} 448 ${x+30} 412 Z"`,i>1?2:1)).join("")+
coloringPart("path","lion-body",'d="M 340 385 C 348 330 405 302 485 305 C 565 308 625 340 632 392 C 635 445 580 475 500 475 C 415 478 342 448 340 385 Z"')+
coloringPart("path","lion-mane",'d="M 625 215 C 652 205 675 220 682 245 C 712 250 724 275 710 300 C 735 323 724 352 700 365 C 708 395 680 420 650 415 C 630 440 600 438 582 415 C 552 422 532 395 540 366 C 515 352 515 322 538 300 C 525 272 545 248 570 245 C 578 220 600 208 625 215 Z"')+
coloringPart("circle","lion-face",'cx="625" cy="325" r="68"',2)+
coloringPart("ellipse","lion-muzzle",'cx="625" cy="350" rx="45" ry="30"',2)+
[390,470,540,590].map((x,i)=>coloringPart("rect",`lion-leg-${i}`,`x="${x}" y="435" width="38" height="110" rx="15"`,i>1?2:1)).join("")+
coloringPart("path","lion-tail",'d="M 355 385 Q 285 350 300 475 Q 325 510 350 475 Q 325 410 370 410 Z"',2)+
coloringPart("ellipse","zebra-body",'cx="200" cy="475" rx="115" ry="65"',2)+
coloringPart("rect","zebra-leg-0",'x="125" y="510" width="30" height="70" rx="12"',3)+
coloringPart("rect","zebra-leg-1",'x="175" y="510" width="30" height="70" rx="12"',3)+
coloringPart("rect","zebra-leg-2",'x="230" y="510" width="30" height="70" rx="12"',3)+
coloringPart("rect","zebra-leg-3",'x="280" y="510" width="30" height="70" rx="12"',3)+
coloringPart("path","zebra-tail",'d="M 100 455 Q 55 425 70 505 Q 90 528 105 500 Q 82 468 120 470 Z"',3)+
[125,175,230,280].map((x,i)=>coloringPart("path",`zebra-leg-${i}`,`d="M ${x} 490 C ${x-3} 520 ${x-5} 552 ${x} 578 H ${x+34} C ${x+38} 565 ${x+30} 558 ${x+25} 552 L ${x+25} 492 Z"`,3)).join("")+
coloringPart("path","zebra-body",'d="M 85 470 C 95 425 135 400 205 405 C 270 408 315 440 318 480 C 315 525 265 545 198 540 C 130 538 82 515 85 470 Z"',2)+
coloringPart("path","zebra-neck",'d="M 270 450 Q 280 390 315 350 L 350 382 L 330 490 Z"',2)+
coloringPart("path","zebra-head",'d="M 295 350 Q 350 315 390 350 Q 402 392 370 418 Q 330 430 300 398 Z"',2)+
coloringPart("path","zebra-head",'d="M 295 350 C 320 325 360 318 388 342 C 405 360 405 390 385 410 C 365 430 330 428 307 410 C 288 395 282 368 295 350 Z"',2)+
coloringPart("ellipse","zebra-muzzle",'cx="372" cy="390" rx="42" ry="27"',3)+
coloringPart("path","zebra-ear-a",'d="M 312 352 L 305 310 L 338 343 Z"',3)+coloringPart("path","zebra-ear-b",'d="M 350 340 L 370 305 L 378 350 Z"',3)+
coloringPart("path","zebra-mane",'d="M 292 360 Q 268 395 282 455 L 306 445 Q 292 402 320 365 Z"',3)+

View file

@ -19,7 +19,7 @@ function coloringLine(tag,attrs="",level=1){
}
function coloringScene(content){
return `<svg class="coloring-sheet" viewBox="0 0 800 600" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
<g class="coloring-svg-lines" fill="#fff" fill-rule="evenodd" clip-rule="evenodd" stroke="#26344a" stroke-width="6.5" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="2">
<g class="coloring-svg-lines" fill="#fff" fill-rule="evenodd" clip-rule="evenodd" stroke="#26344a" stroke-width="5.6" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="2">
${content}
</g>
</svg>`;
@ -39,13 +39,13 @@ function sceneButterfly(){return coloringScene(
coloringPart("rect","sky",'x="4" y="4" width="792" height="592" rx="28"')+
coloringPart("path","grass",'d="M 4 430 Q 170 390 320 438 T 796 420 L 796 596 L 4 596 Z"')+
coloringPart("circle","sun",'cx="685" cy="105" r="62"')+
coloringPart("ellipse","wing-left-top",'cx="304" cy="248" rx="112" ry="135" transform="rotate(-28 304 248)"')+
coloringPart("ellipse","wing-right-top",'cx="496" cy="248" rx="112" ry="135" transform="rotate(28 496 248)"')+
coloringPart("ellipse","wing-left-bottom",'cx="323" cy="386" rx="92" ry="105" transform="rotate(22 323 386)"')+
coloringPart("ellipse","wing-right-bottom",'cx="477" cy="386" rx="92" ry="105" transform="rotate(-22 477 386)"')+
coloringPart("ellipse","body",'cx="400" cy="316" rx="35" ry="142"')+
coloringPart("circle","head",'cx="400" cy="155" r="42"')+
coloringLine("path",'d="M 380 122 Q 340 72 315 82 M 420 122 Q 460 72 485 82" fill="none"')+
coloringPart("path","wing-left-top",'d="M 378 300 C 335 155 250 95 190 145 C 130 195 185 315 355 345 C 370 335 380 320 378 300 Z"')+
coloringPart("path","wing-right-top",'d="M 422 300 C 465 155 550 95 610 145 C 670 195 615 315 445 345 C 430 335 420 320 422 300 Z"')+
coloringPart("path","wing-left-bottom",'d="M 365 330 C 245 320 205 405 255 470 C 305 525 380 440 390 350 C 385 340 378 334 365 330 Z"')+
coloringPart("path","wing-right-bottom",'d="M 435 330 C 555 320 595 405 545 470 C 495 525 420 440 410 350 C 415 340 422 334 435 330 Z"')+
coloringPart("path","body",'d="M 382 185 C 366 225 365 305 374 385 C 378 440 390 475 400 488 C 410 475 422 440 426 385 C 435 305 434 225 418 185 C 407 174 393 174 382 185 Z"')+
coloringPart("path","head",'d="M 365 155 C 365 125 380 108 400 108 C 420 108 435 125 435 155 C 435 180 420 195 400 195 C 380 195 365 180 365 155 Z"')+
coloringLine("path",'d="M 382 122 C 365 92 340 76 315 82 M 418 122 C 435 92 460 76 485 82" fill="none"')+
[[293,220],[507,220],[325,382],[475,382]].map(([x,y],i)=>coloringPart("circle",`wing-dot-${i}`,`cx="${x}" cy="${y}" r="28"`,2)).join("")+
coloringLine("path",'d="M 385 155 Q 400 170 415 155" fill="none"',2)+
coloringPetals(125,475,"flower-a",2)+coloringPetals(665,492,"flower-b",3)
@ -78,13 +78,15 @@ function sceneCake(){return coloringScene(
function sceneOcean(){return coloringScene(
coloringPart("rect","water",'x="4" y="4" width="792" height="592" rx="28"')+
coloringPart("path","sand",'d="M 4 475 Q 140 430 275 482 T 535 470 T 796 458 L 796 596 L 4 596 Z"')+
coloringPart("ellipse","fish-body",'cx="325" cy="260" rx="145" ry="88"')+
coloringPart("path","fish-tail",'d="M 185 260 L 85 178 Q 110 260 85 342 Z"')+
coloringPart("path","fish-tail",'d="M 198 258 C 155 230 120 190 82 172 C 98 220 98 300 82 348 C 125 326 162 294 198 270 Z"')+
coloringPart("path","fish-top-fin",'d="M 280 185 C 315 125 365 135 385 190 C 345 178 315 180 280 185 Z"',2)+
coloringPart("path","fish-bottom-fin",'d="M 300 328 C 335 380 380 365 392 315 C 355 330 330 332 300 328 Z"',3)+
coloringPart("path","fish-body",'d="M 185 260 C 235 165 370 145 458 215 C 490 240 490 280 458 305 C 365 375 235 350 185 260 Z"')+
coloringPart("circle","fish-eye",'cx="395" cy="230" r="18"')+
coloringPart("ellipse","turtle-shell",'cx="590" cy="390" rx="110" ry="75"')+
coloringPart("circle","turtle-head",'cx="720" cy="382" r="40"')+
[[520,335,-28],[525,445,28],[640,330,25],[640,450,-25]].map(([x,y,a],i)=>coloringPart("ellipse",`flipper-${i}`,`cx="${x}" cy="${y}" rx="48" ry="20" transform="rotate(${a} ${x} ${y})"`,2)).join("")+
coloringPart("path","turtle-tail",'d="M 480 388 L 445 365 L 455 410 Z"',2)+
[[515,342,-32],[520,438,32],[650,337,28],[650,443,-28]].map(([x,y,a],i)=>coloringPart("path",`flipper-${i}`,`d="M ${x-38} ${y-8} C ${x-12} ${y-32} ${x+35} ${y-25} ${x+48} ${y} C ${x+12} ${y+12} ${x-20} ${y+14} ${x-38} ${y-8} Z" transform="rotate(${a} ${x} ${y})"`,2)).join("")+
coloringPart("path","turtle-shell",'d="M 478 390 C 485 320 545 292 610 305 C 670 315 705 352 700 400 C 692 455 635 475 575 465 C 515 455 480 430 478 390 Z"')+
coloringPart("path","turtle-head",'d="M 688 365 C 708 340 748 342 765 370 C 780 395 760 425 728 425 C 702 425 685 400 688 365 Z"')+
coloringLine("path",'d="M 270 220 Q 325 265 270 310 M 335 190 Q 370 260 335 330" fill="none"',2)+
coloringLine("path",'d="M 485 390 H 695 M 535 330 Q 590 390 535 450 M 645 330 Q 590 390 645 450 M 520 390 Q 590 340 660 390 Q 590 440 520 390 Z" fill="none"',3)+
[[115,105,18],[150,75,11],[640,100,22],[675,65,13],[730,145,15]].map(([x,y,r],i)=>coloringPart("circle",`bubble-${i}`,`cx="${x}" cy="${y}" r="${r}"`,3)).join("")+
@ -95,12 +97,13 @@ function sceneGarden(){return coloringScene(
coloringPart("rect","garden-sky",'x="4" y="4" width="792" height="592" rx="28"')+
coloringPart("path","garden-ground",'d="M 4 390 Q 160 350 320 400 T 796 385 L 796 596 L 4 596 Z"')+
coloringPart("circle","sun",'cx="690" cy="95" r="58"')+
coloringPart("path","snail-shell",'d="M 120 405 C 120 290 300 290 305 405 C 280 490 155 490 120 405 Z"')+
coloringPart("path","snail-body",'d="M 95 455 Q 250 470 365 430 Q 410 415 438 450 Q 400 500 300 500 L 110 500 Z"')+
coloringPart("circle","snail-head",'cx="410" cy="410" r="48"')+
coloringLine("path",'d="M 388 370 L 370 315 M 427 370 L 450 318" fill="none"')+
coloringPart("circle","ladybird",'cx="600" cy="385" r="82"')+
coloringPart("circle","ladybird-head",'cx="600" cy="298" r="43"')+
coloringPart("path","snail-body",'d="M 82 455 C 160 448 245 470 335 438 C 355 430 365 400 383 380 C 407 352 447 360 458 392 C 470 425 447 457 410 465 C 365 515 250 505 115 500 C 88 500 72 480 82 455 Z"')+
coloringPart("path","snail-shell",'d="M 118 410 C 110 330 165 285 230 295 C 300 305 330 365 305 425 C 280 480 190 485 140 440 C 128 430 120 420 118 410 Z"')+
coloringPart("path","snail-head",'d="M 365 410 C 365 372 385 350 415 350 C 447 350 465 378 458 412 C 452 445 425 465 393 452 C 375 445 365 430 365 410 Z"')+
coloringLine("path",'d="M 388 365 C 382 338 375 320 365 305 M 435 368 C 443 338 452 320 463 306" fill="none"')+
[[525,350,-25],[520,405,20],[675,350,25],[680,405,-20]].map(([x,y,a],i)=>coloringPart("path",`ladybird-leg-${i}`,`d="M ${x} ${y} C ${x+(i<2?-34:34)} ${y+4} ${x+(i<2?-48:48)} ${y+24} ${x+(i<2?-60:60)} ${y+38} L ${x+(i<2?-48:48)} ${y+45} C ${x+(i<2?-30:30)} ${y+28} ${x+(i<2?-15:15)} ${y+20} ${x} ${y+18} Z" transform="rotate(${a} ${x} ${y})"`,3)).join("")+
coloringPart("path","ladybird",'d="M 520 390 C 520 335 555 305 600 305 C 645 305 680 335 680 390 C 680 448 648 475 600 475 C 552 475 520 448 520 390 Z"')+
coloringPart("path","ladybird-head",'d="M 558 320 C 558 282 575 260 600 260 C 625 260 642 282 642 320 C 620 335 580 335 558 320 Z"')+
coloringLine("path",'d="M 600 305 L 600 465 M 540 328 Q 600 385 540 440 M 660 328 Q 600 385 660 440" fill="none"',2)+
[[565,360],[635,360],[560,415],[640,415]].map(([x,y],i)=>coloringPart("circle",`ladybird-dot-${i}`,`cx="${x}" cy="${y}" r="15"`,2)).join("")+
coloringPetals(165,165,"garden-flower-a",2)+coloringPetals(430,145,"garden-flower-b",3)+
@ -113,8 +116,8 @@ function sceneWindmill(){return coloringScene(
coloringPart("path","mill-body",'d="M 310 470 L 350 185 L 485 185 L 535 470 Z"')+
coloringPart("path","mill-roof",'d="M 335 185 Q 420 78 500 185 Z"')+
coloringPart("rect","mill-door",'x="392" y="375" width="65" height="95" rx="25"')+
coloringPart("circle","mill-hub",'cx="418" cy="220" r="35"')+
[["M 405 190 L 320 55 L 345 40 L 430 205 Z"],["M 447 208 L 630 120 L 645 150 L 448 235 Z"],["M 430 250 L 515 410 L 485 425 L 405 248 Z"],["M 389 232 L 205 325 L 190 295 L 388 205 Z"]].map((d,i)=>coloringPart("path",`sail-${i}`,`d="${d}"`,1)).join("")+
coloringPart("circle","mill-hub",'cx="418" cy="220" r="35"')+
coloringPart("rect","house",'x="80" y="345" width="165" height="125" rx="8"',2)+
coloringPart("path","house-roof",'d="M 55 345 L 165 260 L 270 345 Z"',2)+
coloringPart("rect","house-window-a",'x="105" y="375" width="45" height="45"',3)+
@ -125,12 +128,12 @@ function sceneRocket(){return coloringScene(
coloringPart("rect","space",'x="4" y="4" width="792" height="592" rx="28"')+
coloringPart("circle","planet-large",'cx="650" cy="150" r="100"')+
coloringPart("path","planet-ring",'d="M 535 120 Q 650 195 765 120 Q 650 255 535 120 Z"',2)+
coloringPart("path","rocket-body",'d="M 320 420 Q 315 185 400 75 Q 485 185 480 420 Z"')+
coloringPart("circle","rocket-window",'cx="400" cy="225" r="48"')+
coloringPart("path","rocket-left-fin",'d="M 325 335 Q 245 385 245 480 L 350 410 Z"')+
coloringPart("path","rocket-right-fin",'d="M 475 335 Q 555 385 555 480 L 450 410 Z"')+
coloringPart("path","flame-outer",'d="M 350 420 Q 330 525 400 575 Q 470 525 450 420 Z"')+
coloringPart("path","flame-inner",'d="M 380 420 Q 365 495 400 530 Q 435 495 420 420 Z"',2)+
coloringPart("path","rocket-left-fin",'d="M 330 330 C 270 365 240 420 245 485 C 285 465 325 435 355 400 Z"')+
coloringPart("path","rocket-right-fin",'d="M 470 330 C 530 365 560 420 555 485 C 515 465 475 435 445 400 Z"')+
coloringPart("path","rocket-body",'d="M 350 425 C 325 355 320 245 345 165 C 360 118 382 88 400 65 C 418 88 440 118 455 165 C 480 245 475 355 450 425 C 425 440 375 440 350 425 Z"')+
coloringPart("circle","rocket-window",'cx="400" cy="225" r="48"')+
coloringStars([[110,105,30],[210,225,22],[680,350,28],[120,430,20],[575,80,15]],"space-star",2)+
coloringPart("circle","moon",'cx="680" cy="475" r="62"',3)+
[[655,458,13],[700,490,17],[690,445,9]].map(([x,y,r],i)=>coloringPart("circle",`crater-${i}`,`cx="${x}" cy="${y}" r="${r}"`,3)).join("")
@ -144,12 +147,12 @@ function sceneRainforest(){return coloringScene(
coloringPart("path","tree-trunk-right",'d="M 585 500 Q 640 300 615 35 L 760 35 Q 720 300 785 500 Z"')+
coloringLine("path",'d="M 170 55 Q 300 175 230 315 Q 170 430 300 485 Q 400 525 455 425 Q 505 330 430 265" fill="none"',2)+
coloringPart("path","parrot-branch",'d="M 245 408 Q 395 382 575 420 L 568 455 Q 405 425 255 447 Z"')+
coloringPart("path","parrot-body",'d="M 340 270 Q 390 155 490 230 Q 520 330 430 405 Q 330 390 340 270 Z"')+
coloringPart("circle","parrot-head",'cx="450" cy="205" r="75"')+
coloringPart("path","parrot-beak",'d="M 505 205 Q 590 225 515 270 Z"')+
coloringPart("path","parrot-wing",'d="M 365 275 Q 455 245 465 365 Q 390 400 365 275 Z"',2)+
coloringPart("path","parrot-tail",'d="M 385 380 C 365 425 350 475 355 525 L 412 440 C 420 475 430 510 445 530 L 468 390 Z"',2)+
coloringPart("path","parrot-body",'d="M 335 275 C 340 205 395 175 455 198 C 515 220 535 285 505 342 C 478 398 420 425 370 392 C 338 370 325 320 335 275 Z"')+
coloringPart("path","parrot-wing",'d="M 365 275 C 400 240 465 255 480 310 C 480 355 435 392 390 375 C 360 355 350 305 365 275 Z"',2)+
coloringPart("path","parrot-head",'d="M 390 205 C 390 158 425 125 468 132 C 515 140 540 185 522 225 C 505 265 458 278 422 252 C 402 240 390 225 390 205 Z"')+
coloringPart("path","parrot-beak",'d="M 510 198 C 545 198 575 215 590 235 C 565 255 535 265 510 258 C 525 235 525 218 510 198 Z"')+
coloringPart("circle","parrot-eye",'cx="470" cy="185" r="14"')+
coloringPart("path","parrot-tail",'d="M 385 395 L 355 520 L 425 430 L 440 525 L 465 395 Z"',2)+
[[285,125,-25],[540,100,18],[265,405,15],[525,450,-20]].map(([x,y,a],i)=>coloringPart("path",`forest-leaf-${i}`,`d="M ${x-55} ${y} Q ${x} ${y-70} ${x+55} ${y} Q ${x} ${y+70} ${x-55} ${y} Z" transform="rotate(${a} ${x} ${y})"`,i<2?2:3)).join("")+
coloringLine("path",'d="M 285 70 L 285 180 M 540 30 L 540 170 M 265 335 L 265 475 M 525 375 L 525 525" fill="none"',3)
)}
@ -216,7 +219,9 @@ function sceneEcosystem(){return coloringScene(
coloringPart("rect","pond-sky",'x="4" y="4" width="792" height="592" rx="28"')+
coloringPart("path","pond-bank",'d="M 4 390 Q 160 335 315 395 T 620 375 T 796 390 L 796 596 L 4 596 Z"')+
coloringPart("ellipse","pond-water",'cx="405" cy="485" rx="310" ry="90"')+
coloringPart("path","frog-body",'d="M 305 435 Q 345 350 410 430 Q 465 350 505 435 Q 460 500 405 470 Q 350 500 305 435 Z"')+
coloringPart("path","frog-leg-a",'d="M 350 425 C 300 405 265 430 245 475 C 275 485 315 478 365 455 Z"',2)+
coloringPart("path","frog-leg-b",'d="M 460 425 C 510 405 550 430 568 475 C 535 488 495 478 448 455 Z"',2)+
coloringPart("path","frog-body",'d="M 300 435 C 315 382 350 365 405 405 C 460 365 498 382 510 435 C 500 482 455 505 405 475 C 355 505 310 482 300 435 Z"')+
coloringPart("circle","frog-eye-a",'cx="350" cy="385" r="34"')+coloringPart("circle","frog-eye-b",'cx="460" cy="385" r="34"')+
coloringPart("path","lily-pad-a",'d="M 120 505 Q 205 430 280 510 Q 205 555 120 505 Z"',2)+
coloringPart("path","lily-pad-b",'d="M 525 515 Q 610 435 690 510 Q 610 560 525 515 Z"',2)+
@ -353,12 +358,28 @@ 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"));
overlay.querySelectorAll("[data-fillable]").forEach(node=>{
node.removeAttribute("data-fillable");node.removeAttribute("data-part");node.setAttribute("fill","none");
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;
}
@ -397,8 +418,8 @@ function mountColoring(root,initState,opts={}){
const custom=root.querySelector(".coloring-custom input"),reset=root.querySelector(".coloring-reset"),done=root.querySelector(".coloring-done"),hint=root.querySelector(".coloring-hint");
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="brush";refreshTools()};palette.appendChild(button)});
TOOLS.forEach(([id,icon])=>{const button=document.createElement("button"),symbol=document.createElement("span"),label=document.createElement("span");button.type="button";button.dataset.tool=id;symbol.className="coloring-tool-icon";symbol.textContent=icon;symbol.setAttribute("aria-hidden","true");label.className="coloring-tool-label";button.append(symbol,label);button.onclick=()=>{tool=id;refreshTools()};toolHost.appendChild(button)});
COLORING_COLORS.forEach((value,index)=>{const button=document.createElement("button");button.type="button";button.className="coloring-color";button.dataset.color=value;button.dataset.index=index+1;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="brush";refreshTools()};
@ -438,7 +459,9 @@ function mountColoring(root,initState,opts={}){
root.querySelector(".coloring-theme-label").textContent=T("colorTheme");
root.querySelector(".coloring-page-label").textContent=T("colorPage");
root.querySelector(".coloring-fit").textContent=T("colorProfile").replace("{year}",yearGroup).replace("{level}",level);
TOOLS.forEach(([id,,key])=>{const button=toolHost.querySelector(`[data-tool="${id}"]`);button.title=T(key);button.setAttribute("aria-label",T(key))});
toolHost.setAttribute("role","toolbar");toolHost.setAttribute("aria-label",T("penExtras"));palette.setAttribute("role","group");palette.setAttribute("aria-label",T("paletteColors"));sizes.setAttribute("role","group");sizes.setAttribute("aria-label",T("penSize"));
TOOLS.forEach(([id,,key])=>{const button=toolHost.querySelector(`[data-tool="${id}"]`),text=T(key);button.title=text;button.setAttribute("aria-label",text);button.querySelector(".coloring-tool-label").textContent=text});
palette.querySelectorAll("button").forEach(button=>button.setAttribute("aria-label",`${T("penColor")} ${button.dataset.index}`));
custom.parentElement.querySelector("span").textContent=T("customColor");
reset.title=T("colorReset");reset.setAttribute("aria-label",T("colorReset"));
done.textContent=completed?"✓ "+T("colorFinished"):T("colorDone");

View file

@ -92,6 +92,43 @@ test('elk leerjaar heeft zeven herkenbare kleurplaten met diverse themas en d
const page=pages.find(item=>item.id===id);
names.forEach(name=>assert.ok(page.svg.includes(`data-part="${name}"`),`${id} mist ${name}`));
}
const organicContours={
butterfly:['wing-left-top','body'],ocean:['fish-body','turtle-shell'],garden:['snail-body','ladybird'],
farmyard:['cow-body','cow-head'],'forest-picnic':['rabbit-body','rabbit-head'],
'dinosaur-valley':['dino-body','dino-neck','dino-head'],'spring-nest':['bird-body','bird-head'],
'bee-meadow':['bee-body','bee-head'],'coral-reef':['clownfish-body','seahorse-body'],
'savanna-web':['lion-body','lion-mane','zebra-body','zebra-head'],rainforest:['parrot-body','parrot-head'],
ecosystem:['frog-body'],rocket:['rocket-body'],'fire-station':['engine-body']
};
for(const [id,names] of Object.entries(organicContours)){
const page=pages.find(item=>item.id===id);
names.forEach(name=>{
const match=page.svg.match(new RegExp(`<path\\b[^>]*data-part="${name}"[^>]*d="([^"]+)"`));
assert.ok(match,`${id}/${name} gebruikt een getekend contoursilhouet`);
assert.ok((match[1].match(/C/g)||[]).length>=2,`${id}/${name} heeft vloeiende Bézierovergangen`);
});
}
const paintOrder={
farmyard:['cow-tail','cow-leg-0','cow-body','cow-head','cow-snout'],
'forest-picnic':['rabbit-tail','rabbit-ear-a','rabbit-body','rabbit-hind-leg','rabbit-head'],
'dinosaur-valley':['dino-tail','dino-leg-0','dino-plate-0','dino-body','dino-neck','dino-head'],
'spring-nest':['bird-tail-a','bird-body','bird-wing','bird-head','bird-beak'],
ocean:['fish-tail','fish-top-fin','fish-body','turtle-tail','flipper-0','turtle-shell','turtle-head'],
'bee-meadow':['bee-wing-a','bee-body','bee-stripe-a','bee-head'],
'coral-reef':['clownfish-tail','clownfish-fin-top','clownfish-body','seahorse-fin','seahorse-body'],
'savanna-web':['lion-tail','lion-leg-0','lion-body','lion-mane','zebra-tail','zebra-leg-0','zebra-body','zebra-neck','zebra-head'],
rainforest:['parrot-branch','parrot-tail','parrot-body','parrot-wing','parrot-head','parrot-beak'],
ecosystem:['frog-leg-a','frog-body','frog-eye-a'],
rocket:['flame-outer','rocket-left-fin','rocket-body','rocket-window'],
'fire-station':['engine-wheel-a','engine-body','engine-cab','engine-hub-a'],
'robot-workshop':['robot-arm-a','robot-leg-a','robot-neck','robot-head','robot-body'],
windmill:['mill-body','mill-roof','sail-0','mill-hub']
};
for(const [id,names] of Object.entries(paintOrder)){
const page=pages.find(item=>item.id===id),positions=names.map(name=>page.svg.indexOf(`data-part="${name}"`));
assert.ok(positions.every(position=>position>=0),`${id} bevat alle aansluitende onderdelen`);
assert.deepEqual([...positions].sort((a,b)=>a-b),positions,`${id} schildert appendages achter het hoofdsilhouet`);
}
assert.equal(api.pagesFor(5,'science').length,2);
assert.equal(api.pagesFor(3,'science').length,1);
});
@ -111,20 +148,28 @@ test('kwast reageert op pendruk en transparante inktlagen houden lijnen zichtbaa
assert.match(source,/coloringInkOverlay\(svg,"coloring-ink-halo"\)/);
assert.match(source,/coloringInkOverlay\(svg,"coloring-ink-overlay"\)/);
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,/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,/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="6\.5"[^>]*stroke-miterlimit="2"/);
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:4\.25\}/);
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-halo \.coloring-region\{fill:none!important;stroke:#fff;stroke-width:10\.25;stroke-opacity:\.96\}/);
assert.match(css,/\.coloring-ink-overlay \.coloring-region\{fill:none!important;stroke:var\(--coloring-ink\);stroke-width:6\.5;stroke-opacity:1\}/);
assert.match(css,/\.coloring-ink-overlay \.coloring-detail\{fill:none!important;stroke:var\(--coloring-detail\)!important;stroke-width:3\.8;stroke-opacity:1\}/);
assert.match(css,/\.coloring-ink-overlay \.coloring-ink-dot\{fill:var\(--coloring-ink\)!important;stroke:none!important\}/);
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-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"/);
assert.match(source,/toolHost\.setAttribute\("role","toolbar"\)/);
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()=>{