Sierpinski arrowhead curve

From Rosetta Code
Sierpinski arrowhead curve is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.


Task

Produce a graphical or ASCII-art representation of a Sierpinski arrowhead curve of at least order 3.

C

This code is based on the Phix and Go solutions, but produces a file in SVG format. <lang c>// See https://en.wikipedia.org/wiki/Sierpi%C5%84ski_curve#Arrowhead_curve

  1. include <math.h>
  2. include <stdio.h>
  3. include <stdlib.h>

// Structure to keep track of current position and orientation typedef struct cursor_tag {

   double x;
   double y;
   int angle;

} cursor_t;

void turn(cursor_t* cursor, int angle) {

   cursor->angle = (cursor->angle + angle) % 360;

}

void draw_line(FILE* out, cursor_t* cursor, double length) {

   double theta = (M_PI * cursor->angle)/180.0;
   cursor->x += length * cos(theta);
   cursor->y += length * sin(theta);
   fprintf(out, "L%g,%g\n", cursor->x, cursor->y);

}

void curve(FILE* out, int order, double length, cursor_t* cursor, int angle) {

   if (order == 0) {
       draw_line(out, cursor, length);
   } else {
       curve(out, order - 1, length/2, cursor, -angle);
       turn(cursor, angle);
       curve(out, order - 1, length/2, cursor, angle);
       turn(cursor, angle);
       curve(out, order - 1, length/2, cursor, -angle);
   }

}

void write_sierpinski_arrowhead(FILE* out, int size, int order) {

   const double margin = 20.0;
   const double side = size - 2.0 * margin;
   cursor_t cursor;
   cursor.angle = 0;
   cursor.x = margin;
   cursor.y = 0.5 * size + 0.25 * sqrt(3) * side;
   if ((order & 1) != 0)
       turn(&cursor, -60);
   fprintf(out, "<svg xmlns='http://www.w3.org/2000/svg' width='%d' height='%d'>\n",
           size, size);
   fprintf(out, "<rect width='100%%' height='100%%' fill='white'/>\n");
   fprintf(out, "<path stroke-width='1' stroke='black' fill='none' d='");
   fprintf(out, "M%g,%g\n", cursor.x, cursor.y);
   curve(out, order, side, &cursor, 60);
   fprintf(out, "'/>\n</svg>\n");

}

int main(int argc, char** argv) {

   const char* filename = "sierpinski_arrowhead.svg";
   if (argc == 2)
       filename = argv[1];
   FILE* out = fopen(filename, "w");
   if (!out) {
       perror(filename);
       return EXIT_FAILURE;
   }
   write_sierpinski_arrowhead(out, 600, 8);
   fclose(out);
   return EXIT_SUCCESS;

}</lang>

Output:

See: sierpinski_arrowhead.svg (offsite SVG image)

C++

The output of this program is an SVG file. <lang cpp>#include <fstream>

  1. include <iostream>
  2. include <vector>

constexpr double sqrt3_2 = 0.86602540378444; // sqrt(3)/2

struct point {

   double x;
   double y;

};

std::vector<point> sierpinski_arrowhead_next(const std::vector<point>& points) {

   size_t size = points.size();
   std::vector<point> output(3*(size - 1) + 1);
   double x0, y0, x1, y1;
   size_t j = 0;
   for (size_t i = 0; i + 1 < size; ++i, j += 3) {
       x0 = points[i].x;
       y0 = points[i].y;
       x1 = points[i + 1].x;
       y1 = points[i + 1].y;
       double dx = x1 - x0;
       output[j] = {x0, y0};
       if (y0 == y1) {
           double d = dx * sqrt3_2/2;
           if (d < 0) d = -d;
           output[j + 1] = {x0 + dx/4, y0 - d};
           output[j + 2] = {x1 - dx/4, y0 - d};
       } else if (y1 < y0) {
           output[j + 1] = {x1, y0};
           output[j + 2] = {x1 + dx/2, (y0 + y1)/2};
       } else {
           output[j + 1] = {x0 - dx/2, (y0 + y1)/2};
           output[j + 2] = {x0, y1};
       }
   }
   output[j] = {x1, y1};
   return output;

}

