incoming update

This commit is contained in:
Anthony Samms
2025-12-19 19:43:16 -05:00
parent 252c3c3e0b
commit 986ab1baaf
4 changed files with 352 additions and 52 deletions

153
dev/color2alpha.py Normal file
View File

@@ -0,0 +1,153 @@
from PIL import Image
import numpy as np
def gimp_color_to_alpha_exact(image_path, target_color=(0, 0, 0), output_path=None):
"""
Exact replication of GIMP's Color to Alpha algorithm
Args:
image_path: Path to input image
target_color: RGB tuple of color to remove (default: black)
output_path: Optional output path
GIMP Settings replicated:
- Transparency threshold: 0
- Opacity threshold: 1
- Mode: replace
- Opacity: 100%
"""
img = Image.open(image_path).convert("RGBA")
data = np.array(img, dtype=np.float64)
# Normalize to 0-1 range for calculations
data = data / 255.0
target = np.array(target_color, dtype=np.float64) / 255.0
height, width = data.shape[:2]
for y in range(height):
for x in range(width):
pixel = data[y, x]
r, g, b, a = pixel[0], pixel[1], pixel[2], pixel[3]
# GIMP's Color to Alpha algorithm
tr, tg, tb = target[0], target[1], target[2]
# Calculate the alpha based on how much of the target color is present
if tr == 0.0 and tg == 0.0 and tb == 0.0:
# Special case for pure black target
# Alpha is the maximum of the RGB components
new_alpha = max(r, g, b)
if new_alpha > 0:
# Remove the black component, scale remaining color
data[y, x, 0] = r / new_alpha if new_alpha > 0 else 0
data[y, x, 1] = g / new_alpha if new_alpha > 0 else 0
data[y, x, 2] = b / new_alpha if new_alpha > 0 else 0
else:
# Pure black becomes transparent
data[y, x, 0] = 0
data[y, x, 1] = 0
data[y, x, 2] = 0
new_alpha = 0
# Replace mode: completely replace the alpha
data[y, x, 3] = new_alpha * a
else:
# General case for non-black target colors
# Calculate alpha as minimum ratio needed to remove target color
alpha_r = (r - tr) / (1.0 - tr) if tr < 1.0 else 0
alpha_g = (g - tg) / (1.0 - tg) if tg < 1.0 else 0
alpha_b = (b - tb) / (1.0 - tb) if tb < 1.0 else 0
new_alpha = max(0, max(alpha_r, alpha_g, alpha_b))
if new_alpha > 0:
# Calculate new RGB values
data[y, x, 0] = (r - tr) / new_alpha + tr if new_alpha > 0 else tr
data[y, x, 1] = (g - tg) / new_alpha + tg if new_alpha > 0 else tg
data[y, x, 2] = (b - tb) / new_alpha + tb if new_alpha > 0 else tb
else:
data[y, x, 0] = tr
data[y, x, 1] = tg
data[y, x, 2] = tb
# Replace mode: completely replace the alpha
data[y, x, 3] = new_alpha * a
# Convert back to 0-255 range and uint8
data = np.clip(data * 255.0, 0, 255).astype(np.uint8)
result = Image.fromarray(data)
if output_path:
result.save(output_path)
return result
def gimp_color_to_alpha_vectorized(image_path, target_color=(0, 0, 0), output_path=None):
"""
Vectorized version of GIMP's Color to Alpha algorithm for better performance
"""
img = Image.open(image_path).convert("RGBA")
data = np.array(img, dtype=np.float64) / 255.0
target = np.array(target_color, dtype=np.float64) / 255.0
tr, tg, tb = target[0], target[1], target[2]
r, g, b, a = data[:,:,0], data[:,:,1], data[:,:,2], data[:,:,3]
if tr == 0.0 and tg == 0.0 and tb == 0.0:
# Special case for black target - vectorized
new_alpha = np.maximum(np.maximum(r, g), b)
# Avoid division by zero
safe_alpha = np.where(new_alpha > 0, new_alpha, 1)
# Scale RGB values
new_r = np.where(new_alpha > 0, r / safe_alpha, 0)
new_g = np.where(new_alpha > 0, g / safe_alpha, 0)
new_b = np.where(new_alpha > 0, b / safe_alpha, 0)
# Apply new values
data[:,:,0] = new_r
data[:,:,1] = new_g
data[:,:,2] = new_b
data[:,:,3] = new_alpha * a
else:
# General case for non-black colors - vectorized
alpha_r = np.where(tr < 1.0, (r - tr) / (1.0 - tr), 0)
alpha_g = np.where(tg < 1.0, (g - tg) / (1.0 - tg), 0)
alpha_b = np.where(tb < 1.0, (b - tb) / (1.0 - tb), 0)
new_alpha = np.maximum(0, np.maximum(np.maximum(alpha_r, alpha_g), alpha_b))
# Calculate new RGB
safe_alpha = np.where(new_alpha > 0, new_alpha, 1)
new_r = np.where(new_alpha > 0, (r - tr) / safe_alpha + tr, tr)
new_g = np.where(new_alpha > 0, (g - tg) / safe_alpha + tg, tg)
new_b = np.where(new_alpha > 0, (b - tb) / safe_alpha + tb, tb)
data[:,:,0] = new_r
data[:,:,1] = new_g
data[:,:,2] = new_b
data[:,:,3] = new_alpha * a
# Convert back to uint8
data = np.clip(data * 255.0, 0, 255).astype(np.uint8)
result = Image.fromarray(data)
if output_path:
result.save(output_path)
return result
# Usage examples
if __name__ == "__main__":
# Basic usage - convert black to alpha
#for i in range(13):
gimp_color_to_alpha_exact("gradient_clear.png", output_path= "gradient_clear.png")
print("Color to Alpha processing complete!")

