Math Animations
Una colección de fractales y algoritmos visuales generados con Python, NumPy y Raylib. Cada video es el resultado de procesar miles de puntos matemáticos frame por frame.
Dragon Curve
* Animación generada exportando frames en formato PPM y ensamblados con FFMPEG.
Python Source Code
dragon_curve_ppm.pyimport mathimport osimport numpy as npimport pyray as rlWIDTH, HEIGHT = 800, 800FPS = 1DURATION = 10MAX_FRAMES = DURATIONOUTPUT_DIR = "dragon_frames"if not os.path.exists(OUTPUT_DIR):os.makedirs(OUTPUT_DIR)def get_dragon_points(iterations):points = [(0, 0), (1, 0)]for _ in range(iterations):last_x, last_y = points[-1]new_points = []for i in range(len(points) - 2, -1, -1):px, py = points[i]# Rotar 90 grados alrededor del último puntonx = last_x - (py - last_y)ny = last_y + (px - last_x)new_points.append((nx, ny))points.extend(new_points)return pointsdef main():rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN)rl.init_window(WIDTH, HEIGHT, b"Dragon")for i in range(MAX_FRAMES):depth = i + 2 # De 2 a 11 iteracionesrl.begin_drawing()rl.clear_background(rl.BLACK)raw_points = get_dragon_points(depth)# Centrar y escalarxs = [p[0] for p in raw_points]ys = [p[1] for p in raw_points]min_x, max_x = min(xs), max(xs)min_y, max_y = min(ys), max(ys)scale = min((WIDTH - 150) / (max_x - min_x + 1),(HEIGHT - 150) / (max_y - min_y + 1),)off_x = WIDTH / 2 - (min_x + max_x) * scale / 2off_y = HEIGHT / 2 - (min_y + max_y) * scale / 2for p in range(len(raw_points) - 1):p1 = rl.Vector2(off_x + raw_points[p][0] * scale,off_y + raw_points[p][1] * scale,)p2 = rl.Vector2(off_x + raw_points[p + 1][0] * scale,off_y + raw_points[p + 1][1] * scale,)rl.draw_line_ex(p1, p2, 2, rl.GOLD)rl.end_drawing()img = rl.load_image_from_screen()with open(os.path.join(OUTPUT_DIR, f"frame_{i:04d}.ppm"), "wb") as f:pixels = np.frombuffer(rl.ffi.buffer(img.data, WIDTH * HEIGHT * 4)[:],dtype=np.uint8,).reshape((HEIGHT, WIDTH, 4))f.write(f"""P6{WIDTH} {HEIGHT}255""".encode()+ pixels[:, :, :3].tobytes())rl.unload_image(img)rl.close_window()if __name__ == "__main__":main()
Hilbert Curve
* Animación generada exportando frames en formato PPM y ensamblados con FFMPEG.
Python Source Code
hilbert_curve_ppm.pyimport osimport numpy as npimport pyray as rlWIDTH, HEIGHT = 800, 800FPS = 1DURATION = 10MAX_FRAMES = DURATIONOUTPUT_DIR = "hilbert_frames"if not os.path.exists(OUTPUT_DIR):os.makedirs(OUTPUT_DIR)def rot(n, x, y, rx, ry):if ry == 0:if rx == 1:x, y = n - 1 - x, n - 1 - yx, y = y, xreturn x, ydef d2xy(n, d):t, x, y = d, 0, 0s = 1while s < n:rx = 1 & (t // 2)ry = 1 & (t ^ rx)x, y = rot(s, x, y, rx, ry)x += s * rxy += s * ryt //= 4s *= 2return x, ydef main():rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN)rl.init_window(WIDTH, HEIGHT, b"Hilbert")for i in range(MAX_FRAMES):order = i + 1 # De 1 a 10 ordenn = 2**ordertotal_points = n * nrl.begin_drawing()rl.clear_background(rl.BLACK)spacing = (WIDTH - 100) / npoints = []for d in range(total_points):x, y = d2xy(n, d)points.append(rl.Vector2(50 + x * spacing + spacing / 2,50 + y * spacing + spacing / 2,))for p in range(len(points) - 1):rl.draw_line_ex(points[p], points[p + 1], 2, rl.LIME)rl.end_drawing()img = rl.load_image_from_screen()with open(os.path.join(OUTPUT_DIR, f"frame_{i:04d}.ppm"), "wb") as f:pixels = np.frombuffer(rl.ffi.buffer(img.data, WIDTH * HEIGHT * 4)[:],dtype=np.uint8,).reshape((HEIGHT, WIDTH, 4))f.write(f"""P6{WIDTH} {HEIGHT}255""".encode()+ pixels[:, :, :3].tobytes())rl.unload_image(img)rl.close_window()if __name__ == "__main__":main()
Koch Snowflake
* Animación generada exportando frames en formato PPM y ensamblados con FFMPEG.
Python Source Code
koch_snowflake_ppm.pyimport mathimport osimport numpy as npimport pyray as rlWIDTH, HEIGHT = 800, 800FPS = 1DURATION = 10MAX_FRAMES = DURATIONOUTPUT_DIR = "koch_frames"if not os.path.exists(OUTPUT_DIR):os.makedirs(OUTPUT_DIR)def save_ppm(filename, width, height, data):with open(filename, "wb") as f:f.write(f"""P6{width} {height}255""".encode())pixels = np.frombuffer(data, dtype=np.uint8).reshape((height, width, 4))f.write(pixels[:, :, :3].tobytes())def draw_koch_line(p1, p2, depth):if depth == 0:rl.draw_line_v(p1, p2, rl.SKYBLUE)else:dx, dy = (p2.x - p1.x) / 3, (p2.y - p1.y) / 3a = rl.Vector2(p1.x + dx, p1.y + dy)c = rl.Vector2(p1.x + 2 * dx, p1.y + 2 * dy)# Pico del triángulo equiláteroangle = -math.pi / 3s, c_val = math.sin(angle), math.cos(angle)bx = (c.x - a.x) * c_val - (c.y - a.y) * s + a.xby = (c.x - a.x) * s + (c.y - a.y) * c_val + a.yb = rl.Vector2(bx, by)draw_koch_line(p1, a, depth - 1)draw_koch_line(a, b, depth - 1)draw_koch_line(b, c, depth - 1)draw_koch_line(c, p2, depth - 1)def main():rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN)rl.init_window(WIDTH, HEIGHT, b"Koch")for i in range(MAX_FRAMES):depth = i # De 0 a 9 iteracionesrl.begin_drawing()rl.clear_background(rl.BLACK)# Triángulo inicialsize = 500p1 = rl.Vector2(WIDTH / 2, HEIGHT / 2 - 280)p2 = rl.Vector2(WIDTH / 2 - 250, HEIGHT / 2 + 150)p3 = rl.Vector2(WIDTH / 2 + 250, HEIGHT / 2 + 150)draw_koch_line(p1, p3, depth)draw_koch_line(p3, p2, depth)draw_koch_line(p2, p1, depth)rl.end_drawing()img = rl.load_image_from_screen()save_ppm(os.path.join(OUTPUT_DIR, f"frame_{i:04d}.ppm"),WIDTH,HEIGHT,rl.ffi.buffer(img.data, WIDTH * HEIGHT * 4)[:],)rl.unload_image(img)rl.close_window()if __name__ == "__main__":main()
Mandelbrot Zoom
* Animación generada exportando frames en formato PPM y ensamblados con FFMPEG.
Python Source Code
mandelbrot_ppm.pyfrom concurrent.futures import ProcessPoolExecutorimport mathimport osimport shutilimport timeimport numpy as np# CONFIGURACIONWIDTH, HEIGHT = 800, 800FPS = 30DURATION = 30MAX_FRAMES = FPS * DURATIONVIDEO_NAME = "mandelbrot"OUTPUT_DIR = f"{VIDEO_NAME}_frames"MAX_ITER = 120if not os.path.exists(OUTPUT_DIR):os.makedirs(OUTPUT_DIR, exist_ok=True)def get_mandelbrot(width, height, x_min, x_max, y_min, y_max):x = np.linspace(x_min, x_max, width)y = np.linspace(y_min, y_max, height)X, Y = np.meshgrid(x, y)c = X + 1j * Yz = np.zeros(c.shape, dtype=complex)img = np.zeros(c.shape, dtype=float)mask = np.full(c.shape, True, dtype=bool)for i in range(MAX_ITER):z[mask] = z[mask] ** 2 + c[mask]mask[np.abs(z) > 2] = Falseimg[mask] = ireturn imgdef render_frame(i):target_x, target_y = -0.743643887, 0.131825904start_scale, end_scale = 1.5, 0.0001t = i / (MAX_FRAMES - 1)scale = start_scale * math.pow(end_scale / start_scale, t)data = get_mandelbrot(WIDTH,HEIGHT,target_x - scale,target_x + scale,target_y - scale,target_y + scale,)norm = data / MAX_ITERrgb = np.stack([(255 * norm**0.5).astype(np.uint8),(255 * norm).astype(np.uint8),(255 * norm**2).astype(np.uint8),],axis=-1,)filename = os.path.join(OUTPUT_DIR, f"frame_{i:04d}.ppm")with open(filename, "wb") as f:f.write(f"P6\n{WIDTH} {HEIGHT}\n255\n".encode() + rgb.tobytes())return idef main():print(f"Iniciando renderizado de Mandelbrot: {MAX_FRAMES} cuadros")start_time = time.time()# Generar Frameswith ProcessPoolExecutor() as executor:list(executor.map(render_frame, range(MAX_FRAMES)))# Generar videovideo_name = f"{VIDEO_NAME}.mp4"print(f"Generando video final: {video_name}")os.system(f"ffmpeg -y -framerate {FPS} "f"-i {output_subdir}/frame_%04d.ppm "f"-c:v libx264 -pix_fmt yuv420p {video_name} > /dev/null 2>&1")shutil.rmtree(OUTPUT_DIR)end_time = time.time()print(f"Renderizado completado en {end_time - start_time:.2f} s")if __name__ == "__main__":main()
Mandelbulb 3D
* Animación generada exportando frames en formato PPM y ensamblados con FFMPEG.
Python Source Code
mandelbulb_3d.pyimport pyray as rlimport numpy as npimport osimport math# CONFIGURACIONWIDTH, HEIGHT = 800, 800OUTPUT_DIR = "render_mandelbulb_3d"MAX_FRAMES = 900 # 30 segundosFPS = 30if not os.path.exists(OUTPUT_DIR):os.makedirs(OUTPUT_DIR)# SHADER DE RAYMARCHING CON gl_FragCoord (Mas robusto)VS_CODE = b"""#version 330in vec3 vertexPosition;uniform mat4 mvp;void main() {gl_Position = mvp * vec4(vertexPosition, 1.0);}"""FS_CODE = b"""#version 330precision highp float;uniform vec2 uRes;uniform float uTime;uniform vec3 uCamPos;uniform vec3 uCamTarget;out vec4 finalColor;float map(vec3 p) {vec3 z = p;float dr = 1.0;float r = 0.0;float power = 8.0 + sin(uTime * 0.4) * 2.0;for (int i = 0; i < 15; i++) {r = length(z);if (r > 2.0) break;float theta = acos(clamp(z.z / r, -1.0, 1.0));float phi = atan(z.y, z.x);dr = pow(r, power - 1.0) * power * dr + 1.0;float zr = pow(r, power);theta = theta * power;phi = phi * power;z = zr * vec3(sin(theta) * cos(phi), sin(phi) * sin(theta), cos(theta));z += p;}return 0.5 * log(r) * r / dr;}vec3 getNormal(vec3 p) {float h = 0.001;vec2 e = vec2(h, 0.0);return normalize(vec3(map(p + e.xyy) - map(p - e.xyy),map(p + e.yxy) - map(p - e.yxy),map(p + e.yyx) - map(p - e.yyx)));}void main() {// Calcular UVs centrados usando la resolucion realvec2 uv = (gl_FragCoord.xy / uRes.xy) - 0.5;uv.x *= (uRes.x / uRes.y);vec3 ro = uCamPos;vec3 target = uCamTarget;// Matriz de vista de camaravec3 cw = normalize(target - ro);vec3 cp = vec3(0.0, 1.0, 0.0);vec3 cu = normalize(cross(cw, cp));vec3 cv = normalize(cross(cu, cw));// Direccion del rayo (con FOV de 1.5)vec3 rd = normalize(uv.x * cu + uv.y * cv + 1.5 * cw);float tmax = 15.0;float t = 0.0;float d = 0.0;bool hit = false;for (int i = 0; i < 150; i++) {d = map(ro + rd * t);if (d < 0.0001) { hit = true; break; }t += d;if (t > tmax) break;}vec3 color = vec3(0.02, 0.02, 0.05); // Fondo oscuroif (hit) {vec3 p = ro + rd * t;vec3 n = getNormal(p);vec3 ld = normalize(vec3(1.0, 1.0, 1.0)); // Luzfloat diff = max(dot(n, ld), 0.0);float amb = 0.15;// Color psicodelico basado en normalesvec3 baseCol = 0.5 + 0.5 * cos(uTime * 0.2 + n + vec3(0, 2, 4));color = baseCol * (diff + amb);// Niebla para dar profundidadcolor = mix(color, vec3(0.02, 0.02, 0.05), 1.0 - exp(-0.15 * t));}finalColor = vec4(color, 1.0);}"""def save_ppm(filename, width, height, data):with open(filename, "wb") as f:f.write(f"P6\n{width} {height}\n255\n".encode())pixels = np.frombuffer(data, dtype=np.uint8).reshape((height, width, 4))# Raylib load_image_from_screen devuelve la imagen invertida en Y# La flipeamos para que el video salga bienpixels = np.flipud(pixels)f.write(pixels[:, :, :3].tobytes())def main():rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN)rl.init_window(WIDTH, HEIGHT, b"Mandelbulb 3D Ultra Fix")shader = rl.load_shader_from_memory(VS_CODE, FS_CODE)u_res = rl.get_shader_location(shader, b"uRes")u_time = rl.get_shader_location(shader, b"uTime")u_campos = rl.get_shader_location(shader, b"uCamPos")u_camtarget = rl.get_shader_location(shader, b"uCamTarget")res_val = rl.ffi.new("float[2]", [float(WIDTH), float(HEIGHT)])rl.set_shader_value(shader, u_res, res_val, rl.SHADER_UNIFORM_VEC2)print(f"Renderizando Mandelbulb 3D (Correccion UV y Camara)...")for i in range(MAX_FRAMES):time = i / FPS# Orbita estable a radio 4.5cam_x = 4.5 * math.sin(time * 0.2)cam_z = 4.5 * math.cos(time * 0.2)cam_y = 1.5 * math.cos(time * 0.1)u_time_val = rl.ffi.new("float*", time)u_campos_val = rl.ffi.new("float[3]", [cam_x, cam_y, cam_z])u_camtarget_val = rl.ffi.new("float[3]", [0.0, 0.0, 0.0])rl.set_shader_value(shader, u_time, u_time_val, rl.SHADER_UNIFORM_FLOAT)rl.set_shader_value(shader, u_campos, u_campos_val, rl.SHADER_UNIFORM_VEC3)rl.set_shader_value(shader,u_camtarget,u_camtarget_val,rl.SHADER_UNIFORM_VEC3,)rl.begin_drawing()rl.clear_background(rl.BLACK)rl.begin_shader_mode(shader)rl.draw_rectangle(0, 0, WIDTH, HEIGHT, rl.WHITE)rl.end_shader_mode()rl.end_drawing()img = rl.load_image_from_screen()save_ppm(os.path.join(OUTPUT_DIR, f"frame_{i:04d}.ppm"),WIDTH,HEIGHT,rl.ffi.buffer(img.data, WIDTH * HEIGHT * 4)[:],)rl.unload_image(img)if i % 30 == 0:print(f"Progreso: {i}/{MAX_FRAMES}")rl.unload_shader(shader)rl.close_window()print("Renderizado finalizado con exito.")if __name__ == "__main__":main()
Burning Ship Fractal
* Animación generada exportando frames en formato PPM y ensamblados con FFMPEG.
Python Source Code
burning_ship_ppm.pyimport mathimport osimport numpy as np# CONFIGURATIONWIDTH, HEIGHT = 800, 800FPS = 24DURATION = 10MAX_FRAMES = FPS * DURATIONOUTPUT_DIR = "burning_ship_frames"MAX_ITER = 100if not os.path.exists(OUTPUT_DIR):os.makedirs(OUTPUT_DIR, exist_ok=True)def save_ppm(filename, width, height, rgb_data):"""Saves RGB data to a PPM P6 file."""with open(filename, "wb") as f:header = f"""P6{width} {height}255"""f.write(header.encode())f.write(rgb_data.tobytes())def get_burning_ship(width, height, x_min, x_max, y_min, y_max, max_iter):"""Calculates the Burning Ship fractal using numpy."""x = np.linspace(x_min, x_max, width)y = np.linspace(y_min, y_max, height)X, Y = np.meshgrid(x, y)c = X + 1j * Yz = np.zeros(c.shape, dtype=complex)img = np.zeros(c.shape, dtype=int)mask = np.full(c.shape, True, dtype=bool)for i in range(max_iter):# Burning Ship formula: z = (|Re(z)| + i|Im(z)|)^2 + c# Which expands to:# Re(z_next) = Re(z)^2 - Im(z)^2 + Re(c)# Im(z_next) = 2 * |Re(z) * Im(z)| + Im(c)# (Note: taking absolute values before squaring/multiplying)z_re = np.abs(z.real)z_im = np.abs(z.imag)z[mask] = (z_re[mask] + 1j * z_im[mask]) ** 2 + c[mask]mask[np.abs(z) > 2] = Falseimg[mask] = ireturn imgdef colormap(iterations, max_iter):"""Simple colormap for the fractal."""# Normalized iterations 0..1norm = iterations / max_iter# Create some colors (R, G, B)# Burning ship looks good with fire colors: Red, Orange, Yellowr = (255 * norm).astype(np.uint8)g = (255 * (norm**2)).astype(np.uint8)b = (255 * (norm**4)).astype(np.uint8)# Stack them to get RGBreturn np.stack([r, g, b], axis=-1)def main():# Zoom parameters# The "Ship" is around x=-1.75, y=-0.03target_x = -1.755target_y = -0.04start_scale = 2.0end_scale = 0.005print(f"Generating {MAX_FRAMES} frames of Burning Ship Fractal...")for frame_idx in range(MAX_FRAMES):# Exponential zoomt = frame_idx / (MAX_FRAMES - 1)# scale = start_scale * (end_scale / start_scale) ** t# Smoother zoom using ease-in-out or just simple powerscale = start_scale * math.pow(end_scale / start_scale, t)x_min = target_x - scalex_max = target_x + scaley_min = target_y - scaley_max = target_y + scale# Calculate fractalfractal_data = get_burning_ship(WIDTH, HEIGHT, x_min, x_max, y_min, y_max, MAX_ITER)# Apply colormaprgb_image = colormap(fractal_data, MAX_ITER)# Save PPMfilename = os.path.join(OUTPUT_DIR, f"frame_{frame_idx:04d}.ppm")save_ppm(filename, WIDTH, HEIGHT, rgb_image)if frame_idx % 24 == 0:print(f"Progress: {frame_idx}/{MAX_FRAMES} frames "f"({(frame_idx / MAX_FRAMES) * 100:.1f}%)")print(f"Done! Frames saved in {OUTPUT_DIR}")print("To create the video, run:")print(f"ffmpeg -framerate {FPS} -i "f"{OUTPUT_DIR}/frame_%04d.ppm -c:v libx264 ""-pix_fmt yuv420p burning_ship.mp4")if __name__ == "__main__":main()
Flower Bloom (Julia)
* Animación generada exportando frames en formato PPM y ensamblados con FFMPEG.
Python Source Code
flower_bloom_ppm.pyimport mathimport osimport numpy as npimport timefrom concurrent.futures import ProcessPoolExecutor# CONFIGURACIÓNWIDTH, HEIGHT = 800, 800FPS = 30DURATION = 30MAX_FRAMES = FPS * DURATIONOUTPUT_DIR = "flower_bloom_frames"MAX_ITER = 80if not os.path.exists(OUTPUT_DIR):os.makedirs(OUTPUT_DIR, exist_ok=True)def save_ppm(filename, width, height, rgb_data):with open(filename, "wb") as f:header = f"P6\n{width} {height}\n255\n"f.write(header.encode())f.write(rgb_data.tobytes())def get_julia_bloom(width, height, c, zoom=1.2):x = np.linspace(-zoom, zoom, width)y = np.linspace(-zoom, zoom, height)X, Y = np.meshgrid(x, y)z = X + 1j * Yimg = np.zeros(z.shape, dtype=float)mask = np.full(z.shape, True, dtype=bool)for i in range(MAX_ITER):z[mask] = z[mask] ** 2 + cmask[np.abs(z) > 2] = Falseimg[mask] = ireturn imgdef colormap_flower(iterations):norm = iterations / MAX_ITERr = (255 * np.sin(norm * math.pi)).astype(np.uint8)g = (100 * (norm**2)).astype(np.uint8)b = (200 * norm).astype(np.uint8)return np.stack([r, g, b], axis=-1)def render_frame(frame_idx):# CAMBIO CLAVE: Usamos el tiempo real basado en FPS original para que la velocidad sea constante# t = frame_idx / 300 # Si antes 300 cuadros eran 1 ciclo, mantenemos esa escala# O mejor, t representa segundos reales:t_seconds = frame_idx / FPS# Mantenemos la velocidad de rotación original (1 ciclo cada 10 segundos)angle = 2 * math.pi * (t_seconds / 10.0)radius = 0.7885c = radius * np.complex128(complex(math.cos(angle), math.sin(angle)))# El latido del zoom también basado en segundos realeszoom = 1.3 + 0.2 * math.sin(2 * math.pi * (t_seconds / 5.0))fractal_data = get_julia_bloom(WIDTH, HEIGHT, c, zoom)rgb_image = colormap_flower(fractal_data)filename = os.path.join(OUTPUT_DIR, f"frame_{frame_idx:04d}.ppm")save_ppm(filename, WIDTH, HEIGHT, rgb_image)return frame_idxdef main():print(f"Iniciando renderizado paralelo de {MAX_FRAMES} cuadros con tiempo corregido...")start_time = time.time()with ProcessPoolExecutor() as executor:list(executor.map(render_frame, range(MAX_FRAMES)))end_time = time.time()total_time = end_time - start_timeprint(f"\n\u00a1Renderizado completado en {total_time:.2f} segundos!")if __name__ == "__main__":main()
Pythagoras Tree
* Animación generada exportando frames en formato PPM y ensamblados con FFMPEG.
Python Source Code
pythagoras_tree_ppm.pyimport mathimport osimport numpy as npimport pyray as rl# CONFIGURATIONWIDTH, HEIGHT = 800, 800FPS = 24DURATION = 10MAX_FRAMES = FPS * DURATIONOUTPUT_DIR = "pythagoras_tree_frames"if not os.path.exists(OUTPUT_DIR):os.makedirs(OUTPUT_DIR, exist_ok=True)def save_ppm(filename, width, height, image_data):"""Saves Raylib image data to PPM P6."""with open(filename, "wb") as f:header = f"""P6{width} {height}255"""f.write(header.encode())# image_data is RGBA, we need RGBpixels = np.frombuffer(image_data, dtype=np.uint8).reshape((height, width, 4))f.write(pixels[:, :, :3].tobytes())def draw_pythagoras_tree(x, y, size, angle, depth, tilt_angle):if depth == 0:return# Calculate vertices of the square growing UP# base vector along the bottom of the squaredx = size * math.cos(angle)dy = size * math.sin(angle)# perpendicular vector going "up" (subtracting from Y in Raylib)ux = size * math.sin(angle)uy = -size * math.cos(angle)p1 = rl.Vector2(x, y)p2 = rl.Vector2(x + dx, y + dy)p3 = rl.Vector2(p2.x + ux, p2.y + uy)p4 = rl.Vector2(p1.x + ux, p1.y + uy)# Draw the squarecolor = rl.color_from_hsv(depth * 30, 0.6, 0.9)thickness = max(1.0, depth * 0.5)rl.draw_line_ex(p1, p2, thickness, color)rl.draw_line_ex(p2, p3, thickness, color)rl.draw_line_ex(p3, p4, thickness, color)rl.draw_line_ex(p4, p1, thickness, color)# Calculate the top triangle vertex p5alpha = tilt_anglesize_left = size * math.cos(alpha)size_right = size * math.sin(alpha)# The left branch starts at p4 at angle (angle - alpha)# The right branch starts at p5 at angle (angle + pi/2 - alpha)p5 = rl.Vector2(p4.x + size_left * math.cos(angle - alpha),p4.y + size_left * math.sin(angle - alpha),)# Recursive callsdraw_pythagoras_tree(p4.x, p4.y, size_left, angle - alpha, depth - 1, tilt_angle)draw_pythagoras_tree(p5.x,p5.y,size_right,angle + (math.pi / 2 - alpha),depth - 1,tilt_angle,)def main():rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN)rl.init_window(WIDTH, HEIGHT, b"Pythagoras Tree Animation")print(f"Generating {MAX_FRAMES} frames of Pythagoras Tree...")for frame_idx in range(MAX_FRAMES):# Animate the tilt angle (swaying effect)# t goes from 0 to 1t = frame_idx / MAX_FRAMES# Cycle angle between 30 and 60 degrees (in radians)current_tilt = (math.pi / 4) + (math.pi / 12) * math.sin(2 * math.pi * t)rl.begin_drawing()rl.clear_background(rl.BLACK)# Draw the tree from the bottom center# Base size 120, starting at bottomdraw_pythagoras_tree(WIDTH // 2 - 60, HEIGHT - 50, 120, 0, 10, current_tilt)rl.draw_text(f"FRAME: {frame_idx}/{MAX_FRAMES}".encode(),20,20,20,rl.RAYWHITE,)rl.end_drawing()# Capture and saveimg = rl.load_image_from_screen()pixel_data = rl.ffi.buffer(img.data, WIDTH * HEIGHT * 4)[:]filename = os.path.join(OUTPUT_DIR, f"frame_{frame_idx:04d}.ppm")save_ppm(filename, WIDTH, HEIGHT, pixel_data)rl.unload_image(img)if frame_idx % 24 == 0:print(f"Progress: {frame_idx}/{MAX_FRAMES} frames "f"({(frame_idx / MAX_FRAMES) * 100:.1f}%)")rl.close_window()print(f"Done! Frames saved in {OUTPUT_DIR}")print("To create the video, run:")print(f"ffmpeg -framerate {FPS} -i {OUTPUT_DIR}/frame_%04d.ppm ""-c:v libx264 -pix_fmt yuv420p pythagoras_tree.mp4")if __name__ == "__main__":main()
Sierpinski Gasket
* Animación generada exportando frames en formato PPM y ensamblados con FFMPEG.
Python Source Code
sierpinski_ppm.pyimport osimport numpy as npimport pyray as rlWIDTH, HEIGHT = 800, 800FPS = 24DURATION = 10MAX_FRAMES = FPS * DURATIONOUTPUT_DIR = "sierpinski_frames"if not os.path.exists(OUTPUT_DIR):os.makedirs(OUTPUT_DIR)def draw_sierpinski(p1, p2, p3, depth):if depth == 0:rl.draw_triangle(p1, p2, p3, rl.ORANGE)else:p12 = rl.Vector2((p1.x + p2.x) / 2, (p1.y + p2.y) / 2)p23 = rl.Vector2((p2.x + p3.x) / 2, (p2.y + p3.y) / 2)p31 = rl.Vector2((p3.x + p1.x) / 2, (p3.y + p1.y) / 2)draw_sierpinski(p1, p12, p31, depth - 1)draw_sierpinski(p12, p2, p23, depth - 1)draw_sierpinski(p31, p23, p3, depth - 1)def main():rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN)rl.init_window(WIDTH, HEIGHT, b"Sierpinski")for i in range(MAX_FRAMES):t = i / MAX_FRAMESdepth = int(1 + 7 * t)rl.begin_drawing()rl.clear_background(rl.BLACK)p1 = rl.Vector2(WIDTH / 2, 50)p2 = rl.Vector2(50, HEIGHT - 50)p3 = rl.Vector2(WIDTH - 50, HEIGHT - 50)draw_sierpinski(p1, p2, p3, depth)rl.end_drawing()img = rl.load_image_from_screen()with open(os.path.join(OUTPUT_DIR, f"frame_{i:04d}.ppm"), "wb") as f:pixels = np.frombuffer(rl.ffi.buffer(img.data, WIDTH * HEIGHT * 4)[:],dtype=np.uint8,).reshape((HEIGHT, WIDTH, 4))f.write(f"""P6{WIDTH} {HEIGHT}255""".encode()+ pixels[:, :, :3].tobytes())rl.unload_image(img)rl.close_window()if __name__ == "__main__":main()