<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pixel Particle Image</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <canvas id="pixelCanvas"></canvas>
    <script src="script.js"></script>
</body>
</html>
body {
    margin: 0;
    overflow: hidden;
    background-color: #111; /* Dark background */
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
}

canvas {
    display: block;
    border: 1px solid #333; /* Optional: See canvas edges */
}
const canvas = document.getElementById('pixelCanvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });

// --- CONFIGURATION ---
const PIXELATION_LEVEL = 3;       // Detail level for final images (HIGH PERFORMANCE COST!)
const MOUSE_RADIUS = 50;
const MOUSE_REPEL_FORCE = 0.4;
const PARTICLE_FRICTION = 0.94;
const PARTICLE_RETURN_FORCE = 0.06;
const IMAGE_SCALE_FACTOR = 0.85;
const GRID_PADDING = 25;
const MAX_PARTICLES_PER_IMAGE_MOBILE = 1800; // Limit per image on mobile

// Loading Animation Config
const LOADING_PARTICLE_COUNT = 300; // Max number of loading particles
const LOADING_EMISSION_RATE = 3;   // New loading particles per frame
const LOADING_PARTICLE_COLOR = `hsla(200, 80%, 70%, 0.8)`; // Light blueish

// --- IMAGE URLS ---
const imageUrls = [
    "https://pbs.twimg.com/profile_images/1857142933432287232/6T5XP2t2_400x400.jpg",
    "https://tinyurl.com/246zzrd9", "https://tinyurl.com/29ftotun", "https://tinyurl.com/29ueslk5",
    "https://tinyurl.com/27lqh8oo", "https://tinyurl.com/273o4jcq", "https://tinyurl.com/2cv8tuvz",
    "https://tinyurl.com/24mcf84u", "https://tinyurl.com/23bc7fc3", "https://tinyurl.com/23bc7fc3",
    "https://tinyurl.com/23fn8y58", "https://tinyurl.com/25d6pa22"
];

// --- STATE VARIABLES ---
let imagesData = [];
const mouse = { x: undefined, y: undefined, radius: MOUSE_RADIUS };
let isMobile = window.innerWidth < 768;
let gridCols = 0, gridRows = 0, cellWidth = 0, cellHeight = 0;
let mainAnimationId = null; // ID for the main animation loop
let loadingAnimationId = null; // ID for the loading animation loop
let isLoading = true; // Start in loading state
let loadingParticles = []; // Array for loading animation particles

// --- Particle Class (for final images) ---
class Particle {
    constructor(originX, originY, color) {
        this.originX = originX; this.originY = originY;
        this.color = color;
        this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height;
        this.vx = 0; this.vy = 0;
        this.size = PIXELATION_LEVEL;
        this.density = (Math.random() * 15) + 5;
    }
    update() {
        let dxMouse = this.x - mouse.x, dyMouse = this.y - mouse.y;
        let distanceMouseSq = dxMouse * dxMouse + dyMouse * dyMouse;
        let mouseRadiusSq = mouse.radius * mouse.radius;
        let force = 0, forceDirectionX = 0, forceDirectionY = 0;
        if (mouse.x !== undefined && distanceMouseSq > 0 && distanceMouseSq < mouseRadiusSq) {
            let distanceMouse = Math.sqrt(distanceMouseSq);
            forceDirectionX = dxMouse / distanceMouse; forceDirectionY = dyMouse / distanceMouse;
            force = (mouse.radius - distanceMouse) / mouse.radius * MOUSE_REPEL_FORCE * this.density;
        }
        let dxOrigin = this.originX - this.x, dyOrigin = this.originY - this.y;
        this.vx += forceDirectionX * force + dxOrigin * PARTICLE_RETURN_FORCE;
        this.vy += forceDirectionY * force + dyOrigin * PARTICLE_RETURN_FORCE;
        this.vx *= PARTICLE_FRICTION; this.vy *= PARTICLE_FRICTION;
        this.x += this.vx; this.y += this.vy;
    }
    draw() {
        ctx.fillStyle = this.color;
        ctx.fillRect(this.x - this.size / 2, this.y - this.size / 2, this.size, this.size);
    }
}

// --- Loading Particle Class ---
class LoadingParticle {
    constructor(x, y) {
        this.x = x;
        this.y = y;
        const angle = Math.random() * Math.PI * 2;
        const speed = Math.random() * 2 + 0.5;
        this.vx = Math.cos(angle) * speed;
        this.vy = Math.sin(angle) * speed;
        this.size = Math.random() * 2 + 1; // Smaller loading particles
        this.maxLife = Math.random() * 80 + 40; // Lifetime in frames
        this.life = this.maxLife;
        this.opacity = 1;
        this.gravity = 0.01; // Slight downward pull
    }
    update() {
        this.x += this.vx;
        this.y += this.vy;
        this.vy += this.gravity; // Apply gravity
        this.life--;
        this.opacity = Math.max(0, this.life / this.maxLife); // Fade out
        this.vx *= 0.98; // Air friction
        this.vy *= 0.98;
    }
    draw() {
        if (this.opacity <= 0 || this.life <= 0) return;
        ctx.fillStyle = `hsla(200, 80%, ${60 + this.opacity * 30}%, ${this.opacity * 0.7})`; // Fade and change lightness
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
        ctx.fill();
    }
}

// --- Emit Loading Particles ---
function emitLoadingParticles() {
    const centerX = canvas.width / 2;
    const centerY = canvas.height / 2;
    for (let i = 0; i < LOADING_EMISSION_RATE; i++) {
        if (loadingParticles.length >= LOADING_PARTICLE_COUNT) return;
        // Emit from center with slight spread
        const spawnX = centerX + (Math.random() - 0.5) * 20;
        const spawnY = centerY + (Math.random() - 0.5) * 20;
        loadingParticles.push(new LoadingParticle(spawnX, spawnY));
    }
}

// --- Loading Animation Loop ---
function animateLoading() {
    if (!isLoading) return; // Stop if loading is finished

    ctx.fillStyle = 'rgba(17, 17, 17, 0.2)'; // Clear with trails
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    emitLoadingParticles();

    // Update and draw loading particles
    for (let i = loadingParticles.length - 1; i >= 0; i--) {
        const p = loadingParticles[i];
        p.update();
        if (p.life <= 0) {
            loadingParticles.splice(i, 1); // Remove dead particles
        } else {
            p.draw();
        }
    }

    // Optional: Draw "Loading..." text
    ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
    ctx.font = "16px sans-serif";
    ctx.textAlign = "center";
    ctx.fillText("Loading Images...", canvas.width / 2, canvas.height / 2 + 40);

    loadingAnimationId = requestAnimationFrame(animateLoading); // Continue loop
}


// --- Image Loading --- (No changes needed from previous version)
function loadImages() {
    console.log("Starting image loading...");
    imagesData = [];
    const promises = imageUrls.map(url => { /* ... same as before ... */
        return new Promise((resolve, reject) => {
            const img = new Image();
            const data = { url: url, img: img, particles: [], scaledWidth: 0, scaledHeight: 0, offsetX: 0, offsetY: 0, isLoaded: false, error: null };
            imagesData.push(data);
            img.crossOrigin = "anonymous";
            img.onload = () => { console.log("Loaded:", url.substring(0, 50) + "..."); data.isLoaded = true; resolve(data); };
            img.onerror = (err) => { console.error("Error loading:", url, err); data.error = "Load Error"; reject(data); }; // Use reject here
            img.src = url;
        });
    });
    return Promise.allSettled(promises).then(results => {
        console.log("Image loading attempts finished.");
         results.forEach((result, index) => { // Log failures after settling
             if (result.status === 'rejected') {
                 // Error is already set in imagesData[index].error by the onerror handler
                 console.warn(`Image at index ${index} (${imagesData[index].url.substring(0,30)}...) failed to load.`);
             }
        });
        return imagesData;
    });
}

// --- Layout Calculation --- (No changes needed)
function calculateLayout() { /* ... same as before ... */
    const loadedImages = imagesData.filter(d => d.isLoaded && !d.error);
    const numImages = loadedImages.length;
    if (numImages === 0) { console.warn("No images loaded successfully for layout."); gridCols=0; gridRows=0; cellWidth=0; cellHeight=0; return; }
    const aspectRatio = canvas.width / canvas.height;
    gridCols = Math.ceil(Math.sqrt(numImages * aspectRatio)); gridRows = Math.ceil(numImages / gridCols);
    cellWidth = Math.max(1, (canvas.width - GRID_PADDING * (gridCols + 1)) / gridCols);
    cellHeight = Math.max(1, (canvas.height - GRID_PADDING * (gridRows + 1)) / gridRows);
    console.log(`Layout: ${gridCols}x${gridRows} grid. Cell size: ${cellWidth.toFixed(1)}x${cellHeight.toFixed(1)}`);
    let currentImageIndex = 0;
    for (let r = 0; r < gridRows; r++) {
        for (let c = 0; c < gridCols; c++) {
            if (currentImageIndex >= loadedImages.length) break;
            const imgData = loadedImages[currentImageIndex];
            const img = imgData.img; const imgAspect = img.naturalWidth / img.naturalHeight;
            let targetW = cellWidth * IMAGE_SCALE_FACTOR; let targetH = cellHeight * IMAGE_SCALE_FACTOR;
            if (targetW / targetH > imgAspect) { imgData.scaledHeight = Math.min(img.naturalHeight, targetH); imgData.scaledWidth = imgData.scaledHeight * imgAspect; }
            else { imgData.scaledWidth = Math.min(img.naturalWidth, targetW); imgData.scaledHeight = imgData.scaledWidth / imgAspect; }
            imgData.scaledWidth = Math.max(1, Math.floor(imgData.scaledWidth)); imgData.scaledHeight = Math.max(1, Math.floor(imgData.scaledHeight));
            const cellX = GRID_PADDING + c * (cellWidth + GRID_PADDING); const cellY = GRID_PADDING + r * (cellHeight + GRID_PADDING);
            imgData.offsetX = cellX + (cellWidth - imgData.scaledWidth) / 2; imgData.offsetY = cellY + (cellHeight - imgData.scaledHeight) / 2;
            currentImageIndex++;
        }
         if (currentImageIndex >= loadedImages.length) break;
    }
}

// --- Particle Creation (for one image) --- (No changes needed)
function scanImageAndCreateParticles(imgData) { /* ... same as before ... */
    if (!imgData || !imgData.isLoaded || imgData.error || !imgData.scaledWidth || imgData.scaledWidth <= 0) return;
    imgData.particles = [];
    const offscreenCanvas = document.createElement('canvas'); offscreenCanvas.width = imgData.scaledWidth; offscreenCanvas.height = imgData.scaledHeight;
    const offscreenCtx = offscreenCanvas.getContext('2d', { willReadFrequently: true }); offscreenCtx.drawImage(imgData.img, 0, 0, imgData.scaledWidth, imgData.scaledHeight);
    let imageDataResult;
    try { imageDataResult = offscreenCtx.getImageData(0, 0, imgData.scaledWidth, imgData.scaledHeight); }
    catch (e) { console.error(`CORS Error getting ImageData for: ${imgData.url}`, e); imgData.error = "CORS Error"; return; }
    const data = imageDataResult.data; let particleCount = 0; const maxParticles = isMobile ? MAX_PARTICLES_PER_IMAGE_MOBILE : Infinity;
    for (let y = 0; y < imgData.scaledHeight; y += PIXELATION_LEVEL) {
        for (let x = 0; x < imgData.scaledWidth; x += PIXELATION_LEVEL) {
            if (particleCount >= maxParticles) break;
            const index = (Math.floor(y) * imgData.scaledWidth + Math.floor(x)) * 4; if (index >= data.length - 3) continue;
            const r = data[index], g = data[index + 1], b = data[index + 2], a = data[index + 3];
            if (a > 128) { const color = `rgb(${r},${g},${b})`; const originX = x + imgData.offsetX; const originY = y + imgData.offsetY; imgData.particles.push(new Particle(originX, originY, color)); particleCount++; }
        } if (particleCount >= maxParticles) break;
    }
}

// --- Initialization (Called after images loaded and on resize) ---
function initialize() {
    console.log("Initializing layout and particles...");
    // Stop any running animation loops
    if (loadingAnimationId) cancelAnimationFrame(loadingAnimationId);
    if (mainAnimationId) cancelAnimationFrame(mainAnimationId);
    isLoading = false; // Ensure loading state is false now
    loadingParticles = []; // Clear loading particles immediately

    // Setup canvas and mouse state
    canvas.width = window.innerWidth; canvas.height = window.innerHeight;
    isMobile = window.innerWidth < 768;
    mouse.radius = isMobile ? MOUSE_RADIUS * 0.7 : MOUSE_RADIUS;

    calculateLayout(); // Determine positions based on loaded images

    // Scan and create particles for successfully loaded images
    imagesData.forEach(imgData => {
        if (imgData.isLoaded && !imgData.error) {
            scanImageAndCreateParticles(imgData);
        }
    });

    const totalParticles = imagesData.reduce((sum, data) => sum + (data.particles ? data.particles.length : 0), 0);
    console.log("Total particles created:", totalParticles);
    if (totalParticles > 50000) { console.warn(`PERFORMANCE WARNING: ${totalParticles} particles created. This may cause lag.`); }

    if (totalParticles > 0) {
        mainAnimationId = requestAnimationFrame(animate); // Start the *main* animation loop
    } else {
        console.warn("Initialization complete, but no particles were created. Main animation not started.");
        if (imagesData.every(d => !d.isLoaded || d.error)) {
            drawErrorState("Failed to load or process any images. Check URLs and CORS permissions in console (F12).");
        } else {
             ctx.fillStyle = 'rgba(17, 17, 17, 1)'; // Clear canvas if starting blank after loading
             ctx.fillRect(0, 0, canvas.width, canvas.height);
             drawIndividualImageErrors(); // Show specific errors immediately
        }
    }
}

// --- Drawing Error States --- (No changes needed)
function drawErrorState(message) { /* ... same as before ... */
    ctx.fillStyle="rgba(0,0,0,0.8)"; ctx.fillRect(0,0,canvas.width,canvas.height); ctx.fillStyle="white"; ctx.font="18px sans-serif"; ctx.textAlign="center"; ctx.fillText(message,canvas.width/2,canvas.height/2);
}
function drawIndividualImageErrors() { /* ... same as before ... */
    imagesData.forEach(imgData=>{if(imgData.error&&imgData.scaledWidth>0){const cx=imgData.offsetX+imgData.scaledWidth/2; const cy=imgData.offsetY+imgData.scaledHeight/2; ctx.fillStyle="rgba(120,0,0,0.75)"; ctx.fillRect(imgData.offsetX,imgData.offsetY,imgData.scaledWidth,imgData.scaledHeight); ctx.fillStyle="white"; ctx.font="bold 11px sans-serif"; ctx.textAlign="center"; ctx.fillText(imgData.error,cx,cy-5); ctx.font="9px sans-serif"; ctx.fillText("URL: "+imgData.url.substring(0,30)+"...",cx,cy+5,imgData.scaledWidth*0.9);}});
}

// --- Main Animation Loop (for final images) ---
function animate() {
    if (isLoading) return; // Should not run if still loading, but safety check

    ctx.fillStyle = 'rgba(17, 17, 17, 0.3)'; // Clear with trails
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // Update and draw image particles
    imagesData.forEach(imgData => {
        if (imgData.particles && imgData.particles.length > 0) {
            imgData.particles.forEach(particle => {
                particle.update();
                particle.draw();
            });
        }
    });

    // Draw error overlays for failed images
    drawIndividualImageErrors();

    mainAnimationId = requestAnimationFrame(animate); // Continue loop
}

// --- Initial Canvas Setup ---
function setupInitialCanvas() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    ctx.fillStyle = '#111'; // Initial background
    ctx.fillRect(0, 0, canvas.width, canvas.height);
}

