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.cjs CHANGED
@@ -7,30 +7,40 @@ var uuid = require('uuid');
7
7
 
8
8
  const CONFIG = {
9
9
  color: {
10
+ background: "#fff7e0ff",
10
11
  bands: {
11
- silhouette: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" },
12
- workspace: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" }
12
+ silhouette: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" },
13
+ workspace: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" }
13
14
  },
14
- completion: { fill: "#ccfff2", stroke: "#13da57" },
15
+ completion: { fill: "#ccffcc", stroke: "#13da57" },
15
16
  silhouetteMask: "#374151",
16
17
  anchors: { invalid: "#7dd3fc", valid: "#475569" },
17
- piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
18
- ui: { light: "#60a5fa", dark: "#1d4ed8" },
19
- blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
20
- tangramDecomposition: { stroke: "#fef2cc" }
18
+ // validFill used here for placed composites
19
+ piece: { draggingFill: "#8e7cc3", validFill: "#8e7cc3", invalidFill: "#d55c00", invalidStroke: "#dc2626", allGreenStroke: "#86efac"},
20
+ blueprint: { fill: "#374151", badgeFill: "#000000", labelFill: "#ffffff" },
21
+ tangramDecomposition: { stroke: "#fef2cc" },
22
+ primitiveColors: [
23
+ // from seaborn "colorblind" palette, 6 colors, with red omitted
24
+ "#0173b2",
25
+ "#de8f05",
26
+ "#029e73",
27
+ "#cc78bc",
28
+ "#ca9161"
29
+ ]
21
30
  },
22
31
  opacity: {
23
- blueprint: 0.4,
32
+ blueprint: 0.6,
24
33
  silhouetteMask: 0.25,
25
34
  //anchors: { valid: 0.80, invalid: 0.50 },
26
35
  anchors: { invalid: 0, valid: 0 },
27
- piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
36
+ piece: { invalid: 1, dragging: 1, locked: 1, normal: 1 }
28
37
  },
29
38
  size: {
30
- stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
39
+ stroke: { bandPx: 5, allGreenStrokePx: 10, tangramDecompositionPx: 1 },
31
40
  anchorRadiusPx: { valid: 1, invalid: 1 },
32
41
  badgeFontPx: 16,
33
- centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
42
+ centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 },
43
+ invalidMarker: { sizePx: 10, strokePx: 4 }
34
44
  },
35
45
  layout: {
36
46
  grid: { stepPx: 20, unitPx: 40 },
@@ -47,9 +57,7 @@ const CONFIG = {
47
57
  },
48
58
  game: {
49
59
  snapRadiusPx: 15,
50
- showBorders: false,
51
- hideTouchingBorders: true
52
- }
60
+ showBorders: false}
53
61
  };
54
62
 
