<h1 class="banner">
  <span class="flag"></span>
  <span class="flag">W</span>
  <span class="flag">3</span>
  <span class="flag">C</span>
  <span class="flag">P</span>
  <span class="flag">L</span>
  <span class="flag">U</span>
  <span class="flag">S</span>
  <span class="flag">!</span>
  <span class="flag"></span>
  <span class="string">
    <svg width="800" height="230" viewBox="0 0 800 230">
      <path fill="none" d="M0,0 C100,100 700,200 800,100" stroke="#ffffff" stroke-dasharray="5 3"/>
    </svg>
  </span>
</h1>
@import url('https://fonts.googleapis.com/css?family=Gochi+Hand');

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0;
  background-color: #291642;
  font-family: 'Gochi Hand', sans-serif;
  color: #fff;
  font-size: 130%;
  letter-spacing: 0.1rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 100vw;
  min-height: 100vh;
}

.banner {
  width: 800px;
  height: 100px;
  border: 0px dotted cyan;
  position: relative;
  transform-style: preserve-3d;
  transform: var(--transform);
  --transform: scale(1);
  display: none;
}
.banner {
  display: flex;
  justify-content: space-between;
}
.flag {
  display: flex;
  height: 70px;
  width: 50px;
  background: hsl(var(--hue,43), 90%, 55%);
  color: hsl(43, 90%, var(--text,5%));
  clip-path: polygon(0% 0%, 100% 0%, 50% 100%);
  transform-origin: 50% 0%;
  font-size: 1.5rem;
  
  justify-content: center;
  align-items: center;
  padding-bottom: 1rem;
}
.string {
  display: none;
}
.flag:nth-of-type(odd) {
  --hue: 343;
  --text: 95%;
}
.banner:nth-of-type(even) .flag:nth-of-type(even) {
  --hue: 333;
}
.banner:nth-of-type(even) .flag:nth-of-type(odd) {
  --hue: 193;
}

@supports (offset-path: path('M0,0 C100,100 700,200 800,100')) {
  .banner {
    height: 230px;
  }
  .banner:nth-of-type(even) {
    --transform: rotate(0deg);
  }
  .banner:nth-of-type(2) {
    --transform: scaleX(-1) rotate(-8deg);
  }
  .banner:nth-of-type(1) {
    --transform: rotate(-4deg);
  }
  .flag:not(.string) {
    position: absolute;
    offset-anchor: 50% 0%;
    offset-path: path('M0,0 C100,100 700,200 800,100');
  }
}

@supports (d: path('M0,0 C100,100 700,200 800,100')) {
  .string,
  .string svg {
    position: absolute;
    width: 800px;
    top: 0;
    left: 0;
    height: 230px;
    display: block;
    background: transparent;
    clip-path: none;
  }
  .string path {
    stroke: hsla(283, 60%, 80%, .5);
    stroke-width: 1px;
    d: path('M0,0 C100,100 700,200 800,100');
  }
}
View Compiled
const strands = Array.from(document.querySelectorAll('.banner'));
const duration = 5450;
const supportsOffsetPath = 
        window.CSS
        && CSS.supports
        && CSS.supports('offset-path', "path('M0,0 L1,1')");
const rxRandomNegative = -20;
const rxRandomNegativeBase = -30;
const rxRandomPositive = 40;
const rxRandomPositiveBase = 30;


if (document.documentElement.animate) {
  strands.forEach(animateStrands);
}


function animateStrands(strand) {
  let flags = Array.from(strand.querySelectorAll('.flag'));
  let strandPathDuration = Math.random() * (2 * duration) + duration;
  let fromPath = "path('M0,0 C100,100 700,200 800,100')";
  let toPath = `path('M0,0 C${Math.random() * 20 + 80},${Math.random() * 20 + 80} ${Math.random() * -50 + 700},${Math.random() * 100} 800,100')`;

  flags.forEach((flag, i) => {
    flag.style.offsetDistance = `${80 + i * 740 / flags.length}px`;
    animateWindRotate(flag);
    animateWindCurve(flag, fromPath, toPath, strandPathDuration);
  });

  if (supportsOffsetPath) {
    animateStringInWind(strand, fromPath, toPath, strandPathDuration);
  }
}

function animateWindRotate(flag) {
  flag.animate(getRandomizedFlagFrames(), {
    duration: duration,
    iterations: Infinity,
    direction: 'alternate',
    delay: 1000 * Math.random() - 1000
  });
}
function animateWindCurve(flag, fromPath, toPath, strandPathDuration) {
  flag.animate([
    {offsetPath: fromPath},
    {offsetPath: toPath}
  ], {
    duration: strandPathDuration,
    iterations: Infinity,
    easing: 'ease-in-out',
    direction: 'alternate'
  });
}
function animateStringInWind(strand, fromPath, toPath, strandPathDuration) {
  let stringy = strand.querySelector('.string path');
  if (stringy) {
    stringy.animate([
      {d: fromPath},
      {d: toPath}
    ], {
      duration: strandPathDuration,
      iterations: Infinity,
      easing: 'ease-in-out',
      direction: 'alternate'
    });
  }
}




function getRandomizedFlagFrames() {
    let easing1 = `cubic-bezier(${Math.random() * .1 + .3},0,${Math.random() * .1 + .3},${Math.random() * .15 + .95})`;
    let easing2 = `cubic-bezier(${Math.random() * .1 + .3},0,${Math.random() * .1 + .3},${Math.random() * .15 + .95})`
  return [
      { 
        transform: 'rotateX(0deg)',
        filter: 'grayscale(5%)'
      }, { 
        transform: `rotateX(${Math.random() * rxRandomNegative + rxRandomNegativeBase}deg)`,
        filter: 'grayscale(25%)', //shadows for when rotating away from you
        easing: easing1
      }, {
        transform: `rotateX(${Math.random() * rxRandomPositive + rxRandomPositiveBase}deg)`,
        filter: 'grayscale(0%)',
        easing: easing1
      }, {
        transform: `rotateX(${Math.random() * rxRandomNegative + rxRandomNegativeBase}deg)`,
        filter: 'grayscale(25%)',
        easing: easing2
      }, {
        transform: `rotateX(${Math.random() * rxRandomPositive + rxRandomPositiveBase}deg)`,
        filter: 'grayscale(0%)',
        easing: easing2
      }
    ]
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.