← каталог
Distortion Sphere
Дышащая сфера с вершинной дисторсией (слоистый шум в vertex-шейдере) и фейк-френелем; курсор раскачивает амплитуду.
threemouse-moveautomorphadvanced~160 КБreduced-motion ✓
Установка
npx shadcn@latest add https://motiva.pages.dev/r/distortion-sphere.jsonзависимости: three@^0.185.1 · @react-three/fiber@^9.6.1
Что нужно в проекте до установки
components.jsonв корне — с алиасами в форме"components": "@/components". Нет файла — CLI не поймёт, куда класть.- Алиас
@объявлен дважды:"paths": {"@/*": ["./src/*"]}в корневомtsconfig.json(в Vite он часто содержит толькоreferences— тогда CLI создаёт папку@) иresolve.aliasв конфиге сборщика. - Токены подключены один раз на приложение:
import "@/styles/motiva-tokens.css"— они приезжают вместе с записью. Без них секция потеряет цвета, ритм и тему. - Своя типографика не должна спорить: если у вас на обёртке
text-align: center, он протечёт внутрь блоков. - Проект без TypeScript? Всё равно нужен минимальный
tsconfig.jsonсpathsи"tsx": trueвcomponents.json: CLI грузит tsconfig безусловно, а с"tsx": falseидёт заjsconfig.jsonи падает так же. Приехавший рядом.tsxможно не трогать — вам нужен.vanilla.js.
Экспорт
Код
"use client";
import { Canvas, useFrame } from "@react-three/fiber";
import { useMemo, useRef } from "react";
import * as THREE from "three";
const vertex = /* glsl */ `
uniform float uTime;
uniform float uAmp;
varying float vNoise;
varying vec3 vNormal;
// компактный слоистый trig-шум — без тяжёлого simplex, для сферы выглядит органично
float noise3(vec3 p) {
return (
sin(p.x * 2.1 + uTime * 0.9) * 0.45 +
sin(p.y * 3.3 + uTime * 0.7) * 0.3 +
sin(p.z * 4.2 + uTime * 1.2) * 0.25 +
sin((p.x + p.y + p.z) * 5.0 - uTime * 1.5) * 0.18
);
}
void main() {
vNormal = normal;
float n = noise3(position);
vNoise = n;
vec3 displaced = position + normal * n * uAmp;
gl_Position = projectionMatrix * modelViewMatrix * vec4(displaced, 1.0);
}
`;
const fragment = /* glsl */ `
varying float vNoise;
varying vec3 vNormal;
void main() {
// окраска по «высоте» шума + фейк-френель по нормали
float fresnel = pow(1.0 - abs(vNormal.z), 2.0);
vec3 base = mix(vec3(0.32, 0.16, 0.62), vec3(0.94, 0.42, 0.62), vNoise * 0.5 + 0.5);
vec3 color = base + fresnel * vec3(0.35, 0.5, 0.9);
gl_FragColor = vec4(color, 1.0);
}
`;
function Blob({ frozen }: { frozen: boolean }) {
const mat = useRef<THREE.ShaderMaterial>(null);
const mesh = useRef<THREE.Mesh>(null);
const targetAmp = useRef(0.18);
const uniforms = useMemo(
() => ({
uTime: { value: 0 },
uAmp: { value: 0.18 },
}),
[],
);
useFrame((_, delta) => {
if (!mat.current || !mesh.current) return;
// ВАЖНО: пишем в uniforms ЖИВОГО материала. R3F копирует объект из пропа `uniforms`
// в свой (Object.assign на каждый ключ) — мутация исходного объекта до GPU не доедет.
const u = mat.current.uniforms;
u.uTime!.value += delta;
u.uAmp!.value += (targetAmp.current - u.uAmp!.value) * 0.06;
targetAmp.current += (0.18 - targetAmp.current) * 0.02; // спад к покою
mesh.current.rotation.y += delta * 0.15;
});
return (
<mesh
ref={mesh}
onPointerMove={(e) => {
if (frozen) return;
// амплитуда растёт от скорости курсора по сфере
targetAmp.current = Math.min(0.65, targetAmp.current + (Math.abs(e.movementX) + Math.abs(e.movementY)) * 0.002);
}}
>
<icosahedronGeometry args={[1.35, 48]} />
<shaderMaterial ref={mat} uniforms={uniforms} vertexShader={vertex} fragmentShader={fragment} />
</mesh>
);
}
/** Дышащая дисторсия-сфера (vertex-шум в шейдере); движение курсора по ней раскачивает амплитуду. */
export function DistortionSphere() {
const frozen =
typeof document !== "undefined" &&
(document.documentElement.hasAttribute("data-shot") ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches);
return (
<div style={{ position: "absolute", inset: 0 }}>
<Canvas camera={{ position: [0, 0, 4], fov: 50 }} frameloop={frozen ? "demand" : "always"} dpr={[1, 2]}>
<Blob frozen={frozen} />
</Canvas>
</div>
);
}
Похожие эффекты
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0