Verbeter anatomie en plaatsing van avataraccessoires (v0.4.46-beta) #1
4 changed files with 103 additions and 25 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.3.64-beta
|
||||
0.3.65-beta
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<L; i+=5){
|
||||
for(let i=0; i<=scale; i+=5){
|
||||
const cm = i%50===0, half = i%25===0;
|
||||
const len = cm ? 22 : half ? 14 : 8;
|
||||
ticks += `<line x1="${i}" y1="2" x2="${i}" y2="${2+len}" stroke="${ink}" stroke-width="${cm ? (hc?2.5:1.6) : (hc?1.6:1)}"/>`;
|
||||
if(cm) ticks += `<text x="${i}" y="${hc?46:42}" font-size="${hc?18:14}" font-weight="bold" fill="${ink}" text-anchor="middle">${i/50}</text>`;
|
||||
ticks += `<line x1="${pad+i}" y1="2" x2="${pad+i}" y2="${2+len}" stroke="${ink}" stroke-width="${cm ? (hc?2.5:1.6) : (hc?1.6:1)}"/>`;
|
||||
if(cm) ticks += `<text x="${pad+i}" y="${hc?46:42}" font-size="${hc?18:14}" font-weight="bold" fill="${ink}" text-anchor="middle">${i/50}</text>`;
|
||||
}
|
||||
t.style.width = L+"px";
|
||||
t.querySelector(".dtool-svg").innerHTML = `<svg viewBox="0 0 ${L} 76" width="${L}" height="76">
|
||||
|
|
@ -602,18 +631,23 @@ function toggleDTool(kind){
|
|||
<rect x="0" y="0" width="${L}" height="76" rx="9" fill="${bg}" stroke="${ink}" stroke-width="${hc?3:1.5}"/>
|
||||
<rect x="0" y="0" width="${L}" height="76" rx="9" fill="url(#rg)"/>
|
||||
<line x1="0" y1="2" x2="${L}" y2="2" stroke="${ink}" stroke-width="${hc?3:2}"/>
|
||||
<text x="${L/2}" y="66" font-size="12" fill="${ink}" text-anchor="middle" opacity=".65">teach · cm</text>
|
||||
<text x="${L/2}" y="68" font-size="12" fill="${ink}" text-anchor="middle" opacity=".65">${cmLen} cm</text>
|
||||
${ticks}</svg>`;
|
||||
const lenBtn = t.querySelector(".do-len");
|
||||
lenBtn.textContent = cmLen + " cm";
|
||||
lenBtn.title = T("dtLen");
|
||||
};
|
||||
t.innerHTML = `<div class="dtool-svg"></div>
|
||||
<div class="dtool-opts">
|
||||
<button class="do-len" title="↔">↔</button>
|
||||
<button class="do-hc" title="◐">◐</button>
|
||||
<button class="do-len do-wide"></button>
|
||||
<button class="do-hc">◐</button>
|
||||
</div>`;
|
||||
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){
|
|||
<button class="do-len">↔</button>
|
||||
<button class="do-hc">◐</button>
|
||||
</div>`;
|
||||
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; } }
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue