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
@@ -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(() => {
@@ -3273,19 +3455,28 @@ function GameBoard(props) {
3273
3455
  const headerStyle = {
3274
3456
  display: "flex",
3275
3457
  flexDirection: "row",
3276
- justifyContent: "space-between",
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
  };
3464
+ const headerContentStyle = {
3465
+ position: "relative",
3466
+ width: `${svgDimensions.width}px`,
3467
+ maxWidth: "100%",
3468
+ display: "flex",
3469
+ justifyContent: "center",
3470
+ alignItems: "center"
3471
+ };
3282
3472
  const instructionsStyle = {
3283
- flexGrow: 1,
3284
3473
  fontSize: "20px",
3285
3474
  lineHeight: 1.5,
3286
- marginRight: "20px"
3475
+ textAlign: "center"
3287
3476
  };
3288
3477
  const timerStyle = {
3478
+ position: "absolute",
3479
+ right: 0,
3289
3480
  fontSize: "24px",
3290
3481
  fontWeight: "bold",
3291
3482
  fontFamily: "monospace",
@@ -3300,14 +3491,14 @@ function GameBoard(props) {
3300
3491
  justifyContent: "center",
3301
3492
  overflow: "hidden"
3302
3493
  };
3303
- 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(
3304
3495
  "div",
3305
3496
  {
3306
3497
  className: "tangram-instructions",
3307
3498
  style: instructionsStyle,
3308
3499
  dangerouslySetInnerHTML: { __html: instructions }
3309
3500
  }
3310
- ), 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(
3311
3502
  BoardView,
3312
3503
  {
3313
3504
  controller,
@@ -3335,6 +3526,9 @@ function GameBoard(props) {
3335
3526
  onCenterBadgePointerDown,
3336
3527
  showTangramDecomposition: showTangramDecomposition ?? false,
3337
3528
  scaleS,
3529
+ usePrimitiveColorsBlueprints: usePrimitiveColorsBlueprints ?? false,
3530
+ usePrimitiveColorsTargets: usePrimitiveColorsTargets ?? false,
3531
+ primitiveColorIndices: primitiveColorIndices ?? [0, 1, 2, 3, 4],
3338
3532
  ...eventCallbacks
3339
3533
  }
3340
3534
  ))));
@@ -3554,7 +3748,10 @@ function startConstructionTrial(display_element, params, _jsPsych) {
3554
3748
  trialParams,
3555
3749
  ...params.instructions && { instructions: params.instructions },
3556
3750
  ...params.onInteraction && { onInteraction: params.onInteraction },
3557
- ...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 }
3558
3755
  };
3559
3756
  const root = createRoot(display_element);
3560
3757
  root.render(React.createElement(GameBoard, gameBoardProps));
@@ -3639,6 +3836,24 @@ const info = {
3639
3836
  type: ParameterType.FUNCTION,
3640
3837
  default: void 0,
3641
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"
3642
3857
  }
3643
3858
  },
3644
3859
  data: {
@@ -3704,7 +3919,10 @@ class TangramConstructPlugin {
3704
3919
  show_tangram_decomposition: trial.show_tangram_decomposition,
3705
3920
  instructions: trial.instructions,
3706
3921
  onInteraction: trial.onInteraction,
3707
- 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
3708
3926
  };
3709
3927
  const { root, display_element: element, jsPsych } = startConstructionTrial(display_element, params, this.jsPsych);
3710
3928
  element.__reactContext = { root, jsPsych };