08壁に当たると角度が反転する跳ね返り
ボールが左右の壁に当たる瞬間、水平方向の角度値の符号だけが反転し垂直方向の値はそのまま残り、入った角度どおりに跳ね返る。
使いどころ境界衝突の物理デモ
mirrorWallBounceWALL_MARGINmirrorFlashTimer

<main class="stage">
<div class="vd is-demo" id="vdRoot">
<canvas class="vd__canvas" id="vdCanvas" width="480" height="300"></canvas>
<p class="vd__caption">壁に当たると角度が反転する跳ね返り</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
@include stage($stage-paper);
.vd {
position: relative;
width: min(92vw, calc(160vh - 12.8vw));
aspect-ratio: 480 / 300;
margin: 0 auto;
border-radius: $r-card;
overflow: hidden;
box-shadow: $shadow-raised;
}
.vd__canvas {
display: block;
width: 100%;
height: 100%;
}
.vd__caption {
position: absolute;
left: $sp-3;
bottom: $sp-3;
max-width: 72%;
margin: 0;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
background: rgba(255, 247, 230, .85);
color: $subject-ink;
font-weight: 500;
font-size: 12px;
line-height: 1.4;
pointer-events: none;
}
@media (max-width: 340px) {
.vd__caption { font-size: 10px; padding: $sp-1 $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
.vd { cursor: default; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.vd {
position: relative;
width: min(92vw, 160vh - 12.8vw);
aspect-ratio: 480/300;
margin: 0 auto;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.vd__canvas {
display: block;
width: 100%;
height: 100%;
}
.vd__caption {
position: absolute;
left: 12px;
bottom: 12px;
max-width: 72%;
margin: 0;
padding: 4px 12px;
border-radius: 999px;
background: rgba(255, 247, 230, 0.85);
color: #17141a;
font-weight: 500;
font-size: 12px;
line-height: 1.4;
pointer-events: none;
}
@media (max-width: 340px) {
.vd__caption {
font-size: 10px;
padding: 4px 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.vd {
cursor: default;
}
}(function () {
var root = document.getElementById('vdRoot');
var canvas = document.getElementById('vdCanvas');
var ctx = canvas.getContext('2d');
var wrap = canvas.parentElement;
var LW = 320, LH = 200, WALL = 6;
var PADDLE_H = 8, PADDLE_Y = LH - 18, PADDLE_W = 46;
var BALL_R = 4;
var BRICK_COLS = 6, BRICK_ROWS = 3, BRICK_W = 44, BRICK_H = 12, BRICK_GAP = 4, BRICK_TOP = 22;
var BASE_SPEED = 130;
var BIAS_PERIOD = 560;
var STAGE_BG = '#17141a';
var BRICK_ROW_COLORS = ['#2f6df6', '#ff9ebb', '#4cd4a6'];
var PADDLE_COLOR = '#fff7e6';
var BALL_COLOR = '#fff7e6';
var FLASH_LIFE = 0.4, FLASH_MAX_R = 130;
function seedRand(seed) {
var s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
var rand = seedRand(41008);
function clamp(v, a, b) { return v < a ? a : (v > b ? b : v); }
function aabb(ax, ay, aw, ah, bx, by, bw, bh) { return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by; }
var PADDLE_LERP = 0.32;
var MAX_REFLECT_ANGLE = Math.PI / 180 * 48;
var WALL_MARGIN = WALL;
function buildBricks() {
var totalW = BRICK_COLS * BRICK_W + (BRICK_COLS - 1) * BRICK_GAP;
var startX = (LW - totalW) / 2;
var arr = [];
for (var r = 0; r < BRICK_ROWS; r += 1) {
for (var c = 0; c < BRICK_COLS; c += 1) {
arr.push({ x: startX + c * (BRICK_W + BRICK_GAP), y: BRICK_TOP + r * (BRICK_H + BRICK_GAP), w: BRICK_W, h: BRICK_H, alive: true, hits: 0 });
}
}
return arr;
}
var bricks = buildBricks();
var cleared = 0;
var mirrorFlashSide = null; var mirrorFlashTimer = 0;
function rebuildLevel() {
bricks = buildBricks();
cleared = 0;
}
var flashes = [];
function spawnFlash(x, y) { flashes.push({ x: x, y: y, t: 0.001 }); }
function stepFlashes(dt) {
for (var i = flashes.length - 1; i >= 0; i -= 1) {
flashes[i].t += dt;
if (flashes[i].t > FLASH_LIFE) flashes.splice(i, 1);
}
}
function drawFlashes() {
for (var i = 0; i < flashes.length; i += 1) {
var f = flashes[i];
var p = f.t / FLASH_LIFE;
var r = 14 + FLASH_MAX_R * p;
var a = (1 - p) * 0.42;
var g = ctx.createRadialGradient(f.x, f.y, 0, f.x, f.y, r);
g.addColorStop(0, 'rgba(255,247,230,' + a + ')');
g.addColorStop(0.6, 'rgba(255,209,63,' + (a * 0.6) + ')');
g.addColorStop(1, 'rgba(255,209,63,0)');
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(f.x, f.y, r, 0, Math.PI * 2);
ctx.fill();
}
}
var paddle = { x: LW / 2 - PADDLE_W / 2, w: PADDLE_W, target: LW / 2 };
var ball = { x: LW / 2, y: PADDLE_Y - 40, vx: 0, vy: 0 };
function resetBall() {
var ang = rand() * 0.5 - 0.25;
ball.x = LW / 2; ball.y = PADDLE_Y - 40;
ball.vx = Math.sin(ang) * BASE_SPEED;
ball.vy = -Math.cos(ang) * BASE_SPEED;
}
resetBall();
var pointerActive = false;
var keys = { left: false, right: false };
var biasSchedule = [0, 0.75, -0.75, 0.35, -0.35, 0.6];
function autoplay(tMs) {
if (pointerActive || reduce) return;
var slot = Math.floor(tMs / BIAS_PERIOD) % biasSchedule.length;
var bias = biasSchedule[slot] * (paddle.w / 2);
paddle.target = ball.x + bias;
}
function paddleStep(dt) {
if (keys.left) paddle.target -= 160 * dt;
if (keys.right) paddle.target += 160 * dt;
paddle.target = clamp(paddle.target, WALL + paddle.w / 2, LW - WALL - paddle.w / 2);
var goal = paddle.target - paddle.w / 2;
var k = 1 - Math.pow(1 - PADDLE_LERP, dt * 60);
paddle.x += (goal - paddle.x) * k;
paddle.x = clamp(paddle.x, WALL, LW - WALL - paddle.w);
}
function onWallHit(side) { spawnFlash(ball.x, ball.y); mirrorFlashSide = side; mirrorFlashTimer = 0.3; }
function mirrorWallBounce(side) {
ball.vx = side === 'left' ? Math.abs(ball.vx) : -Math.abs(ball.vx);
onWallHit(side);
}
function onPaddleHit() {
var hitPos = clamp((ball.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2), -1, 1);
var angle = hitPos * MAX_REFLECT_ANGLE;
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
ball.vx = Math.sin(angle) * speed;
ball.vy = -Math.abs(Math.cos(angle)) * speed;
ball.y = PADDLE_Y - BALL_R - 0.5;
spawnFlash(ball.x, PADDLE_Y);
}
function onLevelClear() { spawnFlash(LW / 2, LH / 2); rebuildLevel(); }
function onBrickHit(b) {
spawnFlash(b.x + b.w / 2, b.y + b.h / 2);
b.alive = false; cleared += 1;
if (cleared >= bricks.length) { onLevelClear(); }
}
function onMiss() {
spawnFlash(ball.x, LH - 4);
resetBall();
}
function ballStep(dt) {
ball.x += ball.vx * dt;
ball.y += ball.vy * dt;
if (ball.x - BALL_R <= WALL) { ball.x = WALL + BALL_R; mirrorWallBounce('left'); }
else if (ball.x + BALL_R >= LW - WALL) { ball.x = LW - WALL - BALL_R; mirrorWallBounce('right'); }
if (ball.y - BALL_R <= WALL) { ball.y = WALL + BALL_R; ball.vy = Math.abs(ball.vy); }
if (ball.vy > 0 && aabb(ball.x - BALL_R, ball.y - BALL_R, BALL_R * 2, BALL_R * 2, paddle.x, PADDLE_Y, paddle.w, PADDLE_H)) {
onPaddleHit();
}
for (var i = 0; i < bricks.length; i += 1) {
var b = bricks[i];
if (b.alive && aabb(ball.x - BALL_R, ball.y - BALL_R, BALL_R * 2, BALL_R * 2, b.x, b.y, b.w, b.h)) {
ball.vy = -ball.vy;
onBrickHit(b);
break;
}
}
if (ball.y - BALL_R > LH) onMiss();
}
function stepExtra(dt) { if (mirrorFlashTimer > 0) mirrorFlashTimer -= dt; }
function update(dt, tMs) {
autoplay(tMs);
paddleStep(dt);
ballStep(dt);
stepFlashes(dt);
stepExtra(dt);
}
function drawCourt() {
ctx.fillStyle = STAGE_BG;
ctx.fillRect(0, 0, LW, LH);
}
function drawBrick(b) {
ctx.fillStyle = BRICK_ROW_COLORS[Math.floor((b.y - BRICK_TOP) / (BRICK_H + BRICK_GAP)) % BRICK_ROW_COLORS.length];
ctx.fillRect(b.x, b.y, b.w, b.h);
}
function drawBricks() {
for (var i = 0; i < bricks.length; i += 1) {
if (bricks[i].alive) drawBrick(bricks[i]);
}
}
function drawPaddle() {
ctx.fillStyle = PADDLE_COLOR;
var r = 3;
ctx.beginPath();
ctx.moveTo(paddle.x + r, PADDLE_Y);
ctx.arcTo(paddle.x + paddle.w, PADDLE_Y, paddle.x + paddle.w, PADDLE_Y + PADDLE_H, r);
ctx.arcTo(paddle.x + paddle.w, PADDLE_Y + PADDLE_H, paddle.x, PADDLE_Y + PADDLE_H, r);
ctx.arcTo(paddle.x, PADDLE_Y + PADDLE_H, paddle.x, PADDLE_Y, r);
ctx.arcTo(paddle.x, PADDLE_Y, paddle.x + paddle.w, PADDLE_Y, r);
ctx.closePath();
ctx.fill();
}
function drawBall() {
ctx.fillStyle = BALL_COLOR;
ctx.beginPath();
ctx.arc(ball.x, ball.y, BALL_R, 0, Math.PI * 2);
ctx.fill();
}
function drawExtraBelow() {}
function drawExtraAbove() {
if (mirrorFlashTimer > 0) {
var x = mirrorFlashSide === 'left' ? WALL_MARGIN : LW - WALL_MARGIN;
ctx.strokeStyle = 'rgba(255,209,63,' + (mirrorFlashTimer / 0.3) + ')';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x, WALL);
ctx.lineTo(x, LH - WALL);
ctx.stroke();
}
}
var cw = 480, ch = 300, sx = 1, sy = 1;
function fit() {
var r = wrap.getBoundingClientRect();
cw = Math.max(1, Math.round(r.width));
ch = Math.max(1, Math.round(r.height));
canvas.width = cw; canvas.height = ch;
sx = cw / LW; sy = ch / LH;
render();
}
function render() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, cw, ch);
ctx.save();
ctx.scale(sx, sy);
drawCourt();
drawFlashes();
drawBricks();
drawExtraBelow();
drawPaddle();
drawBall();
drawExtraAbove();
ctx.restore();
}
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var playing = !reduce;
if (reduce) { root.classList.remove('is-demo'); }
function calcFromEvent(e) {
var r = wrap.getBoundingClientRect();
var t0 = (e.touches && e.touches[0]) ? e.touches[0] : e;
var relX = (t0.clientX - r.left) / r.width * LW;
paddle.target = clamp(relX, WALL + paddle.w / 2, LW - WALL - paddle.w / 2);
}
wrap.addEventListener('pointerdown', function (e) { pointerActive = true; calcFromEvent(e); if (reduce) { paddleStep(0); render(); } });
wrap.addEventListener('pointermove', function (e) { pointerActive = true; calcFromEvent(e); if (reduce) { paddleStep(0); render(); } });
window.addEventListener('pointerup', function () { pointerActive = false; });
wrap.addEventListener('pointerleave', function () { pointerActive = false; });
window.addEventListener('keydown', function (e) {
if (e.key === 'ArrowLeft') { keys.left = true; pointerActive = true; }
else if (e.key === 'ArrowRight') { keys.right = true; pointerActive = true; }
else return;
if (reduce) { paddleStep(0.05); render(); }
});
window.addEventListener('keyup', function (e) {
if (e.key === 'ArrowLeft') keys.left = false;
else if (e.key === 'ArrowRight') keys.right = false;
if (!keys.left && !keys.right) pointerActive = false;
});
var lastTs = null;
function tick(ts) {
if (lastTs === null) lastTs = ts;
var dt = Math.min((ts - lastTs) / 1000, 0.05);
lastTs = ts;
if (playing) update(dt, ts);
render();
requestAnimationFrame(tick);
}
fit();
window.addEventListener('resize', fit);
render();
if (playing) requestAnimationFrame(tick);
})();import { useEffect, useRef } from "react";
import styles from "./BrickBreakerWallBounceMirror.module.scss";
export default function BrickBreakerWallBounceMirror() {
const rootRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const root = rootRef.current;
const canvas = canvasRef.current;
if (!root || !canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const wrap = canvas.parentElement as HTMLElement;
const LW = 320, LH = 200, WALL = 6;
const PADDLE_H = 8, PADDLE_Y = LH - 18, PADDLE_W = 46;
const BALL_R = 4;
const BRICK_COLS = 6, BRICK_ROWS = 3, BRICK_W = 44, BRICK_H = 12, BRICK_GAP = 4, BRICK_TOP = 22;
const BASE_SPEED = 130;
const BIAS_PERIOD = 560;
const STAGE_BG = "#17141a";
const BRICK_ROW_COLORS = ["#2f6df6", "#ff9ebb", "#4cd4a6"];
const PADDLE_COLOR = "#fff7e6";
const BALL_COLOR = "#fff7e6";
const FLASH_LIFE = 0.4, FLASH_MAX_R = 130;
function seedRand(seed: number) {
let s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
const rand = seedRand(41008);
function clamp(v: number, a: number, b: number): number { return v < a ? a : (v > b ? b : v); }
function aabb(ax: number, ay: number, aw: number, ah: number, bx: number, by: number, bw: number, bh: number): boolean {
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}
const PADDLE_LERP = 0.32;
const MAX_REFLECT_ANGLE = Math.PI / 180 * 48;
const WALL_MARGIN = WALL;
type Brick = { x: number; y: number; w: number; h: number; alive: boolean; hits: number };
function buildBricks(): Brick[] {
const totalW = BRICK_COLS * BRICK_W + (BRICK_COLS - 1) * BRICK_GAP;
const startX = (LW - totalW) / 2;
const arr: Brick[] = [];
for (let r = 0; r < BRICK_ROWS; r += 1) {
for (let c = 0; c < BRICK_COLS; c += 1) {
arr.push({ x: startX + c * (BRICK_W + BRICK_GAP), y: BRICK_TOP + r * (BRICK_H + BRICK_GAP), w: BRICK_W, h: BRICK_H, alive: true, hits: 0 });
}
}
return arr;
}
let bricks: Brick[] = buildBricks();
let cleared = 0;
let mirrorFlashSide: string | null = null;
let mirrorFlashTimer = 0;
function rebuildLevel(): void {
bricks = buildBricks();
cleared = 0;
}
type Flash = { x: number; y: number; t: number };
const flashes: Flash[] = [];
function spawnFlash(x: number, y: number): void { flashes.push({ x: x, y: y, t: 0.001 }); }
function stepFlashes(dt: number): void {
for (let i = flashes.length - 1; i >= 0; i -= 1) {
flashes[i].t += dt;
if (flashes[i].t > FLASH_LIFE) flashes.splice(i, 1);
}
}
function drawFlashes(): void {
for (let i = 0; i < flashes.length; i += 1) {
const f = flashes[i];
const p = f.t / FLASH_LIFE;
const r = 14 + FLASH_MAX_R * p;
const a = (1 - p) * 0.42;
const g = ctx!.createRadialGradient(f.x, f.y, 0, f.x, f.y, r);
g.addColorStop(0, "rgba(255,247,230," + a + ")");
g.addColorStop(0.6, "rgba(255,209,63," + (a * 0.6) + ")");
g.addColorStop(1, "rgba(255,209,63,0)");
ctx!.fillStyle = g;
ctx!.beginPath();
ctx!.arc(f.x, f.y, r, 0, Math.PI * 2);
ctx!.fill();
}
}
const paddle = { x: LW / 2 - PADDLE_W / 2, w: PADDLE_W, target: LW / 2 };
const ball = { x: LW / 2, y: PADDLE_Y - 40, vx: 0, vy: 0 };
function resetBall(): void {
const ang = rand() * 0.5 - 0.25;
ball.x = LW / 2; ball.y = PADDLE_Y - 40;
ball.vx = Math.sin(ang) * BASE_SPEED;
ball.vy = -Math.cos(ang) * BASE_SPEED;
}
resetBall();
let pointerActive = false;
const keys = { left: false, right: false };
const biasSchedule = [0, 0.75, -0.75, 0.35, -0.35, 0.6];
function autoplay(tMs: number): void {
if (pointerActive || reduce) return;
const slot = Math.floor(tMs / BIAS_PERIOD) % biasSchedule.length;
const bias = biasSchedule[slot] * (paddle.w / 2);
paddle.target = ball.x + bias;
}
function paddleStep(dt: number): void {
if (keys.left) paddle.target -= 160 * dt;
if (keys.right) paddle.target += 160 * dt;
paddle.target = clamp(paddle.target, WALL + paddle.w / 2, LW - WALL - paddle.w / 2);
const goal = paddle.target - paddle.w / 2;
const k = 1 - Math.pow(1 - PADDLE_LERP, dt * 60);
paddle.x += (goal - paddle.x) * k;
paddle.x = clamp(paddle.x, WALL, LW - WALL - paddle.w);
}
function onWallHit(side: string): void { spawnFlash(ball.x, ball.y); mirrorFlashSide = side; mirrorFlashTimer = 0.3; }
function mirrorWallBounce(side: string): void {
ball.vx = side === "left" ? Math.abs(ball.vx) : -Math.abs(ball.vx);
onWallHit(side);
}
function onPaddleHit(): void {
const hitPos = clamp((ball.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2), -1, 1);
const angle = hitPos * MAX_REFLECT_ANGLE;
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
ball.vx = Math.sin(angle) * speed;
ball.vy = -Math.abs(Math.cos(angle)) * speed;
ball.y = PADDLE_Y - BALL_R - 0.5;
spawnFlash(ball.x, PADDLE_Y);
}
function onLevelClear(): void { spawnFlash(LW / 2, LH / 2); rebuildLevel(); }
function onBrickHit(b: Brick): void {
spawnFlash(b.x + b.w / 2, b.y + b.h / 2);
b.alive = false; cleared += 1;
if (cleared >= bricks.length) { onLevelClear(); }
}
function onMiss(): void {
spawnFlash(ball.x, LH - 4);
resetBall();
}
function ballStep(dt: number): void {
ball.x += ball.vx * dt;
ball.y += ball.vy * dt;
if (ball.x - BALL_R <= WALL) { ball.x = WALL + BALL_R; mirrorWallBounce("left"); }
else if (ball.x + BALL_R >= LW - WALL) { ball.x = LW - WALL - BALL_R; mirrorWallBounce("right"); }
if (ball.y - BALL_R <= WALL) { ball.y = WALL + BALL_R; ball.vy = Math.abs(ball.vy); }
if (ball.vy > 0 && aabb(ball.x - BALL_R, ball.y - BALL_R, BALL_R * 2, BALL_R * 2, paddle.x, PADDLE_Y, paddle.w, PADDLE_H)) {
onPaddleHit();
}
for (let i = 0; i < bricks.length; i += 1) {
const b = bricks[i];
if (b.alive && aabb(ball.x - BALL_R, ball.y - BALL_R, BALL_R * 2, BALL_R * 2, b.x, b.y, b.w, b.h)) {
ball.vy = -ball.vy;
onBrickHit(b);
break;
}
}
if (ball.y - BALL_R > LH) onMiss();
}
function stepExtra(dt: number): void { if (mirrorFlashTimer > 0) mirrorFlashTimer -= dt; }
function update(dt: number, tMs: number): void {
autoplay(tMs);
paddleStep(dt);
ballStep(dt);
stepFlashes(dt);
stepExtra(dt);
}
function drawCourt(): void {
ctx!.fillStyle = STAGE_BG;
ctx!.fillRect(0, 0, LW, LH);
}
function drawBrick(b: Brick): void {
ctx!.fillStyle = BRICK_ROW_COLORS[Math.floor((b.y - BRICK_TOP) / (BRICK_H + BRICK_GAP)) % BRICK_ROW_COLORS.length];
ctx!.fillRect(b.x, b.y, b.w, b.h);
}
function drawBricks(): void {
for (let i = 0; i < bricks.length; i += 1) {
if (bricks[i].alive) drawBrick(bricks[i]);
}
}
function drawPaddle(): void {
ctx!.fillStyle = PADDLE_COLOR;
const r = 3;
ctx!.beginPath();
ctx!.moveTo(paddle.x + r, PADDLE_Y);
ctx!.arcTo(paddle.x + paddle.w, PADDLE_Y, paddle.x + paddle.w, PADDLE_Y + PADDLE_H, r);
ctx!.arcTo(paddle.x + paddle.w, PADDLE_Y + PADDLE_H, paddle.x, PADDLE_Y + PADDLE_H, r);
ctx!.arcTo(paddle.x, PADDLE_Y + PADDLE_H, paddle.x, PADDLE_Y, r);
ctx!.arcTo(paddle.x, PADDLE_Y, paddle.x + paddle.w, PADDLE_Y, r);
ctx!.closePath();
ctx!.fill();
}
function drawBall(): void {
ctx!.fillStyle = BALL_COLOR;
ctx!.beginPath();
ctx!.arc(ball.x, ball.y, BALL_R, 0, Math.PI * 2);
ctx!.fill();
}
function drawExtraBelow(): void {}
function drawExtraAbove(): void {
if (mirrorFlashTimer > 0) {
const x = mirrorFlashSide === "left" ? WALL_MARGIN : LW - WALL_MARGIN;
ctx!.strokeStyle = "rgba(255,209,63," + (mirrorFlashTimer / 0.3) + ")";
ctx!.lineWidth = 2;
ctx!.beginPath();
ctx!.moveTo(x, WALL);
ctx!.lineTo(x, LH - WALL);
ctx!.stroke();
}
}
let cw = 480, ch = 300, sx = 1, sy = 1;
function fit(): void {
const r = wrap.getBoundingClientRect();
cw = Math.max(1, Math.round(r.width));
ch = Math.max(1, Math.round(r.height));
canvas!.width = cw; canvas!.height = ch;
sx = cw / LW; sy = ch / LH;
render();
}
function render(): void {
ctx!.setTransform(1, 0, 0, 1, 0, 0);
ctx!.clearRect(0, 0, cw, ch);
ctx!.save();
ctx!.scale(sx, sy);
drawCourt();
drawFlashes();
drawBricks();
drawExtraBelow();
drawPaddle();
drawBall();
drawExtraAbove();
ctx!.restore();
}
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const playing = !reduce;
if (reduce) { root.classList.remove("is-demo"); }
function calcFromEvent(e: PointerEvent): void {
const r = wrap.getBoundingClientRect();
const relX = ((e.clientX - r.left) / r.width) * LW;
paddle.target = clamp(relX, WALL + paddle.w / 2, LW - WALL - paddle.w / 2);
}
function onPointerDown(e: PointerEvent): void { pointerActive = true; calcFromEvent(e); if (reduce) { paddleStep(0); render(); } }
function onPointerMove(e: PointerEvent): void { pointerActive = true; calcFromEvent(e); if (reduce) { paddleStep(0); render(); } }
function onPointerUp(): void { pointerActive = false; }
function onPointerLeave(): void { pointerActive = false; }
wrap.addEventListener("pointerdown", onPointerDown);
wrap.addEventListener("pointermove", onPointerMove);
window.addEventListener("pointerup", onPointerUp);
wrap.addEventListener("pointerleave", onPointerLeave);
function onKeyDown(e: KeyboardEvent): void {
if (e.key === "ArrowLeft") { keys.left = true; pointerActive = true; }
else if (e.key === "ArrowRight") { keys.right = true; pointerActive = true; }
else return;
if (reduce) { paddleStep(0.05); render(); }
}
function onKeyUp(e: KeyboardEvent): void {
if (e.key === "ArrowLeft") keys.left = false;
else if (e.key === "ArrowRight") keys.right = false;
if (!keys.left && !keys.right) pointerActive = false;
}
window.addEventListener("keydown", onKeyDown);
window.addEventListener("keyup", onKeyUp);
let lastTs: number | null = null;
let raf = 0;
function tick(ts: number): void {
if (lastTs === null) lastTs = ts;
const dt = Math.min((ts - lastTs) / 1000, 0.05);
lastTs = ts;
if (playing) update(dt, ts);
render();
raf = requestAnimationFrame(tick);
}
fit();
window.addEventListener("resize", fit);
render();
if (playing) raf = requestAnimationFrame(tick);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("resize", fit);
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("keyup", onKeyUp);
window.removeEventListener("pointerup", onPointerUp);
wrap.removeEventListener("pointerdown", onPointerDown);
wrap.removeEventListener("pointermove", onPointerMove);
wrap.removeEventListener("pointerleave", onPointerLeave);
};
}, []);
return (
<main className={styles.stage}>
<div className={`${styles.vd} is-demo`} ref={rootRef}>
<canvas className={styles.vd__canvas} ref={canvasRef} width={480} height={300} />
<p className={styles.vd__caption}>벽에 닿으면 각도가 뒤집히는 튕김</p>
</div>
</main>
);
}08 壁に当たると角度が反転する跳ね返り — 計測 累積面積 30.296% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · javascript ブロック崩し 反射角で変わる9パターン
このデモが属する記事
javascript ブロック崩し 反射角で変わる9パターンjavascript ブロック崩し は9パターンとも同じコートで動き、パターンごとに読む値だけが違います。パドルの中央ではなく端で打て — 当たった位置の値が反射角を決める。2番はその値で跳ね返る角度を曲げ、6番は値が大きいほど残像を長く伸 この記事のデモをすべて見る


