vue3+ts實現一個canvas畫筆繪圖功能,並匯出相關的切片

风中追风wty發表於2024-12-07
  1 <template>
  2     <el-dialog :model-value="addDialog" width="648" @close="closeDialog" :lock-scroll="true"
  3         :close-on-click-modal="false">
  4         <template #header="{ close, titleId, titleClass }">
  5             <div class="my-header">
  6                 <div :id="titleId" :class="titleClass" class="my-header-title"><span>區域性重繪</span></div>
  7                 <p>繪製蒙版區域,AI將在蒙版區域內進行畫面重繪</p>
  8             </div>
  9         </template>
 10         <div class="my-content">
 11             <div class="my-content-menu">
 12 
 13                 <div class="my-content-menu-item" v-for="(item, index) in btnList" :key="index"
 14                     :class="{ active: currentKey == item.id }" @click="changeMenu(item)">
 15                     <el-tooltip :visible="item.visible" placement="top" effect="light" :offset="20">
 16                         <template #content>
 17                             <div style="display: flex;flex-direction: column;gap:8px;padding:0px 8px 8px 8px;">
 18                                 <span>畫筆大小</span>
 19                                 <div>
 20                                     <Pencil @updateNum="updateNum"></Pencil>
 21                                 </div>
 22                             </div>
 23                         </template>
 24                         <div style="display: flex;align-items: center;gap:8px;">
 25                             <SvgIcon :name="item.icon" class="my-content-menu-item-icon"></SvgIcon>
 26                             <span>{{ item.name }}</span>
 27                             <el-tooltip placement="bottom" effect="light" :offset="20">
 28                                 <template #content>
 29                                     <div style="display: flex;flex-direction: column;gap:8px;padding:6px 8px 8px 8px;">
 30                                         <span style="font-size:14px">上傳遮罩圖片,精準控制重繪區域</span>
 31                                         <p style="color:#797979">白色部分為重繪區域,遮罩圖需要與原圖尺寸
 32                                             <br>保持一致。例如下方圖片為手指區域重繪。
 33                                         </p>
 34                                         <div style="display: flex;justify-content: space-between;">
 35                                             <img src="../../../../../assets/function/aipaint/shadow-source.png" />
 36                                             <img src="../../../../../assets/function/aipaint/shadow-pic.png" />
 37                                         </div>
 38                                     </div>
 39                                 </template>
 40                                 <SvgIcon name="aipaint-why" class="my-content-menu-item-icon" v-if="item.id == 2">
 41                                 </SvgIcon>
 42                             </el-tooltip>
 43                         </div>
 44                     </el-tooltip>
 45                    
 46                 </div>
 47                 <input type="file" @change.self="uploadShadowPic" style="display: none" ref="shadowInput" />
 48             </div>
 49             <div class="my-content-image">
 50                 <div class="canvascontainer">
 51                     <!-- <img :src="proxy.$loginUrl + props.reviewUrl" alt=""> -->
 52                     <canvas ref="backgroundCanvas" width="600px" height="600px"></canvas>
 53                     <canvas ref="drawingCanvas" @mousedown="startDrawing" @mousemove="draw" @mouseup="stopDrawing"
 54                         width="600px" height="600px"></canvas>
 55                     <!-- <canvas ref="resultCanvas" width="500" height="500" v-if="showResult"></canvas> -->
 56                 </div>
 57 
 58             </div>
 59         </div>
 60         <div class="my-button">
 61             <div class="my-button-left" @click="closeDialog">取消關閉</div>
 62             <div class="my-button-right" @click="finishReview">完成繪製</div>
 63         </div>
 64 
 65     </el-dialog>
 66 </template>
 67 <script lang="ts" setup>
 68 import { ref, getCurrentInstance, onMounted, nextTick } from 'vue';
 69 import SvgIcon from "@/components/index.vue";
 70 import Pencil from './pencil.vue';
 71 const currentKey = ref(100);
 72 import { ElMessageBox, ElMessage } from "element-plus";
 73 const drawingCanvas: any = ref<HTMLCanvasElement | null>(null);
 74 const backgroundCanvas: any = ref(null);
 75 const ctx: any = ref<CanvasRenderingContext2D | null>(null);
 76 const bgCtx: any = ref<CanvasRenderingContext2D | null>(null);
 77 const popoverRef = ref()
 78 const drawing = ref(false);
 79 const color = ref('#85E822')
 80 const size = ref(2);
 81 const imgs: any = ref([]);
 82 const showResult: any = ref(false);
 83 const backFlag: any = ref(false);
 84 const imgWidth: any = ref(0);
 85 const imgHeight: any = ref(0);
 86 const startX: any = ref(0);
 87 const startY: any = ref(0);
 88 const localUrl:any = ref('http://localhost:8888/api')
 89 const shadowInput: any = ref(null);
 90 import { upLoadFile } from "@/api/api";
 91 const isDraw = ref(false)
 92 let image = new Image();
 93 const { proxy } = getCurrentInstance() as any;
 94 const btnList = ref([{
 95     name: '撤銷',
 96     icon: 'aipaint-review-cancel',
 97     id: 0,
 98     visible: false,
 99 },
100 {
101     name: '畫筆',
102     icon: 'aipaint-review-paint',
103     id: 1,
104     visible: false,
105 },
106 {
107     name: '上傳遮罩',
108     icon: 'aipaint-review-shadow',
109     id: 2,
110     visible: false
111 },
112 {
113     name: '清除繪製',
114     icon: 'aipaint-review-clear',
115     id: 3,
116     visible: false,
117 }])
118 const video: any = ref('');
119 const props = defineProps({
120     addDialog: {
121         type: Boolean,
122         default: false,
123     },
124     title: {
125         default: '',
126         type: String
127     },
128     reviewUrl: {
129         default: '',
130         type: String
131     }
132 });
133 onMounted(() => {
134 
135 
136     // let canvas = document.getElementById("canvas");
137     // canvas!.width = video.offsetWidth;
138     // canvas!.height = video.offsetHeight;
139 
140 })
141 const emit = defineEmits(["update:addDialog", 'getImgUrl']);
142 const closeDialog = () => {
143     btnList.value.map((item) => {
144         item.visible = false;
145     })
146     emit("update:addDialog", false);
147 };
148 const updateNum = (val: any) => {
149     size.value = val / 10
150 }
151 const beforeUpload = (file: any) => {
152     // 檢查檔案型別是否為JPEG或PNG
153     const isJPG = ["image/jpeg", "image/png", "image/jpg"].includes(file.type);
154     if (!isJPG) {
155         // 如果檔案型別不是JPEG或PNG,顯示錯誤提示訊息
156         ElMessage({
157             message: "上傳圖片只能是JPG/png格式!",
158             type: "error",
159         });
160     }
161     // 返回檔案型別檢查結果,決定是否允許上傳
162     return isJPG;
163 };
164 const uploadShadowPic = async (event: any) => {
165     const file = event.target.files[0];
166     if (beforeUpload(file)) {
167         const data: any = await upLoadFile({ icon: file });
168 
169         console.log(data);
170         if (data.status == 1) {
171 
172             const url = data.file_path;
173             emit('getImgUrl', url);
174             ElMessage({
175                 message: "上傳成功",
176                 type: "success",
177             })
178             closeDialog()
179             // picMadeUrl.value = data.file_path;
180         }
181     }
182 }
183 // const finishReview = () => {
184 //     const tempCanvas = document.createElement('canvas');
185 //     tempCanvas.width = imgWidth.value;
186 //     tempCanvas.height = imgHeight.value;
187 //     const tempCtx = tempCanvas.getContext('2d');
188 //     console.log(tempCtx,'ccc')
189 //     const backgroundImage = new Image();
190 //     console.log(backgroundImage)
191 //     backgroundImage.src = '../../../../../assets/function/aipaint/template.png' ;
192 //     // backgroundImage.onload = () => {
193 //     //     console.log(tempCtx,'ggg')
194 //     //     tempCtx!.drawImage(backgroundImage, 0, 0, backgroundImage.width, backgroundImage.height, 0, 0, imgWidth.value, imgHeight.value);
195 //     // }
196 //     tempCtx!.drawImage(backgroundImage, startX.value, startY.value, imgWidth.value, imgHeight.value);
197 //     // tempCtx!.drawImage(canvas.value,  0, 0, canvas.value?.width || 0, canvas.value?.height || 0);
198 
199 // tempCtx!.drawImage(canvas.value, startX.value, startY.value, imgWidth.value, imgHeight.value, 0, 0, imgWidth.value, imgHeight.value);
200 //     const dataURL = tempCanvas.toDataURL('image/png');
201 //     const url = getBase64Url(dataURL)
202 //     
203 //     // closeDialog()
204 // }
205 const finishReview = () => {
206     const bgCanvas = backgroundCanvas.value;
207 
208     const finalCanvas = document.createElement('canvas');
209     finalCanvas.width = bgCanvas.width;
210     finalCanvas.height = bgCanvas.height;
211     const finalCtx = finalCanvas.getContext('2d');
212 
213     // 繪製背景
214     // finalCtx!.drawImage(bgCanvas, 0, 0);
215     // 繪製使用者繪製的內容
216     finalCtx!.fillStyle = 'black';
217     finalCtx!.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
218     finalCtx!.drawImage(drawingCanvas.value, startX.value, startY.value, imgWidth.value, imgHeight.value);
219 
220     // 儲存最終結果
221     const dataURL = finalCanvas.toDataURL('image/png');
222     const file = base64ToFile(dataURL, 'drawing.png');
223     console.log(dataURL)
224     uploadFiles(file);
225     clearCanvas()
226     // const url = getBase64Url(dataURL);
227     // console.log(url)
228 
229     // emit('getImgUrl', url)
230 
231 }
232 const uploadFiles = async (file: any) => {
233     // beforeFileUpload
234 
235     const data: any = await upLoadFile({ icon: file });
236     if (data.status == 1) {
237         const url = data.file_path;
238         emit('getImgUrl', url);
239         closeDialog()
240     }
241     // if (beforeUpload(file)) {
242     //     const data: any = await upLoadFile({ icon: file });
243 
244     //     console.log(data);
245     //     if (data.status == 1) {
246     //         reviewUrl.value = data.file_path;
247     //         reWriteRef.value.initData(reviewUrl.value);
248     //         showRewrite.value = true;
249     //     }
250     // }
251 };
252 function base64ToFile(base64: any, fileName: any) {
253     // 將base64按照 , 進行分割 將字首  與後續內容分隔開
254     let data = base64.split(',');
255     // 利用正規表示式 從字首中獲取圖片的型別資訊(image/png、image/jpeg、image/webp等)
256     let type = data[0].match(/:(.*?);/)[1];
257     // 從圖片的型別資訊中 獲取具體的檔案格式字尾(png、jpeg、webp)
258     let suffix = type.split('/')[1];
259     // 使用atob()對base64資料進行解碼  結果是一個檔案資料流 以字串的格式輸出
260     const bstr = window.atob(data[1]);
261     // 獲取解碼結果字串的長度
262     let n = bstr.length
263     // 根據解碼結果字串的長度建立一個等長的整形數字陣列
264     // 但在建立時 所有元素初始值都為 0
265     const u8arr = new Uint8Array(n)
266     // 將整形陣列的每個元素填充為解碼結果字串對應位置字元的UTF-16 編碼單元
267     while (n--) {
268         // charCodeAt():獲取給定索引處字元對應的 UTF-16 程式碼單元
269         u8arr[n] = bstr.charCodeAt(n)
270     }
271     // 利用建構函式建立File檔案物件
272     // new File(bits, name, options)
273     const file = new File([u8arr], `${fileName}.${suffix}`, {
274         type: type
275     })
276     // 將File檔案物件返回給方法的呼叫者
277     return file;
278 }
279 
280 const getBase64Url = (pic: any) => {
281     const blob = base64ImgtoFile(pic)
282     const blobUrl = window.URL.createObjectURL(blob);
283     console.log(blobUrl)
284     return blobUrl
285 
286 }
287 function base64ImgtoFile(dataurl: any, filename = 'file') {
288     //將base64格式分割:['data:image/png;base64','XXXX']
289     const arr = dataurl.split(',')
290     // .*? 表示匹配任意字元到下一個符合條件的字元 剛好匹配到:
291     // image/png
292     const mime = arr[0].match(/:(.*?);/)[1]  //image/png
293     //[image,png] 獲取圖片型別字尾
294     const suffix = mime.split('/')[1] //png
295     const bstr = atob(arr[1])   //atob() 方法用於解碼使用 base-64 編碼的字串
296     let n = bstr.length
297     const u8arr = new Uint8Array(n)
298     while (n--) {
299         u8arr[n] = bstr.charCodeAt(n)
300     }
301     return new File([u8arr], `${filename}.${suffix}`, {
302         type: mime
303     })
304 }
305 const changeMenu = (item: any) => {
306     if (item.id == 1 || item.id == 2) {
307         currentKey.value = item.id
308     }
309     if (item.id == 3) {
310         clearCanvas()
311         initData(props.reviewUrl)
312     }
313     if (item.id == 1) {
314         // drawing.value = true;
315         isDraw.value = true;
316 
317         item.visible = true;
318     } else {
319         btnList.value.map((item) => {
320             item.visible = false;
321         })
322     }
323     if (item.id == 0) {
324         isDraw.value = false;
325         currentKey.value = 100;
326 
327         undo()
328     } else if (item.id == 2) {
329         console.log('xxxx')
330         shadowInput.value.click();
331     }
332 }
333 const initData = (val: any) => {
334 
335 
336     nextTick(() => {
337         console.log(proxy.$loginUrl + val)
338         if (backgroundCanvas.value) {
339             bgCtx.value = backgroundCanvas.value.getContext('2d');
340 
341             image.src = proxy.$loginUrl + val;
342             console.log(image)
343             image.onload = () => {
344 
345                 if (image.width < image.height) {
346                     imgWidth.value = image.width * (backgroundCanvas.value!.height / image.height);
347                     imgHeight.value = image.height * (backgroundCanvas.value!.height / image.height);
348                 } else {
349                     imgWidth.value = image.width * (backgroundCanvas.value!.width / image.width);
350                     imgHeight.value = image.height * (backgroundCanvas.value!.width / image.width);
351                 }
352 
353 
354                 startX.value = (backgroundCanvas.value!.width - imgWidth.value) / 2;
355                 startY.value = (backgroundCanvas.value!.height - imgHeight.value) / 2;
356                 // canvas.value!.width = imgWidth;
357                 // canvas.value!.height = imgHeight;
358                 bgCtx.value!.drawImage(image, startX.value, startY.value, imgWidth.value, imgHeight.value);
359             };
360         }
361 
362     })
363 }
364 const startDrawing = (event: MouseEvent) => {
365     ctx.value = drawingCanvas.value.getContext('2d');
366     ctx.value.fillStyle = 'white';
367 
368     if (isDraw.value) {
369         drawing.value = true;
370         backFlag.value = true;
371         ctx.value!.beginPath();
372         ctx.value!.moveTo(event.offsetX, event.offsetY);
373         let obj = ctx.value!.getImageData(0, 0, drawingCanvas.value!.width, drawingCanvas.value!.height);
374         imgs.value.push(obj)
375     }
376 
377 
378 
379 
380 };
381 
382 const undo = () => {
383     if (backFlag.value) {
384         let img = imgs.value.pop()
385         ctx.value!.putImageData(img, 0, 0);
386         // 撤銷按鈕的樣式
387         if (imgs.value.length > 0) {
388 
389             backFlag.value = true
390         } else {
391 
392             backFlag.value = false
393         }
394     }
395 
396 
397 };
398 const draw = (event: MouseEvent) => {
399     if (!drawing.value) return;
400 
401     ctx.value!.lineWidth = size.value;
402     // ctx.value!.strokeStyle = color.value;
403     ctx.value!.strokeStyle = '#FFFFFF';
404     ctx.value!.lineTo(event.offsetX, event.offsetY);
405     ctx.value!.stroke();
406 };
407 
408 const stopDrawing = () => {
409     drawing.value = false;
410     ctx.value!.closePath();
411 
412 };
413 
414 const clearCanvas = () => {
415     ctx.value!.clearRect(0, 0, drawingCanvas.value!.width, drawingCanvas.value!.height)
416     bgCtx.value.clearRect(0, 0, backgroundCanvas.value!.width, backgroundCanvas.value!.height)
417 }
418 defineExpose({
419     initData,
420 
421 })
422 
423 </script>
424 <style lang="scss">
425 .el-text {
426     width: 100%;
427     max-width: 928px;
428     // min-width: 800px;
429     // min-height: 24px;
430     // margin-left: 64px;
431     border: none !important;
432     --el-input-border-color: none !important;
433     --el-input-focus-border: none !important;
434     --el-input-border: none !important;
435     --el-input-focus-border-color: none !important;
436     --el-input-hover-border-color: none !important;
437     --el-input-clear-hover-color: none !important;
438     border-radius: 0px 0px 8px 8px;
439 
440     .el-textarea__inner {
441         // height: auto !important;
442         // min-height: 24px !important;
443         font-size: 14px;
444         padding: 8px 8px 8px 8px;
445 
446         min-height: 240px !important;
447         // max-height: 150px !important;
448         line-height: 24px;
449         background-color: rgba(0, 0, 0, 0.03);
450         // line-height: 56px !important;
451         border: none;
452         border: 1px solid rgba(0, 0, 0, 0.01);
453         border-radius: 8px;
454         // border-radius: 8px;
455     }
456 }
457 </style>
458 <style lang="scss" scoped>
459 .canvascontainer {
460     max-width: 600px;
461     max-height: 600px;
462     position: relative;
463 
464 }
465 
466 canvas {
467     position: absolute;
468     top: 50%;
469     left: 50%;
470 
471     transform: translate(-50%, -50%);
472     display: block;
473 }
474 
475 .my-header {
476     display: flex;
477     flex-direction: row;
478     align-items: center;
479     // justify-content: space-between;
480     gap: 24px;
481     padding: 0px 0px 12px 0px;
482     margin-left: 12px;
483     border-bottom: 1px solid #eee;
484 
485     &-title {
486         font-size: 14px;
487         color: #1f1f1f;
488         position: relative;
489         display: flex;
490 
491         font-size: 20px;
492 
493 
494         span {
495             position: relative;
496             z-index: 100;
497 
498         }
499 
500     }
501 
502     &-title:after {
503         content: "";
504         position: absolute;
505         bottom: 0px;
506         width: 100%;
507         left: 0px;
508         height: 8px;
509         background: linear-gradient(to right,
510                 rgba(133, 232, 34, 1),
511                 rgba(133, 232, 34, 0));
512     }
513 
514     p {
515         color: #797979;
516         font-size: 14px;
517     }
518 }
519 
520 .my-content {
521     // display: flex;
522     // gap: 16px;
523     padding: 0px 10px;
524 
525     &-menu {
526         display: flex;
527         gap: 16px;
528         justify-content: space-between;
529         align-items: center;
530         width: 100%;
531 
532         &-item {
533             width: 25%;
534             height: 40px;
535             display: flex;
536             justify-content: center;
537             align-items: center;
538             gap: 4px;
539             background-color: rgba(0, 0, 0, 0.03);
540             font-size: 14px;
541             border-radius: 4px;
542             color: #4c4c4c;
543             cursor: pointer;
544 
545             &-icon {
546                 width: 16px;
547                 height: 16px;
548                 // color:#4c4c4c;
549             }
550         }
551 
552         &-item:hover {
553             background-color: rgba(133, 232, 34, 0.1);
554             color: #65C115;
555 
556         }
557 
558         .active {
559             background-color: rgba(133, 232, 34, 0.1);
560             color: #65C115;
561         }
562     }
563 
564     &-image {
565         width: 100%;
566         height: 600px;
567         margin-top: 16px;
568         border: 1px solid #eee;
569         border-radius: 8px;
570         display: flex;
571         justify-content: center;
572         align-items: center;
573 
574         &-canvas {
575             max-width: 600px;
576             max-height: 600px;
577 
578         }
579 
580 
581 
582 
583     }
584 }
585 
586 .my-button {
587     display: flex;
588     flex-direction: row;
589     justify-content: center;
590     padding: 16px 0px 0px 0px;
591     gap: 16px;
592     font-size: 14px;
593     color: #1f1f1f;
594 
595     &-left {
596         // width:
597         background-color: #F2F3F6;
598         border-radius: 4px;
599         width: 288px;
600         height: 40px;
601         text-align: center;
602         line-height: 40px;
603         cursor: pointer;
604     }
605 
606     &-right {
607         background-color: #85E822;
608         border-radius: 4px;
609         width: 288px;
610         height: 40px;
611         line-height: 40px;
612         text-align: center;
613         cursor: pointer;
614     }
615 }
616 </style>

相關文章