Spaces:
Running
Running
| def whiteboard_iframe(): | |
| board_html = r""" | |
| <html lang="ar" dir="rtl"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>سبورة الطالب</title> | |
| <style> | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| margin: 0; | |
| font-family: Arial, sans-serif; | |
| background: #f1f5f9; | |
| color: #0f172a; | |
| overflow: hidden; | |
| } | |
| .app { | |
| padding: 10px; | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .toolbar { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| align-items: center; | |
| background: #ffffff; | |
| border: 1px solid #cbd5e1; | |
| border-radius: 14px; | |
| padding: 10px; | |
| margin-bottom: 10px; | |
| box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08); | |
| } | |
| button { | |
| border: 1px solid #94a3b8; | |
| background: #ffffff; | |
| color: #0f172a; | |
| padding: 9px 12px; | |
| border-radius: 10px; | |
| font-size: 14px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| } | |
| button.active { | |
| background: #0f172a; | |
| color: #ffffff; | |
| } | |
| .tool-label { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 13px; | |
| font-weight: bold; | |
| } | |
| input[type="range"] { | |
| width: 100px; | |
| } | |
| .board-shell { | |
| flex: 1; | |
| overflow: auto; | |
| background: #e2e8f0; | |
| border: 2px solid #334155; | |
| border-radius: 16px; | |
| position: relative; | |
| min-height: 560px; | |
| } | |
| canvas { | |
| display: block; | |
| background: | |
| linear-gradient(#e5e7eb 1px, transparent 1px), | |
| linear-gradient(90deg, #e5e7eb 1px, transparent 1px), | |
| #ffffff; | |
| background-size: 32px 32px; | |
| touch-action: none; | |
| cursor: crosshair; | |
| } | |
| .note { | |
| font-size: 12px; | |
| line-height: 1.5; | |
| color: #475569; | |
| background: #ffffff; | |
| padding: 8px; | |
| border-radius: 10px; | |
| border: 1px solid #cbd5e1; | |
| margin-top: 8px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <div class="toolbar"> | |
| <button id="penBtn" class="active">✏️ قلم</button> | |
| <button id="eraserBtn">🧽 محاية</button> | |
| <button id="panBtn">🤚 تحريك</button> | |
| <button id="undoBtn">↩️ تراجع</button> | |
| <button id="clearBtn">🗑️ مسح</button> | |
| <label class="tool-label"> | |
| الحجم | |
| <input id="sizeInput" type="range" min="1" max="10" value="3"> | |
| </label> | |
| <label class="tool-label"> | |
| اللون | |
| <input id="colorInput" type="color" value="#000000"> | |
| </label> | |
| </div> | |
| <div id="boardShell" class="board-shell"> | |
| <canvas id="board" width="1800" height="5200"></canvas> | |
| </div> | |
| <div class="note"> | |
| اكتب بالقلم. استخدم "تحريك" للتمرير داخل السبورة. استخدم المحاية للمسح الجزئي. | |
| </div> | |
| </div> | |
| <script> | |
| const canvas = document.getElementById("board"); | |
| const shell = document.getElementById("boardShell"); | |
| const ctx = canvas.getContext("2d"); | |
| const penBtn = document.getElementById("penBtn"); | |
| const eraserBtn = document.getElementById("eraserBtn"); | |
| const panBtn = document.getElementById("panBtn"); | |
| const undoBtn = document.getElementById("undoBtn"); | |
| const clearBtn = document.getElementById("clearBtn"); | |
| const sizeInput = document.getElementById("sizeInput"); | |
| const colorInput = document.getElementById("colorInput"); | |
| let mode = "pen"; | |
| let drawing = false; | |
| let lastX = 0; | |
| let lastY = 0; | |
| let history = []; | |
| ctx.lineCap = "round"; | |
| ctx.lineJoin = "round"; | |
| function setMode(nextMode) { | |
| mode = nextMode; | |
| penBtn.classList.toggle("active", mode === "pen"); | |
| eraserBtn.classList.toggle("active", mode === "eraser"); | |
| panBtn.classList.toggle("active", mode === "pan"); | |
| if (mode === "pan") { | |
| canvas.style.touchAction = "auto"; | |
| canvas.style.cursor = "grab"; | |
| } else { | |
| canvas.style.touchAction = "none"; | |
| canvas.style.cursor = "crosshair"; | |
| } | |
| } | |
| function saveHistory() { | |
| try { | |
| if (history.length > 15) { | |
| history.shift(); | |
| } | |
| history.push(canvas.toDataURL("image/png")); | |
| } catch (e) { | |
| console.log(e); | |
| } | |
| } | |
| function restoreFromDataUrl(dataUrl) { | |
| const img = new Image(); | |
| img.onload = function () { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| ctx.drawImage(img, 0, 0); | |
| }; | |
| img.src = dataUrl; | |
| } | |
| function getPoint(event) { | |
| const rect = canvas.getBoundingClientRect(); | |
| let clientX; | |
| let clientY; | |
| if (event.touches && event.touches.length > 0) { | |
| clientX = event.touches[0].clientX; | |
| clientY = event.touches[0].clientY; | |
| } else { | |
| clientX = event.clientX; | |
| clientY = event.clientY; | |
| } | |
| return { | |
| x: clientX - rect.left, | |
| y: clientY - rect.top | |
| }; | |
| } | |
| function startDraw(event) { | |
| if (mode === "pan") { | |
| return; | |
| } | |
| event.preventDefault(); | |
| saveHistory(); | |
| drawing = true; | |
| const point = getPoint(event); | |
| lastX = point.x; | |
| lastY = point.y; | |
| } | |
| function draw(event) { | |
| if (!drawing || mode === "pan") { | |
| return; | |
| } | |
| event.preventDefault(); | |
| const point = getPoint(event); | |
| ctx.beginPath(); | |
| ctx.moveTo(lastX, lastY); | |
| ctx.lineTo(point.x, point.y); | |
| if (mode === "eraser") { | |
| ctx.globalCompositeOperation = "destination-out"; | |
| ctx.lineWidth = Math.max(12, parseInt(sizeInput.value, 10) * 3); | |
| } else { | |
| ctx.globalCompositeOperation = "source-over"; | |
| ctx.strokeStyle = colorInput.value; | |
| ctx.lineWidth = parseInt(sizeInput.value, 10); | |
| } | |
| ctx.stroke(); | |
| lastX = point.x; | |
| lastY = point.y; | |
| } | |
| function stopDraw() { | |
| drawing = false; | |
| ctx.globalCompositeOperation = "source-over"; | |
| } | |
| penBtn.addEventListener("click", function () { | |
| setMode("pen"); | |
| }); | |
| eraserBtn.addEventListener("click", function () { | |
| setMode("eraser"); | |
| }); | |
| panBtn.addEventListener("click", function () { | |
| setMode("pan"); | |
| }); | |
| undoBtn.addEventListener("click", function () { | |
| if (history.length > 0) { | |
| const last = history.pop(); | |
| restoreFromDataUrl(last); | |
| } | |
| }); | |
| clearBtn.addEventListener("click", function () { | |
| if (confirm("هل تريد مسح السبورة؟")) { | |
| saveHistory(); | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| } | |
| }); | |
| canvas.addEventListener("mousedown", startDraw); | |
| canvas.addEventListener("mousemove", draw); | |
| window.addEventListener("mouseup", stopDraw); | |
| canvas.addEventListener("touchstart", startDraw, { passive: false }); | |
| canvas.addEventListener("touchmove", draw, { passive: false }); | |
| window.addEventListener("touchend", stopDraw); | |
| setMode("pen"); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| escaped_board = html.escape(board_html, quote=True) | |
| return f""" | |
| <iframe | |
| srcdoc="{escaped_board}" | |
| style="width:100%; height:780px; border:1px solid #cbd5e1; border-radius:14px; background:white;" | |
| title="السبورة"> | |
| </iframe> | |
| """ |