jspsych-tangram 0.0.12 → 0.0.14

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.
Files changed (49) hide show
  1. package/dist/construct/index.browser.js +283 -64
  2. package/dist/construct/index.browser.js.map +1 -1
  3. package/dist/construct/index.browser.min.js +11 -11
  4. package/dist/construct/index.browser.min.js.map +1 -1
  5. package/dist/construct/index.cjs +283 -64
  6. package/dist/construct/index.cjs.map +1 -1
  7. package/dist/construct/index.d.ts +36 -0
  8. package/dist/construct/index.js +283 -64
  9. package/dist/construct/index.js.map +1 -1
  10. package/dist/index.cjs +394 -94
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +84 -0
  13. package/dist/index.js +394 -94
  14. package/dist/index.js.map +1 -1
  15. package/dist/nback/index.browser.js +112 -37
  16. package/dist/nback/index.browser.js.map +1 -1
  17. package/dist/nback/index.browser.min.js +11 -11
  18. package/dist/nback/index.browser.min.js.map +1 -1
  19. package/dist/nback/index.cjs +112 -37
  20. package/dist/nback/index.cjs.map +1 -1
  21. package/dist/nback/index.d.ts +24 -0
  22. package/dist/nback/index.js +112 -37
  23. package/dist/nback/index.js.map +1 -1
  24. package/dist/prep/index.browser.js +278 -65
  25. package/dist/prep/index.browser.js.map +1 -1
  26. package/dist/prep/index.browser.min.js +11 -11
  27. package/dist/prep/index.browser.min.js.map +1 -1
  28. package/dist/prep/index.cjs +278 -65
  29. package/dist/prep/index.cjs.map +1 -1
  30. package/dist/prep/index.d.ts +24 -0
  31. package/dist/prep/index.js +278 -65
  32. package/dist/prep/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/core/components/board/BoardView.tsx +372 -124
  35. package/src/core/components/board/GameBoard.tsx +39 -44
  36. package/src/core/components/board/useGameBoard.ts +52 -0
  37. package/src/core/components/index.ts +4 -2
  38. package/src/core/components/pieces/BlueprintRing.tsx +100 -67
  39. package/src/core/components/pieces/useBlueprintRing.ts +39 -0
  40. package/src/core/config/config.ts +25 -10
  41. package/src/plugins/tangram-construct/ConstructionApp.tsx +7 -1
  42. package/src/plugins/tangram-construct/index.ts +22 -1
  43. package/src/plugins/tangram-nback/NBackApp.tsx +88 -29
  44. package/src/plugins/tangram-nback/index.ts +14 -0
  45. package/src/plugins/tangram-prep/PrepApp.tsx +7 -1
  46. package/src/plugins/tangram-prep/index.ts +14 -0
  47. package/tangram-construct.min.js +11 -11
  48. package/tangram-nback.min.js +11 -11
  49. package/tangram-prep.min.js +11 -11
