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
package/dist/index.cjs CHANGED
@@ -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
  ))));
@@ -3565,7 +3760,10 @@ function startConstructionTrial(display_element, params, _jsPsych) {
3565
3760
  trialParams,
3566
3761
  ...params.instructions && { instructions: params.instructions },
3567
3762
  ...params.onInteraction && { onInteraction: params.onInteraction },
3568
- ...params.onTrialEnd && { onTrialEnd: params.onTrialEnd }
3763
+ ...params.onTrialEnd && { onTrialEnd: params.onTrialEnd },
3764
+ ...params.usePrimitiveColorsBlueprints !== void 0 && { usePrimitiveColorsBlueprints: params.usePrimitiveColorsBlueprints },
3765
+ ...params.usePrimitiveColorsTargets !== void 0 && { usePrimitiveColorsTargets: params.usePrimitiveColorsTargets },
3766
+ ...params.primitiveColorIndices !== void 0 && { primitiveColorIndices: params.primitiveColorIndices }
3569
3767
  };
3570
3768
  const root = client.createRoot(display_element);
3571
3769
  root.render(React.createElement(GameBoard, gameBoardProps));
@@ -3650,6 +3848,24 @@ const info$2 = {
3650
3848
  type: jspsych.ParameterType.FUNCTION,
3651
3849
  default: void 0,
3652
3850
  description: "Callback when trial completes with full data"
3851
+ },
3852
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
3853
+ use_primitive_colors_blueprints: {
3854
+ type: jspsych.ParameterType.BOOL,
3855
+ default: false,
3856
+ description: "Whether each primitive shape type should have its own distinct color in the blueprint dock area"
3857
+ },
3858
+ /** Whether to use distinct colors for each primitive shape type in target tangrams */
3859
+ use_primitive_colors_targets: {
3860
+ type: jspsych.ParameterType.BOOL,
3861
+ default: false,
3862
+ description: "Whether each primitive shape type should have its own distinct color in target tangram silhouettes"
3863
+ },
3864
+ /** Indices mapping primitives to colors from the color palette */
3865
+ primitive_color_indices: {
3866
+ type: jspsych.ParameterType.OBJECT,
3867
+ default: [0, 1, 2, 3, 4],
3868
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
3653
3869
  }
3654
3870
  },
3655
3871
  data: {
@@ -3715,7 +3931,10 @@ class TangramConstructPlugin {
3715
3931
  show_tangram_decomposition: trial.show_tangram_decomposition,
3716
3932
  instructions: trial.instructions,
3717
3933
  onInteraction: trial.onInteraction,
3718
- onTrialEnd: wrappedOnTrialEnd
3934
+ onTrialEnd: wrappedOnTrialEnd,
3935
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
3936
+ usePrimitiveColorsTargets: trial.use_primitive_colors_targets,
3937
+ primitiveColorIndices: trial.primitive_color_indices
3719
3938
  };
3720
3939
  const { root, display_element: element, jsPsych } = startConstructionTrial(display_element, params, this.jsPsych);
3721
3940
  element.__reactContext = { root, jsPsych };
@@ -3733,7 +3952,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3733
3952
  quickstashMacros,
3734
3953
  primitiveOrder,
3735
3954
  onInteraction,
3736
- onTrialEnd
3955
+ onTrialEnd,
3956
+ usePrimitiveColorsBlueprints,
3957
+ primitiveColorIndices
3737
3958
  } = params;
3738
3959
  const { onInteraction: _, onTrialEnd: __, ...trialParams } = params;
3739
3960
  const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => primitiveOrder.indexOf(a.kind) - primitiveOrder.indexOf(b.kind));
@@ -3825,7 +4046,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3825
4046
  ...params.instructions && { instructions: params.instructions },
3826
4047
  onControllerReady: handleControllerReady,
3827
4048
  ...onInteraction && { onInteraction },
3828
- ...onTrialEnd && { onTrialEnd }
4049
+ ...onTrialEnd && { onTrialEnd },
4050
+ ...usePrimitiveColorsBlueprints !== void 0 && { usePrimitiveColorsBlueprints },
4051
+ ...primitiveColorIndices !== void 0 && { primitiveColorIndices }
3829
4052
  }));
3830
4053
  return { root, display_element, jsPsych };
3831
4054
  }
