jspsych-tangram 0.0.7 → 0.0.9

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 (40) hide show
  1. package/dist/construct/index.browser.js +187 -214
  2. package/dist/construct/index.browser.js.map +1 -1
  3. package/dist/construct/index.browser.min.js +13 -10
  4. package/dist/construct/index.browser.min.js.map +1 -1
  5. package/dist/construct/index.cjs +187 -214
  6. package/dist/construct/index.cjs.map +1 -1
  7. package/dist/construct/index.d.ts +24 -0
  8. package/dist/construct/index.js +187 -214
  9. package/dist/construct/index.js.map +1 -1
  10. package/dist/index.cjs +198 -215
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +36 -0
  13. package/dist/index.js +198 -215
  14. package/dist/index.js.map +1 -1
  15. package/dist/prep/index.browser.js +173 -213
  16. package/dist/prep/index.browser.js.map +1 -1
  17. package/dist/prep/index.browser.min.js +14 -11
  18. package/dist/prep/index.browser.min.js.map +1 -1
  19. package/dist/prep/index.cjs +173 -213
  20. package/dist/prep/index.cjs.map +1 -1
  21. package/dist/prep/index.d.ts +12 -0
  22. package/dist/prep/index.js +173 -213
  23. package/dist/prep/index.js.map +1 -1
  24. package/package.json +1 -1
  25. package/src/assets/README.md +6 -0
  26. package/src/assets/images.d.ts +19 -0
  27. package/src/assets/locked.png +0 -0
  28. package/src/assets/unlocked.png +0 -0
  29. package/src/core/components/board/BoardView.tsx +121 -39
  30. package/src/core/components/board/GameBoard.tsx +123 -7
  31. package/src/core/config/config.ts +8 -4
  32. package/src/core/domain/types.ts +1 -0
  33. package/src/core/io/InteractionTracker.ts +23 -24
  34. package/src/core/io/data-tracking.ts +6 -7
  35. package/src/plugins/tangram-construct/ConstructionApp.tsx +16 -1
  36. package/src/plugins/tangram-construct/index.ts +14 -0
  37. package/src/plugins/tangram-prep/PrepApp.tsx +6 -0
  38. package/src/plugins/tangram-prep/index.ts +7 -0
  39. package/tangram-construct.min.js +13 -10
  40. package/tangram-prep.min.js +14 -11
