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:
parent
1eb35dd5bc
commit
ee3f408c7d
7 changed files with 100 additions and 29 deletions
|
@ -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'),
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>;
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
38
packages/frontend/src/scripts/idle-render.ts
Normal file
38
packages/frontend/src/scripts/idle-render.ts
Normal 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();
|
Loading…
Reference in a new issue