@modernman00/shared-js-lib 1.2.89 → 1.6.2

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.
@@ -0,0 +1,312 @@
1
+ /**
2
+ * vse-canvas.js
3
+ *
4
+ * Next-Gen Visual Supremacy Engine (VSE) Client Runtime
5
+ *
6
+ * Features:
7
+ * - Dynamic P3 wide colour gamut and HDR capability detection
8
+ * - 2.5D tactile gyroscopic micro-parallax with spring damping
9
+ * - Dr. Viktor Kovács & Marcus security killswitch (suspends sensors on input focus)
10
+ * - Accessibility & reduced-motion invariant compliance
11
+ * - 120Hz ProMotion frame pacing via requestAnimationFrame
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ class VisualSupremacyRuntime {
17
+ constructor() {
18
+ this.isP3 = false;
19
+ this.isHDR = false;
20
+ this.reducedMotion = false;
21
+ this.isLowPowerMode = false;
22
+ this.isSensorActive = false;
23
+ this.isInputFocused = false;
24
+ this.orientation = { gamma: 0, beta: 0 };
25
+ this.targetOrientation = { gamma: 0, beta: 0 };
26
+ this.rafId = null;
27
+ this.activeCards = new Set();
28
+ this.intersectionObserver = null;
29
+ }
30
+
31
+ init() {
32
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
33
+ return;
34
+ }
35
+
36
+ // 1. Telemetry: Wide Colour Gamut & Dynamic Range Detection
37
+ try {
38
+ this.isP3 = window.matchMedia && window.matchMedia('(color-gamut: p3)').matches;
39
+ this.isHDR = window.matchMedia && window.matchMedia('(dynamic-range: high)').matches;
40
+ this.reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
41
+
42
+ const root = document.documentElement;
43
+ if (this.isP3) {
44
+ root.classList.add('vse-p3-capable');
45
+ }
46
+ if (this.isHDR) {
47
+ root.classList.add('vse-hdr-capable');
48
+ }
49
+
50
+ // Watch for live reduced motion preference changes
51
+ if (window.matchMedia) {
52
+ window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', (e) => {
53
+ this.reducedMotion = e.matches;
54
+ if (this.reducedMotion) {
55
+ this.resetTransforms();
56
+ this.stopSensors();
57
+ } else if (!this.isLowPowerMode) {
58
+ this.startSensors();
59
+ }
60
+ });
61
+ }
62
+ } catch (_) {}
63
+
64
+ if (this.reducedMotion) {
65
+ return;
66
+ }
67
+
68
+ // 2. Segun & Victor PWA Battery-Saver & Low-Memory Killswitch
69
+ this.setupBatteryAndMemoryGuard();
70
+
71
+ // 3. Observe media cards using IntersectionObserver to only animate visible elements
72
+ this.setupCardObserver();
73
+
74
+ // 4. Mandatory Adversarial Security Gate (Dr. Viktor Kovács & Marcus): Input Focus Guard
75
+ this.setupSecurityGuard();
76
+
77
+ // 5. Desktop Pointer Parallax Fallback
78
+ this.setupDesktopPointerParallax();
79
+
80
+ // 6. Initialize Sensor Pipeline
81
+ this.startSensors();
82
+ }
83
+
84
+ setupBatteryAndMemoryGuard() {
85
+ try {
86
+ // Low-end Android / memory-constrained device guard (< 2GB RAM)
87
+ if (typeof navigator !== 'undefined' && navigator.deviceMemory && navigator.deviceMemory < 2) {
88
+ this.isLowPowerMode = true;
89
+ this.stopSensors();
90
+ this.resetTransforms();
91
+ return;
92
+ }
93
+
94
+ // Battery Status API guard: Disable 2.5D sensors if battery <= 20% and not charging
95
+ if (typeof navigator !== 'undefined' && typeof navigator.getBattery === 'function') {
96
+ navigator.getBattery().then((battery) => {
97
+ const checkBattery = () => {
98
+ const low = !battery.charging && battery.level <= 0.20;
99
+ this.isLowPowerMode = low;
100
+ if (low) {
101
+ this.stopSensors();
102
+ this.resetTransforms();
103
+ } else if (!this.reducedMotion && !this.isInputFocused && !document.hidden) {
104
+ this.startSensors();
105
+ }
106
+ };
107
+ checkBattery();
108
+ battery.addEventListener('levelchange', checkBattery);
109
+ battery.addEventListener('chargingchange', checkBattery);
110
+ }).catch(() => {});
111
+ }
112
+ } catch (_) {}
113
+ }
114
+
115
+ setupCardObserver() {
116
+ if (!('IntersectionObserver' in window)) {
117
+ return;
118
+ }
119
+
120
+ this.intersectionObserver = new IntersectionObserver((entries) => {
121
+ entries.forEach((entry) => {
122
+ if (entry.isIntersecting) {
123
+ this.activeCards.add(entry.target);
124
+ } else {
125
+ this.activeCards.delete(entry.target);
126
+ // Reset transform when scrolling out of viewport to save GPU compositing
127
+ this.resetCardTransform(entry.target);
128
+ }
129
+ });
130
+ }, { threshold: 0.1 });
131
+
132
+ // Auto-observe post-media-stage containers
133
+ this.refreshObservedCards();
134
+
135
+ // Re-check for dynamically inserted posts (e.g. infinite scroll, new post submission)
136
+ const observer = new MutationObserver(() => this.refreshObservedCards());
137
+ observer.observe(document.body, { childList: true, subtree: true });
138
+ }
139
+
140
+ refreshObservedCards() {
141
+ if (!this.intersectionObserver) return;
142
+ const stages = document.querySelectorAll('.post-media-stage:not([data-vse-observed])');
143
+ stages.forEach((el) => {
144
+ el.setAttribute('data-vse-observed', 'true');
145
+ this.intersectionObserver.observe(el);
146
+ });
147
+ }
148
+
149
+ setupSecurityGuard() {
150
+ // Gyroscopic Timing Side-Channel Defense: Immediately detach listeners on active input
151
+ const pauseEvents = ['focusin', 'focus'];
152
+ const resumeEvents = ['focusout', 'blur'];
153
+
154
+ pauseEvents.forEach((ev) => {
155
+ document.addEventListener(ev, (e) => {
156
+ const tag = e.target && e.target.tagName ? e.target.tagName.toLowerCase() : '';
157
+ const isEditable = e.target && (e.target.isContentEditable || tag === 'input' || tag === 'textarea' || tag === 'select');
158
+ if (isEditable) {
159
+ this.isInputFocused = true;
160
+ this.stopSensors();
161
+ this.resetTransforms();
162
+ }
163
+ }, true);
164
+ });
165
+
166
+ resumeEvents.forEach((ev) => {
167
+ document.addEventListener(ev, () => {
168
+ this.isInputFocused = false;
169
+ if (!document.hidden && !this.reducedMotion) {
170
+ this.startSensors();
171
+ }
172
+ }, true);
173
+ });
174
+
175
+ // Tab Visibility Guard
176
+ document.addEventListener('visibilitychange', () => {
177
+ if (document.hidden) {
178
+ this.stopSensors();
179
+ this.resetTransforms();
180
+ } else if (!this.isInputFocused && !this.reducedMotion) {
181
+ this.startSensors();
182
+ }
183
+ });
184
+ }
185
+
186
+ startSensors() {
187
+ if (this.isSensorActive || this.reducedMotion || this.isInputFocused || this.isLowPowerMode) {
188
+ return;
189
+ }
190
+
191
+ if (typeof window.DeviceOrientationEvent !== 'undefined') {
192
+ this.orientationHandler = (e) => this.handleDeviceOrientation(e);
193
+ window.addEventListener('deviceorientation', this.orientationHandler, { passive: true });
194
+ this.isSensorActive = true;
195
+ this.scheduleRenderLoop();
196
+ }
197
+ }
198
+
199
+ stopSensors() {
200
+ if (!this.isSensorActive) {
201
+ return;
202
+ }
203
+
204
+ if (this.orientationHandler) {
205
+ window.removeEventListener('deviceorientation', this.orientationHandler);
206
+ this.orientationHandler = null;
207
+ }
208
+ if (this.rafId) {
209
+ cancelAnimationFrame(this.rafId);
210
+ this.rafId = null;
211
+ }
212
+ this.isSensorActive = false;
213
+ }
214
+
215
+ handleDeviceOrientation(event) {
216
+ if (this.isInputFocused || this.reducedMotion || this.isLowPowerMode) return;
217
+
218
+ // Gamma: Left to Right (-90 to 90), Beta: Front to Back (-180 to 180)
219
+ let g = event.gamma || 0;
220
+ let b = event.beta || 0;
221
+
222
+ // Normalise resting hand angle: smartphones are typically held tilted forward by ~45 degrees
223
+ b = b - 45;
224
+
225
+ // Clamp to safe, subtle displacement (±15 degrees input, mapping to ±3.5 degrees visual rotation)
226
+ const clamp = (val, max) => Math.max(-max, Math.min(max, val));
227
+ this.targetOrientation.gamma = clamp(g, 15);
228
+ this.targetOrientation.beta = clamp(b, 15);
229
+ }
230
+
231
+ setupDesktopPointerParallax() {
232
+ // For non-touch/pointer devices, provide a subtle interactive hover depth response
233
+ if (window.matchMedia && window.matchMedia('(hover: hover)').matches) {
234
+ document.addEventListener('mousemove', (e) => {
235
+ if (this.isInputFocused || this.reducedMotion || this.activeCards.size === 0) return;
236
+
237
+ this.activeCards.forEach((card) => {
238
+ const rect = card.getBoundingClientRect();
239
+ if (e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom) {
240
+ const relX = ((e.clientX - rect.left) / rect.width) * 2 - 1; // -1 to 1
241
+ const relY = ((e.clientY - rect.top) / rect.height) * 2 - 1; // -1 to 1
242
+
243
+ const rotX = -relY * 3.5;
244
+ const rotY = relX * 3.5;
245
+ const transX = relX * 4;
246
+ const transY = relY * 4;
247
+
248
+ const img = card.querySelector('.post-media-first-img');
249
+ if (img) {
250
+ img.style.transform = `perspective(800px) rotateX(${rotX.toFixed(2)}deg) rotateY(${rotY.toFixed(2)}deg) translate3d(${transX.toFixed(1)}px, ${transY.toFixed(1)}px, 6px) scale(1.02)`;
251
+ img.style.transition = 'transform 0.12s cubic-bezier(0.2, 0, 0, 1)';
252
+ }
253
+ } else {
254
+ this.resetCardTransform(card);
255
+ }
256
+ });
257
+ }, { passive: true });
258
+ }
259
+ }
260
+
261
+ scheduleRenderLoop() {
262
+ if (this.rafId) return;
263
+
264
+ const render = () => {
265
+ // Spring damping / low-pass filter for silky 120fps motion without jitter
266
+ const ease = 0.08;
267
+ this.orientation.gamma += (this.targetOrientation.gamma - this.orientation.gamma) * ease;
268
+ this.orientation.beta += (this.targetOrientation.beta - this.orientation.beta) * ease;
269
+
270
+ const rotY = (this.orientation.gamma / 15) * 3.2; // Max 3.2deg
271
+ const rotX = (-this.orientation.beta / 15) * 3.2; // Max 3.2deg
272
+ const transX = (this.orientation.gamma / 15) * 4.5; // Max 4.5px
273
+ const transY = (this.orientation.beta / 15) * 4.5; // Max 4.5px
274
+
275
+ if (this.activeCards.size > 0 && (Math.abs(rotY) > 0.05 || Math.abs(rotX) > 0.05)) {
276
+ this.activeCards.forEach((card) => {
277
+ const img = card.querySelector('.post-media-first-img');
278
+ if (img) {
279
+ img.style.transform = `perspective(800px) rotateX(${rotX.toFixed(2)}deg) rotateY(${rotY.toFixed(2)}deg) translate3d(${transX.toFixed(1)}px, ${transY.toFixed(1)}px, 5px) scale(1.015)`;
280
+ img.style.transformOrigin = 'center center';
281
+ img.style.willChange = 'transform';
282
+ }
283
+ });
284
+ }
285
+
286
+ this.rafId = requestAnimationFrame(render);
287
+ };
288
+
289
+ this.rafId = requestAnimationFrame(render);
290
+ }
291
+
292
+ resetCardTransform(card) {
293
+ if (!card) return;
294
+ const img = card.querySelector('.post-media-first-img');
295
+ if (img) {
296
+ img.style.transform = '';
297
+ img.style.willChange = 'auto';
298
+ img.style.transition = 'transform 0.3s ease-out';
299
+ }
300
+ }
301
+
302
+ resetTransforms() {
303
+ this.activeCards.forEach((card) => this.resetCardTransform(card));
304
+ this.targetOrientation = { gamma: 0, beta: 0 };
305
+ this.orientation = { gamma: 0, beta: 0 };
306
+ }
307
+ }
308
+
309
+ export const vseRuntime = new VisualSupremacyRuntime();
310
+ export function initVisualSupremacy() {
311
+ vseRuntime.init();
312
+ }