feat: google balls
ebd375d7
3 file(s) · +303 −0
| 9 | 9 | Copyright © {year} |
|
| 10 | 10 | Steve Simkins |
|
| 11 | 11 | </div> |
|
| 12 | + | <div class="googleballs-trigger flex items-center gap-1" role="button" tabindex="0" aria-label="Easter egg"> |
|
| 13 | + | <span style="background:#4285F4;width:6px;height:6px;border-radius:50%;display:inline-block;"></span> |
|
| 14 | + | <span style="background:#EA4335;width:6px;height:6px;border-radius:50%;display:inline-block;"></span> |
|
| 15 | + | <span style="background:#FBBC05;width:6px;height:6px;border-radius:50%;display:inline-block;"></span> |
|
| 16 | + | <span style="background:#34A853;width:6px;height:6px;border-radius:50%;display:inline-block;"></span> |
|
| 17 | + | </div> |
|
| 12 | 18 | <a href="https://github.com/stevedylandev/stevedylan.dev">Source Code</a> |
|
| 13 | 19 | </footer> |
|
| 20 | + | ||
| 21 | + | <script> |
|
| 22 | + | document.querySelector('.googleballs-trigger')?.addEventListener('click', function() { |
|
| 23 | + | document.dispatchEvent(new CustomEvent('googleballs:open')); |
|
| 24 | + | }); |
|
| 25 | + | </script> |
| 1 | + | --- |
|
| 2 | + | --- |
|
| 3 | + | ||
| 4 | + | <canvas id="googleballs-canvas" width="400" height="400"></canvas> |
|
| 5 | + | ||
| 6 | + | <script> |
|
| 7 | + | (function() { |
|
| 8 | + | var canvas = document.getElementById("googleballs-canvas"); |
|
| 9 | + | var canvasHeight; |
|
| 10 | + | var canvasWidth; |
|
| 11 | + | var ctx; |
|
| 12 | + | var lastFrameTime = 0; |
|
| 13 | + | var animationId = null; |
|
| 14 | + | var pointCollection; |
|
| 15 | + | ||
| 16 | + | function init() { |
|
| 17 | + | updateCanvasDimensions(); |
|
| 18 | + | ||
| 19 | + | var g = [new Point(202, 78, 0.0, 9, "#ed9d33"), new Point(348, 83, 0.0, 9, "#d44d61"), new Point(256, 69, 0.0, 9, "#4f7af2"), new Point(214, 59, 0.0, 9, "#ef9a1e"), new Point(265, 36, 0.0, 9, "#4976f3"), new Point(300, 78, 0.0, 9, "#269230"), new Point(294, 59, 0.0, 9, "#1f9e2c"), new Point(45, 88, 0.0, 9, "#1c48dd"), new Point(268, 52, 0.0, 9, "#2a56ea"), new Point(73, 83, 0.0, 9, "#3355d8"), new Point(294, 6, 0.0, 9, "#36b641"), new Point(235, 62, 0.0, 9, "#2e5def"), new Point(353, 42, 0.0, 8, "#d53747"), new Point(336, 52, 0.0, 8, "#eb676f"), new Point(208, 41, 0.0, 8, "#f9b125"), new Point(321, 70, 0.0, 8, "#de3646"), new Point(8, 60, 0.0, 8, "#2a59f0"), new Point(180, 81, 0.0, 8, "#eb9c31"), new Point(146, 65, 0.0, 8, "#c41731"), new Point(145, 49, 0.0, 8, "#d82038"), new Point(246, 34, 0.0, 8, "#5f8af8"), new Point(169, 69, 0.0, 8, "#efa11e"), new Point(273, 99, 0.0, 8, "#2e55e2"), new Point(248, 120, 0.0, 8, "#4167e4"), new Point(294, 41, 0.0, 8, "#0b991a"), new Point(267, 114, 0.0, 8, "#4869e3"), new Point(78, 67, 0.0, 8, "#3059e3"), new Point(294, 23, 0.0, 8, "#10a11d"), new Point(117, 83, 0.0, 8, "#cf4055"), new Point(137, 80, 0.0, 8, "#cd4359"), new Point(14, 71, 0.0, 8, "#2855ea"), new Point(331, 80, 0.0, 8, "#ca273c"), new Point(25, 82, 0.0, 8, "#2650e1"), new Point(233, 46, 0.0, 8, "#4a7bf9"), new Point(73, 13, 0.0, 8, "#3d65e7"), new Point(327, 35, 0.0, 6, "#f47875"), new Point(319, 46, 0.0, 6, "#f36764"), new Point(256, 81, 0.0, 6, "#1d4eeb"), new Point(244, 88, 0.0, 6, "#698bf1"), new Point(194, 32, 0.0, 6, "#fac652"), new Point(97, 56, 0.0, 6, "#ee5257"), new Point(105, 75, 0.0, 6, "#cf2a3f"), new Point(42, 4, 0.0, 6, "#5681f5"), new Point(10, 27, 0.0, 6, "#4577f6"), new Point(166, 55, 0.0, 6, "#f7b326"), new Point(266, 88, 0.0, 6, "#2b58e8"), new Point(178, 34, 0.0, 6, "#facb5e"), new Point(100, 65, 0.0, 6, "#e02e3d"), new Point(343, 32, 0.0, 6, "#f16d6f"), new Point(59, 5, 0.0, 6, "#507bf2"), new Point(27, 9, 0.0, 6, "#5683f7"), new Point(233, 116, 0.0, 6, "#3158e2"), new Point(123, 32, 0.0, 6, "#f0696c"), new Point(6, 38, 0.0, 6, "#3769f6"), new Point(63, 62, 0.0, 6, "#6084ef"), new Point(6, 49, 0.0, 6, "#2a5cf4"), new Point(108, 36, 0.0, 6, "#f4716e"), new Point(169, 43, 0.0, 6, "#f8c247"), new Point(137, 37, 0.0, 6, "#e74653"), new Point(318, 58, 0.0, 6, "#ec4147"), new Point(226, 100, 0.0, 5, "#4876f1"), new Point(101, 46, 0.0, 5, "#ef5c5c"), new Point(226, 108, 0.0, 5, "#2552ea"), new Point(17, 17, 0.0, 5, "#4779f7"), new Point(232, 93, 0.0, 5, "#4b78f1")]; |
|
| 20 | + | ||
| 21 | + | var gLength = g.length; |
|
| 22 | + | for (var i = 0; i < gLength; i++) { |
|
| 23 | + | g[i].curPos.x = (canvasWidth/2 - 180) + g[i].curPos.x; |
|
| 24 | + | g[i].curPos.y = (canvasHeight/2 - 65) + g[i].curPos.y; |
|
| 25 | + | ||
| 26 | + | g[i].originalPos.x = (canvasWidth/2 - 180) + g[i].originalPos.x; |
|
| 27 | + | g[i].originalPos.y = (canvasHeight/2 - 65) + g[i].originalPos.y; |
|
| 28 | + | } |
|
| 29 | + | ||
| 30 | + | pointCollection = new PointCollection(); |
|
| 31 | + | pointCollection.points = g; |
|
| 32 | + | ||
| 33 | + | window.addEventListener('resize', updateCanvasDimensions); |
|
| 34 | + | window.addEventListener('mousemove', onMove); |
|
| 35 | + | ||
| 36 | + | canvas.ontouchmove = function(e) { |
|
| 37 | + | e.preventDefault(); |
|
| 38 | + | onTouchMove(e); |
|
| 39 | + | }; |
|
| 40 | + | ||
| 41 | + | canvas.ontouchstart = function(e) { |
|
| 42 | + | e.preventDefault(); |
|
| 43 | + | }; |
|
| 44 | + | ||
| 45 | + | lastFrameTime = 0; |
|
| 46 | + | animationId = requestAnimationFrame(animateRAF); |
|
| 47 | + | } |
|
| 48 | + | ||
| 49 | + | function stop() { |
|
| 50 | + | if (animationId) { |
|
| 51 | + | cancelAnimationFrame(animationId); |
|
| 52 | + | animationId = null; |
|
| 53 | + | } |
|
| 54 | + | window.removeEventListener('resize', updateCanvasDimensions); |
|
| 55 | + | window.removeEventListener('mousemove', onMove); |
|
| 56 | + | canvas.ontouchmove = null; |
|
| 57 | + | canvas.ontouchstart = null; |
|
| 58 | + | pointCollection = null; |
|
| 59 | + | lastFrameTime = 0; |
|
| 60 | + | if (ctx) { |
|
| 61 | + | ctx.clearRect(0, 0, canvasWidth, canvasHeight); |
|
| 62 | + | } |
|
| 63 | + | } |
|
| 64 | + | ||
| 65 | + | function animateRAF(timestamp) { |
|
| 66 | + | if (!lastFrameTime) lastFrameTime = timestamp; |
|
| 67 | + | var deltaTime = (timestamp - lastFrameTime) / 1000; |
|
| 68 | + | lastFrameTime = timestamp; |
|
| 69 | + | ||
| 70 | + | deltaTime = Math.min(deltaTime, 0.1); |
|
| 71 | + | ||
| 72 | + | draw(); |
|
| 73 | + | update(deltaTime); |
|
| 74 | + | ||
| 75 | + | animationId = requestAnimationFrame(animateRAF); |
|
| 76 | + | } |
|
| 77 | + | ||
| 78 | + | function updateCanvasDimensions() { |
|
| 79 | + | var modal = document.getElementById('googleballs-modal'); |
|
| 80 | + | if (!modal) return; |
|
| 81 | + | ||
| 82 | + | var dpr = window.devicePixelRatio || 1; |
|
| 83 | + | var logicalWidth = modal.clientWidth; |
|
| 84 | + | var logicalHeight = modal.clientHeight; |
|
| 85 | + | ||
| 86 | + | canvas.width = logicalWidth * dpr; |
|
| 87 | + | canvas.height = logicalHeight * dpr; |
|
| 88 | + | canvas.style.width = logicalWidth + 'px'; |
|
| 89 | + | canvas.style.height = logicalHeight + 'px'; |
|
| 90 | + | ||
| 91 | + | canvasWidth = logicalWidth; |
|
| 92 | + | canvasHeight = logicalHeight; |
|
| 93 | + | ||
| 94 | + | if (canvas.getContext) { |
|
| 95 | + | ctx = canvas.getContext('2d'); |
|
| 96 | + | ctx.scale(dpr, dpr); |
|
| 97 | + | } |
|
| 98 | + | ||
| 99 | + | recenterPoints(); |
|
| 100 | + | draw(); |
|
| 101 | + | } |
|
| 102 | + | ||
| 103 | + | function recenterPoints() { |
|
| 104 | + | if (!pointCollection) return; |
|
| 105 | + | ||
| 106 | + | var offsetX = (canvasWidth / 2 - 180); |
|
| 107 | + | var offsetY = (canvasHeight / 2 - 65); |
|
| 108 | + | ||
| 109 | + | for (var i = 0; i < pointCollection.points.length; i++) { |
|
| 110 | + | var point = pointCollection.points[i]; |
|
| 111 | + | ||
| 112 | + | var relX = point.originalPos.x - (canvasWidth / 2 - 180); |
|
| 113 | + | var relY = point.originalPos.y - (canvasHeight / 2 - 65); |
|
| 114 | + | ||
| 115 | + | point.originalPos.x = offsetX + relX; |
|
| 116 | + | point.originalPos.y = offsetY + relY; |
|
| 117 | + | ||
| 118 | + | point.curPos.x = point.originalPos.x; |
|
| 119 | + | point.curPos.y = point.originalPos.y; |
|
| 120 | + | } |
|
| 121 | + | } |
|
| 122 | + | ||
| 123 | + | function onMove(e) { |
|
| 124 | + | if (pointCollection) { |
|
| 125 | + | var modal = document.getElementById('googleballs-modal'); |
|
| 126 | + | var rect = modal.getBoundingClientRect(); |
|
| 127 | + | pointCollection.mousePos.set(e.clientX - rect.left, e.clientY - rect.top); |
|
| 128 | + | } |
|
| 129 | + | } |
|
| 130 | + | ||
| 131 | + | function onTouchMove(e) { |
|
| 132 | + | if (pointCollection) { |
|
| 133 | + | var modal = document.getElementById('googleballs-modal'); |
|
| 134 | + | var rect = modal.getBoundingClientRect(); |
|
| 135 | + | pointCollection.mousePos.set( |
|
| 136 | + | e.targetTouches[0].clientX - rect.left, |
|
| 137 | + | e.targetTouches[0].clientY - rect.top |
|
| 138 | + | ); |
|
| 139 | + | } |
|
| 140 | + | } |
|
| 141 | + | ||
| 142 | + | function draw() { |
|
| 143 | + | if (!ctx) return; |
|
| 144 | + | ctx.clearRect(0, 0, canvasWidth, canvasHeight); |
|
| 145 | + | if (pointCollection) pointCollection.draw(); |
|
| 146 | + | } |
|
| 147 | + | ||
| 148 | + | function update(dt) { |
|
| 149 | + | if (pointCollection) pointCollection.update(dt); |
|
| 150 | + | } |
|
| 151 | + | ||
| 152 | + | function Vector(x, y, z) { |
|
| 153 | + | this.x = x; |
|
| 154 | + | this.y = y; |
|
| 155 | + | this.z = z; |
|
| 156 | + | ||
| 157 | + | this.addX = function(x) { this.x += x; }; |
|
| 158 | + | this.addY = function(y) { this.y += y; }; |
|
| 159 | + | this.addZ = function(z) { this.z += z; }; |
|
| 160 | + | this.set = function(x, y, z) { this.x = x; this.y = y; this.z = z; }; |
|
| 161 | + | } |
|
| 162 | + | ||
| 163 | + | function PointCollection() { |
|
| 164 | + | this.mousePos = new Vector(0, 0); |
|
| 165 | + | this.points = []; |
|
| 166 | + | ||
| 167 | + | this.update = function(dt) { |
|
| 168 | + | var pointsLength = this.points.length; |
|
| 169 | + | for (var i = 0; i < pointsLength; i++) { |
|
| 170 | + | var point = this.points[i]; |
|
| 171 | + | if (point == null) continue; |
|
| 172 | + | ||
| 173 | + | var dx = this.mousePos.x - point.curPos.x; |
|
| 174 | + | var dy = this.mousePos.y - point.curPos.y; |
|
| 175 | + | var dd = (dx * dx) + (dy * dy); |
|
| 176 | + | var d = Math.sqrt(dd); |
|
| 177 | + | ||
| 178 | + | if (d < 150) { |
|
| 179 | + | point.targetPos.x = point.curPos.x - dx; |
|
| 180 | + | point.targetPos.y = point.curPos.y - dy; |
|
| 181 | + | } else { |
|
| 182 | + | point.targetPos.x = point.originalPos.x; |
|
| 183 | + | point.targetPos.y = point.originalPos.y; |
|
| 184 | + | } |
|
| 185 | + | ||
| 186 | + | point.update(dt); |
|
| 187 | + | } |
|
| 188 | + | }; |
|
| 189 | + | ||
| 190 | + | this.draw = function() { |
|
| 191 | + | var pointsLength = this.points.length; |
|
| 192 | + | for (var i = 0; i < pointsLength; i++) { |
|
| 193 | + | var point = this.points[i]; |
|
| 194 | + | if (point == null) continue; |
|
| 195 | + | point.draw(); |
|
| 196 | + | } |
|
| 197 | + | }; |
|
| 198 | + | } |
|
| 199 | + | ||
| 200 | + | function Point(x, y, z, size, colour) { |
|
| 201 | + | this.colour = colour; |
|
| 202 | + | this.curPos = new Vector(x, y, z); |
|
| 203 | + | this.friction = 0.8; |
|
| 204 | + | this.originalPos = new Vector(x, y, z); |
|
| 205 | + | this.radius = size; |
|
| 206 | + | this.size = size; |
|
| 207 | + | this.springStrength = 0.1; |
|
| 208 | + | this.targetPos = new Vector(x, y, z); |
|
| 209 | + | this.velocity = new Vector(0.0, 0.0, 0.0); |
|
| 210 | + | ||
| 211 | + | this.update = function(dt) { |
|
| 212 | + | var targetFrameTime = 0.03; |
|
| 213 | + | var timeScale = dt / targetFrameTime; |
|
| 214 | + | ||
| 215 | + | var dx = this.targetPos.x - this.curPos.x; |
|
| 216 | + | var ax = dx * this.springStrength * timeScale; |
|
| 217 | + | this.velocity.x += ax; |
|
| 218 | + | this.velocity.x *= Math.pow(this.friction, timeScale); |
|
| 219 | + | this.curPos.x += this.velocity.x * timeScale; |
|
| 220 | + | ||
| 221 | + | var dy = this.targetPos.y - this.curPos.y; |
|
| 222 | + | var ay = dy * this.springStrength * timeScale; |
|
| 223 | + | this.velocity.y += ay; |
|
| 224 | + | this.velocity.y *= Math.pow(this.friction, timeScale); |
|
| 225 | + | this.curPos.y += this.velocity.y * timeScale; |
|
| 226 | + | ||
| 227 | + | var dox = this.originalPos.x - this.curPos.x; |
|
| 228 | + | var doy = this.originalPos.y - this.curPos.y; |
|
| 229 | + | var dd = (dox * dox) + (doy * doy); |
|
| 230 | + | var d = Math.sqrt(dd); |
|
| 231 | + | ||
| 232 | + | this.targetPos.z = d/100 + 1; |
|
| 233 | + | var dz = this.targetPos.z - this.curPos.z; |
|
| 234 | + | var az = dz * this.springStrength * timeScale; |
|
| 235 | + | this.velocity.z += az; |
|
| 236 | + | this.velocity.z *= Math.pow(this.friction, timeScale); |
|
| 237 | + | this.curPos.z += this.velocity.z * timeScale; |
|
| 238 | + | ||
| 239 | + | this.radius = this.size * this.curPos.z; |
|
| 240 | + | if (this.radius < 1) this.radius = 1; |
|
| 241 | + | }; |
|
| 242 | + | ||
| 243 | + | this.draw = function() { |
|
| 244 | + | ctx.fillStyle = this.colour; |
|
| 245 | + | ctx.beginPath(); |
|
| 246 | + | ctx.arc(this.curPos.x, this.curPos.y, this.radius, 0, Math.PI*2, true); |
|
| 247 | + | ctx.fill(); |
|
| 248 | + | }; |
|
| 249 | + | } |
|
| 250 | + | ||
| 251 | + | // Expose init/stop for the modal controller |
|
| 252 | + | window.__googleballs = { init: init, stop: stop }; |
|
| 253 | + | })(); |
|
| 254 | + | </script> |
|
| 255 | + | ||
| 256 | + | <style> |
|
| 257 | + | #googleballs-canvas { |
|
| 258 | + | position: absolute; |
|
| 259 | + | top: 0; |
|
| 260 | + | left: 0; |
|
| 261 | + | } |
|
| 262 | + | </style> |
| 3 | 3 | import BaseHead from "@/components/layout/BaseHead"; |
|
| 4 | 4 | import Header from "@/components/layout/Header"; |
|
| 5 | 5 | import Footer from "@/components/layout/Footer"; |
|
| 6 | + | import GoogleBalls from "@/components/page/GoogleBalls.astro"; |
|
| 6 | 7 | import siteConfig from "@/site-config"; |
|
| 7 | 8 | ||
| 8 | 9 | interface Props { |
|
| 35 | 36 | <slot /> |
|
| 36 | 37 | </main> |
|
| 37 | 38 | <Footer /> |
|
| 39 | + | <div id="googleballs-modal" style="display:none;position:fixed;inset:0;z-index:9999;background:#121113;"> |
|
| 40 | + | <a href="https://googleballs.com" target="_blank" rel="noreferrer" style="position:absolute;top:16px;left:16px;color:#fff;font-size:16px;cursor:pointer;z-index:10000;text-decoration:underline;">Google Balls</a> |
|
| 41 | + | <button id="googleballs-close" style="position:absolute;top:16px;right:16px;z-index:10000;background:none;border:none;color:#fff;font-size:32px;cursor:pointer;line-height:1;" aria-label="Close">×</button> |
|
| 42 | + | <GoogleBalls /> |
|
| 43 | + | </div> |
|
| 44 | + | <script> |
|
| 45 | + | (function() { |
|
| 46 | + | var modal = document.getElementById('googleballs-modal'); |
|
| 47 | + | var closeBtn = document.getElementById('googleballs-close'); |
|
| 48 | + | ||
| 49 | + | document.addEventListener('googleballs:open', function() { |
|
| 50 | + | modal.style.display = 'block'; |
|
| 51 | + | if (window.__googleballs) window.__googleballs.init(); |
|
| 52 | + | }); |
|
| 53 | + | ||
| 54 | + | closeBtn.addEventListener('click', function() { |
|
| 55 | + | modal.style.display = 'none'; |
|
| 56 | + | if (window.__googleballs) window.__googleballs.stop(); |
|
| 57 | + | }); |
|
| 58 | + | ||
| 59 | + | document.addEventListener('keydown', function(e) { |
|
| 60 | + | if (e.key === 'Escape' && modal.style.display === 'block') { |
|
| 61 | + | modal.style.display = 'none'; |
|
| 62 | + | if (window.__googleballs) window.__googleballs.stop(); |
|
| 63 | + | } |
|
| 64 | + | }); |
|
| 65 | + | })(); |
|
| 66 | + | </script> |
|
| 38 | 67 | </body> |
|
| 39 | 68 | </html> |
|