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>

25
drills/prompts.md Normal file
View File

@@ -0,0 +1,25 @@
Can you create ultimate frisbee drill animation for me?
```
Drill: Paddy's endzone
Players: 2 short vertical stacks in the endzone separated by slightly bigger space in the middle, players in the stack are close together, one at the back and other on the front each of them moving independently on the other, one player in front of the endzone, about 10-20 metres away and 10 metres from the sideline
Field:
- Field has lines as ultimate frisbee field would have
- make proportions of the endzone and rest of the field realistic
- Field takes up whole width of the canvas
- It is enough to have only half of the field in the picture
Setup: H1 at the disc, H2,H4,H6 in the back stack - starting with H2 closest to back endzone libe, H3, H5, H7 in the front stack - starting with H3 closest to player with disc, whole front stack is fully in the endzone
Extra details:
- when you are showing pass in the animation make it at least in 3 frames
- make default speed faster and faster even faster, possibly add 2 more speed options, faster speed should not skip frames, it just should move quicker from one to another
- make sure players do not cut out of bounds
- allow to go through animation one step at a time
Steps:
1. H2 cuts to the oposite side of H1 with cut parallel to the back line and H1 throws around forehand over both stacks and hits H2 about 3 metres off the sideline curving to the receiver
2. H1 moves into position closest to the front of end zone of the back stack, whole back stack moves so that player closest to the back of endzone is on the same position last player was on the start of the drill
3. H3 cuts out of the endzone to similar place where H1 was, but on the other side and gets pass from H2
4. H2 moves to the back of the front stack which moves in the way it now starts on the same spot as when we begun the drill
5. H4 cuts in the same way H2 cut in the step 1, only to the oposite side and H3 throws around backhand both stacks and hits H4 about 3 metres off the sideline curving to the receiver
7. H3 moves into position closest to the front of end zone of the back stack, whole back stack moves so that player closest to the back of endzone is on the same position last player was on the start of the drill
6. H5 cuts out of the endzone to place where H1 was, gets pass from H4, H4 moves to the back of the front stack
```