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
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var jspsych = require('jspsych');
|
|
4
|
+
var React = require('react');
|
|
5
|
+
var client = require('react-dom/client');
|
|
6
|
+
|
|
7
|
+
const CONFIG = {
|
|
8
|
+
color: {
|
|
9
|
+
background: "#fff7e0ff",
|
|
10
|
+
bands: {
|
|
11
|
+
silhouette: { fillEven: "#ffffff", stroke: "#b1b1b1" }},
|
|
12
|
+
// validFill used here for placed composites
|
|
13
|
+
piece: { validFill: "#8e7cc3"},
|
|
14
|
+
tangramDecomposition: { stroke: "#fef2cc" },
|
|
15
|
+
primitiveColors: [
|
|
16
|
+
// from seaborn "colorblind" palette, 6 colors, with red omitted
|
|
17
|
+
"#0173b2",
|
|
18
|
+
"#de8f05",
|
|
19
|
+
"#029e73",
|
|
20
|
+
"#cc78bc",
|
|
21
|
+
"#ca9161"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
opacity: {
|
|
25
|
+
piece: { normal: 1 }
|
|
26
|
+
},
|
|
27
|
+
size: {
|
|
28
|
+
stroke: { bandPx: 5, tangramDecompositionPx: 1 }}};
|
|
29
|
+
|
|
30
|
+
function startGridTrial(display_element, params, _jsPsych) {
|
|
31
|
+
const root = client.createRoot(display_element);
|
|
32
|
+
root.render(React.createElement(GridView, { params }));
|
|
33
|
+
return { root, display_element, jsPsych: _jsPsych };
|
|
34
|
+
}
|
|
35
|
+
function computeBounds(polys) {
|
|
36
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
37
|
+
for (const poly of polys) {
|
|
38
|
+
for (const p of poly) {
|
|
39
|
+
minX = Math.min(minX, p.x);
|
|
40
|
+
minY = Math.min(minY, p.y);
|
|
41
|
+
maxX = Math.max(maxX, p.x);
|
|
42
|
+
maxY = Math.max(maxY, p.y);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
|
|
46
|
+
}
|
|
47
|
+
function GridView({ params }) {
|
|
48
|
+
const {
|
|
49
|
+
tangrams,
|
|
50
|
+
n_rows,
|
|
51
|
+
n_cols,
|
|
52
|
+
prompt_text,
|
|
53
|
+
button_text,
|
|
54
|
+
show_tangram_decomposition = false,
|
|
55
|
+
usePrimitiveColors = false,
|
|
56
|
+
primitiveColorIndices = [0, 1, 2, 3, 4],
|
|
57
|
+
onTrialEnd
|
|
58
|
+
} = params;
|
|
59
|
+
const trialStartTime = React.useRef(Date.now());
|
|
60
|
+
const [response, setResponse] = React.useState("");
|
|
61
|
+
const [cellSize, setCellSize] = React.useState(100);
|
|
62
|
+
const controlsRef = React.useRef(null);
|
|
63
|
+
const GRID_GAP = 6;
|
|
64
|
+
const CONTAINER_PADDING = 8;
|
|
65
|
+
const CELL_MARGIN = 0.05;
|
|
66
|
+
const PROGRESS_BAR_HEIGHT = 58;
|
|
67
|
+
const CELL_BORDER = CONFIG.size.stroke.bandPx;
|
|
68
|
+
const SAFETY_BUFFER = 8;
|
|
69
|
+
React.useEffect(() => {
|
|
70
|
+
const styleId = "tangram-grid-jspsych-override";
|
|
71
|
+
if (!document.getElementById(styleId)) {
|
|
72
|
+
const style = document.createElement("style");
|
|
73
|
+
style.id = styleId;
|
|
74
|
+
style.textContent = `
|
|
75
|
+
.jspsych-content {
|
|
76
|
+
max-width: 100% !important;
|
|
77
|
+
width: 100% !important;
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
document.head.appendChild(style);
|
|
81
|
+
}
|
|
82
|
+
return () => {
|
|
83
|
+
const style = document.getElementById(styleId);
|
|
84
|
+
if (style) style.remove();
|
|
85
|
+
};
|
|
86
|
+
}, []);
|
|
87
|
+
const CANON = /* @__PURE__ */ new Set([
|
|
88
|
+
"square",
|
|
89
|
+
"smalltriangle",
|
|
90
|
+
"parallelogram",
|
|
91
|
+
"medtriangle",
|
|
92
|
+
"largetriangle"
|
|
93
|
+
]);
|
|
94
|
+
const processedTangrams = React.useMemo(() => {
|
|
95
|
+
return tangrams.map((tangramSpec) => {
|
|
96
|
+
const filteredTans = tangramSpec.solutionTans.filter((tan) => {
|
|
97
|
+
const tanName = tan.name ?? tan.kind;
|
|
98
|
+
return CANON.has(tanName);
|
|
99
|
+
});
|
|
100
|
+
const mask = filteredTans.map((tan) => {
|
|
101
|
+
return tan.vertices.map(([x, y]) => ({
|
|
102
|
+
x: x ?? 0,
|
|
103
|
+
y: -(y ?? 0)
|
|
104
|
+
}));
|
|
105
|
+
});
|
|
106
|
+
const primitiveDecomposition = filteredTans.map((tan) => ({
|
|
107
|
+
kind: tan.name ?? tan.kind,
|
|
108
|
+
polygon: tan.vertices.map(([x, y]) => ({
|
|
109
|
+
x: x ?? 0,
|
|
110
|
+
y: -(y ?? 0)
|
|
111
|
+
}))
|
|
112
|
+
}));
|
|
113
|
+
return {
|
|
114
|
+
tangramId: tangramSpec.tangramID,
|
|
115
|
+
mask,
|
|
116
|
+
primitiveDecomposition
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
}, [tangrams]);
|
|
120
|
+
const maxTangramExtent = React.useMemo(() => {
|
|
121
|
+
let maxExtent = 0;
|
|
122
|
+
for (const t of processedTangrams) {
|
|
123
|
+
const bounds = computeBounds(t.mask);
|
|
124
|
+
maxExtent = Math.max(maxExtent, bounds.width, bounds.height);
|
|
125
|
+
}
|
|
126
|
+
return maxExtent || 1;
|
|
127
|
+
}, [processedTangrams]);
|
|
128
|
+
React.useEffect(() => {
|
|
129
|
+
const calculateCellSize = () => {
|
|
130
|
+
const viewportWidth = document.documentElement.clientWidth;
|
|
131
|
+
const viewportHeight = document.documentElement.clientHeight;
|
|
132
|
+
const controlsHeight = controlsRef.current?.offsetHeight ?? 70;
|
|
133
|
+
const availableWidth = viewportWidth - CONTAINER_PADDING * 2 - SAFETY_BUFFER;
|
|
134
|
+
const availableHeight = viewportHeight - PROGRESS_BAR_HEIGHT - controlsHeight - CONTAINER_PADDING * 2 - SAFETY_BUFFER;
|
|
135
|
+
const totalHorizontalGaps = GRID_GAP * (n_cols - 1);
|
|
136
|
+
const totalVerticalGaps = GRID_GAP * (n_rows - 1);
|
|
137
|
+
const totalHorizontalBorders = CELL_BORDER * 2 * n_cols;
|
|
138
|
+
const totalVerticalBorders = CELL_BORDER * 2 * n_rows;
|
|
139
|
+
const maxCellWidth = (availableWidth - totalHorizontalGaps - totalHorizontalBorders) / n_cols;
|
|
140
|
+
const maxCellHeight = (availableHeight - totalVerticalGaps - totalVerticalBorders) / n_rows;
|
|
141
|
+
const newCellSize = Math.floor(Math.min(maxCellWidth, maxCellHeight));
|
|
142
|
+
setCellSize(Math.max(newCellSize, 50));
|
|
143
|
+
};
|
|
144
|
+
calculateCellSize();
|
|
145
|
+
window.addEventListener("resize", calculateCellSize);
|
|
146
|
+
return () => window.removeEventListener("resize", calculateCellSize);
|
|
147
|
+
}, [n_rows, n_cols]);
|
|
148
|
+
const tangramScale = React.useMemo(() => {
|
|
149
|
+
const usableSize = cellSize * (1 - CELL_MARGIN * 2);
|
|
150
|
+
return usableSize / maxTangramExtent;
|
|
151
|
+
}, [cellSize, maxTangramExtent]);
|
|
152
|
+
const kindToIndex = {
|
|
153
|
+
square: 0,
|
|
154
|
+
smalltriangle: 1,
|
|
155
|
+
parallelogram: 2,
|
|
156
|
+
medtriangle: 3,
|
|
157
|
+
largetriangle: 4
|
|
158
|
+
};
|
|
159
|
+
const pathD = (poly) => {
|
|
160
|
+
if (!poly || poly.length === 0) return "";
|
|
161
|
+
const moves = poly.map(
|
|
162
|
+
(p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`
|
|
163
|
+
);
|
|
164
|
+
return moves.join(" ") + " Z";
|
|
165
|
+
};
|
|
166
|
+
const handleSubmit = () => {
|
|
167
|
+
const rt = Date.now() - trialStartTime.current;
|
|
168
|
+
const trialData = {
|
|
169
|
+
response,
|
|
170
|
+
rt,
|
|
171
|
+
n_rows,
|
|
172
|
+
n_cols,
|
|
173
|
+
tangram_ids: processedTangrams.map((t) => t.tangramId),
|
|
174
|
+
show_tangram_decomposition,
|
|
175
|
+
use_primitive_colors: usePrimitiveColors,
|
|
176
|
+
primitive_color_indices: primitiveColorIndices
|
|
177
|
+
};
|
|
178
|
+
if (onTrialEnd) {
|
|
179
|
+
onTrialEnd(trialData);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const renderTangram = (tangramData, index) => {
|
|
183
|
+
const { mask, primitiveDecomposition } = tangramData;
|
|
184
|
+
if (show_tangram_decomposition) {
|
|
185
|
+
const scaledPrimitives = primitiveDecomposition.map(
|
|
186
|
+
(prim) => {
|
|
187
|
+
const scaledPoly = prim.polygon.map((p) => ({
|
|
188
|
+
x: p.x * tangramScale,
|
|
189
|
+
y: p.y * tangramScale
|
|
190
|
+
}));
|
|
191
|
+
return { kind: prim.kind, polygon: scaledPoly };
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
195
|
+
for (const prim of scaledPrimitives) {
|
|
196
|
+
for (const p of prim.polygon) {
|
|
197
|
+
minX = Math.min(minX, p.x);
|
|
198
|
+
minY = Math.min(minY, p.y);
|
|
199
|
+
maxX = Math.max(maxX, p.x);
|
|
200
|
+
maxY = Math.max(maxY, p.y);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const width = maxX - minX;
|
|
204
|
+
const height = maxY - minY;
|
|
205
|
+
const tx = cellSize / 2 - (minX + width / 2);
|
|
206
|
+
const ty = cellSize / 2 - (minY + height / 2);
|
|
207
|
+
const translatedPrimitives = scaledPrimitives.map(
|
|
208
|
+
(prim) => ({
|
|
209
|
+
kind: prim.kind,
|
|
210
|
+
polygon: prim.polygon.map((p) => ({
|
|
211
|
+
x: p.x + tx,
|
|
212
|
+
y: p.y + ty
|
|
213
|
+
}))
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
return /* @__PURE__ */ React.createElement(
|
|
217
|
+
"svg",
|
|
218
|
+
{
|
|
219
|
+
key: index,
|
|
220
|
+
width: cellSize,
|
|
221
|
+
height: cellSize,
|
|
222
|
+
viewBox: `0 0 ${cellSize} ${cellSize}`,
|
|
223
|
+
style: {
|
|
224
|
+
display: "block",
|
|
225
|
+
background: CONFIG.color.bands.silhouette.fillEven,
|
|
226
|
+
border: `${CONFIG.size.stroke.bandPx}px solid ${CONFIG.color.bands.silhouette.stroke}`,
|
|
227
|
+
borderRadius: "8px"
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
translatedPrimitives.map(
|
|
231
|
+
(prim, i) => {
|
|
232
|
+
let fillColor;
|
|
233
|
+
if (usePrimitiveColors) {
|
|
234
|
+
const primitiveIndex = kindToIndex[prim.kind];
|
|
235
|
+
if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
|
|
236
|
+
const colorIndex = primitiveColorIndices[primitiveIndex];
|
|
237
|
+
const color = CONFIG.color.primitiveColors[colorIndex];
|
|
238
|
+
fillColor = color || CONFIG.color.piece.validFill;
|
|
239
|
+
} else {
|
|
240
|
+
fillColor = CONFIG.color.piece.validFill;
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
fillColor = CONFIG.color.piece.validFill;
|
|
244
|
+
}
|
|
245
|
+
return /* @__PURE__ */ React.createElement(
|
|
246
|
+
"path",
|
|
247
|
+
{
|
|
248
|
+
key: `prim-${i}`,
|
|
249
|
+
d: pathD(prim.polygon),
|
|
250
|
+
fill: fillColor,
|
|
251
|
+
opacity: CONFIG.opacity.piece.normal,
|
|
252
|
+
stroke: CONFIG.color.tangramDecomposition.stroke,
|
|
253
|
+
strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
);
|
|
259
|
+
} else {
|
|
260
|
+
const scaledMask = mask.map(
|
|
261
|
+
(poly) => poly.map((p) => ({
|
|
262
|
+
x: p.x * tangramScale,
|
|
263
|
+
y: p.y * tangramScale
|
|
264
|
+
}))
|
|
265
|
+
);
|
|
266
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
267
|
+
for (const poly of scaledMask) {
|
|
268
|
+
for (const p of poly) {
|
|
269
|
+
minX = Math.min(minX, p.x);
|
|
270
|
+
minY = Math.min(minY, p.y);
|
|
271
|
+
maxX = Math.max(maxX, p.x);
|
|
272
|
+
maxY = Math.max(maxY, p.y);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const width = maxX - minX;
|
|
276
|
+
const height = maxY - minY;
|
|
277
|
+
const tx = cellSize / 2 - (minX + width / 2);
|
|
278
|
+
const ty = cellSize / 2 - (minY + height / 2);
|
|
279
|
+
const placedMask = scaledMask.map(
|
|
280
|
+
(poly) => poly.map((p) => ({ x: p.x + tx, y: p.y + ty }))
|
|
281
|
+
);
|
|
282
|
+
return /* @__PURE__ */ React.createElement(
|
|
283
|
+
"svg",
|
|
284
|
+
{
|
|
285
|
+
key: index,
|
|
286
|
+
width: cellSize,
|
|
287
|
+
height: cellSize,
|
|
288
|
+
viewBox: `0 0 ${cellSize} ${cellSize}`,
|
|
289
|
+
style: {
|
|
290
|
+
display: "block",
|
|
291
|
+
background: CONFIG.color.bands.silhouette.fillEven,
|
|
292
|
+
border: `${CONFIG.size.stroke.bandPx}px solid ${CONFIG.color.bands.silhouette.stroke}`,
|
|
293
|
+
borderRadius: "8px"
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
placedMask.map((poly, i) => /* @__PURE__ */ React.createElement(
|
|
297
|
+
"path",
|
|
298
|
+
{
|
|
299
|
+
key: `sil-${i}`,
|
|
300
|
+
d: pathD(poly),
|
|
301
|
+
fill: CONFIG.color.piece.validFill,
|
|
302
|
+
opacity: CONFIG.opacity.piece.normal,
|
|
303
|
+
stroke: "none"
|
|
304
|
+
}
|
|
305
|
+
))
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const isSubmitDisabled = response.trim().length === 0;
|
|
310
|
+
return /* @__PURE__ */ React.createElement(
|
|
311
|
+
"div",
|
|
312
|
+
{
|
|
313
|
+
style: {
|
|
314
|
+
display: "flex",
|
|
315
|
+
flexDirection: "column",
|
|
316
|
+
alignItems: "center",
|
|
317
|
+
justifyContent: "space-between",
|
|
318
|
+
background: CONFIG.color.background,
|
|
319
|
+
width: "100%",
|
|
320
|
+
height: `calc(100vh - ${PROGRESS_BAR_HEIGHT}px)`,
|
|
321
|
+
overflow: "hidden",
|
|
322
|
+
fontFamily: "Roboto, sans-serif",
|
|
323
|
+
boxSizing: "border-box",
|
|
324
|
+
padding: `${CONTAINER_PADDING}px`
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
/* @__PURE__ */ React.createElement(
|
|
328
|
+
"div",
|
|
329
|
+
{
|
|
330
|
+
style: {
|
|
331
|
+
flex: "1 1 auto",
|
|
332
|
+
display: "flex",
|
|
333
|
+
alignItems: "center",
|
|
334
|
+
justifyContent: "center",
|
|
335
|
+
minHeight: 0
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
/* @__PURE__ */ React.createElement(
|
|
339
|
+
"div",
|
|
340
|
+
{
|
|
341
|
+
style: {
|
|
342
|
+
display: "grid",
|
|
343
|
+
gridTemplateColumns: `repeat(${n_cols}, ${cellSize}px)`,
|
|
344
|
+
gridTemplateRows: `repeat(${n_rows}, ${cellSize}px)`,
|
|
345
|
+
gap: `${GRID_GAP}px`
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
processedTangrams.slice(0, n_rows * n_cols).map(
|
|
349
|
+
(t, i) => renderTangram(t, i)
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
),
|
|
353
|
+
/* @__PURE__ */ React.createElement(
|
|
354
|
+
"div",
|
|
355
|
+
{
|
|
356
|
+
ref: controlsRef,
|
|
357
|
+
style: {
|
|
358
|
+
flex: "0 0 auto",
|
|
359
|
+
display: "flex",
|
|
360
|
+
flexDirection: "column",
|
|
361
|
+
alignItems: "center",
|
|
362
|
+
gap: "4px",
|
|
363
|
+
paddingTop: "4px"
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
/* @__PURE__ */ React.createElement(
|
|
367
|
+
"div",
|
|
368
|
+
{
|
|
369
|
+
style: {
|
|
370
|
+
fontSize: "14px",
|
|
371
|
+
textAlign: "center",
|
|
372
|
+
maxWidth: "90vw"
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
prompt_text
|
|
376
|
+
),
|
|
377
|
+
/* @__PURE__ */ React.createElement(
|
|
378
|
+
"div",
|
|
379
|
+
{
|
|
380
|
+
style: {
|
|
381
|
+
display: "flex",
|
|
382
|
+
alignItems: "center",
|
|
383
|
+
gap: "12px"
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
/* @__PURE__ */ React.createElement(
|
|
387
|
+
"input",
|
|
388
|
+
{
|
|
389
|
+
type: "text",
|
|
390
|
+
value: response,
|
|
391
|
+
onChange: (e) => setResponse(e.target.value),
|
|
392
|
+
style: {
|
|
393
|
+
width: "min(400px, 50vw)",
|
|
394
|
+
padding: "6px 10px",
|
|
395
|
+
fontSize: "14px",
|
|
396
|
+
borderRadius: "6px",
|
|
397
|
+
border: "2px solid #ccc",
|
|
398
|
+
fontFamily: "inherit",
|
|
399
|
+
boxSizing: "border-box"
|
|
400
|
+
},
|
|
401
|
+
placeholder: "Type your response here..."
|
|
402
|
+
}
|
|
403
|
+
),
|
|
404
|
+
/* @__PURE__ */ React.createElement(
|
|
405
|
+
"button",
|
|
406
|
+
{
|
|
407
|
+
className: "jspsych-btn",
|
|
408
|
+
onClick: handleSubmit,
|
|
409
|
+
disabled: isSubmitDisabled,
|
|
410
|
+
style: {
|
|
411
|
+
padding: "6px 16px",
|
|
412
|
+
fontSize: "13px",
|
|
413
|
+
cursor: isSubmitDisabled ? "not-allowed" : "pointer",
|
|
414
|
+
opacity: isSubmitDisabled ? 0.5 : 1,
|
|
415
|
+
flexShrink: 0
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
button_text
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const info = {
|
|
426
|
+
name: "tangram-grid",
|
|
427
|
+
version: "1.0.0",
|
|
428
|
+
parameters: {
|
|
429
|
+
/** Array of tangram specifications to display in the grid */
|
|
430
|
+
tangrams: {
|
|
431
|
+
type: jspsych.ParameterType.COMPLEX,
|
|
432
|
+
default: void 0,
|
|
433
|
+
description: "Array of TangramSpec objects to display in the grid"
|
|
434
|
+
},
|
|
435
|
+
/** Number of rows in the grid */
|
|
436
|
+
n_rows: {
|
|
437
|
+
type: jspsych.ParameterType.INT,
|
|
438
|
+
default: 1,
|
|
439
|
+
description: "Number of rows in the tangram grid"
|
|
440
|
+
},
|
|
441
|
+
/** Number of columns in the grid */
|
|
442
|
+
n_cols: {
|
|
443
|
+
type: jspsych.ParameterType.INT,
|
|
444
|
+
default: 1,
|
|
445
|
+
description: "Number of columns in the tangram grid"
|
|
446
|
+
},
|
|
447
|
+
/** Prompt text displayed above the text input */
|
|
448
|
+
prompt_text: {
|
|
449
|
+
type: jspsych.ParameterType.STRING,
|
|
450
|
+
default: "",
|
|
451
|
+
description: "Text displayed above the text input field"
|
|
452
|
+
},
|
|
453
|
+
/** Label for the submit button */
|
|
454
|
+
button_text: {
|
|
455
|
+
type: jspsych.ParameterType.STRING,
|
|
456
|
+
default: "Submit",
|
|
457
|
+
description: "Text displayed on the submit button"
|
|
458
|
+
},
|
|
459
|
+
/** Whether to show tangrams decomposed into primitives */
|
|
460
|
+
show_tangram_decomposition: {
|
|
461
|
+
type: jspsych.ParameterType.BOOL,
|
|
462
|
+
default: false,
|
|
463
|
+
description: "Whether to show tangrams decomposed into individual primitives"
|
|
464
|
+
},
|
|
465
|
+
/** Whether to use distinct colors for each primitive type */
|
|
466
|
+
use_primitive_colors: {
|
|
467
|
+
type: jspsych.ParameterType.BOOL,
|
|
468
|
+
default: false,
|
|
469
|
+
description: "Whether each primitive shape type should have its own distinct color"
|
|
470
|
+
},
|
|
471
|
+
/** Indices mapping primitives to colors */
|
|
472
|
+
primitive_color_indices: {
|
|
473
|
+
type: jspsych.ParameterType.OBJECT,
|
|
474
|
+
default: [0, 1, 2, 3, 4],
|
|
475
|
+
description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
|
|
476
|
+
},
|
|
477
|
+
/** Callback fired when trial ends */
|
|
478
|
+
onTrialEnd: {
|
|
479
|
+
type: jspsych.ParameterType.FUNCTION,
|
|
480
|
+
default: void 0,
|
|
481
|
+
description: "Callback when trial completes with full data"
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
data: {
|
|
485
|
+
/** The text response entered by the participant */
|
|
486
|
+
response: {
|
|
487
|
+
type: jspsych.ParameterType.STRING,
|
|
488
|
+
description: "The text response entered by the participant"
|
|
489
|
+
},
|
|
490
|
+
/** Reaction time from trial start to submit button click */
|
|
491
|
+
rt: {
|
|
492
|
+
type: jspsych.ParameterType.INT,
|
|
493
|
+
description: "Time in milliseconds from trial start to submit"
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
citations: ""
|
|
497
|
+
};
|
|
498
|
+
class TangramGridPlugin {
|
|
499
|
+
constructor(jsPsych) {
|
|
500
|
+
this.jsPsych = jsPsych;
|
|
501
|
+
}
|
|
502
|
+
static {
|
|
503
|
+
this.info = info;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Launches the trial by invoking startGridTrial with the display element,
|
|
507
|
+
* parameters, and jsPsych instance.
|
|
508
|
+
*
|
|
509
|
+
* REQUIRES: display_element is a valid HTMLElement, trial contains valid
|
|
510
|
+
* parameters
|
|
511
|
+
* MODIFIES: display_element (renders React component)
|
|
512
|
+
* EFFECTS: Starts the grid trial and handles cleanup on completion
|
|
513
|
+
*/
|
|
514
|
+
trial(display_element, trial) {
|
|
515
|
+
const wrappedOnTrialEnd = (data) => {
|
|
516
|
+
if (trial.onTrialEnd) {
|
|
517
|
+
trial.onTrialEnd(data);
|
|
518
|
+
}
|
|
519
|
+
const reactContext = display_element.__reactContext;
|
|
520
|
+
if (reactContext?.root) {
|
|
521
|
+
reactContext.root.unmount();
|
|
522
|
+
}
|
|
523
|
+
display_element.innerHTML = "";
|
|
524
|
+
this.jsPsych.finishTrial(data);
|
|
525
|
+
};
|
|
526
|
+
const params = {
|
|
527
|
+
tangrams: trial.tangrams,
|
|
528
|
+
n_rows: trial.n_rows,
|
|
529
|
+
n_cols: trial.n_cols,
|
|
530
|
+
prompt_text: trial.prompt_text,
|
|
531
|
+
button_text: trial.button_text,
|
|
532
|
+
show_tangram_decomposition: trial.show_tangram_decomposition,
|
|
533
|
+
usePrimitiveColors: trial.use_primitive_colors,
|
|
534
|
+
primitiveColorIndices: trial.primitive_color_indices,
|
|
535
|
+
onTrialEnd: wrappedOnTrialEnd
|
|
536
|
+
};
|
|
537
|
+
const { root, display_element: element, jsPsych } = startGridTrial(
|
|
538
|
+
display_element,
|
|
539
|
+
params,
|
|
540
|
+
this.jsPsych
|
|
541
|
+
);
|
|
542
|
+
element.__reactContext = { root, jsPsych };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
module.exports = TangramGridPlugin;
|
|
547
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../../src/core/config/config.ts","../../src/plugins/tangram-grid/GridApp.tsx","../../src/plugins/tangram-grid/index.ts"],"sourcesContent":["// src/core/config/config.ts\nexport type Config = {\n color: {\n background: string;\n bands: {\n silhouette: { fillEven: string; fillOdd: string; stroke: string };\n workspace: { fillEven: string; fillOdd: string; stroke: string };\n };\n completion: { fill: string; stroke: string };\n silhouetteMask: string;\n anchors: { invalid: string; valid: string };\n piece: { draggingFill: string; validFill: string; invalidFill: string; invalidStroke: string; selectedStroke: string; allGreenStroke: string; borderStroke: string };\n ui: { light: string; dark: string };\n blueprint: { fill: string; selectedStroke: string; badgeFill: string; labelFill: string };\n tangramDecomposition: { stroke: string };\n primitiveColors: string[];\n };\n opacity: {\n blueprint: number;\n silhouetteMask: number;\n anchors: { invalid: number; valid: number };\n piece: { invalid: number; dragging: number; locked: number; normal: number };\n };\n size: {\n stroke: { bandPx: number; pieceSelectedPx: number; allGreenStrokePx: number; pieceBorderPx: number; tangramDecompositionPx: number };\n anchorRadiusPx: { valid: number; invalid: number };\n badgeFontPx: number;\n centerBadge: { fractionOfOuterR: number; minPx: number; marginPx: number };\n invalidMarker: { sizePx: number; strokePx: number };\n };\n layout: {\n grid: { stepPx: number; unitPx: number };\n paddingPx: number;\n viewportScale: number;\n /** renamed from capacity → constraints */\n constraints: {\n workspaceDiamAnchors: number;\n quickstashDiamAnchors: number;\n primitiveDiamAnchors: number;\n };\n defaults: { maxQuickstashSlots: number };\n };\n game: {\n snapRadiusPx: number;\n showBorders: boolean;\n hideTouchingBorders: boolean;\n silhouettesBelowPieces: boolean;\n };\n};\n\nexport const CONFIG: Config = {\n color: {\n background: \"#fff7e0ff\",\n bands: {\n silhouette: { fillEven: \"#ffffff\", fillOdd: \"#ffffff\", stroke: \"#b1b1b1\" },\n workspace: { fillEven: \"#ffffff\", fillOdd: \"#ffffff\", stroke: \"#b1b1b1\" }\n },\n completion: { fill: \"#ccffcc\", stroke: \"#13da57\" },\n silhouetteMask: \"#374151\",\n anchors: { invalid: \"#7dd3fc\", valid: \"#475569\" },\n // validFill used here for placed composites\n piece: { draggingFill: \"#8e7cc3\", validFill: \"#8e7cc3\", invalidFill: \"#d55c00\", invalidStroke: \"#dc2626\", selectedStroke: \"#674ea7\", allGreenStroke: \"#86efac\", borderStroke: \"#674ea7\" },\n ui: { light: \"#60a5fa\", dark: \"#1d4ed8\" },\n blueprint: { fill: \"#374151\", selectedStroke: \"#111827\", badgeFill: \"#000000\", labelFill: \"#ffffff\" },\n tangramDecomposition: { stroke: \"#fef2cc\" },\n primitiveColors: [ // from seaborn \"colorblind\" palette, 6 colors, with red omitted\n '#0173b2',\n '#de8f05',\n '#029e73',\n '#cc78bc',\n '#ca9161'\n ]\n },\n opacity: {\n blueprint: 0.6,\n silhouetteMask: 0.25,\n //anchors: { valid: 0.80, invalid: 0.50 },\n anchors: { invalid: 0.0, valid: 0.0 },\n piece: { invalid: 1, dragging: 1, locked: 1, normal: 1 },\n },\n size: {\n stroke: { bandPx: 5, pieceSelectedPx: 5, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },\n anchorRadiusPx: { valid: 1.0, invalid: 1.0 },\n badgeFontPx: 16,\n centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 },\n invalidMarker: { sizePx: 10, strokePx: 4 }\n },\n layout: {\n grid: { stepPx: 20, unitPx: 40 },\n paddingPx: 1,\n viewportScale: 0.8,\n constraints: {\n workspaceDiamAnchors: 10, // num anchors req'd to be on diagonal\n quickstashDiamAnchors: 7, // num anchors req'd to be in single quickstash slot\n primitiveDiamAnchors: 5,\n },\n defaults: { maxQuickstashSlots: 1 }\n },\n game: {\n snapRadiusPx: 15,\n showBorders: false,\n hideTouchingBorders: true,\n silhouettesBelowPieces: true\n }\n};","/**\n * GridApp.tsx - React wrapper for tangram grid display with text input\n *\n * This component displays a grid of tangrams with a text input field\n * and submit button for collecting free-text responses.\n */\n\nimport React, { useRef, useState, useMemo, useEffect } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { JsPsych } from \"jspsych\";\nimport type { Poly, TanKind } from \"../../core/domain/types\";\nimport { CONFIG } from \"../../core/config/config\";\n\nexport interface StartGridTrialParams {\n tangrams: any[];\n n_rows: number;\n n_cols: number;\n prompt_text: string;\n button_text: string;\n show_tangram_decomposition?: boolean;\n usePrimitiveColors?: boolean;\n primitiveColorIndices?: number[];\n onTrialEnd?: (data: any) => void;\n}\n\n/**\n * Start a grid trial by rendering the GridView component\n *\n * REQUIRES: display_element is a valid HTMLElement\n * MODIFIES: display_element (renders React into it)\n * EFFECTS: Creates a React root and renders GridView with the given params\n */\nexport function startGridTrial(\n display_element: HTMLElement,\n params: StartGridTrialParams,\n _jsPsych: JsPsych\n) {\n const root = createRoot(display_element);\n root.render(React.createElement(GridView, { params }));\n return { root, display_element, jsPsych: _jsPsych };\n}\n\ninterface GridViewProps {\n params: StartGridTrialParams;\n}\n\n/**\n * Compute bounding box for an array of polygons\n *\n * REQUIRES: polys is an array of polygons with {x, y} points\n * MODIFIES: nothing\n * EFFECTS: Returns {minX, minY, maxX, maxY, width, height} of bounding box\n */\nfunction computeBounds(polys: Poly[]) {\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const poly of polys) {\n for (const p of poly) {\n minX = Math.min(minX, p.x);\n minY = Math.min(minY, p.y);\n maxX = Math.max(maxX, p.x);\n maxY = Math.max(maxY, p.y);\n }\n }\n return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };\n}\n\n/**\n * GridView - Main React component for the grid trial\n *\n * REQUIRES: params contains valid tangram specs and grid dimensions\n * MODIFIES: nothing\n * EFFECTS: Renders a grid of tangrams, text input, and submit button;\n * calls onTrialEnd with response data when submitted\n */\nfunction GridView({ params }: GridViewProps) {\n const {\n tangrams,\n n_rows,\n n_cols,\n prompt_text,\n button_text,\n show_tangram_decomposition = false,\n usePrimitiveColors = false,\n primitiveColorIndices = [0, 1, 2, 3, 4],\n onTrialEnd\n } = params;\n\n const trialStartTime = useRef<number>(Date.now());\n const [response, setResponse] = useState<string>(\"\");\n const [cellSize, setCellSize] = useState<number>(100);\n const controlsRef = useRef<HTMLDivElement>(null);\n\n // Layout constants\n const GRID_GAP = 6;\n const CONTAINER_PADDING = 8;\n const CELL_MARGIN = 0.05; // 5% margin inside cell for tangram\n // jsPsych progress bar: 20px height + 8px top/bottom padding + 18px margin\n const PROGRESS_BAR_HEIGHT = 58;\n // Border width on each cell (from CONFIG.size.stroke.bandPx)\n const CELL_BORDER = CONFIG.size.stroke.bandPx ?? 1;\n // Extra buffer to prevent scrollbars from appearing\n const SAFETY_BUFFER = 8;\n\n // Inject style to override jspsych-content max-width constraint\n useEffect(() => {\n const styleId = \"tangram-grid-jspsych-override\";\n if (!document.getElementById(styleId)) {\n const style = document.createElement(\"style\");\n style.id = styleId;\n style.textContent = `\n .jspsych-content {\n max-width: 100% !important;\n width: 100% !important;\n }\n `;\n document.head.appendChild(style);\n }\n return () => {\n const style = document.getElementById(styleId);\n if (style) style.remove();\n };\n }, []);\n\n // Canonical piece names\n const CANON = new Set([\n \"square\",\n \"smalltriangle\",\n \"parallelogram\",\n \"medtriangle\",\n \"largetriangle\"\n ]);\n\n // Convert TangramSpec to internal format with mask and decomposition\n const processedTangrams = useMemo(() => {\n return tangrams.map((tangramSpec) => {\n const filteredTans = tangramSpec.solutionTans.filter((tan: any) => {\n const tanName = tan.name ?? tan.kind;\n return CANON.has(tanName);\n });\n\n const mask: Poly[] = filteredTans.map((tan: any) => {\n return tan.vertices.map(([x, y]: number[]) => ({\n x: x ?? 0,\n y: -(y ?? 0)\n }));\n });\n\n const primitiveDecomposition = filteredTans.map((tan: any) => ({\n kind: (tan.name ?? tan.kind) as TanKind,\n polygon: tan.vertices.map(([x, y]: number[]) => ({\n x: x ?? 0,\n y: -(y ?? 0)\n }))\n }));\n\n return {\n tangramId: tangramSpec.tangramID,\n mask,\n primitiveDecomposition\n };\n });\n }, [tangrams]);\n\n // Find the largest tangram dimensions to determine scaling\n const maxTangramExtent = useMemo(() => {\n let maxExtent = 0;\n for (const t of processedTangrams) {\n const bounds = computeBounds(t.mask);\n maxExtent = Math.max(maxExtent, bounds.width, bounds.height);\n }\n return maxExtent || 1;\n }, [processedTangrams]);\n\n // Calculate cell size based on available space\n useEffect(() => {\n const calculateCellSize = () => {\n // Use document.documentElement for more accurate viewport size\n const viewportWidth = document.documentElement.clientWidth;\n const viewportHeight = document.documentElement.clientHeight;\n\n // Reserve space for controls (prompt + input row) and progress bar\n const controlsHeight = controlsRef.current?.offsetHeight ?? 70;\n\n // Available space for the grid (subtract progress bar, controls, padding, buffer)\n const availableWidth =\n viewportWidth - CONTAINER_PADDING * 2 - SAFETY_BUFFER;\n const availableHeight =\n viewportHeight - PROGRESS_BAR_HEIGHT - controlsHeight -\n CONTAINER_PADDING * 2 - SAFETY_BUFFER;\n\n // Account for gaps between cells AND borders on each cell\n // Each cell has border on all sides, so total border per cell = 2 * CELL_BORDER\n const totalHorizontalGaps = GRID_GAP * (n_cols - 1);\n const totalVerticalGaps = GRID_GAP * (n_rows - 1);\n const totalHorizontalBorders = CELL_BORDER * 2 * n_cols;\n const totalVerticalBorders = CELL_BORDER * 2 * n_rows;\n\n // Calculate max cell size that fits in available space\n const maxCellWidth =\n (availableWidth - totalHorizontalGaps - totalHorizontalBorders) / n_cols;\n const maxCellHeight =\n (availableHeight - totalVerticalGaps - totalVerticalBorders) / n_rows;\n\n // Use the smaller dimension to keep cells square\n const newCellSize = Math.floor(Math.min(maxCellWidth, maxCellHeight));\n setCellSize(Math.max(newCellSize, 50)); // Minimum 50px\n };\n\n calculateCellSize();\n window.addEventListener(\"resize\", calculateCellSize);\n return () => window.removeEventListener(\"resize\", calculateCellSize);\n }, [n_rows, n_cols]);\n\n // Scale factor to fit largest tangram in cell with margin\n const tangramScale = useMemo(() => {\n const usableSize = cellSize * (1 - CELL_MARGIN * 2);\n return usableSize / maxTangramExtent;\n }, [cellSize, maxTangramExtent]);\n\n // Mapping from TanKind to color index\n const kindToIndex: Record<TanKind, number> = {\n square: 0,\n smalltriangle: 1,\n parallelogram: 2,\n medtriangle: 3,\n largetriangle: 4\n };\n\n // Helper to convert polygon to SVG path\n const pathD = (poly: Poly): string => {\n if (!poly || poly.length === 0) return \"\";\n const moves = poly.map(\n (p, i) => `${i === 0 ? \"M\" : \"L\"} ${p.x} ${p.y}`\n );\n return moves.join(\" \") + \" Z\";\n };\n\n // Handle submit\n const handleSubmit = () => {\n const rt = Date.now() - trialStartTime.current;\n const trialData = {\n response,\n rt,\n n_rows,\n n_cols,\n tangram_ids: processedTangrams.map((t) => t.tangramId),\n show_tangram_decomposition,\n use_primitive_colors: usePrimitiveColors,\n primitive_color_indices: primitiveColorIndices\n };\n if (onTrialEnd) {\n onTrialEnd(trialData);\n }\n };\n\n // Render a single tangram SVG\n const renderTangram = (\n tangramData: (typeof processedTangrams)[0],\n index: number\n ) => {\n const { mask, primitiveDecomposition } = tangramData;\n\n if (show_tangram_decomposition) {\n // Show individual primitives with optional coloring\n // Scale each primitive and fit to viewport while preserving relative positions\n const scaledPrimitives = primitiveDecomposition.map(\n (prim: { kind: TanKind; polygon: Poly }) => {\n const scaledPoly = prim.polygon.map((p: { x: number; y: number }) => ({\n x: p.x * tangramScale,\n y: p.y * tangramScale\n }));\n return { kind: prim.kind, polygon: scaledPoly };\n }\n );\n\n // Find bounds of all primitives together\n let minX = Infinity,\n minY = Infinity,\n maxX = -Infinity,\n maxY = -Infinity;\n for (const prim of scaledPrimitives) {\n for (const p of prim.polygon) {\n minX = Math.min(minX, p.x);\n minY = Math.min(minY, p.y);\n maxX = Math.max(maxX, p.x);\n maxY = Math.max(maxY, p.y);\n }\n }\n\n // Compute translation to center all primitives together in viewport\n const width = maxX - minX;\n const height = maxY - minY;\n const tx = cellSize / 2 - (minX + width / 2);\n const ty = cellSize / 2 - (minY + height / 2);\n\n const translatedPrimitives = scaledPrimitives.map(\n (prim: { kind: TanKind; polygon: Poly }) => ({\n kind: prim.kind,\n polygon: prim.polygon.map((p: { x: number; y: number }) => ({\n x: p.x + tx,\n y: p.y + ty\n }))\n })\n );\n\n return (\n <svg\n key={index}\n width={cellSize}\n height={cellSize}\n viewBox={`0 0 ${cellSize} ${cellSize}`}\n style={{\n display: \"block\",\n background: CONFIG.color.bands.silhouette.fillEven,\n border: `${CONFIG.size.stroke.bandPx}px solid ${CONFIG.color.bands.silhouette.stroke}`,\n borderRadius: \"8px\"\n }}\n >\n {translatedPrimitives.map(\n (prim: { kind: TanKind; polygon: Poly }, i: number) => {\n let fillColor: string;\n\n if (usePrimitiveColors) {\n // Use primitive colors: map piece type to color via primitiveColorIndices\n const primitiveIndex = kindToIndex[prim.kind];\n if (\n primitiveIndex !== undefined &&\n primitiveColorIndices[primitiveIndex] !== undefined\n ) {\n const colorIndex = primitiveColorIndices[primitiveIndex];\n const color = CONFIG.color.primitiveColors[colorIndex];\n fillColor = color || CONFIG.color.piece.validFill;\n } else {\n fillColor = CONFIG.color.piece.validFill;\n }\n } else {\n // Use default piece color when not using primitive colors\n fillColor = CONFIG.color.piece.validFill;\n }\n\n return (\n <path\n key={`prim-${i}`}\n d={pathD(prim.polygon)}\n fill={fillColor}\n opacity={CONFIG.opacity.piece.normal}\n stroke={CONFIG.color.tangramDecomposition.stroke}\n strokeWidth={CONFIG.size.stroke.tangramDecompositionPx}\n />\n );\n }\n )}\n </svg>\n );\n } else {\n // Show as silhouette (merged shape) - scale and center\n const scaledMask = mask.map((poly) =>\n poly.map((p) => ({\n x: p.x * tangramScale,\n y: p.y * tangramScale\n }))\n );\n\n // Find bounds\n let minX = Infinity,\n minY = Infinity,\n maxX = -Infinity,\n maxY = -Infinity;\n for (const poly of scaledMask) {\n for (const p of poly) {\n minX = Math.min(minX, p.x);\n minY = Math.min(minY, p.y);\n maxX = Math.max(maxX, p.x);\n maxY = Math.max(maxY, p.y);\n }\n }\n\n // Center in viewport\n const width = maxX - minX;\n const height = maxY - minY;\n const tx = cellSize / 2 - (minX + width / 2);\n const ty = cellSize / 2 - (minY + height / 2);\n\n const placedMask = scaledMask.map((poly) =>\n poly.map((p) => ({ x: p.x + tx, y: p.y + ty }))\n );\n\n return (\n <svg\n key={index}\n width={cellSize}\n height={cellSize}\n viewBox={`0 0 ${cellSize} ${cellSize}`}\n style={{\n display: \"block\",\n background: CONFIG.color.bands.silhouette.fillEven,\n border: `${CONFIG.size.stroke.bandPx}px solid ${CONFIG.color.bands.silhouette.stroke}`,\n borderRadius: \"8px\"\n }}\n >\n {placedMask.map((poly, i) => (\n <path\n key={`sil-${i}`}\n d={pathD(poly)}\n fill={CONFIG.color.piece.validFill}\n opacity={CONFIG.opacity.piece.normal}\n stroke=\"none\"\n />\n ))}\n </svg>\n );\n }\n };\n\n const isSubmitDisabled = response.trim().length === 0;\n\n return (\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n background: CONFIG.color.background,\n width: \"100%\",\n height: `calc(100vh - ${PROGRESS_BAR_HEIGHT}px)`,\n overflow: \"hidden\",\n fontFamily: \"Roboto, sans-serif\",\n boxSizing: \"border-box\",\n padding: `${CONTAINER_PADDING}px`\n }}\n >\n {/* Grid of tangrams - takes up available space */}\n <div\n style={{\n flex: \"1 1 auto\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n minHeight: 0\n }}\n >\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: `repeat(${n_cols}, ${cellSize}px)`,\n gridTemplateRows: `repeat(${n_rows}, ${cellSize}px)`,\n gap: `${GRID_GAP}px`\n }}\n >\n {processedTangrams.slice(0, n_rows * n_cols).map((t, i) =>\n renderTangram(t, i)\n )}\n </div>\n </div>\n\n {/* Controls section - fixed height at bottom */}\n <div\n ref={controlsRef}\n style={{\n flex: \"0 0 auto\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"4px\",\n paddingTop: \"4px\"\n }}\n >\n {/* Prompt text */}\n <div\n style={{\n fontSize: \"14px\",\n textAlign: \"center\",\n maxWidth: \"90vw\"\n }}\n >\n {prompt_text}\n </div>\n\n {/* Text input and submit button side by side */}\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"12px\"\n }}\n >\n <input\n type=\"text\"\n value={response}\n onChange={(e) => setResponse(e.target.value)}\n style={{\n width: \"min(400px, 50vw)\",\n padding: \"6px 10px\",\n fontSize: \"14px\",\n borderRadius: \"6px\",\n border: \"2px solid #ccc\",\n fontFamily: \"inherit\",\n boxSizing: \"border-box\"\n }}\n placeholder=\"Type your response here...\"\n />\n\n <button\n className=\"jspsych-btn\"\n onClick={handleSubmit}\n disabled={isSubmitDisabled}\n style={{\n padding: \"6px 16px\",\n fontSize: \"13px\",\n cursor: isSubmitDisabled ? \"not-allowed\" : \"pointer\",\n opacity: isSubmitDisabled ? 0.5 : 1,\n flexShrink: 0\n }}\n >\n {button_text}\n </button>\n </div>\n </div>\n </div>\n );\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\nimport { startGridTrial, StartGridTrialParams } from \"./GridApp\";\n\nconst info = {\n name: \"tangram-grid\",\n version: \"1.0.0\",\n parameters: {\n /** Array of tangram specifications to display in the grid */\n tangrams: {\n type: ParameterType.COMPLEX,\n default: undefined,\n description: \"Array of TangramSpec objects to display in the grid\"\n },\n /** Number of rows in the grid */\n n_rows: {\n type: ParameterType.INT,\n default: 1,\n description: \"Number of rows in the tangram grid\"\n },\n /** Number of columns in the grid */\n n_cols: {\n type: ParameterType.INT,\n default: 1,\n description: \"Number of columns in the tangram grid\"\n },\n /** Prompt text displayed above the text input */\n prompt_text: {\n type: ParameterType.STRING,\n default: \"\",\n description: \"Text displayed above the text input field\"\n },\n /** Label for the submit button */\n button_text: {\n type: ParameterType.STRING,\n default: \"Submit\",\n description: \"Text displayed on the submit button\"\n },\n /** Whether to show tangrams decomposed into primitives */\n show_tangram_decomposition: {\n type: ParameterType.BOOL,\n default: false,\n description:\n \"Whether to show tangrams decomposed into individual primitives\"\n },\n /** Whether to use distinct colors for each primitive type */\n use_primitive_colors: {\n type: ParameterType.BOOL,\n default: false,\n description:\n \"Whether each primitive shape type should have its own distinct color\"\n },\n /** Indices mapping primitives to colors */\n primitive_color_indices: {\n type: ParameterType.OBJECT,\n default: [0, 1, 2, 3, 4],\n description:\n \"Array of 5 integers indexing into primitiveColors array, mapping \" +\n \"[square, smalltriangle, parallelogram, medtriangle, largetriangle] \" +\n \"to colors\"\n },\n /** Callback fired when trial ends */\n onTrialEnd: {\n type: ParameterType.FUNCTION,\n default: undefined,\n description: \"Callback when trial completes with full data\"\n }\n },\n data: {\n /** The text response entered by the participant */\n response: {\n type: ParameterType.STRING,\n description: \"The text response entered by the participant\"\n },\n /** Reaction time from trial start to submit button click */\n rt: {\n type: ParameterType.INT,\n description: \"Time in milliseconds from trial start to submit\"\n }\n },\n citations: \"\"\n};\n\ntype Info = typeof info;\n\n/**\n * **tangram-grid**\n *\n * A jsPsych plugin that displays a grid of tangrams with a text input field\n * and submit button for collecting free-text responses.\n *\n * @author Sean Paul Anderson & Justin Yang\n * @see {@link https://github.com/cogtoolslab/tangram_construction.git}\n */\nclass TangramGridPlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n /**\n * Launches the trial by invoking startGridTrial with the display element,\n * parameters, and jsPsych instance.\n *\n * REQUIRES: display_element is a valid HTMLElement, trial contains valid\n * parameters\n * MODIFIES: display_element (renders React component)\n * EFFECTS: Starts the grid trial and handles cleanup on completion\n */\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n // Wrap onTrialEnd to handle React cleanup and jsPsych trial completion\n const wrappedOnTrialEnd = (data: any) => {\n // Call user-provided callback if exists\n if (trial.onTrialEnd) {\n trial.onTrialEnd(data);\n }\n\n // Clean up React first (before clearing DOM)\n const reactContext = (display_element as any).__reactContext;\n if (reactContext?.root) {\n reactContext.root.unmount();\n }\n\n // Clear display after React cleanup\n display_element.innerHTML = \"\";\n\n // Finish jsPsych trial with data\n this.jsPsych.finishTrial(data);\n };\n\n // Create parameter object for wrapper\n const params: StartGridTrialParams = {\n tangrams: trial.tangrams,\n n_rows: trial.n_rows,\n n_cols: trial.n_cols,\n prompt_text: trial.prompt_text,\n button_text: trial.button_text,\n show_tangram_decomposition: trial.show_tangram_decomposition,\n usePrimitiveColors: trial.use_primitive_colors,\n primitiveColorIndices: trial.primitive_color_indices,\n onTrialEnd: wrappedOnTrialEnd\n };\n\n // Use React wrapper to start the trial\n const { root, display_element: element, jsPsych } = startGridTrial(\n display_element,\n params,\n this.jsPsych\n );\n\n // Store React context for cleanup\n (element as any).__reactContext = { root, jsPsych };\n }\n}\n\nexport default TangramGridPlugin;\n"],"names":["createRoot","useRef","useState","useEffect","useMemo","ParameterType"],"mappings":"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"}
|