Implementing an Animated Beating Heart Graphic with Python
To create an animtaed, pulsating heart graphic, we will use Python's tkinter for the graphical interface and mathematical functions to generate and animate the shape.
Required Imports
import random
from math import sin, cos, pi, log
from tkinter import *
Canvas Configuration
Define the dimensions of the drawing window.
WINDOW_WIDTH = 640
WINDOW_HEIGHT = 480
CENTER_X = WINDOW_WIDTH / 2
CENTER_Y = WINDOW_HEIGHT / 2
SCALE_FACTOR = 11
COLOR_HEART = "#e3242b"
Core Heart Coordinate Function
This parametric function defines the classic heart shape.
def generate_heart_coords(parameter, scale: float = SCALE_FACTOR):
"""
Calculates x, y coordinates for a heart shape based on a parameter t.
:param scale: Scaling factor.
:param parameter: The parameter t.
:return: Integer x, y coordinates.
"""
# Parametric equations for a heart
x_coord = 16 * (sin(parameter) ** 3)
y_coord = -(13 * cos(parameter) - 5 * cos(2 * parameter) - 2 * cos(3 * parameter) - cos(4 * parameter))
# Apply scaling and center on canvas
x_coord *= scale
y_coord *= scale
x_coord += CENTER_X
y_coord += CENTER_Y
return int(x_coord), int(y_coord)
Coordinate Transformation Functions
Add randomness and movement effects to the points.
def diffuse_point(x, y, intensity=0.15):
"""
Creates a random inward diffusion effect from a given point.
:param x: Original x-coordinate.
:param y: Original y-coordinate.
:param intensity: Diffusion strength.
:return: New x, y coordinates.
"""
factor_x = - intensity * log(random.random())
factor_y = - intensity * log(random.random())
offset_x = factor_x * (x - CENTER_X)
offset_y = factor_y * (y - CENTER_Y)
return x - offset_x, y - offset_y
def apply_pulsation(x, y, pulsation_ratio):
"""
Applies a pulsating motion effect to a point.
:param x: Original x-coordinate.
:param y: Original y-coordinate.
:param pulsation_ratio: Current pulsation strength.
:return: New x, y coordinates.
"""
distance = ((x - CENTER_X) ** 2 + (y - CENTER_Y) ** 2) ** 0.6
force = -1 / distance if distance != 0 else 0
dx = pulsation_ratio * force * (x - CENTER_X)
dy = pulsation_ratio * force * (y - CENTER_Y)
return x - dx, y - dy
Animation Timing Function
Defines the beating rhythm.
def pulsation_curve(p):
"""
A custom periodic function to control the heart's beat rhythm.
:param p: Input parameter.
:return: Scaled sine value.
"""
return 2 * (2 * sin(4 * p)) / (2 * pi)
Heart Animation Class
This class manages the state and renedring of the animated heart.
class BeatingHeart:
def __init__(self, total_frames=20):
self.contour = set()
self.edge_points = set()
self.inner_points = set()
self.frame_data = {}
self.initialize_points(2000)
self.total_frames = total_frames
for frame_index in range(total_frames):
self.compute_frame(frame_index)
def initialize_points(self, num_points):
# Generate base heart contour
for _ in range(num_points):
t = random.uniform(0, 2 * pi)
x, y = generate_heart_coords(t)
self.contour.add((x, y))
# Create diffuse edge points
for point_x, point_y in list(self.contour):
for _ in range(3):
x, y = diffuse_point(point_x, point_y, 0.05)
self.edge_points.add((x, y))
# Create inner fill points
contour_list = list(self.contour)
for _ in range(4000):
x, y = random.choice(contour_list)
x, y = diffuse_point(x, y, 0.17)
self.inner_points.add((x, y))
@staticmethod
def adjust_position(x, y, adjustment_ratio):
dist_power = ((x - CENTER_X) ** 2 + (y - CENTER_Y) ** 2) ** 0.520
force = 1 / dist_power if dist_power != 0 else 0
dx = adjustment_ratio * force * (x - CENTER_X) + random.randint(-1, 1)
dy = adjustment_ratio * force * (y - CENTER_Y) + random.randint(-1, 1)
return x - dx, y - dy
def compute_frame(self, frame_idx):
current_ratio = 10 * pulsation_curve(frame_idx / 10 * pi)
glow_radius = int(4 + 6 * (1 + pulsation_curve(frame_idx / 10 * pi)))
glow_count = int(3000 + 4000 * abs(pulsation_curve(frame_idx / 10 * pi) ** 2))
points_for_frame = []
# Generate glowing halo effect
glow_set = set()
for _ in range(glow_count):
t = random.uniform(0, 2 * pi)
x, y = generate_heart_coords(t, shrink_ratio=11.6)
x, y = apply_pulsation(x, y, glow_radius)
if (x, y) not in glow_set:
glow_set.add((x, y))
x += random.randint(-14, 14)
y += random.randint(-14, 14)
point_size = random.choice((1, 2, 2))
points_for_frame.append((x, y, point_size))
# Process contour points
for x, y in self.contour:
x, y = self.adjust_position(x, y, current_ratio)
size = random.randint(1, 3)
points_for_frame.append((x, y, size))
# Process inner points
for x, y in self.edge_points:
x, y = self.adjust_position(x, y, current_ratio)
size = random.randint(1, 2)
points_for_frame.append((x, y, size))
for x, y in self.inner_points:
x, y = self.adjust_position(x, y, current_ratio)
size = random.randint(1, 2)
points_for_frame.append((x, y, size))
self.frame_data[frame_idx] = points_for_frame
def draw(self, canvas_target, current_frame):
frame_key = current_frame % self.total_frames
for x, y, sz in self.frame_data[frame_key]:
canvas_target.create_rectangle(x, y, x + sz, y + sz, width=0, fill=COLOR_HEART)
Main Rendering Loop
Sets up the Tkinter window and starts the animation.
def render_loop(window: Tk, canvas: Canvas, heart_obj: BeatingHeart, frame_num=0):
canvas.delete('all')
heart_obj.draw(canvas, frame_num)
window.after(160, render_loop, window, canvas, heart_obj, frame_num + 1)
if __name__ == '__main__':
main_window = Tk()
main_canvas = Canvas(main_window, bg='black', height=WINDOW_HEIGHT, width=WINDOW_WIDTH)
main_canvas.pack()
heart_animation = BeatingHeart()
render_loop(main_window, main_canvas, heart_animation)
main_window.mainloop()