Open Coaching Format — Open Standard for Basketball Drill Diagrams

1. Abstract

{ocf} v1.0.0 is an open, ruleset-aware JSON schema for representing basketball drill diagrams and multi-step animations. It defines a court coordinate system based on real-world units (meters or feet), a registry of named court positions per ruleset, a semantic entity model, and a frame-based animation structure.

The format is designed to be:

  • Human-readable — coaching terminology maps directly to named positions

  • LLM-friendly — structured enough for text-to-diagram generation

  • Renderer-agnostic — SVG, Canvas, PDF, mobile

  • Ruleset-aware — FIBA, NBA, NCAA, NFHS, custom

  • FIBA-compatible — import/export path for existing FIBA Europe diagrams


2. Introduction

2.1. Motivation

No open standard exists for basketball drill diagrams. All major tools (FIBA Europe Coaching App, FastDraw, Sportplan.net) use proprietary formats with image-only export. This prevents programmatic drill creation, LLM-assisted coaching tools, and cross-platform interoperability.

{ocf} addresses this gap by providing:

  1. A well-defined JSON Schema (Draft-7)

  2. A coordinate system grounded in real-world units

  3. A named position registry aligned with official ruleset geometry

  4. A frame model for multi-step animated drills

  5. A clear import path from the FIBA Europe internal format

2.2. Design Principles

Principle Decision

Court units = real units

Coordinates in meters (FIBA) or feet (NBA/NCAA). No arbitrary pixel grids.

Origin = midcourt center

Positive y = frontcourt (offense). Negative y = backcourt (defense). Zero = midline.

Viewer perspective

"Left" and "right" are always from the viewer’s top-down perspective.

Named positions over raw coordinates

Standard court landmarks (e.g. top_of_the_key) are first-class citizens.

Entity keys = team + number

offense_1, defense_3. No synthetic IDs for player entities.

Frames are anchored

Each frame carries explicit start_state/end_state anchors plus semantic actions. Any frame can render as a still without simulating history.

Colors are semantic roles

"offense", "defense", "black". Hex values are configured per document.

Registry over fixed enum

Named positions use a known-good standard catalog + user-extensible custom block.


3. Coordinate System

3.1. Overview

The {ocf} coordinate system places the origin at the center of the court (midcourt center circle).

                    viewer's left         viewer's right
                         |                     |
    defense basket  ─────┼─────────────────────┼─────  y = -max
    (backcourt)          │                     │
                         │    BACKCOURT        │
                         │    y < 0            │
                    ─────┼─────────────────────┼─────  y = 0  (midline / origin)
                    x=-max         x=0        x=+max
                         │    FRONTCOURT       │
                         │    y > 0            │
    offense basket  ─────┼─────────────────────┼─────  y = +max
    (frontcourt)         │                     │
Axis Definition

x

Horizontal. 0 = court center line. Negative = viewer’s left. Positive = viewer’s right.

y

Vertical. 0 = midline. Positive = toward offense basket (frontcourt). Negative = toward defense basket (backcourt).

3.2. Coordinate Ranges per Ruleset

Ruleset Unit x range y range (half / full)

fiba

m

−7.5 … +7.5

0 … +14.0 / −14.0 … +14.0

nba

ft

−25.0 … +25.0

0 … +47.0 / −47.0 … +47.0

ncaa

ft

−25.0 … +25.0

0 … +47.0 / −47.0 … +47.0

nfhs

ft

−25.0 … +25.0

0 … +42.0 / −42.0 … +42.0

For half court diagrams, only the frontcourt (y ≥ 0) is rendered. Entities placed at negative y coordinates in a half_court document are outside the visible area.

3.3. Field Dimensions Reference

FIBA Half Court — values in meters, origin at midline center
┌─────────────────────────────────────────────────────────┐  y = 0.0
│                         x=0                             │
│              midcourt.left  midcourt.center  midcourt.right
│           (-7.5,0)          (0,0)             (7.5,0)  │
│                                                         │
│                     TOP OF THE KEY                      │
│                      (0.0, 5.68)                        │
│       left_wing                          right_wing     │
│      (-6.75, 8.60)                    (6.75, 8.60)      │
│                                                         │
│   left_corner   ─ ─ FREE THROW LINE ─ ─  right_corner  │
│                        (0, 8.20)                        │
│  left_elbow                              right_elbow    │
│  (-2.45, 8.20)                          (2.45, 8.20)   │
│                 ┌───────────────────┐                   │
│                 │   PAINT AREA      │                   │
│   left_block    │                   │   right_block     │
│  (-2.45, 11.00) │   paint_center    │  (2.45, 11.00)   │
│                 │    (0, 10.50)     │                   │
│                 └───────────────────┘                   │
│  left_corner                            right_corner    │
│  (-7.5, 13.98)       basket          (7.5, 13.98)       │
│                     (0, 12.43)                          │
└─────────────────────────────────────────────────────────┘  y = +14.0

