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 strong> 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
National Day theme
Spring Festival theme
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
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
- 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 }) }
- 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' }) }) }
- 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) }
- 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() }
- 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) }
- 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
- 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 = '' }
- 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) }
- Set the transparency of the avatar frame
const opacity = ref<number>(1) const opacityChange = (num: number) => DrawRef.value.setFrameOpacity(num)
- 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!