Vue3 + Fabricjs 实现定制头像2.0

2023-10-28 00:59

本文主要是介绍Vue3 + Fabricjs 实现定制头像2.0,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Vue3 + Fabricjs 实现定制头像2.0🌈

在这里插入图片描述

生在国旗下,长在春风里!国庆将至,采黎为大家带来 定制头像2.0(国庆头像),让我们用代码的形式为祖国庆生!欢迎大家点赞收藏加关注哦

前言

想看效果或者想定制春节头像的小伙伴请直奔 效果区域;

想一睹定制头像2.0小工具的原理及实现思路请耐心阅读,本文代码片段较多~

效果

效果直达车,体验地址

github项目地址(欢迎⭐)

喜欢这个小工具的话,动动小手点个star⭐哦,谢谢!

关于迭代

定制兔年春节头像 上线后,很多小伙伴体验后第一时间就给了建议、反馈;在大家的帮助下,工具也在不断的完善;比如导出图片不够清晰、不能设置透明度等等,迭代到1.4.0后,已经可以保证正常的使用了,这里采黎给大家说声谢谢!

由于当时聚焦在兔年春节头像上,工具风格单一,功能还不够完善,内部逻辑有点大材小用等等,于是便有了大版本的定制头像2.0迭代。

更新内容

仓库名称

  • custom-rabbitImage 改为 custom-avatar

页面

  • 重构页面整体风格,调整为通用型风格
  • 兼容pc、移动端
  • 移动端头像墙采用瀑布流

画布相关

  • 用户上传的原图做短边适配,保证不变形
  • 优化元素控件效果,增加删除控件
  • 优化绘制逻辑,减少无用运算。

新增功能

  • 增加多主题选项(中秋节、国庆节、春节等,其他传统节日敬请期待)
  • 增加贴纸效果,可多选、可删除
  • 增加快速切换头像框功能
  • 增加通知功能(xx用户在3分钟前定制了国庆头像)
  • 增加分享海报功能
  • 增加头像墙功能,用户可预览他人定制的头像

修复已知问题

  • 修复qq浏览器无法选择文件
  • 修复微信浏览器无法保存图片

项目架构

vue3 | vite | ts | less | Elemenu UI | eslint | stylelint | husky | lint-staged | commitlint

所需素材

头像框、贴纸正在设计中,会一点一点补起来。

中秋主题

image.png

国庆主题

image.png

春节主题

image.png

思路

基本思路不变,定制兔年春节头像中已经讲过,这里就不再赘述了。

画布交互逻辑优化

这是第一版的逻辑梳理
flow.png

考虑到定制头像工具图层不会过多,功能不会太复杂,于是 在新版中做了如下优化

  • 删除绘制多个图层逻辑(监听图层列表变化,进而绘制图层)
  • 绘制头像框改为主动调用,减少无用调用频次;
  • 绘制贴纸为主动调用,可绘制多个
  • 删除画布操作同步逻辑(不需要回显数据到页面,也不用二次绘制,故删除)

做完上述优化后,代码量明显下来了;只怪当时没有过多的思考,就将其他项目的实现方式生搬硬套了。

代码实现

画布

  1. 初始化画布及控件
const init = () => {/* 初始化控件 */initFabricControl()/* 初始化画布 */Canvas = initCanvas(CanvasId.value, canvasSize, false)// 元素缩放事件Canvas.on('object:scaling', canvasMouseScaling)
}/* 初始化控件 */
const initFabricControl = () => {fabric.Object.prototype.set(control)// 设置缩放摇杆偏移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).hrefconst size = 52const deleteControlHandel = (e, transform:any) => {const target = transform.targetconst canvas = target.canvascanvas.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-ignoremouseUpHandler: deleteControlHandel,render: renderDeleteIcon})
}
  1. 监听原图(用户上传的头像)改变,并进行短边适配
/* 更改原图 */
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' })})
}
  1. 绘制头像框,并隐藏删除按钮控件
