memoscan
← all memos

Memo 0xf0097a97…849492 on Ethereum

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Fever Dream Grid</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #000; overflow: hidden; width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; } .grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); width: 100vmin; height: 100vmin; gap: 0; } .cell { width: 100%; height: 100%; overflow: hidden; position: relative; background: #111; } .cell video { width: 100%; height: 100%; object-fit: cover; display: block; } .status { position: fixed; bottom: 8px; right: 8px; color: #CEFF00; padding: 4px 8px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 11px; font-weight: 500; z-index: 1000; opacity: 0; transition: opacity 0.3s; pointer-events: none; } .status.visible { opacity: 0.9; } </style> </head> <body> <div class="grid" id="grid"></div> <div class="status" id="status">Loading...</div> <script> const POLL_INTERVAL = 10000; // 10 seconds const API_BASE = 'https://chainfeed.online/api/projects'; const SLUG = 'fever-dream'; // Generate random pattern of 9 video indices function generateRandomPattern(videoCount) { const pattern = []; for (let i = 0; i < 9; i++) { pattern.push(Math.floor(Math.random() * videoCount)); } return pattern; } let currentPattern = []; let mintedVideos = []; // Array of {id, name, dataUri, txHash} let lastMintCount = 0; let statusTimer = null; // Decode video from transaction hash async function decodeVideoFromTx(txHash) { try { const response = await fetch(`https://eth-mainnet.g.alchemy.com/v2/T1Y8E3Z8hUkVqyXz-I67Qh_Un2wHsfIZ`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getTransactionByHash', params: [txHash] }) }); const data = await response.json(); if (!data.result || !data.result.input) { throw new Error('No transaction data'); } // Decode hex input to data URI const hex = data.result.input.slice(2); const bytes = new Uint8Array(hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); const decoder = new TextDecoder(); const dataUri = decoder.decode(bytes); return dataUri; } catch (e) { console.error('Failed to decode video:', txHash, e); return null; } } // Poll metadata to check for new mints async function pollMetadata() { try { const response = await fetch(`${API_BASE}/${SLUG}?metadata=true`); const data = await response.json(); const currentMintCount = data.collection.minted; if (currentMintCount > lastMintCount) { console.log('🆕 New mints detected:', currentMintCount - lastMintCount, 'Current videos:', mintedVideos.length); await fetchNewMints(currentMintCount); console.log('✅ After fetch, total videos:', mintedVideos.length); lastMintCount = currentMintCount; // Show status for 1 minute when new mints found updateStatus(`${currentMintCount}/450`, true); } else { // Just update text without showing updateStatus(`${currentMintCount}/450`, false); } } catch (e) { console.error('Poll error:', e); updateStatus('Error polling...'); } } // Fetch newly minted items - get TX hashes from blockchain async function fetchNewMints(expectedCount) { try { updateStatus(`Fetching ${expectedCount} mint TXs...`); // Fetch items to get mint transaction hashes // Since minted items can be anywhere, fetch multiple pages const limit = 100; let allMintTxs = []; let page = 0; // Fetch pages until we have all minted items while (allMintTxs.length < expectedCount && page < 5) { // Max 5 pages (500 items) const response = await fetch(`${API_BASE}/${SLUG}?page=${page}&limit=${limit}`); const data = await response.json(); const mintedItems = data.items.filter(item => item.minted && item.mintTxHash); allMintTxs.push(...mintedItems.map(item => ({ id: item.id, name: item.name, txHash: item.mintTxHash }))); console.log(`Page ${page}: Found ${mintedItems.length} minted (total: ${allMintTxs.length}/${expectedCount})`); page++; if (!data.hasMore) break; } console.log(`Found ${allMintTxs.length} mint transactions`); // Decode videos directly from blockchain TXs for (const item of allMintTxs) { const alreadyHave = mintedVideos.find(v => v.txHash === item.txHash); if (alreadyHave) continue; updateStatus(`Decoding TX ${item.txHash.slice(0, 10)}...`, true); const dataUri = await decodeVideoFromTx(item.txHash); if (dataUri) { mintedVideos.push({ id: item.id, name: item.name, dataUri: dataUri, txHash: item.txHash }); console.log('✓ Decoded from blockchain:', item.name, '| Total videos now:', mintedVideos.length); // Re-render grid immediately after each new video is decoded console.log('🎬 Rendering grid with', mintedVideos.length, 'videos'); renderGrid(true); } } updateStatus(`${expectedCount}/450`, true); } catch (e) { console.error('Fetch error:', e); updateStatus('Error loading videos'); } } // Render the grid with current pattern function renderGrid(stagger = false) { const grid = document.getElementById('grid'); if (mintedVideos.length === 0) { grid.innerHTML = '<div class="cell"></div>'.repeat(9); return; } // Generate new random pattern if we don't have one or video count changed if (currentPattern.length === 0) { currentPattern = generateRandomPattern(mintedVideos.length); } const pattern = currentPattern; if (!stagger) { // Initial render grid.innerHTML = pattern.map(videoIndex => { const video = mintedVideos[videoIndex]; if (!video) return '<div class="cell"></div>'; return ` <div class="cell"> <video src="${video.dataUri}" loop muted playsinline></video> </div> `; }).join(''); // Start videos with random delays const videoElements = grid.querySelectorAll('video'); videoElements.forEach(video => { const randomDelay = Math.random() * 1000; setTimeout(() => { video.play().catch(() => {}); // Ignore play interruption errors }, randomDelay); }); } else { // Staggered pattern change const cells = grid.querySelectorAll('.cell'); // If no cells exist yet, do initial render if (cells.length === 0) { grid.innerHTML = pattern.map(videoIndex => { const video = mintedVideos[videoIndex]; if (!video) return '<div class="cell"></div>'; return ` <div class="cell"> <video src="${video.dataUri}" loop muted playsinline></video> </div> `; }).join(''); const videoElements = grid.querySelectorAll('video'); videoElements.forEach(video => { const randomDelay = Math.random() * 1000; setTimeout(() => { video.play().catch(() => {}); // Ignore play interruption errors }, randomDelay); }); return; } // Update existing cells with stagger cells.forEach((cell, i) => { const randomDelay = Math.random() * 2000; setTimeout(() => { const videoIndex = pattern[i]; const video = mintedVideos[videoIndex]; if (!video) return; cell.innerHTML = `<video src="${video.dataUri}" loop muted playsinline></video>`; const videoEl = cell.querySelector('video'); videoEl.currentTime = Math.random() * 1; videoEl.play().catch(() => {}); // Ignore play interruption errors }, randomDelay); }); } } // Cycle to next pattern function cyclePattern() { if (mintedVideos.length === 0) return; // Generate new random pattern currentPattern = generateRandomPattern(mintedVideos.length); renderGrid(true); } // Update status display and show for 1 minute function updateStatus(msg, showTemporarily = false) { const statusEl = document.getElementById('status'); statusEl.textContent = msg; if (showTemporarily) { // Clear existing timer if (statusTimer) clearTimeout(statusTimer); // Show status statusEl.classList.add('visible'); // Hide after 60 seconds statusTimer = setTimeout(() => { statusEl.classList.remove('visible'); }, 60000); } } // Start async function init() { await pollMetadata(); // Poll for updates setInterval(pollMetadata, POLL_INTERVAL); // Cycle patterns every 3 seconds setInterval(cyclePattern, 3000); } init(); </script> </body> </html>