纯 JS 简单实现类似 404 可跳跃障碍物页面

1,313 阅读4分钟

废话开篇:一些 404 页面为了体现趣味性会添加一些简单的交互效果。 这里用纯 JS 简单实现类似 404 可跳跃障碍物页面,内容全部用 canvas 画布实现。

一、效果展示

屏幕录制2022-01-24 上午10.32.10.gif

二、画面拆解

1、绘制地平线

地平线这里就是简单的一条贯穿屏幕的线。

2、绘制红色精灵

绘制红色精灵分为两部分:

(1)上面圆

(2)下面定点与上面圆的切线。

绘制结果:

image.png

进行颜色填充,再绘制中小的小圆,绘制结果:

image.png

(3)绘制障碍物

这里绘制的是一个黑色的长方形。最后的实现效果:

image.png

三、逻辑拆解

1、全局定时器控制画布重绘

创建全局的定时器。

它有两个具体任务:

(1)全局定时刷新重置,将画布定时擦除之前的绘制结果。

(2)全局定时器刷新动画重绘新内容。

2、精灵跳跃动作

在接收到键盘 “空格” 点击的情况下,让精灵起跳一定高度,到达顶峰的时候进行回落,当然这里设计的是匀速。

3、障碍物移动动作

通过定时器,重绘障碍物从最右侧移动到最左侧。

4、检测碰撞

在障碍物移动到精灵位置时,进行碰撞检测,判断障碍物最上端的左、右顶点是否在精灵的内部。

5、绘制提示语

提示语也是用 canvas 绘制的,当障碍物已移动到左侧的时候进行,结果判断。如果跳跃过程中无碰撞,就显示 “完美跳跃~”,如果调跃过程中有碰撞,就显示 “再接再厉”。

四、代码讲解

1、HTML
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
		<script src="./wsl404.js"></script>
	</head>
	<body>
		<div id="content">
			<canvas id="myCanvas">
			</canvas>
		</div>
	</body>
	<script>
		elves.init();
	</script>
</html>
2、JS