3.4. Coordinate Types

A coordinate in {ocf} can be one of three forms:

3.4.1. Absolute

{ "x": 2.45, "y": 8.20 }

A fixed point in court units. Always valid.

3.4.2. Named

{ "named": "left_elbow" }

References a standard named position. Resolved to absolute coordinates at render time using the document’s ruleset. Custom positions use the custom. prefix:

{ "named": "custom.dunker_spot" }

3.4.3. Relative

{
  "relative_to": "left_elbow",
  "dx": 1.0,
  "dy": -0.5
}

An offset from a named position. dx and dy are in the document’s court unit. Positive dx = viewer’s right. Positive dy = toward offense basket.


4. Named Court Positions

4.1. Standard Positions (FIBA)

All coordinates in meters (m). Backcourt positions are the exact y-negation of their frontcourt counterparts.

Name x (m) y (m) Description

Paint / Near Basket

basket

0.000

12.425

Center of basket ring

left_block

-2.450

11.000

Low post, left side

right_block

2.450

11.000

Low post, right side

paint_center

0.000

10.500

Center of the lane

left_short_corner

-7.500

11.500

Baseline region, outside paint left

right_short_corner

7.500

11.500

Baseline region, outside paint right

Mid-Range / Elbow

left_elbow

-2.450

8.200

Junction of free throw line and paint, left

right_elbow

2.450

8.200

Junction of free throw line and paint, right

free_throw_line

0.000

8.200

Center of free throw line

high_post_left

-2.450

7.000

Above the elbow, left side

high_post_right

2.450

7.000

Above the elbow, right side

3-Point Area

top_of_the_key

0.000

5.680

Apex of 3-point arc

left_wing

-6.750

8.600

3-point line, ~45° left

right_wing

6.750

8.600

3-point line, ~45° right

left_corner

-7.500

13.980

Corner 3, left

right_corner

7.500

13.980

Corner 3, right

Midcourt

midcourt.center

0.000

0.000

Center circle, origin

midcourt.left

-7.500

0.000

Left sideline at midline

midcourt.right

7.500

0.000

Right sideline at midline

Backcourt (Full Court)

backcourt.basket

0.000

-12.425

Defense basket

backcourt.left_block

-2.450

-11.000

Defense left block

backcourt.right_block

2.450

-11.000

Defense right block

backcourt.left_elbow

-2.450

-8.200

Defense left elbow

backcourt.right_elbow

2.450

-8.200

Defense right elbow

backcourt.free_throw_line

0.000

-8.200

Defense free throw line

backcourt.top_of_the_key

0.000

-5.680

Defense top of the key

backcourt.left_wing

-6.750

-8.600

Defense left wing

backcourt.right_wing

6.750

-8.600

Defense right wing

backcourt.left_corner

-7.500

-13.980

Defense left corner

backcourt.right_corner

7.500

-13.980

Defense right corner

Inbound Positions

inbound.baseline_left

-3.000

14.000

Baseline inbound, left of basket

inbound.baseline_right

3.000

14.000

Baseline inbound, right of basket

inbound.baseline_center

0.000

14.000

Baseline inbound, center

inbound.sideline_left_fc

-7.500

8.200

Sideline inbound, frontcourt left

inbound.sideline_right_fc

7.500

8.200

Sideline inbound, frontcourt right

inbound.sideline_left_mid

-7.500

0.000

Sideline inbound, at midline left

inbound.sideline_right_mid

7.500

0.000

Sideline inbound, at midline right

inbound.sideline_left_bc

-7.500

-8.200

Sideline inbound, backcourt left

inbound.sideline_right_bc

7.500

-8.200

Sideline inbound, backcourt right

4.2. Ruleset Differences

Key position differences across rulesets (all values in respective unit):

Position FIBA (m) NBA (ft) NCAA (ft) NFHS (ft)

top_of_the_key y

5.68

20.75

20.75

19.75

left_wing x

±6.75

±22.15

±20.75

±19.75

left_corner x

±7.50

±25.0

±25.0

±25.0

left_corner y

13.98

46.58

46.58

41.58

free_throw_line y

8.20

19.00

19.00

19.00

left_elbow x

±2.45

±8.0

±6.0

±6.0

NBA corners are defined by a straight sideline section, not an arc. The left_corner and right_corner positions mark the junction point where the straight section meets the arc.

4.3. Custom Positions (Registry)

User-defined named positions are stored in the named_positions.custom block:

"named_positions": {
  "custom": {
    "dunker_spot": {
      "x": 1.5,
      "y": 12.8,
      "description": "Dunker spot, weak side baseline"
    },
    "nail": {
      "x": 0.0,
      "y": 8.5,
      "description": "Free throw line extended, center"
    },
    "short_roll": {
      "x": -2.0,
      "y": 9.5
    }
  }
}

