jspsych-tangram 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/construct/index.browser.js +4809 -3948
  2. package/dist/construct/index.browser.js.map +1 -1
  3. package/dist/construct/index.browser.min.js +13 -13
  4. package/dist/construct/index.browser.min.js.map +1 -1
  5. package/dist/construct/index.cjs +275 -66
  6. package/dist/construct/index.cjs.map +1 -1
  7. package/dist/construct/index.d.ts +36 -0
  8. package/dist/construct/index.js +275 -66
  9. package/dist/construct/index.js.map +1 -1
  10. package/dist/index.cjs +385 -95
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +84 -0
  13. package/dist/index.js +385 -95
  14. package/dist/index.js.map +1 -1
  15. package/dist/nback/index.browser.js +4629 -3939
  16. package/dist/nback/index.browser.js.map +1 -1
  17. package/dist/nback/index.browser.min.js +12 -12
  18. package/dist/nback/index.browser.min.js.map +1 -1
  19. package/dist/nback/index.cjs +102 -64
  20. package/dist/nback/index.cjs.map +1 -1
  21. package/dist/nback/index.d.ts +24 -0
  22. package/dist/nback/index.js +102 -64
  23. package/dist/nback/index.js.map +1 -1
  24. package/dist/prep/index.browser.js +4805 -3952
  25. package/dist/prep/index.browser.js.map +1 -1
  26. package/dist/prep/index.browser.min.js +13 -13
  27. package/dist/prep/index.browser.min.js.map +1 -1
  28. package/dist/prep/index.cjs +271 -70
  29. package/dist/prep/index.cjs.map +1 -1
  30. package/dist/prep/index.d.ts +24 -0
  31. package/dist/prep/index.js +271 -70
  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 +26 -2
  36. package/src/core/components/pieces/BlueprintRing.tsx +105 -47
  37. package/src/core/config/config.ts +25 -10
  38. package/src/plugins/tangram-construct/ConstructionApp.tsx +7 -1
  39. package/src/plugins/tangram-construct/index.ts +22 -1
  40. package/src/plugins/tangram-nback/NBackApp.tsx +87 -28
  41. package/src/plugins/tangram-nback/index.ts +14 -0
  42. package/src/plugins/tangram-prep/PrepApp.tsx +7 -1
  43. package/src/plugins/tangram-prep/index.ts +14 -0
  44. package/tangram-construct.min.js +13 -13
  45. package/tangram-nback.min.js +12 -12
  46. package/tangram-prep.min.js +13 -13
package/dist/index.js CHANGED
@@ -5,30 +5,40 @@ import { v4 } from 'uuid';
5
5
 
6
6
  const CONFIG = {
7
7
  color: {
8
+ background: "#fff7e0ff",
8
9
  bands: {
9
- silhouette: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" },
10
- workspace: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" }
10
+ silhouette: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" },
11
+ workspace: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" }
11
12
  },
12
- completion: { fill: "#ccfff2", stroke: "#13da57" },
13
+ completion: { fill: "#ccffcc", stroke: "#13da57" },
13
14
  silhouetteMask: "#374151",
14
15
  anchors: { invalid: "#7dd3fc", valid: "#475569" },
15
- piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
16
- ui: { light: "#60a5fa", dark: "#1d4ed8" },
17
- blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
18
- tangramDecomposition: { stroke: "#fef2cc" }
16
+ // validFill used here for placed composites
17
+ piece: { draggingFill: "#8e7cc3", validFill: "#8e7cc3", invalidFill: "#d55c00", invalidStroke: "#dc2626", allGreenStroke: "#86efac"},
18
+ blueprint: { fill: "#374151", badgeFill: "#000000", labelFill: "#ffffff" },
19
+ tangramDecomposition: { stroke: "#fef2cc" },
20
+ primitiveColors: [
21
+ // from seaborn "colorblind" palette, 6 colors, with red omitted
22
+ "#0173b2",
23
+ "#de8f05",
24
+ "#029e73",
25
+ "#cc78bc",
26
+ "#ca9161"
27
+ ]
19
28
  },
20
29
  opacity: {
21
- blueprint: 0.4,
30
+ blueprint: 0.6,
22
31
  silhouetteMask: 0.25,
23
32
  //anchors: { valid: 0.80, invalid: 0.50 },
24
33
  anchors: { invalid: 0, valid: 0 },
25
- piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
34
+ piece: { invalid: 1, dragging: 1, locked: 1, normal: 1 }
26
35
  },
27
36
  size: {
28
- stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
37
+ stroke: { bandPx: 5, allGreenStrokePx: 10, tangramDecompositionPx: 1 },
29
38
  anchorRadiusPx: { valid: 1, invalid: 1 },
30
39
  badgeFontPx: 16,
31
- centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
40
+ centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 },
41
+ invalidMarker: { sizePx: 10, strokePx: 4 }
32
42
  },
