mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
Create <GridScan /> background
This commit is contained in:
73
package-lock.json
generated
73
package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"@vueuse/motion": "^3.0.3",
|
"@vueuse/motion": "^3.0.3",
|
||||||
"@wdns/vue-code-block": "^2.3.5",
|
"@wdns/vue-code-block": "^2.3.5",
|
||||||
|
"face-api.js": "^0.22.2",
|
||||||
"gl-matrix": "^3.4.3",
|
"gl-matrix": "^3.4.3",
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
"lenis": "^1.3.8",
|
"lenis": "^1.3.8",
|
||||||
@@ -4717,6 +4718,35 @@
|
|||||||
"vite": "^5.2.0 || ^6 || ^7"
|
"vite": "^5.2.0 || ^6 || ^7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tensorflow/tfjs-core": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-uwQdiklNjqBnHPeseOdG0sGxrI3+d6lybaKu2+ou3ajVeKdPEwpWbgqA6iHjq1iylnOGkgkbbnQ6r2lwkiIIHw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/offscreencanvas": "~2019.3.0",
|
||||||
|
"@types/seedrandom": "2.4.27",
|
||||||
|
"@types/webgl-ext": "0.0.30",
|
||||||
|
"@types/webgl2": "0.0.4",
|
||||||
|
"node-fetch": "~2.1.2",
|
||||||
|
"seedrandom": "2.4.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"yarn": ">= 1.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tensorflow/tfjs-core/node_modules/@types/offscreencanvas": {
|
||||||
|
"version": "2019.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz",
|
||||||
|
"integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tensorflow/tfjs-core/node_modules/seedrandom": {
|
||||||
|
"version": "2.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz",
|
||||||
|
"integrity": "sha512-2CkZ9Wn2dS4mMUWQaXLsOAfGD+irMlLEeSP3cMxpGbgyOOzJGFa+MWCOMTOCMyZinHRPxyOj/S/C57li/1to6Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tresjs/cientos": {
|
"node_modules/@tresjs/cientos": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tresjs/cientos/-/cientos-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tresjs/cientos/-/cientos-4.3.1.tgz",
|
||||||
@@ -4899,6 +4929,12 @@
|
|||||||
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/seedrandom": {
|
||||||
|
"version": "2.4.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.27.tgz",
|
||||||
|
"integrity": "sha512-YvMLqFak/7rt//lPBtEHv3M4sRNA+HGxrhFZ+DQs9K2IkYJbNwVIb8avtJfhDiuaUBX/AW0jnjv48FV8h3u9bQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/stats.js": {
|
"node_modules/@types/stats.js": {
|
||||||
"version": "0.17.4",
|
"version": "0.17.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||||
@@ -4926,6 +4962,18 @@
|
|||||||
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
|
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/webgl-ext": {
|
||||||
|
"version": "0.0.30",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz",
|
||||||
|
"integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/webgl2": {
|
||||||
|
"version": "0.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.4.tgz",
|
||||||
|
"integrity": "sha512-PACt1xdErJbMUOUweSrbVM7gSIYm1vTncW2hF6Os/EeWi6TXYAYMPp+8v6rzHmypE5gHrxaxZNXgMkJVIdZpHw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/webxr": {
|
"node_modules/@types/webxr": {
|
||||||
"version": "0.5.23",
|
"version": "0.5.23",
|
||||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.23.tgz",
|
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.23.tgz",
|
||||||
@@ -7821,6 +7869,22 @@
|
|||||||
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
|
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/face-api.js": {
|
||||||
|
"version": "0.22.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/face-api.js/-/face-api.js-0.22.2.tgz",
|
||||||
|
"integrity": "sha512-9Bbv/yaBRTKCXjiDqzryeKhYxmgSjJ7ukvOvEBy6krA0Ah/vNBlsf7iBNfJljWiPA8Tys1/MnB3lyP2Hfmsuyw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tensorflow/tfjs-core": "1.7.0",
|
||||||
|
"tslib": "^1.11.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/face-api.js/node_modules/tslib": {
|
||||||
|
"version": "1.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||||
|
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -10009,6 +10073,15 @@
|
|||||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-fetch": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-IHLHYskTc2arMYsHZH82PVX8CSKT5lzb7AXeyO06QnjGDKtkv+pv3mEki6S7reB/x1QPo+YPxQRNEVgR5V/w3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-fetch-native": {
|
"node_modules/node-fetch-native": {
|
||||||
"version": "1.6.7",
|
"version": "1.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"@vueuse/motion": "^3.0.3",
|
"@vueuse/motion": "^3.0.3",
|
||||||
"@wdns/vue-code-block": "^2.3.5",
|
"@wdns/vue-code-block": "^2.3.5",
|
||||||
|
"face-api.js": "^0.22.2",
|
||||||
"gl-matrix": "^3.4.3",
|
"gl-matrix": "^3.4.3",
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
"lenis": "^1.3.8",
|
"lenis": "^1.3.8",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Highlighted sidebar items
|
// Highlighted sidebar items
|
||||||
export const NEW = ['Color Bends', 'Ghost Cursor', 'Laser Flow', 'Liquid Ether', 'Pixel Blast', 'Floating Lines', 'Light Pillar', 'Pixel Snow'];
|
export const NEW = ['Color Bends', 'Ghost Cursor', 'Grid Scan', 'Laser Flow', 'Liquid Ether', 'Pixel Blast', 'Floating Lines', 'Light Pillar', 'Pixel Snow'];
|
||||||
export const UPDATED = [];
|
export const UPDATED = [];
|
||||||
|
|
||||||
// Used for main sidebar navigation
|
// Used for main sidebar navigation
|
||||||
@@ -119,6 +119,7 @@ export const CATEGORIES = [
|
|||||||
'Gradient Blinds',
|
'Gradient Blinds',
|
||||||
'Grid Distortion',
|
'Grid Distortion',
|
||||||
'Grid Motion',
|
'Grid Motion',
|
||||||
|
'Grid Scan',
|
||||||
'Hyperspeed',
|
'Hyperspeed',
|
||||||
'Iridescence',
|
'Iridescence',
|
||||||
'Letter Glitch',
|
'Letter Glitch',
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ const backgrounds = {
|
|||||||
'floating-lines': () => import('../demo/Backgrounds/FloatingLinesDemo.vue'),
|
'floating-lines': () => import('../demo/Backgrounds/FloatingLinesDemo.vue'),
|
||||||
'light-pillar': () => import('../demo/Backgrounds/LightPillarDemo.vue'),
|
'light-pillar': () => import('../demo/Backgrounds/LightPillarDemo.vue'),
|
||||||
'pixel-snow': () => import('../demo/Backgrounds/PixelSnowDemo.vue'),
|
'pixel-snow': () => import('../demo/Backgrounds/PixelSnowDemo.vue'),
|
||||||
|
'grid-scan': () => import('../demo/Backgrounds/GridScanDemo.vue'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const componentMap = {
|
export const componentMap = {
|
||||||
|
|||||||
28
src/constants/code/Backgrounds/gridScanCode.ts
Normal file
28
src/constants/code/Backgrounds/gridScanCode.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import code from '@content/Backgrounds/GridScan/GridScan.vue?raw';
|
||||||
|
import { createCodeObject } from '@/types/code';
|
||||||
|
|
||||||
|
export const gridScan = createCodeObject(code, 'Backgrounds/GridScan', {
|
||||||
|
installation: `npm install three postprocessing face-api.js`,
|
||||||
|
usage: `<template>
|
||||||
|
<div class="w-full h-[600px] relative">
|
||||||
|
<GridScan
|
||||||
|
:line-thickness="1"
|
||||||
|
:grid-scale="0.1"
|
||||||
|
:line-jitter="0.1"
|
||||||
|
lines-color="#392e4e"
|
||||||
|
scan-color="#FF9FFC"
|
||||||
|
:enable-post="true"
|
||||||
|
:chromatic-aberration="0.002"
|
||||||
|
:noise-intensity="0.01"
|
||||||
|
:scan-glow="0.5"
|
||||||
|
:scan-softness="2"
|
||||||
|
:enable-webcam="false"
|
||||||
|
:show-preview="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import GridScan from "./GridScan.vue";
|
||||||
|
</script>`
|
||||||
|
});
|
||||||
923
src/content/Backgrounds/GridScan/GridScan.vue
Normal file
923
src/content/Backgrounds/GridScan/GridScan.vue
Normal file
@@ -0,0 +1,923 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="containerRef"
|
||||||
|
:class="['relative w-full h-full overflow-hidden', className]"
|
||||||
|
:style="style"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="showPreview"
|
||||||
|
class="absolute right-3 bottom-3 w-[220px] h-[132px] rounded-lg overflow-hidden border border-white/25 shadow-[0_4px_16px_rgba(0,0,0,0.4)] bg-black text-white text-[12px] leading-[1.2] font-sans pointer-events-none"
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
ref="videoRef"
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
autoplay
|
||||||
|
class="w-full h-full object-cover -scale-x-100"
|
||||||
|
/>
|
||||||
|
<div class="absolute left-2 top-2 px-[6px] py-[2px] bg-black/50 rounded-[6px] backdrop-blur-[4px]">
|
||||||
|
{{
|
||||||
|
enableWebcam
|
||||||
|
? modelsReady
|
||||||
|
? uiFaceActive
|
||||||
|
? 'Face: tracking'
|
||||||
|
: 'Face: searching'
|
||||||
|
: 'Loading models'
|
||||||
|
: 'Webcam disabled'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch, useTemplateRef, type CSSProperties } from 'vue';
|
||||||
|
import { EffectComposer, RenderPass, EffectPass, BloomEffect, ChromaticAberrationEffect } from 'postprocessing';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import * as faceapi from 'face-api.js';
|
||||||
|
|
||||||
|
export type LineStyle = 'solid' | 'dashed' | 'dotted';
|
||||||
|
export type ScanDirection = 'forward' | 'backward' | 'pingpong';
|
||||||
|
|
||||||
|
interface GridScanProps {
|
||||||
|
enableWebcam?: boolean;
|
||||||
|
showPreview?: boolean;
|
||||||
|
modelsPath?: string;
|
||||||
|
sensitivity?: number;
|
||||||
|
lineThickness?: number;
|
||||||
|
linesColor?: string;
|
||||||
|
gridScale?: number;
|
||||||
|
lineStyle?: LineStyle;
|
||||||
|
lineJitter?: number;
|
||||||
|
enablePost?: boolean;
|
||||||
|
bloomIntensity?: number;
|
||||||
|
bloomThreshold?: number;
|
||||||
|
bloomSmoothing?: number;
|
||||||
|
chromaticAberration?: number;
|
||||||
|
noiseIntensity?: number;
|
||||||
|
scanColor?: string;
|
||||||
|
scanOpacity?: number;
|
||||||
|
scanDirection?: ScanDirection;
|
||||||
|
scanSoftness?: number;
|
||||||
|
scanGlow?: number;
|
||||||
|
scanPhaseTaper?: number;
|
||||||
|
scanDuration?: number;
|
||||||
|
scanDelay?: number;
|
||||||
|
enableGyro?: boolean;
|
||||||
|
scanOnClick?: boolean;
|
||||||
|
snapBackDelay?: number;
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<GridScanProps>(), {
|
||||||
|
enableWebcam: false,
|
||||||
|
showPreview: false,
|
||||||
|
modelsPath: 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights',
|
||||||
|
sensitivity: 0.55,
|
||||||
|
lineThickness: 1,
|
||||||
|
linesColor: '#392e4e',
|
||||||
|
scanColor: '#FF9FFC',
|
||||||
|
scanOpacity: 0.4,
|
||||||
|
gridScale: 0.1,
|
||||||
|
lineStyle: 'solid',
|
||||||
|
lineJitter: 0.1,
|
||||||
|
scanDirection: 'pingpong',
|
||||||
|
enablePost: true,
|
||||||
|
bloomIntensity: 0,
|
||||||
|
bloomThreshold: 0,
|
||||||
|
bloomSmoothing: 0,
|
||||||
|
chromaticAberration: 0.002,
|
||||||
|
noiseIntensity: 0.01,
|
||||||
|
scanGlow: 0.5,
|
||||||
|
scanSoftness: 2,
|
||||||
|
scanPhaseTaper: 0.9,
|
||||||
|
scanDuration: 2.0,
|
||||||
|
scanDelay: 2.0,
|
||||||
|
enableGyro: false,
|
||||||
|
scanOnClick: false,
|
||||||
|
snapBackDelay: 250,
|
||||||
|
className: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const vert = `
|
||||||
|
varying vec2 vUv;
|
||||||
|
void main(){
|
||||||
|
vUv = uv;
|
||||||
|
gl_Position = vec4(position.xy, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const frag = `
|
||||||
|
precision highp float;
|
||||||
|
uniform vec3 iResolution;
|
||||||
|
uniform float iTime;
|
||||||
|
uniform vec2 uSkew;
|
||||||
|
uniform float uTilt;
|
||||||
|
uniform float uYaw;
|
||||||
|
uniform float uLineThickness;
|
||||||
|
uniform vec3 uLinesColor;
|
||||||
|
uniform vec3 uScanColor;
|
||||||
|
uniform float uGridScale;
|
||||||
|
uniform float uLineStyle;
|
||||||
|
uniform float uLineJitter;
|
||||||
|
uniform float uScanOpacity;
|
||||||
|
uniform float uScanDirection;
|
||||||
|
uniform float uNoise;
|
||||||
|
uniform float uBloomOpacity;
|
||||||
|
uniform float uScanGlow;
|
||||||
|
uniform float uScanSoftness;
|
||||||
|
uniform float uPhaseTaper;
|
||||||
|
uniform float uScanDuration;
|
||||||
|
uniform float uScanDelay;
|
||||||
|
varying vec2 vUv;
|
||||||
|
|
||||||
|
uniform float uScanStarts[8];
|
||||||
|
uniform float uScanCount;
|
||||||
|
|
||||||
|
const int MAX_SCANS = 8;
|
||||||
|
|
||||||
|
float smoother01(float a, float b, float x){
|
||||||
|
float t = clamp((x - a) / max(1e-5, (b - a)), 0.0, 1.0);
|
||||||
|
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void mainImage(out vec4 fragColor, in vec2 fragCoord)
|
||||||
|
{
|
||||||
|
vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
|
||||||
|
|
||||||
|
vec3 ro = vec3(0.0);
|
||||||
|
vec3 rd = normalize(vec3(p, 2.0));
|
||||||
|
|
||||||
|
float cR = cos(uTilt), sR = sin(uTilt);
|
||||||
|
rd.xy = mat2(cR, -sR, sR, cR) * rd.xy;
|
||||||
|
|
||||||
|
float cY = cos(uYaw), sY = sin(uYaw);
|
||||||
|
rd.xz = mat2(cY, -sY, sY, cY) * rd.xz;
|
||||||
|
|
||||||
|
vec2 skew = clamp(uSkew, vec2(-0.7), vec2(0.7));
|
||||||
|
rd.xy += skew * rd.z;
|
||||||
|
|
||||||
|
vec3 color = vec3(0.0);
|
||||||
|
float minT = 1e20;
|
||||||
|
float gridScale = max(1e-5, uGridScale);
|
||||||
|
float fadeStrength = 2.0;
|
||||||
|
vec2 gridUV = vec2(0.0);
|
||||||
|
|
||||||
|
float hitIsY = 1.0;
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
float isY = float(i < 2);
|
||||||
|
float pos = mix(-0.2, 0.2, float(i)) * isY + mix(-0.5, 0.5, float(i - 2)) * (1.0 - isY);
|
||||||
|
float num = pos - (isY * ro.y + (1.0 - isY) * ro.x);
|
||||||
|
float den = isY * rd.y + (1.0 - isY) * rd.x;
|
||||||
|
float t = num / den;
|
||||||
|
vec3 h = ro + rd * t;
|
||||||
|
|
||||||
|
float depthBoost = smoothstep(0.0, 3.0, h.z);
|
||||||
|
h.xy += skew * 0.15 * depthBoost;
|
||||||
|
|
||||||
|
bool use = t > 0.0 && t < minT;
|
||||||
|
gridUV = use ? mix(h.zy, h.xz, isY) / gridScale : gridUV;
|
||||||
|
minT = use ? t : minT;
|
||||||
|
hitIsY = use ? isY : hitIsY;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 hit = ro + rd * minT;
|
||||||
|
float dist = length(hit - ro);
|
||||||
|
|
||||||
|
float jitterAmt = clamp(uLineJitter, 0.0, 1.0);
|
||||||
|
if (jitterAmt > 0.0) {
|
||||||
|
vec2 j = vec2(
|
||||||
|
sin(gridUV.y * 2.7 + iTime * 1.8),
|
||||||
|
cos(gridUV.x * 2.3 - iTime * 1.6)
|
||||||
|
) * (0.15 * jitterAmt);
|
||||||
|
gridUV += j;
|
||||||
|
}
|
||||||
|
float fx = fract(gridUV.x);
|
||||||
|
float fy = fract(gridUV.y);
|
||||||
|
float ax = min(fx, 1.0 - fx);
|
||||||
|
float ay = min(fy, 1.0 - fy);
|
||||||
|
float wx = fwidth(gridUV.x);
|
||||||
|
float wy = fwidth(gridUV.y);
|
||||||
|
float halfPx = max(0.0, uLineThickness) * 0.5;
|
||||||
|
|
||||||
|
float tx = halfPx * wx;
|
||||||
|
float ty = halfPx * wy;
|
||||||
|
|
||||||
|
float aax = wx;
|
||||||
|
float aay = wy;
|
||||||
|
|
||||||
|
float lineX = 1.0 - smoothstep(tx, tx + aax, ax);
|
||||||
|
float lineY = 1.0 - smoothstep(ty, ty + aay, ay);
|
||||||
|
if (uLineStyle > 0.5) {
|
||||||
|
float dashRepeat = 4.0;
|
||||||
|
float dashDuty = 0.5;
|
||||||
|
float vy = fract(gridUV.y * dashRepeat);
|
||||||
|
float vx = fract(gridUV.x * dashRepeat);
|
||||||
|
float dashMaskY = step(vy, dashDuty);
|
||||||
|
float dashMaskX = step(vx, dashDuty);
|
||||||
|
if (uLineStyle < 1.5) {
|
||||||
|
lineX *= dashMaskY;
|
||||||
|
lineY *= dashMaskX;
|
||||||
|
} else {
|
||||||
|
float dotRepeat = 6.0;
|
||||||
|
float dotWidth = 0.18;
|
||||||
|
float cy = abs(fract(gridUV.y * dotRepeat) - 0.5);
|
||||||
|
float cx = abs(fract(gridUV.x * dotRepeat) - 0.5);
|
||||||
|
float dotMaskY = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.y * dotRepeat), cy);
|
||||||
|
float dotMaskX = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.x * dotRepeat), cx);
|
||||||
|
lineX *= dotMaskY;
|
||||||
|
lineY *= dotMaskX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float primaryMask = max(lineX, lineY);
|
||||||
|
|
||||||
|
vec2 gridUV2 = (hitIsY > 0.5 ? hit.xz : hit.zy) / gridScale;
|
||||||
|
if (jitterAmt > 0.0) {
|
||||||
|
vec2 j2 = vec2(
|
||||||
|
cos(gridUV2.y * 2.1 - iTime * 1.4),
|
||||||
|
sin(gridUV2.x * 2.5 + iTime * 1.7)
|
||||||
|
) * (0.15 * jitterAmt);
|
||||||
|
gridUV2 += j2;
|
||||||
|
}
|
||||||
|
float fx2 = fract(gridUV2.x);
|
||||||
|
float fy2 = fract(gridUV2.y);
|
||||||
|
float ax2 = min(fx2, 1.0 - fx2);
|
||||||
|
float ay2 = min(fy2, 1.0 - fy2);
|
||||||
|
float wx2 = fwidth(gridUV2.x);
|
||||||
|
float wy2 = fwidth(gridUV2.y);
|
||||||
|
float tx2 = halfPx * wx2;
|
||||||
|
float ty2 = halfPx * wy2;
|
||||||
|
float aax2 = wx2;
|
||||||
|
float aay2 = wy2;
|
||||||
|
float lineX2 = 1.0 - smoothstep(tx2, tx2 + aax2, ax2);
|
||||||
|
float lineY2 = 1.0 - smoothstep(ty2, ty2 + aay2, ay2);
|
||||||
|
if (uLineStyle > 0.5) {
|
||||||
|
float dashRepeat2 = 4.0;
|
||||||
|
float dashDuty2 = 0.5;
|
||||||
|
float vy2m = fract(gridUV2.y * dashRepeat2);
|
||||||
|
float vx2m = fract(gridUV2.x * dashRepeat2);
|
||||||
|
float dashMaskY2 = step(vy2m, dashDuty2);
|
||||||
|
float dashMaskX2 = step(vx2m, dashDuty2);
|
||||||
|
if (uLineStyle < 1.5) {
|
||||||
|
lineX2 *= dashMaskY2;
|
||||||
|
lineY2 *= dashMaskX2;
|
||||||
|
} else {
|
||||||
|
float dotRepeat2 = 6.0;
|
||||||
|
float dotWidth2 = 0.18;
|
||||||
|
float cy2 = abs(fract(gridUV2.y * dotRepeat2) - 0.5);
|
||||||
|
float cx2 = abs(fract(gridUV2.x * dotRepeat2) - 0.5);
|
||||||
|
float dotMaskY2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.y * dotRepeat2), cy2);
|
||||||
|
float dotMaskX2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.x * dotRepeat2), cx2);
|
||||||
|
lineX2 *= dotMaskY2;
|
||||||
|
lineY2 *= dotMaskX2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float altMask = max(lineX2, lineY2);
|
||||||
|
|
||||||
|
float edgeDistX = min(abs(hit.x - (-0.5)), abs(hit.x - 0.5));
|
||||||
|
float edgeDistY = min(abs(hit.y - (-0.2)), abs(hit.y - 0.2));
|
||||||
|
float edgeDist = mix(edgeDistY, edgeDistX, hitIsY);
|
||||||
|
float edgeGate = 1.0 - smoothstep(gridScale * 0.5, gridScale * 2.0, edgeDist);
|
||||||
|
altMask *= edgeGate;
|
||||||
|
|
||||||
|
float lineMask = max(primaryMask, altMask);
|
||||||
|
|
||||||
|
float fade = exp(-dist * fadeStrength);
|
||||||
|
|
||||||
|
float dur = max(0.05, uScanDuration);
|
||||||
|
float del = max(0.0, uScanDelay);
|
||||||
|
float scanZMax = 2.0;
|
||||||
|
float widthScale = max(0.1, uScanGlow);
|
||||||
|
float sigma = max(0.001, 0.18 * widthScale * uScanSoftness);
|
||||||
|
float sigmaA = sigma * 2.0;
|
||||||
|
|
||||||
|
float combinedPulse = 0.0;
|
||||||
|
float combinedAura = 0.0;
|
||||||
|
|
||||||
|
float cycle = dur + del;
|
||||||
|
float tCycle = mod(iTime, cycle);
|
||||||
|
float scanPhase = clamp((tCycle - del) / dur, 0.0, 1.0);
|
||||||
|
float phase = scanPhase;
|
||||||
|
if (uScanDirection > 0.5 && uScanDirection < 1.5) {
|
||||||
|
phase = 1.0 - phase;
|
||||||
|
} else if (uScanDirection > 1.5) {
|
||||||
|
float t2 = mod(max(0.0, iTime - del), 2.0 * dur);
|
||||||
|
phase = (t2 < dur) ? (t2 / dur) : (1.0 - (t2 - dur) / dur);
|
||||||
|
}
|
||||||
|
float scanZ = phase * scanZMax;
|
||||||
|
float dz = abs(hit.z - scanZ);
|
||||||
|
float lineBand = exp(-0.5 * (dz * dz) / (sigma * sigma));
|
||||||
|
float taper = clamp(uPhaseTaper, 0.0, 0.49);
|
||||||
|
float headW = taper;
|
||||||
|
float tailW = taper;
|
||||||
|
float headFade = smoother01(0.0, headW, phase);
|
||||||
|
float tailFade = 1.0 - smoother01(1.0 - tailW, 1.0, phase);
|
||||||
|
float phaseWindow = headFade * tailFade;
|
||||||
|
float pulseBase = lineBand * phaseWindow;
|
||||||
|
combinedPulse += pulseBase * clamp(uScanOpacity, 0.0, 1.0);
|
||||||
|
float auraBand = exp(-0.5 * (dz * dz) / (sigmaA * sigmaA));
|
||||||
|
combinedAura += (auraBand * 0.25) * phaseWindow * clamp(uScanOpacity, 0.0, 1.0);
|
||||||
|
|
||||||
|
for (int i = 0; i < MAX_SCANS; i++) {
|
||||||
|
if (float(i) >= uScanCount) break;
|
||||||
|
float tActiveI = iTime - uScanStarts[i];
|
||||||
|
float phaseI = clamp(tActiveI / dur, 0.0, 1.0);
|
||||||
|
if (uScanDirection > 0.5 && uScanDirection < 1.5) {
|
||||||
|
phaseI = 1.0 - phaseI;
|
||||||
|
} else if (uScanDirection > 1.5) {
|
||||||
|
phaseI = (phaseI < 0.5) ? (phaseI * 2.0) : (1.0 - (phaseI - 0.5) * 2.0);
|
||||||
|
}
|
||||||
|
float scanZI = phaseI * scanZMax;
|
||||||
|
float dzI = abs(hit.z - scanZI);
|
||||||
|
float lineBandI = exp(-0.5 * (dzI * dzI) / (sigma * sigma));
|
||||||
|
float headFadeI = smoother01(0.0, headW, phaseI);
|
||||||
|
float tailFadeI = 1.0 - smoother01(1.0 - tailW, 1.0, phaseI);
|
||||||
|
float phaseWindowI = headFadeI * tailFadeI;
|
||||||
|
combinedPulse += lineBandI * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);
|
||||||
|
float auraBandI = exp(-0.5 * (dzI * dzI) / (sigmaA * sigmaA));
|
||||||
|
combinedAura += (auraBandI * 0.25) * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float lineVis = lineMask;
|
||||||
|
vec3 gridCol = uLinesColor * lineVis * fade;
|
||||||
|
vec3 scanCol = uScanColor * combinedPulse;
|
||||||
|
vec3 scanAura = uScanColor * combinedAura;
|
||||||
|
|
||||||
|
color = gridCol + scanCol + scanAura;
|
||||||
|
|
||||||
|
float n = fract(sin(dot(gl_FragCoord.xy + vec2(iTime * 123.4), vec2(12.9898,78.233))) * 43758.5453123);
|
||||||
|
color += (n - 0.5) * uNoise;
|
||||||
|
color = clamp(color, 0.0, 1.0);
|
||||||
|
float alpha = clamp(max(lineVis, combinedPulse), 0.0, 1.0);
|
||||||
|
float gx = 1.0 - smoothstep(tx * 2.0, tx * 2.0 + aax * 2.0, ax);
|
||||||
|
float gy = 1.0 - smoothstep(ty * 2.0, ty * 2.0 + aay * 2.0, ay);
|
||||||
|
float halo = max(gx, gy) * fade;
|
||||||
|
alpha = max(alpha, halo * clamp(uBloomOpacity, 0.0, 1.0));
|
||||||
|
fragColor = vec4(color, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main(){
|
||||||
|
vec4 c;
|
||||||
|
mainImage(c, vUv * iResolution.xy);
|
||||||
|
gl_FragColor = c;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const containerRef = useTemplateRef<HTMLDivElement>('containerRef');
|
||||||
|
const videoRef = useTemplateRef<HTMLVideoElement>('videoRef');
|
||||||
|
|
||||||
|
const modelsReady = ref(false);
|
||||||
|
const uiFaceActive = ref(false);
|
||||||
|
|
||||||
|
const MAX_SCANS = 8;
|
||||||
|
|
||||||
|
let cleanupAnimation: () => void = () => {};
|
||||||
|
|
||||||
|
const srgbColor = (hex: string): THREE.Color => {
|
||||||
|
const c = new THREE.Color(hex);
|
||||||
|
return c.convertSRGBToLinear();
|
||||||
|
};
|
||||||
|
|
||||||
|
const smoothDampVec2 = (
|
||||||
|
current: THREE.Vector2,
|
||||||
|
target: THREE.Vector2,
|
||||||
|
currentVelocity: THREE.Vector2,
|
||||||
|
smoothTimeVal: number,
|
||||||
|
maxSpeedVal: number,
|
||||||
|
deltaTime: number
|
||||||
|
): THREE.Vector2 => {
|
||||||
|
const out = current.clone();
|
||||||
|
smoothTimeVal = Math.max(0.0001, smoothTimeVal);
|
||||||
|
const omega = 2 / smoothTimeVal;
|
||||||
|
const x = omega * deltaTime;
|
||||||
|
const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);
|
||||||
|
|
||||||
|
let change = current.clone().sub(target);
|
||||||
|
const originalTo = target.clone();
|
||||||
|
|
||||||
|
const maxChange = maxSpeedVal * smoothTimeVal;
|
||||||
|
if (change.length() > maxChange) change.setLength(maxChange);
|
||||||
|
|
||||||
|
target = current.clone().sub(change);
|
||||||
|
const temp = currentVelocity.clone().addScaledVector(change, omega).multiplyScalar(deltaTime);
|
||||||
|
currentVelocity.sub(temp.clone().multiplyScalar(omega));
|
||||||
|
currentVelocity.multiplyScalar(exp);
|
||||||
|
|
||||||
|
out.copy(target.clone().add(change.add(temp).multiplyScalar(exp)));
|
||||||
|
|
||||||
|
const origMinusCurrent = originalTo.clone().sub(current);
|
||||||
|
const outMinusOrig = out.clone().sub(originalTo);
|
||||||
|
if (origMinusCurrent.dot(outMinusOrig) > 0) {
|
||||||
|
out.copy(originalTo);
|
||||||
|
currentVelocity.set(0, 0);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
const smoothDampFloat = (
|
||||||
|
current: number,
|
||||||
|
target: number,
|
||||||
|
velRef: { v: number },
|
||||||
|
smoothTimeVal: number,
|
||||||
|
maxSpeedVal: number,
|
||||||
|
deltaTime: number
|
||||||
|
): { value: number; v: number } => {
|
||||||
|
smoothTimeVal = Math.max(0.0001, smoothTimeVal);
|
||||||
|
const omega = 2 / smoothTimeVal;
|
||||||
|
const x = omega * deltaTime;
|
||||||
|
const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);
|
||||||
|
|
||||||
|
let change = current - target;
|
||||||
|
const originalTo = target;
|
||||||
|
|
||||||
|
const maxChange = maxSpeedVal * smoothTimeVal;
|
||||||
|
change = Math.sign(change) * Math.min(Math.abs(change), maxChange);
|
||||||
|
|
||||||
|
target = current - change;
|
||||||
|
const temp = (velRef.v + omega * change) * deltaTime;
|
||||||
|
velRef.v = (velRef.v - omega * temp) * exp;
|
||||||
|
|
||||||
|
let out = target + (change + temp) * exp;
|
||||||
|
|
||||||
|
const origMinusCurrent = originalTo - current;
|
||||||
|
const outMinusOrig = out - originalTo;
|
||||||
|
if (origMinusCurrent * outMinusOrig > 0) {
|
||||||
|
out = originalTo;
|
||||||
|
velRef.v = 0;
|
||||||
|
}
|
||||||
|
return { value: out, v: velRef.v };
|
||||||
|
};
|
||||||
|
|
||||||
|
const medianPush = (buf: number[], v: number, maxLen: number): void => {
|
||||||
|
buf.push(v);
|
||||||
|
if (buf.length > maxLen) buf.shift();
|
||||||
|
};
|
||||||
|
|
||||||
|
const median = (buf: number[]): number => {
|
||||||
|
if (buf.length === 0) return 0;
|
||||||
|
const a = [...buf].sort((x, y) => x - y);
|
||||||
|
const mid = Math.floor(a.length / 2);
|
||||||
|
return a.length % 2 ? a[mid] : (a[mid - 1] + a[mid]) * 0.5;
|
||||||
|
};
|
||||||
|
|
||||||
|
const centroid = (points: { x: number; y: number }[]): { x: number; y: number } => {
|
||||||
|
let x = 0,
|
||||||
|
y = 0;
|
||||||
|
const n = points.length || 1;
|
||||||
|
for (const p of points) {
|
||||||
|
x += p.x;
|
||||||
|
y += p.y;
|
||||||
|
}
|
||||||
|
return { x: x / n, y: y / n };
|
||||||
|
};
|
||||||
|
|
||||||
|
const dist2 = (a: { x: number; y: number }, b: { x: number; y: number }): number => {
|
||||||
|
return Math.hypot(a.x - b.x, a.y - b.y);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupAnimation = () => {
|
||||||
|
const container = containerRef.value;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// Non-reactive Three.js objects - stored as plain variables
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||||
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||||
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
||||||
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
|
renderer.toneMapping = THREE.NoToneMapping;
|
||||||
|
renderer.autoClear = false;
|
||||||
|
renderer.setClearColor(0x000000, 0);
|
||||||
|
container.appendChild(renderer.domElement);
|
||||||
|
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||||
|
|
||||||
|
// Animation state - plain objects, not reactive
|
||||||
|
const lookTarget = new THREE.Vector2(0, 0);
|
||||||
|
const lookCurrent = new THREE.Vector2(0, 0);
|
||||||
|
const lookVel = new THREE.Vector2(0, 0);
|
||||||
|
let tiltTarget = 0;
|
||||||
|
let tiltCurrent = 0;
|
||||||
|
let tiltVel = 0;
|
||||||
|
let yawTarget = 0;
|
||||||
|
let yawCurrent = 0;
|
||||||
|
let yawVel = 0;
|
||||||
|
const scanStarts: number[] = [];
|
||||||
|
const bufX: number[] = [];
|
||||||
|
const bufY: number[] = [];
|
||||||
|
const bufT: number[] = [];
|
||||||
|
const bufYaw: number[] = [];
|
||||||
|
let leaveTimer: number | null = null;
|
||||||
|
|
||||||
|
const uniforms = {
|
||||||
|
iResolution: {
|
||||||
|
value: new THREE.Vector3(container.clientWidth, container.clientHeight, renderer.getPixelRatio())
|
||||||
|
},
|
||||||
|
iTime: { value: 0 },
|
||||||
|
uSkew: { value: new THREE.Vector2(0, 0) },
|
||||||
|
uTilt: { value: 0 },
|
||||||
|
uYaw: { value: 0 },
|
||||||
|
uLineThickness: { value: props.lineThickness },
|
||||||
|
uLinesColor: { value: srgbColor(props.linesColor) },
|
||||||
|
uScanColor: { value: srgbColor(props.scanColor) },
|
||||||
|
uGridScale: { value: props.gridScale },
|
||||||
|
uLineStyle: { value: props.lineStyle === 'dashed' ? 1 : props.lineStyle === 'dotted' ? 2 : 0 },
|
||||||
|
uLineJitter: { value: Math.max(0, Math.min(1, props.lineJitter || 0)) },
|
||||||
|
uScanOpacity: { value: props.scanOpacity },
|
||||||
|
uNoise: { value: props.noiseIntensity },
|
||||||
|
uBloomOpacity: { value: props.bloomIntensity },
|
||||||
|
uScanGlow: { value: props.scanGlow },
|
||||||
|
uScanSoftness: { value: props.scanSoftness },
|
||||||
|
uPhaseTaper: { value: props.scanPhaseTaper },
|
||||||
|
uScanDuration: { value: props.scanDuration },
|
||||||
|
uScanDelay: { value: props.scanDelay },
|
||||||
|
uScanDirection: { value: props.scanDirection === 'backward' ? 1 : props.scanDirection === 'pingpong' ? 2 : 0 },
|
||||||
|
uScanStarts: { value: new Array(MAX_SCANS).fill(0) },
|
||||||
|
uScanCount: { value: 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
const material = new THREE.ShaderMaterial({
|
||||||
|
uniforms,
|
||||||
|
vertexShader: vert,
|
||||||
|
fragmentShader: frag,
|
||||||
|
transparent: true,
|
||||||
|
depthWrite: false,
|
||||||
|
depthTest: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const geometry = new THREE.PlaneGeometry(2, 2);
|
||||||
|
const quad = new THREE.Mesh(geometry, material);
|
||||||
|
scene.add(quad);
|
||||||
|
|
||||||
|
let composer: EffectComposer | null = null;
|
||||||
|
let bloom: BloomEffect | null = null;
|
||||||
|
let chroma: ChromaticAberrationEffect | null = null;
|
||||||
|
|
||||||
|
if (props.enablePost) {
|
||||||
|
composer = new EffectComposer(renderer);
|
||||||
|
const renderPass = new RenderPass(scene, camera);
|
||||||
|
composer.addPass(renderPass);
|
||||||
|
|
||||||
|
bloom = new BloomEffect({
|
||||||
|
intensity: 1.0,
|
||||||
|
luminanceThreshold: props.bloomThreshold,
|
||||||
|
luminanceSmoothing: props.bloomSmoothing
|
||||||
|
});
|
||||||
|
bloom.blendMode.opacity.value = Math.max(0, props.bloomIntensity);
|
||||||
|
|
||||||
|
chroma = new ChromaticAberrationEffect({
|
||||||
|
offset: new THREE.Vector2(props.chromaticAberration, props.chromaticAberration),
|
||||||
|
radialModulation: true,
|
||||||
|
modulationOffset: 0.0
|
||||||
|
});
|
||||||
|
|
||||||
|
const effectPass = new EffectPass(camera, bloom, chroma);
|
||||||
|
effectPass.renderToScreen = true;
|
||||||
|
composer.addPass(effectPass);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pushScan = (t: number): void => {
|
||||||
|
if (scanStarts.length >= MAX_SCANS) scanStarts.shift();
|
||||||
|
scanStarts.push(t);
|
||||||
|
const buf = new Array(MAX_SCANS).fill(0);
|
||||||
|
for (let i = 0; i < scanStarts.length && i < MAX_SCANS; i++) buf[i] = scanStarts[i];
|
||||||
|
uniforms.uScanStarts.value = buf;
|
||||||
|
uniforms.uScanCount.value = scanStarts.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseMove = (e: MouseEvent): void => {
|
||||||
|
if (uiFaceActive.value) return;
|
||||||
|
if (leaveTimer) {
|
||||||
|
clearTimeout(leaveTimer);
|
||||||
|
leaveTimer = null;
|
||||||
|
}
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||||||
|
const ny = -(((e.clientY - rect.top) / rect.height) * 2 - 1);
|
||||||
|
lookTarget.set(nx, ny);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClick = async (): Promise<void> => {
|
||||||
|
const nowSec = performance.now() / 1000;
|
||||||
|
if (props.scanOnClick) pushScan(nowSec);
|
||||||
|
if (
|
||||||
|
props.enableGyro &&
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
(window as any).DeviceOrientationEvent &&
|
||||||
|
(DeviceOrientationEvent as any).requestPermission
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await (DeviceOrientationEvent as any).requestPermission();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseEnter = (): void => {
|
||||||
|
if (leaveTimer) {
|
||||||
|
clearTimeout(leaveTimer);
|
||||||
|
leaveTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseLeave = (): void => {
|
||||||
|
if (uiFaceActive.value) return;
|
||||||
|
if (leaveTimer) clearTimeout(leaveTimer);
|
||||||
|
leaveTimer = window.setTimeout(
|
||||||
|
() => {
|
||||||
|
lookTarget.set(0, 0);
|
||||||
|
tiltTarget = 0;
|
||||||
|
yawTarget = 0;
|
||||||
|
},
|
||||||
|
Math.max(0, props.snapBackDelay || 0)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeviceOrientation = (e: DeviceOrientationEvent): void => {
|
||||||
|
if (uiFaceActive.value) return;
|
||||||
|
const gamma = e.gamma ?? 0;
|
||||||
|
const beta = e.beta ?? 0;
|
||||||
|
const nx = THREE.MathUtils.clamp(gamma / 45, -1, 1);
|
||||||
|
const ny = THREE.MathUtils.clamp(-beta / 30, -1, 1);
|
||||||
|
lookTarget.set(nx, ny);
|
||||||
|
tiltTarget = THREE.MathUtils.degToRad(gamma) * 0.4;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResize = (): void => {
|
||||||
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
||||||
|
uniforms.iResolution.value.set(container.clientWidth, container.clientHeight, renderer.getPixelRatio());
|
||||||
|
if (composer) composer.setSize(container.clientWidth, container.clientHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
container.addEventListener('mousemove', onMouseMove);
|
||||||
|
container.addEventListener('mouseenter', onMouseEnter);
|
||||||
|
container.addEventListener('mouseleave', onMouseLeave);
|
||||||
|
if (props.scanOnClick) container.addEventListener('click', onClick);
|
||||||
|
if (props.enableGyro) window.addEventListener('deviceorientation', onDeviceOrientation);
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
|
||||||
|
// Face detection
|
||||||
|
let stopFaceDetection = false;
|
||||||
|
let lastDetect = 0;
|
||||||
|
|
||||||
|
const startFaceDetection = async (): Promise<void> => {
|
||||||
|
if (!props.enableWebcam || !modelsReady.value) return;
|
||||||
|
const video = videoRef.value;
|
||||||
|
if (!video) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } },
|
||||||
|
audio: false
|
||||||
|
});
|
||||||
|
video.srcObject = stream;
|
||||||
|
await video.play();
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 });
|
||||||
|
|
||||||
|
const s = THREE.MathUtils.clamp(props.sensitivity, 0, 1);
|
||||||
|
const depthResponse = THREE.MathUtils.lerp(0.25, 0.45, s);
|
||||||
|
|
||||||
|
const detect = async (ts: number): Promise<void> => {
|
||||||
|
if (stopFaceDetection) return;
|
||||||
|
|
||||||
|
if (ts - lastDetect >= 33) {
|
||||||
|
lastDetect = ts;
|
||||||
|
try {
|
||||||
|
const res = await faceapi.detectSingleFace(video, opts).withFaceLandmarks(true);
|
||||||
|
if (res && res.detection) {
|
||||||
|
const det = res.detection;
|
||||||
|
const box = det.box;
|
||||||
|
const vw = video.videoWidth || 1;
|
||||||
|
const vh = video.videoHeight || 1;
|
||||||
|
|
||||||
|
const cx = box.x + box.width * 0.5;
|
||||||
|
const cy = box.y + box.height * 0.5;
|
||||||
|
const nx = (cx / vw) * 2 - 1;
|
||||||
|
const ny = (cy / vh) * 2 - 1;
|
||||||
|
medianPush(bufX, nx, 5);
|
||||||
|
medianPush(bufY, ny, 5);
|
||||||
|
const nxm = median(bufX);
|
||||||
|
const nym = median(bufY);
|
||||||
|
|
||||||
|
const look = new THREE.Vector2(Math.tanh(nxm), Math.tanh(nym));
|
||||||
|
|
||||||
|
const faceSize = Math.min(1, Math.hypot(box.width / vw, box.height / vh));
|
||||||
|
const depthScaleVal = 1 + depthResponse * (faceSize - 0.25);
|
||||||
|
lookTarget.copy(look.multiplyScalar(depthScaleVal));
|
||||||
|
|
||||||
|
const leftEye = res.landmarks.getLeftEye();
|
||||||
|
const rightEye = res.landmarks.getRightEye();
|
||||||
|
const lc = centroid(leftEye);
|
||||||
|
const rc = centroid(rightEye);
|
||||||
|
const tilt = Math.atan2(rc.y - lc.y, rc.x - lc.x);
|
||||||
|
medianPush(bufT, tilt, 5);
|
||||||
|
tiltTarget = median(bufT);
|
||||||
|
|
||||||
|
const nose = res.landmarks.getNose();
|
||||||
|
const tip = nose[nose.length - 1] || nose[Math.floor(nose.length / 2)];
|
||||||
|
const jaw = res.landmarks.getJawOutline();
|
||||||
|
const leftCheek = jaw[3] || jaw[2];
|
||||||
|
const rightCheek = jaw[13] || jaw[14];
|
||||||
|
const dL = dist2(tip, leftCheek);
|
||||||
|
const dR = dist2(tip, rightCheek);
|
||||||
|
const eyeDist = Math.hypot(rc.x - lc.x, rc.y - lc.y) + 1e-6;
|
||||||
|
let yawSignal = THREE.MathUtils.clamp((dR - dL) / (eyeDist * 1.6), -1, 1);
|
||||||
|
yawSignal = Math.tanh(yawSignal);
|
||||||
|
medianPush(bufYaw, yawSignal, 5);
|
||||||
|
yawTarget = median(bufYaw);
|
||||||
|
|
||||||
|
uiFaceActive.value = true;
|
||||||
|
} else {
|
||||||
|
uiFaceActive.value = false;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
uiFaceActive.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
|
||||||
|
(video as any).requestVideoFrameCallback(() => detect(performance.now()));
|
||||||
|
} else {
|
||||||
|
requestAnimationFrame(detect);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(detect);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopWebcam = (): void => {
|
||||||
|
stopFaceDetection = true;
|
||||||
|
const video = videoRef.value;
|
||||||
|
if (video) {
|
||||||
|
const stream = video.srcObject as MediaStream | null;
|
||||||
|
if (stream) stream.getTracks().forEach(t => t.stop());
|
||||||
|
video.pause();
|
||||||
|
video.srcObject = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for webcam changes
|
||||||
|
const unwatchWebcam = watch(
|
||||||
|
() => props.enableWebcam,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal && modelsReady.value) {
|
||||||
|
stopFaceDetection = false;
|
||||||
|
startFaceDetection();
|
||||||
|
} else {
|
||||||
|
stopWebcam();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const unwatchModels = watch(modelsReady, (ready) => {
|
||||||
|
if (ready && props.enableWebcam) {
|
||||||
|
stopFaceDetection = false;
|
||||||
|
startFaceDetection();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for prop changes
|
||||||
|
const unwatchProps = watch(
|
||||||
|
[
|
||||||
|
() => props.lineThickness,
|
||||||
|
() => props.linesColor,
|
||||||
|
() => props.scanColor,
|
||||||
|
() => props.gridScale,
|
||||||
|
() => props.lineStyle,
|
||||||
|
() => props.lineJitter,
|
||||||
|
() => props.bloomIntensity,
|
||||||
|
() => props.bloomThreshold,
|
||||||
|
() => props.bloomSmoothing,
|
||||||
|
() => props.chromaticAberration,
|
||||||
|
() => props.noiseIntensity,
|
||||||
|
() => props.scanGlow,
|
||||||
|
() => props.scanOpacity,
|
||||||
|
() => props.scanDirection,
|
||||||
|
() => props.scanSoftness,
|
||||||
|
() => props.scanPhaseTaper,
|
||||||
|
() => props.scanDuration,
|
||||||
|
() => props.scanDelay
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
uniforms.uLineThickness.value = props.lineThickness;
|
||||||
|
(uniforms.uLinesColor.value as THREE.Color).copy(srgbColor(props.linesColor));
|
||||||
|
(uniforms.uScanColor.value as THREE.Color).copy(srgbColor(props.scanColor));
|
||||||
|
uniforms.uGridScale.value = props.gridScale;
|
||||||
|
uniforms.uLineStyle.value = props.lineStyle === 'dashed' ? 1 : props.lineStyle === 'dotted' ? 2 : 0;
|
||||||
|
uniforms.uLineJitter.value = Math.max(0, Math.min(1, props.lineJitter || 0));
|
||||||
|
uniforms.uBloomOpacity.value = Math.max(0, props.bloomIntensity);
|
||||||
|
uniforms.uNoise.value = Math.max(0, props.noiseIntensity);
|
||||||
|
uniforms.uScanGlow.value = props.scanGlow;
|
||||||
|
uniforms.uScanOpacity.value = Math.max(0, Math.min(1, props.scanOpacity));
|
||||||
|
uniforms.uScanDirection.value = props.scanDirection === 'backward' ? 1 : props.scanDirection === 'pingpong' ? 2 : 0;
|
||||||
|
uniforms.uScanSoftness.value = props.scanSoftness;
|
||||||
|
uniforms.uPhaseTaper.value = props.scanPhaseTaper;
|
||||||
|
uniforms.uScanDuration.value = Math.max(0.05, props.scanDuration);
|
||||||
|
uniforms.uScanDelay.value = Math.max(0.0, props.scanDelay);
|
||||||
|
|
||||||
|
if (bloom) {
|
||||||
|
bloom.blendMode.opacity.value = Math.max(0, props.bloomIntensity);
|
||||||
|
(bloom as any).luminanceMaterial.threshold = props.bloomThreshold;
|
||||||
|
(bloom as any).luminanceMaterial.smoothing = props.bloomSmoothing;
|
||||||
|
}
|
||||||
|
if (chroma) {
|
||||||
|
chroma.offset.set(props.chromaticAberration, props.chromaticAberration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Animation loop
|
||||||
|
let rafId: number | null = null;
|
||||||
|
let last = performance.now();
|
||||||
|
|
||||||
|
const tick = (): void => {
|
||||||
|
const now = performance.now();
|
||||||
|
const dt = Math.max(0, Math.min(0.1, (now - last) / 1000));
|
||||||
|
last = now;
|
||||||
|
|
||||||
|
const s = THREE.MathUtils.clamp(props.sensitivity, 0, 1);
|
||||||
|
const skewScale = THREE.MathUtils.lerp(0.06, 0.2, s);
|
||||||
|
const tiltScale = THREE.MathUtils.lerp(0.12, 0.3, s);
|
||||||
|
const yawScale = THREE.MathUtils.lerp(0.1, 0.28, s);
|
||||||
|
const smoothTime = THREE.MathUtils.lerp(0.45, 0.12, s);
|
||||||
|
const yBoost = THREE.MathUtils.lerp(1.2, 1.6, s);
|
||||||
|
const maxSpeed = Infinity;
|
||||||
|
|
||||||
|
lookCurrent.copy(smoothDampVec2(lookCurrent, lookTarget, lookVel, smoothTime, maxSpeed, dt));
|
||||||
|
|
||||||
|
const tiltSm = smoothDampFloat(tiltCurrent, tiltTarget, { v: tiltVel }, smoothTime, maxSpeed, dt);
|
||||||
|
tiltCurrent = tiltSm.value;
|
||||||
|
tiltVel = tiltSm.v;
|
||||||
|
|
||||||
|
const yawSm = smoothDampFloat(yawCurrent, yawTarget, { v: yawVel }, smoothTime, maxSpeed, dt);
|
||||||
|
yawCurrent = yawSm.value;
|
||||||
|
yawVel = yawSm.v;
|
||||||
|
|
||||||
|
uniforms.uSkew.value.set(lookCurrent.x * skewScale, -lookCurrent.y * yBoost * skewScale);
|
||||||
|
uniforms.uTilt.value = tiltCurrent * tiltScale;
|
||||||
|
uniforms.uYaw.value = THREE.MathUtils.clamp(yawCurrent * yawScale, -0.6, 0.6);
|
||||||
|
|
||||||
|
uniforms.iTime.value = now / 1000;
|
||||||
|
renderer.clear(true, true, true);
|
||||||
|
if (composer) {
|
||||||
|
composer.render(dt);
|
||||||
|
} else {
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
}
|
||||||
|
rafId = requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
rafId = requestAnimationFrame(tick);
|
||||||
|
|
||||||
|
cleanupAnimation = () => {
|
||||||
|
if (rafId) cancelAnimationFrame(rafId);
|
||||||
|
if (leaveTimer) clearTimeout(leaveTimer);
|
||||||
|
|
||||||
|
container.removeEventListener('mousemove', onMouseMove);
|
||||||
|
container.removeEventListener('mouseenter', onMouseEnter);
|
||||||
|
container.removeEventListener('mouseleave', onMouseLeave);
|
||||||
|
container.removeEventListener('click', onClick);
|
||||||
|
window.removeEventListener('deviceorientation', onDeviceOrientation);
|
||||||
|
window.removeEventListener('resize', onResize);
|
||||||
|
|
||||||
|
unwatchWebcam();
|
||||||
|
unwatchModels();
|
||||||
|
unwatchProps();
|
||||||
|
|
||||||
|
stopWebcam();
|
||||||
|
|
||||||
|
material.dispose();
|
||||||
|
geometry.dispose();
|
||||||
|
if (composer) composer.dispose();
|
||||||
|
renderer.dispose();
|
||||||
|
if (container.contains(renderer.domElement)) {
|
||||||
|
container.removeChild(renderer.domElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFaceModels = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
faceapi.nets.tinyFaceDetector.loadFromUri(props.modelsPath),
|
||||||
|
faceapi.nets.faceLandmark68TinyNet.loadFromUri(props.modelsPath)
|
||||||
|
]);
|
||||||
|
modelsReady.value = true;
|
||||||
|
} catch {
|
||||||
|
modelsReady.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setupAnimation();
|
||||||
|
loadFaceModels();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanupAnimation();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
176
src/demo/Backgrounds/GridScanDemo.vue
Normal file
176
src/demo/Backgrounds/GridScanDemo.vue
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<template>
|
||||||
|
<TabbedLayout>
|
||||||
|
<template #preview>
|
||||||
|
<div class="h-[600px] overflow-hidden demo-container relative">
|
||||||
|
<GridScan
|
||||||
|
:line-thickness="lineThickness"
|
||||||
|
:grid-scale="gridScale"
|
||||||
|
:line-jitter="lineJitter"
|
||||||
|
:lines-color="linesColor"
|
||||||
|
:scan-color="scanColor"
|
||||||
|
:enable-post="enablePost"
|
||||||
|
:chromatic-aberration="chromaticAberration"
|
||||||
|
:noise-intensity="noiseIntensity"
|
||||||
|
:scan-glow="scanGlow"
|
||||||
|
:scan-softness="scanSoftness"
|
||||||
|
:enable-webcam="enableWebcam"
|
||||||
|
:show-preview="showPreview"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BackgroundContent pill-text="New Background" headline="Hold on, scanning for Angular users." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Customize>
|
||||||
|
<PreviewColor title="Lines Color" v-model="linesColor" class="mb-4" />
|
||||||
|
<PreviewColor title="Scan Color" v-model="scanColor" />
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Line Thickness"
|
||||||
|
v-model="lineThickness"
|
||||||
|
:min="1"
|
||||||
|
:max="4"
|
||||||
|
:step="0.1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Grid Scale"
|
||||||
|
v-model="gridScale"
|
||||||
|
:min="0.02"
|
||||||
|
:max="0.5"
|
||||||
|
:step="0.01"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Line Jitter"
|
||||||
|
v-model="lineJitter"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Scan Glow"
|
||||||
|
v-model="scanGlow"
|
||||||
|
:min="0.1"
|
||||||
|
:max="3"
|
||||||
|
:step="0.1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Scan Softness"
|
||||||
|
v-model="scanSoftness"
|
||||||
|
:min="0.1"
|
||||||
|
:max="4"
|
||||||
|
:step="0.1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSwitch title="Enable Post" v-model="enablePost" />
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Chromatic Aberration"
|
||||||
|
v-model="chromaticAberration"
|
||||||
|
:min="0"
|
||||||
|
:max="0.01"
|
||||||
|
:step="0.0005"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Noise Intensity"
|
||||||
|
v-model="noiseIntensity"
|
||||||
|
:min="0"
|
||||||
|
:max="0.1"
|
||||||
|
:step="0.005"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSwitch title="Enable Webcam" v-model="enableWebcam" />
|
||||||
|
<PreviewSwitch title="Show Preview HUD" v-model="showPreview" />
|
||||||
|
</Customize>
|
||||||
|
|
||||||
|
<PropTable :data="propData" />
|
||||||
|
<Dependencies :dependency-list="['three', 'postprocessing', 'face-api.js']" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #code>
|
||||||
|
<CodeExample :code-object="gridScan" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cli>
|
||||||
|
<CliInstallation :command="gridScan.cli" />
|
||||||
|
</template>
|
||||||
|
</TabbedLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import TabbedLayout from '@/components/common/TabbedLayout.vue';
|
||||||
|
import PropTable from '@/components/common/PropTable.vue';
|
||||||
|
import Dependencies from '@/components/code/Dependencies.vue';
|
||||||
|
import CliInstallation from '@/components/code/CliInstallation.vue';
|
||||||
|
import CodeExample from '@/components/code/CodeExample.vue';
|
||||||
|
import Customize from '@/components/common/Customize.vue';
|
||||||
|
import PreviewSwitch from '@/components/common/PreviewSwitch.vue';
|
||||||
|
import PreviewSlider from '@/components/common/PreviewSlider.vue';
|
||||||
|
import PreviewColor from '@/components/common/PreviewColor.vue';
|
||||||
|
import BackgroundContent from '@/components/common/BackgroundContent.vue';
|
||||||
|
import GridScan from '../../content/Backgrounds/GridScan/GridScan.vue';
|
||||||
|
import { gridScan } from '@/constants/code/Backgrounds/gridScanCode';
|
||||||
|
|
||||||
|
const lineThickness = ref(1);
|
||||||
|
const gridScale = ref(0.1);
|
||||||
|
const lineJitter = ref(0.1);
|
||||||
|
const linesColor = ref('#392e4e');
|
||||||
|
const scanColor = ref('#FF9FFC');
|
||||||
|
const enablePost = ref(true);
|
||||||
|
const chromaticAberration = ref(0.002);
|
||||||
|
const noiseIntensity = ref(0.01);
|
||||||
|
const scanGlow = ref(0.5);
|
||||||
|
const scanSoftness = ref(2);
|
||||||
|
const enableWebcam = ref(false);
|
||||||
|
const showPreview = ref(false);
|
||||||
|
|
||||||
|
const propData = [
|
||||||
|
{ name: 'enableWebcam', type: 'boolean', default: 'false', description: 'Enable face tracking via webcam.' },
|
||||||
|
{ name: 'showPreview', type: 'boolean', default: 'false', description: 'Show webcam preview/debug HUD.' },
|
||||||
|
{
|
||||||
|
name: 'modelsPath',
|
||||||
|
type: 'string',
|
||||||
|
default: 'CDN URL',
|
||||||
|
description: 'Path/URL to face-api.js models.'
|
||||||
|
},
|
||||||
|
{ name: 'sensitivity', type: 'number', default: '0.55', description: 'Overall responsiveness to input.' },
|
||||||
|
{ name: 'lineThickness', type: 'number', default: '1', description: 'Grid line thickness.' },
|
||||||
|
{ name: 'linesColor', type: 'string', default: "'#392e4e'", description: 'Color of the grid lines.' },
|
||||||
|
{ name: 'gridScale', type: 'number', default: '0.1', description: 'Grid spacing scale (smaller = denser).' },
|
||||||
|
{ name: 'lineStyle', type: "'solid' | 'dashed' | 'dotted'", default: "'solid'", description: 'Grid line style.' },
|
||||||
|
{ name: 'lineJitter', type: 'number', default: '0.1', description: 'Animated jitter along the grid lines.' },
|
||||||
|
{ name: 'enablePost', type: 'boolean', default: 'true', description: 'Enable post-processing effects.' },
|
||||||
|
{ name: 'bloomIntensity', type: 'number', default: '0', description: 'Bloom strength.' },
|
||||||
|
{ name: 'bloomThreshold', type: 'number', default: '0', description: 'Bloom luminance threshold.' },
|
||||||
|
{ name: 'bloomSmoothing', type: 'number', default: '0', description: 'Bloom threshold smoothing.' },
|
||||||
|
{
|
||||||
|
name: 'chromaticAberration',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.002',
|
||||||
|
description: 'Chromatic aberration offset (post).'
|
||||||
|
},
|
||||||
|
{ name: 'noiseIntensity', type: 'number', default: '0.01', description: 'Additive film grain intensity.' },
|
||||||
|
{ name: 'scanColor', type: 'string', default: "'#FF9FFC'", description: 'Color of the scan beam/aura.' },
|
||||||
|
{ name: 'scanOpacity', type: 'number', default: '0.4', description: 'Opacity of the scan effect.' },
|
||||||
|
{
|
||||||
|
name: 'scanDirection',
|
||||||
|
type: "'forward' | 'backward' | 'pingpong'",
|
||||||
|
default: "'pingpong'",
|
||||||
|
description: 'Scan motion.'
|
||||||
|
},
|
||||||
|
{ name: 'scanSoftness', type: 'number', default: '2', description: 'Softness of scan band edges.' },
|
||||||
|
{ name: 'scanGlow', type: 'number', default: '0.5', description: 'Relative width/intensity of glow.' },
|
||||||
|
{ name: 'scanPhaseTaper', type: 'number', default: '0.9', description: 'Fade-in/out window for the phase.' },
|
||||||
|
{ name: 'scanDuration', type: 'number', default: '2.0', description: 'Duration of a scan cycle (seconds).' },
|
||||||
|
{ name: 'scanDelay', type: 'number', default: '2.0', description: 'Delay between scan cycles (seconds).' },
|
||||||
|
{ name: 'enableGyro', type: 'boolean', default: 'false', description: 'Use device orientation for input.' },
|
||||||
|
{ name: 'scanOnClick', type: 'boolean', default: 'false', description: 'Trigger a scan when clicking.' },
|
||||||
|
{ name: 'snapBackDelay', type: 'number', default: '250', description: 'Delay (ms) before input recenters.' },
|
||||||
|
{ name: 'className', type: 'string', default: "''", description: 'Additional CSS classes.' },
|
||||||
|
{ name: 'style', type: 'CSSProperties', default: '{}', description: 'Inline style overrides.' }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user