40 Pet Images

Case Study: Dynamic Content Generation & Context-Aware UI Animation

The Problem: The Macro-Image Optimization Dilemma

  • The Creative Vision: A regional animal welfare shelter required a hero section consisting of a "super-image" grid containing 40 individual portrait photos of adopted pets. The design dictated that the grid remain dimmed at a baseline opacity, with individual portraits "lighting up" (returning to full opacity) one at a time via a smooth transition at a fixed interval.
  • The UX & Responsiveness Constraint: If the 40-image grid was made fully responsive (scaling down to fit mobile screens), the individual pet portraits would become too small to distinguish, ruining the emotional impact. The layout had to allow images to flow naturally out of the immediate viewport on smaller screens.
  • The Challenge:
    • DOM Performance: Hard-coding 40 image elements would create bloated, unmaintainable HTML.
    • Context-Aware Logic: The animation engine needed to be "viewport-aware." If an image was positioned off-screen due to screen scaling, the script had to skip it to avoid wasting GPU/CPU cycles animating invisible elements.

The Action: Clean Architecture via PHP, CSS Grid, and Vanilla JS

  • HTML Generation: Instead of writing out a massive, repetitive block of HTML, I let PHP do the heavy lifting. A simple server-side for loop dynamically builds the 40 image containers using sequential file names (like 1.png, 2.png). This kept the codebase tiny, clean, and incredibly easy to update.
  • Structural Layout & Hardware Acceleration: I leveraged CSS Grid (compiled via SCSS) over Flexbox to enforce strict two-dimensional alignment for the 40-piece block. To handle the "lighting up" effect efficiently:
    • The base state layer (.bg_random) was set to an opacity of 0.2.
    • A specific active class (.highlighted) brought the opacity to 1.0.
    • I added a 300ms ease-in-out CSS transition to offload the fading animation to the browser's GPU, eliminating any potential layout thrashing.
  • Viewport-Aware Animation Engine: I built a lightweight, vanilla JavaScript animator that triggers every 1.5 seconds.
    • Viewport Detection: To keep things efficient, the script uses getBoundingClientRect() to check exactly where each image is on the screen in real-time. If an image has flowed off the screen on a mobile device, the script simply skips it.
    • Preventing Predictable Patterns: To make sure the highlights feel naturally random, the script tracks a rolling history of the last 5 active images. When choosing the next pet photo to light up, the script picks one at random, double-checks that it wasn't used recently, and verifies it is actually visible on the user's screen.
    • Execution: Once a valid, visible, non-repeat image was selected, animateImage() applied the .highlighted class, using a setTimeout hook to cleanly purge the class after 1 second to prepare for the next cycle.

The Result: High-Fidelity UX with Near-Zero Overhead

  • Perceived Quality: The localized lighting effect preserved the emotional intent of the design, ensuring that mobile users only witnessed active animations on pets they could actually see.
  • Architectural Performance: By combining CSS transitions with visibility checks, the page maintained optimal scrolling and execution speeds.
  • Operational Longevity: The asset management structure allowed the client's internal media team to swap out pet portraits seamlessly by simply overwriting indexed files in the image directory without touching the code.

Under the Hood: Viewport-Aware Grid

To build an engaging, active hero layout without introducing heavy visual assets, the system is split into a lightweight server-rendered canvas and an optimized client-side animation engine.

By generating the grid layout on the server with PHP, we send a lightweight page to the browser right away. From there, a custom JavaScript engine takes over—watching the page as the user scrolls, tracking which elements are actually on screen, and making sure the animation only runs when the grid is in view.

The Layout & Rendering Canvas (HTML/PHP)

To establish a performant animation system, the structural markup must remain light, highly semantic, and accessible. Rather than rendering heavy individual image elements, the DOM utilizes a background image grid populated programmatically.

<header>
  <div class="header-wrapper">
    <div id="hero">
      <div id="overlay">
        <?php
          // generate 40 background image grid units
          for ($i = 1; $i <= 40; $i++) {
            echo '<div class="bg_random" role="img" aria-hidden="true" style="background-image: url(\'images/' . $i . '.png\');"></div>';
          }
        ?>
      </div>
    </div>

    <div class="header-content">
      <a class="icon-link logo" href="https://www.organization.org/" target="_blank" rel="noopener">
        <img src="images/logo-banner.png" alt="Organization Logo" title="Organization Name"/>
      </a>

      <div class="header-main-content">
        <img id="logo-80" src="images/logo-80.png" alt="80 Years celebration">
        <h2>The Future Looks <span>Twice as Bright</span></h2>
        <img src="images/strike.svg" alt="" aria-hidden="true">
        <h1><em>Giving Day</em> 6.10.2024</h1>
      </div>

      <div class="header-footer">
        <a class="donate-link donate-link-green big-button" href="https://purchase.net">Double Your Gift</a>

        <button class="down-arrow scroll-button" data-target="fundraising-section" data-offset="-64" aria-label="Scroll to content">
          <img src="images/arrows.svg" alt="" aria-hidden="true"/>
        </button>
      </div>
    </div>
  </div>
