block by HarryStevens 324ddcfcc92d349f5a3800fb82dddf78

d3-turtle

Full Screen

Use the arrow keys to move the turtle. Don’t get lost!

index.html

<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      margin: 0;
    }
    body.out-of-bounds {
      background: tomato;
    }
    #state {
      background: rgba(255, 255, 255, .8);
      font-family: "Helvetica Neue", sans-serif;
      padding: 5px;
      position: absolute;
    }
    polygon {
      fill: steelblue;
    }
    path.selected {
      fill: green;
    }
  </style>
</head>
<body>
  <table id="state">
    <tr><td>Angle:</td><td id="angle"></td></tr>
    <tr><td>Position: <td id="position"></td></tr>
    <tr><td><input type="checkbox"> Fill path</td></tr>
  </table>
  <script src="https://d3js.org/d3.v5.min.js"></script>
  <script src="https://unpkg.com/geometric@1.0.0/build/geometric.min.js"></script>
  <script src="d3-turtle.js"></script>

  <script>
    var width = window.innerWidth, height = window.innerHeight;
    var turtle = d3.turtle().position([10, 80]).size(30).bounds([[0, 0], [width, 0], [width, height], [0, height]]);
    d3.select("body")
        .append("svg").attr("width", width).attr("height", height)
        .append("g")
        .call(turtle);

    var input = d3.select("input");
    input.on("change", _ => {
      d3.select("path").classed("selected", input.property("checked"));
    });
    
    d3.timer(_ => {
      d3.select("#position").text(turtle.position().map(d => Math.round(d)));
      d3.select("#angle").text(turtle.angle());
      d3.select("body").classed("out-of-bounds", !turtle.inBounds());
    });
  </script>
</body>
</html>

d3-turtle.js