33
43
  layout: {
34
44
  grid: { stepPx: 20, unitPx: 40 },
@@ -45,9 +55,7 @@ const CONFIG = {
45
55
  },
46
56
  game: {
47
57
  snapRadiusPx: 15,
48
- showBorders: false,
49
- hideTouchingBorders: true
50
- }
58
+ showBorders: false}
51
59
  };
52
60
 
53
61
  function isComposite(bp) {
@@ -830,6 +838,31 @@ var unlockedIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAYAAA
830
838
  function pathD(poly) {
831
839
  return `M ${poly.map((pt) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
832
840
  }
841
+ function getPieceColor(blueprint, usePrimitiveColors, defaultColor, primitiveColorIndices) {
842
+ if (!usePrimitiveColors) {
843
+ return defaultColor;
844
+ }
845
+ if ("kind" in blueprint) {
846
+ const kind = blueprint.kind;
847
+ const kindToIndex = {
848
+ "square": 0,
849
+ "smalltriangle": 1,
850
+ "parallelogram": 2,
851
+ "medtriangle": 3,
852
+ "largetriangle": 4
853
+ };
854
+ const primitiveIndex = kindToIndex[kind];
855
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
856
+ const colorIndex = primitiveColorIndices[primitiveIndex];
857
+ const color = CONFIG.color.primitiveColors[colorIndex];
858
+ if (color) {
859
+ return color;
860
+ }
861
+ }
862
+ return defaultColor;
863
+ }
864
+ return defaultColor;
865
+ }
833
866
  function BoardView(props) {
834
867
  const {
835
868
  controller,
@@ -849,6 +882,9 @@ function BoardView(props) {
849
882
  dragInvalid,
850
883
  lockedPieceId,
851
884
  showTangramDecomposition,
885
+ usePrimitiveColorsBlueprints,
886
+ usePrimitiveColorsTargets,
887
+ primitiveColorIndices,
852
888
  svgRef,
853
889
  setPieceRef,
854
890
  onPiecePointerDown,
@@ -859,6 +895,144 @@ function BoardView(props) {
859
895
  onCenterBadgePointerDown
860
896
  } = props;
861
897
  const VW = viewBox.w, VH = viewBox.h;
898
+ const renderSilhouettes = () => layout.sectors.map((s) => {
899
+ const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
900
+ if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
901
+ const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
902
+ const rect = rectForBand(layout, s, "silhouette", 1);
903
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
904
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
905
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
906
+ const primInfo = primitiveDecomposition[i];
907
+ let fillColor = CONFIG.color.silhouetteMask;
908
+ if (usePrimitiveColorsTargets && primInfo?.kind && primitiveColorIndices) {
909
+ const kindToIndex = {
910
+ "square": 0,
911
+ "smalltriangle": 1,
912
+ "parallelogram": 2,
913
+ "medtriangle": 3,
914
+ "largetriangle": 4
915
+ };
916
+ const primitiveIndex = kindToIndex[primInfo.kind];
917
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
918
+ const colorIndex = primitiveColorIndices[primitiveIndex];
919
+ const color = CONFIG.color.primitiveColors[colorIndex];
920
+ if (color) {
921
+ fillColor = color;
922
+ }
923
+ }
924
+ }
925
+ return /* @__PURE__ */ React.createElement(
926
+ "path",
927
+ {
928
+ key: `prim-fill-${i}`,
929
+ d: pathD(scaledPoly),
930
+ fill: fillColor,
931
+ opacity: CONFIG.opacity.silhouetteMask,
932
+ stroke: "none"
933
+ }
934
+ );
935
+ }));
936
+ } else {
937
+ const placedPolys = placedSilBySector.get(s.id) ?? [];
938
+ if (!placedPolys.length) return null;
939
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-${s.id}`, pointerEvents: "none" }, placedPolys.map((poly, i) => /* @__PURE__ */ React.createElement("path", { key: i, d: pathD(poly), fill: CONFIG.color.silhouetteMask, opacity: CONFIG.opacity.silhouetteMask })));
940
+ }
941
+ });
942
+ const renderPieces = (piecesFilter) => {
943
+ const piecesToRender = pieces;
944
+ return piecesToRender.sort((a, b) => {
945
+ if (draggingId === a.id) return 1;
946
+ if (draggingId === b.id) return -1;
947
+ return 0;
948
+ }).map((p) => {
949
+ const bp = controller.getBlueprint(p.blueprintId);
950
+ const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
951
+ const isDragging = draggingId === p.id;
952
+ const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
953
+ const isConnectivityLocked = lockedPieceId === p.id;
954
+ selectedPieceId === p.id;
955
+ const isCarriedInvalid = isDragging && dragInvalid;
956
+ const translateX = p.x - bb.min.x;
957
+ const translateY = p.y - bb.min.y;
958
+ const validFillColor = getPieceColor(
959
+ bp,
960
+ usePrimitiveColorsBlueprints || false,
961
+ CONFIG.color.piece.validFill,
962
+ primitiveColorIndices || [0, 1, 2, 3, 4]
963
+ );
964
+ const draggingFillColor = getPieceColor(
965
+ bp,
966
+ usePrimitiveColorsBlueprints || false,
967
+ CONFIG.color.piece.draggingFill,
968
+ primitiveColorIndices || [0, 1, 2, 3, 4]
969
+ );
970
+ return /* @__PURE__ */ React.createElement("g", { key: p.id, ref: setPieceRef(p.id), transform: `translate(${translateX}, ${translateY})`, style: { cursor: locked ? "default" : clickMode ? "pointer" : "grab" }, pointerEvents: clickMode && isDragging ? "none" : "auto" }, ("shape" in bp ? bp.shape : []).map((poly, idx) => {
971
+ const showBorders = shouldShowBorders();
972
+ shouldUseSelectiveBorders(p.blueprintId);
973
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
974
+ "path",
975
+ {
976
+ d: pathD(poly),
977
+ fill: isConnectivityLocked ? CONFIG.color.piece.invalidFill : isCarriedInvalid ? CONFIG.color.piece.invalidFill : isDragging ? draggingFillColor : validFillColor,
978
+ opacity: isConnectivityLocked ? CONFIG.opacity.piece.invalid : isCarriedInvalid ? CONFIG.opacity.piece.invalid : isDragging ? CONFIG.opacity.piece.dragging : locked ? CONFIG.opacity.piece.locked : CONFIG.opacity.piece.normal,
979
+ stroke: "none",
980
+ onPointerDown: (e) => onPiecePointerDown(e, p)
981
+ }
982
+ ), showBorders);
983
+ }), isDragging && isCarriedInvalid && "shape" in bp && bp.shape.length > 0 && (() => {
984
+ const { cx, cy } = polysAABB$1(bp.shape);
985
+ const size = CONFIG.size.invalidMarker.sizePx;
986
+ const strokeWidth = CONFIG.size.invalidMarker.strokePx;
987
+ const borderWidth = strokeWidth + 2;
988
+ return /* @__PURE__ */ React.createElement("g", { key: "invalid-marker" }, /* @__PURE__ */ React.createElement(
989
+ "line",
990
+ {
991
+ x1: cx - size,
992
+ y1: cy - size,
993
+ x2: cx + size,
994
+ y2: cy + size,
995
+ stroke: "white",
996
+ strokeWidth: borderWidth,
997
+ strokeLinecap: "round"
998
+ }
999
+ ), /* @__PURE__ */ React.createElement(
1000
+ "line",
1001
+ {
1002
+ x1: cx - size,
1003
+ y1: cy - size,
1004
+ x2: cx + size,
1005
+ y2: cy + size,
1006
+ stroke: CONFIG.color.piece.invalidStroke,
1007
+ strokeWidth,
1008
+ strokeLinecap: "round"
1009
+ }
1010
+ ), /* @__PURE__ */ React.createElement(
1011
+ "line",
1012
+ {
1013
+ x1: cx + size,
1014
+ y1: cy - size,
1015
+ x2: cx - size,
1016
+ y2: cy + size,
1017
+ stroke: "white",
1018
+ strokeWidth: borderWidth,
1019
+ strokeLinecap: "round"
1020
+ }
1021
+ ), /* @__PURE__ */ React.createElement(
1022
+ "line",
1023
+ {
1024
+ x1: cx + size,
1025
+ y1: cy - size,
1026
+ x2: cx - size,
1027
+ y2: cy + size,
1028
+ stroke: CONFIG.color.piece.invalidStroke,
1029
+ strokeWidth,
1030
+ strokeLinecap: "round"
1031
+ }
1032
+ ));
1033
+ })());
1034
+ });
1035
+ };
862
1036
  const centerView = controller.state.blueprintView;
