Files
PVS/video_synth/patterns3.py

812 lines
37 KiB
Python

import numpy as np
import cv2
import time
from enum import Enum
import random
import noise # For Perlin noise generation
from param import ParamTable, Param
from config import params
from generators import Oscillator
from sliders import TrackbarCallback, TrackbarRow2
import dearpygui.dearpygui as dpg
posc_bank = []
# --- BarMode Enum ---
class BarMode(Enum):
"""Enumeration for different bar animation modes."""
GROWING_SPACING = 0
FIXED_SCROLL = 1
def __str__(self):
return self.name.replace('_', ' ').title()
# --- PatternType Enum ---
class PatternType(Enum):
"""Enumeration for different visual pattern types."""
NONE = 0
BARS = 1
WAVES = 2
CHECKERS = 3
RADIAL = 4
PERLIN_BLOBS = 5
FRACTAL_SINE = 6 # (sum of sines)
XY_BARS = 7
def __str__(self):
return self.name.replace('_', ' ').title()
# --- PatternGenerator Class ---
class Patterns:
"""
Generates various animated patterns using OpenCV and modulates them
with a bank of Oscillators.
"""
def __init__(self, width, height):
"""
Initializes the PatternGenerator.
Args:
width (int): Width of the generated pattern image.
height (int): Height of the generated pattern image.
posc_bank (list): A list of Oscillator instances to use for modulation.
params_table (ParamTable): An instance of ParamTable to register pattern-specific parameters.
"""
self.width = width
self.height = height
# Create coordinate grids for vectorized operations (much faster than loops)
# These are pre-calculated as they don't change per frame
self.x_coords = np.linspace(0, self.width - 1, self.width, dtype=np.float32)
self.y_coords = np.linspace(0, self.height - 1, self.height, dtype=np.float32)
self.X, self.Y = np.meshgrid(self.x_coords, self.y_coords)
# Define pattern-specific parameters (using the global params_table)
self._pattern_type = params.add("pattern_type", PatternType.NONE.value, len(PatternType)-1, 0)
self.prev_pattern_type = self._pattern_type.value
self.pattern_speed = params.add("pattern_speed", 0.1, 10.0, 1.0)
self.use_fractal = params.add("pattern_use_fractal", 0, 1, 0) # Toggle for fractal noise
self.octaves = params.add("pattern_octaves", 1, 8, 4) # Number of octaves for fractal noise
self.gain = params.add("pattern_gain", 0.0, 1.0, 0.2) # Gain for fractal noise
self.lacunarity = params.add("pattern_lacunarity", 1.0, 4.0, 2.0) # Lacunarity for fractal noise
# controls density of bars
self.bar_x_freq = params.add("bar_x_freq", 0.01, 2, 0.1, family="Bars")
self.bar_y_freq = params.add("bar_y_freq", 0.01, 2, 0.1, family="Bars")
# controls bar scrolling speed
self.bar_x_offset = params.add("bar_x_offset", -100, 100, 2.0, family="Bars") # Offset for X bars
self.bar_y_offset = params.add("bar_y_offset", -10, 10, 1.0, family="Bars") # Offset for Y bars
# creates interesting patterns
self.mod = params.add("pattern_mod", 0.1, 2.0, 1.0, family="Bars") # Modulation factor for bar patterns
# Color parameters for patterns # TODO: mapping seems off
self.r = params.add("pattern_r", 0, 180, 127, family="Pattern Colors")
self.g = params.add("pattern_g", 0, 180, 127, family="Pattern Colors")
self.b = params.add("pattern_b", 0, 180, 127, family="Pattern Colors")
self.grid_size = params.add("pattern_grid_size", 10, 100, 30, family="Checkers") # Base grid size for checkers
self.color_shift = params.add("pattern_color_shift", 0, 255, 127, family="Checkers")
self.color_blend = params.add("pattern_color_blend", 0, 255, 127, family="Checkers")
self.pattern_distance = params.add("pattern_distance", 0.1, 10.0, 5.0)
self.wave_freq_x = params.add("pattern_wave_freq_x", 0.0, 100, 0.05, family="Waves")
self.wave_freq_y = params.add("pattern_wave_freq_y", 0.0, 100, 0.05, family="Waves")
self.brightness = params.add("pattern_brightness", 0.0, 100.0, 50.0, family="Waves")
self.radial_freq = params.add("pattern_radial_freq", 1, 100, 30) # Angle amount for polar warp
self.angular_freq = params.add("pattern_angular_freq", 1, 40, 1) # Radius amount for polar warp
self.radial_mod = params.add("pattern_radial_mod", 0.1, 10.0, 1.0) # Modulation factor for radial patterns
self.angle_mod = params.add("pattern_angle_mod", 0.1, 10.0, 1.0) # Modulation factor for angle patterns
self.x_hue = params.add("x_hue", 0.0, 1.0, 0.5, family="XY Bars") # Hue for X bars
self.y_hue = params.add("y_hue", 0.0, 1.0, 0.5, family="XY Bars") # Hue for Y bars
params.add("pperlin_scale_x", 0.001, 0.05, 0.005, family="Perlin Blobs")
params.add("pperlin_scale_y", 0.001, 0.05, 0.005, family="Perlin Blobs")
params.add("pperlin_octaves", 1, 10, 6, family="Perlin Blobs")
params.add("pperlin_persistence", 0.1, 1.0, 0.5, family="Perlin Blobs")
params.add("pperlin_lacunarity", 1.0, 4.0, 2.0, family="Perlin Blobs")
params.add("pperlin_time_speed", 0.01, 1.0, 0.1, family="Perlin Blobs")
params.add("pfractal_amplitude", 0.5, 5.0, 1.5, family="Fractal Sine")
params.add("pfractal_octaves", 1, 8, 4, family="Fractal Sine")
# Fractal Sine parameters
self.x_perturb = params.add("x_perturb", 0, 50, 25.0, family="Sine")
self.y_perturb = params.add("y_perturb", 0, 50, 25.0, family="Sine")
self.phase_speed = params.add("phase_speed", 0.01, 10.0, 1.0, family="Sine")
self.p1 = params.add("posc0_val", -10, 10, 1.15)
self.p2 = params.add("posc1_val", -10, 10, 1.1)
self.p3 = params.add("posc2_val", -10, 10, 1.1)
self.posc_bank = [] # List to hold Oscillator instances
for i in range(4):
self.posc_bank.append(Oscillator(name=f"posc{i}", frequency=0.5, amplitude=1.0, phase=0.0, shape=0))
self.prev = None
def _normalize_osc_values(self):
"""
Normalizes oscillator values from their raw [-amplitude, +amplitude] range
to a [0, 1] range for easier use in color/modulation.
Returns a tuple of normalized oscillator values.
"""
norm_vals = []
for osc in self.posc_bank:
# Ensure amplitude is not zero to avoid division by zero
amp_val = osc.amplitude.value
if amp_val == 0:
print(f"Warning: Oscillator {osc.name} has zero amplitude. Defaulting to mid-range normalization.\n")
norm_vals.append(0.05) # Default to mid-range if amplitude is zero
else:
# Map from [-amp_val, +amp_val] to [0, 1]
normalized = (osc.value + amp_val) / (2 * amp_val) + 0.001
norm_vals.append(normalized)
print(f"normalized: {norm_vals}")
if self.prev is not None and self.prev == norm_vals:
print(f"previous: {self.prev}\n")
if norm_vals == self.prev:
print("No change in normalized values, returning previous values.")
self.prev = [i-.5 for i in self.prev]
return self.prev
self.prev = norm_vals.copy()
return tuple(norm_vals)
def set_osc_params(self):
if self._pattern_type.value != self.prev_pattern_type:
print(f"Pattern type changed from {self.prev_pattern_type} to {self._pattern_type.value}.")
self.prev_pattern_type = self._pattern_type.value
if self._pattern_type.value == PatternType.BARS.value:
print("PatternType is set to BARS; Linking bar frequency to osc 0.")
self.posc_bank[0].link_param(self.bar_x_offset)
elif self._pattern_type.value == PatternType.WAVES.value:
print("PatternType is set to WAVES; Linking wave frequencies to osc 0, 1.")
self.posc_bank[0].link_param(self.wave_freq_x)
self.posc_bank[1].link_param(self.wave_freq_y)
elif self._pattern_type.value == PatternType.CHECKERS.value:
print("PatternType is set to CHECKERS; Linking grid size to osc 0.")
self.posc_bank[0].link_param(self.grid_size)
self.posc_bank[1].link_param(self.color_shift)
self.posc_bank[2].link_param(self.color_blend)
elif self._pattern_type.value == PatternType.RADIAL.value:
print("PatternType is set to RADIAL; Linking radial parameters to osc 0.")
# self.posc_bank[0].link_param(self.radial_freq)
# self.posc_bank[1].link_param(self.angular_freq)
# self.posc_bank[2].link_param(self.radial_mod)
# self.posc_bank[3].link_param(self.angle_mod)
elif self._pattern_type.value == PatternType.PERLIN_BLOBS.value:
print("PatternType is set to PERLIN BLOBS; Linking Perlin noise parameters to oscillators.")
# Link Perlin noise parameters to oscillators if needed
elif self._pattern_type.value == PatternType.FRACTAL_SINE.value:
print("PatternType is set to FRACTAL SINE; Linking fractal sine parameters to oscillators.")
self.posc_bank[0].link_param(self.bar_x_offset)
self.posc_bank[1].link_param(self.bar_y_offset)
elif self._pattern_type.value == PatternType.XY_BARS.value:
print("PatternType is set to XY_BARS; Linking XY bar parameters to oscillators.")
# self.posc_bank[0].link_param(self.bar_x_freq)
# self.posc_bank[1].link_param(self.bar_y_freq)
self.posc_bank[0].link_param(self.bar_x_offset)
self.posc_bank[1].link_param(self.bar_y_offset)
def generate_pattern_frame(self, frame: np.ndarray):
"""
Generates a single frame of the current pattern.
Args:
frame_time (float): The current time in seconds, used for animation.
Returns:
np.ndarray: The generated pattern image (height x width x 3, uint8 BGR).
"""
# Initialize a black image
pattern = np.zeros((self.height, self.width, 3), dtype=np.uint8)
self.set_osc_params()
posc_vals = [osc.get_next_value() for osc in self.posc_bank if osc.linked_param is not None]
# Dispatch to the appropriate pattern generation function
if self.pattern_type == PatternType.NONE.value:
pass # Returns black image
elif self.pattern_type == PatternType.BARS.value:
pattern = self._generate_bars(pattern, self.X, 0)
elif self.pattern_type == PatternType.WAVES.value:
pattern = self._generate_waves(pattern, self.X, self.Y)
elif self.pattern_type == PatternType.CHECKERS.value:
pattern = self._generate_checkers(pattern, self.X, self.Y)
elif self.pattern_type == PatternType.RADIAL.value:
pattern = self._generate_radial(pattern, self.X, self.Y)
elif self.pattern_type == PatternType.PERLIN_BLOBS.value:
pass
# pattern = self._generate_perlin_blobs(norm_osc_vals, frame_time)
elif self.pattern_type == PatternType.FRACTAL_SINE.value:
pass
# pattern = self._generate_fractal_sine(pattern, self.X, self.Y, norm_osc_vals, frame_time)
elif self.pattern_type == PatternType.XY_BARS.value:
pattern = self._generate_xy_bars(pattern, self.X, self.Y)
else:
print(f"Warning: Unknown pattern type {self._pattern_type}. Returning black frame.")
# return pattern
alpha = 0.5
blended_frame = cv2.addWeighted(frame, 1 - alpha, pattern, alpha, 0)
return blended_frame
@property
def pattern_type(self) -> int:
"""Get the current pattern type."""
return self._pattern_type.value
@pattern_type.setter
def pattern_type(self, value):
"""
Setter for pattern_type to ensure it is a valid PatternType.
"""
print("executing setting")
if isinstance(value, PatternType):
self._pattern_type.value = value
elif isinstance(value, int) and 0 <= value < len(PatternType):
self._pattern_type.value = PatternType(value).value
else:
pass
# raise ValueError(f"Invalid pattern type: {value}. Must be an instance of PatternType or a valid integer index.")
# --- Pattern Generation Methods ---
def _generate_bars(self, pattern: np.ndarray, axis: np.ndarray, osc_idx) -> np.ndarray:
"""
Generates vertical bars that shift color and position based on oscillator values.
Includes modes for growing/spacing and fixed scrolling.
"""
density = self.bar_x_freq.value
offset = self.bar_x_offset.value / 10 # linked to posc 0
mod = self.mod.value # gradient modulation for stripy bar patterns
bar_mod = (np.sin(axis * density + offset) + 1) / mod
# Apply colors based on modulated brightness and other oscillators
# BGR format for OpenCV
blue_channel = (bar_mod * 255 * self.b.value / 255).astype(np.uint8)
green_channel = (bar_mod * 255 * self.g.value / 255).astype(np.uint8)
red_channel = (bar_mod * 255 * self.r.value / 255).astype(np.uint8)
pattern[:, :, 0] = blue_channel
pattern[:, :, 1] = green_channel
pattern[:, :, 2] = red_channel
return pattern
def _generate_xy_bars(self, pattern: np.ndarray, X: np.ndarray, Y: np.ndarray) -> np.ndarray:
"""
Generates a pattern of bars on both X and Y axes, controlled by static parameters.
"""
# Get parameters from ParamTable
x_density = self.bar_x_freq.value
y_density = self.bar_y_freq.value
x_offset = self.bar_x_offset.value / 10 # linked to posc 0
y_offset = self.bar_y_offset.value / 10 # linked to posc 1
color_x_hue = self.x_hue.value
color_y_hue = self.y_hue.value
# Generate X-axis bars
x_bars = (np.sin(X * x_density + x_offset) + 1) / 2 # Range 0-1
# Generate Y-axis bars
y_bars = (np.sin(Y * y_density + y_offset) + 1) / 2 # Range 0-1
# Combine the bar patterns. Multiplying creates a grid-like intersection.
# Adding would create overlapping bars.
combined_bars = x_bars * y_bars # This creates a grid where both X and Y bars are bright
# Apply colors. We can blend colors based on the individual bar patterns
# or apply a single color to the combined pattern.
# Let's try to make X bars one color and Y bars another, with overlap.
# Color for X bars (e.g., more red)
red_x = (combined_bars * 255 * color_x_hue).astype(np.uint8)
green_x = (combined_bars * 255 * (1 - color_x_hue)).astype(np.uint8)
blue_x = np.zeros_like(combined_bars, dtype=np.uint8) # Minimal blue for X
# Color for Y bars (e.g., more blue)
red_y = np.zeros_like(combined_bars, dtype=np.uint8) # Minimal red for Y
green_y = (combined_bars * 255 * (1 - color_y_hue)).astype(np.uint8)
blue_y = (combined_bars * 255 * color_y_hue).astype(np.uint8)
# Combine colors. Additive blending for overlapping effects.
pattern[:, :, 0] = np.clip(blue_x + blue_y, 0, 255) # Blue channel
pattern[:, :, 1] = np.clip(green_x + green_y, 0, 255) # Green channel
pattern[:, :, 2] = np.clip(red_x + red_y, 0, 255) # Red channel
return pattern
def _generate_waves(self, pattern: np.ndarray, X: np.ndarray, Y: np.ndarray) -> np.ndarray:
"""
Generates horizontal/vertical waves that ripple and change color based on oscillators.
"""
# osc0_norm: controls horizontal wave frequency/speed
# osc1_norm: controls vertical wave frequency/speed
# osc2_norm: controls overall brightness/color shift
# Growing/spacing mode: use oscillators to modulate wave frequencies
# freq_x = 0.03 + norm_osc_vals[0] # original is 0.05
# freq_y = 0.03 + norm_osc_vals[1]
freq_x = 0.03 + self.wave_freq_x.value / 5 # original is 0.05
freq_y = 0.03 + self.wave_freq_y.value / 5
# Combine waves and modulate with osc2 for overall brightness/color
# val_x = np.sin(X * freq_x + norm_osc_vals * 1) # 5Horizontal wave
# val_y = np.sin(Y * freq_y + norm_osc_vals[1] * 1) #5 Vertical wave
val_x = np.sin(X * freq_x + self.bar_x_offset.value * .1) # Horizontal wave
val_y = np.sin(Y * freq_y + self.bar_y_offset.value * .1) # Vertical wave
total_val = (val_x + val_y) / 2 # Range -1 to 1
brightness = ((total_val + self.brightness.value) / 2 + 1) / self.mod.value * 255 # Map to 0-255
# Apply color based on brightness and oscillator values
# blue_channel = (brightness * (1 - norm_osc_vals[1])).astype(np.uint8)
# green_channel = (brightness * norm_osc_vals[0]).astype(np.uint8)
# red_channel = (brightness * norm_osc_vals[2]).astype(np.uint8)
blue_channel = (brightness * (1 - self.b.value)).astype(np.uint8)
green_channel = (brightness * self.g.value).astype(np.uint8)
red_channel = (brightness * self.r.value).astype(np.uint8)
pattern[:, :, 0] = blue_channel
pattern[:, :, 1] = green_channel
pattern[:, :, 2] = red_channel
return pattern
def _generate_checkers(self, pattern: np.ndarray, X: np.ndarray, Y: np.ndarray) -> np.ndarray:
"""
Generates a checkerboard pattern whose square size and colors shift.
"""
# osc0_norm: controls grid size
# osc1_norm: controls color blend
# osc2_norm: controls color shift
grid_size_base = 30 # Base size in pixels
grid_size_mod = self.grid_size.value * 40 # Modulation amount
grid_size_x = grid_size_base + grid_size_mod
grid_size_y = grid_size_base + grid_size_mod
# Create checkerboard mask
# Ensure grid_size is at least 1 to prevent division by zero
grid_size_x = max(1, grid_size_x)
grid_size_y = max(1, grid_size_y)
checker_mask = ((X // grid_size_x).astype(int) % 2 == (Y // grid_size_y).astype(int) % 2)
# Define two colors, modulated by oscillators
# color_shift = norm_osc_vals[2] * 255
color_shift = self.color_shift.value # Modulation for color shift
color_blend = self.color_blend.value / 255 # Modulation for color blending
# Color 1: Changes based on osc2 (color_shift)
c1_b = int(color_shift)
c1_g = int(255 - color_shift)
c1_r = int(127 + color_shift / 2)
# Color 2: Inverse or complementary to Color 1, also blended by osc1
c2_b = int((255 - color_shift) * color_blend)
c2_g = int(color_shift * color_blend)
c2_r = int((127 - color_shift / 2) * color_blend)
# Apply colors based on the mask
# Use np.where for efficient assignment
pattern[:, :, 0] = np.where(checker_mask, c1_b, c2_b).astype(np.uint8) # Blue
pattern[:, :, 1] = np.where(checker_mask, c1_g, c2_g).astype(np.uint8) # Green
pattern[:, :, 2] = np.where(checker_mask, c1_r, c2_r).astype(np.uint8) # Red
return pattern
def _generate_radial(self, pattern: np.ndarray, X: np.ndarray, Y: np.ndarray) -> np.ndarray:
"""
Generates a radial pattern that pulsates and rotates based on oscillators.
"""
# osc0_norm: controls radial wave density/pulsation
# osc1_norm: controls angular wave density/rotation speed
# osc2_norm: controls overall color hue
center_x, center_y = self.width / 2, self.height / 2
DX = X - center_x
DY = Y - center_y
distance = np.sqrt(DX**2 + DY**2)
angle = np.arctan2(DY, DX) # Range -pi to pi
# Modulate radial distance and angle with oscillators
radial_freq = 0.05 * self.radial_freq.value * 0.05 # Radial wave frequency
angular_freq = self.angular_freq.value #* 5 # Angular wave frequency
radial_mod = np.sin(distance * radial_freq + self.radial_mod.value * 10) # Radial wave
angle_mod = np.sin(angle * angular_freq + self.angle_mod.value * 5) # Angular wave
# Combine for brightness, modulate color with osc2
brightness_base = ((radial_mod + angle_mod) / 2 + 1) / 2 * 255 # Map to 0-255
# Color mapping with oscillators - ensure final values are arrays
blue_channel = (brightness_base * (1 - self.b.value)).astype(np.uint8)
green_channel = (brightness_base * self.g.value).astype(np.uint8)
red_channel = (brightness_base * ((self.b.value + self.g.value)/2)).astype(np.uint8)
pattern[:, :, 0] = blue_channel
pattern[:, :, 1] = green_channel
pattern[:, :, 2] = red_channel
return pattern
def _generate_perlin_blobs(self, norm_osc_vals: tuple, frame_time: float) -> np.ndarray:
"""
Generates evolving Perlin noise blobs, modulated by oscillators.
"""
pattern = np.zeros((self.height, self.width, 3), dtype=np.uint8)
# Perlin noise parameters from ParamTable, modulated by oscillators
# osc0_norm: modulates X spatial scale
# osc1_norm: modulates Y spatial scale
# osc2_norm: modulates persistence and time evolution speed
scale_x = params.val("pperlin_scale_x") + norm_osc_vals[0] * 0.02
scale_y = params.val("pperlin_scale_y") + norm_osc_vals[1] * 0.02
octaves = int(params.val("pperlin_octaves"))
persistence = params.val("pperlin_persistence") + norm_osc_vals[2] * 0.2
lacunarity = params.val("pperlin_lacunarity")
# Use time as a Z-axis for 3D Perlin noise to ensure continuous evolution
time_speed = params.val("pperlin_time_speed") + norm_osc_vals[2] * 0.2
time_factor = frame_time * time_speed
# Pre-calculate noise for the entire grid for efficiency
# Create a grid of x,y coordinates
x_grid = self.X * scale_x
y_grid = self.Y * scale_y
# Generate 3D Perlin noise for the entire grid
# This is more efficient than looping pixel by pixel for noise generation
noise_val_shape = np.array([
[noise.pnoise3(x_val, y_val, time_factor,
octaves=octaves, persistence=persistence, lacunarity=lacunarity,
repeatx=1024, repeaty=1024, repeatz=1024, base=0)
for x_val, y_val in zip(row, y_row)] for row, y_row in zip(x_grid, y_grid)
])
# Map noise_val_shape from (-1, 1) range to (0, 1)
normalized_noise_val = (noise_val_shape + 1) / 2
# Generate separate noise for color channels, also evolving
noise_val_color_r = np.array([
[noise.pnoise3(x_val * 0.8, y_val * 1.2, time_factor * 0.7,
octaves=4, persistence=0.6, lacunarity=2.2, base=1)
for x_val, y_val in zip(row, y_row)] for row, y_row in zip(x_grid, y_grid)
])
noise_val_color_g = np.array([
[noise.pnoise3(x_val * 1.1, y_val * 0.9, time_factor * 1.1,
octaves=4, persistence=0.7, lacunarity=1.8, base=2)
for x_val, y_val in zip(row, y_row)] for row, y_row in zip(x_grid, y_grid)
])
noise_val_color_b = np.array([
[noise.pnoise3(x_val * 0.9, y_val * 1.0, time_factor * 0.9,
octaves=4, persistence=0.5, lacunarity=2.0, base=3)
for x_val, y_val in zip(row, y_row)] for row, y_row in zip(x_grid, y_grid)
])
# Normalize color noise values to 0-1 and scale to 0-255
r = ((noise_val_color_r + 1) / 2 * 255).astype(np.uint8)
g = ((noise_val_color_g + 1) / 2 * 255).astype(np.uint8)
b = ((noise_val_color_b + 1) / 2 * 255).astype(np.uint8)
# Apply shape to color:
# Blend with black based on normalized_noise_val (brighter areas reveal more color)
final_r = (r * normalized_noise_val).astype(np.uint8)
final_g = (g * normalized_noise_val).astype(np.uint8)
final_b = (b * normalized_noise_val).astype(np.uint8)
# OpenCV uses BGR
pattern[:, :, 0] = final_b
pattern[:, :, 1] = final_g
pattern[:, :, 2] = final_r
return pattern
def _generate_fractal_sine(self, pattern: np.ndarray, X: np.ndarray, Y: np.ndarray, frame_time: float) -> np.ndarray:
"""
Generates a fractal pattern by summing layered, modulated sine waves.
"""
total_val_accumulator = np.zeros_like(X) # Accumulate values for each pixel
# Parameters from ParamTable, modulated by oscillators
# base_freq_x = params.val("pfractal_base_freq_x") # * 0.01
# base_freq_y = params.val("pfractal_base_freq_y") # + norm_osc_vals[1] * 0.01
base_freq_x = self.bar_x_freq.value * 0.01
base_freq_y = self.bar_y_freq.value * 0.01
base_amplitude_for_fractal = params.val("pfractal_amplitude")
num_octaves = int(params.val("pfractal_octaves"))
# Oscillator values to perturb coordinates or phase
# Use a scaling factor to make oscillator influence more noticeable
# x_perturb_osc = norm_osc_vals[0] * 50 # Amount of x perturbation
# y_perturb_osc = norm_osc_vals[1] * 50 # Amount of y perturbation
x_perturb_osc = self.x_perturb.value
y_perturb_osc = self.y_perturb.value
# Use osc2_norm for overall time-based movement / phase shift
phase_speed = self.phase_speed.value * 2
time_offset = frame_time * phase_speed
for i in range(num_octaves):
freq_x = base_freq_x * (2 ** i) # Frequency doubles each octave
freq_y = base_freq_y * (2 ** i)
amplitude = base_amplitude_for_fractal / (2 ** i) # Amplitude halves each octave
# Perturb coordinates using oscillators
current_x = X + x_perturb_osc
current_y = Y + y_perturb_osc
# Calculate wave for current octave
wave = (amplitude * np.sin(current_x * freq_x + time_offset * i) +
amplitude * np.sin(current_y * freq_y + time_offset * i))
total_val_accumulator += wave
# Normalize and scale the accumulated value to 0-255
# Calculate max possible sum of amplitudes
max_possible_val = base_amplitude_for_fractal * (2 - (1 / (2**(num_octaves-1))))
if max_possible_val > 0.001: # Avoid division by zero
brightness = ( (total_val_accumulator / max_possible_val / 2 + 0.5) * 255).astype(np.uint8)
else:
brightness = np.zeros_like(X, dtype=np.uint8) # Default to black if no meaningful range
# Apply a color based on some oscillator or fixed color for the fractal
# Use oscillator to shift color
hue_shift = norm_osc_vals[2]
# Create a color based on brightness and hue shift
# For simplicity, let's mix colors based on hue_shift
blue_channel = (brightness * (1 - hue_shift)).astype(np.uint8)
green_channel = (brightness * hue_shift).astype(np.uint8)
red_channel = (brightness * (1 - np.abs(hue_shift - 0.5) * 2)).astype(np.uint8) # Peaks in middle
pattern[:, :, 0] = blue_channel # Blue
pattern[:, :, 1] = green_channel # Green
pattern[:, :, 2] = red_channel # Red
return pattern
def create_sliders(self, default_font_id=None, global_font_id=None):
with dpg.collapsing_header(label=f"\tPattern Generator", tag="pattern_generator"):
pattern_type_slider = TrackbarRow2(
"Pattern Type",
params.get("pattern_type"),
TrackbarCallback(params.get("pattern_type"), "pattern_type").__call__,
default_font_id)
pattern_mod = TrackbarRow2(
"Pattern Mod",
params.get("pattern_mod"),
TrackbarCallback(params.get("pattern_mod"), "pattern_mod").__call__,
default_font_id)
pattern_r = TrackbarRow2(
"Pattern R",
params.get("pattern_r"),
TrackbarCallback(params.get("pattern_r"), "pattern_r").__call__,
default_font_id)
pattern_g = TrackbarRow2(
"Pattern G",
params.get("pattern_g"),
TrackbarCallback(params.get("pattern_g"), "pattern_g").__call__,
default_font_id)
pattern_b = TrackbarRow2(
"Pattern B",
params.get("pattern_b"),
TrackbarCallback(params.get("pattern_b"), "pattern_b").__call__,
default_font_id)
pattern_bar_x_freq_slider = TrackbarRow2(
"Bar x Density",
params.get("bar_x_freq"),
TrackbarCallback(params.get("bar_x_freq"), "bar_x_freq").__call__,
default_font_id)
pattern_bar_y_freq_slider = TrackbarRow2(
"Bar y Density",
params.get("bar_y_freq"),
TrackbarCallback(params.get("bar_y_freq"), "bar_y_freq").__call__,
default_font_id)
# speed_slider = TrackbarRow2(
# "Speed",
# params.get("pattern_speed"),
# TrackbarCallback(params.get("pattern_speed"), "pattern_speed").__call__,
# self.reset_slider_callback,
# default_font_id)
pattern_distance = TrackbarRow2(
"Pattern Distance",
params.get("pattern_distance"),
TrackbarCallback(params.get("pattern_distance"), "pattern_distance").__call__,
default_font_id)
angle_amt_slider = TrackbarRow2(
"Radial Frequency",
params.get("pattern_radial_freq"),
TrackbarCallback(params.get("pattern_radial_freq"), "pattern_radial_freq").__call__,
default_font_id)
radius_amt_slider = TrackbarRow2(
"Angular Frequency",
params.get("pattern_angular_freq"),
TrackbarCallback(params.get("pattern_angular_freq"), "pattern_angular_freq").__call__,
default_font_id)
radial_mod_slider = TrackbarRow2(
"Radial Mod",
params.get("pattern_radial_mod"),
TrackbarCallback(params.get("pattern_radial_mod"), "pattern_radial_mod").__call__,
default_font_id)
angle_mod_slider = TrackbarRow2(
"Angle Mode",
params.get("pattern_angle_mod"),
TrackbarCallback(params.get("pattern_angle_mod"), "pattern_angle_mod").__call__,
default_font_id)
# use_fractal_slider = TrackbarRow2(
# "Use Fractal",
# params.get("pattern_use_fractal"),
# TrackbarCallback(params.get("pattern_use_fractal"), "pattern_use_fractal").__call__,
# self.reset_slider_callback,
# default_font_id)
# octaves_slider = TrackbarRow2(
# "Octaves",
# params.get("pattern_octaves"),
# TrackbarCallback(params.get("pattern_octaves"), "pattern_octaves").__call__,
# self.reset_slider_callback,
# default_font_id)
# gain_slider = TrackbarRow2(
# "Gain",
# params.get("pattern_gain"),
# TrackbarCallback(params.get("pattern_gain"), "pattern_gain").__call__,
# self.reset_slider_callback,
# default_font_id)
# lacunarity_slider = TrackbarRow2(
# "Lacunarity",
# params.get("pattern_lacunarity"),
# TrackbarCallback(params.get("pattern_lacunarity"), "pattern_lacunarity").__call__,
# self.reset_slider_callback,
# default_font_id)
posc_freq_sliders = []
posc_amp_sliders = []
posc_phase_sliders = []
posc_seed_sliders = []
posc_shape_sliders = []
for i in range(3):
with dpg.collapsing_header(label=f"\tpOscillator {i}", tag=f"posc{i}"):
posc_shape_sliders.append(TrackbarRow2(
f"pOsc {i} Shape",
params.get(f"posc{i}_shape"),
TrackbarCallback(params.get(f"posc{i}_shape"), f"posc{i}_shape").__call__,
default_font_id))
posc_freq_sliders.append(TrackbarRow2(
f"pOsc {i} Freq",
params.get(f"posc{i}_frequency"),
TrackbarCallback(params.get(f"posc{i}_frequency"), f"posc{i}_frequency").__call__,
default_font_id))
posc_amp_sliders.append(TrackbarRow2(
f"pOsc {i} Amp",
params.get(f"posc{i}_amplitude"),
TrackbarCallback(params.get(f"posc{i}_amplitude"), f"posc{i}_amplitude").__call__,
default_font_id))
posc_phase_sliders.append(TrackbarRow2(
f"pOsc {i} Phase",
params.get(f"posc{i}_phase"),
TrackbarCallback(params.get(f"posc{i}_phase"), f"posc{i}_phase").__call__,
default_font_id))
posc_seed_sliders.append(TrackbarRow2(
f"pOsc {i} Seed",
params.get(f"posc{i}_seed"),
TrackbarCallback(params.get(f"posc{i}_seed"), f"posc{i}_seed").__call__,
default_font_id))
dpg.bind_item_font(f"posc{i}", global_font_id)
dpg.bind_item_font("pattern_generator", global_font_id)
# --- Main Execution Block ---
if __name__ == "__main__":
# Define image dimensions
WIDTH, HEIGHT = 800, 600
FPS = 30 # Frames per second
# Initialize ParamTable
global_params = ParamTable()
# Create a bank of Oscillators
# We'll use 3 oscillators for modulation (osc0, osc1, osc2)
posc_bank = []
posc_bank.append(Oscillator("osc0", global_params, frequency=0.1, amplitude=0.5, phase=0.0, shape=0)) # Sine
posc_bank.append(Oscillator("osc1", global_params, frequency=0.05, amplitude=0.7, phase=np.pi/2, shape=1)) # Sawtooth
posc_bank.append(Oscillator("osc2", global_params, frequency=0.08, amplitude=0.6, phase=np.pi, shape=2)) # Square
# Create the PatternGenerator instance
pattern_gen = Pattern(WIDTH, HEIGHT)
# Set initial pattern type
pattern_gen.pattern_type = PatternType.PERLIN_BLOBS
# OpenCV window setup
cv2.namedWindow("Pattern Generator", cv2.WINDOW_AUTOSIZE)
start_time = time.time()
frame_count = 0
print("\n--- Controls ---")
print("Press '1' for BARS pattern")
print("Press '2' for WAVES pattern")
print("Press '3' for CHECKERS pattern")
print("Press '4' for RADIAL pattern")
print("Press '5' for PERLIN BLOBS pattern")
print("Press '6' for FRACTAL SINE pattern")
print("Press '0' for NONE pattern (black screen)")
print("Press 'Q' to quit")
print("----------------")
running = True
while running:
current_time = time.time()
delta_time = current_time - start_time
start_time = current_time # Reset start_time for next frame's delta_time
# Update all oscillators
for osc in posc_bank:
osc.update(delta_time)
# Generate the current frame
frame = pattern_gen.generate_pattern_frame(current_time)
# Display the frame
cv2.imshow("Pattern Generator", frame)
# Handle key presses
key = cv2.waitKey(1) & 0xFF # Wait for 1ms, get key press
if key == ord('q') or key == ord('Q'):
running = False
elif key == ord('0'):
pattern_gen.pattern_type = PatternType.NONE
elif key == ord('1'):
pattern_gen.pattern_type = PatternType.BARS
elif key == ord('2'):
pattern_gen.pattern_type = PatternType.WAVES
elif key == ord('3'):
pattern_gen.pattern_type = PatternType.CHECKERS
elif key == ord('4'):
pattern_gen.pattern_type = PatternType.RADIAL
elif key == ord('5'):
pattern_gen.pattern_type = PatternType.PERLIN_BLOBS
elif key == ord('6'):
pattern_gen.pattern_type = PatternType.FRACTAL_SINE
# Optional: Print FPS for debugging
# frame_count += 1
# if frame_count % 30 == 0:
# print(f"FPS: {1.0 / delta_time:.2f}")
cv2.destroyAllWindows()
print("Pattern Generator stopped.")