Vue3 + Fabricjs implement customized avatar 2.0

Vue3 + Fabricjs implement customized avatar 2.0

Born under the national flag and grown 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 to like, collect and follow

Foreword

If you want to see the effect or customize your Spring Festival avatar, please go straight to the Effect area;

If you want to see the principle and implementation ideas of Customized Avatar 2.0 gadget, please read it patiently. There are many code snippets in this article~

Effect

Effective direct train, experience address

github project address (welcome?)

If you like this gadget, move your little hands and click a star? Oh, thank you!

About iteration

After Customized Year of the Rabbit Spring Festival avatar was launched, many friends gave suggestions and feedback as soon as possible after experiencing it; with everyone’s help, the tool is constantly being improved; for example, the exported pictures are not clear enough, Transparency cannot be set, etc. After iterating to 1.4.0, normal use can be guaranteed. Here Caili would like to say thank you to everyone!

Since the focus at that time was on Year of the Rabbit and Spring Festival Avatars, the tool style was single, the functions were not perfect enough, and the internal logic was a bit overkill, etc., so a large version of >Customized Avatar 2.0Iteration.

Update content

Warehouse name

  • Powered by custom-rabbitImage changed to custom-avatar

Page

  • Reconstruct the overall style of the page and adjust it to be universal style
  • Compatible with pc and mobile terminals
  • Mobile terminal avatar wall adopts Waterfalls flow

Canvas related

  • The original image uploaded by the user is adapted to the short side. Matching, guaranteed not to deform
  • Optimize the effect of element controls, Add delete control
  • Optimize the drawing logic and reduce Useless operation.

New features

  • Add multiple theme options (Mid-Autumn Festival, National Day Festival, Spring Festival, etc., please stay tuned for other traditional festivals)
  • Add sticker effect, you can Multiple selection, deletable
  • Add a quick switching avatar box Function
  • Add notification function (xx The user customized the National Day avatar 3 minutes ago)
  • Add the function of sharing posters
  • Add avatar wall function, Users can preview avatars customized by others

Fix known issues

  • Fix qq browser cannot select files
  • Fix WeChat browser failure save Picture

Project structure

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

Required materials

The avatar frame and stickers are being designed and will be added bit by bit.

Mid-Autumn Festival theme

image.png

National Day theme

image.png

Spring Festival theme

image.png

Ideas

The basic idea remains the same. We have already talked about customizing avatars for the Spring Festival in the Year of the Rabbit, so we won’t go into details here.

Canvas interaction logic optimization

This is the first version of the logic
flow.png

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.

Code implementation

Canvas

  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
    })
}
  1. 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' })
    })
}
  1. 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)
}
  1. 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()
}
  1. 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)
}
  1. Save the image 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.

Page interaction

  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 = ''
}

  1. The user clicks 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)
}
  1. Set the transparency of the avatar frame
const opacity = ref<number>(1)
const opacityChange = (num: number) => DrawRef.value.setFrameOpacity(num)
  1. 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.

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>

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

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 occupy 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.

About open source

This journey has not been easy, and I can’t help but feel the bitterness in it. I also know that this project still has many shortcomings, and it cannot be solved overnight. If you have any suggestions or comments during the use, you can tell me. This is also what I think is more attractive about open source projects. You can brainstorm and combine the strengths of hundreds of schools of thought. I hope this tool will become more perfect and be liked by more people!

The lingering sound

I recently had an idea to make a column about creative tools, but I still need to think about this more.

I wish the motherland a happy holiday, and I also wish everyone a happy National Day. Goodbye!

syntaxbug.com © 2021 All Rights Reserved.