feat: impl IdlingRenderScheduler (#10547)

* feat: impl IdleRender

* test: pin time on Chromatic

* test: pin time on Chromatic

* fix: typo

* style: rename

* style: rename

* chore: back to setTimeout

* style: linebreak

* refactor: remove unused budget option

* refactor: use raw unix time

* fix: conflict error

* fix: floor

* fix: subtract

* Revert "fix: subtract"

This reverts commit 2ef4afaafc69d2fb8329b04c1b124dfa97b7e863.

* Revert "fix: floor"

This reverts commit bef8ecdf45c6afc52138921d16e2caca78cfd38d.

* Revert "refactor: use raw unix time"

This reverts commit 5199e13cb2829f3036101f95445cca3cb9c83703.
This commit is contained in:
Acid Chicken (硫酸鶏) 2023-05-20 03:38:07 +09:00 committed by GitHub
parent 1eb35dd5bc
commit ee3f408c7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 100 additions and 29 deletions

View file

@ -397,6 +397,7 @@ function toStories(component: string): string {
Promise.all([ Promise.all([
glob('src/components/global/*.vue'), glob('src/components/global/*.vue'),
glob('src/components/Mk{A,B}*.vue'), glob('src/components/Mk{A,B}*.vue'),
glob('src/components/MkDigitalClock.vue'),
glob('src/components/MkGalleryPostPreview.vue'), glob('src/components/MkGalleryPostPreview.vue'),
glob('src/components/MkSignupServerRules.vue'), glob('src/components/MkSignupServerRules.vue'),
glob('src/components/MkUserSetupDialog.vue'), glob('src/components/MkUserSetupDialog.vue'),

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import isChromatic from 'chromatic/isChromatic';
import MkAnalogClock from './MkAnalogClock.vue'; import MkAnalogClock from './MkAnalogClock.vue';
import isChromatic from 'chromatic';
export const Default = { export const Default = {
render(args) { render(args) {
return { return {

View file

@ -39,6 +39,7 @@
--> -->
<line <line
ref="sLine"
:class="[$style.s, { [$style.animate]: !disableSAnimate && sAnimation !== 'none', [$style.elastic]: sAnimation === 'elastic', [$style.easeOut]: sAnimation === 'easeOut' }]" :class="[$style.s, { [$style.animate]: !disableSAnimate && sAnimation !== 'none', [$style.elastic]: sAnimation === 'elastic', [$style.easeOut]: sAnimation === 'easeOut' }]"
:x1="5 - (0 * (sHandLengthRatio * handsTailLength))" :x1="5 - (0 * (sHandLengthRatio * handsTailLength))"
:y1="5 + (1 * (sHandLengthRatio * handsTailLength))" :y1="5 + (1 * (sHandLengthRatio * handsTailLength))"
@ -73,9 +74,10 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, onBeforeUnmount } from 'vue'; import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { globalEvents } from '@/events.js'; import { globalEvents } from '@/events.js';
import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js';
// https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles // https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles
const angleDiff = (a: number, b: number) => { const angleDiff = (a: number, b: number) => {
@ -145,6 +147,7 @@ let mAngle = $ref<number>(0);
let sAngle = $ref<number>(0); let sAngle = $ref<number>(0);
let disableSAnimate = $ref(false); let disableSAnimate = $ref(false);
let sOneRound = false; let sOneRound = false;
const sLine = ref<SVGPathElement>();
function tick() { function tick() {
const now = props.now(); const now = props.now();
@ -160,17 +163,21 @@ function tick() {
} }
hAngle = Math.PI * (h % (props.twentyfour ? 24 : 12) + (m + s / 60) / 60) / (props.twentyfour ? 12 : 6); hAngle = Math.PI * (h % (props.twentyfour ? 24 : 12) + (m + s / 60) / 60) / (props.twentyfour ? 12 : 6);
mAngle = Math.PI * (m + s / 60) / 30; mAngle = Math.PI * (m + s / 60) / 30;
if (sOneRound) { // (59->0) if (sOneRound && sLine.value) { // (59->0)
sAngle = Math.PI * 60 / 30; sAngle = Math.PI * 60 / 30;
window.setTimeout(() => { defaultIdlingRenderScheduler.delete(tick);
sLine.value.addEventListener('transitionend', () => {
disableSAnimate = true; disableSAnimate = true;
window.setTimeout(() => { requestAnimationFrame(() => {
sAngle = 0; sAngle = 0;
window.setTimeout(() => { requestAnimationFrame(() => {
disableSAnimate = false; disableSAnimate = false;
}, 100); if (enabled) {
}, 100); defaultIdlingRenderScheduler.add(tick);
}, 700); }
});
});
}, { once: true });
} else { } else {
sAngle = Math.PI * s / 30; sAngle = Math.PI * s / 30;
} }
@ -194,20 +201,13 @@ function calcColors() {
calcColors(); calcColors();
onMounted(() => { onMounted(() => {
const update = () => { defaultIdlingRenderScheduler.add(tick);
if (enabled) {
tick();
window.setTimeout(update, 1000);
}
};
update();
globalEvents.on('themeChanged', calcColors); globalEvents.on('themeChanged', calcColors);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
enabled = false; enabled = false;
defaultIdlingRenderScheduler.delete(tick);
globalEvents.off('themeChanged', calcColors); globalEvents.off('themeChanged', calcColors);
}); });
</script> </script>

View file

@ -0,0 +1,32 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import isChromatic from 'chromatic/isChromatic';
import MkDigitalClock from './MkDigitalClock.vue';
export const Default = {
render(args) {
return {
components: {
MkDigitalClock,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
},
template: '<MkDigitalClock v-bind="props" />',
};
},
args: {
now: isChromatic() ? () => new Date('2023-01-01T10:10:30') : undefined,
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkDigitalClock>;

View file

@ -11,19 +11,21 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onUnmounted, ref, watch } from 'vue'; import { onMounted, onUnmounted, ref, watch } from 'vue';
import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
showS?: boolean; showS?: boolean;
showMs?: boolean; showMs?: boolean;
offset?: number; offset?: number;
now?: () => Date;
}>(), { }>(), {
showS: true, showS: true,
showMs: false, showMs: false,
offset: 0 - new Date().getTimezoneOffset(), offset: 0 - new Date().getTimezoneOffset(),
now: () => new Date(),
}); });
let intervalId;
const hh = ref(''); const hh = ref('');
const mm = ref(''); const mm = ref('');
const ss = ref(''); const ss = ref('');
@ -39,9 +41,9 @@ watch(showColon, (v) => {
} }
}); });
const tick = () => { const tick = (): void => {
const now = new Date(); const now = props.now();
now.setMinutes(now.getMinutes() + (new Date().getTimezoneOffset() + props.offset)); now.setMinutes(now.getMinutes() + now.getTimezoneOffset() + props.offset);
hh.value = now.getHours().toString().padStart(2, '0'); hh.value = now.getHours().toString().padStart(2, '0');
mm.value = now.getMinutes().toString().padStart(2, '0'); mm.value = now.getMinutes().toString().padStart(2, '0');
ss.value = now.getSeconds().toString().padStart(2, '0'); ss.value = now.getSeconds().toString().padStart(2, '0');
@ -52,13 +54,12 @@ const tick = () => {
tick(); tick();
watch(() => props.showMs, () => { onMounted(() => {
if (intervalId) window.clearInterval(intervalId); defaultIdlingRenderScheduler.add(tick);
intervalId = window.setInterval(tick, props.showMs ? 10 : 1000); });
}, { immediate: true });
onUnmounted(() => { onUnmounted(() => {
window.clearInterval(intervalId); defaultIdlingRenderScheduler.delete(tick);
}); });
</script> </script>

View file

@ -58,7 +58,6 @@ function tick() {
if (props.mode === 'relative' || props.mode === 'detail') { if (props.mode === 'relative' || props.mode === 'detail') {
tick(); tick();
onUnmounted(() => { onUnmounted(() => {
window.clearTimeout(tickId); window.clearTimeout(tickId);
}); });

View file

@ -0,0 +1,38 @@
class IdlingRenderScheduler {
#renderers: Set<FrameRequestCallback>;
#rafId: number;
#ricId: number;
constructor() {
this.#renderers = new Set();
this.#rafId = 0;
this.#ricId = requestIdleCallback((deadline) => this.#schedule(deadline));
}
#schedule(deadline: IdleDeadline): void {
if (deadline.timeRemaining()) {
this.#rafId = requestAnimationFrame((time) => {
for (const renderer of this.#renderers) {
renderer(time);
}
});
}
this.#ricId = requestIdleCallback((arg) => this.#schedule(arg));
}
add(renderer: FrameRequestCallback): void {
this.#renderers.add(renderer);
}
delete(renderer: FrameRequestCallback): void {
this.#renderers.delete(renderer);
}
dispose(): void {
this.#renderers.clear();
cancelAnimationFrame(this.#rafId);
cancelIdleCallback(this.#ricId);
}
}
export const defaultIdlingRenderScheduler = new IdlingRenderScheduler();