@srsdesigndev/pi-loader 1.0.0

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.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ ex# π Loader
2
+
3
+ A pi-driven generative art loader. No randomness library. No predefined colors. No loops. Just pure math.
4
+
5
+ Every bar height, every background color, and every opacity is determined by the next unused digit of π — computed live, used once, discarded forever.
6
+
7
+ ---
8
+
9
+ ## How It Works
10
+
11
+ Pi is infinite and non-repeating. This project uses that property as an infinite source of deterministic "randomness."
12
+
13
+ - **10 digits** → mapped to bar heights and opacities
14
+ - **6 digits** → combined into a hex color code for the background
15
+ - Digits 0, 1, 2 → low opacity bars (quiet)
16
+ - Digits 3–9 → full opacity bars (loud)
17
+ - Bar color flips black or white based on background luminance — always readable
18
+ - Every digit is used once and thrown away — never repeated, never stored
19
+
20
+ The algorithm used is the **Rabinowitz-Wagon spigot algorithm**, implemented in JavaScript using `BigInt` for arbitrary precision. It yields one digit of π at a time, forever.
21
+
22
+ ---
23
+
24
+ ## Demo
25
+
26
+ Just open `pi-loader-color.html` in any modern browser. No dependencies. No build step. No install.
27
+
28
+ ```bash
29
+ open pi-loader-color.html
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Files
35
+
36
+ ```
37
+ pi-loader-color.html # Full standalone demo — bars + color bg
38
+ pi-loader.html # Minimal version — bars only, dark bg
39
+ ```
40
+
41
+ ---
42
+
43
+ ## The Algorithm
44
+
45
+ ```js
46
+ function* piSpigot() {
47
+ let q = 1n, r = 0n, t = 1n, k = 1n, n = 3n, l = 3n;
48
+ while (true) {
49
+ if (4n * q + r - t < n * t) {
50
+ yield Number(n);
51
+ const nr = 10n * (r - n * t);
52
+ n = 10n * (3n * q + r) / t - 10n * n;
53
+ q *= 10n;
54
+ r = nr;
55
+ } else {
56
+ const nr = (2n * q + r) * l;
57
+ const nn = (q * (7n * k) + 2n + r * l) / (t * l);
58
+ q *= k;
59
+ t *= l;
60
+ l += 2n;
61
+ k += 1n;
62
+ n = nn;
63
+ r = nr;
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ Reference: Rabinowitz, S. & Wagon, S. (1995). *A spigot algorithm for the digits of π.*
70
+
71
+ ---
72
+
73
+ ## Digit → Visual Mapping
74
+
75
+ | Digits consumed | Used for |
76
+ |---|---|
77
+ | 10 | Bar heights (`digit × 5.4 + 6` px) |
78
+ | 6 | Background hex color (`#d3a9f1` etc.) |
79
+ | **16 total** | Per frame |
80
+
81
+ ---
82
+
83
+ ## Why Pi?
84
+
85
+ Because it's free, infinite, and needs no seed. Most generative art uses `Math.random()` — which is pseudorandom, stateful, and resets. Pi never resets. Pi never repeats. Pi is already there, waiting.
86
+
87
+ ---
88
+
89
+ ## Ideas / Roadmap
90
+
91
+ - [ ] Map digits to musical notes — a pi-driven melody
92
+ - [ ] Export frames as a GIF
93
+ - [ ] Web component `<pi-loader>`
94
+ - [ ] Configurable bar count, speed, color palette
95
+ - [ ] Visualize digit distribution over time
96
+
97
+ ---
98
+
99
+ ## License
100
+
101
+ MIT — do whatever you want with it.
102
+
103
+ ---
104
+
105
+ ## Author
106
+
107
+ Built with curiosity and π.
@@ -0,0 +1 @@
1
+ !function(){"use strict";const t=function*(){let t=1n,e=0n,n=1n,s=1n,i=3n,r=3n;for(;;)if(4n*t+e-n<i*n){yield Number(i);const s=10n*(e-i*n);i=10n*(3n*t+e)/n-10n*i,t*=10n,e=s}else{const a=(2n*t+e)*r,o=(t*(7n*s)+2n+e*r)/(n*r);t*=s,n*=r,r+=2n,s+=1n,i=o,e=a}}();function e(e){const n=[];for(let s=0;s<e;s++)n.push(t.next().value);return n}const n=new Set;let s=null;function i(t){n.forEach(e=>{e._visible&&t-e._lastFrame>=e._interval&&(e._frame(),e._lastFrame=t)}),s=requestAnimationFrame(i)}function r(t,e,n,s,i){const r=t.getAttribute(e);if(null===r)return n;const a=parseFloat(r);return isNaN(a)?n:void 0!==s?function(t,e,n){return Math.min(Math.max(t,e),n)}(a,s,i):a}class a extends HTMLElement{constructor(){super(),this._shadow=this.attachShadow({mode:"open"}),this._bars=[],this._wrapper=null,this._visible=!1,this._lastFrame=0,this._interval=600,this._observer=null}static get observedAttributes(){return["bars","min-height","max-height","width","gap","speed","radius","color","bg","low-opacity","theme","align"]}get config(){return{bars:r(this,"bars",10,3,20),minH:r(this,"min-height",6,1,200),maxH:r(this,"max-height",60,10,400),width:r(this,"width",10,2,100),gap:r(this,"gap",6,0,100),speed:r(this,"speed",600,80,5e3),radius:r(this,"radius",999,0,999),color:this.getAttribute("color")||"auto",bg:this.getAttribute("bg")||"transparent",lowOpacity:r(this,"low-opacity",.2,0,1),theme:this.getAttribute("theme")||"dark",align:this.getAttribute("align")||"center"}}connectedCallback(){this._build(),this._setupObserver(),n.add(this),s||(s=requestAnimationFrame(i))}disconnectedCallback(){n.delete(this),0===n.size&&s&&(cancelAnimationFrame(s),s=null),this._observer&&this._observer.disconnect()}attributeChangedCallback(){this._wrapper&&this._build()}_build(){const t=this.config;this._interval=t.speed,this._shadow.innerHTML="",this._bars=[];const e=document.createElement("style");e.textContent=`\n :host {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n }\n .w {\n display: inline-flex;\n align-items: ${"bottom"===t.align?"flex-end":"center"};\n justify-content: center;\n gap: ${t.gap}px;\n height: ${t.maxH+10}px;\n background: transparent;\n transition: background 1000ms ease-in-out;\n }\n .b {\n width: ${t.width}px;\n height: ${t.minH}px;\n border-radius: ${t.radius}px;\n background: ${"auto"!==t.color?t.color:"light"===t.theme?"#111":"#fff"};\n transition:\n height ${t.speed}ms ease-in-out,\n opacity ${t.speed}ms ease-in-out,\n background 1000ms ease-in-out;\n flex-shrink: 0;\n }\n `;const n=document.createElement("div");n.className="w",this._wrapper=n;for(let e=0;e<t.bars;e++){const t=document.createElement("div");t.className="b",n.appendChild(t),this._bars.push(t)}this._shadow.appendChild(e),this._shadow.appendChild(n)}_setupObserver(){this._observer&&this._observer.disconnect(),this._observer=new IntersectionObserver(t=>{this._visible=t[0].isIntersecting},{threshold:.1}),this._observer.observe(this)}_frame(){const t=this.config,n=e(t.bars),s=t.maxH-t.minH;let i="auto"!==t.color?t.color:"light"===t.theme?"#111111":"#ffffff";if("pi"===t.bg){const n=function(t){return"#"+t.map(t=>t.toString(16)).join("")}(e(6));this._wrapper.style.background=n,this.style.setProperty("--pi-bg",n),"auto"===t.color&&(i=function(t){return parseInt(t.slice(1,3),16)/255*.299+parseInt(t.slice(3,5),16)/255*.587+parseInt(t.slice(5,7),16)/255*.114}(n)>.4?"#111111":"#ffffff")}else this._wrapper.style.background="transparent"===t.bg?"transparent":t.bg;n.forEach((e,n)=>{const r=this._bars[n];r&&(r.style.height=t.minH+e/9*s+"px",r.style.opacity=e<=2?t.lowOpacity:1,r.style.background=i)})}}customElements.get("pi-loader")||customElements.define("pi-loader",a)}();
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@srsdesigndev/pi-loader",
3
+ "version": "1.0.0",
4
+ "description": "A pi-driven generative loader web component. No randomness. No dependencies. Just π.",
5
+ "main": "src/pi-loader.js",
6
+ "module": "src/pi-loader.js",
7
+ "files": [
8
+ "src/",
9
+ "dist/"
10
+ ],
11
+ "keywords": [
12
+ "loader",
13
+ "loading",
14
+ "animation",
15
+ "pi",
16
+ "web-component",
17
+ "custom-element",
18
+ "generative",
19
+ "math",
20
+ "zero-dependency"
21
+ ],
22
+ "author": "srsdesigndev",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/srsdesigndev/pi_loader.git"
27
+ },
28
+ "homepage": "https://srsdesigndev.github.io/pi_loader",
29
+ "bugs": {
30
+ "url": "https://github.com/srsdesigndev/pi_loader/issues"
31
+ }
32
+ }
@@ -0,0 +1,262 @@
1
+ /**
2
+ * pi-loader.js
3
+ * A pi-driven generative loader web component.
4
+ * No dependencies. No randomness. Just π.
5
+ *
6
+ * Usage:
7
+ * <script src="pi-loader.js"></script>
8
+ * <pi-loader></pi-loader>
9
+ *
10
+ * Attributes:
11
+ * bars {number} Number of bars. Min: 3, Max: 20. Default: 10
12
+ * min-height {number} Min bar height in px. Default: 6
13
+ * max-height {number} Max bar height in px. Default: 60
14
+ * width {number} Bar width in px. Default: 10
15
+ * gap {number} Gap between bars in px. Default: 6
16
+ * speed {number} Frame interval in ms. Default: 600
17
+ * radius {number} Border radius px. 999 = pill, 0 = square. Default: 999
18
+ * color {string} Bar color. 'auto' = adapts to bg. Any CSS color. Default: 'auto'
19
+ * bg {string} 'pi' = pi-driven hex, 'transparent', any CSS color. Default: 'transparent'
20
+ * low-opacity {number} Opacity for digits 0–2. Default: 0.2
21
+ * theme {string} 'dark' or 'light'. Default: 'dark'
22
+ * align {string} 'center' or 'bottom'. Default: 'center'
23
+ *
24
+ * Performance:
25
+ * - One shared BigInt pi spigot across ALL instances
26
+ * - One single requestAnimationFrame loop for ALL instances
27
+ * - IntersectionObserver pauses off-screen loaders automatically
28
+ * - No setInterval, no polling, no per-instance timers
29
+ *
30
+ * @version 2.0.0
31
+ * @license MIT
32
+ */
33
+
34
+ (function () {
35
+ 'use strict';
36
+
37
+ // ── Pi Spigot (Rabinowitz-Wagon) ─────────────────────────────────────────
38
+ // One generator shared globally. Every digit consumed is gone forever.
39
+ function* piSpigot() {
40
+ let q = 1n, r = 0n, t = 1n, k = 1n, n = 3n, l = 3n;
41
+ while (true) {
42
+ if (4n * q + r - t < n * t) {
43
+ yield Number(n);
44
+ const nr = 10n * (r - n * t);
45
+ n = 10n * (3n * q + r) / t - 10n * n;
46
+ q *= 10n;
47
+ r = nr;
48
+ } else {
49
+ const nr = (2n * q + r) * l;
50
+ const nn = (q * (7n * k) + 2n + r * l) / (t * l);
51
+ q *= k; t *= l; l += 2n; k += 1n;
52
+ n = nn; r = nr;
53
+ }
54
+ }
55
+ }
56
+
57
+ const GLOBAL_GEN = piSpigot();
58
+
59
+ function nextDigits(n) {
60
+ const out = [];
61
+ for (let i = 0; i < n; i++) out.push(GLOBAL_GEN.next().value);
62
+ return out;
63
+ }
64
+
65
+ // ── Global RAF scheduler ─────────────────────────────────────────────────
66
+ // All instances register here. One rAF loop drives everything.
67
+ const _instances = new Set();
68
+ let _rafId = null;
69
+
70
+ function _tick(now) {
71
+ _instances.forEach(inst => {
72
+ if (inst._visible && now - inst._lastFrame >= inst._interval) {
73
+ inst._frame();
74
+ inst._lastFrame = now;
75
+ }
76
+ });
77
+ _rafId = requestAnimationFrame(_tick);
78
+ }
79
+
80
+ function _startLoop() {
81
+ if (!_rafId) _rafId = requestAnimationFrame(_tick);
82
+ }
83
+
84
+ function _stopLoop() {
85
+ if (_instances.size === 0 && _rafId) {
86
+ cancelAnimationFrame(_rafId);
87
+ _rafId = null;
88
+ }
89
+ }
90
+
91
+ // ── Utilities ────────────────────────────────────────────────────────────
92
+ function clamp(val, min, max) {
93
+ return Math.min(Math.max(val, min), max);
94
+ }
95
+
96
+ function getLuminance(hex) {
97
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
98
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
99
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
100
+ return 0.299 * r + 0.587 * g + 0.114 * b;
101
+ }
102
+
103
+ function digitsToHex(digits) {
104
+ return '#' + digits.map(d => d.toString(16)).join('');
105
+ }
106
+
107
+ function parseAttr(el, name, def, min, max) {
108
+ const raw = el.getAttribute(name);
109
+ if (raw === null) return def;
110
+ const num = parseFloat(raw);
111
+ if (isNaN(num)) return def;
112
+ return (min !== undefined) ? clamp(num, min, max) : num;
113
+ }
114
+
115
+ // ── Web Component ────────────────────────────────────────────────────────
116
+ class PiLoader extends HTMLElement {
117
+
118
+ constructor() {
119
+ super();
120
+ this._shadow = this.attachShadow({ mode: 'open' });
121
+ this._bars = [];
122
+ this._wrapper = null;
123
+ this._visible = false;
124
+ this._lastFrame = 0;
125
+ this._interval = 600;
126
+ this._observer = null;
127
+ }
128
+
129
+ static get observedAttributes() {
130
+ return ['bars','min-height','max-height','width','gap',
131
+ 'speed','radius','color','bg','low-opacity','theme','align'];
132
+ }
133
+
134
+ get config() {
135
+ return {
136
+ bars: parseAttr(this, 'bars', 10, 3, 20),
137
+ minH: parseAttr(this, 'min-height', 6, 1, 200),
138
+ maxH: parseAttr(this, 'max-height', 60, 10, 400),
139
+ width: parseAttr(this, 'width', 10, 2, 100),
140
+ gap: parseAttr(this, 'gap', 6, 0, 100),
141
+ speed: parseAttr(this, 'speed', 600, 80, 5000),
142
+ radius: parseAttr(this, 'radius', 999, 0, 999),
143
+ color: this.getAttribute('color') || 'auto',
144
+ bg: this.getAttribute('bg') || 'transparent',
145
+ lowOpacity: parseAttr(this, 'low-opacity', 0.2, 0, 1),
146
+ theme: this.getAttribute('theme') || 'dark',
147
+ align: this.getAttribute('align') || 'center',
148
+ };
149
+ }
150
+
151
+ connectedCallback() {
152
+ this._build();
153
+ this._setupObserver();
154
+ _instances.add(this);
155
+ _startLoop();
156
+ }
157
+
158
+ disconnectedCallback() {
159
+ _instances.delete(this);
160
+ _stopLoop();
161
+ if (this._observer) this._observer.disconnect();
162
+ }
163
+
164
+ attributeChangedCallback() {
165
+ if (this._wrapper) this._build();
166
+ }
167
+
168
+ _build() {
169
+ const cfg = this.config;
170
+ this._interval = cfg.speed;
171
+ this._shadow.innerHTML = '';
172
+ this._bars = [];
173
+
174
+ const style = document.createElement('style');
175
+ style.textContent = `
176
+ :host {
177
+ display: inline-flex;
178
+ align-items: center;
179
+ justify-content: center;
180
+ }
181
+ .w {
182
+ display: inline-flex;
183
+ align-items: ${cfg.align === 'bottom' ? 'flex-end' : 'center'};
184
+ justify-content: center;
185
+ gap: ${cfg.gap}px;
186
+ height: ${cfg.maxH + 10}px;
187
+ background: transparent;
188
+ transition: background 1000ms ease-in-out;
189
+ }
190
+ .b {
191
+ width: ${cfg.width}px;
192
+ height: ${cfg.minH}px;
193
+ border-radius: ${cfg.radius}px;
194
+ background: ${cfg.color !== 'auto' ? cfg.color : (cfg.theme === 'light' ? '#111' : '#fff')};
195
+ transition:
196
+ height ${cfg.speed}ms ease-in-out,
197
+ opacity ${cfg.speed}ms ease-in-out,
198
+ background 1000ms ease-in-out;
199
+ flex-shrink: 0;
200
+ }
201
+ `;
202
+
203
+ const wrapper = document.createElement('div');
204
+ wrapper.className = 'w';
205
+ this._wrapper = wrapper;
206
+
207
+ for (let i = 0; i < cfg.bars; i++) {
208
+ const bar = document.createElement('div');
209
+ bar.className = 'b';
210
+ wrapper.appendChild(bar);
211
+ this._bars.push(bar);
212
+ }
213
+
214
+ this._shadow.appendChild(style);
215
+ this._shadow.appendChild(wrapper);
216
+ }
217
+
218
+ _setupObserver() {
219
+ if (this._observer) this._observer.disconnect();
220
+ this._observer = new IntersectionObserver(entries => {
221
+ this._visible = entries[0].isIntersecting;
222
+ }, { threshold: 0.1 });
223
+ this._observer.observe(this);
224
+ }
225
+
226
+ _frame() {
227
+ const cfg = this.config;
228
+ const digits = nextDigits(cfg.bars);
229
+ const range = cfg.maxH - cfg.minH;
230
+
231
+ let barColor = cfg.color !== 'auto'
232
+ ? cfg.color
233
+ : (cfg.theme === 'light' ? '#111111' : '#ffffff');
234
+
235
+ if (cfg.bg === 'pi') {
236
+ const hex = digitsToHex(nextDigits(6));
237
+ this._wrapper.style.background = hex;
238
+ // also bubble bg up to host so parent containers can mirror it
239
+ this.style.setProperty('--pi-bg', hex);
240
+ if (cfg.color === 'auto') {
241
+ barColor = getLuminance(hex) > 0.4 ? '#111111' : '#ffffff';
242
+ }
243
+ } else {
244
+ this._wrapper.style.background =
245
+ cfg.bg === 'transparent' ? 'transparent' : cfg.bg;
246
+ }
247
+
248
+ digits.forEach((d, i) => {
249
+ const bar = this._bars[i];
250
+ if (!bar) return;
251
+ bar.style.height = (cfg.minH + (d / 9) * range) + 'px';
252
+ bar.style.opacity = d <= 2 ? cfg.lowOpacity : 1;
253
+ bar.style.background = barColor;
254
+ });
255
+ }
256
+ }
257
+
258
+ if (!customElements.get('pi-loader')) {
259
+ customElements.define('pi-loader', PiLoader);
260
+ }
261
+
262
+ })();