# Tween Desk > Resolve a GSAP timeline: where every tween actually starts, and what its position was > measured from. `"+=1"` does not mean one second after the last tween - it means one > second after the TIMELINE'S END, which is the maximum over every child inserted so > far. Free browser-side resolver, five paid lanes. Live at https://tween-desk.skillsafe.ai/ · API at https://tween-desk.skillsafe.ai/api.html Derived from GreenSock's official `gsap-timeline` and `gsap-core` skills in `greensock/gsap-skills` (https://github.com/greensock/gsap-skills). Not affiliated with or endorsed by GreenSock. ## The one thing to know The timeline's end is `max(start + totalDuration)` over every child inserted so far, not a running total. On a timeline whose first tween runs 4s and whose second ends at 0.7s, a third at `"+=0"` starts at 4s rather than 0.7s - 3.3s out, from a line that never mentions the first tween. **Four spellings, two reference points.** A number is the timeline's own start; `"+=n"`/`"-=n"` are the TIMELINE'S END; `"<"`/`">"` are the PREVIOUS tween's start and end; a label is a fixed time. On the reference timeline `"+=0.4"` lands at 3.4s, `">0.4"` at 1.1s, `0.4` at 0.4s and `"beat+=0.4"` at 1.4s. **A tween's effective duration is not its `duration`.** A stagger makes the last target start `spread` late, so the tween ends at duration + spread - which is what `">"` and the timeline's end both read. `{each: 0.1}` on 6 targets spreads 0.5s; `{amount: 0.1}` spreads 0.1s. A repeat multiplies: repeat 2, repeatDelay 0.25 occupies 3.5s. And `repeat: -1` has no end at all, so nothing after it can be positioned from one. **A percentage is of the tween being inserted**, not of the one it is measured from: `"<50%"` is half of the NEW tween's duration. And `delay` ADDS to the position. ## The position forms | `at=` | Measured from | That is | Offset | Starts at | Reads as "after the previous tween" | | --- | --- | --- | --- | --- | --- | | `0.4` | the timeline's start | 0s | +0.4s | **0.4s** | yes | | `+=0.4` | the timeline's end | 3s | +0.4s | **3.4s** | **no — that would be 1.1s** | | `-=0.4` | the timeline's end | 3s | -0.4s | **2.6s** | **no — that would be 0.3s** | | `<0.4` | `quick`'s start | 0.2s | +0.4s | **0.6s** | yes | | `>0.4` | `quick`'s end | 0.7s | +0.4s | **1.1s** | yes | | `beat+=0.4` | the `beat` label | 1s | +0.4s | **1.4s** | yes | One timeline in every row: `slow` for 3s at 0, then `quick` for 0.5s at 0.2, then the same `probe` placed six ways. **The timeline's end is 3s and the previous tween ended at 0.7s**, so the two reference points are 2.3s apart — and only the `+=`/`-=` rows read the first one. That gap is invisible until something before the position runs long. ## Where `+=` goes wrong | First tween's duration | Previous tween ended | Timeline's end | `at=+=0` starts at | Reads as | Out by | | --- | --- | --- | --- | --- | --- | | 1s | 0.7s | 1s | **1s** | 0.7s | **+0.3s** | | 2s | 0.7s | 2s | **2s** | 0.7s | **+1.3s** | | 3s | 0.7s | 3s | **3s** | 0.7s | **+2.3s** | | 4s | 0.7s | 4s | **4s** | 0.7s | **+3.3s** | The same three-tween timeline with the first tween getting longer. The third tween's source never changes — it is `at=+=0` in every row — and it moves a second at a time, because **the timeline's end is a maximum over every child, not a running total**. Nothing in the source of the third tween mentions the first one. ## What a tween occupies | Tween | `duration` | Targets | Stagger | Spread | One target | Occupies | Over its duration by | | --- | --- | --- | --- | --- | --- | --- | --- | | plain | 1s | 1 | — | 0s | 1s | **1s** | 0s | | stagger each 0.1, 6 targets | 1s | 6 | each | 0.5s | 1s | **1.5s** | +0.5s | | stagger amount 0.1, 6 targets | 1s | 6 | amount | 0.1s | 1s | **1.1s** | +0.1s | | repeat 1 | 1s | 1 | — | 0s | 2s | **2s** | +1s | | repeat 2, repeatDelay 0.25 | 1s | 1 | — | 0s | 3.5s | **3.5s** | +2.5s | | repeat 1 and a stagger | 1s | 6 | each | 0.5s | 2s | **2.5s** | +1.5s | | repeat -1 | 1s | 1 | — | 0s | forever | **forever** | **forever** | `">"` reads the **Occupies** column, and so does the timeline's end. `stagger.each` spreads its value per gap so it grows with the target count; `stagger.amount` spreads that total across all of them — on six targets they are five times apart. A repeat multiplies: `duration * (repeat + 1) + repeatDelay * repeat`. And `repeat: -1` gives the tween no end, so nothing after it can be positioned relative to one. ## Constants and thresholds | Constant | Value | What it decides | | --- | --- | --- | | `DEFAULT_DURATION` | 0.5s | assumed when a tween and the sheet's `DEFAULTS` both omit one — GSAP's own default | | `DEFAULT_TARGETS` | 1 | assumed when a tween declares no target count, which makes a stagger's spread a guess | | `TRAP` | 0.001s | how far a `+=` has to miss the previous tween's end before it is called a miss | | `GAP` | 0.05s | dead air worth naming | | `BIG_GAP` | 0.5s | dead air nobody intended | | `MANY_TARGETS` | 3 | where `each` and `amount` start to be far enough apart to matter | | `LONG_TIMELINE` | 10s | a timeline worth calling long | Every number on this page is one of these or is derived from them. Only the first two are GSAP's; the rest are this page's thresholds for when something is worth saying. ## Sheet grammar A sheet is a header of `KEY: value` lines and up to three blocks. ```text JOB: what this is (optional, echoed back) TIMELINE: main (the root's name; assumed `main`) DEFAULTS: dur=0.5 ease=power2.out (GSAP's `defaults`, applied to every tween) TIMELINES: at= in= LABELS: at= in= TWEENS: in= at= dur= delay= targets= stagger=|each=|amount= repeat= repeatDelay= yoyo on= props= ease= ``` **THE ORDER OF THE LINES IS THE INSERTION ORDER**, across all three blocks, because that is what every relative position resolves against. A `TIMELINES:` line above the `TWEENS:` block means that timeline is added to its parent first. A position is a number, `"+=n"`, `"-=n"`, `"<"`, `">"`, `"n"`, a percentage form of any of those (`"+=50%"`, `"<-25%"`), or a label with an optional offset (`"reveal+=0.2"`). **An omitted `at=` is the timeline's end**, which is GSAP's own default and the same thing as `"+=0"`. Anything the reader cannot place is listed as a problem rather than skipped. A sheet whose tween lines were quietly swallowed would read as an empty timeline, and every figure on the page would be a confident answer about nothing. ## Lanes | Lane | What it produces | | --- | --- | | `plan` | Turn a choreography into a timeline sheet | | `resolve` (primary) | Where every tween actually starts, and what it was measured from | | `insert` | The insertion question: what moves when the timeline changes | | `spread` | The length question: what each tween actually occupies | | `deliver` | Decide what changes: the position form, a label, or the order | ## A worked timeline ```text JOB: hero reveal - backdrop, headline, sub, then the button TIMELINE: main DEFAULTS: dur=0.5 ease=power2.out TWEENS: backdrop dur=2.4 at=0 props=opacity,scale headline dur=0.6 at=0.3 props=y,opacity sub dur=0.4 at=>-0.1 props=y,opacity cta dur=0.5 at=+=0.2 props=y,opacity ``` | Tween | `at=` | Measured from | Starts | Occupies | Ends | Reads as | | --- | --- | --- | --- | --- | --- | --- | | `backdrop` | `0` | the timeline's start = 0s | 0s | 2.4s | 2.4s | the same | | `headline` | `0.3` | the timeline's start = 0s | 0.3s | 0.6s | 0.9s | the same | | `sub` | `>-0.1` | `headline`'s end = 0.9s | 0.8s | 0.4s | 1.2s | the same | | `cta` | `+=0.2` | the timeline's end = 2.4s | **2.6s** | 0.5s | 3.1s | **1.4s** | The sheet in the worked example. 4 tweens, 3.1s long, 1 `+=` position did not land after the previous tween, 1 gap, 3 overlapping. The last column is what a reader assumes a `+=` means — the previous tween's end plus the offset — and bold is where that is not what happens. ## Findings | Finding | Severity | Why it matters | | --- | --- | --- | | `NOTHING-TO-RESOLVE` | error | The sheet declares no tween | | `A-POSITION-DEPENDS-ON-AN-INFINITE-TWEEN` | error | A position is measured from something that never ends | | `THE-TIMELINE-TREE-HAS-A-CYCLE` | error | A timeline is nested inside itself | | `PLUS-EQUALS-IS-NOT-AFTER-THE-PREVIOUS-TWEEN` | warning | A `+=` position did not land after the previous tween | | `A-STAGGER-MOVES-THE-END-A-LATER-POSITION-READS` | warning | A stagger pushed the end that the next `>` is measured from | | `A-STAGGER-SPREAD-EXCEEDS-THE-DURATION` | warning | The stagger spreads the tween over longer than it animates | | `STAGGER-EACH-AND-AMOUNT-ARE-NOT-THE-SAME-NUMBER` | note | `each` and `amount` differ by a factor of the target count | | `A-TWEEN-STARTS-BEFORE-THE-TIMELINE` | warning | A position resolved to a negative time | | `A-BIG-GAP-IN-THE-TIMELINE` | warning | There is dead air nobody asked for | | `TWO-TWEENS-FIGHT-OVER-THE-SAME-PROPERTIES` | warning | Two overlapping tweens declare the same properties | | `A-LABEL-IS-REFERENCED-AND-NOT-DECLARED` | warning | A position names a label the sheet does not declare | | `A-TIMELINE-IS-REFERENCED-AND-NOT-DECLARED` | warning | An entry names a timeline with no TIMELINES line | | `A-POSITION-COULD-NOT-BE-READ` | warning | A position is not one of GSAP's forms | | `THE-SHEET-HAS-LINES-THIS-PAGE-COULD-NOT-READ` | warning | Some lines were not readable | | `A-REPEAT-WITH-NO-YOYO-SNAPS-BACK` | warning | A repeating tween has no `yoyo` | | `A-DURATION-WAS-NOT-STATED` | warning | A tween declares no duration | | `EVERY-RELATIVE-POSITION-IS-SEQUENTIAL` | note | Every `+=` did land after the previous tween | | `A-POSITION-IS-ABSOLUTE` | note | A position is a plain number | | `A-POSITION-IS-A-LABEL` | note | A position is measured from a label | | `THE-TIMELINE-END-HAD-ALREADY-MOVED` | note | The timeline's end was past the previous tween's end | | `A-PERCENTAGE-POSITION-IS-OF-THE-INSERTING-TWEEN` | note | A percentage is of the tween being added | | `DELAY-AND-THE-POSITION-PARAMETER-ADD` | note | A tween has both a delay and a position | | `THE-TOTAL-DURATION-INCLUDES-THE-REPEATS` | note | A repeat makes the tween longer than its duration | | `AN-INFINITE-REPEAT` | note | A tween repeats forever | | `A-STAGGER-SPREAD` | note | A stagger spreads the tween over more time than its duration | | `A-NESTED-TIMELINE` | note | A timeline is inserted into another timeline | | `A-NESTED-DURATION-IS-NOT-WRITTEN-DOWN` | note | A nested timeline's length comes from its children | | `A-ZERO-DURATION-TWEEN-IS-A-SET` | note | A tween with no duration is an instant set | | `THE-LAST-TWEEN-IS-NOT-WHAT-ENDS-THE-TIMELINE` | note | Something other than the last tween sets the duration | | `A-SMALL-GAP-IN-THE-TIMELINE` | note | There is a short stretch with nothing animating | | `TWEENS-OVERLAP` | note | Two tweens run at the same time | | `EVERY-TWEEN-IS-BACK-TO-BACK` | note | Nothing overlaps and there is no dead air | | `DEFAULTS-WERE-APPLIED` | note | The timeline declares `defaults` | | `THE-TIMELINE-IS-LONG` | note | The timeline runs for a long time | | `A-TWEEN-HAS-NO-TARGET-COUNT` | note | A tween declares no target count | 35 findings: 3 errors, 12 warnings, 20 notes. Nothing that fires on every usable sheet is a warning. ## What this page cannot do This page reads a description of a timeline, not your code. It never runs GSAP, touches the DOM or animates anything, so: - **It resolves positions and lengths, and nothing else.** Easing changes how a tween looks and never when it starts or ends, so an ease is carried through and not modelled. The same is true of every animated value: this page is about time. - **The insertion order is the order the lines appear**, because that is what a position parameter resolves against. A sheet whose lines are in a different order from the code describes a different timeline, and the answer will be about the sheet. - **`stagger` here is a number, `each` or `amount`.** GSAP's `from`, `grid`, `axis` and ease-based staggers change which target goes when, not how long the spread is, so the spread is right and the ordering within it is not modelled. - **Overwriting is reported only where the sheet names the same `on=` target**, because GSAP overwrites per target and per property. Two tweens on different elements sharing a property name are not a conflict, and guessing otherwise would flag every crossfade. - **`repeat: -1` really does mean no end.** Where a position depends on one, this page says so rather than inventing a number — GSAP will place it somewhere, and that somewhere is not a choice anybody made. - **Nothing here checks whether the animation is any good.** It checks whether the timeline is the one you described. ## API `POST https://api.skillsafe.ai/v1/app-api/run` with an app session token. The body IS the input object — never wrapped in an `input` key. Fields: `task` (one of `plan`, `resolve`, `insert`, `spread`, `deliver`), `rules`, `prescan`, plus the lane's own fields. `POST .../estimate` with the same body returns `hold_credits` and costs nothing. ## Provenance Every table and every figure above is generated from `tl.js` by `build-skill.js`. Nothing is typed twice, so nothing can drift.