← Broadcast

Corner Logo Bug

The small channel logo that sits in the corner of the screen. It fades in and rests a little see-through so it never gets in the way. Every so often it brightens, a glint of light sweeps across it and a star twinkles on its corner.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-corner-logo-bug to your logo (text, an <img> or an <svg>), and style it as you like. Round corners on it round the glint too.
  3. Add data-corner="top-right" to pin it to that corner of its nearest positioned parent, or place it yourself.

Options

Add any of these attributes to the same element.

data-cornertop-right, top-left, bottom-right or bottom-left.
data-insetGap from the corner, with data-corner. Default 4%.
data-everyTime between glints. Default 6000 ms.
data-opacityHow see-through it rests. Default 0.8.
data-spark="false"Leave out the twinkling star.

Call animacious.cornerLogoBug(el) to glint it now, for example when a segment starts.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Corner Logo Bug: the small channel logo that sits in the corner of a
    // broadcast. It fades in, then rests slightly see-through; every so often it
    // brightens, a glint of light sweeps across it and a little star twinkles
    // on its corner.
    // Usage: <div data-corner-logo-bug>PIXEL TV</div>
    // Put any logo inside (text, an <img> or an <svg>). With data-corner it
    // places itself in that corner of its nearest positioned parent; without
    // it, place it yourself.
    // Optional: data-corner ("top-right", "top-left", "bottom-right" or
    // "bottom-left"), data-inset (gap from the corner, default "4%"),
    // data-every (ms between glints, default 6000), data-opacity (resting
    // opacity, default 0.8), data-spark="false" (no twinkling star).
    // Script use: animacious.cornerLogoBug(el) glints it now.
    var DEFAULTS = { every: 6000, opacity: 0.8, inset: '4%' };

    var css =
      '.clb{display:inline-flex;align-items:center;isolation:isolate}' +
      '.clb-clip{position:absolute;inset:0;border-radius:inherit;overflow:hidden;pointer-events:none;z-index:2}' +
      '.clb-glint{position:absolute;top:-30%;bottom:-30%;left:0;width:45%;transform:translateX(-300%) skewX(-22deg);' +
        'background:linear-gradient(90deg,transparent,rgba(255,255,255,.12) 30%,rgba(255,255,255,.6) 50%,rgba(255,255,255,.12) 70%,transparent)}' +
      '.clb-glint2{width:14%;opacity:.7}' +
      '.clb-spark{position:absolute;right:-0.45em;top:-0.45em;width:0.9em;height:0.9em;z-index:3;pointer-events:none;fill:#fff;filter:drop-shadow(0 0 0.25em rgba(255,255,255,.9));opacity:0}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    var STAR = '<svg class="clb-spark" viewBox="0 0 20 20" aria-hidden="true"><path d="M10 0c.6 5.6 4.4 9.4 10 10-5.6.6-9.4 4.4-10 10-.6-5.6-4.4-9.4-10-10C5.6 9.4 9.4 5.6 10 0z"/></svg>';

    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;
      el.classList.add('clb');
      if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
      if (d.corner) {
        var inset = d.inset || DEFAULTS.inset;
        var c = d.corner.split('-');
        el.style.position = 'absolute';
        el.style[c[0] === 'bottom' ? 'bottom' : 'top'] = inset;
        el.style[c[1] === 'left' ? 'left' : 'right'] = inset;
        el.style.zIndex = '4';
      }
      el.insertAdjacentHTML('beforeend',
        '<span class="clb-clip" aria-hidden="true"><span class="clb-glint"></span><span class="clb-glint clb-glint2"></span></span>' +
        (d.spark === 'false' ? '' : STAR));
      el._clb = {
        glints: [].slice.call(el.querySelectorAll('.clb-glint')),
        spark: el.querySelector('.clb-spark'),
        vis: false
      };
      if (reduced()) el.style.opacity = String(num(el, 'opacity'));
      else anime.utils.set(el, { opacity: 0 });
    }

    function glint(el) {
      var s = el._clb;
      var rest = num(el, 'opacity');
      var tl = anime.createTimeline();
      var w = el.offsetWidth;
      tl.add(el, { opacity: [rest, 1, 1, rest], scale: [1, 1.04, 1], duration: 1500, ease: 'inOut(2)' }, 0);
      s.glints.forEach(function (g, i) {
        var gw = g.offsetWidth + el.offsetHeight;   // clear the slant too
        anime.utils.set(g, { x: -gw, skewX: -22 });
        tl.add(g, { x: [-gw, w + el.offsetHeight], duration: 900, ease: 'inOut(2)' }, 150 + i * 110);
      });
      if (s.spark) {
        tl.add(s.spark, {
          opacity: [0, 1, 0], scale: [0, 1.15, 0], rotate: [0, 180],
          duration: 760, ease: 'inOut(2)'
        }, 720);
      }
      return tl;
    }

    function schedule(el) {
      var s = el._clb;
      clearTimeout(s.wait);
      s.wait = setTimeout(function () {
        if (!s.vis) { s.pending = true; return; }
        glint(el);
        schedule(el);
      }, num(el, 'every'));
    }

    function enter(el) {
      var rest = num(el, 'opacity');
      anime.animate(el, { opacity: [0, rest], scale: [0.85, 1], duration: 700, ease: 'out(3)' });
      setTimeout(function () { glint(el); schedule(el); }, 800);
    }

    window.animacious = window.animacious || {};
    window.animacious.cornerLogoBug = function (el) {
      if (el && el._clb && !reduced()) glint(el);
    };

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var el = e.target, s = el._clb;
          s.vis = e.isIntersecting;
          if (!s.vis) return;
          if (!s.started) { s.started = true; enter(el); }
          else if (s.pending) { s.pending = false; glint(el); schedule(el); }
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-corner-logo-bug]').forEach(function (el) {
        if (el._clb) return;
        setup(el);
        if (!reduced()) io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div style="position: relative">
  <video src="show.mp4" autoplay muted></video>
  <div class="logo" data-corner-logo-bug data-corner="top-right">Pixel TV</div>
</div>