実演
遊び方
- スペースで玉発射
- 矢印キーで移動
- Rキーでリトライ
- Qキーで終了
コード
開発者ツール(F12キーででるやつ)のコンソールに貼り付けて実行
const racket = document.createElement("div");
const racketWidth = 100;
let racketX = (window.innerWidth - racketWidth) / 2;
racket.style.width = `${racketWidth}px`;
racket.style.height = "20px";
racket.style.backgroundColor = "#FFD700";
racket.style.position = "fixed";
racket.style.top = "75%";
racket.style.left = `${racketX}px`;
document.body.appendChild(racket);
let animeId = null;
let codeFrameSet = new Set();
const racketSpeedX = 8;
let shotFrameCount = 0;
const shotFrameMaxCount = 5;
const isPressed = {
left: false, right: false, space: false
};
let isResize = false;
function handleKeyDown(e) {
if (e.code === "ArrowLeft") {
isPressed.left = true;
}
else if (e.code === "ArrowRight") {
isPressed.right = true;
}
else if (e.code === "Space") {
e.preventDefault();
if (!isPressed.space) {
shotFrameCount = 0;
}
isPressed.space = true;
}
else if (e.code === "KeyR") {
cancelAnimationFrame(animeId);
for (const codeFrame of codeFrameSet) {
codeFrame.balls.forEach(ball => {
ball.parentNode.removeChild(ball);
});
codeFrame.balls = [];
codeFrame.codeSpans = [...codeFrame.querySelectorAll("code span")];
codeFrame.codeSpans.forEach(codeSpan => {
codeSpan.style.visibility = "";
});
}
update();
}
else if (e.code === "KeyQ") {
cancelAnimationFrame(animeId);
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("keyup", handleKeyUp);
document.body.removeChild(racket);
for (const codeFrame of codeFrameSet) {
codeFrame.balls.forEach(ball => {
ball.parentNode.removeChild(ball);
});
codeFrame.balls = [];
const codeSpans = codeFrame.querySelectorAll("code span");
codeSpans.forEach(codeSpan => {
codeSpan.style.visibility = "";
});
codeFrame.codeSpans = [];
}
codeFrameSet = null;
}
}
function handleKeyUp(e) {
if (e.code === "ArrowLeft") {
isPressed.left = false;
}
else if (e.code === "ArrowRight") {
isPressed.right = false;
}
else if (e.code === "Space") {
isPressed.space = false;
}
}
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("keyup", handleKeyUp);
window.addEventListener("resize", function() {
isResize = true;
});
function createBall(cx, cy) {
const ball = document.createElement("div");
ball.diameter = 20;
ball.x = cx - ball.diameter / 2;
ball.y = cy - ball.diameter / 2;
ball.speedX = getRandomSpeed(Math.random() < 0.5 ? -1 : 1);
ball.speedY = getRandomSpeed(-1);
ball.canHitRacket = false; // 最初にracketから発射されるため
ball.style.width = `${ball.diameter}px`;
ball.style.height = `${ball.diameter}px`;
ball.style.backgroundColor = `hsl(${Math.random() * (360-1)} 100% 50%)`;
ball.style.borderRadius = "50%";
ball.style.position = "absolute";
ball.style.left = `${ball.x}px`;
ball.style.top = `${ball.y}px`;
return ball;
}
function update() {
if (++shotFrameCount > shotFrameMaxCount) {
shotFrameCount = 1;
}
if (isResize) {
isResize = false;
for (const codeFrame of codeFrameSet) {
const codeFrameRect = codeFrame.getBoundingClientRect();
for (const codeSpan of codeFrame.codeSpans) {
const codeSpanRect = codeSpan.getBoundingClientRect();
codeSpan.x = codeSpanRect.x - codeFrameRect.x;
codeSpan.y = codeSpanRect.y - codeFrameRect.y;
codeSpan.width = codeSpanRect.width;
codeSpan.height = codeSpanRect.height;
}
}
}
if (isPressed.left) {
racketX -= racketSpeedX;
racket.style.left = `${racketX}px`;
}
if (isPressed.right) {
racketX += racketSpeedX;
racket.style.left = `${racketX}px`;
}
const racketRect = racket.getBoundingClientRect();
if (isPressed.space && shotFrameCount === 1) {
const elements = document.elementsFromPoint(
racketRect.x + racketRect.width / 2,
racketRect.y + racketRect.height / 2
);
for (const element of elements) {
if (element.classList.contains("code-frame")) {
const codeFrame = element;
const codeFrameRect = codeFrame.getBoundingClientRect();
if (!codeFrameSet.has(codeFrame)) {
codeFrameSet.add(codeFrame);
codeFrame.codeSpans = [...codeFrame.querySelectorAll("code span")];
for (const codeSpan of codeFrame.codeSpans) {
const codeSpanRect = codeSpan.getBoundingClientRect();
codeSpan.x = codeSpanRect.x - codeFrameRect.x;
codeSpan.y = codeSpanRect.y - codeFrameRect.y;
codeSpan.width = codeSpanRect.width;
codeSpan.height = codeSpanRect.height;
}
}
const ball = createBall(
racketRect.x - codeFrameRect.x + racketRect.width / 2,
racketRect.y - codeFrameRect.y + racketRect.height / 2
);
codeFrame.appendChild(ball);
if (codeFrame.balls === undefined) {
codeFrame.balls = [];
}
codeFrame.balls.push(ball);
break;
}
}
}
for (const codeFrame of codeFrameSet) {
const codeFrameRect = codeFrame.getBoundingClientRect();
const relativeRacket = {};
relativeRacket.x = racketRect.x - codeFrameRect.x;
relativeRacket.y = racketRect.y - codeFrameRect.y;
relativeRacket.width = racketRect.width;
relativeRacket.height = racketRect.height;
const removeBallIndexList = [];
for (let i = 0; i < codeFrame.balls.length; i++) {
const ball = codeFrame.balls[i];
ballPrevY = ball.y;
ball.x += ball.speedX;
ball.y += ball.speedY;
if (ball.x <= 0) {
ball.x = 0;
ball.speedX = -ball.speedX;
}
else if (ball.x + ball.diameter >= codeFrame.clientWidth) {
ball.x = codeFrame.clientWidth - ball.diameter;
ball.speedX = -ball.speedX;
}
if (ball.y <= 0) {
ball.y = 0;
ball.speedY = -ball.speedY;
}
else if (ball.y >= codeFrame.clientHeight) {
removeBallIndexList.unshift(i);
ball.parentNode.removeChild(ball);
continue;
}
const isHitIndexList = [];
let scrollXDiff = 0;
for (let i = 0; i < codeFrame.codeSpans.length; i++) {
const codeSpan = codeFrame.codeSpans[i];
if (i === 0) {
const tmp = codeSpan.getBoundingClientRect().x - codeFrameRect.x;
scrollXDiff = codeSpan.x - tmp;
}
if (isHit(ball, codeSpan, scrollXDiff)) {
isHitIndexList.unshift(i);
codeSpan.style.visibility = "hidden";
}
}
if (isHitIndexList.length > 0) {
isHitIndexList.forEach(index => codeFrame.codeSpans.splice(index, 1));
ball.speedX = -ball.speedX;
ball.speedY = -ball.speedY;
}
if (isHit(ball, relativeRacket)) {
if (ball.canHitRacket) {
ball.speedX = getRandomSpeed(ball.speedX > 0 ? 1 : -1);
ball.speedY = getRandomSpeed(ball.speedY > 0 ? -1 : 1);
ball.canHitRacket = false;
}
}
else {
ball.canHitRacket = true;
}
ball.style.left = `${ball.x}px`;
ball.style.top = `${ball.y}px`;
}
if (removeBallIndexList.length > 0) {
removeBallIndexList.forEach(index => codeFrame.balls.splice(index, 1));
}
}
animeId = requestAnimationFrame(update);
}
function getRandomSpeed(sign = 1) {
return (Math.random() * 5 + 2) * sign;
}
function isHit(ball, rect, scrollXDiff = 0) {
const cx = ball.x + ball.diameter / 2;
const cy = ball.y + ball.diameter / 2;
const tmpRectX = rect.x - scrollXDiff;
const rectX = clamp(cx, tmpRectX, tmpRectX + rect.width);
const rectY = clamp(cy, rect.y, rect.y + rect.height);
return ball.diameter / 2 >= Math.sqrt(Math.pow(cx - rectX, 2) + Math.pow(cy - rectY, 2));
}
function clamp(val, min, max) {
if (val < min) return min;
if (val > max) return max;
return val;
}
update();
関連記事