863
1037
  const bps = centerView === "primitives" ? controller.state.primitives : controller.state.quickstash;
864
1038
  const QS_SLOTS = controller.state.cfg.maxQuickstashSlots;
@@ -879,6 +1053,12 @@ function BoardView(props) {
879
1053
  const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
880
1054
  const cx = bb.min.x + bb.width / 2;
881
1055
  const cy = bb.min.y + bb.height / 2;
1056
+ const fillColor = getPieceColor(
1057
+ bp,
1058
+ usePrimitiveColorsBlueprints || false,
1059
+ CONFIG.color.blueprint.fill,
1060
+ primitiveColorIndices || [0, 1, 2, 3, 4]
1061
+ );
882
1062
  return /* @__PURE__ */ React.createElement(
883
1063
  "g",
884
1064
  {
@@ -890,7 +1070,7 @@ function BoardView(props) {
890
1070
  {
891
1071
  key: idx,
892
1072
  d: pathD(poly),
893
- fill: CONFIG.color.blueprint.fill,
1073
+ fill: fillColor,
894
1074
  opacity: CONFIG.opacity.blueprint,
895
1075
  stroke: "none",
896
1076
  strokeWidth: 0,
@@ -914,7 +1094,7 @@ function BoardView(props) {
914
1094
  onPointerDown: (e) => {
915
1095
  onRootPointerDown(e);
916
1096
  },
917
- style: { background: "#f5f5f5", touchAction: "none", userSelect: "none" }
1097
+ style: { background: CONFIG.color.background, touchAction: "none", userSelect: "none" }
918
1098
  },
919
1099
  layout.sectors.map((s, i) => {
920
1100
  const done = !!controller.state.sectors[s.id].completedAt;
@@ -924,35 +1104,7 @@ function BoardView(props) {
924
1104
  const work = layout.bands.workspace;
925
1105
  return /* @__PURE__ */ React.createElement("g", { key: `bands-${s.id}` }, controller.state.cfg.target === "workspace" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("path", { d: wedgePath(layout.cx, layout.cy, sil[0], sil[1], s.start, s.end), fill: baseSil, stroke: CONFIG.color.bands.silhouette.stroke, strokeWidth: CONFIG.size.stroke.bandPx, pointerEvents: "none" }), /* @__PURE__ */ React.createElement("path", { d: wedgePath(layout.cx, layout.cy, work[0], work[1], s.start, s.end), fill: done ? CONFIG.color.completion.fill : baseWork, stroke: done ? CONFIG.color.completion.stroke : CONFIG.color.bands.workspace.stroke, strokeWidth: CONFIG.size.stroke.bandPx, pointerEvents: "none" })) : /* @__PURE__ */ React.createElement("path", { d: wedgePath(layout.cx, layout.cy, sil[0], sil[1], s.start, s.end), fill: done ? CONFIG.color.completion.fill : baseSil, stroke: done ? CONFIG.color.completion.stroke : CONFIG.color.bands.silhouette.stroke, strokeWidth: CONFIG.size.stroke.bandPx, pointerEvents: "none" }));
926
1106
  }),
927
- pieces.sort((a, b) => {
928
- if (draggingId === a.id) return 1;
929
- if (draggingId === b.id) return -1;
930
- return 0;
931
- }).map((p) => {
932
- const bp = controller.getBlueprint(p.blueprintId);
933
- const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
934
- const isDragging = draggingId === p.id;
935
- const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
936
- const isConnectivityLocked = lockedPieceId === p.id;
937
- selectedPieceId === p.id;
938
- const isCarriedInvalid = isDragging && dragInvalid;
939
- const translateX = p.x - bb.min.x;
940
- const translateY = p.y - bb.min.y;
941
- return /* @__PURE__ */ React.createElement("g", { key: p.id, ref: setPieceRef(p.id), transform: `translate(${translateX}, ${translateY})`, style: { cursor: locked ? "default" : clickMode ? "pointer" : "grab" }, pointerEvents: clickMode && isDragging ? "none" : "auto" }, ("shape" in bp ? bp.shape : []).map((poly, idx) => {
942
- const showBorders = shouldShowBorders();
943
- shouldUseSelectiveBorders(p.blueprintId);
944
- return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
945
- "path",
946
- {
947
- d: pathD(poly),
948
- fill: isConnectivityLocked ? CONFIG.color.piece.invalidFill : isCarriedInvalid ? CONFIG.color.piece.invalidFill : isDragging ? CONFIG.color.piece.draggingFill : CONFIG.color.piece.validFill,
949
- opacity: isConnectivityLocked ? CONFIG.opacity.piece.invalid : isCarriedInvalid ? CONFIG.opacity.piece.invalid : isDragging ? CONFIG.opacity.piece.dragging : locked ? CONFIG.opacity.piece.locked : CONFIG.opacity.piece.normal,
950
- stroke: "none",
951
- onPointerDown: (e) => onPiecePointerDown(e, p)
952
- }
953
- ), showBorders);
954
- }));
955
- }),
1107
+ /* @__PURE__ */ React.createElement(React.Fragment, null, renderSilhouettes(), renderPieces()) ,
956
1108
  layout.sectors.map((s) => {
957
1109
  const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
958
1110
  if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
@@ -960,23 +1112,32 @@ function BoardView(props) {
960
1112
  const rect = rectForBand(layout, s, "silhouette", 1);
961
1113
  const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
962
1114
  const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
963
- return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(React.Fragment, { key: `prim-${i}` }, /* @__PURE__ */ React.createElement(
964
- "path",
965
- {
966
- d: pathD(scaledPoly),
967
- fill: CONFIG.color.silhouetteMask,
968
- opacity: CONFIG.opacity.silhouetteMask,
969
- stroke: "none"
970
- }
971
- ), /* @__PURE__ */ React.createElement(
972
- "path",
973
- {
974
- d: pathD(scaledPoly),
975
- fill: "none",
976
- stroke: CONFIG.color.tangramDecomposition.stroke,
977
- strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
1115
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
1116
+ const primInfo = primitiveDecomposition[i];
1117
+ if (usePrimitiveColorsTargets && primInfo?.kind && primitiveColorIndices) {
1118
+ const kindToIndex = {
1119
+ "square": 0,
1120
+ "smalltriangle": 1,
1121
+ "parallelogram": 2,
1122
+ "medtriangle": 3,
1123
+ "largetriangle": 4
1124
+ };
1125
+ const primitiveIndex = kindToIndex[primInfo.kind];
1126
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
1127
+ primitiveColorIndices[primitiveIndex];
1128
+ }
978
1129
  }
979
- ))));
1130
+ return /* @__PURE__ */ React.createElement(
1131
+ "path",
1132
+ {
1133
+ key: `prim-fill-${i}`,
1134
+ d: pathD(scaledPoly),
1135
+ fill: "none",
1136
+ opacity: 0,
1137
+ stroke: "none"
1138
+ }
1139
+ );
1140
+ }));
980
1141
  } else {
981
1142
  const placedPolys = placedSilBySector.get(s.id) ?? [];
982
1143
  if (!placedPolys.length) return null;
@@ -1067,7 +1228,25 @@ function BoardView(props) {
1067
1228
  const by = layout.cy + blueprintRingR * Math.sin(theta);
1068
1229
  return renderBlueprintGlyph(bp, bx, by);
1069
1230
  }),
1070
- controller.state.endedAt && /* @__PURE__ */ React.createElement("g", { pointerEvents: "none" }, /* @__PURE__ */ React.createElement("circle", { cx: layout.cx, cy: layout.cy, r: layout.outerR - 3, fill: "none", stroke: CONFIG.color.piece.allGreenStroke, strokeWidth: CONFIG.size.stroke.allGreenStrokePx, opacity: 0 }, /* @__PURE__ */ React.createElement("animate", { attributeName: "opacity", from: "0", to: "1", dur: "160ms", fill: "freeze" }), /* @__PURE__ */ React.createElement("animate", { attributeName: "opacity", from: "1", to: "0", begin: "560ms", dur: "520ms", fill: "freeze" })))
1231
+ controller.state.endedAt && /* @__PURE__ */ React.createElement("g", { pointerEvents: "none" }, /* @__PURE__ */ React.createElement("circle", { cx: layout.cx, cy: layout.cy, r: layout.outerR - 3, fill: "none", stroke: CONFIG.color.piece.allGreenStroke, strokeWidth: CONFIG.size.stroke.allGreenStrokePx, opacity: 0 }, /* @__PURE__ */ React.createElement("animate", { attributeName: "opacity", from: "0", to: "1", dur: "160ms", fill: "freeze" }), /* @__PURE__ */ React.createElement("animate", { attributeName: "opacity", from: "1", to: "0", begin: "560ms", dur: "520ms", fill: "freeze" }))),
1232
+ showTangramDecomposition && layout.sectors.map((s) => {
1233
+ const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
1234
+ if (!sectorCfg?.silhouette.primitiveDecomposition) return null;
1235
+ const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
1236
+ const rect = rectForBand(layout, s, "silhouette", 1);
1237
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
1238
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
1239
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-borders-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(
1240
+ "path",
1241
+ {
1242
+ key: `prim-border-${i}`,
1243
+ d: pathD(scaledPoly),
1244
+ fill: "none",
1245
+ stroke: CONFIG.color.tangramDecomposition.stroke,
1246
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
1247
+ }
1248
+ )));
1249
+ })
1071
1250
  );
