Rename to Fractal Viewer, add Sierpinsky Carpet, use arcade 3.2.0

instead
of development
This commit is contained in:
csd4ni3l
2025-05-24 09:24:22 +02:00
parent c295fec105
commit 51c0c6ef05
13 changed files with 280 additions and 109 deletions

View File

@@ -17,9 +17,9 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
architecture: 'x64'
cache: 'pip'
python-version: "3.11"
architecture: "x64"
cache: "pip"
cache-dependency-path: |
**/requirements*.txt
@@ -37,24 +37,24 @@ jobs:
include-data-dir: assets=assets
include-data-files: CREDITS=CREDITS
mode: standalone
output-file: MandelBrotViewer
output-file: FractalViewer
- name: Zip Build Output
shell: bash
run: |
mkdir -p zip_output
if [ "${{ runner.os }}" = "Windows" ]; then
powershell.exe -Command "Compress-Archive -Path 'build/run.dist/*' -DestinationPath 'zip_output/MandelBrotViewer-${{ runner.os }}.zip'"
powershell.exe -Command "Compress-Archive -Path 'build/run.dist/*' -DestinationPath 'zip_output/FractalViewer-${{ runner.os }}.zip'"
else
cd build/run.dist
zip -r "../../zip_output/MandelBrotViewer-${{ runner.os }}.zip" .
zip -r "../../zip_output/FractalViewer-${{ runner.os }}.zip" .
fi
- name: Upload Zipped Build Artifact
uses: actions/upload-artifact@v4
with:
name: MandelBrotViewer-${{ runner.os }}.zip
path: zip_output/MandelBrotViewer-${{ runner.os }}.zip
name: FractalViewer-${{ runner.os }}.zip
path: zip_output/FractalViewer-${{ runner.os }}.zip
release:
name: Create GitHub Release
needs: build
@@ -88,6 +88,6 @@ jobs:
git push origin latest
- name: Create the new release
run: gh release create latest downloads/**/MandelBrotViewer-*.zip --title "Latest Build" --notes "Most recent multi-platform builds of MandelBrotViewer"
run: gh release create latest downloads/**/FractalViewer-*.zip --title "Latest Build" --notes "Most recent multi-platform builds of FractalViewer"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1 +1,3 @@
Mandelbrot viewer in Python using compute shaders and the Arcade and Pyglet modules.
Fractal viewer in Python using compute shaders and the Arcade and Pyglet modules.
Currently supports Sierpinsky Carpet and Mandelbrot.

95
game/mandelbrot.py Normal file
View File

