Vue3 + Fabricjs customized National Day exclusive avatar

Vue3 + Fabricjs implement customized avatar 2.0

  • 1. Online customization
  • 2. Project structure
  • 3. Required materials
    • 3.1 Mid-Autumn Festival theme
    • 3.2 National Day theme
    • 3.3 Spring Festival theme
  • 4. Canvas interaction logic optimization
  • 5. Code implementation
    • 5.1 Initialize canvas and controls
    • 5.2 Monitor changes in the original image (avatar uploaded by the user) and perform short-side adaptation
    • 5.3 Draw the avatar frame and hide the delete button control
    • 5.4 Set the transparency of the avatar frame
    • 5.5 Draw stickers
    • 5.6 Save the image and export base64
  • 6. Page interaction
    • 6.1 The user uploads a picture, generates a local short link, then draws the original avatar, and draws the first avatar frame by default
    • 6.2 The user clicks the avatar frame or clicks the quick switch button to draw the avatar frame
    • 6.3 Set the transparency of the avatar frame
    • 6.4 Click on the sticker to draw the sticker
  • 7. Scroll notification animation effect
  • 8. Poster function
  • 9. Implementation of waterfall flow on mobile terminal

_The National Day is coming soon, let me share the customized private National Day avatar implemented using Vue, so that you can have enough food and clothing by yourself _


Born under the national flag, growing up in the spring breeze! The National Day is approaching, Caili brings you [Customized avatar 2.0 (National Day avatar)], let us celebrate the motherland’s birthday in the form of code! Welcome everyone [Like, favorite and follow]

1. Online customization

  • Customized avatar entrance, experience address
  • github project address
    “https://github.com/docker/compose/releases/download/1.25.5/docker-compose-$(uname -s)-$(uname -m)” -o /usr/local/bin/docker- compose
  • sudo chmod +x /usr/local/bin/docker-compose

2. Project structure

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

3. Required materials

3.1 Mid-Autumn Festival theme

3.2 National Day theme

3.3 Spring Festival theme

4. Canvas interaction logic optimization


Considering that the custom avatar tool will not have too many layers and its functions will not be too complex, the following optimizations have been made in the new version

  • Delete the logic of drawing multiple layers (listen to changes in the layer list and then draw the layers)
  • The drawing of the avatar frame is changed to active calling to reduce the frequency of useless calls;
  • Drawing stickers is an active call and can draw multiple
  • Delete canvas operation synchronization logic (no need to echo data to the page, and no need to draw it twice, so delete it)

After completing the above optimizations, the amount of code has been significantly reduced; it’s just that I didn’t think too much at the time and just copied the implementation methods of other projects.

5. Code implementation

5.1 Initialize canvas and controls

const init = () => {<!-- -->
    /* Initialize control */
    initFabricControl()

    /* Initialize canvas */
    Canvas = initCanvas(CanvasId.value, canvasSize, false)

    // element zoom event
    Canvas.on('object:scaling', canvasMouseScaling)
}


/* Initialize control */
const initFabricControl = () => {<!-- -->
    fabric.Object.prototype.set(control)
    //Set the zoom joystick offset
    fabric.Object.prototype.controls.mtr.offsetY = control.mtrOffsetY
    //Hide unnecessary controls
    hiddenControl.map((name: string) => (fabric.Object.prototype.controls[name].visible = false))

    /* Add delete control */
    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
    })
}

5.2 Monitor changes in the original image (avatar uploaded by the user) and perform short-side adaptation

/* Change original image */
watch(() => props.bg, async (val) => (await drawBackground(Canvas, val)))

/**
 * @function drawBackground draws the background
 * @param { Object } Canvas canvas instance
 * @param { String } bgUrl The original image link uploaded by the user
 */
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'
            })

            /* Short side adaptation */
            img.width > img.height ? img.scaleToHeight(Canvas.height, true) : img.scaleToWidth(Canvas.width, true)
            Canvas.setBackgroundImage(img, Canvas.renderAll.bind(Canvas))

            resolve()
        }, {<!-- --> crossOrigin: 'Anonymous' })
    })
}

5.3 Draw the avatar frame and hide the delete button control

const frameName = 'frame'

/**
 * @function addFrame adds avatar frame layer
 * @param { String } url avatar box link
 */
const addFrame = async (url = '') => {<!-- -->
    if (!url) return

    const frameLayer: any = await drawImg(`${<!-- --> url }!frame`)
    frameLayer.set({<!-- -->
        left: Canvas.width / 2,
        top: Canvas.height / 2
    })

    /* Hide delete button */
    frameLayer.setControlVisible('deleteControl', false)

    frameLayer.scaleToWidth(Canvas.width, true)

    frameLayer.name = frameName
    addOrReplaceLayer(Canvas, frameLayer)
}

5.4 Set the transparency of the avatar frame

/**
 * @function setFrameOpacity sets the transparency of the avatar frame
 * @param { Number } opacity transparency
 */
