MOSAIX
MAKE YOUR MOSAIC
① DISCOGS COLLECTION
② MY IMAGE COLLECTION
③ TRY WITHOUT ACCOUNT
TRY MOSAIX
UPLOAD ANY ALBUM COVER.
AI DETECTS THE GENRE AND BUILDS YOUR MOSAIC
FROM 300 PUBLIC DISCOGS RELEASES.
DROP ANY ALBUM COVER
MY IMAGE COLLECTION
UPLOAD YOUR COVERS AS TILES,
THEN PICK ONE AS SOURCE IMAGE TO MAKE A MOSAIC.
① UPLOAD IMAGES AS TILES
DROP FOLDER OR CLICK TO SELECT
DISCOGS COLLECTION
UPLOAD THE COVER YOU WANT TO RECREATE
AS A MOSAIC FROM YOUR COLLECTION.
DROP COVER OR CLICK TO UPLOAD
← BACK TO START
MOSAIX
SCREENSAVER MODE
MOSAIC SETTINGS
GRID 32
BLEND 30%
GENRE WEIGHT 40%
UNIQUE TILES
PRESS F FOR FULLSCREEN  ·  SPACE TO PAUSE  ·  ESC TO EXIT
close
function colorDist(r1, g1, b1, r2, g2, b2) { return Math.sqrt((r1-r2)*(r1-r2) + (g1-g2)*(g1-g2) + (b1-b2)*(b1-b2)); } function generateMosaic() { if (!sourceImg || !tiles.length) return; var cv = document.getElementById('mosaic-canvas'); var tilePixels = Math.max(8, Math.floor(window.innerWidth / gridSize)); var tilesX = Math.ceil(window.innerWidth / tilePixels); var tilesY = Math.ceil(window.innerHeight / tilePixels); cv.width = window.innerWidth; cv.height = window.innerHeight; cv.style.display = ''; var me=document.getElementById('mosaic-empty');if(me)me.style.display='none'; var ctx = cv.getContext('2d'); var srcCv = document.createElement('canvas'); srcCv.width = tilesX; srcCv.height = tilesY; var srcCtx = srcCv.getContext('2d'); var srcAspect = sourceImg.naturalWidth / sourceImg.naturalHeight; var cvAspect = tilesX / tilesY; var sx=0, sy=0, sw=sourceImg.naturalWidth, sh=sourceImg.naturalHeight; if (srcAspect > cvAspect) { sw = sh * cvAspect; sx = (sourceImg.naturalWidth - sw) / 2; } else { sh = sw / cvAspect; sy = (sourceImg.naturalHeight - sh) / 2; } srcCtx.drawImage(sourceImg, sx, sy, sw, sh, 0, 0, tilesX, tilesY); var srcData = srcCtx.getImageData(0, 0, tilesX, tilesY).data; mosaicMap = []; var used = {}; var uniqueMode = document.getElementById('unique-tiles').checked; var genreW = parseInt(document.getElementById('genre-weight').value) / 100; var colorW = 1 - genreW; var maxColorDist = Math.sqrt(255*255*3); for (var y = 0; y < tilesY; y++) { mosaicMap[y] = []; for (var x = 0; x < tilesX; x++) { var idx = (y * tilesX + x) * 4; var tr = srcData[idx], tg = srcData[idx+1], tb = srcData[idx+2]; var bestScore = Infinity, bestTile = null; for (var t = 0; t < tiles.length; t++) { if (uniqueMode && used[t]) continue; var cd = colorDist(tr, tg, tb, tiles[t].r, tiles[t].g, tiles[t].b) / maxColorDist; var gs = 0; if (genreW > 0 && tiles[t].rel.id) gs = 1 - genreSimilarity(tiles[t].rel.id); var score = cd * colorW + gs * genreW; if (score < bestScore) { bestScore = score; bestTile = t; } } if (bestTile === null) { used = {}; for (var t = 0; t < tiles.length; t++) { var cd = colorDist(tr, tg, tb, tiles[t].r, tiles[t].g, tiles[t].b) / maxColorDist; if (cd < bestScore) { bestScore = cd; bestTile = t; } } } if (uniqueMode) used[bestTile] = true; mosaicMap[y][x] = bestTile; var tw = (x === tilesX-1) ? cv.width - x*tilePixels : tilePixels; var th = (y === tilesY-1) ? cv.height - y*tilePixels : tilePixels; if (tiles[bestTile]) ctx.drawImage(tiles[bestTile].img, 0, 0, tiles[bestTile].img.naturalWidth, tiles[bestTile].img.naturalHeight, x * tilePixels, y * tilePixels, tw, th); if (blendAmount > 0) { ctx.fillStyle = 'rgba(' + tr + ',' + tg + ',' + tb + ',' + blendAmount + ')'; ctx.fillRect(x * tilePixels, y * tilePixels, tilePixels, tilePixels); } } } document.getElementById('mosaic-stats').textContent = gridSize + '\u00d7' + gridSize + ' \u00b7 ' + (gridSize * gridSize) + ' tiles \u00b7 genre ' + Math.round(genreW * 100) + '%'; } function exportMosaic() { var cv = document.getElementById('mosaic-canvas'); var ms = document.getElementById('mosaic-screen'); if (ms && ms.classList.contains('active') && cv && cv.width > 0) { try { var link = document.createElement('a'); link.download = 'mosaix_' + Date.now() + '.png'; link.href = cv.toDataURL('image/png'); link.click(); return; } catch(e) {} } if (!cv || !cv.width || !mosaicMap || !mosaicMap.length) { showSourceStatus('GENERATE A MOSAIC FIRST'); return; } var tX = mosaicMap[0].length; var tY = mosaicMap.length; var tilePx = 64; var hiRes = document.createElement('canvas'); hiRes.width = tX * tilePx; hiRes.height = tY * tilePx; var ctx = hiRes.getContext('2d'); var srcCv2 = document.createElement('canvas'); srcCv2.width = tX; srcCv2.height = tY; var srcCtx2 = srcCv2.getContext('2d'); var srcAspect2 = sourceImg.naturalWidth / sourceImg.naturalHeight; var cvAspect2 = tX / tY; var sx2=0,sy2=0,sw2=sourceImg.naturalWidth,sh2=sourceImg.naturalHeight; if (srcAspect2 > cvAspect2) { sw2=sh2*cvAspect2; sx2=(sourceImg.naturalWidth-sw2)/2; } else { sh2=sw2/cvAspect2; sy2=(sourceImg.naturalHeight-sh2)/2; } srcCtx2.drawImage(sourceImg,sx2,sy2,sw2,sh2,0,0,tX,tY); var srcData2 = srcCtx2.getImageData(0,0,tX,tY).data; for (var y = 0; y < tY; y++) { for (var x = 0; x < tX; x++) { var tileIdx = mosaicMap[y][x]; ctx.drawImage(tiles[tileIdx].img, x * tilePx, y * tilePx, tilePx, tilePx); if (blendAmount > 0) { var idx = (y * tX + x) * 4; var srcData = srcData2; ctx.fillStyle = 'rgba(' + srcData[idx] + ',' + srcData[idx+1] + ',' + srcData[idx+2] + ',' + blendAmount + ')'; ctx.fillRect(x * tilePx, y * tilePx, tilePx, tilePx); } } } var a = document.createElement('a'); a.download = 'expedit-mosaic.png'; a.href = hiRes.toDataURL('image/png'); a.click(); } var _ssAnim = null; var _ssRunning = false; var _ssFreshStart = true; var _ssPausedImageData = null; var _ssFrame = null; var _ssTile = null; var _ssTargetGrid=32,_ssPhase='grid',_ssCurrentGrid=2,_ssCurrentBlend=0,_ssGridStep=1,_ssBlendStep=0.02,_ssPauseFrames=0,_ssRevealAlpha=0,_ssFadeAlpha=1,_ssLoopCount=0; var _ssColorFieldAlpha=0,_ssSpotlightZoom=1.0,_ssSpotlightAlpha=0,_ssAmbientAlpha=0,_ssCascadeCol=0,_ssDominantColor={r:0,g:0,b:0}; var _ssCoverZoom=null,_ssCoverAlpha=null; function renderMosaicAt(gs, blend) { if (!sourceImg || !tiles.length) return; var cv = document.getElementById('mosaic-canvas'); var tilePixels = Math.max(4, Math.floor(window.innerWidth / gs)); var tX = Math.ceil(window.innerWidth / tilePixels); var tY = Math.ceil(window.innerHeight / tilePixels); cv.width = window.innerWidth; cv.height = window.innerHeight; cv.style.display = ''; var srcCv = document.createElement('canvas'); srcCv.width = tX; srcCv.height = tY; var srcCtx = srcCv.getContext('2d'); var srcAspect = sourceImg.naturalWidth / sourceImg.naturalHeight; var cvAspect = tX / tY; var sx=0,sy=0,sw=sourceImg.naturalWidth,sh=sourceImg.naturalHeight; if (srcAspect > cvAspect) { sw=sh*cvAspect; sx=(sourceImg.naturalWidth-sw)/2; } else { sh=sw/cvAspect; sy=(sourceImg.naturalHeight-sh)/2; } srcCtx.drawImage(sourceImg, sx, sy, sw, sh, 0, 0, tX, tY); var srcData = srcCtx.getImageData(0, 0, tX, tY).data; var ctx = cv.getContext('2d'); var used = {}; for (var y = 0; y < tY; y++) { for (var x = 0; x < tX; x++) { var idx = (y * tX + x) * 4; var tr = srcData[idx], tg = srcData[idx+1], tb = srcData[idx+2]; var bestDist = Infinity, bestTile = 0; for (var t = 0; t < tiles.length; t++) { if (used[t]) continue; var d = colorDist(tr, tg, tb, tiles[t].r, tiles[t].g, tiles[t].b); if (d < bestDist) { bestDist = d; bestTile = t; } } used[bestTile] = true; if (Object.keys(used).length >= tiles.length) used = {}; if (tiles[bestTile]) ctx.drawImage(tiles[bestTile].img, x * tilePixels, y * tilePixels, tilePixels, tilePixels); if (blend > 0) { ctx.fillStyle = 'rgba(' + tr + ',' + tg + ',' + tb + ',' + blend + ')'; ctx.fillRect(x * tilePixels, y * tilePixels, tilePixels, tilePixels); } } } } function startScreensaver() { if (!tiles.length) return; if (!sourceImg) { sourceImg = tiles[Math.floor(Math.random() * tiles.length)].img; } var btn = document.getElementById('screensaver-btn'); if (_ssRunning) { stopScreensaver(); return; } _ssRunning = true; btn.textContent = '\u25a0 STOP'; btn.style.borderColor = 'var(--gold)'; btn.style.color = 'var(--gold)'; if (_ssFreshStart) { _ssTargetGrid = gridSize; _ssPhase = 'grid'; _ssCurrentGrid = 2; _ssCurrentBlend = 0; _ssGridStep = 1; _ssBlendStep = 0.02; _ssPauseFrames = 0; _ssRevealAlpha = 0; _ssFadeAlpha = 1; _ssLoopCount = 0; _ssColorFieldAlpha = 0; _ssSpotlightZoom = 1.0; _ssSpotlightAlpha = 0; _ssAmbientAlpha = 0; _ssCascadeCol = 0; } _ssTile = tiles[Math.floor(Math.random() * tiles.length)]; function pickRandomSource() { _ssTile = tiles[Math.floor(Math.random() * tiles.length)]; if (_ssTile.img.complete && _ssTile.img.naturalWidth > 0) { sourceImg = _ssTile.img; document.getElementById('source-img').src = _ssTile.img.src; sourceGenres = (genreCache[_ssTile.rel.id] || {}).genres || []; sourceStyles = (genreCache[_ssTile.rel.id] || {}).styles || []; } else { var img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function() { sourceImg = img; document.getElementById('source-img').src = img.src; sourceGenres = (genreCache[_ssTile.rel.id] || {}).genres || []; sourceStyles = (genreCache[_ssTile.rel.id] || {}).styles || []; }; img.src = _ssTile.img.src; } } _ssColorFieldAlpha = 0; _ssSpotlightZoom = 1.0; _ssSpotlightAlpha = 0; _ssAmbientAlpha = 0; _ssCascadeCol = 0; _ssDominantColor = {r:0,g:0,b:0}; function getDominantColor(img) { var cv = document.createElement('canvas'); cv.width = 16; cv.height = 16; var ctx = cv.getContext('2d'); ctx.drawImage(img, 0, 0, 16, 16); var d = ctx.getImageData(0, 0, 16, 16).data; var r=0,g=0,b=0,count=0; for (var i=0; i 30) _ssGridStep = 2; if (_ssCurrentGrid > 50) _ssGridStep = 3; if (_ssCurrentGrid >= _ssTargetGrid) { _ssCurrentGrid = _ssTargetGrid; _ssPhase = 'blend'; } _ssAnim = setTimeout(frame, getSsPreset().frameDelay); } else if (_ssPhase === 'blend') { renderMosaicAt(_ssTargetGrid, _ssCurrentBlend); _ssCurrentBlend += _ssBlendStep; if (_ssCurrentBlend >= 1) { _ssCurrentBlend = 1; renderMosaicAt(_ssTargetGrid, 1); _ssPhase = 'reveal'; _ssRevealAlpha = 0; _ssDominantColor = getDominantColor(sourceImg); } _ssAnim = setTimeout(frame, 80); } else if (_ssPhase === 'reveal') { var pixelSize = Math.max(1, Math.round(_ssTargetGrid * (1 - _ssRevealAlpha))); var small = document.createElement('canvas'); small.width = Math.max(1, Math.round(cv.width / pixelSize)); small.height = Math.max(1, Math.round(cv.height / pixelSize)); var sctx = small.getContext('2d'); sctx.imageSmoothingEnabled = false; sctx.drawImage(sourceImg, 0, 0, small.width, small.height); ctx.imageSmoothingEnabled = false; ctx.clearRect(0, 0, cv.width, cv.height); ctx.drawImage(small, 0, 0, cv.width, cv.height); ctx.imageSmoothingEnabled = true; document.getElementById('mosaic-stats').textContent = _ssTile.rel.artist + ' \u00b7 ' + _ssTile.rel.album; _ssRevealAlpha += 0.012; if (_ssRevealAlpha >= 1) { _ssPhase = 'colorfield'; _ssColorFieldAlpha = 0; } _ssAnim = setTimeout(frame, 50); } else if (_ssPhase === 'colorfield') { ctx.clearRect(0, 0, cv.width, cv.height); ctx.drawImage(sourceImg, 0, 0, cv.width, cv.height); var grad = ctx.createRadialGradient(cv.width/2, cv.height/2, 0, cv.width/2, cv.height/2, cv.width*0.8); grad.addColorStop(0, 'rgba(' + _ssDominantColor.r + ',' + _ssDominantColor.g + ',' + _ssDominantColor.b + ',0)'); grad.addColorStop(1, 'rgba(' + Math.round(_ssDominantColor.r*0.3) + ',' + Math.round(_ssDominantColor.g*0.3) + ',' + Math.round(_ssDominantColor.b*0.3) + ',' + (_ssColorFieldAlpha * 0.7) + ')'); ctx.fillStyle = grad; ctx.fillRect(0, 0, cv.width, cv.height); var vignette = ctx.createRadialGradient(cv.width/2, cv.height/2, cv.width*0.3, cv.width/2, cv.height/2, cv.width*0.9); vignette.addColorStop(0, 'rgba(0,0,0,0)'); vignette.addColorStop(1, 'rgba(0,0,0,' + (_ssColorFieldAlpha * 0.6) + ')'); ctx.fillStyle = vignette; ctx.fillRect(0, 0, cv.width, cv.height); _ssColorFieldAlpha += 0.015; if (_ssColorFieldAlpha >= 1) { _ssPhase = 'spotlight'; _ssSpotlightZoom = 1.0; _ssSpotlightAlpha = 0; _ssAmbientAlpha = 0; } _ssAnim = setTimeout(frame, 50); } else if (_ssPhase === 'spotlight') { ctx.clearRect(0, 0, cv.width, cv.height); ctx.fillStyle = 'rgb(' + Math.round(_ssDominantColor.r*0.08) + ',' + Math.round(_ssDominantColor.g*0.08) + ',' + Math.round(_ssDominantColor.b*0.08) + ')'; ctx.fillRect(0, 0, cv.width, cv.height); var glowR = Math.min(255, _ssDominantColor.r + 60); var glowG = Math.min(255, _ssDominantColor.g + 60); var glowB = Math.min(255, _ssDominantColor.b + 60); var glow = ctx.createRadialGradient(cv.width/2, cv.height/2, 0, cv.width/2, cv.height/2, cv.width*0.6); glow.addColorStop(0, 'rgba(' + glowR + ',' + glowG + ',' + glowB + ',' + (_ssAmbientAlpha * 0.3) + ')'); glow.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle = glow; ctx.fillRect(0, 0, cv.width, cv.height); var scale = Math.min(cv.width / sourceImg.naturalWidth, cv.height / sourceImg.naturalHeight) * _ssSpotlightZoom; var coverW = sourceImg.naturalWidth * scale; var coverH = sourceImg.naturalHeight * scale; var cx = cv.width/2 - coverW/2; var cy = cv.height/2 - coverH/2; ctx.shadowColor = 'rgba(' + _ssDominantColor.r + ',' + _ssDominantColor.g + ',' + _ssDominantColor.b + ',' + (_ssAmbientAlpha * 0.8) + ')'; ctx.shadowBlur = 80 * _ssAmbientAlpha; ctx.globalAlpha = _ssSpotlightAlpha; ctx.drawImage(sourceImg, cx, cy, coverW, coverH); ctx.globalAlpha = 1; ctx.shadowBlur = 0; if (_ssSpotlightAlpha < 1) { _ssSpotlightAlpha += 0.02; _ssAmbientAlpha += 0.02; } _ssSpotlightZoom += 0.0008; document.getElementById('mosaic-stats').textContent = _ssTile.rel.artist + ' \u00b7 ' + _ssTile.rel.album; _ssPauseFrames++; if (_ssPauseFrames >= 180) { _ssPhase = 'cascade'; _ssCascadeCol = 0; _ssPauseFrames = 0; } _ssAnim = setTimeout(frame, 50); } else if (_ssPhase === 'cascade') { ctx.clearRect(0, 0, cv.width, cv.height); ctx.fillStyle = 'rgb(' + Math.round(_ssDominantColor.r*0.08) + ',' + Math.round(_ssDominantColor.g*0.08) + ',' + Math.round(_ssDominantColor.b*0.08) + ')'; ctx.fillRect(0, 0, cv.width, cv.height); var scaleC = Math.min(cv.width / sourceImg.naturalWidth, cv.height / sourceImg.naturalHeight) * _ssSpotlightZoom; var coverWC = sourceImg.naturalWidth * scaleC; var coverHC = sourceImg.naturalHeight * scaleC; var cxc = cv.width/2 - coverWC/2; var cyc = cv.height/2 - coverHC/2; ctx.globalAlpha = Math.max(0, 1 - _ssCascadeCol / _ssTargetGrid); ctx.drawImage(sourceImg, cxc, cyc, coverWC, coverHC); ctx.globalAlpha = 1; var tilePixels = Math.max(8, Math.floor(cv.width / _ssTargetGrid)); var progress = _ssCascadeCol / _ssTargetGrid; for (var col = 0; col <= _ssCascadeCol && col < _ssTargetGrid; col++) { var colAlpha = Math.min(1, (_ssCascadeCol - col + 1) / 3); for (var row = 0; row < _ssTargetGrid; row++) { var idx = Math.floor(Math.random() * tiles.length); var t = tiles[idx]; ctx.globalAlpha = colAlpha; ctx.drawImage(t.img, col * tilePixels, row * tilePixels, tilePixels, tilePixels); } } ctx.globalAlpha = 1; _ssCascadeCol += getSsPreset().cascadeSpeed; if (_ssCascadeCol >= _ssTargetGrid) { _ssPhase = 'fadeout'; _ssFadeAlpha = 1; } _ssAnim = setTimeout(frame, 80); } else if (_ssPhase === 'fadeout') { if (_ssCoverZoom === null) { _ssCoverZoom = 3.0; _ssCoverAlpha = 0; _ssFadeAlpha = 1; _ssPauseFrames = 0; } if (_ssCoverAlpha < 1 || _ssCoverZoom > 1.0) { ctx.fillStyle = '#0a0a0a'; ctx.fillRect(0, 0, cv.width, cv.height); if (sourceImg) { var sw = cv.width * _ssCoverZoom; var sh = sw / (sourceImg.naturalWidth / sourceImg.naturalHeight); var sx = (cv.width - sw) / 2; var sy = (cv.height - sh) / 2; ctx.globalAlpha = _ssCoverAlpha; ctx.drawImage(sourceImg, sx, sy, sw, sh); ctx.globalAlpha = 1; } _ssCoverZoom = Math.max(1.0, _ssCoverZoom - 0.035); _ssCoverAlpha = Math.min(1, _ssCoverAlpha + 0.025); _ssAnim = setTimeout(frame, 30); } else if (_ssPauseFrames < 40) { _ssPauseFrames++; if (sourceImg) { var sw2 = cv.width; var sh2 = sw2 / (sourceImg.naturalWidth / sourceImg.naturalHeight); ctx.drawImage(sourceImg, 0, (cv.height - sh2) / 2, sw2, sh2); } _ssAnim = setTimeout(frame, 30); } else { _ssFadeAlpha = Math.max(0, _ssFadeAlpha - 0.018); ctx.fillStyle = '#0a0a0a'; ctx.fillRect(0, 0, cv.width, cv.height); if (sourceImg) { var sw3 = cv.width; var sh3 = sw3 / (sourceImg.naturalWidth / sourceImg.naturalHeight); ctx.globalAlpha = _ssFadeAlpha; ctx.drawImage(sourceImg, 0, (cv.height - sh3) / 2, sw3, sh3); ctx.globalAlpha = 1; } if (_ssFadeAlpha > 0) { _ssAnim = setTimeout(frame, 30); } else { _ssCoverZoom = null; _ssCoverAlpha = null; _ssPauseFrames = 0; _ssFadeAlpha = 1; _ssPhase = 'grid'; _ssCurrentGrid = 2; _ssCurrentBlend = 0; _ssGridStep = 1; _ssSpotlightZoom = 1.0; _ssSpotlightAlpha = 0; _ssAmbientAlpha = 0; _ssColorFieldAlpha = 0; _ssLoopCount++; if (_ssLoopCount >= 3) { _ssLoopCount = 0; pickRandomSource(); } _ssAnim = setTimeout(frame, 30); } } } } _ssFrame = frame; frame(); } function stopScreensaver() { _ssRunning = false; _ssPaused = false; _ssFreshStart = true; if (_ssAnim) { clearTimeout(_ssAnim); _ssAnim = null; } var btn = document.getElementById('screensaver-btn'); btn.textContent = '\u25b6 SCREENSAVER'; btn.style.borderColor = ''; btn.style.color = ''; } var _ssPaused = false; function pauseScreensaver() { _ssRunning = false; _ssPaused = true; _ssFreshStart = false; if (_ssAnim) { clearTimeout(_ssAnim); _ssAnim = null; } var cv = document.getElementById('mosaic-canvas'); if (cv) { var ctx = cv.getContext('2d'); _ssPausedImageData = ctx.getImageData(0, 0, cv.width, cv.height); } var btn = document.getElementById('screensaver-btn'); btn.textContent = '\u25b6 RESUME'; btn.style.borderColor = 'var(--gold)'; btn.style.color = 'var(--gold)'; } function resumeScreensaver() { _ssPaused = false; _ssRunning = true; var btn = document.getElementById('screensaver-btn'); btn.textContent = '\u25a0 STOP'; if (_ssFrame) _ssFrame(); } var dropZone = document.getElementById('drop-zone'); if (dropZone) { dropZone.addEventListener('dragover', function(e) { e.preventDefault(); dropZone.classList.add('active'); }); dropZone.addEventListener('dragleave', function() { dropZone.classList.remove('active'); }); dropZone.addEventListener('drop', function(e) { e.preventDefault(); dropZone.classList.remove('active'); handleFile(e.dataTransfer.files[0]); }); } var preview = document.getElementById('source-preview'); var cursor = document.getElementById('color-cursor'); if (preview) preview.addEventListener('mousemove', function(e) { if (!sourceImg) return; var rect = preview.getBoundingClientRect(); var x = e.clientX - rect.left; var y = e.clientY - rect.top; cursor.style.display = ''; cursor.style.left = x + 'px'; cursor.style.top = y + 'px'; var cv = document.createElement('canvas'); cv.width = sourceImg.width; cv.height = sourceImg.height; var ctx = cv.getContext('2d'); ctx.drawImage(sourceImg, 0, 0); var px = Math.floor(x / rect.width * sourceImg.width); var py = Math.floor(y / rect.height * sourceImg.height); var d = ctx.getImageData(px, py, 1, 1).data; cursor.style.background = 'rgb(' + d[0] + ',' + d[1] + ',' + d[2] + ')'; document.getElementById('color-info').style.display = 'flex'; document.getElementById('ci-preview').style.background = 'rgb(' + d[0] + ',' + d[1] + ',' + d[2] + ')'; document.getElementById('ci-hex').textContent = '#' + [d[0], d[1], d[2]].map(function(v) { return v.toString(16).padStart(2, '0'); }).join(''); document.getElementById('ci-rgb').textContent = 'RGB(' + d[0] + ', ' + d[1] + ', ' + d[2] + ')'; }); if (preview) preview.addEventListener('mouseleave', function() { if(cursor) cursor.style.display = 'none'; }); var mosaicCv = document.getElementById('mosaic-canvas'); mosaicCv.addEventListener('mousemove', function(e) { if (!mosaicMap.length) return; var rect = mosaicCv.getBoundingClientRect(); var tilePixelsHov = Math.max(4, Math.floor(window.innerWidth / gridSize)); var x = Math.floor(e.clientX / tilePixelsHov); var y = Math.floor(e.clientY / tilePixelsHov); if (!mosaicMap || !mosaicMap[y] || x >= mosaicMap[0].length) return; var tileIdx = mosaicMap[y][x]; if (tileIdx == null) return; var tile = tiles[tileIdx]; var info = document.getElementById('mosaic-info'); info.style.display = ''; var ht = document.getElementById('hover-thumb'); if(ht) ht.src = ''; document.getElementById('hover-artist').textContent = tile.rel.artist; document.getElementById('hover-album').textContent = tile.rel.album; }); mosaicCv.addEventListener('mouseleave', function() { document.getElementById('mosaic-info').style.display = 'none'; }); window.addEventListener('resize', function() { if (sourceImg) drawGridOverlay(); }); // loadFromNavidrome(); function extractMeta(release) { var bi = release.basic_information; return { id: 'd_' + bi.id, artist: (bi.artists && bi.artists[0]) ? bi.artists[0].name : '', album: bi.title, genres: (bi.genres || []).join(','), styles: (bi.styles || []).join(','), cover_image: bi.cover_image || '' }; } function dbPutMeta(db, record) { return new Promise(function(res) { var tx = db.transaction('meta', 'readwrite'); tx.objectStore('meta').put(record); tx.oncomplete = res; tx.onerror = res; }); } function dbGetMeta(db, key) { return new Promise(function(res) { var tx = db.transaction('meta', 'readonly'); var rq = tx.objectStore('meta').get(key); rq.onsuccess = function() { res(rq.result || null); }; rq.onerror = function() { res(null); }; }); } async function autoDetectGenreFromDiscogs(img) { var cv = document.createElement('canvas'); cv.width = 16; cv.height = 16; var ctx = cv.getContext('2d'); ctx.drawImage(img, 0, 0, 16, 16); var searchBox = document.getElementById('genre-search-input'); if (!searchBox) return; var token = localStorage.getItem('discogs_token') || ''; var query = searchBox.value.trim(); if (!query) return; var headers = token ? { 'Authorization': 'Discogs token=' + token } : { 'User-Agent': 'Mosaix/1.0' }; try { var r = await fetch('/discogs-search/?q=' + encodeURIComponent(query) + '&type=release&per_page=1'); var d = await r.json(); var items = d.results || []; if (!items.length) return; var item = items[0]; sourceGenres = item.genre || []; sourceStyles = item.style || []; var tags = sourceGenres.concat(sourceStyles); document.getElementById('auto-genre-tags').textContent = tags.join(' · '); document.getElementById('auto-genre-display').style.display = ''; await loadFilteredTiles(); } catch(e) {} } async function loadFilteredTiles() { var username = localStorage.getItem('discogs_username'); if (!username) return; var db = await openMosaixDB(); var record = await dbGetMeta(db, 'collection_' + username); if (!record || !record.meta) return; var allMeta = record.meta; var filtered = allMeta.filter(function(rel) { var g = (rel.genres || '').split(',').map(function(s){return s.trim().toLowerCase();}); var s = (rel.styles || '').split(',').map(function(s){return s.trim().toLowerCase();}); var combined = g.concat(s); var matchGenres = sourceGenres.map(function(x){return x.toLowerCase();}); var matchStyles = sourceStyles.map(function(x){return x.toLowerCase();}); return combined.some(function(t){ return matchGenres.indexOf(t) !== -1 || matchStyles.indexOf(t) !== -1; }); }); if (filtered.length < 50) filtered = allMeta; var selection = filtered.sort(function() { return Math.random() - 0.5; }).slice(0, 300); var status = document.getElementById('tile-preview-count'); if(status) status.textContent = 'LOADING ' + selection.length + ' GENRE-MATCHED COVERS...'; tiles = []; tileData = []; var grid = document.getElementById('tile-grid-discogs'); if(grid) grid.innerHTML = ''; var loaded = 0; var prog = document.getElementById('tile-progress-discogs'); selection.forEach(function(rel) { genreCache[rel.id] = { genres: (rel.genres || '').split(',').map(function(s){return s.trim();}).filter(Boolean), styles: (rel.styles || '').split(',').map(function(s){return s.trim();}).filter(Boolean) }; var cachedCheck = null; var imgUrl = proxyImg(rel.cover_image); if (!imgUrl || rel.cover_image.indexOf('spacer') !== -1) { loaded++; if (loaded >= selection.length) { onLoadingComplete(); resolve_covers(); } return; } var img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function() { var cv = document.createElement('canvas'); cv.width = 16; cv.height = 16; var ctx = cv.getContext('2d'); ctx.drawImage(img, 0, 0, 16, 16); var data = ctx.getImageData(0, 0, 16, 16).data; var rr=0,gg=0,bb=0,count=0; for (var p=0; p= selection.length) { onLoadingComplete(); showTilePreview(); if(status) status.textContent = loaded+' COVERS LOADED'; } }; img.onerror = function() { loaded++; if (loaded>=selection.length) onLoadingComplete(); }; img.src = imgUrl; }); } function switchSource(mode) { return; // disabled in new UI document.getElementById('tab-discogs').style.display = 'none'; document.getElementById('tab-upload').style.display = 'none'; document.getElementById('tab-search').style.display = 'none'; document.getElementById('src-btn-discogs').className = 'btn secondary'; document.getElementById('src-btn-upload').className = 'btn secondary'; document.getElementById('src-btn-search').className = 'btn secondary'; if (mode === 'discogs') { document.getElementById('tab-discogs').style.display = ''; document.getElementById('src-btn-discogs').className = 'btn primary'; } else if (mode === 'upload') { document.getElementById('tab-upload').style.display = ''; document.getElementById('src-btn-upload').className = 'btn primary'; } else if (mode === 'search') { document.getElementById('tab-search').style.display = ''; document.getElementById('src-btn-search').className = 'btn primary'; } } async function searchReleaseGenre() { var query = document.getElementById('genre-search-input').value.trim(); if (!query) return; var results = document.getElementById('genre-search-results'); results.innerHTML = '
SEARCHING...
'; var token = document.getElementById('discogs-token').value.trim(); var headers = token ? { 'Authorization': 'Discogs token=' + token } : { 'User-Agent': 'Mosaix/1.0' }; try { var r = await fetch('/discogs-search/?q=' + encodeURIComponent(query) + '&type=release&per_page=5'); var d = await r.json(); var items = d.results || []; if (!items.length) { results.innerHTML = '
NO RESULTS
'; return; } results.innerHTML = ''; items.forEach(function(item) { var genres = (item.genre || []).concat(item.style || []); var div = document.createElement('div'); div.style.cssText = 'padding:5px 6px;margin-top:3px;background:var(--bg3);border-radius:2px;cursor:pointer;font-size:9px;color:var(--gray);letter-spacing:1px'; div.textContent = (item.title || '') + (genres.length ? ' · ' + genres.slice(0,3).join(', ') : ''); div.onmouseover = function() { this.style.background = 'var(--bg)'; }; div.onmouseout = function() { this.style.background = 'var(--bg3)'; }; div.onclick = function() { sourceGenres = item.genre || []; sourceStyles = item.style || []; var tags = sourceGenres.concat(sourceStyles); document.getElementById('auto-genre-tags').textContent = tags.join(' · '); document.getElementById('auto-genre-display').style.display = ''; document.getElementById('source-genres').textContent = tags.join(' · '); results.innerHTML = ''; }; results.appendChild(div); }); } catch(e) { results.innerHTML = '
ERROR: ' + e.message + '
'; } } var MOSAIX_DB = 'mosaix_cache'; var MOSAIX_STORE = 'covers'; function openMosaixDB() { return new Promise(function(res, rej) { var rq = indexedDB.open(MOSAIX_DB, 3); rq.onupgradeneeded = function(e) { var db = e.target.result; if (!db.objectStoreNames.contains(MOSAIX_STORE)) { db.createObjectStore(MOSAIX_STORE, { keyPath: 'id' }); } if (!db.objectStoreNames.contains('meta')) { db.createObjectStore('meta', { keyPath: 'id' }); } }; rq.onsuccess = function(e) { res(e.target.result); }; rq.onerror = function(e) { rej(e.target.error); }; }); } function dbPut(db, record) { return new Promise(function(res) { var tx = db.transaction(MOSAIX_STORE, 'readwrite'); tx.objectStore(MOSAIX_STORE).put(record); tx.oncomplete = res; tx.onerror = res; }); } function dbGetAll(db) { return new Promise(function(res) { var tx = db.transaction(MOSAIX_STORE, 'readonly'); var rq = tx.objectStore(MOSAIX_STORE).getAll(); rq.onsuccess = function() { res(rq.result || []); }; rq.onerror = function() { res([]); }; }); } function dbClear(db) { return new Promise(function(res) { var tx = db.transaction(MOSAIX_STORE, 'readwrite'); tx.objectStore(MOSAIX_STORE).clear(); tx.oncomplete = res; tx.onerror = res; }); } function imgToBase64(img) { var cv = document.createElement('canvas'); cv.width = img.naturalWidth || 150; cv.height = img.naturalHeight || 150; var ctx = cv.getContext('2d'); ctx.drawImage(img, 0, 0); try { return cv.toDataURL('image/jpeg', 0.7); } catch(e) { return null; } } async function clearMosaixCache() { var db = await openMosaixDB(); await dbClear(db); document.getElementById('cache-status').textContent = 'CACHE CLEARED'; } async function updateCacheStatus() { try { var db = await openMosaixDB(); var all = await dbGetAll(db); var el = document.getElementById('cache-status'); if (all.length > 0) { el.textContent = all.length + ' COVERS CACHED'; el.style.color = 'var(--gold)'; } else { el.textContent = 'NO CACHE'; el.style.color = 'var(--dark)'; } } catch(e) {} } async function loadFromDiscogs() { var token = document.getElementById('discogs-token').value.trim(); var status = document.getElementById('discogs-status'); if (!token) { status.textContent = 'TOKEN REQUIRED'; return; } var prog = document.getElementById('tile-progress-discogs'); prog.style.width = '2%'; var db = await openMosaixDB(); var cached = await dbGetAll(db); if (cached.length > 0) { status.textContent = 'LOADING FROM CACHE (' + cached.length + ' COVERS)...'; tiles = []; tileData = []; cached.forEach(function(record) { if (record.rel && record.rel.id) { genreCache[record.rel.id] = { genres: (record.rel.genres || '').split(',').map(function(s){return s.trim();}).filter(Boolean), styles: (record.rel.styles || '').split(',').map(function(s){return s.trim();}).filter(Boolean) }; } }); var grid = document.getElementById('tile-grid-discogs'); grid.innerHTML = ''; var loaded = 0; var total = cached.length; var selection = cached.sort(function() { return Math.random() - 0.5; }).slice(0, 300); selection.forEach(function(record) { var img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function() { var cv = document.createElement('canvas'); cv.width = 16; cv.height = 16; var ctx = cv.getContext('2d'); ctx.drawImage(img, 0, 0, 16, 16); var data = ctx.getImageData(0, 0, 16, 16).data; var rr = 0, gg = 0, bb = 0, count = 0; for (var p = 0; p < data.length; p += 4) { rr += data[p]; gg += data[p+1]; bb += data[p+2]; count++; } tiles.push({ img: img, r: rr/count, g: gg/count, b: bb/count, rel: record.rel }); tileData.push({ r: rr/count, g: gg/count, b: bb/count }); loaded++; prog.style.width = (loaded / selection.length * 100) + '%'; document.getElementById('tile-count-discogs').textContent = loaded; var thumb = document.createElement('img'); thumb.src = record.base64; thumb.title = record.rel.artist + ' - ' + record.rel.album; thumb.loading = 'lazy'; grid.appendChild(thumb); if (loaded >= selection.length) { onLoadingComplete(); status.textContent = loaded + ' COVERS LOADED FROM CACHE'; updateCacheStatus(); resolve_covers(); } }; img.onerror = function() { loaded++; if (loaded >= selection.length) { onLoadingComplete(); } }; img.src = record.base64; }); return; } status.textContent = 'IDENTIFYING USER...'; var identRes = await fetch('/discogs-identity/', { headers: { 'Authorization': 'Discogs token=' + token } }); if (!identRes.ok) { status.textContent = 'INVALID TOKEN'; return; } var ident = await identRes.json(); var username = ident.username; status.textContent = 'HELLO ' + username.toUpperCase(); localStorage.setItem('discogs_username', username); var allMeta = []; try { var firstR = await fetch('/discogs-collection/' + username + '/collection/folders/0/releases?per_page=100&page=1', { headers: { 'Authorization': 'Discogs token=' + token } }); if (!firstR.ok) { status.textContent = 'ERROR ' + firstR.status; return; } var firstD = await firstR.json(); var totalPages = firstD.pagination.pages; var totalItems = firstD.pagination.items; firstD.releases.forEach(function(r) { allMeta.push(extractMeta(r)); }); status.textContent = 'FETCHING COLLECTION 1/' + totalPages + '...'; for (var pg = 2; pg <= totalPages; pg++) { var pr = await fetch('/discogs-collection/' + username + '/collection/folders/0/releases?per_page=100&page=' + pg, { headers: { 'Authorization': 'Discogs token=' + token } }); if (!pr.ok) continue; var pd = await pr.json(); pd.releases.forEach(function(r) { allMeta.push(extractMeta(r)); }); status.textContent = 'FETCHING COLLECTION ' + pg + '/' + totalPages + '... (' + allMeta.length + ')'; prog.style.width = (pg / totalPages * 30) + '%'; await new Promise(function(res) { setTimeout(res, 600); }); } await dbPutMeta(db, { id: 'collection_' + username, meta: allMeta, ts: Date.now() }); status.textContent = allMeta.length + ' RELEASES CACHED. LOADING COVERS...'; } catch(e) { status.textContent = 'FETCH ERROR: ' + e.message; return; } var allReleases = allMeta.sort(function() { return Math.random() - 0.5; }).slice(0, 300); var total = allReleases.length; tiles = []; tileData = []; var grid = document.getElementById('tile-grid-discogs'); grid.innerHTML = ''; var loaded = 0; allReleases.forEach(function(rel) { var bi = { id: rel.id.replace('d_',''), artists: [{name:rel.artist}], title: rel.album, genres: rel.genres.split(','), styles: rel.styles.split(','), cover_image: rel.cover_image }; rel.artist = rel.artist || ''; rel.album = rel.album || ''; genreCache[rel.id] = { genres: bi.genres || [], styles: bi.styles || [] }; var imgUrl = proxyImg(bi.cover_image); if (!imgUrl || bi.cover_image.indexOf('spacer') !== -1) { loaded++; if (loaded >= total) onLoadingComplete(); return; } var img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function() { var cv = document.createElement('canvas'); cv.width = 16; cv.height = 16; var ctx = cv.getContext('2d'); ctx.drawImage(img, 0, 0, 16, 16); var data = ctx.getImageData(0, 0, 16, 16).data; var rr = 0, gg = 0, bb = 0, count = 0; for (var p = 0; p < data.length; p += 4) { rr += data[p]; gg += data[p+1]; bb += data[p+2]; count++; } tiles.push({ img: img, r: rr/count, g: gg/count, b: bb/count, rel: rel }); tileData.push({ r: rr/count, g: gg/count, b: bb/count }); loaded++; prog.style.width = (30 + loaded / total * 70) + '%'; document.getElementById('tile-count-discogs').textContent = loaded; dbPut(db, { id: rel.id, base64: imgUrl, rel: rel }); var thumb = document.createElement('img'); thumb.src = imgUrl; thumb.title = rel.artist + ' - ' + rel.album; thumb.loading = 'lazy'; grid.appendChild(thumb); if (loaded >= total) { onLoadingComplete(); status.textContent = String(loaded) + ' COVERS LOADED'; updateCacheStatus(); } }; img.onerror = function() { loaded++; if (loaded >= total) { onLoadingComplete(); } }; img.src = imgUrl; }); } // old handler removed function handleTileDrop(e) { e.preventDefault(); document.getElementById('upload-drop').style.borderColor = 'var(--border)'; var items = e.dataTransfer.items; var files = []; function readEntry(entry) { if (entry.isFile) { entry.file(function(f) { if (f.type.startsWith('image/')) files.push(f); if (files.length > 0) processUploadedFiles(files); }); } else if (entry.isDirectory) { var reader = entry.createReader(); reader.readEntries(function(entries) { entries.forEach(function(en) { readEntry(en); }); }); } } for (var i = 0; i < items.length; i++) { var entry = items[i].webkitGetAsEntry(); if (entry) readEntry(entry); } } function handleTileFiles(fileList) { var files = Array.from(fileList).filter(function(f) { return f.type.startsWith('image/'); }); if (!files.length) return; var ps = document.getElementById('tile-preview-section'); if(ps) ps.style.display=''; var pp = document.getElementById('tile-preview-progress'); if(pp) pp.style.width='0%'; var pc = document.getElementById('tile-preview-count'); if(pc) pc.textContent='LOADING...'; processUploadedFiles(files); } function processUploadedFiles(files) { var status = document.getElementById('upload-status2') || document.getElementById('upload-status'); var grid = document.getElementById('tile-grid-upload'); grid.innerHTML = ''; tiles = []; tileData = []; var loaded = 0; var total = Math.min(files.length, 500); status.textContent = 'LOADING 0 OF ' + total + '...'; files.slice(0, total).forEach(function(file) { var url = URL.createObjectURL(file); var img = new Image(); img.onload = function() { var cv = document.createElement('canvas'); cv.width = 16; cv.height = 16; var ctx = cv.getContext('2d'); ctx.drawImage(img, 0, 0, 16, 16); var data = ctx.getImageData(0, 0, 16, 16).data; var rr = 0, gg = 0, bb = 0, count = 0; for (var p = 0; p < data.length; p += 4) { rr += data[p]; gg += data[p+1]; bb += data[p+2]; count++; } var rel = { id: 'u_' + loaded, artist: '', album: file.name.replace(/\.[^.]+$/, ''), genres: '', styles: '', cover_image: url }; tiles.push({ img: img, r: rr/count, g: gg/count, b: bb/count, rel: rel }); tileData.push({ r: rr/count, g: gg/count, b: bb/count }); loaded++; document.getElementById('tile-count-upload').textContent = loaded; status.textContent = loaded + ' TILES LOADED'; var pp = document.getElementById('tile-preview-progress'); if(pp) pp.style.width = (loaded/total*100)+'%'; var pc = document.getElementById('tile-preview-count'); if(pc) pc.textContent = 'LOADING ' + loaded + ' OF ' + total + '...'; var thumb = document.createElement('img'); thumb.src = url; thumb.title = file.name; thumb.loading = 'lazy'; grid.appendChild(thumb); if (loaded >= total) { onLoadingComplete(); } }; img.onerror = function() { loaded++; if (loaded >= total) onLoadingComplete(); }; img.src = url; }); } async function supplementFromDiscogs() { var genre = document.getElementById('discogs-genre').value; var status = document.getElementById('supplement-status'); status.textContent = 'PAGE 1 OF 3...'; var bar = document.getElementById('supplement-bar'); var fill = document.getElementById('supplement-fill'); bar.style.display = ''; fill.style.width = '0%'; var grid = document.getElementById('tile-grid-search'); var added = 0; try { var tok = document.getElementById('discogs-token').value.trim() || localStorage.getItem('discogs_token') || ''; var authHdr = tok ? { 'Authorization': 'Discogs token=' + tok, 'User-Agent': 'Mosaix/1.0' } : { 'User-Agent': 'Mosaix/1.0' }; for (var page = 1; page <= 3; page++) { fill.style.width = (page / 3 * 100) + '%'; var r = await fetch('/discogs-search/?type=release&genre=' + encodeURIComponent(genre) + '&per_page=50&page=' + page); if (!r.ok) { status.textContent = 'ERROR ' + r.status; return; } var d = await r.json(); var results = d.results || []; for (var i = 0; i < results.length; i++) { var rel = results[i]; if (!rel.cover_image || rel.cover_image.indexOf('spacer') !== -1) continue; var imgUrl = proxyImg(rel.cover_image); (function(imgUrl, rel) { var img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function() { var cv = document.createElement('canvas'); cv.width = 16; cv.height = 16; var ctx = cv.getContext('2d'); ctx.drawImage(img, 0, 0, 16, 16); var data = ctx.getImageData(0, 0, 16, 16).data; var rr = 0, gg = 0, bb = 0, count = 0; for (var p = 0; p < data.length; p += 4) { rr += data[p]; gg += data[p+1]; bb += data[p+2]; count++; } var relObj = { id: 'ds_' + rel.id, artist: (rel.title || '').split(' - ')[0], album: (rel.title || '').split(' - ')[1] || rel.title, genres: (rel.genre || []).join(','), styles: (rel.style || []).join(',') }; genreCache[relObj.id] = { genres: rel.genre || [], styles: rel.style || [] }; tiles.push({ img: img, r: rr/count, g: gg/count, b: bb/count, rel: relObj }); tileData.push({ r: rr/count, g: gg/count, b: bb/count }); added++; document.getElementById('tile-count-search').textContent = tiles.length; status.textContent = '+' + added + ' FROM DISCOGS'; var thumb = document.createElement('img'); thumb.src = imgUrl; thumb.title = relObj.artist + ' - ' + relObj.album; thumb.loading = 'lazy'; grid.appendChild(thumb); }; img.src = imgUrl; })(imgUrl, rel); } await new Promise(function(res) { setTimeout(res, 500); }); } bar.style.display = 'none'; } catch(e) { status.textContent = 'ERROR: ' + e.message; bar.style.display = 'none'; } } var GEMINI_API = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent'; var GEMINI_KEY = 'AIzaSyC30mIgi9y43eGV6y-rnFVkOeJJ1mbFbxQ'; async function identifyAndSetGenre(img) { var genreBox = document.getElementById('genre-search-box'); if (genreBox) genreBox.style.display = 'none'; var agd = document.getElementById('auto-genre-display'); if (agd) { agd.style.display = ''; document.getElementById('auto-genre-tags').textContent = 'DETECTING GENRE...'; } try { var cv = document.createElement('canvas'); cv.width = 512; cv.height = 512; var ctx = cv.getContext('2d'); ctx.drawImage(img, 0, 0, 512, 512); var b64 = cv.toDataURL('image/jpeg', 0.8).split(',')[1]; var resp = await fetch(GEMINI_API + '?key=' + GEMINI_KEY, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [ { inlineData: { mimeType: 'image/jpeg', data: b64 } }, { text: 'This is an album cover. Identify the artist and album title. Return ONLY a JSON object with "artist" and "album" fields. Example: {"artist":"Yello","album":"The Race"}. No explanation, just JSON.' } ]}], generationConfig: { temperature: 0.1, maxOutputTokens: 100 } }) }); var data = await resp.json(); var text = (data.candidates && data.candidates[0] && data.candidates[0].content && data.candidates[0].content.parts && data.candidates[0].content.parts[0].text) || ''; text = text.replace(/```json|```/g, '').trim(); var parsed = JSON.parse(text); var query = (parsed.artist || '') + ' ' + (parsed.album || ''); if (query.trim()) { var si = document.getElementById('genre-search-input'); if (si) si.value = query.trim(); await searchReleaseGenre(); } } catch(e) { console.error('Genre detection failed:', e.message); if (agd) agd.style.display = 'none'; } } function startScreensaverFromSource() { if (!sourceImg) { showSourceStatus('UPLOAD A SOURCE IMAGE FIRST'); return; } if (!tiles.length) { var status = document.getElementById('connect-status'); if (status) status.textContent = 'WAITING FOR COVERS TO LOAD...'; var check = setInterval(function() { if (tiles.length > 0) { clearInterval(check); _launchScreensaver(); } }, 500); return; } _launchScreensaver(); } function _launchScreensaver() { document.querySelectorAll('.screen').forEach(function(s){ s.classList.remove('active'); }); var ms = document.getElementById('mosaic-screen'); ms.classList.add('active'); var cv = document.getElementById('mosaic-canvas'); cv.width = window.innerWidth; cv.height = window.innerHeight; generateMosaic(); setTimeout(function() { startScreensaver(); }, 800); } var _regenTimer = null; function debouncedRegenerate() { clearTimeout(_regenTimer); _regenTimer = setTimeout(function() { applyMosaicSettings(); }, 300); } function toggleMosaicSettings() { var s = document.getElementById('mosaic-settings'); s.classList.toggle('open'); } // Close settings when clicking outside document.addEventListener('click', function(e) { var s = document.getElementById('mosaic-settings'); var btn = document.querySelector('[onclick="toggleMosaicSettings()"]'); if (!s || !s.classList.contains('open')) return; if (s.contains(e.target) || (btn && btn.contains(e.target))) return; s.classList.remove('open'); }); function applyMosaicSettings() { gridSize = parseInt(document.getElementById('ms-grid').value); blendAmount = parseInt(document.getElementById('ms-blend').value) / 100; var bi = document.getElementById('mosaic-blend-info'); if(bi) bi.textContent = 'blend ' + Math.round(blendAmount*100) + '% · genre ' + document.getElementById('ms-genre').value + '%'; document.getElementById('grid-val').textContent = gridSize; document.getElementById('blend-val').textContent = Math.round(blendAmount * 100) + '%'; document.getElementById('genre-weight').value = document.getElementById('ms-genre').value; document.getElementById('unique-tiles').checked = document.getElementById('ms-unique').checked; var cv = document.getElementById('mosaic-canvas'); cv.width = window.innerWidth; cv.height = window.innerHeight; generateMosaic(); } var _audioCtx = null; var _audioAnalyser = null; var _audioStream = null; var _audioRunning = false; var _audioData = null; var _audioRafId = null; async function toggleAudioInput() { var btn = document.getElementById('audio-btn'); if (_audioRunning) { stopAudioInput(); btn.textContent = '\u266a AUDIO'; btn.classList.remove('audio-active'); return; } try { var stream = await navigator.mediaDevices.getDisplayMedia({ video: { width: 1, height: 1 }, audio: { echoCancellation: false, noiseSuppression: false, sampleRate: 44100 } }); var audioTracks = stream.getAudioTracks(); if (!audioTracks.length) { showMosaicStatus('NO AUDIO TRACK — CHECK SHARE TAB AUDIO IN DIALOG'); stream.getTracks().forEach(function(t){ t.stop(); }); return; } stream.getVideoTracks().forEach(function(t){ t.stop(); }); _audioStream = stream; _audioCtx = new AudioContext(); _audioAnalyser = _audioCtx.createAnalyser(); _audioAnalyser.fftSize = 256; _audioData = new Uint8Array(_audioAnalyser.frequencyBinCount); var source = _audioCtx.createMediaStreamSource(stream); source.connect(_audioAnalyser); _audioRunning = true; btn.textContent = '\u266a LIVE'; btn.classList.add('audio-active'); runAudioLoop(); stream.getAudioTracks()[0].onended = function() { stopAudioInput(); btn.textContent = '\u266a AUDIO'; btn.classList.remove('audio-active'); }; } catch(e) { console.log('Audio error:', e.message); } } function stopAudioInput() { _audioRunning = false; if (_audioRafId) cancelAnimationFrame(_audioRafId); if (_audioStream) _audioStream.getTracks().forEach(function(t){ t.stop(); }); if (_audioCtx) _audioCtx.close(); _audioCtx = null; _audioAnalyser = null; _audioStream = null; } function runAudioLoop() { if (!_audioRunning) return; _audioAnalyser.getByteFrequencyData(_audioData); var bins = _audioData.length; var bassEnd = Math.floor(bins * 0.1); var midEnd = Math.floor(bins * 0.5); var bass = 0, mid = 0, high = 0; for (var i = 0; i < bassEnd; i++) bass += _audioData[i]; for (var i = bassEnd; i < midEnd; i++) mid += _audioData[i]; for (var i = midEnd; i < bins; i++) high += _audioData[i]; bass = bass / bassEnd / 255; mid = mid / (midEnd - bassEnd) / 255; high = high / (bins - midEnd) / 255; var volume = (bass + mid + high) / 3; var meter = document.getElementById('audio-meter-fill'); if (meter) meter.style.width = Math.round(volume * 100) + '%'; if (_ssRunning) { blendStep = 0.02 + bass * 0.06; if (bass > 0.6 && Math.random() < 0.1) { gridStep = Math.max(1, Math.round(bass * 4)); } } else { var newBlend = Math.round(mid * 60); var blendSlider = document.getElementById('ms-blend'); if (blendSlider) { blendSlider.value = newBlend; document.getElementById('ms-blend-val').textContent = newBlend + '%'; } blendAmount = newBlend / 100; } _audioRafId = requestAnimationFrame(runAudioLoop); } function showSourceStatus(msg) { var el = document.getElementById('source-error'); if (!el) return; el.textContent = msg; el.classList.add('show'); setTimeout(function(){ el.classList.remove('show'); el.textContent = ''; }, 3000); } function showMosaicStatus(msg) { var el = document.getElementById('mosaic-stats'); if (el) { var old = el.textContent; el.textContent = msg; setTimeout(function(){ el.textContent = old; }, 3000); } } var _navHideTimer = null; var _mosiacTrigger = document.getElementById('mosaic-trigger'); if (_mosiacTrigger) { _mosiacTrigger.addEventListener('mouseenter', function() { clearTimeout(_navHideTimer); var nav = document.getElementById('mosaic-nav'); if (nav) nav.classList.add('visible'); }); } var _mosaicNav = document.getElementById('mosaic-nav'); if (_mosaicNav) { _mosaicNav.addEventListener('mouseenter', function() { clearTimeout(_navHideTimer); var nav = document.getElementById('mosaic-nav'); if (nav) nav.classList.add('visible'); }); _mosaicNav.addEventListener('mouseleave', function() { _navHideTimer = setTimeout(function() { var nav = document.getElementById('mosaic-nav'); if (nav) nav.classList.remove('visible'); }, 2000); }); } document.addEventListener('mousemove', function(e) { var ms = document.getElementById('mosaic-screen'); if (!ms || !ms.classList.contains('active')) return; var nav = document.getElementById('mosaic-nav'); if (!nav) return; if (e.clientY < 80) { clearTimeout(_navHideTimer); nav.classList.add('visible'); } else { clearTimeout(_navHideTimer); _navHideTimer = setTimeout(function() { nav.classList.remove('visible'); }, 2000); } }); var _ownFiles = []; function showUploadedThumbs() { var container = document.getElementById('own-thumbs'); if (!container) return; container.innerHTML = ''; _ownFiles = Array.from(document.getElementById('tile-file-input').files).filter(function(f){ return f.type.startsWith('image/'); }); if (!_ownFiles.length) return; var section = document.getElementById('own-thumbs-section'); if (section) section.style.display = ''; _ownFiles.forEach(function(file, i) { var url = URL.createObjectURL(file); var wrapper = document.createElement('div'); wrapper.style.cssText = 'position:relative;cursor:pointer;'; var img = document.createElement('img'); img.src = url; img.style.cssText = 'width:80px;height:80px;object-fit:cover;border-radius:2px;border:2px solid var(--border);transition:border-color 0.15s;'; img.title = file.name; img.onmouseover = function(){ if(img.style.borderColor !== 'var(--gold)') img.style.borderColor = 'rgba(223,154,36,0.5)'; }; img.onmouseout = function(){ if(img.style.borderColor !== 'var(--gold)') img.style.borderColor = 'var(--border)'; }; img.onclick = function() { document.querySelectorAll('#own-thumbs img').forEach(function(im){ im.style.borderColor = 'var(--border)'; }); img.style.borderColor = 'var(--gold)'; var statusEl = document.getElementById('own-source-status'); if (statusEl) statusEl.textContent = 'IDENTIFYING GENRE...'; handleFile(file); setTimeout(function() { goToScreen('screen-source'); }, 400); }; wrapper.appendChild(img); container.appendChild(wrapper); }); } function handleDemoDrop(e) { e.preventDefault(); document.getElementById('demo-drop-zone').style.borderColor = 'var(--border)'; var file = e.dataTransfer.files[0]; if (file) handleDemoFile(file); } async function handleDemoFile(file) { if (!file || !file.type.startsWith('image/')) return; var status = document.getElementById('demo-status'); var prog = document.getElementById('demo-progress'); var thumb = document.getElementById('demo-thumb'); var reader = new FileReader(); reader.onload = async function(e) { thumb.src = e.target.result; thumb.style.display = 'block'; document.getElementById('demo-drop-zone').style.display = 'none'; var dcb = document.getElementById('demo-change-btn'); if(dcb) dcb.style.display = 'block'; sourceImg = new Image(); await new Promise(function(res){ sourceImg.onload = res; sourceImg.onerror = res; sourceImg.src = e.target.result; }); status.textContent = 'IDENTIFYING GENRE...'; prog.style.width = '10%'; try { var b64 = e.target.result.split(',')[1]; var resp = await fetch(GEMINI_API + '?key=' + GEMINI_KEY, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [ { inlineData: { mimeType: 'image/jpeg', data: b64 } }, { text: 'This is an album cover. Identify the artist and album title. Return ONLY JSON: {"artist":"...","album":"..."}' } ]}], generationConfig: { temperature: 0.1, maxOutputTokens: 100 } }) }); var data = await resp.json(); var text = (data.candidates && data.candidates[0].content.parts[0].text || '').replace(/```json|```/g, '').trim(); var parsed = JSON.parse(text); var query = (parsed.artist || '') + ' ' + (parsed.album || ''); status.textContent = 'SEARCHING DISCOGS FOR ' + query.toUpperCase() + '...'; prog.style.width = '20%'; var sr = await fetch('/discogs-search/?q=' + encodeURIComponent(query) + '&type=release&per_page=1'); var sd = await sr.json(); var item = (sd.results || [])[0]; if (item) { sourceGenres = item.genre || []; sourceStyles = item.style || []; var tags = sourceGenres.concat(sourceStyles); document.getElementById('demo-genre-tags').textContent = tags.join(' · '); document.getElementById('demo-genre-display').style.display = ''; status.textContent = 'LOADING ' + tags[0] + ' COVERS...'; await fetchDemoTiles(tags[0] || 'Electronic', prog, status); } else { status.textContent = 'NOT FOUND — LOADING ELECTRONIC COVERS'; await fetchDemoTiles('Electronic', prog, status); } } catch(ex) { status.textContent = 'LOADING COVERS...'; await fetchDemoTiles('Electronic', prog, status); } }; reader.readAsDataURL(file); } async function fetchDemoTiles(genre, prog, status) { tiles = []; tileData = []; var loaded = 0; var allReleases = []; for (var page = 1; page <= 3; page++) { prog.style.width = (30 + page * 15) + '%'; status.textContent = 'FETCHING PAGE ' + page + '/3 — ' + genre.toUpperCase(); try { var r = await fetch('/discogs-search/?type=release&genre=' + encodeURIComponent(genre) + '&per_page=100&page=' + page); var d = await r.json(); allReleases = allReleases.concat(d.results || []); await new Promise(function(res){ setTimeout(res, 500); }); } catch(e) { break; } } allReleases = allReleases.filter(function(r){ return r.cover_image && r.cover_image.indexOf('spacer') === -1; }); allReleases = allReleases.sort(function(){ return Math.random()-0.5; }).slice(0, 300); var total = allReleases.length; status.textContent = 'LOADING ' + total + ' COVERS...'; allReleases.forEach(function(rel) { var imgUrl = proxyImg(rel.cover_image); var relObj = { id: 'demo_' + rel.id, artist: (rel.title||'').split(' - ')[0], album: (rel.title||'').split(' - ')[1]||rel.title, genres: (rel.genre||[]).join(','), styles: (rel.style||[]).join(',') }; genreCache[relObj.id] = { genres: rel.genre||[], styles: rel.style||[] }; var img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function() { var cv2 = document.createElement('canvas'); cv2.width=16; cv2.height=16; var ctx2 = cv2.getContext('2d'); ctx2.drawImage(img,0,0,16,16); var data = ctx2.getImageData(0,0,16,16).data; var rr=0,gg=0,bb=0,cnt=0; for(var p=0;p= total) { status.textContent = loaded + ' COVERS READY — GENERATING MOSAIC...'; _returnScreen = 'screen-source'; _returnScreen = 'screen-demo'; document.getElementById('start-btn').disabled = false; setTimeout(function(){ startMosaic(); }, 100); } }; img.onerror = function(){ loaded++; if(loaded>=total && tiles.length > 0){ goToScreen('screen-source'); setTimeout(function(){ startMosaic(); }, 300); }}; img.src = imgUrl; }); } async function logoutDiscogs() { localStorage.removeItem('discogs_token'); localStorage.removeItem('discogs_username'); var db = await openMosaixDB(); await dbClear(db); tiles = []; tileData = []; sourceImg = null; mosaicMap = []; var tf = document.getElementById('discogs-token'); if (tf) tf.value = ''; var cs = document.getElementById('connect-status'); if (cs) cs.textContent = ''; var cp = document.getElementById('connect-progress'); if (cp) cp.style.width = '0%'; goToScreen('screen-connect'); } function showTilePreview() { var section = document.getElementById('tile-preview-section'); var thumbs = document.getElementById('tile-preview-thumbs'); var count = document.getElementById('tile-preview-count'); if (!section || !thumbs || !tiles.length) return; section.style.display = ''; thumbs.innerHTML = ''; var sample = tiles; var show = sample.length; for (var i = 0; i < show; i++) { var img = document.createElement('img'); img.src = sample[i].rel.cover_image || sample[i].img.src; img.style.cssText = 'width:44px;height:44px;object-fit:cover;border-radius:2px;opacity:0.85;cursor:pointer;border:2px solid transparent;'; img.title = tiles[i].rel.artist + ' - ' + tiles[i].rel.album; (function(tile, imgEl, idx) { imgEl.onclick = function() { document.querySelectorAll('#tile-preview-thumbs img').forEach(function(im){ im.style.borderColor='transparent'; }); imgEl.style.borderColor = 'var(--gold)'; sourceImg = tile.img; var st = document.getElementById('source-thumb'); if (st) { st.src = tile.rel.cover_image || tile.img.src; st.style.display='block'; } var sdz = document.getElementById('source-drop-zone'); if(sdz) sdz.style.display='none'; var scb = document.getElementById('source-change-btn'); if(scb) scb.style.display='block'; var scb = document.getElementById('source-change-btn'); if(scb) scb.style.display='block'; document.getElementById('start-btn').disabled = false; sourceGenres = (genreCache[tile.rel.id]||{}).genres||[]; sourceStyles = (genreCache[tile.rel.id]||{}).styles||[]; if (sourceGenres.length || sourceStyles.length) { var tags = sourceGenres.concat(sourceStyles); document.getElementById('auto-genre-tags').textContent = tags.join(' · '); document.getElementById('auto-genre-display').style.display = ''; } }; })(sample[i], img, i); thumbs.appendChild(img); } if (count) count.textContent = tiles.length + ' TILES — CLICK ONE TO USE AS SOURCE IMAGE'; var dcs = document.getElementById('download-covers-section'); if(dcs && _allMeta.length) dcs.style.display=''; if (tiles.length > 0 && tiles[0].rel.id && tiles[0].rel.id.indexOf('u_') === 0) { var gsb = document.getElementById('genre-search-box'); if(gsb) gsb.style.display='none'; } var pp = document.getElementById('tile-preview-progress'); if(pp) pp.style.width = '100%'; var label = document.querySelector('#tile-preview-section > div:first-child'); if(label) label.textContent = 'YOUR TILES'; } function showOwnImageThumbs() { var files = Array.from(document.getElementById('tile-file-input').files).filter(function(f){ return f.type.startsWith('image/'); }); if (!files.length) return; var section = document.getElementById('own-tiles-section'); var thumbs = document.getElementById('own-tiles-thumbs'); var status = document.getElementById('own-tiles-status'); if (section) section.style.display = ''; if (thumbs) thumbs.innerHTML = ''; files.forEach(function(file) { var url = URL.createObjectURL(file); var img = document.createElement('img'); img.src = url; img.style.cssText = 'width:72px;height:72px;object-fit:cover;border-radius:2px;border:2px solid transparent;cursor:pointer;transition:border-color 0.15s;'; img.onmouseover = function(){ img.style.borderColor='rgba(223,154,36,0.5)'; }; img.onmouseout = function(){ if(!img._selected) img.style.borderColor='transparent'; }; img.onclick = function() { document.querySelectorAll('#own-tiles-thumbs img').forEach(function(im){ im.style.borderColor='transparent'; im._selected=false; }); img.style.borderColor = 'var(--gold)'; img._selected = true; handleFile(file); var preview = document.getElementById('own-source-preview'); if (preview) preview.src = url; var src_section = document.getElementById('own-source-section'); if (src_section) src_section.style.display = ''; if (status) status.textContent = file.name.toUpperCase(); }; if (thumbs) thumbs.appendChild(img); }); if (status) status.textContent = files.length + ' IMAGES LOADED'; } function startOwnMosaic() { if (!sourceImg || !tiles.length) { return; } _returnScreen = 'screen-own-images'; startMosaic(); } function startOwnScreensaver() { if (!sourceImg || !tiles.length) { return; } goToScreen('mosaic-screen'); setTimeout(function() { startScreensaver(); }, 100); } function resetSourceImage() { sourceImg = null; var st = document.getElementById('source-thumb'); if (st) { st.src=''; st.style.display='none'; } var sdz = document.getElementById('source-drop-zone'); if (sdz) sdz.style.display=''; var scb = document.getElementById('source-change-btn'); if (scb) scb.style.display='none'; var agd = document.getElementById('auto-genre-display'); if (agd) agd.style.display='none'; var gsb = document.getElementById('genre-search-box'); if (gsb) gsb.style.display='none'; var btn = document.getElementById('start-btn'); if (btn) btn.disabled = true; document.querySelectorAll('#tile-preview-thumbs img').forEach(function(im){ im.style.borderColor='transparent'; }); } var _allMeta = []; function resampleTilesByGenre() { if (!_allMeta.length || (!sourceGenres.length && !sourceStyles.length)) return; var scored = _allMeta.filter(function(rel) { return rel.cover_image && rel.cover_image.indexOf('spacer') === -1; }).map(function(rel) { var g = (rel.genres || '').split(',').map(function(s){return s.trim();}).filter(Boolean); var st = (rel.styles || '').split(',').map(function(s){return s.trim();}).filter(Boolean); var score = 0; sourceGenres.forEach(function(sg) { if (g.indexOf(sg) !== -1) score += 3; if (st.indexOf(sg) !== -1) score += 1; }); sourceStyles.forEach(function(ss) { if (st.indexOf(ss) !== -1) score += 2; if (g.indexOf(ss) !== -1) score += 1; }); return { rel: rel, score: score }; }); scored.sort(function(a, b) { return b.score - a.score || Math.random() - 0.5; }); var top = scored.slice(0, 300).map(function(x) { return x.rel; }); var genreMatches = scored.filter(function(x) { return x.score > 0; }).length; var status = document.getElementById('connect-status'); if (status) status.textContent = genreMatches + ' GENRE MATCHES IN COLLECTION'; tiles = []; tileData = []; var loaded = 0; var prog = document.getElementById('connect-progress'); if (prog) prog.style.width = '50%'; top.forEach(function(rel) { var imgUrl = proxyImg(rel.cover_image); if (!imgUrl) { loaded++; return; } var img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function() { var cv = document.createElement('canvas'); cv.width=16; cv.height=16; var ctx = cv.getContext('2d'); ctx.drawImage(img,0,0,16,16); var data = ctx.getImageData(0,0,16,16).data; var rr=0,gg=0,bb=0,cnt=0; for(var p=0;p= top.length) { onLoadingComplete(); showTilePreview(); } }; img.onerror = function(){ loaded++; }; img.src = imgUrl; }); } function resetDemoImage() { var dt = document.getElementById('demo-thumb'); if(dt){ dt.src=''; dt.style.display='none'; } var dz = document.getElementById('demo-drop-zone'); if(dz) dz.style.display=''; var dcb = document.getElementById('demo-change-btn'); if(dcb) dcb.style.display='none'; var ds = document.getElementById('demo-status'); if(ds) ds.textContent=''; var dp = document.getElementById('demo-progress'); if(dp) dp.style.width='0%'; var dg = document.getElementById('demo-genre-display'); if(dg) dg.style.display='none'; var di = document.getElementById('demo-file-input'); if(di) di.value=''; tiles = []; tileData = []; sourceImg = null; } function toggleDiscogsSection() { var form = document.getElementById('discogs-form'); var arrow = document.getElementById('discogs-arrow'); var open = form && form.style.display !== 'none'; if (form) form.style.display = open ? 'none' : ''; if (arrow) arrow.innerHTML = open ? '▶' : '▼'; } function handleDiscogsHeaderClick() { var username = localStorage.getItem('discogs_username'); if (username && tiles.length > 0) { _returnScreen = 'screen-source'; goToScreen('screen-source'); } else { toggleDiscogsSection(); } } function collapseDiscogsForm() { var form = document.getElementById('discogs-form'); var arrow = document.getElementById('discogs-arrow'); if (form) form.style.display = 'none'; if (arrow) arrow.innerHTML = '▶'; } function showDiscogsConnected(username, count) { var info = document.getElementById('discogs-connected-info'); var name = document.getElementById('discogs-connected-name'); var form = document.getElementById('discogs-form'); if (info) info.style.display = ''; if (name) name.textContent = username.toUpperCase() + ' — ' + count + ' RELEASES'; } async function downloadAllCovers() { if (!_allMeta.length) { alert('Load your collection first.'); return; } var status = document.getElementById('download-status') || document.getElementById('connect-status'); if (status) status.textContent = 'PREPARING...'; var lines = ['id,title,artist,genres,styles,cover_url']; _allMeta.forEach(function(rel) { lines.push([rel.id, JSON.stringify(rel.title||''), JSON.stringify(rel.artist||''), (rel.genres||'').replace(/,/g,';'), (rel.styles||'').replace(/,/g,';'), rel.cover_image||''].join(',')); }); var blob = new Blob([lines.join('\n')], {type:'text/csv'}); var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'discogs_collection_' + new Date().toISOString().slice(0,10) + '.csv'; a.click(); if (status) status.textContent = _allMeta.length + ' RELEASES EXPORTED'; } async function loadAllCoversFromCollection() { if (!_allMeta.length) return; var btn = document.getElementById('load-all-btn'); var status = document.getElementById('download-status'); var progBar = document.getElementById('load-all-progress-bar'); var prog = document.getElementById('load-all-progress'); var idle = document.getElementById('load-all-idle'); if(idle) idle.style.display='none'; var loading = document.getElementById('load-all-loading'); if(loading) loading.style.display=''; tiles = []; tileData = []; var total = _allMeta.filter(function(r){ return r.cover_image && r.cover_image.indexOf('spacer') === -1; }).length; var loaded = 0; var batchSize = 20; var allValid = _allMeta.filter(function(r){ return r.cover_image && r.cover_image.indexOf('spacer') === -1; }); function loadBatch(start) { var batch = allValid.slice(start, start + batchSize); if (!batch.length) { if (status) status.textContent = tiles.length + ' COVERS LOADED AS TILES'; if (btn) { btn.disabled = false; btn.textContent = '✓ ALL COVERS LOADED'; } onLoadingComplete(); showTilePreview(); return; } var batchDone = 0; batch.forEach(function(rel) { var imgUrl = proxyImg(rel.cover_image); var img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function() { var cv2 = document.createElement('canvas'); cv2.width=16; cv2.height=16; var ctx2 = cv2.getContext('2d'); ctx2.drawImage(img,0,0,16,16); var data = ctx2.getImageData(0,0,16,16).data; var rr=0,gg=0,bb=0,cnt=0; for(var p=0;p= batch.length) setTimeout(function(){ loadBatch(start + batchSize); }, 50); }; img.onerror = function(){ loaded++; batchDone++; if(batchDone>=batch.length) setTimeout(function(){ loadBatch(start+batchSize); }, 50); }; img.src = imgUrl; }); } loadBatch(0); } async function refreshDiscogsCovers() { var username = localStorage.getItem('discogs_username'); var token = localStorage.getItem('discogs_token'); if (!username || !token) return; var done = document.getElementById('load-all-done'); var doneText = document.getElementById('load-all-done-text'); if (doneText) doneText.textContent = 'REFRESHING...'; var db = await openMosaixDB(); await dbClear(db); _allMeta = []; await connectDiscogs(); } var _ssPreset = 'balanced'; var _ssPresets = { ambient: { gridStep: 1, blendStep: 0.008, frameDelay: 2000, pauseFrames: 120, loopMax: 5, spotSpeed: 0.003, cascadeSpeed: 1 }, balanced: { gridStep: 2, blendStep: 0.02, frameDelay: 1200, pauseFrames: 60, loopMax: 3, spotSpeed: 0.006, cascadeSpeed: 2 }, energetic: { gridStep: 4, blendStep: 0.04, frameDelay: 400, pauseFrames: 20, loopMax: 2, spotSpeed: 0.012, cascadeSpeed: 4 } }; function setScreensaverPreset(name) { _ssPreset = name; ['ambient','balanced','energetic'].forEach(function(p) { var btn = document.getElementById('preset-' + p); if (!btn) return; if (p === name) { btn.style.background = 'var(--gold)'; btn.style.borderColor = 'var(--gold)'; btn.style.color = '#000'; } else { btn.style.background = 'none'; btn.style.borderColor = 'rgba(255,255,255,0.15)'; btn.style.color = 'rgba(255,255,255,0.5)'; } }); localStorage.setItem('ss_preset', name); if (_ssRunning) { var p = _ssPresets[name]; _ssGridStep = p.gridStep; _ssBlendStep = p.blendStep; } } function getSsPreset() { return _ssPresets[_ssPreset] || _ssPresets.balanced; } var _savedPreset = localStorage.getItem('ss_preset'); if(_savedPreset) setTimeout(function(){ setScreensaverPreset(_savedPreset); }, 100); if('serviceWorker' in navigator){navigator.serviceWorker.register('/sw.js');} // SCREEN NAVIGATION function goToScreen(id) { document.querySelectorAll('.screen').forEach(function(s){ s.classList.remove('active'); }); var el = document.getElementById(id); if (el) el.classList.add('active'); } function exitMosaic() { var pp = document.getElementById('pdf-popup'); if(pp) pp.style.display = 'none'; stopScreensaver(); var ms = document.getElementById('mosaic-screen'); ms.classList.remove('active'); ms.style.zIndex = ''; var target = _returnScreen || 'screen-source'; document.querySelectorAll('.screen').forEach(function(s){ s.classList.remove('active'); }); var targetEl = document.getElementById(target); if (targetEl) targetEl.classList.add('active'); } function startMosaic() { if (!sourceImg || !tiles.length) return; var ms = document.getElementById('mosaic-screen'); ms.style.zIndex = '99999'; ms.classList.add('active'); document.querySelectorAll('.screen').forEach(function(s){ s.classList.remove('active'); s.style.zIndex = ''; }); ms.style.zIndex = '99999'; var cv = document.getElementById('mosaic-canvas'); cv.width = window.innerWidth; cv.height = window.innerHeight; generateMosaic(); var nav = document.getElementById('mosaic-nav'); if (nav && !nav._hoverSetup) { nav._hoverSetup = true; nav.addEventListener('mouseenter', function() { clearTimeout(_navHideTimer); nav.classList.add('visible'); }); nav.addEventListener('mouseleave', function() { _navHideTimer = setTimeout(function() { nav.classList.remove('visible'); }, 2000); }); } } // Override handleFile to update screen UI var _origHandleFile = handleFile; handleFile = function(file) { console.log('handleFile called', file && file.name); if (!file || !file.type.startsWith('image/')) return; var reader = new FileReader(); reader.onload = function(e) { var img = new Image(); img.onload = function() { sourceImg = img; document.getElementById('source-img').src = e.target.result; var thumb = document.getElementById('source-thumb'); thumb.src = e.target.result; thumb.style.display = 'block'; document.getElementById('source-drop-zone').style.display = 'none'; document.getElementById('genre-search-box').style.display = ''; var _sb = document.getElementById('start-btn'); if(_sb) _sb.disabled = tiles.length === 0; extractPalette(img); if (document.getElementById('source-preview')) drawGridOverlay(); var scb = document.getElementById('source-change-btn'); if(scb) scb.style.display='block'; var isUploadTiles = tiles.length > 0 && tiles[0].rel.id && tiles[0].rel.id.indexOf('u_') === 0; if (!isUploadTiles) { var agd2 = document.getElementById('auto-genre-display'); var agt2 = document.getElementById('auto-genre-tags'); if (agd2) agd2.style.display = ''; if (agt2) agt2.textContent = 'DETECTING GENRE...'; var cv2 = document.createElement('canvas'); cv2.width=512; cv2.height=512; var ctx2 = cv2.getContext('2d'); ctx2.drawImage(img,0,0,512,512); var b64 = cv2.toDataURL('image/jpeg',0.8).split(',')[1]; fetch(GEMINI_API + '?key=' + GEMINI_KEY, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({contents:[{parts:[ {inlineData:{mimeType:'image/jpeg',data:b64}}, {text:'This is an album cover. Identify the artist and album title. Return ONLY JSON: {"artist":"...","album":"..."}'} ]}], generationConfig:{temperature:0.1,maxOutputTokens:100}}) }).then(function(r){ return r.json(); }).then(function(data){ console.log('Gemini response:', JSON.stringify(data).slice(0,200)); var text = (data.candidates&&data.candidates[0]&&data.candidates[0].content&&data.candidates[0].content.parts&&data.candidates[0].content.parts[0].text)||''; console.log('Gemini text:', text); text = text.replace(/```json|```/g,'').trim(); var parsed = JSON.parse(text); var query = ((parsed.artist||'')+' '+(parsed.album||'')).trim(); console.log('Query:', query); if (query) { var si = document.getElementById('genre-search-input'); if(si) si.value = query; autoDetectGenreFromDiscogs(img).then(function(){ console.log('genre detection done'); }); } }).catch(function(e){ console.error('Gemini error:',e.message, e); if(agd2) agd2.style.display='none'; }); } }; img.src = e.target.result; }; reader.readAsDataURL(file); }; function handleSourceDrop(e) { e.preventDefault(); var file = e.dataTransfer.files[0]; if (file) handleFile(file); } // CONNECT DISCOGS FLOW async function connectDiscogs() { var token = document.getElementById('discogs-token').value.trim(); var status = document.getElementById('connect-status'); var prog = document.getElementById('connect-progress'); if (!token) { status.textContent = 'TOKEN REQUIRED'; return; } status.textContent = 'IDENTIFYING USER...'; prog.style.width = '5%'; var identRes = await fetch('/discogs-identity/', { headers: { 'Authorization': 'Discogs token=' + token } }); if (!identRes.ok) { status.textContent = 'INVALID TOKEN'; return; } var ident = await identRes.json(); var username = ident.username; localStorage.setItem('discogs_username', username); status.textContent = 'HELLO ' + username.toUpperCase() + ' — CHECKING CACHE...'; var db = await openMosaixDB(); var metaRecord = await dbGetMeta(db, 'collection_' + username); if (metaRecord && metaRecord.meta && metaRecord.meta.length > 0) { status.textContent = metaRecord.meta.length + ' RELEASES IN CACHE'; prog.style.width = '100%'; await loadCoversFromMeta(metaRecord.meta, db); goToScreen('screen-source'); return; } var fetchStart = Date.now(); var fetchStart = Date.now(); var allMeta = []; var firstR = await fetch('/discogs-collection/' + username + '/collection/folders/0/releases?per_page=100&page=1', { headers: { 'Authorization': 'Discogs token=' + token } }); if (!firstR.ok) { status.textContent = 'ERROR ' + firstR.status; return; } var firstD = await firstR.json(); var totalPages = firstD.pagination.pages; var totalItems = firstD.pagination.items; firstD.releases.forEach(function(r) { allMeta.push(extractMeta(r)); }); for (var pg = 2; pg <= totalPages; pg++) { prog.style.width = (pg / totalPages * 90) + '%'; var elapsed = Math.round((Date.now() - fetchStart) / 1000); var pct = Math.round(pg / totalPages * 100); status.textContent = pct + '% — ' + allMeta.length + ' OF ' + totalItems + ' RELEASES (' + elapsed + 's)'; var pr = await fetch('/discogs-collection/' + username + '/collection/folders/0/releases?per_page=100&page=' + pg, { headers: { 'Authorization': 'Discogs token=' + token } }); if (!pr.ok) continue; var pd = await pr.json(); pd.releases.forEach(function(r) { allMeta.push(extractMeta(r)); }); await new Promise(function(res) { setTimeout(res, 600); }); } await dbPutMeta(db, { id: 'collection_' + username, meta: allMeta, ts: Date.now() }); prog.style.width = '100%'; var totalTime = Math.round((Date.now() - fetchStart) / 1000); status.textContent = allMeta.length + ' RELEASES CACHED IN ' + totalTime + 's'; var uname = localStorage.getItem('discogs_username') || ''; showDiscogsConnected(uname, allMeta.length); collapseDiscogsForm(); await loadCoversFromMeta(allMeta, db); var dcs = document.getElementById('download-covers-section'); if(dcs) dcs.style.display=''; _returnScreen = 'screen-source'; goToScreen('screen-source'); } async function loadCoversFromMeta(allMeta, db) { _allMeta = allMeta; var selection = allMeta.sort(function(){ return Math.random()-0.5; }).slice(0,300); tiles = []; tileData = []; var loaded = 0; var prog = document.getElementById('connect-progress'); return new Promise(function(resolve_covers) { selection.forEach(function(rel) { genreCache[rel.id] = { genres: (rel.genres||'').split(',').map(function(s){return s.trim();}).filter(Boolean), styles: (rel.styles||'').split(',').map(function(s){return s.trim();}).filter(Boolean) }; var imgUrl = proxyImg(rel.cover_image); if (!imgUrl || rel.cover_image.indexOf('spacer') !== -1) { loaded++; return; } var img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function() { var cv = document.createElement('canvas'); cv.width=16; cv.height=16; var ctx=cv.getContext('2d'); ctx.drawImage(img,0,0,16,16); var data=ctx.getImageData(0,0,16,16).data; var rr=0,gg=0,bb=0,count=0; for(var p=0;p= selection.length) { onLoadingComplete(); resolve_covers(); } }; img.onerror = function(){ loaded++; if (loaded >= selection.length) { onLoadingComplete(); resolve_covers(); } }; img.src = imgUrl; }); if (selection.length === 0) { onLoadingComplete(); resolve_covers(); } }); } // KEYBOARD CONTROLS document.addEventListener('keydown', function(e) { if (e.key === 's' || e.key === 'S') { if (document.getElementById('mosaic-screen') && document.getElementById('mosaic-screen').style.display !== 'none') { if (typeof _ssRunning !== 'undefined') { if (_ssRunning) { pauseScreensaver(); } else if (_ssPaused) { resumeScreensaver(); } else { startScreensaver(); } } return; } } var ms = document.getElementById('mosaic-screen'); var mosaicActive = ms && ms.classList.contains('active'); if (e.key === 'Escape') { if (mosaicActive) { stopScreensaver(); exitMosaic(); return; } var activeScreen = document.querySelector('.screen.active'); if (activeScreen && activeScreen.id !== 'screen-connect') { goToScreen('screen-connect'); } return; } if (!mosaicActive) return; if (e.key === ' ') { e.preventDefault(); if (_ssRunning) { pauseScreensaver(); } else if (_ssPaused) { resumeScreensaver(); } else if (tiles.length && sourceImg) { startScreensaver(); } } if (e.key === 'f' || e.key === 'F') { if (!document.fullscreenElement) { ms.requestFullscreen(); } else { document.exitFullscreen(); } } }); // Hide nav in fullscreen document.addEventListener('fullscreenchange', function() { var nav = document.getElementById('mosaic-nav'); var hud = document.getElementById('mosaic-hud'); if (document.fullscreenElement) { nav.style.display = 'none'; hud.style.display = 'none'; } else { nav.style.display = 'flex'; hud.style.display = 'block'; } }); // INIT document.addEventListener('DOMContentLoaded', async function() { var t = localStorage.getItem('discogs_token'); if (t) { var tf = document.getElementById('discogs-token'); if(tf) tf.value = t; } var username = localStorage.getItem('discogs_username'); if (t && username) { var cs = document.getElementById('connect-status'); var cp = document.getElementById('connect-progress'); var db = await openMosaixDB(); var metaRecord = await dbGetMeta(db, 'collection_' + username); if (metaRecord && metaRecord.meta && metaRecord.meta.length > 0) { var activeScreen = document.querySelector('.screen.active'); var stillOnConnect = activeScreen && activeScreen.id === 'screen-connect'; if (cs) cs.textContent = ''; if (cp) cp.style.width = '0%'; loadCoversFromMeta(metaRecord.meta, db).then(function() { var activeNow = document.querySelector('.screen.active'); if (activeNow && activeNow.id === 'screen-connect') { _returnScreen = 'screen-source'; goToScreen('screen-source'); showTilePreview(); } }); if (stillOnConnect) { if (cp) cp.style.width = '60%'; } return; } if (cs) cs.textContent = ''; } }); if('serviceWorker' in navigator){ navigator.serviceWorker.register('/sw.js'); } function getUniqueTiles() { var seen = {}; var result = []; if (!mosaicMap || !mosaicMap.length) return result; for (var y = 0; y < mosaicMap.length; y++) { for (var x = 0; x < mosaicMap[y].length; x++) { var idx = mosaicMap[y][x]; if (idx != null && !seen[idx] && tiles[idx]) { seen[idx] = true; result.push(tiles[idx]); } } } return result; } function exportPoster() { if (!mosaicMap || !mosaicMap.length) { alert('Generate a mosaic first'); return; } var cv = document.getElementById('mosaic-canvas'); var { jsPDF } = window.jspdf; var pdf = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a3' }); var W = 420, H = 297; pdf.setFillColor(15, 15, 15); pdf.rect(0, 0, W, H, 'F'); var imgData = cv.toDataURL('image/jpeg', 0.92); var margin = 12; var imgW = W - margin * 2; var imgH = H - margin * 2 - 14; pdf.addImage(imgData, 'JPEG', margin, margin, imgW, imgH); pdf.setFontSize(9); pdf.setTextColor(180, 180, 180); var uniqueTiles = getUniqueTiles(); var label = uniqueTiles.length + ' Records · Mosaix by Delicate Audio · mosaix.delicateaudio.com'; pdf.text(label, W / 2, H - 5, { align: 'center' }); pdf.setDrawColor(223, 58, 187); pdf.setLineWidth(0.8); pdf.line(margin, H - 10, W - margin, H - 10); pdf.save('A3_mosaic.pdf'); } function exportIndexSheet() { if (!mosaicMap || !mosaicMap.length) { alert('Generate a mosaic first'); return; } var uniqueTiles = getUniqueTiles(); if (!uniqueTiles.length) { alert('No tiles found'); return; } var { jsPDF } = window.jspdf; var pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }); var W = 210, H = 297; pdf.setFillColor(15, 15, 15); pdf.rect(0, 0, W, H, 'F'); pdf.setFillColor(223, 58, 187); pdf.rect(0, 0, W, 8, 'F'); pdf.setFontSize(14); pdf.setTextColor(255, 255, 255); pdf.setFont('helvetica', 'bold'); pdf.text('MOSAIX — ALBUM INDEX', 14, 20); pdf.setFontSize(7); pdf.setTextColor(150, 150, 150); pdf.text(uniqueTiles.length + ' unique records', 14, 27); var cols = 2; var colW = (W - 28) / cols; var x = 14, y = 36; var colIdx = 0; pdf.setFontSize(7); for (var i = 0; i < uniqueTiles.length; i++) { var t = uniqueTiles[i]; var num = String(i + 1).padStart(3, '0'); var artist = (t.rel.artist || '').substring(0, 28); var album = (t.rel.album || '').substring(0, 28); var col = i % cols; var row = Math.floor(i / cols); var cx = 14 + col * colW; var cy = 36 + row * 10; if (cy > H - 16) { pdf.addPage(); pdf.setFillColor(15, 15, 15); pdf.rect(0, 0, W, H, 'F'); cy = 16; row = 0; } pdf.setTextColor(223, 58, 187); pdf.setFont('helvetica', 'bold'); pdf.text(num, cx, cy); pdf.setTextColor(255, 255, 255); pdf.setFont('helvetica', 'bold'); pdf.text(artist, cx + 10, cy); pdf.setTextColor(180, 180, 180); pdf.setFont('helvetica', 'normal'); pdf.text(album, cx + 10, cy + 4); pdf.setDrawColor(40, 40, 40); pdf.setLineWidth(0.2); pdf.line(cx, cy + 6, cx + colW - 4, cy + 6); } pdf.setFontSize(7); pdf.setTextColor(80, 80, 80); pdf.text('Mosaix by Delicate Audio', W / 2, H - 5, { align: 'center' }); pdf.save('mosaix-index.pdf'); } function exportContactSheet() { if (!mosaicMap || !mosaicMap.length) { alert('Generate a mosaic first'); return; } var uniqueTiles = getUniqueTiles(); if (!uniqueTiles.length) { alert('No tiles found'); return; } var { jsPDF } = window.jspdf; var pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }); var W = 210, H = 297; pdf.setFillColor(15, 15, 15); pdf.rect(0, 0, W, H, 'F'); pdf.setFillColor(223, 58, 187); pdf.rect(0, 0, W, 8, 'F'); pdf.setFontSize(11); pdf.setTextColor(255, 255, 255); pdf.setFont('helvetica', 'bold'); pdf.text('COVER SHEET', 14, 20); var cols = 5; var cellW = (W - 28) / cols; var cellH = cellW + 8; var startY = 28; for (var i = 0; i < uniqueTiles.length; i++) { var t = uniqueTiles[i]; var col = i % cols; var row = Math.floor(i / cols); var cx = 14 + col * cellW; var cy = startY + row * cellH; if (cy + cellH > H - 10) { pdf.addPage(); pdf.setFillColor(15, 15, 15); pdf.rect(0, 0, W, H, 'F'); cy = 14; row = 0; } try { var imgCv = document.createElement('canvas'); imgCv.width = 64; imgCv.height = 64; var imgCtx = imgCv.getContext('2d'); imgCtx.drawImage(t.img, 0, 0, 64, 64); var imgData = imgCv.toDataURL('image/jpeg', 0.8); pdf.addImage(imgData, 'JPEG', cx, cy, cellW - 2, cellW - 2); } catch(e) { pdf.setFillColor(30, 30, 30); pdf.rect(cx, cy, cellW - 2, cellW - 2, 'F'); } pdf.setFontSize(5); pdf.setTextColor(200, 200, 200); pdf.text((t.rel.artist || '').substring(0, 18), cx, cy + cellW + 1); pdf.setTextColor(140, 140, 140); pdf.text((t.rel.album || '').substring(0, 18), cx, cy + cellW + 4); } pdf.setFontSize(7); pdf.setTextColor(80, 80, 80); pdf.text('Mosaix by Delicate Audio', W / 2, H - 5, { align: 'center' }); pdf.save('mosaix-covers.pdf'); } function exportWallOfFame() { if (!mosaicMap || !mosaicMap.length) { alert('Generate a mosaic first'); return; } var uniqueTiles = getUniqueTiles(); var { jsPDF } = window.jspdf; var pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }); var W = 210, H = 297; pdf.setFillColor(10, 10, 10); pdf.rect(0, 0, W, H, 'F'); pdf.setFillColor(223, 58, 187); pdf.rect(0, 0, W, 2, 'F'); pdf.rect(0, H - 2, W, 2, 'F'); var cv = document.getElementById('mosaic-canvas'); var thumbData = cv.toDataURL('image/jpeg', 0.7); var thumbSize = 60; pdf.addImage(thumbData, 'JPEG', (W - thumbSize) / 2, 30, thumbSize, thumbSize); pdf.setFillColor(223, 58, 187); pdf.setTextColor(223, 58, 187); pdf.setFontSize(72); pdf.setFont('helvetica', 'bold'); pdf.text(String(uniqueTiles.length), W / 2, 165, { align: 'center' }); pdf.setFontSize(14); pdf.setTextColor(255, 255, 255); pdf.text('RECORDS IN MY COLLECTION', W / 2, 178, { align: 'center' }); pdf.setFontSize(9); pdf.setTextColor(150, 150, 150); var now = new Date(); pdf.text(now.getFullYear().toString(), W / 2, 190, { align: 'center' }); pdf.setDrawColor(223, 58, 187); pdf.setLineWidth(0.5); pdf.line(40, 196, W - 40, 196); pdf.setFontSize(8); pdf.setTextColor(100, 100, 100); pdf.text('mosaix.delicateaudio.com', W / 2, 204, { align: 'center' }); pdf.save('mosaix-wall-of-fame.pdf'); } function exportQRSheet() { if (!mosaicMap || !mosaicMap.length) { alert('Generate a mosaic first'); return; } var uniqueTiles = getUniqueTiles().filter(function(t) { return t.rel && t.rel.release_id; }); if (!uniqueTiles.length) { alert('No tiles with Discogs IDs found'); return; } var { jsPDF } = window.jspdf; var pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }); var W = 210, H = 297; pdf.setFillColor(15, 15, 15); pdf.rect(0, 0, W, H, 'F'); pdf.setFillColor(223, 58, 187); pdf.rect(0, 0, W, 8, 'F'); pdf.setFontSize(11); pdf.setTextColor(255, 255, 255); pdf.setFont('helvetica', 'bold'); pdf.text('QR CODE SHEET — Scan for Discogs Info', 14, 20); var cols = 4; var cellW = (W - 28) / cols; var cellH = cellW + 12; var startY = 28; for (var i = 0; i < uniqueTiles.length; i++) { var t = uniqueTiles[i]; var col = i % cols; var row = Math.floor(i / cols); var cx = 14 + col * cellW; var cy = startY + row * cellH; if (cy + cellH > H - 10) { pdf.addPage(); pdf.setFillColor(15, 15, 15); pdf.rect(0, 0, W, H, 'F'); cy = 14; row = 0; } var url = 'https://www.discogs.com/release/' + t.rel.release_id; var qrSize = cellW - 4; try { var qrCv = document.createElement('canvas'); new QRCode(qrCv, { text: url, width: 128, height: 128, colorDark: '#ffffff', colorLight: '#0f0f0f' }); setTimeout((function(qc, pcx, pcy, psz, pt, ppdf) { return function() { try { var qd = qc.toDataURL('image/png'); ppdf.addImage(qd, 'PNG', pcx, pcy, psz, psz); } catch(e) {} ppdf.setFontSize(5); ppdf.setTextColor(200, 200, 200); ppdf.text((pt.rel.artist || '').substring(0, 20), pcx, pcy + psz + 3); ppdf.setTextColor(140, 140, 140); ppdf.text((pt.rel.album || '').substring(0, 20), pcx, pcy + psz + 6.5); }; })(qrCv, cx, cy, qrSize, t, pdf), 120 * i); } catch(e) { pdf.setFillColor(30, 30, 30); pdf.rect(cx, cy, qrSize, qrSize, 'F'); pdf.setFontSize(5); pdf.setTextColor(200, 200, 200); pdf.text((t.rel.artist || '').substring(0, 20), cx, cy + qrSize + 3); } } setTimeout(function() { pdf.setFontSize(7); pdf.setTextColor(80, 80, 80); pdf.text('Mosaix by Delicate Audio', W / 2, H - 5, { align: 'center' }); pdf.save('mosaix-qr-sheet.pdf'); }, 120 * uniqueTiles.length + 300); } close