create-textmode 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +12 -6
  2. package/package.json +1 -1
  3. package/src/constants.js +4 -0
  4. package/templates/vanilla-js/eslint.config.js +22 -0
  5. package/templates/vanilla-js/index.html +0 -1
  6. package/templates/vanilla-js/package.json +5 -3
  7. package/templates/vanilla-js-fxhash/_gitignore +2 -0
  8. package/templates/vanilla-js-fxhash/eslint.config.js +22 -0
  9. package/templates/vanilla-js-fxhash/index.html +27 -0
  10. package/templates/vanilla-js-fxhash/package.json +26 -0
  11. package/templates/vanilla-js-fxhash/public/fxhash.min.js +1 -0
  12. package/templates/vanilla-js-fxhash/src/sketch.js +101 -0
  13. package/templates/vanilla-js-fxhash/vite.config.js +7 -0
  14. package/templates/vanilla-js-tweakpane/_gitignore +2 -0
  15. package/templates/vanilla-js-tweakpane/eslint.config.js +22 -0
  16. package/templates/vanilla-js-tweakpane/index.html +37 -0
  17. package/templates/vanilla-js-tweakpane/package.json +27 -0
  18. package/templates/vanilla-js-tweakpane/src/sketch.js +62 -0
  19. package/templates/vanilla-js-tweakpane/vite.config.js +7 -0
  20. package/templates/vanilla-ts/eslint.config.js +23 -0
  21. package/templates/vanilla-ts/index.html +0 -1
  22. package/templates/vanilla-ts/package.json +7 -6
  23. package/templates/vanilla-ts-fxhash/_gitignore +2 -0
  24. package/templates/vanilla-ts-fxhash/eslint.config.js +23 -0
  25. package/templates/vanilla-ts-fxhash/index.html +27 -0
  26. package/templates/vanilla-ts-fxhash/package.json +30 -0
  27. package/templates/vanilla-ts-fxhash/public/fxhash.min.js +1 -0
  28. package/templates/vanilla-ts-fxhash/src/fxhash.d.ts +54 -0
  29. package/templates/vanilla-ts-fxhash/src/sketch.ts +108 -0
  30. package/templates/vanilla-ts-fxhash/tsconfig.json +15 -0
  31. package/templates/vanilla-ts-fxhash/vite.config.ts +7 -0
  32. package/templates/vanilla-ts-tweakpane/_gitignore +2 -0
  33. package/templates/vanilla-ts-tweakpane/eslint.config.js +23 -0
  34. package/templates/vanilla-ts-tweakpane/index.html +37 -0
  35. package/templates/vanilla-ts-tweakpane/package.json +32 -0
  36. package/templates/vanilla-ts-tweakpane/src/sketch.ts +72 -0
  37. package/templates/vanilla-ts-tweakpane/tsconfig.json +15 -0
  38. package/templates/vanilla-ts-tweakpane/vite.config.ts +7 -0
  39. package/templates/vanilla-js/.eslintrc.json +0 -13
  40. package/templates/vanilla-ts/.eslintrc.json +0 -19
package/README.md CHANGED
@@ -58,12 +58,18 @@ pnpm create textmode@latest demo -- --template vanilla-js --pm pnpm --no-install
58
58
 
59
59
  ## Templates
60
60
 
61
- | Template | Stack | Tooling |
62
- |:---------|:------|:--------|
63
- | `vanilla-js` | Vite + JavaScript + `textmode.js` | ESLint *(JS rules)*, Prettier, `.gitignore` |
64
- | `vanilla-ts` | Vite + TypeScript + `textmode.js` | `tsconfig`, ESLint *(@typescript-eslint)*, Prettier, `.gitignore`, `typecheck` |
65
-
66
- Adding new templates? Follow the same table format so details stay consistent and easy to scan.
61
+ All templates ship with Vite, `textmode.js`, Prettier, ESLint, and a `.gitignore`. TypeScript variants add `tsconfig` and a `typecheck` script. Below are the extras that differ by template:
62
+
63
+ | Template | Flavor | Extras |
64
+ |:---------|:-------|:-------|
65
+ | `vanilla-js` | JavaScript | Base starter sketch |
66
+ | `vanilla-ts` | TypeScript | Base starter sketch + TS config |
67
+ | `vanilla-js-fxhash` | JavaScript + fx(hash) | Bundled `fxhash.min.js`, starter sketch using `$fx` API and features |
68
+ | `vanilla-ts-fxhash` | TypeScript + fx(hash) | Bundled `fxhash.min.js`, `$fx` typings, starter sketch using `$fx` API and features |
69
+ | `vanilla-js-tweakpane` | JavaScript + Tweakpane | UI controls wired with Tweakpane |
70
+ | `vanilla-ts-tweakpane` | TypeScript + Tweakpane | UI controls wired with Tweakpane + TS config |
71
+
72
+ Adding new templates? Keep the shared tooling note above and only list what’s unique in the table.
67
73
 
68
74
  ## Local development
