191 lines
5.3 KiB
TypeScript
191 lines
5.3 KiB
TypeScript
'use client'
|
|
|
|
import { useMemo, useEffect } from 'react'
|
|
import { Canvas, ThreeEvent, useFrame, useThree } from '@react-three/fiber'
|
|
import { shaderMaterial, useTrailTexture } from '@react-three/drei'
|
|
import { useTheme } from 'next-themes' // lub twój provider
|
|
import * as THREE from 'three'
|
|
|
|
const DotMaterial = shaderMaterial(
|
|
{
|
|
time: 0,
|
|
resolution: new THREE.Vector2(),
|
|
dotColor: new THREE.Color('#FFFFFF'),
|
|
bgColor: new THREE.Color('#121212'),
|
|
mouseTrail: null,
|
|
render: 0,
|
|
rotation: 0,
|
|
gridSize: 50,
|
|
dotOpacity: 0.05
|
|
},
|
|
/* glsl */ `
|
|
void main() {
|
|
gl_Position = vec4(position.xy, 0.0, 1.0);
|
|
}
|
|
`,
|
|
/* glsl */ `
|
|
uniform float time;
|
|
uniform int render;
|
|
uniform vec2 resolution;
|
|
uniform vec3 dotColor;
|
|
uniform vec3 bgColor;
|
|
uniform sampler2D mouseTrail;
|
|
uniform float rotation;
|
|
uniform float gridSize;
|
|
uniform float dotOpacity;
|
|
|
|
vec2 rotate(vec2 uv, float angle) {
|
|
float s = sin(angle);
|
|
float c = cos(angle);
|
|
mat2 rotationMatrix = mat2(c, -s, s, c);
|
|
return rotationMatrix * (uv - 0.5) + 0.5;
|
|
}
|
|
|
|
vec2 coverUv(vec2 uv) {
|
|
vec2 s = resolution.xy / max(resolution.x, resolution.y);
|
|
vec2 newUv = (uv - 0.5) * s + 0.5;
|
|
return clamp(newUv, 0.0, 1.0);
|
|
}
|
|
|
|
float sdfCircle(vec2 p, float r) {
|
|
return length(p - 0.5) - r;
|
|
}
|
|
|
|
void main() {
|
|
vec2 screenUv = gl_FragCoord.xy / resolution;
|
|
vec2 uv = coverUv(screenUv);
|
|
|
|
vec2 rotatedUv = rotate(uv, rotation);
|
|
|
|
// Create a grid
|
|
vec2 gridUv = fract(rotatedUv * gridSize);
|
|
vec2 gridUvCenterInScreenCoords = rotate((floor(rotatedUv * gridSize) + 0.5) / gridSize, -rotation);
|
|
|
|
// Calculate distance from the center of each cell
|
|
float baseDot = sdfCircle(gridUv, 0.25);
|
|
|
|
// Screen mask
|
|
float screenMask = smoothstep(0.0, 1.0, 1.0 - uv.y); // 0 at the top, 1 at the bottom
|
|
vec2 centerDisplace = vec2(0.7, 1.1);
|
|
float circleMaskCenter = length(uv - centerDisplace);
|
|
float circleMaskFromCenter = smoothstep(0.5, 1.0, circleMaskCenter);
|
|
|
|
float combinedMask = screenMask * circleMaskFromCenter;
|
|
float circleAnimatedMask = sin(time * 2.0 + circleMaskCenter * 10.0);
|
|
|
|
// Mouse trail effect
|
|
float mouseInfluence = texture2D(mouseTrail, gridUvCenterInScreenCoords).r;
|
|
|
|
float scaleInfluence = max(mouseInfluence * 0.5, circleAnimatedMask * 0.3);
|
|
|
|
// Create dots with animated scale, influenced by mouse
|
|
float dotSize = min(pow(circleMaskCenter, 2.0) * 0.3, 0.3);
|
|
|
|
float sdfDot = sdfCircle(gridUv, dotSize * (1.0 + scaleInfluence * 0.5));
|
|
|
|
float smoothDot = smoothstep(0.05, 0.0, sdfDot);
|
|
|
|
float opacityInfluence = max(mouseInfluence * 50.0, circleAnimatedMask * 0.5);
|
|
|
|
// Mix background color with dot color, using animated opacity to increase visibility
|
|
vec3 composition = mix(bgColor, dotColor, smoothDot * combinedMask * dotOpacity * (1.0 + opacityInfluence));
|
|
|
|
gl_FragColor = vec4(composition, 1.0);
|
|
|
|
#include <tonemapping_fragment>
|
|
#include <colorspace_fragment>
|
|
}
|
|
`
|
|
)
|
|
|
|
function Scene() {
|
|
const size = useThree((s) => s.size)
|
|
const viewport = useThree((s) => s.viewport)
|
|
const { theme } = useTheme()
|
|
|
|
const rotation = 0
|
|
const gridSize = 100
|
|
|
|
const getThemeColors = () => {
|
|
switch (theme) {
|
|
case 'dark':
|
|
return {
|
|
dotColor: '#FFFFFF',
|
|
bgColor: '#121212',
|
|
dotOpacity: 0.025
|
|
}
|
|
case 'light':
|
|
return {
|
|
dotColor: '#e1e1e1',
|
|
bgColor: '#F4F5F5',
|
|
dotOpacity: 0.15
|
|
}
|
|
default:
|
|
return {
|
|
dotColor: '#FFFFFF',
|
|
bgColor: '#121212',
|
|
dotOpacity: 0.05
|
|
}
|
|
}
|
|
}
|
|
|
|
const themeColors = getThemeColors()
|
|
|
|
const [trail, onMove] = useTrailTexture({
|
|
size: 512,
|
|
radius: 0.1,
|
|
maxAge: 400,
|
|
interpolate: 1,
|
|
ease: function easeInOutCirc(x) {
|
|
return x < 0.5 ? (1 - Math.sqrt(1 - Math.pow(2 * x, 2))) / 2 : (Math.sqrt(1 - Math.pow(-2 * x + 2, 2)) + 1) / 2
|
|
}
|
|
})
|
|
|
|
const dotMaterial = useMemo(() => {
|
|
return new DotMaterial()
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
dotMaterial.uniforms.dotColor.value.setHex(themeColors.dotColor.replace('#', '0x'))
|
|
dotMaterial.uniforms.bgColor.value.setHex(themeColors.bgColor.replace('#', '0x'))
|
|
dotMaterial.uniforms.dotOpacity.value = themeColors.dotOpacity
|
|
}, [theme, dotMaterial, themeColors])
|
|
|
|
useFrame((state) => {
|
|
dotMaterial.uniforms.time.value = state.clock.elapsedTime
|
|
})
|
|
|
|
const handlePointerMove = (e: ThreeEvent<PointerEvent>) => {
|
|
onMove(e)
|
|
}
|
|
|
|
const scale = Math.max(viewport.width, viewport.height) / 2
|
|
|
|
return (
|
|
<mesh scale={[scale, scale, 1]} onPointerMove={handlePointerMove}>
|
|
<planeGeometry args={[2, 2]} />
|
|
<primitive
|
|
object={dotMaterial}
|
|
resolution={[size.width * viewport.dpr, size.height * viewport.dpr]}
|
|
rotation={rotation}
|
|
gridSize={gridSize}
|
|
mouseTrail={trail}
|
|
render={0}
|
|
/>
|
|
</mesh>
|
|
)
|
|
}
|
|
|
|
export const DotScreenShader = () => {
|
|
return (
|
|
<Canvas
|
|
gl={{
|
|
antialias: true,
|
|
powerPreference: 'high-performance',
|
|
outputColorSpace: THREE.SRGBColorSpace,
|
|
toneMapping: THREE.NoToneMapping
|
|
}}>
|
|
<Scene />
|
|
</Canvas>
|
|
)
|
|
} |