Created
May 18, 2021 19:55
-
-
Save TACIXAT/d2da3b513a4e9405ea5d2b3de6d4c80e to your computer and use it in GitHub Desktop.
Visualization of cue ball departure angles in Processing3d.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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