Add modified Fragmented project files

This commit is contained in:
ChaoticByte 2025-10-10 22:05:07 +02:00
parent f99f1fbc9b
commit 77ce93fb15
Signed by: ChaoticByte
SSH key fingerprint: SHA256:bHr1NPU+JZFLUbsTl2/mfa6U+6dcM7VjKohzXnshfFY
57 changed files with 1696 additions and 0 deletions

162
README.md Normal file
View file

@ -0,0 +1,162 @@
<h1 align=center>Pigment</h1>
![screenshot](./screenshot.png)
<p align=center>Color grading with code.</p>
## Table of Contents
- [Supported Platforms](#supported-platforms)
- [Usage](#usage)
- [Shaderlib](#shaderlib)
- [Commandline interface](#commandline-interface)
- [Known Issues](#known-issues)
## Supported Platforms
- Linux
You can find the latest release ...
- [here (remotebranch.eu)](https://remotebranch.eu/ChaoticByte/Pigment/releases/latest)
## Usage
With Pigment, you are processing images with GDShaders. This brings almost endless opportunities to create unique art.
If you want to learn GDShader, take a look at the [Godot docs](https://docs.godotengine.org/en/stable/tutorials/shaders/).
**To get started, use the project template (see the Releases section of this repo) and open it in Godot.**
The template includes many examples. You can use them as a starting-point to write your own stuff.
Besides the regular GDShader stuff, Pigment has so-called directives. Those allow to further control the behaviour of the application. **The most important directive is `//!load` to load an image.**
### Load TEXTURE using the `//!load` directive
```glsl
//!load <filepath>
```
The main image file will be read and available as the sampler2D `TEXTURE`.
#### Load additional images
```glsl
//!load+ <name> <filepath>
uniform sampler2D <name>;
```
Have a look at the `place_texture.gdshader` example.
### Have multiple steps with `//!steps n`
You can apply your shaderfile multiple times. At every additional step, `TEXTURE` is the result of the previous step. This can be used to chain effects that cannot be easily chained otherwise.
To query the current step index, a `STEP` uniform is automatically injected. If `steps` is set to `0`, your shader won't be applied at all.
Example:
```glsl
//!load ...
//!steps 5
uniform int STEP;
uniform int STEPS;
void fragment() {
if (STEP == 0) {
...
} else if (STEP == 1) {
...
} else if (STEP == STEPS-1) {
...
}
}
```
## Shaderlib
This repo comes with a (still small) shader library including pre-written functions and more.
Have a look at the `shaderlib` folder.
Here is an example:
```glsl
shader_type canvas_item;
#include "./shaderlib/oklab.gdshaderinc"
//!load ./images/swamp.jpg
void fragment() {
vec4 oklab = rgb2oklab(COLOR);
vec4 oklch = oklab2oklch(oklab);
oklch.z -= 2.0;
COLOR = oklab2rgb(oklch2oklab(oklch));
}
```
## Commandline interface
You can run Pigment from the commandline or scripts.
> Note: Headless mode is not supported. Using the commandline interface still opens a window.
### Usage
```
~ Pigment CLI ~
-================-
Usage:
./Pigment <command> <args...>
Commands:
help
| Shows this help text.
apply --shader PATH --input PATH --output PATH
| Applies a shader file.
--shader PATH The path to the shader
--input PATH The path to the input image.
Passing a folder activates batch mode.
--output PATH Where to write the resulting image to.
In batch mode, this must be a folder.
```
### Batch Mode
Since version v8.0, you can pass a directory to `--input` and `--output`. This will process all images in the input directory and write the output to the output directory.
> Note: You *can* use this feature for video frames, but it will take a loooong time.
#### Examples
```
./Pigment apply --shader ./examples/oklab.gdshader --input ~/Pictures/test.png --output ./output.png
```
## Known Issues
- screen scaling is unsupported; Using screen scaling could lead to an either blurry UI, or no scaling at all -> see #45
- commandline interface: `--headless` is not supported

View file

@ -0,0 +1,21 @@
MAINTAINER ChaoticByte
# Using Ubuntu 20.04
FROM docker.io/ubuntu:focal AS os-base
# https://docs.godotengine.org/en/stable/contributing/development/compiling/compiling_for_linuxbsd.html
RUN apt-get update
RUN DEBIAN_FRONTEND=noninteractive apt-get install -yq python3-pip git build-essential pkg-config libx11-dev libxcursor-dev libxinerama-dev libgl1-mesa-dev libglu1-mesa-dev libasound2-dev libpulse-dev libudev-dev libxi-dev libxrandr-dev libwayland-dev
RUN pip3 install --system scons
FROM os-base AS clone-src
RUN git clone https://github.com/godotengine/godot.git -b 4.5-stable /godot-src
FROM clone-src
WORKDIR /godot-src
ENTRYPOINT scons platform=linuxbsd target=template_release lto=full optimize=size disable_3d=yes module_text_server_adv_enabled=no module_text_server_fb_enabled=yes module_basis_universal_enabled=no module_csg_enabled=no module_enet_enabled=no module_gridmap_enabled=no module_jsonrpc_enabled=no module_mbedtls_enabled=no module_meshoptimizer_enabled=no module_minimp3_enabled=no module_mobile_vr_enabled=no module_msdfgen_enabled=no module_multiplayer_enabled=no module_navigation_enabled=no module_ogg_enabled=no module_openxr_enabled=no module_raycast_enabled=no module_squish_enabled=no module_theora_enabled=no module_upnp_enabled=no module_vhacd_enabled=no module_vorbis_enabled=no module_webrtc_enabled=no module_websocket_enabled=no module_webxr_enabled=no arch=x86_64 && strip bin/godot.linuxbsd.template_release.x86_64

30
build-template/build.sh Executable file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -e
function log {
echo -e "\033[1;36m***** $@\033[0m"
}
log
log "Pigment - Godot Build Template Builder"
log
cd $(dirname $0)
log Switched to $(pwd)
tmpsuffix=$(date +%s%N)
image_name=pigment-godot-template-builder
container_name=${image_name}-${tmpsuffix}
output_file=godot.linuxbsd.template_release.x86_64
log Building image ${image_name} ...
buildah build -t ${image_name}
log Building godot build template with container ${container_name} ...
podman run --name ${container_name} localhost/${image_name}:latest
log Copying ${output_file} from container to $(realpath ./${output_file})
podman cp ${container_name}:/godot-src/bin/${output_file} ./${output_file}
log Removing container ${container_name}
podman container rm ${container_name}
log Done :D

30
dist.sh Executable file
View file

@ -0,0 +1,30 @@
set -e
function log {
echo -e "\033[1;36m***** $@ *****\033[0m"
}
mkdir -p dist
log Building application
VERSION="$(godot --headless --no-header -s tools/get_version.gd)"
godot --headless --export-release "Linux/X11" "dist/Pigment-${VERSION}.x86_64"
log Packing shaderlib
ZIP_PATH_SHADERLIB=$(realpath "dist/Pigment-${VERSION}_shaderlib.zip")
zip -r "${ZIP_PATH_SHADERLIB}" shaderlib/
log Packing project template
ZIP_PATH_PROJECT_TEMPLATE=$(realpath "dist/Pigment-${VERSION}_project_template.zip")
rm -f "${ZIP_PATH_PROJECT_TEMPLATE}"
(
cd examples/
mv project.godot_ project.godot && trap "mv project.godot project.godot_" EXIT
zip -r "${ZIP_PATH_PROJECT_TEMPLATE}" *
)

3
examples/0_empty.tscn Normal file
View file

@ -0,0 +1,3 @@
[gd_scene format=3 uid="uid://db2rhq8rwv5wo"]
[node name="Node" type="Node"]

View file

@ -0,0 +1,8 @@
shader_type canvas_item;
void fragment() {
float v = (COLOR.r + COLOR.g + COLOR.b) / 3.0;
COLOR.r = v;
COLOR.g = v;
COLOR.b = v;
}

View file

@ -0,0 +1 @@
uid://dvarqolt6es27

10
examples/hsv.gdshader Normal file
View file

@ -0,0 +1,10 @@
shader_type canvas_item;
#include "./shaderlib/hsv.gdshaderinc"
void fragment() {
vec4 hsv = rgb2hsv(COLOR);
hsv.xyz += vec3(0.65, .42-(hsv.y*.3), -.125);
hsv.xyz *= vec3(1.0, 1.0, 1.25);
COLOR = hsv2rgb(hsv);
}

View file

@ -0,0 +1 @@
uid://gd23hu7ro148

View file

@ -0,0 +1,5 @@
# Example Images
- swamp.jpg by [clfr21 on Pixabay](https://pixabay.com/de/users/clfr21-6530007/)
- mountain.jpg by [Phghvvcftyyufj on Pixabay](https://pixabay.com/users/phghvvcftyyufj-12646982)

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ben72llmopgaj"
path="res://.godot/imported/mountain.jpg-c1b7de1e6557b826bc6f9324027e11af.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://examples/images/mountain.jpg"
dest_files=["res://.godot/imported/mountain.jpg-c1b7de1e6557b826bc6f9324027e11af.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
examples/images/swamp.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ckjb0agn5btv7"
path="res://.godot/imported/swamp.jpg-1dfdcd52a5ef03d42a82a7f06acefa98.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://examples/images/swamp.jpg"
dest_files=["res://.godot/imported/swamp.jpg-1dfdcd52a5ef03d42a82a7f06acefa98.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -0,0 +1,9 @@
shader_type canvas_item;
const float threshold = 0.6;
void fragment() {
vec4 tex = texture(TEXTURE , UV);
COLOR.rgb = min(tex.rgb, vec3(threshold));
COLOR.a = tex.a;
}

View file

@ -0,0 +1 @@
uid://dn02xsjm1kok8

10
examples/oklab.gdshader Normal file
View file

@ -0,0 +1,10 @@
shader_type canvas_item;
#include "./shaderlib/oklab.gdshaderinc"
void fragment() {
vec4 oklab = rgb2oklab(COLOR);
vec4 oklch = oklab2oklch(oklab);
oklch.z -= 2.0;
COLOR = oklab2rgb(oklch2oklab(oklch));
}

View file

@ -0,0 +1 @@
uid://cu37y8lc0x83

15
examples/project.godot_ Normal file
View file

@ -0,0 +1,15 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="Pigment Project"
run/main_scene="res://0_empty.tscn"
config/features=PackedStringArray("4.4", "Forward Plus")

1
examples/shaderlib Symbolic link
View file

@ -0,0 +1 @@
../shaderlib

45
export_presets.cfg Normal file
View file

@ -0,0 +1,45 @@
[preset.0]
name="Linux/X11"
platform="Linux"
runnable=true
advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter="screenshot.png, examples/*, shaderlib/*, tools/*, build-template/*"
export_path="dist/Pigment.x86_64"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.0.options]
custom_template/debug=""
custom_template/release="./build-template/godot.linuxbsd.template_release.x86_64"
debug/export_console_wrapper=1
binary_format/embed_pck=true
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
binary_format/architecture="x86_64"
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
ssh_remote_deploy/extra_args_ssh=""
ssh_remote_deploy/extra_args_scp=""
ssh_remote_deploy/run_script="#!/usr/bin/env bash
export DISPLAY=:0
unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
\"{temp_dir}/{exe_name}\" {cmd_args}"
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")
rm -rf \"{temp_dir}\""
texture_format/bptc=true
texture_format/s3tc=true
texture_format/etc=false
texture_format/etc2=false

72
project.godot Normal file
View file

@ -0,0 +1,72 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="Pigment"
config/version="v1.0"
run/main_scene="res://src/scenes/main.tscn"
config/features=PackedStringArray("4.5", "Mobile")
config/icon="res://src/assets/icon.png"
[autoload]
Filesystem="*res://src/Filesystem.gd"
[display]
window/size/viewport_width=640
window/size/viewport_height=672
window/energy_saving/keep_screen_on=false
window/subwindows/embed_subwindows=false
[editor_plugins]
enabled=PackedStringArray()
[input]
zoom_out={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":16,"position":Vector2(244, 15),"global_position":Vector2(248, 56),"factor":1.0,"button_index":5,"canceled":false,"pressed":true,"double_click":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":45,"physical_keycode":0,"key_label":0,"unicode":45,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194435,"key_label":0,"unicode":45,"location":0,"echo":false,"script":null)
]
}
zoom_in={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":8,"position":Vector2(270, 19),"global_position":Vector2(274, 60),"factor":1.0,"button_index":4,"canceled":false,"pressed":true,"double_click":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":43,"physical_keycode":0,"key_label":0,"unicode":43,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194437,"key_label":0,"unicode":43,"location":0,"echo":false,"script":null)
]
}
drag={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(183, 23),"global_position":Vector2(187, 64),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
]
}
apply_shader={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194336,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
save_shader={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":83,"physical_keycode":0,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
]
}
[rendering]
renderer/rendering_method="mobile"
textures/vram_compression/import_etc2_astc=true
textures/lossless_compression/force_png=true
shader_compiler/shader_cache/enabled=false
environment/defaults/default_clear_color=Color(0.501961, 0.501961, 0.501961, 1)

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

View file

@ -0,0 +1,5 @@
// inefficient cuberoot function
float cbrt(float x) {
return pow(x, 1.0/3.0);
}

View file

@ -0,0 +1 @@
uid://764b6ekchgb8

27
shaderlib/hsv.gdshaderinc Normal file
View file

@ -0,0 +1,27 @@
/*
rgb2hsv and hsv2rgb functions adapted
from https://godotshaders.com/shader/hsv-adjustment/
original code by https://godotshaders.com/author/al1-ce/
Color space conversion functions always work with vec4.
The fourth value is always alpha.
*/
// Convert RGB to HSV (hue, saturation, brightness)
vec4 rgb2hsv(vec4 c) {
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec4(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x, c.a);
}
// Convert HSV back to RGB (red, green, blue)
vec4 hsv2rgb(vec4 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
vec3 rgb = c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
return vec4(rgb.r, rgb.g, rgb.b, c.a);
}

View file

@ -0,0 +1 @@
uid://bbr3tq6mp5qa2

View file

@ -0,0 +1,70 @@
/*
OkLab and OkLCh
For more details on oklab, see
- https://bottosson.github.io/posts/oklab/
- https://en.wikipedia.org/wiki/Oklab_color_space
Color space conversion functions always work with vec4.
The fourth value is always alpha.
*/
#include "./common.gdshaderinc"
vec4 rgb2oklab(vec4 c) {
// oklab.x and .y (a and b) should range from -0.5 to 0.5
float l = 0.4122214708f * c.r + 0.5363325363f * c.g + 0.0514459929f * c.b;
float m = 0.2119034982f * c.r + 0.6806995451f * c.g + 0.1073969566f * c.b;
float s = 0.0883024619f * c.r + 0.2817188376f * c.g + 0.6299787005f * c.b;
float l_ = cbrt(l);
float m_ = cbrt(m);
float s_ = cbrt(s);
return vec4(
0.2104542553f*l_ + 0.7936177850f*m_ - 0.0040720468f*s_,
1.9779984951f*l_ - 2.4285922050f*m_ + 0.4505937099f*s_,
0.0259040371f*l_ + 0.7827717662f*m_ - 0.8086757660f*s_,
c.a
);
}
vec4 oklab2rgb(vec4 c) {
// oklab.x and .y (a and b) should range from -0.5 to 0.5
float l_ = c.x + 0.3963377774f * c.y + 0.2158037573f * c.z;
float m_ = c.x - 0.1055613458f * c.y - 0.0638541728f * c.z;
float s_ = c.x - 0.0894841775f * c.y - 1.2914855480f * c.z;
float l = l_*l_*l_;
float m = m_*m_*m_;
float s = s_*s_*s_;
return vec4(
+4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s,
-1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s,
-0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s,
c.a
);
}
vec4 oklab2oklch(vec4 c) {
// oklch.z (hue) ranges from -3.6 to 3.6
return vec4(
c.x,
sqrt((c.y * c.y) + (c.z * c.z)),
atan(c.z, c.y),
c.a
);
}
vec4 oklch2oklab(vec4 c) {
// oklch.z (hue) ranges from -3.6 to 3.6
return vec4(
c.x,
c.y * cos(c.z),
c.y * sin(c.z),
c.a
);
}

View file

@ -0,0 +1 @@
uid://ckw4nfslk4m6l

50
src/Camera.gd Normal file
View file

@ -0,0 +1,50 @@
extends Camera2D
@onready var image_viewport_display = %ImageViewportDisplay
var drag = false
func _input(event):
if event.is_action_pressed("zoom_out"):
zoom_out()
elif event.is_action_pressed("zoom_in"):
zoom_in()
if event.is_action_pressed("drag"):
self.drag = true
elif event.is_action_released("drag"):
self.drag = false
if self.drag && event is InputEventMouseMotion:
self.global_position -= event.relative / self.zoom
var old_zoom = self.zoom
func _process(_delta: float) -> void:
if self.zoom != old_zoom:
image_viewport_display.update_zoom_texture_filter(self.zoom)
image_viewport_display.material.set_shader_parameter("zoom_level", self.zoom)
old_zoom = self.zoom
func fit_image():
if Filesystem.original_image != null:
var image_size = Filesystem.original_image.get_size()
var viewport_size = get_viewport_rect().size
var zoomf = 1.0
if viewport_size.x / image_size.x * image_size.y > viewport_size.y:
zoomf = viewport_size.y / image_size.y / 1.25
else:
zoomf = viewport_size.x / image_size.x / 1.2
self.zoom = Vector2(zoomf, zoomf)
self.global_position = Vector2(0, 0)
func zoom_in():
var old_mouse_pos = get_global_mouse_position()
self.zoom *= 1.2
self.global_position += old_mouse_pos - get_global_mouse_position()
func zoom_out():
var old_mouse_pos = get_global_mouse_position()
self.zoom *= 1/1.2
self.global_position += old_mouse_pos - get_global_mouse_position()
func _on_fit_image_button_pressed():
fit_image()

1
src/Camera.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://b6r8rigubdctk

52
src/Filesystem.gd Normal file
View file

@ -0,0 +1,52 @@
extends Node
var cwd = "."
var shader_path = "":
get():
return shader_path
set(v):
shader_path = v
if "/" in v: # update current working directory
cwd = v.substr(0, v.rfind("/"))
var shader: Shader:
get():
if shader_path == "":
return null
print("Load ", shader_path)
return load(shader_path)
var image_path = ""
var original_image: ImageTexture
var result: Image
var last_image_savepath = ""
var last_original_image_path = ""
func get_absolute_path(p: String) -> String:
# this only works on Linux!
if !p.begins_with("/"):
return self.cwd + "/" + p.lstrip("./")
return p
func load_image() -> String: # returns an error message
print("Load ", image_path)
var img = Image.new()
var err = img.load(image_path)
if err == OK:
original_image = ImageTexture.create_from_image(img)
if self.last_image_savepath == "" or image_path != self.last_original_image_path:
self.last_image_savepath = image_path
self.last_original_image_path = image_path
return ""
return error_string(err) + " " + image_path
func save_result(path: String):
print("Export ", path)
var err = self.result.save_png(path)
if err != OK:
print("An error occured!")
else:
self.last_image_savepath = path

1
src/Filesystem.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://rlb041ygdwol

90
src/ImageCompositor.gd Normal file
View file

@ -0,0 +1,90 @@
class_name ImageCompositor extends SubViewport
var image_sprite: Sprite2D
func _init() -> void:
# Overwrite some variables
self.render_target_update_mode = SubViewport.UPDATE_ALWAYS
self.disable_3d = true
self.transparent_bg = true
self.canvas_item_default_texture_filter = Viewport.DEFAULT_CANVAS_ITEM_TEXTURE_FILTER_NEAREST
self.image_sprite = Sprite2D.new()
@onready var camera = %Camera
@onready var image_viewport_display = %ImageViewportDisplay
func _ready() -> void:
# Add image sprite as child to be rendered
self.add_child(image_sprite)
var _fragment_function_regex: RegEx = RegEx.create_from_string(r'\s*void\s+fragment\s*\(\s*\)\s*{\s*')
func validate_shader_compilation(shader: Shader) -> bool:
# Inject code to validate shader compilation
var shader_code = shader.code;
# -> get position of fragment shader
var fragment_function_match = _fragment_function_regex.search(shader.code)
if fragment_function_match == null:
return false
# -> inject uniform
var uniform_name = "shader_compilation_validate_" + str(randi_range(999999999, 100000000))
var uniform_code_line = "\nuniform bool " + uniform_name + ";\n"
shader_code = shader_code.insert(fragment_function_match.get_start(), uniform_code_line)
# -> inject variable access to prevent that the uniform gets optimized away
shader_code = shader_code.insert(fragment_function_match.get_end() + len(uniform_code_line), "\n" + uniform_name + ";\n")
# apply shader code
shader.code = shader_code
# test if uniform list is empty -> if it is empty, the shader compilation failed
return len(shader.get_shader_uniform_list()) > 0
func shader_has_uniform(shader: Shader, var_name: String, type: int) -> bool:
for u in shader.get_shader_uniform_list():
if u["name"] == var_name && u["type"] == type:
return true
return false
func set_vsync(enabled: bool):
if enabled:
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)
else:
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
func update() -> Array: # returns error messages (strings)
if Filesystem.image_path == "":
return ["No image loaded!"]
var shader = Filesystem.shader # read from disk
if shader == null:
return ["No shader opened!"]
# validate shader
if not validate_shader_compilation(shader):
return ["Shader compilation failed!"]
var errors = []
# load texture
var fit_image = Filesystem.image_path != Filesystem.last_original_image_path
var err = Filesystem.load_image()
if err != "":
errors.append(err)
image_viewport_display.hide()
return errors
# apply texture
image_sprite.texture = Filesystem.original_image
image_sprite.offset = Filesystem.original_image.get_size() / 2
self.size = Filesystem.original_image.get_size()
# show the image viewport & fit the image
if fit_image: camera.fit_image()
image_viewport_display.show()
# create shader material
var mat = ShaderMaterial.new()
mat.shader = shader
# assign material
image_sprite.material = mat
# iterate n times
set_vsync(false) # speed up processing
# Get viewport texture
await RenderingServer.frame_post_draw # wait for next frame to get drawn
Filesystem.result = get_texture().get_image()
image_sprite.texture = ImageTexture.create_from_image(Filesystem.result)
set_vsync(true) # reenable vsync
image_sprite.material = null
# done
return errors

View file

@ -0,0 +1 @@
uid://d106170kuigl3

View file

@ -0,0 +1,10 @@
extends Sprite2D
func _ready() -> void:
hide()
func update_zoom_texture_filter(zoom: Vector2):
if zoom.x >= 1.5:
texture_filter = TEXTURE_FILTER_NEAREST_WITH_MIPMAPS
else:
texture_filter = TEXTURE_FILTER_LINEAR

View file

@ -0,0 +1 @@
uid://ctc4lhbdsoq7u

121
src/Main.gd Normal file
View file

@ -0,0 +1,121 @@
extends Node
const BATCH_MODE_SUPPORTED_EXTS = [
".bmp", ".dds", ".exr", ".hdr", ".jpeg", ".jpg", ".ktx", ".png", ".svg", ".webp"
]
@onready var app_name = ProjectSettings.get_setting("application/config/name")
func show_help():
print(
"Usage:\n\n",
"./Pigment <command> <args...>\n\n",
"Commands:\n\n",
" help\n\n",
" | Shows this help text.\n\n",
" apply --shader PATH [--load-image PATH]\n\n",
" | Applies a shader file.\n\n",
" --shader PATH The path to the shader\n",
" --input PATH The path to the image.\n",
" Passing a folder activates batch mode.\n",
" --output PATH Where to write the resulting image to.\n",
" In batch mode, this must be a folder.\n")
func parse_custom_cmdline(args: PackedStringArray):
var kwargs: Dictionary = {"--shader": null, "--output": null, "--input": null}
var args_len = args.size()
var i = 0
while i < args_len:
var a = args[i]
if a in kwargs && args_len > i+1:
i += 1
kwargs[a] = args[i]
i += 1
return kwargs
func cli_handle_errors(errors: Array) -> int:
# returns number of errors
var n_errors = errors.size()
if n_errors > 0:
print("One or more errors occurred.")
for e in errors:
printerr(e)
return n_errors
func cli(args: PackedStringArray):
print(
"~ Pigment CLI ~\n",
"-=============-\n")
if "help" in args:
show_help()
get_tree().quit(1)
return
var kwargs: Dictionary = parse_custom_cmdline(args)
if kwargs["--shader"] == null or kwargs["--output"] == null:
show_help()
get_tree().quit(1)
return
var batch_mode = false
var load_image_dir: DirAccess
if kwargs["--input"] != null:
load_image_dir = DirAccess.open(kwargs["--input"])
if load_image_dir != null:
# batch mode
if DirAccess.open(kwargs["--output"]) == null:
printerr("If --input is a directory, --output has to be one too.\n")
show_help()
get_tree().quit(1)
return
else:
batch_mode = true
#
Filesystem.shader_path = kwargs["--shader"]
#
if batch_mode:
var in_dir_path = load_image_dir.get_current_dir()
var out_dir_path: String = kwargs["--output"].rstrip("/")
for f in load_image_dir.get_files():
var supported = false
for e in BATCH_MODE_SUPPORTED_EXTS:
if f.ends_with(e):
supported = true
break
if supported:
f = in_dir_path + "/" + f
print(f)
var errors = await $Compositor.update(f)
if cli_handle_errors(errors) == 0:
var filename = out_dir_path + "/" + f.substr(f.rfind("/"), -1)
Filesystem.save_result(filename)
else:
get_tree().quit(1)
return
get_tree().quit(0)
else:
var errors = []
if kwargs["--input"] == null:
errors = await $Compositor.update()
else:
errors = await $Compositor.update(kwargs["--input"])
if cli_handle_errors(errors) == 0:
Filesystem.save_result(kwargs["--output"])
get_tree().quit(0)
else:
get_tree().quit(1)
func prepare_gui():
update_title()
func _ready():
var args = OS.get_cmdline_args()
if len(args) > 0 and args[0] in ["apply", "help"]:
# use the commandline interface
cli(args)
else:
prepare_gui()
func update_title(current_file: String = ""):
if current_file == "":
get_window().title = app_name + " - Viewer"
else:
get_window().title = current_file + " - " + app_name + " - Viewer"

1
src/Main.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://5sbslwysin5a

120
src/MainUI.gd Normal file
View file

@ -0,0 +1,120 @@
extends Control
@onready var open_shader_dialog = %OpenShaderDialog
@onready var open_image_dialog = %OpenImageDialog
@onready var save_image_dialog = %SaveImageDialog
@onready var open_shader_button = %OpenShaderButton
@onready var open_image_button = %OpenImageButton
@onready var save_image_button = %SaveImageButton
@onready var fit_image_button = %FitImageButton
@onready var apply_shader_button = %ApplyShaderButton
@onready var status_indicator = %StatusIndicator
@onready var error_msg_dialog = %ErrorMessageDialog
@onready var main = get_tree().root.get_node("Main")
@onready var compositor = %Compositor
@onready var camera = %Camera
var status_okay_texture: CompressedTexture2D = preload("uid://m1omb6g45vst")
var status_error_texture: CompressedTexture2D = preload("uid://04iv1gogpuhu")
enum Status {OKAY, ERROR, UNKNOWN = -1}
#
func _input(event):
if event.is_action_pressed("apply_shader"):
_on_apply_shader_button_pressed()
elif event.is_action_pressed("save_shader"):
accept_event() # Event is now handled.
#
func set_buttons_disabled(disabled: bool):
for b in [open_shader_button, open_image_button, save_image_button, fit_image_button, apply_shader_button, status_indicator]:
b.disabled = disabled
#
func _on_open_shader_button_pressed():
set_buttons_disabled(true)
open_shader_dialog.show()
func _on_open_image_button_pressed():
set_buttons_disabled(true)
open_image_dialog.show()
func _on_fit_image_button_pressed():
camera.fit_image()
func _on_apply_shader_button_pressed():
set_buttons_disabled(true)
var errors = await compositor.update()
set_buttons_disabled(false)
if len(errors) > 0:
update_status(Status.ERROR, "\n".join(errors))
else:
update_status(Status.OKAY)
status_indicator.disabled = true
func _on_save_image_button_pressed():
if Filesystem.result != null:
set_buttons_disabled(true)
save_image_dialog.current_path = Filesystem.last_image_savepath
save_image_dialog.show()
#
func _on_open_shader_dialog_file_selected(path: String):
Filesystem.shader_path = path
main.update_title(path.split("/")[-1])
self._on_apply_shader_button_pressed()
func _on_open_shader_dialog_canceled() -> void:
set_buttons_disabled(false)
func _on_open_shader_dialog_confirmed() -> void:
set_buttons_disabled(false)
func _on_open_image_dialog_file_selected(path: String) -> void:
Filesystem.image_path = path
self._on_apply_shader_button_pressed()
func _on_open_image_dialog_canceled() -> void:
set_buttons_disabled(false)
func _on_open_image_dialog_confirmed() -> void:
set_buttons_disabled(false)
func _on_save_image_dialog_file_selected(path):
Filesystem.save_result(path)
set_buttons_disabled(false)
func _on_save_image_dialog_canceled() -> void:
set_buttons_disabled(false)
func _on_save_image_dialog_confirmed() -> void:
set_buttons_disabled(false)
#
func update_status(status: Status, msg: String = ""):
error_msg_dialog.dialog_text = msg
error_msg_dialog.reset_size()
if status == Status.OKAY:
status_indicator.texture_normal = status_okay_texture
elif status == Status.ERROR:
status_indicator.texture_normal = status_error_texture
else:
status_indicator.texture_normal = null
if msg == "":
status_indicator.disabled = true
else:
status_indicator.disabled = false
func _on_status_indicator_pressed() -> void:
error_msg_dialog.show()

1
src/MainUI.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://bxgmf2ny7yuc8

8
src/VersionLabel.gd Normal file
View file

@ -0,0 +1,8 @@
extends Label
func _ready():
text = ProjectSettings.get_setting("application/config/name") \
+ " " \
+ ProjectSettings.get_setting("application/config/version") \
+ " | Godot " \
+ Engine.get_version_info()["string"]

1
src/VersionLabel.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://bh0gpu3i2p47f

BIN
src/assets/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

40
src/assets/bg.png.import Normal file
View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d2nwchyd6huob"
path="res://.godot/imported/bg.png-7c8713dd1fab321784216191fa747e53.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://src/assets/bg.png"
dest_files=["res://.godot/imported/bg.png-7c8713dd1fab321784216191fa747e53.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

80
src/assets/error.svg Normal file
View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30"
height="30"
viewBox="0 0 30 30"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="error.svg"
inkscape:export-filename="error.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="22.627417"
inkscape:cx="12.28598"
inkscape:cy="16.108776"
inkscape:window-width="1854"
inkscape:window-height="1011"
inkscape:window-x="66"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid9"
originx="0"
originy="0" />
</sodipodi:namedview>
<defs
id="defs2">
<rect
x="-19.109422"
y="15.119667"
width="23.697131"
height="28.949477"
id="rect4519" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="display:none;opacity:0.75;fill:#ff3815;fill-opacity:1;stroke:#ff3815;stroke-width:6;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 4,25 H 26 L 15,6 Z"
id="path11345"
sodipodi:nodetypes="cccc" />
<circle
style="opacity:0.75;fill:#ff3815;fill-opacity:1;stroke:none;stroke-width:6;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="path15121"
cx="15"
cy="15"
r="13" />
<g
aria-label="!"
transform="matrix(0.80238423,0,0,0.80238423,20.811527,-5.9033718)"
id="text4517"
style="font-size:24px;white-space:pre;shape-inside:url(#rect4519);display:inline;fill:#ffffff;fill-opacity:1">
<path
d="m -5.6133052,29.164717 h -3.264 L -9.5630195,16.081288 h 4.6354286 z m -4.0045715,4.580571 q 0,-1.261715 0.6857143,-1.755429 0.6857143,-0.521142 1.6731429,-0.521142 0.96,0 1.6457143,0.521142 0.6857143,0.493714 0.6857143,1.755429 0,1.206857 -0.6857143,1.755429 -0.6857143,0.521142 -1.6457143,0.521142 -0.9874286,0 -1.6731429,-0.521142 -0.6857143,-0.548572 -0.6857143,-1.755429 z"
style="font-weight:bold;-inkscape-font-specification:'sans-serif Bold';text-align:center;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:1.14286"
id="path4674" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://04iv1gogpuhu"
path="res://.godot/imported/error.svg-28fb29635cf59d39cabf7052619f602f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://src/assets/error.svg"
dest_files=["res://.godot/imported/error.svg-28fb29635cf59d39cabf7052619f602f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=2.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

BIN
src/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://kqwc4avs2xdp"
path="res://.godot/imported/icon.png-d8298ab6eda392a806be6bb7eec65b9c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://src/assets/icon.png"
dest_files=["res://.godot/imported/icon.png-d8298ab6eda392a806be6bb7eec65b9c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

81
src/assets/okay.svg Normal file
View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30"
height="30"
viewBox="0 0 30 30"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="success.svg"
inkscape:export-filename="error.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="22.627417"
inkscape:cx="3.314563"
inkscape:cy="14.385204"
inkscape:window-width="1854"
inkscape:window-height="1011"
inkscape:window-x="66"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid9"
originx="0"
originy="0" />
</sodipodi:namedview>
<defs
id="defs2">
<rect
x="0"
y="0"
width="30"
height="30"
id="rect15390" />
<rect
x="-19.109422"
y="15.119667"
width="23.697131"
height="28.949477"
id="rect4519" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="display:none;opacity:0.75;fill:#ff3815;fill-opacity:1;stroke:#ff3815;stroke-width:6;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 4,25 H 26 L 15,6 Z"
id="path11345"
sodipodi:nodetypes="cccc" />
<circle
style="opacity:0.75;fill:#15ff1e;fill-opacity:1;stroke:none;stroke-width:6;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="path15121"
cx="15"
cy="15"
r="13" />
<path
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 8.1284306,14.403599 5.2374004,4.840883 8.39167,-9.6564053"
id="path20589"
sodipodi:nodetypes="ccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://m1omb6g45vst"
path="res://.godot/imported/okay.svg-de66a022ef37753b085371b7c60aefd1.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://src/assets/okay.svg"
dest_files=["res://.godot/imported/okay.svg-de66a022ef37753b085371b7c60aefd1.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=2.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

219
src/scenes/main.tscn Normal file
View file

@ -0,0 +1,219 @@
[gd_scene load_steps=13 format=3 uid="uid://bjah7k4bxo044"]
[ext_resource type="Script" uid="uid://5sbslwysin5a" path="res://src/Main.gd" id="1_64y3g"]
[ext_resource type="Script" uid="uid://d106170kuigl3" path="res://src/ImageCompositor.gd" id="2_4ykh7"]
[ext_resource type="Shader" uid="uid://ctk7jomfyx0fh" path="res://src/shader/ivd_outline.gdshader" id="3_0fllm"]
[ext_resource type="Script" uid="uid://ctc4lhbdsoq7u" path="res://src/ImageViewportDisplay.gd" id="4_pbpx2"]
[ext_resource type="Script" uid="uid://b6r8rigubdctk" path="res://src/Camera.gd" id="5_hkdq6"]
[ext_resource type="Texture2D" uid="uid://d2nwchyd6huob" path="res://src/assets/bg.png" id="6_kokaf"]
[ext_resource type="Theme" uid="uid://cwqlns34rj3vx" path="res://src/theme.tres" id="6_rjp5f"]
[ext_resource type="Script" uid="uid://bxgmf2ny7yuc8" path="res://src/MainUI.gd" id="7_5puhk"]
[ext_resource type="Script" uid="uid://bh0gpu3i2p47f" path="res://src/VersionLabel.gd" id="8_kod8x"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_y2ea0"]
shader = ExtResource("3_0fllm")
shader_parameter/zoom_level = Vector2(1, 1)
[sub_resource type="ViewportTexture" id="ViewportTexture_lct1c"]
viewport_path = NodePath("Compositor")
[sub_resource type="LabelSettings" id="LabelSettings_6o860"]
font_size = 12
shadow_color = Color(0, 0, 0, 1)
[node name="Main" type="Node2D"]
script = ExtResource("1_64y3g")
[node name="Compositor" type="SubViewport" parent="."]
unique_name_in_owner = true
script = ExtResource("2_4ykh7")
[node name="ImageViewportDisplay" type="Sprite2D" parent="."]
unique_name_in_owner = true
material = SubResource("ShaderMaterial_y2ea0")
texture = SubResource("ViewportTexture_lct1c")
script = ExtResource("4_pbpx2")
[node name="Camera" type="Camera2D" parent="."]
unique_name_in_owner = true
offset = Vector2(0, -64)
script = ExtResource("5_hkdq6")
[node name="CanvasLayerBg" type="CanvasLayer" parent="."]
layer = -1
[node name="Control" type="Control" parent="CanvasLayerBg"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="TextureRect" type="TextureRect" parent="CanvasLayerBg/Control"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("6_kokaf")
stretch_mode = 1
[node name="CanvasLayer" type="CanvasLayer" parent="."]
[node name="MainUI" type="Control" parent="CanvasLayer"]
unique_name_in_owner = true
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
pivot_offset = Vector2(320, 320)
theme = ExtResource("6_rjp5f")
script = ExtResource("7_5puhk")
[node name="OpenShaderDialog" type="FileDialog" parent="CanvasLayer/MainUI"]
unique_name_in_owner = true
auto_translate_mode = 1
title = "Load Shader"
size = Vector2i(521, 175)
ok_button_text = "Open"
mode_overrides_title = false
file_mode = 0
access = 2
filters = PackedStringArray("*.gdshader")
use_native_dialog = true
[node name="OpenImageDialog" type="FileDialog" parent="CanvasLayer/MainUI"]
unique_name_in_owner = true
auto_translate_mode = 1
title = "Load Image"
size = Vector2i(521, 175)
ok_button_text = "Open"
mode_overrides_title = false
file_mode = 0
access = 2
filters = PackedStringArray("*.png", "*.jpg", "*.jpeg")
use_native_dialog = true
[node name="SaveImageDialog" type="FileDialog" parent="CanvasLayer/MainUI"]
unique_name_in_owner = true
auto_translate_mode = 1
title = "Export Image"
size = Vector2i(661, 175)
mode_overrides_title = false
access = 2
filters = PackedStringArray("*.png")
use_native_dialog = true
[node name="ErrorMessageDialog" type="AcceptDialog" parent="CanvasLayer/MainUI"]
unique_name_in_owner = true
auto_translate_mode = 1
title = "Status"
initial_position = 2
size = Vector2i(256, 128)
popup_window = true
ok_button_text = "Close"
[node name="OpenShaderButton" type="Button" parent="CanvasLayer/MainUI"]
unique_name_in_owner = true
layout_mode = 1
offset_left = 16.0
offset_top = 16.0
offset_right = 128.0
offset_bottom = 48.0
text = "Open Shader"
[node name="OpenImageButton" type="Button" parent="CanvasLayer/MainUI"]
unique_name_in_owner = true
layout_mode = 1
offset_left = 16.0
offset_top = 56.0
offset_right = 128.0
offset_bottom = 88.0
text = "Open Image"
[node name="SaveImageButton" type="Button" parent="CanvasLayer/MainUI"]
unique_name_in_owner = true
layout_mode = 1
offset_left = 144.0
offset_top = 16.0
offset_right = 216.0
offset_bottom = 48.0
disabled = true
text = "Export"
[node name="FitImageButton" type="Button" parent="CanvasLayer/MainUI"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -176.0
offset_top = 16.0
offset_right = -128.0
offset_bottom = 48.0
grow_horizontal = 0
text = "Fit"
[node name="ApplyShaderButton" type="Button" parent="CanvasLayer/MainUI"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -112.0
offset_top = 16.0
offset_right = -16.0
offset_bottom = 48.0
grow_horizontal = 0
text = "Apply (F5)"
[node name="StatusIndicator" type="TextureButton" parent="CanvasLayer/MainUI"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -220.0
offset_top = 21.0
offset_right = -196.0
offset_bottom = 45.0
grow_horizontal = 0
disabled = true
ignore_texture_size = true
stretch_mode = 0
[node name="VersionLabel" type="Label" parent="CanvasLayer/MainUI"]
layout_mode = 1
anchors_preset = 12
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 16.0
offset_top = -24.0
offset_right = -16.0
grow_horizontal = 2
grow_vertical = 0
label_settings = SubResource("LabelSettings_6o860")
horizontal_alignment = 1
vertical_alignment = 1
script = ExtResource("8_kod8x")
[connection signal="canceled" from="CanvasLayer/MainUI/OpenShaderDialog" to="CanvasLayer/MainUI" method="_on_open_shader_dialog_canceled"]
[connection signal="confirmed" from="CanvasLayer/MainUI/OpenShaderDialog" to="CanvasLayer/MainUI" method="_on_open_shader_dialog_confirmed"]
[connection signal="file_selected" from="CanvasLayer/MainUI/OpenShaderDialog" to="CanvasLayer/MainUI" method="_on_open_shader_dialog_file_selected"]
[connection signal="canceled" from="CanvasLayer/MainUI/OpenImageDialog" to="CanvasLayer/MainUI" method="_on_open_image_dialog_canceled"]
[connection signal="confirmed" from="CanvasLayer/MainUI/OpenImageDialog" to="CanvasLayer/MainUI" method="_on_open_image_dialog_confirmed"]
[connection signal="file_selected" from="CanvasLayer/MainUI/OpenImageDialog" to="CanvasLayer/MainUI" method="_on_open_image_dialog_file_selected"]
[connection signal="canceled" from="CanvasLayer/MainUI/SaveImageDialog" to="CanvasLayer/MainUI" method="_on_save_image_dialog_canceled"]
[connection signal="confirmed" from="CanvasLayer/MainUI/SaveImageDialog" to="CanvasLayer/MainUI" method="_on_save_image_dialog_confirmed"]
[connection signal="file_selected" from="CanvasLayer/MainUI/SaveImageDialog" to="CanvasLayer/MainUI" method="_on_save_image_dialog_file_selected"]
[connection signal="pressed" from="CanvasLayer/MainUI/OpenShaderButton" to="CanvasLayer/MainUI" method="_on_open_shader_button_pressed"]
[connection signal="pressed" from="CanvasLayer/MainUI/OpenImageButton" to="CanvasLayer/MainUI" method="_on_open_image_button_pressed"]
[connection signal="pressed" from="CanvasLayer/MainUI/SaveImageButton" to="CanvasLayer/MainUI" method="_on_save_image_button_pressed"]
[connection signal="pressed" from="CanvasLayer/MainUI/FitImageButton" to="CanvasLayer/MainUI" method="_on_fit_image_button_pressed"]
[connection signal="pressed" from="CanvasLayer/MainUI/ApplyShaderButton" to="CanvasLayer/MainUI" method="_on_apply_shader_button_pressed"]
[connection signal="pressed" from="CanvasLayer/MainUI/StatusIndicator" to="CanvasLayer/MainUI" method="_on_status_indicator_pressed"]

View file

@ -0,0 +1,16 @@
shader_type canvas_item;
uniform vec2 zoom_level = vec2(1.0);
const float thickness = 3.0;
void fragment() {
vec2 t = thickness * TEXTURE_PIXEL_SIZE / zoom_level;
if (
UV.x < t.x ||
UV.y < t.y ||
UV.x > 1.0-t.x ||
UV.y > 1.0-t.y
) {
COLOR = mix(COLOR, vec4(0.5), 0.5);
}
}

View file

@ -0,0 +1 @@
uid://ctk7jomfyx0fh

57
src/theme.tres Normal file
View file

@ -0,0 +1,57 @@
[gd_resource type="Theme" load_steps=4 format=3 uid="uid://cwqlns34rj3vx"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bm5o2"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.1, 0.1, 0.1, 0.3)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(1, 1, 1, 0.27451)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_l0k8a"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.225, 0.225, 0.225, 0.6)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(1, 1, 1, 0.784314)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1dkyv"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.1, 0.1, 0.1, 0.6)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(1, 1, 1, 0.509804)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 5
[resource]
Button/styles/disabled = SubResource("StyleBoxFlat_bm5o2")
Button/styles/hover = SubResource("StyleBoxFlat_l0k8a")
Button/styles/normal = SubResource("StyleBoxFlat_1dkyv")

7
tools/get_version.gd Normal file
View file

@ -0,0 +1,7 @@
extends SceneTree
# godot --headless --no-header -s tools/get_version.gd
func _init() -> void:
print(ProjectSettings.get_setting("application/config/version"))
quit(0)

1
tools/get_version.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://cdhqbascy6pvy