<div id="sky_div">
  <img id="sky" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/409445/sky.png">
</div>

<div id="svg_container">
  <svg id="svg" viewBox="0 0 100 100";></svg>
</div>

<div id="viewbox_overlay">
  <div id="flagpole"></div> 
</div>

#sky_div {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
  overflow: hidden;
}

#sky {
  position: absolute;
  height: 100%;
  right: 0;
}

#svg_container { 
  width: 80%;
  height: 80%;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

svg {
  width: 100%;
  height: 100%;
}

#viewbox_overlay {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

#flagpole {
  position: relative;
  width: 2%;
  top: 34%;
  left: 26.5%;
  background: hsl(20, 100%, 15%);
  border: 1px solid black;
  border-bottom: 0;
  border-radius: 4px 4px 0 0;
}


//////////////////////////////////////////////// 
////////////    ANIMATED SVG FLAG    /////////// 
////////////////////////////////////////////////



///---INITIATION---///
 
//HTML variables
var svg = document.getElementById("svg");
var skyDiv = document.getElementById("sky_div");
var shadowsAndFlagpoleDiv = document.getElementById("viewbox_overlay");
var flagpole = document.getElementById("flagpole");

//settings
var displayPoints = false;
var displaySpans = false;
var displaySkins = true;
var changeColor = true;
var fw = 45;  // flag width (as percentage of svg viewbox width)
var fh = 30;  // flag height (as percentage of svg viewbox height) 
var fwtc = fw+1;  // flag width thread count
var fhtc = fh+1;  // flag height thread count
var rigidity = 50;  // (iterations of position-accuracy refinement)
var gravity = 0.001;  // (rate of y-velocity increase per frame)
var wind = 0.1;  // (rate of x-velocity increase per frame)

//flag components
var pointCount = 0;
var points = [];
var spanCount = 0;
var spans = [];
var skinCount = 0;
var skins = [];
var sgf = 0.1; // skin gap fill (fills gaps between skins)

//other components
var frameCount = 0;
var skyPosition = 0;
scaleHtmlElements();



///---OBJECTS---///4

//POINTS

//point constructor
function Point(current_x, current_y) {
  this.cx = current_x;
  this.cy = current_y; 
  this.px = this.cx;  // previous x value
  this.py = this.cy;  // previous y value
  this.pinned = false;
  this.id = pointCount; 
  pointCount += 1;
}

//adds a new point object instance to points array
function addPt(x,y) {
  points.push( new Point(x,y) ); 
}

//creates flag points
for (i=0; i<fwtc; i++) {
  var x = (i*fh/(fhtc-1))+(100-fw)/2;  // centers flag horizontally
  for (j=0; j<fhtc; j++) {
    var y = (j*fw/(fwtc-1))+(100-fh)/2;  // centers flag vertically
    addPt(x,y);
  }
}

//pins points' left column
for (i=0; i<fhtc; i++) { points[i].pinned = true; }

//adds points to viewbox
placePoints();


//SPANS

//span constructor
function Span(point_1, point_2) {
  this.p1 = point_1;
  this.p2 = point_2;
  this.l = distanceBetween(this.p1,this.p2); // length
  this.id = spanCount;
  spanCount += 1;
}

//adds a new span object instance to spans array
function addSp(p1,p2) {
  spans.push( new Span( getPt(p1), getPt(p2) ) );
}

//creates flag spans
for (i=0; i<points.length-1; i++) {
  if ( (i+1) % fhtc !== 0 ) { 
    addSp(i,i+1);  // vertical spans
    if ( (i+2) % fhtc !== 0) { 
      addSp(i,i+2);  // vertical span reinforcements
    } 
  }  
  if ( i < points.length-fhtc) { addSp(i,i+fhtc); }  // horizontal spans  
}

//adds spans to viewbox
placeSpans();


//SKINS

//skin constructor
function Skin(points_array) { 
  this.pa = points_array;
  this.lh = 0;  // left side hue
  this.rh = 0;  // right side hue
  this.ll = 50;  // left side lightness
  this.rl = 50;  // right side lightness
  this.id = skinCount;
  skinCount += 1;
}

//adds a new skin object instance to skins array
function addSk(points_array) {
  skins.push( new Skin(points_array) );
}

//creates flag skins
for (i=1; i<=fwtc-1; i++) {  // creates skins as columns
  var points_array = [];
  for (j=fhtc*(i-1); j<fhtc*i; j++) { points_array.push( points[j] ); }  // adds all left side points
  for (k=fhtc*(i+1)-1; k>=fhtc*i; k--) { points_array.push( points[k] ); }  // adds all right side points
  points_array.push( points[fhtc*(i-1)] );  // connects path back to first point
  addSk(points_array);
  points_array = [];
}

//adds skin hues
var hcr = 0.25;  // hue change rate (per frame)
var hr = 30;//0.1;  // hue range (from flag's left to right sides; full spectrum = 360)
var hsw = hr/(skins.length-1);  // hue stop width (hue increase from skins' left to right sides)
var clh = 0;  // column left hue 
var crh = 0;  // column right hue
if (changeColor) { 
  for (i=0; i<skins.length; i++) {     
    clh = crh;
    crh += hsw;
    skins[i].lh = clh;
    skins[i].rh = crh;
  }
}

//adds skins to viewbox
placeSkins();



///---FUNCTIONS---///

//scales html elements to match SVG viewbox scaling
function scaleHtmlElements() { 
  if (window.innerWidth > window.innerHeight) {
    shadowsAndFlagpoleDiv.style.height = window.innerHeight*0.8+"px";
    shadowsAndFlagpoleDiv.style.width = shadowsAndFlagpoleDiv.style.height;
    skyDiv.style.height = window.innerHeight*0.8+"px";
    skyDiv.style.width = shadowsAndFlagpoleDiv.style.height;
  } else {
    shadowsAndFlagpoleDiv.style.width = window.innerWidth*0.8+"px";
    shadowsAndFlagpoleDiv.style.height = shadowsAndFlagpoleDiv.style.width;
    skyDiv.style.width = window.innerWidth*0.8+"px";
    skyDiv.style.height = shadowsAndFlagpoleDiv.style.width;
  }
  var fbcr = flagpole.getBoundingClientRect();
  flagpole.style.height = (window.innerHeight - fbcr.top) + "px";
}

//generates random number between a minimum and maximum value
function randNumBetween(min,max) {
  return Math.floor(Math.random()*(max-min+1))+min;
}

//gets a point by id number
function getPt(id) {
  for (var i=0; i<points.length; i++) { 
    if (points[i].id == id) { return points[i]; }
  }
}

//gets distance between two points (pythogorian theorum)
function distanceBetween(point_1, point_2) {
  var x_difference = point_2.cx - point_1.cx;
  var y_difference = point_2.cy - point_1.cy;
  return Math.sqrt( x_difference*x_difference + y_difference*y_difference);
}

//adds points to the viewbox as svg circle elements 
function placePoints() {
  if (displayPoints) {
    for (var i=0; i<points.length; i++) {
      var p = points[i];
      var circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
      circle.classList.add("point");
      circle.id = "p"+p.id;
      circle.style.r = ".2"; 
      circle.style.cx = p.cx+"px";
      circle.style.cy = p.cy+"px";
      circle.style.fill = "green";
      svg.appendChild(circle);
    }
  }
}

//adds spans to the viewbox as svg path elements
function placeSpans() {
  if (displaySpans) {
    for (var i=0; i<spans.length; i++) {
      var sp = spans[i];
      var line = document.createElementNS("http://www.w3.org/2000/svg", "path");
      line.classList.add("span");
      line.id = "sp"+sp.id;
      line.setAttribute("d", "M" + sp.p1.cx + "," + sp.p1.cy + " L" + sp.p2.cx + "," + sp.p2.cy); 
      line.style.stroke = "black";
      line.style.strokeWidth = ".1";
      line.style.strokeLinecap = "round";
      svg.appendChild(line);
    }
  }
}

