nicehero/ManualMask
1
1<!DOCTYPE html>
2<html>
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
6 <title>蒙版王</title>
7 <style>
8 /* 适用于手机的样式 */
9 @media (max-width: 767px) {
10 body {
11 width: 350;
12 margin: 0 auto; /* 水平居中 */
13 }
14 #canvas {
15 border: 1px solid #000;
16 width: 100%;
17 height: auto;
18 }
19 html, body {
20 overflow-x: hidden;
21 }
22 #uploadButton{
23 width: 80%;
24 }
25 #save{
26 width: 20%;
27 }
28 .myDiv {
29 width: 98%;
30 display: flex;
31 flex-direction: row;
32 justify-content: flex-start;
33 margin: 0px 1%;
34 flex-wrap: wrap;
35 }
36 .myDiv1 {
37 width: 98%;
38 display: flex;
39 flex-direction: row;
40 justify-content: flex-start;
41 margin: 0px 1%;
42 }
43 .overlay {
44 left: 20px;
45 }
46 }
47 /* 适用于电脑的样式 */
48 @media (min-width: 768px) {
49 #canvas {
50 border: 1px solid #000;
51 width: auto;
52 height: auto;
53 }
54 .myDiv {
55 display: flex;
56 flex-direction: row;
57 justify-content: center;
58 margin: 0 100px;
59 width: 512px;
60 flex-wrap: wrap;
61 }
62 .myDiv1 {
63 display: flex;
64 flex-direction: row;
65 justify-content: center;
66 margin: 0 100px;
67 width: 512px;
68 }
69 .overlay {
70 left: 100px;
71 }
72 }
73 #circle {
74 position: absolute;
75 width: 10px;
76 height: 10px;
77 border-radius: 50%;
78 border: 2px solid red;
79 background-color: transparent;
80 transition: opacity 1s;
81 pointer-events: none;
82 }
83 .show {
84 opacity: 1;
85 }
86 .hide {
87 opacity: 0;
88 }
89 .overlay {
90 position: absolute;
91 top: 82px;
92 z-index: 10;
93 /* 设置悬浮控件的样式和尺寸 */
94 /* 可以使用 z-index 属性来控制层叠顺序 */
95 }
96 </style>
97</head>
98<body>
99 <div class="myDiv1">
100 <button id="uploadButton" style="box-sizing: border-box; padding: 10px; font-size: 16px; height: 50px; line-height: 30px; overflow: hidden; position: relative;">
101 选择图片
102 <input type="file" id="upload" accept="image/*" style="position: absolute; top: 0; left: 0; width: 98%; height: 100%; opacity: 0; cursor: pointer;">
103 </button>
104 <button id="save" style="height: 50px;">保存蒙版</button>
105 <button id="saveToClipboard" style="height: 50px;">保存蒙版到剪贴板</button>
106 </div>
107 <div class="myDiv" id="tools">
108 <input type="range" id="brushSizeSlider" style="width: 100%" value="40" min="1" max="150" step="1" >
109 </div>
110 <br>
111 <br>
112 <div class="myDiv" id="myImg1">
113 <canvas id="canvas"></canvas>
114 <br>
115 <img id="outputImg"></img>
116 </div>
117 <div class="overlay" style="display: flex; flex-direction: row; align-items: flex-start;">
118 <label style="margin-bottom: 10px;">
119 <input type="radio" name="editMode" value="draw" checked> 画笔模式
120 </label>
121 <label style="margin-bottom: 10px;">
122 <input type="radio" name="editMode" value="erase"> 擦除模式
123 </label>
124 <label>
125 <input type="radio" name="editMode" value="select"> 不编辑
126 </label>
127 </div>
128 <div id="circle" style="width: 20px; height: 20px; border-radius: 50%; border: 2px solid red;"></div>
129 <script src="https://telegram.org/js/telegram-web-app.js"></script>
130 <script>
131 // Init TWA
132 Telegram.WebApp.ready();
133 Telegram.WebApp.expand();
134 Telegram.WebApp.enableClosingConfirmation()
135 Telegram.WebApp.HapticFeedback.impactOccurred("medium");
136 // Event occurs whenever theme settings are changed in the user's Telegram app (including switching to night mode).
137 Telegram.WebApp.onEvent('themeChanged', function() {
138 document.documentElement.className = Telegram.WebApp.colorScheme;
139 });
140window.onload = function() {
141 var canvas = document.getElementById('canvas');
142 var context = canvas.getContext('2d');
143 var image = new Image();
144 var imageMask = new Image();
145 var maskData = null;
146 var isDrawing = false;
147 var brushSize = 40;
148 var intervalHandel = null;
149 var brushSizeSlider = document.getElementById('brushSizeSlider');
150 var editModeRadios = document.getElementsByName('editMode');
151 var selectedMode = "draw";
152 var isRotate = false;
153 function isMobile() {
154 return (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent));
155 }
156 for (var i = 0; i < editModeRadios.length; i++) {
157 editModeRadios[i].addEventListener('change', function() {
158 // 获取选中的编辑模式值
159 selectedMode = this.value;
160 drawImagesInterval(100);
161 });
162 }
163 brushSizeSlider.addEventListener('input', function() {
164 brushSize = parseInt(this.value);
165 setCircleSize(brushSize);
166 if (isMobile()){
167 var canvasRect = canvas.getBoundingClientRect();
168 var scaleX = canvas.width / canvasRect.width;
169 showCircle(175 + window.scrollX, 200 + window.scrollY, scaleX);
170 }
171 });
172 function resizeImage(img,s,resizedImage) {
173 var maxWidth = s; // 最大宽度
174 var maxHeight = s; // 最大高度
175 var width = img.width;
176 var height = img.height;
177 if (width/height < 4/3 && width <= maxWidth && height <= maxHeight) {
178 return false;
179 }
180 if (!isMobile() && width <= maxWidth && height <= maxHeight) {
181 return false;
182 }
183 if (width > maxWidth || height > maxHeight || width/height >= 4/3) {
184 var ratio = Math.max(maxWidth / width, maxHeight / height);
185 if (width <= maxWidth && height <= maxHeight){
186 ratio = 1;
187 }
188 width = Math.floor(width * ratio);
189 height = Math.floor(height * ratio);
190 var tempCanvas = document.createElement('canvas');
191 tempCanvas.width = width;
192 tempCanvas.height = height;
193 var ctx = tempCanvas.getContext('2d');
194 isRotate = false;
195 if (width/height >= 4/3 && isMobile()){
196 tempCanvas.height = width;
197 tempCanvas.width = height;
198 ctx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
199 ctx.save();
200 ctx.translate(tempCanvas.width / 2, tempCanvas.height / 2);
201 ctx.rotate(Math.PI / 2);
202 ctx.drawImage(img, -img.width / 2, -img.height / 2);
203 ctx.restore();
204 isRotate = true;
205 }
206 else {
207 ctx.drawImage(img, 0, 0, width, height);
208 }
209 resizedImage.src = tempCanvas.toDataURL();
210 return true;
211 }
212 return false;
213 }
214
215 function drawImages() {
216 context.clearRect(0, 0, canvas.width, canvas.height);
217 context.putImageData(maskData,0,0);
218 if (selectedMode == "select") {
219 return;
220 }
221 context.globalAlpha = 0.75; // 设置透明度为0.5
222 context.drawImage(image, 0, 0);
223 //context.drawImage(imageMask, 0, 0);
224 context.globalAlpha = 1; // 恢复透明度为1
225 }
226 function drawImagesInterval(t) {
227 if (intervalHandel != null){
228 drawImagesTimeOut(100);
229 window.clearInterval(intervalHandel);
230 intervalHandel = null;
231 }
232 intervalHandel = window.setInterval(function() {drawImages();}, t);
233 }
234 window.addEventListener('scroll', function(event) {
235 var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
236 var overlay = document.querySelector(".overlay");
237 overlay.style.top = (scrollTop + 82) + "px";
238 });
239 function stopDrawImagesInterval() {
240 if (intervalHandel != null){
241 drawImagesTimeOut(100);
242 window.clearInterval(intervalHandel);
243 intervalHandel = null;
244 }
245 }
246 function drawImagesTimeOut(t) {
247 window.setTimeout(function() {drawImages();}, t);
248 }
249 function getImageDataB(img) {
250 context.clearRect(0, 0, canvas.width, canvas.height);
251 var width = image.width;
252 var height = image.height;
253 canvas.width = width;
254 canvas.height = height;
255 context.drawImage(img, 0, 0);
256 var imageData = context.getImageData(0, 0, width, height);
257 return imageData;
258 }
259 function getImageData(img) {
260 var tempCanvas = document.createElement('canvas');
261 tempCanvas.width = canvas.width;
262 tempCanvas.height = canvas.height;
263 var tempContext = tempCanvas.getContext('2d');
264 var width = image.width;
265 var height = image.height;
266 tempCanvas.width = width;
267 tempCanvas.height = height;
268 tempContext.drawImage(img, 0, 0);
269 var imageData = tempContext.getImageData(0, 0, width, height);
270 return imageData;
271 }
272 function createMaskImageData(width, height) {
273 // 创建一个新的ImageData对象
274 var imageData = new ImageData(width, height);
275 // 获取像素数据
276 var data = imageData.data;
277 // 将每个像素设置为黑色
278 for (var i = 0; i < data.length; i += 4) {
279 data[i] = 0; // 设置红色通道
280 data[i + 1] = 0; // 设置绿色通道
281 data[i + 2] = 0; // 设置蓝色通道
282 data[i + 3] = 255; // 设置透明度通道(不透明)
283 }
284 return imageData;
285 }
286 function getImageDataUrl(imageData) {
287 var tempCanvas = document.createElement('canvas');
288 tempCanvas.width = canvas.width;
289 tempCanvas.height = canvas.height;
290 var tempContext = tempCanvas.getContext('2d');
291 tempContext.putImageData(imageData, 0, 0);
292 return tempCanvas.toDataURL();
293 }
294 function handleFile(file) {
295 var reader = new FileReader();
296 reader.onload = function(event) {
297 image.onload = function() {
298 var resizedImage = new Image();
299 resizedImage.onload = _ok;
300 function _ok(event){
301 image = resizedImage;
302 canvas.width = resizedImage.width;
303 canvas.height = resizedImage.height;
304 maskData = createMaskImageData(image.width,image.height);
305 imageMask.src = getImageDataUrl(maskData);
306 drawImagesTimeOut(100);
307 }
308 if (!resizeImage(image,512,resizedImage)) {
309 canvas.width = image.width;
310 canvas.height = image.height;
311 maskData = createMaskImageData(image.width,image.height);
312 imageMask.src = getImageDataUrl(maskData);
313 drawImagesTimeOut(100);
314 }
315 }
316 image.src = event.target.result;
317 }
318 reader.readAsDataURL(file);
319 }
320 // 上传图片
321 document.getElementById("upload").addEventListener('change', function(e) {
322 var file = e.target.files[0];
323 handleFile(file);
324 });
325
326 // 添加拖拽文件事件监听
327 document.addEventListener('dragover', function(e) {
328 e.preventDefault();
329 });
330 document.addEventListener('drop', function(e) {
331 e.preventDefault();
332 // 获取拖拽事件中的文件对象
333 var file = e.dataTransfer.files[0];
334 // 执行与文件上传按钮相同的操作
335 handleFile(file);
336 });
337
338 function inRect(x,y,rect){
339 return (
340 x >= rect.left &&
341 x <= rect.right &&
342 y >= rect.top &&
343 y <= rect.bottom
344 );
345 }
346 function calcCanvasOffset(e,canvas) {
347 var canvasRect = canvas.getBoundingClientRect();
348 var scaleX = canvas.width / canvasRect.width;
349 var scaleY = canvas.height / canvasRect.height;
350 var offsetX = (e.clientX - canvasRect.left) * scaleX;
351 var offsetY = (e.clientY - canvasRect.top) * scaleY;
352 return [offsetX,offsetY,scaleX,scaleY];
353 }
354 // 开始绘制
355 function onTouchDown(e) {
356 if (selectedMode == "select") {
357 return;
358 }
359 var canvasRect = canvas.getBoundingClientRect();
360 var [offsetX,offsetY,scaleX,scaleY] = calcCanvasOffset(e,canvas);
361 //console.log([offsetX,offsetY,scaleX,scaleY]);
362 if (!inRect(e.clientX,e.clientY,canvasRect)){
363 return;
364 }
365 if (draw(offsetX, offsetY)) {
366 isDrawing = true;
367 drawImagesInterval(100);
368 }
369 }
370 document.addEventListener('touchstart', function(event) {
371 var touch = event.touches[0];
372 onTouchDown(touch);
373 });
374 document.addEventListener('mousedown',function(event){
375 onTouchDown(event);
376 } );
377 // 结束绘制
378 document.addEventListener('mouseup', function() {
379 isDrawing = false;
380 stopDrawImagesInterval();
381 });
382 // 绘制中
383 function onMove(e) {
384 var [offsetX,offsetY,scaleX,scaleY] = calcCanvasOffset(e,canvas);
385 if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
386 showCircle(e.clientX + window.scrollX, e.clientY + window.scrollY, scaleX);
387 } else {
388 showCircle(e.clientX + window.scrollX, e.clientY + window.scrollY, scaleX);
389 }
390 var canvasRect = canvas.getBoundingClientRect();
391 if (isDrawing) {
392 //console.log('onMove1');
393 if (!inRect(e.clientX,e.clientY,canvasRect)){
394 return;
395 }
396 draw(offsetX, offsetY);
397 }
398 }
399 canvas.addEventListener('touchmove', function(event) {
400 if (selectedMode == "select") {
401 return;
402 }
403 event.preventDefault();
404 var touch = event.touches[0];
405 onMove(touch);
406 }, { passive: false });
407 if (!isMobile()) {
408 document.addEventListener('mousemove', function(event){
409 onMove(event);
410 });
411 }
412
413
414 // 绘制函数
415 function draw(x, y) {
416 if (selectedMode == "selected") {
417 return false;
418 }
419 if (maskData == null) {
420 return false;
421 }
422 var data = maskData.data;
423 var brushRadius = brushSize / 2;
424
425 for (var i = -brushRadius; i <= brushRadius; i++) {
426 for (var j = -brushRadius; j <= brushRadius; j++) {
427 var pixelX = Math.round(x + i);
428 var pixelY = Math.round(y + j);
429
430 if (pixelX < 0 || pixelX >= canvas.width || pixelY < 0 || pixelY >= canvas.height) {
431 continue;
432 }
433 var distance = Math.sqrt((pixelX - x) * (pixelX - x) + (pixelY - y) * (pixelY - y));
434 if (distance > brushRadius) {
435 continue;
436 }
437 var index = (pixelY * canvas.width + pixelX) * 4;
438 if (selectedMode == "draw"){
439 data[index] = 255; // 设置红色通道为最大值,即白色
440 data[index + 1] = 255; // 设置绿色通道为最大值,即白色
441 data[index + 2] = 255;
442 }
443 else {
444 data[index] = 0; // 设置红色通道为最大值,即白色
445 data[index + 1] = 0; // 设置绿色通道为最大值,即白色
446 data[index + 2] = 0;
447 }
448
449 //data[index + 3] = 128;
450 }
451 }
452 //imageMask.src = getImageDataUrl(maskData);
453 return true;
454 }
455 function generateRandomFileName() {
456 var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
457 var length = 10; // 文件名长度
458 var fileName = '';
459 for (var i = 0; i < length; i++) {
460 var randomIndex = Math.floor(Math.random() * characters.length);
461 fileName += characters.charAt(randomIndex);
462 }
463 return fileName;
464 }
465 function rotateImageData(imageData) {
466 const { width, height } = imageData;
467 const rotatedData = new Uint8ClampedArray(width * height * 4);
468
469 for (let y = 0; y < height; y++) {
470 for (let x = 0; x < width; x++) {
471 const srcIndex = (x + y * width) * 4;
472 const destIndex = ((width - x - 1) * height + y) * 4;
473
474 rotatedData[destIndex] = imageData.data[srcIndex];
475 rotatedData[destIndex + 1] = imageData.data[srcIndex + 1];
476 rotatedData[destIndex + 2] = imageData.data[srcIndex + 2];
477 rotatedData[destIndex + 3] = imageData.data[srcIndex + 3];
478 }
479 }
480
481 return new ImageData(rotatedData, height, width);
482 }
483
484 function openImageInNewTab(dataUrl) {
485 const newTab = window.open();
486 newTab.document.write('<html><body style="margin: 0;"><img src="' + dataUrl + '"></body></html>');
487 newTab.document.close();
488 }
489 function saveImageAsFile(dataUrl, filename) {
490 // 将 Data URL 转换为 Blob 对象
491 const blob = dataURLToBlob(dataUrl);
492
493 // 创建一个链接元素
494 const link = document.createElement('a');
495 link.href = URL.createObjectURL(blob);
496 link.download = filename;
497
498 // 模拟点击下载链接
499 link.click();
500
501 // 释放 URL 对象
502 URL.revokeObjectURL(link.href);
503 }
504
505 function dataURLToBlob(dataUrl) {
506 console.log(dataUrl);
507 const commaIndex = dataUrl.indexOf(',');
508 const mime = dataUrl.substring(5, commaIndex);
509 const base64Data = dataUrl.substring(commaIndex + 1);
510 const byteCharacters = atob(base64Data);
511 const byteNumbers = new Array(byteCharacters.length);
512
513 for (let i = 0; i < byteCharacters.length; i++) {
514 byteNumbers[i] = byteCharacters.charCodeAt(i);
515 }
516
517 const byteArray = new Uint8Array(byteNumbers);
518
519 return new Blob([byteArray], { type: mime });
520 }
521 // 保存蒙版
522 document.getElementById('saveToClipboard').addEventListener('click',async function() {
523 var data = maskData.data;
524 var tempCanvas = document.createElement('canvas');
525 var ctx = tempCanvas.getContext('2d');
526 if (isRotate == true){
527 var rData = rotateImageData(maskData);
528 tempCanvas.height = maskData.width;
529 tempCanvas.width = maskData.height;
530 ctx.putImageData(rData, 0, 0);
531 }
532 else {
533 tempCanvas.width = canvas.width;
534 tempCanvas.height = canvas.height;
535 ctx.putImageData(maskData, 0, 0);
536 }
537 //await navigator.clipboard.writeText(tempCanvas.toDataURL("image/jpeg", 0.9));
538 const response = await fetch(tempCanvas.toDataURL("image/png"));
539 const blob = await response.blob();
540 const item = new ClipboardItem({ [blob.type]: blob });
541 await navigator.clipboard.write([item]);
542
543 const img = document.getElementById('outputImg');
544 img.src = tempCanvas.toDataURL("image/jpeg");
545 const base64String = img.src.split(',')[1];
546 const response2 = await fetch('https://api.imgur.com/3/image', {
547 method: 'POST',
548 headers: {
549 'Authorization': 'Client-ID 955c061744537ff',
550 'Content-Type': 'application/json',
551 },
552 body: JSON.stringify({ image: base64String }),
553 });
554 const r = await response2.json()
555 img.src = r.data.link;
556 const script = document.createElement('script');
557 script.src = "https://telegram.org/js/telegram-widget.js?22";
558 script.setAttribute('data-telegram-share-url', r.data.link);
559 script.async = true;
560 const myImg1 = document.getElementById('myImg1');
561 const firstChild = myImg1.firstChild;
562 myImg1.insertBefore(script, firstChild);
563 });
564
565 // 保存蒙版
566 document.getElementById('save').addEventListener('click', function() {
567 var data = maskData.data;
568 var tempCanvas = document.createElement('canvas');
569 var ctx = tempCanvas.getContext('2d');
570 if (isRotate == true){
571 var rData = rotateImageData(maskData);
572 tempCanvas.height = maskData.width;
573 tempCanvas.width = maskData.height;
574 ctx.putImageData(rData, 0, 0);
575 }
576 else {
577 tempCanvas.width = canvas.width;
578 tempCanvas.height = canvas.height;
579 ctx.putImageData(maskData, 0, 0);
580 }
581 var link = document.createElement('a');
582 link.href = tempCanvas.toDataURL("image/jpeg", 0.9);
583 link.download = 'mask_' + generateRandomFileName() + '.jpg';
584 link.click();
585 var oldMode = selectedMode;
586 selectedMode = "select";
587 drawImages();
588 selectedMode = oldMode;
589 //saveImageAsFile(tempCanvas.toDataURL("image/jpeg", 0.9),'mask_' + generateRandomFileName() + '.jpg');
590 //openImageInNewTab(tempCanvas.toDataURL("image/jpeg", 0.9));
591 });
592 // 获取圆圈元素
593 var circle = document.getElementById('circle');
594
595 // 根据 brushSize 设置圆圈大小
596 function setCircleSize(brushSize) {
597 }
598
599 // 在屏幕上显示圆圈
600 function showCircle(x, y, scale) {
601 circle.style.left = (x - brushSize/scale/2) + 'px';
602 circle.style.top = (y - brushSize/scale/2) + 'px';
603 circle.style.width = (brushSize / scale) + 'px';
604 circle.style.height = (brushSize / scale) + 'px';
605 circle.classList.add('show');
606 circle.classList.remove('hide');
607
608 // 过一秒后隐藏圆圈
609 setTimeout(hideCircle, 1000);
610 }
611
612 // 隐藏圆圈
613 function hideCircle() {
614 circle.classList.remove('show');
615 circle.classList.add('hide');
616 }
617
618 // 手机平台点击事件处理函数
619 function handleTouchStart(e) {
620 // 只处理单指触摸事件
621 if (e.touches.length === 1) {
622 var touch = e.touches[0];
623 var x = touch.clientX;
624 var y = touch.clientY;
625 var [offsetX,offsetY,scaleX,scaleY] = calcCanvasOffset(touch,canvas);
626 // 在屏幕上显示圆圈
627 showCircle(x + window.scrollX, y + window.scrollY, scaleX);
628 }
629 }
630
631
632 // 初始化圆圈大小
633 setCircleSize(brushSize);
634 hideCircle();
635 // 设置事件监听
636 //setEventListeners();
637}
638 </script>
639</body>
640</html>