Python Tkinter 3D Domino Piece

How to Create a 3D Domino Piece in Python Tkinter

Python Tkinter 3D Domino Piece


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.






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:

Python Tkinter 3D Domino Piece

Python Tkinter 3D Domino Piece

Python Tkinter 3D Domino Piece

Python Tkinter 3D Domino Piece

Python Tkinter 3D Domino Piece







Share this

Related Posts

Latest
Previous
Next Post »