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