Skip to content

bluebonesx/psytask

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PsyTask

NPM Version NPM Downloads jsDelivr hits (npm)

JavaScript Framework for Psychology tasks. Make psychology task development like making PPTs. Compatible with the jsPsych plugins.

Compared to jsPsych, PsyTask has:

  • Easier and more flexible development experiment.
  • Higher time precision.
  • Smaller bundle size, Faster loading speed.

API Docs or Play it now ! 🥳

Install

via NPM:

npm create psytask # create a project
npm i psytask # only install

via CDN:

<!-- add required packages  -->
<script type="importmap">
  {
    "imports": {
      "psytask": "https://cdn.jsdelivr.net/npm/psytask@1/dist/index.min.js",
      "@psytask/core": "https://cdn.jsdelivr.net/npm/@psytask/core@1/dist/index.min.js",
      "@psytask/components": "https://cdn.jsdelivr.net/npm/@psytask/components@1/dist/index.min.js",
      "vanjs-core": "https://cdn.jsdelivr.net/npm/[email protected]",
      "vanjs-ext": "https://cdn.jsdelivr.net/npm/[email protected]"
    }
  }
</script>
<!-- load packages -->
<script type="module">
  import { createApp } from 'psytask';

  using app = await creaeApp();
</script>

Warning

PsyTask uses the modern JavaScript using keyword for automatic resource cleanup.

For CDN usage in old browsers that don't support the using keyword, you will see Uncaught SyntaxError: Unexpected identifier 'app'. You need to change the code:

// Instead of: using app = await createApp();
const app = await createApp();
// ... your code ...
app.emit('dispose'); // Manually clean up when done

Or, you can use the bundlers (like Vite, Bun, etc.) to transpile it.

Usage

The psychology tasks are just like PPTs, they both have a series of scenes. So writing a psychology task only requires 2 steps:

  1. create scene
  2. show scene

Create Scene

import { Container } from '@psytask/components';

using simpleText = app.scene(
  // scene setup
  Container,
  // scene options
  {
    defaultProps: { content: '' }, // props is show params
    duration: 1e3, // show 1000ms
    close_on: 'key: ', // close on space key
  },
);

Most of the time, you need to write the scene yourself, see setup scene:

using scene = app.scene(
  /** @param {{ text: string }} props */
  (props, ctx) => {
    /** @type {{ response_key: string; response_time: number }} */
    let data;
    const node = document.createElement('div');

    ctx
      .on('scene:show', (newProps) => {
        // Reset data when the scene shows
        data = { response_key: '', response_time: 0 };
        // update DOM
        node.textContent = newProps.text;
      })
      // Capture keyboard responses
      .on('key:f', (e) => {
        data = { response_key: e.key, response_time: e.timeStamp };
        ctx.close(); // close scene when key f was pressed
      })
      .on('key:j', (e) => {
        data = { response_key: e.key, response_time: e.timeStamp };
        ctx.close();
      });

    // Return the element and data getter
    return {
      // use other Component
      node: Container({ content: node }, ctx),
      // data getter
      data: () => data,
    };
  },
  {
    defaultProps: { text: '' }, // same with setup params
    duration: 1e3,
    close_on: 'mouse:left',
  },
);

Tip

use JSDoc Comment to get type hint in JavaScript.

Show Scene

// show with parameters
const data = await scene.show({ text: 'Press F or J' });
// show with new scene options
const data = await scene.config({ duration: Math.random() * 1e3 }).show();

Usually, we need to show a block:

import { RandomSampling, StairCase } from 'psytask';

// fixed sequence
for (const text of ['A', 'B', 'C']) {
  await scene.show({ text });
}

// random sequence
for (const text of RandomSampling({
  candidates: ['A', 'B', 'C'],
  sample: 10,
  replace: true,
})) {
  await scene.show({ text });
}

// staircase
const staircase = StairCase({
  start: 10,
  step: 1,
  up: 3,
  down: 1,
  reversals: 6,
  min: 1,
  max: 12,
  trial: 20,
});
for (const value of staircase) {
  const data = await scene.show({ text: value });
  const correct = data.response_key === 'f';
  staircase.response(correct); // set response
}

