jspsych-tangram 0.0.9 → 0.0.10

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.
Files changed (37) hide show
  1. package/dist/construct/index.browser.js +4538 -3889
  2. package/dist/construct/index.browser.js.map +1 -1
  3. package/dist/construct/index.browser.min.js +13 -13
  4. package/dist/construct/index.browser.min.js.map +1 -1
  5. package/dist/construct/index.cjs +4 -7
  6. package/dist/construct/index.cjs.map +1 -1
  7. package/dist/construct/index.js +4 -7
  8. package/dist/construct/index.js.map +1 -1
  9. package/dist/index.cjs +332 -11
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.ts +180 -8
  12. package/dist/index.js +333 -13
  13. package/dist/index.js.map +1 -1
  14. package/dist/nback/index.browser.js +17703 -0
  15. package/dist/nback/index.browser.js.map +1 -0
  16. package/dist/nback/index.browser.min.js +42 -0
  17. package/dist/nback/index.browser.min.js.map +1 -0
  18. package/dist/nback/index.cjs +395 -0
  19. package/dist/nback/index.cjs.map +1 -0
  20. package/dist/nback/index.d.ts +175 -0
  21. package/dist/nback/index.js +393 -0
  22. package/dist/nback/index.js.map +1 -0
  23. package/dist/prep/index.browser.js +4538 -3891
  24. package/dist/prep/index.browser.js.map +1 -1
  25. package/dist/prep/index.browser.min.js +13 -13
  26. package/dist/prep/index.browser.min.js.map +1 -1
  27. package/dist/prep/index.cjs +5 -10
  28. package/dist/prep/index.cjs.map +1 -1
  29. package/dist/prep/index.js +5 -10
  30. package/dist/prep/index.js.map +1 -1
  31. package/package.json +9 -3
  32. package/src/index.ts +2 -1
  33. package/src/plugins/tangram-nback/NBackApp.tsx +316 -0
  34. package/src/plugins/tangram-nback/index.ts +141 -0
  35. package/tangram-construct.min.js +13 -13
  36. package/tangram-nback.min.js +42 -0
  37. package/tangram-prep.min.js +13 -13
package/dist/index.cjs CHANGED
@@ -14,9 +14,8 @@ const CONFIG = {
14
14
  completion: { fill: "#ccfff2", stroke: "#13da57" },
15
15
  silhouetteMask: "#374151",
16
16
  anchors: { invalid: "#7dd3fc", valid: "#475569" },
17
- piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
18
- ui: { light: "#60a5fa", dark: "#1d4ed8" },
19
- blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
17
+ piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", allGreenStroke: "#86efac"},
18
+ blueprint: { fill: "#374151", badgeFill: "#000000", labelFill: "#ffffff" },
20
19
  tangramDecomposition: { stroke: "#fef2cc" }
21
20
  },
22
21
  opacity: {
@@ -27,7 +26,7 @@ const CONFIG = {
27
26
  piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
28
27
  },
29
28
  size: {
30
- stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
29
+ stroke: { bandPx: 5, allGreenStrokePx: 10, tangramDecompositionPx: 1 },
31
30
  anchorRadiusPx: { valid: 1, invalid: 1 },
32
31
  badgeFontPx: 16,
33
32
  centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
@@ -47,9 +46,7 @@ const CONFIG = {
47
46
  },
48
47
  game: {
49
48
  snapRadiusPx: 15,
50
- showBorders: false,
51
- hideTouchingBorders: true
52
- }
49
+ showBorders: false}
53
50
  };
54
51
 
