css 如何绘制水滴
- 可以通过box-shadow 来显示阴影
- 可以通过border-radius 改变水滴的形状
- 当然如果像要使其更加灵活,可以使用animation+keyframes关键帧+border-radius,让水滴动起来
是不是很简单
来吧展示效果
html代码,就只有一个div,然后使用一些伪类结合
<div class="water-container">
<div class="water"></div>
</div>
css代码
.water-container {
width: 50vw;
height: 50vh;
background-color: #eff0f4;
display: flex;
justify-content: center;
align-items: center;
}
.water {
width: 120px;
height: 120px;
border-radius: 50%;
box-shadow: inset 20px 20px 20px rgba(0, 0, 0, .05),
25px 35px 20px rgba(0, 0, 0, .05),
25px 30px 30px rgba(0, 0, 0, .05),
inset -20px -20px 25px hsla(0, 0%, 100%, .9);
position: relative;
}
.water::before {
content: "";
position: absolute;
left: 24%;
top: 25%;
width: 16px;
height: 16px;
background: #fff;
border-radius: 50%;
}
效果:
圆形水滴做好了,那我们是不是可以改变其现状呢,只要改变border-radius就可以了
.water {
width: 120px;
height: 120px;
/*改变形状,使其弯曲*/
border-radius: 30% 70% 70% 30% / 30% 35% 65% 70%;
box-shadow: inset 20px 20px 20px rgba(0, 0, 0, .05),
25px 35px 20px rgba(0, 0, 0, .05),
25px 30px 30px rgba(0, 0, 0, .05),
inset -20px -20px 25px hsla(0, 0%, 100%, .9);
position: relative;
}
效果:
形状改变了,我们就让其动起来,动起来就算加上关键帧改变border-radius
.water {
width: 120px;
height: 120px;
/*改变形状,使其弯曲*/
border-radius: 30% 70% 70% 30% / 30% 35% 65% 70%;
box-shadow: inset 20px 20px 20px rgba(0, 0, 0, .05),
25px 35px 20px rgba(0, 0, 0, .05),
25px 30px 30px rgba(0, 0, 0, .05),
inset -20px -20px 25px hsla(0, 0%, 100%, .9);
position: relative;
/*添加动画关键帧*/
animation: waterMove 4s linear infinite;
}
.water::before {
content: "";
position: absolute;
left: 24%;
top: 25%;
width: 16px;
height: 16px;
background: #fff;
border-radius: 30% 70% 70% 30% / 30% 35% 65% 70%;
animation: waterMove 4s linear infinite;
}
/* 关键帧 */
@keyframes waterMove {
20% {
border-radius: 30% 70% 53% 47% / 28% 44% 56% 72%;
}
40% {
border-radius: 30% 70% 39% 61% / 34% 39% 61% 66%;
}
60% {
border-radius: 25% 75% 45% 55% / 40% 55% 45% 60%;
}
80% {
border-radius: 28% 72% 31% 69% / 32% 39% 61% 68%;
}
}
效果:
完成后我们就可以在这个水滴里面添加东西了,比如设计登录页面
<div class="login-container">
<div class="login-content">
<div class="login-box">
<form action="" class="login-form">
<h2>LOGIN</h2>
<div class="input-box">
<input type="text" placeholder="用户名"/>
</div>
<div class="input-box">
<input type="password" placeholder="密码"/>
</div>
<div class="input-box">
<input type="submit" value="登录"/>
</div>
</form>
</div>
<div class="login-btn forget-pwd">
<span>忘记</span>
<span>密码</span>
</div>
<div class="login-btn register"><span>注册</span></div>
</div>
</div>
.login-container {
width: 50vw;
height: 50vh;
background: #eff0f4;
display: flex;
justify-content: center;
align-items: center;
opacity: 0.9;
}
.login-content {
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.login-box {
display: flex;
justify-content: center;
align-items: center;
height: 350px;
position: relative;
width: 350px;
border-radius: 52% 48% 33% 67% / 38% 45% 55% 62%;
box-shadow: inset 20px 20px 20px rgba(0, 0, 0, .05),
25px 35px 20px rgba(0, 0, 0, .05),
25px 30px 30px rgba(0, 0, 0, .05),
inset -20px -20px 25px hsla(0, 0%, 100%, .9);
/*transition: .5s;*/
animation: waterMove 9s linear infinite;
}
/* 关键帧 */
@keyframes waterMove {
20% {
border-radius: 30% 70% 53% 47% / 28% 44% 56% 72%;
}
40% {
border-radius: 30% 70% 39% 61% / 34% 39% 61% 66%;
}
60% {
border-radius: 25% 75% 45% 55% / 40% 55% 45% 60%;
}
80% {
border-radius: 28% 72% 31% 69% / 32% 39% 61% 68%;
}
}
.login-box::before {
content: '';
position: absolute;
top: 50px;
left: 85px;
width: 35px;
height: 35px;
background: #fff;
border-radius: 52% 48% 33% 67% / 38% 45% 55% 62%;
opacity: .9;
animation: waterMove 9s linear infinite;
}
.login-box::after {
content: '';
top: 90px;
position: absolute;
left: 70px;
width: 15px;
height: 15px;
border-radius: 52% 48% 33% 67% / 38% 45% 55% 62%;
background: #fff;
opacity: .9;
animation: waterMove 9s linear infinite;
}
.login-form {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: 40px;
gap: 15px;
}
.login-form h2 {
gap: 20px;
letter-spacing: .3em;
}
.input-box {
width: 225px;
box-shadow: inset 2px 5px 10px rgba(0, 0, 0, .1),
inset -2px -5px 10px rgba(255, 255, 255, 1),
15px 15px 10px rgba(0, 0, 0, .05),
15px 10px 15px rgba(0, 0, 0, .025);
position: relative;
border-radius: 25px;
}
.input-box:last-child {
width: 120px;
background: rgb(105, 199, 199);
transition: .6s;
box-shadow: inset 2px 5px 10px rgba(0, 0, 0, .1),
15px 15px 10px rgba(0, 0, 0, .05),
15px 10px 15px rgba(0, 0, 0, .025);
}
.input-box:last-child:hover {
width: 150px;
}
.input-box::before {
content: '';
position: absolute;
top: 8px;
left: 50%;
transform: translateX(-50%);
width: 65%;
height: 5px;
background: rgba(255, 255, 255, .5);
border-radius: 5px;
}
.input-box input {
border: none;
outline: none;
background: transparent;
width: 100%;
font-size: 1em;
padding: 10px 15px;
box-sizing: border-box;
}
.input-box:last-child input {
color: #fff;
text-transform: uppercase;
font-size: 1em;
cursor: pointer;
letter-spacing: .1em;
font-weight: 500;
}
.login-btn {
position: absolute;
right: -120px;
bottom: 0;
width: 120px;
height: 120px;
background: #c61dff;
color: #fff;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-wrap: wrap;
cursor: pointer;
text-decoration: none;
letter-spacing: .1em;
font-size: 1em;
transition: .25s;
box-shadow: inset 10px 10px 10px rgba(190, 1, 254, .05),
25px 35px 20px rgba(190, 1, 254, .1),
25px 30px 30px rgba(190, 1, 254, .1),
inset -10px -10px 15px rgba(255, 255, 255, .5);
border-radius: 44% 56% 65% 35% / 57% 58% 42% 43%;
user-select: none;
animation: waterMove2 9s linear infinite;
}
.login-btn::before {
content: '';
position: absolute;
top: 15px;
left: 30px;
width: 20px;
height: 20px;
border-radius: 44% 56% 35% / 57% 58% 42% 43%;
background: #fff;
opacity: .45;
}
.login-btn:hover, .login-btn:hover::before {
border-radius: 50%;
}
.register {
bottom: 150px;
right: -120px;
width: 80px;
height: 80px;
background: #01b4ff;
border-radius: 49% 51% 52% 48% / 63% 59% 41% 37%;
box-shadow: inset 10px 10px 10px rgba(1, 180, 255, .005),
25px 35px 20px rgba(1, 180, 255, .1),
25px 30px 30px rgba(1, 180, 255, .1),
inset -10px -10px 15px rgba(255, 255, 255, .5);
}
.register::before {
left: 20px;
width: 15px;
height: 15px;
border-radius: 49% 51% 52% 48% / 63% 59% 41% 37%;
}
@keyframes waterMove2 {
20% {
border-radius: 44% 56% 65% 35% / 57% 58% 42% 43%;
}
40% {
border-radius: 44% 56% 45% 45% / 67% 50% 46% 37%;
}
60% {
border-radius: 39% 61% 70% 30% / 60% 68% 50% 43%;
}
80% {
border-radius: 41% 58% 60% 40% / 52% 48% 70% 52%;
}
}
效果展示:
在这基础上在增加 canvas 背景
背景图片
import bridge from "../../assets/bridge.png";
class WaterRipple {
constructor(props) {
this.init(props);
}
init(props) {
this.boxRef = props.boxRef;
this.canvas = props.canvas;
this.ctx = this.canvas.getContext("2d", {willReadFrequently: true});
this.width = this.canvas.width;
this.height = this.canvas.height;
//最大振幅
this.max_amplitude = 1024;
this.amplitude = props.amplitude || 512;
this.background = props.background;
this.load = false;
this.img_data = null;
this.new_img_data = null;
this.animation_idx = null;
this.mousemove_interval = props.mousemove_interval || 50;
this.animation_interval = props.animation_interval || 20;
//波纹圆角
this.ripple_radius = props.ripple_radius || 3;
this.bridge = new Image();
this.bridge.src = bridge;
if (props.resize) {
this.setResize();
}
}
drawBackGround() {
if (this.background.complate) return;
this.ctx.drawImage(this.background, 0, 0, this.width, this.height);
this.img_data = this.ctx.getImageData(0, 0, this.width, this.height);
this.new_img_data = this.ctx.getImageData(0, 0, this.width, this.height);
this.buffer_1 = new Array(this.width).fill(0).map(() => new Array(this.height).fill(0));
this.buffer_2 = new Array(this.width).fill(0).map(() => new Array(this.height).fill(0));
this.load = true;
}
render() {
if (!this.load) {
this.drawBackGround();
return;
}
const w = this.width;
const h = this.height;
const half_w = w >> 1;
const half_h = h >> 1;
const len = this.new_img_data.data.length;
for (let i = 0; i < len; i += 4) {
const buffer_idx = i >> 2;
const x = buffer_idx % w;
const y = (buffer_idx - x) / w;
const left = Math.max(x - 1, 0);
const right = Math.min(x + 1, w - 1);
const top = Math.min(y + 1, h - 1);
const bottom = Math.max(y - 1, 0);
let next_amplitude =
this.buffer_1[x][top] +
this.buffer_1[x][bottom] +
this.buffer_1[right][y] +
this.buffer_1[left][y];
next_amplitude >>= 1;
next_amplitude -= this.buffer_2[x][y];
next_amplitude -= next_amplitude >> 5;
this.buffer_2[x][y] = next_amplitude;
const ratio = (this.max_amplitude - next_amplitude) / this.max_amplitude;
if (next_amplitude !== 0) {
let offset_x = (((x - half_w) * ratio) << 0) + half_w;
let offset_y = (((y - half_h) * ratio) << 0) + half_h;
offset_x = Math.min(offset_x, w - 1);
offset_x = Math.max(offset_x, 0);
offset_y = Math.max(offset_y, 0);
offset_y = Math.min(offset_y, h - 1);
const img_data_idx = (offset_y * w + offset_x) << 2;
const r = this.img_data.data[img_data_idx];
const g = this.img_data.data[img_data_idx + 1];
const b = this.img_data.data[img_data_idx + 2];
this.new_img_data.data[i] = r;
this.new_img_data.data[i + 1] = g;
this.new_img_data.data[i + 2] = b;
}
}
this.ctx.putImageData(this.new_img_data, 0, 0);
this.ctx.drawImage(
this.bridge,
0,
this.height * 0.35,
this.width,
this.height * 0.65
);
const temp_data = this.buffer_2;
this.buffer_2 = this.buffer_1;
this.buffer_1 = temp_data;
}
animate() {
let start = new Date().getTime();
const update = () => {
const end = new Date().getTime();
if (end - start > this.animation_interval) {
this.render();
start = end;
}
this.animation_idx = requestAnimationFrame(update);
};
update();
}
throttle(func, time) {
let start = new Date().getTime();
return function () {
const end = new Date().getTime();
if (end - start >= time) {
start = end;
func.apply(this, arguments);
}
};
}
ripple(x, y) {
if (!this.load) return;
const left = Math.max(x - this.ripple_radius, 0);
const right = Math.min(x + this.ripple_radius, this.buffer_1.length - 1);
const top = Math.max(y - this.ripple_radius, 0);
const bottom = Math.min(
y + this.ripple_radius,
this.buffer_1[0].length - 1
);
for (let i = left; i < right; i++) {
for (let j = top; j < bottom; j++) {
this.buffer_1[i][j] = this.amplitude;
}
}
}
addMousemove() {
this.canvas.addEventListener(
"mousemove",
this.throttle((e) => {
const {top, left} = this.canvas.getBoundingClientRect();
this.ripple(Math.floor(e.clientX - left), Math.floor(e.clientY - top));
}, this.mousemove_interval)
);
}
setResize() {
window.addEventListener("resize", () => {
const {offsetWidth, offsetHeight} = this.boxRef.value;
this.canvas.width = offsetWidth;
this.canvas.height = offsetHeight;
this.width = this.canvas.width;
this.height = this.canvas.height;
this.load = false;
});
}
stop() {
cancelAnimationFrame(this.animation_idx);
}
resume() {
this.animate();
}
}
export default WaterRipple
<template>
<div class="container" ref="boxRef">
<canvas ref="canvasRef"></canvas>
<login-page></login-page>
</div>
</template>
<script setup>
import WaterRipple from "@/components/FloatWater/floatWater.js";
import waterBg from '@/assets/water.png'
import {onMounted, onUnmounted, ref} from "vue";
import LoginPage from "@/components/LoginPage.vue";
let canvasWidth = 600;
let canvasHeight = 600;
let timer = null;
let waterRipple = null;
const boxRef = ref(null);
const canvasRef = ref(null);
onMounted(() => {
if (boxRef.value && canvasRef.value) {
const {offsetWidth, offsetHeight} = boxRef.value;
canvasWidth = offsetWidth;
canvasHeight = offsetHeight;
canvasRef.value.width = canvasWidth;
canvasRef.value.height = canvasHeight;
const waterImg = new Image();
waterImg.src = waterBg;
waterRipple = new WaterRipple({
canvas: canvasRef.value,
background: waterImg,
boxRef,
})
waterRipple.animate();
timer = window.setInterval(() => {
const x = Math.floor(canvasWidth * Math.random());
const y = Math.floor(canvasHeight * Math.random())
waterRipple?.ripple(x, y)
}, 1000)
waterRipple.addMousemove()
}
})
onUnmounted(() => {
timer && clearInterval(timer);
waterRipple?.stop()
})
</script>
<style scoped>
.container {
boxSizing: border-box;
width: 100vw;
height: 100vh;
display: flex;
position: relative;
}
</style>
效果展示: