diff --git a/VERSION b/VERSION index 2c8f5ac..f5d2f80 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.71-beta +0.3.72-beta diff --git a/public/css/teach.css b/public/css/teach.css index efe0c61..049e4c5 100644 --- a/public/css/teach.css +++ b/public/css/teach.css @@ -342,7 +342,10 @@ .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;} /* 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 .dtool-x{ position:absolute; top:-10px; right:-10px; width:24px; height:24px; border-radius:50%; @@ -356,6 +359,11 @@ } .dtool-ruler{width:600px; height:76px;} .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 button{ width:24px; height:24px; border-radius:50%; border:none; background:var(--surface); diff --git a/public/js/board.js b/public/js/board.js index 0b057d0..d1be138 100644 --- a/public/js/board.js +++ b/public/js/board.js @@ -578,6 +578,7 @@ function makeMovable(t){ const near = Math.round(a/45)*45; if(Math.abs(a-near) < 4) a = near; ang = a; + t._ang = ang; /* voor het kleven van de pen aan de gedraaide rand */ t.style.transform = `rotate(${ang}deg)`; const shown = Math.round(((ang % 360) + 360) % 360); badge.textContent = shown + "°"; @@ -636,6 +637,8 @@ function toggleDTool(kind){ ${cmLen} cm ${ticks}`; + /* 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"); lenBtn.textContent = cmLen + " cm"; lenBtn.title = T("dtLen"); @@ -654,37 +657,76 @@ function toggleDTool(kind){ t.style.height = "76px"; drawRuler(); }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 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 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 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; - let cmTicks = ""; - for(let i=25; i<=half-16; i+=25){ - const cm = i%50===0; + /* cm-schaal op de tekenrand, 0 in het midden; volledig ook mm */ + let cmTicks = `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=>{ - cmTicks += ``; + cmTicks += ``; }); - if(cm){ + if(cm && i<=half-14){ [ax-i, ax+i].forEach(x=>{ cmTicks += `${i/50}`; }); } } - /* 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 = ""; - const R = half*0.60; - for(let d=10; d<180; d+=10){ + const R = half*0.62; + 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 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; - degs += ``; - if(d%30===0 && d>=30 && d<=150){ - const xt = ax - Math.cos(rad)*(R-22), yt = by - Math.sin(rad)*(R-22); - degs += `${d}°`; + degs += ``; + } + /* 10° en 170° liggen vrijwel op de baseline en zouden de cm-cijfers + 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 += `${d}`; + degs += `${180-d}`; + }else{ + const xt = ax - Math.cos(rad)*(R-24), yt = by - Math.sin(rad)*(R-24); + degs += `${d}°`; + } + }); + /* 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 += ``; } } t.style.width = W+"px"; t.style.height = H+"px"; @@ -692,19 +734,27 @@ function toggleDTool(kind){ - 0 - ${cmTicks}${degs}`; + 90° + ${parallels}${cmTicks}${degs}`; + /* 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 = `
- + +
`; - const geoLen = t.querySelector(".do-len"); - geoLen.title = T("dtLen"); - geoLen.addEventListener("click", ()=>{ - t._sz = t._sz===340 ? 440 : t._sz===440 ? 280 : 340; drawGeo(); + t.querySelector(".do-len").addEventListener("click", ()=>{ + t._cm = GEO_CMS[(GEO_CMS.indexOf(t._cm)+1) % GEO_CMS.length]; drawGeo(); }); + t.querySelector(".do-mode").addEventListener("click", ()=>{ t._full = !t._full; drawGeo(); }); const geoHc = t.querySelector(".do-hc"); geoHc.title = T("dtContrast"); geoHc.addEventListener("click", ()=>{ t._hc = !t._hc; drawGeo(); }); @@ -841,6 +891,50 @@ const bPos = ev=>{ /* ratio-based: correct under any display zoom */ 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=>{ if(e.pointerType==="mouse" && e.button!==0) return; e.preventDefault(); @@ -849,13 +943,22 @@ bInk.addEventListener("pointerdown", e=>{ bDraw = true; PEN.nib = PEN.size*0.8; [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; bStroke(bx+0.01, by+0.01, e.pressure, e.pointerType); }); bInk.addEventListener("pointermove", e=>{ if(!bDraw) return; (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 = ()=>{ @@ -874,6 +977,7 @@ const bStop = ()=>{ markBoardChange(); } bDraw = false; + bSnap = null; bCtx.globalAlpha = 1; if(DTOOLS.lens) DTOOLS.lens.lensDraw(); }; diff --git a/public/js/core.js b/public/js/core.js index deeb28d..632db56 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -2,7 +2,7 @@ "use strict"; /* version — shown until /api/version resolves (or if the fetch fails, e.g. offline). Kept in sync by hand with the VERSION file at the repo root on every release. */ -const VERSION = "0.3.71-beta"; +const VERSION = "0.3.72-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; @@ -244,6 +244,8 @@ const I18N = { parentUnlinkWarn:"Verwijdert alleen de koppeling; het account van je kind blijft bestaan.", parentProgress:"📈 Voortgang", parentProgressEmpty:"Nog geen voortgang.", 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.", pupilWatching:"meekijken", amUsers:"Gebruikers", amClasses:"Klassen", amSchools:"Scholen", amOverarching:"Systeem", @@ -515,6 +517,8 @@ const I18N = { parentUnlinkWarn:"Removes only the link; your child's account remains.", parentProgress:"📈 Progress", parentProgressEmpty:"No progress yet.", 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.", pupilWatching:"watching", amUsers:"Users", amClasses:"Classes", amSchools:"Schools", amOverarching:"System",