paddy's endzone
This commit is contained in:
502
drills/html/paddys-endzone.html
Normal file
502
drills/html/paddys-endzone.html
Normal file
@@ -0,0 +1,502 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Paddy's Endzone Drill Animation</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #1a1a1a;
|
||||
--panel-color: #2c2c2c;
|
||||
--accent-color: #4CAF50;
|
||||
--text-color: #ffffff;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 650px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
canvas {
|
||||
background-color: #2e7d32;
|
||||
border: 3px solid #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.controls {
|
||||
background-color: var(--panel-color);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 18px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: #444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button:hover { background: #555; }
|
||||
button:active { transform: translateY(1px); }
|
||||
|
||||
button.primary { background: var(--accent-color); }
|
||||
button.primary:hover { background: #45a049; }
|
||||
|
||||
.speed-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: #444;
|
||||
color: white;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
.info {
|
||||
text-align: center;
|
||||
min-height: 3rem;
|
||||
}
|
||||
|
||||
#stepDescription {
|
||||
font-style: italic;
|
||||
color: #ccc;
|
||||
margin-top: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.legend {
|
||||
font-size: 0.8rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<h2 style="margin: 0; text-align: center;">Paddy's Endzone Drill</h2>
|
||||
|
||||
<div class="info">
|
||||
<strong id="stepTitle">Ready to start</strong>
|
||||
<span id="stepDescription">Click Play or Next Step</span>
|
||||
</div>
|
||||
|
||||
<canvas id="drillCanvas" width="600" height="700"></canvas>
|
||||
|
||||
<div class="controls">
|
||||
<div class="button-row">
|
||||
<button id="prevBtn">Prev Step</button>
|
||||
<button id="playBtn" class="primary">Play</button>
|
||||
<button id="nextBtn">Next Step</button>
|
||||
<button id="resetBtn">Reset</button>
|
||||
</div>
|
||||
|
||||
<div class="speed-row">
|
||||
<label for="speedSelect">Animation Speed:</label>
|
||||
<select id="speedSelect">
|
||||
<option value="1">Normal</option>
|
||||
<option value="2" selected>Faster</option>
|
||||
<option value="4">High Speed</option>
|
||||
<option value="8">Insane (No Skips)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<span>🟡 Disc</span>
|
||||
<span>⚪ Player</span>
|
||||
<span>🟢 Endzone</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* FIELD PROPORTIONS (1m = 16.2px)
|
||||
* Realistic Width: 37m = 600px
|
||||
* Endzone Depth: 18m = 292px
|
||||
* Total view: ~43m height
|
||||
*/
|
||||
|
||||
const canvas = document.getElementById('drillCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Constants
|
||||
const W = 600;
|
||||
const SCALE = W / 37; // px per meter
|
||||
const EZ_DEPTH = 18 * SCALE;
|
||||
const BACK_LINE = 50; // top padding
|
||||
const GOAL_LINE = BACK_LINE + EZ_DEPTH;
|
||||
const SIDELINE_3M = 3 * SCALE;
|
||||
const MID_X = W / 2;
|
||||
|
||||
// Colors
|
||||
const COLORS = {
|
||||
disc: '#ffeb3b',
|
||||
player: '#ffffff',
|
||||
playerText: '#000000',
|
||||
goalHandler: '#ff9800'
|
||||
};
|
||||
|
||||
// Initial Player Data
|
||||
// Back stack: H2(closest back), H4, H6
|
||||
// Front stack: H3(closest handler), H5, H7
|
||||
const defaultBackY = [BACK_LINE + 15, BACK_LINE + 55, BACK_LINE + 95];
|
||||
const defaultFrontY = [GOAL_LINE - 95, GOAL_LINE - 55, GOAL_LINE - 15];
|
||||
|
||||
let players = {
|
||||
H1: { x: 10 * SCALE, y: GOAL_LINE + (15 * SCALE), label: 'H1' },
|
||||
H2: { x: MID_X, y: defaultBackY[0], label: 'H2' },
|
||||
H4: { x: MID_X, y: defaultBackY[1], label: 'H4' },
|
||||
H6: { x: MID_X, y: defaultBackY[2], label: 'H6' },
|
||||
H3: { x: MID_X, y: defaultFrontY[2], label: 'H3' },
|
||||
H5: { x: MID_X, y: defaultFrontY[1], label: 'H5' },
|
||||
H7: { x: MID_X, y: defaultFrontY[0], label: 'H7' }
|
||||
};
|
||||
|
||||
let disc = { x: players.H1.x, y: players.H1.y, attached: 'H1' };
|
||||
|
||||
let currentStep = 0;
|
||||
let progress = 0;
|
||||
let isPlaying = false;
|
||||
let lastTimestamp = 0;
|
||||
|
||||
// Drill Step Definitions
|
||||
const steps = [
|
||||
{
|
||||
title: "Step 1: The Initial Look",
|
||||
desc: "H2 cuts across. H1 throws a curved around-forehand.",
|
||||
duration: 1.5,
|
||||
animate: (t) => {
|
||||
// H2 cut parallel to backline
|
||||
players.H2.x = lerp(MID_X, W - SIDELINE_3M, t);
|
||||
|
||||
// Pass flight (Curved)
|
||||
if (t > 0.1) {
|
||||
const pt = (t - 0.1) / 0.9;
|
||||
disc.attached = null;
|
||||
const curve = Math.sin(pt * Math.PI) * 120; // Around curve
|
||||
disc.x = lerp(players.H1.x, W - SIDELINE_3M, pt) + curve;
|
||||
disc.y = lerp(players.H1.y, defaultBackY[0], pt);
|
||||
}
|
||||
},
|
||||
onEnd: () => { disc.attached = 'H2'; }
|
||||
},
|
||||
{
|
||||
title: "Step 2: Handler Rotation",
|
||||
desc: "H1 joins back stack. Stack shifts to maintain original backline depth.",
|
||||
duration: 1.2,
|
||||
animate: (t) => {
|
||||
// H1 moves to front of back stack
|
||||
players.H1.x = lerp(10 * SCALE, MID_X, t);
|
||||
players.H1.y = lerp(GOAL_LINE + (15 * SCALE), defaultBackY[2], t);
|
||||
// Shift others
|
||||
players.H4.y = lerp(defaultBackY[1], defaultBackY[0], t);
|
||||
players.H6.y = lerp(defaultBackY[2], defaultBackY[1], t);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Step 3: Reset Look",
|
||||
desc: "H3 cuts out to opposite handler spot. Gets pass from H2.",
|
||||
duration: 1.3,
|
||||
animate: (t) => {
|
||||
const targetX = W - (10 * SCALE);
|
||||
const targetY = GOAL_LINE + (15 * SCALE);
|
||||
players.H3.x = lerp(MID_X, targetX, t);
|
||||
players.H3.y = lerp(defaultFrontY[2], targetY, t);
|
||||
|
||||
if (t > 0.1) {
|
||||
const pt = (t - 0.1) / 0.9;
|
||||
disc.attached = null;
|
||||
disc.x = lerp(W - SIDELINE_3M, targetX, pt);
|
||||
disc.y = lerp(defaultBackY[0], targetY, pt);
|
||||
}
|
||||
},
|
||||
onEnd: () => { disc.attached = 'H3'; }
|
||||
},
|
||||
{
|
||||
title: "Step 4: Front Stack Reset",
|
||||
desc: "H2 moves to back of front stack. Stack shifts forward.",
|
||||
duration: 1.2,
|
||||
animate: (t) => {
|
||||
players.H2.x = lerp(W - SIDELINE_3M, MID_X, t);
|
||||
players.H2.y = lerp(defaultBackY[0], defaultFrontY[0], t);
|
||||
// Shift front stack to fill
|
||||
players.H5.y = lerp(defaultFrontY[1], defaultFrontY[2], t);
|
||||
players.H7.y = lerp(defaultFrontY[0], defaultFrontY[1], t);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Step 5: Breakside Look",
|
||||
desc: "H4 cuts opposite. H3 throws curved around-backhand.",
|
||||
duration: 1.5,
|
||||
animate: (t) => {
|
||||
const h3X = W - (10 * SCALE);
|
||||
const h3Y = GOAL_LINE + (15 * SCALE);
|
||||
|
||||
players.H4.x = lerp(MID_X, SIDELINE_3M, t);
|
||||
|
||||
if (t > 0.1) {
|
||||
const pt = (t - 0.1) / 0.9;
|
||||
disc.attached = null;
|
||||
const curve = Math.sin(pt * Math.PI) * -120;
|
||||
disc.x = lerp(h3X, SIDELINE_3M, pt) + curve;
|
||||
disc.y = lerp(h3Y, defaultBackY[0], pt);
|
||||
}
|
||||
},
|
||||
onEnd: () => { disc.attached = 'H4'; }
|
||||
},
|
||||
{
|
||||
title: "Step 6: Secondary Handler Rotation",
|
||||
desc: "H3 joins back stack. Back stack shifts.",
|
||||
duration: 1.2,
|
||||
animate: (t) => {
|
||||
const h3X = W - (10 * SCALE);
|
||||
const h3Y = GOAL_LINE + (15 * SCALE);
|
||||
players.H3.x = lerp(h3X, MID_X, t);
|
||||
players.H3.y = lerp(h3Y, defaultBackY[2], t);
|
||||
// Stack shift
|
||||
players.H1.y = lerp(defaultBackY[2], defaultBackY[1], t);
|
||||
players.H6.y = lerp(defaultBackY[1], defaultBackY[0], t);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Step 7: Final Reset",
|
||||
desc: "H5 cuts out. H4 joins front stack. H5 receives pass.",
|
||||
duration: 1.5,
|
||||
animate: (t) => {
|
||||
// H5 cut
|
||||
players.H5.x = lerp(MID_X, 10 * SCALE, t);
|
||||
players.H5.y = lerp(defaultFrontY[2], GOAL_LINE + (15 * SCALE), t);
|
||||
// H4 reset
|
||||
players.H4.x = lerp(SIDELINE_3M, MID_X, t);
|
||||
players.H4.y = lerp(defaultBackY[0], defaultFrontY[0], t);
|
||||
// Stack reset
|
||||
players.H7.y = lerp(defaultFrontY[1], defaultFrontY[2], t);
|
||||
players.H2.y = lerp(defaultFrontY[0], defaultFrontY[1], t);
|
||||
|
||||
if (t > 0.1) {
|
||||
const pt = (t - 0.1) / 0.9;
|
||||
disc.attached = null;
|
||||
disc.x = lerp(SIDELINE_3M, 10 * SCALE, pt);
|
||||
disc.y = lerp(defaultBackY[0], GOAL_LINE + (15 * SCALE), pt);
|
||||
}
|
||||
},
|
||||
onEnd: () => { disc.attached = 'H5'; }
|
||||
}
|
||||
];
|
||||
|
||||
// Helper Functions
|
||||
function lerp(start, end, t) {
|
||||
return start * (1 - t) + end * t;
|
||||
}
|
||||
|
||||
function drawField() {
|
||||
ctx.clearRect(0, 0, W, canvas.height);
|
||||
|
||||
// Goal and back lines
|
||||
ctx.strokeStyle = "white";
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
// Sidelines
|
||||
ctx.strokeRect(0, -10, W, canvas.height + 20);
|
||||
|
||||
// Goal line
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, GOAL_LINE); ctx.lineTo(W, GOAL_LINE);
|
||||
ctx.stroke();
|
||||
|
||||
// Back line
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, BACK_LINE); ctx.lineTo(W, BACK_LINE);
|
||||
ctx.stroke();
|
||||
|
||||
// Shaded Endzone
|
||||
ctx.fillStyle = "rgba(255, 255, 255, 0.05)";
|
||||
ctx.fillRect(0, BACK_LINE, W, EZ_DEPTH);
|
||||
|
||||
// Players
|
||||
for (const key in players) {
|
||||
const p = players[key];
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, 10, 0, Math.PI * 2);
|
||||
ctx.fillStyle = COLORS.player;
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "#000";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = COLORS.playerText;
|
||||
ctx.font = "bold 10px Arial";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(p.label, p.x, p.y + 4);
|
||||
}
|
||||
|
||||
// Disc
|
||||
const dX = disc.attached ? players[disc.attached].x + 8 : disc.x;
|
||||
const dY = disc.attached ? players[disc.attached].y - 8 : disc.y;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(dX, dY, 5, 0, Math.PI * 2);
|
||||
ctx.fillStyle = COLORS.disc;
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "orange";
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function update(timestamp) {
|
||||
if (!lastTimestamp) lastTimestamp = timestamp;
|
||||
const delta = (timestamp - lastTimestamp) / 1000;
|
||||
lastTimestamp = timestamp;
|
||||
|
||||
if (isPlaying) {
|
||||
const speedMultiplier = parseFloat(document.getElementById('speedSelect').value);
|
||||
const step = steps[currentStep];
|
||||
|
||||
progress += (delta / step.duration) * speedMultiplier;
|
||||
|
||||
if (progress >= 1) {
|
||||
progress = 1;
|
||||
step.animate(1);
|
||||
if (step.onEnd) step.onEnd();
|
||||
|
||||
if (currentStep < steps.length - 1) {
|
||||
currentStep++;
|
||||
progress = 0;
|
||||
} else {
|
||||
isPlaying = false;
|
||||
document.getElementById('playBtn').textContent = "Restart";
|
||||
}
|
||||
} else {
|
||||
step.animate(progress);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
}
|
||||
|
||||
drawField();
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
const step = steps[currentStep];
|
||||
document.getElementById('stepTitle').textContent = step.title;
|
||||
document.getElementById('stepDescription').textContent = step.desc;
|
||||
}
|
||||
|
||||
function resetDrill() {
|
||||
isPlaying = false;
|
||||
currentStep = 0;
|
||||
progress = 0;
|
||||
|
||||
// Deep clone initial state
|
||||
players.H1 = { x: 10 * SCALE, y: GOAL_LINE + (15 * SCALE), label: 'H1' };
|
||||
players.H2 = { x: MID_X, y: defaultBackY[0], label: 'H2' };
|
||||
players.H4 = { x: MID_X, y: defaultBackY[1], label: 'H4' };
|
||||
players.H6 = { x: MID_X, y: defaultBackY[2], label: 'H6' };
|
||||
players.H3 = { x: MID_X, y: defaultFrontY[2], label: 'H3' };
|
||||
players.H5 = { x: MID_X, y: defaultFrontY[1], label: 'H5' };
|
||||
players.H7 = { x: MID_X, y: defaultFrontY[0], label: 'H7' };
|
||||
|
||||
disc = { x: players.H1.x, y: players.H1.y, attached: 'H1' };
|
||||
|
||||
document.getElementById('playBtn').textContent = "Play";
|
||||
updateUI();
|
||||
}
|
||||
|
||||
// Control Listeners
|
||||
document.getElementById('playBtn').addEventListener('click', () => {
|
||||
if (currentStep === steps.length - 1 && progress === 1) {
|
||||
resetDrill();
|
||||
}
|
||||
isPlaying = !isPlaying;
|
||||
document.getElementById('playBtn').textContent = isPlaying ? "Pause" : "Play";
|
||||
});
|
||||
|
||||
document.getElementById('nextBtn').addEventListener('click', () => {
|
||||
isPlaying = false;
|
||||
document.getElementById('playBtn').textContent = "Play";
|
||||
|
||||
// If mid-progress, finish current step
|
||||
if (progress < 1) {
|
||||
progress = 1;
|
||||
steps[currentStep].animate(1);
|
||||
if (steps[currentStep].onEnd) steps[currentStep].onEnd();
|
||||
} else if (currentStep < steps.length - 1) {
|
||||
currentStep++;
|
||||
progress = 1;
|
||||
steps[currentStep].animate(1);
|
||||
if (steps[currentStep].onEnd) steps[currentStep].onEnd();
|
||||
}
|
||||
updateUI();
|
||||
});
|
||||
|
||||
document.getElementById('prevBtn').addEventListener('click', () => {
|
||||
isPlaying = false;
|
||||
document.getElementById('playBtn').textContent = "Play";
|
||||
|
||||
if (currentStep > 0) {
|
||||
currentStep--;
|
||||
progress = 1;
|
||||
// Logic to rewind visual state is complex in simple lerp engines
|
||||
// Best practice for this simple engine is to reset and fast-forward
|
||||
const targetStep = currentStep;
|
||||
resetDrill();
|
||||
for(let i=0; i <= targetStep; i++) {
|
||||
currentStep = i;
|
||||
progress = 1;
|
||||
steps[i].animate(1);
|
||||
if(steps[i].onEnd) steps[i].onEnd();
|
||||
}
|
||||
} else {
|
||||
resetDrill();
|
||||
}
|
||||
updateUI();
|
||||
});
|
||||
|
||||
document.getElementById('resetBtn').addEventListener('click', resetDrill);
|
||||
|
||||
// Start
|
||||
requestAnimationFrame(update);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user