Spaces:
Paused
Paused
Download reference_block_sprite.html from EricaLuvGemma/bimi: direct link, hf CLI and curl.
- Browser
- Download file 20.1 kB
-
https://huggingface.co/spaces/EricaLuvGemma/bimi/resolve/main/reference_block_sprite.html
- Command line
-
hf download hf://spaces/EricaLuvGemma/bimi/reference_block_sprite.html
-
curl -L -o reference_block_sprite.html https://huggingface.co/spaces/EricaLuvGemma/bimi/resolve/main/reference_block_sprite.html
20.1 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta | |
| name="viewport" | |
| content="width=device-width,initial-scale=1,viewport-fit=cover" | |
| > | |
| <title>SNES Side Scroller</title> | |
| <style> | |
| :root { | |
| --bg: #080713; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| html, | |
| body { | |
| width: 100%; | |
| height: 100%; | |
| margin: 0; | |
| overflow: hidden; | |
| background: var(--bg); | |
| } | |
| body { | |
| display: grid; | |
| place-items: center; | |
| } | |
| /* ========================================================= | |
| GAME FRAME | |
| ========================================================= */ | |
| .game-frame { | |
| position: relative; | |
| width: min(96vw, 1000px); | |
| aspect-ratio: 16 / 10; | |
| overflow: hidden; | |
| isolation: isolate; | |
| background: #17102d; | |
| border: 3px solid rgba(63, 239, 255, .35); | |
| border-radius: | |
| 43% 57% 54% 46% / | |
| 48% 40% 60% 52%; | |
| box-shadow: | |
| 0 0 20px rgba(46, 235, 255, .18), | |
| 0 0 70px rgba(105, 46, 180, .28), | |
| 0 30px 100px rgba(0,0,0,.7); | |
| animation: morph-frame 16s ease-in-out infinite; | |
| } | |
| /* | |
| The canvas itself remains pixel-perfect. | |
| */ | |
| canvas { | |
| position: absolute; | |
| inset: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: block; | |
| image-rendering: pixelated; | |
| image-rendering: crisp-edges; | |
| } | |
| /* | |
| Environmental CSS animation. | |
| */ | |
| .game-frame::before { | |
| content: ""; | |
| position: absolute; | |
| inset: -30%; | |
| z-index: 3; | |
| pointer-events: none; | |
| background: | |
| linear-gradient( | |
| 115deg, | |
| transparent 0 42%, | |
| rgba(255,255,255,.025) 44%, | |
| transparent 46% | |
| ); | |
| animation: atmosphere 12s linear infinite; | |
| } | |
| .game-frame::after { | |
| content: ""; | |
| position: absolute; | |
| inset: 0; | |
| z-index: 4; | |
| pointer-events: none; | |
| background: | |
| radial-gradient( | |
| ellipse at center, | |
| transparent 45%, | |
| rgba(0,0,0,.4) | |
| ); | |
| } | |
| @keyframes morph-frame { | |
| 0% { | |
| border-radius: | |
| 43% 57% 54% 46% / | |
| 48% 40% 60% 52%; | |
| } | |
| 50% { | |
| border-radius: | |
| 58% 42% 46% 54% / | |
| 39% 56% 44% 61%; | |
| } | |
| 100% { | |
| border-radius: | |
| 43% 57% 54% 46% / | |
| 48% 40% 60% 52%; | |
| } | |
| } | |
| @keyframes atmosphere { | |
| from { | |
| transform: translateX(-12%); | |
| } | |
| to { | |
| transform: translateX(12%); | |
| } | |
| } | |
| @media (prefers-reduced-motion: reduce) { | |
| .game-frame, | |
| .game-frame::before { | |
| animation: none; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="game-frame"> | |
| <canvas | |
| id="game" | |
| width="256" | |
| height="160" | |
| ></canvas> | |
| </div> | |
| <script> | |
| /* ========================================================= | |
| SNES SIDE-SCROLLER | |
| ========================================================= | |
| IMPORTANT: | |
| This is NOT a vector puppet. | |
| The character is a collection of discrete sprite frames. | |
| Each frame is intentionally redrawn. | |
| Nothing rotates around anatomical joints. | |
| This is the fundamental difference between this version | |
| and the previous implementation. | |
| Internal resolution: | |
| 256 × 160 | |
| Character sprite: | |
| approximately 32 × 56 | |
| Display: | |
| nearest-neighbor | |
| ========================================================= */ | |
| const canvas = | |
| document.getElementById("game"); | |
| const ctx = | |
| canvas.getContext("2d"); | |
| ctx.imageSmoothingEnabled = false; | |
| /* ========================================================= | |
| PALETTE | |
| ========================================================= */ | |
| const PAL = { | |
| /* outline */ | |
| O: "#171126", | |
| O2: "#292044", | |
| /* cyan rim */ | |
| C: "#46efff", | |
| C2: "#168fae", | |
| /* skin */ | |
| S0: "#9c4055", | |
| S1: "#c95d62", | |
| S2: "#e77a6d", | |
| S3: "#ff9b7c", | |
| S4: "#ffc09a", | |
| /* hair */ | |
| H0: "#24123e", | |
| H1: "#3b195c", | |
| H2: "#61267c", | |
| H3: "#8b3f9c", | |
| /* clothing */ | |
| T0: "#07516f", | |
| T1: "#087e9e", | |
| T2: "#24b5c5", | |
| T3: "#65e2dc", | |
| /* skirt */ | |
| K0: "#701e4d", | |
| K1: "#a92d5d", | |
| K2: "#df3f68", | |
| K3: "#ff6680", | |
| /* eyes */ | |
| E0: "#f5fff0", | |
| E1: "#65ff77", | |
| /* environment */ | |
| NIGHT0: "#120c27", | |
| NIGHT1: "#211343", | |
| NIGHT2: "#35205f", | |
| NIGHT3: "#5a286c", | |
| STAR: "#ffd75e" | |
| }; | |
| /* ========================================================= | |
| BASIC PIXEL HELPERS | |
| ========================================================= */ | |
| function p(x, y, color) { | |
| ctx.fillStyle = color; | |
| ctx.fillRect( | |
| Math.round(x), | |
| Math.round(y), | |
| 1, | |
| 1 | |
| ); | |
| } | |
| function rect( | |
| x, | |
| y, | |
| w, | |
| h, | |
| color | |
| ) { | |
| ctx.fillStyle = color; | |
| ctx.fillRect( | |
| Math.round(x), | |
| Math.round(y), | |
| Math.round(w), | |
| Math.round(h) | |
| ); | |
| } | |
| function pixelLine( | |
| x1, | |
| y1, | |
| x2, | |
| y2, | |
| color | |
| ) { | |
| /* | |
| Bresenham-style integer line. | |
| */ | |
| x1 = Math.round(x1); | |
| y1 = Math.round(y1); | |
| x2 = Math.round(x2); | |
| y2 = Math.round(y2); | |
| let dx = | |
| Math.abs(x2 - x1); | |
| let sx = | |
| x1 < x2 ? 1 : -1; | |
| let dy = | |
| -Math.abs(y2 - y1); | |
| let sy = | |
| y1 < y2 ? 1 : -1; | |
| let err = | |
| dx + dy; | |
| while (true) { | |
| p(x1, y1, color); | |
| if ( | |
| x1 === x2 && | |
| y1 === y2 | |
| ) { | |
| break; | |
| } | |
| const e2 = 2 * err; | |
| if (e2 >= dy) { | |
| err += dy; | |
| x1 += sx; | |
| } | |
| if (e2 <= dx) { | |
| err += dx; | |
| y1 += sy; | |
| } | |
| } | |
| } | |
| /* ========================================================= | |
| BACKGROUND | |
| ========================================================= */ | |
| function background() { | |
| ctx.fillStyle = PAL.NIGHT0; | |
| ctx.fillRect( | |
| 0, | |
| 0, | |
| 256, | |
| 160 | |
| ); | |
| /* | |
| Large horizontal SNES-style bands. | |
| */ | |
| rect( | |
| 0, | |
| 55, | |
| 256, | |
| 32, | |
| PAL.NIGHT1 | |
| ); | |
| rect( | |
| 0, | |
| 87, | |
| 256, | |
| 35, | |
| PAL.NIGHT2 | |
| ); | |
| rect( | |
| 0, | |
| 122, | |
| 256, | |
| 38, | |
| PAL.NIGHT0 | |
| ); | |
| /* | |
| Distant skyline. | |
| */ | |
| const buildings = [ | |
| [0, 84, 19], | |
| [22, 72, 27], | |
| [52, 82, 17], | |
| [73, 65, 23], | |
| [102, 78, 29], | |
| [135, 69, 20], | |
| [160, 81, 25], | |
| [191, 61, 31], | |
| [226, 76, 30] | |
| ]; | |
| for (const b of buildings) { | |
| rect( | |
| b[0], | |
| b[1], | |
| b[2], | |
| 60, | |
| PAL.NIGHT1 | |
| ); | |
| /* | |
| Tiny window pixels. | |
| */ | |
| for ( | |
| let y = b[1] + 8; | |
| y < 130; | |
| y += 10 | |
| ) { | |
| if ( | |
| ((b[0] + y) % 3) === 0 | |
| ) { | |
| rect( | |
| b[0] + 4, | |
| y, | |
| 2, | |
| 2, | |
| PAL.STAR | |
| ); | |
| } | |
| } | |
| } | |
| /* | |
| Ground. | |
| */ | |
| rect( | |
| 0, | |
| 137, | |
| 256, | |
| 23, | |
| "#0d0920" | |
| ); | |
| rect( | |
| 0, | |
| 137, | |
| 256, | |
| 2, | |
| PAL.C2 | |
| ); | |
| /* | |
| Pixel ground markings. | |
| */ | |
| for ( | |
| let x = 0; | |
| x < 256; | |
| x += 18 | |
| ) { | |
| rect( | |
| x, | |
| 146, | |
| 8, | |
| 1, | |
| PAL.NIGHT3 | |
| ); | |
| rect( | |
| x + 4, | |
| 154, | |
| 3, | |
| 1, | |
| PAL.NIGHT2 | |
| ); | |
| } | |
| } | |
| /* ========================================================= | |
| BOKEH | |
| ========================================================= */ | |
| const orbs = [ | |
| { | |
| x: 26, | |
| y: 38, | |
| r: 8, | |
| color: "#ffca52", | |
| phase: 0 | |
| }, | |
| { | |
| x: 219, | |
| y: 43, | |
| r: 11, | |
| color: "#45eaff", | |
| phase: 2 | |
| }, | |
| { | |
| x: 225, | |
| y: 111, | |
| r: 13, | |
| color: "#ff4d9e", | |
| phase: 4 | |
| }, | |
| { | |
| x: 45, | |
| y: 112, | |
| r: 9, | |
| color: "#9c63ff", | |
| phase: 1 | |
| } | |
| ]; | |
| function bokeh(time) { | |
| for (const o of orbs) { | |
| const pulse = | |
| 1 + | |
| Math.sin( | |
| time * .001 + | |
| o.phase | |
| ) * .12; | |
| ctx.globalAlpha = .06; | |
| ctx.fillStyle = | |
| o.color; | |
| ctx.beginPath(); | |
| ctx.arc( | |
| o.x, | |
| o.y, | |
| o.r * 2.1 * pulse, | |
| 0, | |
| Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| ctx.globalAlpha = .13; | |
| ctx.beginPath(); | |
| ctx.arc( | |
| o.x, | |
| o.y, | |
| o.r * pulse, | |
| 0, | |
| Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| ctx.globalAlpha = 1; | |
| } | |
| } | |
| /* ========================================================= | |
| METEORS | |
| ========================================================= */ | |
| const meteorPalette = [ | |
| PAL.C, | |
| "#ff4fd4", | |
| "#6eff70", | |
| PAL.STAR, | |
| "#ff6280" | |
| ]; | |
| const meteors = []; | |
| for (let i = 0; i < 42; i++) { | |
| meteors.push({ | |
| x: | |
| Math.random() * 256, | |
| y: | |
| Math.random() * 125, | |
| length: | |
| 3 + | |
| Math.floor( | |
| Math.random() * 9 | |
| ), | |
| speed: | |
| .15 + | |
| Math.random() * .5, | |
| color: | |
| meteorPalette[ | |
| Math.floor( | |
| Math.random() * | |
| meteorPalette.length | |
| ) | |
| ], | |
| phase: | |
| Math.random() * | |
| Math.PI * 2 | |
| }); | |
| } | |
| function meteorLayer(time) { | |
| for (const m of meteors) { | |
| if (!reducedMotion) { | |
| m.x += m.speed; | |
| m.y += | |
| m.speed * .42; | |
| if ( | |
| m.x > 260 || | |
| m.y > 135 | |
| ) { | |
| m.x = | |
| -Math.random() * 20; | |
| m.y = | |
| Math.random() * 100; | |
| m.length = | |
| 3 + | |
| Math.floor( | |
| Math.random() * 9 | |
| ); | |
| m.speed = | |
| .15 + | |
| Math.random() * .5; | |
| m.color = | |
| meteorPalette[ | |
| Math.floor( | |
| Math.random() * | |
| meteorPalette.length | |
| ) | |
| ]; | |
| } | |
| } | |
| const alpha = | |
| .35 + | |
| ( | |
| Math.sin( | |
| time * .004 + | |
| m.phase | |
| ) + 1 | |
| ) * .25; | |
| ctx.globalAlpha = | |
| alpha; | |
| /* | |
| Pixel staircase meteor. | |
| */ | |
| pixelLine( | |
| m.x, | |
| m.y, | |
| m.x + m.length, | |
| m.y + Math.floor(m.length * .45), | |
| m.color | |
| ); | |
| ctx.globalAlpha = 1; | |
| } | |
| } | |
| /* ========================================================= | |
| CHARACTER SPRITE FRAMES | |
| ========================================================= | |
| These are deliberately NOT rotated body parts. | |
| Each pose is a different pixel arrangement. | |
| The renderer treats each frame like a classic game | |
| sprite. | |
| ========================================================= */ | |
| /* | |
| Draw one pixel-art character. | |
| x/y = sprite origin. | |
| frame = animation frame. | |
| dir = direction. | |
| */ | |
| function characterSprite( | |
| x, | |
| y, | |
| frame, | |
| dir = 1 | |
| ) { | |
| /* | |
| Side-scroller animation. | |
| 0 = contact | |
| 1 = passing | |
| 2 = opposite contact | |
| 3 = passing | |
| */ | |
| const poses = [ | |
| { | |
| arm: -2, | |
| legA: 3, | |
| legB: -3, | |
| body: 0 | |
| }, | |
| { | |
| arm: 0, | |
| legA: 0, | |
| legB: 0, | |
| body: -1 | |
| }, | |
| { | |
| arm: 2, | |
| legA: -3, | |
| legB: 3, | |
| body: 0 | |
| }, | |
| { | |
| arm: 0, | |
| legA: 0, | |
| legB: 0, | |
| body: -1 | |
| } | |
| ]; | |
| const pose = | |
| poses[ | |
| frame % | |
| poses.length | |
| ]; | |
| const bob = | |
| pose.body; | |
| /* | |
| Character dimensions: | |
| width ~38px | |
| height ~58px | |
| */ | |
| ctx.save(); | |
| ctx.translate( | |
| Math.round(x), | |
| Math.round( | |
| y + bob | |
| ) | |
| ); | |
| if (dir < 0) { | |
| ctx.translate(38, 0); | |
| ctx.scale(-1, 1); | |
| } | |
| /* ======================================================= | |
| BACK ARM | |
| ======================================================= */ | |
| pixelLine( | |
| 11, | |
| 26 + pose.arm, | |
| 5, | |
| 39 + pose.arm, | |
| PAL.C | |
| ); | |
| pixelLine( | |
| 12, | |
| 26 + pose.arm, | |
| 6, | |
| 39 + pose.arm, | |
| PAL.O | |
| ); | |
| pixelLine( | |
| 12, | |
| 27 + pose.arm, | |
| 7, | |
| 39 + pose.arm, | |
| PAL.S2 | |
| ); | |
| p( | |
| 7, | |
| 39 + pose.arm, | |
| PAL.S3 | |
| ); | |
| /* ======================================================= | |
| BACK LEG | |
| ======================================================= */ | |
| pixelLine( | |
| 16, | |
| 44, | |
| 12 + pose.legB, | |
| 55, | |
| PAL.C | |
| ); | |
| pixelLine( | |
| 16, | |
| 44, | |
| 12 + pose.legB, | |
| 55, | |
| PAL.O | |
| ); | |
| pixelLine( | |
| 16, | |
| 44, | |
| 12 + pose.legB, | |
| 55, | |
| PAL.S2 | |
| ); | |
| /* ======================================================= | |
| BACK FOOT | |
| ======================================================= */ | |
| rect( | |
| 8 + pose.legB, | |
| 54, | |
| 7, | |
| 3, | |
| PAL.C | |
| ); | |
| rect( | |
| 9 + pose.legB, | |
| 54, | |
| 6, | |
| 2, | |
| PAL.S1 | |
| ); | |
| /* ======================================================= | |
| SKIRT | |
| ======================================================= */ | |
| /* | |
| Cyan silhouette first. | |
| */ | |
| rect( | |
| 7, | |
| 39, | |
| 26, | |
| 13, | |
| PAL.C | |
| ); | |
| /* | |
| Main skirt. | |
| */ | |
| rect( | |
| 9, | |
| 40, | |
| 22, | |
| 10, | |
| PAL.K1 | |
| ); | |
| /* | |
| Pixel folds. | |
| */ | |
| rect( | |
| 10, | |
| 41, | |
| 4, | |
| 9, | |
| PAL.K0 | |
| ); | |
| rect( | |
| 17, | |
| 40, | |
| 3, | |
| 11, | |
| PAL.K3 | |
| ); | |
| rect( | |
| 25, | |
| 41, | |
| 5, | |
| 9, | |
| PAL.K2 | |
| ); | |
| /* | |
| Uneven hem. | |
| */ | |
| rect( | |
| 7, | |
| 50, | |
| 5, | |
| 2, | |
| PAL.K0 | |
| ); | |
| rect( | |
| 13, | |
| 51, | |
| 6, | |
| 2, | |
| PAL.K1 | |
| ); | |
| rect( | |
| 20, | |
| 50, | |
| 7, | |
| 2, | |
| PAL.K3 | |
| ); | |
| rect( | |
| 28, | |
| 49, | |
| 5, | |
| 2, | |
| PAL.K1 | |
| ); | |
| /* ======================================================= | |
| TORSO | |
| ======================================================= */ | |
| rect( | |
| 10, | |
| 22, | |
| 21, | |
| 19, | |
| PAL.C | |
| ); | |
| rect( | |
| 12, | |
| 23, | |
| 17, | |
| 16, | |
| PAL.T1 | |
| ); | |
| /* | |
| Shirt highlight. | |
| */ | |
| rect( | |
| 23, | |
| 24, | |
| 5, | |
| 13, | |
| PAL.T3 | |
| ); | |
| /* | |
| Shirt shadow. | |
| */ | |
| rect( | |
| 12, | |
| 34, | |
| 6, | |
| 5, | |
| PAL.T0 | |
| ); | |
| /* ======================================================= | |
| COLLAR | |
| ======================================================= */ | |
| rect( | |
| 17, | |
| 22, | |
| 8, | |
| 4, | |
| PAL.STAR | |
| ); | |
| rect( | |
| 19, | |
| 25, | |
| 4, | |
| 2, | |
| PAL.STAR | |
| ); | |
| /* ======================================================= | |
| FRONT ARM | |
| ======================================================= */ | |
| pixelLine( | |
| 29, | |
| 25, | |
| 35, | |
| 38 + pose.arm, | |
| PAL.C | |
| ); | |
| pixelLine( | |
| 29, | |
| 25, | |
| 35, | |
| 38 + pose.arm, | |
| PAL.O | |
| ); | |
| pixelLine( | |
| 29, | |
| 26, | |
| 34, | |
| 38 + pose.arm, | |
| PAL.S2 | |
| ); | |
| /* | |
| Hand. | |
| */ | |
| rect( | |
| 32, | |
| 37 + pose.arm, | |
| 5, | |
| 4, | |
| PAL.C | |
| ); | |
| rect( | |
| 33, | |
| 37 + pose.arm, | |
| 3, | |
| 3, | |
| PAL.S3 | |
| ); | |
| /* ======================================================= | |
| FRONT LEG | |
| ======================================================= */ | |
| pixelLine( | |
| 23, | |
| 45, | |
| 27 + pose.legA, | |
| 55, | |
| PAL.C | |
| ); | |
| pixelLine( | |
| 23, | |
| 45, | |
| 27 + pose.legA, | |
| 55, | |
| PAL.O | |
| ); | |
| pixelLine( | |
| 23, | |
| 45, | |
| 27 + pose.legA, | |
| 55, | |
| PAL.S2 | |
| ); | |
| /* ======================================================= | |
| FRONT FOOT | |
| ======================================================= */ | |
| rect( | |
| 25 + pose.legA, | |
| 54, | |
| 8, | |
| 3, | |
| PAL.C | |
| ); | |
| rect( | |
| 26 + pose.legA, | |
| 54, | |
| 7, | |
| 2, | |
| PAL.S1 | |
| ); | |
| /* ======================================================= | |
| NECK | |
| ======================================================= */ | |
| rect( | |
| 18, | |
| 18, | |
| 8, | |
| 7, | |
| PAL.C | |
| ); | |
| rect( | |
| 20, | |
| 18, | |
| 5, | |
| 6, | |
| PAL.S2 | |
| ); | |
| /* ======================================================= | |
| HEAD SILHOUETTE | |
| ======================================================= */ | |
| /* | |
| Hair/cyan silhouette. | |
| */ | |
| rect( | |
| 8, | |
| 2, | |
| 25, | |
| 18, | |
| PAL.C | |
| ); | |
| /* | |
| Face. | |
| */ | |
| rect( | |
| 11, | |
| 5, | |
| 19, | |
| 14, | |
| PAL.S2 | |
| ); | |
| /* | |
| Face light. | |
| */ | |
| rect( | |
| 18, | |
| 6, | |
| 11, | |
| 10, | |
| PAL.S3 | |
| ); | |
| /* | |
| Jaw shadow. | |
| */ | |
| rect( | |
| 12, | |
| 15, | |
| 9, | |
| 4, | |
| PAL.S1 | |
| ); | |
| /* ======================================================= | |
| HAIR | |
| ======================================================= */ | |
| rect( | |
| 8, | |
| 2, | |
| 23, | |
| 7, | |
| PAL.H0 | |
| ); | |
| rect( | |
| 10, | |
| 4, | |
| 19, | |
| 6, | |
| PAL.H1 | |
| ); | |
| /* | |
| Hair silhouette around face. | |
| */ | |
| rect( | |
| 8, | |
| 7, | |
| 5, | |
| 13, | |
| PAL.H0 | |
| ); | |
| rect( | |
| 27, | |
| 7, | |
| 6, | |
| 13, | |
| PAL.H0 | |
| ); | |
| /* | |
| Hair highlights. | |
| */ | |
| rect( | |
| 13, | |
| 4, | |
| 7, | |
| 2, | |
| PAL.H3 | |
| ); | |
| rect( | |
| 20, | |
| 5, | |
| 6, | |
| 2, | |
| PAL.H2 | |
| ); | |
| /* ======================================================= | |
| EYES | |
| ======================================================= */ | |
| rect( | |
| 17, | |
| 10, | |
| 5, | |
| 4, | |
| PAL.E0 | |
| ); | |
| rect( | |
| 25, | |
| 10, | |
| 5, | |
| 4, | |
| PAL.E0 | |
| ); | |
| rect( | |
| 20, | |
| 10, | |
| 2, | |
| 4, | |
| PAL.E1 | |
| ); | |
| rect( | |
| 27, | |
| 10, | |
| 2, | |
| 4, | |
| PAL.E1 | |
| ); | |
| /* | |
| Eye pixels. | |
| */ | |
| p(21, 11, PAL.O); | |
| p(28, 11, PAL.O); | |
| /* | |
| Eye highlights. | |
| */ | |
| p(20, 10, PAL.E0); | |
| p(27, 10, PAL.E0); | |
| /* ======================================================= | |
| NOSE | |
| ======================================================= */ | |
| rect( | |
| 29, | |
| 14, | |
| 3, | |
| 2, | |
| PAL.S0 | |
| ); | |
| /* ======================================================= | |
| MOUTH | |
| ======================================================= */ | |
| rect( | |
| 21, | |
| 17, | |
| 7, | |
| 1, | |
| PAL.O | |
| ); | |
| /* ======================================================= | |
| PIXEL SKIN HIGHLIGHTS | |
| ======================================================= */ | |
| rect( | |
| 23, | |
| 7, | |
| 3, | |
| 1, | |
| PAL.S4 | |
| ); | |
| rect( | |
| 28, | |
| 15, | |
| 2, | |
| 1, | |
| PAL.S4 | |
| ); | |
| ctx.restore(); | |
| } | |
| /* ========================================================= | |
| CHARACTER SHADOW | |
| ========================================================= */ | |
| function characterShadow( | |
| x, | |
| y | |
| ) { | |
| ctx.globalAlpha = .45; | |
| ctx.fillStyle = | |
| PAL.O; | |
| ctx.fillRect( | |
| Math.round(x - 7), | |
| Math.round(y), | |
| 48, | |
| 3 | |
| ); | |
| ctx.globalAlpha = 1; | |
| } | |
| /* ========================================================= | |
| GAME STATE | |
| ========================================================= */ | |
| const player = { | |
| x: 111, | |
| y: 78, | |
| direction: 1, | |
| frame: 0, | |
| frameTimer: 0, | |
| frameRate: 115, | |
| moving: true | |
| }; | |
| /* ========================================================= | |
| REDUCED MOTION | |
| ========================================================= */ | |
| const motionQuery = | |
| window.matchMedia( | |
| "(prefers-reduced-motion: reduce)" | |
| ); | |
| let reducedMotion = | |
| motionQuery.matches; | |
| motionQuery.addEventListener( | |
| "change", | |
| e => { | |
| reducedMotion = | |
| e.matches; | |
| } | |
| ); | |
| /* ========================================================= | |
| PLAYER ANIMATION | |
| ========================================================= */ | |
| function updatePlayer(dt) { | |
| if (reducedMotion) { | |
| player.frame = 1; | |
| return; | |
| } | |
| /* | |
| We don't rotate limbs. | |
| We simply select the next authored sprite pose. | |
| */ | |
| player.frameTimer += dt; | |
| if ( | |
| player.frameTimer >= | |
| player.frameRate | |
| ) { | |
| player.frameTimer = 0; | |
| player.frame = | |
| ( | |
| player.frame + 1 | |
| ) % 4; | |
| } | |
| /* | |
| Small horizontal movement gives the impression that | |
| the character is traversing the scene. | |
| This is intentionally discrete rather than floating | |
| the entire body like a 3D rig. | |
| */ | |
| player.x += | |
| .012 * dt; | |
| if ( | |
| player.x > 270 | |
| ) { | |
| player.x = -35; | |
| } | |
| } | |
| /* ========================================================= | |
| CAMERA PARALLAX | |
| ========================================================= */ | |
| let targetCameraX = 0; | |
| let cameraX = 0; | |
| let pointerX = .5; | |
| let pointerY = .5; | |
| const frame = | |
| document.querySelector( | |
| ".game-frame" | |
| ); | |
| frame.addEventListener( | |
| "pointermove", | |
| e => { | |
| const r = | |
| frame.getBoundingClientRect(); | |
| pointerX = | |
| ( | |
| e.clientX - | |
| r.left | |
| ) / r.width; | |
| pointerY = | |
| ( | |
| e.clientY - | |
| r.top | |
| ) / r.height; | |
| } | |
| ); | |
| frame.addEventListener( | |
| "pointerleave", | |
| () => { | |
| pointerX = .5; | |
| pointerY = .5; | |
| } | |
| ); | |
| /* ========================================================= | |
| MAIN RENDER | |
| ========================================================= */ | |
| let lastTime = 0; | |
| function render(time) { | |
| const dt = | |
| Math.min( | |
| 40, | |
| time - lastTime | |
| ); | |
| lastTime = time; | |
| /* | |
| Update. | |
| */ | |
| updatePlayer(dt); | |
| targetCameraX = | |
| ( | |
| pointerX - | |
| .5 | |
| ) * 5; | |
| cameraX += | |
| ( | |
| targetCameraX - | |
| cameraX | |
| ) * .03; | |
| /* | |
| Background. | |
| */ | |
| background(); | |
| bokeh(time); | |
| meteorLayer(time); | |
| /* | |
| Camera layer. | |
| The world shifts slightly, | |
| but the sprite itself remains | |
| a discrete game sprite. | |
| */ | |
| ctx.save(); | |
| ctx.translate( | |
| Math.round(cameraX), | |
| 0 | |
| ); | |
| /* | |
| Ground shadow. | |
| */ | |
| characterShadow( | |
| player.x, | |
| 136 | |
| ); | |
| /* | |
| Actual SNES character. | |
| */ | |
| characterSprite( | |
| player.x, | |
| player.y, | |
| player.frame, | |
| player.direction | |
| ); | |
| ctx.restore(); | |
| requestAnimationFrame( | |
| render | |
| ); | |
| } | |
| requestAnimationFrame( | |
| render | |
| ); | |
| </script> | |
| </body> | |
| </html> |