jspsych-tangram 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +327 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +180 -8
- package/dist/index.js +328 -6
- package/dist/index.js.map +1 -1
- package/dist/nback/index.browser.js +17083 -0
- package/dist/nback/index.browser.js.map +1 -0
- package/dist/nback/index.browser.min.js +42 -0
- package/dist/nback/index.browser.min.js.map +1 -0
- package/dist/nback/index.cjs +427 -0
- package/dist/nback/index.cjs.map +1 -0
- package/dist/nback/index.d.ts +175 -0
- package/dist/nback/index.js +425 -0
- package/dist/nback/index.js.map +1 -0
- package/package.json +9 -3
- package/src/index.ts +2 -1
- package/src/plugins/tangram-nback/NBackApp.tsx +315 -0
- package/src/plugins/tangram-nback/index.ts +141 -0
- package/tangram-nback.min.js +42 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { JsPsychPlugin, ParameterType, JsPsych, TrialType } from 'jspsych';
|
|
2
|
+
|
|
3
|
+
declare const info: {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
parameters: {
|
|
7
|
+
/** Single tangram specification to display */
|
|
8
|
+
tangram: {
|
|
9
|
+
type: ParameterType;
|
|
10
|
+
default: undefined;
|
|
11
|
+
description: string;
|
|
12
|
+
};
|
|
13
|
+
/** Whether this trial is a match (for computing accuracy) */
|
|
14
|
+
isMatch: {
|
|
15
|
+
type: ParameterType;
|
|
16
|
+
default: undefined;
|
|
17
|
+
description: string;
|
|
18
|
+
};
|
|
19
|
+
/** Whether to show tangram decomposed into individual primitives with borders */
|
|
20
|
+
show_tangram_decomposition: {
|
|
21
|
+
type: ParameterType;
|
|
22
|
+
default: boolean;
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
25
|
+
/** HTML content to display above the tangram as instructions */
|
|
26
|
+
instructions: {
|
|
27
|
+
type: ParameterType;
|
|
28
|
+
default: string;
|
|
29
|
+
description: string;
|
|
30
|
+
};
|
|
31
|
+
/** Text to display on response button */
|
|
32
|
+
button_text: {
|
|
33
|
+
type: ParameterType;
|
|
34
|
+
default: string;
|
|
35
|
+
description: string;
|
|
36
|
+
};
|
|
37
|
+
/** Duration to display tangram and accept responses (milliseconds) */
|
|
38
|
+
duration: {
|
|
39
|
+
type: ParameterType;
|
|
40
|
+
default: number;
|
|
41
|
+
description: string;
|
|
42
|
+
};
|
|
43
|
+
/** Callback fired when trial ends */
|
|
44
|
+
onTrialEnd: {
|
|
45
|
+
type: ParameterType;
|
|
46
|
+
default: undefined;
|
|
47
|
+
description: string;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
data: {
|
|
51
|
+
/** Whether participant clicked the response button before duration expired */
|
|
52
|
+
responded_match: {
|
|
53
|
+
type: ParameterType;
|
|
54
|
+
description: string;
|
|
55
|
+
};
|
|
56
|
+
/** Reaction time in milliseconds (NaN if no response or response after duration) */
|
|
57
|
+
rt: {
|
|
58
|
+
type: ParameterType;
|
|
59
|
+
description: string;
|
|
60
|
+
};
|
|
61
|
+
/** Accuracy: 1 if correct, 0 if incorrect, NaN if isMatch not provided */
|
|
62
|
+
accuracy: {
|
|
63
|
+
type: ParameterType;
|
|
64
|
+
description: string;
|
|
65
|
+
};
|
|
66
|
+
/** Whether response occurred after duration expired */
|
|
67
|
+
responded_after_duration: {
|
|
68
|
+
type: ParameterType;
|
|
69
|
+
description: string;
|
|
70
|
+
};
|
|
71
|
+
/** Time of late response (NaN if no late response) */
|
|
72
|
+
rt_after_duration: {
|
|
73
|
+
type: ParameterType;
|
|
74
|
+
description: string;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
citations: string;
|
|
78
|
+
};
|
|
79
|
+
type Info = typeof info;
|
|
80
|
+
/**
|
|
81
|
+
* **tangram-nback**
|
|
82
|
+
*
|
|
83
|
+
* A jsPsych plugin for n-back matching trials displaying a single tangram
|
|
84
|
+
* with a response button.
|
|
85
|
+
*
|
|
86
|
+
* @author Justin Yang & Sean Paul Anderson
|
|
87
|
+
* @see {@link https://github.com/cogtoolslab/tangram_construction.git/tree/main/experiments/jspsych-tangram-prep}
|
|
88
|
+
*/
|
|
89
|
+
declare class TangramNBackPlugin implements JsPsychPlugin<Info> {
|
|
90
|
+
private jsPsych;
|
|
91
|
+
static info: {
|
|
92
|
+
name: string;
|
|
93
|
+
version: string;
|
|
94
|
+
parameters: {
|
|
95
|
+
/** Single tangram specification to display */
|
|
96
|
+
tangram: {
|
|
97
|
+
type: ParameterType;
|
|
98
|
+
default: undefined;
|
|
99
|
+
description: string;
|
|
100
|
+
};
|
|
101
|
+
/** Whether this trial is a match (for computing accuracy) */
|
|
102
|
+
isMatch: {
|
|
103
|
+
type: ParameterType;
|
|
104
|
+
default: undefined;
|
|
105
|
+
description: string;
|
|
106
|
+
};
|
|
107
|
+
/** Whether to show tangram decomposed into individual primitives with borders */
|
|
108
|
+
show_tangram_decomposition: {
|
|
109
|
+
type: ParameterType;
|
|
110
|
+
default: boolean;
|
|
111
|
+
description: string;
|
|
112
|
+
};
|
|
113
|
+
/** HTML content to display above the tangram as instructions */
|
|
114
|
+
instructions: {
|
|
115
|
+
type: ParameterType;
|
|
116
|
+
default: string;
|
|
117
|
+
description: string;
|
|
118
|
+
};
|
|
119
|
+
/** Text to display on response button */
|
|
120
|
+
button_text: {
|
|
121
|
+
type: ParameterType;
|
|
122
|
+
default: string;
|
|
123
|
+
description: string;
|
|
124
|
+
};
|
|
125
|
+
/** Duration to display tangram and accept responses (milliseconds) */
|
|
126
|
+
duration: {
|
|
127
|
+
type: ParameterType;
|
|
128
|
+
default: number;
|
|
129
|
+
description: string;
|
|
130
|
+
};
|
|
131
|
+
/** Callback fired when trial ends */
|
|
132
|
+
onTrialEnd: {
|
|
133
|
+
type: ParameterType;
|
|
134
|
+
default: undefined;
|
|
135
|
+
description: string;
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
data: {
|
|
139
|
+
/** Whether participant clicked the response button before duration expired */
|
|
140
|
+
responded_match: {
|
|
141
|
+
type: ParameterType;
|
|
142
|
+
description: string;
|
|
143
|
+
};
|
|
144
|
+
/** Reaction time in milliseconds (NaN if no response or response after duration) */
|
|
145
|
+
rt: {
|
|
146
|
+
type: ParameterType;
|
|
147
|
+
description: string;
|
|
148
|
+
};
|
|
149
|
+
/** Accuracy: 1 if correct, 0 if incorrect, NaN if isMatch not provided */
|
|
150
|
+
accuracy: {
|
|
151
|
+
type: ParameterType;
|
|
152
|
+
description: string;
|
|
153
|
+
};
|
|
154
|
+
/** Whether response occurred after duration expired */
|
|
155
|
+
responded_after_duration: {
|
|
156
|
+
type: ParameterType;
|
|
157
|
+
description: string;
|
|
158
|
+
};
|
|
159
|
+
/** Time of late response (NaN if no late response) */
|
|
160
|
+
rt_after_duration: {
|
|
161
|
+
type: ParameterType;
|
|
162
|
+
description: string;
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
citations: string;
|
|
166
|
+
};
|
|
167
|
+
constructor(jsPsych: JsPsych);
|
|
168
|
+
/**
|
|
169
|
+
* Launches the trial by invoking startNBackTrial
|
|
170
|
+
* with the display element, parameters, and jsPsych instance.
|
|
171
|
+
*/
|
|
172
|
+
trial(display_element: HTMLElement, trial: TrialType<Info>): void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { TangramNBackPlugin as default };
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { ParameterType } from 'jspsych';
|
|
2
|
+
import React, { useRef, useState, useEffect } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
|
|
5
|
+
function polysAABB(polys) {
|
|
6
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
7
|
+
for (const poly of polys) {
|
|
8
|
+
for (const p of poly) {
|
|
9
|
+
if (p.x < minX) minX = p.x;
|
|
10
|
+
if (p.y < minY) minY = p.y;
|
|
11
|
+
if (p.x > maxX) maxX = p.x;
|
|
12
|
+
if (p.y > maxY) maxY = p.y;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const width = Math.max(1, maxX - minX);
|
|
16
|
+
const height = Math.max(1, maxY - minY);
|
|
17
|
+
return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY }, width, height, cx: (minX + maxX) / 2, cy: (minY + maxY) / 2 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CONFIG = {
|
|
21
|
+
color: {
|
|
22
|
+
bands: {
|
|
23
|
+
silhouette: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" },
|
|
24
|
+
workspace: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" }
|
|
25
|
+
},
|
|
26
|
+
completion: { fill: "#ccfff2", stroke: "#13da57" },
|
|
27
|
+
silhouetteMask: "#374151",
|
|
28
|
+
anchors: { invalid: "#7dd3fc", valid: "#475569" },
|
|
29
|
+
piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
|
|
30
|
+
ui: { light: "#60a5fa", dark: "#1d4ed8" },
|
|
31
|
+
blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
|
|
32
|
+
tangramDecomposition: { stroke: "#fef2cc" }
|
|
33
|
+
},
|
|
34
|
+
opacity: {
|
|
35
|
+
blueprint: 0.4,
|
|
36
|
+
silhouetteMask: 0.25,
|
|
37
|
+
//anchors: { valid: 0.80, invalid: 0.50 },
|
|
38
|
+
anchors: { invalid: 0, valid: 0 },
|
|
39
|
+
piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
|
|
40
|
+
},
|
|
41
|
+
size: {
|
|
42
|
+
stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
|
|
43
|
+
anchorRadiusPx: { valid: 1, invalid: 1 },
|
|
44
|
+
badgeFontPx: 16,
|
|
45
|
+
centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
|
|
46
|
+
},
|
|
47
|
+
layout: {
|
|
48
|
+
grid: { stepPx: 20, unitPx: 40 },
|
|
49
|
+
paddingPx: 1,
|
|
50
|
+
viewportScale: 0.8,
|
|
51
|
+
constraints: {
|
|
52
|
+
workspaceDiamAnchors: 10,
|
|
53
|
+
// num anchors req'd to be on diagonal
|
|
54
|
+
quickstashDiamAnchors: 7,
|
|
55
|
+
// num anchors req'd to be in single quickstash slot
|
|
56
|
+
primitiveDiamAnchors: 5
|
|
57
|
+
},
|
|
58
|
+
defaults: { maxQuickstashSlots: 1 }
|
|
59
|
+
},
|
|
60
|
+
game: {
|
|
61
|
+
snapRadiusPx: 15,
|
|
62
|
+
showBorders: false,
|
|
63
|
+
hideTouchingBorders: true
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const GRID_PX = CONFIG.layout.grid.stepPx;
|
|
68
|
+
function igcd(a, b) {
|
|
69
|
+
a = Math.round(Math.abs(a));
|
|
70
|
+
b = Math.round(Math.abs(b));
|
|
71
|
+
while (b) [a, b] = [b, a % b];
|
|
72
|
+
return a || 1;
|
|
73
|
+
}
|
|
74
|
+
function inferUnitFromPolys(polys) {
|
|
75
|
+
let g = 0;
|
|
76
|
+
for (const poly of polys) {
|
|
77
|
+
for (let i = 0; i < poly.length; i++) {
|
|
78
|
+
const a = poly[i], b = poly[(i + 1) % poly.length];
|
|
79
|
+
if (!a || !b) continue;
|
|
80
|
+
const dx = Math.round(Math.abs(b.x - a.x));
|
|
81
|
+
const dy = Math.round(Math.abs(b.y - a.y));
|
|
82
|
+
if (dx) g = g ? igcd(g, dx) : dx;
|
|
83
|
+
if (dy) g = g ? igcd(g, dy) : dy;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return g || 1;
|
|
87
|
+
}
|
|
88
|
+
function placeSilhouetteGridAlignedAsPolys(polys, S, rectCenter) {
|
|
89
|
+
if (!polys || polys.length === 0) return [];
|
|
90
|
+
const a = polysAABB(polys);
|
|
91
|
+
const cx0 = (a.min.x + a.max.x) / 2;
|
|
92
|
+
const cy0 = (a.min.y + a.max.y) / 2;
|
|
93
|
+
const cx = Math.round(rectCenter.cx / GRID_PX) * GRID_PX;
|
|
94
|
+
const cy = Math.round(rectCenter.cy / GRID_PX) * GRID_PX;
|
|
95
|
+
const tx = cx - S * cx0;
|
|
96
|
+
const ty = cy - S * cy0;
|
|
97
|
+
const stx = Math.round(tx / GRID_PX) * GRID_PX;
|
|
98
|
+
const sty = Math.round(ty / GRID_PX) * GRID_PX;
|
|
99
|
+
return polys.map((poly) => poly.map((p) => ({ x: S * p.x + stx, y: S * p.y + sty })));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function startNBackTrial(display_element, params, _jsPsych) {
|
|
103
|
+
const root = createRoot(display_element);
|
|
104
|
+
root.render(React.createElement(NBackView, { params }));
|
|
105
|
+
return { root, display_element, jsPsych: _jsPsych };
|
|
106
|
+
}
|
|
107
|
+
function NBackView({ params }) {
|
|
108
|
+
const {
|
|
109
|
+
tangram,
|
|
110
|
+
isMatch,
|
|
111
|
+
show_tangram_decomposition,
|
|
112
|
+
instructions,
|
|
113
|
+
button_text,
|
|
114
|
+
duration,
|
|
115
|
+
onTrialEnd
|
|
116
|
+
} = params;
|
|
117
|
+
const trialStartTime = useRef(Date.now());
|
|
118
|
+
const buttonEnabledRef = useRef(true);
|
|
119
|
+
const timeoutIdRef = useRef(null);
|
|
120
|
+
const hasRespondedRef = useRef(false);
|
|
121
|
+
const responseDataRef = useRef(null);
|
|
122
|
+
const [buttonDisabled, setButtonDisabled] = useState(false);
|
|
123
|
+
const CANON = /* @__PURE__ */ new Set([
|
|
124
|
+
"square",
|
|
125
|
+
"smalltriangle",
|
|
126
|
+
"parallelogram",
|
|
127
|
+
"medtriangle",
|
|
128
|
+
"largetriangle"
|
|
129
|
+
]);
|
|
130
|
+
const filteredTans = tangram.solutionTans.filter((tan) => {
|
|
131
|
+
const tanName = tan.name ?? tan.kind;
|
|
132
|
+
return CANON.has(tanName);
|
|
133
|
+
});
|
|
134
|
+
const mask = filteredTans.map((tan) => {
|
|
135
|
+
const polygon = tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }));
|
|
136
|
+
return polygon;
|
|
137
|
+
});
|
|
138
|
+
const primitiveDecomposition = filteredTans.map((tan) => ({
|
|
139
|
+
kind: tan.name ?? tan.kind,
|
|
140
|
+
polygon: tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }))
|
|
141
|
+
}));
|
|
142
|
+
const DISPLAY_SIZE = 400;
|
|
143
|
+
const viewport = {
|
|
144
|
+
w: DISPLAY_SIZE,
|
|
145
|
+
h: DISPLAY_SIZE
|
|
146
|
+
};
|
|
147
|
+
const scaleS = React.useMemo(() => {
|
|
148
|
+
const u = inferUnitFromPolys(mask);
|
|
149
|
+
return u ? CONFIG.layout.grid.unitPx / u : 1;
|
|
150
|
+
}, [mask]);
|
|
151
|
+
const centerPos = {
|
|
152
|
+
cx: viewport.w / 2,
|
|
153
|
+
cy: viewport.h / 2
|
|
154
|
+
};
|
|
155
|
+
const pathD = (poly) => {
|
|
156
|
+
if (!poly || poly.length === 0) return "";
|
|
157
|
+
const moves = poly.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`);
|
|
158
|
+
return moves.join(" ") + " Z";
|
|
159
|
+
};
|
|
160
|
+
const endTrial = (data) => {
|
|
161
|
+
if (timeoutIdRef.current) {
|
|
162
|
+
clearTimeout(timeoutIdRef.current);
|
|
163
|
+
timeoutIdRef.current = null;
|
|
164
|
+
}
|
|
165
|
+
const accuracy = isMatch !== void 0 ? isMatch === data.responded_match ? 1 : 0 : NaN;
|
|
166
|
+
const trialData = {
|
|
167
|
+
...data,
|
|
168
|
+
accuracy,
|
|
169
|
+
tangram_id: tangram.tangramID,
|
|
170
|
+
is_match: isMatch
|
|
171
|
+
};
|
|
172
|
+
if (onTrialEnd) {
|
|
173
|
+
onTrialEnd(trialData);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const handleButtonClick = () => {
|
|
177
|
+
if (!buttonEnabledRef.current) {
|
|
178
|
+
const rt_late = Date.now() - trialStartTime.current;
|
|
179
|
+
hasRespondedRef.current = true;
|
|
180
|
+
responseDataRef.current = {
|
|
181
|
+
responded_match: true,
|
|
182
|
+
rt: NaN,
|
|
183
|
+
responded_after_duration: true,
|
|
184
|
+
rt_after_duration: rt_late
|
|
185
|
+
};
|
|
186
|
+
endTrial(responseDataRef.current);
|
|
187
|
+
} else {
|
|
188
|
+
const rt = Date.now() - trialStartTime.current;
|
|
189
|
+
buttonEnabledRef.current = false;
|
|
190
|
+
setButtonDisabled(true);
|
|
191
|
+
hasRespondedRef.current = true;
|
|
192
|
+
responseDataRef.current = {
|
|
193
|
+
responded_match: true,
|
|
194
|
+
rt,
|
|
195
|
+
responded_after_duration: false,
|
|
196
|
+
rt_after_duration: NaN
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
timeoutIdRef.current = setTimeout(() => {
|
|
202
|
+
buttonEnabledRef.current = false;
|
|
203
|
+
if (hasRespondedRef.current && responseDataRef.current) {
|
|
204
|
+
endTrial(responseDataRef.current);
|
|
205
|
+
} else {
|
|
206
|
+
endTrial({
|
|
207
|
+
responded_match: false,
|
|
208
|
+
rt: NaN,
|
|
209
|
+
responded_after_duration: false,
|
|
210
|
+
rt_after_duration: NaN
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}, duration);
|
|
214
|
+
return () => {
|
|
215
|
+
if (timeoutIdRef.current) {
|
|
216
|
+
clearTimeout(timeoutIdRef.current);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}, []);
|
|
220
|
+
const renderSilhouette = () => {
|
|
221
|
+
if (show_tangram_decomposition) {
|
|
222
|
+
const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
|
|
223
|
+
const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, centerPos);
|
|
224
|
+
return /* @__PURE__ */ React.createElement("g", { key: "sil-decomposed", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(React.Fragment, { key: `prim-${i}` }, /* @__PURE__ */ React.createElement(
|
|
225
|
+
"path",
|
|
226
|
+
{
|
|
227
|
+
d: pathD(scaledPoly),
|
|
228
|
+
fill: CONFIG.color.silhouetteMask,
|
|
229
|
+
opacity: CONFIG.opacity.silhouetteMask,
|
|
230
|
+
stroke: "none"
|
|
231
|
+
}
|
|
232
|
+
), /* @__PURE__ */ React.createElement(
|
|
233
|
+
"path",
|
|
234
|
+
{
|
|
235
|
+
d: pathD(scaledPoly),
|
|
236
|
+
fill: "none",
|
|
237
|
+
stroke: CONFIG.color.tangramDecomposition.stroke,
|
|
238
|
+
strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
|
|
239
|
+
}
|
|
240
|
+
))));
|
|
241
|
+
} else {
|
|
242
|
+
const placedPolys = placeSilhouetteGridAlignedAsPolys(mask, scaleS, centerPos);
|
|
243
|
+
return /* @__PURE__ */ React.createElement("g", { key: "sil-unified", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(
|
|
244
|
+
"path",
|
|
245
|
+
{
|
|
246
|
+
key: `sil-${i}`,
|
|
247
|
+
d: pathD(scaledPoly),
|
|
248
|
+
fill: CONFIG.color.silhouetteMask,
|
|
249
|
+
opacity: CONFIG.opacity.silhouetteMask,
|
|
250
|
+
stroke: "none"
|
|
251
|
+
}
|
|
252
|
+
)));
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
return /* @__PURE__ */ React.createElement("div", { style: {
|
|
256
|
+
display: "flex",
|
|
257
|
+
flexDirection: "column",
|
|
258
|
+
alignItems: "center",
|
|
259
|
+
justifyContent: "center",
|
|
260
|
+
padding: "20px",
|
|
261
|
+
background: "#f5f5f5"
|
|
262
|
+
} }, instructions && /* @__PURE__ */ React.createElement(
|
|
263
|
+
"div",
|
|
264
|
+
{
|
|
265
|
+
style: {
|
|
266
|
+
maxWidth: "800px",
|
|
267
|
+
width: "100%",
|
|
268
|
+
marginBottom: "30px",
|
|
269
|
+
textAlign: "center",
|
|
270
|
+
fontSize: "18px",
|
|
271
|
+
lineHeight: "1.5"
|
|
272
|
+
},
|
|
273
|
+
dangerouslySetInnerHTML: { __html: instructions }
|
|
274
|
+
}
|
|
275
|
+
), /* @__PURE__ */ React.createElement("div", { style: {
|
|
276
|
+
display: "flex",
|
|
277
|
+
flexDirection: "column",
|
|
278
|
+
alignItems: "center",
|
|
279
|
+
gap: "30px"
|
|
280
|
+
} }, /* @__PURE__ */ React.createElement(
|
|
281
|
+
"svg",
|
|
282
|
+
{
|
|
283
|
+
width: viewport.w,
|
|
284
|
+
height: viewport.h,
|
|
285
|
+
viewBox: `0 0 ${viewport.w} ${viewport.h}`,
|
|
286
|
+
style: {
|
|
287
|
+
display: "block",
|
|
288
|
+
background: CONFIG.color.bands.silhouette.fillEven,
|
|
289
|
+
border: `${CONFIG.size.stroke.bandPx}px solid ${CONFIG.color.bands.silhouette.stroke}`,
|
|
290
|
+
borderRadius: "8px"
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
renderSilhouette()
|
|
294
|
+
), /* @__PURE__ */ React.createElement(
|
|
295
|
+
"button",
|
|
296
|
+
{
|
|
297
|
+
className: "jspsych-btn",
|
|
298
|
+
onClick: handleButtonClick,
|
|
299
|
+
disabled: buttonDisabled,
|
|
300
|
+
style: {
|
|
301
|
+
padding: "12px 30px",
|
|
302
|
+
fontSize: "16px",
|
|
303
|
+
cursor: buttonDisabled ? "not-allowed" : "pointer",
|
|
304
|
+
opacity: buttonDisabled ? 0.5 : 1
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
button_text
|
|
308
|
+
)));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const info = {
|
|
312
|
+
name: "tangram-nback",
|
|
313
|
+
version: "1.0.0",
|
|
314
|
+
parameters: {
|
|
315
|
+
/** Single tangram specification to display */
|
|
316
|
+
tangram: {
|
|
317
|
+
type: ParameterType.COMPLEX,
|
|
318
|
+
default: void 0,
|
|
319
|
+
description: "TangramSpec object defining target shape to display"
|
|
320
|
+
},
|
|
321
|
+
/** Whether this trial is a match (for computing accuracy) */
|
|
322
|
+
isMatch: {
|
|
323
|
+
type: ParameterType.BOOL,
|
|
324
|
+
default: void 0,
|
|
325
|
+
description: "Whether this tangram matches the previous one (optional)"
|
|
326
|
+
},
|
|
327
|
+
/** Whether to show tangram decomposed into individual primitives with borders */
|
|
328
|
+
show_tangram_decomposition: {
|
|
329
|
+
type: ParameterType.BOOL,
|
|
330
|
+
default: false,
|
|
331
|
+
description: "Whether to show tangram decomposed into individual primitives with borders"
|
|
332
|
+
},
|
|
333
|
+
/** HTML content to display above the tangram as instructions */
|
|
334
|
+
instructions: {
|
|
335
|
+
type: ParameterType.STRING,
|
|
336
|
+
default: "",
|
|
337
|
+
description: "HTML content to display above the tangram as instructions"
|
|
338
|
+
},
|
|
339
|
+
/** Text to display on response button */
|
|
340
|
+
button_text: {
|
|
341
|
+
type: ParameterType.STRING,
|
|
342
|
+
default: "Same as previous!",
|
|
343
|
+
description: "Text to display on response button"
|
|
344
|
+
},
|
|
345
|
+
/** Duration to display tangram and accept responses (milliseconds) */
|
|
346
|
+
duration: {
|
|
347
|
+
type: ParameterType.INT,
|
|
348
|
+
default: 3e3,
|
|
349
|
+
description: "Duration in milliseconds to display tangram and accept responses"
|
|
350
|
+
},
|
|
351
|
+
/** Callback fired when trial ends */
|
|
352
|
+
onTrialEnd: {
|
|
353
|
+
type: ParameterType.FUNCTION,
|
|
354
|
+
default: void 0,
|
|
355
|
+
description: "Callback when trial completes with full data"
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
data: {
|
|
359
|
+
/** Whether participant clicked the response button before duration expired */
|
|
360
|
+
responded_match: {
|
|
361
|
+
type: ParameterType.BOOL,
|
|
362
|
+
description: "True if participant clicked response button, false otherwise"
|
|
363
|
+
},
|
|
364
|
+
/** Reaction time in milliseconds (NaN if no response or response after duration) */
|
|
365
|
+
rt: {
|
|
366
|
+
type: ParameterType.INT,
|
|
367
|
+
description: "Milliseconds between trial start and button click (NaN if no response or late response)"
|
|
368
|
+
},
|
|
369
|
+
/** Accuracy: 1 if correct, 0 if incorrect, NaN if isMatch not provided */
|
|
370
|
+
accuracy: {
|
|
371
|
+
type: ParameterType.FLOAT,
|
|
372
|
+
description: "1 if response matches isMatch parameter, 0 otherwise (NaN if isMatch not provided)"
|
|
373
|
+
},
|
|
374
|
+
/** Whether response occurred after duration expired */
|
|
375
|
+
responded_after_duration: {
|
|
376
|
+
type: ParameterType.BOOL,
|
|
377
|
+
description: "True if button clicked after duration expired, false otherwise"
|
|
378
|
+
},
|
|
379
|
+
/** Time of late response (NaN if no late response) */
|
|
380
|
+
rt_after_duration: {
|
|
381
|
+
type: ParameterType.INT,
|
|
382
|
+
description: "Milliseconds between trial start and late button click (NaN if no late response)"
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
citations: ""
|
|
386
|
+
};
|
|
387
|
+
class TangramNBackPlugin {
|
|
388
|
+
constructor(jsPsych) {
|
|
389
|
+
this.jsPsych = jsPsych;
|
|
390
|
+
}
|
|
391
|
+
static {
|
|
392
|
+
this.info = info;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Launches the trial by invoking startNBackTrial
|
|
396
|
+
* with the display element, parameters, and jsPsych instance.
|
|
397
|
+
*/
|
|
398
|
+
trial(display_element, trial) {
|
|
399
|
+
const wrappedOnTrialEnd = (data) => {
|
|
400
|
+
if (trial.onTrialEnd) {
|
|
401
|
+
trial.onTrialEnd(data);
|
|
402
|
+
}
|
|
403
|
+
const reactContext = display_element.__reactContext;
|
|
404
|
+
if (reactContext?.root) {
|
|
405
|
+
reactContext.root.unmount();
|
|
406
|
+
}
|
|
407
|
+
display_element.innerHTML = "";
|
|
408
|
+
this.jsPsych.finishTrial(data);
|
|
409
|
+
};
|
|
410
|
+
const params = {
|
|
411
|
+
tangram: trial.tangram,
|
|
412
|
+
isMatch: trial.isMatch,
|
|
413
|
+
show_tangram_decomposition: trial.show_tangram_decomposition,
|
|
414
|
+
instructions: trial.instructions,
|
|
415
|
+
button_text: trial.button_text,
|
|
416
|
+
duration: trial.duration,
|
|
417
|
+
onTrialEnd: wrappedOnTrialEnd
|
|
418
|
+
};
|
|
419
|
+
const { root, display_element: element, jsPsych } = startNBackTrial(display_element, params, this.jsPsych);
|
|
420
|
+
element.__reactContext = { root, jsPsych };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export { TangramNBackPlugin as default };
|
|
425
|
+
//# sourceMappingURL=index.js.map
|