鼠标交互的CANVAS动画壁纸
介绍一个自己摸鱼时间摸出来的一个canvas 2.5D风格方块交互动画的实现方法。
<div class="content">
<canvas
id="my3dCanvas"
width="750"
height="380">
</canvas>
</div>
.content{
display: flex;
width:750px;height:380px;
align-items: center;
justify-content: center;
}
.vp-code-demo-display{
max-height:600px!important;
}
// 常数设置
const RADIX3 = 1.73205 // 更号3常数
const UNITLONG = 15 // 单个方块大小
const TOPCOLOR1 = '#96eaea' // 方块配色
const TOPCOLOR2 = '#c9f1f1'
const LEFTCOLOR1 = '#16c2cd'
const LEFTCOLOR2 = '#1c878e'
const RIGHTCOLOR1 = '#2edad4'
const RIGHTCOLOR2 = '#08beb8'
const ROW = 15 // 渲染行数
const COL = 20 // 渲染列数
const GRAVITY = -10 // 重力加速度
const POWER = 15 // 升力加速度
var my3dCanvas = document.getElementById("my3dCanvas")
var ctx = my3dCanvas.getContext("2d")
var height = [] // 高度储存器
var speed = [] // 运动速度储存器
var touch = { x: 0, y: 0, work: false } // 被触发位置储存器
initParam()
var itv = setInterval(() => draw(), 50) // 计时器 0.05秒渲染一次 20帧
function initParam() { // 初始化运动状态储存器
for (let y = 0; y < ROW; y++) {
height[y] = []
speed[y] = []
for (let x = 0; x < COL; x++) {
height[y][x] = 0
speed[y][x] = 0
}
}
}
function draw() { // 渲染
calStatus() // 运算实时状态
for(let y = 0; y < ROW; y++){
for (let x = 0; x < COL; x++) {
if (y/2 === Math.floor(y / 2)){
drawBox(x * UNITLONG * RADIX3 * 2, y * UNITLONG * 3, height[y][x])
} else {
drawBox(x * UNITLONG * RADIX3 * 2 - UNITLONG * RADIX3, y * UNITLONG * 3, height[y][x])
}
}
}
}
function calStatus() { // 计算瞬时状态
for (let y = 0; y < ROW; y++) {
for (let x = 0; x < COL; x++) {
if (x === touch.x && y === touch.y && touch.work) {
speed[y][x] += POWER
touch.work = false
} else {
speed[y][x] = height[y][x] === 0 ? 0 : speed[y][x] + GRAVITY
}
height[y][x] = height[y][x] + speed[y][x] < 0 ? 0 : height[y][x] + speed[y][x]
}
}
}
my3dCanvas.onmousemove = e => { // 监听鼠标在画布上移动
let pt = getPointOnCanvas(my3dCanvas, e.clientX, e.clientY)
touch.work = true
touch.y = Math.floor(pt.y / (3 * UNITLONG))
if (pt.y/2 === Math.floor(pt.y / 2)) {
touch.x = Math.floor(pt.x / (2 * RADIX3 * UNITLONG))
} else {
touch.x = Math.floor((pt.x + UNITLONG * RADIX3) / (2 * RADIX3 * UNITLONG))
}
}
function drawBox(x, y, h) { // 画整个方体
drawTop(x, y, h)
drawLeft(x, y, h)
drawRight(x, y, h)
}
function drawTop(x, y, h) { // 画方体顶部
ctx.beginPath()
let grd = ctx.createLinearGradient(x, y - h - UNITLONG, x, y - h + UNITLONG);
grd.addColorStop(0, TOPCOLOR1)
grd.addColorStop(1, TOPCOLOR2)
ctx.fillStyle = grd
ctx.strokeStyle = grd
ctx.moveTo(x, y - h)
ctx.lineTo(x + RADIX3 * UNITLONG, y - UNITLONG - h)
ctx.lineTo(x + 2 * RADIX3 * UNITLONG, y - h)
ctx.lineTo(x + RADIX3 * UNITLONG, y + UNITLONG - h)
ctx.stroke()
ctx.fill()
ctx.closePath()
}
function drawLeft(x, y, h) { // 画方体左侧
ctx.beginPath()
let grd = ctx.createLinearGradient(x, y, x - UNITLONG, y + 2 * UNITLONG);
grd.addColorStop(0, LEFTCOLOR1)
grd.addColorStop(1, LEFTCOLOR2)
ctx.fillStyle = grd
ctx.strokeStyle = grd
ctx.moveTo(x, y - h)
ctx.lineTo(x, y + UNITLONG * 2)
ctx.lineTo(x + RADIX3 * UNITLONG, y + UNITLONG * 3)
ctx.lineTo(x + RADIX3 * UNITLONG, y + UNITLONG - h)
ctx.stroke()
ctx.fill()
ctx.closePath()
}
function drawRight(x, y, h) { // 画方体右侧
ctx.beginPath()
let grd = ctx.createLinearGradient(x + RADIX3 * UNITLONG, y + UNITLONG, x + RADIX3 * UNITLONG + UNITLONG, y
+ UNITLONG * 3);
grd.addColorStop(0, RIGHTCOLOR1)
grd.addColorStop(1, RIGHTCOLOR2)
ctx.fillStyle = grd
ctx.strokeStyle = grd
ctx.moveTo(x + RADIX3 * UNITLONG, y + UNITLONG - h)
ctx.lineTo(x + RADIX3 * UNITLONG, y + UNITLONG * 3)
ctx.lineTo(x + RADIX3 * UNITLONG * 2, y + UNITLONG * 2)
ctx.lineTo(x + RADIX3 * UNITLONG * 2, y - h)
ctx.stroke()
ctx.fill()
ctx.closePath()
}
function getPointOnCanvas(canvas, x, y) { // 窗口坐标转画布坐标
const bbox = canvas.getBoundingClientRect();
return {
x: x - bbox.left * (canvas.width / bbox.width),
y: y - bbox.top * (canvas.height / bbox.height)
}
}
实现
虽然画的是3d效果,但还是通过渲染绘制二维多边形去实现的。基本的核心思想是通过一个二维数组去维护小方块矩阵的位置、运动状态。而每个小方块都会根据自身的当前状态,用三个平行四边形拼接出立方体的效果,这样是不是听起来就很容易了。
定义一些常量和存储器
一些常量参数在代码中手动修改后运行,运行起来的动画效果也完全不一样哦。
// 常数设置
const RADIX3 = 1.73205 // 更号3常数
const UNITLONG = 20 // 单个方块大小
const TOPCOLOR1 = '#96eaea' // 方块配色
const TOPCOLOR2 = '#c9f1f1'
const LEFTCOLOR1 = '#16c2cd'
const LEFTCOLOR2 = '#1c878e'
const RIGHTCOLOR1 = '#2edad4'
const RIGHTCOLOR2 = '#08beb8'
const ROW = 15 // 渲染行数
const COL = 20 // 渲染列数
const GRAVITY = -10 // 重力加速度
const POWER = 15 // 升力加速度
var height = [] // 高度储存器
var speed = [] // 运动速度储存器
var touch = { x: 0, y: 0, work: false } // 被触发位置储存器
初始化运动状态储存器
把方块矩阵按照行列数初始化出两个二维数组,分别记录运动高度和垂直运动速度。
function initParam() {
for (let y = 0; y < ROW; y++) {
height[y] = []
speed[y] = []
for (let x = 0; x < COL; x++) {
height[y][x] = 0
speed[y][x] = 0
}
}
}
封装绘制函数
分别封装绘制顶部,左侧,右侧三个平行四边形的函数,再合起来调用一遍,就得到了绘制单个立方体的方法。
function drawBox(x, y, h) { // 画整个方体
drawTop(x, y, h)
drawLeft(x, y, h)
drawRight(x, y, h)
}
function drawTop(x, y, h) { // 画方体顶部
ctx.beginPath()
let grd = ctx.createLinearGradient(x, y - h - UNITLONG, x, y - h + UNITLONG);
grd.addColorStop(0, TOPCOLOR1)
grd.addColorStop(1, TOPCOLOR2)
ctx.fillStyle = grd
ctx.strokeStyle = grd
ctx.moveTo(x, y - h)
ctx.lineTo(x + RADIX3 * UNITLONG, y - UNITLONG - h)
ctx.lineTo(x + 2 * RADIX3 * UNITLONG, y - h)
ctx.lineTo(x + RADIX3 * UNITLONG, y + UNITLONG - h)
ctx.stroke()
ctx.fill()
ctx.closePath()
}
function drawLeft(x, y, h) { // 画方体左侧
ctx.beginPath()
let grd = ctx.createLinearGradient(x, y, x - UNITLONG, y + 2 * UNITLONG);
grd.addColorStop(0, LEFTCOLOR1)
grd.addColorStop(1, LEFTCOLOR2)
ctx.fillStyle = grd
ctx.strokeStyle = grd
ctx.moveTo(x, y - h)
ctx.lineTo(x, y + UNITLONG * 2)
ctx.lineTo(x + RADIX3 * UNITLONG, y + UNITLONG * 3)
ctx.lineTo(x + RADIX3 * UNITLONG, y + UNITLONG - h)
ctx.stroke()
ctx.fill()
ctx.closePath()
}
function drawRight(x, y, h) { // 画方体右侧
ctx.beginPath()
let grd = ctx.createLinearGradient(x + RADIX3 * UNITLONG, y + UNITLONG, x + RADIX3 * UNITLONG + UNITLONG, y
+ UNITLONG * 3);
grd.addColorStop(0, RIGHTCOLOR1)
grd.addColorStop(1, RIGHTCOLOR2)
ctx.fillStyle = grd
ctx.strokeStyle = grd
ctx.moveTo(x + RADIX3 * UNITLONG, y + UNITLONG - h)
ctx.lineTo(x + RADIX3 * UNITLONG, y + UNITLONG * 3)
ctx.lineTo(x + RADIX3 * UNITLONG * 2, y + UNITLONG * 2)
ctx.lineTo(x + RADIX3 * UNITLONG * 2, y - h)
ctx.stroke()
ctx.fill()
ctx.closePath()
}
实时运算与渲染
这一模块由3个函数构成: 监听onmousemove事件:鼠标移动对方块产生向上的加速度,把运动状态写入到速度存储器中。 计算瞬时状态:从存储器读取当前位置和速度,计算下一帧的位置和速度。 draw方法:计算完成下一帧的位置信息后,调用drawBox绘制下一帧的画面。
function draw() { // 渲染一帧画面
calStatus() // 运算实时状态
for(let y = 0; y < ROW; y++){
for (let x = 0; x < COL; x++) {
if (y/2 === Math.floor(y / 2)){
drawBox(x * UNITLONG * RADIX3 * 2, y * UNITLONG * 3, height[y][x])
} else {
drawBox(x * UNITLONG * RADIX3 * 2 - UNITLONG * RADIX3, y * UNITLONG * 3, height[y][x])
}
}
}
}
function calStatus() { // 计算瞬时状态
for (let y = 0; y < ROW; y++) {
for (let x = 0; x < COL; x++) {
if (x === touch.x && y === touch.y && touch.work) {
speed[y][x] += POWER
touch.work = false
} else {
speed[y][x] = height[y][x] === 0 ? 0 : speed[y][x] + GRAVITY
}
height[y][x] = height[y][x] + speed[y][x] < 0 ? 0 : height[y][x] + speed[y][x]
}
}
}
my3dCanvas.onmousemove = e => { // 监听鼠标在画布上移动
let pt = getPointOnCanvas(my3dCanvas, e.clientX, e.clientY)
touch.work = true
touch.y = Math.floor(pt.y / (3 * UNITLONG))
if (pt.y/2 === Math.floor(pt.y / 2)) {
touch.x = Math.floor(pt.x / (2 * RADIX3 * UNITLONG))
} else {
touch.x = Math.floor((pt.x + UNITLONG * RADIX3) / (2 * RADIX3 * UNITLONG))
}
}
开始动起来!
调用初始化! 用计数器循环调用draw方法绘制画面,成功啦!
initParam()
var itv = setInterval(() => draw(), 50) // 计时器 0.05秒渲染一次 20帧
有时候一开始也并不知道自己要做成什么效果,一步一步慢慢完善出来,最后才发现,摸掉的时间已经太久啦。