// Requires: 
// d3-array (https://github.com/d3/d3-array)
// d3-selection (https://github.com/d3/d3-selection)
// d3-timer (https://github.com/d3/d3-timer)
// Geometric.js (https://github.com/HarryStevens/geometric)
d3.turtle = function(context){
  var angle = 0,
      bounds,
      position = [0, 0],
      size = 10,
      speed = 3,
      turnSpeed = 3;

  var stop = {up: 0, down: 0, left: 0, right: 0},
      isStopped = 0;

  var keyStates = {up: [0, 38], down: [0, 40], left: [0, 37], right: [0, 39]},
      keys = Object.keys(keyStates);

  var triangle,
      path,
      visited = [];

  function turtle(context){
    redraw(context);

    d3.select("body")
        .on("keydown", _ => {
          var k = d3.event.which;
          if (![91, 82].includes(k)){d3.event.preventDefault();}
          if (keys.map(d => keyStates[d][1]).includes(k)){
            var direction = keys.find(d => keyStates[d][1] === k);

            keyStates[direction][0] = 1;

            if (direction === "up"){
              keyStates.down[0] = 0;
            }
            if (direction === "down"){
              keyStates.up[0] = 0; 
            }
            if (direction === "left"){ 
              keyStates.right[0] = 0; 
            }
            if (direction === "right"){
              keyStates.left[0] = 0;
            }

            if (stop.up){
              keyStates.up[0] = 0;
            }
            if (stop.down){
              keyStates.down[0] = 0;
            }
            if (stop.left){
              keyStates.left[0] = 0;
            }
            if (stop.right){
              keyStates.right[0] = 0;
            }

            if (isStopped && turtle.vertexTouching().includes("top")){
              keyStates.up[0] = 0;
            }

            if (turtle.boundTouching() === "top"){
              if (turtle.headingVertical() === "down"){
                keyStates.down[0] = 0;
              }
              if(turtle.headingHorizontal() === "right"){
                keyStates.left[0] = 0;
              }
              if (turtle.headingHorizontal() === "left"){
                keyStates.right[0] = 0;
              }
            } 

            if (turtle.boundTouching() === "bottom"){
              if(turtle.headingVertical() === "up"){
                keyStates.down[0] = 0;
              }
              if (turtle.headingHorizontal() === "left"){
                keyStates.left[0] = 0;
              }
              if (turtle.headingHorizontal() === "right"){
                keyStates.right[0] = 0;
              }
            } 
            
            if (turtle.boundTouching() === "left"){
              if(turtle.headingHorizontal() === "right"){
                keyStates.down[0] = 0;
              }
              if (turtle.headingVertical() === "up"){
                keyStates.left[0] = 0;
              }
              if (turtle.headingVertical() === "down"){
                keyStates.right[0] = 0;
              }
            }

            if (turtle.boundTouching() === "right"){
              if (turtle.headingHorizontal() === "left"){
                keyStates.down[0] = 0;
              }
              if (turtle.headingVertical() === "down"){
                keyStates.left[0] = 0;
              }
              if (turtle.headingVertical() === "up"){
                keyStates.right[0] = 0;
              }
            }

          }

        })
        .on("keyup", _ => {
          var k = d3.event.which;
          if (![91, 82].includes(k)){d3.event.preventDefault();}
          if (keys.map(d => keyStates[d][1]).includes(k)){
            var direction = keys.find(d => keyStates[d][1] === k);

            if (bounds && !geometric.polygonInPolygon(turtle.vertices(), bounds)){
 
            }
            else {
              keyStates[direction][0] = 0;
            }

          }
        });

    d3.timer(_ => {
      
      if (keyStates.right[0] && !stop.right) { turtle.angle(angle += turnSpeed); }
      if (keyStates.left[0] && !stop.left) { turtle.angle(angle -= turnSpeed); }
      if (keyStates.up[0] && !stop.up) { turtle.position(geometric.pointTranslate(position, angle, speed)); }
      if (keyStates.down[0] && !stop.down) { turtle.position(geometric.pointTranslate(position, angle, -speed)); }

      visited.push(turtle.position());

      if (bounds && !geometric.polygonInPolygon(turtle.vertices(), bounds)){
        if (!isStopped) {
          Object.keys(keyStates).forEach(d => {
            stop[d] = keyStates[d][0];
          });
          isStopped = 1; 
        }
      } else {
        Object.keys(stop).forEach(d => stop[d] = 0);
        isStopped = 0;
      }

      if (keys.some(d => keyStates[d][0])){
        redraw(context);
      }
      
    });
  }

  turtle.angle = function(_){
    return arguments.length ? (angle = _ > 360 ? _ - 360 : _ < 0 ? _ + 360 : _, turtle) : angle;
  };
  turtle.bounds = function(_){
    return arguments.length ? (bounds = _, turtle) : bounds;
  };
  turtle.position = function(_){
    return arguments.length ? (position = _, turtle) : position;
  };
  turtle.size = function(_){
    return arguments.length ? (size = _, turtle) : size;
  };
  turtle.speed = function(_){
    return arguments.length ? (speed = _, turtle) : speed;
  };
  turtle.turnSpeed = function(_){
    return arguments.length ? (turnSpeed = _, turtle) : turnSpeed;
  };

  turtle.vertices = context => {
    var polygon = turtle.polygon();
    return polygon.map(v => {
      var rotated = geometric.pointRotate(v, angle);
      var translated = position.map((p, i) => (i === 1 ? size / 2 : 0) + p + rotated[i]);
      
      return translated;
    })
  };
  turtle.polygon = _ => [[0, -size / 2], [0, size / 2], [size, 0]];
  turtle.headingVertical = _ => {
    if (angle > 180 && angle < 360){
      return "up";
    }
    if (angle > 0 && angle < 180) {
      return "down";
    }
  }
  turtle.headingHorizontal = _ => {
    if (angle > 270 || angle < 90) {
      return "right";
    }
    if (angle > 90 && angle < 270) {
      return "left";
    }
  }
  turtle.inBounds = _ => {
    return !!!isStopped;
  }
  turtle.boundTouching = _ => {
    var vertexXs = turtle.vertices().map(d => d[0]);
    var vertexYs = turtle.vertices().map(d => d[1]);
    var vertexLeft = d3.min(vertexXs);
    var vertexRight = d3.max(vertexXs);
    var vertexTop = d3.min(vertexYs);
    var vertexBottom = d3.max(vertexYs);

    var boundsXs = bounds.map(d => d[0]);
    var boundsYs = bounds.map(d => d[1]);
    var boundsLeft = d3.min(boundsXs);
    var boundsRight = d3.max(boundsXs);
    var boundsTop = d3.min(boundsYs);
    var boundsBottom = d3.max(boundsYs);

    if (vertexLeft <= boundsLeft){
      return "left";
    }
    if (vertexRight >= boundsRight){
      return "right";
    }
    if (vertexTop <= boundsTop){
      return "top";
    }
    if (vertexBottom >= boundsBottom){
      return "bottom";
    }
  }
  turtle.vertexTouching = _ => {
    var vertexXs = turtle.vertices().map(d => d[0]);
    var vertexYs = turtle.vertices().map(d => d[1]);
    var vertexLeft = d3.min(vertexXs);
    var vertexRight = d3.max(vertexXs);
    var vertexTop = d3.min(vertexYs);
    var vertexBottom = d3.max(vertexYs);

    var boundsXs = bounds.map(d => d[0]);
    var boundsYs = bounds.map(d => d[1]);
    var boundsLeft = d3.min(boundsXs);
    var boundsRight = d3.max(boundsXs);
    var boundsTop = d3.min(boundsYs);
    var boundsBottom = d3.max(boundsYs);

    var vertices = ["left", "right", "top"];
    var verticesWithIndices = turtle.vertices().map((d, i) => ({v: d, i: i}));

    var minDistanceFromBound = 3;

    if (vertexLeft <= boundsLeft){
      return verticesWithIndices.filter(f => Math.abs(f.v[0] - vertexLeft) < minDistanceFromBound).map(d => vertices[d.i]);
    }
    if (vertexRight >= boundsRight){
      return verticesWithIndices.filter(f => Math.abs(f.v[0] - vertexRight) < minDistanceFromBound).map(d => vertices[d.i]);
    }
    if (vertexTop <= boundsTop){
      return verticesWithIndices.filter(f => Math.abs(f.v[1] - vertexTop) < minDistanceFromBound).map(d => vertices[d.i]);
    }
    if (vertexBottom >= boundsBottom){
      return verticesWithIndices.filter(f => Math.abs(f.v[1] - vertexBottom) < minDistanceFromBound).map(d => vertices[d.i]); 
    }
  }

  function redraw(context){
    if (!path){
      path = context.append("path")
          .attr("transform", "translate(0, " + (size / 2) + ")")
          .attr("fill", "none")
          .attr("stroke", "black");
    }

    if (visited.length){
      path
          .attr("d", "M" + visited[0] + " " + visited.filter((d, i) => i !== 0).join("L"));  
    }

    if (!triangle){
      triangle = context.append("polygon");
    }

    triangle
        .attr("fill", "black")
        .attr("points", turtle.vertices().join(" "));  
  }

  return turtle;
}