svelte-gestures
Advanced tools
Comparing version 1.4.1 to 1.5.0
@@ -1,6 +0,8 @@ | ||
const DEFAULT_DELAY = 300; | ||
const DEFAULT_MIN_SWIPE_DISTANCE = 60; // in pixels | ||
const DEFAULT_DELAY = 300; // ms | ||
const DEFAULT_PRESS_SPREAD = 4; // px | ||
const DEFAULT_MIN_SWIPE_DISTANCE = 60; // px | ||
const DEFAULT_TOUCH_ACTION = 'none'; | ||
// export type PointerType = 'mouse' | 'touch' | 'pen' | 'all'; | ||
function addEventListener(node, event, handler) { | ||
@@ -10,3 +12,2 @@ node.addEventListener(event, handler); | ||
} | ||
function getCenterOfTwoPoints(node, activeEvents) { | ||
@@ -27,3 +28,2 @@ const rect = node.getBoundingClientRect(); | ||
} | ||
function removeEvent(event, activeEvents) { | ||
@@ -34,16 +34,14 @@ return activeEvents.filter(activeEvent => { | ||
} | ||
function dispatch(node, gestureName, event, activeEvents, pointerType) { | ||
node.dispatchEvent(new CustomEvent(`${gestureName}${pointerType}`, { | ||
function dispatch(node, gestureName, event, activeEvents, actionType) { | ||
node.dispatchEvent(new CustomEvent(`${gestureName}${actionType}`, { | ||
detail: { | ||
event, | ||
pointersCount: activeEvents.length | ||
pointersCount: activeEvents.length, | ||
target: event.target | ||
} | ||
})); | ||
} | ||
function setPointerControls(gestureName, node, onMoveCallback, onDownCallback, onUpCallback, touchAction = DEFAULT_TOUCH_ACTION) { | ||
node.style.touchAction = touchAction; | ||
let activeEvents = []; | ||
function handlePointerdown(event) { | ||
@@ -54,11 +52,8 @@ activeEvents.push(event); | ||
const pointerId = event.pointerId; | ||
function onup(e) { | ||
if (pointerId === e.pointerId) { | ||
activeEvents = removeEvent(e, activeEvents); | ||
if (!activeEvents.length) { | ||
removeEventHandlers(); | ||
} | ||
dispatch(node, gestureName, e, activeEvents, 'up'); | ||
@@ -68,3 +63,2 @@ onUpCallback?.(activeEvents, e); | ||
} | ||
function removeEventHandlers() { | ||
@@ -76,3 +70,2 @@ removePointermoveHandler(); | ||
} | ||
const removePointermoveHandler = addEventListener(node, 'pointermove', e => { | ||
@@ -98,3 +91,2 @@ activeEvents = activeEvents.map(activeEvent => { | ||
} | ||
const removePointerdownHandler = addEventListener(node, 'pointerdown', handlePointerdown); | ||
@@ -108,9 +100,12 @@ return { | ||
function pan(node, parameters = { | ||
delay: DEFAULT_DELAY | ||
}) { | ||
function pan(node, inputParameters) { | ||
let parameters = { | ||
delay: DEFAULT_DELAY, | ||
composed: false, | ||
touchAction: DEFAULT_TOUCH_ACTION, | ||
...inputParameters | ||
}; | ||
const gestureName = 'pan'; | ||
let startTime; | ||
let target; | ||
function onDown(activeEvents, event) { | ||
@@ -120,3 +115,2 @@ startTime = Date.now(); | ||
} | ||
function onMove(activeEvents, event) { | ||
@@ -127,3 +121,2 @@ if (activeEvents.length === 1 && Date.now() - startTime > parameters.delay) { | ||
const y = Math.round(event.clientY - rect.top); | ||
if (x >= 0 && y >= 0 && x <= rect.width && y <= rect.height) { | ||
@@ -139,5 +132,20 @@ node.dispatchEvent(new CustomEvent(gestureName, { | ||
} | ||
return false; | ||
} | ||
return setPointerControls(gestureName, node, onMove, onDown, null); | ||
if (parameters.composed) { | ||
return { | ||
onMove, | ||
onDown, | ||
onUp: null | ||
}; | ||
} | ||
return { | ||
...setPointerControls(gestureName, node, onMove, onDown, null, parameters.touchAction), | ||
update: updateParameters => { | ||
parameters = { | ||
...parameters, | ||
...updateParameters | ||
}; | ||
} | ||
}; | ||
} | ||
@@ -148,15 +156,17 @@ | ||
} | ||
function pinch(node) { | ||
function pinch(node, inputParameters) { | ||
const parameters = { | ||
touchAction: DEFAULT_TOUCH_ACTION, | ||
composed: false, | ||
...inputParameters | ||
}; | ||
const gestureName = 'pinch'; | ||
let prevDistance = null; | ||
let prevDistance; | ||
let initDistance = 0; | ||
let pinchCenter; | ||
function onUp(activeEvents) { | ||
if (activeEvents.length === 1) { | ||
prevDistance = null; | ||
prevDistance = undefined; | ||
} | ||
} | ||
function onDown(activeEvents) { | ||
@@ -168,8 +178,6 @@ if (activeEvents.length === 2) { | ||
} | ||
function onMove(activeEvents) { | ||
if (activeEvents.length === 2) { | ||
const curDistance = getPointersDistance(activeEvents); | ||
if (prevDistance !== null && curDistance !== prevDistance) { | ||
if (prevDistance !== undefined && curDistance !== prevDistance) { | ||
const scale = curDistance / initDistance; | ||
@@ -183,22 +191,29 @@ node.dispatchEvent(new CustomEvent(gestureName, { | ||
} | ||
prevDistance = curDistance; | ||
} | ||
return false; | ||
} | ||
return setPointerControls(gestureName, node, onMove, onDown, onUp); | ||
if (parameters.composed) { | ||
return { | ||
onMove, | ||
onDown, | ||
onUp: null | ||
}; | ||
} | ||
return setPointerControls(gestureName, node, onMove, onDown, onUp, parameters.touchAction); | ||
} | ||
function press(node, parameters) { | ||
parameters = { | ||
function press(node, inputParameters) { | ||
const parameters = { | ||
composed: false, | ||
timeframe: DEFAULT_DELAY, | ||
triggerBeforeFinished: false, | ||
...parameters | ||
spread: DEFAULT_PRESS_SPREAD, | ||
touchAction: 'auto', | ||
...inputParameters | ||
}; | ||
node.style.userSelect = 'none'; | ||
node.oncontextmenu = e => { | ||
e.preventDefault(); | ||
}; | ||
const gestureName = 'press'; | ||
@@ -208,3 +223,3 @@ let startTime; | ||
let clientY; | ||
let clientMoved = { | ||
const clientMoved = { | ||
x: 0, | ||
@@ -215,6 +230,21 @@ y: 0 | ||
let triggeredOnTimeout = false; | ||
let triggered = false; | ||
function onDone(eventX, eventY, event) { | ||
if (Math.abs(eventX - clientX) < parameters.spread && Math.abs(eventY - clientY) < parameters.spread && Date.now() - startTime > parameters.timeframe) { | ||
const rect = node.getBoundingClientRect(); | ||
const x = Math.round(eventX - rect.left); | ||
const y = Math.round(eventY - rect.top); | ||
triggered = true; | ||
node.dispatchEvent(new CustomEvent(gestureName, { | ||
detail: { | ||
x, | ||
y, | ||
target: event.target, | ||
pointerType: event.pointerType | ||
} | ||
})); | ||
} | ||
} | ||
function onUp(activeEvents, event) { | ||
clearTimeout(timeout); | ||
if (!triggeredOnTimeout) { | ||
@@ -224,9 +254,9 @@ onDone(event.clientX, event.clientY, event); | ||
} | ||
function onMove(activeEvents, event) { | ||
clientMoved.x = event.clientX; | ||
clientMoved.y = event.clientY; | ||
return triggered; | ||
} | ||
function onDown(activeEvents, event) { | ||
triggered = false; | ||
clientX = event.clientX; | ||
@@ -236,8 +266,7 @@ clientY = event.clientY; | ||
triggeredOnTimeout = false; | ||
clientMoved.x = event.clientX; | ||
clientMoved.y = event.clientY; | ||
if (parameters.triggerBeforeFinished) { | ||
timeout = setTimeout(() => { | ||
triggeredOnTimeout = true; | ||
clientMoved.x = event.clientX; | ||
clientMoved.y = event.clientY; | ||
onDone(clientMoved.x, clientMoved.y, event); | ||
@@ -247,19 +276,10 @@ }, parameters.timeframe + 1); | ||
} | ||
function onDone(eventX, eventY, event) { | ||
if (Math.abs(eventX - clientX) < 4 && Math.abs(eventY - clientY) < 4 && Date.now() - startTime > parameters.timeframe) { | ||
const rect = node.getBoundingClientRect(); | ||
const x = Math.round(eventX - rect.left); | ||
const y = Math.round(eventY - rect.top); | ||
node.dispatchEvent(new CustomEvent(gestureName, { | ||
detail: { | ||
x, | ||
y, | ||
target: event.target | ||
} | ||
})); | ||
} | ||
const onSharedDestroy = setPointerControls(gestureName, node, onMove, onDown, onUp, parameters.touchAction); | ||
if (parameters.composed) { | ||
return { | ||
onMove, | ||
onDown, | ||
onUp | ||
}; | ||
} | ||
const onSharedDestroy = setPointerControls(gestureName, node, onMove, onDown, onUp); | ||
return { | ||
@@ -274,3 +294,2 @@ destroy: () => { | ||
function getPointersAngleDeg(activeEvents) { | ||
// instead of hell lot of conditions we use an object mapping | ||
const quadrantsMap = { | ||
@@ -288,2 +307,3 @@ left: { | ||
const height = activeEvents[0].clientY - activeEvents[1].clientY; | ||
/* | ||
@@ -303,15 +323,17 @@ In quadrants 1 and 3 allworks as expected. | ||
} | ||
function rotate(node) { | ||
function rotate(node, inputParameters) { | ||
const parameters = { | ||
touchAction: DEFAULT_TOUCH_ACTION, | ||
composed: false, | ||
...inputParameters | ||
}; | ||
const gestureName = 'rotate'; | ||
let prevAngle = null; | ||
let prevAngle; | ||
let initAngle = 0; | ||
let rotationCenter; | ||
function onUp(activeEvents) { | ||
if (activeEvents.length === 1) { | ||
prevAngle = null; | ||
prevAngle = undefined; | ||
} | ||
} | ||
function onDown(activeEvents) { | ||
@@ -326,15 +348,13 @@ if (activeEvents.length === 2) { | ||
} | ||
function onMove(activeEvents) { | ||
if (activeEvents.length === 2) { | ||
const curAngle = getPointersAngleDeg(activeEvents); | ||
if (prevAngle !== null && curAngle !== prevAngle) { | ||
if (prevAngle !== undefined && curAngle !== prevAngle) { | ||
// Make sure we start at zero, doesnt matter what is the initial angle of fingers | ||
let rotation = curAngle - initAngle; // instead of showing 180 - 360, we will show negative -180 - 0 | ||
let rotation = curAngle - initAngle; | ||
// instead of showing 180 - 360, we will show negative -180 - 0 | ||
if (rotation > 180) { | ||
rotation -= 360; | ||
} | ||
node.dispatchEvent(new CustomEvent(gestureName, { | ||
@@ -347,15 +367,24 @@ detail: { | ||
} | ||
prevAngle = curAngle; | ||
} | ||
return false; | ||
} | ||
return setPointerControls(gestureName, node, onMove, onDown, onUp); | ||
if (parameters.composed) { | ||
return { | ||
onMove, | ||
onDown, | ||
onUp | ||
}; | ||
} | ||
return setPointerControls(gestureName, node, onMove, onDown, onUp, parameters.touchAction); | ||
} | ||
function swipe(node, parameters = { | ||
timeframe: DEFAULT_DELAY, | ||
minSwipeDistance: DEFAULT_MIN_SWIPE_DISTANCE, | ||
touchAction: DEFAULT_TOUCH_ACTION | ||
}) { | ||
function swipe(node, inputParameters) { | ||
const parameters = { | ||
timeframe: DEFAULT_DELAY, | ||
minSwipeDistance: DEFAULT_MIN_SWIPE_DISTANCE, | ||
touchAction: DEFAULT_TOUCH_ACTION, | ||
composed: false, | ||
...inputParameters | ||
}; | ||
const gestureName = 'swipe'; | ||
@@ -366,3 +395,2 @@ let startTime; | ||
let target; | ||
function onDown(activeEvents, event) { | ||
@@ -372,3 +400,2 @@ clientX = event.clientX; | ||
startTime = Date.now(); | ||
if (activeEvents.length === 1) { | ||
@@ -378,3 +405,2 @@ target = event.target; | ||
} | ||
function onUp(activeEvents, event) { | ||
@@ -387,3 +413,2 @@ if (event.type === 'pointerup' && activeEvents.length === 0 && Date.now() - startTime < parameters.timeframe) { | ||
let direction = null; | ||
if (absX >= 2 * absY && absX > parameters.minSwipeDistance) { | ||
@@ -396,3 +421,2 @@ // horizontal (by *2 we eliminate diagonal movements) | ||
} | ||
if (direction) { | ||
@@ -408,9 +432,414 @@ node.dispatchEvent(new CustomEvent(gestureName, { | ||
} | ||
if (parameters.composed) { | ||
return { | ||
onMove: null, | ||
onDown, | ||
onUp | ||
}; | ||
} | ||
return setPointerControls(gestureName, node, null, onDown, onUp, parameters.touchAction); | ||
} | ||
function tap(node, parameters = { | ||
timeframe: DEFAULT_DELAY | ||
}) { | ||
function callAllByType(ListenerType, subGestureFunctions, activeEvents, event) { | ||
subGestureFunctions.forEach(gesture => { | ||
gesture[ListenerType]?.(activeEvents, event); | ||
}); | ||
} | ||
function composedGesture(node, gestureCallback) { | ||
const gestureFunctions = []; | ||
function registerGesture(gestureFn, parameters) { | ||
const subGestureFns = gestureFn(node, { | ||
...parameters, | ||
composed: true | ||
}); | ||
gestureFunctions.push(subGestureFns); | ||
return subGestureFns; | ||
} | ||
const onMoveCallback = gestureCallback(registerGesture); | ||
const gestureName = 'composedGesture'; | ||
function onUp(activeEvents, event) { | ||
callAllByType('onUp', gestureFunctions, activeEvents, event); | ||
} | ||
function onDown(activeEvents, event) { | ||
callAllByType('onDown', gestureFunctions, activeEvents, event); | ||
} | ||
function onMove(activeEvents, event) { | ||
onMoveCallback(activeEvents, event); | ||
return true; | ||
} | ||
return setPointerControls(gestureName, node, onMove, onDown, onUp); | ||
} | ||
const DEFAULT_TRESHOLD = 0.9; | ||
const DEFAULT_NB_OF_SAMPLE_POINTS = 64; | ||
const PHI = (Math.sqrt(5.0) - 1) / 2; | ||
const ANGLE_RANGE_RAD = deg2Rad(45.0); | ||
const ANGLE_PRECISION_RAD = deg2Rad(2.0); | ||
function deg2Rad(d) { | ||
return d * Math.PI / 180; | ||
} | ||
function getDistance(a, b) { | ||
const dx = b.x - a.x; | ||
const dy = b.y - a.y; | ||
return Math.sqrt(dx * dx + dy * dy); | ||
} | ||
function distanceAtBestAngle(pattern, points, center) { | ||
let fromAngleRad = -ANGLE_RANGE_RAD; | ||
let toAngleRad = ANGLE_RANGE_RAD; | ||
let angleOne = PHI * fromAngleRad + (1.0 - PHI) * toAngleRad; | ||
let distanceOne = distanceAtAngle(pattern, angleOne, points, center); | ||
let angleTwo = (1.0 - PHI) * fromAngleRad + PHI * toAngleRad; | ||
let distanceTwo = distanceAtAngle(pattern, angleTwo, points, center); | ||
while (Math.abs(toAngleRad - fromAngleRad) > ANGLE_PRECISION_RAD) { | ||
if (distanceOne < distanceTwo) { | ||
toAngleRad = angleTwo; | ||
angleTwo = angleOne; | ||
distanceTwo = distanceOne; | ||
angleOne = PHI * fromAngleRad + (1.0 - PHI) * toAngleRad; | ||
distanceOne = distanceAtAngle(pattern, angleOne, points, center); | ||
} else { | ||
fromAngleRad = angleOne; | ||
angleOne = angleTwo; | ||
distanceOne = distanceTwo; | ||
angleTwo = (1.0 - PHI) * fromAngleRad + PHI * toAngleRad; | ||
distanceTwo = distanceAtAngle(pattern, angleTwo, points, center); | ||
} | ||
} | ||
return Math.min(distanceOne, distanceTwo); | ||
} | ||
function distanceAtAngle(pattern, angle, points, center) { | ||
const strokePoints = rotateBy(angle, points, center); | ||
const d = strokePoints.reduce((accu, sPoint, i) => { | ||
return accu += getDistance(sPoint, pattern.points[i]); | ||
}, 0); | ||
return d / strokePoints.length; | ||
} | ||
function rotateBy(angle, points, center) { | ||
const cos = Math.cos(angle); | ||
const sin = Math.sin(angle); | ||
return points.map(point => { | ||
return { | ||
x: (point.x - center.x) * cos - (point.y - center.y) * sin + center.x, | ||
y: (point.x - center.x) * sin + (point.y - center.y) * cos + center.y | ||
}; | ||
}); | ||
} | ||
function shapeDetector(inputPatterns, options = {}) { | ||
const threshold = options.threshold || 0; | ||
const NUMBER_OF_SAMPLE_POINTS = options.nbOfSamplePoints || DEFAULT_NB_OF_SAMPLE_POINTS; | ||
const SQUARE_SIZE = 250; | ||
const HALF_SQUARE_DIAGONAL = Math.sqrt(SQUARE_SIZE ** 2 + SQUARE_SIZE ** 2) / 2; | ||
const patterns = inputPatterns.flatMap(pattern => learn(pattern.name, pattern.points, pattern.allowRotation ?? false, pattern.bothDirections ?? true)); | ||
function getStroke(points, name, allowRotation) { | ||
points = resample(); | ||
const center = getCenterPoint(); | ||
if (allowRotation) { | ||
points = rotateBy(-indicativeAngle(center), points, center); | ||
} | ||
points = scaleToSquare(); | ||
points = translateToOrigin(getCenterPoint()); | ||
return { | ||
name, | ||
points, | ||
center: { | ||
x: 0, | ||
y: 0 | ||
}, | ||
allowRotation | ||
}; | ||
function resample() { | ||
let localDistance, q; | ||
let distance = 0; | ||
const interval = strokeLength() / (NUMBER_OF_SAMPLE_POINTS - 1); | ||
const newPoints = [points[0]]; | ||
for (let i = 1; i < points.length; i++) { | ||
localDistance = getDistance(points[i - 1], points[i]); | ||
if (distance + localDistance >= interval) { | ||
q = { | ||
x: points[i - 1].x + (interval - distance) / localDistance * (points[i].x - points[i - 1].x), | ||
y: points[i - 1].y + (interval - distance) / localDistance * (points[i].y - points[i - 1].y) | ||
}; | ||
newPoints.push(q); | ||
points.splice(i, 0, q); | ||
distance = 0; | ||
} else { | ||
distance += localDistance; | ||
} | ||
} | ||
if (newPoints.length === NUMBER_OF_SAMPLE_POINTS - 1) { | ||
newPoints.push(points[points.length - 1]); | ||
} | ||
return newPoints; | ||
} | ||
function scaleToSquare() { | ||
const box = { | ||
minX: +Infinity, | ||
maxX: -Infinity, | ||
minY: +Infinity, | ||
maxY: -Infinity, | ||
width: 0, | ||
height: 0 | ||
}; | ||
points.forEach(point => { | ||
box.minX = Math.min(box.minX, point.x); | ||
box.minY = Math.min(box.minY, point.y); | ||
box.maxX = Math.max(box.maxX, point.x); | ||
box.maxY = Math.max(box.maxY, point.y); | ||
}); | ||
box.width = box.maxX - box.minX; | ||
box.height = box.maxY - box.minY; | ||
return points.map(point => { | ||
return { | ||
x: point.x * (SQUARE_SIZE / box.width), | ||
y: point.y * (SQUARE_SIZE / box.height) | ||
}; | ||
}); | ||
} | ||
function translateToOrigin(center) { | ||
return points.map(point => ({ | ||
x: point.x - center.x, | ||
y: point.y - center.y | ||
})); | ||
} | ||
function getCenterPoint() { | ||
const centre = points.reduce((acc, point) => { | ||
acc.x += point.x; | ||
acc.y += point.y; | ||
return acc; | ||
}, { | ||
x: 0, | ||
y: 0 | ||
}); | ||
centre.x /= points.length; | ||
centre.y /= points.length; | ||
return centre; | ||
} | ||
function indicativeAngle(center) { | ||
return Math.atan2(center.y - points[0].y, center.x - points[0].x); | ||
} | ||
function strokeLength() { | ||
let d = 0; | ||
for (let i = 1; i < points.length; i++) { | ||
d += getDistance(points[i - 1], points[i]); | ||
} | ||
return d; | ||
} | ||
} | ||
function detect(points, patternName = '') { | ||
const strokeRotated = getStroke(points, patternName, true); | ||
const strokeUnrotated = getStroke(points, patternName, false); | ||
let bestDistance = +Infinity; | ||
let bestPattern = null; | ||
let bestScore = 0; | ||
patterns.forEach(pattern => { | ||
if (pattern.name.indexOf(patternName) > -1) { | ||
const distance = pattern.allowRotation ? distanceAtBestAngle(pattern, strokeRotated.points, strokeRotated.center) : distanceAtAngle(pattern, 0, strokeUnrotated.points, strokeUnrotated.center); | ||
const score = 1.0 - distance / HALF_SQUARE_DIAGONAL; | ||
if (distance < bestDistance && score > threshold) { | ||
bestDistance = distance; | ||
bestPattern = pattern.name; | ||
bestScore = score; | ||
} | ||
} | ||
}); | ||
return { | ||
pattern: bestPattern, | ||
score: bestScore | ||
}; | ||
} | ||
function learn(name, points, allowRotation, bothDirections) { | ||
const response = [getStroke([...points], name, allowRotation)]; | ||
if (bothDirections) { | ||
response.push(getStroke([...points.reverse()], name, allowRotation)); | ||
} | ||
return response; | ||
} | ||
return { | ||
detect | ||
}; | ||
} | ||
function shapeGesture(node, inputParameters) { | ||
let parameters = { | ||
composed: false, | ||
shapes: [], | ||
threshold: DEFAULT_TRESHOLD, | ||
timeframe: 1000, | ||
nbOfSamplePoints: DEFAULT_NB_OF_SAMPLE_POINTS, | ||
touchAction: DEFAULT_TOUCH_ACTION, | ||
...inputParameters | ||
}; | ||
const gestureName = 'shapeGesture'; | ||
const detector = shapeDetector(parameters.shapes, { | ||
...parameters | ||
}); | ||
let startTime; | ||
let target; | ||
let stroke = []; | ||
function onDown(activeEvents, event) { | ||
startTime = Date.now(); | ||
target = event.target; | ||
stroke = []; | ||
} | ||
function onMove(activeEvents, event) { | ||
if (activeEvents.length === 1) { | ||
const rect = node.getBoundingClientRect(); | ||
const x = Math.round(event.clientX - rect.left); | ||
const y = Math.round(event.clientY - rect.top); | ||
stroke.push({ | ||
x, | ||
y | ||
}); | ||
} | ||
return false; | ||
} | ||
function onUp() { | ||
if (stroke.length > 2 && Date.now() - startTime < parameters.timeframe) { | ||
const detectionResult = detector.detect(stroke); | ||
node.dispatchEvent(new CustomEvent(gestureName, { | ||
detail: { | ||
...detectionResult, | ||
target | ||
} | ||
})); | ||
} | ||
} | ||
if (parameters.composed) { | ||
return { | ||
onMove, | ||
onDown, | ||
onUp | ||
}; | ||
} | ||
return { | ||
...setPointerControls(gestureName, node, onMove, onDown, onUp, parameters.touchAction), | ||
update: updateParameters => { | ||
parameters = { | ||
...parameters, | ||
...updateParameters | ||
}; | ||
} | ||
}; | ||
} | ||
function isScrollMode(event) { | ||
return event.pointerType === 'touch'; | ||
} | ||
function getScrollParent(node, direction) { | ||
if (!node) { | ||
return undefined; | ||
} | ||
const isElement = node instanceof HTMLElement; | ||
const overflowY = isElement && window.getComputedStyle(node).overflowY; | ||
const isScrollable = overflowY !== 'visible' && overflowY !== 'hidden'; | ||
const directionToDimension = { | ||
x: 'Width', | ||
y: 'Height' | ||
}; | ||
if (isScrollable && node[`scroll${directionToDimension[direction]}`] > node[`client${directionToDimension[direction]}`]) { | ||
return node; | ||
} else { | ||
return getScrollParent(node.parentNode, direction) || document.scrollingElement || document.body; | ||
} | ||
} | ||
function scroll(node, inputParameters) { | ||
let parameters = { | ||
...{ | ||
delay: DEFAULT_DELAY, | ||
touchAction: DEFAULT_TOUCH_ACTION, | ||
composed: false | ||
}, | ||
...inputParameters | ||
}; | ||
const gestureName = 'scroll'; | ||
let nearestScrollEl = { | ||
x: undefined, | ||
y: undefined | ||
}; | ||
let prevCoords; | ||
let scrollDelta = { | ||
x: 0, | ||
y: 0 | ||
}; | ||
let scrollDirectionPositive = { | ||
x: true, | ||
y: true | ||
}; | ||
function scrollElementTo(el, scrollValue, direction) { | ||
el?.scrollBy({ | ||
[direction === 'x' ? 'left' : 'top']: scrollValue, | ||
behavior: 'auto' | ||
}); | ||
} | ||
function onDown(activeEvents, event) { | ||
nearestScrollEl.y = getScrollParent(node, 'y'); | ||
nearestScrollEl.x = getScrollParent(node, 'x'); | ||
prevCoords = undefined; | ||
} | ||
function onMove(activeEvents, event) { | ||
if (activeEvents.length === 1 && isScrollMode(event)) { | ||
if (prevCoords !== undefined) { | ||
scrollDelta.y = Math.round(prevCoords.y - event.clientY); | ||
scrollDelta.x = Math.round(prevCoords.x - event.clientX); | ||
nearestScrollEl.y && scrollElementTo(nearestScrollEl.y, scrollDelta.y, 'y'); | ||
nearestScrollEl.x && scrollElementTo(nearestScrollEl.x, scrollDelta.x, 'x'); | ||
} | ||
prevCoords = { | ||
x: event.clientX, | ||
y: event.clientY | ||
}; | ||
} | ||
return false; | ||
} | ||
function onUp(activeEvents, event) { | ||
if (isScrollMode(event)) { | ||
if (scrollDelta.y || scrollDelta.x) { | ||
scrollDirectionPositive.y = scrollDelta.y > 0; | ||
scrollDirectionPositive.x = scrollDelta.x > 0; | ||
requestAnimationFrame(scrollOutLoop); | ||
} | ||
} | ||
} | ||
function scrollOutByDirection(direction) { | ||
if (!scrollDirectionPositive[direction] && scrollDelta[direction] < 0) { | ||
scrollDelta[direction] += 0.3; | ||
} else if (scrollDirectionPositive[direction] && scrollDelta[direction] > 0) { | ||
scrollDelta[direction] -= 0.3; | ||
} else { | ||
scrollDelta[direction] = 0; | ||
} | ||
if (scrollDelta[direction]) { | ||
scrollElementTo(nearestScrollEl[direction], scrollDelta[direction], direction); | ||
requestAnimationFrame(scrollOutLoop); | ||
} | ||
} | ||
function scrollOutLoop() { | ||
nearestScrollEl.x && scrollOutByDirection('x'); | ||
nearestScrollEl.y && scrollOutByDirection('y'); | ||
} | ||
if (parameters.composed) { | ||
return { | ||
onMove, | ||
onUp, | ||
onDown | ||
}; | ||
} | ||
return { | ||
...setPointerControls(gestureName, node, onMove, onDown, onUp, parameters.touchAction), | ||
update: updateParameters => { | ||
parameters = { | ||
...parameters, | ||
...updateParameters | ||
}; | ||
} | ||
}; | ||
} | ||
function tap(node, inputParameters) { | ||
const parameters = { | ||
timeframe: DEFAULT_DELAY, | ||
composed: false, | ||
touchAction: 'auto', | ||
...inputParameters | ||
}; | ||
const gestureName = 'tap'; | ||
@@ -420,3 +849,2 @@ let startTime; | ||
let clientY; | ||
function onUp(activeEvents, event) { | ||
@@ -436,3 +864,2 @@ if (Math.abs(event.clientX - clientX) < 4 && Math.abs(event.clientY - clientY) < 4 && Date.now() - startTime < parameters.timeframe) { | ||
} | ||
function onDown(activeEvents, event) { | ||
@@ -443,6 +870,12 @@ clientX = event.clientX; | ||
} | ||
return setPointerControls(gestureName, node, null, onDown, onUp); | ||
if (parameters.composed) { | ||
return { | ||
onMove: null, | ||
onDown, | ||
onUp | ||
}; | ||
} | ||
return setPointerControls(gestureName, node, null, onDown, onUp, parameters.touchAction); | ||
} | ||
export { DEFAULT_DELAY, DEFAULT_MIN_SWIPE_DISTANCE, DEFAULT_TOUCH_ACTION, getCenterOfTwoPoints, pan, pinch, press, rotate, setPointerControls, swipe, tap }; | ||
export { DEFAULT_DELAY, DEFAULT_MIN_SWIPE_DISTANCE, DEFAULT_PRESS_SPREAD, DEFAULT_TOUCH_ACTION, composedGesture, getCenterOfTwoPoints, pan, pinch, press, rotate, scroll, setPointerControls, shapeGesture, swipe, tap }; |
'use strict'; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
const DEFAULT_DELAY = 300; // ms | ||
const DEFAULT_PRESS_SPREAD = 4; // px | ||
const DEFAULT_MIN_SWIPE_DISTANCE = 60; // px | ||
const DEFAULT_TOUCH_ACTION = 'none'; | ||
const DEFAULT_DELAY = 300; | ||
const DEFAULT_MIN_SWIPE_DISTANCE = 60; // in pixels | ||
// export type PointerType = 'mouse' | 'touch' | 'pen' | 'all'; | ||
const DEFAULT_TOUCH_ACTION = 'none'; | ||
function addEventListener(node, event, handler) { | ||
@@ -14,3 +14,2 @@ node.addEventListener(event, handler); | ||
} | ||
function getCenterOfTwoPoints(node, activeEvents) { | ||
@@ -31,3 +30,2 @@ const rect = node.getBoundingClientRect(); | ||
} | ||
function removeEvent(event, activeEvents) { | ||
@@ -38,35 +36,29 @@ return activeEvents.filter(activeEvent => { | ||
} | ||
function dispatch(node, gestureName, event, activeEvents, pointerType) { | ||
node.dispatchEvent(new CustomEvent(`${gestureName}${pointerType}`, { | ||
function dispatch(node, gestureName, event, activeEvents, actionType) { | ||
node.dispatchEvent(new CustomEvent(`${gestureName}${actionType}`, { | ||
detail: { | ||
event, | ||
pointersCount: activeEvents.length | ||
pointersCount: activeEvents.length, | ||
target: event.target | ||
} | ||
})); | ||
} | ||
function setPointerControls(gestureName, node, onMoveCallback, onDownCallback, onUpCallback, touchAction = DEFAULT_TOUCH_ACTION) { | ||
node.style.touchAction = touchAction; | ||
let activeEvents = []; | ||
function handlePointerdown(event) { | ||
activeEvents.push(event); | ||
dispatch(node, gestureName, event, activeEvents, 'down'); | ||
onDownCallback?.(activeEvents, event); | ||
onDownCallback === null || onDownCallback === void 0 ? void 0 : onDownCallback(activeEvents, event); | ||
const pointerId = event.pointerId; | ||
function onup(e) { | ||
if (pointerId === e.pointerId) { | ||
activeEvents = removeEvent(e, activeEvents); | ||
if (!activeEvents.length) { | ||
removeEventHandlers(); | ||
} | ||
dispatch(node, gestureName, e, activeEvents, 'up'); | ||
onUpCallback?.(activeEvents, e); | ||
onUpCallback === null || onUpCallback === void 0 ? void 0 : onUpCallback(activeEvents, e); | ||
} | ||
} | ||
function removeEventHandlers() { | ||
@@ -78,3 +70,2 @@ removePointermoveHandler(); | ||
} | ||
const removePointermoveHandler = addEventListener(node, 'pointermove', e => { | ||
@@ -85,3 +76,3 @@ activeEvents = activeEvents.map(activeEvent => { | ||
dispatch(node, gestureName, e, activeEvents, 'move'); | ||
onMoveCallback?.(activeEvents, e); | ||
onMoveCallback === null || onMoveCallback === void 0 ? void 0 : onMoveCallback(activeEvents, e); | ||
}); | ||
@@ -98,6 +89,5 @@ const removeLostpointercaptureHandler = addEventListener(node, 'lostpointercapture', e => { | ||
dispatch(node, gestureName, e, activeEvents, 'up'); | ||
onUpCallback?.(activeEvents, e); | ||
onUpCallback === null || onUpCallback === void 0 ? void 0 : onUpCallback(activeEvents, e); | ||
}); | ||
} | ||
const removePointerdownHandler = addEventListener(node, 'pointerdown', handlePointerdown); | ||
@@ -110,10 +100,12 @@ return { | ||
} | ||
function pan(node, parameters = { | ||
delay: DEFAULT_DELAY | ||
}) { | ||
function pan(node, inputParameters) { | ||
let parameters = { | ||
delay: DEFAULT_DELAY, | ||
composed: false, | ||
touchAction: DEFAULT_TOUCH_ACTION, | ||
...inputParameters | ||
}; | ||
const gestureName = 'pan'; | ||
let startTime; | ||
let target; | ||
function onDown(activeEvents, event) { | ||
@@ -123,3 +115,2 @@ startTime = Date.now(); | ||
} | ||
function onMove(activeEvents, event) { | ||
@@ -130,3 +121,2 @@ if (activeEvents.length === 1 && Date.now() - startTime > parameters.delay) { | ||
const y = Math.round(event.clientY - rect.top); | ||
if (x >= 0 && y >= 0 && x <= rect.width && y <= rect.height) { | ||
@@ -142,23 +132,39 @@ node.dispatchEvent(new CustomEvent(gestureName, { | ||
} | ||
return false; | ||
} | ||
return setPointerControls(gestureName, node, onMove, onDown, null); | ||
if (parameters.composed) { | ||
return { | ||
onMove, | ||
onDown, | ||
onUp: null | ||
}; | ||
} | ||
return { | ||
...setPointerControls(gestureName, node, onMove, onDown, null, parameters.touchAction), | ||
update: updateParameters => { | ||
parameters = { | ||
...parameters, | ||
...updateParameters | ||
}; | ||
} | ||
}; | ||
} | ||
function getPointersDistance(activeEvents) { | ||
return Math.hypot(activeEvents[0].clientX - activeEvents[1].clientX, activeEvents[0].clientY - activeEvents[1].clientY); | ||
} | ||
function pinch(node) { | ||
function pinch(node, inputParameters) { | ||
const parameters = { | ||
touchAction: DEFAULT_TOUCH_ACTION, | ||
composed: false, | ||
...inputParameters | ||
}; | ||
const gestureName = 'pinch'; | ||
let prevDistance = null; | ||
let prevDistance; | ||
let initDistance = 0; | ||
let pinchCenter; | ||
function onUp(activeEvents) { | ||
if (activeEvents.length === 1) { | ||
prevDistance = null; | ||
prevDistance = undefined; | ||
} | ||
} | ||
function onDown(activeEvents) { | ||
@@ -170,8 +176,6 @@ if (activeEvents.length === 2) { | ||
} | ||
function onMove(activeEvents) { | ||
if (activeEvents.length === 2) { | ||
const curDistance = getPointersDistance(activeEvents); | ||
if (prevDistance !== null && curDistance !== prevDistance) { | ||
if (prevDistance !== undefined && curDistance !== prevDistance) { | ||
const scale = curDistance / initDistance; | ||
@@ -185,22 +189,28 @@ node.dispatchEvent(new CustomEvent(gestureName, { | ||
} | ||
prevDistance = curDistance; | ||
} | ||
return false; | ||
} | ||
return setPointerControls(gestureName, node, onMove, onDown, onUp); | ||
if (parameters.composed) { | ||
return { | ||
onMove, | ||
onDown, | ||
onUp: null | ||
}; | ||
} | ||
return setPointerControls(gestureName, node, onMove, onDown, onUp, parameters.touchAction); | ||
} | ||
function press(node, parameters) { | ||
parameters = { | ||
function press(node, inputParameters) { | ||
const parameters = { | ||
composed: false, | ||
timeframe: DEFAULT_DELAY, | ||
triggerBeforeFinished: false, | ||
...parameters | ||
spread: DEFAULT_PRESS_SPREAD, | ||
touchAction: 'auto', | ||
...inputParameters | ||
}; | ||
node.style.userSelect = 'none'; | ||
node.oncontextmenu = e => { | ||
e.preventDefault(); | ||
}; | ||
const gestureName = 'press'; | ||
@@ -210,3 +220,3 @@ let startTime; | ||
let clientY; | ||
let clientMoved = { | ||
const clientMoved = { | ||
x: 0, | ||
@@ -217,6 +227,21 @@ y: 0 | ||
let triggeredOnTimeout = false; | ||
let triggered = false; | ||
function onDone(eventX, eventY, event) { | ||
if (Math.abs(eventX - clientX) < parameters.spread && Math.abs(eventY - clientY) < parameters.spread && Date.now() - startTime > parameters.timeframe) { | ||
const rect = node.getBoundingClientRect(); | ||
const x = Math.round(eventX - rect.left); | ||
const y = Math.round(eventY - rect.top); | ||
triggered = true; | ||
node.dispatchEvent(new CustomEvent(gestureName, { | ||
detail: { | ||
x, | ||
y, | ||
target: event.target, | ||
pointerType: event.pointerType | ||
} | ||
})); | ||
} | ||
} | ||
function onUp(activeEvents, event) { | ||
clearTimeout(timeout); | ||
if (!triggeredOnTimeout) { | ||
@@ -226,9 +251,9 @@ onDone(event.clientX, event.clientY, event); | ||
} | ||
function onMove(activeEvents, event) { | ||
clientMoved.x = event.clientX; | ||
clientMoved.y = event.clientY; | ||
return triggered; | ||
} | ||
function onDown(activeEvents, event) { | ||
triggered = false; | ||
clientX = event.clientX; | ||
@@ -238,8 +263,7 @@ clientY = event.clientY; | ||
triggeredOnTimeout = false; | ||
clientMoved.x = event.clientX; | ||
clientMoved.y = event.clientY; | ||
if (parameters.triggerBeforeFinished) { | ||
timeout = setTimeout(() => { | ||
triggeredOnTimeout = true; | ||
clientMoved.x = event.clientX; | ||
clientMoved.y = event.clientY; | ||
onDone(clientMoved.x, clientMoved.y, event); | ||
@@ -249,19 +273,10 @@ }, parameters.timeframe + 1); | ||
} | ||
function onDone(eventX, eventY, event) { | ||
if (Math.abs(eventX - clientX) < 4 && Math.abs(eventY - clientY) < 4 && Date.now() - startTime > parameters.timeframe) { | ||
const rect = node.getBoundingClientRect(); | ||
const x = Math.round(eventX - rect.left); | ||
const y = Math.round(eventY - rect.top); | ||
node.dispatchEvent(new CustomEvent(gestureName, { | ||
detail: { | ||
x, | ||
y, | ||
target: event.target | ||
} | ||
})); | ||
} | ||
const onSharedDestroy = setPointerControls(gestureName, node, onMove, onDown, onUp, parameters.touchAction); | ||
if (parameters.composed) { | ||
return { | ||
onMove, | ||
onDown, | ||
onUp | ||
}; | ||
} | ||
const onSharedDestroy = setPointerControls(gestureName, node, onMove, onDown, onUp); | ||
return { | ||
@@ -274,5 +289,3 @@ destroy: () => { | ||
} | ||
function getPointersAngleDeg(activeEvents) { | ||
// instead of hell lot of conditions we use an object mapping | ||
const quadrantsMap = { | ||
@@ -290,2 +303,3 @@ left: { | ||
const height = activeEvents[0].clientY - activeEvents[1].clientY; | ||
/* | ||
@@ -305,15 +319,17 @@ In quadrants 1 and 3 allworks as expected. | ||
} | ||
function rotate(node) { | ||
function rotate(node, inputParameters) { | ||
const parameters = { | ||
touchAction: DEFAULT_TOUCH_ACTION, | ||
composed: false, | ||
...inputParameters | ||
}; | ||
const gestureName = 'rotate'; | ||
let prevAngle = null; | ||
let prevAngle; | ||
let initAngle = 0; | ||
let rotationCenter; | ||
function onUp(activeEvents) { | ||
if (activeEvents.length === 1) { | ||
prevAngle = null; | ||
prevAngle = undefined; | ||
} | ||
} | ||
function onDown(activeEvents) { | ||
@@ -328,15 +344,13 @@ if (activeEvents.length === 2) { | ||
} | ||
function onMove(activeEvents) { | ||
if (activeEvents.length === 2) { | ||
const curAngle = getPointersAngleDeg(activeEvents); | ||
if (prevAngle !== null && curAngle !== prevAngle) { | ||
if (prevAngle !== undefined && curAngle !== prevAngle) { | ||
// Make sure we start at zero, doesnt matter what is the initial angle of fingers | ||
let rotation = curAngle - initAngle; // instead of showing 180 - 360, we will show negative -180 - 0 | ||
let rotation = curAngle - initAngle; | ||
// instead of showing 180 - 360, we will show negative -180 - 0 | ||
if (rotation > 180) { | ||
rotation -= 360; | ||
} | ||
node.dispatchEvent(new CustomEvent(gestureName, { | ||
@@ -349,15 +363,23 @@ detail: { | ||
} | ||
prevAngle = curAngle; | ||
} | ||
return false; | ||
} | ||
return setPointerControls(gestureName, node, onMove, onDown, onUp); | ||
if (parameters.composed) { | ||
return { | ||
onMove, | ||
onDown, | ||
onUp | ||
}; | ||
} | ||
return setPointerControls(gestureName, node, onMove, onDown, onUp, parameters.touchAction); | ||
} | ||
function swipe(node, parameters = { | ||
timeframe: DEFAULT_DELAY, | ||
minSwipeDistance: DEFAULT_MIN_SWIPE_DISTANCE, | ||
touchAction: DEFAULT_TOUCH_ACTION | ||
}) { | ||
function swipe(node, inputParameters) { | ||
const parameters = { | ||
timeframe: DEFAULT_DELAY, | ||
minSwipeDistance: DEFAULT_MIN_SWIPE_DISTANCE, | ||
touchAction: DEFAULT_TOUCH_ACTION, | ||
composed: false, | ||
...inputParameters | ||
}; | ||
const gestureName = 'swipe'; | ||
@@ -368,3 +390,2 @@ let startTime; | ||
let target; | ||
function onDown(activeEvents, event) { | ||
@@ -374,3 +395,2 @@ clientX = event.clientX; | ||
startTime = Date.now(); | ||
if (activeEvents.length === 1) { | ||
@@ -380,3 +400,2 @@ target = event.target; | ||
} | ||
function onUp(activeEvents, event) { | ||
@@ -389,3 +408,2 @@ if (event.type === 'pointerup' && activeEvents.length === 0 && Date.now() - startTime < parameters.timeframe) { | ||
let direction = null; | ||
if (absX >= 2 * absY && absX > parameters.minSwipeDistance) { | ||
@@ -398,3 +416,2 @@ // horizontal (by *2 we eliminate diagonal movements) | ||
} | ||
if (direction) { | ||
@@ -410,9 +427,413 @@ node.dispatchEvent(new CustomEvent(gestureName, { | ||
} | ||
if (parameters.composed) { | ||
return { | ||
onMove: null, | ||
onDown, | ||
onUp | ||
}; | ||
} | ||
return setPointerControls(gestureName, node, null, onDown, onUp, parameters.touchAction); | ||
} | ||
function tap(node, parameters = { | ||
timeframe: DEFAULT_DELAY | ||
}) { | ||
function callAllByType(ListenerType, subGestureFunctions, activeEvents, event) { | ||
subGestureFunctions.forEach(gesture => { | ||
var _gesture$ListenerType; | ||
(_gesture$ListenerType = gesture[ListenerType]) === null || _gesture$ListenerType === void 0 ? void 0 : _gesture$ListenerType.call(gesture, activeEvents, event); | ||
}); | ||
} | ||
function composedGesture(node, gestureCallback) { | ||
const gestureFunctions = []; | ||
function registerGesture(gestureFn, parameters) { | ||
const subGestureFns = gestureFn(node, { | ||
...parameters, | ||
composed: true | ||
}); | ||
gestureFunctions.push(subGestureFns); | ||
return subGestureFns; | ||
} | ||
const onMoveCallback = gestureCallback(registerGesture); | ||
const gestureName = 'composedGesture'; | ||
function onUp(activeEvents, event) { | ||
callAllByType('onUp', gestureFunctions, activeEvents, event); | ||
} | ||
function onDown(activeEvents, event) { | ||
callAllByType('onDown', gestureFunctions, activeEvents, event); | ||
} | ||
function onMove(activeEvents, event) { | ||
onMoveCallback(activeEvents, event); | ||
return true; | ||
} | ||
return setPointerControls(gestureName, node, onMove, onDown, onUp); | ||
} | ||
const DEFAULT_TRESHOLD = 0.9; | ||
const DEFAULT_NB_OF_SAMPLE_POINTS = 64; | ||
const PHI = (Math.sqrt(5.0) - 1) / 2; | ||
const ANGLE_RANGE_RAD = deg2Rad(45.0); | ||
const ANGLE_PRECISION_RAD = deg2Rad(2.0); | ||
function deg2Rad(d) { | ||
return d * Math.PI / 180; | ||
} | ||
function getDistance(a, b) { | ||
const dx = b.x - a.x; | ||
const dy = b.y - a.y; | ||
return Math.sqrt(dx * dx + dy * dy); | ||
} | ||
function distanceAtBestAngle(pattern, points, center) { | ||
let fromAngleRad = -ANGLE_RANGE_RAD; | ||
let toAngleRad = ANGLE_RANGE_RAD; | ||
let angleOne = PHI * fromAngleRad + (1.0 - PHI) * toAngleRad; | ||
let distanceOne = distanceAtAngle(pattern, angleOne, points, center); | ||
let angleTwo = (1.0 - PHI) * fromAngleRad + PHI * toAngleRad; | ||
let distanceTwo = distanceAtAngle(pattern, angleTwo, points, center); | ||
while (Math.abs(toAngleRad - fromAngleRad) > ANGLE_PRECISION_RAD) { | ||
if (distanceOne < distanceTwo) { | ||
toAngleRad = angleTwo; | ||
angleTwo = angleOne; | ||
distanceTwo = distanceOne; | ||
angleOne = PHI * fromAngleRad + (1.0 - PHI) * toAngleRad; | ||
distanceOne = distanceAtAngle(pattern, angleOne, points, center); | ||
} else { | ||
fromAngleRad = angleOne; | ||
angleOne = angleTwo; | ||
distanceOne = distanceTwo; | ||
angleTwo = (1.0 - PHI) * fromAngleRad + PHI * toAngleRad; | ||
distanceTwo = distanceAtAngle(pattern, angleTwo, points, center); | ||
} | ||
} | ||
return Math.min(distanceOne, distanceTwo); | ||
} | ||
function distanceAtAngle(pattern, angle, points, center) { | ||
const strokePoints = rotateBy(angle, points, center); | ||
const d = strokePoints.reduce((accu, sPoint, i) => { | ||
return accu += getDistance(sPoint, pattern.points[i]); | ||
}, 0); | ||
return d / strokePoints.length; | ||
} | ||
function rotateBy(angle, points, center) { | ||
const cos = Math.cos(angle); | ||
const sin = Math.sin(angle); | ||
return points.map(point => { | ||
return { | ||
x: (point.x - center.x) * cos - (point.y - center.y) * sin + center.x, | ||
y: (point.x - center.x) * sin + (point.y - center.y) * cos + center.y | ||
}; | ||
}); | ||
} | ||
function shapeDetector(inputPatterns, options = {}) { | ||
const threshold = options.threshold || 0; | ||
const NUMBER_OF_SAMPLE_POINTS = options.nbOfSamplePoints || DEFAULT_NB_OF_SAMPLE_POINTS; | ||
const SQUARE_SIZE = 250; | ||
const HALF_SQUARE_DIAGONAL = Math.sqrt(SQUARE_SIZE ** 2 + SQUARE_SIZE ** 2) / 2; | ||
const patterns = inputPatterns.flatMap(pattern => { | ||
var _pattern$allowRotatio, _pattern$bothDirectio; | ||
return learn(pattern.name, pattern.points, (_pattern$allowRotatio = pattern.allowRotation) !== null && _pattern$allowRotatio !== void 0 ? _pattern$allowRotatio : false, (_pattern$bothDirectio = pattern.bothDirections) !== null && _pattern$bothDirectio !== void 0 ? _pattern$bothDirectio : true); | ||
}); | ||
function getStroke(points, name, allowRotation) { | ||
points = resample(); | ||
const center = getCenterPoint(); | ||
if (allowRotation) { | ||
points = rotateBy(-indicativeAngle(center), points, center); | ||
} | ||
points = scaleToSquare(); | ||
points = translateToOrigin(getCenterPoint()); | ||
return { | ||
name, | ||
points, | ||
center: { | ||
x: 0, | ||
y: 0 | ||
}, | ||
allowRotation | ||
}; | ||
function resample() { | ||
let localDistance, q; | ||
let distance = 0; | ||
const interval = strokeLength() / (NUMBER_OF_SAMPLE_POINTS - 1); | ||
const newPoints = [points[0]]; | ||
for (let i = 1; i < points.length; i++) { | ||
localDistance = getDistance(points[i - 1], points[i]); | ||
if (distance + localDistance >= interval) { | ||
q = { | ||
x: points[i - 1].x + (interval - distance) / localDistance * (points[i].x - points[i - 1].x), | ||
y: points[i - 1].y + (interval - distance) / localDistance * (points[i].y - points[i - 1].y) | ||
}; | ||
newPoints.push(q); | ||
points.splice(i, 0, q); | ||
distance = 0; | ||
} else { | ||
distance += localDistance; | ||
} | ||
} | ||
if (newPoints.length === NUMBER_OF_SAMPLE_POINTS - 1) { | ||
newPoints.push(points[points.length - 1]); | ||
} | ||
return newPoints; | ||
} | ||
function scaleToSquare() { | ||
const box = { | ||
minX: +Infinity, | ||
maxX: -Infinity, | ||
minY: +Infinity, | ||
maxY: -Infinity, | ||
width: 0, | ||
height: 0 | ||
}; | ||
points.forEach(point => { | ||
box.minX = Math.min(box.minX, point.x); | ||
box.minY = Math.min(box.minY, point.y); | ||
box.maxX = Math.max(box.maxX, point.x); | ||
box.maxY = Math.max(box.maxY, point.y); | ||
}); | ||
box.width = box.maxX - box.minX; | ||
box.height = box.maxY - box.minY; | ||
return points.map(point => { | ||
return { | ||
x: point.x * (SQUARE_SIZE / box.width), | ||
y: point.y * (SQUARE_SIZE / box.height) | ||
}; | ||
}); | ||
} | ||
function translateToOrigin(center) { | ||
return points.map(point => ({ | ||
x: point.x - center.x, | ||
y: point.y - center.y | ||
})); | ||
} | ||
function getCenterPoint() { | ||
const centre = points.reduce((acc, point) => { | ||
acc.x += point.x; | ||
acc.y += point.y; | ||
return acc; | ||
}, { | ||
x: 0, | ||
y: 0 | ||
}); | ||
centre.x /= points.length; | ||
centre.y /= points.length; | ||
return centre; | ||
} | ||
function indicativeAngle(center) { | ||
return Math.atan2(center.y - points[0].y, center.x - points[0].x); | ||
} | ||
function strokeLength() { | ||
let d = 0; | ||
for (let i = 1; i < points.length; i++) { | ||
d += getDistance(points[i - 1], points[i]); | ||
} | ||
return d; | ||
} | ||
} | ||
function detect(points, patternName = '') { | ||
const strokeRotated = getStroke(points, patternName, true); | ||
const strokeUnrotated = getStroke(points, patternName, false); | ||
let bestDistance = +Infinity; | ||
let bestPattern = null; | ||
let bestScore = 0; | ||
patterns.forEach(pattern => { | ||
if (pattern.name.indexOf(patternName) > -1) { | ||
const distance = pattern.allowRotation ? distanceAtBestAngle(pattern, strokeRotated.points, strokeRotated.center) : distanceAtAngle(pattern, 0, strokeUnrotated.points, strokeUnrotated.center); | ||
const score = 1.0 - distance / HALF_SQUARE_DIAGONAL; | ||
if (distance < bestDistance && score > threshold) { | ||
bestDistance = distance; | ||
bestPattern = pattern.name; | ||
bestScore = score; | ||
} | ||
} | ||
}); | ||
return { | ||
pattern: bestPattern, | ||
score: bestScore | ||
}; | ||
} | ||
function learn(name, points, allowRotation, bothDirections) { | ||
const response = [getStroke([...points], name, allowRotation)]; | ||
if (bothDirections) { | ||
response.push(getStroke([...points.reverse()], name, allowRotation)); | ||
} | ||
return response; | ||
} | ||
return { | ||
detect | ||
}; | ||
} | ||
function shapeGesture(node, inputParameters) { | ||
let parameters = { | ||
composed: false, | ||
shapes: [], | ||
threshold: DEFAULT_TRESHOLD, | ||
timeframe: 1000, | ||
nbOfSamplePoints: DEFAULT_NB_OF_SAMPLE_POINTS, | ||
touchAction: DEFAULT_TOUCH_ACTION, | ||
...inputParameters | ||
}; | ||
const gestureName = 'shapeGesture'; | ||
const detector = shapeDetector(parameters.shapes, { | ||
...parameters | ||
}); | ||
let startTime; | ||
let target; | ||
let stroke = []; | ||
function onDown(activeEvents, event) { | ||
startTime = Date.now(); | ||
target = event.target; | ||
stroke = []; | ||
} | ||
function onMove(activeEvents, event) { | ||
if (activeEvents.length === 1) { | ||
const rect = node.getBoundingClientRect(); | ||
const x = Math.round(event.clientX - rect.left); | ||
const y = Math.round(event.clientY - rect.top); | ||
stroke.push({ | ||
x, | ||
y | ||
}); | ||
} | ||
return false; | ||
} | ||
function onUp() { | ||
if (stroke.length > 2 && Date.now() - startTime < parameters.timeframe) { | ||
const detectionResult = detector.detect(stroke); | ||
node.dispatchEvent(new CustomEvent(gestureName, { | ||
detail: { | ||
...detectionResult, | ||
target | ||
} | ||
})); | ||
} | ||
} | ||
if (parameters.composed) { | ||
return { | ||
onMove, | ||
onDown, | ||
onUp | ||
}; | ||
} | ||
return { | ||
...setPointerControls(gestureName, node, onMove, onDown, onUp, parameters.touchAction), | ||
update: updateParameters => { | ||
parameters = { | ||
...parameters, | ||
...updateParameters | ||
}; | ||
} | ||
}; | ||
} | ||
function isScrollMode(event) { | ||
return event.pointerType === 'touch'; | ||
} | ||
function getScrollParent(node, direction) { | ||
if (!node) { | ||
return undefined; | ||
} | ||
const isElement = node instanceof HTMLElement; | ||
const overflowY = isElement && window.getComputedStyle(node).overflowY; | ||
const isScrollable = overflowY !== 'visible' && overflowY !== 'hidden'; | ||
const directionToDimension = { | ||
x: 'Width', | ||
y: 'Height' | ||
}; | ||
if (isScrollable && node[`scroll${directionToDimension[direction]}`] > node[`client${directionToDimension[direction]}`]) { | ||
return node; | ||
} else { | ||
return getScrollParent(node.parentNode, direction) || document.scrollingElement || document.body; | ||
} | ||
} | ||
function scroll(node, inputParameters) { | ||
let parameters = { | ||
...{ | ||
delay: DEFAULT_DELAY, | ||
touchAction: DEFAULT_TOUCH_ACTION, | ||
composed: false | ||
}, | ||
...inputParameters | ||
}; | ||
const gestureName = 'scroll'; | ||
let nearestScrollEl = { | ||
x: undefined, | ||
y: undefined | ||
}; | ||
let prevCoords; | ||
let scrollDelta = { | ||
x: 0, | ||
y: 0 | ||
}; | ||
let scrollDirectionPositive = { | ||
x: true, | ||
y: true | ||
}; | ||
function scrollElementTo(el, scrollValue, direction) { | ||
el === null || el === void 0 ? void 0 : el.scrollBy({ | ||
[direction === 'x' ? 'left' : 'top']: scrollValue, | ||
behavior: 'auto' | ||
}); | ||
} | ||
function onDown(activeEvents, event) { | ||
nearestScrollEl.y = getScrollParent(node, 'y'); | ||
nearestScrollEl.x = getScrollParent(node, 'x'); | ||
prevCoords = undefined; | ||
} | ||
function onMove(activeEvents, event) { | ||
if (activeEvents.length === 1 && isScrollMode(event)) { | ||
if (prevCoords !== undefined) { | ||
scrollDelta.y = Math.round(prevCoords.y - event.clientY); | ||
scrollDelta.x = Math.round(prevCoords.x - event.clientX); | ||
nearestScrollEl.y && scrollElementTo(nearestScrollEl.y, scrollDelta.y, 'y'); | ||
nearestScrollEl.x && scrollElementTo(nearestScrollEl.x, scrollDelta.x, 'x'); | ||
} | ||
prevCoords = { | ||
x: event.clientX, | ||
y: event.clientY | ||
}; | ||
} | ||
return false; | ||
} | ||
function onUp(activeEvents, event) { | ||
if (isScrollMode(event)) { | ||
if (scrollDelta.y || scrollDelta.x) { | ||
scrollDirectionPositive.y = scrollDelta.y > 0; | ||
scrollDirectionPositive.x = scrollDelta.x > 0; | ||
requestAnimationFrame(scrollOutLoop); | ||
} | ||
} | ||
} | ||
function scrollOutByDirection(direction) { | ||
if (!scrollDirectionPositive[direction] && scrollDelta[direction] < 0) { | ||
scrollDelta[direction] += 0.3; | ||
} else if (scrollDirectionPositive[direction] && scrollDelta[direction] > 0) { | ||
scrollDelta[direction] -= 0.3; | ||
} else { | ||
scrollDelta[direction] = 0; | ||
} | ||
if (scrollDelta[direction]) { | ||
scrollElementTo(nearestScrollEl[direction], scrollDelta[direction], direction); | ||
requestAnimationFrame(scrollOutLoop); | ||
} | ||
} | ||
function scrollOutLoop() { | ||
nearestScrollEl.x && scrollOutByDirection('x'); | ||
nearestScrollEl.y && scrollOutByDirection('y'); | ||
} | ||
if (parameters.composed) { | ||
return { | ||
onMove, | ||
onUp, | ||
onDown | ||
}; | ||
} | ||
return { | ||
...setPointerControls(gestureName, node, onMove, onDown, onUp, parameters.touchAction), | ||
update: updateParameters => { | ||
parameters = { | ||
...parameters, | ||
...updateParameters | ||
}; | ||
} | ||
}; | ||
} | ||
function tap(node, inputParameters) { | ||
const parameters = { | ||
timeframe: DEFAULT_DELAY, | ||
composed: false, | ||
touchAction: 'auto', | ||
...inputParameters | ||
}; | ||
const gestureName = 'tap'; | ||
@@ -422,3 +843,2 @@ let startTime; | ||
let clientY; | ||
function onUp(activeEvents, event) { | ||
@@ -438,3 +858,2 @@ if (Math.abs(event.clientX - clientX) < 4 && Math.abs(event.clientY - clientY) < 4 && Date.now() - startTime < parameters.timeframe) { | ||
} | ||
function onDown(activeEvents, event) { | ||
@@ -445,9 +864,16 @@ clientX = event.clientX; | ||
} | ||
return setPointerControls(gestureName, node, null, onDown, onUp); | ||
if (parameters.composed) { | ||
return { | ||
onMove: null, | ||
onDown, | ||
onUp | ||
}; | ||
} | ||
return setPointerControls(gestureName, node, null, onDown, onUp, parameters.touchAction); | ||
} | ||
exports.DEFAULT_DELAY = DEFAULT_DELAY; | ||
exports.DEFAULT_MIN_SWIPE_DISTANCE = DEFAULT_MIN_SWIPE_DISTANCE; | ||
exports.DEFAULT_PRESS_SPREAD = DEFAULT_PRESS_SPREAD; | ||
exports.DEFAULT_TOUCH_ACTION = DEFAULT_TOUCH_ACTION; | ||
exports.composedGesture = composedGesture; | ||
exports.getCenterOfTwoPoints = getCenterOfTwoPoints; | ||
@@ -458,4 +884,6 @@ exports.pan = pan; | ||
exports.rotate = rotate; | ||
exports.scroll = scroll; | ||
exports.setPointerControls = setPointerControls; | ||
exports.shapeGesture = shapeGesture; | ||
exports.swipe = swipe; | ||
exports.tap = tap; |
@@ -7,3 +7,6 @@ export * from './pan'; | ||
export * from './swipe'; | ||
export * from './composedGesture'; | ||
export * from './shapeGesture'; | ||
export * from './scroll'; | ||
export * from './tap'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -1,6 +0,6 @@ | ||
export declare function pan(node: HTMLElement, parameters?: { | ||
import { type SvelteAction, type SubGestureFunctions, type BaseParams } from './shared'; | ||
export type PanParameters = { | ||
delay: number; | ||
}): { | ||
destroy: () => void; | ||
}; | ||
} & BaseParams; | ||
export declare function pan(node: HTMLElement, inputParameters?: Partial<PanParameters>): SvelteAction | SubGestureFunctions; | ||
//# sourceMappingURL=pan.d.ts.map |
@@ -1,4 +0,4 @@ | ||
export declare function pinch(node: HTMLElement): { | ||
destroy: () => void; | ||
}; | ||
import { type SvelteAction, type SubGestureFunctions, type BaseParams } from './shared'; | ||
export type PinchParameters = BaseParams; | ||
export declare function pinch(node: HTMLElement, inputParameters?: Partial<PinchParameters>): SvelteAction | SubGestureFunctions; | ||
//# sourceMappingURL=pinch.d.ts.map |
@@ -1,7 +0,8 @@ | ||
export declare function press(node: HTMLElement, parameters: { | ||
timeframe?: number; | ||
triggerBeforeFinished?: boolean; | ||
}): { | ||
destroy: () => void; | ||
}; | ||
import { type SvelteAction, type SubGestureFunctions, type BaseParams } from './shared'; | ||
export type PressParameters = { | ||
timeframe: number; | ||
triggerBeforeFinished: boolean; | ||
spread: number; | ||
} & BaseParams; | ||
export declare function press(node: HTMLElement, inputParameters?: Partial<PressParameters>): SvelteAction | SubGestureFunctions; | ||
//# sourceMappingURL=press.d.ts.map |
@@ -1,4 +0,4 @@ | ||
export declare function rotate(node: HTMLElement): { | ||
destroy: () => void; | ||
}; | ||
import { type SvelteAction, type SubGestureFunctions, type BaseParams } from './shared'; | ||
export type RotateParameters = BaseParams; | ||
export declare function rotate(node: HTMLElement, inputParameters?: Partial<RotateParameters>): SvelteAction | SubGestureFunctions; | ||
//# sourceMappingURL=rotate.d.ts.map |
export declare const DEFAULT_DELAY = 300; | ||
export declare const DEFAULT_PRESS_SPREAD = 4; | ||
export declare const DEFAULT_MIN_SWIPE_DISTANCE = 60; | ||
export declare const DEFAULT_TOUCH_ACTION = "none"; | ||
export type TouchAction = 'auto' | 'none' | 'pan-x' | 'pan-left' | 'pan-right' | 'pan-y' | 'pan-up' | 'pan-down' | 'pinch-zoom' | 'manipulation' | 'inherit' | 'initial' | 'revert' | 'revert-layer' | 'unset'; | ||
export type BaseParams = { | ||
composed: boolean; | ||
touchAction: TouchAction; | ||
}; | ||
export type SvelteAction = { | ||
update?: (parameters: any) => void; | ||
destroy?: () => void; | ||
}; | ||
export type PointerEventCallback<T> = ((activeEvents: PointerEvent[], event: PointerEvent) => T) | null; | ||
export type SubGestureFunctions = { | ||
onMove: PointerEventCallback<boolean>; | ||
onUp: PointerEventCallback<void>; | ||
onDown: PointerEventCallback<void>; | ||
}; | ||
export declare function getCenterOfTwoPoints(node: HTMLElement, activeEvents: PointerEvent[]): { | ||
@@ -8,5 +24,5 @@ x: number; | ||
}; | ||
export declare function setPointerControls(gestureName: string, node: HTMLElement, onMoveCallback: (activeEvents: PointerEvent[], event: PointerEvent) => void, onDownCallback: (activeEvents: PointerEvent[], event: PointerEvent) => void, onUpCallback: (activeEvents: PointerEvent[], event: PointerEvent) => void, touchAction?: string): { | ||
export declare function setPointerControls(gestureName: string, node: HTMLElement, onMoveCallback: PointerEventCallback<boolean>, onDownCallback: PointerEventCallback<void>, onUpCallback: PointerEventCallback<void>, touchAction?: TouchAction): { | ||
destroy: () => void; | ||
}; | ||
//# sourceMappingURL=shared.d.ts.map |
@@ -1,8 +0,8 @@ | ||
export declare function swipe(node: HTMLElement, parameters?: { | ||
import { type SvelteAction, type SubGestureFunctions, type BaseParams } from './shared'; | ||
export type SwipeParameters = { | ||
timeframe: number; | ||
minSwipeDistance: number; | ||
touchAction: string; | ||
}): { | ||
destroy: () => void; | ||
}; | ||
} & BaseParams; | ||
export declare function swipe(node: HTMLElement, inputParameters?: Partial<SwipeParameters>): SvelteAction | SubGestureFunctions; | ||
//# sourceMappingURL=swipe.d.ts.map |
@@ -1,6 +0,6 @@ | ||
export declare function tap(node: HTMLElement, parameters?: { | ||
import { type SvelteAction, type SubGestureFunctions, type BaseParams } from './shared'; | ||
export type TapParameters = { | ||
timeframe: number; | ||
}): { | ||
destroy: () => void; | ||
}; | ||
} & BaseParams; | ||
export declare function tap(node: HTMLElement, inputParameters?: Partial<TapParameters>): SvelteAction | SubGestureFunctions; | ||
//# sourceMappingURL=tap.d.ts.map |
@@ -6,3 +6,3 @@ { | ||
"license": "MIT", | ||
"version": "1.4.1", | ||
"version": "1.5.0", | ||
"main": "dist/index.js", | ||
@@ -17,8 +17,10 @@ "module": "dist/index.esm.js", | ||
"devDependencies": { | ||
"@babel/core": "^7.13.16", | ||
"@babel/preset-typescript": "^7.14.5", | ||
"@rollup/plugin-babel": "^5.3.0", | ||
"@rollup/plugin-node-resolve": "^13.0.2", | ||
"rollup": "^2.53.3", | ||
"typescript": "4.1.3" | ||
"@babel/core": "^7.21.4", | ||
"@babel/preset-typescript": "^7.21.4", | ||
"@rollup/plugin-babel": "^6.0.3", | ||
"@rollup/plugin-node-resolve": "^15.0.2", | ||
"rollup": "^3.20.6", | ||
"typescript": "5.0.4", | ||
"@babel/plugin-proposal-optional-chaining": "^7.21.0", | ||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6" | ||
}, | ||
@@ -25,0 +27,0 @@ "scripts": { |
335
README.md
# svelte-gestures | ||
1.5 KB gzipped - collection of gesture recognisers for Svelte. | ||
3 KB gzipped (you can use just part of those 3 KB) - a collection of gesture recognizers for Svelte. It can be actually used in any framework or native JS as it does not use any specific Svelte syntax at all ;) | ||
##### New in svelte-gestures 1.5: | ||
- New `composedGesture` lets you combine gestures. You can even use it to maintain scrolling behavior on elements with `pan` or `shapeGesture` (see example below) | ||
- New `shapeGesture` lets you define shape/s to be recognized. Just define shapes by coordinates. | ||
- Bugfixes | ||
## installation | ||
@@ -11,31 +17,48 @@ | ||
If you use Svelte language tools (Svelte for VS Code for instance) and you would appreciate seeing return types from svelte-gestures actions `<div on:swipe={fn} >` in your markup, add following line to your `global.d.ts` : | ||
If you use Svelte language tools (Svelte for VS Code for instance) and you would appreciate seeing return types from svelte-gestures actions `<div on:swipe={fn} >` in your markup, add the following line to your `global.d.ts` : | ||
`/// <reference types="svelte-gestures" />` | ||
if you use svelte kit, you already have a `global.d.ts` in `src` folder. Just add this line after | ||
if you use the Svelte kit, you already have a `global.d.ts` in `src` folder. Just add this line after | ||
`/// <reference types="@sveltejs/kit" />` | ||
It must be done this way as language tools use global types and regular npm package cannot expose global types as long as it is used by other package. | ||
It must be done this way as language tools use global types and a regular npm package cannot expose global types as long as it is used by another package. | ||
## About | ||
It contains the most popular gestures: `pan`, `pinch`, `press`, `rotate`, `swipe`, `tap`. It also exposes generic event handling core, which can be extended for your own specific gesture implementation (see sourcecode how gestures are implemented). | ||
It contains the most popular gestures: `pan`, `pinch`, `press`, `rotate`, `swipe`, `tap`. Besides that, it comes with `shapeGesture` which helps with the recognition of custom shapes declared by a set of x and y coordinates. | ||
It uses pointer events under the hood, to make it really cross platform. Gestures will be recognized, if done by mouse, touche, stylus etc. | ||
It also exposes a generic event handling core, which can be extended for your specific gesture implementation (see source code on how gestures are implemented). | ||
Besides above mentioned gestures, there are two more: `composedGesture` and `scroll` gestures: | ||
Recognizers are kept as simple as possible, but still providing desired basic functionality. They are made in form of svelte actions with custom event emiters. **Any number of different recognizers can be used on one element**. | ||
1. The `composedGesture` let you combine more gestures while using just one pointer EventListener per element. It also lets users switch active gestures on the fly. | ||
2. `scroll` is a custom basic implementation of scrolling for touch devices. It is needed, as by default, when a gesture mode is activated on an Element on a touch device, browser scrolling is turned off for that Element. Unfortunately, the gesture mode needs to be set before the first touch/click is done and cannot be changed while there are active pointers. The `scroll` gesture is made to work with the `composedGesture`. | ||
It uses pointer events under the hood, to make it cross-platform. Gestures will be recognized if done by mouse, touch, stylus etc. | ||
Recognizers are kept as simple as possible but still provide desired basic functionality. They are made in the form of svelte actions with custom event emitters. **Any number of different recognizers can be used on one element**, but it is recommended to use `composedGesture` for combined gestures. | ||
## API events | ||
Except main event, each resogniser triggers, three more events with names composed from action name (`pan` | `pinch` | `tap` | `swipe` | `rotate`) and event type (`up` | `down` | `move`). | ||
Except for the main event, each recognizer triggers, three more events with names composed of action name (`pan` | `pinch` | `tap` | `swipe` | `rotate` | `shapeGesture` | `composedGesture`) and event type (`up` | `down` | `move`). | ||
For example `pan` action has for example `panup`, `pandown`, `panmove`. It dispatches `event.detail` with following property `{ event: PointerEvent, pointersCount: number }`. First is native pointer event, second is number of active pointers. | ||
For example `pan` action has for example `panup`, `pandown`, `panmove`. It dispatches `event.`detail` with the following property`{` event: PointerEvent, pointersCount: number , target:HTMLElement}`. First is a native pointer event; the second is the number of active pointers; third is the target Element on which the gesture started (it can be a child of the element on which a gesture is applied) | ||
## Pan | ||
Pan action fires `pan` event: `event.detail` has `x`, `y` and `target` properties (x,y stand for position withing the `element` on which the action is used). `target` is an EventTarget (HTMLElement) of the pan. The target is recorded when pan starts. | ||
Pan action (on:pan) fires `pan` event: | ||
It is triggered on pointer (mouse, touch, etc.) move. But not earlier than `delay` parameter. The `delay` parameter is optional. If used it overwrites 300ms default value. It prevents triggering of tap or swipe gestures when combined on single element. | ||
- `event.detail` object has the following properties | ||
- `x`, `y` (x,y stand for position within the `element`` on which the action is used) | ||
- `target` is an EventTarget (HTMLElement) of the pan. The target is recorded when the pan starts. | ||
The `pan` accepts the following options | ||
- `delay` (default value is 300ms) | ||
- `touchAction` (defaults value is `none`) Apply css _touch-action_ style, letting the browser know which type of gesture is controlled by the browser and your program respectively. | ||
- `composed` is only applicable when used inside `composedGesture`. | ||
on:pan is triggered on the pointer (mouse, touch, etc.) move. But not earlier than `delay` parameter. | ||
[> repl Pan demo](https://svelte.dev/repl/5e8586cb44e54244948f1cd34ee379b3?version=3.38.2) | ||
@@ -68,6 +91,14 @@ | ||
Pinch action fires `pinch` event: `event.detail` with properties `{scale:number; center: {x:number; y:number;}}`. Initial scale after first two registered points is 1, then it either decrease toward zero as the points get nearer, or grow up as their distance grows. | ||
Pinch action (on:pinch) fires `pinch` event: | ||
`x` and `y` represents coordinates in `px` of an imaginary center of the pinch gesture. They originate in top left corner of the element on which pinch is used. | ||
- `event.detail` object has following properties | ||
- `center`: {x:number; y:number;}} | ||
- `x` and `y` represent coordinates in `px` of an imaginary center of the pinch gesture. They originate in the top left corner of the element on which pinch is used. | ||
- `scale`: number. The initial scale after the first two registered points is 1, then it either decreases toward zero as the points get nearer, or grow up as their distance grows. | ||
The `pinch` accepts the following options | ||
- `touchAction` (defaults value is `none`) Apply css _touch-action_ style, letting the browser know which type of gesture is controlled by the browser and your program respectively. | ||
- `composed` is only applicable when used inside `composedGesture`. | ||
[> repl Pinch demo](https://svelte.dev/repl/6f6d34e2b4ab420ab4e192a5046c86b4?version=3.38.2) | ||
@@ -101,8 +132,14 @@ | ||
Rotate action fires `rotate` event: `event.detail`. with properties`{rotation:number; center: {x:number;y:number;}}`. Initial rotation after first two registered points is 0, then it either decrease to -180 as the points rotate anticlockwise, or grow up 180 as they rotate clockwise. | ||
Rotate action (on:rotate) fires `rotate` event: | ||
`x` and `y` represents coordinates in `px` of an imaginary center of the rotation gesture. They originate in top left corner of the element on which rotate is used. | ||
- `event.detail` object has the following properties | ||
- `center`: {x:number; y:number;}} | ||
- `x` and `y` represent coordinates in `px` of an imaginary center of the rotation gesture. They originate in the top left corner of the element on which rotation is used. | ||
- `rotation`: number. Initial rotation after the first two registered points is 0, then it either decreases to -180 as the points rotate anti-clockwise or grows up to 180 as they rotate clockwise. | ||
`event.detail.rotation` represents angle between -180 and 180 degrees. | ||
The `rotate` accepts the following options | ||
- `touchAction` (defaults value is `none`) Apply css _touch-action_ style, letting the browser know which type of gesture is controlled by the browser and your program respectively. | ||
- `composed` is only applicable when used inside `composedGesture`. | ||
[> repl Rotation demo](https://svelte.dev/repl/498077b73d384910825719cd27254f8c?version=3.38.2) | ||
@@ -123,3 +160,2 @@ | ||
</script> | ||
<div | ||
@@ -137,10 +173,21 @@ use:rotate | ||
Swipe action fires `swipe` event: `event.detail`. With properties `direction` and target. `target` is an EventTarget (HTMLElement) of the swipe action. The target is recorded when swipe starts. | ||
It accepts props as parameter: `{ timeframe: number; minSwipeDistance: number; touchAction: string }` with default values 300ms, 60px and `none`. | ||
Swipe is fired if preset distance in proper direction is done in preset time. | ||
You can use the [touchAction](https://developer.mozilla.org/en/docs/Web/CSS/touch-action) parameter to control the default behaviour of the browser. | ||
For example if you only use left/right swipe and want to keep the default browser behaviour (scrolling) for up/down swipe use `touchAction: 'pan-y'`. | ||
Swipe action (on:swipe) fires `swipe` event: | ||
`event.detail.direction` represents direction of swipe: 'top' | 'right' | 'bottom' | 'left' | ||
- `event.detail` object has following properties | ||
- `direction`: 'top' | 'right' | 'bottom' | 'left' | ||
- `target`: HTMLElement. The target is recorded when swipe starts. | ||
The `swipe` accepts the following options | ||
- `timeframe`:number (default value is *300*ms ) | ||
- `minSwipeDistance`: number (default value is *60*px) | ||
- `touchAction` (defaults value is `none`) Apply css _touch-action_ style, letting the browser know which type of gesture is controlled by the browser and your program respectively. | ||
- `composed` is only applicable when used inside `composedGesture`. | ||
Swipe is fired if the preset distance in the proper direction is done in the preset time. | ||
You can use the [touchAction](https://developer.mozilla.org/en/docs/Web/CSS/touch-action) parameter to control the default behavior of the browser. | ||
For example, if you only use left/right swipe and want to keep the default browser behavior (scrolling) for up/down swipe use `touchAction: 'pan-y'`. | ||
[> repl Swipe demo](https://svelte.dev/repl/f696ca27e6374f2cab1691727409a31d?version=3.38.2) | ||
@@ -150,10 +197,10 @@ | ||
<script> | ||
import { swipe } from 'svelte-gestures'; | ||
let direction; | ||
let target; | ||
import { swipe } from 'svelte-gestures'; | ||
let direction; | ||
let target; | ||
function handler(event) { | ||
direction = event.detail.direction; | ||
target = event.detail.target; | ||
} | ||
function handler(event) { | ||
direction = event.detail.direction; | ||
target = event.detail.target; | ||
} | ||
</script> | ||
@@ -164,2 +211,3 @@ | ||
</div> | ||
``` | ||
@@ -169,6 +217,17 @@ | ||
Tap action fires `tap` event: `event.detail` has `x`, `y` and `target` properties (x,y stand for position withing the `element` on which the action is used). `target` is an EventTarget (HTMLElement) of the tap. | ||
Tap action (on:tap) fires `tap` event: | ||
Tap action is fired only when the click/touch is finished within the give `timeframe`, the parameter is optional and overwrites defalut value of 300ms. | ||
- `event.detail` object has the following properties | ||
- `x`: number. X coordinate | ||
- `y`: number. Y coordinate | ||
- `target`: HTMLElement. | ||
The `pinch` accepts the following options | ||
- `timeframe`:number (default value is *300*ms ) | ||
- `touchAction` (defaults value is `auto`) Apply css _touch-action_ style, letting the browser know which type of gesture is controlled by the browser and your program respectively. | ||
- `composed` is only applicable when used inside `composedGesture`. | ||
Tap action is fired only when the click/touch is finished within the given `timeframe`. | ||
[> repl Tap demo](https://svelte.dev/repl/98ec4843c217499b9dcdd3bf47a706f0?version=3.38.2) | ||
@@ -178,15 +237,15 @@ | ||
<script> | ||
import { tap } from 'svelte-gestures'; | ||
import { tap } from 'svelte-gestures'; | ||
let x; | ||
let y; | ||
let target; | ||
let x; | ||
let y; | ||
let target; | ||
function handler(event) { | ||
x = event.detail.x; | ||
y = event.detail.y; | ||
target = event.detail.target; | ||
} | ||
function handler(event) { | ||
x = event.detail.x; | ||
y = event.detail.y; | ||
target = event.detail.target; | ||
} | ||
</script> | ||
<div use:tap={{ timeframe: 300 }} on:tap={handler} style="width:500px;height:500px;border:1px solid black;"> | ||
@@ -199,8 +258,20 @@ tap: {x} {y} | ||
Press action fires `press` event: `event.detail` has `x`, `y`, `target` properties (x,y stand for position withing the `element` on which the action is used). `target` is an EventTarget (HTMLElement) of the press. | ||
Press action (on:press) fires `press` event: | ||
Press action is fired only when the click/touch is finished after the give `timeframe`, the parameter is optional and overwrites defalut value of 300ms. | ||
- `event.detail` object has the following properties | ||
- `x`: number. X coordinate | ||
- `y`: number. Y coordinate | ||
- `target`: HTMLElement. | ||
- `pointerType`: 'touch' | 'mouse' | 'pen'. | ||
Another option is `triggerBeforeFinished`. By default it is set to `false`. If set to true, press event is triggered after given `timeframe`, even if user still keeps pressing (event hasn't finished). | ||
The `press` accepts the following options | ||
- `triggerBeforeFinished`: boolean (default value is `false`). If set to true, the press event is triggered after the given `timeframe`, even if a user still keeps pressing (event hasn't finished). | ||
- `timeframe`:number (default value is *300*ms ) | ||
- `spread`: number; (default value is *4*px). If a user moves farther than the `spread` value from the initial touch point, the event is never triggered. | ||
- `touchAction` (defaults value is `auto`) Apply css _touch-action_ style, letting the browser know which type of gesture is controlled by the browser and your program respectively. | ||
- `composed` is only applicable when used inside `composedGesture`. | ||
Press action is fired only when the click/touch is released after the given `timeframe`. Or when `triggerBeforeFinished` is set to `true`, after given `timeframe` even when click/touch continues. | ||
[> repl Press demo](https://svelte.dev/repl/8bef691ad59f4b2285d2b8a6df5d178a?version=3.38.2) | ||
@@ -210,15 +281,169 @@ | ||
<script> | ||
import { press } from 'svelte-gestures'; | ||
import { press } from 'svelte-gestures'; | ||
let x; | ||
let y; | ||
let target; | ||
function handler(event) { | ||
x = event.detail.x; | ||
y = event.detail.y; | ||
target = event.detail.target | ||
} | ||
</script> | ||
<div use:press={{ timeframe: 300, triggerBeforeFinished: false }} on:press={handler} style="width:500px;height:500px;border:1px solid black;"> | ||
press: {x} {y} | ||
</div> | ||
``` | ||
## Shape gesture | ||
ShapeGesture action (on:shapeGesture) fires `shapeGesture` event: | ||
- `event.detail` object has the following properties | ||
- `score`: number. A number between 0 and 1. The higher the number is, the bigger chance that shape has been recognized. | ||
- `pattern`: string | null. `name` of pattern with best match. `null` in case there is no match | ||
- `target`: HTMLElement. | ||
The `shapeGesture` accepts the following options | ||
- `shapes`: `{ | ||
name: string; | ||
points: { x: number; y: number }[]; | ||
allowRotation?: boolean (default `false`) | ||
bothDirections?: boolean (default `true`) | ||
}[]` | ||
- `timeframe`:number (default value is *1000*ms ). Time within which the gesture need to be done. | ||
- `threshold`:number (default value is 0.9 ). Possible values are between 0 and 1; The higher the threshold is the more precise the gesture needs to be drawn, to trigger the `shapeGesture` action. | ||
- `nbOfSamplePoints`: number (default 64). The number of points the gesture is converted to before the match is done. | ||
- `touchAction` (defaults value is `auto`) Apply css _touch-action_ style, letting the browser know which type of gesture is controlled by the browser and your program respectively. | ||
- `composed` is only applicable when used inside `composedGesture`. | ||
`shapeGesture` action is fired only when the click/touch is finished within the given `timeframe` and gesture similarity is above the `threshold` | ||
##### Tips and hints | ||
1. When defining points in a shape, beware that the coordinants system is same as for SVG. **x increases toward right** and **y increases toward bottom** !! | ||
2. `shapeGesture` can accept more shapes at once. It's not only handy to recognize more gestures, but can be used to define more similar shapes with same `name`. For instance if you need to recognize a triangle shape, it is preferable to define several slightly different triangles with same name, rather than defining one triangle shape and lowering the `threshold`. | ||
3. You don't need to care about scale of your shapes, they are always scaled automaticaly for gesture/shape comparison. | ||
4. When `bothDirections` is set to false, order of points matters, even if the shape is closed (circle, suare, etc) | ||
[> repl ShapeGesture demo](https://svelte.dev/repl/3634b5a64a74418ebb2ce35ec766a30e?version=3.59.1) | ||
```html | ||
<script> | ||
import { shapeGesture } from 'svelte-gestures'; | ||
const shapeOptions = { | ||
threshold: 0.5, | ||
shapes: [ | ||
{ | ||
name: 'triangle', | ||
allowRotation: true, | ||
points: [ | ||
{ x: 0, y: 0 }, | ||
{ x: 50, y: 100 }, | ||
{ x: 100, y: 0 }, | ||
{ x: 0, y: 0 }, | ||
], | ||
}, | ||
{ | ||
name: 'right-down', | ||
points: [ | ||
{ x: 0, y: 0 }, | ||
{ x: 100, y: 0 }, | ||
{ x: 100, y: 100 }, | ||
], | ||
}, | ||
{ | ||
name: 'up-right', | ||
bothDirections: false, | ||
points: [ | ||
{ x: 0, y: 100 }, | ||
{ x: 0, y: 0 }, | ||
{ x: 100, y: 0 }, | ||
], | ||
}, | ||
], | ||
}; | ||
let result; | ||
function handler(event) { | ||
result = event.detail; | ||
} | ||
</script> | ||
<div | ||
use:shapeGesture="{shapeOptions}" | ||
on:shapeGesture="{handler}" | ||
style="width:500px;height:500px;background:#ddd;" | ||
> | ||
{#if result?.score} There is <b>{(result.score * 100).toFixed(0)}%</b> chance | ||
you have drawn a <b>{result.pattern}</b> shape {/if} | ||
</div> | ||
``` | ||
## Composed Gesture | ||
`composedGesture` is a special gesture, which does not listen to any gesture of its own. It rather gives you the power of composing gestures together or switching gestures while a gesture is recorded. | ||
##### Usage | ||
To use `composedGesture`, you need to pass a function definition to the `use:composedGesture`. The function definition should have the following signature: | ||
- `(register: RegisterGestureType) => (activeEvents: PointerEvent[], event: PointerEvent) => boolean` | ||
The `register` parameter is a callback function provided by `composedGesture`, and it has the following signature: | ||
- `(gestureFn: (node: HTMLElement, params: BaseParams) => { onMove: PointerEventCallback<boolean>; onUp: PointerEventCallback<void>; onDown: PointerEventCallback<void>;}, parameters: BaseParams) => void` | ||
Within the function body, you can call the `register` function to add different gestures to the composed gestures. The `register` function accepts two arguments: the first argument is the gesture you want to register, and the second argument is an options object for the gesture. | ||
You can register multiple gestures using the `register` function, and each call to `register` returns an object with `onDown`, `onMove`, and `onUp` properties. The `onDown` and `onUp` functions are automatically executed by `composedGesture`, while the `onMove` function needs to be explicitly triggered by returning a callback function from the option function. This callback function should run all the necessary `onMove` functions for the gestures. You can implement your logic to determine which gesture to execute under which conditions. | ||
##### Example: panning combined with scrolling | ||
Let's use `pan` gesture, but only after the press gesture has been successfully triggered; otherwise, we will trigger the special `scroll` gesture which mimics the default scroll behavior (it is needed, because default scrolling need to be disabled on elements where any kind of swiping gesture is done). The result will be, that a fast swipe over the element will let the user scroll thru as normal, while a move initiated with 100ms press, will end up with panning. | ||
[> repl ComposedGesture demo](https://svelte.dev/repl/bb47278283564ed08e36677d8b43186c?version=3.38.2) | ||
```html | ||
<script lang="ts"> | ||
import { | ||
press, | ||
pan, | ||
scroll, | ||
composedGesture, | ||
type RegisterGestureType, | ||
type GestureCallback, | ||
} from 'svelte-gestures'; | ||
let x; | ||
let y; | ||
let target; | ||
const scrollPan: GestureCallback = (register: RegisterGestureType) => { | ||
const pressFns = register(press, { | ||
triggerBeforeFinished: true, | ||
spread: 10, | ||
timeframe: 100, | ||
}); | ||
const scrollFns = register(scroll, { delay: 0 }); | ||
const panFns = register(pan, { delay: 0 }); | ||
return (activeEvents: PointerEvent[], event: PointerEvent) => { | ||
pressFns.onMove(activeEvents, event) || event.pointerType !== 'touch' | ||
? panFns.onMove(activeEvents, event) | ||
: scrollFns.onMove(activeEvents, event); | ||
}; | ||
}; | ||
function handler(event) { | ||
x = event.detail.x; | ||
y = event.detail.y; | ||
target = event.detail.target | ||
} | ||
</script> | ||
<div use:press={{ timeframe: 300, triggerBeforeFinished: false }} on:press={handler} style="width:500px;height:500px;border:1px solid black;"> | ||
<div | ||
use:composedGesture="{scrollPan}" | ||
on:pan="{handler}" | ||
style="width:500px;height:500px;border:1px solid black;" | ||
> | ||
press: {x} {y} | ||
@@ -228,5 +453,5 @@ </div> | ||
# Custom gestures | ||
# Your own gestures | ||
You are encouraged to define your own custom gestures. There is a `setPointerControls` function exposed by the `svelte-gestures`. It handle all the events registration/deregistration needed for handling gestures; you just need to pass callbacks in it. | ||
You are encouraged to define your own custom gestures. There is a `setPointerControls` function exposed by the `svelte-gestures`. It handles all the events registration/deregistration needed for handling gestures; you just need to pass callbacks in it. | ||
@@ -243,5 +468,5 @@ ```typescript | ||
You can pass `null` instead of a callback if you dont need to call it in that event. In double tap example below you actually do not need any events related to move, as they are irrelevant for tapping. | ||
You can pass `null` instead of a callback if you don't need to call it in that event. In a double tap example below you do not need any events related to move, as they are irrelevant for tapping. | ||
See how doubletap custome gesture is implemented: | ||
See how a doubletap custom gesture is implemented: | ||
@@ -311,7 +536,5 @@ [> repl Custom gesture (doubletap) demo](https://svelte.dev/repl/c56082d9d056460d80e53cd71efddefe?version=3.38.2) | ||
} | ||
return setPointerControls(gestureName, node, null, onDown, onUp); | ||
} | ||
</script> | ||
<div | ||
@@ -318,0 +541,0 @@ use:doubletap |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
88866
31
1871
538
8