Use shift to drag
- - - -diff --git a/Room/room.js b/Room/room.js index 17c7661..f9ea584 100644 --- a/Room/room.js +++ b/Room/room.js @@ -148,6 +148,38 @@ export class Room { } } + + async deleteAllStates(roomKey) { + try { + await this.ensureStorage(); + const room = await this.getRoom(roomKey); + + if(!this.hasDrawings(roomKey)) { + alert('No drawings found.') + return + } + + const updatedStates = [] + const updatedRoom = { + ...room, + states: updatedStates, + lastModified: Date.now() + }; + + await roomDB.put(roomKey, updatedRoom); + console.log(`All states deleted from room:`, roomKey); + NetworkManager.broadcast({ + t: 'all_states_deleted', + from: state.localPeerId, + roomKey, + }); + return updatedStates; + } catch (error) { + console.error('Error deleting states:', error); + return false; + } + } + async getRoomState(roomKey) { await this.ensureStorage(); const room = await this.getRoom(roomKey); @@ -305,10 +337,6 @@ export class Room { } } - async deleteDrawings(roomKey) { - return null - } - async getAllRooms() { await this.ensureStorage(); diff --git a/app.js b/app.js index 1c77860..b2006b4 100644 --- a/app.js +++ b/app.js @@ -1,7 +1,7 @@ import Hyperswarm from 'hyperswarm'; import b4a from 'b4a'; import crypto from 'hypercore-crypto'; -import { addAlphaToColor, getRandomColorPair } from "./helper.js"; +import loadIcons, { addAlphaToColor, getRandomColorPair } from "./helper.js"; import {Room, room} from "./Room/room.js"; import {globalState} from "./storage/GlobalState.js"; @@ -58,6 +58,10 @@ export const ui = { slideStateBtn: $('.slide-state-btn'), slideStateCloseBtn: $('#slide-state-close-btn'), + slideIconBtn: $('#slide-icon-btn'), + slideIconContainer: $('#slide-icon-container'), + slideIconCloseBtn: $('#slide-icon-close-btn'), + // Tools tools: $('#tools'), color: $('#color'), @@ -119,6 +123,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 +405,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 +516,7 @@ export class CanvasManager { // GRID RENDERING // ============================================================================ + class GridRenderer { static render(ctx, scale, translateX, translateY) { const viewWidth = ui.canvas.clientWidth; @@ -329,23 +538,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 +558,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 +589,6 @@ class GridRenderer { return 10 * base; } } - // ============================================================================ // OBJECT RENDERING // ============================================================================ @@ -898,7 +1072,7 @@ class TextEditor { } static createEditorElement(x, y, textObj, fontPixels, initialText) { - const div = document.createElement('div'); + const div = document.createElement('textarea'); div.className = 'text-editor'; div.contentEditable = 'true'; div.dataset.id = textObj.id; @@ -919,10 +1093,11 @@ class TextEditor { outline: 'none', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', zIndex: '9999', - whiteSpace: 'nowrap', + whiteSpace: 'pre-wrap', overflow: 'visible', minWidth: '20px', minHeight: '16px', + maxWidth: '400px', transformOrigin: 'top left', transform: state.zoom !== 1 ? `scale(${state.zoom})` : 'none' }); @@ -958,27 +1133,39 @@ class TextEditor { } static commit() { - if (!state.textEl) return; + // Capture and clear the editor element from state immediately + const editorDiv = state.textEl; + state.textEl = null; + state.activeId = null; + + // If there's no active editor, nothing to do + if (!editorDiv) return; - const id = state.textEl.dataset.id; + const id = editorDiv.dataset.id; const obj = state.doc.objects[id]; if (!obj) { - this.close(false); + // No backing object? Just remove the editor + editorDiv.remove(); return; } - const text = (state.textEl.textContent || '').trim(); + // Read and trim the textContent safely + const text = editorDiv.textContent ? editorDiv.textContent.trim() : ''; + if (text === '') { - // Remove empty text object + // Delete empty text object DocumentManager.deleteObject(id, true); } else { - // Update text object - DocumentManager.updateObject(id, { text, rev: obj.rev + 1 }, true); + // Update the text object + DocumentManager.updateObject(id, { text }, true); } - this.close(false); + // Remove editor from DOM + editorDiv.remove(); } + + static close(shouldCommit = false) { if (!state.textEl) return; @@ -1495,7 +1682,7 @@ class CursorManager { ui.mouse.style.left = `${event.clientX + 20}px`; ui.mouse.style.top = `${event.clientY + 20}px`; ui.mouse.style.transform = "translate(-50%, -50%)"; - ui.mouse.textContent = state.peerName || 'You'; + ui.mouse.textContent = 'You'; } // Throttle network broadcasts @@ -1957,11 +2144,6 @@ class UIManager { const success = await room.addRoomState(state.topicKey); if (success) { alert('Drawing state saved to Hyperbee 🐝'); - // Show visual feedback - ui.saveState.textContent = 'Savinggg'; - setTimeout(() => { - ui.saveState.textContent = 'Save State'; - }, 2000); } else { alert('Failed to save drawing state'); } @@ -2004,6 +2186,7 @@ class SessionManager { UIManager.showLoading(); ui.topicOut.dataset.value = topicHex; + ui.topicOut.textContent = topicHex try { await NetworkManager.initSwarm(topicHex); @@ -2425,10 +2608,10 @@ function renderRoomList(rooms) { const html = rooms .map((room) => `
Created: ${new Date(room.value.createdAt).toLocaleString()} -
Icon
+by ${state.savedBy}
+