"""Rotating Cube, by Al Sweigart al@inventwithpython.com
A rotating cube animation. Press Ctrl-C to stop.
This code is available at https://nostarch.com/big-book-small-python-programming
Tags: large, artistic, math"""
# This program MUST be run in a Terminal/Command Prompt window.
import math, time, sys, os
# Set up the constants:
PAUSE_AMOUNT = 0.1 # Pause length of one-tenth of a second.
WIDTH, HEIGHT = 80, 24
SCALEX = (WIDTH - 4) // 8
SCALEY = (HEIGHT - 4) // 8
# Text cells are twice as tall as they are wide, so set scaley:
SCALEY *= 2
TRANSLATEX = (WIDTH - 4) // 2
TRANSLATEY = (HEIGHT - 4) // 2
# (!) Try changing this to '#' or '*' or some other character:
LINE_CHAR = chr(9608) # Character 9608 is a solid block.
# (!) Try setting two of these values to zero to rotate the cube only
# along a single axis:
X_ROTATE_SPEED = 0.03
Y_ROTATE_SPEED = 0.08
Z_ROTATE_SPEED = 0.13
# This program stores XYZ coordinates in lists, with the X coordinate
# at index 0, Y at 1, and Z at 2. These constants make our code more
# readable when accessing the coordinates in these lists.
X = 0
Y = 1
Z = 2
def line(x1, y1, x2, y2):
"""Returns a list of points in a line between the given points.
Uses the Bresenham line algorithm. More info at:
https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm"""
points = [] # Contains the points of the line.
# "Steep" means the slope of the line is greater than 45 degrees or
# less than -45 degrees:
# Check for the special case where the start and end points are
# certain neighbors, which this function doesn't handle correctly,
# and return a hard coded list instead:
if (x1 == x2 and y1 == y2 + 1) or (y1 == y2 and x1 == x2 + 1):
return [(x1, y1), (x2, y2)]
isSteep = abs(y2 - y1) > abs(x2 - x1)
if isSteep:
# This algorithm only handles non-steep lines, so let's change
# the slope to non-steep and change it back later.
x1, y1 = y1, x1 # Swap x1 and y1
x2, y2 = y2, x2 # Swap x2 and y2
isReversed = x1 > x2 # True if the line goes right-to-left.
if isReversed: # Get the points on the line going right-to-left.
x1, x2 = x2, x1 # Swap x1 and x2
y1, y2 = y2, y1 # Swap y1 and y2
deltax = x2 - x1
deltay = abs(y2 - y1)
extray = int(deltax / 2)
currenty = y2
if y1 < y2:
ydirection = 1
else:
ydirection = -1
# Calculate the y for every x in this line:
for currentx in range(x2, x1 - 1, -1):
if isSteep:
points.append((currenty, currentx))
else:
points.append((currentx, currenty))
extray -= deltay
if extray <= 0: # Only change y once extray <= 0.
currenty -= ydirection
extray += deltax
else: # Get the points on the line going left to right.
deltax = x2 - x1
deltay = abs(y2 - y1)
extray = int(deltax / 2)
currenty = y1
if y1 < y2:
ydirection = 1
else:
ydirection = -1
# Calculate the y for every x in this line:
for currentx in range(x1, x2 + 1):
if isSteep:
points.append((currenty, currentx))
else:
points.append((currentx, currenty))
extray -= deltay
if extray < 0: # Only change y once extray < 0.
currenty += ydirection
extray += deltax
return points
def rotatePoint(x, y, z, ax, ay, az):
"""Returns an (x, y, z) tuple of the x, y, z arguments rotated.
The rotation happens around the 0, 0, 0 origin by angles
ax, ay, az (in radians).
Directions of each axis:
-y
|
+-- +x
/
+z
"""
# Rotate around x axis:
rotatedX = x
rotatedY = (y * math.cos(ax)) - (z * math.sin(ax))
rotatedZ = (y * math.sin(ax)) + (z * math.cos(ax))
x, y, z = rotatedX, rotatedY, rotatedZ
# Rotate around y axis:
rotatedX = (z * math.sin(ay)) + (x * math.cos(ay))
rotatedY = y
rotatedZ = (z * math.cos(ay)) - (x * math.sin(ay))
x, y, z = rotatedX, rotatedY, rotatedZ
# Rotate around z axis:
rotatedX = (x * math.cos(az)) - (y * math.sin(az))
rotatedY = (x * math.sin(az)) + (y * math.cos(az))
rotatedZ = z
return (rotatedX, rotatedY, rotatedZ)
def adjustPoint(point):
"""Adjusts the 3D XYZ point to a 2D XY point fit for displaying on
the screen. This resizes this 2D point by a scale of SCALEX and
SCALEY, then moves the point by TRANSLATEX and TRANSLATEY."""
return (int(point[X] * SCALEX + TRANSLATEX),
int(point[Y] * SCALEY + TRANSLATEY))
"""CUBE_CORNERS stores the XYZ coordinates of the corners of a cube.
The indexes for each corner in CUBE_CORNERS are marked in this diagram:
0---1
/| /|
2---3 |
| 4-|-5
|/ |/
6---7"""
CUBE_CORNERS = [[-1, -1, -1], # Point 0
[ 1, -1, -1], # Point 1
[-1, -1, 1], # Point 2
[ 1, -1, 1], # Point 3
[-1, 1, -1], # Point 4
[ 1, 1, -1], # Point 5
[-1, 1, 1], # Point 6
[ 1, 1, 1]] # Point 7
# rotatedCorners stores the XYZ coordinates from CUBE_CORNERS after
# they've been rotated by rx, ry, and rz amounts:
rotatedCorners = [None, None, None, None, None, None, None, None]
# Rotation amounts for each axis:
xRotation = 0.0
yRotation = 0.0
zRotation = 0.0
try:
while True: # Main program loop.
# Rotate the cube along different axes by different amounts:
xRotation += X_ROTATE_SPEED
yRotation += Y_ROTATE_SPEED
zRotation += Z_ROTATE_SPEED
for i in range(len(CUBE_CORNERS)):
x = CUBE_CORNERS[i][X]
y = CUBE_CORNERS[i][Y]
z = CUBE_CORNERS[i][Z]
rotatedCorners[i] = rotatePoint(x, y, z, xRotation,
yRotation, zRotation)
# Get the points of the cube lines:
cubePoints = []
for fromCornerIndex, toCornerIndex in ((0, 1), (1, 3), (3, 2), (2, 0), (0, 4), (1, 5), (2, 6), (3, 7), (4, 5), (5, 7), (7, 6), (6, 4)):
fromX, fromY = adjustPoint(rotatedCorners[fromCornerIndex])
toX, toY = adjustPoint(rotatedCorners[toCornerIndex])
pointsOnLine = line(fromX, fromY, toX, toY)
cubePoints.extend(pointsOnLine)
# Get rid of duplicate points:
cubePoints = tuple(frozenset(cubePoints))
# Display the cube on the screen:
for y in range(HEIGHT):
for x in range(WIDTH):
if (x, y) in cubePoints:
# Display full block:
print(LINE_CHAR, end='', flush=False)
else:
# Display empty space:
print(' ', end='', flush=False)
print(flush=False)
print('Press Ctrl-C to quit.', end='', flush=True)
time.sleep(PAUSE_AMOUNT) # Pause for a bit.
# Clear the screen:
if sys.platform == 'win32':
os.system('cls') # Windows uses the cls command.
else:
os.system('clear') # macOS and Linux use the clear command.
except KeyboardInterrupt:
print('Rotating Cube, by Al Sweigart al@inventwithpython.com')
sys.exit() # When Ctrl-C is pressed, end the program.