← Cards & images

Instant Photo Develop

An instant print slides out as a flat blue-grey blank, gets a little shake, and the picture slowly comes through: murky and patchy at first, then sharper, until it’s in full colour and the caption appears.

Lake, August

Tap the photo to take another

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-instant-photo-develop to a <figure> holding your picture and, if you like, a <figcaption>. Give it a width; the white border is added for you.

Options

Add any of these attributes to the same element.

data-durationTime to develop. Default 5000 ms.
data-shake="false"Don’t shake the print.
data-eject="false"Don’t slide in: just develop where it is.
data-frame="false"Leave out the white border, to style your own.
data-loopSwap in a fresh print and develop again.
data-holdHow long the developed photo stays before the next. Default 2500 ms.

It develops when it scrolls into view, and a click or tap develops it again. The caption is written in a handwriting font if one is installed. From code: animacious.develop(element). With reduced motion it shows the finished photo.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Instant Photo Develop: a print slides out like it came from an instant
    // camera, starting as a flat blue-grey blank. The picture fades up through
    // murky, washed-out tones, blotchy at first, until it's sharp and in full
    // colour, while the print gives a little shake.
    // Usage: <figure data-instant-photo-develop style="width: 240px">
    //          <img src="photo.jpg" alt="Lake at dawn">
    //          <figcaption>Lake, August</figcaption>
    //        </figure>
    // The first child is the picture; a <figcaption> is written on the frame.
    // Optional: data-duration (ms to develop, default 5000), data-shake
    // ("false" to keep it still), data-eject ("false": no slide-in, it just
    // develops), data-frame ("false": no white instant-print border, for when
    // you style your own), data-loop, data-hold (ms to show the developed
    // photo before the next one, default 2500). A click or tap develops it again.
    // Script use: animacious.develop(el).
    var DEFAULTS = { duration: 5000, hold: 2500 };

    var css =
      '.ipd{-webkit-tap-highlight-color:transparent}' +
      '.ipd.ipd-frame{box-sizing:border-box;margin:0;padding:var(--ipd-p,12px) var(--ipd-p,12px) 0;background:#fbfaf6;border-radius:2px;box-shadow:0 1px 2px rgba(0,0,0,.12),0 12px 30px rgba(15,15,30,.22)}' +
      '.ipd-pic{position:relative;overflow:hidden;aspect-ratio:1;background:#2c3238}' +
      '.ipd-pic>:first-child{display:block;width:100%;height:100%;object-fit:cover}' +
      '.ipd-blank,.ipd-blot{position:absolute;inset:0;pointer-events:none}' +
      '.ipd-blank{background:linear-gradient(160deg,#3d4652,#2a3038 60%,#343a33)}' +
      '.ipd-blot{background:radial-gradient(circle at 28% 32%,#4a5560 0 18%,transparent 42%),radial-gradient(circle at 72% 64%,#56483e 0 14%,transparent 40%),radial-gradient(circle at 60% 20%,#3a4450 0 10%,transparent 30%);mix-blend-mode:multiply}' +
      '.ipd.ipd-frame figcaption,.ipd.ipd-frame>.ipd-cap{display:block;padding:.55em 2% .7em;color:#3b3640;font:500 1em/1.2 "Segoe Print","Bradley Hand","Comic Sans MS",cursive;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function setup(el) {
      var d = el.dataset;
      var pic = el.firstElementChild;
      if (!pic) return;
      el.classList.add('ipd');
      if (d.frame !== 'false') el.classList.add('ipd-frame');
      var box = document.createElement('div');
      box.className = 'ipd-pic';
      el.insertBefore(box, pic);
      box.appendChild(pic);
      var blank = document.createElement('div');
      blank.className = 'ipd-blank';
      var blot = document.createElement('div');
      blot.className = 'ipd-blot';
      [blank, blot].forEach(function (n) { n.setAttribute('aria-hidden', 'true'); box.appendChild(n); });
      var cap = el.querySelector('figcaption');
      // The border is a share of the print's own width (a % padding would follow the parent's).
      var pad = function () { el.style.setProperty('--ipd-p', (el.offsetWidth * 0.06).toFixed(1) + 'px'); };
      pad();
      new ResizeObserver(pad).observe(el);
      var s = el._ipdS = { pic: pic, blank: blank, blot: blot, cap: cap, v: { p: 1 }, tl: null, timer: 0, visible: false, waiting: false };
      if (reduced()) { paint(el); return; }
      s.v.p = 0;
      paint(el);
      if (d.eject !== 'false') el.style.opacity = 0;

      el.addEventListener('click', function () { develop(el); });
      var played = false;
      new IntersectionObserver(function (en) {
        s.visible = en[0].isIntersecting;
        if (s.visible && !played) { played = true; develop(el); }
        if (s.visible && s.waiting) { s.waiting = false; develop(el); }
      }, { threshold: 0.35 }).observe(el);
    }

    // p runs 0 (blank) to 1 (developed).
    function paint(el) {
      var s = el._ipdS, p = s.v.p;
      var e = function (a, b) { return Math.max(0, Math.min(1, (p - a) / (b - a))); };
      // The chemistry clears first, then contrast, then colour comes in last.
      s.blank.style.opacity = (1 - e(0.05, 0.55)).toFixed(3);
      s.blot.style.opacity = (Math.sin(Math.PI * e(0.1, 0.75)) * 0.85).toFixed(3);
      var sat = 0.05 + 0.95 * e(0.45, 1), con = 0.45 + 0.55 * e(0.25, 0.9), bri = 0.7 + 0.3 * e(0.2, 0.8);
      var sep = 0.55 * (1 - e(0.5, 1)), hue = -25 * (1 - e(0.4, 1)), blur = 1.6 * (1 - e(0.3, 0.85));
      s.pic.style.filter = p >= 1 ? '' :
        'sepia(' + sep.toFixed(3) + ') hue-rotate(' + hue.toFixed(1) + 'deg) saturate(' + sat.toFixed(3) + ') contrast(' + con.toFixed(3) + ') brightness(' + bri.toFixed(3) + ') blur(' + blur.toFixed(2) + 'px)';
      if (s.cap) s.cap.style.opacity = e(0.7, 1).toFixed(3);
    }

    function develop(el) {
      var s = el._ipdS, d = el.dataset;
      if (!s || reduced()) return;
      if (s.tl) s.tl.pause();
      clearTimeout(s.timer);
      var dur = +d.duration || DEFAULTS.duration;
      var eject = d.eject !== 'false', shake = d.shake !== 'false';
      var tl = s.tl = anime.createTimeline({ autoplay: false });
      var was = s.v.p;
      // Out with the old print first, if there is one.
      if (was > 0 && eject) {
        tl.add(el, { y: 40, opacity: 0, rotate: 6, duration: 450, ease: 'in(2)' });
        tl.call(function () { s.v.p = 0; paint(el); });
      } else if (was > 0) {
        tl.add(s.v, { p: 0, duration: 500, ease: 'inOut(2)', onUpdate: function () { paint(el); } });
      }
      var at = was > 0 ? (eject ? 500 : 550) : 0;
      if (eject) {
        // Slides out of the camera, top first, and settles at a slight angle.
        tl.add(el, {
          y: { from: -60, to: 0 }, opacity: { from: 0, to: 1, duration: 300 }, rotate: { from: -4, to: -1.5 },
          duration: 900, ease: 'out(3)'
        }, at);
      }
      var devAt = at + (eject ? 500 : 0);
      tl.add(s.v, { p: { from: 0, to: 1 }, duration: dur, ease: 'inOut(1.6)', onUpdate: function () { paint(el); } }, devAt);
      if (shake) {
        tl.add(el, {
          rotate: [-1.5, 3, -4, 3.5, -2.5, 1.5, -1.5],
          x: [0, -6, 6, -5, 4, -2, 0],
          duration: 1100, ease: 'inOut(2)'
        }, devAt + dur * 0.12);
      }
      tl.call(function () {
        s.v.p = 1;
        paint(el);
        if (d.loop !== undefined) s.timer = setTimeout(function () {
          if (s.visible) develop(el); else s.waiting = true;
        }, d.hold !== undefined ? +d.hold : DEFAULTS.hold);
      }, devAt + dur);
      tl.play();
    }

    function init() {
      document.querySelectorAll('[data-instant-photo-develop]').forEach(function (el) {
        if (el._ipd) return;
        el._ipd = true;
        setup(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.develop = develop;
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<figure data-instant-photo-develop style="width: 240px">
  <img src="lake.jpg" alt="The lake at dawn">
  <figcaption>Lake, August</figcaption>
</figure>