Reference them in coordinates with the custom. prefix:

{ "named": "custom.dunker_spot" }
Custom positions use the same unit as the document ruleset. No separate unit declaration is needed.

5. Entities

Entities represent all non-ball objects that appear on the court. Their initial position is defined in the top-level entities array. Balls are stored separately in the top-level balls array (see Balls).

5.1. Entity Reference Keys

Each entity is uniquely identified by a natural key string:

Entity Key Format

Offensive player nr. 1

"offense_1"

Defensive player nr. 3

"defense_3"

Coach

"coach"

Cone nr. 2

"cone_2"

Station nr. 4

"station_4"

offense_1 and defense_1 are distinct entities. In transition drills, both can appear simultaneously.

5.2. Distinct Positions

Two distinct entities MUST NOT occupy the same position. Within any single state (the top-level entities array, or a frame’s start_state / end_state), no two entities may share coordinates that resolve closer than a minimum separation to each other.

The minimum separation is one player diameter in the document’s court unit — the space a player symbol occupies — so that rendered symbols never overlap and each player remains individually identifiable:

Ruleset Minimum separation

fiba

0.5 m

nba / ncaa / nfhs

1.6 ft

A document that places two entities closer than this is invalid; a validator reports it. Two players genuinely "at the same spot" in a coach’s mind (e.g. a hand-off) are still distinct bodies and must be given distinct coordinates a symbol-width apart.

A renderer SHOULD additionally apply a small visual nudge if symbols still touch after a valid layout (a rendering safety net), but this does not relax the data constraint above — the underlying coordinates remain the source of truth.

5.3. Entity Types

5.3.1. Offensive Player

{
  "type": "offense",
  "nr": 1,
  "x": 0.0,
  "y": 5.68,
  "rotation": 0,
  "color": "offense"
}

Rendered as a filled circle with player number. rotation is accepted for symmetry but is not rendered for offense (the filled circle has no facing direction); it is typically 0. The field is meaningful for defenders — see the defender rotation convention below. color defaults to "offense" (resolved via color_scheme.offense_fill).

5.3.2. Defensive Player

{
  "type": "defense",
  "nr": 1,
  "x": 0.5,
  "y": 5.5,
  "rotation": 45,
  "color": "defense"
}

Rendered as a filled shape (an arc — the defender’s outstretched arms — above an inner circle carrying the player number). rotation indicates the direction the defender is facing, i.e. the direction the arms point.

Defender rotation convention

rotation is measured in degrees, clockwise on screen, from a defined zero:

  • (default) — arms point in the −y direction, i.e. away from the attacking basket. This is the natural defensive stance: the defender’s back is toward their own basket and they face an attacker coming from +y (the direction of attack).

  • Increasing values rotate the arms clockwise as seen on the rendered image: 90° → arms point −x (viewer’s left), 180°+y (toward the attacking basket), 270°+x (viewer’s right).

A renderer applies rotation exactly as given; it does not infer a facing direction from nearby players. When rotation is absent it defaults to (arms toward −y).

5.3.3. Balls

Balls are no longer entities. They live in the top-level balls array and each holds exactly one lifecycle state:

State Meaning

carried_by

An entity ref (player or coach) currently holding the ball.

at

A coordinate where the ball rests (floor, rim after a miss). Still in play — can be pickup or rebound.

dead

true. Out of play (made basket with no rebound, or out of bounds).

"balls": [
  { "id": "ball_1", "carried_by": "offense_1" },
  { "id": "ball_2", "carried_by": "coach" },
  { "id": "ball_3", "at": { "named": "right_corner" } }
]

Ball possession moves automatically: pass, shoot, pickup and rebound actions transfer the ball. After a shoot with no following rebound/pickup on the same ball, the ball becomes dead.

5.3.4. Coach

{
  "type": "coach",
  "x": -7.5,
  "y": 6.0
}

Rendered as a circle labeled "C".

5.3.5. Cone

{
  "type": "cone",
  "nr": 1,
  "x": 2.0,
  "y": 7.0
}

Rendered as a small triangle. Used as training markers or obstacles.

5.3.6. Station

{
  "type": "station",
  "nr": 3,
  "label": "Rebound jumps",
  "x": -4.0,
  "y": 11.0
}

Rendered as a numbered marker with optional description. Used for circuit training drills.


6. Actions

Movement and ball events are expressed as semantic actions, not geometric lines. A renderer derives the drawn arrows/paths from the actions. Every action has a required player and type; everything else is optional, so a coach can sketch quickly or annotate in full detail.

6.1. Action Types

Type Ball? Required Optional (variant enum / references)

move

no

moves[]

intensity; per-move intensity

cut

no

