// ==UserScript== // @name 随时草稿 网页绘画 // @namespace http://tampermonkey.net/ // @version 5.3 // @description 你可以随时随地在网页上打草稿/画画(v5.3:UI 重构、笔触线型、三种笔差异化、默认收起) // @author Nuclear_Fish_cyq // @match *://*/* // @license MIT // @grant none // ==/UserScript== (function () { 'use strict'; // ==================== 配置 ==================== const CONFIG = { ENABLE_IN_IFRAME: false, COLLAPSED_BY_DEFAULT: true, // 默认收起侧边栏(只显示浮动按钮) MAX_UNDO: 20, PRESET_COLORS: [ { name: '黑色', value: [0, 0, 0] }, { name: '蓝色', value: [37, 117, 252] }, { name: '绿色', value: [0, 255, 0] }, { name: '黄色', value: [255, 255, 0] }, { name: '紫色', value: [155, 48, 255] }, { name: '红色', value: [255, 0, 0] } ], SIZES: [ { value: 1, label: '细' }, { value: 3, label: '中' }, { value: 5, label: '粗' }, { value: 10, label: '特粗' } ], TOOLS: [ { id: 'brush', label: '✏️', title: '画笔' }, { id: 'line', label: '╱', title: '直线 (Shift=45°约束)' }, { id: 'rect', label: '▭', title: '矩形 (Shift=正方)' }, { id: 'ellipse', label: '◯', title: '椭圆 (Shift=正圆)' }, { id: 'arrow', label: '➤', title: '箭头' } ], BRUSH_TYPES: [ { id: 'pen', label: '✏️笔', title: '普通笔:细、实心' }, { id: 'highlighter', label: '🖍荧光', title: '荧光笔:宽、半透明、平头' }, { id: 'marker', label: '🖊马克', title: '马克笔:宽、中透明' } ], LINE_TYPES: [ { id: 'solid', label: '──', title: '实线' }, { id: 'dash', label: '┄┄', title: '虚线' }, { id: 'dot', label: '···', title: '点线' } ], DEFAULT_RGB: [255, 0, 0], DEFAULT_OPACITY: 1, DEFAULT_SIZE: 3, DEFAULT_TOOL: 'brush', DEFAULT_BRUSH_TYPE: 'pen', DEFAULT_LINE_TYPE: 'solid' }; // ==================== 环境检查 ==================== const isInIframe = window.self !== window.top; if (isInIframe && !CONFIG.ENABLE_IN_IFRAME) return; if (window.__drawingScriptLoaded) return; window.__drawingScriptLoaded = true; // ==================== 状态 ==================== const state = { drawingMode: false, drawing: false, eraserMode: false, collapsed: CONFIG.COLLAPSED_BY_DEFAULT, lastX: 0, lastY: 0, rgb: [...CONFIG.DEFAULT_RGB], opacity: CONFIG.DEFAULT_OPACITY, size: CONFIG.DEFAULT_SIZE, tool: CONFIG.DEFAULT_TOOL, brushType: CONFIG.DEFAULT_BRUSH_TYPE, lineType: CONFIG.DEFAULT_LINE_TYPE, shapeStart: null, points: [], baseSnapshot: null }; const undoStack = []; let strokeDirty = false; function rgbaStr(rgb, a) { return `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${a})`; } function hexStr(rgb) { return '#' + rgb.map(v => Math.round(v).toString(16).padStart(2, '0')).join('').toUpperCase(); } // ==================== DOM 工具 ==================== function makeEl(tag, className, text) { const el = document.createElement(tag); if (className) el.className = className; if (text !== undefined) el.textContent = text; return el; } // ==================== 样式 ==================== const style = document.createElement('style'); style.textContent = ` .dw-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 999999; display: none; } .dw-overlay.active { display: block; } /* ---- 侧边栏:毛玻璃卡片 ---- */ .dw-sidebar { position: fixed; top: 50%; right: 0; transform: translateY(-50%); z-index: 1000000; display: flex; flex-direction: column; align-items: center; background: rgba(255,255,255,.88); backdrop-filter: blur(14px) saturate(1.4); -webkit-backdrop-filter: blur(14px) saturate(1.4); border: 1px solid rgba(255,255,255,.7); box-shadow: 0 8px 32px rgba(31,38,135,.16), 0 2px 8px rgba(0,0,0,.06); border-radius: 16px 0 0 16px; padding: 12px 8px 14px; transition: transform .3s ease, opacity .3s ease; max-height: 90vh; overflow-y: auto; } .dw-sidebar::-webkit-scrollbar { width: 4px; } .dw-sidebar::-webkit-scrollbar-thumb { background: #d8dce8; border-radius: 4px; } .dw-sidebar.iframe-mode { border-right: 3px solid #ff9900; } .dw-sidebar.collapsed { transform: translateY(-50%) translateX(105%); opacity: 0; pointer-events: none; } .dw-collapse { position: absolute; top: 12px; left: -10px; width: 22px; height: 22px; border: 1px solid rgba(0,0,0,.08); border-radius: 50%; background: #fff; color: #8890a8; font-size: 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all .2s; z-index: 1000001; box-shadow: 0 2px 6px rgba(0,0,0,.12); } .dw-collapse:hover { background: #f0f2f8; color: #2575fc; transform: scale(1.1); } /* ---- 右下角浮动按钮 ---- */ .dw-fab { position: fixed; bottom: 22px; right: 22px; width: 52px; height: 52px; border: none; border-radius: 50%; color: #fff; font-size: 24px; cursor: pointer; display: none; align-items: center; justify-content: center; transition: all .25s; box-shadow: 0 6px 20px rgba(37,117,252,.4); z-index: 999998; background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%); } .dw-fab.show { display: flex; } .dw-fab:hover { transform: scale(1.08) translateY(-2px); box-shadow: 0 10px 26px rgba(37,117,252,.5); } .dw-fab.drawing { background: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%); box-shadow: 0 6px 20px rgba(255,75,43,.4); } /* ---- 主按钮 ---- */ .dw-main-btn { width: 50px; height: 50px; border: none; border-radius: 50%; background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%); color: #fff; font-size: 24px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all .2s; box-shadow: 0 4px 14px rgba(37,117,252,.35); margin-top: 12px; } .dw-main-btn:hover { transform: scale(1.06); } .dw-main-btn.drawing { background: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%); } .dw-status { margin-top: 8px; font-size: 12px; font-weight: 600; color: #8a93b0; text-align: center; width: 100%; } .dw-status.drawing { color: #ff416c; } .dw-iframe-tag { font-size: 10px; color: #ff9900; font-weight: bold; margin-top: 5px; } /* ---- 分段控件(通用) ---- */ .dw-seg { display: flex; width: 100%; margin-top: 8px; padding: 3px; background: #eef1f7; border-radius: 12px; gap: 2px; } .dw-seg button { flex: 1; padding: 5px 0; border: none; border-radius: 9px; background: transparent; color: #6b7390; font-size: 12px; cursor: pointer; transition: all .18s; white-space: nowrap; } .dw-seg button:hover { color: #2575fc; } .dw-seg button.active { background: #fff; color: #2575fc; font-weight: 700; box-shadow: 0 1px 4px rgba(31,38,135,.14); } .dw-seg button.active.dw-tool-active { background: #fff; color: #6a11cb; } /* ---- 快捷颜色(单行排列) ---- */ .dw-preset-colors { display: flex; flex-wrap: nowrap; justify-content: center; gap: 6px; margin-top: 10px; width: auto; } .dw-preset { width: 22px; height: 22px; border-radius: 50%; border: 2px solid rgba(255,255,255,.9); cursor: pointer; transition: all .18s; flex-shrink: 0; box-shadow: 0 1px 4px rgba(0,0,0,.18); } .dw-preset:hover { transform: scale(1.12); } .dw-preset.active { transform: scale(1.18); box-shadow: 0 0 0 2px #2575fc, 0 2px 8px rgba(37,117,252,.4); } /* ---- RGB 选择器 ---- */ .dw-rgb { width: 100%; margin-top: 10px; padding: 10px 8px; background: #f6f8fc; border-radius: 12px; } .dw-rgb-row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; } .dw-rgb-row:last-of-type { margin-bottom: 0; } .dw-rgb-label { width: 14px; font-size: 11px; font-weight: 800; text-align: center; } .dw-rgb-label.r { color: #e5484d; } .dw-rgb-label.g { color: #2fa557; } .dw-rgb-label.b { color: #2f6fd0; } .dw-rgb input[type=range] { flex: 1; height: 5px; -webkit-appearance: none; appearance: none; background: linear-gradient(90deg, #000, #fff); border-radius: 5px; outline: none; } .dw-rgb input[type=range].r { background: linear-gradient(90deg, #000, #f00); } .dw-rgb input[type=range].g { background: linear-gradient(90deg, #000, #0f0); } .dw-rgb input[type=range].b { background: linear-gradient(90deg, #000, #00f); } .dw-rgb input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 15px; height: 15px; border-radius: 50%; background: #fff; border: 2px solid #2575fc; box-shadow: 0 1px 4px rgba(0,0,0,.25); cursor: pointer; } .dw-rgb input[type=range]::-moz-range-thumb { width: 15px; height: 15px; border-radius: 50%; background: #fff; border: 2px solid #2575fc; cursor: pointer; } .dw-rgb-preview { display: flex; align-items: center; gap: 7px; margin-top: 8px; } .dw-rgb-swatch { width: 26px; height: 26px; border-radius: 8px; border: 1px solid rgba(0,0,0,.08); background: #f00; box-shadow: inset 0 1px 2px rgba(0,0,0,.12); } .dw-rgb-hex { font-size: 11px; color: #7a8299; font-family: Consolas, monospace; } /* ---- 透明度 ---- */ .dw-opacity { width: 100%; margin-top: 8px; padding: 10px 8px; background: #f6f8fc; border-radius: 12px; } .dw-opacity-row { display: flex; align-items: center; gap: 6px; } .dw-opacity-label { font-size: 11px; color: #6b7390; font-weight: 700; white-space: nowrap; } .dw-opacity input[type=range] { flex: 1; height: 5px; -webkit-appearance: none; appearance: none; background: linear-gradient(90deg, rgba(0,0,0,0), #000); border-radius: 5px; outline: none; } .dw-opacity input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 15px; height: 15px; border-radius: 50%; background: #fff; border: 2px solid #2575fc; box-shadow: 0 1px 4px rgba(0,0,0,.25); cursor: pointer; } .dw-opacity input[type=range]::-moz-range-thumb { width: 15px; height: 15px; border-radius: 50%; background: #fff; border: 2px solid #2575fc; cursor: pointer; } .dw-opacity-val { font-size: 11px; color: #7a8299; width: 34px; text-align: right; font-family: Consolas, monospace; } /* ---- 粗细 ---- */ .dw-sizes { display: flex; flex-direction: column; align-items: center; gap: 5px; margin-top: 10px; width: 100%; } .dw-sizes-label { font-size: 11px; color: #8a93b0; } .dw-sizes-row { display: flex; gap: 4px; } .dw-size { width: 26px; height: 26px; border-radius: 50%; border: none; background: #eef1f7; color: #6b7390; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all .18s; } .dw-size:hover { transform: scale(1.08); } .dw-size.active { background: #2575fc; color: #fff; box-shadow: 0 2px 8px rgba(37,117,252,.35); } /* ---- 工具按钮 ---- */ .dw-tools { display: flex; gap: 8px; margin-top: 12px; } .dw-tool { width: 38px; height: 38px; border: none; border-radius: 50%; background: #eef1f7; font-size: 15px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all .18s; } .dw-tool:hover { transform: scale(1.08); background: #e2e7f2; } .dw-tool.undo:hover { background: #e8e4f8; } .dw-tool.clear:hover { background: #ffe0e0; } .dw-tool.save:hover { background: #dff0e3; } .dw-tool.copy:hover { background: #e0ecff; } `; document.head.appendChild(style); // ==================== 画布引擎 ==================== ['drawing-overlay', 'drawing-sidebar', 'drawing-floating-btn'].forEach(id => { const old = document.getElementById(id); if (old) old.remove(); }); const overlay = makeEl('canvas', 'dw-overlay'); overlay.id = 'drawing-overlay'; document.body.appendChild(overlay); const ctx = overlay.getContext('2d'); // 应用线型(虚线/点线) function applyLineDash() { if (state.lineType === 'dash') ctx.setLineDash([10, 6]); else if (state.lineType === 'dot') ctx.setLineDash([2, 4]); else ctx.setLineDash([]); } // 根据当前工具/笔刷/模式设置画笔样式 function applyBrushStyle() { const isEraser = state.eraserMode; ctx.globalCompositeOperation = isEraser ? 'destination-out' : 'source-over'; if (state.brushType === 'highlighter') { // 荧光笔:超宽 + 低透明 + 平头(视觉差异最明显) ctx.lineWidth = Math.max(10, state.size * 4); ctx.lineCap = 'butt'; ctx.lineJoin = 'miter'; ctx.globalAlpha = 0.35; ctx.strokeStyle = isEraser ? 'rgba(0,0,0,1)' : rgbaStr(state.rgb, 1); } else if (state.brushType === 'marker') { // 马克笔:宽 + 中透明 + 圆头 ctx.lineWidth = Math.max(6, state.size * 2.5); ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.globalAlpha = 0.65; ctx.strokeStyle = isEraser ? 'rgba(0,0,0,1)' : rgbaStr(state.rgb, 1); } else { // 普通笔:细 + 全透明 + 圆头 ctx.lineWidth = state.size; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.globalAlpha = 1; ctx.strokeStyle = isEraser ? 'rgba(0,0,0,1)' : rgbaStr(state.rgb, state.opacity); } applyLineDash(); } function currentFillColor() { if (state.eraserMode) return 'rgba(0,0,0,1)'; if (state.brushType === 'highlighter') return rgbaStr(state.rgb, 0.35); if (state.brushType === 'marker') return rgbaStr(state.rgb, 0.65); return rgbaStr(state.rgb, state.opacity); } function resizeOverlay() { const snapshot = (overlay.width > 0 && overlay.height > 0) ? ctx.getImageData(0, 0, overlay.width, overlay.height) : null; overlay.width = window.innerWidth; overlay.height = window.innerHeight; if (snapshot) ctx.putImageData(snapshot, 0, 0); applyBrushStyle(); } resizeOverlay(); window.addEventListener('resize', resizeOverlay); function pushUndo() { if (overlay.width === 0 || overlay.height === 0) return; undoStack.push(ctx.getImageData(0, 0, overlay.width, overlay.height)); if (undoStack.length > CONFIG.MAX_UNDO) undoStack.shift(); } function undo() { const snap = undoStack.pop(); if (!snap) { showStatus('没有可撤销的操作'); return; } ctx.putImageData(snap, 0, 0); showStatus('已撤销'); } // ==================== UI 构建 ==================== const sidebar = makeEl('div', 'dw-sidebar'); sidebar.id = 'drawing-sidebar'; if (isInIframe) sidebar.classList.add('iframe-mode'); const collapseBtn = makeEl('button', 'dw-collapse', '▶'); collapseBtn.title = '收起侧边栏'; const mainBtn = makeEl('button', 'dw-main-btn', isInIframe ? '🎨(内)' : '🎨'); mainBtn.title = '绘画模式 (按ESC可退出)' + (isInIframe ? ' [iframe模式]' : ''); const statusEl = makeEl('div', 'dw-status', '关闭'); // 画笔/橡皮擦 const modeSwitch = makeEl('div', 'dw-seg'); const brushModeBtn = makeEl('button', 'active', '画笔'); const eraserModeBtn = makeEl('button', '', '橡皮擦'); brushModeBtn.title = '切换到画笔模式'; eraserModeBtn.title = '切换到橡皮擦模式'; modeSwitch.append(brushModeBtn, eraserModeBtn); // 形状工具行 const toolRow = makeEl('div', 'dw-seg'); const toolBtns = {}; CONFIG.TOOLS.forEach(t => { const btn = makeEl('button', t.id === state.tool ? 'active dw-tool-active' : '', t.label); btn.title = t.title; if (t.id === state.tool) btn.classList.add('active', 'dw-tool-active'); btn.addEventListener('click', () => setTool(t.id)); toolBtns[t.id] = btn; toolRow.appendChild(btn); }); // 笔刷类型行 const brushRow = makeEl('div', 'dw-seg'); const brushBtns = {}; CONFIG.BRUSH_TYPES.forEach(b => { const btn = makeEl('button', b.id === state.brushType ? 'active' : '', b.label); btn.title = b.title; if (b.id === state.brushType) btn.classList.add('active'); btn.addEventListener('click', () => setBrushType(b.id)); brushBtns[b.id] = btn; brushRow.appendChild(btn); }); // 线型行 const lineRow = makeEl('div', 'dw-seg'); const lineBtns = {}; CONFIG.LINE_TYPES.forEach(l => { const btn = makeEl('button', l.id === state.lineType ? 'active' : '', l.label); btn.title = l.title; if (l.id === state.lineType) btn.classList.add('active'); btn.addEventListener('click', () => setLineType(l.id)); lineBtns[l.id] = btn; lineRow.appendChild(btn); }); // 快捷颜色 const presetPicker = makeEl('div', 'dw-preset-colors'); CONFIG.PRESET_COLORS.forEach(c => { const btn = makeEl('button', 'dw-preset'); btn.title = c.name; btn.style.background = rgbaStr(c.value, 1); btn.dataset.rgb = c.value.join(','); if (c.value.join(',') === state.rgb.join(',')) btn.classList.add('active'); btn.addEventListener('click', () => setRgb(c.value)); presetPicker.appendChild(btn); }); // RGB 选择器 const rgbPicker = makeEl('div', 'dw-rgb'); const rgbSliders = {}; ['R', 'G', 'B'].forEach((ch, i) => { const row = makeEl('div', 'dw-rgb-row'); const label = makeEl('span', 'dw-rgb-label ' + ch.toLowerCase(), ch); const input = document.createElement('input'); input.type = 'range'; input.min = 0; input.max = 255; input.value = state.rgb[i]; input.className = ch.toLowerCase(); input.addEventListener('input', () => { state.rgb[i] = parseInt(input.value, 10); updateRgbUI(); applyBrushStyle(); }); rgbSliders[ch] = input; row.append(label, input); rgbPicker.appendChild(row); }); const rgbPreviewRow = makeEl('div', 'dw-rgb-preview'); const rgbSwatch = makeEl('div', 'dw-rgb-swatch'); const rgbHex = makeEl('span', 'dw-rgb-hex', hexStr(state.rgb)); rgbPreviewRow.append(rgbSwatch, rgbHex); rgbPicker.appendChild(rgbPreviewRow); function updateRgbUI() { rgbSliders.R.value = state.rgb[0]; rgbSliders.G.value = state.rgb[1]; rgbSliders.B.value = state.rgb[2]; rgbSwatch.style.background = rgbaStr(state.rgb, state.opacity); rgbHex.textContent = hexStr(state.rgb); presetPicker.querySelectorAll('.dw-preset').forEach(b => b.classList.toggle('active', b.dataset.rgb === state.rgb.join(','))); } // 透明度 const opacityBox = makeEl('div', 'dw-opacity'); const opacityRow = makeEl('div', 'dw-opacity-row'); const opacityLabel = makeEl('span', 'dw-opacity-label', '透明度'); const opacityInput = document.createElement('input'); opacityInput.type = 'range'; opacityInput.min = 0; opacityInput.max = 100; opacityInput.value = Math.round(state.opacity * 100); const opacityVal = makeEl('span', 'dw-opacity-val', state.opacity * 100 + '%'); opacityInput.addEventListener('input', () => { state.opacity = parseInt(opacityInput.value, 10) / 100; opacityVal.textContent = Math.round(state.opacity * 100) + '%'; rgbSwatch.style.background = rgbaStr(state.rgb, state.opacity); applyBrushStyle(); }); opacityRow.append(opacityLabel, opacityInput, opacityVal); opacityBox.appendChild(opacityRow); // 粗细 const sizePicker = makeEl('div', 'dw-sizes'); const sizeLabel = makeEl('div', 'dw-sizes-label', '粗细:'); const sizeRow = makeEl('div', 'dw-sizes-row'); CONFIG.SIZES.forEach(s => { const btn = makeEl('button', 'dw-size', '●'); btn.title = `${s.value}px - ${s.label}`; btn.style.fontSize = Math.min(s.value, 8) + 'px'; btn.dataset.size = s.value; if (s.value === state.size) btn.classList.add('active'); btn.addEventListener('click', () => setSize(s.value)); sizeRow.appendChild(btn); }); sizePicker.append(sizeLabel, sizeRow); // 工具按钮 const tools = makeEl('div', 'dw-tools'); const undoBtn = makeEl('button', 'dw-tool undo', '↩️'); const clearBtn = makeEl('button', 'dw-tool clear', '🗑️'); const copyBtn = makeEl('button', 'dw-tool copy', '📋'); const saveBtn = makeEl('button', 'dw-tool save', '💾'); undoBtn.title = '撤销 (Ctrl+Z)'; clearBtn.title = '清除绘画'; copyBtn.title = '复制到剪贴板'; saveBtn.title = '保存绘画 (PNG)'; tools.append(undoBtn, clearBtn, copyBtn, saveBtn); sidebar.append(collapseBtn, mainBtn, statusEl, modeSwitch, toolRow, brushRow, lineRow, presetPicker, rgbPicker, opacityBox, sizePicker, tools); if (isInIframe) sidebar.appendChild(makeEl('div', 'dw-iframe-tag', '内嵌页')); const fab = makeEl('button', 'dw-fab', '🎨'); fab.id = 'drawing-floating-btn'; fab.title = '展开绘画工具'; setTimeout(() => { document.body.appendChild(sidebar); document.body.appendChild(fab); // 默认收起:只显示浮动按钮 if (state.collapsed) { sidebar.classList.add('collapsed'); fab.classList.add('show'); } }, 300); // ==================== 提示 ==================== let statusTimer = null; function showStatus(text) { statusEl.textContent = text; clearTimeout(statusTimer); statusTimer = setTimeout(() => { statusEl.textContent = state.drawingMode ? (state.eraserMode ? '橡皮擦中' : '绘画中') : '关闭'; }, 1200); } // ==================== 模式控制 ==================== function setRgb(rgb) { state.rgb = [...rgb]; updateRgbUI(); state.eraserMode = false; refreshModeUI(); applyBrushStyle(); } function setSize(size) { state.size = size; applyBrushStyle(); sizePicker.querySelectorAll('.dw-size').forEach(b => b.classList.toggle('active', parseInt(b.dataset.size, 10) === size)); } function setTool(tool) { state.tool = tool; Object.keys(toolBtns).forEach(id => toolBtns[id].classList.toggle('active', id === tool)); applyBrushStyle(); } function setBrushType(type) { state.brushType = type; Object.keys(brushBtns).forEach(id => brushBtns[id].classList.toggle('active', id === type)); applyBrushStyle(); } function setLineType(type) { state.lineType = type; Object.keys(lineBtns).forEach(id => lineBtns[id].classList.toggle('active', id === type)); applyBrushStyle(); } function setEraserMode(on) { state.eraserMode = on; refreshModeUI(); applyBrushStyle(); } function refreshModeUI() { brushModeBtn.classList.toggle('active', !state.eraserMode); eraserModeBtn.classList.toggle('active', state.eraserMode); if (state.drawingMode) { statusEl.textContent = state.eraserMode ? '橡皮擦中' : '绘画中'; statusEl.classList.add('drawing'); } if (state.collapsed && state.drawingMode) { fab.textContent = state.eraserMode ? '🧽' : '✏️'; } } // ==================== 绘画事件 ==================== function getPos(e) { return e.type.includes('touch') ? [e.touches[0].clientX, e.touches[0].clientY] : [e.clientX, e.clientY]; } function constrainShape(start, end, shift) { if (!shift) return end; const dx = end.x - start.x, dy = end.y - start.y; if (state.tool === 'line' || state.tool === 'arrow') { const len = Math.hypot(dx, dy); const angle = Math.atan2(dy, dx); const snapped = Math.round(angle / (Math.PI / 4)) * (Math.PI / 4); return { x: start.x + Math.cos(snapped) * len, y: start.y + Math.sin(snapped) * len }; } const side = Math.max(Math.abs(dx), Math.abs(dy)); return { x: start.x + Math.sign(dx || 1) * side, y: start.y + Math.sign(dy || 1) * side }; } function paintShapePreview(start, end) { const x0 = start.x, y0 = start.y; const x1 = end.x, y1 = end.y; const w = Math.abs(x1 - x0), h = Math.abs(y1 - y0); const left = Math.min(x0, x1), top = Math.min(y0, y1); ctx.beginPath(); if (state.tool === 'line' || state.tool === 'arrow') { ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); if (state.tool === 'arrow') { const angle = Math.atan2(y1 - y0, x1 - x0); const head = Math.max(12, ctx.lineWidth * 3); const spread = Math.PI / 7; ctx.moveTo(x1, y1); ctx.lineTo(x1 - head * Math.cos(angle - spread), y1 - head * Math.sin(angle - spread)); ctx.moveTo(x1, y1); ctx.lineTo(x1 - head * Math.cos(angle + spread), y1 - head * Math.sin(angle + spread)); } } else if (state.tool === 'rect') { ctx.rect(left, top, w, h); } else if (state.tool === 'ellipse') { ctx.ellipse((x0 + x1) / 2, (y0 + y1) / 2, w / 2, h / 2, 0, 0, Math.PI * 2); } ctx.stroke(); } function startDrawing(e) { if (!state.drawingMode) return; state.drawing = true; strokeDirty = false; pushUndo(); [state.lastX, state.lastY] = getPos(e); if (state.tool === 'brush') { state.points = [{ x: state.lastX, y: state.lastY }]; ctx.beginPath(); ctx.arc(state.lastX, state.lastY, state.size / 2, 0, Math.PI * 2); ctx.fillStyle = currentFillColor(); ctx.fill(); ctx.globalAlpha = 1; ctx.beginPath(); ctx.moveTo(state.lastX, state.lastY); } else { state.shapeStart = { x: state.lastX, y: state.lastY }; state.baseSnapshot = ctx.getImageData(0, 0, overlay.width, overlay.height); applyBrushStyle(); } } function draw(e) { if (!state.drawing) return; e.preventDefault(); const [x, y] = getPos(e); if (state.tool === 'brush') { const prev = state.points[state.points.length - 1]; const mid = { x: (prev.x + x) / 2, y: (prev.y + y) / 2 }; ctx.quadraticCurveTo(prev.x, prev.y, mid.x, mid.y); ctx.stroke(); state.points.push({ x, y }); } else { if (state.baseSnapshot) { ctx.putImageData(state.baseSnapshot, 0, 0); ctx.globalAlpha = 1; } const end = constrainShape(state.shapeStart, { x, y }, e.shiftKey); applyBrushStyle(); paintShapePreview(state.shapeStart, end); } state.lastX = x; state.lastY = y; strokeDirty = true; } function stopDrawing() { if (!state.drawing) return; state.drawing = false; if (state.tool === 'brush') { ctx.closePath(); } else { if (!strokeDirty) { const s = state.shapeStart; ctx.beginPath(); ctx.arc(s.x, s.y, state.size / 2, 0, Math.PI * 2); ctx.fillStyle = currentFillColor(); ctx.fill(); ctx.globalAlpha = 1; } state.shapeStart = null; state.baseSnapshot = null; } state.points = []; ctx.globalAlpha = 1; if (!strokeDirty) undoStack.pop(); } // ==================== 绘画模式开关 ==================== const preventDefault = (e) => { if (state.drawingMode) { e.preventDefault(); return false; } }; const preventDrawDefault = (e) => { if (state.drawingMode && e.button === 0) { e.preventDefault(); return false; } }; function bindDrawEvents() { document.addEventListener('mousedown', startDrawing); document.addEventListener('mousemove', draw); document.addEventListener('mouseup', stopDrawing); document.addEventListener('touchstart', startDrawing); document.addEventListener('touchmove', draw, { passive: false }); document.addEventListener('touchend', stopDrawing); document.addEventListener('contextmenu', preventDefault); document.addEventListener('mousedown', preventDrawDefault); document.addEventListener('selectstart', preventDefault); if (isInIframe) window.addEventListener('wheel', preventDefault, { passive: false }); } function unbindDrawEvents() { document.removeEventListener('mousedown', startDrawing); document.removeEventListener('mousemove', draw); document.removeEventListener('mouseup', stopDrawing); document.removeEventListener('touchstart', startDrawing); document.removeEventListener('touchmove', draw, { passive: false }); document.removeEventListener('touchend', stopDrawing); document.removeEventListener('contextmenu', preventDefault); document.removeEventListener('mousedown', preventDrawDefault); document.removeEventListener('selectstart', preventDefault); if (isInIframe) window.removeEventListener('wheel', preventDefault); } function toggleDrawingMode() { state.drawingMode = !state.drawingMode; overlay.classList.toggle('active', state.drawingMode); mainBtn.classList.toggle('drawing', state.drawingMode); mainBtn.textContent = state.drawingMode ? (isInIframe ? '✏️(内)' : '✏️') : (isInIframe ? '🎨(内)' : '🎨'); if (state.drawingMode) { statusEl.textContent = state.eraserMode ? '橡皮擦中' : '绘画中'; statusEl.classList.add('drawing'); fab.classList.add('drawing'); if (state.collapsed) fab.textContent = state.eraserMode ? '🧽' : '✏️'; document.body.style.overflow = 'hidden'; applyBrushStyle(); bindDrawEvents(); } else { statusEl.textContent = '关闭'; statusEl.classList.remove('drawing'); fab.classList.remove('drawing'); if (state.collapsed) fab.textContent = '🎨'; document.body.style.overflow = ''; unbindDrawEvents(); } } // ==================== 侧边栏收起/展开 ==================== function collapseSidebar() { if (state.collapsed) return; state.collapsed = true; sidebar.classList.add('collapsed'); fab.classList.add('show'); if (state.drawingMode) { fab.textContent = state.eraserMode ? '🧽' : '✏️'; fab.classList.add('drawing'); } } function expandSidebar() { if (!state.collapsed) return; state.collapsed = false; sidebar.classList.remove('collapsed'); fab.classList.remove('show'); } // ==================== 工具栏操作 ==================== function clearCanvas() { if (overlay.width === 0 || overlay.height === 0) return; pushUndo(); ctx.clearRect(0, 0, overlay.width, overlay.height); ctx.globalAlpha = 1; showStatus('画布已清除'); } function saveCanvas() { try { const link = document.createElement('a'); link.download = `网页绘画_${new Date().getTime()}.png`; link.href = overlay.toDataURL('image/png'); link.click(); showStatus('已保存 PNG'); } catch (err) { console.error('保存绘画时出错:', err); showStatus('保存失败'); } } function copyCanvas() { if (overlay.width === 0 || overlay.height === 0) { showStatus('画布为空'); return; } overlay.toBlob((blob) => { if (!blob) { showStatus('复制失败'); return; } try { navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]) .then(() => showStatus('已复制到剪贴板 ✅')) .catch(() => showStatus('复制失败(浏览器不支持)')); } catch (err) { console.error('复制出错:', err); showStatus('复制失败(浏览器不支持)'); } }, 'image/png'); } // ==================== 事件绑定 ==================== mainBtn.addEventListener('click', toggleDrawingMode); collapseBtn.addEventListener('click', collapseSidebar); fab.addEventListener('click', expandSidebar); undoBtn.addEventListener('click', undo); clearBtn.addEventListener('click', clearCanvas); copyBtn.addEventListener('click', copyCanvas); saveBtn.addEventListener('click', saveCanvas); brushModeBtn.addEventListener('click', () => setEraserMode(false)); eraserModeBtn.addEventListener('click', () => setEraserMode(true)); document.addEventListener('keydown', (e) => { if (state.drawingMode && e.key === 'Escape') { toggleDrawingMode(); } if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z' && state.drawingMode) { e.preventDefault(); undo(); } }); console.log(`绘画脚本 v5.3 已加载${isInIframe ? '(iframe模式)' : ''}${state.collapsed ? '(侧边栏默认收起)' : ''}`); })();