<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ProDraw Studio - 企业级画图软件</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--primary-light: #a5b4fc;
--secondary: #1e293b;
--bg-dark: #0f172a;
--bg-panel: #1e293b;
--bg-card: #334155;
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
--border: #475569;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
overflow: hidden;
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: var(--secondary);
padding: 8px 20px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border);
z-index: 100;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
}
.logo-icon {
width: 32px;
height: 32px;
background: linear-gradient(135deg, var(--primary), #8b5cf6);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 16px;
}
.logo-text {
font-size: 18px;
font-weight: 700;
background: linear-gradient(135deg, var(--primary-light), #c4b5fd);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header-actions {
display: flex;
gap: 8px;
}
.header-btn {
padding: 6px 14px;
border: 1px solid var(--border);
background: var(--bg-card);
color: var(--text-primary);
border-radius: 6px;
cursor: pointer;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.header-btn:hover {
background: var(--primary);
border-color: var(--primary);
}
.header-btn.primary {
background: var(--primary);
border-color: var(--primary);
}
.header-btn.primary:hover {
background: var(--primary-dark);
}
.main-container {
display: flex;
flex: 1;
overflow: hidden;
}
.toolbar {
width: 56px;
background: var(--secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
gap: 4px;
}
.tool-btn {
width: 40px;
height: 40px;
border: none;
background: transparent;
color: var(--text-secondary);
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
font-size: 18px;
}
.tool-btn:hover {
background: var(--bg-card);
color: var(--text-primary);
}
.tool-btn.active {
background: var(--primary);
color: white;
}
.tool-separator {
width: 30px;
height: 1px;
background: var(--border);
margin: 6px 0;
}
.left-panel {
width: 260px;
background: var(--bg-panel);
border-right: 1px solid var(--border);
overflow-y: auto;
padding: 16px;
}
.panel-section {
margin-bottom: 20px;
}
.panel-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-secondary);
margin-bottom: 10px;
}
.color-presets {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 4px;
margin-bottom: 10px;
}
.color-preset {
width: 24px;
height: 24px;
border-radius: 4px;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s;
}
.color-preset:hover {
transform: scale(1.2);
border-color: white;
}
.color-preset.active {
border-color: white;
box-shadow: 0 0 8px rgba(255,255,255,0.5);
}
.color-input-wrapper {
display: flex;
gap: 8px;
align-items: center;
}
.color-input {
width: 40px;
height: 32px;
border: none;
border-radius: 6px;
cursor: pointer;
background: none;
}
.color-input::-webkit-color-swatch-wrapper {
padding: 0;
}
.color-input::-webkit-color-swatch {
border: 2px solid var(--border);
border-radius: 6px;
}
.color-hex {
flex: 1;
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-primary);
padding: 6px 10px;
border-radius: 6px;
font-size: 12px;
font-family: monospace;
}
.slider-group {
margin-bottom: 12px;
}
.slider-label {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
font-size: 12px;
color: var(--text-secondary);
}
.slider-value {
background: var(--bg-card);
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-family: monospace;
}
input[type="range"] {
width: 100%;
height: 4px;
-webkit-appearance: none;
background: var(--bg-card);
border-radius: 2px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: var(--primary);
border-radius: 50%;
cursor: pointer;
transition: transform 0.2s;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.3);
}
.input-group {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.input-field {
flex: 1;
}
.input-field label {
display: block;
font-size: 11px;
color: var(--text-secondary);
margin-bottom: 4px;
}
.input-field input,
.input-field select {
width: 100%;
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-primary);
padding: 6px 10px;
border-radius: 6px;
font-size: 12px;
}
.input-field input:focus,
.input-field select:focus {
outline: none;
border-color: var(--primary);
}
.canvas-area {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #1a1a2e;
position: relative;
overflow: hidden;
}
.canvas-container {
position: relative;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}
#drawingCanvas {
display: block;
cursor: crosshair;
background: white;
transition: transform 0.2s;
}
.right-panel {
width: 240px;
background: var(--bg-panel);
border-left: 1px solid var(--border);
padding: 16px;
overflow-y: auto;
}
.export-section {
background: var(--bg-card);
border-radius: 8px;
padding: 14px;
margin-bottom: 12px;
}
.export-title {
font-size: 13px;
font-weight: 600;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 6px;
}
.btn {
width: 100%;
padding: 10px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
margin-bottom: 8px;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
}
.btn-secondary {
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid var(--border);
}
.btn-secondary:hover {
border-color: var(--primary);
}
.history-list {
max-height: 200px;
overflow-y: auto;
}
.history-item {
padding: 6px 10px;
font-size: 11px;
color: var(--text-secondary);
border-left: 2px solid transparent;
margin-bottom: 2px;
border-radius: 4px;
cursor: pointer;
}
.history-item:hover {
background: rgba(99,102,241,0.1);
}
.history-item.current {
background: rgba(99, 102, 241, 0.2);
border-left-color: var(--primary);
color: var(--text-primary);
}
.zoom-controls {
position: absolute;
bottom: 16px;
right: 16px;
display: flex;
gap: 4px;
background: var(--bg-panel);
padding: 4px;
border-radius: 8px;
border: 1px solid var(--border);
}
.zoom-btn {
width: 32px;
height: 32px;
border: none;
background: transparent;
color: var(--text-primary);
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.zoom-btn:hover {
background: var(--bg-card);
}
.zoom-level {
padding: 0 10px;
display: flex;
align-items: center;
font-size: 12px;
color: var(--text-secondary);
}
.status-bar {
background: var(--secondary);
padding: 4px 16px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 11px;
color: var(--text-secondary);
border-top: 1px solid var(--border);
}
.status-left,
.status-right {
display: flex;
align-items: center;
gap: 16px;
}
.status-dot {
width: 6px;
height: 6px;
background: var(--success);
border-radius: 50%;
display: inline-block;
margin-right: 6px;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-overlay.active {
display: flex;
}
.modal {
background: var(--bg-panel);
border-radius: 12px;
padding: 24px;
width: 400px;
max-width: 90%;
border: 1px solid var(--border);
}
.modal-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
.modal-actions {
display: flex;
gap: 8px;
margin-top: 20px;
justify-content: flex-end;
}
.modal-btn {
padding: 8px 16px;
border: 1px solid var(--border);
background: var(--bg-card);
color: var(--text-primary);
border-radius: 6px;
cursor: pointer;
font-size: 13px;
}
.modal-btn.primary {
background: var(--primary);
border-color: var(--primary);
}
.toast-container {
position: fixed;
top: 60px;
right: 20px;
z-index: 2000;
}
.toast {
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
animation: slideIn 0.3s ease;
min-width: 250px;
}
.toast.success { border-left: 3px solid var(--success); }
.toast.error { border-left: 3px solid var(--danger); }
.toast.info { border-left: 3px solid var(--primary); }
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.brush-preview {
width: 100%;
height: 60px;
background: var(--bg-card);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
}
.brush-dot {
border-radius: 50%;
background: var(--primary);
transition: all 0.2s;
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
</style>
</head>
<body>
<div class="header">
<div class="logo">
<div class="logo-icon">P</div>
<span class="logo-text">ProDraw Studio</span>
</div>
<div class="header-actions">
<button class="header-btn" onclick="createNewCanvas()">📄 新建</button>
<button class="header-btn" onclick="document.getElementById('uploadInput').click()">📁 上传图片</button>
<input type="file" id="uploadInput" accept="image/*" style="display:none" onchange="uploadImage(event)">
<button class="header-btn" onclick="showExportModal()">💾 导出图片</button>
<button class="header-btn primary" onclick="quickSave()">⬇️ 快速保存</button>
</div>
</div>
<div class="main-container">
<div class="toolbar">
<button class="tool-btn active" data-tool="pen" title="画笔 (B)">✏️</button>
<button class="tool-btn" data-tool="brush" title="毛笔 (P)">🖌️</button>
<button class="tool-btn" data-tool="eraser" title="橡皮擦 (E)">🧹</button>
<div class="tool-separator"></div>
<button class="tool-btn" data-tool="line" title="直线 (L)">📏</button>
<button class="tool-btn" data-tool="rectangle" title="矩形 (R)">⬜</button>
<button class="tool-btn" data-tool="circle" title="圆形 (C)">⭕</button>
<button class="tool-btn" data-tool="triangle" title="三角形">🔺</button>
<div class="tool-separator"></div>
<button class="tool-btn" data-tool="fill" title="填充 (G)">🪣</button>
<button class="tool-btn" data-tool="text" title="文字 (T)">📝</button>
<button class="tool-btn" data-tool="eyedropper" title="取色器 (I)">💉</button>
<div class="tool-separator"></div>
<button class="tool-btn" onclick="undo()" title="撤销 (Ctrl+Z)">↩️</button>
<button class="tool-btn" onclick="redo()" title="重做 (Ctrl+Y)">↪️</button>
<div class="tool-separator"></div>
<button class="tool-btn" onclick="clearCanvas()" title="清空画布">🗑️</button>
</div>
<div class="left-panel">
<div class="panel-section">
<div class="panel-title">🎨 颜色</div>
<div class="color-presets" id="colorPresets"></div>
<div class="color-input-wrapper">
<input type="color" class="color-input" id="colorPicker" value="#000000">
<input type="text" class="color-hex" id="colorHex" value="#000000" maxlength="7">
</div>
</div>
<div class="panel-section">
<div class="panel-title">🖊️ 画笔设置</div>
<div class="brush-preview">
<div class="brush-dot" id="brushPreview"></div>
</div>
<div class="slider-group">
<div class="slider-label">
<span>粗细</span>
<span class="slider-value" id="sizeValue">5px</span>
</div>
<input type="range" id="brushSize" min="1" max="100" value="5">
</div>
<div class="slider-group">
<div class="slider-label">
<span>透明度</span>
<span class="slider-value" id="opacityValue">100%</span>
</div>
<input type="range" id="brushOpacity" min="1" max="100" value="100">
</div>
<div class="slider-group">
<div class="slider-label">
<span>平滑度</span>
<span class="slider-value" id="smoothValue">0</span>
</div>
<input type="range" id="brushSmooth" min="0" max="10" value="0">
</div>
</div>
<div class="panel-section">
<div class="panel-title">📐 画布设置</div>
<div class="input-group">
<div class="input-field">
<label>宽度 (px)</label>
<input type="number" id="canvasWidth" value="1200">
</div>
<div class="input-field">
<label>高度 (px)</label>
<input type="number" id="canvasHeight" value="800">
</div>
</div>
<div class="input-group">
<div class="input-field">
<label>背景颜色</label>
<input type="color" id="bgColor" value="#ffffff" style="width:100%;height:30px;border:none;border-radius:6px;cursor:pointer;">
</div>
</div>
<button class="btn btn-secondary" onclick="resizeCanvas()">应用尺寸</button>
<div style="margin-top:8px;">
<div class="input-field">
<label>预设尺寸</label>
<select id="presetSize" onchange="applyPresetSize()">
<option value="">自定义</option>
<option value="1920x1080">1920×1080 (全高清)</option>
<option value="1280x720">1280×720 (高清)</option>
<option value="800x600">800×600</option>
<option value="1080x1080">1080×1080 (Instagram)</option>
<option value="1080x1920">1080×1920 (手机壁纸)</option>
<option value="2480x3508">A4 (300dpi)</option>
</select>
</div>
</div>
</div>
<div class="panel-section">
<div class="panel-title">⬛ 形状设置</div>
<div class="slider-group">
<div class="slider-label">
<span>边框宽度</span>
<span class="slider-value" id="strokeValue">2px</span>
</div>
<input type="range" id="shapeStroke" min="1" max="20" value="2">
</div>
<label style="display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text-secondary);cursor:pointer;">
<input type="checkbox" id="shapeFill"> 填充形状
</label>
</div>
</div>
<div class="canvas-area" id="canvasArea">
<div class="canvas-container" id="canvasContainer">
<canvas id="drawingCanvas" width="1200" height="800"></canvas>
</div>
<div class="zoom-controls">
<button class="zoom-btn" onclick="zoomOut()">−</button>
<span class="zoom-level" id="zoomLevel">100%</span>
<button class="zoom-btn" onclick="zoomIn()">+</button>
<button class="zoom-btn" onclick="resetZoom()">⟲</button>
</div>
</div>
<div class="right-panel">
<div class="export-section">
<div class="export-title">💾 导出设置</div>
<div class="input-field" style="margin-bottom:10px;">
<label>导出格式</label>
<select id="exportFormat">
<option value="png">PNG (无损)</option>
<option value="jpeg">JPEG (压缩)</option>
<option value="webp">WebP (推荐)</option>
<option value="bmp">BMP</option>
</select>
</div>
<div class="slider-group" id="qualitySlider" style="display:none;">
<div class="slider-label">
<span>压缩质量</span>
<span class="slider-value" id="qualityValue">92%</span>
</div>
<input type="range" id="exportQuality" min="1" max="100" value="92">
</div>
<div class="input-field" style="margin-bottom:10px;">
<label>文件名</label>
<input type="text" id="exportFilename" value="my-drawing">
</div>
<button class="btn btn-primary" onclick="exportImage()">⬇️ 下载图片</button>
</div>
<div class="export-section">
<div class="export-title">⚡ 快捷操作</div>
<button class="btn btn-secondary" onclick="flipHorizontal()">↔️ 水平翻转</button>
<button class="btn btn-secondary" onclick="flipVertical()">↕️ 垂直翻转</button>
<button class="btn btn-secondary" onclick="rotateCanvas()">🔄 旋转90°</button>
<button class="btn btn-secondary" onclick="invertColors()">🌓 反色</button>
<button class="btn btn-secondary" onclick="grayscale()">⬛ 灰度</button>
</div>
<div class="export-section">
<div class="export-title">📜 操作历史</div>
<div class="history-list" id="historyList">
<div class="history-item current">初始画布</div>
</div>
</div>
<div class="export-section">
<div class="export-title">ℹ️ 画布信息</div>
<div style="font-size:11px;color:var(--text-secondary);line-height:1.8;">
<div>尺寸: <span id="infoSize">1200 × 800</span></div>
<div>操作次数: <span id="infoOps">0</span></div>
<div>文件大小(预估): <span id="infoFileSize">-</span></div>
</div>
</div>
</div>
</div>
<div class="status-bar">
<div class="status-left">
<span><span class="status-dot"></span>就绪</span>
<span>工具: <strong id="currentTool">画笔</strong></span>
<span>坐标: <strong id="cursorPos">0, 0</strong></span>
</div>
<div class="status-right">
<span>缩放: <strong id="statusZoom">100%</strong></span>
<span>快捷键: B画笔 E橡皮 L线 R矩形 C圆 Ctrl+Z撤销</span>
</div>
</div>
<div class="modal-overlay" id="exportModal">
<div class="modal">
<div class="modal-title">💾 高级导出</div>
<div class="input-field" style="margin-bottom:12px;">
<label>文件格式</label>
<select id="modalFormat">
<option value="png">PNG - 无损压缩,支持透明</option>
<option value="jpeg">JPEG - 高压缩率,不支持透明</option>
<option value="webp">WebP - 最佳平衡,支持透明</option>
</select>
</div>
<div class="slider-group" id="modalQualityGroup">
<div class="slider-label">
<span>图片质量 / 压缩率</span>
<span class="slider-value" id="modalQualityValue">92%</span>
</div>
<input type="range" id="modalQuality" min="1" max="100" value="92">
</div>
<div class="input-field" style="margin-bottom:12px;">
<label>文件名</label>
<input type="text" id="modalFilename" value="my-drawing">
</div>
<div class="input-field" style="margin-bottom:12px;">
<label>缩放比例</label>
<select id="modalScale">
<option value="1">100% (原始尺寸)</option>
<option value="0.75">75%</option>
<option value="0.5">50%</option>
<option value="0.25">25%</option>
<option value="2">200%</option>
</select>
</div>
<div class="modal-actions">
<button class="modal-btn" onclick="closeExportModal()">取消</button>
<button class="modal-btn primary" onclick="modalExport()">导出下载</button>
</div>
</div>
</div>
<div class="modal-overlay" id="textModal">
<div class="modal">
<div class="modal-title">📝 添加文字</div>
<div class="input-field" style="margin-bottom:12px;">
<label>文字内容</label>
<input type="text" id="textContent" placeholder="输入文字...">
</div>
<div class="input-group">
<div class="input-field">
<label>字体大小</label>
<input type="number" id="textSize" value="24" min="8" max="200">
</div>
<div class="input-field">
<label>字体</label>
<select id="textFont">
<option value="Arial">Arial</option>
<option value="Helvetica">Helvetica</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Courier New">Courier New</option>
<option value="Georgia">Georgia</option>
<option value="Verdana">Verdana</option>
<option value="微软雅黑">微软雅黑</option>
<option value="宋体">宋体</option>
<option value="楷体">楷体</option>
</select>
</div>
</div>
<div class="input-group">
<div class="input-field">
<label>样式</label>
<select id="textStyle">
<option value="normal">正常</option>
<option value="bold">粗体</option>
<option value="italic">斜体</option>
<option value="bold italic">粗斜体</option>
</select>
</div>
<div class="input-field">
<label>颜色</label>
<input type="color" id="textColor" value="#000000" style="width:100%;height:32px;border:none;border-radius:6px;cursor:pointer;">
</div>
</div>
<div class="modal-actions">
<button class="modal-btn" onclick="closeTextModal()">取消</button>
<button class="modal-btn primary" onclick="addText()">添加</button>
</div>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script>
const state = {
tool: 'pen',
color: '#000000',
brushSize: 5,
opacity: 100,
smoothness: 0,
shapeStroke: 2,
shapeFill: false,
isDrawing: false,
lastX: 0,
lastY: 0,
startX: 0,
startY: 0,
zoom: 1,
history: [],
historyIndex: -1,
maxHistory: 50,
bgColor: '#ffffff'
};
const canvas = document.getElementById('drawingCanvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = state.bgColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
function init() {
initColors();
saveState('初始画布');
updateBrushPreview();
setupEventListeners();
}
function initColors() {
const colors = [
'#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#d9d9d9', '#ffffff',
'#980000', '#ff0000', '#ff9900', '#ffff00', '#00ff00', '#00ffff', '#4a86e8', '#0000ff',
'#9900ff', '#ff00ff', '#e6b8af', '#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', '#d0e0e3',
'#c9daf8', '#cfe2f3', '#d9d2e9', '#ead1dc', '#dd7e6b', '#ea9999', '#f9cb9c', '#ffe599',
'#b6d7a8', '#a2c4c9', '#a4c2f4', '#9fc5e8', '#b4a7d6', '#d5a6bd', '#cc4125', '#e06666'
];
const container = document.getElementById('colorPresets');
colors.forEach(color => {
const el = document.createElement('div');
el.className = 'color-preset';
el.style.background = color;
el.onclick = () => setColor(color);
container.appendChild(el);
});
}
function setColor(color) {
state.color = color;
document.getElementById('colorPicker').value = color;
document.getElementById('colorHex').value = color;
updateBrushPreview();
}
function setupEventListeners() {
document.querySelectorAll('.tool-btn[data-tool]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tool-btn[data-tool]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.tool = btn.dataset.tool;
updateStatus();
});
});
document.getElementById('colorPicker').addEventListener('input', (e) => {
setColor(e.target.value);
});
document.getElementById('colorHex').addEventListener('change', (e) => {
let val = e.target.value;
if (!val.startsWith('#')) val = '#' + val;
if (/^#[0-9A-Fa-f]{6}$/.test(val)) setColor(val);
});
document.getElementById('brushSize').addEventListener('input', (e) => {
state.brushSize = parseInt(e.target.value);
document.getElementById('sizeValue').textContent = state.brushSize + 'px';
updateBrushPreview();
});
document.getElementById('brushOpacity').addEventListener('input', (e) => {
state.opacity = parseInt(e.target.value);
document.getElementById('opacityValue').textContent = state.opacity + '%';
});
document.getElementById('brushSmooth').addEventListener('input', (e) => {
state.smoothness = parseInt(e.target.value);
document.getElementById('smoothValue').textContent = state.smoothness;
});
document.getElementById('shapeStroke').addEventListener('input', (e) => {
state.shapeStroke = parseInt(e.target.value);
document.getElementById('strokeValue').textContent = state.shapeStroke + 'px';
});
document.getElementById('shapeFill').addEventListener('change', (e) => {
state.shapeFill = e.target.checked;
});
document.getElementById('exportFormat').addEventListener('change', (e) => {
const format = e.target.value;
document.getElementById('qualitySlider').style.display =
(format === 'jpeg' || format === 'webp') ? 'block' : 'none';
});
document.getElementById('exportQuality').addEventListener('input', (e) => {
document.getElementById('qualityValue').textContent = e.target.value + '%';
});
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseout', stopDrawing);
canvas.addEventListener('touchstart', handleTouch);
canvas.addEventListener('touchmove', handleTouch);
canvas.addEventListener('touchend', stopDrawing);
document.addEventListener('keydown', handleKeyboard);
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = Math.round((e.clientX - rect.left) / state.zoom);
const y = Math.round((e.clientY - rect.top) / state.zoom);
document.getElementById('cursorPos').textContent = `${x}, ${y}`;
});
}
function startDrawing(e) {
state.isDrawing = true;
const pos = getPos(e);
state.lastX = pos.x;
state.lastY = pos.y;
state.startX = pos.x;
state.startY = pos.y;
if (state.tool === 'fill') {
floodFill(Math.round(pos.x), Math.round(pos.y), state.color);
saveState('填充');
state.isDrawing = false;
return;
}
if (state.tool === 'eyedropper') {
pickColor(Math.round(pos.x), Math.round(pos.y));
state.isDrawing = false;
return;
}
if (state.tool === 'text') {
state.textX = pos.x;
state.textY = pos.y;
document.getElementById('textModal').classList.add('active');
state.isDrawing = false;
return;
}
if (['line', 'rectangle', 'circle', 'triangle'].includes(state.tool)) {
state.snapshot = ctx.getImageData(0, 0, canvas.width, canvas.height);
}
if (state.tool === 'pen' || state.tool === 'brush' || state.tool === 'eraser') {
ctx.beginPath();
ctx.moveTo(pos.x, pos.y);
}
}
function draw(e) {
if (!state.isDrawing) return;
const pos = getPos(e);
if (state.tool === 'pen') {
ctx.globalAlpha = state.opacity / 100;
ctx.strokeStyle = state.color;
ctx.lineWidth = state.brushSize;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
} else if (state.tool === 'brush') {
ctx.globalAlpha = state.opacity / 100;
ctx.strokeStyle = state.color;
ctx.lineWidth = state.brushSize * 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.shadowBlur = state.brushSize;
ctx.shadowColor = state.color;
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
ctx.shadowBlur = 0;
} else if (state.tool === 'eraser') {
ctx.globalAlpha = 1;
ctx.strokeStyle = state.bgColor;
ctx.lineWidth = state.brushSize * 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
} else if (['line', 'rectangle', 'circle', 'triangle'].includes(state.tool)) {
if (state.snapshot) ctx.putImageData(state.snapshot, 0, 0);
drawShape(state.tool, state.startX, state.startY, pos.x, pos.y);
}
state.lastX = pos.x;
state.lastY = pos.y;
}
function stopDrawing(e) {
if (!state.isDrawing) return;
state.isDrawing = false;
if (state.tool === 'pen' || state.tool === 'brush') {
ctx.closePath();
saveState('绘制 - ' + state.tool);
} else if (state.tool === 'eraser') {
ctx.closePath();
saveState('擦除');
} else if (['line', 'rectangle', 'circle', 'triangle'].includes(state.tool)) {
if (state.snapshot) ctx.putImageData(state.snapshot, 0, 0);
const pos = e ? getPos(e) : { x: state.lastX, y: state.lastY };
drawShape(state.tool, state.startX, state.startY, pos.x, pos.y);
saveState('形状 - ' + state.tool);
}
ctx.globalAlpha = 1;
state.snapshot = null;
updateInfo();
}
function drawShape(type, x1, y1, x2, y2) {
ctx.globalAlpha = state.opacity / 100;
ctx.strokeStyle = state.color;
ctx.fillStyle = state.color;
ctx.lineWidth = state.shapeStroke;
ctx.lineCap = 'round';
ctx.beginPath();
switch(type) {
case 'line':
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
break;
case 'rectangle':
if (state.shapeFill) ctx.fillRect(x1, y1, x2-x1, y2-y1);
ctx.strokeRect(x1, y1, x2-x1, y2-y1);
break;
case 'circle':
const rx = Math.abs(x2 - x1) / 2;
const ry = Math.abs(y2 - y1) / 2;
const cx = Math.min(x1, x2) + rx;
const cy = Math.min(y1, y2) + ry;
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
if (state.shapeFill) ctx.fill();
ctx.stroke();
break;
case 'triangle':
ctx.moveTo((x1 + x2) / 2, y1);
ctx.lineTo(x1, y2);
ctx.lineTo(x2, y2);
ctx.closePath();
if (state.shapeFill) ctx.fill();
ctx.stroke();
break;
}
}
function getPos(e) {
const rect = canvas.getBoundingClientRect();
let clientX, clientY;
if (e.touches) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}
return {
x: (clientX - rect.left) / state.zoom,
y: (clientY - rect.top) / state.zoom
};
}
function handleTouch(e) {
e.preventDefault();
const touch = e.touches[0];
const mouseEvent = new MouseEvent(e.type === 'touchstart' ? 'mousedown' : 'mousemove', {
clientX: touch.clientX, clientY: touch.clientY
});
canvas.dispatchEvent(mouseEvent);
}
function floodFill(startX, startY, fillColor) {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const width = canvas.width;
const height = canvas.height;
const startIdx = (startY * width + startX) * 4;
const startR = data[startIdx], startG = data[startIdx+1], startB = data[startIdx+2], startA = data[startIdx+3];
const hex = fillColor.replace('#', '');
const fillR = parseInt(hex.substr(0,2),16);
const fillG = parseInt(hex.substr(2,2),16);
const fillB = parseInt(hex.substr(4,2),16);
if (startR === fillR && startG === fillG && startB === fillB) return;
const stack = [[startX, startY]];
const tolerance = 30;
function matchColor(idx) {
return Math.abs(data[idx]-startR)<=tolerance && Math.abs(data[idx+1]-startG)<=tolerance &&
Math.abs(data[idx+2]-startB)<=tolerance && Math.abs(data[idx+3]-startA)<=tolerance;
}
while (stack.length > 0) {
const [x, y] = stack.pop();
const idx = (y * width + x) * 4;
if (x<0||x>=width||y<0||y>=height) continue;
if (!matchColor(idx)) continue;
data[idx]=fillR; data[idx+1]=fillG; data[idx+2]=fillB; data[idx+3]=255;
stack.push([x+1,y],[x-1,y],[x,y+1],[x,y-1]);
}
ctx.putImageData(imageData, 0, 0);
}
function pickColor(x, y) {
const pixel = ctx.getImageData(x, y, 1, 1).data;
const hex = '#' + [pixel[0],pixel[1],pixel[2]].map(v=>v.toString(16).padStart(2,'0')).join('');
setColor(hex);
showToast('已拾取颜色: ' + hex, 'info');
}
function saveState(action) {
if (state.historyIndex < state.history.length - 1) {
state.history = state.history.slice(0, state.historyIndex + 1);
}
state.history.push({ data: canvas.toDataURL(), action, timestamp: Date.now() });
if (state.history.length > state.maxHistory) state.history.shift();
state.historyIndex = state.history.length - 1;
updateHistoryUI();
updateInfo();
}
function undo() {
if (state.historyIndex > 0) {
state.historyIndex--;
restoreState(state.historyIndex);
showToast('撤销: ' + state.history[state.historyIndex+1].action, 'info');
} else {
showToast('没有更多可撤销的操作', 'error');
}
}
function redo() {
if (state.historyIndex < state.history.length - 1) {
state.historyIndex++;
restoreState(state.historyIndex);
showToast('重做: ' + state.history[state.historyIndex].action, 'info');
} else {
showToast('没有更多可重做的操作', 'error');
}
}
function restoreState(index) {
const img = new Image();
img.onload = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
updateHistoryUI();
};
img.src = state.history[index].data;
}
function updateHistoryUI() {
const container = document.getElementById('historyList');
container.innerHTML = '';
state.history.forEach((item, i) => {
const el = document.createElement('div');
el.className = 'history-item' + (i === state.historyIndex ? ' current' : '');
el.textContent = `${i+1}. ${item.action}`;
el.onclick = () => { state.historyIndex = i; restoreState(i); };
container.appendChild(el);
});
container.scrollTop = container.scrollHeight;
document.getElementById('infoOps').textContent = state.history.length - 1;
}
function clearCanvas() {
if (confirm('确定要清空画布吗?')) {
ctx.fillStyle = state.bgColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
saveState('清空画布');
showToast('画布已清空', 'info');
}
}
function resizeCanvas() {
const w = parseInt(document.getElementById('canvasWidth').value);
const h = parseInt(document.getElementById('canvasHeight').value);
if (w<1||h<1||w>4000||h>4000) { showToast('尺寸必须在 1-4000 像素之间', 'error'); return; }
const tempCanvas = document.createElement('canvas');
tempCanvas.width = canvas.width; tempCanvas.height = canvas.height;
tempCanvas.getContext('2d').drawImage(canvas, 0, 0);
canvas.width = w; canvas.height = h;
state.bgColor = document.getElementById('bgColor').value;
ctx.fillStyle = state.bgColor;
ctx.fillRect(0, 0, w, h);
ctx.drawImage(tempCanvas, 0, 0);
document.getElementById('infoSize').textContent = `${w} × ${h}`;
saveState(`调整尺寸 ${w}×${h}`);
showToast(`画布已调整为 ${w} × ${h}`, 'success');
}
function applyPresetSize() {
const val = document.getElementById('presetSize').value;
if (val) {
const [w, h] = val.split('x');
document.getElementById('canvasWidth').value = w;
document.getElementById('canvasHeight').value = h;
resizeCanvas();
}
}
function createNewCanvas() {
if (confirm('创建新画布将丢失当前工作,确定继续吗?')) {
ctx.fillStyle = document.getElementById('bgColor').value;
ctx.fillRect(0, 0, canvas.width, canvas.height);
state.history = []; state.historyIndex = -1;
saveState('新建画布');
showToast('已创建新画布', 'success');
}
}
function uploadImage(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
canvas.width = img.width; canvas.height = img.height;
document.getElementById('canvasWidth').value = img.width;
document.getElementById('canvasHeight').value = img.height;
ctx.drawImage(img, 0, 0);
saveState('上传图片: ' + file.name);
showToast('图片已加载: ' + file.name, 'success');
document.getElementById('infoSize').textContent = `${img.width} × ${img.height}`;
resetZoom();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
event.target.value = '';
}
function showExportModal() { document.getElementById('exportModal').classList.add('active'); }
function closeExportModal() { document.getElementById('exportModal').classList.remove('active'); }
document.getElementById('modalFormat').addEventListener('change', (e) => {
document.getElementById('modalQualityGroup').style.display = e.target.value === 'png' ? 'none' : 'block';
});
document.getElementById('modalQuality').addEventListener('input', (e) => {
document.getElementById('modalQualityValue').textContent = e.target.value + '%';
});
function modalExport() {
const format = document.getElementById('modalFormat').value;
const quality = parseInt(document.getElementById('modalQuality').value) / 100;
const filename = document.getElementById('modalFilename').value || 'drawing';
const scale = parseFloat(document.getElementById('modalScale').value);
exportWithSettings(format, quality, filename, scale);
closeExportModal();
}
function exportImage() {
const format = document.getElementById('exportFormat').value;
const quality = parseInt(document.getElementById('exportQuality').value) / 100;
const filename = document.getElementById('exportFilename').value || 'drawing';
exportWithSettings(format, quality, filename, 1);
}
function exportWithSettings(format, quality, filename, scale) {
let exportCanvas = canvas;
if (scale !== 1) {
exportCanvas = document.createElement('canvas');
exportCanvas.width = canvas.width * scale;
exportCanvas.height = canvas.height * scale;
exportCanvas.getContext('2d').drawImage(canvas, 0, 0, exportCanvas.width, exportCanvas.height);
}
let mimeType = 'image/png', ext = 'png';
switch(format) {
case 'jpeg': mimeType = 'image/jpeg'; ext = 'jpg'; break;
case 'webp': mimeType = 'image/webp'; ext = 'webp'; break;
case 'bmp': showToast('浏览器不支持BMP格式,已切换为PNG', 'info'); break;
}
const dataURL = exportCanvas.toDataURL(mimeType, quality);
const link = document.createElement('a');
link.download = `${filename}.${ext}`;
link.href = dataURL;
link.click();
const fileSize = Math.round((dataURL.length * 3) / 4 / 1024);
showToast(`已导出: ${filename}.${ext} (~${fileSize}KB)`, 'success');
}
function quickSave() { exportImage(); }
function flipHorizontal() {
const t = document.createElement('canvas');
t.width = canvas.width; t.height = canvas.height;
const tc = t.getContext('2d');
tc.translate(canvas.width, 0); tc.scale(-1, 1);
tc.drawImage(canvas, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(t, 0, 0);
saveState('水平翻转');
showToast('已水平翻转', 'success');
}
function flipVertical() {
const t = document.createElement('canvas');
t.width = canvas.width; t.height = canvas.height;
const tc = t.getContext('2d');
tc.translate(0, canvas.height); tc.scale(1, -1);
tc.drawImage(canvas, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(t, 0, 0);
saveState('垂直翻转');
showToast('已垂直翻转', 'success');
}
function rotateCanvas() {
const t = document.createElement('canvas');
t.width = canvas.height; t.height = canvas.width;
const tc = t.getContext('2d');
tc.translate(t.width/2, t.height/2);
tc.rotate(Math.PI/2);
tc.drawImage(canvas, -canvas.width/2, -canvas.height/2);
canvas.width = t.width; canvas.height = t.height;
ctx.drawImage(t, 0, 0);
document.getElementById('canvasWidth').value = canvas.width;
document.getElementById('canvasHeight').value = canvas.height;
document.getElementById('infoSize').textContent = `${canvas.width} × ${canvas.height}`;
saveState('旋转90°');
showToast('已旋转90°', 'success');
}
function invertColors() {
const id = ctx.getImageData(0, 0, canvas.width, canvas.height);
const d = id.data;
for (let i=0; i<d.length; i+=4) { d[i]=255-d[i]; d[i+1]=255-d[i+1]; d[i+2]=255-d[i+2]; }
ctx.putImageData(id, 0, 0);
saveState('反色');
showToast('已应用反色效果', 'success');
}
function grayscale() {
const id = ctx.getImageData(0, 0, canvas.width, canvas.height);
const d = id.data;
for (let i=0; i<d.length; i+=4) {
const g = d[i]*0.299 + d[i+1]*0.587 + d[i+2]*0.114;
d[i]=g; d[i+1]=g; d[i+2]=g;
}
ctx.putImageData(id, 0, 0);
saveState('灰度');
showToast('已应用灰度效果', 'success');
}
function closeTextModal() { document.getElementById('textModal').classList.remove('active'); }
function addText() {
const text = document.getElementById('textContent').value;
if (!text) return;
const size = document.getElementById('textSize').value;
const font = document.getElementById('textFont').value;
const style = document.getElementById('textStyle').value;
const color = document.getElementById('textColor').value;
ctx.fillStyle = color;
ctx.font = `${style} ${size}px ${font}`;
ctx.fillText(text, state.textX, state.textY);
saveState('添加文字: ' + text);
closeTextModal();
showToast('文字已添加', 'success');
}
function zoomIn() { state.zoom = Math.min(state.zoom + 0.1, 5); applyZoom(); }
function zoomOut() { state.zoom = Math.max(state.zoom - 0.1, 0.1); applyZoom(); }
function resetZoom() { state.zoom = 1; applyZoom(); }
function applyZoom() {
canvas.style.transform = `scale(${state.zoom})`;
canvas.style.transformOrigin = 'center center';
const percent = Math.round(state.zoom * 100);
document.getElementById('zoomLevel').textContent = percent + '%';
document.getElementById('statusZoom').textContent = percent + '%';
}
function handleKeyboard(e) {
if (e.ctrlKey || e.metaKey) {
switch(e.key) {
case 'z': e.preventDefault(); if (e.shiftKey) redo(); else undo(); break;
case 'y': e.preventDefault(); redo(); break;
case 's': e.preventDefault(); quickSave(); break;
}
} else {
switch(e.key.toLowerCase()) {
case 'b': selectTool('pen'); break;
case 'p': selectTool('brush'); break;
case 'e': selectTool('eraser'); break;
case 'l': selectTool('line'); break;
case 'r': selectTool('rectangle'); break;
case 'c': selectTool('circle'); break;
case 'g': selectTool('fill'); break;
case 't': selectTool('text'); break;
case 'i': selectTool('eyedropper'); break;
}
}
}
function selectTool(tool) {
state.tool = tool;
document.querySelectorAll('.tool-btn[data-tool]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tool === tool);
});
updateStatus();
}
function updateBrushPreview() {
const preview = document.getElementById('brushPreview');
const size = Math.max(4, Math.min(state.brushSize, 50));
preview.style.width = size + 'px';
preview.style.height = size + 'px';
preview.style.background = state.color;
}
function updateStatus() {
const names = {pen:'画笔',brush:'毛笔',eraser:'橡皮擦',line:'直线',rectangle:'矩形',circle:'圆形',triangle:'三角形',fill:'填充',text:'文字',eyedropper:'取色器'};
document.getElementById('currentTool').textContent = names[state.tool] || state.tool;
}
function updateInfo() {
document.getElementById('infoSize').textContent = `${canvas.width} × ${canvas.height}`;
const data = canvas.toDataURL('image/png');
const size = Math.round((data.length * 3) / 4 / 1024);
document.getElementById('infoFileSize').textContent = `~${size}KB`;
}
function showToast(message, type = 'info') {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icons = {success:'✅', error:'❌', info:'ℹ️'};
toast.innerHTML = `<span>${icons[type]||'ℹ️'}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
toast.style.transition = 'all 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
init();
</script>
</body>
</html>
index.html
index.html