Java - Modern Toggle Buttons in Java Swing

How to Create Animated Toggle Buttons in Java Netbeans

How to Create Animated Toggle Buttons in Java Netbeans


In this Java Tutorial we will see How To Create Toggle Buttons with five different styles: Neon, Gradient, Minimal, Cyberpunk, and Neumorphic In Java Using Netbeans.

What We Are Gonna Use In This Project:

- Java Programming Language.
- NetBeans Editor.





Project Source Code:



/**
 *
 * @author 1BestCsharp
 */
public class ModernToggleButtons extends JFrame {

    public ModernToggleButtons(){
        
        // Set the title of the window
        setTitle("Modern Toggle Buttons");
        // Make the application close when the window is closed
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        // Set the initial size of the window
        setSize(500, 600);
        // Center the window on the screen
        setLocationRelativeTo(null);
        
        // Create the main panel that will hold all components
        JPanel mainPanel = new JPanel();
        // Set the layout to stack components vertically
        mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
        // Set the background color to dark gray (RGB: 26, 26, 26)
        mainPanel.setBackground(new Color(26, 26, 26));
        // Add a 20-pixel empty border around all sides of the panel
        mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
        
        // Add toggle buttons with labels and vertical spacing
        mainPanel.add(createTogglePanel("Neon", new NeonToggleButton()));
        mainPanel.add(Box.createVerticalStrut(30));
        mainPanel.add(createTogglePanel("Gradient", new GradientToggleButton()));
        mainPanel.add(Box.createVerticalStrut(30));
        mainPanel.add(createTogglePanel("Minimal", new MinimalToggleButton()));
        mainPanel.add(Box.createVerticalStrut(30));
        mainPanel.add(createTogglePanel("Cyberpunk", new CyberpunkToggleButton()));
        mainPanel.add(Box.createVerticalStrut(30));
        mainPanel.add(createTogglePanel("Neumorphic", new NeumorphicToggleButton()));
        
        
        // Add the main panel to the JFrame window
        add(mainPanel);
        
    }
    
    
    
    // Method that creates a panel containing a label, toggle button, and status label
    private JPanel createTogglePanel(String label, JToggleButton toggleButton){
        
        // Create a new panel to hold the components
        JPanel panel = new JPanel();
        // Use FlowLayout to arrange components horizontally with 20-pixel horizontal gaps
        panel.setLayout(new FlowLayout(FlowLayout.CENTER, 20, 0));
        // Set the panel's background color to dark gray
        panel.setBackground(new Color(26, 26, 26));
        
        // Create a label with the specified text
        JLabel nameLabel = new JLabel(label);
        // Set the label's text color to white
        nameLabel.setForeground(Color.WHITE);
        // Set the font to Segoe UI, normal style, 24-point size
        nameLabel.setFont(new Font("Segoe UI", Font.PLAIN, 24));
        // Set a fixed size for the label
        nameLabel.setPreferredSize(new Dimension(150, 30));
        
         // Create a label that will show "ON" or "OFF" status
        JLabel statusLabel = new JLabel("OFF");
        // Set the status label's text color to white
        statusLabel.setForeground(Color.WHITE);
        // Set the font to Segoe UI, normal style, 20-point size
        statusLabel.setFont(new Font("Segoe UI", Font.PLAIN, 20));
        // Set a fixed size for the status label
        statusLabel.setPreferredSize(new Dimension(70, 30));
        
        // Check if the toggle button is an instance of our custom BaseToggleButton class
         if (toggleButton instanceof BaseToggleButton) {
             
            // Cast the toggle button to BaseToggleButton to access its specific methods
            BaseToggleButton baseToggle = (BaseToggleButton) toggleButton;
            // Set a callback function that updates the status label when the toggle state changes
            baseToggle.setStatusUpdateCallback(isOn -> {
                
                // Update the text to show "ON" or "OFF"
                statusLabel.setText(isOn ? "ON" : "OFF");
                // Change the text color to green when ON, white when OFF
                statusLabel.setForeground(isOn ? new Color(0, 255, 136) : Color.WHITE);
                
            });
             
         }

        // Add the components to the panel
        panel.add(nameLabel);
        panel.add(toggleButton);
        panel.add(statusLabel);
        
        return panel;
        
    }
    
    
    
    
    // Abstract base class for all custom toggle buttons
    private abstract class BaseToggleButton extends JToggleButton {
        
