Springs and throws
What a drag does when you let go: it glides on with the speed it had, then springs home. Stew Engine has no physics world (no gravity, forces or collisions): the feel comes from springs and a few exact rules, all on this page.
Throw it
Pick the ball up and throw it. A dot marks where it is on every frame, so wide gaps are fast and tight ones slow: the glide easing out, then the spring swinging about home.
throw sets how far it glides on, spring how it comes home, jelly how much it tilts with its speed and lift how much bigger it gets while held. Pointer actions has the basics; this page has the exact rules.
import { createStage, dragAction, frameClock, GRAB, RELEASE, THROW } from "@stew/engine";
const stage = createStage(), clock = frameClock();
const feel = {
spring: 0.5,
throw: 0.8,
jelly: 0.6,
lift: 0.5,
lockX: false,
lockY: false,
};
// the ball tells the page what happens; the layer round it moves
dragAction({ el: ball, layer: ballLayer }, feel, stage, clock);
for (const name of [GRAB, RELEASE, THROW]) {
ball.addEventListener(name, () => log(name));
}How fast it was going
While it is held, each move updates its speed: 0.9 times the speed of that move, plus 0.1 times the speed before. A steady hand settles on its true speed, and a sudden flick counts at once. Only moves count: holding still before letting go doesn't slow it down.
Speed starts at 0 on each press. It is measured in the units of the layer's parent (the move on the page, turned through the parent's screen matrix), on the action's clock. A locked axis has no speed.
each movespeed = 0.9 x distance / time + 0.1 x speed, per second, for x and y apart. The time since the last move counts as at least 1 ms.over 300 a second- Let go faster than 300 units a second (x and y together) and it also sends
stew:throw, right afterstew:release, whateverthrowis. The limit is fixed.
The glide
Letting go hands the position to two springs, one for x and one for y. The glide sets off at the speed it was let go at and eases off at a steady rate, so it comes to rest exactly as far on as the rules below say. With a spring, it springs home to 0, 0 once the glide has covered 99% of its way, carrying the speed it still has. Either part can be left out. Press it again mid-flight and it is caught where it is.
Set the speed it was let go at, throw and spring. The clip below lays a release out as keyframes to play, plot and print: the glide along power2.out, then the spring along elastic.out. It lands on the same spots as the springs, on a slightly different curve.
- cap
- 60 + 240 x 0.6 = 204
- glide
- 1000 x 0.12 x 0.6 = 72
- glide time
- 0.35 + 0.25 x 0.6 = 0.5 s
- period
- max(0.12, 0.65 - 0.5 x 0.5) = 0.4
- spring time
- 0.6 + 1.1 x 0.5 = 1.15 s
- stew:throw
- 1000 > 300: sent
{
"duration": 1.65,
"tracks": [
{
"targetUid": "ball",
"property": "x",
"keyframes": [
{ "time": 0, "value": -150, "easing": "power2.out" },
{ "time": 0.5, "value": -78, "easing": "elastic.out(1, 0.4)" },
{ "time": 1.65, "value": 0, "easing": "elastic.out(1, 0.4)" }
]
},
{
"targetUid": "ball",
"property": "y",
"keyframes": [
{ "time": 0, "value": -72, "easing": "power2.out" },
{ "time": 0.5, "value": -72, "easing": "elastic.out(1, 0.4)" },
{ "time": 1.65, "value": 0, "easing": "elastic.out(1, 0.4)" }
]
}
]
}how farspeed x 0.12 x throwon each axis, held within plus or minus60 + 240 x throw.how fast- It leaves the hand at the hand's speed, then covers all but 1 / e of what's left every
how far / speedseconds:0.12 x throws when it isn't capped. Within a thousandth of a unit, it is there. when- Only when an axis would move more than 2 units. With
throwat 0, or both axes locked, there is no glide. throw 0.25- At most 120 units.
throw 0.6- At most 204 units. Let go at 1000 a second, it glides 72, all but 1 / e of what's left every 0.072 s.
throw 1- At most 300 units.
The spring
After the glide (or at once, with none), it springs home, keeping the speed it had: it swings about home once every P of the spring's time T, and its swing dies away as e^(-9.23 t / T). A stronger spring swings faster and rings longer.
P is 0.65 - 0.5 x spring, never under 0.12, and T is 0.6 + 1.1 x spring seconds, to three places. Once what's left of its swing is under a thousandth of a unit, it rests exactly on 0, 0. A spring of 0.01 or less is no spring: it stays where it lands, and the next drag starts from there.
{
"duration": 2.7,
"tracks": [
{
"targetUid": "spring-25",
"property": "x",
"keyframes": [
{ "time": 0.4, "value": -170, "easing": "elastic.out(1, 0.525)" },
{ "time": 1.275, "value": 0, "easing": "elastic.out(1, 0.525)" }
]
},
{
"targetUid": "spring-50",
"property": "x",
"keyframes": [
{ "time": 0.4, "value": -170, "easing": "elastic.out(1, 0.4)" },
{ "time": 1.55, "value": 0, "easing": "elastic.out(1, 0.4)" }
]
},
{
"targetUid": "spring-70",
"property": "x",
"keyframes": [
{ "time": 0.4, "value": -170, "easing": "elastic.out(1, 0.3)" },
{ "time": 1.77, "value": 0, "easing": "elastic.out(1, 0.3)" }
]
},
{
"targetUid": "spring-100",
"property": "x",
"keyframes": [
{ "time": 0.4, "value": -170, "easing": "elastic.out(1, 0.15)" },
{ "time": 2.1, "value": 0, "easing": "elastic.out(1, 0.15)" }
]
}
]
}spring 0.25- A swing every 0.525 of 0.875 s (bounce 0.389).
spring 0.5- A swing every 0.4 of 1.15 s (bounce 0.493).
spring 0.7- A swing every 0.3 of 1.37 s (bounce 0.597).
spring 1- A swing every 0.15 of 1.7 s (bounce 0.785). From 1.06 up,
Pstays 0.12. the curve- How far from home it is:
e^(-9.23 t / T) x (A cos(2 pi t / (P T)) + B sin(2 pi t / (P T))), withAandBset by where it starts and how fast. Its bounce is1 - 9.23 / sqrt(9.23^2 + (2 pi / P)^2), andcreateSpring(clock, T, bounce)is the same spring.
Jelly and lift
jelly- On each move it leans toward its sideways speed x 0.015 degrees, held within plus or minus 16, times
jelly, on a spring that settles in 0.2 s and keeps its speed from move to move. Let go, it leans back to 0 the same way. Only sideways speed counts: a drag straight up never tilts. lift- On the press it grows to
1 + 0.08 x lifton a spring that settles in 0.18 s, and back to 1 in 0.3 s when let go. about- Both turn and stretch about the middle of the layer's box, measured once when
dragActionstarts.
On a single node
A Drag on one node of a path (nodeBend with kind: "drag") follows the same rules in the path's own units, with a shorter glide. A press must land within 18 pixels of the node on the page.
glide- At most
40 + 160 x throwunits, once an axis would move more than 1 unit. spring- The same as a shape's.
jelly, lift- Not used: a node doesn't tilt or grow.
eventsstew:grab,stew:releaseandstew:throw, on the path.
Springy eases
Any keyframe can spring too: three ease families pass the mark or bounce on it. Easing shows them moving. These are their in curves: .out plays the in curve backwards and upside down, and .inOut plays the in curve over the first half and the out curve over the second.
back.in(s)p^2 x ((s + 1) x p - s), withs1.70158 by default. It dips below 0 first, soback.outpasses the mark by about 10% and comes back.elastic.in(a, P)-a x 2^(10(p - 1)) x sin((p - 1 - s) x 2 pi / P), withs = P / (2 pi) x asin(1 / a).ais 1 by default and counts as 1 below that;Pis 0.3 by default, 0.45 for.inOut.bounce.out- Four arcs, each landing lower: it first reaches the mark at
p = 1 / 2.75, then dips back to 0.75, 0.9375 and 0.984375 between landings.