UNPKG

neft

Version:

JavaScript. Everywhere.

453 lines (362 loc) 13.3 kB
'use strict' utils = require 'src/utils' signal = require 'src/signal' WHEEL_DIVISOR = 3 MIN_POINTER_DELTA = 7 module.exports = (impl) -> {Types} = impl impl._scrollableUsePointer ?= true impl._scrollableUseWheel ?= true ### Scroll container by given x and y deltas ### scroll = do -> (item, x=0, y=0) -> deltaX = getDeltaX item, x deltaY = getDeltaY item, y x = Math.round item._contentX - deltaX y = Math.round item._contentY - deltaY x = getLimitedX item, x y = getLimitedY item, y if item._contentX isnt x or item._contentY isnt y item.contentX = x item.contentY = y return true false getDeltaX = (item, x) -> x / item._impl.globalScale getDeltaY = (item, y) -> y / item._impl.globalScale getLimitedX = (item, x) -> max = item._impl.contentItem._width - item._width Math.round Math.max(0, Math.min(max, x)) getLimitedY = (item, y) -> max = item._impl.contentItem._height - item._height Math.round Math.max(0, Math.min(max, y)) getItemGlobalScale = (item) -> val = item.scale while item = item.parent val *= item.scale val createContinuous = (item, prop) -> MIN_DISTANCE_TO_SNAP = 4 velocity = 0 amplitude = 0 timestamp = 0 target = 0 reversed = false shouldSnap = false lastSnapTargetProp = do -> switch prop when 'x' 'lastSnapTargetX' when 'y' 'lastSnapTargetY' scrollAxis = do -> switch prop when 'x' (val) -> scroll item, val, 0 when 'y' (val) -> scroll item, 0, val contentProp = do -> switch prop when 'x' '_contentX' when 'y' '_contentY' positionProp = do -> switch prop when 'x' '_x' when 'y' '_y' sizeProp = do -> switch prop when 'x' '_width' when 'y' '_height' anim = -> if amplitude isnt 0 elapsed = Date.now() - timestamp if shouldSnap targetDelta = item[contentProp] - target if (amplitude < 0 and targetDelta < 0) or (amplitude > 0 and targetDelta > 0) amplitude = -amplitude if reversed amplitude *= 0.3 else reversed = true delta = -amplitude * 0.7 * Math.exp(-elapsed / 325) if shouldSnap if targetDelta > MIN_DISTANCE_TO_SNAP or targetDelta < -MIN_DISTANCE_TO_SNAP if (delta > 0 and delta < MIN_DISTANCE_TO_SNAP) or (delta is 0 and targetDelta > 0) delta = Math.min(targetDelta, 7) else if (delta < 0 and delta > -7) or delta is 0 delta = Math.max(targetDelta, -7) if (not shouldSnap or targetDelta > MIN_DISTANCE_TO_SNAP or targetDelta < -MIN_DISTANCE_TO_SNAP) and (delta > 0.5 or delta < -0.5) scrollAxis delta requestAnimationFrame anim else scrollAxis targetDelta return getSnapTarget = (contentPos) -> children = item._snapItem?._children or item._contentItem?._children minDiff = Infinity minVal = 0 if children child = children.firstChild while child diff = contentPos - child[positionProp] if velocity > 0 diff += child[sizeProp] * 0.5 else diff -= child[sizeProp] * 0.5 if velocity >= 0 and diff >= 0 or velocity <= 0 and diff <= 0 diff = Math.abs diff if diff < minDiff minDiff = diff minVal = child[positionProp] child = child.nextSibling if minDiff is Infinity child?[positionProp] or 0 else minVal press: -> velocity = amplitude = 0 reversed = false timestamp = Date.now() release: -> data = item._impl {snap} = data if Math.abs(velocity) < 5 return amplitude = 0.8 * velocity timestamp = Date.now() target = item[contentProp] + amplitude * 4 if snap snapTarget = getSnapTarget target shouldSnap = data[lastSnapTargetProp] isnt snapTarget if shouldSnap target = snapTarget data[lastSnapTargetProp] = snapTarget shouldAnimate = Math.abs(velocity) > 10 shouldAnimate ||= snap and target is snapTarget if shouldAnimate anim() return update: (val) -> now = Date.now() elapsed = now - timestamp timestamp = now v = 100 * -val / (1 + elapsed) velocity = 0.8 * v + 0.2 * velocity return DELTA_VALIDATION_PENDING = 1 pointerWindowMoveListeners = [] onImplReady = -> impl.window.pointer.onMove (e) -> stop = false for listener in pointerWindowMoveListeners r = listener(e) if r is signal.STOP_PROPAGATION stop = true break if r is DELTA_VALIDATION_PENDING stop = true if stop signal.STOP_PROPAGATION if impl.window? onImplReady() else impl.onWindowReady onImplReady pointerUsed = false usePointer = (item) -> horizontalContinuous = createContinuous item, 'x' verticalContinuous = createContinuous item, 'y' focus = false listen = false dx = dy = 0 moveMovement = (e) -> unless scroll(item, e.movementX + dx, e.movementY + dy) e.stopPropagation = false onImplReady = -> pointerWindowMoveListeners.push (e) -> if not listen return if not focus if pointerUsed return dx += e.movementX dy += e.movementY limitedX = getLimitedX(item, dx) limitedY = getLimitedY(item, dy) if limitedX isnt item._contentX or limitedY isnt item._contentY if Math.abs(limitedX-item._contentX) < MIN_POINTER_DELTA and Math.abs(limitedY-item._contentY) < MIN_POINTER_DELTA return DELTA_VALIDATION_PENDING dx = dy = 0 if moveMovement(e) is signal.STOP_PROPAGATION focus = true pointerUsed = true item._impl.swingDisabled = true horizontalContinuous.update dx + e.movementX verticalContinuous.update dy + e.movementY signal.STOP_PROPAGATION impl.window.pointer.onRelease (e) -> listen = false dx = dy = 0 return unless focus focus = false pointerUsed = false item._impl.swingDisabled = false moveMovement e horizontalContinuous.release() verticalContinuous.release() return if impl.window? onImplReady() else impl.onWindowReady onImplReady item.pointer.onPress (e) -> listen = true item._impl.globalScale = getItemGlobalScale item horizontalContinuous.press() verticalContinuous.press() return wheelUsed = false lastActionTimestamp = 0 useWheel = (item) -> i = 0 used = false accepts = false pending = false clear = true lastAcceptedActionTimestamp = 0 horizontalContinuous = createContinuous item, 'x' verticalContinuous = createContinuous item, 'y' x = y = 0 minX = minY = maxX = maxY = 0 timer = -> now = Date.now() if accepts or now - lastAcceptedActionTimestamp > 70 pending = false accepts = true horizontalContinuous.update x verticalContinuous.update y horizontalContinuous.release() verticalContinuous.release() else requestAnimationFrame timer return item.pointer.onWheel (e) -> unless item._impl.snap x = e.deltaX / WHEEL_DIVISOR y = e.deltaY / WHEEL_DIVISOR item._impl.globalScale = getItemGlobalScale item unless scroll(item, x, y) e.stopPropagation = false return now = Date.now() if now - lastActionTimestamp > 300 wheelUsed = false lastActionTimestamp = now if wheelUsed and not used return if not wheelUsed and not clear used = false accepts = false i = 0 minX = minY = maxX = maxY = 0 i++ clear = false x = e.deltaX / WHEEL_DIVISOR y = e.deltaY / WHEEL_DIVISOR if x > 0 and x > maxX maxX = (maxX * (i-1) + x) / i else if x < minX minX = (minX * (i-1) + x) / i if y > 0 and y > maxY maxY = (maxY * (i-1) + y) / i else if y < minY minY = (minY * (i-1) + y) / i if (x > 0 and x < maxX * 0.3) or (x < 0 and x > minX * 0.3) or (y > 0 and y < maxY * 0.3) or (y < 0 and y > minY * 0.3) unless accepts accepts = true return signal.STOP_PROPAGATION else accepts = false if accepts return signal.STOP_PROPAGATION item._impl.globalScale = getItemGlobalScale item if scroll(item, x, y) lastAcceptedActionTimestamp = now unless pending pending = true horizontalContinuous.press() verticalContinuous.press() requestAnimationFrame timer else horizontalContinuous.update x verticalContinuous.update y wheelUsed = true used = true unless used e.stopPropagation = false return onWidthChange = (oldVal) -> if @contentItem.width < oldVal scroll @ return onHeightChange = (oldVal) -> if @contentItem.height < oldVal scroll @ return DATA = contentItem: null globalScale: 1 snap: false lastSnapTargetX: 0 lastSnapTargetY: 0 swingPending: false swingDisabled: false DATA: DATA _getLimitedX: getLimitedX _getLimitedY: getLimitedY createData: impl.utils.createDataCloner 'Item', DATA create: (data) -> impl.Types.Item.create.call @, data # signals if impl._scrollableUsePointer usePointer @ if impl._scrollableUseWheel useWheel @ return setScrollableContentItem: (val) -> if oldVal = @_impl.contentItem impl.setItemParent.call oldVal, null oldVal.onWidthChange.disconnect onWidthChange, @ oldVal.onHeightChange.disconnect onHeightChange, @ if val if @children.length > 0 impl.insertItemBefore.call val, @children.first else impl.setItemParent.call val, @ @_impl.contentItem = val val.onWidthChange onWidthChange, @ val.onHeightChange onHeightChange, @ return setScrollableContentX: (val) -> if item = @_impl.contentItem impl.setItemX.call item, -val return setScrollableContentY: (val) -> if item = @_impl.contentItem impl.setItemY.call item, -val return setScrollableSnap: (val) -> @_impl.snap = val return setScrollableSnapItem: (val) -> return