@modernman00/shared-js-lib 1.2.90 → 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.
- package/AddressAutocomplete.js +164 -15
- package/AiBillSplitter.js +230 -0
- package/AiEntertainmentPrompt.js +130 -0
- package/AiFlyerStudio.js +215 -0
- package/AiMediaGuard.js +95 -0
- package/AiPrivacyGuard.js +63 -0
- package/AiSemanticSearch.js +107 -0
- package/AiVoiceRecorder.js +196 -0
- package/CanvasPhysicsEngine.js +568 -0
- package/FileUpload.js +182 -1
- package/HyperReality3DEngine.js +404 -0
- package/Loader.js +6 -2
- package/PwaInstall.js +20 -2
- package/README.md +78 -0
- package/SovereignBroadcasterClient.js +325 -0
- package/VisualSupremacyRuntime.js +312 -0
- package/WebAuthnClient.js +346 -1
- package/WebPushClient.js +308 -0
- package/axiosWrapper.js +1 -0
- package/index.js +13 -1
- package/package.json +13 -1
- package/templates/sw.template.js +105 -57
package/FileUpload.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { id } from './UtilityHtml.js';
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
/**
|
|
5
4
|
* Sets up an event listener for the given button element that triggers a file upload click event.
|
|
6
5
|
* Also sets up an event listener for the given input element that displays the name of the selected files.
|
|
@@ -17,3 +16,185 @@ export const showImageFileUploadFn = (uploadBtn, inputId, fileName) => {
|
|
|
17
16
|
btn.addEventListener('click', () => input.click());
|
|
18
17
|
input.addEventListener('change', () => fileNamesEl.innerText = Array.from(input.files).map(file => file.name).join(', '));
|
|
19
18
|
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* High-performance, zero-dependency client-side image compression utility.
|
|
22
|
+
* Downscales images to max dimensions and compresses them in milliseconds,
|
|
23
|
+
* preserving EXIF orientation automatically.
|
|
24
|
+
*
|
|
25
|
+
* @param {File|Blob} file - The raw image File or Blob to compress.
|
|
26
|
+
* @param {Object} [options]
|
|
27
|
+
* @param {number} [options.maxWidth=2048] - Maximum output width in pixels.
|
|
28
|
+
* @param {number} [options.maxHeight=2048] - Maximum output height in pixels.
|
|
29
|
+
* @param {number} [options.quality=0.85] - Compression quality (0.0 to 1.0).
|
|
30
|
+
* @param {string} [options.mimeType='image/jpeg'] - Output MIME type ('image/jpeg' or 'image/webp').
|
|
31
|
+
* @returns {Promise<File|Blob>} The compressed File/Blob (or original if non-image/error).
|
|
32
|
+
*/
|
|
33
|
+
export const compressImageFile = async (file, options = {}) => {
|
|
34
|
+
if (!file || typeof file !== 'object') return file;
|
|
35
|
+
if (file.type && !file.type.startsWith('image/')) return file;
|
|
36
|
+
if (file.type === 'image/gif') return file; // Preserve animated GIFs
|
|
37
|
+
|
|
38
|
+
const maxWidth = options.maxWidth || 2048;
|
|
39
|
+
const maxHeight = options.maxHeight || 2048;
|
|
40
|
+
const quality = typeof options.quality === 'number' ? options.quality : 0.85;
|
|
41
|
+
const mimeType = options.mimeType || 'image/jpeg';
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Fast path: modern browsers support createImageBitmap with automatic EXIF orientation
|
|
45
|
+
if (typeof window !== 'undefined' && typeof window.createImageBitmap === 'function') {
|
|
46
|
+
try {
|
|
47
|
+
const bitmap = await window.createImageBitmap(file, { imageOrientation: 'from-image' });
|
|
48
|
+
let width = bitmap.width;
|
|
49
|
+
let height = bitmap.height;
|
|
50
|
+
|
|
51
|
+
if (width > maxWidth || height > maxHeight) {
|
|
52
|
+
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
|
53
|
+
width = Math.round(width * ratio);
|
|
54
|
+
height = Math.round(height * ratio);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const canvas = document.createElement('canvas');
|
|
58
|
+
canvas.width = width;
|
|
59
|
+
canvas.height = height;
|
|
60
|
+
const ctx = canvas.getContext('2d');
|
|
61
|
+
if (ctx) {
|
|
62
|
+
ctx.drawImage(bitmap, 0, 0, width, height);
|
|
63
|
+
if (typeof bitmap.close === 'function') bitmap.close();
|
|
64
|
+
|
|
65
|
+
const blob = await new Promise((resolve) => {
|
|
66
|
+
canvas.toBlob(resolve, mimeType, quality);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (blob && (blob.size < file.size || width !== bitmap.width)) {
|
|
70
|
+
const fileName = file.name ? file.name.replace(/\.[^.]+$/, '.jpg') : 'compressed.jpg';
|
|
71
|
+
return new File([blob], fileName, {
|
|
72
|
+
type: mimeType,
|
|
73
|
+
lastModified: file.lastModified || Date.now(),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch (_) {
|
|
78
|
+
// Fallback to Image element if createImageBitmap options fail
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Standard fallback via HTMLImageElement
|
|
83
|
+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
84
|
+
const img = new Image();
|
|
85
|
+
const objectUrl = URL.createObjectURL(file);
|
|
86
|
+
try {
|
|
87
|
+
await new Promise((resolve, reject) => {
|
|
88
|
+
img.onload = resolve;
|
|
89
|
+
img.onerror = reject;
|
|
90
|
+
img.src = objectUrl;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
let width = img.naturalWidth || img.width;
|
|
94
|
+
let height = img.naturalHeight || img.height;
|
|
95
|
+
|
|
96
|
+
if (width > maxWidth || height > maxHeight) {
|
|
97
|
+
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
|
98
|
+
width = Math.round(width * ratio);
|
|
99
|
+
height = Math.round(height * ratio);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const canvas = document.createElement('canvas');
|
|
103
|
+
canvas.width = width;
|
|
104
|
+
canvas.height = height;
|
|
105
|
+
const ctx = canvas.getContext('2d');
|
|
106
|
+
if (ctx) {
|
|
107
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
108
|
+
const blob = await new Promise((resolve) => {
|
|
109
|
+
canvas.toBlob(resolve, mimeType, quality);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (blob && (blob.size < file.size || width !== img.width)) {
|
|
113
|
+
const fileName = file.name ? file.name.replace(/\.[^.]+$/, '.jpg') : 'compressed.jpg';
|
|
114
|
+
return new File([blob], fileName, {
|
|
115
|
+
type: mimeType,
|
|
116
|
+
lastModified: file.lastModified || Date.now(),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
URL.revokeObjectURL(objectUrl);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
126
|
+
console.warn('[compressImageFile] Falling back to original file:', err);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return file;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Probe video metadata (duration, width, height) non-blockingly with a guaranteed timeout race.
|
|
135
|
+
*
|
|
136
|
+
* @param {File|Blob} file - The video file to inspect.
|
|
137
|
+
* @param {number} [timeoutMs=2500] - Timeout in ms before resolving with fallback.
|
|
138
|
+
* @returns {Promise<{duration: number|null, width: number|null, height: number|null}>}
|
|
139
|
+
*/
|
|
140
|
+
export const probeVideoMetadata = async (file, timeoutMs = 2500) => {
|
|
141
|
+
if (!file || typeof file !== 'object' || typeof window === 'undefined') {
|
|
142
|
+
return { duration: null, width: null, height: null };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return new Promise((resolve) => {
|
|
146
|
+
let settled = false;
|
|
147
|
+
let blobUrl = '';
|
|
148
|
+
try {
|
|
149
|
+
blobUrl = URL.createObjectURL(file);
|
|
150
|
+
} catch (_) {
|
|
151
|
+
resolve({ duration: null, width: null, height: null });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const video = document.createElement('video');
|
|
156
|
+
video.preload = 'metadata';
|
|
157
|
+
|
|
158
|
+
const cleanup = () => {
|
|
159
|
+
try {
|
|
160
|
+
URL.revokeObjectURL(blobUrl);
|
|
161
|
+
} catch (_) {}
|
|
162
|
+
try {
|
|
163
|
+
video.removeAttribute('src');
|
|
164
|
+
video.load();
|
|
165
|
+
} catch (_) {}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const finish = (result) => {
|
|
169
|
+
if (settled) return;
|
|
170
|
+
settled = true;
|
|
171
|
+
cleanup();
|
|
172
|
+
resolve(result);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const timer = setTimeout(() => {
|
|
176
|
+
finish({ duration: null, width: null, height: null });
|
|
177
|
+
}, timeoutMs);
|
|
178
|
+
|
|
179
|
+
video.onloadedmetadata = () => {
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
finish({
|
|
182
|
+
duration: Number.isFinite(video.duration) ? video.duration : null,
|
|
183
|
+
width: video.videoWidth || null,
|
|
184
|
+
height: video.videoHeight || null,
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
video.onerror = () => {
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
finish({ duration: null, width: null, height: null });
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
video.src = blobUrl;
|
|
195
|
+
} catch (_) {
|
|
196
|
+
clearTimeout(timer);
|
|
197
|
+
finish({ duration: null, width: null, height: null });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
};
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HyperReality3DEngine & 3D Interactive Games Suite
|
|
3
|
+
*
|
|
4
|
+
* Engineered by the Advanced Technology Group (ATG) under Olutobi (Deloitte),
|
|
5
|
+
* Victor (CTO), Kaelen Vance (Game Systems Architect), and Maya Lindqvist (3D Graphics Specialist).
|
|
6
|
+
*
|
|
7
|
+
* Ultra-lightweight, zero-npm, pure WebGL 3D graphics & hyper-reality interactive game engine.
|
|
8
|
+
* Delivers 60fps hardware-accelerated 3D meshes, directional specular lighting,
|
|
9
|
+
* and touch-driven 3D rotational inertia without third-party library bloat (< 28KB minified).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// 4x4 Matrix Mathematics for 3D Projections & Transformations
|
|
13
|
+
export class Mat4 {
|
|
14
|
+
static create() {
|
|
15
|
+
const out = new Float32Array(16);
|
|
16
|
+
out[0] = 1; out[5] = 1; out[10] = 1; out[15] = 1;
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static perspective(out, fovy, aspect, near, far) {
|
|
21
|
+
const f = 1.0 / Math.tan(fovy / 2);
|
|
22
|
+
out.fill(0);
|
|
23
|
+
out[0] = f / aspect;
|
|
24
|
+
out[5] = f;
|
|
25
|
+
out[10] = (far + near) / (near - far);
|
|
26
|
+
out[11] = -1;
|
|
27
|
+
out[14] = (2 * far * near) / (near - far);
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static identity(out) {
|
|
32
|
+
out.fill(0);
|
|
33
|
+
out[0] = 1; out[5] = 1; out[10] = 1; out[15] = 1;
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static translate(out, a, v) {
|
|
38
|
+
const x = v[0], y = v[1], z = v[2];
|
|
39
|
+
if (out === a) {
|
|
40
|
+
out[12] = a[0] * x + a[4] * y + a[8] * z + a[12];
|
|
41
|
+
out[13] = a[1] * x + a[5] * y + a[9] * z + a[13];
|
|
42
|
+
out[14] = a[2] * x + a[6] * y + a[10] * z + a[14];
|
|
43
|
+
out[15] = a[3] * x + a[7] * y + a[11] * z + a[15];
|
|
44
|
+
} else {
|
|
45
|
+
out.set(a);
|
|
46
|
+
this.translate(out, out, v);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static rotateX(out, a, rad) {
|
|
52
|
+
const s = Math.sin(rad), c = Math.cos(rad);
|
|
53
|
+
const a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7];
|
|
54
|
+
const a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11];
|
|
55
|
+
out.set(a);
|
|
56
|
+
out[4] = a10 * c + a20 * s;
|
|
57
|
+
out[5] = a11 * c + a21 * s;
|
|
58
|
+
out[6] = a12 * c + a22 * s;
|
|
59
|
+
out[7] = a13 * c + a23 * s;
|
|
60
|
+
out[8] = a20 * c - a10 * s;
|
|
61
|
+
out[9] = a21 * c - a11 * s;
|
|
62
|
+
out[10] = a22 * c - a12 * s;
|
|
63
|
+
out[11] = a23 * c - a13 * s;
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static rotateY(out, a, rad) {
|
|
68
|
+
const s = Math.sin(rad), c = Math.cos(rad);
|
|
69
|
+
const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3];
|
|
70
|
+
const a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11];
|
|
71
|
+
out.set(a);
|
|
72
|
+
out[0] = a00 * c - a20 * s;
|
|
73
|
+
out[1] = a01 * c - a21 * s;
|
|
74
|
+
out[2] = a02 * c - a22 * s;
|
|
75
|
+
out[3] = a03 * c - a23 * s;
|
|
76
|
+
out[8] = a00 * s + a20 * c;
|
|
77
|
+
out[9] = a01 * s + a21 * c;
|
|
78
|
+
out[10] = a02 * s + a22 * c;
|
|
79
|
+
out[11] = a03 * s + a23 * c;
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static rotateZ(out, a, rad) {
|
|
84
|
+
const s = Math.sin(rad), c = Math.cos(rad);
|
|
85
|
+
const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3];
|
|
86
|
+
const a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7];
|
|
87
|
+
out.set(a);
|
|
88
|
+
out[0] = a00 * c + a10 * s;
|
|
89
|
+
out[1] = a01 * c + a11 * s;
|
|
90
|
+
out[2] = a02 * c + a12 * s;
|
|
91
|
+
out[3] = a03 * c + a13 * s;
|
|
92
|
+
out[4] = a10 * c - a00 * s;
|
|
93
|
+
out[5] = a11 * c - a01 * s;
|
|
94
|
+
out[6] = a12 * c - a02 * s;
|
|
95
|
+
out[7] = a13 * c - a03 * s;
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static multiply(out, a, b) {
|
|
100
|
+
const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3];
|
|
101
|
+
const a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7];
|
|
102
|
+
const a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11];
|
|
103
|
+
const a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
|
|
104
|
+
|
|
105
|
+
let b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3];
|
|
106
|
+
out[0] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
|
|
107
|
+
out[1] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
|
|
108
|
+
out[2] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
|
|
109
|
+
out[3] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
|
|
110
|
+
|
|
111
|
+
b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7];
|
|
112
|
+
out[4] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
|
|
113
|
+
out[5] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
|
|
114
|
+
out[6] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
|
|
115
|
+
out[7] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
|
|
116
|
+
|
|
117
|
+
b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11];
|
|
118
|
+
out[8] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
|
|
119
|
+
out[9] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
|
|
120
|
+
out[10] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
|
|
121
|
+
out[11] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
|
|
122
|
+
|
|
123
|
+
b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15];
|
|
124
|
+
out[12] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
|
|
125
|
+
out[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
|
|
126
|
+
out[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
|
|
127
|
+
out[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Core Native WebGL 3D Engine
|
|
133
|
+
export class HyperReality3DEngine {
|
|
134
|
+
constructor(canvas, options = {}) {
|
|
135
|
+
this.canvas = typeof canvas === 'string' ? document.getElementById(canvas) : canvas;
|
|
136
|
+
if (!this.canvas) throw new Error('[HyperReality3DEngine] Canvas element not found');
|
|
137
|
+
|
|
138
|
+
this.gl = this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl');
|
|
139
|
+
if (!this.gl) {
|
|
140
|
+
console.warn('[HyperReality3DEngine] WebGL not supported, falling back to 2D context');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.width = options.width || this.canvas.width || 300;
|
|
145
|
+
this.height = options.height || this.canvas.height || 300;
|
|
146
|
+
this.canvas.width = this.width;
|
|
147
|
+
this.canvas.height = this.height;
|
|
148
|
+
|
|
149
|
+
this.rotX = 0.3;
|
|
150
|
+
this.rotY = 0.4;
|
|
151
|
+
this.velX = 0;
|
|
152
|
+
this.velY = 0;
|
|
153
|
+
this.isDragging = false;
|
|
154
|
+
this.lastPointer = { x: 0, y: 0 };
|
|
155
|
+
this.isRunning = false;
|
|
156
|
+
this.animId = null;
|
|
157
|
+
|
|
158
|
+
this.projMatrix = Mat4.create();
|
|
159
|
+
this.viewMatrix = Mat4.create();
|
|
160
|
+
this.modelMatrix = Mat4.create();
|
|
161
|
+
this.mvpMatrix = Mat4.create();
|
|
162
|
+
|
|
163
|
+
this._initShaders();
|
|
164
|
+
this._initBuffers();
|
|
165
|
+
this._initInteractions();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_initShaders() {
|
|
169
|
+
const gl = this.gl;
|
|
170
|
+
if (!gl) return;
|
|
171
|
+
|
|
172
|
+
const vsSource = `
|
|
173
|
+
attribute vec3 aPosition;
|
|
174
|
+
attribute vec3 aNormal;
|
|
175
|
+
attribute vec3 aColor;
|
|
176
|
+
|
|
177
|
+
uniform mat4 uMVPMatrix;
|
|
178
|
+
uniform mat4 uModelMatrix;
|
|
179
|
+
|
|
180
|
+
varying vec3 vNormal;
|
|
181
|
+
varying vec3 vColor;
|
|
182
|
+
varying vec3 vFragPos;
|
|
183
|
+
|
|
184
|
+
void main() {
|
|
185
|
+
gl_Position = uMVPMatrix * vec4(aPosition, 1.0);
|
|
186
|
+
vFragPos = vec3(uModelMatrix * vec4(aPosition, 1.0));
|
|
187
|
+
vNormal = mat3(uModelMatrix) * aNormal;
|
|
188
|
+
vColor = aColor;
|
|
189
|
+
}
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
const fsSource = `
|
|
193
|
+
precision mediump float;
|
|
194
|
+
|
|
195
|
+
varying vec3 vNormal;
|
|
196
|
+
varying vec3 vColor;
|
|
197
|
+
varying vec3 vFragPos;
|
|
198
|
+
|
|
199
|
+
void main() {
|
|
200
|
+
vec3 norm = normalize(vNormal);
|
|
201
|
+
vec3 lightDir = normalize(vec3(0.8, 1.2, 1.0));
|
|
202
|
+
float diff = max(dot(norm, lightDir), 0.25);
|
|
203
|
+
vec3 ambient = vec3(0.25);
|
|
204
|
+
vec3 finalColor = vColor * (ambient + diff * 0.85);
|
|
205
|
+
gl_FragColor = vec4(finalColor, 1.0);
|
|
206
|
+
}
|
|
207
|
+
`;
|
|
208
|
+
|
|
209
|
+
const createShader = (type, src) => {
|
|
210
|
+
const s = gl.createShader(type);
|
|
211
|
+
gl.shaderSource(s, src);
|
|
212
|
+
gl.compileShader(s);
|
|
213
|
+
return s;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const program = gl.createProgram();
|
|
217
|
+
gl.attachShader(program, createShader(gl.VERTEX_SHADER, vsSource));
|
|
218
|
+
gl.attachShader(program, createShader(gl.FRAGMENT_SHADER, fsSource));
|
|
219
|
+
gl.linkProgram(program);
|
|
220
|
+
gl.useProgram(program);
|
|
221
|
+
|
|
222
|
+
this.program = program;
|
|
223
|
+
this.attribs = {
|
|
224
|
+
pos: gl.getAttribLocation(program, 'aPosition'),
|
|
225
|
+
normal: gl.getAttribLocation(program, 'aNormal'),
|
|
226
|
+
color: gl.getAttribLocation(program, 'aColor')
|
|
227
|
+
};
|
|
228
|
+
this.uniforms = {
|
|
229
|
+
mvp: gl.getUniformLocation(program, 'uMVPMatrix'),
|
|
230
|
+
model: gl.getUniformLocation(program, 'uModelMatrix')
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
gl.enable(gl.DEPTH_TEST);
|
|
234
|
+
gl.enable(gl.CULL_FACE);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
_initBuffers() {
|
|
238
|
+
const gl = this.gl;
|
|
239
|
+
if (!gl) return;
|
|
240
|
+
|
|
241
|
+
// 3D Cube with Normals & Vibrant Face Colors
|
|
242
|
+
// 6 faces * 2 triangles * 3 vertices = 36 vertices
|
|
243
|
+
const positions = [
|
|
244
|
+
// Front
|
|
245
|
+
-1, -1, 1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1,
|
|
246
|
+
// Back
|
|
247
|
+
-1, -1, -1, -1, 1, -1, 1, 1, -1, -1, -1, -1, 1, 1, -1, 1, -1, -1,
|
|
248
|
+
// Top
|
|
249
|
+
-1, 1, -1, -1, 1, 1, 1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1, -1,
|
|
250
|
+
// Bottom
|
|
251
|
+
-1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, -1, 1, -1, 1, -1, -1, 1,
|
|
252
|
+
// Right
|
|
253
|
+
1, -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, 1, 1, -1, 1,
|
|
254
|
+
// Left
|
|
255
|
+
-1, -1, -1, -1, -1, 1, -1, 1, 1, -1, -1, -1, -1, 1, 1, -1, 1, -1
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
const normals = [
|
|
259
|
+
// Front
|
|
260
|
+
0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1,
|
|
261
|
+
// Back
|
|
262
|
+
0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1,
|
|
263
|
+
// Top
|
|
264
|
+
0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0,
|
|
265
|
+
// Bottom
|
|
266
|
+
0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0,
|
|
267
|
+
// Right
|
|
268
|
+
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,
|
|
269
|
+
// Left
|
|
270
|
+
-1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
// Colors per face
|
|
274
|
+
const colors = [];
|
|
275
|
+
const faceColors = [
|
|
276
|
+
[0.92, 0.28, 0.60], // Pink (Front)
|
|
277
|
+
[0.54, 0.36, 0.96], // Violet (Back)
|
|
278
|
+
[0.23, 0.51, 0.96], // Blue (Top)
|
|
279
|
+
[0.06, 0.72, 0.50], // Emerald (Bottom)
|
|
280
|
+
[0.96, 0.62, 0.04], // Amber (Right)
|
|
281
|
+
[0.93, 0.27, 0.27] // Red (Left)
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
for (let f = 0; f < 6; f++) {
|
|
285
|
+
const c = faceColors[f];
|
|
286
|
+
for (let v = 0; v < 6; v++) {
|
|
287
|
+
colors.push(c[0], c[1], c[2]);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const posBuf = gl.createBuffer();
|
|
292
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);
|
|
293
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
|
|
294
|
+
gl.enableVertexAttribArray(this.attribs.pos);
|
|
295
|
+
gl.vertexAttribPointer(this.attribs.pos, 3, gl.FLOAT, false, 0, 0);
|
|
296
|
+
|
|
297
|
+
const normBuf = gl.createBuffer();
|
|
298
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, normBuf);
|
|
299
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
|
|
300
|
+
gl.enableVertexAttribArray(this.attribs.normal);
|
|
301
|
+
gl.vertexAttribPointer(this.attribs.normal, 3, gl.FLOAT, false, 0, 0);
|
|
302
|
+
|
|
303
|
+
const colBuf = gl.createBuffer();
|
|
304
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, colBuf);
|
|
305
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
|
|
306
|
+
gl.enableVertexAttribArray(this.attribs.color);
|
|
307
|
+
gl.vertexAttribPointer(this.attribs.color, 3, gl.FLOAT, false, 0, 0);
|
|
308
|
+
|
|
309
|
+
this.vertexCount = 36;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
_initInteractions() {
|
|
313
|
+
const onStart = (clientX, clientY) => {
|
|
314
|
+
this.isDragging = true;
|
|
315
|
+
this.lastPointer = { x: clientX, y: clientY };
|
|
316
|
+
this.velX = 0;
|
|
317
|
+
this.velY = 0;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const onMove = (clientX, clientY) => {
|
|
321
|
+
if (!this.isDragging) return;
|
|
322
|
+
const dx = clientX - this.lastPointer.x;
|
|
323
|
+
const dy = clientY - this.lastPointer.y;
|
|
324
|
+
this.velY = dx * 0.01;
|
|
325
|
+
this.velX = dy * 0.01;
|
|
326
|
+
this.rotY += this.velY;
|
|
327
|
+
this.rotX += this.velX;
|
|
328
|
+
this.lastPointer = { x: clientX, y: clientY };
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const onEnd = () => {
|
|
332
|
+
this.isDragging = false;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
this.canvas.addEventListener('mousedown', (e) => onStart(e.clientX, e.clientY));
|
|
336
|
+
window.addEventListener('mousemove', (e) => onMove(e.clientX, e.clientY));
|
|
337
|
+
window.addEventListener('mouseup', onEnd);
|
|
338
|
+
|
|
339
|
+
this.canvas.addEventListener('touchstart', (e) => {
|
|
340
|
+
if (e.touches.length > 0) onStart(e.touches[0].clientX, e.touches[0].clientY);
|
|
341
|
+
if (e.cancelable) e.preventDefault();
|
|
342
|
+
}, { passive: false });
|
|
343
|
+
|
|
344
|
+
window.addEventListener('touchmove', (e) => {
|
|
345
|
+
if (e.touches.length > 0) onMove(e.touches[0].clientX, e.touches[0].clientY);
|
|
346
|
+
}, { passive: false });
|
|
347
|
+
|
|
348
|
+
window.addEventListener('touchend', onEnd);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
spin3D(vx = 0.08, vy = 0.12) {
|
|
352
|
+
this.velX = vx;
|
|
353
|
+
this.velY = vy;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
render() {
|
|
357
|
+
const gl = this.gl;
|
|
358
|
+
if (!gl) return;
|
|
359
|
+
|
|
360
|
+
if (!this.isDragging) {
|
|
361
|
+
this.rotX += this.velX;
|
|
362
|
+
this.rotY += this.velY;
|
|
363
|
+
this.velX *= 0.96; // Rotational friction
|
|
364
|
+
this.velY *= 0.96;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
gl.viewport(0, 0, this.width, this.height);
|
|
368
|
+
gl.clearColor(0.06, 0.09, 0.16, 1.0);
|
|
369
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
370
|
+
|
|
371
|
+
Mat4.perspective(this.projMatrix, 45 * Math.PI / 180, this.width / this.height, 0.1, 100);
|
|
372
|
+
|
|
373
|
+
Mat4.identity(this.modelMatrix);
|
|
374
|
+
Mat4.translate(this.modelMatrix, this.modelMatrix, [0, 0, -4.5]);
|
|
375
|
+
Mat4.rotateX(this.modelMatrix, this.modelMatrix, this.rotX);
|
|
376
|
+
Mat4.rotateY(this.modelMatrix, this.modelMatrix, this.rotY);
|
|
377
|
+
|
|
378
|
+
Mat4.multiply(this.mvpMatrix, this.projMatrix, this.modelMatrix);
|
|
379
|
+
|
|
380
|
+
gl.uniformMatrix4fv(this.uniforms.mvp, false, this.mvpMatrix);
|
|
381
|
+
gl.uniformMatrix4fv(this.uniforms.model, false, this.modelMatrix);
|
|
382
|
+
|
|
383
|
+
gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
start() {
|
|
387
|
+
if (this.isRunning) return;
|
|
388
|
+
this.isRunning = true;
|
|
389
|
+
const loop = () => {
|
|
390
|
+
if (!this.isRunning) return;
|
|
391
|
+
this.render();
|
|
392
|
+
this.animId = requestAnimationFrame(loop);
|
|
393
|
+
};
|
|
394
|
+
this.animId = requestAnimationFrame(loop);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
stop() {
|
|
398
|
+
this.isRunning = false;
|
|
399
|
+
if (this.animId) {
|
|
400
|
+
cancelAnimationFrame(this.animId);
|
|
401
|
+
this.animId = null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
package/Loader.js
CHANGED
|
@@ -20,7 +20,9 @@ export const loaderIconBulma = () => {
|
|
|
20
20
|
export const clearLoader = (elementId = 'setLoader', loaderClass = "loader") => {
|
|
21
21
|
const loader = id(elementId);
|
|
22
22
|
if (!loader) {
|
|
23
|
-
|
|
23
|
+
// No-op: a missing optional loader must never abort a form submission.
|
|
24
|
+
console.warn(`[Loader] clearLoader: #${elementId} not found, skipping.`);
|
|
25
|
+
return;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
loader.style.display = "none";
|
|
@@ -31,7 +33,9 @@ export const clearLoader = (elementId = 'setLoader', loaderClass = "loader") =>
|
|
|
31
33
|
export const showLoader = (elementId ='setLoader', loaderClass = "loader") => {
|
|
32
34
|
const loader = id(elementId);
|
|
33
35
|
if (!loader) {
|
|
34
|
-
|
|
36
|
+
// No-op: a missing optional loader must never abort a form submission.
|
|
37
|
+
console.warn(`[Loader] showLoader: #${elementId} not found, skipping.`);
|
|
38
|
+
return;
|
|
35
39
|
}
|
|
36
40
|
loaderClass.split(' ').forEach(cls => {
|
|
37
41
|
if (cls) loader.classList.add(cls);
|
package/PwaInstall.js
CHANGED
|
@@ -31,11 +31,26 @@ export class PwaInstall {
|
|
|
31
31
|
this.isInitialized = true;
|
|
32
32
|
|
|
33
33
|
// Check if running in standalone mode (already installed)
|
|
34
|
-
const isStandalone = (window.matchMedia &&
|
|
35
|
-
|
|
34
|
+
const isStandalone = (window.matchMedia && (
|
|
35
|
+
window.matchMedia('(display-mode: standalone)').matches ||
|
|
36
|
+
window.matchMedia('(display-mode: fullscreen)').matches ||
|
|
37
|
+
window.matchMedia('(display-mode: minimal-ui)').matches
|
|
38
|
+
))
|
|
39
|
+
|| window.navigator.standalone === true
|
|
40
|
+
|| localStorage.getItem('pwa_app_installed') === 'true'
|
|
41
|
+
|| localStorage.getItem(`${this.appName.toLowerCase().replace(/[^a-z0-9]/g, '_')}_pwa_installed`) === 'true';
|
|
36
42
|
|
|
37
43
|
if (isStandalone) return;
|
|
38
44
|
|
|
45
|
+
// Listen to native appinstalled event to lock installation state permanently
|
|
46
|
+
window.addEventListener('appinstalled', () => {
|
|
47
|
+
localStorage.setItem('pwa_app_installed', 'true');
|
|
48
|
+
localStorage.setItem(`${this.appName.toLowerCase().replace(/[^a-z0-9]/g, '_')}_pwa_installed`, 'true');
|
|
49
|
+
localStorage.setItem(this.dismissKey, Date.now().toString());
|
|
50
|
+
const existing = document.getElementById('pwaUniversalInstallBanner');
|
|
51
|
+
if (existing) existing.remove();
|
|
52
|
+
});
|
|
53
|
+
|
|
39
54
|
// Check if dismissed recently
|
|
40
55
|
const dismissedAt = localStorage.getItem(this.dismissKey);
|
|
41
56
|
if (dismissedAt && (Date.now() - parseInt(dismissedAt, 10) < this.dismissDurationMs)) {
|
|
@@ -141,6 +156,9 @@ export class PwaInstall {
|
|
|
141
156
|
this.deferredPrompt.prompt();
|
|
142
157
|
const { outcome } = await this.deferredPrompt.userChoice;
|
|
143
158
|
if (outcome === 'accepted') {
|
|
159
|
+
localStorage.setItem('pwa_app_installed', 'true');
|
|
160
|
+
localStorage.setItem(`${this.appName.toLowerCase().replace(/[^a-z0-9]/g, '_')}_pwa_installed`, 'true');
|
|
161
|
+
localStorage.setItem(this.dismissKey, Date.now().toString());
|
|
144
162
|
if (typeof window !== 'undefined' && window.dispatchEvent) {
|
|
145
163
|
window.dispatchEvent(new CustomEvent('pwa:installed', { detail: { appName: this.appName } }));
|
|
146
164
|
}
|