mirror of
https://github.com/litruv/PNGTuber-Plus.git
synced 2026-07-24 02:26:02 +10:00
- 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
469 lines
10 KiB
GDScript
469 lines
10 KiB
GDScript
extends Node2D
|
|
|
|
var type = "sprite"
|
|
|
|
#Passed Variables
|
|
var imageData = null
|
|
var tex = null
|
|
@export var path = ""
|
|
|
|
var loadedImageData = null
|
|
|
|
var id = 0
|
|
var parentId = null
|
|
var parentSprite = null
|
|
|
|
var imageSize = Vector2.ZERO
|
|
|
|
#Node Reference
|
|
@onready var sprite = $WobbleOrigin/DragOrigin/Sprite
|
|
|
|
@onready var grabArea = $WobbleOrigin/DragOrigin/Grab
|
|
|
|
@onready var dragOrigin = $WobbleOrigin/DragOrigin
|
|
@onready var dragger = $WobbleOrigin/Dragger
|
|
|
|
@onready var originSprite = $WobbleOrigin/DragOrigin/Sprite/Origin
|
|
|
|
@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
|
|
var grabDelay = 0
|
|
var size = Vector2(1,1)
|
|
|
|
var showOnTalk = 0
|
|
var showOnBlink = 0
|
|
|
|
var z = 0
|
|
|
|
#Movement
|
|
var heldTicks = 0
|
|
var dragSpeed = 0
|
|
|
|
|
|
#Origin
|
|
var origTick = 0
|
|
var offset = Vector2.ZERO
|
|
|
|
#Wobble
|
|
var xFrq = 0.0
|
|
var xAmp = 0.0
|
|
|
|
var yFrq = 0.0
|
|
var yAmp = 0.0
|
|
|
|
#Rotation Wobble
|
|
var rFrq = 0.0
|
|
var rAmp = 0.0
|
|
|
|
#Rotational Drag
|
|
var rdragStr = 0
|
|
var rLimitMax = 180
|
|
var rLimitMin = -180
|
|
|
|
#Layer
|
|
var costumeLayers = [1,1,1,1,1,1,1,1,1,1]
|
|
|
|
#Stretch
|
|
var stretchAmount = 0.0
|
|
|
|
#Ignore Bounce
|
|
var ignoreBounce = false
|
|
|
|
#Animation
|
|
var frames = 1
|
|
var animSpeed = 0
|
|
|
|
var remadePolygon = false
|
|
|
|
var clipped = false
|
|
|
|
var tick = 0
|
|
|
|
#Opacity
|
|
var spriteOpacity = 1.0
|
|
var affectChildrenOpacity = false
|
|
|
|
#Vis toggle
|
|
var toggle = "null"
|
|
|
|
func _ready():
|
|
|
|
Global.main.spriteVisToggles.connect(visToggle)
|
|
|
|
var img = Image.new()
|
|
var err = img.load(path)
|
|
if err != OK:
|
|
#Runs if image import fails. Needs error dialog box at some point
|
|
if loadedImageData == null:
|
|
Global.epicFail(err)
|
|
print_debug("Failed to load image.")
|
|
queue_free()
|
|
return
|
|
else:
|
|
var data = Marshalls.base64_to_raw(loadedImageData)
|
|
var errr = img.load_png_from_buffer(data)
|
|
if errr != OK:
|
|
Global.epicFail(err)
|
|
print_debug("Failed to load image.")
|
|
queue_free()
|
|
return
|
|
|
|
var texture = ImageTexture.new()
|
|
texture = ImageTexture.create_from_image(img)
|
|
|
|
|
|
tex = texture
|
|
imageData = img
|
|
|
|
imageSize = img.get_size()
|
|
|
|
sprite.texture = tex
|
|
|
|
var bitmap = BitMap.new()
|
|
bitmap.create_from_image_alpha(imageData)
|
|
|
|
var polygons = bitmap.opaque_to_polygons(Rect2(Vector2(0, 0), bitmap.get_size()),4.0) #bitmap.get_size()
|
|
|
|
var b = false
|
|
for polygon in polygons:
|
|
b = true
|
|
var collider = CollisionPolygon2D.new()
|
|
collider.polygon = polygon
|
|
grabArea.add_child(collider)
|
|
|
|
var outline = outlineScene.instantiate()
|
|
outline.points = polygon
|
|
outline.add_point(outline.points[0])
|
|
grabArea.add_child(outline)
|
|
|
|
size = imageData.get_size()
|
|
grabArea.position = size*-0.5
|
|
|
|
sprite.offset = offset
|
|
|
|
grabArea.position = (size*-0.5) + offset
|
|
|
|
changeFrames()
|
|
setZIndex()
|
|
|
|
if frames > 1:
|
|
remakePolygon()
|
|
if !b:
|
|
remakePolygon()
|
|
|
|
|
|
add_to_group(str(id))
|
|
await get_tree().create_timer(0.1).timeout
|
|
if parentId != null:
|
|
var nodes = get_tree().get_nodes_in_group(str(parentId))
|
|
get_parent().remove_child(self)
|
|
nodes[0].sprite.add_child(self)
|
|
parentSprite = nodes[0]
|
|
set_owner(nodes[0].sprite)
|
|
|
|
setClip(clipped)
|
|
|
|
|
|
if Global.filtering:
|
|
sprite.texture_filter = 2
|
|
|
|
func replaceSprite(pathNew):
|
|
var img = Image.new()
|
|
var err = img.load(pathNew)
|
|
if err != OK:
|
|
#Runs if image import fails.
|
|
Global.epicFail(err)
|
|
print_debug("Failed to load image.")
|
|
return
|
|
|
|
path = pathNew
|
|
|
|
var texture = ImageTexture.new()
|
|
texture = ImageTexture.create_from_image(img)
|
|
|
|
|
|
tex = texture
|
|
imageData = img
|
|
|
|
|
|
sprite.texture = tex
|
|
|
|
var bitmap = BitMap.new()
|
|
bitmap.create_from_image_alpha(imageData)
|
|
|
|
var polygons = bitmap.opaque_to_polygons(Rect2(Vector2(0, 0), bitmap.get_size()))
|
|
|
|
for i in grabArea.get_children():
|
|
i.queue_free()
|
|
|
|
var b = false
|
|
for polygon in polygons:
|
|
b = true
|
|
var collider = CollisionPolygon2D.new()
|
|
collider.polygon = polygon
|
|
grabArea.add_child(collider)
|
|
|
|
var outline = outlineScene.instantiate()
|
|
outline.points = polygon
|
|
outline.add_point(outline.points[0])
|
|
grabArea.add_child(outline)
|
|
size = imageData.get_size()
|
|
|
|
sprite.offset = offset
|
|
|
|
grabArea.position = (size*-0.5) + offset
|
|
|
|
if !b:
|
|
remakePolygon()
|
|
|
|
func _process(delta):
|
|
tick += 1
|
|
if Global.heldSprite == self:
|
|
|
|
grabArea.visible = true
|
|
originSprite.visible = true
|
|
|
|
else:
|
|
grabArea.visible = false
|
|
originSprite.visible = false
|
|
|
|
var glob = dragger.global_position
|
|
if ignoreBounce:
|
|
glob.y -= Global.main.bounceChange
|
|
|
|
drag(delta)
|
|
wobble()
|
|
|
|
var length = (glob.y - dragger.global_position.y)
|
|
|
|
rotationalDrag(length,delta)
|
|
stretch(length,delta)
|
|
|
|
if grabDelay > 0:
|
|
grabDelay -= 1
|
|
|
|
talkBlink()
|
|
|
|
animation()
|
|
|
|
func animation():
|
|
|
|
var speed = max(float(animSpeed),Engine.max_fps*6.0)
|
|
if animSpeed > 0 and frames > 1:
|
|
if Global.animationTick % int((speed)/float(animSpeed)) == 0:
|
|
if sprite.frame == frames - 1:
|
|
sprite.frame = 0
|
|
else:
|
|
sprite.frame += 1
|
|
if frames > 1:
|
|
remakePolygon()
|
|
|
|
func setZIndex():
|
|
sprite.z_index = z
|
|
|
|
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))
|
|
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()
|
|
|
|
func _physics_process(delta):
|
|
if Global.heldSprite == self:
|
|
var dir = pressingDirection()
|
|
if Input.is_action_pressed("origin"):
|
|
moveOrigin(dir)
|
|
else:
|
|
moveSprite(dir)
|
|
else:
|
|
set_physics_process(false)
|
|
|
|
func pressingDirection():
|
|
var dir = Vector2.ZERO
|
|
|
|
dir.x = Input.get_action_strength("move_left") - Input.get_action_strength("move_right")
|
|
dir.y = Input.get_action_strength("move_up") - Input.get_action_strength("move_down")
|
|
return dir
|
|
|
|
func moveSprite(dir):
|
|
if dir != Vector2.ZERO:
|
|
heldTicks += 1
|
|
else:
|
|
heldTicks = 0
|
|
|
|
if heldTicks > 30 or heldTicks == 1:
|
|
var multiplier = 2
|
|
if heldTicks == 1:
|
|
multiplier = 1
|
|
position -= dir * multiplier
|
|
|
|
position = Vector2(int(position.x),int(position.y))
|
|
|
|
func moveOrigin(dir):
|
|
if dir != Vector2.ZERO:
|
|
origTick += 1
|
|
else:
|
|
origTick = 0
|
|
|
|
if origTick > 30 or origTick == 1:
|
|
var multiplier = 2
|
|
if origTick == 1:
|
|
multiplier = 1
|
|
|
|
offset += dir * multiplier
|
|
position -= dir * multiplier
|
|
|
|
offset = Vector2(int(offset.x),int(offset.y))
|
|
|
|
sprite.offset = offset
|
|
grabArea.position = (size*-0.5) + offset
|
|
|
|
func drag(delta):
|
|
if dragSpeed == 0:
|
|
dragger.global_position = wob.global_position
|
|
else:
|
|
dragger.global_position = lerp(dragger.global_position,wob.global_position,1/dragSpeed)
|
|
dragOrigin.global_position = dragger.global_position
|
|
|
|
func wobble():
|
|
wob.position.x = sin(tick*xFrq)*xAmp
|
|
wob.position.y = sin(tick*yFrq)*yAmp
|
|
|
|
func rotationalDrag(length,delta):
|
|
var yvel = (length * rdragStr)
|
|
|
|
#Calculate Max angle
|
|
yvel = clamp(yvel,rLimitMin,rLimitMax)
|
|
|
|
# Add rotation wobble as percentage of rotational limits range
|
|
var rotationRange = rLimitMax - rLimitMin
|
|
var rotationWobble = sin(tick*rFrq) * (rotationRange * rAmp * 0.01)
|
|
var totalRotation = yvel + rotationWobble
|
|
|
|
sprite.rotation = lerp_angle(sprite.rotation,deg_to_rad(totalRotation),0.25)
|
|
|
|
func stretch(length,delta):
|
|
var yvel = (length * stretchAmount * 0.01)
|
|
var target = Vector2(1.0-yvel,1.0+yvel)
|
|
|
|
sprite.scale = lerp(sprite.scale,target,0.5)
|
|
|
|
func changeCollision(enable):
|
|
grabArea.monitorable = enable
|
|
grabArea.monitorable = enable
|
|
|
|
func changeFrames():
|
|
sprite.hframes = frames
|
|
sprite.frame = 0
|
|
|
|
func remakePolygon():
|
|
if remadePolygon:
|
|
return
|
|
for c in grabArea.get_children():
|
|
c.queue_free()
|
|
var collider = CollisionShape2D.new()
|
|
var shape = RectangleShape2D.new()
|
|
shape.size = Vector2(imageSize.y,imageSize.y)
|
|
collider.shape = shape
|
|
collider.position = Vector2(imageSize.x,imageSize.y) * Vector2(0.5,0.5)
|
|
grabArea.add_child(collider)
|
|
|
|
var p = imageSize.y * 0.5
|
|
var outline = outlineScene.instantiate()
|
|
outline.add_point(Vector2(-p,-p))
|
|
outline.add_point(Vector2(p,-p))
|
|
outline.add_point(Vector2(p,p))
|
|
outline.add_point(Vector2(-p,p))
|
|
outline.add_point(Vector2(-p,-p))
|
|
outline.position = collider.position
|
|
grabArea.add_child(outline)
|
|
|
|
remadePolygon = true
|
|
|
|
func setClip(toggle):
|
|
if toggle:
|
|
sprite.clip_children = CLIP_CHILDREN_AND_DRAW
|
|
|
|
for node in getAllLinkedSprites():
|
|
node.z = z
|
|
node.setZIndex()
|
|
|
|
else:
|
|
sprite.clip_children = CLIP_CHILDREN_DISABLED
|
|
|
|
clipped = toggle
|
|
|
|
func getAllLinkedSprites():
|
|
var nodes = get_tree().get_nodes_in_group("saved")
|
|
var linkedSprites = []
|
|
for node in nodes:
|
|
if node.parentId == id:
|
|
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
|
|
|
|
func makeVis():
|
|
$WobbleOrigin/DragOrigin.visible = true
|