diff --git a/README.md b/README.md index cb472fc..c3ced4e 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,20 @@ 

Flux

A beautiful dynamic flowfield visualization with particle simulation. It demonstrates the movement of particles within a vector field, offering an engaging visualization of the underlying noisemaps.

- +

- +

- +

- -

-

- +

+#### For additional screenshots, visit the full folder: [Screenshots Folder](assets/screenshots) + ## System Support | OS | Supported | | :---: | :---: | diff --git a/assets/flux1.png b/assets/flux1.png deleted file mode 100644 index 1f4b058..0000000 Binary files a/assets/flux1.png and /dev/null differ diff --git a/assets/flux2.png b/assets/flux2.png deleted file mode 100644 index 4be8271..0000000 Binary files a/assets/flux2.png and /dev/null differ diff --git a/assets/flux3.png b/assets/flux3.png deleted file mode 100644 index c6a9820..0000000 Binary files a/assets/flux3.png and /dev/null differ diff --git a/assets/flux4-gif.gif b/assets/flux4-gif.gif deleted file mode 100644 index e004c51..0000000 Binary files a/assets/flux4-gif.gif and /dev/null differ diff --git a/assets/flux4.png b/assets/flux4.png deleted file mode 100644 index 909c89f..0000000 Binary files a/assets/flux4.png and /dev/null differ diff --git a/assets/flux5.png b/assets/flux5.png deleted file mode 100644 index aaa747a..0000000 Binary files a/assets/flux5.png and /dev/null differ diff --git a/assets/fonts/Inter-BlackItalic.otf b/assets/fonts/Inter-BlackItalic.otf new file mode 100644 index 0000000..7001434 Binary files /dev/null and b/assets/fonts/Inter-BlackItalic.otf differ diff --git a/assets/fonts/impact.ttf b/assets/fonts/impact.ttf new file mode 100644 index 0000000..114e6c1 Binary files /dev/null and b/assets/fonts/impact.ttf differ diff --git a/assets/screenshots/Blues_Swirly.png b/assets/screenshots/Blues_Swirly.png new file mode 100644 index 0000000..c25bc1e Binary files /dev/null and b/assets/screenshots/Blues_Swirly.png differ diff --git a/assets/screenshots/Bubbles_FNS.png b/assets/screenshots/Bubbles_FNS.png new file mode 100644 index 0000000..e07290f Binary files /dev/null and b/assets/screenshots/Bubbles_FNS.png differ diff --git a/assets/screenshots/Bubbles_Quattro.png b/assets/screenshots/Bubbles_Quattro.png new file mode 100644 index 0000000..af43347 Binary files /dev/null and b/assets/screenshots/Bubbles_Quattro.png differ diff --git a/assets/screenshots/Flux_FranksLabText.png b/assets/screenshots/Flux_FranksLabText.png new file mode 100644 index 0000000..c8b5627 Binary files /dev/null and b/assets/screenshots/Flux_FranksLabText.png differ diff --git a/assets/screenshots/Flux_FranksLabText_Light.png b/assets/screenshots/Flux_FranksLabText_Light.png new file mode 100644 index 0000000..3a27ac3 Binary files /dev/null and b/assets/screenshots/Flux_FranksLabText_Light.png differ diff --git a/assets/screenshots/Greens_Swirly.png b/assets/screenshots/Greens_Swirly.png new file mode 100644 index 0000000..96294b3 Binary files /dev/null and b/assets/screenshots/Greens_Swirly.png differ diff --git a/assets/screenshots/SeaAnemone_FNS.png b/assets/screenshots/SeaAnemone_FNS.png new file mode 100644 index 0000000..94a7705 Binary files /dev/null and b/assets/screenshots/SeaAnemone_FNS.png differ diff --git a/color_functions.py b/color_functions.py deleted file mode 100644 index 08e061f..0000000 --- a/color_functions.py +++ /dev/null @@ -1,35 +0,0 @@ -import numpy as np - -def color_by_position(props, min_rgb, max_rgb, base_alpha): - x_norm = np.divide(props['particles'][0], props['ff_width']) - y_norm = np.divide(props['particles'][1], props['ff_height']) - r = np.add(min_rgb[0], np.multiply(x_norm, (max_rgb[0] - min_rgb[0]))) - g = np.add(min_rgb[1], np.multiply(y_norm, (max_rgb[1] - min_rgb[1]))) - b = np.add(min_rgb[2], np.multiply(np.multiply(np.add(x_norm, y_norm), (max_rgb[2] - min_rgb[2])),0.5)) - a = np.repeat(base_alpha, props['ttl_particles']) - return r, g, b, a - - -def color_by_age(props, min_rgb, max_rgb, base_alpha): - age_range = props['max_age'] - props['min_age'] - age_norm = np.divide(np.subtract(props['particles'][2], props['min_age']), age_range) - r = np.add(min_rgb[0], np.multiply(age_norm, (max_rgb[0] - min_rgb[0]))) - g = np.add(min_rgb[1], np.multiply(age_norm, (max_rgb[1] - min_rgb[1]))) - b = np.add(min_rgb[2], np.multiply(age_norm, (max_rgb[2] - min_rgb[2]))) - a = np.repeat(base_alpha, props['ttl_particles']) - return r, g, b, a - -def color_by_angle(props, min_rgb, max_rgb, base_alpha): - angle_norm = props['angles'] - r = np.add(min_rgb[0], np.multiply(angle_norm, (max_rgb[0] - min_rgb[0]))) - g = np.add(min_rgb[1], np.multiply(angle_norm, (max_rgb[1] - min_rgb[1]))) - b = np.add(min_rgb[2], np.multiply(angle_norm, (max_rgb[2] - min_rgb[2]))) - a = np.repeat(base_alpha, props['ttl_particles']) - return r,g,b,a - - -clr_funcs = { - 'Position': color_by_position, - 'Age': color_by_age, - 'Angle': color_by_angle, -} diff --git a/flux.py b/flux.py deleted file mode 100644 index 48006d9..0000000 --- a/flux.py +++ /dev/null @@ -1,296 +0,0 @@ -import dearpygui.dearpygui as dpg -import pyfastnoisesimd as fns -import numpy as np -import color_functions as cf - - -# CONSTANTS -TAU = np.pi * 2 - - -# FLOWFIELD CONFIG -sp_width = 300 # Side Panel Width -ff_width = 1000 # Flowfield Width -ff_height = 750 # Flowfield Height -n_scale = 0.1 # Noise Scale -t_scale = 0.01 # Time Scale - -## PARTICLE CONFIG -ttl_particles = 1600 # Total Particles -min_age = 50 # Min Age of Particles -max_age = 250 # Max Age of Particles -speed = 1 # Speed of particles - -min_rgb = [91,109,255,255] -max_rgb = [154,0,190,255] - -# COLOR CONFIG -bg_color = [1,5,58,255] # Background Color -d_alpha = 10 # Dimmer alpha -p_alpha = 50 # Particle alpha -clr_func = cf.clr_funcs['Angle'] - -# CONTAINERS -particles = np.ndarray((8, ttl_particles)) -coords = fns.empty_coords(ttl_particles) - - -noise = fns.Noise() - -def spawn_paricles(): - global ff_width, ff_height, particles, ttl_particles, min_age, max_age - particles[0,:] = [np.random.random() * ff_width for _ in range(ttl_particles)] # X - particles[1,:] = [np.random.random() * ff_height for _ in range(ttl_particles)] # Y - particles[2,:] = [np.random.randint(min_age, max_age) for _ in range(ttl_particles)] # age - particles[3,:] = np.repeat(bg_color[0], ttl_particles) # Red - particles[4,:] = np.repeat(bg_color[1], ttl_particles) # Green - particles[5,:] = np.repeat(bg_color[2], ttl_particles) # Blue - particles[6,:] = np.repeat(p_alpha, ttl_particles) # Opacity - # for each coardinates draw a particle - for p in particles.T: - p[7] = dpg.draw_circle(center=(p[0], p[1]), radius=1, parent='flowfield', fill=bg_color, color=bg_color, show=True) # Reference to drawn object - -def recalc_particles(): - global noise, TAU, coords, particles, ttl_particles, ff_width, ff_height, min_age, max_age, speed - coords[0,:] = particles[0,:] * n_scale - coords[1,:] = particles[1,:] * n_scale - coords[2,:] = np.repeat(dpg.get_frame_count()*t_scale, ttl_particles) - angles = noise.genFromCoords(coords) * TAU - cos_angles = np.cos(angles) - sin_angles = np.sin(angles) - particles[0,:] = np.add(particles[0,:], np.multiply(cos_angles, speed)) - particles[1,:] = np.add(particles[1,:], np.multiply(sin_angles, speed)) - particles[2,:] = np.add(particles[2,:], -1) - - # Reset particles if out of bounds or expired - out_of_bounds = (particles[0,:] < 0) | (particles[0,:] > ff_width) | (particles[1,:] < 0) | (particles[1,:] > ff_height) - expired = particles[2,:] <= 0 - reset_indices = np.logical_or(out_of_bounds, expired) - particles[0, reset_indices] = np.multiply(np.random.rand(np.sum(reset_indices)), ff_width) - particles[1, reset_indices] = np.multiply(np.random.rand(np.sum(reset_indices)), ff_height) - particles[2, reset_indices] = np.random.randint(min_age, max_age + 1, size=np.sum(reset_indices)) - - props = { - 'particles': particles, - 'angles': angles, - 'cos_angles': cos_angles, - 'sin_angles': sin_angles, - 'ttl_particles': ttl_particles, - 'ff_width': ff_width, - 'ff_height': ff_height, - 'speed': speed, - 'min_age': min_age, - 'max_age': max_age, - } - particles[3:7,:] = clr_func(props, min_rgb, max_rgb, p_alpha) # RGB - - for p in particles.T: - clr = list(p[3:7]) - dpg.configure_item(int(p[7]), center=(p[0], p[1]), fill=clr, color=clr) - -def background(clr): - global ff_width, ff_height - with dpg.mutex(): - # Window Background - if dpg.does_item_exist('w_background'): - dpg.configure_item('w_background', fill=clr, color=clr, show=True) - else: - dpg.draw_rectangle(tag='w_background', pmin=(0,0), pmax=(ff_width, ff_height), parent='flowfield', fill=clr, color=clr) - -def dimmer(clr, d_alpha): - with dpg.mutex(): - # Dimmer - # Let the dimmer be drawn every frame - color = clr[:3] - color.append(d_alpha) - if dpg.does_item_exist('dimmer'): - dpg.configure_item('dimmer', fill=color, color=color) - else: - dpg.draw_rectangle(tag='dimmer', pmin=(0,0), pmax=(ff_width, ff_height), parent='flowfield', fill=color, color=color) - - -def init_frame_buffer(sender, buffer): - # Prepare of frame buffer handling - global ff_width, ff_height, ttl_particles - # First resolve the dimensions - with dpg.mutex(): - # Window dimensions - w_height = dpg.get_viewport_client_height() - w_width = dpg.get_viewport_client_width() - dpg.configure_item('parameters', width=w_width-ff_width) - # Setup Initial Frame - with dpg.texture_registry(): - dpg.add_raw_texture(width=w_width, height=w_height, default_value=buffer, format=dpg.mvFormat_Float_rgba, tag="prev-frame-texture") - dpg.add_image('prev-frame-texture',tag='prev-frame', width=ff_width, parent='flowfield', pos=(0,0), uv_min=(0,0), uv_max=(ff_width/w_width, 1)) - background(bg_color) - dimmer(bg_color, d_alpha) - spawn_paricles() - # Start the frame buffer - dpg.set_frame_callback(dpg.get_frame_count()+1, callback=lambda: dpg.output_frame_buffer(callback=clear_frame)) - - -def handle_frame_buffer(sender, buffer): - with dpg.mutex(): - dpg.set_value('prev-frame-texture', buffer) - recalc_particles() - dpg.set_frame_callback(dpg.get_frame_count()+2, callback=lambda: dpg.output_frame_buffer(callback=handle_frame_buffer)) - -def clear_frame(sender, buffer): - with dpg.mutex(): - if dpg.is_item_shown('prev-frame'): - dpg.hide_item('prev-frame') - dpg.set_frame_callback(dpg.get_frame_count()+1, callback=lambda: dpg.output_frame_buffer(callback=clear_frame)) - else: - dpg.set_value('prev-frame', buffer) - dpg.show_item('prev-frame') - # Make sure the background is only drawn once - if dpg.is_item_shown('w_background'): - dpg.hide_item('w_background') - dpg.set_frame_callback(dpg.get_frame_count()+1, callback=lambda: dpg.output_frame_buffer(callback=handle_frame_buffer)) - -def setup_flux(): - # Setup flux window - - # Callbacks - def set_n_scale(sender, data): - global n_scale - n_scale = data - - def set_t_scale(sender, data): - global t_scale - t_scale = data - - def set_particle_speed(sender, data): - global speed - speed = data - - def set_color_function(sender, data): - global clr_func - clr_func = cf.clr_funcs[data] - - def set_min_max_age(sender, data): - global min_age, max_age - if sender == 'min-age': - min_age = data - elif sender == 'max-age': - max_age = data - - def set_min_max_rgb(sender, data): - global min_rgb, max_rgb - if sender == 'min_rgb': - min_rgb = [int(c*255) for c in data] - elif sender == 'max_rgb': - max_rgb = [int(c*255) for c in data] - - def set_particle_opacity(sender, data): - global p_alpha - p_alpha = data - - def set_dimmer_opacity(semder, data): - global d_alpha, bg_color - d_alpha = data - background(bg_color) - dimmer(bg_color, d_alpha) - dpg.set_frame_callback(dpg.get_frame_count()+1, callback=lambda: dpg.output_frame_buffer(callback=clear_frame)) - - - def set_background_color(sender, data): - global bg_color - r,g,b,a = [int(c*255) for c in data] - bg_color = [r,g,b,a] - background(bg_color) - dimmer(bg_color, d_alpha) - # Calculate Luminance of background - # Needed for runntime UI themes. - l = 0.2126 * r + 0.7152 * g + 0.0722 * b - threshold = 180 - a = 180 - with dpg.theme() as flowfield_theme: - with dpg.theme_component(dpg.mvAll): - dpg.add_theme_style(dpg.mvStyleVar_WindowPadding, 0) - dpg.add_theme_color(dpg.mvThemeCol_ChildBg, [r,g,b,a], category=dpg.mvThemeCat_Core) - dpg.add_theme_color(dpg.mvThemeCol_Text, [255,255,255,255] if l < threshold else [0,0,0,255], category=dpg.mvThemeCat_Core) - dpg.add_theme_color(dpg.mvThemeCol_FrameBg, [int(r-(r/20)),int(g-(g/20)),int(b-(b/20)),100], category=dpg.mvThemeCat_Core) - dpg.bind_item_theme('flowfield', flowfield_theme) - dpg.set_frame_callback(dpg.get_frame_count()+1, callback=lambda: dpg.output_frame_buffer(callback=clear_frame)) - - def handle_dropdown(sender, data, group): - if dpg.is_item_shown(group): - dpg.configure_item(sender, direction=dpg.mvDir_Right) - dpg.configure_item(group, show=False) - else: - dpg.configure_item(sender, direction=dpg.mvDir_Down) - dpg.configure_item(group, show=True) - - with dpg.window(tag='flowfield', pos=(0,0)): - # Theme setting - dpg.set_primary_window('flowfield', True) - with dpg.theme() as flowfield_theme: - with dpg.theme_component(dpg.mvAll): - dpg.add_theme_style(dpg.mvStyleVar_WindowPadding, 0) - dpg.add_theme_color(dpg.mvThemeCol_ChildBg, bg_color, category=dpg.mvThemeCat_Core) - dpg.bind_item_theme('flowfield', flowfield_theme) - - # Flux GUI - with dpg.child_window(tag='parameters', pos=(ff_width, 0), width=sp_width, height=-1): - with dpg.theme() as side_panel_theme: - with dpg.theme_component(dpg.mvAll): - dpg.add_theme_style(dpg.mvStyleVar_WindowPadding, 8) - dpg.bind_item_theme('parameters', side_panel_theme) - - # Flowfield Settings - dpg.add_spacer(height=3) - with dpg.group(horizontal=True): - dpg.add_button(tag='ff-dropdown', arrow=True, direction=dpg.mvDir_Down, callback=handle_dropdown, user_data='flowfield-settings') - dpg.add_text(default_value='Flowfield Properties') - with dpg.group(tag='flowfield-settings'): - dpg.add_slider_float(width=sp_width/2, label='noisescale', min_value=0.05, default_value=n_scale, max_value=3, callback=set_n_scale) - dpg.add_slider_float(width=sp_width/2, label='timescale', min_value=0, default_value=t_scale, max_value=0.1, callback=set_t_scale) - - dpg.add_separator() - - # Particle Settings - with dpg.group(horizontal=True): - dpg.add_button(tag='pp-dropdown', arrow=True, direction=dpg.mvDir_Down, callback=handle_dropdown, user_data='particle-settings') - dpg.add_text(default_value='Particle Properties') - with dpg.group(tag='particle-settings'): - dpg.add_slider_float(width=sp_width/2, label='speed', min_value=0.5, default_value=speed, max_value=4, callback=set_particle_speed) - dpg.add_slider_int(width=sp_width/2, label='min age', tag='min-age', min_value=min_age, default_value=min_age, max_value=100, callback=set_min_max_age) - dpg.add_slider_int(width=sp_width/2, label='max age', tag='max-age', min_value=101, default_value=max_age, max_value=max_age, callback=set_min_max_age) - - dpg.add_separator() - - # Color Settings - with dpg.group(horizontal=True): - dpg.add_button(tag='cl-dropdown', arrow=True, direction=dpg.mvDir_Down, callback=handle_dropdown, user_data='color-settings') - dpg.add_text(default_value='Color Settings') - with dpg.group(tag='color-settings'): - dpg.add_combo(width=sp_width/2, label='Color Function', tag='color-functions', items=list(cf.clr_funcs.keys()), default_value='Age', callback=set_color_function) - with dpg.tab_bar(tag='color-pickers'): - with dpg.tab(tag='background', label='background'): - dpg.add_slider_int(width=sp_width/2, label='dimmer alpha', default_value=d_alpha, max_value=255, callback=set_dimmer_opacity) - dpg.add_color_picker(width=sp_width/2, label='background', tag='bg_rgb', default_value=bg_color, no_tooltip=True, no_alpha=True, callback=set_background_color) - with dpg.tab(tag='particles', label='particles'): - dpg.add_slider_int(width=sp_width/2, label='particle alpha', default_value=p_alpha, max_value=255, callback=set_particle_opacity) - dpg.add_color_picker(width=sp_width/2, label='min_rgb', tag='min_rgb', default_value=min_rgb, no_tooltip=True, no_alpha=True, callback=set_min_max_rgb) - dpg.add_color_picker(width=sp_width/2, label='max_rgb', tag='max_rgb', default_value=max_rgb, no_tooltip=True, no_alpha=True, callback=set_min_max_rgb) - - # Bottom Padding - dpg.add_spacer(height=3) - - -def start_flux(): - # Start Flux - dpg.create_context() - dpg.create_viewport(title='Flux', width=ff_width+sp_width, height=ff_height, resizable=False) - dpg.setup_dearpygui() - setup_flux() - dpg.show_viewport() - dpg.set_frame_callback(20, callback=lambda: dpg.output_frame_buffer(callback=init_frame_buffer)) - # dpg.set_viewport_vsync(False) - # dpg.show_metrics() - dpg.start_dearpygui() - dpg.destroy_context() - -if __name__ == '__main__': - start_flux() diff --git a/flux/__init__.py b/flux/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flux/__main__.py b/flux/__main__.py new file mode 100644 index 0000000..085edc1 --- /dev/null +++ b/flux/__main__.py @@ -0,0 +1,3 @@ +from flux import start_flux + +start_flux() \ No newline at end of file diff --git a/flux/color_functions/__init__.py b/flux/color_functions/__init__.py new file mode 100644 index 0000000..2a1afe6 --- /dev/null +++ b/flux/color_functions/__init__.py @@ -0,0 +1,16 @@ +import os +import importlib + +__globals = globals() +__clr_funcs = {} +for file in os.listdir(os.path.dirname(__file__)): + mod_name = file[:-3] # strip .py at the end + if not mod_name.startswith('__'): # Avoid importing __init__.py + __globals[mod_name] = importlib.import_module('.' + mod_name, package=__name__) + __clr_funcs[__globals[mod_name].get_color_function_name()] = __globals[mod_name].color + +def get_color_function(func): + return __clr_funcs.get(func) + +def get_color_function_names(): + return list(__clr_funcs.keys()) diff --git a/flux/color_functions/age.py b/flux/color_functions/age.py new file mode 100644 index 0000000..1d88907 --- /dev/null +++ b/flux/color_functions/age.py @@ -0,0 +1,13 @@ +import numpy as np + +def get_color_function_name(): + return 'Age' + +def color(min_rgb, max_rgb, base_alpha, args): + age_range = args['max_age'] - args['min_age'] + age_norm = np.divide(np.subtract(args['particles'][2], args['min_age']), age_range) + r = np.add(min_rgb[0], np.multiply(age_norm, (max_rgb[0] - min_rgb[0]))) + g = np.add(min_rgb[1], np.multiply(age_norm, (max_rgb[1] - min_rgb[1]))) + b = np.add(min_rgb[2], np.multiply(age_norm, (max_rgb[2] - min_rgb[2]))) + a = np.repeat(base_alpha, r.size) + return r, g, b, a \ No newline at end of file diff --git a/flux/color_functions/angle.py b/flux/color_functions/angle.py new file mode 100644 index 0000000..94c4e2e --- /dev/null +++ b/flux/color_functions/angle.py @@ -0,0 +1,12 @@ +import numpy as np + +def get_color_function_name(): + return 'Angle' + +def color(min_rgb, max_rgb, base_alpha, args): + angle_norm = np.arctan2(args['dx'], args['dy']) + r = np.add(min_rgb[0], np.multiply(angle_norm, (max_rgb[0] - min_rgb[0]))) + g = np.add(min_rgb[1], np.multiply(angle_norm, (max_rgb[1] - min_rgb[1]))) + b = np.add(min_rgb[2], np.multiply(angle_norm, (max_rgb[2] - min_rgb[2]))) + a = np.repeat(base_alpha, r.size) + return r, g, b, a \ No newline at end of file diff --git a/flux/color_functions/position.py b/flux/color_functions/position.py new file mode 100644 index 0000000..3dcd220 --- /dev/null +++ b/flux/color_functions/position.py @@ -0,0 +1,13 @@ +import numpy as np + +def get_color_function_name(): + return 'Position' + +def color(min_rgb, max_rgb, base_alpha, args): + x_norm = np.divide(args['particles'][0], args['ff_width']) + y_norm = np.divide(args['particles'][1], args['ff_height']) + r = np.add(min_rgb[0], np.multiply(x_norm, (max_rgb[0] - min_rgb[0]))) + g = np.add(min_rgb[1], np.multiply(y_norm, (max_rgb[1] - min_rgb[1]))) + b = np.add(min_rgb[2], np.multiply(np.multiply(np.add(x_norm, y_norm), (max_rgb[2] - min_rgb[2])),0.5)) + a = np.repeat(base_alpha, r.size) + return r, g, b, a diff --git a/flux/color_functions/radius.py b/flux/color_functions/radius.py new file mode 100644 index 0000000..4dfac4f --- /dev/null +++ b/flux/color_functions/radius.py @@ -0,0 +1,13 @@ +import numpy as np +import config as cfg + +def get_color_function_name(): + return 'Radius' + +def color(min_rgb, max_rgb, base_alpha, args): + radius_norm = np.divide(args['particles'][7], cfg.radius) + r = np.add(min_rgb[0], np.multiply(radius_norm, (max_rgb[0] - min_rgb[0]))) + g = np.add(min_rgb[1], np.multiply(radius_norm, (max_rgb[1] - min_rgb[1]))) + b = np.add(min_rgb[2], np.multiply(radius_norm, (max_rgb[2] - min_rgb[2]))) + a = np.repeat(base_alpha, r.size) + return r, g, b, a \ No newline at end of file diff --git a/flux/config.py b/flux/config.py new file mode 100644 index 0000000..45c96c6 --- /dev/null +++ b/flux/config.py @@ -0,0 +1,59 @@ +import pyfastnoisesimd as fns +import numpy as np + +# UI TAGS/IDS +prev_frame_texture = '' +prev_frame = '' +ff_func_settings = 'ff-func-settings' + + +ff_func = '' # Flowfield Function +default_fff = 'Franks Lab Text' # Default Flowfield Function Name + +clr_func = '' # Color Function +default_cf = 'Angle' # Default Color Function Name + + +sp_width = 300 # Side Panel Width +ff_width = 1000 # Flowfield Width +ff_height = 750 # Flowfield Height + +w_width = ff_width+sp_width # Window Width +w_height = ff_height # Window Height + +max_particles = 5000 # Max number of particles +ttl_particles = 1500 # Total Particles +min_age = 50 # Min Age of Particles +max_age = 250 # Max Age of Particles +speed = 1 # Speed of particles +radius = 1 # Radius of particles +max_radius = 20 # Max radius of particles +random_radius = False # Random Radius + + +bg_color = [1,5,58,255] # Background Color +min_rgb = [91,109,255,255] # Particle Color - Min +max_rgb = [154,0,190,255] # Particle Color - Max +d_alpha = 10 # Dimmer alpha +p_alpha = 25 # Particle alpha +border = False # Border of particles +border_rgb = [1,5,58,255] # Border Color + +# CONTAINERS +particles = np.ndarray((9, max_particles)) + +# TYPES +TYPE_SLIDER_INT = 'SLIDER_INT' +TYPE_SLIDER_FLOAT = 'SLIDER_FLOAT' +TYPE_INPUT_TEXT = 'INPUT_TEXT' + +# DEFAULT FUNCTION DEFINITIONS +def default_reset_particles(reset_indices): + global particles, ff_width, ff_height, min_age, max_age + particles[0, reset_indices] = np.multiply(np.random.rand(np.sum(reset_indices)), ff_width) + particles[1, reset_indices] = np.multiply(np.random.rand(np.sum(reset_indices)), ff_height) + particles[2, reset_indices] = np.random.randint(min_age, max_age + 1, size=np.sum(reset_indices)) + + +# DEFAULT FUNCTION REFERENCES +reset_particles = default_reset_particles \ No newline at end of file diff --git a/flux/flowfield_functions/__init__.py b/flux/flowfield_functions/__init__.py new file mode 100644 index 0000000..85d60b6 --- /dev/null +++ b/flux/flowfield_functions/__init__.py @@ -0,0 +1,36 @@ +import os +import importlib +import dearpygui.dearpygui as dpg +import config as cfg +from types import SimpleNamespace +from config import ff_func_settings, sp_width + +__globals = globals() +__ff_funcs = {} +for file in os.listdir(os.path.dirname(__file__)): + mod_name = file[:-3] # strip .py at the end + if not mod_name.startswith('__'): # Avoid importing __init__.py + __globals[mod_name] = importlib.import_module('.' + mod_name, package=__name__) + __ff_funcs[__globals[mod_name].get_flowfield_function_name()] = __globals[mod_name].flowfield + + +def _setup_args(args: SimpleNamespace): + for arg in list(args.__dict__): + prop = getattr(args, arg) + # callback = getattr(args, 'callback', lambda sender, data, property: callback if hasattr(arg, 'callback') else setattr(property, 'val', data)) + # dpg.add_slider_float(width=sp_width/2, label=arg, parent=ff_func_settings, default_value=prop.val, max_value=prop.max_val, min_value=prop.min_val, user_data=prop, callback= print(getattr(arg, callback)) if hasattr(arg, 'callback') else lambda sender, data, property: setattr(property, 'val', data)) + if hasattr(prop, 'type') and prop.type == cfg.TYPE_SLIDER_INT: + dpg.add_slider_int(width=sp_width/2, label=arg, parent=ff_func_settings, default_value=prop.val, max_value=prop.max_val, min_value=prop.min_val, user_data=prop, callback= lambda sender, data, property: getattr(property, 'callback')(sender, data, property) if hasattr(property, 'callback') else setattr(property, 'val', data)) + elif hasattr(prop, 'type') and prop.type == cfg.TYPE_INPUT_TEXT: + dpg.add_input_text(width=sp_width/2, label=arg, parent=ff_func_settings, default_value=prop.val, user_data=prop, callback= lambda sender, data, property: getattr(property, 'callback')(sender, data, property) if hasattr(property, 'callback') else setattr(property, 'val', data)) + else: + dpg.add_slider_float(width=sp_width/2, label=arg, parent=ff_func_settings, default_value=prop.val, max_value=prop.max_val, min_value=prop.min_val, user_data=prop, callback= lambda sender, data, property: getattr(property, 'callback')(sender, data, property) if hasattr(property, 'callback') else setattr(property, 'val', data)) + +def get_flowfield_function(func): + args, noise, init_flowfield = __ff_funcs.get(func)() + _setup_args(args) + init_flowfield() + return SimpleNamespace(noise=noise, init_flowfield=init_flowfield) + +def get_flowfield_function_names(): + return list(__ff_funcs.keys()) diff --git a/flux/flowfield_functions/fastnoisesimd.py b/flux/flowfield_functions/fastnoisesimd.py new file mode 100644 index 0000000..591f419 --- /dev/null +++ b/flux/flowfield_functions/fastnoisesimd.py @@ -0,0 +1,40 @@ +import pyfastnoisesimd as fns +import numpy as np +import config as cfg +from types import SimpleNamespace + +def get_flowfield_function_name(): + return 'FastNoiseSIMD' + +def flowfield(): + coords = fns.empty_coords(cfg.max_particles) + fns_noise = fns.Noise() + TAU = np.pi * 2 + + def init_flowfield(): + cfg.reset_particles = cfg.default_reset_particles + # cfg.reset_particles(np.repeat(True, cfg.max_particles)) + + args = SimpleNamespace( + noise_scale = SimpleNamespace( + val = 0.5, + min_val = 0.1, + max_val = 3 + ), + time_scale = SimpleNamespace( + val = 0.01, + min_val = 0, + max_val = 0.1 + ) + ) + + init_flowfield() + + def noise(particles, frame_count): + nonlocal fns_noise, coords, TAU, args + coords[0] = cfg.particles[0] * args.noise_scale.val + coords[1] = cfg.particles[1] * args.noise_scale.val + coords[2] = np.repeat(frame_count, coords[0].size) * args.time_scale.val + angles = fns_noise.genFromCoords(coords) * TAU + return np.cos(angles), np.sin(angles) + return args, noise, init_flowfield \ No newline at end of file diff --git a/flux/flowfield_functions/frank_lab_text.py b/flux/flowfield_functions/frank_lab_text.py new file mode 100644 index 0000000..60767d9 --- /dev/null +++ b/flux/flowfield_functions/frank_lab_text.py @@ -0,0 +1,112 @@ +import numpy as np +import config as cfg +from PIL import Image, ImageDraw, ImageFont +from types import SimpleNamespace + +def radial_gradient(i, center, c1, c2, bbox): + mask = Image.new('L', i.size, 0) + draw = ImageDraw.Draw(mask) + draw.rectangle([bbox[0]+cfg.ff_width//2, bbox[1]+cfg.ff_height//2, bbox[2]+cfg.ff_width//2, bbox[3]+cfg.ff_height//2], fill=255) + center = np.array(center) + max_dist = (bbox[2]-bbox[0]) / 2 + x, y = np.meshgrid(np.arange(i.size[0]), np.arange(i.size[1])) + c = np.linalg.norm(np.stack((x, y), axis=2) - center, axis=2) / max_dist + c = np.clip(c, 0, 1) + c = np.tile(np.expand_dims(c, axis=2), [1, 1, 3]) + c = (c1 * (1 - c) + c2 * c).astype(np.uint8) + c = Image.fromarray(c) + + i.paste(c, mask=mask) + return i + +def get_flowfield_function_name(): + return 'Franks Lab Text' + +def flowfield(): + previous_angles = None + w,h = (cfg.ff_width, cfg.ff_height) + angle_corrector = np.random.random(size=cfg.max_particles)*0.5 + 0.01 + font_coords, red, green, blue, alpha = [], None, None, None, None + + text = 'FLUX' + font_size = 450 + font_path = 'assets/fonts/impact.ttf' + + def set_text(new_text): + nonlocal text + text = new_text + init_flowfield() + + def set_font_size(new_size): + nonlocal font_size + font_size = new_size + init_flowfield() + + def set_font_path(new_path): + nonlocal font_path + font_path = new_path + init_flowfield() + + def init_flowfield(): + nonlocal text, font_size, font_path, font_coords, w, h, red, green, blue, alpha, reset_particles + + w,h = (cfg.ff_width, cfg.ff_height) + cfg.reset_particles = reset_particles + font = ImageFont.truetype(font_path,size=font_size, encoding='utf-8') + img = Image.new(mode="RGBA", size=(w,h), color=(0, 0, 0, 0)) + img = radial_gradient(img, (w//2, h//2), (0, 0, 255), (255, 255, 0), font.getbbox(text, anchor='mm')) + + font_mask = Image.new('L', (w,h)) + draw = ImageDraw.Draw(font_mask) + draw.text((w//2, h//2), text, font=font, fill=255, anchor='mm') + + img.putalpha(font_mask) + + red = np.array(img.split()[0])/255.0 + green = np.array(img.split()[1])/255.0 + blue = np.array(img.split()[2])/255.0 + alpha = np.array(img.split()[3])/255.0 + font_coords = np.argwhere(alpha==1)[:, ::-1] + reset_particles(np.repeat(True, cfg.max_particles)) + + def reset_particles(reset_indices): + nonlocal font_coords + choice = np.random.choice(len(font_coords), size=np.sum(reset_indices), replace=False) + cfg.particles[:2, reset_indices] = font_coords[choice].T + cfg.particles[2, reset_indices] = np.random.randint(cfg.min_age, cfg.max_age + 1, size=np.sum(reset_indices)) + + + args = SimpleNamespace( + text = SimpleNamespace( + val = text, + callback = lambda sender, data, property: set_text(data), + type = cfg.TYPE_INPUT_TEXT + ), + font_size = SimpleNamespace( + val = 450, + min_val = 100, + max_val = 800, + type = cfg.TYPE_SLIDER_INT, + callback = lambda sender, data, property: set_font_size(data) + ) + ) + + def noise(particles, frame_count): + nonlocal previous_angles, args, angle_corrector, w, h, red, green, blue, alpha + x = np.clip(particles[0].astype(int), 0, w-1) + y = np.clip(particles[1].astype(int), 0,h-1) + + r = red[y,x] + g = green[y,x] + b = blue[y,x] + a = alpha[y,x] + + angles = (2 * np.pi * (r+g+b)/3)*a + if previous_angles is not None: + angles = np.select( [angles > previous_angles, angles < previous_angles], + [previous_angles + angle_corrector, previous_angles - angle_corrector], angles) + previous_angles = angles + dx = np.cos(angles) + dy = np.sin(angles) + return dx, dy + return args, noise, init_flowfield \ No newline at end of file diff --git a/flux/flowfield_functions/quattro.py b/flux/flowfield_functions/quattro.py new file mode 100644 index 0000000..47a855a --- /dev/null +++ b/flux/flowfield_functions/quattro.py @@ -0,0 +1,52 @@ +import numpy as np +import config as cfg +from types import SimpleNamespace + +def get_flowfield_function_name(): + return 'Quattro' + +def flowfield(): + TAU = np.pi * 2 + + def init_flowfield(): + cfg.reset_particles = cfg.default_reset_particles + + args = SimpleNamespace( + scale = SimpleNamespace( + val = 1500, + min_val = 500, + max_val = 5000 + ), + + a = SimpleNamespace( + val = 15, + min_val = 1, + max_val = 20 + ), + b = SimpleNamespace( + val = 10, + min_val = 1, + max_val = 20 + ), + n = SimpleNamespace( + val = 6, + min_val = 1, + max_val = 10 + ), + m = SimpleNamespace( + val = 4, + min_val = 1, + max_val = 10 + ) + ) + + init_flowfield() + + def noise(particles, frame_count): + nonlocal args, TAU + x, y = particles[:2, :]/(args.scale.val) + angles = np.cos(TAU*args.m.val*x)*np.cos(TAU*args.n.val*y)*args.a.val \ + - np.cos(TAU*args.n.val*x)*np.cos(TAU*args.m.val*y)*args.b.val + return np.cos(angles), np.sin(angles) + return args, noise, init_flowfield + diff --git a/flux/flowfield_functions/swirly.py b/flux/flowfield_functions/swirly.py new file mode 100644 index 0000000..131186e --- /dev/null +++ b/flux/flowfield_functions/swirly.py @@ -0,0 +1,31 @@ +import numpy as np +import config as cfg +from types import SimpleNamespace + +def get_flowfield_function_name(): + return 'Swirly' + +def flowfield(): + def init_flowfield(): + cfg.reset_particles = cfg.default_reset_particles + + args = SimpleNamespace( + curviness = SimpleNamespace( + val = 3.5, + min_val = 0.0, + max_val = 20.0 + ), + scale = SimpleNamespace( + val = 0.1, + min_val = 0.001, + max_val = 0.5 + ) + ) + + def noise(particles, frame_count): + nonlocal args + angles = (np.cos(particles[0]*args.scale.val) + np.sin(particles[1]*args.scale.val)) * args.curviness.val + return np.cos(angles), np.sin(angles) + + return args, noise, init_flowfield + diff --git a/flux/flowfield_functions/vortex.py b/flux/flowfield_functions/vortex.py new file mode 100644 index 0000000..d131aaa --- /dev/null +++ b/flux/flowfield_functions/vortex.py @@ -0,0 +1,38 @@ +import numpy as np +import config as cfg +from types import SimpleNamespace + +def get_flowfield_function_name(): + return 'Vortex' + +def flowfield(): + def spawn_attractors(sender, data, property): + nonlocal attractors, rotations, args + setattr(property, 'val', data) + attractors = (np.random.rand(data, 2) * (cfg.ff_width, cfg.ff_height)).astype(np.int32) + rotations = np.random.choice([np.pi / 2, -np.pi / 2], size=(data)) + + args = SimpleNamespace( + points = SimpleNamespace( + val = 3, + min_val = 1, + max_val = 20, + type = cfg.TYPE_SLIDER_INT, + callback = spawn_attractors + ) + ) + attractors = (np.random.rand(args.points.val, 2) * (cfg.ff_width, cfg.ff_height)).astype(np.int32) + rotations = np.random.choice([np.pi / 2, -np.pi / 2], size=(args.points.val)) + + def noise(particles, frame_count): + nonlocal args, attractors + vectors = attractors[:, :, np.newaxis] - particles[:2] + angles = np.arctan2(vectors[:,1,:], vectors[:,0,:]) + rotations[:, np.newaxis] + weights = 1/np.maximum(np.linalg.norm(vectors, axis=1), 1e-8) + f_sin = np.sum(np.sin(angles) * weights, axis=0) + f_cos = np.sum(np.cos(angles) * weights, axis=0) + + # Normalize the resulting angle components + norm = np.maximum(np.sqrt(f_sin**2 + f_cos**2), 1e-8) + return f_cos/norm, f_sin/norm + return args, noise, init_flowfield \ No newline at end of file diff --git a/flux/flux.py b/flux/flux.py new file mode 100644 index 0000000..1d51086 --- /dev/null +++ b/flux/flux.py @@ -0,0 +1,284 @@ +import dearpygui.dearpygui as dpg +import numpy as np +import color_functions as cf +import flowfield_functions as fff +import config as cfg + +def spawn_paricles(): + cfg.particles[0] = [np.random.random() * cfg.ff_width for _ in range(cfg.max_particles)] # X + cfg.particles[1] = [np.random.random() * cfg.ff_height for _ in range(cfg.max_particles)] # Y + cfg.particles[2] = [np.random.randint(cfg.min_age, cfg.max_age) for _ in range(cfg.max_particles)] # age + cfg.particles[3] = np.repeat(cfg.bg_color[0], cfg.max_particles) # Red + cfg.particles[4] = np.repeat(cfg.bg_color[1], cfg.max_particles) # Green + cfg.particles[5] = np.repeat(cfg.bg_color[2], cfg.max_particles) # Blue + cfg.particles[6] = np.repeat(cfg.p_alpha, cfg.max_particles) # Opacity + cfg.particles[7] = np.repeat(cfg.radius, cfg.max_particles) # Radius + # for each coordinates draw a particle + for p in cfg.particles.T: + p[8] = dpg.draw_circle(center=(p[0], p[1]), radius=cfg.radius, parent='flowfield', fill=list(p[3:7]), color=list(p[3:7]), show=False) # Reference to drawn object + +def recalc_particles(): + + dx, dy = cfg.ff_func.noise(cfg.particles, dpg.get_frame_count()) + + cfg.particles[0] = np.add(cfg.particles[0], np.multiply(dx, cfg.speed)) + cfg.particles[1] = np.add(cfg.particles[1], np.multiply(dy, cfg.speed)) + cfg.particles[2] = np.add(cfg.particles[2], -1) + + # Reset particles if out of bounds or expired + out_of_bounds = (cfg.particles[0] < 0) | (cfg.particles[0] > cfg.ff_width) | (cfg.particles[1] < 0) | (cfg.particles[1] > cfg.ff_height) + expired = cfg.particles[2] <= 0 + reset_indices = np.logical_or(out_of_bounds, expired) + cfg.reset_particles(reset_indices) + + args = { + 'particles': cfg.particles, + 'dx': dx, + 'dy': dy, + 'max_particles': cfg.max_particles, + 'ttl_particles': cfg.ttl_particles, + 'ff_width': cfg.ff_width, + 'ff_height': cfg.ff_height, + 'speed': cfg.speed, + 'min_age': cfg.min_age, + 'max_age': cfg.max_age, + } + cfg.particles[3:7] = cfg.clr_func(cfg.min_rgb, cfg.max_rgb, cfg.p_alpha, args) # RGB + + for p in cfg.particles[:,:cfg.ttl_particles].T: + dpg.configure_item(int(p[8]), radius=p[7], center=(p[0], p[1]), fill=list(p[3:7]), color=cfg.border_rgb if cfg.border else list(p[3:7]), show=True) + +def background(clr): + with dpg.mutex(): + # Window Background + if dpg.does_item_exist('w_background'): + dpg.configure_item('w_background', fill=clr, color=clr,pmax=(cfg.ff_width, cfg.ff_height), show=True) + else: + dpg.draw_rectangle(tag='w_background', pmin=(0,0), pmax=(cfg.ff_width, cfg.ff_height), parent='flowfield', fill=clr, color=clr) + +def dimmer(clr, d_alpha): + with dpg.mutex(): + # Dimmer + # Let the dimmer be drawn every frame + color = clr[:3] + color.append(cfg.d_alpha) + if dpg.does_item_exist('dimmer'): + dpg.configure_item('dimmer', pmax=(cfg.ff_width, cfg.ff_height), fill=color, color=color) + else: + dpg.draw_rectangle(tag='dimmer', pmin=(0,0), pmax=(cfg.ff_width, cfg.ff_height), parent='flowfield', fill=color, color=color) + +def handle_viewport_resize(sender, data): + with dpg.mutex(): + if data[2] != cfg.w_width or data[3] != cfg.w_height: + cfg.w_width, cfg.w_height = data[2:] + cfg.ff_width = cfg.w_width - cfg.sp_width + cfg.ff_height = cfg.w_height + dpg.configure_item('parameters', pos=(cfg.ff_width, 0)) # Update side panel position + background(cfg.bg_color) + dimmer(cfg.bg_color, cfg.d_alpha) + cfg.ff_func.init_flowfield() + dpg.set_frame_callback(dpg.get_frame_count()+1, callback=lambda: dpg.output_frame_buffer(callback=init_frame_buffer)) + + +def init_frame_buffer(sender, buffer): + # Prepare for frame buffer handling + with dpg.mutex(): + # Setup Initial Frame + with dpg.texture_registry(): + if dpg.does_item_exist(cfg.prev_frame_texture): + dpg.delete_item(cfg.prev_frame_texture) + cfg.prev_frame_texture = dpg.add_raw_texture(width=cfg.w_width, height=cfg.w_height, default_value=buffer, format=dpg.mvFormat_Float_rgba) + if dpg.does_item_exist(cfg.prev_frame): + dpg.delete_item(cfg.prev_frame) + cfg.prev_frame = dpg.add_image(cfg.prev_frame_texture, width=cfg.ff_width, height=cfg.ff_height, parent='flowfield', pos=(0,0), uv_min=(0,0), uv_max=(cfg.ff_width/cfg.w_width, 1)) + # Start the frame buffer + + dpg.set_frame_callback(dpg.get_frame_count()+1, callback=lambda: dpg.output_frame_buffer(callback=clear_frame)) + +def clear_frame(sender, buffer): + with dpg.mutex(): + if dpg.is_item_shown(cfg.prev_frame): + dpg.hide_item(cfg.prev_frame) + dpg.set_frame_callback(dpg.get_frame_count()+1, callback=lambda: dpg.output_frame_buffer(callback=clear_frame)) + else: + dpg.set_value(cfg.prev_frame, buffer) + dpg.show_item(cfg.prev_frame) + # Make sure the background is only drawn once + if dpg.is_item_shown('w_background'): + dpg.hide_item('w_background') + dpg.set_frame_callback(dpg.get_frame_count()+1, callback=lambda: dpg.output_frame_buffer(callback=handle_frame_buffer)) + +def handle_frame_buffer(sender, buffer): + with dpg.mutex(): + dpg.set_value(cfg.prev_frame_texture, buffer) + recalc_particles() + dpg.set_frame_callback(dpg.get_frame_count()+3, callback=lambda: dpg.output_frame_buffer(callback=handle_frame_buffer)) + +def setup_flux(): + # Callbacks + def set_flowfield_function(sender, data): + dpg.delete_item(cfg.ff_func_settings, children_only=True) + cfg.ff_func = fff.get_flowfield_function(data) + + def set_ttl_particles(sender, data): + if data > cfg.ttl_particles: + for p in cfg.particles[8,cfg.ttl_particles:min(data+1, cfg.max_particles)]: + dpg.configure_item(int(p), show=True) + else: + for p in cfg.particles[8, data:cfg.ttl_particles]: + dpg.configure_item(int(p), show=False) + cfg.ttl_particles = data + + def set_particle_speed(sender, data): + cfg.speed = data + + def set_particle_radius(sender, data): + cfg.radius = data + cfg.particles[7] = np.repeat(cfg.radius, cfg.max_particles) if not cfg.random_radius else np.random.rand(cfg.max_particles)*cfg.radius + + def set_random_radius(sender, data): + cfg.random_radius = data + cfg.particles[7] = np.repeat(cfg.radius, cfg.max_particles) if not cfg.random_radius else np.random.rand(cfg.max_particles)*cfg.radius + + + def set_color_function(sender, data): + cfg.clr_func = cf.get_color_function(data) + + def set_min_max_age(sender, data): + if sender == 'min-age': + cfg.min_age = data + elif sender == 'max-age': + cfg.max_age = data + + def set_min_max_rgb(sender, data): + if sender == 'min_rgb': + cfg.min_rgb = [int(c*255) for c in data] + elif sender == 'max_rgb': + cfg.max_rgb = [int(c*255) for c in data] + + def set_particle_opacity(sender, data): + cfg.p_alpha = data + + def set_border(sender, data): + cfg.border = data + + def set_border_rgb(sender, data): + cfg.border_rgb = [int(c*255) for c in data] + + def set_dimmer_opacity(semder, data): + cfg.d_alpha = data + background(cfg.bg_color) + dimmer(cfg.bg_color, cfg.d_alpha) + dpg.set_frame_callback(dpg.get_frame_count()+1, callback=lambda: dpg.output_frame_buffer(callback=clear_frame)) + + + def set_background_color(sender, data): + r,g,b,a = [int(c*255) for c in data] + cfg.bg_color = [r,g,b,a] + background(cfg.bg_color) + dimmer(cfg.bg_color, cfg.d_alpha) + # Calculate Luminance of background + # Needed for runntime UI themes. + l = 0.2126 * r + 0.7152 * g + 0.0722 * b + threshold = 180 + a = 180 + with dpg.theme() as flowfield_theme: + with dpg.theme_component(dpg.mvAll): + dpg.add_theme_style(dpg.mvStyleVar_WindowPadding, 0) + dpg.add_theme_color(dpg.mvThemeCol_ChildBg, [r,g,b,a], category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_Text, [255,255,255,255] if l < threshold else [0,0,0,255], category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_FrameBg, [int(r-(r/20)),int(g-(g/20)),int(b-(b/20)),100], category=dpg.mvThemeCat_Core) + dpg.bind_item_theme('flowfield', flowfield_theme) + dpg.set_frame_callback(dpg.get_frame_count()+1, callback=lambda: dpg.output_frame_buffer(callback=clear_frame)) + + def handle_dropdown(sender, data, group): + if dpg.is_item_shown(group): + dpg.configure_item(sender, direction=dpg.mvDir_Right) + dpg.configure_item(group, show=False) + else: + dpg.configure_item(sender, direction=dpg.mvDir_Down) + dpg.configure_item(group, show=True) + + # Main Window + with dpg.window(tag='flowfield', pos=(0,0)): + # Theme setting + dpg.set_primary_window('flowfield', True) + with dpg.theme() as flowfield_theme: + with dpg.theme_component(dpg.mvAll): + dpg.add_theme_style(dpg.mvStyleVar_WindowPadding, 0) + dpg.add_theme_color(dpg.mvThemeCol_ChildBg, cfg.bg_color, category=dpg.mvThemeCat_Core) + dpg.bind_item_theme('flowfield', flowfield_theme) + + # Flux GUI + with dpg.child_window(tag='parameters', pos=(cfg.ff_width, 0), width=cfg.sp_width, height=-1): + with dpg.theme() as side_panel_theme: + with dpg.theme_component(dpg.mvAll): + dpg.add_theme_style(dpg.mvStyleVar_WindowPadding, 8) + dpg.bind_item_theme('parameters', side_panel_theme) + + # Flowfield Settings UI + dpg.add_spacer(height=3) + with dpg.group(horizontal=True): + dpg.add_button(tag='ff-dropdown', arrow=True, direction=dpg.mvDir_Down, callback=handle_dropdown, user_data='flowfield-settings') + dpg.add_text(default_value='Flowfield Properties') + with dpg.group(tag='flowfield-settings'): + dpg.add_combo(width=cfg.sp_width/2, label='FlowField Function', tag='flowfield-functions', items=fff.get_flowfield_function_names(), default_value=cfg.default_fff, callback=set_flowfield_function) + with dpg.group(tag=cfg.ff_func_settings): + # UI container for selected Flowfield Function. + pass + dpg.add_separator() + + # Particle Settings UI + with dpg.group(horizontal=True): + dpg.add_button(tag='pp-dropdown', arrow=True, direction=dpg.mvDir_Down, callback=handle_dropdown, user_data='particle-settings') + dpg.add_text(default_value='Particle Properties') + with dpg.group(tag='particle-settings'): + dpg.add_slider_int(width=cfg.sp_width/2, label='particles', tag='total-cfg.particles', min_value=0, default_value=cfg.ttl_particles, max_value=cfg.max_particles, callback=set_ttl_particles) + dpg.add_slider_float(width=cfg.sp_width/2, label='speed', min_value=0.5, default_value=cfg.speed, max_value=4, callback=set_particle_speed) + dpg.add_slider_int(width=cfg.sp_width/2, label='min age', tag='min-age', min_value=cfg.min_age, default_value=cfg.min_age, max_value=100, callback=set_min_max_age) + dpg.add_slider_int(width=cfg.sp_width/2, label='max age', tag='max-age', min_value=101, default_value=cfg.max_age, max_value=cfg.max_age, callback=set_min_max_age) + dpg.add_checkbox(label='random radius', tag='random-radius', default_value=cfg.random_radius, callback=set_random_radius) + dpg.add_slider_float(width=cfg.sp_width/2, label='radius', tag='radius', min_value=0, default_value=cfg.radius, max_value=cfg.max_radius, callback=set_particle_radius) + + dpg.add_separator() + + # Color Settings UI + with dpg.group(horizontal=True): + dpg.add_button(tag='cl-dropdown', arrow=True, direction=dpg.mvDir_Down, callback=handle_dropdown, user_data='color-settings') + dpg.add_text(default_value='Color Settings') + with dpg.group(tag='color-settings'): + dpg.add_combo(width=cfg.sp_width/2, label='Color Function', tag='color-functions', items=cf.get_color_function_names(), default_value=cfg.default_cf, callback=set_color_function) + with dpg.tab_bar(tag='color-pickers'): + with dpg.tab(tag='background', label='background'): + dpg.add_slider_int(width=cfg.sp_width/2, label='dimmer alpha', default_value=cfg.d_alpha, max_value=255, callback=set_dimmer_opacity) + dpg.add_color_picker(width=cfg.sp_width/2, label='background', tag='bg_rgb', default_value=cfg.bg_color, no_tooltip=True, no_alpha=True, callback=set_background_color) + with dpg.tab(tag='particles', label='particles'): + dpg.add_slider_int(width=cfg.sp_width/2, label='particle alpha', default_value=cfg.p_alpha, max_value=255, callback=set_particle_opacity) + dpg.add_color_picker(width=cfg.sp_width/2, label='min_rgb', tag='min_rgb', default_value=cfg.min_rgb, no_tooltip=True, no_alpha=True, callback=set_min_max_rgb) + dpg.add_color_picker(width=cfg.sp_width/2, label='max_rgb', tag='max_rgb', default_value=cfg.max_rgb, no_tooltip=True, no_alpha=True, callback=set_min_max_rgb) + dpg.add_checkbox(label='Border', tag='border', default_value=cfg.border, callback=set_border) + dpg.add_color_picker(width=cfg.sp_width/2, label='border_rgb', tag='border_rgb', default_value=cfg.border_rgb, no_tooltip=True, callback=set_border_rgb) + # Bottom Padding + dpg.add_spacer(height=3) + + # initializing with default functions + spawn_paricles() + cfg.ff_func = fff.get_flowfield_function(cfg.default_fff) # FlowField Function + cfg.clr_func = cf.get_color_function(cfg.default_cf) # Color Function + + + +def start_flux(): + # Start Flux + dpg.create_context() + dpg.create_viewport(title='Flux', width=cfg.w_width, height=cfg.w_height) + dpg.setup_dearpygui() + setup_flux() + dpg.show_viewport() + dpg.set_viewport_resize_callback(callback=handle_viewport_resize) + # dpg.set_frame_callback(20, callback=lambda: dpg.output_frame_buffer(callback=init_frame_buffer)) + # dpg.set_viewport_vsync(False) + # dpg.show_metrics() + # dpg.show_style_editor() + dpg.start_dearpygui() + dpg.destroy_context() diff --git a/requirements.txt b/requirements.txt index 409707d..8a0ed66 100644 Binary files a/requirements.txt and b/requirements.txt differ