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.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",
|
|
16
|
-
|
|
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,
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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
|