jspsych-tangram 0.0.8 → 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 (44) hide show
  1. package/dist/construct/index.browser.js +4572 -3884
  2. package/dist/construct/index.browser.js.map +1 -1
  3. package/dist/construct/index.browser.min.js +15 -12
  4. package/dist/construct/index.browser.min.js.map +1 -1
  5. package/dist/construct/index.cjs +45 -9
  6. package/dist/construct/index.cjs.map +1 -1
  7. package/dist/construct/index.js +45 -9
  8. package/dist/construct/index.js.map +1 -1
  9. package/dist/index.cjs +373 -13
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.ts +180 -8
  12. package/dist/index.js +374 -15
  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 +4578 -3892
  24. package/dist/prep/index.browser.js.map +1 -1
  25. package/dist/prep/index.browser.min.js +16 -13
  26. package/dist/prep/index.browser.min.js.map +1 -1
  27. package/dist/prep/index.cjs +46 -12
  28. package/dist/prep/index.cjs.map +1 -1
  29. package/dist/prep/index.js +46 -12
  30. package/dist/prep/index.js.map +1 -1
  31. package/package.json +9 -3
  32. package/src/assets/README.md +6 -0
  33. package/src/assets/images.d.ts +19 -0
  34. package/src/assets/locked.png +0 -0
  35. package/src/assets/unlocked.png +0 -0
  36. package/src/core/components/board/BoardView.tsx +72 -29
  37. package/src/core/io/InteractionTracker.ts +16 -8
  38. package/src/core/io/data-tracking.ts +3 -0
  39. package/src/index.ts +2 -1
  40. package/src/plugins/tangram-nback/NBackApp.tsx +316 -0
  41. package/src/plugins/tangram-nback/index.ts +141 -0
  42. package/tangram-construct.min.js +15 -12
  43. package/tangram-nback.min.js +42 -0
  44. package/tangram-prep.min.js +16 -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) {
@@ -825,6 +822,10 @@ function shouldUseSelectiveBorders(blueprintId) {
825
822
  return CONFIG.game.showBorders;
826
823
  }
827
824
 
825
+ var lockedIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAYAAAA4qEECAAAACXBIWXMAAAsSAAALEgHS3X78AAAFY0lEQVR4nO2d7XXrNgyGH/r0f7zB9QbXnaDqBt6gvhM0I2SE3A2UDXwnqLKBPEHdDewJ0B+kYieWZEoiQfrjPQcnjqOI0GuYAAFQMiJCLjDGLIEFsHQyB/7w/Pd397MCamAnInVgFUfDpCTaGLMACmDlfj4FHuKAJX4DVCKyC3x+b6gTbYyZA2sn31UHhy1QAqWI7FVHFhEVwVpsCUgmUgKF2vUrEVxlQGyXVBqE3zPBqoTHIHhOXlPEUCmBedZEY6OHfQZkTZU9sMqO6Buw4ujWHYLkJXaBkJqUWFIDy6REYx3eLUwVl2TPREc5heR1BgRoy1qV6DsleRLZD5KVyH6QrET2UMeX+uJyE28H6UvykvuILobKHs/Qz4fkObcdJ0+VGo9FzcV8tDGmBP7qPSg+DtgLqrFWBLYSs8C/AhMTbyKy7j3igjWvSPu1LPH4ajo9y4S6ChdyI5emjFTz8isjcgxYC98kNIxOnfuUTmEhk5e6Tvd1IiMpBxFNmlDOy6kMIDtVpNRqKK3O0BhToetk/sPOxZ0FU1cxX2CNAGxFZCc9lW3XvlARvrreh3cRKc7ezcSaOx0e1tH1hZc1PY6INA79zKrbFKuUlXrpccZDdKnomHqwzlXzmqpeotG35l0PyWMWSa3zPGkiqKKP6FJZmXUH0VVIa0pk1WUr0e5T11REaLe+dYwPEOtIk13fqSLPykp0Wd4uwLm7piTtnM1zM/aMI9boovr6hgvHvgU49zdjTNHyvnZ36bp5MYOPGFW74bDtoouA52871y7g+X3w3XH7YdGFsgJwzMKdYh55zCry+dtQwJHoVQIF7gUrSGvR94ICYOYckGYu4N7wZIxZzLDx5QNxsZxh04kPxMUS9JNINfBChB7kS4KNal4JsygaIhtNooNUTwKS/oxeoqky7kVsbB3JujuhLkCzMKBF9O+S0ebKU7il+j+xx5ldPmQyfuZKMoCIVMCv2ONoEP2qMMZUlLEHiD11HEQkdv4iCIwxUafQ2Bad7ZShjd9SK+CLkz3kTQJsQ4o93RMQNX4MFPN2NcN4t816jBE1ltZwhpPgLLmiPdZ9Aip3TNbInmjsdNG3oHhCvww3GDOOd27JFT5FidwLF+8z2ktKD4TFfkb+Idgm0DEpUV8L0Yeevx94ED0dYttyC9rJPmCzgjtFlcagnjkl+ywmOVxSasHn5M8vYJFzwsrhICK7JryrUmriA7cCPCW1vpJVYQXHODr3Oe6asYEj0VU6PW4eFTii3Ty9TajMrWLbOOrTJXiZRJXbRtm8eBAdF2Xz4oNo58HfUmhzo3g7jYq+Zu9KXV0Go+54nSPKT7+1JMArwiW86wiNL2sm3ERKKfF/VvA42zkbus9BREyoc8WC68r/N+Ap/3RtDB84S/y7A4LlqI0xueeKIWx/+PtXkoHzqeOkRpdV3TCmELbpsbWG2Td4yA2QwefUgCS/BLzO185xehSYB/6ksyObsLeX2zHmxihOkdB3Biixqc3UBC8If6eaom9Mn5tXvQJ/9x40HFtsGKmd5pxjHV/oPZU/ReS57wCvp1YYY2r0N3xeC7YicnF7im9fx4rMqzCJcMCz1cGLaDnW7R74jEI865XenUpia3M/xmp0g/ghQ+qViUOia5XBoWoO8ee1yaj1QC7B/rXIKJK94ug+JLqvXAo0jTqjc+CT2nbdwEtuu7C7xSaKphUaAi1pm62/qb/aoWXUzWiDztEdhD8e4aRB9I1YdzArjkr0CeGNo0xNnK9UBNp4pEr0CeFF5oRXXPODIzsILzMgtpFSg+BGHg/3VcLjcdVKSEr0Vzjil4x/AHvTrF5jm3d2wZUcif8BqSLxz8FiAOgAAAAASUVORK5CYII=";
826
+
827
+ var unlockedIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAYAAAA4qEECAAAACXBIWXMAAAsSAAALEgHS3X78AAAFYklEQVR4nO1d0ZGjOBB9ct3/+iJYMlg2giODcwjeDCYEbwbeDNgMvBloIjgmAyYDHEHfh4SHsQUIaLVk7FfVNTblkZpHq1u0GqGICKlAKZUDyADkVrYA/vH891f7VwOoANREVDGrOBsqJtFKqQxAAWBn/35h7uIMQ/wJgCaimrl9b4gTrZTaAthb+SbaOfAGoARQElEj2jMRiQiMxZYAKBEpARRi5y9EsE6A2D7REoQ/MsGihIcgeIu0XMRUKQFskyYaZvbQJEDWUmkA7JIjegVWHNy6OUjOYW4QYpMSSioAeVSiYQLeGlzFmDRYGCiXkLxPgABp2YsS/aAkLyL7SbIQ2U+ShcieGvhin1xq4h0gvbJ3Nk+swZ/G9MEZZooFmKlkDB36cIYhezzv7WHJW8jOkxsAR5i7TOfNAszoOgKoBfXqk6pPz0muA3J3fDXmBBkTN2ITXi4i2lqVhKJHH6sYGXXHyGQP5kbGlJe465tsxSPWHYvoZshYYrsMNpITIbvXhfQpWwgo9eJB2AmfR1Vjjw1eoMhkF1OI1oGV0SNxofZoo8aAX7QXJAbRznOLZc1ZDzlzAtqxp60sEtEEh1XHsGanHwNwWNCm0w0h3mLEjVUnYc1M/d4k55GQVUtbQBUwJpx62q4jEV06iYaZN4fu/MafgtfqXKMlFtGEzrx6gw/sER6u5MuOsf1D94tS6gXAV8b2p2J/+dS58hKJo9tovCwIukTbNrXA+YxJ9cl1QC5ouIiONd+VkqzrOgrEQzI1zIFQALgQzeknh5AJ9ZMSdgDELTpzHDsJ9R0LRfshR4TgkNAUTMRPbyA7nL/ZxymuoQV1iIF8A2PRknDFg7W7j3wD+RnHTX9EdIJZUV4r8r8idPqvUmpLtw/rHBH+oscqV9gCskGhgblrY6+o9xWYmHSAfBWsWEenmAQ7CN9C9q5UpJMyNrEDhGsJDlTLdkC8wyTlZR+g9IR9wLRGYN+9Gf/JYhxSJRkArG7H0P1IWPTfKRMNXIo4/wvaB8IS/U5EWcD22aCUCmpwoV1HHbj9u4GEj2aBUmqnlKqUUmSlUkpJpXdZEHJao5mmYEOFNc4Cmhl9BJ9H65SJhl/NR5E40XoDcyuaMnzcwz60EgvRbJD+mp1PGjcLrcRCVPdAtM+IS31U3gXRPosCqS8cVG0gCJUy5Jp1DBX3ONchEwqGDdFHXYdmuGohUQD44zj+B3FrUnyggY8blqSHHhE1RLQD8LNz+CcR7VLPo8Byey8Wfc/QgCWazA6HbxGVWSveLLefch1lFFXWjbL98CQ6LMr2w4VoG1R+x9BmpfjdDdTXadJSVpdVo+x++UQ0EWl87MP8xHy8Wi4vcCX+DyKqzENbOnZG2nP/w/UB5w40SikN/53Ih8C+ZmgXUkHMu50zrhm+ElFx034P0Zyrwo+2Cv7dZQTONUP7w19MHd/Duh6Xjr96R9pANqut4FmavaqRUM1dz3lyZC8Hz7N3FdwO95cJV7MPXyFQCbQAJ/CUg+0HXaTHFefaq6hEQpYN3mrS0ZV4X6W4nqqtYRZS11Qf7bXw4LvBYGbJ5qy4fId8JRN3xf8ZplK2Hvuh93tYJAoB7xDOqZwL3iVhtsEfs1VaH374kgzAz0df+bg9eHzbPct+Mm8zA8ojkz2Z5NlEPzDZs0j2nnX0IfJ2x5Lw3764B4vqo23HOda9sPsGM4Vbli1kuglIYbfbELJoF2AWH91D+PMVThJEr8S62aw4KNEdwttAGZs4X9FgeFWTONEdwovECde45xdH9hBeJkBsK6UEwa08X+4rhOfrqoUQlehrWOJzzH8BewOTN69gEvI1u5Iz8T+TOcUtjzRRXwAAAABJRU5ErkJggg==";
828
+
828
829
  function pathD(poly) {
829
830
  return `M ${poly.map((pt) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
830
831
  }
@@ -995,10 +996,20 @@ function BoardView(props) {
995
996
  }
996
997
  )));
997
998
  }),
999
+ /* @__PURE__ */ React.createElement("defs", null, /* @__PURE__ */ React.createElement("filter", { id: "invert-to-white" }, /* @__PURE__ */ React.createElement(
1000
+ "feColorMatrix",
1001
+ {
1002
+ type: "matrix",
1003
+ values: "-1 0 0 0 1\n 0 -1 0 0 1\n 0 0 -1 0 1\n 0 0 0 1 0"
1004
+ }
1005
+ ))),
998
1006
  (() => {
999
1007
  const isPrep = controller.state.cfg.mode === "prep";
1000
1008
  const isSubmitEnabled = isPrep ? controller.isSubmitEnabled() : true;
1001
1009
  const isClickable = !draggingId && (!isPrep || isSubmitEnabled);
1010
+ const [imageError, setImageError] = React.useState(false);
1011
+ const iconSize = badgeR * 1.6;
1012
+ const iconOffset = iconSize / 2;
1002
1013
  return /* @__PURE__ */ React.createElement(
1003
1014
  "g",
1004
1015
  {
@@ -1014,7 +1025,7 @@ function BoardView(props) {
1014
1025
  opacity: isSubmitEnabled ? 1 : 0.5
1015
1026
  }
1016
1027
  ),
1017
- /* @__PURE__ */ React.createElement(
1028
+ isPrep ? /* @__PURE__ */ React.createElement(
1018
1029
  "text",
1019
1030
  {
1020
1031
  textAnchor: "middle",
@@ -1023,7 +1034,29 @@ function BoardView(props) {
1023
1034
  fill: isSubmitEnabled ? CONFIG.color.blueprint.labelFill : "#888",
1024
1035
  pointerEvents: "none"
1025
1036
  },
1026
- isPrep ? "Submit" : controller.state.blueprintView
1037
+ "Submit"
1038
+ ) : imageError ? /* @__PURE__ */ React.createElement(
1039
+ "text",
1040
+ {
1041
+ textAnchor: "middle",
1042
+ dominantBaseline: "middle",
1043
+ fontSize: CONFIG.size.badgeFontPx,
1044
+ fill: CONFIG.color.blueprint.labelFill,
1045
+ pointerEvents: "none"
1046
+ },
1047
+ "inventory"
1048
+ ) : /* @__PURE__ */ React.createElement(
1049
+ "image",
1050
+ {
1051
+ href: controller.state.blueprintView === "quickstash" ? lockedIcon : unlockedIcon,
1052
+ x: -iconOffset,
1053
+ y: -iconOffset,
1054
+ width: iconSize,
1055
+ height: iconSize,
1056
+ pointerEvents: "none",
1057
+ onError: () => setImageError(true),
1058
+ filter: "url(#invert-to-white)"
1059
+ }
1027
1060
  )
1028
1061
  );
1029
1062
  })(),
@@ -2611,6 +2644,7 @@ class InteractionTracker {
2611
2644
  const trialEndTime = Date.now();
2612
2645
  const totalDuration = trialEndTime - this.trialStartTime;
2613
2646
  const finalSnapshot = this.buildStateSnapshot();
2647
+ const anchorToStimuliRatio = CONFIG.layout.grid.stepPx / CONFIG.layout.grid.unitPx;
2614
2648
  const mode = this.controller.state.cfg.mode;
2615
2649
  if (mode === "construction") {
2616
2650
  const finalBlueprintState = this.buildFinalBlueprintState(finalSnapshot);
@@ -2625,6 +2659,7 @@ class InteractionTracker {
2625
2659
  trialStartTime: this.trialStartTime,
2626
2660
  trialEndTime,
2627
2661
  totalDuration,
2662
+ anchorToStimuliRatio,
2628
2663
  trialParams: this.trialParams,
2629
2664
  endReason,
2630
2665
  completionTimes: this.completionTimes,
@@ -2647,6 +2682,7 @@ class InteractionTracker {
2647
2682
  trialStartTime: this.trialStartTime,
2648
2683
  trialEndTime,
2649
2684
  totalDuration,
2685
+ anchorToStimuliRatio,
2650
2686
  trialParams: this.trialParams,
2651
2687
  endReason: "submit",
2652
2688
  createdMacros: finalMacros,
@@ -3524,7 +3560,7 @@ function startConstructionTrial(display_element, params, _jsPsych) {
3524
3560
  return { root, display_element, jsPsych: _jsPsych };
3525
3561
  }
3526
3562
 
3527
- const info$1 = {
3563
+ const info$2 = {
3528
3564
  name: "tangram-construct",
3529
3565
  version: "1.0.0",
3530
3566
  parameters: {
@@ -3638,7 +3674,7 @@ class TangramConstructPlugin {
3638
3674
  this.jsPsych = jsPsych;
3639
3675
  }
3640
3676
  static {
3641
- this.info = info$1;
3677
+ this.info = info$2;
3642
3678
  }
3643
3679
  /**
3644
3680
  * Launches the trial by invoking startConstructionTrial
@@ -3782,7 +3818,7 @@ function startPrepTrial(display_element, params, jsPsych) {
3782
3818
  return { root, display_element, jsPsych };
3783
3819
  }
3784
3820
 
3785
- const info = {
3821
+ const info$1 = {
3786
3822
  name: "tangram-prep",
3787
3823
  version: "1.0.0",
3788
3824
  parameters: {
@@ -3858,7 +3894,7 @@ class TangramPrepPlugin {
3858
3894
  this.jsPsych = jsPsych;
3859
3895
  }
3860
3896
  static {
3861
- this.info = info;
3897
+ this.info = info$1;
3862
3898
  }
3863
3899
  /**
3864
3900
  * Launches the trial by invoking startPrepTrial
@@ -3894,6 +3930,330 @@ class TangramPrepPlugin {
3894
3930
  }
3895
3931
  }
3896
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
+
3897
4256
  exports.TangramConstructPlugin = TangramConstructPlugin;
4257
+ exports.TangramNBackPlugin = TangramNBackPlugin;
3898
4258
  exports.TangramPrepPlugin = TangramPrepPlugin;
3899
4259
  //# sourceMappingURL=index.cjs.map