(function WSLNotFoundPage(window) {
	var elves = {};//精灵对象
	elves.ctx = null;//画布
	elves.width = 0;//屏幕的宽度
	elves.height = 0;//屏幕的高度
	elves.point = null;//精灵圆中心
	elves.elvesR = 20;//精灵圆半径
	elves.runloopTargets = [];//任务序列(暂时只保存跳跃)
	elves.upDistance = 50;//当前中心位置距离地面高度
	elves.upDistanceInitNum = 50;//中心位置距离地面高度初始值
	elves.isJumping = false;//是否跳起
	elves.jumpTarget = null;//跳跃任务
	elves.jumpTop = false;//是否跳到最高点
	elves.maxCheckCollisionWith = 0;//碰撞检测的最大宽度尺寸
	elves.obstaclesMovedDistance = 0;//障碍物移动的距离
	elves.isCollisioned = false;//是否碰撞过
	elves.congratulationFont = 13;//庆祝文字大小
	elves.congratulationPosition = 40;//庆祝文字位移
	elves.isShowCongratulation = false;//是否展示庆祝文字
	elves.congratulationContent = "完美一跃~";
	elves.congratulationColor = "red";
	
	//初始化
	elves.init = function(){
		this.drawFullScreen("content");
		this.drawElves(this.upDistance);
		this.keyBoard();   
		this.runloop();
	}
	
	//键盘点击事件
	elves.keyBoard = function(){
		var that = this;
		document.onkeydown = function whichButton(event)
		{
		  if(event.keyCode == 32){
		    //空格
		    that.elvesJump();
		   }
		 }
	}
	
	//开始跑圈
	elves.runloop = function(){
		var that = this;
		setInterval(function(){
			//清除画布
			that.cleareAll();
			//绘制障碍物
			that.creatObstacles();
			if(that.isJumping == false){
				//未跳起时重绘精灵
				that.drawElves(that.upDistanceInitNum);
			}
			//绘制地面
			that.drawGround();
			
			//跳起任务
			for(index in that.runloopTargets){
				let target = that.runloopTargets[index];
				if(target.isRun != null && target.isRun == true){
					
					if(target.runCallBack){
						target.runCallBack();
					}
				}
			}
			//碰撞检测
			that.checkCollision();
			//展示庆祝文字
			if(that.isShowCongratulation == true){
				that.congratulation();
			}
			
		},10);
	}
	
	//画布
	elves.drawFullScreen = function (id){
		var element = document.getElementById(id);
		this.height = window.screen.height - 200;
		this.width = window.screen.width;
		element.style.width = this.width + "px";
		element.style.height = this.height + "px";
		element.style.background = "white";
		this.getCanvas("myCanvas",this.width,this.height);
	}
	
	elves.getCanvas = function(id,width,height){
		var c = document.getElementById(id);
		this.ctx = c.getContext("2d");
		//锯齿修复
		if (window.devicePixelRatio) {
		   c.style.width = this.width + "px";
		   c.style.height = this.height + "px";
		   c.height = height * window.devicePixelRatio;
		   c.width = width * window.devicePixelRatio;
		   this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
		}
	};
	
	//绘制地面
	elves.drawGround = function() {
		// 设置线条的颜色
		this.ctx.strokeStyle = 'gray';
		// 设置线条的宽度
		this.ctx.lineWidth = 1;
		// 绘制直线
		this.ctx.beginPath();
		// 起点
		this.ctx.moveTo(0, this.height / 2.0 + 1);
		// 终点
		this.ctx.lineTo(this.width,this.height / 2.0);
		this.ctx.closePath();
		this.ctx.stroke();
	}
	
	//绘制精灵
	elves.drawElves = function(upDistance){
		
		//绘制圆
		var angle = Math.acos(this.elvesR / upDistance);
		this.point = {x:this.width / 3,y : this.height / 2.0 - upDistance};
		this.ctx.fillStyle = "#FF0000";
		this.ctx.lineWidth = 1;
		this.ctx.beginPath();
		this.ctx.arc(this.point.x,this.point.y,this.elvesR,Math.PI / 2 + angle,Math.PI / 2 - angle,false);
		
		//绘制切线
		var bottomPoint = {x:this.width / 3,y : this.point.y + this.upDistanceInitNum};
		
		let leftPointY = this.height / 2.0 - (upDistance - Math.cos(angle) * this.elvesR);
		let leftPointX = this.point.x - (Math.sin(angle) * this.elvesR);
		var leftPoint = {x:leftPointX,y:leftPointY};
		
		let rightPointY = this.height / 2.0 - (upDistance - Math.cos(angle) * this.elvesR);
		let rightPointX = this.point.x + (Math.sin(angle) * this.elvesR);
		var rightPoint = {x:rightPointX,y:rightPointY};
		
		this.maxCheckCollisionWith = (rightPointX - leftPointX) * 20 / (upDistance - Math.cos(angle) * this.elvesR);
		this.ctx.moveTo(bottomPoint.x, bottomPoint.y);
		this.ctx.lineTo(leftPoint.x,leftPoint.y);
		this.ctx.lineTo(rightPoint.x,rightPoint.y);
		
		this.ctx.closePath();
		this.ctx.fill();
		
		//绘制小圆
		this.ctx.fillStyle = "#FFF";
		this.ctx.lineWidth = 1;
		this.ctx.beginPath();
		this.ctx.arc(this.point.x,this.point.y,this.elvesR / 3,0,Math.PI * 2,false);
		this.ctx.closePath();
		this.ctx.fill();
	}
	
	//清除画布
	elves.cleareAll = function(){
		this.ctx.clearRect(0,0,this.width,this.height);
	}
	
	//精灵跳动
	elves.elvesJump = function(){
		if(this.isJumping == true){
			return;
		}
		this.isJumping = true;
		if(this.jumpTarget == null){
			var that = this;
			this.jumpTarget = {type:'jump',isRun:true,runCallBack:function(){
				let maxDistance = that.upDistanceInitNum + 55;
				if(that.jumpTop == false){
					if(that.upDistance > maxDistance){
						that.jumpTop = true;
					}
					that.upDistance += 1;
				} else if(that.jumpTop == true) {
					that.upDistance -= 1;
					if(that.upDistance < 50) {
						that.upDistance = 50;
						that.jumpTop = false;
						that.jumpTarget.isRun = false;
						that.isJumping = false;
					}
				}
				that.drawElves(that.upDistance);
			}};
			this.runloopTargets.push(this.jumpTarget);
		} else {
			this.jumpTarget.isRun = true;
		}
	}
	
	//绘制障碍物
	elves.creatObstacles = function(){
		let obstacles = {width:20,height:20};
		if(this.obstaclesMovedDistance != 0){
			this.ctx.clearRect(this.width - obstacles.width - this.obstaclesMovedDistance + 0.5, this.height / 2.0 - obstacles.height,obstacles.width,obstacles.height);
		}
		this.obstaclesMovedDistance += 0.5;
		if(this.obstaclesMovedDistance >= this.width + obstacles.width) {
			this.obstaclesMovedDistance = 0; 
			//重置是否碰撞
			this.isCollisioned = false;
		}
		this.ctx.beginPath();
		this.ctx.fillStyle = "#000";
		this.ctx.moveTo(this.width - obstacles.width - this.obstaclesMovedDistance, this.height / 2.0 - obstacles.height);
		this.ctx.lineTo(this.width - this.obstaclesMovedDistance,this.height / 2.0 - obstacles.height);
		this.ctx.lineTo(this.width - this.obstaclesMovedDistance,this.height / 2.0);
		this.ctx.lineTo(this.width - obstacles.width - this.obstaclesMovedDistance, this.height / 2.0);
		this.ctx.closePath();
		this.ctx.fill();
	}
	
	//检测是否碰撞
	elves.checkCollision = function(){
		var obstaclesMarginLeft = this.width - this.obstaclesMovedDistance - 20;
		var elvesUpDistance = this.upDistanceInitNum - this.upDistance + 20;
		if(obstaclesMarginLeft > this.point.x - this.elvesR && obstaclesMarginLeft < this.point.x + this.elvesR && elvesUpDistance <= 20) {
			//需要检测的最大范围
			let currentCheckCollisionWith = this.maxCheckCollisionWith * elvesUpDistance / 20;
			if((obstaclesMarginLeft < this.point.x + currentCheckCollisionWith / 2.0 && obstaclesMarginLeft > this.point.x - currentCheckCollisionWith / 2.0) || (obstaclesMarginLeft + 20 < this.point.x + currentCheckCollisionWith / 2.0 && obstaclesMarginLeft + 20 > this.point.x - currentCheckCollisionWith / 2.0)){
				this.isCollisioned = true;
			}
		}
		
		//记录障碍物移动到精灵左侧
		if(obstaclesMarginLeft + 20 < this.point.x - this.elvesR && obstaclesMarginLeft + 20 > this.point.x - this.elvesR - 1){
			if(this.isCollisioned == false){
				//跳跃成功,防止检测距离内重复得分置为true,在下一次循环前再置为false
				this.isCollisioned = true;
				//庆祝
				if(this.isShowCongratulation == false) {
					this.congratulationContent = "完美一跃~";
					this.congratulationColor = "red";
					this.isShowCongratulation = true;
				}
			} else {
				//鼓励
				if(this.isShowCongratulation == false) {
					this.isShowCongratulation = true;
					this.congratulationColor = "gray";
					this.congratulationContent = "再接再厉~";
				}
			}
		}
	}
	
	//庆祝绘制文字
	elves.congratulation = function(){
		
		this.congratulationFont += 0.1;
		this.congratulationPosition += 0.1;
		if(this.congratulationFont >= 30){
                        //重置
			this.congratulationFont = 13;
			this.congratulationPosition = 30;
			this.isShowCongratulation = false;
			return;
		}
		this.ctx.fillStyle = this.congratulationColor;        
		this.ctx.font = this.congratulationFont + 'px "微软雅黑"';        
		this.ctx.textBaseline = "bottom";         
		this.ctx.textAlign = "center"; 
		this.ctx.fillText( this.congratulationContent, this.point.x, this.height / 2.0 - this.upDistanceInitNum - this.congratulationPosition);   
	}
	
	window.elves = elves;
})(window)

五、总结与思考

逻辑注释基本都写在代码里,里面的一些计算可能会绕一些。代码拙劣,大神勿笑[抱拳][抱拳][抱拳]