View File

@@ -4,6 +4,8 @@ import logging
from pathlib import Path from pathlib import Path
import random import random
from typing import Optional, Union from typing import Optional, Union
from raylib import SHADER_UNIFORM_FLOAT, SHADER_UNIFORM_VEC3
from libs.audio import audio from libs.audio import audio
from libs.animation import Animation, MoveAnimation from libs.animation import Animation, MoveAnimation
from libs.global_data import Crown, Difficulty from libs.global_data import Crown, Difficulty
@@ -18,6 +20,45 @@ BOX_CENTER = 594 * tex.screen_scale
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def rgb_to_hue(r, g, b):
rf = r / 255.0
gf = g / 255.0
bf = b / 255.0
max_val = max(rf, gf, bf)
min_val = min(rf, gf, bf)
delta = max_val - min_val
if delta == 0:
return 0 # Gray/white, no hue
if max_val == rf:
hue = 60.0 * (((gf - bf) / delta) % 6)
elif max_val == gf:
hue = 60.0 * ((bf - rf) / delta + 2.0)
else:
hue = 60.0 * ((rf - gf) / delta + 4.0)
if hue < 0:
hue += 360.0
return hue
def calculate_hue_shift(source_rgb, target_rgb):
source_hue = rgb_to_hue(*source_rgb)
target_hue = rgb_to_hue(*target_rgb)
shift = (target_hue - source_hue) / 360.0
# Normalize to 0.0-1.0 range
while shift < 0:
shift += 1.0
while shift >= 1.0:
shift -= 1.0
return shift
class BaseBox(): class BaseBox():
OUTLINE_MAP = { OUTLINE_MAP = {
1: ray.Color(0, 77, 104, 255), 1: ray.Color(0, 77, 104, 255),
@@ -69,6 +110,17 @@ class BaseBox():
def load_text(self): def load_text(self):
self.name = OutlinedText(self.text_name, tex.skin_config["song_box_name"].font_size, ray.WHITE, outline_thickness=5, vertical=True) self.name = OutlinedText(self.text_name, tex.skin_config["song_box_name"].font_size, ray.WHITE, outline_thickness=5, vertical=True)
'''
self.shader = ray.load_shader('', 'shader/colortransform.fs')
source_rgb = (142, 212, 30)
target_rgb = (209, 162, 19)
source_color = ray.ffi.new('float[3]', [source_rgb[0]/255.0, source_rgb[1]/255.0, source_rgb[2]/255.0])
target_color = ray.ffi.new('float[3]', [target_rgb[0]/255.0, target_rgb[1]/255.0, target_rgb[2]/255.0])
source_loc = ray.get_shader_location(self.shader, 'sourceColor')
target_loc = ray.get_shader_location(self.shader, 'targetColor')
ray.set_shader_value(self.shader, source_loc, source_color, SHADER_UNIFORM_VEC3)
ray.set_shader_value(self.shader, target_loc, target_color, SHADER_UNIFORM_VEC3)
'''
def move_box(self, current_time: float): def move_box(self, current_time: float):
if self.position != self.target_position and self.move is None: if self.position != self.target_position and self.move is None:
@@ -96,10 +148,12 @@ class BaseBox():
self.open_fade.update(current_time) self.open_fade.update(current_time)
def _draw_closed(self, x: float, y: float, outer_fade_override: float): def _draw_closed(self, x: float, y: float, outer_fade_override: float):
#ray.begin_shader_mode(self.shader)
tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x, fade=outer_fade_override) tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x, fade=outer_fade_override)
offset = 1 * tex.screen_scale if self.texture_index == 3 or self.texture_index >= 9 and self.texture_index not in {10,11,12} else 0 offset = 1 * tex.screen_scale if self.texture_index == 3 or self.texture_index >= 9 and self.texture_index not in {10,11,12} else 0
tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x, x2=tex.skin_config["song_box_bg"].width, y=offset, fade=outer_fade_override) tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x, x2=tex.skin_config["song_box_bg"].width, y=offset, fade=outer_fade_override)
tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x, fade=outer_fade_override) tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x, fade=outer_fade_override)
#ray.end_shader_mode()
if self.texture_index == BaseBox.DEFAULT_INDEX: if self.texture_index == BaseBox.DEFAULT_INDEX:
tex.draw_texture('box', 'genre_overlay', x=x, y=y, fade=outer_fade_override) tex.draw_texture('box', 'genre_overlay', x=x, y=y, fade=outer_fade_override)
elif self.texture_index == BaseBox.DIFFICULTY_SORT_INDEX: elif self.texture_index == BaseBox.DIFFICULTY_SORT_INDEX:
@@ -938,20 +992,27 @@ class DanCourse(FileSystemItem):
self.charts: list[tuple[TJAParser, int, int, int]] = [] self.charts: list[tuple[TJAParser, int, int, int]] = []
for chart in data["charts"]: for chart in data["charts"]:
hash = chart["hash"] hash = chart["hash"]
#chart_title = chart["title"] chart_title = chart["title"]
#chart_subtitle = chart["subtitle"] chart_subtitle = chart["subtitle"]
difficulty = chart["difficulty"] difficulty = chart["difficulty"]
if hash in global_data.song_hashes: if hash in global_data.song_hashes:
path = Path(global_data.song_hashes[hash][0]["file_path"]) path = Path(global_data.song_hashes[hash][0]["file_path"])
if (path.parent.parent / "box.def").exists():
_, genre_index, _ = parse_box_def(path.parent.parent)
else:
genre_index = 9
tja = TJAParser(path)
self.charts.append((tja, genre_index, difficulty, tja.metadata.course_data[difficulty].level))
else: else:
pass for key, value in global_data.song_hashes.items():
#do something with song_title, song_subtitle for i in range(len(value)):
song = value[i]
if (song["title"]["en"].strip() == chart_title and
song["subtitle"]["en"].strip() == chart_subtitle.removeprefix('--') and
Path(song["file_path"]).exists()):
hash_val = key
path = Path(global_data.song_hashes[hash_val][i]["file_path"])
break
if (path.parent.parent / "box.def").exists():
_, genre_index, _ = parse_box_def(path.parent.parent)
else:
genre_index = 9
tja = TJAParser(path)
self.charts.append((tja, genre_index, difficulty, tja.metadata.course_data[difficulty].level))
self.exams = [] self.exams = []
for exam in data["exams"]: for exam in data["exams"]:
self.exams.append(Exam(exam["type"], exam["value"][0], exam["value"][1], exam["range"])) self.exams.append(Exam(exam["type"], exam["value"][0], exam["value"][1], exam["range"]))
@@ -1037,7 +1098,7 @@ class FileNavigator:
self._generate_objects_recursive(root_path) self._generate_objects_recursive(root_path)
if self.favorite_folder is not None: if self.favorite_folder is not None and self.favorite_folder.path.exists():
song_list = self._read_song_list(self.favorite_folder.path) song_list = self._read_song_list(self.favorite_folder.path)
for song_obj in song_list: for song_obj in song_list:
if str(song_obj) in self.all_song_files: if str(song_obj) in self.all_song_files:
@@ -1121,16 +1182,8 @@ class FileNavigator:
for tja_path in sorted(tja_files): for tja_path in sorted(tja_files):
song_key = str(tja_path) song_key = str(tja_path)
if song_key not in self.all_song_files and tja_path.name == "dan.json": if song_key not in self.all_song_files and tja_path.name == "dan.json":
valid_dan = True song_obj = DanCourse(tja_path, tja_path.name)
with open(tja_path, 'r', encoding='utf-8') as file: self.all_song_files[song_key] = song_obj
dan_data = json.load(file)
for chart in dan_data["charts"]:
hash = chart["hash"]
if hash not in global_data.song_hashes:
valid_dan = False
if valid_dan:
song_obj = DanCourse(tja_path, tja_path.name)
self.all_song_files[song_key] = song_obj
elif song_key not in self.all_song_files and tja_path in global_data.song_paths: elif song_key not in self.all_song_files and tja_path in global_data.song_paths:
song_obj = SongFile(tja_path, tja_path.name, texture_index) song_obj = SongFile(tja_path, tja_path.name, texture_index)
song_obj.box.get_scores() song_obj.box.get_scores()

