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
@@ -1,23 +1,23 @@
1
1
  /**
2
2
  * BlueprintRing - Reusable blueprint selection interface component
3
- *
3
+ *
4
4
  * This component provides the central blueprint selection interface that displays
5
5
  * available piece blueprints in a circular or semicircular arrangement around
6
6
  * a center badge. It handles both primitive pieces and quickstash macros.
7
- *
7
+ *
8
8
  * ## Key Features
9
9
  * - **Dual Mode Display**: Switches between primitives and quickstash collections
10
- * - **Flexible Layout**: Adapts to circle and semicircle layout modes
10
+ * - **Flexible Layout**: Adapts to circle and semicircle layout modes
11
11
  * - **Interactive Badge**: Center button to toggle between piece collections
12
12
  * - **Responsive Geometry**: Automatically calculates ring radius based on content
13
13
  * - **Event Integration**: Provides structured callbacks for piece selection
14
- *
14
+ *
15
15
  * ## Architecture
16
16
  * - Pure presentation component (no internal state management)
17
17
  * - Receives all data and callbacks via props (dependency injection)
18
18
  * - Computes ring geometry based on layout constraints
19
19
  * - Renders SVG blueprint glyphs with proper scaling and positioning
20
- *
20
+ *
21
21
  * ## Usage
22
22
  * Typically used within GameBoard or plugin components:
23
23
  * ```typescript
@@ -30,10 +30,10 @@
30
30
  * onCenterBadgePointerDown={handleViewToggle}
31
31
  * />
32
32
  * ```
33
- *
33
+ *
34
34
  * @see {@link GameBoard} Primary container that uses this component
35
35
  * @see {@link useBlueprintRing} Hook for managing blueprint ring state
36
- * @since Phase 3.3 - Extracted from monolithic Board component
36
+ * @since Phase 3.3 - Extracted from monolithic Board component
37
37
  * @author Claude Code Assistant
38
38
  */
39
39
 
