Skip to content

Commit

Permalink
add turtle for l-system evolving
Browse files Browse the repository at this point in the history
  • Loading branch information
Gryxis committed Sep 1, 2024
1 parent 9a2ebf1 commit ac9d590
Show file tree
Hide file tree
Showing 9 changed files with 505 additions and 166 deletions.
331 changes: 166 additions & 165 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@ng-bootstrap/ng-bootstrap": "^14.1.1",
"@popperjs/core": "^2.11.6",
"bootstrap": "^5.2.3",
"lodash": "^4.17.21",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.12.0"
Expand All @@ -32,6 +33,7 @@
"@angular/compiler-cli": "^15.2.0",
"@angular/localize": "^15.2.0",
"@types/jasmine": "~4.3.0",
"@types/lodash": "^4.17.7",
"jasmine-core": "~4.5.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
Expand All @@ -40,4 +42,4 @@
"karma-jasmine-html-reporter": "~2.0.0",
"typescript": "~4.9.4"
}
}
}
4 changes: 4 additions & 0 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ const routes: Routes = [
path: 'endless-dungeon-crawler',
loadChildren: () => import('./endlessDungeonCrawler/endless-dungeon-crawler.module').then( m => m.EndlessDungeonCrawlerModule),
},
{
path: 'evolving-l-systems',
loadChildren: () => import('./evolvingLSystems/evolving-l-systems.module').then( m => m.EvolvingLSystemsModule),
},
{
path: '',
pathMatch: 'full',
Expand Down
83 changes: 83 additions & 0 deletions src/app/evolvingLSystems/evolving-l-systems.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<h1>
Evolving L Systems
</h1>


<p>
This is a Turtle which will draw some evolutions of L-Systems.
</p>

<p>
This means that you will tell a turtle where it shall go to step-by-step only defining a rule,
and this rule will be re-interpreted for every expansion.
</p>
<p>
Therefore here are some background informations:
</p>

<p>
The turtle has a direction it looks at and a position.
You can the turtle to turn left or right by a fixed degree.
And you can tell the turtle to take one step into the direction.

At the end I will show you which way the turtle took.
</p>

<p>
A L-System is a Grammar which has one function.
And the function will lead to a combination of 'F', '-', '+', '[' or ']'
End for each expansion any 'F' will be replaced by the value of the function.

e.g. the Function F -> F - F will lead to F-F by one expansion, F-F - F-F by two expansion and so forth.
</p>

<div class="was-validated">
<p>
<label>
The right side of the Function: F ->
<span class="input-group is-invalid" style="width: 10rem; display: inline-flex;">
<input type="text" class="form-control" [(ngModel)]="LGrammar" [min]="1" pattern="[fF\-\+\[\] ]*"/>

</span>
</label>
</p>
<p>
<label>
the rotation angle of the turtle in degrees:
<span class="input-group is-invalid" style="width: 10rem; display: inline-flex;">
<input type="number" class="form-control" [(ngModel)]="angle" [min]="1" [max]="360"/>

</span>
</label>
</p>
<p>
<label>
how often shall I expand the grammar?:
<span class="input-group is-invalid" style="width: 10rem; display: inline-flex;">
<input type="number" class="form-control" [(ngModel)]="expansions" [min]="0"/>
</span>
</label>
</p>
<p>
<label>
how far does the turtle go with every step ?:
<span class="input-group is-invalid" style="width: 10rem; display: inline-flex;">
<input type="number" class="form-control" [(ngModel)]="stepSize" [min]="1"/>
</span>
</label>
</p>
</div>

<p class="mb-5">
<button type="button"
class="btn btn-outline-primary"
(click)="onStart()">
Start:
</button>
</p>


<p style="height: 2rem;"></p>

<canvas #turtleCanvas [attr.width]="canvasWidth" [attr.height]="canvasHeight">
</canvas>
Empty file.
129 changes: 129 additions & 0 deletions src/app/evolvingLSystems/evolving-l-systems.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { Turtle } from './turtle';

@Component({
selector: 'app-evolving-l-systems',
templateUrl: './evolving-l-systems.component.html',
styleUrls: ['./evolving-l-systems.component.scss']
})
export class EvolvingLSystemsComponent implements AfterViewInit {

// e.g. F + F - F -F + F
public LGrammar: string = ' F + F - F -F + F';

public stepSize: number = 8;

public expansions: number = 1;
public angle: number = 90;

@ViewChild('turtleCanvas')
private turtleCanvas!: ElementRef<HTMLCanvasElement>;
private canvasContext!: CanvasRenderingContext2D;

public canvasWidth : number = 64;
public canvasHeight : number = 64;


ngAfterViewInit(): void {
this.canvasContext = this.turtleCanvas?.nativeElement.getContext('2d') as CanvasRenderingContext2D;
}


public onStart(): void {

if (this.expansions < 0) {
return;
}

if (this.LGrammar.length == 0) {
return;
}

const commands = this.expand();

const turtle = new Turtle();

for (let i = 0; i < commands.length; i++) {
const current = commands.charAt(i);

switch (current) {
case 'F':
case 'f':
turtle.step();
break;
case '-':
turtle.rotate( -1 * this.angle);
break;
case '+':
turtle.rotate( this.angle);
break;

// TODO: add stack for self-similarity
default:
break;
}
}

this.drawPath( turtle);

}

private expand(): string {
let result = this.LGrammar;

for (let i = 0; i < this.expansions; i++) {
result = result.replaceAll( 'F', this.LGrammar);
}

return result.replaceAll(' ', '');
}

private drawPath( turtle: Turtle) {
const span = turtle.getDimension();
const padding = 1;
this.canvasWidth = span.bottomRight.x - span.topLeft.x + 2 * padding; // add padding
this.canvasWidth = Math.max( this.canvasWidth, 1 + 2 * padding);

this.canvasHeight = span.topLeft.y - span.bottomRight.y + 2 * padding ;
this.canvasHeight = Math.max( this.canvasHeight, 1 + 2 * padding );


this.canvasWidth *= this.stepSize;
this.canvasHeight *= this.stepSize;



setTimeout( () => {

this.canvasContext.clearRect( 0, 0, this.canvasWidth, this.canvasHeight);

this.canvasContext.beginPath();

// shift turtle away from dimension minimum
const origin = { x : Math.abs( span.topLeft.x) + padding, y: Math.abs( span.bottomRight.y) + padding};

this.canvasContext.moveTo( origin.x * this.stepSize , this.canvasHeight - origin.y * this.stepSize);

turtle.getPath().reduce( (prev, cur, idx, arr) => {
if ( !!prev && prev.x == cur.x && prev.y == cur.y || cur.teleported) {
return cur;
}
const posX = (cur.x + origin.x) * this.stepSize;
const posY = this.canvasHeight - (cur.y + origin.y) * this.stepSize; // flip y since canvas y direction is from top to bottom

this.canvasContext.lineTo( posX, posY);

return cur;
});

this.canvasContext.stroke();

}, 0);
}

}

interface IPosition {
x: number,
y: number,
};
29 changes: 29 additions & 0 deletions src/app/evolvingLSystems/evolving-l-systems.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { EvolvingLSystemsComponent } from './evolving-l-systems.component';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';

const routes: Routes = [
{
path: '',
component: EvolvingLSystemsComponent,
}
];

@NgModule({
declarations: [
EvolvingLSystemsComponent,
],
imports: [
CommonModule,
FormsModule,
NgbTooltipModule,
RouterModule.forChild(routes),
],
exports: [
RouterModule,
]
})
export class EvolvingLSystemsModule { }
86 changes: 86 additions & 0 deletions src/app/evolvingLSystems/turtle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as _ from 'lodash';

export class Turtle {

private pos: IPosition;

public getPosition(): IPosition {
return _.cloneDeep( this.pos);
}

private dimension: Rectangle = {
topLeft: { x: 0, y:0},
bottomRight: { x: 0, y:0},
}

public getDimension(): Rectangle {
return _.cloneDeep( this.dimension);
}

private path: Array<IPosition>;

public getPath() {
return _.cloneDeep( this.path);
}

constructor() {
this.pos = { x: 0, y: 0, dir: 0};
this.path = [ _.cloneDeep( this.pos)];
}

public rotate( degree: number) {
this.pos.dir += degree;
this.path.push ( _.cloneDeep( this.pos));
}

public step() {
const radians = this.pos.dir / 360 * Math.PI * 2;

const stepSizeX = Math.cos( radians);
const stepSizeY = Math.sin( radians);

this.pos.x += stepSizeX,
this.pos.y += stepSizeY,

this.path.push ( _.cloneDeep( this.pos));
this.updateDimension();
}


public teleport( newPosition : IPosition) {
this.pos = _.cloneDeep( newPosition);
this.pos.teleported = true;
this.updateDimension();
this.path.push ( _.cloneDeep( this.pos));
}

private updateDimension() {
this.dimension.topLeft.x = Math.min( this.pos.x, this.dimension.topLeft.x);
this.dimension.topLeft.y = Math.max( this.pos.y, this.dimension.topLeft.y);
this.dimension.bottomRight.x = Math.max( this.pos.x, this.dimension.bottomRight.x);
this.dimension.bottomRight.y = Math.min( this.pos.y, this.dimension.bottomRight.y);
}

}

export interface IPosition {
/**
* x-Position
*/
x: number,

/**
* y-Position
*/
y: number,

/** direction in degrees */
dir: number,

teleported?: boolean
}

export interface Rectangle {
topLeft: { x: number, y: number},
bottomRight: { x: number, y: number},
}
5 changes: 5 additions & 0 deletions src/app/home/home.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@ export class HomeComponent {
link: 'endless-dungeon-crawler',
description: 'an example of a procedurally generated dungeon crawler using cellular automata.',
},
{
title: 'Evolving L-systems',
link: 'evolving-l-systems',
description: 'an example of a turtle machine evolving L-systems.',
},
];
}

0 comments on commit ac9d590

Please sign in to comment.