├── doc ├── random1.png ├── random2.png ├── random3.png ├── spirograph1.png ├── spirograph2.png ├── spirograph3.png ├── tileygener.jpg ├── diamond_square1.png ├── diamond_square2.png ├── diamond_square3.png ├── generate_pixels.jpg ├── cellular_automata1.png ├── cellular_automata2.png ├── cellular_automata3.png ├── fixed_attractors1.png ├── fixed_attractors2.png └── fixed_attractors3.png ├── fonts ├── Roboto-Bold.ttf ├── Roboto-Light.ttf ├── Roboto-Regular.ttf ├── Roboto-BoldItalic.ttf ├── Inconsolata-Regular.ttf └── Roboto-RegularItalic.ttf ├── default_env.tres ├── .gitignore ├── project.godot ├── Global.gd ├── LICENSE ├── Sprites.gd ├── gener_template.gd ├── geners ├── spirograph.gd ├── random_pixels.gd ├── diamond_square.gd ├── cellular_automata.gd └── fixed_set_attractors.gd ├── Palettes.gd ├── Tiley.gd ├── README.md ├── GenerInspector.gd └── Tiley.tscn /doc/random1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/random1.png -------------------------------------------------------------------------------- /doc/random2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/random2.png -------------------------------------------------------------------------------- /doc/random3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/random3.png -------------------------------------------------------------------------------- /doc/spirograph1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/spirograph1.png -------------------------------------------------------------------------------- /doc/spirograph2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/spirograph2.png -------------------------------------------------------------------------------- /doc/spirograph3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/spirograph3.png -------------------------------------------------------------------------------- /doc/tileygener.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/tileygener.jpg -------------------------------------------------------------------------------- /fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /fonts/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/fonts/Roboto-Light.ttf -------------------------------------------------------------------------------- /doc/diamond_square1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/diamond_square1.png -------------------------------------------------------------------------------- /doc/diamond_square2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/diamond_square2.png -------------------------------------------------------------------------------- /doc/diamond_square3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/diamond_square3.png -------------------------------------------------------------------------------- /doc/generate_pixels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/generate_pixels.jpg -------------------------------------------------------------------------------- /fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /doc/cellular_automata1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/cellular_automata1.png -------------------------------------------------------------------------------- /doc/cellular_automata2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/cellular_automata2.png -------------------------------------------------------------------------------- /doc/cellular_automata3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/cellular_automata3.png -------------------------------------------------------------------------------- /doc/fixed_attractors1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/fixed_attractors1.png -------------------------------------------------------------------------------- /doc/fixed_attractors2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/fixed_attractors2.png -------------------------------------------------------------------------------- /doc/fixed_attractors3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/doc/fixed_attractors3.png -------------------------------------------------------------------------------- /fonts/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/fonts/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /fonts/Inconsolata-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/fonts/Inconsolata-Regular.ttf -------------------------------------------------------------------------------- /fonts/Roboto-RegularItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolfdenpublishing/tileygener/HEAD/fonts/Roboto-RegularItalic.ttf -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | 5 | [resource] 6 | background_mode = 2 7 | background_sky = SubResource( 1 ) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot-specific ignores 2 | .import/ 3 | *.import 4 | export.cfg 5 | export_presets.cfg 6 | 7 | # Imported translations (automatically generated from CSV files) 8 | *.translation 9 | 10 | # Mono-specific ignores 11 | .mono/ 12 | data_*/ 13 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=4 10 | 11 | _global_script_classes=[ ] 12 | _global_script_class_icons={ 13 | 14 | } 15 | 16 | [application] 17 | 18 | config/name="TileyGener" 19 | run/main_scene="res://Tiley.tscn" 20 | run/low_processor_mode=true 21 | 22 | [autoload] 23 | 24 | Global="*res://Global.gd" 25 | 26 | [debug] 27 | 28 | gdscript/warnings/unused_class_variable=true 29 | gdscript/warnings/integer_division=false 30 | 31 | [display] 32 | 33 | window/size/width=1920 34 | window/size/height=1080 35 | window/stretch/mode="2d" 36 | window/stretch/aspect="keep" 37 | 38 | [rendering] 39 | 40 | environment/default_environment="res://default_env.tres" 41 | -------------------------------------------------------------------------------- /Global.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # Convenience variables for accessing UI elements. We simply search for the 4 | # needed nodes at runtime and store their references, allowing the UI tree 5 | # to be freely manipulated, controls can be moved around whever, but no code 6 | # changes are ever required (assuming all the ui elements we need to access 7 | # have unique names and we don't change them). 8 | 9 | var tiley # root node; will be set by Tiley.gd _init() 10 | var controls = {} # nodes of ui controls that we need to access 11 | 12 | 13 | func _init(): 14 | OS.window_maximized = true 15 | randomize() 16 | 17 | 18 | func _ready(): 19 | # the list of controls we want direct access to 20 | var global_controls = [ 21 | "GenerOption", 22 | "GenerInspector", 23 | "GenerDescription", 24 | "GridSize", 25 | "TileySize", 26 | "TileyScale", 27 | "Colors", 28 | "Background", 29 | "PaletteOption", 30 | "SaveDialog", 31 | ] 32 | # go find 'em all! 33 | for control in global_controls: 34 | controls[control] = tiley.find_node("*" + control) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Wolfden Publishing LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Sprites.gd: -------------------------------------------------------------------------------- 1 | extends PanelContainer 2 | 3 | onready var save_dialog:FileDialog = Global.controls["SaveDialog"] 4 | var ignore_clicks:bool = false 5 | 6 | 7 | func _input(event): 8 | if ignore_clicks: 9 | return 10 | if event is InputEventMouseButton and event.pressed and event.button_index == BUTTON_LEFT: 11 | var sprite:Sprite = clicked_sprite(event) 12 | if sprite != null: 13 | ignore_clicks = true 14 | save_image(sprite) 15 | 16 | 17 | func clicked_sprite(event:InputEventMouseButton) -> Sprite: 18 | for sprite in get_children(): 19 | var rect:Rect2 = sprite.get_rect() 20 | rect.position.x *= sprite.scale.x 21 | rect.position.y *= sprite.scale.y 22 | rect.size.x *= sprite.scale.x 23 | rect.size.y *= sprite.scale.y 24 | rect.position = sprite.position + rect.position 25 | if rect.has_point(event.position): 26 | return sprite 27 | return null 28 | 29 | 30 | func save_image(sprite:Sprite) -> void: 31 | var dt = OS.get_datetime() 32 | save_dialog.set_meta("sprite", sprite) 33 | save_dialog.get_line_edit().text = "%04d%02d%02d%02d%02d%02d.png" % [dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second] 34 | save_dialog.popup_centered() 35 | 36 | 37 | func _on_SaveDialog_file_selected(path): 38 | save_dialog.get_meta("sprite").texture.get_data().save_png(path) 39 | 40 | 41 | func _on_SaveDialog_popup_hide(): 42 | ignore_clicks = false 43 | -------------------------------------------------------------------------------- /gener_template.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # --- DON'T CHANGE THESE - EDIT THEM IN THE INSPECTOR -------------------------- 4 | # gener_name identifies this gener to the app, used in the option button 5 | # warning-ignore:unused_class_variable 6 | export(String) var gener_name 7 | # gener_description is the help info for this gener, supports bbcode 8 | # warning-ignore:unused_class_variable 9 | export(String, MULTILINE) var gener_description 10 | # ------------------------------------------------------------------------------ 11 | 12 | # optional grid defaults 13 | # warning-ignore:unused_class_variable 14 | var gener_grid_defaults = { 15 | "grid_size": 4, 16 | "tiley_size": 128, 17 | "tiley_scale": 2, 18 | "background_color": Color.black, 19 | } 20 | 21 | # list of properties to add to the inspector 22 | # supported types: 23 | # ["bool"] 24 | # ["int", min, max] 25 | # ["float", min, max, step] 26 | # ["string"] 27 | # ["option", [Option_1, Option_2, ...]] 28 | # ["array", array_name, array_max, "bool", default_value] 29 | # ["array", array_name, array_max, "int", default_value, min, max] 30 | # ["array", array_name, array_max, "float", default_value, min, max, step] 31 | # ["array", array_name, array_max, "string", default_value] 32 | # ["array", array_name, array_max, "option", default_value, [Option_1, Option_2, ...]] 33 | # warning-ignore:unused_class_variable 34 | var gener_properties = { 35 | "symmetry": ["option", ["None", "Vertical", "Horizontal", "Both"]], 36 | } 37 | # each property above must have a script variable of the same name 38 | # and should be initialized to a default value 39 | var symmetry:String = "None" 40 | 41 | 42 | func generate_pixels(image:Image, colors:Array) -> void: 43 | image.lock() 44 | # --- GENERATE PIXELS HERE with image.setpixel() or image.setpixelv() 45 | image.unlock() 46 | 47 | 48 | # Main function to generate a single tile. Template version handles basic 49 | # symmetry and calls generate_pixels() to draw actual pixels into a tile 50 | func tiley_gener(size:Vector2, colors:Array = []) -> Image: 51 | var image := Image.new() 52 | image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 53 | match symmetry: 54 | "None": 55 | generate_pixels(image, colors) 56 | "Vertical": 57 | var left_image := Image.new() 58 | left_image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 59 | generate_pixels(left_image, colors) 60 | left_image.crop(round(size.x / 2) as int, round(size.y) as int) 61 | image.blit_rect(left_image, Rect2(Vector2.ZERO, left_image.get_size()), Vector2.ZERO) 62 | left_image.flip_x() 63 | image.blit_rect(left_image, Rect2(Vector2.ZERO, left_image.get_size()), Vector2(left_image.get_width(), 0)) 64 | "Horizontal": 65 | var top_image := Image.new() 66 | top_image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 67 | generate_pixels(top_image, colors) 68 | top_image.crop(round(size.x) as int, round(size.y / 2) as int) 69 | image.blit_rect(top_image, Rect2(Vector2.ZERO, top_image.get_size()), Vector2.ZERO) 70 | top_image.flip_y() 71 | image.blit_rect(top_image, Rect2(Vector2.ZERO, top_image.get_size()), Vector2(0, top_image.get_height())) 72 | "Both": 73 | var quadrant_image := Image.new() 74 | quadrant_image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 75 | generate_pixels(quadrant_image, colors) 76 | quadrant_image.crop(round(size.x / 2) as int, round(size.y / 2) as int) 77 | image.blit_rect(quadrant_image, Rect2(Vector2.ZERO, quadrant_image.get_size()), Vector2.ZERO) 78 | quadrant_image.flip_x() 79 | image.blit_rect(quadrant_image, Rect2(Vector2.ZERO, quadrant_image.get_size()), Vector2(quadrant_image.get_width(), 0)) 80 | quadrant_image.flip_y() 81 | image.blit_rect(quadrant_image, Rect2(Vector2.ZERO, quadrant_image.get_size()), Vector2(quadrant_image.get_width(), quadrant_image.get_height())) 82 | quadrant_image.flip_x() 83 | image.blit_rect(quadrant_image, Rect2(Vector2.ZERO, quadrant_image.get_size()), Vector2(0, quadrant_image.get_height())) 84 | return image 85 | -------------------------------------------------------------------------------- /geners/spirograph.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # warning-ignore:unused_class_variable 4 | export(String) var gener_name 5 | # warning-ignore:unused_class_variable 6 | export(String, MULTILINE) var gener_description 7 | 8 | # warning-ignore:unused_class_variable 9 | var gener_grid_defaults = { 10 | "grid_size": 4, 11 | "tiley_size": 256, 12 | "tiley_scale": 1, 13 | "background_color": Color.black, 14 | } 15 | 16 | # warning-ignore:unused_class_variable 17 | var gener_properties = { 18 | "style": ["option", ["Hypocycloid", "Epicycloid", "Random"]], 19 | "big_r_min": ["float", 0.1, 1.0, 0.01], 20 | "big_r_max": ["float", 0.1, 1.0, 0.01], 21 | "small_r_min": ["float", 0.1, 1.0, 0.01], 22 | "small_r_max": ["float", 0.1, 1.0, 0.01], 23 | "a_min": ["float", 0.1, 1.0, 0.01], 24 | "a_max": ["float", 0.1, 1.0, 0.01], 25 | "t_min": ["int", 1, 100000], 26 | "t_max": ["int", 1, 100000], 27 | "step_min": ["float", 0.01, 1.0, 0.01], 28 | "step_max": ["float", 0.01, 1.0, 0.01], 29 | "min_spiros": ["int", 1, 10], 30 | "max_spiros": ["int", 1, 10], 31 | } 32 | var style:String = "Random" 33 | var big_r_min:float = 0.5 setget set_big_r_min 34 | var big_r_max:float = 1.0 setget set_big_r_max 35 | var small_r_min:float = 0.1 setget set_small_r_min 36 | var small_r_max:float = 0.4 setget set_small_r_max 37 | var a_min:float = 0.1 setget set_a_min 38 | var a_max:float = 0.9 setget set_a_max 39 | var t_min:int = 100 setget set_t_min 40 | var t_max:int = 1000 setget set_t_max 41 | var step_min:float = 0.01 setget set_step_min 42 | var step_max:float = 0.2 setget set_step_max 43 | var min_spiros:int = 1 setget set_min_spiros 44 | var max_spiros:int = 4 setget set_max_spiros 45 | 46 | 47 | func set_big_r_min(value): 48 | big_r_min = value 49 | if big_r_min > big_r_max: 50 | big_r_max = big_r_min 51 | Global.controls.GenerInspector.update_inspector() 52 | 53 | 54 | func set_big_r_max(value): 55 | big_r_max = value 56 | if big_r_max < big_r_min: 57 | big_r_min = big_r_max 58 | Global.controls.GenerInspector.update_inspector() 59 | 60 | 61 | func set_small_r_min(value): 62 | small_r_min = value 63 | if small_r_min > small_r_max: 64 | small_r_max = small_r_min 65 | Global.controls.GenerInspector.update_inspector() 66 | 67 | 68 | func set_small_r_max(value): 69 | small_r_max = value 70 | if small_r_max < small_r_min: 71 | small_r_min = small_r_max 72 | Global.controls.GenerInspector.update_inspector() 73 | 74 | 75 | func set_a_min(value): 76 | a_min = value 77 | if a_min > a_max: 78 | a_max = a_min 79 | Global.controls.GenerInspector.update_inspector() 80 | 81 | 82 | func set_a_max(value): 83 | a_max = value 84 | if a_max < a_min: 85 | a_min = a_max 86 | Global.controls.GenerInspector.update_inspector() 87 | 88 | 89 | func set_t_min(value): 90 | t_min = value 91 | if t_min > t_max: 92 | t_max = t_min 93 | Global.controls.GenerInspector.update_inspector() 94 | 95 | 96 | func set_t_max(value): 97 | t_max = value 98 | if t_max < t_min: 99 | t_min = t_max 100 | Global.controls.GenerInspector.update_inspector() 101 | 102 | 103 | func set_step_min(value): 104 | step_min = value 105 | if step_min > step_max: 106 | step_max = step_min 107 | Global.controls.GenerInspector.update_inspector() 108 | 109 | 110 | func set_step_max(value): 111 | step_max = value 112 | if step_max < step_min: 113 | step_min = step_max 114 | Global.controls.GenerInspector.update_inspector() 115 | 116 | 117 | func set_min_spiros(value): 118 | min_spiros = value 119 | if min_spiros > max_spiros: 120 | max_spiros = min_spiros 121 | Global.controls.GenerInspector.update_inspector() 122 | 123 | 124 | func set_max_spiros(value): 125 | max_spiros = value 126 | if max_spiros < min_spiros: 127 | min_spiros = max_spiros 128 | Global.controls.GenerInspector.update_inspector() 129 | 130 | 131 | func tiley_gener(size:Vector2, colors:Array = []) -> Image: 132 | var image := Image.new() 133 | image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 134 | for i in randi() % (max_spiros - min_spiros + 1) + min_spiros: 135 | spirograph(image, colors) 136 | return image 137 | 138 | 139 | func spirograph(image:Image, colors:Array): 140 | var image_rect:Rect2 = Rect2(Vector2.ZERO, image.get_size()) 141 | var s:float = image.get_width() / 2.0 142 | var R:float = s * rand_range(big_r_min, big_r_max) 143 | var r:float = s * rand_range(small_r_min, small_r_max) 144 | var a:float = s * rand_range(a_min, a_max) 145 | var k1:float = R - r 146 | var k2:float = r / R 147 | var k3:float = R + r 148 | var mode:int = 0 if style == "Hypocycloid" or (style == "Random" and randf() > 0.5) else 1 149 | var x:int 150 | var y:int 151 | var c := Color(randf(), randf(), randf(), rand_range(0.75, 1.0)) 152 | if colors.size() > 0: 153 | c = colors[randi() % colors.size()] 154 | var step:float = rand_range(step_min, step_max) 155 | var t:float = rand_range(t_max, t_max) 156 | image.lock() 157 | while t >= 0: 158 | match mode: 159 | 0: # hypocycloid 160 | x = round(s + k1 * cos(k2 * t) + a * cos((1 - k2) * t)) as int 161 | y = round(s + k1 * sin(k2 * t) - a * sin((1 - k2) * t)) as int 162 | 1: # epicycloid 163 | x = round(s + k3 * cos(k2 * t) - a * cos((1 + k2) * t)) as int 164 | y = round(s + k3 * sin(k2 * t) - a * sin((1 + k2) * t)) as int 165 | if image_rect.has_point(Vector2(x, y)): 166 | image.set_pixel(x, y, c) 167 | t -= step 168 | image.unlock() 169 | -------------------------------------------------------------------------------- /Palettes.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # The pallete control generator will look for exported arrays of Color objects. 4 | 5 | # Default pallete from http://alumni.media.mit.edu/~wad/color/palette.html 6 | # warning-ignore:unused_class_variable 7 | export(Array, Color) var Default:Array = [ 8 | Color.black, Color.darkgray, Color.blue, Color.red, 9 | Color.green, Color.brown, Color.purple, Color.lightgray, 10 | Color.lightgreen, Color.lightblue, Color.cyan, Color.orange, 11 | Color.yellow, Color.tan, Color.pink, Color.white 12 | ] 13 | 14 | # Simple 15 | # warning-ignore:unused_class_variable 16 | export(Array, Color) var Simple = [ 17 | Color.black, Color.red, Color.green, Color.blue, Color.white 18 | ] 19 | 20 | # These "Scale" palettes are actually generated at run-time, but they are 21 | # exported here to the palette control generator will see them 22 | export(Array, Color) var Gray_Scale:Array 23 | export(Array, Color) var Red_Scale:Array 24 | export(Array, Color) var Green_Scale:Array 25 | export(Array, Color) var Blue_Scale:Array 26 | 27 | # Rainbow colors from R rainbow color() ramp 28 | # warning-ignore:unused_class_variable 29 | export(Array, Color) var Rainbow:Array = [ 30 | Color("#FF0000"), Color("#FF5500"), Color("#FFAA00"), Color("#FFFF00"), 31 | Color("#AAFF00"), Color("#55FF00"), Color("#00FF00"), Color("#00FF55"), 32 | Color("#00FFAA"), Color("#00FFFF"), Color("#00AAFF"), Color("#0055FF"), 33 | Color("#0000FF"), Color("#5500FF"), Color("#AA00FF"), Color("#FF00FF") 34 | ] 35 | 36 | # Terrain colors from R terrain.colors() ramp 37 | # warning-ignore:unused_class_variable 38 | export(Array, Color) var Terrain:Array = [ 39 | Color("#00A600"), Color("#19AF00"), Color("#35B800"), Color("#53C100"), 40 | Color("#74CA00"), Color("#97D300"), Color("#BDDC00"), Color("#E6E600"), 41 | Color("#E7CE1D"), Color("#E9BD3A"), Color("#EAB358"), Color("#ECB176"), 42 | Color("#EDB694"), Color("#EFC2B3"), Color("#F1D6D3"), Color("#F2F2F2") 43 | ] 44 | 45 | # Topographic colors from R topo.colors() ramp 46 | # warning-ignore:unused_class_variable 47 | export(Array, Color) var Topographic:Array = [ 48 | Color("#4C00FF"), Color("#0F00FF"), Color("#002EFF"), Color("#006BFF"), 49 | Color("#00A8FF"), Color("#00E5FF"), Color("#00FF4D"), Color("#00FF00"), 50 | Color("#4DFF00"), Color("#99FF00"), Color("#E6FF00"), Color("#FFFF00"), 51 | Color("#FFEA2D"), Color("#FFDE59"), Color("#FFDB86"), Color("#FFE0B3") 52 | ] 53 | 54 | 55 | func _init(): # fill in the "Scale" palettes 56 | var step:float = 1.0 / 16.0 57 | for n in 16: 58 | var intensity:float = step * (n + 1) 59 | Gray_Scale.push_back(Color(intensity, intensity, intensity, 1.0)) 60 | Red_Scale.push_back(Color(intensity, 0.0, 0.0, 1.0)) 61 | Green_Scale.push_back(Color(0.0, intensity, 0.0, 1.0)) 62 | Blue_Scale.push_back(Color(0.0, 0.0, intensity, 1.0)) 63 | 64 | 65 | func set_palette_options(): # populate the PaletteOption UI button 66 | var flags = PROPERTY_USAGE_SCRIPT_VARIABLE + PROPERTY_USAGE_DEFAULT 67 | var id:int = 0 68 | for property in get_property_list(): 69 | if property.usage & flags == flags: 70 | Global.controls["PaletteOption"].add_item(property.name.capitalize(), id) 71 | Global.controls["PaletteOption"].set_item_metadata(id, self[property.name]) 72 | id += 1 73 | 74 | 75 | func build_active_palette_controls(): 76 | # "Colors" is a VBoxContainer that will hold all the palette controls 77 | var colors:VBoxContainer = Global.controls["Colors"] 78 | # Remove any controls already in the container 79 | while colors.get_child_count() > 0: 80 | colors.get_child(0).free() 81 | # Get the active palette and append 8 transparent colors (user colors) 82 | var palette_colors:Array = Global.controls["PaletteOption"].get_selected_metadata().duplicate() 83 | for n in 8: 84 | palette_colors.push_back(Color.transparent) 85 | # For each color build an HBoxContainer with a checkbox, a number, and a color picker 86 | var n:int = 0 87 | for color in palette_colors: 88 | var hbox = HBoxContainer.new() 89 | hbox.set("custom_constants/separation", 8) 90 | colors.add_child(hbox) 91 | var check_box = CheckBox.new() 92 | hbox.add_child(check_box) 93 | var label = Label.new() 94 | label.text = "%3s:" % [n] 95 | label.size_flags_horizontal = Control.SIZE_EXPAND_FILL 96 | label.size_flags_stretch_ratio = 0.3 97 | n += 1 98 | hbox.add_child(label) 99 | var color_picker_button = ColorPickerButton.new() 100 | color_picker_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL 101 | color_picker_button.color = color 102 | color_picker_button.get_picker().add_preset(color) 103 | hbox.add_child(color_picker_button) 104 | 105 | 106 | func active_color_list(): 107 | var colors:VBoxContainer = Global.controls["Colors"] 108 | var color_list = [] 109 | for hbox in colors.get_children(): 110 | if hbox.get_child(0).pressed: 111 | color_list.push_back(hbox.get_child(2).color) 112 | return color_list 113 | 114 | 115 | func _on_PaletteOption_item_selected(_index): 116 | build_active_palette_controls() 117 | var colors:VBoxContainer = Global.controls["Colors"] 118 | for n in colors.get_child_count() - 8: 119 | colors.get_child(n).get_child(0).pressed = true 120 | 121 | 122 | var previous_selected:int = -1 123 | 124 | 125 | func _on_PaletteOption_button_up(): 126 | previous_selected = Global.controls["PaletteOption"].selected 127 | 128 | 129 | func _on_PaletteOption_toggled(_button_pressed): 130 | var colors:VBoxContainer = Global.controls["Colors"] 131 | if Global.controls["PaletteOption"].selected == previous_selected: 132 | var new_state:bool = not colors.get_child(0).get_child(0).pressed 133 | for n in colors.get_child_count() - 8: 134 | colors.get_child(n).get_child(0).pressed = new_state 135 | previous_selected = -1 136 | -------------------------------------------------------------------------------- /Tiley.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | var sprites = [] # array of generated Sprite instances 4 | var active_gener # keep track of currently active gener node 5 | 6 | # references to nodes we need often (i.e. generally meaning more than once) 7 | onready var sprites_container:PanelContainer = $Sprites 8 | onready var palettes:Node = $Palettes 9 | onready var gener_option:OptionButton = Global.controls.GenerOption 10 | onready var gener_inspector:MarginContainer = Global.controls.GenerInspector 11 | onready var gener_description:RichTextLabel = Global.controls.GenerDescription 12 | onready var grid_size:SpinBox = Global.controls.GridSize 13 | onready var tiley_size:SpinBox = Global.controls.TileySize 14 | onready var tiley_scale:SpinBox = Global.controls.TileyScale 15 | onready var background:ColorPickerButton = Global.controls.Background 16 | 17 | 18 | func _init(): 19 | Global.tiley = self 20 | 21 | 22 | func _ready(): 23 | # build the UI control that shows the list of geners 24 | set_gener_options() 25 | # build the UI controls that shows the available palettes 26 | palettes.set_palette_options() 27 | # build the controls for the active (default in this case) palette 28 | palettes.build_active_palette_controls() 29 | # build the controls for the active (default in this case) gener 30 | set_active_gener() 31 | # set optional default grid options 32 | set_grid_defaults() 33 | # generate an initial set of Sprites from the default settings 34 | generate() 35 | 36 | 37 | func set_gener_options(): # build the GenerOption UI control 38 | for gener in $Gener.get_children(): 39 | gener_option.add_item(gener.gener_name, gener.get_index()) 40 | gener_option.set_item_metadata(gener.get_index(), gener) 41 | 42 | 43 | func set_active_gener(): # build the UI for the active gener 44 | active_gener = gener_option.get_selected_metadata() 45 | gener_inspector.create_inspector(active_gener) 46 | gener_description.bbcode_text = active_gener.gener_description 47 | 48 | 49 | func set_grid_defaults(): 50 | var d:Dictionary = active_gener.gener_grid_defaults if active_gener.gener_grid_defaults else {} 51 | grid_size.value = d.grid_size if d.has("grid_size") else 4 52 | tiley_size.value = d.tiley_size if d.has("tiley_size") else 128 53 | tiley_scale.value = d.tiley_scale if d.has("tiley_scale") else 2.0 54 | background.color = d.background_color if d.has("background_color") else Color.black 55 | sprites_container.get("custom_styles/panel").bg_color = background.color 56 | adjust_scale() 57 | 58 | 59 | func generate(): 60 | # create the Sprite instances 61 | sprites = [] 62 | while sprites_container.get_child_count() > 0: 63 | sprites_container.get_child(0).free() 64 | for n in pow(grid_size.value, 2 ): 65 | var sprite := Sprite.new() 66 | sprites_container.add_child(sprite) 67 | sprites.push_back(sprite) 68 | var cell_size:Vector2 = (1.0 / grid_size.value) * sprites_container.rect_size 69 | var start_position:Vector2 = 0.5 * cell_size 70 | for n in sprites_container.get_child_count(): 71 | var r:int = n / int(grid_size.value) 72 | var c:int = n % int(grid_size.value) 73 | var p:Vector2 = start_position + Vector2(c * cell_size.x, r * cell_size.y) 74 | sprites[n].position = p 75 | # call the active gener's tiley_gener() for each Sprite 76 | var active_colors = palettes.active_color_list() 77 | for n in sprites.size(): 78 | sprites[n].texture = ImageTexture.new() 79 | sprites[n].texture.create_from_image(active_gener.tiley_gener(Vector2(tiley_size.value, tiley_size.value), active_colors), 0) 80 | sprites[n].scale = Vector2(tiley_scale.value, tiley_scale.value) 81 | 82 | 83 | func adjust_scale(): # make sure the images fit nicely in the window 84 | var pixel_count = (tiley_size.value / 32) + grid_size.value * (tiley_size.value + (tiley_size.value / 32)) 85 | var max_scale = stepify(sprites_container.rect_size.x / pixel_count, 0.1) 86 | if tiley_scale.value > max_scale: 87 | tiley_scale.value = max_scale 88 | 89 | 90 | func _on_Generate_pressed(): 91 | generate() 92 | 93 | 94 | func _on_GridSize_value_changed(_value): 95 | adjust_scale() 96 | 97 | 98 | func _on_TileySize_value_changed(_value): 99 | adjust_scale() 100 | 101 | 102 | func _on_TileyScale_value_changed(_value): 103 | adjust_scale() 104 | 105 | 106 | # todo - something other than hardcode these defaults? 107 | func _on_Reset_pressed(): 108 | set_grid_defaults() 109 | 110 | 111 | func _on_Background_color_changed(color): 112 | sprites_container.get("custom_styles/panel").bg_color = color 113 | 114 | 115 | # call the OS to handle clicked links in the gener descriptions 116 | func _on_GenerDescription_meta_clicked(meta): 117 | var err = OS.shell_open(meta) 118 | assert(err == OK) 119 | 120 | 121 | func _on_GenerOption_item_selected(_index): 122 | set_active_gener() 123 | set_grid_defaults() 124 | generate() 125 | 126 | 127 | # "reset" active gener by instancing a new one 128 | func _on_ResetGener_pressed(): 129 | # we'll need our script path and our position in the tree 130 | var active_script = active_gener.script.resource_path 131 | var active_index = active_gener.get_index() 132 | # preserve export properties 133 | var active_name = active_gener.gener_name 134 | var active_description = active_gener.gener_description 135 | # remove current gener from the tree and free it 136 | $Gener.remove_child(active_gener) 137 | active_gener.free() 138 | # instance a new one and restore export properties 139 | active_gener = load(active_script).new() 140 | active_gener.gener_name = active_name 141 | active_gener.gener_description = active_description 142 | # put it back in the tree at the same location 143 | $Gener.add_child(active_gener) 144 | $Gener.move_child(active_gener, active_index) 145 | # rebuild the controls and go 146 | gener_option.set_item_metadata(active_gener.get_index(), active_gener) 147 | set_active_gener() 148 | generate() 149 | -------------------------------------------------------------------------------- /geners/random_pixels.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # warning-ignore:unused_class_variable 4 | export(String) var gener_name 5 | # warning-ignore:unused_class_variable 6 | export(String, MULTILINE) var gener_description 7 | # warning-ignore:unused_class_variable 8 | var gener_grid_defaults = { 9 | "grid_size": 4, 10 | "tiley_size": 128, 11 | "tiley_scale": 2, 12 | "background_color": Color.black, 13 | } 14 | # warning-ignore:unused_class_variable 15 | var gener_properties = { 16 | "number_of_passes": ["array", "pass_pixels", 4, "int", 1000, 0, 1000000], 17 | "dice_for_passes": ["array", "pass_dice", 4, "int", 1, 1, 10], 18 | "symmetry": ["option", ["None", "Vertical", "Horizontal", "Both"]], 19 | "symmetry_style": ["option", ["Full", "Slice"]], 20 | "color_mode": ["option", ["Per Pass (Random)", "Per Pass (Ordered)", "Per Pixel", "Intensity", "Intensity Palette"]], 21 | "use_random_color_alpha": ["bool"], 22 | } 23 | var number_of_passes:int = 1 24 | var pass_pixels = [] 25 | var dice_for_passes:int = 1 26 | var pass_dice = [] 27 | var symmetry:String = "None" 28 | var symmetry_style:String = "Full" 29 | var color_mode = "Per Pass (Random)" 30 | var use_random_color_alpha:bool = false 31 | 32 | 33 | func tiley_gener(size:Vector2, colors:Array = []) -> Image: 34 | var image := Image.new() 35 | image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 36 | match symmetry: 37 | "None": 38 | random_pixels(image, colors) 39 | "Vertical": 40 | var left_image := Image.new() 41 | if symmetry_style == "Full": 42 | left_image.create(round(size.x / 2) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 43 | random_pixels(left_image, colors) 44 | else: 45 | left_image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 46 | random_pixels(left_image, colors) 47 | left_image.crop(round(size.x / 2) as int, round(size.y) as int) 48 | image.blit_rect(left_image, Rect2(Vector2.ZERO, left_image.get_size()), Vector2.ZERO) 49 | left_image.flip_x() 50 | image.blit_rect(left_image, Rect2(Vector2.ZERO, left_image.get_size()), Vector2(left_image.get_width(), 0)) 51 | "Horizontal": 52 | var top_image := Image.new() 53 | if symmetry_style == "Full": 54 | top_image.create(round(size.x) as int, round(size.y / 2) as int, false, Image.FORMAT_RGBA8) 55 | random_pixels(top_image, colors) 56 | else: 57 | top_image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 58 | random_pixels(top_image, colors) 59 | top_image.crop(round(size.x) as int, round(size.y / 2) as int) 60 | image.blit_rect(top_image, Rect2(Vector2.ZERO, top_image.get_size()), Vector2.ZERO) 61 | top_image.flip_y() 62 | image.blit_rect(top_image, Rect2(Vector2.ZERO, top_image.get_size()), Vector2(0, top_image.get_height())) 63 | "Both": 64 | var quadrant_image := Image.new() 65 | if symmetry_style == "Full": 66 | quadrant_image.create(round(size.x / 2) as int, round(size.y / 2) as int, false, Image.FORMAT_RGBA8) 67 | random_pixels(quadrant_image, colors) 68 | else: 69 | quadrant_image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 70 | random_pixels(quadrant_image, colors) 71 | quadrant_image.crop(round(size.x / 2) as int, round(size.y / 2) as int) 72 | image.blit_rect(quadrant_image, Rect2(Vector2.ZERO, quadrant_image.get_size()), Vector2.ZERO) 73 | quadrant_image.flip_x() 74 | image.blit_rect(quadrant_image, Rect2(Vector2.ZERO, quadrant_image.get_size()), Vector2(quadrant_image.get_width(), 0)) 75 | quadrant_image.flip_y() 76 | image.blit_rect(quadrant_image, Rect2(Vector2.ZERO, quadrant_image.get_size()), Vector2(quadrant_image.get_width(), quadrant_image.get_height())) 77 | quadrant_image.flip_x() 78 | image.blit_rect(quadrant_image, Rect2(Vector2.ZERO, quadrant_image.get_size()), Vector2(0, quadrant_image.get_height())) 79 | return image 80 | 81 | 82 | func random_pixels(image:Image, colors:Array): 83 | image.lock() 84 | for n in number_of_passes: 85 | var color_chooser = ColorChooser.new(colors, color_mode, use_random_color_alpha, image.get_size(), n) 86 | var dice_this_pass:int = pass_dice[n % dice_for_passes] 87 | for i in pass_pixels[n]: 88 | var x:int = dice(dice_this_pass, image.get_width()) 89 | var y:int = dice(dice_this_pass, image.get_height()) 90 | var c:Color = color_chooser.next_color(x, y) 91 | image.set_pixel(x, y, c) 92 | image.unlock() 93 | 94 | 95 | func dice(dice:int, pixels:float) -> int: 96 | var roll:float = 0 97 | for n in dice: 98 | roll += randf() * (pixels / dice) 99 | return int(roll) 100 | 101 | 102 | class ColorChooser: 103 | 104 | var palette:Array 105 | var mode:String 106 | var size:Vector2 107 | var layer:int 108 | var randomize_alpha:bool 109 | var base_color:Color 110 | var hits:Array 111 | 112 | 113 | func _init(colors:Array, color_mode:String, use_random_alpha:bool, image_size:Vector2, pass_number:int): 114 | palette = colors 115 | mode = color_mode 116 | randomize_alpha = use_random_alpha 117 | size = image_size 118 | layer = pass_number 119 | match mode: 120 | "Per Pass (Random)", "Intensity": 121 | base_color = choose_color() 122 | "Per Pass (Ordered)": 123 | base_color = choose_color(layer) 124 | if mode in ["Intensity", "Intensity Palette"]: 125 | hits = [] 126 | hits.resize(round(size.x) as int) 127 | for i in size.x: 128 | hits[i] = [] 129 | hits[i].resize(round(size.y) as int) 130 | for j in size.y: 131 | hits[i][j] = 0 132 | 133 | 134 | func next_color(x:int, y:int) -> Color: 135 | match mode: 136 | "Per Pass (Random)", "Per Pass (Ordered)": 137 | return base_color 138 | "Per Pixel": 139 | return choose_color() 140 | "Intensity": 141 | hits[x][y] += 1 142 | var alpha = min(1.0, (1.0 / 16) * hits[x][y]) 143 | return Color(base_color.r, base_color.g, base_color.b, alpha) 144 | "Intensity Palette": 145 | if palette.size() > 0: 146 | var color = min(hits[x][y], palette.size() - 1) 147 | hits[x][y] += 1 148 | return palette[color] 149 | else: 150 | return Color.transparent 151 | _: 152 | return Color.transparent 153 | 154 | 155 | func choose_color(n:int = -1) -> Color: 156 | if palette.size() == 0: 157 | if randomize_alpha: 158 | return Color(randf(), randf(), randf(), randf()) 159 | else: 160 | return Color(randf(), randf(), randf()) 161 | else: 162 | if n == -1: 163 | return palette[randi() % palette.size()] 164 | else: 165 | return palette[n % palette.size()] 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tiley Gener 2 | Tiley Gener (i.e. "Tile Generator") is a random tile/sprite generator written in GDScript using the Godot engine. The app is a GUI framework and a set of generators. New generators can be easily added by simply adding a new Node under the "Gener" node in the scene. Several interesting generators are included with the app. Generated tiles can be saved as PNG files for use in other projects. 3 | 4 | [Here is a video that provides a nice overview of the app and a quick tutorial on adding your own tile generators.](https://youtu.be/YLijwgcLor4) 5 | 6 | ## App Overview 7 | ![doc/tileygener.jpg](doc/tileygener.jpg) 8 | 9 | The app is divided into four panels: 10 | 11 | - The leftmost panel displays the generated tiles 12 | - The top right panel controls the size and number of tiles displayed 13 | - The far right panel allows for selecting palettes and colors for tiles 14 | - The central panel is a property inspector for the current tile generator 15 | 16 | Generated tile images can be saved by clicking on them. A file save dialog will popup with a default option to save the image to the "res://save" folder with a timestamp for a default filename. 17 | 18 | ## Included Generators 19 | The included generators each contain fairly detailed information about how they work and their options in the app itself, but here is an overview of each generator and samples of generated images. 20 | ### Random Pixels 21 | Literally, it generates tiles with random pixels. It has some options that allow for the generation of multiple layers (passes) of random pixels, and the ability to change the random distribution by "rolling dice" instead of just using RNG across the range of pixels. It also has some options for symmetry, and various options for assigning colors to layers or pixels. 22 | 23 | ![](doc/random1.png) ![](doc/random2.png) ![](doc/random3.png) 24 | ### Fixed Set Attractors 25 | Images are generated by first creating a set of fixed points, each with an attraction strength. One of the fixed points is chosen at random as the starting pixel. The next pixel is generated by choosing any one of the other fixed points at random, and moving toward it by the distance specified by the new point's attraction value. For example, if the attraction value is 0.5, the new pixel will be exactly halfway between the current position and the new pixel. This process is repeated until the desired number of pixels has been generated. 26 | 27 | *The default setup can result in a special case: three fixed points uniformly distributed along the edge of a circle, with each point having an attraction of 0.5. The result is a fractal known as the [Sierpiński triangle](https://en.wikipedia.org/wiki/Sierpi%C5%84ski_triangle).* 28 | 29 | ![](doc/fixed_attractors1.png) ![](doc/fixed_attractors2.png) ![](doc/fixed_attractors3.png) 30 | ### Cellular Automata 31 | The tile is initialized to a random collection of points, and then one or more passes of a 2D 3x3 [cellular automaton](https://en.wikipedia.org/wiki/Cellular_automaton) rule is run on the points. In simple terms, for each pixel, it's eight neighbors are examined, and the number of "on" or "alive" neighbors is counted. Based on that count, and whether the center is pixel is itself on or off, the automaton rule specifies whether this pixel should be on or off in the next generation. 32 | 33 | ![](doc/cellular_automata1.png) ![](doc/cellular_automata2.png) ![](doc/cellular_automata3.png) 34 | ### Diamond-Square Terrain Algorithm 35 | The [Diamond-Square Algorithm](https://en.wikipedia.org/wiki/Diamond-square_algorithm) is a fractal heightmap algorithm. The algorithm has a tendency to create artifacts along major rows and diagonals, and there are better noise-based terrain generators now in existence, but the diamond-square algorithm is easy to implement and produces some very nice results for the minimal effort. 36 | 37 | ![](doc/diamond_square1.png) ![](doc/diamond_square2.png) ![](doc/diamond_square3.png) 38 | ### Spirograph 39 | A Spirograph, if you've never seen one, is perhaps best explained by the result of a [Google image search for the word "spirograph"](https://www.google.com/search?q=spirograph&tbm=isch). Technically, a Spirograph drawing is a curve formed by rotating a moving circular disc against the inner or outer edge of a fixed circluar disc, with a pen fixed at some distance from the center of the moving circle. [This page](http://www.mathematische-basteleien.de/spirographs.htm) contains an excellent explanation of the actual mathematics, and this implementation is based on the formulas presented on that page. (Note: Spirographs generated using moving or fixed discs that have shapes other than circular are possible, but this implementation considers only circular discs.) 40 | 41 | ![](doc/spirograph1.png) ![](doc/spirograph2.png) ![](doc/spirograph3.png) 42 | ## Adding New Generators 43 | The best way to learn to add a new generator is to watch [this video](https://youtu.be/YLijwgcLor4) which shows step by step how to add a new generator, get properties for your generator into the inspector, work with colors, and add new palettes. The video is a walk-through of implementing a simple generator that produces some suprisingly complex and interesting images. 44 | 45 | If you prefer to skip the video and just dive in, it's not difficult at all, studying the code of the included generators will get you going. Here's a quick overview of the steps involved in adding a new generator: 46 | 47 | 1. Add a new node of type Node under the node named Gener in the scene tree. (Note that the order in which the nodes appear in the list of Gener's children is the order they will appear in the OptionButton for selecting them.) Name your node something descriptive. 48 | 2. Add a script to your new node, and then copy the code from gener_template.gd (in the res:// folder) into your new script. 49 | 3. In the inspector, edit the Gener Name and Gener Description properties. The Gener Name is the text that will be displayed in the OptionButton for selecting your generator. The Gener Description is the text that describes how your generator works, the options, etc. The Gener Description supports bbcode. 50 | 4. In your new script, find the code for the generate_pixels() function. 51 | ![](doc/generate_pixels.jpg) 52 | This is where you will add the code to generate pixels. The function receives an image and an array (possibly empty) of colors. The image is the image where you should generate the pixels, and the colors from the color palette if any colors are selected. If no colors are selected by the user, the colors array will be empty. It is up to your code to decide what to do with the color palette in any case. See the included generators for examples on ways to apply colors. 53 | 5. Your generator can specify the grid defaults it prefers by changing the values of the gener_grid_defaults dictionary near the top of the script. 54 | 6. If your generator has properties or options that you want the user to be able to manipulate at runtime, create variables for those properties and identify them to the inspector via the gener_properties dictionary. 55 | 56 | ## Author 57 | 58 | [Ron Pacheco](mailto:ron@wolfden.pub) 59 | 60 | ## License 61 | 62 | [MIT](https://github.com/wolfdenpublishing/tileygener/blob/master/LICENSE) 63 | -------------------------------------------------------------------------------- /geners/diamond_square.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # warning-ignore:unused_class_variable 4 | export(String) var gener_name 5 | # warning-ignore:unused_class_variable 6 | export(String, MULTILINE) var gener_description 7 | 8 | # warning-ignore:unused_class_variable 9 | var gener_grid_defaults = { 10 | "grid_size": 2, 11 | "tiley_size": 256, 12 | "tiley_scale": 4, 13 | "background_color": Color.black, 14 | } 15 | 16 | # warning-ignore:unused_class_variable 17 | var gener_properties = { 18 | "use_random_seeds": ["bool"], 19 | "minimum_random_seed": ["float", -1.0, 1.0, 0.1], 20 | "maximum_random_seed": ["float", -1.0, 1.0, 0.1], 21 | "upper_left_seed": ["float", -1.0, 1.0, 0.1], 22 | "upper_right_seed": ["float", -1.0, 1.0, 0.1], 23 | "lower_right_seed": ["float", -1.0, 1.0, 0.1], 24 | "lower_left_seed": ["float", -1.0, 1.0, 0.1], 25 | "minimum_passes": ["int", 1, 10], 26 | "maximum_passes": ["int", 1, 10], 27 | "random_falloff": ["float", 0.0, 1.0,0.01], 28 | "perform_square_step": ["bool"], 29 | "perform_diamond_step": ["bool"], 30 | "palette_smoothness": ["float", 0.0, 1.0, 0.1], 31 | "random_palette_colors": ["int", 1, 32], 32 | } 33 | var use_random_seeds:bool = true 34 | var minimum_random_seed:float = -0.25 setget set_minimum_random_seed 35 | var maximum_random_seed:float = 0.25 setget set_maximum_random_seed 36 | var upper_left_seed:float = 0.0 37 | var upper_right_seed:float = 0.0 38 | var lower_right_seed:float = 0.0 39 | var lower_left_seed:float = 0.0 40 | var minimum_passes:int = 1 setget set_minimum_passes 41 | var maximum_passes:int = 2 setget set_maximum_passes 42 | var random_falloff:float = 0.25 43 | var perform_square_step:bool = true 44 | var perform_diamond_step:bool = true 45 | var palette_smoothness:float = 0.1 46 | var random_palette_colors:int = 16 47 | 48 | 49 | func set_minimum_random_seed(value): 50 | minimum_random_seed = value 51 | if minimum_random_seed > maximum_random_seed: 52 | maximum_random_seed = minimum_random_seed 53 | Global.controls.GenerInspector.update_inspector() 54 | 55 | 56 | func set_maximum_random_seed(value): 57 | maximum_random_seed = value 58 | if maximum_random_seed < minimum_random_seed: 59 | minimum_random_seed = maximum_random_seed 60 | Global.controls.GenerInspector.update_inspector() 61 | 62 | 63 | func set_minimum_passes(value): 64 | minimum_passes = value 65 | if minimum_passes > maximum_passes: 66 | maximum_passes = minimum_passes 67 | Global.controls.GenerInspector.update_inspector() 68 | 69 | 70 | func set_maximum_passes(value): 71 | maximum_passes = value 72 | if maximum_passes < minimum_passes: 73 | minimum_passes = maximum_passes 74 | Global.controls.GenerInspector.update_inspector() 75 | 76 | 77 | func tiley_gener(size:Vector2, colors:Array = []) -> Image: 78 | var image := Image.new() 79 | image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 80 | diamond_square(image, colors) 81 | return image 82 | 83 | 84 | func diamond_square(image:Image, colors:Array) -> void: 85 | var map = diamond_square_map(image) 86 | var palette = colors.duplicate() 87 | if palette.size() < 1: 88 | palette.push_back(Color(randf(), randf(), randf(), randf())) 89 | while palette.size() < random_palette_colors: 90 | palette.push_back(Color(randf(), randf(), randf(), randf())) 91 | var color_count:int = palette.size() 92 | image.lock() 93 | for x in image.get_width(): 94 | for y in image.get_height(): 95 | # compute distance along palette 96 | var d:float = color_count * clamp(map[x][y], 0, 1) 97 | # determine current palette position 98 | var i:int = min(floor(d) as int, color_count - 1) 99 | # distance into current position 100 | d = d - i 101 | var p := Color(palette[i].r, palette[i].g, palette[i].b, palette[i].a) 102 | if color_count == 1: 103 | p.a = d 104 | elif palette_smoothness > 0.05 and ((d < 0.5 * palette_smoothness) or (d > 1 - 0.5 * palette_smoothness)): 105 | var q:Color 106 | if d > 0.5: 107 | d = 1 - d 108 | q = palette[i+1] if i < color_count - 2 else palette[color_count - 1] 109 | else: 110 | q = palette[i-1] if i > 0 else palette[0] 111 | var ap:float = 0.5 + d / palette_smoothness 112 | var aq:float = 0.5 - d / palette_smoothness 113 | p.r = ap * p.r + aq * q.r 114 | p.g = ap * p.g + aq * q.g 115 | p.b = ap * p.b + aq * q.b 116 | image.set_pixel(x, y, p) 117 | image.unlock() 118 | 119 | 120 | func diamond_square_map(image:Image) -> Array: 121 | var dim = pow(2, ceil(log(image.get_size().x) / log(2))) + 1 122 | var seeds = [upper_left_seed, upper_right_seed, lower_right_seed, lower_left_seed] 123 | if use_random_seeds: 124 | for i in 4: 125 | seeds[i] = rand_range(minimum_random_seed, maximum_random_seed) 126 | var map := Map.new(dim, seeds) 127 | var passes:int = randi() % (maximum_passes - minimum_passes + 1) + minimum_passes 128 | for i in passes: 129 | var r := Vector2(rand_range(-0.25, 0.0), rand_range(0.0, 0.5)) 130 | var step_size:int = dim - 1 131 | while(step_size > 1): 132 | var x:int = 0 133 | while(x < dim - 1): 134 | var y:int = 0 135 | while(y < dim - 1): 136 | var x1:int = x + step_size 137 | var y1:int = y + step_size 138 | var x2:int = x + step_size / 2 139 | var y2:int = y + step_size / 2 140 | # diamond step 141 | if perform_diamond_step: 142 | map.setv(x2, y2, (map.map[x][y] + map.map[x][y1] + map.map[x1][y1] + map.map[x1][y]) / 4.0 + rand_range(r.x, r.y)) 143 | # square step 144 | if perform_square_step: 145 | var c:float = map.getv(x2, y2) 146 | var nw:float = map.getv(x, y) 147 | var ne:float = map.getv(x1, y) 148 | var sw:float = map.getv(x, y1) 149 | var se:float = map.getv(x1, y1) 150 | var nn:float = map.getv(x2, y - step_size / 2) 151 | var ss:float = map.getv(x2, y1 + step_size / 2) 152 | var ww:float = map.getv(x - step_size / 2, y2) 153 | var ee:float = map.getv(x1 + step_size / 2, y2) 154 | map.setv(x2, y, (c + nw + ne + nn) / 4.0 + rand_range(r.x, r.y)) #up 155 | map.setv(x2, y1, (c + sw + se + ss) / 4.0 + rand_range(r.x, r.y)) #down 156 | map.setv(x, y2, (c + nw + sw + ww) / 4.0 + rand_range(r.x, r.y)) #left 157 | map.setv(x1, y2, (c + ne + se + ee) / 4.0 + rand_range(r.x, r.y)) #right 158 | r = (1 - random_falloff) * r 159 | y += step_size 160 | x += step_size 161 | step_size /= 2 162 | map.normalize(-0.25, 1.0) 163 | return map.map 164 | 165 | 166 | class Map: 167 | 168 | var dim:int 169 | var bound:int 170 | var map:Array = [] 171 | 172 | 173 | func _init(dimen:int, seeds:Array = []) -> void: 174 | dim = dimen 175 | bound = dim - 1 176 | for i in dim: 177 | map.push_back([]) 178 | for j in dim: 179 | map[i].push_back(0.0) 180 | while seeds.size() < 4: 181 | seeds.push_back(0.0) 182 | map[0][0] = seeds[0] 183 | map[0][bound] = seeds[1] 184 | map[bound][bound] = seeds[2] 185 | map[bound][0] = seeds[3] 186 | 187 | 188 | func getv(x:int, y:int) -> float: 189 | if x > bound: 190 | x -= dim 191 | elif x < 0: 192 | x += dim 193 | if y > bound: 194 | y -= dim 195 | elif y < 0: 196 | y += dim 197 | return map[x][y] 198 | 199 | 200 | func setv(x:int, y:int, v:float) -> void: 201 | map[x][y] = v 202 | 203 | 204 | func normalize(lo:float = 0.0, hi:float = 1.0) -> void: 205 | var map_lo:float = map[0][0] 206 | var map_hi:float = map[0][0] 207 | for i in dim: 208 | for j in dim: 209 | if map[i][j] < map_lo: 210 | map_lo = map[i][j] 211 | if map[i][j] > map_hi: 212 | map_hi = map[i][j] 213 | var scale:float = abs(hi - lo) / abs(map_hi - map_lo) 214 | for i in map.size(): 215 | for j in map[i].size(): 216 | map[i][j] = scale * (map[i][j] - map_lo) + lo 217 | -------------------------------------------------------------------------------- /GenerInspector.gd: -------------------------------------------------------------------------------- 1 | extends MarginContainer 2 | 3 | 4 | var property_monitors = [] 5 | 6 | 7 | func create_inspector(node:Node) -> void: 8 | while get_child_count() > 0: 9 | get_child(0).free() 10 | property_monitors = [] 11 | var vbox := VBoxContainer.new() 12 | vbox.set("custom_constants/separation", 8) 13 | add_child(vbox) 14 | for property in node.gener_properties: 15 | add_control(vbox, node, property) 16 | 17 | 18 | func add_control(vbox:VBoxContainer, node:Node, property:String): 19 | var hbox := HBoxContainer.new() 20 | vbox.add_child(hbox) 21 | add_label(hbox, property.capitalize()) 22 | var control 23 | match node.gener_properties[property][0]: 24 | "bool": 25 | control = add_bool_control(hbox, node, property) 26 | "int", "float": 27 | control = add_numeric_control(hbox, node, property) 28 | "string": 29 | control = add_string_control(hbox, node, property) 30 | "option": 31 | control = add_option_control(hbox, node, property) 32 | "array": 33 | control = add_array_control(hbox, node, property) 34 | _: 35 | control = add_label(hbox, "?: %s" % node.gener_properties[property][0]) 36 | property_monitors.push_back(PropertyUpdater.new(node, property, control)) 37 | 38 | 39 | func add_label(hbox:HBoxContainer, label_text:String): 40 | var label := Label.new() 41 | label.size_flags_horizontal = SIZE_EXPAND_FILL 42 | label.text = label_text 43 | hbox.add_child(label) 44 | 45 | 46 | func add_bool_control(hbox:HBoxContainer, node:Node, property:String, array_element:int = -1): 47 | var check_box = CheckBox.new() 48 | check_box.size_flags_horizontal = SIZE_EXPAND_FILL 49 | if array_element == -1: 50 | check_box.pressed = node[property] 51 | else: 52 | check_box.pressed = node[node.gener_properties[property][1]][array_element] 53 | hbox.add_child(check_box) 54 | return check_box 55 | 56 | 57 | func add_numeric_control(hbox:HBoxContainer, node:Node, property:String, array_element:int = -1): 58 | var spin_box := SpinBox.new() 59 | spin_box.size_flags_horizontal = SIZE_EXPAND_FILL 60 | if array_element == -1: 61 | spin_box.min_value = node.gener_properties[property][1] 62 | spin_box.max_value = node.gener_properties[property][2] 63 | if node.gener_properties[property][0] == "float": 64 | spin_box.step = node.gener_properties[property][3] 65 | spin_box.value = node[property] 66 | else: 67 | spin_box.min_value = node.gener_properties[property][5] 68 | spin_box.max_value = node.gener_properties[property][6] 69 | if node.gener_properties[property][3] == "float": 70 | spin_box.step = node.gener_properties[property][7] 71 | spin_box.value = node[node.gener_properties[property][1]][array_element] 72 | hbox.add_child(spin_box) 73 | return spin_box 74 | 75 | 76 | func add_string_control(hbox:HBoxContainer, node:Node, property:String, array_element:int = -1): 77 | var line_edit := LineEdit.new() 78 | line_edit.size_flags_horizontal = SIZE_EXPAND_FILL 79 | if array_element == -1: 80 | line_edit.text = node[property] 81 | else: 82 | line_edit.text = node[node.gener_properties[property][1]][array_element] 83 | hbox.add_child(line_edit) 84 | return line_edit 85 | 86 | 87 | func add_option_control(hbox:HBoxContainer, node:Node, property:String, array_element:int = -1): 88 | var option_button := OptionButton.new() 89 | option_button.size_flags_horizontal = SIZE_EXPAND_FILL 90 | var option_list 91 | if array_element == -1: 92 | option_list = node.gener_properties[property][1] 93 | else: 94 | option_list = node.gener_properties[property][5] 95 | var n := 0 96 | for option in option_list: 97 | option_button.add_item(option, n) 98 | if array_element == -1: 99 | if node[property] == option: 100 | option_button.selected = n 101 | else: 102 | if option == node.gener_properties[property][4]: 103 | option_button.selected = n 104 | n += 1 105 | hbox.add_child(option_button) 106 | return option_button 107 | 108 | 109 | func add_array_control(hbox:HBoxContainer, node:Node, property:String): 110 | var parent_vbox = hbox.get_parent() 111 | parent_vbox.remove_child(hbox) 112 | var vbox = VBoxContainer.new() 113 | parent_vbox.add_child(vbox) 114 | vbox.add_child(hbox) 115 | var control_variable := SpinBox.new() 116 | control_variable.size_flags_horizontal = SIZE_EXPAND_FILL 117 | control_variable.min_value = 0 118 | control_variable.max_value = node.gener_properties[property][2] 119 | control_variable.value = node[property] 120 | hbox.add_child(control_variable) 121 | var vbox2 := VBoxContainer.new() 122 | vbox2.set("custom_constants/separation", 8) 123 | hbox.get_parent().add_child(vbox2) 124 | var property_array = node[node.gener_properties[property][1]] 125 | var array_elements = node.gener_properties[property][2] 126 | if property_array.size() < array_elements: 127 | property_array.resize(array_elements) 128 | for n in array_elements: 129 | property_array[n] = node.gener_properties[property][4] 130 | for n in array_elements: 131 | var hbox2 := HBoxContainer.new() 132 | vbox2.add_child(hbox2) 133 | add_label(hbox2, " %s [%s]" % [node.gener_properties[property][1].capitalize(), n]) 134 | var control 135 | match node.gener_properties[property][3]: 136 | "bool": 137 | control = add_bool_control(hbox2, node, property, n) 138 | "int", "float": 139 | control = add_numeric_control(hbox2, node, property, n) 140 | "string": 141 | control = add_string_control(hbox2, node, property, n) 142 | "option": 143 | control = add_option_control(hbox2, node, property, n) 144 | _: 145 | control = add_label(hbox2, "?: %s" % node.gener_properties[property][3]) 146 | if n >= node[property]: 147 | control.get_parent().visible = false 148 | return control_variable 149 | 150 | 151 | func update_inspector(): 152 | for monitor in property_monitors: 153 | match monitor.node.gener_properties[monitor.property][0]: 154 | "bool": 155 | monitor.control.pressed = monitor.node[monitor.property] 156 | "int", "float": 157 | monitor.control.value = monitor.node[monitor.property] 158 | "string": 159 | monitor.control.text = monitor.node[monitor.property] 160 | "option": 161 | pass 162 | "array": 163 | pass 164 | 165 | 166 | class PropertyUpdater: 167 | 168 | var node:Node 169 | var property:String 170 | var control:Control 171 | var array_element:int 172 | var array_element_monitors = [] 173 | 174 | 175 | func _init(node_in:Node, property_in:String, control_in:Control, array_element_in:int = -1): 176 | node = node_in 177 | property = property_in 178 | control = control_in 179 | array_element = array_element_in 180 | var type:String 181 | var on_array_element:String 182 | if array_element == -1: 183 | type = node.gener_properties[property][0] 184 | on_array_element = "" 185 | else: 186 | type = node.gener_properties[property][3] 187 | on_array_element = "_array_element" 188 | match type: 189 | "bool": 190 | var err = control.connect("toggled", self, "_on_update" + on_array_element) 191 | assert(err == OK) 192 | "int", "float": 193 | var err = control.connect("value_changed", self, "_on_update" + on_array_element) 194 | assert(err == OK) 195 | "string": 196 | var err = control.connect("text_changed", self, "_on_update" + on_array_element) 197 | assert(err == OK) 198 | "option": 199 | var err = control.connect("item_selected", self, "_on_option_update" + on_array_element) 200 | assert(err == OK) 201 | "array": 202 | var err = control.connect("value_changed", self, "_on_array_size_update") 203 | assert(err == OK) 204 | var vbox = control.get_parent().get_parent().get_child(1) 205 | for n in vbox.get_child_count(): 206 | var array_element_control = vbox.get_child(n).get_child(1) 207 | array_element_monitors.push_back(PropertyUpdater.new(node, property, array_element_control, n)) 208 | 209 | 210 | func _on_update(value): 211 | node[property] = value 212 | 213 | 214 | func _on_option_update(index): 215 | node[property] = node.gener_properties[property][1][index] 216 | 217 | 218 | func _on_array_size_update(value): 219 | node[property] = value 220 | var vbox = control.get_parent().get_parent().get_child(1) 221 | for n in vbox.get_child_count(): 222 | vbox.get_child(n).visible = n < value 223 | 224 | 225 | func _on_update_array_element(value): 226 | node[node.gener_properties[property][1]][array_element] = value 227 | 228 | 229 | func _on_option_update_array_element(index): 230 | node[node.gener_properties[property][1]][array_element] = node.gener_properties[property][5][index] 231 | -------------------------------------------------------------------------------- /geners/cellular_automata.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # warning-ignore:unused_class_variable 4 | export(String) var gener_name 5 | # warning-ignore:unused_class_variable 6 | export(String, MULTILINE) var gener_description 7 | 8 | # warning-ignore:unused_class_variable 9 | var gener_grid_defaults = { 10 | "grid_size": 4, 11 | "tiley_size": 32, 12 | "tiley_scale": 8, 13 | "background_color": Color.black, 14 | } 15 | 16 | # warning-ignore:unused_class_variable 17 | var gener_properties = { 18 | "minimum_start_pixels": ["int", 0, 1000000], 19 | "maximum_start_pixels": ["int", 0, 1000000], 20 | "start_pixel_dice": ["int", 1, 10], 21 | "use_random_automata": ["bool"], 22 | "state_0_rules": ["string"], 23 | "state_1_rules": ["string"], 24 | "minimum_generations": ["int", 0, 1000000], 25 | "maximum_generations": ["int", 0, 1000000], 26 | "color_mode": ["option", ["Image", "Pixel", "Generation Intensity", "Generation Palette"]], 27 | "symmetry": ["option", ["None", "Vertical", "Horizontal", "Both"]], 28 | } 29 | var minimum_start_pixels:int = 500 setget set_minimum_start_pixels 30 | var maximum_start_pixels:int = 1000 setget set_maximum_start_pixels 31 | var start_pixel_dice:int = 2 32 | var use_random_automata:bool = false 33 | var state_0_rules:String = "000101000" setget set_state_0_rules 34 | var state_1_rules:String = "001100010" setget set_state_1_rules 35 | var minimum_generations:int = 1 setget set_minimum_generations 36 | var maximum_generations:int = 8 setget set_maximum_generations 37 | var color_mode:String = "Image" 38 | var symmetry:String = "Both" 39 | 40 | var user_rules:Array 41 | 42 | 43 | func set_minimum_start_pixels(value): 44 | minimum_start_pixels = value 45 | if minimum_start_pixels > maximum_start_pixels: 46 | maximum_start_pixels = minimum_start_pixels 47 | Global.controls.GenerInspector.update_inspector() 48 | 49 | 50 | func set_maximum_start_pixels(value): 51 | maximum_start_pixels = value 52 | if maximum_start_pixels < minimum_start_pixels: 53 | minimum_start_pixels = maximum_start_pixels 54 | Global.controls.GenerInspector.update_inspector() 55 | 56 | 57 | func set_state_0_rules(value): 58 | state_0_rules = value 59 | user_rules = rules(state_0_rules, state_1_rules) 60 | 61 | 62 | func set_state_1_rules(value): 63 | state_1_rules = value 64 | user_rules = rules(state_0_rules, state_1_rules) 65 | 66 | 67 | func set_minimum_generations(value): 68 | minimum_generations = value 69 | if minimum_generations > maximum_generations: 70 | maximum_generations = minimum_generations 71 | Global.controls.GenerInspector.update_inspector() 72 | 73 | 74 | func set_maximum_generations(value): 75 | maximum_generations = value 76 | if maximum_generations < minimum_generations: 77 | minimum_generations = maximum_generations 78 | Global.controls.GenerInspector.update_inspector() 79 | 80 | 81 | func _init(): 82 | user_rules = rules(state_0_rules, state_1_rules) 83 | 84 | 85 | func rules(state_0:String = "", state_1:String = "") -> Array: 86 | if state_0 == "": 87 | state_0 = random_rule_string() 88 | if state_1 == "": 89 | state_1 = random_rule_string() 90 | var rules:Array = [] 91 | rules.append([]) 92 | for i in state_0.length(): 93 | rules[0].append(state_0[i] == "1") 94 | rules.append([]) 95 | for i in state_1.length(): 96 | rules[1].append(state_1[i] == "1") 97 | return rules 98 | 99 | 100 | func random_rule_string() -> String: 101 | var s:String = "" 102 | for i in 9: 103 | s += "0" if randi() % 2 == 0 else "1" 104 | return s 105 | 106 | 107 | func tiley_gener(size:Vector2, colors:Array = []) -> Image: 108 | var image := Image.new() 109 | image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 110 | match symmetry: 111 | "None": 112 | generate_pixels(image, colors) 113 | "Vertical": 114 | var left_image := Image.new() 115 | left_image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 116 | generate_pixels(left_image, colors) 117 | left_image.crop(round(size.x / 2) as int, round(size.y) as int) 118 | image.blit_rect(left_image, Rect2(Vector2.ZERO, left_image.get_size()), Vector2.ZERO) 119 | left_image.flip_x() 120 | image.blit_rect(left_image, Rect2(Vector2.ZERO, left_image.get_size()), Vector2(left_image.get_width(), 0)) 121 | "Horizontal": 122 | var top_image := Image.new() 123 | top_image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 124 | generate_pixels(top_image, colors) 125 | top_image.crop(round(size.x) as int, round(size.y / 2) as int) 126 | image.blit_rect(top_image, Rect2(Vector2.ZERO, top_image.get_size()), Vector2.ZERO) 127 | top_image.flip_y() 128 | image.blit_rect(top_image, Rect2(Vector2.ZERO, top_image.get_size()), Vector2(0, top_image.get_height())) 129 | "Both": 130 | var quadrant_image := Image.new() 131 | quadrant_image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 132 | generate_pixels(quadrant_image, colors) 133 | quadrant_image.crop(round(size.x / 2) as int, round(size.y / 2) as int) 134 | image.blit_rect(quadrant_image, Rect2(Vector2.ZERO, quadrant_image.get_size()), Vector2.ZERO) 135 | quadrant_image.flip_x() 136 | image.blit_rect(quadrant_image, Rect2(Vector2.ZERO, quadrant_image.get_size()), Vector2(quadrant_image.get_width(), 0)) 137 | quadrant_image.flip_y() 138 | image.blit_rect(quadrant_image, Rect2(Vector2.ZERO, quadrant_image.get_size()), Vector2(quadrant_image.get_width(), quadrant_image.get_height())) 139 | quadrant_image.flip_x() 140 | image.blit_rect(quadrant_image, Rect2(Vector2.ZERO, quadrant_image.get_size()), Vector2(0, quadrant_image.get_height())) 141 | return image 142 | 143 | 144 | func generate_pixels(image:Image, colors:Array) -> void: 145 | var color_chooser = ColorChooser.new(colors, color_mode) 146 | random_pixels(image, color_chooser) 147 | var rules:Array = rules() if use_random_automata else user_rules 148 | var generations:int = randi() % (maximum_generations - minimum_generations + 1) + minimum_generations 149 | for gen in generations: 150 | next_generation(image, color_chooser, rules, gen, generations) 151 | 152 | 153 | func next_generation(image:Image, color_chooser:ColorChooser, rules:Array, gen:int, generations:int) -> void: 154 | var generation_color = color_chooser.next_color(float(gen) / float(generations)) 155 | var next_image := Image.new() 156 | next_image.create(image.get_width(), image.get_height(), false, Image.FORMAT_RGBA8) 157 | next_image.lock() 158 | image.lock() 159 | for x in image.get_width(): 160 | for y in image.get_height(): 161 | var p:int = 1 if image.get_pixel(x, y).a > 0.0 else 0 162 | var n:int = neighbor_count(image, x, y) 163 | if rules[p][n]: 164 | if color_mode == "Pixel": 165 | next_image.set_pixel(x, y, color_chooser.next_color()) 166 | elif image.get_pixel(x, y).a > 0.0: 167 | next_image.set_pixel(x, y, image.get_pixel(x, y)) 168 | else: 169 | next_image.set_pixel(x, y, generation_color) 170 | image.unlock() 171 | next_image.unlock() 172 | image.copy_from(next_image) 173 | 174 | 175 | func neighbor_count(image:Image, x:int, y:int) -> int: 176 | var image_rect:Rect2 = Rect2(Vector2.ZERO, image.get_size()) 177 | var n:int = 0 178 | for i in [x - 1, x, x + 1]: 179 | for j in [y - 1, y, y + 1]: 180 | if image_rect.has_point(Vector2(i,j)) and (i != x or j != y): 181 | if image.get_pixel(i,j).a > 0.0: 182 | n += 1 183 | return n 184 | 185 | 186 | func random_pixels(image:Image, color_chooser:ColorChooser) -> void: 187 | image.lock() 188 | var start_pixels := randi() % (maximum_start_pixels - minimum_start_pixels + 1) + minimum_start_pixels 189 | for i in start_pixels: 190 | var x:int = dice(start_pixel_dice, image.get_width()) 191 | var y:int = dice(start_pixel_dice, image.get_height()) 192 | var c:Color = color_chooser.next_color() 193 | image.set_pixel(x, y, c) 194 | image.unlock() 195 | 196 | 197 | func dice(dice:int, pixels:float) -> int: 198 | var roll:float = 0 199 | for n in dice: 200 | roll += randf() * (pixels / dice) 201 | return int(roll) 202 | 203 | 204 | class ColorChooser: 205 | 206 | var palette:Array 207 | var mode:String 208 | var base_color:Color 209 | 210 | 211 | func _init(colors:Array, color_mode:String): 212 | palette = colors 213 | mode = color_mode 214 | if mode in ["Image", "Generation Intensity"]: 215 | base_color = choose_color() 216 | 217 | 218 | func next_color(gen:float = 0.0) -> Color: 219 | match mode: 220 | "Image": 221 | return base_color 222 | "Pixel": 223 | return choose_color() 224 | "Generation Intensity": 225 | if gen > 0.0: 226 | return Color(base_color.r, base_color.g, base_color.b, gen) 227 | else: 228 | return base_color 229 | "Generation Palette": 230 | if palette.size() > 0: 231 | var color:int = round((palette.size() - 1) * gen) as int 232 | return palette[color] 233 | else: 234 | return Color.transparent 235 | _: 236 | return Color.transparent 237 | 238 | 239 | func choose_color() -> Color: 240 | if palette.size() == 0: 241 | return Color(randf(), randf(), randf()) 242 | else: 243 | return palette[randi() % palette.size()] 244 | -------------------------------------------------------------------------------- /geners/fixed_set_attractors.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # warning-ignore:unused_class_variable 4 | export(String) var gener_name 5 | # warning-ignore:unused_class_variable 6 | export(String, MULTILINE) var gener_description 7 | # warning-ignore:unused_class_variable 8 | var gener_grid_defaults = { 9 | "grid_size": 4, 10 | "tiley_size": 128, 11 | "tiley_scale": 2, 12 | "background_color": Color.black, 13 | } 14 | # warning-ignore:unused_class_variable 15 | var gener_properties = { 16 | "minimum_fixed_points": ["int", 3, 100], 17 | "maximum_fixed_points": ["int", 3, 100], 18 | "angle_style": ["option", ["Uniform", "Random"]], 19 | "start_angle": ["float", 0.0, 360.0, 1.0], 20 | "end_angle": ["float", 0.0, 360.0, 1.0], 21 | "radius_style": ["option", ["Fixed", "Uniform (Min to Max)", "Uniform (Max to Min)", "Random"]], 22 | "minimum_radius": ["float", 0.0, 1.0, 0.1], 23 | "maximum_radius": ["float", 0.0, 1.0, 0.1], 24 | "attraction_style": ["option", ["Fixed", "Uniform (Min to Max)", "Uniform (Max to Min)", "Random"]], 25 | "minimum_attraction": ["float", -10.0, 10.0, 0.1], 26 | "maximum_attraction": ["float", -10.0, 10.0, 0.1], 27 | "specific_attraction_values": ["array", "fixed_point_attraction", 1000, "float", 0.5, -10.0, 10.0, 0.1], 28 | "minimum_pixels": ["int", 0, 1000000], 29 | "maximum_pixels": ["int", 0, 1000000], 30 | "color_mode": ["option", ["Image", "Pixel", "Intensity", "Intensity Palette"]], 31 | "symmetry": ["option", ["None", "Vertical", "Horizontal", "Both"]], 32 | } 33 | var minimum_fixed_points := 3 setget set_minimum_fixed_points 34 | var maximum_fixed_points := 9 setget set_maximum_fixed_points 35 | var angle_style := "Uniform" 36 | var start_angle := 0.0 37 | var end_angle := 360.0 38 | var radius_style := "Fixed" 39 | var minimum_radius := 0.2 setget set_minimum_radius 40 | var maximum_radius := 0.8 setget set_maximum_radius 41 | var attraction_style := "Fixed" 42 | var minimum_attraction := 0.25 setget set_minimum_attraction 43 | var maximum_attraction := 1.50 setget set_maximum_attraction 44 | var specific_attraction_values := 0 45 | var fixed_point_attraction = [] 46 | var minimum_pixels := 1000 setget set_minimum_pixels 47 | var maximum_pixels := 10000 setget set_maximum_pixels 48 | var color_mode := "Image" 49 | var symmetry := "None" 50 | 51 | 52 | func set_minimum_fixed_points(value): 53 | minimum_fixed_points = value 54 | if minimum_fixed_points > maximum_fixed_points: 55 | maximum_fixed_points = minimum_fixed_points 56 | Global.controls.GenerInspector.update_inspector() 57 | 58 | 59 | func set_maximum_fixed_points(value): 60 | maximum_fixed_points = value 61 | if maximum_fixed_points < minimum_fixed_points: 62 | minimum_fixed_points = maximum_fixed_points 63 | Global.controls.GenerInspector.update_inspector() 64 | 65 | 66 | func set_minimum_radius(value): 67 | minimum_radius = value 68 | if minimum_radius > maximum_radius: 69 | maximum_radius = minimum_radius 70 | Global.controls.GenerInspector.update_inspector() 71 | 72 | 73 | func set_maximum_radius(value): 74 | maximum_radius = value 75 | if maximum_radius < minimum_radius: 76 | minimum_radius = maximum_radius 77 | Global.controls.GenerInspector.update_inspector() 78 | 79 | 80 | func set_minimum_attraction(value): 81 | minimum_attraction = value 82 | if minimum_attraction > maximum_attraction: 83 | maximum_attraction = minimum_attraction 84 | Global.controls.GenerInspector.update_inspector() 85 | 86 | 87 | func set_maximum_attraction(value): 88 | maximum_attraction = value 89 | if maximum_attraction < minimum_attraction: 90 | minimum_attraction = maximum_attraction 91 | Global.controls.GenerInspector.update_inspector() 92 | 93 | 94 | func set_minimum_pixels(value): 95 | minimum_pixels = value 96 | if minimum_pixels > maximum_pixels: 97 | maximum_pixels = minimum_pixels 98 | Global.controls.GenerInspector.update_inspector() 99 | 100 | 101 | func set_maximum_pixels(value): 102 | maximum_pixels = value 103 | if maximum_pixels < minimum_pixels: 104 | minimum_pixels = maximum_pixels 105 | Global.controls.GenerInspector.update_inspector() 106 | 107 | 108 | func tiley_gener(size:Vector2, colors:Array = []) -> Image: 109 | var image := Image.new() 110 | image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 111 | var fixed_points = generate_fixed_points(size) 112 | match symmetry: 113 | "None": 114 | generate_pixels(image, fixed_points, colors) 115 | "Vertical": 116 | var left_image := Image.new() 117 | left_image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 118 | generate_pixels(left_image, fixed_points, colors) 119 | left_image.crop(round(size.x / 2) as int, round(size.y) as int) 120 | image.blit_rect(left_image, Rect2(Vector2.ZERO, left_image.get_size()), Vector2.ZERO) 121 | left_image.flip_x() 122 | image.blit_rect(left_image, Rect2(Vector2.ZERO, left_image.get_size()), Vector2(left_image.get_width(), 0)) 123 | "Horizontal": 124 | var top_image := Image.new() 125 | top_image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 126 | generate_pixels(top_image, fixed_points, colors) 127 | top_image.crop(round(size.x) as int, round(size.y / 2) as int) 128 | image.blit_rect(top_image, Rect2(Vector2.ZERO, top_image.get_size()), Vector2.ZERO) 129 | top_image.flip_y() 130 | image.blit_rect(top_image, Rect2(Vector2.ZERO, top_image.get_size()), Vector2(0, top_image.get_height())) 131 | "Both": 132 | var quadrant_image := Image.new() 133 | quadrant_image.create(round(size.x) as int, round(size.y) as int, false, Image.FORMAT_RGBA8) 134 | generate_pixels(quadrant_image, fixed_points, colors) 135 | quadrant_image.crop(round(size.x / 2) as int, round(size.y / 2) as int) 136 | image.blit_rect(quadrant_image, Rect2(Vector2.ZERO, quadrant_image.get_size()), Vector2.ZERO) 137 | quadrant_image.flip_x() 138 | image.blit_rect(quadrant_image, Rect2(Vector2.ZERO, quadrant_image.get_size()), Vector2(quadrant_image.get_width(), 0)) 139 | quadrant_image.flip_y() 140 | image.blit_rect(quadrant_image, Rect2(Vector2.ZERO, quadrant_image.get_size()), Vector2(quadrant_image.get_width(), quadrant_image.get_height())) 141 | quadrant_image.flip_x() 142 | image.blit_rect(quadrant_image, Rect2(Vector2.ZERO, quadrant_image.get_size()), Vector2(0, quadrant_image.get_height())) 143 | return image 144 | 145 | 146 | func generate_fixed_points(size:Vector2): 147 | var image_points := randi() % (maximum_fixed_points - minimum_fixed_points + 1) + minimum_fixed_points 148 | var image_radius := rand_range(minimum_radius, maximum_radius) 149 | var image_attraction := rand_range(minimum_attraction, maximum_attraction) 150 | var fixed_points = [] 151 | for n in image_points: 152 | var a:float 153 | var r:float 154 | var t:float 155 | match angle_style: 156 | "Uniform": 157 | a = uniform(start_angle, end_angle, image_points, n) 158 | "Random": 159 | a = rand_range(start_angle, end_angle) 160 | match radius_style: 161 | "Fixed": 162 | r = image_radius 163 | "Uniform (Min to Max)": 164 | r = uniform(minimum_radius, maximum_radius, image_points, n) 165 | "Uniform (Max to Min)": 166 | r = uniform(maximum_radius, minimum_radius, image_points, n) 167 | "Random": 168 | r = rand_range(minimum_radius, maximum_radius) 169 | if n < specific_attraction_values: 170 | t = fixed_point_attraction[n] 171 | else: 172 | match attraction_style: 173 | "Fixed": 174 | t = image_attraction 175 | "Uniform (Min to Max)": 176 | t = uniform(minimum_attraction, maximum_attraction, image_points, n) 177 | "Uniform (Max to Min)": 178 | t = uniform(maximum_attraction, minimum_attraction, image_points, n) 179 | "Random": 180 | t = rand_range(minimum_attraction, maximum_attraction) 181 | var p:Vector2 = r * 0.5 * size.x * Vector2(cos(deg2rad(a)), sin(deg2rad(a))) 182 | fixed_points.push_back(Vector3(p.x + 0.5 * size.x, p.y + 0.5 * size.y, t)) 183 | return fixed_points 184 | 185 | 186 | func uniform(start:float, end:float, partitions:int, n:int): 187 | return start + n * ((end - start) / partitions) 188 | 189 | 190 | func generate_pixels(image:Image, fixed_points:Array, colors:Array): 191 | var image_rect := Rect2(Vector2.ZERO, image.get_size()) 192 | var image_pixels := randi() % (maximum_pixels - minimum_pixels + 1) + minimum_pixels 193 | var color_chooser = ColorChooser.new(colors, color_mode, image.get_size()) 194 | var p3:Vector3 = fixed_points[0] 195 | var p2 := Vector2(p3.x, p3.y) 196 | image.lock() 197 | for n in image_pixels: 198 | var q3:Vector3 = choose_element(fixed_points) 199 | var q2 := Vector2(q3.x, q3.y) 200 | p2 = p2 + p2.direction_to(q2) * q3.z * p2.distance_to(q2) 201 | if image_rect.has_point(p2): 202 | image.set_pixelv(p2, color_chooser.next_color(p2.x, p2.y)) 203 | image.unlock() 204 | 205 | 206 | func choose_element(array:Array): 207 | assert(array.size() > 0) 208 | return array[randi() % array.size()] 209 | 210 | 211 | class ColorChooser: 212 | 213 | var palette:Array 214 | var mode:String 215 | var size:Vector2 216 | var base_color:Color 217 | var hits:Array 218 | 219 | 220 | func _init(colors:Array, color_mode:String, image_size:Vector2): 221 | palette = colors 222 | mode = color_mode 223 | size = image_size 224 | if mode in ["Image", "Intensity"]: 225 | base_color = choose_color() 226 | if mode in ["Intensity", "Intensity Palette"]: 227 | hits = [] 228 | hits.resize(round(size.x) as int) 229 | for i in size.x: 230 | hits[i] = [] 231 | hits[i].resize(round(size.y) as int) 232 | for j in size.y: 233 | hits[i][j] = 0 234 | 235 | 236 | func next_color(x:int, y:int) -> Color: 237 | match mode: 238 | "Image": 239 | return base_color 240 | "Pixel": 241 | return choose_color() 242 | "Intensity": 243 | hits[x][y] += 1 244 | var alpha = min(1.0, (1.0 / 16) * hits[x][y]) 245 | return Color(base_color.r, base_color.g, base_color.b, alpha) 246 | "Intensity Palette": 247 | if palette.size() > 0: 248 | var color = min(hits[x][y], palette.size() - 1) 249 | hits[x][y] += 1 250 | return palette[color] 251 | else: 252 | return Color.transparent 253 | _: 254 | return Color.transparent 255 | 256 | 257 | func choose_color() -> Color: 258 | if palette.size() == 0: 259 | return Color(randf(), randf(), randf()) 260 | else: 261 | return palette[randi() % palette.size()] 262 | -------------------------------------------------------------------------------- /Tiley.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=32 format=2] 2 | 3 | [ext_resource path="res://Tiley.gd" type="Script" id=1] 4 | [ext_resource path="res://fonts/Roboto-Regular.ttf" type="DynamicFontData" id=2] 5 | [ext_resource path="res://fonts/Roboto-Bold.ttf" type="DynamicFontData" id=3] 6 | [ext_resource path="res://GenerInspector.gd" type="Script" id=4] 7 | [ext_resource path="res://geners/random_pixels.gd" type="Script" id=5] 8 | [ext_resource path="res://geners/fixed_set_attractors.gd" type="Script" id=6] 9 | [ext_resource path="res://fonts/Roboto-Light.ttf" type="DynamicFontData" id=7] 10 | [ext_resource path="res://fonts/Roboto-BoldItalic.ttf" type="DynamicFontData" id=8] 11 | [ext_resource path="res://fonts/Inconsolata-Regular.ttf" type="DynamicFontData" id=9] 12 | [ext_resource path="res://Palettes.gd" type="Script" id=10] 13 | [ext_resource path="res://fonts/Roboto-RegularItalic.ttf" type="DynamicFontData" id=11] 14 | [ext_resource path="res://geners/cellular_automata.gd" type="Script" id=12] 15 | [ext_resource path="res://geners/spirograph.gd" type="Script" id=13] 16 | [ext_resource path="res://geners/diamond_square.gd" type="Script" id=14] 17 | [ext_resource path="res://Sprites.gd" type="Script" id=16] 18 | 19 | [sub_resource type="StyleBoxFlat" id=1] 20 | bg_color = Color( 0, 0, 0, 1 ) 21 | 22 | [sub_resource type="DynamicFont" id=2] 23 | font_data = ExtResource( 2 ) 24 | 25 | [sub_resource type="Theme" id=3] 26 | default_font = SubResource( 2 ) 27 | 28 | [sub_resource type="DynamicFont" id=4] 29 | font_data = ExtResource( 2 ) 30 | 31 | [sub_resource type="Theme" id=5] 32 | default_font = SubResource( 4 ) 33 | 34 | [sub_resource type="StyleBoxFlat" id=6] 35 | bg_color = Color( 0.110757, 0.110757, 0.166016, 1 ) 36 | 37 | [sub_resource type="DynamicFont" id=7] 38 | size = 18 39 | font_data = ExtResource( 3 ) 40 | 41 | [sub_resource type="DynamicFont" id=8] 42 | size = 14 43 | font_data = ExtResource( 7 ) 44 | 45 | [sub_resource type="StyleBoxFlat" id=9] 46 | bg_color = Color( 0.109804, 0.109804, 0.164706, 1 ) 47 | 48 | [sub_resource type="DynamicFont" id=10] 49 | size = 14 50 | font_data = ExtResource( 7 ) 51 | 52 | [sub_resource type="DynamicFont" id=11] 53 | font_data = ExtResource( 9 ) 54 | 55 | [sub_resource type="DynamicFont" id=12] 56 | font_data = ExtResource( 8 ) 57 | 58 | [sub_resource type="DynamicFont" id=13] 59 | font_data = ExtResource( 11 ) 60 | 61 | [sub_resource type="DynamicFont" id=14] 62 | font_data = ExtResource( 3 ) 63 | 64 | [sub_resource type="DynamicFont" id=15] 65 | font_data = ExtResource( 2 ) 66 | 67 | [sub_resource type="StyleBoxFlat" id=16] 68 | bg_color = Color( 0.109804, 0.109804, 0.164706, 1 ) 69 | 70 | [node name="Tiley" type="Node"] 71 | script = ExtResource( 1 ) 72 | 73 | [node name="Palettes" type="Node" parent="."] 74 | script = ExtResource( 10 ) 75 | 76 | [node name="Sprites" type="PanelContainer" parent="."] 77 | margin_right = 1080.0 78 | margin_bottom = 1080.0 79 | custom_styles/panel = SubResource( 1 ) 80 | script = ExtResource( 16 ) 81 | __meta__ = { 82 | "_edit_use_anchors_": false 83 | } 84 | 85 | [node name="SaveDialog" type="FileDialog" parent="."] 86 | margin_right = 502.0 87 | margin_bottom = 309.0 88 | rect_min_size = Vector2( 300, 105 ) 89 | theme = SubResource( 3 ) 90 | popup_exclusive = true 91 | window_title = "Save Image" 92 | resizable = true 93 | mode_overrides_title = false 94 | filters = PoolStringArray( "*.png;PNG Images" ) 95 | current_dir = "res://save" 96 | current_file = "r" 97 | current_path = "res://save/r" 98 | __meta__ = { 99 | "_edit_use_anchors_": false 100 | } 101 | 102 | [node name="Gener" type="Node" parent="."] 103 | 104 | [node name="random_pixels" type="Node" parent="Gener"] 105 | script = ExtResource( 5 ) 106 | gener_name = "Random Pixels" 107 | gener_description = "[u][b]DESCRIPTION[/b][/u] 108 | 109 | Fill a tile with one or more passes of random pixels. Colors can be random, or a specific color can be assigned to each pass. The number of pixels generated for a pass can be controlled. The distribution of the random pixels can be modified by using \"dice\" to generate the random numbers. 110 | 111 | [u][b]Colors[/b][/u] 112 | 113 | [i]Per Pass (Random)[/i] chooses a random color for each pass. If any palette colors are selected the random color is chosen from among them, otherwise the random color is chosen from among all possible colors. Randomly chosen colors use alpha = 1.0 unless [i]Use Random Color Alpha[/i] is checked, in which case alpha is also random. [i]Per Pass (Ordered)[/i] uses the palette colors, in order, for each pass. If no palette colors are selected, the behavior is the same as [i]Per Pass (Random)[/i]. [i]Per Pixel[/i] assigns a random color to each pixel, choosing from the palette colors if availble, or all colors if no palette colors are selected. [i]Intensity[/i] works like [i]Per Pass (Random)[/i] except pixels are given increasing alpha values across 16 intensities each time they are reused. [i]Intensity Palette[/i] is similar, except that it uses the selected palette colors for increasing intensity. 114 | 115 | [u][b]\"Dice\"[/b][/u] 116 | 117 | Random pixels are generated using the concept of rolling [i]n[/i] dice with [i]pixels[/i] / [i]n[/i] sides, where [i]pixels[/i] is the pixel dimensin of the image tile being generated. For example, for a tile of 128 pixels with the default of 1 die, the random x or y position is generated by \"rolling\" a single 128-sided die, resultin in pixels that spread more-or-less uniformly across the pixel range. If 2 dice are used, each die will have 128 / 2 = 64 sides. Rollding two 64-sided dice will generated random numbers across the pixel range, but with a tendency to be more centralized. The greater the number of dice, the greater the central tendency. 118 | 119 | [u][b]Symmetry[/b][/u] 120 | 121 | Options 122 | 123 | Three symmetry options are available, [i]Vertical[/i], [i]Horizontal[/i], and [i]Both[/i]. Vertical symmetry generates the random pixels over the left half of the tile and then mirrors it to the right side of the tile. Horizontal symmetry generates the random pixels over the top half of the tile and then mirrors it to the bottom half of the tile. Choose [i]Both[/i] generates the random pixels in the upper left quadrant and then mirrors it to the other four quadrants. 124 | 125 | Style 126 | 127 | [i]Full[/i] style treats the area over which pixels are being generated, e.g. the upper left quadrant or the top half, as the entire image for the purpose of generating pixels. Thus, if a number of dice are in use which causes pixels to cluster centrally, in [i]Full[/i] style they will cluster near the center of the pixel region that will eventually be mirrored. [i]Slice[/i] instead generates pixels across the entire image as usual, and then \"slices\" out the region to be mirrored, and mirrors it using whatever symmetry is active." 128 | 129 | [node name="fixed_set_attractors" type="Node" parent="Gener"] 130 | script = ExtResource( 6 ) 131 | gener_name = "Fixed Set of Attractors" 132 | gener_description = "[u][b]DESCRIPTION[/b][/u] 133 | 134 | Images are generated by first creating a set of fixed points, each with an attraction strength. One of the fixed points is chosen at random as the starting pixel. The next pixel is generated by choosing any one of the other fixed points at random, and moving toward it by the distance specified by the new point's attraction value. For example, if the attraction value is 0.5, the new pixel will be exactly halfway between the current position and the new pixel. This process is repeated until the desired number of pixels has been generated. 135 | 136 | [i]The default setup can result in a special case: three fixed points uniformly distributed along the edge of a circle, with each point having an attraction of 0.5. The result is a fractal known as the [url=https://en.wikipedia.org/wiki/Sierpi%C5%84ski_triangle][color=aqua]Sierpiński triangle[/color][/url].[/i] 137 | 138 | [u][b]Phase 1: Fixed Point Generation[/b][/u] 139 | 140 | The position of each fixed point is established by its angle and radius relative to the center of the tile. There are several options to control how the set of fixed points are generated and their attraction values: 141 | 142 | [table=2] 143 | 144 | [cell]Angle[/cell] 145 | [cell]Points will be distributed between the minimum and maximum angle. The distribution can be uniform along the arc, or randomly placed on the arc.[/cell] 146 | 147 | [cell]Radius[/cell] 148 | [cell]Radius is specified as a fraction of the image size. For example, if the image is 256 pixels, a radius of 1.0 would be 128 pixels, 0.5 would be 64 pixels, etc. The radius of each point can be set either to fixed, uniform, or random. Fixed chooses a radius in the radius range and all points in the image are set to the specified radius. Uniform spreads the point radii uniformly from the minimum to the maximum across the range of possible radius values. Random chooses a random radius, within the radius range, for every point.[/cell] 149 | 150 | [cell]Attraction [/cell] 151 | [cell]Attraction is defined as a fraction of the straight line distance between the current pixel and the fixed point. See the text below under Pixel Generation for a more complete discussion of attraction. Attraction values can be fixed, uniform, and random, similar to radius values. A fixed attraction chooses a random attraction value from the allowed range and all fixed points in the image are set to this same attraction. Uniform spreads the attraction uniformly from the minimum to the maximum across the range of possible attraction values. Random chooses a random attraction, within the possible range of values, for each point. Finally, it is possible to directly set the attraction values for the points.[/cell] 152 | 153 | [/table] 154 | 155 | [u][b]Phase 1: Pixel Generation[/b][/u] 156 | 157 | A starting pixel is selected randomly from among the set of fixed points. The next pixel is generated by choosing any one of the other fixed points at random, and moving toward it by the distance specified by the new point's attraction value.This process is repeated until the desired number of pixels has been generated. 158 | 159 | Attraction works by considering the straight line between the current pixel and randomly selected fixed point. The new pixel will be selected by moving toward the fixed point a distance equal to the distance between them multiplied by the attraction value of the fixed point. For example, an attraction value of 0.5 would move exactly halfway between the current pixel and the new point. An attraction value of 0.0 would mean the current point doesn't move at all for the next pixel. An attraction value of 1.0 would mean that the next pixel will end up directly on top of the fixed point. Attraction values are possible outside the range of 0.0 to 1.0, and indeed may be any real number. An attraction value greater than 1.0 means the next pixel will move toward the fixed point and then [i]beyond[/i] it. A negative attraction is, in effect, repulsion, and will cause the new pixel to move directly [i]away[/i] from the fixed point." 160 | 161 | [node name="cellular_automata" type="Node" parent="Gener"] 162 | script = ExtResource( 12 ) 163 | gener_name = "Cellular Automata" 164 | gener_description = "[u][b]DESCRIPTION[/b][/u] 165 | 166 | The tile is initialized to a random collection of points, and then one or more passes of a 2D 3x3 [url=https://en.wikipedia.org/wiki/Cellular_automaton][color=aqua]cellular automaton[/color][/url] rule is run on the points. In simple terms, for each pixel, it's eight neighbors are examined, and the number of \"on\" or \"alive\" neighbors is counted. Based on that count, and whether the center is pixel is itself on or off, the automaton rule specifies whether this pixel should be on or off in the next generation. 167 | 168 | [u][b]Start Pixels[/b][/u] 169 | 170 | The starting pixels is just a simple random pixel generator. You can specify a range of pixels, by default there will be at least 500 but no more than 1000, and you can give the random pixels central tendency by increasing the number of \"dice\" used for the random numbers, in exactly the same manner as the Random Pixels generator. 171 | 172 | [u][b]Rules[/b][/u] 173 | 174 | Rules are composed of two strings. The \"State 0\" rule string specify the rules for pixels that are off, and the \"State 1\" string for pixels that are on. Each rule string is 9 characters in length, and must be a 0 or 1. The first position in the string is the rule for the next generation when the pixel is found to have 0 neigbors. The second position in the string specifies the rule if the pixel is found to have 1 neighbor. Each position in the string specifies the value of the pixel in the next generation for that specific number of neighbors. 175 | 176 | For example, if the State 0 rule is \"000100000\", then there is only one condition where an off cell will ever turn on: when it has precisely three on (i.e. live) neighbors. If the State 1 rule is \"001100000\", then an on pixel will turn off (i.e. die) unless it has exactly two or three neighbors. This example set of rules is actually [url=https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life][color=aqua]Conway's Game of Life[/color][/url], and the default ruleset for this generator is a slight variation of Conway's rules. 177 | 178 | All tiles are generated with the specified rules unless [i]Use Random Automata[/i] is checked, in which case every tile is generated from a random ruleset. 179 | 180 | [u][b]Generations[/b][/u] 181 | 182 | A single generation means that the ruleset is run once on the entire image. A second generation would be run on the result of the first generation, and so on. The default setting choose from a range of 1 to 8 generations for a tile. 183 | 184 | [u][b]Colors[/b][/u] 185 | 186 | [i]Image[/i] chooses a random color for each image generated, choosing from the palette colors if available, or from all colors if there is no palette. [i]Pixel[/i] chooses a random color for each pixel, also choosing from the palette colors if available, or from all colors if there is no palette. [i]Generation Intensity[/i] chooses a color similar to [i]Image[/i], but colors each generation with a brighter intensity, with brightness spread evenly across the number of generations. Finally, [i]Generation Palette[/i] works like [i]Generation Intensity[/i], but instead of increasing the brightness, colors are selected in order from the palette." 187 | 188 | [node name="diamond_square" type="Node" parent="Gener"] 189 | script = ExtResource( 14 ) 190 | gener_name = "Diamond-Square Algorithm" 191 | gener_description = "[u][b]DESCRIPTION[/b][/u] 192 | 193 | The [url=https://en.wikipedia.org/wiki/Diamond-square_algorithm][color=aqua]Diamond-Square Algorithm[/color][/url] is a fractal heightmap algorithm. The algorithm has a tendency to create artifacts along major rows and diagonals, and there are better noise-based terrain generators now in existence, but the diamond-square algorithm is easy to implement and produces some very nice results for the minimal effort. 194 | 195 | The algorithm works by taking a square arrangement of pixels and seeding the four corner pixels to some height value, either chosen at random or chosen to achieve a specific desired overall shape. The pixel at the very center of those four corner points is then set to to the average of the four corners (the \"diamond\" step). Next, the pixels halfway between each pair of adjacent corner points are then set to the average of the two corner points, the pixel just set in the square step, and the pixel that mirrors the square step pixel in the other direction. At the edges of the image this mirror pixel may not exist, and the general approach is to \"wrap\" around to the other side of the image in this situation, which makes for images that can be tiled in the plane. 196 | 197 | The original square image is now four \"sub\" images over which the algorithm is then performed on each of these smaller section, and the next pass will have 16 sub images, etc., and this continues until the final block is just a 3x3 array of pixels. At each level, the magnitude of the random number added to each averge is decreased slightly. 198 | 199 | [u][b]Generator Options[/b][/u] 200 | 201 | The default settings will choose random seeds for each corner, but you can also specify the seeds directly. You can change the number of times the algorithm is run over a given image. Two passes seems to be ideal for achieving a decent result. The rate at which the random number generator falls off can be adjusted with the [i]Random Falloff[/i] option. A value of 0.0 would mean no falloff at all, and a rate of 1.0 means that there is no randomness in the generated image, the result arises strictly from the seed values. 202 | 203 | [u][b]Colors[/b][/u] 204 | 205 | If no palette colors are selected, levels of increasing height will be indicated by randomly chosen color palette. The default setting will choose 16 random colors and assign that palette to the appropriate range of height values. If the setting is for a single color, the height values in the image will instead be output as alpha values over the monotone image. 206 | 207 | If a palette is active, height value are mapped evenly across the palette, with 0.0 being mapped to the first palette color, and 1.0 being mapped to the other end of the palette, and the remaining colors distributed evenly across the range of height values. Smoothing, which is blending of colors where bands of color from the palette meet up, can be adjusted. Smoothing of 0.0 means the bands will be solid regions of color with no blending between bands." 208 | 209 | [node name="spirograph" type="Node" parent="Gener"] 210 | script = ExtResource( 13 ) 211 | gener_name = "Spirograph" 212 | gener_description = "[u][b]DESCRIPTION[/b][/u] 213 | 214 | A Spirograph, if you've never seen one, is perhaps best explained by the result of a [url=https://www.google.com/search?q=spirograph&tbm=isch][color=aqua]Google image search for the word \"spirograph\"[/color][/url]. Technically, a Spirograph drawing is a curve formed by rotating a moving circular disc against the inner or outer edge of a fixed circluar disc, with a pen fixed at some distance from the center of the moving circle. [url=http://www.mathematische-basteleien.de/spirographs.htm][color=aqua]This page[/color][/url] contains an excellent explanation of the actual mathematics, and this implementation is based on the formulas presented on that page. (Note: Spirographs generated using moving or fixed discs that have shapes other than circular are possible, but this implementation considers only circular discs.) 215 | 216 | [u][b]Generator Options[/b][/u] 217 | 218 | The default settings will draw from 1 to 4 spirographs, randomly chosen to be a hypocycloid or epicycloid, overlaid in the same image. The default ranges for the circle radii, position of the pen, number of rotations, and size of the parametric step generate a wide variety of spirograph shapes. For detailed descriptions of all the variables, see [url=http://www.mathematische-basteleien.de/spirographs.htm][color=aqua]this page[/color][/url] from which the mathematics for this implementation were derived. 219 | 220 | [u][b]Colors[/b][/u] 221 | 222 | If no palette colors are selected, each spirograph curve is drawn in a random color. If palette colors are selected, each spirograph curve is drawn in a color selected randomly from the palette." 223 | 224 | [node name="UI" type="PanelContainer" parent="."] 225 | margin_left = 1080.0 226 | margin_right = 1920.0 227 | margin_bottom = 1080.0 228 | theme = SubResource( 5 ) 229 | __meta__ = { 230 | "_edit_use_anchors_": false 231 | } 232 | 233 | [node name="UIVbox" type="VBoxContainer" parent="UI"] 234 | margin_left = 7.0 235 | margin_top = 7.0 236 | margin_right = 833.0 237 | margin_bottom = 1073.0 238 | 239 | [node name="MenuBar" type="PanelContainer" parent="UI/UIVbox"] 240 | margin_right = 826.0 241 | margin_bottom = 45.0 242 | custom_styles/panel = SubResource( 6 ) 243 | 244 | [node name="MarginContainer" type="MarginContainer" parent="UI/UIVbox/MenuBar"] 245 | margin_right = 826.0 246 | margin_bottom = 45.0 247 | custom_constants/margin_right = 8 248 | custom_constants/margin_top = 8 249 | custom_constants/margin_left = 8 250 | custom_constants/margin_bottom = 8 251 | 252 | [node name="HBoxContainer" type="HBoxContainer" parent="UI/UIVbox/MenuBar/MarginContainer"] 253 | margin_left = 8.0 254 | margin_top = 8.0 255 | margin_right = 818.0 256 | margin_bottom = 37.0 257 | 258 | [node name="Generate" type="Button" parent="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer"] 259 | margin_right = 117.0 260 | margin_bottom = 29.0 261 | custom_fonts/font = SubResource( 7 ) 262 | text = " GENERATE " 263 | 264 | [node name="Label" type="Label" parent="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer"] 265 | margin_left = 121.0 266 | margin_top = 5.0 267 | margin_right = 158.0 268 | margin_bottom = 24.0 269 | text = " Grid" 270 | 271 | [node name="GridSize" type="SpinBox" parent="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer"] 272 | margin_left = 162.0 273 | margin_right = 236.0 274 | margin_bottom = 29.0 275 | min_value = 1.0 276 | max_value = 16.0 277 | value = 4.0 278 | 279 | [node name="Label2" type="Label" parent="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer"] 280 | margin_left = 240.0 281 | margin_top = 5.0 282 | margin_right = 282.0 283 | margin_bottom = 24.0 284 | text = " Tiley" 285 | 286 | [node name="TileySize" type="SpinBox" parent="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer"] 287 | margin_left = 286.0 288 | margin_right = 360.0 289 | margin_bottom = 29.0 290 | min_value = 16.0 291 | max_value = 1024.0 292 | step = 16.0 293 | value = 128.0 294 | 295 | [node name="Label3" type="Label" parent="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer"] 296 | margin_left = 364.0 297 | margin_top = 5.0 298 | margin_right = 411.0 299 | margin_bottom = 24.0 300 | text = " Scale" 301 | 302 | [node name="TileyScale" type="SpinBox" parent="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer"] 303 | margin_left = 415.0 304 | margin_right = 489.0 305 | margin_bottom = 29.0 306 | min_value = 0.1 307 | max_value = 64.0 308 | step = 0.1 309 | value = 2.0 310 | 311 | [node name="Label4" type="Label" parent="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer"] 312 | margin_left = 493.0 313 | margin_top = 5.0 314 | margin_right = 586.0 315 | margin_bottom = 24.0 316 | text = " Background" 317 | 318 | [node name="Background" type="ColorPickerButton" parent="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer"] 319 | margin_left = 590.0 320 | margin_right = 622.0 321 | margin_bottom = 29.0 322 | text = " " 323 | __meta__ = { 324 | "_edit_use_anchors_": false 325 | } 326 | 327 | [node name="Spacer" type="Label" parent="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer"] 328 | margin_left = 626.0 329 | margin_top = 5.0 330 | margin_right = 753.0 331 | margin_bottom = 24.0 332 | size_flags_horizontal = 3 333 | 334 | [node name="Reset" type="Button" parent="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer"] 335 | margin_left = 757.0 336 | margin_right = 810.0 337 | margin_bottom = 29.0 338 | custom_fonts/font = SubResource( 8 ) 339 | text = "RESET" 340 | 341 | [node name="ControlArea" type="HBoxContainer" parent="UI/UIVbox"] 342 | margin_top = 49.0 343 | margin_right = 826.0 344 | margin_bottom = 1066.0 345 | size_flags_vertical = 3 346 | 347 | [node name="InspectorMargin" type="MarginContainer" parent="UI/UIVbox/ControlArea"] 348 | margin_right = 616.0 349 | margin_bottom = 1017.0 350 | size_flags_horizontal = 3 351 | size_flags_vertical = 3 352 | size_flags_stretch_ratio = 3.0 353 | custom_constants/margin_right = 2 354 | custom_constants/margin_top = 2 355 | 356 | [node name="InspectorPanel" type="PanelContainer" parent="UI/UIVbox/ControlArea/InspectorMargin"] 357 | margin_top = 2.0 358 | margin_right = 614.0 359 | margin_bottom = 1017.0 360 | custom_styles/panel = SubResource( 9 ) 361 | 362 | [node name="ScrollContainer" type="ScrollContainer" parent="UI/UIVbox/ControlArea/InspectorMargin/InspectorPanel"] 363 | margin_right = 614.0 364 | margin_bottom = 1015.0 365 | scroll_horizontal_enabled = false 366 | 367 | [node name="InspectorControls" type="VBoxContainer" parent="UI/UIVbox/ControlArea/InspectorMargin/InspectorPanel/ScrollContainer"] 368 | margin_right = 614.0 369 | margin_bottom = 377.0 370 | size_flags_horizontal = 3 371 | 372 | [node name="MarginContainer" type="MarginContainer" parent="UI/UIVbox/ControlArea/InspectorMargin/InspectorPanel/ScrollContainer/InspectorControls"] 373 | margin_right = 614.0 374 | margin_bottom = 357.0 375 | custom_constants/margin_right = 8 376 | custom_constants/margin_top = 8 377 | custom_constants/margin_left = 8 378 | custom_constants/margin_bottom = 8 379 | 380 | [node name="VBoxContainer" type="VBoxContainer" parent="UI/UIVbox/ControlArea/InspectorMargin/InspectorPanel/ScrollContainer/InspectorControls/MarginContainer"] 381 | margin_left = 8.0 382 | margin_top = 8.0 383 | margin_right = 606.0 384 | margin_bottom = 349.0 385 | custom_constants/separation = 16 386 | 387 | [node name="HBoxContainer" type="HBoxContainer" parent="UI/UIVbox/ControlArea/InspectorMargin/InspectorPanel/ScrollContainer/InspectorControls/MarginContainer/VBoxContainer"] 388 | margin_right = 598.0 389 | margin_bottom = 25.0 390 | custom_constants/separation = 8 391 | 392 | [node name="Label" type="Label" parent="UI/UIVbox/ControlArea/InspectorMargin/InspectorPanel/ScrollContainer/InspectorControls/MarginContainer/VBoxContainer/HBoxContainer"] 393 | margin_top = 3.0 394 | margin_right = 79.0 395 | margin_bottom = 22.0 396 | text = "Tiley Gener" 397 | 398 | [node name="GenerOption" type="OptionButton" parent="UI/UIVbox/ControlArea/InspectorMargin/InspectorPanel/ScrollContainer/InspectorControls/MarginContainer/VBoxContainer/HBoxContainer"] 399 | margin_left = 87.0 400 | margin_right = 488.0 401 | margin_bottom = 25.0 402 | size_flags_horizontal = 3 403 | 404 | [node name="Spacer" type="Label" parent="UI/UIVbox/ControlArea/InspectorMargin/InspectorPanel/ScrollContainer/InspectorControls/MarginContainer/VBoxContainer/HBoxContainer"] 405 | margin_left = 496.0 406 | margin_top = 3.0 407 | margin_right = 536.0 408 | margin_bottom = 22.0 409 | size_flags_horizontal = 3 410 | size_flags_stretch_ratio = 0.1 411 | 412 | [node name="ResetGener" type="Button" parent="UI/UIVbox/ControlArea/InspectorMargin/InspectorPanel/ScrollContainer/InspectorControls/MarginContainer/VBoxContainer/HBoxContainer"] 413 | margin_left = 544.0 414 | margin_right = 597.0 415 | margin_bottom = 25.0 416 | custom_fonts/font = SubResource( 10 ) 417 | text = "RESET" 418 | 419 | [node name="GenerDescription" type="RichTextLabel" parent="UI/UIVbox/ControlArea/InspectorMargin/InspectorPanel/ScrollContainer/InspectorControls/MarginContainer/VBoxContainer"] 420 | margin_top = 41.0 421 | margin_right = 598.0 422 | margin_bottom = 341.0 423 | rect_min_size = Vector2( 0, 300 ) 424 | custom_fonts/mono_font = SubResource( 11 ) 425 | custom_fonts/bold_italics_font = SubResource( 12 ) 426 | custom_fonts/italics_font = SubResource( 13 ) 427 | custom_fonts/bold_font = SubResource( 14 ) 428 | custom_fonts/normal_font = SubResource( 15 ) 429 | bbcode_enabled = true 430 | 431 | [node name="GenerInspector" type="MarginContainer" parent="UI/UIVbox/ControlArea/InspectorMargin/InspectorPanel/ScrollContainer/InspectorControls"] 432 | margin_top = 361.0 433 | margin_right = 614.0 434 | margin_bottom = 377.0 435 | size_flags_horizontal = 3 436 | size_flags_vertical = 3 437 | custom_constants/margin_right = 8 438 | custom_constants/margin_top = 8 439 | custom_constants/margin_left = 8 440 | custom_constants/margin_bottom = 8 441 | script = ExtResource( 4 ) 442 | 443 | [node name="ColorMargin" type="MarginContainer" parent="UI/UIVbox/ControlArea"] 444 | margin_left = 620.0 445 | margin_right = 826.0 446 | margin_bottom = 1017.0 447 | size_flags_horizontal = 3 448 | size_flags_vertical = 3 449 | custom_constants/margin_top = 2 450 | custom_constants/margin_left = 2 451 | 452 | [node name="ColorPanel" type="PanelContainer" parent="UI/UIVbox/ControlArea/ColorMargin"] 453 | margin_left = 2.0 454 | margin_top = 2.0 455 | margin_right = 206.0 456 | margin_bottom = 1017.0 457 | custom_styles/panel = SubResource( 16 ) 458 | 459 | [node name="MarginContainer" type="MarginContainer" parent="UI/UIVbox/ControlArea/ColorMargin/ColorPanel"] 460 | margin_right = 204.0 461 | margin_bottom = 1015.0 462 | custom_constants/margin_right = 8 463 | custom_constants/margin_top = 8 464 | custom_constants/margin_left = 8 465 | 466 | [node name="Palette" type="VBoxContainer" parent="UI/UIVbox/ControlArea/ColorMargin/ColorPanel/MarginContainer"] 467 | margin_left = 8.0 468 | margin_top = 8.0 469 | margin_right = 196.0 470 | margin_bottom = 1015.0 471 | custom_constants/separation = 12 472 | 473 | [node name="PaletteOption" type="OptionButton" parent="UI/UIVbox/ControlArea/ColorMargin/ColorPanel/MarginContainer/Palette"] 474 | margin_right = 188.0 475 | margin_bottom = 25.0 476 | hint_tooltip = "Reselect the same palette to toggle the entire palette on/off" 477 | 478 | [node name="RichTextLabel" type="RichTextLabel" parent="UI/UIVbox/ControlArea/ColorMargin/ColorPanel/MarginContainer/Palette"] 479 | margin_top = 37.0 480 | margin_right = 188.0 481 | margin_bottom = 97.0 482 | text = "Reselect the same palette to toggle its colors on/off. Click a color to edit." 483 | fit_content_height = true 484 | 485 | [node name="Colors" type="VBoxContainer" parent="UI/UIVbox/ControlArea/ColorMargin/ColorPanel/MarginContainer/Palette"] 486 | margin_top = 109.0 487 | margin_right = 188.0 488 | margin_bottom = 109.0 489 | size_flags_horizontal = 3 490 | custom_constants/separation = 10 491 | [connection signal="file_selected" from="SaveDialog" to="Sprites" method="_on_SaveDialog_file_selected"] 492 | [connection signal="popup_hide" from="SaveDialog" to="Sprites" method="_on_SaveDialog_popup_hide"] 493 | [connection signal="pressed" from="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer/Generate" to="." method="_on_Generate_pressed"] 494 | [connection signal="value_changed" from="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer/GridSize" to="." method="_on_GridSize_value_changed"] 495 | [connection signal="value_changed" from="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer/TileySize" to="." method="_on_TileySize_value_changed"] 496 | [connection signal="value_changed" from="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer/TileyScale" to="." method="_on_TileyScale_value_changed"] 497 | [connection signal="color_changed" from="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer/Background" to="." method="_on_Background_color_changed"] 498 | [connection signal="pressed" from="UI/UIVbox/MenuBar/MarginContainer/HBoxContainer/Reset" to="." method="_on_Reset_pressed"] 499 | [connection signal="item_selected" from="UI/UIVbox/ControlArea/InspectorMargin/InspectorPanel/ScrollContainer/InspectorControls/MarginContainer/VBoxContainer/HBoxContainer/GenerOption" to="." method="_on_GenerOption_item_selected"] 500 | [connection signal="pressed" from="UI/UIVbox/ControlArea/InspectorMargin/InspectorPanel/ScrollContainer/InspectorControls/MarginContainer/VBoxContainer/HBoxContainer/ResetGener" to="." method="_on_ResetGener_pressed"] 501 | [connection signal="meta_clicked" from="UI/UIVbox/ControlArea/InspectorMargin/InspectorPanel/ScrollContainer/InspectorControls/MarginContainer/VBoxContainer/GenerDescription" to="." method="_on_GenerDescription_meta_clicked"] 502 | [connection signal="button_up" from="UI/UIVbox/ControlArea/ColorMargin/ColorPanel/MarginContainer/Palette/PaletteOption" to="Palettes" method="_on_PaletteOption_button_up"] 503 | [connection signal="item_selected" from="UI/UIVbox/ControlArea/ColorMargin/ColorPanel/MarginContainer/Palette/PaletteOption" to="Palettes" method="_on_PaletteOption_item_selected"] 504 | [connection signal="toggled" from="UI/UIVbox/ControlArea/ColorMargin/ColorPanel/MarginContainer/Palette/PaletteOption" to="Palettes" method="_on_PaletteOption_toggled"] 505 | --------------------------------------------------------------------------------