├── .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 |
54 |
55 |
76 |
77 |
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 |
--------------------------------------------------------------------------------