├── LICENSE ├── README.md ├── fxhash.min.js ├── index.html ├── index.js └── styles.css /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) by fxhash 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fx(hash) boilerplate 2 | 3 | A boilerplate for the creation of generative art that can be published on fx(hash). 4 | 5 | ## Introduction 6 | 7 | This repository contains the most simple and recommended setup to publish a generative artwork on fxhash. You can do modifications to the existing files, create a zip that contains all of them and upload it on fxhash.xyz. 8 | 9 | This are the hard facts of the required setup: 10 | 11 | - A html entry point called `index.html` 12 | - The `@fxhash/project-sdk` as a local script file included in the html entry point called `./fxhash.min.js` 13 | - A script that generates the generative art included in the html entry point called `index.js` 14 | 15 | Anything else from there is optional (even this README 🙃). The boilerplate contains a .css file but this is theoretically not needed if you don't want to set any css. 16 | For a better developer experience we are offering the `@fxhash/cli` that will help you to create your generative artwork. 17 | 18 | The rest of the README will actually speak about the usage of the `@fxhash/cli`. 19 | 20 | ## Prerequisites 21 | 22 | - `node >= 18.0.0` 23 | - `npm >= 9.0.0` 24 | 25 | That's it you are ready to develop your artwork with the `@fxhash/cli` 26 | 27 | ### Creating a new project 28 | 29 | You probably think: "Why we start with creating a project if I am using the boilerplate?". Thats because you don't need to clone the boilerplate to start a project. You can create a project by using the `@fxhash/cli`. 30 | 31 | ``` 32 | npx fxhash create 33 | ``` 34 | 35 | This command will prompt you with the dialog to create a new project. Give your project a name and choose the "simple" project template. You just created your first project. We will speak about the "ejected" template later. 36 | 37 | > The first time you run npx fxhash npm is actually installing the `@fxhash/cli` package globally on your computer. 38 | 39 | ### Starting the development environment 40 | 41 | The whole fx(lens) environment is exposed via the `@fxhash/cli`. So you just have to run the following command in the root of your project. 42 | 43 | ``` 44 | npx fxhash dev 45 | ``` 46 | 47 | This will open up the fx(lens) environment in your browser. In the backend two servers are running: 48 | 49 | - `http://localhost:3300` serves fx(lens) you can connect to a token 50 | - `http://localhost:3301` serves your project with live reloading 51 | 52 | ### Building your project 53 | 54 | ``` 55 | npx fxhash build 56 | ``` 57 | 58 | Will build your project and create an `upload.zip` that you can use to publish your artwork on fxhash.xyz 59 | 60 | ## Advanced usage: Ejected Project 61 | 62 | When you created your first project with `fxhash create` you saw that there is a second project template you can choose: "ejected" 63 | 64 | If you want to use a package manager to install dependencies for your project or customize how webpack builds your project, the "ejected" template provides all those functionalities. 65 | 66 | The structure of the ejected template will look like this: 67 | 68 | ``` 69 | ├─ package.json 70 | ├─ webpack.dev.config.js 71 | ├─ webpack.prod.config.js 72 | ├─ src/ 73 | ├─ index.html 74 | ├─ index.js 75 | ├─ fxhash.min.js 76 | ├─ LICENSE 77 | ``` 78 | 79 | You can still use all the functionality the `@fxhash/cli` provides, but e.g. customize the webpack configuration for the `fxhash dev`(webpack.dev.config.js) and `fxhash build` (webpack.prod.config.js) commands. 80 | 81 | ### Going from simple to ejected 82 | 83 | Even if you started your project with a simple template you can go all "ejected" by running 84 | 85 | ``` 86 | fxhash eject 87 | ``` 88 | 89 | This will transform your simple project structure into the ejected project structure. But be aware this change is not reversable via the `@fxhash/cli`. 90 | -------------------------------------------------------------------------------- /fxhash.min.js: -------------------------------------------------------------------------------- 1 | "use strict";(()=>{var p="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";function A(){return`tz1${Array.from({length:33},()=>p[Math.random()*p.length|0]).join("")}`}function b(t){if(t.length!==36||!/^(tz|KT)[1-4]/.test(t))return!1;for(let r=0;rp[Math.random()*p.length|0]).join("")}`}function P(t){return/^(0x)?([A-Fa-f0-9]{64})$/.test(t)}function j(t){return[...t].reduce(function(r,e){return r*p.length+p.indexOf(e)|0},0)}function U([t,r,e,n]){return function(){t|=0,r|=0,e|=0,n|=0;let s=(t+r|0)+n|0;return n=n+1|0,t=r^r>>>9,r=e+(e<<3)|0,e=e<<21|e>>>11,e=e+s|0,(s>>>0)/4294967296}}function I(t,r,e=j){return t.slice(r).match(new RegExp(".{"+(t.length-r>>2)+"}","g")).map(e)}function C(t){return P(t)||T(t)?I(t,2,r=>Number(BigInt(`0x${r}`)%BigInt(4294967295))):b(t)?I(t,3):I(t,2)}function l(t){let r=C(t);return U(r)}function H(t){let r=t.replace("#","");return r.length===6&&(r=`${r}ff`),r.length===3&&(r=`${r[0]}${r[0]}${r[1]}${r[1]}${r[2]}${r[2]}ff`),r}var w=function(t){let r="";for(let e=0;e{let r=new DataView(new ArrayBuffer(8));return r.setFloat64(0,t),r.getBigUint64(0).toString(16).padStart(16,"0")},deserialize:t=>{let r=new DataView(new ArrayBuffer(8));for(let e=0;e<8;e++)r.setUint8(e,parseInt(t.substring(e*2,e*2+2),16));return r.getFloat64(0)},bytesLength:()=>8,constrain:(t,r)=>{let e=Number.MIN_SAFE_INTEGER;typeof r.options?.min<"u"&&(e=Number(r.options.min));let n=Number.MAX_SAFE_INTEGER;typeof r.options?.max<"u"&&(n=Number(r.options.max)),n=Math.min(n,Number.MAX_SAFE_INTEGER),e=Math.max(e,Number.MIN_SAFE_INTEGER);let s=Math.min(Math.max(t,e),n);if(r?.options?.step){let a=1/r?.options?.step;return Math.round(s*a)/a}return s},random:t=>{let r=Number.MIN_SAFE_INTEGER;typeof t.options?.min<"u"&&(r=Number(t.options.min));let e=Number.MAX_SAFE_INTEGER;typeof t.options?.max<"u"&&(e=Number(t.options.max)),e=Math.min(e,Number.MAX_SAFE_INTEGER),r=Math.max(r,Number.MIN_SAFE_INTEGER);let n=Math.random()*(e-r)+r;if(t?.options?.step){let s=1/t?.options?.step;return Math.round(n*s)/s}return n}},bigint:{serialize:t=>{let r=new DataView(new ArrayBuffer(8));return r.setBigInt64(0,BigInt(t)),r.getBigUint64(0).toString(16).padStart(16,"0")},deserialize:t=>{let r=new DataView(new ArrayBuffer(8));for(let e=0;e<8;e++)r.setUint8(e,parseInt(t.substring(e*2,e*2+2),16));return r.getBigInt64(0)},bytesLength:()=>8,random:t=>{let r=k,e=R;typeof t.options?.min<"u"&&(r=BigInt(t.options.min)),typeof t.options?.max<"u"&&(e=BigInt(t.options.max));let n=e-r,s=n.toString(2).length,a;do a=BigInt("0b"+Array.from(crypto.getRandomValues(new Uint8Array(Math.ceil(s/8)))).map(u=>u.toString(2).padStart(8,"0")).join(""));while(a>n);return a+r}},boolean:{serialize:t=>typeof t=="boolean"?t?"01":"00":typeof t=="string"&&t==="true"?"01":"00",deserialize:t=>t!=="00",bytesLength:()=>1,random:()=>Math.random()<.5},color:{serialize:t=>H(t),deserialize:t=>t,bytesLength:()=>4,transform:t=>{let r=H(t),e=parseInt(r.slice(0,2),16),n=parseInt(r.slice(2,4),16),s=parseInt(r.slice(4,6),16),a=parseInt(r.slice(6,8),16);return{hex:{rgb:"#"+t.slice(0,6),rgba:"#"+t},obj:{rgb:{r:e,g:n,b:s},rgba:{r:e,g:n,b:s,a}},arr:{rgb:[e,n,s],rgba:[e,n,s,a]}}},constrain:t=>t.replace("#","").slice(0,8).padEnd(8,"f"),random:()=>`${[...Array(8)].map(()=>Math.floor(Math.random()*16).toString(16)).join("")}`},string:{serialize:(t,r)=>{if(!r.version){let s=w(t.substring(0,64));return s=s.padEnd(64*4,"0"),s}let e=64;typeof r.options?.maxLength<"u"&&(e=Number(r.options.maxLength));let n=w(t.substring(0,e));return n=n.padEnd(e*4,"0"),n},deserialize:t=>O(t),bytesLength:t=>t.version&&typeof t.options?.maxLength<"u"?Number(t.options.maxLength)*2:64*2,random:t=>{let r=0;typeof t.options?.minLength<"u"&&(r=t.options.minLength);let e=64;typeof t.options?.maxLength<"u"&&(e=t.options.maxLength);let n=Math.round(Math.random()*(e-r)+r);return[...Array(n)].map(s=>(~~(Math.random()*36)).toString(36)).join("")},constrain:(t,r)=>{let e=0;typeof r.options?.minLength<"u"&&(e=r.options.minLength);let n=64;typeof r.options?.maxLength<"u"&&(n=r.options.maxLength);let s=t.slice(0,n);return s.lengthArray.from(t).map(e=>e.toString(16).padStart(2,"0")).join(""),deserialize:(t,r)=>{let e=t.length/2,n=new Uint8Array(e),s;for(let a=0;at.options.length,random:t=>{let r=t.options?.length||0,e=new Uint8Array(r);for(let n=0;nMath.min(255,r.options?.options?.indexOf(t)||0).toString(16).padStart(2,"0"),deserialize:(t,r)=>{let e=parseInt(t,16);return r.options?.options?.[e]||r.options?.options?.[0]||""},bytesLength:()=>1,constrain:(t,r)=>r.options.options.includes(t)?t:r.options.options[0],random:t=>{let r=Math.round(Math.random()*(t.options.options.length-1)+0);return t?.options?.options[r]}}};function M(t,r){let e="";if(!r)return e;for(let n of r){let{id:s,type:a}=n,u=h[a],m=t[s],g=typeof m<"u"?m:typeof n.default<"u"?n.default:u.random(n),d=u.serialize(g,n);e+=d}return e}function F(t,r,e){let n={};for(let s of r){let a=h[s.type],u=e.withTransform&&a[e.transformType||"transform"];if(!t){let x;typeof s.default>"u"?x=a.random(s):x=s.default,n[s.id]=u?u(x,s):x;continue}let m=a.bytesLength(s),g=t.substring(0,m*2);t=t.substring(m*2);let d=a.deserialize(g,s);n[s.id]=u?u(d,s):d}return n}var z=(t,r,e,n)=>{let s=e.find(m=>m.id===t),u=h[s.type][n];return u?.(r,s)||r},E=(t,r,e)=>{let n={};for(let s of r){let a=h[s.type],u=t[s.id],m=a[e];n[s.id]=m?.(u,s)||u}return n};function V(t,r){let{parent:e}=t,n=new URLSearchParams(t.location.search),s=n.get("fxhash")||v(),a=l(s),u=n.get("fxminter")||A(),m=l(u),g=n.get("preview")==="1";function d(){t.dispatchEvent(new Event("fxhash-preview")),setTimeout(()=>d(),500)}let $=t.location.hash?.replace("#0x",""),_={_version:"4.0.1",_processors:h,_params:void 0,_features:void 0,_paramValues:{},_listeners:{},_receiveUpdateParams:async function(o,i){let c=await this.propagateEvent("params:update",o);c.forEach(([f,y])=>{typeof f=="boolean"&&!f||(this._updateParams(o),i?.()),y?.(f,o)}),c.length===0&&(this._updateParams(o),i?.())},_updateParams:function(o){let i=E({...this._rawValues,...o},this._params,"constrain");Object.keys(i).forEach(c=>{this._rawValues[c]=i[c]}),this._paramValues=E(this._rawValues,this._params,"transform"),this._updateInputBytes()},_updateInputBytes:function(){let o=M(this._rawValues,this._params);this.inputBytes=o},_emitParams:function(o){let i=Object.keys(o).reduce((c,f)=>(c[f]=z(f,o[f],this._params,"constrain"),c),{});this._receiveUpdateParams(i,()=>{e.postMessage({id:"fxhash_emit:params:update",data:{params:i}},"*")})},hash:s,rand:a,minter:u,randminter:m,iteration:Number(n.get("fxiteration"))||1,context:n.get("fxcontext")||"standalone",preview:d,isPreview:g,params:function(o){this._params=o.map(i=>({...i,version:this._version})),this._rawValues=F($,this._params,{withTransform:!0,transformType:"constrain"}),this._paramValues=E(this._rawValues,this._params,"transform"),this._updateInputBytes()},features:function(o){this._features=o},getFeature:function(o){return this._features[o]},getFeatures:function(){return this._features},getParam:function(o){return this._paramValues[o]},getParams:function(){return this._paramValues},getRawParam:function(o){return this._rawValues[o]},getRawParams:function(){return this._rawValues},getRandomParam:function(o){let i=this._params.find(f=>f.id===o);return h[i.type].random(i)},getDefinitions:function(){return this._params},stringifyParams:function(o){return JSON.stringify(o||this._rawValues,(i,c)=>typeof c=="bigint"?c.toString():c,2)},on:function(o,i,c){return this._listeners[o]||(this._listeners[o]=[]),this._listeners[o].push([i,c]),()=>{let f=this._listeners[o].findIndex(([y])=>y===i);f>-1&&this._listeners[o].splice(f,1)}},propagateEvent:async function(o,i){let c=[];if(this._listeners?.[o])for(let[f,y]of this._listeners[o]){let S=f(i);c.push([S instanceof Promise?await S:S,y])}return c},emit:function(o,i){switch(o){case"params:update":this._emitParams(i);break;default:console.log("$fx.emit called with unknown id:",o);break}}},B=()=>{a=l(s),_.rand=a,a.reset=B};a.reset=B;let N=()=>{m=l(u),_.randminter=m,m.reset=N};return m.reset=N,t.addEventListener("message",o=>{if(o.data==="fxhash_getInfo"&&e.postMessage({id:"fxhash_getInfo",data:{version:t.$fx._version,hash:t.$fx.hash,iteration:t.$fx.iteration,features:t.$fx.getFeatures(),params:{definitions:t.$fx.getDefinitions(),values:t.$fx.getRawParams()},minter:t.$fx.minter}},"*"),o.data?.id==="fxhash_params:update"){let{params:i}=o.data.data;i&&t.$fx._receiveUpdateParams(i)}}),_}window.$fx=V(window,{});})(); 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | simple 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // demonstrate seed reset 2 | // for (let i = 0; i < 10; i++) { 3 | // console.log(i, $fx.rand(), $fx.randminter()) 4 | // $fx.rand.reset(); 5 | // $fx.randminter.reset(); 6 | // } 7 | 8 | const sp = new URLSearchParams(window.location.search) 9 | // console.log(sp); 10 | 11 | // this is how to define parameters 12 | $fx.params([ 13 | { 14 | id: "number_id", 15 | name: "A number/float64", 16 | type: "number", 17 | //default: Math.PI, 18 | options: { 19 | min: 1, 20 | max: 10, 21 | step: 0.0001, 22 | }, 23 | }, 24 | 25 | { 26 | id: "bigint_id", 27 | name: "A bigint", 28 | type: "bigint", 29 | update: "code-driven", 30 | //default: BigInt(Number.MAX_SAFE_INTEGER * 2), 31 | options: { 32 | min: Number.MIN_SAFE_INTEGER * 4, 33 | max: Number.MAX_SAFE_INTEGER * 4, 34 | step: 1, 35 | }, 36 | }, 37 | { 38 | id: "string_id_long", 39 | name: "A string long", 40 | type: "string", 41 | update: "code-driven", 42 | //default: "hello", 43 | options: { 44 | minLength: 1, 45 | maxLength: 512, 46 | }, 47 | }, 48 | { 49 | id: "select_id", 50 | name: "A selection", 51 | type: "select", 52 | update: "code-driven", 53 | //default: "pear", 54 | options: { 55 | options: ["apple", "orange", "pear"], 56 | }, 57 | }, 58 | { 59 | id: "color_id", 60 | name: "A color", 61 | type: "color", 62 | update: "code-driven", 63 | //default: "ff0000", 64 | }, 65 | { 66 | id: "boolean_id", 67 | name: "A boolean", 68 | type: "boolean", 69 | update: "code-driven", 70 | //default: true, 71 | }, 72 | { 73 | id: "string_id", 74 | name: "A string", 75 | type: "string", 76 | update: "code-driven", 77 | //default: "hello", 78 | options: { 79 | minLength: 1, 80 | maxLength: 512, 81 | }, 82 | }, 83 | ]) 84 | 85 | // this is how features can be defined 86 | $fx.features({ 87 | "A random feature": Math.floor($fx.rand() * 10), 88 | "A random boolean": $fx.rand() > 0.5, 89 | "A random string": ["A", "B", "C", "D"].at(Math.floor($fx.rand() * 4)), 90 | "Feature from params, its a number": $fx.getParam("number_id"), 91 | }) 92 | 93 | function main() { 94 | // log the parameters, for debugging purposes, artists won't have to do that 95 | // console.log("Current param values:"); 96 | // // Raw deserialize param values 97 | // console.log($fx.getRawParams()); 98 | // // Added addtional transformation to the parameter for easier usage 99 | // // e.g. color.hex.rgba, color.obj.rgba.r, color.arr.rgb[0] 100 | // console.log($fx.getParams()); 101 | 102 | // // how to read a single raw parameter 103 | // console.log("Single raw value:"); 104 | // console.log($fx.getRawParam("color_id")); 105 | // // how to read a single transformed parameter 106 | // console.log("Single transformed value:"); 107 | // console.log($fx.getParam("color_id")); 108 | 109 | const getContrastTextColor = backgroundColor => 110 | ((parseInt(backgroundColor, 16) >> 16) & 0xff) > 0xaa 111 | ? "#000000" 112 | : "#ffffff" 113 | 114 | const bgcolor = $fx.getParam("color_id").hex.rgba 115 | const textcolor = getContrastTextColor(bgcolor.replace("#", "")) 116 | 117 | // update the document based on the parameters 118 | document.body.style.background = bgcolor 119 | document.body.innerHTML = ` 120 |
121 |

