mirror of
https://github.com/litruv/PNGTuber-Plus.git
synced 2026-07-24 02:26:02 +10:00
Compare commits
10 Commits
v2025.07.2
...
v2025.07.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 085d6a21a0 | |||
| 99d2d3f508 | |||
| dad22ece22 | |||
| 52496df10e | |||
| 29b903fadd | |||
| e8191e2de8 | |||
| b7b7d7cdec | |||
| fc6e4a1028 | |||
| 1dfe21363b | |||
| fde3f23ed5 |
46
.github/workflows/build-windows.yml
vendored
46
.github/workflows/build-windows.yml
vendored
@@ -16,6 +16,7 @@ jobs:
|
||||
export-windows:
|
||||
name: Windows Export
|
||||
runs-on: self-hosted
|
||||
if: contains(github.event.head_commit.message, '!build')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -124,11 +125,49 @@ jobs:
|
||||
Write-Host "Generated tag: $tag"
|
||||
Write-Host "Generated release name: $releaseName"
|
||||
|
||||
# Get the latest release tag to compare against
|
||||
$latestTag = ""
|
||||
try {
|
||||
$latestTag = & git describe --tags --abbrev=0 2>$null
|
||||
Write-Host "Latest tag found: $latestTag"
|
||||
} catch {
|
||||
Write-Host "No previous tags found, showing all commits"
|
||||
}
|
||||
|
||||
# Generate commit history since last tag (or all commits if no tag)
|
||||
$commitRange = if ($latestTag) { "$latestTag..HEAD" } else { "HEAD" }
|
||||
|
||||
# Get commits with format: short hash, subject, author
|
||||
$commits = & git log $commitRange --pretty=format:"%h|%s|%an" --no-merges
|
||||
|
||||
# Build commit history as a single string
|
||||
$commitHistory = ""
|
||||
|
||||
if ($commits) {
|
||||
$commitHistory += "`n`n## Changes Since Last Release`n`n"
|
||||
|
||||
$commits | ForEach-Object {
|
||||
$parts = $_ -split '\|', 3
|
||||
$hash = $parts[0]
|
||||
$subject = $parts[1] -replace '"', '""'
|
||||
$author = $parts[2]
|
||||
$commitUrl = "https://github.com/${{ github.repository }}/commit/$hash"
|
||||
$commitHistory += "- [$hash]($commitUrl) $subject`n"
|
||||
}
|
||||
} else {
|
||||
$commitHistory += "`n`n## Changes Since Last Release`n`nNo new commits since last release.`n"
|
||||
}
|
||||
|
||||
# Set outputs for use in next steps
|
||||
echo "tag=$tag" >> $env:GITHUB_OUTPUT
|
||||
echo "release_name=$releaseName" >> $env:GITHUB_OUTPUT
|
||||
echo "short_sha=$shortSha" >> $env:GITHUB_OUTPUT
|
||||
echo "build_number=$buildNumber" >> $env:GITHUB_OUTPUT
|
||||
|
||||
# Use heredoc syntax for multiline commit history
|
||||
Add-Content -Path $env:GITHUB_OUTPUT -Value "commit_history<<EOF"
|
||||
Add-Content -Path $env:GITHUB_OUTPUT -Value $commitHistory
|
||||
Add-Content -Path $env:GITHUB_OUTPUT -Value "EOF"
|
||||
- name: Download Windows Build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -145,15 +184,14 @@ jobs:
|
||||
release_name: ${{ steps.release_info.outputs.release_name }}
|
||||
body: |
|
||||
Automated build of PNGTuber Plus
|
||||
|
||||
|
||||
${{ steps.release_info.outputs.commit_history }}
|
||||
|
||||
## Installation
|
||||
1. Download PNGTuber-Plus.exe from the assets below
|
||||
2. Run the executable - no installation required!
|
||||
3. For StreamDeck integration, download the plugin from [here](https://github.com/BoyneGames/streamdeck-godot-plugin/releases/tag/1.0.0)
|
||||
|
||||
## Credits
|
||||
- Original project by [kaiakairos](https://github.com/kaiakairos/PNGTuber-Plus)
|
||||
- Microphone improvements by [k0ffinz](https://github.com/k0ffinz/PNGTuber-Plus)
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
|
||||
2
.vscode/tasks.json
vendored
2
.vscode/tasks.json
vendored
@@ -4,7 +4,7 @@
|
||||
{
|
||||
"label": "Build PNGTuber-Plus",
|
||||
"type": "shell",
|
||||
"command": "cd c:\\DEV\\Sync\\PNGTuber-Plus && .\\build-windows.bat",
|
||||
"command": "cd c:\\DEV\\Sync\\PNGTuber-Plus; .\\build-windows.bat",
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
|
||||
68
README.md
68
README.md
@@ -2,17 +2,61 @@
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
- **Enhanced Modifier Keys**: Support for complex key combinations (Ctrl+Shift+Alt+Cmd)
|
||||
- **Godot 4.4.1 Support**: Updated for latest Godot stable release
|
||||
- **Microphone Improvements**: Audio delay fix & more *from [k0ffinz/PNGTuber-Plus](https://github.com/k0ffinz/PNGTuber-Plus)*
|
||||
A fork of [kaiakairos/PNGTuber-Plus](https://github.com/kaiakairos/PNGTuber-Plus) with some quality-of-life upgrades, editor polish, and a few power-user toys.
|
||||
|
||||
## 🙏 Credits
|
||||
---
|
||||
|
||||
- **Original Project**: [PNGTuber Plus by kaiakairos](https://github.com/kaiakairos/PNGTuber-Plus)
|
||||
- **StreamDeck Plugin**: Based on [BoyneGames StreamDeck Godot Plugin](https://github.com/BoyneGames/streamdeck-godot-plugin)
|
||||
- **Microphone Improvements**: k0ffinz - Experimental mic loudness detection, improved audio initialization, and audio delay fixes from [k0ffinz/PNGTuber-Plus](https://github.com/k0ffinz/PNGTuber-Plus)
|
||||
## 🔧 Fork Changes (Litruv Edition)
|
||||
|
||||
### 🎛️ Opacity Controls
|
||||
|
||||
Added a proper opacity slider for sprites, with an "affect children" toggle. Clip masks still behave like they should.
|
||||
|
||||

|
||||
|
||||
|
||||
### 🌀 Rotation Wobble
|
||||
|
||||
Sprites can now do a subtle (or chaotic) rotational wobble. Frequency + amplitude sliders included.
|
||||
|
||||

|
||||
|
||||
|
||||
### ⌨️ Enhanced Modifier Keys
|
||||
|
||||
Supports combos like Ctrl+Shift+Alt+Cmd. For people with more keybinds than fingers.
|
||||
|
||||

|
||||
|
||||
|
||||
### ↗️ Middle Mouse Button Panning
|
||||
|
||||
Hold MMB in the editor to pan around the scene, it'll reset when you go back to live.
|
||||
|
||||

|
||||
|
||||
|
||||
### 🎤 Microphone Fixes
|
||||
|
||||
Merged in [k0ffinz’s](https://github.com/k0ffinz/PNGTuber-Plus) audio tweaks:
|
||||
|
||||
* Reduced mic startup delay
|
||||
* Better loudness detection
|
||||
* General stability improvements
|
||||
|
||||
|
||||
### 🧱 Engine Update
|
||||
|
||||
Updated to **Godot 4.4.1** — no more editor nags or compatibility weirdness.
|
||||
|
||||
|
||||
### 🎚️ StreamDeck Integration
|
||||
|
||||
StreamDeck plugin was already in, this just wires up the buttons.
|
||||
---
|
||||
|
||||
## 🧾 Credits
|
||||
|
||||
* Original project by [kaiakairos](https://github.com/kaiakairos/PNGTuber-Plus)
|
||||
* StreamDeck plugin from [BoyneGames](https://github.com/BoyneGames/streamdeck-godot-plugin)
|
||||
* Mic improvements by [k0ffinz](https://github.com/k0ffinz/PNGTuber-Plus)
|
||||
|
||||
@@ -106,7 +106,7 @@ func _process(delta):
|
||||
heldSprite.z += 1
|
||||
heldSprite.setZIndex()
|
||||
pushUpdate("Moved sprite layer.")
|
||||
if main.editMode:
|
||||
if main and main.editMode:
|
||||
if Input.is_action_just_pressed("reparent"):
|
||||
reparentMode = !reparentMode
|
||||
Global.chain.enable(reparentMode)
|
||||
@@ -115,7 +115,7 @@ func _process(delta):
|
||||
reparentMode = false
|
||||
Global.chain.enable(reparentMode)
|
||||
|
||||
if main.editMode:
|
||||
if main and main.editMode:
|
||||
if reparentMode:
|
||||
RenderingServer.set_default_clear_color(Color.POWDER_BLUE)
|
||||
else:
|
||||
@@ -126,7 +126,7 @@ func _process(delta):
|
||||
scrollSprites()
|
||||
|
||||
|
||||
if !main.fileSystemOpen:
|
||||
if !main or main.fileSystemOpen:
|
||||
|
||||
if Input.is_action_just_pressed("refresh"):
|
||||
refresh()
|
||||
@@ -222,7 +222,7 @@ func scrollSprites():
|
||||
if Input.is_action_pressed("control"):
|
||||
return
|
||||
|
||||
if !main.editMode:
|
||||
if !main or !main.editMode:
|
||||
return
|
||||
|
||||
if main.fileSystemOpen:
|
||||
|
||||
@@ -43,6 +43,12 @@ var bounceOnCostumeChange = false
|
||||
#Zooming
|
||||
var scaleOverall = 100
|
||||
|
||||
#Camera Panning
|
||||
var isPanning = false
|
||||
var initialPanPosition = Vector2.ZERO
|
||||
var initialCameraOffset = Vector2.ZERO
|
||||
var cameraOffset = Vector2.ZERO
|
||||
|
||||
var bounceChange = 0.0
|
||||
|
||||
#IMPORTANT
|
||||
@@ -130,7 +136,7 @@ func _ready():
|
||||
|
||||
var s = get_viewport().get_visible_rect().size
|
||||
origin.position = s*0.5
|
||||
camera.position = origin.position
|
||||
updateCameraPosition()
|
||||
|
||||
func _process(delta):
|
||||
var hold = origin.get_parent().position.y
|
||||
@@ -152,6 +158,43 @@ func _process(delta):
|
||||
|
||||
followShadow()
|
||||
|
||||
## Handle input events, especially for camera panning
|
||||
func _input(event):
|
||||
# Handle middle mouse button for camera panning/reset
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_MIDDLE:
|
||||
if editMode:
|
||||
if event.pressed:
|
||||
# Start panning in edit mode
|
||||
isPanning = true
|
||||
initialPanPosition = event.global_position
|
||||
initialCameraOffset = cameraOffset
|
||||
else:
|
||||
# Stop panning
|
||||
isPanning = false
|
||||
else:
|
||||
if event.pressed:
|
||||
# Reset camera in view mode
|
||||
resetCameraPosition()
|
||||
|
||||
# Handle mouse movement during panning
|
||||
elif editMode and event is InputEventMouseMotion and isPanning:
|
||||
var totalDelta = event.global_position - initialPanPosition
|
||||
|
||||
# Calculate new camera offset from initial position plus total movement
|
||||
cameraOffset = initialCameraOffset - totalDelta
|
||||
|
||||
# Update camera position immediately
|
||||
var s = get_viewport().get_visible_rect().size
|
||||
camera.position = origin.position + cameraOffset
|
||||
|
||||
# Update UI positions
|
||||
controlPanel.position = camera.position + (s/(camera.zoom*2.0))
|
||||
tutorial.position = controlPanel.position
|
||||
editControls.position = camera.position - (s/(camera.zoom*2.0))
|
||||
viewerArrows.position = editControls.position
|
||||
pushUpdates.position.y = controlPanel.position.y
|
||||
pushUpdates.position.x = editControls.position.x
|
||||
|
||||
func followShadow():
|
||||
# Shadow disabled for selected sprites
|
||||
shadow.visible = false
|
||||
@@ -189,14 +232,8 @@ func onWindowSizeChange():
|
||||
lines.position = s*0.5
|
||||
lines.drawLine()
|
||||
|
||||
camera.position = origin.position
|
||||
controlPanel.position = camera.position + (s/(camera.zoom*2.0))
|
||||
tutorial.position = controlPanel.position
|
||||
editControls.position = camera.position - (s/(camera.zoom*2.0))
|
||||
viewerArrows.position = editControls.position
|
||||
updateCameraPosition()
|
||||
spriteList.position.x = s.x - 233
|
||||
pushUpdates.position.y = controlPanel.position.y
|
||||
pushUpdates.position.x = editControls.position.x
|
||||
|
||||
func zoomScene():
|
||||
#Handles Zooming
|
||||
@@ -224,11 +261,43 @@ func changeZoom():
|
||||
pushUpdates.scale = newZoom
|
||||
Global.mouse.scale = newZoom
|
||||
|
||||
# Update UI positions to account for new zoom level
|
||||
# Force update even during panning since zoom affects UI positioning calculations
|
||||
var s = get_viewport().get_visible_rect().size
|
||||
controlPanel.position = camera.position + (s/(camera.zoom*2.0))
|
||||
tutorial.position = controlPanel.position
|
||||
editControls.position = camera.position - (s/(camera.zoom*2.0))
|
||||
viewerArrows.position = editControls.position
|
||||
pushUpdates.position.y = controlPanel.position.y
|
||||
pushUpdates.position.x = editControls.position.x
|
||||
|
||||
$ControlPanel/ZoomLabel.modulate.a = 6.0
|
||||
$ControlPanel/ZoomLabel.text = "Zoom : " + str(scaleOverall) + "%"
|
||||
|
||||
Global.pushUpdate("Set zoom to " + str(scaleOverall) + "%")
|
||||
onWindowSizeChange()
|
||||
|
||||
## Update camera position with current offset
|
||||
func updateCameraPosition():
|
||||
var s = get_viewport().get_visible_rect().size
|
||||
|
||||
# Only update if not currently panning to avoid conflicts
|
||||
if !isPanning:
|
||||
camera.position = origin.position + cameraOffset
|
||||
|
||||
# Update UI element positions relative to new camera position
|
||||
controlPanel.position = camera.position + (s/(camera.zoom*2.0))
|
||||
tutorial.position = controlPanel.position
|
||||
editControls.position = camera.position - (s/(camera.zoom*2.0))
|
||||
viewerArrows.position = editControls.position
|
||||
pushUpdates.position.y = controlPanel.position.y
|
||||
pushUpdates.position.x = editControls.position.x
|
||||
|
||||
## Reset camera position to center
|
||||
func resetCameraPosition():
|
||||
cameraOffset = Vector2.ZERO
|
||||
isPanning = false
|
||||
updateCameraPosition()
|
||||
Global.pushUpdate("Camera reset to center.")
|
||||
|
||||
#When the user speaks!
|
||||
func onSpeak():
|
||||
@@ -243,6 +312,10 @@ func swapMode():
|
||||
editMode = !editMode
|
||||
Global.pushUpdate("Toggled editing mode.")
|
||||
|
||||
# Reset camera when entering view mode
|
||||
if !editMode:
|
||||
resetCameraPosition()
|
||||
|
||||
get_viewport().transparent_bg = !editMode
|
||||
if Global.backgroundColor.a != 0.0:
|
||||
get_viewport().transparent_bg = false
|
||||
@@ -365,6 +438,13 @@ func _on_load_dialog_file_selected(path):
|
||||
Saving.settings["lastAvatar"] = path
|
||||
Global.spriteList.updateData()
|
||||
|
||||
# Update opacity shaders for all loaded sprites after they're all created
|
||||
await get_tree().process_frame # Wait for all sprites to be fully initialized
|
||||
var allSprites = get_tree().get_nodes_in_group("saved")
|
||||
for sprite in allSprites:
|
||||
if sprite.has_method("updateOpacity"):
|
||||
sprite.updateOpacity()
|
||||
|
||||
Global.pushUpdate("Loaded avatar at: " + path)
|
||||
|
||||
onWindowSizeChange()
|
||||
@@ -504,6 +584,10 @@ func _on_duplicate_button_pressed():
|
||||
origin.add_child(sprite)
|
||||
sprite.position = Global.heldSprite.position + Vector2(16,16)
|
||||
|
||||
# Update opacity shader for the duplicated sprite
|
||||
await get_tree().process_frame # Wait for sprite to be fully initialized
|
||||
sprite.updateOpacity()
|
||||
|
||||
Global.heldSprite = sprite
|
||||
|
||||
Global.spriteList.updateData()
|
||||
|
||||
@@ -136,6 +136,11 @@ control={
|
||||
"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":0,"physical_keycode":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
mouse_middle={
|
||||
"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":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":3,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||
]
|
||||
}
|
||||
saveImages={
|
||||
"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":0,"physical_keycode":76,"key_label":0,"unicode":108,"location":0,"echo":false,"script":null)
|
||||
|
||||
@@ -9,7 +9,7 @@ func _ready():
|
||||
Global.mouse = self
|
||||
|
||||
func _process(delta):
|
||||
if Global.main.editMode:
|
||||
if Global.main and Global.main.editMode:
|
||||
if text != "":
|
||||
label.text = text
|
||||
visible = true
|
||||
|
||||
@@ -269,7 +269,7 @@ func setZIndex():
|
||||
sprite.z_index = z
|
||||
|
||||
func talkBlink():
|
||||
var faded = 0.2 * int(Global.main.editMode)
|
||||
var faded = 0.2 * int(Global.main and 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)
|
||||
|
||||
Reference in New Issue
Block a user