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