# Plugin: faceScroll
- Move facePointer above or below a scroll area to scroll in that direction
- Hover over scroll areas to focus it
- Adjust focus time and scroll zones and speeds
- Tags:
['browser']
Models: Jeeliz Weboji
Activate:
handsfree.plugin.faceScroll.enable()
Tags:
['browser']
About: This plugin is used in combination with the facePointer to help you scroll pages with head movements.
# Config
# During instantiation
const handsfree = new Handsfree({
weboji: true,
plugin: {
faceScroll: {
// Number of frames over the same element before activating that element
framesToFocus: 10,
vertScroll: {
// The multiplier to scroll by. Lower numbers are slower
scrollSpeed: 0.15,
// How many pixels from the top/bottom of the scroll area to scroll
scrollZone: 100
}
}
}
})
# After instantiation
// Require 100ms to focus that element
handsfree.plugin.faceScroll.config.framesToFocus = 100
// Require that the pointer moves way above or way below the scroll area
handsfree.plugin.faceScroll.config.offset.yaw = -100
# Full plugin code
import throttle from 'lodash/throttle'
/**
* Scrolls the page vertically
*/
export default {
models: 'weboji',
enabled: false,
tags: ['browser'],
// Number of frames the current element is the same as the last
numFramesFocused: 0,
// The last scrollable target focused
$lastTarget: null,
// The current scrollable target
$target: null,
config: {
// Number of frames over the same element before activating that element
framesToFocus: 10,
vertScroll: {
// The multiplier to scroll by. Lower numbers are slower
scrollSpeed: 0.05,
// How many pixels from the top/bottom of the scroll area to scroll
scrollZone: 100
}
},
onUse () {
this.$target = window
},
/**
* Scroll the page when the cursor goes above/below the threshold
*/
onFrame ({weboji}) {
// @FIXME we shouldn't need to do this, but this is occasionally reset to {x: 0, y: 0} when running in client mode
if (!weboji.pointer.x && !weboji.pointer.y) return
// Check for hover
this.checkForFocus(weboji)
let isScrolling = false
// Get bounds
let bounds
let scrollTop = this.getTargetScrollTop()
if (this.$target.getBoundingClientRect) {
bounds = this.$target.getBoundingClientRect()
} else {
bounds = { top: 0, bottom: window.innerHeight }
}
// Check on click
if (weboji.pointer.state === 'mouseDown') {
this.numFramesFocused = 0
this.maybeSetTarget(weboji)
}
// Scroll up
if (weboji.pointer.y < bounds.top + this.config.vertScroll.scrollZone) {
this.$target.scrollTo(
0,
scrollTop +
(weboji.pointer.y - bounds.top - this.config.vertScroll.scrollZone) *
this.config.vertScroll.scrollSpeed
)
isScrolling = true
}
// Scroll down
if (weboji.pointer.y > bounds.bottom - this.config.vertScroll.scrollZone) {
this.$target.scrollTo(
0,
scrollTop -
(bounds.bottom -
weboji.pointer.y -
this.config.vertScroll.scrollZone) *
this.config.vertScroll.scrollSpeed
)
isScrolling = true
}
isScrolling && this.maybeSelectNewTarget()
},
/**
* Check that the scroll is actually happening, otherwise traverse up the DOM
*/
maybeSelectNewTarget() {
let curScrollTop = this.getTargetScrollTop()
let didNotScroll = false
// Check if we have scrolled up
this.$target.scrollTo(0, curScrollTop + this.config.vertScroll.scrollSpeed)
if (curScrollTop === this.getTargetScrollTop()) {
didNotScroll = true
} else {
this.$target.scrollTo(0, curScrollTop - this.config.vertScroll.scrollSpeed)
return
}
// Check if we have scrolled down
this.$target.scrollTo(0, curScrollTop - this.config.vertScroll.scrollSpeed)
if (curScrollTop === this.getTargetScrollTop()) {
if (didNotScroll) {
this.numFramesFocused = 0
this.selectTarget(
this.recursivelyFindScrollbar(this.$target.parentElement)
)
}
} else {
this.$target.scrollTo(0, curScrollTop + this.config.vertScroll.scrollSpeed)
return
}
},
/**
* Gets the scrolltop, taking account the window object
*/
getTargetScrollTop() {
return this.$target?.scrollY || this.$target?.scrollTop || 0
},
/**
* Checks to see if we've hovered over an element for x turns
*/
checkForFocus: throttle(function(weboji) {
let $potTarget = document.elementFromPoint(
weboji.pointer.x,
weboji.pointer.y
)
if (!$potTarget) return
$potTarget = this.recursivelyFindScrollbar($potTarget)
if ($potTarget === this.$lastTarget) {
++this.numFramesFocused
} else {
this.numFramesFocused = 0
}
if (this.numFramesFocused > this.config.framesToFocus) {
this.selectTarget($potTarget)
}
this.$lastTarget = $potTarget
}, 100),
/**
* Select and style the element
*/
selectTarget($potTarget) {
// Check required in case the window is the target
if (this.$target.classList) {
this.$target.classList.remove('handsfree-scroll-focus')
}
if ($potTarget && $potTarget.classList) {
$potTarget.classList.add('handsfree-scroll-focus')
}
if ($potTarget.nodeName === 'HTML' || !$potTarget.nodeName) {
$potTarget = window
}
this.$target = $potTarget
},
/**
* Sets a new scroll target on click
*/
maybeSetTarget(weboji) {
if (weboji.pointer.state === 'mouseDown' && weboji.pointer.$target) {
this.selectTarget(this.recursivelyFindScrollbar(weboji.pointer.$target))
}
},
/**
* Traverses up the DOM until a scrollbar is found, or until we hit the body/window
*/
recursivelyFindScrollbar($target) {
const styles =
$target && $target.getBoundingClientRect ? getComputedStyle($target) : {}
if (
$target &&
$target.scrollHeight > $target.clientHeight &&
(styles.overflow === 'auto' ||
styles.overflow === 'auto scroll' ||
styles.overflowY === 'auto' ||
styles.overflowY === 'auto scroll')
) {
return $target
} else {
if ($target && $target.parentElement) {
return this.recursivelyFindScrollbar($target.parentElement)
} else {
return window
}
}
}
}