感觉最近喜欢把以前看到的效果重新实现一遍,以前看到很惊艳的效果,知道原理,但是代码能力不行,勉强写出来也是一团麻,只能作罢。但在心里一直是个疙瘩,感觉不自己实现一遍自己就不会一样。趁着最近不忙,写写练练,算是解开疙瘩。
原理分析
整个效果其实有两部分构成,一部分是倒计时效果,另外一部分就是小球散落效果。将它们联系起来的就是每次数字变化时的位置,我们要讲小球初始位置设置在时间显示位置上,然后让小球做自由落体。
代码实现
其实原理不难,难就难在如何去实现所有在 canvas 中绘制的元素的坐标一致性,如果都用同一个坐标系,那么在计算过程中会很繁琐。这里我们用一个个 stage 来记录元素的坐标系,将绘制动作集中在一起,这样可以不用在绘制每一个元素时都要去留意坐标系是什么,要不要更改坐标系。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>彩色倒计时</title>
<style>
#canvas {
width: 800;
height: 600px;
border: 1px solid;
}
#canvas.hover {
cursor: pointer;
}
</style>
</head>
<body>
<canvas id="canvas" width="1000" height="600"></canvas>
<script>
(function() {
// 定义数字显示二维数组,9(行) x 5(列)
var NUM_POINT_MAP = {
0: [[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1]],
1: [[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1]],
2: [[1, 1, 1, 1, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[1, 1, 1, 1, 1],
[1, 0, 0, 0, 0],
[1, 0, 0, 0, 0],
[1, 0, 0, 0, 0],
[1, 1, 1, 1, 1]],
3: [[1, 1, 1, 1, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[1, 1, 1, 1, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[1, 1, 1, 1, 1]],
4: [[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1]],
5: [[1, 1, 1, 1, 1],
[1, 0, 0, 0, 0],
[1, 0, 0, 0, 0],
[1, 0, 0, 0, 0],
[1, 1, 1, 1, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[1, 1, 1, 1, 1]],
6: [[1, 1, 1, 1, 1],
[1, 0, 0, 0, 0],
[1, 0, 0, 0, 0],
[1, 0, 0, 0, 0],
[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1]],
7: [[1, 1, 1, 1, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 1, 0],
[0, 0, 0, 1, 0],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0]],
8: [[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1]],
9: [[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[1, 1, 1, 1, 1]],
}
// 类式继承 extend 函数
function extend(subClass, superClass) {
// 用一个空函数引用父类原型
// 因为是空函数,所以在构造过程中不会出现因参数问题导致的报错
var F = function() {}
F.prototype = superClass.prototype
// 子类继承
subClass.prototype = new F()
// 重设 constructor
subClass.constructor = subClass
// 存储父类构造函数
subClass.superClass = superClass
}
// 订阅发布者
function Observer() {
this.subs = {}
}
Observer.prototype.addSub = function(name, func) {
if (!this.subs[name]) {
this.subs[name] = []
}
this.subs[name].push(func)
}
Observer.prototype.fire = function(name, data) {
var listeners = this.subs[name] || []
for (var i = 0, len = listeners.length; i < len; i++) {
listeners[i].call(this, data)
}
}
// 动画舞台
function Stage(canvas, options) {
if (options === undefined) {
throw new Error('Stage class required options arugment')
}
// canvas
this.canvas = canvas
// context
this.context = this.canvas.getContext('2d')
// 子元素集合
this.children = []
this.addChildren(options.children || [])
// 动画判断 flag
this.isRunning = false
// 坐标系
this.width = this.canvas.width
this.height = this.canvas.height
this.origin = options.origin || {x: 0, y: 0}
}
// 添加子元素
Stage.prototype.addChild = function(child) {
child.$parent = this
this.children.push(child)
}
Stage.prototype.addChildren = function(children) {
for (var i = 0, len = children.length; i < len; i++) {
this.addChild(children[i])
}
}
// 子元素绘制函数
Stage.prototype.draw = function() {
for (var i = 0, len = this.children.length; i < len; i++) {
this.children[i].draw(this.context)
}
}
// 舞台开始动画函数
Stage.prototype.start = function() {
if (!this.isRunning) {
var self = this
self.isRunning = true
self.context.save()
self.context.translate(self.origin.x, self.origin.y)
var animate = function() {
if (self.isRunning) {
self.context.clearRect(-self.origin.x, -self.origin.y, self.width, self.height)
self.draw()
window.requestAnimationFrame(animate)
}
}
animate()
}
}
// 舞台停止动画
Stage.prototype.stop = function() {
this.context.restore()
this.isRunning = false
}
// 自由运动小球
function Ball(x, y, r) {
this.x = x
this.vx = (function() {
var v = ~~(Math.random() * 16 - 8)
return v === 0 ? arguments.callee() : v
})()
this.y = y
this.vy = ~~(Math.random() * 16 - 8)
this.g = 0.98
this.r = r
this.c = this.getColor()
}
Ball.prototype.move = function() {
this.x += this.vx
this.y += (this.vy *= 0.96)
this.vy += this.g
}
Ball.prototype.draw = function(ctx) {
ctx.save()
ctx.translate(this.x, this.y)
ctx.fillStyle = this.c
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.arc(0, 0, this.r, 0, 2 * Math.PI)
ctx.closePath()
ctx.fill()
ctx.restore()
this.move()
}
Ball.prototype.getBoundingClientRect = function() {
return {
left: this.x - this.r,
right: this.x + this.r,
top: this.y - this.r,
bottom: this.y + this.r
}
}
Ball.prototype.getColor = function() {
return '#' + (~~(Math.random() * 16777215)).toString(16)
}
// 小球管理
function BallsManager(x, y, w, h) {
this.x = x
this.y = y
this.w = w
this.h = h
this.balls = []
}
BallsManager.prototype.addBall = function(ball) {
this.balls.push(ball)
}
BallsManager.prototype.addBalls = function(balls) {
for (var i = 0, len = balls.length; i < len; i++) {
this.addBall(balls[i])
}
}
BallsManager.prototype.isOut = function(ball) {
var rect = ball.getBoundingClientRect()
if (rect.left > this.x + this.w ||
rect.right < this.x) {
return true
}
if (rect.top > this.y + this.h) {
ball.vy *= -1
}
return false
}
BallsManager.prototype.draw = function(ctx) {
var i = 0
while(true) {
var ball = this.balls[i]
if (ball) {
if (this.isOut(ball)) {
this.balls.splice(i, 1)
} else {
ball.draw(ctx)
i++
}
} else {
break
}
}
}
var ballsManager = new BallsManager(0, 0, 1000, 600)
/**
* 0 ~ 9 数字显示对象
* @param {[type]} x [对象绘图基准坐标 x]
* @param {[type]} y [对象绘图基准坐标 y]
* @param {[type]} s [数字每个点宽度]
* @param {[type]} num [初始显示数字]
*/
function NumberBox(x, y, s, num) {
NumberBox.superClass.call(this)
this.x = x
this.y = y
this.s = s
this.n = 2
this.num = num || 0
this.points = []
this.createPoints()
}
extend(NumberBox, Observer)
NumberBox.prototype.draw = function(context) {
context.save()
context.translate(this.x, this.y)
this.drawPoint(context)
context.restore()
}
NumberBox.prototype.drawPoint = function(ctx) {
ctx.fillStyle = 'red'
var point
for (var i = 0, len = this.points.length; i < len; i++) {
point = this.points[i]
ctx.moveTo(point.x, point.y)
ctx.arc(point.x, point.y, point.r, 0, 2 * Math.PI)
}
ctx.closePath()
ctx.fill()
}
NumberBox.prototype.createPoints = function() {
this.points = []
var numPoint = NUM_POINT_MAP[this.num]
var row, col
var step, initX, initY
for (var y = 0, rows = numPoint.length; y < rows; y++) {
row = numPoint[y]
for (var x = 0, cols = row.length; x < cols; x++) {
col = row[x]
if (col === 1) {
step = this.s / this.n
initX = step / 2 - this.s / 2
initY = initX
for (var i = 0; i < this.n; i++) {
for (var j = 0; j < this.n; j++) {
this.points.push({
x: x * this.s + initX + step * i,
y: y * this.s + initY + step * j,
r: step / 2 * 0.96
})
}
}
}
}
}
}
NumberBox.prototype.getAbsolutePos = function(x, y) {
x = (function(box) {
return box.$parent ? arguments.callee(box.$parent) + box.x : box.origin.x
})(this) + x
y = (function(box) {
return box.$parent ? arguments.callee(box.$parent) + box.y : box.origin.y
})(this) + y
return {
x: x,
y: y
}
}
NumberBox.prototype.createBurstPoints = function() {
var balls = this.points.map(function(point) {
var pos = this.getAbsolutePos(point.x, point.y)
return new Ball(pos.x, pos.y, point.r)
}, this)
ballsManager.addBalls(balls)
}
NumberBox.prototype.plus = function() {
this.createBurstPoints()
if (++this.num > 9) {
this.fire('carry')
this.num = 0
}
this.fire('change', this.num)
this.createPoints()
}
// 两个点
function DotBox(x, y, w, h) {
this.x = x
this.y = y
this.w = w
this.h = h
}
DotBox.prototype.draw = function(ctx) {
ctx.save()
ctx.translate(this.x, this.y)
ctx.fillStyle = 'black'
ctx.beginPath()
ctx.moveTo(this.w / 2, 0)
ctx.arc(this.w / 2, 0, this.w / 6, 0, 2 * Math.PI)
ctx.moveTo(this.w / 2, this.h)
ctx.arc(this.w / 2, this.h, this.w / 6, 0, 2 * Math.PI)
ctx.closePath()
ctx.fill()
ctx.restore()
}
// 时间显示器构造函数
function TimeBox(time, x, y, w, h) {
this.x = x
this.y = y
this.w = w
this.h = h
this.hoursNumBoxs = []
this.minutesNumBoxs = []
this.secondsNumBoxs = []
this.dotBoxs = []
var date = new Date(time)
var hours = date.getHours()
this.createNumberBox(hours, 'hours')
var minutes = date.getMinutes()
this.createNumberBox(minutes, 'minutes')
var seconds = date.getSeconds()
this.createNumberBox(seconds, 'seconds')
this.createDotBox()
this.init()
var self = this
window.setInterval(function() {
self.secondsNumBoxs[0].plus()
}, 1000)
}
TimeBox.prototype.createDotBox = function() {
var dotBoxWidht = this.w / 14
var dotBoxHeight = this.h * 0.2
var step = this.w * 4 / 14
var y = (this.h - dotBoxHeight) / 2
for (var i = 0; i < 2; i++) {
var x = i * (step + dotBoxWidht) + step
this.addChild('dot', new DotBox(x, y, dotBoxWidht, dotBoxHeight))
}
}
TimeBox.prototype.createNumberBox = function(num, type) {
var numBoxWidth = this.w * 2 / 14
var numBoxHeight = numBoxWidth / 5 * 9
var s = numBoxWidth * 0.9 / 5
var step = 0
var units = num % 10
var unitsX = numBoxWidth + numBoxWidth * 0.1 / 2
var tens = ~~(num / 10)
var tensX = numBoxWidth * 0.1 / 2
var y = (this.h - numBoxHeight) / 2
var unitsNumBox
var tensNumBox
if (type === 'minutes') {
step = numBoxWidth * 2 + this.w / 14
} else if (type === 'seconds') {
step = numBoxWidth * 2 * 2 + this.w * 2 / 14
}
unitsNumBox = new NumberBox(unitsX + step, y, s, units)
tensNumBox = new NumberBox(tensX + step, y, s, tens)
this.addChildren(type, [unitsNumBox, tensNumBox])
}
TimeBox.prototype.addChild = function(type, child) {
child.$parent = this
if (type === 'minutes') {
this.minutesNumBoxs.push(child)
} else if (type === 'seconds') {
this.secondsNumBoxs.push(child)
} else if (type === 'hours') {
this.hoursNumBoxs.push(child)
} else if (type === 'dot') {
this.dotBoxs.push(child)
}
}
TimeBox.prototype.addChildren = function(type, children) {
for (var i = 0, len = children.length; i < len; i++) {
this.addChild(type, children[i])
}
}
TimeBox.prototype.init = function() {
var self = this
self.secondsNumBoxs[0].addSub('carry', function() {
self.secondsNumBoxs[1].plus()
})
self.secondsNumBoxs[1].addSub('change', function(val) {
if (val === 6) {
this.num = 0
self.minutesNumBoxs[0].plus()
}
})
self.minutesNumBoxs[0].addSub('carry', function() {
self.minutesNumBoxs[1].plus()
})
self.minutesNumBoxs[1].addSub('change', function(val) {
if (val === 6) {
this.num = 0
self.hoursNumBoxs[0].plus()
}
})
self.hoursNumBoxs[1].addSub('change', function(val) {
if (val === 2 && self.hoursNumBoxs[0].num === 3) {
this.num = 0
self.hoursNumBoxs[0].num = 0
}
})
}
TimeBox.prototype.draw = function(context) {
context.save()
context.translate(this.x, this.y)
for (var i = 0, len = this.hoursNumBoxs.length; i < len; i++) {
this.hoursNumBoxs[i].draw(context)
}
for (var i = 0, len = this.minutesNumBoxs.length; i < len; i++) {
this.minutesNumBoxs[i].draw(context)
}
for (var i = 0, len = this.secondsNumBoxs.length; i < len; i++) {
this.secondsNumBoxs[i].draw(context)
}
for (var i = 0, len = this.dotBoxs.length; i < len; i++) {
this.dotBoxs[i].draw(context)
}
context.restore()
}
var timeBox = new TimeBox(new Date(), 100, 0, 800, 300)
// 新建舞台
var canvasEl = document.querySelector('#canvas')
var stage = new Stage(canvasEl, {
children: [
timeBox,
ballsManager
],
origin: {
x: 0,
y: 0
}
})
stage.start()
})()
</script>
</body>
</html>