├── .gitignore
├── LICENSE
├── package.json
├── public
├── electron.js
├── favicon.ico
├── icon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
└── src
├── App.css
├── App.js
├── assets
├── icon.ico
├── logo.png
├── mask.png
└── sl_logo.png
├── components
├── Aside.css
├── Aside.js
├── Canvas.js
├── Footer.css
├── Footer.js
├── Header.css
├── Header.js
├── Main.css
└── Main.js
├── index.css
├── index.js
├── reportWebVitals.js
└── setupTests.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 | /.pnp
4 | .pnp.js
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | /dist
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | yarn.lock
25 | package-lock.json
26 |
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 HashLips
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hashlips-art-engine-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@electron/remote": "^2.0.8",
7 | "@testing-library/jest-dom": "^5.16.4",
8 | "@testing-library/react": "^12.1.4",
9 | "@testing-library/user-event": "^13.5.0",
10 | "canvas": "^2.9.1",
11 | "electron-is-dev": "^2.0.0",
12 | "electron-squirrel-startup": "^1.0.0",
13 | "react": "^18.0.0",
14 | "react-dom": "^18.0.0",
15 | "react-icons": "^4.3.1",
16 | "react-scripts": "5.0.0",
17 | "react-sort-list": "^0.0.13",
18 | "web-vitals": "^2.1.4"
19 | },
20 | "main": "public/electron.js",
21 | "homepage": "./",
22 | "author": "HashLips",
23 | "scripts": {
24 | "start": "react-scripts start",
25 | "build": "react-scripts build",
26 | "test": "react-scripts test",
27 | "eject": "react-scripts eject",
28 | "electron-dev": "concurrently \"BROWSER=none yarn start\" \"wait-on http://localhost:3000 && electron .\"",
29 | "electron-pack": "electron-builder -c.extraMetadata.main=build/electron.js",
30 | "electron-pack-mac": "electron-builder --mac --x64 -c.extraMetadata.main=build/electron.js",
31 | "electron-pack-win": "electron-builder --win --x64 -c.extraMetadata.main=build/electron.js"
32 | },
33 | "eslintConfig": {
34 | "extends": [
35 | "react-app",
36 | "react-app/jest"
37 | ]
38 | },
39 | "browserslist": {
40 | "production": [
41 | ">0.2%",
42 | "not dead",
43 | "not op_mini all"
44 | ],
45 | "development": [
46 | "last 1 chrome version",
47 | "last 1 firefox version",
48 | "last 1 safari version"
49 | ]
50 | },
51 | "devDependencies": {
52 | "@electron-forge/cli": "^6.0.0-beta.63",
53 | "@electron-forge/maker-deb": "^6.0.0-beta.63",
54 | "@electron-forge/maker-rpm": "^6.0.0-beta.63",
55 | "@electron-forge/maker-squirrel": "^6.0.0-beta.63",
56 | "@electron-forge/maker-zip": "^6.0.0-beta.63",
57 | "concurrently": "^7.1.0",
58 | "electron": "^18.0.3",
59 | "electron-builder": "^23.0.3",
60 | "wait-on": "^6.0.1"
61 | },
62 | "build": {
63 | "appId": "com.hashlips.ea",
64 | "icon": "build/icon.ico",
65 | "files": [
66 | "build/**/*",
67 | "node_modules/**/*"
68 | ],
69 | "directories": {
70 | "buildResources": "assets"
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/public/electron.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const { app, BrowserWindow } = require("electron");
3 | const isDev = require("electron-is-dev");
4 |
5 | require("@electron/remote/main").initialize();
6 |
7 | function createWindow() {
8 | // Create the browser window.
9 | const win = new BrowserWindow({
10 | width: 1080,
11 | height: 720,
12 | webPreferences: {
13 | nodeIntegration: true,
14 | enableRemoteModule: true,
15 | contextIsolation: false,
16 | webSecurity: false,
17 | },
18 | });
19 |
20 | require("@electron/remote/main").enable(win.webContents);
21 |
22 | // and load the index.html of the app.
23 | // win.loadFile("index.html");
24 | win.loadURL(
25 | isDev
26 | ? "http://localhost:3000"
27 | : `file://${path.join(__dirname, "../build/index.html")}`
28 | );
29 | // Open the DevTools.
30 | if (isDev) {
31 | win.webContents.openDevTools({ mode: "detach" });
32 | }
33 | }
34 |
35 | // This method will be called when Electron has finished
36 | // initialization and is ready to create browser windows.
37 | // Some APIs can only be used after this event occurs.
38 | app.whenReady().then(createWindow);
39 |
40 | // Quit when all windows are closed, except on macOS. There, it's common
41 | // for applications and their menu bar to stay active until the user quits
42 | // explicitly with Cmd + Q.
43 | app.on("window-all-closed", () => {
44 | if (process.platform !== "darwin") {
45 | app.quit();
46 | }
47 | });
48 |
49 | app.on("activate", () => {
50 | if (BrowserWindow.getAllWindows().length === 0) {
51 | createWindow();
52 | }
53 | });
54 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HashLips/hashlips_art_engine_app/8aa47384939ccf03f09fafdc37548b502416056e/public/favicon.ico
--------------------------------------------------------------------------------
/public/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HashLips/hashlips_art_engine_app/8aa47384939ccf03f09fafdc37548b502416056e/public/icon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | HashLips Art Engine
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HashLips/hashlips_art_engine_app/8aa47384939ccf03f09fafdc37548b502416056e/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HashLips/hashlips_art_engine_app/8aa47384939ccf03f09fafdc37548b502416056e/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --pink: #dc16d9;
3 | --yellow: #f2d21e;
4 | --grey: #2e2e2e;
5 | --white: #fdfdfd;
6 | --offWhite: #e6e6e6;
7 | }
8 |
9 | body {
10 | color: var(--white);
11 | background-color: var(--white);
12 | font-size: 16px;
13 | }
14 |
15 | .grid-container {
16 | display: grid;
17 | grid-template-columns: 1fr;
18 | grid-template-rows: 50px 1fr 35px;
19 |
20 | grid-template-areas:
21 | "header"
22 | "main"
23 | "footer";
24 | height: 150vh;
25 | }
26 |
27 | /* responsive layout */
28 | @media only screen and (min-width: 750px) {
29 | .grid-container {
30 | display: grid;
31 | grid-template-columns: 240px 1fr;
32 | grid-template-rows: 50px 1fr 35px;
33 | grid-template-areas:
34 | "aside header"
35 | "aside main"
36 | "aside footer";
37 | height: 100vh;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import Header from "./components/Header";
3 | import Aside from "./components/Aside";
4 | import Main from "./components/Main";
5 | import Footer from "./components/Footer";
6 | import "./App.css";
7 |
8 | const { app } = window.require("@electron/remote");
9 | const fs = window.require("fs");
10 | const path = window.require("path");
11 |
12 | function App() {
13 | const [sideBarOpen, setSideBarOpen] = useState(false);
14 | const [config, setConfig] = useState({
15 | supply: 5,
16 | name: "HashLips NFT",
17 | symbol: "HNFT",
18 | description: "This is a collection about...",
19 | width: 1024,
20 | height: 1024,
21 | baseUri: "ipfs://ReplaceCID",
22 | inputPath: app.getAppPath(),
23 | outputPath: app.getAppPath(),
24 | });
25 | const [folderNames, setFolderNames] = useState([]);
26 | const [progress, setProgress] = useState(0);
27 | const [status, setStatus] = useState("");
28 |
29 | const handleConfigChange = (event) => {
30 | console.log(event.target.name);
31 | setConfig({ ...config, [event.target.name]: event.target.value });
32 | console.log(config);
33 | };
34 |
35 | const toggleSideBar = () => {
36 | setSideBarOpen(!sideBarOpen);
37 | };
38 |
39 | const getRarityWeight = (_str) => {
40 | let nameWithoutExtension = _str.slice(0, -4);
41 | var nameWithoutWeight = Number(nameWithoutExtension.split("$").pop());
42 | if (isNaN(nameWithoutWeight)) {
43 | nameWithoutWeight = 1;
44 | }
45 | return nameWithoutWeight;
46 | };
47 |
48 | const cleanName = (_str) => {
49 | let nameWithoutExtension = _str.slice(0, -4);
50 | var nameWithoutWeight = nameWithoutExtension.split("$").shift();
51 | return nameWithoutWeight;
52 | };
53 |
54 | const getElements = (_path) => {
55 | return fs
56 | .readdirSync(_path)
57 | .filter((item) => !/(^|\/)\.[^\/\.]/g.test(item))
58 | .map((i, index) => {
59 | if (i.includes("-")) {
60 | setStatus(`Layer name can not contain dashes, please fix: ${i}`);
61 | throw new Error(
62 | `Layer name can not contain dashes, please fix: ${i}`
63 | );
64 | }
65 | return {
66 | id: index,
67 | name: cleanName(i),
68 | filename: i,
69 | path: `${_path}/${i}`,
70 | weight: getRarityWeight(i),
71 | };
72 | });
73 | };
74 |
75 | const getFolders = async () => {
76 | fs.readdir(config.inputPath, (err, files) => {
77 | if (err) {
78 | setStatus("Unable to load the folder set");
79 | return;
80 | }
81 | let newFiles = files
82 | .filter((item) => !/(^|\/)\.[^\/\.]/g.test(item))
83 | .map((file, index) => {
84 | return {
85 | id: index + 1,
86 | elements: getElements(path.join(config.inputPath, file)),
87 | name: file,
88 | };
89 | });
90 | setFolderNames(newFiles);
91 | });
92 | };
93 |
94 | console.log(status);
95 |
96 | useEffect(() => {
97 | setStatus("");
98 | }, [config, folderNames]);
99 |
100 | return (
101 |
102 |
103 |
115 |
121 |
122 |
123 | );
124 | }
125 |
126 | export default App;
127 |
--------------------------------------------------------------------------------
/src/assets/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HashLips/hashlips_art_engine_app/8aa47384939ccf03f09fafdc37548b502416056e/src/assets/icon.ico
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HashLips/hashlips_art_engine_app/8aa47384939ccf03f09fafdc37548b502416056e/src/assets/logo.png
--------------------------------------------------------------------------------
/src/assets/mask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HashLips/hashlips_art_engine_app/8aa47384939ccf03f09fafdc37548b502416056e/src/assets/mask.png
--------------------------------------------------------------------------------
/src/assets/sl_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HashLips/hashlips_art_engine_app/8aa47384939ccf03f09fafdc37548b502416056e/src/assets/sl_logo.png
--------------------------------------------------------------------------------
/src/components/Aside.css:
--------------------------------------------------------------------------------
1 | .aside {
2 | grid-area: aside;
3 | display: flex;
4 | flex-direction: column;
5 | height: 100%;
6 | width: 240px;
7 | position: fixed;
8 | overflow-y: auto;
9 | z-index: 2;
10 | transform: translateX(-245px);
11 | color: var(--grey);
12 | background-color: var(--offWhite);
13 | }
14 |
15 | .aside.active {
16 | transform: translateX(0);
17 | }
18 |
19 | .aside_list_title {
20 | height: 50px;
21 | padding: 0px 15px;
22 | display: flex;
23 | align-items: center;
24 | font-weight: 700;
25 | background-color: var(--grey);
26 | color: var(--white);
27 | }
28 |
29 | .aside_img_link {
30 | margin-right: 10px;
31 | }
32 |
33 | .aside_list {
34 | padding: 0;
35 | list-style-type: none;
36 | }
37 |
38 | .aside_list_item {
39 | padding: 10px 15px;
40 | font-size: 14px;
41 | cursor: pointer;
42 | }
43 |
44 | .aside_list_item_input_label {
45 | margin-top: 10px;
46 | font-size: 12px;
47 | font-weight: 700;
48 | width: 100%;
49 | }
50 |
51 | .aside_list_item_button {
52 | margin-top: 5px;
53 | width: 210px;
54 | font-size: 14px;
55 | resize: none;
56 | padding: 5px;
57 | }
58 |
59 | .aside_list_item_input {
60 | margin-top: 5px;
61 | width: 195px;
62 | font-size: 14px;
63 | resize: none;
64 | padding: 5px;
65 | }
66 |
67 | .aside_list_item_filename_container {
68 | margin-top: 5px;
69 | width: 200px;
70 | padding: 5px;
71 | font-size: 14px;
72 | cursor: pointer;
73 | color: var(--white);
74 | background-color: var(--grey);
75 | }
76 |
77 | .aside_close-icon {
78 | position: absolute;
79 | visibility: visible;
80 | top: 20px;
81 | right: 20px;
82 | cursor: pointer;
83 | color: var(--white);
84 | }
85 |
86 | /* responsive layout */
87 | @media only screen and (min-width: 750px) {
88 | .aside {
89 | display: flex;
90 | flex-direction: column;
91 | position: relative;
92 | transform: translateX(0);
93 | }
94 |
95 | .aside_close-icon {
96 | display: none;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/components/Aside.js:
--------------------------------------------------------------------------------
1 | import { FiX } from "react-icons/fi";
2 | import mask from "../assets/mask.png";
3 | import { SortableItem, swapArrayPositions } from "react-sort-list";
4 | import "./Aside.css";
5 | import { useState } from "react";
6 | const { createCanvas, loadImage } = require(`canvas`);
7 |
8 | const { dialog } = window.require("@electron/remote");
9 | const fs = window.require("fs");
10 | const path = window.require("path");
11 |
12 | function Aside(props) {
13 | const canvas = createCanvas(props.config.width, props.config.height);
14 | const ctx = canvas.getContext("2d");
15 | var metadataList = [];
16 | var attributesList = [];
17 | var dnaList = new Set();
18 |
19 | const buildFolders = () => {
20 | if (fs.existsSync(path.join(props.config.outputPath, "build"))) {
21 | fs.rmdirSync(path.join(props.config.outputPath, "build"), {
22 | recursive: true,
23 | });
24 | }
25 | fs.mkdirSync(path.join(props.config.outputPath, "build"));
26 | fs.mkdirSync(path.join(props.config.outputPath, "build", "json"));
27 | fs.mkdirSync(path.join(props.config.outputPath, "build", "images"));
28 | };
29 |
30 | const swap = (dragIndex, dropIndex) => {
31 | let swappedFolders = swapArrayPositions(
32 | props.folderNames,
33 | dragIndex,
34 | dropIndex
35 | );
36 |
37 | props.setFolderNames([...swappedFolders]);
38 | };
39 |
40 | const input = (_label, _name, _initialValue, _type) => {
41 | return (
42 | <>
43 | {_label}
44 |
51 | >
52 | );
53 | };
54 |
55 | const setPath = async (_field) => {
56 | let path = await dialog.showOpenDialog({
57 | properties: ["openDirectory"],
58 | });
59 | if (path.filePaths[0]) {
60 | props.setConfig({
61 | ...props.config,
62 | [_field]: path.filePaths[0],
63 | });
64 | }
65 | };
66 |
67 | const filterDNAOptions = (_dna) => {
68 | const dnaItems = _dna.split("-");
69 | const filteredDNA = dnaItems.filter((element) => {
70 | const query = /(\?.*$)/;
71 | const querystring = query.exec(element);
72 | if (!querystring) {
73 | return true;
74 | }
75 | const options = querystring[1].split("&").reduce((r, setting) => {
76 | const keyPairs = setting.split("=");
77 | return { ...r, [keyPairs[0]]: keyPairs[1] };
78 | }, []);
79 |
80 | return options.bypassDNA;
81 | });
82 | return filteredDNA.join("-");
83 | };
84 |
85 | const isDnaUnique = (_DnaList = new Set(), _dna = "") => {
86 | const _filteredDNA = filterDNAOptions(_dna);
87 | return !_DnaList.has(_filteredDNA);
88 | };
89 |
90 | const createDna = (_layers) => {
91 | let randNum = [];
92 | _layers.forEach((layer) => {
93 | var totalWeight = 0;
94 | layer.elements.forEach((element) => {
95 | totalWeight += element.weight;
96 | });
97 | // number between 0 - totalWeight
98 | let random = Math.floor(Math.random() * totalWeight);
99 | for (var i = 0; i < layer.elements.length; i++) {
100 | // subtract the current weight from the random weight until we reach a sub zero value.
101 | random -= layer.elements[i].weight;
102 | if (random < 0) {
103 | return randNum.push(
104 | `${layer.elements[i].id}:${layer.elements[i].filename}`
105 | );
106 | }
107 | }
108 | });
109 | return randNum.join("-");
110 | };
111 |
112 | const removeQueryStrings = (_dna) => {
113 | const query = /(\?.*$)/;
114 | return _dna.replace(query, "");
115 | };
116 |
117 | const cleanDna = (_str) => {
118 | const withoutOptions = removeQueryStrings(_str);
119 | var dna = Number(withoutOptions.split(":").shift());
120 | return dna;
121 | };
122 |
123 | const constructLayerToDna = (_dna = "", _layers = []) => {
124 | let mappedDnaToLayers = _layers.map((layer, index) => {
125 | let selectedElement = layer.elements.find(
126 | (e) => e.id == cleanDna(_dna.split("-")[index])
127 | );
128 | return {
129 | name: layer.name,
130 | selectedElement: selectedElement,
131 | };
132 | });
133 | return mappedDnaToLayers;
134 | };
135 |
136 | const loadLayerImg = async (_layer) => {
137 | try {
138 | return new Promise(async (resolve) => {
139 | const image = await loadImage(`file://${_layer.selectedElement.path}`);
140 | resolve({ layer: _layer, loadedImage: image });
141 | });
142 | } catch (error) {
143 | console.error("Error loading image:", error);
144 | }
145 | };
146 |
147 | const addAttributes = (_element) => {
148 | let selectedElement = _element.layer.selectedElement;
149 | attributesList.push({
150 | trait_type: _element.layer.name,
151 | value: selectedElement.name,
152 | });
153 | };
154 |
155 | const drawElement = (_renderObject, _index) => {
156 | ctx.drawImage(
157 | _renderObject.loadedImage,
158 | 0,
159 | 0,
160 | props.config.width,
161 | props.config.height
162 | );
163 |
164 | addAttributes(_renderObject);
165 | };
166 |
167 | const saveImage = (_editionCount) => {
168 | const url = canvas.toDataURL("image/png");
169 | const base64Data = url.replace(/^data:image\/png;base64,/, "");
170 | fs.writeFileSync(
171 | path.join(
172 | props.config.outputPath,
173 | "build",
174 | "images",
175 | `${_editionCount}.png`
176 | ),
177 | base64Data,
178 | "base64"
179 | );
180 | };
181 |
182 | const addMetadata = (_dna, _edition) => {
183 | let dateTime = Date.now();
184 | let tempMetadata = {
185 | name: `${props.config.name} #${_edition}`,
186 | description: props.config.description,
187 | image: `REPLACE/${_edition}.png`,
188 | edition: _edition,
189 | date: dateTime,
190 | attributes: attributesList,
191 | compiler: "HashLips Art Engine",
192 | };
193 | metadataList.push(tempMetadata);
194 | attributesList = [];
195 | };
196 |
197 | const saveMetaDataSingleFile = (_editionCount) => {
198 | let metadata = metadataList.find((meta) => meta.edition == _editionCount);
199 | fs.writeFileSync(
200 | path.join(
201 | props.config.outputPath,
202 | "build",
203 | "json",
204 | `${_editionCount}.json`
205 | ),
206 | JSON.stringify(metadata, null, 2)
207 | );
208 | };
209 |
210 | const writeMetaData = (_data) => {
211 | fs.writeFileSync(
212 | path.join(props.config.outputPath, "build", "json", `_metadata.json`),
213 | _data
214 | );
215 | };
216 |
217 | const startCreating = async () => {
218 | props.setProgress(0);
219 | let editionCount = 1;
220 | let failedCount = 0;
221 | while (editionCount <= props.config.supply) {
222 | let newDna = createDna(props.folderNames);
223 | if (isDnaUnique(dnaList, newDna)) {
224 | let results = constructLayerToDna(newDna, props.folderNames);
225 | let loadedElements = [];
226 |
227 | results.forEach((layer) => {
228 | loadedElements.push(loadLayerImg(layer));
229 | });
230 |
231 | await Promise.all(loadedElements).then((renderObjectArray) => {
232 | ctx.clearRect(0, 0, props.config.width, props.config.height);
233 | renderObjectArray.forEach((renderObject, index) => {
234 | drawElement(renderObject, index);
235 | });
236 |
237 | saveImage(editionCount);
238 | addMetadata(newDna, editionCount);
239 | saveMetaDataSingleFile(editionCount);
240 | console.log(`Created edition: ${editionCount}`);
241 | });
242 | dnaList.add(filterDNAOptions(newDna));
243 | editionCount++;
244 | props.setProgress(editionCount - 1);
245 | } else {
246 | console.log("DNA exists!");
247 | failedCount++;
248 | if (failedCount >= 1000) {
249 | console.log(
250 | `You need more layers or elements to grow your edition to ${props.config.supply} artworks!`
251 | );
252 | process.exit();
253 | }
254 | }
255 | }
256 | writeMetaData(JSON.stringify(metadataList, null, 2));
257 | };
258 |
259 | // Metadata ====================
260 |
261 | const updateMetadata = () => {
262 | let rawdata = fs.readFileSync(
263 | path.join(props.config.outputPath, "build", "json", `_metadata.json`)
264 | );
265 | let data = JSON.parse(rawdata);
266 |
267 | data.forEach((item) => {
268 | item.name = `${props.config.name} #${item.edition}`;
269 | item.description = props.config.description;
270 | item.image = `${props.config.baseUri}/${item.edition}.png`;
271 |
272 | fs.writeFileSync(
273 | path.join(
274 | props.config.outputPath,
275 | "build",
276 | "json",
277 | `${item.edition}.json`
278 | ),
279 | JSON.stringify(item, null, 2)
280 | );
281 | });
282 |
283 | fs.writeFileSync(
284 | path.join(props.config.outputPath, "build", "json", `_metadata.json`),
285 | JSON.stringify(data, null, 2)
286 | );
287 |
288 | console.log(`Updated baseUri for images to ===> ${props.config.baseUri}`);
289 | console.log(
290 | `Updated description for images to ===> ${props.config.description}`
291 | );
292 | console.log(`Updated name prefix for images to ===> ${props.config.name}`);
293 | };
294 |
295 | const generate = () => {
296 | props.setStatus("");
297 | if (props.config.supply <= 0) {
298 | props.setStatus("Your need to increase the supply.");
299 | return;
300 | }
301 | console.log(props.folderNames);
302 | if (props.folderNames.length == 0) {
303 | props.setStatus(
304 | "Make sure to get the folder with only image files in them."
305 | );
306 | return;
307 | }
308 | buildFolders();
309 | startCreating();
310 | };
311 |
312 | return (
313 |
400 | );
401 | }
402 |
403 | export default Aside;
404 |
--------------------------------------------------------------------------------
/src/components/Canvas.js:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect, useState } from "react";
2 |
3 | const Canvas = (props) => {
4 | const canvasRef = useRef(null);
5 |
6 | const loadImage = (url) => {
7 | return new Promise((r) => {
8 | let i = new Image();
9 | i.onload = () => r(i);
10 | i.src = url;
11 | });
12 | };
13 |
14 | const updateCanvas = async (_ctx, _paths) => {
15 | _paths.forEach(async (path) => {
16 | let img = await loadImage(path);
17 | _ctx.drawImage(img, 0, 0, 300, 300);
18 | });
19 | };
20 |
21 | useEffect(() => {
22 | const canvas = canvasRef.current;
23 | const ctx = canvas.getContext("2d");
24 |
25 | let renderImages = props.folderNames.map((i) => {
26 | return encodeURI("file://" + i.elements[0].path);
27 | });
28 |
29 | ctx.clearRect(0, 0, 300, 300);
30 | updateCanvas(ctx, renderImages);
31 | }, [props.folderNames]);
32 |
33 | return ;
34 | };
35 |
36 | export default Canvas;
37 |
--------------------------------------------------------------------------------
/src/components/Footer.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | grid-area: footer;
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | gap: 10px;
7 | padding: 0 15px;
8 | color: var(--pink);
9 | background-color: var(--offWhite);
10 | }
11 |
12 | .footer_link {
13 | display: flex;
14 | align-items: center;
15 | justify-content: center;
16 | color: var(--pink);
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import { FiGithub, FiYoutube, FiLifeBuoy } from "react-icons/fi";
2 | import smLogo from "../assets/sl_logo.png";
3 | import "./Footer.css";
4 |
5 | function Footer() {
6 | return (
7 |
33 | );
34 | }
35 |
36 | export default Footer;
37 |
--------------------------------------------------------------------------------
/src/components/Header.css:
--------------------------------------------------------------------------------
1 | .header {
2 | grid-area: header;
3 | display: flex;
4 | align-items: center;
5 | justify-content: space-between;
6 | padding: 0 15px;
7 | background-color: var(--pink);
8 | font-weight: 700;
9 | font-size: 16px;
10 | }
11 |
12 | .header_menu {
13 | cursor: pointer;
14 | }
15 |
16 | /* responsive layout */
17 | @media only screen and (min-width: 750px) {
18 | .header_menu {
19 | display: none;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import logo from "../assets/logo.png";
2 | import { FiMenu } from "react-icons/fi";
3 | import smLogo from "../assets/sl_logo.png";
4 | import "./Header.css";
5 |
6 | function Header(props) {
7 | return (
8 |
17 | );
18 | }
19 |
20 | export default Header;
21 |
--------------------------------------------------------------------------------
/src/components/Main.css:
--------------------------------------------------------------------------------
1 | .main {
2 | grid-area: main;
3 | background-color: var(--grey);
4 | display: flex;
5 | flex-direction: column;
6 | }
7 |
8 | .main_info {
9 | margin: 15px;
10 | padding: 15px;
11 | display: flex;
12 | flex-direction: column;
13 | justify-content: space-between;
14 | align-items: flex-start;
15 | font-size: 12px;
16 | gap: 5px;
17 | white-space: nowrap;
18 | overflow: hidden;
19 | border: 1px solid var(--white);
20 | text-overflow: ellipsis;
21 | margin-bottom: 0;
22 | }
23 |
24 | .main_cards {
25 | margin: 15px;
26 | display: grid;
27 | grid-template-columns: 1fr;
28 | grid-template-rows: 400px 200px;
29 | grid-template-areas:
30 | "card1"
31 | "card2";
32 | grid-gap: 15px;
33 | margin-bottom: 0;
34 | }
35 |
36 | .log_info {
37 | margin: 15px;
38 | padding: 15px;
39 | display: flex;
40 | flex: 1;
41 | flex-direction: column;
42 | font-size: 12px;
43 | background-color: black;
44 | }
45 |
46 | .log_info_title {
47 | margin-top: 10px;
48 | font-size: 12px;
49 | }
50 |
51 | .log_info_text {
52 | margin-top: 5px;
53 | font-size: 10px;
54 | }
55 |
56 | .card {
57 | padding: 15px;
58 | border: 1px solid var(--white);
59 | }
60 |
61 | .card:first-child {
62 | grid-area: card1;
63 | display: flex;
64 | align-items: center;
65 | justify-content: center;
66 | }
67 | .card:nth-child(2) {
68 | grid-area: card2;
69 | overflow-y: auto;
70 | }
71 |
72 | .card_tree_title {
73 | font-size: 14px;
74 | font-weight: 700;
75 | }
76 |
77 | .card_tree_item {
78 | font-size: 12px;
79 | margin-top: 10px;
80 | font-weight: 700;
81 | }
82 |
83 | .card_tree_sub_item_title {
84 | font-size: 10px;
85 | margin-top: 5px;
86 | }
87 |
88 | /* responsive layout */
89 | @media only screen and (min-width: 750px) {
90 | .main_cards {
91 | margin: 15px;
92 | display: grid;
93 | grid-template-columns: 2fr 1fr;
94 | grid-template-rows: 100px 300px;
95 | grid-template-areas:
96 | "card1 card2"
97 | "card1 card2";
98 | grid-gap: 15px;
99 | margin-bottom: 0;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/components/Main.js:
--------------------------------------------------------------------------------
1 | import Canvas from "./Canvas";
2 | import "./Main.css";
3 |
4 | function Main(props) {
5 | return (
6 |
7 |
8 |
Supply: {props.config.supply}
9 |
Name: {props.config.name}
10 |
Symbol: {props.config.symbol}
11 |
Description: {props.config.description}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
Tree
19 | {props.folderNames.length > 0 ? (
20 | props.folderNames.map((item, index) => {
21 | return (
22 |
23 |
{item.name}
24 | {item.elements.map((element) => {
25 | return (
26 |
27 | ---{element.name}
28 | {element.weight}
29 |
30 | );
31 | })}
32 |
33 | );
34 | })
35 | ) : (
36 |
37 | The tree is empty. Please set the configurations.
38 |
39 | )}
40 |
41 |
42 |
43 |
44 | Progress: {props.progress}/{props.config.supply}
45 |
46 |
56 |
Status:
57 |
61 | {props.status != "" ? props.status : "Everything look good"}
62 |
63 |
64 |
65 | );
66 | }
67 |
68 | export default Main;
69 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | body {
7 | margin: 0;
8 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
9 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
10 | sans-serif;
11 | -webkit-font-smoothing: antialiased;
12 | -moz-osx-font-smoothing: grayscale;
13 | }
14 |
15 | code {
16 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
17 | monospace;
18 | }
19 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { createRoot } from "react-dom/client";
3 | import "./index.css";
4 | import App from "./App";
5 | import reportWebVitals from "./reportWebVitals";
6 |
7 | const rootElement = document.getElementById("root");
8 | const root = createRoot(rootElement);
9 |
10 | root.render(
11 |
12 |
13 |
14 | );
15 |
16 | // If you want to start measuring performance in your app, pass a function
17 | // to log results (for example: reportWebVitals(console.log))
18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
19 | reportWebVitals();
20 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------