|
|
|
|
@ -23,6 +23,9 @@ class ScreenshotTool { |
|
|
|
|
this.annotations = []; |
|
|
|
|
this.textInput = null; |
|
|
|
|
this.dpr = window.devicePixelRatio || 1; |
|
|
|
|
|
|
|
|
|
this.lineWidth = 2; |
|
|
|
|
this.lineColor = '#ff4d4f'; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async start(onComplete, onCancel) { |
|
|
|
|
@ -30,23 +33,23 @@ class ScreenshotTool { |
|
|
|
|
this.onCancel = onCancel; |
|
|
|
|
|
|
|
|
|
if (!this.isSupported()) { |
|
|
|
|
throw new Error("当前浏览器不支持截图"); |
|
|
|
|
throw new Error('当前浏览器不支持截图'); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
this.stream = await navigator.mediaDevices.getDisplayMedia({ |
|
|
|
|
video: { displaySurface: "monitor", width: 3840, height: 2160 }, |
|
|
|
|
video: { displaySurface: 'monitor', width: 3840, height: 2160 }, |
|
|
|
|
audio: false, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
this.video = document.createElement("video"); |
|
|
|
|
this.video = document.createElement('video'); |
|
|
|
|
this.video.srcObject = this.stream; |
|
|
|
|
this.video.muted = true; |
|
|
|
|
await this.video.play(); |
|
|
|
|
this.createOverlay(); |
|
|
|
|
} catch (err) { |
|
|
|
|
if (err.name === "NotAllowedError") throw new Error("已拒绝屏幕权限"); |
|
|
|
|
throw new Error("截图启动失败"); |
|
|
|
|
if (err.name === 'NotAllowedError') throw new Error('已拒绝屏幕权限'); |
|
|
|
|
throw new Error('截图启动失败'); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -57,14 +60,14 @@ class ScreenshotTool { |
|
|
|
|
createOverlay() { |
|
|
|
|
this.scrollX = window.scrollX; |
|
|
|
|
this.scrollY = window.scrollY; |
|
|
|
|
document.body.style.overflow = "hidden"; |
|
|
|
|
document.documentElement.style.overflow = "hidden"; |
|
|
|
|
document.body.style.overflow = 'hidden'; |
|
|
|
|
document.documentElement.style.overflow = 'hidden'; |
|
|
|
|
|
|
|
|
|
this.overlay = document.createElement("div"); |
|
|
|
|
this.overlay.id = "screenshot-overlay"; |
|
|
|
|
this.overlay = document.createElement('div'); |
|
|
|
|
this.overlay.id = 'screenshot-overlay'; |
|
|
|
|
this.overlay.style.cssText = ` |
|
|
|
|
position:fixed;top:0;left:0;width:100vw;height:100vh; |
|
|
|
|
background:rgba(0,0,0,0.6);z-index:999999;cursor:crosshair;user-select:none; |
|
|
|
|
background:rgba(0,0,0,0.5);z-index:999999;cursor:crosshair;user-select:none; |
|
|
|
|
`;
|
|
|
|
|
document.body.appendChild(this.overlay); |
|
|
|
|
|
|
|
|
|
@ -77,78 +80,118 @@ class ScreenshotTool { |
|
|
|
|
const w = this.video.videoWidth; |
|
|
|
|
const h = this.video.videoHeight; |
|
|
|
|
|
|
|
|
|
this.canvas = document.createElement("canvas"); |
|
|
|
|
this.canvas = document.createElement('canvas'); |
|
|
|
|
this.canvas.width = w; |
|
|
|
|
this.canvas.height = h; |
|
|
|
|
this.canvas.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%"; |
|
|
|
|
this.canvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%'; |
|
|
|
|
this.overlay.appendChild(this.canvas); |
|
|
|
|
this.canvas.getContext("2d").drawImage(this.video, 0, 0, w, h); |
|
|
|
|
this.canvas.getContext('2d').drawImage(this.video, 0, 0, w, h); |
|
|
|
|
|
|
|
|
|
this.selectionCanvas = document.createElement("canvas"); |
|
|
|
|
this.selectionCanvas = document.createElement('canvas'); |
|
|
|
|
this.selectionCanvas.width = w; |
|
|
|
|
this.selectionCanvas.height = h; |
|
|
|
|
this.selectionCanvas.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none"; |
|
|
|
|
this.selectionCanvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none'; |
|
|
|
|
this.overlay.appendChild(this.selectionCanvas); |
|
|
|
|
|
|
|
|
|
this.drawCanvas = document.createElement("canvas"); |
|
|
|
|
this.drawCanvas = document.createElement('canvas'); |
|
|
|
|
this.drawCanvas.width = w; |
|
|
|
|
this.drawCanvas.height = h; |
|
|
|
|
this.drawCanvas.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;display:none"; |
|
|
|
|
this.drawCanvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;display:none'; |
|
|
|
|
this.overlay.appendChild(this.drawCanvas); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
createToolbar() { |
|
|
|
|
this.toolbar = document.createElement("div"); |
|
|
|
|
this.toolbar = document.createElement('div'); |
|
|
|
|
this.toolbar.style.cssText = ` |
|
|
|
|
position:fixed;bottom:60px;left:50%;transform:translateX(-50%); |
|
|
|
|
background:#1f1f1f;border-radius:10px;padding:10px 20px; |
|
|
|
|
display:flex;gap:28px;box-shadow:0 4px 20px rgba(0,0,0,0.4);z-index:1000000; |
|
|
|
|
position:fixed;top:40px;left:50%;transform:translateX(-50%); |
|
|
|
|
background:#fff;border-radius:8px;padding:6px; |
|
|
|
|
display:flex;gap:4px;box-shadow:0 2px 12px rgba(0,0,0,0.15);z-index:1000000; |
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const btns = [ |
|
|
|
|
{ icon: "✓", color: "#67c23a", action: "confirm" }, |
|
|
|
|
{ icon: "✕", color: "#f56c6c", action: "cancel" }, |
|
|
|
|
{ icon: "🖵", color: "#409eff", action: "fullscreen" }, |
|
|
|
|
const tools = [ |
|
|
|
|
{ icon: '▢', tool: 'rect', title: '矩形' }, |
|
|
|
|
{ icon: '○', tool: 'circle', title: '圆形' }, |
|
|
|
|
{ icon: '✏️', tool: 'pen', title: '画笔' }, |
|
|
|
|
{ icon: '➤', tool: 'arrow', title: '箭头' }, |
|
|
|
|
{ icon: 'A', tool: 'text', title: '文字' }, |
|
|
|
|
{ icon: '▦', tool: 'mosaic', title: '马赛克' }, |
|
|
|
|
{ icon: '↩', tool: 'undo', title: '撤销' }, |
|
|
|
|
{ icon: '🖵', tool: 'fullscreen', title: '全屏' }, |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
btns.forEach((b) => { |
|
|
|
|
const el = document.createElement("button"); |
|
|
|
|
el.innerHTML = `<span style="font-size:20px">${b.icon}</span>`; |
|
|
|
|
el.style.cssText = `background:none;border:none;color:${b.color};cursor:pointer;padding:6px;border-radius:6px`; |
|
|
|
|
el.onmouseover = () => el.style.background = "rgba(255,255,255,0.1)"; |
|
|
|
|
el.onmouseout = () => el.style.background = "transparent"; |
|
|
|
|
el.onclick = (e) => { e.stopPropagation(); this.handleAction(b.action); }; |
|
|
|
|
tools.forEach((t) => { |
|
|
|
|
const el = document.createElement('button'); |
|
|
|
|
el.innerHTML = `<span style="font-size:18px">${t.icon}</span>`; |
|
|
|
|
el.style.cssText = ` |
|
|
|
|
background:none;border:none;color:#333;cursor:pointer; |
|
|
|
|
padding:8px 12px;border-radius:6px; |
|
|
|
|
display:flex;align-items:center;justify-content:center; |
|
|
|
|
min-width:36px;height:36px; |
|
|
|
|
transition:background-color 0.2s; |
|
|
|
|
`;
|
|
|
|
|
el.title = t.title; |
|
|
|
|
el.onmouseover = () => { el.style.background = '#f0f0f0'; }; |
|
|
|
|
el.onmouseout = () => { el.style.background = 'transparent'; }; |
|
|
|
|
el.onclick = (e) => { |
|
|
|
|
e.stopPropagation(); |
|
|
|
|
this.handleToolbarTool(t.tool); |
|
|
|
|
}; |
|
|
|
|
this.toolbar.appendChild(el); |
|
|
|
|
}); |
|
|
|
|
this.overlay.appendChild(this.toolbar); |
|
|
|
|
|
|
|
|
|
this.actionToolbar = document.createElement("div"); |
|
|
|
|
this.actionToolbar = document.createElement('div'); |
|
|
|
|
this.actionToolbar.style.cssText = ` |
|
|
|
|
position:fixed;background:#fff;border-radius:8px;padding:6px 10px; |
|
|
|
|
display:none;gap:12px;box-shadow:0 2px 12px rgba(0,0,0,0.15);z-index:1000001; |
|
|
|
|
position:fixed;bottom:40px;left:50%;transform:translateX(-50%); |
|
|
|
|
background:#fff;border-radius:8px;padding:6px; |
|
|
|
|
display:flex;gap:4px;box-shadow:0 2px 12px rgba(0,0,0,0.15);z-index:1000001; |
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const tools = [ |
|
|
|
|
{ icon: "▢", tool: "rect" }, { icon: "○", tool: "circle" }, |
|
|
|
|
{ icon: "✏️", tool: "pen" }, { icon: "➤", tool: "arrow" }, |
|
|
|
|
{ icon: "A", tool: "text" }, { icon: "▦", tool: "mosaic" }, |
|
|
|
|
{ icon: "↩", tool: "undo" }, |
|
|
|
|
const actions = [ |
|
|
|
|
{ icon: '✕', action: 'cancel', color: '#666', title: '取消' }, |
|
|
|
|
{ icon: '✓', action: 'confirm', color: '#52c41a', title: '确认' }, |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
tools.forEach((t) => { |
|
|
|
|
const el = document.createElement("button"); |
|
|
|
|
el.innerHTML = `<span style="font-size:16px">${t.icon}</span>`; |
|
|
|
|
el.style.cssText = "background:none;border:none;color:#333;cursor:pointer;padding:4px 6px;border-radius:4px"; |
|
|
|
|
el.onmouseover = () => el.style.background = "#f0f0f0"; |
|
|
|
|
el.onmouseout = () => el.style.background = "transparent"; |
|
|
|
|
el.onclick = (e) => { e.stopPropagation(); this.handleAnnotateTool(t.tool); }; |
|
|
|
|
actions.forEach((a) => { |
|
|
|
|
const el = document.createElement('button'); |
|
|
|
|
el.innerHTML = `<span style="font-size:20px">${a.icon}</span>`; |
|
|
|
|
el.style.cssText = ` |
|
|
|
|
background:none;border:none;color:${a.color};cursor:pointer; |
|
|
|
|
padding:10px 20px;border-radius:6px; |
|
|
|
|
font-weight:500; |
|
|
|
|
transition:background-color 0.2s; |
|
|
|
|
min-width:80px;height:36px; |
|
|
|
|
display:flex;align-items:center;justify-content:center; |
|
|
|
|
`;
|
|
|
|
|
el.title = a.title; |
|
|
|
|
el.onmouseover = () => { el.style.background = '#f0f0f0'; }; |
|
|
|
|
el.onmouseout = () => { el.style.background = 'transparent'; }; |
|
|
|
|
el.onclick = (e) => { |
|
|
|
|
e.stopPropagation(); |
|
|
|
|
this.handleAction(a.action); |
|
|
|
|
}; |
|
|
|
|
this.actionToolbar.appendChild(el); |
|
|
|
|
}); |
|
|
|
|
this.overlay.appendChild(this.actionToolbar); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
handleToolbarTool(tool) { |
|
|
|
|
if (tool === 'undo') { |
|
|
|
|
this.undoAnnotation(); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
if (tool === 'fullscreen') { |
|
|
|
|
this.captureFullscreen(); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
this.currentTool = tool; |
|
|
|
|
this.isAnnotating = true; |
|
|
|
|
this.drawCanvas.style.display = 'block'; |
|
|
|
|
this.overlay.style.cursor = tool === 'text' ? 'text' : 'crosshair'; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
addEventListeners() { |
|
|
|
|
this.overlay.addEventListener("mousedown", (e) => { |
|
|
|
|
this.overlay.addEventListener('mousedown', (e) => { |
|
|
|
|
if (this.toolbar.contains(e.target) || this.actionToolbar.contains(e.target)) return; |
|
|
|
|
if (this.isAnnotating && this.currentTool) { |
|
|
|
|
this.startAnnotation(e); |
|
|
|
|
@ -161,7 +204,7 @@ class ScreenshotTool { |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
this.overlay.addEventListener("mousemove", (e) => { |
|
|
|
|
this.overlay.addEventListener('mousemove', (e) => { |
|
|
|
|
if (this.isDrawing) { |
|
|
|
|
this.currentX = e.clientX; |
|
|
|
|
this.currentY = e.clientY; |
|
|
|
|
@ -171,65 +214,77 @@ class ScreenshotTool { |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
this.overlay.addEventListener("mouseup", () => { |
|
|
|
|
this.overlay.addEventListener('mouseup', () => { |
|
|
|
|
if (this.isDrawing) { |
|
|
|
|
this.isDrawing = false; |
|
|
|
|
this.showActionToolbar(); |
|
|
|
|
const w = Math.abs(this.currentX - this.startX); |
|
|
|
|
const h = Math.abs(this.currentY - this.startY); |
|
|
|
|
if (w >= 15 && h >= 15) { |
|
|
|
|
this.isAnnotating = true; |
|
|
|
|
this.drawCanvas.style.display = 'block'; |
|
|
|
|
} |
|
|
|
|
} else if (this.isDrawingAnnotation) { |
|
|
|
|
this.finishAnnotation(); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
this.keydownHandler = (e) => e.key === "Escape" && this.cancel(); |
|
|
|
|
document.addEventListener("keydown", this.keydownHandler); |
|
|
|
|
this.keydownHandler = (e) => e.key === 'Escape' && this.cancel(); |
|
|
|
|
document.addEventListener('keydown', this.keydownHandler); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
updateSelection() { |
|
|
|
|
const ctx = this.selectionCanvas.getContext("2d"); |
|
|
|
|
const ctx = this.selectionCanvas.getContext('2d'); |
|
|
|
|
ctx.clearRect(0, 0, this.selectionCanvas.width, this.selectionCanvas.height); |
|
|
|
|
const x = Math.min(this.startX, this.currentX); |
|
|
|
|
const y = Math.min(this.startY, this.currentY); |
|
|
|
|
const w = Math.abs(this.currentX - this.startX); |
|
|
|
|
const h = Math.abs(this.currentY - this.startY); |
|
|
|
|
|
|
|
|
|
ctx.fillStyle = "rgba(0,0,0,0.6)"; |
|
|
|
|
ctx.fillStyle = 'rgba(0,0,0,0.5)'; |
|
|
|
|
ctx.fillRect(0, 0, this.selectionCanvas.width, this.selectionCanvas.height); |
|
|
|
|
ctx.clearRect(x, y, w, h); |
|
|
|
|
ctx.strokeStyle = "#409eff"; |
|
|
|
|
ctx.lineWidth = 2; |
|
|
|
|
ctx.strokeRect(x, y, w, h); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
showActionToolbar() { |
|
|
|
|
const x = Math.min(this.startX, this.currentX); |
|
|
|
|
const y = Math.min(this.startY, this.currentY); |
|
|
|
|
const w = Math.abs(this.currentX - this.startX); |
|
|
|
|
const h = Math.abs(this.currentY - this.startY); |
|
|
|
|
if (w < 15 || h < 15) return; |
|
|
|
|
ctx.strokeStyle = '#fff'; |
|
|
|
|
ctx.lineWidth = 1; |
|
|
|
|
ctx.strokeRect(x, y, w, h); |
|
|
|
|
|
|
|
|
|
this.isAnnotating = true; |
|
|
|
|
this.drawCanvas.style.display = "block"; |
|
|
|
|
this.actionToolbar.style.display = "flex"; |
|
|
|
|
this.actionToolbar.style.left = `${x + w - 220}px`; |
|
|
|
|
this.actionToolbar.style.top = `${y + h + 10}px`; |
|
|
|
|
ctx.strokeStyle = '#52c41a'; |
|
|
|
|
ctx.lineWidth = 2; |
|
|
|
|
ctx.strokeRect(x + 1, y + 1, w - 2, h - 2); |
|
|
|
|
|
|
|
|
|
this.drawCorner(ctx, x, y, 12); |
|
|
|
|
this.drawCorner(ctx, x + w, y, 12); |
|
|
|
|
this.drawCorner(ctx, x, y + h, 12); |
|
|
|
|
this.drawCorner(ctx, x + w, y + h, 12); |
|
|
|
|
|
|
|
|
|
ctx.fillStyle = '#52c41a'; |
|
|
|
|
ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto'; |
|
|
|
|
ctx.textAlign = 'left'; |
|
|
|
|
ctx.textBaseline = 'top'; |
|
|
|
|
const sizeText = `${Math.round(w)} × ${Math.round(h)}`; |
|
|
|
|
ctx.fillText(sizeText, x + 8, y - 20); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
handleAnnotateTool(tool) { |
|
|
|
|
if (tool === "undo") return this.undoAnnotation(); |
|
|
|
|
this.currentTool = tool; |
|
|
|
|
this.overlay.style.cursor = tool === "text" ? "text" : "crosshair"; |
|
|
|
|
drawCorner(ctx, x, y, size) { |
|
|
|
|
ctx.strokeStyle = '#52c41a'; |
|
|
|
|
ctx.lineWidth = 2; |
|
|
|
|
ctx.beginPath(); |
|
|
|
|
ctx.moveTo(x - size, y); |
|
|
|
|
ctx.lineTo(x, y); |
|
|
|
|
ctx.lineTo(x, y - size); |
|
|
|
|
ctx.stroke(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
startAnnotation(e) { |
|
|
|
|
this.isDrawingAnnotation = true; |
|
|
|
|
this.annotStartX = e.clientX; |
|
|
|
|
this.annotStartY = e.clientY; |
|
|
|
|
if (this.currentTool === "text") this.createTextInput(e.clientX, e.clientY); |
|
|
|
|
if (this.currentTool === 'text') this.createTextInput(e.clientX, e.clientY); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
continueAnnotation(e) { |
|
|
|
|
if (!this.isDrawingAnnotation || !this.currentTool || this.currentTool === "text") return; |
|
|
|
|
const ctx = this.drawCanvas.getContext("2d"); |
|
|
|
|
if (!this.isDrawingAnnotation || !this.currentTool || this.currentTool === 'text') return; |
|
|
|
|
const ctx = this.drawCanvas.getContext('2d'); |
|
|
|
|
ctx.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height); |
|
|
|
|
this.redrawAnnotations(ctx); |
|
|
|
|
|
|
|
|
|
@ -237,61 +292,88 @@ class ScreenshotTool { |
|
|
|
|
const y = this.annotStartY; |
|
|
|
|
const ex = e.clientX; |
|
|
|
|
const ey = e.clientY; |
|
|
|
|
ctx.strokeStyle = "#ff4d4f"; |
|
|
|
|
ctx.fillStyle = "#ff4d4f"; |
|
|
|
|
ctx.lineWidth = 2; |
|
|
|
|
ctx.strokeStyle = this.lineColor; |
|
|
|
|
ctx.fillStyle = this.lineColor; |
|
|
|
|
ctx.lineWidth = this.lineWidth; |
|
|
|
|
|
|
|
|
|
switch (this.currentTool) { |
|
|
|
|
case "rect": ctx.strokeRect(x, y, ex - x, ey - y); break; |
|
|
|
|
case "circle": ctx.beginPath(); ctx.arc(x, y, Math.hypot(ex - x, ey - y), 0, Math.PI * 2); ctx.stroke(); break; |
|
|
|
|
case "pen": ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(ex, ey); ctx.stroke(); this.annotStartX = ex; this.annotStartY = ey; break; |
|
|
|
|
case "arrow": this.drawArrow(ctx, x, y, ex, ey); break; |
|
|
|
|
case "mosaic": this.drawMosaic(ctx, ex, ey); break; |
|
|
|
|
case 'rect': |
|
|
|
|
ctx.strokeRect(x, y, ex - x, ey - y); |
|
|
|
|
break; |
|
|
|
|
case 'circle': |
|
|
|
|
ctx.beginPath(); |
|
|
|
|
ctx.arc(x, y, Math.hypot(ex - x, ey - y), 0, Math.PI * 2); |
|
|
|
|
ctx.stroke(); |
|
|
|
|
break; |
|
|
|
|
case 'pen': |
|
|
|
|
ctx.beginPath(); |
|
|
|
|
ctx.moveTo(x, y); |
|
|
|
|
ctx.lineTo(ex, ey); |
|
|
|
|
ctx.stroke(); |
|
|
|
|
this.annotStartX = ex; |
|
|
|
|
this.annotStartY = ey; |
|
|
|
|
break; |
|
|
|
|
case 'arrow': |
|
|
|
|
this.drawArrow(ctx, x, y, ex, ey); |
|
|
|
|
break; |
|
|
|
|
case 'mosaic': |
|
|
|
|
this.drawMosaic(ctx, ex, ey); |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
finishAnnotation() { |
|
|
|
|
if (!this.isDrawingAnnotation || !this.currentTool || this.currentTool === "text") return; |
|
|
|
|
if (!this.isDrawingAnnotation || !this.currentTool || this.currentTool === 'text') return; |
|
|
|
|
this.isDrawingAnnotation = false; |
|
|
|
|
const ctx = this.drawCanvas.getContext("2d"); |
|
|
|
|
const ctx = this.drawCanvas.getContext('2d'); |
|
|
|
|
this.annotations.push(ctx.getImageData(0, 0, this.drawCanvas.width, this.drawCanvas.height)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
drawArrow(ctx, x1, y1, x2, y2) { |
|
|
|
|
const head = 15; |
|
|
|
|
const head = 12; |
|
|
|
|
const angle = Math.atan2(y2 - y1, x2 - x1); |
|
|
|
|
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); |
|
|
|
|
ctx.beginPath(); |
|
|
|
|
ctx.moveTo(x1, y1); |
|
|
|
|
ctx.lineTo(x2, y2); |
|
|
|
|
ctx.stroke(); |
|
|
|
|
ctx.beginPath(); |
|
|
|
|
ctx.moveTo(x2, y2); |
|
|
|
|
ctx.lineTo(x2 - head * Math.cos(angle - Math.PI / 6), y2 - head * Math.sin(angle - Math.PI / 6)); |
|
|
|
|
ctx.lineTo(x2 - head * Math.cos(angle + Math.PI / 6), y2 - head * Math.sin(angle + Math.PI / 6)); |
|
|
|
|
ctx.closePath(); ctx.fill(); |
|
|
|
|
ctx.closePath(); |
|
|
|
|
ctx.fill(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
drawMosaic(ctx, x, y) { |
|
|
|
|
const s = 8; |
|
|
|
|
const data = ctx.getImageData(x - s / 2, y - s / 2, s, s); |
|
|
|
|
const r = data.data[0], g = data.data[1], b = data.data[2]; |
|
|
|
|
const rectX = Math.floor(x / s) * s; |
|
|
|
|
const rectY = Math.floor(y / s) * s; |
|
|
|
|
const data = ctx.getImageData(rectX, rectY, s, s); |
|
|
|
|
const r = data.data[0]; |
|
|
|
|
const g = data.data[1]; |
|
|
|
|
const b = data.data[2]; |
|
|
|
|
ctx.fillStyle = `rgb(${r},${g},${b})`; |
|
|
|
|
ctx.fillRect(x - s / 2, y - s / 2, s, s); |
|
|
|
|
ctx.fillRect(rectX, rectY, s, s); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
createTextInput(x, y) { |
|
|
|
|
if (this.textInput) this.textInput.remove(); |
|
|
|
|
this.textInput = document.createElement("input"); |
|
|
|
|
this.textInput = document.createElement('input'); |
|
|
|
|
this.textInput.style.cssText = ` |
|
|
|
|
position:fixed;left:${x}px;top:${y}px;border:none;outline:none; |
|
|
|
|
font-size:16px;color:#ff4d4f;background:transparent;z-index:1000002;min-width:100px; |
|
|
|
|
`;
|
|
|
|
|
this.textInput.onkeydown = (e) => { |
|
|
|
|
if (e.key === "Enter") { |
|
|
|
|
if (e.key === 'Enter') { |
|
|
|
|
this.addText(x, y, this.textInput.value); |
|
|
|
|
this.textInput.remove(); this.textInput = null; |
|
|
|
|
this.textInput.remove(); |
|
|
|
|
this.textInput = null; |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
this.textInput.onblur = () => { |
|
|
|
|
if (this.textInput.value) this.addText(x, y, this.textInput.value); |
|
|
|
|
this.textInput?.remove(); this.textInput = null; |
|
|
|
|
this.textInput?.remove(); |
|
|
|
|
this.textInput = null; |
|
|
|
|
}; |
|
|
|
|
this.overlay.appendChild(this.textInput); |
|
|
|
|
this.textInput.focus(); |
|
|
|
|
@ -299,30 +381,33 @@ class ScreenshotTool { |
|
|
|
|
|
|
|
|
|
addText(x, y, text) { |
|
|
|
|
if (!text) return; |
|
|
|
|
const ctx = this.drawCanvas.getContext("2d"); |
|
|
|
|
ctx.font = "16px Arial"; |
|
|
|
|
ctx.fillStyle = "#ff4d4f"; |
|
|
|
|
const ctx = this.drawCanvas.getContext('2d'); |
|
|
|
|
ctx.font = '16px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; |
|
|
|
|
ctx.fillStyle = '#ff4d4f'; |
|
|
|
|
ctx.fillText(text, x, y + 16); |
|
|
|
|
this.annotations.push(ctx.getImageData(0, 0, this.drawCanvas.width, this.drawCanvas.height)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
redrawAnnotations(ctx) { |
|
|
|
|
this.annotations.forEach(d => ctx.putImageData(d, 0, 0)); |
|
|
|
|
this.annotations.forEach((d) => ctx.putImageData(d, 0, 0)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
undoAnnotation() { |
|
|
|
|
if (this.annotations.length === 0) return; |
|
|
|
|
this.annotations.pop(); |
|
|
|
|
const ctx = this.drawCanvas.getContext("2d"); |
|
|
|
|
const ctx = this.drawCanvas.getContext('2d'); |
|
|
|
|
ctx.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height); |
|
|
|
|
this.redrawAnnotations(ctx); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
handleAction(a) { |
|
|
|
|
switch (a) { |
|
|
|
|
case "confirm": this.confirm(); break; |
|
|
|
|
case "cancel": this.cancel(); break; |
|
|
|
|
case "fullscreen": this.captureFullscreen(); break; |
|
|
|
|
case 'confirm': |
|
|
|
|
this.confirm(); |
|
|
|
|
break; |
|
|
|
|
case 'cancel': |
|
|
|
|
this.cancel(); |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -339,10 +424,10 @@ class ScreenshotTool { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
capture(cx, cy, cw, ch) { |
|
|
|
|
const out = document.createElement("canvas"); |
|
|
|
|
const out = document.createElement('canvas'); |
|
|
|
|
out.width = cw * this.dpr; |
|
|
|
|
out.height = ch * this.dpr; |
|
|
|
|
const ctx = out.getContext("2d"); |
|
|
|
|
const ctx = out.getContext('2d'); |
|
|
|
|
ctx.scale(this.dpr, this.dpr); |
|
|
|
|
|
|
|
|
|
const vw = this.video.videoWidth; |
|
|
|
|
@ -354,7 +439,7 @@ class ScreenshotTool { |
|
|
|
|
ctx.drawImage(this.drawCanvas, cx * sx, cy * sy, cw * sx, ch * sy, 0, 0, cw, ch); |
|
|
|
|
|
|
|
|
|
this.cleanup(); |
|
|
|
|
out.toBlob(b => this.onComplete?.(b), "image/png", 1); |
|
|
|
|
out.toBlob((b) => this.onComplete?.(b), 'image/png', 1); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
cancel() { |
|
|
|
|
@ -363,13 +448,13 @@ class ScreenshotTool { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
cleanup() { |
|
|
|
|
document.body.style.overflow = ""; |
|
|
|
|
document.documentElement.style.overflow = ""; |
|
|
|
|
document.body.style.overflow = ''; |
|
|
|
|
document.documentElement.style.overflow = ''; |
|
|
|
|
window.scrollTo(this.scrollX, this.scrollY); |
|
|
|
|
this.overlay?.remove(); |
|
|
|
|
this.textInput?.remove(); |
|
|
|
|
this.stream?.getTracks().forEach(t => t.stop()); |
|
|
|
|
document.removeEventListener("keydown", this.keydownHandler); |
|
|
|
|
this.stream?.getTracks().forEach((t) => t.stop()); |
|
|
|
|
document.removeEventListener('keydown', this.keydownHandler); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|