Motiva
Мои коллекции
← каталог

Noise / Grain

Плёночное зерно поверх любого фона: SVG feTurbulence как data-URI + steps()-джиттер — зерно «живёт» как на плёнке.

cssautobasic0reduced-motion ✓
🎛 Настройки — крути и копируй

Пропсы

Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.

ПропТипПо умолчаниюЧто это
children?ReactNode
opacity?number0.25Непрозрачность зерна 0–1 (формат «0.25»).
size?number240Размер тайла зерна, px (формат «240»): меньше — мельче зерно.
blend?"screen" | "overlay" | "soft-light" | "normal""screen"Режим смешивания плёнки.
animated?booleantrueЖивое зерно (лёгкий джиттер кадрами) или статичная плёнка.

Установка

npx shadcn@latest add https://motiva.pages.dev/r/noise-grain.json
Что нужно в проекте до установки
  1. components.json в корне — с алиасами в форме"components": "@/components". Нет файла — CLI не поймёт, куда класть.
  2. Алиас @ объявлен дважды: "paths": {"@/*": ["./src/*"]} в корневом tsconfig.json (в Vite он часто содержит только references — тогда CLI создаёт папку@) и resolve.alias в конфиге сборщика.
  3. Токены подключены один раз на приложение:import "@/styles/motiva-tokens.css" — они приезжают вместе с записью. Без них секция потеряет цвета, ритм и тему.
  4. Своя типографика не должна спорить: если у вас на обёрткеtext-align: center, он протечёт внутрь блоков.
  5. Проект без TypeScript? Всё равно нужен минимальныйtsconfig.json с pathsи "tsx": true в components.json: CLI грузит tsconfig безусловно, а с "tsx": false идёт за jsconfig.json и падает так же. Приехавший рядом.tsx можно не трогать — вам нужен.vanilla.js.

Экспорт

↓ Скачать .zip

Код

import type { CSSProperties, ReactNode } from "react";

/** SVG-шум (feTurbulence) как data-URI — без внешних ассетов и canvas. */
const NOISE_URI =
  "data:image/svg+xml," +
  encodeURIComponent(
    `<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3" stitchTiles="stitch"/><feColorMatrix type="saturate" values="0"/></filter><rect width="240" height="240" filter="url(%23n)" opacity="0.85"/></svg>`,
  );

export interface NoiseGrainProps {
  children?: ReactNode;
  /** Непрозрачность зерна 0–1 (формат «0.25»). */
  opacity?: number;
  /** Размер тайла зерна, px (формат «240»): меньше — мельче зерно. */
  size?: number;
  /** Режим смешивания плёнки. */
  blend?: "screen" | "overlay" | "soft-light" | "normal";
  /** Живое зерно (лёгкий джиттер кадрами) или статичная плёнка. */
  animated?: boolean;
}

/**
 * Плёночное зерно поверх любого фона: SVG feTurbulence data-URI + steps()-джиттер позиции —
 * зерно «живёт» как на плёнке. Чистый CSS. Reduced-motion/data-shot — зерно статично.
 */
export function NoiseGrain({ children, opacity = 0.25, size = 240, blend = "screen", animated = true }: NoiseGrainProps) {
  return (
    <div style={{ position: "absolute", inset: 0, overflow: "hidden" }}>
      <style>{`
        /* джиттер через transform (композитор), НЕ background-position — тот перекрашивает
           весь полноэкранный слой на каждый шаг */
        @keyframes ng-jitter {
          0% { transform: translate(0, 0); } 25% { transform: translate(-70px, 40px); }
          50% { transform: translate(40px, -90px); } 75% { transform: translate(-30px, -50px); }
          100% { transform: translate(60px, 70px); }
        }
        .ng-film { animation: ng-jitter 0.7s steps(5) infinite; will-change: transform; }
        @media (prefers-reduced-motion: reduce) { .ng-film { animation: none; } }
      `}</style>
      {children ?? (
        <div
          style={{
            position: "absolute",
            inset: 0,
            background:
              "radial-gradient(80% 90% at 30% 20%, oklch(0.45 0.12 300 / 0.7), transparent 60%), radial-gradient(70% 80% at 75% 75%, oklch(0.5 0.12 220 / 0.55), transparent 60%), oklch(0.16 0.02 270)",
            display: "grid",
            placeItems: "center",
            color: "oklch(0.92 0.01 265)",
          }}
        >
          <div style={{ textAlign: "center" }}>
            <div style={{ fontSize: "1.8rem", fontWeight: 700, letterSpacing: "-0.02em" }}>Noise / Grain</div>
            <div style={{ marginTop: 6, fontFamily: "ui-monospace, monospace", fontSize: "0.75rem", opacity: 0.6 }}>
              плёночное зерно поверх градиента
            </div>
          </div>
        </div>
      )}
      <div
        className={animated ? "ng-film" : undefined}
        aria-hidden
        style={{
          position: "absolute",
          inset: "-120px",
          backgroundImage: `url("${NOISE_URI}")`,
          backgroundSize: `${size}px ${size}px`,
          opacity,
          pointerEvents: "none",
          mixBlendMode: blend as CSSProperties["mixBlendMode"],
        }}
      />
    </div>
  );
}
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0