View File

@@ -62,14 +62,23 @@ class TextureWrapper:
self.animations: dict[int, BaseAnimation] = dict() self.animations: dict[int, BaseAnimation] = dict()
self.skin_config: dict[str, SkinInfo] = dict() self.skin_config: dict[str, SkinInfo] = dict()
self.graphics_path = Path(get_config()['paths']['graphics_path']) self.graphics_path = Path(get_config()['paths']['graphics_path'])
if (self.graphics_path / "skin_config.json").exists(): self.parent_graphics_path = Path(get_config()['paths']['graphics_path'])
data = json.loads((self.graphics_path / "skin_config.json").read_text()) if not (self.graphics_path / "skin_config.json").exists():
self.skin_config: dict[str, SkinInfo] = { raise Exception("skin is missing a skin_config.json")
k: SkinInfo(v.get('x', 0), v.get('y', 0), v.get('font_size', 0), v.get('width', 0), v.get('height', 0)) for k, v in data.items()
} data = json.loads((self.graphics_path / "skin_config.json").read_text())
self.skin_config: dict[str, SkinInfo] = {
k: SkinInfo(v.get('x', 0), v.get('y', 0), v.get('font_size', 0), v.get('width', 0), v.get('height', 0)) for k, v in data.items()
}
self.screen_width = int(self.skin_config["screen"].width) self.screen_width = int(self.skin_config["screen"].width)
self.screen_height = int(self.skin_config["screen"].height) self.screen_height = int(self.skin_config["screen"].height)
self.screen_scale = self.screen_width / 1280 self.screen_scale = self.screen_width / 1280
if "parent" in data["screen"]:
parent = data["screen"]["parent"]
self.parent_graphics_path = Path("Graphics") / parent
parent_data = json.loads((self.parent_graphics_path / "skin_config.json").read_text())
for k, v in parent_data.items():
self.skin_config[k] = SkinInfo(v.get('x', 0) * self.screen_scale, v.get('y', 0) * self.screen_scale, v.get('font_size', 0) * self.screen_scale, v.get('width', 0) * self.screen_scale, v.get('height', 0) * self.screen_scale)
def unload_textures(self): def unload_textures(self):
"""Unload all textures and animations.""" """Unload all textures and animations."""
@@ -133,26 +142,62 @@ class TextureWrapper:
tex_object.controllable = [tex_mapping.get("controllable", False)] tex_object.controllable = [tex_mapping.get("controllable", False)]
def load_animations(self, screen_name: str): def load_animations(self, screen_name: str):
"""Load animations for a screen.""" """Load animations for a screen, falling back to parent if not found."""
screen_path = self.graphics_path / screen_name screen_path = self.graphics_path / screen_name
parent_screen_path = self.parent_graphics_path / screen_name
if (screen_path / 'animation.json').exists(): if (screen_path / 'animation.json').exists():
with open(screen_path / 'animation.json') as json_file: with open(screen_path / 'animation.json') as json_file:
self.animations = parse_animations(json.loads(json_file.read())) self.animations = parse_animations(json.loads(json_file.read()))
logger.info(f"Animations loaded for screen: {screen_name}") logger.info(f"Animations loaded for screen: {screen_name}")
elif self.parent_graphics_path != self.graphics_path and (parent_screen_path / 'animation.json').exists():
with open(parent_screen_path / 'animation.json') as json_file:
anim_json = json.loads(json_file.read())
for anim in anim_json:
if "total_distance" in anim and not isinstance(anim["total_distance"], dict):
anim["total_distance"] = anim["total_distance"] * self.screen_scale
self.animations = parse_animations(anim_json)
logger.info(f"Animations loaded for screen: {screen_name} (from parent)")
def load_zip(self, screen_name: str, subset: str): def load_zip(self, screen_name: str, subset: str):
"""Load textures from a zip file.""" """Load textures from child zip, using parent texture.json if child doesn't have one."""
zip = (self.graphics_path / screen_name / subset).with_suffix('.zip') zip_path = (self.graphics_path / screen_name / subset).with_suffix('.zip')
parent_zip_path = (self.parent_graphics_path / screen_name / subset).with_suffix('.zip')
if screen_name in self.textures and subset in self.textures[screen_name]: if screen_name in self.textures and subset in self.textures[screen_name]:
return return
try:
with zipfile.ZipFile(zip, 'r') as zip_ref:
if 'texture.json' not in zip_ref.namelist():
raise Exception(f"texture.json file missing from {zip}")
with zip_ref.open('texture.json') as json_file: # Child zip must exist
tex_mapping_data: dict[str, dict] = json.loads(json_file.read().decode('utf-8')) if not zip_path.exists():
self.textures[zip.stem] = dict() logger.warning(f"Zip file not found: {subset} for screen {screen_name}")
return
try:
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
# Try to get texture.json from child first, then parent
tex_mapping_data = None
texture_json_source = "child"
if 'texture.json' in zip_ref.namelist():
with zip_ref.open('texture.json') as json_file:
tex_mapping_data = json.loads(json_file.read().decode('utf-8'))
elif self.parent_graphics_path != self.graphics_path and parent_zip_path.exists():
# Fall back to parent's texture.json
with zipfile.ZipFile(parent_zip_path, 'r') as parent_zip_ref:
if 'texture.json' in parent_zip_ref.namelist():
with parent_zip_ref.open('texture.json') as json_file:
tex_mapping_data = json.loads(json_file.read().decode('utf-8'))
for tex_map in tex_mapping_data:
for key in tex_mapping_data[tex_map]:
if key in ["x", "y", "x2", "y2"]:
tex_mapping_data[tex_map][key] = tex_mapping_data[tex_map][key] * self.screen_scale
texture_json_source = "parent"
else:
raise Exception(f"texture.json file missing from both {zip_path} and {parent_zip_path}")
else:
raise Exception(f"texture.json file missing from {zip_path}")
self.textures[zip_path.stem] = dict()
encoding = sys.getfilesystemencoding() encoding = sys.getfilesystemencoding()
for tex_name in tex_mapping_data: for tex_name in tex_mapping_data:
@@ -166,11 +211,11 @@ class TextureWrapper:
extracted_path = Path(temp_dir) / tex_name extracted_path = Path(temp_dir) / tex_name
if extracted_path.is_dir(): if extracted_path.is_dir():
frames = [ray.LoadTexture(str(frame).encode(encoding)) for frame in sorted(extracted_path.iterdir(), frames = [ray.LoadTexture(str(frame).encode(encoding)) for frame in sorted(extracted_path.iterdir(),
key=lambda x: int(x.stem)) if frame.is_file()] key=lambda x: int(x.stem)) if frame.is_file()]
else: else:
frames = [ray.LoadTexture(str(extracted_path).encode(encoding))] frames = [ray.LoadTexture(str(extracted_path).encode(encoding))]
self.textures[zip.stem][tex_name] = Texture(tex_name, frames, tex_mapping) self.textures[zip_path.stem][tex_name] = Texture(tex_name, frames, tex_mapping)
self._read_tex_obj_data(tex_mapping, self.textures[zip.stem][tex_name]) self._read_tex_obj_data(tex_mapping, self.textures[zip_path.stem][tex_name])
elif f"{tex_name}.png" in zip_ref.namelist(): elif f"{tex_name}.png" in zip_ref.namelist():
tex_mapping = tex_mapping_data[tex_name] tex_mapping = tex_mapping_data[tex_name]
@@ -181,30 +226,34 @@ class TextureWrapper:
try: try:
tex = ray.LoadTexture(temp_path.encode(encoding)) tex = ray.LoadTexture(temp_path.encode(encoding))
self.textures[zip.stem][tex_name] = Texture(tex_name, tex, tex_mapping) self.textures[zip_path.stem][tex_name] = Texture(tex_name, tex, tex_mapping)
self._read_tex_obj_data(tex_mapping, self.textures[zip.stem][tex_name]) self._read_tex_obj_data(tex_mapping, self.textures[zip_path.stem][tex_name])
finally: finally:
os.unlink(temp_path) os.unlink(temp_path)
else: else:
logger.error(f"Texture {tex_name} was not found in {zip}") logger.error(f"Texture {tex_name} was not found in {zip_path}")
logger.info(f"Textures loaded from zip: {zip}")
json_note = f" (texture.json from {texture_json_source})" if texture_json_source == "parent" else ""
logger.info(f"Textures loaded from zip: {zip_path}{json_note}")
except Exception as e: except Exception as e:
logger.error(f"Failed to load textures from zip {zip}: {e}") logger.error(f"Failed to load textures from zip {zip_path}: {e}")
def load_screen_textures(self, screen_name: str) -> None: def load_screen_textures(self, screen_name: str) -> None:
"""Load textures for a screen.""" """Load textures for a screen."""
screen_path = self.graphics_path / screen_name screen_path = self.graphics_path / screen_name
if not screen_path.exists(): if not screen_path.exists():
logger.warning(f"Textures for Screen {screen_name} do not exist") logger.warning(f"Textures for Screen {screen_name} do not exist")
return return
if (screen_path / 'animation.json').exists():
with open(screen_path / 'animation.json') as json_file: # Load animations
self.animations = parse_animations(json.loads(json_file.read())) self.load_animations(screen_name)
logger.info(f"Animations loaded for screen: {screen_name}")
for zip in screen_path.iterdir(): # Load zip files from child screen path only
if zip.is_dir() or zip.suffix != ".zip": for zip_file in screen_path.iterdir():
continue if zip_file.is_file() and zip_file.suffix == ".zip":
self.load_zip(screen_name, zip.name) self.load_zip(screen_name, zip_file.stem)
logger.info(f"Screen textures loaded for: {screen_name}") logger.info(f"Screen textures loaded for: {screen_name}")
def control(self, tex_object: Texture, index: int = 0): def control(self, tex_object: Texture, index: int = 0):