void write_sierpinski_arrowhead(std::ostream& out, int size, int iterations) {

   out << "<svg xmlns='http://www.w3.org/2000/svg' width='"
       << size << "' height='" << size << "'>\n";
   out << "<rect width='100%' height='100%' fill='white'/>\n";
   out << "<path stroke-width='1' stroke='black' fill='none' d='";
   const double margin = 20.0;
   const double side = size - 2.0 * margin;
   const double x = margin;
   const double y = 0.5 * size + 0.5 * sqrt3_2 * side;
   std::vector<point> points{{x, y}, {x + side, y}};
   for (int i = 0; i < iterations; ++i)
       points = sierpinski_arrowhead_next(points);
   for (size_t i = 0, n = points.size(); i < n; ++i)
       out << (i == 0 ? "M" : "L") << points[i].x << ',' << points[i].y << '\n';
   out << "'/>\n</svg>\n";

}

int main() {

   std::ofstream out("sierpinski_arrowhead.svg");
   if (!out) {
       std::cerr << "Cannot open output file\n";
       return EXIT_FAILURE;
   }
   write_sierpinski_arrowhead(out, 600, 8);
   return EXIT_SUCCESS;

}</lang>

Output:

See: sierpinski_arrowhead.svg (offsite SVG image)

Factor

Works with: Factor version 0.99 2020-08-14

<lang factor>USING: accessors L-system ui ;

arrowhead ( L-system -- L-system )
   L-parser-dialect >>commands
   [ 60 >>angle ] >>turtle-values
   "XF" >>axiom
   {
       { "X" "YF+XF+Y" }
       { "Y" "XF-YF-X" }
   } >>rules ;

[ <L-system> arrowhead "Arrowhead" open-window ] with-ui</lang>


When using the L-system visualizer, the following controls apply:

Camera controls
Button Command
a zoom in
z zoom out
left arrow turn left
right arrow turn right
up arrow pitch down
down arrow pitch up
q roll left
w roll right
Other controls
Button Command
x iterate L-system

Go

Library: Go Graphics
Translation of: Phix

A partial translation anyway which produces a static image of a SAC of order 6, magenta on black, which can be viewed with a utility such as EOG. <lang go>package main

import (

   "github.com/fogleman/gg"
   "math"

)

var (

   width  = 770.0
   height = 770.0
   dc     = gg.NewContext(int(width), int(height))
   iy     = 1.0
   theta  = 0

)

var cx, cy, h float64

func arrowhead(order int, length float64) {

   // if order is even, we can just draw the curve
   if order&1 == 0 {
       curve(order, length, 60)
   } else {
       turn(60)
       curve(order, length, -60)
   }
   drawLine(length) // needed to make base symmetric

}

func drawLine(length float64) {

   dc.LineTo(cx-width/2+h, (height-cy)*iy+2*h)
   rads := gg.Radians(float64(theta))
   cx += length * math.Cos(rads)
   cy += length * math.Sin(rads)

}

func turn(angle int) {

   theta = (theta + angle) % 360

}

func curve(order int, length float64, angle int) {

   if order == 0 {
       drawLine(length)
   } else {
       curve(order-1, length/2, -angle)
       turn(angle)
       curve(order-1, length/2, angle)
       turn(angle)
       curve(order-1, length/2, -angle)
   }

}

func main() {

   dc.SetRGB(0, 0, 0) // black background
   dc.Clear()
   order := 6
   if order&1 == 0 {
       iy = -1 // apex will point upwards
   }
   cx, cy = width/2, height
   h = cx / 2
   arrowhead(order, cx)
   dc.SetRGB255(255, 0, 255) // magenta curve
   dc.SetLineWidth(2)
   dc.Stroke()
   dc.SavePNG("sierpinski_arrowhead_curve.png")

}</lang>


Julia

<lang julia>using Lindenmayer # https://github.com/cormullion/Lindenmayer.jl

scurve = LSystem(Dict("F" => "G+F+Gt", "G"=>"F-G-F"), "G")

drawLSystem(scurve,

   forward = 3,
   turn = 60,
   startingy = -350,
   iterations = 8,
   startingorientation = π/3,
   filename = "sierpinski_arrowhead_curve.png",
   showpreview = true

) </lang>


Perl

<lang perl>use strict; use warnings; use SVG; use List::Util qw(max min); use constant pi => 2 * atan2(1, 0);

my %rules = (

   X => 'YF+XF+Y',
   Y => 'XF-YF-X'

); my $S = 'Y'; $S =~ s/([XY])/$rules{$1}/eg for 1..7;

my (@X, @Y); my ($x, $y) = (0, 0); my $theta = 0; my $r = 6;

