diff --git a/index.html b/index.html index 517e0bc..d931a71 100644 --- a/index.html +++ b/index.html @@ -10,7 +10,7 @@ - +

Fractious

- v20260924a + v20260924b +
+

Keyboard shortcuts

+
+
+ W A S D / ← + ↑ → ↓ +
+
Move
+
Q E / - +
+
Zoom out / in
+
Z X
+
Rotate anticlockwise / clockwise
+
R T
+
Shift colours left / right
+
F G
+
Decrease / increase colour cycling
+
Scroll
+
Zoom at cursor
+
Double-click
+
Zoom in at cursor (Shift to zoom out)
+
Shift + drag / scroll
+
Rotate
+
?
+
Show / hide this help
+
+
diff --git a/public/img/fractious-icon.png b/public/img/fractious-icon.png deleted file mode 100644 index db1ba76..0000000 Binary files a/public/img/fractious-icon.png and /dev/null differ diff --git a/public/img/fractious-player.png b/public/img/fractious-player.png deleted file mode 100644 index 89fdce4..0000000 Binary files a/public/img/fractious-player.png and /dev/null differ diff --git a/public/manifest.json b/public/manifest.json index eacda10..4edcd7c 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -6,8 +6,8 @@ "scope": ".", "display": "standalone", "start_url": ".", - "background_color": "#3d5b88", - "theme_color": "#3d5b88", + "background_color": "#000000", + "theme_color": "#000000", "description": "Explore infinite fractals in the Mandelbrot set. Discover beautiful downloadable images and share locations with friends—all with simple, fun controls.", "categories": ["education", "entertainment"], "orientation": "any", @@ -36,7 +36,7 @@ "src": "./img/icon-144x144.png", "sizes": "144x144", "type": "image/png", - "purpose": "any maskable" + "purpose": "any" }, { "src": "./img/icon-152x152.png", @@ -48,19 +48,31 @@ "src": "./img/icon-192x192.png", "sizes": "192x192", "type": "image/png", - "purpose": "any maskable" + "purpose": "any" }, { "src": "./img/icon-384x384.png", "sizes": "384x384", "type": "image/png", - "purpose": "any maskable" + "purpose": "any" + }, + { + "src": "./img/icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "./img/icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" }, { "src": "./img/icon-512x512.png", "sizes": "512x512", "type": "image/png", - "purpose": "any maskable" + "purpose": "maskable" } ], "screenshots": [ diff --git a/src/InteractionManager.js b/src/InteractionManager.js index 21a465b..0ed6915 100644 --- a/src/InteractionManager.js +++ b/src/InteractionManager.js @@ -400,6 +400,7 @@ export class InteractionManager { ['t', hueInc], ['f', hueStepDec], ['g', hueStepInc], + ['?', () => this.el.shortcuts?.togglePopover?.()], ]); } diff --git a/src/InteractionManager.test.js b/src/InteractionManager.test.js index 9960c6d..08dbd7c 100644 --- a/src/InteractionManager.test.js +++ b/src/InteractionManager.test.js @@ -92,6 +92,17 @@ describe('InteractionManager updateUI', () => { expect(elements.inputs.c_re.value).toBe('1.5'); }); + it('should toggle the shortcuts popover on ? without interacting with the fractal', () => { + elements.shortcuts = { togglePopover: vi.fn() }; + const preventDefault = vi.fn(); + + interactionManager.handleKeyDown({ key: '?', preventDefault }); + + expect(elements.shortcuts.togglePopover).toHaveBeenCalledTimes(1); + expect(preventDefault).toHaveBeenCalled(); + expect(callbacks.onInteract).not.toHaveBeenCalled(); + }); + it('should normalize negative rotation angles into [0, 360) degrees', () => { config.rotation = -Math.PI / 4; // -45 degrees -> 315.0 interactionManager.updateUI(); diff --git a/src/main.js b/src/main.js index 4ec2acd..8312b38 100644 --- a/src/main.js +++ b/src/main.js @@ -13,6 +13,7 @@ const elements = { bgCanvas: document.getElementById('fractal-bg'), crosshair: document.getElementById('crosshair'), iterIcon: document.getElementById('iter-icon'), + shortcuts: document.getElementById('shortcuts'), inputs: { c_re: document.getElementById('c_re'), c_im: document.getElementById('c_im'), diff --git a/src/style.css b/src/style.css index 2778b3f..135e3fb 100644 --- a/src/style.css +++ b/src/style.css @@ -301,3 +301,50 @@ footer { display: none; /* Hide verbose link text but keep the visual icons visible */ } } + +#shortcuts { + color: #cbd5e1; + background-color: rgba(10, 10, 15, 0.85); + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + padding: 0.75rem 1rem; + font-size: calc(var(--font-size) * 1.3); +} + +#shortcuts::backdrop { + background: rgba(0, 0, 0, 0.3); +} + +#shortcuts h2 { + font-size: inherit; + margin: 0 0 0.5rem; + color: #fff; +} + +#shortcuts dl { + display: grid; + grid-template-columns: auto auto; + gap: 0.35rem 1rem; + margin: 0; +} + +#shortcuts dd { + margin: 0; +} + +kbd { + font-family: monospace; + padding: 0 0.3em; + border: 1px solid rgba(255, 255, 255, 0.25); + border-radius: 3px; + background: rgba(255, 255, 255, 0.08); +} + +/* No keyboard shortcuts to show on touch-only devices */ +@media (hover: none) and (pointer: coarse) { + .controls .help { + display: none; + } +} diff --git a/vite.config.js b/vite.config.js index 6f7c1ac..09da850 100644 --- a/vite.config.js +++ b/vite.config.js @@ -14,6 +14,9 @@ export default defineConfig({ skipWaiting: true, clientsClaim: true, globPatterns: ['**/*.{js,css,html,ico,png,jpg,svg,wasm,wgsl}'], + // Screenshots, social preview and install icons are only fetched by the + // browser/OS on demand; don't make every visitor download them upfront. + globIgnores: ['**/img/**'], }, }), ],