@@ -47,45 +47,94 @@ function pathD(poly: any[]) {
47
47
  return `M ${poly.map((pt: any) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
48
48
  }
49
49
 
50
+ /**
51
+ * Get the fill color for a blueprint based on its type
52
+ *
53
+ * REQUIRES: blueprint is valid, primitiveColorIndices is an array of 5 valid indices
54
+ * EFFECTS: Returns color based on primitive type if usePrimitiveColors is true,
55
+ * otherwise returns the default color. Composites always use default color.
56
+ */
57
+ function getBlueprintColor(
58
+ blueprint: Blueprint,
59
+ usePrimitiveColors: boolean,
60
+ defaultColor: string,
61
+ primitiveColorIndices: number[]
62
+ ): string {
63
+ if (!usePrimitiveColors) {
64
+ return defaultColor;
65
+ }
66
+
67
+ // For primitive blueprints, use the mapped color from CONFIG
68
+ if ('kind' in blueprint) {
69
+ const kind = blueprint.kind as TanKind;
70
+ // Map primitive kind to index: square=0, smalltriangle=1, parallelogram=2, medtriangle=3, largetriangle=4
71
+ const kindToIndex: Record<TanKind, number> = {
72
+ 'square': 0,
73
+ 'smalltriangle': 1,
74
+ 'parallelogram': 2,
75
+ 'medtriangle': 3,
76
+ 'largetriangle': 4
77
+ };
78
+ const primitiveIndex = kindToIndex[kind];
79
+ if (primitiveIndex !== undefined && primitiveColorIndices[primitiveIndex] !== undefined) {
80
+ const colorIndex = primitiveColorIndices[primitiveIndex];
81
+ const color = CONFIG.color.primitiveColors[colorIndex];
82
+ if (color) {
83
+ return color;
84
+ }
85
+ }
86
+ return defaultColor;
87
+ }
88
+
89
+ // For composite blueprints, always use the default color
90
+ return defaultColor;
91
+ }
92
+
50
93
  /**
51
94
  * Props interface for BlueprintRing component
52
- *
95
+ *
53
96
  * This interface defines all the data and callbacks needed to render
54
97
  * the blueprint selection interface. All props use dependency injection.
55
98
  */
56
99
  export interface BlueprintRingProps {
57
100
  /** Array of primitive tangram piece blueprints */
58
101
  primitives: PrimitiveBlueprint[];
59
-
102
+
60
103
  /** Array of quickstash/macro piece blueprints */
61
104
  quickstash: Blueprint[];
62
-
63
- /**
105
+
106
+ /**
64
107
  * Current view mode for blueprint ring
65
108
  * - "primitives": Show primitive tangram pieces
66
109
  * - "quickstash": Show macro/composite pieces
67
110
  */
68
111
  currentView: "primitives" | "quickstash";
69
-
112
+
70
113
  /** Computed circle layout containing geometry information */
71
114
  layout: CircleLayout;
72
-
115
+
73
116
  /** Radius of the center toggle badge in pixels */
74
117
  badgeR: number;
75
-
118
+
76
119
  /** Center position of the toggle badge */
77
120
  badgeCenter: { x: number; y: number };
78
-
121
+
79
122
  /** Maximum number of quickstash slots to display */
80
123
  maxQuickstashSlots: number;
81
-
124
+
82
125
  /** ID of currently dragging piece (null if none) - used for interaction state */
83
126
  draggingId: string | null;
84
-
85
- /**
127
+
128
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
129
+ usePrimitiveColorsBlueprints?: boolean;
130
+
131
+ /** Array of 5 integers mapping primitives to colors from CONFIG.color.primitiveColors */
132
+ primitiveColorIndices?: number[];
133
+
134
+ /**
86
135
  * Callback fired when user starts interacting with a blueprint
87
136
  * @param e - Pointer event from the interaction
88
- * @param bp - Blueprint that was selected
137
+ * @param bp - Blueprint that was selected
89
138
  * @param bpGeom - Geometry information for the selected blueprint
90
139
  */
91
140
  onBlueprintPointerDown: (
@@ -93,14 +142,14 @@ export interface BlueprintRingProps {
93
142
  bp: Blueprint,
94
143
  bpGeom: { bx: number; by: number; cx: number; cy: number }
95
144
  ) => void;
96
-
97
- /**
145
+
146
+ /**
98
147
  * Callback fired when user clicks the center badge to toggle views
99
148
  * @param e - Pointer event from the badge click
100
149
  */
101
150
  onCenterBadgePointerDown: (e: React.PointerEvent) => void;
102
-
103
- /**
151
+
152
+ /**
104
153
  * Helper function to lookup primitive blueprints by kind
105
154
  * Used for rendering composite pieces that reference primitives
106
155
  */
@@ -119,22 +168,24 @@ export default function BlueprintRing(props: BlueprintRingProps) {
119
168
  draggingId,
120
169
  onBlueprintPointerDown,
121
170
  onCenterBadgePointerDown,
122
- getPrimitive
171
+ getPrimitive,
172
+ usePrimitiveColorsBlueprints,
173
+ primitiveColorIndices
123
174
  } = props;
124
-
175
+
125
176
  // Determine which blueprints to show
126
177
  const blueprints: Blueprint[] = currentView === "primitives" ? primitives : quickstash;
127
-
178
+
128
179
  // Calculate ring geometry
129
180
  const QS_SLOTS = maxQuickstashSlots;
130
181
  const PRIM_SLOTS = primitives.length;
131
182
  const slotsForView = currentView === "primitives" ? Math.max(1, PRIM_SLOTS) : Math.max(1, QS_SLOTS);
132
183
  const sweep = layout.mode === "circle" ? Math.PI * 2 : Math.PI;
133
184
  const delta = sweep / slotsForView;
134
-
185
+
135
186
  const start = layout.mode === "circle" ? -Math.PI / 2 : Math.PI;
136
187
  const blueprintTheta = (i: number) => start + (i + 0.5) * delta;
137
-
188
+
138
189
  // Chord requirement calculation
139
190
  const anchorsDiameterToPx = (anchorsDiag: number, gridPx: number = CONFIG.layout.grid.stepPx) =>
140
191
  anchorsDiag * Math.SQRT2 * gridPx;
@@ -143,22 +194,29 @@ export default function BlueprintRing(props: BlueprintRingProps) {
143
194
  : CONFIG.layout.constraints.quickstashDiamAnchors;
144
195
  const D_slot = anchorsDiameterToPx(reqAnchors);
145
196
  const R_needed = D_slot / (2 * Math.max(1e-9, Math.sin(delta / 2)));
146
-
197
+
147
198
  // Minimum radius to avoid overlapping with badge
148
199
  // Badge takes up: badgeR + margin, plus we need half the slot diameter for clearance
149
200
  const R_min = badgeR + CONFIG.size.centerBadge.marginPx + D_slot / 2;
150
201
  const ringMax = layout.innerR - (badgeR + CONFIG.size.centerBadge.marginPx);
151
-
202
+
152
203
  // Clamp to [R_min, ringMax]
153
204
  const blueprintRingR = Math.min(Math.max(R_needed, R_min), ringMax);
154
-
205
+
155
206
  // Blueprint glyph renderer
156
207
  const renderBlueprintGlyph = (bp: Blueprint, bx: number, by: number) => {
157
208
  const bb = boundsOfBlueprint(bp, (k: string) => getPrimitive(k as TanKind)!);
158
209
  const cx = bb.min.x + bb.width / 2;
159
210
  const cy = bb.min.y + bb.height / 2;
160
211
  const selected = false; // Future enhancement: add selection highlighting
161
-
212
+
213
+ const fillColor = getBlueprintColor(
214
+ bp,
215
+ usePrimitiveColorsBlueprints || false,
216
+ CONFIG.color.blueprint.fill,
217
+ primitiveColorIndices || [0, 1, 2, 3, 4]
218
+ );
219
+
162
220
  return (
163
221
  <g
164
222
  key={bp.id}
@@ -168,7 +226,7 @@ export default function BlueprintRing(props: BlueprintRingProps) {
168
226
  <path
169
227
  key={idx}
170
228
  d={pathD(poly)}
171
- fill={CONFIG.color.blueprint.fill}
229
+ fill={fillColor}
172
230
  opacity={CONFIG.opacity.blueprint}
173
231
  stroke={selected ? CONFIG.color.blueprint.selectedStroke : "none"}
174
232
  strokeWidth={selected ? 2 : 0}
@@ -180,21 +238,21 @@ export default function BlueprintRing(props: BlueprintRingProps) {
180
238
  </g>
181
239
  );
182
240
  };
183
-
241
+
184
242
  return (
185
243
  <g className="blueprint-ring">
186
244
  {/* Center badge */}
187
- <g
188
- transform={`translate(${badgeCenter.x}, ${badgeCenter.y})`}
189
- style={{ cursor: draggingId ? "default" : "pointer" }}
245
+ <g
246
+ transform={`translate(${badgeCenter.x}, ${badgeCenter.y})`}
247
+ style={{ cursor: draggingId ? "default" : "pointer" }}
190
248
  onPointerDown={onCenterBadgePointerDown}
191
249
  >
192
250
  <circle r={badgeR} fill={CONFIG.color.blueprint.badgeFill} />
193
- <text
194
- textAnchor="middle"
195
- dominantBaseline="middle"
196
- fontSize={CONFIG.size.badgeFontPx}
197
- fill={CONFIG.color.blueprint.labelFill}
251
+ <text
252
+ textAnchor="middle"
253
+ dominantBaseline="middle"
254
+ fontSize={CONFIG.size.badgeFontPx}
255
+ fill={CONFIG.color.blueprint.labelFill}
198
256
  pointerEvents="none"
199
257
  >
200
258
  {currentView}
@@ -217,18 +275,18 @@ export default function BlueprintRing(props: BlueprintRingProps) {
217
275
  * Useful for components that need more control over blueprint ring behavior
218
276
  */
219
277
  export function useBlueprintRing(
220
- primitives: PrimitiveBlueprint[],
221
- quickstash: Blueprint[],
278
+ primitives: PrimitiveBlueprint[],
279
+ quickstash: Blueprint[],
222
280
  initialView: "primitives" | "quickstash" = "quickstash"
223
281
  ) {
224
282
  const [currentView, setCurrentView] = React.useState<"primitives" | "quickstash">(initialView);
225
-
283
+
226
284
  const switchView = React.useCallback(() => {
227
285
  setCurrentView(prev => prev === "primitives" ? "quickstash" : "primitives");
228
286
  }, []);
229
-
287
+
230
288
  const blueprints = currentView === "primitives" ? primitives : quickstash;
231
-
289
+
232
290
  return {
233
291
  currentView,
234
292
  blueprints,
@@ -1,6 +1,7 @@
1
1
  // src/core/config/config.ts
2
2
  export type Config = {
3
3
  color: {
4
+ background: string;
4
5
  bands: {
5
6
  silhouette: { fillEven: string; fillOdd: string; stroke: string };
6
7
  workspace: { fillEven: string; fillOdd: string; stroke: string };
@@ -12,6 +13,7 @@ export type Config = {
12
13
  ui: { light: string; dark: string };
13
14
  blueprint: { fill: string; selectedStroke: string; badgeFill: string; labelFill: string };
14
15
  tangramDecomposition: { stroke: string };
16
+ primitiveColors: string[];
15
17
  };
16
18
  opacity: {
17
19
  blueprint: number;
@@ -24,6 +26,7 @@ export type Config = {
24
26
  anchorRadiusPx: { valid: number; invalid: number };
25
27
  badgeFontPx: number;
26
28
  centerBadge: { fractionOfOuterR: number; minPx: number; marginPx: number };
29
+ invalidMarker: { sizePx: number; strokePx: number };
27
30
  };
28
31
  layout: {
29
32
  grid: { stepPx: number; unitPx: number };
@@ -41,35 +44,46 @@ export type Config = {
41
44
  snapRadiusPx: number;
42
45
  showBorders: boolean;
43
46
  hideTouchingBorders: boolean;
47
+ silhouettesBelowPieces: boolean;
44
48
  };
45
49
  };
46
50
 
47
51
  export const CONFIG: Config = {
48
52
  color: {
53
+ background: "#fff7e0ff",
49
54
  bands: {
50
- silhouette: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" },
51
- workspace: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" }
55
+ silhouette: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" },
56
+ workspace: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" }
52
57
  },
53
- completion: { fill: "#ccfff2", stroke: "#13da57" },
58
+ completion: { fill: "#ccffcc", stroke: "#13da57" },
54
59
  silhouetteMask: "#374151",
55
60
  anchors: { invalid: "#7dd3fc", valid: "#475569" },
56
- piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
61
+ // validFill used here for placed composites
62
+ piece: { draggingFill: "#8e7cc3", validFill: "#8e7cc3", invalidFill: "#d55c00", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
57
63
  ui: { light: "#60a5fa", dark: "#1d4ed8" },
58
64
  blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
59
- tangramDecomposition: { stroke: "#fef2cc" }
65
+ tangramDecomposition: { stroke: "#fef2cc" },
66
+ primitiveColors: [ // from seaborn "colorblind" palette, 6 colors, with red omitted
67
+ '#0173b2',
68
+ '#de8f05',
69
+ '#029e73',
70
+ '#cc78bc',
71
+ '#ca9161'
72
+ ]
60
73
  },
61
74
  opacity: {
62
- blueprint: 0.4,
75
+ blueprint: 0.6,
63
76
  silhouetteMask: 0.25,
64
77
  //anchors: { valid: 0.80, invalid: 0.50 },
65
78
  anchors: { invalid: 0.0, valid: 0.0 },
66
- piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 },
79
+ piece: { invalid: 1, dragging: 1, locked: 1, normal: 1 },
67
80
  },
68
81
  size: {
69
- stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
82
+ stroke: { bandPx: 5, pieceSelectedPx: 5, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
70
83
  anchorRadiusPx: { valid: 1.0, invalid: 1.0 },
71
84
  badgeFontPx: 16,
72
- centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
85
+ centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 },
86
+ invalidMarker: { sizePx: 10, strokePx: 4 }
73
87
  },
74
88
  layout: {
75
89
  grid: { stepPx: 20, unitPx: 40 },
@@ -85,6 +99,7 @@ export const CONFIG: Config = {
85
99
  game: {
86
100
  snapRadiusPx: 15,
87
101
  showBorders: false,
88
- hideTouchingBorders: true
102
+ hideTouchingBorders: true,
103
+ silhouettesBelowPieces: true
89
104
  }
90
105
  };
@@ -34,6 +34,9 @@ export interface StartConstructionTrialParams {
34
34
  instructions?: string;
35
35
  onInteraction?: (event: any) => void;
36
36
  onTrialEnd?: (data: any) => void;
37
+ usePrimitiveColorsBlueprints?: boolean;
38
+ usePrimitiveColorsTargets?: boolean;
39
+ primitiveColorIndices?: number[];
37
40
  }
38
41
 
39
42
  // Type for anchor-based composite definitions
@@ -143,7 +146,10 @@ export function startConstructionTrial(
143
146
  trialParams,
144
147
  ...(params.instructions && { instructions: params.instructions }),
145
148
  ...(params.onInteraction && { onInteraction: params.onInteraction }),
146
- ...(params.onTrialEnd && { onTrialEnd: params.onTrialEnd })
149
+ ...(params.onTrialEnd && { onTrialEnd: params.onTrialEnd }),
150
+ ...(params.usePrimitiveColorsBlueprints !== undefined && { usePrimitiveColorsBlueprints: params.usePrimitiveColorsBlueprints }),
151
+ ...(params.usePrimitiveColorsTargets !== undefined && { usePrimitiveColorsTargets: params.usePrimitiveColorsTargets }),
152
+ ...(params.primitiveColorIndices !== undefined && { primitiveColorIndices: params.primitiveColorIndices })
147
153
  };
148
154
 
149
155
  const root = createRoot(display_element);
@@ -79,6 +79,24 @@ const info = {
79
79
  type: ParameterType.FUNCTION,
80
80
  default: undefined,
81
81
  description: "Callback when trial completes with full data"
82
+ },
83
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
84
+ use_primitive_colors_blueprints: {
85
+ type: ParameterType.BOOL,
86
+ default: false,
87
+ description: "Whether each primitive shape type should have its own distinct color in the blueprint dock area"
88
+ },
89
+ /** Whether to use distinct colors for each primitive shape type in target tangrams */
90
+ use_primitive_colors_targets: {
91
+ type: ParameterType.BOOL,
92
+ default: false,
93
+ description: "Whether each primitive shape type should have its own distinct color in target tangram silhouettes"
94
+ },
95
+ /** Indices mapping primitives to colors from the color palette */
96
+ primitive_color_indices: {
97
+ type: ParameterType.OBJECT,
98
+ default: [0, 1, 2, 3, 4],
99
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
82
100
  }
83
101
  },
84
102
  data: {
@@ -163,7 +181,10 @@ class TangramConstructPlugin implements JsPsychPlugin<Info> {
163
181
  show_tangram_decomposition: trial.show_tangram_decomposition,
164
182
  instructions: trial.instructions,
165
183
  onInteraction: trial.onInteraction,
166
- onTrialEnd: wrappedOnTrialEnd
184
+ onTrialEnd: wrappedOnTrialEnd,
185
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
186
+ usePrimitiveColorsTargets: trial.use_primitive_colors_targets,
187
+ primitiveColorIndices: trial.primitive_color_indices
167
188
  };
168
189
 
169
190
  // Use React wrapper to start the trial
@@ -19,6 +19,8 @@ export interface StartNBackTrialParams {
19
19
  instructions?: string;
20
20
  button_text?: string;
21
21
  duration: number;
22
+ usePrimitiveColors?: boolean;
23
+ primitiveColorIndices?: number[];
22
24
  onTrialEnd?: (data: any) => void;
23
25
  }
24
26
 
@@ -49,6 +51,8 @@ function NBackView({ params }: NBackViewProps) {
49
51
  instructions,
50
52
  button_text,
51
53
  duration,
54
+ usePrimitiveColors,
55
+ primitiveColorIndices,
52
56
  onTrialEnd
53
57
  } = params;
54
58
 
@@ -135,7 +139,12 @@ function NBackView({ params }: NBackViewProps) {
135
139
  ...data,
136
140
  accuracy,
137
141
  tangram_id: tangram.tangramID,
138
- is_match: isMatch
142
+ is_match: isMatch,
143
+ show_tangram_decomposition: show_tangram_decomposition,
144
+ use_primitive_colors: usePrimitiveColors,
145
+ primitive_color_indices: primitiveColorIndices,
146
+ duration: duration,
147
+ button_text: button_text
139
148
  };
140
149
 
141
150
  if (onTrialEnd) {
@@ -208,25 +217,50 @@ function NBackView({ params }: NBackViewProps) {
208
217
 
209
218
  return (
210
219
  <g key="sil-decomposed" pointerEvents="none">
211
- {placedPolys.map((scaledPoly, i) => (
212
- <React.Fragment key={`prim-${i}`}>
213
- {/* Fill path */}
214
- <path
215
- d={pathD(scaledPoly)}
216
- fill={CONFIG.color.silhouetteMask}
217
- opacity={CONFIG.opacity.silhouetteMask}
218
- stroke="none"
219
- />
220
+ {placedPolys.map((scaledPoly, i) => {
221
+ // Get color for this primitive based on its kind
222
+ const primInfo = primitiveDecomposition[i];
223
+ let fillColor = CONFIG.color.silhouetteMask;
220
224
 
221
- {/* Full perimeter border */}
222
- <path
223
- d={pathD(scaledPoly)}
224
- fill="none"
225
- stroke={CONFIG.color.tangramDecomposition.stroke}
226
- strokeWidth={CONFIG.size.stroke.tangramDecompositionPx}
227
- />
228
- </React.Fragment>
229
- ))}
225
+ if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {
226
+ // Map primitive kind to index
227
+ const kindToIndex: Record<TanKind, number> = {
228
+ 'square': 0,
229
+ 'smalltriangle': 1,
230
+ 'parallelogram': 2,
231
+ 'medtriangle': 3,
232
+ 'largetriangle': 4
233
+ };
234
+ const primitiveIndex = kindToIndex[primInfo.kind as TanKind];
235
+ if (primitiveIndex !== undefined && primitiveColorIndices[primitiveIndex] !== undefined) {
236
+ const colorIndex = primitiveColorIndices[primitiveIndex];
237
+ const color = CONFIG.color.primitiveColors[colorIndex];
238
+ if (color) {
239
+ fillColor = color;
240
+ }
241
+ }
242
+ }
243
+
244
+ return (
245
+ <React.Fragment key={`prim-${i}`}>
246
+ {/* Fill path */}
247
+ <path
248
+ d={pathD(scaledPoly)}
249
+ fill={fillColor}
250
+ opacity={CONFIG.opacity.silhouetteMask}
251
+ stroke="none"
252
+ />
253
+
254
+ {/* Full perimeter border */}
255
+ <path
256
+ d={pathD(scaledPoly)}
257
+ fill="none"
258
+ stroke={CONFIG.color.tangramDecomposition.stroke}
259
+ strokeWidth={CONFIG.size.stroke.tangramDecompositionPx}
260
+ />
261
+ </React.Fragment>
262
+ );
263
+ })}
230
264
  </g>
231
265
  );
232
266
  } else {
@@ -235,15 +269,40 @@ function NBackView({ params }: NBackViewProps) {
235
269
 
236
270
  return (
237
271
  <g key="sil-unified" pointerEvents="none">
238
- {placedPolys.map((scaledPoly, i) => (
239
- <path
240
- key={`sil-${i}`}
241
- d={pathD(scaledPoly)}
242
- fill={CONFIG.color.silhouetteMask}
243
- opacity={CONFIG.opacity.silhouetteMask}
244
- stroke="none"
245
- />
246
- ))}
272
+ {placedPolys.map((scaledPoly, i) => {
273
+ // Get color for this primitive based on its kind
274
+ const primInfo = primitiveDecomposition[i];
275
+ let fillColor = CONFIG.color.silhouetteMask;
276
+
277
+ if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {
278
+ // Map primitive kind to index
279
+ const kindToIndex: Record<TanKind, number> = {
280
+ 'square': 0,
281
+ 'smalltriangle': 1,
282
+ 'parallelogram': 2,
283
+ 'medtriangle': 3,
284
+ 'largetriangle': 4
285
+ };
286
+ const primitiveIndex = kindToIndex[primInfo.kind as TanKind];
287
+ if (primitiveIndex !== undefined && primitiveColorIndices[primitiveIndex] !== undefined) {
288
+ const colorIndex = primitiveColorIndices[primitiveIndex];
289
+ const color = CONFIG.color.primitiveColors[colorIndex];
290
+ if (color) {
291
+ fillColor = color;
292
+ }
293
+ }
294
+ }
295
+
296
+ return (
297
+ <path
298
+ key={`sil-${i}`}
299
+ d={pathD(scaledPoly)}
300
+ fill={fillColor}
301
+ opacity={CONFIG.opacity.silhouetteMask}
302
+ stroke="none"
303
+ />
304
+ );
305
+ })}
247
306
  </g>
248
307
  );
249
308
  }
@@ -41,6 +41,18 @@ const info = {
41
41
  default: 3000,
42
42
  description: "Duration in milliseconds to display tangram and accept responses"
43
43
  },
44
+ /** Whether to use distinct colors for each primitive shape type */
45
+ use_primitive_colors: {
46
+ type: ParameterType.BOOL,
47
+ default: false,
48
+ description: "Whether each primitive shape type should have its own distinct color in the displayed tangram"
49
+ },
50
+ /** Indices mapping primitives to colors from the color palette */
51
+ primitive_color_indices: {
52
+ type: ParameterType.OBJECT,
53
+ default: [0, 1, 2, 3, 4],
54
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
55
+ },
44
56
  /** Callback fired when trial ends */
45
57
  onTrialEnd: {
46
58
  type: ParameterType.FUNCTION,
@@ -127,6 +139,8 @@ class TangramNBackPlugin implements JsPsychPlugin<Info> {
127
139
  instructions: trial.instructions,
128
140
  button_text: trial.button_text,
129
141
  duration: trial.duration,
142
+ usePrimitiveColors: trial.use_primitive_colors,
143
+ primitiveColorIndices: trial.primitive_color_indices,
130
144
  onTrialEnd: wrappedOnTrialEnd
131
145
  };
132
146
 
@@ -36,6 +36,8 @@ export interface StartPrepTrialParams {
36
36
  instructions?: string;
37
37
  onInteraction?: (event: any) => void;
38
38
  onTrialEnd?: (data: any) => void;
39
+ usePrimitiveColorsBlueprints?: boolean;
40
+ primitiveColorIndices?: number[];
39
41
  }
40
42
 
41
43
  /**
@@ -58,6 +60,8 @@ export function startPrepTrial(
58
60
  primitiveOrder,
59
61
  onInteraction,
60
62
  onTrialEnd,
63
+ usePrimitiveColorsBlueprints,
64
+ primitiveColorIndices,
61
65
  } = params;
62
66
 
63
67
  // Extract non-callback params for trial data
@@ -188,7 +192,9 @@ export function startPrepTrial(
188
192
  ...(params.instructions && { instructions: params.instructions }),
189
193
  onControllerReady: handleControllerReady,
190
194
  ...(onInteraction && { onInteraction }),
191
- ...(onTrialEnd && { onTrialEnd })
195
+ ...(onTrialEnd && { onTrialEnd }),
196
+ ...(usePrimitiveColorsBlueprints !== undefined && { usePrimitiveColorsBlueprints }),
197
+ ...(primitiveColorIndices !== undefined && { primitiveColorIndices })
192
198
  }));
193
199
 
194
200
  return { root, display_element, jsPsych };
@@ -62,6 +62,18 @@ const info = {
62
62
  onTrialEnd: {
63
63
  type: ParameterType.FUNCTION,
64
64
  default: undefined
65
+ },
66
+ /** Whether to use distinct colors for each primitive shape type in blueprints */
67
+ use_primitive_colors_blueprints: {
68
+ type: ParameterType.BOOL,
69
+ default: false,
70
+ description: "Whether each primitive shape type should have its own distinct color in the blueprint dock area"
71
+ },
72
+ /** Indices mapping primitives to colors from the color palette */
73
+ primitive_color_indices: {
74
+ type: ParameterType.OBJECT,
75
+ default: [0, 1, 2, 3, 4],
76
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
65
77
  }
66
78
  },
67
79
  data: {
@@ -124,6 +136,8 @@ class TangramPrepPlugin implements JsPsychPlugin<Info> {
124
136
  instructions: trial.instructions,
125
137
  onInteraction: trial.onInteraction,
126
138
  onTrialEnd: wrappedOnTrialEnd,
139
+ usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
140
+ primitiveColorIndices: trial.primitive_color_indices,
127
141
  };
128
142
 
129
143
  const { root, display_element: element, jsPsych } = startPrepTrial(display_element, params, this.jsPsych);