jspsych-tangram 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/construct/index.browser.js +4805 -3935
  2. package/dist/construct/index.browser.js.map +1 -1
  3. package/dist/construct/index.browser.min.js +13 -13
  4. package/dist/construct/index.browser.min.js.map +1 -1
  5. package/dist/construct/index.cjs +289 -71
  6. package/dist/construct/index.cjs.map +1 -1
  7. package/dist/construct/index.d.ts +36 -0
  8. package/dist/construct/index.js +289 -71
  9. package/dist/construct/index.js.map +1 -1
  10. package/dist/index.cjs +399 -100
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +84 -0
  13. package/dist/index.js +399 -100
  14. package/dist/index.js.map +1 -1
  15. package/dist/nback/index.browser.js +4629 -3939
  16. package/dist/nback/index.browser.js.map +1 -1
  17. package/dist/nback/index.browser.min.js +12 -12
  18. package/dist/nback/index.browser.min.js.map +1 -1
  19. package/dist/nback/index.cjs +102 -64
  20. package/dist/nback/index.cjs.map +1 -1
  21. package/dist/nback/index.d.ts +24 -0
  22. package/dist/nback/index.js +102 -64
  23. package/dist/nback/index.js.map +1 -1
  24. package/dist/prep/index.browser.js +4803 -3941
  25. package/dist/prep/index.browser.js.map +1 -1
  26. package/dist/prep/index.browser.min.js +13 -13
  27. package/dist/prep/index.browser.min.js.map +1 -1
  28. package/dist/prep/index.cjs +285 -75
  29. package/dist/prep/index.cjs.map +1 -1
  30. package/dist/prep/index.d.ts +24 -0
  31. package/dist/prep/index.js +285 -75
  32. package/dist/prep/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/core/components/board/BoardView.tsx +372 -124
  35. package/src/core/components/board/GameBoard.tsx +128 -91
  36. package/src/core/components/pieces/BlueprintRing.tsx +105 -47
  37. package/src/core/config/config.ts +25 -10
  38. package/src/plugins/tangram-construct/ConstructionApp.tsx +7 -1
  39. package/src/plugins/tangram-construct/index.ts +22 -1
  40. package/src/plugins/tangram-nback/NBackApp.tsx +87 -28
  41. package/src/plugins/tangram-nback/index.ts +14 -0
  42. package/src/plugins/tangram-prep/PrepApp.tsx +7 -1
  43. package/src/plugins/tangram-prep/index.ts +14 -0
  44. package/tangram-construct.min.js +13 -13
  45. package/tangram-nback.min.js +12 -12
  46. package/tangram-prep.min.js +13 -13
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(() => {
@@ -3273,19 +3455,28 @@ function GameBoard(props) {
3273
3455
  const headerStyle = {
3274
3456
  display: "flex",
3275
3457
  flexDirection: "row",
3276
- justifyContent: "space-between",
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
  };
3464
+ const headerContentStyle = {
3465
+ position: "relative",
3466
+ width: `${svgDimensions.width}px`,
3467
+ maxWidth: "100%",
3468
+ display: "flex",
3469
+ justifyContent: "center",
3470
+ alignItems: "center"
3471
+ };
3282
3472
  const instructionsStyle = {
3283
- flexGrow: 1,
3284
3473
  fontSize: "20px",
3285
3474
  lineHeight: 1.5,
3286
- marginRight: "20px"
3475
+ textAlign: "center"
3287
3476
  };
3288
3477
  const timerStyle = {
3478
+ position: "absolute",
3479
+ right: 0,
3289
3480
  fontSize: "24px",
3290
3481
  fontWeight: "bold",
3291
3482
  fontFamily: "monospace",
@@ -3300,14 +3491,14 @@ function GameBoard(props) {
3300
3491
  justifyContent: "center",
3301
3492
  overflow: "hidden"
3302
3493
  };
3303
- return /* @__PURE__ */ React.createElement("div", { className: "tangram-trial-container", style: containerStyle }, (instructions || timeLimitMs > 0) && /* @__PURE__ */ React.createElement("div", { className: "tangram-header", style: headerStyle }, instructions && /* @__PURE__ */ React.createElement(
3494
+ return /* @__PURE__ */ React.createElement("div", { className: "tangram-trial-container", style: containerStyle }, (instructions || timeLimitMs > 0) && /* @__PURE__ */ React.createElement("div", { className: "tangram-header", style: headerStyle }, /* @__PURE__ */ React.createElement("div", { className: "tangram-header-content", style: headerContentStyle }, instructions && /* @__PURE__ */ React.createElement(
3304
3495
  "div",
3305
3496
  {
3306
3497
  className: "tangram-instructions",
3307
3498
  style: instructionsStyle,
3308
3499
  dangerouslySetInnerHTML: { __html: instructions }
3309
3500
  }
3310
- ), timeLimitMs > 0 && /* @__PURE__ */ React.createElement("div", { className: "tangram-timer", style: timerStyle }, formatTime(timeRemaining))), /* @__PURE__ */ React.createElement("div", { className: "tangram-gameboard-wrapper", style: gameboardWrapperStyle }, /* @__PURE__ */ React.createElement("div", { className: "tangram-gameboard", style: getGameboardStyle() }, /* @__PURE__ */ React.createElement(
3501
+ ), timeLimitMs > 0 && /* @__PURE__ */ React.createElement("div", { className: "tangram-timer", style: timerStyle }, formatTime(timeRemaining)))), /* @__PURE__ */ React.createElement("div", { className: "tangram-gameboard-wrapper", style: gameboardWrapperStyle }, /* @__PURE__ */ React.createElement("div", { className: "tangram-gameboard", style: getGameboardStyle() }, /* @__PURE__ */ React.createElement(
3311
3502
  BoardView,
3312
3503
  {
3313
3504
  controller,
@@ -3335,6 +3526,9 @@ function GameBoard(props) {
3335
3526
  onCenterBadgePointerDown,
3336
3527
  showTangramDecomposition: showTangramDecomposition ?? false,
3337
3528
  scaleS,
3529
+ usePrimitiveColorsBlueprints: usePrimitiveColorsBlueprints ?? false,
3530
+ usePrimitiveColorsTargets: usePrimitiveColorsTargets ?? false,
3531
+ primitiveColorIndices: primitiveColorIndices ?? [0, 1, 2, 3, 4],
3338
3532
  ...eventCallbacks
3339
3533
  }
3340
3534
  ))));
@@ -3554,7 +3748,10 @@ function startConstructionTrial(display_element, params, _jsPsych) {
3554
3748
  trialParams,
3555
3749
  ...params.instructions && { instructions: params.instructions },
3556
3750
  ...params.onInteraction && { onInteraction: params.onInteraction },
3557
- ...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 }
3558
3755
  };
3559
3756
  const root = createRoot(display_element);
3560
3757
  root.render(React.createElement(GameBoard, gameBoardProps));
@@ -3639,6 +3836,24 @@ const info$2 = {
3639
3836
  type: ParameterType.FUNCTION,
3640
3837
  default: void 0,
3641
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"
3642
3857
  }
3643
3858
  },
3644
3859
  data: {
@@ -3704,7 +3919,10 @@ class TangramConstructPlugin {
3704
3919
  show_tangram_decomposition: trial.show_tangram_decomposition,
3705
3920
  instructions: trial.instructions,
3706
3921
  onInteraction: trial.onInteraction,
3707
- 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
3708
3926
  };
3709
3927
  const { root, display_element: element, jsPsych } = startConstructionTrial(display_element, params, this.jsPsych);
3710
3928
  element.__reactContext = { root, jsPsych };
@@ -3722,7 +3940,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3722
3940
  quickstashMacros,
3723
3941
  primitiveOrder,
3724
3942
  onInteraction,
3725
- onTrialEnd
3943
+ onTrialEnd,
3944
+ usePrimitiveColorsBlueprints,
3945
+ primitiveColorIndices
3726
3946
  } = params;
3727
3947
  const { onInteraction: _, onTrialEnd: __, ...trialParams } = params;
3728
3948
  const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => primitiveOrder.indexOf(a.kind) - primitiveOrder.indexOf(b.kind));
@@ -3814,7 +4034,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3814
4034
  ...params.instructions && { instructions: params.instructions },
3815
4035
  onControllerReady: handleControllerReady,
3816
4036
  ...onInteraction && { onInteraction },
3817
- ...onTrialEnd && { onTrialEnd }
4037
+ ...onTrialEnd && { onTrialEnd },
4038
+ ...usePrimitiveColorsBlueprints !== void 0 && { usePrimitiveColorsBlueprints },
4039
+ ...primitiveColorIndices !== void 0 && { primitiveColorIndices }
3818
4040
  }));
3819
4041
  return { root, display_element, jsPsych };
3820
4042
  }
@@ -3880,6 +4102,18 @@ const info$1 = {
3880
4102
  onTrialEnd: {
3881
4103
  type: ParameterType.FUNCTION,
3882
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"
3883
4117
  }
3884
4118
  },
3885
4119
  data: {
@@ -3924,7 +4158,9 @@ class TangramPrepPlugin {
3924
4158
  primitiveOrder: trial.primitive_order,
3925
4159
  instructions: trial.instructions,
3926
4160
  onInteraction: trial.onInteraction,
3927
- onTrialEnd: wrappedOnTrialEnd
4161
+ onTrialEnd: wrappedOnTrialEnd,
4162
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
4163
+ primitiveColorIndices: trial.primitive_color_indices
3928
4164
  };
3929
4165
  const { root, display_element: element, jsPsych } = startPrepTrial(display_element, params, this.jsPsych);
3930
4166
  element.__reactContext = { root, jsPsych };
@@ -3944,6 +4180,8 @@ function NBackView({ params }) {
3944
4180
  instructions,
3945
4181
  button_text,
3946
4182
  duration,
4183
+ usePrimitiveColors,
4184
+ primitiveColorIndices,
3947
4185
  onTrialEnd
3948
4186
  } = params;
3949
4187
  const trialStartTime = useRef(Date.now());
@@ -3999,7 +4237,12 @@ function NBackView({ params }) {
3999
4237
  ...data,
4000
4238
  accuracy,
4001
4239
  tangram_id: tangram.tangramID,
4002
- 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
4003
4246
  };
4004
4247
  if (onTrialEnd) {
4005
4248
  onTrialEnd(trialData);
@@ -4053,35 +4296,77 @@ function NBackView({ params }) {
4053
4296
  if (show_tangram_decomposition) {
4054
4297
  const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
4055
4298
  const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, centerPos);
4056
- 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(
4057
- "path",
4058
- {
4059
- d: pathD(scaledPoly),
4060
- fill: CONFIG.color.silhouetteMask,
4061
- opacity: CONFIG.opacity.silhouetteMask,
4062
- stroke: "none"
4063
- }
4064
- ), /* @__PURE__ */ React.createElement(
4065
- "path",
4066
- {
4067
- d: pathD(scaledPoly),
4068
- fill: "none",
4069
- stroke: CONFIG.color.tangramDecomposition.stroke,
4070
- 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
+ }
4071
4318
  }
4072
- ))));
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
+ }));
4073
4337
  } else {
4074
4338
  const placedPolys = placeSilhouetteGridAlignedAsPolys(mask, scaleS, centerPos);
4075
- return /* @__PURE__ */ React.createElement("g", { key: "sil-unified", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(
4076
- "path",
4077
- {
4078
- key: `sil-${i}`,
4079
- d: pathD(scaledPoly),
4080
- fill: CONFIG.color.silhouetteMask,
4081
- opacity: CONFIG.opacity.silhouetteMask,
4082
- 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
+ }
4083
4358
  }
4084
- )));
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
+ }));
4085
4370
  }
4086
4371
  };
4087
4372
  return /* @__PURE__ */ React.createElement("div", { style: {
@@ -4180,6 +4465,18 @@ const info = {
4180
4465
  default: 3e3,
4181
4466
  description: "Duration in milliseconds to display tangram and accept responses"
4182
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
+ },
4183
4480
  /** Callback fired when trial ends */
4184
4481
  onTrialEnd: {
4185
4482
  type: ParameterType.FUNCTION,
@@ -4246,6 +4543,8 @@ class TangramNBackPlugin {
4246
4543
  instructions: trial.instructions,
4247
4544
  button_text: trial.button_text,
4248
4545
  duration: trial.duration,
4546
+ usePrimitiveColors: trial.use_primitive_colors,
4547
+ primitiveColorIndices: trial.primitive_color_indices,
4249
4548
  onTrialEnd: wrappedOnTrialEnd
4250
4549
  };
4251
4550
  const { root, display_element: element, jsPsych } = startNBackTrial(display_element, params, this.jsPsych);