likes
comments
collection
share

3分钟定制中秋贺卡 🌕 送祝福·庆团圆·寄相思

作者站长头像
站长
· 阅读数 23

3分钟定制中秋贺卡 🌕 送祝福·庆团圆·寄相思

3分钟定制中秋贺卡 🌕 送祝福·庆团圆·寄相思

露从今夜白,月是故乡明!中秋将至,团圆之时近。中秋将至,采黎为大家带来 定制中秋贺卡。值此佳节,让我们通过贺卡传递温馨祝福,庆祝团圆,寄托思念。欢迎大家一键三连~🙏🙏🙏

效果

好奇的小伙伴可以先看效果,项目链接呈上~ 🤣

效果直达车

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

github项目地址(欢迎⭐) 代码已开源,如果喜欢这个项目请动动小手点个star⭐,谢谢!

使用教程

点击头像,点击右上角即可上传头像。 3分钟定制中秋贺卡 🌕 送祝福·庆团圆·寄相思

双击文本,即可修改文本内容。 3分钟定制中秋贺卡 🌕 送祝福·庆团圆·寄相思

头像和文本皆可移动位置、缩放、旋转等。

项目架构

vue3 | ts | less | Elemenu UI | fabricjs

项目素材

页面素材

3分钟定制中秋贺卡 🌕 送祝福·庆团圆·寄相思

海报所需素材

3分钟定制中秋贺卡 🌕 送祝福·庆团圆·寄相思

贺卡主题

1. 祝福主题

  • 月光所至,万事如意。祝你,祝我,祝我们!

2. 团聚主题

  • 一家人团聚在中秋佳节,灯火可亲~

3. 思念主题

  • 在外漂泊、异国他乡的游子,寄托月是故乡明的思乡情
  • 相隔万里的恋人,诉说两处相思同望月,此刻也算共团圆!

思路

  • 以祝福、团圆、思念为载体,预置三个贺卡主题。
  • 用户只需上传头像,修改文案,简单调整位置即可快速定制出中秋贺卡。
  • 支持预览、保存。
  • 支持生成海报,分享给朋友。
  • 支持中秋贺卡集功能,用户可观看他人定制的贺卡。

页面布局交互

  1. 贺卡画布居中
  2. 页面用素材点缀,添加动画过渡等效果。
  3. 支持快速切换主主题功能(左右箭头)。

贺卡实现原理

  1. 固定贺卡画布固定比例。
  2. 背景图片短边适配,且不可操作。
  3. 绘制圆形头像,支持用户重新上传头像。
  4. 绘制文本(横排、竖排文本),支持用户更改文案。
  5. 导出贺卡图片,生成贺卡、海报等。

代码实现

创建初始画布

 /**  
  * @function initCanvas 初始化画布  
  * @param { String } inkId 画布dom id  
  * @param { Object } size 画布大小 { width, height }  
  * @param { Boolean } isStatic 是否静态画布  
  * @return { Object } Canvas 返回画布实例对象  
  */  
export const initCanvas = (inkId: string, size: CanvasSizeType, isStatic= false) => {  
    const Canvas: Canvas | StaticCanvas = new fabric[isStatic ? 'StaticCanvas' : 'Canvas'](inkId, size)  
    // 关闭点击后图层被置顶  
    Canvas.preserveObjectStacking = true  
    // 关闭多选  
    Canvas.selection = false  
    // 设置中心缩放  
    Canvas.centeredScaling = true
    
    return Canvas  
}

背景图层

/**  
* @function drawBackground 绘制背景  
* @param { Object } Canvas 画布实例  
* @param { Object } designInfo 背景信息 背景图片链接、url等  
*/  
export const drawBackground = async (Canvas, designInfo: DesignInfo) => {  
    return new Promise((resolve: any) => {  
        if (!designInfo.bg) return resolve()
        fabric.Image.fromURL(designInfo.bg, (img: any) => {  
            img.set({  
                left: Canvas.width / 2,  
                top: Canvas.height / 2,  
                originX: 'center',  
                originY: 'center'  
            })  

            // 背景短边适配
            const imgRatio = img.width / img.height  
            const canvasRatio = 0.5  
            canvasRatio >= imgRatio ? img.scaleToWidth(Canvas.width, true) : img.scaleToHeight(Canvas.height, true)  
            
            Canvas.setBackgroundImage(img, Canvas.renderAll.bind(Canvas))  

            resolve()  
        }, { crossOrigin: 'Anonymous' })  
    })  
}

头像图层

头像头层稍微复杂一点,一步步来就好。

1. 绘制图片图层

