// 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; }