diff --git a/demo/src/App.tsx b/demo/src/App.tsx index a7273bd..b5d4838 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -7,6 +7,7 @@ import VideoSection from './components/VideoSection'; import { Timer } from './utils/Timer'; import DotModeSection from './components/DotModeSection'; import PathModeSection from './components/PathModeSection'; +import NewPathModeSection from './components/NewPathModeSection'; const APP_ID = '67d0055d69fb8c79a66b1cb6'; const API_KEY = 'Sv3sOVOSeLFl8t8QTKpK'; @@ -49,6 +50,7 @@ export default function App() { + diff --git a/demo/src/components/DotModeSection.tsx b/demo/src/components/DotModeSection.tsx index 6bc0896..ebd3aee 100644 --- a/demo/src/components/DotModeSection.tsx +++ b/demo/src/components/DotModeSection.tsx @@ -1,14 +1,14 @@ import { useState } from 'react'; import HapticDriver, { PositionType } from 'tact-js'; -const motors = Array.from({ length: 40 }).map(() => 0); +const motors = Array.from({ length: 32 }).map(() => 0); export default function DotModeSection() { const [intensity, setIntensity] = useState(100); const [duration, setDuration] = useState(1000); const handleClick = (index: number) => (front: boolean) => { - const offset = front ? 0 : 20; + const offset = front ? 0 : 16; const newMotors = [...motors]; newMotors[index + offset] = intensity; @@ -20,8 +20,10 @@ export default function DotModeSection() { }; return ( -
-

3. Dot Mode Test

+
+

Dot Mode Test

{`If you have a TactSuit connected, you can test the motors by clicking the buttons below. Each button will trigger a single motor for 1 second.`} @@ -55,7 +57,8 @@ export default function DotModeSection() { {Array.from({ length: 16 }).map((_, index) => ( ))} @@ -64,7 +67,8 @@ export default function DotModeSection() { {motors.slice(16, 32).map((_, index) => ( ))} diff --git a/demo/src/components/EventKeySection.tsx b/demo/src/components/EventKeySection.tsx index 0275cba..8575f69 100644 --- a/demo/src/components/EventKeySection.tsx +++ b/demo/src/components/EventKeySection.tsx @@ -18,7 +18,7 @@ export function EventKeySection() { return (

-

2. Play Events

+

Play Events

These are the event keys you can use to play haptic feedback.

@@ -66,22 +66,33 @@ function Event({ eventKey }: { eventKey: EventKey }) {
  • {eventKey.key}
    - {`${currentTime} / ${eventKey.durationMillis}`} + + {`${currentTime} / ${eventKey.durationMillis}`}{' '} +
    diff --git a/demo/src/components/MotorTestSection.tsx b/demo/src/components/MotorTestSection.tsx index 9b3f9d9..167f49c 100644 --- a/demo/src/components/MotorTestSection.tsx +++ b/demo/src/components/MotorTestSection.tsx @@ -10,17 +10,22 @@ export default function MotorTestSection() { return (
    -

    1. Motor Test

    -

    If you have a TactSuit connected, you can test the motors by clicking the button below.

    +

    Motor Test

    +

    + If you have a TactSuit connected, you can test the motors by clicking + the button below. +

    diff --git a/demo/src/components/NewPathModeSection.tsx b/demo/src/components/NewPathModeSection.tsx new file mode 100644 index 0000000..91fb57b --- /dev/null +++ b/demo/src/components/NewPathModeSection.tsx @@ -0,0 +1,256 @@ +import HapticDriver, { PositionType } from 'tact-js'; +import { useState } from 'react'; +import { linearScale } from '../utils/Scale'; +import { clamp } from '../utils/common'; + +type Point = { + x: number; + y: number; + lifespan: number; +}; + +const MOTOR_POSITIONS = Array.from({ length: 32 }, (_, index) => { + const isFront = index < 16; + const localIndex = index % 16; + const col = localIndex % 4; + const row = Math.floor(localIndex / 4); + + const flippedCol = isFront ? col : 3 - col; + + const baseX = isFront ? 0 : 0.5; + const x = baseX + (flippedCol / 4) * 0.5; + const y = row / 3; + + return { x, y, index }; +}); + +export default function NewPathModeSection() { + const [points, setPoints] = useState([]); + const [intensity, setIntensity] = useState(100); + const [duration, setDuration] = useState(100); + const [thickness, setThickness] = useState(1); + const [sharpness, setSharpness] = useState(2); + + const activateDrawing = (e: React.PointerEvent) => { + e.currentTarget.setPointerCapture(e.pointerId); + setPoints([]); + }; + + const deactivateDrawing = (e: React.PointerEvent) => { + e.currentTarget.releasePointerCapture(e.pointerId); + setTimeout(() => { + setPoints([]); + }, 1000); + }; + + const playPath = (e: React.PointerEvent) => { + if (e.buttons !== 1) return; + + const x = linearScale( + e.nativeEvent.offsetX, + e.currentTarget.clientWidth, + 0 + ); + const y = linearScale( + e.nativeEvent.offsetY, + e.currentTarget.clientHeight, + 0 + ); + + const clapmedX = clamp(x, 0, 1); + const clampedY = clamp(y, 0, 1); + + const adjustedX = + clapmedX - 0.087 < 0 ? clapmedX - 0.087 + 1 : clapmedX - 0.087; + + const distances = MOTOR_POSITIONS.map((motor) => { + const directDx = motor.x - adjustedX; + const wrapDx = directDx > 0 ? directDx - 1 : directDx + 1; + + const dx = + (Math.abs(directDx) < Math.abs(wrapDx) ? directDx : wrapDx) * 2; + const dy = motor.y - clampedY; + const distance = Math.sqrt(dx * dx + dy * dy); + return { ...motor, distance }; + }); + + const closestMotors = distances + .sort((a, b) => a.distance - b.distance) + .slice(0, Math.floor(1 + (thickness - 1) * 5)); + + const motorValues = Array.from({ length: 32 }, () => 0); + + const sigma = 0.3 / sharpness; + + closestMotors.forEach((motor) => { + const gaussianRatio = Math.exp( + -(motor.distance * motor.distance) / (2 * sigma * sigma) + ); + const adjustedIntensity = Math.round(intensity * gaussianRatio); + motorValues[motor.index] = adjustedIntensity; + }); + + HapticDriver.playDot({ + position: PositionType.Vest, + duration: duration, + motorValues: motorValues, + }); + }; + + const draw = (e: React.PointerEvent) => { + if (e.buttons !== 1) return; + + playPath(e); + + const x = e.clientX - e.currentTarget.getBoundingClientRect().left; + const y = e.clientY - e.currentTarget.getBoundingClientRect().top; + + const clampedX = clamp(x, 0, e.currentTarget.clientWidth); + const clampedY = clamp(y, 0, e.currentTarget.clientHeight); + + const newPath = [ + ...points.map((value) => ({ ...value, lifespan: value.lifespan - 5 })), + { x: clampedX, y: clampedY, lifespan: 100 }, + ].filter((value) => value.lifespan > 0); + + setPoints(newPath); + }; + + return ( +
    +

    New Path Mode Test

    +

    + {`If you have a TactSuit connected, you can test the motors by dragging over the + area below. The motors will vibrate according to the position of your mouse.`} +

    +
    +
    + + setIntensity(Number(e.target.value))} + /> +
    +
    + + setDuration(Number(e.target.value))} + className="border border-neutral-200 p-2 rounded w-20" + /> +
    +
    + + setThickness(Number(e.target.value))} + /> + {thickness} +
    +
    + + setSharpness(Number(e.target.value))} + /> + {sharpness} +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + ); +} + +function Drawings({ points }: { points: Point[] }) { + const createBezierPath = (points: Point[]) => { + if (points.length < 2) return ''; + let d = `M ${points[0].x},${points[0].y}`; + for (let i = 1; i < points.length - 1; i++) { + const current = points[i]; + const next = points[i + 1]; + const midX = (current.x + next.x) / 2; + const midY = (current.y + next.y) / 2; + d += ` Q ${current.x},${current.y} ${midX},${midY}`; + } + const last = points[points.length - 1]; + d += ` T ${last.x},${last.y}`; + return d; + }; + + return ( +
    + + + +
    + ); +} + +function Header() { + return ( +
    +
    +

    Front

    +

    Back

    +
    +
    +

    L

    +

    R

    +

    L

    +
    +
    + ); +} + +function Indicator() { + return ( + <> +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/demo/src/components/PathModeSection.tsx b/demo/src/components/PathModeSection.tsx index 0d3fa1c..1c68eb7 100644 --- a/demo/src/components/PathModeSection.tsx +++ b/demo/src/components/PathModeSection.tsx @@ -29,19 +29,30 @@ export default function PathModeSection() { const playPath = (e: React.PointerEvent) => { if (e.buttons !== 1) return; - const x = linearScale(e.nativeEvent.offsetX, e.currentTarget.clientWidth, 0); - const y = linearScale(e.nativeEvent.offsetY, e.currentTarget.clientHeight, 0); + const x = linearScale( + e.nativeEvent.offsetX, + e.currentTarget.clientWidth, + 0 + ); + const y = linearScale( + e.nativeEvent.offsetY, + e.currentTarget.clientHeight, + 0 + ); const clapmedX = clamp(x, 0, 1); const clampedY = clamp(y, 0, 1); + const adjustedX = + clapmedX - 0.087 < 0 ? clapmedX - 0.087 + 1 : clapmedX - 0.087; + /** * Play the path with the given position, duration, x, y, and intensity. */ HapticDriver.playPath({ position: PositionType.Vest, duration: duration, - x: [clapmedX], + x: [adjustedX], y: [clampedY], intensity: [intensity], }); @@ -67,8 +78,10 @@ export default function PathModeSection() { }; return ( -
    -

    4. Path Mode Test

    +
    +

    Path Mode Test

    {`If you have a TactSuit connected, you can test the motors by dragging over the area below. The motors will vibrate according to the position of your mouse.`} @@ -103,7 +116,8 @@ export default function PathModeSection() { className="flex w-full justify-center divide-x divide-gray-400" onPointerDown={activateDrawing} onPointerUp={deactivateDrawing} - onPointerMove={draw}> + onPointerMove={draw} + >

    diff --git a/demo/src/components/VideoSection.tsx b/demo/src/components/VideoSection.tsx index 751b19b..f0cd3df 100644 --- a/demo/src/components/VideoSection.tsx +++ b/demo/src/components/VideoSection.tsx @@ -17,7 +17,7 @@ export default function VideoSection() { return (
    -

    5. Video with Haptic

    +

    Video with Haptic

    Watch this video with haptic feedback.

    @@ -30,7 +30,10 @@ export default function VideoSection() { height={'100%'} onPlay={async () => { setPlaying(true); - HapticDriver.play({ eventKey: EVENT_KEY, startTime: currentTime * 1000 }); + HapticDriver.play({ + eventKey: EVENT_KEY, + startTime: currentTime * 1000, + }); }} onProgress={({ playedSeconds }) => setCurrentTime(playedSeconds)} onPause={async () => { @@ -51,8 +54,15 @@ export default function VideoSection() { />
    - @@ -80,7 +91,11 @@ export default function VideoSection() { const width = rect.width; const time = (x / width) * (player?.getDuration() ?? 1); - const availableTime = clamp(time, 0, player?.getDuration() ?? 1); + const availableTime = clamp( + time, + 0, + player?.getDuration() ?? 1 + ); setCurrentTime(availableTime); player?.seekTo(availableTime); } @@ -91,7 +106,8 @@ export default function VideoSection() { setPlaying(true); } e.currentTarget.releasePointerCapture(e.pointerId); - }}> + }} + >