Use the arrow keys to move the turtle. Don't get lost!
Last active
November 13, 2022 17:13
-
-
Save HarryStevens/324ddcfcc92d349f5a3800fb82dddf78 to your computer and use it in GitHub Desktop.
d3-turtle
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: gpl-3.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment