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.js CHANGED
@@ -5,30 +5,41 @@ 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
+ // validFill used here for placed composites
17
+ piece: { draggingFill: "#8e7cc3", validFill: "#8e7cc3", invalidFill: "#d55c00", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
16
18
  ui: { light: "#60a5fa", dark: "#1d4ed8" },
17
19
  blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
18
- tangramDecomposition: { stroke: "#fef2cc" }
20
+ tangramDecomposition: { stroke: "#fef2cc" },
21
+ primitiveColors: [
22
+ // from seaborn "colorblind" palette, 6 colors, with red omitted
23
+ "#0173b2",
24
+ "#de8f05",
25
+ "#029e73",
26
+ "#cc78bc",
27
+ "#ca9161"
28
+ ]
19
29
  },
20
30
  opacity: {
21
- blueprint: 0.4,
31
+ blueprint: 0.6,
22
32
  silhouetteMask: 0.25,
23
33
  //anchors: { valid: 0.80, invalid: 0.50 },
24
34
  anchors: { invalid: 0, valid: 0 },
25
- piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
35
+ piece: { invalid: 1, dragging: 1, locked: 1, normal: 1 }
26
36
  },
27
37
  size: {
28
- stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
38
+ stroke: { bandPx: 5, pieceSelectedPx: 5, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
29
39
  anchorRadiusPx: { valid: 1, invalid: 1 },
30
40
  badgeFontPx: 16,
31
- centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
41
+ centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 },
42
+ invalidMarker: { sizePx: 10, strokePx: 4 }
32
43
  },
