diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index 7c51d4c00ce432313fd90182624d1426321a46fb..f4424221090fd4e88d33c58f8c2fbbf092a22311 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -397,6 +397,7 @@ function toStories(component: string): string { Promise.all([ glob('src/components/global/*.vue'), glob('src/components/Mk{A,B}*.vue'), + glob('src/components/MkDigitalClock.vue'), glob('src/components/MkGalleryPostPreview.vue'), glob('src/components/MkSignupServerRules.vue'), glob('src/components/MkUserSetupDialog.vue'), diff --git a/packages/frontend/src/components/MkAnalogClock.stories.impl.ts b/packages/frontend/src/components/MkAnalogClock.stories.impl.ts index e7fbb472846e28a1bb79355cf56f34b673b51010..0aebdccf4ff5060b5f261d2faecc9071518aab61 100644 --- a/packages/frontend/src/components/MkAnalogClock.stories.impl.ts +++ b/packages/frontend/src/components/MkAnalogClock.stories.impl.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { StoryObj } from '@storybook/vue3'; +import isChromatic from 'chromatic/isChromatic'; import MkAnalogClock from './MkAnalogClock.vue'; -import isChromatic from 'chromatic'; export const Default = { render(args) { return { diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue index f12020f81047e0a391ec342fd10310e01e2e44ff..05caffe7d005ee63c32aae254527ec07a3fbcfa1 100644 --- a/packages/frontend/src/components/MkAnalogClock.vue +++ b/packages/frontend/src/components/MkAnalogClock.vue @@ -39,6 +39,7 @@ --> <line + ref="sLine" :class="[$style.s, { [$style.animate]: !disableSAnimate && sAnimation !== 'none', [$style.elastic]: sAnimation === 'elastic', [$style.easeOut]: sAnimation === 'easeOut' }]" :x1="5 - (0 * (sHandLengthRatio * handsTailLength))" :y1="5 + (1 * (sHandLengthRatio * handsTailLength))" @@ -73,9 +74,10 @@ </template> <script lang="ts" setup> -import { computed, onMounted, onBeforeUnmount } from 'vue'; +import { computed, onMounted, onBeforeUnmount, ref } from 'vue'; import tinycolor from 'tinycolor2'; 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 const angleDiff = (a: number, b: number) => { @@ -145,6 +147,7 @@ let mAngle = $ref<number>(0); let sAngle = $ref<number>(0); let disableSAnimate = $ref(false); let sOneRound = false; +const sLine = ref<SVGPathElement>(); function tick() { 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); mAngle = Math.PI * (m + s / 60) / 30; - if (sOneRound) { // 秒é‡ãŒä¸€å‘¨ã—ãŸéš›ã®ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’よã—ãªã«å‡¦ç†ã™ã‚‹(ã“ã‚ŒãŒç„¡ã„ã¨ç§’ãŒ59->0ã«ãªã£ãŸã¨ãã«æœŸå¾…ã—ãŸã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã«ãªã‚‰ãªã„) + if (sOneRound && sLine.value) { // 秒é‡ãŒä¸€å‘¨ã—ãŸéš›ã®ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’よã—ãªã«å‡¦ç†ã™ã‚‹(ã“ã‚ŒãŒç„¡ã„ã¨ç§’ãŒ59->0ã«ãªã£ãŸã¨ãã«æœŸå¾…ã—ãŸã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã«ãªã‚‰ãªã„) sAngle = Math.PI * 60 / 30; - window.setTimeout(() => { + defaultIdlingRenderScheduler.delete(tick); + sLine.value.addEventListener('transitionend', () => { disableSAnimate = true; - window.setTimeout(() => { + requestAnimationFrame(() => { sAngle = 0; - window.setTimeout(() => { + requestAnimationFrame(() => { disableSAnimate = false; - }, 100); - }, 100); - }, 700); + if (enabled) { + defaultIdlingRenderScheduler.add(tick); + } + }); + }); + }, { once: true }); } else { sAngle = Math.PI * s / 30; } @@ -194,20 +201,13 @@ function calcColors() { calcColors(); onMounted(() => { - const update = () => { - if (enabled) { - tick(); - window.setTimeout(update, 1000); - } - }; - update(); - + defaultIdlingRenderScheduler.add(tick); globalEvents.on('themeChanged', calcColors); }); onBeforeUnmount(() => { enabled = false; - + defaultIdlingRenderScheduler.delete(tick); globalEvents.off('themeChanged', calcColors); }); </script> diff --git a/packages/frontend/src/components/MkDigitalClock.stories.impl.ts b/packages/frontend/src/components/MkDigitalClock.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..344f6de47c3050cd241d5a5497956fd8b9e99a2d --- /dev/null +++ b/packages/frontend/src/components/MkDigitalClock.stories.impl.ts @@ -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>; diff --git a/packages/frontend/src/components/MkDigitalClock.vue b/packages/frontend/src/components/MkDigitalClock.vue index 278dc8a5e7ebbedb784bf52b430177d8071d159c..aea20f24890de1e7d70e15ca8dc8eced22d87ccf 100644 --- a/packages/frontend/src/components/MkDigitalClock.vue +++ b/packages/frontend/src/components/MkDigitalClock.vue @@ -11,19 +11,21 @@ </template> <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<{ showS?: boolean; showMs?: boolean; offset?: number; + now?: () => Date; }>(), { showS: true, showMs: false, offset: 0 - new Date().getTimezoneOffset(), + now: () => new Date(), }); -let intervalId; const hh = ref(''); const mm = ref(''); const ss = ref(''); @@ -39,9 +41,9 @@ watch(showColon, (v) => { } }); -const tick = () => { - const now = new Date(); - now.setMinutes(now.getMinutes() + (new Date().getTimezoneOffset() + props.offset)); +const tick = (): void => { + const now = props.now(); + now.setMinutes(now.getMinutes() + now.getTimezoneOffset() + props.offset); hh.value = now.getHours().toString().padStart(2, '0'); mm.value = now.getMinutes().toString().padStart(2, '0'); ss.value = now.getSeconds().toString().padStart(2, '0'); @@ -52,13 +54,12 @@ const tick = () => { tick(); -watch(() => props.showMs, () => { - if (intervalId) window.clearInterval(intervalId); - intervalId = window.setInterval(tick, props.showMs ? 10 : 1000); -}, { immediate: true }); +onMounted(() => { + defaultIdlingRenderScheduler.add(tick); +}); onUnmounted(() => { - window.clearInterval(intervalId); + defaultIdlingRenderScheduler.delete(tick); }); </script> diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index 261cc0ee18ca2f93c7ac927d1aa2cb5f4af8d0ea..dfc3c89798b6b4837a35a06207b1c81ea7a9434f 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -58,7 +58,6 @@ function tick() { if (props.mode === 'relative' || props.mode === 'detail') { tick(); - onUnmounted(() => { window.clearTimeout(tickId); }); diff --git a/packages/frontend/src/scripts/idle-render.ts b/packages/frontend/src/scripts/idle-render.ts new file mode 100644 index 0000000000000000000000000000000000000000..ccce8b02bfc47afb3ab75670a3fbe984f1d6cfb6 --- /dev/null +++ b/packages/frontend/src/scripts/idle-render.ts @@ -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();