All notable changes to this project are recorded here, grouped as
Added, Changed, Fixed, Removed and Security. The project uses
Semantic Versioning. From 1.0.0 every release of
1 keeps the API across 1.y and the ABI within a minor version; a context def
built against another minor version's headers is refused with
mtwn_errorVersion. Before 1.0.0, any release could change both.
This release makes mtwnCreateClipPlayer check its bindings in time linear in their count, where it compared each binding with every one before it (#292). A player of 2^20 bindings took 194 s to make at -O2 and now takes 12 ms. At the old rate, the 2^24 slots a clip may have would have taken about 11 hours. Two refusals of a def that binds a slot twice change their status, as below. MTWN_ABI_VERSION and a shared library's SOVERSION are now 1.1. A context def stamped by 1.0.0's headers is refused with mtwn_errorVersion, so a program built against 1.0.0 is rebuilt; its source builds unchanged. No value, velocity or event record changes.
mtwnCreateClipPlayer checks its refusals in this order, which its header now states: an invalid def, then the rate's limit, then the context's capacities of players and of bindings, and a slot bound twice last (#292). A def that binds a slot twice changes its status only where another refusal also applies:mtwn_errorCapacity, not counted as misuse, where it was mtwn_errorInvalid, counted;MTWN_RATE_LIMIT, mtwn_errorLimit, where it was mtwn_errorInvalid; both are counted as misuse.mtwnClipTrack's header say that a track read with mtwnClip_GetTrack points into the clip's storage. The values layer takes it as it takes a track the host made, reading only within the counts the track states, so the host keeps it as it was read (#299).This is the first release of 1. Maul Tween's API is settled, and from here it ships only minor and patch releases of 1 (#273). Every function, type, enum value, macro and struct field in include/maul-tween/ keeps its name, signature and meaning across 1.y. The binary interface holds within a minor version. The API is the same as 0.17.0's, so a program built against 0.17.0 needs only a rebuild. MTWN_ABI_VERSION and a shared library's SOVERSION are now 1.0, and a context def stamped by 0.17.0's headers is refused with mtwn_errorVersion. No value, velocity or event record changes.
MTWN_ABI_VERSION and the SOVERSION at major.minor, and the CMake package taking any later 1.y;mtwnMakeElasticEasing's header give the largest difference from GSAP's and tween.js's elastic for every amplitude (#272). The new bound is q (1 + q) 2^-10 / (1 - q 2^-10), with q the larger of the amplitude and 1. The old bound, a (1 + a) 2^-10, was too small below amplitude 1 and at large amplitudes. The curves do not change; test_rival_easing checks 90 of the rivals' curves against the new bound.This release changes two results to follow the references they name, ahead of 1.0.0. A finished player given a new rate runs at it from the time it holds, so a positive rate keeps it finished at its end where it jumped back (#264). A path's direction through a hold or a step is the way the path goes on, as SVG 2's path directionality takes it, where it was the way it came (#266). The clips guide's GSAP porting notes now cover grids with fewer items than columns and every way a clip's positions differ from GSAP's timeline, each checked against GSAP 3.15.0's outputs (#260, #262). Results change for a finished player given a positive rate (its time, the values it writes and its records), and for the direction and angle a host asks of a timed path through a hold or a step.
mtwnClipPlayer_SetRate on a finished player runs it at the new rate from the time it holds, its start time set again from it, start = now - current / rate (#264). A positive rate keeps the player finished there. It kept the start time it finished with, as Web Animations' setting of the playback rate leaves it, so the next update put it back at (now - start) * rate: a 2 s clip finished at 12 s and given 0.5 at 12.5 s jumped back to 1.3 s and finished again at 14 s.docs/internals/timing.md names this, and a running player's pending rate, as the two places the players depart from the specification's text.tools/oracle/player_oracle.py writes Level 1's player procedures again from the specification, with those two departures, and test_player matches its 300 scripts to the bit.mtwnComputePathDirection and mtwnComputePathAngle give a timed hold (a segment of no length between keyframes at different offsets) and a step the direction of the nearest later segment with a length, at its start, as SVG 2 moves past a segment of no length (#266). They gave the nearest earlier one, at its end. A path right to (1, 0), held there from 0.4 to 0.6 and then up, faced along x through the hold, and now faces up from 0.4.each over count - 1. A grid of one row whose origin GSAP puts off that row has no translation, measured across both axes. test_rival_stagger checks 832 of GSAP's cases.test_rival_position checks 400 of GSAP's timeline scripts.This release changes two stagger results, found by comparing staggers with GSAP's distribute, which the clips guide names as their model. A negative spacing now runs from the farthest items, as GSAP's does, so no offset falls below the start and every offset is a delay a transition takes (#257). An offset whose host easing turns out not finite when it is written is refused, where it was returned (#254). The guide's porting note now gives every translation a port from GSAP needs, checked against GSAP 3.15.0's own outputs (#258). Offsets change for every stagger with a negative spacing; no other value, velocity or event record changes.
mtwnComputeStagger with a negative spacing puts the farthest items at the start and the nearest at the start plus the spread's size, on the easing turned around: an offset is start - span * ease(1 - t) (#257). It put the nearest items at the start and the others below it, on the same easing. A row of six from the first with a spacing of -0.1 gave 0, -0.1, ..., -0.5, which as delays a transition refuses; it now gives 0.5, 0.4, ..., 0, as GSAP does.start - spacing, where it gave start.mtwnComputeStagger refuses with mtwn_errorInvalid a stagger whose host easing gives a value that is not finite when an offset is written, where it returned mtwn_success with that offset not finite (#254). The easing is sampled at every item before any offset is written, so only a host function that gives another value at the same progress when sampled again gets there. Its offsets are then unspecified; every other refusal still writes none. Every offset of a success is finite, as the header says.distribute, each with its translation (#258):each becomes an amount;"end" and "center" in a grid, become points of the grid;"edges" becomes the center with the spacing negated;tools/oracle/rivals.mjs writes 448 of GSAP's cases, and test/test_rival_stagger.c checks that each case, translated as the note says, agrees within GSAP's rounding.
This release keeps linear() easings monotonic where mtwnIsEasingMonotonic says they are, which a timeline's crossing count relies on (#243). A sample inside a segment could land an ulp past the segment's later point and step down at it. A sample moves only there, by at most about an ulp, so only hosts sampling linear() just below a point see it.
linear() sample no longer passes its segment's later point (#243). A segment's sample is a + share * (b - a), and the rounded difference could carry it an ulp past b where the share is 1 or rounds to it: linear(0, 0.03 1%, 0.3 3%, 1) gave 0.30000000000000004 just below 3% and 0.3 at 3%. A timeline eased by such a curve could count a mark twice, depending on where the frames fell. Inside a segment the sample is now held at the point's output where it would pass it.mtwnSampleEasing, and the tracks, transitions and timelines eased by linear(), its fits and rough easings included, change only at those progresses, just below a point, by at most about an ulp of the segment's larger output.linear() had a frame land there: a mark it crossed twice is now crossed once.This release fixes two ways the event records broke their promise that every crossing between two updates is counted, however far time moved. An endless node whose iteration passes the largest double is held there, as the timing model holds it, where its crossing count was NaN (#237). A sweep's count is taken between its two ends, so it no longer depends on how much the clip crossed before the sweep (#241). No value or velocity changes; event records change only for clips that cross more than 2^53 points before a sweep, or whose endless nodes pass the doubles. Each fix below names who sees it.
UINT64_MAX, and that an endless node whose iteration passes the largest double crosses nothing between two times past it. The iteration field of mtwnTimingResult says where it is infinite and what holds there.UINT64_MAX crossings each way they run, where none were recorded.MTWN_MAX_CLIP_DEPTH stays linear in the depth.This release fixes two results that broke what the headers promise. An Oklab or OkLCh color far past its ends keeps each channel's sign (#227), and mtwnIsEasingMonotonic no longer approves a linear() easing whose halves step down where in-out or out-in joins them, so a timeline refuses it (#230). Values change only for colors driven past about 5.6e102 in Oklab or OkLCh; nothing within range moves. Each fix below names who sees it.
mtwnMixColor, color transitions, color tracks and mtwnConvertColorFromSpace (#227). A channel whose sum met infinities of both signs was held at +FLT_MAX: black to white at a progress of -1e110 gave +FLT_MAX in each channel, brighter than white, where sRGB gives -FLT_MAX. Oklab is the default space of transitions and tracks.+FLT_MAX.mtwnIsEasingMonotonic returns false for linear() in in-out ending above 1, or in out-in starting below 0, where it returned true (#230). Those modes join the curve's halves at 1/2, and such an easing steps down there: linear(0, 2) in in-out gives 1 just below 1/2 and 0 at it. mtwnClip_AddTimeline took a timeline eased so, and its marks could be counted twice, or trap in a build with assertions.mtwnClip_AddTimeline now refuses such a timeline with mtwn_errorInvalid. A host that eased a timeline so sees the refusal.This release fixes five results that broke what the headers promise. A timeline eased by a cubic bezier or circ never steps back, so a mark at its end is crossed once (#214). A spring's settling time leaves room for the sample's rounding (#216). A stagger whose offsets would pass the doubles is refused (#221), an easing table holds a sample past the floats at the largest float (#223), and a host easing's NaN is taken as its progress and counted as misuse, while its infinity keeps equal ends in place (#225). Values change: circ samples everywhere, the child times of a timeline eased by a cubic bezier, the ends of spring transitions, and what a host easing's NaN or infinity gives. Each fix below names who sees it.
mtwnIsEasingMonotonic promises for circ in every mode and for a cubic bezier with both y in [0, 1], CSS's ease, ease-in, ease-out and ease-in-out among them. Their samples could step back between two progresses a hair apart: circ by an ulp, a cubic bezier by an ulp next to its end (ease) up to 2.6e-5 (cubic-bezier(1, 0, 0, 1) near 1/2). A mark at the end of a timeline eased by ease could then be crossed twice, and a build with assertions trapped (#214).mtwnSampleEasing, keyframes, transitions, timelines and the fit. Over a million progresses, 10% (in), 16% (out), 6% (in-out) and 9% (out-in) of them move. A host that plays circ and compares frames across versions sees these.mtwnSampleEasing, keyframes, transitions) keeps its bits, so a timeline's values can differ from them by a few 1e-14 for CSS's keywords, as the easing and clips guides now say.ease, take about 1.9 times as long to update as on 0.15.0; players in step share the sample and take 1.4% longer. A linear timeline takes 0.5% longer.mtwnComputeSpringSettlingTime gives a time from which mtwnSampleSpring's offset and velocity stay within their tolerances, as it promised. The sample, rounded, could be just outside at that time: a 2 Hz critically damped spring from 0.1 at rest, both tolerances 1e-3, sampled a velocity 3 ulps past it (#216).mtwnComputeStagger refuses with mtwn_errorInvalid, writing nothing, a stagger whose offsets would pass the largest double, where it wrote infinity and returned mtwn_success: a start of 1e308 with an amount of 1e308 over 3 items, or an easing overshooting far past its ends. Every stagger accepted before with finite offsets gives the same bits (#221).mtwnSampleEasingTable holds a sample past the floats' range at the largest finite float of its sign, as every other float the library writes is held, where it wrote infinity: cubic-bezier(0.5, 1e200, 0.5, 1), a back easing with an overshoot of 1e100 and linear(0, -1e300), for example. A sample within the range keeps its bits (#223).FLT_MAX, a scale target a NaN velocity, a clip track the base where it came from the track's timing easing, and mtwnSampleEasing and the table NaN.FLT_MAX or NaN, and a scale track's cubic segment gave NaN between other keyframes too. mtwnMixRotation between equal ends far out (a progress of 1e20) gives the rotation, where it gave a zero quaternion.This release follows the Maul family's conventions of .github v0.0.40. The enumerations are int32_t and the rough easing's flags uint32_t, so every struct holding one changes its layout, and the dropped events counter is mtwnContext_GetEventsDropped (#207). mtwnContext_GetMisuse no longer counts two refusals that depend on the library's state (#203). An unknown color space, hue method, angle method or rotation path is refused whatever the target's or track's kind (#210), and five corrections make the headers and guides say what the code does. No value, velocity or event record changes.
int32_t, no longer uint8_t, and mtwnRoughDef.flags is the new mtwnRoughFlags, a uint32_t (#207). Their values do not change. The 25 are mtwnValueKind, mtwnNodeKind, mtwnPositionKind, mtwnEventKind, mtwnPhase, mtwnFill, mtwnDirection, mtwnAlternateEasing, mtwnStaggerOrigin, mtwnStaggerAxis, mtwnStaggerSpacing, mtwnSegmentKind, mtwnTransitionKind, mtwnVelocitySource, mtwnColorSpace, mtwnHueMethod, mtwnAngleMethod, mtwnRotationPath, mtwnComposite, mtwnPlayState, mtwnEasingKind, mtwnStepPosition, mtwnCssKeyword, mtwnEaseMode and mtwnRoughTaper.mtwnEvent, mtwnBinding, mtwnTiming and mtwnClipNodeInfo. A program rebuilds against the new headers.mtwn_errorInvalid as an unknown one is, in a call, a def or a made easing, color mix or track; a path's keyframe kind is read within its tables.mtwnContext_GetDroppedEventCount is renamed mtwnContext_GetEventsDropped, as the family names a counter of work lost (#207).mtwnContext_GetMisuse no longer counts two refusals the library's state decides (#203):mtwnDestroyPlayer, mtwnClipPlayer_GetState or any clip player control. It is still refused with mtwn_errorStale. A player id never handed out still counts: the null id, an index past the highest slot used, generation 0 and a generation its slot has not reached;mtwnDestroyClip refused with mtwn_errorBusy while a player holds the clip.Stale target and clip ids still count. A host now reads no misuse for stopping or reading a player after it ended, naming a kept player after destroying it, or destroying a clip a player still holds: the count is smaller for the same calls, never larger. The transitions guide no longer asks for mtwnPlayer_IsValid before stopping a transition that may have ended.
mtwnStartTransition refuses an unknown color space, hue method, angle method or rotation path with mtwn_errorInvalid and one misuse whatever the target's kind, and mtwnContext_MeasureTransition an unknown angle method or rotation path. mtwnMakeTrack and mtwnClip_AddTrack refuse an unknown color space, hue method or angle method whatever the track's kind, as they already did the rotation path, the add counting one misuse. A kind that does not read the choice took any value, though mtwnStartTransition's header said every unknown choice is refused (#210).mtwn_errorStale is an id that names no live object: one destroyed since it was handed out, one never handed out, or the null id. base.h and the getting started guide said only the first (#189).context.h's opening comment names the const readers refused while the host's code runs: mtwnTarget_GetState called back in, and four readers inside an update's groups (#190).mtwnClipPlayer_GetState's header lists only the codes it returns. It said "As mtwnClipPlayer_Play", whose list has mtwn_errorState (#191).mtwnContext_GetSerialAdvanceCount's header said it did whenever other players ran (#192).MTWN_SEEK_LIMIT's comment derived a margin of 16 from a bound that gives 8. It now derives it from the tighter bound the same lines give (#195).This release finishes the API review for 1.0. The readers take the context const and read it when host code calls back in (#170). The calls on a clip's player are named for it (#178) and the two counters that were not are named Get...Count (#176). The reduced motion switch returns nothing (#174), the host easing's callback is a function type (#172), and mtwnMakeTrack takes the keyframes it rewrites first (#184). Three getters no longer crash on a null context, and a refused rough easing leaves its points. The getting started guide says what 1.0 will keep. No value, velocity or event record changes.
const mtwnContext* (#170): mtwnClip_GetTrack, mtwnClip_GetNode, mtwnClip_FindLabel, mtwnClip_GetDuration, mtwnTarget_GetValue, mtwnTarget_GetState, mtwnPlayer_GetState (now mtwnClipPlayer_GetState), mtwnContext_ReadTargets, mtwnContext_GetClipBytes and mtwnContext_MeasureTransition. C callers compile as before; a C++ pointer to one of them has a new type.mtwnTarget_GetState now read the context as the call in progress has left it, where they returned mtwn_errorBusy, and count no misuse.mtwnTarget_GetState is still refused when called back in: a target's velocity may be computed when read, by sampling a host easing, which could otherwise call it again without end.mtwnTarget_GetValue, mtwnContext_ReadTargets, mtwnClipPlayer_GetState and mtwnContext_MeasureTransition called from the host's code are refused with mtwn_errorBusy, one misuse counted, their outputs left as they were, since a group may be writing what they read.mtwnDestroyClip and mtwnDestroyContext release a clip's id before the host's free gets its block, so a free that reads back the clip finds the id stale.| Before | Now |
|---|---|
mtwnPlayer_Play, _Pause, _Reverse, _Finish, _Cancel |
mtwnClipPlayer_Play, _Pause, _Reverse, _Finish, _Cancel |
mtwnPlayer_Seek, _Sweep, _SeekProgress, _SeekLabel |
mtwnClipPlayer_Seek, _Sweep, _SeekProgress, _SeekLabel |
mtwnPlayer_SetRate, _SetWeight, _GetState |
mtwnClipPlayer_SetRate, _SetWeight, _GetState |
mtwnCreatePlayer |
mtwnCreateClipPlayer |
mtwnPlayerDef |
mtwnClipPlayerDef |
mtwnDefaultPlayerDef |
mtwnDefaultClipPlayerDef |
MTWN_PLAYER_DEF_COOKIE |
MTWN_CLIP_PLAYER_DEF_COOKIE |
mtwnPlayerState |
mtwnClipPlayerState |
mtwnPlayerId, mtwnDestroyPlayer and mtwnPlayer_IsValid, which take a transition's player too, and mtwnPlayState keep their names. Parameters, values and the cookie's value are unchanged.
- Breaking: mtwnContext_GetDroppedEvents is mtwnContext_GetDroppedEventCount and mtwnContext_GetSerialAdvances is mtwnContext_GetSerialAdvanceCount, as the family names its counters. Their parameters and behavior are unchanged (#176).
- Breaking: mtwnContext_EnableReducedMotion returns void. A null context changes nothing, and a call back in changes nothing and counts one misuse, as their refusals did; read the state with mtwnContext_IsReducedMotionEnabled (#174).
- Breaking: mtwnEaseFunction is a function type, double (void* context, double progress), as the task hooks' are: mtwnMakeHostEasing takes, and mtwnEasing.function holds, a mtwnEaseFunction*. Passing a function's name compiles as before; a variable of the old type is declared mtwnEaseFunction* (#172).
- Breaking: mtwnMakeTrack takes the keyframes and their count first and the def third: mtwnMakeTrack(keyframes, count, def, lengthsOut, mixesOut, trackOut). It rewrites the keyframes in place for angles and for rotations on the shortest path, so they are the call's object, as the family's conventions order it. A call in the old order no longer compiles (#184).
- docs/user/getting-started.md has a Compatibility section, linked from the README: what 1.0.0 will keep across 1.y (every function, type, enum value, macro and struct field in the headers, things only added), what it leaves out (the fields of the made value structs a host never reads), the binary interface within a minor version, and results the same bits on every platform, any change of a bit stated here. The transitions guide says to ask mtwnPlayer_IsValid before stopping a transition that may have ended (#180).
mtwnContext_GetMisuse, mtwnContext_GetSerialAdvanceCount and mtwnContext_GetMemoryBytes read 0 from a null context, where they crashed, as the other getters do (#165).mtwnMakeRoughEasing leaves the caller's points as they were. A host shape giving a value that is not finite past the first inner points left part of a new set there; the shape is now sampled at every inner point before one is written (#168).mtwnFitLinearEasing says what a refused fit leaves in its points, which its header called unspecified: the fit's first points, at most the capacity, exactly as a larger buffer would start, and the rest as they were (#182).This release settles what a refused call on a context leaves in its outputs, as the Maul family's conventions say. A refused update, event read or target read now clears its array as far as a success could write it, so a host that misses a status reads nothing stale (#160). A call refused as busy, because host code the library runs called the context back in, now writes none of its outputs, since the call in progress may have written them (#161). Only refusals change: no value, velocity or event record does.
mtwnContext_Update, mtwnContext_ReadEvents or mtwnContext_ReadTargets now clears its array as far as a success could write it, where it left it as it was (#160):changedCapacity entries, up to the context's target capacity;capacity entries, up to the context's event capacity;count zero values, without a context too.A null context leaves the update's report and the events, since how far a success could write them depends on the context. A host that reused an array across a refused call now finds the null id or zeros there, not the last call's entries, so it checks the status before it reads the array.
- Breaking: a call back in from the context's own host code (a host easing, the allocator, the task system's functions or tasks), refused with mtwn_errorBusy, now writes none of its outputs (#160, #161):
- mtwnContext_Update leaves its total, and mtwnContext_ReadEvents its count and dropped count, beside the arrays they already left, where it set them to 0;
- mtwnContext_GetClipBytes, mtwnClip_GetTrack, mtwnClip_GetNode, mtwnClip_FindLabel, mtwnClip_GetDuration, mtwnTarget_GetValue, mtwnTarget_GetState and mtwnPlayer_GetState leave their output as it was, where they cleared it.
The creations still give the null id: mtwnCreateTarget, mtwnCreateClip, mtwnCreatePlayer, mtwnStartTransition and the mtwnClip_Add... calls' node. Host code that calls the context back in now finds these outputs as it left them, not 0.
This release gives every public name the Maul family's shape (#148): a call on the context starts with mtwnContext_, every other function starts with its verb, a switch is an Enable and Is...Enabled pair, and a callback type ends in Function. Each rename keeps the function's parameters and behavior, so a caller only updates its calls. No value, velocity or event record changes.
| Before | Now |
|---|---|
mtwnGetContextMisuse |
mtwnContext_GetMisuse |
mtwnGrowContext |
mtwnContext_Grow |
mtwnMeasureTransition |
mtwnContext_MeasureTransition |
mtwnContext_SetReducedMotion |
mtwnContext_EnableReducedMotion |
mtwnContext_IsReducedMotion |
mtwnContext_IsReducedMotionEnabled |
mtwnResultName |
mtwnGetResultName |
mtwnEase |
mtwnSampleEasing |
mtwnEaseCubicBezier |
mtwnSampleCubicBezierEasing |
mtwnBind |
mtwnMakeBinding |
mtwnLinearEasing |
mtwnMakeLinearEasing |
mtwnTaskFn |
mtwnTaskFunction |
mtwnEnqueueTaskFn |
mtwnEnqueueTaskFunction |
mtwnFinishTaskFn |
mtwnFinishTaskFunction |
Before 0.11.0, mtwnMakeLinearEasing was the maker of CSS's linear() from points, which is mtwnMakeLinearPointsEasing since 0.11.0. A call written for that maker passes points, so it no longer compiles.
- The user guides and samples use the new names (#148).
This release settles names and signatures ahead of 1.0.0 (#135). Every rename keeps the function's parameters and behavior. A caller updates its calls, and no value, velocity or event record changes, except a player weight given as a double (below).
mtwn_errorInvalid, counted as misuse, wherever an angle reads one. That covers mtwnStartTransition and mtwnMeasureTransition for an angle target, and mtwnMakeTrack and mtwnClip_AddTrack for an angle track. They took it as mtwn_angleRaw. Every call still taken gives the same results (#137).mtwnMixAngle, mtwnMixInteger and mtwnMixDiscrete return mtwnResult and give their value through a new last parameter: angleOut, valueOut and valueOut. Each refuses with mtwn_errorInvalid a null out and a progress that is not finite, as every other mix does. mtwnMixAngle also refuses an unknown method, which it took as raw, and an angle that is not finite. Every call still taken gives the same bits (#140).mtwnPlayerDef.weight is a float, and mtwnPlayer_SetWeight takes a float, as mtwnBinding.weight is. The def's layout changes. Hosts that give a player a weight that is not a float's value (0.3 rather than 0.3f) see every target the player writes compose by the float nearest it. A weight a float holds keeps its bits (#138).mtwnUpdate, mtwnReadEvents, mtwnReadTargets and mtwnGetClipBytes are mtwnContext_Update, mtwnContext_ReadEvents, mtwnContext_ReadTargets and mtwnContext_GetClipBytes (#141).mtwnMakeCubicBezier, mtwnMakeSteps and mtwnMakeLinearEasing (CSS linear() from points) are mtwnMakeCubicBezierEasing, mtwnMakeStepsEasing and mtwnMakeLinearPointsEasing. mtwnLinearEasing, the identity, keeps its name (#142).mtwnSpringMotionAt and mtwnSpringSettlingTime are mtwnComputeSpringMotion and mtwnComputeSpringSettlingTime;mtwnDecayStopTime, mtwnDecayCrossingTime and mtwnBoundedDecayStopTime are mtwnComputeDecayStopTime, mtwnComputeDecayCrossingTime and mtwnComputeBoundedDecayStopTime;mtwnPathDirection, mtwnPathAngle and mtwnZoomLength are mtwnComputePathDirection, mtwnComputePathAngle and mtwnComputeZoomLength;mtwnShakeNoise is mtwnSampleShakeNoise;mtwnColorToSpace and mtwnColorFromSpace are mtwnConvertColorToSpace and mtwnConvertColorFromSpace;mtwnRubberBand and mtwnRubberBandInverse are mtwnApplyRubberBand and mtwnInvertRubberBand;mtwnSampleEasing, which fills a table, is mtwnSampleEasingTable.mtwn_staggerCentre is mtwn_staggerCenter, with the same value, as the family spells it (#144).mtwnTransitionSpec is mtwnTransitionKind, and mtwnTransitionDef.spec is kind (#145).mtwnClip_Finish is mtwnClip_Seal, with the same statuses. The docs call such a clip sealed (#146).MTWN_MIN_RATE, 2^-20, names the smallest size of a clip's player's rate other than 0, which was written only as 1 / MTWN_RATE_LIMIT. It is the same value, so every call takes and refuses what it did (#126).mtwn_errorStale is -3 (was -5), mtwn_errorState -6 (was -7), mtwn_errorVersion -7 (was -4), mtwn_errorLimit -9 (was -3) and mtwn_errorBusy -10 (was -6); mtwn_errorInvalid stays -1 and mtwn_errorCapacity -2. These refusals change status:mtwn_errorCapacity, where it was mtwn_errorLimit: targets, players, clips, bindings, clip storage, a clip's nodes, keyframes and linear() points, and a transition's points;MTWN_MAX_COUNT, MTWN_MAX_CLIP_STORAGE, MTWN_MIN_RATE, MTWN_RATE_LIMIT or MTWN_SEEK_LIMIT, an infinite rate, seek time or share included, is mtwn_errorLimit, where it was mtwn_errorInvalid; a NaN stays mtwn_errorInvalid;mtwn_errorBusy, where it was mtwn_errorState;mtwnPlayer_GetState, and mtwnPlayer_SeekProgress on an endless clip, are mtwn_errorInvalid, where they were mtwn_errorState;MTWN_MAX_CLIP_DEPTH keeps mtwn_errorLimit and now counts one misuse, as every other refused argument does.A caller that switched on the old statuses handles the new ones at those calls; one that logs or stores the raw numbers reads them anew. No value, velocity or event record changes (#124).
- Breaking: a clip's player keeps every start time finite, so its time can no longer become infinite. mtwnCreatePlayer and mtwnPlayer_SetRate refuse a rate other than 0 below MTWN_MIN_RATE (2^-20) in size. mtwnPlayer_Seek, mtwnPlayer_Sweep, mtwnPlayer_SeekProgress and mtwnPlayer_SeekLabel refuse a time past the new MTWN_SEEK_LIMIT (2^1000) in size: the argument, the share times the duration, or a root label's start. mtwnClip_AddTrack and mtwnClip_AddTimeline refuse a node that would take its clip's finite end past it. Each is mtwn_errorLimit, counted as misuse. These calls used to be taken: a share of 1e308 finished the player at an infinite time, and a rate of 5e-324 jumped it to its end at the next update. Every call still taken gives the same results (#119).
- Breaking: the public structs have no reserved fields. The reserved member is gone from mtwnContextDef, mtwnClipDef, mtwnClipTrackDef, mtwnClipTimelineDef, mtwnPlayerDef, mtwnPosition, mtwnBinding, mtwnTiming, mtwnStagger, mtwnEasing, mtwnSpring, mtwnKeyframe, mtwnPath, mtwnTrackDef, mtwnTrack, mtwnColorMix, mtwnClipNodeInfo, mtwnPlayerState, mtwnTimingResult, mtwnTargetState and mtwnEvent, and no call refuses a struct for a reserved byte any longer. A host that set one, or listed it in a positional initializer, drops it. Layouts change: on x86-64 mtwnClipDef shrinks from 24 to 20 bytes and mtwnContextDef from 104 to 96, and on 32-bit x86 many shrink by 4 to 8 bytes, mtwnEvent from 64 to 60. A cleared output is still 0 in every byte, its padding included. No value, velocity or event record changes (#122).
- The getting-started guide lists every result with its value, and the players guide gives the bounds of a rate and of a seek (#119, #124).
mtwnComputeTiming, and a clip's timelines and tracks, no longer take the iteration and the overall progress to infinity where a repeat delay and an iterationStart of very long iterations take the position on the cycles past the doubles. Such a timing now gives what it gives without the overflow: iterations of 1e308 s from an iterationStart of 2.5, with a repeat delay, at rate 0.5 and local time 1.4 give iteration 2 and overall progress 2.5, as they do without the delay. A build with assertions no longer traps sweeping a clip over such a track. Two cases whose count no double can carry stay infinite: cycles whose count itself passes the doubles, and a start past 2^53 cycles whose count rounds by more than one. Results change only where they were infinite, or the iterations' end, before; a clip's directions, incremental steps and event iterations follow (#117).mtwnComputeTiming(timing, localTime, backwards, resultOut) returns mtwnResult (MTWN_NODISCARD) where it returned void. It refuses with mtwn_errorInvalid, leaving the result as it was, a null timing or result, a timing mtwnValidateTiming refuses (a playback rate of 0, nonzero padding, an easing of an unknown kind or with fields no maker gives), and a local time that is NaN or negative infinity. A caller takes the returned status; a refused call leaves the result as it was. Every call it still takes gives the same bits (#87).mtwnComputeTiming, and the clips' timelines and tracks that compute the same way, keep the iterations at their end where rounding lost them. An active duration that rounds to 0, or a repeat delay's cycle that swallows the active time, no longer leaves the overall progress at the start with the iteration before it. A start so large that adding the iterations rounds back to it no longer holds the iteration before the start's. A repeat delay no longer gives NaN where an effect is due. A clip's values, directions and event iterations follow (#100).mtwnSpringSettlingTime no longer returns a time before the motion settles when a product of the start with a coefficient underflows, for a spring with a subnormal w0^2, or when one bound's time is NaN. A start already settled on a spring of subnormal rate gets 0 where it got infinity. A spring transition that ends by it ends at the new time. Elsewhere a time moves only within rounding, where a tolerance lies at a bound's peak, and never early (#90).mtwnSpringSettlingTime; every settling time keeps its bits (#95).mtwnDecayCrossingTime, mtwnDecayStopTime and mtwnSampleDecay stay within the doubles on the way to results that lie within them, where they used to give infinity, 0 or a value with few digits. Every input that worked before keeps its bits (#92).mtwnSamplePunch, mtwnSampleShake and
mtwnShakeNoise return mtwnResult and give their factor through
the new factorOut. Each refuses with mtwn_errorInvalid a null
pointer, a punch or shake whose fields its maker would refuse, and a
NaN time; an infinite time stays taken. A shake of 2^53 lattice
points or more over its duration is refused, where a forged one of
2^64 or more cast an index past uint64_t's range, which was
undefined behavior in every build. mtwnRubberBand and
mtwnRubberBandInverse return mtwnResult and give their result
through the new offsetOut and dragOut. They refuse with
mtwn_errorInvalid a null out, a dimension or coefficient not above
0, and a NaN drag or offset, exactly the inputs that gave 0 before.
Every output is unchanged on refusal, and every input still taken
gives the same bits. A call costs up to about 11 ns more
(#76).mtwnMixColor, mtwnMixRotation,
mtwnMixScale and mtwnMixZoom return mtwnResult and give their
value through the new colorOut, rotationOut, scaleOut and
viewOut; mtwnZoomLength returns mtwnResult and gives the length
through lengthOut. Each refuses with mtwn_errorInvalid a null
pointer and a mix whose fields are outside their ranges, and the
samplers also refuse a progress that is not finite.
mtwnSampleTrack refuses a segment whose color or rotation mix,
where it reads one, fails that check. A mix that
passes gives no undefined behavior and no NaN, but is not promised to
equal what a maker would give: a color or rotation is finite, held
within the floats, and a scale or zoom reaches infinity or 0 only
past the doubles' range. Every output is unchanged on refusal. A
rotation, here and wherever transitions, tracks and layers slerp, is
held within the finite floats; a maker's rotation changes no bits
except where a float would have been infinite. Results for a maker's
mix are otherwise unchanged. A checked call costs about 4 to 17 ns
more. Transitions, layers and clips pay nothing per sample beyond the
rotation's hold; mtwnSampleTrack pays the check of the segment's
mix it reads. This completes the mixes, punches and shakes part of
#60; its
timing part is still to come
(#82).mtwnMixZoom no longer gives a NaN centre for a maker's mix far
outside progress 0 to 1. Past about 710 along the path the centre
settles on its limit, as the header says the path extends, and the
width is unchanged everywhere. Results change only where a field was
NaN or infinite before; no finite field changes a bit. A sample
costs up to about 3 ns more
(#81).mtwnSamplePath, mtwnPathDirection, mtwnPathAngle and
mtwnSampleTrack return mtwnResult, and every output is unchanged
on refusal: mtwnPathAngle gives its angle through the new
angleOut, and mtwnSampleTrack its value through the new
valueOut. Each refuses with mtwn_errorInvalid a null struct or
output, a progress that is not finite, a path or track whose fields
no maker gives, and the segment it reads unless its kind is known (at
constant speed, not a step) and, when its keyframes time it, its
easing is a maker's. Keyframes or lengths the host changes after
making give an unspecified value, but nothing is read outside their
arrays; what a keyframe's easing references, such as linear()'s
points and their count, stays the host's. At constant speed a
keyframe changed to NaN no longer makes one sample cost about 600,000
evaluations. A sample costs 1 to 13 ns more. Results for every input
still taken are unchanged. This is the path and track part of
#60
(#70).mtwnMakeSpringEasing refuses with mtwn_errorInvalid a
spring whose natural frequency, in radians a second, times the
duration reaches 2^512 (about 1.34e154); it used to make such an
easing from a critical spring, which eased finite, and already
refused an overdamped one. mtwnEase and every call taking an easing
refuse a spring easing whose squared frequency is infinite, which a
host-written one could have and which eased NaN. Every easing still
made gives the same bits
(#73).linear() points are copied where an easing enters a
context. A transition, track or timeline eased by linear() copies
the points into the context, so the host's array may change or go once
the call returns. mtwnContextDef gains pointsPerTransition and
mtwnClipDef gains pointCapacity, both 64 by default, which changes
both defs' layout; a host with no linear() transitions may set
pointsPerTransition to 0 and pay nothing. A transition with more
points than pointsPerTransition, and a track or timeline whose
points pass the clip's pointCapacity, are refused with
mtwn_errorLimit. Points the host changed from their maker's
canonical form (an input or output not finite, or an input below the
one before) are refused with mtwn_errorInvalid. A refusal leaves the
running transition as it was. mtwnGrowContext moves each running
transition's points and refuses a smaller pointsPerTransition. The
default context grows by 256 KiB, the default clip by 1 KiB, and the
default clip storage from 208 KiB to 224 KiB. A start pays about 1 ns
a point for the check and the copy; an update costs the same as
before. With 0.5.0 this completes the easing part of
#60
(#68).mtwnValidateTiming (and so mtwnComputeTiming), mtwnMakePath,
mtwnMakeTrack, the stagger functions and the clips refuse an easing
no maker produced, as the easing calls already do. Results for every
input still taken are unchanged
(#68).-msse2 -mfpmath=sse), so it needs a processor with SSE2, and every
build now leaves trapping math at the compiler's default, from the
family's compiler flags. The GCC builds, 64-bit and 32-bit x86, pass
every suite unchanged, the determinism hash and the oracle tables
included
(#67).mtwnEase, mtwnEaseCubicBezier and mtwnIsEasingMonotonic return
an mtwnResult and give their results through pointers, unchanged on
refusal. They, mtwnSetEaseMode, mtwnSampleEasing,
mtwnFitLinearEasing, mtwnMakeRoughEasing (its shape) and a
transition's start and measure refuse an easing whose fields are not
as its maker sets them. mtwnEase and mtwnEaseCubicBezier refuse a
progress that is not finite, and mtwnEaseCubicBezier refuses an
easing of another kind instead of returning NaN. Results for every
input still taken are unchanged, but for the fixes below. A sample
through mtwnEase costs about 2 to 12 ns more; a transition checks
its easing once, at start.
This is the second part of
#60
(#64).mtwnMakeElasticEasing refuses an amplitude past 512, and a period
below 2^-10, after an amplitude below 1 has stretched it.
mtwnMakeCubicBezier refuses a y past 2^1000 in magnitude.
mtwnMakeSpringEasing refuses a spring that oscillates more than
once in 2^-10 of the duration, and one whose eased progress could pass
the doubles' range
(#64).linear() extension or a step far
past its end gave infinity or NaN, it now gives the largest finite
double of its sign, or a flat tangent's end. linear() with inputs
spanning more than the doubles interpolates instead of giving its
first output. The free linear() sampler is finite for any finite
points, so points the host changes after making the easing give an
unspecified but finite value. Every result that was already finite
keeps its bits
(#64).mtwnSpringMotionAt, mtwnSampleSpring,
mtwnSpringSettlingTime, mtwnSampleDecay, mtwnDecayStopTime,
mtwnDecayCrossingTime, mtwnSampleBoundedDecay and
mtwnBoundedDecayStopTime return an mtwnResult and give their
results through pointers, unchanged on refusal. They refuse a spring,
decay or bounded decay no maker produced, a start that is not finite,
a time below 0 or NaN, and a tolerance or stop speed not above 0,
which used to give infinity. mtwnMakeBoundedDecay and
mtwnMakeSpringEasing refuse a decay or spring no maker produced,
and spring transitions and bounded decay transitions refuse a spring
whose derived fields or regime differ from what its frequency and
ratio make. Results for every input still taken are unchanged. A
sample costs a few nanoseconds more (5 to 8 ns for a spring, 2 to
4 ns for a decay, 15 to 21 ns for a bounded decay); a transition
checks once, at start. This is the first part of
#60
(#62).mtwnValidateTiming refuses timings whose active
duration, end time or iteration cycle would overflow a double, and so
do mtwnClip_AddTrack and mtwnClip_AddTimeline, which check a
node's timing, with mtwn_errorInvalid. timing.h already promised
that a negative rate needs a finite active duration, whose end a
backward play starts from. 0.2.1 checked only iterations times
duration, so it accepted backward timings whose repeat delays or small
rate overflowed the active duration, and those gave NaN iterations.
Newly refused: finite iterations and duration whose active duration
overflows, played forwards, which became endless; an end time (delay
plus active duration plus end delay) past a double, which became
endless the same way; a duration plus repeat delay past a double,
whose iterations came out NaN; and a child that would give a timeline
lasting as long as its children such a timing, which leaves the clip
as it was. Endless timings played forwards stay valid: infinite
iterations or an infinite duration with a positive rate. Infinitely
many iterations of nothing, or none of an infinite duration, last 0
and are checked as finite ones are. Keep a timing's sums within a
double; every timing within one validates and computes as before.timing.h links: values 60,185 bytes (from
59,673), clips 151,836 (151,388), and the whole library 169,666
(169,218), as tools/code_size.py measures them.mtwnTarget_GetState, a transition
started on the target (such as a spring taking its velocity) and a
clip layer composed over it now get the transition's velocity. The
value was always right
(#50).mtwnSpringSettlingTime and mtwnBoundedDecayStopTime return the
true, finite time. A spring transition from such a start, or a
bounded decay handing over to a spring, ends at that time: the end
value becomes the target's base and an end record is written, where
before it ran until replaced or stopped. An absurdly stiff spring from
far out really settles almost at once. Every result that did not
overflow keeps its bits.tools/code_size.py measures them. An ordinary mtwnSpringSettlingTime
takes 3,204 instructions where it took 3,169.mtwnEaseCubicBezier takes 315 instructions where it took 272,
mtwnSampleSpring 474 where it took 399, and an empty mtwnUpdate
188 where it took 166.tools/code_size.py measures them.
The wrappers add about 70 bytes each; building with
-fno-math-errno takes back about 0.3 to 1.2 kB of that per part.sqrt and ldexp compute on the x87
unit, which rounded by the host's x87 setting: under upward rounding,
mtwnMakeZoomMix gave other bits. The library's environment there
now also sets the x87 control word (round to nearest, every exception
masked, 64-bit precision), and the library is built with
-fno-math-errno, so sqrt is always the SSE instruction.mtwnEaseCubicBezier samples a cubic-bezier easing with the same
bits as mtwnEase. With link-time optimization or unused sections
dropped, a program that samples cubic beziers only through it links
none of the other curves.mtwnSpringMotionAt,
mtwnSampleSpring and the spring easings, bounded decays and spring
transitions built on them return different bits; results below 2^52
are unchanged.mtwnSpringSettlingTime and mtwnBoundedDecayStopTime now
return infinity, as the capped path already did. A spring transition
from such a start, which ended at once (a path jumped to its target,
other kinds ended with the other channels), now never ends by
settling. Where only the offset bound overflowed, the settling time
was the velocity bound's early finite time; it is now infinity too.The first release: easing, springs, tweens and timelines driven by the host's clock, with the same results on every platform.
cubic-bezier(), steps() with every jump position and linear()
with points, rough easings from a seed, springs as easings and host
functions, each in the in, out, in-out and out-in modes, with even
tables and linear() points fitted to a tolerance.clang-cl, and WebAssembly.mtwnGetClipBytes now
takes the context as its first argument; the transition, clip, clip
track, clip timeline and player defs lose their version field, the
transition def's value methods move after target, the player def's
bindingCount before bindings, the clip track def's keyframeCount
after slot, and the player, clip track and timeline defs' reserved
bytes grow. Only the context def carries the ABI version, which holds
the whole release before 1.0.