diff --git a/apps/example-nextjs-dev/app/stateless-test/page.tsx b/apps/example-nextjs-dev/app/stateless-test/page.tsx
new file mode 100644
index 00000000..f9f603e8
--- /dev/null
+++ b/apps/example-nextjs-dev/app/stateless-test/page.tsx
@@ -0,0 +1,29 @@
+'use client'
+
+import { ShaderGradientCanvas } from '@shadergradient/react'
+import {
+ useQueryState,
+ ShaderGradientStateless,
+} from '@shadergradient/react/stateless'
+
+export default function Page() {
+ const [, setColor1] = useQueryState('color1')
+
+ return (
+
+
+
+ {/* When control is 'query', the gradient props follows url query params */}
+
+
+
+
+
+
+ )
+}
diff --git a/packages/shadergradient-v2/package.json b/packages/shadergradient-v2/package.json
index 8f60ab3c..1cb3cabc 100644
--- a/packages/shadergradient-v2/package.json
+++ b/packages/shadergradient-v2/package.json
@@ -10,6 +10,15 @@
"files": [
"dist/**"
],
+ "exports": {
+ ".": {
+ "import": "./dist/index.mjs",
+ "types": "./dist/index.d.mts"
+ },
+ "./stateless": {
+ "import": "./dist/ShaderGradientStateless/index.mjs"
+ }
+ },
"scripts": {
"build": "tsup",
"dev": "tsup --config tsup.config.ts --watch",
diff --git a/packages/shadergradient-v2/src/ShaderGradientStateless/ShaderGradientStateless.tsx b/packages/shadergradient-v2/src/ShaderGradientStateless/ShaderGradientStateless.tsx
new file mode 100644
index 00000000..90e95488
--- /dev/null
+++ b/packages/shadergradient-v2/src/ShaderGradientStateless/ShaderGradientStateless.tsx
@@ -0,0 +1,11 @@
+import { GradientT } from '@/types'
+import { ShaderGradient } from '../ShaderGradient/ShaderGradient'
+import { useSearchParamToStore } from './store/useSearchParamToStore'
+import { useControlValues } from './useControlValues'
+
+export function ShaderGradientStateless(passedProps: GradientT): JSX.Element {
+ useSearchParamToStore() // init gradient state with url query
+ const props = useControlValues(passedProps.control, passedProps) // make props using url query, control and passed props
+
+ return
+}
diff --git a/packages/shadergradient-v2/src/ShaderGradientStateless/index.ts b/packages/shadergradient-v2/src/ShaderGradientStateless/index.ts
new file mode 100644
index 00000000..9c3f4063
--- /dev/null
+++ b/packages/shadergradient-v2/src/ShaderGradientStateless/index.ts
@@ -0,0 +1,2 @@
+export * from './store/useQueryState'
+export * from './ShaderGradientStateless'
diff --git a/packages/shadergradient-v2/src/ShaderGradientStateless/store/index.ts b/packages/shadergradient-v2/src/ShaderGradientStateless/store/index.ts
new file mode 100644
index 00000000..9017c218
--- /dev/null
+++ b/packages/shadergradient-v2/src/ShaderGradientStateless/store/index.ts
@@ -0,0 +1,5 @@
+export * from './store'
+export * from './useQueryState'
+
+export * from './presetURLs'
+export * from './usePresetToStore'
diff --git a/packages/shadergradient-v2/src/ShaderGradientStateless/store/presetURLs.ts b/packages/shadergradient-v2/src/ShaderGradientStateless/store/presetURLs.ts
new file mode 100644
index 00000000..eaf3426e
--- /dev/null
+++ b/packages/shadergradient-v2/src/ShaderGradientStateless/store/presetURLs.ts
@@ -0,0 +1,30 @@
+import { presets } from '@/presets'
+
+export const initialActivePreset = 0
+
+export const PRESETS = convertPresets(presets)
+
+// export const DEFAUlT_PRESET = '?pixelDensity=1&fov=45'
+export const DEFAUlT_PRESET = PRESETS[0].url
+
+function convertPresets(presets: Record) {
+ const PRESETS = Object.entries(presets).map(([key, value]) => {
+ const { title, color, props } = value
+
+ const urlParams = new URLSearchParams(
+ Object.entries(props).reduce((acc, [propKey, propValue]) => {
+ acc[propKey] =
+ typeof propValue === 'boolean' ? String(propValue) : String(propValue)
+ return acc
+ }, {} as Record)
+ ).toString()
+
+ return {
+ title,
+ color,
+ url: `?${urlParams}`,
+ }
+ })
+
+ return PRESETS
+}
diff --git a/packages/shadergradient-v2/src/ShaderGradientStateless/store/store.ts b/packages/shadergradient-v2/src/ShaderGradientStateless/store/store.ts
new file mode 100644
index 00000000..bbeb13ef
--- /dev/null
+++ b/packages/shadergradient-v2/src/ShaderGradientStateless/store/store.ts
@@ -0,0 +1,77 @@
+import * as qs from 'query-string'
+import { create } from 'zustand'
+import { combine } from 'zustand/middleware'
+import { DEFAUlT_PRESET, initialActivePreset } from './presetURLs'
+
+// without embedMode
+// it renders without the dom & other gradient controls at first, and add it after the first updateGradientState() excuted.
+
+const defaultState = { ...parseState() }
+export const useQueryStore = create((set) => defaultState)
+
+export const updateGradientState = (querystate: object | string) => {
+ const isString = typeof querystate === 'string'
+
+ let state = querystate
+ if (isString) state = parseState(querystate)
+
+ useQueryStore.setState(state, isString) // replace true if it's a string
+}
+
+// defaultGradient could be replaced by window.location.search
+function parseState(search = DEFAUlT_PRESET) {
+ return qs.parse(search, {
+ parseNumbers: true,
+ parseBooleans: true,
+ arrayFormat: 'index',
+ })
+}
+
+export const useDomStore = create(() => {
+ return { dom: null }
+})
+
+// store for UI updates
+export const useCursorStore = create((set) => ({
+ hoverState: 0,
+ hover: 'default',
+ updateHoverState: (payload) => set({ hoverState: payload }),
+}))
+export const useUIStore = create(
+ combine(
+ { activePreset: initialActivePreset, mode: 'full', loadingPercentage: 0 },
+ (set) => ({
+ setActivePreset: (by: number) => set((state) => ({ activePreset: by })),
+ setMode: (data: any) => set((state) => ({ ...state, mode: data })),
+ setLoadingPercentage: (data: any) =>
+ set((state) => ({ ...state, loadingPercentage: data })),
+ })
+ )
+)
+
+// store for Figma Plugin
+const useFigmaStore = create((set) => ({
+ figma: { selection: 0, user: null },
+ setFigma: (payload) =>
+ set((prev) => ({ figma: { ...prev.figma, ...payload } })),
+}))
+export function useFigma() {
+ const figma = useFigmaStore((state: any) => state.figma)
+ const setFigma = useFigmaStore((state: any) => state.setFigma)
+ return [figma, setFigma]
+}
+
+export const useBillingIntervalStore = create((set) => ({
+ billingInterval: 'year',
+ setBillingInterval: (payload) =>
+ set((state) => ({ billingInterval: payload })),
+}))
+export function useBillingInterval() {
+ const billingInterval = useBillingIntervalStore(
+ (state: any) => state.billingInterval
+ )
+ const setBillingInterval = useBillingIntervalStore(
+ (state: any) => state.setBillingInterval
+ )
+ return [billingInterval, setBillingInterval]
+}
diff --git a/packages/shadergradient-v2/src/ShaderGradientStateless/store/usePresetToStore.ts b/packages/shadergradient-v2/src/ShaderGradientStateless/store/usePresetToStore.ts
new file mode 100644
index 00000000..effd7c50
--- /dev/null
+++ b/packages/shadergradient-v2/src/ShaderGradientStateless/store/usePresetToStore.ts
@@ -0,0 +1,25 @@
+import { useEffect } from 'react'
+import { PRESETS } from './presetURLs'
+import { useUIStore, updateGradientState } from './store'
+
+let pageLoaded = false
+export function usePresetToStore() {
+ // ----------------------------- Preset to Custom Material ---------------------------------
+ const activePreset = useUIStore((state: any) => state.activePreset)
+ useEffect(() => {
+ let gradientURL
+
+ // CASE 1. use search params at the first load.
+ if (
+ !pageLoaded &&
+ window.location.search?.includes('pixelDensity') // checking just window.location.search existing is not valid for the Framer Preview search (?target=preview-web)
+ )
+ gradientURL = window.location.search
+ // CASE 2. When activePreset changes by UI buttons
+ else gradientURL = PRESETS[activePreset].url
+
+ updateGradientState(gradientURL)
+
+ pageLoaded = true
+ }, [activePreset])
+}
diff --git a/packages/shadergradient-v2/src/ShaderGradientStateless/store/useQueryState.ts b/packages/shadergradient-v2/src/ShaderGradientStateless/store/useQueryState.ts
new file mode 100644
index 00000000..7f84dec6
--- /dev/null
+++ b/packages/shadergradient-v2/src/ShaderGradientStateless/store/useQueryState.ts
@@ -0,0 +1,87 @@
+import { useCallback } from 'react'
+import * as qs from 'query-string'
+import { useQueryStore } from './store'
+
+export const useQueryState = (propName: any, defaultValue: any = null) => {
+ const selector = useCallback(
+ (state) =>
+ typeof state[propName] !== 'undefined' ? state[propName] : defaultValue,
+ [propName, defaultValue]
+ )
+ const globalValue = useQueryStore(selector)
+ const _setGlobalValue = useCallback(
+ (valueFun) =>
+ useQueryStore.setState({
+ [propName]: valueFun(useQueryStore.getState()[propName]),
+ }),
+ [propName]
+ )
+
+ const setQueryValue = useCallback(
+ (newVal) => {
+ _setGlobalValue((currentState: any) => {
+ if (typeof newVal === 'function') {
+ newVal = newVal(currentState || defaultValue)
+ }
+ if (Number.isFinite(newVal)) {
+ newVal = parseFloat(newVal.toFixed(2))
+ }
+
+ // defer update of URL
+ setTimeout(() => {
+ const query = useQueryStore.getState()
+ updateHistory(
+ qs.stringifyUrl(
+ // @ts-ignore
+ { url: window.location.pathname, query },
+ { skipNull: true, arrayFormat: 'index' }
+ )
+ )
+ }, 0)
+
+ return newVal
+ })
+ },
+ [_setGlobalValue]
+ )
+
+ return [globalValue, setQueryValue]
+}
+
+export const useURLQueryState = () => {
+ // it's weird, but need to wrap below func as a hook
+ const setQueryValue = (search) => {
+ // defer update of URL
+ setTimeout(() => {
+ const query: any = useQueryStore.getState()
+ updateHistory(
+ qs.stringifyUrl(
+ { url: window.location.pathname, query },
+ { skipNull: true, arrayFormat: 'index' }
+ )
+ )
+ }, 0)
+
+ const state = qs.parse(search, {
+ parseNumbers: true,
+ parseBooleans: true,
+ arrayFormat: 'index',
+ })
+
+ useQueryStore.setState(state)
+ }
+ return setQueryValue
+}
+
+function updateHistory(path: string) {
+ window.history.pushState(
+ {
+ prevUrls: [
+ ...(window.history.state?.prevUrls || []),
+ window.location.origin + path,
+ ],
+ },
+ document.title,
+ path
+ )
+}
diff --git a/packages/shadergradient-v2/src/ShaderGradientStateless/store/useSearchParamToStore.ts b/packages/shadergradient-v2/src/ShaderGradientStateless/store/useSearchParamToStore.ts
new file mode 100644
index 00000000..856d0064
--- /dev/null
+++ b/packages/shadergradient-v2/src/ShaderGradientStateless/store/useSearchParamToStore.ts
@@ -0,0 +1,11 @@
+import { useEffect } from 'react'
+import { updateGradientState } from './store'
+
+export function useSearchParamToStore() {
+ useEffect(() => {
+ // if (
+ // window.location.search?.includes('pixelDensity') // checking just window.location.search existing is not valid for the Framer Preview search (?target=preview-web)
+ // )
+ updateGradientState(window.location.search)
+ }, [])
+}
diff --git a/packages/shadergradient-v2/src/ShaderGradientStateless/useControlValues.ts b/packages/shadergradient-v2/src/ShaderGradientStateless/useControlValues.ts
new file mode 100644
index 00000000..6ba65a80
--- /dev/null
+++ b/packages/shadergradient-v2/src/ShaderGradientStateless/useControlValues.ts
@@ -0,0 +1,127 @@
+import { useCursorStore } from './store'
+import { GradientT } from '@/types'
+import { useQueryState } from './store/useQueryState'
+import { formatUrlString } from '@/utils'
+import * as qs from 'query-string'
+
+export function useControlValues(
+ control,
+ { urlString, ...props }: any
+): GradientT {
+ // shape
+ const [type] = useQueryState('type')
+ const [animate] = useQueryState('animate')
+ const [range] = useQueryState('range')
+ const [rangeStart] = useQueryState('rangeStart')
+ const [rangeEnd] = useQueryState('rangeEnd')
+ const [uTime] = useQueryState('uTime')
+ const [uSpeed] = useQueryState('uSpeed')
+ const [uStrength] = useQueryState('uStrength')
+ const [uDensity] = useQueryState('uDensity')
+ const [uFrequency] = useQueryState('uFrequency')
+ const [uAmplitude] = useQueryState('uAmplitude')
+ const [positionX] = useQueryState('positionX')
+ const [positionY] = useQueryState('positionY')
+ const [positionZ] = useQueryState('positionZ')
+ const [rotationX] = useQueryState('rotationX')
+ const [rotationY] = useQueryState('rotationY')
+ const [rotationZ] = useQueryState('rotationZ')
+
+ // colors
+ const [color1] = useQueryState('color1')
+ const [color2] = useQueryState('color2')
+ const [color3] = useQueryState('color3')
+ // const hoverStateColor = getHoverColor(hoverState, [color1, color2, color3])
+
+ // camera
+ const [cAzimuthAngle] = useQueryState('cAzimuthAngle')
+ const [cPolarAngle] = useQueryState('cPolarAngle')
+ const [cDistance] = useQueryState('cDistance')
+ const [cameraZoom] = useQueryState('cameraZoom')
+
+ const [wireframe] = useQueryState('wireframe')
+
+ // shader
+ const [shader] = useQueryState('shader')
+
+ // effects
+ const [lightType] = useQueryState('lightType')
+ const [brightness] = useQueryState('brightness')
+ const [envPreset] = useQueryState('envPreset')
+ const [grain] = useQueryState('grain')
+ const [reflection] = useQueryState('reflection')
+
+ // tools
+ const [zoomOut] = useQueryState('zoomOut')
+ const [toggleAxis] = useQueryState('toggleAxis')
+ const hoverState = useCursorStore((state: any) => state.hoverState)
+
+ // figma
+ const [frameRate] = useQueryState('frameRate')
+ const [destination] = useQueryState('destination')
+ const [format] = useQueryState('format')
+ const queryProps = {
+ type,
+ animate,
+ range,
+ rangeStart,
+ rangeEnd,
+ frameRate,
+ destination,
+ format,
+ uTime,
+ uSpeed,
+ uStrength,
+ uDensity,
+ uFrequency,
+ uAmplitude,
+ positionX,
+ positionY,
+ positionZ,
+ rotationX,
+ rotationY,
+ rotationZ,
+ color1,
+ color2,
+ color3,
+ cAzimuthAngle,
+ cPolarAngle,
+ cDistance,
+ cameraZoom,
+ wireframe,
+ shader,
+ lightType,
+ brightness,
+ envPreset,
+ grain,
+ reflection,
+ zoomOut,
+ toggleAxis,
+
+ hoverState, // include hoverState to flush the shader when it is hovered
+ }
+
+ if (control === 'props') return clean({ ...queryProps, ...props })
+ else if (control === 'query')
+ return clean(
+ urlString
+ ? qs.parse(formatUrlString(urlString), {
+ parseNumbers: true,
+ parseBooleans: true,
+ arrayFormat: 'index',
+ })
+ : queryProps
+ )
+}
+
+function clean(obj) {
+ const cleanedObject = {}
+
+ for (const key in obj) {
+ if (obj[key] !== null) {
+ cleanedObject[key] = obj[key]
+ }
+ }
+
+ return cleanedObject
+}
diff --git a/packages/shadergradient-v2/src/presets.ts b/packages/shadergradient-v2/src/presets.ts
index 3e92fd95..8101af6d 100644
--- a/packages/shadergradient-v2/src/presets.ts
+++ b/packages/shadergradient-v2/src/presets.ts
@@ -66,6 +66,141 @@ export const presets = {
wireframe: false,
},
},
+ pensive: {
+ title: 'Pensive',
+ color: 'white',
+ props: {
+ range: 'enabled',
+ rangeStart: 0,
+ rangeEnd: 40,
+ frameRate: 10,
+ destination: 'onCanvas',
+ format: 'gif',
+ animate: 'on',
+ axesHelper: 'off',
+ brightness: 1.5,
+ cAzimuthAngle: 250,
+ cDistance: 1.5,
+ cPolarAngle: 140,
+ cameraZoom: 12.5,
+ color1: '#809bd6',
+ color2: '#910aff',
+ color3: '#af38ff',
+ embedMode: 'off',
+ envPreset: 'city',
+ gizmoHelper: 'hide',
+ grain: 'on',
+ lightType: '3d',
+ pixelDensity: 1,
+ fov: 45,
+ positionX: 0,
+ positionY: 0,
+ positionZ: 0,
+ reflection: 0.5,
+ rotationX: 0,
+ rotationY: 0,
+ rotationZ: 140,
+ shader: 'defaults',
+ type: 'sphere',
+ uAmplitude: 7,
+ uDensity: 0.8,
+ uFrequency: 5.5,
+ uSpeed: 0.3,
+ uStrength: 0.4,
+ uTime: 0,
+ wireframe: false,
+ },
+ },
+ mint: {
+ title: 'Mint',
+ color: 'white',
+ props: {
+ range: 'enabled',
+ rangeStart: 0,
+ rangeEnd: 40,
+ frameRate: 10,
+ destination: 'onCanvas',
+ format: 'gif',
+ animate: 'on',
+ axesHelper: 'off',
+ brightness: 1.2,
+ cAzimuthAngle: 170,
+ cDistance: 4.4,
+ cPolarAngle: 70,
+ cameraZoom: 1,
+ color1: '#94ffd1',
+ color2: '#6bf5ff',
+ color3: '#ffffff',
+ embedMode: 'off',
+ envPreset: 'city',
+ gizmoHelper: 'hide',
+ grain: 'off',
+ lightType: '3d',
+ pixelDensity: 1,
+ fov: 45,
+ positionX: 0,
+ positionY: 0.9,
+ positionZ: -0.3,
+ reflection: 0.1,
+ rotationX: 45,
+ rotationY: 0,
+ rotationZ: 0,
+ shader: 'defaults',
+ type: 'waterPlane',
+ uAmplitude: 0,
+ uDensity: 1.2,
+ uFrequency: 0,
+ uSpeed: 0.2,
+ uStrength: 3.4,
+ uTime: 0,
+ wireframe: false,
+ },
+ },
+ interstella: {
+ title: 'Interstella',
+ color: 'white',
+ props: {
+ range: 'enabled',
+ rangeStart: 0,
+ rangeEnd: 40,
+ frameRate: 10,
+ destination: 'onCanvas',
+ format: 'gif',
+ animate: 'on',
+ axesHelper: 'off',
+ brightness: 0.8,
+ cAzimuthAngle: 270,
+ cDistance: 0.5,
+ cPolarAngle: 180,
+ cameraZoom: 15.1,
+ color1: '#73bfc4',
+ color2: '#ff810a',
+ color3: '#8da0ce',
+ embedMode: 'off',
+ envPreset: 'city',
+ gizmoHelper: 'hide',
+ grain: 'on',
+ lightType: 'env',
+ pixelDensity: 1,
+ fov: 45,
+ positionX: -0.1,
+ positionY: 0,
+ positionZ: 0,
+ reflection: 0.4,
+ rotationX: 0,
+ rotationY: 130,
+ rotationZ: 70,
+ shader: 'defaults',
+ type: 'sphere',
+ uAmplitude: 3.2,
+ uDensity: 0.8,
+ uFrequency: 5.5,
+ uSpeed: 0.3,
+ uStrength: 0.3,
+ uTime: 0,
+ wireframe: false,
+ },
+ },
}
export const initialActivePreset = 0
diff --git a/packages/shadergradient-v2/tsup.config.ts b/packages/shadergradient-v2/tsup.config.ts
index c50c7ad5..9534ef6c 100644
--- a/packages/shadergradient-v2/tsup.config.ts
+++ b/packages/shadergradient-v2/tsup.config.ts
@@ -54,6 +54,9 @@ export default defineConfig(async (options) => {
'@react-three/fiber',
'@react-three/drei',
'three',
+ // zustand need to be external
+ // it is a dependency of @react-three/fiber
+ 'zustand',
],
esbuildPlugins: [glslLoader],
async onSuccess() {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e195a827..5dec6693 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -36,7 +36,7 @@ importers:
specifier: ^8.12.0
version: 8.12.0(react-dom@18.2.0)(react@18.2.0)(three@0.169.0)
'@shadergradient/react':
- specifier: ^2.0.6
+ specifier: ^2.0.7
version: link:../../packages/shadergradient-v2
'@testing-library/jest-dom':
specifier: ^5.16.5
@@ -162,7 +162,7 @@ importers:
specifier: ^8.12.0
version: 8.12.0(react-dom@18.2.0)(react@18.2.0)(three@0.169.0)
'@shadergradient/react':
- specifier: ^2.0.6
+ specifier: ^2.0.7
version: link:../../packages/shadergradient-v2
'@types/three':
specifier: ^0.150.0
@@ -256,7 +256,7 @@ importers:
specifier: ^8.12.0
version: 8.12.0(react-dom@18.2.0)(react@18.2.0)(three@0.169.0)
'@shadergradient/react':
- specifier: ^2.0.6
+ specifier: ^2.0.7
version: link:../../packages/shadergradient-v2
glsl-random:
specifier: ^0.0.5