jspsych-tangram 0.0.12 → 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 +4809 -3948
  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 +275 -66
  6. package/dist/construct/index.cjs.map +1 -1
  7. package/dist/construct/index.d.ts +36 -0
  8. package/dist/construct/index.js +275 -66
  9. package/dist/construct/index.js.map +1 -1
  10. package/dist/index.cjs +385 -95
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +84 -0
  13. package/dist/index.js +385 -95
  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 +4805 -3952
  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 +271 -70
  29. package/dist/prep/index.cjs.map +1 -1
  30. package/dist/prep/index.d.ts +24 -0
  31. package/dist/prep/index.js +271 -70
  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 +26 -2
  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(() => {
@@ -3278,7 +3458,7 @@ function GameBoard(props) {
3278
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
  };
3284
3464
  const headerContentStyle = {
@@ -3346,6 +3526,9 @@ function GameBoard(props) {
3346
3526
  onCenterBadgePointerDown,
3347
3527
  showTangramDecomposition: showTangramDecomposition ?? false,
3348
3528
  scaleS,
3529
+ usePrimitiveColorsBlueprints: usePrimitiveColorsBlueprints ?? false,
3530
+ usePrimitiveColorsTargets: usePrimitiveColorsTargets ?? false,
3531
+ primitiveColorIndices: primitiveColorIndices ?? [0, 1, 2, 3, 4],
3349
3532
  ...eventCallbacks
3350
3533
  }
3351
3534
  ))));
@@ -3512,7 +3695,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3512
3695
  quickstashMacros,
3513
3696
  primitiveOrder,
3514
3697
  onInteraction,
3515
- onTrialEnd
3698
+ onTrialEnd,
3699
+ usePrimitiveColorsBlueprints,
3700
+ primitiveColorIndices
3516
3701
  } = params;
3517
3702
  const { onInteraction: _, onTrialEnd: __, ...trialParams } = params;
3518
3703
  const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => primitiveOrder.indexOf(a.kind) - primitiveOrder.indexOf(b.kind));
@@ -3604,7 +3789,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3604
3789
  ...params.instructions && { instructions: params.instructions },
3605
3790
  onControllerReady: handleControllerReady,
3606
3791
  ...onInteraction && { onInteraction },
3607
- ...onTrialEnd && { onTrialEnd }
3792
+ ...onTrialEnd && { onTrialEnd },
3793
+ ...usePrimitiveColorsBlueprints !== void 0 && { usePrimitiveColorsBlueprints },
3794
+ ...primitiveColorIndices !== void 0 && { primitiveColorIndices }
3608
3795
  }));
3609
3796
  return { root, display_element, jsPsych };
3610
3797
  }
@@ -3670,6 +3857,18 @@ const info = {
3670
3857
  onTrialEnd: {
3671
3858
  type: jspsych.ParameterType.FUNCTION,
3672
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"
3673
3872
  }
3674
3873
  },
3675
3874
  data: {
@@ -3714,7 +3913,9 @@ class TangramPrepPlugin {
3714
3913
  primitiveOrder: trial.primitive_order,
3715
3914
  instructions: trial.instructions,
3716
3915
  onInteraction: trial.onInteraction,
3717
- onTrialEnd: wrappedOnTrialEnd
3916
+ onTrialEnd: wrappedOnTrialEnd,
3917
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
3918
+ primitiveColorIndices: trial.primitive_color_indices
3718
3919
  };
3719
3920
  const { root, display_element: element, jsPsych } = startPrepTrial(display_element, params, this.jsPsych);
3720
3921
  element.__reactContext = { root, jsPsych };