1072
1251
  }
1073
1252
 
@@ -2952,7 +3131,10 @@ function GameBoard(props) {
2952
3131
  onPieceRemove,
2953
3132
  onInteraction,
2954
3133
  onTrialEnd,
2955
- onControllerReady
3134
+ onControllerReady,
3135
+ usePrimitiveColorsBlueprints,
3136
+ usePrimitiveColorsTargets,
3137
+ primitiveColorIndices
2956
3138
  } = props;
2957
3139
  const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1e3) : 0);
2958
3140
  const controller = React.useMemo(() => {
@@ -3276,7 +3458,7 @@ function GameBoard(props) {
3276
3458
  justifyContent: "center",
3277
3459
  alignItems: "center",
3278
3460
  padding: "20px",
3279
- background: "#f5f5f5",
3461
+ background: CONFIG.color.background,
3280
3462
  flex: "0 0 auto"
3281
3463
  };
3282
3464
  const headerContentStyle = {
@@ -3344,6 +3526,9 @@ function GameBoard(props) {
3344
3526
  onCenterBadgePointerDown,
3345
3527
  showTangramDecomposition: showTangramDecomposition ?? false,
3346
3528
  scaleS,
3529
+ usePrimitiveColorsBlueprints: usePrimitiveColorsBlueprints ?? false,
3530
+ usePrimitiveColorsTargets: usePrimitiveColorsTargets ?? false,
3531
+ primitiveColorIndices: primitiveColorIndices ?? [0, 1, 2, 3, 4],
3347
3532
  ...eventCallbacks
3348
3533
  }
3349
3534
  ))));
