pybonsai 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pybonsai-0.1.0/.gitignore +2 -0
- pybonsai-0.1.0/Images/demo-windows.gif +0 -0
- pybonsai-0.1.0/Images/demo.gif +0 -0
- pybonsai-0.1.0/Images/env-example.png +0 -0
- pybonsai-0.1.0/Images/options/big.png +0 -0
- pybonsai-0.1.0/Images/options/chars.png +0 -0
- pybonsai-0.1.0/Images/options/leafy.png +0 -0
- pybonsai-0.1.0/Images/types/classic.png +0 -0
- pybonsai-0.1.0/Images/types/fib.png +0 -0
- pybonsai-0.1.0/Images/types/offset_fib.png +0 -0
- pybonsai-0.1.0/Images/types/rand_fib.png +0 -0
- pybonsai-0.1.0/LICENSE +7 -0
- pybonsai-0.1.0/PKG-INFO +163 -0
- pybonsai-0.1.0/README.md +144 -0
- pybonsai-0.1.0/install.sh +7 -0
- pybonsai-0.1.0/pybonsai/__init__.py +0 -0
- pybonsai-0.1.0/pybonsai/__main__.py +222 -0
- pybonsai-0.1.0/pybonsai/draw.py +243 -0
- pybonsai-0.1.0/pybonsai/tests/.gitkeep +0 -0
- pybonsai-0.1.0/pybonsai/tree.py +416 -0
- pybonsai-0.1.0/pybonsai/utils.py +66 -0
- pybonsai-0.1.0/pyproject.toml +33 -0
- pybonsai-0.1.0/uv.lock +8 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
pybonsai-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 Ivan Karlov
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
pybonsai-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pybonsai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A project that generates ASCII art trees in your terminal.
|
|
5
|
+
Project-URL: Source, https://github.com/DaEtoJostka/PyBonsai
|
|
6
|
+
Project-URL: Original, https://github.com/Ben-Edwards44/PyBonsai
|
|
7
|
+
Author: Ben Edwards
|
|
8
|
+
Author-email: Ivan Karlov <karlovivan679@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: pybonsai
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# PyBonsai :palm_tree:
|
|
21
|
+
|
|
22
|
+
PyBonsai is a Python script that generates procedural ASCII art trees in the comfort of your terminal.
|
|
23
|
+
|
|
24
|
+
## About :seedling:
|
|
25
|
+
|
|
26
|
+
<img src="Images/demo.gif" align="right" width="450px">
|
|
27
|
+
|
|
28
|
+
PyBonsai is inspired by the amazing [cbonsai](https://gitlab.com/jallbrit/cbonsai) repository.
|
|
29
|
+
Whereas cbonsai grows bonsai trees, PyBonsai trees look more like trees you would find in a forest (oak, ash and so on).
|
|
30
|
+
|
|
31
|
+
The trees are configurable via CLI options to make them different sizes, more or less complex, grow at different rates, or use a different set of characters. See [useage](#useage-wrench) for more information.
|
|
32
|
+
|
|
33
|
+
Currently, PyBonsai supports 4 different types of tree. Details of these are shown in the [tree types](#tree-types-leaves) section.
|
|
34
|
+
|
|
35
|
+
PyBonsai uses [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) for colouring characters. Almost all modern terminals will support this but, if yours does not, PyBonsai will not work.
|
|
36
|
+
|
|
37
|
+
If you encounter an issue with PyBonsai, or have an idea for an improvement, do not be afraid to open an issue.
|
|
38
|
+
|
|
39
|
+
## Installation :link:
|
|
40
|
+
|
|
41
|
+
Requirements:
|
|
42
|
+
|
|
43
|
+
- Python 3.9 or greater
|
|
44
|
+
|
|
45
|
+
To use PyBonsai, you need to first clone the repository:
|
|
46
|
+
|
|
47
|
+
git clone https://github.com/Ben-Edwards44/PyBonsai.git
|
|
48
|
+
|
|
49
|
+
### Linux
|
|
50
|
+
|
|
51
|
+
To install for all users, run the included `install.sh` script to create a symlink in `/usr/local/bin`
|
|
52
|
+
|
|
53
|
+
cd PyBonsai
|
|
54
|
+
sudo bash install.sh
|
|
55
|
+
|
|
56
|
+
Verify the installation by running:
|
|
57
|
+
|
|
58
|
+
pybonsai --version
|
|
59
|
+
|
|
60
|
+
### Windows
|
|
61
|
+
|
|
62
|
+
After cloning the repository you need to follow these steps in order to run PyBonsai in your terminal:
|
|
63
|
+
|
|
64
|
+
1. Install the `pyinstaller` package
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
pip install pyinstaller
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
> You may need to run the terminal in admin mode while installing `pyinstaller`
|
|
71
|
+
|
|
72
|
+
2. Change to the directory containing the cloned repo and run the `pyinstaller` command on `main.py`
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
cd directory-that-contains-pybonsai
|
|
76
|
+
pyinstaller main.py
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This will create a new `dist` folder with an executable of the PyBonsai, you can test it by changing to the `dist` folder and running `main.exe`
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
cd dist
|
|
83
|
+
main.exe
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
You should see something like this:
|
|
87
|
+
|
|
88
|
+
<img src="Images/demo-windows.gif" width="450px">
|
|
89
|
+
|
|
90
|
+
<br />
|
|
91
|
+
|
|
92
|
+
Now rename the `main.exe` to `pybonsai.exe`
|
|
93
|
+
|
|
94
|
+
3. Copy the path of the `dist` folder and add it to your `Path` environment variable
|
|
95
|
+
|
|
96
|
+
<img src="Images/env-example.png" width="450px">
|
|
97
|
+
|
|
98
|
+
<br />
|
|
99
|
+
|
|
100
|
+
Now you can run PyBonsai with the command `pybonsai.exe` or `pybonsai`
|
|
101
|
+
|
|
102
|
+
You can even pass flags
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
pybonsai --layers 10
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### MacOS
|
|
109
|
+
|
|
110
|
+
Please note that PyBonsai has not been tested on MacOS, but I see no reason why it would not work on Mac OS.
|
|
111
|
+
To install PyBonsai, you will need to clone the repository and add it to your system's PATH.
|
|
112
|
+
|
|
113
|
+
## Usage :wrench:
|
|
114
|
+
|
|
115
|
+
Run `pybonsai --help` for useage:
|
|
116
|
+
|
|
117
|
+
USEAGE pybonsai [OPTION]...
|
|
118
|
+
|
|
119
|
+
PyBonsai procedurally generates ASCII art trees in your terminal.
|
|
120
|
+
|
|
121
|
+
OPTIONS:
|
|
122
|
+
-h, --help display help
|
|
123
|
+
--version display version
|
|
124
|
+
|
|
125
|
+
-s, --seed seed for the random number generator
|
|
126
|
+
|
|
127
|
+
-i, --instant instant mode: display finished tree immediately
|
|
128
|
+
-w, --wait time delay between drawing characters when not in instant mode [default 0]
|
|
129
|
+
|
|
130
|
+
-c, --branch-chars string of chars randomly chosen for branches [default "~;:="]
|
|
131
|
+
-C, --leaf-chars string of chars randomly chosen for leaves [default "&%#@"]
|
|
132
|
+
|
|
133
|
+
-x, --width maximum width of the tree [default 80]
|
|
134
|
+
-y, --height maximum height of the tree [default 25]
|
|
135
|
+
|
|
136
|
+
-t, --type tree type: integer between 0 and 3 inclusive [default random]
|
|
137
|
+
-S, --start-len length of the root branch [default 15]
|
|
138
|
+
-L, --leaf-len length of each leaf [default 4]
|
|
139
|
+
-l, --layers number of branch layers: more => more branches [default 8]
|
|
140
|
+
-a, --angle mean angle of branches to their parent, in degrees; more => more arched trees [default 40]
|
|
141
|
+
|
|
142
|
+
-f, --fixed-window do not allow window height to increase when tree grows off screen
|
|
143
|
+
|
|
144
|
+
The following images demonstrate the use of the different options:
|
|
145
|
+
|
|
146
|
+
| Effect | Image |
|
|
147
|
+
| -------------------- | -------------------------------------------------- |
|
|
148
|
+
| Big tree |  |
|
|
149
|
+
| Different characters |  |
|
|
150
|
+
| Longer leaves |  |
|
|
151
|
+
|
|
152
|
+
## Tree Types :leaves:
|
|
153
|
+
|
|
154
|
+
PyBonsai supports 4 different tree types. Unless specified with the `--type` option, the tree type will be chosen at random.
|
|
155
|
+
|
|
156
|
+
All tree types are generated recursively and are, essentially, variations on [this](https://www.youtube.com/watch?v=0jjeOYMjmDU) basic fractal tree.
|
|
157
|
+
|
|
158
|
+
| Type | Image | Description |
|
|
159
|
+
| ---------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
160
|
+
| Classic |  | The number of child branches is normally distributed with $\mu = 2$ and $\sigma = 0.5$. |
|
|
161
|
+
| Fibonacci |  | The number of branches on the $n^{th}$ layer is the $n^{th}$ fibonacci number. |
|
|
162
|
+
| Offset fibonacci |  | Similar to above, except child branches grow in the middle of the parent as well as the end. |
|
|
163
|
+
| Random fibonacci |  | Similar to above, except child branches grow at random positions on the parent and leaves can grow in the middle layers. |
|
pybonsai-0.1.0/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# PyBonsai :palm_tree:
|
|
2
|
+
|
|
3
|
+
PyBonsai is a Python script that generates procedural ASCII art trees in the comfort of your terminal.
|
|
4
|
+
|
|
5
|
+
## About :seedling:
|
|
6
|
+
|
|
7
|
+
<img src="Images/demo.gif" align="right" width="450px">
|
|
8
|
+
|
|
9
|
+
PyBonsai is inspired by the amazing [cbonsai](https://gitlab.com/jallbrit/cbonsai) repository.
|
|
10
|
+
Whereas cbonsai grows bonsai trees, PyBonsai trees look more like trees you would find in a forest (oak, ash and so on).
|
|
11
|
+
|
|
12
|
+
The trees are configurable via CLI options to make them different sizes, more or less complex, grow at different rates, or use a different set of characters. See [useage](#useage-wrench) for more information.
|
|
13
|
+
|
|
14
|
+
Currently, PyBonsai supports 4 different types of tree. Details of these are shown in the [tree types](#tree-types-leaves) section.
|
|
15
|
+
|
|
16
|
+
PyBonsai uses [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) for colouring characters. Almost all modern terminals will support this but, if yours does not, PyBonsai will not work.
|
|
17
|
+
|
|
18
|
+
If you encounter an issue with PyBonsai, or have an idea for an improvement, do not be afraid to open an issue.
|
|
19
|
+
|
|
20
|
+
## Installation :link:
|
|
21
|
+
|
|
22
|
+
Requirements:
|
|
23
|
+
|
|
24
|
+
- Python 3.9 or greater
|
|
25
|
+
|
|
26
|
+
To use PyBonsai, you need to first clone the repository:
|
|
27
|
+
|
|
28
|
+
git clone https://github.com/Ben-Edwards44/PyBonsai.git
|
|
29
|
+
|
|
30
|
+
### Linux
|
|
31
|
+
|
|
32
|
+
To install for all users, run the included `install.sh` script to create a symlink in `/usr/local/bin`
|
|
33
|
+
|
|
34
|
+
cd PyBonsai
|
|
35
|
+
sudo bash install.sh
|
|
36
|
+
|
|
37
|
+
Verify the installation by running:
|
|
38
|
+
|
|
39
|
+
pybonsai --version
|
|
40
|
+
|
|
41
|
+
### Windows
|
|
42
|
+
|
|
43
|
+
After cloning the repository you need to follow these steps in order to run PyBonsai in your terminal:
|
|
44
|
+
|
|
45
|
+
1. Install the `pyinstaller` package
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
pip install pyinstaller
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> You may need to run the terminal in admin mode while installing `pyinstaller`
|
|
52
|
+
|
|
53
|
+
2. Change to the directory containing the cloned repo and run the `pyinstaller` command on `main.py`
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
cd directory-that-contains-pybonsai
|
|
57
|
+
pyinstaller main.py
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This will create a new `dist` folder with an executable of the PyBonsai, you can test it by changing to the `dist` folder and running `main.exe`
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
cd dist
|
|
64
|
+
main.exe
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
You should see something like this:
|
|
68
|
+
|
|
69
|
+
<img src="Images/demo-windows.gif" width="450px">
|
|
70
|
+
|
|
71
|
+
<br />
|
|
72
|
+
|
|
73
|
+
Now rename the `main.exe` to `pybonsai.exe`
|
|
74
|
+
|
|
75
|
+
3. Copy the path of the `dist` folder and add it to your `Path` environment variable
|
|
76
|
+
|
|
77
|
+
<img src="Images/env-example.png" width="450px">
|
|
78
|
+
|
|
79
|
+
<br />
|
|
80
|
+
|
|
81
|
+
Now you can run PyBonsai with the command `pybonsai.exe` or `pybonsai`
|
|
82
|
+
|
|
83
|
+
You can even pass flags
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
pybonsai --layers 10
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### MacOS
|
|
90
|
+
|
|
91
|
+
Please note that PyBonsai has not been tested on MacOS, but I see no reason why it would not work on Mac OS.
|
|
92
|
+
To install PyBonsai, you will need to clone the repository and add it to your system's PATH.
|
|
93
|
+
|
|
94
|
+
## Usage :wrench:
|
|
95
|
+
|
|
96
|
+
Run `pybonsai --help` for useage:
|
|
97
|
+
|
|
98
|
+
USEAGE pybonsai [OPTION]...
|
|
99
|
+
|
|
100
|
+
PyBonsai procedurally generates ASCII art trees in your terminal.
|
|
101
|
+
|
|
102
|
+
OPTIONS:
|
|
103
|
+
-h, --help display help
|
|
104
|
+
--version display version
|
|
105
|
+
|
|
106
|
+
-s, --seed seed for the random number generator
|
|
107
|
+
|
|
108
|
+
-i, --instant instant mode: display finished tree immediately
|
|
109
|
+
-w, --wait time delay between drawing characters when not in instant mode [default 0]
|
|
110
|
+
|
|
111
|
+
-c, --branch-chars string of chars randomly chosen for branches [default "~;:="]
|
|
112
|
+
-C, --leaf-chars string of chars randomly chosen for leaves [default "&%#@"]
|
|
113
|
+
|
|
114
|
+
-x, --width maximum width of the tree [default 80]
|
|
115
|
+
-y, --height maximum height of the tree [default 25]
|
|
116
|
+
|
|
117
|
+
-t, --type tree type: integer between 0 and 3 inclusive [default random]
|
|
118
|
+
-S, --start-len length of the root branch [default 15]
|
|
119
|
+
-L, --leaf-len length of each leaf [default 4]
|
|
120
|
+
-l, --layers number of branch layers: more => more branches [default 8]
|
|
121
|
+
-a, --angle mean angle of branches to their parent, in degrees; more => more arched trees [default 40]
|
|
122
|
+
|
|
123
|
+
-f, --fixed-window do not allow window height to increase when tree grows off screen
|
|
124
|
+
|
|
125
|
+
The following images demonstrate the use of the different options:
|
|
126
|
+
|
|
127
|
+
| Effect | Image |
|
|
128
|
+
| -------------------- | -------------------------------------------------- |
|
|
129
|
+
| Big tree |  |
|
|
130
|
+
| Different characters |  |
|
|
131
|
+
| Longer leaves |  |
|
|
132
|
+
|
|
133
|
+
## Tree Types :leaves:
|
|
134
|
+
|
|
135
|
+
PyBonsai supports 4 different tree types. Unless specified with the `--type` option, the tree type will be chosen at random.
|
|
136
|
+
|
|
137
|
+
All tree types are generated recursively and are, essentially, variations on [this](https://www.youtube.com/watch?v=0jjeOYMjmDU) basic fractal tree.
|
|
138
|
+
|
|
139
|
+
| Type | Image | Description |
|
|
140
|
+
| ---------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
141
|
+
| Classic |  | The number of child branches is normally distributed with $\mu = 2$ and $\sigma = 0.5$. |
|
|
142
|
+
| Fibonacci |  | The number of branches on the $n^{th}$ layer is the $n^{th}$ fibonacci number. |
|
|
143
|
+
| Offset fibonacci |  | Similar to above, except child branches grow in the middle of the parent as well as the end. |
|
|
144
|
+
| Random fibonacci |  | Similar to above, except child branches grow at random positions on the parent and leaves can grow in the middle layers. |
|
|
File without changes
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# #&
|
|
6
|
+
# %&@&
|
|
7
|
+
# &%@% %& %@|
|
|
8
|
+
# &&@@&%#@%_\@@&&@# @=&
|
|
9
|
+
# &#@# #%## ;&@%# %@ @% &%
|
|
10
|
+
# @ % #& %%~| @%@%@&#
|
|
11
|
+
# |;; # %
|
|
12
|
+
# \\ @ % %@%#
|
|
13
|
+
# |~ =;@ __%%
|
|
14
|
+
# =| ~_=___ %&#
|
|
15
|
+
# || /~ % #
|
|
16
|
+
# |// &
|
|
17
|
+
# |=
|
|
18
|
+
# ~|
|
|
19
|
+
# ;|
|
|
20
|
+
# .---. ./||\. .-.
|
|
21
|
+
#
|
|
22
|
+
# I speak for the trees, for the trees have no voice.
|
|
23
|
+
# - The Lorax, 1971
|
|
24
|
+
#
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
from . import draw
|
|
28
|
+
from . import tree
|
|
29
|
+
import argparse
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
import random
|
|
33
|
+
from math import radians
|
|
34
|
+
from os import get_terminal_size
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
VERSION = "1.2.2"
|
|
38
|
+
DESC = "PyBonsai procedurally generates ASCII art trees in your terminal."
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Options:
|
|
42
|
+
#stores all parameters that can be edited via the command line arguments
|
|
43
|
+
|
|
44
|
+
#default values
|
|
45
|
+
NUM_LAYERS = 8
|
|
46
|
+
INITIAL_LEN = 15
|
|
47
|
+
ANGLE_MEAN = 40
|
|
48
|
+
|
|
49
|
+
LEAF_LEN = 4
|
|
50
|
+
|
|
51
|
+
INSTANT = False
|
|
52
|
+
WAIT_TIME = 0
|
|
53
|
+
|
|
54
|
+
BRANCH_CHARS = "~;:="
|
|
55
|
+
LEAF_CHARS = "&%#@"
|
|
56
|
+
|
|
57
|
+
WINDOW_WIDTH = 80
|
|
58
|
+
WINDOW_HEIGHT = 25
|
|
59
|
+
|
|
60
|
+
FIXED = False
|
|
61
|
+
|
|
62
|
+
def __init__(self):
|
|
63
|
+
#set the default values
|
|
64
|
+
self.num_layers = Options.NUM_LAYERS
|
|
65
|
+
self.initial_len = Options.INITIAL_LEN
|
|
66
|
+
self.angle_mean = radians(Options.ANGLE_MEAN)
|
|
67
|
+
|
|
68
|
+
self.leaf_len = Options.LEAF_LEN
|
|
69
|
+
|
|
70
|
+
self.instant = Options.INSTANT
|
|
71
|
+
self.wait_time = Options.WAIT_TIME
|
|
72
|
+
|
|
73
|
+
self.branch_chars = Options.BRANCH_CHARS
|
|
74
|
+
self.leaf_chars = Options.LEAF_CHARS
|
|
75
|
+
|
|
76
|
+
self.user_set_type = False
|
|
77
|
+
self.type = random.randint(0, 3)
|
|
78
|
+
self.fractal_mode = False
|
|
79
|
+
|
|
80
|
+
self.fixed_window = Options.FIXED
|
|
81
|
+
|
|
82
|
+
self.window_width, self.window_height = self.get_default_window()
|
|
83
|
+
|
|
84
|
+
def get_default_window(self):
|
|
85
|
+
#ensure the default values fit the current terminal size
|
|
86
|
+
width, height = get_terminal_size()
|
|
87
|
+
|
|
88
|
+
#check the default values fit the current terminal
|
|
89
|
+
width = min(width, Options.WINDOW_WIDTH)
|
|
90
|
+
height = min(height, Options.WINDOW_HEIGHT)
|
|
91
|
+
|
|
92
|
+
return width, height
|
|
93
|
+
|
|
94
|
+
def set_seed(self, seed):
|
|
95
|
+
random.seed(seed)
|
|
96
|
+
|
|
97
|
+
#the type must be re-chosen because the rng seed has been changed (this ensures repeatable results)
|
|
98
|
+
if not self.user_set_type:
|
|
99
|
+
self.type = random.randint(0, 3)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _print_help():
|
|
103
|
+
OPTION_DESCS = f"""OPTIONS:
|
|
104
|
+
-h, --help display help
|
|
105
|
+
--version display version
|
|
106
|
+
|
|
107
|
+
-s, --seed seed for the random number generator
|
|
108
|
+
|
|
109
|
+
-i, --instant instant mode: display finished tree immediately
|
|
110
|
+
-w, --wait time delay between drawing characters when not in instant mode [default {Options.WAIT_TIME}]
|
|
111
|
+
|
|
112
|
+
-c, --branch-chars string of chars randomly chosen for branches [default "{Options.BRANCH_CHARS}"]
|
|
113
|
+
-C, --leaf-chars string of chars randomly chosen for leaves [default "{Options.LEAF_CHARS}"]
|
|
114
|
+
|
|
115
|
+
-x, --width maximum width of the tree [default {Options.WINDOW_WIDTH}]
|
|
116
|
+
-y, --height maximum height of the tree [default {Options.WINDOW_HEIGHT}]
|
|
117
|
+
|
|
118
|
+
-t, --type tree type: integer between 0 and 3 inclusive [default random]
|
|
119
|
+
-S, --start-len length of the root branch [default {Options.INITIAL_LEN}]
|
|
120
|
+
-L, --leaf-len length of each leaf [default {Options.LEAF_LEN}]
|
|
121
|
+
-l, --layers number of branch layers: more => more branches [default {Options.NUM_LAYERS}]
|
|
122
|
+
-a, --angle mean angle of branches to their parent, in degrees; more => more arched trees [default {Options.ANGLE_MEAN}]
|
|
123
|
+
|
|
124
|
+
-f, --fixed-window do not allow window height to increase when tree grows off screen
|
|
125
|
+
"""
|
|
126
|
+
usage = "usage: pybonsai [-h] [--version] [-s SEED] [-i] [-w WAIT] [-c BRANCH_CHARS]\n" \
|
|
127
|
+
" [-C LEAF_CHARS] [-x WIDTH] [-y HEIGHT] [-t {{0,1,2,3}}]\n" \
|
|
128
|
+
" [-S START_LEN] [-L LEAF_LEN] [-l LAYERS] [-a ANGLE] [-f]"
|
|
129
|
+
|
|
130
|
+
print(usage)
|
|
131
|
+
print()
|
|
132
|
+
print(DESC)
|
|
133
|
+
print()
|
|
134
|
+
print(OPTION_DESCS)
|
|
135
|
+
exit()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def parse_cli_args():
|
|
139
|
+
if '-h' in sys.argv or '--help' in sys.argv:
|
|
140
|
+
_print_help()
|
|
141
|
+
#convert sys.argv into a dictionary in the form {option_name : option_value}
|
|
142
|
+
options = Options()
|
|
143
|
+
default_width, default_height = options.get_default_window()
|
|
144
|
+
|
|
145
|
+
parser = argparse.ArgumentParser(
|
|
146
|
+
description=DESC,
|
|
147
|
+
epilog = "Based on PyBonsai by Ben Edwards.",
|
|
148
|
+
add_help=False
|
|
149
|
+
)
|
|
150
|
+
parser.add_argument('--version', action='version', version=f'PyBonsai version {VERSION}')
|
|
151
|
+
parser.add_argument('-s', '--seed', type=int)
|
|
152
|
+
parser.add_argument('-i', '--instant', action='store_true')
|
|
153
|
+
parser.add_argument('-w', '--wait', type=float, default=options.wait_time)
|
|
154
|
+
parser.add_argument('-c', '--branch-chars', type=str, default=options.branch_chars)
|
|
155
|
+
parser.add_argument('-C', '--leaf-chars', type=str, default=options.leaf_chars)
|
|
156
|
+
parser.add_argument('-x', '--width', type=int, default=default_width)
|
|
157
|
+
parser.add_argument('-y', '--height', type=int, default=default_height)
|
|
158
|
+
parser.add_argument('-t', '--type', type=int, choices=range(4))
|
|
159
|
+
parser.add_argument('-S', '--start-len', type=int, default=options.initial_len)
|
|
160
|
+
parser.add_argument('-L', '--leaf-len', type=int, default=options.leaf_len)
|
|
161
|
+
parser.add_argument('-l', '--layers', type=int, default=options.num_layers)
|
|
162
|
+
parser.add_argument('-a', '--angle', type=int, default=Options.ANGLE_MEAN)
|
|
163
|
+
parser.add_argument('-f', '--fixed-window', action='store_true')
|
|
164
|
+
|
|
165
|
+
args = parser.parse_args()
|
|
166
|
+
|
|
167
|
+
# Update options with parsed arguments
|
|
168
|
+
options.instant = args.instant
|
|
169
|
+
options.wait_time = args.wait
|
|
170
|
+
options.branch_chars = args.branch_chars
|
|
171
|
+
options.leaf_chars = args.leaf_chars
|
|
172
|
+
options.window_width = args.width
|
|
173
|
+
options.window_height = args.height
|
|
174
|
+
options.initial_len = args.start_len
|
|
175
|
+
options.leaf_len = args.leaf_len
|
|
176
|
+
options.num_layers = args.layers
|
|
177
|
+
options.angle_mean = radians(args.angle)
|
|
178
|
+
options.fixed_window = args.fixed_window
|
|
179
|
+
|
|
180
|
+
if args.seed is not None:
|
|
181
|
+
options.set_seed(args.seed)
|
|
182
|
+
|
|
183
|
+
if args.type is not None:
|
|
184
|
+
options.type = args.type
|
|
185
|
+
options.user_set_type = True
|
|
186
|
+
|
|
187
|
+
return options
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_tree(window, options):
|
|
191
|
+
root_x = window.width // 2
|
|
192
|
+
|
|
193
|
+
root_y = tree.Tree.BOX_HEIGHT + 4
|
|
194
|
+
root_y = root_y + root_y % 2 #round to nearest even number (odd numbers cause off-by-one errors as chars are twice as tall as they are wide)
|
|
195
|
+
|
|
196
|
+
root_pos = (root_x, root_y)
|
|
197
|
+
|
|
198
|
+
if options.type == 0:
|
|
199
|
+
t = tree.ClassicTree(window, root_pos, options)
|
|
200
|
+
elif options.type == 1:
|
|
201
|
+
t = tree.FibonacciTree(window, root_pos, options)
|
|
202
|
+
elif options.type == 2:
|
|
203
|
+
t = tree.OffsetFibTree(window, root_pos, options)
|
|
204
|
+
else:
|
|
205
|
+
t = tree.RandomOffsetFibTree(window, root_pos, options)
|
|
206
|
+
|
|
207
|
+
return t
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def main():
|
|
211
|
+
options = parse_cli_args()
|
|
212
|
+
window = draw.TerminalWindow(options.window_width, options.window_height, options)
|
|
213
|
+
|
|
214
|
+
t = get_tree(window, options)
|
|
215
|
+
|
|
216
|
+
t.draw()
|
|
217
|
+
window.draw()
|
|
218
|
+
window.reset_cursor()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if __name__ == "__main__":
|
|
222
|
+
main()
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from . import utils
|
|
3
|
+
import random
|
|
4
|
+
from time import sleep
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
#ANSI escape codes (https://en.wikipedia.org/wiki/ANSI_escape_code)
|
|
8
|
+
END_COLOUR = "\033[00m"
|
|
9
|
+
HIDE_CURSOR = "\033[?25l" #not supported in all terminals
|
|
10
|
+
SHOW_CURSOR = "\033[?25h" #not supported in all terminals
|
|
11
|
+
|
|
12
|
+
CHAR_THRESHOLD = 0.3
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TerminalWindow:
|
|
16
|
+
CHAR_WIDTH = 1
|
|
17
|
+
CHAR_HEIGHT = 2
|
|
18
|
+
|
|
19
|
+
BACKGROUND_CHAR = " "
|
|
20
|
+
|
|
21
|
+
def __init__(self, width, height, options):
|
|
22
|
+
self.width = width
|
|
23
|
+
self.height = height
|
|
24
|
+
|
|
25
|
+
self.options = options
|
|
26
|
+
|
|
27
|
+
self.chars = [[TerminalWindow.BACKGROUND_CHAR for _ in range(width)] for _ in range(height)]
|
|
28
|
+
|
|
29
|
+
colour_char = lambda self, char, r, g, b: f"\033[38;2;{r};{g};{b}m{char}{END_COLOUR}" #ANSI escape code for 24 bit true colour (which most modern terminals support)
|
|
30
|
+
|
|
31
|
+
def extract_colour(self, coloured_char):
|
|
32
|
+
#get the rgb colour from an ANSI coloured character
|
|
33
|
+
splitted = coloured_char.split(";")
|
|
34
|
+
|
|
35
|
+
r = int(splitted[2])
|
|
36
|
+
g = int(splitted[3])
|
|
37
|
+
|
|
38
|
+
b = ""
|
|
39
|
+
for i in splitted[4]:
|
|
40
|
+
if i == "m":
|
|
41
|
+
break
|
|
42
|
+
else:
|
|
43
|
+
b += i
|
|
44
|
+
|
|
45
|
+
b = int(b)
|
|
46
|
+
|
|
47
|
+
return r, g, b
|
|
48
|
+
|
|
49
|
+
def clear_chars(self):
|
|
50
|
+
self.chars = [[TerminalWindow.BACKGROUND_CHAR for _ in range(self.width)] for _ in range(self.height)]
|
|
51
|
+
|
|
52
|
+
def draw(self):
|
|
53
|
+
print(HIDE_CURSOR, end="")
|
|
54
|
+
|
|
55
|
+
for i in self.chars:
|
|
56
|
+
print("".join(i))
|
|
57
|
+
|
|
58
|
+
print(f"\033[{self.height}A", end="") #move cursor to the top after we have finished
|
|
59
|
+
print(SHOW_CURSOR, end="")
|
|
60
|
+
|
|
61
|
+
self.needs_clear = True
|
|
62
|
+
|
|
63
|
+
def reset_cursor(self):
|
|
64
|
+
#cursor will have been left at the top from drawing, so we need to place it back at the bottom
|
|
65
|
+
print(f"\033[{self.height}B", end="")
|
|
66
|
+
|
|
67
|
+
def plane_to_screen(self, x, y):
|
|
68
|
+
#convert cartesian coords to array indices
|
|
69
|
+
scaled_x = x / TerminalWindow.CHAR_WIDTH
|
|
70
|
+
scaled_y = y / TerminalWindow.CHAR_HEIGHT
|
|
71
|
+
|
|
72
|
+
inx1 = round(self.height - scaled_y)
|
|
73
|
+
inx2 = round(scaled_x)
|
|
74
|
+
|
|
75
|
+
return inx1, inx2
|
|
76
|
+
|
|
77
|
+
def screen_to_plane(self, x, y):
|
|
78
|
+
#convert array indices to cartesian coords (inverse of plane_to_screen())
|
|
79
|
+
swapped_x = y
|
|
80
|
+
swapped_y = self.height - x
|
|
81
|
+
|
|
82
|
+
scaled_x = swapped_x * TerminalWindow.CHAR_WIDTH
|
|
83
|
+
scaled_y = swapped_y * TerminalWindow.CHAR_HEIGHT
|
|
84
|
+
|
|
85
|
+
return scaled_x, scaled_y
|
|
86
|
+
|
|
87
|
+
def increase_height(self, delta_height):
|
|
88
|
+
if self.options.fixed_window:
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
self.height += delta_height
|
|
92
|
+
|
|
93
|
+
for _ in range(delta_height):
|
|
94
|
+
self.chars.insert(0, [TerminalWindow.BACKGROUND_CHAR for _ in range(self.width)])
|
|
95
|
+
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
def set_char_instant(self, x, y, char, colour, is_screen_coords):
|
|
99
|
+
if not is_screen_coords:
|
|
100
|
+
x, y = self.plane_to_screen(x, y)
|
|
101
|
+
|
|
102
|
+
#check the point will fit
|
|
103
|
+
if x < 0:
|
|
104
|
+
height_changed = self.increase_height(abs(x))
|
|
105
|
+
|
|
106
|
+
if height_changed:
|
|
107
|
+
x = 0
|
|
108
|
+
|
|
109
|
+
if not 0 <= x < self.height or not 0 <= y < self.width:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
coloured = self.colour_char(char, colour[0], colour[1], colour[2])
|
|
113
|
+
self.chars[x][y] = coloured
|
|
114
|
+
|
|
115
|
+
def set_char_wait(self, x, y, char, colour, is_screen_coords, wait_time):
|
|
116
|
+
#in non instant mode, we want to draw each new character after it is set
|
|
117
|
+
self.set_char_instant(x, y, char, colour, is_screen_coords)
|
|
118
|
+
|
|
119
|
+
self.draw()
|
|
120
|
+
sleep(wait_time)
|
|
121
|
+
|
|
122
|
+
def get_line_char(self, line):
|
|
123
|
+
theta = line.get_theta()
|
|
124
|
+
|
|
125
|
+
upper = math.pi / 2 * (2 / 3)
|
|
126
|
+
lower = math.pi / 2 * (1 / 3)
|
|
127
|
+
|
|
128
|
+
if abs(theta) > upper:
|
|
129
|
+
return "|"
|
|
130
|
+
elif abs(theta) < lower:
|
|
131
|
+
return "_"
|
|
132
|
+
elif theta > 0:
|
|
133
|
+
return "/"
|
|
134
|
+
else:
|
|
135
|
+
return "\\"
|
|
136
|
+
|
|
137
|
+
def choose_colour(self, colour):
|
|
138
|
+
if type(colour[0]) == int:
|
|
139
|
+
#the colour is not a range, so no choice must be made
|
|
140
|
+
return colour
|
|
141
|
+
elif len(colour[0]) == 2:
|
|
142
|
+
#colour should be random with rgb values in the given range
|
|
143
|
+
rand_colour = []
|
|
144
|
+
for lower, upper in colour:
|
|
145
|
+
value = random.randint(lower, upper)
|
|
146
|
+
rand_colour.append(value)
|
|
147
|
+
|
|
148
|
+
return rand_colour
|
|
149
|
+
else:
|
|
150
|
+
raise Exception("Invalid colour argument")
|
|
151
|
+
|
|
152
|
+
def draw_steep_line(self, start, end, colour, width, char, mid_line):
|
|
153
|
+
start_inx, _ = self.plane_to_screen(*start)
|
|
154
|
+
end_inx, _ = self.plane_to_screen(*end)
|
|
155
|
+
|
|
156
|
+
step = 1 if end_inx > start_inx else -1
|
|
157
|
+
|
|
158
|
+
for inx1 in range(start_inx, end_inx + step, step):
|
|
159
|
+
dists = []
|
|
160
|
+
#get the distance away from the mid line for each cell
|
|
161
|
+
for inx2 in range(self.width):
|
|
162
|
+
x, y = self.screen_to_plane(inx1, inx2)
|
|
163
|
+
|
|
164
|
+
desired_x = mid_line.get_x(y)
|
|
165
|
+
dist = abs(desired_x - x)
|
|
166
|
+
|
|
167
|
+
dists.append([dist, inx2])
|
|
168
|
+
|
|
169
|
+
#draw the n closest cells (n = width)
|
|
170
|
+
dists.sort()
|
|
171
|
+
for i in range(width):
|
|
172
|
+
if i >= len(dists):
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
if random.uniform(0, 1) < CHAR_THRESHOLD:
|
|
176
|
+
chosen_char = random.choice(self.options.branch_chars)
|
|
177
|
+
else:
|
|
178
|
+
chosen_char = char
|
|
179
|
+
|
|
180
|
+
chosen_colour = self.choose_colour(colour)
|
|
181
|
+
|
|
182
|
+
if self.options.instant:
|
|
183
|
+
self.set_char_instant(inx1, dists[i][1], chosen_char, chosen_colour, True)
|
|
184
|
+
else:
|
|
185
|
+
self.set_char_wait(inx1, dists[i][1], chosen_char, chosen_colour, True, self.options.wait_time)
|
|
186
|
+
|
|
187
|
+
def draw_shallow_line(self, start, end, colour, width, char, mid_line):
|
|
188
|
+
_, start_inx = self.plane_to_screen(*start)
|
|
189
|
+
_, end_inx = self.plane_to_screen(*end)
|
|
190
|
+
|
|
191
|
+
step = 1 if end_inx > start_inx else -1
|
|
192
|
+
|
|
193
|
+
for inx2 in range(start_inx, end_inx + step, step):
|
|
194
|
+
dists = []
|
|
195
|
+
#get the distance away from the mid line for each cell
|
|
196
|
+
for inx1 in range(self.height):
|
|
197
|
+
x, y = self.screen_to_plane(inx1, inx2)
|
|
198
|
+
|
|
199
|
+
desired_y = mid_line.get_y(x)
|
|
200
|
+
dist = abs(desired_y - y)
|
|
201
|
+
|
|
202
|
+
dists.append([dist, inx1])
|
|
203
|
+
|
|
204
|
+
#draw the n closest cells (n = width)
|
|
205
|
+
dists.sort()
|
|
206
|
+
for i in range(width):
|
|
207
|
+
if i >= len(dists):
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
if random.uniform(0, 1) < CHAR_THRESHOLD:
|
|
211
|
+
chosen_char = random.choice(self.options.branch_chars)
|
|
212
|
+
else:
|
|
213
|
+
chosen_char = char
|
|
214
|
+
|
|
215
|
+
chosen_colour = self.choose_colour(colour)
|
|
216
|
+
|
|
217
|
+
if self.options.instant:
|
|
218
|
+
self.set_char_instant(dists[i][1], inx2, chosen_char, chosen_colour, True)
|
|
219
|
+
else:
|
|
220
|
+
self.set_char_wait(dists[i][1], inx2, chosen_char, chosen_colour, True, self.options.wait_time)
|
|
221
|
+
|
|
222
|
+
def check_line_bounds(self, start, end):
|
|
223
|
+
#if the line will not fit in the current window, update the window size so that it willoptions
|
|
224
|
+
h1, _ = self.plane_to_screen(*start)
|
|
225
|
+
h2, _ = self.plane_to_screen(*end)
|
|
226
|
+
|
|
227
|
+
room_from_top = min(h1, h2)
|
|
228
|
+
|
|
229
|
+
if room_from_top < 0:
|
|
230
|
+
self.increase_height(abs(room_from_top))
|
|
231
|
+
|
|
232
|
+
def draw_line(self, start, end, colour, width):
|
|
233
|
+
mid_line = utils.Line()
|
|
234
|
+
mid_line.set_end_points(start, end)
|
|
235
|
+
|
|
236
|
+
char = self.get_line_char(mid_line)
|
|
237
|
+
|
|
238
|
+
self.check_line_bounds(start, end)
|
|
239
|
+
|
|
240
|
+
if mid_line.is_vertical or abs(mid_line.m) >= 1:
|
|
241
|
+
self.draw_steep_line(start, end, colour, width, char, mid_line)
|
|
242
|
+
else:
|
|
243
|
+
self.draw_shallow_line(start, end, colour, width, char, mid_line)
|
|
File without changes
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from . import utils
|
|
3
|
+
import random
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Tree:
|
|
7
|
+
#base tree class - not a drawable tree
|
|
8
|
+
BOX_HEIGHT = 3
|
|
9
|
+
MAX_TOP_WIDTH = 35
|
|
10
|
+
|
|
11
|
+
MOUND_THRESHOLD = 0.1
|
|
12
|
+
SOIL_CHAR_THRESHOLD = 0.1
|
|
13
|
+
|
|
14
|
+
SOIL_CHARS = ".~*"
|
|
15
|
+
|
|
16
|
+
MOUND_WIDTH_MEAN = 2
|
|
17
|
+
MOUND_WIDTH_STD_DEV = 1
|
|
18
|
+
|
|
19
|
+
SOIL_COLOUR = (0, 150, 0)
|
|
20
|
+
BOX_COLOUR = (200, 200, 200)
|
|
21
|
+
|
|
22
|
+
BRANCH_COLOUR = ((200, 255), (150, 255), (0, 0)) #range of rgb values
|
|
23
|
+
|
|
24
|
+
def __init__(self, window, root_pos, options):
|
|
25
|
+
self.window = window
|
|
26
|
+
self.root_x, self.root_y = root_pos
|
|
27
|
+
self.options = options
|
|
28
|
+
|
|
29
|
+
self.box_top_width = self.get_box_width()
|
|
30
|
+
|
|
31
|
+
def get_box_width(self):
|
|
32
|
+
width = min(self.window.width // 3, Tree.MAX_TOP_WIDTH)
|
|
33
|
+
|
|
34
|
+
if width % 2 == 0:
|
|
35
|
+
width += 1 #width should be odd to allow tree to go in middle
|
|
36
|
+
|
|
37
|
+
return width
|
|
38
|
+
|
|
39
|
+
def draw_box(self):
|
|
40
|
+
root_inx1, root_inx2 = self.window.plane_to_screen(self.root_x, self.root_y)
|
|
41
|
+
|
|
42
|
+
for i in range(Tree.BOX_HEIGHT):
|
|
43
|
+
inx1 = root_inx1 + i
|
|
44
|
+
width = self.box_top_width - i * 2
|
|
45
|
+
|
|
46
|
+
for x in range(width):
|
|
47
|
+
inx2 = root_inx2 - width // 2 + x
|
|
48
|
+
|
|
49
|
+
if x == 0:
|
|
50
|
+
char = "\\"
|
|
51
|
+
colour = Tree.BOX_COLOUR
|
|
52
|
+
elif x == width - 1:
|
|
53
|
+
char = "/"
|
|
54
|
+
colour = Tree.BOX_COLOUR
|
|
55
|
+
elif i == 0:
|
|
56
|
+
char = "_"
|
|
57
|
+
colour = Tree.SOIL_COLOUR
|
|
58
|
+
elif i == Tree.BOX_HEIGHT - 1:
|
|
59
|
+
char = "_"
|
|
60
|
+
colour = Tree.BOX_COLOUR
|
|
61
|
+
else:
|
|
62
|
+
if random.uniform(0, 1) < Tree.SOIL_CHAR_THRESHOLD:
|
|
63
|
+
char = random.choice(Tree.SOIL_CHARS)
|
|
64
|
+
else:
|
|
65
|
+
char = " "
|
|
66
|
+
|
|
67
|
+
colour = Tree.SOIL_COLOUR
|
|
68
|
+
|
|
69
|
+
self.window.set_char_instant(inx1, inx2, char, colour, True)
|
|
70
|
+
|
|
71
|
+
self.draw_box_feet(root_inx1, root_inx2)
|
|
72
|
+
self.draw_all_mounds(root_inx1, root_inx2) #must be called after the top layer has been drawn normally
|
|
73
|
+
|
|
74
|
+
def draw_box_feet(self, root_inx1, root_inx2):
|
|
75
|
+
#draw feet for the box
|
|
76
|
+
inx1 = root_inx1 + Tree.BOX_HEIGHT
|
|
77
|
+
offset = self.box_top_width // 2 - Tree.BOX_HEIGHT - 1
|
|
78
|
+
|
|
79
|
+
for sign in range(-1, 2, 2):
|
|
80
|
+
inx2 = root_inx2 + sign * offset
|
|
81
|
+
self.window.set_char_instant(inx1, inx2, "‾", Tree.BOX_COLOUR, True)
|
|
82
|
+
|
|
83
|
+
def draw_all_mounds(self, root_inx1, root_inx2):
|
|
84
|
+
#draw .---._____.--. on top layer
|
|
85
|
+
num_drawn = 0
|
|
86
|
+
for i in range(1, self.box_top_width):
|
|
87
|
+
inx2 = root_inx2 - self.box_top_width // 2 + i
|
|
88
|
+
|
|
89
|
+
if random.uniform(0, 1) < Tree.MOUND_THRESHOLD / (num_drawn + 1):
|
|
90
|
+
num_drawn += 1
|
|
91
|
+
max_width = self.box_top_width - i - 1
|
|
92
|
+
|
|
93
|
+
self.draw_mound(root_inx1, inx2, max_width)
|
|
94
|
+
|
|
95
|
+
def draw_mound(self, inx1, start_inx2, max_width):
|
|
96
|
+
top_width = round(random.normalvariate(Tree.MOUND_WIDTH_MEAN, Tree.MOUND_WIDTH_STD_DEV))
|
|
97
|
+
top_width = min(top_width, max_width - 2)
|
|
98
|
+
|
|
99
|
+
if top_width <= 0:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
for i in range(top_width + 2):
|
|
103
|
+
inx2 = start_inx2 + i
|
|
104
|
+
|
|
105
|
+
if i == 0 or i == top_width + 1:
|
|
106
|
+
char = "."
|
|
107
|
+
else:
|
|
108
|
+
char = "-"
|
|
109
|
+
|
|
110
|
+
self.window.set_char_instant(inx1, inx2, char, Tree.SOIL_COLOUR, True)
|
|
111
|
+
|
|
112
|
+
def draw_tree_base(self, trunk_width):
|
|
113
|
+
#just add some extra padding at the bottom of the trunk to look nice. Must be called after tree is grown
|
|
114
|
+
inx1, inx2 = self.window.plane_to_screen(self.root_x, self.root_y)
|
|
115
|
+
|
|
116
|
+
left_x = inx2 - trunk_width // 2
|
|
117
|
+
right_x = inx2 + trunk_width // 2
|
|
118
|
+
|
|
119
|
+
#off-by-one error: line drawing moves left to right, causing trunk to be to the left of the center, so move the base right to compensate
|
|
120
|
+
if trunk_width % 2 == 0:
|
|
121
|
+
right_x -= 1
|
|
122
|
+
|
|
123
|
+
self.window.set_char_instant(inx1, left_x - 2, ".", (255, 255, 0), True)
|
|
124
|
+
self.window.set_char_instant(inx1, left_x - 1, "/", (255, 255, 0), True)
|
|
125
|
+
self.window.set_char_instant(inx1, right_x + 1, "\\", (255, 255, 0), True)
|
|
126
|
+
self.window.set_char_instant(inx1, right_x + 2, ".", (255, 255, 0), True)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class RecursiveTree(Tree):
|
|
130
|
+
#all recursive tree types are based off of the fractal canopy: https://en.wikipedia.org/wiki/Fractal_canopy
|
|
131
|
+
ANGLE_STD_DEV = math.radians(8)
|
|
132
|
+
|
|
133
|
+
LEN_SCALE = 0.75
|
|
134
|
+
|
|
135
|
+
MAX_INITIAL_WIDTH = 6
|
|
136
|
+
|
|
137
|
+
def __init__(self, window, root_pos, options):
|
|
138
|
+
super().__init__(window, root_pos, options)
|
|
139
|
+
|
|
140
|
+
def get_end_coords(self, start_x, start_y, length, theta):
|
|
141
|
+
x = start_x + length * math.sin(theta)
|
|
142
|
+
y = start_y + length * math.cos(theta)
|
|
143
|
+
|
|
144
|
+
return x, y
|
|
145
|
+
|
|
146
|
+
def get_initial_params(self):
|
|
147
|
+
initial_width = self.options.initial_len // 5
|
|
148
|
+
initial_angle = random.normalvariate(0, RecursiveTree.ANGLE_STD_DEV)
|
|
149
|
+
|
|
150
|
+
#ensure the width is in a suitable range
|
|
151
|
+
initial_width = max(0, initial_width)
|
|
152
|
+
initial_width = min(RecursiveTree.MAX_INITIAL_WIDTH, initial_width)
|
|
153
|
+
|
|
154
|
+
return initial_width, initial_angle
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class ClassicTree(RecursiveTree):
|
|
158
|
+
#trees with a random number of branches on each layer
|
|
159
|
+
MEAN_BRANCHES = 2
|
|
160
|
+
BRANCHES_STD_DEV = 0.5
|
|
161
|
+
|
|
162
|
+
def __init__(self, window, root_pos, options):
|
|
163
|
+
super().__init__(window, root_pos, options)
|
|
164
|
+
|
|
165
|
+
def draw_branch(self, x, y, layer, length, width, theta):
|
|
166
|
+
if layer >= self.options.num_layers:
|
|
167
|
+
leaves = Leaves(self.window, (x, y), self.options)
|
|
168
|
+
leaves.draw()
|
|
169
|
+
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
end_x, end_y = self.get_end_coords(x, y, length, theta)
|
|
173
|
+
|
|
174
|
+
self.window.draw_line((x, y), (end_x, end_y), ClassicTree.BRANCH_COLOUR, round(width))
|
|
175
|
+
|
|
176
|
+
self.draw_end_branches(x, y, layer, length, width, theta)
|
|
177
|
+
|
|
178
|
+
def draw_end_branches(self, start_x, start_y, layer, length, width, theta):
|
|
179
|
+
sign = 1
|
|
180
|
+
num_branches = max(0, round(random.normalvariate(ClassicTree.MEAN_BRANCHES, ClassicTree.BRANCHES_STD_DEV)))
|
|
181
|
+
|
|
182
|
+
step = length / num_branches if num_branches != 0 else 0
|
|
183
|
+
|
|
184
|
+
new_width = max(1, width - 1)
|
|
185
|
+
new_length = length * ClassicTree.LEN_SCALE
|
|
186
|
+
|
|
187
|
+
for i in range(num_branches):
|
|
188
|
+
dist_up_branch = (i + 1) * step #branches are linearly distributed along the parent
|
|
189
|
+
new_theta = theta + sign * random.normalvariate(self.options.angle_mean, ClassicTree.ANGLE_STD_DEV)
|
|
190
|
+
|
|
191
|
+
x, y = self.get_end_coords(start_x, start_y, dist_up_branch, theta) #start point of new branch
|
|
192
|
+
|
|
193
|
+
self.draw_branch(x, y, layer + 1, new_length, new_width, new_theta)
|
|
194
|
+
|
|
195
|
+
sign *= -1 #ensure next branch is on opposite side of the parent
|
|
196
|
+
|
|
197
|
+
def draw(self):
|
|
198
|
+
initial_width, initial_angle = self.get_initial_params()
|
|
199
|
+
|
|
200
|
+
self.draw_box()
|
|
201
|
+
self.draw_tree_base(initial_width)
|
|
202
|
+
|
|
203
|
+
self.draw_branch(self.root_x, self.root_y, 1, self.options.initial_len, initial_width, initial_angle)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class FibonacciTree(RecursiveTree):
|
|
207
|
+
#trees with a fibonacci number of branches on each layer
|
|
208
|
+
def __init__(self, window, root_pos, options):
|
|
209
|
+
super().__init__(window, root_pos, options)
|
|
210
|
+
|
|
211
|
+
self.fib = self.fib_nums()
|
|
212
|
+
self.branch_nums = self.generate_branch_nums()
|
|
213
|
+
|
|
214
|
+
def fib_nums(self):
|
|
215
|
+
fib = [1, 1]
|
|
216
|
+
|
|
217
|
+
for _ in range(self.options.num_layers):
|
|
218
|
+
fib.append(fib[-1] + fib[-2])
|
|
219
|
+
|
|
220
|
+
return fib
|
|
221
|
+
|
|
222
|
+
def generate_branch_nums(self):
|
|
223
|
+
#generate the number of child branches branching off of each parent
|
|
224
|
+
branch_nums = [[1]] #1st index is the layer from the root, 2nd index is the position of the parent branch in its layer
|
|
225
|
+
for i in range(self.options.num_layers):
|
|
226
|
+
num_branches = self.fib[i + 2]
|
|
227
|
+
num_parents = sum(branch_nums[-1])
|
|
228
|
+
|
|
229
|
+
base = num_branches // num_parents
|
|
230
|
+
diff = num_branches - base * num_parents
|
|
231
|
+
|
|
232
|
+
current_nums = [base + 1 if x < diff else base for x in range(num_parents)]
|
|
233
|
+
|
|
234
|
+
random.shuffle(current_nums)
|
|
235
|
+
|
|
236
|
+
branch_nums.append(current_nums)
|
|
237
|
+
|
|
238
|
+
return branch_nums
|
|
239
|
+
|
|
240
|
+
def draw_branch(self, x, y, layer_inx, branch_inx, length, width, theta):
|
|
241
|
+
if layer_inx > self.options.num_layers:
|
|
242
|
+
leaf = Leaves(self.window, (x, y), self.options)
|
|
243
|
+
leaf.draw()
|
|
244
|
+
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
end_x, end_y = self.get_end_coords(x, y, length, theta)
|
|
248
|
+
|
|
249
|
+
self.window.draw_line((x, y), (end_x, end_y), FibonacciTree.BRANCH_COLOUR, round(width))
|
|
250
|
+
|
|
251
|
+
self.draw_end_branches(x, y, layer_inx, branch_inx, length, width, theta)
|
|
252
|
+
|
|
253
|
+
def draw_end_branches(self, start_x, start_y, layer_inx, branch_inx, length, width, theta):
|
|
254
|
+
#draw the child branches off of the end of the parent branch
|
|
255
|
+
sign = 1
|
|
256
|
+
num_branches = self.branch_nums[layer_inx][branch_inx]
|
|
257
|
+
new_width = max(1, width - 1)
|
|
258
|
+
|
|
259
|
+
x, y = self.get_end_coords(start_x, start_y, length, theta)
|
|
260
|
+
|
|
261
|
+
for i in range(num_branches):
|
|
262
|
+
angle = random.normalvariate(self.options.angle_mean, FibonacciTree.ANGLE_STD_DEV)
|
|
263
|
+
new_theta = theta + sign * angle
|
|
264
|
+
|
|
265
|
+
new_len = length * FibonacciTree.LEN_SCALE
|
|
266
|
+
|
|
267
|
+
self.draw_branch(x, y, layer_inx + 1, branch_inx + i, new_len, new_width, new_theta)
|
|
268
|
+
|
|
269
|
+
sign *= -1
|
|
270
|
+
|
|
271
|
+
def draw(self):
|
|
272
|
+
initial_width, initial_angle = self.get_initial_params()
|
|
273
|
+
|
|
274
|
+
self.draw_box()
|
|
275
|
+
self.draw_tree_base(initial_width)
|
|
276
|
+
|
|
277
|
+
self.draw_branch(self.root_x, self.root_y, 1, 0, self.options.initial_len, initial_width, initial_angle)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class OffsetFibTree(FibonacciTree):
|
|
281
|
+
#similar to fibonacci tree, but branches grow from the middle of the parent branch
|
|
282
|
+
def __init__(self, window, root_pos, options):
|
|
283
|
+
super().__init__(window, root_pos, options)
|
|
284
|
+
|
|
285
|
+
def draw_end_branches(self, start_x, start_y, layer_inx, branch_inx, length, width, theta):
|
|
286
|
+
sign = 1
|
|
287
|
+
num_branches = self.branch_nums[layer_inx][branch_inx]
|
|
288
|
+
|
|
289
|
+
step = length / num_branches if num_branches != 0 else 0
|
|
290
|
+
|
|
291
|
+
new_width = max(1, width - 1)
|
|
292
|
+
new_length = length * ClassicTree.LEN_SCALE
|
|
293
|
+
|
|
294
|
+
for i in range(num_branches):
|
|
295
|
+
dist_up_branch = (i + 1) * step #branches are now linearly distributed along the parent branch
|
|
296
|
+
new_theta = theta + sign * random.normalvariate(self.options.angle_mean, OffsetFibTree.ANGLE_STD_DEV)
|
|
297
|
+
|
|
298
|
+
x, y = self.get_end_coords(start_x, start_y, dist_up_branch, theta)
|
|
299
|
+
|
|
300
|
+
self.draw_branch(x, y, layer_inx + 1, branch_inx + i, new_length, new_width, new_theta)
|
|
301
|
+
|
|
302
|
+
sign *= -1
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class RandomOffsetFibTree(FibonacciTree):
|
|
306
|
+
#an offset fib tree, but the branches are randomly placed along parent branch and leaves can grow on non end branches
|
|
307
|
+
GROW_END_THRESHOLD = 0.5
|
|
308
|
+
|
|
309
|
+
NON_END_MIN = 0.3
|
|
310
|
+
NON_END_MAX = 0.9
|
|
311
|
+
|
|
312
|
+
def __init__(self, window, root_pos, options):
|
|
313
|
+
super().__init__(window, root_pos, options)
|
|
314
|
+
|
|
315
|
+
def draw_end_branches(self, start_x, start_y, layer_inx, branch_inx, length, width, theta):
|
|
316
|
+
sign = 1
|
|
317
|
+
num_branches = self.branch_nums[layer_inx][branch_inx]
|
|
318
|
+
|
|
319
|
+
new_width = max(1, width - 1)
|
|
320
|
+
new_length = length * ClassicTree.LEN_SCALE
|
|
321
|
+
|
|
322
|
+
need_leaves = True
|
|
323
|
+
for i in range(num_branches):
|
|
324
|
+
grow_at_end = random.uniform(0, 1) < RandomOffsetFibTree.GROW_END_THRESHOLD
|
|
325
|
+
|
|
326
|
+
if grow_at_end:
|
|
327
|
+
#this branch will grow at the end of the parent, so do not draw leaves at the end of the parent
|
|
328
|
+
need_leaves = False
|
|
329
|
+
dist_up_branch = length
|
|
330
|
+
else:
|
|
331
|
+
#this branch will grow at a random distance along the parent (not the end), so we may need leaves at the end of the parent
|
|
332
|
+
dist_up_branch = random.uniform(length * RandomOffsetFibTree.NON_END_MIN, length * RandomOffsetFibTree.NON_END_MAX)
|
|
333
|
+
|
|
334
|
+
new_theta = theta + sign * random.normalvariate(self.options.angle_mean, OffsetFibTree.ANGLE_STD_DEV)
|
|
335
|
+
|
|
336
|
+
x, y = self.get_end_coords(start_x, start_y, dist_up_branch, theta)
|
|
337
|
+
|
|
338
|
+
self.draw_branch(x, y, layer_inx + 1, branch_inx + i, new_length, new_width, new_theta)
|
|
339
|
+
|
|
340
|
+
sign *= -1
|
|
341
|
+
|
|
342
|
+
if need_leaves:
|
|
343
|
+
#no branches have grown at the end of the parent, so we can grow leaves
|
|
344
|
+
end_pos = self.get_end_coords(start_x, start_y, length, theta)
|
|
345
|
+
|
|
346
|
+
leaves = Leaves(self.window, end_pos, self.options)
|
|
347
|
+
leaves.draw()
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class FractalTree(RecursiveTree):
|
|
351
|
+
"""A simple recursive tree that splits into two branches at the end of each branch."""
|
|
352
|
+
NUM_BRANCHES = 2
|
|
353
|
+
|
|
354
|
+
def __init__(self, window, root_pos, options):
|
|
355
|
+
super().__init__(window, root_pos, options)
|
|
356
|
+
|
|
357
|
+
def draw_branch(self, x, y, layer, length, width, theta):
|
|
358
|
+
if layer >= self.options.num_layers:
|
|
359
|
+
leaves = Leaves(self.window, (x, y), self.options)
|
|
360
|
+
leaves.draw()
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
end_x, end_y = self.get_end_coords(x, y, length, theta)
|
|
364
|
+
self.window.draw_line((x, y), (end_x, end_y), Tree.BRANCH_COLOUR, round(width))
|
|
365
|
+
self.draw_end_branches(x, y, layer, length, width, theta)
|
|
366
|
+
|
|
367
|
+
def draw_end_branches(self, start_x, start_y, layer, length, width, theta):
|
|
368
|
+
sign = 1
|
|
369
|
+
new_width = max(1, width - 1)
|
|
370
|
+
new_length = length * self.LEN_SCALE
|
|
371
|
+
|
|
372
|
+
x, y = self.get_end_coords(start_x, start_y, length, theta)
|
|
373
|
+
|
|
374
|
+
for _ in range(self.NUM_BRANCHES):
|
|
375
|
+
angle = random.normalvariate(self.options.angle_mean, self.ANGLE_STD_DEV)
|
|
376
|
+
new_theta = theta + sign * angle
|
|
377
|
+
self.draw_branch(x, y, layer + 1, new_length, new_width, new_theta)
|
|
378
|
+
sign *= -1
|
|
379
|
+
|
|
380
|
+
def draw(self):
|
|
381
|
+
initial_width, initial_angle = self.get_initial_params()
|
|
382
|
+
self.draw_box()
|
|
383
|
+
self.draw_tree_base(initial_width)
|
|
384
|
+
self.draw_branch(self.root_x, self.root_y, 1, self.options.initial_len, initial_width, initial_angle)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class Leaves:
|
|
388
|
+
NUM_LEAVES = 4
|
|
389
|
+
|
|
390
|
+
def __init__(self, window, branch_end, options):
|
|
391
|
+
self.window = window
|
|
392
|
+
self.branch_x, self.branch_y = branch_end
|
|
393
|
+
self.options = options
|
|
394
|
+
|
|
395
|
+
def draw(self):
|
|
396
|
+
g = utils.Vector(0, -1)
|
|
397
|
+
|
|
398
|
+
for _ in range(Leaves.NUM_LEAVES):
|
|
399
|
+
vel = utils.Vector(random.uniform(-1, 1), random.uniform(-1, 1)) #random starting velocity for the leaves to step along
|
|
400
|
+
vel.normalise()
|
|
401
|
+
pos = utils.Vector(self.branch_x, self.branch_y)
|
|
402
|
+
|
|
403
|
+
for i in range(self.options.leaf_len):
|
|
404
|
+
pos += vel
|
|
405
|
+
|
|
406
|
+
colour = (0, random.randint(75, 255), 0)
|
|
407
|
+
char = random.choice(self.options.leaf_chars)
|
|
408
|
+
|
|
409
|
+
if self.options.instant:
|
|
410
|
+
self.window.set_char_instant(pos.x, pos.y, char, colour, False)
|
|
411
|
+
else:
|
|
412
|
+
self.window.set_char_wait(pos.x, pos.y, char, colour, False, self.options.wait_time)
|
|
413
|
+
|
|
414
|
+
#make the leaves droop downwards by adding some gravity force
|
|
415
|
+
weight = i / self.options.leaf_len
|
|
416
|
+
vel += g * weight
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Line:
|
|
5
|
+
#line in form y = mx + c
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.start = None
|
|
8
|
+
self.end = None
|
|
9
|
+
|
|
10
|
+
self.m = None
|
|
11
|
+
self.c = None
|
|
12
|
+
|
|
13
|
+
self.is_vertical = False
|
|
14
|
+
|
|
15
|
+
def get_y(self, x):
|
|
16
|
+
if not self.is_vertical:
|
|
17
|
+
return self.m * x + self.c
|
|
18
|
+
|
|
19
|
+
def get_x(self, y):
|
|
20
|
+
if self.is_vertical:
|
|
21
|
+
return self.c
|
|
22
|
+
elif self.m != 0:
|
|
23
|
+
return (y - self.c) / self.m
|
|
24
|
+
|
|
25
|
+
def set_end_points(self, start, end):
|
|
26
|
+
self.start = start
|
|
27
|
+
self.end = end
|
|
28
|
+
|
|
29
|
+
if self.start[0] == self.end[0]:
|
|
30
|
+
self.is_vertical = True
|
|
31
|
+
self.m = None
|
|
32
|
+
self.c = self.start[0]
|
|
33
|
+
else:
|
|
34
|
+
self.m = (self.start[1] - self.end[1]) / (self.start[0] - self.end[0])
|
|
35
|
+
self.c = self.start[1] - self.m * self.start[0]
|
|
36
|
+
|
|
37
|
+
def set_gradient(self, m, point):
|
|
38
|
+
self.m = m
|
|
39
|
+
self.c = point[1] - m * point[0]
|
|
40
|
+
|
|
41
|
+
def get_theta(self):
|
|
42
|
+
#get angle above x axis
|
|
43
|
+
if self.is_vertical:
|
|
44
|
+
return math.pi / 2
|
|
45
|
+
else:
|
|
46
|
+
return math.atan(self.m)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Vector:
|
|
50
|
+
def __init__(self, x, y):
|
|
51
|
+
self.x = x
|
|
52
|
+
self.y = y
|
|
53
|
+
|
|
54
|
+
mag = lambda self: (self.x**2 + self.y**2)**0.5
|
|
55
|
+
|
|
56
|
+
def __iadd__(self, other_vec):
|
|
57
|
+
return Vector(self.x + other_vec.x, self.y + other_vec.y)
|
|
58
|
+
|
|
59
|
+
def __mul__(self, scalar):
|
|
60
|
+
return Vector(self.x * scalar, self.y * scalar)
|
|
61
|
+
|
|
62
|
+
def normalise(self):
|
|
63
|
+
m = self.mag()
|
|
64
|
+
|
|
65
|
+
self.x /= m
|
|
66
|
+
self.y /= m
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pybonsai"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A project that generates ASCII art trees in your terminal."
|
|
5
|
+
license = {text = "MIT"}
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
keywords = ["pybonsai"]
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Ben Edwards"},
|
|
11
|
+
{name = "Ivan Karlov", email = "karlovivan679@gmail.com"}
|
|
12
|
+
]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Programming Language :: Python :: 3.9",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
pybonsai = "pybonsai.__main__:main"
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
"Source" = "https://github.com/DaEtoJostka/PyBonsai"
|
|
26
|
+
"Original" = "https://github.com/Ben-Edwards44/PyBonsai"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["hatchling"]
|
|
30
|
+
build-backend = "hatchling.build"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["pybonsai"]
|