Holographic Foil Text
The letters look stamped in rainbow foil, like a trading card or a gift wrap. Move the pointer, scroll the page or tilt your phone and the colours slide across them, a bright glare follows the light and the text tips gently towards you.
Holographic
How to use it
- Paste the snippet just before
</head>. - Add
data-holographic-foil-textto a big, bold heading. The foil shows best on a dark background.
Options
Add any of these attributes to the same element.
data-tilt | How far the text tips with the light, in degrees. Default 8; 0 keeps it flat. |
data-colors | Foil colours, separated by commas. Default a pastel rainbow. |
data-autoplay | Let the light drift around by itself whenever no real pointer is moving. Handy for demos and touch screens. |
On most Android phones the foil also follows how the phone is held. iPhones ask permission for that, so there it follows touches and scrolling.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Holographic Foil Text: the letters look stamped in rainbow foil. The
// colours slide across them, a bright glare follows the light and the text
// tilts a little, all steered by the pointer, the page scroll and (on
// phones that allow it) the way the device is held.
// Usage: <h1 data-holographic-foil-text>Holographic</h1>
// Optional: data-tilt (how far the text tilts, in degrees; default 8; 0
// keeps it flat), data-colors (comma-separated foil colours; default a
// pastel rainbow), data-autoplay (the light drifts around by itself while
// no real pointer is moving; handy for demos and touch screens).
var DEFAULTS = {
tilt: 8,
colors: ['#ff8fcf', '#ffd879', '#9dffbf', '#7fd4ff', '#b79bff', '#ff8fcf']
};
var css =
'.hof-face{display:inline-block;color:transparent;-webkit-background-clip:text;background-clip:text;' +
'-webkit-text-fill-color:transparent;transform:perspective(600px) rotateX(var(--hof-rx,0deg)) rotateY(var(--hof-ry,0deg));' +
'background-image:radial-gradient(circle at var(--hof-gx,50%) var(--hof-gy,40%),rgba(255,255,255,.95),rgba(255,255,255,0) 38%),' +
'repeating-linear-gradient(62deg,rgba(255,255,255,0) 0 .05em,rgba(255,255,255,.28) .05em .075em),' +
'var(--hof-rainbow);background-blend-mode:normal,overlay,normal;' +
'filter:drop-shadow(0 .03em .05em rgba(0,0,0,.25));will-change:transform}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
// A repeating rainbow, 3.2em per cycle, slid along by `shift` em.
function rainbow(colors, shift) {
var off = ((shift % 3.2) + 3.2) % 3.2;
var stops = colors.map(function (c, i) {
return c + ' ' + (off + i * 3.2 / (colors.length - 1)).toFixed(3) + 'em';
});
return 'repeating-linear-gradient(115deg,' + stops.join(',') + ')';
}
function setup(el) {
var d = el.dataset;
var tilt = d.tilt !== undefined ? +d.tilt : DEFAULTS.tilt;
var colors = d.colors ? d.colors.split(',').map(function (c) { return c.trim(); }) : DEFAULTS.colors;
if (colors[0] !== colors[colors.length - 1]) colors = colors.concat(colors[0]);
var autoplay = d.autoplay !== undefined && d.autoplay !== 'false';
// The foil goes on an inner span so the element keeps its own display.
var face = document.createElement('span');
face.className = 'hof-face';
while (el.firstChild) face.appendChild(el.firstChild);
el.appendChild(face);
// Paint the foil for a light direction (x, y), each from -1 to 1, plus a
// scroll-driven slide s (in em).
function paint(x, y, s) {
face.style.setProperty('--hof-rainbow', rainbow(colors, x * 1.6 + s));
face.style.setProperty('--hof-gx', (50 + x * 45).toFixed(1) + '%');
face.style.setProperty('--hof-gy', (45 + y * 45).toFixed(1) + '%');
face.style.setProperty('--hof-ry', (x * tilt).toFixed(2) + 'deg');
face.style.setProperty('--hof-rx', (-y * tilt).toFixed(2) + 'deg');
}
if (reduced()) { paint(-0.35, -0.3, 0); return; }
var target = { x: 0, y: 0 };
var cur = { x: 0, y: 0 };
var lastReal = -1e9;
var clock = 0;
function onPointer(e) {
var r = el.getBoundingClientRect();
if (r.bottom < 0 || r.top > innerHeight) return;
var reach = Math.max(160, Math.min(r.width, innerWidth) * 0.6);
target.x = Math.max(-1, Math.min(1, (e.clientX - (r.left + r.width / 2)) / reach));
target.y = Math.max(-1, Math.min(1, (e.clientY - (r.top + r.height / 2)) / reach));
lastReal = performance.now();
}
window.addEventListener('pointermove', onPointer, { passive: true });
window.addEventListener('pointerdown', onPointer, { passive: true });
// Phones that report orientation without asking (most Android browsers).
window.addEventListener('deviceorientation', function (e) {
if (e.gamma == null || e.beta == null) return;
target.x = Math.max(-1, Math.min(1, e.gamma / 30));
target.y = Math.max(-1, Math.min(1, (e.beta - 45) / 30));
lastReal = performance.now();
});
var prev = 0;
var ticker = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
var dt = prev ? Math.min(50, now - prev) : 16;
prev = now;
clock += dt;
if (autoplay && now - lastReal > 2000) {
target.x = Math.sin(clock * 0.0009) * 0.9;
target.y = Math.sin(clock * 0.0014 + 1) * 0.6;
}
var ease = 1 - Math.pow(0.9, dt / 16);
cur.x += (target.x - cur.x) * ease;
cur.y += (target.y - cur.y) * ease;
paint(cur.x, cur.y, window.scrollY * 0.004);
}
});
paint(0, 0, 0);
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { prev = 0; ticker.play(); }
else ticker.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-holographic-foil-text]').forEach(function (el) {
if (el._hof) return;
el._hof = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-holographic-foil-text>Holographic</h1>
<!-- gold and silver foil, no tilt: -->
<h1 data-holographic-foil-text data-tilt="0" data-colors="#f7e7a1,#d4af37,#fff6d5,#c0c0c0">Limited edition</h1>