+ Sound effects: https://snd.dev/ +
{/* contents end */} + + diff --git a/src/scripts/alpinejs/drop-text.component.ts b/src/scripts/alpinejs/drop-text.component.ts new file mode 100644 index 0000000..5ad56b3 --- /dev/null +++ b/src/scripts/alpinejs/drop-text.component.ts @@ -0,0 +1,208 @@ +import sound1 from '@/assets/audio/button.wav' +import sound3 from '@/assets/audio/notification.wav' +import sound4 from '@/assets/audio/progress_loop.wav' +import sound5 from '@/assets/audio/toggle_off.wav' +import Alpine from 'alpinejs' +import gsap from 'gsap' +import { Physics2DPlugin } from 'gsap/Physics2DPlugin' +import { SplitText } from 'gsap/SplitText' +import GUI from 'lil-gui' +import debounce from 'lodash/debounce' + +Alpine.data('dropText', () => { + const soundSources = [sound1, sound3, sound4, sound5] as const + let sounds: HTMLAudioElement[] | null = null + + let splitInstance: SplitText | null = null + let splitInstance2: SplitText | null = null + + let pointerSpeed = 0 + let pointerLastX = 0 + let pointerLastY = 0 + let lastTime = 0 + + let isResetAnimating = false + + const gui = new GUI() + + return { + guiParams: { + text: '👉Hover me!!!', + textColor: '#1C1C1C', + gravity: 800, + acceleration: 1200, + velocity: { + min: 100, + max: 650, + }, + randomRotate: 90, + velocityMultiplier: 1, + spinMultiplier: 1, // 回転倍率 (1 = 現在と同等, 4 = 4倍激しく) + }, + init() { + gsap.ticker.fps(60) + gsap.registerPlugin(Physics2DPlugin, SplitText) + this.initAudio() + this.updateText() + this.setGui() + }, + initAudio() { + sounds = soundSources.map((src) => { + const audio = new Audio(src) + audio.preload = 'auto' + audio.volume = 0.5 + return audio + }) + }, + drop(el: HTMLElement, event: PointerEvent) { + // 既に落下開始済みなら二度反応させない + if (el.dataset.dropped || isResetAnimating) return + el.dataset.dropped = 'true' + el.classList.add('!pointer-events-none') + this.playSound() + + const rect = el.getBoundingClientRect() + const centerX = rect.left + rect.width / 2 + const centerY = rect.top + rect.height / 2 + const deltaX = event.clientX - centerX + const deltaY = event.clientY - centerY + + const angle = gsap.utils.wrap(0, 360, (Math.atan2(deltaY, deltaX) * 180) / Math.PI + 180) + const velocity = + gsap.utils.clamp( + this.guiParams.velocity.min, + this.guiParams.velocity.max, + pointerSpeed * 1.2, + ) * this.guiParams.velocityMultiplier + + gsap.to(el, { + duration: 5, + physics2D: { + angle: angle, + velocity: velocity, + gravity: this.guiParams.gravity, + // acceleration: this.guiParams.acceleration, + }, + rotation: + gsap.utils.random(-this.guiParams.randomRotate, this.guiParams.randomRotate) * + this.guiParams.spinMultiplier, + onComplete: () => { + el.style.visibility = 'hidden' + }, + }) + }, + onPointermove(event: PointerEvent) { + const now = performance.now() + if (lastTime) { + const dt = now - lastTime + const dx = event.clientX - pointerLastX + const dy = event.clientY - pointerLastY + const distance = Math.hypot(dx, dy) + pointerSpeed = (distance / dt) * 1000 // px/s + } + lastTime = now + pointerLastX = event.clientX + pointerLastY = event.clientY + }, + destroySplitInstance() { + if (splitInstance) { + splitInstance.revert() + splitInstance = null + } + if (splitInstance2) { + splitInstance2.revert() + splitInstance2 = null + } + }, + updateText() { + this.destroySplitInstance() + + // テキストを書き換え + this.$refs.hoverText.innerHTML = this.guiParams.text + this.$refs.staticText.innerHTML = this.guiParams.text + + // 再スプリット + splitInstance = SplitText.create(this.$refs.hoverText, { type: 'chars' }) + splitInstance2 = SplitText.create(this.$refs.staticText, { type: 'chars' }) + + splitInstance.chars.forEach((char) => { + char.classList.add('cursor-pointer') + // @ts-ignore + char.addEventListener('pointerenter', (event: PointerEvent) => this.drop(char, event)) + }) + }, + playSound() { + if (!sounds || !sounds.length) return + const audio = sounds[Math.floor(Math.random() * sounds.length)] + audio.currentTime = 0 + audio.volume = 0.5 + audio.play().catch(() => {}) + }, + randomText() { + const phrases = [ + 'Breathe in calm 😌', + 'Keep moving!', + 'Drift on silent clouds', + 'Slow moments, soft hearts', + 'Coffee steam and