for (split //, $S) {

   if (/F/) {
       push @X, sprintf "%.0f", $x;
       push @Y, sprintf "%.0f", $y;
       $x += $r * cos($theta);
       $y += $r * sin($theta);
   }
   elsif (/\+/) { $theta += pi/3; }
   elsif (/\-/) { $theta -= pi/3; }

}

my ($xrng, $yrng) = ( max(@X) - min(@X), max(@Y) - min(@Y)); my ($xt, $yt) = (-min(@X) + 10, -min(@Y) + 10);

my $svg = SVG->new(width=>$xrng+20, height=>$yrng+20); my $points = $svg->get_path(x=>\@X, y=>\@Y, -type=>'polyline'); $svg->rect(width=>"100%", height=>"100%", style=>{'fill'=>'black'}); $svg->polyline(%$points, style=>{'stroke'=>'orange', 'stroke-width'=>1}, transform=>"translate($xt,$yt)");

open my $fh, '>', 'sierpinski-arrowhead-curve.svg'; print $fh $svg->xmlify(-namespace=>'svg'); close $fh;</lang> See: sierpinski-arrowhead-curve.svg (offsite SVG image)

Phix

Library: Phix/pGUI

<lang Phix>-- demo\rosetta\Sierpinski_arrowhead_curve.exw -- -- Draws curves lo to hi (simultaneously), initially {6,6}, max {10,10} -- Press +/- to change hi, shift +/- to change lo. -- ("=_" are also mapped to "+-", for the non-numpad +/-) -- include pGUI.e

Ihandle dlg, canvas cdCanvas cddbuffer, cdcanvas

integer width, height,

       lo = 6, hi = 6

atom cx, cy, h, theta

integer iy = +1

procedure draw_line(atom l)

   cdCanvasVertex(cddbuffer, cx-width/2+h, (height-cy)*iy+2*h)
   cx += l*cos(theta*CD_DEG2RAD)
   cy += l*sin(theta*CD_DEG2RAD)

end procedure

procedure turn(integer angle)

   theta = mod(theta+angle,360)

end procedure

procedure curve(integer order, atom l, integer angle)

   if order=0 then
       draw_line(l)
   else
       curve(order-1, l/2, -angle)
       turn(angle)
       curve(order-1, l/2,  angle)
       turn(angle)
       curve(order-1, l/2, -angle)
   end if

end procedure

procedure sierpinski_arrowhead_curve(integer order, atom l)

   -- If order is even we can just draw the curve.
   if and_bits(order,1)=0 then
       curve(order, l, +60)
   else -- order is odd
       turn( +60)
       curve(order, l, -60)
   end if
   draw_line(l)

end procedure

function redraw_cb(Ihandle /*ih*/, integer /*posx*/, integer /*posy*/)

   {width, height} = IupGetIntInt(canvas, "DRAWSIZE")
   cdCanvasActivate(cddbuffer)
   for order=lo to hi do
       cx = width/2
       cy = height
       h = cx/2
       theta = 0
       iy = iff(and_bits(order,1)?-1:+1)
       cdCanvasBegin(cddbuffer, CD_OPEN_LINES)
       sierpinski_arrowhead_curve(order, cx)
       cdCanvasEnd(cddbuffer)
   end for
   cdCanvasFlush(cddbuffer)
   return IUP_DEFAULT

end function

function map_cb(Ihandle ih)

   cdcanvas = cdCreateCanvas(CD_IUP, ih)
   cddbuffer = cdCreateCanvas(CD_DBUFFER, cdcanvas)
   cdCanvasSetBackground(cddbuffer, CD_WHITE)
   cdCanvasSetForeground(cddbuffer, CD_BLUE)
   return IUP_DEFAULT

end function

function key_cb(Ihandle /*ih*/, atom c)

   if c=K_ESC then return IUP_CLOSE end if
   if find(c,"+=-_") then
       bool bShift = IupGetInt(NULL,"SHIFTKEY")
       if c='+' or c='=' then
           if bShift then
               lo = min(lo+1,hi)
           else
               hi = min(10,hi+1)
           end if
       elsif c='-' or c='_' then
           if bShift then
               lo = max(1,lo-1)
           else
               hi = max(lo,hi-1)
           end if
       end if
       IupSetStrAttribute(dlg, "TITLE", "Sierpinski arrowhead curve (%d..%d)",{lo,hi})
       cdCanvasClear(cddbuffer)
       IupUpdate(canvas)
   end if
   return IUP_CONTINUE

