jspsych-tangram 0.0.11 → 0.0.13

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 (46) hide show
  1. package/dist/construct/index.browser.js +4805 -3935
  2. package/dist/construct/index.browser.js.map +1 -1
  3. package/dist/construct/index.browser.min.js +13 -13
  4. package/dist/construct/index.browser.min.js.map +1 -1
  5. package/dist/construct/index.cjs +289 -71
  6. package/dist/construct/index.cjs.map +1 -1
  7. package/dist/construct/index.d.ts +36 -0
  8. package/dist/construct/index.js +289 -71
  9. package/dist/construct/index.js.map +1 -1
  10. package/dist/index.cjs +399 -100
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +84 -0
  13. package/dist/index.js +399 -100
  14. package/dist/index.js.map +1 -1
  15. package/dist/nback/index.browser.js +4629 -3939
  16. package/dist/nback/index.browser.js.map +1 -1
  17. package/dist/nback/index.browser.min.js +12 -12
  18. package/dist/nback/index.browser.min.js.map +1 -1
  19. package/dist/nback/index.cjs +102 -64
  20. package/dist/nback/index.cjs.map +1 -1
  21. package/dist/nback/index.d.ts +24 -0
  22. package/dist/nback/index.js +102 -64
  23. package/dist/nback/index.js.map +1 -1
  24. package/dist/prep/index.browser.js +4803 -3941
  25. package/dist/prep/index.browser.js.map +1 -1
  26. package/dist/prep/index.browser.min.js +13 -13
  27. package/dist/prep/index.browser.min.js.map +1 -1
  28. package/dist/prep/index.cjs +285 -75
  29. package/dist/prep/index.cjs.map +1 -1
  30. package/dist/prep/index.d.ts +24 -0
  31. package/dist/prep/index.js +285 -75
  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 +128 -91
  36. package/src/core/components/pieces/BlueprintRing.tsx +105 -47
  37. package/src/core/config/config.ts +25 -10
  38. package/src/plugins/tangram-construct/ConstructionApp.tsx +7 -1
  39. package/src/plugins/tangram-construct/index.ts +22 -1
  40. package/src/plugins/tangram-nback/NBackApp.tsx +87 -28
  41. package/src/plugins/tangram-nback/index.ts +14 -0
  42. package/src/plugins/tangram-prep/PrepApp.tsx +7 -1
  43. package/src/plugins/tangram-prep/index.ts +14 -0
  44. package/tangram-construct.min.js +13 -13
  45. package/tangram-nback.min.js +12 -12
  46. package/tangram-prep.min.js +13 -13
@@ -7,30 +7,40 @@ var uuid = require('uuid');
7
7
 
8
8
  const CONFIG = {
9
9
  color: {
10
+ background: "#fff7e0ff",
10
11
  bands: {
11
- silhouette: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" },
12
- workspace: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" }
12
+ silhouette: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" },
13
+ workspace: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" }
13
14
  },
14
- completion: { fill: "#ccfff2", stroke: "#13da57" },
15
+ completion: { fill: "#ccffcc", stroke: "#13da57" },
15
16
  silhouetteMask: "#374151",
16
17
  anchors: { invalid: "#7dd3fc", valid: "#475569" },
17
- piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
18
- ui: { light: "#60a5fa", dark: "#1d4ed8" },
19
- blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
20
- tangramDecomposition: { stroke: "#fef2cc" }
18
+ // validFill used here for placed composites
19
+ piece: { draggingFill: "#8e7cc3", validFill: "#8e7cc3", invalidFill: "#d55c00", invalidStroke: "#dc2626", allGreenStroke: "#86efac"},
20
+ blueprint: { fill: "#374151", badgeFill: "#000000", labelFill: "#ffffff" },
21
+ tangramDecomposition: { stroke: "#fef2cc" },
22
+ primitiveColors: [
23
+ // from seaborn "colorblind" palette, 6 colors, with red omitted
24
+ "#0173b2",
25
+ "#de8f05",
26
+ "#029e73",
27
+ "#cc78bc",
28
+ "#ca9161"
29
+ ]
21
30
  },
22
31
  opacity: {
23
- blueprint: 0.4,
32
+ blueprint: 0.6,
24
33
  silhouetteMask: 0.25,
25
34
  //anchors: { valid: 0.80, invalid: 0.50 },
26
35
  anchors: { invalid: 0, valid: 0 },
27
- piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
36
+ piece: { invalid: 1, dragging: 1, locked: 1, normal: 1 }
28
37
  },
29
38
  size: {
30
- stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
39
+ stroke: { bandPx: 5, allGreenStrokePx: 10, tangramDecompositionPx: 1 },
31
40
  anchorRadiusPx: { valid: 1, invalid: 1 },
32
41
  badgeFontPx: 16,
33
- centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
42
+ centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 },
43
+ invalidMarker: { sizePx: 10, strokePx: 4 }
34
44
  },
