- Add buildStylisticFilterChain() with authentic decade-based effects - Implement 8mm Film (1960s-80s home movies) with fine grain and gate weave - Implement 16mm Film (professional/educational) with higher quality and scratches - Implement B&W Film with proper silver halide characteristics and halation - Implement Silent Film (1920s) with 18fps, sepia, and heavy grain/jitter - Implement VHS effects across decades with chroma bleeding and tracking errors - Implement 70s/80s/90s video era characteristics - Implement Webcam (early 2000s) low-res compression artifacts - Add CRT scanline simulation and interlacing options - Use FFmpeg filters with technical accuracy for film restoration workflows All effects are based on authentic technical specifications rather than artistic filters to maintain VideoTools as a serious video processing tool.
679 lines
22 KiB
Go
679 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/dialog"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/widget"
|
|
|
|
"git.leaktechnologies.dev/stu/VideoTools/internal/ui"
|
|
)
|
|
|
|
func (s *appState) showFiltersView() {
|
|
s.stopPreview()
|
|
s.lastModule = s.active
|
|
s.active = "filters"
|
|
s.setContent(buildFiltersView(s))
|
|
}
|
|
|
|
// buildStylisticFilterChain creates FFmpeg filter chains for decade-based stylistic effects
|
|
func buildStylisticFilterChain(state *appState) []string {
|
|
var chain []string
|
|
|
|
switch state.filterStylisticMode {
|
|
case "8mm Film":
|
|
// 8mm/Super 8 film characteristics (1960s-1980s home movies)
|
|
// - Very fine grain structure
|
|
// - Slight color shifts toward warm/cyan
|
|
// - Film gate weave and frame instability
|
|
// - Lower resolution and softer details
|
|
chain = append(chain, "eq=contrast=1.0:saturation=0.9:brightness=0.02") // Slightly desaturated, natural contrast
|
|
chain = append(chain, "unsharp=6:6:0.2:6:6:0.2") // Very soft, film-like
|
|
chain = append(chain, "scale=iw*0.8:ih*0.8:flags=lanczos") // Lower resolution
|
|
chain = append(chain, "fftnorm=nor=0.08:Links=0") // Subtle film grain
|
|
|
|
if state.filterTapeNoise > 0 {
|
|
// Film grain with proper frequency
|
|
grain := fmt.Sprintf("fftnorm=nor=%.2f:Links=0", state.filterTapeNoise*0.1)
|
|
chain = append(chain, grain)
|
|
}
|
|
|
|
// Subtle frame weave (film movement in gate)
|
|
if state.filterTrackingError > 0 {
|
|
weave := fmt.Sprintf("crop='iw-mod(iw*%f/200,1)':'ih-mod(ih*%f/200,1)':%f:%f",
|
|
state.filterTrackingError, state.filterTrackingError*0.5,
|
|
state.filterTrackingError*2, state.filterTrackingError)
|
|
chain = append(chain, weave)
|
|
}
|
|
|
|
case "16mm Film":
|
|
// 16mm film characteristics (professional/educational films 1930s-1990s)
|
|
// - Higher resolution than 8mm but still grainy
|
|
// - More accurate color response
|
|
// - Film scratches and dust (age-dependent)
|
|
// - Stable but still organic movement
|
|
chain = append(chain, "eq=contrast=1.05:saturation=1.0:brightness=0.0") // Natural contrast
|
|
chain = append(chain, "unsharp=5:5:0.4:5:5:0.4") // Slightly sharper than 8mm
|
|
chain = append(chain, "scale=iw*0.9:ih*0.9:flags=lanczos") // Moderate resolution
|
|
chain = append(chain, "fftnorm=nor=0.06:Links=0") // Fine grain
|
|
|
|
if state.filterTapeNoise > 0 {
|
|
grain := fmt.Sprintf("fftnorm=nor=%.2f:Links=0", state.filterTapeNoise*0.08)
|
|
chain = append(chain, grain)
|
|
}
|
|
|
|
if state.filterDropout > 0 {
|
|
// Occasional film scratches
|
|
scratches := int(state.filterDropout * 5) // Max 5 scratches
|
|
if scratches > 0 {
|
|
chain = append(chain, "geq=lum=lum:cb=cb:cr=cr,boxblur=1:1:cr=0:ar=1")
|
|
}
|
|
}
|
|
|
|
case "B&W Film":
|
|
// Black and white film characteristics (various eras)
|
|
// - Rich tonal range with silver halide characteristics
|
|
// - Film grain in luminance only
|
|
// - High contrast potential
|
|
// - No color bleeding, but potential for halation
|
|
chain = append(chain, "colorchannelmixer=.299:.587:.114:0:.299:.587:.114:0:.299:.587:.114") // True B&W conversion
|
|
chain = append(chain, "eq=contrast=1.1:brightness=-0.02") // Higher contrast for B&W
|
|
chain = append(chain, "unsharp=4:4:0.3:4:4:0.3") // Moderate sharpness
|
|
chain = append(chain, "fftnorm=nor=0.05:Links=0") // Film grain
|
|
|
|
// Add subtle halation effect (bright edge bleed)
|
|
if state.filterColorBleeding {
|
|
chain = append(chain, "unsharp=7:7:0.8:7:7:0.8") // Glow effect for highlights
|
|
}
|
|
|
|
case "Silent Film":
|
|
// 1920s silent film characteristics
|
|
// - Very low frame rate (16-22 fps)
|
|
// - Sepia or B&W toning
|
|
// - Film grain with age-related deterioration
|
|
// - Frame jitter and instability
|
|
chain = append(chain, "framerate=18") // Classic silent film speed
|
|
chain = append(chain, "colorchannelmixer=.393:.769:.189:0:.393:.769:.189:0:.393:.769:.189") // Sepia tone
|
|
chain = append(chain, "eq=contrast=1.15:brightness=0.05") // High contrast, slightly bright
|
|
chain = append(chain, "unsharp=8:8:0.1:8:8:0.1") // Very soft, aged film look
|
|
chain = append(chain, "fftnorm=nor=0.12:Links=0") // Heavy grain
|
|
|
|
// Pronounced frame instability
|
|
if state.filterTrackingError > 0 {
|
|
jitter := fmt.Sprintf("crop='iw-mod(iw*%f/100,2)':'ih-mod(ih*%f/100,2)':%f:%f",
|
|
state.filterTrackingError*3, state.filterTrackingError*1.5,
|
|
state.filterTrackingError*5, state.filterTrackingError*2)
|
|
chain = append(chain, jitter)
|
|
}
|
|
|
|
case "70s":
|
|
// 1970s film/video characteristics
|
|
// - Lower resolution, softer images
|
|
// - Warmer color temperature, faded colors
|
|
// - Film grain (if film) or early video noise
|
|
// - Slight color shifts common in analog processing
|
|
chain = append(chain, "eq=contrast=0.95:saturation=0.85:brightness=0.05") // Slightly washed out
|
|
chain = append(chain, "unsharp=5:5:0.3:5:5:0.3") // Soften
|
|
chain = append(chain, "fftnorm=nor=0.15:Links=0") // Subtle noise
|
|
if state.filterChromaNoise > 0 {
|
|
noise := fmt.Sprintf("fftnorm=nor=%.2f:Links=0", state.filterChromaNoise*0.2)
|
|
chain = append(chain, noise)
|
|
}
|
|
|
|
case "80s":
|
|
// 1980s video characteristics
|
|
// - Early home video camcorders (VHS, Betamax)
|
|
// - More pronounced color bleeding
|
|
// - Noticeable video noise and artifacts
|
|
// - Stronger contrast, vibrant colors
|
|
chain = append(chain, "eq=contrast=1.1:saturation=1.2:brightness=0.02") // Enhanced contrast/saturation
|
|
chain = append(chain, "unsharp=3:3:0.4:3:3:0.4") // Moderate sharpening (80s video look)
|
|
chain = append(chain, "fftnorm=nor=0.2:Links=0") // Moderate noise
|
|
|
|
if state.filterColorBleeding {
|
|
// Simulate chroma bleeding common in 80s video
|
|
chain = append(chain, "format=yuv420p,scale=iw+2:ih+2:flags=neighbor,crop=iw:ih")
|
|
}
|
|
|
|
if state.filterChromaNoise > 0 {
|
|
noise := fmt.Sprintf("fftnorm=nor=%.2f:Links=0", state.filterChromaNoise*0.3)
|
|
chain = append(chain, noise)
|
|
}
|
|
|
|
case "90s":
|
|
// 1990s video characteristics
|
|
// - Improved VHS quality, early digital video
|
|
// - Less color bleeding but still present
|
|
// - Better resolution but still analog artifacts
|
|
// - More stable but with tape noise
|
|
chain = append(chain, "eq=contrast=1.05:saturation=1.1:brightness=0.0") // Slight enhancement
|
|
chain = append(chain, "unsharp=3:3:0.5:3:3:0.5") // Light sharpening
|
|
chain = append(chain, "fftnorm=nor=0.1:Links=0") // Light noise
|
|
|
|
if state.filterTapeNoise > 0 {
|
|
// Magnetic tape noise simulation
|
|
noise := fmt.Sprintf("fftnorm=nor=%.2f:Links=0", state.filterTapeNoise*0.15)
|
|
chain = append(chain, noise)
|
|
}
|
|
|
|
case "VHS":
|
|
// General VHS characteristics across decades
|
|
// - Resolution: ~240-320 lines horizontal
|
|
// - Chroma subsampling issues
|
|
// - Tracking errors and dropouts
|
|
// - Scanline artifacts
|
|
chain = append(chain, "eq=contrast=1.08:saturation=1.15:brightness=0.03") // VHS color boost
|
|
chain = append(chain, "unsharp=4:4:0.4:4:4:0.4") // VHS softness
|
|
chain = append(chain, "fftnorm=nor=0.18:Links=0") // VHS noise floor
|
|
|
|
if state.filterColorBleeding {
|
|
// Classic VHS chroma bleeding
|
|
chain = append(chain, "format=yuv420p,scale=iw+4:ih+4:flags=neighbor,crop=iw:ih")
|
|
}
|
|
|
|
if state.filterTrackingError > 0 {
|
|
// Simulate tracking errors (slight image shifts/stutters)
|
|
errorLevel := state.filterTrackingError * 2.0
|
|
wobble := fmt.Sprintf("crop='iw-mod(iw*%f/100,2)':'ih-mod(ih*%f/100,2)':%f:%f",
|
|
errorLevel, errorLevel/2, errorLevel/2, errorLevel/4)
|
|
chain = append(chain, wobble)
|
|
}
|
|
|
|
if state.filterDropout > 0 {
|
|
// Tape dropout effect (random horizontal lines)
|
|
dropoutLevel := int(state.filterDropout * 20) // 0-20 dropouts max
|
|
if dropoutLevel > 0 {
|
|
chain = append(chain, fmt.Sprintf("geq=lum=lum:cb=cb:cr=cr,sendcmd=f=%d:'drawbox w=iw h=2 y=%f:color=black@1:t=fill',drawbox w=iw h=2 y=%f:color=black@1:t=fill'",
|
|
dropoutLevel, 100.0, 200.0))
|
|
}
|
|
}
|
|
|
|
case "Webcam":
|
|
// Early 2000s webcam characteristics
|
|
// - Low resolution (320x240, 640x480)
|
|
// - High compression artifacts
|
|
// - Poor low-light performance
|
|
// - Frame rate issues
|
|
chain = append(chain, "eq=contrast=1.15:saturation=0.9:brightness=-0.05") // Webcam contrast boost, desaturation
|
|
chain = append(chain, "scale=640:480:flags=neighbor") // Typical low resolution
|
|
chain = append(chain, "unsharp=2:2:0.8:2:2:0.8") // Over-sharpened (common in webcams)
|
|
chain = append(chain, "fftnorm=nor=0.25:Links=0") // High compression noise
|
|
|
|
if state.filterChromaNoise > 0 {
|
|
// Webcam compression artifacts
|
|
noise := fmt.Sprintf("fftnorm=nor=%.2f:Links=0", state.filterChromaNoise*0.4)
|
|
chain = append(chain, noise)
|
|
}
|
|
}
|
|
|
|
// Add scanlines if enabled (across all modes)
|
|
if state.filterScanlines {
|
|
// CRT scanline simulation
|
|
scanlineFilter := "format=yuv420p,scale=ih*2/3:ih:flags=neighbor,setsar=1,scale=ih*3/2:ih"
|
|
chain = append(chain, scanlineFilter)
|
|
}
|
|
|
|
// Add interlacing if specified
|
|
switch state.filterInterlacing {
|
|
case "Interlaced":
|
|
// Add interlacing artifacts
|
|
chain = append(chain, "interlace=scan=tff:lowpass=1")
|
|
case "Progressive":
|
|
// Ensure progressive output
|
|
chain = append(chain, "yadif=0:-1:0")
|
|
}
|
|
|
|
return chain
|
|
}
|
|
|
|
func buildFiltersView(state *appState) fyne.CanvasObject {
|
|
filtersColor := moduleColor("filters")
|
|
|
|
// Back button
|
|
backBtn := widget.NewButton("< FILTERS", func() {
|
|
state.showMainMenu()
|
|
})
|
|
backBtn.Importance = widget.LowImportance
|
|
|
|
// Queue button
|
|
queueBtn := widget.NewButton("View Queue", func() {
|
|
state.showQueue()
|
|
})
|
|
state.queueBtn = queueBtn
|
|
state.updateQueueButtonLabel()
|
|
|
|
clearCompletedBtn := widget.NewButton("⌫", func() {
|
|
state.clearCompletedJobs()
|
|
})
|
|
clearCompletedBtn.Importance = widget.LowImportance
|
|
|
|
// Top bar with module color
|
|
topBar := ui.TintedBar(filtersColor, container.NewHBox(backBtn, layout.NewSpacer(), clearCompletedBtn, queueBtn))
|
|
bottomBar := moduleFooter(filtersColor, layout.NewSpacer(), state.statsBar)
|
|
|
|
// Instructions
|
|
instructions := widget.NewLabel("Apply filters and color corrections to your video. Preview changes in real-time.")
|
|
instructions.Wrapping = fyne.TextWrapWord
|
|
instructions.Alignment = fyne.TextAlignCenter
|
|
|
|
// Initialize state defaults
|
|
if state.filterBrightness == 0 && state.filterContrast == 0 && state.filterSaturation == 0 {
|
|
state.filterBrightness = 0.0 // -1.0 to 1.0
|
|
state.filterContrast = 1.0 // 0.0 to 3.0
|
|
state.filterSaturation = 1.0 // 0.0 to 3.0
|
|
state.filterSharpness = 0.0 // 0.0 to 5.0
|
|
state.filterDenoise = 0.0 // 0.0 to 10.0
|
|
}
|
|
if state.filterInterpPreset == "" {
|
|
state.filterInterpPreset = "Balanced"
|
|
}
|
|
if state.filterInterpFPS == "" {
|
|
state.filterInterpFPS = "60"
|
|
}
|
|
|
|
buildFilterChain := func() {
|
|
var chain []string
|
|
|
|
// Add basic color correction/enhancement first
|
|
if state.filterBrightness != 0 || state.filterContrast != 1.0 || state.filterSaturation != 1.0 {
|
|
eqFilter := fmt.Sprintf("eq=brightness=%.2f:contrast=%.2f:saturation=%.2f",
|
|
state.filterBrightness, state.filterContrast, state.filterSaturation)
|
|
chain = append(chain, eqFilter)
|
|
}
|
|
|
|
if state.filterSharpness != 0.5 {
|
|
sharpenFilter := fmt.Sprintf("unsharp=5:5:%.1f:5:5:%.1f", state.filterSharpness, state.filterSharpness)
|
|
chain = append(chain, sharpenFilter)
|
|
}
|
|
|
|
if state.filterDenoise != 0 {
|
|
denoiseFilter := fmt.Sprintf("hqdn3d=%.1f:%.1f:%.1f:%.1f",
|
|
state.filterDenoise, state.filterDenoise, state.filterDenoise, state.filterDenoise)
|
|
chain = append(chain, denoiseFilter)
|
|
}
|
|
|
|
if state.filterGrayscale {
|
|
chain = append(chain, "colorchannelmixer=.299:.587:.114:0:.299:.587:.114:0:.299:.587:.114")
|
|
}
|
|
|
|
// Add stylistic effects after basic corrections
|
|
if state.filterStylisticMode != "None" && state.filterStylisticMode != "" {
|
|
stylisticChain := buildStylisticFilterChain(state)
|
|
chain = append(chain, stylisticChain...)
|
|
}
|
|
|
|
// Add geometric transforms
|
|
if state.filterFlipH || state.filterFlipV {
|
|
var transform string
|
|
if state.filterFlipH && state.filterFlipV {
|
|
transform = "hflip,vflip"
|
|
} else if state.filterFlipH {
|
|
transform = "hflip"
|
|
} else {
|
|
transform = "vflip"
|
|
}
|
|
chain = append(chain, transform)
|
|
}
|
|
|
|
if state.filterRotation != 0 {
|
|
rotateFilter := fmt.Sprintf("rotate=%d*PI/180", state.filterRotation)
|
|
chain = append(chain, rotateFilter)
|
|
}
|
|
|
|
// Add frame interpolation last
|
|
if state.filterInterpEnabled {
|
|
fps := state.filterInterpFPS
|
|
if fps == "" {
|
|
fps = "60"
|
|
}
|
|
var filter string
|
|
switch state.filterInterpPreset {
|
|
case "Ultra Fast":
|
|
filter = fmt.Sprintf("minterpolate=fps=%s:mi_mode=blend", fps)
|
|
case "Fast":
|
|
filter = fmt.Sprintf("minterpolate=fps=%s:mi_mode=duplicate", fps)
|
|
case "High Quality":
|
|
filter = fmt.Sprintf("minterpolate=fps=%s:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1:search_param=32", fps)
|
|
case "Maximum Quality":
|
|
filter = fmt.Sprintf("minterpolate=fps=%s:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1:search_param=64", fps)
|
|
default: // Balanced
|
|
filter = fmt.Sprintf("minterpolate=fps=%s:mi_mode=mci:mc_mode=obmc:me_mode=bidir:me=epzs:search_param=16:vsbmc=0", fps)
|
|
}
|
|
chain = append(chain, filter)
|
|
}
|
|
|
|
state.filterActiveChain = chain
|
|
}
|
|
|
|
// File label
|
|
fileLabel := widget.NewLabel("No file loaded")
|
|
fileLabel.TextStyle = fyne.TextStyle{Bold: true}
|
|
|
|
var videoContainer fyne.CanvasObject
|
|
if state.filtersFile != nil {
|
|
fileLabel.SetText(fmt.Sprintf("File: %s", filepath.Base(state.filtersFile.Path)))
|
|
videoContainer = buildVideoPane(state, fyne.NewSize(480, 270), state.filtersFile, nil)
|
|
} else {
|
|
videoContainer = container.NewCenter(widget.NewLabel("No video loaded"))
|
|
}
|
|
|
|
// Load button
|
|
loadBtn := widget.NewButton("Load Video", func() {
|
|
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
|
if err != nil || reader == nil {
|
|
return
|
|
}
|
|
defer reader.Close()
|
|
|
|
path := reader.URI().Path()
|
|
go func() {
|
|
src, err := probeVideo(path)
|
|
if err != nil {
|
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
|
dialog.ShowError(err, state.window)
|
|
}, false)
|
|
return
|
|
}
|
|
|
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
|
state.filtersFile = src
|
|
state.showFiltersView()
|
|
}, false)
|
|
}()
|
|
}, state.window)
|
|
})
|
|
loadBtn.Importance = widget.HighImportance
|
|
|
|
// Navigation to Upscale module
|
|
upscaleNavBtn := widget.NewButton("Send to Upscale →", func() {
|
|
if state.filtersFile != nil {
|
|
state.upscaleFile = state.filtersFile
|
|
buildFilterChain()
|
|
state.upscaleFilterChain = append([]string{}, state.filterActiveChain...)
|
|
}
|
|
state.showUpscaleView()
|
|
})
|
|
|
|
// Color Correction Section
|
|
brightnessSlider := widget.NewSlider(-1.0, 1.0)
|
|
brightnessSlider.SetValue(state.filterBrightness)
|
|
brightnessSlider.OnChanged = func(f float64) {
|
|
state.filterBrightness = f
|
|
buildFilterChain()
|
|
}
|
|
|
|
contrastSlider := widget.NewSlider(0.0, 3.0)
|
|
contrastSlider.SetValue(state.filterContrast)
|
|
contrastSlider.OnChanged = func(f float64) {
|
|
state.filterContrast = f
|
|
buildFilterChain()
|
|
}
|
|
|
|
saturationSlider := widget.NewSlider(0.0, 3.0)
|
|
saturationSlider.SetValue(state.filterSaturation)
|
|
saturationSlider.OnChanged = func(f float64) {
|
|
state.filterSaturation = f
|
|
buildFilterChain()
|
|
}
|
|
|
|
colorSection := widget.NewCard("Color Correction", "", container.NewVBox(
|
|
widget.NewLabel("Adjust brightness, contrast, and saturation"),
|
|
container.NewGridWithColumns(2,
|
|
widget.NewLabel("Brightness:"),
|
|
brightnessSlider,
|
|
widget.NewLabel("Contrast:"),
|
|
contrastSlider,
|
|
widget.NewLabel("Saturation:"),
|
|
saturationSlider,
|
|
),
|
|
))
|
|
|
|
// Enhancement Section
|
|
sharpnessSlider := widget.NewSlider(0.0, 5.0)
|
|
sharpnessSlider.SetValue(state.filterSharpness)
|
|
sharpnessSlider.OnChanged = func(f float64) {
|
|
state.filterSharpness = f
|
|
buildFilterChain()
|
|
}
|
|
|
|
denoiseSlider := widget.NewSlider(0.0, 10.0)
|
|
denoiseSlider.SetValue(state.filterDenoise)
|
|
denoiseSlider.OnChanged = func(f float64) {
|
|
state.filterDenoise = f
|
|
buildFilterChain()
|
|
}
|
|
|
|
enhanceSection := widget.NewCard("Enhancement", "", container.NewVBox(
|
|
widget.NewLabel("Sharpen, blur, and denoise"),
|
|
container.NewGridWithColumns(2,
|
|
widget.NewLabel("Sharpness:"),
|
|
sharpnessSlider,
|
|
widget.NewLabel("Denoise:"),
|
|
denoiseSlider,
|
|
),
|
|
))
|
|
|
|
// Transform Section
|
|
rotationSelect := widget.NewSelect([]string{"0°", "90°", "180°", "270°"}, func(s string) {
|
|
switch s {
|
|
case "90°":
|
|
state.filterRotation = 90
|
|
case "180°":
|
|
state.filterRotation = 180
|
|
case "270°":
|
|
state.filterRotation = 270
|
|
default:
|
|
state.filterRotation = 0
|
|
}
|
|
buildFilterChain()
|
|
})
|
|
|
|
var rotationStr string
|
|
switch state.filterRotation {
|
|
case 90:
|
|
rotationStr = "90°"
|
|
case 180:
|
|
rotationStr = "180°"
|
|
case 270:
|
|
rotationStr = "270°"
|
|
default:
|
|
rotationStr = "0°"
|
|
}
|
|
rotationSelect.SetSelected(rotationStr)
|
|
|
|
flipHCheck := widget.NewCheck("", func(b bool) {
|
|
state.filterFlipH = b
|
|
buildFilterChain()
|
|
})
|
|
flipHCheck.SetChecked(state.filterFlipH)
|
|
|
|
flipVCheck := widget.NewCheck("", func(b bool) {
|
|
state.filterFlipV = b
|
|
buildFilterChain()
|
|
})
|
|
flipVCheck.SetChecked(state.filterFlipV)
|
|
|
|
transformSection := widget.NewCard("Transform", "", container.NewVBox(
|
|
widget.NewLabel("Rotate and flip video"),
|
|
container.NewGridWithColumns(2,
|
|
widget.NewLabel("Rotation:"),
|
|
rotationSelect,
|
|
widget.NewLabel("Flip Horizontal:"),
|
|
flipHCheck,
|
|
widget.NewLabel("Flip Vertical:"),
|
|
flipVCheck,
|
|
),
|
|
))
|
|
|
|
// Creative Effects Section
|
|
grayscaleCheck := widget.NewCheck("Grayscale", func(b bool) {
|
|
state.filterGrayscale = b
|
|
buildFilterChain()
|
|
})
|
|
grayscaleCheck.SetChecked(state.filterGrayscale)
|
|
|
|
creativeSection := widget.NewCard("Creative Effects", "", container.NewVBox(
|
|
widget.NewLabel("Apply artistic effects"),
|
|
grayscaleCheck,
|
|
))
|
|
|
|
// Stylistic Effects Section
|
|
stylisticModeSelect := widget.NewSelect([]string{"None", "8mm Film", "16mm Film", "B&W Film", "Silent Film", "70s", "80s", "90s", "VHS", "Webcam"}, func(s string) {
|
|
state.filterStylisticMode = s
|
|
buildFilterChain()
|
|
})
|
|
stylisticModeSelect.SetSelected(state.filterStylisticMode)
|
|
|
|
scanlinesCheck := widget.NewCheck("CRT Scanlines", func(b bool) {
|
|
state.filterScanlines = b
|
|
buildFilterChain()
|
|
})
|
|
scanlinesCheck.SetChecked(state.filterScanlines)
|
|
|
|
chromaNoiseSlider := widget.NewSlider(0.0, 1.0)
|
|
chromaNoiseSlider.SetValue(state.filterChromaNoise)
|
|
chromaNoiseSlider.OnChanged = func(f float64) {
|
|
state.filterChromaNoise = f
|
|
buildFilterChain()
|
|
}
|
|
|
|
colorBleedingCheck := widget.NewCheck("Color Bleeding", func(b bool) {
|
|
state.filterColorBleeding = b
|
|
buildFilterChain()
|
|
})
|
|
colorBleedingCheck.SetChecked(state.filterColorBleeding)
|
|
|
|
tapeNoiseSlider := widget.NewSlider(0.0, 1.0)
|
|
tapeNoiseSlider.SetValue(state.filterTapeNoise)
|
|
tapeNoiseSlider.OnChanged = func(f float64) {
|
|
state.filterTapeNoise = f
|
|
buildFilterChain()
|
|
}
|
|
|
|
trackingErrorSlider := widget.NewSlider(0.0, 1.0)
|
|
trackingErrorSlider.SetValue(state.filterTrackingError)
|
|
trackingErrorSlider.OnChanged = func(f float64) {
|
|
state.filterTrackingError = f
|
|
buildFilterChain()
|
|
}
|
|
|
|
dropoutSlider := widget.NewSlider(0.0, 1.0)
|
|
dropoutSlider.SetValue(state.filterDropout)
|
|
dropoutSlider.OnChanged = func(f float64) {
|
|
state.filterDropout = f
|
|
buildFilterChain()
|
|
}
|
|
|
|
interlacingSelect := widget.NewSelect([]string{"None", "Progressive", "Interlaced"}, func(s string) {
|
|
state.filterInterlacing = s
|
|
buildFilterChain()
|
|
})
|
|
interlacingSelect.SetSelected(state.filterInterlacing)
|
|
|
|
stylisticSection := widget.NewCard("Stylistic Effects", "", container.NewVBox(
|
|
widget.NewLabel("Authentic decade-based video effects"),
|
|
container.NewGridWithColumns(2,
|
|
widget.NewLabel("Era Mode:"),
|
|
stylisticModeSelect,
|
|
widget.NewLabel("Interlacing:"),
|
|
interlacingSelect,
|
|
),
|
|
scanlinesCheck,
|
|
widget.NewSeparator(),
|
|
container.NewGridWithColumns(2,
|
|
widget.NewLabel("Chroma Noise:"),
|
|
chromaNoiseSlider,
|
|
widget.NewLabel("Tape Noise:"),
|
|
tapeNoiseSlider,
|
|
widget.NewLabel("Tracking Error:"),
|
|
trackingErrorSlider,
|
|
widget.NewLabel("Tape Dropout:"),
|
|
dropoutSlider,
|
|
),
|
|
colorBleedingCheck,
|
|
))
|
|
|
|
// Frame Interpolation Section
|
|
interpEnabledCheck := widget.NewCheck("Enable Frame Interpolation", func(checked bool) {
|
|
state.filterInterpEnabled = checked
|
|
buildFilterChain()
|
|
})
|
|
interpEnabledCheck.SetChecked(state.filterInterpEnabled)
|
|
|
|
interpPresetSelect := widget.NewSelect([]string{"Ultra Fast", "Fast", "Balanced", "High Quality", "Maximum Quality"}, func(val string) {
|
|
state.filterInterpPreset = val
|
|
buildFilterChain()
|
|
})
|
|
interpPresetSelect.SetSelected(state.filterInterpPreset)
|
|
|
|
interpFPSSelect := widget.NewSelect([]string{"24", "30", "50", "59.94", "60"}, func(val string) {
|
|
state.filterInterpFPS = val
|
|
buildFilterChain()
|
|
})
|
|
interpFPSSelect.SetSelected(state.filterInterpFPS)
|
|
|
|
interpHint := widget.NewLabel("Balanced preset is recommended; higher presets are CPU-intensive.")
|
|
interpHint.TextStyle = fyne.TextStyle{Italic: true}
|
|
interpHint.Wrapping = fyne.TextWrapWord
|
|
|
|
interpSection := widget.NewCard("Frame Interpolation (Minterpolate)", "", container.NewVBox(
|
|
widget.NewLabel("Generate smoother motion by interpolating new frames"),
|
|
interpEnabledCheck,
|
|
container.NewGridWithColumns(2,
|
|
widget.NewLabel("Preset:"),
|
|
interpPresetSelect,
|
|
widget.NewLabel("Target FPS:"),
|
|
interpFPSSelect,
|
|
),
|
|
interpHint,
|
|
))
|
|
buildFilterChain()
|
|
|
|
// Apply button
|
|
applyBtn := widget.NewButton("Apply Filters", func() {
|
|
if state.filtersFile == nil {
|
|
dialog.ShowInformation("No Video", "Please load a video first.", state.window)
|
|
return
|
|
}
|
|
buildFilterChain()
|
|
dialog.ShowInformation("Filters", "Filters are now configured and will be applied when sent to Upscale.", state.window)
|
|
})
|
|
applyBtn.Importance = widget.HighImportance
|
|
|
|
// Main content
|
|
leftPanel := container.NewVBox(
|
|
instructions,
|
|
widget.NewSeparator(),
|
|
fileLabel,
|
|
loadBtn,
|
|
upscaleNavBtn,
|
|
)
|
|
|
|
settingsPanel := container.NewVBox(
|
|
colorSection,
|
|
enhanceSection,
|
|
transformSection,
|
|
interpSection,
|
|
creativeSection,
|
|
stylisticSection,
|
|
applyBtn,
|
|
)
|
|
|
|
settingsScroll := container.NewVScroll(settingsPanel)
|
|
// Adaptive height for small screens - allow content to flow
|
|
settingsScroll.SetMinSize(fyne.NewSize(350, 400))
|
|
|
|
mainContent := container.New(&fixedHSplitLayout{ratio: 0.6},
|
|
container.NewVBox(leftPanel, container.NewCenter(videoContainer)),
|
|
settingsScroll,
|
|
)
|
|
|
|
content := container.NewPadded(mainContent)
|
|
|
|
return container.NewBorder(topBar, bottomBar, nil, nil, content)
|
|
}
|