// --- Event Listeners --- (No changes needed)
window.addEventListener('resize', () => {
    // If resizing happens during initial load, just resize canvas and let loading continue
    // If resizing happens after load, re-initialize everything
    if (isLoading) {
        setupInitialCanvas(); // Just resize canvas
    } else {
        initialize(); // Re-layout, re-scan particles after load
    }
});
window.addEventListener('mousemove', (event) => { mouse.x = event.clientX; mouse.y = event.clientY; });
window.addEventListener('mouseout', () => { mouse.x = undefined; mouse.y = undefined; });
window.addEventListener('touchstart', (event) => { if (event.touches.length > 0) { mouse.x = event.touches[0].clientX; mouse.y = event.touches[0].clientY; } }, { passive: true });
window.addEventListener('touchmove', (event) => { if (event.touches.length > 0) { mouse.x = event.touches[0].clientX; mouse.y = event.touches[0].clientY; } }, { passive: true });
window.addEventListener('touchend', () => { mouse.x = undefined; mouse.y = undefined; });


// --- Start ---
console.log("Page loaded, setting up initial canvas and starting loading sequence...");
setupInitialCanvas(); // Set initial canvas size immediately
loadingAnimationId = requestAnimationFrame(animateLoading); // Start the loading animation loop

// Begin loading images
loadImages().then(() => {
    // This .then() block executes after all image load attempts are settled
    isLoading = false; // Set loading state to false
    if (loadingAnimationId) cancelAnimationFrame(loadingAnimationId); // Stop loading animation
    loadingParticles = []; // Clear loading particles
    // Note: We don't explicitly clear the canvas here, letting initialize handle it
    // or letting the first frame of the main 'animate' loop draw over the last loading frame.
    initialize(); // Setup layout, create image particles, start main animation
}).catch(err => {
    // Catch should ideally not be needed with allSettled, but handles unexpected setup errors
    console.error("Critical error during initial setup:", err);
    isLoading = false; // Ensure loading stops even on error
    if (loadingAnimationId) cancelAnimationFrame(loadingAnimationId);
    drawErrorState("An unexpected error occurred during setup.");
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.