feat: impl IdleRender
This commit is contained in:
		| @@ -397,6 +397,8 @@ function toStories(component: string): string { | ||||
| // glob('src/{components,pages,ui,widgets}/**/*.vue') | ||||
| Promise.all([ | ||||
| 	glob('src/components/global/*.vue'), | ||||
| 	glob('src/components/MkAnalogClock.vue'), | ||||
| 	glob('src/components/MkDigitalClock.vue'), | ||||
| 	glob('src/components/MkGalleryPostPreview.vue'), | ||||
| 	glob('src/pages/user/home.vue'), | ||||
| ]) | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| /* eslint-disable @typescript-eslint/explicit-function-return-type */ | ||||
| import { StoryObj } from '@storybook/vue3'; | ||||
| import isChromatic from 'chromatic'; | ||||
| import MkAnalogClock from './MkAnalogClock.vue'; | ||||
| export const Default = { | ||||
| 	render(args) { | ||||
| @@ -22,6 +23,14 @@ export const Default = { | ||||
| 			template: '<MkAnalogClock v-bind="props" />', | ||||
| 		}; | ||||
| 	}, | ||||
| 	args: { | ||||
| 		now: isChromatic() ? () => new Date('2023-01-01T10:10:30') : undefined, | ||||
| 	}, | ||||
| 	decorators: [ | ||||
| 		() => ({ | ||||
| 			template: '<div style="container-type:inline-size;height:100%"><div style="height:100cqmin;margin:auto;width:100cqmin"><story/></div></div>', | ||||
| 		}), | ||||
| 	], | ||||
| 	parameters: { | ||||
| 		layout: 'fullscreen', | ||||
| 	}, | ||||
|   | ||||
| @@ -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 { defaultIdleRender } 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) => { | ||||
| @@ -99,6 +101,7 @@ const props = withDefaults(defineProps<{ | ||||
| 	graduations?: 'none' | 'dots' | 'numbers'; | ||||
| 	fadeGraduations?: boolean; | ||||
| 	sAnimation?: 'none' | 'elastic' | 'easeOut'; | ||||
| 	now?: () => Date; | ||||
| }>(), { | ||||
| 	numbers: false, | ||||
| 	thickness: 0.1, | ||||
| @@ -107,6 +110,7 @@ const props = withDefaults(defineProps<{ | ||||
| 	graduations: 'dots', | ||||
| 	fadeGraduations: true, | ||||
| 	sAnimation: 'elastic', | ||||
| 	now: () => new Date(), | ||||
| }); | ||||
|  | ||||
| const graduationsMajor = computed(() => { | ||||
| @@ -143,26 +147,37 @@ 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 = new Date(); | ||||
| 	now.setMinutes(now.getMinutes() + (new Date().getTimezoneOffset() + props.offset)); | ||||
| 	const now = props.now(); | ||||
| 	now.setMinutes(now.getMinutes() + now.getTimezoneOffset() + props.offset); | ||||
| 	const previousS = s; | ||||
| 	const previousM = m; | ||||
| 	const previousH = h; | ||||
| 	s = now.getSeconds(); | ||||
| 	m = now.getMinutes(); | ||||
| 	h = now.getHours(); | ||||
| 	if (previousS === s && previousM === m && previousH === h) { | ||||
| 		return; | ||||
| 	} | ||||
| 	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(() => { | ||||
| 		defaultIdleRender.delete(tick); | ||||
| 		sLine.value.addEventListener('transitionend', () => { | ||||
| 			disableSAnimate = true; | ||||
| 			window.setTimeout(() => { | ||||
| 			requestAnimationFrame(() => { | ||||
| 				sAngle = 0; | ||||
| 				window.setTimeout(() => { | ||||
| 				requestAnimationFrame(() => { | ||||
| 					disableSAnimate = false; | ||||
| 				}, 100); | ||||
| 			}, 100); | ||||
| 		}, 700); | ||||
| 					if (enabled) { | ||||
| 						defaultIdleRender.add(tick); | ||||
| 					} | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, { once: true }); | ||||
| 	} else { | ||||
| 		sAngle = Math.PI * s / 30; | ||||
| 	} | ||||
| @@ -186,20 +201,13 @@ function calcColors() { | ||||
| calcColors(); | ||||
|  | ||||
| onMounted(() => { | ||||
| 	const update = () => { | ||||
| 		if (enabled) { | ||||
| 			tick(); | ||||
| 			window.setTimeout(update, 1000); | ||||
| 		} | ||||
| 	}; | ||||
| 	update(); | ||||
|  | ||||
| 	defaultIdleRender.add(tick); | ||||
| 	globalEvents.on('themeChanged', calcColors); | ||||
| }); | ||||
|  | ||||
| onBeforeUnmount(() => { | ||||
| 	enabled = false; | ||||
|  | ||||
| 	defaultIdleRender.delete(tick); | ||||
| 	globalEvents.off('themeChanged', calcColors); | ||||
| }); | ||||
| </script> | ||||
|   | ||||
| @@ -11,7 +11,8 @@ | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { onUnmounted, ref, watch } from 'vue'; | ||||
| import { onMounted, onUnmounted, ref, watch } from 'vue'; | ||||
| import { defaultIdleRender } from '@/scripts/idle-render.js'; | ||||
|  | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	showS?: boolean; | ||||
| @@ -23,7 +24,6 @@ const props = withDefaults(defineProps<{ | ||||
| 	offset: 0 - new Date().getTimezoneOffset(), | ||||
| }); | ||||
|  | ||||
| let intervalId; | ||||
| const hh = ref(''); | ||||
| const mm = ref(''); | ||||
| const ss = ref(''); | ||||
| @@ -52,13 +52,12 @@ const tick = () => { | ||||
|  | ||||
| tick(); | ||||
|  | ||||
| watch(() => props.showMs, () => { | ||||
| 	if (intervalId) window.clearInterval(intervalId); | ||||
| 	intervalId = window.setInterval(tick, props.showMs ? 10 : 1000); | ||||
| }, { immediate: true }); | ||||
| onMounted(() => { | ||||
| 	defaultIdleRender.add(tick); | ||||
| }); | ||||
|  | ||||
| onUnmounted(() => { | ||||
| 	window.clearInterval(intervalId); | ||||
| 	defaultIdleRender.remove(tick); | ||||
| }); | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -11,6 +11,7 @@ | ||||
| import { onUnmounted } from 'vue'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { dateTimeFormat } from '@/scripts/intl-const'; | ||||
| import { defaultIdleRender } from '@/scripts/idle-render.js'; | ||||
|  | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	time: Date | string | number | null; | ||||
| @@ -45,21 +46,16 @@ const relative = $computed<string>(() => { | ||||
| 		i18n.ts._ago.future); | ||||
| }); | ||||
|  | ||||
| let tickId: number; | ||||
|  | ||||
| function tick() { | ||||
| function tick(): void { | ||||
| 	now = props.origin ?? (new Date()).getTime(); | ||||
| 	const ago = (now - _time) / 1000/*ms*/; | ||||
| 	const next = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000; | ||||
|  | ||||
| 	tickId = window.setTimeout(tick, next); | ||||
| } | ||||
|  | ||||
| if (props.mode === 'relative' || props.mode === 'detail') { | ||||
| 	tick(); | ||||
| 	defaultIdleRender.add(tick); | ||||
|  | ||||
| 	onUnmounted(() => { | ||||
| 		window.clearTimeout(tickId); | ||||
| 		defaultIdleRender.delete(tick); | ||||
| 	}); | ||||
| } | ||||
| </script> | ||||
|   | ||||
							
								
								
									
										41
									
								
								packages/frontend/src/scripts/idle-render.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								packages/frontend/src/scripts/idle-render.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| // eslint-disable-next-line import/no-default-export | ||||
| export default class IdleRender { | ||||
| 	#renderers: Set<FrameRequestCallback>; | ||||
| 	#budget: number; | ||||
| 	#rafId: number; | ||||
| 	#ricId: number; | ||||
|  | ||||
| 	constructor(budget = 0) { | ||||
| 		this.#renderers = new Set(); | ||||
| 		this.#budget = budget; | ||||
| 		this.#rafId = 0; | ||||
| 		this.#ricId = requestIdleCallback((deadline) => this.#render(deadline)); | ||||
| 	} | ||||
|  | ||||
| 	#render(deadline: IdleDeadline): void { | ||||
| 		if (deadline.timeRemaining() > this.#budget) { | ||||
| 			this.#rafId = requestAnimationFrame((time) => { | ||||
| 				for (const renderer of this.#renderers) { | ||||
| 					renderer(time); | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 		this.#ricId = requestIdleCallback((arg) => this.#render(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 defaultIdleRender = new IdleRender(); | ||||
		Reference in New Issue
	
	Block a user
	 Acid Chicken (硫酸鶏)
					Acid Chicken (硫酸鶏)