55
63
  function isComposite(bp) {
@@ -832,6 +840,31 @@ var unlockedIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAYAAA
832
840
  function pathD(poly) {
833
841
  return `M ${poly.map((pt) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
834
842
  }
843
+ function getPieceColor(blueprint, usePrimitiveColors, defaultColor, primitiveColorIndices) {
844
+ if (!usePrimitiveColors) {
845
+ return defaultColor;
846
+ }
847
+ if ("kind" in blueprint) {
848
+ const kind = blueprint.kind;
849
+ const kindToIndex = {
850
+ "square": 0,
851
+ "smalltriangle": 1,
852
+ "parallelogram": 2,
853
+ "medtriangle": 3,
854
+ "largetriangle": 4
855
+ };
856
+ const primitiveIndex = kindToIndex[kind];
857
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
858
+ const colorIndex = primitiveColorIndices[primitiveIndex];
859
+ const color = CONFIG.color.primitiveColors[colorIndex];
860
+ if (color) {
861
+ return color;
862
+ }
863
+ }
864
+ return defaultColor;
865
+ }
866
+ return defaultColor;
867
+ }
835
868
  function BoardView(props) {
836
869
  const {
837
870
  controller,
@@ -851,6 +884,9 @@ function BoardView(props) {
851
884
  dragInvalid,
852
885
  lockedPieceId,
853
886
  showTangramDecomposition,
887
+ usePrimitiveColorsBlueprints,
888
+ usePrimitiveColorsTargets,
889
+ primitiveColorIndices,
854
890
  svgRef,
855
891
  setPieceRef,
856
892
  onPiecePointerDown,
@@ -861,6 +897,144 @@ function BoardView(props) {
861
897
  onCenterBadgePointerDown
862
898
  } = props;
863
899
  const VW = viewBox.w, VH = viewBox.h;
900
+ const renderSilhouettes = () => layout.sectors.map((s) => {
901
+ const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
902
+ if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
903
+ const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
904
+ const rect = rectForBand(layout, s, "silhouette", 1);
905
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
906
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
907
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
908
+ const primInfo = primitiveDecomposition[i];
909
+ let fillColor = CONFIG.color.silhouetteMask;
910
+ if (usePrimitiveColorsTargets && primInfo?.kind && primitiveColorIndices) {
911
+ const kindToIndex = {
912
+ "square": 0,
913
+ "smalltriangle": 1,
914
+ "parallelogram": 2,
915
+ "medtriangle": 3,
916
+ "largetriangle": 4
917
+ };
918
+ const primitiveIndex = kindToIndex[primInfo.kind];
919
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
920
+ const colorIndex = primitiveColorIndices[primitiveIndex];
921
+ const color = CONFIG.color.primitiveColors[colorIndex];
922
+ if (color) {
923
+ fillColor = color;
924
+ }
925
+ }
926
+ }
927
+ return /* @__PURE__ */ React.createElement(
928
+ "path",
929
+ {
930
+ key: `prim-fill-${i}`,
931
+ d: pathD(scaledPoly),
932
+ fill: fillColor,
933
+ opacity: CONFIG.opacity.silhouetteMask,
934
+ stroke: "none"
935
+ }
936
+ );
937
+ }));
938
+ } else {
939
+ const placedPolys = placedSilBySector.get(s.id) ?? [];
940
+ if (!placedPolys.length) return null;
941
+ 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 })));
942
+ }
943
+ });
944
+ const renderPieces = (piecesFilter) => {
945
+ const piecesToRender = pieces;
946
+ return piecesToRender.sort((a, b) => {
947
+ if (draggingId === a.id) return 1;
948
+ if (draggingId === b.id) return -1;
949
+ return 0;
950
+ }).map((p) => {
951
+ const bp = controller.getBlueprint(p.blueprintId);
952
+ const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
953
+ const isDragging = draggingId === p.id;
954
+ const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
955
+ const isConnectivityLocked = lockedPieceId === p.id;
956
+ selectedPieceId === p.id;
957
+ const isCarriedInvalid = isDragging && dragInvalid;
958
+ const translateX = p.x - bb.min.x;
959
+ const translateY = p.y - bb.min.y;
960
+ const validFillColor = getPieceColor(
961
+ bp,
962
+ usePrimitiveColorsBlueprints || false,
963
+ CONFIG.color.piece.validFill,
964
+ primitiveColorIndices || [0, 1, 2, 3, 4]
965
+ );
966
+ const draggingFillColor = getPieceColor(
967
+ bp,
968
+ usePrimitiveColorsBlueprints || false,
969
+ CONFIG.color.piece.draggingFill,
970
+ primitiveColorIndices || [0, 1, 2, 3, 4]
971
+ );
972
+ 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) => {
973
+ const showBorders = shouldShowBorders();
974
+ shouldUseSelectiveBorders(p.blueprintId);
975
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
976
+ "path",
977
+ {
978
+ d: pathD(poly),
979
+ fill: isConnectivityLocked ? CONFIG.color.piece.invalidFill : isCarriedInvalid ? CONFIG.color.piece.invalidFill : isDragging ? draggingFillColor : validFillColor,
980
+ 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,
981
+ stroke: "none",
982
+ onPointerDown: (e) => onPiecePointerDown(e, p)
983
+ }
984
+ ), showBorders);
985
+ }), isDragging && isCarriedInvalid && "shape" in bp && bp.shape.length > 0 && (() => {
986
+ const { cx, cy } = polysAABB$1(bp.shape);
987
+ const size = CONFIG.size.invalidMarker.sizePx;
988
+ const strokeWidth = CONFIG.size.invalidMarker.strokePx;
989
+ const borderWidth = strokeWidth + 2;
990
+ return /* @__PURE__ */ React.createElement("g", { key: "invalid-marker" }, /* @__PURE__ */ React.createElement(
991
+ "line",
992
+ {
993
+ x1: cx - size,
994
+ y1: cy - size,
995
+ x2: cx + size,
996
+ y2: cy + size,
997
+ stroke: "white",
998
+ strokeWidth: borderWidth,
999
+ strokeLinecap: "round"
1000
+ }
1001
+ ), /* @__PURE__ */ React.createElement(
1002
+ "line",
1003
+ {
1004
+ x1: cx - size,
1005
+ y1: cy - size,
1006
+ x2: cx + size,
1007
+ y2: cy + size,
1008
+ stroke: CONFIG.color.piece.invalidStroke,
1009
+ strokeWidth,
1010
+ strokeLinecap: "round"
1011
+ }
1012
+ ), /* @__PURE__ */ React.createElement(
1013
+ "line",
1014
+ {
1015
+ x1: cx + size,
1016
+ y1: cy - size,
1017
+ x2: cx - size,
1018
+ y2: cy + size,
1019
+ stroke: "white",
1020
+ strokeWidth: borderWidth,
1021
+ strokeLinecap: "round"
1022
+ }
1023
+ ), /* @__PURE__ */ React.createElement(
1024
+ "line",
1025
+ {
1026
+ x1: cx + size,
1027
+ y1: cy - size,
1028
+ x2: cx - size,
1029
+ y2: cy + size,
1030
+ stroke: CONFIG.color.piece.invalidStroke,
1031
+ strokeWidth,
1032
+ strokeLinecap: "round"
1033
+ }
1034
+ ));
1035
+ })());
1036
+ });
1037
+ };
864
1038
  const centerView = controller.state.blueprintView;
865
1039
  const bps = centerView === "primitives" ? controller.state.primitives : controller.state.quickstash;
866
1040
  const QS_SLOTS = controller.state.cfg.maxQuickstashSlots;
@@ -881,6 +1055,12 @@ function BoardView(props) {
881
1055
  const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
882
1056
  const cx = bb.min.x + bb.width / 2;
883
1057
  const cy = bb.min.y + bb.height / 2;
1058
+ const fillColor = getPieceColor(
1059
+ bp,
1060
+ usePrimitiveColorsBlueprints || false,
1061
+ CONFIG.color.blueprint.fill,
1062
+ primitiveColorIndices || [0, 1, 2, 3, 4]
1063
+ );
884
1064
  return /* @__PURE__ */ React.createElement(
885
1065
  "g",
886
1066
  {
@@ -892,7 +1072,7 @@ function BoardView(props) {
892
1072
  {
893
1073
  key: idx,
894
1074
  d: pathD(poly),
895
- fill: CONFIG.color.blueprint.fill,
1075
+ fill: fillColor,
896
1076
  opacity: CONFIG.opacity.blueprint,
897
1077
  stroke: "none",
898
1078
  strokeWidth: 0,
@@ -916,7 +1096,7 @@ function BoardView(props) {
916
1096
  onPointerDown: (e) => {
917
1097
  onRootPointerDown(e);
918
1098
  },
919
- style: { background: "#f5f5f5", touchAction: "none", userSelect: "none" }
1099
+ style: { background: CONFIG.color.background, touchAction: "none", userSelect: "none" }
920
1100
  },
921
1101
  layout.sectors.map((s, i) => {
922
1102
  const done = !!controller.state.sectors[s.id].completedAt;
@@ -926,35 +1106,7 @@ function BoardView(props) {
926
1106
  const work = layout.bands.workspace;
927
1107
  return /* @__PURE__ */ React.createElement("g", { key: `bands-${s.id}` }, controller.state.cfg.target === "workspace" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("path", { d: wedgePath(layout.cx, layout.cy, sil[0], sil[1], s.start, s.end), fill: baseSil, stroke: CONFIG.color.bands.silhouette.stroke, strokeWidth: CONFIG.size.stroke.bandPx, pointerEvents: "none" }), /* @__PURE__ */ React.createElement("path", { d: wedgePath(layout.cx, layout.cy, work[0], work[1], s.start, s.end), fill: done ? CONFIG.color.completion.fill : baseWork, stroke: done ? CONFIG.color.completion.stroke : CONFIG.color.bands.workspace.stroke, strokeWidth: CONFIG.size.stroke.bandPx, pointerEvents: "none" })) : /* @__PURE__ */ React.createElement("path", { d: wedgePath(layout.cx, layout.cy, sil[0], sil[1], s.start, s.end), fill: done ? CONFIG.color.completion.fill : baseSil, stroke: done ? CONFIG.color.completion.stroke : CONFIG.color.bands.silhouette.stroke, strokeWidth: CONFIG.size.stroke.bandPx, pointerEvents: "none" }));
928
1108
  }),
929
- pieces.sort((a, b) => {
930
- if (draggingId === a.id) return 1;
931
- if (draggingId === b.id) return -1;
932
- return 0;
933
- }).map((p) => {
934
- const bp = controller.getBlueprint(p.blueprintId);
935
- const bb = boundsOfBlueprint(bp, (k) => controller.getPrimitive(k));
936
- const isDragging = draggingId === p.id;
937
- const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
938
- const isConnectivityLocked = lockedPieceId === p.id;
939
- selectedPieceId === p.id;
940
- const isCarriedInvalid = isDragging && dragInvalid;
941
- const translateX = p.x - bb.min.x;
942
- const translateY = p.y - bb.min.y;
943
- return /* @__PURE__ */ React.createElement("g", { key: p.id, ref: setPieceRef(p.id), transform: `translate(${translateX}, ${translateY})`, style: { cursor: locked ? "default" : clickMode ? "pointer" : "grab" }, pointerEvents: clickMode && isDragging ? "none" : "auto" }, ("shape" in bp ? bp.shape : []).map((poly, idx) => {
944
- const showBorders = shouldShowBorders();
945
- shouldUseSelectiveBorders(p.blueprintId);
946
- return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
947
- "path",
948
- {
949
- d: pathD(poly),
950
- fill: isConnectivityLocked ? CONFIG.color.piece.invalidFill : isCarriedInvalid ? CONFIG.color.piece.invalidFill : isDragging ? CONFIG.color.piece.draggingFill : CONFIG.color.piece.validFill,
951
- opacity: isConnectivityLocked ? CONFIG.opacity.piece.invalid : isCarriedInvalid ? CONFIG.opacity.piece.invalid : isDragging ? CONFIG.opacity.piece.dragging : locked ? CONFIG.opacity.piece.locked : CONFIG.opacity.piece.normal,
952
- stroke: "none",
953
- onPointerDown: (e) => onPiecePointerDown(e, p)
954
- }
955
- ), showBorders);
956
- }));
957
- }),
1109
+ /* @__PURE__ */ React.createElement(React.Fragment, null, renderSilhouettes(), renderPieces()) ,
958
1110
  layout.sectors.map((s) => {
959
1111
  const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
960
1112
  if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
@@ -962,23 +1114,32 @@ function BoardView(props) {
962
1114
  const rect = rectForBand(layout, s, "silhouette", 1);
963
1115
  const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
964
1116
  const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
965
- return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(React.Fragment, { key: `prim-${i}` }, /* @__PURE__ */ React.createElement(
966
- "path",
967
- {
968
- d: pathD(scaledPoly),
969
- fill: CONFIG.color.silhouetteMask,
970
- opacity: CONFIG.opacity.silhouetteMask,
971
- stroke: "none"
972
- }
973
- ), /* @__PURE__ */ React.createElement(
974
- "path",
975
- {
976
- d: pathD(scaledPoly),
977
- fill: "none",
978
- stroke: CONFIG.color.tangramDecomposition.stroke,
979
- strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
1117
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
1118
+ const primInfo = primitiveDecomposition[i];
1119
+ if (usePrimitiveColorsTargets && primInfo?.kind && primitiveColorIndices) {
1120
+ const kindToIndex = {
1121
+ "square": 0,
1122
+ "smalltriangle": 1,
1123
+ "parallelogram": 2,
1124
+ "medtriangle": 3,
1125
+ "largetriangle": 4
1126
+ };
1127
+ const primitiveIndex = kindToIndex[primInfo.kind];
1128
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
1129
+ primitiveColorIndices[primitiveIndex];
1130
+ }
980
1131
  }
981
- ))));
1132
+ return /* @__PURE__ */ React.createElement(
1133
+ "path",
1134
+ {
1135
+ key: `prim-fill-${i}`,
1136
+ d: pathD(scaledPoly),
1137
+ fill: "none",
1138
+ opacity: 0,
1139
+ stroke: "none"
1140
+ }
1141
+ );
1142
+ }));
982
1143
  } else {
983
1144
  const placedPolys = placedSilBySector.get(s.id) ?? [];
984
1145
  if (!placedPolys.length) return null;
@@ -1069,7 +1230,25 @@ function BoardView(props) {
1069
1230
  const by = layout.cy + blueprintRingR * Math.sin(theta);
1070
1231
  return renderBlueprintGlyph(bp, bx, by);
1071
1232
  }),
1072
- controller.state.endedAt && /* @__PURE__ */ React.createElement("g", { pointerEvents: "none" }, /* @__PURE__ */ React.createElement("circle", { cx: layout.cx, cy: layout.cy, r: layout.outerR - 3, fill: "none", stroke: CONFIG.color.piece.allGreenStroke, strokeWidth: CONFIG.size.stroke.allGreenStrokePx, opacity: 0 }, /* @__PURE__ */ React.createElement("animate", { attributeName: "opacity", from: "0", to: "1", dur: "160ms", fill: "freeze" }), /* @__PURE__ */ React.createElement("animate", { attributeName: "opacity", from: "1", to: "0", begin: "560ms", dur: "520ms", fill: "freeze" })))
1233
+ 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" }))),
1234
+ showTangramDecomposition && layout.sectors.map((s) => {
1235
+ const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
1236
+ if (!sectorCfg?.silhouette.primitiveDecomposition) return null;
1237
+ const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
1238
+ const rect = rectForBand(layout, s, "silhouette", 1);
1239
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
1240
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
1241
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-borders-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(
1242
+ "path",
1243
+ {
1244
+ key: `prim-border-${i}`,
1245
+ d: pathD(scaledPoly),
1246
+ fill: "none",
1247
+ stroke: CONFIG.color.tangramDecomposition.stroke,
1248
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
1249
+ }
1250
+ )));
1251
+ })
1073
1252
  );
1074
1253
  }
1075
1254
 
@@ -2954,7 +3133,10 @@ function GameBoard(props) {
2954
3133
  onPieceRemove,
2955
3134
  onInteraction,
2956
3135
  onTrialEnd,
2957
- onControllerReady
3136
+ onControllerReady,
3137
+ usePrimitiveColorsBlueprints,
3138
+ usePrimitiveColorsTargets,
3139
+ primitiveColorIndices
2958
3140
  } = props;
2959
3141
  const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1e3) : 0);
2960
3142
  const controller = React.useMemo(() => {
@@ -3275,19 +3457,28 @@ function GameBoard(props) {
3275
3457
  const headerStyle = {
3276
3458
  display: "flex",
3277
3459
  flexDirection: "row",
3278
- justifyContent: "space-between",
3460
+ justifyContent: "center",
3279
3461
  alignItems: "center",
3280
3462
  padding: "20px",
3281
- background: "#f5f5f5",
3463
+ background: CONFIG.color.background,
3282
3464
  flex: "0 0 auto"
3283
3465
  };
3466
+ const headerContentStyle = {
3467
+ position: "relative",
3468
+ width: `${svgDimensions.width}px`,
3469
+ maxWidth: "100%",
3470
+ display: "flex",
3471
+ justifyContent: "center",
3472
+ alignItems: "center"
3473
+ };
3284
3474
  const instructionsStyle = {
3285
- flexGrow: 1,
3286
3475
  fontSize: "20px",
3287
3476
  lineHeight: 1.5,
3288
- marginRight: "20px"
3477
+ textAlign: "center"
3289
3478
  };
3290
3479
  const timerStyle = {
3480
+ position: "absolute",
3481
+ right: 0,
3291
3482
  fontSize: "24px",
3292
3483
  fontWeight: "bold",
3293
3484
  fontFamily: "monospace",
@@ -3302,14 +3493,14 @@ function GameBoard(props) {
3302
3493
  justifyContent: "center",
3303
3494
  overflow: "hidden"
3304
3495
  };
3305
- 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(
3496
+ 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(
3306
3497
  "div",
3307
3498
  {
3308
3499
  className: "tangram-instructions",
3309
3500
  style: instructionsStyle,
3310
3501
  dangerouslySetInnerHTML: { __html: instructions }
3311
3502
  }
3312
- ), 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(
3503
+ ), 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(
3313
3504
  BoardView,
3314
3505
  {
3315
3506
  controller,
@@ -3337,6 +3528,9 @@ function GameBoard(props) {
3337
3528
  onCenterBadgePointerDown,
3338
3529
  showTangramDecomposition: showTangramDecomposition ?? false,
3339
3530
  scaleS,
3531
+ usePrimitiveColorsBlueprints: usePrimitiveColorsBlueprints ?? false,
3532
+ usePrimitiveColorsTargets: usePrimitiveColorsTargets ?? false,
3533
+ primitiveColorIndices: primitiveColorIndices ?? [0, 1, 2, 3, 4],
3340
3534
  ...eventCallbacks
3341
3535
  }
3342
3536
  ))));
@@ -3556,7 +3750,10 @@ function startConstructionTrial(display_element, params, _jsPsych) {
3556
3750
  trialParams,
3557
3751
  ...params.instructions && { instructions: params.instructions },
3558
3752
  ...params.onInteraction && { onInteraction: params.onInteraction },
3559
- ...params.onTrialEnd && { onTrialEnd: params.onTrialEnd }
3753
+ ...params.onTrialEnd && { onTrialEnd: params.onTrialEnd },
3754
+ ...params.usePrimitiveColorsBlueprints !== void 0 && { usePrimitiveColorsBlueprints: params.usePrimitiveColorsBlueprints },
3755
+ ...params.usePrimitiveColorsTargets !== void 0 && { usePrimitiveColorsTargets: params.usePrimitiveColorsTargets },
3756
+ ...params.primitiveColorIndices !== void 0 && { primitiveColorIndices: params.primitiveColorIndices }
3560
3757
  };
3561
3758
  const root = client.createRoot(display_element);
3562
3759
  root.render(React.createElement(GameBoard, gameBoardProps));
@@ -3641,6 +3838,24 @@ const info$2 = {
3641
3838
  type: jspsych.ParameterType.FUNCTION,
3642
3839
  default: void 0,
3643
3840
  description: "Callback when trial completes with full data"
3841
+ },
3842
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
3843
+ use_primitive_colors_blueprints: {
3844
+ type: jspsych.ParameterType.BOOL,
3845
+ default: false,
3846
+ description: "Whether each primitive shape type should have its own distinct color in the blueprint dock area"
3847
+ },
3848
+ /** Whether to use distinct colors for each primitive shape type in target tangrams */
3849
+ use_primitive_colors_targets: {
3850
+ type: jspsych.ParameterType.BOOL,
3851
+ default: false,
3852
+ description: "Whether each primitive shape type should have its own distinct color in target tangram silhouettes"
3853
+ },
3854
+ /** Indices mapping primitives to colors from the color palette */
3855
+ primitive_color_indices: {
3856
+ type: jspsych.ParameterType.OBJECT,
3857
+ default: [0, 1, 2, 3, 4],
3858
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
3644
3859
  }
3645
3860
  },
3646
3861
  data: {
@@ -3706,7 +3921,10 @@ class TangramConstructPlugin {
3706
3921
  show_tangram_decomposition: trial.show_tangram_decomposition,
3707
3922
  instructions: trial.instructions,
3708
3923
  onInteraction: trial.onInteraction,
3709
- onTrialEnd: wrappedOnTrialEnd
3924
+ onTrialEnd: wrappedOnTrialEnd,
3925
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
3926
+ usePrimitiveColorsTargets: trial.use_primitive_colors_targets,
3927
+ primitiveColorIndices: trial.primitive_color_indices
3710
3928
  };
3711
3929
  const { root, display_element: element, jsPsych } = startConstructionTrial(display_element, params, this.jsPsych);
3712
3930
  element.__reactContext = { root, jsPsych };
@@ -3724,7 +3942,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3724
3942
  quickstashMacros,
3725
3943
  primitiveOrder,
3726
3944
  onInteraction,
3727
- onTrialEnd
3945
+ onTrialEnd,
3946
+ usePrimitiveColorsBlueprints,
3947
+ primitiveColorIndices
3728
3948
  } = params;
3729
3949
  const { onInteraction: _, onTrialEnd: __, ...trialParams } = params;
3730
3950
  const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => primitiveOrder.indexOf(a.kind) - primitiveOrder.indexOf(b.kind));
@@ -3816,7 +4036,9 @@ function startPrepTrial(display_element, params, jsPsych) {
3816
4036
  ...params.instructions && { instructions: params.instructions },
3817
4037
  onControllerReady: handleControllerReady,
3818
4038
  ...onInteraction && { onInteraction },
3819
- ...onTrialEnd && { onTrialEnd }
4039
+ ...onTrialEnd && { onTrialEnd },
4040
+ ...usePrimitiveColorsBlueprints !== void 0 && { usePrimitiveColorsBlueprints },
4041
+ ...primitiveColorIndices !== void 0 && { primitiveColorIndices }
3820
4042
  }));
3821
4043
  return { root, display_element, jsPsych };
3822
4044
  }
@@ -3882,6 +4104,18 @@ const info$1 = {
3882
4104
  onTrialEnd: {
3883
4105
  type: jspsych.ParameterType.FUNCTION,
3884
4106
  default: void 0
4107
+ },
4108
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
4109
+ use_primitive_colors_blueprints: {
4110
+ type: jspsych.ParameterType.BOOL,
4111
+ default: false,
4112
+ description: "Whether each primitive shape type should have its own distinct color in the blueprint dock area"
4113
+ },
4114
+ /** Indices mapping primitives to colors from the color palette */
4115
+ primitive_color_indices: {
4116
+ type: jspsych.ParameterType.OBJECT,
4117
+ default: [0, 1, 2, 3, 4],
4118
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
3885
4119
  }
3886
4120
  },
3887
4121
  data: {
@@ -3926,7 +4160,9 @@ class TangramPrepPlugin {
3926
4160
  primitiveOrder: trial.primitive_order,
3927
4161
  instructions: trial.instructions,
3928
4162
  onInteraction: trial.onInteraction,
3929
- onTrialEnd: wrappedOnTrialEnd
4163
+ onTrialEnd: wrappedOnTrialEnd,
4164
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
4165
+ primitiveColorIndices: trial.primitive_color_indices
3930
4166
  };
3931
4167
  const { root, display_element: element, jsPsych } = startPrepTrial(display_element, params, this.jsPsych);
3932
4168
  element.__reactContext = { root, jsPsych };
@@ -3946,6 +4182,8 @@ function NBackView({ params }) {
3946
4182
  instructions,
3947
4183
  button_text,
3948
4184
  duration,
4185
+ usePrimitiveColors,
4186
+ primitiveColorIndices,
3949
4187
  onTrialEnd
3950
4188
  } = params;
3951
4189
  const trialStartTime = React.useRef(Date.now());
@@ -4001,7 +4239,12 @@ function NBackView({ params }) {
4001
4239
  ...data,
4002
4240
  accuracy,
4003
4241
  tangram_id: tangram.tangramID,
4004
- is_match: isMatch
4242
+ is_match: isMatch,
4243
+ show_tangram_decomposition,
4244
+ use_primitive_colors: usePrimitiveColors,
4245
+ primitive_color_indices: primitiveColorIndices,
4246
+ duration,
4247
+ button_text
4005
4248
  };
4006
4249
  if (onTrialEnd) {
4007
4250
  onTrialEnd(trialData);
@@ -4055,35 +4298,77 @@ function NBackView({ params }) {
4055
4298
  if (show_tangram_decomposition) {
4056
4299
  const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
4057
4300
  const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, centerPos);
4058
- 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(
4059
- "path",
4060
- {
4061
- d: pathD(scaledPoly),
4062
- fill: CONFIG.color.silhouetteMask,
4063
- opacity: CONFIG.opacity.silhouetteMask,
4064
- stroke: "none"
4065
- }
4066
- ), /* @__PURE__ */ React.createElement(
4067
- "path",
4068
- {
4069
- d: pathD(scaledPoly),
4070
- fill: "none",
4071
- stroke: CONFIG.color.tangramDecomposition.stroke,
4072
- strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
4301
+ return /* @__PURE__ */ React.createElement("g", { key: "sil-decomposed", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
4302
+ const primInfo = primitiveDecomposition[i];
4303
+ let fillColor = CONFIG.color.silhouetteMask;
4304
+ if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {
4305
+ const kindToIndex = {
4306
+ "square": 0,
4307
+ "smalltriangle": 1,
4308
+ "parallelogram": 2,
4309
+ "medtriangle": 3,
4310
+ "largetriangle": 4
4311
+ };
4312
+ const primitiveIndex = kindToIndex[primInfo.kind];
4313
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
4314
+ const colorIndex = primitiveColorIndices[primitiveIndex];
4315
+ const color = CONFIG.color.primitiveColors[colorIndex];
4316
+ if (color) {
4317
+ fillColor = color;
4318
+ }
4319
+ }
4073
4320
  }
4074
- ))));
4321
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: `prim-${i}` }, /* @__PURE__ */ React.createElement(
4322
+ "path",
4323
+ {
4324
+ d: pathD(scaledPoly),
4325
+ fill: fillColor,
4326
+ opacity: CONFIG.opacity.silhouetteMask,
4327
+ stroke: "none"
4328
+ }
4329
+ ), /* @__PURE__ */ React.createElement(
4330
+ "path",
4331
+ {
4332
+ d: pathD(scaledPoly),
4333
+ fill: "none",
4334
+ stroke: CONFIG.color.tangramDecomposition.stroke,
4335
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
4336
+ }
4337
+ ));
4338
+ }));
4075
4339
  } else {
4076
4340
  const placedPolys = placeSilhouetteGridAlignedAsPolys(mask, scaleS, centerPos);
4077
- return /* @__PURE__ */ React.createElement("g", { key: "sil-unified", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(
4078
- "path",
4079
- {
4080
- key: `sil-${i}`,
4081
- d: pathD(scaledPoly),
4082
- fill: CONFIG.color.silhouetteMask,
4083
- opacity: CONFIG.opacity.silhouetteMask,
4084
- stroke: "none"
4341
+ return /* @__PURE__ */ React.createElement("g", { key: "sil-unified", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
4342
+ const primInfo = primitiveDecomposition[i];
4343
+ let fillColor = CONFIG.color.silhouetteMask;
4344
+ if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {
4345
+ const kindToIndex = {
4346
+ "square": 0,
4347
+ "smalltriangle": 1,
4348
+ "parallelogram": 2,
4349
+ "medtriangle": 3,
4350
+ "largetriangle": 4
4351
+ };
4352
+ const primitiveIndex = kindToIndex[primInfo.kind];
4353
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
4354
+ const colorIndex = primitiveColorIndices[primitiveIndex];
4355
+ const color = CONFIG.color.primitiveColors[colorIndex];
4356
+ if (color) {
4357
+ fillColor = color;
4358
+ }
4359
+ }
4085
4360
  }
4086
- )));
4361
+ return /* @__PURE__ */ React.createElement(
4362
+ "path",
4363
+ {
4364
+ key: `sil-${i}`,
4365
+ d: pathD(scaledPoly),
4366
+ fill: fillColor,
4367
+ opacity: CONFIG.opacity.silhouetteMask,
4368
+ stroke: "none"
4369
+ }
4370
+ );
4371
+ }));
4087
4372
  }
4088
4373
  };
4089
4374
  return /* @__PURE__ */ React.createElement("div", { style: {
@@ -4182,6 +4467,18 @@ const info = {
4182
4467
  default: 3e3,
4183
4468
  description: "Duration in milliseconds to display tangram and accept responses"
4184
4469
  },
4470
+ /** Whether to use distinct colors for each primitive shape type */
4471
+ use_primitive_colors: {
4472
+ type: jspsych.ParameterType.BOOL,
4473
+ default: false,
4474
+ description: "Whether each primitive shape type should have its own distinct color in the displayed tangram"
4475
+ },
4476
+ /** Indices mapping primitives to colors from the color palette */
4477
+ primitive_color_indices: {
4478
+ type: jspsych.ParameterType.OBJECT,
4479
+ default: [0, 1, 2, 3, 4],
4480
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
4481
+ },
4185
4482
  /** Callback fired when trial ends */
4186
4483
  onTrialEnd: {
4187
4484
  type: jspsych.ParameterType.FUNCTION,
@@ -4248,6 +4545,8 @@ class TangramNBackPlugin {
4248
4545
  instructions: trial.instructions,
4249
4546
  button_text: trial.button_text,
4250
4547
  duration: trial.duration,
4548
+ usePrimitiveColors: trial.use_primitive_colors,
4549
+ primitiveColorIndices: trial.primitive_color_indices,
4251
4550
  onTrialEnd: wrappedOnTrialEnd
4252
4551
  };
4253
4552
  const { root, display_element: element, jsPsych } = startNBackTrial(display_element, params, this.jsPsych);