<div id="hex-a-portal"></div>
body, html {
  margin: 0;
  padding: 0;
}

.input-range-rv {
  vertical-align: middle;
  background-color: transparent;
  padding-top: 8px;
  padding-top: .5rem;
  padding-bottom: 8px;
  padding-bottom: .5rem;
  color: inherit;
  background-color: transparent;
  -webkit-appearance: none;
}

.input-range-rv::-webkit-slider-thumb {
  position: relative;
  width: 8px;
  width: .5rem;
  height: 20px;
  height: 1.25rem;
  cursor: pointer;
  margin-top: -8px;
  margin-top: -0.5rem;
  border-radius: 3px;
  background-color: #fff;
  -webkit-appearance: none;
}

/* Touch screen friendly pseudo element */
.input-range-rv::-webkit-slider-thumb:before {
  content: '';
  display: block;
  position: absolute;
  top: -8px;
  top: -0.5rem;
  left: -14px;
  left: -0.875rem;
  width: 36px;
  width: 2.25rem;
  height: 36px;
  height: 2.25rem;
  opacity: 0;
}

.input-range-rv::-moz-range-thumb {
  width: 8px;
  width: .5rem;
  height: 20px;
  height: 1.25rem;
  cursor: pointer;
  border-radius: 3px;
  border-color: transparent;
  border-width: 0;
  background-color: #fff;
}

.input-range-rv::-webkit-slider-runnable-track {
  height: 4px;
  height: 0.25rem;
  cursor: pointer;
  border-radius: 3px;
  background-color: rgba(255, 255, 255, .25);
}

.input-range-rv::-moz-range-track {
  height: 4px;
  height: 0.25rem;
  cursor: pointer;
  border-radius: 3px;
  background-color: rgba(255, 255, 255, .25);
}

.input-range-rv:focus {
  outline: none;
}
const degreesToRadians = angle => (Math.PI * angle) / 180;

const range = count => Array.from(Array(count).keys());

function polygon(sideCount, radius) {
  const theta = 360 / sideCount;
  const vertexIndices = range(sideCount);

  return vertexIndices.map(idx => ({
    theta: degreesToRadians(theta * idx),
    r: radius,
  }));
}

const pathDef = vertices =>
  `M${vertices.map(({ theta, r }) => [
    300 + (r * Math.cos(theta)),
    300 + (r * Math.sin(theta)),
  ]).join('L')}Z`;

class HexAPortal extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      sideCount: 6,
      radius: 150,
    };
  }

  componentDidMount() {
    const ease = Linear.easeNone;
    const tl = new TimelineMax({
      repeat: -1,
      onUpdate: () => {
        TweenMax.to([
          this.chaser1,
          this.chaser2,
          this.chaser3,
          this.chaser4,
          this.chaser5,
        ], 1, {
          strokeDashoffset: '-=450',
        });
      },
    });

    tl.to(this.canvas, 2, {
      rotation: -360,
      transformOrigin: '50% 50%',
      ease,
    });

    tl.timeScale(1);
  }

  update = (prop, value) => {
    this.setState({ [prop]: Number(value) });
  };

  render() {
    const { sideCount, radius } = this.state;

    return (
      <div className="bg-black vh-100 ph4 flex flex-column">
        <svg
          className="mw7 center mb4 db flex-auto"
          ref={(canvas) => { this.canvas = canvas; }}
          viewBox="0 0 600 600" 
          fill="none" 
        >
          <defs>
            <filter id="glow" y="-50%" height="200%">
              <feGaussianBlur stdDeviation="12 1" result="coloredBlur" />
              <feGaussianBlur stdDeviation="12 1" result="coloredBlur" />
              <feGaussianBlur stdDeviation="12 1" result="coloredBlur" />
              <feMerge>
                <feMergeNode in="coloredBlur" />
                <feMergeNode in="SourceGraphic" />
              </feMerge>
            </filter>
            <path
              id="hexPath"
              d={pathDef(polygon(sideCount, radius))}
              fill="none"
              strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"
              strokeMiterlimit="10" strokeDasharray="112"
            />
          </defs>
          <g filter="url(#glow)" >
            <use
              xlinkHref="#hexPath"
              ref={(p) => { this.chaser1 = p; }}
              stroke="#FFFFFF" strokeDashoffset="10100"
            />
            <use
              xlinkHref="#hexPath"
              ref={(p) => { this.chaser2 = p; }}
              stroke="#0000FF" strokeDashoffset="10060"
            />
            <use
              xlinkHref="#hexPath"
              ref={(p) => { this.chaser3 = p; }}
              stroke="#00FF00" strokeDashoffset="10040"
            />
            <use
              xlinkHref="#hexPath"
              ref={(p) => { this.chaser4 = p; }}
              stroke="#FF0000" strokeDashoffset="10020"
            />
            <use
              xlinkHref="#hexPath"
              ref={(p) => { this.chaser5 = p; }}
              stroke="#1d1d1d" strokeDashoffset="10000"
            />
          </g>
        </svg>
        <div className="flex mb4 mw7 center">
          <label htmlFor="sideCount" className="f5 ttu tracked b w5 tr dib mr3 white">
            Number of Sides <span className="gray">({ sideCount })</span>
          </label>
          <input
            name="sideCount" type="range" className="input-range-rv flex-auto white"
            min="3" max="16" step="1"
            value={sideCount}
            onChange={e => this.update('sideCount', e.target.value)}
          />
        </div>
        <div className="flex mb5 mw7 center">
          <label htmlFor="radius" className="f5 ttu tracked b w5 tr dib mr3 white">
            Radius <span className="gray">({ radius })</span>
          </label>
          <input
            name="radius" type="range" className="input-range-rv flex-auto white"
            min="100" max="300" step="1"
            value={radius}
            onChange={e => this.update('radius', e.target.value)}
          />
        </div>
      </div>
    );
  }
}


ReactDOM.render(
  <HexAPortal />,
  document.getElementById('hex-a-portal')
);
View Compiled

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/tachyons/4.6.2/tachyons.min.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.2/TweenMax.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react.js
  3. https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react-dom.js