mirror of
https://github.com/litruv/PNGTuber-Plus.git
synced 2026-07-24 02:26:02 +10:00
Add opacity slider with shader-based rendering that preserves clip mask behavior
- Added opacity slider (0-100%) in sprite options with 'affect children' checkbox - Implemented hierarchical opacity that affects entire sprite hierarchy when enabled - Created shader-based opacity system that doesn't interfere with clip mask functionality - Clip masks now work with original image pixels regardless of opacity settings - Removed shadow effects from selected sprites to prevent visual conflicts - Updated save/load system to persist opacity properties with backward compatibility Fixes clipping behavior issues where opacity changes previously affected clip mask detection
This commit is contained in:
11
.vscode/tasks.json
vendored
Normal file
11
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build PNGTuber-Plus",
|
||||
"type": "shell",
|
||||
"command": "cd c:\\DEV\\Sync\\PNGTuber-Plus && .\\build-windows.bat",
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||

|
||||
|
||||
### Recent Updates by Litruv
|
||||
- **Opacity Controls**: Sprite opacity slider, with hierarchical "affect children" option that also preserves clip mask behavior
|
||||
- **GitHub Actions**: Automated Windows builds on every commit
|
||||
- **StreamDeck Integration**: Add buttons to enable the streamdeck plugin that was built in
|
||||
- **Rotation Wobble**: New sprite rotation effects with frequency and amplitude controls
|
||||
|
||||
Binary file not shown.
BIN
bin/~libgdexample.windows.template_debug.x86_64.dll
Normal file
BIN
bin/~libgdexample.windows.template_debug.x86_64.dll
Normal file
Binary file not shown.
@@ -153,17 +153,8 @@ func _process(delta):
|
||||
followShadow()
|
||||
|
||||
func followShadow():
|
||||
shadow.visible = is_instance_valid(Global.heldSprite)
|
||||
if !shadow.visible:
|
||||
return
|
||||
|
||||
shadow.global_position = Global.heldSprite.sprite.global_position + Vector2(6,6)
|
||||
shadow.global_rotation = Global.heldSprite.sprite.global_rotation
|
||||
shadow.offset = Global.heldSprite.sprite.offset
|
||||
|
||||
shadow.texture = Global.heldSprite.sprite.texture
|
||||
shadow.hframes = Global.heldSprite.sprite.hframes
|
||||
shadow.frame = Global.heldSprite.sprite.frame
|
||||
# Shadow disabled for selected sprites
|
||||
shadow.visible = false
|
||||
|
||||
|
||||
func isFileSystemOpen():
|
||||
@@ -362,6 +353,10 @@ func _on_load_dialog_file_selected(path):
|
||||
sprite.clipped = data[item]["clipped"]
|
||||
if data[item].has("toggle"):
|
||||
sprite.toggle = data[item]["toggle"]
|
||||
if data[item].has("spriteOpacity"):
|
||||
sprite.spriteOpacity = data[item]["spriteOpacity"]
|
||||
if data[item].has("affectChildrenOpacity"):
|
||||
sprite.affectChildrenOpacity = data[item]["affectChildrenOpacity"]
|
||||
|
||||
origin.add_child(sprite)
|
||||
sprite.position = str_to_var(data[item]["pos"])
|
||||
@@ -424,6 +419,9 @@ func _on_save_dialog_file_selected(path):
|
||||
|
||||
data[id]["toggle"] = child.toggle
|
||||
|
||||
data[id]["spriteOpacity"] = child.spriteOpacity
|
||||
data[id]["affectChildrenOpacity"] = child.affectChildrenOpacity
|
||||
|
||||
id += 1
|
||||
|
||||
Saving.settings["lastAvatar"] = path
|
||||
@@ -500,6 +498,9 @@ func _on_duplicate_button_pressed():
|
||||
|
||||
sprite.costumeLayers = Global.heldSprite.costumeLayers
|
||||
|
||||
sprite.spriteOpacity = Global.heldSprite.spriteOpacity
|
||||
sprite.affectChildrenOpacity = Global.heldSprite.affectChildrenOpacity
|
||||
|
||||
origin.add_child(sprite)
|
||||
sprite.position = Global.heldSprite.position + Vector2(16,16)
|
||||
|
||||
@@ -547,7 +548,7 @@ func moveSpriteMenu(delta):
|
||||
|
||||
var size = get_viewport().get_visible_rect().size
|
||||
|
||||
var windowLength = 1400
|
||||
var windowLength = 1500
|
||||
|
||||
$ViewerArrows/Arrows.position.y = size.y - 25
|
||||
|
||||
@@ -673,4 +674,3 @@ func _on_background_input_capture_bg_key_pressed(node, keys_pressed):
|
||||
if i >= 0:
|
||||
changeCostume(i+1)
|
||||
|
||||
|
||||
|
||||
8
ui_scenes/selectedSprite/opacity.gdshader
Normal file
8
ui_scenes/selectedSprite/opacity.gdshader
Normal file
@@ -0,0 +1,8 @@
|
||||
shader_type canvas_item;
|
||||
|
||||
uniform float opacity : hint_range(0.0, 1.0) = 1.0;
|
||||
|
||||
void fragment() {
|
||||
COLOR = texture(TEXTURE, UV);
|
||||
COLOR.a *= opacity;
|
||||
}
|
||||
1
ui_scenes/selectedSprite/opacity.gdshader.uid
Normal file
1
ui_scenes/selectedSprite/opacity.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://byxs44vljsg2f
|
||||
@@ -28,6 +28,9 @@ var imageSize = Vector2.ZERO
|
||||
@onready var wob = $WobbleOrigin
|
||||
|
||||
@onready var outlineScene = preload("res://ui_scenes/selectedSprite/outline.tscn")
|
||||
@onready var opacityShader = preload("res://ui_scenes/selectedSprite/opacity.gdshader")
|
||||
|
||||
var opacityMaterial = null
|
||||
|
||||
#Visuals
|
||||
var mouseOffset = Vector2.ZERO
|
||||
@@ -83,6 +86,10 @@ var clipped = false
|
||||
|
||||
var tick = 0
|
||||
|
||||
#Opacity
|
||||
var spriteOpacity = 1.0
|
||||
var affectChildrenOpacity = false
|
||||
|
||||
#Vis toggle
|
||||
var toggle = "null"
|
||||
|
||||
@@ -265,7 +272,24 @@ func talkBlink():
|
||||
var faded = 0.2 * int(Global.main.editMode)
|
||||
var value = (showOnTalk + (showOnBlink*3)) + (int(Global.speaking)*10) + (int(Global.blink)*20)
|
||||
var yes = [0,10,20,30,1,21,12,32,3,13,4,15,26,36,27,38].has(int(value))
|
||||
sprite.self_modulate.a = max(int(yes),faded)
|
||||
var baseAlpha = max(int(yes), faded)
|
||||
|
||||
# Only handle talk/blink/edit visibility with modulate
|
||||
# Opacity slider effects are handled by shader
|
||||
sprite.self_modulate.a = 1.0 # Always keep original texture alpha for clipping
|
||||
sprite.modulate.a = baseAlpha # Only apply talk/blink/edit visibility
|
||||
|
||||
func calculateParentOpacityMultiplier():
|
||||
var multiplier = 1.0
|
||||
var currentParent = parentSprite
|
||||
|
||||
# Traverse up the entire parent hierarchy
|
||||
while currentParent != null:
|
||||
if currentParent.affectChildrenOpacity:
|
||||
multiplier *= currentParent.spriteOpacity
|
||||
currentParent = currentParent.parentSprite
|
||||
|
||||
return multiplier
|
||||
|
||||
func delete():
|
||||
queue_free()
|
||||
@@ -403,6 +427,39 @@ func getAllLinkedSprites():
|
||||
linkedSprites.append(node)
|
||||
return linkedSprites
|
||||
|
||||
func updateOpacity():
|
||||
# Force a refresh of opacity for this sprite and its children
|
||||
talkBlink()
|
||||
updateShaderOpacity()
|
||||
|
||||
# Update all child sprites in the entire hierarchy if this sprite affects children
|
||||
if affectChildrenOpacity:
|
||||
updateChildrenOpacityRecursively()
|
||||
|
||||
func updateShaderOpacity():
|
||||
# Calculate total opacity from user settings and parent hierarchy
|
||||
var totalOpacity = spriteOpacity * calculateParentOpacityMultiplier()
|
||||
|
||||
if totalOpacity >= 1.0:
|
||||
# No shader needed for 100% opacity
|
||||
sprite.material = null
|
||||
else:
|
||||
# Create or update shader material for opacity less than 100%
|
||||
if opacityMaterial == null:
|
||||
opacityMaterial = ShaderMaterial.new()
|
||||
opacityMaterial.shader = opacityShader
|
||||
|
||||
opacityMaterial.set_shader_parameter("opacity", totalOpacity)
|
||||
sprite.material = opacityMaterial
|
||||
|
||||
func updateChildrenOpacityRecursively():
|
||||
var linkedSprites = getAllLinkedSprites()
|
||||
for child in linkedSprites:
|
||||
child.talkBlink()
|
||||
child.updateShaderOpacity()
|
||||
# Always update the entire hierarchy below this child
|
||||
child.updateChildrenOpacityRecursively()
|
||||
|
||||
func visToggle(keys):
|
||||
if keys.has(toggle):
|
||||
$WobbleOrigin/DragOrigin.visible = !$WobbleOrigin/DragOrigin.visible
|
||||
|
||||
@@ -73,6 +73,10 @@ func setImage():
|
||||
$Animation/animFramesLabel.text = "sprite frames: " + str(Global.heldSprite.frames)
|
||||
$Animation/animFrames.value = Global.heldSprite.frames
|
||||
|
||||
$Opacity/opacityLabel.text = "opacity: " + str(int(Global.heldSprite.spriteOpacity * 100)) + "%"
|
||||
$Opacity/opacitySlider.value = Global.heldSprite.spriteOpacity
|
||||
$Opacity/affectChildrenCheck.button_pressed = Global.heldSprite.affectChildrenOpacity
|
||||
|
||||
$VisToggle/setToggle/Label.text = "toggle: \"" + Global.heldSprite.toggle + "\""
|
||||
|
||||
changeRotLimit()
|
||||
@@ -369,3 +373,12 @@ func _on_set_toggle_pressed():
|
||||
var key = keys[0]
|
||||
Global.heldSprite.toggle = key
|
||||
$VisToggle/setToggle/Label.text = "toggle: \"" + Global.heldSprite.toggle + "\""
|
||||
|
||||
func _on_opacity_slider_value_changed(value):
|
||||
$Opacity/opacityLabel.text = "opacity: " + str(int(value * 100)) + "%"
|
||||
Global.heldSprite.spriteOpacity = value
|
||||
Global.heldSprite.updateOpacity()
|
||||
|
||||
func _on_affect_children_check_toggled(button_pressed):
|
||||
Global.heldSprite.affectChildrenOpacity = button_pressed
|
||||
Global.heldSprite.updateOpacity()
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
[ext_resource type="Texture2D" uid="uid://gflmeocxfnrq" path="res://ui_scenes/settings/deleteHotkey.png" id="27_fgu6g"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_21kva"]
|
||||
size = Vector2(245, 1341)
|
||||
size = Vector2(245, 1455)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_hxmah"]
|
||||
shader = ExtResource("3_ky0lu")
|
||||
@@ -14285,11 +14285,12 @@ script = ExtResource("1_hp0e3")
|
||||
z_index = -2
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
|
||||
position = Vector2(122.5, 670.5)
|
||||
position = Vector2(122.5, 727.5)
|
||||
shape = SubResource("RectangleShape2D_21kva")
|
||||
|
||||
[node name="Border" type="Sprite2D" parent="."]
|
||||
scale = Vector2(1, 1.16)
|
||||
position = Vector2(0, 2.22901)
|
||||
scale = Vector2(1, 1.37547)
|
||||
texture = ExtResource("1_b54o8")
|
||||
centered = false
|
||||
offset = Vector2(-9, -10.3448)
|
||||
@@ -14349,13 +14350,13 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3.362)
|
||||
fov = 36.5
|
||||
|
||||
[node name="Position" type="Node2D" parent="."]
|
||||
position = Vector2(0, 10)
|
||||
position = Vector2(0, 20)
|
||||
|
||||
[node name="fileTitle" type="Label" parent="Position"]
|
||||
offset_left = 10.0
|
||||
offset_top = 134.0
|
||||
offset_top = 127.0
|
||||
offset_right = 236.0
|
||||
offset_bottom = 154.0
|
||||
offset_bottom = 147.0
|
||||
text = "user://greenThing/head1.png"
|
||||
label_settings = SubResource("LabelSettings_6favr")
|
||||
|
||||
@@ -14381,7 +14382,7 @@ offset_bottom = 227.0
|
||||
text = "layer : 0"
|
||||
|
||||
[node name="Buttons" type="Node2D" parent="."]
|
||||
position = Vector2(0, 27)
|
||||
position = Vector2(0, 141)
|
||||
|
||||
[node name="Speaking" type="Sprite2D" parent="Buttons"]
|
||||
position = Vector2(42, 523)
|
||||
@@ -14468,7 +14469,7 @@ scrollable = false
|
||||
|
||||
[node name="WobbleControl" type="Node2D" parent="."]
|
||||
z_index = 1
|
||||
position = Vector2(-3, 378)
|
||||
position = Vector2(-3, 486)
|
||||
|
||||
[node name="animationBox" type="Sprite2D" parent="WobbleControl"]
|
||||
position = Vector2(125, 384.5)
|
||||
@@ -14612,7 +14613,7 @@ scrollable = false
|
||||
|
||||
[node name="RotationalLimits" type="Node2D" parent="."]
|
||||
z_index = -1
|
||||
position = Vector2(0, 325)
|
||||
position = Vector2(0, 435)
|
||||
|
||||
[node name="RotBack" type="Sprite2D" parent="RotationalLimits"]
|
||||
clip_children = 2
|
||||
@@ -14725,8 +14726,43 @@ offset_bottom = 1068.0
|
||||
theme = SubResource("Theme_mrbyn")
|
||||
scrollable = false
|
||||
|
||||
[node name="Opacity" type="Node2D" parent="."]
|
||||
position = Vector2(-2, -611)
|
||||
|
||||
[node name="opacityLabel" type="Label" parent="Opacity"]
|
||||
offset_left = 11.0
|
||||
offset_top = 1090.0
|
||||
offset_right = 159.0
|
||||
offset_bottom = 1116.0
|
||||
text = "opacity: 100%"
|
||||
|
||||
[node name="opacitySlider" type="HSlider" parent="Opacity"]
|
||||
offset_left = 10.0
|
||||
offset_top = 1111.0
|
||||
offset_right = 233.0
|
||||
offset_bottom = 1131.0
|
||||
theme = SubResource("Theme_mrbyn")
|
||||
max_value = 1.0
|
||||
step = 0.01
|
||||
value = 1.0
|
||||
scrollable = false
|
||||
|
||||
[node name="affectChildrenLabel" type="Label" parent="Opacity"]
|
||||
offset_left = 11.0
|
||||
offset_top = 1140.0
|
||||
offset_right = 159.0
|
||||
offset_bottom = 1166.0
|
||||
text = "affect children:"
|
||||
|
||||
[node name="affectChildrenCheck" type="CheckBox" parent="Opacity"]
|
||||
offset_left = 140.0
|
||||
offset_top = 1140.0
|
||||
offset_right = 200.0
|
||||
offset_bottom = 1166.0
|
||||
theme = SubResource("Theme_mrbyn")
|
||||
|
||||
[node name="Layers" type="Node2D" parent="."]
|
||||
position = Vector2(0, 327)
|
||||
position = Vector2(0, 443)
|
||||
|
||||
[node name="Layer1" type="Sprite2D" parent="Layers"]
|
||||
position = Vector2(23, 878)
|
||||
@@ -14863,7 +14899,7 @@ position = Vector2(23, 878)
|
||||
texture = ExtResource("20_px0dt")
|
||||
|
||||
[node name="VisToggle" type="Node2D" parent="."]
|
||||
position = Vector2(11, 1289)
|
||||
position = Vector2(11, 1404)
|
||||
|
||||
[node name="setToggle" type="Button" parent="VisToggle"]
|
||||
custom_minimum_size = Vector2(0, 37.125)
|
||||
@@ -14929,6 +14965,8 @@ centered = false
|
||||
[connection signal="value_changed" from="RotationalLimits/rotLimitMax" to="." method="_on_rot_limit_max_value_changed"]
|
||||
[connection signal="value_changed" from="Animation/animFrames" to="." method="_on_anim_frames_value_changed"]
|
||||
[connection signal="value_changed" from="Animation/animSpeed" to="." method="_on_anim_speed_value_changed"]
|
||||
[connection signal="value_changed" from="Opacity/opacitySlider" to="." method="_on_opacity_slider_value_changed"]
|
||||
[connection signal="toggled" from="Opacity/affectChildrenCheck" to="." method="_on_affect_children_check_toggled"]
|
||||
[connection signal="pressed" from="Layers/Layer1/layerButton1" to="." method="_on_layer_button_1_pressed"]
|
||||
[connection signal="pressed" from="Layers/Layer2/layerButton2" to="." method="_on_layer_button_2_pressed"]
|
||||
[connection signal="pressed" from="Layers/Layer3/layerButton3" to="." method="_on_layer_button_3_pressed"]
|
||||
|
||||
Reference in New Issue
Block a user