993 lines
25 KiB
HTML
993 lines
25 KiB
HTML
|
|
<!-- =============================================================================
|
|||
|
|
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.
|