p5-phone 1.5.0 → 1.6.0

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 (67) hide show
  1. package/README.md +149 -3
  2. package/dist/p5-phone.js +525 -0
  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/movement/01_orientation_basic/index.html +1 -1
  6. package/examples/Phone Sensor Examples/movement/02_rotational_velocity/index.html +1 -1
  7. package/examples/Phone Sensor Examples/movement/03_acceleration/index.html +1 -1
  8. package/examples/Phone Sensor Examples/sound/01_dual_audio/index.html +1 -1
  9. package/examples/Phone Sensor Examples/sound/02_volume_touches/index.html +1 -1
  10. package/examples/Phone Sensor Examples/touch/01_touch_basic/index.html +1 -1
  11. package/examples/Phone Sensor Examples/touch/02_touch_zones/index.html +1 -1
  12. package/examples/Phone Sensor Examples/touch/03_touch_count/index.html +1 -1
  13. package/examples/Phone Sensor Examples/touch/04_touch_distance/index.html +1 -1
  14. package/examples/Phone Sensor Examples/touch/05_touch_angle/index.html +1 -1
  15. package/examples/Phone Sensor Examples/vibration/01_haptic_feedback/index.html +1 -1
  16. package/examples/Phone Sensor Examples - Minimal/microphone/01_mic_level/index.html +1 -1
  17. package/examples/Phone Sensor Examples - Minimal/movement/01_orientation_basic/index.html +1 -1
  18. package/examples/Phone Sensor Examples - Minimal/movement/02_rotational_velocity/index.html +1 -1
  19. package/examples/Phone Sensor Examples - Minimal/movement/03_acceleration/index.html +1 -1
  20. package/examples/Phone Sensor Examples - Minimal/sound/01_sound_basic/index.html +1 -1
  21. package/examples/Phone Sensor Examples - Minimal/sound/02_sound_amplitude/index.html +1 -1
  22. package/examples/Phone Sensor Examples - Minimal/touch/01_touch_basic/index.html +1 -1
  23. package/examples/Phone Sensor Examples - Minimal/touch/02_touch_zones/index.html +1 -1
  24. package/examples/Phone Sensor Examples - Minimal/touch/03_touch_count/index.html +1 -1
  25. package/examples/Phone Sensor Examples - Minimal/touch/04_touch_distance/index.html +1 -1
  26. package/examples/Phone Sensor Examples - Minimal/touch/05_touch_angle/index.html +1 -1
  27. package/examples/Phone Sensor Examples - Minimal/vibration/01_haptic_feedback/index.html +18 -0
  28. package/examples/Phone Sensor Examples - Minimal/vibration/01_haptic_feedback/sketch.js +87 -0
  29. package/examples/Phone and Gif/collision/index.html +1 -1
  30. package/examples/Phone and Gif/fetch/index.html +1 -1
  31. package/examples/Phone and Gif/fly/index.html +1 -1
  32. package/examples/Phone and Gif/roll/index.html +1 -1
  33. package/examples/UXcompare/button-vs-movement/index.html +1 -1
  34. package/examples/UXcompare/button-vs-orientation/index.html +1 -1
  35. package/examples/UXcompare/button-vs-shake/index.html +1 -1
  36. package/examples/UXcompare/gyroscope-demo/index.html +1 -1
  37. package/examples/UXcompare/microphone-demo/index.html +1 -1
  38. package/examples/UXcompare/slider-vs-angle/index.html +1 -1
  39. package/examples/UXcompare/slider-vs-distance/index.html +1 -1
  40. package/examples/UXcompare/slider-vs-microphone/index.html +1 -1
  41. package/examples/UXcompare/slider-vs-touches/index.html +1 -1
  42. package/examples/UXcompare/sliders-vs-acceleration/index.html +1 -1
  43. package/examples/UXcompare/sliders-vs-rotation/index.html +1 -1
  44. package/examples/blankTemplate/index.html +1 -1
  45. package/examples/homepage/index.html +24 -5
  46. package/examples/workArea/01_camera-selector/README.md +119 -0
  47. package/examples/workArea/01_camera-selector/index.html +28 -0
  48. package/examples/workArea/01_camera-selector/sketch.js +239 -0
  49. package/examples/workArea/03_facemesh-nose/index.html +34 -0
  50. package/examples/workArea/03_facemesh-nose/sketch.js +247 -0
  51. package/examples/workArea/03_facemesh-nose-preload/index.html +34 -0
  52. package/examples/workArea/03_facemesh-nose-preload/sketch.js +173 -0
  53. package/examples/workArea/04_facemesh-FINAL/README.md +85 -0
  54. package/examples/workArea/04_facemesh-FINAL/index.html +31 -0
  55. package/examples/workArea/04_facemesh-FINAL/sketch.js +240 -0
  56. package/examples/workArea/04_facemesh-simplified_temp/README.md +93 -0
  57. package/examples/workArea/04_facemesh-simplified_temp/index.html +31 -0
  58. package/examples/workArea/04_facemesh-simplified_temp/sketch.js +259 -0
  59. package/examples/workArea/05_handpose/extra.js +0 -0
  60. package/examples/workArea/05_handpose/index.html +31 -0
  61. package/examples/workArea/05_handpose/sketch.js +362 -0
  62. package/examples/workArea/05_handpose-preload/index.html +31 -0
  63. package/examples/workArea/05_handpose-preload/sketch.js +362 -0
  64. package/examples/workArea/06_bodypose-FINAL/index.html +31 -0
  65. package/examples/workArea/06_bodypose-FINAL/sketch.js +360 -0
  66. package/package.json +1 -1
  67. package/src/p5-phone.js +525 -0
