A web app to help you design things, local, offline, on device. In your browser.

TypeScript 53.1% JSON 45.6% CSS 0.9% Markdown 0.3% JavaScript 0.1%
EffectsPanel.tsx 348 lines (12 KB)
'use client';

import React, { useState } from 'react';
import { Sparkles, Layers, Sun, Droplets } from 'lucide-react';
import { useEditorStore } from '@/lib/store/editor-store';
import {
  GLASS_PRESETS,
  MESH_GRADIENT_PRESETS,
  defaultLiquidGlassConfig,
  type GlassPreset,
  type MeshGradientPreset,
  type LiquidGlassConfig,
} from '@/lib/effects/liquid-glass';
import type { EffectLayerData, GlassEffectConfig } from '@/types';

const effectCategories = [
  { id: 'glass', label: 'Glass', icon: Layers },
  { id: 'backgrounds', label: 'Backgrounds', icon: Droplets },
  { id: 'spotlight', label: 'Spotlight', icon: Sun },
] as const;

export function EffectsPanel() {
  const [activeCategory, setActiveCategory] = useState<(typeof effectCategories)[number]['id']>(
    'glass'
  );
  const [glassConfig, setGlassConfig] = useState<LiquidGlassConfig>(defaultLiquidGlassConfig);

  const { addLayer, canvas, setBackgroundColor } = useEditorStore();

  const handleAddGlassEffect = (preset?: GlassPreset) => {
    const config = preset ? GLASS_PRESETS[preset] : glassConfig;

    const effectConfig: GlassEffectConfig = {
      blur: config.blur,
      opacity: config.opacity,
      saturation: config.saturation,
      borderRadius: config.borderRadius,
      borderColor: config.borderColor,
      borderWidth: config.borderWidth,
    };

    const data: EffectLayerData = {
      type: 'effect',
      effectType: 'glass',
      config: effectConfig,
    };

    const size = Math.min(canvas.width, canvas.height) * 0.4;

    addLayer({
      type: 'effect',
      name: preset ? `Glass (${preset})` : 'Glass Panel',
      visible: true,
      locked: false,
      opacity: 1,
      position: {
        x: (canvas.width - size) / 2,
        y: (canvas.height - size) / 2,
      },
      size: { width: size, height: size * 0.6 },
      rotation: 0,
      data,
    });
  };

  const handleApplyMeshGradient = (preset: MeshGradientPreset) => {
    const meshConfig = MESH_GRADIENT_PRESETS[preset];

    // Apply as canvas background using a gradient approximation
    setBackgroundColor({
      type: 'radial',
      stops: meshConfig.colors.map((color, i) => ({
        offset: i / (meshConfig.colors.length - 1),
        color,
      })),
    });
  };

  const handleAddSpotlight = () => {
    const data: EffectLayerData = {
      type: 'effect',
      effectType: 'blur',
      config: {
        blur: 20,
        shape: 'circle',
      },
    };

    const size = Math.min(canvas.width, canvas.height) * 0.5;

    addLayer({
      type: 'effect',
      name: 'Spotlight',
      visible: true,
      locked: false,
      opacity: 0.8,
      position: {
        x: (canvas.width - size) / 2,
        y: (canvas.height - size) / 2,
      },
      size: { width: size, height: size },
      rotation: 0,
      data,
    });
  };

  return (
    <div className="p-4">
      <h2 className="text-lg font-semibold text-white mb-4">Effects</h2>

      {/* Category tabs */}
      <div className="flex gap-1 mb-6 overflow-x-auto pb-1">
        {effectCategories.map((cat) => {
          const Icon = cat.icon;
          return (
            <button
              key={cat.id}
              onClick={() => setActiveCategory(cat.id)}
              className={`
                flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm whitespace-nowrap
                transition-colors
                ${
                  activeCategory === cat.id
                    ? 'bg-indigo-500/20 text-indigo-400'
                    : 'text-neutral-400 hover:text-white hover:bg-neutral-800'
                }
              `}
            >
              <Icon className="w-4 h-4" />
              {cat.label}
            </button>
          );
        })}
      </div>

      {activeCategory === 'glass' && (
        <div className="space-y-6">
          {/* Glass presets */}
          <div>
            <label className="text-sm text-neutral-400 block mb-3">Liquid Glass Presets</label>
            <div className="grid grid-cols-2 gap-3">
              {(Object.keys(GLASS_PRESETS) as GlassPreset[]).map((preset) => (
                <button
                  key={preset}
                  onClick={() => handleAddGlassEffect(preset)}
                  className="
                    p-4 rounded-xl
                    bg-gradient-to-br from-white/10 to-white/5
                    backdrop-blur-sm
                    border border-white/10 hover:border-indigo-500/50
                    transition-all group
                  "
                >
                  {/* Glass preview */}
                  <div
                    className="h-12 rounded-lg mb-2"
                    style={{
                      background: `rgba(255, 255, 255, ${GLASS_PRESETS[preset].opacity})`,
                      backdropFilter: `blur(${GLASS_PRESETS[preset].blur / 2}px)`,
                      border: `1px solid rgba(255, 255, 255, 0.1)`,
                    }}
                  />
                  <div className="text-xs text-neutral-300 capitalize group-hover:text-indigo-400 transition-colors">
                    {preset}
                  </div>
                </button>
              ))}
            </div>
          </div>

          {/* Custom glass controls */}
          <div>
            <label className="text-sm text-neutral-400 block mb-3">Customize</label>
            <div className="space-y-3">
              <div>
                <div className="flex justify-between text-xs text-neutral-500 mb-1">
                  <span>Blur</span>
                  <span>{glassConfig.blur}px</span>
                </div>
                <input
                  type="range"
                  min={0}
                  max={50}
                  value={glassConfig.blur}
                  onChange={(e) =>
                    setGlassConfig({ ...glassConfig, blur: Number(e.target.value) })
                  }
                  className="w-full"
                />
              </div>

              <div>
                <div className="flex justify-between text-xs text-neutral-500 mb-1">
                  <span>Opacity</span>
                  <span>{Math.round(glassConfig.opacity * 100)}%</span>
                </div>
                <input
                  type="range"
                  min={0}
                  max={100}
                  value={glassConfig.opacity * 100}
                  onChange={(e) =>
                    setGlassConfig({ ...glassConfig, opacity: Number(e.target.value) / 100 })
                  }
                  className="w-full"
                />
              </div>

              <div>
                <div className="flex justify-between text-xs text-neutral-500 mb-1">
                  <span>Saturation</span>
                  <span>{Math.round(glassConfig.saturation * 100)}%</span>
                </div>
                <input
                  type="range"
                  min={100}
                  max={200}
                  value={glassConfig.saturation * 100}
                  onChange={(e) =>
                    setGlassConfig({ ...glassConfig, saturation: Number(e.target.value) / 100 })
                  }
                  className="w-full"
                />
              </div>

              <div>
                <div className="flex justify-between text-xs text-neutral-500 mb-1">
                  <span>Border Radius</span>
                  <span>{glassConfig.borderRadius}px</span>
                </div>
                <input
                  type="range"
                  min={0}
                  max={50}
                  value={glassConfig.borderRadius}
                  onChange={(e) =>
                    setGlassConfig({ ...glassConfig, borderRadius: Number(e.target.value) })
                  }
                  className="w-full"
                />
              </div>

              <button
                onClick={() => handleAddGlassEffect()}
                className="
                  w-full py-2 rounded-lg
                  bg-indigo-500 hover:bg-indigo-600
                  text-white text-sm font-medium
                  transition-colors flex items-center justify-center gap-2
                "
              >
                <Sparkles className="w-4 h-4" />
                Add Glass Panel
              </button>
            </div>
          </div>
        </div>
      )}

      {activeCategory === 'backgrounds' && (
        <div className="space-y-4">
          <label className="text-sm text-neutral-400 block">Mesh Gradient Backgrounds</label>
          <div className="grid grid-cols-2 gap-3">
            {(Object.keys(MESH_GRADIENT_PRESETS) as MeshGradientPreset[]).map((preset) => {
              const config = MESH_GRADIENT_PRESETS[preset];
              const gradientCSS = `radial-gradient(at 20% 80%, ${config.colors[0]} 0px, transparent 50%),
                radial-gradient(at 80% 20%, ${config.colors[1]} 0px, transparent 50%),
                radial-gradient(at 60% 60%, ${config.colors[2]} 0px, transparent 50%),
                ${config.colors[3]}`;

              return (
                <button
                  key={preset}
                  onClick={() => handleApplyMeshGradient(preset)}
                  className="
                    aspect-video rounded-xl overflow-hidden
                    border border-neutral-700/50 hover:border-indigo-500/50
                    transition-all hover:scale-[1.02]
                  "
                >
                  <div
                    className="w-full h-full"
                    style={{ background: gradientCSS }}
                  />
                </button>
              );
            })}
          </div>

          <p className="text-xs text-neutral-500">
            Click a mesh gradient to apply it as your canvas background.
          </p>
        </div>
      )}

      {activeCategory === 'spotlight' && (
        <div className="space-y-4">
          <p className="text-sm text-neutral-400">
            Add spotlight effects to highlight specific features in your screenshots.
          </p>

          <div className="grid grid-cols-2 gap-3">
            <button
              onClick={handleAddSpotlight}
              className="
                p-4 rounded-xl
                bg-neutral-800/50 hover:bg-neutral-800
                border border-neutral-700/50 hover:border-indigo-500/50
                transition-all group
              "
            >
              <div className="h-16 rounded-lg bg-gradient-to-br from-black/80 to-black/40 mb-2 flex items-center justify-center">
                <div className="w-8 h-8 rounded-full bg-white/20 blur-sm" />
              </div>
              <div className="text-xs text-neutral-300 group-hover:text-indigo-400">
                Circle Spotlight
              </div>
            </button>

            <button
              onClick={handleAddSpotlight}
              className="
                p-4 rounded-xl
                bg-neutral-800/50 hover:bg-neutral-800
                border border-neutral-700/50 hover:border-indigo-500/50
                transition-all group
              "
            >
              <div className="h-16 rounded-lg bg-gradient-to-br from-black/80 to-black/40 mb-2 flex items-center justify-center">
                <div className="w-12 h-8 rounded bg-white/20 blur-sm" />
              </div>
              <div className="text-xs text-neutral-300 group-hover:text-indigo-400">
                Rectangle Focus
              </div>
            </button>
          </div>

          <div className="p-3 rounded-lg bg-indigo-500/10 border border-indigo-500/20">
            <p className="text-xs text-indigo-300">
              Tip: Position the spotlight over a feature, then adjust opacity to create focus.
            </p>
          </div>
        </div>
      )}
    </div>
  );
}

About

A web app to help you design things, local, offline, on device. In your browser.

0 stars
0 forks