Feat(platform): Kong easter egg#5255
Open
Guaris wants to merge 2 commits into
Open
Conversation
✅ Deploy Preview for kongdeveloper ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a hidden "bullet hell" easter egg to the Kong Developer docs site. When a desktop visitor scrolls to the bottom of a long page, scrolls away, then back, the page's SVG icons morph into enemy sprites and launch a full-screen Touhou-style mini-game rendered on a canvas overlay.
Changes:
- New
app/_assets/javascripts/bullet_hell/module (~1100 LOC) implementing the game loop, sprite morph, bullet patterns, HUD, and player/boss/minion mechanics. - New CSS keyframe
bullet-hell-shakefor the page-shake telegraph. - Wires the new module into the global
application.jsentrypoint.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| app/_assets/entrypoints/application.js | Imports the new bullet_hell module so it runs on every page. |
| app/_assets/stylesheets/index.css | Adds keyframes and class for the page-shake trigger. |
| app/_assets/javascripts/bullet_hell/index.js | Scroll/state machine that detects the trigger, freezes scroll, and starts the game. |
| app/_assets/javascripts/bullet_hell/morph.js | Captures qualifying page SVGs, animates clones into boss formation, returns sprite Images. |
| app/_assets/javascripts/bullet_hell/patterns.js | Boss bullet patterns (aimed, spread, ring, stream) with seeded jitter. |
| app/_assets/javascripts/bullet_hell/game.js | Main Game class: input, fixed-step loop, boss/minion AI, powerups, bombs, HUD, menus, rendering. |
Comment on lines
+98
to
+105
| function svgToImage(html) { | ||
| const img = new Image(); | ||
| try { | ||
| img.src = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(html)))}`; | ||
| } catch (_) { | ||
| img.src = "data:image/svg+xml;utf8," + encodeURIComponent(html); | ||
| } | ||
| return img; |
Comment on lines
+34
to
+71
| let activeGame = null; | ||
| let prevY = scroller().scrollTop; | ||
|
|
||
| window.addEventListener("scroll", () => { | ||
| if (state === "running") return; | ||
|
|
||
| const y = scroller().scrollTop; | ||
| const goingDown = y > prevY; | ||
| prevY = y; | ||
|
|
||
| if (state === "idle" && goingDown && atBottom()) { | ||
| state = "shaken"; | ||
| document.body.classList.add("bullet-hell-shake"); | ||
| setTimeout(() => document.body.classList.remove("bullet-hell-shake"), SHAKE_MS); | ||
| return; | ||
| } | ||
|
|
||
| if (state === "shaken" && scrolledAway()) { | ||
| state = "armed"; | ||
| return; | ||
| } | ||
|
|
||
| if (state === "armed" && goingDown && atBottom()) { | ||
| state = "running"; | ||
| start(); | ||
| } | ||
| }, { passive: true }); | ||
|
|
||
| async function start() { | ||
| document.documentElement.style.overflow = "hidden"; | ||
|
|
||
| const game = new Game([], () => { | ||
| document.documentElement.style.overflow = ""; | ||
| activeGame = null; | ||
| state = "idle"; | ||
| }); | ||
| game.mountChrome(); | ||
| activeGame = game; |
Comment on lines
+65
to
+80
| const game = new Game([], () => { | ||
| document.documentElement.style.overflow = ""; | ||
| activeGame = null; | ||
| state = "idle"; | ||
| }); | ||
| game.mountChrome(); | ||
| activeGame = game; | ||
|
|
||
| const { sprites, restore } = await morph(); | ||
| game.onExit = (orig => () => { | ||
| restore(); | ||
| orig(); | ||
| })(game.onExit); | ||
|
|
||
| game.setSprites(sprites); | ||
| game.start(); |
Comment on lines
+30
to
+40
| if (isMobile()) return; | ||
| if (scroller().scrollHeight <= window.innerHeight + BOTTOM_THRESHOLD) return; | ||
|
|
||
| let state = "idle"; | ||
| let activeGame = null; | ||
| let prevY = scroller().scrollTop; | ||
|
|
||
| window.addEventListener("scroll", () => { | ||
| if (state === "running") return; | ||
|
|
||
| const y = scroller().scrollTop; |
Comment on lines
+8
to
+13
| function isMobile() { | ||
| return ( | ||
| window.matchMedia("(pointer: coarse)").matches || | ||
| window.innerWidth < 768 | ||
| ); | ||
| } |
Comment on lines
+12
to
+13
| const STAGES_TOTAL = 3; | ||
| const STAGE_BOSS_COUNTS = [0, 2, 2, 3]; |
| return { el, rect, html: serializeSvg(el) }; | ||
| }); | ||
|
|
||
| for (const c of captured) c.el.style.visibility = "hidden"; |
Comment on lines
+284
to
+305
| this.onKeyDown = (e) => { | ||
| if (e.key === "Escape") { | ||
| e.preventDefault(); | ||
| this.exit(); | ||
| return; | ||
| } | ||
| if (e.key === "r" || e.key === "R") { | ||
| if (this.state === "lost") this.restart(); | ||
| return; | ||
| } | ||
| if (e.key === "n" || e.key === "N") { | ||
| if (this.state === "stage-clear") this.nextStage(); | ||
| return; | ||
| } | ||
| if (e.key === "x" || e.key === "X" || e.key === "b" || e.key === "B") { | ||
| e.preventDefault(); | ||
| this.fireBomb(); | ||
| return; | ||
| } | ||
| this.keys.add(e.key); | ||
| }; | ||
| this.onKeyUp = (e) => this.keys.delete(e.key); |
Comment on lines
+250
to
+266
| this.hud.innerHTML = ` | ||
| <div data-hud="banner" style="position:absolute;top:38%;left:32px;max-width:440px;color:#fff;font-family:'Inter',system-ui,sans-serif;pointer-events:none;transform:translateY(-50%)"> | ||
| <h1 style="font-size:40px;font-weight:800;line-height:1.15;margin:0 0 16px 0;color:#fff;text-shadow:0 2px 16px rgba(0,0,0,0.95),0 0 4px rgba(0,0,0,0.9)">Kong Developer</h1> | ||
| <p style="font-size:16px;line-height:1.55;margin:0;opacity:0.95;text-shadow:0 1px 8px rgba(0,0,0,0.95),0 0 2px rgba(0,0,0,0.9)">Discover Kong's tools, APIs, and tutorials to help you build, secure, and scale your services.</p> | ||
| </div> | ||
| <div style="display:flex;justify-content:space-between;gap:12px;font-size:14px"> | ||
| <div style="display:flex;flex-direction:column;gap:6px;align-items:flex-start"> | ||
| <span data-hud="score" style="${chip}">SCORE 0</span> | ||
| <span data-hud="power" style="${chip}">POWER ●○○○</span> | ||
| </div> | ||
| <div style="display:flex;flex-direction:column;gap:6px;align-items:flex-end"> | ||
| <span data-hud="bombs" style="${chip}">SPECIAL ATTACK ●●●</span> | ||
| <span style="${chip};opacity:0.85">arrows · Z fire · X special · shift focus · esc exit</span> | ||
| </div> | ||
| </div> | ||
| <div data-hud="overlay" style="text-align:center;font-size:24px"></div> | ||
| `; |
Comment on lines
+268
to
+270
| this.resize = () => { | ||
| this.canvas.width = window.innerWidth; | ||
| this.canvas.height = window.innerHeight; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.