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.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
 
@@ -12,9 +12,8 @@ const CONFIG = {
12
12
  completion: { fill: "#ccfff2", stroke: "#13da57" },
13
13
  silhouetteMask: "#374151",
14
14
  anchors: { invalid: "#7dd3fc", valid: "#475569" },
15
- piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
16
- ui: { light: "#60a5fa", dark: "#1d4ed8" },
17
- blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
15
+ piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", allGreenStroke: "#86efac"},
16
+ blueprint: { fill: "#374151", badgeFill: "#000000", labelFill: "#ffffff" },
18
17
  tangramDecomposition: { stroke: "#fef2cc" }
19
18
  },
20
19
  opacity: {
@@ -25,7 +24,7 @@ const CONFIG = {
25
24
  piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
26
25
  },
27
26
  size: {
28
- stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
27
+ stroke: { bandPx: 5, allGreenStrokePx: 10, tangramDecompositionPx: 1 },
29
28
  anchorRadiusPx: { valid: 1, invalid: 1 },
30
29
  badgeFontPx: 16,
31
30
  centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
@@ -45,9 +44,7 @@ const CONFIG = {
45
44
  },
46
45
  game: {
47
46
  snapRadiusPx: 15,
48
- showBorders: false,
49
- hideTouchingBorders: true
50
- }
47
+ showBorders: false}
51
48
  };
52
49
 
