Spinning Arrows

Arrows pointing in all kinds of directions.

Log in to post a comment.

// Forked from "Double Circle Turnaround" by mcfly
// https://turtletoy.net/turtle/49150890a6

// You can find the Turtle API reference here: https://turtletoy.net/syntax
Canvas.setpenopacity(1);

// Global code will be evaluated once.
const turtle = new Turtle();

// Adjustable variables.
const size = 8.5; // min=1, max=10, step=.1
const grow = -.2; // min=-3, max=3, step=.1
const rows = 15; // min=1, max=50, step=1
const columns = 15; // min=1, max=50, step=1
const maxAngle = 90; // min=0, max=360, step=45
const turns = 1; // min=0, max=5, step=1
const border = 4; // min=1, max=20, step=1

// Adjust some parameters so they fit the screen.
var len = size,
    space = 5,
    factor = 180 / (Math.max(columns,rows) * (len+space));
len *= factor;
space *= factor;

// Helper variables.
const width = columns * (len+space),
    height = rows * (len+space);
    
// The walk function will be called until it returns false.
function walk(i) {
    if(!(function() {
        var row = Math.floor(i / columns);
            column = i % columns;
        if(row >= rows)
            return false;
    
        // Position.
        turtle.jump(-width/2 + (column+.5) * (len+space),-height/2 + (row+.5) * (len+space));

        // Calculate rotation.
        let x = column - columns/2,
            y = row - rows/2,
            angle = Math.atan2(y,x) * 180 / Math.PI,
            distance = Math.sqrt(x*x + y*y) / (columns/2),
            rotation = Math.max(0,1 - distance) * maxAngle + turns*angle;
            
        // Rotate.
        turtle.setheading(rotation);
        
        // Drawn maple leaf.
        traceSvgPath("M17.5,10L10,17.5V14H3V6h7V2.5L17.5,10z",10,10,.1*len*(1+grow*Math.max(0,1-distance)));

        return true;
    })()) {
        finish();
        return false;
    }
    return true;
}

// Evaluated once at the end.
function finish() {
    // Cut the frame last.
    const frameWidth = width + 2 * border,
        frameHeight = height + 2 * border;
    turtle.jump(-frameWidth/2,-frameHeight/2);
    turtle.setheading(0);
    turtle.pendown();
    turtle.forward(frameWidth);
    turtle.right(90);
    turtle.forward(frameHeight);
    turtle.right(90);
    turtle.forward(frameWidth);
    turtle.right(90);
    turtle.forward(frameHeight);
}

// Trace the provided "d" attribute of an SVG <path> element with the turtle,
// starting at the current position (and heading). An offset and scaling factor
// may be applied (offset first, scale second). Curves will become straight
// lines. The position and heading of the turtle will be preserved, then pen
// will be up.
function traceSvgPath(path,offsetX = 0,offsetY = 0,scale = 1) {
  const pos = turtle.pos(),
    heading = turtle.heading();
    angle = (dx,dy) => Math.atan2(dy,dx) * 180 / Math.PI,
    length = (dx,dy) => Math.sqrt(dx*dx + dy*dy);
  turtle.penup();
  var x = 0,
    y = 0,
    a = 0,
    startX = 0,
    startY = 0,
    ta;
  parseSvgPath(path).forEach((cmd,i) => {
    // Transform commands into line commands.
    cmd = {
      H : ["L",cmd[1],y/(scale || 1) + offsetY],
      h : ["l",cmd[1],0],
      V : ["L",x/(scale || 1) + offsetX,cmd[1]],
      v : ["l",0,cmd[1]],
      Z : ["L",startX,startY],
      z : ["L",startX,startY],
      C : ["L",cmd[5],cmd[6]],
      c : ["l",cmd[1]+cmd[3]+cmd[5],cmd[2]+cmd[4]+cmd[6]],
      S : ["L",cmd[3],cmd[4]],
      s : ["l",cmd[1]+cmd[3],cmd[2]+cmd[4]],
      Q : ["L",cmd[3],cmd[4]],
      q : ["l",cmd[1]+cmd[3],cmd[2]+cmd[4]],
      T : ["L",cmd[1],cmd[2]],
      t : ["l",cmd[1],cmd[2]],
      A : ["L",cmd[6],cmd[7]],
      a : ["l",cmd[6],cmd[7]],
    }[cmd[0]] || cmd;
    switch(cmd[0]) {
      case "M":
      case "L":
        cmd[1] -= offsetX;
        cmd[2] -= offsetY;
        cmd[1] *= scale;
        cmd[2] *= scale;
        ta = angle(cmd[1]-x,cmd[2]-y);
        turtle.right(ta - a);
        if(cmd[0] === "L")
          turtle.pendown();
        turtle.forward(length(cmd[1]-x,cmd[2]-y));
        if(cmd[0] === "L")
          turtle.penup();
        x = cmd[1];
        y = cmd[2];
        a = ta;
        break;
      case "m":
      case "l":
        cmd[1] *= scale;
        cmd[2] *= scale;
        ta = angle(cmd[1],cmd[2]);
        turtle.right(ta - a);
        if(cmd[0] === "l")
          turtle.pendown();
        turtle.forward(length(cmd[1],cmd[2]));
        if(cmd[0] === "l")
          turtle.penup();
        x += cmd[1];
        y += cmd[2];
        a = ta;
        break;
    }
    if(!i) {
      startX = x/(scale || 1) + offsetX;
      startY = y/(scale || 1) + offsetY;
    }
  });
  turtle.jump(pos);
  turtle.setheading(heading);
}

// Taken from https://github.com/jkroso/parse-svg-path and adjusted somewhat.
const cmdLength = {a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0};
function parseSvgPath(path) {
	var data = [];
	path.replace(/([astvzqmhlc])([^astvzqmhlc]*)/ig, function(_, command, args){
		var type = command.toLowerCase();
    args = (args.match(/-?[0-9]*\.?[0-9]+(?:e[-+]?\d+)?/ig) || []).map(Number);

		// overloaded moveTo
		if(type == 'm' && args.length > 2) {
			data.push([command].concat(args.splice(0, 2)));
			type = 'l';
			command = command == 'm' ? 'l' : 'L';
		}

		while(true) {
			if(args.length == cmdLength[type]) {
				args.unshift(command);
				return data.push(args);
			}
			if(args.length < cmdLength[type]) throw new Error('malformed path data')
			   data.push([command].concat(args.splice(0, cmdLength[type])));
		}
	});
	return data;
}