moves[]

variant (backdoor, give_and_go, flash, v_cut, l_cut, curl, flare, fade, basket); intensity; per-move around_player, off_screen_by, intensity

screen

no

for_player

on_player, at, variant (ball_screen, back_screen, down_screen, flare_screen, cross_screen, pin_down), physicality

defend

no

guards_player

variant (on_ball, deny, help, hedge, switch, box_out), physicality

dribble

yes

moves[]

ball_id, intensity; per-move variant (speed, hesitation, crossover, behind_back, between_legs, spin, retreat), intensity

pass

yes

to_player

ball_id, variant (chest, bounce, overhead, lob, baseball, hand_off, outlet), intensity

shoot

yes

player

ball_id, variant (jumper, three, layup, floater, dunk, hook, free_throw), result (make/miss), intensity

rebound

becomes carrier

player

ball_id, variant (offensive, defensive), physicality

pickup

becomes carrier

ball_id

physicality

Any action may also carry a free tags array (e.g. eurostep, reverse). Curated variant values are what a renderer can distinguish visually; most tags are open coaching metadata that need no schema change.

Curated tags: left_handed / right_handed (ball-handling side)

Two tags are curated because a renderer interprets them visually: on a ball-handling action (dribble, pass, shoot) the tags left_handed and right_handed state which hand handles the ball.

This drives where the ball is drawn relative to its carrier:

  • A carried ball is drawn slightly ahead of and to one side of the carrier — not on top of them — so the player’s number stays visible and the handling side is legible. "Ahead" is the direction of the ball-handling action (dribble / pass / shoot) in that frame.

  • Default (no tag, or right_handed): the ball is on the right of the direction of travel.

  • left_handed: the ball is on the left.

These tags remain ordinary entries in the free tags array (no schema change); they are simply the agreed vocabulary for ball side. A renderer that does not model the ball side may ignore them. This convention also lets a future animated renderer show a hand change (e.g. a crossover, or dribble→shoot) correctly.

A loose ball (at: a coordinate, not carried) is drawn on its coordinate and is not offset — the offset applies only to a carried ball.

6.2. Movement Sequences

move, cut and dribble carry a moves[] sequence. A move without to is a move on the spot (ball handling / fake); a move with to carries the player (and ball, for dribble) to a waypoint. The final to must match the player’s position in the frame end_state. Reference fields (around_player, off_screen_by) may sit on an individual move and override the action level.

For dribble (and move), each move’s variant describes that step’s character — a single dribble action can chain e.g. crossover then speed — whereas cut also has an action-level variant for the overall cut type (e.g. backdoor).

{
  "player": "offense_1",
  "type": "dribble",
  "ball_id": "ball_1",
  "moves": [
    { "variant": "crossover" },
    { "variant": "speed", "to": { "named": "basket" }, "around_player": "defense_1" }
  ]
}

6.3. Intensity & Physicality

Two optional, independent enums express relative execution style. Neither implies a concrete duration — a downstream application combines them with actual player/roster data to derive timing or pick renderer animations.

Field Applies to Enum

intensity (movement_intensity)

move, cut, dribble (action-level and per-move, move overrides action)

slow, normal (default), fast, explosive

intensity (ball_intensity)

pass, shoot

soft, normal (default), hard, bullet

physicality

screen, defend, rebound, pickup

passive, normal (default), aggressive, hard

intensity describes tempo over distance (how fast a player/ball moves). physicality describes contact/collision style and has no bearing on timing — it exists purely to help a renderer pick the right animation (e.g. a clean screen vs. a moving-screen-style rammed one). The two are deliberately separate so a distance→time calculation built on intensity never has to reason about physicality values.

{
  "player": "offense_1",
  "type": "cut",
  "intensity": "slow",
  "moves": [
    { "to": { "x": -2.0, "y": 4.5 } },
    { "to": { "named": "basket" }, "intensity": "explosive" }
  ]
}

duration_ms (see Frame Model) is deprecated in favor of intensity. It remains valid for backward compatibility with existing play files, but new play files should express tempo via intensity instead.

6.4. Timing

Actions express ordering semantically, not in seconds (omitted = at frame start):

Field Meaning

after

"<player>.<type>" — starts after the referenced action begins.

with

"<player>.<type>" — simultaneous with the referenced action.

on_catch

true — straight out of the catch (catch-and-shoot).


7. Frames

A frame is a coaching phase. It combines explicit anchors with semantic actions.

7.1. Frame Model

Field Required Meaning

id

yes

Unique frame id within the drill.

label

no

Short display label.

description

no

Coaching instruction. LLMs should generate this.

duration_ms

no

Deprecated — use per-action intensity instead. Suggested animation duration for the phase.

start_state

no

Positional + ball anchor at phase start. Defaults to the previous frame’s end_state.

actions

yes

