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,174 @@
|
|
|
1
|
+
import { JsPsychPlugin, ParameterType, JsPsych, TrialType } from 'jspsych';
|
|
2
|
+
|
|
3
|
+
declare const info: {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
parameters: {
|
|
7
|
+
/** Array of tangram specifications to display in the grid */
|
|
8
|
+
tangrams: {
|
|
9
|
+
type: ParameterType;
|
|
10
|
+
default: undefined;
|
|
11
|
+
description: string;
|
|
12
|
+
};
|
|
13
|
+
/** Number of rows in the grid */
|
|
14
|
+
n_rows: {
|
|
15
|
+
type: ParameterType;
|
|
16
|
+
default: number;
|
|
17
|
+
description: string;
|
|
18
|
+
};
|
|
19
|
+
/** Number of columns in the grid */
|
|
20
|
+
n_cols: {
|
|
21
|
+
type: ParameterType;
|
|
22
|
+
default: number;
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
25
|
+
/** Prompt text displayed above the text input */
|
|
26
|
+
prompt_text: {
|
|
27
|
+
type: ParameterType;
|
|
28
|
+
default: string;
|
|
29
|
+
description: string;
|
|
30
|
+
};
|
|
31
|
+
/** Label for the submit button */
|
|
32
|
+
button_text: {
|
|
33
|
+
type: ParameterType;
|
|
34
|
+
default: string;
|
|
35
|
+
description: string;
|
|
36
|
+
};
|
|
37
|
+
/** Whether to show tangrams decomposed into primitives */
|
|
38
|
+
show_tangram_decomposition: {
|
|
39
|
+
type: ParameterType;
|
|
40
|
+
default: boolean;
|
|
41
|
+
description: string;
|
|
42
|
+
};
|
|
43
|
+
/** Whether to use distinct colors for each primitive type */
|
|
44
|
+
use_primitive_colors: {
|
|
45
|
+
type: ParameterType;
|
|
46
|
+
default: boolean;
|
|
47
|
+
description: string;
|
|
48
|
+
};
|
|
49
|
+
/** Indices mapping primitives to colors */
|
|
50
|
+
primitive_color_indices: {
|
|
51
|
+
type: ParameterType;
|
|
52
|
+
default: number[];
|
|
53
|
+
description: string;
|
|
54
|
+
};
|
|
55
|
+
/** Callback fired when trial ends */
|
|
56
|
+
onTrialEnd: {
|
|
57
|
+
type: ParameterType;
|
|
58
|
+
default: undefined;
|
|
59
|
+
description: string;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
data: {
|
|
63
|
+
/** The text response entered by the participant */
|
|
64
|
+
response: {
|
|
65
|
+
type: ParameterType;
|
|
66
|
+
description: string;
|
|
67
|
+
};
|
|
68
|
+
/** Reaction time from trial start to submit button click */
|
|
69
|
+
rt: {
|
|
70
|
+
type: ParameterType;
|
|
71
|
+
description: string;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
citations: string;
|
|
75
|
+
};
|
|
76
|
+
type Info = typeof info;
|
|
77
|
+
/**
|
|
78
|
+
* **tangram-grid**
|
|
79
|
+
*
|
|
80
|
+
* A jsPsych plugin that displays a grid of tangrams with a text input field
|
|
81
|
+
* and submit button for collecting free-text responses.
|
|
82
|
+
*
|
|
83
|
+
* @author Sean Paul Anderson & Justin Yang
|
|
84
|
+
* @see {@link https://github.com/cogtoolslab/tangram_construction.git}
|
|
85
|
+
*/
|
|
86
|
+
declare class TangramGridPlugin implements JsPsychPlugin<Info> {
|
|
87
|
+
private jsPsych;
|
|
88
|
+
static info: {
|
|
89
|
+
name: string;
|
|
90
|
+
version: string;
|
|
91
|
+
parameters: {
|
|
92
|
+
/** Array of tangram specifications to display in the grid */
|
|
93
|
+
tangrams: {
|
|
94
|
+
type: ParameterType;
|
|
95
|
+
default: undefined;
|
|
96
|
+
description: string;
|
|
97
|
+
};
|
|
98
|
+
/** Number of rows in the grid */
|
|
99
|
+
n_rows: {
|
|
100
|
+
type: ParameterType;
|
|
101
|
+
default: number;
|
|
102
|
+
description: string;
|
|
103
|
+
};
|
|
104
|
+
/** Number of columns in the grid */
|
|
105
|
+
n_cols: {
|
|
106
|
+
type: ParameterType;
|
|
107
|
+
default: number;
|
|
108
|
+
description: string;
|
|
109
|
+
};
|
|
110
|
+
/** Prompt text displayed above the text input */
|
|
111
|
+
prompt_text: {
|
|
112
|
+
type: ParameterType;
|
|
113
|
+
default: string;
|
|
114
|
+
description: string;
|
|
115
|
+
};
|
|
116
|
+
/** Label for the submit button */
|
|
117
|
+
button_text: {
|
|
118
|
+
type: ParameterType;
|
|
119
|
+
default: string;
|
|
120
|
+
description: string;
|
|
121
|
+
};
|
|
122
|
+
/** Whether to show tangrams decomposed into primitives */
|
|
123
|
+
show_tangram_decomposition: {
|
|
124
|
+
type: ParameterType;
|
|
125
|
+
default: boolean;
|
|
126
|
+
description: string;
|
|
127
|
+
};
|
|
128
|
+
/** Whether to use distinct colors for each primitive type */
|
|
129
|
+
use_primitive_colors: {
|
|
130
|
+
type: ParameterType;
|
|
131
|
+
default: boolean;
|
|
132
|
+
description: string;
|
|
133
|
+
};
|
|
134
|
+
/** Indices mapping primitives to colors */
|
|
135
|
+
primitive_color_indices: {
|
|
136
|
+
type: ParameterType;
|
|
137
|
+
default: number[];
|
|
138
|
+
description: string;
|
|
139
|
+
};
|
|
140
|
+
/** Callback fired when trial ends */
|
|
141
|
+
onTrialEnd: {
|
|
142
|
+
type: ParameterType;
|
|
143
|
+
default: undefined;
|
|
144
|
+
description: string;
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
data: {
|
|
148
|
+
/** The text response entered by the participant */
|
|
149
|
+
response: {
|
|
150
|
+
type: ParameterType;
|
|
151
|
+
description: string;
|
|
152
|
+
};
|
|
153
|
+
/** Reaction time from trial start to submit button click */
|
|
154
|
+
rt: {
|
|
155
|
+
type: ParameterType;
|
|
156
|
+
description: string;
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
citations: string;
|
|
160
|
+
};
|
|
161
|
+
constructor(jsPsych: JsPsych);
|
|
162
|
+
/**
|
|
163
|
+
* Launches the trial by invoking startGridTrial with the display element,
|
|
164
|
+
* parameters, and jsPsych instance.
|
|
165
|
+
*
|
|
166
|
+
* REQUIRES: display_element is a valid HTMLElement, trial contains valid
|
|
167
|
+
* parameters
|
|
168
|
+
* MODIFIES: display_element (renders React component)
|
|
169
|
+
* EFFECTS: Starts the grid trial and handles cleanup on completion
|
|
170
|
+
*/
|
|
171
|
+
trial(display_element: HTMLElement, trial: TrialType<Info>): void;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { TangramGridPlugin as default };
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import { ParameterType } from 'jspsych';
|
|
2
|
+
import React, { useRef, useState, useEffect, useMemo } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
|
|
5
|
+
const CONFIG = {
|
|
6
|
+
color: {
|
|
7
|
+
background: "#fff7e0ff",
|
|
8
|
+
bands: {
|
|
9
|
+
silhouette: { fillEven: "#ffffff", stroke: "#b1b1b1" }},
|
|
10
|
+
// validFill used here for placed composites
|
|
11
|
+
piece: { validFill: "#8e7cc3"},
|
|
12
|
+
tangramDecomposition: { stroke: "#fef2cc" },
|
|
13
|
+
primitiveColors: [
|
|
14
|
+
// from seaborn "colorblind" palette, 6 colors, with red omitted
|
|
15
|
+
"#0173b2",
|
|
16
|
+
"#de8f05",
|
|
17
|
+
"#029e73",
|
|
18
|
+
"#cc78bc",
|
|
19
|
+
"#ca9161"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
opacity: {
|
|
23
|
+
piece: { normal: 1 }
|
|
24
|
+
},
|
|
25
|
+
size: {
|
|
26
|
+
stroke: { bandPx: 5, tangramDecompositionPx: 1 }}};
|
|
27
|
+
|
|
28
|
+
function startGridTrial(display_element, params, _jsPsych) {
|
|
29
|
+
const root = createRoot(display_element);
|
|
30
|
+
root.render(React.createElement(GridView, { params }));
|
|
31
|
+
return { root, display_element, jsPsych: _jsPsych };
|
|
32
|
+
}
|
|
33
|
+
function computeBounds(polys) {
|
|
34
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
35
|
+
for (const poly of polys) {
|
|
36
|
+
for (const p of poly) {
|
|
37
|
+
minX = Math.min(minX, p.x);
|
|
38
|
+
minY = Math.min(minY, p.y);
|
|
39
|
+
maxX = Math.max(maxX, p.x);
|
|
40
|
+
maxY = Math.max(maxY, p.y);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
|
|
44
|
+
}
|
|
45
|
+
function GridView({ params }) {
|
|
46
|
+
const {
|
|
47
|
+
tangrams,
|
|
48
|
+
n_rows,
|
|
49
|
+
n_cols,
|
|
50
|
+
prompt_text,
|
|
51
|
+
button_text,
|
|
52
|
+
show_tangram_decomposition = false,
|
|
53
|
+
usePrimitiveColors = false,
|
|
54
|
+
primitiveColorIndices = [0, 1, 2, 3, 4],
|
|
55
|
+
onTrialEnd
|
|
56
|
+
} = params;
|
|
57
|
+
const trialStartTime = useRef(Date.now());
|
|
58
|
+
const [response, setResponse] = useState("");
|
|
59
|
+
const [cellSize, setCellSize] = useState(100);
|
|
60
|
+
const controlsRef = useRef(null);
|
|
61
|
+
const GRID_GAP = 6;
|
|
62
|
+
const CONTAINER_PADDING = 8;
|
|
63
|
+
const CELL_MARGIN = 0.05;
|
|
64
|
+
const PROGRESS_BAR_HEIGHT = 58;
|
|
65
|
+
const CELL_BORDER = CONFIG.size.stroke.bandPx;
|
|
66
|
+
const SAFETY_BUFFER = 8;
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const styleId = "tangram-grid-jspsych-override";
|
|
69
|
+
if (!document.getElementById(styleId)) {
|
|
70
|
+
const style = document.createElement("style");
|
|
71
|
+
style.id = styleId;
|
|
72
|
+
style.textContent = `
|
|
73
|
+
.jspsych-content {
|
|
74
|
+
max-width: 100% !important;
|
|
75
|
+
width: 100% !important;
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
document.head.appendChild(style);
|
|
79
|
+
}
|
|
80
|
+
return () => {
|
|
81
|
+
const style = document.getElementById(styleId);
|
|
82
|
+
if (style) style.remove();
|
|
83
|
+
};
|
|
84
|
+
}, []);
|
|
85
|
+
const CANON = /* @__PURE__ */ new Set([
|
|
86
|
+
"square",
|
|
87
|
+
"smalltriangle",
|
|
88
|
+
"parallelogram",
|
|
89
|
+
"medtriangle",
|
|
90
|
+
"largetriangle"
|
|
91
|
+
]);
|
|
92
|
+
const processedTangrams = useMemo(() => {
|
|
93
|
+
return tangrams.map((tangramSpec) => {
|
|
94
|
+
const filteredTans = tangramSpec.solutionTans.filter((tan) => {
|
|
95
|
+
const tanName = tan.name ?? tan.kind;
|
|
96
|
+
return CANON.has(tanName);
|
|
97
|
+
});
|
|
98
|
+
const mask = filteredTans.map((tan) => {
|
|
99
|
+
return tan.vertices.map(([x, y]) => ({
|
|
100
|
+
x: x ?? 0,
|
|
101
|
+
y: -(y ?? 0)
|
|
102
|
+
}));
|
|
103
|
+
});
|
|
104
|
+
const primitiveDecomposition = filteredTans.map((tan) => ({
|
|
105
|
+
kind: tan.name ?? tan.kind,
|
|
106
|
+
polygon: tan.vertices.map(([x, y]) => ({
|
|
107
|
+
x: x ?? 0,
|
|
108
|
+
y: -(y ?? 0)
|
|
109
|
+
}))
|
|
110
|
+
}));
|
|
111
|
+
return {
|
|
112
|
+
tangramId: tangramSpec.tangramID,
|
|
113
|
+
mask,
|
|
114
|
+
primitiveDecomposition
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}, [tangrams]);
|
|
118
|
+
const maxTangramExtent = useMemo(() => {
|
|
119
|
+
let maxExtent = 0;
|
|
120
|
+
for (const t of processedTangrams) {
|
|
121
|
+
const bounds = computeBounds(t.mask);
|
|
122
|
+
maxExtent = Math.max(maxExtent, bounds.width, bounds.height);
|
|
123
|
+
}
|
|
124
|
+
return maxExtent || 1;
|
|
125
|
+
}, [processedTangrams]);
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const calculateCellSize = () => {
|
|
128
|
+
const viewportWidth = document.documentElement.clientWidth;
|
|
129
|
+
const viewportHeight = document.documentElement.clientHeight;
|
|
130
|
+
const controlsHeight = controlsRef.current?.offsetHeight ?? 70;
|
|
131
|
+
const availableWidth = viewportWidth - CONTAINER_PADDING * 2 - SAFETY_BUFFER;
|
|
132
|
+
const availableHeight = viewportHeight - PROGRESS_BAR_HEIGHT - controlsHeight - CONTAINER_PADDING * 2 - SAFETY_BUFFER;
|
|
133
|
+
const totalHorizontalGaps = GRID_GAP * (n_cols - 1);
|
|
134
|
+
const totalVerticalGaps = GRID_GAP * (n_rows - 1);
|
|
135
|
+
const totalHorizontalBorders = CELL_BORDER * 2 * n_cols;
|
|
136
|
+
const totalVerticalBorders = CELL_BORDER * 2 * n_rows;
|
|
137
|
+
const maxCellWidth = (availableWidth - totalHorizontalGaps - totalHorizontalBorders) / n_cols;
|
|
138
|
+
const maxCellHeight = (availableHeight - totalVerticalGaps - totalVerticalBorders) / n_rows;
|
|
139
|
+
const newCellSize = Math.floor(Math.min(maxCellWidth, maxCellHeight));
|
|
140
|
+
setCellSize(Math.max(newCellSize, 50));
|
|
141
|
+
};
|
|
142
|
+
calculateCellSize();
|
|
143
|
+
window.addEventListener("resize", calculateCellSize);
|
|
144
|
+
return () => window.removeEventListener("resize", calculateCellSize);
|
|
145
|
+
}, [n_rows, n_cols]);
|
|
146
|
+
const tangramScale = useMemo(() => {
|
|
147
|
+
const usableSize = cellSize * (1 - CELL_MARGIN * 2);
|
|
148
|
+
return usableSize / maxTangramExtent;
|
|
149
|
+
}, [cellSize, maxTangramExtent]);
|
|
150
|
+
const kindToIndex = {
|
|
151
|
+
square: 0,
|
|
152
|
+
smalltriangle: 1,
|
|
153
|
+
parallelogram: 2,
|
|
154
|
+
medtriangle: 3,
|
|
155
|
+
largetriangle: 4
|
|
156
|
+
};
|
|
157
|
+
const pathD = (poly) => {
|
|
158
|
+
if (!poly || poly.length === 0) return "";
|
|
159
|
+
const moves = poly.map(
|
|
160
|
+
(p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`
|
|
161
|
+
);
|
|
162
|
+
return moves.join(" ") + " Z";
|
|
163
|
+
};
|
|
164
|
+
const handleSubmit = () => {
|
|
165
|
+
const rt = Date.now() - trialStartTime.current;
|
|
166
|
+
const trialData = {
|
|
167
|
+
response,
|
|
168
|
+
rt,
|
|
169
|
+
n_rows,
|
|
170
|
+
n_cols,
|
|
171
|
+
tangram_ids: processedTangrams.map((t) => t.tangramId),
|
|
172
|
+
show_tangram_decomposition,
|
|
173
|
+
use_primitive_colors: usePrimitiveColors,
|
|
174
|
+
primitive_color_indices: primitiveColorIndices
|
|
175
|
+
};
|
|
176
|
+
if (onTrialEnd) {
|
|
177
|
+
onTrialEnd(trialData);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const renderTangram = (tangramData, index) => {
|
|
181
|
+
const { mask, primitiveDecomposition } = tangramData;
|
|
182
|
+
if (show_tangram_decomposition) {
|
|
183
|
+
const scaledPrimitives = primitiveDecomposition.map(
|
|
184
|
+
(prim) => {
|
|
185
|
+
const scaledPoly = prim.polygon.map((p) => ({
|
|
186
|
+
x: p.x * tangramScale,
|
|
187
|
+
y: p.y * tangramScale
|
|
188
|
+
}));
|
|
189
|
+
return { kind: prim.kind, polygon: scaledPoly };
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
193
|
+
for (const prim of scaledPrimitives) {
|
|
194
|
+
for (const p of prim.polygon) {
|
|
195
|
+
minX = Math.min(minX, p.x);
|
|
196
|
+
minY = Math.min(minY, p.y);
|
|
197
|
+
maxX = Math.max(maxX, p.x);
|
|
198
|
+
maxY = Math.max(maxY, p.y);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const width = maxX - minX;
|
|
202
|
+
const height = maxY - minY;
|
|
203
|
+
const tx = cellSize / 2 - (minX + width / 2);
|
|
204
|
+
const ty = cellSize / 2 - (minY + height / 2);
|
|
205
|
+
const translatedPrimitives = scaledPrimitives.map(
|
|
206
|
+
(prim) => ({
|
|
207
|
+
kind: prim.kind,
|
|
208
|
+
polygon: prim.polygon.map((p) => ({
|
|
209
|
+
x: p.x + tx,
|
|
210
|
+
y: p.y + ty
|
|
211
|
+
}))
|
|
212
|
+
})
|
|
213
|
+
);
|
|
214
|
+
return /* @__PURE__ */ React.createElement(
|
|
215
|
+
"svg",
|
|
216
|
+
{
|
|
217
|
+
key: index,
|
|
218
|
+
width: cellSize,
|
|
219
|
+
height: cellSize,
|
|
220
|
+
viewBox: `0 0 ${cellSize} ${cellSize}`,
|
|
221
|
+
style: {
|
|
222
|
+
display: "block",
|
|
223
|
+
background: CONFIG.color.bands.silhouette.fillEven,
|
|
224
|
+
border: `${CONFIG.size.stroke.bandPx}px solid ${CONFIG.color.bands.silhouette.stroke}`,
|
|
225
|
+
borderRadius: "8px"
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
translatedPrimitives.map(
|
|
229
|
+
(prim, i) => {
|
|
230
|
+
let fillColor;
|
|
231
|
+
if (usePrimitiveColors) {
|
|
232
|
+
const primitiveIndex = kindToIndex[prim.kind];
|
|
233
|
+
if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
|
|
234
|
+
const colorIndex = primitiveColorIndices[primitiveIndex];
|
|
235
|
+
const color = CONFIG.color.primitiveColors[colorIndex];
|
|
236
|
+
fillColor = color || CONFIG.color.piece.validFill;
|
|
237
|
+
} else {
|
|
238
|
+
fillColor = CONFIG.color.piece.validFill;
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
fillColor = CONFIG.color.piece.validFill;
|
|
242
|
+
}
|
|
243
|
+
return /* @__PURE__ */ React.createElement(
|
|
244
|
+
"path",
|
|
245
|
+
{
|
|
246
|
+
key: `prim-${i}`,
|
|
247
|
+
d: pathD(prim.polygon),
|
|
248
|
+
fill: fillColor,
|
|
249
|
+
opacity: CONFIG.opacity.piece.normal,
|
|
250
|
+
stroke: CONFIG.color.tangramDecomposition.stroke,
|
|
251
|
+
strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
);
|
|
257
|
+
} else {
|
|
258
|
+
const scaledMask = mask.map(
|
|
259
|
+
(poly) => poly.map((p) => ({
|
|
260
|
+
x: p.x * tangramScale,
|
|
261
|
+
y: p.y * tangramScale
|
|
262
|
+
}))
|
|
263
|
+
);
|
|
264
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
265
|
+
for (const poly of scaledMask) {
|
|
266
|
+
for (const p of poly) {
|
|
267
|
+
minX = Math.min(minX, p.x);
|
|
268
|
+
minY = Math.min(minY, p.y);
|
|
269
|
+
maxX = Math.max(maxX, p.x);
|
|
270
|
+
maxY = Math.max(maxY, p.y);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const width = maxX - minX;
|
|
274
|
+
const height = maxY - minY;
|
|
275
|
+
const tx = cellSize / 2 - (minX + width / 2);
|
|
276
|
+
const ty = cellSize / 2 - (minY + height / 2);
|
|
277
|
+
const placedMask = scaledMask.map(
|
|
278
|
+
(poly) => poly.map((p) => ({ x: p.x + tx, y: p.y + ty }))
|
|
279
|
+
);
|
|
280
|
+
return /* @__PURE__ */ React.createElement(
|
|
281
|
+
"svg",
|
|
282
|
+
{
|
|
283
|
+
key: index,
|
|
284
|
+
width: cellSize,
|
|
285
|
+
height: cellSize,
|
|
286
|
+
viewBox: `0 0 ${cellSize} ${cellSize}`,
|
|
287
|
+
style: {
|
|
288
|
+
display: "block",
|
|
289
|
+
background: CONFIG.color.bands.silhouette.fillEven,
|
|
290
|
+
border: `${CONFIG.size.stroke.bandPx}px solid ${CONFIG.color.bands.silhouette.stroke}`,
|
|
291
|
+
borderRadius: "8px"
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
placedMask.map((poly, i) => /* @__PURE__ */ React.createElement(
|
|
295
|
+
"path",
|
|
296
|
+
{
|
|
297
|
+
key: `sil-${i}`,
|
|
298
|
+
d: pathD(poly),
|
|
299
|
+
fill: CONFIG.color.piece.validFill,
|
|
300
|
+
opacity: CONFIG.opacity.piece.normal,
|
|
301
|
+
stroke: "none"
|
|
302
|
+
}
|
|
303
|
+
))
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
const isSubmitDisabled = response.trim().length === 0;
|
|
308
|
+
return /* @__PURE__ */ React.createElement(
|
|
309
|
+
"div",
|
|
310
|
+
{
|
|
311
|
+
style: {
|
|
312
|
+
display: "flex",
|
|
313
|
+
flexDirection: "column",
|
|
314
|
+
alignItems: "center",
|
|
315
|
+
justifyContent: "space-between",
|
|
316
|
+
background: CONFIG.color.background,
|
|
317
|
+
width: "100%",
|
|
318
|
+
height: `calc(100vh - ${PROGRESS_BAR_HEIGHT}px)`,
|
|
319
|
+
overflow: "hidden",
|
|
320
|
+
fontFamily: "Roboto, sans-serif",
|
|
321
|
+
boxSizing: "border-box",
|
|
322
|
+
padding: `${CONTAINER_PADDING}px`
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
/* @__PURE__ */ React.createElement(
|
|
326
|
+
"div",
|
|
327
|
+
{
|
|
328
|
+
style: {
|
|
329
|
+
flex: "1 1 auto",
|
|
330
|
+
display: "flex",
|
|
331
|
+
alignItems: "center",
|
|
332
|
+
justifyContent: "center",
|
|
333
|
+
minHeight: 0
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
/* @__PURE__ */ React.createElement(
|
|
337
|
+
"div",
|
|
338
|
+
{
|
|
339
|
+
style: {
|
|
340
|
+
display: "grid",
|
|
341
|
+
gridTemplateColumns: `repeat(${n_cols}, ${cellSize}px)`,
|
|
342
|
+
gridTemplateRows: `repeat(${n_rows}, ${cellSize}px)`,
|
|
343
|
+
gap: `${GRID_GAP}px`
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
processedTangrams.slice(0, n_rows * n_cols).map(
|
|
347
|
+
(t, i) => renderTangram(t, i)
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
),
|
|
351
|
+
/* @__PURE__ */ React.createElement(
|
|
352
|
+
"div",
|
|
353
|
+
{
|
|
354
|
+
ref: controlsRef,
|
|
355
|
+
style: {
|
|
356
|
+
flex: "0 0 auto",
|
|
357
|
+
display: "flex",
|
|
358
|
+
flexDirection: "column",
|
|
359
|
+
alignItems: "center",
|
|
360
|
+
gap: "4px",
|
|
361
|
+
paddingTop: "4px"
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
/* @__PURE__ */ React.createElement(
|
|
365
|
+
"div",
|
|
366
|
+
{
|
|
367
|
+
style: {
|
|
368
|
+
fontSize: "14px",
|
|
369
|
+
textAlign: "center",
|
|
370
|
+
maxWidth: "90vw"
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
prompt_text
|
|
374
|
+
),
|
|
375
|
+
/* @__PURE__ */ React.createElement(
|
|
376
|
+
"div",
|
|
377
|
+
{
|
|
378
|
+
style: {
|
|
379
|
+
display: "flex",
|
|
380
|
+
alignItems: "center",
|
|
381
|
+
gap: "12px"
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
/* @__PURE__ */ React.createElement(
|
|
385
|
+
"input",
|
|
386
|
+
{
|
|
387
|
+
type: "text",
|
|
388
|
+
value: response,
|
|
389
|
+
onChange: (e) => setResponse(e.target.value),
|
|
390
|
+
style: {
|
|
391
|
+
width: "min(400px, 50vw)",
|
|
392
|
+
padding: "6px 10px",
|
|
393
|
+
fontSize: "14px",
|
|
394
|
+
borderRadius: "6px",
|
|
395
|
+
border: "2px solid #ccc",
|
|
396
|
+
fontFamily: "inherit",
|
|
397
|
+
boxSizing: "border-box"
|
|
398
|
+
},
|
|
399
|
+
placeholder: "Type your response here..."
|
|
400
|
+
}
|
|
401
|
+
),
|
|
402
|
+
/* @__PURE__ */ React.createElement(
|
|
403
|
+
"button",
|
|
404
|
+
{
|
|
405
|
+
className: "jspsych-btn",
|
|
406
|
+
onClick: handleSubmit,
|
|
407
|
+
disabled: isSubmitDisabled,
|
|
408
|
+
style: {
|
|
409
|
+
padding: "6px 16px",
|
|
410
|
+
fontSize: "13px",
|
|
411
|
+
cursor: isSubmitDisabled ? "not-allowed" : "pointer",
|
|
412
|
+
opacity: isSubmitDisabled ? 0.5 : 1,
|
|
413
|
+
flexShrink: 0
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
button_text
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const info = {
|
|
424
|
+
name: "tangram-grid",
|
|
425
|
+
version: "1.0.0",
|
|
426
|
+
parameters: {
|
|
427
|
+
/** Array of tangram specifications to display in the grid */
|
|
428
|
+
tangrams: {
|
|
429
|
+
type: ParameterType.COMPLEX,
|
|
430
|
+
default: void 0,
|
|
431
|
+
description: "Array of TangramSpec objects to display in the grid"
|
|
432
|
+
},
|
|
433
|
+
/** Number of rows in the grid */
|
|
434
|
+
n_rows: {
|
|
435
|
+
type: ParameterType.INT,
|
|
436
|
+
default: 1,
|
|
437
|
+
description: "Number of rows in the tangram grid"
|
|
438
|
+
},
|
|
439
|
+
/** Number of columns in the grid */
|
|
440
|
+
n_cols: {
|
|
441
|
+
type: ParameterType.INT,
|
|
442
|
+
default: 1,
|
|
443
|
+
description: "Number of columns in the tangram grid"
|
|
444
|
+
},
|
|
445
|
+
/** Prompt text displayed above the text input */
|
|
446
|
+
prompt_text: {
|
|
447
|
+
type: ParameterType.STRING,
|
|
448
|
+
default: "",
|
|
449
|
+
description: "Text displayed above the text input field"
|
|
450
|
+
},
|
|
451
|
+
/** Label for the submit button */
|
|
452
|
+
button_text: {
|
|
453
|
+
type: ParameterType.STRING,
|
|
454
|
+
default: "Submit",
|
|
455
|
+
description: "Text displayed on the submit button"
|
|
456
|
+
},
|
|
457
|
+
/** Whether to show tangrams decomposed into primitives */
|
|
458
|
+
show_tangram_decomposition: {
|
|
459
|
+
type: ParameterType.BOOL,
|
|
460
|
+
default: false,
|
|
461
|
+
description: "Whether to show tangrams decomposed into individual primitives"
|
|
462
|
+
},
|
|
463
|
+
/** Whether to use distinct colors for each primitive type */
|
|
464
|
+
use_primitive_colors: {
|
|
465
|
+
type: ParameterType.BOOL,
|
|
466
|
+
default: false,
|
|
467
|
+
description: "Whether each primitive shape type should have its own distinct color"
|
|
468
|
+
},
|
|
469
|
+
/** Indices mapping primitives to colors */
|
|
470
|
+
primitive_color_indices: {
|
|
471
|
+
type: ParameterType.OBJECT,
|
|
472
|
+
default: [0, 1, 2, 3, 4],
|
|
473
|
+
description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
|
|
474
|
+
},
|
|
475
|
+
/** Callback fired when trial ends */
|
|
476
|
+
onTrialEnd: {
|
|
477
|
+
type: ParameterType.FUNCTION,
|
|
478
|
+
default: void 0,
|
|
479
|
+
description: "Callback when trial completes with full data"
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
data: {
|
|
483
|
+
/** The text response entered by the participant */
|
|
484
|
+
response: {
|
|
485
|
+
type: ParameterType.STRING,
|
|
486
|
+
description: "The text response entered by the participant"
|
|
487
|
+
},
|
|
488
|
+
/** Reaction time from trial start to submit button click */
|
|
489
|
+
rt: {
|
|
490
|
+
type: ParameterType.INT,
|
|
491
|
+
description: "Time in milliseconds from trial start to submit"
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
citations: ""
|
|
495
|
+
};
|
|
496
|
+
class TangramGridPlugin {
|
|
497
|
+
constructor(jsPsych) {
|
|
498
|
+
this.jsPsych = jsPsych;
|
|
499
|
+
}
|
|
500
|
+
static {
|
|
501
|
+
this.info = info;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Launches the trial by invoking startGridTrial with the display element,
|
|
505
|
+
* parameters, and jsPsych instance.
|
|
506
|
+
*
|
|
507
|
+
* REQUIRES: display_element is a valid HTMLElement, trial contains valid
|
|
508
|
+
* parameters
|
|
509
|
+
* MODIFIES: display_element (renders React component)
|
|
510
|
+
* EFFECTS: Starts the grid trial and handles cleanup on completion
|
|
511
|
+
*/
|
|
512
|
+
trial(display_element, trial) {
|
|
513
|
+
const wrappedOnTrialEnd = (data) => {
|
|
514
|
+
if (trial.onTrialEnd) {
|
|
515
|
+
trial.onTrialEnd(data);
|
|
516
|
+
}
|
|
517
|
+
const reactContext = display_element.__reactContext;
|
|
518
|
+
if (reactContext?.root) {
|
|
519
|
+
reactContext.root.unmount();
|
|
520
|
+
}
|
|
521
|
+
display_element.innerHTML = "";
|
|
522
|
+
this.jsPsych.finishTrial(data);
|
|
523
|
+
};
|
|
524
|
+
const params = {
|
|
525
|
+
tangrams: trial.tangrams,
|
|
526
|
+
n_rows: trial.n_rows,
|
|
527
|
+
n_cols: trial.n_cols,
|
|
528
|
+
prompt_text: trial.prompt_text,
|
|
529
|
+
button_text: trial.button_text,
|
|
530
|
+
show_tangram_decomposition: trial.show_tangram_decomposition,
|
|
531
|
+
usePrimitiveColors: trial.use_primitive_colors,
|
|
532
|
+
primitiveColorIndices: trial.primitive_color_indices,
|
|
533
|
+
onTrialEnd: wrappedOnTrialEnd
|
|
534
|
+
};
|
|
535
|
+
const { root, display_element: element, jsPsych } = startGridTrial(
|
|
536
|
+
display_element,
|
|
537
|
+
params,
|
|
538
|
+
this.jsPsych
|
|
539
|
+
);
|
|
540
|
+
element.__reactContext = { root, jsPsych };
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export { TangramGridPlugin as default };
|
|
545
|
+
//# sourceMappingURL=index.js.map
|