+ 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) => (
handleClick(index)(true)}>
+ onClick={() => handleClick(index)(true)}
+ >
{index}
))}
@@ -64,7 +67,8 @@ export default function DotModeSection() {
{motors.slice(16, 32).map((_, index) => (
handleClick(index)(false)}>
+ onClick={() => handleClick(index)(false)}
+ >
{index + 20}
))}
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}`}{' '}
+
playEvent(eventKey.key)}
- className=" hover:bg-neutral-700 cursor-pointer text-neutral-white size-8 items-center justify-center flex rounded">
- {currentTime !== 0 ? : }
+ className=" hover:bg-neutral-700 cursor-pointer text-neutral-white size-8 items-center justify-center flex rounded"
+ >
+ {currentTime !== 0 ? (
+
+ ) : (
+
+ )}
stopEvent(eventKey.key)}
- className=" hover:bg-neutral-700 cursor-pointer text-neutral-white size-8 items-center justify-center flex rounded">
+ className=" hover:bg-neutral-700 cursor-pointer text-neutral-white size-8 items-center justify-center flex rounded"
+ >
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.
+
+ onClick={ping}
+ >
Ping
+ onClick={testMotors}
+ >
Test
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.`}
+
+
+
+
+
+
+ );
+}
+
+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 (
+
+ );
+}
+
+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() {
/>
-
setPlaying(!playing)}>
- {playing ? : }
+ setPlaying(!playing)}
+ >
+ {playing ? (
+
+ ) : (
+
+ )}
+ }}
+ >
@@ -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);
- }}>
+ }}
+ >