const frameName = 'frame'/*** @function addFrame 添加头像框图层* @param { String } url 头像框链接*/
const addFrame = async (url = '') => {if (!url) returnconst 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 = frameNameaddOrReplaceLayer(Canvas, frameLayer)
}
  1. 设置头像框透明度
/*** @function setFrameOpacity 设置头像框透明度* @param { Number } opacity 透明度*/
const setFrameOpacity = (opacity = 1) => {const frameLayer: any = findCanvasItem(Canvas, frameName)[1] || ''if (!frameLayer) returnframeLayer.set({ opacity })Canvas.renderAll()
}
  1. 绘制贴纸
/*** @function addMark 添加贴纸* @param { String } url 贴纸链接*/
const addMark = async (url) => {if (!url) returnconst 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)
}
  1. 保存图片,导出base64
/*** @function save 保存效果图* @return { String } result base64 保存/预览时返回*/
const save = async (): Promise<string> => {return Canvas.toDataURL({format: 'png',left: 0,top: 0,width: Canvas.width,height: Canvas.height})
}

现在代码明朗了很多,犹如柳暗花明。

页面交互

  1. 用户上传图片,生成本地短链,然后绘制原头像,并默认绘制第一个头像框。
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 = urlselectFrame(0)} else {originAvatarUrl.value = url}(document.getElementById('uploadImg') as HTMLInputElement).value = ''
}
  1. 用户点击头像框或点击快速切换按钮,绘制头像框
/* 快速切换头像框 */
const changeFrame = (isNext) => {if (!originAvatarUrl.value) return ElMessage.warning('请先上传头像!')const frameList =  picList[styleIndex.value].frameListif (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)
}/* 绘制头像框-调用画布绘制函数 */
const selectFrame = (index: number) => {if (!originAvatarUrl.value) return ElMessage.warning('请先上传头像!')opacity.value = 1selectFrameIndex.value = indexframeUrl.value = picList[styleIndex.value].frameList[index]DrawRef.value.addFrame(frameUrl.value)
}
  1. 设置头像框透明度
const opacity = ref<number>(1)
const opacityChange = (num: number) => DrawRef.value.setFrameOpacity(num)
  1. 点击贴纸,绘制贴纸
const selectMark = (index: number) => {if (!originAvatarUrl.value) return ElMessage.warning('请先上传头像!')const markUrl = picList[styleIndex.value].markList[index]DrawRef.value.addMark(markUrl)
}

页面的交互逻辑相对简单,一步一步走就ok。

滚动通知动画效果

这里使用vue的过渡动画,模拟了滚动的效果, 本质就是key变了后,会触发弹入弹出效果。

<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属性,他都可以实现。

<!-- 生成海报 -->
<div id="poster" class="poster"><!-- 内容省略 -->
</div>
/* 注意图片跨域 */
await nextTick(() => {/* 生成海报 */const posterDom = document.getElementById('poster') as HTMLElementhtml2canvas(posterDom, { useCORS: true }).then((canvas) => {shareUrl.value = canvas.toDataURL('image/png')shareShow.value = trueloading.value = false})
})

移动端瀑布流实现

pc和移动端都是grid布局,我们给移动端的行列份数随机,pc端强制设为1,保证行、列所占的份数一致就好(定制头像导出都是正方形的)

grid-auto-flow: dense; 这个样式是关键,

<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端不使用瀑布流,强覆盖行列份数 */
@media only screen and (min-width: 769px) {.wall {.wall-list {> div {grid-row: span 1 !important;grid-column: span 1 !important;}}}
}

到这里,基本核心、细节的点都实现了;若想知道更多代码设计、开发思路,请移步github,代码已开源。

关于开源

这一路走来实属不易,其中苦涩不禁言说;我也深知,这个项目还有许多的不足,这并不能够一蹴而就的;大家在使用的过程中,有什么建议或意见,可以告诉我。这也是我觉得开源项目更有魅力的点,可以集思广益,集百家之众长。希望这个工具能愈发完善,得到更多人的喜欢!

余音

最近有个想法,准备做个创意工具专栏,这个还得再斟酌斟酌。