const setFrameOpacity = (opacity = 1) => {<!-- -->
    const frameLayer: any = findCanvasItem(Canvas, frameName)[1] || ''

    if (!frameLayer) return

    frameLayer.set({<!-- --> opacity })
    Canvas.renderAll()
}

5.5 Draw stickers

/**
 * @function addMark adds stickers
 * @param { String } url sticker link
 */
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)
}

5.6 Save pictures and export base64

/**
 * @function save save the renderings
 * @return { String } result base64 Returned when saving/previewing
 */
const save = async (): Promise<string> => {<!-- -->
    return Canvas.toDataURL({<!-- -->
        format: 'png',
        left: 0,
        top: 0,
        width: Canvas.width,
        height: Canvas.height
    })
}

Now the code is much clearer, like a flower in the dark.

6. Page interaction

6.1 The user uploads a picture, generates a local short link, then draws the original avatar, and draws the first avatar frame by default

const uploadFile = async (e: any) => {<!-- -->
    if (!e.target.files || !e.target.files.length) return ElMessage.warning('Upload failed!')

    const file = e.target.files[0]
    if (!file.type.includes('image')) return ElMessage.warning('Please upload the correct image format!')

    const url = getCreatedUrl(file)  ''
    /* When a user uploads an avatar for the first time, the first avatar box is selected by default */
    if (!originAvatarUrl.value) {<!-- -->
        originAvatarUrl.value = url
        selectFrame(0)
    } else {<!-- -->
        originAvatarUrl.value = url
    }

    (document.getElementById('uploadImg') as HTMLInputElement).value = ''
}

6.2 The user clicks on the avatar frame or clicks the quick switch button to draw the avatar frame

/* Quickly switch avatar frames */
const changeFrame = (isNext) => {<!-- -->
    if (!originAvatarUrl.value) return ElMessage.warning('Please upload your avatar first!')

    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)
}

/* Draw the avatar frame - call the canvas drawing function */
const selectFrame = (index: number) => {<!-- -->
    if (!originAvatarUrl.value) return ElMessage.warning('Please upload your avatar first!')

    opacity.value = 1
    selectFrameIndex.value = index
    frameUrl.value = picList[styleIndex.value].frameList[index]
    DrawRef.value.addFrame(frameUrl.value)
}

6.3 Set the transparency of the avatar frame

const opacity = ref<number>(1)
const opacityChange = (num: number) => DrawRef.value.setFrameOpacity(num)

6.4 Click on the sticker to draw the sticker

const selectMark = (index: number) => {<!-- -->
    if (!originAvatarUrl.value) return ElMessage.warning('Please upload your avatar first!')

    const markUrl = picList[styleIndex.value].markList[index]
    DrawRef.value.addMark(markUrl)
}

The interaction logic of the page is relatively simple, just follow it step by step.

7. Scroll notification animation effect

The transition animation of Vue is used here to simulate the scrolling effect. The essence is that after the key changes, the pop-in and pop-up effect will be triggered.

<transition name="notice" mode="out-in">
    <div v-if="avatarList & amp; & amp; avatarList.length" class="notice" :key="avatarList[noticeIndex].last_modified">
        <p>
            <span style="color: #409eff;">Visitor{<!-- -->{ (avatarList[noticeIndex].last_modified + '').slice(-5) }} </span>
            <span style="padding-left: 2px;">{<!-- -->{ calcOverTime(avatarList[noticeIndex].last_modified) }}before</span>
            <span style="padding-right: 2px;">Made</span>
            <span style="color: #f56c6c;">{<!-- -->{ styleEnums[avatarList[noticeIndex].id] }}Avatar </span>
            <span style="padding-left: 4px;"></span>
        </p>
        <img :src="avatarList[noticeIndex].url" alt="">
    </div>
</transition>

8. Poster function

Just use the [html2canvas] library for this. It can be implemented using normal css attributes.

<!-- Generate poster -->
<div id="poster" class="poster">
    <!-- Content omitted -->
</div>
/* Pay attention to cross-domain images */
await nextTick(() => {<!-- -->
    /* Generate poster */
    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
    })
})

9. Mobile waterfall flow implementation

Both the PC and mobile terminals have a grid layout. We give the mobile terminal a random number of rows and columns, and the PC terminal is forced to set it to 1, just to ensure that the rows and columns have the same proportion (customized avatar exports are all square)
[grid-auto-flow: dense;] This style is key,

<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;
    }
}

/* The PC side does not use waterfall flow, and strongly covers the number of rows and columns */
@media only screen and (min-width: 769px) {<!-- -->
    .wall {<!-- -->
        .wall-list {<!-- -->
            > div {<!-- -->
                grid-row: span 1 !important;
                grid-column: span 1 !important;
            }
        }
    }
}

At this point, the basic core and details have been implemented; if you want to know more code design and development ideas, please go to github, the code has been open source.