jspsych-tangram 0.0.16 → 0.0.18
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 +23 -5
- package/dist/construct/index.browser.js.map +1 -1
- package/dist/construct/index.browser.min.js +8 -8
- package/dist/construct/index.browser.min.js.map +1 -1
- package/dist/construct/index.cjs +23 -5
- package/dist/construct/index.cjs.map +1 -1
- package/dist/construct/index.js +23 -5
- package/dist/construct/index.js.map +1 -1
- package/dist/grid/index.browser.js +17855 -0
- package/dist/grid/index.browser.js.map +1 -0
- package/dist/grid/index.browser.min.js +47 -0
- package/dist/grid/index.browser.min.js.map +1 -0
- package/dist/grid/index.cjs +547 -0
- package/dist/grid/index.cjs.map +1 -0
- package/dist/grid/index.d.ts +174 -0
- package/dist/grid/index.js +545 -0
- package/dist/grid/index.js.map +1 -0
- package/dist/index.cjs +548 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +187 -16
- package/dist/index.js +549 -15
- package/dist/index.js.map +1 -1
- package/dist/prep/index.browser.js +23 -5
- package/dist/prep/index.browser.js.map +1 -1
- package/dist/prep/index.browser.min.js +6 -6
- package/dist/prep/index.browser.min.js.map +1 -1
- package/dist/prep/index.cjs +23 -5
- package/dist/prep/index.cjs.map +1 -1
- package/dist/prep/index.js +23 -5
- package/dist/prep/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/components/board/GameBoard.tsx +12 -0
- package/src/core/io/InteractionTracker.ts +19 -7
- package/src/core/io/data-tracking.ts +5 -0
- package/src/index.ts +2 -1
- package/src/plugins/tangram-grid/GridApp.tsx +522 -0
- package/src/plugins/tangram-grid/index.ts +154 -0
- package/tangram-construct.min.js +8 -8
- package/tangram-prep.min.js +6 -6
package/dist/index.cjs
CHANGED
|
@@ -2657,7 +2657,9 @@ function useClickController(controller, layout, pieces, clickMode, draggingId, s
|
|
|
2657
2657
|
|
|
2658
2658
|
class InteractionTracker {
|
|
2659
2659
|
constructor(controller, callbacks, trialParams) {
|
|
2660
|
-
this.
|
|
2660
|
+
this.completionTimes = [];
|
|
2661
|
+
// Sector centers (set by GameBoard after layout computation)
|
|
2662
|
+
this.sectorCenters = {};
|
|
2661
2663
|
// Interaction state
|
|
2662
2664
|
this.interactionIndex = 0;
|
|
2663
2665
|
this.currentPickup = null;
|
|
@@ -2667,14 +2669,13 @@ class InteractionTracker {
|
|
|
2667
2669
|
this.mouseTracking = [];
|
|
2668
2670
|
// Interaction history (for TrialEndData)
|
|
2669
2671
|
this.interactions = [];
|
|
2670
|
-
// Construction-specific tracking
|
|
2671
|
-
this.completionTimes = [];
|
|
2672
2672
|
// Prep-specific tracking
|
|
2673
2673
|
this.createdMacros = [];
|
|
2674
2674
|
this.controller = controller;
|
|
2675
2675
|
this.callbacks = callbacks;
|
|
2676
2676
|
this.trialParams = trialParams;
|
|
2677
2677
|
this.trialStartTime = Date.now();
|
|
2678
|
+
this.gridStep = CONFIG.layout.grid.stepPx;
|
|
2678
2679
|
this.controller.setTrackingCallbacks({
|
|
2679
2680
|
onSectorCompleted: (sectorId) => this.recordSectorCompletion(sectorId)
|
|
2680
2681
|
});
|
|
@@ -2804,7 +2805,13 @@ class InteractionTracker {
|
|
|
2804
2805
|
});
|
|
2805
2806
|
}
|
|
2806
2807
|
/**
|
|
2807
|
-
*
|
|
2808
|
+
* Set sector centers (for anchor alignment)
|
|
2809
|
+
*/
|
|
2810
|
+
setSectorCenters(centers) {
|
|
2811
|
+
this.sectorCenters = centers;
|
|
2812
|
+
}
|
|
2813
|
+
/**
|
|
2814
|
+
* Record a sector completion event
|
|
2808
2815
|
*/
|
|
2809
2816
|
recordSectorCompletion(sectorId) {
|
|
2810
2817
|
this.completionTimes.push({
|
|
@@ -2981,7 +2988,8 @@ class InteractionTracker {
|
|
|
2981
2988
|
sectorId: sector.id,
|
|
2982
2989
|
completed,
|
|
2983
2990
|
pieceCount: pieces.length,
|
|
2984
|
-
pieces
|
|
2991
|
+
pieces,
|
|
2992
|
+
center: this.sectorCenters[sector.id] ? this.toAnchorPoint(this.sectorCenters[sector.id]) : void 0
|
|
2985
2993
|
};
|
|
2986
2994
|
if (sectorState?.completedAt !== void 0) {
|
|
2987
2995
|
snapshot.completedAt = sectorState.completedAt;
|
|
@@ -3209,6 +3217,16 @@ function GameBoard(props) {
|
|
|
3209
3217
|
viewBox: { w: logicalBox.LOGICAL_W, h: logicalBox.LOGICAL_H }
|
|
3210
3218
|
};
|
|
3211
3219
|
}, [sectors, layoutMode, target, maxQuickstashSlots, primitives.length]);
|
|
3220
|
+
React.useEffect(() => {
|
|
3221
|
+
if (tracker && layout) {
|
|
3222
|
+
const centers = {};
|
|
3223
|
+
layout.sectors.forEach((s) => {
|
|
3224
|
+
const rect = rectForBand(layout, s, "silhouette", 1);
|
|
3225
|
+
centers[s.id] = { x: rect.cx, y: rect.cy };
|
|
3226
|
+
});
|
|
3227
|
+
tracker.setSectorCenters(centers);
|
|
3228
|
+
}
|
|
3229
|
+
}, [tracker, layout]);
|
|
3212
3230
|
const [, force] = React.useReducer((x) => x + 1, 0);
|
|
3213
3231
|
React.useEffect(() => {
|
|
3214
3232
|
if (onControllerReady) {
|
|
@@ -3766,7 +3784,7 @@ function startConstructionTrial(display_element, params, _jsPsych) {
|
|
|
3766
3784
|
return { root, display_element, jsPsych: _jsPsych };
|
|
3767
3785
|
}
|
|
3768
3786
|
|
|
3769
|
-
const info$
|
|
3787
|
+
const info$4 = {
|
|
3770
3788
|
name: "tangram-construct",
|
|
3771
3789
|
version: "1.0.0",
|
|
3772
3790
|
parameters: {
|
|
@@ -3898,7 +3916,7 @@ class TangramConstructPlugin {
|
|
|
3898
3916
|
this.jsPsych = jsPsych;
|
|
3899
3917
|
}
|
|
3900
3918
|
static {
|
|
3901
|
-
this.info = info$
|
|
3919
|
+
this.info = info$4;
|
|
3902
3920
|
}
|
|
3903
3921
|
/**
|
|
3904
3922
|
* Launches the trial by invoking startConstructionTrial
|
|
@@ -4049,7 +4067,7 @@ function startPrepTrial(display_element, params, jsPsych) {
|
|
|
4049
4067
|
return { root, display_element, jsPsych };
|
|
4050
4068
|
}
|
|
4051
4069
|
|
|
4052
|
-
const info$
|
|
4070
|
+
const info$3 = {
|
|
4053
4071
|
name: "tangram-prep",
|
|
4054
4072
|
version: "1.0.0",
|
|
4055
4073
|
parameters: {
|
|
@@ -4137,7 +4155,7 @@ class TangramPrepPlugin {
|
|
|
4137
4155
|
this.jsPsych = jsPsych;
|
|
4138
4156
|
}
|
|
4139
4157
|
static {
|
|
4140
|
-
this.info = info$
|
|
4158
|
+
this.info = info$3;
|
|
4141
4159
|
}
|
|
4142
4160
|
/**
|
|
4143
4161
|
* Launches the trial by invoking startPrepTrial
|
|
@@ -4433,7 +4451,7 @@ function NBackView({ params }) {
|
|
|
4433
4451
|
)));
|
|
4434
4452
|
}
|
|
4435
4453
|
|
|
4436
|
-
const info$
|
|
4454
|
+
const info$2 = {
|
|
4437
4455
|
name: "tangram-nback",
|
|
4438
4456
|
version: "1.0.0",
|
|
4439
4457
|
parameters: {
|
|
@@ -4526,7 +4544,7 @@ class TangramNBackPlugin {
|
|
|
4526
4544
|
this.jsPsych = jsPsych;
|
|
4527
4545
|
}
|
|
4528
4546
|
static {
|
|
4529
|
-
this.info = info$
|
|
4547
|
+
this.info = info$2;
|
|
4530
4548
|
}
|
|
4531
4549
|
/**
|
|
4532
4550
|
* Launches the trial by invoking startNBackTrial
|
|
@@ -4809,7 +4827,7 @@ function TangramOption({
|
|
|
4809
4827
|
));
|
|
4810
4828
|
}
|
|
4811
4829
|
|
|
4812
|
-
const info = {
|
|
4830
|
+
const info$1 = {
|
|
4813
4831
|
name: "tangram-afc",
|
|
4814
4832
|
version: "1.0.0",
|
|
4815
4833
|
parameters: {
|
|
@@ -4887,7 +4905,7 @@ class TangramAFCPlugin {
|
|
|
4887
4905
|
this.jsPsych = jsPsych;
|
|
4888
4906
|
}
|
|
4889
4907
|
static {
|
|
4890
|
-
this.info = info;
|
|
4908
|
+
this.info = info$1;
|
|
4891
4909
|
}
|
|
4892
4910
|
/**
|
|
4893
4911
|
* Launches the trial by invoking startAFCTrial
|
|
@@ -4921,8 +4939,525 @@ class TangramAFCPlugin {
|
|
|
4921
4939
|
}
|
|
4922
4940
|
}
|
|
4923
4941
|
|
|
4942
|
+
function startGridTrial(display_element, params, _jsPsych) {
|
|
4943
|
+
const root = client.createRoot(display_element);
|
|
4944
|
+
root.render(React.createElement(GridView, { params }));
|
|
4945
|
+
return { root, display_element, jsPsych: _jsPsych };
|
|
4946
|
+
}
|
|
4947
|
+
function computeBounds(polys) {
|
|
4948
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
4949
|
+
for (const poly of polys) {
|
|
4950
|
+
for (const p of poly) {
|
|
4951
|
+
minX = Math.min(minX, p.x);
|
|
4952
|
+
minY = Math.min(minY, p.y);
|
|
4953
|
+
maxX = Math.max(maxX, p.x);
|
|
4954
|
+
maxY = Math.max(maxY, p.y);
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4957
|
+
return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
|
|
4958
|
+
}
|
|
4959
|
+
function GridView({ params }) {
|
|
4960
|
+
const {
|
|
4961
|
+
tangrams,
|
|
4962
|
+
n_rows,
|
|
4963
|
+
n_cols,
|
|
4964
|
+
prompt_text,
|
|
4965
|
+
button_text,
|
|
4966
|
+
show_tangram_decomposition = false,
|
|
4967
|
+
usePrimitiveColors = false,
|
|
4968
|
+
primitiveColorIndices = [0, 1, 2, 3, 4],
|
|
4969
|
+
onTrialEnd
|
|
4970
|
+
} = params;
|
|
4971
|
+
const trialStartTime = React.useRef(Date.now());
|
|
4972
|
+
const [response, setResponse] = React.useState("");
|
|
4973
|
+
const [cellSize, setCellSize] = React.useState(100);
|
|
4974
|
+
const controlsRef = React.useRef(null);
|
|
4975
|
+
const GRID_GAP = 6;
|
|
4976
|
+
const CONTAINER_PADDING = 8;
|
|
4977
|
+
const CELL_MARGIN = 0.05;
|
|
4978
|
+
const PROGRESS_BAR_HEIGHT = 58;
|
|
4979
|
+
const CELL_BORDER = CONFIG.size.stroke.bandPx;
|
|
4980
|
+
const SAFETY_BUFFER = 8;
|
|
4981
|
+
React.useEffect(() => {
|
|
4982
|
+
const styleId = "tangram-grid-jspsych-override";
|
|
4983
|
+
if (!document.getElementById(styleId)) {
|
|
4984
|
+
const style = document.createElement("style");
|
|
4985
|
+
style.id = styleId;
|
|
4986
|
+
style.textContent = `
|
|
4987
|
+
.jspsych-content {
|
|
4988
|
+
max-width: 100% !important;
|
|
4989
|
+
width: 100% !important;
|
|
4990
|
+
}
|
|
4991
|
+
`;
|
|
4992
|
+
document.head.appendChild(style);
|
|
4993
|
+
}
|
|
4994
|
+
return () => {
|
|
4995
|
+
const style = document.getElementById(styleId);
|
|
4996
|
+
if (style) style.remove();
|
|
4997
|
+
};
|
|
4998
|
+
}, []);
|
|
4999
|
+
const CANON = /* @__PURE__ */ new Set([
|
|
5000
|
+
"square",
|
|
5001
|
+
"smalltriangle",
|
|
5002
|
+
"parallelogram",
|
|
5003
|
+
"medtriangle",
|
|
5004
|
+
"largetriangle"
|
|
5005
|
+
]);
|
|
5006
|
+
const processedTangrams = React.useMemo(() => {
|
|
5007
|
+
return tangrams.map((tangramSpec) => {
|
|
5008
|
+
const filteredTans = tangramSpec.solutionTans.filter((tan) => {
|
|
5009
|
+
const tanName = tan.name ?? tan.kind;
|
|
5010
|
+
return CANON.has(tanName);
|
|
5011
|
+
});
|
|
5012
|
+
const mask = filteredTans.map((tan) => {
|
|
5013
|
+
return tan.vertices.map(([x, y]) => ({
|
|
5014
|
+
x: x ?? 0,
|
|
5015
|
+
y: -(y ?? 0)
|
|
5016
|
+
}));
|
|
5017
|
+
});
|
|
5018
|
+
const primitiveDecomposition = filteredTans.map((tan) => ({
|
|
5019
|
+
kind: tan.name ?? tan.kind,
|
|
5020
|
+
polygon: tan.vertices.map(([x, y]) => ({
|
|
5021
|
+
x: x ?? 0,
|
|
5022
|
+
y: -(y ?? 0)
|
|
5023
|
+
}))
|
|
5024
|
+
}));
|
|
5025
|
+
return {
|
|
5026
|
+
tangramId: tangramSpec.tangramID,
|
|
5027
|
+
mask,
|
|
5028
|
+
primitiveDecomposition
|
|
5029
|
+
};
|
|
5030
|
+
});
|
|
5031
|
+
}, [tangrams]);
|
|
5032
|
+
const maxTangramExtent = React.useMemo(() => {
|
|
5033
|
+
let maxExtent = 0;
|
|
5034
|
+
for (const t of processedTangrams) {
|
|
5035
|
+
const bounds = computeBounds(t.mask);
|
|
5036
|
+
maxExtent = Math.max(maxExtent, bounds.width, bounds.height);
|
|
5037
|
+
}
|
|
5038
|
+
return maxExtent || 1;
|
|
5039
|
+
}, [processedTangrams]);
|
|
5040
|
+
React.useEffect(() => {
|
|
5041
|
+
const calculateCellSize = () => {
|
|
5042
|
+
const viewportWidth = document.documentElement.clientWidth;
|
|
5043
|
+
const viewportHeight = document.documentElement.clientHeight;
|
|
5044
|
+
const controlsHeight = controlsRef.current?.offsetHeight ?? 70;
|
|
5045
|
+
const availableWidth = viewportWidth - CONTAINER_PADDING * 2 - SAFETY_BUFFER;
|
|
5046
|
+
const availableHeight = viewportHeight - PROGRESS_BAR_HEIGHT - controlsHeight - CONTAINER_PADDING * 2 - SAFETY_BUFFER;
|
|
5047
|
+
const totalHorizontalGaps = GRID_GAP * (n_cols - 1);
|
|
5048
|
+
const totalVerticalGaps = GRID_GAP * (n_rows - 1);
|
|
5049
|
+
const totalHorizontalBorders = CELL_BORDER * 2 * n_cols;
|
|
5050
|
+
const totalVerticalBorders = CELL_BORDER * 2 * n_rows;
|
|
5051
|
+
const maxCellWidth = (availableWidth - totalHorizontalGaps - totalHorizontalBorders) / n_cols;
|
|
5052
|
+
const maxCellHeight = (availableHeight - totalVerticalGaps - totalVerticalBorders) / n_rows;
|
|
5053
|
+
const newCellSize = Math.floor(Math.min(maxCellWidth, maxCellHeight));
|
|
5054
|
+
setCellSize(Math.max(newCellSize, 50));
|
|
5055
|
+
};
|
|
5056
|
+
calculateCellSize();
|
|
5057
|
+
window.addEventListener("resize", calculateCellSize);
|
|
5058
|
+
return () => window.removeEventListener("resize", calculateCellSize);
|
|
5059
|
+
}, [n_rows, n_cols]);
|
|
5060
|
+
const tangramScale = React.useMemo(() => {
|
|
5061
|
+
const usableSize = cellSize * (1 - CELL_MARGIN * 2);
|
|
5062
|
+
return usableSize / maxTangramExtent;
|
|
5063
|
+
}, [cellSize, maxTangramExtent]);
|
|
5064
|
+
const kindToIndex = {
|
|
5065
|
+
square: 0,
|
|
5066
|
+
smalltriangle: 1,
|
|
5067
|
+
parallelogram: 2,
|
|
5068
|
+
medtriangle: 3,
|
|
5069
|
+
largetriangle: 4
|
|
5070
|
+
};
|
|
5071
|
+
const pathD = (poly) => {
|
|
5072
|
+
if (!poly || poly.length === 0) return "";
|
|
5073
|
+
const moves = poly.map(
|
|
5074
|
+
(p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`
|
|
5075
|
+
);
|
|
5076
|
+
return moves.join(" ") + " Z";
|
|
5077
|
+
};
|
|
5078
|
+
const handleSubmit = () => {
|
|
5079
|
+
const rt = Date.now() - trialStartTime.current;
|
|
5080
|
+
const trialData = {
|
|
5081
|
+
response,
|
|
5082
|
+
rt,
|
|
5083
|
+
n_rows,
|
|
5084
|
+
n_cols,
|
|
5085
|
+
tangram_ids: processedTangrams.map((t) => t.tangramId),
|
|
5086
|
+
show_tangram_decomposition,
|
|
5087
|
+
use_primitive_colors: usePrimitiveColors,
|
|
5088
|
+
primitive_color_indices: primitiveColorIndices
|
|
5089
|
+
};
|
|
5090
|
+
if (onTrialEnd) {
|
|
5091
|
+
onTrialEnd(trialData);
|
|
5092
|
+
}
|
|
5093
|
+
};
|
|
5094
|
+
const renderTangram = (tangramData, index) => {
|
|
5095
|
+
const { mask, primitiveDecomposition } = tangramData;
|
|
5096
|
+
if (show_tangram_decomposition) {
|
|
5097
|
+
const scaledPrimitives = primitiveDecomposition.map(
|
|
5098
|
+
(prim) => {
|
|
5099
|
+
const scaledPoly = prim.polygon.map((p) => ({
|
|
5100
|
+
x: p.x * tangramScale,
|
|
5101
|
+
y: p.y * tangramScale
|
|
5102
|
+
}));
|
|
5103
|
+
return { kind: prim.kind, polygon: scaledPoly };
|
|
5104
|
+
}
|
|
5105
|
+
);
|
|
5106
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
5107
|
+
for (const prim of scaledPrimitives) {
|
|
5108
|
+
for (const p of prim.polygon) {
|
|
5109
|
+
minX = Math.min(minX, p.x);
|
|
5110
|
+
minY = Math.min(minY, p.y);
|
|
5111
|
+
maxX = Math.max(maxX, p.x);
|
|
5112
|
+
maxY = Math.max(maxY, p.y);
|
|
5113
|
+
}
|
|
5114
|
+
}
|
|
5115
|
+
const width = maxX - minX;
|
|
5116
|
+
const height = maxY - minY;
|
|
5117
|
+
const tx = cellSize / 2 - (minX + width / 2);
|
|
5118
|
+
const ty = cellSize / 2 - (minY + height / 2);
|
|
5119
|
+
const translatedPrimitives = scaledPrimitives.map(
|
|
5120
|
+
(prim) => ({
|
|
5121
|
+
kind: prim.kind,
|
|
5122
|
+
polygon: prim.polygon.map((p) => ({
|
|
5123
|
+
x: p.x + tx,
|
|
5124
|
+
y: p.y + ty
|
|
5125
|
+
}))
|
|
5126
|
+
})
|
|
5127
|
+
);
|
|
5128
|
+
return /* @__PURE__ */ React.createElement(
|
|
5129
|
+
"svg",
|
|
5130
|
+
{
|
|
5131
|
+
key: index,
|
|
5132
|
+
width: cellSize,
|
|
5133
|
+
height: cellSize,
|
|
5134
|
+
viewBox: `0 0 ${cellSize} ${cellSize}`,
|
|
5135
|
+
style: {
|
|
5136
|
+
display: "block",
|
|
5137
|
+
background: CONFIG.color.bands.silhouette.fillEven,
|
|
5138
|
+
border: `${CONFIG.size.stroke.bandPx}px solid ${CONFIG.color.bands.silhouette.stroke}`,
|
|
5139
|
+
borderRadius: "8px"
|
|
5140
|
+
}
|
|
5141
|
+
},
|
|
5142
|
+
translatedPrimitives.map(
|
|
5143
|
+
(prim, i) => {
|
|
5144
|
+
let fillColor;
|
|
5145
|
+
if (usePrimitiveColors) {
|
|
5146
|
+
const primitiveIndex = kindToIndex[prim.kind];
|
|
5147
|
+
if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
|
|
5148
|
+
const colorIndex = primitiveColorIndices[primitiveIndex];
|
|
5149
|
+
const color = CONFIG.color.primitiveColors[colorIndex];
|
|
5150
|
+
fillColor = color || CONFIG.color.piece.validFill;
|
|
5151
|
+
} else {
|
|
5152
|
+
fillColor = CONFIG.color.piece.validFill;
|
|
5153
|
+
}
|
|
5154
|
+
} else {
|
|
5155
|
+
fillColor = CONFIG.color.piece.validFill;
|
|
5156
|
+
}
|
|
5157
|
+
return /* @__PURE__ */ React.createElement(
|
|
5158
|
+
"path",
|
|
5159
|
+
{
|
|
5160
|
+
key: `prim-${i}`,
|
|
5161
|
+
d: pathD(prim.polygon),
|
|
5162
|
+
fill: fillColor,
|
|
5163
|
+
opacity: CONFIG.opacity.piece.normal,
|
|
5164
|
+
stroke: CONFIG.color.tangramDecomposition.stroke,
|
|
5165
|
+
strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
|
|
5166
|
+
}
|
|
5167
|
+
);
|
|
5168
|
+
}
|
|
5169
|
+
)
|
|
5170
|
+
);
|
|
5171
|
+
} else {
|
|
5172
|
+
const scaledMask = mask.map(
|
|
5173
|
+
(poly) => poly.map((p) => ({
|
|
5174
|
+
x: p.x * tangramScale,
|
|
5175
|
+
y: p.y * tangramScale
|
|
5176
|
+
}))
|
|
5177
|
+
);
|
|
5178
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
5179
|
+
for (const poly of scaledMask) {
|
|
5180
|
+
for (const p of poly) {
|
|
5181
|
+
minX = Math.min(minX, p.x);
|
|
5182
|
+
minY = Math.min(minY, p.y);
|
|
5183
|
+
maxX = Math.max(maxX, p.x);
|
|
5184
|
+
maxY = Math.max(maxY, p.y);
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
const width = maxX - minX;
|
|
5188
|
+
const height = maxY - minY;
|
|
5189
|
+
const tx = cellSize / 2 - (minX + width / 2);
|
|
5190
|
+
const ty = cellSize / 2 - (minY + height / 2);
|
|
5191
|
+
const placedMask = scaledMask.map(
|
|
5192
|
+
(poly) => poly.map((p) => ({ x: p.x + tx, y: p.y + ty }))
|
|
5193
|
+
);
|
|
5194
|
+
return /* @__PURE__ */ React.createElement(
|
|
5195
|
+
"svg",
|
|
5196
|
+
{
|
|
5197
|
+
key: index,
|
|
5198
|
+
width: cellSize,
|
|
5199
|
+
height: cellSize,
|
|
5200
|
+
viewBox: `0 0 ${cellSize} ${cellSize}`,
|
|
5201
|
+
style: {
|
|
5202
|
+
display: "block",
|
|
5203
|
+
background: CONFIG.color.bands.silhouette.fillEven,
|
|
5204
|
+
border: `${CONFIG.size.stroke.bandPx}px solid ${CONFIG.color.bands.silhouette.stroke}`,
|
|
5205
|
+
borderRadius: "8px"
|
|
5206
|
+
}
|
|
5207
|
+
},
|
|
5208
|
+
placedMask.map((poly, i) => /* @__PURE__ */ React.createElement(
|
|
5209
|
+
"path",
|
|
5210
|
+
{
|
|
5211
|
+
key: `sil-${i}`,
|
|
5212
|
+
d: pathD(poly),
|
|
5213
|
+
fill: CONFIG.color.piece.validFill,
|
|
5214
|
+
opacity: CONFIG.opacity.piece.normal,
|
|
5215
|
+
stroke: "none"
|
|
5216
|
+
}
|
|
5217
|
+
))
|
|
5218
|
+
);
|
|
5219
|
+
}
|
|
5220
|
+
};
|
|
5221
|
+
const isSubmitDisabled = response.trim().length === 0;
|
|
5222
|
+
return /* @__PURE__ */ React.createElement(
|
|
5223
|
+
"div",
|
|
5224
|
+
{
|
|
5225
|
+
style: {
|
|
5226
|
+
display: "flex",
|
|
5227
|
+
flexDirection: "column",
|
|
5228
|
+
alignItems: "center",
|
|
5229
|
+
justifyContent: "space-between",
|
|
5230
|
+
background: CONFIG.color.background,
|
|
5231
|
+
width: "100%",
|
|
5232
|
+
height: `calc(100vh - ${PROGRESS_BAR_HEIGHT}px)`,
|
|
5233
|
+
overflow: "hidden",
|
|
5234
|
+
fontFamily: "Roboto, sans-serif",
|
|
5235
|
+
boxSizing: "border-box",
|
|
5236
|
+
padding: `${CONTAINER_PADDING}px`
|
|
5237
|
+
}
|
|
5238
|
+
},
|
|
5239
|
+
/* @__PURE__ */ React.createElement(
|
|
5240
|
+
"div",
|
|
5241
|
+
{
|
|
5242
|
+
style: {
|
|
5243
|
+
flex: "1 1 auto",
|
|
5244
|
+
display: "flex",
|
|
5245
|
+
alignItems: "center",
|
|
5246
|
+
justifyContent: "center",
|
|
5247
|
+
minHeight: 0
|
|
5248
|
+
}
|
|
5249
|
+
},
|
|
5250
|
+
/* @__PURE__ */ React.createElement(
|
|
5251
|
+
"div",
|
|
5252
|
+
{
|
|
5253
|
+
style: {
|
|
5254
|
+
display: "grid",
|
|
5255
|
+
gridTemplateColumns: `repeat(${n_cols}, ${cellSize}px)`,
|
|
5256
|
+
gridTemplateRows: `repeat(${n_rows}, ${cellSize}px)`,
|
|
5257
|
+
gap: `${GRID_GAP}px`
|
|
5258
|
+
}
|
|
5259
|
+
},
|
|
5260
|
+
processedTangrams.slice(0, n_rows * n_cols).map(
|
|
5261
|
+
(t, i) => renderTangram(t, i)
|
|
5262
|
+
)
|
|
5263
|
+
)
|
|
5264
|
+
),
|
|
5265
|
+
/* @__PURE__ */ React.createElement(
|
|
5266
|
+
"div",
|
|
5267
|
+
{
|
|
5268
|
+
ref: controlsRef,
|
|
5269
|
+
style: {
|
|
5270
|
+
flex: "0 0 auto",
|
|
5271
|
+
display: "flex",
|
|
5272
|
+
flexDirection: "column",
|
|
5273
|
+
alignItems: "center",
|
|
5274
|
+
gap: "4px",
|
|
5275
|
+
paddingTop: "4px"
|
|
5276
|
+
}
|
|
5277
|
+
},
|
|
5278
|
+
/* @__PURE__ */ React.createElement(
|
|
5279
|
+
"div",
|
|
5280
|
+
{
|
|
5281
|
+
style: {
|
|
5282
|
+
fontSize: "14px",
|
|
5283
|
+
textAlign: "center",
|
|
5284
|
+
maxWidth: "90vw"
|
|
5285
|
+
}
|
|
5286
|
+
},
|
|
5287
|
+
prompt_text
|
|
5288
|
+
),
|
|
5289
|
+
/* @__PURE__ */ React.createElement(
|
|
5290
|
+
"div",
|
|
5291
|
+
{
|
|
5292
|
+
style: {
|
|
5293
|
+
display: "flex",
|
|
5294
|
+
alignItems: "center",
|
|
5295
|
+
gap: "12px"
|
|
5296
|
+
}
|
|
5297
|
+
},
|
|
5298
|
+
/* @__PURE__ */ React.createElement(
|
|
5299
|
+
"input",
|
|
5300
|
+
{
|
|
5301
|
+
type: "text",
|
|
5302
|
+
value: response,
|
|
5303
|
+
onChange: (e) => setResponse(e.target.value),
|
|
5304
|
+
style: {
|
|
5305
|
+
width: "min(400px, 50vw)",
|
|
5306
|
+
padding: "6px 10px",
|
|
5307
|
+
fontSize: "14px",
|
|
5308
|
+
borderRadius: "6px",
|
|
5309
|
+
border: "2px solid #ccc",
|
|
5310
|
+
fontFamily: "inherit",
|
|
5311
|
+
boxSizing: "border-box"
|
|
5312
|
+
},
|
|
5313
|
+
placeholder: "Type your response here..."
|
|
5314
|
+
}
|
|
5315
|
+
),
|
|
5316
|
+
/* @__PURE__ */ React.createElement(
|
|
5317
|
+
"button",
|
|
5318
|
+
{
|
|
5319
|
+
className: "jspsych-btn",
|
|
5320
|
+
onClick: handleSubmit,
|
|
5321
|
+
disabled: isSubmitDisabled,
|
|
5322
|
+
style: {
|
|
5323
|
+
padding: "6px 16px",
|
|
5324
|
+
fontSize: "13px",
|
|
5325
|
+
cursor: isSubmitDisabled ? "not-allowed" : "pointer",
|
|
5326
|
+
opacity: isSubmitDisabled ? 0.5 : 1,
|
|
5327
|
+
flexShrink: 0
|
|
5328
|
+
}
|
|
5329
|
+
},
|
|
5330
|
+
button_text
|
|
5331
|
+
)
|
|
5332
|
+
)
|
|
5333
|
+
)
|
|
5334
|
+
);
|
|
5335
|
+
}
|
|
5336
|
+
|
|
5337
|
+
const info = {
|
|
5338
|
+
name: "tangram-grid",
|
|
5339
|
+
version: "1.0.0",
|
|
5340
|
+
parameters: {
|
|
5341
|
+
/** Array of tangram specifications to display in the grid */
|
|
5342
|
+
tangrams: {
|
|
5343
|
+
type: jspsych.ParameterType.COMPLEX,
|
|
5344
|
+
default: void 0,
|
|
5345
|
+
description: "Array of TangramSpec objects to display in the grid"
|
|
5346
|
+
},
|
|
5347
|
+
/** Number of rows in the grid */
|
|
5348
|
+
n_rows: {
|
|
5349
|
+
type: jspsych.ParameterType.INT,
|
|
5350
|
+
default: 1,
|
|
5351
|
+
description: "Number of rows in the tangram grid"
|
|
5352
|
+
},
|
|
5353
|
+
/** Number of columns in the grid */
|
|
5354
|
+
n_cols: {
|
|
5355
|
+
type: jspsych.ParameterType.INT,
|
|
5356
|
+
default: 1,
|
|
5357
|
+
description: "Number of columns in the tangram grid"
|
|
5358
|
+
},
|
|
5359
|
+
/** Prompt text displayed above the text input */
|
|
5360
|
+
prompt_text: {
|
|
5361
|
+
type: jspsych.ParameterType.STRING,
|
|
5362
|
+
default: "",
|
|
5363
|
+
description: "Text displayed above the text input field"
|
|
5364
|
+
},
|
|
5365
|
+
/** Label for the submit button */
|
|
5366
|
+
button_text: {
|
|
5367
|
+
type: jspsych.ParameterType.STRING,
|
|
5368
|
+
default: "Submit",
|
|
5369
|
+
description: "Text displayed on the submit button"
|
|
5370
|
+
},
|
|
5371
|
+
/** Whether to show tangrams decomposed into primitives */
|
|
5372
|
+
show_tangram_decomposition: {
|
|
5373
|
+
type: jspsych.ParameterType.BOOL,
|
|
5374
|
+
default: false,
|
|
5375
|
+
description: "Whether to show tangrams decomposed into individual primitives"
|
|
5376
|
+
},
|
|
5377
|
+
/** Whether to use distinct colors for each primitive type */
|
|
5378
|
+
use_primitive_colors: {
|
|
5379
|
+
type: jspsych.ParameterType.BOOL,
|
|
5380
|
+
default: false,
|
|
5381
|
+
description: "Whether each primitive shape type should have its own distinct color"
|
|
5382
|
+
},
|
|
5383
|
+
/** Indices mapping primitives to colors */
|
|
5384
|
+
primitive_color_indices: {
|
|
5385
|
+
type: jspsych.ParameterType.OBJECT,
|
|
5386
|
+
default: [0, 1, 2, 3, 4],
|
|
5387
|
+
description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
|
|
5388
|
+
},
|
|
5389
|
+
/** Callback fired when trial ends */
|
|
5390
|
+
onTrialEnd: {
|
|
5391
|
+
type: jspsych.ParameterType.FUNCTION,
|
|
5392
|
+
default: void 0,
|
|
5393
|
+
description: "Callback when trial completes with full data"
|
|
5394
|
+
}
|
|
5395
|
+
},
|
|
5396
|
+
data: {
|
|
5397
|
+
/** The text response entered by the participant */
|
|
5398
|
+
response: {
|
|
5399
|
+
type: jspsych.ParameterType.STRING,
|
|
5400
|
+
description: "The text response entered by the participant"
|
|
5401
|
+
},
|
|
5402
|
+
/** Reaction time from trial start to submit button click */
|
|
5403
|
+
rt: {
|
|
5404
|
+
type: jspsych.ParameterType.INT,
|
|
5405
|
+
description: "Time in milliseconds from trial start to submit"
|
|
5406
|
+
}
|
|
5407
|
+
},
|
|
5408
|
+
citations: ""
|
|
5409
|
+
};
|
|
5410
|
+
class TangramGridPlugin {
|
|
5411
|
+
constructor(jsPsych) {
|
|
5412
|
+
this.jsPsych = jsPsych;
|
|
5413
|
+
}
|
|
5414
|
+
static {
|
|
5415
|
+
this.info = info;
|
|
5416
|
+
}
|
|
5417
|
+
/**
|
|
5418
|
+
* Launches the trial by invoking startGridTrial with the display element,
|
|
5419
|
+
* parameters, and jsPsych instance.
|
|
5420
|
+
*
|
|
5421
|
+
* REQUIRES: display_element is a valid HTMLElement, trial contains valid
|
|
5422
|
+
* parameters
|
|
5423
|
+
* MODIFIES: display_element (renders React component)
|
|
5424
|
+
* EFFECTS: Starts the grid trial and handles cleanup on completion
|
|
5425
|
+
*/
|
|
5426
|
+
trial(display_element, trial) {
|
|
5427
|
+
const wrappedOnTrialEnd = (data) => {
|
|
5428
|
+
if (trial.onTrialEnd) {
|
|
5429
|
+
trial.onTrialEnd(data);
|
|
5430
|
+
}
|
|
5431
|
+
const reactContext = display_element.__reactContext;
|
|
5432
|
+
if (reactContext?.root) {
|
|
5433
|
+
reactContext.root.unmount();
|
|
5434
|
+
}
|
|
5435
|
+
display_element.innerHTML = "";
|
|
5436
|
+
this.jsPsych.finishTrial(data);
|
|
5437
|
+
};
|
|
5438
|
+
const params = {
|
|
5439
|
+
tangrams: trial.tangrams,
|
|
5440
|
+
n_rows: trial.n_rows,
|
|
5441
|
+
n_cols: trial.n_cols,
|
|
5442
|
+
prompt_text: trial.prompt_text,
|
|
5443
|
+
button_text: trial.button_text,
|
|
5444
|
+
show_tangram_decomposition: trial.show_tangram_decomposition,
|
|
5445
|
+
usePrimitiveColors: trial.use_primitive_colors,
|
|
5446
|
+
primitiveColorIndices: trial.primitive_color_indices,
|
|
5447
|
+
onTrialEnd: wrappedOnTrialEnd
|
|
5448
|
+
};
|
|
5449
|
+
const { root, display_element: element, jsPsych } = startGridTrial(
|
|
5450
|
+
display_element,
|
|
5451
|
+
params,
|
|
5452
|
+
this.jsPsych
|
|
5453
|
+
);
|
|
5454
|
+
element.__reactContext = { root, jsPsych };
|
|
5455
|
+
}
|
|
5456
|
+
}
|
|
5457
|
+
|
|
4924
5458
|
exports.TangramAFCPlugin = TangramAFCPlugin;
|
|
4925
5459
|
exports.TangramConstructPlugin = TangramConstructPlugin;
|
|
5460
|
+
exports.TangramGridPlugin = TangramGridPlugin;
|
|
4926
5461
|
exports.TangramNBackPlugin = TangramNBackPlugin;
|
|
4927
5462
|
exports.TangramPrepPlugin = TangramPrepPlugin;
|
|
4928
5463
|
//# sourceMappingURL=index.cjs.map
|