|
| 1 | +import { DynamicObject } from "src/types"; |
| 2 | +import { CalcSpring } from "./type"; |
| 3 | + |
| 4 | +const saved: DynamicObject<number[]> = {}; |
| 5 | + |
| 6 | +const calcSpring: CalcSpring = (arg = {}) => { |
| 7 | + const { |
| 8 | + tension = 320, |
| 9 | + friction = 25, |
| 10 | + mass = 1, |
| 11 | + precision = 0.01, |
| 12 | + velocity = 0, |
| 13 | + stopAttempt = 20, |
| 14 | + } = arg; |
| 15 | + |
| 16 | + // path to save this `createSpring` call, to avoid repetition |
| 17 | + const savePath = `${tension}~${friction}~${mass}~${precision}~${stopAttempt}~${velocity}`; |
| 18 | + |
| 19 | + if (saved?.[savePath]) { |
| 20 | + return Promise.resolve(saved[savePath]); |
| 21 | + } |
| 22 | + |
| 23 | + // get springs from 0 to 1; |
| 24 | + // then interpolate it with any other value(s) |
| 25 | + let current = 0; |
| 26 | + |
| 27 | + const to = 1; |
| 28 | + |
| 29 | + // initial velocity |
| 30 | + let _velocity = velocity; |
| 31 | + |
| 32 | + // frames created; on every change in the `current` position, a new frame is created |
| 33 | + let frames = 0; |
| 34 | + |
| 35 | + // `halt` holds how many times the difference between the current spring and previous spring is <= the `precision`. |
| 36 | + // This means having a higher precision (closer to 1) will result in a very short spring. While having a lower precision (closer to 0) will result in a smoother, but longer spring. |
| 37 | + let halt = 0; |
| 38 | + |
| 39 | + // `stoppingAttempt` is how many times the spring should halt (check `halt` above), before it comes to a stop. This means if you need the final moments of the spring to go back and forth a few times, use a smaller number (> 0). But if you need a fine tuned spring ending, use a higher `stoppingAttempt`. |
| 40 | + const stoppingAttempt = Math.max(Math.abs(stopAttempt), 1) || 5; |
| 41 | + |
| 42 | + // `positions` holds an array of spring values. Each item in this array is considered a frame. |
| 43 | + const positions: number[] = []; |
| 44 | + |
| 45 | + // to achieve 60fps. This shouldn't be changed. However, reducing the frames to say 1/30 will result in a choppy looking spring (not smooth). While increasing it eg (1/120) will make the spring unnecessarily long with no visible difference from the 1/60 frames. |
| 46 | + const FPS = 1 / 60; |
| 47 | + |
| 48 | + const maxFrames = Math.floor(FPS * 1000 * 1000); |
| 49 | + |
| 50 | + for (let step = 0; step <= maxFrames; step += 1) { |
| 51 | + const springForce = -tension * (current - to); |
| 52 | + |
| 53 | + const frictionForce = -friction * _velocity; |
| 54 | + |
| 55 | + const acceleration = (springForce + frictionForce) / mass; |
| 56 | + |
| 57 | + _velocity += acceleration * FPS; |
| 58 | + |
| 59 | + const nextValue = current + _velocity * FPS; |
| 60 | + |
| 61 | + const stopping = Math.abs(nextValue - current) < Math.abs(precision); |
| 62 | + |
| 63 | + if (stopping) { |
| 64 | + halt += 1; |
| 65 | + } else halt = 0; |
| 66 | + |
| 67 | + if (halt >= stoppingAttempt) { |
| 68 | + positions.push(to); |
| 69 | + frames = step + 1; |
| 70 | + break; |
| 71 | + } |
| 72 | + |
| 73 | + current = nextValue; |
| 74 | + |
| 75 | + if (step == 0) { |
| 76 | + positions.push(0); |
| 77 | + } |
| 78 | + |
| 79 | + positions.push(current); |
| 80 | + } |
| 81 | + |
| 82 | + if (frames == 0) { |
| 83 | + frames = 1000; |
| 84 | + positions.push(to); |
| 85 | + } |
| 86 | + |
| 87 | + // save results to avoid repetition |
| 88 | + return Promise.resolve((saved[savePath] = positions)); |
| 89 | +}; |
| 90 | + |
| 91 | +export default calcSpring; |
0 commit comments