├── .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 |
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 |
9 | props.toggleSideBar()} /> 10 | 11 | 12 | 13 | 14 | 15 | 16 |
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 | --------------------------------------------------------------------------------