06Ball Afterimage That Lengthens With the Speed Value
As the ball's speed value grows, the count and length values of the afterimages behind it grow together, barely visible when slow and stretching into a long tail when fast.
Best forSpeed emphasis for fast sections
SPEED_STEPtrailpushTrail

<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">Ball Afterimage That Lengthens With the Speed Value</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
@include stage($stage-yellow);
.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: #ffd23f;
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(41006);
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 SPEED_STEP = 14;
var SPEED_MAX = 210;
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 trail = []; var TRAIL_MAX = 16;
function rebuildLevel() {
bricks = buildBricks();
cleared = 0;
trail = [];
}
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); }
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);
var sp = Math.min(Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy) + SPEED_STEP, SPEED_MAX);
var ang2 = Math.atan2(ball.vx, -ball.vy);
ball.vx = Math.sin(ang2) * sp;
ball.vy = -Math.abs(Math.cos(ang2)) * sp;
}
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);
trail.length = 0;
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 pushTrail() { trail.push({ x: ball.x, y: ball.y }); }
function stepExtra(dt) {
pushTrail();
var speedNow = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
var count = Math.round(clamp((speedNow - BASE_SPEED) / (SPEED_MAX - BASE_SPEED), 0, 1) * (TRAIL_MAX - 3)) + 3;
while (trail.length > count) trail.shift();
}
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() {
for (var i = 0; i < trail.length; i += 1) {
var t = trail[i];
var a = (i + 1) / trail.length;
ctx.fillStyle = 'rgba(110,200,255,' + (a * 0.5) + ')';
ctx.beginPath();
ctx.arc(t.x, t.y, BALL_R * (0.4 + a * 0.6), 0, Math.PI * 2);
ctx.fill();
}
}
function drawExtraAbove() {}
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 "./BrickBreakerTrailSpeedAfterimage.module.scss";
export default function BrickBreakerTrailSpeedAfterimage() {
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(41006);
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 SPEED_STEP = 14;
const SPEED_MAX = 210;
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;
type Pt = { x: number; y: number };
let trail: Pt[] = [];
const TRAIL_MAX = 16;
function rebuildLevel(): void {
bricks = buildBricks();
cleared = 0;
trail = [];
}
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); }
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);
const sp = Math.min(Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy) + SPEED_STEP, SPEED_MAX);
const ang2 = Math.atan2(ball.vx, -ball.vy);
ball.vx = Math.sin(ang2) * sp;
ball.vy = -Math.abs(Math.cos(ang2)) * sp;
}
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);
trail.length = 0;
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 pushTrail(): void { trail.push({ x: ball.x, y: ball.y }); }
function stepExtra(dt: number): void {
pushTrail();
const speedNow = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
const count = Math.round(clamp((speedNow - BASE_SPEED) / (SPEED_MAX - BASE_SPEED), 0, 1) * (TRAIL_MAX - 3)) + 3;
while (trail.length > count) trail.shift();
}
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 {
for (let i = 0; i < trail.length; i += 1) {
const t = trail[i];
const a = (i + 1) / trail.length;
ctx!.fillStyle = "rgba(110,200,255," + (a * 0.5) + ")";
ctx!.beginPath();
ctx!.arc(t.x, t.y, BALL_R * (0.4 + a * 0.6), 0, Math.PI * 2);
ctx!.fill();
}
}
function drawExtraAbove(): void {}
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>
);
}06 Ball Afterimage That Lengthens With the Speed Value — Measured area 31.421% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · Brick Breaker Game JavaScript — 9 Physics Patterns
The post this demo lives in
Brick Breaker Game JavaScript — 9 Physics Patternsbrick breaker game javascript: Hit the paddle edge, not its center — the hit position value sets the reflection angle. I See all demos in this post


