- Notifications
You must be signed in to change notification settings - Fork0
World smallest JavaScript Behavioral Experimental Framework. Compatible with the jsPsych plugin.
License
bluebonesx/psytask
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
JavaScript Framework for Psychology tasks.Make development like making PPTs.
Compared to others, it:
- Easier and more flexible
- Higher time precision. Seebenchmark
- Smaller bundle size, Faster loading speed. Seebenchmark
- Type-Safe
Integration with:
- jsPsych plugins.
- Data server:JATOS ...
- UI framework:Vue,Solid,Lit,Van ...
- Reactive framework:Rxjs,Mobx,Valtio ...
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 templatenpm install psytask# only packagenpm install @psytask/component vanjs-core vanjs-ext# optional: use components
via CDN:
<!-- add required packages --><scripttype="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 --><scripttype="module">import{createApp}from'psytask';usingapp=awaitcreaeApp();</script>
Warning
PsyTask uses the modern JavaScriptusing keyword for automatic resource cleanup.
For CDN usage in old browsers that don't support theusing keyword, you will seeUncaught SyntaxError: Unexpected identifier 'app'. You need to change the code:
// Instead of: using app = await createApp();constapp=awaitcreateApp();// ... 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 isComponent:
import{Grating,adapter}from'@psytask/components';usingsimpleText=app.scene(// componentGrating,// scene options{ adapter,// VanJS supportdefaultProps:{type:Math.sin,size:100,sf:0.02},// show paramsduration:1e3,// show 1000 msclose_on:'key: ',// close on space key},);
Override default props or options:
constdata=awaitscene.show({text:'Press F or J'});// new propsconstdata=awaitscene.config({duration:1e3}).show();// new options
Block:
import{RandomSampling,StairCase}from'psytask';// fixed sequencefor(consttextof['A','B','C']){awaitscene.show({ text});}// random sequencefor(consttextofRandomSampling({candidates:['A','B','C'],sample:10,replace:true,})){awaitscene.show({ text});}// staircaseconststaircase=StairCase({start:10,step:1,up:3,down:1,reversals:6,min:1,max:12,trial:20,});for(constvalueofstaircase){constdata=awaitscene.show({text:value});constcorrect=data.response_key==='f';staircase.response(correct);// set response}
usingdc=app.collector('data.csv');for(consttextof['A','B','C']){constdata=awaitscene.show({ text});// `frame_times` will be recorded automaticallyconststart_time=/**@type {number} */(data.frame_times[0]);// add a rowdc.add({ text,response:data.response_key,rt:data.response_time-start_time,correct:data.response_key==='f',});}dc.final();// file contentdc.download();// download file
It a functionthat inputsProps and outputs an object includesNode andData 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.
constComponent=(props)=>{constctx=getCurrentScene();return{node:'',data:()=>({})};};constComponent=(props)=>'text node';constComponent=(props)=>document.createElement('div');constComponent=(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 inRedux andVue.
A practical example:
import{on,getCurrentScene}from'psytask';import{ImageStim,adapter}from'@psytask/components';importvanfrom'vanjs-core';const{ div}=van.tags;constComponent=/**@param {{ text: string }} props */(props)=>{/**@type {{ response_key: string; response_time: number }} */letdata;constctx=getCurrentScene();// add DOM event listenerconstcleanup=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 getterreturn{node:div(// use other ComponentImageStim({image:newImageData(1)}),),data:()=>data,};};
Tip
UseJSDoc Comment to get type hint in JavaScript.
When you callapp.scene(Component, { adapter, defaultProps }), it will useadapter.render to callComponent withdefaultProps once, thenNode will be mounted tothis.root.
Note
The component will be called only once; the following DOM update will be triggered by theProps update. Seereactivity.
When you callawait scene.show(patchProps), it will execute the following process:
- Update props: merge patch props with default props to update current props, which will triggerreactivity update.
- Listeners added by
this.on('show')will be called. - Display and focus
this.root, it will be displayed on the screenin the next frame. - Create a timer by
this.options.timerand wait for it to stop. - Listeners added by
this.on('frame')will be called when the timer is running. - Hide
this.rootwhen the timer is stopped, it will be hidden on the screenin the next frame. - Listeners added by
this.on('close')will be called. - Merge the timer records and the data fromData Getter.
graph LRa[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-clozenpm i -d jspsych# optional: for type hintOr using CDN:
<!-- load jspsych css--><linkrel="stylesheet"href="https://cdn.jsdelivr.net/npm/jspsych@8.2.2/css/jspsych.css"/><!-- add packages --><scripttype="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 useesm.sh.
import{jsPsychStim}from'@psytask/jspsych';importClozefrom'@jspsych/plugin-cloze';usingjspsych=app.scene(jsPsychStim,{defaultProps:{type:Cloze,text:'aba%%aba',check_answers:true,},});constdata=awaitjspsych.show();
<!-- add jatos script --><scriptsrc="jatos.js"></script>
// wait for jatos loadingawaitnewPromise((r)=>jatos.onLoad(r));usingdc=app.collector().on('add',(row)=>{// send data to JATOS serverjatos.appendResultData(row);});
About
World smallest JavaScript Behavioral Experimental Framework. Compatible with the jsPsych plugin.
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.