diff --git a/VERSION b/VERSION index c39603d..131ab5f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.64-beta +0.3.65-beta diff --git a/public/css/teach.css b/public/css/teach.css index 828a62c..354ce52 100644 --- a/public/css/teach.css +++ b/public/css/teach.css @@ -123,6 +123,9 @@ /* drawing layer on the board: sits ABOVE widgets like a transparency sheet, so you can annotate over everything; clicks pass through when pen is off */ #boardInk{position:absolute; inset:0; pointer-events:none; z-index:4000;} + /* live-laag van de markeerstift: dekkend getekend, doorschijnend getoond; + bij het loslaten wordt de haal in één keer op de inktlaag gezet */ + #boardMark{position:absolute; inset:0; pointer-events:none; z-index:4000; opacity:.32;} body.pen #boardInk{pointer-events:auto; cursor:crosshair;} body.pen.pen-text #boardInk{cursor:text;} body.pen.pen-hand #boardInk{pointer-events:none;} /* hand tool: interact with everything */ @@ -360,6 +363,16 @@ box-shadow:0 2px 6px rgba(0,0,0,.25); } .dtool-opts button:hover{background:var(--accent-soft);} + /* lengteknop toont de maat ("10 cm") en is dus breder dan een rond knopje */ + .dtool-opts .do-wide{width:auto; min-width:48px; padding:0 9px; border-radius:12px;} + /* live hoek-uitlezing tijdens het draaien van liniaal/geodriehoek */ + .dtool-deg{ + display:none; position:absolute; left:50%; top:50%; margin:-16px 0 0 -24px; + width:48px; height:32px; line-height:32px; text-align:center; pointer-events:none; + background:rgba(30,50,90,.85); color:#fff; font-size:14px; font-weight:800; + border-radius:9px; z-index:3; + } + .dtool-deg.snapped{background:var(--green);} .dtool-lens{z-index:4500;} /* the glass sits above the ink sheet */ .dtool-lens canvas{ border:5px solid #5a6c85; box-shadow:0 8px 22px rgba(0,0,0,.3); diff --git a/public/js/board.js b/public/js/board.js index fdabac9..5b2cd6e 100644 --- a/public/js/board.js +++ b/public/js/board.js @@ -352,16 +352,27 @@ const bInk = document.createElement("canvas"); bInk.id = "boardInk"; board.insertBefore(bInk, board.firstChild); const bCtx = bInk.getContext("2d"); -const bDpr = window.devicePixelRatio || 1; +/* aparte laag voor de markeerstift: dáár tekent hij dekkend, de laag zelf is + doorschijnend (CSS), en pas bij het loslaten wordt de hele haal in één keer + met 32% op de inktlaag gezet. Zo stapelen overlappende lijnstukjes binnen + één haal niet meer tot donkere vlekken op elke overgang. */ +const bMark = document.createElement("canvas"); +bMark.id = "boardMark"; +board.insertBefore(bMark, bInk.nextSibling); +const bMarkCtx = bMark.getContext("2d"); +let bDpr = window.devicePixelRatio || 1; function bResize(){ const w = board.clientWidth, h = board.clientHeight; if(w<2 || h<2) return; + bDpr = window.devicePixelRatio || 1; /* vers: verhuizen naar ander scherm/zoom */ const keep = document.createElement("canvas"); keep.width = bInk.width; keep.height = bInk.height; if(bInk.width) keep.getContext("2d").drawImage(bInk,0,0); bInk.width = Math.round(w*bDpr); bInk.height = Math.round(h*bDpr); if(keep.width) bCtx.drawImage(keep,0,0); bCtx.lineCap = "round"; bCtx.lineJoin = "round"; + bMark.width = bInk.width; bMark.height = bInk.height; + bMarkCtx.lineCap = "round"; bMarkCtx.lineJoin = "round"; } new ResizeObserver(bResize).observe(board); bResize(); @@ -555,11 +566,25 @@ function makeMovable(t){ const r = t.getBoundingClientRect(); const cx = r.left+r.width/2, cy = r.top+r.height/2; const start = Math.atan2(e.clientY-cy, e.clientX-cx)*180/Math.PI - ang; + /* klikt vast op veelvouden van 45° (waterpas/haaks in één beweging) + en toont tijdens het draaien de hoek in graden */ + const badge = t.querySelector(".dtool-deg") || t.appendChild(Object.assign(document.createElement("div"), { className:"dtool-deg" })); + badge.style.display = "block"; const mv = ev=>{ - ang = Math.atan2(ev.clientY-cy, ev.clientX-cx)*180/Math.PI - start; + let a = Math.atan2(ev.clientY-cy, ev.clientX-cx)*180/Math.PI - start; + const near = Math.round(a/45)*45; + if(Math.abs(a-near) < 4) a = near; + ang = a; t.style.transform = `rotate(${ang}deg)`; + const shown = Math.round(((ang % 360) + 360) % 360); + badge.textContent = shown + "°"; + badge.style.transform = `rotate(${-ang}deg)`; /* leesbaar houden */ + badge.classList.toggle("snapped", Math.abs(((ang % 45) + 45) % 45) < 0.01); + }; + const up = ()=>{ + badge.style.display = "none"; + document.removeEventListener("pointermove",mv); document.removeEventListener("pointerup",up); }; - const up = ()=>{ document.removeEventListener("pointermove",mv); document.removeEventListener("pointerup",up); }; document.addEventListener("pointermove",mv); document.addEventListener("pointerup",up); return; } @@ -582,17 +607,21 @@ function toggleDTool(kind){ const t = document.createElement("div"); t.className = "dtool dtool-"+kind; if(kind==="ruler"){ - t._len = 600; t._hc = false; + /* 50px = 1 cm; logische lengtes in hele centimeters, schaal begint bij 0 + met een kleine kantlijn zoals op een echte liniaal */ + const RULER_CMS = [5, 10, 15, 20]; + t._cm = 10; t._hc = false; const drawRuler = ()=>{ - const L = t._len, hc = t._hc; + const cmLen = Math.min(t._cm, Math.max(5, Math.floor((board.clientWidth-60)/50))); + const pad = 18, scale = cmLen*50, L = scale + pad*2, hc = t._hc; const ink = hc ? "#000" : "#5a4a20"; const bg = hc ? "rgba(255,214,0,.95)" : "rgba(255,243,205,.82)"; let ticks = ""; - for(let i=5; i`; - if(cm) ticks += `${i/50}`; + ticks += ``; + if(cm) ticks += `${i/50}`; } t.style.width = L+"px"; t.querySelector(".dtool-svg").innerHTML = ` @@ -602,18 +631,23 @@ function toggleDTool(kind){ - teach · cm + ${cmLen} cm ${ticks}`; + const lenBtn = t.querySelector(".do-len"); + lenBtn.textContent = cmLen + " cm"; + lenBtn.title = T("dtLen"); }; t.innerHTML = `
- - + +
`; t.querySelector(".do-len").addEventListener("click", ()=>{ - t._len = t._len===600 ? 800 : t._len===800 ? 400 : 600; drawRuler(); + t._cm = RULER_CMS[(RULER_CMS.indexOf(t._cm)+1) % RULER_CMS.length]; drawRuler(); }); - t.querySelector(".do-hc").addEventListener("click", ()=>{ t._hc = !t._hc; drawRuler(); }); + const hcBtn = t.querySelector(".do-hc"); + hcBtn.title = T("dtContrast"); + hcBtn.addEventListener("click", ()=>{ t._hc = !t._hc; drawRuler(); }); t.style.height = "76px"; drawRuler(); }else if(kind==="geo"){ @@ -663,10 +697,14 @@ function toggleDTool(kind){ `; - t.querySelector(".do-len").addEventListener("click", ()=>{ + 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-hc").addEventListener("click", ()=>{ t._hc = !t._hc; drawGeo(); }); + const geoHc = t.querySelector(".do-hc"); + geoHc.title = T("dtContrast"); + geoHc.addEventListener("click", ()=>{ t._hc = !t._hc; drawGeo(); }); drawGeo(); }else if(kind==="light"){ /* compact traffic light directly on the board */ let on = "green"; @@ -794,7 +832,7 @@ function startBoardText(e){ inp.addEventListener("blur", commit); } -let bDraw = false, bx = 0, by = 0; +let bDraw = false, bx = 0, by = 0, bMidX = null, bMidY = null, bUsedMark = false; const bPos = ev=>{ const r = bInk.getBoundingClientRect(); /* ratio-based: correct under any display zoom */ @@ -808,6 +846,7 @@ bInk.addEventListener("pointerdown", e=>{ bDraw = true; PEN.nib = PEN.size*0.8; [bx,by] = bPos(e); + bMidX = bMidY = null; bStroke(bx+0.01, by+0.01, e.pressure, e.pointerType); }); bInk.addEventListener("pointermove", e=>{ @@ -817,7 +856,20 @@ bInk.addEventListener("pointermove", e=>{ }); }); const bStop = ()=>{ - if(bDraw) markBoardChange(); + if(bDraw){ + /* markeerstift: de haal staat dekkend op de overlay - nu in één keer + met vaste transparantie op de inktlaag zetten (egale kleur, geen + donkere overgangen) en de overlay leegmaken */ + if(bUsedMark && bMark.width){ + bCtx.globalCompositeOperation = "source-over"; + bCtx.globalAlpha = 0.32; + bCtx.drawImage(bMark, 0, 0); + bCtx.globalAlpha = 1; + bMarkCtx.clearRect(0, 0, bMark.width, bMark.height); + } + bUsedMark = false; + markBoardChange(); + } bDraw = false; bCtx.globalAlpha = 1; if(DTOOLS.lens) DTOOLS.lens.lensDraw(); @@ -825,12 +877,15 @@ const bStop = ()=>{ bInk.addEventListener("pointerup", bStop); bInk.addEventListener("pointercancel", bStop); function bStroke(x,y,pressure,ptype){ - bCtx.globalCompositeOperation = PEN.tool==="erase" ? "destination-out" : "source-over"; - bCtx.globalAlpha = PEN.tool==="mark" ? 0.32 : 1; - bCtx.strokeStyle = PEN.color; + const mark = PEN.tool==="mark"; + if(mark) bUsedMark = true; + const ctx = mark ? bMarkCtx : bCtx; /* markeerstift op de eigen laag */ + ctx.globalCompositeOperation = PEN.tool==="erase" ? "destination-out" : "source-over"; + ctx.globalAlpha = 1; + ctx.strokeStyle = PEN.color; let w = PEN.size; if(PEN.tool==="erase") w = PEN.size*6; - else if(PEN.tool==="mark") w = PEN.size*3.5; + else if(mark) w = PEN.size*3.5; else if(PEN.tool==="vulp"){ let target; if(ptype==="pen" && pressure > 0){ @@ -842,8 +897,16 @@ function bStroke(x,y,pressure,ptype){ PEN.nib += (target - PEN.nib)*0.3; w = PEN.nib; } - bCtx.lineWidth = w*bDpr; - bCtx.beginPath(); bCtx.moveTo(bx,by); bCtx.lineTo(x,y); bCtx.stroke(); + ctx.lineWidth = w*bDpr; + /* vloeiende haal: teken van middelpunt naar middelpunt met het vorige + punt als stuurpunt (kwadratische curve) - geen hoekige knikken meer + tussen de losse pointer-stapjes, ook bij snelle halen */ + const mx = (bx + x) / 2, my = (by + y) / 2; + ctx.beginPath(); + ctx.moveTo(bMidX ?? bx, bMidY ?? by); + ctx.quadraticCurveTo(bx, by, mx, my); + ctx.stroke(); + bMidX = mx; bMidY = my; bx = x; by = y; } function boardInkURL(){ try{ return bInk.toDataURL("image/png"); }catch(e){ return null; } } diff --git a/public/js/core.js b/public/js/core.js index 726cf2f..a5a68f3 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.64-beta"; +const VERSION = "0.3.65-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; @@ -231,6 +231,7 @@ const I18N = { parentUnlink:"Ontkoppel", parentUnlinkWarn:"Verwijdert alleen de koppeling; het account van je kind blijft bestaan.", parentProgress:"📈 Voortgang", parentProgressEmpty:"Nog geen voortgang.", + dtLen:"Lengte", dtContrast:"Hoog contrast", pupilEmpty:"Je leerkracht heeft nog niets voor je klaargezet.", pupilWatching:"meekijken", amUsers:"Gebruikers", amClasses:"Klassen", amSchools:"Scholen", amOverarching:"Systeem", @@ -489,6 +490,7 @@ const I18N = { parentUnlink:"Unlink", parentUnlinkWarn:"Removes only the link; your child's account remains.", parentProgress:"📈 Progress", parentProgressEmpty:"No progress yet.", + dtLen:"Length", dtContrast:"High contrast", pupilEmpty:"Your teacher hasn't set up anything for you yet.", pupilWatching:"watching", amUsers:"Users", amClasses:"Classes", amSchools:"Schools", amOverarching:"System",