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.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { JsPsychPlugin, ParameterType, JsPsych, TrialType } from 'jspsych';
2
2
 
3
- declare const info$1: {
3
+ declare const info$2: {
4
4
  name: string;
5
5
  version: string;
6
6
  parameters: {
@@ -109,7 +109,7 @@ declare const info$1: {
109
109
  };
110
110
  citations: string;
111
111
  };
112
- type Info$1 = typeof info$1;
112
+ type Info$2 = typeof info$2;
113
113
  /**
114
114
  * **tangram-construct**
115
115
  *
@@ -118,7 +118,7 @@ type Info$1 = typeof info$1;
118
118
  * @author Justin Yang & Sean Paul Anderson
119
119
  * @see {@link https://github.com/cogtoolslab/tangram_construction.git/tree/main/experiments/jspsych-tangram-prep}
120
120
  */
121
- declare class TangramConstructPlugin implements JsPsychPlugin<Info$1> {
121
+ declare class TangramConstructPlugin implements JsPsychPlugin<Info$2> {
122
122
  private jsPsych;
123
123
  static info: {
124
124
  name: string;
@@ -234,10 +234,10 @@ declare class TangramConstructPlugin implements JsPsychPlugin<Info$1> {
234
234
  * Launches the trial by invoking startConstructionTrial
235
235
  * with the display element, parameters, and jsPsych instance.
236
236
  */
237
- trial(display_element: HTMLElement, trial: TrialType<Info$1>): void;
237
+ trial(display_element: HTMLElement, trial: TrialType<Info$2>): void;
238
238
  }
239
239
 
240
- declare const info: {
240
+ declare const info$1: {
241
241
  name: string;
242
242
  version: string;
243
243
  parameters: {
@@ -308,7 +308,7 @@ declare const info: {
308
308
  };
309
309
  citations: string;
310
310
  };
311
- type Info = typeof info;
311
+ type Info$1 = typeof info$1;
312
312
  /**
313
313
  * **tangram-prep**
314
314
  *
@@ -316,7 +316,7 @@ type Info = typeof info;
316
316
  *
317
317
  * @author Justin Yang & Sean Paul Anderson
318
318
  */
319
- declare class TangramPrepPlugin implements JsPsychPlugin<Info> {
319
+ declare class TangramPrepPlugin implements JsPsychPlugin<Info$1> {
320
320
  private jsPsych;
321
321
  static info: {
322
322
  name: string;
@@ -394,7 +394,179 @@ declare class TangramPrepPlugin implements JsPsychPlugin<Info> {
394
394
  * Launches the trial by invoking startPrepTrial
395
395
  * with the display element, parameters, and jsPsych instance.
396
396
  */
397
+ trial(display_element: HTMLElement, trial: TrialType<Info$1>): void;
398
+ }
399
+
400
+ declare const info: {
401
+ name: string;
402
+ version: string;
403
+ parameters: {
404
+ /** Single tangram specification to display */
405
+ tangram: {
406
+ type: ParameterType;
407
+ default: undefined;
408
+ description: string;
409
+ };
410
+ /** Whether this trial is a match (for computing accuracy) */
411
+ isMatch: {
412
+ type: ParameterType;
413
+ default: undefined;
414
+ description: string;
415
+ };
416
+ /** Whether to show tangram decomposed into individual primitives with borders */
417
+ show_tangram_decomposition: {
418
+ type: ParameterType;
419
+ default: boolean;
420
+ description: string;
421
+ };
422
+ /** HTML content to display above the tangram as instructions */
423
+ instructions: {
424
+ type: ParameterType;
425
+ default: string;
426
+ description: string;
427
+ };
428
+ /** Text to display on response button */
429
+ button_text: {
430
+ type: ParameterType;
431
+ default: string;
432
+ description: string;
433
+ };
434
+ /** Duration to display tangram and accept responses (milliseconds) */
435
+ duration: {
436
+ type: ParameterType;
437
+ default: number;
438
+ description: string;
439
+ };
440
+ /** Callback fired when trial ends */
441
+ onTrialEnd: {
442
+ type: ParameterType;
443
+ default: undefined;
444
+ description: string;
445
+ };
446
+ };
447
+ data: {
448
+ /** Whether participant clicked the response button before duration expired */
449
+ responded_match: {
450
+ type: ParameterType;
451
+ description: string;
452
+ };
453
+ /** Reaction time in milliseconds (NaN if no response or response after duration) */
454
+ rt: {
455
+ type: ParameterType;
456
+ description: string;
457
+ };
458
+ /** Accuracy: 1 if correct, 0 if incorrect, NaN if isMatch not provided */
459
+ accuracy: {
460
+ type: ParameterType;
461
+ description: string;
462
+ };
463
+ /** Whether response occurred after duration expired */
464
+ responded_after_duration: {
465
+ type: ParameterType;
466
+ description: string;
467
+ };
468
+ /** Time of late response (NaN if no late response) */
469
+ rt_after_duration: {
470
+ type: ParameterType;
471
+ description: string;
472
+ };
473
+ };
474
+ citations: string;
475
+ };
476
+ type Info = typeof info;
477
+ /**
478
+ * **tangram-nback**
479
+ *
480
+ * A jsPsych plugin for n-back matching trials displaying a single tangram
481
+ * with a response button.
482
+ *
483
+ * @author Justin Yang & Sean Paul Anderson
484
+ * @see {@link https://github.com/cogtoolslab/tangram_construction.git/tree/main/experiments/jspsych-tangram-prep}
485
+ */
486
+ declare class TangramNBackPlugin implements JsPsychPlugin<Info> {
487
+ private jsPsych;
488
+ static info: {
489
+ name: string;
490
+ version: string;
491
+ parameters: {
492
+ /** Single tangram specification to display */
493
+ tangram: {
494
+ type: ParameterType;
495
+ default: undefined;
496
+ description: string;
497
+ };
498
+ /** Whether this trial is a match (for computing accuracy) */
499
+ isMatch: {
500
+ type: ParameterType;
501
+ default: undefined;
502
+ description: string;
503
+ };
504
+ /** Whether to show tangram decomposed into individual primitives with borders */
505
+ show_tangram_decomposition: {
506
+ type: ParameterType;
507
+ default: boolean;
508
+ description: string;
509
+ };
510
+ /** HTML content to display above the tangram as instructions */
511
+ instructions: {
512
+ type: ParameterType;
513
+ default: string;
514
+ description: string;
515
+ };
516
+ /** Text to display on response button */
517
+ button_text: {
518
+ type: ParameterType;
519
+ default: string;
520
+ description: string;
521
+ };
522
+ /** Duration to display tangram and accept responses (milliseconds) */
523
+ duration: {
524
+ type: ParameterType;
525
+ default: number;
526
+ description: string;
527
+ };
528
+ /** Callback fired when trial ends */
529
+ onTrialEnd: {
530
+ type: ParameterType;
531
+ default: undefined;
532
+ description: string;
533
+ };
534
+ };
535
+ data: {
536
+ /** Whether participant clicked the response button before duration expired */
537
+ responded_match: {
538
+ type: ParameterType;
539
+ description: string;
540
+ };
541
+ /** Reaction time in milliseconds (NaN if no response or response after duration) */
542
+ rt: {
543
+ type: ParameterType;
544
+ description: string;
545
+ };
546
+ /** Accuracy: 1 if correct, 0 if incorrect, NaN if isMatch not provided */
547
+ accuracy: {
548
+ type: ParameterType;
549
+ description: string;
550
+ };
551
+ /** Whether response occurred after duration expired */
552
+ responded_after_duration: {
553
+ type: ParameterType;
554
+ description: string;
555
+ };
556
+ /** Time of late response (NaN if no late response) */
557
+ rt_after_duration: {
558
+ type: ParameterType;
559
+ description: string;
560
+ };
561
+ };
562
+ citations: string;
563
+ };
564
+ constructor(jsPsych: JsPsych);
565
+ /**
566
+ * Launches the trial by invoking startNBackTrial
567
+ * with the display element, parameters, and jsPsych instance.
568
+ */
397
569
  trial(display_element: HTMLElement, trial: TrialType<Info>): void;
398
570
  }
399
571
 
400
- export { TangramConstructPlugin, TangramPrepPlugin };
572
+ export { TangramConstructPlugin, TangramNBackPlugin, TangramPrepPlugin };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ParameterType } from 'jspsych';
2
- import React from 'react';
2
+ import React, { useRef, useState, useEffect } from 'react';
3
3
  import { createRoot } from 'react-dom/client';
4
4
  import { v4 } from 'uuid';
5
5
 
@@ -3561,7 +3561,7 @@ function startConstructionTrial(display_element, params, _jsPsych) {
3561
3561
  return { root, display_element, jsPsych: _jsPsych };
3562
3562
  }
3563
3563
 
3564
- const info$1 = {
3564
+ const info$2 = {
3565
3565
  name: "tangram-construct",
3566
3566
  version: "1.0.0",
3567
3567
  parameters: {
@@ -3675,7 +3675,7 @@ class TangramConstructPlugin {
3675
3675
  this.jsPsych = jsPsych;
3676
3676
  }
3677
3677
  static {
3678
- this.info = info$1;
3678
+ this.info = info$2;
3679
3679
  }
3680
3680
  /**
3681
3681
  * Launches the trial by invoking startConstructionTrial
@@ -3819,7 +3819,7 @@ function startPrepTrial(display_element, params, jsPsych) {
3819
3819
  return { root, display_element, jsPsych };
3820
3820
  }
3821
3821
 
3822
- const info = {
3822
+ const info$1 = {
3823
3823
  name: "tangram-prep",
3824
3824
  version: "1.0.0",
3825
3825
  parameters: {
@@ -3895,7 +3895,7 @@ class TangramPrepPlugin {
3895
3895
  this.jsPsych = jsPsych;
3896
3896
  }
3897
3897
  static {
3898
- this.info = info;
3898
+ this.info = info$1;
3899
3899
  }
3900
3900
  /**
3901
3901
  * Launches the trial by invoking startPrepTrial
@@ -3931,5 +3931,327 @@ class TangramPrepPlugin {
3931
3931
  }
3932
3932
  }
3933
3933
 
3934
- export { TangramConstructPlugin, TangramPrepPlugin };
3934
+ function startNBackTrial(display_element, params, _jsPsych) {
3935
+ const root = createRoot(display_element);
3936
+ root.render(React.createElement(NBackView, { params }));
3937
+ return { root, display_element, jsPsych: _jsPsych };
3938
+ }
3939
+ function NBackView({ params }) {
3940
+ const {
3941
+ tangram,
3942
+ isMatch,
3943
+ show_tangram_decomposition,
3944
+ instructions,
3945
+ button_text,
3946
+ duration,
3947
+ onTrialEnd
3948
+ } = params;
3949
+ const trialStartTime = useRef(Date.now());
3950
+ const buttonEnabledRef = useRef(true);
3951
+ const timeoutIdRef = useRef(null);
3952
+ const hasRespondedRef = useRef(false);
3953
+ const responseDataRef = useRef(null);
3954
+ const [buttonDisabled, setButtonDisabled] = useState(false);
3955
+ const CANON = /* @__PURE__ */ new Set([
3956
+ "square",
3957
+ "smalltriangle",
3958
+ "parallelogram",
3959
+ "medtriangle",
3960
+ "largetriangle"
3961
+ ]);
3962
+ const filteredTans = tangram.solutionTans.filter((tan) => {
3963
+ const tanName = tan.name ?? tan.kind;
3964
+ return CANON.has(tanName);
3965
+ });
3966
+ const mask = filteredTans.map((tan) => {
3967
+ const polygon = tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }));
3968
+ return polygon;
3969
+ });
3970
+ const primitiveDecomposition = filteredTans.map((tan) => ({
3971
+ kind: tan.name ?? tan.kind,
3972
+ polygon: tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }))
3973
+ }));
3974
+ const DISPLAY_SIZE = 400;
3975
+ const viewport = {
3976
+ w: DISPLAY_SIZE,
3977
+ h: DISPLAY_SIZE
3978
+ };
3979
+ const scaleS = React.useMemo(() => {
3980
+ const u = inferUnitFromPolys$1(mask);
3981
+ return u ? CONFIG.layout.grid.unitPx / u : 1;
3982
+ }, [mask]);
3983
+ const centerPos = {
3984
+ cx: viewport.w / 2,
3985
+ cy: viewport.h / 2
3986
+ };
3987
+ const pathD = (poly) => {
3988
+ if (!poly || poly.length === 0) return "";
3989
+ const moves = poly.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`);
3990
+ return moves.join(" ") + " Z";
3991
+ };
3992
+ const endTrial = (data) => {
3993
+ if (timeoutIdRef.current) {
3994
+ clearTimeout(timeoutIdRef.current);
3995
+ timeoutIdRef.current = null;
3996
+ }
3997
+ const accuracy = isMatch !== void 0 ? isMatch === data.responded_match ? 1 : 0 : NaN;
3998
+ const trialData = {
3999
+ ...data,
4000
+ accuracy,
4001
+ tangram_id: tangram.tangramID,
4002
+ is_match: isMatch
4003
+ };
4004
+ if (onTrialEnd) {
4005
+ onTrialEnd(trialData);
4006
+ }
4007
+ };
4008
+ const handleButtonClick = () => {
4009
+ if (!buttonEnabledRef.current) {
4010
+ const rt_late = Date.now() - trialStartTime.current;
4011
+ hasRespondedRef.current = true;
4012
+ responseDataRef.current = {
4013
+ responded_match: true,
4014
+ rt: NaN,
4015
+ responded_after_duration: true,
4016
+ rt_after_duration: rt_late
4017
+ };
4018
+ endTrial(responseDataRef.current);
4019
+ } else {
4020
+ const rt = Date.now() - trialStartTime.current;
4021
+ buttonEnabledRef.current = false;
4022
+ setButtonDisabled(true);
4023
+ hasRespondedRef.current = true;
4024
+ responseDataRef.current = {
4025
+ responded_match: true,
4026
+ rt,
4027
+ responded_after_duration: false,
4028
+ rt_after_duration: NaN
4029
+ };
4030
+ }
4031
+ };
4032
+ useEffect(() => {
4033
+ timeoutIdRef.current = setTimeout(() => {
4034
+ buttonEnabledRef.current = false;
4035
+ if (hasRespondedRef.current && responseDataRef.current) {
4036
+ endTrial(responseDataRef.current);
4037
+ } else {
4038
+ endTrial({
4039
+ responded_match: false,
4040
+ rt: NaN,
4041
+ responded_after_duration: false,
4042
+ rt_after_duration: NaN
4043
+ });
4044
+ }
4045
+ }, duration);
4046
+ return () => {
4047
+ if (timeoutIdRef.current) {
4048
+ clearTimeout(timeoutIdRef.current);
4049
+ }
4050
+ };
4051
+ }, []);
4052
+ const renderSilhouette = () => {
4053
+ if (show_tangram_decomposition) {
4054
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
4055
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, centerPos);
4056
+ 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(
4057
+ "path",
4058
+ {
4059
+ d: pathD(scaledPoly),
4060
+ fill: CONFIG.color.silhouetteMask,
4061
+ opacity: CONFIG.opacity.silhouetteMask,
4062
+ stroke: "none"
4063
+ }
4064
+ ), /* @__PURE__ */ React.createElement(
4065
+ "path",
4066
+ {
4067
+ d: pathD(scaledPoly),
4068
+ fill: "none",
4069
+ stroke: CONFIG.color.tangramDecomposition.stroke,
4070
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
4071
+ }
4072
+ ))));
4073
+ } else {
4074
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(mask, scaleS, centerPos);
4075
+ return /* @__PURE__ */ React.createElement("g", { key: "sil-unified", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(
4076
+ "path",
4077
+ {
4078
+ key: `sil-${i}`,
4079
+ d: pathD(scaledPoly),
4080
+ fill: CONFIG.color.silhouetteMask,
4081
+ opacity: CONFIG.opacity.silhouetteMask,
4082
+ stroke: "none"
4083
+ }
4084
+ )));
4085
+ }
4086
+ };
4087
+ return /* @__PURE__ */ React.createElement("div", { style: {
4088
+ display: "flex",
4089
+ flexDirection: "column",
4090
+ alignItems: "center",
4091
+ justifyContent: "center",
4092
+ padding: "20px",
4093
+ background: "#f5f5f5"
4094
+ } }, instructions && /* @__PURE__ */ React.createElement(
4095
+ "div",
4096
+ {
4097
+ style: {
4098
+ maxWidth: "800px",
4099
+ width: "100%",
4100
+ marginBottom: "30px",
4101
+ textAlign: "center",
4102
+ fontSize: "18px",
4103
+ lineHeight: "1.5"
4104
+ },
4105
+ dangerouslySetInnerHTML: { __html: instructions }
4106
+ }
4107
+ ), /* @__PURE__ */ React.createElement("div", { style: {
4108
+ display: "flex",
4109
+ flexDirection: "column",
4110
+ alignItems: "center",
4111
+ gap: "30px"
4112
+ } }, /* @__PURE__ */ React.createElement(
4113
+ "svg",
4114
+ {
4115
+ width: viewport.w,
4116
+ height: viewport.h,
4117
+ viewBox: `0 0 ${viewport.w} ${viewport.h}`,
4118
+ style: {
4119
+ display: "block",
4120
+ background: CONFIG.color.bands.silhouette.fillEven,
4121
+ border: `${CONFIG.size.stroke.bandPx}px solid ${CONFIG.color.bands.silhouette.stroke}`,
4122
+ borderRadius: "8px"
4123
+ }
4124
+ },
4125
+ renderSilhouette()
4126
+ ), /* @__PURE__ */ React.createElement(
4127
+ "button",
4128
+ {
4129
+ className: "jspsych-btn",
4130
+ onClick: handleButtonClick,
4131
+ disabled: buttonDisabled,
4132
+ style: {
4133
+ padding: "12px 30px",
4134
+ fontSize: "16px",
4135
+ cursor: buttonDisabled ? "not-allowed" : "pointer",
4136
+ opacity: buttonDisabled ? 0.5 : 1
4137
+ }
4138
+ },
4139
+ button_text
4140
+ )));
4141
+ }
4142
+
4143
+ const info = {
4144
+ name: "tangram-nback",
4145
+ version: "1.0.0",
4146
+ parameters: {
4147
+ /** Single tangram specification to display */
4148
+ tangram: {
4149
+ type: ParameterType.COMPLEX,
4150
+ default: void 0,
4151
+ description: "TangramSpec object defining target shape to display"
4152
+ },
4153
+ /** Whether this trial is a match (for computing accuracy) */
4154
+ isMatch: {
4155
+ type: ParameterType.BOOL,
4156
+ default: void 0,
4157
+ description: "Whether this tangram matches the previous one (optional)"
4158
+ },
4159
+ /** Whether to show tangram decomposed into individual primitives with borders */
4160
+ show_tangram_decomposition: {
4161
+ type: ParameterType.BOOL,
4162
+ default: false,
4163
+ description: "Whether to show tangram decomposed into individual primitives with borders"
4164
+ },
4165
+ /** HTML content to display above the tangram as instructions */
4166
+ instructions: {
4167
+ type: ParameterType.STRING,
4168
+ default: "",
4169
+ description: "HTML content to display above the tangram as instructions"
4170
+ },
4171
+ /** Text to display on response button */
4172
+ button_text: {
4173
+ type: ParameterType.STRING,
4174
+ default: "Same as previous!",
4175
+ description: "Text to display on response button"
4176
+ },
4177
+ /** Duration to display tangram and accept responses (milliseconds) */
4178
+ duration: {
4179
+ type: ParameterType.INT,
4180
+ default: 3e3,
4181
+ description: "Duration in milliseconds to display tangram and accept responses"
4182
+ },
4183
+ /** Callback fired when trial ends */
4184
+ onTrialEnd: {
4185
+ type: ParameterType.FUNCTION,
4186
+ default: void 0,
4187
+ description: "Callback when trial completes with full data"
4188
+ }
4189
+ },
4190
+ data: {
4191
+ /** Whether participant clicked the response button before duration expired */
4192
+ responded_match: {
4193
+ type: ParameterType.BOOL,
4194
+ description: "True if participant clicked response button, false otherwise"
4195
+ },
4196
+ /** Reaction time in milliseconds (NaN if no response or response after duration) */
4197
+ rt: {
4198
+ type: ParameterType.INT,
4199
+ description: "Milliseconds between trial start and button click (NaN if no response or late response)"
4200
+ },
4201
+ /** Accuracy: 1 if correct, 0 if incorrect, NaN if isMatch not provided */
4202
+ accuracy: {
4203
+ type: ParameterType.FLOAT,
4204
+ description: "1 if response matches isMatch parameter, 0 otherwise (NaN if isMatch not provided)"
4205
+ },
4206
+ /** Whether response occurred after duration expired */
4207
+ responded_after_duration: {
4208
+ type: ParameterType.BOOL,
4209
+ description: "True if button clicked after duration expired, false otherwise"
4210
+ },
4211
+ /** Time of late response (NaN if no late response) */
4212
+ rt_after_duration: {
4213
+ type: ParameterType.INT,
4214
+ description: "Milliseconds between trial start and late button click (NaN if no late response)"
4215
+ }
4216
+ },
4217
+ citations: ""
4218
+ };
4219
+ class TangramNBackPlugin {
4220
+ constructor(jsPsych) {
4221
+ this.jsPsych = jsPsych;
4222
+ }
4223
+ static {
4224
+ this.info = info;
4225
+ }
4226
+ /**
4227
+ * Launches the trial by invoking startNBackTrial
4228
+ * with the display element, parameters, and jsPsych instance.
4229
+ */
4230
+ trial(display_element, trial) {
4231
+ const wrappedOnTrialEnd = (data) => {
4232
+ if (trial.onTrialEnd) {
4233
+ trial.onTrialEnd(data);
4234
+ }
4235
+ const reactContext = display_element.__reactContext;
4236
+ if (reactContext?.root) {
4237
+ reactContext.root.unmount();
4238
+ }
4239
+ display_element.innerHTML = "";
4240
+ this.jsPsych.finishTrial(data);
4241
+ };
4242
+ const params = {
4243
+ tangram: trial.tangram,
4244
+ isMatch: trial.isMatch,
4245
+ show_tangram_decomposition: trial.show_tangram_decomposition,
4246
+ instructions: trial.instructions,
4247
+ button_text: trial.button_text,
4248
+ duration: trial.duration,
4249
+ onTrialEnd: wrappedOnTrialEnd
4250
+ };
4251
+ const { root, display_element: element, jsPsych } = startNBackTrial(display_element, params, this.jsPsych);
4252
+ element.__reactContext = { root, jsPsych };
4253
+ }
4254
+ }
4255
+
4256
+ export { TangramConstructPlugin, TangramNBackPlugin, TangramPrepPlugin };
3935
4257
  //# sourceMappingURL=index.js.map