/**  
* @function drawImg 绘制图片  
* @param { String } url 图片链接  
* @return { Object } img 返回图片画布对象  
*/  
export const drawImg = (url: string) => {   
    return new Promise(async (resolve: any) => {  
        fabric.Image.fromURL(url, (img) => resolve(img), { crossOrigin: 'Anonymous' })  
    })  
}

const { name, url, left, top, w, angle } = layer  
  
const imgLayer: any = await drawImg(url)  
imgLayer.set({ left, top, angle })  
imgLayer.scaleToWidth(w, true)  
imgLayer.name = name  
addOrReplaceLayer(Canvas, imgLayer)

2. 自定义右上角上传图片控件

/* 绘制自定义上传控件 */  
const uploadLayer: any = await drawImg(new URL('../../icons/upload-img.png', import.meta.url).href)  
uploadLayer.scaleToHeight(40, true)  
  
const uploadImgDom = document.getElementById('uploadImg') as HTMLInputElement  
  
const customControl = new fabric.Control({  
    x: 0.5,  
    y: -0.5,  
    offsetY: 0, // 垂直偏移以使图标居中  
    cursorStyle: 'pointer', // 鼠标悬停样式  
    mouseUpHandler: () => uploadImgDom.click(),  
    render: function (ctx, left, top) {  
        uploadLayer.set({ left, top: top })  
        uploadLayer.render(ctx)  
    }  
})  
  
imgLayer.setControlsVisibility({ tr: false })  
  
// 将自定义控制控件添加到元素1  
imgLayer.setControlVisible('mtr', true) // 显示元素1的默认控制控件  
imgLayer.controls.customControl = customControl // 添加自定义控制控件

3. 监听控件点击事件触发file文件上传

/* 图片上传逻辑 */  
uploadImgDom.addEventListener('change', async (e) => {  
    const url = await uploadImg(e) as string  
    uploadImgDom.value = ''  

    const imgLayerWidth = imgLayer.getScaledWidth()  

    /* 仅更新图片源,其他参数保留 */  
    imgLayer.setSrc(url, (newImgLayer) => {  
        const ratio = imgLayerWidth / newImgLayer.width  
        newImgLayer.set({  
            left: imgLayer.left,  
            top: imgLayer.top,  
            angle: imgLayer.angle,  
            scaleX: ratio,  
            scaleY: ratio  
        })  

        // 刷新 Canvas 以显示更新后的图片元素  
        Canvas.renderAll()  
    })  
})

3.1 图片上传后裁剪为圆形(短边适配)

因为圆形图片绘制的问题,只能先对图片进行裁剪,下面 项目难点1 中会讲到。

const uploadImg = async (e) => {  
    return new Promise((resolve, reject) => {  
        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 canvas: any = document.getElementById('circleCanvas')  
        const ctx = canvas.getContext('2d')  
        const imgUrl = getCreatedUrl(file) ?? ''  

        const image = new Image()  
        image.src = imgUrl  

        image.onload = () => {  
            const diameter = Math.min(image.width, image.height) // 获取最短边作为直径  
            canvas.width = diameter  
            canvas.height = diameter  

            const centerX = diameter / 2  
            const centerY = diameter / 2  

            // 创建一个圆形路径  
            ctx.beginPath()  
            ctx.arc(centerX, centerY, diameter / 2, 0, Math.PI * 2, false)  
            ctx.closePath()  
            ctx.clip()  

            // 计算图片的偏移,使其居中  
            const offsetX = (image.width - diameter) / 2  
            const offsetY = (image.height - diameter) / 2  

            // 将图片绘制到圆形区域内,不变形  
            ctx.drawImage(image, offsetX, offsetY, diameter, diameter, 0, 0, diameter, diameter)  

            return resolve(canvas.toDataURL('image/png'))  
        }  

        image.onerror = () => reject('')  
    })  
}

横排文本

/**  
* @function drawTextLayer 绘制文本图层  
* @param { Object } Canvas 画布实例对象  
* @param { Object } layer 图层对象  
* @return { Object } layer 返回文本 图层对象  
*/  
export const drawTextLayer = (Canvas: any, layer: any) => {  
    return new Promise(async (resolve: any) => {  
        const { name, left, top, text, url, fontSize, fontColor, fontWeight } = layer  

        /* 注册字体 */  
        if (url && !globalThis.fontObj[url]) {  
            const font = new window.FontFace(url, `url(${url})`)  
            document.fonts.add(font)  
            const res = await font.load().catch(() => ({}))  
            if (res && res.status === 'loaded') globalThis.fontObj[url] = url  
        }  

        const textStyle = {  
            left,  
            top,  
            fontSize,  
            fontWeight,  
            fontFamily: url,  
            fill: fontColor,  
            lineHeight: 1,  
            cursorColor: fontColor,  
            editingBorderColo: '#fff'  
        }  
        const textSprite = new fabric.Textbox(text, textStyle)  
        textSprite.name = name  
        // 保留死角缩放,去除其他按钮控件
        textSprite.setControlsVisibility({ ml: false, mr: false, mt: false, mb: false })  
        addOrReplaceLayer(Canvas, textSprite)  

        return resolve(textSprite)  
    })  
}