@@ -0,0 +1,95 @@
import arcade, arcade.gui, pyglet, json
from PIL import Image
from game.shader import create_mandelbrot_shader
from utils.constants import menu_background_color, button_style, initial_real_min, initial_real_max, initial_imag_min, initial_imag_max
from utils.preload import button_texture, button_hovered_texture
class MandelbrotViewer(arcade.gui.UIView):
def __init__(self, pypresence_client):
super().__init__()
self.pypresence_client = pypresence_client
self.real_min = initial_real_min
self.real_max = initial_real_max
self.imag_min = initial_imag_min
self.imag_max = initial_imag_max
with open("settings.json", "r") as file:
self.settings_dict = json.load(file)
self.max_iter = self.settings_dict.get("mandelbrot_max_iter", 200)
self.zoom = 1.0
def on_show_view(self):
super().on_show_view()
self.shader_program, self.mandelbrot_image = create_mandelbrot_shader(self.window.width, self.window.height, self.settings_dict.get("mandelbrot_precision", "Single").lower())
self.mandelbrot_sprite = pyglet.sprite.Sprite(img=self.mandelbrot_image)
self.create_image()
self.pypresence_client.update(state='Viewing Mandelbrot', details=f'Zoom: {self.zoom}\nMax Iterations: {self.max_iter}', start=self.pypresence_client.start_time)
self.setup_ui()
def main_exit(self):
from menus.main import Main
self.window.show_view(Main(self.pypresence_client))
def setup_ui(self):
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
self.info_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10, vertical=False), anchor_x="center", anchor_y="top")
self.zoom_label = self.info_box.add(arcade.gui.UILabel(text=f"Zoom: {self.zoom}", font_name="Protest Strike", font_size=16))
self.max_iter_label = self.info_box.add(arcade.gui.UILabel(text=f"Max Iterations: {self.max_iter}", font_name="Protest Strike", font_size=16))
self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
self.back_button.on_click = lambda event: self.main_exit()
self.anchor.add(self.back_button, anchor_x="left", anchor_y="top", align_x=5, align_y=-5)
def zoom_at(self, center_x, center_y, zoom_factor):
center_real = self.real_min + (center_x / self.width) * (self.real_max - self.real_min)
center_imag = self.imag_min + (center_y / self.height) * (self.imag_max - self.imag_min)
new_real_range = (self.real_max - self.real_min) / zoom_factor
new_imag_range = (self.imag_max - self.imag_min) / zoom_factor
self.real_min = center_real - new_real_range / 2
self.real_max = center_real + new_real_range / 2
self.imag_min = center_imag - new_imag_range / 2
self.imag_max = center_imag + new_imag_range / 2
def create_image(self):
with self.shader_program:
self.shader_program['u_maxIter'] = self.max_iter
self.shader_program['u_resolution'] = (self.window.width, self.window.height)
self.shader_program['u_real_range'] = (self.real_min, self.real_max)
self.shader_program['u_imag_range'] = (self.imag_min, self.imag_max)
self.shader_program.dispatch(self.mandelbrot_image.width, self.mandelbrot_image.height, 1, barrier=pyglet.gl.GL_ALL_BARRIER_BITS)
def on_mouse_press(self, x: int, y: int, button: int, modifiers: int) -> bool | None:
super().on_mouse_press(x, y, button, modifiers)
if button == arcade.MOUSE_BUTTON_LEFT:
zoom = self.settings_dict.get("mandelbrot_zoom_increase", 2)
elif button == arcade.MOUSE_BUTTON_RIGHT:
zoom = 1 / self.settings_dict.get("mandelbrot_zoom_increase", 2)
else:
return
self.zoom *= zoom
self.zoom_label.text = f"Zoom: {self.zoom}"
self.zoom_at(self.window.mouse.data["x"], self.window.mouse.data["y"], zoom)
self.create_image()
self.pypresence_client.update(state='Viewing Mandelbrot', details=f'Zoom: {self.zoom}\nMax Iterations: {self.max_iter}', start=self.pypresence_client.start_time)
def on_draw(self):
self.window.clear()
self.mandelbrot_sprite.draw()
self.ui.draw()

View File

