# GIF Desk > A GIF stores each frame's delay as a whole number of centiseconds, so the only > frame rates it has are 100/n. 24, 30 and 60 are all absent. This works out what > your clip actually plays at, how far the duration drifts, and the repeating > pattern of delays that would average to the rate you wanted exactly. ## The one thing to know ```text delay_cs = round(100 / fps) actual_fps = 100 / delay_cs ``` Not milliseconds, not a rational, not a rate in a header: a whole number of hundredths of a second, per frame, and nothing else. | Delay | Rate | Reachable? | | --- | --- | --- | | 1 tick (10 ms) | 100 fps | **no — every browser replaces it with 10 ticks** | | 2 ticks (20 ms) | 50 fps | yes | | 3 ticks (30 ms) | 33.33 fps | yes | | 4 ticks (40 ms) | 25 fps | yes | | 5 ticks (50 ms) | 20 fps | yes | | 6 ticks (60 ms) | 16.67 fps | yes | | 7 ticks (70 ms) | 14.29 fps | yes | | 8 ticks (80 ms) | 12.50 fps | yes | | 9 ticks (90 ms) | 11.11 fps | yes | | 10 ticks (100 ms) | 10 fps | yes | These are all of them. There is nothing between the rows. ## What the common video rates become | Asked for | Exists? | Single delay | Plays at | Error | Mix that averages to it | | --- | --- | --- | --- | --- | --- | | 60 fps | **no** | 2 ticks | 50 fps | -16.7% | **none — the pattern needs a clamped delay** | | 50 fps | yes | 2 ticks | 50 fps | +0.0% | not needed | | 30 fps | **no** | 3 ticks | 33.33 fps | +11.1% | `[3, 3, 4]` over 3 | | 25 fps | yes | 4 ticks | 25 fps | +0.0% | not needed | | 24 fps | **no** | 4 ticks | 25 fps | +4.2% | `[4, 4, 4, 4, 4, 5]` over 6 | | 20 fps | yes | 5 ticks | 20 fps | +0.0% | not needed | | 15 fps | **no** | 7 ticks | 14.29 fps | -4.8% | `[6, 7, 7]` over 3 | | 12.50 fps | yes | 8 ticks | 12.50 fps | +0.0% | not needed | | 10 fps | yes | 10 ticks | 10 fps | +0.0% | not needed | **24, 30 and 60 — the three commonest frame rates in video — are all absent.** ## The fix almost nobody uses Nothing requires every frame to carry the same delay. If a repeating pattern of delays sums to a whole number of ticks, the **average** rate is exactly what you asked for, even though no single delay is: - **30 fps** → `[3, 3, 4]` over 3 frames = 10 ticks = exactly 30 fps - **24 fps** → `[4, 4, 4, 4, 4, 5]` over 6 frames = 25 ticks = exactly 24 fps - **15 fps** → `[6, 7, 7]` over 3 frames = 20 ticks = exactly 15 fps **60 fps is the one genuinely out of reach.** The pattern would be `[1, 2, 2]`, and every browser replaces a delay below 2 ticks with 10 — a rule older than most of the web, written down in no specification. The practical ceiling is **50 fps**, and a file claiming 100 fps plays at 10. ## The error is per frame | Source | Frames | Delay | Should last | Actually lasts | Difference | | --- | --- | --- | --- | --- | --- | | 30 fps | 300 frames | 3 ticks | 10.00 s | 9.00 s | -1.00 s | | 30 fps | 900 frames | 3 ticks | 30.00 s | 27.00 s | -3.00 s | | 24 fps | 240 frames | 4 ticks | 10.00 s | 9.60 s | -0.40 s | | 15 fps | 150 frames | 7 ticks | 10.00 s | 10.50 s | +0.50 s | | 60 fps | 600 frames | 2 ticks | 10.00 s | 12.00 s | +2.00 s | It does not average out and it does not cancel — every frame is rounded the same way — and the same error is paid again on every loop. ## The palette | | Slots for colour | | --- | --- | | No transparency | 256 | | With transparency | 255 | One palette of 256 entries. **Transparency is one of those entries**, not a separate channel, so using it costs a colour. And it is binary: a pixel is fully opaque or fully transparent, which is why a cut-out in a GIF has a hard fringe the colour of whatever it was composited against when it was made. ## What it is A single page at https://gif-desk.skillsafe.ai. The engine that computes all of this runs entirely in your browser, needs no account and costs nothing. Writing the report calls a model and needs credits. ## The free engine Everything below is computed client-side in `gif.js` and sent with every run as `prescan`: - **The clock.** What the clip actually plays at against what was asked for, the frames, the total ticks, the seconds, and the error as a percentage. - **The ladder.** Every rate the format has, and which of them a browser honours. - **The neighbours.** The two representable rates the asked-for one falls between. - **The mix.** The repeating delay pattern that averages to the asked rate, what it sums to, and whether any of its delays would be clamped. - **Every delay row.** What the file says beside what a browser really waits. - **The drift.** How far the duration is from the source, and what that compounds to over the stated loops. - **The palette.** The colour count against 256 slots, minus one for transparency. The engine makes no network calls. ## Sheet grammar Two blocks. Blank lines and `#` comments are ignored. ```text LOOP name | the product demo source | 30 frames | 300 loops | 3 duration | 10 colours | 412 alpha | yes DELAYS d1 | 3 | 200 | two frames in every three d2 | 4 | 100 | and the third ``` - A DELAYS row is `id | centiseconds | how many frames | what it is`. - **The delay is in centiseconds**, because that is the only unit the format has. A fractional one is refused rather than rounded silently. - DELAYS is optional. Without it one delay is derived for every frame from the source rate, which is what an exporter does. - `loops` of 0 means forever. - `duration` is what the clip should last, and is what makes drift detectable. - `holds` is free text; saying it must stay with audio raises the finding that it cannot. ## Lanes | Lane | What it decides | Fields | | --- | --- | --- | | `plan` | The loop before you export it | `brief`, `known` | | `check` | What this GIF actually plays at | `sheet`, `symptom` | | `rate` | Which rates exist, and which you are asking for | `sheet`, `priority` | | `palette` | What fits in 256 slots | `sheet`, `material` | | `deliver` | What changes: the rate, the delays, or the format | `sheet`, `fixed` | `check` is the primary lane. Every lane ships a worked example that costs nothing to read. ## Severity Severity is fixed by the code, not by the model. **Errors are reserved for a clip that cannot work at all** — a delay every viewer replaces, a rate above what any GIF plays, more colours than a palette holds, or a demand the format has no mechanism for. A rate the format does not have is a **warning**. A GIF exported at 30 and playing at 33.33 is what a large share of every GIF on the internet already is: it plays, it looks like the shot, and it is wrong. Calling the ordinary case an error would mark almost every GIF broken. There are 26 codes. `RATE-ABOVE-CEILING`, `DELAY-CLAMPED`, `DELAY-ZERO`, `SYNC-IMPOSSIBLE` and `PALETTE-OVER` are the five errors. ## API ``` POST https://api.skillsafe.ai/v1/app-api/run X-App-Key: Content-Type: application/json {"task": "check", "sheet": "LOOP\nsource | 30\nframes | 300\n...", "symptom": "it looks fast"} ``` The run body **is** the input object. There is no `input` wrapper. Every field is a string. Full documentation: https://gif-desk.skillsafe.ai/api.html ## What this page cannot do - **It has not opened the file.** Every figure is arithmetic on numbers somebody typed in. - **It does not know how many colours the material has.** A colour count on a sheet is a claim, and a photograph has tens of thousands. - **It says nothing about file size.** Size depends on how the frames compress against each other, which is a property of the pictures. - **The clamp is a browser behaviour, not a specification.** It is in every engine that matters and it is written down in nothing normative. - **It says nothing about dithering or quantisation quality.** Only about how many slots there are and whether the count fits. ## Source Lanes derived from the `gifgrep` skill in https://github.com/steipete/clawdis, which searches GIFs. Every GIF it finds has already made this rounding, whether or not anybody chose it. Not affiliated with or endorsed by that repository's authors.