69
75
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-textmode",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Scaffold textmode.js projects with zero friction",
5
5
  "type": "module",
6
6
  "bin": {
package/src/constants.js CHANGED
@@ -10,6 +10,10 @@ export const HEADER = `
10
10
  export const templates = [
11
11
  { name: 'vanilla-js', label: 'Vanilla JavaScript (vite)', dir: 'vanilla-js' },
12
12
  { name: 'vanilla-ts', label: 'Vanilla TypeScript (vite)', dir: 'vanilla-ts' },
13
+ { name: 'vanilla-js-fxhash', label: 'Vanilla JavaScript + fx(hash) (vite)', dir: 'vanilla-js-fxhash' },
14
+ { name: 'vanilla-ts-fxhash', label: 'Vanilla TypeScript + fx(hash) (vite)', dir: 'vanilla-ts-fxhash' },
15
+ { name: 'vanilla-js-tweakpane', label: 'Vanilla JavaScript + Tweakpane (vite)', dir: 'vanilla-js-tweakpane' },
16
+ { name: 'vanilla-ts-tweakpane', label: 'Vanilla TypeScript + Tweakpane (vite)', dir: 'vanilla-ts-tweakpane' }
13
17
  ];
14
18
 
15
19
  // File name patterns considered text for placeholder replacement.
@@ -0,0 +1,22 @@
1
+ import { defineConfig } from "eslint/config";
2
+ import js from "@eslint/js";
3
+ import globals from "globals";
4
+ import prettierConfig from "eslint-config-prettier";
5
+
6
+ export default defineConfig([
7
+ {
8
+ ignores: ["dist/**", "node_modules/**"],
9
+ },
10
+ js.configs.recommended,
11
+ prettierConfig,
12
+ {
13
+ files: ["**/*.js"],
14
+ languageOptions: {
15
+ ecmaVersion: "latest",
16
+ sourceType: "module",
17
+ globals: {
18
+ ...globals.browser,
19
+ },
20
+ },
21
+ },
22
+ ]);
@@ -20,7 +20,6 @@
20
20
  </head>
21
21
 
22
22
  <body>
23
- <div id="app"></div>
24
23
  <script type="module" src="/src/sketch.js"></script>
25
24
  </body>
26
25
 
@@ -16,9 +16,11 @@
16
16
  "textmode.js": "latest"
17
17
  },
18
18
  "devDependencies": {
19
- "prettier": "^3.7.4",
20
- "vite": "^7.2.7",
19
+ "@eslint/js": "^9.39.1",
21
20
  "eslint": "^9.39.1",
22
- "eslint-config-prettier": "^10.1.8"
21
+ "eslint-config-prettier": "^10.1.8",
22
+ "globals": "^15.14.0",
23
+ "prettier": "^3.7.4",
24
+ "vite": "^7.2.7"
23
25
  }
24
26
  }
@@ -0,0 +1,2 @@
1
+ node_modules
2
+ dist
@@ -0,0 +1,22 @@
1
+ import { defineConfig } from "eslint/config";
2
+ import js from "@eslint/js";
3
+ import globals from "globals";
4
+ import prettierConfig from "eslint-config-prettier";
5
+
6
+ export default defineConfig([
7
+ {
8
+ ignores: ["dist/**", "node_modules/**"],
9
+ },
10
+ js.configs.recommended,
11
+ prettierConfig,
12
+ {
13
+ files: ["**/*.js"],
14
+ languageOptions: {
15
+ ecmaVersion: "latest",
16
+ sourceType: "module",
17
+ globals: {
18
+ ...globals.browser,
19
+ },
20
+ },
21
+ },
22
+ ]);
@@ -0,0 +1,27 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>{{name}}</title>
8
+
9
+ <style>
10
+ html,
11
+ body {
12
+ margin: 0;
13
+ padding: 0;
14
+ }
15
+
16
+ canvas {
17
+ display: block;
18
+ }
19
+ </style>
20
+ </head>
21
+
22
+ <body>
23
+ <script src="/fxhash.min.js"></script>
24
+ <script type="module" src="/src/sketch.js"></script>
25
+ </body>
26
+
27
+ </html>
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "format": "prettier --write \"src/**/*.{js,ts}\"",
11
+ "format:check": "prettier --check \"src/**/*.{js,ts}\"",
12
+ "lint": "eslint src --ext .js",
13
+ "lint:fix": "eslint src --ext .js --fix"
14
+ },
15
+ "dependencies": {
16
+ "textmode.js": "latest"
17
+ },
18
+ "devDependencies": {
19
+ "@eslint/js": "^9.39.1",
20
+ "eslint": "^9.39.1",
21
+ "eslint-config-prettier": "^10.1.8",
22
+ "globals": "^15.14.0",
23
+ "prettier": "^3.7.4",
24
+ "vite": "^7.2.7"
25
+ }
26
+ }
@@ -0,0 +1 @@
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;r<t.length;r++)if(!p.includes(t[r]))return!1;return!0}function T(t){return/^(0x)?[0-9a-fA-F]{40}$/.test(t)}function v(){return`oo${Array.from({length:49},()=>p[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<t.length;e++)r+=t.charCodeAt(e).toString(16).padStart(4,"0");return r},O=function(t){let r=t.match(/.{1,4}/g)||[],e="";for(let n=0;n<r.length;n++){let s=parseInt(r[n],16);if(s===0)break;e+=String.fromCharCode(s)}return e},k=BigInt("-9223372036854775808"),R=BigInt("9223372036854775807"),h={number:{serialize:t=>{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.length<e?s.padEnd(e):s}},bytes:{serialize:(t,r)=>Array.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;a<e;a++)s=a*2,n[a]=parseInt(`${t[s]}${t[s+1]}`,16);return n},bytesLength:t=>t.options.length,random:t=>{let r=t.options?.length||0,e=new Uint8Array(r);for(let n=0;n<r;n++)e[n]=Math.random()*255|0;return e}},select:{serialize:(t,r)=>Math.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,{});})();
@@ -0,0 +1,101 @@
1
+ /* global $fx */
2
+ import { textmode } from 'textmode.js';
3
+
4
+ const palettes = [
5
+ {
6
+ name: 'Electric Bloom',
7
+ bg: [6, 6, 12],
8
+ colors: [
9
+ [246, 70, 104],
10
+ [255, 191, 0],
11
+ [89, 247, 255]
12
+ ],
13
+ glyphs: ['▓', '▒', '░']
14
+ },
15
+ {
16
+ name: 'Monotone Void',
17
+ bg: [10, 10, 12],
18
+ colors: [
19
+ [240, 240, 240],
20
+ [180, 180, 180],
21
+ [120, 120, 120]
22
+ ],
23
+ glyphs: ['█', '▓', '░']
24
+ },
25
+ {
26
+ name: 'Emerald Night',
27
+ bg: [4, 10, 8],
28
+ colors: [
29
+ [122, 217, 147],
30
+ [48, 163, 84],
31
+ [10, 102, 85]
32
+ ],
33
+ glyphs: ['▒', '░', '▓']
34
+ }
35
+ ];
36
+
37
+ const palette = pick(palettes);
38
+ const symmetry = $fx.rand() > 0.5 ? 'mirror' : 'radial';
39
+ const density = Math.floor($fx.rand() * 5) + 3; // 3..7 controls spacing
40
+ const jitter = $fx.rand() > 0.5;
41
+
42
+ $fx.features({
43
+ Palette: palette.name,
44
+ Symmetry: symmetry,
45
+ Density: density,
46
+ Jitter: jitter ? 'yes' : 'no'
47
+ });
48
+
49
+ const tm = textmode.create({ width: window.innerWidth, height: window.innerHeight });
50
+ const seed = $fx.rand() * 10000;
51
+ let previewSent = false;
52
+
53
+ function pick(items) {
54
+ const idx = Math.floor($fx.rand() * items.length);
55
+ return items[idx];
56
+ }
57
+
58
+ function waveNoise(x, y, t) {
59
+ const base = Math.sin(x * 0.45 + y * 0.35 + seed + t);
60
+ const ripple = Math.sin(Math.sqrt(x * x + y * y) * 0.35 + t * 0.5 + seed * 0.3);
61
+ return 0.5 + 0.25 * base + 0.25 * ripple;
62
+ }
63
+
64
+ tm.draw(() => {
65
+ tm.background(...palette.bg);
66
+
67
+ const halfCols = Math.floor(tm.grid.cols / 2) + 1;
68
+ const halfRows = Math.floor(tm.grid.rows / 2) + 1;
69
+
70
+ for (let y = -halfRows; y < halfRows; y++) {
71
+ for (let x = -halfCols; x < halfCols; x++) {
72
+ if ((Math.abs(x) + Math.abs(y)) % density !== 0) continue;
73
+
74
+ const sx = symmetry === 'mirror' ? Math.abs(x) : x;
75
+ const t = tm.frameCount * 0.02;
76
+ const wobble = jitter ? waveNoise(x * 0.05, y * 0.05, t * 1.5) - 0.5 : 0;
77
+ const n = waveNoise(sx * 0.35, y * 0.35, t) + wobble;
78
+ const colorIdx = Math.floor(Math.abs(n * palette.colors.length)) % palette.colors.length;
79
+ const glyph = palette.glyphs[colorIdx % palette.glyphs.length];
80
+ const [r, g, b] = palette.colors[colorIdx];
81
+
82
+ tm.push();
83
+ tm.translate(x, y, 0);
84
+ tm.char(glyph);
85
+ tm.charColor(r, g, b);
86
+ tm.point();
87
+ tm.pop();
88
+ }
89
+ }
90
+
91
+ if (!previewSent && tm.frameCount > 12) {
92
+ $fx.preview();
93
+ previewSent = true;
94
+ }
95
+ });
96
+
97
+ tm.windowResized(() => {
98
+ tm.resizeCanvas(window.innerWidth, window.innerHeight);
99
+ });
100
+
101
+ console.log('fxhash:', $fx.hash);
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ export default defineConfig({
4
+ server: {
5
+ open: true
6
+ }
7
+ });
@@ -0,0 +1,2 @@
1
+ node_modules
2
+ dist
@@ -0,0 +1,22 @@
1
+ import { defineConfig } from "eslint/config";
2
+ import js from "@eslint/js";
3
+ import globals from "globals";
4
+ import prettierConfig from "eslint-config-prettier";
5
+
6
+ export default defineConfig([
7
+ {
8
+ ignores: ["dist/**", "node_modules/**"],
9
+ },
10
+ js.configs.recommended,
11
+ prettierConfig,
12
+ {
13
+ files: ["**/*.js"],
14
+ languageOptions: {
15
+ ecmaVersion: "latest",
16
+ sourceType: "module",
17
+ globals: {
18
+ ...globals.browser,
19
+ },
20
+ },
21
+ },
22
+ ]);
@@ -0,0 +1,37 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>{{name}}</title>
8
+
9
+ <style>
10
+ html,
11
+ body {
12
+ margin: 0;
13
+ padding: 0;
14
+ }
15
+
16
+ canvas {
17
+ display: block;
18
+ }
19
+
20
+ #pane {
21
+ position: fixed;
22
+ top: 12px;
23
+ right: 12px;
24
+ width: 260px;
25
+ z-index: 10;
26
+ pointer-events: auto;
27
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
28
+ }
29
+ </style>
30
+ </head>
31
+
32
+ <body>
33
+ <div id="pane"></div>
34
+ <script type="module" src="/src/sketch.js"></script>
35
+ </body>
36
+
37
+ </html>
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "format": "prettier --write \"src/**/*.{js,ts}\"",
11
+ "format:check": "prettier --check \"src/**/*.{js,ts}\"",
12
+ "lint": "eslint src --ext .js",
13
+ "lint:fix": "eslint src --ext .js --fix"
14
+ },
15
+ "dependencies": {
16
+ "textmode.js": "latest",
17
+ "tweakpane": "^4.0.5"
18
+ },
19
+ "devDependencies": {
20
+ "@eslint/js": "^9.39.1",
21
+ "eslint": "^9.39.1",
22
+ "eslint-config-prettier": "^10.1.8",
23
+ "globals": "^15.14.0",
24
+ "prettier": "^3.7.4",
25
+ "vite": "^7.2.7"
26
+ }
27
+ }
@@ -0,0 +1,62 @@
1
+ import { textmode } from 'textmode.js';
2
+ import { Pane } from 'tweakpane';
3
+
4
+ const tm = textmode.create({ width: window.innerWidth, height: window.innerHeight });
5
+
6
+ const params = {
7
+ speed: 0.1,
8
+ frequency: 0.22,
9
+ amplitude: 1.1,
10
+ baseCharColor: { r: 0, g: 170, b: 255 },
11
+ baseCellColor: { r: 0, g: 0, b: 0 }
12
+ };
13
+
14
+ const paneContainer = document.querySelector('#pane') ?? undefined;
15
+ const pane = new Pane({ title: 'Controls', container: paneContainer });
16
+
17
+ pane.addBinding(params, 'speed', { min: 0, max: 0.4, step: 0.005, label: 'Speed' });
18
+ pane.addBinding(params, 'frequency', { min: 0.05, max: 0.8, step: 0.01, label: 'Frequency' });
19
+ pane.addBinding(params, 'amplitude', { min: 0.5, max: 2.0, step: 0.05, label: 'Amplitude' });
20
+ pane.addBinding(params, 'baseCharColor', { label: 'Base Char Color' });
21
+ pane.addBinding(params, 'baseCellColor', { label: 'Base Cell Color' });
22
+
23
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
24
+
25
+ const getChar = (wave) => {
26
+ if (wave > 0.6) return '▓';
27
+ if (wave > 0.2) return '▒';
28
+ return '░';
29
+ };
30
+
31
+ tm.draw(() => {
32
+ const halfCols = Math.round(tm.grid.cols / 2) + 1;
33
+ const halfRows = Math.round(tm.grid.rows / 2) + 1;
34
+
35
+ for (let y = -halfRows; y < halfRows; y++) {
36
+ for (let x = -halfCols; x < halfCols; x++) {
37
+ const dist = Math.hypot(x, y);
38
+ const wave = Math.sin(dist * params.frequency - tm.frameCount * params.speed) * params.amplitude;
39
+ const char = getChar(wave);
40
+
41
+ const lightness = 0.35 + 0.65 * ((wave + params.amplitude) / (2 * params.amplitude));
42
+ const charR = clamp(params.baseCharColor.r * lightness, 0, 255);
43
+ const charG = clamp(params.baseCharColor.g * lightness, 0, 255);
44
+ const charB = clamp(params.baseCharColor.b * lightness, 0, 255);
45
+ const cellR = clamp(params.baseCellColor.r * lightness, 0, 255);
46
+ const cellG = clamp(params.baseCellColor.g * lightness, 0, 255);
47
+ const cellB = clamp(params.baseCellColor.b * lightness, 0, 255);
48
+
49
+ tm.push();
50
+ tm.translate(x, y, 0);
51
+ tm.char(char);
52
+ tm.charColor(charR, charG, charB);
53
+ tm.cellColor(cellR, cellG, cellB);
54
+ tm.point();
55
+ tm.pop();
56
+ }
57
+ }
58
+ });
59
+
60
+ tm.windowResized(() => {
61
+ tm.resizeCanvas(window.innerWidth, window.innerHeight);
62
+ });
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ export default defineConfig({
4
+ server: {
5
+ open: true
6
+ }
7
+ });
@@ -0,0 +1,23 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import prettierConfig from "eslint-config-prettier";
4
+ import tseslint from "typescript-eslint";
5
+
6
+ export default tseslint.config(
7
+ {
8
+ ignores: ["dist/**", "node_modules/**"],
9
+ },
10
+ js.configs.recommended,
11
+ ...tseslint.configs.recommended,
12
+ prettierConfig,
13
+ {
14
+ files: ["**/*.{ts,tsx}"],
15
+ languageOptions: {
16
+ ecmaVersion: "latest",
17
+ sourceType: "module",
18
+ globals: {
19
+ ...globals.browser,
20
+ },
21
+ },
22
+ }
23
+ );
@@ -21,7 +21,6 @@
21
21
  </head>
