board / index.html
madansa7's picture
Update index.html
727e4f5 verified
Raw
History Blame Contribute Delete
49.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CollabBoard - Online Whiteboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
#whiteboard {
touch-action: none;
background-image: linear-gradient(#e5e7eb 1px, transparent 1px), linear-gradient(90deg, #e5e7eb 1px, transparent 1px);
background-size: 20px 20px;
cursor: crosshair;
}
.tool-btn.active {
@apply bg-blue-100 border-blue-500;
}
.color-option {
width: 24px;
height: 24px;
border-radius: 50%;
cursor: pointer;
transition: transform 0.2s;
}
.color-option:hover {
transform: scale(1.2);
}
.color-option.selected {
transform: scale(1.3);
box-shadow: 0 0 0 2px white, 0 0 0 3px #3b82f6;
}
.sticky-note {
min-width: 150px;
min-height: 150px;
resize: both;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.sticky-note textarea {
background: transparent;
resize: none;
outline: none;
}
.shape-preview {
position: absolute;
pointer-events: none;
opacity: 0.7;
}
.board-thumbnail {
transition: transform 0.2s, box-shadow 0.2s;
}
.board-thumbnail:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* Selection rectangle */
.selection-rect {
position: absolute;
border: 2px dashed #3b82f6;
background-color: rgba(59, 130, 246, 0.1);
pointer-events: none;
}
/* Resize handles */
.resize-handle {
position: absolute;
width: 8px;
height: 8px;
background-color: #3b82f6;
border: 1px solid white;
border-radius: 50%;
pointer-events: all;
cursor: pointer;
z-index: 10;
}
.resize-handle.nw {
top: -4px;
left: -4px;
cursor: nw-resize;
}
.resize-handle.ne {
top: -4px;
right: -4px;
cursor: ne-resize;
}
.resize-handle.sw {
bottom: -4px;
left: -4px;
cursor: sw-resize;
}
.resize-handle.se {
bottom: -4px;
right: -4px;
cursor: se-resize;
}
.resize-handle.n {
top: -4px;
left: 50%;
margin-left: -4px;
cursor: n-resize;
}
.resize-handle.s {
bottom: -4px;
left: 50%;
margin-left: -4px;
cursor: s-resize;
}
.resize-handle.e {
right: -4px;
top: 50%;
margin-top: -4px;
cursor: e-resize;
}
.resize-handle.w {
left: -4px;
top: 50%;
margin-top: -4px;
cursor: w-resize;
}
</style>
</head>
<body class="bg-gray-50 h-screen flex flex-col">
<!-- Header -->
<header class="bg-white shadow-sm py-3 px-4 flex items-center justify-between">
<div class="flex items-center space-x-2">
<div class="bg-blue-600 text-white p-2 rounded-lg">
<i class="fas fa-chalkboard text-xl"></i>
</div>
<h1 class="text-xl font-bold text-gray-800">CollabBoard</h1>
</div>
<div class="flex items-center space-x-4">
<button id="presentation-btn" class="flex items-center space-x-1 bg-blue-50 hover:bg-blue-100 text-blue-700 px-3 py-1.5 rounded-lg text-sm font-medium">
<i class="fas fa-expand"></i>
<span>Presentation</span>
</button>
<button id="share-btn" class="flex items-center space-x-1 bg-green-50 hover:bg-green-100 text-green-700 px-3 py-1.5 rounded-lg text-sm font-medium">
<i class="fas fa-share-alt"></i>
<span>Share</span>
</button>
<div class="relative">
<button id="user-menu-btn" class="flex items-center space-x-1 bg-gray-100 hover:bg-gray-200 px-3 py-1.5 rounded-lg text-sm font-medium">
<i class="fas fa-user-circle text-gray-700"></i>
<span>User</span>
</button>
<div id="user-menu" class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50">
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Account Settings</a>
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Sign Out</a>
</div>
</div>
</div>
</header>
<div class="flex flex-1 overflow-hidden">
<!-- Sidebar -->
<div id="sidebar" class="w-64 bg-white border-r border-gray-200 flex flex-col">
<div class="p-4 border-b border-gray-200">
<button id="new-board-btn" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg font-medium flex items-center justify-center space-x-2">
<i class="fas fa-plus"></i>
<span>New Board</span>
</button>
</div>
<div class="p-4 border-b border-gray-200">
<div class="flex items-center justify-between mb-2">
<h3 class="font-medium text-gray-700">Your Boards</h3>
</div>
<div class="space-y-2" id="boards-list">
<!-- Boards will be added here dynamically -->
</div>
</div>
<div class="p-4 border-b border-gray-200">
<h3 class="font-medium text-gray-700 mb-2">Tags</h3>
<div class="space-y-1">
<div class="flex items-center space-x-2 p-1.5 rounded hover:bg-gray-100 cursor-pointer">
<div class="w-3 h-3 rounded-full bg-red-500"></div>
<span class="text-sm">Urgent</span>
</div>
<div class="flex items-center space-x-2 p-1.5 rounded hover:bg-gray-100 cursor-pointer">
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
<span class="text-sm">Ideas</span>
</div>
<div class="flex items-center space-x-2 p-1.5 rounded hover:bg-gray-100 cursor-pointer">
<div class="w-3 h-3 rounded-full bg-green-500"></div>
<span class="text-sm">Completed</span>
</div>
</div>
</div>
<div class="p-4">
<h3 class="font-medium text-gray-700 mb-2">Collaborators</h3>
<div class="space-y-2">
<div class="flex items-center space-x-2">
<div class="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-600">
<i class="fas fa-user"></i>
</div>
<span class="text-sm">You</span>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Toolbar -->
<div class="bg-white border-b border-gray-200 p-2 flex items-center justify-between">
<div class="flex items-center space-x-2 overflow-x-auto py-1">
<!-- Undo/Redo -->
<div class="flex items-center space-x-1 border-r border-gray-200 pr-2">
<button id="undo-btn" class="p-2 border rounded hover:bg-gray-100" title="Undo (Ctrl+Z)">
<i class="fas fa-undo"></i>
</button>
<button id="redo-btn" class="p-2 border rounded hover:bg-gray-100" title="Redo (Ctrl+Y)">
<i class="fas fa-redo"></i>
</button>
</div>
<!-- Drawing Tools -->
<div class="flex items-center space-x-1 border-r border-gray-200 pr-2">
<button id="select-tool" class="tool-btn p-2 border rounded hover:bg-gray-100 active" title="Select (V)">
<i class="fas fa-mouse-pointer"></i>
</button>
<button id="pen-tool" class="tool-btn p-2 border rounded hover:bg-gray-100" title="Pen (P)">
<i class="fas fa-pen"></i>
</button>
<button id="highlighter-tool" class="tool-btn p-2 border rounded hover:bg-gray-100" title="Highlighter (H)">
<i class="fas fa-highlighter"></i>
</button>
<button id="eraser-tool" class="tool-btn p-2 border rounded hover:bg-gray-100" title="Eraser (E)">
<i class="fas fa-eraser"></i>
</button>
<button id="text-tool" class="tool-btn p-2 border rounded hover:bg-gray-100" title="Text (T)">
<i class="fas fa-font"></i>
</button>
<button id="shape-tool" class="tool-btn p-2 border rounded hover:bg-gray-100" title="Shapes (S)">
<i class="fas fa-shapes"></i>
</button>
<button id="sticky-tool" class="tool-btn p-2 border rounded hover:bg-gray-100" title="Sticky Note (N)">
<i class="fas fa-sticky-note"></i>
</button>
<button id="laser-tool" class="tool-btn p-2 border rounded hover:bg-gray-100" title="Laser Pointer (L)">
<i class="fas fa-location-arrow"></i>
</button>
</div>
<!-- Brush Size -->
<div class="flex items-center space-x-1 border-r border-gray-200 pr-2">
<span class="text-sm text-gray-600">Size:</span>
<input id="brush-size" type="range" min="1" max="30" value="3" class="w-20">
<span id="brush-size-value" class="text-xs text-gray-600 w-6 text-center">3</span>
</div>
<!-- Colors -->
<div class="flex items-center space-x-1 border-r border-gray-200 pr-2">
<div class="color-option bg-black selected" data-color="#000000"></div>
<div class="color-option bg-red-500" data-color="#ef4444"></div>
<div class="color-option bg-yellow-500" data-color="#eab308"></div>
<div class="color-option bg-green-500" data-color="#22c55e"></div>
<div class="color-option bg-blue-500" data-color="#3b82f6"></div>
<div class="color-option bg-purple-500" data-color="#a855f7"></div>
<div class="color-option bg-pink-500" data-color="#ec4899"></div>
<div class="color-option bg-white border border-gray-300" data-color="#ffffff"></div>
</div>
<!-- Custom Color -->
<div class="flex items-center space-x-1">
<input type="color" id="custom-color" value="#000000" class="w-6 h-6 border border-gray-300 rounded cursor-pointer">
<span class="text-sm text-gray-600">Custom</span>
</div>
</div>
<div class="flex items-center space-x-2">
<button id="clear-btn" class="p-2 border rounded hover:bg-gray-100 text-red-500" title="Clear Board">
<i class="fas fa-trash-alt"></i>
</button>
<button id="save-btn" class="p-2 border rounded hover:bg-gray-100 text-green-500" title="Save Board">
<i class="fas fa-save"></i>
</button>
</div>
</div>
<!-- Whiteboard Canvas -->
<div class="flex-1 overflow-auto relative">
<canvas id="whiteboard" class="absolute top-0 left-0"></canvas>
<div id="selection-rect" class="selection-rect hidden"></div>
<!-- Resize handles will be added here dynamically -->
</div>
<!-- Zoom Controls -->
<div class="bg-white border-t border-gray-200 p-2 flex items-center justify-between">
<div class="flex items-center space-x-2">
<button id="zoom-out" class="p-1.5 border rounded hover:bg-gray-100">
<i class="fas fa-search-minus"></i>
</button>
<span id="zoom-level" class="text-sm font-medium">100%</span>
<button id="zoom-in" class="p-1.5 border rounded hover:bg-gray-100">
<i class="fas fa-search-plus"></i>
</button>
<button id="zoom-fit" class="p-1.5 border rounded hover:bg-gray-100 text-sm ml-2">
Fit to Screen
</button>
</div>
<div class="text-sm text-gray-500">
<span id="cursor-position">0, 0</span>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Canvas setup
const canvas = document.getElementById('whiteboard');
const ctx = canvas.getContext('2d');
const selectionRect = document.getElementById('selection-rect');
let isDrawing = false;
let isSelecting = false;
let isMoving = false;
let isResizing = false;
let currentTool = 'select';
let currentColor = '#000000';
let brushSize = 3;
let zoomLevel = 1;
let offsetX = 0;
let offsetY = 0;
let startX, startY;
let actionHistory = [];
let historyIndex = -1;
let boards = [];
let currentBoardIndex = -1;
let selectedActions = [];
let selectionStartX, selectionStartY;
let selectionEndX, selectionEndY;
let moveOffsetX = 0, moveOffsetY = 0;
let resizeHandle = null;
let selectionBounds = null;
// Initialize canvas size
function initCanvas() {
const container = canvas.parentElement;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
redrawCanvas();
}
// Redraw everything on the canvas
function redrawCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw grid
ctx.fillStyle = '#e5e7eb';
const gridSize = 20 * zoomLevel;
for (let x = 0; x < canvas.width; x += gridSize) {
ctx.fillRect(x - offsetX % gridSize, 0, 1, canvas.height);
}
for (let y = 0; y < canvas.height; y += gridSize) {
ctx.fillRect(0, y - offsetY % gridSize, canvas.width, 1);
}
// Draw all actions
if (currentBoardIndex >= 0 && boards[currentBoardIndex]) {
const board = boards[currentBoardIndex];
for (let i = 0; i <= historyIndex; i++) {
drawAction(board.actions[i]);
}
}
// Update selection bounds and handles
updateSelectionHandles();
}
// Draw a single action
function drawAction(action) {
if (!action) return;
ctx.strokeStyle = action.color || currentColor;
ctx.fillStyle = action.color || currentColor;
ctx.lineWidth = (action.size || brushSize) * zoomLevel;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
switch (action.type) {
case 'freehand':
ctx.beginPath();
ctx.moveTo(
action.points[0].x * zoomLevel + offsetX,
action.points[0].y * zoomLevel + offsetY
);
for (let i = 1; i < action.points.length; i++) {
ctx.lineTo(
action.points[i].x * zoomLevel + offsetX,
action.points[i].y * zoomLevel + offsetY
);
}
ctx.stroke();
break;
case 'erase':
ctx.globalCompositeOperation = 'destination-out';
ctx.beginPath();
ctx.moveTo(
action.points[0].x * zoomLevel + offsetX,
action.points[0].y * zoomLevel + offsetY
);
for (let i = 1; i < action.points.length; i++) {
ctx.lineTo(
action.points[i].x * zoomLevel + offsetX,
action.points[i].y * zoomLevel + offsetY
);
}
ctx.stroke();
ctx.globalCompositeOperation = 'source-over';
break;
}
}
// Get bounding box of an action
function getActionBounds(action) {
if (!action.points || action.points.length === 0) {
return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
}
let minX = action.points[0].x;
let minY = action.points[0].y;
let maxX = action.points[0].x;
let maxY = action.points[0].y;
for (let i = 1; i < action.points.length; i++) {
minX = Math.min(minX, action.points[i].x);
minY = Math.min(minY, action.points[i].y);
maxX = Math.max(maxX, action.points[i].x);
maxY = Math.max(maxY, action.points[i].y);
}
// Add some padding for the brush size
const padding = (action.size || brushSize) / 2;
return {
minX: minX - padding,
minY: minY - padding,
maxX: maxX + padding,
maxY: maxY + padding
};
}
// Get combined bounds of all selected actions
function getSelectionBounds() {
if (selectedActions.length === 0) return null;
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
selectedActions.forEach(action => {
const bounds = getActionBounds(action);
minX = Math.min(minX, bounds.minX);
minY = Math.min(minY, bounds.minY);
maxX = Math.max(maxX, bounds.maxX);
maxY = Math.max(maxY, bounds.maxY);
});
return { minX, minY, maxX, maxY };
}
// Check if a point is inside an action's bounds
function isPointInAction(x, y, action) {
const bounds = getActionBounds(action);
return x >= bounds.minX && x <= bounds.maxX && y >= bounds.minY && y <= bounds.maxY;
}
// Check if a point is inside the selection rectangle
function isPointInSelection(x, y) {
if (!isSelecting) return false;
const minX = Math.min(selectionStartX, selectionEndX);
const maxX = Math.max(selectionStartX, selectionEndX);
const minY = Math.min(selectionStartY, selectionEndY);
const maxY = Math.max(selectionStartY, selectionEndY);
return x >= minX && x <= maxX && y >= minY && y <= maxY;
}
// Select actions within a rectangle
function selectActionsInRect() {
if (!isSelecting) return;
const minX = Math.min(selectionStartX, selectionEndX);
const maxX = Math.max(selectionStartX, selectionEndX);
const minY = Math.min(selectionStartY, selectionEndY);
const maxY = Math.max(selectionStartY, selectionEndY);
selectedActions = [];
if (currentBoardIndex >= 0 && boards[currentBoardIndex]) {
const board = boards[currentBoardIndex];
for (let i = 0; i <= historyIndex; i++) {
const action = board.actions[i];
if (action && (action.type === 'freehand' || action.type === 'erase')) {
const bounds = getActionBounds(action);
// Check if action bounds intersect with selection rectangle
if (bounds.maxX >= minX && bounds.minX <= maxX &&
bounds.maxY >= minY && bounds.minY <= maxY) {
selectedActions.push(action);
}
}
}
}
updateSelectionHandles();
redrawCanvas();
}
// Move selected actions
function moveSelectedActions(dx, dy) {
if (selectedActions.length === 0) return;
selectedActions.forEach(action => {
action.points.forEach(point => {
point.x += dx / zoomLevel;
point.y += dy / zoomLevel;
});
});
updateSelectionHandles();
redrawCanvas();
}
// Scale selected actions
function scaleSelectedActions(scaleX, scaleY, originX, originY) {
if (selectedActions.length === 0) return;
selectedActions.forEach(action => {
action.points.forEach(point => {
// Calculate distance from origin
const dx = point.x - originX;
const dy = point.y - originY;
// Apply scaling
point.x = originX + dx * scaleX;
point.y = originY + dy * scaleY;
});
// Also scale the brush size
if (action.size) {
action.size = action.size * ((scaleX + scaleY) / 2);
}
});
updateSelectionHandles();
redrawCanvas();
}
// Create resize handles for the selection
function updateSelectionHandles() {
// Remove existing handles
document.querySelectorAll('.resize-handle').forEach(handle => handle.remove());
if (selectedActions.length === 0) {
selectionRect.classList.add('hidden');
return;
}
// Get combined bounds of all selected actions
selectionBounds = getSelectionBounds();
if (!selectionBounds) return;
// Position the selection rectangle
const left = selectionBounds.minX * zoomLevel + offsetX;
const top = selectionBounds.minY * zoomLevel + offsetY;
const width = (selectionBounds.maxX - selectionBounds.minX) * zoomLevel;
const height = (selectionBounds.maxY - selectionBounds.minY) * zoomLevel;
selectionRect.style.left = `${left}px`;
selectionRect.style.top = `${top}px`;
selectionRect.style.width = `${width}px`;
selectionRect.style.height = `${height}px`;
selectionRect.classList.remove('hidden');
// Create resize handles
const handles = [
{ class: 'nw', x: left, y: top },
{ class: 'ne', x: left + width, y: top },
{ class: 'sw', x: left, y: top + height },
{ class: 'se', x: left + width, y: top + height },
{ class: 'n', x: left + width/2, y: top },
{ class: 's', x: left + width/2, y: top + height },
{ class: 'e', x: left + width, y: top + height/2 },
{ class: 'w', x: left, y: top + height/2 }
];
handles.forEach(handle => {
const handleElement = document.createElement('div');
handleElement.className = `resize-handle ${handle.class}`;
handleElement.style.left = `${handle.x}px`;
handleElement.style.top = `${handle.y}px`;
handleElement.dataset.handle = handle.class;
handleElement.addEventListener('mousedown', (e) => {
e.stopPropagation();
isResizing = true;
resizeHandle = handle.class;
startX = e.clientX;
startY = e.clientY;
});
canvas.parentElement.appendChild(handleElement);
});
}
// Handle resizing of selected elements
function handleResize(e) {
if (!isResizing || !resizeHandle || !selectionBounds) return;
const rect = canvas.getBoundingClientRect();
const mouseX = (e.clientX - rect.left - offsetX) / zoomLevel;
const mouseY = (e.clientY - rect.top - offsetY) / zoomLevel;
const originX = selectionBounds.minX + (selectionBounds.maxX - selectionBounds.minX) / 2;
const originY = selectionBounds.minY + (selectionBounds.maxY - selectionBounds.minY) / 2;
let scaleX = 1, scaleY = 1;
// Calculate scale factors based on which handle is being dragged
switch (resizeHandle) {
case 'nw':
scaleX = (selectionBounds.maxX - mouseX) / (selectionBounds.maxX - selectionBounds.minX);
scaleY = (selectionBounds.maxY - mouseY) / (selectionBounds.maxY - selectionBounds.minY);
break;
case 'ne':
scaleX = (mouseX - selectionBounds.minX) / (selectionBounds.maxX - selectionBounds.minX);
scaleY = (selectionBounds.maxY - mouseY) / (selectionBounds.maxY - selectionBounds.minY);
break;
case 'sw':
scaleX = (selectionBounds.maxX - mouseX) / (selectionBounds.maxX - selectionBounds.minX);
scaleY = (mouseY - selectionBounds.minY) / (selectionBounds.maxY - selectionBounds.minY);
break;
case 'se':
scaleX = (mouseX - selectionBounds.minX) / (selectionBounds.maxX - selectionBounds.minX);
scaleY = (mouseY - selectionBounds.minY) / (selectionBounds.maxY - selectionBounds.minY);
break;
case 'n':
scaleY = (selectionBounds.maxY - mouseY) / (selectionBounds.maxY - selectionBounds.minY);
break;
case 's':
scaleY = (mouseY - selectionBounds.minY) / (selectionBounds.maxY - selectionBounds.minY);
break;
case 'e':
scaleX = (mouseX - selectionBounds.minX) / (selectionBounds.maxX - selectionBounds.minX);
break;
case 'w':
scaleX = (selectionBounds.maxX - mouseX) / (selectionBounds.maxX - selectionBounds.minX);
break;
}
// Apply minimum scale to prevent inversion
scaleX = Math.max(0.1, scaleX);
scaleY = Math.max(0.1, scaleY);
scaleSelectedActions(scaleX, scaleY, originX, originY);
}
// Save current state to history
function saveToHistory(action) {
if (currentBoardIndex >= 0) {
// If we're not at the end of history, remove future actions
if (historyIndex < boards[currentBoardIndex].actions.length - 1) {
boards[currentBoardIndex].actions = boards[currentBoardIndex].actions.slice(0, historyIndex + 1);
}
boards[currentBoardIndex].actions.push(action);
historyIndex++;
updateUndoRedoButtons();
}
}
// Undo last action
function undo() {
if (historyIndex >= 0) {
historyIndex--;
selectedActions = [];
updateSelectionHandles();
redrawCanvas();
updateUndoRedoButtons();
}
}
// Redo last undone action
function redo() {
if (currentBoardIndex >= 0 && historyIndex < boards[currentBoardIndex].actions.length - 1) {
historyIndex++;
drawAction(boards[currentBoardIndex].actions[historyIndex]);
updateUndoRedoButtons();
}
}
// Update undo/redo button states
function updateUndoRedoButtons() {
document.getElementById('undo-btn').disabled = historyIndex < 0;
document.getElementById('redo-btn').disabled =
currentBoardIndex < 0 || historyIndex >= boards[currentBoardIndex].actions.length - 1;
}
// Create a new board
function createNewBoard() {
const newBoard = {
id: Date.now(),
title: `Board ${boards.length + 1}`,
actions: [],
createdAt: new Date()
};
boards.push(newBoard);
currentBoardIndex = boards.length - 1;
historyIndex = -1;
selectedActions = [];
// Add to sidebar
addBoardToSidebar(newBoard);
// Clear canvas
redrawCanvas();
updateUndoRedoButtons();
}
// Add board to sidebar
function addBoardToSidebar(board) {
const boardElement = document.createElement('div');
boardElement.className = 'board-thumbnail bg-white p-2 rounded-lg border border-gray-200 cursor-pointer';
boardElement.innerHTML = `
<div class="bg-gray-100 h-24 rounded-md mb-2 flex items-center justify-center">
<i class="fas fa-chalkboard text-gray-400 text-2xl"></i>
</div>
<h4 class="font-medium text-sm truncate">${board.title}</h4>
<p class="text-xs text-gray-500">Just now</p>
`;
boardElement.addEventListener('click', () => {
// Switch to this board
const index = boards.findIndex(b => b.id === board.id);
if (index >= 0) {
currentBoardIndex = index;
historyIndex = boards[currentBoardIndex].actions.length - 1;
selectedActions = [];
redrawCanvas();
updateUndoRedoButtons();
}
});
document.getElementById('boards-list').prepend(boardElement);
}
// Event listeners for drawing
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseout', stopDrawing);
// Touch events for mobile
canvas.addEventListener('touchstart', handleTouchStart);
canvas.addEventListener('touchmove', handleTouchMove);
canvas.addEventListener('touchend', handleTouchEnd);
function startDrawing(e) {
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left - offsetX) / zoomLevel;
const y = (e.clientY - rect.top - offsetY) / zoomLevel;
if (currentTool === 'select') {
// Check if clicking on a selected action
let clickedOnSelection = false;
for (const action of selectedActions) {
if (isPointInAction(x, y, action)) {
clickedOnSelection = true;
break;
}
}
if (clickedOnSelection) {
isMoving = true;
moveOffsetX = 0;
moveOffsetY = 0;
} else {
isSelecting = true;
selectionStartX = x;
selectionStartY = y;
selectionEndX = x;
selectionEndY = y;
selectedActions = [];
// Show selection rectangle
selectionRect.style.left = `${x * zoomLevel + offsetX}px`;
selectionRect.style.top = `${y * zoomLevel + offsetY}px`;
selectionRect.style.width = '0';
selectionRect.style.height = '0';
selectionRect.classList.remove('hidden');
}
} else if (currentTool === 'pen' || currentTool === 'highlighter' || currentTool === 'eraser') {
isDrawing = true;
startX = x;
startY = y;
const action = {
type: currentTool === 'eraser' ? 'erase' : 'freehand',
color: currentTool === 'highlighter' ? currentColor + '80' : currentColor,
size: brushSize,
points: [{ x: startX, y: startY }]
};
if (currentBoardIndex >= 0) {
boards[currentBoardIndex].actions.push(action);
historyIndex = boards[currentBoardIndex].actions.length - 1;
}
}
draw(e);
}
function draw(e) {
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left - offsetX) / zoomLevel;
const y = (e.clientY - rect.top - offsetY) / zoomLevel;
// Update cursor position display
document.getElementById('cursor-position').textContent =
`${Math.round(x)}, ${Math.round(y)}`;
if (currentTool === 'select') {
if (isSelecting) {
selectionEndX = x;
selectionEndY = y;
// Update selection rectangle
const left = Math.min(selectionStartX, selectionEndX) * zoomLevel + offsetX;
const top = Math.min(selectionStartY, selectionEndY) * zoomLevel + offsetY;
const width = Math.abs(selectionEndX - selectionStartX) * zoomLevel;
const height = Math.abs(selectionEndY - selectionStartY) * zoomLevel;
selectionRect.style.left = `${left}px`;
selectionRect.style.top = `${top}px`;
selectionRect.style.width = `${width}px`;
selectionRect.style.height = `${height}px`;
// Select actions in real-time
selectActionsInRect();
} else if (isMoving) {
const dx = (x - selectionStartX) * zoomLevel - moveOffsetX;
const dy = (y - selectionStartY) * zoomLevel - moveOffsetY;
moveOffsetX += dx;
moveOffsetY += dy;
moveSelectedActions(dx, dy);
} else if (isResizing) {
handleResize(e);
}
} else if ((currentTool === 'pen' || currentTool === 'highlighter' || currentTool === 'eraser') && isDrawing) {
if (currentBoardIndex >= 0 && boards[currentBoardIndex].actions.length > 0) {
const lastAction = boards[currentBoardIndex].actions[historyIndex];
if (lastAction && (lastAction.type === 'freehand' || lastAction.type === 'erase')) {
lastAction.points.push({ x, y });
redrawCanvas();
}
}
}
}
function stopDrawing() {
if (currentTool === 'select') {
if (isSelecting) {
isSelecting = false;
selectionRect.classList.add('hidden');
selectActionsInRect();
} else if (isMoving) {
isMoving = false;
} else if (isResizing) {
isResizing = false;
resizeHandle = null;
}
} else {
isDrawing = false;
}
}
// Touch event handlers
function handleTouchStart(e) {
e.preventDefault();
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousedown', {
clientX: touch.clientX,
clientY: touch.clientY
});
startDrawing(mouseEvent);
}
function handleTouchMove(e) {
e.preventDefault();
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousemove', {
clientX: touch.clientX,
clientY: touch.clientY
});
draw(mouseEvent);
}
function handleTouchEnd(e) {
e.preventDefault();
const mouseEvent = new MouseEvent('mouseup', {});
stopDrawing(mouseEvent);
}
// Tool selection
document.querySelectorAll('.tool-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
switch (this.id) {
case 'select-tool': currentTool = 'select'; break;
case 'pen-tool': currentTool = 'pen'; break;
case 'highlighter-tool': currentTool = 'highlighter'; break;
case 'eraser-tool': currentTool = 'eraser'; break;
case 'text-tool': currentTool = 'text'; break;
case 'shape-tool': currentTool = 'shape'; break;
case 'sticky-tool': currentTool = 'sticky'; break;
case 'laser-tool': currentTool = 'laser'; break;
}
// Change cursor based on tool
if (currentTool === 'pen' || currentTool === 'highlighter') {
canvas.style.cursor = 'crosshair';
} else if (currentTool === 'eraser') {
canvas.style.cursor = 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'16\' height=\'16\' viewBox=\'0 0 16 16\'><circle cx=\'8\' cy=\'8\' r=\'8\' fill=\'black\'/></svg>") 8 8, auto';
} else if (currentTool === 'select') {
canvas.style.cursor = 'default';
} else {
canvas.style.cursor = 'default';
}
// Clear selection when switching tools
if (currentTool !== 'select') {
selectedActions = [];
updateSelectionHandles();
redrawCanvas();
}
});
});
// Color selection
document.querySelectorAll('.color-option').forEach(option => {
option.addEventListener('click', function() {
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected'));
this.classList.add('selected');
currentColor = this.dataset.color;
document.getElementById('custom-color').value = currentColor;
});
});
// Custom color picker
document.getElementById('custom-color').addEventListener('input', function() {
currentColor = this.value;
// Find and select the matching color option if it exists
let found = false;
document.querySelectorAll('.color-option').forEach(option => {
if (option.dataset.color.toLowerCase() === currentColor.toLowerCase()) {
option.classList.add('selected');
found = true;
} else {
option.classList.remove('selected');
}
});
if (!found) {
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected'));
}
});
// Brush size
document.getElementById('brush-size').addEventListener('input', function() {
brushSize = this.value;
document.getElementById('brush-size-value').textContent = brushSize;
});
// Zoom controls
document.getElementById('zoom-in').addEventListener('click', function() {
zoomLevel = Math.min(zoomLevel * 1.2, 5);
updateZoomDisplay();
redrawCanvas();
});
document.getElementById('zoom-out').addEventListener('click', function() {
zoomLevel = Math.max(zoomLevel / 1.2, 0.2);
updateZoomDisplay();
redrawCanvas();
});
document.getElementById('zoom-fit').addEventListener('click', function() {
zoomLevel = 1;
offsetX = 0;
offsetY = 0;
updateZoomDisplay();
redrawCanvas();
});
function updateZoomDisplay() {
document.getElementById('zoom-level').textContent = `${Math.round(zoomLevel * 100)}%`;
}
// Undo/redo buttons
document.getElementById('undo-btn').addEventListener('click', undo);
document.getElementById('redo-btn').addEventListener('click', redo);
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
// Ctrl+Z for undo
if (e.ctrlKey && e.key === 'z') {
e.preventDefault();
undo();
}
// Ctrl+Y for redo
else if (e.ctrlKey && e.key === 'y') {
e.preventDefault();
redo();
}
// Escape to clear selection
else if (e.key === 'Escape') {
selectedActions = [];
updateSelectionHandles();
redrawCanvas();
}
// Delete to remove selected actions
else if (e.key === 'Delete' && selectedActions.length > 0) {
if (currentBoardIndex >= 0) {
const board = boards[currentBoardIndex];
board.actions = board.actions.filter(action => !selectedActions.includes(action));
historyIndex = board.actions.length - 1;
selectedActions = [];
updateSelectionHandles();
redrawCanvas();
updateUndoRedoButtons();
}
}
});
// New board button
document.getElementById('new-board-btn').addEventListener('click', createNewBoard);
// Clear button
document.getElementById('clear-btn').addEventListener('click', function() {
if (confirm('Are you sure you want to clear the board?')) {
if (currentBoardIndex >= 0) {
boards[currentBoardIndex].actions = [];
historyIndex = -1;
selectedActions = [];
updateSelectionHandles();
redrawCanvas();
updateUndoRedoButtons();
}
}
});
// Save button
document.getElementById('save-btn').addEventListener('click', function() {
alert('Board saved!');
// In a real app, this would save to localStorage or a server
});
// Initialize
initCanvas();
window.addEventListener('resize', initCanvas);
// Create first board
createNewBoard();
});
</script>
</html>