55
52
  function isComposite(bp) {
@@ -3563,7 +3560,7 @@ function startConstructionTrial(display_element, params, _jsPsych) {
3563
3560
  return { root, display_element, jsPsych: _jsPsych };
3564
3561
  }
3565
3562
 
3566
- const info$1 = {
3563
+ const info$2 = {
3567
3564
  name: "tangram-construct",
3568
3565
  version: "1.0.0",
3569
3566
  parameters: {
@@ -3677,7 +3674,7 @@ class TangramConstructPlugin {
3677
3674
  this.jsPsych = jsPsych;
3678
3675
  }
3679
3676
  static {
3680
- this.info = info$1;
3677
+ this.info = info$2;
3681
3678
  }
3682
3679
  /**
3683
3680
  * Launches the trial by invoking startConstructionTrial
@@ -3821,7 +3818,7 @@ function startPrepTrial(display_element, params, jsPsych) {
3821
3818
  return { root, display_element, jsPsych };
3822
3819
  }
3823
3820
 
3824
- const info = {
3821
+ const info$1 = {
3825
3822
  name: "tangram-prep",
3826
3823
  version: "1.0.0",
3827
3824
  parameters: {
@@ -3897,7 +3894,7 @@ class TangramPrepPlugin {
3897
3894
  this.jsPsych = jsPsych;
3898
3895
  }
3899
3896
  static {
3900
- this.info = info;
3897
+ this.info = info$1;
3901
3898
  }
3902
3899
  /**
3903
3900
  * Launches the trial by invoking startPrepTrial
@@ -3933,6 +3930,330 @@ class TangramPrepPlugin {
3933
3930
  }
3934
3931
  }
3935
3932
 
3933
+ function startNBackTrial(display_element, params, _jsPsych) {
3934
+ const root = client.createRoot(display_element);
3935
+ root.render(React.createElement(NBackView, { params }));
3936
+ return { root, display_element, jsPsych: _jsPsych };
3937
+ }
3938
+ function NBackView({ params }) {
3939
+ const {
3940
+ tangram,
3941
+ isMatch,
3942
+ show_tangram_decomposition,
3943
+ instructions,
3944
+ button_text,
3945
+ duration,
3946
+ onTrialEnd
3947
+ } = params;
3948
+ const trialStartTime = React.useRef(Date.now());
3949
+ const buttonEnabledRef = React.useRef(true);
3950
+ const timeoutIdRef = React.useRef(null);
3951
+ const hasRespondedRef = React.useRef(false);
3952
+ const responseDataRef = React.useRef(null);
3953
+ const [buttonDisabled, setButtonDisabled] = React.useState(false);
3954
+ const CANON = /* @__PURE__ */ new Set([
3955
+ "square",
3956
+ "smalltriangle",
3957
+ "parallelogram",
3958
+ "medtriangle",
3959
+ "largetriangle"
3960
+ ]);
3961
+ const filteredTans = tangram.solutionTans.filter((tan) => {
3962
+ const tanName = tan.name ?? tan.kind;
3963
+ return CANON.has(tanName);
3964
+ });
3965
+ const mask = filteredTans.map((tan) => {
3966
+ const polygon = tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }));
3967
+ return polygon;
3968
+ });
3969
+ const primitiveDecomposition = filteredTans.map((tan) => ({
3970
+ kind: tan.name ?? tan.kind,
3971
+ polygon: tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }))
3972
+ }));
3973
+ const DISPLAY_SIZE = 400;
3974
+ const viewport = {
3975
+ w: DISPLAY_SIZE,
3976
+ h: DISPLAY_SIZE
3977
+ };
3978
+ const scaleS = React.useMemo(() => {
3979
+ const u = inferUnitFromPolys$1(mask);
3980
+ return u ? CONFIG.layout.grid.unitPx / u : 1;
3981
+ }, [mask]);
3982
+ const centerPos = {
3983
+ cx: viewport.w / 2,
3984
+ cy: viewport.h / 2
3985
+ };
3986
+ const pathD = (poly) => {
3987
+ if (!poly || poly.length === 0) return "";
3988
+ const moves = poly.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`);
3989
+ return moves.join(" ") + " Z";
3990
+ };
3991
+ const endTrial = (data) => {
3992
+ if (timeoutIdRef.current) {
3993
+ clearTimeout(timeoutIdRef.current);
3994
+ timeoutIdRef.current = null;
3995
+ }
3996
+ const accuracy = isMatch !== void 0 ? isMatch === data.responded_match ? 1 : 0 : NaN;
3997
+ const trialData = {
3998
+ ...data,
3999
+ accuracy,
4000
+ tangram_id: tangram.tangramID,
4001
+ is_match: isMatch
4002
+ };
4003
+ if (onTrialEnd) {
4004
+ onTrialEnd(trialData);
4005
+ }
4006
+ };
4007
+ const handleButtonClick = () => {
4008
+ if (!buttonEnabledRef.current) {
4009
+ const rt_late = Date.now() - trialStartTime.current;
4010
+ hasRespondedRef.current = true;
4011
+ responseDataRef.current = {
4012
+ responded_match: true,
4013
+ rt: NaN,
4014
+ responded_after_duration: true,
4015
+ rt_after_duration: rt_late
4016
+ };
4017
+ endTrial(responseDataRef.current);
4018
+ } else {
4019
+ const rt = Date.now() - trialStartTime.current;
4020
+ buttonEnabledRef.current = false;
4021
+ setButtonDisabled(true);
4022
+ hasRespondedRef.current = true;
4023
+ responseDataRef.current = {
4024
+ responded_match: true,
4025
+ rt,
4026
+ responded_after_duration: false,
4027
+ rt_after_duration: NaN
4028
+ };
4029
+ }
4030
+ };
4031
+ React.useEffect(() => {
4032
+ timeoutIdRef.current = setTimeout(() => {
4033
+ buttonEnabledRef.current = false;
4034
+ if (hasRespondedRef.current && responseDataRef.current) {
4035
+ endTrial(responseDataRef.current);
4036
+ } else {
4037
+ endTrial({
4038
+ responded_match: false,
4039
+ rt: NaN,
4040
+ responded_after_duration: false,
4041
+ rt_after_duration: NaN
4042
+ });
4043
+ }
4044
+ }, duration);
4045
+ return () => {
4046
+ if (timeoutIdRef.current) {
4047
+ clearTimeout(timeoutIdRef.current);
4048
+ }
4049
+ };
4050
+ }, []);
4051
+ const renderSilhouette = () => {
4052
+ if (show_tangram_decomposition) {
4053
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
4054
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, centerPos);
4055
+ 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(
4056
+ "path",
4057
+ {
4058
+ d: pathD(scaledPoly),
4059
+ fill: CONFIG.color.silhouetteMask,
4060
+ opacity: CONFIG.opacity.silhouetteMask,
4061
+ stroke: "none"
4062
+ }
4063
+ ), /* @__PURE__ */ React.createElement(
4064
+ "path",
4065
+ {
4066
+ d: pathD(scaledPoly),
4067
+ fill: "none",
4068
+ stroke: CONFIG.color.tangramDecomposition.stroke,
4069
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
4070
+ }
4071
+ ))));
4072
+ } else {
4073
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(mask, scaleS, centerPos);
4074
+ return /* @__PURE__ */ React.createElement("g", { key: "sil-unified", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(
4075
+ "path",
4076
+ {
4077
+ key: `sil-${i}`,
4078
+ d: pathD(scaledPoly),
4079
+ fill: CONFIG.color.silhouetteMask,
4080
+ opacity: CONFIG.opacity.silhouetteMask,
4081
+ stroke: "none"
4082
+ }
4083
+ )));
4084
+ }
4085
+ };
4086
+ return /* @__PURE__ */ React.createElement("div", { style: {
4087
+ display: "flex",
4088
+ flexDirection: "column",
4089
+ alignItems: "center",
4090
+ justifyContent: "flex-start",
4091
+ minHeight: "100vh",
4092
+ padding: "40px 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: jspsych.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: jspsych.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: jspsych.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: jspsych.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: jspsych.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: jspsych.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: jspsych.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: jspsych.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: jspsych.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: jspsych.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: jspsych.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: jspsych.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
+
3936
4256
  exports.TangramConstructPlugin = TangramConstructPlugin;
4257
+ exports.TangramNBackPlugin = TangramNBackPlugin;
3937
4258
  exports.TangramPrepPlugin = TangramPrepPlugin;
3938
4259
  //# sourceMappingURL=index.cjs.map