先来看下完成的效果:
实现
1. 一个非粒子化的时钟
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas-粒子时钟</title>
<style>
* {
margin: 0;
padding: 0;
}
canvas {
background:radial-gradient(#fff, #8c738c);
display: block;
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<canvas></canvas>
<script src="./index.js"></script>
</body>
</html>
index.js
function getRandom(min, max) {
return Math.floor(Math.random() * (max + 1 - min) + min)
}
function getCurrentTimeString() {
return new Date().toTimeString().slice(0, 8)
}
class ParticleClick {
constructor(el) {
/** @type {HTMLCanvasElement} */
this.canvas = document.querySelector(el)
/** @type {CanvasRenderingContext2D} */
this.ctx = this.canvas.getContext('2d')
/** @type {string | null} */
this.text = null
this.init()
}
init() {
this.canvas.width = window.innerWidth
this.canvas.height = window.innerHeight
}
clear() {
this.ctx.clearRect(0, 0, this.canvas.width,this.canvas.height)
}
draw() {
this.clear()
this.drawText()
requestAnimationFrame(() => this.draw())
}
drawText() {
const { ctx, canvas: {width, height} } = this
this.text = getCurrentTimeString()
// 开始画文本
ctx.beginPath()
// 文本样式
ctx.fillStyle = '#000'
ctx.textBaseline = 'middle'
ctx.font = '140px sans-serif'
// 画文本,位置水平和垂直居中
ctx.fillText(this.text, (width - ctx.measureText(this.text).width) / 2, height /2)
}
}
const clock = new ParticleClick('canvas')
clock.draw()
效果:
2. 粒子化
- 初始化 生成粒子,如下:
- 通过移动,粒子到达时间文本上的每个点
具体实现
- 粒子类
// 粒子类
class Particle {
/**
* 粒子类的构造函数
* @param {HTMLCanvasElement} canvas
* @param {CanvasRenderingContext2D} ctx
*/
constructor(canvas, ctx) {
this.canvas = canvas
this.ctx = ctx
// 初始化粒子位置,以canvas中心为圆心的圆周上
// 半径
const r = Math.min(canvas.width, canvas.height) / 2
// cx, cy 为圆心坐标
const cx = canvas.width / 2
const cy = canvas.height / 2
// 弧度
const rad = getRandom(0, 360) / 180 * Math.PI
this.x = cx + r * Math.cos(rad)
this.y = cy + r * Math.sin(rad)
// 粒子半径,随机
this.size = getRandom(4, 8)
}
draw() {
const { ctx } = this
ctx.beginPath()
ctx.fillStyle = '#5445544d'
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2)
ctx.fill()
}
}
- 获取时间文本上的像素点信息
class ParticleClick {
...原有的代码
getPoints() {
const gap = 6
const { ctx, canvas: { width, height } } = this
const { data } = ctx.getImageData(0, 0, width, height)
const points = []
for (let i = 0; i < width; i+= gap) {
for (let j = 0; j < height; j+= gap) {
// 这里getImageData 返回的数据是一个像素点由rgba四个数组成
const index = (i + j * 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
}
}
- 粒子开始是在圆周上的,需要移动到时间文本位置上,给
Particle添加上moveTo方法。
如果是直接移动,太生硬,需要给他加上动画(如下,在500ms内缓慢移动,就有了动画效果)
class Particle {
...原有的代码
moveTo(dx, dy) {
const duration = 500
const sx = this.x, sy = this.y
const xSpeed = (dx - sx) / duration
const ySpeed = (dy - sy) / duration
const startTime = Date.now()
const _move = () => {
const elapsedTime = Date.now() - startTime
this.x = sx + xSpeed * elapsedTime
this.y = sy + ySpeed * elapsedTime
if (elapsedTime >= duration) {
return
}
requestAnimationFrame(_move)
}
_move()
}
}
- 此时,我们就可以把粒子绘制到时间文本上了
class ParticleClick {
...原有的代码
constructor(el) {
...原有的代码
/** @type {Particle[]} */
this.particles = []
}
draw() {
this.clear()
this.drawText()
this.particles.forEach((p) => p.draw())
requestAnimationFrame(() => this.draw())
}
drawText() {
const { ctx, canvas: {width, height} } = this
const newText = getCurrentTimeString()
if (newText === this.text) {
return
}
this.text = newText
// 开始画文本
ctx.beginPath()
// 文本样式
ctx.fillStyle = '#000'
ctx.textBaseline = 'middle'
ctx.font = '140px sans-serif'
// 画文本,位置水平和垂直居中
ctx.fillText(this.text, (width - ctx.measureText(this.text).width) / 2, height /2)
const points = this.getPoints()
this.clear()
for (let i = 0; i < points.length; i++) {
let p = this.particles[i]
if (!p) {
p = new Particle(this.canvas, this.ctx)
this.particles.push(p)
}
const [x, y] = points[i]
p.moveTo(x, y)
}
// 去掉多余的例子
if (points.length < this.particles.length) {
this.particles.splice(points.length)
}
}
}
- 所有代码
function getRandom(min, max) {
return Math.floor(Math.random() * (max + 1 - min) + min)
}
function getCurrentTimeString() {
return new Date().toTimeString().slice(0, 8)
}
class ParticleClick {
constructor(el) {
/** @type {HTMLCanvasElement} */
this.canvas = document.querySelector(el)
/** @type {CanvasRenderingContext2D} */
this.ctx = this.canvas.getContext('2d', {
willReadFrequently: true
})
/** @type {string | null} */
this.text = null
/** @type {Particle[]} */
this.particles = []
this.init()
}
init() {
this.canvas.width = window.innerWidth
this.canvas.height = window.innerHeight
}
clear() {
this.ctx.clearRect(0, 0, this.canvas.width,this.canvas.height)
}
draw() {
this.clear()
this.drawText()
this.particles.forEach((p) => p.draw())
requestAnimationFrame(() => this.draw())
}
drawText() {
const { ctx, canvas: {width, height} } = this
const newText = getCurrentTimeString()
if (newText === this.text) {
return
}
this.text = newText
// 开始画文本
ctx.beginPath()
// 文本样式
ctx.fillStyle = '#000'
ctx.textBaseline = 'middle'
ctx.font = '140px sans-serif'
// 画文本,位置水平和垂直居中
ctx.fillText(this.text, (width - ctx.measureText(this.text).width) / 2, height /2)
const points = this.getPoints()
this.clear()
for (let i = 0; i < points.length; i++) {
let p = this.particles[i]
if (!p) {
p = new Particle(this.canvas, this.ctx)
this.particles.push(p)
}
const [x, y] = points[i]
p.moveTo(x, y)
}
// 去掉多余的例子
if (points.length < this.particles.length) {
this.particles.splice(points.length)
}
}
getPoints() {
const gap = 6
const { ctx, canvas: { width, height } } = this
const { data } = ctx.getImageData(0, 0, width, height)
const points = []
for (let i = 0; i < width; i+= gap) {
for (let j = 0; j < height; j+= gap) {
const index = (i + j * 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
}
}
// 粒子类
class Particle {
/**
* 粒子类的构造函数
* @param {HTMLCanvasElement} canvas
* @param {CanvasRenderingContext2D} ctx
*/
constructor(canvas, ctx) {
this.canvas = canvas
this.ctx = ctx
// 初始化粒子位置,以canvas中心为圆心的圆周上
// 半径
const r = Math.min(canvas.width, canvas.height) / 2
// cx, cy 为圆心坐标
const cx = canvas.width / 2
const cy = canvas.height / 2
// 弧度
const rad = getRandom(0, 360) / 180 * Math.PI
this.x = cx + r * Math.cos(rad)
this.y = cy + r * Math.sin(rad)
// 粒子半径,随机
this.size = getRandom(4, 8)
}
draw() {
const { ctx } = this
ctx.beginPath()
ctx.fillStyle = '#5445544d'
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2)
ctx.fill()
}
moveTo(dx, dy) {
const duration = 500
const sx = this.x, sy = this.y
const xSpeed = (dx - sx) / duration
const ySpeed = (dy - sy) / duration
const startTime = Date.now()
const _move = () => {
const elapsedTime = Date.now() - startTime
this.x = sx + xSpeed * elapsedTime
this.y = sy + ySpeed * elapsedTime
if (elapsedTime >= duration) {
return
}
requestAnimationFrame(_move)
}
_move()
}
}
const clock = new ParticleClick('canvas')
clock.draw()
效果: