Dashboard Temp Share Shortlinks Frames API

HTMLify

script.js
Views: 17 | Author: karbonsites
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
let scene, camera, renderer, particles, geometry, material, controls;
let clock = new THREE.Clock();
let expansionFactor = 1.0;
let particleTemplate = 'hearts';
const numParticles = 10000;

// Hand tracking variables
let holistic = null;
let video = document.getElementById('webcam');
let videoInitialized = false;

// --- INITIALIZATION FUNCTIONS ---

function initThreeJS() {
    const canvas = document.getElementById('three-canvas');
    const overlay = document.getElementById('overlay');

    // 1. Scene Setup
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x0a0a0f);

    // 2. Camera Setup
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.z = 50;

    // 3. Renderer Setup
    renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);

    // 4. Controls (for debugging/initial setup, will be overridden by gesture tracking)
    controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true; // an animation loop is required when damping is set to true
    controls.dampingFactor = 0.05;

    // 5. Lighting (Minimal for particle focus)
    const ambientLight = new THREE.AmbientLight(0x404040, 2); 
    scene.add(ambientLight);

    // 6. Create initial particle system
    createParticles();

    // 7. Event Listeners
    window.addEventListener('resize', onWindowResize, false);
    setupUIControls();

    // Start rendering loop
    animate();
}

function createParticles() {
    if (particles) {
        scene.remove(particles);
        geometry.dispose();
        material.dispose();
    }

    geometry = new THREE.BufferGeometry();
    const positions = [];
    const colors = [];
    const sizes = [];
    const color = new THREE.Color();

    for (let i = 0; i < numParticles; i++) {
        // Initial distribution based on template type
        let x = (Math.random() - 0.5) * 10;
        let y = (Math.random() - 0.5) * 10;
        let z = (Math.random() - 0.5) * 10;
        
        if (particleTemplate === 'saturn') {
            const radius = 15;
            const angle = Math.random() * Math.PI * 2;
            x = radius * Math.cos(angle) + (Math.random() - 0.5) * 2;
            y = (Math.random() - 0.5) * 1;
            z = radius * Math.sin(angle) + (Math.random() - 0.5) * 2;
        }

        positions.push(x, y, z);
        
        // Base color (will be modified by gesture)
        color.setHSL(Math.random(), 0.7, 0.7);
        colors.push(color.r, color.g, color.b);
        
        sizes.push(Math.random() * 0.8 + 0.2);
    }

    geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
    geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
    geometry.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 1));

    // Shader Material for custom sizing and color manipulation
    material = new THREE.ShaderMaterial({
        uniforms: {
            color: { value: new THREE.Color(0xffffff) },
            pointTexture: { value: new THREE.TextureLoader().load(getParticleTexturePath()) },
            expansion: { value: expansionFactor },
            template: { value: particleTemplate }
        },
        vertexShader: `
            attribute float size;
            attribute vec3 color;
            varying vec3 vColor;
            uniform float expansion;
            uniform string template;

            void main() {
                vColor = color;
                vec3 newPosition = position;
                
                // Apply global expansion scale
                newPosition *= expansion;

                // Apply template-specific transformations (simplified here, complex ones require compute shaders or CPU manipulation)
                if (template == "saturn") {
                    float radius = 15.0;
                    float angle = atan(position.z, position.x);
                    float currentRadius = length(vec2(position.x, position.z));
                    
                    // Simple ring effect by pushing particles slightly off the plane
                    newPosition.y += (currentRadius - radius) * 0.5;
                }

                gl_PointSize = size * (10.0 + (10.0 / -position.z)); // Size based on distance
                gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
            }
        `,
        fragmentShader: `
            varying vec3 vColor;
            uniform vec3 color;
            uniform sampler2D pointTexture;

            void main() {
                // Circular texture fading
                gl_FragColor = vec4(vColor * color, 1.0);
                gl_FragColor = gl_FragColor * texture2D(pointTexture, gl_PointCoord);
                
                // Simple fade out at edges
                float strength = distance(gl_PointCoord, vec2(0.5));
                if (strength > 0.5) discard;

                gl_FragColor.a = clamp(gl_FragColor.a, 0.0, 1.0);
            }
        `,
        blending: THREE.AdditiveBlending,
        depthTest: true,
        depthWrite: false,
        transparent: true,
        vertexColors: false // We use attribute colors, not the uniform color
    });

    particles = new THREE.Points(geometry, material);
    scene.add(particles);
}

function getParticleTexturePath() {
    // In a real application, load SVG/PNG paths. For this simulation, we use a simple placeholder.
    // We will simulate a heart shape using geometry manipulation in the vertex shader instead of a texture for simplicity here.
    if (particleTemplate === 'hearts') return 'placeholder_heart.png'; 
    if (particleTemplate === 'flowers') return 'placeholder_flower.png';
    return 'placeholder_dot.png'; // Default dot
}

// --- ANIMATION LOOP ---

function animate() {
    requestAnimationFrame(animate);

    const delta = clock.getDelta();
    const elapsed = clock.getElapsedTime();

    controls.update(); // Only required if controls.enableDamping or controls.autoRotate is set to true

    // Update particle movement based on template and time
    updateParticleMovement(elapsed, delta);

    renderer.render(scene, camera);
}

function updateParticleMovement(elapsed, delta) {
    const positions = geometry.attributes.position.array;
    const template = particleTemplate;

    for (let i = 0; i < numParticles; i++) {
        let index = i * 3;
        let x = positions[index];
        let y = positions[index + 1];
        let z = positions[index + 2];

        // Base motion (slow drift)
        x += Math.sin(elapsed * 0.5 + i * 0.1) * 0.005;
        y += Math.cos(elapsed * 0.3 + i * 0.15) * 0.005;

        // Template specific movement
        if (template === 'hearts') {
            // Simple sinusoidal oscillation to suggest a gentle 'breathing' or 'floating'
            positions[index + 1] = y + Math.sin(elapsed * 3 + i * 0.05) * 0.05;
        } else if (template === 'fireworks') {
            // Particles move away from the center rapidly
            const dist = Math.sqrt(x * x + y * y + z * z);
            if (dist < 1) { // Reset if too far
                positions[index] = (Math.random() - 0.5) * 0.5;
                positions[index + 1] = (Math.random() - 0.5) * 0.5;
                positions[index + 2] = (Math.random() - 0.5) * 0.5;
            } else {
                positions[index] *= 1.005;
                positions[index + 1] *= 1.005;
                positions[index + 2] *= 1.005;
            }
        } else if (template === 'saturn') {
            // Maintain ring structure, slight rotation
            const radius = Math.sqrt(x * x + z * z);
            positions[index] = radius * Math.cos(elapsed * 0.5 + i * 0.01);
            positions[index + 2] = radius * Math.sin(elapsed * 0.5 + i * 0.01);
        }
        
        // Update position
        positions[index] = x;
        positions[index + 1] = y;
        positions[index + 2] = z;
    }
    geometry.attributes.position.needsUpdate = true;

    // Update uniforms
    material.uniforms.expansion.value = expansionFactor;
    material.uniforms.template.value = particleTemplate;
}

// --- HAND GESTURE CONTROL (Simulated/Placeholder) ---

async function setupHandTracking() {
    if (!holistic) {
        holistic = new Holistic({
            locateFile: (path) => {
                return `https://cdn.jsdelivr.net/npm/@mediapipe/holistic@0.5.16/${path}`;
            }
        });
        holistic.setOptions({
            modelComplexity: 1,
            smoothLandmarks: true,
            enableSegmentation: true,
            refineFaceLandmarks: true
        });
    }

    try {
        const stream = await navigator.mediaDevices.getUserMedia({ video: true });
        video.srcObject = stream;
        video.play();
        video.addEventListener('loadeddata', processVideo);
        document.getElementById('status').textContent = "Camera Ready. Detecting Gestures...";
        document.body.classList.add('initialized');
        videoInitialized = true;
    } catch (err) {
        console.error("Error accessing webcam: ", err);
        document.getElementById('status').textContent = "Error: Could not access camera. Please allow camera permissions.";
    }
}

async function processVideo() {
    if (!videoInitialized) return;

    const canvas = document.createElement('canvas'); // Off-screen canvas for processing
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    const ctx = canvas.getContext('2d');

    const results = await holistic.send({ image: video });

    if (results.rightHand && results.rightHand.landmarks) {
        // We primarily use the right hand for control
        const landmarks = results.rightHand.landmarks;
        
        // 1. Expansion Control: Based on distance between Thumb (4) and Pinky (20)
        const thumbTip = landmarks[4];
        const pinkyTip = landmarks[20];
        
        if (thumbTip && pinkyTip) {
            const dist = Math.sqrt(
                Math.pow(thumbTip.x - pinkyTip.x, 2) + 
                Math.pow(thumbTip.y - pinkyTip.y, 2) + 
                Math.pow(thumbTip.z - pinkyTip.z, 2)
            );
            // Map distance (e.g., 0.1 to 0.5 normalized screen space) to expansion factor (0.1 to 5.0)
            expansionFactor = THREE.MathUtils.mapLinear(dist, 0.1, 0.5, 0.5, 3.5);
            expansionFactor = Math.min(5.0, Math.max(0.1, expansionFactor));
            document.getElementById('current-expansion').textContent = expansionFactor.toFixed(1);
            document.getElementById('expansion-slider').value = expansionFactor;
        }

        // 2. Color/Template Change Control: Based on a specific gesture (e.g., Open Palm vs Closed Fist)
        // A simple proxy: Check if the index finger (8) is extended relative to the middle finger (6)
        const indexFinger = landmarks[8];
        const middleFinger = landmarks[6];

        if (indexFinger && middleFinger) {
            // If index finger is significantly higher (closer to the top of the screen in video feed) than the middle finger, trigger a change.
            if (indexFinger.y < middleFinger.y - 0.05) {
                // Trigger template change if this state persists for a short time (to avoid flickering)
                if (!window.gestureCooldown || Date.now() > window.gestureCooldown) {
                    cycleTemplate();
                    window.gestureCooldown = Date.now() + 1000; // Cooldown for 1 second
                }
            }
        }

    } else {
        // If no hand is detected, slowly drift back to default expansion
        expansionFactor += (1.0 - expansionFactor) * 0.01;
        document.getElementById('current-expansion').textContent = expansionFactor.toFixed(1);
        document.getElementById('expansion-slider').value = expansionFactor;
    }

    // Loop the processing
    if (video.readyState === video.HAVE_ENOUGH_DATA) {
        requestAnimationFrame(processVideo);
    }
}

// --- UI & UTILITY FUNCTIONS ---

function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}

function setupUIControls() {
    // Template Buttons
    document.querySelectorAll('.button-group button').forEach(button => {
        button.addEventListener('click', (e) => {
            const newTemplate = e.target.dataset.template;
            if (newTemplate !== particleTemplate) {
                particleTemplate = newTemplate;
                updateButtonStates();
                createParticles(); // Recreate particles with new shape logic
            }
        });
    });

    // Expansion Slider
    const slider = document.getElementById('expansion-slider');
    slider.addEventListener('input', (e) => {
        expansionFactor = parseFloat(e.target.value);
        document.getElementById('current-expansion').textContent = expansionFactor.toFixed(1);
    });

    // Start Button (Hidden, but we need to initiate tracking)
    document.getElementById('status').addEventListener('click', () => {
        if (!videoInitialized) {
            setupHandTracking();
        }
    });

    updateButtonStates();
}

function updateButtonStates() {
    document.querySelectorAll('.button-group button').forEach(button => {
        if (button.dataset.template === particleTemplate) {
            button.classList.add('active');
        } else {
            button.classList.remove('active');
        }
    });
    document.getElementById('current-template').textContent = particleTemplate.charAt(0).toUpperCase() + particleTemplate.slice(1);
}

function cycleTemplate() {
    const templates = ['hearts', 'flowers', 'saturn', 'fireworks'];
    let currentIndex = templates.indexOf(particleTemplate);
    let nextIndex = (currentIndex + 1) % templates.length;
    particleTemplate = templates[nextIndex];
    updateButtonStates();
    createParticles(); // Recreate particles
}

// --- MAIN EXECUTION ---

window.onload = () => {
    initThreeJS();
    // Start by attempting to initialize camera tracking immediately
    setupHandTracking();
};