//adds skins to the viewbox as svg path elements
function placeSkins() {
  if (displaySkins) {
    for (var i=0; i<skins.length; i++) {
      var sk = skins[i];
      var defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); 
      var linearGradient = document.createElementNS("http://www.w3.org/2000/svg", "linearGradient");
      var stop1 = document.createElementNS("http://www.w3.org/2000/svg", "stop");
      var stop2 = document.createElementNS("http://www.w3.org/2000/svg", "stop");
      var skinPath = document.createElementNS("http://www.w3.org/2000/svg", "path");

      linearGradient.id = "gradient"+sk.id;
      linearGradient.setAttribute( "style", "x1:0%; y1:50%; x2:100%; y2:50%" );
      stop1.id = "st1_"+sk.id;
      stop1.setAttribute( "offset", "0%" );
      stop1.setAttribute( "stop-color", `hsl(${sk.lh}, 100%, ${sk.ll}%)` );
      stop2.id = "st2_"+sk.id;
      stop2.setAttribute( "offset", "100%" );
      stop2.setAttribute( "stop-color", `hsl(${sk.rh}, 100%, ${sk.rl}%)` );

      skinPath.classList.add("skin");
      skinPath.id = "sk"+sk.id;
      var path_string = "M" + (sk.pa[0].cx-sgf) + "," + sk.pa[0].cy + " ";
      for (j=1; j<sk.pa.length; j++) {
        if ( j < (sk.pa.length-1)/2 || j == sk.pa.length ) {
          path_string += "L" + (sk.pa[j].cx-sgf) + "," + sk.pa[j].cy + " ";
        } else {
          path_string += "L" + (sk.pa[j].cx+sgf) + "," + sk.pa[j].cy + " ";
        }
      }
      skinPath.setAttribute("d", path_string);
      skinPath.style.fill = "url(#gradient"+sk.id+")";

      svg.appendChild(defs);
      defs.appendChild(linearGradient);
      linearGradient.appendChild(stop1);
      linearGradient.appendChild(stop2);
      svg.appendChild(skinPath);
    }
  }
}

//updates points using verlet velocity 
function updatePoints() {
  for(var i=0; i<points.length; i++) {
    var p = points[i];  // point object
    if (!p.pinned) {
      var xv = (p.cx - p.px);  // x velocity
      var yv = (p.cy - p.py);  // y velocity
      p.px = p.cx;  // updates previous x as current x
      p.py = p.cy;  // updates previous y as current y
      p.cx += xv;  // updates current x with new velocity
      p.cy += yv;  // updates current y with new velocity
      p.cx += wind;  // add wind to x
      p.cy += gravity;  // add gravity to y
    }
  } 
}

//updates spans by reconciling new point positions with span length
function updateSpans() {
  for (var i=0; i<spans.length; i++) {
    var sp = spans[i];
    var dx = sp.p2.cx - sp.p1.cx;  // distance between x values
    var dy = sp.p2.cy - sp.p1.cy;  // distance between y values
    var d = Math.sqrt( dx*dx + dy*dy);  // distance between the points
    var r = sp.l / d; // ratio (span length over distance between points)
    var mx = sp.p1.cx + dx / 2;  // midpoint between x values 
    var my = sp.p1.cy + dy / 2;  // midpoint between y values
    var ox = dx / 2 * r;  // offset of each x value (compared to span length)
    var oy = dy / 2 * r;  // offset of each y value (compared to span length) 
    if (!sp.p1.pinned) {
      sp.p1.cx = mx - ox;  // updates span's first point x value
      sp.p1.cy = my - oy;  // updates span's first point y value
    }
    if (!sp.p2.pinned) {
      sp.p2.cx = mx + ox;  // updates span's second point x value
      sp.p2.cy = my + oy;  // updates span's second point y value 
    }
  }
}

//updates sky background
function moveClouds() {
  var skyImage = document.getElementById("sky");
  var siw = skyImage.width;  // sky image width
  var sdw = parseInt(skyDiv.style.width);  // sky div width
  if (skyPosition >= sdw - siw) {
    skyPosition -= siw/6000;
  } else {
    skyPosition = -(siw/4);
  }
  skyImage.style.right = skyPosition+"px"; 
}

