| name | football-throw-mechanics |
| description | Guide for implementing realistic football throw physics including impulse calculation, trajectory preview, and spin mechanics. Use this when working with ball throwing, trajectory calculation, pass physics, or spiral mechanics. |
Football Throw Mechanics
This skill provides guidance for implementing realistic football throw physics, including swipe-to-throw translation, trajectory calculation, spiral spin mechanics, and visual trajectory preview.
Physics Parameters
Regulation NFL Football
const FOOTBALL_CONFIG = {
radius: 0.143,
mass: 0.41,
restitution: 0.35,
friction: 0.7,
linearDamping: 0.12,
angularDamping: 0.18,
groundY: 0.143,
releaseHeight: 1.8,
maxThrowVelocity: 30,
minThrowVelocity: 10,
};
const GRAVITY = -9.81;
Swipe-to-Throw Translation
Convert touch swipe input to physics impulse:
interface SwipeData {
startX: number;
startY: number;
endX: number;
endY: number;
duration: number;
}
interface ThrowImpulse {
force: [number, number, number];
spin: [number, number, number];
power: number;
angle: number;
}
function calculateThrowImpulse(swipe: SwipeData): ThrowImpulse {
const dx = swipe.endX - swipe.startX;
const dy = swipe.endY - swipe.startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const velocity = distance / swipe.duration;
const power = Math.min(velocity * 40, 100);
const angle = Math.atan2(-dy, dx);
const throwVelocity = mapPowerToVelocity(power);
const horizontalAngle = angle - Math.PI / 2;
const horizontalDeviation = Math.sin(horizontalAngle) * throwVelocity * 0.3;
const verticalFactor = Math.max(0, -Math.sin(angle)) * 0.5 + 0.3;
const force: [number, number, number] = [
horizontalDeviation,
throwVelocity * verticalFactor,
-throwVelocity * Math.cos(angle * 0.5)
];
const spin = calculateSpiral(angle, velocity);
return {
force,
spin,
power,
angle
};
}
function mapPowerToVelocity(power: number): number {
const { minThrowVelocity, maxThrowVelocity } = FOOTBALL_CONFIG;
const normalizedPower = power / 100;
const t = normalizedPower * normalizedPower;
return minThrowVelocity + t * (maxThrowVelocity - minThrowVelocity);
}
Spiral Spin Mechanics
Generate realistic football spiral rotation:
function calculateSpiral(
angle: number,
swipeVelocity: number
): [number, number, number] {
const spiralSpeed = 15 + swipeVelocity * 5;
const wobble = swipeVelocity * 0.3;
return [
angle * spiralSpeed * 0.1,
0,
spiralSpeed
];
}
Trajectory Physics
Basic Projectile Motion
function simulateTrajectory(
initialPosition: [number, number, number],
initialVelocity: [number, number, number],
steps: number = 100,
dt: number = 0.016
): [number, number, number][] {
const trajectory: [number, number, number][] = [];
let pos: [number, number, number] = [...initialPosition];
let vel: [number, number, number] = [...initialVelocity];
for (let i = 0; i < steps; i++) {
trajectory.push([...pos]);
vel[1] += GRAVITY * dt;
vel[0] *= 1 - FOOTBALL_CONFIG.linearDamping * dt;
vel[1] *= 1 - FOOTBALL_CONFIG.linearDamping * dt * 0.5;
vel[2] *= 1 - FOOTBALL_CONFIG.linearDamping * dt;
pos[0] += vel[0] * dt;
pos[1] += vel[1] * dt;
pos[2] += vel[2] * dt;
if (pos[1] <= FOOTBALL_CONFIG.groundY) {
pos[1] = FOOTBALL_CONFIG.groundY;
trajectory.push([...pos]);
break;
}
}
return trajectory;
}
Advanced Trajectory with Magnus Effect
interface AdvancedTrajectoryParams {
position: [number, number, number];
velocity: [number, number, number];
angularVelocity: [number, number, number];
dt: number;
}
function advancedStep(params: AdvancedTrajectoryParams): AdvancedTrajectoryParams {
const { position, velocity, angularVelocity, dt } = params;
const pos = [...position] as [number, number, number];
const vel = [...velocity] as [number, number, number];
const angVel = [...angularVelocity] as [number, number, number];
vel[1] += GRAVITY * dt;
const speed = Math.sqrt(vel[0]**2 + vel[1]**2 + vel[2]**2);
const dragCoeff = 0.002;
if (speed > 0) {
const dragForce = dragCoeff * speed * speed;
vel[0] -= (vel[0] / speed) * dragForce * dt;
vel[1] -= (vel[1] / speed) * dragForce * dt;
vel[2] -= (vel[2] / speed) * dragForce * dt;
}
const magnusCoeff = 0.0001;
vel[0] += magnusCoeff * (angVel[1] * vel[2] - angVel[2] * vel[1]) * dt;
vel[1] += magnusCoeff * (angVel[2] * vel[0] - angVel[0] * vel[2]) * dt;
vel[2] += magnusCoeff * (angVel[0] * vel[1] - angVel[1] * vel[0]) * dt;
angVel[0] *= 1 - FOOTBALL_CONFIG.angularDamping * dt;
angVel[1] *= 1 - FOOTBALL_CONFIG.angularDamping * dt;
angVel[2] *= 1 - FOOTBALL_CONFIG.angularDamping * dt;
pos[0] += vel[0] * dt;
pos[1] += vel[1] * dt;
pos[2] += vel[2] * dt;
return {
position: pos,
velocity: vel,
angularVelocity: angVel,
dt
};
}
Trajectory Preview Visualization
Render a dashed line showing predicted ball path:
import * as THREE from 'three';
class TrajectoryPreview {
private line: THREE.Line;
private points: THREE.Vector3[] = [];
private geometry: THREE.BufferGeometry;
private material: THREE.LineDashedMaterial;
private maxPoints = 100;
constructor(scene: THREE.Scene) {
for (let i = 0; i < this.maxPoints; i++) {
this.points.push(new THREE.Vector3());
}
this.geometry = new THREE.BufferGeometry().setFromPoints(this.points);
this.material = new THREE.LineDashedMaterial({
color: 0xffff00,
dashSize: 0.3,
gapSize: 0.15,
transparent: true,
opacity: 0.7
});
this.line = new THREE.Line(this.geometry, this.material);
this.line.computeLineDistances();
this.line.visible = false;
scene.add(this.line);
}
update(
startPosition: [number, number, number],
velocity: [number, number, number]
) {
const trajectory = simulateTrajectory(startPosition, velocity, this.maxPoints);
const positions = this.geometry.attributes.position as THREE.BufferAttribute;
for (let i = 0; i < this.maxPoints; i++) {
if (i < trajectory.length) {
positions.setXYZ(i, trajectory[i][0], trajectory[i][1], trajectory[i][2]);
} else {
const last = trajectory[trajectory.length - 1];
positions.setXYZ(i, last[0], last[1], last[2]);
}
}
positions.needsUpdate = true;
this.geometry.computeBoundingSphere();
this.line.computeLineDistances();
}
show() {
this.line.visible = true;
}
hide() {
this.line.visible = false;
}
dispose() {
this.geometry.dispose();
this.material.dispose();
}
}
React Component for Trajectory Preview
"use client";
import { useRef, useEffect } from 'react';
import * as THREE from 'three';
import { useFrame, useThree } from '@react-three/fiber';
interface TrajectoryPreviewProps {
startPosition: [number, number, number];
angle: number;
power: number;
visible: boolean;
}
export function TrajectoryPreview({
startPosition,
angle,
power,
visible
}: TrajectoryPreviewProps) {
const lineRef = useRef<THREE.Line>(null);
const pointsRef = useRef<THREE.Vector3[]>([]);
useEffect(() => {
pointsRef.current = Array(100).fill(null).map(() => new THREE.Vector3());
}, []);
useFrame(() => {
if (!visible || !lineRef.current) return;
const throwVelocity = 10 + (power / 100) * 20;
const velocity: [number, number, number] = [
Math.sin(angle) * throwVelocity * 0.3,
throwVelocity * 0.4,
-throwVelocity * Math.cos(angle * 0.5)
];
const trajectory = simulateTrajectory(startPosition, velocity);
const positions = lineRef.current.geometry.attributes.position as THREE.BufferAttribute;
trajectory.forEach((pos, i) => {
positions.setXYZ(i, pos[0], pos[1], pos[2]);
});
positions.needsUpdate = true;
});
if (!visible) return null;
return (
<line ref={lineRef}>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
count={100}
array={new Float32Array(300)}
itemSize={3}
/>
</bufferGeometry>
<lineDashedMaterial
color="#ffff00"
dashSize={0.3}
gapSize={0.15}
transparent
opacity={0.7}
/>
</line>
);
}
Target Hit Detection
interface Target {
id: string;
position: [number, number, number];
radius: number;
}
function checkTargetHit(
ballPosition: [number, number, number],
targets: Target[]
): Target | null {
for (const target of targets) {
const dx = ballPosition[0] - target.position[0];
const dy = ballPosition[1] - target.position[1];
const dz = ballPosition[2] - target.position[2];
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (distance <= target.radius + FOOTBALL_CONFIG.radius) {
return target;
}
}
return null;
}
Scoring System
interface ThrowScore {
accuracy: number;
power: number;
spiral: number;
distance: number;
total: number;
}
function calculateThrowScore(
throwData: ThrowImpulse,
targetHit: Target | null,
impactPosition: [number, number, number]
): ThrowScore {
let accuracy = 0;
let power = 0;
let spiral = 0;
let distance = 0;
if (targetHit) {
const dx = impactPosition[0] - targetHit.position[0];
const dy = impactPosition[1] - targetHit.position[1];
const dz = impactPosition[2] - targetHit.position[2];
const distFromCenter = Math.sqrt(dx * dx + dy * dy + dz * dz);
accuracy = Math.max(0, 100 * (1 - distFromCenter / targetHit.radius));
distance = Math.min(50, targetHit.position[2] * -1);
const optimalPower = throwData.power >= 50 && throwData.power <= 80;
power = optimalPower ? 20 : 10;
const spiralQuality = Math.abs(throwData.spin[2]) / 20;
spiral = Math.min(30, spiralQuality * 30);
}
return {
accuracy,
power,
spiral,
distance,
total: Math.round(accuracy + power + spiral + distance)
};
}
Complete Throw Handler
function handleThrow(
swipeData: SwipeData,
playerPosition: [number, number, number],
targets: Target[],
onComplete: (result: { hit: boolean; target?: Target; score: ThrowScore }) => void
) {
const impulse = calculateThrowImpulse(swipeData);
const startPos: [number, number, number] = [
playerPosition[0],
FOOTBALL_CONFIG.releaseHeight,
playerPosition[2]
];
physicsWorker.postMessage({
type: 'THROW',
data: {
position: startPos,
force: impulse.force,
spin: impulse.spin
}
});
const checkInterval = setInterval(() => {
const ballPos = getCurrentBallPosition();
const hitTarget = checkTargetHit(ballPos, targets);
if (hitTarget) {
clearInterval(checkInterval);
const score = calculateThrowScore(impulse, hitTarget, ballPos);
onComplete({ hit: true, target: hitTarget, score });
return;
}
if (ballPos[1] <= FOOTBALL_CONFIG.groundY + 0.1 && !isBallActive()) {
clearInterval(checkInterval);
const score = calculateThrowScore(impulse, null, ballPos);
onComplete({ hit: false, score });
}
}, 16);
}
References