package/README.md CHANGED
@@ -67,10 +67,10 @@ This library simplifies access to the following p5.js mobile sensor and audio co
67
67
 
68
68
  ```html
69
69
  <!-- Minified version (recommended) -->
70
- <script src="https://cdn.jsdelivr.net/npm/p5-phone@1.4.4/dist/p5-phone.min.js"></script>
70
+ <script src="https://cdn.jsdelivr.net/npm/p5-phone@1.5.0/dist/p5-phone.min.js"></script>
71
71
 
72
72
  <!-- Development version (larger, with comments) -->
73
- <!-- <script src="https://cdn.jsdelivr.net/npm/p5-phone@1.4.4/dist/p5-phone.js"></script> -->
73
+ <!-- <script src="https://cdn.jsdelivr.net/npm/p5-phone@1.5.0/dist/p5-phone.js"></script> -->
74
74
  ```
75
75
 
76
76
  ### Basic Setup
@@ -98,7 +98,7 @@ This library simplifies access to the following p5.js mobile sensor and audio co
98
98
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.10/p5.min.js"></script>
99
99
 
100
100
  <!-- Load p5-phone library -->
101
- <script src="https://cdn.jsdelivr.net/npm/p5-phone@1.4.4/dist/p5-phone.min.js"></script>
101
+ <script src="https://cdn.jsdelivr.net/npm/p5-phone@1.5.0/dist/p5-phone.min.js"></script>
102
102
 
103
103
  </head>
104
104
  <body>
