Last active
March 4, 2025 06:16
-
-
Save Jeremiah-England/7ee1a484fb99796f5c3ba84492e6a56b to your computer and use it in GitHub Desktop.
Snowflakes Generator with Pygame and Pyscript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<meta name="viewport" content="width=device-width,initial-scale=1" /> | |
<title>Snowflake Generator - PyScript</title> | |
<link rel="stylesheet" href="https://pyscript.net/releases/2025.2.4/core.css"> | |
<script type="module" src="https://pyscript.net/releases/2025.2.4/core.js"></script> | |
<style> | |
body { | |
background-color: black; | |
color: white; | |
font-family: Arial, sans-serif; | |
margin: 0; | |
padding: 0; | |
overflow: hidden; | |
height: 100vh; | |
display: flex; | |
flex-direction: column; | |
} | |
#header { | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
padding: 5px 20px; | |
background-color: rgba(20, 20, 20, 0.8); | |
flex-shrink: 0; | |
} | |
h1 { | |
margin: 0; | |
flex: 1; | |
text-align: center; | |
} | |
#info { | |
color: #aaa; | |
font-size: 16px; | |
margin-right: 20px; | |
min-width: 150px; | |
} | |
#controls { | |
display: flex; | |
gap: 10px; | |
} | |
#snowflakes-container { | |
width: 100%; | |
flex: 1; /* Take up all available space */ | |
margin: 0 auto; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
} | |
button { | |
padding: 6px 12px; | |
background-color: #444; | |
color: white; | |
border: none; | |
border-radius: 4px; | |
cursor: pointer; | |
} | |
button:hover { | |
background-color: #555; | |
} | |
/* Info Modal Styles */ | |
#info-button { | |
margin-left: 10px; | |
font-size: 16px; | |
width: 30px; | |
height: 30px; | |
border-radius: 50%; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
background-color: #555; | |
} | |
#modal-overlay { | |
position: fixed; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background-color: rgba(0, 0, 0, 0.7); | |
display: none; | |
align-items: center; | |
justify-content: center; | |
z-index: 1000; | |
} | |
#info-modal { | |
background-color: #222; | |
border-radius: 8px; | |
padding: 20px; | |
max-width: 600px; | |
max-height: 80vh; | |
overflow-y: auto; | |
color: #ddd; | |
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); | |
} | |
#info-modal h2 { | |
margin-top: 0; | |
color: #fff; | |
border-bottom: 1px solid #444; | |
padding-bottom: 10px; | |
} | |
#info-modal section { | |
margin-bottom: 20px; | |
} | |
#info-modal h3 { | |
color: #aaa; | |
margin-bottom: 8px; | |
} | |
#modal-close { | |
background-color: #444; | |
color: white; | |
border: none; | |
border-radius: 4px; | |
padding: 8px 16px; | |
cursor: pointer; | |
margin-top: 10px; | |
} | |
#modal-close:hover { | |
background-color: #555; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="header"> | |
<div id="info"></div> | |
<h1>Snowflake Generator</h1> | |
<div id="controls"> | |
<button id="pause-btn">Pause</button> | |
<button id="zoom-in-btn">Zoom In</button> | |
<button id="zoom-out-btn">Zoom Out</button> | |
<button id="info-button">ⓘ</button> | |
</div> | |
</div> | |
<div id="snowflakes-container"></div> | |
<!-- Information Modal --> | |
<div id="modal-overlay"> | |
<div id="info-modal"> | |
<h2>About Snowflake Generator</h2> | |
<section> | |
<h3>Controls</h3> | |
<ul> | |
<li><strong>Drag mouse:</strong> Pan camera</li> | |
<li><strong>Mouse wheel:</strong> Zoom in/out</li> | |
<li><strong>Arrow keys / WASD:</strong> Pan camera</li> | |
<li><strong>Space:</strong> Pause/Resume</li> | |
<li><strong>+/- keys:</strong> Zoom in/out</li> | |
</ul> | |
</section> | |
<section> | |
<h3>About PyGame</h3> | |
<p>PyGame is a set of Python modules designed for writing video games. It provides functionality for graphics, sound, input handling, and more, making it easy to create interactive applications.</p> | |
</section> | |
<section> | |
<h3>About PyScript</h3> | |
<p>PyScript allows Python to run in web browsers, bringing Python's capabilities to the web platform. It enables developers to create Python applications that can be run directly in a browser without server-side processing.</p> | |
</section> | |
<section> | |
<h3>Credits</h3> | |
<p>This Snowflake Generator was initially created with Grok 3, with refinements and enhancements by Claude 3 Sonnet.</p> | |
<p>The application uses fractal L-systems to generate unique, procedurally-created snowflakes with realistic physics and interactive controls.</p> | |
</section> | |
<button id="modal-close">Close</button> | |
</div> | |
</div> | |
<py-config> | |
packages = ["pygame-ce"] | |
</py-config> | |
<script type="py"> | |
import js | |
import asyncio | |
import math | |
import random | |
import base64 | |
from pyscript import document | |
# Define keyboard shortcuts | |
keys_pressed = set() | |
def key_down(event): | |
global keys_pressed | |
keys_pressed.add(event.code) | |
print(f"Key down: {event.code}") | |
def key_up(event): | |
global keys_pressed | |
if event.code in keys_pressed: | |
keys_pressed.remove(event.code) | |
# Handle single-press keys on key up to prevent repeats | |
if event.code == "Space": | |
toggle_pause(None) # Call the pause function | |
print(f"Key up: {event.code}") | |
# Add keyboard handlers | |
try: | |
document.onkeydown = key_down | |
document.onkeyup = key_up | |
print("Keyboard handlers attached") | |
except Exception as e: | |
print(f"Error setting up keyboard handlers: {e}") | |
# Process keyboard input during animation | |
def process_keyboard_input(): | |
global camera_x, camera_y, zoom | |
pan_speed = 5 | |
# Handle continuous-press keys | |
if "ArrowLeft" in keys_pressed or "KeyA" in keys_pressed: | |
camera_x += pan_speed | |
if "ArrowRight" in keys_pressed or "KeyD" in keys_pressed: | |
camera_x -= pan_speed | |
if "ArrowUp" in keys_pressed or "KeyW" in keys_pressed: | |
camera_y += pan_speed | |
if "ArrowDown" in keys_pressed or "KeyS" in keys_pressed: | |
camera_y -= pan_speed | |
if "Equal" in keys_pressed or "NumpadAdd" in keys_pressed: # + key | |
old_zoom = zoom | |
zoom = min(5.0, zoom * 1.02) | |
adjust_camera_for_zoom(old_zoom, zoom) | |
if "Minus" in keys_pressed or "NumpadSubtract" in keys_pressed: # - key | |
old_zoom = zoom | |
zoom = max(0.1, zoom / 1.02) | |
adjust_camera_for_zoom(old_zoom, zoom) | |
# Prepare the output area | |
container = document.getElementById("snowflakes-container") | |
info_div = document.getElementById("info") | |
# Get button elements | |
pause_btn = document.getElementById("pause-btn") | |
zoom_in_btn = document.getElementById("zoom-in-btn") | |
zoom_out_btn = document.getElementById("zoom-out-btn") | |
# Set a fixed width to the pause button to prevent layout shifts | |
pause_btn.style.minWidth = "80px" | |
# Define callback functions for buttons | |
def toggle_pause(event): | |
global paused | |
print("Toggle pause button clicked") | |
paused = not paused | |
# Set a fixed width to the button to prevent layout shifts when text changes | |
pause_btn.style.minWidth = "80px" | |
pause_btn.textContent = "Resume" if paused else "Pause" | |
# Remove focus from the button to prevent repeat spacebar interactions | |
pause_btn.blur() | |
def zoom_in(event): | |
global zoom | |
print("Zoom in button clicked") | |
old_zoom = zoom | |
zoom = min(5.0, zoom * 1.1) | |
# Adjust camera to zoom toward center | |
adjust_camera_for_zoom(old_zoom, zoom) | |
# Remove focus from the button to prevent spacebar interactions | |
zoom_in_btn.blur() | |
def zoom_out(event): | |
global zoom | |
print("Zoom out button clicked") | |
old_zoom = zoom | |
zoom = max(0.1, zoom / 1.1) | |
# Adjust camera to zoom toward center | |
adjust_camera_for_zoom(old_zoom, zoom) | |
# Remove focus from the button to prevent spacebar interactions | |
zoom_out_btn.blur() | |
# Attach event listeners to buttons using onclick property | |
try: | |
print("Setting up button event handlers") | |
pause_btn.onclick = toggle_pause | |
zoom_in_btn.onclick = zoom_in | |
zoom_out_btn.onclick = zoom_out | |
print("Button event handlers attached") | |
except Exception as e: | |
print(f"Error setting up button events: {e}") | |
# Setup mouse events for panning | |
dragging = False | |
last_mouse_pos = None | |
def mouse_down(event): | |
global dragging, last_mouse_pos | |
# Prevent default browser behavior of dragging | |
try: | |
event.preventDefault() | |
except: | |
pass | |
dragging = True | |
last_mouse_pos = (event.clientX, event.clientY) | |
print("Mouse down:", last_mouse_pos) | |
def mouse_up(event): | |
global dragging | |
print("Mouse up") | |
dragging = False | |
def mouse_move(event): | |
global last_mouse_pos, camera_x, camera_y | |
if dragging and last_mouse_pos: | |
dx = event.clientX - last_mouse_pos[0] | |
dy = event.clientY - last_mouse_pos[1] | |
print(f"Mouse drag: dx={dx}, dy={dy}") | |
camera_x += dx | |
camera_y += dy | |
last_mouse_pos = (event.clientX, event.clientY) | |
# Make sure we have the container element before adding listeners | |
try: | |
print("Setting up mouse event listeners on container:", container) | |
# Add event listeners using on* properties instead of addEventListener | |
container.onmousedown = mouse_down | |
document.onmouseup = mouse_up | |
document.onmousemove = mouse_move | |
print("Mouse event listeners attached") | |
except Exception as e: | |
print(f"Error setting up mouse events: {e}") | |
# Function to adjust camera for zoom changes | |
def adjust_camera_for_zoom(old_zoom, new_zoom): | |
global camera_x, camera_y | |
# Get the center of the screen | |
center_x = screen_width / 2 | |
center_y = screen_height / 2 | |
# Calculate how the center point's position changes with zoom | |
camera_x = camera_x * (new_zoom / old_zoom) | |
camera_y = camera_y * (new_zoom / old_zoom) | |
# Try to import pygame | |
try: | |
import pygame | |
# Initialize Pygame | |
pygame.init() | |
# Setup the display - making it larger | |
screen_width, screen_height = 1600, 900 | |
surface = pygame.Surface((screen_width, screen_height)) | |
# Colors | |
BLACK = (0, 0, 0) | |
WHITE = (255, 255, 255) | |
GRAY = (150, 150, 150) | |
# Constants | |
MIN_SCALE = 0.05 | |
MIN_DIMENSION = 2 | |
EDGE_TOP = 0 | |
EDGE_RIGHT = 1 | |
EDGE_BOTTOM = 2 | |
EDGE_LEFT = 3 | |
# Stochastic L-system generation | |
def generate_l_system(axiom, rules, iterations): | |
current = axiom | |
for _ in range(iterations): | |
next_str = "" | |
for char in current: | |
if char in rules: | |
next_str += random.choice(rules[char]) | |
else: | |
next_str += char | |
current = next_str | |
return current | |
# Drawing function with tapering | |
def draw_l_system(surface, string, pos, angle, length, depth=0, scale_factor=0.7): | |
x, y = pos | |
stack = [] | |
for char in string: | |
if char == "F": | |
current_length = length * (scale_factor ** depth) | |
new_x = x + current_length * math.cos(math.radians(angle)) | |
new_y = y + current_length * math.sin(math.radians(angle)) | |
pygame.draw.line(surface, WHITE, (x, y), (new_x, new_y)) | |
x, y = new_x, new_y | |
elif char == "+": | |
angle += 60 | |
elif char == "-": | |
angle -= 60 | |
elif char == "[": | |
stack.append((x, y, angle, depth)) | |
depth += 1 | |
elif char == "]": | |
x, y, angle, depth = stack.pop() | |
# Create a snowflake surface | |
def create_snowflake_surface(size, iterations): | |
surface = pygame.Surface((size, size), pygame.SRCALPHA) | |
# Rules for snowflake | |
rules = { | |
"F": ["F[+F]F[-F]F", "F[+F]F", "F[-F]F", "F[+F][-F]"] | |
} | |
# Generate l-system string | |
string = generate_l_system("F", rules, iterations) | |
# Draw 6 branches | |
for i in range(6): | |
draw_l_system(surface, string, (size/2, size/2), i * 60, size/10, depth=0, scale_factor=0.7) | |
return surface | |
class Snowflake: | |
def __init__(self, original_surface, x, y, dx, dy, rotation_speed, scale): | |
self.original_surface = original_surface | |
self.x = x | |
self.y = y | |
self.dx = dx | |
self.dy = dy | |
self.rotation = 0 | |
self.rotation_speed = rotation_speed | |
self.scale = scale | |
self.cached_surface = None | |
self.cached_rect = None | |
self._cache_key = None | |
def update(self, screen_width, screen_height, zoom, camera_x, camera_y): | |
# Update position | |
self.x += self.dx | |
self.y += self.dy | |
self.rotation += self.rotation_speed | |
# Calculate visible world area based on zoom level and camera position | |
visible_width = screen_width / zoom | |
visible_height = screen_height / zoom | |
center_x = screen_width / 2 | |
center_y = screen_height / 2 | |
# Visible area in world coordinates | |
world_left = (0 - center_x) / zoom - camera_x / zoom + center_x | |
world_top = (0 - center_y) / zoom - camera_y / zoom + center_y | |
world_right = world_left + visible_width | |
world_bottom = world_top + visible_height | |
# Buffer to avoid sudden appearances/disappearances | |
buffer = 100 / zoom # Scale buffer with zoom | |
# Reset if off-screen | |
if (self.y > world_bottom + buffer or self.y < world_top - buffer or | |
self.x > world_right + buffer or self.x < world_left - buffer): | |
# Only respawn with a probability inversely proportional to zoom | |
respawn_chance = min(1.0, 1.0 / zoom) | |
if random.random() > respawn_chance: | |
# Don't respawn this snowflake at higher zoom levels | |
self.x = world_left - buffer * 10 | |
self.y = world_top - buffer * 10 | |
return | |
# Choose a random edge to spawn from | |
edge = random.randint(0, 3) | |
if edge == EDGE_TOP: | |
self.y = world_top - buffer | |
self.x = random.uniform(world_left - buffer, world_right + buffer) | |
elif edge == EDGE_RIGHT: | |
self.x = world_right + buffer | |
self.y = random.uniform(world_top - buffer, world_bottom + buffer) | |
elif edge == EDGE_BOTTOM: | |
self.y = world_bottom + buffer | |
self.x = random.uniform(world_left - buffer, world_right + buffer) | |
else: # EDGE_LEFT | |
self.x = world_left - buffer | |
self.y = random.uniform(world_top - buffer, world_bottom + buffer) | |
def draw(self, surface, camera_x, camera_y, zoom, screen_width, screen_height): | |
# Calculate screen position with camera and zoom | |
center_x = screen_width / 2 | |
center_y = screen_height / 2 | |
# Position relative to center, scaled by zoom, with camera offset | |
screen_x = center_x + (self.x - center_x) * zoom + camera_x | |
screen_y = center_y + (self.y - center_y) * zoom + camera_y | |
# Skip if off-screen | |
margin = 100 | |
if (screen_x < -margin or screen_x > screen_width + margin or | |
screen_y < -margin or screen_y > screen_height + margin): | |
return | |
# Calculate scale (affected by both snowflake's own scale and zoom level) | |
total_scale = self.scale * zoom | |
# Skip if too small | |
if total_scale <= MIN_SCALE: | |
return | |
# Cache key combines scale and rotation to know when to regenerate | |
cache_key = (total_scale, self.rotation) | |
# Use cached surface if available and parameters haven't changed | |
if not self.cached_surface or self._cache_key != cache_key: | |
width = max(1, int(self.original_surface.get_width() * total_scale)) | |
height = max(1, int(self.original_surface.get_height() * total_scale)) | |
# Skip if dimensions are too small | |
if width < MIN_DIMENSION or height < MIN_DIMENSION: | |
return | |
try: | |
# Scale original surface | |
scaled_surface = pygame.transform.scale(self.original_surface, (width, height)) | |
# Rotate surface | |
self.cached_surface = pygame.transform.rotate(scaled_surface, self.rotation) | |
self._cache_key = cache_key | |
except (pygame.error, ValueError, OverflowError): | |
return | |
# Position the surface | |
rect = self.cached_surface.get_rect(center=(screen_x, screen_y)) | |
# Draw to screen | |
try: | |
surface.blit(self.cached_surface, rect) | |
except (pygame.error, ValueError, OverflowError): | |
self.cached_surface = None # Clear cache if error occurs | |
# Pre-generate snowflake designs | |
snowflake_surfaces = [create_snowflake_surface(50, 2) for _ in range(25)] | |
# Create snowflakes | |
snowflakes = [] | |
num_snowflakes = 150 # Reduced for better web performance | |
def create_snowflake(x_range=None, y_range=None): | |
surface = random.choice(snowflake_surfaces) | |
x = random.randint(-screen_width, screen_width * 2) if x_range is None else random.uniform(*x_range) | |
y = random.randint(-screen_height, screen_height * 2) if y_range is None else random.uniform(*y_range) | |
dx = random.uniform(-0.5, 0.5) # Wind effect | |
dy = random.uniform(1, 3) # Falling speed | |
rotation_speed = random.uniform(-1, 1) | |
scale = random.uniform(0.5, 1.5) # Size variation | |
return Snowflake(surface, x, y, dx, dy, rotation_speed, scale) | |
# Create initial snowflakes | |
for _ in range(num_snowflakes): | |
x = random.uniform(-screen_width, screen_width * 2) | |
y = random.uniform(-screen_height, screen_height * 2) | |
snowflakes.append(create_snowflake([x, x], [y, y])) | |
# Initialize variables | |
paused = False | |
zoom = 1.0 | |
camera_x, camera_y = 0, 0 | |
clock = pygame.time.Clock() | |
# Try to use default font instead of system font | |
try: | |
font = pygame.font.Font(None, 24) # Default font | |
print("Using default font") | |
except Exception as e: | |
print(f"Font error: {e}") | |
# Create a very basic font alternative | |
font = None | |
# Function to display the current frame | |
def update_display(): | |
try: | |
# Save surface to a BytesIO object as PNG | |
import io | |
buffer = io.BytesIO() | |
pygame.image.save(surface, buffer, "PNG") | |
buffer.seek(0) | |
# Convert buffer to base64 | |
encoded = base64.b64encode(buffer.read()).decode('utf-8') | |
# Create an <img> element with the data URL, set to 100% width to fill container | |
img_html = f'<img width="100%" src="data:image/png;base64,{encoded}" alt="Snowflakes" style="border:1px solid #333;" />' | |
container.innerHTML = img_html | |
# Update info text | |
info_div.textContent = f"Zoom: {zoom:.1f}x | FPS: {int(clock.get_fps())}" | |
except Exception as e: | |
print(f"Display update error: {e}") | |
container.innerHTML += f"<div style='color:red'>Error in update_display: {str(e)}</div>" | |
# Handle mouse wheel events for zooming | |
def on_wheel(event): | |
global zoom, camera_x, camera_y | |
try: | |
# Try to prevent default scroll behavior | |
try: | |
event.preventDefault() | |
except: | |
pass # If not supported, continue anyway | |
# Get wheel delta (can be different across browsers) | |
delta = 0 | |
if hasattr(event, 'wheelDelta'): | |
delta = event.wheelDelta # Older browsers | |
elif hasattr(event, 'deltaY'): | |
delta = -event.deltaY # Negative because wheel down should zoom out | |
if delta == 0: | |
return # No scroll detected | |
# Get mouse position | |
mouse_x, mouse_y = 0, 0 | |
try: | |
# Try to get mouse position relative to container | |
rect = container.getBoundingClientRect() | |
mouse_x = event.clientX - rect.left | |
mouse_y = event.clientY - rect.top | |
except: | |
# Fallback to center of container | |
mouse_x = screen_width / 2 | |
mouse_y = screen_height / 2 | |
# Store old zoom for camera adjustment | |
old_zoom = zoom | |
# Adjust zoom level | |
if delta > 0: | |
zoom = min(5.0, zoom * 1.1) # Zoom in | |
else: | |
zoom = max(0.1, zoom / 1.1) # Zoom out | |
# Calculate the world position under the mouse before zooming | |
world_x = (mouse_x - screen_width / 2) / old_zoom - camera_x / old_zoom | |
world_y = (mouse_y - screen_height / 2) / old_zoom - camera_y / old_zoom | |
# Calculate what the screen position would be after the zoom | |
new_screen_x = world_x * zoom + camera_x | |
new_screen_y = world_y * zoom + camera_y | |
# Adjust the camera to keep the world position under the mouse | |
camera_x -= new_screen_x - (mouse_x - screen_width / 2) | |
camera_y -= new_screen_y - (mouse_y - screen_height / 2) | |
print(f"Zoomed to {zoom:.2f}x") | |
except Exception as e: | |
print(f"Wheel event error: {e}") | |
# Handle modal functionality | |
def show_modal(event): | |
modal = document.getElementById("modal-overlay") | |
modal.style.display = "flex" | |
print("Modal opened") | |
# Remove focus from the button | |
info_button.blur() | |
def hide_modal(event): | |
modal = document.getElementById("modal-overlay") | |
modal.style.display = "none" | |
print("Modal closed") | |
# Add event listeners for modal buttons | |
try: | |
info_button = document.getElementById("info-button") | |
info_button.onclick = show_modal | |
close_button = document.getElementById("modal-close") | |
close_button.onclick = hide_modal | |
# Also close by clicking overlay | |
modal_overlay = document.getElementById("modal-overlay") | |
def overlay_click(event): | |
if event.target == modal_overlay: | |
hide_modal(event) | |
modal_overlay.onclick = overlay_click | |
print("Modal handlers attached") | |
except Exception as e: | |
print(f"Error setting up modal handlers: {e}") | |
# Try to add wheel event listener using different methods | |
try: | |
# First try modern event listener | |
document.onwheel = on_wheel | |
print("Mouse wheel handler attached (onwheel)") | |
except Exception as e: | |
print(f"Error setting up wheel handler: {e}") | |
try: | |
# Fallback to mousewheel event for older browsers | |
document.onmousewheel = on_wheel | |
print("Mouse wheel handler attached (onmousewheel)") | |
except Exception as e2: | |
print(f"Error setting up mousewheel handler: {e2}") | |
# Main animation loop | |
async def animation_loop(): | |
global paused, zoom, camera_x, camera_y | |
try: | |
print("Animation loop starting") | |
frame_count = 0 | |
while True: | |
try: | |
# Process keyboard input | |
process_keyboard_input() | |
# Clear the surface | |
surface.fill(BLACK) | |
# Update snowflakes | |
if not paused: | |
for snowflake in snowflakes: | |
snowflake.update(screen_width, screen_height, zoom, camera_x, camera_y) | |
# Draw snowflakes | |
for snowflake in snowflakes: | |
snowflake.draw(surface, camera_x, camera_y, zoom, screen_width, screen_height) | |
# Update display | |
update_display() | |
frame_count += 1 | |
if frame_count % 100 == 0: | |
print(f"Rendered {frame_count} frames") | |
# Cap at 30 FPS for better web performance | |
clock.tick(30) | |
# Yield to browser to keep it responsive | |
await asyncio.sleep(0.01) | |
except Exception as e: | |
print(f"Error in animation frame: {e}") | |
container.innerHTML = f"<div style='color:red; padding:10px;'>Error in animation frame: {str(e)}</div>" | |
await asyncio.sleep(1) # Wait a bit before continuing | |
except Exception as e: | |
print(f"Animation loop error: {e}") | |
container.innerHTML = f"<div style='color:red; padding:10px;'>Animation loop error: {str(e)}</div>" | |
# Start the animation | |
asyncio.ensure_future(animation_loop()) | |
except Exception as e: | |
# Display any errors | |
container.innerHTML = f"<div style='color:red; background:#300; padding:20px; border-radius:5px;'>Error: {str(e)}</div>" | |
print(f"Error: {str(e)}") | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment