<canvas id="canvas"></canvas>
body {
  position: relative;
  margin: 0;
  height: 100vh;
}

#canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

import { 
  Renderer, Camera, Transform, Mesh,
  Polyline, Color, Orbit, Vec2, Vec3, Post
} from "https://cdn.skypack.dev/[email protected]";

const canvas = document.getElementById('canvas');

const vertex = /* glsl */ `
  precision highp float;
  attribute float aOffset;
  attribute vec3 position;
  attribute vec3 next;
  attribute vec3 prev;
  attribute vec2 uv;
  attribute float side;
  uniform mat4 modelMatrix;
  uniform mat4 modelViewMatrix;
  uniform mat4 projectionMatrix;
  uniform mat3 normalMatrix;
  uniform vec2 uResolution;
  uniform float uDPR;
  uniform float uThickness;
  uniform float uThicknessMax;
  uniform float uMiter;
  uniform float uTime;
  uniform float uDuration;
  uniform float uProgress;
  uniform float uDir;
  
  varying vec2 vUv;
  varying vec2 vNormal;
  varying float vDepth;
  
  // ================================================
  // Simplex 2D noise
  // https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83
  vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); }

  float snoise(vec2 v) {
    const vec4 C = vec4(0.211324865405187, 0.366025403784439,
             -0.577350269189626, 0.024390243902439);
    vec2 i  = floor(v + dot(v, C.yy) );
    vec2 x0 = v -   i + dot(i, C.xx);
    vec2 i1;
    i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
    vec4 x12 = x0.xyxy + C.xxzz;
    x12.xy -= i1;
    i = mod(i, 289.0);
    vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
    + i.x + vec3(0.0, i1.x, 1.0 ));
    vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy),
      dot(x12.zw,x12.zw)), 0.0);
    m = m*m ;
    m = m*m ;
    vec3 x = 2.0 * fract(p * C.www) - 1.0;
    vec3 h = abs(x) - 0.5;
    vec3 ox = floor(x + 0.5);
    vec3 a0 = x - ox;
    m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
    vec3 g;
    g.x  = a0.x  * x0.x  + h.x  * x0.y;
    g.yz = a0.yz * x12.xz + h.yz * x12.yw;
    return 130.0 * dot(m, g);
  }

  float easeInQuad(float t) {
    return t * t;
  }
  
  float easeOutQuad(float t) {
    return t * (2.0 - t);
  }
  
  float easeInCubic(float t) {
    return t * t * t;
  }
  
  float easeOutCubic(float t) {
    float x = 1.0 - t;
    return 1.0 - x * x * x;
  }

  void main() {
    float progress = fract((uTime + aOffset) / uDuration);
    float ease;
    if (progress < 0.5) {
      ease = easeInQuad(progress * 2.0);
    }
    else {
      ease = 1.0 - easeOutQuad((progress - 0.5) * 2.0);
    }
    
    float thickness = mix(uThicknessMax, uThickness, aOffset);
    float scale = mix(mix(0.5, 1.0, aOffset), 1.0, ease);
    float instOffset = mix(0.25, 1.0, aOffset);
    vec3 cPosition = position * instOffset * scale;
    vec3 nPosition = next * instOffset * scale;
    vec3 pPosition = prev * instOffset * scale;
  
    float noiseAmp = 0.1;
    float currNoise = snoise(cPosition.xy * 1.5 - uTime * 0.25) * noiseAmp;
    float nextNoise = snoise(nPosition.xy * 1.5 - uTime * 0.25) * noiseAmp;
    float prevNoise = snoise(pPosition.xy * 1.5 - uTime * 0.25) * noiseAmp;
    
    mat4 mvp = projectionMatrix * modelViewMatrix;
    vec4 currPos = mvp * vec4(cPosition * (1.0 + currNoise), 1);
    vec4 nextPos = mvp * vec4(nPosition * (1.0 + nextNoise), 1);
    vec4 prevPos = mvp * vec4(pPosition * (1.0 + prevNoise), 1);
    
    vec2 aspect = vec2(uResolution.x / uResolution.y, 1);
    vec2 currScreen = currPos.xy / currPos.w * aspect;
    vec2 nextScreen = nextPos.xy / nextPos.w * aspect;
    vec2 prevScreen = prevPos.xy / prevPos.w * aspect;

    vec2 dirPrev = normalize(currScreen - prevScreen);
    vec2 dirNext = normalize(nextScreen - currScreen);
    vec2 dir2 = normalize(dirPrev + dirNext);

    vec2 normal = vec2(-dir2.y, dir2.x);
    normal /= mix(1.0, max(0.3, dot(normal, vec2(-dirPrev.y, dirPrev.x))), uMiter);
    normal /= aspect;
    float pixelWidthRatio = 1.0 / (uResolution.y / uDPR);
    float pixelWidth = currPos.w * pixelWidthRatio;
    normal *= pixelWidth * thickness;
    currPos.xy -= normal * side;

    vDepth = currNoise / noiseAmp * 0.5 + 0.5;
    vUv = uv;
    gl_Position = currPos;
  }
`;

const fragment = /* glsl */ `
  precision highp float;
  uniform float uTime;
  uniform vec3 uColor1;
  uniform vec3 uColor2;

  varying vec2 vUv;
  varying float vDepth;
  void main() {
    float shading = vDepth * vDepth * 0.9 + 0.1;
    gl_FragColor.rgb = mix(uColor1, uColor2, vDepth) * shading;
    gl_FragColor.a = 1.0;
  }
`;

const renderer = new Renderer({ canvas, antialias: true });
const gl = renderer.gl;
gl.clearColor(0.0, 0.0, 0.0, 1);

const camera = new Camera(gl, { fov: 15 });
camera.position.set(0, 0, 10);

const controls = new Orbit(camera, {
  target: new Vec3(0, 0, 0),
});

function resize() {
  const { innerWidth: width, innerHeight: height } = window;
  renderer.setSize(width, height);
  camera.perspective({ aspect: width / height });
}
window.addEventListener('resize', resize, false);
resize();

const scene = new Transform();

const segsPerLine = 512;
const points = [];
for (let i = 0; i <= segsPerLine; i++) {
  const a = 2 * Math.PI * i / segsPerLine;
  const x = Math.cos(a);
  const y = Math.sin(a);
  points.push(new Vec3(x, y, 0));
}

const instancesTotal = 16;
const offsets = new Float32Array(instancesTotal);
for (let i = 0; i < instancesTotal; i++) {
  const offs = i / (instancesTotal - 1);
  offsets[i] = offs;
}

const polyline = new Polyline(gl, {
  points, vertex, fragment,
  uniforms: {
    uColor1: { value: new Color('#e91e63') },
    uColor2: { value: new Color('#673ab7') },
    uTime: { value: 0 },
    uDuration: { value: 4 },
    uProgress: { value: 0 },
    uThickness: { value: 1 },
    uThicknessMax: { value: 4 },
  },
  attributes: {
    aOffset: { instanced: 1, size: 1, data: offsets }
  }
});

const mesh = new Mesh(gl, polyline);
mesh.setParent(scene);

let begin = 0;

requestAnimationFrame(update);
function update(now) {
  begin = begin || now;
  mesh.program.uniforms.uTime.value = (now - begin) * 0.001;
  controls.update();
  renderer.render({ scene, camera });
  requestAnimationFrame(update);
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.