纵排文本

竖排文字跟横排文字类似,在此基础上固定了文本框的宽度

/* 补充了这两个属性 */
const textStyle = {  
    width: fontSize,  
    splitByGrapheme: true  
}

封装画布组件(使用fabric.js),其具有绘制、调整、预览、导出图片等功能;在页面引入组件,通过props传递参数,组件通信等实现定制兔年春节头像工具的功能。

项目难点

1. 绘制圆形头像

最初用fabricjs 中的 clipPath 做的, 但是在移动端裁剪失效,图片无法绘制。

const { name, url, left, top, w, angle  } = layer
 /* 绘制圆形图片 */
if (!url) return resolve()
const imgLayer: any = await drawImg(url)

imgLayer.set({
    left,
    top,
    angle,
    clipPath: new fabric.Circle({
        radius: Math.min(imgLayer.height, imgLayer.width) / 2
    })
})

试了几种方式, 还是以失败告终。随后便想到了——将图片先裁剪为圆形再进行绘制。 试了后果然可以,这个问题就解决了

2. 绘制自定义上传控件

自定义上传控件,这不算是个难点,但是知识点很细。官方文档 有相应的案例,还有一个细小的知识点是 设置控件的显示隐藏

/* 自定义控件在代码实现中有贴出 */

/* 设置某一元素右上角的控件隐藏 */
imgLayer.setControlsVisibility({ tr: false })

3. 使用自定义字体

自定义字体就跟正常的api一样去使用就OK,有个细节在于处理自定义字体的异常捕获,它会导致文本绘制失败。 这里我还做了字体加载的性能优化。

/* 初始化字体缓存 */  
if (!('fontObj' in globalThis)) globalThis.fontObj = {}

/* 注册字体 */ 
/* 若字体缓存池中未加载字体,则开始加载,否则跳过 */
if (url && !globalThis.fontObj[url]) {  
    const font = new window.FontFace(url, `url(${url})`)  
    document.fonts.add(font)  
    /* 这里的catch捕获异常必须加上,否则文本绘制失败   */
    /* 加上异常捕获后, 即使字体未加载成功,也会使用默认字体绘制 */
    const res = await font.load().catch(() => ({}))  
    if (res && res.status === 'loaded') globalThis.fontObj[url] = url  
}

3. 移动端输入文字时页面跳跃

这个问题有两个原因

  1. 画布过大,且不在页面中心区域;我的画布600 * 1080,然后进行缩放定位。
  2. 文本框编辑时,fabricjs默认处理会将当前文本框置于画布的中心区域,在body下加一个 textarea,根据画布大小和页面进行计算;
<textarea autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-fabric-hiddentextarea="" wrap="off" style="position: absolute; top: 1152.77px; left: 849.5px; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px; padding-top: 42px;"></textarea>

画布我已经做过兼容处理,一直处于页面的中心区域, 所以我对其textarea样式做了覆盖。

/* 解决fabric.js 文本框输入时页面跳跃 */  
textarea {  
    top: 0 !important;  
    left: 0 !important;  
    padding-top: 0 !important;  
}

4. 移动端图片保存及分享

这是个老生常谈的问题了,在 定制春节头像 项目上就栽了跟头;pc端不存在这个问题,主要集中在了移动端;微信内置浏览器是不允许下载文件的,所以只能曲线救国;

解决方案:将要保存或分享的图片展示在页面上, 引导用户长按保存、分享。

开源

2023年已过大半,开源项目大大小小也有十来个了。但这个开源项目是不一样的,构思、开发、设计都是我一个人独立完成,虽然从设计、产品层面来说,她不是那么的完美,但却是极其有意义的!

从构思到完成,用了不到五天的时间;熬夜写页面,优化交互,好像有使不完的劲儿。这种感觉无疑是美妙的,难于言表~

意见&建议

关于中秋贺卡,有任何意见或建议可评论、私信或提交 github issues ,鸣谢!

招募

细心的小伙伴可能发现了,我喜欢做一些有创意的、好玩的、有趣的项目;如果你也喜欢,可以点个关注哦。不管是设计、前后端开发,我们都可以相聚于此,沟通交流,绽放创作之花!

余音

月光所照皆是故乡,双脚所踏皆是生活。 制贺卡,送祝福,再会!