diff --git a/filters_module.go b/filters_module.go index 63af8b9..0dc856d 100644 --- a/filters_module.go +++ b/filters_module.go @@ -20,6 +20,216 @@ func (s *appState) showFiltersView() { 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") @@ -67,6 +277,54 @@ func buildFiltersView(state *appState) fyne.CanvasObject { 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 == "" { @@ -87,6 +345,7 @@ func buildFiltersView(state *appState) fyne.CanvasObject { } chain = append(chain, filter) } + state.filterActiveChain = chain } @@ -140,46 +399,202 @@ func buildFiltersView(state *appState) fyne.CanvasObject { }) // 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:"), - widget.NewSlider(-1.0, 1.0), + brightnessSlider, widget.NewLabel("Contrast:"), - widget.NewSlider(0.0, 3.0), + contrastSlider, widget.NewLabel("Saturation:"), - widget.NewSlider(0.0, 3.0), + 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:"), - widget.NewSlider(0.0, 5.0), + sharpnessSlider, widget.NewLabel("Denoise:"), - widget.NewSlider(0.0, 10.0), + 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:"), - widget.NewSelect([]string{"0°", "90°", "180°", "270°"}, func(s string) {}), + rotationSelect, widget.NewLabel("Flip Horizontal:"), - widget.NewCheck("", func(b bool) { state.filterFlipH = b }), + flipHCheck, widget.NewLabel("Flip Vertical:"), - widget.NewCheck("", func(b bool) { state.filterFlipV = b }), + 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"), - widget.NewCheck("Grayscale", func(b bool) { state.filterGrayscale = b }), + 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 @@ -244,6 +659,7 @@ func buildFiltersView(state *appState) fyne.CanvasObject { transformSection, interpSection, creativeSection, + stylisticSection, applyBtn, )