@@ -16660,30 +16660,41 @@ var TangramConstructPlugin = (function (jspsych) {
16660
16660
 
16661
16661
  const CONFIG = {
16662
16662
  color: {
16663
+ background: "#fff7e0ff",
16663
16664
  bands: {
16664
- silhouette: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" },
16665
- workspace: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" }
16665
+ silhouette: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" },
16666
+ workspace: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" }
16666
16667
  },
16667
- completion: { fill: "#ccfff2", stroke: "#13da57" },
16668
+ completion: { fill: "#ccffcc", stroke: "#13da57" },
16668
16669
  silhouetteMask: "#374151",
16669
16670
  anchors: { invalid: "#7dd3fc", valid: "#475569" },
16670
- piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
16671
+ // validFill used here for placed composites
16672
+ piece: { draggingFill: "#8e7cc3", validFill: "#8e7cc3", invalidFill: "#d55c00", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
16671
16673
  ui: { light: "#60a5fa", dark: "#1d4ed8" },
16672
16674
  blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
16673
- tangramDecomposition: { stroke: "#fef2cc" }
16675
+ tangramDecomposition: { stroke: "#fef2cc" },
16676
+ primitiveColors: [
16677
+ // from seaborn "colorblind" palette, 6 colors, with red omitted
16678
+ "#0173b2",
16679
+ "#de8f05",
16680
+ "#029e73",
16681
+ "#cc78bc",
16682
+ "#ca9161"
16683
+ ]
16674
16684
  },
16675
16685
  opacity: {
16676
- blueprint: 0.4,
16686
+ blueprint: 0.6,
16677
16687
  silhouetteMask: 0.25,
16678
16688
  //anchors: { valid: 0.80, invalid: 0.50 },
16679
16689
  anchors: { invalid: 0, valid: 0 },
16680
- piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
16690
+ piece: { invalid: 1, dragging: 1, locked: 1, normal: 1 }
16681
16691
  },
16682
16692
  size: {
16683
- stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
16693
+ stroke: { bandPx: 5, pieceSelectedPx: 5, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
16684
16694
  anchorRadiusPx: { valid: 1, invalid: 1 },
16685
16695
  badgeFontPx: 16,
16686
- centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
16696
+ centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 },
16697
+ invalidMarker: { sizePx: 10, strokePx: 4 }
16687
16698
  },
16688
16699
  layout: {
16689
16700
  grid: { stepPx: 20, unitPx: 40 },
@@ -16701,7 +16712,8 @@ var TangramConstructPlugin = (function (jspsych) {
16701
16712
  game: {
16702
16713
  snapRadiusPx: 15,
16703
16714
  showBorders: false,
16704
- hideTouchingBorders: true
16715
+ hideTouchingBorders: true,
16716
+ silhouettesBelowPieces: true
16705
16717
  }
16706
16718
  };
16707
16719
 
@@ -17485,6 +17497,31 @@ var TangramConstructPlugin = (function (jspsych) {
17485
17497
  function pathD(poly) {
17486
17498
  return `M ${poly.map((pt) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
17487
17499
  }
17500
+ function getPieceColor(blueprint, usePrimitiveColors, defaultColor, primitiveColorIndices) {
17501
+ if (!usePrimitiveColors) {
17502
+ return defaultColor;
17503
+ }
17504
+ if ("kind" in blueprint) {
17505
+ const kind = blueprint.kind;
17506
+ const kindToIndex = {
17507
+ "square": 0,
17508
+ "smalltriangle": 1,
17509
+ "parallelogram": 2,
17510
+ "medtriangle": 3,
17511
+ "largetriangle": 4
17512
+ };
17513
+ const primitiveIndex = kindToIndex[kind];
17514
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
17515
+ const colorIndex = primitiveColorIndices[primitiveIndex];
17516
+ const color = CONFIG.color.primitiveColors[colorIndex];
17517
+ if (color) {
17518
+ return color;
17519
+ }
17520
+ }
17521
+ return defaultColor;
17522
+ }
17523
+ return defaultColor;
17524
+ }
17488
17525
  function BoardView(props) {
17489
17526
  const {
17490
17527
  controller,
@@ -17504,6 +17541,9 @@ var TangramConstructPlugin = (function (jspsych) {
17504
17541
  dragInvalid,
17505
17542
  lockedPieceId,
17506
17543
  showTangramDecomposition,
17544
+ usePrimitiveColorsBlueprints,
17545
+ usePrimitiveColorsTargets,
17546
+ primitiveColorIndices,
17507
17547
  svgRef,
17508
17548
  setPieceRef,
17509
17549
  onPiecePointerDown,
@@ -17514,6 +17554,144 @@ var TangramConstructPlugin = (function (jspsych) {
17514
17554
  onCenterBadgePointerDown
17515
17555
  } = props;
17516
17556
  const VW = viewBox.w, VH = viewBox.h;
17557
+ const renderSilhouettes = () => layout.sectors.map((s) => {
17558
+ const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
17559
+ if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
17560
+ const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
17561
+ const rect = rectForBand(layout, s, "silhouette", 1);
17562
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
17563
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
17564
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
17565
+ const primInfo = primitiveDecomposition[i];
17566
+ let fillColor = CONFIG.color.silhouetteMask;
17567
+ if (usePrimitiveColorsTargets && primInfo?.kind && primitiveColorIndices) {
17568
+ const kindToIndex = {
17569
+ "square": 0,
17570
+ "smalltriangle": 1,
17571
+ "parallelogram": 2,
17572
+ "medtriangle": 3,
17573
+ "largetriangle": 4
17574
+ };
17575
+ const primitiveIndex = kindToIndex[primInfo.kind];
17576
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
17577
+ const colorIndex = primitiveColorIndices[primitiveIndex];
17578
+ const color = CONFIG.color.primitiveColors[colorIndex];
17579
+ if (color) {
17580
+ fillColor = color;
17581
+ }
17582
+ }
17583
+ }
17584
+ return /* @__PURE__ */ React.createElement(
17585
+ "path",
17586
+ {
17587
+ key: `prim-fill-${i}`,
17588
+ d: pathD(scaledPoly),
17589
+ fill: fillColor,
17590
+ opacity: CONFIG.opacity.silhouetteMask,
17591
+ stroke: "none"
17592
+ }
17593
+ );
17594
+ }));
17595
+ } else {
17596
+ const placedPolys = placedSilBySector.get(s.id) ?? [];
17597
+ if (!placedPolys.length) return null;
17598
+ 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 })));
17599
+ }
17600
+ });
17601
+ const renderPieces = (piecesFilter) => {
17602
+ const piecesToRender = pieces;
17603
+ return piecesToRender.sort((a, b) => {
17604
+ if (draggingId === a.id) return 1;
17605
+ if (draggingId === b.id) return -1;
17606
+ return 0;
17607
+ }).map((p) => {
17608
+ const bp = controller.getBlueprint(p.blueprintId);
17609
+ const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
17610
+ const isDragging = draggingId === p.id;
17611
+ const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
17612
+ const isConnectivityLocked = lockedPieceId === p.id;
17613
+ selectedPieceId === p.id;
17614
+ const isCarriedInvalid = isDragging && dragInvalid;
17615
+ const translateX = p.x - bb.min.x;
17616
+ const translateY = p.y - bb.min.y;
17617
+ const validFillColor = getPieceColor(
17618
+ bp,
17619
+ usePrimitiveColorsBlueprints || false,
17620
+ CONFIG.color.piece.validFill,
17621
+ primitiveColorIndices || [0, 1, 2, 3, 4]
17622
+ );
17623
+ const draggingFillColor = getPieceColor(
17624
+ bp,
17625
+ usePrimitiveColorsBlueprints || false,
17626
+ CONFIG.color.piece.draggingFill,
17627
+ primitiveColorIndices || [0, 1, 2, 3, 4]
17628
+ );
17629
+ 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) => {
17630
+ const showBorders = shouldShowBorders();
17631
+ shouldUseSelectiveBorders(p.blueprintId);
17632
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
17633
+ "path",
17634
+ {
17635
+ d: pathD(poly),
17636
+ fill: isConnectivityLocked ? CONFIG.color.piece.invalidFill : isCarriedInvalid ? CONFIG.color.piece.invalidFill : isDragging ? draggingFillColor : validFillColor,
17637
+ opacity: isConnectivityLocked ? CONFIG.opacity.piece.invalid : isCarriedInvalid ? CONFIG.opacity.piece.invalid : isDragging ? CONFIG.opacity.piece.dragging : locked ? CONFIG.opacity.piece.locked : CONFIG.opacity.piece.normal,
17638
+ stroke: "none",
17639
+ onPointerDown: (e) => onPiecePointerDown(e, p)
17640
+ }
17641
+ ), showBorders);
17642
+ }), isDragging && isCarriedInvalid && "shape" in bp && bp.shape.length > 0 && (() => {
17643
+ const { cx, cy } = polysAABB$1(bp.shape);
17644
+ const size = CONFIG.size.invalidMarker.sizePx;
17645
+ const strokeWidth = CONFIG.size.invalidMarker.strokePx;
17646
+ const borderWidth = strokeWidth + 2;
17647
+ return /* @__PURE__ */ React.createElement("g", { key: "invalid-marker" }, /* @__PURE__ */ React.createElement(
17648
+ "line",
17649
+ {
17650
+ x1: cx - size,
17651
+ y1: cy - size,
17652
+ x2: cx + size,
17653
+ y2: cy + size,
17654
+ stroke: "white",
17655
+ strokeWidth: borderWidth,
17656
+ strokeLinecap: "round"
17657
+ }
17658
+ ), /* @__PURE__ */ React.createElement(
17659
+ "line",
17660
+ {
17661
+ x1: cx - size,
17662
+ y1: cy - size,
17663
+ x2: cx + size,
17664
+ y2: cy + size,
17665
+ stroke: CONFIG.color.piece.invalidStroke,
17666
+ strokeWidth,
17667
+ strokeLinecap: "round"
17668
+ }
17669
+ ), /* @__PURE__ */ React.createElement(
17670
+ "line",
17671
+ {
17672
+ x1: cx + size,
17673
+ y1: cy - size,
17674
+ x2: cx - size,
17675
+ y2: cy + size,
17676
+ stroke: "white",
17677
+ strokeWidth: borderWidth,
17678
+ strokeLinecap: "round"
17679
+ }
17680
+ ), /* @__PURE__ */ React.createElement(
17681
+ "line",
17682
+ {
17683
+ x1: cx + size,
17684
+ y1: cy - size,
17685
+ x2: cx - size,
17686
+ y2: cy + size,
17687
+ stroke: CONFIG.color.piece.invalidStroke,
17688
+ strokeWidth,
17689
+ strokeLinecap: "round"
17690
+ }
17691
+ ));
17692
+ })());
17693
+ });
17694
+ };
17517
17695
  const centerView = controller.state.blueprintView;
17518
17696
  const bps = centerView === "primitives" ? controller.state.primitives : controller.state.quickstash;
17519
17697
  const QS_SLOTS = controller.state.cfg.maxQuickstashSlots;
@@ -17534,6 +17712,12 @@ var TangramConstructPlugin = (function (jspsych) {
17534
17712
  const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
17535
17713
  const cx = bb.min.x + bb.width / 2;
17536
17714
  const cy = bb.min.y + bb.height / 2;
17715
+ const fillColor = getPieceColor(
17716
+ bp,
17717
+ usePrimitiveColorsBlueprints || false,
17718
+ CONFIG.color.blueprint.fill,
17719
+ primitiveColorIndices || [0, 1, 2, 3, 4]
17720
+ );
17537
17721
  return /* @__PURE__ */ React.createElement(
17538
17722
  "g",
17539
17723
  {
@@ -17545,7 +17729,7 @@ var TangramConstructPlugin = (function (jspsych) {
17545
17729
  {
17546
17730
  key: idx,
17547
17731
  d: pathD(poly),
17548
- fill: CONFIG.color.blueprint.fill,
17732
+ fill: fillColor,
17549
17733
  opacity: CONFIG.opacity.blueprint,
17550
17734
  stroke: "none",
17551
17735
  strokeWidth: 0,
@@ -17569,7 +17753,7 @@ var TangramConstructPlugin = (function (jspsych) {
17569
17753
  onPointerDown: (e) => {
17570
17754
  onRootPointerDown(e);
17571
17755
  },
17572
- style: { background: "#f5f5f5", touchAction: "none", userSelect: "none" }
17756
+ style: { background: CONFIG.color.background, touchAction: "none", userSelect: "none" }
17573
17757
  },
17574
17758
  layout.sectors.map((s, i) => {
17575
17759
  const done = !!controller.state.sectors[s.id].completedAt;
@@ -17579,35 +17763,7 @@ var TangramConstructPlugin = (function (jspsych) {
17579
17763
  const work = layout.bands.workspace;
17580
17764
  return /* @__PURE__ */ React.createElement("g", { key: `bands-${s.id}` }, controller.state.cfg.target === "workspace" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("path", { d: wedgePath(layout.cx, layout.cy, sil[0], sil[1], s.start, s.end), fill: baseSil, stroke: CONFIG.color.bands.silhouette.stroke, strokeWidth: CONFIG.size.stroke.bandPx, pointerEvents: "none" }), /* @__PURE__ */ React.createElement("path", { d: wedgePath(layout.cx, layout.cy, work[0], work[1], s.start, s.end), fill: done ? CONFIG.color.completion.fill : baseWork, stroke: done ? CONFIG.color.completion.stroke : CONFIG.color.bands.workspace.stroke, strokeWidth: CONFIG.size.stroke.bandPx, pointerEvents: "none" })) : /* @__PURE__ */ React.createElement("path", { d: wedgePath(layout.cx, layout.cy, sil[0], sil[1], s.start, s.end), fill: done ? CONFIG.color.completion.fill : baseSil, stroke: done ? CONFIG.color.completion.stroke : CONFIG.color.bands.silhouette.stroke, strokeWidth: CONFIG.size.stroke.bandPx, pointerEvents: "none" }));
17581
17765
  }),
17582
- pieces.sort((a, b) => {
17583
- if (draggingId === a.id) return 1;
17584
- if (draggingId === b.id) return -1;
17585
- return 0;
17586
- }).map((p) => {
17587
- const bp = controller.getBlueprint(p.blueprintId);
17588
- const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
17589
- const isDragging = draggingId === p.id;
17590
- const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
17591
- const isConnectivityLocked = lockedPieceId === p.id;
17592
- selectedPieceId === p.id;
17593
- const isCarriedInvalid = isDragging && dragInvalid;
17594
- const translateX = p.x - bb.min.x;
17595
- const translateY = p.y - bb.min.y;
17596
- 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) => {
17597
- const showBorders = shouldShowBorders();
17598
- shouldUseSelectiveBorders(p.blueprintId);
17599
- return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
17600
- "path",
17601
- {
17602
- d: pathD(poly),
17603
- fill: isConnectivityLocked ? CONFIG.color.piece.invalidFill : isCarriedInvalid ? CONFIG.color.piece.invalidFill : isDragging ? CONFIG.color.piece.draggingFill : CONFIG.color.piece.validFill,
17604
- opacity: isConnectivityLocked ? CONFIG.opacity.piece.invalid : isCarriedInvalid ? CONFIG.opacity.piece.invalid : isDragging ? CONFIG.opacity.piece.dragging : locked ? CONFIG.opacity.piece.locked : CONFIG.opacity.piece.normal,
17605
- stroke: "none",
17606
- onPointerDown: (e) => onPiecePointerDown(e, p)
17607
- }
17608
- ), showBorders);
17609
- }));
17610
- }),
17766
+ /* @__PURE__ */ React.createElement(React.Fragment, null, renderSilhouettes(), renderPieces()) ,
17611
17767
  layout.sectors.map((s) => {
17612
17768
  const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
17613
17769
  if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
@@ -17615,23 +17771,32 @@ var TangramConstructPlugin = (function (jspsych) {
17615
17771
  const rect = rectForBand(layout, s, "silhouette", 1);
17616
17772
  const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
17617
17773
  const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
17618
- 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(
17619
- "path",
17620
- {
17621
- d: pathD(scaledPoly),
17622
- fill: CONFIG.color.silhouetteMask,
17623
- opacity: CONFIG.opacity.silhouetteMask,
17624
- stroke: "none"
17625
- }
17626
- ), /* @__PURE__ */ React.createElement(
17627
- "path",
17628
- {
17629
- d: pathD(scaledPoly),
17630
- fill: "none",
17631
- stroke: CONFIG.color.tangramDecomposition.stroke,
17632
- strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
17774
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
17775
+ const primInfo = primitiveDecomposition[i];
17776
+ if (usePrimitiveColorsTargets && primInfo?.kind && primitiveColorIndices) {
17777
+ const kindToIndex = {
17778
+ "square": 0,
17779
+ "smalltriangle": 1,
17780
+ "parallelogram": 2,
17781
+ "medtriangle": 3,
17782
+ "largetriangle": 4
17783
+ };
17784
+ const primitiveIndex = kindToIndex[primInfo.kind];
17785
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
17786
+ primitiveColorIndices[primitiveIndex];
17787
+ }
17633
17788
  }
17634
- ))));
17789
+ return /* @__PURE__ */ React.createElement(
17790
+ "path",
17791
+ {
17792
+ key: `prim-fill-${i}`,
17793
+ d: pathD(scaledPoly),
17794
+ fill: "none",
17795
+ opacity: 0,
17796
+ stroke: "none"
17797
+ }
17798
+ );
17799
+ }));
17635
17800
  } else {
17636
17801
  const placedPolys = placedSilBySector.get(s.id) ?? [];
17637
17802
  if (!placedPolys.length) return null;
@@ -17722,7 +17887,25 @@ var TangramConstructPlugin = (function (jspsych) {
17722
17887
  const by = layout.cy + blueprintRingR * Math.sin(theta);
17723
17888
  return renderBlueprintGlyph(bp, bx, by);
17724
17889
  }),
17725
- controller.state.endedAt && /* @__PURE__ */ React.createElement("g", { pointerEvents: "none" }, /* @__PURE__ */ React.createElement("circle", { cx: layout.cx, cy: layout.cy, r: layout.outerR - 3, fill: "none", stroke: CONFIG.color.piece.allGreenStroke, strokeWidth: CONFIG.size.stroke.allGreenStrokePx, opacity: 0 }, /* @__PURE__ */ React.createElement("animate", { attributeName: "opacity", from: "0", to: "1", dur: "160ms", fill: "freeze" }), /* @__PURE__ */ React.createElement("animate", { attributeName: "opacity", from: "1", to: "0", begin: "560ms", dur: "520ms", fill: "freeze" })))
17890
+ controller.state.endedAt && /* @__PURE__ */ React.createElement("g", { pointerEvents: "none" }, /* @__PURE__ */ React.createElement("circle", { cx: layout.cx, cy: layout.cy, r: layout.outerR - 3, fill: "none", stroke: CONFIG.color.piece.allGreenStroke, strokeWidth: CONFIG.size.stroke.allGreenStrokePx, opacity: 0 }, /* @__PURE__ */ React.createElement("animate", { attributeName: "opacity", from: "0", to: "1", dur: "160ms", fill: "freeze" }), /* @__PURE__ */ React.createElement("animate", { attributeName: "opacity", from: "1", to: "0", begin: "560ms", dur: "520ms", fill: "freeze" }))),
17891
+ showTangramDecomposition && layout.sectors.map((s) => {
17892
+ const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
17893
+ if (!sectorCfg?.silhouette.primitiveDecomposition) return null;
17894
+ const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
17895
+ const rect = rectForBand(layout, s, "silhouette", 1);
17896
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
17897
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
17898
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-borders-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(
17899
+ "path",
17900
+ {
17901
+ key: `prim-border-${i}`,
17902
+ d: pathD(scaledPoly),
17903
+ fill: "none",
17904
+ stroke: CONFIG.color.tangramDecomposition.stroke,
17905
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
17906
+ }
17907
+ )));
17908
+ })
17726
17909
  );
17727
17910
  }
17728
17911
 
@@ -19663,7 +19846,10 @@ var TangramConstructPlugin = (function (jspsych) {
19663
19846
  onPieceRemove,
19664
19847
  onInteraction,
19665
19848
  onTrialEnd,
19666
- onControllerReady
19849
+ onControllerReady,
19850
+ usePrimitiveColorsBlueprints,
19851
+ usePrimitiveColorsTargets,
19852
+ primitiveColorIndices
19667
19853
  } = props;
19668
19854
  const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1e3) : 0);
19669
19855
  const controller = React.useMemo(() => {
@@ -19987,7 +20173,7 @@ var TangramConstructPlugin = (function (jspsych) {
19987
20173
  justifyContent: "center",
19988
20174
  alignItems: "center",
19989
20175
  padding: "20px",
19990
- background: "#f5f5f5",
20176
+ background: CONFIG.color.background,
19991
20177
  flex: "0 0 auto"
19992
20178
  };
19993
20179
  const headerContentStyle = {
@@ -20003,14 +20189,20 @@ var TangramConstructPlugin = (function (jspsych) {
20003
20189
  lineHeight: 1.5,
20004
20190
  textAlign: "center"
20005
20191
  };
20192
+ const scaleX = svgDimensions.width / viewBox.w;
20193
+ const rightEdgeOfSemicircleLogical = viewBox.w / 2 + layout.outerR;
20194
+ const distanceFromRightEdgeLogical = viewBox.w - rightEdgeOfSemicircleLogical;
20195
+ const distanceFromRightEdgePx = distanceFromRightEdgeLogical * scaleX;
20196
+ const charWidth = 24 * 0.6;
20197
+ const offsetLeft = charWidth * 5;
20006
20198
  const timerStyle = {
20007
20199
  position: "absolute",
20008
- right: 0,
20200
+ right: `${distanceFromRightEdgePx + offsetLeft}px`,
20009
20201
  fontSize: "24px",
20010
20202
  fontWeight: "bold",
20011
20203
  fontFamily: "monospace",
20012
20204
  color: "#333",
20013
- minWidth: "80px",
20205
+ whiteSpace: "nowrap",
20014
20206
  textAlign: "right"
20015
20207
  };
20016
20208
  const gameboardWrapperStyle = {
@@ -20055,6 +20247,9 @@ var TangramConstructPlugin = (function (jspsych) {
20055
20247
  onCenterBadgePointerDown,
20056
20248
  showTangramDecomposition: showTangramDecomposition ?? false,
20057
20249
  scaleS,
20250
+ usePrimitiveColorsBlueprints: usePrimitiveColorsBlueprints ?? false,
20251
+ usePrimitiveColorsTargets: usePrimitiveColorsTargets ?? false,
20252
+ primitiveColorIndices: primitiveColorIndices ?? [0, 1, 2, 3, 4],
20058
20253
  ...eventCallbacks
20059
20254
  }
20060
20255
  ))));
@@ -20274,7 +20469,10 @@ var TangramConstructPlugin = (function (jspsych) {
20274
20469
  trialParams,
20275
20470
  ...params.instructions && { instructions: params.instructions },
20276
20471
  ...params.onInteraction && { onInteraction: params.onInteraction },
20277
- ...params.onTrialEnd && { onTrialEnd: params.onTrialEnd }
20472
+ ...params.onTrialEnd && { onTrialEnd: params.onTrialEnd },
20473
+ ...params.usePrimitiveColorsBlueprints !== void 0 && { usePrimitiveColorsBlueprints: params.usePrimitiveColorsBlueprints },
20474
+ ...params.usePrimitiveColorsTargets !== void 0 && { usePrimitiveColorsTargets: params.usePrimitiveColorsTargets },
20475
+ ...params.primitiveColorIndices !== void 0 && { primitiveColorIndices: params.primitiveColorIndices }
20278
20476
  };
20279
20477
  const root = clientExports.createRoot(display_element);
20280
20478
  root.render(React.createElement(GameBoard, gameBoardProps));
@@ -20359,6 +20557,24 @@ var TangramConstructPlugin = (function (jspsych) {
20359
20557
  type: jspsych.ParameterType.FUNCTION,
20360
20558
  default: void 0,
20361
20559
  description: "Callback when trial completes with full data"
20560
+ },
20561
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
20562
+ use_primitive_colors_blueprints: {
20563
+ type: jspsych.ParameterType.BOOL,
20564
+ default: false,
20565
+ description: "Whether each primitive shape type should have its own distinct color in the blueprint dock area"
20566
+ },
20567
+ /** Whether to use distinct colors for each primitive shape type in target tangrams */
20568
+ use_primitive_colors_targets: {
20569
+ type: jspsych.ParameterType.BOOL,
20570
+ default: false,
20571
+ description: "Whether each primitive shape type should have its own distinct color in target tangram silhouettes"
20572
+ },
20573
+ /** Indices mapping primitives to colors from the color palette */
20574
+ primitive_color_indices: {
20575
+ type: jspsych.ParameterType.OBJECT,
20576
+ default: [0, 1, 2, 3, 4],
20577
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
20362
20578
  }
20363
20579
  },
20364
20580
  data: {
@@ -20424,7 +20640,10 @@ var TangramConstructPlugin = (function (jspsych) {
20424
20640
  show_tangram_decomposition: trial.show_tangram_decomposition,
20425
20641
  instructions: trial.instructions,
20426
20642
  onInteraction: trial.onInteraction,
20427
- onTrialEnd: wrappedOnTrialEnd
20643
+ onTrialEnd: wrappedOnTrialEnd,
20644
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
20645
+ usePrimitiveColorsTargets: trial.use_primitive_colors_targets,
20646
+ primitiveColorIndices: trial.primitive_color_indices
20428
20647
  };
20429
20648
  const { root, display_element: element, jsPsych } = startConstructionTrial(display_element, params, this.jsPsych);
20430
20649
  element.__reactContext = { root, jsPsych };