        // Value between 0.0 (OFF) and 1.0 (ON) for animation progress
        protected float animationProgress = 0.0f;
        // Timer object to handle the animation
        protected Timer animationTimer;
        // Duration of the animation in milliseconds
        protected final int ANIMATION_DURATION = 200;
        // Number of steps in the animation
        protected final int ANIMATION_STEPS = 20;
       // Callback function that will be called when the toggle state changes
        protected Runnable statusUpdateCallback;
        
        public BaseToggleButton(){
            
            // Set the size of the button
            setPreferredSize(new Dimension(120, 60));
            // Remove the default styling
            setBorderPainted(false);
            setFocusPainted(false);
            setContentAreaFilled(false);
            setCursor(new Cursor(Cursor.HAND_CURSOR));
            
            // Add a listener to detect when the button is clicked
            addItemListener(e -> {
                startAnimation(e.getStateChange() == ItemEvent.SELECTED);
            });
            
        }
        
        // Method to set the callback function that updates the status label
        public void setStatusUpdateCallback(Runnable callback) {
            this.statusUpdateCallback = callback;
        }
        
        // Method to start the animation when the button state changes
        protected void startAnimation(boolean targetState){
            
            // Stop any existing animation
            if (animationTimer != null && animationTimer.isRunning()) {
                animationTimer.stop();
            }
            
            // Calculate animation parameters
            final float targetProgress = targetState ? 1.0f : 0.0f;
            final float startProgress = animationProgress;
            final float progressStep = (targetProgress - startProgress) / ANIMATION_STEPS;
            final int delay = ANIMATION_DURATION / ANIMATION_STEPS;
            
            // Create a new timer for the animation
            animationTimer = new Timer(delay, null);
            animationTimer.addActionListener(new ActionListener() {
                
                private int currentStep = 0;
                
                @Override
                public void actionPerformed(ActionEvent e) {
                
                    currentStep++;
                    
                     if (currentStep <= ANIMATION_STEPS) {
                         
                        animationProgress = startProgress + (progressStep * currentStep);
                        repaint();
                         
                     }
                     else{
                        animationProgress = targetProgress;
                        animationTimer.stop();
                         if (statusUpdateCallback != null) {
                            statusUpdateCallback.run(targetState);
                        }
                     }
                    
                }
                
            });
            
            // Start the animation timer
            animationTimer.start();
            
        }
        
        // Interface for the status update callback
        public interface Runnable {
            void run(boolean isOn);
        }
        
    }
    
    
    // Neon-style toggle button implementation
    private class NeonToggleButton extends BaseToggleButton {
        
        @Override
        protected void paintComponent(Graphics g) {
            
            Graphics2D g2 = (Graphics2D) g.create();
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            
            // Define colors for OFF and ON states
            Color bgOffColor = new Color(51, 51, 51);
            Color bgOnColor = new Color(0, 255, 136);
            Color currentBgColor = interpolateColor(bgOffColor, bgOnColor, animationProgress);
            
            // Draw the rounded rectangle background
            g2.setColor(currentBgColor);
            g2.fill(new RoundRectangle2D.Float(0, 0, getWidth(), getHeight(), 60, 60));
            
            // Draw the white circle that moves
            g2.setColor(Color.WHITE);
            int diameter = getHeight() - 8;
            float offX = 4;
            float onX = getWidth() - diameter - 4;
            float currentX = offX + (onX - offX) * animationProgress;
            g2.fillOval(Math.round(currentX), 4, diameter, diameter);
            
            // Add glow effect that increases with animation progress
            int alpha = Math.round(50 * animationProgress);
            if (alpha > 0) {
                g2.setColor(new Color(0, 255, 136, alpha));
                g2.setStroke(new BasicStroke(6));
                g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 60, 60);
            }
            
