Skip to content

Instantly share code, notes, and snippets.

@TACIXAT
Created May 18, 2021 19:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TACIXAT/d2da3b513a4e9405ea5d2b3de6d4c80e to your computer and use it in GitHub Desktop.
Save TACIXAT/d2da3b513a4e9405ea5d2b3de6d4c80e to your computer and use it in GitHub Desktop.
Visualization of cue ball departure angles in Processing3d.
import math
def setup():
size(1000, 1000)
background(0x33, 0x66, 0x00)
# carom
DIAMETER = 61.0 # mm
RADIUS = DIAMETER / 2
# get y on circle given x and radius
def circle_y(r, x):
return math.sqrt(abs(r**2 - x**2))
# get slope and intercept given two points
def linear(start_x, start_y, end_x, end_y):
m = (end_y - start_y) / (end_x - start_x)
b = end_y - m*end_x
return m, b
# get x given y, m b
def linear_x(m, b, y):
return (y - b) / m
# get y given m, x, b
def linear_y(m, b, x):
return m*x + b
# distance between two points
def dist(x1, y1, x2, y2):
return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
# adapted from
# https://cp-algorithms.com/geometry/circle-line-intersection.html
def collisions(a, b, c, x, y, radius):
# adjust to origin
c += a*x
c -= y
# get initials
d0 = abs(c) / math.sqrt(a**2 + b**2)
x0 = - a*c / (a**2 + b**2)
y0 = - b*c / (a**2 + b**2)
# single point
if abs(radius - d0) < 1:
return (x0+x, y0+y)
# no intersection
elif radius < d0:
return None
# radius > dist
# calc both intersects
d = math.sqrt(radius**2 - c**2 / (a**2 + b**2))
m = math.sqrt(d**2 / (a**2 + b**2))
x1 = x0 + b*m
y1 = y0 - a*m
x2 = x0 - b*m
y2 = y0 + a*m
return (x1+x, y1+y, x2+x, y2+y)
def ghost(cue_x, cue_y, obj_x, obj_y, angle):
# right triangle formed between full ball hit and aimed shot
opp = DIAMETER * (1-angle['thickness'])
adj = dist(cue_x, cue_y, obj_x, obj_y)
hyp = math.sqrt(opp**2 + adj**2)
# angle between full ball hit and aimed shot
rads_diff = math.asin(opp / hyp)
# angle of full ball hit
if cue_x == obj_x: # handle tan(90)
if cue_y > obj_y: # shooting downwards
rads_full = 3*PI/2
else: # shooting upwards
rads_full = PI/2
else: # easy mode just do arctan
m, _ = linear(cue_x, cue_y, obj_x, obj_y)
rads_full = math.atan(m)
# calculate aim line
aim_rads = rads_full - rads_diff
aim_x = cue_x + hyp*math.cos(aim_rads)
aim_y = cue_y + hyp*math.sin(aim_rads)
aim_m, aim_b = linear(aim_x, aim_y, cue_x, cue_y)
# double radius to get center of ghost ball ;)
# we're just looking for the aim line's intersection at
# a ball "twice as big" i.e. the radius of our two balls colliding
cols = collisions(aim_m, -1, aim_b, obj_x, obj_y, DIAMETER)
if cols is None:
return
if len(cols) == 2:
ghost_x, ghost_y = cols
else: # get closer of two intersections
ghost_x, ghost_y, x2, y2 = cols
if dist(cue_x, cue_y, x2, y2) < dist(cue_x, cue_y, ghost_x, ghost_y):
ghost_x, ghost_y = x2, y2
# project the lines this much further
extend = -2*DIAMETER
m, b = linear(cue_x, cue_y, ghost_x, ghost_y)
line(cue_x, cue_y, linear_x(m, b, aim_y+extend), aim_y+extend)
m, b = linear(cue_x+RADIUS, cue_y, ghost_x+RADIUS, ghost_y)
line(cue_x+RADIUS, cue_y, linear_x(m, b, ghost_y+extend), ghost_y+extend)
m, b = linear(cue_x-RADIUS, cue_y, ghost_x-RADIUS, ghost_y)
line(cue_x-RADIUS, cue_y, linear_x(m, b, ghost_y+extend), ghost_y+extend)
# draw ghost ball
fill(0xff, 0xff, 0xff, 0x88)
ellipse(ghost_x, ghost_y, DIAMETER, DIAMETER)
# draw departure line
departure_rads = aim_rads + -math.radians(angle['departure'])
departure_x = ghost_x + hyp*math.cos(departure_rads)
departure_y = ghost_y + hyp*math.sin(departure_rads)
line(ghost_x, ghost_y, departure_x, departure_y)
idx = 0
angles = [
{'name':'7/8', 'thickness': 7./8., 'departure': 20},
{'name':'3/4', 'thickness': 3./4., 'departure': 30},
{'name':'5/8', 'thickness': 5./8., 'departure': 34},
{'name':'1/2', 'thickness': 1./2., 'departure': 36},
{'name':'3/8', 'thickness': 3./8., 'departure': 34},
{'name':'1/4', 'thickness': 1./4., 'departure': 30},
{'name':'1/8', 'thickness': 1./8., 'departure': 22},
{'name':'3%', 'thickness': 0.03, 'departure': 12},
]
def draw():
global idx
background(0x33, 0x66, 0x00)
cue_x = 500
cue_y = 700
fill(0xff, 0xff, 0xff, 0xff)
ellipse(cue_x, cue_y,DIAMETER, DIAMETER)
obj_x = 500
obj_y = 300
fill(0xdd, 0x33, 0x33, 0xff)
ellipse(obj_x, obj_y, DIAMETER, DIAMETER)
ghost(cue_x, cue_y, obj_x, obj_y, angles[idx])
fill(0x66, 0x66, 0xff, 0xff)
textSize(64)
text(angles[idx]['name'], 50, 100)
translate(0, width)
rotate(-PI/2)
fill(0xff, 0xff, 0xff, 0x08)
textSize(64)
text('tacixat', width/2, 300)
rotate(PI/2)
translate(0, -width)
# delay(1000)
# idx = (idx + 1) % len(angles)
def mouseReleased():
global idx
idx += 1
if idx >= len(angles):
idx = 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment