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
@@ -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(() => {
@@ -3273,19 +3453,28 @@ function GameBoard(props) {
3273
3453
  const headerStyle = {
3274
3454
  display: "flex",
3275
3455
  flexDirection: "row",
3276
- justifyContent: "space-between",
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
  };
3462
+ const headerContentStyle = {
3463
+ position: "relative",
3464
+ width: `${svgDimensions.width}px`,
3465
+ maxWidth: "100%",
3466
+ display: "flex",
3467
+ justifyContent: "center",
3468
+ alignItems: "center"
3469
+ };
3282
3470
  const instructionsStyle = {
3283
- flexGrow: 1,
3284
3471
  fontSize: "20px",
3285
3472
  lineHeight: 1.5,
3286
- marginRight: "20px"
3473
+ textAlign: "center"
3287
3474
  };
3288
3475
  const timerStyle = {
3476
+ position: "absolute",
3477
+ right: 0,
3289
3478
  fontSize: "24px",
3290
3479
  fontWeight: "bold",
3291
3480
  fontFamily: "monospace",
@@ -3300,14 +3489,14 @@ function GameBoard(props) {
3300
3489
  justifyContent: "center",
3301
3490
  overflow: "hidden"
3302
3491
  };
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(
3492
+ 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
3493
  "div",
3305
3494
  {
3306
3495
  className: "tangram-instructions",
3307
3496
  style: instructionsStyle,
3308
3497
  dangerouslySetInnerHTML: { __html: instructions }
3309
3498
  }
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(
3499
+ ), 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
3500
  BoardView,
3312
3501
  {
3313
3502
  controller,
@@ -3335,6 +3524,9 @@ function GameBoard(props) {
3335
3524
  onCenterBadgePointerDown,
3336
3525
  showTangramDecomposition: showTangramDecomposition ?? false,
3337
3526
  scaleS,
3527
+ usePrimitiveColorsBlueprints: usePrimitiveColorsBlueprints ?? false,
3528
+ usePrimitiveColorsTargets: usePrimitiveColorsTargets ?? false,
3529
+ primitiveColorIndices: primitiveColorIndices ?? [0, 1, 2, 3, 4],
3338
3530
  ...eventCallbacks
3339
3531
  }
3340
3532
  ))));
@@ -3501,7 +3693,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3501
3693
  quickstashMacros,
3502
3694
  primitiveOrder,
3503
3695
  onInteraction,
3504
- onTrialEnd
3696
+ onTrialEnd,
3697
+ usePrimitiveColorsBlueprints,
3698
+ primitiveColorIndices
3505
3699
  } = params;
3506
3700
  const { onInteraction: _, onTrialEnd: __, ...trialParams } = params;
3507
3701
  const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => primitiveOrder.indexOf(a.kind) - primitiveOrder.indexOf(b.kind));
@@ -3593,7 +3787,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3593
3787
  ...params.instructions && { instructions: params.instructions },
3594
3788
  onControllerReady: handleControllerReady,
3595
3789
  ...onInteraction && { onInteraction },
3596
- ...onTrialEnd && { onTrialEnd }
3790
+ ...onTrialEnd && { onTrialEnd },
3791
+ ...usePrimitiveColorsBlueprints !== void 0 && { usePrimitiveColorsBlueprints },
3792
+ ...primitiveColorIndices !== void 0 && { primitiveColorIndices }
3597
3793
  }));
3598
3794
  return { root, display_element, jsPsych };
3599
3795
  }
@@ -3659,6 +3855,18 @@ const info = {
3659
3855
  onTrialEnd: {
3660
3856
  type: ParameterType.FUNCTION,
3661
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"
3662
3870
  }
3663
3871
  },
3664
3872
  data: {
@@ -3703,7 +3911,9 @@ class TangramPrepPlugin {
3703
3911
  primitiveOrder: trial.primitive_order,
3704
3912
  instructions: trial.instructions,
3705
3913
  onInteraction: trial.onInteraction,
3706
- onTrialEnd: wrappedOnTrialEnd
3914
+ onTrialEnd: wrappedOnTrialEnd,
3915
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
3916
+ primitiveColorIndices: trial.primitive_color_indices
3707
3917
  };
3708
3918
  const { root, display_element: element, jsPsych } = startPrepTrial(display_element, params, this.jsPsych);
3709
3919
  element.__reactContext = { root, jsPsych };