這里給大家分享我在網(wǎng)上總結(jié)出來的一些知識,希望對大家有所幫助
生在國旗下,長在春風里!國慶將至,采黎為大家?guī)?定制頭像2.0(國慶頭像),讓我們用代碼的形式為祖國慶生!歡迎大家點贊收藏加關(guān)注哦
前言
想看效果或者想定制春節(jié)頭像的小伙伴請直奔 效果區(qū)域;
想一睹定制頭像2.0小工具的原理及實現(xiàn)思路請耐心閱讀,本文代碼片段較多~
在線定制
?????? 定制頭像入口, 體驗地址 ??????
?????? github項目地址(歡迎?) ??????
喜歡這個小工具的話,動動小手點個star?哦,謝謝!
關(guān)于迭代
繼 定制兔年春節(jié)頭像
上線后,很多小伙伴體驗后第一時間就給了建議、反饋;在大家的幫助下,工具也在不斷的完善;比如導(dǎo)出圖片不夠清晰、不能設(shè)置透明度等等,迭代到1.4.0后,已經(jīng)可以保證正常的使用了,這里采黎給大家說聲謝謝!
由于當時聚焦在兔年、春節(jié)頭像上,工具風格單一,功能還不夠完善,內(nèi)部邏輯有點大材小用等等,于是便有了大版本的定制頭像2.0迭代。
更新內(nèi)容
倉庫名稱
- 由 custom-rabbitImage 改為 custom-avatar
頁面
- 重構(gòu)頁面整體風格,調(diào)整為通用型風格
- 兼容pc、移動端
- 移動端頭像墻采用瀑布流
畫布相關(guān)
- 用戶上傳的原圖做短邊適配,保證不變形
- 優(yōu)化元素控件效果,增加刪除控件
- 優(yōu)化繪制邏輯,減少無用運算。
新增功能
- 增加多主題選項(中秋節(jié)、國慶節(jié)、春節(jié)等,其他傳統(tǒng)節(jié)日敬請期待)
- 增加貼紙效果,可多選、可刪除
- 增加快速切換頭像框功能
- 增加通知功能(xx用戶在3分鐘前定制了國慶頭像)
- 增加分享海報功能
- 增加頭像墻功能,用戶可預(yù)覽他人定制的頭像
修復(fù)已知問題
- 修復(fù)qq瀏覽器無法選擇文件
- 修復(fù)微信瀏覽器無法保存圖片
項目架構(gòu)
vue3 | vite | ts | less | Elemenu UI | eslint | stylelint | husky | lint-staged | commitlint
所需素材
頭像框、貼紙正在設(shè)計中,會一點一點補起來。
中秋主題
國慶主題
春節(jié)主題
思路
基本思路不變,定制兔年春節(jié)頭像中已經(jīng)講過,這里就不再贅述了。
畫布交互邏輯優(yōu)化
這是第一版的邏輯梳理
考慮到定制頭像工具圖層不會過多,功能不會太復(fù)雜,于是 在新版中做了如下優(yōu)化
- 刪除繪制多個圖層邏輯(監(jiān)聽圖層列表變化,進而繪制圖層)
- 繪制頭像框改為主動調(diào)用,減少無用調(diào)用頻次;
- 繪制貼紙為主動調(diào)用,可繪制多個
- 刪除畫布操作同步邏輯(不需要回顯數(shù)據(jù)到頁面,也不用二次繪制,故刪除)
做完上述優(yōu)化后,代碼量明顯下來了;只怪當時沒有過多的思考,就將其他項目的實現(xiàn)方式生搬硬套了。
代碼實現(xiàn)
畫布
- 初始化畫布及控件
const init = () => { /* 初始化控件 */ initFabricControl() /* 初始化畫布 */ Canvas = initCanvas(CanvasId.value, canvasSize, false) // 元素縮放事件 Canvas.on('object:scaling', canvasMouseScaling) } /* 初始化控件 */ const initFabricControl = () => { fabric.Object.prototype.set(control) // 設(shè)置縮放搖桿偏移 fabric.Object.prototype.controls.mtr.offsetY = control.mtrOffsetY // 隱藏不需要的控件 hiddenControl.map((name: string) => (fabric.Object.prototype.controls[name].visible = false)) /* 添加刪除控件 */ const delImgElement = document.createElement('img') delImgElement.src = new URL('./icons/delete.png', import.meta.url).href const size = 52 const deleteControlHandel = (e, transform:any) => { const target = transform.target const canvas = target.canvas canvas.remove(target).renderAll() } const renderDeleteIcon = (ctx:any, left:any, top:any, styleOverride:any, fabricObject:any) => { ctx.save() ctx.translate(left, top) ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle)) ctx.drawImage(delImgElement, -size / 2, -size / 2, size, size) ctx.restore() } fabric.Object.prototype.controls.deleteControl = new fabric.Control({ x: 0.5, y: -0.5, cornerSize: size, offsetY: -48, offsetX: 48, cursorStyle: 'pointer', // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore mouseUpHandler: deleteControlHandel, render: renderDeleteIcon }) }
- 監(jiān)聽原圖(用戶上傳的頭像)改變,并進行短邊適配
/* 更改原圖 */ watch(() => props.bg, async (val) => (await drawBackground(Canvas, val))) /** * @function drawBackground 繪制背景 * @param { Object } Canvas 畫布實例 * @param { String } bgUrl 用戶上傳得原圖片鏈接 */ export const drawBackground = async (Canvas, bgUrl: string) => { return new Promise((resolve: any) => { if (!bgUrl) return resolve() fabric.Image.fromURL(bgUrl, (img: any) => { img.set({ left: Canvas.width / 2, top: Canvas.height / 2, originX: 'center', originY: 'center' }) /* 短邊適配 */ img.width > img.height ? img.scaleToHeight(Canvas.height, true) : img.scaleToWidth(Canvas.width, true) Canvas.setBackgroundImage(img, Canvas.renderAll.bind(Canvas)) resolve() }, { crossOrigin: 'Anonymous' }) }) }
- 繪制頭像框,并隱藏刪除按鈕控件
const frameName = 'frame' /** * @function addFrame 添加頭像框圖層 * @param { String } url 頭像框鏈接 */ const addFrame = async (url = '') => { if (!url) return const frameLayer: any = await drawImg(`${ url }!frame`) frameLayer.set({ left: Canvas.width / 2, top: Canvas.height / 2 }) /* 隱藏刪除按鈕 */ frameLayer.setControlVisible('deleteControl', false) frameLayer.scaleToWidth(Canvas.width, true) frameLayer.name = frameName addOrReplaceLayer(Canvas, frameLayer) }
- 設(shè)置頭像框透明度
/** * @function setFrameOpacity 設(shè)置頭像框透明度 * @param { Number } opacity 透明度 */ const setFrameOpacity = (opacity = 1) => { const frameLayer: any = findCanvasItem(Canvas, frameName)[1] || '' if (!frameLayer) return frameLayer.set({ opacity }) Canvas.renderAll() }
- 繪制貼紙
/** * @function addMark 添加貼紙 * @param { String } url 貼紙鏈接 */ const addMark = async (url) => { if (!url) return const markLayer: any = await drawImg(url) markLayer.set({ left: Canvas.width / 2, top: Canvas.height / 2 }) markLayer.width > markLayer.height ? markLayer.scaleToHeight(200, true) : markLayer.scaleToWidth(200, true) markLayer.name = `mark-${ createUuid() }` addOrReplaceLayer(Canvas, markLayer) }
- 保存圖片,導(dǎo)出base64
/** * @function save 保存效果圖 * @return { String } result base64 保存/預(yù)覽時返回 */ const save = async (): Promise<string> => { return Canvas.toDataURL({ format: 'png', left: 0, top: 0, width: Canvas.width, height: Canvas.height }) }
現(xiàn)在代碼明朗了很多,猶如柳暗花明。
頁面交互
- 用戶上傳圖片,生成本地短鏈,然后繪制原頭像,并默認繪制第一個頭像框。
const uploadFile = async (e: any) => { if (!e.target.files || !e.target.files.length) return ElMessage.warning('上傳失??!') const file = e.target.files[0] if (!file.type.includes('image')) return ElMessage.warning('請上傳正確的圖片格式!') const url = getCreatedUrl(file) ?? '' /* 用戶初次上傳頭像默認選中第一個頭像框 */ if (!originAvatarUrl.value) { originAvatarUrl.value = url selectFrame(0) } else { originAvatarUrl.value = url } (document.getElementById('uploadImg') as HTMLInputElement).value = '' }
- 用戶點擊頭像框或點擊快速切換按鈕,繪制頭像框
/* 快速切換頭像框 */ const changeFrame = (isNext) => { if (!originAvatarUrl.value) return ElMessage.warning('請先上傳頭像!') const frameList = picList[styleIndex.value].frameList if (isNext) { (selectFrameIndex.value === frameList.length - 1) ? selectFrameIndex.value = 0 : (selectFrameIndex.value as number)++ } else { (selectFrameIndex.value === 0) ? selectFrameIndex.value = frameList.length - 1 : (selectFrameIndex.value as number)-- } selectFrame(selectFrameIndex.value as number) } /* 繪制頭像框-調(diào)用畫布繪制函數(shù) */ const selectFrame = (index: number) => { if (!originAvatarUrl.value) return ElMessage.warning('請先上傳頭像!') opacity.value = 1 selectFrameIndex.value = index frameUrl.value = picList[styleIndex.value].frameList[index] DrawRef.value.addFrame(frameUrl.value) }
- 設(shè)置頭像框透明度
const opacity = ref<number>(1) const opacityChange = (num: number) => DrawRef.value.setFrameOpacity(num)
- 點擊貼紙,繪制貼紙
const selectMark = (index: number) => { if (!originAvatarUrl.value) return ElMessage.warning('請先上傳頭像!') const markUrl = picList[styleIndex.value].markList[index] DrawRef.value.addMark(markUrl) }
頁面的交互邏輯相對簡單,一步一步走就ok。
滾動通知動畫效果
這里使用vue的過渡動畫,模擬了滾動的效果, 本質(zhì)就是key變了后,會觸發(fā)彈入彈出效果。
<transition name="notice" mode="out-in"> <div v-if="avatarList && avatarList.length" class="notice" :key="avatarList[noticeIndex].last_modified"> <p> <span style="color: #409eff;">游客{{ (avatarList[noticeIndex].last_modified + '').slice(-5) }} </span> <span style="padding-left: 2px;">{{ calcOverTime(avatarList[noticeIndex].last_modified) }}前</span> <span style="padding-right: 2px;">制作了</span> <span style="color: #f56c6c;">{{ styleEnums[avatarList[noticeIndex].id] }}頭像 </span> <span style="padding-left: 4px;"></span> </p> <img :src="avatarList[noticeIndex].url" alt=""> </div> </transition>
海報功能
這個用html2canvas庫就好了,用正常的css屬性,他都可以實現(xiàn)。
<!-- 生成海報 --> <div id="poster" class="poster"> <!-- 內(nèi)容省略 --> </div>
/* 注意圖片跨域 */ await nextTick(() => { /* 生成海報 */ const posterDom = document.getElementById('poster') as HTMLElement html2canvas(posterDom, { useCORS: true }).then((canvas) => { shareUrl.value = canvas.toDataURL('image/png') shareShow.value = true loading.value = false }) })
移動端瀑布流實現(xiàn)
pc和移動端都是grid布局,我們給移動端的行列份數(shù)隨機,pc端強制設(shè)為1,保證行、列所占的份數(shù)一致就好(定制頭像導(dǎo)出都是正方形的)
grid-auto-flow: dense;?這個樣式是關(guān)鍵,
<div class="wall"> <div class="wall-list"> <el-image v-for="(url, index) in avatarPageUrlList" :key="url" :src="url" :style="{ gridColumn: `span ${ avatarList[index].span}`, gridRow: `span ${ avatarList[index].span }` }" /> </div> </div>
.wall { .wall-list { display: grid; gap: 8px; grid-template-columns: repeat(8, minmax(0, 1fr)); grid-auto-flow: dense; } .wall-more { padding-top: 16px; text-align: center; } } /* pc端不使用瀑布流,強覆蓋行列份數(shù) */ @media only screen and (min-width: 769px) { .wall { .wall-list { > div { grid-row: span 1 !important; grid-column: span 1 !important; } } } }
到這里,基本核心、細節(jié)的點都實現(xiàn)了;若想知道更多代碼設(shè)計、開發(fā)思路,請移步github,代碼已開源。文章來源:http://www.zghlxwxcb.cn/news/detail-710178.html
本文轉(zhuǎn)載于:
https://juejin.cn/post/7283018190594572328
如果對您有所幫助,歡迎您點個關(guān)注,我會定時更新技術(shù)文檔,大家一起討論學習,一起進步。
?文章來源地址http://www.zghlxwxcb.cn/news/detail-710178.html
到了這里,關(guān)于記錄--Vue3 + Fabricjs 定制國慶專屬頭像的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!