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.
- package/dist/construct/index.browser.js +187 -214
- package/dist/construct/index.browser.js.map +1 -1
- package/dist/construct/index.browser.min.js +13 -10
- package/dist/construct/index.browser.min.js.map +1 -1
- package/dist/construct/index.cjs +187 -214
- package/dist/construct/index.cjs.map +1 -1
- package/dist/construct/index.d.ts +24 -0
- package/dist/construct/index.js +187 -214
- package/dist/construct/index.js.map +1 -1
- package/dist/index.cjs +198 -215
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +36 -0
- package/dist/index.js +198 -215
- package/dist/index.js.map +1 -1
- package/dist/prep/index.browser.js +173 -213
- package/dist/prep/index.browser.js.map +1 -1
- package/dist/prep/index.browser.min.js +14 -11
- package/dist/prep/index.browser.min.js.map +1 -1
- package/dist/prep/index.cjs +173 -213
- package/dist/prep/index.cjs.map +1 -1
- package/dist/prep/index.d.ts +12 -0
- package/dist/prep/index.js +173 -213
- package/dist/prep/index.js.map +1 -1
- package/package.json +1 -1
- package/src/assets/README.md +6 -0
- package/src/assets/images.d.ts +19 -0
- package/src/assets/locked.png +0 -0
- package/src/assets/unlocked.png +0 -0
- package/src/core/components/board/BoardView.tsx +121 -39
- package/src/core/components/board/GameBoard.tsx +123 -7
- package/src/core/config/config.ts +8 -4
- package/src/core/domain/types.ts +1 -0
- package/src/core/io/InteractionTracker.ts +23 -24
- package/src/core/io/data-tracking.ts +6 -7
- package/src/plugins/tangram-construct/ConstructionApp.tsx +16 -1
- package/src/plugins/tangram-construct/index.ts +14 -0
- package/src/plugins/tangram-prep/PrepApp.tsx +6 -0
- package/src/plugins/tangram-prep/index.ts +7 -0
- package/tangram-construct.min.js +13 -10
- 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
|
-
|
|
127
|
-
gameId?: string
|
|
125
|
+
trialParams?: any
|
|
128
126
|
) {
|
|
129
127
|
this.controller = controller;
|
|
130
128
|
this.callbacks = callbacks;
|
|
131
|
-
this.
|
|
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
|
|
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
|
};
|