Voeg thematische kleurplaten en creatieve niveaus toe (v0.4.65-beta) #3
22 changed files with 611 additions and 58 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.4.64-beta
|
||||
0.4.65-beta
|
||||
|
|
|
|||
7
db/024_creative_pupil_levels.sql
Normal file
7
db/024_creative_pupil_levels.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
-- Kleurplaten gebruiken dezelfde klas-standaard-met-leerling-override als de
|
||||
-- andere vakgebieden. Bestaande niveaukeuzes blijven ongewijzigd; alleen de
|
||||
-- toegestane waarde 'creatief' wordt aan de constraint toegevoegd.
|
||||
ALTER TABLE pupil_levels DROP CONSTRAINT IF EXISTS pupil_levels_subject_check;
|
||||
ALTER TABLE pupil_levels
|
||||
ADD CONSTRAINT pupil_levels_subject_check
|
||||
CHECK (subject IN ('taal','rekenen','world','creatief'));
|
||||
46
public/css/coloring.css
Normal file
46
public/css/coloring.css
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/* Thematische kleurplaten */
|
||||
.coloring-app{height:100%;min-height:0;display:flex;flex-direction:column;background:var(--surface);color:var(--ink)}
|
||||
.coloring-choices,.coloring-tools{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;border-bottom:1px solid var(--line);background:color-mix(in srgb,var(--orange) 7%,var(--surface))}
|
||||
.coloring-choices label{display:grid;grid-template-columns:auto minmax(120px,1fr);align-items:center;gap:6px;color:var(--muted);font-size:11px;font-weight:850}
|
||||
.coloring-choices select{height:34px;max-width:230px;border:1px solid var(--line);border-radius:9px;padding:0 28px 0 9px;background:var(--surface);color:var(--ink);font:inherit;font-weight:750}
|
||||
.coloring-fit{margin-left:auto;padding:5px 9px;border-radius:999px;background:var(--surface);color:var(--muted);font-size:11px;font-weight:800;white-space:nowrap}
|
||||
.coloring-tools{gap:6px;background:var(--surface)}
|
||||
.coloring-tool-buttons,.coloring-palette,.coloring-sizes{display:flex;align-items:center;gap:4px;padding:3px;border-radius:11px;background:var(--surface-2)}
|
||||
.coloring-tools button{width:36px;height:36px;display:grid;place-items:center;border:0;border-radius:9px;background:transparent;color:var(--ink);font:inherit;font-size:18px;cursor:pointer}
|
||||
.coloring-tools button:is(:hover,:focus-visible),.coloring-tools button.on{background:var(--surface);box-shadow:0 1px 5px rgba(30,50,90,.18)}
|
||||
.coloring-tool-buttons button.on{outline:2px solid var(--orange);outline-offset:-2px}
|
||||
.coloring-color i{width:22px;height:22px;display:block;border:1px solid rgba(38,52,74,.22);border-radius:50%;box-shadow:inset 0 0 0 1px rgba(255,255,255,.18)}
|
||||
.coloring-color.on{outline:2.5px solid var(--ink);outline-offset:-3px}
|
||||
.coloring-size i{display:block;border-radius:50%;background:var(--ink)}
|
||||
.coloring-size.on{outline:2px solid var(--orange);outline-offset:-2px}
|
||||
.coloring-custom{display:flex;align-items:center;gap:5px;color:var(--muted);font-size:10px;font-weight:800}
|
||||
.coloring-custom input{width:36px;height:36px;padding:3px;border:1px solid var(--line);border-radius:9px;background:var(--surface);cursor:pointer}
|
||||
.coloring-tools .coloring-reset{margin-left:auto;background:var(--red-soft);color:var(--red)}
|
||||
.coloring-tools .coloring-done{width:auto;min-width:112px;padding:0 12px;color:#fff;font-size:13px}
|
||||
.coloring-stage{flex:1;min-height:0;overflow:auto;display:grid;place-items:center;padding:12px;background:linear-gradient(135deg,var(--surface-2),color-mix(in srgb,var(--purple) 5%,var(--surface)))}
|
||||
.coloring-paper{position:relative;width:min(100%,900px);aspect-ratio:4/3;flex:none;overflow:hidden;border:1px solid var(--line);border-radius:16px;background:#fff;box-shadow:0 10px 28px rgba(30,50,90,.14);isolation:isolate}
|
||||
.coloring-art,.coloring-paper canvas{position:absolute;inset:0;width:100%;height:100%}
|
||||
.coloring-art svg{width:100%;height:100%;display:block;background:#fff}
|
||||
.coloring-art [hidden]{display:none}
|
||||
.coloring-paper canvas{z-index:2;touch-action:none;cursor:crosshair;mix-blend-mode:multiply}
|
||||
.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-hint{min-height:30px;padding:7px 11px;color:var(--muted);font-size:11px;font-weight:700;text-align:center;background:var(--surface)}
|
||||
.widget[data-widget="coloring"]{--widget-accent:var(--orange)}
|
||||
|
||||
@media(max-width:620px){
|
||||
.coloring-choices,.coloring-tools{padding:6px;gap:4px}
|
||||
.coloring-choices label{grid-template-columns:1fr;gap:2px;flex:1;min-width:125px}
|
||||
.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-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}
|
||||
.coloring-tools .coloring-done{min-width:90px;font-size:11px}
|
||||
.coloring-stage{place-items:start center;padding:6px}
|
||||
.coloring-paper{min-width:360px;border-radius:10px}
|
||||
.coloring-hint{font-size:10px}
|
||||
}
|
||||
@media(pointer:coarse){.coloring-tools button,.coloring-custom input{min-width:40px;min-height:40px}.coloring-choices select{min-height:40px}}
|
||||
@media(prefers-reduced-motion:reduce){.coloring-app *{scroll-behavior:auto!important}}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
<link rel="icon" type="image/svg+xml" href="img/logo.svg?v=__V__">
|
||||
<link rel="preload" href="fonts/quicksand.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="css/teach.css?v=__V__">
|
||||
<link rel="stylesheet" href="css/coloring.css?v=__V__">
|
||||
<link rel="stylesheet" href="css/live-classroom.css?v=__V__">
|
||||
<link rel="stylesheet" href="css/image-catalog.css?v=__V__">
|
||||
<!-- alle app-scripts: defer = parallel downloaden, uitvoeren in dézelfde
|
||||
|
|
@ -49,6 +50,7 @@
|
|||
<script src="js/widgets/sentence.js?v=__V__" defer></script>
|
||||
<script src="js/widgets/learning.js?v=__V__" defer></script>
|
||||
<script src="js/widgets/world.js?v=__V__" defer></script>
|
||||
<script src="js/widgets/coloring.js?v=__V__" defer></script>
|
||||
<script src="js/board.js?v=__V__" defer></script>
|
||||
<script src="js/pupil.js?v=__V__" defer></script>
|
||||
<script src="js/pupil-live-actions.js?v=__V__" defer></script>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
/* Klassenmanagement: welke klas/vakgebied nu getoond wordt, blijft behouden
|
||||
over her-renders heen zoals adminTab hierboven. */
|
||||
let kmClass = null, kmSubject = "rekenen";
|
||||
const KM_SUBJECTS = ["taal", "rekenen", "world"];
|
||||
const KM_SUBJECTS = ["taal", "rekenen", "world", "creatief"];
|
||||
let stopDashboardLive=()=>{};
|
||||
let liveDashboardState="reconnecting";
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ const REGISTRY = [
|
|||
{ id:"birds", cat:"world", icon:"🐦", w:620, h:600, minW:360, minH:400, mount:mountBirds },
|
||||
{ id:"plants", cat:"world", icon:"🌼", w:620, h:600, minW:360, minH:400, mount:mountPlants },
|
||||
{ id:"topography", cat:"world", icon:"🗺️", w:680, h:640, minW:380, minH:440, mount:mountTopography },
|
||||
{ id:"coloring", cat:"creatief", icon:"🎨", w:760, h:700, minW:390, minH:430, mount:mountColoring },
|
||||
{ id:"write", cat:"tools", icon:"✏️", w:560, h:440, minW:360, minH:280, mount:mountWrite },
|
||||
{ id:"mind", cat:"tools", icon:"🕸️", w:640, h:480, minW:420, minH:320, mount:mountMind },
|
||||
{ id:"notes", cat:"tools", icon:"📝", w:340, h:300, minW:240, minH:180, mount:mountNotes },
|
||||
|
|
@ -68,7 +69,7 @@ const REGISTRY = [
|
|||
{ id:"avatar", cat:"class", icon:"🦖", w:620, h:560, minW:400, minH:400, mount:mountAvatarWidget },
|
||||
{ id:"media", cat:"media", icon:"🎬", w:560, h:420, minW:320, minH:240, mount:mountMedia }
|
||||
];
|
||||
const WIDGET_CATS = [["favorites","galleryFavorites"],["taal","catTaal"],["rekenen","catRekenen"],["world","catWorld"],["class","catClass"],["tools","catTools"],["media","catMedia"]];
|
||||
const WIDGET_CATS = [["favorites","galleryFavorites"],["taal","catTaal"],["rekenen","catRekenen"],["world","catWorld"],["creatief","catCreative"],["class","catClass"],["tools","catTools"],["media","catMedia"]];
|
||||
|
||||
/* houdt een (opgeslagen of standaard) widgetgrootte binnen het bord. Op een
|
||||
telefoon is de beschikbare ruimte leidend: een desktop-minimum van bv.
|
||||
|
|
@ -369,7 +370,7 @@ function makeWidget(def, saved){
|
|||
? boardLearningMountState(def, state, boardLearning) : state;
|
||||
mountedApi = def.mount(body, mountedState) || {};
|
||||
if(typeof appendLearningProfileBadge === "function" && mountedState &&
|
||||
["taal","rekenen"].includes(def.cat)){
|
||||
["taal","rekenen","creatief"].includes(def.cat)){
|
||||
appendLearningProfileBadge(body, def.cat, mountedState, def.id);
|
||||
}
|
||||
if(boardLearning && typeof appendBoardLearningLevelControls === "function"){
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ let castNotice = "";
|
|||
let castActiveKeys = new Set();
|
||||
let castError = "";
|
||||
|
||||
const CAST_WIDGET_CATEGORIES=new Set(["taal","rekenen"]);
|
||||
const CAST_WIDGET_CATEGORIES=new Set(["taal","rekenen","creatief"]);
|
||||
const CAST_THEME_WIDGETS=new Set(["letters","flits","hak","ball"]);
|
||||
function assignmentLibraryWidgets(){
|
||||
return REGISTRY.filter(widget=>CAST_WIDGET_CATEGORIES.has(widget.cat));
|
||||
|
|
|
|||
|
|
@ -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.64-beta";
|
||||
const VERSION = "0.4.65-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,6 +49,12 @@ 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.",
|
||||
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.",
|
||||
colorReset:"Kleurplaat leegmaken", colorDone:"Ik ben klaar", colorFinished:"Kleurplaat klaar!",
|
||||
bgBlank:"Leeg blad", bgLines:"Lijntjes", bgWrite:"Schrijflijnen",
|
||||
bgGridS:"Kleine rekenblokjes", bgGridB:"Grote rekenblokjes",
|
||||
penSize:"Dikte", penColor:"Kleur", penExtras:"Hulpmiddelen", recentColors:"Recent gebruikt", paletteColors:"Kleuren", customColor:"Eigen kleur", eraser:"Gum", clearAll:"Alles wissen",
|
||||
|
|
@ -73,7 +79,7 @@ const I18N = {
|
|||
anchorLibEmptyGrid:"Zet eerst woorden in het diagram.",
|
||||
numWords:"Woorden per spel", all:"Alle", noThemes:"— nog geen thema's —",
|
||||
muteAll:"Alle geluiden aan/uit", sndWidget:"Geluid van deze widget aan/uit",
|
||||
catTaal:"Taal", catRekenen:"Rekenen", catWorld:"Wereldoriëntatie", catClass:"Feest & klas", catTools:"Hulpmiddelen", catMedia:"Media",
|
||||
catTaal:"Taal", catRekenen:"Rekenen", catWorld:"Wereldoriëntatie", catCreative:"Creatief", catClass:"Feest & klas", catTools:"Hulpmiddelen", catMedia:"Media",
|
||||
wg_madd:"Plussommen", wg_madd_d:"Optellen tot 10, 20 of 100.",
|
||||
wg_msub:"Minsommen", wg_msub_d:"Aftrekken tot 10, 20 of 100.",
|
||||
wg_mmul:"Keersommen", wg_mmul_d:"De tafels van 1 t/m 12 oefenen.",
|
||||
|
|
@ -263,8 +269,8 @@ const I18N = {
|
|||
wikiOfficialSource:"Open officiële SLO-publicatie",
|
||||
wikiOfficialArea:"Bekijk {area} in de officiële publicatie",
|
||||
wikiTuleTitle:"TULE en de nieuwe leerlijnen",
|
||||
wikiYearProfiles:"Leerjaarprofielen taal en rekenen",
|
||||
wikiYearProfilesText:"Praktische Teach-uitwerking voor widgetopdrachten per groep. Gebaseerd op de kerndoelen 2026 en TULE; geen officiële SLO-jaarnormen.",
|
||||
wikiYearProfiles:"Leerjaarprofielen taal, rekenen en creatief",
|
||||
wikiYearProfilesText:"Praktische Teach-uitwerking voor widgetopdrachten per groep. De creatieve profielen volgen de opbouw in detail en techniek; het zijn geen officiële jaarnormen.",
|
||||
wikiYear:"Groep {n}", wikiProfileGoals:"Leerdoelen",
|
||||
wikiTuleText:"TULE werkt de kerndoelen uit 2006 voorbeeldmatig uit. Nieuwe SLO-leerlijnen bij de doelen van 2026 zijn nog in ontwikkeling. Een niveau-indeling in Teach is daarom geen officiële SLO-leerlijn.",
|
||||
wikiTuleSource:"Open TULE bij SLO", wikiProgressionsSource:"Volg de ontwikkeling van nieuwe leerlijnen",
|
||||
|
|
@ -310,9 +316,9 @@ const I18N = {
|
|||
vwPreview:"Voorbeeldweergave", vwPreviewReset:"Terug naar systeem",
|
||||
vwTeacherPath:"🎯 Opdrachtenreeks", vwTeacherDashboard:"📈 Klasdashboard",
|
||||
vwParentPreview:"Voorbeeld van het ouderportaal. Echte kindgegevens en acties zijn in deze weergave uitgeschakeld.",
|
||||
vwPupilPreview:"Voorbeeld van de leerlingomgeving met de taal- en rekenwidgets van het huidige bord. Bediening en voortgang zijn uitgeschakeld.",
|
||||
vwPupilPreview:"Voorbeeld van de leerlingomgeving met de taal-, reken- en creatieve widgets van het huidige bord. Bediening en voortgang zijn uitgeschakeld.",
|
||||
vwPupilAddWidget:"Widget toevoegen", vwPupilChooseWidget:"Kies een leerlingwidget",
|
||||
vwPupilAddWidgetNote:"Alleen taal- en rekenwidgets die in de leerlingomgeving werken worden getoond.",
|
||||
vwPupilAddWidgetNote:"Alleen taal-, reken- en creatieve widgets die in de leerlingomgeving werken worden getoond.",
|
||||
vwNote:"Deze voorkeuren gelden voor dit apparaat.",
|
||||
pwChanged:"Wachtwoord gewijzigd ✓", pwCurrentPh:"huidig wachtwoord",
|
||||
namePh:"naam", passPh:"wachtwoord",
|
||||
|
|
@ -342,7 +348,7 @@ const I18N = {
|
|||
parentUnlink:"Ontkoppel",
|
||||
parentUnlinkWarn:"Verwijdert alleen de koppeling; het account van je kind blijft bestaan.",
|
||||
parentProgress:"📈 Voortgang", parentProgressEmpty:"Nog geen voortgang.",
|
||||
parentLevel:"🎯 Leerniveau", parentLevelTaal:"Taal", parentLevelRekenen:"Rekenen",
|
||||
parentLevel:"🎯 Leerniveau", parentLevelTaal:"Taal", parentLevelRekenen:"Rekenen", parentLevelCreative:"Creatief",
|
||||
parentLevelYear:"Groep", parentLevelPath:"Uitwerking", parentLevelSave:"Opslaan",
|
||||
parentLevelSaved:"Leerniveau opgeslagen.",
|
||||
dtLen:"Lengte", dtContrast:"Hoog contrast",
|
||||
|
|
@ -409,7 +415,7 @@ const I18N = {
|
|||
castClasses:"Klassen", castPupils:"Leerlingen",
|
||||
castNoClasses:"Geen klassen in deze school.", castNoPupils:"Geen leerlingen in deze school.",
|
||||
castActive:"✓ Actief", castPathTitle:"Opdrachtenreeks (optioneel)", castPathPlaceholder:"Naam van de reeks…",
|
||||
castPathAdd:"+ Geselecteerde widget als stap", castPathOnlyLearning:"Reeksen ondersteunen taal- en rekenwidgets.", castPathDefault:"Mijn opdrachtenreeks", castPathHint:"{count} van maximaal 12 stappen. Gebruik de pijlen om de volgorde te bepalen.",
|
||||
castPathAdd:"+ Geselecteerde widget als stap", castPathOnlyLearning:"Reeksen ondersteunen taal-, reken- en creatieve widgets.", castPathDefault:"Mijn opdrachtenreeks", castPathHint:"{count} van maximaal 12 stappen. Gebruik de pijlen om de volgorde te bepalen.",
|
||||
castIntro:"Stel zelfstandig een opdracht of reeks samen uit alle leerlingwidgets en geef deze daarna in één keer door.",
|
||||
castLibrarySubtitle:"Zelfstandige leerlingopdrachten · geen bord nodig",
|
||||
castStepChoose:"Kies vorm", castStepBuild:"Stel samen", castStepAudience:"Geef door",
|
||||
|
|
@ -451,7 +457,7 @@ const I18N = {
|
|||
amLevelClassDefault:"Klas-standaard", amLevelPupils:"Leerlingen",
|
||||
amLevelInherited:"erft van klas", amLevelNone:"nog niet ingesteld",
|
||||
amLevelReset:"terug naar klas-standaard", amLevelSaved:"Opgeslagen ✓",
|
||||
amSubject_taal:"Taal", amSubject_rekenen:"Rekenen", amSubject_world:"Wereldoriëntatie",
|
||||
amSubject_taal:"Taal", amSubject_rekenen:"Rekenen", amSubject_world:"Wereldoriëntatie", amSubject_creatief:"Creatief",
|
||||
amConfirmDel:"Weet je zeker dat je deze gebruiker wilt verwijderen?",
|
||||
amCopy:"kopieer", amCopyAll:"kopieer alles",
|
||||
amResetClass:"reset wachtwoorden klas", amResetSchool:"reset alle leerlingwachtwoorden",
|
||||
|
|
@ -473,6 +479,12 @@ 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.",
|
||||
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.",
|
||||
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",
|
||||
penSize:"Thickness", penColor:"Colour", penExtras:"Tools", recentColors:"Recently used", paletteColors:"Colours", customColor:"Custom colour", eraser:"Eraser", clearAll:"Clear all",
|
||||
|
|
@ -497,7 +509,7 @@ const I18N = {
|
|||
anchorLibEmptyGrid:"Put words in the diagram first.",
|
||||
numWords:"Words per game", all:"All", noThemes:"— no themes yet —",
|
||||
muteAll:"All sounds on/off", sndWidget:"This widget's sound on/off",
|
||||
catTaal:"Language", catRekenen:"Maths", catWorld:"World studies", catClass:"Celebration & class", catTools:"Tools", catMedia:"Media",
|
||||
catTaal:"Language", catRekenen:"Maths", catWorld:"World studies", catCreative:"Creative", catClass:"Celebration & class", catTools:"Tools", catMedia:"Media",
|
||||
wg_madd:"Addition", wg_madd_d:"Adding up to 10, 20 or 100.",
|
||||
wg_msub:"Subtraction", wg_msub_d:"Subtracting up to 10, 20 or 100.",
|
||||
wg_mmul:"Multiplication", wg_mmul_d:"Practise the times tables 1–12.",
|
||||
|
|
@ -687,8 +699,8 @@ const I18N = {
|
|||
wikiOfficialSource:"Open the official SLO publication",
|
||||
wikiOfficialArea:"View {area} in the official publication",
|
||||
wikiTuleTitle:"TULE and the new learning progressions",
|
||||
wikiYearProfiles:"Language and maths year profiles",
|
||||
wikiYearProfilesText:"Practical Teach interpretation for widget tasks per year. Based on the 2026 targets and TULE; not official SLO year standards.",
|
||||
wikiYearProfiles:"Language, maths and creative year profiles",
|
||||
wikiYearProfilesText:"Practical Teach interpretation for widget tasks per year. Creative profiles progress through detail and technique; these are not official year standards.",
|
||||
wikiYear:"Year {n}", wikiProfileGoals:"Learning goals",
|
||||
wikiTuleText:"TULE is an illustrative elaboration of the 2006 targets. New SLO learning progressions for the 2026 targets are still being developed. Teach levels are therefore not official SLO progressions.",
|
||||
wikiTuleSource:"Open TULE at SLO", wikiProgressionsSource:"Follow the development of new progressions",
|
||||
|
|
@ -734,9 +746,9 @@ const I18N = {
|
|||
vwPreview:"Preview mode", vwPreviewReset:"Return to system",
|
||||
vwTeacherPath:"🎯 Learning path", vwTeacherDashboard:"📈 Class dashboard",
|
||||
vwParentPreview:"Preview of the parent portal. Real child data and actions are disabled in this view.",
|
||||
vwPupilPreview:"Preview of the pupil environment using language and maths widgets from the current board. Controls and progress are disabled.",
|
||||
vwPupilPreview:"Preview of the pupil environment using language, maths and creative widgets from the current board. Controls and progress are disabled.",
|
||||
vwPupilAddWidget:"Add widget", vwPupilChooseWidget:"Choose a pupil widget",
|
||||
vwPupilAddWidgetNote:"Only language and maths widgets supported in the pupil environment are shown.",
|
||||
vwPupilAddWidgetNote:"Only language, maths and creative widgets supported in the pupil environment are shown.",
|
||||
vwNote:"These preferences apply to this device.",
|
||||
pwChanged:"Password changed ✓", pwCurrentPh:"current password",
|
||||
namePh:"name", passPh:"password",
|
||||
|
|
@ -766,7 +778,7 @@ const I18N = {
|
|||
parentUnlink:"Unlink",
|
||||
parentUnlinkWarn:"Removes only the link; your child's account remains.",
|
||||
parentProgress:"📈 Progress", parentProgressEmpty:"No progress yet.",
|
||||
parentLevel:"🎯 Learning level", parentLevelTaal:"Language", parentLevelRekenen:"Maths",
|
||||
parentLevel:"🎯 Learning level", parentLevelTaal:"Language", parentLevelRekenen:"Maths", parentLevelCreative:"Creative",
|
||||
parentLevelYear:"Year", parentLevelPath:"Path", parentLevelSave:"Save",
|
||||
parentLevelSaved:"Learning level saved.",
|
||||
dtLen:"Length", dtContrast:"High contrast",
|
||||
|
|
@ -833,7 +845,7 @@ const I18N = {
|
|||
castClasses:"Classes", castPupils:"Pupils",
|
||||
castNoClasses:"No classes in this school.", castNoPupils:"No pupils in this school.",
|
||||
castActive:"✓ Active", castPathTitle:"Learning path (optional)", castPathPlaceholder:"Name of the path…",
|
||||
castPathAdd:"+ Add selected widget as step", castPathOnlyLearning:"Paths support language and maths widgets.", castPathDefault:"My learning path", castPathHint:"{count} of up to 12 steps. Use the arrows to set the order.",
|
||||
castPathAdd:"+ Add selected widget as step", castPathOnlyLearning:"Paths support language, maths and creative widgets.", castPathDefault:"My learning path", castPathHint:"{count} of up to 12 steps. Use the arrows to set the order.",
|
||||
castIntro:"Build an independent assignment or path from every pupil widget, then share it in one go.",
|
||||
castLibrarySubtitle:"Independent pupil assignments · no board required",
|
||||
castStepChoose:"Choose format", castStepBuild:"Build it", castStepAudience:"Assign it",
|
||||
|
|
@ -875,7 +887,7 @@ const I18N = {
|
|||
amLevelClassDefault:"Class default", amLevelPupils:"Pupils",
|
||||
amLevelInherited:"inherited from class", amLevelNone:"not set yet",
|
||||
amLevelReset:"reset to class default", amLevelSaved:"Saved ✓",
|
||||
amSubject_taal:"Language", amSubject_rekenen:"Maths", amSubject_world:"World orientation",
|
||||
amSubject_taal:"Language", amSubject_rekenen:"Maths", amSubject_world:"World orientation", amSubject_creatief:"Creative",
|
||||
amConfirmDel:"Are you sure you want to delete this user?",
|
||||
amCopy:"copy", amCopyAll:"copy all",
|
||||
amResetClass:"reset class passwords", amResetSchool:"reset all pupil passwords",
|
||||
|
|
|
|||
|
|
@ -91,8 +91,8 @@ const CURRICULUM_PO_2026=Object.freeze({
|
|||
id:"kunst-en-cultuur",icon:"🎨",title:"Kunst en cultuur",goalsRange:"35–37",page:92,
|
||||
description:"Artistiek creatief vermogen ontwikkelen, kunstzinnige uitingen maken en kunst en cultuur beleven en duiden.",
|
||||
goals:[
|
||||
{id:35,domain:"Artistiek creatief vermogen",summary:"Leerlingen gebruiken creatieve strategieën in een iteratief proces en geven betekenis aan eigen en andermans werk.",parts:["Creatieve maak- en denkstrategieën","Ontwikkelen van creatieve vermogens","Betekenis geven"],coverage:"support",widgets:["write","mind","media","birthday","avatar"]},
|
||||
{id:36,domain:"Maken en betekenis geven",summary:"Leerlingen verbeelden ideeën en gebruiken technieken, vaardigheden, materialen en vaktaal uit verschillende kunstdisciplines.",parts:["Verbeelden en creëren","Kunstzinnige technieken en vaardigheden"],coverage:"support",widgets:["write","media","birthday","avatar"]},
|
||||
{id:35,domain:"Artistiek creatief vermogen",summary:"Leerlingen gebruiken creatieve strategieën in een iteratief proces en geven betekenis aan eigen en andermans werk.",parts:["Creatieve maak- en denkstrategieën","Ontwikkelen van creatieve vermogens","Betekenis geven"],coverage:"support",widgets:["write","mind","media","birthday","avatar","coloring"]},
|
||||
{id:36,domain:"Maken en betekenis geven",summary:"Leerlingen verbeelden ideeën en gebruiken technieken, vaardigheden, materialen en vaktaal uit verschillende kunstdisciplines.",parts:["Verbeelden en creëren","Kunstzinnige technieken en vaardigheden"],coverage:"support",widgets:["write","media","birthday","avatar","coloring"]},
|
||||
{id:37,domain:"Meemaken en betekenis geven",summary:"Leerlingen beleven kunst en cultuur actief en verbinden uitingen aan maker, tijd, plaats, functie en context.",parts:["Kunst en cultuur actief meemaken","Kunst en cultuur in context"],coverage:"open",widgets:[]}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* teach - gedeelde leerjaarprofielen voor taal en rekenen
|
||||
/* teach - gedeelde leerjaarprofielen voor taal, rekenen en creatief
|
||||
Praktische Teach-uitwerking van de nieuwe kerndoelen po (SLO, juli 2026)
|
||||
en TULE. SLO ontwikkelt de nieuwe leerlijnen nog en kent ze geen wettelijke
|
||||
status toe; deze profielen zijn daarom geen officiele SLO-jaarnormen. */
|
||||
|
|
@ -31,6 +31,17 @@ const LEARNING_YEAR_PROFILES=Object.freeze({
|
|||
lp("5-6","Grote getallen, kommagetallen en procenten",["standaardprocedures gebruiken","kommagetallen en breuken verbinden","eenvoudige procenten en verhoudingen berekenen"]),
|
||||
lp("7-8","Verhoudingen, procenten en meten",["breuken, kommagetallen en procenten verbinden","bewerkingen doelmatig kiezen","meetkundige problemen oplossen"]),
|
||||
lp("7-8","Redeneren en rekenen in samenhang",["meer-stapsproblemen modelleren","schatten en uitkomsten beoordelen","rekenwijzen helder uitleggen"])
|
||||
]),
|
||||
creatief:Object.freeze([
|
||||
null,
|
||||
lp("1-2","Grote vormen en vrij kleur ontdekken",["grote vlakken doelgericht kleuren","potloodgreep en druk verkennen","herkenbare thema's uit de leefwereld kiezen"]),
|
||||
lp("1-2","Kleur en eenvoudige patronen",["binnen duidelijke contouren werken","kleuren combineren","eenvoudige herhaling herkennen"]),
|
||||
lp("3-4","Vormen combineren en details zien",["middelgrote vlakken vullen","details met een potlood toevoegen","kleurkeuzes bij een thema uitleggen"]),
|
||||
lp("3-4","Detail, herhaling en contrast",["kleinere vakken nauwkeurig kleuren","patronen voortzetten","licht-donkercontrast toepassen"]),
|
||||
lp("5-6","Ruimte, natuur en verbeelding",["voor- en achtergrond onderscheiden","thematische kleuren onderzoeken","eigen details aan lijnwerk toevoegen"]),
|
||||
lp("5-6","Kleurharmonie en complexe vormen",["kleurfamilies bewust combineren","complexere contouren beheersen","techniek en sfeer verbinden"]),
|
||||
lp("7-8","Complexe patronen en betekenis",["veel detail planmatig uitwerken","kleurcontrast doelgericht inzetten","beeldkeuzes aan een thema koppelen"]),
|
||||
lp("7-8","Zelfstandige stijl en verfijning",["een eigen kleurpalet ontwikkelen","gelaagd en nauwkeurig werken","artistieke keuzes toelichten"])
|
||||
])
|
||||
});
|
||||
const LANGUAGE_WIDGET_TASKS=Object.freeze({
|
||||
|
|
@ -53,6 +64,9 @@ const MATH_WIDGET_TASKS=Object.freeze({
|
|||
placevalue:["","hoeveelheid en cijfer","tientallen en eenheden","getallen tot 100","getallen tot 1.000","getallen tot 10.000","getallen tot 100.000","getallen tot 1.000.000","grote getalstructuur"],
|
||||
money:["","munten herkennen","hele eurobedragen","eenvoudige bedragen","euros en centen","bedragen en teruggeven","precies rekenen met centen","korting en wisselgeld","budget en procentuele verandering"]
|
||||
});
|
||||
const CREATIVE_WIDGET_TASKS=Object.freeze({
|
||||
coloring:["","grote kleurvlakken","duidelijke vormen en patronen","middelgrote vakken en details","meer detail en contrast","thematische composities","complexe contouren","fijne patronen en kleurplan","zelfstandige kleurstijl"]
|
||||
});
|
||||
|
||||
function learningYearGroup(value){const year=Number(value);return Number.isInteger(year)&&year>=1&&year<=8?year:null}
|
||||
function learningLevel(value){return Math.max(1,Math.min(3,Number(value)||1))}
|
||||
|
|
@ -63,7 +77,8 @@ function learningProfile(subject,yearGroup,level){
|
|||
function learningLevelLabel(level){return (LEARNING_LEVEL_LABELS[LANG]||LEARNING_LEVEL_LABELS.nl)[learningLevel(level)]}
|
||||
function learningProfileSummary(subject,yearGroup,level,widgetId){
|
||||
const p=learningProfile(subject,yearGroup,level);if(!p)return"";
|
||||
const tasks=subject==="taal"?LANGUAGE_WIDGET_TASKS:MATH_WIDGET_TASKS,task=tasks[widgetId]&&tasks[widgetId][p.yearGroup];
|
||||
const tasks=subject==="taal"?LANGUAGE_WIDGET_TASKS:subject==="rekenen"?MATH_WIDGET_TASKS:CREATIVE_WIDGET_TASKS;
|
||||
const task=tasks[widgetId]&&tasks[widgetId][p.yearGroup];
|
||||
return (LANG==="nl"?"Groep ":"Year ")+p.yearGroup+" | "+learningLevelLabel(p.level)+" | "+(task||p.title)
|
||||
}
|
||||
function languageWordComplexity(word){
|
||||
|
|
@ -147,14 +162,15 @@ function mathTaskRules(yearGroup,level){
|
|||
return{addSub:Math.max(10,Math.round(addCap*factor/5)*5),timesTable:year<3?2:year===3?(l===1?2:l===2?5:10):year===4?(l===1?5:10):12,factorMax:year<=3?5:year<=4?10:year<=6?12:20,numberlineMax:Math.max(20,Math.round(numberMax*(l===1?.4:l===2?.7:1))),numberlineMin:year>=7&&l>=2?-100:0,numberlineStep:lineStep,fractionDen:year===1?2:year<=3?4:year===4?8:12,clockLevel:year<=2?1:year<=4?2:3,clockMinutes:year===1?[0]:year===2?[0,30]:year===3?[0,15,30,45]:null,placeLevel:year<=2?1:year<=4?2:3,moneyLevel:year<=2?1:year<=4?2:3,percentSets:year<=5?[50,100]:year===6?[10,25,50,75,100]:year===7?[5,10,20,25,50,75,100]:[5,10,15,20,25,30,40,50,60,70,75,80,90,100]}
|
||||
}
|
||||
|
||||
let SELF_LEARNING_LEVELS={taal:null,rekenen:null};
|
||||
const LEARNING_SUBJECTS=["taal","rekenen","creatief"];
|
||||
let SELF_LEARNING_LEVELS={taal:null,rekenen:null,creatief:null};
|
||||
async function refreshSelfLearningLevels(){
|
||||
if(!currentUser||currentUser.role!=="pupil"||currentUser.schoolId!=null){SELF_LEARNING_LEVELS={taal:null,rekenen:null};return SELF_LEARNING_LEVELS}
|
||||
const pairs=await Promise.all(["taal","rekenen"].map(async subject=>{try{return[subject,await api("/my/level?subject="+subject)]}catch(_){return[subject,null]}}));
|
||||
if(!currentUser||currentUser.role!=="pupil"||currentUser.schoolId!=null){SELF_LEARNING_LEVELS={taal:null,rekenen:null,creatief:null};return SELF_LEARNING_LEVELS}
|
||||
const pairs=await Promise.all(LEARNING_SUBJECTS.map(async subject=>{try{return[subject,await api("/my/level?subject="+subject)]}catch(_){return[subject,null]}}));
|
||||
SELF_LEARNING_LEVELS=Object.fromEntries(pairs);return SELF_LEARNING_LEVELS
|
||||
}
|
||||
function applyLearningLevelToState(def,state,level,managed){
|
||||
const next=state?Object.assign({},state):{};if(!def||!["taal","rekenen"].includes(def.cat)||!level||level.yearGroup==null)return next;
|
||||
const next=state?Object.assign({},state):{};if(!def||!LEARNING_SUBJECTS.includes(def.cat)||!level||level.yearGroup==null)return next;
|
||||
next.yearGroup=learningYearGroup(level.yearGroup);next.level=learningLevel(level.level);next.managedLearningLevel=managed!==false;return next
|
||||
}
|
||||
function learningStateForWidget(def,state){
|
||||
|
|
@ -166,7 +182,7 @@ function learningStateForWidget(def,state){
|
|||
in de systeem-, schoolbeheerder- en leerkrachtweergave. Ouder- en
|
||||
leerlingvoorbeelden gebruiken een aparte, alleen-lezen voorbeeldweergave. */
|
||||
function canConfigureBoardLearningLevel(def){
|
||||
if(!def||!["taal","rekenen"].includes(def.cat)||!currentUser)return false;
|
||||
if(!def||!LEARNING_SUBJECTS.includes(def.cat)||!currentUser)return false;
|
||||
const role=typeof activeRole==="function"?activeRole():currentUser.role;
|
||||
return currentUser.role==="teacher"||
|
||||
(currentUser.role==="super"&&["super","admin","teacher"].includes(role))
|
||||
|
|
@ -203,10 +219,10 @@ function appendLearningProfileBadge(root,subject,state,widgetId){
|
|||
|
||||
(function registerOwnLearningLevelSettings(){
|
||||
const host=document.createElement("div");host.className="st-section";host.dataset.section="learning-level";host.style.display="none";host.id="learningLevelSettings";document.getElementById("stContent").appendChild(host);
|
||||
const text=()=>LANG==="nl"?{title:"Leerniveau",sub:"Kies zelf het passende leerjaar voor taal en rekenen.",note:"Deze keuze bepaalt woorden, getalgrenzen en opdrachten. De profielen zijn een Teach-uitwerking op basis van SLO/TULE; geen officiele SLO-jaarnormen.",save:"Opslaan",saved:"Leerniveau opgeslagen.",year:"Groep",taal:"Taal",rekenen:"Rekenen"}:{title:"Learning level",sub:"Choose the suitable year for language and maths.",note:"This choice determines words, number ranges and tasks. The profiles are a Teach interpretation based on SLO/TULE; not official SLO year standards.",save:"Save",saved:"Learning level saved.",year:"Year",taal:"Language",rekenen:"Maths"};
|
||||
const text=()=>LANG==="nl"?{title:"Leerniveau",sub:"Kies het passende leerjaar voor taal, rekenen en creatief.",note:"Deze keuze bepaalt woorden, getalgrenzen, kleurplaatdetail en opdrachten. De profielen zijn een praktische Teach-uitwerking; geen officiele jaarnormen.",save:"Opslaan",saved:"Leerniveau opgeslagen.",year:"Groep",taal:"Taal",rekenen:"Rekenen",creatief:"Creatief"}:{title:"Learning level",sub:"Choose the suitable year for language, maths and creative work.",note:"This choice determines words, number ranges, colouring-page detail and tasks. The profiles are a practical Teach interpretation; not official year standards.",save:"Save",saved:"Learning level saved.",year:"Year",taal:"Language",rekenen:"Maths",creatief:"Creative"};
|
||||
const render=async()=>{
|
||||
host.innerHTML="";const tx=text(),msg=document.createElement("div");msg.className="formMsg";host.appendChild(msg);await refreshSelfLearningLevels();
|
||||
for(const subject of ["taal","rekenen"]){
|
||||
for(const subject of LEARNING_SUBJECTS){
|
||||
const current=SELF_LEARNING_LEVELS[subject]||{yearGroup:4,level:2},card=document.createElement("section");card.className="learning-level-card";
|
||||
const h=document.createElement("h3");h.textContent=tx[subject];const summary=document.createElement("p");summary.className="guestnote";const controls=document.createElement("div");controls.className="am-inline";
|
||||
const year=document.createElement("select"),level=document.createElement("select");year.className=level.className="am-sel";
|
||||
|
|
@ -217,5 +233,5 @@ function appendLearningProfileBadge(root,subject,state,widgetId){
|
|||
}
|
||||
const note=document.createElement("p");note.className="guestnote learning-source-note";note.textContent=tx.note;host.appendChild(note)
|
||||
};
|
||||
registerSettingsSection({id:"learning-level",icon:"🎯",group:"personal",order:1.5,label:()=>text().title,sub:()=>text().sub,visible:()=>!!currentUser&¤tUser.role==="pupil"&¤tUser.schoolId==null,activate(){showSettingsDom("learning-level");return render()},keywords:()=>[text().title,text().year,"taal","rekenen","groep","niveau"]})
|
||||
registerSettingsSection({id:"learning-level",icon:"🎯",group:"personal",order:1.5,label:()=>text().title,sub:()=>text().sub,visible:()=>!!currentUser&¤tUser.role==="pupil"&¤tUser.schoolId==null,activate(){showSettingsDom("learning-level");return render()},keywords:()=>[text().title,text().year,"taal","rekenen","creatief","groep","niveau"]})
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -324,7 +324,7 @@ function renderLiveClassOverview(host,dashboard,live,onPupil,onAction){
|
|||
?REGISTRY.find(def=>def.id===pupil.session?.widgetType)?.cat:null;
|
||||
actionButton("↘ "+T("amActionEasier"),"easier",
|
||||
()=>{if(window.confirm(T("amActionEasierConfirm")))trigger("easier",{subject});},
|
||||
!pupil.session||!["taal","rekenen","world"].includes(subject));
|
||||
!pupil.session||!["taal","rekenen","world","creatief"].includes(subject));
|
||||
const pauseKind=pupil.session?.paused?"resume":"pause";
|
||||
actionButton((pauseKind==="pause"?"⏸ ":"▶ ")+
|
||||
T(pauseKind==="pause"?"amActionPause":"amActionResume"),pauseKind,
|
||||
|
|
|
|||
|
|
@ -106,10 +106,11 @@ function parentChildRow(c){
|
|||
levelHolder.innerHTML = "";
|
||||
try{
|
||||
const data = await api(`/parent/children/${c.id}/levels`);
|
||||
["taal","rekenen"].forEach(subject=>{
|
||||
["taal","rekenen","creatief"].forEach(subject=>{
|
||||
const current = data.levels?.[subject] || {yearGroup:4,level:2};
|
||||
const card = prH("section","learning-level-card");
|
||||
card.appendChild(prH("h3",null,T(subject==="taal"?"parentLevelTaal":"parentLevelRekenen")));
|
||||
const subjectKey={taal:"parentLevelTaal",rekenen:"parentLevelRekenen",creatief:"parentLevelCreative"}[subject];
|
||||
card.appendChild(prH("h3",null,T(subjectKey)));
|
||||
const summary = prH("p","guestnote");
|
||||
const controls = prH("div","am-inline");
|
||||
const year = prH("select","am-sel"), level = prH("select","am-sel");
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
voortgang wordt gelogd) of "kijken" (alleen meekijken: alle interactie
|
||||
geblokkeerd via een overlay, snellere poll zodat wijzigingen van de
|
||||
leerkracht vlot doorkomen). */
|
||||
const PUPIL_WIDGET_CATS = ["taal", "rekenen"];
|
||||
const PUPIL_WIDGET_CATS = ["taal", "rekenen", "creatief"];
|
||||
const PUPIL_POLL_MS = { kijken: 5000, werken: 20000 };
|
||||
const PUPIL_LIVE_HEARTBEAT_MS = 12000;
|
||||
const pupilView = document.getElementById("pupilView");
|
||||
|
|
|
|||
368
public/js/widgets/coloring.js
Normal file
368
public/js/widgets/coloring.js
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
/* teach - thematische kleurplaten met gekleurd potlood en vlakvulling */
|
||||
"use strict";
|
||||
|
||||
const COLORING_THEME_KEYS=Object.freeze({
|
||||
all:"colorThemeAll",animals:"colorThemeAnimals",seasons:"colorThemeSeasons",
|
||||
nature:"colorThemeNature",science:"colorThemeScience",culture:"colorThemeCulture"
|
||||
});
|
||||
const COLORING_COLORS=Object.freeze([
|
||||
"#26344a","#e0554d","#f28c3c","#f7c948","#3fae6a","#2fb7a8",
|
||||
"#3b7dd8","#7557c8","#d85aa5","#8b5a3c","#f2b38f","#ffffff"
|
||||
]);
|
||||
|
||||
function coloringPart(tag,id,attrs="",level=1){
|
||||
return `<${tag} data-fillable="true" data-part="${id}"${level>1?` data-level="${level}"`:""} ${attrs}/>`;
|
||||
}
|
||||
function coloringLine(tag,attrs="",level=1){
|
||||
return `<${tag}${level>1?` data-level="${level}"`:""} ${attrs}/>`;
|
||||
}
|
||||
function coloringScene(content){
|
||||
return `<svg viewBox="0 0 800 600" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<g class="coloring-svg-lines" fill="#fff" stroke="#26344a" stroke-width="7" stroke-linecap="round" stroke-linejoin="round">
|
||||
${content}
|
||||
</g>
|
||||
</svg>`;
|
||||
}
|
||||
function coloringPetals(cx,cy,prefix,level=2){
|
||||
return [0,60,120,180,240,300].map((angle,index)=>{
|
||||
const rad=angle*Math.PI/180,x=(cx+Math.cos(rad)*27).toFixed(1),y=(cy+Math.sin(rad)*27).toFixed(1);
|
||||
return coloringPart("circle",`${prefix}-petal-${index}`,`cx="${x}" cy="${y}" r="20"`,level);
|
||||
}).join("")+coloringPart("circle",`${prefix}-heart`,`cx="${cx}" cy="${cy}" r="17"`,level);
|
||||
}
|
||||
function coloringStars(points,prefix,level=2){
|
||||
return points.map(([x,y,r],index)=>coloringPart("path",`${prefix}-${index}`,
|
||||
`d="M ${x} ${y-r} L ${x+r*.24} ${y-r*.25} L ${x+r} ${y} L ${x+r*.24} ${y+r*.25} L ${x} ${y+r} L ${x-r*.24} ${y+r*.25} L ${x-r} ${y} L ${x-r*.24} ${y-r*.25} Z"`,level)).join("");
|
||||
}
|
||||
|
||||
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"')+
|
||||
[[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)
|
||||
)}
|
||||
function sceneAutumn(){return coloringScene(
|
||||
coloringPart("rect","sky",'x="4" y="4" width="792" height="592" rx="28"')+
|
||||
coloringPart("path","ground",'d="M 4 440 Q 190 405 380 448 T 796 430 L 796 596 L 4 596 Z"')+
|
||||
coloringPart("path","trunk",'d="M 337 500 Q 365 380 358 250 Q 398 285 420 250 Q 415 390 463 500 Z"')+
|
||||
[[390,162,115],[285,215,95],[505,225,104],[340,290,92],[460,315,88]].map(([x,y,r],i)=>
|
||||
coloringPart("circle",`crown-${i}`,`cx="${x}" cy="${y}" r="${r}"`,i>2?2:1)).join("")+
|
||||
[[115,175,-18],[660,165,20],[175,355,12],[610,390,-24],[90,505,15],[705,500,-8]].map(([x,y,a],i)=>
|
||||
coloringPart("path",`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})"`,2)).join("")+
|
||||
coloringStars([[155,90,20],[585,95,18],[730,300,16]],"wind-leaf",3)+
|
||||
coloringLine("path",'d="M 85 120 Q 170 80 240 118 M 560 120 Q 645 80 720 118" fill="none"',3)
|
||||
)}
|
||||
function sceneCake(){return coloringScene(
|
||||
coloringPart("rect","party-wall",'x="4" y="4" width="792" height="592" rx="28"')+
|
||||
coloringPart("path","table",'d="M 4 445 L 796 445 L 796 596 L 4 596 Z"')+
|
||||
coloringPart("rect","cake-bottom",'x="190" y="360" width="420" height="120" rx="24"')+
|
||||
coloringPart("rect","cake-middle",'x="245" y="265" width="310" height="105" rx="22"')+
|
||||
coloringPart("rect","cake-top",'x="305" y="180" width="190" height="95" rx="20"')+
|
||||
coloringPart("path","icing-bottom",'d="M 190 390 Q 225 430 260 390 Q 300 430 340 390 Q 380 430 420 390 Q 460 430 500 390 Q 555 430 610 390 L 610 360 L 190 360 Z"',2)+
|
||||
coloringPart("path","icing-middle",'d="M 245 295 Q 285 330 325 295 Q 365 330 405 295 Q 450 330 495 295 Q 525 325 555 295 L 555 265 L 245 265 Z"',2)+
|
||||
coloringPart("rect","candle",'x="382" y="93" width="36" height="92" rx="8"')+
|
||||
coloringPart("path","flame",'d="M 400 92 Q 355 55 400 20 Q 445 55 400 92 Z"')+
|
||||
[[110,115],[690,120],[135,280],[665,275]].map(([x,y],i)=>coloringPart("path",`balloon-${i}`,`d="M ${x} ${y+55} C ${x-65} ${y+20} ${x-48} ${y-65} ${x} ${y-65} C ${x+48} ${y-65} ${x+65} ${y+20} ${x} ${y+55} Z"`,i>1?2:1)).join("")+
|
||||
coloringLine("path",'d="M 110 170 Q 130 260 95 350 M 690 175 Q 670 260 705 350 M 135 335 Q 160 390 140 440 M 665 330 Q 640 390 660 440" fill="none"')+
|
||||
coloringStars([[205,95,22],[595,72,22],[90,405,15],[720,410,15]],"confetti",3)
|
||||
)}
|
||||
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("circle","fish-eye",'cx="395" cy="230" r="18"')+
|
||||
coloringPart("path","turtle-shell",'d="M 510 390 Q 585 285 665 390 Q 600 485 510 390 Z"')+
|
||||
coloringPart("circle","turtle-head",'cx="690" cy="382" r="38"')+
|
||||
[[530,330,-28],[550,450,28],[635,330,25],[625,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("")+
|
||||
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 542 380 L 648 400 M 570 335 L 610 450 M 545 390 L 590 330 L 645 390 L 590 450 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("")+
|
||||
coloringPart("path","seaweed-a",'d="M 120 525 Q 70 455 120 385 Q 165 455 120 525 Z"',2)+
|
||||
coloringPart("path","seaweed-b",'d="M 190 535 Q 145 470 205 410 Q 240 475 190 535 Z"',3)
|
||||
)}
|
||||
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"')+
|
||||
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)+
|
||||
coloringLine("path",'d="M 165 205 L 165 315 M 430 185 L 430 280" fill="none"',2)
|
||||
)}
|
||||
function sceneWindmill(){return coloringScene(
|
||||
coloringPart("rect","dutch-sky",'x="4" y="4" width="792" height="592" rx="28"')+
|
||||
coloringPart("path","polder",'d="M 4 410 Q 190 380 365 420 T 796 405 L 796 596 L 4 596 Z"')+
|
||||
coloringPart("path","water",'d="M 4 505 Q 200 475 400 515 T 796 500 L 796 596 L 4 596 Z"',2)+
|
||||
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("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)+
|
||||
coloringPart("rect","house-window-b",'x="175" y="375" width="45" height="45"',3)+
|
||||
coloringLine("path",'d="M 35 185 Q 75 155 115 185 Q 155 155 195 185 M 595 75 Q 630 50 665 75 Q 700 50 735 75" fill="none"',3)
|
||||
)}
|
||||
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)+
|
||||
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("")
|
||||
)}
|
||||
function sceneRainforest(){return coloringScene(
|
||||
coloringPart("rect","forest-sky",'x="4" y="4" width="792" height="592" rx="28"')+
|
||||
coloringPart("path","forest-floor",'d="M 4 475 Q 190 430 360 490 T 796 455 L 796 596 L 4 596 Z"')+
|
||||
coloringPart("path","tree-trunk-left",'d="M 40 500 Q 95 330 70 45 L 205 45 Q 165 330 235 500 Z"')+
|
||||
coloringPart("path","tree-trunk-right",'d="M 585 500 Q 640 300 615 35 L 760 35 Q 720 300 785 500 Z"')+
|
||||
coloringPart("path","vine",'d="M 170 55 Q 300 175 230 315 Q 170 430 300 485 Q 400 525 455 425 Q 505 330 430 265"',2)+
|
||||
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("circle","parrot-eye",'cx="470" cy="185" r="14"')+
|
||||
[[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)
|
||||
)}
|
||||
function sceneCastle(){return coloringScene(
|
||||
coloringPart("rect","castle-sky",'x="4" y="4" width="792" height="592" rx="28"')+
|
||||
coloringPart("path","castle-hill",'d="M 4 445 Q 200 380 400 450 T 796 420 L 796 596 L 4 596 Z"')+
|
||||
coloringPart("rect","castle-main",'x="225" y="245" width="350" height="250"')+
|
||||
coloringPart("rect","tower-left",'x="115" y="195" width="155" height="300"')+
|
||||
coloringPart("rect","tower-right",'x="530" y="195" width="155" height="300"')+
|
||||
coloringPart("path","roof-left",'d="M 85 195 L 193 75 L 300 195 Z"')+
|
||||
coloringPart("path","roof-right",'d="M 500 195 L 608 75 L 715 195 Z"')+
|
||||
coloringPart("path","gate",'d="M 350 495 L 350 385 Q 400 325 450 385 L 450 495 Z"')+
|
||||
coloringPart("rect","flag-left",'x="193" y="55" width="92" height="50"',2)+
|
||||
coloringPart("rect","flag-right",'x="608" y="55" width="92" height="50"',2)+
|
||||
[[160,255],[225,255],[575,255],[640,255],[280,300],[475,300]].map(([x,y],i)=>coloringPart("rect",`window-${i}`,`x="${x-20}" y="${y-25}" width="40" height="58" rx="18"`,i>3?3:2)).join("")+
|
||||
coloringLine("path",'d="M 225 345 L 575 345 M 155 385 L 230 385 M 570 385 L 645 385" fill="none"',3)+
|
||||
coloringPart("path","path",'d="M 350 596 Q 365 510 400 445 Q 435 510 470 596 Z"',2)
|
||||
)}
|
||||
function sceneFourSeasons(){return coloringScene(
|
||||
coloringPart("rect","season-sky",'x="4" y="4" width="792" height="592" rx="28"')+
|
||||
coloringPart("path","season-ground",'d="M 4 455 Q 190 420 390 465 T 796 440 L 796 596 L 4 596 Z"')+
|
||||
coloringPart("path","season-trunk",'d="M 345 510 Q 380 350 375 210 Q 420 255 445 205 Q 425 365 475 510 Z"')+
|
||||
coloringPart("path","spring-crown",'d="M 385 230 Q 245 250 210 165 Q 225 65 355 85 Q 430 125 385 230 Z"')+
|
||||
coloringPart("path","summer-crown",'d="M 395 230 Q 430 80 545 75 Q 675 95 620 220 Q 520 285 395 230 Z"')+
|
||||
coloringPart("path","autumn-crown",'d="M 405 240 Q 560 220 635 295 Q 670 415 535 425 Q 420 390 405 240 Z"')+
|
||||
coloringPart("path","winter-crown",'d="M 380 235 Q 335 385 225 420 Q 105 390 165 280 Q 250 210 380 235 Z"')+
|
||||
coloringPetals(280,145,"spring-flower",2)+
|
||||
coloringPart("circle","summer-fruit-a",'cx="515" cy="165" r="24"',2)+coloringPart("circle","summer-fruit-b",'cx="570" cy="215" r="24"',3)+
|
||||
[[500,310,-15],[575,355,20],[460,375,-30]].map(([x,y,a],i)=>coloringPart("path",`autumn-leaf-${i}`,`d="M ${x-28} ${y} Q ${x} ${y-38} ${x+28} ${y} Q ${x} ${y+38} ${x-28} ${y} Z" transform="rotate(${a} ${x} ${y})"`,i===2?3:2)).join("")+
|
||||
coloringStars([[205,310,17],[285,345,15],[160,365,13]],"snowflake",3)
|
||||
)}
|
||||
function sceneEcoCity(){return coloringScene(
|
||||
coloringPart("rect","city-sky",'x="4" y="4" width="792" height="592" rx="28"')+
|
||||
coloringPart("path","city-ground",'d="M 4 455 L 796 455 L 796 596 L 4 596 Z"')+
|
||||
coloringPart("rect","tower",'x="95" y="190" width="155" height="265" rx="8"')+
|
||||
coloringPart("rect","school",'x="285" y="275" width="235" height="180" rx="8"')+
|
||||
coloringPart("path","school-roof",'d="M 260 275 L 402 185 L 545 275 Z"')+
|
||||
coloringPart("rect","apartments",'x="565" y="135" width="145" height="320" rx="8"')+
|
||||
[[135,235],[205,235],[135,305],[205,305],[135,375],[205,375],[600,185],[670,185],[600,255],[670,255],[600,325],[670,325]].map(([x,y],i)=>coloringPart("rect",`city-window-${i}`,`x="${x-22}" y="${y-25}" width="44" height="50"`,i>5?2:1)).join("")+
|
||||
coloringPart("rect","school-door",'x="375" y="365" width="55" height="90" rx="12"')+
|
||||
coloringPart("circle","tree-crown",'cx="60" cy="410" r="55"',2)+coloringPart("rect","tree-trunk",'x="48" y="430" width="24" height="75"',2)+
|
||||
coloringPart("path","bike-frame",'d="M 250 515 L 315 515 L 285 470 L 250 515 L 330 465"',3)+
|
||||
coloringLine("circle",'cx="240" cy="520" r="34" fill="none"',3)+coloringLine("circle",'cx="330" cy="520" r="34" fill="none"',3)+
|
||||
coloringPart("path","solar-a",'d="M 100 190 L 125 135 L 245 135 L 225 190 Z"',2)+coloringPart("path","solar-b",'d="M 580 135 L 600 80 L 705 80 L 690 135 Z"',2)+
|
||||
coloringLine("path",'d="M 125 160 L 235 160 M 165 135 L 155 190 M 205 135 L 195 190 M 600 105 L 700 105 M 635 80 L 625 135 M 670 80 L 660 135" fill="none"',3)
|
||||
)}
|
||||
function sceneMandala(){
|
||||
const rings=[];
|
||||
for(let i=0;i<16;i++){
|
||||
const angle=i*Math.PI/8,cx=400+Math.cos(angle)*155,cy=300+Math.sin(angle)*155;
|
||||
rings.push(coloringPart("ellipse",`mandala-outer-${i}`,`cx="${cx.toFixed(1)}" cy="${cy.toFixed(1)}" rx="38" ry="82" transform="rotate(${i*22.5+90} ${cx.toFixed(1)} ${cy.toFixed(1)})"`,i%2?3:2));
|
||||
}
|
||||
for(let i=0;i<12;i++){
|
||||
const angle=i*Math.PI/6,cx=400+Math.cos(angle)*92,cy=300+Math.sin(angle)*92;
|
||||
rings.push(coloringPart("path",`mandala-inner-${i}`,`d="M 400 300 Q ${cx.toFixed(1)} ${cy.toFixed(1)} ${(400+Math.cos(angle+.28)*145).toFixed(1)} ${(300+Math.sin(angle+.28)*145).toFixed(1)} Q ${cx.toFixed(1)} ${cy.toFixed(1)} 400 300 Z"`,1));
|
||||
}
|
||||
return coloringScene(
|
||||
coloringPart("rect","mandala-bg",'x="4" y="4" width="792" height="592" rx="28"')+
|
||||
rings.join("")+coloringPart("circle","mandala-heart",'cx="400" cy="300" r="62"')+
|
||||
coloringStars([[400,300,34]],"mandala-star",3)
|
||||
);
|
||||
}
|
||||
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("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)+
|
||||
coloringPart("path","dragonfly-body",'d="M 585 220 Q 600 300 585 365 Q 570 300 585 220 Z"',2)+
|
||||
[[545,250,-25],[625,250,25],[550,310,25],[620,310,-25]].map(([x,y,a],i)=>coloringPart("ellipse",`dragonfly-wing-${i}`,`cx="${x}" cy="${y}" rx="55" ry="22" transform="rotate(${a} ${x} ${y})"`,2)).join("")+
|
||||
coloringPart("circle","sun",'cx="115" cy="105" r="55"')+
|
||||
coloringPart("path","reed-a",'d="M 105 440 Q 75 320 105 210 Q 135 320 105 440 Z"',3)+coloringPart("path","reed-b",'d="M 160 430 Q 130 300 165 185 Q 195 310 160 430 Z"',3)+
|
||||
coloringLine("path",'d="M 105 310 L 65 270 M 160 325 L 205 270" fill="none"',3)
|
||||
)}
|
||||
|
||||
const COLORING_PAGES=Object.freeze([
|
||||
{id:"butterfly",theme:"animals",years:[1,2],title:{nl:"Blije vlinder",en:"Happy butterfly"},svg:sceneButterfly()},
|
||||
{id:"autumn",theme:"seasons",years:[1,2],title:{nl:"Herfstboom",en:"Autumn tree"},svg:sceneAutumn()},
|
||||
{id:"cake",theme:"culture",years:[1,2],title:{nl:"Feesttaart",en:"Party cake"},svg:sceneCake()},
|
||||
{id:"ocean",theme:"animals",years:[3,4],title:{nl:"Vrienden in zee",en:"Ocean friends"},svg:sceneOcean()},
|
||||
{id:"garden",theme:"nature",years:[3,4],title:{nl:"Kleine tuindieren",en:"Little garden animals"},svg:sceneGarden()},
|
||||
{id:"windmill",theme:"culture",years:[3,4],title:{nl:"Molen in de polder",en:"Windmill in the polder"},svg:sceneWindmill()},
|
||||
{id:"rocket",theme:"science",years:[5,6],title:{nl:"Raket door de ruimte",en:"Rocket through space"},svg:sceneRocket()},
|
||||
{id:"rainforest",theme:"nature",years:[5,6],title:{nl:"Regenwoud",en:"Rainforest"},svg:sceneRainforest()},
|
||||
{id:"castle",theme:"culture",years:[5,6],title:{nl:"Kasteel en dorp",en:"Castle and village"},svg:sceneCastle()},
|
||||
{id:"four-seasons",theme:"seasons",years:[7,8],title:{nl:"Boom van vier seizoenen",en:"Four seasons tree"},svg:sceneFourSeasons()},
|
||||
{id:"eco-city",theme:"science",years:[7,8],title:{nl:"Duurzame stad",en:"Sustainable city"},svg:sceneEcoCity()},
|
||||
{id:"mandala",theme:"culture",years:[7,8],title:{nl:"Geometrische mandala",en:"Geometric mandala"},svg:sceneMandala()},
|
||||
{id:"ecosystem",theme:"nature",years:[7,8],title:{nl:"Leven rond de vijver",en:"Life around the pond"},svg:sceneEcosystem()}
|
||||
]);
|
||||
|
||||
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){
|
||||
const base=Math.max(2,Math.min(18,Number(size)||6));
|
||||
return pointerType==="pen"&&pressure>0?base*(.45+pressure*1.2):base;
|
||||
}
|
||||
function coloringSafeColor(value,fallback=COLORING_COLORS[1]){
|
||||
return /^#[0-9a-f]{6}$/i.test(String(value||""))?String(value).toLowerCase():fallback;
|
||||
}
|
||||
|
||||
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";
|
||||
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";
|
||||
let pageId=String(initState&&initState.pageId||"");
|
||||
let fills=initState&&initState.pageId===pageId&&initState.fills&&typeof initState.fills==="object"?{...initState.fills}:{};
|
||||
let completed=!!(initState&&initState.completed),restoreInk=initState&&initState.ink;
|
||||
|
||||
root.innerHTML=`<div class="coloring-app">
|
||||
<div class="coloring-choices">
|
||||
<label><span class="coloring-theme-label"></span><select class="coloring-theme"></select></label>
|
||||
<label><span class="coloring-page-label"></span><select class="coloring-page"></select></label>
|
||||
<span class="coloring-fit"></span>
|
||||
</div>
|
||||
<div class="coloring-tools">
|
||||
<div class="coloring-tool-buttons"></div>
|
||||
<div class="coloring-palette"></div>
|
||||
<label class="coloring-custom"><span></span><input type="color"></label>
|
||||
<div class="coloring-sizes"></div>
|
||||
<button class="coloring-reset" type="button">↺</button>
|
||||
<button class="coloring-done tbtn" type="button"></button>
|
||||
</div>
|
||||
<div class="coloring-stage"><div class="coloring-paper" role="img"><div class="coloring-art"></div><canvas width="800" height="600"></canvas></div></div>
|
||||
<div class="coloring-hint" aria-live="polite"></div>
|
||||
</div>`;
|
||||
const app=root.querySelector(".coloring-app"),themeSel=root.querySelector(".coloring-theme"),pageSel=root.querySelector(".coloring-page");
|
||||
const art=root.querySelector(".coloring-art"),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"]];
|
||||
|
||||
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)});
|
||||
[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()};
|
||||
|
||||
function pageTitle(page){return page.title[LANG]||page.title.nl}
|
||||
function eligibleThemes(){return [...new Set(coloringPagesFor(yearGroup).map(page=>page.theme))]}
|
||||
function currentPages(){return coloringPagesFor(yearGroup,theme)}
|
||||
function rebuildThemeOptions(){
|
||||
const previous=theme;themeSel.replaceChildren();
|
||||
const all=new Option(T("colorThemeAll"),"all");themeSel.add(all);
|
||||
eligibleThemes().forEach(id=>themeSel.add(new Option(T(COLORING_THEME_KEYS[id]),id)));
|
||||
theme=themeSel.querySelector(`option[value="${previous}"]`)?previous:"all";themeSel.value=theme;
|
||||
}
|
||||
function rebuildPageOptions(prefer=pageId){
|
||||
const pages=currentPages();pageSel.replaceChildren();
|
||||
pages.forEach(page=>pageSel.add(new Option(pageTitle(page),page.id)));
|
||||
pageId=pages.some(page=>page.id===prefer)?prefer:(pages[0]&&pages[0].id)||"";pageSel.value=pageId;
|
||||
}
|
||||
function clearInk(){ctx.clearRect(0,0,canvas.width,canvas.height)}
|
||||
function loadInk(url){
|
||||
clearInk();if(!url)return;
|
||||
const image=new Image();image.onload=()=>{clearInk();ctx.drawImage(image,0,0,canvas.width,canvas.height)};image.src=url;
|
||||
}
|
||||
function renderPage(keepState){
|
||||
const page=COLORING_PAGES.find(item=>item.id===pageId)||currentPages()[0];if(!page)return;
|
||||
pageId=page.id;art.innerHTML=page.svg;
|
||||
const svg=art.querySelector("svg");paper.setAttribute("aria-label",pageTitle(page));
|
||||
svg.querySelectorAll("[data-level]").forEach(node=>node.toggleAttribute("hidden",Number(node.dataset.level)>level));
|
||||
if(!keepState){fills={};completed=false;restoreInk=null;clearInk()}
|
||||
svg.querySelectorAll("[data-fillable][data-part]").forEach(node=>{
|
||||
const saved=coloringSafeColor(fills[node.dataset.part],"");if(saved)node.setAttribute("fill",saved);
|
||||
});
|
||||
if(keepState&&restoreInk){loadInk(restoreInk);restoreInk=null}
|
||||
refreshTools();updateTexts();
|
||||
}
|
||||
function updateTexts(){
|
||||
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))});
|
||||
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");
|
||||
const selected=pageId;rebuildPageOptions(selected);
|
||||
}
|
||||
function refreshTools(){
|
||||
toolHost.querySelectorAll("button").forEach(button=>{const on=button.dataset.tool===tool;button.classList.toggle("on",on);button.setAttribute("aria-pressed",String(on))});
|
||||
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");
|
||||
}
|
||||
themeSel.onchange=()=>{theme=themeSel.value;rebuildPageOptions("");renderPage(false)};
|
||||
pageSel.onchange=()=>{pageId=pageSel.value;renderPage(false)};
|
||||
art.addEventListener("click",event=>{
|
||||
if(tool!=="bucket")return;const part=event.target.closest("[data-fillable][data-part]");if(!part||part.hasAttribute("hidden"))return;
|
||||
part.setAttribute("fill",color);fills[part.dataset.part]=color;completed=false;updateTexts();
|
||||
});
|
||||
|
||||
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;
|
||||
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))});
|
||||
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}})};
|
||||
document.addEventListener("langchange",()=>{rebuildThemeOptions();updateTexts();renderPage(true)});
|
||||
|
||||
rebuildThemeOptions();rebuildPageOptions(pageId);renderPage(true);
|
||||
return{getState:()=>{
|
||||
let ink=null;try{ink=canvas.toDataURL("image/png")}catch(_){/* een leeg canvas blijft gewoon herstelbaar */}
|
||||
return{theme,pageId,fills:{...fills},ink,tool,color,size,completed,yearGroup,level,managedLearningLevel:!!(initState&&initState.managedLearningLevel)};
|
||||
}};
|
||||
}
|
||||
|
|
@ -130,9 +130,10 @@
|
|||
profiles.appendChild(node("summary",null,T("wikiYearProfiles")));
|
||||
profiles.appendChild(node("p",null,T("wikiYearProfilesText")));
|
||||
const profileGrid=node("div","wiki-profile-grid");
|
||||
[["taal","📖"],["rekenen","🔢"]].forEach(([subject,icon])=>{
|
||||
[["taal","📖"],["rekenen","🔢"],["creatief","🎨"]].forEach(([subject,icon])=>{
|
||||
const column=node("section","wiki-profile-column");
|
||||
column.appendChild(node("h3",null,icon+" "+(subject==="taal"?T("parentLevelTaal"):T("parentLevelRekenen"))));
|
||||
const subjectKey={taal:"parentLevelTaal",rekenen:"parentLevelRekenen",creatief:"parentLevelCreative"}[subject];
|
||||
column.appendChild(node("h3",null,icon+" "+T(subjectKey)));
|
||||
LEARNING_YEAR_PROFILES[subject].slice(1).forEach((profile,index)=>{
|
||||
const row=node("article","wiki-profile-row");
|
||||
row.appendChild(node("strong",null,T("wikiYear").replace("{n}",index+1)));
|
||||
|
|
|
|||
10
src/api.js
10
src/api.js
|
|
@ -1167,12 +1167,12 @@ export default async function api(app) {
|
|||
|
||||
// ---- klassenmanagement: niveaugroep + niveau per leerling per vakgebied -------
|
||||
// Zelfde klas-standaard-met-leerling-override-vorm als assignments hierboven,
|
||||
// nu met vakgebied als extra dimensie (subject: taal/rekenen/world - dezelfde
|
||||
// nu met vakgebied als extra dimensie (subject: taal/rekenen/world/creatief - dezelfde
|
||||
// waarden als REGISTRY's cat in board.js). "groep" (year_group, 1-8) is de
|
||||
// niveaugroep gebaseerd op de leerjaren van de basisschool waaraan de
|
||||
// leerdoelen gekoppeld zijn; "level" (1-3, niet geklemd in het datamodel) is
|
||||
// de fijnmazige verfijning daarbinnen die widgets nu al kennen.
|
||||
const LEVEL_SUBJECTS = ['taal', 'rekenen', 'world'];
|
||||
const LEVEL_SUBJECTS = ['taal', 'rekenen', 'world', 'creatief'];
|
||||
const describePupilLevel = (row) => ({ yearGroup: Number(row.year_group), level: Number(row.level) });
|
||||
const parseLevelBody = (req, reply) => {
|
||||
const { subject, yearGroup, level } = req.body ?? {};
|
||||
|
|
@ -1291,7 +1291,7 @@ export default async function api(app) {
|
|||
if (req.user.school_id != null) return fail(reply, 403, 'het leerniveau wordt door de school beheerd');
|
||||
const body = parseLevelBody(req, reply);
|
||||
if (body.error) return body;
|
||||
if (!['taal', 'rekenen'].includes(body.subject)) return fail(reply, 400, 'ongeldig vakgebied');
|
||||
if (!['taal', 'rekenen', 'creatief'].includes(body.subject)) return fail(reply, 400, 'ongeldig vakgebied');
|
||||
await pool.query(
|
||||
`INSERT INTO pupil_levels (pupil_id, subject, year_group, level) VALUES ($1,$2,$3,$4)
|
||||
ON CONFLICT (pupil_id, subject) WHERE pupil_id IS NOT NULL
|
||||
|
|
@ -1525,7 +1525,7 @@ export default async function api(app) {
|
|||
if (!child || child.role !== 'pupil') return fail(reply, 404, 'kind onbekend');
|
||||
if (child.school_id != null) return { schoolManaged: true, levels: {} };
|
||||
const rows = (await pool.query(
|
||||
"SELECT * FROM pupil_levels WHERE pupil_id = $1 AND subject IN ('taal','rekenen')",
|
||||
"SELECT * FROM pupil_levels WHERE pupil_id = $1 AND subject IN ('taal','rekenen','creatief')",
|
||||
[child.id])).rows;
|
||||
return {
|
||||
schoolManaged: false,
|
||||
|
|
@ -1541,7 +1541,7 @@ export default async function api(app) {
|
|||
if (child.school_id != null) return fail(reply, 403, 'het leerniveau wordt door de school beheerd');
|
||||
const body = parseLevelBody(req, reply);
|
||||
if (body.error) return body;
|
||||
if (!['taal', 'rekenen'].includes(body.subject)) return fail(reply, 400, 'ongeldig vakgebied');
|
||||
if (!['taal', 'rekenen', 'creatief'].includes(body.subject)) return fail(reply, 400, 'ongeldig vakgebied');
|
||||
await pool.query(
|
||||
`INSERT INTO pupil_levels (pupil_id, subject, year_group, level) VALUES ($1,$2,$3,$4)
|
||||
ON CONFLICT (pupil_id, subject) WHERE pupil_id IS NOT NULL
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
const LIVE_WINDOW_MS = 45_000;
|
||||
const HELP_IDLE_MS = 3 * 60_000;
|
||||
const ACTION_KINDS = new Set(['hint', 'encourage', 'pause', 'resume', 'easier']);
|
||||
const LEVEL_SUBJECTS = new Set(['taal', 'rekenen', 'world']);
|
||||
const LEVEL_SUBJECTS = new Set(['taal', 'rekenen', 'world', 'creatief']);
|
||||
|
||||
export default function registerLiveClassroom(app, options) {
|
||||
const {
|
||||
|
|
|
|||
96
test/coloring.test.js
Normal file
96
test/coloring.test.js
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {readFile} from 'node:fs/promises';
|
||||
import {runInNewContext} from 'node:vm';
|
||||
|
||||
async function coloringContext(){
|
||||
const source=await readFile('public/js/widgets/coloring.js','utf8');
|
||||
const context={};
|
||||
runInNewContext(source+`;
|
||||
globalThis.__coloring={
|
||||
pages:COLORING_PAGES,
|
||||
themes:COLORING_THEME_KEYS,
|
||||
pagesFor:coloringPagesFor,
|
||||
pencilWidth:coloringPencilWidth,
|
||||
safeColor:coloringSafeColor
|
||||
}`,context);
|
||||
return{source,api:context.__coloring};
|
||||
}
|
||||
|
||||
test('kleurplatenwidget laadt vóór het bord en is als creatieve leerlingwidget geregistreerd',async()=>{
|
||||
const [html,board,pupil,cast,core,css]=await Promise.all([
|
||||
readFile('public/index.html','utf8'),readFile('public/js/board.js','utf8'),
|
||||
readFile('public/js/pupil.js','utf8'),readFile('public/js/cast.js','utf8'),
|
||||
readFile('public/js/core.js','utf8'),readFile('public/css/coloring.css','utf8')
|
||||
]);
|
||||
assert.ok(html.indexOf('js/widgets/coloring.js')>0);
|
||||
assert.ok(html.indexOf('js/widgets/coloring.js')<html.indexOf('js/board.js'));
|
||||
assert.match(html,/css\/coloring\.css\?v=__V__/);
|
||||
assert.match(board,/id:"coloring"[^\n]*cat:"creatief"[^\n]*mount:mountColoring/);
|
||||
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']){
|
||||
assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key);
|
||||
}
|
||||
});
|
||||
|
||||
test('elk leerjaar heeft meerdere passende kleurplaten met thema en drie detailniveaus',async()=>{
|
||||
const {api}=await coloringContext();
|
||||
const pages=JSON.parse(JSON.stringify(api.pages));
|
||||
assert.equal(pages.length,13);
|
||||
assert.equal(new Set(pages.map(page=>page.id)).size,pages.length);
|
||||
for(let year=1;year<=8;year++){
|
||||
const suitable=JSON.parse(JSON.stringify(api.pagesFor(year)));
|
||||
assert.ok(suitable.length>=3,`groep ${year} heeft minimaal drie kleurplaten`);
|
||||
assert.ok(suitable.every(page=>year>=page.years[0]&&year<=page.years[1]));
|
||||
assert.ok(new Set(suitable.map(page=>page.theme)).size>=3,`groep ${year} heeft diverse thema's`);
|
||||
}
|
||||
for(const page of pages){
|
||||
assert.ok(api.themes[page.theme],page.id+' heeft een bekend thema');
|
||||
assert.ok(page.title.nl&&page.title.en,page.id+' heeft een tweetalige titel');
|
||||
assert.match(page.svg,/data-fillable="true"/);
|
||||
assert.match(page.svg,/data-level="2"/);
|
||||
assert.match(page.svg,/data-level="3"/);
|
||||
assert.doesNotMatch(page.svg,/<script|on\w+=/i);
|
||||
}
|
||||
assert.equal(api.pagesFor(5,'science').length,1);
|
||||
assert.equal(api.pagesFor(3,'science').length,0);
|
||||
});
|
||||
|
||||
test('potlood reageert op echte pendruk en bouwt kleur transparant op',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.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);
|
||||
const css=await readFile('public/css/coloring.css','utf8');
|
||||
assert.match(css,/mix-blend-mode:multiply/);
|
||||
});
|
||||
|
||||
test('verfemmer vult een SVG-vak in één keer en state bewaart potlood én vlakkleuren',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,/canvas\.toDataURL\("image\/png"\)/);
|
||||
assert.match(source,/return\{theme,pageId,fills:\{\.\.\.fills\},ink,tool,color,size,completed,yearGroup,level/);
|
||||
assert.match(source,/opts\.onProgress/);
|
||||
});
|
||||
|
||||
test('creatief niveau is beschikbaar per klas, leerling en thuiskind',async()=>{
|
||||
const [api,admin,progressions,parent,wiki,migration,live]=await Promise.all([
|
||||
readFile('src/api.js','utf8'),readFile('public/js/admin.js','utf8'),
|
||||
readFile('public/js/learning-progressions.js','utf8'),readFile('public/js/parent.js','utf8'),
|
||||
readFile('public/js/wiki.js','utf8'),
|
||||
readFile('db/024_creative_pupil_levels.sql','utf8'),readFile('src/live-classroom.js','utf8')
|
||||
]);
|
||||
assert.match(api,/LEVEL_SUBJECTS = \[[^\]]*'creatief'/);
|
||||
assert.match(admin,/KM_SUBJECTS = \[[^\]]*"creatief"/);
|
||||
assert.match(progressions,/LEARNING_SUBJECTS=\[[^\]]*"creatief"/);
|
||||
assert.match(parent,/\["taal","rekenen","creatief"\]\.forEach/);
|
||||
assert.match(wiki,/creatief:"parentLevelCreative"/);
|
||||
assert.match(migration,/CHECK \(subject IN \('taal','rekenen','world','creatief'\)\)/);
|
||||
assert.match(live,/LEVEL_SUBJECTS = new Set\(\[[^\]]*'creatief'/);
|
||||
});
|
||||
|
|
@ -17,7 +17,7 @@ async function progressionContext(){
|
|||
return context;
|
||||
}
|
||||
|
||||
test('taal en rekenen hebben een compleet inhoudelijk profiel voor groep 1 tot en met 8',async()=>{
|
||||
test('taal, rekenen en creatief hebben een compleet inhoudelijk profiel voor groep 1 tot en met 8',async()=>{
|
||||
const {__profiles:profiles}=await progressionContext();
|
||||
for(const subject of ['taal','rekenen']){
|
||||
assert.equal(profiles[subject].length,9);
|
||||
|
|
|
|||
|
|
@ -89,15 +89,15 @@ test('ouder leest en wijzigt het leerniveau van een gekoppeld thuiskind', async
|
|||
const respond = (sql) => {
|
||||
if (sql.includes('FROM parent_children WHERE parent_id')) return { rows: [{ 1: 1 }] };
|
||||
if (sql.startsWith('SELECT * FROM users WHERE id')) return { rows: [homeChild] };
|
||||
if (sql.includes("subject IN ('taal','rekenen')")) return { rows: [
|
||||
{ subject:'taal',year_group:3,level:2 },{ subject:'rekenen',year_group:4,level:1 },
|
||||
if (sql.includes("subject IN ('taal','rekenen','creatief')")) return { rows: [
|
||||
{ subject:'taal',year_group:3,level:2 },{ subject:'rekenen',year_group:4,level:1 },{ subject:'creatief',year_group:5,level:3 },
|
||||
] };
|
||||
return undefined;
|
||||
};
|
||||
const {app,calls}=await makeApp({user:parent,respond});
|
||||
const get=await app.inject({method:'GET',url:'/api/parent/children/30/levels',cookies});
|
||||
assert.equal(get.statusCode,200,get.body);
|
||||
assert.deepEqual(get.json().levels,{taal:{yearGroup:3,level:2},rekenen:{yearGroup:4,level:1}});
|
||||
assert.deepEqual(get.json().levels,{taal:{yearGroup:3,level:2},rekenen:{yearGroup:4,level:1},creatief:{yearGroup:5,level:3}});
|
||||
const put=await app.inject({method:'PUT',url:'/api/parent/children/30/level',cookies,
|
||||
payload:{subject:'rekenen',yearGroup:5,level:3}});
|
||||
assert.equal(put.statusCode,200,put.body);
|
||||
|
|
|
|||
|
|
@ -49,12 +49,12 @@ test('klassenmanagement: klas-standaard opslaan doet de juiste upsert-query', as
|
|||
const { app, calls } = await makeApp(admin);
|
||||
const res = await app.inject({
|
||||
method: 'PUT', url: '/api/levels/class/100', cookies,
|
||||
payload: { subject: 'rekenen', yearGroup: 4, level: 2 },
|
||||
payload: { subject: 'creatief', yearGroup: 4, level: 2 },
|
||||
});
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
const ins = calls.find(c => c.sql.startsWith('INSERT INTO pupil_levels'));
|
||||
assert.ok(ins);
|
||||
assert.deepEqual(ins.params, [100, 'rekenen', 4, 2]);
|
||||
assert.deepEqual(ins.params, [100, 'creatief', 4, 2]);
|
||||
});
|
||||
|
||||
test('klassenmanagement: ongeldig vakgebied geeft 400', async () => {
|
||||
|
|
@ -123,17 +123,17 @@ test('klassenmanagement: leerkracht-rol wordt geweigerd op /my/level (alleen lee
|
|||
assert.equal(res.statusCode, 403, res.body);
|
||||
});
|
||||
|
||||
test('thuiskind kan eigen taalniveau opslaan, schoolleerling niet', async () => {
|
||||
test('thuiskind kan eigen creatief niveau opslaan, schoolleerling niet', async () => {
|
||||
const { app, calls } = await makeApp(homePupil);
|
||||
const res = await app.inject({ method: 'PUT', url: '/api/my/level', cookies,
|
||||
payload: { subject: 'taal', yearGroup: 5, level: 2 } });
|
||||
payload: { subject: 'creatief', yearGroup: 5, level: 2 } });
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
const insert = calls.find(call=>call.sql.startsWith('INSERT INTO pupil_levels'));
|
||||
assert.deepEqual(insert.params,[10,'taal',5,2]);
|
||||
assert.deepEqual(insert.params,[10,'creatief',5,2]);
|
||||
|
||||
const { app: schoolApp } = await makeApp(pupil);
|
||||
const denied = await schoolApp.inject({ method: 'PUT', url: '/api/my/level', cookies,
|
||||
payload: { subject: 'taal', yearGroup: 5, level: 2 } });
|
||||
payload: { subject: 'creatief', yearGroup: 5, level: 2 } });
|
||||
assert.equal(denied.statusCode,403,denied.body);
|
||||
await app.close();await schoolApp.close();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -324,11 +324,13 @@ test('letter- en cijferdoelvakken blijven gecentreerd en lange woorden blijven b
|
|||
});
|
||||
|
||||
test('alle geregistreerde widgets hebben mobiele dekking en passen op een 320px-bord',async()=>{
|
||||
const [board,pupil,css]=await Promise.all([
|
||||
const [board,pupil,css,coloringCss]=await Promise.all([
|
||||
readFile('public/js/board.js','utf8'),
|
||||
readFile('public/js/pupil.js','utf8'),
|
||||
readFile('public/css/teach.css','utf8'),
|
||||
readFile('public/css/coloring.css','utf8'),
|
||||
]);
|
||||
const styles=css+coloringCss;
|
||||
const registrySource=board.slice(board.indexOf('const REGISTRY'),board.indexOf('const WIDGET_CATS'));
|
||||
const registered=[...registrySource.matchAll(/id:"([^"]+)"/g)].map(match=>match[1]).sort();
|
||||
const coverage={
|
||||
|
|
@ -353,6 +355,7 @@ test('alle geregistreerde widgets hebben mobiele dekking en passen op een 320px-
|
|||
placevalue:'.place-grid',
|
||||
money:'.money-task-card',
|
||||
wordtypes:'.wordtypes',
|
||||
coloring:'.coloring-paper',
|
||||
write:'.wp-bar',
|
||||
mind:'.mm-top',
|
||||
notes:'.notes{',
|
||||
|
|
@ -373,7 +376,7 @@ test('alle geregistreerde widgets hebben mobiele dekking en passen op een 320px-
|
|||
};
|
||||
assert.deepEqual(registered,Object.keys(coverage).sort(),'ieder geregistreerd widgettype staat in de mobiele audit');
|
||||
for(const [id,selector] of Object.entries(coverage)){
|
||||
assert.ok(css.includes(selector),id+' mist responsive/breedte-onafhankelijke opmaak');
|
||||
assert.ok(styles.includes(selector),id+' mist responsive/breedte-onafhankelijke opmaak');
|
||||
}
|
||||
|
||||
assert.match(css,/\.widget-body\{[^}]*container-type:inline-size[^}]*container-name:widget/);
|
||||
|
|
|
|||
Loading…
Reference in a new issue