22
22
 
23
23
  <body>
24
- <div id="app"></div>
25
24
  <script type="module" src="/src/sketch.ts"></script>
26
25
  </body>
27
26
 
@@ -17,13 +17,14 @@
17
17
  "textmode.js": "latest"
18
18
  },
19
19
  "devDependencies": {
20
- "prettier": "^3.7.4",
21
- "typescript": "^5.9.3",
22
- "vite": "^7.2.7",
20
+ "@eslint/js": "^9.39.1",
21
+ "@types/node": "^24.10.2",
23
22
  "eslint": "^9.39.1",
24
23
  "eslint-config-prettier": "^10.1.8",
25
- "@types/node": "^24.10.2",
26
- "@typescript-eslint/parser": "^8.49.0",
27
- "@typescript-eslint/eslint-plugin": "^8.49.0"
24
+ "globals": "^15.14.0",
25
+ "prettier": "^3.7.4",
26
+ "typescript": "^5.9.3",
27
+ "typescript-eslint": "^8.49.0",
28
+ "vite": "^7.2.7"
28
29
  }
29
30
  }
@@ -0,0 +1,2 @@
1
+ node_modules
2
+ dist
@@ -0,0 +1,23 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import prettierConfig from "eslint-config-prettier";
4
+ import tseslint from "typescript-eslint";
5
+
6
+ export default tseslint.config(
7
+ {
8
+ ignores: ["dist/**", "node_modules/**"],
9
+ },
10
+ js.configs.recommended,
11
+ ...tseslint.configs.recommended,
12
+ prettierConfig,
13
+ {
14
+ files: ["**/*.{ts,tsx}"],
15
+ languageOptions: {
16
+ ecmaVersion: "latest",
17
+ sourceType: "module",
18
+ globals: {
19
+ ...globals.browser,
20
+ },
21
+ },
22
+ }
23
+ );
@@ -0,0 +1,27 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>{{name}}</title>
8
+
9
+ <style>
10
+ html,
11
+ body {
12
+ margin: 0;
13
+ padding: 0;
14
+ }
15
+
16
+ canvas {
17
+ display: block;
18
+ }
19
+ </style>
20
+ </head>
21
+
22
+ <body>
23
+ <script src="/fxhash.min.js"></script>
24
+ <script type="module" src="/src/sketch.ts"></script>
25
+ </body>
26
+
27
+ </html>
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "format": "prettier --write \"src/**/*.{js,ts}\"",
11
+ "format:check": "prettier --check \"src/**/*.{js,ts}\"",
12
+ "lint": "eslint src --ext .ts",
13
+ "lint:fix": "eslint src --ext .ts --fix",
14
+ "typecheck": "tsc --noEmit"
15
+ },
16
+ "dependencies": {
17
+ "textmode.js": "latest"
18
+ },
19
+ "devDependencies": {
20
+ "@eslint/js": "^9.39.1",
21
+ "@types/node": "^24.10.2",
22
+ "eslint": "^9.39.1",
23
+ "eslint-config-prettier": "^10.1.8",
24
+ "globals": "^15.14.0",
25
+ "prettier": "^3.7.4",
26
+ "typescript": "^5.9.3",
27
+ "typescript-eslint": "^8.49.0",
28
+ "vite": "^7.2.7"
29
+ }
30
+ }
@@ -0,0 +1 @@
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;r<t.length;r++)if(!p.includes(t[r]))return!1;return!0}function T(t){return/^(0x)?[0-9a-fA-F]{40}$/.test(t)}function v(){return`oo${Array.from({length:49},()=>p[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<t.length;e++)r+=t.charCodeAt(e).toString(16).padStart(4,"0");return r},O=function(t){let r=t.match(/.{1,4}/g)||[],e="";for(let n=0;n<r.length;n++){let s=parseInt(r[n],16);if(s===0)break;e+=String.fromCharCode(s)}return e},k=BigInt("-9223372036854775808"),R=BigInt("9223372036854775807"),h={number:{serialize:t=>{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.length<e?s.padEnd(e):s}},bytes:{serialize:(t,r)=>Array.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;a<e;a++)s=a*2,n[a]=parseInt(`${t[s]}${t[s+1]}`,16);return n},bytesLength:t=>t.options.length,random:t=>{let r=t.options?.length||0,e=new Uint8Array(r);for(let n=0;n<r;n++)e[n]=Math.random()*255|0;return e}},select:{serialize:(t,r)=>Math.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,{});})();
@@ -0,0 +1,54 @@
1
+ type FxFeatureValue = string | number | boolean;
2
+ type FxFeatures = Record<string, FxFeatureValue>;
3
+
4
+ interface FxRandFn {
5
+ (): number;
6
+ reset(): void;
7
+ }
8
+
9
+ type FxParamType = 'number' | 'bigint' | 'boolean' | 'color' | 'string' | 'bytes' | 'select';
10
+
11
+ type FxParamUpdate = 'page-reload' | 'sync' | 'code-driven';
12
+
13
+ type FxParamDefinition = {
14
+ id: string;
15
+ name?: string;
16
+ type: FxParamType;
17
+ options?: Record<string, unknown>;
18
+ default?: unknown;
19
+ update?: FxParamUpdate;
20
+ version?: string;
21
+ };
22
+
23
+ type FxParamDefinitions = FxParamDefinition[];
24
+ type FxParamsObject = Record<string, unknown>;
25
+ type FxParamEventId = 'params:update' | string;
26
+
27
+ interface FxApi {
28
+ hash: string;
29
+ minter: string;
30
+ iteration: number;
31
+ context: 'standalone' | 'capture' | 'minting' | string;
32
+ isPreview: boolean;
33
+ lineage?: string[];
34
+ depth?: number;
35
+ rand: FxRandFn;
36
+ randminter: FxRandFn;
37
+ randAt?: (depth: number) => number;
38
+ features: (features: FxFeatures) => void;
39
+ getFeature: (name: string) => FxFeatureValue | undefined;
40
+ getFeatures: () => FxFeatures;
41
+ preview: () => void;
42
+ params: (definitions: FxParamDefinitions) => void;
43
+ getParam: (id: string) => unknown;
44
+ getParams: () => FxParamsObject;
45
+ getRawParam: (id: string) => string | undefined;
46
+ emit: (eventId: FxParamEventId, data?: FxParamsObject) => void;
47
+ on: (
48
+ eventId: FxParamEventId,
49
+ handler: (...args: any[]) => boolean | Promise<boolean>,
50
+ onDone?: (optInDefault: boolean, ...args: any[]) => void
51
+ ) => () => void;
52
+ }
53
+
54
+ declare const $fx: FxApi;
@@ -0,0 +1,108 @@
1
+ /* global $fx */
2
+ import { textmode } from 'textmode.js';
3
+
4
+ type Palette = {
5
+ name: string;
6
+ bg: [number, number, number];
7
+ colors: [number, number, number][];
8
+ glyphs: string[];
9
+ };
10
+
11
+ const palettes: Palette[] = [
12
+ {
13
+ name: 'Electric Bloom',
14
+ bg: [6, 6, 12],
15
+ colors: [
16
+ [246, 70, 104],
17
+ [255, 191, 0],
18
+ [89, 247, 255]
19
+ ],
20
+ glyphs: ['▓', '▒', '░']
21
+ },
22
+ {
23
+ name: 'Monotone Void',
24
+ bg: [10, 10, 12],
25
+ colors: [
26
+ [240, 240, 240],
27
+ [180, 180, 180],
28
+ [120, 120, 120]
29
+ ],
30
+ glyphs: ['█', '▓', '░']
31
+ },
32
+ {
33
+ name: 'Emerald Night',
34
+ bg: [4, 10, 8],
35
+ colors: [
36
+ [122, 217, 147],
37
+ [48, 163, 84],
38
+ [10, 102, 85]
39
+ ],
40
+ glyphs: ['▒', '░', '▓']
41
+ }
42
+ ];
43
+
44
+ const palette = pick(palettes);
45
+ const symmetry = $fx.rand() > 0.5 ? 'mirror' : 'radial';
46
+ const density = Math.floor($fx.rand() * 5) + 3; // 3..7 controls spacing
47
+ const jitter = $fx.rand() > 0.5;
48
+
49
+ $fx.features({
50
+ Palette: palette.name,
51
+ Symmetry: symmetry,
52
+ Density: density,
53
+ Jitter: jitter ? 'yes' : 'no'
54
+ });
55
+
56
+ const tm = textmode.create({ width: window.innerWidth, height: window.innerHeight });
57
+ const seed = $fx.rand() * 10000;
58
+ let previewSent = false;
59
+
60
+ function pick<T>(items: T[]): T {
61
+ const idx = Math.floor($fx.rand() * items.length);
62
+ return items[idx];
63
+ }
64
+
65
+ function waveNoise(x: number, y: number, t: number): number {
66
+ const base = Math.sin(x * 0.45 + y * 0.35 + seed + t);
67
+ const ripple = Math.sin(Math.sqrt(x * x + y * y) * 0.35 + t * 0.5 + seed * 0.3);
68
+ return 0.5 + 0.25 * base + 0.25 * ripple;
69
+ }
70
+
71
+ tm.draw(() => {
72
+ tm.background(...palette.bg);
73
+
74
+ const halfCols = Math.floor(tm.grid.cols / 2) + 1;
75
+ const halfRows = Math.floor(tm.grid.rows / 2) + 1;
76
+
77
+ for (let y = -halfRows; y < halfRows; y++) {
78
+ for (let x = -halfCols; x < halfCols; x++) {
79
+ if ((Math.abs(x) + Math.abs(y)) % density !== 0) continue;
80
+
81
+ const sx = symmetry === 'mirror' ? Math.abs(x) : x;
82
+ const t = tm.frameCount * 0.02;
83
+ const wobble = jitter ? waveNoise(x * 0.05, y * 0.05, t * 1.5) - 0.5 : 0;
84
+ const n = waveNoise(sx * 0.35, y * 0.35, t) + wobble;
85
+ const colorIdx = Math.floor(Math.abs(n * palette.colors.length)) % palette.colors.length;
86
+ const glyph = palette.glyphs[colorIdx % palette.glyphs.length];
87
+ const [r, g, b] = palette.colors[colorIdx];
88
+
89
+ tm.push();
90
+ tm.translate(x, y, 0);
91
+ tm.char(glyph);
92
+ tm.charColor(r, g, b);
93
+ tm.point();
94
+ tm.pop();
95
+ }
96
+ }
97
+
98
+ if (!previewSent && tm.frameCount > 12) {
99
+ $fx.preview();
100
+ previewSent = true;
101
+ }
102
+ });
103
+
104
+ tm.windowResized(() => {
105
+ tm.resizeCanvas(window.innerWidth, window.innerHeight);
106
+ });
107
+
108
+ console.log('fxhash:', $fx.hash);
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "Node",
7
+ "strict": true,
8
+ "jsx": "preserve",
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "types": ["vite/client"]
12
+ },
13
+ "include": ["src"],
14
+ "references": []
15
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ export default defineConfig({
4
+ server: {
5
+ open: true
6
+ }
7
+ });
@@ -0,0 +1,2 @@
1
+ node_modules
2
+ dist
@@ -0,0 +1,23 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import prettierConfig from "eslint-config-prettier";
4
+ import tseslint from "typescript-eslint";
5
+
6
+ export default tseslint.config(
7
+ {
8
+ ignores: ["dist/**", "node_modules/**"],
9
+ },
10
+ js.configs.recommended,
11
+ ...tseslint.configs.recommended,
12
+ prettierConfig,
13
+ {
14
+ files: ["**/*.{ts,tsx}"],
15
+ languageOptions: {
16
+ ecmaVersion: "latest",
17
+ sourceType: "module",
18
+ globals: {
19
+ ...globals.browser,
20
+ },
21
+ },
22
+ }
23
+ );
@@ -0,0 +1,37 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>{{name}}</title>
8
+
9
+ <style>
10
+ html,
11
+ body {
12
+ margin: 0;
13
+ padding: 0;
14
+ }
15
+
16
+ canvas {
17
+ display: block;
18
+ }
19
+
20
+ #pane {
21
+ position: fixed;
22
+ top: 12px;
23
+ right: 12px;
24
+ width: 260px;
25
+ z-index: 10;
26
+ pointer-events: auto;
27
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
28
+ }
29
+ </style>
30
+ </head>
31
+
32
+ <body>
33
+ <div id="pane"></div>
34
+ <script type="module" src="/src/sketch.ts"></script>
35
+ </body>
36
+
37
+ </html>
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "format": "prettier --write \"src/**/*.{js,ts}\"",
11
+ "format:check": "prettier --check \"src/**/*.{js,ts}\"",
12
+ "lint": "eslint src --ext .ts",
13
+ "lint:fix": "eslint src --ext .ts --fix",
14
+ "typecheck": "tsc --noEmit"
15
+ },
16
+ "dependencies": {
17
+ "textmode.js": "latest",
18
+ "tweakpane": "^4.0.5"
19
+ },
20
+ "devDependencies": {
21
+ "@eslint/js": "^9.39.1",
22
+ "@tweakpane/core": "^2.0.5",
23
+ "@types/node": "^24.10.2",
24
+ "eslint": "^9.39.1",
25
+ "eslint-config-prettier": "^10.1.8",
26
+ "globals": "^15.14.0",
27
+ "prettier": "^3.7.4",
28
+ "typescript": "^5.9.3",
29
+ "typescript-eslint": "^8.49.0",
30
+ "vite": "^7.2.7"
31
+ }
32
+ }
@@ -0,0 +1,72 @@
1
+ import { textmode } from 'textmode.js';
2
+ import { Pane } from 'tweakpane';
3
+
4
+ type RGB = { r: number; g: number; b: number };
5
+
6
+ type Params = {
7
+ speed: number;
8
+ frequency: number;
9
+ amplitude: number;
10
+ baseCharColor: RGB;
11
+ baseCellColor: RGB;
12
+ };
13
+
14
+ const tm = textmode.create({ width: window.innerWidth, height: window.innerHeight });
15
+
16
+ const params: Params = {
17
+ speed: 0.1,
18
+ frequency: 0.22,
19
+ amplitude: 1.1,
20
+ baseCharColor: { r: 0, g: 170, b: 255 },
21
+ baseCellColor: { r: 0, g: 0, b: 0 }
22
+ };
23
+
24
+ const paneContainer = document.querySelector<HTMLElement>('#pane') ?? undefined;
25
+ const pane = new Pane({ title: 'Controls', container: paneContainer });
26
+
27
+ pane.addBinding(params, 'speed', { min: 0, max: 0.4, step: 0.005, label: 'Speed' });
28
+ pane.addBinding(params, 'frequency', { min: 0.05, max: 0.8, step: 0.01, label: 'Frequency' });
29
+ pane.addBinding(params, 'amplitude', { min: 0.5, max: 2.0, step: 0.05, label: 'Amplitude' });
30
+ pane.addBinding(params, 'baseCharColor', { label: 'Base Char Color' });
31
+ pane.addBinding(params, 'baseCellColor', { label: 'Base Cell Color' });
32
+
33
+ const clamp = (value: number, min: number, max: number): number => Math.min(Math.max(value, min), max);
34
+
35
+ const getChar = (wave: number): string => {
36
+ if (wave > 0.6) return '▓';
37
+ if (wave > 0.2) return '▒';
38
+ return '░';
39
+ };
40
+
41
+ tm.draw(() => {
42
+ const halfCols = Math.round(tm.grid.cols / 2) + 1;
43
+ const halfRows = Math.round(tm.grid.rows / 2) + 1;
44
+
45
+ for (let y = -halfRows; y < halfRows; y++) {
46
+ for (let x = -halfCols; x < halfCols; x++) {
47
+ const dist = Math.hypot(x, y);
48
+ const wave = Math.sin(dist * params.frequency - tm.frameCount * params.speed) * params.amplitude;
49
+ const char = getChar(wave);
50
+
51
+ const lightness = 0.35 + 0.65 * ((wave + params.amplitude) / (2 * params.amplitude));
52
+ const charR = clamp(params.baseCharColor.r * lightness, 0, 255);
53
+ const charG = clamp(params.baseCharColor.g * lightness, 0, 255);
54
+ const charB = clamp(params.baseCharColor.b * lightness, 0, 255);
55
+ const cellR = clamp(params.baseCellColor.r * lightness, 0, 255);
56
+ const cellG = clamp(params.baseCellColor.g * lightness, 0, 255);
57
+ const cellB = clamp(params.baseCellColor.b * lightness, 0, 255);
58
+
59
+ tm.push();
60
+ tm.translate(x, y, 0);
61
+ tm.char(char);
62
+ tm.charColor(charR, charG, charB);
63
+ tm.cellColor(cellR, cellG, cellB);
64
+ tm.point();
65
+ tm.pop();
66
+ }
67
+ }
68
+ });
69
+
70
+ tm.windowResized(() => {
71
+ tm.resizeCanvas(window.innerWidth, window.innerHeight);
72
+ });
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "Node",
7
+ "strict": true,
8
+ "jsx": "preserve",
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "types": ["vite/client"]
12
+ },
13
+ "include": ["src"],
14
+ "references": []
15
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ export default defineConfig({
4
+ server: {
5
+ open: true
6
+ }
7
+ });
@@ -1,13 +0,0 @@
1
- {
2
- "root": true,
3
- "env": {
4
- "browser": true,
5
- "es2021": true
6
- },
7
- "parserOptions": {
8
- "ecmaVersion": "latest",
9
- "sourceType": "module"
10
- },
11
- "extends": ["eslint:recommended", "prettier"],
12
- "ignorePatterns": ["dist/", "node_modules/"]
13
- }
@@ -1,19 +0,0 @@
1
- {
2
- "root": true,
3
- "env": {
4
- "browser": true,
5
- "es2021": true
6
- },
7
- "parser": "@typescript-eslint/parser",
8
- "parserOptions": {
9
- "ecmaVersion": "latest",
10
- "sourceType": "module"
11
- },
12
- "plugins": ["@typescript-eslint"],
13
- "extends": [
14
- "eslint:recommended",
15
- "plugin:@typescript-eslint/recommended",
16
- "prettier"
17
- ],
18
- "ignorePatterns": ["dist/", "node_modules/"]
19
- }