Data Collection

using dc = app.collector('data.csv');

for (const text of ['A', 'B', 'C']) {
  const data = await scene.show({ text });
  // add a row
  dc.add({
    text,
    response: data.response_key,
    rt: data.response_time - data.start_time,
    correct: data.response_key === 'f',
  });
}

dc.final(); // get final text
dc.download(); // download file

Integration

Add packages:

npm i @psytask/jspsych @jspsych/plugin-cloze
npm i -d jspsych # for type hint

Or using CDN:

<!-- load jspsych css-->
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/[email protected]/css/jspsych.css"
/>
<!-- add packages -->
<script type="importmap">
  {
    "imports": {
      ...
      "@psytask/jspsych": "https://cdn.jsdelivr.net/npm/@psytask/jspsych@1/dist/index.min.js",
      "@jspsych/plugin-cloze": "https://cdn.jsdelivr.net/npm/@jspsych/[email protected]/+esm"
    }
  }
</script>

Important

For CDNer, you should add the +esm after the jspsych plugin CDN URL, because jspsych plugins do not release ESM versions.

Then use it:

import { jsPsychStim } from '@psytask/jspsych';
import Cloze from '@jspsych/plugin-cloze';

using jspsych = app.scene(jsPsychStim, { defaultProps: {} });
const data = await jspsych.show({
  type: Cloze,
  text: 'aba%%aba',
  check_answers: true,
});

See official docs

<script src="jatos.js"></script>
// wait for jatos loading
await new Promise((r) => jatos.onLoad(r));

using dc = app.collector().on('add', (row) => {
  // send data to JATOS server
  jatos.appendResultData(row);
});

Learn more

So, how it works.

Setup Scene

To create a scene, we need a setup function that inputs Props and Context, and outputs a object includes Node and Data Getter:

const setup = (props, ctx) => ({
  node: '',
  data: () => ({}),
});
using scene = app.scene(setup);
  • Props means show params that control the display of the scene.
  • Context is the current scene itself, which is usually used to add event listeners.
  • Node is the string or element which be mounted to the scene root element.
  • Data Getter is used to get generated data for each show.

If you don't want to generate any data, just return Node:

const setup = (props, ctx) => '';

Caution

You shouldn't modify props, as it may change the default props.

If you don't know whether you modify the default props, try to recursively freeze all its properties:

const recurFreeze =
  /**
   * @template {object} T
   * @param {T} obj
   * @returns {T}
   */
  (obj) => {
    for (const v of Object.values(obj))
      v != null && typeof v === 'object' && recurFreeze(v);
    return Object.freeze(obj);
  };
const createProps =
  /**
   * @template {object} T
   * @param {T} obj
   * @returns {T}
   */
  (obj) => Object.create(recurFreeze(obj));

using scene = app.scene(
  /**
   * @param {{
   *   a: string;
   *   b: number[];
   *   c: { d: string[] };
   * }} props
   */
  (props) => '',
  {
    defaultProps: createProps({
      a: '',
      b: [],
      c: { d: [] },
    }),
  },
);

Update DOM

When you create a scene, the setup function will be called with the default Props, then the Node will be mounted. So if you want to update Node in each show, you should listen scene:show event:

const setup = (props, ctx) => {
  const node = document.createElement('div');
  ctx.on('scene:show', (newProps) => {
    node.textContent = newProps.text;
  });
  return node;
};

Or, you can use VanJS power via adapter, which provides reactivity update:

import { adapter } from '@psytask/components';
import van from 'vanjs-core';

const { div } = van.tags;
const setup = adapter((props, ctx) => div(() => props.text));

Show

graph TD
scene:show --> b[show & focus root<br>add listeners to root] --> d[wait timer] --> scene:frame --> d --> e[hide root] --> scene:close
Loading

Reactivity

Stay tuned...

About

JavaScript Framework for Psychology tasks. Compatible with the jsPsych plugin.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published