jspsych-tangram 0.0.6 → 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 +157 -222
- 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 +157 -222
- package/dist/construct/index.cjs.map +1 -1
- package/dist/construct/index.d.ts +24 -0
- package/dist/construct/index.js +157 -222
- package/dist/construct/index.js.map +1 -1
- package/dist/index.cjs +168 -223
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +36 -0
- package/dist/index.js +168 -223
- package/dist/index.js.map +1 -1
- package/dist/prep/index.browser.js +143 -221
- 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 +143 -221
- package/dist/prep/index.cjs.map +1 -1
- package/dist/prep/index.d.ts +12 -0
- package/dist/prep/index.js +143 -221
- 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 +19 -14
- 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
|
@@ -16661,31 +16661,34 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
16661
16661
|
const CONFIG = {
|
|
16662
16662
|
color: {
|
|
16663
16663
|
bands: {
|
|
16664
|
-
silhouette: { fillEven: "#
|
|
16665
|
-
workspace: { fillEven: "#
|
|
16664
|
+
silhouette: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" },
|
|
16665
|
+
workspace: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" }
|
|
16666
16666
|
},
|
|
16667
|
-
completion: { fill: "#
|
|
16668
|
-
silhouetteMask: "#
|
|
16667
|
+
completion: { fill: "#ccfff2", stroke: "#13da57" },
|
|
16668
|
+
silhouetteMask: "#374151",
|
|
16669
16669
|
anchors: { invalid: "#7dd3fc", valid: "#475569" },
|
|
16670
|
-
piece: { draggingFill: "#
|
|
16670
|
+
piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
|
|
16671
16671
|
ui: { light: "#60a5fa", dark: "#1d4ed8" },
|
|
16672
|
-
blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#
|
|
16672
|
+
blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
|
|
16673
|
+
tangramDecomposition: { stroke: "#fef2cc" }
|
|
16673
16674
|
},
|
|
16674
16675
|
opacity: {
|
|
16675
|
-
blueprint: 0.
|
|
16676
|
-
silhouetteMask: 0.
|
|
16677
|
-
anchors: { valid: 0.
|
|
16678
|
-
|
|
16676
|
+
blueprint: 0.4,
|
|
16677
|
+
silhouetteMask: 0.25,
|
|
16678
|
+
//anchors: { valid: 0.80, invalid: 0.50 },
|
|
16679
|
+
anchors: { invalid: 0, valid: 0 },
|
|
16680
|
+
piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
|
|
16679
16681
|
},
|
|
16680
16682
|
size: {
|
|
16681
|
-
stroke: { bandPx:
|
|
16683
|
+
stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
|
|
16682
16684
|
anchorRadiusPx: { valid: 1, invalid: 1 },
|
|
16683
|
-
badgeFontPx:
|
|
16685
|
+
badgeFontPx: 16,
|
|
16684
16686
|
centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
|
|
16685
16687
|
},
|
|
16686
16688
|
layout: {
|
|
16687
16689
|
grid: { stepPx: 20, unitPx: 40 },
|
|
16688
16690
|
paddingPx: 1,
|
|
16691
|
+
viewportScale: 0.8,
|
|
16689
16692
|
constraints: {
|
|
16690
16693
|
workspaceDiamAnchors: 10,
|
|
16691
16694
|
// num anchors req'd to be on diagonal
|
|
@@ -16697,7 +16700,7 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
16697
16700
|
},
|
|
16698
16701
|
game: {
|
|
16699
16702
|
snapRadiusPx: 15,
|
|
16700
|
-
showBorders:
|
|
16703
|
+
showBorders: false,
|
|
16701
16704
|
hideTouchingBorders: true
|
|
16702
16705
|
}
|
|
16703
16706
|
};
|
|
@@ -17468,150 +17471,11 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
17468
17471
|
return { cx, cy, w, h };
|
|
17469
17472
|
}
|
|
17470
17473
|
|
|
17471
|
-
function
|
|
17472
|
-
|
|
17473
|
-
for (let i = 0; i < poly.length; i++) {
|
|
17474
|
-
const current = poly[i];
|
|
17475
|
-
const next = poly[(i + 1) % poly.length];
|
|
17476
|
-
if (current && next) {
|
|
17477
|
-
strokePaths.push(`M ${current.x} ${current.y} L ${next.x} ${next.y}`);
|
|
17478
|
-
}
|
|
17479
|
-
}
|
|
17480
|
-
return strokePaths;
|
|
17481
|
-
}
|
|
17482
|
-
function edgeToUnitSegments$1(start, end, gridSize) {
|
|
17483
|
-
const segments = [];
|
|
17484
|
-
const startGrid = {
|
|
17485
|
-
x: Math.round(start.x / gridSize),
|
|
17486
|
-
y: Math.round(start.y / gridSize)
|
|
17487
|
-
};
|
|
17488
|
-
const endGrid = {
|
|
17489
|
-
x: Math.round(end.x / gridSize),
|
|
17490
|
-
y: Math.round(end.y / gridSize)
|
|
17491
|
-
};
|
|
17492
|
-
const dx = endGrid.x - startGrid.x;
|
|
17493
|
-
const dy = endGrid.y - startGrid.y;
|
|
17494
|
-
if (dx === 0 && dy === 0) return [];
|
|
17495
|
-
const steps = Math.max(Math.abs(dx), Math.abs(dy));
|
|
17496
|
-
const stepX = dx / steps;
|
|
17497
|
-
const stepY = dy / steps;
|
|
17498
|
-
for (let i = 0; i < steps; i++) {
|
|
17499
|
-
const aX = Math.round(startGrid.x + i * stepX);
|
|
17500
|
-
const aY = Math.round(startGrid.y + i * stepY);
|
|
17501
|
-
const bX = Math.round(startGrid.x + (i + 1) * stepX);
|
|
17502
|
-
const bY = Math.round(startGrid.y + (i + 1) * stepY);
|
|
17503
|
-
segments.push({
|
|
17504
|
-
a: { x: aX, y: aY },
|
|
17505
|
-
b: { x: bX, y: bY }
|
|
17506
|
-
});
|
|
17507
|
-
}
|
|
17508
|
-
return segments;
|
|
17509
|
-
}
|
|
17510
|
-
function getHiddenEdgesForPolygon(piece, polyIndex, allPiecesInSector, getBlueprint, getPrimitive) {
|
|
17511
|
-
let blueprint;
|
|
17512
|
-
try {
|
|
17513
|
-
blueprint = getBlueprint(piece.blueprintId);
|
|
17514
|
-
} catch (error) {
|
|
17515
|
-
console.warn("getBlueprint failed in getHiddenEdgesForPolygon:", error);
|
|
17516
|
-
return [];
|
|
17517
|
-
}
|
|
17518
|
-
if (!blueprint?.shape) {
|
|
17519
|
-
return [];
|
|
17520
|
-
}
|
|
17521
|
-
const poly = blueprint.shape[polyIndex];
|
|
17522
|
-
if (!poly) return [];
|
|
17523
|
-
const gridSize = CONFIG.layout.grid.stepPx;
|
|
17524
|
-
const bb = boundsOfBlueprint(blueprint, getPrimitive);
|
|
17525
|
-
const ox = piece.pos.x - bb.min.x;
|
|
17526
|
-
const oy = piece.pos.y - bb.min.y;
|
|
17527
|
-
const translatedPoly = poly.map((vertex) => ({
|
|
17528
|
-
x: vertex.x + ox,
|
|
17529
|
-
y: vertex.y + oy
|
|
17530
|
-
}));
|
|
17531
|
-
const hiddenEdges = [];
|
|
17532
|
-
for (let i = 0; i < translatedPoly.length; i++) {
|
|
17533
|
-
const current = translatedPoly[i];
|
|
17534
|
-
const next = translatedPoly[(i + 1) % translatedPoly.length];
|
|
17535
|
-
if (!current || !next) {
|
|
17536
|
-
hiddenEdges.push(false);
|
|
17537
|
-
continue;
|
|
17538
|
-
}
|
|
17539
|
-
const edgeSegments = edgeToUnitSegments$1(current, next, gridSize);
|
|
17540
|
-
let isTouching = false;
|
|
17541
|
-
if (piece.blueprintId.startsWith("comp:")) {
|
|
17542
|
-
for (let otherPolyIndex = 0; otherPolyIndex < blueprint.shape.length; otherPolyIndex++) {
|
|
17543
|
-
if (otherPolyIndex === polyIndex) continue;
|
|
17544
|
-
const otherPoly = blueprint.shape[otherPolyIndex];
|
|
17545
|
-
if (!otherPoly) continue;
|
|
17546
|
-
const otherTranslatedPoly = otherPoly.map((vertex) => ({
|
|
17547
|
-
x: vertex.x + ox,
|
|
17548
|
-
y: vertex.y + oy
|
|
17549
|
-
}));
|
|
17550
|
-
for (let j = 0; j < otherTranslatedPoly.length; j++) {
|
|
17551
|
-
const otherCurrent = otherTranslatedPoly[j];
|
|
17552
|
-
const otherNext = otherTranslatedPoly[(j + 1) % otherTranslatedPoly.length];
|
|
17553
|
-
if (!otherCurrent || !otherNext) continue;
|
|
17554
|
-
const otherEdgeSegments = edgeToUnitSegments$1(otherCurrent, otherNext, gridSize);
|
|
17555
|
-
for (const edgeSeg of edgeSegments) {
|
|
17556
|
-
const isShared = otherEdgeSegments.some(
|
|
17557
|
-
(otherSeg) => (
|
|
17558
|
-
// Check if segments share the same endpoints (identical or reversed)
|
|
17559
|
-
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
|
|
17560
|
-
)
|
|
17561
|
-
);
|
|
17562
|
-
if (isShared) {
|
|
17563
|
-
isTouching = true;
|
|
17564
|
-
break;
|
|
17565
|
-
}
|
|
17566
|
-
}
|
|
17567
|
-
if (isTouching) break;
|
|
17568
|
-
}
|
|
17569
|
-
if (isTouching) break;
|
|
17570
|
-
}
|
|
17571
|
-
}
|
|
17572
|
-
if (!isTouching && CONFIG.game.hideTouchingBorders) {
|
|
17573
|
-
for (const otherPiece of allPiecesInSector) {
|
|
17574
|
-
if (otherPiece.id === piece.id) continue;
|
|
17575
|
-
const otherBlueprint = getBlueprint(otherPiece.blueprintId);
|
|
17576
|
-
if (!otherBlueprint?.shape) continue;
|
|
17577
|
-
const otherBb = boundsOfBlueprint(otherBlueprint, getPrimitive);
|
|
17578
|
-
const otherOx = otherPiece.pos.x - otherBb.min.x;
|
|
17579
|
-
const otherOy = otherPiece.pos.y - otherBb.min.y;
|
|
17580
|
-
for (const otherPoly of otherBlueprint.shape) {
|
|
17581
|
-
const otherTranslatedPoly = otherPoly.map((vertex) => ({
|
|
17582
|
-
x: vertex.x + otherOx,
|
|
17583
|
-
y: vertex.y + otherOy
|
|
17584
|
-
}));
|
|
17585
|
-
for (let j = 0; j < otherTranslatedPoly.length; j++) {
|
|
17586
|
-
const otherCurrent = otherTranslatedPoly[j];
|
|
17587
|
-
const otherNext = otherTranslatedPoly[(j + 1) % otherTranslatedPoly.length];
|
|
17588
|
-
if (!otherCurrent || !otherNext) continue;
|
|
17589
|
-
const otherEdgeSegments = edgeToUnitSegments$1(otherCurrent, otherNext, gridSize);
|
|
17590
|
-
for (const edgeSeg of edgeSegments) {
|
|
17591
|
-
const isShared = otherEdgeSegments.some(
|
|
17592
|
-
(otherSeg) => (
|
|
17593
|
-
// Check if segments share the same endpoints (identical or reversed)
|
|
17594
|
-
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
|
|
17595
|
-
)
|
|
17596
|
-
);
|
|
17597
|
-
if (isShared) {
|
|
17598
|
-
isTouching = true;
|
|
17599
|
-
break;
|
|
17600
|
-
}
|
|
17601
|
-
}
|
|
17602
|
-
if (isTouching) break;
|
|
17603
|
-
}
|
|
17604
|
-
if (isTouching) break;
|
|
17605
|
-
}
|
|
17606
|
-
if (isTouching) break;
|
|
17607
|
-
}
|
|
17608
|
-
}
|
|
17609
|
-
hiddenEdges.push(isTouching);
|
|
17610
|
-
}
|
|
17611
|
-
return hiddenEdges;
|
|
17474
|
+
function shouldShowBorders() {
|
|
17475
|
+
return CONFIG.game.showBorders;
|
|
17612
17476
|
}
|
|
17613
17477
|
function shouldUseSelectiveBorders(blueprintId) {
|
|
17614
|
-
return
|
|
17478
|
+
return CONFIG.game.showBorders;
|
|
17615
17479
|
}
|
|
17616
17480
|
|
|
17617
17481
|
function pathD(poly) {
|
|
@@ -17629,11 +17493,13 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
17629
17493
|
placedSilBySector,
|
|
17630
17494
|
anchorDots,
|
|
17631
17495
|
pieces,
|
|
17496
|
+
scaleS,
|
|
17632
17497
|
clickMode,
|
|
17633
17498
|
draggingId,
|
|
17634
17499
|
selectedPieceId,
|
|
17635
17500
|
dragInvalid,
|
|
17636
17501
|
lockedPieceId,
|
|
17502
|
+
showTangramDecomposition,
|
|
17637
17503
|
svgRef,
|
|
17638
17504
|
setPieceRef,
|
|
17639
17505
|
onPiecePointerDown,
|
|
@@ -17699,7 +17565,7 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
17699
17565
|
onPointerDown: (e) => {
|
|
17700
17566
|
onRootPointerDown(e);
|
|
17701
17567
|
},
|
|
17702
|
-
style: { background: "#
|
|
17568
|
+
style: { background: "#f5f5f5", touchAction: "none", userSelect: "none" }
|
|
17703
17569
|
},
|
|
17704
17570
|
layout.sectors.map((s, i) => {
|
|
17705
17571
|
const done = !!controller.state.sectors[s.id].completedAt;
|
|
@@ -17719,11 +17585,12 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
17719
17585
|
const isDragging = draggingId === p.id;
|
|
17720
17586
|
const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
|
|
17721
17587
|
const isConnectivityLocked = lockedPieceId === p.id;
|
|
17722
|
-
|
|
17588
|
+
selectedPieceId === p.id;
|
|
17723
17589
|
const isCarriedInvalid = isDragging && dragInvalid;
|
|
17724
17590
|
const translateX = p.x - bb.min.x;
|
|
17725
17591
|
const translateY = p.y - bb.min.y;
|
|
17726
17592
|
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) => {
|
|
17593
|
+
const showBorders = shouldShowBorders();
|
|
17727
17594
|
shouldUseSelectiveBorders(p.blueprintId);
|
|
17728
17595
|
return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
|
|
17729
17596
|
"path",
|
|
@@ -17734,51 +17601,38 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
17734
17601
|
stroke: "none",
|
|
17735
17602
|
onPointerDown: (e) => onPiecePointerDown(e, p)
|
|
17736
17603
|
}
|
|
17737
|
-
),
|
|
17738
|
-
// For pieces with selective borders: render individual edge strokes with edge detection
|
|
17739
|
-
(() => {
|
|
17740
|
-
const allPiecesInSector = pieces.filter((piece) => piece.sectorId === p.sectorId);
|
|
17741
|
-
const pieceAsPiece = { ...p, pos: { x: p.x, y: p.y } };
|
|
17742
|
-
const allPiecesAsPieces = allPiecesInSector.map((piece) => ({ ...piece, pos: { x: piece.x, y: piece.y } }));
|
|
17743
|
-
const hiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, allPiecesAsPieces, (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
|
|
17744
|
-
const draggedPiece = draggingId ? allPiecesInSector.find((piece) => piece.id === draggingId) : null;
|
|
17745
|
-
let wasTouchingDraggedPiece;
|
|
17746
|
-
if (p.blueprintId.startsWith("comp:")) {
|
|
17747
|
-
const internalHiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, [], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
|
|
17748
|
-
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);
|
|
17749
|
-
wasTouchingDraggedPiece = externalHiddenEdges.map((external, i) => external && !internalHiddenEdges[i]);
|
|
17750
|
-
} else {
|
|
17751
|
-
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);
|
|
17752
|
-
}
|
|
17753
|
-
return generateEdgeStrokePaths(poly).map((strokePath, strokeIdx) => {
|
|
17754
|
-
const wasHiddenDueToDraggedPiece = wasTouchingDraggedPiece[strokeIdx] || false;
|
|
17755
|
-
let isHidden;
|
|
17756
|
-
if (isDragging && p.blueprintId.startsWith("comp:")) {
|
|
17757
|
-
const internalHiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, [], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
|
|
17758
|
-
isHidden = internalHiddenEdges[strokeIdx] || false;
|
|
17759
|
-
} else {
|
|
17760
|
-
isHidden = isDragging ? false : (hiddenEdges[strokeIdx] || false) && !wasHiddenDueToDraggedPiece;
|
|
17761
|
-
}
|
|
17762
|
-
return /* @__PURE__ */ React.createElement(
|
|
17763
|
-
"path",
|
|
17764
|
-
{
|
|
17765
|
-
key: `stroke-${idx}-${strokeIdx}`,
|
|
17766
|
-
d: strokePath,
|
|
17767
|
-
fill: "none",
|
|
17768
|
-
stroke: isHidden ? "none" : isConnectivityLocked ? CONFIG.color.piece.invalidStroke : isCarriedInvalid ? CONFIG.color.piece.invalidStroke : isSelected || isDragging ? CONFIG.color.piece.selectedStroke : CONFIG.color.piece.borderStroke,
|
|
17769
|
-
strokeWidth: isHidden ? 0 : isSelected || isDragging ? CONFIG.size.stroke.pieceSelectedPx : CONFIG.size.stroke.pieceBorderPx,
|
|
17770
|
-
onPointerDown: (e) => onPiecePointerDown(e, p)
|
|
17771
|
-
}
|
|
17772
|
-
);
|
|
17773
|
-
});
|
|
17774
|
-
})()
|
|
17775
|
-
) ));
|
|
17604
|
+
), showBorders);
|
|
17776
17605
|
}));
|
|
17777
17606
|
}),
|
|
17778
17607
|
layout.sectors.map((s) => {
|
|
17779
|
-
const
|
|
17780
|
-
if (
|
|
17781
|
-
|
|
17608
|
+
const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
|
|
17609
|
+
if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
|
|
17610
|
+
const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
|
|
17611
|
+
const rect = rectForBand(layout, s, "silhouette", 1);
|
|
17612
|
+
const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
|
|
17613
|
+
const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
|
|
17614
|
+
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(
|
|
17615
|
+
"path",
|
|
17616
|
+
{
|
|
17617
|
+
d: pathD(scaledPoly),
|
|
17618
|
+
fill: CONFIG.color.silhouetteMask,
|
|
17619
|
+
opacity: CONFIG.opacity.silhouetteMask,
|
|
17620
|
+
stroke: "none"
|
|
17621
|
+
}
|
|
17622
|
+
), /* @__PURE__ */ React.createElement(
|
|
17623
|
+
"path",
|
|
17624
|
+
{
|
|
17625
|
+
d: pathD(scaledPoly),
|
|
17626
|
+
fill: "none",
|
|
17627
|
+
stroke: CONFIG.color.tangramDecomposition.stroke,
|
|
17628
|
+
strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
|
|
17629
|
+
}
|
|
17630
|
+
))));
|
|
17631
|
+
} else {
|
|
17632
|
+
const placedPolys = placedSilBySector.get(s.id) ?? [];
|
|
17633
|
+
if (!placedPolys.length) return null;
|
|
17634
|
+
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 })));
|
|
17635
|
+
}
|
|
17782
17636
|
}),
|
|
17783
17637
|
anchorDots.map(({ sectorId, valid, invalid }) => {
|
|
17784
17638
|
const isInnerRing = sectorId === "inner-ring";
|
|
@@ -19296,7 +19150,7 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
19296
19150
|
}
|
|
19297
19151
|
|
|
19298
19152
|
class InteractionTracker {
|
|
19299
|
-
constructor(controller, callbacks,
|
|
19153
|
+
constructor(controller, callbacks, trialParams) {
|
|
19300
19154
|
this.gridStep = CONFIG.layout.grid.stepPx;
|
|
19301
19155
|
// Interaction state
|
|
19302
19156
|
this.interactionIndex = 0;
|
|
@@ -19313,8 +19167,7 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
19313
19167
|
this.createdMacros = [];
|
|
19314
19168
|
this.controller = controller;
|
|
19315
19169
|
this.callbacks = callbacks;
|
|
19316
|
-
this.
|
|
19317
|
-
this.gameId = gameId || v4();
|
|
19170
|
+
this.trialParams = trialParams;
|
|
19318
19171
|
this.trialStartTime = Date.now();
|
|
19319
19172
|
this.controller.setTrackingCallbacks({
|
|
19320
19173
|
onSectorCompleted: (sectorId) => this.recordSectorCompletion(sectorId)
|
|
@@ -19362,8 +19215,6 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
19362
19215
|
const event = {
|
|
19363
19216
|
// Metadata
|
|
19364
19217
|
interactionId: v4(),
|
|
19365
|
-
trialId: this.trialId,
|
|
19366
|
-
gameId: this.gameId,
|
|
19367
19218
|
interactionIndex: this.interactionIndex++,
|
|
19368
19219
|
// Interaction type
|
|
19369
19220
|
interactionType,
|
|
@@ -19480,13 +19331,10 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
19480
19331
|
}));
|
|
19481
19332
|
const data = {
|
|
19482
19333
|
trialType: "construction",
|
|
19483
|
-
trialId: this.trialId,
|
|
19484
|
-
gameId: this.gameId,
|
|
19485
|
-
trialNum: 0,
|
|
19486
|
-
// TODO: Plugin should provide this
|
|
19487
19334
|
trialStartTime: this.trialStartTime,
|
|
19488
19335
|
trialEndTime,
|
|
19489
19336
|
totalDuration,
|
|
19337
|
+
trialParams: this.trialParams,
|
|
19490
19338
|
endReason,
|
|
19491
19339
|
completionTimes: this.completionTimes,
|
|
19492
19340
|
finalBlueprintState,
|
|
@@ -19505,13 +19353,10 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
19505
19353
|
}));
|
|
19506
19354
|
const data = {
|
|
19507
19355
|
trialType: "prep",
|
|
19508
|
-
trialId: this.trialId,
|
|
19509
|
-
gameId: this.gameId,
|
|
19510
|
-
trialNum: 0,
|
|
19511
|
-
// TODO: Plugin should provide this
|
|
19512
19356
|
trialStartTime: this.trialStartTime,
|
|
19513
19357
|
trialEndTime,
|
|
19514
19358
|
totalDuration,
|
|
19359
|
+
trialParams: this.trialParams,
|
|
19515
19360
|
endReason: "submit",
|
|
19516
19361
|
createdMacros: finalMacros,
|
|
19517
19362
|
quickstashMacros,
|
|
@@ -19769,6 +19614,9 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
19769
19614
|
maxQuickstashSlots,
|
|
19770
19615
|
maxCompositeSize,
|
|
19771
19616
|
mode,
|
|
19617
|
+
showTangramDecomposition,
|
|
19618
|
+
instructions,
|
|
19619
|
+
trialParams,
|
|
19772
19620
|
width: _width,
|
|
19773
19621
|
height: _height,
|
|
19774
19622
|
onSectorComplete,
|
|
@@ -19778,6 +19626,7 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
19778
19626
|
onTrialEnd,
|
|
19779
19627
|
onControllerReady
|
|
19780
19628
|
} = props;
|
|
19629
|
+
const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1e3) : 0);
|
|
19781
19630
|
const controller = React.useMemo(() => {
|
|
19782
19631
|
const gameConfig = {
|
|
19783
19632
|
n: sectors.length,
|
|
@@ -19804,8 +19653,8 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
19804
19653
|
const callbacks = {};
|
|
19805
19654
|
if (onInteraction) callbacks.onInteraction = onInteraction;
|
|
19806
19655
|
if (onTrialEnd) callbacks.onTrialEnd = onTrialEnd;
|
|
19807
|
-
return new InteractionTracker(controller, callbacks);
|
|
19808
|
-
}, [controller, onInteraction, onTrialEnd]);
|
|
19656
|
+
return new InteractionTracker(controller, callbacks, trialParams);
|
|
19657
|
+
}, [controller, onInteraction, onTrialEnd, trialParams]);
|
|
19809
19658
|
React.useEffect(() => {
|
|
19810
19659
|
if (onControllerReady) {
|
|
19811
19660
|
onControllerReady(controller);
|
|
@@ -19885,22 +19734,22 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
19885
19734
|
}), [handleSectorComplete, onPiecePlace, onPieceRemove]);
|
|
19886
19735
|
const getGameboardStyle = () => {
|
|
19887
19736
|
const baseStyle = {
|
|
19888
|
-
margin: "
|
|
19737
|
+
margin: "10px",
|
|
19889
19738
|
display: "flex",
|
|
19890
19739
|
alignItems: "center",
|
|
19891
19740
|
justifyContent: "center",
|
|
19892
19741
|
position: "relative"
|
|
19893
19742
|
};
|
|
19894
19743
|
if (layoutMode === "circle") {
|
|
19895
|
-
const size = Math.min(window.innerWidth *
|
|
19744
|
+
const size = Math.min(window.innerWidth * CONFIG.layout.viewportScale, window.innerHeight * CONFIG.layout.viewportScale);
|
|
19896
19745
|
return {
|
|
19897
19746
|
...baseStyle,
|
|
19898
19747
|
width: `${size}px`,
|
|
19899
19748
|
height: `${size}px`
|
|
19900
19749
|
};
|
|
19901
19750
|
} else {
|
|
19902
|
-
const maxWidth = Math.min(window.innerWidth *
|
|
19903
|
-
const maxHeight = Math.min(window.innerWidth *
|
|
19751
|
+
const maxWidth = Math.min(window.innerWidth * CONFIG.layout.viewportScale, window.innerHeight * CONFIG.layout.viewportScale * 2);
|
|
19752
|
+
const maxHeight = Math.min(window.innerWidth * CONFIG.layout.viewportScale / 2, window.innerHeight * CONFIG.layout.viewportScale);
|
|
19904
19753
|
return {
|
|
19905
19754
|
...baseStyle,
|
|
19906
19755
|
width: `${maxWidth}px`,
|
|
@@ -20069,7 +19918,68 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
20069
19918
|
force();
|
|
20070
19919
|
e.stopPropagation();
|
|
20071
19920
|
};
|
|
20072
|
-
|
|
19921
|
+
React.useEffect(() => {
|
|
19922
|
+
if (timeLimitMs === 0) return;
|
|
19923
|
+
const interval = setInterval(() => {
|
|
19924
|
+
setTimeRemaining((prev) => {
|
|
19925
|
+
if (prev <= 1) {
|
|
19926
|
+
clearInterval(interval);
|
|
19927
|
+
return 0;
|
|
19928
|
+
}
|
|
19929
|
+
return prev - 1;
|
|
19930
|
+
});
|
|
19931
|
+
}, 1e3);
|
|
19932
|
+
return () => clearInterval(interval);
|
|
19933
|
+
}, [timeLimitMs]);
|
|
19934
|
+
const formatTime = (seconds) => {
|
|
19935
|
+
const mins = Math.floor(seconds / 60);
|
|
19936
|
+
const secs = Math.floor(seconds % 60);
|
|
19937
|
+
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
19938
|
+
};
|
|
19939
|
+
const containerStyle = {
|
|
19940
|
+
display: "flex",
|
|
19941
|
+
flexDirection: "column",
|
|
19942
|
+
width: "100%"
|
|
19943
|
+
// minHeight: '100vh'
|
|
19944
|
+
};
|
|
19945
|
+
const headerStyle = {
|
|
19946
|
+
display: "flex",
|
|
19947
|
+
flexDirection: "row",
|
|
19948
|
+
justifyContent: "space-between",
|
|
19949
|
+
alignItems: "center",
|
|
19950
|
+
padding: "20px",
|
|
19951
|
+
background: "#f5f5f5",
|
|
19952
|
+
flex: "0 0 auto"
|
|
19953
|
+
};
|
|
19954
|
+
const instructionsStyle = {
|
|
19955
|
+
flexGrow: 1,
|
|
19956
|
+
fontSize: "20px",
|
|
19957
|
+
lineHeight: 1.5,
|
|
19958
|
+
marginRight: "20px"
|
|
19959
|
+
};
|
|
19960
|
+
const timerStyle = {
|
|
19961
|
+
fontSize: "24px",
|
|
19962
|
+
fontWeight: "bold",
|
|
19963
|
+
fontFamily: "monospace",
|
|
19964
|
+
color: "#333",
|
|
19965
|
+
minWidth: "80px",
|
|
19966
|
+
textAlign: "right"
|
|
19967
|
+
};
|
|
19968
|
+
const gameboardWrapperStyle = {
|
|
19969
|
+
flex: "1 1 auto",
|
|
19970
|
+
display: "flex",
|
|
19971
|
+
alignItems: "center",
|
|
19972
|
+
justifyContent: "center",
|
|
19973
|
+
overflow: "hidden"
|
|
19974
|
+
};
|
|
19975
|
+
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(
|
|
19976
|
+
"div",
|
|
19977
|
+
{
|
|
19978
|
+
className: "tangram-instructions",
|
|
19979
|
+
style: instructionsStyle,
|
|
19980
|
+
dangerouslySetInnerHTML: { __html: instructions }
|
|
19981
|
+
}
|
|
19982
|
+
), 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(
|
|
20073
19983
|
BoardView,
|
|
20074
19984
|
{
|
|
20075
19985
|
controller,
|
|
@@ -20095,9 +20005,11 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
20095
20005
|
onPointerMove,
|
|
20096
20006
|
onPointerUp,
|
|
20097
20007
|
onCenterBadgePointerDown,
|
|
20008
|
+
showTangramDecomposition: showTangramDecomposition ?? false,
|
|
20009
|
+
scaleS,
|
|
20098
20010
|
...eventCallbacks
|
|
20099
20011
|
}
|
|
20100
|
-
));
|
|
20012
|
+
))));
|
|
20101
20013
|
}
|
|
20102
20014
|
|
|
20103
20015
|
const U = 40;
|
|
@@ -20263,8 +20175,9 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
20263
20175
|
onInteraction,
|
|
20264
20176
|
onTrialEnd
|
|
20265
20177
|
} = params;
|
|
20178
|
+
const { onInteraction: _, onTrialEnd: __, ...trialParams } = params;
|
|
20266
20179
|
const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => primitiveOrder.indexOf(a.kind) - primitiveOrder.indexOf(b.kind));
|
|
20267
|
-
const prepSectors = Array.from({ length: numQuickstashSlots }, (
|
|
20180
|
+
const prepSectors = Array.from({ length: numQuickstashSlots }, (_2, i) => ({
|
|
20268
20181
|
id: `prep-sector-${i}`,
|
|
20269
20182
|
tangramId: `prep-sector-${i}`,
|
|
20270
20183
|
// dummy value since prep mode doesn't have tangrams
|
|
@@ -20348,6 +20261,8 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
20348
20261
|
// Enable prep-specific behavior
|
|
20349
20262
|
minPiecesPerMacro,
|
|
20350
20263
|
requireAllSlots,
|
|
20264
|
+
trialParams,
|
|
20265
|
+
...params.instructions && { instructions: params.instructions },
|
|
20351
20266
|
onControllerReady: handleControllerReady,
|
|
20352
20267
|
...onInteraction && { onInteraction },
|
|
20353
20268
|
...onTrialEnd && { onTrialEnd }
|
|
@@ -20401,6 +20316,12 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
20401
20316
|
default: ["square", "smalltriangle", "parallelogram", "medtriangle", "largetriangle"],
|
|
20402
20317
|
description: "Array of primitive names in the order they should be displayed"
|
|
20403
20318
|
},
|
|
20319
|
+
/** HTML content to display above the gameboard as instructions */
|
|
20320
|
+
instructions: {
|
|
20321
|
+
type: jspsych.ParameterType.STRING,
|
|
20322
|
+
default: void 0,
|
|
20323
|
+
description: "HTML content to display above the gameboard as instructions"
|
|
20324
|
+
},
|
|
20404
20325
|
/** Callback fired after each interaction (optional analytics hook) */
|
|
20405
20326
|
onInteraction: {
|
|
20406
20327
|
type: jspsych.ParameterType.FUNCTION,
|
|
@@ -20452,6 +20373,7 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
20452
20373
|
requireAllSlots: trial.require_all_slots,
|
|
20453
20374
|
quickstashMacros: trial.quickstash_macros,
|
|
20454
20375
|
primitiveOrder: trial.primitive_order,
|
|
20376
|
+
instructions: trial.instructions,
|
|
20455
20377
|
onInteraction: trial.onInteraction,
|
|
20456
20378
|
onTrialEnd: wrappedOnTrialEnd
|
|
20457
20379
|
};
|