<html lang="en">
<head>
<meta charset="UTF-8">
<title>HTML5 Canvas Generative Art</title>

<style type="text/css">
  
</style>

</head>
<body>
<div id="container"> 
    <canvas id="displayCanvas" width="1024px" height="576px">
        Your browser does not support HTML5 canvas.
    </canvas>
    <form>
    <p id="caption">
        HTML5 Canvas - Morphing Fractal Curves, version 2.
        <input type="button", id="btnRegenerate" value="regenerate"/>
      <input type="button", id="btnExport" value="export image (png)"/>
        <br><a href="http://www.rectangleworld.com">rectangleworld.com</a>
    </p>
    </form>
</div>
</body>
</html>
body {background-color:#000000; color:#333333;} 
  h4 {font-family: sans-serif; color:#333333; font-size:16px;}
  h3 {font-family: sans-serif; color:#333333;}
  p {font-family: sans-serif; color:#333333; font-size:14px;}
    #caption {position:absolute; width:1024px; text-align:center; top:520px; z-index:1}
  a {font-family: sans-serif; color:#d15423; text-decoration:none;}
  canvas {}
    #displayCanvas {position:absolute; top:10px; z-index:0;}
  div {}
    #container {width:1024px; height:576px; margin:auto;}
window.addEventListener("load", windowLoadHandler, false);

//for debug messages while testing code
var Debugger = function() { };
Debugger.log = function(message) {
  try {
    console.log(message);
  }
  catch (exception) {
    return;
  }
}

function windowLoadHandler() {
  canvasApp();
}

function canvasSupport() {
  return Modernizr.canvas;
}

function canvasApp() {
  if (!canvasSupport()) {
    return;
  }
  
  var displayCanvas = document.getElementById("displayCanvas");
  var context = displayCanvas.getContext("2d");
  var displayWidth = displayCanvas.width;
  var displayHeight = displayCanvas.height;
  
  //off screen canvas used only when exporting image
  var exportCanvas = document.createElement('canvas');
  exportCanvas.width = displayWidth;
  exportCanvas.height = displayHeight;
  var exportCanvasContext = exportCanvas.getContext("2d");
  
  //var exportImage = document.createElement('img');
  
  //buttons
  var btnExport = document.getElementById("btnExport");
  btnExport.addEventListener("click", exportPressed, false);
  
  var btnRegenerate = document.getElementById("btnRegenerate");
  btnRegenerate.addEventListener("click", regeneratePressed, false);
  
  var numCircles;
  var maxMaxRad;
  var minMaxRad;
  var minRadFactor;
  var circles;
  var iterations;
  var numPoints;
  var timer;
  var drawsPerFrame;
  var drawCount;
  var bgColor,urlColor;
  var lineWidth;
  var colorParamArray;
  var colorArray;
  var dataLists;
  var minX, maxX, minY, maxY;
  var xSpace, ySpace;
  var lineNumber;
  var twistAmount;
  var fullTurn;
  var lineAlpha;
  var maxColorValue;
  var minColorValue;
  
  init();
  
  function init() {
    numCircles = 15; //35
    maxMaxRad = 200;
    minMaxRad = 200;
    minRadFactor = 0;
    iterations = 11;
    numPoints = Math.pow(2,iterations)+1;
    drawsPerFrame = 4;
    
    fullTurn = Math.PI*2*numPoints/(1+numPoints);
    
    minX = -maxMaxRad;
    maxX = displayWidth + maxMaxRad;
    minY = displayHeight/2-50;
    maxY = displayHeight/2+50;
    
    twistAmount = 0.67*Math.PI*2;
    
    stepsPerSegment = Math.floor(800/numCircles);
    
    maxColorValue = 100;
    minColorValue = 20;
    lineAlpha = 0.10;
    
    bgColor = "#000000";
    urlColor = "#333333";
    
    lineWidth = 1.01;
    
    startGenerate();
  }
    
  function startGenerate() {
    drawCount = 0;
    context.setTransform(1,0,0,1,0,0);
    
    context.clearRect(0,0,displayWidth,displayHeight);
    
    setCircles();
    
    colorArray = setColorList(iterations);
        
    lineNumber = 0;
    
    if(timer) {clearInterval(timer);}
    timer = setInterval(onTimer,1000/60);
    
  }
  
  function setColorList(iter) {
    var r0,g0,b0;
    var r1,g1,b1;
    var r2,g2,b2;
    var param;
    var colorArray;
    var lastColorObject;
    var i, len;
    
    var maxComponentDistance = 32;
    var maxComponentFactor = 0.5;
    
    
    r0 = minColorValue + Math.random()*(maxColorValue-minColorValue);
    g0 = minColorValue + Math.random()*(maxColorValue-minColorValue);
    b0 = minColorValue + Math.random()*(maxColorValue-minColorValue);;
    
    r1 = minColorValue + Math.random()*(maxColorValue-minColorValue);
    g1 = minColorValue + Math.random()*(maxColorValue-minColorValue);
    b1 = minColorValue + Math.random()*(maxColorValue-minColorValue);
    
    
    /*
    //can also set colors explicitly here if you like.
    r1 = 90;
    g1 = 60;
    b1 = 20;
    
    r0 = 30;
    g0 = 77;
    b0 = 66;
    */
    
    a = lineAlpha;
    
    var colorParamArray = setLinePoints(iter);
    colorArray = [];
    
    len = colorParamArray.length;
    
    for (i = 0; i < len; i++) {
      param = colorParamArray[i];
      
      r = Math.floor(r0 + param*(r1 - r0));
      g = Math.floor(g0 + param*(g1 - g0));
      b = Math.floor(b0 + param*(b1 - b0));
        
      var newColor = "rgba("+r+","+g+","+b+","+a+")";
      
      colorArray.push(newColor);
    }
    
    return colorArray;
    
  }
  
  function setCircles() {
    var i;
    var r,g,b,a;
    var grad;
    
    circles = [];
    
    for (i = 0; i < numCircles; i++) {
      maxR = minMaxRad+Math.random()*(maxMaxRad-minMaxRad);
      minR = minRadFactor*maxR;
      
      var newCircle = {
        centerX: minX + i/(numCircles-1)*(maxX - minX),
        centerY: minY + i/(numCircles-1)*(maxY - minY),
        //centerY: minY + Math.random()*(maxY - minY),
        maxRad : maxR,
        minRad : minR,
        phase : i/(numCircles-1)*twistAmount,
        pointArray : setLinePoints(iterations)
        };
      circles.push(newCircle);
    }
  }
  
  function onTimer() {
    var i;
    var cosTheta, sinTheta;
    var theta;
    
    var numCircles = circles.length;

    var linParam;
    var cosParam;
    var centerX, centerY;
    var xSqueeze = 0.75;
    var x0,y0;
    var rad, rad0, rad1;
    var phase, phase0, phase1;
    
    for (var k = 0; k < drawsPerFrame; k++) {
    
      theta = lineNumber/(numPoints-1)*fullTurn;
      
      context.globalCompositeOperation = "lighter";
      
      context.lineJoin = "miter";
      
      context.strokeStyle = colorArray[lineNumber];
      context.lineWidth = lineWidth;
      context.beginPath();
      
      //move to first point
      centerX = circles[0].centerX;
      centerY = circles[0].centerY;
      rad = circles[0].minRad + circles[0].pointArray[lineNumber]*(circles[0].maxRad - circles[0].minRad);
      phase = circles[0].phase;
      x0 = centerX + xSqueeze*rad*Math.cos(theta + phase);
      y0 = centerY + rad*Math.sin(theta + phase);
      context.moveTo(x0,y0);
      
      for (i=0; i< numCircles-1; i++) {
        //draw between i and i+1 circle
        rad0 = circles[i].minRad + circles[i].pointArray[lineNumber]*(circles[i].maxRad - circles[i].minRad);
        rad1 = circles[i+1].minRad + circles[i+1].pointArray[lineNumber]*(circles[i+1].maxRad - circles[i+1].minRad);
        phase0 = circles[i].phase;
        phase1 = circles[i+1].phase;
        
        for (j = 0; j < stepsPerSegment; j++) {
          linParam = j/(stepsPerSegment-1);
          cosParam = 0.5-0.5*Math.cos(linParam*Math.PI);
          
          //interpolate center
          centerX = circles[i].centerX + linParam*(circles[i+1].centerX - circles[i].centerX);
          centerY = circles[i].centerY + cosParam*(circles[i+1].centerY - circles[i].centerY);
          
          //interpolate radius
          rad = rad0 + cosParam*(rad1 - rad0);
          
          //interpolate phase
          phase = phase0 + cosParam*(phase1 - phase0);
          
          x0 = centerX + xSqueeze*rad*Math.cos(theta + phase);
          y0 = centerY + rad*Math.sin(theta + phase);
          
          context.lineTo(x0,y0);
          
        }
        
      }
      
      context.stroke();
          
      lineNumber++;
      if (lineNumber > numPoints-1) {
        clearInterval(timer);
        timer = null;
        break;
      }
    }
  }
    
  //Here is the function that defines a noisy (but not wildly varying) data set which we will use to draw the curves.
  //We first define the points in a linked list, but then store the values in an array.
  function setLinePoints(iterations) {
    var pointList = {};
    var pointArray = [];
    pointList.first = {x:0, y:1};
    var lastPoint = {x:1, y:1}
    var minY = 1;
    var maxY = 1;
    var point;
    var nextPoint;
    var dx, newX, newY;
    var ratio;
    
    var minRatio = 0.5;
        
    pointList.first.next = lastPoint;
    for (var i = 0; i < iterations; i++) {
      point = pointList.first;
      while (point.next != null) {
        nextPoint = point.next;
        
        dx = nextPoint.x - point.x;
        newX = 0.5*(point.x + nextPoint.x);
        newY = 0.5*(point.y + nextPoint.y);
        newY += dx*(Math.random()*2 - 1);
        
        var newPoint = {x:newX, y:newY};
        
        //min, max
        if (newY < minY) {
          minY = newY;
        }
        else if (newY > maxY) {
          maxY = newY;
        }
        
        //put between points
        newPoint.next = nextPoint;
        point.next = newPoint;
        
        point = nextPoint;
      }
    }
    
    //normalize to values between 0 and 1
    //Also store y values in array here.
    if (maxY != minY) {
      var normalizeRate = 1/(maxY - minY);
      point = pointList.first;
      while (point != null) {
        point.y = normalizeRate*(point.y - minY);
        pointArray.push(point.y);
        point = point.next;
      }
    }
    //unlikely that max = min, but could happen if using zero iterations. In this case, set all points equal to 1.
    else {
      point = pointList.first;
      while (point != null) {
        point.y = 1;
        pointArray.push(point.y);
        point = point.next;
      }
    }
        
    return pointArray;    
  }
    
  function exportPressed(evt) {
    //background - otherwise background will be transparent.
    exportCanvasContext.fillStyle = bgColor;
    exportCanvasContext.fillRect(0,0,displayWidth,displayHeight);
    
    //draw
    exportCanvasContext.drawImage(displayCanvas, 0,0,displayWidth,displayHeight,0,0,displayWidth,displayHeight);
    
    //add printed url to image
    exportCanvasContext.fillStyle = urlColor;
    exportCanvasContext.font = 'bold italic 16px Helvetica, Arial, sans-serif';
    exportCanvasContext.textBaseline = "top";
    var metrics = exportCanvasContext.measureText("rectangleworld.com");
    exportCanvasContext.fillText("rectangleworld.com", displayWidth - metrics.width - 10, 5);
    
    //we will open a new window with the image contained within:    
    //retrieve canvas image as data URL:
    var dataURL = exportCanvas.toDataURL("image/png");
    //open a new window of appropriate size to hold the image:
    var imageWindow = window.open("", "fractalLineImage", "left=0,top=0,width="+displayWidth+",height="+displayHeight+",toolbar=0,resizable=0");
    //write some html into the new window, creating an empty image:
    imageWindow.document.write("<title>Export Image</title>")
    imageWindow.document.write("<img id='exportImage'"
                  + " alt=''"
                  + " height='" + displayHeight + "'"
                  + " width='"  + displayWidth  + "'"
                  + " style='position:absolute;left:0;top:0'/>");
    imageWindow.document.close();
    //copy the image into the empty img in the newly opened window:
    var exportImage = imageWindow.document.getElementById("exportImage");
    exportImage.src = dataURL;
  }
  
  function regeneratePressed(evt) {
    startGenerate();
  }
  
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.