Semantic actions in this phase. May be empty.

end_state

yes

Positional + ball anchor at phase end.

branches

no

Outcome → target frame id.

7.2. States

start_state and end_state map entity refs to coordinates, plus an optional balls map of ball ref → ball state. Because each frame carries an explicit anchor, any frame can be rendered as a still without simulating history — which is what enables PDF export, editor scrubbing and video overlay.

start_state is optional and inherits the previous frame’s end_state. Set it explicitly only at jumps, steals, rebounds or continuum resets.

"end_state": {
  "offense_1": { "x": -1.0, "y": 9.0 },
  "offense_4": { "named": "right_block" },
  "balls": { "ball_1": { "carried_by": "offense_4" } }
}

7.3. Branches

Without branches, the next frame in the array follows. With branches, the outcome selects the next frame. Outcomes are make, miss, turnover, steal, foul. A branch may point at an earlier frame to build a continuum loop. For a single set play, omit branches and let the final frame set the ball dead.

"branches": { "make": "frame_reset", "miss": "frame_oreb" }

8. Areas and Labels

8.1. Areas

Highlighted zones for marking training areas, defensive zones, or emphasis regions.

{
  "form": "rectangle",
  "color": "yellow",
  "opacity": 0.4,
  "x": 0.0,
  "y": 10.0,
  "width": 4.9,
  "height": 4.0
}
{
  "form": "ellipse",
  "color": "green",
  "opacity": 0.3,
  "x": 0.0,
  "y": 8.2,
  "width": 5.0,
  "height": 3.0
}
{
  "form": "triangle",
  "color": "red",
  "opacity": 0.25,
  "x": 0,
  "y": 0,
  "coords": [
    { "named": "left_block" },
    { "named": "right_block" },
    { "named": "basket" }
  ]
}

8.2. Labels

Free text annotations on the court:

{
  "text": "Pick & Roll",
  "x": -7.0,
  "y": 3.0,
  "color": "black"
}

9. Color Scheme

9.1. Design

Colors are defined as semantic roles, not as direct hex values in entities or lines. This allows editors to customize the appearance globally without modifying individual elements.

Role FIBA Default Usage

offense_fill

#003366

Offensive player symbol fill

offense_stroke

#ffffff

Offensive player symbol outline + number

defense_fill

#58001d

Defensive player symbol fill

defense_stroke

#ffffff

Defensive player symbol outline + number

black

#000000

Lines, cones, text (default)

grey

#7f7f7f

Subtle highlights, background areas

yellow

#ffff00

Highlighted zones

green

#7ce86a

Secondary highlights

red

#ff0000

Warning zones, alternative team color

blue

#5dd5ff

Alternative team color

white

#ffffff

Backgrounds, inverted labels

9.2. WCAG Accessibility

All color combinations used for rendered symbols must meet at minimum WCAG AA (4.5:1 contrast ratio). WCAG AAA (7:1) is recommended for printed materials.

Validators should emit a warning when color_scheme values produce contrast below 4.5:1 against the court background used by the target renderer.

FIBA default — contrast analysis:

Combination Foreground Background Ratio WCAG

Offense number on fill

#ffffff

#003366

12.6:1

✅ AAA

Defense number on fill

#ffffff

#58001d

11.2:1

✅ AAA

Black line on court

#000000

#0089cf

4.6:1

✅ AA

Offense fill on court

#003366

#0089cf

3.8:1

⚠️ Fail (use stroke)

Defense fill on court

#58001d

#0089cf

3.1:1

⚠️ Fail (use stroke)

Renderers should apply a 2px white stroke around player symbols to compensate for the low contrast between symbol fill and court background. This matches the FIBA SVG renderer behavior.

9.3. Overriding Colors

"color_scheme": {
  "offense_fill": "#1a4f8a",
  "offense_stroke": "#ffffff",
  "defense_fill": "#8a1a1a",
  "defense_stroke": "#ffffff"
}

Only fields that differ from the FIBA defaults need to be included.


10. Court Configuration

10.1. Supported Rulesets

ruleset unit Court (L × W) Notes

fiba

m

28.0 × 15.0

International standard

nba

ft

94.0 × 50.0

NBA-specific 3-point geometry (straight corners)

ncaa

ft

94.0 × 50.0

Shorter 3-point line than NBA

nfhs

ft

84.0 × 50.0

US high school, shorter court

custom

*

defined

Requires custom_dimensions block

10.2. Half Court vs Full Court

type Rendered region

half_court

y = 0 to +max. Offense basket at top. Midline forms the baseline.

full_court

y = -max to +max. Both baskets rendered.

10.3. Drill Focus

drill_focus Meaning

offense

Offensive drill. Positive y = direction of attack. Default.

defense

Defensive drill. Positive y = basket being defended.

transition