@@ -570,6 +570,152 @@ function gameOver() {
570
570
  - Don't overuse - vibration can quickly drain battery
571
571
  - Test on Android devices as iOS doesn't support vibration
572
572
 
573
+ ### PhoneCamera (ML5 Integration)
574
+
575
+ **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.
576
+
577
+ **Key Features:**
578
+ - **Automatic Coordinate Mapping** - ML5 keypoints automatically mapped to canvas coordinates
579
+ - **Mirror Support** - Handles front camera mirroring for natural interaction
580
+ - **Display Modes** - Multiple video sizing options (fitHeight, cover, contain, fixed)
581
+ - **ML5 Optimized** - Direct integration with ML5 v1.x models
582
+ - **Auto-initialization** - Camera starts automatically when permissions are granted
583
+
584
+ **Commands:**
585
+
586
+ | Function | Purpose | Parameters |
587
+ |----------|---------|------------|
588
+ | `createPhoneCamera(active, mirror, mode)` | Create new camera instance | active: 'user' or 'environment'<br>mirror: true/false<br>mode: 'fitHeight', 'cover', 'contain', 'fixed' |
589
+ | `enableCameraTap(message)` | Tap to enable camera | Optional message string |
590
+ | `cam.onReady(callback)` | Execute code when camera ready | Callback function |
591
+ | `cam.mapKeypoint(keypoint)` | Map single ML5 keypoint to screen | ML5 keypoint object |
592
+ | `cam.mapKeypoints(keypoints)` | Map array of ML5 keypoints | Array of ML5 keypoints |
593
+
594
+ **Properties:**
595
+
596
+ | Property | Description | Type |
597
+ |----------|-------------|------|
598
+ | `cam.ready` | Camera initialization status | Boolean |
599
+ | `cam.video` | p5.js video element | p5.Element |
600
+ | `cam.active` | Current camera ('user'/'environment') | String |
601
+ | `cam.mirror` | Mirror state | Boolean |
602
+ | `cam.mode` | Display mode | String |
603
+ | `cam.width` | Video width | Number |
604
+ | `cam.height` | Video height | Number |
605
+
606
+ **Basic Setup:**
607
+ ```javascript
608
+ let cam;
609
+ let facemesh;
610
+ let faces = [];
611
+
612
+ function setup() {
613
+ createCanvas(windowWidth, windowHeight);
614
+
615
+ // Create camera: front camera, mirrored, fit to canvas height
616
+ cam = createPhoneCamera('user', true, 'fitHeight');
617
+
618
+ // Enable camera (auto-starts if permission granted)
619
+ enableCameraTap();
620
+
621
+ // Start ML5 when camera is ready
622
+ cam.onReady(() => {
623
+ let options = {
624
+ maxFaces: 1,
625
+ refineLandmarks: false,
626
+ flipHorizontal: false // cam.mapKeypoint() handles mirroring
627
+ };
628
+
629
+ facemesh = ml5.faceMesh(options, modelLoaded);
630
+ });
631
+ }
632
+
633
+ function modelLoaded() {
634
+ // Start detection - use cam.videoElement for ML5
635
+ facemesh.detectStart(cam.videoElement, (results) => {
636
+ faces = results;
637
+ });
638
+ }
639
+
640
+ function draw() {
641
+ background(220);
642
+
643
+ // Draw camera feed
644
+ if (cam.ready) {
645
+ image(cam, 0, 0); // PhoneCamera handles positioning automatically
646
+ }
647
+
648
+ // Draw tracked face keypoints
649
+ if (faces.length > 0) {
650
+ let face = faces[0];
651
+
652
+ // Map nose tip keypoint (index 1) to screen coordinates
653
+ let nose = cam.mapKeypoint(face.keypoints[1]);
654
+
655
+ // Use coordinates for interaction
656
+ fill(255, 0, 0);
657
+ circle(nose.x, nose.y, 30);
658
+
659
+ // Map all keypoints at once
660
+ let allPoints = cam.mapKeypoints(face.keypoints);
661
+ for (let point of allPoints) {
662
+ circle(point.x, point.y, 3);
663
+ }
664
+ }
665
+ }
666
+ ```
667
+
668
+ **Display Modes:**
669
+
670
+ | Mode | Behavior |
671
+ |------|----------|
672
+ | `'fitHeight'` | Scale video to canvas height (default, recommended) |
673
+ | `'cover'` | Fill entire canvas (may crop video) |
674
+ | `'contain'` | Fit entire video in canvas (may show letterboxing) |
675
+ | `'fixed'` | Fixed size (set with `cam.fixedWidth`, `cam.fixedHeight`) |
676
+
677
+ **Coordinate Mapping:**
678
+
679
+ The `mapKeypoint()` and `mapKeypoints()` functions automatically handle:
680
+ - Video-to-canvas scaling
681
+ - Mirror transformation (for front camera)
682
+ - Offset positioning (for different display modes)
683
+ - 3D coordinates (preserves z-depth from BlazePose)
684
+
685
+ ```javascript
686
+ // Single keypoint
687
+ let nose = cam.mapKeypoint(face.keypoints[1]);
688
+ console.log(nose.x, nose.y, nose.z); // Screen coordinates + depth
689
+
690
+ // Multiple keypoints
691
+ let hands = cam.mapKeypoints(hand.keypoints);
692
+ hands.forEach(point => {
693
+ circle(point.x, point.y, 5);
694
+ });
695
+ ```
696
+
697
+ **ML5 Model Examples:**
698
+
699
+ ```javascript
700
+ // FaceMesh (468 keypoints)
701
+ let options = { maxFaces: 1, refineLandmarks: false, flipHorizontal: false };
702
+ facemesh = ml5.faceMesh(options, modelLoaded);
703
+
704
+ // HandPose (21 keypoints per hand)
705
+ let options = { maxHands: 2, runtime: 'mediapipe', flipHorizontal: false };
706
+ handpose = ml5.handPose(options, modelLoaded);
707
+
708
+ // BodyPose (33 keypoints with 3D)
709
+ let options = { modelType: 'MULTIPOSE_LIGHTNING', flipped: false };
710
+ bodypose = ml5.bodyPose('BlazePose', options, modelLoaded);
711
+ ```
712
+
713
+ **Important Notes:**
714
+ - Always set `flipHorizontal: false` in ML5 options (PhoneCamera handles mirroring)
715
+ - Use `cam.videoElement` (native HTML video element) when passing to ML5's `detectStart()`
716
+ - Check `cam.ready` before using video or drawing keypoints
717
+ - Call `enableCameraTap()` to handle camera permissions automatically
718
+
573
719
  ### Debug System
574
720
 
575
721
  **Purpose:** Essential on-screen debugging system for mobile development where traditional browser dev tools aren't accessible. Provides automatic error catching, timestamped logging, and color-coded messages.
package/dist/p5-phone.js CHANGED
@@ -1129,6 +1129,526 @@ function _updateDebugDisplay() {
1129
1129
  content.scrollTop = content.scrollHeight;
1130
1130
  }
1131
1131
 
1132
+ // =========================================
1133
+ // PHONE CAMERA - ML5-OPTIMIZED VIDEO CAPTURE
1134
+ // =========================================
1135
+
1136
+ /**
1137
+ * PhoneCamera class - Video capture optimized for ML5 integration
1138
+ * Handles camera switching, mirroring, display modes, and coordinate mapping
1139
+ */
1140
+ class PhoneCamera {
1141
+ constructor(active = 'user', mirror = true, mode = 'fitHeight') {
1142
+ this._active = active;
1143
+ this._mirror = mirror;
1144
+ this._mode = mode;
1145
+ this._fixedWidth = 640;
1146
+ this._fixedHeight = 480;
1147
+ this._video = null;
1148
+ this._ready = false;
1149
+ this._p5Instance = window;
1150
+ this._onReadyCallback = null;
1151
+
1152
+ // Store reference to createCapture for later use
1153
+ this._createCaptureRef = null;
1154
+
1155
+ // Register this camera instance globally
1156
+ if (!window._phoneCameras) {
1157
+ window._phoneCameras = [];
1158
+ }
1159
+ window._phoneCameras.push(this);
1160
+
1161
+ // Don't initialize immediately - wait for enableCameraTap() or explicit initialization
1162
+ // This fixes iOS rotation bug where camera was initialized before permissions granted
1163
+ }
1164
+
1165
+ // ========================================
1166
+ // READ-ONLY PROPERTIES
1167
+ // ========================================
1168
+
1169
+ get ready() {
1170
+ return this._ready;
1171
+ }
1172
+
1173
+ get video() {
1174
+ return this._video;
1175
+ }
1176
+
1177
+ get videoElement() {
1178
+ // Returns the native HTML video element for ML5/other libraries
1179
+ return this._video ? this._video.elt : null;
1180
+ }
1181
+
1182
+ get width() {
1183
+ if (!this._ready) return 0;
1184
+ const dims = this.getDimensions();
1185
+ return dims.width;
1186
+ }
1187
+
1188
+ get height() {
1189
+ if (!this._ready) return 0;
1190
+ const dims = this.getDimensions();
1191
+ return dims.height;
1192
+ }
1193
+
1194
+ // ========================================
1195
+ // READ-WRITE PROPERTIES
1196
+ // ========================================
1197
+
1198
+ get active() {
1199
+ return this._active;
1200
+ }
1201
+
1202
+ set active(value) {
1203
+ if (value !== 'user' && value !== 'environment') {
1204
+ console.error('PhoneCamera: active must be "user" or "environment"');
1205
+ return;
1206
+ }
1207
+ if (this._active !== value) {
1208
+ this._active = value;
1209
+ if (this._ready) {
1210
+ this._switchCamera();
1211
+ }
1212
+ }
1213
+ }
1214
+
1215
+ get mirror() {
1216
+ return this._mirror;
1217
+ }
1218
+
1219
+ set mirror(value) {
1220
+ this._mirror = !!value;
1221
+ }
1222
+
1223
+ get mode() {
1224
+ return this._mode;
1225
+ }
1226
+
1227
+ set mode(value) {
1228
+ const validModes = ['fitWidth', 'fitHeight', 'cover', 'contain', 'fixed'];
1229
+ if (!validModes.includes(value)) {
1230
+ console.error('PhoneCamera: mode must be one of:', validModes.join(', '));
1231
+ return;
1232
+ }
1233
+ this._mode = value;
1234
+ }
1235
+
1236
+ get fixedWidth() {
1237
+ return this._fixedWidth;
1238
+ }
1239
+
1240
+ set fixedWidth(value) {
1241
+ this._fixedWidth = Math.max(1, value);
1242
+ }
1243
+
1244
+ get fixedHeight() {
1245
+ return this._fixedHeight;
1246
+ }
1247
+
1248
+ set fixedHeight(value) {
1249
+ this._fixedHeight = Math.max(1, value);
1250
+ }
1251
+
1252
+ /**
1253
+ * Set callback to run when video is fully ready
1254
+ * @param {function} callback - Function to call when video is ready for ML5
1255
+ */
1256
+ onReady(callback) {
1257
+ this._onReadyCallback = callback;
1258
+
1259
+ // If already ready, call immediately
1260
+ if (this._ready && this._video && this._video.elt && this._video.elt.readyState >= 2) {
1261
+ callback();
1262
+ } else if (this._video) {
1263
+ // Video exists but not ready yet - start checking
1264
+ this._checkVideoReady();
1265
+ }
1266
+ // If video doesn't exist yet, callback will fire when _initializeCamera completes
1267
+ }
1268
+
1269
+ // ========================================
1270
+ // INTERNAL METHODS
1271
+ // ========================================
1272
+
1273
+ _initializeCamera() {
1274
+ if (this._ready || this._video) return;
1275
+
1276
+ const constraints = {
1277
+ video: {
1278
+ facingMode: this._active
1279
+ },
1280
+ audio: false
1281
+ };
1282
+
1283
+ // Use p5's createCapture
1284
+ this._video = createCapture(constraints, () => {
1285
+ this._ready = true;
1286
+ this._video.hide(); // Hide default video element
1287
+ console.log('✅ PhoneCamera ready');
1288
+ this._checkVideoReady();
1289
+ });
1290
+
1291
+ // Handle load event for older p5 versions
1292
+ if (this._video && this._video.elt) {
1293
+ this._video.elt.addEventListener('loadeddata', () => {
1294
+ this._ready = true;
1295
+ this._checkVideoReady();
1296
+ });
1297
+ }
1298
+ }
1299
+
1300
+ _checkVideoReady() {
1301
+ // Check if video element has enough data for ML5
1302
+ if (this._video && this._video.elt && this._video.elt.readyState >= 2) {
1303
+ if (this._onReadyCallback) {
1304
+ const callback = this._onReadyCallback;
1305
+ this._onReadyCallback = null; // Clear callback so it only fires once
1306
+ callback();
1307
+ }
1308
+ } else {
1309
+ // Check again shortly
1310
+ setTimeout(() => this._checkVideoReady(), 100);
1311
+ }
1312
+ }
1313
+
1314
+ _switchCamera() {
1315
+ if (!this._video) return;
1316
+
1317
+ // Remove old video
1318
+ const wasReady = this._ready;
1319
+ this._ready = false;
1320
+ this._video.remove();
1321
+
1322
+ // Create new video with new facing mode
1323
+ const constraints = {
1324
+ video: {
1325
+ facingMode: this._active
1326
+ },
1327
+ audio: false
1328
+ };
1329
+
1330
+ this._video = createCapture(constraints, () => {
1331
+ this._ready = true;
1332
+ this._video.hide();
1333
+ console.log(`✅ PhoneCamera switched to ${this._active} camera`);
1334
+ });
1335
+
1336
+ // Handle load event for older p5 versions
1337
+ if (this._video && this._video.elt) {
1338
+ this._video.elt.addEventListener('loadeddata', () => {
1339
+ this._ready = true;
1340
+ });
1341
+ }
1342
+ }
1343
+
1344
+ // ========================================
1345
+ // PUBLIC METHODS
1346
+ // ========================================
1347
+
1348
+ /**
1349
+ * Remove and stop the camera
1350
+ */
1351
+ remove() {
1352
+ if (this._video) {
1353
+ this._video.remove();
1354
+ this._video = null;
1355
+ }
1356
+ this._ready = false;
1357
+ }
1358
+
1359
+ /**
1360
+ * Get dimension information for the current display mode
1361
+ * Returns: { x, y, width, height, scaleX, scaleY }
1362
+ */
1363
+ getDimensions() {
1364
+ if (!this._ready || !this._video) {
1365
+ return { x: 0, y: 0, width: 0, height: 0, scaleX: 1, scaleY: 1 };
1366
+ }
1367
+
1368
+ const videoWidth = this._video.width;
1369
+ const videoHeight = this._video.height;
1370
+ const canvasWidth = window.width || window.innerWidth;
1371
+ const canvasHeight = window.height || window.innerHeight;
1372
+
1373
+ let drawWidth, drawHeight, drawX, drawY;
1374
+
1375
+ if (this._mode === 'fixed') {
1376
+ drawWidth = this._fixedWidth;
1377
+ drawHeight = this._fixedHeight;
1378
+ drawX = (canvasWidth - drawWidth) / 2;
1379
+ drawY = (canvasHeight - drawHeight) / 2;
1380
+
1381
+ } else if (this._mode === 'fitWidth') {
1382
+ drawWidth = canvasWidth;
1383
+ drawHeight = (videoHeight / videoWidth) * drawWidth;
1384
+ drawX = 0;
1385
+ drawY = (canvasHeight - drawHeight) / 2;
1386
+
1387
+ } else if (this._mode === 'fitHeight') {
1388
+ drawHeight = canvasHeight;
1389
+ drawWidth = (videoWidth / videoHeight) * drawHeight;
1390
+ drawX = (canvasWidth - drawWidth) / 2;
1391
+ drawY = 0;
1392
+
1393
+ } else if (this._mode === 'cover') {
1394
+ const scale = Math.max(
1395
+ canvasWidth / videoWidth,
1396
+ canvasHeight / videoHeight
1397
+ );
1398
+ drawWidth = videoWidth * scale;
1399
+ drawHeight = videoHeight * scale;
1400
+ drawX = (canvasWidth - drawWidth) / 2;
1401
+ drawY = (canvasHeight - drawHeight) / 2;
1402
+
1403
+ } else if (this._mode === 'contain') {
1404
+ const scale = Math.min(
1405
+ canvasWidth / videoWidth,
1406
+ canvasHeight / videoHeight
1407
+ );
1408
+ drawWidth = videoWidth * scale;
1409
+ drawHeight = videoHeight * scale;
1410
+ drawX = (canvasWidth - drawWidth) / 2;
1411
+ drawY = (canvasHeight - drawHeight) / 2;
1412
+ }
1413
+
1414
+ return {
1415
+ x: drawX,
1416
+ y: drawY,
1417
+ width: drawWidth,
1418
+ height: drawHeight,
1419
+ scaleX: drawWidth / videoWidth,
1420
+ scaleY: drawHeight / videoHeight
1421
+ };
1422
+ }
1423
+
1424
+ /**
1425
+ * Map a simple point (x, y) to display coordinates
1426
+ * Handles mirroring automatically
1427
+ * @param {number} x - X coordinate in video space
1428
+ * @param {number} y - Y coordinate in video space
1429
+ * @returns {object} - { x, y } in display space
1430
+ */
1431
+ mapPoint(x, y) {
1432
+ const dims = this.getDimensions();
1433
+
1434
+ // Scale the coordinates from video space to display space
1435
+ let scaledX = x * dims.scaleX;
1436
+ const scaledY = y * dims.scaleY;
1437
+
1438
+ // Apply mirroring if enabled
1439
+ if (this._mirror) {
1440
+ // Mirror the scaled coordinate within the video width
1441
+ scaledX = dims.width - scaledX;
1442
+ }
1443
+
1444
+ // Add offset to position on canvas
1445
+ // Note: dims.x can be negative in fitHeight mode when video is wider than canvas
1446
+ // In that case, the video is drawn starting off-screen, but we want coordinates
1447
+ // relative to the visible portion, so we use max(0, dims.x)
1448
+ const mappedX = scaledX + Math.max(0, dims.x);
1449
+ const mappedY = scaledY + Math.max(0, dims.y);
1450
+
1451
+ return { x: mappedX, y: mappedY };
1452
+ }
1453
+
1454
+ /**
1455
+ * Map an ML5 keypoint object to display coordinates
1456
+ * Handles mirroring automatically
1457
+ * Preserves z coordinate and any other properties
1458
+ * @param {object} keypoint - ML5 keypoint { x, y, z?, ... }
1459
+ * @returns {object} - Keypoint with mapped coordinates
1460
+ */
1461
+ mapKeypoint(keypoint) {
1462
+ if (!keypoint || typeof keypoint.x === 'undefined' || typeof keypoint.y === 'undefined') {
1463
+ console.warn('PhoneCamera.mapKeypoint: invalid keypoint', keypoint);
1464
+ return keypoint;
1465
+ }
1466
+
1467
+ const mapped = this.mapPoint(keypoint.x, keypoint.y);
1468
+
1469
+ // Preserve all properties from original keypoint
1470
+ return {
1471
+ ...keypoint,
1472
+ x: mapped.x,
1473
+ y: mapped.y
1474
+ };
1475
+ }
1476
+
1477
+ /**
1478
+ * Map an array of ML5 keypoints to display coordinates
1479
+ * Handles mirroring automatically
1480
+ * @param {array} keypoints - Array of ML5 keypoints
1481
+ * @returns {array} - Array of keypoints with mapped coordinates
1482
+ */
1483
+ mapKeypoints(keypoints) {
1484
+ if (!Array.isArray(keypoints)) {
1485
+ console.warn('PhoneCamera.mapKeypoints: expected array, got', typeof keypoints);
1486
+ return keypoints;
1487
+ }
1488
+
1489
+ return keypoints.map(kp => this.mapKeypoint(kp));
1490
+ }
1491
+
1492
+ // ========================================
1493
+ // CANVAS DRAWING INTEGRATION
1494
+ // ========================================
1495
+
1496
+ /**
1497
+ * Custom draw method for p5.image() compatibility
1498
+ * This allows image(cam, x, y) to work
1499
+ */
1500
+ _draw() {
1501
+ if (!this._ready || !this._video) return;
1502
+
1503
+ const dims = this.getDimensions();
1504
+
1505
+ // Save current drawing state
1506
+ push();
1507
+
1508
+ // Apply mirroring if needed
1509
+ if (this._mirror) {
1510
+ translate(dims.x + dims.width, dims.y);
1511
+ scale(-1, 1);
1512
+ image(this._video, 0, 0, dims.width, dims.height);
1513
+ } else {
1514
+ image(this._video, dims.x, dims.y, dims.width, dims.height);
1515
+ }
1516
+
1517
+ pop();
1518
+ }
1519
+ }
1520
+
1521
+ /**
1522
+ * Create a new PhoneCamera instance
1523
+ * @param {string} active - 'user' (front) or 'environment' (back) camera
1524
+ * @param {boolean} mirror - Whether to mirror the video horizontally
1525
+ * @param {string} mode - Display mode: 'fitWidth', 'fitHeight', 'cover', 'contain', 'fixed'
1526
+ * @returns {PhoneCamera} - Camera instance
1527
+ */
1528
+ function createPhoneCamera(active = 'user', mirror = true, mode = 'fitHeight') {
1529
+ return new PhoneCamera(active, mirror, mode);
1530
+ }
1531
+
1532
+ /**
1533
+ * Enable camera with a button interface
1534
+ * Creates a start button that user must click
1535
+ */
1536
+ function enableCameraButton(buttonText = 'ENABLE CAMERA', statusText = 'Starting camera...') {
1537
+ _createPermissionButton(buttonText, statusText, async () => {
1538
+ await _requestCameraPermission();
1539
+ console.log('✅ Camera enabled via button');
1540
+ });
1541
+ }
1542
+
1543
+ /**
1544
+ * Enable camera with tap-to-start
1545
+ * User taps anywhere on screen to enable
1546
+ */
1547
+ function enableCameraTap(message = 'Tap screen to enable camera') {
1548
+ // Check if camera permission is already granted
1549
+ if (navigator.permissions && navigator.permissions.query) {
1550
+ navigator.permissions.query({ name: 'camera' })
1551
+ .then(permissionStatus => {
1552
+ if (permissionStatus.state === 'granted') {
1553
+ // Permission already granted - skip the tap UI and initialize immediately
1554
+ console.log('✅ Camera permission already granted - auto-starting');
1555
+ _requestCameraPermission();
1556
+ } else {
1557
+ // Permission not granted - show tap UI
1558
+ _createTapToEnable(message, async () => {
1559
+ await _requestCameraPermission();
1560
+ console.log('✅ Camera enabled via tap');
1561
+ });
1562
+ }
1563
+ })
1564
+ .catch(() => {
1565
+ // Permissions API not supported - show tap UI
1566
+ _createTapToEnable(message, async () => {
1567
+ await _requestCameraPermission();
1568
+ console.log('✅ Camera enabled via tap');
1569
+ });
1570
+ });
1571
+ } else {
1572
+ // Fallback - show tap UI
1573
+ _createTapToEnable(message, async () => {
1574
+ await _requestCameraPermission();
1575
+ console.log('✅ Camera enabled via tap');
1576
+ });
1577
+ }
1578
+ }
1579
+
1580
+ async function _requestCameraPermission() {
1581
+ try {
1582
+ // Initialize any PhoneCamera instances that haven't been initialized yet
1583
+ // This happens after user interaction grants camera permission
1584
+ if (typeof window._phoneCameras !== 'undefined' && Array.isArray(window._phoneCameras)) {
1585
+ for (let cam of window._phoneCameras) {
1586
+ if (cam && !cam._ready && !cam._video) {
1587
+ cam._initializeCamera();
1588
+ }
1589
+ }
1590
+ }
1591
+
1592
+ // Call userCameraReady callback if it exists (user-defined function)
1593
+ if (typeof userCameraReady === 'function') {
1594
+ userCameraReady();
1595
+ }
1596
+
1597
+ _notifySketchReady();
1598
+
1599
+ } catch (error) {
1600
+ console.error('Camera permission error:', error);
1601
+ if (_debugVisible) {
1602
+ debugError('Camera permission error:', error);
1603
+ }
1604
+ _notifySketchReady();
1605
+ }
1606
+ }
1607
+
1608
+ // Make camera functions globally accessible
1609
+ window.createPhoneCamera = createPhoneCamera;
1610
+ window.enableCameraButton = enableCameraButton;
1611
+ window.enableCameraTap = enableCameraTap;
1612
+
1613
+ // Override p5's image() function to support PhoneCamera
1614
+ if (typeof p5 !== 'undefined' && p5.prototype) {
1615
+ const originalImage = p5.prototype.image;
1616
+
1617
+ p5.prototype.image = function(...args) {
1618
+ // Check if first argument is a PhoneCamera instance
1619
+ if (args[0] instanceof PhoneCamera) {
1620
+ const cam = args[0];
1621
+
1622
+ // If x and y are provided, use them, otherwise use camera's calculated position
1623
+ if (args.length >= 3) {
1624
+ // User provided position: image(cam, x, y, [w], [h])
1625
+ if (!cam.ready || !cam.video) return;
1626
+
1627
+ const x = args[1];
1628
+ const y = args[2];
1629
+ const w = args[3] || cam.width;
1630
+ const h = args[4] || cam.height;
1631
+
1632
+ this.push();
1633
+ if (cam.mirror) {
1634
+ this.translate(x + w, y);
1635
+ this.scale(-1, 1);
1636
+ originalImage.call(this, cam.video, 0, 0, w, h);
1637
+ } else {
1638
+ originalImage.call(this, cam.video, x, y, w, h);
1639
+ }
1640
+ this.pop();
1641
+ } else {
1642
+ // No position provided: image(cam) - use auto-positioning
1643
+ cam._draw();
1644
+ }
1645
+ } else {
1646
+ // Not a PhoneCamera, use original image function
1647
+ originalImage.apply(this, args);
1648
+ }
1649
+ };
1650
+ }
1651
+
1132
1652
  // =========================================
1133
1653
  // P5.JS NAMESPACE SUPPORT
1134
1654
  // =========================================
@@ -1153,6 +1673,11 @@ if (typeof p5 !== 'undefined' && p5.prototype) {
1153
1673
  p5.prototype.enableAllTap = enableAllTap;
1154
1674
  p5.prototype.enableAllButton = enableAllButton;
1155
1675
 
1676
+ // Camera functions
1677
+ p5.prototype.createPhoneCamera = createPhoneCamera;
1678
+ p5.prototype.enableCameraButton = enableCameraButton;
1679
+ p5.prototype.enableCameraTap = enableCameraTap;
1680
+
1156
1681
  // Debug functions
1157
1682
  p5.prototype.showDebug = showDebug;
1158
1683
  p5.prototype.hideDebug = hideDebug;