122 | hash: ${$fx.hash} 123 |

124 |

125 | minter: ${$fx.minter} 126 |

127 |

128 | iteration: ${$fx.iteration} 129 |

130 |

131 | inputBytes: ${$fx.inputBytes} 132 |

133 |

134 | context: ${$fx.context} 135 |

136 |

137 | params: 138 |

139 |
140 |     ${$fx.stringifyParams($fx.getRawParams())}
141 |     
142 |
143 | ` 144 | const btn = document.createElement("button") 145 | btn.textContent = "emit random params" 146 | btn.addEventListener("click", () => { 147 | $fx.emit("params:update", { 148 | number_id: $fx.getRandomParam("number_id"), 149 | bigint_id: $fx.getRandomParam("bigint_id"), 150 | string_id_long: $fx.getRandomParam("string_id_long"), 151 | select_id: $fx.getRandomParam("select_id"), 152 | color_id: $fx.getRandomParam("color_id"), 153 | boolean_id: $fx.getRandomParam("boolean_id"), 154 | string_id: $fx.getRandomParam("string_id"), 155 | }) 156 | main() 157 | }) 158 | document.body.appendChild(btn) 159 | } 160 | 161 | main() 162 | 163 | $fx.on( 164 | "params:update", 165 | newRawValues => { 166 | // opt-out default behaviour 167 | if (newRawValues.number_id === 5) return false 168 | // opt-in default behaviour 169 | return true 170 | }, 171 | (optInDefault, newValues) => main() 172 | ) 173 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fxhash/fxhash-boilerplate/48ce0520c7521cfeb08183e58617347da4aed7c4/styles.css --------------------------------------------------------------------------------