+ {children} diff --git a/src/components/texts/privacy.mdx b/src/components/texts/privacy.mdx index 2cba190..d8839b9 100644 --- a/src/components/texts/privacy.mdx +++ b/src/components/texts/privacy.mdx @@ -1,5 +1,5 @@ # Privacy Policy -This website ("nonszy.space") fully respects the privacy of its visitors. This document explains how we handle visitor information. The short answer is: **we do not collect, track, or store your personal information**. +This website ("nonszy.space") fully respects the privacy of its visitors. This page explains how we handle visitor information. The short answer is: **we do not collect, track, or store your personal information**. However, future feature additions will update the privacy policy. \ No newline at end of file diff --git a/src/components/wobbling-image.tsx b/src/components/wobbling-image.tsx new file mode 100644 index 0000000..169eda1 --- /dev/null +++ b/src/components/wobbling-image.tsx @@ -0,0 +1,132 @@ +'use client' + +import clsx from "clsx"; +import Image from "next/image"; +import { useCallback, useEffect, useRef, useState } from "react" + +interface WobblingImageInterface { + img1: string + img2?: string +} + +function WobblingImage ({ + img1, img2 +}: WobblingImageInterface) { + const size = 400; + const [isAnimating, setIsAnimating] = useState(false); + + const audioPath = "/sound/poke.wav"; + const audioContextRef = useRef(null); + const [audioBuffer, setAudioBuffer] = useState(null); + const [audioLoaded, setAudioLoaded] = useState(false); + + const [dummyAudio, setDummyAudio] = useState(null); + + useEffect(() => { + if (typeof Audio !== undefined) setDummyAudio(new Audio(audioPath)) + return () => { + if (dummyAudio) dummyAudio.pause(); + } + }, []) + + const initializeAudioContext = useCallback((): AudioContext => { + if (!audioContextRef.current) { + audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)(); + } + return audioContextRef.current; + }, []); + + const loadAudio = useCallback(async (url: string): Promise => { + const response = await fetch(url); + const arrayBuffer = await response.arrayBuffer(); + const context = initializeAudioContext(); + return await context.decodeAudioData(arrayBuffer); + }, [initializeAudioContext]); + + const initializeAudio = useCallback(async (): Promise => { + if (audioBuffer) return; + + setAudioLoaded(false); + try { + const buffer = await loadAudio(audioPath); + setAudioBuffer(buffer); + } catch (error) { + console.error('Failed to load audio:', error); + } + finally { + setAudioLoaded(true); + } + }, [audioBuffer, loadAudio]); + + const playRandomPitch = useCallback(async (): Promise => { + await initializeAudio(); + + if (!audioBuffer || !audioContextRef.current) return; + + const context = audioContextRef.current; + const changeToLowPitch = Math.floor(Math.random() * 10); + const randomPitch = Math.random() * 0.2 + (changeToLowPitch == 1 ? 0.25 : 0.85); + + const source = context.createBufferSource(); + source.buffer = audioBuffer; + source.playbackRate.value = randomPitch; + source.connect(context.destination); + source.start(0); + + source.onended = () => { + source.disconnect(); + }; + }, [audioBuffer, initializeAudio]); + + + function handleClick() { + setIsAnimating(false); + requestAnimationFrame(() => setIsAnimating(true)); + playRandomPitch(); + if (dummyAudio && !audioLoaded) { + dummyAudio.currentTime = 0; + dummyAudio.play(); + } + } + + function handleAnimationEnd() { + setIsAnimating(false); + } + + return ( + + { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleClick(); } }} + onMouseDown={(e) => e.preventDefault()} + onPointerDown={(e) => e.preventDefault()} + > + + + + + ) +} + +export default WobblingImage; \ No newline at end of file diff --git a/tailwind.config.ts b/tailwind.config.ts index 7fce617..4f7a79f 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -24,6 +24,7 @@ const config: Config = { }, animation: { "silly-bouncing": 'silly-bounce 0.8s infinite', + "click-bouncing": 'click-bounce 200ms', 'window-popup': 'window-popup 1s', 'window-popdown': 'window-popdown 1s' },