Liquid Flow Effects with SVG, CSS Masking, and Canvas – A Frontend Development Tutorial
This article demonstrates how to create dynamic liquid‑flow animations for static images using SVG filters (feTurbulence, feDisplacementMap), CSS mask‑image, Canvas drawing, and TweenMax, providing step‑by‑step code for generating hotspots, animating them, and integrating image upload and canvas clearing in a web page.
The article explains the background of liquid‑flow visual effects commonly seen in mobile games such as "Honor of Kings" and "Onmyoji", where static backgrounds are enhanced with localized fluid animations to reduce development cost and increase visual appeal.
It introduces a front‑end implementation that combines SVG filters ( feTurbulence and feDisplacementMap ), CSS mask-image , the filter property, Canvas drawing, TimelineMax animation, and input[type=file] for local image loading.
Effect Demonstration
Several example animations (mist diffusion, flag waving, character sleeves, lake ripples, text liquefaction) are shown, with a link to an online demo where users can experience the effect and generate their own animated skins.
Implementation Overview
The page consists of two main parts: a top area for loading an image and drawing a hotspot path with mouse dragging, and a bottom control panel with buttons to clear the canvas and switch images.
HTML Structure
<main id="sketch">
<canvas id="canvas" data-img=""></canvas>
<div class="mask">
<div id="maskInner" class="mask-inner"></div>
</div>
</main>
<section class="button_container">
<button class="button">Clear Canvas</button>
<button class="button"><input class="input" type="file" id="upload">Upload Image</button>
</section>
<svg>
<filter id="heat" filterUnits="objectBoundingBox" x="0" y="0" width="100%" height="100%">
<feTurbulence id="heatturb" type="fractalNoise" numOctaves="1" seed="2"/>
<feDisplacementMap xChannelSelector="G" yChannelSelector="B" scale="22" in="SourceGraphic"/>
</filter>
</svg>CSS Styles
main {
position: relative;
background-image: url('bg.jpg');
background-size: cover;
background-position: 100% 50%;
}
canvas {
opacity: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.mask {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
mask-mode: luminance;
mask-size: 100% 100%;
backdrop-filter: hard-light;
mask-image: url('mask.png');
}
.mask-inner {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: url('bg.jpg') 0% 0% repeat;
background-size: cover;
background-position: 100% 50%;
filter: url(#heat);
mask-image: url('mask.png');
}The mask-image property creates a mask that blends the generated hotspot map with the background image, while the filter applies the SVG filter to produce the liquid distortion.
JavaScript Methods
1. Drawing the Hotspot Map
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var sketch = document.getElementById('sketch');
var sketchStyle = window.getComputedStyle(sketch);
var mouse = { x: 0, y: 0 };
canvas.width = parseInt(sketchStyle.getPropertyValue('width'));
canvas.height = parseInt(sketchStyle.getPropertyValue('height'));
canvas.addEventListener('mousemove', e => {
mouse.x = e.pageX - canvas.getBoundingClientRect().left;
mouse.y = e.pageY - canvas.getBoundingClientRect().top;
}, false);
ctx.lineWidth = 40;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.strokeStyle = 'black';
canvas.addEventListener('mousedown', () => {
ctx.beginPath();
ctx.moveTo(mouse.x, mouse.y);
canvas.addEventListener('mousemove', onPaint, false);
}, false);
canvas.addEventListener('mouseup', () => {
canvas.removeEventListener('mousemove', onPaint, false);
}, false);
var onPaint = () => {
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
var url = canvas.toDataURL();
document.querySelectorAll('div').forEach(item => {
item.style.cssText += `
display: initial;
-webkit-mask-image: url(${url});
mask-image: url(${url});
`;
});
};2. Generating the Animation
feTurb = document.querySelector('#heatturb');
var timeline = new TimelineMax({
repeat: -1,
yoyo: true
});
timeline.add(
new TweenMax.to(feTurb, 8, {
onUpdate: () => {
var bfX = this.progress() * 0.01 + 0.025,
bfY = this.progress() * 0.003 + 0.01,
bfStr = bfX.toString() + ' ' + bfY.toString();
feTurb.setAttribute('baseFrequency', bfStr);
}
}),
0);The animation continuously updates the baseFrequency of feTurbulence to create a flowing effect; the same result can be achieved with other animation libraries or requestAnimationFrame .
3. Clearing the Canvas
function clear() {
document.querySelectorAll('div').forEach(item => {
item.style.cssText += `
display: none;
-webkit-mask-image: none;
mask-image: none;
`;
});
}
document.querySelectorAll('.button').forEach(item => {
item.addEventListener('click', () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
clear();
})
});4. Switching Images
document.getElementById('upload').onchange = function() {
var imageFile = this.files[0];
var newImg = window.URL.createObjectURL(imageFile);
clear();
document.getElementById('sketch').style.cssText += `
background: url(${newImg});
background-size: cover;
background-position: center;
`;
document.getElementById('maskInner').style.cssText += `
background: url(${newImg});
background-size: cover;
background-position: center;
`;
};After implementing all four functions, users can draw custom hotspot paths on any image, animate the liquid flow, clear the effect, or load a new picture, enabling the creation of epic skin launch pages or mini‑games.
Summary of New Knowledge Points
CSS mask-image for masking elements.
SVG filters feTurbulence and feDisplacementMap for liquid distortion.
CSS filter property to apply SVG filters.
Canvas drawing techniques for hotspot generation.
TimelineMax animation of SVG filter attributes.
Using input[type=file] to load local images.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
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.