paddy's endzone

This commit is contained in:
Jan Novak
2026-01-17 13:21:16 +01:00
commit 164230356c
2 changed files with 527 additions and 0 deletions

View 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>