239
100

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Qiita】糞コードは直すな!ぶっ壊せ!

Last updated at Posted at 2023-09-19

実演

sample.gif

遊び方

  • スペースで玉発射
  • 矢印キーで移動
  • 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();

関連記事

239
100
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
239
100

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?