Full court drill with directional change. Implies full_court.

neutral

No directional focus (e.g. conditioning drill, station circuit).

10.4. Custom Ruleset

"court": {
  "ruleset": "custom",
  "type": "half_court",
  "drill_focus": "offense",
  "custom_dimensions": {
    "unit": "m",
    "length": 24.0,
    "width": 13.0,
    "basket_from_baseline": 1.575,
    "three_point_distance": 5.77,
    "paint_width": 3.6,
    "paint_depth": 5.8,
    "free_throw_distance": 5.8
  }
}

11. Ruleset Conversion

Converting a drill between rulesets is an explicit, lossless operation for named positions and a best-effort proportional mapping for free coordinates.

11.1. Named Position Conversion

Named positions are looked up independently in each ruleset’s coordinate table. The semantic meaning is preserved exactly.

"left_wing" in FIBA  →  (-6.75m, 8.60m)
"left_wing" in NBA   →  (-22.15ft, 28.5ft)   ← independent lookup, no math

11.2. Free Coordinate Conversion

Free absolute coordinates are converted proportionally:

x_target = x_source / (source.half_width)  * target.half_width
y_target = y_source / (source.half_depth)  * target.half_depth

Proportional conversion of free coordinates may place entities at positions that are semantically incorrect (e.g. inside or outside the 3-point line) relative to the target ruleset’s geometry.

Editors should surface a warning: "X free coordinates were proportionally scaled. Please verify their positions against the target ruleset."

11.3. FIBA Legacy Import

Existing FIBA Europe diagrams use a proprietary pixel coordinate system (384 × 540). Conversion to {ocf} FIBA coordinates:

x_m = (fiba_x - 192) / 192.0 * 7.5
y_m = (270 - fiba_y) / 270.0 * 14.0
The FIBA coordinate origin is at the top-left of the SVG canvas. The y-axis is inverted (y increases downward in FIBA, upward in {ocf}).

12. Complete Example

12.1. Pick & Roll — Half Court, FIBA

{
  "$schema": "https://opencoachingformat.org/schema/v1.json",
  "meta": {
    "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "title": "Basic Pick & Roll",
    "description": "Ball handler uses a ball screen at the top of the key. Screener rolls to the basket for the finish.",
    "author": "OCF Examples",
    "tags": ["pick-and-roll", "offense", "half-court", "2-player", "beginner"],
    "difficulty": "beginner",
    "created": "2025-02-27T10:00:00Z",
    "source_format": "open"
  },
  "court": { "ruleset": "fiba", "type": "half_court", "drill_focus": "offense" },
  "entities": [
    { "type": "offense", "nr": 1, "x": 0.0, "y": 5.68 },
    { "type": "offense", "nr": 4, "x": -2.45, "y": 8.20 },
    { "type": "defense", "nr": 1, "x": 0.5, "y": 5.20 },
    { "type": "defense", "nr": 4, "x": -2.80, "y": 7.80 }
  ],
  "balls": [ { "id": "ball_1", "carried_by": "offense_1" } ],
  "frames": [
    {
      "id": "frame_1",
      "label": "Step 1 — Screen",
      "description": "offense_4 moves up from the left elbow to set a ball screen for offense_1 at the top of the key.",
      "duration_ms": 1500,
      "actions": [
        { "player": "offense_4", "type": "screen", "for_player": "offense_1", "on_player": "defense_1", "variant": "ball_screen", "at": { "x": -0.5, "y": 6.7 } }
      ],
      "end_state": {
        "offense_1": { "named": "top_of_the_key" },
        "offense_4": { "x": -0.5, "y": 6.2 },
        "balls": { "ball_1": { "carried_by": "offense_1" } }
      }
    },
    {
      "id": "frame_2",
      "label": "Step 2 — Attack",
      "description": "offense_1 uses the screen and dribbles hard left toward the elbow; offense_4 rolls to the right block.",
      "duration_ms": 2000,
      "actions": [
        {
          "player": "offense_1",
          "type": "dribble",
          "ball_id": "ball_1",
          "moves": [
            { "variant": "hesitation" },
            { "variant": "speed", "to": { "named": "left_elbow" }, "around_player": "defense_1" }
          ]
        },
        { "player": "offense_4", "type": "cut", "variant": "basket", "moves": [ { "to": { "named": "right_block" } } ], "tags": ["roll"] }
      ],
      "end_state": {
        "offense_1": { "named": "left_elbow" },
        "offense_4": { "named": "right_block" },
        "balls": { "ball_1": { "carried_by": "offense_1" } }
      }
    },
    {
      "id": "frame_3",
      "label": "Step 3 — Finish",
      "description": "offense_1 throws a bounce pass to the rolling offense_4, who finishes with a layup.",
      "duration_ms": 1500,
      "actions": [
        { "player": "offense_1", "type": "pass", "to_player": "offense_4", "ball_id": "ball_1", "variant": "bounce" },
        { "player": "offense_4", "type": "shoot", "ball_id": "ball_1", "variant": "layup", "result": "make", "on_catch": true }
      ],
      "end_state": {
        "offense_1": { "named": "left_elbow" },
        "offense_4": { "named": "right_block" },
        "balls": { "ball_1": { "dead": true } }
      }
    }
  ],
  "areas": [
    { "form": "rectangle", "color": "yellow", "opacity": 0.2, "x": 0.0, "y": 10.5, "width": 4.9, "height": 4.0, "rotation": 0 }
  ],
  "labels": [ { "text": "Roll!", "x": 3.2, "y": 9.5, "color": "black" } ]
}

