diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..bcb07a5 --- /dev/null +++ b/README.MD @@ -0,0 +1 @@ +# cmzi.uk \ No newline at end of file diff --git a/index.html b/index.html index b4234e0..f5a2839 100644 --- a/index.html +++ b/index.html @@ -26,8 +26,6 @@ @@ -37,153 +35,112 @@
-
+
-
-
- -
+
+
- -
- Cameron B. R. Redmore - + +
+ Cameron B. R. Redmore + - -
diff --git a/src/client.ts b/src/client.ts index 46813de..4e49320 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,6 +1,5 @@ import { treaty } from '@elysiajs/eden'; import type { App } from './server'; -import './gameoflife'; // Create Eden Treaty client using current page origin const client = treaty(window.location.origin); @@ -252,19 +251,6 @@ async function updateLastSongDisplay(song: LastSong | null, errorMessage?: strin // Initialize when DOM is loaded document.addEventListener('DOMContentLoaded', () => { - // Card flip logic - attach to orbit buttons - const cardContainer = document.querySelector('.card-container'); - const flipButtons = document.querySelectorAll('icon-mdi-orbit-variant'); - - flipButtons.forEach(button => { - button.addEventListener('click', (event) => { - event.stopPropagation(); - if (cardContainer) { - cardContainer.classList.toggle('is-flipped'); - } - }); - }); - // Lazy-load: observe the desktop and mobile containers const targets = [ document.getElementById('last-song-container'), diff --git a/src/gameoflife.ts b/src/gameoflife.ts deleted file mode 100644 index 8bec9d5..0000000 --- a/src/gameoflife.ts +++ /dev/null @@ -1,573 +0,0 @@ -// File to implement Game of Life using stored co-ordinates. -document.addEventListener('DOMContentLoaded', () => { - const canvas = document.getElementById('game-of-life-canvas') as HTMLCanvasElement; - - const gameOfLife = new GameOfLife(canvas); - - gameOfLife.start(); -}); - -interface Cell { - x: number; - y: number; -} - -class GameOfLife { - private canvas: HTMLCanvasElement; - private ctx: CanvasRenderingContext2D; - - // Define sets of cells (much more efficient for large grids than 2D array) - private activeCells: Set = new Set(); // Cells that are currently alive - private activeCellsNext: Set = new Set(); // Cells that will be alive in the next generation - - private potentialCells: Set = new Set(); // Cells that could potentially become alive in the next generation - private potentialCellsNext: Set = new Set(); // Cells that will be alive in the next generation - - // Viewport and interaction state - private offsetX: number = 0; - private offsetY: number = 0; - private zoomLevel: number = 1; - private isPaused: boolean = true; - - private isDragging: boolean = false; - private lastMouseX: number = 0; - private lastMouseY: number = 0; - - private targetFrameRate: number = 144; - private targetSimulationRate: number = 10; - - constructor(canvas: HTMLCanvasElement) { - this.canvas = canvas; - this.ctx = canvas.getContext('2d')!; - - // Set canvas size - this.canvas.width = 800; - this.canvas.height = 550; - - this.setupEventListeners(); - this.setupSimulationSpeedSlider(); - this.setupSaveLoadButtons(); - this.seedInitialPattern(); - } - - start() { - this.update(); - this.drawCells(); - } - - private setupEventListeners() { - // Click to add cells - this.canvas.addEventListener('click', (e) => { - if (!this.isDragging) { - this.handleCellClick(e); - } - }); - - // Right click to fill area with random cells - this.canvas.addEventListener('contextmenu', (e) => { - e.preventDefault(); // Prevent context menu - if (!this.isDragging) { - this.handleRightClick(e); - } - }); - - // Mouse events for panning - this.canvas.addEventListener('mousedown', (e) => { - this.isDragging = false; - this.lastMouseX = e.clientX; - this.lastMouseY = e.clientY; - }); - - this.canvas.addEventListener('mousemove', (e) => { - if (e.buttons === 1) { // Left mouse button pressed - const deltaX = e.clientX - this.lastMouseX; - const deltaY = e.clientY - this.lastMouseY; - - if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) { - this.isDragging = true; - this.offsetX -= deltaX; - this.offsetY -= deltaY; - } - - this.lastMouseX = e.clientX; - this.lastMouseY = e.clientY; - } - }); - - // Zoom with mouse wheel - this.canvas.addEventListener('wheel', (e) => { - e.preventDefault(); - const rect = this.canvas.getBoundingClientRect(); - const mouseX = e.clientX - rect.left; - const mouseY = e.clientY - rect.top; - - // Calculate world coordinates of mouse position before zoom - const worldX = (mouseX + this.offsetX) / this.zoomLevel; - const worldY = (mouseY + this.offsetY) / this.zoomLevel; - - const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; - const newZoomLevel = Math.max(0.1, Math.min(5, this.zoomLevel * zoomFactor)); - - // Calculate new offset to keep the mouse position at the same world coordinates - this.offsetX = (worldX * newZoomLevel) - mouseX; - this.offsetY = (worldY * newZoomLevel) - mouseY; - - this.zoomLevel = newZoomLevel; - }); - - // Keyboard controls - document.addEventListener('keydown', (e) => { - if (e.code === 'Space') { - e.preventDefault(); - this.isPaused = !this.isPaused; - } - }); - } - - private setupSimulationSpeedSlider() { - const speedSlider = document.getElementById('simulation-speed') as HTMLInputElement; - const speedDisplay = document.getElementById('speed-display') as HTMLSpanElement; - - if (speedSlider && speedDisplay) { - // Update display with initial value - speedDisplay.textContent = `${speedSlider.value} Hz`; - - // Listen for slider changes - speedSlider.addEventListener('input', (e) => { - const target = e.target as HTMLInputElement; - const newSpeed = parseInt(target.value); - this.targetSimulationRate = newSpeed; - speedDisplay.textContent = `${newSpeed} Hz`; - }); - } - } - - private setupSaveLoadButtons() { - const saveButton = document.getElementById('save-pattern') as HTMLButtonElement; - const loadButton = document.getElementById('load-pattern') as HTMLButtonElement; - const fileInput = document.getElementById('pattern-file') as HTMLInputElement; - - if (saveButton && loadButton) { - saveButton.addEventListener('click', async () => { - try { - const patternBlob = await this.savePattern(); - - // Create a download link - const url = URL.createObjectURL(patternBlob); - const a = document.createElement('a'); - a.href = url; - a.download = `game-of-life-pattern-${Date.now()}.gol.zz`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - - // Show success feedback - saveButton.textContent = 'Downloaded!'; - setTimeout(() => { - saveButton.textContent = 'Save Pattern'; - }, 1000); - } catch (error) { - console.error('Failed to save pattern:', error); - saveButton.textContent = 'Save Failed'; - setTimeout(() => { - saveButton.textContent = 'Save Pattern'; - }, 1000); - } - }); - - loadButton.addEventListener('click', () => { - // Trigger the file input - if (fileInput) { - fileInput.click(); - } else { - // Create a temporary file input if none exists - const tempInput = document.createElement('input'); - tempInput.type = 'file'; - tempInput.accept = '.gol.zz,.zz'; - tempInput.addEventListener('change', async (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (file) { - await this.handleFileLoad(file, loadButton); - } - }); - tempInput.click(); - } - }); - - // Set up file input if it exists - if (fileInput) { - fileInput.addEventListener('change', async (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (file) { - await this.handleFileLoad(file, loadButton); - } - }); - } - } - } - - private async handleFileLoad(file: File, loadButton: HTMLButtonElement) { - try { - await this.loadPattern(file); - loadButton.textContent = 'Loaded!'; - setTimeout(() => { - loadButton.textContent = 'Load Pattern'; - }, 1000); - } catch (error) { - console.error('Failed to load pattern:', error); - loadButton.textContent = 'Load Failed'; - setTimeout(() => { - loadButton.textContent = 'Load Pattern'; - }, 1000); - } - } - - private cellToString(cell: Cell): string { - return `${cell.x},${cell.y}`; - } - - private stringToCell(str: string): Cell { - const [x, y] = str.split(',').map(Number); - return { x, y }; - } - - private async savePattern(): Promise { - // Convert active cells to an array of coordinates - const cells = Array.from(this.activeCells).map(cellKey => { - const cell = this.stringToCell(cellKey); - return [cell.x, cell.y]; - }); - - // Convert to JSON - const jsonData = JSON.stringify(cells); - const encoder = new TextEncoder(); - const jsonBytes = encoder.encode(jsonData); - - // Compress using deflate - const compressionStream = new CompressionStream('deflate-raw'); - const writer = compressionStream.writable.getWriter(); - const reader = compressionStream.readable.getReader(); - - // Write the data to the compression stream - writer.write(jsonBytes); - writer.close(); - - // Read the compressed data - const compressedChunks: Uint8Array[] = []; - let done = false; - - while (!done) { - const { value, done: readerDone } = await reader.read(); - done = readerDone; - if (value) { - compressedChunks.push(value); - } - } - - // Combine all chunks into a single blob - const combinedArray = new Uint8Array( - compressedChunks.reduce((totalLength, chunk) => totalLength + chunk.length, 0) - ); - let offset = 0; - for (const chunk of compressedChunks) { - combinedArray.set(chunk, offset); - offset += chunk.length; - } - - return new Blob([combinedArray], { type: 'application/deflate' }); - } - - private async loadPattern(file: File): Promise { - try { - // Read the file as array buffer - const compressedData = await file.arrayBuffer(); - - // Decompress using deflate - const decompressionStream = new DecompressionStream('deflate-raw'); - const writer = decompressionStream.writable.getWriter(); - const reader = decompressionStream.readable.getReader(); - - // Write the compressed data to the decompression stream - writer.write(new Uint8Array(compressedData)); - writer.close(); - - // Read the decompressed data - const decompressedChunks: Uint8Array[] = []; - let done = false; - - while (!done) { - const { value, done: readerDone } = await reader.read(); - done = readerDone; - if (value) { - decompressedChunks.push(value); - } - } - - // Combine chunks and decode to string - const totalLength = decompressedChunks.reduce((sum, chunk) => sum + chunk.length, 0); - const combinedArray = new Uint8Array(totalLength); - let offset = 0; - - for (const chunk of decompressedChunks) { - combinedArray.set(chunk, offset); - offset += chunk.length; - } - - const decoder = new TextDecoder(); - const jsonData = decoder.decode(combinedArray); - const cells = JSON.parse(jsonData) as number[][]; - - // Clear current pattern - this.activeCells.clear(); - this.activeCellsNext.clear(); - this.potentialCells.clear(); - this.potentialCellsNext.clear(); - - // Load new pattern - for (const [x, y] of cells) { - const cellKey = this.cellToString({ x, y }); - this.activeCells.add(cellKey); - this.potentialCells.add(cellKey); - - // Add neighbors to potential cells - const neighbors = this.getNeighbors({ x, y }); - for (const neighbor of neighbors) { - this.potentialCells.add(this.cellToString(neighbor)); - } - } - - // Initialize next generation sets - this.activeCellsNext = new Set(this.activeCells); - this.potentialCellsNext = new Set(this.potentialCells); - - } catch (error) { - throw new Error('Invalid pattern file or decompression failed'); - } - } - - private handleCellClick(e: MouseEvent) { - const rect = this.canvas.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - - // Convert screen coordinates to grid coordinates - const cellSize = 10 * this.zoomLevel; - const gridX = Math.floor((x + this.offsetX) / cellSize); - const gridY = Math.floor((y + this.offsetY) / cellSize); - - const cellKey = this.cellToString({ x: gridX, y: gridY }); - - // Toggle cell state - if (this.activeCells.has(cellKey)) { - this.activeCells.delete(cellKey); - this.activeCellsNext.delete(cellKey); - this.potentialCellsNext.add(cellKey); - - const neighbors = this.getNeighbors({ x: gridX, y: gridY }); - for (const neighbor of neighbors) { - this.potentialCellsNext.add(this.cellToString(neighbor)); - } - } else { - this.activeCells.add(cellKey); - this.activeCellsNext.add(cellKey); - this.potentialCellsNext.add(cellKey); - - // Add neighbors to potential cells - const neighbors = this.getNeighbors({ x: gridX, y: gridY }); - for (const neighbor of neighbors) { - this.potentialCellsNext.add(this.cellToString(neighbor)); - } - } - } - - private handleRightClick(e: MouseEvent) { - const rect = this.canvas.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - - // Convert screen coordinates to grid coordinates - const cellSize = 10 * this.zoomLevel; - const centerX = Math.floor((x + this.offsetX) / cellSize); - const centerY = Math.floor((y + this.offsetY) / cellSize); - - // Define the area size (radius around the cursor) - scale inversely with zoom level - // Base radius of 5 at zoom level 1, scales up when zoomed out - const areaRadius = Math.max(2, Math.round(5 / this.zoomLevel)); - const fillDensity = 0.3; // 30% chance for each cell to be filled - - // Fill random cells in the area - for (let dx = -areaRadius; dx <= areaRadius; dx++) { - for (let dy = -areaRadius; dy <= areaRadius; dy++) { - // Only fill cells within a circular area - if (dx * dx + dy * dy <= areaRadius * areaRadius) { - if (Math.random() < fillDensity) { - const gridX = centerX + dx; - const gridY = centerY + dy; - const cellKey = this.cellToString({ x: gridX, y: gridY }); - - // Add the cell if it's not already alive - if (!this.activeCells.has(cellKey)) { - this.activeCells.add(cellKey); - this.activeCellsNext.add(cellKey); - this.potentialCellsNext.add(cellKey); - - // Add neighbors to potential cells - const neighbors = this.getNeighbors({ x: gridX, y: gridY }); - for (const neighbor of neighbors) { - this.potentialCellsNext.add(this.cellToString(neighbor)); - } - } - } - } - } - } - } - - private update() { - if (!this.isPaused) { - this.updateCells(); - } - - setTimeout(() => requestAnimationFrame(this.update.bind(this)), 1000 / this.targetSimulationRate); - } - - private updateCells() { - //Check if the card is currently flipped, otherwise do not update cells. - const isFlipped = document.querySelector('main')?.classList.contains('is-flipped'); - if (!isFlipped) { - return; - } - - this.activeCells = new Set(this.activeCellsNext); - this.activeCellsNext = new Set(); - - this.potentialCells = new Set(this.potentialCellsNext); - - // Each current cell has the potential to cause change on this update. - this.potentialCellsNext = new Set(this.activeCells); - - for (const cellKey of this.potentialCells) { - const cell = this.stringToCell(cellKey); - const neighbors = this.getNeighbors(cell); - const aliveNeighbors = neighbors.filter(n => this.activeCells.has(this.cellToString(n))).length; - - if (this.activeCells.has(cellKey)) { - // Cell is currently alive - if (aliveNeighbors < 2 || aliveNeighbors > 3) { - // Cell dies, so we don't add it to the next generation - - // However, this means the surrounding cells could change on the next generation. - for (const neighbor of neighbors) { - // Add neighbors to potential cells for next generation - this.potentialCellsNext.add(this.cellToString(neighbor)); - } - this.potentialCellsNext.add(cellKey); - continue; - } else { - // Cell stays alive - this.activeCellsNext.add(cellKey); - } - } else { - // Cell is currently dead - if (aliveNeighbors === 3) { - // Cell becomes alive - this.activeCellsNext.add(cellKey); - this.potentialCellsNext.add(cellKey); - - for (const neighbor of neighbors) { - // Add neighbors to potential cells for next generation - this.potentialCellsNext.add(this.cellToString(neighbor)); - } - } - } - } - } - - private getNeighbors(cell: Cell): Cell[] { - const neighbors: Cell[] = []; - for (let x = -1; x <= 1; x++) { - for (let y = -1; y <= 1; y++) { - // Ignore the cell itself. - if (x === 0 && y === 0) - { - continue; - } - neighbors.push({ x: cell.x + x, y: cell.y + y }); - } - } - return neighbors; - } - - private drawCells() { - const isFlipped = document.querySelector('main')?.classList.contains('is-flipped'); - if (!isFlipped) { - setTimeout(() => requestAnimationFrame(this.drawCells.bind(this)), 1000 / this.targetFrameRate); - return; - } - - // Draw the cells, taking into account viewport offset and zoom level. - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - - // Find what range of cells are currently visible based on the canvas size and zoom level. - let cellSize = 10; // Default size of each cell in pixels - - // Adjust size by zoom level - cellSize *= this.zoomLevel; - - // Calculate the visible range of cells - const startX = Math.floor(this.offsetX / cellSize); - const startY = Math.floor(this.offsetY / cellSize); - const endX = Math.ceil((this.offsetX + this.canvas.width) / cellSize); - const endY = Math.ceil((this.offsetY + this.canvas.height) / cellSize); - - // Draw grid lines for better visibility - this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; - this.ctx.lineWidth = 1; - - for (let x = startX; x <= endX; x++) { - const screenX = (x * cellSize) - this.offsetX; - this.ctx.beginPath(); - this.ctx.moveTo(screenX, 0); - this.ctx.lineTo(screenX, this.canvas.height); - this.ctx.stroke(); - } - - for (let y = startY; y <= endY; y++) { - const screenY = (y * cellSize) - this.offsetY; - this.ctx.beginPath(); - this.ctx.moveTo(0, screenY); - this.ctx.lineTo(this.canvas.width, screenY); - this.ctx.stroke(); - } - - this.ctx.fillStyle = '#67e8f9'; // cyan color to match theme - - // Draw active cells - for (let x = startX; x < endX; x++) { - for (let y = startY; y < endY; y++) { - const cellKey = this.cellToString({ x, y }); - if (this.activeCells.has(cellKey)) { - const screenX = (x * cellSize) - this.offsetX; - const screenY = (y * cellSize) - this.offsetY; - this.ctx.fillRect(screenX, screenY, cellSize - 1, cellSize - 1); - } - } - } - - this.ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; - this.ctx.font = '20px Exo 2 Variable, sans-serif'; - - // Draw pause indicator - if (this.isPaused) { - this.ctx.fillText('PAUSED - Press SPACE to resume', 10, 30); - } - - // Draw current X, Y and zoom level along with total number of active cells in bottom left - this.ctx.fillText(`X: ${Math.floor(this.offsetX / cellSize)}, Y: ${Math.floor(this.offsetY / cellSize)}, Zoom: ${this.zoomLevel.toFixed(2)}`, 10, this.canvas.height - 10); - this.ctx.fillText(`Active Cells: ${this.activeCells.size}`, 10, this.canvas.height - 30); - - - setTimeout(() => requestAnimationFrame(this.drawCells.bind(this)), 1000 / this.targetFrameRate); - } -} \ No newline at end of file diff --git a/src/style.css b/src/style.css index a5468ee..4222afc 100644 --- a/src/style.css +++ b/src/style.css @@ -219,85 +219,4 @@ details.history ul li { font-weight: 400; color: #cbd5e0; font-size: 12px; -} - -/* Card flip styles */ -.card-container { - perspective: 1500px; -} - -.card-flipper { - position: relative; - width: 100%; - min-height: 720px; - transform-style: preserve-3d; - transition: transform 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -.card-container.is-flipped .card-flipper { - transform: rotateY(-180deg); -} - -.card-front, -.card-back { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - backface-visibility: hidden; - -webkit-backface-visibility: hidden; -} - -.card-front { - z-index: 2; - transform: rotateY(0deg); -} - -.card-back { - transform: rotateY(-180deg); - z-index: 1; -} - -/* Slider styles for simulation speed control */ -.slider { - background: rgba(255, 255, 255, 0.2); - outline: none; - -webkit-appearance: none; - appearance: none; -} - -.slider::-webkit-slider-thumb { - appearance: none; - width: 20px; - height: 20px; - border-radius: 50%; - background: #67e8f9; - cursor: pointer; - border: 2px solid rgba(255, 255, 255, 0.3); - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); - transition: all 0.3s ease; -} - -.slider::-webkit-slider-thumb:hover { - background: #4dd0e1; - transform: scale(1.1); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); -} - -.slider::-moz-range-thumb { - width: 20px; - height: 20px; - border-radius: 50%; - background: #67e8f9; - cursor: pointer; - border: 2px solid rgba(255, 255, 255, 0.3); - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); - transition: all 0.3s ease; -} - -.slider::-moz-range-thumb:hover { - background: #4dd0e1; - transform: scale(1.1); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); } \ No newline at end of file