neft
Version:
JavaScript. Everywhere.
453 lines (362 loc) • 13.3 kB
text/coffeescript
'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 .width < oldVal
scroll @
return
onHeightChange = (oldVal) ->
if .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 = .contentItem
impl.setItemParent.call oldVal, null
oldVal.onWidthChange.disconnect onWidthChange, @
oldVal.onHeightChange.disconnect onHeightChange, @
if val
if .length > 0
impl.insertItemBefore.call val, .first
else
impl.setItemParent.call val, @
.contentItem = val
val.onWidthChange onWidthChange, @
val.onHeightChange onHeightChange, @
return
setScrollableContentX: (val) ->
if item = .contentItem
impl.setItemX.call item, -val
return
setScrollableContentY: (val) ->
if item = .contentItem
impl.setItemY.call item, -val
return
setScrollableSnap: (val) ->
.snap = val
return
setScrollableSnapItem: (val) ->
return