diff --git a/.gitignore b/.gitignore
index e0f570c..4c4e804 100644
--- a/.gitignore
+++ b/.gitignore
@@ -206,5 +206,4 @@ marimo/_static/
marimo/_lsp/
__marimo__/
-assets/
_vite
diff --git a/README-zh_CN.md b/README-zh_CN.md
index 03c8ef4..c9ac763 100644
--- a/README-zh_CN.md
+++ b/README-zh_CN.md
@@ -103,8 +103,12 @@ pip install dash-vite-plugin
有关详细的使用示例,请参阅示例文件:
-- [Vue.js 示例](example_vue.py) - 演示如何将插件与 Vue.js 一起使用
-- [React 示例](example_react.py) - 演示如何将插件与 React 一起使用
+- [React Basic Demo](./examples/react-basic-demo) - 集成`React`的基础示例
+- [React Bits Grid Scan Demo](./examples/react-bits-grid-scan-demo) - 示例:集成使用`react-bits`中的`GridScan`组件
+- [React Bits Lightning Demo](./examples/react-bits-lightning-demo) - 示例:集成使用`react-bits`中的`Lightning`组件
+- [React Bits Shiny Text Demo](./examples/react-bits-shiny-text-demo) - 示例:集成使用`react-bits`中的`ShinyText`组件
+- [Vue Basic Demo](./examples/vue-basic-demo) - 集成`Vue`的基础示例
+
这些示例展示了如何使用不同前端框架设置插件,并包含测试回调以验证集成是否正常工作。
diff --git a/README.md b/README.md
index 5306178..8c0b209 100644
--- a/README.md
+++ b/README.md
@@ -103,8 +103,11 @@ Note: This plugin requires Python 3.8 or higher.
For detailed usage examples, please refer to the example files:
-- [Vue.js Example](example_vue.py) - Demonstrates how to use the plugin with Vue.js
-- [React Example](example_react.py) - Demonstrates how to use the plugin with React
+- [React Basic Demo](./examples/react-basic-demo) - Basic usage with React
+- [React Bits Grid Scan Demo](./examples/react-bits-grid-scan-demo) - Example: integrating `react-bits` GridScan component
+- [React Bits Lightning Demo](./examples/react-bits-lightning-demo) - Example: integrating `react-bits` Lightning component
+- [React Bits Shiny Text Demo](./examples/react-bits-shiny-text-demo) - Example: integrating `react-bits` ShinyText component
+- [Vue Basic Demo](./examples/vue-basic-demo) - Basic usage with Vue
These examples show how to set up the plugin with different frontend frameworks and include test callbacks to verify that the integration is working correctly.
diff --git a/example_react.py b/example_react.py
deleted file mode 100644
index e88ff28..0000000
--- a/example_react.py
+++ /dev/null
@@ -1,157 +0,0 @@
-import os
-from dash import Dash, html, Input, Output
-from dash_vite_plugin import VitePlugin, NpmPackage
-
-
-# Create assets structure
-os.makedirs('assets/js', exist_ok=True)
-os.makedirs('assets/react', exist_ok=True)
-
-# Create a simple React component
-with open('assets/react/App.jsx', 'w') as f:
- f.write("""import React, { useState } from 'react';
-
-const App = () => {
- const [message, setMessage] = useState("Hello from React!");
-
- const updateDash = () => {
- setMessage("Hello from React!");
- window.dash_clientside.set_props('dash-title', { children: 'Hello from React!' });
- };
-
- const updateMessage = () => {
- setMessage("Hello from Dash!");
- };
-
- return (
-
-
{message}
-
-
-
-
-
-
-
- );
-};
-
-export default App;
- """)
-
-# Create JavaScript file that imports and renders React
-with open('assets/js/main.js', 'w') as f:
- f.write("""import React from "react";
-import { createRoot } from "react-dom/client";
-import App from "../react/App.jsx";
-
-// Add a global variable to test JavaScript execution
-window.testVariable = "VitePluginReactTest";
-
-// Wait for the mount point to appear and then create the application
-function waitForElement(selector) {
- return new Promise((resolve) => {
- const element = document.querySelector(selector);
- if (element) {
- resolve(element);
- return;
- }
-
- const observer = new MutationObserver((mutations) => {
- const targetElement = document.querySelector(selector);
- if (targetElement) {
- observer.disconnect();
- resolve(targetElement);
- }
- });
-
- observer.observe(document.body, {
- childList: true,
- subtree: true,
- });
- });
-}
-
-// Wait for the mount point to appear and then create the application
-waitForElement("#react-container")
- .then((element) => {
- const root = createRoot(element);
- root.render(React.createElement(App));
- })
- .catch((error) => {
- console.error("Unable to find mount point:", error);
- });
- """)
-
-# Create VitePlugin instance with React support
-vite_plugin = VitePlugin(
- build_assets_paths=['assets/js', 'assets/react'],
- entry_js_paths=['assets/js/main.js'],
- npm_packages=[
- NpmPackage('react'),
- NpmPackage('react-dom'),
- ],
- download_node=True,
- clean_after=True,
-)
-
-# Call setup BEFORE creating Dash app (as required by the plugin architecture)
-vite_plugin.setup()
-
-# Create a Dash app
-app = Dash(__name__)
-
-# Call use AFTER creating Dash app (as required by the plugin architecture)
-vite_plugin.use(app)
-
-# Define app layout with a container for React
-app.layout = html.Div(
- [
- html.H1('Vite Plugin Test - React Support', id='header'),
- html.P('This tests the Vite plugin with React support.', id='paragraph'),
- # Container for React app
- html.Div(
- [
- 'The content from React',
- html.Div(id='react-container'),
- ]
- ),
- html.Div(
- [
- 'The content from Dash',
- html.Div(
- [html.H1('Hello from Dash!', id='dash-title'), html.Button('Control React', id='dash-button')],
- id='dash-app',
- style={'margin': '20px'},
- ),
- ],
- id='dash-container',
- ),
- ]
-)
-
-
-# Add callback to test React functionality with a simpler approach
-app.clientside_callback(
- """
- function(n_clicks) {
- if (n_clicks > 0) {
- const reactApp = document.getElementById('react-app');
- if (reactApp) {
- const button = reactApp.querySelector('#control-react-button');
- if (button) {
- button.click();
- return 'Hello from Dash!';
- }
- }
- }
- return 'Hello from Dash!';
- }
- """,
- Output('dash-title', 'children'),
- Input('dash-button', 'n_clicks'),
-)
-
-
-if __name__ == '__main__':
- app.run(debug=True)
diff --git a/example_vue.py b/example_vue.py
deleted file mode 100644
index ed487df..0000000
--- a/example_vue.py
+++ /dev/null
@@ -1,162 +0,0 @@
-import os
-from dash import Dash, html, Input, Output
-from dash_vite_plugin import VitePlugin, NpmPackage
-
-
-# Create assets structure
-os.makedirs('assets/js', exist_ok=True)
-os.makedirs('assets/vue', exist_ok=True)
-
-# Create a simple Vue component using Vue 3 Composition API
-with open('assets/vue/App.vue', 'w') as f:
- f.write("""
-
-
{{ message }}
-
-
-
-
-
-
-
-
-
-
-
-
- """)
-
-# Create JavaScript file that imports and mounts Vue
-with open('assets/js/main.js', 'w') as f:
- f.write("""import { createApp } from "vue";
-import App from "../vue/App.vue";
-
-// Add a global variable to test JavaScript execution
-window.testVariable = "VitePluginVueTest";
-
-// Mount the Vue app
-function waitForElement(selector) {
- return new Promise((resolve) => {
- const element = document.querySelector(selector);
- if (element) {
- resolve(element);
- return;
- }
-
- const observer = new MutationObserver((mutations) => {
- const targetElement = document.querySelector(selector);
- if (targetElement) {
- observer.disconnect();
- resolve(targetElement);
- }
- });
-
- observer.observe(document.body, {
- childList: true,
- subtree: true,
- });
- });
-}
-
-// Wait for the mount point to appear and then create the application
-waitForElement("#vue-container")
- .then((element) => {
- const app = createApp(App);
- app.mount(element);
- })
- .catch((error) => {
- console.error("Unable to find mount point:", error);
- });
- """)
-
-# Create VitePlugin instance with Vue support
-vite_plugin = VitePlugin(
- build_assets_paths=['assets/js', 'assets/vue'],
- entry_js_paths=['assets/js/main.js'],
- npm_packages=[
- NpmPackage('vue'),
- ],
- download_node=True,
- clean_after=True,
-)
-
-# Call setup BEFORE creating Dash app (as required by the plugin architecture)
-vite_plugin.setup()
-
-# Create a Dash app
-app = Dash(__name__)
-
-# Call use AFTER creating Dash app (as required by the plugin architecture)
-vite_plugin.use(app)
-
-# Define app layout with a container for Vue
-app.layout = html.Div(
- [
- html.H1('Vite Plugin Test - Vue Support', id='header'),
- html.P('This tests the Vite plugin with Vue support.', id='paragraph'),
- # Container for Vue app
- html.Div(
- [
- 'The content from Vue',
- html.Div(id='vue-container'),
- ]
- ),
- html.Div(
- [
- 'The content from Dash',
- html.Div(
- [html.H1('Hello from Dash!', id='dash-title'), html.Button('Control Vue', id='dash-button')],
- id='dash-app',
- style={'margin': '20px'},
- ),
- ],
- id='dash-container',
- ),
- ]
-)
-
-
-# Add callback to test Vue functionality with a simpler approach
-app.clientside_callback(
- """
- function(n_clicks) {
- if (n_clicks > 0) {
- const vueApp = document.getElementById('vue-app');
- if (vueApp) {
- const button = vueApp.querySelector('#control-vue-button');
- if (button) {
- button.click();
- return 'Hello from Dash!';
- }
- }
- }
- return 'Hello from Dash!';
- }
- """,
- Output('dash-title', 'children'),
- Input('dash-button', 'n_clicks'),
-)
-
-
-if __name__ == '__main__':
- app.run(debug=True)
diff --git a/examples/react-basic-demo/app.py b/examples/react-basic-demo/app.py
new file mode 100644
index 0000000..c1e9394
--- /dev/null
+++ b/examples/react-basic-demo/app.py
@@ -0,0 +1,77 @@
+import dash
+from dash import html
+from dash.dependencies import Input, Output
+from dash_vite_plugin import VitePlugin, NpmPackage
+
+# Create VitePlugin instance with React support
+vite_plugin = VitePlugin(
+ build_assets_paths=['assets/js', 'assets/react'],
+ entry_js_paths=['assets/js/main.js'],
+ npm_packages=[
+ NpmPackage('react'),
+ NpmPackage('react-dom'),
+ ],
+ download_node=True,
+ clean_after=True,
+)
+
+# Call setup BEFORE creating Dash app (as required by the plugin architecture)
+vite_plugin.setup()
+
+# Create a Dash app
+app = dash.Dash(__name__)
+
+# Call use AFTER creating Dash app (as required by the plugin architecture)
+vite_plugin.use(app)
+
+# Define app layout with a container for React
+app.layout = html.Div(
+ [
+ html.H1('Vite Plugin Test - React Support', id='header'),
+ html.P('This tests the Vite plugin with React support.', id='paragraph'),
+ # Container for React app
+ html.Div(
+ [
+ 'The content from React',
+ html.Div(id='react-container'),
+ ]
+ ),
+ html.Div(
+ [
+ 'The content from Dash',
+ html.Div(
+ [html.H1('Hello from Dash!', id='dash-title'), html.Button('Control React', id='dash-button')],
+ id='dash-app',
+ style={'margin': '20px'},
+ ),
+ ],
+ id='dash-container',
+ ),
+ ]
+)
+
+
+# Add callback to test React functionality with a simpler approach
+app.clientside_callback(
+ """
+ function(n_clicks) {
+ if (n_clicks > 0) {
+ const reactApp = document.getElementById('react-app');
+ if (reactApp) {
+ const button = reactApp.querySelector('#control-react-button');
+ if (button) {
+ button.click();
+ return 'Hello from Dash!';
+ }
+ }
+ }
+ return 'Hello from Dash!';
+ }
+ """,
+ Output('dash-title', 'children'),
+ Input('dash-button', 'n_clicks'),
+)
+
+
+if __name__ == '__main__':
+ app.run(debug=True)
diff --git a/examples/react-basic-demo/assets/js/main.js b/examples/react-basic-demo/assets/js/main.js
new file mode 100644
index 0000000..1015043
--- /dev/null
+++ b/examples/react-basic-demo/assets/js/main.js
@@ -0,0 +1,40 @@
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "../react/App.jsx";
+
+// Add a global variable to test JavaScript execution
+window.testVariable = "VitePluginReactTest";
+
+// Wait for the mount point to appear and then create the application
+function waitForElement(selector) {
+ return new Promise((resolve) => {
+ const element = document.querySelector(selector);
+ if (element) {
+ resolve(element);
+ return;
+ }
+
+ const observer = new MutationObserver((mutations) => {
+ const targetElement = document.querySelector(selector);
+ if (targetElement) {
+ observer.disconnect();
+ resolve(targetElement);
+ }
+ });
+
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true,
+ });
+ });
+}
+
+// Wait for the mount point to appear and then create the application
+waitForElement("#react-container")
+ .then((element) => {
+ const root = createRoot(element);
+ root.render(React.createElement(App));
+ })
+ .catch((error) => {
+ console.error("Unable to find mount point:", error);
+ });
diff --git a/examples/react-basic-demo/assets/react/App.jsx b/examples/react-basic-demo/assets/react/App.jsx
new file mode 100644
index 0000000..b7acdb7
--- /dev/null
+++ b/examples/react-basic-demo/assets/react/App.jsx
@@ -0,0 +1,28 @@
+import React, { useState } from 'react';
+
+const App = () => {
+ const [message, setMessage] = useState("Hello from React!");
+
+ const updateDash = () => {
+ setMessage("Hello from React!");
+ window.dash_clientside.set_props('dash-title', { children: 'Hello from React!' });
+ };
+
+ const updateMessage = () => {
+ setMessage("Hello from Dash!");
+ };
+
+ return (
+
+
{message}
+
+
+
+
+
+
+
+ );
+};
+
+export default App;
\ No newline at end of file
diff --git a/examples/react-basic-demo/requirements.txt b/examples/react-basic-demo/requirements.txt
new file mode 100644
index 0000000..5f7b3fb
--- /dev/null
+++ b/examples/react-basic-demo/requirements.txt
@@ -0,0 +1 @@
+dash-vite-plugin>=0.1.3
\ No newline at end of file
diff --git a/examples/react-bits-grid-scan-demo/app.py b/examples/react-bits-grid-scan-demo/app.py
new file mode 100644
index 0000000..5c9347b
--- /dev/null
+++ b/examples/react-bits-grid-scan-demo/app.py
@@ -0,0 +1,41 @@
+import dash
+from dash import html
+import feffery_utils_components as fuc
+from dash_vite_plugin import VitePlugin, NpmPackage
+from dash.dependencies import Input, State, ClientsideFunction
+
+vite_plugin = VitePlugin(
+ build_assets_paths=['assets'],
+ entry_js_paths=['assets/callback.js'],
+ npm_packages=[
+ NpmPackage('react'),
+ NpmPackage('react-dom'),
+ NpmPackage('three'),
+ NpmPackage('postprocessing'),
+ NpmPackage('face-api.js'),
+ ],
+ download_node=True,
+ clean_after=False, # 开发阶段建议设置为False以加速构建
+)
+
+vite_plugin.setup()
+
+app = dash.Dash(__name__)
+
+vite_plugin.use(app)
+
+app.layout = html.Div(
+ [
+ fuc.FefferyTimeout(id='trigger-background-init', delay=100),
+ html.Div(id='background-mount'),
+ ]
+)
+
+app.clientside_callback(
+ ClientsideFunction(namespace='clientside', function_name='renderBackground'),
+ Input('trigger-background-init', 'timeoutCount'),
+ State('background-mount', 'id'),
+)
+
+if __name__ == '__main__':
+ app.run(debug=True)
diff --git a/examples/react-bits-grid-scan-demo/assets/callback.js b/examples/react-bits-grid-scan-demo/assets/callback.js
new file mode 100644
index 0000000..4e657d0
--- /dev/null
+++ b/examples/react-bits-grid-scan-demo/assets/callback.js
@@ -0,0 +1,15 @@
+import React from "react";
+import { createRoot } from "react-dom/client";
+import GridScanComponent from "./react_components/GridScan.jsx";
+
+window.dash_clientside = Object.assign({}, window.dash_clientside, {
+ clientside: {
+ renderBackground: (timeoutCount, id) => {
+ const container = document.getElementById(id);
+ const root = createRoot(container);
+ root.render(React.createElement(GridScanComponent));
+
+ return window.dash_clientside.no_update;
+ },
+ },
+});
diff --git a/examples/react-bits-grid-scan-demo/assets/react_components/GridScan.css b/examples/react-bits-grid-scan-demo/assets/react_components/GridScan.css
new file mode 100644
index 0000000..5974000
--- /dev/null
+++ b/examples/react-bits-grid-scan-demo/assets/react_components/GridScan.css
@@ -0,0 +1,44 @@
+.gridscan {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
+
+.gridscan__preview {
+ position: absolute;
+ right: 12px;
+ bottom: 12px;
+ width: 220px;
+ height: 132px;
+ border-radius: 8px;
+ overflow: hidden;
+ border: 1px solid rgba(255, 255, 255, 0.25);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
+ background: #000;
+ color: #fff;
+ font:
+ 12px/1.2 system-ui,
+ -apple-system,
+ Segoe UI,
+ Roboto,
+ sans-serif;
+ pointer-events: none;
+}
+
+.gridscan__video {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transform: scaleX(-1);
+}
+
+.gridscan__badge {
+ position: absolute;
+ left: 8px;
+ top: 8px;
+ padding: 2px 6px;
+ background: rgba(0, 0, 0, 0.5);
+ border-radius: 6px;
+ backdrop-filter: blur(4px);
+}
\ No newline at end of file
diff --git a/examples/react-bits-grid-scan-demo/assets/react_components/GridScan.jsx b/examples/react-bits-grid-scan-demo/assets/react_components/GridScan.jsx
new file mode 100644
index 0000000..456bce7
--- /dev/null
+++ b/examples/react-bits-grid-scan-demo/assets/react_components/GridScan.jsx
@@ -0,0 +1,915 @@
+import { useEffect, useRef, useState } from 'react';
+import { EffectComposer, RenderPass, EffectPass, BloomEffect, ChromaticAberrationEffect } from 'postprocessing';
+import * as THREE from 'three';
+import * as faceapi from 'face-api.js';
+import './GridScan.css';
+
+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 GridScan = ({
+ 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,
+ style
+}) => {
+ const containerRef = useRef(null);
+ const videoRef = useRef(null);
+
+ const rendererRef = useRef(null);
+ const materialRef = useRef(null);
+ const composerRef = useRef(null);
+ const bloomRef = useRef(null);
+ const chromaRef = useRef(null);
+ const rafRef = useRef(null);
+
+ const [modelsReady, setModelsReady] = useState(false);
+ const [uiFaceActive, setUiFaceActive] = useState(false);
+
+ const lookTarget = useRef(new THREE.Vector2(0, 0));
+ const tiltTarget = useRef(0);
+ const yawTarget = useRef(0);
+
+ const lookCurrent = useRef(new THREE.Vector2(0, 0));
+ const lookVel = useRef(new THREE.Vector2(0, 0));
+ const tiltCurrent = useRef(0);
+ const tiltVel = useRef(0);
+ const yawCurrent = useRef(0);
+ const yawVel = useRef(0);
+
+ const MAX_SCANS = 8;
+ const scanStartsRef = useRef([]);
+
+ const pushScan = t => {
+ const arr = scanStartsRef.current.slice();
+ if (arr.length >= MAX_SCANS) arr.shift();
+ arr.push(t);
+ scanStartsRef.current = arr;
+ if (materialRef.current) {
+ const u = materialRef.current.uniforms;
+ const buf = new Array(MAX_SCANS).fill(0);
+ for (let i = 0; i < arr.length && i < MAX_SCANS; i++) buf[i] = arr[i];
+ u.uScanStarts.value = buf;
+ u.uScanCount.value = arr.length;
+ }
+ };
+
+ const bufX = useRef([]);
+ const bufY = useRef([]);
+ const bufT = useRef([]);
+ const bufYaw = useRef([]);
+
+ const s = THREE.MathUtils.clamp(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 depthResponse = THREE.MathUtils.lerp(0.25, 0.45, s);
+ const smoothTime = THREE.MathUtils.lerp(0.45, 0.12, s);
+ const maxSpeed = Infinity;
+
+ const yBoost = THREE.MathUtils.lerp(1.2, 1.6, s);
+
+ useEffect(() => {
+ const el = containerRef.current;
+ if (!el) return;
+ let leaveTimer = null;
+ const onMove = e => {
+ if (uiFaceActive) return;
+ if (leaveTimer) {
+ clearTimeout(leaveTimer);
+ leaveTimer = null;
+ }
+ const rect = el.getBoundingClientRect();
+ const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1;
+ const ny = -(((e.clientY - rect.top) / rect.height) * 2 - 1);
+ lookTarget.current.set(nx, ny);
+ };
+ const onClick = async () => {
+ const nowSec = performance.now() / 1000;
+ if (scanOnClick) pushScan(nowSec);
+ if (
+ enableGyro &&
+ typeof window !== 'undefined' &&
+ window.DeviceOrientationEvent &&
+ DeviceOrientationEvent.requestPermission
+ ) {
+ try {
+ await DeviceOrientationEvent.requestPermission();
+ } catch {
+ // noop
+ }
+ }
+ };
+ const onEnter = () => {
+ if (leaveTimer) {
+ clearTimeout(leaveTimer);
+ leaveTimer = null;
+ }
+ };
+ const onLeave = () => {
+ if (uiFaceActive) return;
+ if (leaveTimer) clearTimeout(leaveTimer);
+ leaveTimer = window.setTimeout(
+ () => {
+ lookTarget.current.set(0, 0);
+ tiltTarget.current = 0;
+ yawTarget.current = 0;
+ },
+ Math.max(0, snapBackDelay || 0)
+ );
+ };
+ el.addEventListener('mousemove', onMove);
+ el.addEventListener('mouseenter', onEnter);
+ if (scanOnClick) el.addEventListener('click', onClick);
+ el.addEventListener('mouseleave', onLeave);
+ return () => {
+ el.removeEventListener('mousemove', onMove);
+ el.removeEventListener('mouseenter', onEnter);
+ el.removeEventListener('mouseleave', onLeave);
+ if (scanOnClick) el.removeEventListener('click', onClick);
+ if (leaveTimer) clearTimeout(leaveTimer);
+ };
+ }, [uiFaceActive, snapBackDelay, scanOnClick, enableGyro]);
+
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+
+ const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
+ rendererRef.current = renderer;
+ 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 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: lineThickness },
+ uLinesColor: { value: srgbColor(linesColor) },
+ uScanColor: { value: srgbColor(scanColor) },
+ uGridScale: { value: gridScale },
+ uLineStyle: { value: lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0 },
+ uLineJitter: { value: Math.max(0, Math.min(1, lineJitter || 0)) },
+ uScanOpacity: { value: scanOpacity },
+ uNoise: { value: noiseIntensity },
+ uBloomOpacity: { value: bloomIntensity },
+ uScanGlow: { value: scanGlow },
+ uScanSoftness: { value: scanSoftness },
+ uPhaseTaper: { value: scanPhaseTaper },
+ uScanDuration: { value: scanDuration },
+ uScanDelay: { value: scanDelay },
+ uScanDirection: { value: scanDirection === 'backward' ? 1 : 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
+ });
+ materialRef.current = material;
+
+ const scene = new THREE.Scene();
+ const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
+ const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);
+ scene.add(quad);
+
+ let composer = null;
+ if (enablePost) {
+ composer = new EffectComposer(renderer);
+ composerRef.current = composer;
+ const renderPass = new RenderPass(scene, camera);
+ composer.addPass(renderPass);
+
+ const bloom = new BloomEffect({
+ intensity: 1.0,
+ luminanceThreshold: bloomThreshold,
+ luminanceSmoothing: bloomSmoothing
+ });
+ bloom.blendMode.opacity.value = Math.max(0, bloomIntensity);
+ bloomRef.current = bloom;
+
+ const chroma = new ChromaticAberrationEffect({
+ offset: new THREE.Vector2(chromaticAberration, chromaticAberration),
+ radialModulation: true,
+ modulationOffset: 0.0
+ });
+ chromaRef.current = chroma;
+
+ const effectPass = new EffectPass(camera, bloom, chroma);
+ effectPass.renderToScreen = true;
+ composer.addPass(effectPass);
+ }
+
+ const onResize = () => {
+ renderer.setSize(container.clientWidth, container.clientHeight);
+ material.uniforms.iResolution.value.set(container.clientWidth, container.clientHeight, renderer.getPixelRatio());
+ if (composerRef.current) composerRef.current.setSize(container.clientWidth, container.clientHeight);
+ };
+ window.addEventListener('resize', onResize);
+
+ let last = performance.now();
+ const tick = () => {
+ const now = performance.now();
+ const dt = Math.max(0, Math.min(0.1, (now - last) / 1000));
+ last = now;
+
+ lookCurrent.current.copy(
+ smoothDampVec2(lookCurrent.current, lookTarget.current, lookVel.current, smoothTime, maxSpeed, dt)
+ );
+
+ const tiltSm = smoothDampFloat(
+ tiltCurrent.current,
+ tiltTarget.current,
+ { v: tiltVel.current },
+ smoothTime,
+ maxSpeed,
+ dt
+ );
+ tiltCurrent.current = tiltSm.value;
+ tiltVel.current = tiltSm.v;
+
+ const yawSm = smoothDampFloat(
+ yawCurrent.current,
+ yawTarget.current,
+ { v: yawVel.current },
+ smoothTime,
+ maxSpeed,
+ dt
+ );
+ yawCurrent.current = yawSm.value;
+ yawVel.current = yawSm.v;
+
+ const skew = new THREE.Vector2(lookCurrent.current.x * skewScale, -lookCurrent.current.y * yBoost * skewScale);
+ material.uniforms.uSkew.value.set(skew.x, skew.y);
+ material.uniforms.uTilt.value = tiltCurrent.current * tiltScale;
+ material.uniforms.uYaw.value = THREE.MathUtils.clamp(yawCurrent.current * yawScale, -0.6, 0.6);
+
+ material.uniforms.iTime.value = now / 1000;
+ renderer.clear(true, true, true);
+ if (composerRef.current) {
+ composerRef.current.render(dt);
+ } else {
+ renderer.render(scene, camera);
+ }
+ rafRef.current = requestAnimationFrame(tick);
+ };
+ rafRef.current = requestAnimationFrame(tick);
+
+ return () => {
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
+ window.removeEventListener('resize', onResize);
+ material.dispose();
+ quad.geometry.dispose();
+
+ if (composerRef.current) {
+ composerRef.current.dispose();
+ composerRef.current = null;
+ }
+ renderer.dispose();
+ container.removeChild(renderer.domElement);
+ };
+ }, [
+ sensitivity,
+ lineThickness,
+ linesColor,
+ scanColor,
+ scanOpacity,
+ gridScale,
+ lineStyle,
+ lineJitter,
+ scanDirection,
+ enablePost,
+ noiseIntensity,
+ bloomIntensity,
+ scanGlow,
+ scanSoftness,
+ scanPhaseTaper,
+ scanDuration,
+ scanDelay,
+ bloomThreshold,
+ bloomSmoothing,
+ chromaticAberration,
+ smoothTime,
+ maxSpeed,
+ skewScale,
+ yBoost,
+ tiltScale,
+ yawScale
+ ]);
+
+ useEffect(() => {
+ const m = materialRef.current;
+ if (m) {
+ const u = m.uniforms;
+ u.uLineThickness.value = lineThickness;
+ u.uLinesColor.value.copy(srgbColor(linesColor));
+ u.uScanColor.value.copy(srgbColor(scanColor));
+ u.uGridScale.value = gridScale;
+ u.uLineStyle.value = lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0;
+ u.uLineJitter.value = Math.max(0, Math.min(1, lineJitter || 0));
+ u.uBloomOpacity.value = Math.max(0, bloomIntensity);
+ u.uNoise.value = Math.max(0, noiseIntensity);
+ u.uScanGlow.value = scanGlow;
+ u.uScanOpacity.value = Math.max(0, Math.min(1, scanOpacity));
+ u.uScanDirection.value = scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0;
+ u.uScanSoftness.value = scanSoftness;
+ u.uPhaseTaper.value = scanPhaseTaper;
+ u.uScanDuration.value = Math.max(0.05, scanDuration);
+ u.uScanDelay.value = Math.max(0.0, scanDelay);
+ }
+ if (bloomRef.current) {
+ bloomRef.current.blendMode.opacity.value = Math.max(0, bloomIntensity);
+ bloomRef.current.luminanceMaterial.threshold = bloomThreshold;
+ bloomRef.current.luminanceMaterial.smoothing = bloomSmoothing;
+ }
+ if (chromaRef.current) {
+ chromaRef.current.offset.set(chromaticAberration, chromaticAberration);
+ }
+ }, [
+ lineThickness,
+ linesColor,
+ scanColor,
+ gridScale,
+ lineStyle,
+ lineJitter,
+ bloomIntensity,
+ bloomThreshold,
+ bloomSmoothing,
+ chromaticAberration,
+ noiseIntensity,
+ scanGlow,
+ scanOpacity,
+ scanDirection,
+ scanSoftness,
+ scanPhaseTaper,
+ scanDuration,
+ scanDelay
+ ]);
+
+ useEffect(() => {
+ if (!enableGyro) return;
+ const handler = e => {
+ if (uiFaceActive) 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.current.set(nx, ny);
+ tiltTarget.current = THREE.MathUtils.degToRad(gamma) * 0.4;
+ };
+ window.addEventListener('deviceorientation', handler);
+ return () => {
+ window.removeEventListener('deviceorientation', handler);
+ };
+ }, [enableGyro, uiFaceActive]);
+
+ useEffect(() => {
+ let canceled = false;
+ const load = async () => {
+ try {
+ await Promise.all([
+ faceapi.nets.tinyFaceDetector.loadFromUri(modelsPath),
+ faceapi.nets.faceLandmark68TinyNet.loadFromUri(modelsPath)
+ ]);
+ if (!canceled) setModelsReady(true);
+ } catch {
+ if (!canceled) setModelsReady(false);
+ }
+ };
+ load();
+ return () => {
+ canceled = true;
+ };
+ }, [modelsPath]);
+
+ useEffect(() => {
+ let stop = false;
+ let lastDetect = 0;
+ const video = videoRef.current;
+
+ const start = async () => {
+ if (!enableWebcam || !modelsReady) return;
+ 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 detect = async ts => {
+ if (stop) 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.current, nx, 5);
+ medianPush(bufY.current, ny, 5);
+ const nxm = median(bufX.current);
+ const nym = median(bufY.current);
+
+ 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 depthScale = 1 + depthResponse * (faceSize - 0.25);
+ lookTarget.current.copy(look.multiplyScalar(depthScale));
+
+ 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.current, tilt, 5);
+ tiltTarget.current = median(bufT.current);
+
+ 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.current, yawSignal, 5);
+ yawTarget.current = median(bufYaw.current);
+
+ setUiFaceActive(true);
+ } else {
+ setUiFaceActive(false);
+ }
+ } catch {
+ setUiFaceActive(false);
+ }
+ }
+
+ if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
+ video.requestVideoFrameCallback(() => detect(performance.now()));
+ } else {
+ requestAnimationFrame(detect);
+ }
+ };
+
+ requestAnimationFrame(detect);
+ };
+
+ start();
+
+ return () => {
+ stop = true;
+ if (video) {
+ const stream = video.srcObject;
+ if (stream) stream.getTracks().forEach(t => t.stop());
+ video.pause();
+ video.srcObject = null;
+ }
+ };
+ }, [enableWebcam, modelsReady, depthResponse]);
+
+ return (
+
+ {showPreview && (
+
+
+
+ {enableWebcam
+ ? modelsReady
+ ? uiFaceActive
+ ? 'Face: tracking'
+ : 'Face: searching'
+ : 'Loading models'
+ : 'Webcam disabled'}
+
+
+ )}
+
+ );
+};
+
+function srgbColor(hex) {
+ const c = new THREE.Color(hex);
+ return c.convertSRGBToLinear();
+}
+
+function smoothDampVec2(current, target, currentVelocity, smoothTime, maxSpeed, deltaTime) {
+ const out = current.clone();
+ smoothTime = Math.max(0.0001, smoothTime);
+ const omega = 2 / smoothTime;
+ 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 = maxSpeed * smoothTime;
+ 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;
+}
+
+function smoothDampFloat(current, target, velRef, smoothTime, maxSpeed, deltaTime) {
+ smoothTime = Math.max(0.0001, smoothTime);
+ const omega = 2 / smoothTime;
+ 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 = maxSpeed * smoothTime;
+ 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 };
+}
+
+function medianPush(buf, v, maxLen) {
+ buf.push(v);
+ if (buf.length > maxLen) buf.shift();
+}
+
+function median(buf) {
+ 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;
+}
+
+function centroid(points) {
+ 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 };
+}
+
+function dist2(a, b) {
+ return Math.hypot(a.x - b.x, a.y - b.y);
+}
+
+const GridScanComponent = () => {
+ return (
+
+
+
+ );
+};
+
+export default GridScanComponent;
\ No newline at end of file
diff --git a/examples/react-bits-grid-scan-demo/requirements.txt b/examples/react-bits-grid-scan-demo/requirements.txt
new file mode 100644
index 0000000..575eedf
--- /dev/null
+++ b/examples/react-bits-grid-scan-demo/requirements.txt
@@ -0,0 +1,2 @@
+dash-vite-plugin>=0.1.3
+feffery-utils-components>=0.3.4
\ No newline at end of file
diff --git a/examples/react-bits-lightning-demo/app.py b/examples/react-bits-lightning-demo/app.py
new file mode 100644
index 0000000..9deaac5
--- /dev/null
+++ b/examples/react-bits-lightning-demo/app.py
@@ -0,0 +1,38 @@
+import dash
+from dash import html
+import feffery_utils_components as fuc
+from dash_vite_plugin import VitePlugin, NpmPackage
+from dash.dependencies import Input, State, ClientsideFunction
+
+vite_plugin = VitePlugin(
+ build_assets_paths=['assets'],
+ entry_js_paths=['assets/callback.js'],
+ npm_packages=[
+ NpmPackage('react'),
+ NpmPackage('react-dom'),
+ ],
+ download_node=True,
+ clean_after=False, # 开发阶段建议设置为False以加速构建
+)
+
+vite_plugin.setup()
+
+app = dash.Dash(__name__)
+
+vite_plugin.use(app)
+
+app.layout = html.Div(
+ [
+ fuc.FefferyTimeout(id='trigger-background-init', delay=100),
+ html.Div(id='background-mount'),
+ ]
+)
+
+app.clientside_callback(
+ ClientsideFunction(namespace='clientside', function_name='renderBackground'),
+ Input('trigger-background-init', 'timeoutCount'),
+ State('background-mount', 'id'),
+)
+
+if __name__ == '__main__':
+ app.run(debug=True)
diff --git a/examples/react-bits-lightning-demo/assets/callback.js b/examples/react-bits-lightning-demo/assets/callback.js
new file mode 100644
index 0000000..d47fbcf
--- /dev/null
+++ b/examples/react-bits-lightning-demo/assets/callback.js
@@ -0,0 +1,15 @@
+import React from "react";
+import { createRoot } from "react-dom/client";
+import LightningComponent from "./react_components/Lightning.jsx";
+
+window.dash_clientside = Object.assign({}, window.dash_clientside, {
+ clientside: {
+ renderBackground: (timeoutCount, id) => {
+ const container = document.getElementById(id);
+ const root = createRoot(container);
+ root.render(React.createElement(LightningComponent));
+
+ return window.dash_clientside.no_update;
+ },
+ },
+});
diff --git a/examples/react-bits-lightning-demo/assets/react_components/Lightning.css b/examples/react-bits-lightning-demo/assets/react_components/Lightning.css
new file mode 100644
index 0000000..4e62440
--- /dev/null
+++ b/examples/react-bits-lightning-demo/assets/react_components/Lightning.css
@@ -0,0 +1,7 @@
+.lightning-container {
+ width: 100vw;
+ height: 100vh;
+ top: 0;
+ left: 0;
+ position: fixed;
+}
\ No newline at end of file
diff --git a/examples/react-bits-lightning-demo/assets/react_components/Lightning.jsx b/examples/react-bits-lightning-demo/assets/react_components/Lightning.jsx
new file mode 100644
index 0000000..d084abf
--- /dev/null
+++ b/examples/react-bits-lightning-demo/assets/react_components/Lightning.jsx
@@ -0,0 +1,193 @@
+import { useRef, useEffect } from 'react';
+import './Lightning.css';
+
+const Lightning = ({ hue = 230, xOffset = 0, speed = 1, intensity = 1, size = 1 }) => {
+ const canvasRef = useRef(null);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+
+ const resizeCanvas = () => {
+ canvas.width = canvas.clientWidth;
+ canvas.height = canvas.clientHeight;
+ };
+ resizeCanvas();
+ window.addEventListener('resize', resizeCanvas);
+
+ const gl = canvas.getContext('webgl');
+ if (!gl) {
+ console.error('WebGL not supported');
+ return;
+ }
+
+ const vertexShaderSource = `
+ attribute vec2 aPosition;
+ void main() {
+ gl_Position = vec4(aPosition, 0.0, 1.0);
+ }
+ `;
+
+ const fragmentShaderSource = `
+ precision mediump float;
+ uniform vec2 iResolution;
+ uniform float iTime;
+ uniform float uHue;
+ uniform float uXOffset;
+ uniform float uSpeed;
+ uniform float uIntensity;
+ uniform float uSize;
+
+ #define OCTAVE_COUNT 10
+
+ vec3 hsv2rgb(vec3 c) {
+ vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0,4.0,2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
+ return c.z * mix(vec3(1.0), rgb, c.y);
+ }
+
+ float hash11(float p) {
+ p = fract(p * .1031);
+ p *= p + 33.33;
+ p *= p + p;
+ return fract(p);
+ }
+
+ float hash12(vec2 p) {
+ vec3 p3 = fract(vec3(p.xyx) * .1031);
+ p3 += dot(p3, p3.yzx + 33.33);
+ return fract((p3.x + p3.y) * p3.z);
+ }
+
+ mat2 rotate2d(float theta) {
+ float c = cos(theta);
+ float s = sin(theta);
+ return mat2(c, -s, s, c);
+ }
+
+ float noise(vec2 p) {
+ vec2 ip = floor(p);
+ vec2 fp = fract(p);
+ float a = hash12(ip);
+ float b = hash12(ip + vec2(1.0, 0.0));
+ float c = hash12(ip + vec2(0.0, 1.0));
+ float d = hash12(ip + vec2(1.0, 1.0));
+
+ vec2 t = smoothstep(0.0, 1.0, fp);
+ return mix(mix(a, b, t.x), mix(c, d, t.x), t.y);
+ }
+
+ float fbm(vec2 p) {
+ float value = 0.0;
+ float amplitude = 0.5;
+ for (int i = 0; i < OCTAVE_COUNT; ++i) {
+ value += amplitude * noise(p);
+ p *= rotate2d(0.45);
+ p *= 2.0;
+ amplitude *= 0.5;
+ }
+ return value;
+ }
+
+ void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
+ vec2 uv = fragCoord / iResolution.xy;
+ uv = 2.0 * uv - 1.0;
+ uv.x *= iResolution.x / iResolution.y;
+ uv.x += uXOffset;
+
+ uv += 2.0 * fbm(uv * uSize + 0.8 * iTime * uSpeed) - 1.0;
+
+ float dist = abs(uv.x);
+ vec3 baseColor = hsv2rgb(vec3(uHue / 360.0, 0.7, 0.8));
+ vec3 col = baseColor * pow(mix(0.0, 0.07, hash11(iTime * uSpeed)) / dist, 1.0) * uIntensity;
+ col = pow(col, vec3(1.0));
+ fragColor = vec4(col, 1.0);
+ }
+
+ void main() {
+ mainImage(gl_FragColor, gl_FragCoord.xy);
+ }
+ `;
+
+ const compileShader = (source, type) => {
+ const shader = gl.createShader(type);
+ if (!shader) return null;
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+ console.error('Shader compile error:', gl.getShaderInfoLog(shader));
+ gl.deleteShader(shader);
+ return null;
+ }
+ return shader;
+ };
+
+ const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER);
+ const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);
+ if (!vertexShader || !fragmentShader) return;
+
+ const program = gl.createProgram();
+ if (!program) return;
+ gl.attachShader(program, vertexShader);
+ gl.attachShader(program, fragmentShader);
+ gl.linkProgram(program);
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+ console.error('Program linking error:', gl.getProgramInfoLog(program));
+ return;
+ }
+ gl.useProgram(program);
+
+ const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]);
+ const vertexBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
+
+ const aPosition = gl.getAttribLocation(program, 'aPosition');
+ gl.enableVertexAttribArray(aPosition);
+ gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
+
+ const iResolutionLocation = gl.getUniformLocation(program, 'iResolution');
+ const iTimeLocation = gl.getUniformLocation(program, 'iTime');
+ const uHueLocation = gl.getUniformLocation(program, 'uHue');
+ const uXOffsetLocation = gl.getUniformLocation(program, 'uXOffset');
+ const uSpeedLocation = gl.getUniformLocation(program, 'uSpeed');
+ const uIntensityLocation = gl.getUniformLocation(program, 'uIntensity');
+ const uSizeLocation = gl.getUniformLocation(program, 'uSize');
+
+ const startTime = performance.now();
+ const render = () => {
+ resizeCanvas();
+ gl.viewport(0, 0, canvas.width, canvas.height);
+ gl.uniform2f(iResolutionLocation, canvas.width, canvas.height);
+ const currentTime = performance.now();
+ gl.uniform1f(iTimeLocation, (currentTime - startTime) / 1000.0);
+ gl.uniform1f(uHueLocation, hue);
+ gl.uniform1f(uXOffsetLocation, xOffset);
+ gl.uniform1f(uSpeedLocation, speed);
+ gl.uniform1f(uIntensityLocation, intensity);
+ gl.uniform1f(uSizeLocation, size);
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
+ requestAnimationFrame(render);
+ };
+ requestAnimationFrame(render);
+
+ return () => {
+ window.removeEventListener('resize', resizeCanvas);
+ };
+ }, [hue, xOffset, speed, intensity, size]);
+
+ return ;
+};
+
+const LightningComponent = () => {
+ return (
+
+ );
+};
+
+export default LightningComponent;
\ No newline at end of file
diff --git a/examples/react-bits-lightning-demo/requirements.txt b/examples/react-bits-lightning-demo/requirements.txt
new file mode 100644
index 0000000..575eedf
--- /dev/null
+++ b/examples/react-bits-lightning-demo/requirements.txt
@@ -0,0 +1,2 @@
+dash-vite-plugin>=0.1.3
+feffery-utils-components>=0.3.4
\ No newline at end of file
diff --git a/examples/react-bits-shiny-text-demo/app.py b/examples/react-bits-shiny-text-demo/app.py
new file mode 100644
index 0000000..09d3ca5
--- /dev/null
+++ b/examples/react-bits-shiny-text-demo/app.py
@@ -0,0 +1,53 @@
+import dash
+from dash import html
+import feffery_antd_components as fac
+import feffery_utils_components as fuc
+from feffery_dash_utils.style_utils import style
+from dash_vite_plugin import VitePlugin, NpmPackage
+from dash.dependencies import Input, State, ClientsideFunction
+
+
+vite_plugin = VitePlugin(
+ build_assets_paths=['assets'],
+ entry_js_paths=['assets/callback.js'],
+ npm_packages=[
+ NpmPackage('react'),
+ NpmPackage('react-dom'),
+ NpmPackage('motion'),
+ ],
+ download_node=True,
+ clean_after=False, # 开发阶段建议设置为False以加速构建
+)
+
+vite_plugin.setup()
+
+app = dash.Dash(__name__)
+
+vite_plugin.use(app)
+
+app.layout = html.Div(
+ [
+ fuc.FefferyTimeout(id='trigger-text-init', delay=100),
+ fac.AntdCenter(
+ id='text-mount',
+ style=style(
+ position='fixed',
+ width='100vw',
+ height='100vh',
+ left=0,
+ top=0,
+ background='#060010',
+ fontSize=56,
+ ),
+ ),
+ ]
+)
+
+app.clientside_callback(
+ ClientsideFunction(namespace='clientside', function_name='renderText'),
+ Input('trigger-text-init', 'timeoutCount'),
+ State('text-mount', 'id'),
+)
+
+if __name__ == '__main__':
+ app.run(debug=True)
diff --git a/examples/react-bits-shiny-text-demo/assets/callback.js b/examples/react-bits-shiny-text-demo/assets/callback.js
new file mode 100644
index 0000000..1ef4ad7
--- /dev/null
+++ b/examples/react-bits-shiny-text-demo/assets/callback.js
@@ -0,0 +1,15 @@
+import React from "react";
+import { createRoot } from "react-dom/client";
+import ShinyTextComponent from "./react_components/ShinyText.jsx";
+
+window.dash_clientside = Object.assign({}, window.dash_clientside, {
+ clientside: {
+ renderText: (timeoutCount, id) => {
+ const container = document.getElementById(id);
+ const root = createRoot(container);
+ root.render(React.createElement(ShinyTextComponent));
+
+ return window.dash_clientside.no_update;
+ },
+ },
+});
diff --git a/examples/react-bits-shiny-text-demo/assets/react_components/ShinyText.css b/examples/react-bits-shiny-text-demo/assets/react_components/ShinyText.css
new file mode 100644
index 0000000..9d07f2d
--- /dev/null
+++ b/examples/react-bits-shiny-text-demo/assets/react_components/ShinyText.css
@@ -0,0 +1,3 @@
+.shiny-text {
+ display: inline-block;
+}
\ No newline at end of file
diff --git a/examples/react-bits-shiny-text-demo/assets/react_components/ShinyText.jsx b/examples/react-bits-shiny-text-demo/assets/react_components/ShinyText.jsx
new file mode 100644
index 0000000..a5fc9e7
--- /dev/null
+++ b/examples/react-bits-shiny-text-demo/assets/react_components/ShinyText.jsx
@@ -0,0 +1,133 @@
+import { useState, useCallback, useEffect, useRef } from 'react';
+import { motion, useMotionValue, useAnimationFrame, useTransform } from 'motion/react';
+import './ShinyText.css';
+
+const ShinyText = ({
+ text,
+ disabled = false,
+ speed = 2,
+ className = '',
+ color = '#b5b5b5',
+ shineColor = '#ffffff',
+ spread = 120,
+ yoyo = false,
+ pauseOnHover = false,
+ direction = 'left',
+ delay = 0
+}) => {
+ const [isPaused, setIsPaused] = useState(false);
+ const progress = useMotionValue(0);
+ const elapsedRef = useRef(0);
+ const lastTimeRef = useRef(null);
+ const directionRef = useRef(direction === 'left' ? 1 : -1);
+
+ const animationDuration = speed * 1000;
+ const delayDuration = delay * 1000;
+
+ useAnimationFrame(time => {
+ if (disabled || isPaused) {
+ lastTimeRef.current = null;
+ return;
+ }
+
+ if (lastTimeRef.current === null) {
+ lastTimeRef.current = time;
+ return;
+ }
+
+ const deltaTime = time - lastTimeRef.current;
+ lastTimeRef.current = time;
+
+ elapsedRef.current += deltaTime;
+
+ if (yoyo) {
+ const cycleDuration = animationDuration + delayDuration;
+ const fullCycle = cycleDuration * 2;
+ const cycleTime = elapsedRef.current % fullCycle;
+
+ if (cycleTime < animationDuration) {
+ // Forward animation: 0 -> 100
+ const p = (cycleTime / animationDuration) * 100;
+ progress.set(directionRef.current === 1 ? p : 100 - p);
+ } else if (cycleTime < cycleDuration) {
+ // Delay at end
+ progress.set(directionRef.current === 1 ? 100 : 0);
+ } else if (cycleTime < cycleDuration + animationDuration) {
+ // Reverse animation: 100 -> 0
+ const reverseTime = cycleTime - cycleDuration;
+ const p = 100 - (reverseTime / animationDuration) * 100;
+ progress.set(directionRef.current === 1 ? p : 100 - p);
+ } else {
+ // Delay at start
+ progress.set(directionRef.current === 1 ? 0 : 100);
+ }
+ } else {
+ const cycleDuration = animationDuration + delayDuration;
+ const cycleTime = elapsedRef.current % cycleDuration;
+
+ if (cycleTime < animationDuration) {
+ // Animation phase: 0 -> 100
+ const p = (cycleTime / animationDuration) * 100;
+ progress.set(directionRef.current === 1 ? p : 100 - p);
+ } else {
+ // Delay phase - hold at end (shine off-screen)
+ progress.set(directionRef.current === 1 ? 100 : 0);
+ }
+ }
+ });
+
+ useEffect(() => {
+ directionRef.current = direction === 'left' ? 1 : -1;
+ elapsedRef.current = 0;
+ progress.set(0);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [direction]);
+
+ // Transform: p=0 -> 150% (shine off right), p=100 -> -50% (shine off left)
+ const backgroundPosition = useTransform(progress, p => `${150 - p * 2}% center`);
+
+ const handleMouseEnter = useCallback(() => {
+ if (pauseOnHover) setIsPaused(true);
+ }, [pauseOnHover]);
+
+ const handleMouseLeave = useCallback(() => {
+ if (pauseOnHover) setIsPaused(false);
+ }, [pauseOnHover]);
+
+ const gradientStyle = {
+ backgroundImage: `linear-gradient(${spread}deg, ${color} 0%, ${color} 35%, ${shineColor} 50%, ${color} 65%, ${color} 100%)`,
+ backgroundSize: '200% auto',
+ WebkitBackgroundClip: 'text',
+ backgroundClip: 'text',
+ WebkitTextFillColor: 'transparent'
+ };
+
+ return (
+
+ {text}
+
+ );
+};
+
+const ShinyTextComponent = () => {
+ return (
+
+ );
+};
+
+export default ShinyTextComponent;
\ No newline at end of file
diff --git a/examples/react-bits-shiny-text-demo/requirements.txt b/examples/react-bits-shiny-text-demo/requirements.txt
new file mode 100644
index 0000000..ca1fd63
--- /dev/null
+++ b/examples/react-bits-shiny-text-demo/requirements.txt
@@ -0,0 +1,4 @@
+dash-vite-plugin>=0.1.3
+feffery-antd-components>=0.4.2
+feffery-dash-utils>=0.2.6
+feffery-utils-components>=0.3.4
\ No newline at end of file
diff --git a/examples/vue-basic-demo/app.py b/examples/vue-basic-demo/app.py
new file mode 100644
index 0000000..7704a48
--- /dev/null
+++ b/examples/vue-basic-demo/app.py
@@ -0,0 +1,76 @@
+import dash
+from dash import html
+from dash.dependencies import Input, Output
+from dash_vite_plugin import VitePlugin, NpmPackage
+
+# Create VitePlugin instance with Vue support
+vite_plugin = VitePlugin(
+ build_assets_paths=['assets/js', 'assets/vue'],
+ entry_js_paths=['assets/js/main.js'],
+ npm_packages=[
+ NpmPackage('vue'),
+ ],
+ download_node=True,
+ clean_after=True,
+)
+
+# Call setup BEFORE creating Dash app (as required by the plugin architecture)
+vite_plugin.setup()
+
+# Create a Dash app
+app = dash.Dash(__name__)
+
+# Call use AFTER creating Dash app (as required by the plugin architecture)
+vite_plugin.use(app)
+
+# Define app layout with a container for Vue
+app.layout = html.Div(
+ [
+ html.H1('Vite Plugin Test - Vue Support', id='header'),
+ html.P('This tests the Vite plugin with Vue support.', id='paragraph'),
+ # Container for Vue app
+ html.Div(
+ [
+ 'The content from Vue',
+ html.Div(id='vue-container'),
+ ]
+ ),
+ html.Div(
+ [
+ 'The content from Dash',
+ html.Div(
+ [html.H1('Hello from Dash!', id='dash-title'), html.Button('Control Vue', id='dash-button')],
+ id='dash-app',
+ style={'margin': '20px'},
+ ),
+ ],
+ id='dash-container',
+ ),
+ ]
+)
+
+
+# Add callback to test Vue functionality with a simpler approach
+app.clientside_callback(
+ """
+ function(n_clicks) {
+ if (n_clicks > 0) {
+ const vueApp = document.getElementById('vue-app');
+ if (vueApp) {
+ const button = vueApp.querySelector('#control-vue-button');
+ if (button) {
+ button.click();
+ return 'Hello from Dash!';
+ }
+ }
+ }
+ return 'Hello from Dash!';
+ }
+ """,
+ Output('dash-title', 'children'),
+ Input('dash-button', 'n_clicks'),
+)
+
+
+if __name__ == '__main__':
+ app.run(debug=True)
diff --git a/examples/vue-basic-demo/assets/js/main.js b/examples/vue-basic-demo/assets/js/main.js
new file mode 100644
index 0000000..c92a342
--- /dev/null
+++ b/examples/vue-basic-demo/assets/js/main.js
@@ -0,0 +1,39 @@
+import { createApp } from "vue";
+import App from "../vue/App.vue";
+
+// Add a global variable to test JavaScript execution
+window.testVariable = "VitePluginVueTest";
+
+// Mount the Vue app
+function waitForElement(selector) {
+ return new Promise((resolve) => {
+ const element = document.querySelector(selector);
+ if (element) {
+ resolve(element);
+ return;
+ }
+
+ const observer = new MutationObserver((mutations) => {
+ const targetElement = document.querySelector(selector);
+ if (targetElement) {
+ observer.disconnect();
+ resolve(targetElement);
+ }
+ });
+
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true,
+ });
+ });
+}
+
+// Wait for the mount point to appear and then create the application
+waitForElement("#vue-container")
+ .then((element) => {
+ const app = createApp(App);
+ app.mount(element);
+ })
+ .catch((error) => {
+ console.error("Unable to find mount point:", error);
+ });
diff --git a/examples/vue-basic-demo/assets/vue/App.vue b/examples/vue-basic-demo/assets/vue/App.vue
new file mode 100644
index 0000000..9c816bf
--- /dev/null
+++ b/examples/vue-basic-demo/assets/vue/App.vue
@@ -0,0 +1,39 @@
+
+
+
{{ message }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/vue-basic-demo/requirements.txt b/examples/vue-basic-demo/requirements.txt
new file mode 100644
index 0000000..5f7b3fb
--- /dev/null
+++ b/examples/vue-basic-demo/requirements.txt
@@ -0,0 +1 @@
+dash-vite-plugin>=0.1.3
\ No newline at end of file