@@ -3891,6 +4114,18 @@ const info$1 = {
3891
4114
  onTrialEnd: {
3892
4115
  type: jspsych.ParameterType.FUNCTION,
3893
4116
  default: void 0
4117
+ },
4118
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
4119
+ use_primitive_colors_blueprints: {
4120
+ type: jspsych.ParameterType.BOOL,
4121
+ default: false,
4122
+ description: "Whether each primitive shape type should have its own distinct color in the blueprint dock area"
4123
+ },
4124
+ /** Indices mapping primitives to colors from the color palette */
4125
+ primitive_color_indices: {
4126
+ type: jspsych.ParameterType.OBJECT,
4127
+ default: [0, 1, 2, 3, 4],
4128
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
3894
4129
  }
3895
4130
  },
3896
4131
  data: {
@@ -3935,7 +4170,9 @@ class TangramPrepPlugin {
3935
4170
  primitiveOrder: trial.primitive_order,
3936
4171
  instructions: trial.instructions,
3937
4172
  onInteraction: trial.onInteraction,
3938
- onTrialEnd: wrappedOnTrialEnd
4173
+ onTrialEnd: wrappedOnTrialEnd,
4174
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
4175
+ primitiveColorIndices: trial.primitive_color_indices
3939
4176
  };
3940
4177
  const { root, display_element: element, jsPsych } = startPrepTrial(display_element, params, this.jsPsych);
3941
4178
  element.__reactContext = { root, jsPsych };
@@ -3955,6 +4192,8 @@ function NBackView({ params }) {
3955
4192
  instructions,
3956
4193
  button_text,
3957
4194
  duration,
4195
+ usePrimitiveColors,
4196
+ primitiveColorIndices,
3958
4197
  onTrialEnd
3959
4198
  } = params;
3960
4199
  const trialStartTime = React.useRef(Date.now());
@@ -4010,7 +4249,12 @@ function NBackView({ params }) {
4010
4249
  ...data,
4011
4250
  accuracy,
4012
4251
  tangram_id: tangram.tangramID,
4013
- is_match: isMatch
4252
+ is_match: isMatch,
4253
+ show_tangram_decomposition,
4254
+ use_primitive_colors: usePrimitiveColors,
4255
+ primitive_color_indices: primitiveColorIndices,
4256
+ duration,
4257
+ button_text
4014
4258
  };
4015
4259
  if (onTrialEnd) {
4016
4260
  onTrialEnd(trialData);
@@ -4064,35 +4308,77 @@ function NBackView({ params }) {
4064
4308
  if (show_tangram_decomposition) {
4065
4309
  const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
4066
4310
  const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, centerPos);
4067
- return /* @__PURE__ */ React.createElement("g", { key: "sil-decomposed", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(React.Fragment, { key: `prim-${i}` }, /* @__PURE__ */ React.createElement(
4068
- "path",
4069
- {
4070
- d: pathD(scaledPoly),
4071
- fill: CONFIG.color.silhouetteMask,
4072
- opacity: CONFIG.opacity.silhouetteMask,
4073
- stroke: "none"
4074
- }
4075
- ), /* @__PURE__ */ React.createElement(
4076
- "path",
4077
- {
4078
- d: pathD(scaledPoly),
4079
- fill: "none",
4080
- stroke: CONFIG.color.tangramDecomposition.stroke,
4081
- strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
4311
+ return /* @__PURE__ */ React.createElement("g", { key: "sil-decomposed", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
4312
+ const primInfo = primitiveDecomposition[i];
4313
+ let fillColor = CONFIG.color.silhouetteMask;
4314
+ if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {
4315
+ const kindToIndex = {
4316
+ "square": 0,
4317
+ "smalltriangle": 1,
4318
+ "parallelogram": 2,
4319
+ "medtriangle": 3,
4320
+ "largetriangle": 4
4321
+ };
4322
+ const primitiveIndex = kindToIndex[primInfo.kind];
4323
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
4324
+ const colorIndex = primitiveColorIndices[primitiveIndex];
4325
+ const color = CONFIG.color.primitiveColors[colorIndex];
4326
+ if (color) {
4327
+ fillColor = color;
4328
+ }
4329
+ }
4082
4330
  }
4083
- ))));
4331
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: `prim-${i}` }, /* @__PURE__ */ React.createElement(
4332
+ "path",
4333
+ {
4334
+ d: pathD(scaledPoly),
4335
+ fill: fillColor,
4336
+ opacity: CONFIG.opacity.silhouetteMask,
4337
+ stroke: "none"
4338
+ }
4339
+ ), /* @__PURE__ */ React.createElement(
4340
+ "path",
4341
+ {
4342
+ d: pathD(scaledPoly),
4343
+ fill: "none",
4344
+ stroke: CONFIG.color.tangramDecomposition.stroke,
4345
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
4346
+ }
4347
+ ));
4348
+ }));
4084
4349
  } else {
4085
4350
  const placedPolys = placeSilhouetteGridAlignedAsPolys(mask, scaleS, centerPos);
4086
- return /* @__PURE__ */ React.createElement("g", { key: "sil-unified", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(
4087
- "path",
4088
- {
4089
- key: `sil-${i}`,
4090
- d: pathD(scaledPoly),
4091
- fill: CONFIG.color.silhouetteMask,
4092
- opacity: CONFIG.opacity.silhouetteMask,
4093
- stroke: "none"
4351
+ return /* @__PURE__ */ React.createElement("g", { key: "sil-unified", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
4352
+ const primInfo = primitiveDecomposition[i];
4353
+ let fillColor = CONFIG.color.silhouetteMask;
4354
+ if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {
4355
+ const kindToIndex = {
4356
+ "square": 0,
4357
+ "smalltriangle": 1,
4358
+ "parallelogram": 2,
4359
+ "medtriangle": 3,
4360
+ "largetriangle": 4
4361
+ };
4362
+ const primitiveIndex = kindToIndex[primInfo.kind];
4363
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
4364
+ const colorIndex = primitiveColorIndices[primitiveIndex];
4365
+ const color = CONFIG.color.primitiveColors[colorIndex];
4366
+ if (color) {
4367
+ fillColor = color;
4368
+ }
4369
+ }
4094
4370
  }
4095
- )));
4371
+ return /* @__PURE__ */ React.createElement(
4372
+ "path",
4373
+ {
4374
+ key: `sil-${i}`,
4375
+ d: pathD(scaledPoly),
4376
+ fill: fillColor,
4377
+ opacity: CONFIG.opacity.silhouetteMask,
4378
+ stroke: "none"
4379
+ }
4380
+ );
4381
+ }));
4096
4382
  }
