Search code examples
javascriptcanvasrecursionfractalsl-systems

how to apply L-system logic to segments


Edit
Here's a new version which correctly applies the length and model but doesn't position the model correctly. I figured it might help.

http://codepen.io/pixelass/pen/78f9e97579f99dc4ae0473e33cae27d5?editors=001


I have 2 canvas instances

  1. model
  2. result

On the model view the user can drag the handles to modify the model The result view should then apply the model to every segment (relatively)

This is just a basic l-system logic for fractal curves though I am having problems applying the model to the segments.

Se the picture below: The red lines should replicate the model, but I can't figure out how to correctly apply the logic

I have a demo version here: http://codepen.io/pixelass/pen/c4d7650af7ce4901425b326ad7a4b259

enter image description here

ES6

// simplify Math

'use strict';

Object.getOwnPropertyNames(Math).map(function(prop) {
  window[prop] = Math[prop];
});

// add missing math functions
var rad = (degree)=> {
  return degree * PI / 180;
};
var deg = (radians)=> {
  return radians * 180 / PI;
};

// get our drawing areas

var model = document.getElementById('model');
var modelContext = model.getContext('2d');

var result = document.getElementById('result');
var resultContext = result.getContext('2d');

var setSize = function setSize() {
  model.height = 200;
  model.width = 200;
  result.height = 400;
  result.width = 400;
};

// size of the grabbing dots
var dotSize = 5;
// flag to determine if we are grabbing a point
var grab = -1;
// set size to init instances
setSize();
//
var iterations = 1;

// define points
// this only defines the initial model

var returnPoints = function returnPoints(width) {
  return [{
    x: 0,
    y: width
  }, {
    x: width / 3,
    y: width
  }, {
    x: width / 2,
    y: width / 3*2
  }, {
    x: width / 3 * 2,
    y: width
  }, {
    x: width,
    y: width
  }];
};

// set initial state for model
var points = returnPoints(model.width);

// handle interaction
// grab points only if hovering
var grabPoint = function grabPoint(e) {
  var X = e.layerX;
  var Y = e.layerY;
  for (var i = 1; i < points.length - 1; i++) {
    if (abs(X - points[i].x) < dotSize && abs(Y - points[i].y) < dotSize) {
      model.classList.add('grabbing');
      grab = i;
    }
  }
};
// release point
var releasePoint = function releasePoint(e) {
  if (grab > -1) {
    model.classList.add('grab');
    model.classList.remove('grabbing');
  }
  grab = -1;
};

// set initial state for result

// handle mouse movement on the model canvas
var handleMove = function handleMove(e) {
  // determine current mouse position
  var X = e.layerX;
  var Y = e.layerY;
  // clear classes
  model.classList.remove('grabbing');
  model.classList.remove('grab');

  // check if hovering a dot
  for (var i = 1; i < points.length - 1; i++) {
    if (abs(X - points[i].x) < dotSize && abs(Y - points[i].y) < dotSize) {
      // indicate grabbable
      model.classList.add('grab');
    }
  }

  // if grabbing
  if (grab > -1) {
    // indicate grabbing
    model.classList.add('grabbing');
    // modify dot on the model canvas
    points[grab] = {
      x: X,
      y: Y
    };
    // modify dots on the result canvas
    drawSegment({
      x: points[grab - 1].x,
      y: points[grab - 1].y
    }, {
      x: X,
      y: Y
    });

  }
};

let m2 = points[1].x / points[4].x
let m3 = points[2].x / points[4].x
let m4 = points[3].x / points[4].x
let n2 = points[1].y / points[4].y
let n3 = points[2].y / points[4].y
let n4 = points[3].y / points[4].y

var drawSegment = function drawSegment(start, end) {
  var dx = end.x - start.x
  var dy = end.y - start.y
  var dist = sqrt(dx * dx + dy * dy)
  var angle = atan2(dy, dx)
  let x1 = end.x
  let y1 = end.y
  let x2 = round(cos(angle) * dist)
  let y2 = round(sin(angle) * dist)

  resultContext.srtokeStyle = 'red'
  resultContext.beginPath()
  resultContext.moveTo(x1, y1)
  resultContext.lineTo(x2, y2)
  resultContext.stroke()

  m2 = points[1].x / points[4].x
  m3 = points[2].x / points[4].x
  m4 = points[3].x / points[4].x
  n2 = points[1].y / points[4].y
  n3 = points[2].y / points[4].y
  n4 = points[3].y / points[4].y

};

var drawDots = function drawDots(points) {
  // draw dots
  for (var i = 1; i < points.length - 1; i++) {
    modelContext.lineWidth = 4; //
    modelContext.beginPath();
    modelContext.strokeStyle = 'hsla(' + 360 / 5 * i + ',100%,40%,1)';
    modelContext.fillStyle = 'hsla(0,100%,100%,1)';
    modelContext.arc(points[i].x, points[i].y, dotSize, 0, 2 * PI);
    modelContext.stroke();
    modelContext.fill();
  }
};

