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
@@ -79,6 +79,24 @@ declare const info: {
79
79
  default: undefined;
80
80
  description: string;
81
81
  };
82
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
83
+ use_primitive_colors_blueprints: {
84
+ type: ParameterType;
85
+ default: boolean;
86
+ description: string;
87
+ };
88
+ /** Whether to use distinct colors for each primitive shape type in target tangrams */
89
+ use_primitive_colors_targets: {
90
+ type: ParameterType;
91
+ default: boolean;
92
+ description: string;
93
+ };
94
+ /** Indices mapping primitives to colors from the color palette */
95
+ primitive_color_indices: {
96
+ type: ParameterType;
97
+ default: number[];
98
+ description: string;
99
+ };
82
100
  };
83
101
  data: {
84
102
  /** Array of all interaction events during trial */
@@ -199,6 +217,24 @@ declare class TangramConstructPlugin implements JsPsychPlugin<Info> {
199
217
  default: undefined;
200
218
  description: string;
201
219
  };
220
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
221
+ use_primitive_colors_blueprints: {
222
+ type: ParameterType;
223
+ default: boolean;
224
+ description: string;
225
+ };
226
+ /** Whether to use distinct colors for each primitive shape type in target tangrams */
227
+ use_primitive_colors_targets: {
228
+ type: ParameterType;
229
+ default: boolean;
230
+ description: string;
231
+ };
232
+ /** Indices mapping primitives to colors from the color palette */
233
+ primitive_color_indices: {
234
+ type: ParameterType;
235
+ default: number[];
236
+ description: string;
237
+ };
202
238
  };
203
239
  data: {
204
240
  /** Array of all interaction events during trial */
@@ -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 },
@@ -45,9 +55,7 @@ const CONFIG = {
45
55
  },
46
56
  game: {
47
57
  snapRadiusPx: 15,
48
- showBorders: false,
49
- hideTouchingBorders: true
50
- }
58
+ showBorders: false}
51
59
  };
52
60
 