@@ -3563,7 +3748,10 @@ function startConstructionTrial(display_element, params, _jsPsych) {
3563
3748
  trialParams,
3564
3749
  ...params.instructions && { instructions: params.instructions },
3565
3750
  ...params.onInteraction && { onInteraction: params.onInteraction },
3566
- ...params.onTrialEnd && { onTrialEnd: params.onTrialEnd }
3751
+ ...params.onTrialEnd && { onTrialEnd: params.onTrialEnd },
3752
+ ...params.usePrimitiveColorsBlueprints !== void 0 && { usePrimitiveColorsBlueprints: params.usePrimitiveColorsBlueprints },
3753
+ ...params.usePrimitiveColorsTargets !== void 0 && { usePrimitiveColorsTargets: params.usePrimitiveColorsTargets },
3754
+ ...params.primitiveColorIndices !== void 0 && { primitiveColorIndices: params.primitiveColorIndices }
3567
3755
  };
3568
3756
  const root = createRoot(display_element);
3569
3757
  root.render(React.createElement(GameBoard, gameBoardProps));
@@ -3648,6 +3836,24 @@ const info$2 = {
3648
3836
  type: ParameterType.FUNCTION,
3649
3837
  default: void 0,
3650
3838
  description: "Callback when trial completes with full data"
3839
+ },
3840
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
3841
+ use_primitive_colors_blueprints: {
3842
+ type: ParameterType.BOOL,
3843
+ default: false,
3844
+ description: "Whether each primitive shape type should have its own distinct color in the blueprint dock area"
3845
+ },
3846
+ /** Whether to use distinct colors for each primitive shape type in target tangrams */
3847
+ use_primitive_colors_targets: {
3848
+ type: ParameterType.BOOL,
3849
+ default: false,
3850
+ description: "Whether each primitive shape type should have its own distinct color in target tangram silhouettes"
3851
+ },
3852
+ /** Indices mapping primitives to colors from the color palette */
3853
+ primitive_color_indices: {
3854
+ type: ParameterType.OBJECT,
3855
+ default: [0, 1, 2, 3, 4],
3856
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
3651
3857
  }
3652
3858
  },
