@jspsych-contrib/plugin-trail-making 0.1.0

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.js ADDED
@@ -0,0 +1,406 @@
1
+ import { ParameterType } from 'jspsych';
2
+
3
+ var version = "0.1.0";
4
+
5
+ const info = {
6
+ name: "trail-making",
7
+ version,
8
+ parameters: {
9
+ /**
10
+ * The type of trail making test to run.
11
+ * "A" = numbers only (1-2-3-4...)
12
+ * "B" = alternating numbers and letters (1-A-2-B-3-C...)
13
+ */
14
+ test_type: {
15
+ type: ParameterType.STRING,
16
+ default: "A"
17
+ },
18
+ /**
19
+ * The number of targets to display. For type "B", this should be an even number
20
+ * to have equal numbers and letters.
21
+ */
22
+ num_targets: {
23
+ type: ParameterType.INT,
24
+ default: 25
25
+ },
26
+ /**
27
+ * The width of the display area in pixels.
28
+ */
29
+ canvas_width: {
30
+ type: ParameterType.INT,
31
+ default: 600
32
+ },
33
+ /**
34
+ * The height of the display area in pixels.
35
+ */
36
+ canvas_height: {
37
+ type: ParameterType.INT,
38
+ default: 600
39
+ },
40
+ /**
41
+ * The radius of each target circle in pixels.
42
+ */
43
+ target_radius: {
44
+ type: ParameterType.INT,
45
+ default: 25
46
+ },
47
+ /**
48
+ * The minimum distance between target centers in pixels.
49
+ */
50
+ min_separation: {
51
+ type: ParameterType.INT,
52
+ default: 80
53
+ },
54
+ /**
55
+ * The color of unvisited target circles.
56
+ */
57
+ target_color: {
58
+ type: ParameterType.STRING,
59
+ default: "#ffffff"
60
+ },
61
+ /**
62
+ * The border color of target circles.
63
+ */
64
+ target_border_color: {
65
+ type: ParameterType.STRING,
66
+ default: "#000000"
67
+ },
68
+ /**
69
+ * The color of visited (correctly clicked) target circles.
70
+ */
71
+ visited_color: {
72
+ type: ParameterType.STRING,
73
+ default: "#90EE90"
74
+ },
75
+ /**
76
+ * The color of the line connecting targets.
77
+ */
78
+ line_color: {
79
+ type: ParameterType.STRING,
80
+ default: "#000000"
81
+ },
82
+ /**
83
+ * The width of the connecting line in pixels.
84
+ */
85
+ line_width: {
86
+ type: ParameterType.INT,
87
+ default: 2
88
+ },
89
+ /**
90
+ * The color to flash when an error is made.
91
+ */
92
+ error_color: {
93
+ type: ParameterType.STRING,
94
+ default: "#FF6B6B"
95
+ },
96
+ /**
97
+ * Duration in milliseconds to show error feedback.
98
+ */
99
+ error_duration: {
100
+ type: ParameterType.INT,
101
+ default: 500
102
+ },
103
+ /**
104
+ * Optional array of {x, y, label} objects to specify exact target positions.
105
+ * If provided, overrides num_targets and random positioning.
106
+ * Coordinates are in pixels from top-left of canvas.
107
+ */
108
+ targets: {
109
+ type: ParameterType.COMPLEX,
110
+ default: null
111
+ },
112
+ /**
113
+ * Text prompt displayed above the canvas.
114
+ */
115
+ prompt: {
116
+ type: ParameterType.HTML_STRING,
117
+ default: null
118
+ },
119
+ /**
120
+ * Random seed for reproducible target layouts. If null, uses random seed.
121
+ */
122
+ seed: {
123
+ type: ParameterType.INT,
124
+ default: null
125
+ }
126
+ },
127
+ data: {
128
+ /** The type of trail making test ("A" or "B"). */
129
+ test_type: {
130
+ type: ParameterType.STRING
131
+ },
132
+ /** Array of target objects with x, y, and label properties. */
133
+ targets: {
134
+ type: ParameterType.COMPLEX,
135
+ array: true
136
+ },
137
+ /** Array of click events with target_index, label, time, x, y, and correct properties. */
138
+ clicks: {
139
+ type: ParameterType.COMPLEX,
140
+ array: true
141
+ },
142
+ /** Total time in milliseconds from first click to last correct click. */
143
+ completion_time: {
144
+ type: ParameterType.INT
145
+ },
146
+ /** Number of errors (incorrect clicks). */
147
+ num_errors: {
148
+ type: ParameterType.INT
149
+ },
150
+ /** Total path distance in pixels (sum of distances between consecutive correct targets). */
151
+ total_path_distance: {
152
+ type: ParameterType.FLOAT
153
+ },
154
+ /** Array of response times between consecutive correct clicks. */
155
+ inter_click_times: {
156
+ type: ParameterType.INT,
157
+ array: true
158
+ }
159
+ }
160
+ };
161
+ class TrailMakingPlugin {
162
+ constructor(jsPsych) {
163
+ this.jsPsych = jsPsych;
164
+ }
165
+ static {
166
+ this.info = info;
167
+ }
168
+ trial(display_element, trial) {
169
+ const targets = trial.targets ? trial.targets : this.generateTargets(trial);
170
+ const sequence = this.getCorrectSequence(trial.test_type, targets);
171
+ let currentTargetIndex = 0;
172
+ let startTime = null;
173
+ let lastClickTime = null;
174
+ const clicks = [];
175
+ const interClickTimes = [];
176
+ let numErrors = 0;
177
+ let isShowingError = false;
178
+ const container = document.createElement("div");
179
+ container.style.cssText = "display: flex; flex-direction: column; align-items: center;";
180
+ if (trial.prompt) {
181
+ const promptDiv = document.createElement("div");
182
+ promptDiv.innerHTML = trial.prompt;
183
+ promptDiv.style.marginBottom = "10px";
184
+ container.appendChild(promptDiv);
185
+ }
186
+ const canvas = document.createElement("canvas");
187
+ canvas.width = trial.canvas_width;
188
+ canvas.height = trial.canvas_height;
189
+ canvas.style.cssText = "border: 1px solid #ccc; cursor: pointer; touch-action: none;";
190
+ container.appendChild(canvas);
191
+ display_element.appendChild(container);
192
+ const ctx = canvas.getContext("2d");
193
+ this.drawCanvas(ctx, targets, [], trial);
194
+ const handleClick = (e) => {
195
+ if (isShowingError) return;
196
+ const rect = canvas.getBoundingClientRect();
197
+ let clientX, clientY;
198
+ if (e instanceof TouchEvent) {
199
+ clientX = e.changedTouches[0].clientX;
200
+ clientY = e.changedTouches[0].clientY;
201
+ } else {
202
+ clientX = e.clientX;
203
+ clientY = e.clientY;
204
+ }
205
+ const x = clientX - rect.left;
206
+ const y = clientY - rect.top;
207
+ const now = performance.now();
208
+ if (startTime === null) {
209
+ startTime = now;
210
+ }
211
+ const clickedTargetIndex = this.findClickedTarget(x, y, targets, trial.target_radius);
212
+ const expectedLabel = sequence[currentTargetIndex];
213
+ const clickEvent = {
214
+ target_index: clickedTargetIndex,
215
+ label: clickedTargetIndex !== null ? targets[clickedTargetIndex].label : null,
216
+ time: Math.round(now - startTime),
217
+ x: Math.round(x),
218
+ y: Math.round(y),
219
+ correct: false
220
+ };
221
+ if (clickedTargetIndex !== null && targets[clickedTargetIndex].label === expectedLabel) {
222
+ clickEvent.correct = true;
223
+ if (lastClickTime !== null) {
224
+ interClickTimes.push(Math.round(now - lastClickTime));
225
+ }
226
+ lastClickTime = now;
227
+ currentTargetIndex++;
228
+ clicks.push(clickEvent);
229
+ const visitedLabels = sequence.slice(0, currentTargetIndex);
230
+ this.drawCanvas(ctx, targets, visitedLabels, trial);
231
+ if (currentTargetIndex >= sequence.length) {
232
+ this.endTrial(targets, clicks, startTime, now, numErrors, interClickTimes, trial);
233
+ }
234
+ } else {
235
+ clickEvent.correct = false;
236
+ clicks.push(clickEvent);
237
+ numErrors++;
238
+ isShowingError = true;
239
+ const visitedLabels = sequence.slice(0, currentTargetIndex);
240
+ this.drawCanvas(ctx, targets, visitedLabels, trial, true);
241
+ this.jsPsych.pluginAPI.setTimeout(() => {
242
+ isShowingError = false;
243
+ this.drawCanvas(ctx, targets, visitedLabels, trial);
244
+ }, trial.error_duration);
245
+ }
246
+ };
247
+ canvas.addEventListener("click", handleClick);
248
+ canvas.addEventListener("touchend", handleClick);
249
+ }
250
+ generateTargets(trial) {
251
+ const labels = this.generateLabels(trial.test_type, trial.num_targets);
252
+ const targets = [];
253
+ const padding = trial.target_radius + 10;
254
+ const maxAttempts = 1e3;
255
+ let seed = trial.seed ?? Math.floor(Math.random() * 1e6);
256
+ const random = () => {
257
+ seed = seed * 1103515245 + 12345 & 2147483647;
258
+ return seed / 2147483647;
259
+ };
260
+ for (const label of labels) {
261
+ let placed = false;
262
+ let attempts = 0;
263
+ while (!placed && attempts < maxAttempts) {
264
+ const x = padding + random() * (trial.canvas_width - 2 * padding);
265
+ const y = padding + random() * (trial.canvas_height - 2 * padding);
266
+ let validPosition = true;
267
+ for (const target of targets) {
268
+ const dist = Math.sqrt((x - target.x) ** 2 + (y - target.y) ** 2);
269
+ if (dist < trial.min_separation) {
270
+ validPosition = false;
271
+ break;
272
+ }
273
+ }
274
+ if (validPosition) {
275
+ targets.push({ x: Math.round(x), y: Math.round(y), label });
276
+ placed = true;
277
+ }
278
+ attempts++;
279
+ }
280
+ if (!placed) {
281
+ const x = padding + random() * (trial.canvas_width - 2 * padding);
282
+ const y = padding + random() * (trial.canvas_height - 2 * padding);
283
+ targets.push({ x: Math.round(x), y: Math.round(y), label });
284
+ }
285
+ }
286
+ return targets;
287
+ }
288
+ generateLabels(type, numTargets) {
289
+ const labels = [];
290
+ if (type === "A") {
291
+ for (let i = 1; i <= numTargets; i++) {
292
+ labels.push(i.toString());
293
+ }
294
+ } else if (type === "B") {
295
+ const numbers = [];
296
+ const letters = [];
297
+ const numPairs = Math.floor(numTargets / 2);
298
+ for (let i = 1; i <= numPairs; i++) {
299
+ numbers.push(i.toString());
300
+ }
301
+ for (let i = 0; i < numPairs; i++) {
302
+ letters.push(String.fromCharCode(65 + i));
303
+ }
304
+ for (let i = 0; i < numPairs; i++) {
305
+ labels.push(numbers[i]);
306
+ labels.push(letters[i]);
307
+ }
308
+ if (numTargets % 2 === 1) {
309
+ labels.push((numPairs + 1).toString());
310
+ }
311
+ }
312
+ return labels;
313
+ }
314
+ getCorrectSequence(type, targets) {
315
+ if (type === "A") {
316
+ return targets.map((t) => t.label).sort((a, b) => parseInt(a) - parseInt(b));
317
+ } else {
318
+ const numbers = targets.filter((t) => /^\d+$/.test(t.label)).map((t) => t.label).sort((a, b) => parseInt(a) - parseInt(b));
319
+ const letters = targets.filter((t) => /^[A-Z]$/.test(t.label)).map((t) => t.label).sort();
320
+ const sequence = [];
321
+ const maxLen = Math.max(numbers.length, letters.length);
322
+ for (let i = 0; i < maxLen; i++) {
323
+ if (i < numbers.length) sequence.push(numbers[i]);
324
+ if (i < letters.length) sequence.push(letters[i]);
325
+ }
326
+ return sequence;
327
+ }
328
+ }
329
+ findClickedTarget(x, y, targets, radius) {
330
+ const hitRadius = radius + 5;
331
+ for (let i = 0; i < targets.length; i++) {
332
+ const dist = Math.sqrt((x - targets[i].x) ** 2 + (y - targets[i].y) ** 2);
333
+ if (dist <= hitRadius) {
334
+ return i;
335
+ }
336
+ }
337
+ return null;
338
+ }
339
+ drawCanvas(ctx, targets, visitedLabels, trial, showError = false) {
340
+ const width = trial.canvas_width;
341
+ const height = trial.canvas_height;
342
+ ctx.fillStyle = "#f5f5f5";
343
+ ctx.fillRect(0, 0, width, height);
344
+ if (visitedLabels.length > 1) {
345
+ ctx.strokeStyle = trial.line_color;
346
+ ctx.lineWidth = trial.line_width;
347
+ ctx.beginPath();
348
+ for (let i = 0; i < visitedLabels.length; i++) {
349
+ const target = targets.find((t) => t.label === visitedLabels[i]);
350
+ if (target) {
351
+ if (i === 0) {
352
+ ctx.moveTo(target.x, target.y);
353
+ } else {
354
+ ctx.lineTo(target.x, target.y);
355
+ }
356
+ }
357
+ }
358
+ ctx.stroke();
359
+ }
360
+ for (const target of targets) {
361
+ const isVisited = visitedLabels.includes(target.label);
362
+ if (showError && !isVisited) {
363
+ ctx.fillStyle = trial.error_color;
364
+ } else if (isVisited) {
365
+ ctx.fillStyle = trial.visited_color;
366
+ } else {
367
+ ctx.fillStyle = trial.target_color;
368
+ }
369
+ ctx.beginPath();
370
+ ctx.arc(target.x, target.y, trial.target_radius, 0, Math.PI * 2);
371
+ ctx.fill();
372
+ ctx.strokeStyle = trial.target_border_color;
373
+ ctx.lineWidth = 2;
374
+ ctx.stroke();
375
+ ctx.fillStyle = "#000000";
376
+ ctx.font = `bold ${Math.round(trial.target_radius * 0.8)}px Arial`;
377
+ ctx.textAlign = "center";
378
+ ctx.textBaseline = "middle";
379
+ ctx.fillText(target.label, target.x, target.y);
380
+ }
381
+ }
382
+ endTrial(targets, clicks, startTime, endTime, numErrors, interClickTimes, trial) {
383
+ const correctClicks = clicks.filter((c) => c.correct);
384
+ let totalPathDistance = 0;
385
+ for (let i = 1; i < correctClicks.length; i++) {
386
+ const prev = targets.find((t) => t.label === correctClicks[i - 1].label);
387
+ const curr = targets.find((t) => t.label === correctClicks[i].label);
388
+ if (prev && curr) {
389
+ totalPathDistance += Math.sqrt((curr.x - prev.x) ** 2 + (curr.y - prev.y) ** 2);
390
+ }
391
+ }
392
+ const trial_data = {
393
+ test_type: trial.test_type,
394
+ targets,
395
+ clicks,
396
+ completion_time: Math.round(endTime - startTime),
397
+ num_errors: numErrors,
398
+ total_path_distance: Math.round(totalPathDistance * 100) / 100,
399
+ inter_click_times: interClickTimes
400
+ };
401
+ this.jsPsych.finishTrial(trial_data);
402
+ }
403
+ }
404
+
405
+ export { TrailMakingPlugin as default };
406
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych-contrib/plugin-trail-making\",\n \"version\": \"0.1.0\",\n \"description\": \"A jsPsych plugin for the Trail Making Test (TMT), where participants connect circles in sequence. Supports Part A (numbers only) and Part B (alternating numbers and letters).\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/jspsych/jspsych-contrib.git\",\n \"directory\": \"packages/plugin-trail-making\"\n },\n \"keywords\": [\n \"jsPsych\",\n \"trail-making\",\n \"TMT\",\n \"neuropsychology\",\n \"cognitive-assessment\"\n ],\n \"author\": {\n \"name\": \"Josh de Leeuw\",\n \"url\": \"https://github.com/jodeleeuw\"\n },\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jspsych-contrib/issues\"\n },\n \"homepage\": \"https://github.com/jspsych/jspsych-contrib/tree/main/packages/plugin-trail-making\",\n \"peerDependencies\": {\n \"jspsych\": \">=8.0.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.2.2\",\n \"@jspsych/test-utils\": \"^1.0.0\",\n \"jspsych\": \"^8.0.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"trail-making\",\n version: version,\n parameters: {\n /**\n * The type of trail making test to run.\n * \"A\" = numbers only (1-2-3-4...)\n * \"B\" = alternating numbers and letters (1-A-2-B-3-C...)\n */\n test_type: {\n type: ParameterType.STRING,\n default: \"A\",\n },\n /**\n * The number of targets to display. For type \"B\", this should be an even number\n * to have equal numbers and letters.\n */\n num_targets: {\n type: ParameterType.INT,\n default: 25,\n },\n /**\n * The width of the display area in pixels.\n */\n canvas_width: {\n type: ParameterType.INT,\n default: 600,\n },\n /**\n * The height of the display area in pixels.\n */\n canvas_height: {\n type: ParameterType.INT,\n default: 600,\n },\n /**\n * The radius of each target circle in pixels.\n */\n target_radius: {\n type: ParameterType.INT,\n default: 25,\n },\n /**\n * The minimum distance between target centers in pixels.\n */\n min_separation: {\n type: ParameterType.INT,\n default: 80,\n },\n /**\n * The color of unvisited target circles.\n */\n target_color: {\n type: ParameterType.STRING,\n default: \"#ffffff\",\n },\n /**\n * The border color of target circles.\n */\n target_border_color: {\n type: ParameterType.STRING,\n default: \"#000000\",\n },\n /**\n * The color of visited (correctly clicked) target circles.\n */\n visited_color: {\n type: ParameterType.STRING,\n default: \"#90EE90\",\n },\n /**\n * The color of the line connecting targets.\n */\n line_color: {\n type: ParameterType.STRING,\n default: \"#000000\",\n },\n /**\n * The width of the connecting line in pixels.\n */\n line_width: {\n type: ParameterType.INT,\n default: 2,\n },\n /**\n * The color to flash when an error is made.\n */\n error_color: {\n type: ParameterType.STRING,\n default: \"#FF6B6B\",\n },\n /**\n * Duration in milliseconds to show error feedback.\n */\n error_duration: {\n type: ParameterType.INT,\n default: 500,\n },\n /**\n * Optional array of {x, y, label} objects to specify exact target positions.\n * If provided, overrides num_targets and random positioning.\n * Coordinates are in pixels from top-left of canvas.\n */\n targets: {\n type: ParameterType.COMPLEX,\n default: null,\n },\n /**\n * Text prompt displayed above the canvas.\n */\n prompt: {\n type: ParameterType.HTML_STRING,\n default: null,\n },\n /**\n * Random seed for reproducible target layouts. If null, uses random seed.\n */\n seed: {\n type: ParameterType.INT,\n default: null,\n },\n },\n data: {\n /** The type of trail making test (\"A\" or \"B\"). */\n test_type: {\n type: ParameterType.STRING,\n },\n /** Array of target objects with x, y, and label properties. */\n targets: {\n type: ParameterType.COMPLEX,\n array: true,\n },\n /** Array of click events with target_index, label, time, x, y, and correct properties. */\n clicks: {\n type: ParameterType.COMPLEX,\n array: true,\n },\n /** Total time in milliseconds from first click to last correct click. */\n completion_time: {\n type: ParameterType.INT,\n },\n /** Number of errors (incorrect clicks). */\n num_errors: {\n type: ParameterType.INT,\n },\n /** Total path distance in pixels (sum of distances between consecutive correct targets). */\n total_path_distance: {\n type: ParameterType.FLOAT,\n },\n /** Array of response times between consecutive correct clicks. */\n inter_click_times: {\n type: ParameterType.INT,\n array: true,\n },\n },\n};\n\ntype Info = typeof info;\n\ninterface Target {\n x: number;\n y: number;\n label: string;\n}\n\ninterface ClickEvent {\n target_index: number | null;\n label: string | null;\n time: number;\n x: number;\n y: number;\n correct: boolean;\n}\n\n/**\n * **trail-making**\n *\n * A jsPsych plugin for the Trail Making Test (TMT), a neuropsychological test of\n * visual attention and task switching. Participants connect circles in sequence\n * as quickly as possible.\n *\n * Part A: Connect numbers in order (1-2-3-4...)\n * Part B: Alternate between numbers and letters (1-A-2-B-3-C...)\n *\n * @author Josh de Leeuw\n * @see {@link https://github.com/jspsych/jspsych-contrib/tree/main/packages/plugin-trail-making}\n */\nclass TrailMakingPlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n // Generate or use provided targets\n const targets = trial.targets ? (trial.targets as Target[]) : this.generateTargets(trial);\n\n // Determine the correct sequence\n const sequence = this.getCorrectSequence(trial.test_type, targets);\n\n // State variables\n let currentTargetIndex = 0;\n let startTime: number | null = null;\n let lastClickTime: number | null = null;\n const clicks: ClickEvent[] = [];\n const interClickTimes: number[] = [];\n let numErrors = 0;\n let isShowingError = false;\n\n // Create display\n const container = document.createElement(\"div\");\n container.style.cssText = \"display: flex; flex-direction: column; align-items: center;\";\n\n if (trial.prompt) {\n const promptDiv = document.createElement(\"div\");\n promptDiv.innerHTML = trial.prompt;\n promptDiv.style.marginBottom = \"10px\";\n container.appendChild(promptDiv);\n }\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = trial.canvas_width;\n canvas.height = trial.canvas_height;\n canvas.style.cssText = \"border: 1px solid #ccc; cursor: pointer; touch-action: none;\";\n container.appendChild(canvas);\n\n display_element.appendChild(container);\n\n const ctx = canvas.getContext(\"2d\")!;\n\n // Draw initial state\n this.drawCanvas(ctx, targets, [], trial);\n\n // Handle clicks/taps\n const handleClick = (e: MouseEvent | TouchEvent) => {\n if (isShowingError) return;\n\n const rect = canvas.getBoundingClientRect();\n let clientX: number, clientY: number;\n\n if (e instanceof TouchEvent) {\n clientX = e.changedTouches[0].clientX;\n clientY = e.changedTouches[0].clientY;\n } else {\n clientX = e.clientX;\n clientY = e.clientY;\n }\n\n const x = clientX - rect.left;\n const y = clientY - rect.top;\n const now = performance.now();\n\n // Start timing on first click\n if (startTime === null) {\n startTime = now;\n }\n\n // Find clicked target\n const clickedTargetIndex = this.findClickedTarget(x, y, targets, trial.target_radius);\n const expectedLabel = sequence[currentTargetIndex];\n\n const clickEvent: ClickEvent = {\n target_index: clickedTargetIndex,\n label: clickedTargetIndex !== null ? targets[clickedTargetIndex].label : null,\n time: Math.round(now - startTime),\n x: Math.round(x),\n y: Math.round(y),\n correct: false,\n };\n\n if (clickedTargetIndex !== null && targets[clickedTargetIndex].label === expectedLabel) {\n // Correct click\n clickEvent.correct = true;\n\n if (lastClickTime !== null) {\n interClickTimes.push(Math.round(now - lastClickTime));\n }\n lastClickTime = now;\n\n currentTargetIndex++;\n clicks.push(clickEvent);\n\n // Redraw with updated visited targets\n const visitedLabels = sequence.slice(0, currentTargetIndex);\n this.drawCanvas(ctx, targets, visitedLabels, trial);\n\n // Check if complete\n if (currentTargetIndex >= sequence.length) {\n this.endTrial(targets, clicks, startTime, now, numErrors, interClickTimes, trial);\n }\n } else {\n // Error\n clickEvent.correct = false;\n clicks.push(clickEvent);\n numErrors++;\n\n // Show error feedback\n isShowingError = true;\n const visitedLabels = sequence.slice(0, currentTargetIndex);\n this.drawCanvas(ctx, targets, visitedLabels, trial, true);\n\n this.jsPsych.pluginAPI.setTimeout(() => {\n isShowingError = false;\n this.drawCanvas(ctx, targets, visitedLabels, trial);\n }, trial.error_duration);\n }\n };\n\n canvas.addEventListener(\"click\", handleClick);\n canvas.addEventListener(\"touchend\", handleClick);\n }\n\n private generateTargets(trial: TrialType<Info>): Target[] {\n const labels = this.generateLabels(trial.test_type, trial.num_targets);\n const targets: Target[] = [];\n const padding = trial.target_radius + 10;\n const maxAttempts = 1000;\n\n // Simple seeded random for reproducibility\n let seed = trial.seed ?? Math.floor(Math.random() * 1000000);\n const random = () => {\n seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n return seed / 0x7fffffff;\n };\n\n for (const label of labels) {\n let placed = false;\n let attempts = 0;\n\n while (!placed && attempts < maxAttempts) {\n const x = padding + random() * (trial.canvas_width - 2 * padding);\n const y = padding + random() * (trial.canvas_height - 2 * padding);\n\n // Check distance from all existing targets\n let validPosition = true;\n for (const target of targets) {\n const dist = Math.sqrt((x - target.x) ** 2 + (y - target.y) ** 2);\n if (dist < trial.min_separation) {\n validPosition = false;\n break;\n }\n }\n\n if (validPosition) {\n targets.push({ x: Math.round(x), y: Math.round(y), label });\n placed = true;\n }\n\n attempts++;\n }\n\n if (!placed) {\n // Fallback: place with reduced separation requirement\n const x = padding + random() * (trial.canvas_width - 2 * padding);\n const y = padding + random() * (trial.canvas_height - 2 * padding);\n targets.push({ x: Math.round(x), y: Math.round(y), label });\n }\n }\n\n return targets;\n }\n\n private generateLabels(type: string, numTargets: number): string[] {\n const labels: string[] = [];\n\n if (type === \"A\") {\n for (let i = 1; i <= numTargets; i++) {\n labels.push(i.toString());\n }\n } else if (type === \"B\") {\n const numbers = [];\n const letters = [];\n const numPairs = Math.floor(numTargets / 2);\n\n for (let i = 1; i <= numPairs; i++) {\n numbers.push(i.toString());\n }\n for (let i = 0; i < numPairs; i++) {\n letters.push(String.fromCharCode(65 + i)); // A, B, C, ...\n }\n\n // Interleave: 1, A, 2, B, 3, C, ...\n for (let i = 0; i < numPairs; i++) {\n labels.push(numbers[i]);\n labels.push(letters[i]);\n }\n\n // If odd number, add one more number\n if (numTargets % 2 === 1) {\n labels.push((numPairs + 1).toString());\n }\n }\n\n return labels;\n }\n\n private getCorrectSequence(type: string, targets: Target[]): string[] {\n // The correct sequence is the labels in order\n if (type === \"A\") {\n // Sort by numeric value\n return targets.map((t) => t.label).sort((a, b) => parseInt(a) - parseInt(b));\n } else {\n // For type B, sequence is 1, A, 2, B, 3, C, ...\n const numbers = targets\n .filter((t) => /^\\d+$/.test(t.label))\n .map((t) => t.label)\n .sort((a, b) => parseInt(a) - parseInt(b));\n const letters = targets\n .filter((t) => /^[A-Z]$/.test(t.label))\n .map((t) => t.label)\n .sort();\n\n const sequence: string[] = [];\n const maxLen = Math.max(numbers.length, letters.length);\n for (let i = 0; i < maxLen; i++) {\n if (i < numbers.length) sequence.push(numbers[i]);\n if (i < letters.length) sequence.push(letters[i]);\n }\n return sequence;\n }\n }\n\n private findClickedTarget(\n x: number,\n y: number,\n targets: Target[],\n radius: number\n ): number | null {\n // Add a small cushion (5px) around each target to make clicking easier\n const hitRadius = radius + 5;\n for (let i = 0; i < targets.length; i++) {\n const dist = Math.sqrt((x - targets[i].x) ** 2 + (y - targets[i].y) ** 2);\n if (dist <= hitRadius) {\n return i;\n }\n }\n return null;\n }\n\n private drawCanvas(\n ctx: CanvasRenderingContext2D,\n targets: Target[],\n visitedLabels: string[],\n trial: TrialType<Info>,\n showError: boolean = false\n ) {\n const width = trial.canvas_width;\n const height = trial.canvas_height;\n\n // Clear canvas\n ctx.fillStyle = \"#f5f5f5\";\n ctx.fillRect(0, 0, width, height);\n\n // Draw lines between visited targets in sequence order\n if (visitedLabels.length > 1) {\n ctx.strokeStyle = trial.line_color;\n ctx.lineWidth = trial.line_width;\n ctx.beginPath();\n\n for (let i = 0; i < visitedLabels.length; i++) {\n const target = targets.find((t) => t.label === visitedLabels[i]);\n if (target) {\n if (i === 0) {\n ctx.moveTo(target.x, target.y);\n } else {\n ctx.lineTo(target.x, target.y);\n }\n }\n }\n ctx.stroke();\n }\n\n // Draw targets\n for (const target of targets) {\n const isVisited = visitedLabels.includes(target.label);\n\n // Set fill color before drawing\n if (showError && !isVisited) {\n ctx.fillStyle = trial.error_color;\n } else if (isVisited) {\n ctx.fillStyle = trial.visited_color;\n } else {\n ctx.fillStyle = trial.target_color;\n }\n\n // Circle\n ctx.beginPath();\n ctx.arc(target.x, target.y, trial.target_radius, 0, Math.PI * 2);\n ctx.fill();\n ctx.strokeStyle = trial.target_border_color;\n ctx.lineWidth = 2;\n ctx.stroke();\n\n // Label\n ctx.fillStyle = \"#000000\";\n ctx.font = `bold ${Math.round(trial.target_radius * 0.8)}px Arial`;\n ctx.textAlign = \"center\";\n ctx.textBaseline = \"middle\";\n ctx.fillText(target.label, target.x, target.y);\n }\n }\n\n private endTrial(\n targets: Target[],\n clicks: ClickEvent[],\n startTime: number,\n endTime: number,\n numErrors: number,\n interClickTimes: number[],\n trial: TrialType<Info>\n ) {\n // Calculate total path distance\n const correctClicks = clicks.filter((c) => c.correct);\n let totalPathDistance = 0;\n\n for (let i = 1; i < correctClicks.length; i++) {\n const prev = targets.find((t) => t.label === correctClicks[i - 1].label);\n const curr = targets.find((t) => t.label === correctClicks[i].label);\n if (prev && curr) {\n totalPathDistance += Math.sqrt((curr.x - prev.x) ** 2 + (curr.y - prev.y) ** 2);\n }\n }\n\n const trial_data = {\n test_type: trial.test_type,\n targets: targets,\n clicks: clicks,\n completion_time: Math.round(endTime - startTime),\n num_errors: numErrors,\n total_path_distance: Math.round(totalPathDistance * 100) / 100,\n inter_click_times: interClickTimes,\n };\n\n this.jsPsych.finishTrial(trial_data);\n }\n}\n\nexport default TrailMakingPlugin;\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@jspsych-contrib/plugin-trail-making",
3
+ "version": "0.1.0",
4
+ "description": "A jsPsych plugin for the Trail Making Test (TMT), where participants connect circles in sequence. Supports Part A (numbers only) and Part B (alternating numbers and letters).",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "exports": {
8
+ "import": "./dist/index.js",
9
+ "require": "./dist/index.cjs"
10
+ },
11
+ "typings": "dist/index.d.ts",
12
+ "unpkg": "dist/index.browser.min.js",
13
+ "files": [
14
+ "src",
15
+ "dist"
16
+ ],
17
+ "source": "src/index.ts",
18
+ "scripts": {
19
+ "test": "jest",
20
+ "test:watch": "npm test -- --watch",
21
+ "tsc": "tsc",
22
+ "build": "rollup --config",
23
+ "build:watch": "npm run build -- --watch"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/jspsych/jspsych-contrib.git",
28
+ "directory": "packages/plugin-trail-making"
29
+ },
30
+ "keywords": [
31
+ "jsPsych",
32
+ "trail-making",
33
+ "TMT",
34
+ "neuropsychology",
35
+ "cognitive-assessment"
36
+ ],
37
+ "author": {
38
+ "name": "Josh de Leeuw",
39
+ "url": "https://github.com/jodeleeuw"
40
+ },
41
+ "license": "MIT",
42
+ "bugs": {
43
+ "url": "https://github.com/jspsych/jspsych-contrib/issues"
44
+ },
45
+ "homepage": "https://github.com/jspsych/jspsych-contrib/tree/main/packages/plugin-trail-making",
46
+ "peerDependencies": {
47
+ "jspsych": ">=8.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "@jspsych/config": "^3.2.2",
51
+ "@jspsych/test-utils": "^1.0.0",
52
+ "jspsych": "^8.0.0"
53
+ }
54
+ }