├── .gitattributes ├── .gitignore ├── README.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── App.jsx ├── App.module.css ├── components │ ├── DeStijlGrid.jsx │ ├── Link.jsx │ ├── LygiaGrid.jsx │ ├── Orthographic.jsx │ ├── RichterGrid.jsx │ └── ShapesGrid.jsx ├── context │ └── index.ts ├── index.css ├── main.jsx └── pages │ ├── DeStijl.jsx │ ├── Lygia.jsx │ ├── Richter.tsx │ ├── RichterFarben.tsx │ └── Shapes.jsx └── vite.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generative Artwork with Three.js 2 | 3 | A collection of generative art pieces inspired by various artists and art movements, created using Three.js and React. The project features interactive controls that allow users to modify parameters in real-time and experiment with different color palettes. This is a demo project for the article [Creating a Generative Artwork with Three.js](https://tympanus.net/codrops/2025/01/09/creating-a-generative-artwork-with-three-js/). 4 | 5 | ## Art Pieces 6 | 7 | - **Lygia**: Inspired by Lygia Clark's geometric abstractions 8 | - **Richter**: Based on Gerhard Richter's stripes artworks 9 | - **Richter Farben**: A variation on Richter's color grid paintings 10 | - **De Stijl**: Homage to the De Stijl movement's geometric style 11 | - **Shapes**: Minimalist circular compositions 12 | 13 | ## Features 14 | 15 | - Real-time parameter controls using Leva 16 | - Custom color palettes for each piece 17 | - Responsive design 18 | - Interactive 3D rendering with Three.js 19 | - Orthographic and perspective camera views 20 | 21 | ## Getting Started 22 | 23 | ### Prerequisites 24 | 25 | - Node.js (version 16 or higher) 26 | - npm or yarn 27 | 28 | ### Installation 29 | 30 | 1. Clone the repository: 31 | 32 | ```bash 33 | git clone https://github.com/codrops/generative-artwork-three.git 34 | cd generative-artwork-three 35 | ``` 36 | 37 | 2. Install dependencies: 38 | 39 | ```bash 40 | npm install 41 | ``` 42 | 43 | 3. Start the development server: 44 | 45 | ```bash 46 | npm run dev 47 | ``` 48 | 49 | 4. Open the project in your browser: 50 | 51 | The application will be available at `http://localhost:5173` 52 | 53 | ## Built With 54 | 55 | - [React](https://reactjs.org/) 56 | - [Three.js](https://threejs.org/) 57 | - [React Three Fiber](https://docs.pmnd.rs/react-three-fiber) 58 | - [Leva](https://github.com/pmndrs/leva) 59 | - [Vite](https://vitejs.dev/) 60 | 61 | ## Author 62 | 63 | Eduard Fossas - [Portfolio](http://eduardfossas.vercel.app/) 64 | 65 | ## License 66 | 67 | This project is licensed under the MIT License 68 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | import react from "eslint-plugin-react"; 4 | import reactHooks from "eslint-plugin-react-hooks"; 5 | import reactRefresh from "eslint-plugin-react-refresh"; 6 | 7 | export default [ 8 | { ignores: ["dist"] }, 9 | { 10 | files: ["**/*.{js,jsx}"], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: "latest", 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: "module", 18 | }, 19 | }, 20 | settings: { react: { version: "18.3" } }, 21 | plugins: { 22 | react, 23 | "react-hooks": reactHooks, 24 | "react-refresh": reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs["jsx-runtime"].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | "react/jsx-no-target-blank": "off", 32 | "react-refresh/only-export-components": [ 33 | "warn", 34 | { allowConstantExport: true }, 35 | ], 36 | "react/no-unknown-property": [ 37 | "error", 38 | { 39 | ignore: [ 40 | "args", 41 | "attach", 42 | "vertexColors", 43 | "toneMapped", 44 | "transparent", 45 | "side", 46 | "intensity", 47 | "position", 48 | ], 49 | }, 50 | ], 51 | }, 52 | }, 53 | ]; 54 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Creating a Generative Artwork with Three.js 8 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codrops-generative-artwork-three", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@react-three/fiber": "^8.17.10", 14 | "clsx": "^2.1.1", 15 | "leva": "^0.9.35", 16 | "open-simplex-noise": "^2.5.0", 17 | "react": "^18.3.1", 18 | "react-dom": "^18.3.1", 19 | "three": "^0.171.0", 20 | "wouter": "^3.3.5" 21 | }, 22 | "devDependencies": { 23 | "@eslint/js": "^9.15.0", 24 | "@types/react": "^18.3.12", 25 | "@types/react-dom": "^18.3.1", 26 | "@types/three": "^0.171.0", 27 | "@vitejs/plugin-react": "^4.3.4", 28 | "eslint": "^9.15.0", 29 | "eslint-plugin-react": "^7.37.2", 30 | "eslint-plugin-react-hooks": "^5.0.0", 31 | "eslint-plugin-react-refresh": "^0.4.14", 32 | "globals": "^15.12.0", 33 | "vite": "^6.0.1" 34 | }, 35 | "overrides": { 36 | "leva": { 37 | "@radix-ui/react-portal": "1.0.2" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduardfossas/codrops-generative-artwork-three/c4148f0de3fa5b629f8b2e7ea7971afd19a5eebc/public/favicon.ico -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from "@react-three/fiber"; 2 | import { Lygia } from "./pages/Lygia"; 3 | import { Richter } from "./pages/Richter"; 4 | import { RichterFarben } from "./pages/RichterFarben"; 5 | import { DeStijl } from "./pages/DeStijl"; 6 | import { Route, Link } from "wouter"; 7 | import { OrthographicCamera } from "./components/Orthographic"; 8 | import styles from "./App.module.css"; 9 | import { Shapes } from "./pages/Shapes"; 10 | 11 | const BASE_PATH = __BASE_PATH__; 12 | 13 | function App() { 14 | return ( 15 | <> 16 |
17 |
18 |

Generative Artwork with Three.js

19 | 53 |
54 | 55 | 76 |
77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | ); 108 | } 109 | 110 | export default App; 111 | -------------------------------------------------------------------------------- /src/App.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: grid; 3 | padding: 4rem 1rem 1rem 1rem; 4 | grid-template-columns: 1fr; 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | width: 100%; 9 | box-sizing: border-box; 10 | z-index: 1; 11 | gap: 1rem; 12 | background: rgba(255,255,255,0.5); 13 | } 14 | 15 | .titlewrap { 16 | display: flex; 17 | flex-direction: column; 18 | gap: 1rem; 19 | } 20 | 21 | .title { 22 | font-size: 1rem; 23 | font-weight: 700; 24 | margin: 0; 25 | line-height: 1.15; 26 | white-space: break-spaces; 27 | } 28 | 29 | .meta { 30 | display: flex; 31 | flex-wrap: wrap; 32 | gap: 1rem; 33 | } 34 | 35 | .metalink { 36 | font-size: 1rem; 37 | font-weight: 400; 38 | margin: 0; 39 | display: block; 40 | margin-top: 0.25rem; 41 | color: #000; 42 | } 43 | 44 | .nav { 45 | z-index: 1; 46 | font-size: 1rem; 47 | } 48 | 49 | .link { 50 | color: #666; 51 | text-decoration: none; 52 | } 53 | 54 | .link.active { 55 | color: #000; 56 | } 57 | 58 | .separator { 59 | margin: 0 0.5rem; 60 | color: #cacaca; 61 | } 62 | 63 | /* Media query for larger screens */ 64 | @media (min-width: 50em) { 65 | .header { 66 | grid-template-columns: 1fr 1fr 1fr; 67 | padding: 1.5rem; 68 | background: transparent; 69 | } 70 | .nav { 71 | justify-self: center; 72 | } 73 | } -------------------------------------------------------------------------------- /src/components/DeStijlGrid.jsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useRef } from "react"; 2 | import { Color, DoubleSide, MathUtils, Object3D } from "three"; 3 | import { useFrame } from "@react-three/fiber"; 4 | import { makeNoise2D } from "open-simplex-noise"; 5 | import PropTypes from "prop-types"; 6 | 7 | const dummy = new Object3D(); 8 | const c = new Color(); 9 | const noise = makeNoise2D(10); 10 | 11 | const DeStijlGrid = ({ 12 | width = 50, 13 | height = 80, 14 | columns = 12, 15 | rows = 10, 16 | palette = [ 17 | "#B04E26", 18 | "#007443", 19 | "#263E66", 20 | "#CABCA2", 21 | "#C3C3B7", 22 | "#8EA39C", 23 | "#E5C03C", 24 | "#66857F", 25 | "#3A5D57", 26 | ], 27 | }) => { 28 | const mesh = useRef(); 29 | const colorRef = useRef([]); 30 | const smallColumns = useMemo(() => { 31 | const baseColumns = [2, 4, 6, 9]; 32 | 33 | if (columns <= 12) { 34 | return baseColumns; 35 | } 36 | 37 | const additionalColumns = Array.from( 38 | { length: Math.floor((columns - 12) / 2) }, 39 | () => Math.floor(Math.random() * (columns - 12)) + 13 40 | ); 41 | 42 | return [...new Set([...baseColumns, ...additionalColumns])].sort( 43 | (a, b) => a - b 44 | ); 45 | }, [columns]); 46 | 47 | const colors = useMemo(() => { 48 | const temp = []; 49 | for (let i = 0; i < columns; i++) { 50 | for (let j = 0; j < rows; j++) { 51 | const rand = noise(i, j) * 1.2; 52 | const range = smallColumns.includes(i + 1) 53 | ? 0 54 | : Math.floor(MathUtils.mapLinear(rand, -1, 1, ...[1, 4])); 55 | 56 | const color = c.set(palette[range]).toArray(); 57 | colorRef.current.push(palette[range]); 58 | temp.push(color); 59 | } 60 | } 61 | return new Float32Array(temp.flat()); 62 | }, [columns, rows, palette, smallColumns]); 63 | 64 | const squares = useMemo(() => { 65 | const temp = []; 66 | let x = 0; 67 | const row = height / rows; 68 | 69 | for (let i = 0; i < columns; i++) { 70 | const n = noise(i, 0); 71 | const remainingWidth = width - x; 72 | const ratio = remainingWidth / (columns - i); 73 | const column = smallColumns.includes(i + 1) 74 | ? 1.5 75 | : ratio * MathUtils.mapLinear(n, -1, 1, 1.5, 2); 76 | const adjustedColumn = i === columns - 1 ? remainingWidth : column; 77 | 78 | for (let j = 0; j < rows; j++) { 79 | const currentColor = colorRef.current[i * rows + j]; 80 | let z = 0; 81 | 82 | if (currentColor === palette[0]) { 83 | z = 0; 84 | } else if (currentColor === palette[1]) { 85 | z = -10; 86 | } else if (currentColor === palette[2]) { 87 | z = 0; 88 | } else if (currentColor === palette[3]) { 89 | z = 8; 90 | } else if (currentColor === palette[4]) { 91 | z = -5000; 92 | } 93 | 94 | temp.push({ 95 | x: x + adjustedColumn / 2 - width / 2, 96 | y: j * row + row / 2 - height / 2, 97 | z: z, 98 | scaleX: adjustedColumn, 99 | scaleY: row, 100 | }); 101 | } 102 | 103 | x += column; 104 | } 105 | return temp; 106 | }, [height, width, rows, columns, smallColumns]); 107 | 108 | useFrame(() => { 109 | for (let i = 0; i < squares.length; i++) { 110 | const { x, y, z, scaleX, scaleY } = squares[i]; 111 | dummy.position.set(x, y, z); 112 | dummy.scale.set(scaleX, scaleY, 4); 113 | dummy.updateMatrix(); 114 | mesh.current.setMatrixAt(i, dummy.matrix); 115 | } 116 | 117 | mesh.current.instanceMatrix.needsUpdate = true; 118 | }); 119 | 120 | return ( 121 | 126 | 127 | 131 | 132 | 138 | 139 | ); 140 | }; 141 | 142 | DeStijlGrid.propTypes = { 143 | width: PropTypes.number, 144 | height: PropTypes.number, 145 | columns: PropTypes.number, 146 | rows: PropTypes.number, 147 | palette: PropTypes.arrayOf(PropTypes.string), 148 | }; 149 | 150 | export { DeStijlGrid }; 151 | -------------------------------------------------------------------------------- /src/components/Link.jsx: -------------------------------------------------------------------------------- 1 | import { useRoute, Link as WouterLink } from "wouter"; 2 | import clsx from "clsx"; 3 | 4 | const Link = ({ children, href, className }) => { 5 | const [isActive] = useRoute(href); 6 | return ( 7 | 8 | {children} 9 | 10 | ); 11 | }; 12 | 13 | export { Link }; 14 | -------------------------------------------------------------------------------- /src/components/LygiaGrid.jsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useRef } from "react"; 2 | import { Color, DoubleSide, MathUtils, Object3D } from "three"; 3 | import { useFrame } from "@react-three/fiber"; 4 | import { makeNoise2D } from "open-simplex-noise"; 5 | import PropTypes from "prop-types"; 6 | 7 | const dummy = new Object3D(); 8 | const c = new Color(); 9 | const noise = makeNoise2D(10); 10 | 11 | const LygiaGrid = ({ 12 | width = 50, 13 | height = 80, 14 | columns = 12, 15 | rows = 10, 16 | palette = [ 17 | "#B04E26", 18 | "#007443", 19 | "#263E66", 20 | "#CABCA2", 21 | "#C3C3B7", 22 | "#8EA39C", 23 | "#E5C03C", 24 | "#66857F", 25 | "#3A5D57", 26 | ], 27 | }) => { 28 | const mesh = useRef(); 29 | const smallColumns = useMemo(() => { 30 | const baseColumns = [2, 4, 7, 8, 10, 11]; 31 | 32 | if (columns <= 12) { 33 | return baseColumns; 34 | } 35 | 36 | const additionalColumns = Array.from( 37 | { length: Math.floor((columns - 12) / 2) }, 38 | () => Math.floor(Math.random() * (columns - 12)) + 13 39 | ); 40 | 41 | return [...new Set([...baseColumns, ...additionalColumns])].sort( 42 | (a, b) => a - b 43 | ); 44 | }, [columns]); 45 | 46 | const colors = useMemo(() => { 47 | const temp = []; 48 | for (let i = 0; i < columns; i++) { 49 | for (let j = 0; j < rows; j++) { 50 | const rand = noise(i, j) * 1.5; 51 | const range = smallColumns.includes(i + 1) 52 | ? [0, 4] 53 | : [1, palette.length - 1]; 54 | const colorIndex = Math.floor( 55 | MathUtils.mapLinear(rand, -1.5, 1.5, ...range) 56 | ); 57 | const color = c.set(palette[colorIndex]).toArray(); 58 | temp.push(color); 59 | } 60 | } 61 | return new Float32Array(temp.flat()); 62 | }, [columns, rows, palette, smallColumns]); 63 | 64 | const squares = useMemo(() => { 65 | const temp = []; 66 | let x = 0; 67 | const row = height / rows; 68 | 69 | for (let i = 0; i < columns; i++) { 70 | const n = noise(i, 0) * 5; 71 | const remainingWidth = width - x; 72 | const ratio = remainingWidth / (columns - i); 73 | const column = smallColumns.includes(i + 1) 74 | ? ratio / MathUtils.mapLinear(n, -1, 1, 3, 4) 75 | : ratio * MathUtils.mapLinear(n, -1, 1, 1.5, 2); 76 | const adjustedColumn = i === columns - 1 ? remainingWidth : column; 77 | for (let j = 0; j < rows; j++) { 78 | temp.push({ 79 | x: x + adjustedColumn / 2 - width / 2, 80 | y: j * row + row / 2 - height / 2, 81 | scaleX: adjustedColumn, 82 | scaleY: row, 83 | }); 84 | } 85 | 86 | x += column; 87 | } 88 | return temp; 89 | }, [height, width, rows, columns, smallColumns]); 90 | 91 | useFrame(() => { 92 | for (let i = 0; i < squares.length; i++) { 93 | const { x, y, scaleX, scaleY } = squares[i]; 94 | dummy.position.set(x, y, 0); 95 | dummy.scale.set(scaleX, scaleY, 0); 96 | dummy.updateMatrix(); 97 | mesh.current.setMatrixAt(i, dummy.matrix); 98 | } 99 | 100 | mesh.current.instanceMatrix.needsUpdate = true; 101 | }); 102 | 103 | return ( 104 | 109 | 110 | 114 | 115 | 121 | 122 | ); 123 | }; 124 | 125 | LygiaGrid.propTypes = { 126 | width: PropTypes.number, 127 | height: PropTypes.number, 128 | columns: PropTypes.number, 129 | rows: PropTypes.number, 130 | palette: PropTypes.arrayOf(PropTypes.string), 131 | }; 132 | 133 | export { LygiaGrid }; 134 | -------------------------------------------------------------------------------- /src/components/Orthographic.jsx: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | import { useThree } from "@react-three/fiber"; 4 | import { useEffect, useRef } from "react"; 5 | import PropTypes from "prop-types"; 6 | import { useLocation } from "wouter"; 7 | 8 | const BASE_PATH = __BASE_PATH__; 9 | 10 | export const OrthographicCamera = () => { 11 | const [location] = useLocation(); 12 | const set = useThree(({ set }) => set); 13 | const camera = useThree(({ camera }) => camera); 14 | const size = useThree(({ size }) => size); 15 | const cameraRef = useRef( 16 | new THREE.OrthographicCamera(0, 0, 0, 0, -1000, 1000) 17 | ); 18 | const perspectiveRef = useRef(new THREE.PerspectiveCamera(75, 0, 0.1, 1000)); 19 | 20 | useEffect(() => { 21 | const oldCam = camera; 22 | if (location === `${BASE_PATH}/de-stijl`) { 23 | cameraRef.current.left = size.width / -2; 24 | cameraRef.current.right = size.width / 2; 25 | cameraRef.current.top = size.height / 2; 26 | cameraRef.current.bottom = size.height / -2; 27 | cameraRef.current.position.set(-10, 8, 10); 28 | cameraRef.current.lookAt(0, 0, 0); 29 | cameraRef.current.zoom = 10; 30 | 31 | cameraRef.current.updateProjectionMatrix(); 32 | cameraRef.current.updateMatrix(); 33 | set(() => ({ camera: cameraRef.current })); 34 | return () => set(() => ({ camera: oldCam })); 35 | } else { 36 | perspectiveRef.current.position.set(0, 0, 64); 37 | perspectiveRef.current.aspect = size.width / size.height; 38 | perspectiveRef.current.updateProjectionMatrix(); 39 | 40 | perspectiveRef.current.updateMatrix(); 41 | set(() => ({ camera: perspectiveRef.current })); 42 | return () => set(() => ({ camera: oldCam })); 43 | } 44 | }, [camera, set, size, location]); 45 | 46 | return null; 47 | }; 48 | 49 | OrthographicCamera.propTypes = { 50 | makeDefault: PropTypes.bool, 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/RichterGrid.jsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useRef } from "react"; 2 | import { Color, DoubleSide, MathUtils, Object3D } from "three"; 3 | import { useFrame } from "@react-three/fiber"; 4 | import { makeNoise2D } from "open-simplex-noise"; 5 | import PropTypes from "prop-types"; 6 | 7 | const dummy = new Object3D(); 8 | const c = new Color(); 9 | const noise = makeNoise2D(Date.now()); 10 | 11 | const RichterGrid = ({ 12 | width = 50, 13 | height = 80, 14 | columns = 12, 15 | rows = 10, 16 | isRandom, 17 | palette = [ 18 | "#B04E26", 19 | "#007443", 20 | "#263E66", 21 | "#CABCA2", 22 | "#C3C3B7", 23 | "#8EA39C", 24 | "#E5C03C", 25 | "#66857F", 26 | "#3A5D57", 27 | ], 28 | }) => { 29 | const mesh = useRef(); 30 | const smallColumns = useMemo(() => { 31 | const baseColumns = [2, 4, 7, 8, 10, 11]; 32 | 33 | if (columns <= 12) { 34 | return baseColumns; 35 | } 36 | 37 | const additionalColumns = Array.from( 38 | { length: Math.floor((columns - 12) / 2) }, 39 | () => Math.floor(Math.random() * (columns - 12)) + 13 40 | ); 41 | 42 | return [...new Set([...baseColumns, ...additionalColumns])].sort( 43 | (a, b) => a - b 44 | ); 45 | }, [columns]); 46 | 47 | const randomValues = useMemo(() => { 48 | const values = []; 49 | for (let i = 0; i < columns * rows; i++) { 50 | values.push(MathUtils.randInt(0, palette.length - 1)); 51 | } 52 | return values; 53 | }, [columns, rows]); 54 | 55 | const colors = useMemo(() => { 56 | const temp = []; 57 | for (let i = 0; i < columns; i++) { 58 | for (let j = 0; j < rows; j++) { 59 | const rand = noise(i, j) * 1.5; 60 | const colorIndex = isRandom 61 | ? smallColumns.includes(i + 1) 62 | ? Math.floor(MathUtils.mapLinear(rand, -1, 1, 0, 4)) 63 | : Math.floor( 64 | MathUtils.mapLinear(rand, -1, 1, 1, palette.length - 1) 65 | ) 66 | : randomValues[i * rows + j]; 67 | 68 | temp.push(c.set(palette[colorIndex]).toArray()); 69 | } 70 | } 71 | return new Float32Array(temp.flat()); 72 | }, [columns, rows, palette, smallColumns]); 73 | 74 | const squares = useMemo(() => { 75 | const temp = []; 76 | let x = 0; 77 | const row = height / rows; 78 | 79 | for (let i = 0; i < columns; i++) { 80 | const remainingWidth = width - x; 81 | const ratio = remainingWidth / (columns - i); 82 | const column = ratio; 83 | const adjustedColumn = i === columns - 1 ? remainingWidth : column; 84 | for (let j = 0; j < rows; j++) { 85 | temp.push({ 86 | x: x + adjustedColumn / 2 - width / 2, 87 | y: j * row + row / 2 - height / 2, 88 | scaleX: adjustedColumn, 89 | scaleY: row, 90 | }); 91 | } 92 | 93 | x += column; 94 | } 95 | return temp; 96 | }, [height, width, rows, columns]); 97 | 98 | useFrame(() => { 99 | for (let i = 0; i < squares.length; i++) { 100 | const { x, y, scaleX, scaleY } = squares[i]; 101 | dummy.position.set(x, y, 0); 102 | dummy.scale.set(scaleX, scaleY, 0); 103 | dummy.updateMatrix(); 104 | mesh.current.setMatrixAt(i, dummy.matrix); 105 | } 106 | 107 | mesh.current.instanceMatrix.needsUpdate = true; 108 | }); 109 | 110 | return ( 111 | 116 | 117 | 121 | 122 | 128 | 129 | ); 130 | }; 131 | 132 | RichterGrid.propTypes = { 133 | width: PropTypes.number, 134 | height: PropTypes.number, 135 | columns: PropTypes.number, 136 | rows: PropTypes.number, 137 | palette: PropTypes.arrayOf(PropTypes.string), 138 | isRandom: PropTypes.bool, 139 | }; 140 | 141 | export { RichterGrid }; 142 | -------------------------------------------------------------------------------- /src/components/ShapesGrid.jsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useRef } from "react"; 2 | import { Color, DoubleSide, MathUtils, Object3D } from "three"; 3 | import { useFrame } from "@react-three/fiber"; 4 | import { makeNoise2D } from "open-simplex-noise"; 5 | import PropTypes from "prop-types"; 6 | 7 | const dummy = new Object3D(); 8 | const c = new Color(); 9 | const noise = makeNoise2D(10); 10 | 11 | const ShapesGrid = ({ 12 | width = 50, 13 | height = 80, 14 | columns = 12, 15 | rows = 10, 16 | palette = [ 17 | "#B04E26", 18 | "#007443", 19 | "#263E66", 20 | "#CABCA2", 21 | "#C3C3B7", 22 | "#8EA39C", 23 | "#E5C03C", 24 | "#66857F", 25 | "#3A5D57", 26 | ], 27 | }) => { 28 | const mesh = useRef(); 29 | const smallColumns = useMemo(() => { 30 | const baseColumns = [2, 4, 7, 8, 10, 11]; 31 | 32 | if (columns <= 12) { 33 | return baseColumns; 34 | } 35 | 36 | const additionalColumns = Array.from( 37 | { length: Math.floor((columns - 12) / 2) }, 38 | () => Math.floor(Math.random() * (columns - 12)) + 13 39 | ); 40 | 41 | return [...new Set([...baseColumns, ...additionalColumns])].sort( 42 | (a, b) => a - b 43 | ); 44 | }, [columns]); 45 | 46 | const colors = useMemo(() => { 47 | const temp = []; 48 | for (let i = 0; i < columns; i++) { 49 | for (let j = 0; j < rows; j++) { 50 | const rand = noise(i, j) * 1.5; 51 | const range = smallColumns.includes(i + 1) 52 | ? [0, 4] 53 | : [1, palette.length - 1]; 54 | const colorIndex = Math.floor( 55 | MathUtils.mapLinear(rand, -1.5, 1.5, ...range) 56 | ); 57 | const color = c.set(palette[colorIndex]).toArray(); 58 | temp.push(color); 59 | } 60 | } 61 | return new Float32Array(temp.flat()); 62 | }, [columns, rows, palette, smallColumns]); 63 | 64 | const squares = useMemo(() => { 65 | const temp = []; 66 | let x = 0; 67 | const row = height / rows; 68 | 69 | for (let i = 0; i < columns; i++) { 70 | const n = noise(i, 0); 71 | const remainingWidth = width - x; 72 | const ratio = remainingWidth / (columns - i); 73 | const column = smallColumns.includes(i + 1) 74 | ? ratio / MathUtils.mapLinear(n, -1, 1, 1, 2) 75 | : ratio * MathUtils.mapLinear(n, -1, 1, 1, 2); 76 | const adjustedColumn = i === columns - 1 ? remainingWidth : column; 77 | for (let j = 0; j < rows; j++) { 78 | temp.push({ 79 | x: x + adjustedColumn / 2 - width / 2, 80 | y: j * row + row / 2 - height / 2, 81 | scaleX: adjustedColumn, 82 | scaleY: row, 83 | }); 84 | } 85 | 86 | x += column; 87 | } 88 | return temp; 89 | }, [height, width, rows, columns, smallColumns]); 90 | 91 | useFrame(() => { 92 | for (let i = 0; i < squares.length; i++) { 93 | const { x, y, scaleX, scaleY } = squares[i]; 94 | dummy.position.set(x, y, 0); 95 | dummy.scale.set(scaleX, scaleY, 0); 96 | dummy.updateMatrix(); 97 | mesh.current.setMatrixAt(i, dummy.matrix); 98 | } 99 | 100 | mesh.current.instanceMatrix.needsUpdate = true; 101 | }); 102 | 103 | return ( 104 | 109 | 110 | 114 | 115 | 121 | 122 | ); 123 | }; 124 | 125 | ShapesGrid.propTypes = { 126 | width: PropTypes.number, 127 | height: PropTypes.number, 128 | columns: PropTypes.number, 129 | rows: PropTypes.number, 130 | palette: PropTypes.arrayOf(PropTypes.string), 131 | }; 132 | 133 | export { ShapesGrid }; 134 | -------------------------------------------------------------------------------- /src/context/index.ts: -------------------------------------------------------------------------------- 1 | import { atom } from "jotai"; 2 | 3 | export const isOrthographic = atom(false); 4 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | margin: 0; 6 | font-size: 14px; 7 | background-color: #fff; 8 | } 9 | 10 | body { 11 | font-family: "Inter", sans-serif; 12 | } -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./index.css"; 4 | 5 | import App from "./App.jsx"; 6 | 7 | createRoot(document.getElementById("root")).render( 8 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /src/pages/DeStijl.jsx: -------------------------------------------------------------------------------- 1 | import { DeStijlGrid } from "../components/DeStijlGrid"; 2 | import { folder, useControls } from "leva"; 3 | 4 | const DeStijl = () => { 5 | const { 6 | width, 7 | height, 8 | columns, 9 | rows, 10 | color1, 11 | color2, 12 | color3, 13 | color4, 14 | color5, 15 | } = useControls("De Stijl", { 16 | width: { value: 60, min: 1, max: 224, step: 1 }, 17 | height: { value: 60, min: 1, max: 224, step: 1 }, 18 | columns: { value: 10, min: 1, max: 500, step: 1 }, 19 | rows: { value: 12, min: 1, max: 500, step: 1 }, 20 | palette: folder({ 21 | color1: "#1a1a1a", 22 | color2: "#4d74cc", 23 | color3: "#bc3d30", 24 | color4: "#ffef00", 25 | color5: "#ffffff", 26 | }), 27 | }); 28 | 29 | return ( 30 | 37 | ); 38 | }; 39 | 40 | export { DeStijl }; 41 | -------------------------------------------------------------------------------- /src/pages/Lygia.jsx: -------------------------------------------------------------------------------- 1 | import { LygiaGrid } from "../components/LygiaGrid"; 2 | import { folder, useControls } from "leva"; 3 | 4 | const Lygia = () => { 5 | const { 6 | width, 7 | height, 8 | columns, 9 | rows, 10 | color1, 11 | color2, 12 | color3, 13 | color4, 14 | color5, 15 | color6, 16 | color7, 17 | color8, 18 | color9, 19 | } = useControls("Lygia", { 20 | width: { value: 50, min: 1, max: 224, step: 1 }, 21 | height: { value: 80, min: 1, max: 224, step: 1 }, 22 | columns: { value: 12, min: 1, max: 500, step: 1 }, 23 | rows: { value: 10, min: 1, max: 500, step: 1 }, 24 | palette: folder({ 25 | color1: "#B04E26", 26 | color2: "#007443", 27 | color3: "#263E66", 28 | color4: "#CABCA2", 29 | color5: "#C3C3B7", 30 | color6: "#8EA39C", 31 | color7: "#E5C03C", 32 | color8: "#66857F", 33 | color9: "#3A5D57", 34 | }), 35 | }); 36 | 37 | return ( 38 | <> 39 | 56 | 57 | ); 58 | }; 59 | 60 | export { Lygia }; 61 | -------------------------------------------------------------------------------- /src/pages/Richter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { folder, LevaPanel, useControls } from "leva"; 3 | import { RichterGrid } from "../components/RichterGrid"; 4 | 5 | const Richter = () => { 6 | const { 7 | width, 8 | height, 9 | columns, 10 | rows, 11 | color1, 12 | color2, 13 | color3, 14 | color4, 15 | color5, 16 | color6, 17 | color7, 18 | color8, 19 | color9, 20 | } = useControls("Richter", { 21 | width: { value: 80, min: 1, max: 224, step: 1 }, 22 | height: { value: 35, min: 1, max: 224, step: 1 }, 23 | columns: { value: 1, min: 1, max: 500, step: 1 }, 24 | rows: { value: 224, min: 1, max: 500, step: 1 }, 25 | palette: folder({ 26 | color1: "#B04E26", 27 | color2: "#007443", 28 | color3: "#263E66", 29 | color4: "#CABCA2", 30 | color5: "#C3C3B7", 31 | color6: "#8EA39C", 32 | color7: "#E5C03C", 33 | color8: "#66857F", 34 | color9: "#3A5D57", 35 | }), 36 | }); 37 | 38 | return ( 39 | <> 40 | 41 | 59 | 60 | ); 61 | }; 62 | 63 | export { Richter }; 64 | -------------------------------------------------------------------------------- /src/pages/RichterFarben.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { folder, LevaPanel, useControls } from "leva"; 3 | import { RichterGrid } from "../components/RichterGrid"; 4 | 5 | const RichterFarben = () => { 6 | const { 7 | width, 8 | height, 9 | columns, 10 | rows, 11 | color1, 12 | color2, 13 | color3, 14 | color4, 15 | color5, 16 | color6, 17 | color7, 18 | color8, 19 | color9, 20 | color10, 21 | color11, 22 | color12, 23 | color13, 24 | color14, 25 | color15, 26 | color16, 27 | color17, 28 | color18, 29 | color19, 30 | color20, 31 | color21, 32 | color22, 33 | color23, 34 | color24, 35 | } = useControls("Richter Farben", { 36 | width: { value: 80, min: 1, max: 224, step: 1 }, 37 | height: { value: 80, min: 1, max: 224, step: 1 }, 38 | columns: { value: 70, min: 1, max: 500, step: 1 }, 39 | rows: { value: 70, min: 1, max: 500, step: 1 }, 40 | palette: folder({ 41 | color1: "#909495", 42 | color2: "#ED7140", 43 | color3: "#C38CBA", 44 | color4: "#E95C06", 45 | color5: "#018933", 46 | color6: "#FFEB1B", 47 | color7: "#023F28", 48 | color8: "#062C6A", 49 | color9: "#FDCC02", 50 | color10: "#41338A", 51 | color11: "#F7B6BE", 52 | color12: "#C6CC04", 53 | color13: "#FBA601", 54 | color14: "#FFF7F5", 55 | color15: "#E5020D", 56 | color16: "#C10E22", 57 | color17: "#0B0A06", 58 | color18: "#EB5F64", 59 | color19: "#0376B6", 60 | color20: "#065797", 61 | color21: "#411943", 62 | color22: "#6B2B6B", 63 | color23: "#02603C", 64 | color24: "#BF2C45", 65 | }), 66 | }); 67 | 68 | useEffect(() => { 69 | console.log("camera"); 70 | }, []); 71 | 72 | return ( 73 | <> 74 | 75 | 108 | 109 | ); 110 | }; 111 | 112 | export { RichterFarben }; 113 | -------------------------------------------------------------------------------- /src/pages/Shapes.jsx: -------------------------------------------------------------------------------- 1 | import { ShapesGrid } from "../components/ShapesGrid"; 2 | import { folder, useControls } from "leva"; 3 | 4 | const Shapes = () => { 5 | const { width, height, columns, rows, color1 } = useControls("Shapes", { 6 | width: { value: 70, min: 1, max: 224, step: 1 }, 7 | height: { value: 70, min: 1, max: 224, step: 1 }, 8 | columns: { value: 11, min: 1, max: 500, step: 1 }, 9 | rows: { value: 11, min: 1, max: 500, step: 1 }, 10 | palette: folder({ 11 | color1: "#000000", 12 | }), 13 | }); 14 | 15 | return ( 16 | <> 17 | 24 | 25 | ); 26 | }; 27 | 28 | export { Shapes }; 29 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig(({ command }) => ({ 6 | base: "./", 7 | define: { 8 | __BASE_PATH__: 9 | command === "build" 10 | ? JSON.stringify("/Tutorials/GenerativeArtworkThreejs") 11 | : JSON.stringify(""), 12 | }, 13 | plugins: [react()], 14 | })); 15 | --------------------------------------------------------------------------------