除夕将至,快来定制你的春节头像叭

除夕将至,快来定制你的春节🐇头像叭🌈

小年已经到来,除夕还会远嘛!新年向我们招手,便用代码与她来一次新年邂逅吧~

前言

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

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

定制兔年春节头像的前世今生

写完2023年的第一篇文章——🐇年新春,快来领取你的春节全屏动效🌈,新年才姗姗来迟。觉得还是缺少些许年味,至少在没有浏览器的地方还是感觉不到的。冥思苦想,盯着自己的灰色头像发呆;突然就想到了她——春节头像,头像已经悄无声息的渗透在了我们生活的各个角落;不光我自己可以使用,我的家人、朋友、在外漂泊的诸位都可以使用。于是便诞生了定制兔年春节头像 小工具。

效果

所有言语都过于苍白,3,2,1,上链接~ 🤣

效果直达车

效果直达车(github备用链接)

github项目地址(欢迎⭐)

代码已开源,如果喜欢这个项目请动动小手点个star⭐,谢谢!

项目架构

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 画布大小配置等
 * @author xiao li
 * @copyright 黎<https://www.xiaoli.vip>
 * @createDate 2023-01-12 14:36
 */
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 控件样式、显示隐藏等
 * @author xiao li
 * @copyright 黎<https://www.xiaoli.vip>
 * @createDate 2023-01-12 14:42
 */

/**
 * @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 图层固定命名
 * @author xiao li
 * @copyright 黎<https://www.xiaoli.vip>
 * @createDate 2023-01-12 17:50
 */

/**
 * @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 绘制图片、模板照片图层等
 * @author xiao li
 * @copyright 北溟有鱼<https://inkinkme.com>
 * @createDate 2022-07-25 14:20
 */
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) => {
    // eslint-disable-next-line no-async-promise-executor
    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 处理不同类型的图层
 * @author xiao li
 * @copyright 黎<https://www.xiaoli.vip>
 * @createDate 2023-01-12 21:18
 */

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 用于画布绘制背景色、背景图等
 * @author xiao li
 * @copyright 黎<https://www.xiaoli.vip>
 * @createDate 2023-01-12 14:50
 */
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 初始化画布、遮罩层、可视区域等
 * @author xiao li
 * @copyright 黎<https://www.xiaoli.vip>
 * @createDate 2023-01-12 18:04
 */

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及其他参数类型接口
 * @author xiao li
 * @copyright 黎<https://www.xiaoli.vip>
 * @createDate 2023-01-12 14:54
 */

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 ,鸣谢!

余音

除夕将至,愿诸位抱着平安,拥着健康,揣着幸福,搂着温馨,携着快乐,牵着财运,拽着吉祥,迈入新年!欢迎大家一键三连~🙏🙏🙏

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

采黎

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值