├── .prettierignore
├── assets
└── preview.jpg
├── src
├── helpers
│ ├── math.js
│ ├── hash.js
│ ├── screenshot.js
│ ├── iota.js
│ └── csv.js
├── pages
│ ├── 404.js
│ └── index.js
├── css
│ └── style.css
├── components
│ ├── copy
│ │ ├── title.js
│ │ └── instructions.js
│ ├── modals
│ │ ├── set-resolution.js
│ │ ├── init-automation.js
│ │ ├── set-hash.js
│ │ └── run-automation.js
│ ├── info
│ │ └── features.js
│ ├── panels
│ │ ├── controls.js
│ │ └── viewer.js
│ └── inputs
│ │ └── range-slider.js
├── hooks
│ ├── useFeatures.js
│ ├── useURL.js
│ ├── useAutomation.js
│ └── useHash.js
└── containers
│ └── app.js
├── Makefile
├── gatsby-config.js
├── .gitignore
├── .prettierrc
├── .eslintrc.js
├── package.json
├── LICENSE
├── lib
└── connector.js
└── README.md
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | public
--------------------------------------------------------------------------------
/assets/preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/owmo-dev/token-art-tools/HEAD/assets/preview.jpg
--------------------------------------------------------------------------------
/src/helpers/math.js:
--------------------------------------------------------------------------------
1 | export function clamp(num, min, max) {
2 | return Math.min(Math.max(num, min), max);
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/404.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const NotFound = () => {
4 | return
404
;
5 | };
6 |
7 | export default NotFound;
8 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | run-server:
2 | gatsby develop
3 |
4 | build:
5 | gatsby build
6 |
7 | serve: build
8 | gatsby serve
9 |
10 | deploy:
11 | npm run deploy
12 |
--------------------------------------------------------------------------------
/src/helpers/hash.js:
--------------------------------------------------------------------------------
1 | function isValidHash(str) {
2 | const regexExp = /^0x[a-f0-9]{64}$/gi;
3 | return regexExp.test(str);
4 | }
5 |
6 | export {isValidHash};
7 |
--------------------------------------------------------------------------------
/gatsby-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | pathPrefix: '/token-art-tools',
3 | siteMetadata: {
4 | title: 'Token Art Tool',
5 | },
6 | plugins: [],
7 | };
8 |
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import App from '../containers/app';
3 |
4 | const Index = () => {
5 | return ;
6 | };
7 |
8 | export default Index;
9 |
--------------------------------------------------------------------------------
/src/css/style.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | height: 100%;
3 | width: 100%;
4 | background:rgb(40,40,40);
5 | background: linear-gradient(0deg, rgba(40,40,40,1) 0%, rgba(60,60,60,1) 80%);
6 | }
--------------------------------------------------------------------------------
/src/helpers/screenshot.js:
--------------------------------------------------------------------------------
1 | export function screenshot(hash) {
2 | var iframe = window.document.querySelector('iframe').contentWindow;
3 | if (iframe === undefined) return;
4 | iframe.postMessage({command: 'screenshot', token: hash}, '*');
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .cache/
2 | .idea/
3 | .vscode/
4 | node_modules/
5 | build
6 | .DS_Store
7 | *.tgz
8 | my-app*
9 | template/src/__tests__/__snapshots__/
10 | lerna-debug.log
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | /.changelog
15 | .npm/
16 | yarn.lock
17 | public/
18 | *.env*
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "bracketSpacing": false,
4 | "endOfLine": "lf",
5 | "htmlWhitespaceSensitivity": "css",
6 | "printWidth": 160,
7 | "semi": true,
8 | "singleQuote": true,
9 | "tabWidth": 4,
10 | "trailingComma": "all",
11 | "useTabs": false
12 | }
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-undef
2 | module.exports = {
3 | env: {
4 | browser: true,
5 | es2021: true,
6 | },
7 | extends: ['eslint:recommended', 'plugin:react/recommended'],
8 | parserOptions: {
9 | ecmaFeatures: {
10 | jsx: true,
11 | },
12 | ecmaVersion: 12,
13 | sourceType: 'module',
14 | },
15 | plugins: ['react'],
16 | rules: {
17 | 'react/prop-types': 0,
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/src/helpers/iota.js:
--------------------------------------------------------------------------------
1 | export function iota(start = 0) {
2 | let count = start;
3 | let firstProp = true;
4 | return new Proxy(
5 | {},
6 | {
7 | get(o, prop) {
8 | if (firstProp) {
9 | firstProp = false;
10 | return {
11 | // Enum descriptor
12 | get values() {
13 | return o;
14 | },
15 | };
16 | }
17 | if (prop in o) return o[prop];
18 | else return (o[prop] = count++);
19 | },
20 | },
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/helpers/csv.js:
--------------------------------------------------------------------------------
1 | export function exportCSV(features) {
2 | let csvContent = 'data:text/csv;charset=utf-8,';
3 |
4 | let keys = Object.keys(features[0]);
5 | csvContent += keys;
6 | csvContent += '\r\n';
7 |
8 | features.map(feature => {
9 | csvContent += Object.keys(feature).map(key => {
10 | return feature[key];
11 | });
12 | csvContent += '\r\n';
13 | return null;
14 | });
15 |
16 | var encodedUri = encodeURI(csvContent);
17 | var hrefElement = document.createElement('a');
18 | hrefElement.href = encodedUri;
19 | hrefElement.download = `features_${new Date().toJSON().slice(0, 10)}.csv`;
20 | document.body.appendChild(hrefElement);
21 | hrefElement.click();
22 | hrefElement.remove();
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/copy/title.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Header, Icon} from 'semantic-ui-react';
3 |
4 | const Title = () => {
5 | return (
6 |
18 | );
19 | };
20 |
21 | export default Title;
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "token-art-tools",
3 | "version": "1.6.5",
4 | "private": true,
5 | "description": "Token Art Tools",
6 | "author": "Owen Moore",
7 | "keywords": [
8 | "gatsby"
9 | ],
10 | "dependencies": {
11 | "react": "^18.2.0",
12 | "react-dom": "^18.2.0",
13 | "react-range-step-input": "^1.3.0",
14 | "semantic-ui-react": "^2.1.4",
15 | "fomantic-ui-css": "^2.9.1"
16 | },
17 | "scripts": {
18 | "deploy": "gatsby build --prefix-paths && gh-pages -d public -b deploy"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/owmo-dev/token-art-tools"
23 | },
24 | "devDependencies": {
25 | "eslint": "^8.32.0",
26 | "eslint-plugin-react": "^7.32.1",
27 | "gh-pages": "^4.0.0",
28 | "gatsby": "^5.4.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Owen Moore
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 |
--------------------------------------------------------------------------------
/src/hooks/useFeatures.js:
--------------------------------------------------------------------------------
1 | import React, {createContext, useContext, useReducer, useMemo} from 'react';
2 | import {iota} from '../helpers/iota';
3 |
4 | const FeaturesContext = createContext();
5 |
6 | export const {F_SET, F_CLEAR} = iota();
7 |
8 | const init = {
9 | data: {},
10 | };
11 |
12 | function automationReducer(state, dispatch) {
13 | switch (dispatch.type) {
14 | case F_SET: {
15 | return {...state, data: dispatch.data};
16 | }
17 | case F_CLEAR: {
18 | return init;
19 | }
20 | default:
21 | throw new Error(`automationReducer type '${dispatch.type}' not supported`);
22 | }
23 | }
24 |
25 | function FeaturesProvider(props) {
26 | const [state, dispatch] = useReducer(automationReducer, init);
27 | const value = useMemo(() => [state, dispatch], [state]);
28 | return ;
29 | }
30 |
31 | function useFeatures() {
32 | const context = useContext(FeaturesContext);
33 | if (!context) {
34 | throw new Error(`useFeatures must be used within the FeaturesProvider`);
35 | }
36 | return context;
37 | }
38 |
39 | export {useFeatures, FeaturesProvider};
40 |
--------------------------------------------------------------------------------
/src/containers/app.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Viewer from '../components/panels/viewer';
4 | import Controls from '../components/panels/controls';
5 |
6 | import 'fomantic-ui-css/semantic.css';
7 | import '../css/style.css';
8 |
9 | import {HashProvider} from '../hooks/useHash';
10 | import {URLProvider} from '../hooks/useURL';
11 | import {FeaturesProvider} from '../hooks/useFeatures';
12 | import {AutomationProvider} from '../hooks/useAutomation';
13 |
14 | const App = () => {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | export default App;
44 |
--------------------------------------------------------------------------------
/src/hooks/useURL.js:
--------------------------------------------------------------------------------
1 | import React, {createContext, useContext, useReducer, useMemo} from 'react';
2 | import {iota} from '../helpers/iota';
3 |
4 | const URLContext = createContext();
5 |
6 | export const {U_SET, U_REFRESH, U_CLEAR} = iota();
7 |
8 | const init = {
9 | url: '',
10 | isValid: false,
11 | iframeKey: '',
12 | };
13 |
14 | function urlReducer(state, dispatch) {
15 | switch (dispatch.type) {
16 | case U_SET: {
17 | return {
18 | ...state,
19 | url: dispatch.url,
20 | isValid: validateURL(dispatch.url),
21 | iframeKey: getRandomString(),
22 | };
23 | }
24 | case U_REFRESH: {
25 | return {...state, iframeKey: getRandomString()};
26 | }
27 | case U_CLEAR: {
28 | return init;
29 | }
30 | default:
31 | throw new Error(`urlReducer type '${dispatch.type}' not supported`);
32 | }
33 | }
34 |
35 | function getRandomString() {
36 | return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
37 | }
38 |
39 | function validateURL(string) {
40 | let url;
41 |
42 | try {
43 | url = new URL(string);
44 | } catch (_) {
45 | return false;
46 | }
47 |
48 | return url.protocol === 'http:' || url.protocol === 'https:';
49 | }
50 |
51 | function URLProvider(props) {
52 | const [state, dispatch] = useReducer(urlReducer, init);
53 | const value = useMemo(() => [state, dispatch], [state]);
54 | return ;
55 | }
56 |
57 | function useURL() {
58 | const context = useContext(URLContext);
59 | if (!context) {
60 | throw new Error(`useURL must be used within the URLProvider`);
61 | }
62 | return context;
63 | }
64 |
65 | export {useURL, URLProvider};
66 |
--------------------------------------------------------------------------------
/lib/connector.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | const params = new URLSearchParams(window.location.search);
3 |
4 | var hash = params.get('hash');
5 | var number = params.get('number');
6 |
7 | if (hash && number) tokenData = {hash: hash, tokenId: 1000000 + number};
8 |
9 | alphabet = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ';
10 | fxhash = 'oo' + hash.slice(2, 51);
11 | b58dec = str => [...str].reduce((p, c) => (p * alphabet.length + alphabet.indexOf(c)) | 0, 0);
12 | fxhashTrunc = fxhash.slice(2);
13 | regex = new RegExp('.{' + ((fxhashTrunc.length / 4) | 0) + '}', 'g');
14 | hashes = fxhashTrunc.match(regex).map(h => b58dec(h));
15 | sfc32 = (a, b, c, d) => {
16 | return () => {
17 | a |= 0;
18 | b |= 0;
19 | c |= 0;
20 | d |= 0;
21 | var t = (((a + b) | 0) + d) | 0;
22 | d = (d + 1) | 0;
23 | a = b ^ (b >>> 9);
24 | b = (c + (c << 3)) | 0;
25 | c = (c << 21) | (c >>> 11);
26 | c = (c + t) | 0;
27 | return (t >>> 0) / 4294967296;
28 | };
29 | };
30 | fxrand = sfc32(...hashes);
31 |
32 | var features = {};
33 |
34 | function screenshot(name) {
35 | const art = document.querySelector('canvas');
36 | const img = document.createElement('img');
37 | const canvas = document.createElement('canvas');
38 | canvas.width = art.width;
39 | canvas.height = art.height;
40 | canvas.getContext('2d').drawImage(art, 0, 0);
41 |
42 | let dataUrl = canvas.toDataURL('image/png');
43 | img.src = dataUrl;
44 |
45 | var hrefElement = document.createElement('a');
46 | hrefElement.href = dataUrl;
47 | document.body.append(hrefElement);
48 | hrefElement.download = name + '.png';
49 | hrefElement.click();
50 | hrefElement.remove();
51 | }
52 |
53 | window.onload = function () {
54 | function handleMessage(e) {
55 | switch (e.data['command']) {
56 | case 'screenshot':
57 | screenshot(e.data['token']);
58 | break;
59 | case 'getFeatures':
60 | window.parent.postMessage({command: 'loadFeatures', features: features}, '*');
61 | break;
62 | default:
63 | break;
64 | }
65 | }
66 | window.addEventListener('message', handleMessage);
67 | };
68 |
--------------------------------------------------------------------------------
/src/components/modals/set-resolution.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import {Modal, Form, Message, Button} from 'semantic-ui-react';
3 |
4 | const SetResolution = props => {
5 | const [isError, setErrorState] = useState(false);
6 | const [isSubmitting, setSubmitState] = useState(false);
7 |
8 | const emptyFormData = {x: '', y: ''};
9 | const [formData, setFormData] = useState(emptyFormData);
10 |
11 | function onChange(e) {
12 | setFormData(prev => ({
13 | ...prev,
14 | [e.target.name]: e.target.value,
15 | }));
16 | }
17 |
18 | function cancel() {
19 | closeModal(true);
20 | }
21 |
22 | function closeModal() {
23 | setErrorState(false);
24 | setSubmitState(false);
25 | setFormData(emptyFormData);
26 | props.close();
27 | }
28 |
29 | function handleSubmit() {
30 | setErrorState(false);
31 | setSubmitState(true);
32 |
33 | var x = parseInt(formData.x);
34 | var y = parseInt(formData.y);
35 |
36 | if (isNaN(x) || isNaN(y) || x < 10 || x > 10000 || y < 10 || y > 10000) {
37 | setErrorState(true);
38 | setSubmitState(false);
39 | return;
40 | }
41 |
42 | props.set(x, y);
43 | closeModal();
44 | }
45 |
46 | return (
47 |
48 | Set Custom Resolution
49 |
50 |
52 |
53 |
54 |
55 |
56 | {isError ? ERROR: numbers only, min 10, max 10,000 : null}
57 |
58 |
59 |
62 |
65 |
66 |
67 | );
68 | };
69 |
70 | export default SetResolution;
71 |
--------------------------------------------------------------------------------
/src/hooks/useAutomation.js:
--------------------------------------------------------------------------------
1 | import React, {createContext, useContext, useReducer, useMemo} from 'react';
2 | import {iota} from '../helpers/iota';
3 | import {clamp} from '../helpers/math';
4 |
5 | const AutomationContext = createContext();
6 |
7 | export const {A_START, A_TICK, A_STOP, A_EXPORT, A_RESET} = iota();
8 |
9 | const init = {
10 | status: 'idle',
11 | total: 0,
12 | doScreenshot: true,
13 | doCSVExport: false,
14 | waitTime: 2000,
15 | progress: 0,
16 | tick: 0,
17 | };
18 |
19 | function automationReducer(state, dispatch) {
20 | switch (dispatch.type) {
21 | case A_START: {
22 | return {
23 | ...state,
24 | status: 'active',
25 | total: dispatch.total,
26 | doScreenshot: dispatch.doScreenshot,
27 | doCSVExport: dispatch.doCSVExport,
28 | waitTime: dispatch.waitTime,
29 | progress: 0,
30 | tick: 0,
31 | };
32 | }
33 | case A_TICK: {
34 | let tick = state.tick + 1;
35 | let progress = clamp(parseInt((tick / state.total) * 100), 0, 100);
36 | return {
37 | ...state,
38 | status: 'active',
39 | progress: progress,
40 | tick: tick,
41 | };
42 | }
43 | case A_STOP: {
44 | return {
45 | ...state,
46 | status: 'stopping',
47 | progress: 100,
48 | tick: state.total,
49 | };
50 | }
51 | case A_EXPORT: {
52 | return {
53 | ...state,
54 | status: 'exporting',
55 | progress: 100,
56 | tick: state.total,
57 | };
58 | }
59 | case A_RESET: {
60 | return init;
61 | }
62 | default:
63 | throw new Error(`automationReducer type '${dispatch.type}' not supported`);
64 | }
65 | }
66 |
67 | function AutomationProvider(props) {
68 | const [state, dispatch] = useReducer(automationReducer, init);
69 | const value = useMemo(() => [state, dispatch], [state]);
70 | return ;
71 | }
72 |
73 | function useAutomation() {
74 | const context = useContext(AutomationContext);
75 | if (!context) {
76 | throw new Error(`useAutomation must be used within the AutomationProvider`);
77 | }
78 | return context;
79 | }
80 |
81 | export {useAutomation, AutomationProvider};
82 |
--------------------------------------------------------------------------------
/src/components/modals/init-automation.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import {Modal, Form, Message, Button} from 'semantic-ui-react';
3 |
4 | import {A_START, useAutomation} from '../../hooks/useAutomation';
5 |
6 | const InitAutomation = props => {
7 | const {active, close} = props;
8 |
9 | const [, automationAction] = useAutomation();
10 |
11 | const [isError, setErrorState] = useState(false);
12 | const [isSubmitting, setSubmitState] = useState(false);
13 |
14 | const emptyFormData = {total: 0, wait: 2000, csv: false};
15 | const [formData, setFormData] = useState(emptyFormData);
16 |
17 | function onChange(e, v) {
18 | let data = v.type === 'checkbox' ? v.checked : v.value;
19 | setFormData(prev => ({
20 | ...prev,
21 | [v.name]: data,
22 | }));
23 | }
24 |
25 | function closeModal() {
26 | setErrorState(false);
27 | setSubmitState(false);
28 | setFormData(emptyFormData);
29 | close();
30 | }
31 |
32 | function handleSubmit() {
33 | setErrorState(false);
34 | setSubmitState(true);
35 |
36 | var t = parseInt(formData.total);
37 | var w = parseInt(formData.wait);
38 | var c = formData.csv;
39 |
40 | if (isNaN(t) || isNaN(w) || t < 2 || t > 10000 || w < 2000 || w > 10000) {
41 | setErrorState(true);
42 | setSubmitState(false);
43 | return;
44 | }
45 |
46 | closeModal();
47 |
48 | automationAction({
49 | type: A_START,
50 | total: t,
51 | doScreenshot: true,
52 | doCSVExport: c,
53 | waitTime: w,
54 | });
55 | }
56 |
57 | return (
58 |
59 | Setup Automation
60 |
61 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | {isError ? ERROR: Total and Wait numbers only, within ranges specified : null}
71 |
72 |
73 |
76 |
79 |
80 |
81 | );
82 | };
83 |
84 | export default InitAutomation;
85 |
--------------------------------------------------------------------------------
/src/components/modals/set-hash.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import {Modal, Form, Message, Button} from 'semantic-ui-react';
3 | import {H_SET, useHash} from '../../hooks/useHash';
4 | import {isValidHash} from '../../helpers/hash';
5 |
6 | const SetHash = props => {
7 | const [hash, hashAction] = useHash();
8 |
9 | const [isError, setErrorState] = useState(false);
10 | const [error, setError] = useState('');
11 | const [isSubmitting, setSubmitState] = useState(false);
12 |
13 | const {active, close} = props;
14 |
15 | const emptyFormData = {hash: '', number: ''};
16 | const [formData, setFormData] = useState(emptyFormData);
17 |
18 | function onChange(e) {
19 | setFormData(prev => ({
20 | ...prev,
21 | [e.target.name]: e.target.value,
22 | }));
23 | }
24 |
25 | function cancel() {
26 | closeModal(true);
27 | }
28 |
29 | function closeModal() {
30 | setErrorState(false);
31 | setSubmitState(false);
32 | setFormData(emptyFormData);
33 | close();
34 | }
35 |
36 | function handleSubmit() {
37 | setErrorState(false);
38 | setSubmitState(true);
39 |
40 | if (formData.hash === '' && formData.number === '') {
41 | setError('ERROR: To submit you must provide either an Edition number or Hash string (or both at once)');
42 | setErrorState(true);
43 | setSubmitState(false);
44 | return;
45 | }
46 |
47 | let h = formData.hash !== '' ? formData.hash : undefined;
48 |
49 | if (h) {
50 | if (!isValidHash(h)) {
51 | setError("ERROR: Hash string must be a valid 64 character hash, including '0x' at the start");
52 | setErrorState(true);
53 | setSubmitState(false);
54 | return;
55 | }
56 | }
57 |
58 | let n = formData.number !== '' ? Number(formData.number) : undefined;
59 |
60 | if (n) {
61 | if (n < hash.params.start || n > hash.params.editions || !Number.isInteger(n)) {
62 | setError(`ERROR: Edition number must be an integer within ${hash.params.start} and ${hash.params.editions}`);
63 | setErrorState(true);
64 | setSubmitState(false);
65 | return;
66 | }
67 | }
68 |
69 | hashAction({type: H_SET, hash: h, number: n});
70 | closeModal();
71 | }
72 |
73 | return (
74 |
75 | Set an Edition and/or Hash (overrides locks)
76 |
77 |
79 |
80 |
81 |
82 |
83 | {isError ? {error} : null}
84 |
85 |
86 |
89 |
92 |
93 |
94 | );
95 | };
96 |
97 | export default SetHash;
98 |
--------------------------------------------------------------------------------
/src/components/info/features.js:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState} from 'react';
2 | import {List, Header, Loader} from 'semantic-ui-react';
3 |
4 | import {useURL} from '../../hooks/useURL';
5 | import {useHash} from '../../hooks/useHash';
6 | import {F_CLEAR, F_SET, useFeatures} from '../../hooks/useFeatures';
7 |
8 | const Features = () => {
9 | const [url] = useURL();
10 | const [hash] = useHash();
11 | const [features, featuresAction] = useFeatures();
12 |
13 | const [isLoading, setLoading] = useState(false);
14 | const [list, setList] = useState([]);
15 |
16 | useEffect(() => {
17 | window.addEventListener('message', e => {
18 | switch (e.data['command']) {
19 | case 'loadFeatures':
20 | {
21 | featuresAction({type: F_SET, data: e.data['features']});
22 | }
23 | break;
24 | default:
25 | break;
26 | }
27 | });
28 | }, []);
29 |
30 | useEffect(() => {
31 | featuresAction({type: F_CLEAR});
32 | if (!url.isValid) {
33 | setList([]);
34 | return;
35 | }
36 |
37 | setLoading(true);
38 |
39 | let timerGet = setTimeout(() => {
40 | var iframe = window.document.querySelector('iframe').contentWindow;
41 | if (iframe === undefined) return;
42 | iframe.postMessage({command: 'getFeatures'}, '*');
43 | }, 600);
44 |
45 | let timerTimeout = setTimeout(() => {
46 | setLoading(false);
47 | }, 1200);
48 |
49 | return () => {
50 | clearTimeout(timerGet);
51 | clearTimeout(timerTimeout);
52 | };
53 | }, [hash.hash, url.isValid, url.iframeKey, hash.number]);
54 |
55 | useEffect(() => {
56 | setList(
57 | Object.keys(features.data).map(key => {
58 | return (
59 |
60 |
61 | {features.data[key].toString()}
62 | [ {key} ]
63 |
64 |
65 | );
66 | }),
67 | );
68 | }, [features.data]);
69 |
70 | const example = `features = { Feature: "Value of Feature"}`;
71 |
72 | return (
73 |
82 |
83 | {!url.isValid ? (
84 |
85 | {'assign variables (float, int, string) to global "features"'}
86 | {example}
87 |
88 | ) : url.isValid && list.length === 0 ? (
89 | isLoading ? (
90 |
91 | ) : (
92 | no features found
93 | )
94 | ) : (
95 |
96 | {list}
97 |
98 | )}
99 |
100 |
101 | );
102 | };
103 |
104 | export default Features;
105 |
--------------------------------------------------------------------------------
/src/components/copy/instructions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Segment, Header, Icon, Input} from 'semantic-ui-react';
3 |
4 | const Instructions = () => {
5 | const script = '';
6 | const code = 'console.log(hash, number);';
7 | const boilerplate = 'https://github.com/owmo-dev/token-art-tools-boilerplate';
8 | const localURL = 'https://127.0.0.1:8080';
9 |
10 | return (
11 |
23 |
24 |
25 | include the connector.js script in your project
26 |
27 |
28 | {
34 | navigator.clipboard.writeText(script);
35 | },
36 | }}
37 | value={script}
38 | readOnly={true}
39 | />
40 |
41 |
42 |
43 | use the global variables (hash & number) in your script
44 |
45 |
46 | {
52 | navigator.clipboard.writeText(code);
53 | },
54 | }}
55 | value={code}
56 | readOnly={true}
57 | />
58 |
59 |
60 |
61 | host your script via http(s) server, custom boilerplate setup is available:
62 |
63 |
64 | {
70 | window.open(boilerplate);
71 | },
72 | }}
73 | value={boilerplate}
74 | readOnly={true}
75 | />
76 |
77 |
78 |
79 | enter the URL for your hosted work above
80 |
81 |
82 | {
88 | navigator.clipboard.writeText(localURL);
89 | },
90 | }}
91 | value={localURL}
92 | readOnly={true}
93 | />
94 |
95 |
103 |
104 | );
105 | };
106 |
107 | export default Instructions;
108 |
--------------------------------------------------------------------------------
/src/hooks/useHash.js:
--------------------------------------------------------------------------------
1 | import React, {createContext, useContext, useReducer, useMemo} from 'react';
2 | import {iota} from '../helpers/iota';
3 |
4 | const HashContext = createContext();
5 |
6 | export const {H_RANDOM, H_SET, H_BACK, H_CLEAR, H_SET_VALUE, H_LOCK, H_LOCK_NUM} = iota();
7 |
8 | const actions = {};
9 |
10 | function init() {
11 | let values = new Array(32).fill(0);
12 | let locked = new Array(32).fill(false);
13 | return {
14 | number: 0,
15 | hash: convertValuesToHash(values),
16 | values: values,
17 | locked: locked,
18 | numberLocked: false,
19 | history: [],
20 | params: {
21 | min: 0,
22 | max: 255,
23 | step: 1,
24 | count: 32,
25 | start: 0,
26 | editions: 1000,
27 | },
28 | };
29 | }
30 |
31 | function hashReducer(state, dispatch) {
32 | switch (dispatch.type) {
33 | case H_RANDOM: {
34 | let data = generateRandomValues(state);
35 | data['history'] = [...state.history, {hash: state.hash, number: state.number}];
36 | return {...state, ...data};
37 | }
38 | case H_SET: {
39 | let hash = dispatch?.hash ?? state.hash;
40 | let number = dispatch?.number ?? state.number;
41 | let values = convertHashToValues(hash);
42 | let history = [...state.history, {hash: state.hash, number: state.number}];
43 | let data = {hash: hash, number: number, values: values, history: history};
44 | return {...state, ...data};
45 | }
46 | case H_BACK: {
47 | let last = state.history[state.history.length - 1];
48 | let hash = last.hash;
49 | let number = last.number;
50 | let history = state.history;
51 | history.pop();
52 | let values = convertHashToValues(hash);
53 | let data = {hash: hash, number: number, values: values, history: history};
54 | return {...state, ...data};
55 | }
56 | case H_CLEAR: {
57 | return {...state, ...init()};
58 | }
59 | case H_SET_VALUE: {
60 | let values = state.values;
61 | values[dispatch.index] = dispatch.value;
62 | let data = {
63 | hash: convertValuesToHash(values),
64 | values: values,
65 | };
66 | data['history'] = [...state.history, {hash: state.hash, number: state.number}];
67 | return {...state, ...data};
68 | }
69 | case H_LOCK: {
70 | let locked = state.locked;
71 | locked[dispatch.index] = !locked[dispatch.index];
72 | return {...state, ...{locked: locked}};
73 | }
74 | case H_LOCK_NUM: {
75 | let locked = !state.numberLocked;
76 | return {...state, numberLocked: locked};
77 | }
78 | default:
79 | throw new Error(`hashReducer type '${dispatch.type}' not supported`);
80 | }
81 | }
82 |
83 | function generateRandomValues(state) {
84 | let values = state.values;
85 | for (let i = 0; i < 32; i++) {
86 | if (!state.locked[i]) values[i] = Math.floor(Math.random() * 255);
87 | }
88 | let number = !state.numberLocked ? Math.floor(state.params.start + Math.random() * state.params.editions) : state.number;
89 | return {
90 | hash: convertValuesToHash(values),
91 | values: values,
92 | number: number,
93 | };
94 | }
95 |
96 | function convertHashToValues(hash) {
97 | let values = [];
98 | hash = hash.substring(2);
99 | for (let i = 0; i < hash.length; i += 2) {
100 | values.push(parseInt('0x' + hash[i] + hash[i + 1]));
101 | }
102 | return values;
103 | }
104 |
105 | function convertValuesToHash(values) {
106 | return '0x' + values.map(x => Number(x).toString(16).padStart(2, '0')).join('');
107 | }
108 |
109 | function HashProvider(props) {
110 | const [state, dispatch] = useReducer(hashReducer, null, init);
111 | const value = useMemo(() => [state, dispatch], [state]);
112 | return ;
113 | }
114 |
115 | function useHash() {
116 | const context = useContext(HashContext);
117 | if (!context) {
118 | throw new Error(`useHash must be used within the HashProvider`);
119 | }
120 | return context;
121 | }
122 |
123 | export {useHash, HashProvider, actions};
124 |
--------------------------------------------------------------------------------
/src/components/modals/run-automation.js:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState} from 'react';
2 | import {Button, Modal, Progress, Icon} from 'semantic-ui-react';
3 |
4 | import {H_RANDOM, useHash} from '../../hooks/useHash';
5 | import {A_EXPORT, A_RESET, A_STOP, A_TICK, useAutomation} from '../../hooks/useAutomation';
6 | import {useFeatures} from '../../hooks/useFeatures';
7 |
8 | import {screenshot} from '../../helpers/screenshot';
9 | import {exportCSV} from '../../helpers/csv';
10 |
11 | const RunAutomation = () => {
12 | const [hash, hashAction] = useHash();
13 | const [automation, automationAction] = useAutomation();
14 | const [features] = useFeatures();
15 |
16 | const [featuresList, setFeaturesList] = useState([]);
17 |
18 | const msg_cap = 'Generating & Capturing Images';
19 | const msg_exp = 'Exporting CSV Features List';
20 |
21 | const [isSubmitting, setSubmitState] = useState(false);
22 | const [message, setMessage] = useState(msg_cap);
23 |
24 | const [runner, setRunner] = useState(null);
25 |
26 | useEffect(() => {
27 | if (automation.status === 'active' && runner === null) {
28 | setFeaturesList([]);
29 | hashAction({type: H_RANDOM});
30 | setRunner(
31 | setInterval(() => {
32 | automationAction({
33 | type: A_TICK,
34 | });
35 | }, automation.waitTime),
36 | );
37 | }
38 | }, [automation.status]);
39 |
40 | useEffect(() => {
41 | if (automation.status === 'idle' && isSubmitting === true) {
42 | setSubmitState(false);
43 | }
44 | }, [automation.status]);
45 |
46 | useEffect(() => {
47 | if (automation.status === 'active') {
48 | if (automation.tick > 0 && automation.tick <= automation.total) {
49 | if (message !== msg_cap) setMessage(msg_cap);
50 |
51 | if (automation.doScreenshot) {
52 | screenshot(hash.hash);
53 | }
54 |
55 | if (automation.doCSVExport) {
56 | setTimeout(() => {
57 | let f = features.data;
58 | if (f !== undefined) {
59 | f['hash'] = hash.hash;
60 | f['edition'] = hash.number;
61 | setFeaturesList(prev => [...prev, f]);
62 | }
63 | }, 900);
64 | }
65 | }
66 |
67 | if (automation.tick === automation.total) {
68 | automationAction({type: A_STOP});
69 | } else {
70 | hashAction({type: H_RANDOM});
71 | }
72 | }
73 | }, [automation.status, automation.tick]);
74 |
75 | useEffect(() => {
76 | if (automation.status === 'stopping') {
77 | clearInterval(runner);
78 | setRunner(null);
79 |
80 | if (automation.doCSVExport) {
81 | if (message !== msg_exp) setMessage(msg_exp);
82 | setTimeout(() => {
83 | automationAction({type: A_EXPORT});
84 | }, automation.waitTime);
85 | } else {
86 | setTimeout(() => {
87 | automationAction({type: A_RESET});
88 | }, 500);
89 | }
90 | }
91 | }, [automation.status]);
92 |
93 | useEffect(() => {
94 | if (automation.status === 'exporting') {
95 | exportCSV(featuresList);
96 | setTimeout(() => {
97 | automationAction({type: A_RESET});
98 | }, 500);
99 | }
100 | });
101 |
102 | return (
103 |
104 | Running Automation
105 |
106 |
109 |
110 |
111 |
123 |
124 |
125 | );
126 | };
127 |
128 | export default RunAutomation;
129 |
--------------------------------------------------------------------------------
/src/components/panels/controls.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useCallback} from 'react';
2 | import {Segment, Grid, Button, Icon, Divider} from 'semantic-ui-react';
3 |
4 | import {useURL} from '../../hooks/useURL';
5 | import {H_BACK, H_CLEAR, H_RANDOM, useHash} from '../../hooks/useHash';
6 | import {useAutomation} from '../../hooks/useAutomation';
7 |
8 | import Title from '../copy/title';
9 | import RangeSlider from '../inputs/range-slider';
10 | import SetHash from '../modals/set-hash';
11 | import InitAutomation from '../modals/init-automation';
12 | import RunAutomation from '../modals/run-automation';
13 |
14 | import {TYPE_HASH, TYPE_NUMBER} from '../inputs/range-slider';
15 |
16 | const Controls = () => {
17 | const [url] = useURL();
18 | const [hash, hashAction] = useHash();
19 | const [automation] = useAutomation();
20 |
21 | function createHashSliders({count, min, max, step}) {
22 | let sliders = [];
23 | for (let i = 0; i < count; i++) {
24 | sliders.push();
25 | }
26 | return sliders;
27 | }
28 |
29 | const hashSliders = useCallback(createHashSliders({...hash.params}), [hash.params]);
30 |
31 | function createNumberSlider({start, editions}) {
32 | return ;
33 | }
34 |
35 | const numberSlider = useCallback(createNumberSlider({...hash.params}), [hash.params]);
36 |
37 | const [isSetHashModalOpen, setSetHashModalState] = useState(false);
38 |
39 | function openSetHashModal() {
40 | setSetHashModalState(true);
41 | }
42 |
43 | function closeSetHashModal() {
44 | setSetHashModalState(false);
45 | }
46 |
47 | const [isInitAutoModalOpen, setInitAutoModalState] = useState(false);
48 |
49 | function openInitAutoModal() {
50 | setInitAutoModalState(true);
51 | }
52 |
53 | function closeInitAutoModal() {
54 | setInitAutoModalState(false);
55 | }
56 |
57 | return (
58 | <>
59 |
60 |
61 |
62 |
63 |
71 |
72 |
73 |
74 |
85 |
96 |
105 |
116 |
127 |
128 |
129 |
139 | {numberSlider}
140 |
141 | {hashSliders}
142 |
143 | {
156 | navigator.clipboard.writeText(hash.hash);
157 | }}
158 | >
159 | {hash.hash}
160 |
161 |
162 | >
163 | );
164 | };
165 |
166 | export default Controls;
167 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # token-art-tools
2 |
3 | Static webapp for generative artists to explore a script's creative domain via sliders mapped to hashpairs, automate image generation for a sample set, and capture features as a CSV for analyzing probability of outcomes. Developed in React using Gatsby and Semantic UI libraries.
4 |
5 | https://owmo-dev.github.io/token-art-tools/
6 |
7 | 
8 |
9 | # Project Configuration
10 |
11 | This webapp expects a `localhost` web server hosting your script and that you have referenced the `lib/connector.js` script before executing your sketch. Global variables `hash` and `number` are available for your sketch to use, as well as some platform sepcific implementations.
12 |
13 | ## Boilerplate Setup
14 |
15 | The following boilerplate project setup supports all of Token Art Tool's features
16 |
17 | https://github.com/owmo-dev/token-art-tools-boilerplate
18 |
19 | ## Manual Setup
20 |
21 | The `lib/connector.js` script must be referenced in your project before your artwork sketch executes. Either copy it into your repo or use this CDN.
22 |
23 | ```html
24 |
25 | ```
26 |
27 | ## Host Locally
28 |
29 | You can run this webapp locally if you want by doing the following:
30 |
31 | 1. `npm install`
32 | 2. `make run-server`
33 | 3. `http://localhost:8000`
34 |
35 | ## Platform Specific Features
36 |
37 | ### [Art Blocks](https://www.artblocks.io)
38 |
39 | The global variable `tokenData` is made available by the `lib/connector.js` script by using the hash directly provided by the webapp. All 32 hashpairs are used in the hash and the edition number simulates project "0" with a possitble edition range of "0 to 1000", smaller than the possible million for practical UI purposes.
40 |
41 | ```js
42 | tokenData = {
43 | hash: '0x0000000000000000000000000000000000000000000000000000000000000000',
44 | tokenId: 1000000,
45 | };
46 | ```
47 |
48 | Please refer to [Art Block's 101 Docs](https://docs.artblocks.io/creator-docs/creator-onboarding/readme/) for more information.
49 |
50 | ### [fx(hash)](https://www.fxhash.xyz)
51 |
52 | The global variable `fxhash` and function `fxrand` are made available by the `lib/connector.js` script by using a slice of the hash provided by the webapp. The script simply overrides the code snippet required by the fx(hash) creator minting process. If you are including this snippet in your project setup (recommended), please ensure that the reference to `lib/connector.js` is made **AFTER** the fx(hash) code snippet to have them properly overriden. Also, please don't forget to remove it when you are ready to mint.
53 |
54 | ```js
55 | fxhash = 'oo89fd946ca9ce6b038b4434c205da26767bf632748f5cf8292';
56 |
57 | console.log('new random number between 0 and 1', fxrand());
58 | ```
59 |
60 | Please refer to the [fxhash publish docs](https://www.fxhash.xyz/doc/artist/guide-publish-generative-token) for more inforamtion.
61 |
62 | ## Technical Requirements
63 |
64 | ### Canvas is Required
65 |
66 | Artwork must be displayed within a `canvas` element for all features to work as expected.
67 |
68 | ### preserveDrawingBuffer: true
69 |
70 | The `preserveDrawingBuffer` must be `true` for screenshots to work.
71 |
72 | ##### ThreeJS
73 |
74 | ```javascript
75 | let renderer = new THREE.WebGLRenderer({preserveDrawingBuffer: true});
76 | ```
77 |
78 | ##### WebGL
79 |
80 | ```javascript
81 | const gl = canvas.getContext('webgl', {preserveDrawingBuffer: true});
82 | ```
83 |
84 | # Tips & Tricks
85 |
86 | ## Use HTTPS
87 |
88 | Some browsers will require that you serve your artwork locally via `https` rather than `http`
89 |
90 | ## Share Hosted URL & Hash
91 |
92 | If your `URL` is publicly accessible, you can share a Token Art Tools initialization by using `url`, `hash` and `number` variables in the URL for the application (click the "shared" button next to the address bar to copy the current to clipboard). Not that a valid `url` is required for anything to be set.
93 |
94 | `https://owmo-dev.github.io/token-art-tools//?url={URL}&hash={HASH}&number={NUMBER}`
95 |
96 | ## Hashpairs for Exploration
97 |
98 | The hashpair sliders are best used early on while exploring ranges and mixes of different creative features.
99 |
100 | ```js
101 | function mpd(n, a1, b1, a2, b2) {
102 | return ((n - a1) / (b1 - a1)) * (b2 - a2) + a2;
103 | }
104 |
105 | let hs = [];
106 |
107 | for (j = 0; j < 32; j++) {
108 | hs.push(hash.slice(2 + j * 2, 4 + j * 2));
109 | }
110 |
111 | let rns = hs.map(x => {
112 | return parseInt(x, 16);
113 | });
114 |
115 | let features = {
116 | hue: mpd(rns[0], 0, 255, 0, 360),
117 | size: mpd(rns[1], 0, 255, 0.5, 1.8),
118 | offset: mpd(rns[2], 0, 255, -2.0, 2.0),
119 | };
120 | ```
121 |
122 | ## Hash to Seed Randomd
123 |
124 | While I have used hashparis directly in projects, I wouldn't recommend it because the hash produced by external services (such as minting on chain) may not produce a sufficient randomization and it's more difficult to control probabilities. The best way to use the `hash` is simply to use it as a seed in a random function you trust.
125 |
126 | Below is an excellent Random function [Piter Pasma](https://twitter.com/piterpasma) made available for everyone to use.
127 |
128 | ```js
129 | let S = Uint32Array.from([0, 0, 0, 0]).map(i => parseInt(hash.substr(i * 8 + 5, 8), 16));
130 |
131 | let R = (a = 1) => {
132 | let t = S[3];
133 | S[3] = S[2];
134 | S[2] = S[1];
135 | let s = (S[1] = S[0]);
136 | t ^= t << 11;
137 | S[0] ^= t ^ (t >>> 8) ^ (s >>> 19);
138 | return (a * S[0]) / 2 ** 32;
139 | };
140 |
141 | console.log('random value between 0 and 1', R());
142 |
143 | let myArray = ['a', 'b', 'c', 'd'];
144 |
145 | console.log('pick from array', myArray[(R() * myArray.length) | 0]);
146 | ```
147 |
148 | ## Longer Delays for Reliable Screenshots & CSV Capture
149 |
150 | The automated process can sometimes produce unreliable results, especially if your artwork is particularly taxing. I simply suggest increasing the wait time between capturing and testing on smaller sample sizes before commiting to a larger set to run overnight.
151 |
152 | ## Define Features as Early as Possible
153 |
154 | The `lib/connector.js` script defines a global `features` variable as an empty object which you can then assign key-value pairs to display in the webapp. You must set the features variables no later than `500ms` because the webapp will attempt to retrieve them at about `600ms`.
155 |
156 | ```js
157 | features = {
158 | Palette: 'Blue Sky',
159 | Style: 'Shadow',
160 | };
161 | features['Size'] = 10;
162 | ```
163 |
164 | You can only assign `int`, `float`, and `string` values as a feature entry.
165 |
166 | # Known Issues
167 |
168 | - When using a simple web server (ex: `python -m http.server 5500`), Chrome will block HTML files within iframes. At the time of writing, Firefox will still allow this, but it's much better to simple use a `node` project setup.
169 |
--------------------------------------------------------------------------------
/src/components/inputs/range-slider.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect, memo, forwardRef} from 'react';
2 | import {Segment, Grid, Button} from 'semantic-ui-react';
3 | import {RangeStepInput} from 'react-range-step-input';
4 |
5 | import {useURL} from '../../hooks/useURL';
6 | import {H_LOCK, H_LOCK_NUM, H_SET, H_SET_VALUE, useHash} from '../../hooks/useHash';
7 | import {useAutomation} from '../../hooks/useAutomation';
8 |
9 | import {clamp} from '../../helpers/math';
10 | import {iota} from '../../helpers/iota';
11 |
12 | export const {TYPE_HASH, TYPE_NUMBER} = iota();
13 |
14 | function withStateSlice(Comp, slice) {
15 | const MemoComp = memo(Comp);
16 | function Wrapper(props, ref) {
17 | const state = useHash();
18 | return ;
19 | }
20 | Wrapper.displayName = `withStateSlice(${Comp.displayName || Comp.name})`;
21 | return memo(forwardRef(Wrapper));
22 | }
23 |
24 | function SliderControl({index, min, max, step, type}) {
25 | const [url] = useURL();
26 | const [hash, hashAction] = useHash();
27 | const [automation] = useAutomation();
28 |
29 | const [value, setValue] = useState(0);
30 | const [locked, setLocked] = useState(false);
31 |
32 | useEffect(() => {
33 | if (type === TYPE_HASH) {
34 | if (hash.values[index] !== value) {
35 | setValue(hash.values[index]);
36 | }
37 | } else if (type === TYPE_NUMBER) {
38 | if (hash.number !== value) {
39 | setValue(hash.number);
40 | }
41 | }
42 | }, [hash]);
43 |
44 | useEffect(() => {
45 | if (type === TYPE_HASH) {
46 | if (value !== hash.values[index]) {
47 | hashAction({type: H_SET_VALUE, index: index, value: value});
48 | }
49 | } else if (type === TYPE_NUMBER) {
50 | if (value !== hash.number) {
51 | hashAction({type: H_SET, number: value});
52 | }
53 | }
54 | }, [value]);
55 |
56 | useEffect(() => {
57 | if (type === TYPE_HASH) {
58 | if (locked !== hash.locked[index]) {
59 | setLocked(hash.locked[index]);
60 | }
61 | } else if (type === TYPE_NUMBER) {
62 | if (locked !== hash.numberLocked) {
63 | setLocked(hash.numberLocked);
64 | }
65 | }
66 | }, [hash.locked[index], hash.numberLocked]);
67 |
68 | const handleChange = e => {
69 | const v = parseInt(e.target.value);
70 | setValue(v);
71 | };
72 |
73 | const stepValue = inc => {
74 | if (type === TYPE_HASH) {
75 | setValue(clamp(value + inc, hash.params.min, hash.params.max));
76 | } else {
77 | setValue(clamp(value + inc, hash.params.start, hash.params.editions));
78 | }
79 | };
80 |
81 | return (
82 |
83 |
84 |
85 |
95 | {index}
96 |
97 |
98 |
99 |
109 |
110 |
111 |
120 |
121 |
122 |
123 |
133 |
134 |
157 | {hash.hash !== undefined
158 | ? type === TYPE_HASH
159 | ? hash.hash[index * 2 + 2] + hash.hash[index * 2 + 3]
160 | : type === TYPE_NUMBER
161 | ? hash.number
162 | : '-'
163 | : 'ER'}
164 |
165 |
166 |
167 |
178 |
179 |
180 | );
181 | }
182 | const RangeSlider = withStateSlice(SliderControl, (state, {index}) => state.values[index]);
183 |
184 | export default RangeSlider;
185 |
--------------------------------------------------------------------------------
/src/components/panels/viewer.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from 'react';
2 | import {Input, Button, Icon, Dropdown, Segment} from 'semantic-ui-react';
3 |
4 | import {H_CLEAR, H_SET, useHash} from '../../hooks/useHash';
5 | import {useURL, U_CLEAR, U_REFRESH, U_SET} from '../../hooks/useURL';
6 | import {F_CLEAR, useFeatures} from '../../hooks/useFeatures';
7 | import {useAutomation} from '../../hooks/useAutomation';
8 |
9 | import SetResolution from '../modals/set-resolution';
10 | import Instructions from '../copy/instructions';
11 | import Features from '../info/features';
12 |
13 | import {screenshot} from '../../helpers/screenshot';
14 | import {isValidHash} from '../../helpers/hash';
15 |
16 | const Viewer = () => {
17 | const [hash, hashAction] = useHash();
18 | const [url, urlAction] = useURL();
19 | const [, featuresAction] = useFeatures();
20 | const [automation] = useAutomation();
21 |
22 | const [resolutionValue, setResolutionValue] = useState('fill');
23 | const [iframeResolution, setIFrameResolution] = useState({x: '100%', y: '100%'});
24 |
25 | useEffect(() => {
26 | const params = new URLSearchParams(window.location.search);
27 |
28 | const ext_url = params.get('url');
29 | if (ext_url !== null && ext_url !== '') {
30 | urlAction({type: U_SET, url: ext_url});
31 |
32 | const hashData = {};
33 |
34 | const set_hash = params.get('hash');
35 | if (set_hash !== null) {
36 | if (isValidHash(set_hash)) {
37 | hashData['hash'] = set_hash;
38 | }
39 | }
40 |
41 | const set_number = params.get('number');
42 | if (set_number !== null) {
43 | const num = Number(set_number);
44 | if (Number.isInteger(num) && num >= hash.params.start && num <= hash.params.editions) {
45 | hashData['number'] = num;
46 | }
47 | }
48 |
49 | if (Object.keys(hashData).length > 0) {
50 | hashAction({type: H_SET, ...hashData});
51 | }
52 | }
53 | }, []);
54 |
55 | function onChange(e) {
56 | urlAction({type: U_SET, url: e.target.value});
57 | }
58 |
59 | function handleClearURL() {
60 | urlAction({type: U_CLEAR});
61 | hashAction({type: H_CLEAR});
62 | featuresAction({type: F_CLEAR});
63 | setResolutionValue('fill');
64 | }
65 |
66 | const resolutionOptions = [
67 | {
68 | key: 'fill',
69 | text: 'Fill Available',
70 | value: 'fill',
71 | },
72 | {
73 | key: 'detailed',
74 | text: 'Detailed',
75 | value: 'detailed',
76 | },
77 | {
78 | key: 'preview',
79 | text: 'Preview',
80 | value: 'preview',
81 | },
82 | {
83 | key: 'thumb',
84 | text: 'Thumbnail',
85 | value: 'thumb',
86 | },
87 | {
88 | key: 'custom',
89 | text: 'Custom Resolution',
90 | value: 'custom',
91 | },
92 | ];
93 |
94 | function handleChange(e, d) {
95 | if (d.value === 'custom') {
96 | openResolutionModal();
97 | } else {
98 | setResolutionValue(d.value);
99 | urlAction({type: U_REFRESH});
100 | }
101 | }
102 |
103 | useEffect(() => {
104 | switch (resolutionValue) {
105 | case 'custom':
106 | break;
107 | case 'thumb':
108 | setIFrameResolution({x: '258px', y: '258px'});
109 | break;
110 | case 'preview':
111 | setIFrameResolution({x: '514px', y: '514px'});
112 | break;
113 | case 'detailed':
114 | setIFrameResolution({x: '1026px', y: '1026px'});
115 | break;
116 | default:
117 | case 'fill':
118 | setIFrameResolution({x: '100%', y: '100%'});
119 | break;
120 | }
121 | }, [resolutionValue]);
122 |
123 | const [isResolutionModalOpen, setResolutionModalState] = useState(false);
124 |
125 | function openResolutionModal() {
126 | setResolutionModalState(true);
127 | }
128 |
129 | function closeResolutionModal() {
130 | setResolutionModalState(false);
131 | }
132 |
133 | function setRes(x, y) {
134 | setResolutionValue('custom');
135 | setIFrameResolution({
136 | x: x + 2 + 'px',
137 | y: y + 2 + 'px',
138 | });
139 | urlAction({type: U_REFRESH});
140 | }
141 |
142 | return (
143 | <>
144 |
145 |
146 |
156 |
167 |
176 |
187 | {
201 | handleClearURL();
202 | }}
203 | />
204 | ) : null
205 | }
206 | />
207 |
208 |
217 |
240 |
248 | {url.isValid && hash.hash !== undefined ? (
249 |
265 |
276 |
277 | ) : (
278 |
279 |
280 |
281 | )}
282 |
283 |
284 |
285 |
286 |
287 | >
288 | );
289 | };
290 |
291 | export default Viewer;
292 |
--------------------------------------------------------------------------------