<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>忍者闯关</title>
<style>
html,
body {
height: 100%;
margin: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
cursor: pointer;
}
.container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
#score {
position: absolute;
top: 30px;
right: 30px;
font-size: 2em;
font-weight: 900;
}
#introduction {
width: 200px;
height: 150px;
position: absolute;
font-weight: 600;
font-size: 0.8em;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
text-align: center;
transition: opacity 2s;
}
#restart {
width: 120px;
height: 120px;
position: absolute;
border-radius: 50%;
color: white;
background-color: red;
border: none;
font-weight: 700;
font-size: 1.2em;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: none;
cursor: pointer;
}
#perfect {
position: absolute;
opacity: 0;
transition: opacity 2s;
}
</style>
</head>
<body>
<div class="container">
<div id="score"></div>
<canvas id="game" width="375" height="375"></canvas>
<div id="introduction">按住鼠标可伸出一根棍子</div>
<div id="perfect">双倍积分</div>
<button id="restart">重新开始</button>
</div>
<script>
Array.prototype.last = function () {
return this[this.length - 1];
};
Math.sinus = function (degree) {
return Math.sin((degree / 180) * Math.PI);
};
let phase = 'waiting';
let lastTimestamp;
let heroX;
let heroY;
let sceneOffset;
let platforms = [];
let sticks = [];
let trees = [];
let score = 0;
const canvasWidth = 375;
const canvasHeight = 375;
const platformHeight = 100;
const heroDistanceFromEdge = 10;
const paddingX = 100;
const perfectAreaSize = 10;
const backgroundSpeedMultiplier = 0.2;
const hill1BaseHeight = 100;
const hill1Amplitude = 10;
const hill1Stretch = 1;
const hill2BaseHeight = 70;
const hill2Amplitude = 20;
const hill2Stretch = 0.5;
const stretchingSpeed = 4;
const turningSpeed = 4;
const walkingSpeed = 4;
const transitioningSpeed = 2;
const fallingSpeed = 2;
const heroWidth = 17;
const heroHeight = 30;
const canvas = document.getElementById('game');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d');
const introductionElement = document.getElementById('introduction');
const perfectElement = document.getElementById('perfect');
const restartButton = document.getElementById('restart');
const scoreElement = document.getElementById('score');
resetGame();
function resetGame() {
phase = 'waiting';
lastTimestamp = undefined;
sceneOffset = 0;
score = 0;
introductionElement.style.opacity = 1;
perfectElement.style.opacity = 0;
restartButton.style.display = 'none';
scoreElement.innerText = score;
platforms = [{ x: 50, w: 50 }];
generatePlatform();
generatePlatform();
generatePlatform();
generatePlatform();
sticks = [
{ x: platforms[0].x + platforms[0].w, length: 0, rotation: 0 },
];
trees = [];
generateTree();
generateTree();
generateTree();
generateTree();
generateTree();
generateTree();
generateTree();
generateTree();
generateTree();
generateTree();
heroX = platforms[0].x + platforms[0].w - heroDistanceFromEdge;
heroY = 0;
draw();
}
function generateTree() {
const minimumGap = 30;
const maximumGap = 150;
const lastTree = trees[trees.length - 1];
let furthestX = lastTree ? lastTree.x : 0;
const x =
furthestX +
minimumGap +
Math.floor(Math.random() * (maximumGap - minimumGap));
const treeColors = ['#6D8821', '#8FAC34', '#98B333'];
const color = treeColors[Math.floor(Math.random() * 3)];
trees.push({ x, color });
}
function generatePlatform() {
const minimumGap = 40;
const maximumGap = 200;
const minimumWidth = 20;
const maximumWidth = 100;
const lastPlatform = platforms[platforms.length - 1];
let furthestX = lastPlatform.x + lastPlatform.w;
const x =
furthestX +
minimumGap +
Math.floor(Math.random() * (maximumGap - minimumGap));
const w =
minimumWidth +
Math.floor(Math.random() * (maximumWidth - minimumWidth));
platforms.push({ x, w });
}
resetGame();
window.addEventListener('keydown', function (event) {
if (event.key == ' ') {
event.preventDefault();
resetGame();
return;
}
});
window.addEventListener('mousedown', function (event) {
if (phase == 'waiting') {
lastTimestamp = undefined;
introductionElement.style.opacity = 0;
phase = 'stretching';
window.requestAnimationFrame(animate);
}
});
window.addEventListener('mouseup', function (event) {
if (phase == 'stretching') {
phase = 'turning';
}
});
window.addEventListener('resize', function (event) {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
draw();
});
window.requestAnimationFrame(animate);
function animate(timestamp) {
if (!lastTimestamp) {
lastTimestamp = timestamp;
window.requestAnimationFrame(animate);
return;
}
switch (phase) {
case 'waiting':
return;
case 'stretching': {
sticks.last().length +=
(timestamp - lastTimestamp) / stretchingSpeed;
break;
}
case 'turning': {
sticks.last().rotation +=
(timestamp - lastTimestamp) / turningSpeed;
if (sticks.last().rotation > 90) {
sticks.last().rotation = 90;
const [nextPlatform, perfectHit] = thePlatformTheStickHits();
if (nextPlatform) {
score += perfectHit ? 2 : 1;
scoreElement.innerText = score;
if (perfectHit) {
perfectElement.style.opacity = 1;
setTimeout(() => (perfectElement.style.opacity = 0), 1000);
}
generatePlatform();
generateTree();
generateTree();
}
phase = 'walking';
}
break;
}
case 'walking': {
heroX += (timestamp - lastTimestamp) / walkingSpeed;
const [nextPlatform] = thePlatformTheStickHits();
if (nextPlatform) {
const maxHeroX =
nextPlatform.x + nextPlatform.w - heroDistanceFromEdge;
if (heroX > maxHeroX) {
heroX = maxHeroX;
phase = 'transitioning';
}
} else {
const maxHeroX =
sticks.last().x + sticks.last().length + heroWidth;
if (heroX > maxHeroX) {
heroX = maxHeroX;
phase = 'falling';
}
}
break;
}
case 'transitioning': {
sceneOffset += (timestamp - lastTimestamp) / transitioningSpeed;
const [nextPlatform] = thePlatformTheStickHits();
if (sceneOffset > nextPlatform.x + nextPlatform.w - paddingX) {
sticks.push({
x: nextPlatform.x + nextPlatform.w,
length: 0,
rotation: 0,
});
phase = 'waiting';
}
break;
}
case 'falling': {
if (sticks.last().rotation < 180)
sticks.last().rotation +=
(timestamp - lastTimestamp) / turningSpeed;
heroY += (timestamp - lastTimestamp) / fallingSpeed;
const maxHeroY =
platformHeight + 100 + (window.innerHeight - canvasHeight) / 2;
if (heroY > maxHeroY) {
restartButton.style.display = 'block';
return;
}
break;
}
default:
throw Error('Wrong phase');
}
draw();
window.requestAnimationFrame(animate);
lastTimestamp = timestamp;
}
function thePlatformTheStickHits() {
if (sticks.last().rotation != 90)
throw Error(`Stick is ${sticks.last().rotation}°`);
const stickFarX = sticks.last().x + sticks.last().length;
const platformTheStickHits = platforms.find(
(platform) =>
platform.x < stickFarX && stickFarX < platform.x + platform.w
);
if (
platformTheStickHits &&
platformTheStickHits.x +
platformTheStickHits.w / 2 -
perfectAreaSize / 2 <
stickFarX &&
stickFarX <
platformTheStickHits.x +
platformTheStickHits.w / 2 +
perfectAreaSize / 2
)
return [platformTheStickHits, true];
return [platformTheStickHits, false];
}
function draw() {
ctx.save();
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
drawBackground();
ctx.translate(
(window.innerWidth - canvasWidth) / 2 - sceneOffset,
(window.innerHeight - canvasHeight) / 2
);
drawPlatforms();
drawHero();
drawSticks();
ctx.restore();
}
restartButton.addEventListener('click', function (event) {
event.preventDefault();
resetGame();
restartButton.style.display = 'none';
});
function drawPlatforms() {
platforms.forEach(({ x, w }) => {
ctx.fillStyle = 'black';
ctx.fillRect(
x,
canvasHeight - platformHeight,
w,
platformHeight + (window.innerHeight - canvasHeight) / 2
);
if (sticks.last().x < x) {
ctx.fillStyle = 'red';
ctx.fillRect(
x + w / 2 - perfectAreaSize / 2,
canvasHeight - platformHeight,
perfectAreaSize,
perfectAreaSize
);
}
});
}
function drawHero() {
ctx.save();
ctx.fillStyle = 'black';
ctx.translate(
heroX - heroWidth / 2,
heroY + canvasHeight - platformHeight - heroHeight / 2
);
drawRoundedRect(
-heroWidth / 2,
-heroHeight / 2,
heroWidth,
heroHeight - 4,
5
);
const legDistance = 5;
ctx.beginPath();
ctx.arc(legDistance, 11.5, 3, 0, Math.PI * 2, false);
ctx.fill();
ctx.beginPath();
ctx.arc(-legDistance, 11.5, 3, 0, Math.PI * 2, false);
ctx.fill();
ctx.beginPath();
ctx.fillStyle = 'white';
ctx.arc(5, -7, 3, 0, Math.PI * 2, false);
ctx.fill();
ctx.fillStyle = 'red';
ctx.fillRect(-heroWidth / 2 - 1, -12, heroWidth + 2, 4.5);
ctx.beginPath();
ctx.moveTo(-9, -14.5);
ctx.lineTo(-17, -18.5);
ctx.lineTo(-14, -8.5);
ctx.fill();
ctx.beginPath();
ctx.moveTo(-10, -10.5);
ctx.lineTo(-15, -3.5);
ctx.lineTo(-5, -7);
ctx.fill();
ctx.restore();
}
function drawRoundedRect(x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x, y + radius);
ctx.lineTo(x, y + height - radius);
ctx.arcTo(x, y + height, x + radius, y + height, radius);
ctx.lineTo(x + width - radius, y + height);
ctx.arcTo(
x + width,
y + height,
x + width,
y + height - radius,
radius
);
ctx.lineTo(x + width, y + radius);
ctx.arcTo(x + width, y, x + width - radius, y, radius);
ctx.lineTo(x + radius, y);
ctx.arcTo(x, y, x, y + radius, radius);
ctx.fill();
}
function drawSticks() {
sticks.forEach((stick) => {
ctx.save();
ctx.translate(stick.x, canvasHeight - platformHeight);
ctx.rotate((Math.PI / 180) * stick.rotation);
ctx.beginPath();
ctx.lineWidth = 2;
ctx.moveTo(0, 0);
ctx.lineTo(0, -stick.length);
ctx.stroke();
ctx.restore();
});
}
function drawBackground() {
var gradient = ctx.createLinearGradient(0, 0, 0, window.innerHeight);
gradient.addColorStop(0, '#BBD691');
gradient.addColorStop(1, '#FEF1E1');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
drawHill(hill1BaseHeight, hill1Amplitude, hill1Stretch, '#95C629');
drawHill(hill2BaseHeight, hill2Amplitude, hill2Stretch, '#659F1C');
trees.forEach((tree) => drawTree(tree.x, tree.color));
}
function drawHill(baseHeight, amplitude, stretch, color) {
ctx.beginPath();
ctx.moveTo(0, window.innerHeight);
ctx.lineTo(0, getHillY(0, baseHeight, amplitude, stretch));
for (let i = 0; i < window.innerWidth; i++) {
ctx.lineTo(i, getHillY(i, baseHeight, amplitude, stretch));
}
ctx.lineTo(window.innerWidth, window.innerHeight);
ctx.fillStyle = color;
ctx.fill();
}
function drawTree(x, color) {
ctx.save();
ctx.translate(
(-sceneOffset * backgroundSpeedMultiplier + x) * hill1Stretch,
getTreeY(x, hill1BaseHeight, hill1Amplitude)
);
const treeTrunkHeight = 5;
const treeTrunkWidth = 2;
const treeCrownHeight = 25;
const treeCrownWidth = 10;
ctx.fillStyle = '#7D833C';
ctx.fillRect(
-treeTrunkWidth / 2,
-treeTrunkHeight,
treeTrunkWidth,
treeTrunkHeight
);
ctx.beginPath();
ctx.moveTo(-treeCrownWidth / 2, -treeTrunkHeight);
ctx.lineTo(0, -(treeTrunkHeight + treeCrownHeight));
ctx.lineTo(treeCrownWidth / 2, -treeTrunkHeight);
ctx.fillStyle = color;
ctx.fill();
ctx.restore();
}
function getHillY(windowX, baseHeight, amplitude, stretch) {
const sineBaseY = window.innerHeight - baseHeight;
return (
Math.sinus(
(sceneOffset * backgroundSpeedMultiplier + windowX) * stretch
) *
amplitude +
sineBaseY
);
}
function getTreeY(x, baseHeight, amplitude) {
const sineBaseY = window.innerHeight - baseHeight;
return Math.sinus(x) * amplitude + sineBaseY;
}
</script>
</body>
</html>