PsyTask | API Docs
    Preparing search index...

    Module psytask

    PsyTask

    NPM Version NPM Downloads jsDelivr hits (npm)

    JavaScript Framework for Psychology tasks. Make development like making PPTs.

    Compared to others, it:

    • Easier and more flexible
    • Higher time precision. See benchmark
    • Smaller bundle size, Faster loading speed. See benchmark
    • Type-Safe

    Integration with:

    API Docs | Benchmark | Tests | Play it now ! 🥳

    Note

    The project is in early development. Please pin the version when using it.

    via NPM:

    npm create psytask # optional: use template
    npm install psytask # only package
    npm install @psytask/component vanjs-core vanjs-ext # optional: use components

    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/vanjs-core@1.6",
    "vanjs-ext": "https://cdn.jsdelivr.net/npm/vanjs-ext@0.6"
    }
    }
    </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.

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

    All you need is Component:

    import { Grating, adapter } from '@psytask/components';

    using simpleText = app.scene(
    // component
    Grating,
    // scene options
    {
    adapter, // VanJS support
    defaultProps: { type: Math.sin, size: 100, sf: 0.02 }, // show params
    duration: 1e3, // show 1000 ms
    close_on: 'key: ', // close on space key
    },
    );

    Override default props or options:

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

    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
    }
    using dc = app.collector('data.csv');

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

    // `frame_times` will be recorded automatically
    const start_time = /** @type {number} */ (data.frame_times[0]);

    // add a row
    dc.add({
    text,
    response: data.response_key,
    rt: data.response_time - start_time,
    correct: data.response_key === 'f',
    });
    }

    dc.final(); // file content
    dc.download(); // download file

    It a function that inputs Props and outputs an object includes Node and Data Getter:

    • Props means the show parameters that control the display of the scene.
    • Node is a string or element, or array, that is mounted to the scene root element.
    • Data Getter is used to get generated data.
    const Component = (props) => {
    const ctx = getCurrentScene();
    return { node: '', data: () => ({}) };
    };
    const Component = (props) => 'text node';
    const Component = (props) => document.createElement('div');
    const Component = (props) => ['text node', document.createElement('div')];
    Caution

    You shouldn't modify props, whatever, as it may change the default props. See one-way data flow in Redux and Vue.

    A practical example:

    import { on, getCurrentScene } from 'psytask';
    import { ImageStim, adapter } from '@psytask/components';
    import van from 'vanjs-core';

    const { div } = van.tags;
    const Component =
    /** @param {{ text: string }} props */
    (props) => {
    /** @type {{ response_key: string; response_time: number }} */
    let data;
    const ctx = getCurrentScene();

    // add DOM event listener
    const cleanup = on(ctx.root, 'keydown', (e) => {
    if (e.key !== 'f' || e.key !== 'j') return;
    data = { response_key: e.key, response_time: e.timeStamp };
    ctx.close(); // close on 'f' or 'j'
    });

    ctx
    // reset data on show
    .on('show', () => {
    data = { response_key: '', response_time: 0 };
    })
    // remove DOM event listener on dispose
    .on('dispose', cleanup);

    // Return the element and data getter
    return {
    node: div(
    // use other Component
    ImageStim({ image: new ImageData(1) }),
    ),
    data: () => data,
    };
    };
    Tip

    Use JSDoc Comment to get type hint in JavaScript.

    When you call app.scene(Component, { adapter, defaultProps }), it will use adapter.render to call Component with defaultProps once, then Node will be mounted to this.root.

    Note

    The component will be called only once; the following DOM update will be triggered by the Props update. See reactivity.

    When you call await scene.show(patchProps), it will execute the following process:

    • Update props: merge patch props with default props to update current props, which will trigger reactivity update.
    • Listeners added by this.on('show') will be called.
    • Display and focus this.root, it will be displayed on the screen in the next frame.
    • Create a timer by this.options.timer and wait for it to stop.
    • Listeners added by this.on('frame') will be called when the timer is running.
    • Hide this.root when the timer is stopped, it will be hidden on the screen in the next frame.
    • Listeners added by this.on('close') will be called.
    • Merge the timer records and the data from Data Getter.
    graph LR
    a[update props] --> l1[on show] --> b[display & focus DOM] --> d[wait timer] --> l2[on frame] --> d --> e[hide root] --> l3[on close] --> f[merge data]
    

    Stay tuned...

    Better to see: VanJS tutorial, Vue reactivity

    The bundle size of PsyTask is 1/12 of labjs, 1/50 of jspsych, and 1/260 of psychojs.

    xychart
        title "Bundle Size (KB)"
        x-axis [psytask, labjs, jspsych, psychojs]
        y-axis 0 --> 2600
        bar [10.67, 122.45, 502.06, 2598.33]
    
    npm i @psytask/jspsych @jspsych/plugin-cloze
    npm i -d jspsych # optional: for type hint

    Or using CDN:

    <!-- load jspsych css-->
    <link
    rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/jspsych@8.2.2/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/plugin-cloze@2.2.0/+esm"
    }
    }
    </script>
    Important

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

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

    using jspsych = app.scene(jsPsychStim, {
    defaultProps: {
    type: Cloze,
    text: 'aba%%aba',
    check_answers: true,
    },
    });
    const data = await jspsych.show();
    <!-- add jatos script -->
    <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);
    });

    Classes

    App
    Collector
    EventEmitter
    Scene

    Type Aliases

    CloseEventMap
    Component
    ComponentAdapter
    NodeLike
    SceneEventMap
    SceneOptions
    Serializer
    Timer
    TimerRecords

    Variables

    createComponentAdapter
    createTimer
    generic
    getCurrentScene
    RandomSampling
    StairCase

    Functions

    createApp
    createIterableBuilder
    css
    defaultProps
    detectFPS
    on