            g2.dispose();
            
        }
        
    }
    
    
   // Gradient-style toggle button implementation
    private class GradientToggleButton extends BaseToggleButton {
        
        @Override
        protected void paintComponent(Graphics g) {
            
            Graphics2D g2 = (Graphics2D) g.create();
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            
            // Define colors for OFF and ON states
            Color offStartColor = new Color(255, 107, 107);
            Color offEndColor = new Color(254, 202, 87);
            Color onStartColor = new Color(72, 52, 212);
            Color onEndColor = new Color(104, 109, 224);
            
            // Calculate the current colors based on animation progress
            Color currentStartColor = interpolateColor(offStartColor, onStartColor, animationProgress);
            Color currentEndColor = interpolateColor(offEndColor, onEndColor, animationProgress);
            
            // Create a gradient and apply it
            GradientPaint gradient = new GradientPaint(0, 0, currentStartColor,
                                      getWidth(), getHeight(), currentEndColor);
            g2.setPaint(gradient);
            g2.fill(new RoundRectangle2D.Float(0, 0, getWidth(), getHeight(), 60, 60));
            
            // Draw the white circle that moves
            g2.setColor(Color.WHITE);
            int diameter = getHeight() - 8;
            float offX = 4;
            float onX = getWidth() - diameter - 4;
            float currentX = offX + (onX - offX) * animationProgress;
            g2.fillOval(Math.round(currentX), 4, diameter, diameter);
            
            g2.dispose();
        }
        
    }
    
    
     // Minimal-style toggle button implementation
     private class MinimalToggleButton extends BaseToggleButton {  
         
         @Override
         protected void paintComponent(Graphics g) {
             
            Graphics2D g2 = (Graphics2D) g.create();
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            
            // Define colors for OFF and ON states
            Color offColor = Color.WHITE;
            Color onColor = new Color(0, 255, 136);
            Color currentColor = interpolateColor(offColor, onColor, animationProgress); 
            
            // Draw the border of the toggle button
            g2.setColor(currentColor);
            g2.setStroke(new BasicStroke(3));
            g2.draw(new RoundRectangle2D.Float(1, 1, getWidth() - 3, getHeight() - 3, 60, 60));
            
            // Draw the circle that moves
            g2.setColor(currentColor);
            int diameter = getHeight() - 12;
            float offX = 6;
            float onX = getWidth() - diameter - 6;
            float currentX = offX + (onX - offX) * animationProgress;
            g2.fillOval(Math.round(currentX), 6, diameter, diameter);
            
            g2.dispose();
            
         }
         
     }
    
    
     
     // Cyberpunk-style toggle button implementation
     private class CyberpunkToggleButton extends BaseToggleButton {
         
         @Override
         protected void paintComponent(Graphics g) {
             
            Graphics2D g2 = (Graphics2D) g.create();
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            
            // Draw the dark background with less rounded corners
            g2.setColor(new Color(45, 52, 54));
            g2.fill(new RoundRectangle2D.Float(0, 0, getWidth(), getHeight(), 10, 10));
            
             // Define colors for OFF and ON states
            Color offColor = new Color(255, 0, 255); // Magenta
            Color onColor = new Color(0, 255, 255);  // Cyan
            Color currentColor = interpolateColor(offColor, onColor, animationProgress);
            
             // Draw a thin border around the toggle
            g2.setColor(currentColor);
            g2.setStroke(new BasicStroke(2));
            g2.draw(new RoundRectangle2D.Float(1, 1, getWidth() - 3, getHeight() - 3, 10, 10));
            
            // Create a gradient for the slider
            GradientPaint gradient = new GradientPaint(0, 0, new Color(255, 0, 255),
                                      getWidth(), getHeight(), new Color(0, 255, 255));
            g2.setPaint(gradient);
            
            // Draw the slider as a rounded rectangle
            float offX = 4;
            float onX = getWidth() - 52;
            float currentX = offX + (onX - offX) * animationProgress;
            g2.fill(new RoundRectangle2D.Float(currentX, 4, 48, getHeight() - 8, 5, 5));
            
            g2.dispose();
             
         }
         
     }
     
     
     // Neumorphic-style toggle button implementation
     private class NeumorphicToggleButton extends BaseToggleButton {
         
         @Override
         protected void paintComponent(Graphics g) {
             
            Graphics2D g2 = (Graphics2D) g.create();
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            
            // Store dimensions
            int width = getWidth();
            int height = getHeight();
            int cornerRadius = 60;
            
            // Define colors for the neumorphic effect
            Color baseColor = new Color(26, 26, 26);
            Color lightShadow = new Color(40, 40, 40);
            Color darkShadow = new Color(10, 10, 10);
            
            // Draw the base shape
            g2.setColor(baseColor);
            g2.fill(new RoundRectangle2D.Float(0, 0, width, height, cornerRadius, cornerRadius));
            
            // Draw the lighter edge on top-left
            g2.setColor(lightShadow);
            g2.setStroke(new BasicStroke(2f));
            g2.draw(new RoundRectangle2D.Float(1, 1, width-2, height-2, cornerRadius, cornerRadius));
            
            // Draw the darker edge on bottom-right
            g2.setColor(darkShadow);
            g2.setStroke(new BasicStroke(2f));
            g2.draw(new RoundRectangle2D.Float(3, 3, width-6, height-6, cornerRadius, cornerRadius));
            
            // Draw the inner background
            g2.setColor(new Color(22, 22, 22));
            g2.fill(new RoundRectangle2D.Float(4, 4, width-8, height-8, cornerRadius-8, cornerRadius-8));
            
            // Define colors for OFF and ON states
            Color offStartColor = new Color(30, 30, 30);
            Color offEndColor = new Color(34, 34, 34);
            Color onStartColor = new Color(0, 255, 136);
            Color onEndColor = new Color(0, 204, 106);
            
            // Calculate the current colors based on animation progress
            Color currentStartColor = interpolateColor(offStartColor, onStartColor, animationProgress);
            Color currentEndColor = interpolateColor(offEndColor, onEndColor, animationProgress);
            
            // Create a gradient and apply it
            GradientPaint gradient = new GradientPaint(0, 0, currentStartColor,
                                      width, height, currentEndColor);
            
            // Position and draw the toggle circle
            g2.setPaint(gradient);
            int diameter = height - 16;
            float offX = 8;
            float onX = width - diameter - 8;
            float currentX = offX + (onX - offX) * animationProgress;
            
            // Draw the circle shadow with slight offset
            g2.setColor(darkShadow);
            g2.fillOval(Math.round(currentX)+2, 6, diameter, diameter);
            
            // Draw the main circle with gradient
            g2.setPaint(gradient);
            g2.fillOval(Math.round(currentX), 8, diameter, diameter);
            
            // Add highlight to the circle
            g2.setColor(new Color(255, 255, 255, 30));
            g2.fillArc(Math.round(currentX), 8, diameter, diameter, 45, 180);
            
            g2.dispose();
             
         }
         
     }
     
     
     
     
    // Method to blend between two colors based on a ratio (0.0 to 1.0)
    private Color interpolateColor(Color c1, Color c2, float ratio) {
        
        int red = Math.round(c1.getRed() * (1 - ratio) + c2.getRed() * ratio);
        int green = Math.round(c1.getGreen() * (1 - ratio) + c2.getGreen() * ratio);
        int blue = Math.round(c1.getBlue() * (1 - ratio) + c2.getBlue() * ratio);
        
        return new Color(red, green, blue);
        
    }
    
    
    
    public static void main(String[] args) {
     
        SwingUtilities.invokeLater(() ->{
        
          try {
                // Set the application's look and feel to match the OS
                UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            } catch (Exception e) {
                e.printStackTrace();
            }
          
          // Create and display the frame
          new ModernToggleButtons().setVisible(true);
            
        });
        
    }
    
}


  


The Final Result:



Modern Toggle Buttons in Java Swing (OFF)

Modern Toggle Buttons in Java Swing (ON)
















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