45
shader/colortransform.fs Normal file
View File

@@ -0,0 +1,45 @@
#version 330
in vec2 fragTexCoord;
in vec4 fragColor;
out vec4 finalColor;
uniform sampler2D texture0;
uniform vec3 sourceColor; // RGB as 0-1
uniform vec3 targetColor; // RGB as 0-1
vec3 rgb2hsv(vec3 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 vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
vec3 hsv2rgb(vec3 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);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void main() {
vec4 texColor = texture(texture0, fragTexCoord);
vec3 sourceHSV = rgb2hsv(sourceColor);
vec3 targetHSV = rgb2hsv(targetColor);
vec3 pixelHSV = rgb2hsv(texColor.rgb);
// Calculate the transformation
float hueShift = targetHSV.x - sourceHSV.x;
float satScale = sourceHSV.y > 0.0 ? targetHSV.y / sourceHSV.y : 1.0;
float valScale = sourceHSV.z > 0.0 ? targetHSV.z / sourceHSV.z : 1.0;
// Apply transformation
pixelHSV.x = fract(pixelHSV.x + hueShift);
pixelHSV.y = clamp(pixelHSV.y * satScale, 0.0, 1.0);
pixelHSV.z = clamp(pixelHSV.z * valScale, 0.0, 1.0);
vec3 rgb = hsv2rgb(pixelHSV);
finalColor = vec4(rgb, texColor.a) * fragColor;
}