祝祖国节日快乐,也祝大家国庆快乐哦,再会!

这篇关于Vue3 + Fabricjs 实现定制头像2.0的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/289606

相关文章

pytorch自动求梯度autograd的实现

《pytorch自动求梯度autograd的实现》autograd是一个自动微分引擎,它可以自动计算张量的梯度,本文主要介绍了pytorch自动求梯度autograd的实现,具有一定的参考价值,感兴趣... autograd是pytorch构建神经网络的核心。在 PyTorch 中,结合以下代码例子,当你

SpringBoot集成Milvus实现数据增删改查功能

《SpringBoot集成Milvus实现数据增删改查功能》milvus支持的语言比较多,支持python,Java,Go,node等开发语言,本文主要介绍如何使用Java语言,采用springboo... 目录1、Milvus基本概念2、添加maven依赖3、配置yml文件4、创建MilvusClient

JS+HTML实现在线图片水印添加工具

《JS+HTML实现在线图片水印添加工具》在社交媒体和内容创作日益频繁的今天,如何保护原创内容、展示品牌身份成了一个不得不面对的问题,本文将实现一个完全基于HTML+CSS构建的现代化图片水印在线工具... 目录概述功能亮点使用方法技术解析延伸思考运行效果项目源码下载总结概述在社交媒体和内容创作日益频繁的

前端CSS Grid 布局示例详解

《前端CSSGrid布局示例详解》CSSGrid是一种二维布局系统,可以同时控制行和列,相比Flex(一维布局),更适合用在整体页面布局或复杂模块结构中,:本文主要介绍前端CSSGri... 目录css Grid 布局详解(通俗易懂版)一、概述二、基础概念三、创建 Grid 容器四、定义网格行和列五、设置行

前端下载文件时如何后端返回的文件流一些常见方法

《前端下载文件时如何后端返回的文件流一些常见方法》:本文主要介绍前端下载文件时如何后端返回的文件流一些常见方法,包括使用Blob和URL.createObjectURL创建下载链接,以及处理带有C... 目录1. 使用 Blob 和 URL.createObjectURL 创建下载链接例子:使用 Blob

Vuex Actions多参数传递的解决方案

《VuexActions多参数传递的解决方案》在Vuex中,actions的设计默认只支持单个参数传递,这有时会限制我们的使用场景,下面我将详细介绍几种处理多参数传递的解决方案,从基础到高级,... 目录一、对象封装法(推荐)二、参数解构法三、柯里化函数法四、Payload 工厂函数五、TypeScript

openCV中KNN算法的实现

《openCV中KNN算法的实现》KNN算法是一种简单且常用的分类算法,本文主要介绍了openCV中KNN算法的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录KNN算法流程使用OpenCV实现KNNOpenCV 是一个开源的跨平台计算机视觉库,它提供了各

OpenCV图像形态学的实现

《OpenCV图像形态学的实现》本文主要介绍了OpenCV图像形态学的实现,包括腐蚀、膨胀、开运算、闭运算、梯度运算、顶帽运算和黑帽运算,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起... 目录一、图像形态学简介二、腐蚀(Erosion)1. 原理2. OpenCV 实现三、膨胀China编程(

通过Spring层面进行事务回滚的实现

《通过Spring层面进行事务回滚的实现》本文主要介绍了通过Spring层面进行事务回滚的实现,包括声明式事务和编程式事务,具有一定的参考价值,感兴趣的可以了解一下... 目录声明式事务回滚:1. 基础注解配置2. 指定回滚异常类型3. ​不回滚特殊场景编程式事务回滚:1. ​使用 TransactionT

Android实现打开本地pdf文件的两种方式

《Android实现打开本地pdf文件的两种方式》在现代应用中,PDF格式因其跨平台、稳定性好、展示内容一致等特点,在Android平台上,如何高效地打开本地PDF文件,不仅关系到用户体验,也直接影响... 目录一、项目概述二、相关知识2.1 PDF文件基本概述2.2 android 文件访问与存储权限2.