除夕将至,快来定制你的春节🐇头像叭🌈
我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛”
小年已至,除夕还会远嘛!新年向我们招手,便用代码与她来一次新年邂逅吧~
前言
想看效果或者想定制春节头像的小伙伴请直奔 效果区域;
想一睹定制兔年春节头像小工具的原理及实现思路请耐心阅读,本文代码片段较多~
效果
所有言语都过于苍白,3,2,1,上链接~ 🤣
效果直达车
效果直达车(github备用链接)
github项目地址(欢迎⭐)
代码已开源,如果喜欢这个项目请动动小手点个star⭐,谢谢!
她的前世今生
写完2023年的第一篇文章——🐇年新春,快来领取你的春节全屏动效🌈,新年才姗姗来迟。觉得还是缺少些许年味,至少在没有浏览器的地方还是感觉不到的。冥思苦想,盯着自己的灰色头像发呆;突然就想到了她——春节头像,头像已经悄无声息的渗透在了我们生活的各个角落;不光我自己可以使用,我的家人、朋友、在外漂泊的诸位都可以使用。于是便诞生了定制兔年春节头像 小工具。
项目架构
vue3 | vite | ts | less | Elemenu UI | eslint | stylelint | husky | lint-staged | commitlint
完整版vue3工程模板额外有vue-router、pinia,github项目地址(vue3模板)
思路
原理
创建画布,用户头像作为背景,效果图盖在背景上,调整后合成图片
交互
用户上传原头像,选择喜欢的效果图;点击预览查看效果,保存头像。
实现
封装画布组件(使用fabric.js),其具有绘制、调整、预览、导出图片等功能;在页面引入组件,通过props传递参数,组件通信等实现定制兔年春节头像工具的功能。
代码
本文以阐述思路为主,有关于fabric.js相关内容会在后面出几期详细文章(fabric.js内容较多,知识点较细,恐误佳人😉)。fabric.js官网入口 fabric.js官网
RabbitLi(画布组件)
目录结构
ok,我们先搞清楚这些文件的作用。代码注释很详细,我就不过多赘述了,上代码🤣
- config/canvas.ts 画布配置文件
/**
* @file canvas.ts 画布基础配置
* @description 画布大小配置等
*/
import { judgePC } from '@c/RabbitLi/modules/common'
export interface canvasType {
width: number,
height: number
}
/**
* @desc 操纵控件
* @param { Object } canvasSize 画布尺寸 { width, height }
*/
export const canvasSize: canvasType = {
width: judgePC() ? 400 : 320,
height: judgePC() ? 400 : 320
}
- config/control.ts 画布控件配置文件
/**
* @file control.ts 控件基础配置
* @description 控件样式、显示隐藏等
*/
/**
* @desc 操纵控件
* @param { Object } controlMobile
* @param { Boolean } transparentCorners 边角控件是否透明
* @param { String } cornerStrokeColor 边角描边颜色
* @param { String } cornerColor 边角颜色
* @param { String } cornerStyle 边角形状 rect | circle
* @param { Number } cornerSize 边角大小
* @param { Number } borderScaleFactor 描边边框大小
* @param { Number } padding 控件距离内容的边距
* @param { Number } mtrOffsetY 旋转摇杆偏移
*/
export const controlMobile: any = {
transparentCorners: false,
cornerStrokeColor: '#00BFFF',
cornerColor: '#00BFFF',
cornerStyle: 'rect',
cornerSize: 10,
borderScaleFactor: 2,
borderColor: '#00BFFF',
padding: 8,
mtrOffsetY: -40
}
/**
* @desc 操纵控件
* @param { Object } controlPc
* @param { Boolean } transparentCorners 边角控件是否透明
* @param { String } cornerStrokeColor 边角描边颜色
* @param { String } cornerColor 边角颜色
* @param { String } cornerStyle 边角形状 rect | circle
* @param { Number } cornerSize 边角大小
* @param { Number } borderScaleFactor 描边边框大小
* @param { Number } padding 控件距离内容的边距
* @param { Number } mtrOffsetY 旋转摇杆偏移
*/
export const controlPc: any = {
transparentCorners: false,
cornerStrokeColor: '#00BFFF',
cornerColor: '#00BFFF',
cornerStyle: 'rect',
cornerSize: 36,
borderScaleFactor: 5,
borderColor: '#00BFFF',
padding: 30,
mtrOffsetY: -40
}
/**
* @desc 需要隐藏的控件
* @param { Array } hiddenControl 藏的控件名的数组集合
*/
export const hiddenControl: Array<string> = ['ml', 'mb', 'mr', 'mt']
export const control = controlMobile
- config/name.ts 画布图层固定命名文件
/**
* @file name.ts 固定图层命名配置文件
* @description 图层固定命名
*/
/**
* @constant { Object } fixedLayerName 固定name
*/
export const fixedLayerName = {
visibleArea: 'visibleArea-line'
}
/**
* @constant { Array } fixedLayerNameArr 重绘图层时不参与重绘的图层
*/
export const fixedLayerNameArr: Array<string> = Object.values(fixedLayerName)
/**
* @constant { Array } hiddenLayerNameArr 输出图片时隐藏的图层
*/
export const hiddenLayerNameArr: Array<string> = ['visibleArea-line']
- modules/layer/image.ts 绘制图片图层
/**
* @file image.ts 图片图层
* @description 绘制图片、模板照片图层等
*/
import { fabric } from 'fabric'
import { addOrReplaceLayer } from '../common'
import { LayerType } from '../../types'
/**
* @function drawImgLayer 绘制图片图层
* @param { Object } Canvas 画布实例对象
* @param { Object } layer 图层对象
* @return { Object } layer 返回图片 图层对象
*/
export const drawImgLayer = (Canvas: any, layer: LayerType) => {
return new Promise(async (resolve: any) => {
const { uuid, url, x, y, scale, angle } = layer
if (!url) return resolve()
const imgLayer: any = await drawImg(url)
imgLayer.set({
originX: 'center',
originY: 'center',
left: (x === 0 && y === 0 ) ? Canvas.width / 2 : x,
top: (x === 0 && y === 0 ) ? Canvas.height / 2 : y,
scaleX: scale || Canvas.width / imgLayer.width,
scaleY: scale || Canvas.width / imgLayer.height,
angle
})
addOrReplaceLayer(Canvas, imgLayer)
imgLayer.name = uuid
return resolve(imgLayer)
})
}
省略部分...
- modules/layer/index.ts 绘制图层转发
/**
* @file layer/index.ts 图绘绘制
* @description 处理不同类型的图层
*/
import { drawImgLayer } from './image'
import { LayerType } from '../../types'
/**
* @function drawLayer 绘制图层
* @param { Object } Canvas 画布实例对象
* @param { Object } layer 图层对象
*/
export const drawLayer = (Canvas: any, layer: LayerType) => {
if (layer.type === 'img') {
return drawImgLayer(Canvas, layer)
}
}
- modules/background.ts 绘制画布背景
/**
* @file background.ts 绘制画布背景
* @description 用于画布绘制背景色、背景图等
*/
import { BgInfoType } from '../types/index'
/**
* @function drawBackground 绘制背景
* @param { Object } Canvas 画布实例
* @param { bgInfo } bgInfo 背景信息 背景图片链接、url等
*/
export const drawBackground = async (Canvas, bgInfo: BgInfoType) => {
return new Promise((resolve: any) => {
if (!bgInfo.url) return resolve()
const config = {
originX: 'center',
originY: 'center',
left: Canvas.width / 2,
top: Canvas.height / 2,
scaleX: Canvas.width / bgInfo.w,
scaleY: Canvas.width / bgInfo.h
}
Canvas.setBackgroundImage(bgInfo.url, Canvas.renderAll.bind(Canvas), config)
resolve()
})
}
-
modules/common.ts 工具函数
-
modules/init.ts 初始化画布
/**
* @file init.ts 初始化
* @description 初始化画布、遮罩层、可视区域等
*/
import { fabric } from 'fabric'
import { Canvas, StaticCanvas } from 'fabric/fabric-impl'
import { addOrReplaceLayer } from './common'
import { canvasType } from '../config/canvas'
import { fixedLayerName } from '../config/name'
/**
* @function initCanvas 初始化画布
* @param { String } inkId 画布dom id
* @param { Object } size 画布大小 { width, height }
* @param { Boolean } isStatic 是否静态画布
* @return { Object } Canvas 返回画布实例对象
*/
export const initCanvas = (inkId: string, size: canvasType, isStatic: boolean) => {
const Canvas: Canvas | StaticCanvas = new fabric[isStatic ? 'StaticCanvas' : 'Canvas'](inkId, size)
Canvas.preserveObjectStacking = true
Canvas.selection = false
Canvas.centeredScaling = true
return Canvas
}
- types/index.ts 公共类型、接口等
/**
* @file index.ts type类型、接口等
* @description 用于画布组件的props及其他参数类型接口
*/
export interface BgInfoType {
url: string
w: number
h: number,
name: string
}
export interface LayerType {
uuid: string,
type: string,
url: string,
w: number,
h: number,
x: number,
y: number,
scale: number,
angle: number
[propName: string]: any
}
export interface ControlType {
[propName: string]: any
}
- index.vue 组件
看完目录后,可能有的小伙伴感觉还是很模糊,思路不清楚。稍等,小黎现场画个流程图。
现在思路是不是清晰了很多,上面是流程图(画的不好,诸位将就看),接下来我们用代码走一遍。
const props = defineProps({})
/* 字父通信 */
const emit = defineEmits(['drawComplete', 'updateLayer'])
/* 初始化控件 */
const initFabricControl = () => {}
/* 鼠标按下 */
const canvasMouseDown = (e: any) => {}
/* 鼠标抬起 */
const canvasMouseUp = (e: any) => { /* 处理画布交互 */ }
/* 元素缩放时 */
const canvasMouseScaling = (e: any) => {}
/**
* @function drawAll 绘制所有图层
* @param { Object } canvas 画布实例
* @param { Array } layerList 图层数组
*/
const drawAll = async (canvas: any, layerList: LayerType[]) => {
for (const item of layerList) {
await drawLayer(canvas, item)
}
}
// 绘制完成emit
const drawComplete = () => emit('drawComplete')
/**
* @function save 保存作品图及效果图
* @return { String } result base64 保存/预览时返回
*/
const save = async (): Promise<string> => {
/* 输出合成后的图片 */
}
// 被动更改背景
watch(() => props.bgInfo, async (val) => (await drawBackground(Canvas, val)))
// 被动更改 layerList
watch(() => props.layerList, async (layerList, oldLayerList) => {
/* 重绘所有图层 */
})
onMounted(async () => {
/* 初始化控件 */
initFabricControl()
/* 初始化画布 */
Canvas = initCanvas(CanvasId.value, canvasSize, false)
/* 初始化背景 */
await drawBackground(Canvas, val)
/* 初始化绘制图层 */
await drawAll()
Loading.value = false
/* 绘制完成回调 */
drawComplete()
/* 绑定交互事件 */
/* 鼠标按下事件 */
Canvas.on('mouse:down', canvasMouseDown)
/* 鼠标抬起事件 */
Canvas.on('mouse:up', canvasMouseUp)
/* 元素缩放事件 */
Canvas.on('object:scaling', canvasMouseScaling)
})
现在有没有豁然开朗的感觉,这基本就是整个画布初版的核心代码。至于具体实现代码,各种api调用、细节实现请移步 github
页面交互(App.vue)
主要核心代码在画布组件,页面的交互相对较少。
引入组件
import RabbitLi from './components/RabbitLi/index.vue'
<RabbitLi ref="rabbitLi" :bg-info="avatarInfo" :layer-list="layerList" @drawComplete="drawComplete" />
初始化变量
const avatarInfo = ref<{ url: string, w: number, h: number, name: string }>({ url: '', w: 0, h: 0, name: '' })
const layerList = ref<LayerType[]>([])
交互
/* 上传原头像并更新背景信息 */
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) ?? ''
const imgInfo: any = await getImgInfo(url)
const name = file.name.split('.').splice(0, file.name.split('.').length - 1).join('.')
avatarInfo.value = { url, w: imgInfo.width, h: imgInfo.height, name };
(document.getElementById('uploadImg') as HTMLInputElement).value = ''
}
/* 选择效果图并更新图层列表 */
const selectEffect = (index: number) => {
if (!avatarInfo.value.url) return ElMessage.warning('请先上传原头像!')
effectIndex.value = index
loading.value = true
layerList.value = [
{
uuid: 'effect',
type: 'img',
url: effectList[index].imgUrl,
w: 0,
h: 0,
x: 0,
y: 0,
scale: 0,
angle: 0
}
]
}
保存&预览
const save = async (isSave) => {
if (!avatarInfo.value.url || !layerList.value.length) return ElMessage.warning('请上传原头像并选择效果图!')
previewShow.value = false
const url = await rabbitLi.value.save()
if (isSave) return downloadImg(url, avatarInfo.value.name)
previewShow.value= true
previewUrl.value= url
}
代码内容到现在已经差不多了,本篇基础api调用、实现方法阐述的较少,更注重思路及思想。
开源
这是2023年第2个开源项目,我的开源计划正在有条不紊的进行着。现在该工具唯一不足之处在于效果图较少,合成的春节头比较单一(设计图都是我买的,我的钱包又轻了些许🤕)。 效果图正在抓紧制作中,请大家耐心等待~
若您是设计师,且愿意为爱发电,为这个项目贡献几张效果图,尽自己绵薄之力,小黎不胜感激,该项目也因您的参与变得更有意义!
凡是有贡献的设计师,效果图下方将会出现您的署名,展示您的联系方式和个人链接。让更多人使用并认识您和您的作品,并在春节会收到小黎的新年祝福红包🧧~有意请评论或私信🙏🙏🙏。
年末渐渐忙了起来,熬了几个夜终于肝完了;〖定制兔年春节头像〗也顺理成章的有了第一个用户——我对象。自己做的东西用起来灵魂都在摇摆,这种感觉无疑是美妙的。
意见&建议
关于定制兔年春节头像小工具,有任何意见或建议可评论、私信或提交 github issues ,鸣谢!
余音
除夕将至,愿诸位抱着平安,拥着健康,揣着幸福,搂着温馨,携着快乐,牵着财运,拽着吉祥,迈入新年!欢迎大家一键三连~🙏🙏🙏