1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-02-20 20:43:14 +00:00
Pixelorama/src/Palette/Palette.gd
2023-12-23 19:00:24 +02:00

312 lines
9.1 KiB
GDScript

class_name Palette
extends RefCounted
const DEFAULT_WIDTH := 8
const DEFAULT_HEIGHT := 8
# Metadata
var name := "Custom Palette"
var comment := ""
var path := ""
## The width of the grid
var width := DEFAULT_WIDTH
## The height of the grid
var height := DEFAULT_HEIGHT
## Sparse colors dictionary of [int] and [PaletteColor]
## Actual color position in the palette is determined by its index
var colors := {}
## How many colors fit in palette grid
var colors_max := 0
class PaletteColor:
var color := Color.TRANSPARENT
var index := -1
func _init(init_color := Color.BLACK, init_index := -1) -> void:
color = init_color
index = init_index
func duplicate() -> PaletteColor:
return PaletteColor.new(color, index)
func serialize() -> Dictionary:
return {"color": color, "index": index}
func deserialize(dict: Dictionary) -> void:
if dict.has("color"):
color = dict["color"]
if dict.has("index"):
color = dict["index"]
func _init(
_name := "Custom Palette", _width := DEFAULT_WIDTH, _height := DEFAULT_HEIGHT, _comment := ""
) -> void:
name = _name
comment = _comment
width = _width
height = _height
colors_max = _width * _height
colors = {}
func edit(new_name: String, new_width: int, new_height: int, new_comment: String) -> void:
var old_width := width
width = new_width
height = new_height
name = new_name
comment = new_comment
var old_colors_max := colors_max
colors_max = width * height
if colors_max < old_colors_max:
# If size was reduced colors must be reindexed to fit into new smaller size
reindex_colors_on_size_reduce(true)
if old_width < new_width and colors_max > old_colors_max:
# If width increases colors have to be reindexed so they keep same grid positions
# unless the height has become smaller and we have to re-position the colors
# so that they won't get erased
reindex_colors_on_width_increase(old_width)
func duplicate() -> Palette:
var new_palette := Palette.new(name, width, height, comment)
var new_colors := colors.duplicate(true)
new_palette.colors = new_colors
return new_palette
func _serialize() -> String:
var serialize_data := {"comment": comment, "colors": [], "width": width, "height": height}
for color in colors:
serialize_data.colors.push_back(colors[color].serialize())
return JSON.stringify(serialize_data, " ")
func deserialize(json_string: String) -> void:
var test_json_conv := JSON.new()
var err := test_json_conv.parse(json_string)
if err != OK: # If parse has errors
printerr("JSON palette import error")
printerr("Error: ", err)
printerr("Error Line: ", test_json_conv.get_error_line())
printerr("Error String: ", test_json_conv.get_error_message())
return
var data = test_json_conv.get_data()
if not typeof(data) == TYPE_DICTIONARY:
return
if data.has("comment"):
comment = data.comment
if data.has("colors"):
for color_data in data.colors:
var color := str_to_var("Color" + color_data["color"]) as Color
var index := color_data["index"] as int
var palette_color := PaletteColor.new(color, index)
colors[index] = palette_color
if data.has("width"):
width = data.width
if data.has("height"):
height = data.height
colors_max = width * height
func save_to_file() -> Error:
var file := FileAccess.open(path, FileAccess.WRITE)
if not is_instance_valid(file):
return FileAccess.get_open_error()
file.store_string(_serialize())
file.close()
return OK
## Iterates all colors from lowest index and reindexes them so they start at zero index
## Remove trailing removes all colors that are over colors_max limit and thus don't fit into grid
func reindex_colors_on_size_reduce(remove_trailing: bool) -> void:
var sorted_colors_indexes := colors.keys()
sorted_colors_indexes.sort()
var new_index := 0
for old_index: int in sorted_colors_indexes:
# Color cannot fit into grid anymore - erase it
if remove_trailing and new_index >= colors_max:
colors.erase(old_index)
# Move color to new lower index - erase it from its original index
elif new_index < old_index:
colors[new_index] = colors[old_index]
colors[new_index].index = new_index
colors.erase(old_index)
new_index += 1
## Adds difference of old and new width to color indexes
## so they remain on the same position as before resize
func reindex_colors_on_width_increase(old_width: int) -> void:
var sorted_colors_indexes := colors.keys()
sorted_colors_indexes.sort()
var new_colors := {}
for old_index: int in sorted_colors_indexes:
var new_index := old_index + (width - old_width) * (old_index / old_width)
new_colors[new_index] = colors[old_index]
colors = new_colors
## Adds new color to the first empty swatch
func add_color(new_color: Color, start_index := 0) -> void:
if start_index >= colors_max:
return
# If palette is full automatically increase the palette height
if is_full():
height += 1
# Find the first empty index since start index and insert a new color
for i in range(start_index, colors_max):
if not colors.has(i):
colors[i] = PaletteColor.new(new_color, i)
break
## Returns color at index or null if no color exists
func get_color(index: int):
var palette_color: PaletteColor = colors.get(index)
if palette_color != null:
return palette_color.color
return null
## Changes color data
func set_color(index: int, new_color: Color) -> void:
if colors.has(index):
colors[index].color = new_color
## Removes a color at the specified index
func remove_color(index: int) -> void:
colors.erase(index)
## Inserts a color to the specified index
## If index is already occupied move the original color to right
func insert_color(index: int, new_color: Color) -> void:
var c := PaletteColor.new(new_color, index)
# If insert happens on non empty swatch recursively move the original color
# and every other color to its right one swatch to right
if colors[index] != null:
move_right(index)
colors[index] = c
## Recursive function that moves every color to right until one of them is moved to empty swatch
func move_right(index: int) -> void:
# Moving colors to right would overflow the size of the palette
# so increase its height automatically
if index + 1 == colors_max:
height += 1
colors_max = width * height
# If swatch to right to this color is not empty move that color right too
if colors[index + 1] != null:
move_right(index + 1)
colors[index + 1] = colors[index]
## Swaps two colors
func swap_colors(from_index: int, to_index: int) -> void:
var from_color: PaletteColor = colors.get(from_index)
var to_color: PaletteColor = colors.get(to_index)
if not from_color and to_color:
colors[from_index] = to_color
colors[from_index].index = from_index
colors.erase(to_index)
elif from_color and not to_color:
colors[to_index] = from_color
colors[to_index].index = to_index
colors.erase(from_index)
elif from_color and to_color:
colors[to_index] = from_color
colors[to_index].index = to_index
colors[from_index] = to_color
colors[from_index].index = from_index
## Copies color
func copy_colors(from_index: int, to_index: int) -> void:
# Only allow copy of existing colors
if colors[from_index] != null:
colors[to_index] = colors[from_index].duplicate()
colors[to_index].index = to_index
func reverse_colors() -> void:
var reversed_colors := colors.values()
reversed_colors.reverse()
colors.clear()
for i in reversed_colors.size():
reversed_colors[i].index = i
colors[i] = reversed_colors[i]
func sort(option: Palettes.SortOptions) -> void:
var sorted_colors := colors.values()
var sort_method: Callable
match option:
Palettes.SortOptions.HUE:
sort_method = func(a: PaletteColor, b: PaletteColor): return a.color.h < b.color.h
Palettes.SortOptions.SATURATION:
sort_method = func(a: PaletteColor, b: PaletteColor): return a.color.s < b.color.s
Palettes.SortOptions.VALUE:
sort_method = func(a: PaletteColor, b: PaletteColor): return a.color.v < b.color.v
Palettes.SortOptions.RED:
sort_method = func(a: PaletteColor, b: PaletteColor): return a.color.r < b.color.r
Palettes.SortOptions.GREEN:
sort_method = func(a: PaletteColor, b: PaletteColor): return a.color.g < b.color.g
Palettes.SortOptions.BLUE:
sort_method = func(a: PaletteColor, b: PaletteColor): return a.color.b < b.color.b
Palettes.SortOptions.ALPHA:
sort_method = func(a: PaletteColor, b: PaletteColor): return a.color.a < b.color.a
sorted_colors.sort_custom(sort_method)
colors.clear()
for i in sorted_colors.size():
sorted_colors[i].index = i
colors[i] = sorted_colors[i]
## True if all swatches are occupied
func is_full() -> bool:
return colors.size() >= colors_max
## True if palette has no colors
func is_empty() -> bool:
return colors.size() == 0
func has_theme_color(color: Color) -> bool:
for palette_color in colors.values():
if palette_color.color == color:
return true
return false
static func strip_unvalid_characters(string_to_strip: String) -> String:
var regex := RegEx.new()
regex.compile("[^a-zA-Z0-9_]+")
return regex.sub(string_to_strip, "", true)
func convert_to_image() -> Image:
var image := Image.create(colors_max, 1, false, Image.FORMAT_RGBA8)
for i in colors_max:
if colors.has(i):
image.set_pixel(i, 0, colors[i].color)
return image