touch.mjs 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. // Utilities
  2. import { CircularBuffer } from "../util/index.mjs";
  3. const HORIZON = 100; // ms
  4. const HISTORY = 20; // number of samples to keep
  5. /** @see https://android.googlesource.com/platform/frameworks/native/+/master/libs/input/VelocityTracker.cpp */
  6. function kineticEnergyToVelocity(work) {
  7. const sqrt2 = 1.41421356237;
  8. return (work < 0 ? -1.0 : 1.0) * Math.sqrt(Math.abs(work)) * sqrt2;
  9. }
  10. /**
  11. * Returns pointer velocity in px/s
  12. */
  13. export function calculateImpulseVelocity(samples) {
  14. // The input should be in reversed time order (most recent sample at index i=0)
  15. if (samples.length < 2) {
  16. // if 0 or 1 points, velocity is zero
  17. return 0;
  18. }
  19. // if (samples[1].t > samples[0].t) {
  20. // // Algorithm will still work, but not perfectly
  21. // consoleWarn('Samples provided to calculateImpulseVelocity in the wrong order')
  22. // }
  23. if (samples.length === 2) {
  24. // if 2 points, basic linear calculation
  25. if (samples[1].t === samples[0].t) {
  26. // consoleWarn(`Events have identical time stamps t=${samples[0].t}, setting velocity = 0`)
  27. return 0;
  28. }
  29. return (samples[1].d - samples[0].d) / (samples[1].t - samples[0].t);
  30. }
  31. // Guaranteed to have at least 3 points here
  32. // start with the oldest sample and go forward in time
  33. let work = 0;
  34. for (let i = samples.length - 1; i > 0; i--) {
  35. if (samples[i].t === samples[i - 1].t) {
  36. // consoleWarn(`Events have identical time stamps t=${samples[i].t}, skipping sample`)
  37. continue;
  38. }
  39. const vprev = kineticEnergyToVelocity(work); // v[i-1]
  40. const vcurr = (samples[i].d - samples[i - 1].d) / (samples[i].t - samples[i - 1].t); // v[i]
  41. work += (vcurr - vprev) * Math.abs(vcurr);
  42. if (i === samples.length - 1) {
  43. work *= 0.5;
  44. }
  45. }
  46. return kineticEnergyToVelocity(work) * 1000;
  47. }
  48. export function useVelocity() {
  49. const touches = {};
  50. function addMovement(e) {
  51. Array.from(e.changedTouches).forEach(touch => {
  52. const samples = touches[touch.identifier] ?? (touches[touch.identifier] = new CircularBuffer(HISTORY));
  53. samples.push([e.timeStamp, touch]);
  54. });
  55. }
  56. function endTouch(e) {
  57. Array.from(e.changedTouches).forEach(touch => {
  58. delete touches[touch.identifier];
  59. });
  60. }
  61. function getVelocity(id) {
  62. const samples = touches[id]?.values().reverse();
  63. if (!samples) {
  64. throw new Error(`No samples for touch id ${id}`);
  65. }
  66. const newest = samples[0];
  67. const x = [];
  68. const y = [];
  69. for (const val of samples) {
  70. if (newest[0] - val[0] > HORIZON) break;
  71. x.push({
  72. t: val[0],
  73. d: val[1].clientX
  74. });
  75. y.push({
  76. t: val[0],
  77. d: val[1].clientY
  78. });
  79. }
  80. return {
  81. x: calculateImpulseVelocity(x),
  82. y: calculateImpulseVelocity(y),
  83. get direction() {
  84. const {
  85. x,
  86. y
  87. } = this;
  88. const [absX, absY] = [Math.abs(x), Math.abs(y)];
  89. return absX > absY && x >= 0 ? 'right' : absX > absY && x <= 0 ? 'left' : absY > absX && y >= 0 ? 'down' : absY > absX && y <= 0 ? 'up' : oops();
  90. }
  91. };
  92. }
  93. return {
  94. addMovement,
  95. endTouch,
  96. getVelocity
  97. };
  98. }
  99. function oops() {
  100. throw new Error();
  101. }
  102. //# sourceMappingURL=touch.mjs.map