botui/ui/suite/canvas/canvas.html

993 lines
25 KiB
HTML
Raw Normal View History

<!-- =============================================================================
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.