53
61
  function isComposite(bp) {
@@ -830,6 +838,31 @@ var unlockedIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAYAAA
830
838
  function pathD(poly) {
831
839
  return `M ${poly.map((pt) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
832
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
+ }
833
866
  function BoardView(props) {
834
867
  const {
835
868
  controller,
@@ -849,6 +882,9 @@ function BoardView(props) {
849
882
  dragInvalid,
850
883
  lockedPieceId,
851
884
  showTangramDecomposition,
885
+ usePrimitiveColorsBlueprints,
886
+ usePrimitiveColorsTargets,
887
+ primitiveColorIndices,
852
888
  svgRef,
853
889
  setPieceRef,
854
890
  onPiecePointerDown,
@@ -859,6 +895,144 @@ function BoardView(props) {
859
895
  onCenterBadgePointerDown
860
896
  } = props;
861
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
+ };
862
1036
  const centerView = controller.state.blueprintView;
863
1037
  const bps = centerView === "primitives" ? controller.state.primitives : controller.state.quickstash;
864
1038
  const QS_SLOTS = controller.state.cfg.maxQuickstashSlots;
@@ -879,6 +1053,12 @@ function BoardView(props) {
879
1053
  const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
880
1054
  const cx = bb.min.x + bb.width / 2;
881
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
+ );
882
1062
  return /* @__PURE__ */ React.createElement(
883
1063
  "g",
884
1064
  {
@@ -890,7 +1070,7 @@ function BoardView(props) {
890
1070
  {
891
1071
  key: idx,
892
1072
  d: pathD(poly),
893
- fill: CONFIG.color.blueprint.fill,
1073
+ fill: fillColor,
894
1074
  opacity: CONFIG.opacity.blueprint,
895
1075
  stroke: "none",
896
1076
  strokeWidth: 0,
@@ -914,7 +1094,7 @@ function BoardView(props) {
914
1094
  onPointerDown: (e) => {
915
1095
  onRootPointerDown(e);
916
1096
  },
917
- style: { background: "#f5f5f5", touchAction: "none", userSelect: "none" }
1097
+ style: { background: CONFIG.color.background, touchAction: "none", userSelect: "none" }
918
1098
  },
919
1099
  layout.sectors.map((s, i) => {
920
1100
  const done = !!controller.state.sectors[s.id].completedAt;
@@ -924,35 +1104,7 @@ function BoardView(props) {
924
1104
  const work = layout.bands.workspace;
925
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" }));
926
1106
  }),
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
- }),
1107
+ /* @__PURE__ */ React.createElement(React.Fragment, null, renderSilhouettes(), renderPieces()) ,
956
1108
  layout.sectors.map((s) => {
957
1109
  const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
958
1110
  if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
@@ -960,23 +1112,32 @@ function BoardView(props) {
960
1112
  const rect = rectForBand(layout, s, "silhouette", 1);
961
1113
  const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
962
1114
  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
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
+ }
978
1129
  }
979
- ))));
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
+ }));
980
1141
  } else {
981
1142
  const placedPolys = placedSilBySector.get(s.id) ?? [];
982
1143
  if (!placedPolys.length) return null;
@@ -1067,7 +1228,25 @@ function BoardView(props) {
1067
1228
  const by = layout.cy + blueprintRingR * Math.sin(theta);
1068
1229
  return renderBlueprintGlyph(bp, bx, by);
1069
1230
  }),
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" })))
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
+ })
1071
1250
  );
1072
1251
  }
1073
1252
 
@@ -2952,7 +3131,10 @@ function GameBoard(props) {
2952
3131
  onPieceRemove,
2953
3132
  onInteraction,
2954
3133
  onTrialEnd,
2955
- onControllerReady
3134
+ onControllerReady,
3135
+ usePrimitiveColorsBlueprints,
3136
+ usePrimitiveColorsTargets,
3137
+ primitiveColorIndices
2956
3138
  } = props;
2957
3139
  const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1e3) : 0);
2958
3140
  const controller = React.useMemo(() => {
@@ -3276,7 +3458,7 @@ function GameBoard(props) {
3276
3458
  justifyContent: "center",
3277
3459
  alignItems: "center",
3278
3460
  padding: "20px",
3279
- background: "#f5f5f5",
3461
+ background: CONFIG.color.background,
3280
3462
  flex: "0 0 auto"
3281
3463
  };
3282
3464
  const headerContentStyle = {
@@ -3344,6 +3526,9 @@ function GameBoard(props) {
3344
3526
  onCenterBadgePointerDown,
3345
3527
  showTangramDecomposition: showTangramDecomposition ?? false,
3346
3528
  scaleS,
3529
+ usePrimitiveColorsBlueprints: usePrimitiveColorsBlueprints ?? false,
3530
+ usePrimitiveColorsTargets: usePrimitiveColorsTargets ?? false,
3531
+ primitiveColorIndices: primitiveColorIndices ?? [0, 1, 2, 3, 4],
3347
3532
  ...eventCallbacks
3348
3533
  }
3349
3534
  ))));
@@ -3563,7 +3748,10 @@ function startConstructionTrial(display_element, params, _jsPsych) {
3563
3748
  trialParams,
3564
3749
  ...params.instructions && { instructions: params.instructions },
3565
3750
  ...params.onInteraction && { onInteraction: params.onInteraction },
3566
- ...params.onTrialEnd && { onTrialEnd: params.onTrialEnd }
3751
+ ...params.onTrialEnd && { onTrialEnd: params.onTrialEnd },
3752
+ ...params.usePrimitiveColorsBlueprints !== void 0 && { usePrimitiveColorsBlueprints: params.usePrimitiveColorsBlueprints },
3753
+ ...params.usePrimitiveColorsTargets !== void 0 && { usePrimitiveColorsTargets: params.usePrimitiveColorsTargets },
3754
+ ...params.primitiveColorIndices !== void 0 && { primitiveColorIndices: params.primitiveColorIndices }
3567
3755
  };
3568
3756
  const root = createRoot(display_element);
3569
3757
  root.render(React.createElement(GameBoard, gameBoardProps));
@@ -3648,6 +3836,24 @@ const info = {
3648
3836
  type: ParameterType.FUNCTION,
3649
3837
  default: void 0,
3650
3838
  description: "Callback when trial completes with full data"
3839
+ },
3840
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
3841
+ use_primitive_colors_blueprints: {
3842
+ type: ParameterType.BOOL,
3843
+ default: false,
3844
+ description: "Whether each primitive shape type should have its own distinct color in the blueprint dock area"
3845
+ },
3846
+ /** Whether to use distinct colors for each primitive shape type in target tangrams */
3847
+ use_primitive_colors_targets: {
3848
+ type: ParameterType.BOOL,
3849
+ default: false,
3850
+ description: "Whether each primitive shape type should have its own distinct color in target tangram silhouettes"
3851
+ },
3852
+ /** Indices mapping primitives to colors from the color palette */
3853
+ primitive_color_indices: {
3854
+ type: ParameterType.OBJECT,
3855
+ default: [0, 1, 2, 3, 4],
3856
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
3651
3857
  }
3652
3858
  },
3653
3859
  data: {
@@ -3713,7 +3919,10 @@ class TangramConstructPlugin {
3713
3919
  show_tangram_decomposition: trial.show_tangram_decomposition,
3714
3920
  instructions: trial.instructions,
3715
3921
  onInteraction: trial.onInteraction,
3716
- onTrialEnd: wrappedOnTrialEnd
3922
+ onTrialEnd: wrappedOnTrialEnd,
3923
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
3924
+ usePrimitiveColorsTargets: trial.use_primitive_colors_targets,
3925
+ primitiveColorIndices: trial.primitive_color_indices
3717
3926
  };
3718
3927
  const { root, display_element: element, jsPsych } = startConstructionTrial(display_element, params, this.jsPsych);
3719
3928
  element.__reactContext = { root, jsPsych };