33
44
  layout: {
34
45
  grid: { stepPx: 20, unitPx: 40 },
@@ -46,7 +57,8 @@ const CONFIG = {
46
57
  game: {
47
58
  snapRadiusPx: 15,
48
59
  showBorders: false,
49
- hideTouchingBorders: true
60
+ hideTouchingBorders: true,
61
+ silhouettesBelowPieces: true
50
62
  }
51
63
  };
52
64
 
@@ -830,6 +842,31 @@ var unlockedIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAYAAA
830
842
  function pathD(poly) {
831
843
  return `M ${poly.map((pt) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
832
844
  }
845
+ function getPieceColor(blueprint, usePrimitiveColors, defaultColor, primitiveColorIndices) {
846
+ if (!usePrimitiveColors) {
847
+ return defaultColor;
848
+ }
849
+ if ("kind" in blueprint) {
850
+ const kind = blueprint.kind;
851
+ const kindToIndex = {
852
+ "square": 0,
853
+ "smalltriangle": 1,
854
+ "parallelogram": 2,
855
+ "medtriangle": 3,
856
+ "largetriangle": 4
857
+ };
858
+ const primitiveIndex = kindToIndex[kind];
859
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
860
+ const colorIndex = primitiveColorIndices[primitiveIndex];
861
+ const color = CONFIG.color.primitiveColors[colorIndex];
862
+ if (color) {
863
+ return color;
864
+ }
865
+ }
866
+ return defaultColor;
867
+ }
868
+ return defaultColor;
869
+ }
833
870
  function BoardView(props) {
834
871
  const {
835
872
  controller,
@@ -849,6 +886,9 @@ function BoardView(props) {
849
886
  dragInvalid,
850
887
  lockedPieceId,
851
888
  showTangramDecomposition,
889
+ usePrimitiveColorsBlueprints,
890
+ usePrimitiveColorsTargets,
891
+ primitiveColorIndices,
852
892
  svgRef,
853
893
  setPieceRef,
854
894
  onPiecePointerDown,
@@ -859,6 +899,144 @@ function BoardView(props) {
859
899
  onCenterBadgePointerDown
860
900
  } = props;
861
901
  const VW = viewBox.w, VH = viewBox.h;
902
+ const renderSilhouettes = () => layout.sectors.map((s) => {
903
+ const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
904
+ if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
905
+ const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
906
+ const rect = rectForBand(layout, s, "silhouette", 1);
907
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
908
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
909
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
910
+ const primInfo = primitiveDecomposition[i];
911
+ let fillColor = CONFIG.color.silhouetteMask;
912
+ if (usePrimitiveColorsTargets && primInfo?.kind && primitiveColorIndices) {
913
+ const kindToIndex = {
914
+ "square": 0,
915
+ "smalltriangle": 1,
916
+ "parallelogram": 2,
917
+ "medtriangle": 3,
918
+ "largetriangle": 4
919
+ };
920
+ const primitiveIndex = kindToIndex[primInfo.kind];
921
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
922
+ const colorIndex = primitiveColorIndices[primitiveIndex];
923
+ const color = CONFIG.color.primitiveColors[colorIndex];
924
+ if (color) {
925
+ fillColor = color;
926
+ }
927
+ }
928
+ }
929
+ return /* @__PURE__ */ React.createElement(
930
+ "path",
931
+ {
932
+ key: `prim-fill-${i}`,
933
+ d: pathD(scaledPoly),
934
+ fill: fillColor,
935
+ opacity: CONFIG.opacity.silhouetteMask,
936
+ stroke: "none"
937
+ }
938
+ );
939
+ }));
940
+ } else {
941
+ const placedPolys = placedSilBySector.get(s.id) ?? [];
942
+ if (!placedPolys.length) return null;
943
+ 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 })));
944
+ }
945
+ });
946
+ const renderPieces = (piecesFilter) => {
947
+ const piecesToRender = pieces;
948
+ return piecesToRender.sort((a, b) => {
949
+ if (draggingId === a.id) return 1;
950
+ if (draggingId === b.id) return -1;
951
+ return 0;
952
+ }).map((p) => {
953
+ const bp = controller.getBlueprint(p.blueprintId);
954
+ const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
955
+ const isDragging = draggingId === p.id;
956
+ const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
957
+ const isConnectivityLocked = lockedPieceId === p.id;
958
+ selectedPieceId === p.id;
959
+ const isCarriedInvalid = isDragging && dragInvalid;
960
+ const translateX = p.x - bb.min.x;
961
+ const translateY = p.y - bb.min.y;
962
+ const validFillColor = getPieceColor(
963
+ bp,
964
+ usePrimitiveColorsBlueprints || false,
965
+ CONFIG.color.piece.validFill,
966
+ primitiveColorIndices || [0, 1, 2, 3, 4]
967
+ );
968
+ const draggingFillColor = getPieceColor(
969
+ bp,
970
+ usePrimitiveColorsBlueprints || false,
971
+ CONFIG.color.piece.draggingFill,
972
+ primitiveColorIndices || [0, 1, 2, 3, 4]
973
+ );
974
+ 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) => {
975
+ const showBorders = shouldShowBorders();
976
+ shouldUseSelectiveBorders(p.blueprintId);
977
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
978
+ "path",
979
+ {
980
+ d: pathD(poly),
981
+ fill: isConnectivityLocked ? CONFIG.color.piece.invalidFill : isCarriedInvalid ? CONFIG.color.piece.invalidFill : isDragging ? draggingFillColor : validFillColor,
982
+ 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,
983
+ stroke: "none",
984
+ onPointerDown: (e) => onPiecePointerDown(e, p)
985
+ }
986
+ ), showBorders);
987
+ }), isDragging && isCarriedInvalid && "shape" in bp && bp.shape.length > 0 && (() => {
988
+ const { cx, cy } = polysAABB$1(bp.shape);
989
+ const size = CONFIG.size.invalidMarker.sizePx;
990
+ const strokeWidth = CONFIG.size.invalidMarker.strokePx;
991
+ const borderWidth = strokeWidth + 2;
992
+ return /* @__PURE__ */ React.createElement("g", { key: "invalid-marker" }, /* @__PURE__ */ React.createElement(
993
+ "line",
994
+ {
995
+ x1: cx - size,
996
+ y1: cy - size,
997
+ x2: cx + size,
998
+ y2: cy + size,
999
+ stroke: "white",
1000
+ strokeWidth: borderWidth,
1001
+ strokeLinecap: "round"
1002
+ }
1003
+ ), /* @__PURE__ */ React.createElement(
1004
+ "line",
1005
+ {
1006
+ x1: cx - size,
1007
+ y1: cy - size,
1008
+ x2: cx + size,
1009
+ y2: cy + size,
1010
+ stroke: CONFIG.color.piece.invalidStroke,
1011
+ strokeWidth,
1012
+ strokeLinecap: "round"
1013
+ }
1014
+ ), /* @__PURE__ */ React.createElement(
1015
+ "line",
1016
+ {
1017
+ x1: cx + size,
1018
+ y1: cy - size,
1019
+ x2: cx - size,
1020
+ y2: cy + size,
1021
+ stroke: "white",
1022
+ strokeWidth: borderWidth,
1023
+ strokeLinecap: "round"
1024
+ }
1025
+ ), /* @__PURE__ */ React.createElement(
1026
+ "line",
1027
+ {
1028
+ x1: cx + size,
1029
+ y1: cy - size,
1030
+ x2: cx - size,
1031
+ y2: cy + size,
1032
+ stroke: CONFIG.color.piece.invalidStroke,
1033
+ strokeWidth,
1034
+ strokeLinecap: "round"
1035
+ }
1036
+ ));
1037
+ })());
1038
+ });
1039
+ };
862
1040
  const centerView = controller.state.blueprintView;
863
1041
  const bps = centerView === "primitives" ? controller.state.primitives : controller.state.quickstash;
864
1042
  const QS_SLOTS = controller.state.cfg.maxQuickstashSlots;
@@ -879,6 +1057,12 @@ function BoardView(props) {
879
1057
  const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
880
1058
  const cx = bb.min.x + bb.width / 2;
881
1059
  const cy = bb.min.y + bb.height / 2;
1060
+ const fillColor = getPieceColor(
1061
+ bp,
1062
+ usePrimitiveColorsBlueprints || false,
1063
+ CONFIG.color.blueprint.fill,
1064
+ primitiveColorIndices || [0, 1, 2, 3, 4]
1065
+ );
882
1066
  return /* @__PURE__ */ React.createElement(
883
1067
  "g",
884
1068
  {
@@ -890,7 +1074,7 @@ function BoardView(props) {
890
1074
  {
891
1075
  key: idx,
892
1076
  d: pathD(poly),
893
- fill: CONFIG.color.blueprint.fill,
1077
+ fill: fillColor,
894
1078
  opacity: CONFIG.opacity.blueprint,
895
1079
  stroke: "none",
896
1080
  strokeWidth: 0,
@@ -914,7 +1098,7 @@ function BoardView(props) {
914
1098
  onPointerDown: (e) => {
915
1099
  onRootPointerDown(e);
916
1100
  },
917
- style: { background: "#f5f5f5", touchAction: "none", userSelect: "none" }
1101
+ style: { background: CONFIG.color.background, touchAction: "none", userSelect: "none" }
918
1102
  },
919
1103
  layout.sectors.map((s, i) => {
920
1104
  const done = !!controller.state.sectors[s.id].completedAt;
@@ -924,35 +1108,7 @@ function BoardView(props) {
924
1108
  const work = layout.bands.workspace;
925
1109
  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
1110
  }),
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
- }),
1111
+ /* @__PURE__ */ React.createElement(React.Fragment, null, renderSilhouettes(), renderPieces()) ,
956
1112
  layout.sectors.map((s) => {
957
1113
  const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
958
1114
  if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
@@ -960,23 +1116,32 @@ function BoardView(props) {
960
1116
  const rect = rectForBand(layout, s, "silhouette", 1);
961
1117
  const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
962
1118
  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
1119
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
1120
+ const primInfo = primitiveDecomposition[i];
1121
+ if (usePrimitiveColorsTargets && primInfo?.kind && primitiveColorIndices) {
1122
+ const kindToIndex = {
1123
+ "square": 0,
1124
+ "smalltriangle": 1,
1125
+ "parallelogram": 2,
1126
+ "medtriangle": 3,
1127
+ "largetriangle": 4
1128
+ };
1129
+ const primitiveIndex = kindToIndex[primInfo.kind];
1130
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
1131
+ primitiveColorIndices[primitiveIndex];
1132
+ }
978
1133
  }
979
- ))));
1134
+ return /* @__PURE__ */ React.createElement(
1135
+ "path",
1136
+ {
1137
+ key: `prim-fill-${i}`,
1138
+ d: pathD(scaledPoly),
1139
+ fill: "none",
1140
+ opacity: 0,
1141
+ stroke: "none"
1142
+ }
1143
+ );
1144
+ }));
980
1145
  } else {
981
1146
  const placedPolys = placedSilBySector.get(s.id) ?? [];
982
1147
  if (!placedPolys.length) return null;
@@ -1067,7 +1232,25 @@ function BoardView(props) {
1067
1232
  const by = layout.cy + blueprintRingR * Math.sin(theta);
1068
1233
  return renderBlueprintGlyph(bp, bx, by);
1069
1234
  }),
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" })))
1235
+ 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" }))),
1236
+ showTangramDecomposition && layout.sectors.map((s) => {
1237
+ const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
1238
+ if (!sectorCfg?.silhouette.primitiveDecomposition) return null;
1239
+ const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
1240
+ const rect = rectForBand(layout, s, "silhouette", 1);
1241
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
1242
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
1243
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-borders-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(
1244
+ "path",
1245
+ {
1246
+ key: `prim-border-${i}`,
1247
+ d: pathD(scaledPoly),
1248
+ fill: "none",
1249
+ stroke: CONFIG.color.tangramDecomposition.stroke,
1250
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
1251
+ }
1252
+ )));
1253
+ })
1071
1254
  );
1072
1255
  }
1073
1256
 
@@ -2952,7 +3135,10 @@ function GameBoard(props) {
2952
3135
  onPieceRemove,
2953
3136
  onInteraction,
2954
3137
  onTrialEnd,
2955
- onControllerReady
3138
+ onControllerReady,
3139
+ usePrimitiveColorsBlueprints,
3140
+ usePrimitiveColorsTargets,
3141
+ primitiveColorIndices
2956
3142
  } = props;
2957
3143
  const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1e3) : 0);
2958
3144
  const controller = React.useMemo(() => {
@@ -3276,7 +3462,7 @@ function GameBoard(props) {
3276
3462
  justifyContent: "center",
3277
3463
  alignItems: "center",
3278
3464
  padding: "20px",
3279
- background: "#f5f5f5",
3465
+ background: CONFIG.color.background,
3280
3466
  flex: "0 0 auto"
3281
3467
  };
3282
3468
  const headerContentStyle = {
@@ -3292,14 +3478,20 @@ function GameBoard(props) {
3292
3478
  lineHeight: 1.5,
3293
3479
  textAlign: "center"
3294
3480
  };
3481
+ const scaleX = svgDimensions.width / viewBox.w;
3482
+ const rightEdgeOfSemicircleLogical = viewBox.w / 2 + layout.outerR;
3483
+ const distanceFromRightEdgeLogical = viewBox.w - rightEdgeOfSemicircleLogical;
3484
+ const distanceFromRightEdgePx = distanceFromRightEdgeLogical * scaleX;
3485
+ const charWidth = 24 * 0.6;
3486
+ const offsetLeft = charWidth * 5;
3295
3487
  const timerStyle = {
3296
3488
  position: "absolute",
3297
- right: 0,
3489
+ right: `${distanceFromRightEdgePx + offsetLeft}px`,
3298
3490
  fontSize: "24px",
3299
3491
  fontWeight: "bold",
3300
3492
  fontFamily: "monospace",
3301
3493
  color: "#333",
3302
- minWidth: "80px",
3494
+ whiteSpace: "nowrap",
3303
3495
  textAlign: "right"
3304
3496
  };
3305
3497
  const gameboardWrapperStyle = {
@@ -3344,6 +3536,9 @@ function GameBoard(props) {
3344
3536
  onCenterBadgePointerDown,
3345
3537
  showTangramDecomposition: showTangramDecomposition ?? false,
3346
3538
  scaleS,
3539
+ usePrimitiveColorsBlueprints: usePrimitiveColorsBlueprints ?? false,
3540
+ usePrimitiveColorsTargets: usePrimitiveColorsTargets ?? false,
3541
+ primitiveColorIndices: primitiveColorIndices ?? [0, 1, 2, 3, 4],
3347
3542
  ...eventCallbacks
3348
3543
  }
3349
3544
  ))));
@@ -3563,7 +3758,10 @@ function startConstructionTrial(display_element, params, _jsPsych) {
3563
3758
  trialParams,
3564
3759
  ...params.instructions && { instructions: params.instructions },
3565
3760
  ...params.onInteraction && { onInteraction: params.onInteraction },
3566
- ...params.onTrialEnd && { onTrialEnd: params.onTrialEnd }
3761
+ ...params.onTrialEnd && { onTrialEnd: params.onTrialEnd },
3762
+ ...params.usePrimitiveColorsBlueprints !== void 0 && { usePrimitiveColorsBlueprints: params.usePrimitiveColorsBlueprints },
3763
+ ...params.usePrimitiveColorsTargets !== void 0 && { usePrimitiveColorsTargets: params.usePrimitiveColorsTargets },
3764
+ ...params.primitiveColorIndices !== void 0 && { primitiveColorIndices: params.primitiveColorIndices }
3567
3765
  };
3568
3766
  const root = createRoot(display_element);
3569
3767
  root.render(React.createElement(GameBoard, gameBoardProps));
@@ -3648,6 +3846,24 @@ const info$2 = {
3648
3846
  type: ParameterType.FUNCTION,
3649
3847
  default: void 0,
3650
3848
  description: "Callback when trial completes with full data"
3849
+ },
3850
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
3851
+ use_primitive_colors_blueprints: {
3852
+ type: ParameterType.BOOL,
3853
+ default: false,
3854
+ description: "Whether each primitive shape type should have its own distinct color in the blueprint dock area"
3855
+ },
3856
+ /** Whether to use distinct colors for each primitive shape type in target tangrams */
3857
+ use_primitive_colors_targets: {
3858
+ type: ParameterType.BOOL,
3859
+ default: false,
3860
+ description: "Whether each primitive shape type should have its own distinct color in target tangram silhouettes"
3861
+ },
3862
+ /** Indices mapping primitives to colors from the color palette */
3863
+ primitive_color_indices: {
3864
+ type: ParameterType.OBJECT,
3865
+ default: [0, 1, 2, 3, 4],
3866
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
3651
3867
  }
3652
3868
  },
3653
3869
  data: {
@@ -3713,7 +3929,10 @@ class TangramConstructPlugin {
3713
3929
  show_tangram_decomposition: trial.show_tangram_decomposition,
3714
3930
  instructions: trial.instructions,
3715
3931
  onInteraction: trial.onInteraction,
3716
- onTrialEnd: wrappedOnTrialEnd
3932
+ onTrialEnd: wrappedOnTrialEnd,
3933
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
3934
+ usePrimitiveColorsTargets: trial.use_primitive_colors_targets,
3935
+ primitiveColorIndices: trial.primitive_color_indices
3717
3936
  };
3718
3937
  const { root, display_element: element, jsPsych } = startConstructionTrial(display_element, params, this.jsPsych);
3719
3938
  element.__reactContext = { root, jsPsych };
@@ -3731,7 +3950,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3731
3950
  quickstashMacros,
3732
3951
  primitiveOrder,
3733
3952
  onInteraction,
3734
- onTrialEnd
3953
+ onTrialEnd,
3954
+ usePrimitiveColorsBlueprints,
3955
+ primitiveColorIndices
3735
3956
  } = params;
3736
3957
  const { onInteraction: _, onTrialEnd: __, ...trialParams } = params;
3737
3958
  const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => primitiveOrder.indexOf(a.kind) - primitiveOrder.indexOf(b.kind));
@@ -3823,7 +4044,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3823
4044
  ...params.instructions && { instructions: params.instructions },
3824
4045
  onControllerReady: handleControllerReady,
3825
4046
  ...onInteraction && { onInteraction },
3826
- ...onTrialEnd && { onTrialEnd }
4047
+ ...onTrialEnd && { onTrialEnd },
4048
+ ...usePrimitiveColorsBlueprints !== void 0 && { usePrimitiveColorsBlueprints },
4049
+ ...primitiveColorIndices !== void 0 && { primitiveColorIndices }
3827
4050
  }));
3828
4051
  return { root, display_element, jsPsych };
3829
4052
  }
@@ -3889,6 +4112,18 @@ const info$1 = {
3889
4112
  onTrialEnd: {
3890
4113
  type: ParameterType.FUNCTION,
3891
4114
  default: void 0
4115
+ },
4116
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
4117
+ use_primitive_colors_blueprints: {
4118
+ type: ParameterType.BOOL,
4119
+ default: false,
4120
+ description: "Whether each primitive shape type should have its own distinct color in the blueprint dock area"
4121
+ },
4122
+ /** Indices mapping primitives to colors from the color palette */
4123
+ primitive_color_indices: {
4124
+ type: ParameterType.OBJECT,
4125
+ default: [0, 1, 2, 3, 4],
4126
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
3892
4127
  }
3893
4128
  },
3894
4129
  data: {
@@ -3933,7 +4168,9 @@ class TangramPrepPlugin {
3933
4168
  primitiveOrder: trial.primitive_order,
3934
4169
  instructions: trial.instructions,
3935
4170
  onInteraction: trial.onInteraction,
3936
- onTrialEnd: wrappedOnTrialEnd
4171
+ onTrialEnd: wrappedOnTrialEnd,
4172
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
4173
+ primitiveColorIndices: trial.primitive_color_indices
3937
4174
  };
3938
4175
  const { root, display_element: element, jsPsych } = startPrepTrial(display_element, params, this.jsPsych);
3939
4176
  element.__reactContext = { root, jsPsych };
@@ -3953,6 +4190,8 @@ function NBackView({ params }) {
3953
4190
  instructions,
3954
4191
  button_text,
3955
4192
  duration,
4193
+ usePrimitiveColors,
4194
+ primitiveColorIndices,
3956
4195
  onTrialEnd
3957
4196
  } = params;
3958
4197
  const trialStartTime = useRef(Date.now());
@@ -4008,7 +4247,12 @@ function NBackView({ params }) {
4008
4247
  ...data,
4009
4248
  accuracy,
4010
4249
  tangram_id: tangram.tangramID,
4011
- is_match: isMatch
4250
+ is_match: isMatch,
4251
+ show_tangram_decomposition,
4252
+ use_primitive_colors: usePrimitiveColors,
4253
+ primitive_color_indices: primitiveColorIndices,
4254
+ duration,
4255
+ button_text
4012
4256
  };
4013
4257
  if (onTrialEnd) {
4014
4258
  onTrialEnd(trialData);
@@ -4062,35 +4306,77 @@ function NBackView({ params }) {
4062
4306
  if (show_tangram_decomposition) {
4063
4307
  const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
4064
4308
  const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, centerPos);
4065
- 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(
4066
- "path",
4067
- {
4068
- d: pathD(scaledPoly),
4069
- fill: CONFIG.color.silhouetteMask,
4070
- opacity: CONFIG.opacity.silhouetteMask,
4071
- stroke: "none"
4072
- }
4073
- ), /* @__PURE__ */ React.createElement(
4074
- "path",
4075
- {
4076
- d: pathD(scaledPoly),
4077
- fill: "none",
4078
- stroke: CONFIG.color.tangramDecomposition.stroke,
4079
- strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
4309
+ return /* @__PURE__ */ React.createElement("g", { key: "sil-decomposed", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
4310
+ const primInfo = primitiveDecomposition[i];
4311
+ let fillColor = CONFIG.color.silhouetteMask;
4312
+ if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {
4313
+ const kindToIndex = {
4314
+ "square": 0,
4315
+ "smalltriangle": 1,
4316
+ "parallelogram": 2,
4317
+ "medtriangle": 3,
4318
+ "largetriangle": 4
4319
+ };
4320
+ const primitiveIndex = kindToIndex[primInfo.kind];
4321
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
4322
+ const colorIndex = primitiveColorIndices[primitiveIndex];
4323
+ const color = CONFIG.color.primitiveColors[colorIndex];
4324
+ if (color) {
4325
+ fillColor = color;
4326
+ }
4327
+ }
4080
4328
  }
4081
- ))));
4329
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: `prim-${i}` }, /* @__PURE__ */ React.createElement(
4330
+ "path",
4331
+ {
4332
+ d: pathD(scaledPoly),
4333
+ fill: fillColor,
4334
+ opacity: CONFIG.opacity.silhouetteMask,
4335
+ stroke: "none"
4336
+ }
4337
+ ), /* @__PURE__ */ React.createElement(
4338
+ "path",
4339
+ {
4340
+ d: pathD(scaledPoly),
4341
+ fill: "none",
4342
+ stroke: CONFIG.color.tangramDecomposition.stroke,
4343
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
4344
+ }
4345
+ ));
4346
+ }));
4082
4347
  } else {
4083
4348
  const placedPolys = placeSilhouetteGridAlignedAsPolys(mask, scaleS, centerPos);
4084
- return /* @__PURE__ */ React.createElement("g", { key: "sil-unified", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(
4085
- "path",
4086
- {
4087
- key: `sil-${i}`,
4088
- d: pathD(scaledPoly),
4089
- fill: CONFIG.color.silhouetteMask,
4090
- opacity: CONFIG.opacity.silhouetteMask,
4091
- stroke: "none"
4349
+ return /* @__PURE__ */ React.createElement("g", { key: "sil-unified", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
4350
+ const primInfo = primitiveDecomposition[i];
4351
+ let fillColor = CONFIG.color.silhouetteMask;
4352
+ if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {
4353
+ const kindToIndex = {
4354
+ "square": 0,
4355
+ "smalltriangle": 1,
4356
+ "parallelogram": 2,
4357
+ "medtriangle": 3,
4358
+ "largetriangle": 4
4359
+ };
4360
+ const primitiveIndex = kindToIndex[primInfo.kind];
4361
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
4362
+ const colorIndex = primitiveColorIndices[primitiveIndex];
4363
+ const color = CONFIG.color.primitiveColors[colorIndex];
4364
+ if (color) {
4365
+ fillColor = color;
4366
+ }
4367
+ }
4092
4368
  }
4093
- )));
4369
+ return /* @__PURE__ */ React.createElement(
4370
+ "path",
4371
+ {
4372
+ key: `sil-${i}`,
4373
+ d: pathD(scaledPoly),
4374
+ fill: fillColor,
4375
+ opacity: CONFIG.opacity.silhouetteMask,
4376
+ stroke: "none"
4377
+ }
4378
+ );
4379
+ }));
4094
4380
  }
4095
4381
  };
4096
4382
  return /* @__PURE__ */ React.createElement("div", { style: {
@@ -4099,7 +4385,7 @@ function NBackView({ params }) {
4099
4385
  alignItems: "center",
4100
4386
  justifyContent: "center",
4101
4387
  padding: "20px",
4102
- background: "#f5f5f5"
4388
+ background: "#fff7e0ff"
4103
4389
  } }, instructions && /* @__PURE__ */ React.createElement(
4104
4390
  "div",
4105
4391
  {
@@ -4189,6 +4475,18 @@ const info = {
4189
4475
  default: 3e3,
4190
4476
  description: "Duration in milliseconds to display tangram and accept responses"
4191
4477
  },
4478
+ /** Whether to use distinct colors for each primitive shape type */
4479
+ use_primitive_colors: {
4480
+ type: ParameterType.BOOL,
4481
+ default: false,
4482
+ description: "Whether each primitive shape type should have its own distinct color in the displayed tangram"
4483
+ },
4484
+ /** Indices mapping primitives to colors from the color palette */
4485
+ primitive_color_indices: {
4486
+ type: ParameterType.OBJECT,
4487
+ default: [0, 1, 2, 3, 4],
4488
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
4489
+ },
4192
4490
  /** Callback fired when trial ends */
4193
4491
  onTrialEnd: {
4194
4492
  type: ParameterType.FUNCTION,
@@ -4255,6 +4553,8 @@ class TangramNBackPlugin {
4255
4553
  instructions: trial.instructions,
4256
4554
  button_text: trial.button_text,
4257
4555
  duration: trial.duration,
4556
+ usePrimitiveColors: trial.use_primitive_colors,
4557
+ primitiveColorIndices: trial.primitive_color_indices,
4258
4558
  onTrialEnd: wrappedOnTrialEnd
4259
4559
  };
4260
4560
  const { root, display_element: element, jsPsych } = startNBackTrial(display_element, params, this.jsPsych);