/* widget: Schrijfblad */ /* ========================================================= Writing pad — pen on paper with lines or maths squares ==========================================================*/ function mountWrite(root, initState){ const COLORS = ["#26344a","#3b7dd8","#e0554d","#3fae6a","#f7c948"]; const SIZES = [3,6,10]; let color = COLORS[0], size = SIZES[1], tool = "pen", // pen | mark | text | erase bg = (initState && initState.bg) || "write"; root.innerHTML = `
`; const stage = root.querySelector(".wp-stage"), canvas = root.querySelector("canvas"), ctx = canvas.getContext("2d"), toolsEl = root.querySelector(".wp-tools"), bgsEl = root.querySelector(".wp-bgs"), colorsEl = root.querySelector(".wp-colors"), sizesEl = root.querySelector(".wp-sizes"), clearBtn = root.querySelector(".wp-clear"); const TOOLS = [["pen","✏️","toolPen"],["vulp","🖋️","toolFountain"],["mark","🖍️","toolMark"],["text","🔤","toolText"],["erase","🧽","eraser"]]; TOOLS.forEach(([id,icon,key])=>{ const b = document.createElement("button"); b.dataset.tool = id; b.textContent = icon; b.addEventListener("click", ()=>{ tool = id; refresh(); }); toolsEl.appendChild(b); }); /* background choices */ const BGS = [["blank","bgBlank"],["lines","bgLines"],["write","bgWrite"],["grid-s","bgGridS"],["grid-b","bgGridB"]]; BGS.forEach(([id,key])=>{ const b = document.createElement("button"); b.className = "wp-bg"; b.dataset.bg = id; b.appendChild(document.createElement("i")); b.addEventListener("click", ()=>{ bg = id; refresh(); }); bgsEl.appendChild(b); }); COLORS.forEach(c=>{ const b = document.createElement("button"); b.className = "wp-color"; b.dataset.c = c; const i = document.createElement("i"); i.style.background = c; b.appendChild(i); b.addEventListener("click", ()=>{ color = c; if(tool==="erase") tool = "pen"; refresh(); }); colorsEl.appendChild(b); }); SIZES.forEach(s=>{ const b = document.createElement("button"); b.className = "wp-size"; b.dataset.s = s; const i = document.createElement("i"); i.style.width = i.style.height = (s+4)+"px"; b.appendChild(i); b.addEventListener("click", ()=>{ size = s; refresh(); }); sizesEl.appendChild(b); }); clearBtn.addEventListener("click", ()=>{ ctx.clearRect(0,0,canvas.width,canvas.height); }); function refresh(){ stage.className = "wp-stage" + (bg==="blank" ? "" : " bg-"+bg) + (tool==="text" ? " t-text" : ""); toolsEl.querySelectorAll("button").forEach(b=>{ b.classList.toggle("on", b.dataset.tool===tool); b.title = T(TOOLS.find(x=>x[0]===b.dataset.tool)[2]); }); bgsEl.querySelectorAll("button").forEach(b=>{ b.classList.toggle("on", b.dataset.bg===bg); b.title = T(BGS.find(x=>x[0]===b.dataset.bg)[1]); }); colorsEl.querySelectorAll("button").forEach(b=>b.classList.toggle("on", b.dataset.c===color && tool!=="erase")); sizesEl.querySelectorAll("button").forEach(b=>{ b.classList.toggle("on", +b.dataset.s===size); b.title = T("penSize"); }); clearBtn.title = T("clearAll"); } document.addEventListener("langchange", refresh); /* canvas sizing (preserves ink on resize) */ const dpr = window.devicePixelRatio || 1; function resize(){ const w = stage.clientWidth, h = stage.clientHeight; if(w<2 || h<2) return; const keep = document.createElement("canvas"); keep.width = canvas.width; keep.height = canvas.height; if(canvas.width) keep.getContext("2d").drawImage(canvas,0,0); canvas.width = Math.round(w*dpr); canvas.height = Math.round(h*dpr); canvas.style.width = w+"px"; canvas.style.height = h+"px"; if(keep.width) ctx.drawImage(keep,0,0); ctx.lineCap = "round"; ctx.lineJoin = "round"; } new ResizeObserver(resize).observe(stage); resize(); /* restore saved ink */ if(initState && initState.ink){ const im = new Image(); im.onload = ()=>{ resize(); ctx.drawImage(im,0,0); }; im.src = initState.ink; } /* drawing */ let drawing = false, lx=0, ly=0, nibW = size; const pos = ev=>{ const r = canvas.getBoundingClientRect(); /* ratio-based: correct under app zoom and widget zoom */ return [(ev.clientX-r.left)*(canvas.width/r.width), (ev.clientY-r.top)*(canvas.height/r.height)]; }; canvas.addEventListener("pointerdown", e=>{ if(e.pointerType==="mouse" && e.button!==0) return; e.preventDefault(); if(tool==="text"){ startText(e); return; } canvas.setPointerCapture(e.pointerId); drawing = true; nibW = size*0.8; [lx,ly] = pos(e); strokeTo(lx+0.01, ly+0.01, e.pressure, e.pointerType); }); canvas.addEventListener("pointermove", e=>{ if(!drawing) return; const evs = e.getCoalescedEvents ? e.getCoalescedEvents() : [e]; evs.forEach(ev=>{ const [x,y] = pos(ev); strokeTo(x, y, ev.pressure, ev.pointerType); }); }); const stopDraw = ()=>{ drawing = false; ctx.globalAlpha = 1; }; canvas.addEventListener("pointerup", stopDraw); canvas.addEventListener("pointercancel", stopDraw); function strokeTo(x,y,pressure,ptype){ ctx.globalCompositeOperation = tool==="erase" ? "destination-out" : "source-over"; ctx.globalAlpha = tool==="mark" ? 0.32 : 1; ctx.strokeStyle = color; let w = size; if(tool==="erase") w = size*5; else if(tool==="mark") w = size*3.5; else if(tool==="vulp"){ /* nib: real pen pressure when available, otherwise slower = thicker */ let target; if(ptype==="pen" && pressure > 0){ target = size*(0.45 + pressure*1.9); }else{ const speed = Math.hypot(x-lx, y-ly)/dpr; target = Math.max(size*0.5, Math.min(size*2.3, size*2.3 - speed*0.12)); } nibW += (target - nibW)*0.3; /* smooth transitions between thin and thick */ w = nibW; } ctx.lineWidth = w*dpr; ctx.beginPath(); ctx.moveTo(lx,ly); ctx.lineTo(x,y); ctx.stroke(); lx = x; ly = y; } /* text tool: click the sheet, type, Enter (or click away) to place */ function startText(e){ if(stage.querySelector(".wp-text-input")) return; const r = stage.getBoundingClientRect(); const sc = r.width / stage.offsetWidth; /* effective zoom on this widget */ const x = (e.clientX - r.left)/sc, y = (e.clientY - r.top)/sc; const fontPx = size*6 + 10; const inp = document.createElement("input"); inp.type = "text"; inp.className = "wp-text-input"; inp.style.left = Math.min(x, r.width-140)+"px"; inp.style.top = Math.min(y, r.height-34)+"px"; inp.style.color = color; inp.style.fontSize = fontPx+"px"; stage.appendChild(inp); setTimeout(()=>inp.focus(), 0); let done = false; const commit = ()=>{ if(done) return; done = true; const v = inp.value.trim(); if(v){ ctx.globalCompositeOperation = "source-over"; ctx.globalAlpha = 1; ctx.fillStyle = color; ctx.font = `700 ${fontPx*dpr}px "Segoe UI Rounded","Segoe UI",Verdana,sans-serif`; ctx.textBaseline = "top"; ctx.fillText(v, x*dpr, y*dpr); } inp.remove(); }; inp.addEventListener("keydown", ev=>{ ev.stopPropagation(); if(ev.key==="Enter") commit(); if(ev.key==="Escape"){ done = true; inp.remove(); } }); inp.addEventListener("blur", commit); } refresh(); return { getState: ()=>{ let ink = null; try{ ink = canvas.toDataURL("image/png"); }catch(e){} return { bg, ink }; } }; }