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.
- package/README.md +149 -3
- package/dist/p5-phone.js +525 -0
- package/dist/p5-phone.min.js +1 -1
- package/examples/Phone Sensor Examples/microphone/01_mic_level/index.html +1 -1
- package/examples/Phone Sensor Examples/movement/01_orientation_basic/index.html +1 -1
- package/examples/Phone Sensor Examples/movement/02_rotational_velocity/index.html +1 -1
- package/examples/Phone Sensor Examples/movement/03_acceleration/index.html +1 -1
- package/examples/Phone Sensor Examples/sound/01_dual_audio/index.html +1 -1
- package/examples/Phone Sensor Examples/sound/02_volume_touches/index.html +1 -1
- package/examples/Phone Sensor Examples/touch/01_touch_basic/index.html +1 -1
- package/examples/Phone Sensor Examples/touch/02_touch_zones/index.html +1 -1
- package/examples/Phone Sensor Examples/touch/03_touch_count/index.html +1 -1
- package/examples/Phone Sensor Examples/touch/04_touch_distance/index.html +1 -1
- package/examples/Phone Sensor Examples/touch/05_touch_angle/index.html +1 -1
- package/examples/Phone Sensor Examples/vibration/01_haptic_feedback/index.html +1 -1
- package/examples/Phone Sensor Examples - Minimal/microphone/01_mic_level/index.html +1 -1
- package/examples/Phone Sensor Examples - Minimal/movement/01_orientation_basic/index.html +1 -1
- package/examples/Phone Sensor Examples - Minimal/movement/02_rotational_velocity/index.html +1 -1
- package/examples/Phone Sensor Examples - Minimal/movement/03_acceleration/index.html +1 -1
- package/examples/Phone Sensor Examples - Minimal/sound/01_sound_basic/index.html +1 -1
- package/examples/Phone Sensor Examples - Minimal/sound/02_sound_amplitude/index.html +1 -1
- package/examples/Phone Sensor Examples - Minimal/touch/01_touch_basic/index.html +1 -1
- package/examples/Phone Sensor Examples - Minimal/touch/02_touch_zones/index.html +1 -1
- package/examples/Phone Sensor Examples - Minimal/touch/03_touch_count/index.html +1 -1
- package/examples/Phone Sensor Examples - Minimal/touch/04_touch_distance/index.html +1 -1
- package/examples/Phone Sensor Examples - Minimal/touch/05_touch_angle/index.html +1 -1
- package/examples/Phone Sensor Examples - Minimal/vibration/01_haptic_feedback/index.html +18 -0
- package/examples/Phone Sensor Examples - Minimal/vibration/01_haptic_feedback/sketch.js +87 -0
- package/examples/Phone and Gif/collision/index.html +1 -1
- package/examples/Phone and Gif/fetch/index.html +1 -1
- package/examples/Phone and Gif/fly/index.html +1 -1
- package/examples/Phone and Gif/roll/index.html +1 -1
- package/examples/UXcompare/button-vs-movement/index.html +1 -1
- package/examples/UXcompare/button-vs-orientation/index.html +1 -1
- package/examples/UXcompare/button-vs-shake/index.html +1 -1
- package/examples/UXcompare/gyroscope-demo/index.html +1 -1
- package/examples/UXcompare/microphone-demo/index.html +1 -1
- package/examples/UXcompare/slider-vs-angle/index.html +1 -1
- package/examples/UXcompare/slider-vs-distance/index.html +1 -1
- package/examples/UXcompare/slider-vs-microphone/index.html +1 -1
- package/examples/UXcompare/slider-vs-touches/index.html +1 -1
- package/examples/UXcompare/sliders-vs-acceleration/index.html +1 -1
- package/examples/UXcompare/sliders-vs-rotation/index.html +1 -1
- package/examples/blankTemplate/index.html +1 -1
- package/examples/homepage/index.html +24 -5
- package/examples/workArea/01_camera-selector/README.md +119 -0
- package/examples/workArea/01_camera-selector/index.html +28 -0
- package/examples/workArea/01_camera-selector/sketch.js +239 -0
- package/examples/workArea/03_facemesh-nose/index.html +34 -0
- package/examples/workArea/03_facemesh-nose/sketch.js +247 -0
- package/examples/workArea/03_facemesh-nose-preload/index.html +34 -0
- package/examples/workArea/03_facemesh-nose-preload/sketch.js +173 -0
- package/examples/workArea/04_facemesh-FINAL/README.md +85 -0
- package/examples/workArea/04_facemesh-FINAL/index.html +31 -0
- package/examples/workArea/04_facemesh-FINAL/sketch.js +240 -0
- package/examples/workArea/04_facemesh-simplified_temp/README.md +93 -0
- package/examples/workArea/04_facemesh-simplified_temp/index.html +31 -0
- package/examples/workArea/04_facemesh-simplified_temp/sketch.js +259 -0
- package/examples/workArea/05_handpose/extra.js +0 -0
- package/examples/workArea/05_handpose/index.html +31 -0
- package/examples/workArea/05_handpose/sketch.js +362 -0
- package/examples/workArea/05_handpose-preload/index.html +31 -0
- package/examples/workArea/05_handpose-preload/sketch.js +362 -0
- package/examples/workArea/06_bodypose-FINAL/index.html +31 -0
- package/examples/workArea/06_bodypose-FINAL/sketch.js +360 -0
- package/package.json +1 -1
- 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.
|
|
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.
|
|
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.
|
|
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;
|