Stinger Wipe
A stream-style transition between scenes. Slanted bands of colour race across, a wide panel with your logo slides in to cover the whole screen, the scene changes underneath while it is hidden, then the panel and its trailing bands sweep off the other side.
Just chattingLive from the studio
GameplayLevel 7: the ice caves
Be right backGrab a snack
How to use it
- Paste the snippet just before
</head>. - Add
data-stinger-wipeto a sized box whose children are the scenes. They are stacked to fill it, and the first one shows. - Add
data-loopto cycle through them, or callanimacious.stingerWipe(el)to wipe to the next scene.
Options
Add any of these attributes to the same element.
data-label | Text on the covering panel, such as your channel name. |
data-colors | Comma-separated band colours; the last is the panel. Default #f59e0b,#ec4899,#7c3aed. |
data-duration | How long the whole wipe takes. Default 1300 ms. |
data-direction | left (default) sweeps from right to left; right the other way. |
data-loop | Wipe to the next scene, over and over. |
data-hold | Time on each scene between wipes. Default 2200 ms. |
Without data-loop it wipes once, to the second scene, shortly after it comes into view. With reduced motion the scenes simply swap.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Stinger Wipe: a stream-style transition. Slanted colour bands race across
// the screen, a wide panel with your logo or name slides in to cover it
// completely, the scene underneath swaps while it is covered, then the
// panel and its trailing bands sweep off the other side.
// Usage: <div data-stinger-wipe data-label="PIXEL TV" style="height: 360px">
// <div>First scene</div>
// <div>Second scene</div>
// </div>
// Each child is one scene; they are stacked to fill the element, which
// needs a size. Without data-loop it plays once, to the second scene.
// Optional: data-label (text on the panel), data-colors (comma-separated
// band colours, the last one is the panel; default "#f59e0b,#ec4899,#7c3aed"),
// data-duration (ms for the whole wipe, default 1300), data-direction
// ("left", the default, sweeps right to left; "right" the other way),
// data-loop (wipe to the next scene over and over), data-hold (ms between
// wipes, default 2200).
// Script use: animacious.stingerWipe(el) wipes to the next scene now.
var DEFAULTS = { colors: '#f59e0b,#ec4899,#7c3aed', duration: 1300, hold: 2200 };
var css =
'.stgw{position:relative;overflow:hidden}' +
'.stgw-scene{position:absolute;inset:0;visibility:hidden}' +
'.stgw-scene.stgw-on{visibility:visible}' +
'.stgw-layer{position:absolute;inset:0;z-index:5;pointer-events:none;overflow:hidden;visibility:hidden}' +
'.stgw-band{position:absolute;top:-10%;bottom:-10%;left:0;will-change:transform}' +
'.stgw-panel{display:grid;place-items:center;box-shadow:0 0 2em rgba(0,0,0,.35)}' +
'.stgw-panel::before{content:"";position:absolute;inset:0;background:repeating-linear-gradient(90deg,rgba(255,255,255,.06) 0 2px,transparent 2px 22px)}' +
'.stgw-label{position:relative;display:flex;align-items:center;gap:0.5em;color:#fff;font-weight:900;font-size:clamp(16px,6cqw,44px);letter-spacing:0.04em;text-transform:uppercase;font-style:italic;white-space:nowrap;text-shadow:0 0.06em 0.2em rgba(0,0,0,.35)}' +
'.stgw-label i{display:block;width:0.9em;height:0.9em;border-radius:0.2em;background:#fff;transform:rotate(45deg) scale(.6);box-shadow:0 0 0.8em #fff}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function num(el, k) { var v = el.dataset[k]; return v !== undefined && v !== '' ? +v : DEFAULTS[k]; }
function setup(el) {
var d = el.dataset;
var scenes = [].slice.call(el.children);
if (!scenes.length) return false;
el.classList.add('stgw');
el.style.containerType = 'inline-size';
scenes.forEach(function (sc, i) {
sc.classList.add('stgw-scene');
if (i === 0) sc.classList.add('stgw-on');
else sc.setAttribute('aria-hidden', 'true');
});
var colors = (d.colors || DEFAULTS.colors).split(',').map(function (c) { return c.trim(); });
var panelColor = colors[colors.length - 1];
var accents = colors.slice(0, -1);
if (!accents.length) accents = [panelColor];
var layer = document.createElement('div');
layer.className = 'stgw-layer';
layer.setAttribute('aria-hidden', 'true');
// Bands in sweep order: two leading accents, the panel, two trailing ones.
var spec = [
{ c: accents[0], w: 0.14 },
{ c: accents[1 % accents.length], w: 0.26 },
{ c: panelColor, w: 1.7, panel: true },
{ c: accents[1 % accents.length], w: 0.12 },
{ c: accents[0], w: 0.07 }
];
var bands = spec.map(function (b) {
var band = document.createElement('div');
band.className = 'stgw-band' + (b.panel ? ' stgw-panel' : '');
band.style.background = b.panel
? 'linear-gradient(90deg,' + panelColor + ',color-mix(in srgb,' + panelColor + ' 70%,#000))'
: b.c;
if (b.panel) {
band.innerHTML = '<span class="stgw-label"><i></i><span></span></span>';
band.querySelector('.stgw-label > span').textContent = d.label || '';
if (!d.label) band.querySelector('.stgw-label').style.display = 'none';
}
layer.appendChild(band);
return { el: band, w: b.w, panel: !!b.panel };
});
el.appendChild(layer);
el._stgw = {
scenes: scenes, index: 0, bands: bands, layer: layer,
label: layer.querySelector('.stgw-label'), vis: false
};
return true;
}
function swap(el) {
var s = el._stgw;
var cur = s.scenes[s.index];
s.index = (s.index + 1) % s.scenes.length;
var next = s.scenes[s.index];
cur.classList.remove('stgw-on');
cur.setAttribute('aria-hidden', 'true');
next.classList.add('stgw-on');
next.removeAttribute('aria-hidden');
}
function wipe(el, done) {
var s = el._stgw;
if (s.busy) return;
if (reduced()) { swap(el); if (done) done(); return; }
s.busy = true;
var W = el.clientWidth, H = el.clientHeight;
var slant = H * 0.4; // horizontal reach of the slanted edges
var gap = Math.max(4, W * 0.015);
var dur = num(el, 'duration');
var inDur = dur * 0.42, outDur = dur * 0.42, pause = dur * 0.16;
// Lay the bands out in a row: each offset is its left edge relative to
// the panel's. The sweep runs right to left; "right" mirrors the layer.
var pi = 0, i, x;
for (i = 0; i < s.bands.length; i++) if (s.bands[i].panel) pi = i;
var panelW = s.bands[pi].w * W;
var offsets = [];
offsets[pi] = 0;
for (x = 0, i = pi - 1; i >= 0; i--) { x -= s.bands[i].w * W + gap; offsets[i] = x; }
for (x = panelW + gap, i = pi + 1; i < s.bands.length; i++) { offsets[i] = x; x += s.bands[i].w * W + gap; }
var start = W + slant - offsets[0]; // the leading band just off the right
var cover = (W - panelW) / 2; // the panel centred, covering it all
var end = -(x + slant); // the last band just off the left
var mirror = el.dataset.direction === 'right';
anime.utils.set(s.layer, { scaleX: mirror ? -1 : 1 });
s.bands.forEach(function (b, k) {
b.el.style.width = b.w * W + 'px';
b.el._stgwO = offsets[k];
anime.utils.set(b.el, { skewX: -20, x: start + offsets[k] });
});
anime.utils.set(s.label, { skewX: 20, scaleX: mirror ? -1 : 1, opacity: 0 });
s.layer.style.visibility = 'visible';
var targets = s.bands.map(function (b) { return b.el; });
var mid = inDur + 140;
var tl = anime.createTimeline();
tl.add(targets, {
x: function (t) { return cover + t._stgwO; },
duration: inDur, ease: 'out(3)', delay: anime.stagger(40)
}, 0)
.add(s.label, { opacity: [0, 1], scale: [0.7, 1], duration: 380, ease: 'outBack(2)' }, inDur * 0.6)
.call(function () { swap(el); }, mid)
.add(s.label, { opacity: 0, x: '-1.5em', duration: 220, ease: 'in(2)' }, mid + pause)
.add(targets, {
x: function (t) { return end + t._stgwO; },
duration: outDur, ease: 'in(3)', delay: anime.stagger(40, { reversed: true })
}, mid + pause);
tl.then(function () {
s.layer.style.visibility = 'hidden';
anime.utils.set(s.label, { x: 0 });
s.busy = false;
if (done) done();
});
s.tl = tl;
}
function cycle(el) {
var s = el._stgw;
clearTimeout(s.wait);
s.wait = setTimeout(function () {
if (!s.vis) { s.pending = true; return; }
wipe(el, function () { cycle(el); });
}, num(el, 'hold'));
}
window.animacious = window.animacious || {};
window.animacious.stingerWipe = function (el) {
if (el && el._stgw) wipe(el);
};
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var el = e.target, s = el._stgw;
s.vis = e.isIntersecting;
if (!s.vis) return;
var loop = el.dataset.loop !== undefined && el.dataset.loop !== 'false';
if (!s.started) {
s.started = true;
if (loop) cycle(el);
else setTimeout(function () { wipe(el); }, 900);
} else if (s.pending) { s.pending = false; cycle(el); }
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-stinger-wipe]').forEach(function (el) {
if (el._stgw) return;
if (!setup(el)) return;
if (!reduced()) io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-stinger-wipe data-label="Pixel TV" data-loop
style="height: 360px">
<div>Just chatting</div>
<div>Gameplay</div>
</div>