jspsych-tangram 0.0.12 → 0.0.14

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 (49) hide show
  1. package/dist/construct/index.browser.js +283 -64
  2. package/dist/construct/index.browser.js.map +1 -1
  3. package/dist/construct/index.browser.min.js +11 -11
  4. package/dist/construct/index.browser.min.js.map +1 -1
  5. package/dist/construct/index.cjs +283 -64
  6. package/dist/construct/index.cjs.map +1 -1
  7. package/dist/construct/index.d.ts +36 -0
  8. package/dist/construct/index.js +283 -64
  9. package/dist/construct/index.js.map +1 -1
  10. package/dist/index.cjs +394 -94
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +84 -0
  13. package/dist/index.js +394 -94
  14. package/dist/index.js.map +1 -1
  15. package/dist/nback/index.browser.js +112 -37
  16. package/dist/nback/index.browser.js.map +1 -1
  17. package/dist/nback/index.browser.min.js +11 -11
  18. package/dist/nback/index.browser.min.js.map +1 -1
  19. package/dist/nback/index.cjs +112 -37
  20. package/dist/nback/index.cjs.map +1 -1
  21. package/dist/nback/index.d.ts +24 -0
  22. package/dist/nback/index.js +112 -37
  23. package/dist/nback/index.js.map +1 -1
  24. package/dist/prep/index.browser.js +278 -65
  25. package/dist/prep/index.browser.js.map +1 -1
  26. package/dist/prep/index.browser.min.js +11 -11
  27. package/dist/prep/index.browser.min.js.map +1 -1
  28. package/dist/prep/index.cjs +278 -65
  29. package/dist/prep/index.cjs.map +1 -1
  30. package/dist/prep/index.d.ts +24 -0
  31. package/dist/prep/index.js +278 -65
  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 +39 -44
  36. package/src/core/components/board/useGameBoard.ts +52 -0
  37. package/src/core/components/index.ts +4 -2
  38. package/src/core/components/pieces/BlueprintRing.tsx +100 -67
  39. package/src/core/components/pieces/useBlueprintRing.ts +39 -0
  40. package/src/core/config/config.ts +25 -10
  41. package/src/plugins/tangram-construct/ConstructionApp.tsx +7 -1
  42. package/src/plugins/tangram-construct/index.ts +22 -1
  43. package/src/plugins/tangram-nback/NBackApp.tsx +88 -29
  44. package/src/plugins/tangram-nback/index.ts +14 -0
  45. package/src/plugins/tangram-prep/PrepApp.tsx +7 -1
  46. package/src/plugins/tangram-prep/index.ts +14 -0
  47. package/tangram-construct.min.js +11 -11
  48. package/tangram-nback.min.js +11 -11
  49. package/tangram-prep.min.js +11 -11
@@ -7,30 +7,41 @@ var uuid = require('uuid');
7
7
 
8
8
  const CONFIG = {
9
9
  color: {
10
+ background: "#fff7e0ff",
10
11
  bands: {
11
- silhouette: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" },
12
- workspace: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" }
12
+ silhouette: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" },
13
+ workspace: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" }
13
14
  },
14
- completion: { fill: "#ccfff2", stroke: "#13da57" },
15
+ completion: { fill: "#ccffcc", stroke: "#13da57" },
15
16
  silhouetteMask: "#374151",
16
17
  anchors: { invalid: "#7dd3fc", valid: "#475569" },
17
- piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
18
+ // validFill used here for placed composites
19
+ piece: { draggingFill: "#8e7cc3", validFill: "#8e7cc3", invalidFill: "#d55c00", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
18
20
  ui: { light: "#60a5fa", dark: "#1d4ed8" },
19
21
  blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
20
- tangramDecomposition: { stroke: "#fef2cc" }
22
+ tangramDecomposition: { stroke: "#fef2cc" },
23
+ primitiveColors: [
24
+ // from seaborn "colorblind" palette, 6 colors, with red omitted
25
+ "#0173b2",
26
+ "#de8f05",
27
+ "#029e73",
28
+ "#cc78bc",
29
+ "#ca9161"
30
+ ]
21
31
  },
22
32
  opacity: {
23
- blueprint: 0.4,
33
+ blueprint: 0.6,
24
34
  silhouetteMask: 0.25,
25
35
  //anchors: { valid: 0.80, invalid: 0.50 },
26
36
  anchors: { invalid: 0, valid: 0 },
27
- piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
37
+ piece: { invalid: 1, dragging: 1, locked: 1, normal: 1 }
28
38
  },
29
39
  size: {
30
- stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
40
+ stroke: { bandPx: 5, pieceSelectedPx: 5, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
31
41
  anchorRadiusPx: { valid: 1, invalid: 1 },
32
42
  badgeFontPx: 16,
33
- centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
43
+ centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 },
44
+ invalidMarker: { sizePx: 10, strokePx: 4 }
34
45
  },
35
46
  layout: {
36
47
  grid: { stepPx: 20, unitPx: 40 },
@@ -48,7 +59,8 @@ const CONFIG = {
48
59
  game: {
49
60
  snapRadiusPx: 15,
50
61
  showBorders: false,
51
- hideTouchingBorders: true
62
+ hideTouchingBorders: true,
63
+ silhouettesBelowPieces: true
52
64
  }
53
65
  };
54
66
 
@@ -832,6 +844,31 @@ var unlockedIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAYAAA
832
844
  function pathD(poly) {
833
845
  return `M ${poly.map((pt) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
834
846
  }
847
+ function getPieceColor(blueprint, usePrimitiveColors, defaultColor, primitiveColorIndices) {
848
+ if (!usePrimitiveColors) {
849
+ return defaultColor;
850
+ }
851
+ if ("kind" in blueprint) {
852
+ const kind = blueprint.kind;
853
+ const kindToIndex = {
854
+ "square": 0,
855
+ "smalltriangle": 1,
856
+ "parallelogram": 2,
857
+ "medtriangle": 3,
858
+ "largetriangle": 4
859
+ };
860
+ const primitiveIndex = kindToIndex[kind];
861
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
862
+ const colorIndex = primitiveColorIndices[primitiveIndex];
863
+ const color = CONFIG.color.primitiveColors[colorIndex];
864
+ if (color) {
865
+ return color;
866
+ }
867
+ }
868
+ return defaultColor;
869
+ }
870
+ return defaultColor;
871
+ }
835
872
  function BoardView(props) {
836
873
  const {
837
874
  controller,
@@ -851,6 +888,9 @@ function BoardView(props) {
851
888
  dragInvalid,
852
889
  lockedPieceId,
853
890
  showTangramDecomposition,
891
+ usePrimitiveColorsBlueprints,
892
+ usePrimitiveColorsTargets,
893
+ primitiveColorIndices,
854
894
  svgRef,
855
895
  setPieceRef,
856
896
  onPiecePointerDown,
@@ -861,6 +901,144 @@ function BoardView(props) {
861
901
  onCenterBadgePointerDown
862
902
  } = props;
863
903
  const VW = viewBox.w, VH = viewBox.h;
904
+ const renderSilhouettes = () => layout.sectors.map((s) => {
905
+ const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
906
+ if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
907
+ const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
908
+ const rect = rectForBand(layout, s, "silhouette", 1);
909
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
910
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
911
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
912
+ const primInfo = primitiveDecomposition[i];
913
+ let fillColor = CONFIG.color.silhouetteMask;
914
+ if (usePrimitiveColorsTargets && primInfo?.kind && primitiveColorIndices) {
915
+ const kindToIndex = {
916
+ "square": 0,
917
+ "smalltriangle": 1,
918
+ "parallelogram": 2,
919
+ "medtriangle": 3,
920
+ "largetriangle": 4
921
+ };
922
+ const primitiveIndex = kindToIndex[primInfo.kind];
923
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
924
+ const colorIndex = primitiveColorIndices[primitiveIndex];
925
+ const color = CONFIG.color.primitiveColors[colorIndex];
926
+ if (color) {
927
+ fillColor = color;
928
+ }
929
+ }
930
+ }
931
+ return /* @__PURE__ */ React.createElement(
932
+ "path",
933
+ {
934
+ key: `prim-fill-${i}`,
935
+ d: pathD(scaledPoly),
936
+ fill: fillColor,
937
+ opacity: CONFIG.opacity.silhouetteMask,
938
+ stroke: "none"
939
+ }
940
+ );
941
+ }));
942
+ } else {
943
+ const placedPolys = placedSilBySector.get(s.id) ?? [];
944
+ if (!placedPolys.length) return null;
945
+ 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 })));
946
+ }
947
+ });
948
+ const renderPieces = (piecesFilter) => {
949
+ const piecesToRender = pieces;
950
+ return piecesToRender.sort((a, b) => {
951
+ if (draggingId === a.id) return 1;
952
+ if (draggingId === b.id) return -1;
953
+ return 0;
954
+ }).map((p) => {
955
+ const bp = controller.getBlueprint(p.blueprintId);
956
+ const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
957
+ const isDragging = draggingId === p.id;
958
+ const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
959
+ const isConnectivityLocked = lockedPieceId === p.id;
960
+ selectedPieceId === p.id;
961
+ const isCarriedInvalid = isDragging && dragInvalid;
962
+ const translateX = p.x - bb.min.x;
963
+ const translateY = p.y - bb.min.y;
964
+ const validFillColor = getPieceColor(
965
+ bp,
966
+ usePrimitiveColorsBlueprints || false,
967
+ CONFIG.color.piece.validFill,
968
+ primitiveColorIndices || [0, 1, 2, 3, 4]
969
+ );
970
+ const draggingFillColor = getPieceColor(
971
+ bp,
972
+ usePrimitiveColorsBlueprints || false,
973
+ CONFIG.color.piece.draggingFill,
974
+ primitiveColorIndices || [0, 1, 2, 3, 4]
975
+ );
976
+ 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) => {
977
+ const showBorders = shouldShowBorders();
978
+ shouldUseSelectiveBorders(p.blueprintId);
979
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
980
+ "path",
981
+ {
982
+ d: pathD(poly),
983
+ fill: isConnectivityLocked ? CONFIG.color.piece.invalidFill : isCarriedInvalid ? CONFIG.color.piece.invalidFill : isDragging ? draggingFillColor : validFillColor,
984
+ 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,
985
+ stroke: "none",
986
+ onPointerDown: (e) => onPiecePointerDown(e, p)
987
+ }
988
+ ), showBorders);
989
+ }), isDragging && isCarriedInvalid && "shape" in bp && bp.shape.length > 0 && (() => {
990
+ const { cx, cy } = polysAABB$1(bp.shape);
991
+ const size = CONFIG.size.invalidMarker.sizePx;
992
+ const strokeWidth = CONFIG.size.invalidMarker.strokePx;
993
+ const borderWidth = strokeWidth + 2;
994
+ return /* @__PURE__ */ React.createElement("g", { key: "invalid-marker" }, /* @__PURE__ */ React.createElement(
995
+ "line",
996
+ {
997
+ x1: cx - size,
998
+ y1: cy - size,
999
+ x2: cx + size,
1000
+ y2: cy + size,
1001
+ stroke: "white",
1002
+ strokeWidth: borderWidth,
1003
+ strokeLinecap: "round"
1004
+ }
1005
+ ), /* @__PURE__ */ React.createElement(
1006
+ "line",
1007
+ {
1008
+ x1: cx - size,
1009
+ y1: cy - size,
1010
+ x2: cx + size,
1011
+ y2: cy + size,
1012
+ stroke: CONFIG.color.piece.invalidStroke,
1013
+ strokeWidth,
1014
+ strokeLinecap: "round"
1015
+ }
1016
+ ), /* @__PURE__ */ React.createElement(
1017
+ "line",
1018
+ {
1019
+ x1: cx + size,
1020
+ y1: cy - size,
1021
+ x2: cx - size,
1022
+ y2: cy + size,
1023
+ stroke: "white",
1024
+ strokeWidth: borderWidth,
1025
+ strokeLinecap: "round"
1026
+ }
1027
+ ), /* @__PURE__ */ React.createElement(
1028
+ "line",
1029
+ {
1030
+ x1: cx + size,
1031
+ y1: cy - size,
1032
+ x2: cx - size,
1033
+ y2: cy + size,
1034
+ stroke: CONFIG.color.piece.invalidStroke,
1035
+ strokeWidth,
1036
+ strokeLinecap: "round"
1037
+ }
1038
+ ));
1039
+ })());
1040
+ });
1041
+ };
864
1042
  const centerView = controller.state.blueprintView;
865
1043
  const bps = centerView === "primitives" ? controller.state.primitives : controller.state.quickstash;
866
1044
  const QS_SLOTS = controller.state.cfg.maxQuickstashSlots;
@@ -881,6 +1059,12 @@ function BoardView(props) {
881
1059
  const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
882
1060
  const cx = bb.min.x + bb.width / 2;
883
1061
  const cy = bb.min.y + bb.height / 2;
1062
+ const fillColor = getPieceColor(
1063
+ bp,
1064
+ usePrimitiveColorsBlueprints || false,
1065
+ CONFIG.color.blueprint.fill,
1066
+ primitiveColorIndices || [0, 1, 2, 3, 4]
1067
+ );
884
1068
  return /* @__PURE__ */ React.createElement(
885
1069
  "g",
886
1070
  {
@@ -892,7 +1076,7 @@ function BoardView(props) {
892
1076
  {
893
1077
  key: idx,
894
1078
  d: pathD(poly),
895
- fill: CONFIG.color.blueprint.fill,
1079
+ fill: fillColor,
896
1080
  opacity: CONFIG.opacity.blueprint,
897
1081
  stroke: "none",
898
1082
  strokeWidth: 0,
@@ -916,7 +1100,7 @@ function BoardView(props) {
916
1100
  onPointerDown: (e) => {
917
1101
  onRootPointerDown(e);
918
1102
  },
919
- style: { background: "#f5f5f5", touchAction: "none", userSelect: "none" }
1103
+ style: { background: CONFIG.color.background, touchAction: "none", userSelect: "none" }
920
1104
  },
921
1105
  layout.sectors.map((s, i) => {
922
1106
  const done = !!controller.state.sectors[s.id].completedAt;
@@ -926,35 +1110,7 @@ function BoardView(props) {
926
1110
  const work = layout.bands.workspace;
927
1111
  return /* @__PURE__ */ React.createElement("g", { key: `bands-${s.id}` }, controller.state.cfg.target === "workspace" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("path", { d: wedgePath(layout.cx, layout.cy, sil[0], sil[1], s.start, s.end), fill: baseSil, stroke: CONFIG.color.bands.silhouette.stroke, strokeWidth: CONFIG.size.stroke.bandPx, pointerEvents: "none" }), /* @__PURE__ */ React.createElement("path", { d: wedgePath(layout.cx, layout.cy, work[0], work[1], s.start, s.end), fill: done ? CONFIG.color.completion.fill : baseWork, stroke: done ? CONFIG.color.completion.stroke : CONFIG.color.bands.workspace.stroke, strokeWidth: CONFIG.size.stroke.bandPx, pointerEvents: "none" })) : /* @__PURE__ */ React.createElement("path", { d: wedgePath(layout.cx, layout.cy, sil[0], sil[1], s.start, s.end), fill: done ? CONFIG.color.completion.fill : baseSil, stroke: done ? CONFIG.color.completion.stroke : CONFIG.color.bands.silhouette.stroke, strokeWidth: CONFIG.size.stroke.bandPx, pointerEvents: "none" }));
928
1112
  }),
929
- pieces.sort((a, b) => {
930
- if (draggingId === a.id) return 1;
931
- if (draggingId === b.id) return -1;
932
- return 0;
933
- }).map((p) => {
934
- const bp = controller.getBlueprint(p.blueprintId);
935
- const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
936
- const isDragging = draggingId === p.id;
937
- const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
938
- const isConnectivityLocked = lockedPieceId === p.id;
939
- selectedPieceId === p.id;
940
- const isCarriedInvalid = isDragging && dragInvalid;
941
- const translateX = p.x - bb.min.x;
942
- const translateY = p.y - bb.min.y;
943
- return /* @__PURE__ */ React.createElement("g", { key: p.id, ref: setPieceRef(p.id), transform: `translate(${translateX}, ${translateY})`, style: { cursor: locked ? "default" : clickMode ? "pointer" : "grab" }, pointerEvents: clickMode && isDragging ? "none" : "auto" }, ("shape" in bp ? bp.shape : []).map((poly, idx) => {
944
- const showBorders = shouldShowBorders();
945
- shouldUseSelectiveBorders(p.blueprintId);
946
- return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
947
- "path",
948
- {
949
- d: pathD(poly),
950
- fill: isConnectivityLocked ? CONFIG.color.piece.invalidFill : isCarriedInvalid ? CONFIG.color.piece.invalidFill : isDragging ? CONFIG.color.piece.draggingFill : CONFIG.color.piece.validFill,
951
- opacity: isConnectivityLocked ? CONFIG.opacity.piece.invalid : isCarriedInvalid ? CONFIG.opacity.piece.invalid : isDragging ? CONFIG.opacity.piece.dragging : locked ? CONFIG.opacity.piece.locked : CONFIG.opacity.piece.normal,
952
- stroke: "none",
953
- onPointerDown: (e) => onPiecePointerDown(e, p)
954
- }
955
- ), showBorders);
956
- }));
957
- }),
1113
+ /* @__PURE__ */ React.createElement(React.Fragment, null, renderSilhouettes(), renderPieces()) ,
958
1114
  layout.sectors.map((s) => {
959
1115
  const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
960
1116
  if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
@@ -962,23 +1118,32 @@ function BoardView(props) {
962
1118
  const rect = rectForBand(layout, s, "silhouette", 1);
963
1119
  const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
964
1120
  const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
965
- return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(React.Fragment, { key: `prim-${i}` }, /* @__PURE__ */ React.createElement(
966
- "path",
967
- {
968
- d: pathD(scaledPoly),
969
- fill: CONFIG.color.silhouetteMask,
970
- opacity: CONFIG.opacity.silhouetteMask,
971
- stroke: "none"
972
- }
973
- ), /* @__PURE__ */ React.createElement(
974
- "path",
975
- {
976
- d: pathD(scaledPoly),
977
- fill: "none",
978
- stroke: CONFIG.color.tangramDecomposition.stroke,
979
- strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
1121
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
1122
+ const primInfo = primitiveDecomposition[i];
1123
+ if (usePrimitiveColorsTargets && primInfo?.kind && primitiveColorIndices) {
1124
+ const kindToIndex = {
1125
+ "square": 0,
1126
+ "smalltriangle": 1,
1127
+ "parallelogram": 2,
1128
+ "medtriangle": 3,
1129
+ "largetriangle": 4
1130
+ };
1131
+ const primitiveIndex = kindToIndex[primInfo.kind];
1132
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
1133
+ primitiveColorIndices[primitiveIndex];
1134
+ }
980
1135
  }
981
- ))));
1136
+ return /* @__PURE__ */ React.createElement(
1137
+ "path",
1138
+ {
1139
+ key: `prim-fill-${i}`,
1140
+ d: pathD(scaledPoly),
1141
+ fill: "none",
1142
+ opacity: 0,
1143
+ stroke: "none"
1144
+ }
1145
+ );
1146
+ }));
982
1147
  } else {
983
1148
  const placedPolys = placedSilBySector.get(s.id) ?? [];
984
1149
  if (!placedPolys.length) return null;
@@ -1069,7 +1234,25 @@ function BoardView(props) {
1069
1234
  const by = layout.cy + blueprintRingR * Math.sin(theta);
1070
1235
  return renderBlueprintGlyph(bp, bx, by);
1071
1236
  }),
1072
- controller.state.endedAt && /* @__PURE__ */ React.createElement("g", { pointerEvents: "none" }, /* @__PURE__ */ React.createElement("circle", { cx: layout.cx, cy: layout.cy, r: layout.outerR - 3, fill: "none", stroke: CONFIG.color.piece.allGreenStroke, strokeWidth: CONFIG.size.stroke.allGreenStrokePx, opacity: 0 }, /* @__PURE__ */ React.createElement("animate", { attributeName: "opacity", from: "0", to: "1", dur: "160ms", fill: "freeze" }), /* @__PURE__ */ React.createElement("animate", { attributeName: "opacity", from: "1", to: "0", begin: "560ms", dur: "520ms", fill: "freeze" })))
1237
+ 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" }))),
1238
+ showTangramDecomposition && layout.sectors.map((s) => {
1239
+ const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
1240
+ if (!sectorCfg?.silhouette.primitiveDecomposition) return null;
1241
+ const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
1242
+ const rect = rectForBand(layout, s, "silhouette", 1);
1243
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
1244
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
1245
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-borders-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(
1246
+ "path",
1247
+ {
1248
+ key: `prim-border-${i}`,
1249
+ d: pathD(scaledPoly),
1250
+ fill: "none",
1251
+ stroke: CONFIG.color.tangramDecomposition.stroke,
1252
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
1253
+ }
1254
+ )));
1255
+ })
1073
1256
  );
1074
1257
  }
1075
1258
 
@@ -2954,7 +3137,10 @@ function GameBoard(props) {
2954
3137
  onPieceRemove,
2955
3138
  onInteraction,
2956
3139
  onTrialEnd,
2957
- onControllerReady
3140
+ onControllerReady,
3141
+ usePrimitiveColorsBlueprints,
3142
+ usePrimitiveColorsTargets,
3143
+ primitiveColorIndices
2958
3144
  } = props;
2959
3145
  const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1e3) : 0);
2960
3146
  const controller = React.useMemo(() => {
@@ -3278,7 +3464,7 @@ function GameBoard(props) {
3278
3464
  justifyContent: "center",
3279
3465
  alignItems: "center",
3280
3466
  padding: "20px",
3281
- background: "#f5f5f5",
3467
+ background: CONFIG.color.background,
3282
3468
  flex: "0 0 auto"
3283
3469
  };
3284
3470
  const headerContentStyle = {
@@ -3294,14 +3480,20 @@ function GameBoard(props) {
3294
3480
  lineHeight: 1.5,
3295
3481
  textAlign: "center"
3296
3482
  };
3483
+ const scaleX = svgDimensions.width / viewBox.w;
3484
+ const rightEdgeOfSemicircleLogical = viewBox.w / 2 + layout.outerR;
3485
+ const distanceFromRightEdgeLogical = viewBox.w - rightEdgeOfSemicircleLogical;
3486
+ const distanceFromRightEdgePx = distanceFromRightEdgeLogical * scaleX;
3487
+ const charWidth = 24 * 0.6;
3488
+ const offsetLeft = charWidth * 5;
3297
3489
  const timerStyle = {
3298
3490
  position: "absolute",
3299
- right: 0,
3491
+ right: `${distanceFromRightEdgePx + offsetLeft}px`,
3300
3492
  fontSize: "24px",
3301
3493
  fontWeight: "bold",
3302
3494
  fontFamily: "monospace",
3303
3495
  color: "#333",
3304
- minWidth: "80px",
3496
+ whiteSpace: "nowrap",
3305
3497
  textAlign: "right"
3306
3498
  };
3307
3499
  const gameboardWrapperStyle = {
@@ -3346,6 +3538,9 @@ function GameBoard(props) {
3346
3538
  onCenterBadgePointerDown,
3347
3539
  showTangramDecomposition: showTangramDecomposition ?? false,
3348
3540
  scaleS,
3541
+ usePrimitiveColorsBlueprints: usePrimitiveColorsBlueprints ?? false,
3542
+ usePrimitiveColorsTargets: usePrimitiveColorsTargets ?? false,
3543
+ primitiveColorIndices: primitiveColorIndices ?? [0, 1, 2, 3, 4],
3349
3544
  ...eventCallbacks
3350
3545
  }
3351
3546
  ))));
@@ -3512,7 +3707,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3512
3707
  quickstashMacros,
3513
3708
  primitiveOrder,
3514
3709
  onInteraction,
3515
- onTrialEnd
3710
+ onTrialEnd,
3711
+ usePrimitiveColorsBlueprints,
3712
+ primitiveColorIndices
3516
3713
  } = params;
3517
3714
  const { onInteraction: _, onTrialEnd: __, ...trialParams } = params;
3518
3715
  const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => primitiveOrder.indexOf(a.kind) - primitiveOrder.indexOf(b.kind));
@@ -3604,7 +3801,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3604
3801
  ...params.instructions && { instructions: params.instructions },
3605
3802
  onControllerReady: handleControllerReady,
3606
3803
  ...onInteraction && { onInteraction },
3607
- ...onTrialEnd && { onTrialEnd }
3804
+ ...onTrialEnd && { onTrialEnd },
3805
+ ...usePrimitiveColorsBlueprints !== void 0 && { usePrimitiveColorsBlueprints },
3806
+ ...primitiveColorIndices !== void 0 && { primitiveColorIndices }
3608
3807
  }));
3609
3808
  return { root, display_element, jsPsych };
3610
3809
  }
@@ -3670,6 +3869,18 @@ const info = {
3670
3869
  onTrialEnd: {
3671
3870
  type: jspsych.ParameterType.FUNCTION,
3672
3871
  default: void 0
3872
+ },
3873
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
3874
+ use_primitive_colors_blueprints: {
3875
+ type: jspsych.ParameterType.BOOL,
3876
+ default: false,
3877
+ description: "Whether each primitive shape type should have its own distinct color in the blueprint dock area"
3878
+ },
3879
+ /** Indices mapping primitives to colors from the color palette */
3880
+ primitive_color_indices: {
3881
+ type: jspsych.ParameterType.OBJECT,
3882
+ default: [0, 1, 2, 3, 4],
3883
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
3673
3884
  }
3674
3885
  },
3675
3886
  data: {
@@ -3714,7 +3925,9 @@ class TangramPrepPlugin {
3714
3925
  primitiveOrder: trial.primitive_order,
3715
3926
  instructions: trial.instructions,
3716
3927
  onInteraction: trial.onInteraction,
3717
- onTrialEnd: wrappedOnTrialEnd
3928
+ onTrialEnd: wrappedOnTrialEnd,
3929
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
3930
+ primitiveColorIndices: trial.primitive_color_indices
3718
3931
  };
3719
3932
  const { root, display_element: element, jsPsych } = startPrepTrial(display_element, params, this.jsPsych);
3720
3933
  element.__reactContext = { root, jsPsych };