A cardioid from light reflection

Fri, Oct 16, 2015 4-minute read

This is a small experiment using HTML5 canvas and javascript. The module created allows you to plot a cardioid on a canvas starting from the reflection of a ray of light in a circumference.

The cardioid object has a lot of properties and below the canvas you can find some sliders to play with them.

Here you can see the source code for the cardioid module:

(function()
{
  Cardioid = function(canvas, kwargs)
  {
    'use strict';
    
    // Private
    var properties = {};

    if(kwargs !== undefined)
      properties = {
        center_x: kwargs.pos_x || canvas.width / 2,
        center_y: kwargs.pos_y || canvas.height / 2,
        step: 360 / kwargs.pieces || 360 / 64,
        radius: kwargs.radius || 200,
        pieces: kwargs.pieces || 64,
        deltaTime: kwargs.deltaTime || 32,
        initialRotation: kwargs.initialRotation || 90,
        fun1: kwargs.fun1 || 2,
        fun2: kwargs.fun2 || 1,
        mirror: kwargs.mirror || true,
        numIterations: kwargs.iterations || 32
      };
    else
      properties = {
        center_x: canvas.width / 2,
        center_y: canvas.height / 2,
        step: 360 / 64,
        radius: 200,
        pieces: 64,
        deltaTime: 32,
        initialRotation: 90,
        fun1: 2,
        fun2: 1,
        mirror: true,
        numIterations: 32
      };
    
    function hsv2rgb(hsv)
    {
      var h = hsv.hue, s = hsv.sat, v = hsv.val;
      var rgb, i, data = [];
      if (s === 0) {
        rgb = [v,v,v];
      } else {
        h = h / 60;
        i = Math.floor(h);
        data = [v*(1-s), v*(1-s*(h-i)), v*(1-s*(1-(h-i)))];
        switch(i) {
          case 0:
            rgb = [v, data[2], data[0]];
            break;
          case 1:
            rgb = [data[1], v, data[0]];
            break;
          case 2:
            rgb = [data[0], v, data[2]];
            break;
          case 3:
            rgb = [data[0], data[1], v];
            break;
          case 4:
            rgb = [data[2], data[0], v];
            break;
          default:
            rgb = [v, data[0], data[1]];
            break;
        }
      }
      return '#' + rgb.map(function(x){ 
        return ("0" + Math.round(x*255).toString(16)).slice(-2);
      }).join('');
    }
    
    Number.prototype.mod = function(base) { return ((this % base) + base) % base; }
    
    function _genPoints() 
    {
      points = [];
      for(var angle = 0; angle < 360; angle += properties.step)
      {
        var tmp_angle = angle + properties.initialRotation;
        var rad = (tmp_angle * 2 * Math.PI) / 360;

        var point =
        {
          x: properties.center_x + properties.radius * Math.cos(rad),
          y: properties.center_y + properties.radius * Math.sin(rad),
          text_x: properties.center_x - 6 + (properties.radius + 16) * Math.cos(rad),
          text_y: properties.center_y + 4 + (properties.radius + 16) * Math.sin(rad),
          rad: rad,
          angle: tmp_angle
        };

        points.push(point);
      }
    }
    
    function _drawCircle()
    {
      ctx.beginPath();
      ctx.strokeStyle = "#000000";
      ctx.arc(properties.center_x, properties.center_y, properties.radius, 0, 2*Math.PI);
      ctx.stroke();
      ctx.closePath();
      return obj;
    }
    
    function _drawPoints(drawStep)
    {
      var point;

      if(drawStep === undefined)
      {
        for(var index = 0; index < points.length; ++index)
        {
          point = points[index];

          ctx.beginPath();
          ctx.strokeStyle = "#000000";
          ctx.arc(point.x, point.y, 1, 0, 2*Math.PI);
          ctx.stroke();
          ctx.fillText(index.toString(), point.text_x, point.text_y);
          ctx.closePath();
        }
      }
      else
      {
        point = points[drawStep];
        ctx.beginPath();
        ctx.arc(point.x, point.y, 2, 0, 2*Math.PI);
        ctx.stroke();
        ctx.fillText(drawStep.toString(), point.text_x, point.text_y); 
        ctx.closePath();
        
        if(drawStep < points.length - 1)
          animationReferencePoints = setTimeout(function() { _drawPoints(drawStep + 1); }, Math.ceil(properties.deltaTime / 2));
      }
    }
    
    function _drawCarioid(drawStep)
    {
      var start, end;
      
      if(drawStep === undefined)
      {
        for(var curStep = 0; curStep < properties.numIterations; ++curStep)
        {
          start = points[(curStep * properties.fun1).mod(properties.pieces)];
          end = points[((curStep * properties.fun2 + properties.pieces / 2)).mod(properties.pieces)];
          
          ctx.beginPath();
          ctx.strokeStyle = hsv2rgb({hue: curStep % 360, sat: 1, val: 1});
          ctx.lineWidth = 1;
          
          ctx.moveTo(start.x, start.y);
          ctx.lineTo(end.x, end.y);
          ctx.stroke();
          ctx.closePath();
          
          if (properties.mirror === true)
          {
            start = points[(-curStep * properties.fun1).mod(properties.pieces)];
            end = points[((-curStep * properties.fun2 - properties.pieces / 2)).mod(properties.pieces)];
            ctx.moveTo(start.x, start.y);
            ctx.lineTo(end.x, end.y);
            ctx.stroke();
          } 
        }
      }
      else
      {
        start = points[(drawStep * properties.fun1) % properties.pieces];
        end = points[((drawStep * properties.fun2 + properties.pieces / 2)) % properties.pieces];
        
        ctx.beginPath();
        ctx.strokeStyle = hsv2rgb({hue: drawStep % 360, sat: 1, val: 1});
        ctx.lineWidth = 1;
        
        ctx.moveTo(start.x, start.y);
        ctx.lineTo(end.x, end.y);
        ctx.stroke();
        ctx.closePath();
        
        if (properties.mirror === true)
        {
          start = points[(-drawStep * properties.fun1).mod(properties.pieces)];
          end = points[((-drawStep * properties.fun2 - properties.pieces / 2)).mod(properties.pieces)];
          ctx.moveTo(start.x, start.y);
          ctx.lineTo(end.x, end.y);
          ctx.stroke();
        }

        if(drawStep < properties.numIterations) {
          animationReferenceCardioid = setTimeout(function() { _drawCarioid(++drawStep); }, properties.deltaTime);
        }
      }
      
    }
    
    /**
      * args = {
      *  animCard: boolean,
      *  animNum: boolean
      * }
      */
    function _draw(args)
    {
      clearTimeout(animationReferencePoints);
      clearTimeout(animationReferenceCardioid);
  
      var drawStepCar, drawStepNum;

      if(args !== undefined && args.animCard === true)
        drawStepCar = 0;
      if(args !== undefined && args.animNum === true)
        drawStepNum = 0;
  
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      obj.drawCircle();
      obj.drawPoints(drawStepNum);
      obj.drawCarioid(drawStepCar);
      
    }
    
    function _clear() { _genPoints(); _draw(); }
    
    var animationReferencePoints = null;
    var animationReferenceCardioid = null;
    
    var ctx = canvas.getContext("2d");
    var points = [];
    _genPoints();
    
    // Public
    var obj = {
      drawCircle: _drawCircle,
      drawPoints: _drawPoints,
      drawCarioid: _drawCarioid,
      draw: _draw,
      changeRadius: function(newRaius)
      {
        properties.radius = newRaius;
        _clear();
      },
      changeInitialRotation: function(newInitialRotation)
      {
        properties.initialRotation = newInitialRotation;
        _clear();
      },
      changeNumPoints: function(newPointsNum)
      {
        if(newPointsNum !== properties.pieces)
        {
          properties.pieces = newPointsNum;
          properties.step = 360 / newPointsNum;
          _clear();
        }
      },
      changeDeltaTime: function(newDeltaTime)
      {
        if(newDeltaTime !== properties.deltaTime)
        {
          properties.deltaTime = newDeltaTime;
        }
      },
      changeNumInterations: function(newNumInterations)
      {
        if(newNumInterations !== properties.numIterations)
        {
          properties.numIterations = newNumInterations;
          _clear();
        }
      },
      setMirror: function(newState)
      {
        properties.mirror = newState;
      },
      changeFun: function(obj)
      {
        var dirty = false;
        if(obj.fun1 !== undefined && obj.fun1 !== properties.fun1) {
          properties.fun1 = obj.fun1;
          dirty = true;
        }
        else if(obj.fun2 !== undefined && obj.fun2 !== properties.fun2) {
          properties.fun2 = obj.fun2;
          dirty = true;
        }
        
        if(dirty === true) _clear();
      }
    };
    
    return obj;
    
  };
})();