Verbeter grafische kwaliteit van kleurplaten (v0.4.67-beta) #5

Open
bes-r wants to merge 208 commits from bes-r/coloring-quality-v0.4.67 into main AGit
4 changed files with 144 additions and 28 deletions
Showing only changes of commit 55520ba367 - Show all commits

View file

@ -1 +1 @@
0.3.71-beta 0.3.72-beta

View file

@ -342,7 +342,10 @@
.mm-pop .mp-ls[data-ls=dot] i{border-top-style:dotted;} .mm-pop .mp-ls[data-ls=dot] i{border-top-style:dotted;}
.mm-pop .mp-x{margin-left:auto; font-weight:900; color:var(--red); font-size:12px;} .mm-pop .mp-x{margin-left:auto; font-weight:900; color:var(--red); font-size:12px;}
/* physical tools: ruler, set square, magnifier */ /* physical tools: ruler, set square, magnifier */
.dtool{position:absolute; z-index:3; touch-action:none; user-select:none; cursor:grab; left:120px; top:120px;} /* boven de inktlaag (4000): net als een echte lat ligt het gereedschap óp
het bord - ook in penmodus te verplaatsen en in te stellen; tekenen
begint ernaast en kleeft aan de tekenrand */
.dtool{position:absolute; z-index:4200; touch-action:none; user-select:none; cursor:grab; left:120px; top:120px;}
.dtool:active{cursor:grabbing;} .dtool:active{cursor:grabbing;}
.dtool .dtool-x{ .dtool .dtool-x{
position:absolute; top:-10px; right:-10px; width:24px; height:24px; border-radius:50%; position:absolute; top:-10px; right:-10px; width:24px; height:24px; border-radius:50%;
@ -356,6 +359,11 @@
} }
.dtool-ruler{width:600px; height:76px;} .dtool-ruler{width:600px; height:76px;}
.dtool-geo{width:340px; height:184px;} .dtool-geo{width:340px; height:184px;}
/* alleen de driehoek zelf is aan te raken - de lege hoeken van de
rechthoekige box laten aanrakingen door naar het bord, anders kon er
niet vlak naast de schuine randen getekend worden */
.dtool-geo{pointer-events:none;}
.dtool-geo svg polygon,.dtool-geo .dtool-opts,.dtool-geo .dtool-x,.dtool-geo .dtool-rot{pointer-events:auto;}
.dtool-opts{position:absolute; top:-12px; left:8px; display:flex; gap:5px;} .dtool-opts{position:absolute; top:-12px; left:8px; display:flex; gap:5px;}
.dtool-opts button{ .dtool-opts button{
width:24px; height:24px; border-radius:50%; border:none; background:var(--surface); width:24px; height:24px; border-radius:50%; border:none; background:var(--surface);

View file

@ -578,6 +578,7 @@ function makeMovable(t){
const near = Math.round(a/45)*45; const near = Math.round(a/45)*45;
if(Math.abs(a-near) < 4) a = near; if(Math.abs(a-near) < 4) a = near;
ang = a; ang = a;
t._ang = ang; /* voor het kleven van de pen aan de gedraaide rand */
t.style.transform = `rotate(${ang}deg)`; t.style.transform = `rotate(${ang}deg)`;
const shown = Math.round(((ang % 360) + 360) % 360); const shown = Math.round(((ang % 360) + 360) % 360);
badge.textContent = shown + "°"; badge.textContent = shown + "°";
@ -636,6 +637,8 @@ function toggleDTool(kind){
<line x1="0" y1="2" x2="${L}" y2="2" stroke="${ink}" stroke-width="${hc?3:2}"/> <line x1="0" y1="2" x2="${L}" y2="2" stroke="${ink}" stroke-width="${hc?3:2}"/>
<text x="${L/2}" y="68" font-size="12" fill="${ink}" text-anchor="middle" opacity=".65">${cmLen} cm</text> <text x="${L/2}" y="68" font-size="12" fill="${ink}" text-anchor="middle" opacity=".65">${cmLen} cm</text>
${ticks}</svg>`; ${ticks}</svg>`;
/* tekenranden voor het kleven van de pen (bord-lokale coördinaten) */
t._edges = [[[0,0],[L,0]], [[0,76],[L,76]]];
const lenBtn = t.querySelector(".do-len"); const lenBtn = t.querySelector(".do-len");
lenBtn.textContent = cmLen + " cm"; lenBtn.textContent = cmLen + " cm";
lenBtn.title = T("dtLen"); lenBtn.title = T("dtLen");
@ -654,37 +657,76 @@ function toggleDTool(kind){
t.style.height = "76px"; t.style.height = "76px";
drawRuler(); drawRuler();
}else if(kind==="geo"){ }else if(kind==="geo"){
t._sz = 340; t._hc = false; /* geodriehoek zoals de echte: cm-schaal op de hypotenusa met 0 in het
midden, gradenbogen in twee richtingen, en twee niveaus:
basis - grote cijfers, alleen de hoofdhoeken (30/45/60/90/) en
hele centimeters, voor jonge leerlingen;
volledig - mm-streepjes, graden per 2° met dubbele schaal (links- én
rechtsom, zoals op een echte geodriehoek) en evenwijdige
hulplijnen per cm om parallelle lijnen te tekenen. */
const GEO_CMS = [6, 8, 12];
t._cm = 8; t._hc = false; t._full = false;
const drawGeo = ()=>{ const drawGeo = ()=>{
const W = t._sz, H = W/2 + 22, hc = t._hc; const maxCm = Math.max(6, Math.floor((board.clientWidth-40)/50));
const cmLen = Math.min(t._cm, maxCm);
const W = cmLen*50 + 28, H = W/2 + 22, hc = t._hc, full = t._full;
const ink = hc ? "#000" : "#1e4f74"; const ink = hc ? "#000" : "#1e4f74";
const bg = hc ? "rgba(255,214,0,.9)" : "rgba(185,222,248,.6)"; const bg = hc ? "rgba(255,214,0,.9)" : "rgba(185,222,248,.55)";
const halo = `stroke="${hc?"#ffd400":"#ffffff"}" stroke-width="3.5" paint-order="stroke" stroke-linejoin="round"`; const halo = `stroke="${hc?"#ffd400":"#ffffff"}" stroke-width="3.5" paint-order="stroke" stroke-linejoin="round"`;
const ax = W/2, ay = 16, by = H-18; /* apex + baseline */ const ax = W/2, ay = 16, by = H-18; /* apex + tekenrand */
const half = W/2 - 14; const half = W/2 - 14;
let cmTicks = ""; /* cm-schaal op de tekenrand, 0 in het midden; volledig ook mm */
for(let i=25; i<=half-16; i+=25){ let cmTicks = `<text x="${ax}" y="${by-18}" font-size="${hc?15:12}" font-weight="bold" fill="${ink}" ${halo} text-anchor="middle">0</text>`;
const cm = i%50===0; const stepPx = full ? 5 : 25;
for(let i=stepPx; i<=half; i+=stepPx){
const cm = i%50===0, halfCm = i%25===0;
const len = cm ? 13 : halfCm ? 9 : 5;
[ax-i, ax+i].forEach(x=>{ [ax-i, ax+i].forEach(x=>{
cmTicks += `<line x1="${x}" y1="${by}" x2="${x}" y2="${by-(cm?13:7)}" stroke="${ink}" stroke-width="${cm?(hc?2.2:1.4):1}"/>`; cmTicks += `<line x1="${x}" y1="${by}" x2="${x}" y2="${by-len}" stroke="${ink}" stroke-width="${cm?(hc?2.2:1.4):1}"/>`;
}); });
if(cm){ if(cm && i<=half-14){
[ax-i, ax+i].forEach(x=>{ [ax-i, ax+i].forEach(x=>{
cmTicks += `<text x="${x}" y="${by-18}" font-size="${hc?15:12}" font-weight="bold" fill="${ink}" ${halo} text-anchor="middle">${i/50}</text>`; cmTicks += `<text x="${x}" y="${by-18}" font-size="${hc?15:12}" font-weight="bold" fill="${ink}" ${halo} text-anchor="middle">${i/50}</text>`;
}); });
} }
} }
/* degree arc with labels INSIDE the arc so they stay readable */ /* gradenboog: basis = hoofdhoeken groot; volledig = per 2° met dubbele
schaal (buiten linksom, binnen rechtsom) zoals op een echte geodriehoek */
let degs = ""; let degs = "";
const R = half*0.60; const R = half*0.62;
for(let d=10; d<180; d+=10){ const degStep = full ? 2 : 10;
for(let d=degStep; d<180; d+=degStep){
const major = full ? d%10===0 : d%30===0 || d===45 || d===135;
const rad = d*Math.PI/180; const rad = d*Math.PI/180;
const x1 = ax - Math.cos(rad)*(R-7), y1 = by - Math.sin(rad)*(R-7); const x1 = ax - Math.cos(rad)*(R-(major?10:5)), y1 = by - Math.sin(rad)*(R-(major?10:5));
const x2 = ax - Math.cos(rad)*R, y2 = by - Math.sin(rad)*R; const x2 = ax - Math.cos(rad)*R, y2 = by - Math.sin(rad)*R;
degs += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${ink}" stroke-width="${d%30===0?(hc?2.2:1.5):1}"/>`; degs += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${ink}" stroke-width="${major?(hc?2:1.4):0.7}"/>`;
if(d%30===0 && d>=30 && d<=150){ }
const xt = ax - Math.cos(rad)*(R-22), yt = by - Math.sin(rad)*(R-22); /* 10° en 170° liggen vrijwel op de baseline en zouden de cm-cijfers
degs += `<text x="${xt}" y="${yt+4}" font-size="${hc?14:11.5}" font-weight="bold" fill="${ink}" ${halo} text-anchor="middle">${d}°</text>`; overlappen - net als op een echte geodriehoek beginnen de labels
daar pas een stap hoger (de streepjes staan er wel) */
const labels = full
? Array.from({length:15}, (_,i)=>(i+2)*10)
: [30,45,60,90,120,135,150];
labels.forEach(d=>{
const rad = d*Math.PI/180;
if(full){
const xo = ax - Math.cos(rad)*(R-20), yo = by - Math.sin(rad)*(R-20);
const xi = ax - Math.cos(rad)*(R-36), yi = by - Math.sin(rad)*(R-36);
degs += `<text x="${xo}" y="${yo+3.5}" font-size="10.5" font-weight="bold" fill="${ink}" ${halo} text-anchor="middle">${d}</text>`;
degs += `<text x="${xi}" y="${yi+3.5}" font-size="9" fill="${ink}" ${halo} text-anchor="middle" opacity=".75">${180-d}</text>`;
}else{
const xt = ax - Math.cos(rad)*(R-24), yt = by - Math.sin(rad)*(R-24);
degs += `<text x="${xt}" y="${yt+5}" font-size="${hc?16:14}" font-weight="bold" fill="${ink}" ${halo} text-anchor="middle">${d}°</text>`;
}
});
/* volledig: evenwijdige hulplijnen per cm, parallel aan de tekenrand */
let parallels = "";
if(full){
for(let yy=by-50; yy>ay+24; yy-=50){
const hw = half * (yy-ay)/(by-ay) - 10;
if(hw < 30) break;
parallels += `<line x1="${ax-hw}" y1="${yy}" x2="${ax+hw}" y2="${yy}" stroke="${ink}" stroke-width="0.8" opacity=".35"/>`;
} }
} }
t.style.width = W+"px"; t.style.height = H+"px"; t.style.width = W+"px"; t.style.height = H+"px";
@ -692,19 +734,27 @@ function toggleDTool(kind){
<polygon points="14,${by} ${W-14},${by} ${ax},${ay}" fill="${bg}" stroke="${ink}" stroke-width="${hc?3:2}"/> <polygon points="14,${by} ${W-14},${by} ${ax},${ay}" fill="${bg}" stroke="${ink}" stroke-width="${hc?3:2}"/>
<path d="M ${ax-R} ${by} A ${R} ${R} 0 0 1 ${ax+R} ${by}" fill="none" stroke="${ink}" stroke-width="1.2"/> <path d="M ${ax-R} ${by} A ${R} ${R} 0 0 1 ${ax+R} ${by}" fill="none" stroke="${ink}" stroke-width="1.2"/>
<line x1="${ax}" y1="${by}" x2="${ax}" y2="${ay+18}" stroke="${ink}" stroke-width="1" stroke-dasharray="4 4"/> <line x1="${ax}" y1="${by}" x2="${ax}" y2="${ay+18}" stroke="${ink}" stroke-width="1" stroke-dasharray="4 4"/>
<text x="${ax}" y="${by-6}" font-size="${hc?13:10}" font-weight="bold" fill="${ink}" ${halo} text-anchor="middle">0</text> <text x="${ax}" y="${by+14}" font-size="${hc?12:10}" font-weight="bold" fill="${ink}" text-anchor="middle" opacity=".7">90°</text>
${cmTicks}${degs}</svg>`; ${parallels}${cmTicks}${degs}</svg>`;
/* tekenranden voor het kleven van de pen (bord-lokale coördinaten) */
t._edges = [[[14,by],[W-14,by]], [[14,by],[ax,ay]], [[W-14,by],[ax,ay]]];
const lenBtn = t.querySelector(".do-len");
lenBtn.textContent = cmLen + " cm";
lenBtn.title = T("dtLen");
const modeBtn = t.querySelector(".do-mode");
modeBtn.textContent = T(full ? "dtModeFull" : "dtModeBasic");
modeBtn.title = T("dtMode");
}; };
t.innerHTML = `<div class="dtool-svg"></div> t.innerHTML = `<div class="dtool-svg"></div>
<div class="dtool-opts"> <div class="dtool-opts">
<button class="do-len"></button> <button class="do-len do-wide"></button>
<button class="do-mode do-wide"></button>
<button class="do-hc"></button> <button class="do-hc"></button>
</div>`; </div>`;
const geoLen = t.querySelector(".do-len"); t.querySelector(".do-len").addEventListener("click", ()=>{
geoLen.title = T("dtLen"); t._cm = GEO_CMS[(GEO_CMS.indexOf(t._cm)+1) % GEO_CMS.length]; drawGeo();
geoLen.addEventListener("click", ()=>{
t._sz = t._sz===340 ? 440 : t._sz===440 ? 280 : 340; drawGeo();
}); });
t.querySelector(".do-mode").addEventListener("click", ()=>{ t._full = !t._full; drawGeo(); });
const geoHc = t.querySelector(".do-hc"); const geoHc = t.querySelector(".do-hc");
geoHc.title = T("dtContrast"); geoHc.title = T("dtContrast");
geoHc.addEventListener("click", ()=>{ t._hc = !t._hc; drawGeo(); }); geoHc.addEventListener("click", ()=>{ t._hc = !t._hc; drawGeo(); });
@ -841,6 +891,50 @@ const bPos = ev=>{
/* ratio-based: correct under any display zoom */ /* ratio-based: correct under any display zoom */
return [(ev.clientX-r.left)*(bInk.width/r.width), (ev.clientY-r.top)*(bInk.height/r.height)]; return [(ev.clientX-r.left)*(bInk.width/r.width), (ev.clientY-r.top)*(bInk.height/r.height)];
}; };
/* ---- langs de liniaal/geodriehoek tekenen -----------------------------------
Elke rand van de liniaal en geodriehoek is een "tekenrand". Begint een haal
binnen 16px van zo'n rand, dan wordt die rand voor de hele haal vergrendeld
en wordt elk punt erop geprojecteerd: een kaarsrechte lijn, precies zoals
tekenen tegen een echte lat. Verder dan ~60px van de rand laat de pen weer
los (bewust weggetrokken). Randen draaien mee met het gereedschap. */
let bSnap = null;
function toolEdges(){
const edges = [];
["ruler","geo"].forEach(kind=>{
const t = DTOOLS[kind];
if(!t || !t._edges) return;
const rad = (t._ang||0)*Math.PI/180;
const cosA = Math.cos(rad), sinA = Math.sin(rad);
const cx = t.offsetLeft + t.offsetWidth/2, cy = t.offsetTop + t.offsetHeight/2;
const rot = ([x,y])=>{
const dx = x - t.offsetWidth/2, dy = y - t.offsetHeight/2;
return [cx + dx*cosA - dy*sinA, cy + dx*sinA + dy*cosA];
};
t._edges.forEach(seg=>edges.push([rot(seg[0]), rot(seg[1])]));
});
return edges;
}
function nearestEdge(px, py){
let best = null;
toolEdges().forEach(([a,b])=>{
const ux = b[0]-a[0], uy = b[1]-a[1], len = Math.hypot(ux,uy);
if(len < 1) return;
const along = Math.max(0, Math.min(len, ((px-a[0])*ux + (py-a[1])*uy)/len));
const qx = a[0] + ux/len*along, qy = a[1] + uy/len*along;
const d = Math.hypot(px-qx, py-qy);
if(!best || d < best.d) best = { d, ax:a[0], ay:a[1], ux:ux/len, uy:uy/len, len };
});
return best;
}
function snapPoint(x, y){
if(!bSnap) return [x, y];
const k = bInk.width / board.clientWidth; /* canvas-px per bord-px */
const px = x/k, py = y/k;
const along = Math.max(0, Math.min(bSnap.len, ((px-bSnap.ax)*bSnap.ux + (py-bSnap.ay)*bSnap.uy)));
const qx = bSnap.ax + bSnap.ux*along, qy = bSnap.ay + bSnap.uy*along;
if(Math.hypot(px-qx, py-qy) > 60){ bSnap = null; return [x, y]; }
return [qx*k, qy*k];
}
bInk.addEventListener("pointerdown", e=>{ bInk.addEventListener("pointerdown", e=>{
if(e.pointerType==="mouse" && e.button!==0) return; if(e.pointerType==="mouse" && e.button!==0) return;
e.preventDefault(); e.preventDefault();
@ -849,13 +943,22 @@ bInk.addEventListener("pointerdown", e=>{
bDraw = true; bDraw = true;
PEN.nib = PEN.size*0.8; PEN.nib = PEN.size*0.8;
[bx,by] = bPos(e); [bx,by] = bPos(e);
bSnap = null;
if(PEN.tool!=="erase" && PEN.tool!=="hand"){
const k = bInk.width / board.clientWidth;
const hit = nearestEdge(bx/k, by/k);
if(hit && hit.d <= 16) bSnap = hit;
}
[bx,by] = snapPoint(bx, by);
bMidX = bMidY = null; bMidX = bMidY = null;
bStroke(bx+0.01, by+0.01, e.pressure, e.pointerType); bStroke(bx+0.01, by+0.01, e.pressure, e.pointerType);
}); });
bInk.addEventListener("pointermove", e=>{ bInk.addEventListener("pointermove", e=>{
if(!bDraw) return; if(!bDraw) return;
(e.getCoalescedEvents ? e.getCoalescedEvents() : [e]).forEach(ev=>{ (e.getCoalescedEvents ? e.getCoalescedEvents() : [e]).forEach(ev=>{
const [x,y] = bPos(ev); bStroke(x, y, ev.pressure, ev.pointerType); const [rx,ry] = bPos(ev);
const [x,y] = snapPoint(rx, ry);
bStroke(x, y, ev.pressure, ev.pointerType);
}); });
}); });
const bStop = ()=>{ const bStop = ()=>{
@ -874,6 +977,7 @@ const bStop = ()=>{
markBoardChange(); markBoardChange();
} }
bDraw = false; bDraw = false;
bSnap = null;
bCtx.globalAlpha = 1; bCtx.globalAlpha = 1;
if(DTOOLS.lens) DTOOLS.lens.lensDraw(); if(DTOOLS.lens) DTOOLS.lens.lensDraw();
}; };

View file

@ -2,7 +2,7 @@
"use strict"; "use strict";
/* version shown until /api/version resolves (or if the fetch fails, e.g. offline). /* 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. */ Kept in sync by hand with the VERSION file at the repo root on every release. */
const VERSION = "0.3.71-beta"; const VERSION = "0.3.72-beta";
(function(){ (function(){
const tag = document.getElementById("verTag"); const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION; tag.textContent = "v"+VERSION;
@ -244,6 +244,8 @@ const I18N = {
parentUnlinkWarn:"Verwijdert alleen de koppeling; het account van je kind blijft bestaan.", parentUnlinkWarn:"Verwijdert alleen de koppeling; het account van je kind blijft bestaan.",
parentProgress:"📈 Voortgang", parentProgressEmpty:"Nog geen voortgang.", parentProgress:"📈 Voortgang", parentProgressEmpty:"Nog geen voortgang.",
dtLen:"Lengte", dtContrast:"Hoog contrast", dtLen:"Lengte", dtContrast:"Hoog contrast",
dtMode:"Basis: grote hoofdhoeken en hele centimeters. Volledig: graden per 2° met dubbele schaal, millimeters en evenwijdige hulplijnen.",
dtModeBasic:"basis", dtModeFull:"volledig",
pupilEmpty:"Je leerkracht heeft nog niets voor je klaargezet.", pupilEmpty:"Je leerkracht heeft nog niets voor je klaargezet.",
pupilWatching:"meekijken", pupilWatching:"meekijken",
amUsers:"Gebruikers", amClasses:"Klassen", amSchools:"Scholen", amOverarching:"Systeem", amUsers:"Gebruikers", amClasses:"Klassen", amSchools:"Scholen", amOverarching:"Systeem",
@ -515,6 +517,8 @@ const I18N = {
parentUnlinkWarn:"Removes only the link; your child's account remains.", parentUnlinkWarn:"Removes only the link; your child's account remains.",
parentProgress:"📈 Progress", parentProgressEmpty:"No progress yet.", parentProgress:"📈 Progress", parentProgressEmpty:"No progress yet.",
dtLen:"Length", dtContrast:"High contrast", dtLen:"Length", dtContrast:"High contrast",
dtMode:"Basic: big main angles and whole centimetres. Full: degrees every 2° with a double scale, millimetres and parallel guide lines.",
dtModeBasic:"basic", dtModeFull:"full",
pupilEmpty:"Your teacher hasn't set up anything for you yet.", pupilEmpty:"Your teacher hasn't set up anything for you yet.",
pupilWatching:"watching", pupilWatching:"watching",
amUsers:"Users", amClasses:"Classes", amSchools:"Schools", amOverarching:"System", amUsers:"Users", amClasses:"Classes", amSchools:"Schools", amOverarching:"System",