Skip to content

Instantly share code, notes, and snippets.

@HarryStevens
Last active November 13, 2022 17:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save HarryStevens/324ddcfcc92d349f5a3800fb82dddf78 to your computer and use it in GitHub Desktop.
Save HarryStevens/324ddcfcc92d349f5a3800fb82dddf78 to your computer and use it in GitHub Desktop.
d3-turtle
license: gpl-3.0

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

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