jspsych-tangram 0.0.14 → 0.0.16

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 (47) hide show
  1. package/dist/afc/index.browser.js +17751 -0
  2. package/dist/afc/index.browser.js.map +1 -0
  3. package/dist/afc/index.browser.min.js +42 -0
  4. package/dist/afc/index.browser.min.js.map +1 -0
  5. package/dist/afc/index.cjs +443 -0
  6. package/dist/afc/index.cjs.map +1 -0
  7. package/dist/afc/index.d.ts +169 -0
  8. package/dist/afc/index.js +441 -0
  9. package/dist/afc/index.js.map +1 -0
  10. package/dist/construct/index.browser.js +4538 -3890
  11. package/dist/construct/index.browser.js.map +1 -1
  12. package/dist/construct/index.browser.min.js +13 -13
  13. package/dist/construct/index.browser.min.js.map +1 -1
  14. package/dist/construct/index.cjs +4 -8
  15. package/dist/construct/index.cjs.map +1 -1
  16. package/dist/construct/index.js +4 -8
  17. package/dist/construct/index.js.map +1 -1
  18. package/dist/index.cjs +374 -16
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.ts +178 -12
  21. package/dist/index.js +374 -17
  22. package/dist/index.js.map +1 -1
  23. package/dist/nback/index.browser.js +4536 -3919
  24. package/dist/nback/index.browser.js.map +1 -1
  25. package/dist/nback/index.browser.min.js +12 -12
  26. package/dist/nback/index.browser.min.js.map +1 -1
  27. package/dist/nback/index.cjs +6 -41
  28. package/dist/nback/index.cjs.map +1 -1
  29. package/dist/nback/index.js +6 -41
  30. package/dist/nback/index.js.map +1 -1
  31. package/dist/prep/index.browser.js +4538 -3892
  32. package/dist/prep/index.browser.js.map +1 -1
  33. package/dist/prep/index.browser.min.js +13 -13
  34. package/dist/prep/index.browser.min.js.map +1 -1
  35. package/dist/prep/index.cjs +5 -11
  36. package/dist/prep/index.cjs.map +1 -1
  37. package/dist/prep/index.js +5 -11
  38. package/dist/prep/index.js.map +1 -1
  39. package/package.json +9 -3
  40. package/src/index.ts +2 -1
  41. package/src/plugins/tangram-afc/AFCApp.tsx +341 -0
  42. package/src/plugins/tangram-afc/index.ts +140 -0
  43. package/src/plugins/tangram-nback/NBackApp.tsx +2 -2
  44. package/tangram-afc.min.js +42 -0
  45. package/tangram-construct.min.js +13 -13
  46. package/tangram-nback.min.js +12 -12
  47. package/tangram-prep.min.js +13 -13
package/dist/index.cjs CHANGED
@@ -16,9 +16,8 @@ const CONFIG = {
16
16
  silhouetteMask: "#374151",
17
17
  anchors: { invalid: "#7dd3fc", valid: "#475569" },
18
18
  // validFill used here for placed composites
19
- piece: { draggingFill: "#8e7cc3", validFill: "#8e7cc3", invalidFill: "#d55c00", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
20
- ui: { light: "#60a5fa", dark: "#1d4ed8" },
21
- blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
19
+ piece: { draggingFill: "#8e7cc3", validFill: "#8e7cc3", invalidFill: "#d55c00", invalidStroke: "#dc2626", allGreenStroke: "#86efac"},
20
+ blueprint: { fill: "#374151", badgeFill: "#000000", labelFill: "#ffffff" },
22
21
  tangramDecomposition: { stroke: "#fef2cc" },
23
22
  primitiveColors: [
24
23
  // from seaborn "colorblind" palette, 6 colors, with red omitted
@@ -37,7 +36,7 @@ const CONFIG = {
37
36
  piece: { invalid: 1, dragging: 1, locked: 1, normal: 1 }
38
37
  },
39
38
  size: {
40
- stroke: { bandPx: 5, pieceSelectedPx: 5, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
39
+ stroke: { bandPx: 5, allGreenStrokePx: 10, tangramDecompositionPx: 1 },
41
40
  anchorRadiusPx: { valid: 1, invalid: 1 },
42
41
  badgeFontPx: 16,
43
42
  centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 },
@@ -58,10 +57,7 @@ const CONFIG = {
58
57
  },
59
58
  game: {
60
59
  snapRadiusPx: 15,
61
- showBorders: false,
62
- hideTouchingBorders: true,
63
- silhouettesBelowPieces: true
64
- }
60
+ showBorders: false}
65
61
  };
66
62
 
67
63
  function isComposite(bp) {
@@ -3770,7 +3766,7 @@ function startConstructionTrial(display_element, params, _jsPsych) {
3770
3766
  return { root, display_element, jsPsych: _jsPsych };
3771
3767
  }
3772
3768
 
3773
- const info$2 = {
3769
+ const info$3 = {
3774
3770
  name: "tangram-construct",
3775
3771
  version: "1.0.0",
3776
3772
  parameters: {
@@ -3902,7 +3898,7 @@ class TangramConstructPlugin {
3902
3898
  this.jsPsych = jsPsych;
3903
3899
  }
3904
3900
  static {
3905
- this.info = info$2;
3901
+ this.info = info$3;
3906
3902
  }
3907
3903
  /**
3908
3904
  * Launches the trial by invoking startConstructionTrial
@@ -4053,7 +4049,7 @@ function startPrepTrial(display_element, params, jsPsych) {
4053
4049
  return { root, display_element, jsPsych };
4054
4050
  }
4055
4051
 
4056
- const info$1 = {
4052
+ const info$2 = {
4057
4053
  name: "tangram-prep",
4058
4054
  version: "1.0.0",
4059
4055
  parameters: {
@@ -4141,7 +4137,7 @@ class TangramPrepPlugin {
4141
4137
  this.jsPsych = jsPsych;
4142
4138
  }
4143
4139
  static {
4144
- this.info = info$1;
4140
+ this.info = info$2;
4145
4141
  }
4146
4142
  /**
4147
4143
  * Launches the trial by invoking startPrepTrial
@@ -4333,7 +4329,7 @@ function NBackView({ params }) {
4333
4329
  {
4334
4330
  d: pathD(scaledPoly),
4335
4331
  fill: fillColor,
4336
- opacity: CONFIG.opacity.silhouetteMask,
4332
+ opacity: usePrimitiveColors ? CONFIG.opacity.piece.normal : CONFIG.opacity.silhouetteMask,
4337
4333
  stroke: "none"
4338
4334
  }
4339
4335
  ), /* @__PURE__ */ React.createElement(
@@ -4374,7 +4370,7 @@ function NBackView({ params }) {
4374
4370
  key: `sil-${i}`,
4375
4371
  d: pathD(scaledPoly),
4376
4372
  fill: fillColor,
4377
- opacity: CONFIG.opacity.silhouetteMask,
4373
+ opacity: usePrimitiveColors ? CONFIG.opacity.piece.normal : CONFIG.opacity.silhouetteMask,
4378
4374
  stroke: "none"
4379
4375
  }
4380
4376
  );
@@ -4437,7 +4433,7 @@ function NBackView({ params }) {
4437
4433
  )));
4438
4434
  }
4439
4435
 
4440
- const info = {
4436
+ const info$1 = {
4441
4437
  name: "tangram-nback",
4442
4438
  version: "1.0.0",
4443
4439
  parameters: {
@@ -4530,7 +4526,7 @@ class TangramNBackPlugin {
4530
4526
  this.jsPsych = jsPsych;
4531
4527
  }
4532
4528
  static {
4533
- this.info = info;
4529
+ this.info = info$1;
4534
4530
  }
4535
4531
  /**
4536
4532
  * Launches the trial by invoking startNBackTrial
@@ -4564,6 +4560,368 @@ class TangramNBackPlugin {
4564
4560
  }
4565
4561
  }
4566
4562
 
4563
+ function startAFCTrial(display_element, params, _jsPsych) {
4564
+ const root = client.createRoot(display_element);
4565
+ root.render(React.createElement(AFCView, { params }));
4566
+ return { root, display_element, jsPsych: _jsPsych };
4567
+ }
4568
+ function AFCView({ params }) {
4569
+ const {
4570
+ tangramLeft,
4571
+ tangramRight,
4572
+ instructions,
4573
+ buttonTextLeft,
4574
+ buttonTextRight,
4575
+ showTangramDecomposition,
4576
+ usePrimitiveColors,
4577
+ primitiveColorIndices,
4578
+ onTrialEnd
4579
+ } = params;
4580
+ const trialStartTime = React.useRef(Date.now());
4581
+ const [hasResponded, setHasResponded] = React.useState(false);
4582
+ const handleResponse = (choice) => {
4583
+ if (hasResponded) return;
4584
+ setHasResponded(true);
4585
+ const rt = Date.now() - trialStartTime.current;
4586
+ if (onTrialEnd) {
4587
+ onTrialEnd({
4588
+ rt,
4589
+ response: choice,
4590
+ show_tangram_decomposition: showTangramDecomposition,
4591
+ use_primitive_colors: usePrimitiveColors,
4592
+ primitive_color_indices: primitiveColorIndices,
4593
+ button_text_left: buttonTextLeft,
4594
+ button_text_right: buttonTextRight
4595
+ });
4596
+ }
4597
+ };
4598
+ return /* @__PURE__ */ React.createElement("div", { style: {
4599
+ display: "flex",
4600
+ flexDirection: "column",
4601
+ alignItems: "center",
4602
+ justifyContent: "center",
4603
+ padding: "20px",
4604
+ background: "#fff7e0ff",
4605
+ minHeight: "100vh"
4606
+ } }, instructions && /* @__PURE__ */ React.createElement(
4607
+ "div",
4608
+ {
4609
+ style: {
4610
+ maxWidth: "800px",
4611
+ width: "100%",
4612
+ marginBottom: "30px",
4613
+ textAlign: "center",
4614
+ fontSize: "18px",
4615
+ lineHeight: "1.5"
4616
+ },
4617
+ dangerouslySetInnerHTML: { __html: instructions }
4618
+ }
4619
+ ), /* @__PURE__ */ React.createElement("div", { style: {
4620
+ display: "flex",
4621
+ flexDirection: "row",
4622
+ gap: "50px",
4623
+ justifyContent: "center",
4624
+ alignItems: "flex-start",
4625
+ flexWrap: "wrap"
4626
+ } }, /* @__PURE__ */ React.createElement(
4627
+ TangramOption,
4628
+ {
4629
+ tangram: tangramLeft,
4630
+ buttonText: buttonTextLeft,
4631
+ onClick: () => handleResponse("left"),
4632
+ disabled: hasResponded,
4633
+ showDecomposition: showTangramDecomposition,
4634
+ usePrimitiveColors,
4635
+ primitiveColorIndices
4636
+ }
4637
+ ), /* @__PURE__ */ React.createElement(
4638
+ TangramOption,
4639
+ {
4640
+ tangram: tangramRight,
4641
+ buttonText: buttonTextRight,
4642
+ onClick: () => handleResponse("right"),
4643
+ disabled: hasResponded,
4644
+ showDecomposition: showTangramDecomposition,
4645
+ usePrimitiveColors,
4646
+ primitiveColorIndices
4647
+ }
4648
+ )));
4649
+ }
4650
+ function TangramOption({
4651
+ tangram,
4652
+ buttonText,
4653
+ onClick,
4654
+ disabled,
4655
+ showDecomposition,
4656
+ usePrimitiveColors,
4657
+ primitiveColorIndices
4658
+ }) {
4659
+ if (!tangram) {
4660
+ return /* @__PURE__ */ React.createElement("div", { style: { width: 300, height: 300, background: "#eee" } }, "No Tangram Data");
4661
+ }
4662
+ const CANON = /* @__PURE__ */ new Set([
4663
+ "square",
4664
+ "smalltriangle",
4665
+ "parallelogram",
4666
+ "medtriangle",
4667
+ "largetriangle"
4668
+ ]);
4669
+ const filteredTans = tangram.solutionTans.filter((tan) => {
4670
+ const tanName = tan.name ?? tan.kind;
4671
+ return CANON.has(tanName);
4672
+ });
4673
+ const mask = filteredTans.map((tan) => {
4674
+ const polygon = tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }));
4675
+ return polygon;
4676
+ });
4677
+ const primitiveDecomposition = filteredTans.map((tan) => ({
4678
+ kind: tan.name ?? tan.kind,
4679
+ polygon: tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }))
4680
+ }));
4681
+ const DISPLAY_SIZE = 300;
4682
+ const viewport = {
4683
+ w: DISPLAY_SIZE,
4684
+ h: DISPLAY_SIZE
4685
+ };
4686
+ const scaleS = React.useMemo(() => {
4687
+ const u = inferUnitFromPolys$1(mask);
4688
+ return u ? CONFIG.layout.grid.unitPx / u : 1;
4689
+ }, [mask]);
4690
+ const centerPos = {
4691
+ cx: viewport.w / 2,
4692
+ cy: viewport.h / 2
4693
+ };
4694
+ const pathD = (poly) => {
4695
+ if (!poly || poly.length === 0) return "";
4696
+ const moves = poly.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`);
4697
+ return moves.join(" ") + " Z";
4698
+ };
4699
+ const renderSilhouette = () => {
4700
+ if (showDecomposition) {
4701
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
4702
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, centerPos);
4703
+ return /* @__PURE__ */ React.createElement("g", { key: "sil-decomposed", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
4704
+ const primInfo = primitiveDecomposition[i];
4705
+ let fillColor = CONFIG.color.silhouetteMask;
4706
+ if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {
4707
+ const kindToIndex = {
4708
+ "square": 0,
4709
+ "smalltriangle": 1,
4710
+ "parallelogram": 2,
4711
+ "medtriangle": 3,
4712
+ "largetriangle": 4
4713
+ };
4714
+ const primitiveIndex = kindToIndex[primInfo.kind];
4715
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
4716
+ const colorIndex = primitiveColorIndices[primitiveIndex];
4717
+ const color = CONFIG.color.primitiveColors[colorIndex];
4718
+ if (color) {
4719
+ fillColor = color;
4720
+ }
4721
+ }
4722
+ }
4723
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: `prim-${i}` }, /* @__PURE__ */ React.createElement(
4724
+ "path",
4725
+ {
4726
+ d: pathD(scaledPoly),
4727
+ fill: fillColor,
4728
+ opacity: usePrimitiveColors ? CONFIG.opacity.piece.normal : CONFIG.opacity.silhouetteMask,
4729
+ stroke: "none"
4730
+ }
4731
+ ), /* @__PURE__ */ React.createElement(
4732
+ "path",
4733
+ {
4734
+ d: pathD(scaledPoly),
4735
+ fill: "none",
4736
+ stroke: CONFIG.color.tangramDecomposition.stroke,
4737
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
4738
+ }
4739
+ ));
4740
+ }));
4741
+ } else {
4742
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(mask, scaleS, centerPos);
4743
+ return /* @__PURE__ */ React.createElement("g", { key: "sil-unified", pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => {
4744
+ const primInfo = primitiveDecomposition[i];
4745
+ let fillColor = CONFIG.color.silhouetteMask;
4746
+ if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {
4747
+ const kindToIndex = {
4748
+ "square": 0,
4749
+ "smalltriangle": 1,
4750
+ "parallelogram": 2,
4751
+ "medtriangle": 3,
4752
+ "largetriangle": 4
4753
+ };
4754
+ const primitiveIndex = kindToIndex[primInfo.kind];
4755
+ if (primitiveIndex !== void 0 && primitiveColorIndices[primitiveIndex] !== void 0) {
4756
+ const colorIndex = primitiveColorIndices[primitiveIndex];
4757
+ const color = CONFIG.color.primitiveColors[colorIndex];
4758
+ if (color) {
4759
+ fillColor = color;
4760
+ }
4761
+ }
4762
+ }
4763
+ return /* @__PURE__ */ React.createElement(
4764
+ "path",
4765
+ {
4766
+ key: `sil-${i}`,
4767
+ d: pathD(scaledPoly),
4768
+ fill: fillColor,
4769
+ opacity: usePrimitiveColors ? CONFIG.opacity.piece.normal : CONFIG.opacity.silhouetteMask,
4770
+ stroke: "none"
4771
+ }
4772
+ );
4773
+ }));
4774
+ }
4775
+ };
4776
+ return /* @__PURE__ */ React.createElement("div", { style: {
4777
+ display: "flex",
4778
+ flexDirection: "column",
4779
+ alignItems: "center",
4780
+ gap: "20px"
4781
+ } }, /* @__PURE__ */ React.createElement(
4782
+ "svg",
4783
+ {
4784
+ width: viewport.w,
4785
+ height: viewport.h,
4786
+ viewBox: `0 0 ${viewport.w} ${viewport.h}`,
4787
+ style: {
4788
+ display: "block",
4789
+ background: CONFIG.color.bands.silhouette.fillEven,
4790
+ border: `${CONFIG.size.stroke.bandPx}px solid ${CONFIG.color.bands.silhouette.stroke}`,
4791
+ borderRadius: "8px"
4792
+ }
4793
+ },
4794
+ renderSilhouette()
4795
+ ), /* @__PURE__ */ React.createElement(
4796
+ "button",
4797
+ {
4798
+ className: "jspsych-btn",
4799
+ onClick,
4800
+ disabled,
4801
+ style: {
4802
+ padding: "12px 30px",
4803
+ fontSize: "16px",
4804
+ cursor: disabled ? "not-allowed" : "pointer",
4805
+ opacity: disabled ? 0.5 : 1
4806
+ }
4807
+ },
4808
+ buttonText
4809
+ ));
4810
+ }
4811
+
4812
+ const info = {
4813
+ name: "tangram-afc",
4814
+ version: "1.0.0",
4815
+ parameters: {
4816
+ /** Left tangram specification to display */
4817
+ tangram_left: {
4818
+ type: jspsych.ParameterType.COMPLEX,
4819
+ default: void 0,
4820
+ description: "TangramSpec object defining left target shape to display"
4821
+ },
4822
+ /** Right tangram specification to display */
4823
+ tangram_right: {
4824
+ type: jspsych.ParameterType.COMPLEX,
4825
+ default: void 0,
4826
+ description: "TangramSpec object defining right target shape to display"
4827
+ },
4828
+ /** HTML content to display above the tangrams as instructions */
4829
+ instructions: {
4830
+ type: jspsych.ParameterType.STRING,
4831
+ default: "",
4832
+ description: "HTML content to display above the tangrams as instructions"
4833
+ },
4834
+ /** Text to display on left response button */
4835
+ button_text_left: {
4836
+ type: jspsych.ParameterType.STRING,
4837
+ default: "Select Left",
4838
+ description: "Text to display on left response button"
4839
+ },
4840
+ /** Text to display on right response button */
4841
+ button_text_right: {
4842
+ type: jspsych.ParameterType.STRING,
4843
+ default: "Select Right",
4844
+ description: "Text to display on right response button"
4845
+ },
4846
+ /** Whether to show tangram decomposed into individual primitives with borders */
4847
+ show_tangram_decomposition: {
4848
+ type: jspsych.ParameterType.BOOL,
4849
+ default: false,
4850
+ description: "Whether to show tangram decomposed into individual primitives with borders"
4851
+ },
4852
+ /** Whether to use distinct colors for each primitive shape type */
4853
+ use_primitive_colors: {
4854
+ type: jspsych.ParameterType.BOOL,
4855
+ default: false,
4856
+ description: "Whether each primitive shape type should have its own distinct color in the displayed tangram"
4857
+ },
4858
+ /** Indices mapping primitives to colors from the color palette */
4859
+ primitive_color_indices: {
4860
+ type: jspsych.ParameterType.OBJECT,
4861
+ default: [0, 1, 2, 3, 4],
4862
+ description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
4863
+ },
4864
+ /** Callback fired when trial ends */
4865
+ onTrialEnd: {
4866
+ type: jspsych.ParameterType.FUNCTION,
4867
+ default: void 0,
4868
+ description: "Callback when trial completes with full data"
4869
+ }
4870
+ },
4871
+ data: {
4872
+ /** Reaction time in milliseconds */
4873
+ rt: {
4874
+ type: jspsych.ParameterType.INT,
4875
+ description: "Milliseconds between trial start and button click"
4876
+ },
4877
+ /** Response choice */
4878
+ response: {
4879
+ type: jspsych.ParameterType.STRING,
4880
+ description: "Which button was clicked: 'left' or 'right'"
4881
+ }
4882
+ },
4883
+ citations: ""
4884
+ };
4885
+ class TangramAFCPlugin {
4886
+ constructor(jsPsych) {
4887
+ this.jsPsych = jsPsych;
4888
+ }
4889
+ static {
4890
+ this.info = info;
4891
+ }
4892
+ /**
4893
+ * Launches the trial by invoking startAFCTrial
4894
+ * with the display element, parameters, and jsPsych instance.
4895
+ */
4896
+ trial(display_element, trial) {
4897
+ const wrappedOnTrialEnd = (data) => {
4898
+ if (trial.onTrialEnd) {
4899
+ trial.onTrialEnd(data);
4900
+ }
4901
+ const reactContext = display_element.__reactContext;
4902
+ if (reactContext?.root) {
4903
+ reactContext.root.unmount();
4904
+ }
4905
+ display_element.innerHTML = "";
4906
+ this.jsPsych.finishTrial(data);
4907
+ };
4908
+ const params = {
4909
+ tangramLeft: trial.tangram_left,
4910
+ tangramRight: trial.tangram_right,
4911
+ instructions: trial.instructions,
4912
+ buttonTextLeft: trial.button_text_left,
4913
+ buttonTextRight: trial.button_text_right,
4914
+ showTangramDecomposition: trial.show_tangram_decomposition,
4915
+ usePrimitiveColors: trial.use_primitive_colors,
4916
+ primitiveColorIndices: trial.primitive_color_indices,
4917
+ onTrialEnd: wrappedOnTrialEnd
4918
+ };
4919
+ const { root, display_element: element, jsPsych } = startAFCTrial(display_element, params, this.jsPsych);
4920
+ element.__reactContext = { root, jsPsych };
4921
+ }
4922
+ }
4923
+
4924
+ exports.TangramAFCPlugin = TangramAFCPlugin;
4567
4925
  exports.TangramConstructPlugin = TangramConstructPlugin;
4568
4926
  exports.TangramNBackPlugin = TangramNBackPlugin;
4569
4927
  exports.TangramPrepPlugin = TangramPrepPlugin;