How to Build a Stunning Like Animation with CSS and Canvas for Live Streams
This tutorial walks through creating a dynamic, multi‑stage like animation for H5 live streams using CSS keyframe animations and a Canvas‑based implementation, covering trajectory analysis, sprite handling, SCSS loops, performance comparison, and practical code snippets for seamless integration.
1. Introduction
When watching WeChat video‑channel live streams, the rising like counter and vibration feedback are appealing, which inspired the development of a custom like animation for a Tencent Classroom H5 live room.
Final effect shows a more complex trajectory than the original, consisting of three stages: appearance with scaling, upward movement with random horizontal sway, and fade‑out while shrinking.
2. CSS Implementation of the Like Effect
2.1 Trajectory Analysis
The motion is split into vertical (y‑axis) uniform upward movement and horizontal (x‑axis) simple harmonic sway.
2.2 Trajectory Design
Keyframes combine scaling, opacity, and margin‑bottom changes to create the vertical motion, while multiple keyframe sets define different horizontal sway patterns.
@keyframes bubble_y {0% {transform:scale(1);margin-bottom:0;opacity:0;}5% {transform:scale(1.5);opacity:1;}80% {transform:scale(1);opacity:1;}100% {margin-bottom:var(--cntHeight);transform:scale(0.8);opacity:0;}} @keyframes bubble_swing_1 {0% {margin-left:0;}25% {margin-left:-12px;}75% {margin-left:12px;}100% {margin-left:0;}}2.3 Random Sprite Selection
Sprites are defined with SCSS loops to assign one of eight icons.
@for $i from 0 through 7 {.b#{$i} {background:url('../../images/like_sprites.png') $i * -24px 0;}}2.4 Generating a Like Icon
JavaScript creates a container div, appends a new bubble element with random class names, and removes it after a timeout.
const addBubble = () => { const d=document.createElement('div'); d.className=`like-bubble b${cacheRef.current.bubbleIndex} bl_${swing}_${speed}`; bubbleCnt?.appendChild(d); cacheRef.current.bubbleIndex++; setTimeout(()=>bubbleCnt?.removeChild(d),2600); };Click handling adds a bounce animation to the like button.
const onClick = () => { if(timer){clearTimeout(timer); timer=null;} likeIcon.classList.remove('bounce-click'); setTimeout(()=>likeIcon.classList.add('bounce-click'),0); timer=window.setTimeout(()=>{likeIcon.classList.remove('bounce-click');},300); addBubble(); };2.5 Final CSS Effect
3. Canvas Implementation of the Like Effect
3.1 Canvas Creation
The canvas element is obtained by ID and its 2D context is stored along with width, height, and a scaling factor.
constructor(canvasId,canvasScale){ const canvas=document.getElementById(canvasId); this.context=canvas.getContext('2d'); this.width=canvas.width; this.height=canvas.height; this.canvasScale=canvasScale; this.img=null; this.loadImages(); }3.2 Pre‑loading Sprite Image
loadImages=()=>{ const p=new Promise(resolve=>{ const img=new Image(); img.onerror=()=>resolve(img); img.onload=()=>resolve(img); img.src=likeSprites; }); p.then(img=>{ if(img && img.width>0){ this.img=img; } }); };3.3 Trajectory Decomposition
Vertical motion mirrors the CSS version; horizontal motion uses a sine function y = A sin(Bx + C) + D with random amplitude and frequency.
const getTranslateX = (progress)=>{ if(progress<ENLARGE_STAGE){return basicX;} return basicX + amplitude*Math.sin(frequency*(progress-ENLARGE_STAGE)); }; const getTranslateY = (progress)=>{ return IMAGE_WIDTH/2 + (this.height-IMAGE_WIDTH/2)*(1-progress); };3.4 Size and Opacity Calculation
const getScale = (p)=>{ let r=1; if(p<ENLARGE_STAGE){ r=p/ENLARGE_STAGE; } else if(p>FADE_OUT_STAGE){ r=(1-p)/(1-FADE_OUT_STAGE); } return r; }; const getAlpha = (p)=>{ if(p<FADE_OUT_STAGE){return 1;} return 1-(p-FADE_OUT_STAGE)/(1-FADE_OUT_STAGE); };3.5 Canvas Rendering
return (progress)=>{ if(progress>=1) return true; context.save(); const scale=getScale(progress); const tx=getTranslateX(progress); const ty=getTranslateY(progress); context.translate(tx,ty); context.scale(scale,scale); context.globalAlpha=getAlpha(progress); context.drawImage(this.img, SOURCE_IMAGE_WIDTH*curImgIndex,0, SOURCE_IMAGE_WIDTH, SOURCE_IMAGE_WIDTH, -newWidth/2, -newWidth/2, newWidth, newWidth); context.restore(); return false; };3.6 Animation Loop
The start function creates a render task, pushes it to a list, and triggers a requestAnimationFrame loop that clears the canvas, renders each active bubble, and removes completed tasks.
scan=()=>{ this.context.clearRect(0,0,this.width,this.height); let i=0; while(i<renderList.length){ const child=renderList[i]; if(!child.render(child.progress())){ renderList.splice(i,1); } else { i++; } } if(renderList.length) requestAnimationFrame(this.scan); };3.7 Triggering the Animation
Clicking the like button calls start(), which adds a new render task to the canvas.
const onClick=()=>{ cacheRef.current.LikeAni?.start?.(); };4. Performance Comparison
4.1 Frame Rendering Stats
Chrome DevTools shows that the CSS version generates many DOM elements, causing higher GPU memory usage and frequent repaint highlights, while the Canvas version draws directly on a single canvas, resulting in smoother frames and lower memory consumption.
4.2 Detailed Performance
In the Performance panel, the CSS implementation shows layout shifts and higher CPU/GPU load, whereas the Canvas implementation maintains stable frame rates and lower resource usage.
5. Related Resources
Implementation reference: https://github.com/antiter/praise-animation
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
