How to Create a 3D Domino Piece in Python Tkinter
In this Python Tutorial we will see How to Create a 3D Domino with 3D rotations, floating animations, and mouse interactions Using Python Tkinter.
What We Are Gonna Use In This Project:
- Python Programming Language.- Tkinter (GUI).
- VS Editor.
- VS Editor.
Project Source Code:
import tkinter as tk
import math
from tkinter import ttk
class Domino(tk.Tk):
def __init__(self):
super().__init__()
# Window setup with centering
self.title("3D Domino")
self.geometry("800x700")
self.configure(bg="#121212")
# Center the window on the screen
self.center_window()
# Domino visual properties
self.domino_width = 120
self.domino_height = 240
self.domino_depth = 25
self.domino_color = "#fafafa"
self.dot_color = "#141414"
self.divider_color = "#282828"
# Rotation angles - current and target for smooth animation
self.current_rotation_x = -30
self.current_rotation_y = 45
self.current_rotation_z = 0
self.target_rotation_x = -30
self.target_rotation_y = 45
self.target_rotation_z = 0
# Animation properties
self.rotation_speed = 0.15 # Speed of rotation interpolation
self.float_offset = 0 # Vertical floating offset
self.floating_up = True # Direction of floating animation
# Mouse interaction properties
self.is_dragging = False
self.previous_x = 0
self.previous_y = 0
self.drag_sensitivity = 0.3 # Reduced sensitivity for smoother control
# Domino face values (1-6)
self.top_value = 6
self.bottom_value = 3
# Canvas dimensions - will be updated during rendering
self.canvas_width = 800
self.canvas_height = 600
# Initialize UI components
self.setup_ui()
# Initialize 3D geometry
self.init_face_vertices()
self.init_dot_positions()
# Start the animation loop
self.animate()
def center_window(self):
"""Center the window on the screen"""
# Update window to ensure geometry is calculated
self.update_idletasks()
# Get screen dimensions
screen_width = self.winfo_screenwidth()
screen_height = self.winfo_screenheight()
# Get window dimensions
window_width = 800
window_height = 700
# Calculate center position
center_x = int(screen_width/2 - window_width/2)
center_y = int(screen_height/2 - window_height/2)
# Set window position
self.geometry(f"{window_width}x{window_height}+{center_x}+{center_y}")
def setup_ui(self):
"""Initialize the user interface components"""
# Configure style for buttons
self.style = ttk.Style()
self.style.theme_use('clam')
self.style.configure("TButton",
background="#3d5afe",
foreground="white",
padding=8,
font=("Segoe UI", 10),
borderwidth=0)
self.style.map("TButton",
background=[('active', '#536dfe')])
# Create main container frame
main_frame = tk.Frame(self, bg="#121212")
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Create 3D rendering canvas
self.canvas = tk.Canvas(
main_frame,
bg="#121212",
highlightthickness=0,
width=800,
height=600
)
self.canvas.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# Bind mouse events for 3D rotation interaction
self.canvas.bind("<ButtonPress-1>", self.on_mouse_down)
self.canvas.bind("<ButtonRelease-1>", self.on_mouse_up)
self.canvas.bind("<B1-Motion>", self.on_mouse_drag)
self.canvas.bind("<Enter>", lambda e: self.canvas.config(cursor="hand2"))
self.canvas.bind("<Leave>", lambda e: self.canvas.config(cursor=""))
# Bind canvas resize event to update dimensions
self.canvas.bind("<Configure>", self.on_canvas_resize)
# Create control buttons frame
controls_frame = tk.Frame(main_frame, bg="#1e1e1e", height=80)
controls_frame.pack(fill=tk.X)
# Define control buttons with their actions
buttons = [
("Rotate X", self.rotate_x),
("Rotate Y", self.rotate_y),
("Reset View", self.reset_view),
("Change Top", self.change_top),
("Change Bottom", self.change_bottom)
]
# Create and pack buttons
for text, command in buttons:
btn = ttk.Button(controls_frame, text=text, command=command, width=18)
btn.pack(side=tk.LEFT, padx=8, pady=10)
def on_canvas_resize(self, event):
"""Handle canvas resize events to maintain proper rendering"""
self.canvas_width = event.width
self.canvas_height = event.height
def init_face_vertices(self):
"""Initialize 3D coordinates for all six faces of the domino cube"""
self.face_vertices = []
# Face index constants for clarity
self.FRONT = 0 # Front face (shows domino values)
self.BACK = 1 # Back face (shows domino values reversed)
self.RIGHT = 2 # Right side face
self.LEFT = 3 # Left side face
self.TOP = 4 # Top face
self.BOTTOM = 5 # Bottom face
# Half dimensions for centered cube
w = self.domino_width / 2.0 # Half width
h = self.domino_height / 2.0 # Half height
d = self.domino_depth / 2.0 # Half depth
# Define vertices for each face
# FRONT face (facing viewer)
self.face_vertices.append([[-w, -h, d], [w, -h, d], [w, h, d], [-w, h, d]])
# BACK face (away from viewer)
self.face_vertices.append([[w, -h, -d], [-w, -h, -d], [-w, h, -d], [w, h, -d]])
# RIGHT face
self.face_vertices.append([[w, -h, d], [w, -h, -d], [w, h, -d], [w, h, d]])
# LEFT face
self.face_vertices.append([[-w, -h, -d], [-w, -h, d], [-w, h, d], [-w, h, -d]])
# TOP face
self.face_vertices.append([[-w, -h, -d], [w, -h, -d], [w, -h, d], [-w, -h, d]])
# BOTTOM face
self.face_vertices.append([[-w, h, d], [w, h, d], [w, h, -d], [-w, h, -d]])
def init_dot_positions(self):
"""Initialize dot patterns for domino values 1-6"""
# Relative positions (0-1 range) for dots on each domino face half
self.dot_positions = [
[], # 0 (not used)
[(0.5, 0.5)], # 1: center dot
[(0.25, 0.25), (0.75, 0.75)], # 2: diagonal dots
[(0.25, 0.25), (0.5, 0.5), (0.75, 0.75)], # 3: diagonal + center
[(0.25, 0.25),(0.25, 0.75),(0.75, 0.25),(0.75, 0.75)], # 4: four corners
# 5: four corners + center
[(0.25, 0.25), (0.25, 0.75), (0.5, 0.5), (0.75, 0.25), (0.75, 0.75)],
# 6: two columns
[(0.25, 0.25),(0.25, 0.5),(0.25, 0.75),(0.75, 0.25),(0.75, 0.5),(0.75, 0.75)]
]
# Mouse interaction event handlers
def on_mouse_down(self, event):
"""Handle mouse button press - start dragging"""
self.is_dragging = True
self.previous_x = event.x
self.previous_y = event.y
def on_mouse_up(self, event):
"""Handle mouse button release - stop dragging"""
self.is_dragging = False
def on_mouse_drag(self, event):
"""Handle mouse drag movement - rotate domino smoothly"""
if self.is_dragging:
# Calculate mouse movement delta
delta_x = event.x - self.previous_x
delta_y = event.y - self.previous_y
# Apply rotation based on mouse movement with reduced sensitivity
# Horizontal mouse movement rotates around Y axis (left-right rotation)
self.target_rotation_y += delta_x * self.drag_sensitivity
# Vertical mouse movement rotates around X axis (up-down rotation)
self.target_rotation_x += delta_y * self.drag_sensitivity
# Clamp rotation values to prevent excessive spinning
self.target_rotation_x = max(-90, min(90, self.target_rotation_x))
# Update previous mouse position
self.previous_x = event.x
self.previous_y = event.y
# Button action handlers
def rotate_x(self):
"""Rotate domino 90 degrees around X axis"""
self.target_rotation_x += 90
def rotate_y(self):
"""Rotate domino 90 degrees around Y axis"""
self.target_rotation_y += 90
def reset_view(self):
"""Reset domino to default viewing angle"""
self.target_rotation_x = -30
self.target_rotation_y = 45
self.target_rotation_z = 0
def change_top(self):
"""Cycle through top half values (1-6)"""
self.top_value = (self.top_value % 6) + 1
def change_bottom(self):
"""Cycle through bottom half values (1-6)"""
self.bottom_value = (self.bottom_value % 6) + 1
def animate(self):
"""Main animation loop - handles floating and rotation interpolation"""
# Handle floating animation (gentle up-down movement)
if self.floating_up:
self.float_offset += 0.15
if self.float_offset >= 8:
self.floating_up = False
else:
self.float_offset -= 0.15
if self.float_offset <= -8:
self.floating_up = True
# Smooth rotation interpolation - gradually move current rotation towards target
# This creates smooth animations instead of instant jumps
rotation_threshold = 0.1 # Minimum difference before stopping interpolation
if abs(self.target_rotation_x - self.current_rotation_x) > rotation_threshold:
self.current_rotation_x += (
self.target_rotation_x - self.current_rotation_x) * self.rotation_speed
else:
self.current_rotation_x = self.target_rotation_x
if abs(self.target_rotation_y - self.current_rotation_y) > rotation_threshold:
self.current_rotation_y += (
self.target_rotation_y - self.current_rotation_y) * self.rotation_speed
else:
self.current_rotation_y = self.target_rotation_y
if abs(self.target_rotation_z - self.current_rotation_z) > rotation_threshold:
self.current_rotation_z += (
self.target_rotation_z - self.current_rotation_z) * self.rotation_speed
else:
self.current_rotation_z = self.target_rotation_z
# Render the current frame
self.render_domino()
# Schedule next frame
self.after(16, self.animate)
def render_domino(self):
"""Main rendering function - draws the complete 3D domino"""
# Clear previous frame
self.canvas.delete("all")
# Get current canvas dimensions
width = self.canvas.winfo_width()
height = self.canvas.winfo_height()
# Ensure valid dimensions (fallback if canvas not ready)
if width < 10:
width = self.canvas_width
if height < 10:
height = self.canvas_height
# Calculate center point for domino positioning
center_x = width // 2
center_y = height // 2
# Apply scale effect for floating animation
scale_float = 1.0 + abs(self.float_offset) / 100.0
# Calculate which faces are visible and in what order
face_visibility_order = self.calculate_face_visibility()
# Draw faces in back-to-front order (painter's algorithm)
for face_idx in face_visibility_order:
self.draw_face(center_x, center_y + self.float_offset, scale_float, face_idx)
def calculate_face_visibility(self):
"""Calculate face drawing order using painter's algorithm (back to front)"""
face_z_values = []
# Calculate average Z coordinate for each face after rotation
for face_idx in range(6):
total_z = 0
for vertex in self.face_vertices[face_idx]:
rotated_vertex = self.rotate_point(vertex)
total_z += rotated_vertex[2]
# Store average Z value for this face
average_z = total_z / 4
face_z_values.append((face_idx, average_z))
# Sort faces by Z coordinate (back to front for painter's algorithm)
face_z_values.sort(key=lambda x: x[1])
# Return face indices in drawing order
return [face_idx for face_idx, z in face_z_values]
def draw_face(self, center_x, center_y, scale, face_idx):
"""Draw a single face of the domino cube"""
# Project 3D vertices to 2D screen coordinates
projected_points = []
for vertex in self.face_vertices[face_idx]:
rotated_vertex = self.rotate_point(vertex)
# Apply perspective scaling
projection_scale = 1.2 + rotated_vertex[2] / 1000.0
screen_x = center_x + rotated_vertex[0] * scale * projection_scale
screen_y = center_y + rotated_vertex[1] * scale * projection_scale
projected_points.append([screen_x, screen_y])
# Flatten points for polygon drawing
polygon_points = []
for point in projected_points:
polygon_points.extend(point)
# Determine face color based on orientation (lighting simulation)
face_colors = {
self.FRONT: "#ffffff", # Pure white for front face
self.BACK: "#ffffff", # Pure white for back face
self.LEFT: "#f0f0f0", # Slightly darker for sides
self.RIGHT: "#f0f0f0", # Slightly darker for sides
self.TOP: "#e6e6e6", # Darker for top
self.BOTTOM: "#d9d9d9" # Darkest for bottom
}
face_color = face_colors.get(face_idx, self.domino_color)
# Determine outline color (highlight front/back faces)
outline_color = "#3d5afe" if face_idx in [self.FRONT, self.BACK] else "#1a237e"
# Draw face polygon
self.canvas.create_polygon(
polygon_points,
fill=face_color,
outline=outline_color,
width=2
)
# Draw domino details only on front and back faces
if face_idx in [self.FRONT, self.BACK]:
self.draw_domino_details(projected_points, face_idx)
def draw_domino_details(self, projected_points, face_idx):
"""Draw divider line and dots on front/back faces"""
# Calculate middle points for horizontal divider
left_middle = [
(projected_points[0][0] + projected_points[3][0]) / 2,
(projected_points[0][1] + projected_points[3][1]) / 2
]
right_middle = [
(projected_points[1][0] + projected_points[2][0]) / 2,
(projected_points[1][1] + projected_points[2][1]) / 2
]
# Draw divider line
self.canvas.create_line(
left_middle[0], left_middle[1],
right_middle[0], right_middle[1],
fill="#3d5afe",
width=3
)
# Draw dots on appropriate halves
if face_idx == self.FRONT:
# Front face: top value on top, bottom value on bottom
self.draw_dots(
projected_points[0], projected_points[1],
right_middle, left_middle,
self.top_value
)
self.draw_dots(
left_middle, right_middle,
projected_points[2], projected_points[3],
self.bottom_value
)
else: # BACK face
# Back face: values are reversed
self.draw_dots(
projected_points[0], projected_points[1],
right_middle, left_middle,
self.bottom_value
)
self.draw_dots(
left_middle, right_middle,
projected_points[2], projected_points[3],
self.top_value
)
def draw_dots(self, top_left, top_right, bottom_right, bottom_left, value):
"""Draw dots pattern for given domino value on specified face area"""
if value < 1 or value > 6:
return
# Calculate face dimensions for dot sizing
width = math.sqrt((top_right[0] - top_left[0])**2 + (top_right[1] - top_left[1])**2)
height = math.sqrt((bottom_left[0] - top_left[0])**2 + (bottom_left[1] - top_left[1])**2)
# Scale dot size based on face size
dot_size = min(width, height) * 0.15
# Draw each dot in the pattern
for pos in self.dot_positions[value]:
# Calculate absolute position using bilinear interpolation
x = (top_left[0] + (top_right[0] - top_left[0]) * pos[0] +
(bottom_left[0] - top_left[0]) * pos[1])
y = (top_left[1] + (top_right[1] - top_left[1]) * pos[0] +
(bottom_left[1] - top_left[1]) * pos[1])
# Draw main dot circle
self.canvas.create_oval(
x - dot_size/2, y - dot_size/2,
x + dot_size/2, y + dot_size/2,
fill="#212121",
outline="#3d5afe",
width=1
)
# Add highlight for 3D effect
highlight_size = dot_size / 3
self.canvas.create_oval(
x - highlight_size, y - highlight_size,
x - highlight_size/3, y - highlight_size/3,
fill="#3d5afe",
outline=""
)
def rotate_point(self, point):
"""Apply 3D rotation transformations to a point"""
# Start with original point coordinates
result = [0, 0, 0]
temp = point.copy()
# Convert rotation angles to radians
rad_x = math.radians(self.current_rotation_x)
rad_y = math.radians(self.current_rotation_y)
rad_z = math.radians(self.current_rotation_z)
# Apply X rotation (pitch)
result[0] = temp[0]
result[1] = temp[1] * math.cos(rad_x) - temp[2] * math.sin(rad_x)
result[2] = temp[1] * math.sin(rad_x) + temp[2] * math.cos(rad_x)
temp = result.copy()
# Apply Y rotation (yaw)
result[0] = temp[0] * math.cos(rad_y) + temp[2] * math.sin(rad_y)
result[1] = temp[1]
result[2] = -temp[0] * math.sin(rad_y) + temp[2] * math.cos(rad_y)
temp = result.copy()
# Apply Z rotation (roll)
result[0] = temp[0] * math.cos(rad_z) - temp[1] * math.sin(rad_z)
result[1] = temp[0] * math.sin(rad_z) + temp[1] * math.cos(rad_z)
result[2] = temp[2]
return result
# Application entry point
if __name__ == "__main__":
# Create and run the domino application
app = Domino()
app.mainloop()
The Final Result:
Download Projects Source Code