//updates svg elements
function updateSvg() {
  //(updates points)
  if (displayPoints) {
    for (i=0; i<points.length; i++) {
      var p = points[i]; 
      var svgElement = document.getElementById("p"+p.id);  // gets svg element by id value
      svgElement.style.cx = p.cx;  // updates svg circle element's "center x" value
      svgElement.style.cy = p.cy;  // updates svg circle element's "center y" value 
    }
  }
  //(updates spans)
  if (displaySpans) {
    for (j=0; j<spans.length; j++) {
      var sp = spans[j];
      var svgElement = document.getElementById("sp"+sp.id);  // gets svg element by id value
      svgElement.setAttribute("d",
                              "M" + sp.p1.cx + "," + sp.p1.cy + " " +
                              "L" + sp.p2.cx + "," + sp.p2.cy);
    }
  }

  //(updates skins)
  if (displaySkins) {
    for (k=0; k<skins.length; k++) {
      //(shape)
      var sk = skins[k];
      var skinPath = document.getElementById("sk"+sk.id);  // gets svg element by id value
      var path_string = "M" + (sk.pa[0].cx-sgf) + "," + sk.pa[0].cy + " ";
      for (j=1; j<sk.pa.length; j++) {
        if ( j<(sk.pa.length-1)/2 || j==sk.pa.length ) {
          path_string += "L" + (sk.pa[j].cx-sgf) + "," + sk.pa[j].cy + " ";
        } else {
          path_string += "L" + (sk.pa[j].cx+sgf) + "," + sk.pa[j].cy + " ";
        }
      }
      skinPath.setAttribute("d", path_string);

      //(color) 
      if (changeColor) {
        sk.lh < 360 ? sk.lh -= hcr : sk.lh = 0;
        sk.rh < 360 ? sk.rh -= hcr : sk.rh = 0;
      }

      //(shading)
      var bskw = fw/(fwtc-1);  // base skin width
      var cskw = Math.abs(sk.pa[sk.pa.length-2].cx - sk.pa[sk.pa.length-1].cx);  // current skin width
      var lpct = cskw*50/bskw;  // lightness percentage
      if (lpct > 49) {lpct = 49;}  // maximizes lightness percentage
      if (lpct < 35) {lpct = 35;}  // minimizes lightness percentage
      if (k === 0) {  // sets leftmost skin with solid lightness
        sk.ll = lpct;
        sk.rl = lpct;
      } else {  // then sets gradient from previous skin to current skin's lightness based on width
        sk.ll = skins[k-1].rl;
        sk.rl = lpct;
      }

      //(updates gradients)
      document.getElementById("st1_"+sk.id).setAttribute( "stop-color", `hsl(${sk.lh}, 100%, ${sk.ll}%)` );
      document.getElementById("st2_"+sk.id).setAttribute( "stop-color", `hsl(${sk.rh}, 100%, ${sk.rl}%)` );
      skinPath.style.fill = "url(#gradient"+sk.id+")";
    }
      
  }
}

//waves flag
function waveFlag() {
  var ry = Math.random()*2.5-1.25;  // random y-velocity change
  if (frameCount % 8 === 0) {  // frequency of y-velocity change
    for (var i=0; i<points.length/5; i++) {  // number of points' y-velocities changed
      //(random point near left of flag)
      var rp = randNumBetween( Math.round(points.length*0.15), Math.round(points.length*0.5) );
      points[rp].py += ry;  // updates selected points' y-velocity
    }
  }
}

//updates coordinates and frames continuously
function updateViewbox() {
  updatePoints();
  for (var i=0; i<rigidity; i++) { updateSpans(); }
  updateSvg();
  waveFlag();
  moveClouds();
  window.requestAnimationFrame(updateViewbox);
  frameCount += 1;
}



///---EVENTS---///
 
//updates objects & renders svg
window.addEventListener("load", updateViewbox);

//scales viewbox shadow on window resize
window.addEventListener('resize', scaleHtmlElements);


External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.