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
@@ -62,6 +62,18 @@ declare const info: {
62
62
  type: ParameterType;
63
63
  default: undefined;
64
64
  };
65
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
66
+ use_primitive_colors_blueprints: {
67
+ type: ParameterType;
68
+ default: boolean;
69
+ description: string;
70
+ };
71
+ /** Indices mapping primitives to colors from the color palette */
72
+ primitive_color_indices: {
73
+ type: ParameterType;
74
+ default: number[];
75
+ description: string;
76
+ };
65
77
  };
66
78
  data: {
67
79
  /** Completion status */
@@ -143,6 +155,18 @@ declare class TangramPrepPlugin implements JsPsychPlugin<Info> {
143
155
  type: ParameterType;
144
156
  default: undefined;
145
157
  };
158
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
159
+ use_primitive_colors_blueprints: {
160
+ type: ParameterType;
161
+ default: boolean;
162
+ description: string;
163
+ };
164
+ /** Indices mapping primitives to colors from the color palette */
165
+ primitive_color_indices: {
166
+ type: ParameterType;
167
+ default: number[];
168
+ description: string;
169
+ };
146
170
  };
147
171
  data: {
148
172
  /** Completion status */
@@ -5,30 +5,40 @@ import { v4 } from 'uuid';
5
5
 
6
6
  const CONFIG = {
7
7
  color: {
8
+ background: "#fff7e0ff",
8
9
  bands: {
9
- silhouette: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" },
10
- workspace: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" }
10
+ silhouette: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" },
11
+ workspace: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" }
11
12
  },
12
- completion: { fill: "#ccfff2", stroke: "#13da57" },
13
+ completion: { fill: "#ccffcc", stroke: "#13da57" },
13
14
  silhouetteMask: "#374151",
14
15
  anchors: { invalid: "#7dd3fc", valid: "#475569" },
15
- piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
16
- ui: { light: "#60a5fa", dark: "#1d4ed8" },
17
- blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
18
- tangramDecomposition: { stroke: "#fef2cc" }
16
+ // validFill used here for placed composites
17
+ piece: { draggingFill: "#8e7cc3", validFill: "#8e7cc3", invalidFill: "#d55c00", invalidStroke: "#dc2626", allGreenStroke: "#86efac"},
18
+ blueprint: { fill: "#374151", badgeFill: "#000000", labelFill: "#ffffff" },
19
+ tangramDecomposition: { stroke: "#fef2cc" },
20
+ primitiveColors: [
21
+ // from seaborn "colorblind" palette, 6 colors, with red omitted
22
+ "#0173b2",
23
+ "#de8f05",
24
+ "#029e73",
25
+ "#cc78bc",
26
+ "#ca9161"
27
+ ]
19
28
  },
20
29
  opacity: {
21
- blueprint: 0.4,
30
+ blueprint: 0.6,
22
31
  silhouetteMask: 0.25,
23
32
  //anchors: { valid: 0.80, invalid: 0.50 },
24
33
  anchors: { invalid: 0, valid: 0 },
25
- piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
34
+ piece: { invalid: 1, dragging: 1, locked: 1, normal: 1 }
26
35
  },
27
36
  size: {
28
- stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
37
+ stroke: { bandPx: 5, allGreenStrokePx: 10, tangramDecompositionPx: 1 },
29
38
  anchorRadiusPx: { valid: 1, invalid: 1 },
30
39
  badgeFontPx: 16,
31
- centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
40
+ centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 },
41
+ invalidMarker: { sizePx: 10, strokePx: 4 }
32
42
  },
33
43
  layout: {
34
44
  grid: { stepPx: 20, unitPx: 40 },
@@ -40,14 +50,10 @@ const CONFIG = {
40
50
  quickstashDiamAnchors: 7,
41
51
  // num anchors req'd to be in single quickstash slot
42
52
  primitiveDiamAnchors: 5
43
- },
44
- defaults: { maxQuickstashSlots: 1 }
45
- },
53
+ }},
46
54
  game: {
47
55
  snapRadiusPx: 15,
48
- showBorders: false,
49
- hideTouchingBorders: true
50
- }
56
+ showBorders: false}
51
57
  };
52
58
 