</header>

Key Implementation Details:

  • Decoupled Server Rendering: Rather than hardcoding 40 individual layout items, a basic PHP for loop dynamically compiles the canvas grid at render-time. This keeps maintenance trivial and makes it incredibly simple to scale or adjust the density of the grid.
  • Accessible Semantic Optimization: Because the 40 grid pieces are purely cosmetic and contain no meaningful contextual information, they are assigned aria-hidden="true" and an explicit role="img". This prevents screen readers from hitting 40 confusing "empty" background blocks, preserving a seamless document outline.
  • Anchor Tag Security Hardening: Outbound external links include rel="noopener" alongside target="_blank" to protect user sessions against reverse tabnabbing exploits and prevent unnecessary resource overhead on external redirects.

The Viewport & Collision-Resolution Engine (JavaScript)

The core execution layer coordinates selecting, verifying, and animating random grid items. To prevent CPU degradation, it limits rendering actions exclusively to elements visible within the immediate screen bounds.

// cache elements and track highlighted history
const allImages = document.querySelectorAll('.bg_random');
let repeat = [];

// check if at least 50% of the element is visible in the viewport
function isElemInView(elem) {
  if (!elem) return false;

  const rect = elem.getBoundingClientRect();
  const offsetWidth = window.innerWidth - (rect.width / 2);
  const offsetHeight = window.innerHeight - (rect.height / 2);

  return (
    rect.left < offsetWidth &&
    rect.left + rect.width > 0 &&
    rect.top < offsetHeight &&
    rect.top + rect.height > 0
  );
}

// verify index hasn't been recently highlighted (max 5 in history)
function isNotRepeat(num) {
  if (repeat.length > 5) {
    repeat.shift();
  }
  return !repeat.includes(num);
}

// get a random image that is visible and hasn't run recently
function getRandomImage() {
  if (allImages.length === 0) return null;

  let randomIndex = Math.trunc(Math.random() * allImages.length);

  // loop through to find the next available visible image
  for (let i = 0; i < allImages.length; i++) {
    const candidateIndex = (randomIndex + i) % allImages.length;
    const candidateElem = allImages[candidateIndex];

    if (isElemInView(candidateElem) && isNotRepeat(candidateIndex)) {
      repeat.push(candidateIndex);
      return candidateElem;
    }
  }

  // fallback if nothing fits the criteria
  return allImages[randomIndex];
}

// trigger highlighted class for 1 second
function animateRandomImage() {
  const img = getRandomImage();
  if (!img) return;

  img.classList.add('highlighted');
  setTimeout(() => img.classList.remove('highlighted'), 1000);
}

// stop animation loop when grid scrolls out of view
let animationInterval;

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      animateRandomImage();
      animationInterval = setInterval(animateRandomImage, 1500);
    } else {
      clearInterval(animationInterval);
    }
  });
}, { threshold: 0.1 });

// start observer on hero grid
const overlayContainer = document.getElementById('overlay');
if (overlayContainer) {
  observer.observe(overlayContainer);
}

Key Implementation Details:

  • Smart DOM Caching: Instead of making the browser search the entire page for grid images every single time a tile lights up, the script queries and caches the elements once at startup. This drastically reduces CPU overhead and keeps the page scrolling buttery-smooth.
  • Grid Randomization: Standard randomizers can easily get stuck in a loop trying to find a tile that is both currently on-screen and hasn't been highlighted recently. To solve this, the script uses a smart fallback system: if a randomly picked tile isn't valid, it simply steps to the very next neighbor in line. This guarantees the browser instantly finds a valid tile without ever locking up or freezing.
  • Recent-Highlight Memory: To keep the animation looking naturally random and prevent the same few tiles from lighting up repeatedly, the engine maintains a sliding "memory" of the last five highlighted images. When a new tile is added to this list, the oldest one is automatically dropped, ensuring a smooth, continuous variety.
  • HTML-Native Load Timing: By using the standard HTML async defer attributes on the script tag, we guarantee that the JavaScript waits to run until the PHP loop has finished generating all 40 grid pieces in the HTML. This prevents timing bugs without needing to write complex load-listening code.
  • Battery-Saving Scroll Awareness: Standard timers keep running in the background even if a user minimizes the tab, switches windows, or scrolls miles down the page. To save device battery and CPU power, this engine uses a native IntersectionObserver to completely pause the timer when the grid leaves the screen, instantly resuming it only when the user scrolls back to the top.