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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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;
|
|
@@ -20265,17 +20177,22 @@ var TangramConstructPlugin = (function (jspsych) {
|
|
|
20265
20177
|
const isCanonical = CANON.has(tanName);
|
|
20266
20178
|
return isCanonical;
|
|
20267
20179
|
});
|
|
20268
|
-
const mask = filteredTans.map((tan
|
|
20180
|
+
const mask = filteredTans.map((tan) => {
|
|
20269
20181
|
const polygon = tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }));
|
|
20270
20182
|
return polygon;
|
|
20271
20183
|
});
|
|
20184
|
+
const primitiveDecomposition = filteredTans.map((tan) => ({
|
|
20185
|
+
kind: tan.name ?? tan.kind,
|
|
20186
|
+
polygon: tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }))
|
|
20187
|
+
}));
|
|
20272
20188
|
const sectorId = `sector${index}`;
|
|
20273
20189
|
const sector = {
|
|
20274
20190
|
id: sectorId,
|
|
20275
20191
|
tangramId: tangramSpec.tangramID,
|
|
20276
20192
|
silhouette: {
|
|
20277
20193
|
id: sectorId,
|
|
20278
|
-
mask
|
|
20194
|
+
mask,
|
|
20195
|
+
primitiveDecomposition
|
|
20279
20196
|
}
|
|
20280
20197
|
};
|
|
20281
20198
|
return sector;
|
|
@@ -20294,6 +20211,7 @@ var TangramConstructPlugin = (function (jspsych) {
|
|
|
20294
20211
|
quickstash = params.quickstash_macros;
|
|
20295
20212
|
}
|
|
20296
20213
|
}
|
|
20214
|
+
const { onInteraction, onTrialEnd, ...trialParams } = params;
|
|
20297
20215
|
const gameBoardProps = {
|
|
20298
20216
|
sectors,
|
|
20299
20217
|
quickstash,
|
|
@@ -20304,6 +20222,9 @@ var TangramConstructPlugin = (function (jspsych) {
|
|
|
20304
20222
|
timeLimitMs: params.time_limit_ms,
|
|
20305
20223
|
maxQuickstashSlots: CONFIG.layout.defaults.maxQuickstashSlots,
|
|
20306
20224
|
mode: "construction",
|
|
20225
|
+
showTangramDecomposition: params.show_tangram_decomposition ?? false,
|
|
20226
|
+
trialParams,
|
|
20227
|
+
...params.instructions && { instructions: params.instructions },
|
|
20307
20228
|
...params.onInteraction && { onInteraction: params.onInteraction },
|
|
20308
20229
|
...params.onTrialEnd && { onTrialEnd: params.onTrialEnd }
|
|
20309
20230
|
};
|
|
@@ -20367,6 +20288,18 @@ var TangramConstructPlugin = (function (jspsych) {
|
|
|
20367
20288
|
default: 18,
|
|
20368
20289
|
description: "Snap radius for anchor-based piece placement"
|
|
20369
20290
|
},
|
|
20291
|
+
/** Whether to show tangram target shapes decomposed into individual primitives with borders */
|
|
20292
|
+
show_tangram_decomposition: {
|
|
20293
|
+
type: jspsych.ParameterType.BOOL,
|
|
20294
|
+
default: false,
|
|
20295
|
+
description: "Whether to show tangram target shapes decomposed into individual primitives with borders"
|
|
20296
|
+
},
|
|
20297
|
+
/** HTML content to display above the gameboard as instructions */
|
|
20298
|
+
instructions: {
|
|
20299
|
+
type: jspsych.ParameterType.STRING,
|
|
20300
|
+
default: "",
|
|
20301
|
+
description: "HTML content to display above the gameboard as instructions"
|
|
20302
|
+
},
|
|
20370
20303
|
/** Callback fired after each interaction (piece pickup + placedown) */
|
|
20371
20304
|
onInteraction: {
|
|
20372
20305
|
type: jspsych.ParameterType.FUNCTION,
|
|
@@ -20440,6 +20373,8 @@ var TangramConstructPlugin = (function (jspsych) {
|
|
|
20440
20373
|
input: trial.input,
|
|
20441
20374
|
layout: trial.layout,
|
|
20442
20375
|
time_limit_ms: trial.time_limit_ms,
|
|
20376
|
+
show_tangram_decomposition: trial.show_tangram_decomposition,
|
|
20377
|
+
instructions: trial.instructions,
|
|
20443
20378
|
onInteraction: trial.onInteraction,
|
|
20444
20379
|
onTrialEnd: wrappedOnTrialEnd
|
|
20445
20380
|
};
|