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


## 実演

![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/649673/c30b3347-e624-b21f-18dc-4c25e37525e9.gif)

## 遊び方

* スペースで玉発射
* 矢印キーで移動
* Rキーでリトライ
* Qキーで終了

## コード

開発者ツール（F12キーででるやつ）のコンソールに貼り付けて実行

```js
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();
```

## 関連記事

https://qiita.com/kotauchisunsun/items/d03c1e6936ffb250e4a1