p5-phone 1.9.0 → 1.9.1

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 (92) hide show
  1. package/README.md +71 -24
  2. package/dist/p5-phone.js +196 -6
  3. package/dist/p5-phone.min.js +1 -1
  4. package/examples/Phone Sensor Examples/microphone/01_mic_level/index.html +1 -1
  5. package/examples/Phone Sensor Examples/microphone/02_speech_recognition/index.html +1 -1
  6. package/examples/Phone Sensor Examples/movement/01_orientation_basic/index.html +1 -1
  7. package/examples/Phone Sensor Examples/movement/02_rotational_velocity/index.html +1 -1
  8. package/examples/Phone Sensor Examples/movement/03_acceleration/index.html +1 -1
  9. package/examples/Phone Sensor Examples/movement/04_device_shaken/index.html +23 -0
  10. package/examples/Phone Sensor Examples/movement/04_device_shaken/sketch.js +146 -0
  11. package/examples/Phone Sensor Examples/movement/05_device_moved/index.html +23 -0
  12. package/examples/Phone Sensor Examples/movement/05_device_moved/sketch.js +175 -0
  13. package/examples/Phone Sensor Examples/movement/06_device_orientation/index.html +23 -0
  14. package/examples/Phone Sensor Examples/movement/06_device_orientation/sketch.js +120 -0
  15. package/examples/Phone Sensor Examples/nfc/01_nfc_read/index.html +3 -3
  16. package/examples/Phone Sensor Examples/nfc/01_nfc_read/sketch.js +252 -32
  17. package/examples/Phone Sensor Examples/nfc/02_two_tag_effects/index.html +25 -0
  18. package/examples/Phone Sensor Examples/nfc/02_two_tag_effects/sketch.js +208 -0
  19. package/examples/Phone Sensor Examples/sound/01_dual_audio/index.html +1 -1
  20. package/examples/Phone Sensor Examples/sound/01_dual_audio/sketch.js +52 -54
  21. package/examples/Phone Sensor Examples/sound/02_volume_touches/index.html +1 -1
  22. package/examples/Phone Sensor Examples/sound/02_volume_touches/sketch.js +66 -67
  23. package/examples/Phone Sensor Examples/sound/03_motion_synth/index.html +24 -0
  24. package/examples/Phone Sensor Examples/sound/03_motion_synth/sketch.js +172 -0
  25. package/examples/Phone Sensor Examples/touch/01_touch_basic/index.html +1 -1
  26. package/examples/Phone Sensor Examples/touch/02_touch_zones/index.html +1 -1
  27. package/examples/Phone Sensor Examples/touch/03_touch_count/index.html +1 -1
  28. package/examples/Phone Sensor Examples/touch/04_touch_distance/index.html +1 -1
  29. package/examples/Phone Sensor Examples/touch/05_touch_angle/index.html +1 -1
  30. package/examples/Phone Sensor Examples/vibration/01_haptic_feedback/index.html +1 -1
  31. package/examples/Phone Sensor Examples/vibration/01_haptic_feedback/sketch.js +72 -74
  32. package/examples/Phone Sensor Examples - Minimal/microphone/01_mic_level/index.html +1 -1
  33. package/examples/Phone Sensor Examples - Minimal/movement/01_orientation_basic/index.html +1 -1
  34. package/examples/Phone Sensor Examples - Minimal/movement/02_rotational_velocity/index.html +1 -1
  35. package/examples/Phone Sensor Examples - Minimal/movement/03_acceleration/index.html +1 -1
  36. package/examples/Phone Sensor Examples - Minimal/movement/04_device_shaken/index.html +18 -0
  37. package/examples/Phone Sensor Examples - Minimal/movement/04_device_shaken/sketch.js +46 -0
  38. package/examples/Phone Sensor Examples - Minimal/movement/05_device_moved/index.html +18 -0
  39. package/examples/Phone Sensor Examples - Minimal/movement/05_device_moved/sketch.js +46 -0
  40. package/examples/Phone Sensor Examples - Minimal/movement/06_device_orientation/index.html +18 -0
  41. package/examples/Phone Sensor Examples - Minimal/movement/06_device_orientation/sketch.js +40 -0
  42. package/examples/Phone Sensor Examples - Minimal/sound/01_sound_basic/index.html +1 -1
  43. package/examples/Phone Sensor Examples - Minimal/sound/02_sound_amplitude/index.html +1 -1
  44. package/examples/Phone Sensor Examples - Minimal/sound/03_motion_synth/index.html +19 -0
  45. package/examples/Phone Sensor Examples - Minimal/sound/03_motion_synth/sketch.js +63 -0
  46. package/examples/Phone Sensor Examples - Minimal/touch/01_touch_basic/index.html +1 -1
  47. package/examples/Phone Sensor Examples - Minimal/touch/02_touch_zones/index.html +1 -1
  48. package/examples/Phone Sensor Examples - Minimal/touch/03_touch_count/index.html +1 -1
  49. package/examples/Phone Sensor Examples - Minimal/touch/04_touch_distance/index.html +1 -1
  50. package/examples/Phone Sensor Examples - Minimal/touch/05_touch_angle/index.html +1 -1
  51. package/examples/Phone Sensor Examples - Minimal/vibration/01_haptic_feedback/index.html +1 -1
  52. package/examples/Phone Sensor Examples - Minimal/vibration/01_haptic_feedback/sketch.js +45 -44
  53. package/examples/Phone and Gif/collision/index.html +1 -1
  54. package/examples/Phone and Gif/fetch/index.html +1 -1
  55. package/examples/Phone and Gif/fly/index.html +1 -1
  56. package/examples/Phone and Gif/roll/index.html +1 -1
  57. package/examples/UIStyles/canvas-style/sketch.js +13 -13
  58. package/examples/UIStyles/custom-element/sketch.js +19 -20
  59. package/examples/UXcompare/button-vs-movement/index.html +1 -1
  60. package/examples/UXcompare/button-vs-orientation/index.html +1 -1
  61. package/examples/UXcompare/button-vs-shake/index.html +1 -1
  62. package/examples/UXcompare/gyroscope-demo/index.html +1 -1
  63. package/examples/UXcompare/microphone-demo/index.html +1 -1
  64. package/examples/UXcompare/slider-vs-angle/index.html +1 -1
  65. package/examples/UXcompare/slider-vs-distance/index.html +1 -1
  66. package/examples/UXcompare/slider-vs-microphone/index.html +1 -1
  67. package/examples/UXcompare/slider-vs-touches/index.html +1 -1
  68. package/examples/UXcompare/sliders-vs-acceleration/index.html +1 -1
  69. package/examples/UXcompare/sliders-vs-rotation/index.html +1 -1
  70. package/examples/blankTemplate/index.html +1 -1
  71. package/examples/homepage/index.html +105 -6
  72. package/examples/homepage-v2/index.html +263 -0
  73. package/examples/homepage-v2/scripts/api-data.js +97 -0
  74. package/examples/homepage-v2/scripts/examples-data.js +463 -0
  75. package/examples/homepage-v2/scripts/navigation.js +45 -0
  76. package/examples/homepage-v2/scripts/render.js +214 -0
  77. package/examples/homepage-v2/styles/main.css +119 -0
  78. package/examples/ml5/Gaze_detector_class/GazeDetector.js +1 -1
  79. package/examples/ml5/Gaze_detector_class/README.md +1 -1
  80. package/examples/ml5/Gaze_detector_class/index.html +1 -1
  81. package/examples/ml5/PHONE_BodyPose_two_points/index.html +1 -1
  82. package/examples/ml5/PHONE_BodyPose_two_points/sketch.js +1 -1
  83. package/examples/ml5/PHONE_FaceMesh_two_points/index.html +1 -1
  84. package/examples/ml5/PHONE_FaceMesh_two_points/sketch.js +1 -1
  85. package/examples/ml5/PHONE_HandPose_two_points/index.html +1 -1
  86. package/examples/ml5/PHONE_HandPose_two_points/sketch.js +1 -1
  87. package/examples/ml5/PHONE_ObjectDetection/index.html +27 -0
  88. package/examples/ml5/PHONE_ObjectDetection/sketch.js +236 -0
  89. package/examples/ml5/THREE_FaceMesh_two_points/sketch.js +1 -1
  90. package/examples/ml5/THREE_HandPose_two_points/sketch.js +1 -1
  91. package/package.json +1 -1
  92. package/src/p5-phone.js +196 -6
