演示页面请看这里
HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>汉字像素排斥</title>
<style>
html,body{margin:0;height:100%;overflow:hidden;background:#000;display:flex;justify-content:center;align-items:center;font-family:monospace}
#stage{border:1px solid #333}
</style>
</head>
<body>
<canvas id="stage"></canvas>
<script>
/* ========== 配置 ========== */
const CONFIG_TEXT = '文于止墨'; // 要写什么汉字
const CONFIG_FONT_SIZE = 120; // 字体大小(像素)
const CONFIG_GRID_GAP = 5; // 每个“像素块”多大(正方形)
const CONFIG_REPULS_RADIUS = 300; // 鼠标排斥半径
const CONFIG_REPULS_STRENGTH = 100; // 排斥强度 0~1
const CONFIG_SPRING_BACK = 0.001; // 弹簧回拉系数
const CONFIG_FONT_FAMILY = 'serif'; // 字体样式
const CONFIG_REPULS_EXP_FACTOR = 0.1; // k 值,越大衰减越快
/* ========== 工具 ========== */
const $ = sel => document.querySelector(sel);
const stage = $('#stage');
const ctx = stage.getContext('2d');
/* ========== 像素实体 ========== */
class PixelGlyphEntity {
constructor(gridX, gridY) {
this.originX = gridX * CONFIG_GRID_GAP; // 原始网格坐标
this.originY = gridY * CONFIG_GRID_GAP;
this.x = this.originX;
this.y = this.originY;
this.vx = this.vy = 0; // 速度
}
update(mouseX, mouseY) {
const cx = this.x + CONFIG_GRID_GAP / 2;
const cy = this.y + CONFIG_GRID_GAP / 2;
const dx = cx - mouseX;
const dy = cy - mouseY;
const dist = Math.hypot(dx, dy);
if (dist < CONFIG_REPULS_RADIUS && dist > 0) {
// 指数衰减:F ∝ e^{-k·d}
const forceMag = CONFIG_REPULS_STRENGTH * Math.exp(-dist * CONFIG_REPULS_EXP_FACTOR);
this.vx += (dx / dist) * forceMag;
this.vy += (dy / dist) * forceMag;
}
// 原有弹簧与阻尼保持不变
this.vx += (this.originX - this.x) * CONFIG_SPRING_BACK;
this.vy += (this.originY - this.y) * CONFIG_SPRING_BACK;
this.vx *= 0.85;
this.vy *= 0.85;
this.x += this.vx;
this.y += this.vy;
}
draw() {
ctx.fillStyle = '#4fffa4';
ctx.fillRect(this.x, this.y, CONFIG_GRID_GAP - 1, CONFIG_GRID_GAP - 1);
}
}
/* ========== 主流程 ========== */
const pixelEntities = [];
function initPixelEntities() {
// 1. 离屏 canvas 渲染文字
const off = document.createElement('canvas');
const offCtx = off.getContext('2d');
off.width = off.height = 1024; // 足够大即可
offCtx.fillStyle = '#000';
offCtx.fillRect(0, 0, off.width, off.height);
offCtx.font = `bold ${CONFIG_FONT_SIZE}px ${CONFIG_FONT_FAMILY}`;
offCtx.textAlign = 'center';
offCtx.textBaseline = 'middle';
offCtx.fillStyle = '#fff';
offCtx.fillText(CONFIG_TEXT, off.width/2, off.height/2);
// 2. 取像素数据
const imgData = offCtx.getImageData(0, 0, off.width, off.height);
const { data, width } = imgData;
// 3. 生成实体
for (let y = 0; y < off.height; y += CONFIG_GRID_GAP) {
for (let x = 0; x < off.width; x += CONFIG_GRID_GAP) {
const idx = (y * width + x) * 4;
if (data[idx] > 128) { // 白色像素
pixelEntities.push(new PixelGlyphEntity(
Math.floor(x / CONFIG_GRID_GAP),
Math.floor(y / CONFIG_GRID_GAP)
));
}
}
}
// 4. 调整舞台大小
const cols = Math.ceil(off.width / CONFIG_GRID_GAP);
const rows = Math.ceil(off.height / CONFIG_GRID_GAP);
stage.width = cols * CONFIG_GRID_GAP;
stage.height = rows * CONFIG_GRID_GAP;
}
/* ========== 鼠标坐标 ========== */
let mouseX = -Infinity, mouseY = -Infinity;
stage.addEventListener('mousemove', e => {
const rect = stage.getBoundingClientRect();
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top;
});
stage.addEventListener('mouseleave', () => {
mouseX = mouseY = -Infinity;
});
/* ========== 动画循环 ========== */
function animate() {
ctx.clearRect(0, 0, stage.width, stage.height);
pixelEntities.forEach(p => {
p.update(mouseX, mouseY);
p.draw();
});
requestAnimationFrame(animate);
}
initPixelEntities();
animate();
</script>
</body>
</html>
发表回复