🤯 使用 React 构建你的第一个神经科学应用
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
如今大多数应用程序都会根据用户意图改变状态。更具体地说,是根据用户的手部动作(例如点击、轻触、按压等)来改变状态。然而,每一个意图都始于我们的大脑。
今天,我们将开发一款不同类型的应用程序。我们将开发一款能够根据你的认知状态改变自身状态的应用程序。
听我说完。
如果我们的应用能根据你的平静程度改变WebGL海洋的运动轨迹,会怎么样?一种由你的情绪驱动的“视觉冥想”体验。
第一步是测量和获取这些数据。为此,我们将使用 Notion 头戴式设备。
入门
我们首先使用 Create React App (CRA) 来启动我们的应用程序。我们在 VS Code 中打开项目,并在本地运行应用程序。
npx create-react-app mind-controlled-oceancode mind-controlled-oceannpm start
如果一切顺利,你应该会看到类似这样的画面:
🔑 身份验证
我们重视隐私。因此,Notion 是首款配备身份验证功能的脑机接口。为应用添加身份验证功能非常简单。为此,我们需要一个登录表单和三个副作用来同步身份验证状态。
您只需一个Neurosity 账户和一个设备 ID 即可连接到您的 Notion 脑机接口。那么,让我们首先创建一个新的登录表单组件来收集这些信息。
// src/components/LoginForm.js
import React, { useState } from "react";
export function LoginForm({ onLogin, loading, error }) {
const [deviceId, setDeviceId] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
function onSubmit(event) {
event.preventDefault();
onLogin({ deviceId, email, password });
}
return (
<form className="card login-form" onSubmit={onSubmit}>
<h3 className="card-heading">Login</h3>
{!!error ? <h4 className="card-error">{error}</h4> : null}
<div className="row">
<label>Notion Device ID</label>
<input
type="text"
value={deviceId}
disabled={loading}
onChange={e => setDeviceId(e.target.value)}
/>
</div>
<div className="row">
<label>Email</label>
<input
type="email"
value={email}
disabled={loading}
onChange={e => setEmail(e.target.value)}
/>
</div>
<div className="row">
<label>Password</label>
<input
type="password"
value={password}
disabled={loading}
onChange={e => setPassword(e.target.value)}
/>
</div>
<div className="row">
<button type="submit" className="card-btn" disabled={loading}>
{loading ? "Logging in..." : "Login"}
</button>
</div>
</form>
);
}
🖍 为表单添加样式 -以下是我使用的 CSS。
deviceId此组件将保存 `<form> `、` <form> email` 和 ` <form>` 的状态password。此外,我们的表单组件将接收一个 ` onLoginprop`,该属性会在用户点击“登录”按钮时执行。我们还会接收一个loading`prop`,用于指示表单提交正在进行中,以及一个error`prop`,用于在发生错误时显示消息。
现在我们已经创建了登录组件,让我们添加一个登录页面来使用我们的新组件。
// src/pages/Login.js
import React, { useState, useEffect } from "react";
import { LoginForm } from "../components/LoginForm";
export function Login({ notion, user, setUser, setDeviceId }) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [isLoggingIn, setIsLoggingIn] = useState(false);
function onLogin({ email, password, deviceId }) {
if (email && password && deviceId) {
setError("");
setEmail(email);
setPassword(password);
setDeviceId(deviceId);
} else {
setError("Please fill the form");
}
}
return (
<LoginForm
onLogin={onLogin}
loading={isLoggingIn}
error={error}
/>
);
}
本页面的目标是显示登录表单,通过setError函数添加基本的表单验证,并执行登录功能。对于后者,我们添加一个副作用,该副作用将与email页面password接收到的 props 同步。
useEffect(() => {
if (!user && notion && email && password) {
login();
}
async function login() {
setIsLoggingIn(true);
const auth = await notion
.login({ email, password })
.catch(error => {
setError(error.message);
});
if (auth) {
setUser(auth.user);
}
setIsLoggingIn(false);
}
}, [email, password, notion, user, setUser, setError]);
你可以把它理解user为保存 Notion API 设置的身份验证用户会话的对象。因此,只有login()当不存在身份验证会话、状态中存在 Notion 实例,且用户已提交电子邮件和密码时,我们才会调用我们的函数。
很快你们就会知道我们将如何接收道具了:notion, user, setUser, setDeviceId。但在那之前,让我们回到我们的工作区App.js,开始把所有东西组装起来。
⚙️ 应用状态
为了简化应用,我们将只使用 React 的Hook、Reach Router 以及react-useuseState提供的本地存储 Hook 👍 。这意味着我们的应用状态策略是将全局状态保存在 App 组件级别,并将必要的 props 传递给子组件。
npm install @reach/router react-use
我们将从一条路由开始,但随着我们继续构建应用程序,我们将再添加 2 条路由。
// src/App.js
import React, { useState, useEffect } from "react";
import { Router, navigate } from "@reach/router";
import useLocalStorage from "react-use/lib/useLocalStorage";
import { Login } from "./pages/Login";
export function App() {
const [notion, setNotion] = useState(null);
const [user, setUser] = useState(null);
const [deviceId, setDeviceId] = useLocalStorage("deviceId");
const [loading, setLoading] = useState(true);
return (
<Router>
<Login
path="/"
notion={notion}
user={user}
setUser={setUser}
setDeviceId={setDeviceId}
/>
</Router>
);
}
🏆 我发现Ryan Florence(及其团队)开发的Reach Router在简洁性和可预测性之间取得了完美的平衡。他们的文档非常清晰,让我几分钟内就实现了基本的路由配置。
如果您想知道为什么我们决定将地址保存deviceId在本地存储中,那是因为我们需要在用户登录前后访问它。此外,避免用户多次输入地址也能带来更佳的用户体验。
🧠 Notion
现在我们已经实现了基本的状态管理,接下来让我们通过安装 API 并将其导入到 Notion 中,将我们的应用程序与NotionApp.js集成。
npm install @neurosity/notion
🤯 Notion API 实现了设备和应用之间的全面通信。我们将使用它来获取基于用户认知状态的实时反馈。在这个应用中,我们将专门使用Calm API。
import { Notion } from "@neurosity/notion";
连接到 Notion 设备很简单。我们实例化一个新的Notion 对象并传递设备 ID。我们可以添加一个副作用,通过与 App 组件同步,将实例的状态设置为 App 组件的状态deviceId。
📖 您可以在docs.neurosity.co找到完整的 Notion 文档。
useEffect(() => {
if (deviceId) {
const notion = new Notion({ deviceId }); // 😲
setNotion(notion);
} else {
setLoading(false);
}
}, [deviceId]);
我们需要同步的另一个状态是user状态。
在以下示例中,我们将添加一个与实例值同步的副作用notion。如果实例notion值尚未设置,我们将跳过订阅Calm事件,直到notion实例创建完成。
useEffect(() => {
if (!notion) {
return;
}
const subscription = notion.onAuthStateChanged().subscribe(user => {
if (user) {
setUser(user);
} else {
navigate("/");
}
setLoading(false);
});
return () => {
subscription.unsubscribe();
};
}, [notion]);
如果应用程序通过 Notion 身份验证持久化了活动用户会话,我们将需要获取当前登录用户,并将其设置为我们的 App 组件中的状态。
该onAuthStateChanged方法返回一个包含用户身份验证事件的可观察对象。需要注意的是,在浏览器中使用 Notion API 时,会话将通过本地存储持久保存。因此,如果您关闭应用程序或重新加载页面,会话将保持不变,并onAuthStateChanged返回用户会话,而不是其他状态null。这正是我们所需要的。
如果未检测到会话,我们可以跳转到登录页面。否则,user在组件状态中进行设置。
我们可以通过添加注销页面来实现完整的身份验证。
// src/pages/Logout.js
import { useEffect } from "react";
import { navigate } from "@reach/router";
export function Logout({ notion, resetState }) {
useEffect(() => {
if (notion) {
notion.logout().then(() => {
resetState();
navigate("/");
});
}
}, [notion, resetState]);
return null;
}
登出页面只是一个简单的 React 组件,不包含任何 DOM 元素。我们只需要一个副作用,notion.logout()即如果notion实例存在,则调用相应的方法。最后,它会在用户登出后将其重定向到初始路由。
现在可以将此组件添加为路由App.js。
// src/App.js
// ...
import { Logout } from "./pages/Logout";
// ...
return (
<Router>
{/* ... */}
<Logout path="/logout" notion={notion} resetState={() => {
setNotion(null);
setUser(null);
setDeviceId("");
}} />
</Router>
);
身份验证完成后,让我们根据认知状态添加应用程序逻辑!
🌊 WebGL Ocean
我第一眼看到David用WebGL制作的海洋模型就爱上了它。所以,用Notion来控制天气,从而模拟海浪,感觉就像是一个很有趣的实验。
💡 有趣的事实:这个海浪模拟程序是 David Li 于 2013 年使用 JavaScript 和 WebGL 创建的。它曾作为 Google Experiments 的一部分进行展示。我很高兴看到它以 MIT 许可证开源。
接下来,我们要创建一个使用 WebGL Ocean 的新组件。因此,我们创建一个名为 Ocean 的目录./src/components/Ocean,并将以下文件添加到该目录中。
- simulation.js
- weather.js
- Ocean.js:
// src/components/Ocean/Ocean.js
import React, { useState, useEffect, useRef } from "react";
import useRafState from "react-use/lib/useRafState";
import { Simulator, Camera } from "./simulation.js"; // by David Li
import { mapCalmToWeather } from "./weather.js";
const camera = new Camera();
export function Ocean({ calm }) {
const ref = useRef();
const [simulator, setSimulator] = useState();
const [lastTime, setLastTime] = useRafState(Date.now());
useEffect(() => {
const { innerWidth, innerHeight } = window;
const simulator = new Simulator(ref.current, innerWidth, innerHeight);
setSimulator(simulator);
}, [ref, setSimulator]);
useEffect(() => {
if (simulator) {
const currentTime = Date.now();
const deltaTime = (currentTime - lastTime) / 1000 || 0.0;
setLastTime(currentTime);
simulator.render(deltaTime, camera);
}
}, [simulator, lastTime, setLastTime]);
return <canvas className="simulation" ref={ref}></canvas>;
}
如果一切顺利,我们应该能看到这一幕。
让我来解释一下这里发生了什么。
- 1️⃣ React 组件返回一个用于 WebGL 3D 场景的 canvas 元素
- 2️⃣ 我们使用 React 的方法
useRef来访问 canvas HTML 元素 - 3️⃣
Simulator当引用发生变化时,我们会实例化一个新的对象。该类Simulator负责控制渲染以及天气属性,例如风力、波浪大小和风场大小。 - 4️⃣ 我们使用
useRaf(requestAnimationFrame) hook 创建一个循环,其中回调函数在每个动画帧上执行。
目前,我们的海浪是根据静态天气参数(波浪的汹涌程度、风力和大小)来变化的。那么,我们如何根据calm评分来映射这些天气参数呢?
为此,我创建了一个实用函数,用于将平静度weather.js评分映射到相应的天气设置:波浪的湍急程度、风力和波浪大小。然后,我们可以创建一个副作用,在每次评分变化时同步更新。calm
useEffect(() => {
if (simulator) {
setWeatherBasedOnCalm(animatedCalm, 0, 0);
}
function setWeatherBasedOnCalm(calm) {
const { choppiness, wind, size } = mapCalmToWeather(calm);
simulator.setChoppiness(choppiness);
simulator.setWind(wind, wind);
simulator.setSize(size);
}
}, [calm, simulator]);
认知状态
这才是最有趣的部分。在这里,我们可以获取大脑数据并将其映射到应用程序状态。
通过订阅,我们可以大约每秒notion.calm()获得一个新的分数。因此,让我们添加组件,将其作为 prop 添加,并创建一个副作用,使其与实例和同步。如果这两个状态都存在,那么我们就可以安全地订阅calm 了。calm<Ocean calm={calm} />calmnotionuser
🧘🏿♀️平静度评分源自您的被动认知状态。该指标基于α波。平静度评分范围为
0.00 到 100。1.0分数越高,检测到平静0.3感的可能性就越大。平静度评分超过100 分意义重大。有助于提高平静度评分的方法包括闭上眼睛、保持静止、深呼吸或冥想。
// src/pages/Calm.js
import React, { useState, useEffect } from "react";
import { Ocean } from "../components/Ocean/Ocean";
export function Calm({ user, notion }) {
const [calm, setCalm] = useState(0);
useEffect(() => {
if (!user || !notion) {
return;
}
const subscription = notion.calm().subscribe(calm => {
const calmScore = Number(calm.probability.toFixed(2));
setCalm(calmScore);
});
return () => {
subscription.unsubscribe();
};
}, [user, notion]);
return (
<Ocean calm={calm} />
);
}
💡 所有 notion 指标,包括
notion.calm()返回一个 RxJS 订阅,我们可以使用该订阅在组件卸载时安全地取消订阅。
最后,我们将 Calm 页面添加到App.js.
// src/App.js
// ...
import { Calm } from "./pages/Calm";
// ...
// If already authenticated, redirect user to the Calm page
useEffect(() => {
if (user) {
navigate("/calm");
}
}, [user]);
return (
<Router>
{/* ... */}
<Calm path="/calm" notion={notion} user={user} />
</Router>
);
至此,我们的Neuro React应用程序就完成了。
我非常期待能根据用户个性特点打造个性化的应用体验。每个人的大脑都不同,但我们开发的应用却总是给所有用户呈现相同的体验。如果应用能为你量身定制呢?
如果有些应用程序能帮助你在压力大的时候放松身心呢?
如果能用脑电波验证应用程序的身份呢?
如果电子游戏可以根据玩家的感受改变剧情走向呢?
如果什么...
文章来源:https://dev.to/neurosity/building-your-first-neuro-app-with-react-49pj