@@ -88,16 +88,15 @@ interface MouseTrackingData {
88
88
  * InteractionTracker - Main tracking class
89
89
  */
90
90
  export class InteractionTracker {
91
- // IDs
92
- private gameId: string;
93
- private trialId: string;
94
-
95
91
  // Callbacks
96
92
  private callbacks: DataTrackingCallbacks;
97
93
 
98
94
  // Controller reference
99
95
  private controller: BaseGameController;
100
96
 
97
+ // Trial parameters
98
+ private trialParams: any;
99
+
101
100
  // Trial timing
102
101
  private trialStartTime: number;
103
102
  private readonly gridStep: number = CONFIG.layout.grid.stepPx;
@@ -123,13 +122,11 @@ export class InteractionTracker {
123
122
  constructor(
124
123
  controller: BaseGameController,
125
124
  callbacks: DataTrackingCallbacks,
126
- trialId?: string,
127
- gameId?: string
125
+ trialParams?: any
128
126
  ) {
129
127
  this.controller = controller;
130
128
  this.callbacks = callbacks;
131
- this.trialId = trialId || uuidv4();
132
- this.gameId = gameId || uuidv4();
129
+ this.trialParams = trialParams;
133
130
  this.trialStartTime = Date.now();
134
131
 
135
132
  // Register tracking callbacks with controller
@@ -211,8 +208,6 @@ export class InteractionTracker {
211
208
  const event: InteractionEvent = {
212
209
  // Metadata
213
210
  interactionId: uuidv4(),
214
- trialId: this.trialId,
215
- gameId: this.gameId,
216
211
  interactionIndex: this.interactionIndex++,
217
212
 
218
213
  // Interaction type
@@ -356,11 +351,17 @@ export class InteractionTracker {
356
351
  const totalDuration = trialEndTime - this.trialStartTime;
357
352
  const finalSnapshot = this.buildStateSnapshot();
358
353
 
354
+ // Calculate anchor to stimuli ratio (grid step in stimuli units)
355
+ // CONFIG.layout.grid.stepPx is the pixel size of one anchor unit
356
+ // CONFIG.layout.grid.unitPx is the pixel size of one stimuli unit (UNIT in tangram.py)
357
+ // So the ratio is stepPx / unitPx (typically 20 / 40 = 0.5)
358
+ const anchorToStimuliRatio = CONFIG.layout.grid.stepPx / CONFIG.layout.grid.unitPx;
359
+
359
360
  const mode = this.controller.state.cfg.mode;
360
361
 
361
362
  if (mode === 'construction') {
362
363
  const finalBlueprintState = this.buildFinalBlueprintState(finalSnapshot);
363
-
364
+
364
365
  // Extract composites as quickstash macros (ready for next trial input)
365
366
  const quickstashMacros = finalBlueprintState
366
367
  .filter(bp => bp.blueprintType === 'composite')
@@ -372,12 +373,11 @@ export class InteractionTracker {
372
373
 
373
374
  const data: ConstructionTrialData = {
374
375
  trialType: 'construction',
375
- trialId: this.trialId,
376
- gameId: this.gameId,
377
- trialNum: 0, // TODO: Plugin should provide this
378
376
  trialStartTime: this.trialStartTime,
379
377
  trialEndTime,
380
378
  totalDuration,
379
+ anchorToStimuliRatio,
380
+ trialParams: this.trialParams,
381
381
  endReason: endReason as 'timeout' | 'auto_complete',
382
382
  completionTimes: this.completionTimes,
383
383
  finalBlueprintState,
@@ -403,12 +403,11 @@ export class InteractionTracker {
403
403
 
404
404
  const data: PrepTrialData = {
405
405
  trialType: 'prep',
406
- trialId: this.trialId,
407
- gameId: this.gameId,
408
- trialNum: 0, // TODO: Plugin should provide this
409
406
  trialStartTime: this.trialStartTime,
410
407
  trialEndTime,
411
408
  totalDuration,
409
+ anchorToStimuliRatio,
410
+ trialParams: this.trialParams,
412
411
  endReason: 'submit',
413
412
  createdMacros: finalMacros,
414
413
  quickstashMacros,
@@ -458,7 +457,7 @@ export class InteractionTracker {
458
457
  const shapeOriginPositions = sec.pieces.map(piece => {
459
458
  const bp = this.controller.getBlueprint(piece.blueprintId);
460
459
  const kind = bp && 'kind' in bp ? bp.kind : piece.blueprintId;
461
-
460
+
462
461
  // Get the piece's actual position in pixels from controller state
463
462
  const actualPiece = this.controller.findPiece(piece.pieceId);
464
463
  if (!actualPiece || !bp) {
@@ -467,10 +466,10 @@ export class InteractionTracker {
467
466
  position: piece.position // Fallback to bbox position
468
467
  };
469
468
  }
470
-
469
+
471
470
  // Get bounding box of the blueprint using the geometry helper
472
471
  const bb = boundsOfBlueprint(bp, this.controller.getPrimitive);
473
-
472
+
474
473
  // Convert bbox position to shape origin position
475
474
  // In pixels: shapeOriginPos = piece.pos - bb.min
476
475
  // Then convert to anchor coordinates
@@ -479,7 +478,7 @@ export class InteractionTracker {
479
478
  y: actualPiece.pos.y - bb.min.y
480
479
  };
481
480
  const shapeOriginAnchor = this.toAnchorPoint(shapeOriginPixels);
482
-
481
+
483
482
  return {
484
483
  kind,
485
484
  position: shapeOriginAnchor // Shape origin in anchor coordinates
@@ -675,7 +674,7 @@ export class InteractionTracker {
675
674
  // Convert to array format with blueprint definitions
676
675
  return Array.from(counts.entries()).map(([blueprintId, data]) => {
677
676
  const blueprint = this.controller.getBlueprint(blueprintId);
678
-
677
+
679
678
  const result: {
680
679
  blueprintId: string;
681
680
  blueprintType: 'primitive' | 'composite';
@@ -708,7 +707,7 @@ export class InteractionTracker {
708
707
 
709
708
  // Convert pixel-based shape to anchor-based shape
710
709
  if (blueprint.shape) {
711
- result.shape = blueprint.shape.map(poly =>
710
+ result.shape = blueprint.shape.map(poly =>
712
711
  poly.map(vertex => ({
713
712
  x: Math.round(vertex.x / this.gridStep),
714
713
  y: Math.round(vertex.y / this.gridStep)
@@ -723,7 +722,7 @@ export class InteractionTracker {
723
722
  } else if (data.blueprintType === 'primitive' && 'kind' in blueprint) {
724
723
  // Primitive blueprint: convert pixel-based shape to anchor-based shape
725
724
  if (blueprint.shape) {
726
- result.shape = blueprint.shape.map(poly =>
725
+ result.shape = blueprint.shape.map(poly =>
727
726
  poly.map(vertex => ({
728
727
  x: Math.round(vertex.x / this.gridStep),
729
728
  y: Math.round(vertex.y / this.gridStep)
@@ -16,8 +16,6 @@
16
16
  export interface InteractionEvent {
17
17
  // === Event Metadata ===
18
18
  interactionId: string; // UUID for this interaction
19
- trialId: string; // UUID for the trial
20
- gameId: string; // UUID for the game session
21
19
  interactionIndex: number; // Sequential counter (0, 1, 2, ...) within the trial
22
20
 
23
21
  // === Interaction Type ===
@@ -118,16 +116,17 @@ export interface StateSnapshot {
118
116
  * Base trial data shared between construction and prep trials
119
117
  */
120
118
  export interface BaseTrialData {
121
- // Trial identifiers
122
- trialId: string; // UUID for this trial
123
- gameId: string; // UUID for game session
124
- trialNum: number; // Trial number in experiment sequence
125
-
126
119
  // Timing
127
120
  trialStartTime: number; // Absolute timestamp (Date.now())
128
121
  trialEndTime: number; // Absolute timestamp (Date.now())
129
122
  totalDuration: number; // duration in ms (trialEndTime - trialStartTime)
130
123
 
124
+ // Coordinate system metadata
125
+ anchorToStimuliRatio: number; // Ratio of anchor units to stimuli units (gridStepPx / unitPx)
126
+
127
+ // Parameters passed to the trial
128
+ trialParams: any; // Cleaned jsPsych plugin parameters (excluding callbacks)
129
+
131
130
  // Final state snapshot
132
131
  finalSnapshot: StateSnapshot;
133
132
  }
@@ -30,6 +30,8 @@ export interface StartConstructionTrialParams {
30
30
  input?: InputMode;
31
31
  layout?: LayoutMode;
32
32
  time_limit_ms: number;
33
+ show_tangram_decomposition?: boolean;
34
+ instructions?: string;
33
35
  onInteraction?: (event: any) => void;
34
36
  onTrialEnd?: (data: any) => void;
35
37
  }
@@ -72,11 +74,17 @@ export function startConstructionTrial(
72
74
  return isCanonical;
73
75
  });
74
76
 
75
- const mask = filteredTans.map((tan: any, tanIndex: number) => {
77
+ const mask = filteredTans.map((tan: any) => {
76
78
  const polygon = tan.vertices.map(([x, y]: number[]) => ({ x: x ?? 0, y: -(y ?? 0) }));
77
79
  return polygon;
78
80
  });
79
81
 
82
+ // Store primitive decomposition for optional decomposition view
83
+ const primitiveDecomposition = filteredTans.map((tan: any) => ({
84
+ kind: (tan.name ?? tan.kind) as TanKind,
85
+ polygon: tan.vertices.map(([x, y]: number[]) => ({ x: x ?? 0, y: -(y ?? 0) }))
86
+ }));
87
+
80
88
  // Assign sector ID from alphabetical sequence
81
89
  const sectorId = `sector${index}`;
82
90
 
@@ -86,6 +94,7 @@ export function startConstructionTrial(
86
94
  silhouette: {
87
95
  id: sectorId,
88
96
  mask,
97
+ primitiveDecomposition,
89
98
  },
90
99
  };
91
100
 
@@ -116,6 +125,9 @@ export function startConstructionTrial(
116
125
  } else {
117
126
  }
118
127
 
128
+ // Extract non-callback params for trial data
129
+ const { onInteraction, onTrialEnd, ...trialParams } = params;
130
+
119
131
  // Create React root and render GameBoard
120
132
  const gameBoardProps = {
121
133
  sectors,
@@ -127,6 +139,9 @@ export function startConstructionTrial(
127
139
  timeLimitMs: params.time_limit_ms,
128
140
  maxQuickstashSlots: CONFIG.layout.defaults.maxQuickstashSlots,
129
141
  mode: 'construction' as const,
142
+ showTangramDecomposition: params.show_tangram_decomposition ?? false,
143
+ trialParams,
144
+ ...(params.instructions && { instructions: params.instructions }),
130
145
  ...(params.onInteraction && { onInteraction: params.onInteraction }),
131
146
  ...(params.onTrialEnd && { onTrialEnd: params.onTrialEnd })
132
147
  };
@@ -56,6 +56,18 @@ const info = {
56
56
  default: 18,
57
57
  description: "Snap radius for anchor-based piece placement"
58
58
  },
59
+ /** Whether to show tangram target shapes decomposed into individual primitives with borders */
60
+ show_tangram_decomposition: {
61
+ type: ParameterType.BOOL,
62
+ default: false,
63
+ description: "Whether to show tangram target shapes decomposed into individual primitives with borders"
64
+ },
65
+ /** HTML content to display above the gameboard as instructions */
66
+ instructions: {
67
+ type: ParameterType.STRING,
68
+ default: "",
69
+ description: "HTML content to display above the gameboard as instructions"
70
+ },
59
71
  /** Callback fired after each interaction (piece pickup + placedown) */
60
72
  onInteraction: {
61
73
  type: ParameterType.FUNCTION,
@@ -148,6 +160,8 @@ class TangramConstructPlugin implements JsPsychPlugin<Info> {
148
160
  input: trial.input,
149
161
  layout: trial.layout,
150
162
  time_limit_ms: trial.time_limit_ms,
163
+ show_tangram_decomposition: trial.show_tangram_decomposition,
164
+ instructions: trial.instructions,
151
165
  onInteraction: trial.onInteraction,
152
166
  onTrialEnd: wrappedOnTrialEnd
153
167
  };
@@ -33,6 +33,7 @@ export interface StartPrepTrialParams {
33
33
  requireAllSlots: boolean;
34
34
  quickstashMacros?: AnchorComposite[];
35
35
  primitiveOrder: string[];
36
+ instructions?: string;
36
37
  onInteraction?: (event: any) => void;
37
38
  onTrialEnd?: (data: any) => void;
38
39
  }
@@ -59,6 +60,9 @@ export function startPrepTrial(
59
60
  onTrialEnd,
60
61
  } = params;
61
62
 
63
+ // Extract non-callback params for trial data
64
+ const { onInteraction: _, onTrialEnd: __, ...trialParams } = params;
65
+
62
66
  // make copy of PRIMITIVE_BLUEPRINTS sorted by primitiveOrder
63
67
  const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => primitiveOrder.indexOf(a.kind) - primitiveOrder.indexOf(b.kind));
64
68
 
@@ -180,6 +184,8 @@ export function startPrepTrial(
180
184
  mode: 'prep', // Enable prep-specific behavior
181
185
  minPiecesPerMacro: minPiecesPerMacro,
182
186
  requireAllSlots: requireAllSlots,
187
+ trialParams,
188
+ ...(params.instructions && { instructions: params.instructions }),
183
189
  onControllerReady: handleControllerReady,
184
190
  ...(onInteraction && { onInteraction }),
185
191
  ...(onTrialEnd && { onTrialEnd })
@@ -47,6 +47,12 @@ const info = {
47
47
  default: ["square", "smalltriangle", "parallelogram", "medtriangle", "largetriangle"],
48
48
  description: "Array of primitive names in the order they should be displayed"
49
49
  },
50
+ /** HTML content to display above the gameboard as instructions */
51
+ instructions: {
52
+ type: ParameterType.STRING,
53
+ default: undefined,
54
+ description: "HTML content to display above the gameboard as instructions"
55
+ },
50
56
  /** Callback fired after each interaction (optional analytics hook) */
51
57
  onInteraction: {
52
58
  type: ParameterType.FUNCTION,
@@ -115,6 +121,7 @@ class TangramPrepPlugin implements JsPsychPlugin<Info> {
115
121
  requireAllSlots: trial.require_all_slots,
116
122
  quickstashMacros: trial.quickstash_macros,
117
123
  primitiveOrder: trial.primitive_order,
124
+ instructions: trial.instructions,
118
125
  onInteraction: trial.onInteraction,
119
126
  onTrialEnd: wrappedOnTrialEnd,
120
127
  };