import dash
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
import plotly.graph_objs as go
import numpy as np
# Initialize the app
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
# Cube dimensions
cube_size = 10
ball_radius = 0.5
# Initial ball state
initial_ball_state = {
'x': cube_size / 2,
'y': cube_size / 2,
'z': cube_size / 2,
'vx': 0.1,
'vy': 0.15,
'vz': 0.2,
'trajectory_x': [],
'trajectory_y': [],
'trajectory_z': [],
'hits_x': [],
'hits_y': [],
'hits_z': [],
'hit_sides': []
}
# Define the layout
app.layout = dbc.Container(
[
dbc.Row(
dbc.Col(html.H1("3D Bouncing Ball Simulation"), className="text-center")
),
dbc.Row(
dbc.Col(
dcc.Slider(
id='velocity-slider',
min=0.1,
max=1,
step=0.05,
value=0.5,
marks={i: f'{i:.1f}' for i in np.arange(0.1, 1.1, 0.1)},
tooltip={"placement": "bottom", "always_visible": True}
),
width=12,
)
),
dbc.Row(
dbc.Col(
dcc.Graph(id='ball-graph', style={"height": "600px"}),
width=12,
)
),
dcc.Interval(id='interval-component', interval=30, n_intervals=0),
dcc.Store(id='ball-state', data=initial_ball_state),
],
fluid=True,
)
def update_ball_state(ball_state, velocity_multiplier):
x, y, z = ball_state['x'], ball_state['y'], ball_state['z']
vx, vy, vz = ball_state['vx'], ball_state['vy'], ball_state['vz']
# Update ball position
x += vx * velocity_multiplier
y += vy * velocity_multiplier
z += vz * velocity_multiplier
# Update trajectory
ball_state['trajectory_x'].append(x)
ball_state['trajectory_y'].append(y)
ball_state['trajectory_z'].append(z)
# Keep trajectory length limited
if len(ball_state['trajectory_x']) > 100:
ball_state['trajectory_x'] = ball_state['trajectory_x'][-100:]
ball_state['trajectory_y'] = ball_state['trajectory_y'][-100:]
ball_state['trajectory_z'] = ball_state['trajectory_z'][-100:]
# Check for collisions with the walls and mark hits
if x - ball_radius < 0:
vx = abs(vx)
ball_state['hits_x'].append(0)
ball_state['hits_y'].append(y)
ball_state['hits_z'].append(z)
ball_state['hit_sides'].append('x=0')
elif x + ball_radius > cube_size:
vx = -abs(vx)
ball_state['hits_x'].append(cube_size)
ball_state['hits_y'].append(y)
ball_state['hits_z'].append(z)
ball_state['hit_sides'].append('x=cube_size')
if y - ball_radius < 0:
vy = abs(vy)
ball_state['hits_x'].append(x)
ball_state['hits_y'].append(0)
ball_state['hits_z'].append(z)
ball_state['hit_sides'].append('y=0')
elif y + ball_radius > cube_size:
vy = -abs(vy)
ball_state['hits_x'].append(x)
ball_state['hits_y'].append(cube_size)
ball_state['hits_z'].append(z)
ball_state['hit_sides'].append('y=cube_size')
if z - ball_radius < 0:
vz = abs(vz)
ball_state['hits_x'].append(x)
ball_state['hits_y'].append(y)
ball_state['hits_z'].append(0)
ball_state['hit_sides'].append('z=0')
elif z + ball_radius > cube_size:
vz = -abs(vz)
ball_state['hits_x'].append(x)
ball_state['hits_y'].append(y)
ball_state['hits_z'].append(cube_size)
ball_state['hit_sides'].append('z=cube_size')
ball_state.update({'x': x, 'y': y, 'z': z, 'vx': vx, 'vy': vy, 'vz': vz})
return ball_state
@app.callback(
[Output('ball-graph', 'figure'),
Output('ball-state', 'data')],
[Input('interval-component', 'n_intervals'),
Input('velocity-slider', 'value')],
[State('ball-state', 'data')]
)
def update_graph(n_intervals, velocity_multiplier, ball_state):
ball_state = update_ball_state(ball_state, velocity_multiplier)
x, y, z = ball_state['x'], ball_state['y'], ball_state['z']
# Ball trace with size adjustment based on z position
ball_trace = go.Scatter3d(
x=[x],
y=[y],
z=[z],
mode='markers',
marker=dict(size=10 * (1 + z / cube_size), color='red'), # Adjust size for depth perception
showlegend=False,
hoverinfo='skip'
)
# Ball trajectory trace with fireball-like appearance
trajectory_trace = go.Scatter3d(
x=ball_state['trajectory_x'],
y=ball_state['trajectory_y'],
z=ball_state['trajectory_z'],
mode='lines',
line=dict(color='orange', width=2),
showlegend=False,
hoverinfo='skip'
)
# Hit locations trace with size adjustment based on z position
hit_sides = ball_state['hit_sides']
hits_trace = []
colors = {
'x=0': 'green',
'x=cube_size': 'yellow',
'y=0': 'blue',
'y=cube_size': 'purple',
'z=0': 'cyan',
'z=cube_size': 'magenta'
}
for side in colors:
hit_indices = [i for i, hit_side in enumerate(hit_sides) if hit_side == side]
hits_trace.append(go.Scatter3d(
x=[ball_state['hits_x'][i] for i in hit_indices],
y=[ball_state['hits_y'][i] for i in hit_indices],
z=[ball_state['hits_z'][i] for i in hit_indices],
mode='markers',
marker=dict(size=5 * (1 + np.array([ball_state['hits_z'][i] for i in hit_indices]) / cube_size), color=colors[side]), # Adjust size for depth perception
showlegend=False,
hoverinfo='skip'
))
# Cube edges
cube_edges = go.Scatter3d(
x=[0, cube_size, cube_size, 0, 0, 0, 0, 0, cube_size, cube_size, cube_size, cube_size, 0, 0, 0, cube_size, cube_size, cube_size, 0, 0],
y=[0, 0, cube_size, cube_size, 0, cube_size, cube_size, 0, 0, cube_size, cube_size, 0, cube_size, cube_size, 0, 0, 0, 0, cube_size, cube_size],
z=[0, 0, 0, 0, 0, 0, cube_size, cube_size, 0, 0, cube_size, cube_size, 0, cube_size, cube_size, cube_size, cube_size, 0, 0, cube_size],
mode='lines',
line=dict(color='black', width=6), # Thicker edges
showlegend=False,
hoverinfo='skip'
)
# Light grey back surfaces
back_surfaces = go.Mesh3d(
x=[0, cube_size, cube_size, 0, 0, 0, 0, cube_size, cube_size, 0, 0, cube_size, cube_size, cube_size, cube_size, 0],
y=[0, 0, cube_size, cube_size, 0, cube_size, cube_size, 0, 0, cube_size, cube_size, cube_size, 0, 0, cube_size, 0],
z=[0, 0, 0, 0, cube_size, cube_size, 0, 0, cube_size, cube_size, 0, cube_size, cube_size, cube_size, cube_size, cube_size],
color='lightgrey',
opacity=0.1,
showlegend=False,
hoverinfo='skip'
)
layout = go.Layout(
scene=dict(
xaxis=dict(range=[-1, cube_size + 1], showbackground=False, showticklabels=False, visible=False),
yaxis=dict(range=[-1, cube_size + 1], showbackground=False, showticklabels=False, visible=False),
zaxis=dict(range=[-1, cube_size + 1], showbackground=False, showticklabels=False, visible=False),
aspectratio=dict(x=1, y=1, z=1),
aspectmode='manual',
camera=dict(
eye=dict(x=1.5, y=1.5, z=1.5),
up=dict(x=0, y=0, z=1),
center=dict(x=0.5, y=0.5, z=0.5)
)
),
margin=dict(l=0, r=0, b=0, t=0),
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
showlegend=False # Hide the legend
)
fig = go.Figure(data=[ball_trace, trajectory_trace, back_surfaces, cube_edges] + hits_trace, layout=layout)
return fig, ball_state
# Run the app
if __name__ == "__main__":
app.run_server(debug=True)