13. Schema Reference

13.1. Top-Level Structure

@startuml
skinparam backgroundColor white
skinparam defaultFontName Helvetica
skinparam classBackgroundColor #f8f8f8
skinparam classBorderColor #aaaaaa

class Drill {
  +meta: Meta
  +court: Court
  +entities: Entity[]
  +balls: Ball[]
  +frames: Frame[]
  --
  +color_scheme: ColorScheme?
  +named_positions: NamedPositions?
  +areas: Area[]?
  +labels: Label[]?
}

class Meta {
  +id: uuid
  +title: string
  --
  +description: string?
  +author: string?
  +tags: string[]?
  +difficulty: enum?
  +created: datetime?
  +source_format: enum?
}

class Court {
  +ruleset: enum
  +type: enum
  --
  +drill_focus: enum
  +wheelchair: boolean
  +custom_dimensions: CustomDimensions?
}

class Entity {
  +type: enum
  +x: number
  +y: number
  --
  +nr: integer?
  +rotation: number?
  +color: ColorRole?
  +label: string?
}

class Frame {
  +id: string
  +actions: Action[]
  +end_state: State
  --
  +label: string?
  +description: string?
  +duration_ms: integer?
  +start_state: State?
  +branches: map?
}

class Action {
  +player: EntityRef
  +type: enum
  --
  +ball_id: string?
  +moves: Move[]?
  +variant: string?
  +tags: string[]?
}

class Coordinate {
  <<oneOf>>
  FreeCoordinate
  NamedCoordinate
  RelativeCoordinate
}

Drill *-- Meta
Drill *-- Court
Drill *-- Entity
Drill *-- Frame
Frame *-- Action
Action *-- Coordinate
@enduml

13.1.1. Top-Level Fields

Field Required Description

meta

yes

Drill metadata (id, title, author, tags, etc.).

court

yes

Ruleset, court type, drill focus.

entities

yes

Non-ball entities: offense, defense, coach, cones, stations.

balls

yes

Array of balls; each ball has id + one of carried_by, at, or dead.

frames

yes

Ordered coaching phases; each carries actions and end_state.

color_scheme

no

Override FIBA default color roles.

named_positions

no

Custom named court positions.

areas

no

Highlighted zones on the court.

labels

no

Free text annotations on the court.

13.2. Coordinate oneOf

@startuml
skinparam backgroundColor white
skinparam defaultFontName Helvetica

abstract class Coordinate <<oneOf>>

class FreeCoordinate {
  +x: number
  +y: number
}

class NamedCoordinate {
  +named: string
}

class RelativeCoordinate {
  +relative_to: string
  +dx: number
  +dy: number
}

Coordinate <|-- FreeCoordinate
Coordinate <|-- NamedCoordinate
Coordinate <|-- RelativeCoordinate
@enduml

13.3. Enums Summary

Enum Values

ruleset

fiba, nba, ncaa, nfhs, custom

court.type

half_court, full_court

drill_focus

offense, defense, transition, neutral

difficulty

beginner, intermediate, advanced

source_format

fiba, fastdraw, open, custom

entity.type

offense, defense, coach, cone, station

area.form

rectangle, ellipse, triangle

color_role

offense, defense, black, grey, yellow, green, red, blue, white

unit

m, ft

Action type

move, cut, screen, defend, dribble, pass, shoot, rebound, pickup

Cut variant

backdoor, give_and_go, flash, v_cut, l_cut, curl, flare, fade, basket

Screen variant

ball_screen, back_screen, down_screen, flare_screen, cross_screen, pin_down

Defend variant

on_ball, deny, help, hedge, switch, box_out

Dribble per-move variant

speed, hesitation, crossover, behind_back, between_legs, spin, retreat

Pass variant

chest, bounce, overhead, lob, baseball, hand_off, outlet

Shoot variant

jumper, three, layup, floater, dunk, hook, free_throw

Rebound variant

offensive, defensive

Frame outcome

make, miss, turnover, steal, foul

movement_intensity (move/cut/dribble intensity)

slow, normal, fast, explosive

