Wayfindersix's picture
More air — triangle stat blocks, diamond step numbers, hover jumps, bigger everything
8efa7b0
Raw
History Blame Contribute Delete
22.5 kB
"""
Hoverboard Calculator — Because Michael J. Fox Lied To Me (But It's Cool)
Built for the Build Small Hackathon 2026.
An AI-powered acoustic phase array designer for frictionless surfaces.
Not levitation. Friction cancellation. The distinction matters —
it requires orders of magnitude less force.
Tile a surface with equilateral triangles. Speaker at each vertex.
Three speakers converge on each centroid = acoustic pressure node.
2,300 nodes = a skateboard that glides like it's on ice.
"It's not a hoverboard. It's an acoustic air hockey table.
And the grid is triangles, not holes."
Inspired by Real Genius (1985), Weird Science (1985), and one guy
who drew helicopters 400 years too early.
"""
import gradio as gr
import math
import json
import os
from openai import OpenAI
# --- NVIDIA Nemotron ---
NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY", "")
NEMOTRON_MODEL = "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning"
nvidia_client = None
if NVIDIA_API_KEY:
nvidia_client = OpenAI(
base_url="https://integrate.api.nvidia.com/v1",
api_key=NVIDIA_API_KEY,
)
print("NVIDIA Nemotron connected.")
else:
print("No NVIDIA_API_KEY — running in fallback mode.")
# --- Physics Engine ---
SPEED_OF_SOUND = 343.0 # m/s at 20C
FREQUENCY = 40000 # 40kHz ultrasonic
WAVELENGTH = SPEED_OF_SOUND / FREQUENCY # ~8.575mm
FORCE_PER_NODE_GRAMS = 3.0 # conservative, from published research
def calculate_grid(width_m, height_m, triangle_side_mm):
"""Calculate the full triangular grid for a surface."""
side_m = triangle_side_mm / 1000.0
h = side_m * math.sqrt(3) / 2 # triangle height
cols = int(width_m / side_m) + 1
rows = int(height_m / h) + 1
# Generate vertex positions (speaker locations)
vertices = set()
triangles = []
for row in range(rows):
for col in range(cols):
x = col * side_m + (0.5 * side_m if row % 2 else 0)
y = row * h
if x <= width_m + 0.001 and y <= height_m + 0.001:
vertices.add((round(x, 6), round(y, 6)))
vertices = sorted(vertices)
# Count triangles (each upward + downward pair in the grid)
num_triangles_approx = int((width_m * height_m) / (0.5 * side_m * h))
# Centroids = pressure nodes
num_nodes = num_triangles_approx
return {
"vertices": len(vertices),
"triangles": num_triangles_approx,
"nodes": num_nodes,
"triangle_side_mm": triangle_side_mm,
"surface_area_m2": round(width_m * height_m, 2),
"wavelength_mm": round(WAVELENGTH * 1000, 1),
"rows": rows,
"cols": cols,
}
def calculate_bom(grid):
"""Generate the bill of materials with purchase links."""
n_transducers = grid["vertices"]
n_hv583 = math.ceil(n_transducers / 16)
n_esp32 = math.ceil(n_hv583 / 8)
n_driver_ics = math.ceil(n_transducers / 4)
n_pcb_tiles = math.ceil(n_transducers / 30)
n_psu = max(1, math.ceil(n_transducers / 600))
items = [
{"item": "Ultrasonic transducers (40kHz, 16mm)", "qty": n_transducers, "unit": 0.15,
"link": "https://www.amazon.com/s?k=40khz+ultrasonic+transducer+16mm",
"note": "Buy in bulk packs of 100+. TCT40-16T/R pairs."},
{"item": "HV583 shift registers (16-ch)", "qty": n_hv583, "unit": 7.00,
"link": "https://www.amazon.com/s?k=HV583+shift+register",
"note": "High-voltage 16-ch shift register. Alt: check Mouser/DigiKey if unavailable."},
{"item": "ESP32-S3 DevKit", "qty": n_esp32, "unit": 5.00,
"link": "https://www.amazon.com/s?k=ESP32-S3+development+board",
"note": "One ESP32 controls up to 8 shift registers (128 transducers)."},
{"item": "L293D motor driver IC", "qty": n_driver_ics, "unit": 0.05,
"link": "https://www.amazon.com/s?k=L293D+motor+driver+IC",
"note": "Quad half-H bridge. 4 transducers per chip. Buy bulk packs."},
{"item": "Prototype PCB boards (large)", "qty": n_pcb_tiles, "unit": 8.00,
"link": "https://www.amazon.com/s?k=prototype+PCB+board+large+double+sided",
"note": "~30 transducers per tile. Or order custom PCBs from JLCPCB ($2/board)."},
{"item": "48V DC power supply (10A)", "qty": n_psu, "unit": 60.00,
"link": "https://www.amazon.com/s?k=48V+10A+DC+power+supply+switching",
"note": "Switching PSU. One per ~600 transducers."},
{"item": "Table frame materials", "qty": 1, "unit": 150.00,
"link": "https://www.amazon.com/s?k=plywood+sheet+project+board",
"note": "3/4\" plywood base + aluminum angle brackets + acrylic top sheet."},
{"item": "Wiring & connectors kit", "qty": 1, "unit": 100.00,
"link": "https://www.amazon.com/s?k=22AWG+hookup+wire+kit+electronics+connectors",
"note": "22AWG solid core wire, JST connectors, standoffs, solder."},
]
for item in items:
item["total"] = round(item["qty"] * item["unit"], 2)
grand_total = sum(i["total"] for i in items)
return items, grand_total
def calculate_weight_capacity(grid):
"""How much can the surface support?"""
total_force_grams = grid["nodes"] * FORCE_PER_NODE_GRAMS
total_force_kg = total_force_grams / 1000
scenarios = [
{"object": "Skateboard deck", "weight_kg": 2.0},
{"object": "Small package", "weight_kg": 5.0},
{"object": "Heavy package", "weight_kg": 20.0},
{"object": "Small human (child)", "weight_kg": 30.0},
]
for s in scenarios:
force_per_node = (s["weight_kg"] * 1000) / grid["nodes"]
s["force_per_node_g"] = round(force_per_node, 2)
s["feasible"] = force_per_node <= FORCE_PER_NODE_GRAMS
return scenarios, total_force_kg
def calculate_phase_delay(node_x, node_y, speaker_x, speaker_y):
"""Calculate phase delay for a single speaker targeting a node."""
dist = math.sqrt((node_x - speaker_x)**2 + (node_y - speaker_y)**2)
phase = (dist % WAVELENGTH) / WAVELENGTH * 360
return round(phase, 1)
def generate_ai_insight(grid, bom_total, scenarios):
"""Let the AI add some flavor."""
if not nvidia_client:
return "AI insight requires NVIDIA_API_KEY."
feasible = [s for s in scenarios if s["feasible"]]
infeasible = [s for s in scenarios if not s["feasible"]]
prompt = f"""I just designed an acoustic frictionless surface. Here are the specs:
- Surface: {grid['surface_area_m2']} m2
- Triangles: {grid['triangles']} equilateral, {grid['triangle_side_mm']}mm sides
- Transducers: {grid['vertices']} at 40kHz ultrasonic
- Pressure nodes: {grid['nodes']}
- Total cost: ${bom_total:.0f}
- Can support: {', '.join(s['object'] for s in feasible) if feasible else 'nothing yet'}
- Cannot support: {', '.join(s['object'] for s in infeasible) if infeasible else 'everything works!'}
Give me a one-paragraph excited but technically grounded assessment.
Include one creative application idea nobody would think of.
Keep it under 100 words. Be fun."""
try:
response = nvidia_client.chat.completions.create(
model=NEMOTRON_MODEL,
messages=[
{"role": "system", "content": "You are an enthusiastic physics engineer who loves acoustic levitation. Be fun but accurate. Short responses."},
{"role": "user", "content": prompt}
],
max_tokens=200,
temperature=0.7,
)
return response.choices[0].message.content.strip()
except Exception as e:
return f"(AI thinking failed: {e})"
# --- Main Function ---
def design_hoverboard(width_ft, height_ft, triangle_side_mm):
"""The main calculation pipeline."""
width_m = width_ft * 0.3048
height_m = height_ft * 0.3048
grid = calculate_grid(width_m, height_m, triangle_side_mm)
bom, bom_total = calculate_bom(grid)
scenarios, total_force = calculate_weight_capacity(grid)
insight = generate_ai_insight(grid, bom_total, scenarios)
# Pull counts for build steps
n_pcb_tiles = math.ceil(grid["vertices"] / 30)
n_esp32 = math.ceil(math.ceil(grid["vertices"] / 16) / 8)
n_psu = max(1, math.ceil(grid["vertices"] / 600))
# Build the HTML output
html = f"""
<div class="hb-container">
<div class="hb-header">
<div class="hb-title">Your Hoverboard Design</div>
<div class="hb-subtitle">{width_ft}' x {height_ft}' surface &mdash; {grid['nodes']:,} pressure nodes</div>
</div>
<div class="hb-grid-viz">
<div class="hb-stat-row">
<div class="hb-stat">
<div class="hb-stat-num">{grid['vertices']:,}</div>
<div class="hb-stat-label">Transducers</div>
</div>
<div class="hb-stat">
<div class="hb-stat-num">{grid['triangles']:,}</div>
<div class="hb-stat-label">Triangles</div>
</div>
<div class="hb-stat">
<div class="hb-stat-num">{grid['nodes']:,}</div>
<div class="hb-stat-label">Pressure Nodes</div>
</div>
<div class="hb-stat">
<div class="hb-stat-num">{grid['wavelength_mm']}mm</div>
<div class="hb-stat-label">Wavelength</div>
</div>
</div>
</div>
<div class="hb-section">
<div class="hb-section-title">Weight Capacity</div>
<div class="hb-weight-grid">
{''.join(f"""
<div class="hb-weight-item {'hb-feasible' if s['feasible'] else 'hb-infeasible'}">
<div class="hb-weight-icon">{'&#9989;' if s['feasible'] else '&#10060;'}</div>
<div class="hb-weight-name">{s['object']}</div>
<div class="hb-weight-detail">{s['weight_kg']}kg &mdash; {s['force_per_node_g']}g/node</div>
</div>
""" for s in scenarios)}
</div>
<div class="hb-note">Published research: ~3g force per 40kHz node. Friction cancellation requires less than full levitation.</div>
</div>
<div class="hb-section">
<div class="hb-section-title">Bill of Materials &mdash; Order Parts</div>
<table class="hb-bom">
<tr><th>Component</th><th>Qty</th><th>Unit $</th><th>Total</th><th>Buy</th></tr>
{''.join(f'<tr><td>{i["item"]}<div class="hb-part-note">{i.get("note","")}</div></td><td>{i["qty"]:,}</td><td>${i["unit"]:.2f}</td><td>${i["total"]:.2f}</td><td><a href="{i.get("link","#")}" target="_blank" class="hb-buy-link">Amazon</a></td></tr>' for i in bom)}
<tr class="hb-bom-total"><td colspan="3">TOTAL</td><td>${bom_total:,.2f}</td><td></td></tr>
</table>
</div>
<div class="hb-section">
<div class="hb-section-title">Build Steps</div>
<div class="hb-build-steps">
<div class="hb-step">
<div class="hb-step-num">1</div>
<div class="hb-step-text">
<strong>Cut the base.</strong> Cut 3/4" plywood to {width_ft}' x {height_ft}'.
This is your table surface. Sand it flat.
</div>
</div>
<div class="hb-step">
<div class="hb-step-num">2</div>
<div class="hb-step-text">
<strong>Mark the triangle grid.</strong> Draw equilateral triangles with {grid['triangle_side_mm']}mm sides
across the entire surface. Each vertex = one transducer hole.
You need {grid['vertices']:,} holes. Use a template and a drill press with a 16mm bit.
</div>
</div>
<div class="hb-step">
<div class="hb-step-num">3</div>
<div class="hb-step-text">
<strong>Mount transducers.</strong> Press-fit or hot-glue one 40kHz transducer into each hole,
facing UP. All transducers should be flush with the surface.
</div>
</div>
<div class="hb-step">
<div class="hb-step-num">4</div>
<div class="hb-step-text">
<strong>Wire PCB tiles.</strong> Solder {grid['vertices']:,} transducers to {n_pcb_tiles} PCB tiles
(~30 per tile). Each tile connects to one L293D driver cluster and one HV583 shift register.
</div>
</div>
<div class="hb-step">
<div class="hb-step-num">5</div>
<div class="hb-step-text">
<strong>Connect controllers.</strong> Wire {n_esp32} ESP32(s) to the shift registers via SPI.
Each ESP32 handles up to 8 HV583 chips (128 transducers).
Flash with the phase array firmware (open source, link in references).
</div>
</div>
<div class="hb-step">
<div class="hb-step-num">6</div>
<div class="hb-step-text">
<strong>Power it up.</strong> Connect {n_psu} 48V PSU(s) to the driver boards.
Add a master power switch and a fuse (10A per PSU).
<em>Warning: 48V can hurt you. Respect the voltage.</em>
</div>
</div>
<div class="hb-step">
<div class="hb-step-num">7</div>
<div class="hb-step-text">
<strong>Top it off.</strong> Lay a thin acrylic sheet (1/8") over the transducer surface.
This is your friction-cancellation surface. The acoustic pressure nodes form
between the acrylic and anything sitting on it.
</div>
</div>
<div class="hb-step">
<div class="hb-step-num">8</div>
<div class="hb-step-text">
<strong>Test it.</strong> Power on. Place a lightweight object (playing card, small foam block)
on the surface. It should glide with near-zero friction. Adjust phase delays
in firmware to optimize node placement.
</div>
</div>
</div>
</div>
<div class="hb-section hb-ai">
<div class="hb-section-title">AI Assessment</div>
<div class="hb-insight">{insight}</div>
</div>
<div class="hb-footer">
"It's not a hoverboard. It's an acoustic air hockey table. And the grid is triangles, not holes."<br>
<span class="hb-dedication">If this ever makes money, the first check goes to the Michael J. Fox Foundation.</span>
</div>
</div>
"""
return html
# --- CSS --- 80s Nintendo / Excitebike / LEGO primary colors
CUSTOM_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap');
.hb-container {
background: #F0F0E8;
color: #222;
border-radius: 0;
border: 4px solid #222;
padding: 32px;
font-family: 'VT323', monospace;
max-width: 850px;
margin: 0 auto;
box-shadow: 8px 8px 0px #222;
}
.hb-header { text-align: center; margin-bottom: 28px; }
.hb-title { font-family: 'Press Start 2P', cursive; font-size: 1.4em; color: #E40000; letter-spacing: 0.05em; }
.hb-subtitle { font-size: 1.1em; color: #666; margin-top: 8px; font-family: 'VT323', monospace; }
.hb-stat-row { display: flex; justify-content: space-around; margin: 24px 0; gap: 12px; }
.hb-stat {
text-align: center; background: #FFD700; border: 4px solid #222;
padding: 24px 20px; box-shadow: 6px 6px 0px #222;
flex: 1; clip-path: polygon(50% 0%, 100% 25%, 100% 100%, 0% 100%, 0% 25%);
padding-top: 36px;
}
.hb-stat:nth-child(2) { background: #E40000; }
.hb-stat:nth-child(2) .hb-stat-num { color: #FFD700; }
.hb-stat:nth-child(2) .hb-stat-label { color: #fff; }
.hb-stat:nth-child(3) { background: #0050DD; }
.hb-stat:nth-child(3) .hb-stat-num { color: #FFD700; }
.hb-stat:nth-child(3) .hb-stat-label { color: #fff; }
.hb-stat:nth-child(4) { background: #00A800; }
.hb-stat:nth-child(4) .hb-stat-num { color: #fff; }
.hb-stat:nth-child(4) .hb-stat-label { color: #222; }
.hb-stat-num { font-family: 'Press Start 2P', cursive; font-size: 1.1em; color: #E40000; }
.hb-stat-label { font-size: 1.1em; color: #222; text-transform: uppercase; letter-spacing: 0.1em; font-family: 'VT323', monospace; margin-top: 6px; }
.hb-section { margin: 24px 0; }
.hb-section-title {
font-family: 'Press Start 2P', cursive; font-size: 0.7em; color: #0050DD;
text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 12px;
border-bottom: 3px solid #0050DD; padding-bottom: 6px;
}
.hb-weight-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.hb-weight-item { padding: 16px; border: 3px solid #222; background: #fff; box-shadow: 4px 4px 0px #222; }
.hb-feasible { border-left: 8px solid #00A800; background: #F0FFF0; }
.hb-infeasible { border-left: 8px solid #E40000; background: #FFF0F0; opacity: 0.8; }
.hb-weight-icon { font-size: 1.2em; }
.hb-weight-name { font-size: 1.1em; font-weight: bold; color: #222; font-family: 'VT323', monospace; }
.hb-weight-detail { font-size: 1em; color: #666; font-family: 'VT323', monospace; }
.hb-note { font-size: 0.9em; color: #888; margin-top: 8px; font-style: italic; }
.hb-bom { width: 100%; border-collapse: collapse; font-size: 1em; font-family: 'VT323', monospace; }
.hb-bom th { text-align: left; color: #fff; background: #0050DD; padding: 8px; font-size: 0.9em; }
.hb-bom td { padding: 6px 8px; border-bottom: 2px solid #ddd; }
.hb-bom tr:hover { background: #FFD700; }
.hb-bom-total { font-weight: bold; color: #00A800; }
.hb-bom-total td { border-top: 3px solid #E40000; padding-top: 8px; font-size: 1.2em; }
.hb-part-note { font-size: 0.85em; color: #888; margin-top: 2px; font-style: italic; }
.hb-buy-link {
color: #fff; text-decoration: none; font-weight: bold;
background: #E40000; border: 2px solid #222; padding: 3px 10px;
font-size: 0.9em; font-family: 'Press Start 2P', cursive; font-size: 0.6em;
box-shadow: 2px 2px 0px #222;
}
.hb-buy-link:hover { background: #00A800; }
.hb-build-steps { display: flex; flex-direction: column; gap: 16px; }
.hb-step {
display: flex; gap: 16px; align-items: flex-start;
background: #fff; border: 3px solid #222; padding: 18px;
box-shadow: 5px 5px 0px #222;
transform: translateX(0);
transition: transform 0.15s;
}
.hb-step:hover { transform: translateY(-4px); box-shadow: 5px 9px 0px #222; }
.hb-step:nth-child(odd) { border-left: 8px solid #0050DD; }
.hb-step:nth-child(even) { border-left: 8px solid #E40000; }
.hb-step-num {
background: #E40000; color: #fff; font-weight: bold;
font-family: 'Press Start 2P', cursive; font-size: 0.7em;
width: 40px; height: 40px; border: 3px solid #222;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
}
.hb-step:nth-child(even) .hb-step-num { background: #0050DD; }
.hb-step:nth-child(3) .hb-step-num { background: #FFD700; color: #222; }
.hb-step:nth-child(5) .hb-step-num { background: #00A800; }
.hb-step-text { font-size: 1.15em; line-height: 1.7; color: #333; font-family: 'VT323', monospace; }
.hb-step-text strong { color: #0050DD; }
.hb-step-text em { color: #E40000; }
.hb-ai { background: #FFD700; border: 3px solid #222; padding: 16px; box-shadow: 4px 4px 0px #222; }
.hb-insight { font-size: 1.1em; line-height: 1.6; color: #222; font-family: 'VT323', monospace; }
.hb-footer {
text-align: center; margin-top: 28px; padding-top: 16px;
border-top: 3px solid #222; font-size: 0.95em; color: #666;
font-family: 'VT323', monospace; line-height: 1.8;
}
.hb-dedication { color: #E40000; font-family: 'Press Start 2P', cursive; font-size: 0.6em; }
.gradio-container { background: #C8C8B8 !important; }
footer { display: none !important; }
button.primary {
background: #E40000 !important; color: #fff !important;
border: 3px solid #222 !important; font-family: 'Press Start 2P', cursive !important;
font-size: 0.8em !important; box-shadow: 4px 4px 0px #222 !important;
border-radius: 0 !important;
}
button.primary:hover { background: #00A800 !important; }
"""
# --- Gradio App ---
with gr.Blocks(css=CUSTOM_CSS, title="Hoverboard Calculator", theme=gr.themes.Base()) as app:
gr.HTML("""
<div style="text-align:center; padding: 20px 0;">
<h1 style="color: #E40000; font-family: 'Press Start 2P', cursive; font-size: 1.6em; letter-spacing: 0.05em; text-shadow: 3px 3px 0px #222;">
HOVERBOARD CALCULATOR
</h1>
<p style="color: #0050DD; font-family: 'VT323', monospace; font-size: 1.4em; margin-top: 8px;">
Because Michael J. Fox Lied To Me (But It's Cool)
</p>
<p style="color: #666; font-family: 'VT323', monospace; font-size: 1.1em; margin-top: 4px;">
Not levitation. Friction cancellation. The distinction matters.
</p>
</div>
""")
with gr.Row():
width = gr.Slider(minimum=1, maximum=12, value=6, step=0.5, label="Width (feet)")
height = gr.Slider(minimum=1, maximum=12, value=6, step=0.5, label="Height (feet)")
tri_size = gr.Slider(minimum=25, maximum=150, value=75, step=5, label="Triangle side (mm)")
calc_btn = gr.Button("CALCULATE PHASE ARRAY", variant="primary", size="lg")
output = gr.HTML()
calc_btn.click(
fn=design_hoverboard,
inputs=[width, height, tri_size],
outputs=[output],
)
gr.HTML("""
<div style="text-align:center; padding: 16px 0; color: #888; font-size: 1em; font-family: 'VT323', monospace;">
Inspired by Real Genius (1985), Weird Science (1985), and one guy who drew helicopters 400 years too early.<br>
<span style="color: #E40000; font-family: 'Press Start 2P', cursive; font-size: 0.6em;">Heuremen -- Build Small Hackathon 2026</span>
</div>
""")
if __name__ == "__main__":
app.launch()