53
59
  function isComposite(bp) {
@@ -830,6 +836,31 @@ var unlockedIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAYAAA
830
836
  function pathD(poly) {
831
837
  return `M ${poly.map((pt) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
832
838
  }
839
+ function getPieceColor(blueprint, usePrimitiveColors, defaultColor, primitiveColorIndices) {
840
+ if (!usePrimitiveColors) {
841
+ return defaultColor;
842
+ }
843
+ if ("kind" in blueprint) {
844
+ const kind = blueprint.kind;
845
+ const kindToIndex = {
846
+ "square": 0,
847
+ "smalltriangle": 1,
848
+ "parallelogram": 2,
849
+ "medtriangle": 3,
850
+ "largetriangle": 4
851
+ };
852
+ const primitiveIndex = kindToIndex[kind];
853
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
854
+ const colorIndex = primitiveColorIndices[primitiveIndex];
855
+ const color = CONFIG.color.primitiveColors[colorIndex];
856
+ if (color) {
857
+ return color;
858
+ }
859
+ }
860
+ return defaultColor;
861
+ }
862
+ return defaultColor;
863
+ }
833
864
  function BoardView(props) {
834
865
  const {
835
866
  controller,
@@ -849,6 +880,9 @@ function BoardView(props) {
849
880
  dragInvalid,
850
881
  lockedPieceId,
851
882
  showTangramDecomposition,
883
+ usePrimitiveColorsBlueprints,
884
+ usePrimitiveColorsTargets,
885
+ primitiveColorIndices,
852
886
  svgRef,
853
887
  setPieceRef,
854
888
  onPiecePointerDown,
@@ -859,6 +893,144 @@ function BoardView(props) {
859
893
  onCenterBadgePointerDown
860
894
  } = props;
861
895
  const VW = viewBox.w, VH = viewBox.h;
896
+ const renderSilhouettes = () => layout.sectors.map((s) => {
897
+ const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
898
+ if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
899
+ const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
900
+ const rect = rectForBand(layout, s, "silhouette", 1);
901
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
902
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
903
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
904
+ const primInfo = primitiveDecomposition[i];
905
+ let fillColor = CONFIG.color.silhouetteMask;
906
+ if (usePrimitiveColorsTargets && primInfo?.kind && primitiveColorIndices) {
907
+ const kindToIndex = {
908
+ "square": 0,
909
+ "smalltriangle": 1,
910
+ "parallelogram": 2,
911
+ "medtriangle": 3,
912
+ "largetriangle": 4
913
+ };
914
+ const primitiveIndex = kindToIndex[primInfo.kind];
915
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
916
+ const colorIndex = primitiveColorIndices[primitiveIndex];
917
+ const color = CONFIG.color.primitiveColors[colorIndex];
918
+ if (color) {
919
+ fillColor = color;
920
+ }
921
+ }
922
+ }
923
+ return /* @__PURE__ */ React.createElement(
924
+ "path",
925
+ {
926
+ key: `prim-fill-${i}`,
927
+ d: pathD(scaledPoly),
928
+ fill: fillColor,
929
+ opacity: CONFIG.opacity.silhouetteMask,
930
+ stroke: "none"
931
+ }
932
+ );
933
+ }));
934
+ } else {
935
+ const placedPolys = placedSilBySector.get(s.id) ?? [];
936
+ if (!placedPolys.length) return null;
937
+ 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 })));
938
+ }
939
+ });
940
+ const renderPieces = (piecesFilter) => {
941
+ const piecesToRender = pieces;
942
+ return piecesToRender.sort((a, b) => {
943
+ if (draggingId === a.id) return 1;
944
+ if (draggingId === b.id) return -1;
945
+ return 0;
946
+ }).map((p) => {
947
+ const bp = controller.getBlueprint(p.blueprintId);
948
+ const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
949
+ const isDragging = draggingId === p.id;
950
+ const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
951
+ const isConnectivityLocked = lockedPieceId === p.id;
952
+ selectedPieceId === p.id;
953
+ const isCarriedInvalid = isDragging && dragInvalid;
954
+ const translateX = p.x - bb.min.x;
955
+ const translateY = p.y - bb.min.y;
956
+ const validFillColor = getPieceColor(
957
+ bp,
958
+ usePrimitiveColorsBlueprints || false,
959
+ CONFIG.color.piece.validFill,
960
+ primitiveColorIndices || [0, 1, 2, 3, 4]
961
+ );
962
+ const draggingFillColor = getPieceColor(
963
+ bp,
964
+ usePrimitiveColorsBlueprints || false,
965
+ CONFIG.color.piece.draggingFill,
966
+ primitiveColorIndices || [0, 1, 2, 3, 4]
967
+ );
968
+ 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) => {
969
+ const showBorders = shouldShowBorders();
970
+ shouldUseSelectiveBorders(p.blueprintId);
971
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
972
+ "path",
973
+ {
974
+ d: pathD(poly),
975
+ fill: isConnectivityLocked ? CONFIG.color.piece.invalidFill : isCarriedInvalid ? CONFIG.color.piece.invalidFill : isDragging ? draggingFillColor : validFillColor,
976
+ 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,
977
+ stroke: "none",
978
+ onPointerDown: (e) => onPiecePointerDown(e, p)
979
+ }
980
+ ), showBorders);
981
+ }), isDragging && isCarriedInvalid && "shape" in bp && bp.shape.length > 0 && (() => {
982
+ const { cx, cy } = polysAABB$1(bp.shape);
983
+ const size = CONFIG.size.invalidMarker.sizePx;
984
+ const strokeWidth = CONFIG.size.invalidMarker.strokePx;
985
+ const borderWidth = strokeWidth + 2;
986
+ return /* @__PURE__ */ React.createElement("g", { key: "invalid-marker" }, /* @__PURE__ */ React.createElement(
987
+ "line",
988
+ {
989
+ x1: cx - size,
990
+ y1: cy - size,
991
+ x2: cx + size,
992
+ y2: cy + size,
993
+ stroke: "white",
994
+ strokeWidth: borderWidth,
995
+ strokeLinecap: "round"
996
+ }
997
+ ), /* @__PURE__ */ React.createElement(
998
+ "line",
999
+ {
1000
+ x1: cx - size,
1001
+ y1: cy - size,
1002
+ x2: cx + size,
1003
+ y2: cy + size,
1004
+ stroke: CONFIG.color.piece.invalidStroke,
1005
+ strokeWidth,
1006
+ strokeLinecap: "round"
1007
+ }
1008
+ ), /* @__PURE__ */ React.createElement(
1009
+ "line",
1010
+ {
1011
+ x1: cx + size,
1012
+ y1: cy - size,
1013
+ x2: cx - size,
1014
+ y2: cy + size,
1015
+ stroke: "white",
1016
+ strokeWidth: borderWidth,
1017
+ strokeLinecap: "round"
1018
+ }
1019
+ ), /* @__PURE__ */ React.createElement(
1020
+ "line",
1021
+ {
1022
+ x1: cx + size,
1023
+ y1: cy - size,
1024
+ x2: cx - size,
1025
+ y2: cy + size,
1026
+ stroke: CONFIG.color.piece.invalidStroke,
1027
+ strokeWidth,
1028
+ strokeLinecap: "round"
1029
+ }
1030
+ ));
1031
+ })());
1032
+ });
1033
+ };
862
1034
  const centerView = controller.state.blueprintView;
863
1035
  const bps = centerView === "primitives" ? controller.state.primitives : controller.state.quickstash;
864
1036
  const QS_SLOTS = controller.state.cfg.maxQuickstashSlots;
@@ -879,6 +1051,12 @@ function BoardView(props) {
879
1051
  const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
880
1052
  const cx = bb.min.x + bb.width / 2;
881
1053
  const cy = bb.min.y + bb.height / 2;
1054
+ const fillColor = getPieceColor(
1055
+ bp,
1056
+ usePrimitiveColorsBlueprints || false,
1057
+ CONFIG.color.blueprint.fill,
1058
+ primitiveColorIndices || [0, 1, 2, 3, 4]
1059
+ );
882
1060
  return /* @__PURE__ */ React.createElement(
883
1061
  "g",
884
1062
  {
@@ -890,7 +1068,7 @@ function BoardView(props) {
890
1068
  {
891
1069
  key: idx,
892
1070
  d: pathD(poly),
893
- fill: CONFIG.color.blueprint.fill,
1071
+ fill: fillColor,
894
1072
  opacity: CONFIG.opacity.blueprint,
895
1073
  stroke: "none",
896
1074
  strokeWidth: 0,
@@ -914,7 +1092,7 @@ function BoardView(props) {
914
1092
  onPointerDown: (e) => {
915
1093
  onRootPointerDown(e);
916
1094
  },
917
- style: { background: "#f5f5f5", touchAction: "none", userSelect: "none" }
1095
+ style: { background: CONFIG.color.background, touchAction: "none", userSelect: "none" }
918
1096
  },
919
1097
  layout.sectors.map((s, i) => {
920
1098
  const done = !!controller.state.sectors[s.id].completedAt;
@@ -924,35 +1102,7 @@ function BoardView(props) {
924
1102
  const work = layout.bands.workspace;
925
1103
  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" }));
926
1104
  }),
927
- pieces.sort((a, b) => {
928
- if (draggingId === a.id) return 1;
929
- if (draggingId === b.id) return -1;
930
- return 0;
931
- }).map((p) => {
932
- const bp = controller.getBlueprint(p.blueprintId);
933
- const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
934
- const isDragging = draggingId === p.id;
935
- const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
936
- const isConnectivityLocked = lockedPieceId === p.id;
937
- selectedPieceId === p.id;
938
- const isCarriedInvalid = isDragging && dragInvalid;
939
- const translateX = p.x - bb.min.x;
940
- const translateY = p.y - bb.min.y;
941
- 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) => {
942
- const showBorders = shouldShowBorders();
943
- shouldUseSelectiveBorders(p.blueprintId);
944
- return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
945
- "path",
946
- {
947
- d: pathD(poly),
948
- fill: isConnectivityLocked ? CONFIG.color.piece.invalidFill : isCarriedInvalid ? CONFIG.color.piece.invalidFill : isDragging ? CONFIG.color.piece.draggingFill : CONFIG.color.piece.validFill,
949
- 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,
950
- stroke: "none",
951
- onPointerDown: (e) => onPiecePointerDown(e, p)
952
- }
953
- ), showBorders);
954
- }));
955
- }),
1105
+ /* @__PURE__ */ React.createElement(React.Fragment, null, renderSilhouettes(), renderPieces()) ,
956
1106
  layout.sectors.map((s) => {
957
1107
  const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
958
1108
  if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
@@ -960,23 +1110,32 @@ function BoardView(props) {
960
1110
  const rect = rectForBand(layout, s, "silhouette", 1);
961
1111
  const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
962
1112
  const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
963
- 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(
964
- "path",
965
- {
966
- d: pathD(scaledPoly),
967
- fill: CONFIG.color.silhouetteMask,
968
- opacity: CONFIG.opacity.silhouetteMask,
969
- stroke: "none"
970
- }
971
- ), /* @__PURE__ */ React.createElement(
972
- "path",
973
- {
974
- d: pathD(scaledPoly),
975
- fill: "none",
976
- stroke: CONFIG.color.tangramDecomposition.stroke,
977
- strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
1113
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
1114
+ const primInfo = primitiveDecomposition[i];
1115
+ if (usePrimitiveColorsTargets && primInfo?.kind && primitiveColorIndices) {
1116
+ const kindToIndex = {
1117
+ "square": 0,
1118
+ "smalltriangle": 1,
1119
+ "parallelogram": 2,
1120
+ "medtriangle": 3,
1121
+ "largetriangle": 4
1122
+ };
1123
+ const primitiveIndex = kindToIndex[primInfo.kind];
1124
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
1125
+ primitiveColorIndices[primitiveIndex];
1126
+ }
978
1127
  }
979
- ))));
1128
+ return /* @__PURE__ */ React.createElement(
1129
+ "path",
1130
+ {
1131
+ key: `prim-fill-${i}`,
1132
+ d: pathD(scaledPoly),
1133
+ fill: "none",
1134
+ opacity: 0,
1135
+ stroke: "none"
1136
+ }
1137
+ );
1138
+ }));
980
1139
  } else {
981
1140
  const placedPolys = placedSilBySector.get(s.id) ?? [];
982
1141
  if (!placedPolys.length) return null;
@@ -1067,7 +1226,25 @@ function BoardView(props) {
1067
1226
  const by = layout.cy + blueprintRingR * Math.sin(theta);
1068
1227
  return renderBlueprintGlyph(bp, bx, by);
1069
1228
  }),
1070
- 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" })))
1229
+ 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" }))),
1230
+ showTangramDecomposition && layout.sectors.map((s) => {
1231
+ const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
1232
+ if (!sectorCfg?.silhouette.primitiveDecomposition) return null;
1233
+ const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
1234
+ const rect = rectForBand(layout, s, "silhouette", 1);
1235
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
1236
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
1237
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-borders-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(
1238
+ "path",
1239
+ {
1240
+ key: `prim-border-${i}`,
1241
+ d: pathD(scaledPoly),
1242
+ fill: "none",
1243
+ stroke: CONFIG.color.tangramDecomposition.stroke,
1244
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
1245
+ }
1246
+ )));
1247
+ })
1071
1248
  );
1072
1249
  }
1073
1250
 
@@ -2952,7 +3129,10 @@ function GameBoard(props) {
2952
3129
  onPieceRemove,
2953
3130
  onInteraction,
2954
3131
  onTrialEnd,
2955
- onControllerReady
3132
+ onControllerReady,
3133
+ usePrimitiveColorsBlueprints,
3134
+ usePrimitiveColorsTargets,
3135
+ primitiveColorIndices
2956
3136
  } = props;
2957
3137
  const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1e3) : 0);
2958
3138
  const controller = React.useMemo(() => {
@@ -3276,7 +3456,7 @@ function GameBoard(props) {
3276
3456
  justifyContent: "center",
3277
3457
  alignItems: "center",
3278
3458
  padding: "20px",
3279
- background: "#f5f5f5",
3459
+ background: CONFIG.color.background,
3280
3460
  flex: "0 0 auto"
3281
3461
  };
3282
3462
  const headerContentStyle = {
@@ -3344,6 +3524,9 @@ function GameBoard(props) {
3344
3524
  onCenterBadgePointerDown,
3345
3525
  showTangramDecomposition: showTangramDecomposition ?? false,
3346
3526
  scaleS,
3527
+ usePrimitiveColorsBlueprints: usePrimitiveColorsBlueprints ?? false,
3528
+ usePrimitiveColorsTargets: usePrimitiveColorsTargets ?? false,
3529
+ primitiveColorIndices: primitiveColorIndices ?? [0, 1, 2, 3, 4],
3347
3530
  ...eventCallbacks
3348
3531
  }
3349
3532
  ))));
@@ -3510,7 +3693,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3510
3693
  quickstashMacros,
3511
3694
  primitiveOrder,
3512
3695
  onInteraction,
3513
- onTrialEnd
3696
+ onTrialEnd,
3697
+ usePrimitiveColorsBlueprints,
3698
+ primitiveColorIndices
3514
3699
  } = params;
3515
3700
  const { onInteraction: _, onTrialEnd: __, ...trialParams } = params;
3516
3701
  const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => primitiveOrder.indexOf(a.kind) - primitiveOrder.indexOf(b.kind));
@@ -3602,7 +3787,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3602
3787
  ...params.instructions && { instructions: params.instructions },
3603
3788
  onControllerReady: handleControllerReady,
3604
3789
  ...onInteraction && { onInteraction },
3605
- ...onTrialEnd && { onTrialEnd }
3790
+ ...onTrialEnd && { onTrialEnd },
3791
+ ...usePrimitiveColorsBlueprints !== void 0 && { usePrimitiveColorsBlueprints },
3792
+ ...primitiveColorIndices !== void 0 && { primitiveColorIndices }
3606
3793
  }));
3607
3794
  return { root, display_element, jsPsych };
3608
3795
  }
@@ -3668,6 +3855,18 @@ const info = {
3668
3855
  onTrialEnd: {
3669
3856
  type: ParameterType.FUNCTION,
3670
3857
  default: void 0
3858
+ },
3859
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
3860
+ use_primitive_colors_blueprints: {
3861
+ type: ParameterType.BOOL,
3862
+ default: false,
3863
+ description: "Whether each primitive shape type should have its own distinct color in the blueprint dock area"
3864
+ },
3865
+ /** Indices mapping primitives to colors from the color palette */
3866
+ primitive_color_indices: {
3867
+ type: ParameterType.OBJECT,
3868
+ default: [0, 1, 2, 3, 4],
3869
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
3671
3870
  }
3672
3871
  },
3673
3872
  data: {
@@ -3712,7 +3911,9 @@ class TangramPrepPlugin {
3712
3911
  primitiveOrder: trial.primitive_order,
3713
3912
  instructions: trial.instructions,
3714
3913
  onInteraction: trial.onInteraction,
3715
- onTrialEnd: wrappedOnTrialEnd
3914
+ onTrialEnd: wrappedOnTrialEnd,
3915
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
3916
+ primitiveColorIndices: trial.primitive_color_indices
3716
3917
  };
3717
3918
  const { root, display_element: element, jsPsych } = startPrepTrial(display_element, params, this.jsPsych);
3718
3919
  element.__reactContext = { root, jsPsych };