You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
407 lines
15 KiB
407 lines
15 KiB
import lsystempkg/raylib, lsystempkg/raygui
|
|
import math, os
|
|
import algorithm, heapqueue, random, options, sequtils, sugar, tables, system
|
|
|
|
# Seed RNG with current time
|
|
randomize()
|
|
|
|
type Term = enum LeafTerm, LineTerm, GoLeft, GoRight, PushTerm, PopTerm
|
|
type Terms = seq[Term]
|
|
|
|
proc rewrite(terms: Terms, maxIterations: int) : Terms =
|
|
var currentTerms: Terms = terms
|
|
var newTerms: Terms
|
|
|
|
for _ in repeat(0, maxIterations):
|
|
# Reset this each iteration to gather new expansions
|
|
newTerms = @[]
|
|
for term in currentTerms:
|
|
case term:
|
|
of LeafTerm:
|
|
case sample(toSeq(0..15)):
|
|
# Instead of using sample, make it a markov chain grammar
|
|
# Allow defining a set of productions that choose randomly
|
|
of 0..10: newTerms &= @[LineTerm, LineTerm, PushTerm, GoRight, LeafTerm, PopTerm, GoLeft, LeafTerm]
|
|
of 11..12: newTerms &= @[LineTerm, LineTerm, PushTerm, GoRight, PopTerm, GoLeft, LeafTerm]
|
|
of 13..15: newTerms &= @[LineTerm, LineTerm, PushTerm, GoRight, LeafTerm, PopTerm, GoLeft]
|
|
else:
|
|
continue
|
|
|
|
else: newTerms &= @[term]
|
|
currentTerms = newTerms
|
|
|
|
# Add a trunk proportional to the number of iterations
|
|
# Maybe should be proportional to the total magnitude of the entire thing somehow?
|
|
for _ in repeat(0, maxIterations):
|
|
currentTerms = @[LineTerm] & currentTerms
|
|
return currentTerms
|
|
|
|
type PolarCoord = object
|
|
angle: float64
|
|
magnitude: float64
|
|
|
|
type StackControl = enum Push, Pop
|
|
|
|
# An intruction along with a change in angle and magnitude (i.e. a vector)
|
|
type DrawInstruction = object
|
|
width: float64
|
|
color: Color
|
|
coord: PolarCoord
|
|
|
|
type
|
|
InstructionKind = enum pkDraw, pkStack
|
|
Instruction = object
|
|
case kind: InstructionKind
|
|
of pkDraw: drawInstruction: DrawInstruction
|
|
of pkStack: stackInstruction: StackControl
|
|
|
|
proc `$` (i: Instruction): string =
|
|
case i.kind:
|
|
of pkDraw: return "angle_change = " & $i.drawInstruction.coord.angle & ", magnitude = " & $i.drawInstruction.coord.magnitude
|
|
of pkStack: return "direction = " & $i.stackInstruction
|
|
|
|
iterator axiomToInstructions(maxIterations: int, magnitude: float64, angle: float64, leafColor: Color = DARKGREEN) : Instruction =
|
|
var currentLeafColor = leafColor
|
|
let axiom = @[LeafTerm]
|
|
let termsToConvert = rewrite(axiom, maxIterations)
|
|
var angle_delta: float64 = angle
|
|
var magnitudes: seq[float64] = @[magnitude]
|
|
var widths: seq[float64] = @[maxIterations.float64]
|
|
var current_magnitude = magnitude
|
|
var current_width: float64 = widths[0]
|
|
# axiom
|
|
yield Instruction(kind: pkDraw, drawInstruction: DrawInstruction(coord: PolarCoord(angle: 180, magnitude: magnitude)))
|
|
|
|
for term in termsToConvert:
|
|
# TODO make this definable by the grammar and/or tweakable
|
|
let angle_delta = angle_delta * sample(@[1.0, 1.0, 0.9])
|
|
case term:
|
|
of LeafTerm:
|
|
# when there's a leaf we want to make the magnitude smaller
|
|
# TODO make this definable by the grammar and/or tweakable
|
|
let leaf_width = (16 * sample(@[1.2, 1.0, 0.50]))
|
|
|
|
# TODO make this definable by the grammar and/or tweakable
|
|
currentLeafColor.r += (sample(@[5, 2, 10, -1, -2, 3]).uint8)
|
|
|
|
yield Instruction(kind: pkDraw,
|
|
drawInstruction:
|
|
DrawInstruction(color: currentLeafColor,
|
|
width: leaf_width,
|
|
coord: PolarCoord(angle: angle_delta, magnitude: magnitudes[0])))
|
|
|
|
yield Instruction(kind: pkDraw,
|
|
drawInstruction:
|
|
DrawInstruction(color: currentLeafColor,
|
|
width: leaf_width,
|
|
coord: PolarCoord(angle: 0, magnitude: -magnitudes[0]))) # hack
|
|
|
|
yield Instruction(kind: pkDraw,
|
|
drawInstruction:
|
|
DrawInstruction(color: currentLeafColor,
|
|
width: leaf_width,
|
|
coord: PolarCoord(angle: -(angle_delta*2),
|
|
magnitude: magnitudes[0])))
|
|
of LineTerm:
|
|
# Draw without changing direction
|
|
yield Instruction(kind: pkDraw,
|
|
drawInstruction:
|
|
DrawInstruction(color: DARKBROWN,
|
|
width: current_width,
|
|
coord: PolarCoord(angle: 0, magnitude: magnitudes[0])))
|
|
|
|
# L-systems don't go "backwards"
|
|
# So you can go left or right on the x-axis at a given angle delta
|
|
of GoLeft:
|
|
# TODO make this definable by the grammar and/or tweakable
|
|
current_magnitude = current_magnitude - (current_magnitude * sample(@[0.05, 0.10]))
|
|
current_width = current_width - (current_width * sample(@[0.15, 0.10]))
|
|
|
|
yield Instruction(kind: pkDraw,
|
|
drawInstruction:
|
|
DrawInstruction(color: DARKBROWN,
|
|
width: current_width,
|
|
coord: PolarCoord(angle: angle_delta, magnitude: current_magnitude)))
|
|
|
|
of GoRight:
|
|
# TODO make this definable by the grammar and/or tweakable
|
|
current_magnitude = current_magnitude - (current_magnitude * sample(@[0.05, 0.01]))
|
|
current_width = current_width - (current_width * sample(@[0.15, 0.10]))
|
|
|
|
yield Instruction(kind: pkDraw,
|
|
drawInstruction:
|
|
DrawInstruction(color: DARKBROWN,
|
|
width: current_width,
|
|
coord: PolarCoord(angle: -angle_delta, magnitude: current_magnitude)))
|
|
|
|
# Control the stack of saved positions
|
|
of PushTerm:
|
|
# Save current location
|
|
magnitudes = @[current_magnitude] & magnitudes
|
|
widths = @[current_width] & widths
|
|
yield Instruction(kind: pkStack, stackInstruction: Push)
|
|
of PopTerm:
|
|
current_magnitude = magnitudes[0]
|
|
current_width = widths[0]
|
|
magnitudes = magnitudes[1..^1]
|
|
widths = widths[1..^1]
|
|
# Pop location stack and set current location to it
|
|
# reset magnitude
|
|
yield Instruction(kind: pkStack, stackInstruction: Pop)
|
|
|
|
# A Position along with its angle
|
|
type StartingPosition = object
|
|
x: float64
|
|
y: float64
|
|
mid: Vector2
|
|
angle: float64 # Defines which direction it will start in
|
|
|
|
type TreeLocation = object
|
|
iterationAngle: float64
|
|
iterationNumber: int
|
|
startingMagnitude: float64
|
|
startingPosition: StartingPosition
|
|
startingColor: Color
|
|
|
|
proc `$` (p: StartingPosition): string =
|
|
return "x = " & $p.x & ", y = " & $p.y & ", angle = " & $p.angle
|
|
|
|
# Line (along with the angle relative to origin
|
|
type DrawLine = object
|
|
start_pos: Vector2
|
|
mid_pos: Vector2
|
|
end_pos: Vector2
|
|
width: float64
|
|
angle: float64
|
|
color: Color
|
|
|
|
proc `$` (d: DrawLine): string =
|
|
return "start_pos = " & $d.start_pos & ", " & "end_pos = " & $d.end_pos
|
|
|
|
proc polarToCartesian(coord: PolarCoord): Vector2 =
|
|
# Convert from polar coordinates to cartesian
|
|
# angle is in degrees
|
|
let new_x = -(coord.magnitude * cos(degToRad(coord.angle)))
|
|
let new_y = coord.magnitude * sin(degToRad(coord.angle))
|
|
|
|
return Vector2(x: new_x, y: new_y)
|
|
|
|
proc cartesianToPolar(start_pos: Vector2, end_pos: Vector2): PolarCoord =
|
|
# Convert from cartesian coordinates to polar
|
|
# angle is in degrees
|
|
|
|
let magnitude = sqrt((end_pos.y - start_pos.y)^2 + (end_pos.x - start_pos.x)^2)
|
|
let angle = radToDeg(arcsin(end_pos.y / magnitude))
|
|
|
|
return PolarCoord(magnitude: magnitude, angle: angle)
|
|
|
|
proc calculateNextLine(inst: DrawInstruction, pos: StartingPosition) : DrawLine =
|
|
# Change the angle
|
|
let new_angle = inst.coord.angle + pos.angle
|
|
let endPosition = polarToCartesian(PolarCoord(magnitude: inst.coord.magnitude, angle: new_angle))
|
|
|
|
result.start_pos = Vector2(x: pos.x, y: pos.y)
|
|
|
|
# Ending position is relative to the starting position, so add the coordinates
|
|
result.end_pos = Vector2(x: result.start_pos.x+endPosition.x, y: result.start_pos.y+endPosition.y)
|
|
result.mid_pos = Vector2(x: result.start_pos.x, y: result.end_pos.y)
|
|
|
|
result.width = inst.width
|
|
result.color = inst.color
|
|
result.angle = new_angle
|
|
|
|
proc executeProgram(instructions: seq[Instruction], starting_pos: StartingPosition) : seq[DrawLine] =
|
|
# each instruction will be followed by a stack control instruction
|
|
var insts = instructions
|
|
var positions = @[starting_pos]
|
|
var current_pos = starting_pos
|
|
|
|
var draw_lines : seq[DrawLine] = @[]
|
|
|
|
while insts.len > 0:
|
|
let inst = insts[0]
|
|
|
|
var nextLine: DrawLine
|
|
|
|
case inst.kind:
|
|
of pkStack:
|
|
if inst.stackInstruction == Push:
|
|
insts = insts[1..^1]
|
|
positions = current_pos & positions
|
|
elif inst.stackInstruction == Pop:
|
|
current_pos = positions[0]
|
|
insts = insts[1..^1]
|
|
positions = positions[1..^1]
|
|
else:
|
|
continue
|
|
of pkDraw:
|
|
nextLine = calculateNextLine(inst.drawInstruction, current_pos)
|
|
let new_position = StartingPosition(x: nextLine.end_pos.x,
|
|
y: nextLine.end_pos.y,
|
|
mid: nextLine.mid_pos,
|
|
angle: nextLine.angle)
|
|
# leave the stack alone, set the current position however
|
|
|
|
draw_lines = draw_lines & @[nextLine]
|
|
insts = insts[1..^1]
|
|
current_pos = new_position
|
|
return draw_lines
|
|
|
|
proc guiLoop*() =
|
|
# TODO get from xlib
|
|
var screenWidth: int = 100
|
|
var screenHeight: int = 100
|
|
|
|
SetConfigFlags(ord(ConfigFlags.FLAG_WINDOW_UNDECORATED))
|
|
|
|
InitWindow(screenWidth, screenHeight, "L-Systems")
|
|
|
|
let monitor = GetCurrentMonitor()
|
|
screenWidth = (monitor.GetMonitorWidth()).int
|
|
screenHeight = (monitor.GetMonitorHeight()).int
|
|
|
|
SetWindowSize(screenWidth, screenHeight)
|
|
SetWindowTitle("L-Systems")
|
|
MaximizeWindow()
|
|
SetTargetFPS(60)
|
|
|
|
# Control variables
|
|
var dragWindow = false
|
|
var restartSimulation = false
|
|
var clearForest = false
|
|
var restartButton = false
|
|
var magnitude: int = 10
|
|
var angle: int = 30
|
|
var iterations = 2
|
|
|
|
var startingPosition_x: float32 = screenWidth/2
|
|
var startingPosition_y: float32 = screenHeight.float32
|
|
var treeLocations: seq[TreeLocation] = @[]
|
|
|
|
var instructionLists: seq[seq[Instruction]] = @[]
|
|
var drawLinesList: seq[seq[DrawLine]] = @[]
|
|
var zoom: float32 = 1
|
|
var rotation: float32 = 0
|
|
var camera_x_offset = screenWidth/2
|
|
var camera_y_offset = screenHeight/2
|
|
var color: Color = DARKGREEN
|
|
|
|
var camera: Camera2D
|
|
|
|
#var testTexture = LoadTextureFromImage(LoadImage("./test_texture.png"))
|
|
|
|
camera.offset = Vector2(x: camera_x_offset, y: camera_y_offset)
|
|
camera.target = Vector2(x: screenWidth/2, y: screenHeight/2)
|
|
|
|
camera.rotation = rotation
|
|
camera.zoom = zoom
|
|
|
|
while not WindowShouldClose():
|
|
BeginDrawing()
|
|
|
|
restartSimulation = GuiButton(Rectangle(x: 0.float32, y: 20.float32, width: 200.float32, height: 50.float32), "Restart".cstring)
|
|
clearForest = GuiButton(Rectangle(x: 0.float32, y: 70.float32, width: 200.float32, height: 50.float32), "Clear".cstring)
|
|
|
|
let iterationsBox = Rectangle(x: 0.float32, y: 120.float32, width: 200.float32, height: 50.float32)
|
|
let magnitudeBox = Rectangle(x: 0.float32, y: 170.float32, width: 200.float32, height: 50.float32)
|
|
let angleBox = Rectangle(x: 0.float32, y: 220.float32, width: 200.float32, height: 50.float32)
|
|
let colorPickerBox = Rectangle(x: 0.float32, y: 270.float32, width: 200.float32, height: 50.float32)
|
|
let mouseVector = Vector2(x: GetMouseX().float64, y: GetMouseY().float64)
|
|
|
|
GuiValueBox(bounds=iterationsBox,
|
|
text="Iterations".cstring,
|
|
value=iterations.addr,
|
|
minValue=1,
|
|
maxValue=15,
|
|
editMode=CheckCollisionPointRec(mouseVector, iterationsBox))
|
|
|
|
GuiValueBox(bounds=magnitudeBox,
|
|
text="Size".cstring,
|
|
value=magnitude.addr,
|
|
minValue=1,
|
|
maxValue=500,
|
|
editMode=CheckCollisionPointRec(mouseVector, magnitudeBox))
|
|
|
|
GuiValueBox(bounds=angleBox,
|
|
text="Angle".cstring,
|
|
value=angle.addr,
|
|
minValue=1,
|
|
maxValue=360,
|
|
editMode=CheckCollisionPointRec(mouseVector, angleBox))
|
|
|
|
color = GuiColorPicker(colorPickerBox, color)
|
|
|
|
if IsKeyDown(KEY_DOWN) and IsKeyDown(KEY_LEFT_CONTROL):
|
|
zoom -= 0.01
|
|
if IsKeyDown(KEY_UP) and IsKeyDown(KEY_LEFT_CONTROL):
|
|
zoom += 0.01
|
|
|
|
if IsKeyDown(KEY_LEFT) and IsKeyDown(KEY_LEFT_CONTROL) and rotation < 360:
|
|
rotation += 1
|
|
if IsKeyDown(KEY_RIGHT) and IsKeyDown(KEY_LEFT_CONTROL) and rotation > -360:
|
|
rotation -= 1
|
|
|
|
camera.zoom = zoom
|
|
camera.rotation = rotation
|
|
|
|
if GetMouseX() <= 0:
|
|
camera_x_offset += 5
|
|
|
|
if GetMouseX() >= (GetScreenWidth() - 10):
|
|
camera_x_offset -= 5
|
|
|
|
if GetMouseY() <= 10:
|
|
camera_y_offset += 5
|
|
|
|
if GetMouseY() >= (GetScreenHeight() - 10):
|
|
camera_y_offset -= 5
|
|
|
|
camera.offset = Vector2(x: camera_x_offset, y: camera_y_offset)
|
|
|
|
if IsKeyDown(KEY_LEFT_CONTROL) and IsMouseButtonPressed(MOUSE_LEFT_BUTTON):
|
|
# Save and place an object
|
|
let newPositionVector = GetScreenToWorld2D(Vector2(x: mouseVector.x, y: mouseVector.y), camera)
|
|
let newPosition = StartingPosition(x: newPositionVector.x, y: newPositionVector.y, angle: 90)
|
|
|
|
# Store the location of the tree and its starting attributes
|
|
treeLocations &= @[TreeLocation(startingPosition: newPosition,
|
|
iterationAngle: angle.float32,
|
|
iterationNumber: iterations,
|
|
startingMagnitude: magnitude.float64,
|
|
startingColor: color)]
|
|
|
|
let newInstructions = toSeq(axiomToInstructions(iterations, magnitude.float64, angle.float64, color))
|
|
drawLinesList &= @[executeProgram(newInstructions, newPosition)]
|
|
|
|
if restartSimulation:
|
|
echo "Re-executing"
|
|
drawLinesList = @[]
|
|
for tree in treeLocations:
|
|
let instructions = toSeq(axiomToInstructions(tree.iterationNumber,
|
|
tree.startingMagnitude,
|
|
tree.iterationAngle,
|
|
tree.startingColor))
|
|
|
|
drawLinesList &= @[executeProgram(instructions, tree.startingPosition)]
|
|
|
|
if clearForest:
|
|
drawLinesList = @[]
|
|
instructionLists = @[]
|
|
treeLocations = @[]
|
|
|
|
# Make sure to clear the background before drawing
|
|
ClearBackground(BLACK)
|
|
|
|
# Only want the camera to apply to drawn stuff, not controls
|
|
BeginMode2D(camera)
|
|
for drawLines in drawLinesList:
|
|
for line in drawLines:
|
|
DrawLineEx(line.start_pos, line.end_pos, line.width, line.color)
|
|
|
|
EndMode2D()
|
|
EndDrawing()
|
|
|
|
CloseWindow()
|
|
|
|
when isMainModule:
|
|
guiLoop()
|
|
|