53
50
  function isComposite(bp) {
@@ -823,6 +820,10 @@ function shouldUseSelectiveBorders(blueprintId) {
823
820
  return CONFIG.game.showBorders;
824
821
  }
825
822
 
823
+ 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=";
824
+
825
+ 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==";
826
+
826
827
  function pathD(poly) {
827
828
  return `M ${poly.map((pt) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
828
829
  }
@@ -993,10 +994,20 @@ function BoardView(props) {
993
994
  }
994
995
  )));
995
996
  }),
997
+ /* @__PURE__ */ React.createElement("defs", null, /* @__PURE__ */ React.createElement("filter", { id: "invert-to-white" }, /* @__PURE__ */ React.createElement(
998
+ "feColorMatrix",
999
+ {
1000
+ type: "matrix",
1001
+ values: "-1 0 0 0 1\n 0 -1 0 0 1\n 0 0 -1 0 1\n 0 0 0 1 0"
1002
+ }
1003
+ ))),
996
1004
  (() => {
997
1005
  const isPrep = controller.state.cfg.mode === "prep";
998
1006
  const isSubmitEnabled = isPrep ? controller.isSubmitEnabled() : true;
999
1007
  const isClickable = !draggingId && (!isPrep || isSubmitEnabled);
1008
+ const [imageError, setImageError] = React.useState(false);
1009
+ const iconSize = badgeR * 1.6;
1010
+ const iconOffset = iconSize / 2;
1000
1011
  return /* @__PURE__ */ React.createElement(
1001
1012
  "g",
1002
1013
  {
@@ -1012,7 +1023,7 @@ function BoardView(props) {
1012
1023
  opacity: isSubmitEnabled ? 1 : 0.5
1013
1024
  }
1014
1025
  ),
1015
- /* @__PURE__ */ React.createElement(
1026
+ isPrep ? /* @__PURE__ */ React.createElement(
1016
1027
  "text",
1017
1028
  {
1018
1029
  textAnchor: "middle",
@@ -1021,7 +1032,29 @@ function BoardView(props) {
1021
1032
  fill: isSubmitEnabled ? CONFIG.color.blueprint.labelFill : "#888",
1022
1033
  pointerEvents: "none"
1023
1034
  },
1024
- isPrep ? "Submit" : controller.state.blueprintView
1035
+ "Submit"
1036
+ ) : imageError ? /* @__PURE__ */ React.createElement(
1037
+ "text",
1038
+ {
1039
+ textAnchor: "middle",
1040
+ dominantBaseline: "middle",
1041
+ fontSize: CONFIG.size.badgeFontPx,
1042
+ fill: CONFIG.color.blueprint.labelFill,
1043
+ pointerEvents: "none"
1044
+ },
1045
+ "inventory"
1046
+ ) : /* @__PURE__ */ React.createElement(
1047
+ "image",
1048
+ {
1049
+ href: controller.state.blueprintView === "quickstash" ? lockedIcon : unlockedIcon,
1050
+ x: -iconOffset,
1051
+ y: -iconOffset,
1052
+ width: iconSize,
1053
+ height: iconSize,
1054
+ pointerEvents: "none",
1055
+ onError: () => setImageError(true),
1056
+ filter: "url(#invert-to-white)"
1057
+ }
1025
1058
  )
1026
1059
  );
1027
1060
  })(),
@@ -2609,6 +2642,7 @@ class InteractionTracker {
2609
2642
  const trialEndTime = Date.now();
2610
2643
  const totalDuration = trialEndTime - this.trialStartTime;
2611
2644
  const finalSnapshot = this.buildStateSnapshot();
2645
+ const anchorToStimuliRatio = CONFIG.layout.grid.stepPx / CONFIG.layout.grid.unitPx;
2612
2646
  const mode = this.controller.state.cfg.mode;
2613
2647
  if (mode === "construction") {
2614
2648
  const finalBlueprintState = this.buildFinalBlueprintState(finalSnapshot);
@@ -2623,6 +2657,7 @@ class InteractionTracker {
2623
2657
  trialStartTime: this.trialStartTime,
2624
2658
  trialEndTime,
2625
2659
  totalDuration,
2660
+ anchorToStimuliRatio,
2626
2661
  trialParams: this.trialParams,
2627
2662
  endReason,
2628
2663
  completionTimes: this.completionTimes,
@@ -2645,6 +2680,7 @@ class InteractionTracker {
2645
2680
  trialStartTime: this.trialStartTime,
2646
2681
  trialEndTime,
2647
2682
  totalDuration,
2683
+ anchorToStimuliRatio,
2648
2684
  trialParams: this.trialParams,
2649
2685
  endReason: "submit",
2650
2686
  createdMacros: finalMacros,
@@ -3522,7 +3558,7 @@ function startConstructionTrial(display_element, params, _jsPsych) {
3522
3558
  return { root, display_element, jsPsych: _jsPsych };
3523
3559
  }
3524
3560
 
3525
- const info$1 = {
3561
+ const info$2 = {
3526
3562
  name: "tangram-construct",
3527
3563
  version: "1.0.0",
3528
3564
  parameters: {
@@ -3636,7 +3672,7 @@ class TangramConstructPlugin {
3636
3672
  this.jsPsych = jsPsych;
3637
3673
  }
3638
3674
  static {
3639
- this.info = info$1;
3675
+ this.info = info$2;
3640
3676
  }
3641
3677
  /**
3642
3678
  * Launches the trial by invoking startConstructionTrial
@@ -3780,7 +3816,7 @@ function startPrepTrial(display_element, params, jsPsych) {
3780
3816
  return { root, display_element, jsPsych };
3781
3817
  }
3782
3818
 
3783
- const info = {
3819
+ const info$1 = {
3784
3820
  name: "tangram-prep",
3785
3821
  version: "1.0.0",
3786
3822
  parameters: {
@@ -3856,7 +3892,7 @@ class TangramPrepPlugin {
3856
3892
  this.jsPsych = jsPsych;
3857
3893
  }
3858
3894
  static {
3859
- this.info = info;
3895
+ this.info = info$1;
3860
3896
  }
3861
3897
  /**
3862
3898
  * Launches the trial by invoking startPrepTrial
@@ -3892,5 +3928,328 @@ class TangramPrepPlugin {
3892
3928
  }
3893
3929
  }
3894
3930
 
3895
- export { TangramConstructPlugin, TangramPrepPlugin };
3931
+ function startNBackTrial(display_element, params, _jsPsych) {
3932
+ const root = createRoot(display_element);
3933
+ root.render(React.createElement(NBackView, { params }));
3934
+ return { root, display_element, jsPsych: _jsPsych };
3935
+ }
3936
+ function NBackView({ params }) {
3937
+ const {
3938
+ tangram,
3939
+ isMatch,
3940
+ show_tangram_decomposition,
3941
+ instructions,
3942
+ button_text,
3943
+ duration,
3944
+ onTrialEnd
3945
+ } = params;
3946
+ const trialStartTime = useRef(Date.now());
3947
+ const buttonEnabledRef = useRef(true);
3948
+ const timeoutIdRef = useRef(null);
3949
+ const hasRespondedRef = useRef(false);
3950
+ const responseDataRef = useRef(null);
3951
+ const [buttonDisabled, setButtonDisabled] = useState(false);
3952
+ const CANON = /* @__PURE__ */ new Set([
3953
+ "square",
3954
+ "smalltriangle",
3955
+ "parallelogram",
3956
+ "medtriangle",
3957
+ "largetriangle"
3958
+ ]);
3959
+ const filteredTans = tangram.solutionTans.filter((tan) => {
3960
+ const tanName = tan.name ?? tan.kind;
3961
+ return CANON.has(tanName);
3962
+ });
3963
+ const mask = filteredTans.map((tan) => {
3964
+ const polygon = tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }));
3965
+ return polygon;
3966
+ });
3967
+ const primitiveDecomposition = filteredTans.map((tan) => ({
3968
+ kind: tan.name ?? tan.kind,
3969
+ polygon: tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }))
3970
+ }));
3971
+ const DISPLAY_SIZE = 400;
3972
+ const viewport = {
3973
+ w: DISPLAY_SIZE,
3974
+ h: DISPLAY_SIZE
3975
+ };
3976
+ const scaleS = React.useMemo(() => {
3977
+ const u = inferUnitFromPolys$1(mask);
3978
+ return u ? CONFIG.layout.grid.unitPx / u : 1;
3979
+ }, [mask]);
3980
+ const centerPos = {
3981
+ cx: viewport.w / 2,
3982
+ cy: viewport.h / 2
3983
+ };
3984
+ const pathD = (poly) => {
3985
+ if (!poly || poly.length === 0) return "";
3986
+ const moves = poly.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`);
3987
+ return moves.join(" ") + " Z";
3988
+ };
3989
+ const endTrial = (data) => {
3990
+ if (timeoutIdRef.current) {
3991
+ clearTimeout(timeoutIdRef.current);
3992
+ timeoutIdRef.current = null;
3993
+ }
3994
+ const accuracy = isMatch !== void 0 ? isMatch === data.responded_match ? 1 : 0 : NaN;
3995
+ const trialData = {
3996
+ ...data,
3997
+ accuracy,
3998
+ tangram_id: tangram.tangramID,
3999
+ is_match: isMatch
4000
+ };
4001
+ if (onTrialEnd) {
4002
+ onTrialEnd(trialData);
4003
+ }
4004
+ };
4005
+ const handleButtonClick = () => {
4006
+ if (!buttonEnabledRef.current) {
4007
+ const rt_late = Date.now() - trialStartTime.current;
4008
+ hasRespondedRef.current = true;
4009
+ responseDataRef.current = {
4010
+ responded_match: true,
4011
+ rt: NaN,
4012
+ responded_after_duration: true,
4013
+ rt_after_duration: rt_late
4014
+ };
4015
+ endTrial(responseDataRef.current);
4016
+ } else {
4017
+ const rt = Date.now() - trialStartTime.current;
4018
+ buttonEnabledRef.current = false;
4019
+ setButtonDisabled(true);
4020
+ hasRespondedRef.current = true;
4021
+ responseDataRef.current = {
4022
+ responded_match: true,
4023
+ rt,
4024
+ responded_after_duration: false,
4025
+ rt_after_duration: NaN
4026
+ };
4027
+ }
4028
+ };
4029
+ useEffect(() => {
4030
+ timeoutIdRef.current = setTimeout(() => {
4031
+ buttonEnabledRef.current = false;
4032
+ if (hasRespondedRef.current && responseDataRef.current) {
4033
+ endTrial(responseDataRef.current);
4034
+ } else {
4035
+ endTrial({
4036
+ responded_match: false,
4037
+ rt: NaN,
4038
+ responded_after_duration: false,
4039
+ rt_after_duration: NaN
4040
+ });
4041
+ }
4042
+ }, duration);
4043
+ return () => {
4044
+ if (timeoutIdRef.current) {
4045
+ clearTimeout(timeoutIdRef.current);
4046
+ }
4047
+ };
4048
+ }, []);
4049
+ const renderSilhouette = () => {
4050
+ if (show_tangram_decomposition) {
4051
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
4052
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, centerPos);
4053
+ 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(
4054
+ "path",
4055
+ {
4056
+ d: pathD(scaledPoly),
4057
+ fill: CONFIG.color.silhouetteMask,
4058
+ opacity: CONFIG.opacity.silhouetteMask,
4059
+ stroke: "none"
4060
+ }
4061
+ ), /* @__PURE__ */ React.createElement(
4062
+ "path",
4063
+ {
4064
+ d: pathD(scaledPoly),
4065
+ fill: "none",
4066
+ stroke: CONFIG.color.tangramDecomposition.stroke,
4067
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
4068
+ }
4069
+ ))));
4070
+ } else {
4071
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(mask, scaleS, centerPos);
4072
+ return /* @__PURE__ */ React.createElement("g", { key: "sil-unified", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(
4073
+ "path",
4074
+ {
4075
+ key: `sil-${i}`,
4076
+ d: pathD(scaledPoly),
4077
+ fill: CONFIG.color.silhouetteMask,
4078
+ opacity: CONFIG.opacity.silhouetteMask,
4079
+ stroke: "none"
4080
+ }
4081
+ )));
4082
+ }
4083
+ };
4084
+ return /* @__PURE__ */ React.createElement("div", { style: {
4085
+ display: "flex",
4086
+ flexDirection: "column",
4087
+ alignItems: "center",
4088
+ justifyContent: "flex-start",
4089
+ minHeight: "100vh",
4090
+ padding: "40px 20px",
4091
+ background: "#f5f5f5"
4092
+ } }, instructions && /* @__PURE__ */ React.createElement(
4093
+ "div",
4094
+ {
4095
+ style: {
4096
+ maxWidth: "800px",
4097
+ width: "100%",
4098
+ marginBottom: "30px",
4099
+ textAlign: "center",
4100
+ fontSize: "18px",
4101
+ lineHeight: "1.5"
4102
+ },
4103
+ dangerouslySetInnerHTML: { __html: instructions }
4104
+ }
4105
+ ), /* @__PURE__ */ React.createElement("div", { style: {
4106
+ display: "flex",
4107
+ flexDirection: "column",
4108
+ alignItems: "center",
4109
+ gap: "30px"
4110
+ } }, /* @__PURE__ */ React.createElement(
4111
+ "svg",
4112
+ {
4113
+ width: viewport.w,
4114
+ height: viewport.h,
4115
+ viewBox: `0 0 ${viewport.w} ${viewport.h}`,
4116
+ style: {
4117
+ display: "block",
4118
+ background: CONFIG.color.bands.silhouette.fillEven,
4119
+ border: `${CONFIG.size.stroke.bandPx}px solid ${CONFIG.color.bands.silhouette.stroke}`,
4120
+ borderRadius: "8px"
4121
+ }
4122
+ },
4123
+ renderSilhouette()
4124
+ ), /* @__PURE__ */ React.createElement(
4125
+ "button",
4126
+ {
4127
+ className: "jspsych-btn",
4128
+ onClick: handleButtonClick,
4129
+ disabled: buttonDisabled,
4130
+ style: {
4131
+ padding: "12px 30px",
4132
+ fontSize: "16px",
4133
+ cursor: buttonDisabled ? "not-allowed" : "pointer",
4134
+ opacity: buttonDisabled ? 0.5 : 1
4135
+ }
4136
+ },
4137
+ button_text
4138
+ )));
4139
+ }
4140
+
4141
+ const info = {
4142
+ name: "tangram-nback",
4143
+ version: "1.0.0",
4144
+ parameters: {
4145
+ /** Single tangram specification to display */
4146
+ tangram: {
4147
+ type: ParameterType.COMPLEX,
4148
+ default: void 0,
4149
+ description: "TangramSpec object defining target shape to display"
4150
+ },
4151
+ /** Whether this trial is a match (for computing accuracy) */
4152
+ isMatch: {
4153
+ type: ParameterType.BOOL,
4154
+ default: void 0,
4155
+ description: "Whether this tangram matches the previous one (optional)"
4156
+ },
4157
+ /** Whether to show tangram decomposed into individual primitives with borders */
4158
+ show_tangram_decomposition: {
4159
+ type: ParameterType.BOOL,
4160
+ default: false,
4161
+ description: "Whether to show tangram decomposed into individual primitives with borders"
4162
+ },
4163
+ /** HTML content to display above the tangram as instructions */
4164
+ instructions: {
4165
+ type: ParameterType.STRING,
4166
+ default: "",
4167
+ description: "HTML content to display above the tangram as instructions"
4168
+ },
4169
+ /** Text to display on response button */
4170
+ button_text: {
4171
+ type: ParameterType.STRING,
4172
+ default: "Same as previous!",
4173
+ description: "Text to display on response button"
4174
+ },
4175
+ /** Duration to display tangram and accept responses (milliseconds) */
4176
+ duration: {
4177
+ type: ParameterType.INT,
4178
+ default: 3e3,
4179
+ description: "Duration in milliseconds to display tangram and accept responses"
4180
+ },
4181
+ /** Callback fired when trial ends */
4182
+ onTrialEnd: {
4183
+ type: ParameterType.FUNCTION,
4184
+ default: void 0,
4185
+ description: "Callback when trial completes with full data"
4186
+ }
4187
+ },
4188
+ data: {
4189
+ /** Whether participant clicked the response button before duration expired */
4190
+ responded_match: {
4191
+ type: ParameterType.BOOL,
4192
+ description: "True if participant clicked response button, false otherwise"
4193
+ },
4194
+ /** Reaction time in milliseconds (NaN if no response or response after duration) */
4195
+ rt: {
4196
+ type: ParameterType.INT,
4197
+ description: "Milliseconds between trial start and button click (NaN if no response or late response)"
4198
+ },
4199
+ /** Accuracy: 1 if correct, 0 if incorrect, NaN if isMatch not provided */
4200
+ accuracy: {
4201
+ type: ParameterType.FLOAT,
4202
+ description: "1 if response matches isMatch parameter, 0 otherwise (NaN if isMatch not provided)"
4203
+ },
4204
+ /** Whether response occurred after duration expired */
4205
+ responded_after_duration: {
4206
+ type: ParameterType.BOOL,
4207
+ description: "True if button clicked after duration expired, false otherwise"
4208
+ },
4209
+ /** Time of late response (NaN if no late response) */
4210
+ rt_after_duration: {
4211
+ type: ParameterType.INT,
4212
+ description: "Milliseconds between trial start and late button click (NaN if no late response)"
4213
+ }
4214
+ },
4215
+ citations: ""
4216
+ };
4217
+ class TangramNBackPlugin {
4218
+ constructor(jsPsych) {
4219
+ this.jsPsych = jsPsych;
4220
+ }
4221
+ static {
4222
+ this.info = info;
4223
+ }
4224
+ /**
4225
+ * Launches the trial by invoking startNBackTrial
4226
+ * with the display element, parameters, and jsPsych instance.
4227
+ */
4228
+ trial(display_element, trial) {
4229
+ const wrappedOnTrialEnd = (data) => {
4230
+ if (trial.onTrialEnd) {
4231
+ trial.onTrialEnd(data);
4232
+ }
4233
+ const reactContext = display_element.__reactContext;
4234
+ if (reactContext?.root) {
4235
+ reactContext.root.unmount();
4236
+ }
4237
+ display_element.innerHTML = "";
4238
+ this.jsPsych.finishTrial(data);
4239
+ };
4240
+ const params = {
4241
+ tangram: trial.tangram,
4242
+ isMatch: trial.isMatch,
4243
+ show_tangram_decomposition: trial.show_tangram_decomposition,
4244
+ instructions: trial.instructions,
4245
+ button_text: trial.button_text,
4246
+ duration: trial.duration,
4247
+ onTrialEnd: wrappedOnTrialEnd
4248
+ };
4249
+ const { root, display_element: element, jsPsych } = startNBackTrial(display_element, params, this.jsPsych);
4250
+ element.__reactContext = { root, jsPsych };
4251
+ }
4252
+ }
4253
+
4254
+ export { TangramConstructPlugin, TangramNBackPlugin, TangramPrepPlugin };
3896
4255
  //# sourceMappingURL=index.js.map