这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

生在国旗下,长在春风里!国庆将至,采黎为大家带来 定制头像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

所需素材

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

中秋主题

国庆主题

春节主题

思路

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

画布交互逻辑优化

这是第一版的逻辑梳理

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

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

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

代码实现

画布

  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).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
})
}
  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) 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)
}
  1. 设置头像框透明度
/**
* @function setFrameOpacity 设置头像框透明度
* @param { Number } opacity 透明度
*/
const setFrameOpacity = (opacity = 1) => {
const frameLayer: any = findCanvasItem(Canvas, frameName)[1] || '' if (!frameLayer) return frameLayer.set({ opacity })
Canvas.renderAll()
}
  1. 绘制贴纸
/**
* @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)
}
  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 = url
selectFrame(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].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)
} /* 绘制头像框-调用画布绘制函数 */
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)
}
  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 HTMLElement
html2canvas(posterDom, { useCORS: true }).then((canvas) => {
shareUrl.value = canvas.toDataURL('image/png')
shareShow.value = true
loading.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,代码已开源。

本文转载于:

https://juejin.cn/post/7283018190594572328

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

记录--Vue3 + Fabricjs 定制国庆专属头像的更多相关文章

  1. VIM 编辑器 -使用详解记录

    1.什么是 vim? Vim是从 vi 发展出来的一个文本编辑器.代码补完.编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用.简单的来说, vi 是老式的字处理器,不过功能已经很齐全了,但 ...

  2. openerp 经典收藏 记录规则 – 销售只能看到自己的客户,经理可以看到全部(转载)

    记录规则 – 销售只能看到自己的客户,经理可以看到全部 原文地址:http://cn.openerp.cn/record_rule/ OpenERP中的权限管理有四个层次: 菜单级别: 即,不属于指定 ...

  3. 记录规则 – 销售只能看到自己的客户,经理可以看到全部

    OpenERP中的权限管理有四个层次: 菜单级别: 即,不属于指定菜单所包含组的用户看不到该菜单.不安全,只是隐藏菜单,若用户知道菜单ID,仍然可以通过指定URL访问 对象级别: 即,对某个对象是否有 ...

  4. 安居客Android项目架构演进

    入职安居客三年从工程师到Team Leader,见证了Android团队一路走来的发展历程.因此有心将这些记录下来与大家分享,也算是对自己三年来一部分工作的总结.希望对大家有所帮助,更希望能得到大家宝 ...

  5. ImCash:论拥有靠谱数字钱包的重要性!

    数字货币被盗已经不是什么新鲜事,前有交易所币安被黑客攻击,Youbit破产,后有“钓鱼邮件“盗号木马,安全对于数字货币用户来讲至关重要. 现行市场痛点:   2017年9月以太坊Parity钱包的漏洞 ...

  6. 环信easeui集成:坑总结2018

    环信EaseUI 集成,集成不做描述,看文档即可,下面主要谈一些对easeui的个性化需求修改. 该篇文章将解决的问题: 1.如何将App用户体系的用户名和用户头像 显示于环信的easeui 2.如何 ...

  7. 【云栖大会】阿里巴巴集团CTO张建锋:用计算和数据去改变整个世界

    摘要: 当浩瀚的数字化信息能够联网在线,在万物互联网的新世界中,所有东西都可能有感知.变智能,想象一下电表.冰箱.心电图监测仪等设备的信息都能数字化并联网,从城市管理到个人生活,都会迎来翻天覆地的变化 ...

  8. NFC模组,开发NFC功能 仅仅要几条指令的事情

    特点:实现NFC透明传输.内置NFC协议栈,支持UART串口直接读写,用于门禁能够同一时候兼容手机和卡片开门,还能实现动态密钥,读到的NFC数据自己主动串口输出,会串口就能开发NFC,不须要研究LLC ...

  9. Java 前后端分离项目:微人事

    本文适合刚学习完 Java 语言基础的人群,跟着本文可了解和运行项目,本示例是在 Windows 操作系统下演示. 本文作者:HelloGitHub-秦人 大家好!这里是 HelloGitHub 推出 ...

  10. 用 GitHub Action 构建一套 CI/CD 系统

    ​ 缘起 Nebula Graph 最早的自动化测试是使用搭建在 Azure 上的 Jenkins,配合着 GitHub 的 Webhook 实现的,在用户提交 Pull Request 时,加个 r ...

随机推荐

  1. Linux-双网卡绑定bond详解

    1.什么是bond 网卡bond是通过多张物理网卡绑定为一个逻辑网卡,实现本地网卡的冗余,带宽扩容和负载均衡,在生产场景中是一种常用的技术.Kernels 2.4.12及以后的版本均供bonding模 ...

  2. 开源.NetCore通用工具库Xmtool使用连载 - 随机值篇

    [Github源码] <上一篇> 详细介绍了Xmtool工具库中的散列算法类库,今天我们继续为大家介绍其中的随机值类库. 基于系统提供的Random获取随机值方法已经足够简单和易用,本类库 ...

  3. SP21690 POWERUP - Power the Power Up 题解

    题目传送门 前置知识 扩展欧拉定理 解法 直接对 \(a\) 和 \(b^c\) 分讨,跑一遍扩展欧拉定理就行了. 另外由于本题的特殊规定 \(0^0=1\),故需要在当 \(a=0\) 时,对 \( ...

  4. NC16610 [NOIP2009]Hankson的趣味题

    题目链接 题目 题目描述 Hanks博士是BT(Bio-Tech,生物技术)领域的知名专家,他的儿子名叫Hankson.现在,刚刚放学回家的Hankson正在思考一个有趣的问题. 今天在课堂上,老师讲 ...

  5. 用ELK分析每天4亿多条腾讯云MySQL审计日志(2)--EQL

    上一篇介绍了用ELK分析4亿多条审计日志过程,现在介绍如何用Python3分析ES的程序 需要分析的核心库审计数据: 1,950多张表,几十个账号, 2,5种操作类型(select,update,in ...

  6. Java集合框架学习(二) HashSet详解

    HashSet介绍 这个类实现了Set接口,背后是一个hash table(实际上是个HashMap 实例) .它不保证元素的迭代顺序.尤其是,随着时间推 移它不保证某一元素的位置不变.这个类是非线程 ...

  7. 常用JDBC连接池

    如下整理常用JDBC连接池组件. HikariCP 针对不同的JDK需要引入对应的HikariCP,详见:Github项目地址 . 以JDK8为例子,在项目中引入如下依赖: <dependenc ...

  8. python部署-nginx部署带docker的https请求

    使用带docker的服务器配置https需要两层web服务器 首先例如使用https://www.Se7eN_HOU.com进行首页访问,首先会先进入到主服务器里面,经过主服务器的Nginx Web服 ...

  9. HTTP1.0/HTTP1.1/HTTP2.0的演进

    HTTP1.0 短连接,每次请求都需要重新建立连接 不支持断点续传 HTTP1.1 支持长连接,同一个客户端连接可保持长连接,请求可在连接中顺序发出. 查看http请求头中有keepalive 参数 ...

  10. 【Azure App Service】同一个App Service下创建多个测试站点的方式

    问题描述 在一个App Service中,部署多个应用,每个应用相互独立,类似与IIS中在根目录下创建多个子应用的情况. 问题解答 可以的.通过App Service Configuration页面, ...