scet_v2 / pong-pc.html
DevForML's picture
Upload 70 files
015a372 verified
Raw History Blame Contribute Delete
11.9 kB
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Craft Time · Pong</title>
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
<style>
:root{--ink:#16130F;--mag:#E5277F;--phos:#A8FF60;--amber:#FFC24A;--tube:#07140E}
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:transparent;overflow:hidden}
body{display:flex;align-items:center;justify-content:center;font-family:"Space Grotesk",system-ui,sans-serif}
.machine{
position:relative; width:min(560px,92%); z-index:3;
animation:float 7s ease-in-out infinite;
filter:drop-shadow(0 26px 22px rgba(30,26,20,.28));
}
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-9px)}}
.mon{
background:linear-gradient(178deg,#EDE5CF 0%,#DCD1B7 38%,#C6BA9C 100%);
border-radius:24px 24px 14px 14px; padding:20px 20px 14px;
box-shadow:
inset 0 3px 0 rgba(255,255,255,.85), inset 4px 0 0 rgba(255,255,255,.4),
inset -4px 0 0 rgba(90,78,55,.28), inset 0 -6px 0 rgba(90,78,55,.3),
inset 0 0 60px rgba(120,104,74,.22);
position:relative;
}
.mon::after{ /* fine moulded texture */
content:""; position:absolute; inset:0; border-radius:inherit; pointer-events:none; opacity:.35;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='m'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.4' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23m)' opacity='.4'/%3E%3C/svg%3E");
mix-blend-mode:multiply;
}
.bezel{
background:linear-gradient(#22221E,#131311); border-radius:16px; padding:15px;
box-shadow:inset 0 3px 8px rgba(0,0,0,.9), 0 1px 0 rgba(255,255,255,.55);
position:relative; z-index:2;
}
.screen{
position:relative; aspect-ratio:4/3; background:var(--tube); overflow:hidden;
border-radius:14px/20px;
box-shadow:inset 0 0 46px rgba(0,0,0,.95), inset 0 0 100px rgba(0,0,0,.65);
}
.screen canvas{position:absolute; inset:0; width:100%; height:100%; display:block}
.scan{
position:absolute; inset:0; pointer-events:none;
background:repeating-linear-gradient(to bottom, rgba(0,0,0,.34) 0 1px, rgba(0,0,0,0) 1px 3px);
opacity:.75;
}
.glass{
position:absolute; inset:0; pointer-events:none;
background:
radial-gradient(130% 78% at 24% 6%, rgba(255,255,255,.17), rgba(255,255,255,0) 52%),
radial-gradient(150% 130% at 50% 50%, rgba(0,0,0,0) 52%, rgba(0,0,0,.62));
}
.screenwrap{position:relative; border-radius:14px/20px; overflow:hidden; animation:flick 4.4s steps(2,end) infinite}
@keyframes flick{0%,100%{opacity:1}47%{opacity:.965}48%{opacity:.91}49%{opacity:.985}}
.screenwrap.point::after{
content:""; position:absolute; inset:0; pointer-events:none;
box-shadow:inset 0 0 0 3px rgba(255,194,74,.95), inset 0 0 54px rgba(255,194,74,.4);
animation:pflash .5s ease-out forwards;
}
@keyframes pflash{from{opacity:1}to{opacity:0}}
.hud{
position:absolute; top:9px; left:0; right:0; display:flex; justify-content:center; gap:16px;
font-family:"VT323","DM Mono",Menlo,monospace; font-size:clamp(14px,2.1vw,20px); color:var(--phos);
text-shadow:0 0 10px rgba(168,255,96,.65); letter-spacing:.08em; pointer-events:none;
font-variant-numeric:tabular-nums;
}
.hud .n{color:var(--amber); text-shadow:0 0 10px rgba(255,194,74,.55)}
.hud .tag{color:rgba(168,255,96,.55); font-size:.72em; letter-spacing:.22em}
.hudbottom{
position:absolute; bottom:8px; left:0; right:0; display:flex; justify-content:space-between;
padding:0 10px; font-family:"VT323","DM Mono",Menlo,monospace; font-size:clamp(9px,1.5vw,12px);
color:rgba(168,255,96,.55); letter-spacing:.14em; pointer-events:none;
}
.panel{display:flex; align-items:center; gap:14px; padding:14px 6px 4px; position:relative; z-index:2}
.brand{font-size:13px; font-weight:700; letter-spacing:.3em; color:#6C6250}
.model{font-family:"VT323","DM Mono",Menlo,monospace; font-size:15px; color:#8A7F68; letter-spacing:.1em}
.vents{
flex:1; height:12px; border-radius:2px;
background:repeating-linear-gradient(90deg, rgba(70,60,42,.42) 0 2px, rgba(255,255,255,.5) 2px 5px);
box-shadow:inset 0 1px 2px rgba(0,0,0,.3);
}
.led{width:9px; height:9px; border-radius:50%; background:var(--mag); box-shadow:0 0 10px rgba(229,39,127,.9), inset 0 0 3px rgba(255,255,255,.8); animation:pulse 2.6s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.55}}
.knob{
width:20px; height:20px; border-radius:50%;
background:radial-gradient(circle at 32% 28%, #F2EBD6, #B9AC8B 62%, #8C8064);
box-shadow:0 2px 3px rgba(0,0,0,.35), inset 0 0 0 1px rgba(255,255,255,.5);
position:relative;
}
.knob::after{content:""; position:absolute; left:50%; top:3px; width:1.5px; height:6px; background:#4B4335; transform:translateX(-50%)}
.stand{width:44%; height:20px; margin:0 auto; background:linear-gradient(#CFC3A3,#AFA382); clip-path:polygon(14% 0,86% 0,100% 100%,0 100%); border-radius:0 0 8px 8px; box-shadow:inset 0 2px 0 rgba(255,255,255,.6)}
.foot{width:62%; height:13px; margin:0 auto; background:linear-gradient(#DCD1B5,#B4A887); border-radius:0 0 10px 10px; box-shadow:inset 0 2px 0 rgba(255,255,255,.7), 0 4px 8px rgba(30,26,20,.28)}
@media (prefers-reduced-motion:reduce){.machine,.screenwrap,.led{animation:none!important}}
</style></head>
<body>
<div class="machine">
<div class="mon">
<div class="bezel">
<div class="screenwrap" id="screenWrap">
<div class="screen">
<canvas id="pong"></canvas>
<div class="hud"><span class="tag">CPU·A</span><span class="n" id="scoreA">0</span><span class="tag">—</span><span class="n" id="scoreB">0</span><span class="tag">CPU·B</span></div>
<div class="hudbottom"><span id="hudRally">RALLY 0</span><span>SELF PLAY</span><span id="hudSpd">0 PX/S</span></div>
<div class="scan"></div><div class="glass"></div>
</div>
</div>
</div>
<div class="panel"><span class="brand">CRAFT TIME</span><span class="model">PONG-1</span><span class="vents"></span><span class="knob"></span><span class="knob"></span><span class="led"></span></div>
</div>
<div class="stand"></div><div class="foot"></div>
</div>
<script>
(() => {
const reduce = false;
/* ---------- the match ---------- */
const cv = document.getElementById('pong');
const ctx = cv.getContext('2d', { alpha: false });
const W = 640, H = 480;
const PW = 10, PH = 78, PX = 30, PX2 = W - PX - PW;
const BASE = 330, MAXBALL = 760;
const g = {
p1: H/2, p2: H/2,
bx: W/2, by: H/2, vx: 330, vy: 140,
s1: 0, s2: 0, rally: 0, maxRally: 0,
speed: 330, serve: 0.9, t: 0, points: 0
};
let rw = 1, rh = 1;
function resize() {
const r = cv.getBoundingClientRect();
const dpr = Math.min(window.devicePixelRatio || 1, 2);
cv.width = Math.max(2, Math.round(r.width * dpr));
cv.height = Math.max(2, Math.round(r.height * dpr));
rw = cv.width / W; rh = cv.height / H;
ctx.fillStyle = '#07140E'; ctx.fillRect(0, 0, cv.width, cv.height);
}
new ResizeObserver(resize).observe(cv);
window.addEventListener('resize', resize);
resize();
function targetFor(paddleCenterX, dir) {
if (dir > 0 && g.vx <= 0) return H/2;
if (dir < 0 && g.vx >= 0) return H/2;
const px = dir > 0 ? PX2 : PX;
const t = Math.max(0, (px - g.bx) / g.vx);
let y = g.by + g.vy * t;
const span = H;
y = ((y % (2*span)) + 2*span) % (2*span);
if (y > span) y = 2*span - y;
return y;
}
function serve(dir) {
const ang = (Math.random() * 0.7 - 0.35);
g.bx = W/2; g.by = H/2 + (Math.random()*80 - 40);
g.vx = Math.cos(ang) * BASE * dir;
g.vy = Math.sin(ang) * BASE;
g.speed = BASE; g.rally = 0; g.serve = 0.75;
}
function step(dt) {
g.t += dt;
if (g.serve > 0) { g.serve -= dt; return; }
// ---- paddles (imperfect AI) ----
const err1 = Math.sin(g.t * 0.9) * 26 + Math.sin(g.t * 2.7) * 9;
const err2 = Math.sin(g.t * 1.15 + 2.1) * 24 + Math.sin(g.t * 3.1) * 8;
const t1 = targetFor(PX, -1) + err1;
const t2 = targetFor(PX2, 1) + err2;
const sp1 = 352, sp2 = 344;
const d1 = t1 - g.p1, d2 = t2 - g.p2;
g.p1 += Math.sign(d1) * Math.min(Math.abs(d1), sp1 * dt);
g.p2 += Math.sign(d2) * Math.min(Math.abs(d2), sp2 * dt);
g.p1 = Math.max(PH/2, Math.min(H - PH/2, g.p1));
g.p2 = Math.max(PH/2, Math.min(H - PH/2, g.p2));
// ---- ball (previous x kept for tunneling-free paddle tests) ----
const prev = g.bx;
g.bx += g.vx * dt; g.by += g.vy * dt;
if (g.by < 8) { g.by = 8; g.vy = Math.abs(g.vy); }
if (g.by > H - 8) { g.by = H - 8; g.vy = -Math.abs(g.vy); }
// dir: -1 = left paddle, +1 = right paddle
const hit = (px, center, dir) => {
if (dir < 0) {
if (g.vx >= 0) return false; // must be travelling left
if (!(prev + 8 > px + PW && g.bx - 8 <= px + PW)) return false; // crossed this frame
} else {
if (g.vx <= 0) return false;
if (!(prev + 8 < px && g.bx + 8 >= px)) return false;
}
if (Math.abs(g.by - center) > PH/2 + 8) return false;
const off = (g.by - center) / (PH/2);
g.speed = Math.min(MAXBALL, g.speed * 1.045 + 6);
const ang = off * 0.82 + (Math.random() * 0.16 - 0.08);
g.vx = -dir * Math.cos(ang) * g.speed; // bounce back toward the other machine
g.vy = Math.sin(ang) * g.speed;
g.bx = dir < 0 ? px + PW + 9 : px - 9;
g.rally++;
if (g.rally > g.maxRally) g.maxRally = g.rally;
return true;
};
hit(PX, g.p1, -1); hit(PX2, g.p2, 1);
if (g.bx < -24) { g.s2++; g.points++; serve(1); flashScreen(); }
else if (g.bx > W + 24) { g.s1++; g.points++; serve(-1); flashScreen(); }
}
function flashScreen() {
const w = document.getElementById('screenWrap');
if (!w) return;
w.classList.remove('point'); void w.offsetWidth; w.classList.add('point');
}
function draw() {
ctx.setTransform(rw, 0, 0, rh, 0, 0);
ctx.fillStyle = 'rgba(7,20,14,0.30)'; // phosphor persistence
ctx.fillRect(0, 0, W, H);
// centre line
ctx.save();
ctx.fillStyle = 'rgba(168,255,96,0.30)';
for (let y = 8; y < H; y += 26) ctx.fillRect(W/2 - 1, y, 2, 15);
// border ticks
ctx.fillStyle = 'rgba(168,255,96,0.14)';
ctx.fillRect(0, 0, W, 2); ctx.fillRect(0, H-2, W, 2);
ctx.restore();
ctx.shadowBlur = 16;
ctx.shadowColor = 'rgba(168,255,96,0.85)';
ctx.fillStyle = '#A8FF60';
ctx.fillRect(PX, g.p1 - PH/2, PW, PH);
ctx.fillRect(PX2, g.p2 - PH/2, PW, PH);
ctx.shadowColor = 'rgba(230,255,220,0.95)';
ctx.fillStyle = '#EAFFE2';
ctx.beginPath(); ctx.arc(g.bx, g.by, 7.5, 0, Math.PI*2); ctx.fill();
ctx.shadowBlur = 0;
if (g.serve > 0) {
ctx.fillStyle = 'rgba(255,194,74,0.75)';
ctx.font = '26px VT323, monospace';
ctx.textAlign = 'center';
ctx.fillText('SERVE…', W/2, H/2 + 60);
}
}
const $ = id => document.getElementById(id);
const scoreA = $('scoreA'), scoreB = $('scoreB'),
hudRally = $('hudRally'), hudSpd = $('hudSpd');
let lastT = performance.now(), acc = 0, hudAcc = 0;
function loop(now) {
let dt = (now - lastT) / 1000; lastT = now;
if (dt > 0.05) dt = 0.05;
step(dt); draw();
hudAcc += dt;
if (hudAcc > 0.12) {
hudAcc = 0;
const sp = Math.round(Math.hypot(g.vx, g.vy));
scoreA.textContent = g.s1; scoreB.textContent = g.s2;
hudRally.textContent = 'RALLY ' + g.rally;
hudSpd.textContent = sp + ' PX/S';
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();
</script></body></html>