Manager
View Site
Name
Type
React (.jsx)
HTML (.html)
Icon
Description
Code Editor
Revision History (0)
import React, { useState, useMemo, useEffect } from 'react'; import { Activity, Move3d, Navigation, Lock, Unlock, Zap, Smartphone } from 'lucide-react'; // --- ISOMETRIC MATH HELPERS --- const COS30 = Math.cos(Math.PI / 6); const SIN30 = Math.sin(Math.PI / 6); const projectIso = (x, y, z) => ({ cx: (x - y) * COS30, cy: (x + y) * SIN30 - z }); const IsoPolygon = ({ points, ...props }) => { const d = points.map(p => { const proj = projectIso(p[0], p[1], p[2]); return `${proj.cx},${proj.cy}`; }).join(' '); return <polygon points={d} {...props} />; }; const IsoLine = ({ p1, p2, ...props }) => { const proj1 = projectIso(p1[0], p1[1], p1[2]); const proj2 = projectIso(p2[0], p2[1], p2[2]); return <line x1={proj1.cx} y1={proj1.cy} x2={proj2.cx} y2={proj2.cy} {...props} />; }; const ScreenRect = ({ cx, cz, w, h, ...props }) => { const p1 = [cx - w / 2, 0, cz + h / 2]; const p2 = [cx + w / 2, 0, cz + h / 2]; const p3 = [cx + w / 2, 0, cz - h / 2]; const p4 = [cx - w / 2, 0, cz - h / 2]; return <IsoPolygon points={[p1, p2, p3, p4]} {...props} />; }; const App = () => { // Absolute Physical Wand State const [yaw, setYaw] = useState(0); const [pitch, setPitch] = useState(0); const [roll, setRoll] = useState(0); const [distance, setDistance] = useState(120); // Sensor State const [sensorActive, setSensorActive] = useState(false); // Clutch & Relative Mapping State const [clutchActive, setClutchActive] = useState(false); const [cursorPos, setCursorPos] = useState({ x: 0, z: 0 }); const [calibration, setCalibration] = useState({ yawOffset: 0, pitchOffset: 0 }); // Constants const SCREEN_W = 80; const SCREEN_H = 45; const FLOOR_Z = -30; const WAND_LEN = 35; // Handle Device Orientation Sensors const handleOrientation = (event) => { if (event.alpha !== null) { // Alpha: 0 to 360 (compass). Invert so turning right increases Yaw setYaw(-event.alpha); } if (event.beta !== null) { // Beta: -180 to 180 (front/back tilt). Invert so pointing up increases Pitch setPitch(-event.beta); } if (event.gamma !== null) { // Gamma: -90 to 90 (left/right twist). Maps to roll. setRoll(event.gamma); } }; const toggleSensors = async () => { if (sensorActive) { window.removeEventListener('deviceorientation', handleOrientation); setSensorActive(false); return; } // iOS 13+ requires explicit permission request via a user gesture if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') { try { const permissionState = await DeviceOrientationEvent.requestPermission(); if (permissionState === 'granted') { window.addEventListener('deviceorientation', handleOrientation); setSensorActive(true); } else { alert('Sensor permission denied. Please allow motion access in your browser settings.'); } } catch (error) { console.error('Error requesting sensor access:', error); alert('Sensor API cannot be requested in this secure environment context.'); } } else if (window.DeviceOrientationEvent) { // Standard browser support (Android, older iOS) window.addEventListener('deviceorientation', handleOrientation); setSensorActive(true); } else { alert("Your device doesn't appear to have orientation sensors or the API is blocked."); } }; // Cleanup sensors on unmount useEffect(() => { return () => window.removeEventListener('deviceorientation', handleOrientation); }, []); // Handle Clutch Toggle const toggleClutch = () => { if (!clutchActive) { // Engaging: Re-center wand to point exactly at current cursor const targetYaw = Math.atan2(cursorPos.x, distance) * (180 / Math.PI); const targetPitch = Math.atan2(cursorPos.z, distance) * (180 / Math.PI); setCalibration({ yawOffset: yaw - targetYaw, pitchOffset: pitch - targetPitch }); setClutchActive(true); } else { // Disengaging: Cursor locks, wand raw movement shown setClutchActive(false); } }; // Update Cursor Position based on calibrated aim ONLY when clutched useEffect(() => { if (clutchActive) { const effYawRad = (yaw - calibration.yawOffset) * (Math.PI / 180); const effPitchRad = (pitch - calibration.pitchOffset) * (Math.PI / 180); setCursorPos({ x: distance * Math.tan(effYawRad), z: distance * Math.tan(effPitchRad) }); } }, [yaw, pitch, distance, clutchActive, calibration]); // Calculate rendering metrics const metrics = useMemo(() => { const effYawRad = (yaw - calibration.yawOffset) * (Math.PI / 180); const effPitchRad = (pitch - calibration.pitchOffset) * (Math.PI / 180); // 1. CALIBRATED AIM (Where the wand appears to point after recentering) const hitX = distance * Math.tan(effYawRad); const hitZ = distance * Math.tan(effPitchRad); const isHit = Math.abs(hitX) <= SCREEN_W / 2 && Math.abs(hitZ) <= SCREEN_H / 2; // 2. VIRTUAL CURSOR (Where the relative pointer is on the screen) const isCursorHit = Math.abs(cursorPos.x) <= SCREEN_W / 2 && Math.abs(cursorPos.z) <= SCREEN_H / 2; // 3. WAND BODY (Calculated from calibrated orientation) const L = Math.sqrt(hitX * hitX + distance * distance + hitZ * hitZ); const tailX = 0 - WAND_LEN * (hitX / L); const tailY = distance - WAND_LEN * (-distance / L); const tailZ = 0 - WAND_LEN * (hitZ / L); // 4. CURSOR CROSSHAIR (Applying roll to the cursor position) const r = 5; const rollRad = (roll * Math.PI) / 180; return { hitX, hitZ, isHit, tailX, tailY, tailZ, isCursorHit, crosshair: { topX: cursorPos.x - r * Math.sin(rollRad), topZ: cursorPos.z + r * Math.cos(rollRad), botX: cursorPos.x + r * Math.sin(rollRad), botZ: cursorPos.z - r * Math.cos(rollRad), leftX: cursorPos.x - r * Math.cos(rollRad), leftZ: cursorPos.z - r * Math.sin(rollRad), rightX: cursorPos.x + r * Math.cos(rollRad), rightZ: cursorPos.z + r * Math.sin(rollRad) } }; }, [yaw, pitch, roll, distance, cursorPos]); const gridLines = useMemo(() => { const lines = []; for (let i = -150; i <= 150; i += 20) { lines.push(<IsoLine key={`x${i}`} p1={[i, -50, FLOOR_Z]} p2={[i, 250, FLOOR_Z]} stroke="#334155" strokeWidth={1} opacity={0.5} />); lines.push(<IsoLine key={`y${i}`} p1={[-150, i, FLOOR_Z]} p2={[150, i, FLOOR_Z]} stroke="#334155" strokeWidth={1} opacity={0.5} />); } return lines; }, []); return ( <div className="min-h-screen bg-slate-950 text-slate-200 font-sans p-6 flex flex-col xl:flex-row gap-6"> {/* LEFT COLUMN: The Isometric Scene */} <div className="flex-1 flex flex-col gap-4 relative"> <div className="flex items-center justify-between absolute top-0 left-0 right-0 z-10 px-4 pt-2 pointer-events-none"> <div> <h1 className="text-2xl font-bold text-white flex items-center gap-2 drop-shadow-md"> <Navigation className="text-cyan-400" /> Relative Clutch Telemetry </h1> </div> <div className="flex gap-2"> <div className={`px-4 py-1.5 rounded-full text-sm font-semibold border backdrop-blur-md transition-colors ${ clutchActive ? 'bg-cyan-500/20 text-cyan-300 border-cyan-500/40 shadow-[0_0_15px_rgba(34,211,238,0.3)]' : 'bg-slate-800/80 text-slate-400 border-slate-700' }`}> CLUTCH: {clutchActive ? 'ENGAGED' : 'RELEASED'} </div> </div> </div> {/* 3D SVG Canvas */} <div className="flex-1 bg-slate-900 border-2 border-slate-800 rounded-2xl overflow-hidden shadow-2xl relative min-h-[600px]"> <svg viewBox="-180 -180 360 300" className="w-full h-full absolute inset-0"> <defs> <radialGradient id="screenGlow" cx="50%" cy="50%" r="50%"> <stop offset="0%" stopColor="#0ea5e9" stopOpacity="0.10" /> <stop offset="100%" stopColor="#0ea5e9" stopOpacity="0" /> </radialGradient> <radialGradient id="cursorGlow" cx="50%" cy="50%" r="50%"> <stop offset="0%" stopColor={clutchActive ? "#22d3ee" : "#64748b"} stopOpacity="0.8" /> <stop offset="100%" stopColor={clutchActive ? "#22d3ee" : "#64748b"} stopOpacity="0" /> </radialGradient> </defs> {/* Floor Plane & Grid */} <IsoPolygon points={[[-150, -50, FLOOR_Z], [150, -50, FLOOR_Z], [150, 250, FLOOR_Z], [-150, 250, FLOOR_Z]]} fill="#020617" /> {gridLines} {/* Drop Shadows */} <IsoPolygon points={[ [-SCREEN_W/2, 0, FLOOR_Z], [SCREEN_W/2, 0, FLOOR_Z], [SCREEN_W/2 + 20, 30, FLOOR_Z], [-SCREEN_W/2 + 20, 30, FLOOR_Z] ]} fill="rgba(0,0,0,0.6)" /> <IsoLine p1={[metrics.tailX, metrics.tailY, FLOOR_Z]} p2={[0, distance, FLOOR_Z]} stroke="rgba(0,0,0,0.6)" strokeWidth={10} strokeLinecap="round" /> {/* --- THE SCREEN (Y=0) --- */} <ScreenRect cx={0} cz={0} w={SCREEN_W} h={SCREEN_H} fill="#0f172a" fillOpacity={0.8} stroke="#0ea5e9" strokeWidth={2} /> <ScreenRect cx={0} cz={0} w={SCREEN_W} h={SCREEN_H} fill="url(#screenGlow)" /> <ScreenRect cx={-15} cz={6} w={40} h={22} fill="#38bdf8" fillOpacity={0.15} stroke="#38bdf8" strokeWidth={0.5} /> <ScreenRect cx={20} cz={11} w={20} h={12} fill="#34d399" fillOpacity={0.15} stroke="#34d399" strokeWidth={0.5} /> <ScreenRect cx={20} cz={-4} w={20} h={14} fill="#818cf8" fillOpacity={0.15} stroke="#818cf8" strokeWidth={0.5} /> <IsoPolygon points={[[-5, 0, -SCREEN_H/2], [5, 0, -SCREEN_H/2], [3, 0, FLOOR_Z], [-3, 0, FLOOR_Z]]} fill="#334155" /> {/* --- THE WAND --- */} <IsoLine p1={[metrics.tailX, metrics.tailY, metrics.tailZ]} p2={[0, distance, 0]} stroke="#e2e8f0" strokeWidth={8} strokeLinecap="round" /> <circle cx={projectIso(0, distance, 0).cx} cy={projectIso(0, distance, 0).cy} r={3} fill="#fff" /> {/* WAND PROJECTION RAY */} <IsoLine p1={[0, distance, 0]} p2={clutchActive ? [cursorPos.x, 0, cursorPos.z] : [metrics.hitX, 0, metrics.hitZ]} stroke={clutchActive ? "#22d3ee" : "#cbd5e1"} strokeWidth={clutchActive ? 2 : 1} strokeDasharray={clutchActive ? "none" : "3 3"} opacity={clutchActive ? 0.8 : 0.3} /> {/* --- THE CURSOR --- */} <g opacity={metrics.isCursorHit ? 1 : 0.3} className="transition-opacity duration-200"> {/* Soft glow base */} <circle cx={projectIso(cursorPos.x, 0, cursorPos.z).cx} cy={projectIso(cursorPos.x, 0, cursorPos.z).cy} r={10} fill="url(#cursorGlow)" /> {/* Center Dot */} <circle cx={projectIso(cursorPos.x, 0, cursorPos.z).cx} cy={projectIso(cursorPos.x, 0, cursorPos.z).cy} r={2} fill={clutchActive ? "#fff" : "#94a3b8"} /> {/* Roll Crosshair lines */} <IsoLine p1={[metrics.crosshair.topX, 0, metrics.crosshair.topZ]} p2={[metrics.crosshair.botX, 0, metrics.crosshair.botZ]} stroke={metrics.isCursorHit ? (clutchActive ? "#22d3ee" : "#64748b") : "#f43f5e"} strokeWidth={1.5} /> <IsoLine p1={[metrics.crosshair.leftX, 0, metrics.crosshair.leftZ]} p2={[metrics.crosshair.rightX, 0, metrics.crosshair.rightZ]} stroke={metrics.isCursorHit ? (clutchActive ? "#22d3ee" : "#64748b") : "#f43f5e"} strokeWidth={1.5} /> {/* Up-indicator for Roll orientation */} <IsoPolygon points={[ [metrics.crosshair.topX, 0, metrics.crosshair.topZ], [metrics.crosshair.topX - 1.5, 0, metrics.crosshair.topZ - 2], [metrics.crosshair.topX + 1.5, 0, metrics.crosshair.topZ - 2] ]} fill={metrics.isCursorHit ? (clutchActive ? "#22d3ee" : "#64748b") : "#f43f5e"} /> {/* Locked Icon (Shows when clutch is off) */} {!clutchActive && ( <text x={projectIso(cursorPos.x, 0, cursorPos.z).cx + 8} y={projectIso(cursorPos.x, 0, cursorPos.z).cy - 8} fill="#94a3b8" fontSize="8" fontFamily="sans-serif" > 🔒 </text> )} </g> {/* Out of bounds alert */} {!metrics.isCursorHit && ( <text x={projectIso(cursorPos.x, 0, cursorPos.z).cx} y={projectIso(cursorPos.x, 0, cursorPos.z).cy - 15} fill="#f43f5e" fontSize="6" fontFamily="monospace" textAnchor="middle" > CURSOR OOB </text> )} </svg> </div> </div> {/* RIGHT COLUMN: Controls & Telemetry */} <div className="w-full xl:w-96 flex flex-col gap-6"> {/* Main Interaction: The Clutch */} <button onClick={toggleClutch} className={`w-full p-4 rounded-2xl font-bold flex flex-col items-center justify-center gap-2 transition-all active:scale-95 ${ clutchActive ? 'bg-cyan-500 text-slate-950 shadow-[0_0_30px_rgba(34,211,238,0.4)]' : 'bg-slate-800 text-slate-300 hover:bg-slate-700 border border-slate-600' }`} > {clutchActive ? <Zap size={28} /> : <Lock size={28} className="text-slate-500" />} <span className="text-lg tracking-wider"> {clutchActive ? 'CLUTCH ACTIVE' : 'PRESS TO CLUTCH'} </span> <span className={`text-xs font-normal ${clutchActive ? 'text-slate-800' : 'text-slate-500'}`}> {clutchActive ? 'Wand re-centered to cursor. Projection is straight.' : 'Cursor locked. Wand tracks raw drift.'} </span> </button> {/* Telemetry Panel */} <div className="bg-slate-900 border border-slate-800 p-5 rounded-2xl"> <h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 mb-4 flex items-center gap-2"> <Activity size={16} /> Data Vectors </h2> <div className="grid grid-cols-2 gap-3 mb-4"> <div className={`p-3 rounded-xl border transition-colors ${clutchActive ? 'bg-cyan-950/30 border-cyan-500/30' : 'bg-slate-950 border-slate-800/50'}`}> <span className="block text-xs text-slate-500 mb-1">Cursor X</span> <span className={`font-mono text-xl ${clutchActive ? 'text-cyan-400' : 'text-slate-400'}`}> {cursorPos.x.toFixed(1)} </span> </div> <div className={`p-3 rounded-xl border transition-colors ${clutchActive ? 'bg-cyan-950/30 border-cyan-500/30' : 'bg-slate-950 border-slate-800/50'}`}> <span className="block text-xs text-slate-500 mb-1">Cursor Z</span> <span className={`font-mono text-xl ${clutchActive ? 'text-cyan-400' : 'text-slate-400'}`}> {cursorPos.z.toFixed(1)} </span> </div> </div> <div className="bg-slate-950 p-3 rounded-xl border border-slate-800/50 flex justify-between items-center text-xs"> <span className="text-slate-500">Raw Sensor (Yaw, Pitch)</span> <span className="font-mono text-slate-400"> {yaw.toFixed(0)}°, {pitch.toFixed(0)}° </span> </div> </div> {/* Input Controls */} <div className="bg-slate-900 border border-slate-800 p-5 rounded-2xl flex-1"> <div className="flex items-center justify-between mb-6"> <h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 flex items-center gap-2"> <Move3d size={16} /> Raw Device Inputs </h2> <button onClick={toggleSensors} className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-bold transition-colors ${ sensorActive ? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30' : 'bg-slate-800 text-slate-400 hover:bg-slate-700 border border-slate-700' }`} > <Smartphone size={14} /> {sensorActive ? 'SENSORS LIVE' : 'ENABLE SENSORS'} </button> </div> <div className="space-y-6"> <ControlSlider label="Yaw (Horizontal)" value={yaw} setValue={setYaw} min={-360} max={360} unit="°" color={clutchActive ? "cyan" : "slate"} disabled={sensorActive} /> <ControlSlider label="Pitch (Vertical)" value={pitch} setValue={setPitch} min={-180} max={180} unit="°" color={clutchActive ? "cyan" : "slate"} disabled={sensorActive} /> <ControlSlider label="Roll (Wrist Twist)" value={roll} setValue={setRoll} min={-90} max={90} unit="°" color="purple" disabled={sensorActive} /> </div> </div> </div> </div> ); }; const ControlSlider = ({ label, value, setValue, min, max, unit, color, disabled }) => { const colorMap = { cyan: 'accent-cyan-500', purple: 'accent-purple-500', emerald: 'accent-emerald-500', slate: 'accent-slate-500' }; const textMap = { cyan: 'text-cyan-400', purple: 'text-purple-400', emerald: 'text-emerald-400', slate: 'text-slate-400' }; return ( <div className="flex flex-col gap-2"> <div className="flex justify-between items-center"> <label className={`text-sm font-medium ${color === 'slate' ? 'text-slate-500' : 'text-slate-300'}`}>{label}</label> <span className={`font-mono text-sm transition-colors ${textMap[color]}`}> {value > 0 ? '+' : ''}{Math.round(value)}{unit} </span> </div> <input type="range" min={min} max={max} value={value} disabled={disabled} onChange={(e) => setValue(Number(e.target.value))} className={`w-full h-1.5 bg-slate-800 rounded-lg appearance-none transition-all ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} ${colorMap[color]}`} /> <div className="flex justify-between text-[10px] text-slate-600 font-mono"> <span>{min}</span> <span>{max}</span> </div> </div> ); }; export default App;
No revisions found. Save the file to create a backup.
Delete
Update App