35
45
  layout: {
36
46
  grid: { stepPx: 20, unitPx: 40 },
@@ -42,14 +52,10 @@ const CONFIG = {
42
52
  quickstashDiamAnchors: 7,
43
53
  // num anchors req'd to be in single quickstash slot
44
54
  primitiveDiamAnchors: 5
45
- },
46
- defaults: { maxQuickstashSlots: 1 }
47
- },
55
+ }},
48
56
  game: {
49
57
  snapRadiusPx: 15,
50
- showBorders: false,
51
- hideTouchingBorders: true
52
- }
58
+ showBorders: false}
53
59
  };
54
60
 
55
61
  function isComposite(bp) {
@@ -832,6 +838,31 @@ var unlockedIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAYAAA
832
838
  function pathD(poly) {
833
839
  return `M ${poly.map((pt) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
834
840
  }
841
+ function getPieceColor(blueprint, usePrimitiveColors, defaultColor, primitiveColorIndices) {
842
+ if (!usePrimitiveColors) {
843
+ return defaultColor;
844
+ }
845
+ if ("kind" in blueprint) {
846
+ const kind = blueprint.kind;
847
+ const kindToIndex = {
848
+ "square": 0,
849
+ "smalltriangle": 1,
850
+ "parallelogram": 2,
851
+ "medtriangle": 3,
852
+ "largetriangle": 4
853
+ };
854
+ const primitiveIndex = kindToIndex[kind];
855
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
856
+ const colorIndex = primitiveColorIndices[primitiveIndex];
857
+ const color = CONFIG.color.primitiveColors[colorIndex];
858
+ if (color) {
859
+ return color;
860
+ }
861
+ }
862
+ return defaultColor;
863
+ }
864
+ return defaultColor;
865
+ }
835
866
  function BoardView(props) {
836
867
  const {
837
868
  controller,
@@ -851,6 +882,9 @@ function BoardView(props) {
851
882
  dragInvalid,
852
883
  lockedPieceId,
853
884
  showTangramDecomposition,
885
+ usePrimitiveColorsBlueprints,
886
+ usePrimitiveColorsTargets,
887
+ primitiveColorIndices,
854
888
  svgRef,
855
889
  setPieceRef,
856
890
  onPiecePointerDown,
@@ -861,6 +895,144 @@ function BoardView(props) {
861
895
  onCenterBadgePointerDown
862
896
  } = props;
863
897
  const VW = viewBox.w, VH = viewBox.h;
898
+ const renderSilhouettes = () => layout.sectors.map((s) => {
899
+ const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
900
+ if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
901
+ const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
902
+ const rect = rectForBand(layout, s, "silhouette", 1);
903
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
904
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
905
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
906
+ const primInfo = primitiveDecomposition[i];
907
+ let fillColor = CONFIG.color.silhouetteMask;
908
+ if (usePrimitiveColorsTargets && primInfo?.kind && primitiveColorIndices) {
909
+ const kindToIndex = {
910
+ "square": 0,
911
+ "smalltriangle": 1,
912
+ "parallelogram": 2,
913
+ "medtriangle": 3,
914
+ "largetriangle": 4
915
+ };
916
+ const primitiveIndex = kindToIndex[primInfo.kind];
917
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
918
+ const colorIndex = primitiveColorIndices[primitiveIndex];
919
+ const color = CONFIG.color.primitiveColors[colorIndex];
920
+ if (color) {
921
+ fillColor = color;
922
+ }
923
+ }
924
+ }
925
+ return /* @__PURE__ */ React.createElement(
926
+ "path",
927
+ {
928
+ key: `prim-fill-${i}`,
929
+ d: pathD(scaledPoly),
930
+ fill: fillColor,
931
+ opacity: CONFIG.opacity.silhouetteMask,
932
+ stroke: "none"
933
+ }
934
+ );
935
+ }));
936
+ } else {
937
+ const placedPolys = placedSilBySector.get(s.id) ?? [];
938
+ if (!placedPolys.length) return null;
939
+ 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 })));
940
+ }
941
+ });
942
+ const renderPieces = (piecesFilter) => {
943
+ const piecesToRender = pieces;
944
+ return piecesToRender.sort((a, b) => {
945
+ if (draggingId === a.id) return 1;
946
+ if (draggingId === b.id) return -1;
947
+ return 0;
948
+ }).map((p) => {
949
+ const bp = controller.getBlueprint(p.blueprintId);
950
+ const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
951
+ const isDragging = draggingId === p.id;
952
+ const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
953
+ const isConnectivityLocked = lockedPieceId === p.id;
954
+ selectedPieceId === p.id;
955
+ const isCarriedInvalid = isDragging && dragInvalid;
956
+ const translateX = p.x - bb.min.x;
957
+ const translateY = p.y - bb.min.y;
958
+ const validFillColor = getPieceColor(
959
+ bp,
960
+ usePrimitiveColorsBlueprints || false,
961
+ CONFIG.color.piece.validFill,
962
+ primitiveColorIndices || [0, 1, 2, 3, 4]
963
+ );
964
+ const draggingFillColor = getPieceColor(
965
+ bp,
966
+ usePrimitiveColorsBlueprints || false,
967
+ CONFIG.color.piece.draggingFill,
968
+ primitiveColorIndices || [0, 1, 2, 3, 4]
969
+ );
970
+ 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) => {
971
+ const showBorders = shouldShowBorders();
972
+ shouldUseSelectiveBorders(p.blueprintId);
973
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
974
+ "path",
975
+ {
976
+ d: pathD(poly),
977
+ fill: isConnectivityLocked ? CONFIG.color.piece.invalidFill : isCarriedInvalid ? CONFIG.color.piece.invalidFill : isDragging ? draggingFillColor : validFillColor,
978
+ 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,
979
+ stroke: "none",
980
+ onPointerDown: (e) => onPiecePointerDown(e, p)
981
+ }
982
+ ), showBorders);
983
+ }), isDragging && isCarriedInvalid && "shape" in bp && bp.shape.length > 0 && (() => {
984
+ const { cx, cy } = polysAABB$1(bp.shape);
985
+ const size = CONFIG.size.invalidMarker.sizePx;
986
+ const strokeWidth = CONFIG.size.invalidMarker.strokePx;
987
+ const borderWidth = strokeWidth + 2;
988
+ return /* @__PURE__ */ React.createElement("g", { key: "invalid-marker" }, /* @__PURE__ */ React.createElement(
989
+ "line",
990
+ {
991
+ x1: cx - size,
992
+ y1: cy - size,
993
+ x2: cx + size,
994
+ y2: cy + size,
995
+ stroke: "white",
996
+ strokeWidth: borderWidth,
997
+ strokeLinecap: "round"
998
+ }
999
+ ), /* @__PURE__ */ React.createElement(
1000
+ "line",
1001
+ {
1002
+ x1: cx - size,
1003
+ y1: cy - size,
1004
+ x2: cx + size,
1005
+ y2: cy + size,
1006
+ stroke: CONFIG.color.piece.invalidStroke,
1007
+ strokeWidth,
1008
+ strokeLinecap: "round"
1009
+ }
1010
+ ), /* @__PURE__ */ React.createElement(
1011
+ "line",
1012
+ {
1013
+ x1: cx + size,
1014
+ y1: cy - size,
1015
+ x2: cx - size,
1016
+ y2: cy + size,
1017
+ stroke: "white",
1018
+ strokeWidth: borderWidth,
1019
+ strokeLinecap: "round"
1020
+ }
1021
+ ), /* @__PURE__ */ React.createElement(
1022
+ "line",
1023
+ {
1024
+ x1: cx + size,
1025
+ y1: cy - size,
1026
+ x2: cx - size,
1027
+ y2: cy + size,
1028
+ stroke: CONFIG.color.piece.invalidStroke,
1029
+ strokeWidth,
1030
+ strokeLinecap: "round"
1031
+ }
1032
+ ));
1033
+ })());
1034
+ });
1035
+ };
864
1036
  const centerView = controller.state.blueprintView;
865
1037
  const bps = centerView === "primitives" ? controller.state.primitives : controller.state.quickstash;
866
1038
  const QS_SLOTS = controller.state.cfg.maxQuickstashSlots;
@@ -881,6 +1053,12 @@ function BoardView(props) {
881
1053
  const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
882
1054
  const cx = bb.min.x + bb.width / 2;
883
1055
  const cy = bb.min.y + bb.height / 2;
1056
+ const fillColor = getPieceColor(
1057
+ bp,
1058
+ usePrimitiveColorsBlueprints || false,
1059
+ CONFIG.color.blueprint.fill,
1060
+ primitiveColorIndices || [0, 1, 2, 3, 4]
1061
+ );
884
1062
  return /* @__PURE__ */ React.createElement(
885
1063
  "g",
886
1064
  {
@@ -892,7 +1070,7 @@ function BoardView(props) {
892
1070
  {
893
1071
  key: idx,
894
1072
  d: pathD(poly),
895
- fill: CONFIG.color.blueprint.fill,
1073
+ fill: fillColor,
896
1074
  opacity: CONFIG.opacity.blueprint,
897
1075
  stroke: "none",
898
1076
  strokeWidth: 0,
@@ -916,7 +1094,7 @@ function BoardView(props) {
916
1094
  onPointerDown: (e) => {
917
1095
  onRootPointerDown(e);
918
1096
  },
919
- style: { background: "#f5f5f5", touchAction: "none", userSelect: "none" }
1097
+ style: { background: CONFIG.color.background, touchAction: "none", userSelect: "none" }
920
1098
  },
921
1099
  layout.sectors.map((s, i) => {
922
1100
  const done = !!controller.state.sectors[s.id].completedAt;
@@ -926,35 +1104,7 @@ function BoardView(props) {
926
1104
  const work = layout.bands.workspace;
927
1105
  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" }));
928
1106
  }),
929
- pieces.sort((a, b) => {
930
- if (draggingId === a.id) return 1;
931
- if (draggingId === b.id) return -1;
932
- return 0;
933
- }).map((p) => {
934
- const bp = controller.getBlueprint(p.blueprintId);
935
- const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
936
- const isDragging = draggingId === p.id;
937
- const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
938
- const isConnectivityLocked = lockedPieceId === p.id;
939
- selectedPieceId === p.id;
940
- const isCarriedInvalid = isDragging && dragInvalid;
941
- const translateX = p.x - bb.min.x;
942
- const translateY = p.y - bb.min.y;
943
- 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) => {
944
- const showBorders = shouldShowBorders();
945
- shouldUseSelectiveBorders(p.blueprintId);
946
- return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
947
- "path",
948
- {
949
- d: pathD(poly),
950
- fill: isConnectivityLocked ? CONFIG.color.piece.invalidFill : isCarriedInvalid ? CONFIG.color.piece.invalidFill : isDragging ? CONFIG.color.piece.draggingFill : CONFIG.color.piece.validFill,
951
- 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,
952
- stroke: "none",
953
- onPointerDown: (e) => onPiecePointerDown(e, p)
954
- }
955
- ), showBorders);
956
- }));
957
- }),
1107
+ /* @__PURE__ */ React.createElement(React.Fragment, null, renderSilhouettes(), renderPieces()) ,
958
1108
  layout.sectors.map((s) => {
959
1109
  const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
960
1110
  if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
@@ -962,23 +1112,32 @@ function BoardView(props) {
962
1112
  const rect = rectForBand(layout, s, "silhouette", 1);
963
1113
  const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
964
1114
  const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
965
- 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(
966
- "path",
967
- {
968
- d: pathD(scaledPoly),
969
- fill: CONFIG.color.silhouetteMask,
970
- opacity: CONFIG.opacity.silhouetteMask,
971
- stroke: "none"
972
- }
973
- ), /* @__PURE__ */ React.createElement(
974
- "path",
975
- {
976
- d: pathD(scaledPoly),
977
- fill: "none",
978
- stroke: CONFIG.color.tangramDecomposition.stroke,
979
- strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
1115
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
1116
+ const primInfo = primitiveDecomposition[i];
1117
+ if (usePrimitiveColorsTargets && primInfo?.kind && primitiveColorIndices) {
1118
+ const kindToIndex = {
1119
+ "square": 0,
1120
+ "smalltriangle": 1,
1121
+ "parallelogram": 2,
1122
+ "medtriangle": 3,
1123
+ "largetriangle": 4
1124
+ };
1125
+ const primitiveIndex = kindToIndex[primInfo.kind];
1126
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
1127
+ primitiveColorIndices[primitiveIndex];
1128
+ }
980
1129
  }
981
- ))));
1130
+ return /* @__PURE__ */ React.createElement(
1131
+ "path",
1132
+ {
1133
+ key: `prim-fill-${i}`,
1134
+ d: pathD(scaledPoly),
1135
+ fill: "none",
1136
+ opacity: 0,
1137
+ stroke: "none"
1138
+ }
1139
+ );
1140
+ }));
982
1141
  } else {
983
1142
  const placedPolys = placedSilBySector.get(s.id) ?? [];
984
1143
  if (!placedPolys.length) return null;
@@ -1069,7 +1228,25 @@ function BoardView(props) {
1069
1228
  const by = layout.cy + blueprintRingR * Math.sin(theta);
1070
1229
  return renderBlueprintGlyph(bp, bx, by);
1071
1230
  }),
1072
- 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" })))
1231
+ 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" }))),
1232
+ showTangramDecomposition && layout.sectors.map((s) => {
1233
+ const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
1234
+ if (!sectorCfg?.silhouette.primitiveDecomposition) return null;
1235
+ const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
1236
+ const rect = rectForBand(layout, s, "silhouette", 1);
1237
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
1238
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
1239
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-borders-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(
1240
+ "path",
1241
+ {
1242
+ key: `prim-border-${i}`,
1243
+ d: pathD(scaledPoly),
1244
+ fill: "none",
1245
+ stroke: CONFIG.color.tangramDecomposition.stroke,
1246
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
1247
+ }
1248
+ )));
1249
+ })
1073
1250
  );
1074
1251
  }
1075
1252
 
@@ -2954,7 +3131,10 @@ function GameBoard(props) {
2954
3131
  onPieceRemove,
2955
3132
  onInteraction,
2956
3133
  onTrialEnd,
2957
- onControllerReady
3134
+ onControllerReady,
3135
+ usePrimitiveColorsBlueprints,
3136
+ usePrimitiveColorsTargets,
3137
+ primitiveColorIndices
2958
3138
  } = props;
2959
3139
  const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1e3) : 0);
2960
3140
  const controller = React.useMemo(() => {
@@ -3275,19 +3455,28 @@ function GameBoard(props) {
3275
3455
  const headerStyle = {
3276
3456
  display: "flex",
3277
3457
  flexDirection: "row",
3278
- justifyContent: "space-between",
3458
+ justifyContent: "center",
3279
3459
  alignItems: "center",
3280
3460
  padding: "20px",
3281
- background: "#f5f5f5",
3461
+ background: CONFIG.color.background,
3282
3462
  flex: "0 0 auto"
3283
3463
  };
3464
+ const headerContentStyle = {
3465
+ position: "relative",
3466
+ width: `${svgDimensions.width}px`,
3467
+ maxWidth: "100%",
3468
+ display: "flex",
3469
+ justifyContent: "center",
3470
+ alignItems: "center"
3471
+ };
3284
3472
  const instructionsStyle = {
3285
- flexGrow: 1,
3286
3473
  fontSize: "20px",
3287
3474
  lineHeight: 1.5,
3288
- marginRight: "20px"
3475
+ textAlign: "center"
3289
3476
  };
3290
3477
  const timerStyle = {
3478
+ position: "absolute",
3479
+ right: 0,
3291
3480
  fontSize: "24px",
3292
3481
  fontWeight: "bold",
3293
3482
  fontFamily: "monospace",
@@ -3302,14 +3491,14 @@ function GameBoard(props) {
3302
3491
  justifyContent: "center",
3303
3492
  overflow: "hidden"
3304
3493
  };
3305
- 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(
3494
+ return /* @__PURE__ */ React.createElement("div", { className: "tangram-trial-container", style: containerStyle }, (instructions || timeLimitMs > 0) && /* @__PURE__ */ React.createElement("div", { className: "tangram-header", style: headerStyle }, /* @__PURE__ */ React.createElement("div", { className: "tangram-header-content", style: headerContentStyle }, instructions && /* @__PURE__ */ React.createElement(
3306
3495
  "div",
3307
3496
  {
3308
3497
  className: "tangram-instructions",
3309
3498
  style: instructionsStyle,
3310
3499
  dangerouslySetInnerHTML: { __html: instructions }
3311
3500
  }
3312
- ), 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(
3501
+ ), 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(
3313
3502
  BoardView,
3314
3503
  {
3315
3504
  controller,
@@ -3337,6 +3526,9 @@ function GameBoard(props) {
3337
3526
  onCenterBadgePointerDown,
3338
3527
  showTangramDecomposition: showTangramDecomposition ?? false,
3339
3528
  scaleS,
3529
+ usePrimitiveColorsBlueprints: usePrimitiveColorsBlueprints ?? false,
3530
+ usePrimitiveColorsTargets: usePrimitiveColorsTargets ?? false,
3531
+ primitiveColorIndices: primitiveColorIndices ?? [0, 1, 2, 3, 4],
3340
3532
  ...eventCallbacks
3341
3533
  }
3342
3534
  ))));
@@ -3503,7 +3695,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3503
3695
  quickstashMacros,
3504
3696
  primitiveOrder,
3505
3697
  onInteraction,
3506
- onTrialEnd
3698
+ onTrialEnd,
3699
+ usePrimitiveColorsBlueprints,
3700
+ primitiveColorIndices
3507
3701
  } = params;
3508
3702
  const { onInteraction: _, onTrialEnd: __, ...trialParams } = params;
3509
3703
  const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => primitiveOrder.indexOf(a.kind) - primitiveOrder.indexOf(b.kind));
@@ -3595,7 +3789,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3595
3789
  ...params.instructions && { instructions: params.instructions },
3596
3790
  onControllerReady: handleControllerReady,
3597
3791
  ...onInteraction && { onInteraction },
3598
- ...onTrialEnd && { onTrialEnd }
3792
+ ...onTrialEnd && { onTrialEnd },
3793
+ ...usePrimitiveColorsBlueprints !== void 0 && { usePrimitiveColorsBlueprints },
3794
+ ...primitiveColorIndices !== void 0 && { primitiveColorIndices }
3599
3795
  }));
3600
3796
  return { root, display_element, jsPsych };
3601
3797
  }
@@ -3661,6 +3857,18 @@ const info = {
3661
3857
  onTrialEnd: {
3662
3858
  type: jspsych.ParameterType.FUNCTION,
3663
3859
  default: void 0
3860
+ },
3861
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
3862
+ use_primitive_colors_blueprints: {
3863
+ type: jspsych.ParameterType.BOOL,
3864
+ default: false,
3865
+ description: "Whether each primitive shape type should have its own distinct color in the blueprint dock area"
3866
+ },
3867
+ /** Indices mapping primitives to colors from the color palette */
3868
+ primitive_color_indices: {
3869
+ type: jspsych.ParameterType.OBJECT,
3870
+ default: [0, 1, 2, 3, 4],
3871
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
3664
3872
  }
3665
3873
  },
3666
3874
  data: {
@@ -3705,7 +3913,9 @@ class TangramPrepPlugin {
3705
3913
  primitiveOrder: trial.primitive_order,
3706
3914
  instructions: trial.instructions,
3707
3915
  onInteraction: trial.onInteraction,
3708
- onTrialEnd: wrappedOnTrialEnd
3916
+ onTrialEnd: wrappedOnTrialEnd,
3917
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
3918
+ primitiveColorIndices: trial.primitive_color_indices
3709
3919
  };
3710
3920
  const { root, display_element: element, jsPsych } = startPrepTrial(display_element, params, this.jsPsych);
3711
3921
  element.__reactContext = { root, jsPsych };