psytask
Version:
JavaScript Framework for Psychology task
329 lines (260 loc) • 8.13 kB
Markdown
# Psytask



JavaScript Framework for Psychology task. Compatible with the [jsPsych](https://github.com/jspsych/jsPsych) plugin.
Compare to jsPsych, Psytask has:
- Easier and more flexible development experiment.
- Higher time precision, try [Benchmark](https://bluebones-team.github.io/psytask/benchmark) on your browser.
- Smaller bundle size, Faster loading speed.
**🥳 You can play it online now via [Playground & Examples](https://bluebones-team.github.io/psytask/playground) !**
## Install
via NPM:
```bash
npm create psytask # create a project folder
```
```bash
npm i psytask # just install
```
via CDN:
```html
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- load css -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/psytask@1.1/dist/main.css"
/>
</head>
<body>
<script type="module">
// load js
import { createApp } from 'https://cdn.jsdelivr.net/npm/psytask@1.1/dist/index.min.js';
using app = await creaeApp();
//...
</script>
</body>
</html>
```
> [!WARNING]
> Psytask uses the modern JavaScript [`using` keyword](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/using) for automatic resource cleanup.
>
> When using bundlers (like Vite, Bun, etc.), the `using` keyword will be transpiled automatically, so you don't need to worry about browser compatibility.
>
> For CDN usage in older browsers that don't support the `using` keyword, you need to manually call the cleanup method:
>
> ```js
> // Instead of: using app = await createApp();
> const app = await createApp();
> // ... your code ...
> app.emit('cleanup'); // Manually clean up when done
> ```
## Usage
All psychology tasks are combinations of a series of scenes,
writing a psychology task requires only 2 steps:
1. create scene
2. show scene
### Create Scene
```js
import 'psytask/main.css';
import { createApp, effect, h } from 'psytask';
// create app
using app = await createApp();
// create built-in scenes
using fixation = app.text('+', { duration: 500 });
using blank = app.text('');
using guide = app.text('Welcome to our task', { close_on: 'key: ' }); // close on space key
// create custom scene with response collection
using scene = app.scene(
// 1st. argument: component (setup function)
/** @param {{ stimulus: string }} props */
(props, ctx) => {
/** @type {{ response_key: string; response_time: number }} */
let data;
// Reset data when scene shows
ctx
.on('scene:show', () => {
data = { response_key: '', response_time: 0 };
})
// Capture keyboard responses
.on('key:f', (e) => {
data.response_key = 'f';
data.response_time = e.timeStamp;
ctx.close();
})
.on('key:j', (e) => {
data.response_key = 'j';
data.response_time = e.timeStamp;
ctx.close();
});
// Create stimulus element
const el = h('div', { className: 'psytask-center' });
effect(() => {
el.textContent = props.stimulus; // update element when `props.stimulus` changed
});
// Return the element and data getter
return { node: el, data: () => data };
},
// 2nd. argument: scene options
{
defaultProps: { stimulus: '' },
},
);
```
### Show Scene
Based on the above example:
```js
// show with parameters
const data = await scene.show({ stimulus: 'Press F or J' });
console.log(`Response: ${data.response_key}, RT: ${data.response_time}ms`);
// show with new scene options
const data = await scene.config({ duration: Math.random() * 1000 }).show();
```
Scene will log the first frame time in each show:
```js
const data = await scene.show();
console.log("this scene's start time is", data.start_time);
```
Usually, we need to show a series of scenes:
```js
import { RandomSampling, StairCase } from 'psytask';
// show a fixed sequence
for (const stimulus of ['A', 'B', 'C']) {
await scene.show({ stimulus });
}
// show a random sequence
for (const stimulus of new RandomSampling({
candidates: ['A', 'B', 'C'],
sampleSize: 10,
replace: true,
})) {
await scene.show({ stimulus });
}
// adaptive testing with staircase
const staircase = new StairCase({
start: 10,
step: 1,
up: 3,
down: 1,
reversal: 6,
min: 1,
max: 12,
});
for (const duration of staircase) {
const data = await scene.config({ duration }).show({ stimulus: 'X' });
const correct = data.response_key === 'f'; // example response
staircase.response(correct); // set this trial response
}
```
### Data Collection
```js
// create data collector
using dc = app.collector('data.csv');
// show scenes and collect data
for (const stimulus of ['A', 'B', 'C']) {
const data = await scene.show({ stimulus });
// add a row
dc.add({
stimulus,
response: data.response_key,
rt: data.response_time - data.start_time,
correct: data.response_key === 'f', // example response
});
}
```
## Reference
See [API docs](https://bluebones-team.github.io/psytask).
## Integration
### jsPsych
Psytask is compatible with jsPsych plugins. Here's how to integrate jsPsych with Psytask:
#### Installation with jsPsych
```bash
npm i psytask jspsych @jspsych/plugin-html-button-response
```
#### CDN with jsPsych
```html
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- load jspsych css if needed, it should be above psytask css -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/jspsych@8.2.2/css/jspsych.css"
/>
<!-- load psytask css -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/psytask@1.1/dist/main.css"
/>
</head>
<body>
<!-- main script -->
<script type="module">
// load psytask js
import { createApp } from 'https://cdn.jsdelivr.net/npm/psytask@1.1/dist/index.min.js';
using app = await createApp();
//...
</script>
<!-- load jspsych plugin if needed, it should be below psytask js and add `defer` property -->
<script
defer
src="https://cdn.jsdelivr.net/npm/@jspsych/plugin-html-keyboard-response@2.1.0/dist/index.browser.min.js"
></script>
</body>
</html>
```
#### Using jsPsych Plugins
```js
import 'jspsych/css/jspsych.css';
import 'psytask/main.css';
import HtmlButtonResponsePlugin from '@jspsych/plugin-html-button-response';
import { createApp, jsPsychStim } from 'psytask';
// create app
using app = await createApp();
// create jsPsych scene
using jsPsychScene = app.scene(jsPsychStim, {
defaultProps: {
type: HtmlButtonResponsePlugin,
stimulus: 'Hello world',
choices: ['f', 'j'],
},
});
// show jsPsych scene
const data = await jsPsychScene.show();
console.log(data);
```
### JATOS
JATOS (Just Another Tool for Online Studies) is a popular platform for running online psychology experiments. Psytask integrates seamlessly with JATOS for data collection and experiment management. See more: https://www.jatos.org/Write-your-own-Study-Basics-and-Beyond.html
#### Setup with JATOS
```html
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- load jatos.js -->
<script src="jatos.js"></script>
</head>
<body>
<!-- load your task script -->
<script type="module" src="./index.js"></script>
</body>
</html>
```
#### Data Upload
Psytask's data collector can automatically send data to JATOS using event listeners:
```js
import { createApp } from 'psytask';
// wait for jatos loading
await new Promise((r) => jatos.onLoad(r));
using app = await createApp();
using dc = app.collector('experiment_data.csv').on('add', (row) => {
// send data to JATOS server when `dc.add` be called.
jatos.appendResultData(row);
});
```