botui/ui/suite/canvas/canvas.html
Rodrigo Rodriguez (Pragmatismo) e3b5929b99 fix(slides): remove duplicate cacheElements/bindEvents functions causing null error
The duplicate functions at lines 455-486 were redefining cacheElements and
bindEvents with wrong element IDs (kebab-case vs camelCase in HTML).
This caused 'Cannot read properties of null' error on slides app init.
2026-01-12 14:05:06 -03:00

1448 lines
38 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- =============================================================================
CANVAS APP - Collaborative Whiteboard & Drawing
Respects Theme Manager - No hardcoded theme
============================================================================= -->
<div class="canvas-app">
<!-- Toolbar -->
<aside class="canvas-toolbar">
<div class="toolbar-section">
<button
class="tool-btn active"
data-tool="select"
onclick="selectTool('select')"
title="Select (V)"
>
<span></span>
</button>
<button
class="tool-btn"
data-tool="pan"
onclick="selectTool('pan')"
title="Pan (H)"
>
<span></span>
</button>
</div>
<div class="toolbar-section">
<button
class="tool-btn"
data-tool="pencil"
onclick="selectTool('pencil')"
title="Pencil (P)"
>
<span>✏️</span>
</button>
<button
class="tool-btn"
data-tool="brush"
onclick="selectTool('brush')"
title="Brush (B)"
>
<span>🖌️</span>
</button>
<button
class="tool-btn"
data-tool="eraser"
onclick="selectTool('eraser')"
title="Eraser (E)"
>
<span>🧹</span>
</button>
</div>
<div class="toolbar-section">
<button
class="tool-btn"
data-tool="rectangle"
onclick="selectTool('rectangle')"
title="Rectangle (R)"
>
<span></span>
</button>
<button
class="tool-btn"
data-tool="ellipse"
onclick="selectTool('ellipse')"
title="Ellipse (O)"
>
<span></span>
</button>
<button
class="tool-btn"
data-tool="line"
onclick="selectTool('line')"
title="Line (L)"
>
<span></span>
</button>
<button
class="tool-btn"
data-tool="arrow"
onclick="selectTool('arrow')"
title="Arrow (A)"
>
<span></span>
</button>
</div>
<div class="toolbar-section">
<button
class="tool-btn"
data-tool="text"
onclick="selectTool('text')"
title="Text (T)"
>
<span>T</span>
</button>
<button
class="tool-btn"
data-tool="sticky"
onclick="selectTool('sticky')"
title="Sticky Note (S)"
>
<span>📝</span>
</button>
<button
class="tool-btn"
data-tool="image"
onclick="selectTool('image')"
title="Image (I)"
>
<span>🖼️</span>
</button>
</div>
<div class="toolbar-section">
<button
class="tool-btn"
data-tool="connector"
onclick="selectTool('connector')"
title="Connector (C)"
>
<span></span>
</button>
<button
class="tool-btn"
data-tool="frame"
onclick="selectTool('frame')"
title="Frame (F)"
>
<span></span>
</button>
</div>
<div class="toolbar-divider"></div>
<div class="toolbar-section colors">
<input
type="color"
id="stroke-color"
value="#000000"
title="Stroke Color"
onchange="setStrokeColor(this.value)"
/>
<input
type="color"
id="fill-color"
value="#ffffff"
title="Fill Color"
onchange="setFillColor(this.value)"
/>
</div>
<div class="toolbar-section">
<select
id="stroke-width"
onchange="setStrokeWidth(this.value)"
title="Stroke Width"
>
<option value="1">1px</option>
<option value="2" selected>2px</option>
<option value="4">4px</option>
<option value="6">6px</option>
<option value="8">8px</option>
</select>
</div>
</aside>
<!-- Main Canvas Area -->
<main class="canvas-main">
<!-- Top Bar -->
<header class="canvas-header">
<div class="header-left">
<input
type="text"
id="canvas-name"
value="Untitled Canvas"
class="canvas-name-input"
onblur="renameCanvas(this.value)"
/>
<span class="save-status" id="save-status">Saved</span>
</div>
<div class="header-center">
<div class="zoom-controls">
<button
class="zoom-btn"
onclick="zoomOut()"
title="Zoom Out"
>
</button>
<span id="zoom-level">100%</span>
<button class="zoom-btn" onclick="zoomIn()" title="Zoom In">
+
</button>
<button
class="zoom-btn"
onclick="resetZoom()"
title="Reset Zoom"
>
</button>
<button
class="zoom-btn"
onclick="fitToScreen()"
title="Fit to Screen"
>
</button>
</div>
</div>
<div class="header-right">
<button
class="btn-icon"
onclick="undo()"
title="Undo (Ctrl+Z)"
data-i18n-title="canvas-undo"
>
<span></span>
</button>
<button
class="btn-icon"
onclick="redo()"
title="Redo (Ctrl+Y)"
data-i18n-title="canvas-redo"
>
<span></span>
</button>
<div class="separator"></div>
<button
class="btn-icon"
onclick="clearCanvas()"
title="Clear Canvas"
data-i18n-title="canvas-clear"
>
<span>🗑️</span>
</button>
<button
class="btn-secondary"
onclick="exportCanvas()"
data-i18n="canvas-export"
>
<span>📤</span> Export
</button>
<button
class="btn-primary"
onclick="shareCanvas()"
data-i18n="canvas-collaborate"
>
<span>👥</span> Share
</button>
</div>
</header>
<!-- Canvas Container -->
<div class="canvas-container" id="canvas-container">
<canvas id="main-canvas"></canvas>
<div id="selection-box" class="selection-box hidden"></div>
<div id="cursor-indicators" class="cursor-indicators"></div>
</div>
<!-- Mini Map -->
<div class="mini-map" id="mini-map">
<canvas id="mini-map-canvas"></canvas>
<div class="viewport-indicator" id="viewport-indicator"></div>
</div>
</main>
<!-- Properties Panel -->
<aside class="properties-panel collapsed" id="properties-panel">
<button class="panel-toggle" onclick="togglePropertiesPanel()">
<span>⚙️</span>
</button>
<div class="panel-content">
<h3>Properties</h3>
<div id="element-properties" class="properties-group hidden">
<div class="property-row">
<label>Position</label>
<div class="input-group">
<input
type="number"
id="prop-x"
placeholder="X"
onchange="updateElementProperty('x', this.value)"
/>
<input
type="number"
id="prop-y"
placeholder="Y"
onchange="updateElementProperty('y', this.value)"
/>
</div>
</div>
<div class="property-row">
<label>Size</label>
<div class="input-group">
<input
type="number"
id="prop-width"
placeholder="W"
onchange="updateElementProperty('width', this.value)"
/>
<input
type="number"
id="prop-height"
placeholder="H"
onchange="updateElementProperty('height', this.value)"
/>
</div>
</div>
<div class="property-row">
<label>Rotation</label>
<input
type="number"
id="prop-rotation"
placeholder="0°"
onchange="updateElementProperty('rotation', this.value)"
/>
</div>
<div class="property-row">
<label>Opacity</label>
<input
type="range"
id="prop-opacity"
min="0"
max="100"
value="100"
onchange="updateElementProperty('opacity', this.value)"
/>
</div>
<div class="property-row">
<label>Stroke</label>
<input
type="color"
id="prop-stroke"
onchange="updateElementProperty('strokeColor', this.value)"
/>
</div>
<div class="property-row">
<label>Fill</label>
<input
type="color"
id="prop-fill"
onchange="updateElementProperty('fillColor', this.value)"
/>
</div>
</div>
<div id="canvas-properties" class="properties-group">
<h4>Canvas</h4>
<div class="property-row">
<label>Background</label>
<input
type="color"
id="canvas-bg"
value="#ffffff"
onchange="setCanvasBackground(this.value)"
/>
</div>
<div class="property-row">
<label>Grid</label>
<select id="grid-type" onchange="setGridType(this.value)">
<option value="none">None</option>
<option value="dots" selected>Dots</option>
<option value="lines">Lines</option>
<option value="squares">Squares</option>
</select>
</div>
<div class="property-row">
<label>Snap to Grid</label>
<input
type="checkbox"
id="snap-grid"
onchange="toggleSnapToGrid(this.checked)"
/>
</div>
</div>
<div class="properties-group">
<h4>Layers</h4>
<div id="layers-list" class="layers-list">
<div class="layer-item active">
<span class="layer-visibility">👁️</span>
<span class="layer-name">Layer 1</span>
<span class="layer-lock">🔓</span>
</div>
</div>
<button class="btn-sm" onclick="addLayer()">+ Add Layer</button>
</div>
</div>
</aside>
<!-- Collaborators Panel -->
<div class="collaborators-panel" id="collaborators-panel">
<div class="collaborators-list" id="collaborators-list">
<!-- Collaborator avatars will be added here -->
</div>
</div>
<!-- Context Menu -->
<div id="context-menu" class="context-menu hidden">
<button onclick="duplicateSelected()">Duplicate</button>
<button onclick="copySelected()">Copy</button>
<button onclick="pasteClipboard()">Paste</button>
<div class="menu-divider"></div>
<button onclick="bringToFront()">Bring to Front</button>
<button onclick="sendToBack()">Send to Back</button>
<div class="menu-divider"></div>
<button onclick="deleteSelected()">Delete</button>
</div>
<!-- Export Modal -->
<div id="export-modal" class="modal hidden">
<div class="modal-content">
<h2 data-i18n="canvas-export">Export Canvas</h2>
<div class="export-options">
<div class="export-format">
<label>Format</label>
<select id="export-format">
<option value="png">PNG Image</option>
<option value="svg">SVG Vector</option>
<option value="pdf">PDF Document</option>
<option value="json">JSON Data</option>
</select>
</div>
<div class="export-scale">
<label>Scale</label>
<select id="export-scale">
<option value="1">1x</option>
<option value="2" selected>2x</option>
<option value="3">3x</option>
</select>
</div>
<div class="export-bg">
<label>
<input type="checkbox" id="export-bg" checked />
Include Background
</label>
</div>
</div>
<div class="modal-actions">
<button class="btn-secondary" onclick="closeExportModal()">
Cancel
</button>
<button class="btn-primary" onclick="doExport()">Export</button>
</div>
</div>
</div>
</div>
<style>
.canvas-app {
display: flex;
height: 100%;
background: var(--bg-primary);
color: var(--text-primary);
overflow: hidden;
}
.canvas-toolbar {
width: 56px;
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
overflow-y: auto;
}
.toolbar-section {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.toolbar-section.colors {
flex-direction: row;
justify-content: center;
gap: 0.25rem;
}
.toolbar-section.colors input[type="color"] {
width: 20px;
height: 20px;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 0;
cursor: pointer;
}
.tool-btn {
width: 40px;
height: 40px;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 1.125rem;
cursor: pointer;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.tool-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.tool-btn.active {
background: var(--accent-color);
color: white;
}
.toolbar-divider {
height: 1px;
background: var(--border-color);
margin: 0.5rem 0;
}
.toolbar-section select {
width: 100%;
padding: 0.25rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 0.75rem;
}
.canvas-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.canvas-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
gap: 1rem;
}
.header-left {
display: flex;
align-items: center;
gap: 0.75rem;
}
.canvas-name-input {
border: none;
background: transparent;
color: var(--text-primary);
font-size: 1rem;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: 4px;
width: 200px;
}
.canvas-name-input:hover,
.canvas-name-input:focus {
background: var(--bg-primary);
outline: none;
}
.save-status {
font-size: 0.75rem;
color: var(--text-muted);
}
.header-center {
display: flex;
align-items: center;
}
.zoom-controls {
display: flex;
align-items: center;
gap: 0.25rem;
background: var(--bg-primary);
padding: 0.25rem;
border-radius: 6px;
border: 1px solid var(--border-color);
}
.zoom-btn {
width: 28px;
height: 28px;
border: none;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
border-radius: 4px;
font-size: 1rem;
}
.zoom-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
#zoom-level {
font-size: 0.875rem;
min-width: 50px;
text-align: center;
}
.header-right {
display: flex;
align-items: center;
gap: 0.5rem;
}
.separator {
width: 1px;
height: 24px;
background: var(--border-color);
margin: 0 0.5rem;
}
.btn-icon {
width: 32px;
height: 32px;
border: none;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
border-radius: 6px;
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.btn-secondary {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.75rem;
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 0.875rem;
cursor: pointer;
}
.btn-secondary:hover {
background: var(--bg-hover);
}
.btn-primary {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.75rem;
background: var(--accent-color);
color: white;
border: none;
border-radius: 6px;
font-size: 0.875rem;
cursor: pointer;
}
.btn-primary:hover {
background: var(--accent-hover);
}
.btn-sm {
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
width: 100%;
}
.canvas-container {
flex: 1;
position: relative;
overflow: hidden;
background: #f0f0f0;
cursor: crosshair;
}
#main-canvas {
position: absolute;
top: 0;
left: 0;
}
.selection-box {
position: absolute;
border: 2px dashed var(--accent-color);
background: rgba(59, 130, 246, 0.1);
pointer-events: none;
}
.selection-box.hidden {
display: none;
}
.cursor-indicators {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.cursor-indicator {
position: absolute;
display: flex;
align-items: center;
gap: 0.25rem;
pointer-events: none;
}
.cursor-dot {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid white;
}
.cursor-name {
font-size: 0.75rem;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 0.125rem 0.375rem;
border-radius: 4px;
white-space: nowrap;
}
.mini-map {
position: absolute;
bottom: 1rem;
right: 1rem;
width: 200px;
height: 150px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
box-shadow: var(--shadow-md);
}
#mini-map-canvas {
width: 100%;
height: 100%;
}
.viewport-indicator {
position: absolute;
border: 2px solid var(--accent-color);
background: rgba(59, 130, 246, 0.2);
pointer-events: none;
}
.properties-panel {
width: 280px;
background: var(--bg-secondary);
border-left: 1px solid var(--border-color);
transition: width 0.2s;
display: flex;
flex-direction: column;
}
.properties-panel.collapsed {
width: 48px;
}
.properties-panel.collapsed .panel-content {
display: none;
}
.panel-toggle {
width: 100%;
padding: 0.75rem;
border: none;
background: transparent;
cursor: pointer;
font-size: 1.25rem;
}
.panel-content {
flex: 1;
padding: 1rem;
overflow-y: auto;
}
.panel-content h3 {
font-size: 1rem;
font-weight: 600;
margin: 0 0 1rem 0;
}
.properties-group {
margin-bottom: 1.5rem;
}
.properties-group.hidden {
display: none;
}
.properties-group h4 {
font-size: 0.75rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
margin: 0 0 0.75rem 0;
}
.property-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem;
}
.property-row label {
font-size: 0.875rem;
color: var(--text-secondary);
}
.property-row input[type="number"],
.property-row input[type="text"],
.property-row select {
width: 100px;
padding: 0.375rem 0.5rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 0.875rem;
}
.property-row input[type="color"] {
width: 32px;
height: 32px;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 2px;
cursor: pointer;
}
.property-row input[type="range"] {
width: 100px;
}
.property-row input[type="checkbox"] {
width: 16px;
height: 16px;
}
.input-group {
display: flex;
gap: 0.25rem;
}
.input-group input {
width: 48px !important;
}
.layers-list {
margin-bottom: 0.5rem;
}
.layer-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
}
.layer-item:hover {
background: var(--bg-hover);
}
.layer-item.active {
background: var(--accent-color);
color: white;
}
.layer-visibility,
.layer-lock {
cursor: pointer;
}
.layer-name {
flex: 1;
}
.collaborators-panel {
position: absolute;
top: 60px;
right: 1rem;
display: flex;
gap: -0.5rem;
}
.collaborators-list {
display: flex;
}
.collaborator-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid var(--bg-secondary);
margin-left: -8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
color: white;
}
.collaborator-avatar:first-child {
margin-left: 0;
}
.context-menu {
position: fixed;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: var(--shadow-lg);
padding: 0.5rem 0;
min-width: 160px;
z-index: 1000;
}
.context-menu.hidden {
display: none;
}
.context-menu button {
display: block;
width: 100%;
padding: 0.5rem 1rem;
border: none;
background: transparent;
color: var(--text-primary);
font-size: 0.875rem;
text-align: left;
cursor: pointer;
}
.context-menu button:hover {
background: var(--bg-hover);
}
.menu-divider {
height: 1px;
background: var(--border-color);
margin: 0.5rem 0;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal.hidden {
display: none;
}
.modal-content {
background: var(--bg-secondary);
border-radius: 12px;
padding: 1.5rem;
min-width: 400px;
max-width: 90%;
}
.modal-content h2 {
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 1.5rem 0;
}
.export-options {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1.5rem;
}
.export-format,
.export-scale,
.export-bg {
display: flex;
align-items: center;
justify-content: space-between;
}
.export-format select,
.export-scale select {
width: 150px;
padding: 0.5rem;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-primary);
color: var(--text-primary);
}
.export-bg label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
@media (max-width: 1024px) {
.properties-panel {
position: absolute;
right: 0;
top: 0;
height: 100%;
z-index: 50;
}
}
@media (max-width: 768px) {
.canvas-toolbar {
width: 48px;
padding: 0.25rem;
}
.tool-btn {
width: 36px;
height: 36px;
font-size: 1rem;
}
.mini-map {
width: 120px;
height: 90px;
}
.properties-panel.collapsed {
width: 0;
border: none;
}
.canvas-header {
flex-wrap: wrap;
}
.header-center {
order: 3;
width: 100%;
justify-content: center;
margin-top: 0.5rem;
}
}
</style>
<script>
let currentTool = "select";
let isDrawing = false;
let startX, startY;
let elements = [];
let selectedElement = null;
let history = [];
let historyIndex = -1;
let zoom = 1;
let panX = 0,
panY = 0;
let strokeColor = "#000000";
let fillColor = "#ffffff";
let strokeWidth = 2;
let canvas, ctx;
let gridType = "dots";
let snapToGrid = false;
document.addEventListener("DOMContentLoaded", function () {
canvas = document.getElementById("main-canvas");
ctx = canvas.getContext("2d");
resizeCanvas();
drawGrid();
window.addEventListener("resize", resizeCanvas);
canvas.addEventListener("mousedown", handleMouseDown);
canvas.addEventListener("mousemove", handleMouseMove);
canvas.addEventListener("mouseup", handleMouseUp);
canvas.addEventListener("wheel", handleWheel);
canvas.addEventListener("contextmenu", handleContextMenu);
document.addEventListener("keydown", handleKeyDown);
});
function resizeCanvas() {
const container = document.getElementById("canvas-container");
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
redraw();
}
function selectTool(tool) {
currentTool = tool;
document.querySelectorAll(".tool-btn").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.tool === tool);
});
const container = document.getElementById("canvas-container");
switch (tool) {
case "select":
container.style.cursor = "default";
break;
case "pan":
container.style.cursor = "grab";
break;
case "text":
container.style.cursor = "text";
break;
default:
container.style.cursor = "crosshair";
}
}
function handleMouseDown(e) {
const rect = canvas.getBoundingClientRect();
startX = (e.clientX - rect.left - panX) / zoom;
startY = (e.clientY - rect.top - panY) / zoom;
isDrawing = true;
if (currentTool === "select") {
selectedElement = findElementAt(startX, startY);
if (selectedElement) {
showElementProperties(selectedElement);
} else {
hideElementProperties();
}
} else if (currentTool === "pan") {
document.getElementById("canvas-container").style.cursor =
"grabbing";
}
}
function handleMouseMove(e) {
if (!isDrawing) return;
const rect = canvas.getBoundingClientRect();
const currentX = (e.clientX - rect.left - panX) / zoom;
const currentY = (e.clientY - rect.top - panY) / zoom;
if (currentTool === "pan") {
panX += e.movementX;
panY += e.movementY;
redraw();
}
}
function handleMouseUp(e) {
if (!isDrawing) return;
isDrawing = false;
const rect = canvas.getBoundingClientRect();
const endX = (e.clientX - rect.left - panX) / zoom;
const endY = (e.clientY - rect.top - panY) / zoom;
if (currentTool === "pan") {
document.getElementById("canvas-container").style.cursor = "grab";
return;
}
if (currentTool !== "select" && currentTool !== "pan") {
const element = {
id: Date.now(),
type: currentTool,
x: Math.min(startX, endX),
y: Math.min(startY, endY),
width: Math.abs(endX - startX),
height: Math.abs(endY - startY),
stroke: strokeColor,
fill: fillColor,
strokeWidth: strokeWidth,
};
elements.push(element);
saveHistory();
redraw();
}
}
function handleWheel(e) {
e.preventDefault();
const delta = e.deltaY > 0 ? 0.9 : 1.1;
zoom *= delta;
zoom = Math.max(0.1, Math.min(5, zoom));
document.getElementById("zoom-level").textContent =
Math.round(zoom * 100) + "%";
redraw();
}
function handleContextMenu(e) {
e.preventDefault();
}
function handleKeyDown(e) {
if (e.key === "Delete" && selectedElement) {
elements = elements.filter((el) => el !== selectedElement);
selectedElement = null;
hideElementProperties();
saveHistory();
redraw();
}
if (e.ctrlKey && e.key === "z") {
undo();
}
if (e.ctrlKey && e.key === "y") {
redo();
}
}
function drawGrid() {
if (!ctx) return;
ctx.save();
ctx.strokeStyle = "var(--border-color, #e0e0e0)";
ctx.lineWidth = 0.5;
const gridSize = 20;
for (let x = 0; x < canvas.width; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = 0; y < canvas.height; y += gridSize) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
ctx.restore();
}
function redraw() {
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawGrid();
ctx.save();
ctx.translate(panX, panY);
ctx.scale(zoom, zoom);
elements.forEach((el) => drawElement(el));
if (selectedElement) {
drawSelection(selectedElement);
}
ctx.restore();
}
function drawElement(el) {
ctx.strokeStyle = el.stroke;
ctx.fillStyle = el.fill;
ctx.lineWidth = el.strokeWidth;
switch (el.type) {
case "rectangle":
ctx.strokeRect(el.x, el.y, el.width, el.height);
if (el.fill !== "transparent") {
ctx.fillRect(el.x, el.y, el.width, el.height);
}
break;
case "ellipse":
ctx.beginPath();
ctx.ellipse(
el.x + el.width / 2,
el.y + el.height / 2,
el.width / 2,
el.height / 2,
0,
0,
Math.PI * 2,
);
ctx.stroke();
if (el.fill !== "transparent") ctx.fill();
break;
case "line":
ctx.beginPath();
ctx.moveTo(el.x, el.y);
ctx.lineTo(el.x + el.width, el.y + el.height);
ctx.stroke();
break;
}
}
function drawSelection(el) {
ctx.strokeStyle = "#007bff";
ctx.lineWidth = 1;
ctx.setLineDash([5, 5]);
ctx.strokeRect(el.x - 5, el.y - 5, el.width + 10, el.height + 10);
ctx.setLineDash([]);
}
function findElementAt(x, y) {
for (let i = elements.length - 1; i >= 0; i--) {
const el = elements[i];
if (
x >= el.x &&
x <= el.x + el.width &&
y >= el.y &&
y <= el.y + el.height
) {
return el;
}
}
return null;
}
function showElementProperties(el) {
const panel = document.getElementById("properties-panel");
if (panel) panel.style.display = "block";
}
function hideElementProperties() {
const panel = document.getElementById("properties-panel");
if (panel) panel.style.display = "none";
}
function saveHistory() {
history = history.slice(0, historyIndex + 1);
history.push(JSON.stringify(elements));
historyIndex = history.length - 1;
}
function undo() {
if (historyIndex > 0) {
historyIndex--;
elements = JSON.parse(history[historyIndex]);
redraw();
}
}
function redo() {
if (historyIndex < history.length - 1) {
historyIndex++;
elements = JSON.parse(history[historyIndex]);
redraw();
}
}
function setStrokeColor(color) {
strokeColor = color;
if (selectedElement) {
selectedElement.stroke = color;
redraw();
}
}
function setFillColor(color) {
fillColor = color;
if (selectedElement) {
selectedElement.fill = color;
redraw();
}
}
function setStrokeWidth(width) {
strokeWidth = parseInt(width);
if (selectedElement) {
selectedElement.strokeWidth = strokeWidth;
redraw();
}
}
function zoomIn() {
zoom = Math.min(5, zoom * 1.2);
document.getElementById("zoom-level").textContent =
Math.round(zoom * 100) + "%";
redraw();
}
function zoomOut() {
zoom = Math.max(0.1, zoom / 1.2);
document.getElementById("zoom-level").textContent =
Math.round(zoom * 100) + "%";
redraw();
}
function resetZoom() {
zoom = 1;
panX = 0;
panY = 0;
document.getElementById("zoom-level").textContent = "100%";
redraw();
}
function clearCanvas() {
if (confirm("Clear all elements?")) {
elements = [];
selectedElement = null;
saveHistory();
redraw();
}
}
function exportCanvas() {
const link = document.createElement("a");
link.download = "canvas.png";
link.href = canvas.toDataURL();
link.click();
}
function saveCanvas() {
console.log("Canvas saved");
}
function toggleCollaborate() {
console.log("Collaboration toggled");
}
</script>