外观
粒子时钟实现
粒子时钟实现方案
1、预期效果
- 网页刷新后,首先生成一定数量的粒子,随机分布在一个环形上
- 粒子迅速按照当前时间的数字为轨迹移动到指定位置上排列
- 粒子的排布随着时间的变化而变化
2、实现思路
创建一个画布,在画布上按照需求画出粒子,起始粒子所处的环形直径为画布高度
将每一个粒子当做一个对象,为确定其位置,设计属性
① 粒子本身属性:x坐标、y坐标、粒子半径(粒子大小随机)
② 粒子(圆心)所处位置
③ 起始位置形成的环的半径(画布高度的一半),粒子圆心坐标到环圆心坐标直线形成的夹角角度(随机,同时也为了确定起始位置)
④ 为类本身设计draw和moveTo两个方法,分别用于在画布上绘画和移动粒子
获取当前时间字符串,在画布上按照所需字体、大小使用黑色画出当前时间,然后在画布上找出颜色为黑色的像素点坐标,记录这些坐标,然后清空画布
Tips:不一定要获取所有坐标,因为粒子密度可以根据需求减小,因此可以设置一个步长来跳过一些坐标
在环形上动态调整所需(粒子)坐标数量,然后调用粒子本身的draw函数绘制出粒子,然后调用其moveTo函数移动到上一步获取到的坐标上去,不但重复这一过程。
3、代码实现(vue3环境,封装了组件)
<!-- 动态时钟组件 -->
<template>
<canvas ref="canvasRef"></canvas>
</template>
<script setup>
import { ref, onMounted, defineProps } from 'vue';
const canvasRef = ref(null);
const ctx = ref(null);
// 父组件数据
const props = defineProps({
// 粒子颜色
color: {
type: String,
default: "#5445544D"
},
// 字体
font: {
type: String,
default: 'sans-serif'
},
// 字体大小
fontSize: {
type: Number,
default: 240
},
// 粒子密度
density: {
type: Number,
default: 6
}
});
onMounted(async () => {
canvasRef.value.width = window.innerWidth * devicePixelRatio;
canvasRef.value.height = window.innerHeight * devicePixelRatio;
draw();
});
// 获取[min,max]之间的随机整数
const getRandom = (min, max) => Math.floor(Math.random() * (max - min + 1) + min);
// 时钟数字由一个个粒子构成,将每一个粒子当做一个对象,创建粒子对象
class Particle {
constructor() {
// 粒子半径
this.r = getRandom(2 * devicePixelRatio, 7 * devicePixelRatio);
// 环半径
const ar = Math.min(canvasRef.value.width, canvasRef.value.height) / 2;
// 粒子初始位置圆心与环圆心形成的夹角(随机)
const rad = getRandom(0, 360) * Math.PI / 180; // 角度转换为弧度
// 环圆心横坐标
const ax = canvasRef.value.width / 2;
// 环圆心纵坐标
const ay = canvasRef.value.height / 2;
// 粒子本身横坐标
this.x = ax + ar * Math.cos(rad);
// 粒子本身纵坐标
this.y = ay + ar * Math.sin(rad);
}
// 绘制粒子
draw() {
ctx.value.beginPath();
ctx.value.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
ctx.value.fillStyle = props.color || '#5445544D';
ctx.value.fill();
}
// 移动粒子
moveTo(tx, ty) {
const duration = 500; // 500ms运动时间
// 设置起始坐标
const sx = this.x, sy = this.y;
// 计算横纵向速度
const xSpeed = (tx - sx) / duration;
const ySpeed = (ty - sy) / duration;
// 记录开始运动时间
const startTime = new Date();
const _move = () => {
const t = Date.now() - startTime; // 当前已运动时间
const currX = sx + xSpeed * t; // 当前x坐标 = 起始x坐标 + x坐标速度 * 当前已运动时间
const currY = sy + ySpeed * t; // 当前y坐标 = 起始y坐标 + y坐标速度 * 当前已运动时间
this.x = currX;
this.y = currY;
// 停止条件
if (t >= duration) {
this.x = tx;
this.y = ty;
return;
}
requestAnimationFrame(_move);
}
_move();
}
}
// 用于存放粒子的数组
const particles = [];
// 清空画布函数
const clear = () => ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
// 重画粒子函数(每次移动都清空画布再重画,消除轨迹)
const draw = () => {
if (!canvasRef.value) return;
ctx.value = canvasRef.value.getContext('2d', { willReadFrequently: true });
// 1. 清空画布
clear();
// 2. 更新粒子数量和坐标
update();
// 3. 画出每一个粒子
for (let particle of particles) {
particle.draw();
}
requestAnimationFrame(draw);
}
// 获取当前时间文字
const getTimeText = () => new Date().toTimeString().substring(0, 8);
// 更新画布粒子
let curTimeText = undefined;
const update = () => {
// 判断差异,避免无意义更新
const text = getTimeText();
if (text === curTimeText) {
return;
}
curTimeText = text;
// 画文字
const { width, height } = canvasRef.value;
ctx.value.fillStyle = '#000';
ctx.value.textBaseline = 'middle';
ctx.value.font = `${props.fontSize * devicePixelRatio}px 'DS-Digital', ${props.font}`;
ctx.value.textAlign = 'center';
ctx.value.fillText(text, width / 2, height / 2);
// 获取像素信息
const points = getPoints();
// 获取像素点后清空画布,重新通过粒子画出时间文字
clear();
for (let i = 0; i < points.length; i++) {
const [x, y] = points[i];
let p = particles[i];
if (!p) {
p = new Particle();
particles.push(p);
}
p.moveTo(x, y);
}
if (points.length < particles.length) {
particles.splice(points.length);
}
};
// 从画布获取将要画的文字坐标
const getPoints = () => {
const points = [];
const { data } = ctx.value.getImageData(0, 0, canvasRef.value.width, canvasRef.value.height);
const gap = props.density; // 设置步长,不需要获取所有像素点(步长越大,生成的粒子数量越小)
for (let i = 0; i < canvasRef.value.width; i += gap) {
for (let j = 0; j < canvasRef.value.height; j += gap) {
const index = (i + j * canvasRef.value.width) * 4; // 每连续的四位为一个像素点的信息
const r = data[index];
const g = data[index + 1];
const b = data[index + 2];
const a = data[index + 3];
// 判断是否为黑色像素,为黑色像素则为所需坐标
if (r === 0 && g === 0 && b === 0 && a === 255) {
points.push([i, j]);
}
}
}
return points;
}
</script>
<style lang="scss" scoped>
canvas {
/*background: radial-gradient(#FFF, #8C738C);*/
background: transparent;
display: block;
width: 35vw;
height: 35vh;
}
</style>
4、实现效果
<Clock color="#283747AF" font="sans-serif" :fontSize="320" :density="6"/>