Spaces:
Running
Running
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> | |
| <title>蒙版王</title> | |
| <style> | |
| /* 适用于手机的样式 */ | |
| @media (max-width: 767px) { | |
| body { | |
| width: 350; | |
| margin: 0 auto; /* 水平居中 */ | |
| } | |
| #canvas { | |
| border: 1px solid #000; | |
| width: 100%; | |
| height: auto; | |
| } | |
| html, body { | |
| overflow-x: hidden; | |
| } | |
| #uploadButton{ | |
| width: 80%; | |
| } | |
| #save{ | |
| width: 20%; | |
| } | |
| .myDiv { | |
| width: 98%; | |
| display: flex; | |
| flex-direction: row; | |
| justify-content: flex-start; | |
| margin: 0px 1%; | |
| flex-wrap: wrap; | |
| } | |
| .myDiv1 { | |
| width: 98%; | |
| display: flex; | |
| flex-direction: row; | |
| justify-content: flex-start; | |
| margin: 0px 1%; | |
| } | |
| .overlay { | |
| left: 20px; | |
| } | |
| } | |
| /* 适用于电脑的样式 */ | |
| @media (min-width: 768px) { | |
| #canvas { | |
| border: 1px solid #000; | |
| width: auto; | |
| height: auto; | |
| } | |
| .myDiv { | |
| display: flex; | |
| flex-direction: row; | |
| justify-content: center; | |
| margin: 0 100px; | |
| width: 512px; | |
| flex-wrap: wrap; | |
| } | |
| .myDiv1 { | |
| display: flex; | |
| flex-direction: row; | |
| justify-content: center; | |
| margin: 0 100px; | |
| width: 512px; | |
| } | |
| .overlay { | |
| left: 100px; | |
| } | |
| } | |
| #circle { | |
| position: absolute; | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| border: 2px solid red; | |
| background-color: transparent; | |
| transition: opacity 1s; | |
| pointer-events: none; | |
| } | |
| .show { | |
| opacity: 1; | |
| } | |
| .hide { | |
| opacity: 0; | |
| } | |
| .overlay { | |
| position: absolute; | |
| top: 82px; | |
| z-index: 10; | |
| /* 设置悬浮控件的样式和尺寸 */ | |
| /* 可以使用 z-index 属性来控制层叠顺序 */ | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="myDiv1"> | |
| <button id="uploadButton" style="box-sizing: border-box; padding: 10px; font-size: 16px; height: 50px; line-height: 30px; overflow: hidden; position: relative;"> | |
| 选择图片 | |
| <input type="file" id="upload" accept="image/*" style="position: absolute; top: 0; left: 0; width: 98%; height: 100%; opacity: 0; cursor: pointer;"> | |
| </button> | |
| <button id="save" style="height: 50px;">保存蒙版</button> | |
| <button id="saveToClipboard" style="height: 50px;">保存蒙版到剪贴板</button> | |
| </div> | |
| <div class="myDiv" id="tools"> | |
| <input type="range" id="brushSizeSlider" style="width: 100%" value="40" min="1" max="150" step="1" > | |
| </div> | |
| <br> | |
| <br> | |
| <div class="myDiv" id="myImg1"> | |
| <canvas id="canvas"></canvas> | |
| <br> | |
| <img id="outputImg"></img> | |
| </div> | |
| <div class="overlay" style="display: flex; flex-direction: row; align-items: flex-start;"> | |
| <label style="margin-bottom: 10px;"> | |
| <input type="radio" name="editMode" value="draw" checked> 画笔模式 | |
| </label> | |
| <label style="margin-bottom: 10px;"> | |
| <input type="radio" name="editMode" value="erase"> 擦除模式 | |
| </label> | |
| <label> | |
| <input type="radio" name="editMode" value="select"> 不编辑 | |
| </label> | |
| </div> | |
| <div id="circle" style="width: 20px; height: 20px; border-radius: 50%; border: 2px solid red;"></div> | |
| <script src="https://telegram.org/js/telegram-web-app.js"></script> | |
| <script> | |
| // Init TWA | |
| Telegram.WebApp.ready(); | |
| Telegram.WebApp.expand(); | |
| Telegram.WebApp.enableClosingConfirmation() | |
| Telegram.WebApp.HapticFeedback.impactOccurred("medium"); | |
| // Event occurs whenever theme settings are changed in the user's Telegram app (including switching to night mode). | |
| Telegram.WebApp.onEvent('themeChanged', function() { | |
| document.documentElement.className = Telegram.WebApp.colorScheme; | |
| }); | |
| window.onload = function() { | |
| var canvas = document.getElementById('canvas'); | |
| var context = canvas.getContext('2d'); | |
| var image = new Image(); | |
| var imageMask = new Image(); | |
| var maskData = null; | |
| var isDrawing = false; | |
| var brushSize = 40; | |
| var intervalHandel = null; | |
| var brushSizeSlider = document.getElementById('brushSizeSlider'); | |
| var editModeRadios = document.getElementsByName('editMode'); | |
| var selectedMode = "draw"; | |
| var isRotate = false; | |
| function isMobile() { | |
| return (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)); | |
| } | |
| for (var i = 0; i < editModeRadios.length; i++) { | |
| editModeRadios[i].addEventListener('change', function() { | |
| // 获取选中的编辑模式值 | |
| selectedMode = this.value; | |
| drawImagesInterval(100); | |
| }); | |
| } | |
| brushSizeSlider.addEventListener('input', function() { | |
| brushSize = parseInt(this.value); | |
| setCircleSize(brushSize); | |
| if (isMobile()){ | |
| var canvasRect = canvas.getBoundingClientRect(); | |
| var scaleX = canvas.width / canvasRect.width; | |
| showCircle(175 + window.scrollX, 200 + window.scrollY, scaleX); | |
| } | |
| }); | |
| function resizeImage(img,s,resizedImage) { | |
| var maxWidth = s; // 最大宽度 | |
| var maxHeight = s; // 最大高度 | |
| var width = img.width; | |
| var height = img.height; | |
| if (width/height < 4/3 && width <= maxWidth && height <= maxHeight) { | |
| return false; | |
| } | |
| if (!isMobile() && width <= maxWidth && height <= maxHeight) { | |
| return false; | |
| } | |
| if (width > maxWidth || height > maxHeight || width/height >= 4/3) { | |
| var ratio = Math.max(maxWidth / width, maxHeight / height); | |
| if (width <= maxWidth && height <= maxHeight){ | |
| ratio = 1; | |
| } | |
| width = Math.floor(width * ratio); | |
| height = Math.floor(height * ratio); | |
| var tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = width; | |
| tempCanvas.height = height; | |
| var ctx = tempCanvas.getContext('2d'); | |
| isRotate = false; | |
| if (width/height >= 4/3 && isMobile()){ | |
| tempCanvas.height = width; | |
| tempCanvas.width = height; | |
| ctx.clearRect(0, 0, tempCanvas.width, tempCanvas.height); | |
| ctx.save(); | |
| ctx.translate(tempCanvas.width / 2, tempCanvas.height / 2); | |
| ctx.rotate(Math.PI / 2); | |
| ctx.drawImage(img, -img.width / 2, -img.height / 2); | |
| ctx.restore(); | |
| isRotate = true; | |
| } | |
| else { | |
| ctx.drawImage(img, 0, 0, width, height); | |
| } | |
| resizedImage.src = tempCanvas.toDataURL(); | |
| return true; | |
| } | |
| return false; | |
| } | |
| function drawImages() { | |
| context.clearRect(0, 0, canvas.width, canvas.height); | |
| context.putImageData(maskData,0,0); | |
| if (selectedMode == "select") { | |
| return; | |
| } | |
| context.globalAlpha = 0.75; // 设置透明度为0.5 | |
| context.drawImage(image, 0, 0); | |
| //context.drawImage(imageMask, 0, 0); | |
| context.globalAlpha = 1; // 恢复透明度为1 | |
| } | |
| function drawImagesInterval(t) { | |
| if (intervalHandel != null){ | |
| drawImagesTimeOut(100); | |
| window.clearInterval(intervalHandel); | |
| intervalHandel = null; | |
| } | |
| intervalHandel = window.setInterval(function() {drawImages();}, t); | |
| } | |
| window.addEventListener('scroll', function(event) { | |
| var scrollTop = window.pageYOffset || document.documentElement.scrollTop; | |
| var overlay = document.querySelector(".overlay"); | |
| overlay.style.top = (scrollTop + 82) + "px"; | |
| }); | |
| function stopDrawImagesInterval() { | |
| if (intervalHandel != null){ | |
| drawImagesTimeOut(100); | |
| window.clearInterval(intervalHandel); | |
| intervalHandel = null; | |
| } | |
| } | |
| function drawImagesTimeOut(t) { | |
| window.setTimeout(function() {drawImages();}, t); | |
| } | |
| function getImageDataB(img) { | |
| context.clearRect(0, 0, canvas.width, canvas.height); | |
| var width = image.width; | |
| var height = image.height; | |
| canvas.width = width; | |
| canvas.height = height; | |
| context.drawImage(img, 0, 0); | |
| var imageData = context.getImageData(0, 0, width, height); | |
| return imageData; | |
| } | |
| function getImageData(img) { | |
| var tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = canvas.width; | |
| tempCanvas.height = canvas.height; | |
| var tempContext = tempCanvas.getContext('2d'); | |
| var width = image.width; | |
| var height = image.height; | |
| tempCanvas.width = width; | |
| tempCanvas.height = height; | |
| tempContext.drawImage(img, 0, 0); | |
| var imageData = tempContext.getImageData(0, 0, width, height); | |
| return imageData; | |
| } | |
| function createMaskImageData(width, height) { | |
| // 创建一个新的ImageData对象 | |
| var imageData = new ImageData(width, height); | |
| // 获取像素数据 | |
| var data = imageData.data; | |
| // 将每个像素设置为黑色 | |
| for (var i = 0; i < data.length; i += 4) { | |
| data[i] = 0; // 设置红色通道 | |
| data[i + 1] = 0; // 设置绿色通道 | |
| data[i + 2] = 0; // 设置蓝色通道 | |
| data[i + 3] = 255; // 设置透明度通道(不透明) | |
| } | |
| return imageData; | |
| } | |
| function getImageDataUrl(imageData) { | |
| var tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = canvas.width; | |
| tempCanvas.height = canvas.height; | |
| var tempContext = tempCanvas.getContext('2d'); | |
| tempContext.putImageData(imageData, 0, 0); | |
| return tempCanvas.toDataURL(); | |
| } | |
| function handleFile(file) { | |
| var reader = new FileReader(); | |
| reader.onload = function(event) { | |
| image.onload = function() { | |
| var resizedImage = new Image(); | |
| resizedImage.onload = _ok; | |
| function _ok(event){ | |
| image = resizedImage; | |
| canvas.width = resizedImage.width; | |
| canvas.height = resizedImage.height; | |
| maskData = createMaskImageData(image.width,image.height); | |
| imageMask.src = getImageDataUrl(maskData); | |
| drawImagesTimeOut(100); | |
| } | |
| if (!resizeImage(image,512,resizedImage)) { | |
| canvas.width = image.width; | |
| canvas.height = image.height; | |
| maskData = createMaskImageData(image.width,image.height); | |
| imageMask.src = getImageDataUrl(maskData); | |
| drawImagesTimeOut(100); | |
| } | |
| } | |
| image.src = event.target.result; | |
| } | |
| reader.readAsDataURL(file); | |
| } | |
| // 上传图片 | |
| document.getElementById("upload").addEventListener('change', function(e) { | |
| var file = e.target.files[0]; | |
| handleFile(file); | |
| }); | |
| // 添加拖拽文件事件监听 | |
| document.addEventListener('dragover', function(e) { | |
| e.preventDefault(); | |
| }); | |
| document.addEventListener('drop', function(e) { | |
| e.preventDefault(); | |
| // 获取拖拽事件中的文件对象 | |
| var file = e.dataTransfer.files[0]; | |
| // 执行与文件上传按钮相同的操作 | |
| handleFile(file); | |
| }); | |
| function inRect(x,y,rect){ | |
| return ( | |
| x >= rect.left && | |
| x <= rect.right && | |
| y >= rect.top && | |
| y <= rect.bottom | |
| ); | |
| } | |
| function calcCanvasOffset(e,canvas) { | |
| var canvasRect = canvas.getBoundingClientRect(); | |
| var scaleX = canvas.width / canvasRect.width; | |
| var scaleY = canvas.height / canvasRect.height; | |
| var offsetX = (e.clientX - canvasRect.left) * scaleX; | |
| var offsetY = (e.clientY - canvasRect.top) * scaleY; | |
| return [offsetX,offsetY,scaleX,scaleY]; | |
| } | |
| // 开始绘制 | |
| function onTouchDown(e) { | |
| if (selectedMode == "select") { | |
| return; | |
| } | |
| var canvasRect = canvas.getBoundingClientRect(); | |
| var [offsetX,offsetY,scaleX,scaleY] = calcCanvasOffset(e,canvas); | |
| //console.log([offsetX,offsetY,scaleX,scaleY]); | |
| if (!inRect(e.clientX,e.clientY,canvasRect)){ | |
| return; | |
| } | |
| if (draw(offsetX, offsetY)) { | |
| isDrawing = true; | |
| drawImagesInterval(100); | |
| } | |
| } | |
| document.addEventListener('touchstart', function(event) { | |
| var touch = event.touches[0]; | |
| onTouchDown(touch); | |
| }); | |
| document.addEventListener('mousedown',function(event){ | |
| onTouchDown(event); | |
| } ); | |
| // 结束绘制 | |
| document.addEventListener('mouseup', function() { | |
| isDrawing = false; | |
| stopDrawImagesInterval(); | |
| }); | |
| // 绘制中 | |
| function onMove(e) { | |
| var [offsetX,offsetY,scaleX,scaleY] = calcCanvasOffset(e,canvas); | |
| if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { | |
| showCircle(e.clientX + window.scrollX, e.clientY + window.scrollY, scaleX); | |
| } else { | |
| showCircle(e.clientX + window.scrollX, e.clientY + window.scrollY, scaleX); | |
| } | |
| var canvasRect = canvas.getBoundingClientRect(); | |
| if (isDrawing) { | |
| //console.log('onMove1'); | |
| if (!inRect(e.clientX,e.clientY,canvasRect)){ | |
| return; | |
| } | |
| draw(offsetX, offsetY); | |
| } | |
| } | |
| canvas.addEventListener('touchmove', function(event) { | |
| if (selectedMode == "select") { | |
| return; | |
| } | |
| event.preventDefault(); | |
| var touch = event.touches[0]; | |
| onMove(touch); | |
| }, { passive: false }); | |
| if (!isMobile()) { | |
| document.addEventListener('mousemove', function(event){ | |
| onMove(event); | |
| }); | |
| } | |
| // 绘制函数 | |
| function draw(x, y) { | |
| if (selectedMode == "selected") { | |
| return false; | |
| } | |
| if (maskData == null) { | |
| return false; | |
| } | |
| var data = maskData.data; | |
| var brushRadius = brushSize / 2; | |
| for (var i = -brushRadius; i <= brushRadius; i++) { | |
| for (var j = -brushRadius; j <= brushRadius; j++) { | |
| var pixelX = Math.round(x + i); | |
| var pixelY = Math.round(y + j); | |
| if (pixelX < 0 || pixelX >= canvas.width || pixelY < 0 || pixelY >= canvas.height) { | |
| continue; | |
| } | |
| var distance = Math.sqrt((pixelX - x) * (pixelX - x) + (pixelY - y) * (pixelY - y)); | |
| if (distance > brushRadius) { | |
| continue; | |
| } | |
| var index = (pixelY * canvas.width + pixelX) * 4; | |
| if (selectedMode == "draw"){ | |
| data[index] = 255; // 设置红色通道为最大值,即白色 | |
| data[index + 1] = 255; // 设置绿色通道为最大值,即白色 | |
| data[index + 2] = 255; | |
| } | |
| else { | |
| data[index] = 0; // 设置红色通道为最大值,即白色 | |
| data[index + 1] = 0; // 设置绿色通道为最大值,即白色 | |
| data[index + 2] = 0; | |
| } | |
| //data[index + 3] = 128; | |
| } | |
| } | |
| //imageMask.src = getImageDataUrl(maskData); | |
| return true; | |
| } | |
| function generateRandomFileName() { | |
| var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; | |
| var length = 10; // 文件名长度 | |
| var fileName = ''; | |
| for (var i = 0; i < length; i++) { | |
| var randomIndex = Math.floor(Math.random() * characters.length); | |
| fileName += characters.charAt(randomIndex); | |
| } | |
| return fileName; | |
| } | |
| function rotateImageData(imageData) { | |
| const { width, height } = imageData; | |
| const rotatedData = new Uint8ClampedArray(width * height * 4); | |
| for (let y = 0; y < height; y++) { | |
| for (let x = 0; x < width; x++) { | |
| const srcIndex = (x + y * width) * 4; | |
| const destIndex = ((width - x - 1) * height + y) * 4; | |
| rotatedData[destIndex] = imageData.data[srcIndex]; | |
| rotatedData[destIndex + 1] = imageData.data[srcIndex + 1]; | |
| rotatedData[destIndex + 2] = imageData.data[srcIndex + 2]; | |
| rotatedData[destIndex + 3] = imageData.data[srcIndex + 3]; | |
| } | |
| } | |
| return new ImageData(rotatedData, height, width); | |
| } | |
| function openImageInNewTab(dataUrl) { | |
| const newTab = window.open(); | |
| newTab.document.write('<html><body style="margin: 0;"><img src="' + dataUrl + '"></body></html>'); | |
| newTab.document.close(); | |
| } | |
| function saveImageAsFile(dataUrl, filename) { | |
| // 将 Data URL 转换为 Blob 对象 | |
| const blob = dataURLToBlob(dataUrl); | |
| // 创建一个链接元素 | |
| const link = document.createElement('a'); | |
| link.href = URL.createObjectURL(blob); | |
| link.download = filename; | |
| // 模拟点击下载链接 | |
| link.click(); | |
| // 释放 URL 对象 | |
| URL.revokeObjectURL(link.href); | |
| } | |
| function dataURLToBlob(dataUrl) { | |
| console.log(dataUrl); | |
| const commaIndex = dataUrl.indexOf(','); | |
| const mime = dataUrl.substring(5, commaIndex); | |
| const base64Data = dataUrl.substring(commaIndex + 1); | |
| const byteCharacters = atob(base64Data); | |
| const byteNumbers = new Array(byteCharacters.length); | |
| for (let i = 0; i < byteCharacters.length; i++) { | |
| byteNumbers[i] = byteCharacters.charCodeAt(i); | |
| } | |
| const byteArray = new Uint8Array(byteNumbers); | |
| return new Blob([byteArray], { type: mime }); | |
| } | |
| // 保存蒙版 | |
| document.getElementById('saveToClipboard').addEventListener('click',async function() { | |
| var data = maskData.data; | |
| var tempCanvas = document.createElement('canvas'); | |
| var ctx = tempCanvas.getContext('2d'); | |
| if (isRotate == true){ | |
| var rData = rotateImageData(maskData); | |
| tempCanvas.height = maskData.width; | |
| tempCanvas.width = maskData.height; | |
| ctx.putImageData(rData, 0, 0); | |
| } | |
| else { | |
| tempCanvas.width = canvas.width; | |
| tempCanvas.height = canvas.height; | |
| ctx.putImageData(maskData, 0, 0); | |
| } | |
| //await navigator.clipboard.writeText(tempCanvas.toDataURL("image/jpeg", 0.9)); | |
| const response = await fetch(tempCanvas.toDataURL("image/png")); | |
| const blob = await response.blob(); | |
| const item = new ClipboardItem({ [blob.type]: blob }); | |
| await navigator.clipboard.write([item]); | |
| const img = document.getElementById('outputImg'); | |
| img.src = tempCanvas.toDataURL("image/jpeg"); | |
| const base64String = img.src.split(',')[1]; | |
| const response2 = await fetch('https://api.imgur.com/3/image', { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': 'Client-ID 955c061744537ff', | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ image: base64String }), | |
| }); | |
| const r = await response2.json() | |
| img.src = r.data.link; | |
| const script = document.createElement('script'); | |
| script.src = "https://telegram.org/js/telegram-widget.js?22"; | |
| script.setAttribute('data-telegram-share-url', r.data.link); | |
| script.async = true; | |
| const myImg1 = document.getElementById('myImg1'); | |
| const firstChild = myImg1.firstChild; | |
| myImg1.insertBefore(script, firstChild); | |
| }); | |
| // 保存蒙版 | |
| document.getElementById('save').addEventListener('click', function() { | |
| var data = maskData.data; | |
| var tempCanvas = document.createElement('canvas'); | |
| var ctx = tempCanvas.getContext('2d'); | |
| if (isRotate == true){ | |
| var rData = rotateImageData(maskData); | |
| tempCanvas.height = maskData.width; | |
| tempCanvas.width = maskData.height; | |
| ctx.putImageData(rData, 0, 0); | |
| } | |
| else { | |
| tempCanvas.width = canvas.width; | |
| tempCanvas.height = canvas.height; | |
| ctx.putImageData(maskData, 0, 0); | |
| } | |
| var link = document.createElement('a'); | |
| link.href = tempCanvas.toDataURL("image/jpeg", 0.9); | |
| link.download = 'mask_' + generateRandomFileName() + '.jpg'; | |
| link.click(); | |
| var oldMode = selectedMode; | |
| selectedMode = "select"; | |
| drawImages(); | |
| selectedMode = oldMode; | |
| //saveImageAsFile(tempCanvas.toDataURL("image/jpeg", 0.9),'mask_' + generateRandomFileName() + '.jpg'); | |
| //openImageInNewTab(tempCanvas.toDataURL("image/jpeg", 0.9)); | |
| }); | |
| // 获取圆圈元素 | |
| var circle = document.getElementById('circle'); | |
| // 根据 brushSize 设置圆圈大小 | |
| function setCircleSize(brushSize) { | |
| } | |
| // 在屏幕上显示圆圈 | |
| function showCircle(x, y, scale) { | |
| circle.style.left = (x - brushSize/scale/2) + 'px'; | |
| circle.style.top = (y - brushSize/scale/2) + 'px'; | |
| circle.style.width = (brushSize / scale) + 'px'; | |
| circle.style.height = (brushSize / scale) + 'px'; | |
| circle.classList.add('show'); | |
| circle.classList.remove('hide'); | |
| // 过一秒后隐藏圆圈 | |
| setTimeout(hideCircle, 1000); | |
| } | |
| // 隐藏圆圈 | |
| function hideCircle() { | |
| circle.classList.remove('show'); | |
| circle.classList.add('hide'); | |
| } | |
| // 手机平台点击事件处理函数 | |
| function handleTouchStart(e) { | |
| // 只处理单指触摸事件 | |
| if (e.touches.length === 1) { | |
| var touch = e.touches[0]; | |
| var x = touch.clientX; | |
| var y = touch.clientY; | |
| var [offsetX,offsetY,scaleX,scaleY] = calcCanvasOffset(touch,canvas); | |
| // 在屏幕上显示圆圈 | |
| showCircle(x + window.scrollX, y + window.scrollY, scaleX); | |
| } | |
| } | |
| // 初始化圆圈大小 | |
| setCircleSize(brushSize); | |
| hideCircle(); | |
| // 设置事件监听 | |
| //setEventListeners(); | |
| } | |
| </script> | |
| </body> | |
| </html> |