var drawModel = function drawModel(ctx, points, n) {


  var dx = points[1].x - points[0].x
  var dy = points[1].y - points[0].y
  var dist = sqrt(dx * dx + dy * dy)
  var angle = atan2(dy, dx)
  let x1 = points[1].x
  let y1 = points[1].y
  let x2 = round(cos(angle) * dist)
  let y2 = round(sin(angle) * dist)

  ctx.strokeStyle = 'hsla(0,0%,80%,1)';
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(points[0].x,       
             points[0].y)
  ctx.lineTo(points[1].x * m2,  
             points[1].y * n2)
  ctx.lineTo(points[1].x * m3,  
             points[1].y * n3)
  ctx.lineTo(points[1].x * m4,  
             points[1].y * n4)
  ctx.lineTo(points[1].x,       
             points[1].y)

  ctx.stroke();

    ctx.strokeStyle = 'hsla(100,100%,80%,1)';

  ctx.beginPath();
  ctx.moveTo(points[0].x,       
             points[0].y)
  ctx.lineTo(points[1].x,       
             points[1].y)

  ctx.stroke()
  if (n > 0 ) {

    drawModel(resultContext, [{
      x: points[0].x,
      y: points[0].y
    }, {
      x: points[1].x * m2,
      y: points[1].y * n2 
    }], n - 1);
    drawModel(resultContext, [{
      x: points[1].x * m2,
      y: points[1].y * n2
    }, {
      x: points[1].x * m3,
      y: points[1].y * n3 
    }], n - 1);
    /*
    drawModel(resultContext, [{
      x: points[1].x * m3,
      y: points[1].y * m3
    }, {
      x: points[1].x * m4,
      y: points[1].y * n4 
    }], n - 1);

    drawModel(resultContext, [{
      x: points[1].x * m4,
      y: points[1].y * m4
    }, {
      x: points[1].x,
      y: points[1].y 
    }], n - 1);*/
  } else {
 ctx.strokeStyle = 'hsla(0,100%,50%,1)';
  ctx.beginPath();
  ctx.moveTo(points[0].x,       
             points[0].y)
  ctx.lineTo(points[1].x * m2,  
             points[1].y * n2)
  ctx.lineTo(points[1].x * m3,  
             points[1].y * n3)
  ctx.lineTo(points[1].x * m4,  
             points[1].y * n4)
  ctx.lineTo(points[1].x,       
             points[1].y)

  ctx.stroke();
  }
};

var draw = function draw() {

  // clear both screens
  modelContext.fillStyle = 'hsla(0,0%,100%,.5)';
  modelContext.fillRect(0, 0, model.width, model.height);

  resultContext.fillStyle = 'hsla(0,0%,100%,1)';
  resultContext.fillRect(0, 0, result.width, result.height);

  // draw model
  drawModel(modelContext, [{
    x: 0,
    y: 200
  }, {
    x: 200,
    y: 200
  }]);

  drawModel(resultContext, [{
    x: 0,
    y: 400
  }, {
    x: 400,
    y: 400
  }],iterations);


  // draw the dots to indicate grabbing points
  drawDots(points);
  // redraw
  requestAnimationFrame(draw);
};

window.addEventListener('resize', setSize);
model.addEventListener('mousemove', handleMove);
model.addEventListener('mousedown', grabPoint);
window.addEventListener('mouseup', releasePoint);

setSize();
draw();

Solution

  • Write a function to transform a point given the point, an old origin (the start of the model line segment), a new origin (the start of the child line segment), an angle and a scale (you have already calculated these):

    var transformPoint = function transformPoint(point, oldOrigin, newOrigin, angle, dist) {
    
      // subtract old origin to rotate and scale relative to it:
      var x = point.x - oldOrigin.x;
      var y = point.y - oldOrigin.y;
    
      // rotate by angle
      var sine = sin(angle)
      var cosine = cos(angle)
      var rotatedX = (x * cosine) - (y * sine);
      var rotatedY = (x * sine) + (y * cosine);
    
      // scale
      rotatedX *= dist;
      rotatedY *= dist;
    
      // offset by new origin and return:
      return {x: rotatedX + newOrigin.x - oldOrigin.x, y: rotatedY + newOrigin.y - oldOrigin.y }
    }
    

    You need to translate it by the old origin (so that you can rotate around it), then rotate, then scale, then translate by the new origin. Then return the point.

    modelLogic[0] is the old origin because it defines the start of the segment in the model and points[0] is the new origin because that is what it is mapped to by the transformation.

    You can call the function from your drawModel function like this:

      let p1 = transformPoint(modelLogic[0], modelLogic[0], points[0], angle, dist);
      let p2 = transformPoint(modelLogic[1], modelLogic[0], points[0], angle, dist);
      let p3 = transformPoint(modelLogic[2], modelLogic[0], points[0], angle, dist);
      let p4 = transformPoint(modelLogic[3], modelLogic[0], points[0], angle, dist);
      let p5 = transformPoint(modelLogic[4], modelLogic[0], points[0], angle, dist);
    

    and change your drawing code to use the returned points p1, p2 etc instead of x1, y1, x2, y2 etc.

    Alternatively, you can create a single matrix to represent all of these translation, rotation and scaling transforms and transform each point by it in turn.