Download pong-pc.html from DevForML/scet_v2: direct link, hf CLI and curl.
- Browser
- Download file 11.9 kB
-
https://huggingface.co/spaces/DevForML/scet_v2/resolve/main/pong-pc.html
- Command line
-
hf download hf://spaces/DevForML/scet_v2/pong-pc.html
-
curl -L -o pong-pc.html https://huggingface.co/spaces/DevForML/scet_v2/resolve/main/pong-pc.html
11.9 kB
| <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}} | |
| </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> | |