From 697c5ce69ee9197d4dd9c10d267a04059d50cb41 Mon Sep 17 00:00:00 2001 From: Rohan Chaudhary Date: Tue, 21 Oct 2025 16:02:18 +0530 Subject: [PATCH 1/3] improving UI and bugs --- app.js | 357 +++++++++++++++++++++++++++++--------- assets/.DS_Store | Bin 6148 -> 6148 bytes assets/icons/diamond.png | Bin 0 -> 1404 bytes assets/icons/download.png | Bin 0 -> 623 bytes assets/icons/ellipse.png | Bin 0 -> 1002 bytes assets/icons/eraser.png | Bin 0 -> 1110 bytes assets/icons/hand.png | Bin 0 -> 1617 bytes assets/icons/line.png | Bin 0 -> 904 bytes assets/icons/load.png | Bin 0 -> 1035 bytes assets/icons/peers.png | Bin 0 -> 1731 bytes assets/icons/rect.png | Bin 0 -> 555 bytes assets/icons/redo.png | Bin 0 -> 860 bytes assets/icons/setting.png | Bin 0 -> 1851 bytes assets/icons/slide.png | Bin 0 -> 7502 bytes assets/icons/text.png | Bin 0 -> 964 bytes assets/icons/undo.png | Bin 0 -> 821 bytes index.html | 112 ++++++++---- style.css | 254 ++++++++++++++++++++------- 18 files changed, 543 insertions(+), 180 deletions(-) create mode 100644 assets/icons/diamond.png create mode 100644 assets/icons/download.png create mode 100644 assets/icons/ellipse.png create mode 100644 assets/icons/eraser.png create mode 100644 assets/icons/hand.png create mode 100644 assets/icons/line.png create mode 100644 assets/icons/load.png create mode 100644 assets/icons/peers.png create mode 100644 assets/icons/rect.png create mode 100644 assets/icons/redo.png create mode 100644 assets/icons/setting.png create mode 100644 assets/icons/slide.png create mode 100644 assets/icons/text.png create mode 100644 assets/icons/undo.png diff --git a/app.js b/app.js index 1c77860..d6e887f 100644 --- a/app.js +++ b/app.js @@ -119,6 +119,232 @@ export class CanvasManager { this.setupEventListeners(); this.startRenderLoop(); this.renderFrame(); + + // Add state for tracking touches and space key + state.isDragging = false; + state.isSpacePressed = false; + state.lastTouchX = 0; + state.lastTouchY = 0; + } + + static setupEventListeners() { + window.addEventListener('resize', () => { + this.resizeCanvas(); + if (typeof CursorManager !== 'undefined') { + CursorManager.handleWindowResize(); + } + }); + + ui.canvas.addEventListener('wheel', (e) => { + e.preventDefault(); + if (e.ctrlKey) { + const zoomFactor = e.deltaY < 0 ? CONFIG.ZOOM_STEP : 1 / CONFIG.ZOOM_STEP; + const newZoom = Math.min( + CONFIG.MAX_ZOOM, + Math.max(CONFIG.MIN_ZOOM, state.zoom * zoomFactor) + ); + + if (newZoom === state.zoom) return; + + const rect = ui.canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + const worldX = (mouseX - state.panX) / state.zoom; + const worldY = (mouseY - state.panY) / state.zoom; + + state.zoom = newZoom; + + state.panX = mouseX - (worldX * newZoom); + state.panY = mouseY - (worldY * newZoom); + + const scalePercent = Math.round(newZoom * 100); + ui.scaleDisplay.textContent = `${scalePercent}%`; + + this.clampPan(); + state.requestRender(); + + if (typeof CursorManager !== 'undefined') { + CursorManager.handleCanvasTransform(); + } + } else { + state.panX -= e.deltaX; + state.panY -= e.deltaY; + + this.clampPan(); + state.requestRender(); + + if (typeof CursorManager !== 'undefined') { + CursorManager.handleCanvasTransform(); + } + } + }, { passive: false }); + + + // Add these variables at the class level (keep as-is) + let lastPinchDistance = 0; + let initialPinchDistance = 0; // Add this + let pinchStartZoom = 0; + let pinchCenter = { x: 0, y: 0 }; + +// CORRECTED touchstart handler + ui.canvas.addEventListener('touchstart', (e) => { + if (e.touches.length === 2) { + e.preventDefault(); + const touch1 = e.touches[0]; + const touch2 = e.touches[1]; + + // Calculate initial pinch distance + initialPinchDistance = Math.hypot( + touch2.clientX - touch1.clientX, + touch2.clientY - touch1.clientY + ); + lastPinchDistance = initialPinchDistance; + + // Store initial zoom level + pinchStartZoom = state.zoom; + + // Calculate and STORE the initial pinch center point + const rect = ui.canvas.getBoundingClientRect(); + pinchCenter = { + x: (touch1.clientX + touch2.clientX) / 2, + y: (touch1.clientY + touch2.clientY) / 2 + }; + + // Convert initial pinch center to world coordinates + pinchCenter.worldX = (pinchCenter.x - rect.left - state.panX) / state.zoom; + pinchCenter.worldY = (pinchCenter.y - rect.top - state.panY) / state.zoom; + } + }); + +// CORRECTED touchmove handler + ui.canvas.addEventListener('touchmove', (e) => { + if (e.touches.length === 2) { + e.preventDefault(); + const touch1 = e.touches[0]; + const touch2 = e.touches[1]; + + // Calculate current pinch distance + const currentPinchDistance = Math.hypot( + touch2.clientX - touch1.clientX, + touch2.clientY - touch1.clientY + ); + + // Calculate zoom based on initial distance (not last distance) + const scale = currentPinchDistance / initialPinchDistance; + const newZoom = Math.min( + CONFIG.MAX_ZOOM, + Math.max(CONFIG.MIN_ZOOM, pinchStartZoom * scale) + ); + + if (newZoom !== state.zoom) { + // Get current pinch center + const currentPinchCenter = { + x: (touch1.clientX + touch2.clientX) / 2, + y: (touch1.clientY + touch2.clientY) / 2 + }; + + const rect = ui.canvas.getBoundingClientRect(); + + // Apply new zoom + state.zoom = newZoom; + + // Update pan to keep the world point under the pinch center fixed + state.panX = currentPinchCenter.x - rect.left - (pinchCenter.worldX * newZoom); + state.panY = currentPinchCenter.y - rect.top - (pinchCenter.worldY * newZoom); + + // Update zoom display + ui.scaleDisplay.textContent = `${Math.round(newZoom * 100)}%`; + + // Update canvas and bounds + CanvasManager.clampPan(); + state.requestRender(); + if (typeof CursorManager !== 'undefined') { + CursorManager.handleCanvasTransform(); + } + } + } + }); + +// touchend and touchcancel remain the same + ui.canvas.addEventListener('touchend', (e) => { + if (e.touches.length < 2) { + lastPinchDistance = 0; + initialPinchDistance = 0; // Also reset this + pinchStartZoom = 0; + } + }); + + ui.canvas.addEventListener('touchcancel', () => { + lastPinchDistance = 0; + initialPinchDistance = 0; // Also reset this + pinchStartZoom = 0; + }); + + // Add space + mouse drag handlers + document.addEventListener('keydown', (e) => { + if (e.code === 'Space' && !state.isSpacePressed) { + state.isSpacePressed = true; + ui.canvas.style.cursor = 'grab'; + } + }); + + document.addEventListener('keyup', (e) => { + if (e.code === 'Space') { + state.isSpacePressed = false; + ui.canvas.style.cursor = 'default'; + } + }); + + ui.canvas.addEventListener('mousedown', (e) => { + if (state.isSpacePressed) { + e.preventDefault(); + state.isDragging = true; + state.lastTouchX = e.clientX; + state.lastTouchY = e.clientY; + ui.canvas.style.cursor = 'grabbing'; + } + }); + + ui.canvas.addEventListener('mousemove', (e) => { + if (state.isDragging && state.isSpacePressed) { + e.preventDefault(); + const dx = e.clientX - state.lastTouchX; + const dy = e.clientY - state.lastTouchY; + + state.panX += dx; + state.panY += dy; + + state.lastTouchX = e.clientX; + state.lastTouchY = e.clientY; + + this.clampPan(); + state.requestRender(); + } + }); + + ui.canvas.addEventListener('mouseup', () => { + if (state.isSpacePressed) { + state.isDragging = false; + ui.canvas.style.cursor = 'grab'; + } + }); + + // Prevent space from scrolling the page + window.addEventListener('keydown', (e) => { + if (e.code === 'Space') { + e.preventDefault(); + } + }); + + // Existing zoom button listeners... + ui.zoomMax.addEventListener('click', () => { + this.handleZoomButton(CONFIG.ZOOM_STEP); + }); + + ui.zoomMin.addEventListener('click', () => { + this.handleZoomButton(1 / CONFIG.ZOOM_STEP); + }); } static generateThumbnail(canvas, maxWidth = 300, maxHeight = 150) { @@ -175,28 +401,6 @@ export class CanvasManager { state.panY = -startTopWorld * state.zoom; } - static setupEventListeners() { - window.addEventListener('resize', () => { - this.resizeCanvas(); - if (typeof CursorManager !== 'undefined') { - CursorManager.handleWindowResize(); - } - }); - - ui.canvas.addEventListener('wheel', (e) => { - e.preventDefault(); - this.handleWheel(e); - }, { passive: false }); - - ui.zoomMax.addEventListener('click', () => { - this.handleZoomButton(CONFIG.ZOOM_STEP); - }); - - ui.zoomMin.addEventListener('click', () => { - this.handleZoomButton(1 / CONFIG.ZOOM_STEP); - }); - } - static handleZoomButton(zoomFactor) { const newZoom = Math.min( CONFIG.MAX_ZOOM, @@ -308,6 +512,7 @@ export class CanvasManager { // GRID RENDERING // ============================================================================ + class GridRenderer { static render(ctx, scale, translateX, translateY) { const viewWidth = ui.canvas.clientWidth; @@ -329,23 +534,19 @@ class GridRenderer { const minorPixels = minorStep * state.zoom; const showMinor = minorPixels >= CONFIG.MIN_MINOR_PX; - // Switch to screen space for crisp lines + // Switch to screen space for crisp dots ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); - ctx.lineWidth = 1; - // Render minor grid + // Render minor grid dots if (showMinor) { - this.renderGridLines(ctx, minorStep, leftWorld, topWorld, rightWorld, bottomWorld, - scale, translateX, translateY, 'rgba(0,0,0,0.05)'); + this.renderGridDots(ctx, minorStep, leftWorld, topWorld, rightWorld, bottomWorld, + scale, translateX, translateY, 'rgba(0,0,0,0.15)', 1); } - // Render major grid - this.renderGridLines(ctx, majorStep, leftWorld, topWorld, rightWorld, bottomWorld, - scale, translateX, translateY, 'rgba(0,0,0,0.12)'); - - // Render axes - this.renderAxes(ctx, scale, translateX, translateY); + // Render major grid dots + this.renderGridDots(ctx, majorStep, leftWorld, topWorld, rightWorld, bottomWorld, + scale, translateX, translateY, 'rgba(0,0,0,0.45)', 3); ctx.restore(); @@ -353,52 +554,22 @@ class GridRenderer { ctx.setTransform(scale, 0, 0, scale, translateX, translateY); } - static renderGridLines(ctx, step, leftWorld, topWorld, rightWorld, bottomWorld, - scale, translateX, translateY, color) { - ctx.strokeStyle = color; + static renderGridDots(ctx, step, leftWorld, topWorld, rightWorld, bottomWorld, + scale, translateX, translateY, color, dotSize) { + ctx.fillStyle = color; const startX = Math.floor(leftWorld / step) * step; const startY = Math.floor(topWorld / step) * step; - // Vertical lines for (let x = startX; x <= rightWorld; x += step) { - const screenX = Math.round(scale * x + translateX) + 0.5; - ctx.beginPath(); - ctx.moveTo(screenX, 0); - ctx.lineTo(screenX, ui.canvas.height); - ctx.stroke(); - } - - // Horizontal lines - for (let y = startY; y <= bottomWorld; y += step) { - const screenY = Math.round(scale * y + translateY) + 0.5; - ctx.beginPath(); - ctx.moveTo(0, screenY); - ctx.lineTo(ui.canvas.width, screenY); - ctx.stroke(); - } - } - - static renderAxes(ctx, scale, translateX, translateY) { - const screenX0 = Math.round(scale * 0 + translateX) + 0.5; - const screenY0 = Math.round(scale * 0 + translateY) + 0.5; - - ctx.strokeStyle = 'rgba(0,0,0,0.25)'; + for (let y = startY; y <= bottomWorld; y += step) { + const screenX = Math.round(scale * x + translateX); + const screenY = Math.round(scale * y + translateY); - // Y-axis - if (screenX0 >= 0 && screenX0 <= ui.canvas.width) { - ctx.beginPath(); - ctx.moveTo(screenX0, 0); - ctx.lineTo(screenX0, ui.canvas.height); - ctx.stroke(); - } - - // X-axis - if (screenY0 >= 0 && screenY0 <= ui.canvas.height) { - ctx.beginPath(); - ctx.moveTo(0, screenY0); - ctx.lineTo(ui.canvas.width, screenY0); - ctx.stroke(); + ctx.beginPath(); + ctx.arc(screenX, screenY, dotSize, 0, Math.PI * 2); + ctx.fill(); + } } } @@ -414,7 +585,6 @@ class GridRenderer { return 10 * base; } } - // ============================================================================ // OBJECT RENDERING // ============================================================================ @@ -2004,6 +2174,7 @@ class SessionManager { UIManager.showLoading(); ui.topicOut.dataset.value = topicHex; + ui.topicOut.textContent = topicHex try { await NetworkManager.initSwarm(topicHex); @@ -2425,10 +2596,10 @@ function renderRoomList(rooms) { const html = rooms .map((room) => `
  • -
    ${room.value.roomName}
    -
    +
    ${room.value.roomName}
    +

    Created: ${new Date(room.value.createdAt).toLocaleString()} -

    +

  • `) @@ -2562,12 +2733,14 @@ if (!window.__WB_EVENTS_BOUND__) {
    State preview
    -
    State ${index + 1}
    -
    ${new Date(state.savedAt).toLocaleString()}
    -
    ${state.order?.length || 0} objects
    -
    by ${state.savedBy}
    +
    State ${index + 1}
    +
    +

    ${new Date(state.savedAt).toLocaleString()}

    + +

    by ${state.savedBy}

    +
    +
    -
    `; @@ -2642,6 +2815,26 @@ if (!window.__WB_EVENTS_BOUND__) { console.log(state) }) + // Add this to your initialization code + document.addEventListener('DOMContentLoaded', () => { + const toggleRightPanel = document.getElementById('toggleRightPanel'); + const rightPanelContainer = document.getElementById('rightPanelContainer'); + + toggleRightPanel.addEventListener('click', () => { + const isExpanded = toggleRightPanel.getAttribute('aria-expanded') === 'true'; + toggleRightPanel.setAttribute('aria-expanded', !isExpanded); + rightPanelContainer.classList.toggle('hidden'); + }); + + // Close panel when clicking outside + document.addEventListener('click', (e) => { + if (!e.target.closest('.right-top-data')) { + toggleRightPanel.setAttribute('aria-expanded', 'false'); + rightPanelContainer.classList.add('hidden'); + } + }); + }); + // Export for potential external use if (typeof module !== 'undefined' && module.exports) { module.exports = { diff --git a/assets/.DS_Store b/assets/.DS_Store index 59146f038b2a426abb7f727f8f59b1246f72fe6a..65f6bf08d8f7ed1f52e92bbc4865b3a8e4a081fe 100644 GIT binary patch delta 131 zcmZoMXfc@JFUrfnz`)4BAi%(o&rrmm$56nK$B@2RkYhQsAV`XfA(^2RC{YZOo_vs1 zoYfI1syA7WMQpMZiz(xd$pI{d>Jrt}#zvMp3Wi4JwK@vbmPQ6T3MR&8wY8iaqRRT# dLGjr+xq10rlb^H5Gj?xgXFbidnVsV=KLFxk9?k#& delta 101 zcmZoMXfc@JFU-Thz`)4BAi%&-?3t6FoRpKFv{{g2B{L(>WCs>!#_f|^SPIp|tE(*x z&2<#aOlx%%sx2&xbQDYt&1!2oIYgE9t%KsTb8_?YJ15Jq$}@ItwqZTZG_k>SGdss$ FegG288;1Y@ diff --git a/assets/icons/diamond.png b/assets/icons/diamond.png new file mode 100644 index 0000000000000000000000000000000000000000..bc2c6f1d56c80df131a5dfe7a98d4bbcb92c8081 GIT binary patch literal 1404 zcmV-?1%vvDP)Cz=EWCZ=mwsVpMR8roa=V3#u(#l09}BQ(GSd& z(XY;TIo0IQ`~u(~F?knQrmiiMb4KPY7*+uQlYuMhE^n?eXS2F~1(-Z60suw>XOy{p zF?08+d1rvpO&tIb0Ed*RFMyFT^GC{@aQ-3WqqX_vX-y6sIF0I&+U zCno*`=A>9VN7fj@Sk+Vk0CRx>Wng6k7%NqP1Hjy-2mqJ>^r^S-whUO?6iy#7q0j*U zqkxl2^g#=l2Nm8)U{sL<*ssKX4YXRoY!$2!cYh%R0JbQRzXFpiVowsx5PwUd0sxi+ zgCg#4V7|?}BpmYv+X&b4LIeQJ0&b|gv&Oc0#kEH9+yG{I4FDJqTu`FA^D?hEyA|IB zV7!+AfEM7m5_1G7IS8O6Tp{mqpv6Z3dzFaqfVRBNEABSo4Egsu4FK4v$gcs@T$xuI zrU`ebW22J*fF;1cLV5>SN=mRxf&T!E^KH6@#&M!8 z)Va$_0Khs0+7Ha|Z8k>h45~})>oNrZECg;-DP=G9Z8AX!OR1FDZvzV(3ILc2Tvcgq z@@+0f2%D7dtH9J$0RUrwa|)zxDZ8kH>Js}oU`(0-cBsWCh$&?kbxXhf=0HS56|2@wu>Y(~LV{CbDC?H*is(PYZ)Is%= z<{`^-NdR>jsvnr?+hmN^Xs^KwL#{~(tjkc%@>TeaMC%fQmw=BH_-AA&L*-z;MKp!_ zJ|y8&r3`fkSnSdK z;)s?fQ)4tp{IW{@cc8Q!HXn5zG+(6>Hd*@6x+UjP1*;-@HR(1Us;9)1)T#$&tC)8Kj zL12Ztwt`Bh>0iL(&UCpV5~DAwyL=OvE#sy-|GZO8F3BW>H4nJ zFx~)R#$PeZihzQWC9V-A!TD(=<%vb94CUqJdYO6I#mR{Use1WE>9gP2NC8#9@pN$v zshIP2)_%W30V3`8qK0~U0^+G z)s;2CaPPP0wh8K~4`xHIaW}%_zopr0EE5xwEzGB literal 0 HcmV?d00001 diff --git a/assets/icons/ellipse.png b/assets/icons/ellipse.png new file mode 100644 index 0000000000000000000000000000000000000000..fb57f9a6de0db41e89f48069e332d2e62b20bed6 GIT binary patch literal 1002 zcmVuEk6jvg2 z;X>SWBSl0I1rc1h?aD=V(v~8khzmE0;!?qtb)i_LB8egq)CyKDv?j!UE@qyYOy{LZ z-efL$^A3FE&2aymbLQUpCimVfB6JzO)C5vvY(Q!PsWCPnHG$L^8<3hnYINR!i8eaP zvTO(T;-(^QEd8FqdHh-Y?Z>YXabe53HT@-+Wm$$haj1y)+70y{b&(CAo%KM-nH)9_TU~(OO%;A-YcyB{O*w_e=&+YfE{bc@-PlXrp zAg&v7Y8}8)oWPG)>H9O>+xQeHp2{qi26Vhs81n>nHZeB`@W|vt{-iu>)N#3tx3ISn z1LQ+-en4kG<9Xb&RifA6HHjzjayTVla=fJJYo@t3p3Vc(!p6Ow_bmNjdWsE8oCt37?}!c?oQ+QpPyV;}R?5x|vc zag=aZ<$A8?%Cz@POJq{uXyy9M_)|C{7V&zyEM97vUmJMA>fP|;5wY0$e}~J3y1O7! z6zukrSW%(7i=w59-CY(hHPGFSky1n5-5M)3+TG33QU~4L9xrv)-2)O80wZ@sW_(C$ z$RWAykI6MPH@EFU$<7~@ZQOv`PP%khdb?q~FOZr*YK#p?O&~SK2BapC8e;=e6G)B! Y02}kMa4TB12><{907*qoM6N<$f`?h&S^xk5 literal 0 HcmV?d00001 diff --git a/assets/icons/eraser.png b/assets/icons/eraser.png new file mode 100644 index 0000000000000000000000000000000000000000..1f233a15aef0a4fee488f4058ddb6765086c81ac GIT binary patch literal 1110 zcmV-c1gZOpP)UKiiVEA=ajrS}$?2W#dj-QnVr~F8l*n zWp602u`W_AfV;rkaHaXB3AhIQ2F9vfy};T)HF{$i z@P+EaFZ zff`NB65yF4rgSFg#2+w-tpjc-VoPH}8}OaDfWs+r)B;x&IX(jOT^8#a7vcq_Gr^^J zL1|3zAzn~;6Kp0t-D)6H3tU&U5qx6ID!viu0Y->kk0%?nRX7uDdj6Oc^MI0>U?U!& zTM_HRggM3vEU*<1Fb#OA$YED|M4sh7eTzB5qMEi!>)w; z?=ZgR&a@2{NxYyA*a9q1xUYrigH>hSEomAmqIgpph`$H)rb(a^f6!@}erRC)V2RJV!xQF@IQK3w5CLZG3MC~c# z^ESb$cz|zGqAaJb@-o4>c!+jKE0aygn8&vXUc>`@k`gYoD$~yr>13(XabHT>Wtq?Q zNbuJ4f<02=KC(Xs0#_z@7Y{I-_}Scj4)WTXkc)W11*wq{V1t9aSxv}QJishqNNQ{t zSmz*jS`%^?FK8#eZTt(^=qSI*>fm*@H(>|xhcF`{SkKFJ5ypg9iofSBUeHPWSi0}W zA=t3j5z>U<;srh*hnc`v3N-9>_!tSnXZy6cG@*3j1uje|t$2ZMPS|7+TbOm;7L^I3 zzzOt1=Ak~*6I{IM=BG>-a840F09*y05dGA(30MPcBYr0DJ+Kw{nXv|228e3R%y#0R z9m;h50ruJ$08#Iz{kdzAuAy2>1NwnWL>nrtn4Q)v67RCk0V*Z|>w#6kBBCQ~wp(jH c=?A+10?E{&jM#03%>V!Z07*qoM6N<$f}jNSdH?_b literal 0 HcmV?d00001 diff --git a/assets/icons/hand.png b/assets/icons/hand.png new file mode 100644 index 0000000000000000000000000000000000000000..d42c9d3e6824644792f413f6b379ab59aee7a79e GIT binary patch literal 1617 zcmV-X2Cn&uP) zfk{hY68%w81eJ*vssCh=MUfR1LS~^9B}P(_NGjQ5MaoxMWa}f`wOaGB?&**F+q`pk zW`6UVnfu${&-}uL`#5u6bMKj%bMJ_%;u|6|0(bzp3^*V78Q2WGpsE|ZD-Z`&^;&?L zz@LDIly+btP~%&VNc}t;-wmmY7+{q(Rs-DUQI}}sINgT=lYlzR-Cas>noo1+#{eDHcvDrz0H6{0 z%`x@>&}OyUfSw`Ot|J%_%N`7C(#{+a=>vQSTo*E2>$H!l>hTb|zpQ#sphiTF13e<> zb4QjKFrRSav*1(UAn><{Y!Z>DM5K4lGJ;4s?Z*>bzX~)1i-3)Eeay8+@L3%~>y{sY zu@w~qfX5sq-@#hHz>_&TK!NXoUOpUwq(?`e9XVvIkZ509}npg13RQRkaTD3Tl9w#EAg|(9xVjUnNHJJ7Mcnocx3hn)mscMIa{KyFh3;=qQerbiG*bI0P zn3bo%N<(A}5Ru6wGv0qf!6V{=V1v{C7`PR9-=8|=N8A#)#H#-T%uv-95m^mf5A-jL zowPGxh*jUGsx1JjdIy zASQ?C8gJt)V7Z9&iy}ztfc;i|Uj)r=tG+jarq!w+1db#TE+YLg>9lcHcLydPxzQo7 z1Lj0Bi^==#;zUkvmoqjI*arLp+z@g77+?+X6>voa%~(vnuNk-_LC&xb&=*+i^dH6A zcmZy5#y=@JDH`X1a+MGV)B>M5eMf+so$Cbn!=l+5B_od@SWc|KY`=Bl2ai$BmF|FDA$|LS+gzhWWD4-0LwmQXZi|&r;ZK1pf#a4Q2Z5_Y z=mybsi~jXQGAh{t1wAHW60vUr^$|4hImYh*`kfRGNZ_Em9OFCB8zs8F(qlj@MC2S` zvD5vGs;*DUqpBSMEFt*F+WQQKf;o+1W45a9 zD3w=LIskZ{;Kt1+y2gS;s5%FT$W&mu)zuEn)Rd>n;v8_ust-uRJZ}^-JFht1&#LOT zX?eyuz+O+ah5TE0V?Hq4YHtVTW?>lT0QZc~=q!S=LPRD2_gh^G+^edYPK=b|fOE45 z%7VgCoh1bIeN9!r$jUp;0q$4Setl-c+i;7dn`kMfjJ>^)$q({>Lp`o==wDy!W_Fc4-(kvq^E zXd!q9x+Ek#-Ti5fTm@$3E+>H^iyS4k;OXG9CG#Ro_Q#g{mKRwGl<9~0&o8hJfzv!0 zKMQjz%{?vdM_xjuri4t#b->^p4Ico!8zQF#80BrYm7w5rAc#vksg@S8j~@{QifvUfL+=!dkuDJ0@i9CN~FZU(K8)1Y7v2q P00000NkvXXu0mjfYG2|P literal 0 HcmV?d00001 diff --git a/assets/icons/line.png b/assets/icons/line.png new file mode 100644 index 0000000000000000000000000000000000000000..477441f8ba35d405c13f079106c58a89a7311c30 GIT binary patch literal 904 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 literal 0 HcmV?d00001 diff --git a/assets/icons/slide.png b/assets/icons/slide.png new file mode 100644 index 0000000000000000000000000000000000000000..7bac13243dbb0649966c76516844a0326e251f92 GIT binary patch literal 7502 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 GcmE4vYDa7U literal 0 HcmV?d00001 diff --git a/assets/icons/text.png b/assets/icons/text.png new file mode 100644 index 0000000000000000000000000000000000000000..c2b602b311ca914c72c0cf72a10f7ba9b29529e6 GIT binary patch literal 964 zcmV;#13UbQP)i$fSy54KR&dfX5BYt1!+XpL_z$0o{#Dt^QPi zH|PaOfNKrQt^oFve)0)04yzkpJ$8qBrEka+@G6C_GU%H6^@lzjxddtrzB*Pm*`z&s%OQQvNCG-szpx*NH3CU2qAYU#gqg$&-Vixad%87?zY#ie6Rr4A0BxVS~J{@?eo{Vi3KQ9em>ikM5|?Z zS&7N507c8s$FgQPpyZfK0g9HNPoFTplu9-9RPXoUQH+JLV@4$_)oD{vNF zcAs(6XYf_{&j-bNV3#n^Fz~*n)5^vk+yZPh32yNVi| zK~#90?V8PRjX@NLA3|)9NP80+(JQD*2@xByA(3>$#>PS{#76KBNU$K0pfpWGS=g}< zD;9LY!dkjQgS0|?w6qb)O{uTG7IVYRy)*A55OMHj;3e+h`-nLBHNYF(!C%y~CQlpi7I*L$^sLFV z7I=p{_%1zb^0Wi*aR+}+&zd~z$PfOkp0(ns|8j2x?gPz^zpnuO1!;c>z$UrS4Nd4TfxPw; z=%L`HG2M^gC*VOr+Dm|in3HDHG{y}Bdx3$1d^E5D_<(yRjtOX%fIYw^%&y@3ZRQN0pE4_yrg&y+ktV1hhM->N%2*{YlnCSbQ7i^(w^gE6yZ0CmB1^9cn0(mCJC!? z?^3s=#aqcQjfJWUZUMeJJnWVh_f>|wr43X$)J&a6Yc8?JBJ3QIfDFZ)fOh8{?AFAA= zF##h){8&@ZKJ3e&L(^19N|@cJJ^@F31Lw;63dR`l#L~asH!zk2fVno;Whiteboard — Vanilla JS + + + @@ -45,22 +48,24 @@
    Canvas Room History
    - +