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.
- package/dist/construct/index.browser.js +4572 -3884
- package/dist/construct/index.browser.js.map +1 -1
- package/dist/construct/index.browser.min.js +15 -12
- package/dist/construct/index.browser.min.js.map +1 -1
- package/dist/construct/index.cjs +45 -9
- package/dist/construct/index.cjs.map +1 -1
- package/dist/construct/index.js +45 -9
- package/dist/construct/index.js.map +1 -1
- package/dist/index.cjs +373 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +180 -8
- package/dist/index.js +374 -15
- package/dist/index.js.map +1 -1
- package/dist/nback/index.browser.js +17703 -0
- package/dist/nback/index.browser.js.map +1 -0
- package/dist/nback/index.browser.min.js +42 -0
- package/dist/nback/index.browser.min.js.map +1 -0
- package/dist/nback/index.cjs +395 -0
- package/dist/nback/index.cjs.map +1 -0
- package/dist/nback/index.d.ts +175 -0
- package/dist/nback/index.js +393 -0
- package/dist/nback/index.js.map +1 -0
- package/dist/prep/index.browser.js +4578 -3892
- package/dist/prep/index.browser.js.map +1 -1
- package/dist/prep/index.browser.min.js +16 -13
- package/dist/prep/index.browser.min.js.map +1 -1
- package/dist/prep/index.cjs +46 -12
- package/dist/prep/index.cjs.map +1 -1
- package/dist/prep/index.js +46 -12
- package/dist/prep/index.js.map +1 -1
- package/package.json +9 -3
- package/src/assets/README.md +6 -0
- package/src/assets/images.d.ts +19 -0
- package/src/assets/locked.png +0 -0
- package/src/assets/unlocked.png +0 -0
- package/src/core/components/board/BoardView.tsx +72 -29
- package/src/core/io/InteractionTracker.ts +16 -8
- package/src/core/io/data-tracking.ts +3 -0
- package/src/index.ts +2 -1
- package/src/plugins/tangram-nback/NBackApp.tsx +316 -0
- package/src/plugins/tangram-nback/index.ts +141 -0
- package/tangram-construct.min.js +15 -12
- package/tangram-nback.min.js +42 -0
- 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",
|
|
18
|
-
|
|
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,
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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
|