3653
3859
  data: {
@@ -3713,7 +3919,10 @@ class TangramConstructPlugin {
3713
3919
  show_tangram_decomposition: trial.show_tangram_decomposition,
3714
3920
  instructions: trial.instructions,
3715
3921
  onInteraction: trial.onInteraction,
3716
- onTrialEnd: wrappedOnTrialEnd
3922
+ onTrialEnd: wrappedOnTrialEnd,
3923
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
3924
+ usePrimitiveColorsTargets: trial.use_primitive_colors_targets,
3925
+ primitiveColorIndices: trial.primitive_color_indices
3717
3926
  };
3718
3927
  const { root, display_element: element, jsPsych } = startConstructionTrial(display_element, params, this.jsPsych);
3719
3928
  element.__reactContext = { root, jsPsych };
@@ -3731,7 +3940,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3731
3940
  quickstashMacros,
3732
3941
  primitiveOrder,
3733
3942
  onInteraction,
3734
- onTrialEnd
3943
+ onTrialEnd,
3944
+ usePrimitiveColorsBlueprints,
3945
+ primitiveColorIndices
3735
3946
  } = params;
3736
3947
  const { onInteraction: _, onTrialEnd: __, ...trialParams } = params;
3737
3948
  const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => primitiveOrder.indexOf(a.kind) - primitiveOrder.indexOf(b.kind));
@@ -3823,7 +4034,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3823
4034
  ...params.instructions && { instructions: params.instructions },
3824
4035
  onControllerReady: handleControllerReady,
3825
4036
  ...onInteraction && { onInteraction },
3826
- ...onTrialEnd && { onTrialEnd }
4037
+ ...onTrialEnd && { onTrialEnd },
4038
+ ...usePrimitiveColorsBlueprints !== void 0 && { usePrimitiveColorsBlueprints },
4039
+ ...primitiveColorIndices !== void 0 && { primitiveColorIndices }
3827
4040
  }));
3828
4041
  return { root, display_element, jsPsych };
3829
4042
  }
@@ -3889,6 +4102,18 @@ const info$1 = {
3889
4102
  onTrialEnd: {
3890
4103
  type: ParameterType.FUNCTION,
3891
4104
  default: void 0
4105
+ },
4106
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
4107
+ use_primitive_colors_blueprints: {
4108
+ type: ParameterType.BOOL,
4109
+ default: false,
4110
+ description: "Whether each primitive shape type should have its own distinct color in the blueprint dock area"
4111
+ },
4112
+ /** Indices mapping primitives to colors from the color palette */
4113
+ primitive_color_indices: {
4114
+ type: ParameterType.OBJECT,
4115
+ default: [0, 1, 2, 3, 4],
4116
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
3892
4117
  }
3893
4118
  },
