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.
Tap the photo to take another
How to use it
- Paste the snippet just before
</head>. - Add
data-instant-photo-developto 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-duration | Time 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-loop | Swap in a fresh print and develop again. |
data-hold | How 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.
<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><figure data-instant-photo-develop style="width: 240px">
<img src="lake.jpg" alt="The lake at dawn">
<figcaption>Lake, August</figcaption>
</figure>