声明:本文涉及图文和模型素材仅用于个人学习、研究和欣赏。
如果你有玩过🎮《王者荣耀》、《阴阳师》等手游,一定注意到过它的启动动画、皮肤立绘卡片等场景,经常采用静态底图加局部液态流动效果的简单动画,这些流动动画可能出现在缓缓流动的水流🌊、迎风飘动的旗帜🎏、游戏角色衣袖🧜♀️、随着时间缓动的云、雨、雾天气效果⛅等。这种过渡效果不仅节省了开发全量动画的成本,而且使得游戏画面更加热血、冒险、奥德赛、高级,也更加容易吸引玩家氪金💰。
本文使用前端开发技术,结合SVG和CSS来实现类似的液化流动效果。本文包含的知识点主要包括:mask-image遮罩、feTurbulence和feDisplacementMap滤镜、filter属性、canvas绘制方法、TimelineMax动画以及input[type=file]本地图片资源加载等。
先来看看实现效果,下面几个示例以及👆文章Banner图都是应用了由本文内容生成的液态流动动画效果。由于GIF图压缩比较严重,动画效果看起来不是很流畅🙃,大家不妨通过以下演示页面链接,亲自体验一下效果,生成自己的传说、典藏皮肤立绘吧😅。
🌀雾气扩散塞尔达传说:旷野之息
💃衣袖飘动貂蝉:猫影幻舞
🌅湖光波动
🔠文字液化
📌ps:体验页面部署在Gitpage上传图片功能不是真正上传到服务器,而是只会加载到浏览器本地,页面不会获取任何信息,大家可以放心体验,不用担心隐私泄漏问题。
页面主要由2部分构成,顶部用于加载图片 ,并且可以通过按住🖱鼠标划动的方式绘制热点路径,给图片添加流动效果;底部是控制区域,点击按钮🔘清除画布,可以清除绘制的流动动画效果、点击按钮🔘切换图片可以加载本地的图片。
📌注意,还有一个隐形的功能,当你绘制完成时,可以点击🖱鼠标右键,然后选择保存图片,保存的这张图片就是我们绘制流体动画路径的热点图,利用这张热点图,使用本文的CSS知识,就能把静态图片转化成动态图啦!
#sketch元素主要是用于绘制和加载流动效果热点图的画板;#button_container是页面底部的按钮控制区域;svg元素用于利用其filter滤镜实现液态流动动画效果,包括feTurbulence和feDisplacementMap滤镜。
<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">清除画布</button>
<button class="button"><input class="input" type="file" id="upload">上传图片</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>
接着看看样式的实现,main元素作为主容器并将主图案作为背景图片;canvas作为画布占据100%的空间位置;.mask和.mask-inner用于生成如下图所示热点路径与背景图相溶的效果,这种效果是借助mask-image实现的。最后,为了生成动态流动效果,.mask-inner通过filter: url(#heat)将前面生成的svg作为滤镜来源,后续即将在JavaScript中通过不间断修改svg滤镜的属性,来生成液态流动动画。
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')
}
mask-imageCSS属性用于设置元素上遮罩层的图像。
语法:
// 默认值,透明的黑色图像层,也就是没有遮罩层。
mask-image: none;
// <mask-source><mask>或CSS图像的url的值
mask-image: url(masks.svg#mask1);
// <image> 图片作为遮罩层
mask-image: linear-gradient(rgba(0, 0, 0, 1.0), transparent);
mask-image: image(url(mask.png), skyblue);
// 多个值
mask-image: image(url(mask.png), skyblue), linear-gradient(rgba(0, 0, 0, 1.0), transparent);
// 全局值
mask-image: inherit;
mask-image: initial;
mask-image: unset;
兼容性:
⚡此功能某些浏览器尚在开发中,需要使用浏览器前缀以兼容不同浏览器。
监听鼠标移动和点击事件,在canvas上绘制波动路径热点。
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});
`;
});
};
绘制完成后,可以在页面中右键保存生成的波动路径热点图,直接将绘制满意的热点图放到CSS中,就能给喜欢的图片添加局部波动效果了,下面这张图片就是本示例页面使用的波动的热点路径图。
为了生成实时更新的波动效果,本文使用了TweenMax来通过改变feTurbulence的baseFrequency属性值来实现,使用其他动画库或使用requestAnimationFrame也是可以实现相同的功能。
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);
点击清除画布按钮,可以清空已经绘制的波动路径,主要是通过清除页面元素mask-image的属性值以及清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();
})
});
点击切换图片,可以加载本地的一张图片作为绘制底图,该功能是通过input[type=file]来实现图片资源的获取,然后通过修改CSS将它设置成新的画布背景。
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;
`;
};
到这里,全部功能都实现完毕了,大家赶快制作一张自己喜欢的史诗皮肤或奥德赛小游戏的启动页面吧🤣。
本文包含的新知识点主要包括:
想了解其他前端知识或其他未在本文中详细描述的Web 3D开发技术相关知识,可阅读我往期的文章。