package/README.md CHANGED
@@ -102,10 +102,10 @@ p5-phone automatically detects the p5.js version and adjusts its internal touch
102
102
 
103
103
  ```html
104
104
  <!-- Minified version (recommended) -->
105
- <script src="https://cdn.jsdelivr.net/npm/p5-phone@1.9.0/dist/p5-phone.min.js"></script>
105
+ <script src="https://cdn.jsdelivr.net/npm/p5-phone@1.9.1/dist/p5-phone.min.js"></script>
106
106
 
107
107
  <!-- Development version (larger, with comments) -->
108
- <!-- <script src="https://cdn.jsdelivr.net/npm/p5-phone@1.9.0/dist/p5-phone.js"></script> -->
108
+ <!-- <script src="https://cdn.jsdelivr.net/npm/p5-phone@1.9.1/dist/p5-phone.js"></script> -->
109
109
  ```
110
110
 
111
111
  ### Basic Setup
@@ -134,7 +134,7 @@ p5-phone automatically detects the p5.js version and adjusts its internal touch
134
134
  <!-- For p5.js 2.0: <script src="https://cdn.jsdelivr.net/npm/p5@2/lib/p5.min.js"></script> -->
135
135
 
136
136
  <!-- Load p5-phone library -->
137
- <script src="https://cdn.jsdelivr.net/npm/p5-phone@1.9.0/dist/p5-phone.min.js"></script>
137
+ <script src="https://cdn.jsdelivr.net/npm/p5-phone@1.9.1/dist/p5-phone.min.js"></script>
138
138
 