end function

procedure main()

   IupOpen()
   
   canvas = IupCanvas(NULL)
   IupSetAttribute(canvas, "RASTERSIZE", "770x770")
   IupSetCallback(canvas, "MAP_CB", Icallback("map_cb"))
   IupSetCallback(canvas, "ACTION", Icallback("redraw_cb"))
   dlg = IupDialog(canvas)
   IupSetAttribute(dlg, "TITLE", "Sierpinski arrowhead curve (6..6)")
   IupSetCallback(dlg, "K_ANY", Icallback("key_cb"))
   IupMap(dlg)
   IupShowXY(dlg,IUP_CENTER,IUP_CENTER)
   IupMainLoop()
   IupClose()

end procedure

main()</lang>

Raku

(formerly Perl 6)

Works with: Rakudo version 2020.02

<lang perl6>use SVG;

role Lindenmayer {

   has %.rules;
   method succ {
       self.comb.map( { %!rules{$^c} // $c } ).join but Lindenmayer(%!rules)
   }

}

my $arrow = 'X' but Lindenmayer( { X => 'YF+XF+Y', Y => 'XF-YF-X' } );

$arrow++ xx 7;

my $w = 800; my $h = ($w * 3**.5 / 2).round(1);

my $scale = 6; my @points = (400, 15); my $dir = pi/3;

for $arrow.comb {

   state ($x, $y) = @points[0,1];
   state $d = $dir;
   when 'F' { @points.append: ($x += $scale * $d.cos).round(1), ($y += $scale * $d.sin).round(1) }
   when '+' { $d += $dir }
   when '-' { $d -= $dir }
   default { }

}

my $out = './sierpinski-arrowhead-curve-perl6.svg'.IO;

$out.spurt: SVG.serialize(

   svg => [
       :width($w), :height($h),
       :rect[:width<100%>, :height<100%>, :fill<black>],
       :polyline[ :points(@points.join: ','), :fill<black>, :style<stroke:#FF4EA9> ],
   ],

);</lang> See: Sierpinski-arrowhead-curve-perl6.svg (offsite SVG image)

Ruby

Library: RubyGems
Library: JRubyArt

For grammar see Hilbert Curve <lang ruby> load_libraries :grammar attr_reader :points

def setup

 sketch_title 'Sierpinski Arrowhead'
 sierpinski = SierpinskiArrowhead.new(Vec2D.new(width * 0.15, height * 0.7))
 production = sierpinski.generate 6 # 6 generations looks OK
 @points = sierpinski.translate_rules(production)
 no_loop

end

def draw

 background(0)
 render points

end

def render(points)

 no_fill
 stroke 200.0
 stroke_weight 3
 begin_shape
 points.each_slice(2) do |v0, v1|
   v0.to_vertex(renderer)
   v1.to_vertex(renderer)
 end
 end_shape

end

def renderer

 @renderer ||= GfxRender.new(g)

end

def settings

 size(800, 800)

end

  1. SierpinskiArrowhead class

class SierpinskiArrowhead

 include Processing::Proxy
 attr_reader :draw_length, :pos, :theta, :axiom, :grammar
 DELTA = PI / 3 # 60 degrees
 def initialize(pos)
   @axiom = 'XF' # Axiom
   rules = {
     'X' => 'YF+XF+Y',
     'Y' => 'XF-YF-X'
   }
   @grammar = Grammar.new(axiom, rules)
   @theta = 0
   @draw_length = 200
   @pos = pos
 end
 def generate(gen)
   @draw_length = draw_length * 0.6**gen
   grammar.generate gen
 end
 def forward(pos)
   pos + Vec2D.from_angle(theta) * draw_length
 end
 def translate_rules(prod)
   [].tap do |pts| # An array to store line vertices as Vec2D
     prod.scan(/./) do |ch|
       case ch
       when 'F'
         new_pos = forward(pos)
         pts << pos << new_pos
         @pos = new_pos
       when '+'
         @theta += DELTA
       when '-'
         @theta -= DELTA
       when 'X', 'Y'
       else
         puts("character #{ch} not in grammar")
       end
     end
   end
 end

end

</lang>

Rust

Output is a file in SVG format. Another variation on the same theme as the C, Go and Phix solutions. <lang rust>// [dependencies] // svg = "0.8.0"

const SQRT3_2: f64 = 0.86602540378444;

use svg::node::element::path::Data;

struct Cursor {

   x: f64,
   y: f64,
   angle: i32,

}

impl Cursor {

   fn new(x: f64, y: f64) -> Cursor {
       Cursor {
           x: x,
           y: y,
           angle: 0,
       }
   }
   fn turn(&mut self, angle: i32) {
       self.angle = (self.angle + angle) % 360;
   }
   fn draw_line(&mut self, data: Data, length: f64) -> Data {
       let theta = (self.angle as f64).to_radians();
       self.x += length * theta.cos();
       self.y += length * theta.sin();
       data.line_to((self.x, self.y))
   }

}

fn curve(mut data: Data, order: usize, length: f64, cursor: &mut Cursor, angle: i32) -> Data {

   if order == 0 {
       return cursor.draw_line(data, length);
   }
   data = curve(data, order - 1, length / 2.0, cursor, -angle);
   cursor.turn(angle);
   data = curve(data, order - 1, length / 2.0, cursor, angle);
   cursor.turn(angle);
   curve(data, order - 1, length / 2.0, cursor, -angle)

}

fn write_sierpinski_arrowhead(file: &str, size: usize, order: usize) -> std::io::Result<()> {

   use svg::node::element::Path;
   use svg::node::element::Rectangle;
   let margin = 20.0;
   let side = (size as f64) - 2.0 * margin;
   let y = 0.5 * (size as f64) + 0.5 * SQRT3_2 * side;
   let x = margin;
   let mut cursor = Cursor::new(x, y);
   if (order & 1) != 0 {
       cursor.turn(-60);
   }
   let mut data = Data::new().move_to((x, y));
   data = curve(data, order, side, &mut cursor, 60);
   let rect = Rectangle::new()
       .set("width", "100%")
       .set("height", "100%")
       .set("fill", "white");
   let mut document = svg::Document::new()
       .set("width", size)
       .set("height", size)
       .add(rect);
   let path = Path::new()
       .set("fill", "none")
       .set("stroke", "black")
       .set("stroke-width", "1")
       .set("d", data);
   document = document.add(path);
   svg::save(file, &document)

}

fn main() {

   write_sierpinski_arrowhead("sierpinski_arrowhead.svg", 600, 8).unwrap();

}</lang>

Output:

See: sierpinski_arrowhead.svg (offsite SVG image)

Sidef

Uses the LSystem() class from Hilbert curve. <lang ruby>var rules = Hash(

   x => 'yF+xF+y',
   y => 'xF-yF-x',

)

var lsys = LSystem(

   width:  550,
   height: 500,
   xoff: -20,
   yoff: -30,
   len:   4,
   turn: -90,
   angle: 60,
   color: 'dark green',

)

lsys.execute('xF', 7, "sierpiński_arrowhead.png", rules)</lang> Output image: Sierpiński arrowhead

zkl

Uses Image Magick and the PPM class from http://rosettacode.org/wiki/Bitmap/Bresenham%27s_line_algorithm#zkl <lang zkl>order:=7; sierpinskiArrowheadCurve(order) : turtle(_,order);

fcn sierpinskiArrowheadCurve(n){ // Lindenmayer system --> Data of As & Bs

  var [const] A="BF+AF+B", B="AF-BF-A";  // Production rules
  var [const] Axiom="AF";
  buf1,buf2 := Data(Void,Axiom).howza(3), Data().howza(3);  // characters
  do(n){
     buf1.pump(buf2.clear(),fcn(c){ if(c=="A") A else if(c=="B") B else c });
     t:=buf1; buf1=buf2; buf2=t;	// swap buffers
  }
  buf1		// n=7 --> 6,560 characters

}

fcn turtle(curve,order){ // Turtle with that can turn +-60*

  const D=10.0, a60=60;
  dir:=order.isOdd and a60 or 0;	   // start direction depends on order
  img,color := PPM(1300,1200), 0x00ff00;  // green on black
  x,y := 10, 10;
  foreach c in (curve){  // A & B are no-op during drawing
     switch(c){

case("F"){ // draw forward a,b := D.toRectangular(dir.toFloat().toRad()); img.line(x,y, (x+=a.round()),(y+=b.round()), color) } case("+"){ dir=(dir - a60)%360; } // turn left 60* case("-"){ dir=(dir + a60)%360; } // turn right 60*

     }
  }
  img.writeJPGFile("sierpinskiArrowheadCurve.zkl.jpg");

}</lang>

Output:

Offsite image at Sierpinski arrowhead curve order 7