4097
4383
  };
4098
4384
  return /* @__PURE__ */ React.createElement("div", { style: {
@@ -4101,7 +4387,7 @@ function NBackView({ params }) {
4101
4387
  alignItems: "center",
4102
4388
  justifyContent: "center",
4103
4389
  padding: "20px",
4104
- background: "#f5f5f5"
4390
+ background: "#fff7e0ff"
4105
4391
  } }, instructions && /* @__PURE__ */ React.createElement(
4106
4392
  "div",
4107
4393
  {
@@ -4191,6 +4477,18 @@ const info = {
4191
4477
  default: 3e3,
4192
4478
  description: "Duration in milliseconds to display tangram and accept responses"
4193
4479
  },
4480
+ /** Whether to use distinct colors for each primitive shape type */
4481
+ use_primitive_colors: {
4482
+ type: jspsych.ParameterType.BOOL,
4483
+ default: false,
4484
+ description: "Whether each primitive shape type should have its own distinct color in the displayed tangram"
4485
+ },
4486
+ /** Indices mapping primitives to colors from the color palette */
4487
+ primitive_color_indices: {
4488
+ type: jspsych.ParameterType.OBJECT,
4489
+ default: [0, 1, 2, 3, 4],
4490
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
4491
+ },
4194
4492
  /** Callback fired when trial ends */
4195
4493
  onTrialEnd: {
4196
4494
  type: jspsych.ParameterType.FUNCTION,
@@ -4257,6 +4555,8 @@ class TangramNBackPlugin {
4257
4555
  instructions: trial.instructions,
4258
4556
  button_text: trial.button_text,
4259
4557
  duration: trial.duration,
4558
+ usePrimitiveColors: trial.use_primitive_colors,
4559
+ primitiveColorIndices: trial.primitive_color_indices,
4260
4560
  onTrialEnd: wrappedOnTrialEnd
4261
4561
  };
4262
4562
  const { root, display_element: element, jsPsych } = startNBackTrial(display_element, params, this.jsPsych);