139
139
  </head>
140
140
  <body>
@@ -305,6 +305,8 @@ this.enableGyroTap('Tap to start');
305
305
  - `window.speechEnabled` - Boolean indicating if speech recognition is active
306
306
  - `window.vibrationEnabled` - Boolean indicating if vibration is available (Android only)
307
307
  - `window.nfcEnabled` - Boolean indicating if NFC scanning is active (Android only)
308
+ - `window.lastNfcSerialNumber` - Serial number string for the most recently read NFC tag
309
+ - `window.lastNfcAlias` - Alias string for the most recently read NFC tag, if one has been set
308
310
 
309
311
  **Usage:**
310
312
  ```javascript
@@ -337,11 +339,10 @@ function draw() {
337
339
  }
338
340
 
339
341
  // You can also use them for conditional UI
340
- function setup() {
341
- enableGyroTap('Tap to enable motion');
342
-
343
- // Show different instructions based on status
344
- if (!window.sensorsEnabled) {
342
+ function draw() {
343
+ if (window.sensorsEnabled) {
344
+ debug("Motion sensors enabled");
345
+ } else {
345
346
  debug("Motion sensors not yet enabled");
346
347
  }
347
348
  }
@@ -661,11 +662,23 @@ function gameOver() {
661
662
  - `enableNfcTap(message)` - Tap anywhere on screen to enable NFC scanning
662
663
  - `enableNfcButton(text)` - Creates a button with custom text to enable NFC
663
664
  - `stopNfc()` - Stop NFC scanning
665
+ - `setNfcTagAlias(serialNumber, alias)` - Give a tag ID a human-friendly name
666
+ - `getNfcTagAlias(serialNumber)` - Get a saved alias for a tag ID
667
+ - `isNfcTag(aliasOrSerialNumber)` - Check whether the most recently read tag matches an alias or ID
664
668
 
665
669
  **Status Variables:**
666
670
  - `window.nfcEnabled` - Boolean indicating if NFC scanning is active
671
+ - `window.nfcStatus` - String describing NFC startup/read status
672
+ - `window.nfcError` - String containing the latest NFC startup/read error
673
+ - `window.nfcTagAliases` - Object mapping tag IDs to aliases
667
674
  - `window.lastNfcMessage` - Object containing the most recently read tag's data
668
675
  - `window.lastNfcSerialNumber` - Serial number string of the most recently read tag
676
+ - `window.lastNfcAlias` - Alias string for the most recently read tag, if one has been set
677
+
678
+ **Two-step workflow:**
679
+
680
+ 1. Open the NFC identifier example, scan each physical tag, type an alias, then download the tag list.
681
+ 2. Paste the generated `setNfcTagAlias()` lines into your sketch and use `isNfcTag()` in conditionals.
669
682
 
670
683
  **User Callback:**
671
684
 
@@ -674,6 +687,7 @@ Define an `nfcRead()` function in your sketch to receive tag data when a tag is
674
687
  ```javascript