@@ -1,95 +1,42 @@
import arcade, arcade.gui, pyglet, json
import arcade, arcade.gui
from PIL import Image
from game.shader import create_mandelbrot_shader
from utils.constants import menu_background_color, button_style, initial_real_min, initial_real_max, initial_imag_min, initial_imag_max
from utils.constants import button_style
from utils.preload import button_texture, button_hovered_texture
class Game(arcade.gui.UIView):
class FractalChooser(arcade.gui.UIView):
def __init__(self, pypresence_client):
super().__init__()
self.pypresence_client = pypresence_client
self.real_min = initial_real_min
self.real_max = initial_real_max
self.imag_min = initial_imag_min
self.imag_max = initial_imag_max
with open("settings.json", "r") as file:
self.settings_dict = json.load(file)
self.max_iter = self.settings_dict.get("max_iter", 200)
self.zoom = 1.0
def on_show_view(self):
super().on_show_view()
self.shader_program, self.mandelbrot_image = create_mandelbrot_shader(self.window.width, self.window.height, self.settings_dict.get("precision", "Single").lower())
self.mandelbrot_sprite = pyglet.sprite.Sprite(img=self.mandelbrot_image)
self.create_image()
self.pypresence_client.update(state='Viewing Mandelbrot', details=f'Zoom: {self.zoom}\nMax Iterations: {self.max_iter}', start=self.pypresence_client.start_time)
self.setup_ui()
def main_exit(self):
from menus.main import Main
self.window.show_view(Main(self.pypresence_client))
def setup_ui(self):
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
self.info_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10, vertical=False), anchor_x="center", anchor_y="top")
self.zoom_label = self.info_box.add(arcade.gui.UILabel(text=f"Zoom: {self.zoom}", font_name="Protest Strike", font_size=16))
self.max_iter_label = self.info_box.add(arcade.gui.UILabel(text=f"Max Iterations: {self.max_iter}", font_name="Protest Strike", font_size=16))
self.grid = self.add_widget(arcade.gui.UIGridLayout(row_count=3, column_count=3, horizontal_spacing=10, vertical_spacing=10))
self.anchor.add(self.grid, anchor_x="center", anchor_y="center")
self.title_label = self.anchor.add(arcade.gui.UILabel(text="Choose a fractal to view.", font_name="Protest Strike", font_size=32), anchor_x="center", anchor_y="top", align_y=-50)
self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
self.back_button.on_click = lambda event: self.main_exit()
self.anchor.add(self.back_button, anchor_x="left", anchor_y="top", align_x=5, align_y=-5)
def zoom_at(self, center_x, center_y, zoom_factor):
center_real = self.real_min + (center_x / self.width) * (self.real_max - self.real_min)
center_imag = self.imag_min + (center_y / self.height) * (self.imag_max - self.imag_min)
self.mandelbrot_button = self.grid.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Mandelbrot', style=button_style, width=200, height=200), row=0, column=0)
self.mandelbrot_button.on_click = lambda event: self.mandelbrot()
new_real_range = (self.real_max - self.real_min) / zoom_factor
new_imag_range = (self.imag_max - self.imag_min) / zoom_factor
self.sierpinsky_carpet_button = self.grid.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Sierpinsky Carpet', style=button_style, width=200, height=200), row=0, column=1)
self.sierpinsky_carpet_button.on_click = lambda event: self.sierpinsky_carpet()
self.real_min = center_real - new_real_range / 2
self.real_max = center_real + new_real_range / 2
self.imag_min = center_imag - new_imag_range / 2
self.imag_max = center_imag + new_imag_range / 2
def main_exit(self):
from menus.main import Main
self.window.show_view(Main(self.pypresence_client))
def create_image(self):
with self.shader_program:
self.shader_program['u_maxIter'] = self.max_iter
self.shader_program['u_resolution'] = (self.window.width, self.window.height)
self.shader_program['u_real_range'] = (self.real_min, self.real_max)
self.shader_program['u_imag_range'] = (self.imag_min, self.imag_max)
self.shader_program.dispatch(self.mandelbrot_image.width, self.mandelbrot_image.height, 1, barrier=pyglet.gl.GL_ALL_BARRIER_BITS)
def mandelbrot(self):
from game.mandelbrot import MandelbrotViewer
self.window.show_view(MandelbrotViewer(self.pypresence_client))
def on_mouse_press(self, x: int, y: int, button: int, modifiers: int) -> bool | None:
super().on_mouse_press(x, y, button, modifiers)
if button == arcade.MOUSE_BUTTON_LEFT:
zoom = self.settings_dict.get("zoom_increase", 2)
elif button == arcade.MOUSE_BUTTON_RIGHT:
zoom = 1 / self.settings_dict.get("zoom_increase", 2)
else:
return
self.zoom *= zoom
self.zoom_label.text = f"Zoom: {self.zoom}"
self.zoom_at(self.window.mouse.data["x"], self.window.mouse.data["y"], zoom)
self.create_image()
self.pypresence_client.update(state='Viewing Mandelbrot', details=f'Zoom: {self.zoom}\nMax Iterations: {self.max_iter}', start=self.pypresence_client.start_time)
def on_draw(self):
self.window.clear()
self.mandelbrot_sprite.draw()
self.ui.draw()
def sierpinsky_carpet(self):
from game.sierpinsky_carpet import SierpinskyCarpetViewer
self.window.show_view(SierpinskyCarpetViewer(self.pypresence_client))

View File

