UNPKG

js-draw

Version:

Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.

191 lines (190 loc) 7.86 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const inputEvents_1 = require("../../inputEvents"); const InputMapper_1 = __importDefault(require("./InputMapper")); const math_1 = require("@js-draw/math"); const untilNextAnimationFrame_1 = __importDefault(require("../../util/untilNextAnimationFrame")); var StabilizerType; (function (StabilizerType) { StabilizerType[StabilizerType["IntertialStabilizer"] = 0] = "IntertialStabilizer"; })(StabilizerType || (StabilizerType = {})); const defaultOptions = { kind: StabilizerType.IntertialStabilizer, mass: 0.4, // kg springConstant: 100.0, // N/m frictionCoefficient: 0.28, maxPointDist: 10, // screen units inertiaFraction: 0.75, minSimilarityToFinalize: 0.0, velocityDecayFactor: 0.1, }; // Stabilizes input for a single cursor class StylusInputStabilizer { constructor( // The initial starting point of the pointer. start, // Emits a pointer motion event, returns true if the event was handled. updatePointer, options) { this.updatePointer = updatePointer; this.options = options; this.runLoop = true; this.lastUpdateTime = 0; this.velocity = math_1.Vec2.zero; this.strokePoint = start; this.targetPoint = start; this.targetInterval = 10; // ms void this.loop(); } async loop() { this.lastUpdateTime = performance.now(); while (this.runLoop) { this.update(false); await (0, untilNextAnimationFrame_1.default)(); } } setTarget(point) { this.targetPoint = point; } getNextVelocity(deltaTimeMs) { const toTarget = this.targetPoint.minus(this.strokePoint); const springForce = toTarget.times(this.options.springConstant); const gravityAccel = 10; const normalForceMagnitude = this.options.mass * gravityAccel; const frictionForce = this.velocity .normalizedOrZero() .times(-this.options.frictionCoefficient * normalForceMagnitude); const acceleration = springForce.plus(frictionForce).times(1 / this.options.mass); const decayFactor = this.options.velocityDecayFactor; const springVelocity = this.velocity .times(1 - decayFactor) .plus(acceleration.times(deltaTimeMs / 1000)); // An alternate velocity that goes directly towards the target. const toTargetVelocity = toTarget.normalizedOrZero().times(springVelocity.length()); return toTargetVelocity.lerp(springVelocity, this.options.inertiaFraction); } update(force) { const nowTime = performance.now(); const deltaTime = nowTime - this.lastUpdateTime; const reachedTarget = this.strokePoint.eq(this.targetPoint); if (deltaTime > this.targetInterval || force) { if (!reachedTarget) { let velocity; let deltaX; let parts = 1; do { velocity = this.getNextVelocity(deltaTime / parts); deltaX = velocity.times(deltaTime / 1000); parts++; } while (deltaX.magnitude() > this.options.maxPointDist && parts < 10); for (let i = 0; i < parts; i++) { this.velocity = this.getNextVelocity(deltaTime / parts); deltaX = this.velocity.times(deltaTime / 1000); this.strokePoint = this.strokePoint.plus(deltaX); // Allows the last updatePointer to be returned. if (i < parts - 1) { this.updatePointer(this.strokePoint, nowTime); } } } // Even if we have reached the target, ensure that lastUpdateTime is updated // (prevent large deltaTime). this.lastUpdateTime = nowTime; if (force || !reachedTarget) { return this.updatePointer(this.strokePoint, nowTime); } } return false; } /** Finalizes the current stroke. */ finish() { this.runLoop = false; const toTarget = this.targetPoint.minus(this.strokePoint); if (this.velocity.dot(toTarget) > this.options.minSimilarityToFinalize) { // Connect the stroke to its end point this.updatePointer(this.targetPoint, performance.now()); } } cancel() { this.runLoop = false; } } class InputStabilizer extends InputMapper_1.default { constructor(viewport, options = defaultOptions) { super(); this.viewport = viewport; this.options = options; this.stabilizer = null; this.lastPointerEvent = null; } mapPointerEvent(event) { // Don't store the last pointer event for use with pressure/button data -- // this information can be very different for a pointerup event. if ((0, inputEvents_1.isPointerEvt)(event) && event.kind !== inputEvents_1.InputEvtType.PointerUpEvt) { this.lastPointerEvent = event; } // Only apply smoothing if there is a single pointer. if (event.kind === inputEvents_1.InputEvtType.GestureCancelEvt || event.allPointers.length > 1 || this.stabilizer === null) { return this.emit(event); } this.stabilizer.setTarget(event.current.screenPos); if (event.kind === inputEvents_1.InputEvtType.PointerMoveEvt) { return this.stabilizer.update(true); } else if (event.kind === inputEvents_1.InputEvtType.PointerUpEvt) { this.stabilizer.finish(); return this.emit(event); } else { return this.emit(event); } } // Assumes that there is exactly one pointer that is currently down. emitPointerMove(screenPoint, timeStamp) { if (!this.lastPointerEvent) { return false; } const pointer = this.lastPointerEvent.current .withScreenPosition(screenPoint, this.viewport) .withTimestamp(timeStamp); const event = { kind: inputEvents_1.InputEvtType.PointerMoveEvt, current: pointer, allPointers: [pointer], }; const handled = this.emit(event); return handled; } onEvent(event) { if ((0, inputEvents_1.isPointerEvt)(event) || event.kind === inputEvents_1.InputEvtType.GestureCancelEvt) { if (event.kind === inputEvents_1.InputEvtType.PointerDownEvt) { if (event.allPointers.length > 1) { // Do not attempt to stabilize multiple pointers. this.stabilizer?.cancel(); this.stabilizer = null; } else { // Create a new stabilizer for the new stroke. this.stabilizer?.cancel(); this.stabilizer = new StylusInputStabilizer(event.current.screenPos, (screenPoint, timeStamp) => this.emitPointerMove(screenPoint, timeStamp), this.options); } } const handled = this.mapPointerEvent(event); if (event.kind === inputEvents_1.InputEvtType.PointerUpEvt || event.kind === inputEvents_1.InputEvtType.GestureCancelEvt) { this.stabilizer?.cancel(); this.stabilizer = null; } return handled; } return this.emit(event); } static fromEditor(editor) { return new InputStabilizer(editor.viewport); } } exports.default = InputStabilizer;