jspsych-tangram 0.0.7 → 0.0.8
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 +146 -212
- package/dist/construct/index.browser.js.map +1 -1
- package/dist/construct/index.browser.min.js +11 -11
- package/dist/construct/index.browser.min.js.map +1 -1
- package/dist/construct/index.cjs +146 -212
- package/dist/construct/index.cjs.map +1 -1
- package/dist/construct/index.d.ts +24 -0
- package/dist/construct/index.js +146 -212
- package/dist/construct/index.js.map +1 -1
- package/dist/index.cjs +157 -213
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +36 -0
- package/dist/index.js +157 -213
- package/dist/index.js.map +1 -1
- package/dist/prep/index.browser.js +132 -211
- package/dist/prep/index.browser.js.map +1 -1
- package/dist/prep/index.browser.min.js +13 -13
- package/dist/prep/index.browser.min.js.map +1 -1
- package/dist/prep/index.cjs +132 -211
- package/dist/prep/index.cjs.map +1 -1
- package/dist/prep/index.d.ts +12 -0
- package/dist/prep/index.js +132 -211
- package/dist/prep/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/components/board/BoardView.tsx +53 -14
- package/src/core/components/board/GameBoard.tsx +123 -7
- package/src/core/config/config.ts +8 -4
- package/src/core/domain/types.ts +1 -0
- package/src/core/io/InteractionTracker.ts +7 -16
- package/src/core/io/data-tracking.ts +3 -7
- package/src/plugins/tangram-construct/ConstructionApp.tsx +16 -1
- package/src/plugins/tangram-construct/index.ts +14 -0
- package/src/plugins/tangram-prep/PrepApp.tsx +6 -0
- package/src/plugins/tangram-prep/index.ts +7 -0
- package/tangram-construct.min.js +11 -11
- package/tangram-prep.min.js +13 -13
package/dist/prep/index.cjs
CHANGED
|
@@ -16,7 +16,8 @@ const CONFIG = {
|
|
|
16
16
|
anchors: { invalid: "#7dd3fc", valid: "#475569" },
|
|
17
17
|
piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
|
|
18
18
|
ui: { light: "#60a5fa", dark: "#1d4ed8" },
|
|
19
|
-
blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" }
|
|
19
|
+
blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
|
|
20
|
+
tangramDecomposition: { stroke: "#fef2cc" }
|
|
20
21
|
},
|
|
21
22
|
opacity: {
|
|
22
23
|
blueprint: 0.4,
|
|
@@ -26,7 +27,7 @@ const CONFIG = {
|
|
|
26
27
|
piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
|
|
27
28
|
},
|
|
28
29
|
size: {
|
|
29
|
-
stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2 },
|
|
30
|
+
stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
|
|
30
31
|
anchorRadiusPx: { valid: 1, invalid: 1 },
|
|
31
32
|
badgeFontPx: 16,
|
|
32
33
|
centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
|
|
@@ -34,6 +35,7 @@ const CONFIG = {
|
|
|
34
35
|
layout: {
|
|
35
36
|
grid: { stepPx: 20, unitPx: 40 },
|
|
36
37
|
paddingPx: 1,
|
|
38
|
+
viewportScale: 0.8,
|
|
37
39
|
constraints: {
|
|
38
40
|
workspaceDiamAnchors: 10,
|
|
39
41
|
// num anchors req'd to be on diagonal
|
|
@@ -45,7 +47,7 @@ const CONFIG = {
|
|
|
45
47
|
},
|
|
46
48
|
game: {
|
|
47
49
|
snapRadiusPx: 15,
|
|
48
|
-
showBorders:
|
|
50
|
+
showBorders: false,
|
|
49
51
|
hideTouchingBorders: true
|
|
50
52
|
}
|
|
51
53
|
};
|
|
@@ -816,150 +818,11 @@ function rectForBand(layout, sector, band, pad = 0.85) {
|
|
|
816
818
|
return { cx, cy, w, h };
|
|
817
819
|
}
|
|
818
820
|
|
|
819
|
-
function
|
|
820
|
-
|
|
821
|
-
for (let i = 0; i < poly.length; i++) {
|
|
822
|
-
const current = poly[i];
|
|
823
|
-
const next = poly[(i + 1) % poly.length];
|
|
824
|
-
if (current && next) {
|
|
825
|
-
strokePaths.push(`M ${current.x} ${current.y} L ${next.x} ${next.y}`);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
return strokePaths;
|
|
829
|
-
}
|
|
830
|
-
function edgeToUnitSegments$1(start, end, gridSize) {
|
|
831
|
-
const segments = [];
|
|
832
|
-
const startGrid = {
|
|
833
|
-
x: Math.round(start.x / gridSize),
|
|
834
|
-
y: Math.round(start.y / gridSize)
|
|
835
|
-
};
|
|
836
|
-
const endGrid = {
|
|
837
|
-
x: Math.round(end.x / gridSize),
|
|
838
|
-
y: Math.round(end.y / gridSize)
|
|
839
|
-
};
|
|
840
|
-
const dx = endGrid.x - startGrid.x;
|
|
841
|
-
const dy = endGrid.y - startGrid.y;
|
|
842
|
-
if (dx === 0 && dy === 0) return [];
|
|
843
|
-
const steps = Math.max(Math.abs(dx), Math.abs(dy));
|
|
844
|
-
const stepX = dx / steps;
|
|
845
|
-
const stepY = dy / steps;
|
|
846
|
-
for (let i = 0; i < steps; i++) {
|
|
847
|
-
const aX = Math.round(startGrid.x + i * stepX);
|
|
848
|
-
const aY = Math.round(startGrid.y + i * stepY);
|
|
849
|
-
const bX = Math.round(startGrid.x + (i + 1) * stepX);
|
|
850
|
-
const bY = Math.round(startGrid.y + (i + 1) * stepY);
|
|
851
|
-
segments.push({
|
|
852
|
-
a: { x: aX, y: aY },
|
|
853
|
-
b: { x: bX, y: bY }
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
return segments;
|
|
857
|
-
}
|
|
858
|
-
function getHiddenEdgesForPolygon(piece, polyIndex, allPiecesInSector, getBlueprint, getPrimitive) {
|
|
859
|
-
let blueprint;
|
|
860
|
-
try {
|
|
861
|
-
blueprint = getBlueprint(piece.blueprintId);
|
|
862
|
-
} catch (error) {
|
|
863
|
-
console.warn("getBlueprint failed in getHiddenEdgesForPolygon:", error);
|
|
864
|
-
return [];
|
|
865
|
-
}
|
|
866
|
-
if (!blueprint?.shape) {
|
|
867
|
-
return [];
|
|
868
|
-
}
|
|
869
|
-
const poly = blueprint.shape[polyIndex];
|
|
870
|
-
if (!poly) return [];
|
|
871
|
-
const gridSize = CONFIG.layout.grid.stepPx;
|
|
872
|
-
const bb = boundsOfBlueprint(blueprint, getPrimitive);
|
|
873
|
-
const ox = piece.pos.x - bb.min.x;
|
|
874
|
-
const oy = piece.pos.y - bb.min.y;
|
|
875
|
-
const translatedPoly = poly.map((vertex) => ({
|
|
876
|
-
x: vertex.x + ox,
|
|
877
|
-
y: vertex.y + oy
|
|
878
|
-
}));
|
|
879
|
-
const hiddenEdges = [];
|
|
880
|
-
for (let i = 0; i < translatedPoly.length; i++) {
|
|
881
|
-
const current = translatedPoly[i];
|
|
882
|
-
const next = translatedPoly[(i + 1) % translatedPoly.length];
|
|
883
|
-
if (!current || !next) {
|
|
884
|
-
hiddenEdges.push(false);
|
|
885
|
-
continue;
|
|
886
|
-
}
|
|
887
|
-
const edgeSegments = edgeToUnitSegments$1(current, next, gridSize);
|
|
888
|
-
let isTouching = false;
|
|
889
|
-
if (piece.blueprintId.startsWith("comp:")) {
|
|
890
|
-
for (let otherPolyIndex = 0; otherPolyIndex < blueprint.shape.length; otherPolyIndex++) {
|
|
891
|
-
if (otherPolyIndex === polyIndex) continue;
|
|
892
|
-
const otherPoly = blueprint.shape[otherPolyIndex];
|
|
893
|
-
if (!otherPoly) continue;
|
|
894
|
-
const otherTranslatedPoly = otherPoly.map((vertex) => ({
|
|
895
|
-
x: vertex.x + ox,
|
|
896
|
-
y: vertex.y + oy
|
|
897
|
-
}));
|
|
898
|
-
for (let j = 0; j < otherTranslatedPoly.length; j++) {
|
|
899
|
-
const otherCurrent = otherTranslatedPoly[j];
|
|
900
|
-
const otherNext = otherTranslatedPoly[(j + 1) % otherTranslatedPoly.length];
|
|
901
|
-
if (!otherCurrent || !otherNext) continue;
|
|
902
|
-
const otherEdgeSegments = edgeToUnitSegments$1(otherCurrent, otherNext, gridSize);
|
|
903
|
-
for (const edgeSeg of edgeSegments) {
|
|
904
|
-
const isShared = otherEdgeSegments.some(
|
|
905
|
-
(otherSeg) => (
|
|
906
|
-
// Check if segments share the same endpoints (identical or reversed)
|
|
907
|
-
edgeSeg.a.x === otherSeg.a.x && edgeSeg.a.y === otherSeg.a.y && edgeSeg.b.x === otherSeg.b.x && edgeSeg.b.y === otherSeg.b.y || edgeSeg.a.x === otherSeg.b.x && edgeSeg.a.y === otherSeg.b.y && edgeSeg.b.x === otherSeg.a.x && edgeSeg.b.y === otherSeg.a.y
|
|
908
|
-
)
|
|
909
|
-
);
|
|
910
|
-
if (isShared) {
|
|
911
|
-
isTouching = true;
|
|
912
|
-
break;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
if (isTouching) break;
|
|
916
|
-
}
|
|
917
|
-
if (isTouching) break;
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
if (!isTouching && CONFIG.game.hideTouchingBorders) {
|
|
921
|
-
for (const otherPiece of allPiecesInSector) {
|
|
922
|
-
if (otherPiece.id === piece.id) continue;
|
|
923
|
-
const otherBlueprint = getBlueprint(otherPiece.blueprintId);
|
|
924
|
-
if (!otherBlueprint?.shape) continue;
|
|
925
|
-
const otherBb = boundsOfBlueprint(otherBlueprint, getPrimitive);
|
|
926
|
-
const otherOx = otherPiece.pos.x - otherBb.min.x;
|
|
927
|
-
const otherOy = otherPiece.pos.y - otherBb.min.y;
|
|
928
|
-
for (const otherPoly of otherBlueprint.shape) {
|
|
929
|
-
const otherTranslatedPoly = otherPoly.map((vertex) => ({
|
|
930
|
-
x: vertex.x + otherOx,
|
|
931
|
-
y: vertex.y + otherOy
|
|
932
|
-
}));
|
|
933
|
-
for (let j = 0; j < otherTranslatedPoly.length; j++) {
|
|
934
|
-
const otherCurrent = otherTranslatedPoly[j];
|
|
935
|
-
const otherNext = otherTranslatedPoly[(j + 1) % otherTranslatedPoly.length];
|
|
936
|
-
if (!otherCurrent || !otherNext) continue;
|
|
937
|
-
const otherEdgeSegments = edgeToUnitSegments$1(otherCurrent, otherNext, gridSize);
|
|
938
|
-
for (const edgeSeg of edgeSegments) {
|
|
939
|
-
const isShared = otherEdgeSegments.some(
|
|
940
|
-
(otherSeg) => (
|
|
941
|
-
// Check if segments share the same endpoints (identical or reversed)
|
|
942
|
-
edgeSeg.a.x === otherSeg.a.x && edgeSeg.a.y === otherSeg.a.y && edgeSeg.b.x === otherSeg.b.x && edgeSeg.b.y === otherSeg.b.y || edgeSeg.a.x === otherSeg.b.x && edgeSeg.a.y === otherSeg.b.y && edgeSeg.b.x === otherSeg.a.x && edgeSeg.b.y === otherSeg.a.y
|
|
943
|
-
)
|
|
944
|
-
);
|
|
945
|
-
if (isShared) {
|
|
946
|
-
isTouching = true;
|
|
947
|
-
break;
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
if (isTouching) break;
|
|
951
|
-
}
|
|
952
|
-
if (isTouching) break;
|
|
953
|
-
}
|
|
954
|
-
if (isTouching) break;
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
hiddenEdges.push(isTouching);
|
|
958
|
-
}
|
|
959
|
-
return hiddenEdges;
|
|
821
|
+
function shouldShowBorders() {
|
|
822
|
+
return CONFIG.game.showBorders;
|
|
960
823
|
}
|
|
961
824
|
function shouldUseSelectiveBorders(blueprintId) {
|
|
962
|
-
return
|
|
825
|
+
return CONFIG.game.showBorders;
|
|
963
826
|
}
|
|
964
827
|
|
|
965
828
|
function pathD(poly) {
|
|
@@ -977,11 +840,13 @@ function BoardView(props) {
|
|
|
977
840
|
placedSilBySector,
|
|
978
841
|
anchorDots,
|
|
979
842
|
pieces,
|
|
843
|
+
scaleS,
|
|
980
844
|
clickMode,
|
|
981
845
|
draggingId,
|
|
982
846
|
selectedPieceId,
|
|
983
847
|
dragInvalid,
|
|
984
848
|
lockedPieceId,
|
|
849
|
+
showTangramDecomposition,
|
|
985
850
|
svgRef,
|
|
986
851
|
setPieceRef,
|
|
987
852
|
onPiecePointerDown,
|
|
@@ -1047,7 +912,7 @@ function BoardView(props) {
|
|
|
1047
912
|
onPointerDown: (e) => {
|
|
1048
913
|
onRootPointerDown(e);
|
|
1049
914
|
},
|
|
1050
|
-
style: { background: "#
|
|
915
|
+
style: { background: "#f5f5f5", touchAction: "none", userSelect: "none" }
|
|
1051
916
|
},
|
|
1052
917
|
layout.sectors.map((s, i) => {
|
|
1053
918
|
const done = !!controller.state.sectors[s.id].completedAt;
|
|
@@ -1067,11 +932,12 @@ function BoardView(props) {
|
|
|
1067
932
|
const isDragging = draggingId === p.id;
|
|
1068
933
|
const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
|
|
1069
934
|
const isConnectivityLocked = lockedPieceId === p.id;
|
|
1070
|
-
|
|
935
|
+
selectedPieceId === p.id;
|
|
1071
936
|
const isCarriedInvalid = isDragging && dragInvalid;
|
|
1072
937
|
const translateX = p.x - bb.min.x;
|
|
1073
938
|
const translateY = p.y - bb.min.y;
|
|
1074
939
|
return /* @__PURE__ */ React.createElement("g", { key: p.id, ref: setPieceRef(p.id), transform: `translate(${translateX}, ${translateY})`, style: { cursor: locked ? "default" : clickMode ? "pointer" : "grab" }, pointerEvents: clickMode && isDragging ? "none" : "auto" }, ("shape" in bp ? bp.shape : []).map((poly, idx) => {
|
|
940
|
+
const showBorders = shouldShowBorders();
|
|
1075
941
|
shouldUseSelectiveBorders(p.blueprintId);
|
|
1076
942
|
return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
|
|
1077
943
|
"path",
|
|
@@ -1082,51 +948,38 @@ function BoardView(props) {
|
|
|
1082
948
|
stroke: "none",
|
|
1083
949
|
onPointerDown: (e) => onPiecePointerDown(e, p)
|
|
1084
950
|
}
|
|
1085
|
-
),
|
|
1086
|
-
// For pieces with selective borders: render individual edge strokes with edge detection
|
|
1087
|
-
(() => {
|
|
1088
|
-
const allPiecesInSector = pieces.filter((piece) => piece.sectorId === p.sectorId);
|
|
1089
|
-
const pieceAsPiece = { ...p, pos: { x: p.x, y: p.y } };
|
|
1090
|
-
const allPiecesAsPieces = allPiecesInSector.map((piece) => ({ ...piece, pos: { x: piece.x, y: piece.y } }));
|
|
1091
|
-
const hiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, allPiecesAsPieces, (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
|
|
1092
|
-
const draggedPiece = draggingId ? allPiecesInSector.find((piece) => piece.id === draggingId) : null;
|
|
1093
|
-
let wasTouchingDraggedPiece;
|
|
1094
|
-
if (p.blueprintId.startsWith("comp:")) {
|
|
1095
|
-
const internalHiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, [], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
|
|
1096
|
-
const externalHiddenEdges = draggedPiece ? getHiddenEdgesForPolygon(pieceAsPiece, idx, [{ ...draggedPiece, pos: { x: draggedPiece.x, y: draggedPiece.y } }], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind)) : new Array(hiddenEdges.length).fill(false);
|
|
1097
|
-
wasTouchingDraggedPiece = externalHiddenEdges.map((external, i) => external && !internalHiddenEdges[i]);
|
|
1098
|
-
} else {
|
|
1099
|
-
wasTouchingDraggedPiece = draggedPiece ? getHiddenEdgesForPolygon(pieceAsPiece, idx, [{ ...draggedPiece, pos: { x: draggedPiece.x, y: draggedPiece.y } }], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind)) : new Array(hiddenEdges.length).fill(false);
|
|
1100
|
-
}
|
|
1101
|
-
return generateEdgeStrokePaths(poly).map((strokePath, strokeIdx) => {
|
|
1102
|
-
const wasHiddenDueToDraggedPiece = wasTouchingDraggedPiece[strokeIdx] || false;
|
|
1103
|
-
let isHidden;
|
|
1104
|
-
if (isDragging && p.blueprintId.startsWith("comp:")) {
|
|
1105
|
-
const internalHiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, [], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
|
|
1106
|
-
isHidden = internalHiddenEdges[strokeIdx] || false;
|
|
1107
|
-
} else {
|
|
1108
|
-
isHidden = isDragging ? false : (hiddenEdges[strokeIdx] || false) && !wasHiddenDueToDraggedPiece;
|
|
1109
|
-
}
|
|
1110
|
-
return /* @__PURE__ */ React.createElement(
|
|
1111
|
-
"path",
|
|
1112
|
-
{
|
|
1113
|
-
key: `stroke-${idx}-${strokeIdx}`,
|
|
1114
|
-
d: strokePath,
|
|
1115
|
-
fill: "none",
|
|
1116
|
-
stroke: isHidden ? "none" : isConnectivityLocked ? CONFIG.color.piece.invalidStroke : isCarriedInvalid ? CONFIG.color.piece.invalidStroke : isSelected || isDragging ? CONFIG.color.piece.selectedStroke : CONFIG.color.piece.borderStroke,
|
|
1117
|
-
strokeWidth: isHidden ? 0 : isSelected || isDragging ? CONFIG.size.stroke.pieceSelectedPx : CONFIG.size.stroke.pieceBorderPx,
|
|
1118
|
-
onPointerDown: (e) => onPiecePointerDown(e, p)
|
|
1119
|
-
}
|
|
1120
|
-
);
|
|
1121
|
-
});
|
|
1122
|
-
})()
|
|
1123
|
-
) ));
|
|
951
|
+
), showBorders);
|
|
1124
952
|
}));
|
|
1125
953
|
}),
|
|
1126
954
|
layout.sectors.map((s) => {
|
|
1127
|
-
const
|
|
1128
|
-
if (
|
|
1129
|
-
|
|
955
|
+
const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
|
|
956
|
+
if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
|
|
957
|
+
const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
|
|
958
|
+
const rect = rectForBand(layout, s, "silhouette", 1);
|
|
959
|
+
const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
|
|
960
|
+
const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
|
|
961
|
+
return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(React.Fragment, { key: `prim-${i}` }, /* @__PURE__ */ React.createElement(
|
|
962
|
+
"path",
|
|
963
|
+
{
|
|
964
|
+
d: pathD(scaledPoly),
|
|
965
|
+
fill: CONFIG.color.silhouetteMask,
|
|
966
|
+
opacity: CONFIG.opacity.silhouetteMask,
|
|
967
|
+
stroke: "none"
|
|
968
|
+
}
|
|
969
|
+
), /* @__PURE__ */ React.createElement(
|
|
970
|
+
"path",
|
|
971
|
+
{
|
|
972
|
+
d: pathD(scaledPoly),
|
|
973
|
+
fill: "none",
|
|
974
|
+
stroke: CONFIG.color.tangramDecomposition.stroke,
|
|
975
|
+
strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
|
|
976
|
+
}
|
|
977
|
+
))));
|
|
978
|
+
} else {
|
|
979
|
+
const placedPolys = placedSilBySector.get(s.id) ?? [];
|
|
980
|
+
if (!placedPolys.length) return null;
|
|
981
|
+
return /* @__PURE__ */ React.createElement("g", { key: `sil-${s.id}`, pointerEvents: "none" }, placedPolys.map((poly, i) => /* @__PURE__ */ React.createElement("path", { key: i, d: pathD(poly), fill: CONFIG.color.silhouetteMask, opacity: CONFIG.opacity.silhouetteMask })));
|
|
982
|
+
}
|
|
1130
983
|
}),
|
|
1131
984
|
anchorDots.map(({ sectorId, valid, invalid }) => {
|
|
1132
985
|
const isInnerRing = sectorId === "inner-ring";
|
|
@@ -2588,7 +2441,7 @@ function useClickController(controller, layout, pieces, clickMode, draggingId, s
|
|
|
2588
2441
|
}
|
|
2589
2442
|
|
|
2590
2443
|
class InteractionTracker {
|
|
2591
|
-
constructor(controller, callbacks,
|
|
2444
|
+
constructor(controller, callbacks, trialParams) {
|
|
2592
2445
|
this.gridStep = CONFIG.layout.grid.stepPx;
|
|
2593
2446
|
// Interaction state
|
|
2594
2447
|
this.interactionIndex = 0;
|
|
@@ -2605,8 +2458,7 @@ class InteractionTracker {
|
|
|
2605
2458
|
this.createdMacros = [];
|
|
2606
2459
|
this.controller = controller;
|
|
2607
2460
|
this.callbacks = callbacks;
|
|
2608
|
-
this.
|
|
2609
|
-
this.gameId = gameId || uuid.v4();
|
|
2461
|
+
this.trialParams = trialParams;
|
|
2610
2462
|
this.trialStartTime = Date.now();
|
|
2611
2463
|
this.controller.setTrackingCallbacks({
|
|
2612
2464
|
onSectorCompleted: (sectorId) => this.recordSectorCompletion(sectorId)
|
|
@@ -2654,8 +2506,6 @@ class InteractionTracker {
|
|
|
2654
2506
|
const event = {
|
|
2655
2507
|
// Metadata
|
|
2656
2508
|
interactionId: uuid.v4(),
|
|
2657
|
-
trialId: this.trialId,
|
|
2658
|
-
gameId: this.gameId,
|
|
2659
2509
|
interactionIndex: this.interactionIndex++,
|
|
2660
2510
|
// Interaction type
|
|
2661
2511
|
interactionType,
|
|
@@ -2772,13 +2622,10 @@ class InteractionTracker {
|
|
|
2772
2622
|
}));
|
|
2773
2623
|
const data = {
|
|
2774
2624
|
trialType: "construction",
|
|
2775
|
-
trialId: this.trialId,
|
|
2776
|
-
gameId: this.gameId,
|
|
2777
|
-
trialNum: 0,
|
|
2778
|
-
// TODO: Plugin should provide this
|
|
2779
2625
|
trialStartTime: this.trialStartTime,
|
|
2780
2626
|
trialEndTime,
|
|
2781
2627
|
totalDuration,
|
|
2628
|
+
trialParams: this.trialParams,
|
|
2782
2629
|
endReason,
|
|
2783
2630
|
completionTimes: this.completionTimes,
|
|
2784
2631
|
finalBlueprintState,
|
|
@@ -2797,13 +2644,10 @@ class InteractionTracker {
|
|
|
2797
2644
|
}));
|
|
2798
2645
|
const data = {
|
|
2799
2646
|
trialType: "prep",
|
|
2800
|
-
trialId: this.trialId,
|
|
2801
|
-
gameId: this.gameId,
|
|
2802
|
-
trialNum: 0,
|
|
2803
|
-
// TODO: Plugin should provide this
|
|
2804
2647
|
trialStartTime: this.trialStartTime,
|
|
2805
2648
|
trialEndTime,
|
|
2806
2649
|
totalDuration,
|
|
2650
|
+
trialParams: this.trialParams,
|
|
2807
2651
|
endReason: "submit",
|
|
2808
2652
|
createdMacros: finalMacros,
|
|
2809
2653
|
quickstashMacros,
|
|
@@ -3061,6 +2905,9 @@ function GameBoard(props) {
|
|
|
3061
2905
|
maxQuickstashSlots,
|
|
3062
2906
|
maxCompositeSize,
|
|
3063
2907
|
mode,
|
|
2908
|
+
showTangramDecomposition,
|
|
2909
|
+
instructions,
|
|
2910
|
+
trialParams,
|
|
3064
2911
|
width: _width,
|
|
3065
2912
|
height: _height,
|
|
3066
2913
|
onSectorComplete,
|
|
@@ -3070,6 +2917,7 @@ function GameBoard(props) {
|
|
|
3070
2917
|
onTrialEnd,
|
|
3071
2918
|
onControllerReady
|
|
3072
2919
|
} = props;
|
|
2920
|
+
const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1e3) : 0);
|
|
3073
2921
|
const controller = React.useMemo(() => {
|
|
3074
2922
|
const gameConfig = {
|
|
3075
2923
|
n: sectors.length,
|
|
@@ -3096,8 +2944,8 @@ function GameBoard(props) {
|
|
|
3096
2944
|
const callbacks = {};
|
|
3097
2945
|
if (onInteraction) callbacks.onInteraction = onInteraction;
|
|
3098
2946
|
if (onTrialEnd) callbacks.onTrialEnd = onTrialEnd;
|
|
3099
|
-
return new InteractionTracker(controller, callbacks);
|
|
3100
|
-
}, [controller, onInteraction, onTrialEnd]);
|
|
2947
|
+
return new InteractionTracker(controller, callbacks, trialParams);
|
|
2948
|
+
}, [controller, onInteraction, onTrialEnd, trialParams]);
|
|
3101
2949
|
React.useEffect(() => {
|
|
3102
2950
|
if (onControllerReady) {
|
|
3103
2951
|
onControllerReady(controller);
|
|
@@ -3177,22 +3025,22 @@ function GameBoard(props) {
|
|
|
3177
3025
|
}), [handleSectorComplete, onPiecePlace, onPieceRemove]);
|
|
3178
3026
|
const getGameboardStyle = () => {
|
|
3179
3027
|
const baseStyle = {
|
|
3180
|
-
margin: "
|
|
3028
|
+
margin: "10px",
|
|
3181
3029
|
display: "flex",
|
|
3182
3030
|
alignItems: "center",
|
|
3183
3031
|
justifyContent: "center",
|
|
3184
3032
|
position: "relative"
|
|
3185
3033
|
};
|
|
3186
3034
|
if (layoutMode === "circle") {
|
|
3187
|
-
const size = Math.min(window.innerWidth *
|
|
3035
|
+
const size = Math.min(window.innerWidth * CONFIG.layout.viewportScale, window.innerHeight * CONFIG.layout.viewportScale);
|
|
3188
3036
|
return {
|
|
3189
3037
|
...baseStyle,
|
|
3190
3038
|
width: `${size}px`,
|
|
3191
3039
|
height: `${size}px`
|
|
3192
3040
|
};
|
|
3193
3041
|
} else {
|
|
3194
|
-
const maxWidth = Math.min(window.innerWidth *
|
|
3195
|
-
const maxHeight = Math.min(window.innerWidth *
|
|
3042
|
+
const maxWidth = Math.min(window.innerWidth * CONFIG.layout.viewportScale, window.innerHeight * CONFIG.layout.viewportScale * 2);
|
|
3043
|
+
const maxHeight = Math.min(window.innerWidth * CONFIG.layout.viewportScale / 2, window.innerHeight * CONFIG.layout.viewportScale);
|
|
3196
3044
|
return {
|
|
3197
3045
|
...baseStyle,
|
|
3198
3046
|
width: `${maxWidth}px`,
|
|
@@ -3361,7 +3209,68 @@ function GameBoard(props) {
|
|
|
3361
3209
|
force();
|
|
3362
3210
|
e.stopPropagation();
|
|
3363
3211
|
};
|
|
3364
|
-
|
|
3212
|
+
React.useEffect(() => {
|
|
3213
|
+
if (timeLimitMs === 0) return;
|
|
3214
|
+
const interval = setInterval(() => {
|
|
3215
|
+
setTimeRemaining((prev) => {
|
|
3216
|
+
if (prev <= 1) {
|
|
3217
|
+
clearInterval(interval);
|
|
3218
|
+
return 0;
|
|
3219
|
+
}
|
|
3220
|
+
return prev - 1;
|
|
3221
|
+
});
|
|
3222
|
+
}, 1e3);
|
|
3223
|
+
return () => clearInterval(interval);
|
|
3224
|
+
}, [timeLimitMs]);
|
|
3225
|
+
const formatTime = (seconds) => {
|
|
3226
|
+
const mins = Math.floor(seconds / 60);
|
|
3227
|
+
const secs = Math.floor(seconds % 60);
|
|
3228
|
+
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
3229
|
+
};
|
|
3230
|
+
const containerStyle = {
|
|
3231
|
+
display: "flex",
|
|
3232
|
+
flexDirection: "column",
|
|
3233
|
+
width: "100%"
|
|
3234
|
+
// minHeight: '100vh'
|
|
3235
|
+
};
|
|
3236
|
+
const headerStyle = {
|
|
3237
|
+
display: "flex",
|
|
3238
|
+
flexDirection: "row",
|
|
3239
|
+
justifyContent: "space-between",
|
|
3240
|
+
alignItems: "center",
|
|
3241
|
+
padding: "20px",
|
|
3242
|
+
background: "#f5f5f5",
|
|
3243
|
+
flex: "0 0 auto"
|
|
3244
|
+
};
|
|
3245
|
+
const instructionsStyle = {
|
|
3246
|
+
flexGrow: 1,
|
|
3247
|
+
fontSize: "20px",
|
|
3248
|
+
lineHeight: 1.5,
|
|
3249
|
+
marginRight: "20px"
|
|
3250
|
+
};
|
|
3251
|
+
const timerStyle = {
|
|
3252
|
+
fontSize: "24px",
|
|
3253
|
+
fontWeight: "bold",
|
|
3254
|
+
fontFamily: "monospace",
|
|
3255
|
+
color: "#333",
|
|
3256
|
+
minWidth: "80px",
|
|
3257
|
+
textAlign: "right"
|
|
3258
|
+
};
|
|
3259
|
+
const gameboardWrapperStyle = {
|
|
3260
|
+
flex: "1 1 auto",
|
|
3261
|
+
display: "flex",
|
|
3262
|
+
alignItems: "center",
|
|
3263
|
+
justifyContent: "center",
|
|
3264
|
+
overflow: "hidden"
|
|
3265
|
+
};
|
|
3266
|
+
return /* @__PURE__ */ React.createElement("div", { className: "tangram-trial-container", style: containerStyle }, (instructions || timeLimitMs > 0) && /* @__PURE__ */ React.createElement("div", { className: "tangram-header", style: headerStyle }, instructions && /* @__PURE__ */ React.createElement(
|
|
3267
|
+
"div",
|
|
3268
|
+
{
|
|
3269
|
+
className: "tangram-instructions",
|
|
3270
|
+
style: instructionsStyle,
|
|
3271
|
+
dangerouslySetInnerHTML: { __html: instructions }
|
|
3272
|
+
}
|
|
3273
|
+
), timeLimitMs > 0 && /* @__PURE__ */ React.createElement("div", { className: "tangram-timer", style: timerStyle }, formatTime(timeRemaining))), /* @__PURE__ */ React.createElement("div", { className: "tangram-gameboard-wrapper", style: gameboardWrapperStyle }, /* @__PURE__ */ React.createElement("div", { className: "tangram-gameboard", style: getGameboardStyle() }, /* @__PURE__ */ React.createElement(
|
|
3365
3274
|
BoardView,
|
|
3366
3275
|
{
|
|
3367
3276
|
controller,
|
|
@@ -3387,9 +3296,11 @@ function GameBoard(props) {
|
|
|
3387
3296
|
onPointerMove,
|
|
3388
3297
|
onPointerUp,
|
|
3389
3298
|
onCenterBadgePointerDown,
|
|
3299
|
+
showTangramDecomposition: showTangramDecomposition ?? false,
|
|
3300
|
+
scaleS,
|
|
3390
3301
|
...eventCallbacks
|
|
3391
3302
|
}
|
|
3392
|
-
));
|
|
3303
|
+
))));
|
|
3393
3304
|
}
|
|
3394
3305
|
|
|
3395
3306
|
const U = 40;
|
|
@@ -3555,8 +3466,9 @@ function startPrepTrial(display_element, params, jsPsych) {
|
|
|
3555
3466
|
onInteraction,
|
|
3556
3467
|
onTrialEnd
|
|
3557
3468
|
} = params;
|
|
3469
|
+
const { onInteraction: _, onTrialEnd: __, ...trialParams } = params;
|
|
3558
3470
|
const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => primitiveOrder.indexOf(a.kind) - primitiveOrder.indexOf(b.kind));
|
|
3559
|
-
const prepSectors = Array.from({ length: numQuickstashSlots }, (
|
|
3471
|
+
const prepSectors = Array.from({ length: numQuickstashSlots }, (_2, i) => ({
|
|
3560
3472
|
id: `prep-sector-${i}`,
|
|
3561
3473
|
tangramId: `prep-sector-${i}`,
|
|
3562
3474
|
// dummy value since prep mode doesn't have tangrams
|
|
@@ -3640,6 +3552,8 @@ function startPrepTrial(display_element, params, jsPsych) {
|
|
|
3640
3552
|
// Enable prep-specific behavior
|
|
3641
3553
|
minPiecesPerMacro,
|
|
3642
3554
|
requireAllSlots,
|
|
3555
|
+
trialParams,
|
|
3556
|
+
...params.instructions && { instructions: params.instructions },
|
|
3643
3557
|
onControllerReady: handleControllerReady,
|
|
3644
3558
|
...onInteraction && { onInteraction },
|
|
3645
3559
|
...onTrialEnd && { onTrialEnd }
|
|
@@ -3693,6 +3607,12 @@ const info = {
|
|
|
3693
3607
|
default: ["square", "smalltriangle", "parallelogram", "medtriangle", "largetriangle"],
|
|
3694
3608
|
description: "Array of primitive names in the order they should be displayed"
|
|
3695
3609
|
},
|
|
3610
|
+
/** HTML content to display above the gameboard as instructions */
|
|
3611
|
+
instructions: {
|
|
3612
|
+
type: jspsych.ParameterType.STRING,
|
|
3613
|
+
default: void 0,
|
|
3614
|
+
description: "HTML content to display above the gameboard as instructions"
|
|
3615
|
+
},
|
|
3696
3616
|
/** Callback fired after each interaction (optional analytics hook) */
|
|
3697
3617
|
onInteraction: {
|
|
3698
3618
|
type: jspsych.ParameterType.FUNCTION,
|
|
@@ -3744,6 +3664,7 @@ class TangramPrepPlugin {
|
|
|
3744
3664
|
requireAllSlots: trial.require_all_slots,
|
|
3745
3665
|
quickstashMacros: trial.quickstash_macros,
|
|
3746
3666
|
primitiveOrder: trial.primitive_order,
|
|
3667
|
+
instructions: trial.instructions,
|
|
3747
3668
|
onInteraction: trial.onInteraction,
|
|
3748
3669
|
onTrialEnd: wrappedOnTrialEnd
|
|
3749
3670
|
};
|