@@ -50,6 +50,43 @@ void main() {
}
"""
sierpinsky_carpet_compute_source = """#version 430 core
uniform int u_depth;
uniform int u_zoom;
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout(location = 0, rgba32f) uniform image2D img_output;
void main() {
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
bool isHole = false;
for (int i = 0; i < u_depth; ++i) {
if (coord.x % 3 == 1 && coord.y % 3 == 1) {
isHole = true;
break;
}
coord /= 3;
}
vec4 color = isHole ? vec4(0, 0, 0, 1) : vec4(1, 1, 1, 1);
imageStore(img_output, ivec2(gl_GlobalInvocationID.xy), color);
}
"""
def create_sierpinsky_carpet_shader(width, height, precision="single"):
shader_source = sierpinsky_carpet_compute_source
shader_program = pyglet.graphics.shader.ComputeShaderProgram(shader_source)
sierpinsky_carpet_image = pyglet.image.Texture.create(width, height, internalformat=pyglet.gl.GL_RGBA32F)
uniform_location = shader_program['img_output']
sierpinsky_carpet_image.bind_image_texture(unit=uniform_location)
return shader_program, sierpinsky_carpet_image
def create_mandelbrot_shader(width, height, precision="single"):
shader_source = mandelbrot_compute_source

81
game/sierpinsky_carpet.py Normal file
View File

@@ -0,0 +1,81 @@
import arcade, arcade.gui, pyglet, json
from PIL import Image
from game.shader import create_sierpinsky_carpet_shader
from utils.constants import menu_background_color, button_style
from utils.preload import button_texture, button_hovered_texture
class SierpinskyCarpetViewer(arcade.gui.UIView):
def __init__(self, pypresence_client):
super().__init__()
self.pypresence_client = pypresence_client
with open("settings.json", "r") as file:
self.settings_dict = json.load(file)
self.depth = self.settings_dict.get("sierpinsky_depth", 10)
self.zoom = 1.0
self.click_center = (self.width / 2, self.height / 2)
def on_show_view(self):
super().on_show_view()
self.shader_program, self.sierpinsky_carpet_image = create_sierpinsky_carpet_shader(self.window.width, self.window.height, self.settings_dict.get("precision", "Single").lower())
self.sierpinsky_carpet_sprite = pyglet.sprite.Sprite(img=self.sierpinsky_carpet_image)
self.create_image()
self.pypresence_client.update(state='Viewing Sierpinsky Carpet', details=f'Zoom: {self.zoom}\nDepth: {self.depth}', start=self.pypresence_client.start_time)
self.setup_ui()
def main_exit(self):
from menus.main import Main
self.window.show_view(Main(self.pypresence_client))
def setup_ui(self):
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
self.info_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10, vertical=False), anchor_x="center", anchor_y="top")
self.zoom_label = self.info_box.add(arcade.gui.UILabel(text=f"Zoom: {self.zoom}", font_name="Protest Strike", font_size=16))
self.depth_label = self.info_box.add(arcade.gui.UILabel(text=f"Depth: {self.depth}", font_name="Protest Strike", font_size=16))
self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
self.back_button.on_click = lambda event: self.main_exit()
self.anchor.add(self.back_button, anchor_x="left", anchor_y="top", align_x=5, align_y=-5)
def create_image(self):
with self.shader_program:
self.shader_program['u_depth'] = self.depth
#self.shader_program['u_zoom'] = int(self.zoom)
#self.shader_program['u_resolution'] = self.window.size
#self.shader_program['u_center'] = self.click_center
self.shader_program.dispatch(self.sierpinsky_carpet_image.width, self.sierpinsky_carpet_image.height, 1, barrier=pyglet.gl.GL_ALL_BARRIER_BITS)
def on_mouse_press(self, x: int, y: int, button: int, modifiers: int) -> bool | None:
super().on_mouse_press(x, y, button, modifiers)
if button == arcade.MOUSE_BUTTON_LEFT:
zoom = self.settings_dict.get("sierpinsky_zoom_increase", 2)
elif button == arcade.MOUSE_BUTTON_RIGHT:
zoom = 1 / self.settings_dict.get("sierpinsky_zoom_increase", 2)
else:
return
self.zoom *= zoom
self.click_center = (x, y)
self.zoom_label.text = f"Zoom: {self.zoom}"
self.create_image()
self.pypresence_client.update(state='Viewing Sierpinsky Carpet', details=f'Zoom: {self.zoom}\nMax Iterations: {self.depth}', start=self.pypresence_client.start_time)
def on_draw(self):
self.window.clear()
self.sierpinsky_carpet_sprite.draw()
self.ui.draw()

View File

@@ -1,4 +1,5 @@
import arcade, arcade.gui, asyncio, pypresence, time, copy, json
from game.play import FractalChooser
from utils.preload import button_texture, button_hovered_texture
from utils.constants import big_button_style, discord_presence_id
from utils.utils import FakePyPresence
@@ -50,7 +51,7 @@ class Main(arcade.gui.UIView):
def on_show_view(self):
super().on_show_view()
self.title_label = self.box.add(arcade.gui.UILabel(text="Mandelbrot Viewer", font_name="Protest Strike", font_size=48))
self.title_label = self.box.add(arcade.gui.UILabel(text="Fractal Viewer", font_name="Protest Strike", font_size=48))
self.play_button = self.box.add(arcade.gui.UITextureButton(text="Play", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=150, style=big_button_style))
self.play_button.on_click = lambda event: self.play()
@@ -59,8 +60,8 @@ class Main(arcade.gui.UIView):
self.settings_button.on_click = lambda event: self.settings()
def play(self):
from game.play import Game
self.window.show_view(Game(self.pypresence_client))
from game.play import FractalChooser
self.window.show_view(FractalChooser(self.pypresence_client))
def settings(self):
from menus.settings import Settings

View File

@@ -1,10 +1,10 @@
[project]
name = "MandelbrotViewer"
name = "FractalViewer"
version = "0.1.0"
description = "Mandelbrot viewer in Python using compute shaders and the Arcade and Pyglet modules."
description = "Fractal viewer in Python using compute shaders and the Arcade and Pyglet modules."
readme = "README.md"
requires-python = ">=3.11"
dependencies = ["arcade", "pypresence>=4.3.0"]
[tool.uv.sources]
arcade = { git = "https://github.com/pythonarcade/arcade.git", rev = "development" }
dependencies = [
"arcade==3.2.0",
"pypresence>=4.3.0",
]

View File

@@ -1,3 +1,3 @@
Pillow
git+https://github.com/pythonarcade/arcade.git@development
arcade==3.2.0
pypresence

2
run.py
View File

@@ -64,7 +64,7 @@ else:
with open("settings.json", "w") as file:
file.write(json.dumps(settings))
window = arcade.Window(width=resolution[0], height=resolution[1], title='Mandelbrot Viewer', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style)
window = arcade.Window(width=resolution[0], height=resolution[1], title='Fractal Viewer', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style)
if vsync:
window.set_vsync(True)

View File

@@ -27,9 +27,13 @@ slider_style = {'normal': slider_default_style, 'hover': slider_hover_style, 'pr
settings = {
"Mandelbrot": {
"Float Precision": {"type": "option", "options": ["Single", "Double"], "config_key": "precision", "default": "Single"},
"Zoom Increase Per Click": {"type": "slider", "min": 2, "max": 100, "config_key": "zoom_increase", "default": 2},
"Max Iterations": {"type": "slider", "min": 100, "max": 10000, "config_key": "max_iter", "default": 200}
"Float Precision": {"type": "option", "options": ["Single", "Double"], "config_key": "mandelbrot_precision", "default": "Single"},
"Zoom Increase Per Click": {"type": "slider", "min": 2, "max": 100, "config_key": "mandelbrot_zoom_increase", "default": 2},
"Max Iterations": {"type": "slider", "min": 100, "max": 10000, "config_key": "mandelbrot_max_iter", "default": 200}
},
"Sierpinsky Carpet": {
"Zoom Increase Per Click": {"type": "slider", "min": 2, "max": 100, "config_key": "sierpinsky_zoom_increase", "default": 2},
"Depth": {"type": "slider", "min": 2, "max": 10000, "config_key": "sierpinsky_depth", "default": 10}
},
"Graphics": {
"Window Mode": {"type": "option", "options": ["Windowed", "Fullscreen", "Borderless"], "config_key": "window_mode", "default": "Windowed"},

View File

@@ -52,7 +52,7 @@ class ErrorView(arcade.gui.UIView):
def on_show_view(self):
super().on_show_view()
self.window.set_caption('Mandelbrot Viewer - Error')
self.window.set_caption('Fractal Viewer - Error')
self.window.set_mouse_visible(True)
self.window.set_exclusive_mouse(False)
arcade.set_background_color(menu_background_color)

12
uv.lock generated
View File

@@ -4,14 +4,18 @@ requires-python = ">=3.11"
[[package]]
name = "arcade"
version = "3.1.0"
source = { git = "https://github.com/pythonarcade/arcade.git?rev=development#a17e95502bc0c7cd0527445e62bff84a49183c43" }
version = "3.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pillow" },
{ name = "pyglet" },
{ name = "pymunk" },
{ name = "pytiled-parser" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e5/39/87eaffdfc50ec9d4b4573652ef8b80cca0592e5ccafb5fc5bc8612b1445d/arcade-3.2.0.tar.gz", hash = "sha256:1c2c56181560665f6542157b9ab316b9551274a9ee8468bae017ed5b8fee18fd", size = 41941030, upload_time = "2025-05-09T20:16:20.112Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/40/9a/ac86f5cbccfe5455a28308fcf2d7179af8d9c3087ad4eb45706c2a7b089b/arcade-3.2.0-py3-none-any.whl", hash = "sha256:7bb47cf643b43272e4300d8a5ca5f1b1e9e131b0f3f1d3fad013cb29528d3062", size = 42635264, upload_time = "2025-05-09T20:16:15.98Z" },
]
[[package]]
name = "attrs"
@@ -68,7 +72,7 @@ wheels = [
]
[[package]]
name = "mandelbrotviewer"
name = "fractalviewer"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
@@ -78,7 +82,7 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "arcade", git = "https://github.com/pythonarcade/arcade.git?rev=development" },
{ name = "arcade", specifier = "==3.2.0" },
{ name = "pypresence", specifier = ">=4.3.0" },
]