fix: improve focal points draggable style/perf (#1371)

* fix: improve focal points draggable style/perf

* remove unnecessary global

* fix all the things

* fix comment
This commit is contained in:
Nolan Lawson 2019-08-04 13:31:51 -07:00 committed by GitHub
parent 00945a3608
commit d58ab52a09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 195 additions and 65 deletions

View file

@ -46,7 +46,6 @@
"@babel/core": "^7.5.0", "@babel/core": "^7.5.0",
"@gamestdio/websocket": "^0.3.2", "@gamestdio/websocket": "^0.3.2",
"@webcomponents/custom-elements": "^1.2.4", "@webcomponents/custom-elements": "^1.2.4",
"@wessberg/pointer-events": "^1.0.9",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24", "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"cheerio": "^1.0.0-rc.2", "cheerio": "^1.0.0-rc.2",
@ -150,7 +149,8 @@
"CSS", "CSS",
"customElements", "customElements",
"AbortController", "AbortController",
"matchMedia" "matchMedia",
"MessageChannel"
], ],
"ignore": [ "ignore": [
"dist", "dist",

View file

@ -1,13 +1,13 @@
<div class="draggable-area {draggableClass}" <div class="draggable-area {draggableClassAfterRaf}"
on:pointermove="onPointerMove(event)" on:pointerMove="onPointerMove(event)"
on:pointerleave="onPointerLeave(event)" on:pointerLeave="onPointerLeave(event)"
on:pointerUp="onPointerUp(event)"
on:click="onClick(event)" on:click="onClick(event)"
ref:area ref:area
> >
<div class="draggable-indicator {indicatorClass}" <div class="draggable-indicator {indicatorClassAfterRaf}"
style={indicatorStyle} style={indicatorStyleAfterRaf}
on:pointerdown="onPointerDown(event)" on:pointerDown="onPointerDown(event)"
on:pointerup="onPointerUp(event)"
ref:indicator ref:indicator
> >
<div class="draggable-indicator-inner"> <div class="draggable-indicator-inner">
@ -30,19 +30,60 @@
} }
</style> </style>
<script> <script>
import { throttleRaf } from '../_utils/throttleRaf' import { observe } from 'svelte-extras'
import {
throttleRequestAnimationFrame,
throttleRequestPostAnimationFrame
} from '../_utils/throttleTimers'
import { pointerUp, pointerDown, pointerLeave, pointerMove } from '../_utils/pointerEvents'
// ensure DOM writes only happen once after a rAF
const updateIndicatorStyle = throttleRequestAnimationFrame()
const updateIndicatorClass = throttleRequestAnimationFrame()
const updateDraggableClass = throttleRequestAnimationFrame()
// ensure DOM reads only happen once after a rPAF
const calculateGBCR = throttleRequestPostAnimationFrame()
const clamp = x => Math.max(0, Math.min(1, x)) const clamp = x => Math.max(0, Math.min(1, x))
const throttledRaf = throttleRaf()
export default { export default {
oncreate () {
this.observe('dragging', dragging => {
if (dragging) {
this.fire('dragStart')
} else {
this.fire('dragEnd')
}
}, { init: false })
this.observe('indicatorStyle', indicatorStyle => {
console.log('Draggable indicatorStyle', indicatorStyle)
updateIndicatorStyle(() => {
this.set({ indicatorStyleAfterRaf: indicatorStyle })
})
})
this.observe('indicatorClass', indicatorClass => {
updateIndicatorClass(() => {
this.set(({ indicatorClassAfterRaf: indicatorClass }))
})
})
this.observe('draggableClass', draggableClass => {
updateDraggableClass(() => {
this.set({ draggableClassAfterRaf: draggableClass })
})
})
},
data: () => ({ data: () => ({
dragging: false,
draggableClass: '', draggableClass: '',
draggableClassAfterRaf: '',
indicatorClass: '', indicatorClass: '',
indicatorClassAfterRaf: '',
x: 0, x: 0,
y: 0, y: 0,
indicatorWidth: 0, indicatorWidth: 0,
indicatorHeight: 0 indicatorHeight: 0,
indicatorStyleAfterRaf: ''
}), }),
computed: { computed: {
indicatorStyle: ({ x, y, indicatorWidth, indicatorHeight }) => ( indicatorStyle: ({ x, y, indicatorWidth, indicatorHeight }) => (
@ -50,10 +91,12 @@
) )
}, },
methods: { methods: {
observe,
onPointerDown (e) { onPointerDown (e) {
e.preventDefault() console.log('Draggable: onPointerDown')
e.stopPropagation()
const rect = this.refs.indicator.getBoundingClientRect() const rect = this.refs.indicator.getBoundingClientRect()
console.log('Draggable: e.clientX', e.clientX)
console.log('Draggable: e.clientY', e.clientY)
this.set({ this.set({
dragging: true, dragging: true,
dragOffsetX: e.clientX - rect.left, dragOffsetX: e.clientX - rect.left,
@ -61,11 +104,11 @@
}) })
}, },
onPointerMove (e) { onPointerMove (e) {
if (this.get().dragging) { console.log('Draggable: onPointerMove')
e.preventDefault() const { dragging, indicatorWidth, indicatorHeight, dragOffsetX, dragOffsetY } = this.get()
e.stopPropagation() if (dragging) {
const { indicatorWidth, indicatorHeight, dragOffsetX, dragOffsetY } = this.get() console.log('Draggable: dragging')
throttledRaf(() => { calculateGBCR(() => {
const rect = this.refs.area.getBoundingClientRect() const rect = this.refs.area.getBoundingClientRect()
const offsetX = dragOffsetX - (indicatorWidth / 2) const offsetX = dragOffsetX - (indicatorWidth / 2)
const offsetY = dragOffsetY - (indicatorHeight / 2) const offsetY = dragOffsetY - (indicatorHeight / 2)
@ -77,19 +120,19 @@
} }
}, },
onPointerUp (e) { onPointerUp (e) {
e.preventDefault() console.log('Draggable: onPointerUp')
e.stopPropagation()
this.set({ dragging: false }) this.set({ dragging: false })
}, },
onPointerLeave (e) { onPointerLeave (e) {
e.preventDefault() console.log('Draggable: onPointerLeave')
e.stopPropagation()
this.set({ dragging: false }) this.set({ dragging: false })
}, },
onClick (e) { onClick (e) {
console.log('Draggable: onClick')
console.log('Draggable: target classList', e.target.classList)
console.log('Draggable: currentTarget classList', e.currentTarget.classList)
if (!e.target.classList.contains('draggable-indicator')) { if (!e.target.classList.contains('draggable-indicator')) {
e.preventDefault() console.log('Draggable: onClick handled')
e.stopPropagation()
const rect = this.refs.area.getBoundingClientRect() const rect = this.refs.area.getBoundingClientRect()
const x = clamp((e.clientX - rect.left) / rect.width) const x = clamp((e.clientX - rect.left) / rect.width)
const y = clamp((e.clientY - rect.top) / rect.height) const y = clamp((e.clientY - rect.top) / rect.height)
@ -97,6 +140,12 @@
this.fire('change', { x, y }) this.fire('change', { x, y })
} }
} }
},
events: {
pointerUp,
pointerDown,
pointerLeave,
pointerMove
} }
} }
</script> </script>

View file

@ -25,12 +25,14 @@
<!-- 52px == 32px icon width + 10px padding --> <!-- 52px == 32px icon width + 10px padding -->
<Draggable <Draggable
draggableClass="media-draggable-area-inner" draggableClass="media-draggable-area-inner"
indicatorClass="media-focal-point-indicator {imageLoaded ? '': 'hidden'}" indicatorClass="media-focal-point-indicator {imageLoaded ? '': 'hidden'} {dragging ? 'dragging' : ''}"
indicatorWidth={52} indicatorWidth={52}
indicatorHeight={52} indicatorHeight={52}
x={indicatorX} x={indicatorX}
y={indicatorY} y={indicatorY}
on:change="onDraggableChange(event)" on:change="onDraggableChange(event)"
on:dragStart="set({dragging: true})"
on:dragEnd="set({dragging: false})"
> >
<SvgIcon <SvgIcon
className="media-focal-point-indicator-svg" className="media-focal-point-indicator-svg"
@ -142,6 +144,14 @@
display: flex; display: flex;
} }
:global(.media-focal-point-indicator:hover) {
background: var(--focal-bg-hover);
}
:global(.media-focal-point-indicator.dragging) {
background: var(--focal-bg-drag);
}
:global(.media-draggable-area-inner) { :global(.media-draggable-area-inner) {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -177,13 +187,17 @@
import { store } from '../../../_store/store' import { store } from '../../../_store/store'
import { get } from '../../../_utils/lodash-lite' import { get } from '../../../_utils/lodash-lite'
import { observe } from 'svelte-extras' import { observe } from 'svelte-extras'
import debounce from 'lodash-es/debounce'
import { scheduleIdleTask } from '../../../_utils/scheduleIdleTask' import { scheduleIdleTask } from '../../../_utils/scheduleIdleTask'
import { coordsToPercent, percentToCoords } from '../../../_utils/coordsToPercent' import { coordsToPercent, percentToCoords } from '../../../_utils/coordsToPercent'
import SvgIcon from '../../SvgIcon.html' import SvgIcon from '../../SvgIcon.html'
import { intrinsicScale } from '../../../_thirdparty/intrinsic-scale/intrinsicScale' import { intrinsicScale } from '../../../_thirdparty/intrinsic-scale/intrinsicScale'
import { resize } from '../../../_utils/events' import { resize } from '../../../_utils/events'
import Draggable from '../../Draggable.html' import Draggable from '../../Draggable.html'
import { throttleScheduleIdleTask } from '../../../_utils/throttleTimers'
// Updating the focal points in the store causes a lot of computations (extra JS work),
// so we really don't want to do it for every drag event.
const updateFocalPointsInStore = throttleScheduleIdleTask()
const parseAndValidateFloat = rawText => { const parseAndValidateFloat = rawText => {
let float = parseFloat(rawText) let float = parseFloat(rawText)
@ -208,6 +222,7 @@
Draggable Draggable
}, },
data: () => ({ data: () => ({
dragging: false,
rawFocusX: '0', rawFocusX: '0',
rawFocusY: '0', rawFocusY: '0',
containerWidth: 0, containerWidth: 0,
@ -276,8 +291,6 @@
}) })
}, },
setupSyncToStore () { setupSyncToStore () {
const saveStore = debounce(() => scheduleIdleTask(() => this.store.save()), 1000)
const observeAndSync = (rawKey, key) => { const observeAndSync = (rawKey, key) => {
this.observe(rawKey, rawFocus => { this.observe(rawKey, rawFocus => {
const { realm, index, media } = this.get() const { realm, index, media } = this.get()
@ -285,7 +298,7 @@
if (media[index][key] !== rawFocusDecimal) { if (media[index][key] !== rawFocusDecimal) {
media[index][key] = rawFocusDecimal media[index][key] = rawFocusDecimal
this.store.setComposeData(realm, { media }) this.store.setComposeData(realm, { media })
saveStore() scheduleIdleTask(() => this.store.save())
} }
}, { init: false }) }, { init: false })
} }
@ -294,16 +307,16 @@
observeAndSync('rawFocusY', 'focusY') observeAndSync('rawFocusY', 'focusY')
}, },
onDraggableChange ({ x, y }) { onDraggableChange ({ x, y }) {
const saveStore = debounce(() => scheduleIdleTask(() => this.store.save()), 1000) updateFocalPointsInStore(() => {
const focusX = parseAndValidateFloat(percentToCoords(x * 100))
scheduleIdleTask(() => { const focusY = parseAndValidateFloat(percentToCoords(100 - (y * 100)))
const focusX = percentToCoords(x * 100)
const focusY = percentToCoords(100 - (y * 100))
const { realm, index, media } = this.get() const { realm, index, media } = this.get()
media[index].focusX = parseAndValidateFloat(focusX) if (media[index].focusX !== focusX || media[index].focusY !== focusY) {
media[index].focusY = parseAndValidateFloat(focusY) media[index].focusX = focusX
this.store.setComposeData(realm, { media }) media[index].focusY = focusY
saveStore() this.store.setComposeData(realm, { media })
scheduleIdleTask(() => this.store.save())
}
}) })
}, },
measure () { measure () {

View file

@ -13,7 +13,3 @@ export const importIndexedDBGetAllShim = () => import(
export const importCustomElementsPolyfill = () => import( export const importCustomElementsPolyfill = () => import(
/* webpackChunkName: '$polyfill$-@webcomponents/custom-elements' */ '@webcomponents/custom-elements' /* webpackChunkName: '$polyfill$-@webcomponents/custom-elements' */ '@webcomponents/custom-elements'
) )
export const importPointerEventsPolyfill = () => import(
/* webpackChunkName: '$polyfill$-@wessberg/pointer-events' */ '@wessberg/pointer-events'
)

View file

@ -2,8 +2,7 @@ import {
importCustomElementsPolyfill, importCustomElementsPolyfill,
importIndexedDBGetAllShim, importIndexedDBGetAllShim,
importIntersectionObserver, importIntersectionObserver,
importRequestIdleCallback, importRequestIdleCallback
importPointerEventsPolyfill
} from './asyncPolyfills' } from './asyncPolyfills'
export function loadPolyfills () { export function loadPolyfills () {
@ -11,7 +10,6 @@ export function loadPolyfills () {
typeof IntersectionObserver === 'undefined' && importIntersectionObserver(), typeof IntersectionObserver === 'undefined' && importIntersectionObserver(),
typeof requestIdleCallback === 'undefined' && importRequestIdleCallback(), typeof requestIdleCallback === 'undefined' && importRequestIdleCallback(),
!IDBObjectStore.prototype.getAll && importIndexedDBGetAllShim(), !IDBObjectStore.prototype.getAll && importIndexedDBGetAllShim(),
typeof customElements === 'undefined' && importCustomElementsPolyfill(), typeof customElements === 'undefined' && importCustomElementsPolyfill()
typeof PointerEvent === 'undefined' && importPointerEventsPolyfill()
]) ])
} }

View file

@ -0,0 +1,53 @@
import { get } from './lodash-lite'
const hasPointerEvents = process.browser && typeof PointerEvent === 'function'
// Epiphany browser reports that it's a touch device even though it's not
const isTouchDevice = process.browser && 'ontouchstart' in document && !/Epiphany/.test(navigator.userAgent)
let pointerDown
let pointerUp
let pointerLeave
let pointerMove
function createEventListener (event) {
return (node, callback) => {
const listener = e => {
// lightweight polyfill for clientX/clientY in pointer events,
// which is slightly different in touch events
if (typeof e.clientX !== 'number') {
e.clientX = get(e, ['touches', 0, 'clientX'])
}
if (typeof e.clientY !== 'number') {
e.clientY = get(e, ['touches', 0, 'clientY'])
}
callback(e)
}
node.addEventListener(event, listener)
return {
destroy () {
node.removeEventListener(event, listener)
}
}
}
}
if (hasPointerEvents) {
pointerDown = createEventListener('pointerdown')
pointerUp = createEventListener('pointerup')
pointerLeave = createEventListener('pointerleave')
pointerMove = createEventListener('pointermove')
} else if (isTouchDevice) {
pointerDown = createEventListener('touchstart')
pointerUp = createEventListener('touchend')
pointerLeave = createEventListener('touchend')
pointerMove = createEventListener('touchmove')
} else {
pointerDown = createEventListener('mousedown')
pointerUp = createEventListener('mouseup')
pointerLeave = createEventListener('mouseleave')
pointerMove = createEventListener('mousemove')
}
export { pointerDown, pointerUp, pointerLeave, pointerMove }

View file

@ -0,0 +1,9 @@
// modeled after https://github.com/andrewiggins/afterframe
// see also https://github.com/WICG/requestPostAnimationFrame
export const requestPostAnimationFrame = cb => {
requestAnimationFrame(() => {
const channel = new MessageChannel()
channel.port1.onmessage = cb
channel.port2.postMessage(undefined)
})
}

View file

@ -1,18 +0,0 @@
// ensure callback is only executed once per raf
export const throttleRaf = () => {
let rafCallback
let rafQueued
return function throttledRaf (callback) {
rafCallback = callback
if (!rafQueued) {
rafQueued = true
requestAnimationFrame(() => {
const cb = rafCallback
rafCallback = null
rafQueued = false
cb()
})
}
}
}

View file

@ -0,0 +1,28 @@
// Sometimes we want to queue multiple requestAnimationFrames but only run the last one.
// It's tedious to do this using cancelAnimationFrame, so this is a utility to throttle
// a timer such that it only runs the last callback when it fires.
import { requestPostAnimationFrame } from './requestPostAnimationFrame'
import { scheduleIdleTask } from './scheduleIdleTask'
const throttle = (timer) => {
return () => {
let queuedCallback
return function throttledRaf (callback) {
const alreadyQueued = !!queuedCallback
queuedCallback = callback
if (!alreadyQueued) {
timer(() => {
const cb = queuedCallback
queuedCallback = null
cb()
})
}
}
}
}
export const throttleRequestAnimationFrame = throttle(requestAnimationFrame)
export const throttleRequestPostAnimationFrame = throttle(requestPostAnimationFrame)
export const throttleScheduleIdleTask = throttle(scheduleIdleTask)

View file

@ -124,5 +124,7 @@
--focal-img-backdrop-filter: #{rgba($main-bg-color, 0.5)}; --focal-img-backdrop-filter: #{rgba($main-bg-color, 0.5)};
--focal-img-bg: #{rgba($main-bg-color, 0.3)}; --focal-img-bg: #{rgba($main-bg-color, 0.3)};
--focal-bg: #{rgba($toast-bg, 0.8)}; --focal-bg: #{rgba($toast-bg, 0.8)};
--focal-bg-drag: #{rgba($toast-bg, 0.9)};
--focal-bg-hover: #{lighten(rgba($toast-bg, 0.8), 5%)};
--focal-color: #{$secondary-text-color}; --focal-color: #{$secondary-text-color};
} }