Skip to content

Commit

Permalink
Background img transform optimization (#1255)
Browse files Browse the repository at this point in the history
  • Loading branch information
hworld authored Jan 11, 2024
1 parent 871d0c1 commit 1a26c50
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 67 deletions.
68 changes: 30 additions & 38 deletions src/_common/background/AppBackgroundImg.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts" setup>
import { CSSProperties, PropType, computed, ref, toRefs, watch, watchEffect } from 'vue';
import { PageScrollSubscriptionTimeout, usePageScrollSubscription } from '../scroll/scroll.service';
import { CSSProperties, PropType, computed, ref, toRefs, watch } from 'vue';
import { styleAbsoluteFill, styleWhen } from '../../_styles/mixins';
import { usePageScrollSubscription } from '../scroll/scroll.service';
import { SettingParallaxBackgrounds } from '../settings/settings.service';
import { BackgroundModel, getBackgroundCSSProperties } from './background.model';
Expand Down Expand Up @@ -28,44 +29,22 @@ const root = ref<HTMLElement>();
const shouldParallax = computed(() => enablePageScroll.value && SettingParallaxBackgrounds.get());
const baseStyles = computed(() => {
// We're casting this to CSSStyleDeclaration so that we can more easily
// apply to the style property.
return {
...getBackgroundCSSProperties(background.value),
...backgroundStyle?.value,
} as CSSStyleDeclaration;
});
// We freeze the tile height for now since it shouldn't really change.
const tileHeight = background.value.media_item.croppedHeight / background.value.scale;
const baseStyles = computed(() => getBackgroundCSSProperties(background.value));
watchEffect(() => {
function attachPageOffsetBackgroundStyles(top: number) {
if (!root.value) {
return;
}
const _styles = baseStyles.value;
for (const key in _styles) {
// Don't assign backgroundPosition if we're currently modifying it
// through page scroll offset.
if (key === 'backgroundPosition' && pageScrollSubscription.isActive.value) {
continue;
}
root.value.style[key] = _styles[key];
}
});
function attachPageOffsetBackgroundStyles(top: number) {
if (!root.value) {
if (!enablePageScroll.value) {
root.value.style.transform = `translateY(0)`;
return;
}
if (!shouldParallax.value || scrollDirection?.value) {
root.value.style.backgroundPosition = baseStyles.value.backgroundPosition;
root.value.style.transition = '';
} else {
root.value.style.backgroundPosition = `center ${top / 5}px`;
root.value.style.transition = `background-position ${
PageScrollSubscriptionTimeout + 20
}ms ease-out`;
}
top /= 5;
top %= tileHeight;
root.value.style.transform = `translateY(${top}px)`;
}
const pageScrollSubscription = usePageScrollSubscription(attachPageOffsetBackgroundStyles, {
Expand All @@ -84,11 +63,24 @@ watch(shouldParallax, enabled => {
<template>
<div
ref="root"
:class="{
_scroll: scrollDirection,
[`_scroll-${scrollDirection}`]: scrollDirection,
}"
/>
:style="
styleWhen(shouldParallax, {
...styleAbsoluteFill({ inset: `-${tileHeight}px` }),
// We change this a lot when we have the susbcription
// active, so let the browser know.
willChange: `transform`,
})
"
>
<div
:class="scrollDirection ? ['_scroll', `_scroll-${scrollDirection}`] : []"
:style="{
...styleAbsoluteFill(),
...baseStyles,
...backgroundStyle,
}"
/>
</div>
</template>

<style lang="stylus" scoped>
Expand Down
4 changes: 2 additions & 2 deletions src/_common/background/background.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ export function getBackgroundCSSProperties(background: BackgroundModel) {

switch (scaling) {
case BackgroundScaling.tile: {
const width = media_item.width / scale;
const height = media_item.height / scale;
const width = media_item.croppedWidth / scale;
const height = media_item.croppedHeight / scale;
backgroundSize = `${width}px ${height}px`;
backgroundRepeat = 'repeat';
break;
Expand Down
57 changes: 30 additions & 27 deletions src/_common/scroll/scroll.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { onUnmounted, ref, shallowReadonly, toRef } from 'vue';
import { arrayRemove } from '../../utils/array';
import { sleep } from '../../utils/utils';
import { Ruler } from '../ruler/ruler-service';
import { AppAutoscrollAnchor } from './auto-scroll/anchor';

Expand Down Expand Up @@ -172,14 +171,15 @@ export const Scroll = /** @__PURE__ */ new ScrollService();
* Minimum interval in milliseconds that {@link _onScroll} will assign to
* {@link PageScrollSubscription.top} refs.
*/
export const PageScrollSubscriptionTimeout = 1_000 / 24;
export const PageScrollSubscriptionTimeout = 1_000 / 30;

export type PageScrollSubscription = ReturnType<typeof usePageScrollSubscription>;
type OnScrollCallback = (top: number) => void;

const _onScrollCallbacks: OnScrollCallback[] = [];
let _isOnScrollBusy = false;
let _isScrollLooping = false;
let _lastOnScrollTop: number | null = null;
let _lastScrollTime = 0;

/** Should only be used in a setup block. */
export function usePageScrollSubscription(
Expand Down Expand Up @@ -242,38 +242,41 @@ export function usePageScrollSubscription(
}

function _afterSubscriptionsChanged() {
if (_onScrollCallbacks.length) {
window.document.addEventListener('scroll', _onScroll, {
passive: true,
});

// Need to call _onScroll lazily here, otherwise things may not be
// loaded in yet. This can happen if you're on a page that holds the
// only subscription, go to a new page, then go back through the
// browser.
sleep(0).then(() => _onScroll());
} else {
window.document.removeEventListener('scroll', _onScroll);
_lastOnScrollTop = null;
// If we don't have any scroll callbacks, the scroll loop will stop itself.
// We don't need to do anything here.
if (!_onScrollCallbacks.length) {
return;
}
}

async function _onScroll() {
if (_isOnScrollBusy) {
if (_isScrollLooping) {
return;
}
_isOnScrollBusy = true;

// Wait a bit so we don't do this too often.
await sleep(PageScrollSubscriptionTimeout);
// Start us next animation frame so that the page is finished mounting.
_isScrollLooping = true;
window.requestAnimationFrame(_scrollLoop);
}

let top = 0;
if (_onScrollCallbacks.length) {
top = Scroll.getScrollTop();
function _scrollLoop() {
if (!_onScrollCallbacks.length) {
// Don't register another animation frame. Just reset ourselves.
_lastOnScrollTop = null;
_isScrollLooping = false;
return;
}

// const startTime = Date.now();
// if (startTime - _lastScrollTime >= PageScrollSubscriptionTimeout) {
const top = Scroll.getScrollTop();
if (top !== _lastOnScrollTop) {
for (const cb of _onScrollCallbacks) {
cb(top);
}
_lastOnScrollTop = top;
}
_lastOnScrollTop = top;
_isOnScrollBusy = false;
// _lastScrollTime = startTime;
// }

// Continue looping.
window.requestAnimationFrame(_scrollLoop);
}

0 comments on commit 1a26c50

Please sign in to comment.