675
688
  function nfcRead(message, serialNumber) {
676
689
  // message.serialNumber — tag serial number
690
+ // message.alias — saved alias, or '' if unnamed
677
691
  // message.records — array of NDEF records, each with:
678
692
  // .recordType — 'text', 'url', 'mime', etc.
679
693
  // .data — decoded content (string for text/url, object for JSON, raw for others)
@@ -690,6 +704,11 @@ let tagText = 'No tag scanned yet';
690
704
  function setup() {
691
705
  createCanvas(windowWidth, windowHeight);
692
706
  lockGestures();
707
+
708
+ // Paste IDs from the NFC identifier example.
709
+ setNfcTagAlias('04:85:2a:1b:9f:61:80', 'paintbrush');
710
+ setNfcTagAlias('04:42:18:3c:9f:61:80', 'desk');
711
+
693
712
  enableNfcTap('Tap to enable NFC');
694
713
  }
695
714
 
@@ -698,16 +717,29 @@ function draw() {
698
717
  textAlign(CENTER, CENTER);
699
718
  textSize(20);
700
719
 
701
- if (!window.nfcEnabled) {
702
- text('NFC not active', width / 2, height / 2);
720
+ if (window.nfcEnabled) {
721
+ if (isNfcTag('paintbrush')) {
722
+ background(120, 220, 160);
723
+ text('Paintbrush tag read', width / 2, height / 2);
724
+ } else if (isNfcTag('desk')) {
725
+ background(140, 180, 240);
726
+ text('Desk tag read', width / 2, height / 2);
727
+ } else {
728
+ text(tagText, width / 2, height / 2);
729
+ text('Hold an NFC tag near your phone', width / 2, height / 2 + 40);
730
+ }
703
731
  } else {
704
- text(tagText, width / 2, height / 2);
705
- text('Hold an NFC tag near your phone', width / 2, height / 2 + 40);
732
+ text('NFC not active', width / 2, height / 2);
706
733
  }
707
734
  }
708
735
 
709
736
  function nfcRead(message, serialNumber) {
710
- tagText = 'Tag: ' + serialNumber;
737
+ tagText = 'Tag: ' + (message.alias || serialNumber);
738
+
739
+ if (isNfcTag('paintbrush', serialNumber)) {
740
+ // This runs once when the paintbrush tag is scanned.
741
+ }
742
+
711
743
  for (let record of message.records) {
712
744
  if (record.recordType === 'text' || record.recordType === 'url') {
713
745
  tagText += '\n' + record.data;
@@ -730,6 +762,7 @@ NFC tags contain NDEF records. The most common types are:
730
762
  **Best Practices:**
731
763
  - Always check `window.nfcEnabled` before relying on NFC features
732
764
  - Use the `nfcRead()` callback for real-time tag processing
765
+ - Use `setNfcTagAlias()` and `isNfcTag()` when a sketch should respond to named physical objects
733
766
  - Use `window.lastNfcMessage` in `draw()` for displaying the most recent tag
734
767
  - Test on Android devices with Chrome — NFC is not available on iOS or desktop browsers
735
768
  - Tags must be NDEF-formatted to be read by the Web NFC API
@@ -810,10 +843,10 @@ function draw() {
810
843
 
811
844
  ### PhoneCamera (ML5 Integration)
812
845
 
813
- **Purpose:** Simplified camera access optimized for ML5.js machine learning models (FaceMesh, HandPose, BodyPose, etc.). Handles camera initialization, coordinate mapping, mirroring, and display modes automatically.
846
+ **Purpose:** Simplified camera access optimized for ML5.js machine learning models (FaceMesh, HandPose, BodyPose, ObjectDetection, etc.). Handles camera initialization, coordinate mapping, mirroring, and display modes automatically.
814
847
 
815
848
  **Key Features:**
816
- - **Automatic Coordinate Mapping** - ML5 keypoints automatically mapped to canvas coordinates
849
+ - **Automatic Coordinate Mapping** - ML5 keypoints and bounding boxes automatically mapped to canvas coordinates
817
850
  - **Mirror Support** - Handles front camera mirroring for natural interaction
818
851
  - **Display Modes** - Multiple video sizing options (fitHeight, cover, contain, fixed)
819
852
  - **ML5 Optimized** - Direct integration with ML5 v1.x models
@@ -828,6 +861,8 @@ function draw() {
828
861
  | `cam.onReady(callback)` | Execute code when camera ready | Callback function |
829
862
  | `cam.mapKeypoint(keypoint)` | Map single ML5 keypoint to screen | ML5 keypoint object |
830
863
  | `cam.mapKeypoints(keypoints)` | Map array of ML5 keypoints | Array of ML5 keypoints |
864
+ | `cam.mapBox(box)` | Map single ML5 bounding box to screen | ML5 detection object with x, y, width, height |
865
+ | `cam.mapBoxes(boxes)` | Map array of ML5 bounding boxes | Array of ML5 detection objects |
831
866
 
832
867
  **Properties:**
833
868
 
@@ -861,7 +896,7 @@ function setup() {
861
896
  let options = {
862
897
  maxFaces: 1,
863
898
  refineLandmarks: false,
864
- flipHorizontal: false // cam.mapKeypoint() handles mirroring
899
+ flipped: false // cam.mapKeypoint() handles mirroring
865
900
  };
866
901
 
867
902
  facemesh = ml5.faceMesh(options, modelLoaded);
@@ -914,7 +949,7 @@ function draw() {
914
949
 
915
950
  **Coordinate Mapping:**
916
951
 
917
- The `mapKeypoint()` and `mapKeypoints()` functions automatically handle:
952
+ The `mapKeypoint()`, `mapKeypoints()`, `mapBox()`, and `mapBoxes()` functions automatically handle:
918
953
  - Video-to-canvas scaling
919
954
  - Mirror transformation (for front camera)
920
955
  - Offset positioning (for different display modes)
@@ -930,26 +965,34 @@ let hands = cam.mapKeypoints(hand.keypoints);
930
965
  hands.forEach(point => {
931
966
  circle(point.x, point.y, 5);
932
967
  });
968
+
969
+ // Object detection bounding box
970
+ let mappedObject = cam.mapBox(detection);
971
+ rect(mappedObject.x, mappedObject.y, mappedObject.width, mappedObject.height);
933
972
  ```
934
973
 
935
974
  **ML5 Model Examples:**
936
975
 
937
976
  ```javascript
938
977
  // FaceMesh (468 keypoints)
939
- let options = { maxFaces: 1, refineLandmarks: false, flipHorizontal: false };
978
+ let options = { maxFaces: 1, refineLandmarks: false, flipped: false };
940
979
  facemesh = ml5.faceMesh(options, modelLoaded);
941
980
 
942
981
  // HandPose (21 keypoints per hand)
943
- let options = { maxHands: 2, runtime: 'mediapipe', flipHorizontal: false };
982
+ let options = { maxHands: 2, runtime: 'mediapipe', flipped: false };
944
983
  handpose = ml5.handPose(options, modelLoaded);
945
984
 
946
985
  // BodyPose (33 keypoints with 3D)
947
986
  let options = { modelType: 'MULTIPOSE_LIGHTNING', flipped: false };
948
987
  bodypose = ml5.bodyPose('BlazePose', options, modelLoaded);
988
+
989
+ // ObjectDetection (bounding boxes)
990
+ objectDetector = await ml5.objectDetection('cocossd');
991
+ objectDetector.detectStart(cam.videoElement, gotDetections);
949
992
  ```
950
993
 
951
994
  **Important Notes:**
952
- - Always set `flipHorizontal: false` in ML5 options (PhoneCamera handles mirroring)
995
+ - Always set `flipped: false` in ML5 options when available (PhoneCamera handles mirroring)
953
996
  - Use `cam.videoElement` (native HTML video element) when passing to ML5's `detectStart()`
954
997
  - Check `cam.ready` before using video or drawing keypoints
955
998
  - Call `enableCameraTap()` to handle camera permissions automatically
@@ -1033,9 +1076,11 @@ function setup() {
1033
1076
  }
1034
1077
 
1035
1078
  function draw() {
1036
- if (!window.sensorsEnabled) return;
1037
1079
  background(220);
1038
- circle(width/2 + rotationY * 3, height/2 + rotationX * 3, 50);
1080
+
1081
+ if (window.sensorsEnabled) {
1082
+ circle(width/2 + rotationY * 3, height/2 + rotationX * 3, 50);
1083
+ }
1039
1084
  }
1040
1085
  ```
1041
1086
 
@@ -1095,9 +1140,11 @@ function setup() {
1095
1140
  }
1096
1141
 
1097
1142
  function draw() {
1098
- if (!window.sensorsEnabled) return;
1099
1143
  background(220);
1100
- circle(width/2 + rotationY * 3, height/2 + rotationX * 3, 50);
1144
+
1145
+ if (window.sensorsEnabled) {
1146
+ circle(width/2 + rotationY * 3, height/2 + rotationX * 3, 50);
1147
+ }
1101
1148
  }
1102
1149
  ```
1103
1150
 
package/dist/p5-phone.js CHANGED
@@ -88,8 +88,12 @@ window.gesturesLocked = false;
88
88
  window.vibrationEnabled = false;
89
89
  window.speechEnabled = false;
90
90
  window.nfcEnabled = false;
91
+ window.nfcError = '';
92
+ window.nfcStatus = 'idle';
93
+ window.nfcTagAliases = {};
91
94
  window.lastNfcMessage = null;
92
95
  window.lastNfcSerialNumber = null;
96
+ window.lastNfcAlias = '';
93
97
 
94
98
  // Internal state
95
99
  let _micInstance = null;
@@ -557,9 +561,80 @@ function stopNfc() {
557
561
  }
558
562
  _nfcReader = null;
559
563
  window.nfcEnabled = false;
564
+ window.nfcStatus = 'stopped';
560
565
  console.log('NFC scanning stopped');
561
566
  }
562
567
 
568
+ function _normalizeNfcText(value) {
569
+ return value == null ? '' : String(value).trim();
570
+ }
571
+
572
+ function _normalizeNfcTagId(serialNumber) {
573
+ return _normalizeNfcText(serialNumber).toLowerCase();
574
+ }
575
+
576
+ function _nfcTextMatches(firstValue, secondValue) {
577
+ const firstText = _normalizeNfcText(firstValue).toLowerCase();
578
+ const secondText = _normalizeNfcText(secondValue).toLowerCase();
579
+ return firstText !== '' && firstText === secondText;
580
+ }
581
+
582
+ /**
583
+ * Give an NFC tag a human-friendly alias.
584
+ * Pass an empty alias to remove the stored name for a tag.
585
+ */
586
+ function setNfcTagAlias(serialNumber, alias) {
587
+ const tagId = _normalizeNfcTagId(serialNumber);
588
+ const tagAlias = _normalizeNfcText(alias);
589
+
590
+ if (!tagId) {
591
+ console.warn('p5-phone: setNfcTagAlias() needs an NFC serial number');
592
+ return '';
593
+ }
594
+
595
+ if (!tagAlias) {
596
+ delete window.nfcTagAliases[tagId];
597
+ } else {
598
+ window.nfcTagAliases[tagId] = tagAlias;
599
+ }
600
+
601
+ if (_normalizeNfcTagId(window.lastNfcSerialNumber) === tagId) {
602
+ window.lastNfcAlias = tagAlias;
603
+ if (window.lastNfcMessage) {
604
+ window.lastNfcMessage.alias = tagAlias;
605
+ }
606
+ }
607
+
608
+ return tagAlias;
609
+ }
610
+
611
+ /**
612
+ * Get the human-friendly alias for an NFC tag serial number.
613
+ */
614
+ function getNfcTagAlias(serialNumber = window.lastNfcSerialNumber) {
615
+ const tagId = _normalizeNfcTagId(serialNumber);
616
+ return tagId ? (window.nfcTagAliases[tagId] || '') : '';
617
+ }
618
+
619
+ /**
620
+ * Check whether the most recently read NFC tag matches an alias or serial number.
621
+ * Optionally pass a serial number from nfcRead(message, serialNumber) as the second argument.
622
+ */
623
+ function isNfcTag(aliasOrSerialNumber, serialNumber = window.lastNfcSerialNumber) {
624
+ const tagId = _normalizeNfcTagId(serialNumber);
625
+ const targetText = _normalizeNfcText(aliasOrSerialNumber);
626
+
627
+ if (!tagId || !targetText) {
628
+ return false;
629
+ }
630
+
631
+ if (tagId === _normalizeNfcTagId(targetText)) {
632
+ return true;
633
+ }
634
+
635
+ return _nfcTextMatches(getNfcTagAlias(serialNumber), targetText);
636
+ }
637
+
563
638
  // =========================================
564
639
  // INTERNAL PERMISSION HANDLERS
565
640
  // =========================================
@@ -687,6 +762,13 @@ async function _requestVibrationPermissionCore() {
687
762
 
688
763
  async function _requestNfcPermissionCore() {
689
764
  try {
765
+ if (window.nfcEnabled && _nfcReader) {
766
+ return true;
767
+ }
768
+
769
+ window.nfcError = '';
770
+ window.nfcStatus = 'starting';
771
+
690
772
  // Check if Web NFC API is supported
691
773
  if (!('NDEFReader' in window)) {
692
774
  console.warn('⚠️ Web NFC API not supported on this device/browser (Android Chrome 89+ required)');
@@ -694,9 +776,14 @@ async function _requestNfcPermissionCore() {
694
776
  debugWarn('Web NFC not supported on this device/browser');
695
777
  }
696
778
  window.nfcEnabled = false;
697
- return;
779
+ window.nfcStatus = 'unsupported';
780
+ window.nfcError = window.isSecureContext === false
781
+ ? 'NFC requires HTTPS. Serve this sketch from an HTTPS URL, not plain HTTP.'
782
+ : 'Web NFC is not supported in this browser. Use Android Chrome 89+ over HTTPS.';
783
+ return false;
698
784
  }
699
785
 
786
+ window.nfcStatus = 'requesting-permission';
700
787
  _nfcAbortController = new AbortController();
701
788
  _nfcReader = new NDEFReader();
702
789
 
@@ -734,9 +821,13 @@ async function _requestNfcPermissionCore() {
734
821
  records.push(entry);
735
822
  }
736
823
 
737
- const message = { serialNumber: serialNumber, records: records };
824
+ const alias = getNfcTagAlias(serialNumber);
825
+ const message = { serialNumber: serialNumber, alias: alias, records: records };
738
826
  window.lastNfcMessage = message;
739
827
  window.lastNfcSerialNumber = serialNumber;
828
+ window.lastNfcAlias = alias;
829
+ window.nfcStatus = 'tag-read';
830
+ window.nfcError = '';
740
831
 
741
832
  // Call user-defined callback if it exists
742
833
  if (typeof nfcRead === 'function') {
@@ -751,6 +842,7 @@ async function _requestNfcPermissionCore() {
751
842
 
752
843
  _nfcReader.onreadingerror = (event) => {
753
844
  console.warn('⚠️ NFC read error — tag may be incompatible or out of range');
845
+ window.nfcError = 'NFC read error. Make sure the tag is NDEF formatted and hold it near the phone NFC antenna.';
754
846
  if (_debugVisible) {
755
847
  debugWarn('NFC read error — tag incompatible or out of range');
756
848
  }
@@ -758,26 +850,44 @@ async function _requestNfcPermissionCore() {
758
850
 
759
851
  await _nfcReader.scan({ signal: _nfcAbortController.signal });
760
852
  window.nfcEnabled = true;
853
+ window.nfcStatus = 'scanning';
761
854
  console.log('✅ NFC scanning active');
855
+ return true;
762
856
 
763
857
  } catch (error) {
764
858
  if (error.name === 'NotAllowedError') {
765
859
  console.warn('⚠️ NFC permission denied by user');
860
+ window.nfcStatus = 'permission-denied';
861
+ window.nfcError = 'NFC permission was denied. Reload and tap Allow if Chrome asks.';
766
862
  if (_debugVisible) {
767
863
  debugWarn('NFC permission denied');
768
864
  }
769
865
  } else if (error.name === 'NotSupportedError') {
770
866
  console.warn('⚠️ NFC not supported on this device');
867
+ window.nfcStatus = 'unsupported';
868
+ window.nfcError = 'NFC is not supported on this device/browser, or this page is not using HTTPS.';
771
869
  if (_debugVisible) {
772
870
  debugWarn('NFC not supported on this device');
773
871
  }
872
+ } else if (error.name === 'SecurityError') {
873
+ console.warn('⚠️ NFC requires a secure HTTPS context');
874
+ window.nfcStatus = 'secure-context-required';
875
+ window.nfcError = 'NFC requires HTTPS. Serve this sketch from an HTTPS URL, not plain HTTP.';
876
+ if (_debugVisible) {
877
+ debugWarn('NFC requires HTTPS');
878
+ }
774
879
  } else {
775
880
  console.error('NFC permission error:', error);
881
+ window.nfcStatus = 'error';
882
+ window.nfcError = error && error.message ? error.message : 'NFC could not start.';
776
883
  if (_debugVisible) {
777
884
  debugError('NFC error: ' + error.message);
778
885
  }
779
886
  }
780
887
  window.nfcEnabled = false;
888
+ _nfcReader = null;
889
+ _nfcAbortController = null;
890
+ return false;
781
891
  }
782
892
  }
783
893
 
@@ -808,8 +918,9 @@ async function _requestVibrationPermission() {
808
918
  }
809
919
 
810
920
  async function _requestNfcPermission() {
811
- await _requestNfcPermissionCore();
921
+ const enabled = await _requestNfcPermissionCore();
812
922
  _notifySketchReady();
923
+ return enabled;
813
924
  }
814
925
 
815
926
  function _notifySketchReady() {
@@ -839,6 +950,7 @@ function _notifySketchReady() {
839
950
  function _createPermissionButton(buttonText, statusText, onClickHandler) {
840
951
  // Remove existing button if present
841
952
  _removeExistingUI();
953
+ let activating = false;
842
954
 
843
955
  // Create button
844
956
  const button = document.createElement('button');
@@ -891,7 +1003,8 @@ function _createPermissionButton(buttonText, statusText, onClickHandler) {
891
1003
 
892
1004
  // Add multiple event handlers to ensure responsiveness
893
1005
  const handleButtonClick = async () => {
894
- if (button.parentNode) {
1006
+ if (!activating && button.parentNode) {
1007
+ activating = true;
895
1008
  button.style.display = 'none';
896
1009
  status.style.display = 'block';
897
1010
 
@@ -922,6 +1035,7 @@ function _createPermissionButton(buttonText, statusText, onClickHandler) {
922
1035
  function _createTapToEnable(message, onTapHandler) {
923
1036
  // Remove existing UI if present
924
1037
  _removeExistingUI();
1038
+ let activating = false;
925
1039
 
926
1040
  // Create overlay
927
1041
  const overlay = document.createElement('div');
@@ -960,7 +1074,8 @@ function _createTapToEnable(message, onTapHandler) {
960
1074
 
961
1075
  // Add multiple event handlers to ensure responsiveness
962
1076
  const handleActivation = async () => {
963
- if (overlay.parentNode) {
1077
+ if (!activating && overlay.parentNode) {
1078
+ activating = true;
964
1079
  messageDiv.textContent = 'Enabling...';
965
1080
  await onTapHandler();
966
1081
  if (overlay.parentNode) {
@@ -1074,6 +1189,7 @@ function _createCanvasToEnable(message, onActivateHandler) {
1074
1189
  */
1075
1190
  function _createBannerToEnable(message, position, onActivateHandler) {
1076
1191
  _removeExistingUI();
1192
+ let activating = false;
1077
1193
 
1078
1194
  const banner = document.createElement('div');
1079
1195
  banner.id = 'permissionBanner';
@@ -1110,7 +1226,8 @@ function _createBannerToEnable(message, position, onActivateHandler) {
1110
1226
  });
1111
1227
 
1112
1228
  const handleActivation = async () => {
1113
- if (!banner.parentNode) return;
1229
+ if (activating || !banner.parentNode) return;
1230
+ activating = true;
1114
1231
 
1115
1232
  banner.textContent = 'Enabling...';
1116
1233
  banner.style.pointerEvents = 'none';
@@ -1574,6 +1691,9 @@ window.stopVibration = stopVibration;
1574
1691
  window.enableNfcTap = enableNfcTap;
1575
1692
  window.enableNfcButton = enableNfcButton;
1576
1693
  window.stopNfc = stopNfc;
1694
+ window.setNfcTagAlias = setNfcTagAlias;
1695
+ window.getNfcTagAlias = getNfcTagAlias;
1696
+ window.isNfcTag = isNfcTag;
1577
1697
  window.enableAllTap = enableAllTap;
1578
1698
  window.enableAllButton = enableAllButton;
1579
1699
 
@@ -2177,6 +2297,70 @@ class PhoneCamera {
2177
2297
 
2178
2298
  return keypoints.map(kp => this.mapKeypoint(kp));
2179
2299
  }
2300
+
2301
+ /**
2302
+ * Map an ML5 bounding box object to display coordinates
2303
+ * Handles scaling and mirroring automatically
2304
+ * Preserves labels, confidence, and any other detection properties
2305
+ * @param {object} box - ML5 box/detection { x, y, width, height, ... }
2306
+ * @returns {object} - Box with mapped x, y, width, and height
2307
+ */
2308
+ mapBox(box) {
2309
+ if (!box) {
2310
+ console.warn('PhoneCamera.mapBox: invalid box', box);
2311
+ return box;
2312
+ }
2313
+
2314
+ const boxX = typeof box.x !== 'undefined' ? box.x : box.xMin;
2315
+ const boxY = typeof box.y !== 'undefined' ? box.y : box.yMin;
2316
+ const boxWidth = typeof box.width !== 'undefined' ? box.width : box.xMax - box.xMin;
2317
+ const boxHeight = typeof box.height !== 'undefined' ? box.height : box.yMax - box.yMin;
2318
+ const numericX = Number(boxX);
2319
+ const numericY = Number(boxY);
2320
+ const numericWidth = Number(boxWidth);
2321
+ const numericHeight = Number(boxHeight);
2322
+
2323
+ if (!Number.isFinite(numericX) ||
2324
+ !Number.isFinite(numericY) ||
2325
+ !Number.isFinite(numericWidth) ||
2326
+ !Number.isFinite(numericHeight)) {
2327
+ console.warn('PhoneCamera.mapBox: invalid box', box);
2328
+ return box;
2329
+ }
2330
+
2331
+ const topLeft = this.mapPoint(numericX, numericY);
2332
+ const bottomRight = this.mapPoint(numericX + numericWidth, numericY + numericHeight);
2333
+ const mappedX = Math.min(topLeft.x, bottomRight.x);
2334
+ const mappedY = Math.min(topLeft.y, bottomRight.y);
2335
+ const mappedWidth = Math.abs(bottomRight.x - topLeft.x);
2336
+ const mappedHeight = Math.abs(bottomRight.y - topLeft.y);
2337
+
2338
+ return {
2339
+ ...box,
2340
+ x: mappedX,
2341
+ y: mappedY,
2342
+ width: mappedWidth,
2343
+ height: mappedHeight,
2344
+ xMin: mappedX,
2345
+ yMin: mappedY,
2346
+ xMax: mappedX + mappedWidth,
2347
+ yMax: mappedY + mappedHeight
2348
+ };
2349
+ }
2350
+
2351
+ /**
2352
+ * Map an array of ML5 bounding boxes to display coordinates
2353
+ * @param {array} boxes - Array of ML5 boxes/detections
2354
+ * @returns {array} - Array of boxes with mapped coordinates
2355
+ */
2356
+ mapBoxes(boxes) {
2357
+ if (!Array.isArray(boxes)) {
2358
+ console.warn('PhoneCamera.mapBoxes: expected array, got', typeof boxes);
2359
+ return boxes;
2360
+ }
2361
+
2362
+ return boxes.map(box => this.mapBox(box));
2363
+ }
2180
2364
 
2181
2365
  // ========================================
2182
2366
  // CANVAS DRAWING INTEGRATION
@@ -2362,6 +2546,9 @@ if (typeof p5 !== 'undefined' && p5.prototype) {
2362
2546
  p5.prototype.enableNfcTap = enableNfcTap;
2363
2547
  p5.prototype.enableNfcButton = enableNfcButton;
2364
2548
  p5.prototype.stopNfc = stopNfc;
2549
+ p5.prototype.setNfcTagAlias = setNfcTagAlias;
2550
+ p5.prototype.getNfcTagAlias = getNfcTagAlias;
2551
+ p5.prototype.isNfcTag = isNfcTag;
2365
2552
  p5.prototype.enableAllTap = enableAllTap;
2366
2553
  p5.prototype.enableAllButton = enableAllButton;
2367
2554
 
@@ -2440,6 +2627,9 @@ if (typeof p5 !== 'undefined' && typeof p5.registerAddon === 'function') {
2440
2627
  fn.enableNfcTap = enableNfcTap;
2441
2628
  fn.enableNfcButton = enableNfcButton;
2442
2629
  fn.stopNfc = stopNfc;
2630
+ fn.setNfcTagAlias = setNfcTagAlias;
2631
+ fn.getNfcTagAlias = getNfcTagAlias;
2632
+ fn.isNfcTag = isNfcTag;
2443
2633
  fn.enableAllTap = enableAllTap;
2444
2634
  fn.enableAllButton = enableAllButton;
2445
2635