0x26cda2b3…87d9sent to0xb8952959…ccf4·#23,923,048·view on Etherscan
<!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>