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)
+[](https://nodejs.org/)
+[](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 (
-
- Export as Image
-
-
- Export as CSS
-
-
- {/* CSS Code Preview */}
-
- {cssCode}
-
-