3894
4119
  data: {
@@ -3933,7 +4158,9 @@ class TangramPrepPlugin {
3933
4158
  primitiveOrder: trial.primitive_order,
3934
4159
  instructions: trial.instructions,
3935
4160
  onInteraction: trial.onInteraction,
3936
- onTrialEnd: wrappedOnTrialEnd
4161
+ onTrialEnd: wrappedOnTrialEnd,
4162
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
4163
+ primitiveColorIndices: trial.primitive_color_indices
3937
4164
  };
3938
4165
  const { root, display_element: element, jsPsych } = startPrepTrial(display_element, params, this.jsPsych);
3939
4166
  element.__reactContext = { root, jsPsych };
@@ -3953,6 +4180,8 @@ function NBackView({ params }) {
3953
4180
  instructions,
3954
4181
  button_text,
3955
4182
  duration,
4183
+ usePrimitiveColors,
4184
+ primitiveColorIndices,
3956
4185
  onTrialEnd
3957
4186
  } = params;
3958
4187
  const trialStartTime = useRef(Date.now());
@@ -4008,7 +4237,12 @@ function NBackView({ params }) {
4008
4237
  ...data,
4009
4238
  accuracy,
4010
4239
  tangram_id: tangram.tangramID,
4011
- is_match: isMatch
4240
+ is_match: isMatch,
4241
+ show_tangram_decomposition,
4242
+ use_primitive_colors: usePrimitiveColors,
4243
+ primitive_color_indices: primitiveColorIndices,
4244
+ duration,
4245
+ button_text
4012
4246
  };
4013
4247
  if (onTrialEnd) {
4014
4248
  onTrialEnd(trialData);
@@ -4062,35 +4296,77 @@ function NBackView({ params }) {
4062
4296
  if (show_tangram_decomposition) {
4063
4297
  const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
4064
4298
  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
4299
+ return /* @__PURE__ */ React.createElement("g", { key: "sil-decomposed", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
4300
+ const primInfo = primitiveDecomposition[i];
4301
+ let fillColor = CONFIG.color.silhouetteMask;
4302
+ if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {
4303
+ const kindToIndex = {
4304
+ "square": 0,
4305
+ "smalltriangle": 1,
4306
+ "parallelogram": 2,
4307
+ "medtriangle": 3,
4308
+ "largetriangle": 4
4309
+ };
4310
+ const primitiveIndex = kindToIndex[primInfo.kind];
4311
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
4312
+ const colorIndex = primitiveColorIndices[primitiveIndex];
4313
+ const color = CONFIG.color.primitiveColors[colorIndex];
4314
+ if (color) {
4315
+ fillColor = color;
4316
+ }
4317
+ }
4080
4318
  }
4081
- ))));
4319
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: `prim-${i}` }, /* @__PURE__ */ React.createElement(
4320
+ "path",
4321
+ {
4322
+ d: pathD(scaledPoly),
4323
+ fill: fillColor,
4324
+ opacity: CONFIG.opacity.silhouetteMask,
4325
+ stroke: "none"
4326
+ }
4327
+ ), /* @__PURE__ */ React.createElement(
4328
+ "path",
4329
+ {
4330
+ d: pathD(scaledPoly),
4331
+ fill: "none",
4332
+ stroke: CONFIG.color.tangramDecomposition.stroke,
4333
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
4334
+ }
4335
+ ));
4336
+ }));
4082
4337
  } else {
4083
4338
  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"
4339
+ return /* @__PURE__ */ React.createElement("g", { key: "sil-unified", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
4340
+ const primInfo = primitiveDecomposition[i];
4341
+ let fillColor = CONFIG.color.silhouetteMask;
4342
+ if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {
4343
+ const kindToIndex = {
4344
+ "square": 0,
4345
+ "smalltriangle": 1,
4346
+ "parallelogram": 2,
4347
+ "medtriangle": 3,
4348
+ "largetriangle": 4
4349
+ };
4350
+ const primitiveIndex = kindToIndex[primInfo.kind];
4351
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
4352
+ const colorIndex = primitiveColorIndices[primitiveIndex];
4353
+ const color = CONFIG.color.primitiveColors[colorIndex];
4354
+ if (color) {
4355
+ fillColor = color;
4356
+ }
4357
+ }
4092
4358
  }
4093
- )));
4359
+ return /* @__PURE__ */ React.createElement(
4360
+ "path",
4361
+ {
4362
+ key: `sil-${i}`,
4363
+ d: pathD(scaledPoly),
4364
+ fill: fillColor,
4365
+ opacity: CONFIG.opacity.silhouetteMask,
4366
+ stroke: "none"
4367
+ }
4368
+ );
4369
+ }));
4094
4370
  }
4095
4371
  };
4096
4372
  return /* @__PURE__ */ React.createElement("div", { style: {
@@ -4189,6 +4465,18 @@ const info = {
4189
4465
  default: 3e3,
4190
4466
  description: "Duration in milliseconds to display tangram and accept responses"
4191
4467
  },
4468
+ /** Whether to use distinct colors for each primitive shape type */
4469
+ use_primitive_colors: {
4470
+ type: ParameterType.BOOL,
4471
+ default: false,
4472
+ description: "Whether each primitive shape type should have its own distinct color in the displayed tangram"
4473
+ },
4474
+ /** Indices mapping primitives to colors from the color palette */
4475
+ primitive_color_indices: {
4476
+ type: ParameterType.OBJECT,
4477
+ default: [0, 1, 2, 3, 4],
4478
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
4479
+ },
4192
4480
  /** Callback fired when trial ends */
4193
4481
  onTrialEnd: {
4194
4482
  type: ParameterType.FUNCTION,
@@ -4255,6 +4543,8 @@ class TangramNBackPlugin {
4255
4543
  instructions: trial.instructions,
4256
4544
  button_text: trial.button_text,
4257
4545
  duration: trial.duration,
4546
+ usePrimitiveColors: trial.use_primitive_colors,
4547
+ primitiveColorIndices: trial.primitive_color_indices,
4258
4548
  onTrialEnd: wrappedOnTrialEnd
4259
4549
  };
4260
4550
  const { root, display_element: element, jsPsych } = startNBackTrial(display_element, params, this.jsPsych);