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