ball_intensity (pass/shoot intensity)

soft, normal, hard, bullet

physicality (screen/defend/rebound/pickup)

passive, normal, aggressive, hard

Per-move variant values are recommended vocabulary; the schema accepts any string for a move’s variant so renderers can fall back gracefully.

14. LLM Generation Guide

14.1. Prompt Pattern

{ocf} is designed so that LLMs can generate valid drill JSON from natural language descriptions. The key is the named position vocabulary — it grounds spatial references in a controlled set of terms.

Example prompt structure:

Generate an Open Coaching Format v1 JSON for the following drill:

Ruleset: FIBA, Half Court, Offense
Title: "3-Man Weave"

Description:
offense_1 starts at midcourt.center with the ball.
offense_2 starts at inbound.sideline_left_mid.
offense_3 starts at inbound.sideline_right_mid.

Step 1: offense_1 passes to offense_2, cuts behind toward the right sideline.
Step 2: offense_2 passes to offense_3 who has filled the center lane.
Step 3: offense_3 finishes at the basket.

Output only valid JSON matching https://opencoachingformat.org/schema/v1.json

14.2. LLM Output Tips

  • Actions express intent, not geometry — prefer named positions in end_state over raw coordinates wherever a standard name applies

  • Leave optional action fields out when the description is vague; add variant, ball_id, and timing only when the description implies them

  • Use relative_to for positions defined as offsets ("2 meters right of the elbow")

  • Every frame requires an end_state; list only the entities whose positions change in that frame (others inherit)

  • Ball state in end_state.balls must reflect the result of the actions: use carried_by after a pass/pickup, dead after a made shot with no rebound

  • Set the ball dead in end_state.balls after a shoot with result: "make" if no rebound or pickup follows in that frame or the next

  • Frame description should mirror the coaching instruction in natural language — LLMs should always generate this field

14.3. Validation

After LLM generation, validate against the JSON Schema:

npx ajv-cli validate \
  -s opencoachingformat-v1.schema.json \
  -d my_drill.json

Appendix A: FIBA Import Mapping

FIBA Field OCF Field Notes

id (GUID)

meta.id

GUID → UUID

fullCourt

court.type

truefull_court

wheelchair

court.wheelchair

Direct

dynamicElement.type

entity.type

See type map below

dynamicElement.nr

entity.nr

Direct

dynamicElement.color

entity.color

See color map below

dynamicElement.x

entity.x

(fiba_x - 192) / 192 * 7.5

dynamicElement.y

entity.y

(270 - fiba_y) / 270 * 14.0

lineElement.type

frame.actions[].type

See action type map below

lineElement.curved

— (derived from action type)

Renderer infers path style

lineElement.coords

frame.actions[].moves[].to

Coordinate conversion each point

staticElement (area)

areas[]

form, color, position

staticElement (text)

labels[]

text, position

A.1. FIBA Dynamic Element Type Map

FIBA type OCF entity.type

0 (offence)

offense

1 (defence)

defense

2 (ball)

— (map to balls[] entry with carried_by or at)

3 (coach)

coach

4 (cone)

cone

5 (shooting)

— (map to shoot action)

6 (handoff)

— (map to pass action with variant: "hand_off")

A.2. FIBA Line Type Map

FIBA type OCF action type

0 (movement)

move or cut

1 (passing)

pass

2 (dribbling)

dribble

3 (screen)

screen

4 (line)

— (annotate with tags)

5 (free)

— (annotate with tags)

A.3. FIBA Color Map

FIBA color FIBA name OCF color_role

-1

unknown

black

0

yellow

yellow

1

green

green

2

red

red

3

grey

grey

4

black

black

5

blue

blue

6

offence

offense

7

defence

defense

Appendix B: Changelog

B.1. v2.1.0

Adds optional intensity (movement_intensity for move/cut/dribble, ball_intensity for pass/shoot) and physicality (screen/defend/rebound/ pickup) enums. Deprecates frame-level duration_ms in favor of per-action intensity; duration_ms remains valid for backward compatibility.

B.2. v2.0.0

Semantic action model. Replaces line-based frames with per-player actions (move, cut, screen, defend, dribble, pass, shoot, rebound, pickup). The ball entity is replaced by a top-level balls array with a carried_by/at/dead lifecycle and automatic possession transfer. Frames gain explicit start_state/end_state anchors and outcome branches. The lines array and entity_states are removed.

B.3. v1.0.0 (2025-02-27)

  • Initial release

  • FIBA-compatible entity model

  • Normalized coordinate system (meters/feet, origin at midcourt)

  • Named position registry (FIBA, NBA, NCAA, NFHS)

  • Frame-based animation model with entity delta states

  • Registry approach for custom named positions

  • Semantic color roles with WCAG guidance

  • FIBA legacy import coordinate mapping