From f5090b9c109d497b6e3cde01bcaac4cf8db2394f Mon Sep 17 00:00:00 2001 From: Ryan Walters Date: Thu, 6 Nov 2025 23:40:43 -0600 Subject: [PATCH] refactor: simplify export to PNG-only, improve controls UI - Replace html2canvas with modern-screenshot for better performance - Remove ExportModal and CSS export functionality - Simplify export to single PNG download - Improve controls layout with better spacing and text shadows - Increase glass-panel opacity for better readability - Remove unused ExportData interface and CSS generation utilities --- README.md | 39 +++++----- package.json | 2 +- pnpm-lock.yaml | 47 ++--------- src/components/ExportModal.tsx | 116 ---------------------------- src/components/GradientControls.tsx | 103 ++++++++++++------------ src/components/PaletteSelector.tsx | 11 ++- src/components/SliderControl.tsx | 12 +-- src/index.css | 2 +- src/index.tsx | 21 +---- src/utils/exportGradient.ts | 84 +++----------------- 10 files changed, 110 insertions(+), 327 deletions(-) delete mode 100644 src/components/ExportModal.tsx diff --git a/README.md b/README.md index 3e009ad..6a4d4dd 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,32 @@ -![Grain Project - Banner Image][grain-banner] +[![Grain Project - Banner Image][grain-banner]][grain-website] -A small experiment on creating beautiful, dynamic backgrounds with -colorful gradients & film grain. Built in React & Vite with SVGs and layers of Radial Gradients +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE) +[![Node Version](https://img.shields.io/badge/node-v22-brightgreen.svg)](https://nodejs.org/) +[![pnpm](https://img.shields.io/badge/maintained%20with-pnpm-cc00ff.svg)](https://pnpm.io/) -### Dependencies Used +Create beautiful, dynamic backgrounds with colorful gradients and film grain. Built with modern web technologies for smooth performance and stunning visuals. -- Hero Icons -- React -- Typescript -- Vite -- Sass +- **Customizable gradients & noise** via SVG filters +- **Preset palettes** for quick styling +- **PNG export** with a single click -### Installation +Built with [Preact](https://preactjs.com/), [Vite](https://vite.dev/), [Tailwind CSS](https://tailwindcss.com/), [Radix UI](https://www.radix-ui.com/), and [Lucide Icons](https://lucide.dev/). -- Built on Node v16, packages managed with Yarn. +## Usage ```bash -npm install --global yarn # If you don't have yarn installed -yarn # Run inside root directory to install all dependencies. +git clone https://github.com/Xevion/grain.git # Clone the repository +cd grain # Navigate to the repository +npm install --global pnpm # Install pnpm if needed +pnpm install # Install dependencies +pnpm dev # Start development server +pnpm build # Build for production +pnpm preview # Preview the production build ``` -### Development +## License -```bash -yarn dev # Starts a development server with Hot Module Replacement -``` +Licensed under the [GNU General Public License v3.0](LICENSE). -[grain-banner]: ./.media/banner.jpeg \ No newline at end of file +[grain-banner]: ./.media/banner.jpeg +[grain-website]: https://grain.xevion.dev/ diff --git a/package.json b/package.json index 9eeb379..ba89033 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,8 @@ "@tailwindcss/vite": "^4.1.17", "cssnano": "^7.1.0", "framer-motion": "^12.23.24", - "html2canvas": "^1.4.1", "lucide-preact": "^0.468.0", + "modern-screenshot": "^4.6.6", "preact": "^10.27.2", "preact-iso": "^2.11.0", "preact-render-to-string": "^6.6.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b91911..187ef60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,12 +41,12 @@ importers: framer-motion: specifier: ^12.23.24 version: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - html2canvas: - specifier: ^1.4.1 - version: 1.4.1 lucide-preact: specifier: ^0.468.0 version: 0.468.0(preact@10.27.2) + modern-screenshot: + specifier: ^4.6.6 + version: 4.6.6 preact: specifier: ^10.27.2 version: 10.27.2 @@ -1297,10 +1297,6 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - base64-arraybuffer@1.0.2: - resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} - engines: {node: '>= 0.6.0'} - boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -1384,9 +1380,6 @@ packages: peerDependencies: postcss: ^8.0.9 - css-line-break@2.1.0: - resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} - css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} @@ -1831,10 +1824,6 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true - html2canvas@1.4.1: - resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} - engines: {node: '>=8.0.0'} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2159,6 +2148,9 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + modern-screenshot@4.6.6: + resolution: {integrity: sha512-8tF0xEpe7yx37mK95UcIghSCWYeu628K2hLJl+ZNY2ANmRzYLlRLpquPHAQcL8keF6BoeEzTEw4GrgmUpGuZ8w==} + motion-dom@12.23.23: resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==} @@ -2734,9 +2726,6 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - text-segmentation@1.0.3: - resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2829,9 +2818,6 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - utrie@1.0.2: - resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} - vite-prerender-plugin@0.5.11: resolution: {integrity: sha512-xWOhb8Ef2zoJIiinYVunIf3omRfUbEXcPEvrkQcrDpJ2yjDokxhvQ26eSJbkthRhymntWx6816jpATrJphh+ug==} peerDependencies: @@ -4004,8 +3990,6 @@ snapshots: balanced-match@1.0.2: {} - base64-arraybuffer@1.0.2: {} - boolbase@1.0.0: {} brace-expansion@1.1.12: @@ -4095,10 +4079,6 @@ snapshots: dependencies: postcss: 8.5.6 - css-line-break@2.1.0: - dependencies: - utrie: 1.0.2 - css-select@5.2.2: dependencies: boolbase: 1.0.0 @@ -4711,11 +4691,6 @@ snapshots: he@1.2.0: {} - html2canvas@1.4.1: - dependencies: - css-line-break: 2.1.0 - text-segmentation: 1.0.3 - ignore@5.3.2: {} immutable@5.1.4: @@ -5007,6 +4982,8 @@ snapshots: dependencies: brace-expansion: 1.1.12 + modern-screenshot@4.6.6: {} + motion-dom@12.23.23: dependencies: motion-utils: 12.23.6 @@ -5626,10 +5603,6 @@ snapshots: tapable@2.3.0: {} - text-segmentation@1.0.3: - dependencies: - utrie: 1.0.2 - tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -5723,10 +5696,6 @@ snapshots: util-deprecate@1.0.2: {} - utrie@1.0.2: - dependencies: - base64-arraybuffer: 1.0.2 - vite-prerender-plugin@0.5.11(vite@7.2.1(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(yaml@2.8.1)): dependencies: kolorist: 1.8.0 diff --git a/src/components/ExportModal.tsx b/src/components/ExportModal.tsx deleted file mode 100644 index 21796b8..0000000 --- a/src/components/ExportModal.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useState } from "preact/hooks"; -import * as Dialog from "@radix-ui/react-dialog"; -import { X, Download, Copy, Check } from "lucide-preact"; -import { GlassButton } from "./GlassButton"; -import { exportAsPNG, generateCSS, copyCSSToClipboard, downloadCSS, type ExportData } from "../utils/exportGradient"; - -interface ExportModalProps { - open: boolean; - onClose: () => void; - exportData: ExportData; -} - -export function ExportModal({ open, onClose, exportData }: ExportModalProps) { - const [copied, setCopied] = useState(false); - const [exporting, setExporting] = useState(false); - - const cssCode = generateCSS(exportData); - - const handleCopyCSS = async () => { - try { - await copyCSSToClipboard(cssCode); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (error) { - console.error("Failed to copy:", error); - } - }; - - const handleDownloadCSS = () => { - downloadCSS(cssCode); - }; - - const handleExportPNG = async () => { - setExporting(true); - try { - await exportAsPNG("gradient-container"); - } catch (error) { - console.error("Failed to export PNG:", error); - } finally { - setExporting(false); - } - }; - - return ( - !isOpen && onClose()}> - - - -
- {/* Header */} -
- - Export Gradient - - - - -
- - {/* PNG Export Section */} -
-

- Export as Image -

- - - {exporting ? "Exporting..." : "Download PNG"} - -
- - {/* CSS Export Section */} -
-

- Export as CSS -

- - {/* CSS Code Preview */} -
-
-                  {cssCode}
-                
-
- - {/* CSS Actions */} -
- - {copied ? : } - {copied ? "Copied!" : "Copy CSS"} - - - - Download CSS - -
-
-
-
-
-
- ); -} diff --git a/src/components/GradientControls.tsx b/src/components/GradientControls.tsx index 43f9be3..5700a36 100644 --- a/src/components/GradientControls.tsx +++ b/src/components/GradientControls.tsx @@ -28,69 +28,70 @@ export function GradientControls({ return (
{/* Header */} -
-

+
+

Grain

- + {/* Controls */} -
- {/* Palette Selector */} - + {/* Palette Selector */} + - + - {/* Gradient Count Slider */} - + {/* Gradient Count Slider */} + - {/* Noise Intensity Slider */} - `${Math.round(v * 100)}%`} - /> + {/* Noise Intensity Slider */} + `${Math.round(v * 100)}%`} + className="px-6 py-4" + /> - + - {/* Action Buttons */} -
- - - Regenerate - + {/* Action Buttons */} +
+ + + Regenerate + - - - Export - -
+ + + Export +
); diff --git a/src/components/PaletteSelector.tsx b/src/components/PaletteSelector.tsx index ae69b98..b8a65b9 100644 --- a/src/components/PaletteSelector.tsx +++ b/src/components/PaletteSelector.tsx @@ -4,12 +4,17 @@ import { motion } from "framer-motion"; interface PaletteSelectorProps { selectedPaletteId: string; onSelect: (paletteId: string) => void; + className?: string; } -export function PaletteSelector({ selectedPaletteId, onSelect }: PaletteSelectorProps) { +export function PaletteSelector({ + selectedPaletteId, + onSelect, + className, +}: PaletteSelectorProps) { return ( -
-