<canvas>不支持canvas</canvas>
* {
  margin: 0;
  padding: 0;
}

canvas {
  display: block;
  background: radial-gradient(#ffffff, #8c738c);
}
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d', {
	willReadFrequently: true
})

// 粒子数量
const PARTICLE_NUM = 1500
// 绘制文本
let text = ''
// 绘制的文本字体大小
const FONT_SIZE = 140
// 粒子对象形成的数组
const particles = new Array(PARTICLE_NUM)
// 颗粒颜色
const COLOR = '#5445544d'
// 随机尺寸范围
const SIZES = [2, 7]
// 开始移动的时间
let startMoveTime = 0

// 获取[min, max] 范围内随机整数
function getRandom(min, max) {
	return Math.floor(Math.random() * (max + 1 - min) + min)
}

// 初始化
function init() {
	// 初始化画布尺寸
	canvas.width = window.innerWidth
	canvas.height = window.innerHeight
	// 初始化可以对象数组
	const cx = canvas.width / 2
	const cy = canvas.height / 2
	// 循环创建每一个颗粒点
	for (let i = 0; i < particles.length; i++) {
		const rad = Math.random() * 2 * Math.PI
		const size = getRandom(SIZES[0], SIZES[1])
		const r = canvas.height / 2
		particles[i] = {
			sx: cx + Math.cos(rad) * r,
			sy: cy + Math.sin(rad) * r,
			x: cx + Math.cos(rad) * r,
			y: cy + Math.sin(rad) * r,
			size
		}
		drawParticle(particles[i])
	}
}

// 刷新绘制
function fps() {
	requestAnimationFrame(() => {
		// 绘制的文本
		const curText = getText()

		if (curText !== text) {
			text = curText // 更新文本
			// 文本变化,颗粒要开始准备移动
			// 更新颗粒的其实坐标
			for (const p of particles) {
				p.sx = p.x
				p.sy = p.y
			}
			// 更新开始移动的时间点
			startMoveTime = Date.now()
		}

		// 获取像素点
		const imgData = getBMP()
		// 更新绘制
		update(imgData)
		fps()
	})
}

fps()

// 在画布上绘制一个点
function drawParticle(p) {
	ctx.fillStyle = COLOR
	ctx.beginPath()
	ctx.arc(p.x, p.y, p.size,0, Math.PI * 2, true)
	ctx.closePath()
	ctx.fill()
}

// 获取要绘制的文本
function getText() {
	return new Date().toTimeString().substring(0, 8)
}

// 绘制文本并得到像素信息
function getBMP() {
	const { width, height } = canvas
	clear()
	ctx.fillStyle = '#ffffff'
	ctx.textBaseline = 'middle'
	ctx.font = `${FONT_SIZE}px '手札体-简', sans-serif`
	const textWidth = ctx.measureText(text).width
	ctx.fillText(text, (width - textWidth) / 2, height / 2)
	const imgData = ctx.getImageData(0, 0, width, height)

	return imgData
}

// 更新绘制所以颗粒
function update(imgData) {
	clear()
	const { width, height, data} = imgData
	// 取点
	const dis = 4 // 间隔距离
	const pxls = [] // 颗粒目标坐标

	for (let w = 0; w < width; w += dis) {
		for( let h = 0; h < height; h += dis) {
			const i = (w + h * width) * 4
			if (data[i] > 10) {
				pxls.push([w, h])
			}
		}
	}

	const count = Math.min(particles.length, pxls.length)
	// 多少时间内到达目标位置(毫秒)
	const duration = 400
	// 现在已过了多少时间
	const timeSpan = Date.now() - startMoveTime

	for (let i = 0; i < count; i++) {
		const p = particles[i]
		// 起始位置
		const {sx, sy} = p
		// 目标位置
		const [tx, ty] = pxls[i]
		// x和y移动的距离
		const disX = tx - sx
		const disY = ty - sy
		// 得到一次移动偏移量
		let moveX = (disX / duration) * timeSpan
		let moveY = (disY / duration) * timeSpan

		if (Math.abs(moveX) > Math.abs(disX)) {
			moveX = disX
		}
		if (Math.abs(moveY) > Math.abs(disY)) {
			moveY = disY
		}

		// 设置新的坐标
		p.x = sx + moveX
		p.y = sy + moveY
		drawParticle(p)
	}
}

init()

// 清空画布
function clear() {
	const {width, height} = canvas
	ctx.clearRect(0, 0, width, height)
}
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.