Source: p5.sensor.js

// Encapsulate variables within the p5 prototype

//Touch

/**
 * Sets up touch event handling by disabling default touch behaviors and preventing unwanted interactions.
 * This includes disabling text selection, context menus, and other default touch gestures.
 * @method setupTouchSensor
 * @example
 * function setup() {
 *   setupTouchSensor();
 * }
 */

p5.prototype.setupTouchSensor = function(options = {}) {
  // Set default options
  const defaults = {
    disableGestures: false,
    disableSelection: true, 
    disableContextMenu: true,
    disableHighlighting: true,
    disableLongPressZoom: true
  };

  // Merge provided options with defaults
  const settings = {...defaults, ...options};

  // Disable touch gestures like zooming and panning
  if (settings.disableGestures) {
    this.disableTouchGestures();
  }

  // Prevent text selection on the page
  if (settings.disableSelection) {
    this.disableTextSelection();
  }

  // Prevent the context menu (right-click) from appearing anywhere
  if (settings.disableContextMenu) {
    this.disableContextMenu();
  }

  // Prevent text selection
  if (settings.disableHighlighting) {
    this.disableTextHighlighting();
  }

  if (settings.disableLongPressZoom){
    this.disableLongPressZoom();
  }

  
}

/**
 * Disables touch gestures like zooming and panning while allowing touch actions on buttons
 * @method disableTouchGestures
 * @private
 */
p5.prototype.disableTouchGestures = function() {
  document.body.style.touchAction = "manipulation";
};

/**
 * Prevents text selection on the page
 * @method disableTextSelection
 * @private
 */
p5.prototype.disableTextSelection = function() {
  document.body.style.userSelect = "none";
};

/**
 * Prevents the context menu (right-click) from appearing
 * @method disableContextMenu
 * @private
 */
p5.prototype.disableContextMenu = function() {
  document.addEventListener("contextmenu", (event) => event.preventDefault());
};

/**
 * Prevents text highlighting/selection
 * @method disableTextHighlighting
 * @private
 */
p5.prototype.disableTextHighlighting = function() {
  document.addEventListener("selectstart", (event) => event.preventDefault());
};

/**
 * Prevents zoom lens popup
 * @method disableLongPressZoom
 * @private
 */
p5.prototype.disableLongPressZoom = function() {

  document.body.style.userSelect = 'none';            // Standard
  document.body.style.webkitUserSelect = 'none';      // iOS/Chrome on iOS
  document.body.style.webkitTouchCallout = 'none';    // iOS callout (copy/paste)

  document.addEventListener('touchstart', function (event) {
    // Call your custom function
    touchStarted(event);
  }, { passive: false });
};


/**
 * Checks if the sensor permission has been granted.
 * @method isSensorPermissionGranted
 * @return {boolean} True if the sensor permission is granted, false otherwise.
 */
p5.prototype.isSensorPermissionGranted = function() {
  return this._sensorPermissionGranted;
};

/**
 * @property {boolean} _sensorPermissionGranted - Indicates whether device motion/orientation sensor access is allowed
 * @private 
 */
p5.prototype._sensorPermissionGranted = false;

/**
 * @property {Object|null} _accelerationWithGravity
 * @private
 */
p5.prototype._accelerationWithGravity = null;

/**
 * @property {Object|null} _acceleration
 * @private
 */
p5.prototype._acceleration = null;

/**
 * @property {Object|null} _rotation
 * @private
 */
p5.prototype._rotation = null;

/**
 * @property {Object|null} _orientation
 * @private
 */
p5.prototype._orientation = null;

/**
 * @property {Object|null} _absoluteOrientation
 * @private
 */
p5.prototype._absoluteOrientation = null;

// Add p5 prototype variables for gravity, orientation, and absolute orientation
/**
 * @property {number} gravityX
 * @readOnly
 */
p5.prototype.gravityX = 0;

/**
 * @property {number} gravityY
 * @readOnly
 */
p5.prototype.gravityY = 0;

/**
 * @property {number} gravityZ
 * @readOnly
 */
p5.prototype.gravityZ = 0;

/**
 * @property {number} orientationX
 * @readOnly
 */
p5.prototype.orientationX = 0;

/**
 * @property {number} orientationY
 * @readOnly
 */
p5.prototype.orientationY = 0;

/**
 * @property {number} orientationZ
 * @readOnly
 */
p5.prototype.orientationZ = 0;

/**
 * @property {number} absoluteOrientationX
 * @readOnly
 */
p5.prototype.absoluteOrientationX = 0;

/**
 * @property {number} absoluteOrientationY
 * @readOnly
 */
p5.prototype.absoluteOrientationY = 0;

/**
 * @property {number} absoluteOrientationZ
 * @readOnly
 */
p5.prototype.absoluteOrientationZ = 0;

/*
Motion Sensors
*/

/**
 * Sets up motion sensors by checking for necessary permissions.
 * @method setupMotionSensor
 */
p5.prototype.setupMotionSensor = function() {
    this.checkSensorPermission();
};

/**
 * Checks if the device needs permission for motion sensors and requests it if necessary.
 * @method checkSensorPermission
 * @private
 */
p5.prototype.checkSensorPermission = function() {
    const needsPermissions = (typeof DeviceOrientationEvent !== "undefined" && typeof DeviceOrientationEvent?.requestPermission === "function") ||
                            (typeof DeviceMotionEvent !== "undefined" && typeof DeviceMotionEvent?.requestPermission === "function");

    if (needsPermissions) {
        Promise.all([
            DeviceOrientationEvent?.requestPermission?.() || Promise.resolve(),
            DeviceMotionEvent?.requestPermission?.() || Promise.resolve()
        ]).catch(() => {
            let button = createButton("Allow Sensors")
                .id("sensorPermissionButton")
                .style("font-size", "24px")
                .center()
                .mousePressed(this.requestSensorAccess.bind(this));
            //throw new Error("Sensor permission required");
        }).then(() => {
            this._sensorPermissionGranted = true;
            window.addEventListener("deviceorientation", this.updateOrientation.bind(this), true);
            window.addEventListener("deviceorientationabsolute", this.updateOrientation.bind(this), true);
            window.addEventListener("devicemotion", this.updateMotion.bind(this), true);
        });
    } else {
        this._sensorPermissionGranted = true;
        window.addEventListener("deviceorientation", this.updateOrientation.bind(this), true);
        window.addEventListener("deviceorientationabsolute", this.updateOrientation.bind(this), true);
        window.addEventListener("devicemotion", this.updateMotion.bind(this), true);
    }
};


/**
 * Handles the sensor permission request.
 * @method requestSensorAccess
 * @private
 */
p5.prototype.requestSensorAccess = function() {
    // Request permission from the user
    Promise.all([
      DeviceOrientationEvent.requestPermission(),
      DeviceMotionEvent.requestPermission()
    ]).then(([orientationResponse, motionResponse]) => {
      // If both permissions are granted, set _sensorPermissionGranted to true
      if (orientationResponse === "granted" && motionResponse === "granted") {
        this._sensorPermissionGranted = true;
      } else {
        // If either permission is denied, keep it false
        this._sensorPermissionGranted = false;
      }
    })
    .catch(console.error); // Log errors to the console
  
    // Remove the button after the user interacts with it
    select("#sensorPermissionButton").remove();
};


/* p5 default motion variables
let accelerationX;
let accelerationY;
let accelerationZ;
let rotationX;
let rotationY;
let rotationZ;
*/

/**
 * Updates orientation data from device sensors.
 * @method updateOrientation
 * @param {DeviceOrientationEvent} event - The orientation event containing sensor data.
 * @private
 */
p5.prototype.updateOrientation = function(event) {
  // Handle regular DeviceOrientationEvent
  if (!event.absolute) {
    let orientX = event.beta;    // x-axis rotation [-180,180]
    let orientY = event.gamma;   // y-axis rotation [-90,90]
    let orientZ = event.alpha;   // z-axis rotation [0,360)
    
    // Use p5.js deviceOrientation
    if (deviceOrientation === 'landscape') {
      orientX = event.gamma;
      orientY = -event.beta;
    } else if (deviceOrientation === 'portrait') {
      orientX = event.beta;
      orientY = event.gamma;
    } else {
      // Default or unknown orientation
      orientX = event.beta;
      orientY = event.gamma;
    }

    this._orientation = {
      x: orientX,
      y: orientY,
      z: orientZ,
      alpha: orientZ,
      beta: orientX,
      gamma: orientY
    };
    
    this._setProperty('orientationX', orientX);
    this._setProperty('orientationY', orientY);
    this._setProperty('orientationZ', orientZ);
  }
  // Handle Absolute Orientation
  else {
    let absOrientX = event.beta;
    let absOrientY = event.gamma;
    let absOrientZ = event.alpha;
    
    // Adjust values based on screen orientation
    if (deviceOrientation === 'landscape') {
      absOrientX = event.gamma;
      absOrientY = -event.beta;
    } else if (deviceOrientation === 'portrait') {
      absOrientX = -event.gamma;
      absOrientY = event.beta;
    }

    this._absoluteOrientation = {
      x: absOrientX,
      y: absOrientY,
      z: absOrientZ,
      alpha: absOrientZ,
      beta: absOrientX,
      gamma: absOrientY
    };
    
    this._setProperty('absoluteOrientationX', absOrientX);
    this._setProperty('absoluteOrientationY', absOrientY);
    this._setProperty('absoluteOrientationZ', absOrientZ);

    // Handle compass heading
    if (event.webkitCompassHeading !== undefined) {
      this._setProperty('heading', event.webkitCompassHeading);
    } else if (event.alpha !== null) {
      // Convert alpha to compass heading (alpha is measured clockwise, heading counter-clockwise)
      let heading = (360 - event.alpha) % 360;
      this._setProperty('heading', heading);
    }
  }
};

/**
 * Updates motion data from device sensors.
 * @method updateMotion
 * @param {Event} event - The motion event containing sensor data.
 * @private
 */
p5.prototype.updateMotion = function(event) {
  // Handle DeviceMotionEvent
  if (event instanceof DeviceMotionEvent) {
    const accWithGravity = event.accelerationIncludingGravity;
    const acc = event.acceleration;
    const rot = event.rotationRate;

    // Handle acceleration including gravity
    if (accWithGravity) {
      // Adjust for landscape mode if needed
      let gravityX = accWithGravity.x;
      let gravityY = accWithGravity.y;
      
      // Adjust values based on screen orientation
      if (deviceOrientation === 'landscape') {
        gravityX = -accWithGravity.y;
        gravityY = accWithGravity.x;
      } else if (deviceOrientation === 'portrait') {
        gravityX = accWithGravity.y;
        gravityY = -accWithGravity.x;
      }

      this._accelerationWithGravity = {
        x: gravityX,
        y: gravityY, 
        z: accWithGravity.z
      };
      this._setProperty('gravityX', gravityX);
      this._setProperty('gravityY', gravityY);
      this._setProperty('gravityZ', accWithGravity.z);
    }

    // Handle pure acceleration
    if (acc) {
      let accelX = acc.x;
      let accelY = acc.y;
      
      // Adjust values based on screen orientation
      if (deviceOrientation === 'landscape') {
        accelX = -acc.y;
        accelY = acc.x;
      } else if (deviceOrientation === 'portrait') {
        accelX = acc.y;
        accelY = -acc.x;
      }

      this._acceleration = {
        x: accelX,
        y: accelY,
        z: acc.z
      };
    }

    // Handle rotation rate
    if (rot) {
      let rotX = rot.beta;   // rotation around x-axis
      let rotY = rot.gamma;  // rotation around y-axis
      let rotZ = rot.alpha;  // rotation around z-axis
      
      // Adjust values based on screen orientation
      if (deviceOrientation === 'landscape') {
        rotX = rot.gamma;
        rotY = -rot.beta;
      } else if (deviceOrientation === 'portrait') {
        rotX = -rot.gamma;
        rotY = rot.beta;
      }

      this._rotation = {
        x: rotX,
        y: rotY,
        z: rotZ,
        alpha: rotZ,
        beta: rotX,
        gamma: rotY
      };
    }
  }
};

/**
 * Returns a JSON string containing debug information about motion sensor data.
 * @method debugMotion
 * @return {string} JSON string with motion sensor data
 */
p5.prototype.debugMotion = function() {
  return JSON.stringify({
    accelerationWithGravity: this._accelerationWithGravity,
    acceleration: this._acceleration, 
    rotation: this._rotation,
    orientation: this._orientation,
    absoluteOrientation: this._absoluteOrientation,
    screenOrientation: deviceOrientation
  });
};

/**
 * Retrieves the current acceleration data including gravity.
 * @method getAccelerationWithGravity
 * @return {Object|null} An object containing acceleration data or null if unavailable.
 * @alias getGravity
 */
p5.prototype.getAccelerationWithGravity = function() {
  if (this._sensorPermissionGranted && this._accelerationWithGravity) {
    return { ...this._accelerationWithGravity };
  } else {
    console.log("Accelerometer data not available.");
    return null;
  }
};


p5.prototype.getGravity = p5.prototype.getAccelerationWithGravity;

/**
 * Retrieves the current acceleration data.
 * @method getAcceleration
 * @return {Object|null} An object containing acceleration data or null if unavailable.
 */
p5.prototype.getAcceleration = function() {
  if (this._sensorPermissionGranted && this._acceleration) {
    return { ...this._acceleration };
  } else {
    console.log("Accelerometer data not available.");
    return null;
  }
};

/**
 * Retrieves the current rotation data.
 * @method getRotation
 * @return {Object|null} An object containing rotation data or null if unavailable.
 */
p5.prototype.getRotation = function() {
  if (this._sensorPermissionGranted && this._rotation) {
    return { ...this._rotation };
  } else {
    console.log("Rotation data not available.");
    return null;
  }
};

/**
 * Retrieves the current orientation data.
 * @method getOrientation
 * @return {Object|null} An object containing orientation data or null if unavailable.
 */
p5.prototype.getOrientation = function() {
  if (this._sensorPermissionGranted && this._orientation) {
    return { ...this._orientation };
  } else {
    console.log("Orientation data not available.");
    return null;
  }
};

/**
 * Retrieves the current absolute orientation data.
 * @method getAbsoluteOrientation
 * @return {Object|null} An object containing absolute orientation data or null if unavailable.
 */
p5.prototype.getAbsoluteOrientation = function() {
  if (this._sensorPermissionGranted && this._absoluteOrientation) {
    return { ...this._absoluteOrientation };
  } else {
    console.log("Absolute orientation data not available.");
    return null;
  }
};  

/*
Geolocation API
*/

/**
 * Requests the current geolocation of the device.
 * @method requestGeolocation
 */
p5.prototype.requestGeolocation = function(handleGeolocation, interval = 0) {
    if (!navigator.geolocation) {
        alert("Geolocation is not supported by this browser.");
        return;
    }

    const options = {
        enableHighAccuracy: true
    };

    const successCallback = (position) => {
        const geolocationData = {
            latitude: position.coords.latitude,
            longitude: position.coords.longitude
        };
        this._setProperty('latitude', position.coords.latitude);
        this._setProperty('longitude', position.coords.longitude);
        handleGeolocation(geolocationData);
    };

    if (interval > 0) {
        // Set up recurring geolocation updates
        const watchId = navigator.geolocation.watchPosition(
            successCallback,
            this.showGeolocationError.bind(this),
            options
        );
        // Store watch ID to allow clearing later
        this._geolocationWatchId = watchId;

        // Set timeout to clear watch after interval
        setTimeout(() => {
            navigator.geolocation.clearWatch(watchId);
        }, interval);
    } else {
        // One-time geolocation request
        navigator.geolocation.getCurrentPosition(
            successCallback,
            this.showGeolocationError.bind(this),
            options
        );
    }
};

// /**
//  * Callback function to handle successful geolocation retrieval.
//  * @method getGeolocation
//  * @param {Position} position - The position object containing latitude and longitude.
//  * @return {Object} An object containing latitude and longitude.
//  */
// p5.prototype.getGeolocation = function(position) {
//     this._setProperty('latitude', position.coords.latitude);
//     this._setProperty('longitude', position.coords.longitude);
//     return {
//         latitude: position.coords.latitude,
//         longitude: position.coords.longitude
//     };
// };

/**
 * Callback function to handle geolocation errors.
 * @method showGeolocationError
 * @param {PositionError} error - The error object containing error code and message.
 */
p5.prototype.showGeolocationError = function(error) {
    let errorMsg = "";
    switch (error.code) {
        case error.PERMISSION_DENIED:
            errorMsg = "User denied the request for Geolocation.";
            break;
        case error.POSITION_UNAVAILABLE:
            errorMsg = "Location information is unavailable.";
            break;
        case error.TIMEOUT:
            errorMsg = "The request to get user location timed out.";
            break;
        case error.UNKNOWN_ERROR:
            errorMsg = "An unknown error occurred.";
            break;
    }
    console.error(errorMsg);
};

/*
Camera & Light Sensor
*/

/**
 * @property {p5.Element|null} _video
 * @private
 */
p5.prototype._video = null;

/**
 * @property {boolean} _isFrontCamera
 * @private
 */
p5.prototype._isFrontCamera = true;

/**
 * Returns whether the front camera is currently active.
 * @method isFrontCamera
 * @return {boolean} True if the front camera is active, false if the back camera is active.
 */
p5.prototype.isFrontCamera = function() {
  return this._isFrontCamera;
};

/**
 * @property {number} _brightnessLevel
 * @private
 */
p5.prototype._brightnessLevel = 0;

/**
 * Captures the color at the center of the video feed.
 * @method captureColor
 * @return {Array} An array containing RGB values.
 * @alias getColor
 */
p5.prototype.captureColor = function() {
    this._video.loadPixels();
    let index = ((this._video.height / 2) * this._video.width + this._video.width / 2) * 4;
    let r = this._video.pixels[index];
    let g = this._video.pixels[index + 1];
    let b = this._video.pixels[index + 2];

    let pickedColor = [r, g, b];

    return pickedColor;
};

/**
 * Captures the light level from the video feed.
 * @method captureLightLevel
 * @return {number} The brightness level.
 * @alias getLightLevel
 */
p5.prototype.captureLightLevel = function() {
    return this.updateBrightness();
};

/**
 * Captures the brightness level from the video feed.
 * @method captureBrightness
 * @return {number} The brightness level.
 * @alias getBrightness
 */
p5.prototype.captureBrightness = function() {
  return this.updateBrightness();
};

/**
 * Updates the brightness level from the video feed.
 * @method updateBrightness
 * @return {number} The updated brightness level.
 * @alias updateBrightness
 */
p5.prototype.updateBrightness = function() {
    this._video.loadPixels();
    let index = ((this._video.height / 2) * this._video.width + this._video.width / 2) * 4;
    let r = this._video.pixels[index];
    let g = this._video.pixels[index + 1];
    let b = this._video.pixels[index + 2];
    this._setProperty('_brightnessLevel', (r + g + b) / 3); // Compute grayscale brightness

    return this._brightnessLevel;
};

/**
 * Sets up the camera with the specified orientation.
 * @method setupCamera
 * @param {string} [selectCamera="front"] - The camera orientation, either "front" or "back".
 */
p5.prototype.setupCamera = function(selectCamera = "front") {
    if (this._video) {
        this._video.remove();
    }

    this._setProperty('_isFrontCamera', selectCamera === "front");

    let constraints = this._isFrontCamera
        ? { video: { facingMode: "user" }, audio: false }
        : { video: { facingMode: "environment" }, audio: false };

    this._video = createCapture(constraints);
    this._video.size(160, 120);
    this._video.loadPixels();

    this._video.hide();

    this._setProperty('_video', this._video);
    
};

/**
 * Sets up the light sensor using the camera.
 * @method setupLightSensor
 * @param {string} [selectCamera="front"] - The camera orientation, either "front" or "back".
 */
p5.prototype.setupLightSensor = function(selectCamera = "front") {
    this.setupCamera(selectCamera);
};

/**
 * Sets up the color sensor using the camera.
 * @method setupColorSensor
 * @param {string} [selectCamera="front"] - The camera orientation, either "front" or "back".
 */
p5.prototype.setupColorSensor = function(selectCamera = "front") {
    this.setupCamera(selectCamera);
};

/**
 * Initializes the camera with the specified orientation.
 * @method initializeCamera
 * @param {string} [selectCamera="front"] - The camera orientation, either "front" or "back".
 */
p5.prototype.initializeCamera = function(selectCamera = "front") {
    this.setupCamera(selectCamera);
};

/**
 * Toggles the camera between front and back.
 * @method toggleCamera
 */
p5.prototype.toggleCamera = function() {
    this._setProperty('_isFrontCamera', !this._isFrontCamera);
    this.initializeCamera(this._isFrontCamera ? "front" : "back");
};

/*
Sound Sensor
*/

/**
 * @property {p5.AudioIn|null} _mic
 * @private
 */
p5.prototype._mic = null;
p5.prototype._micStarted = false;

/**
 * Sets up the sound sensor.
 * @method setupSoundSensor
 */
p5.prototype.setupSoundSensor = function() {
    this.userStartAudio().then(() => {
        this._mic = new p5.AudioIn();
        this._mic.start();
        this._micStarted = true;
    });
};

/**
 * Checks if the sound sensor is started.
 * @method isSoundSensorStarted
 * @return {boolean} True if the sound sensor is started, false otherwise.
 */
p5.prototype.isSoundSensorStarted = function() {
    return this._micStarted;
};

/**
 * Captures the current sound level.
 * @method captureSoundLevel
 * @return {number} The current sound level.
 */
p5.prototype.captureSoundLevel = function() {
    return this._mic.getLevel();
};

/**
 * Gets the current sound level from the microphone.
 * @method getSoundLevel
 * @return {number} The current sound level.
 */
p5.prototype.getSoundLevel = p5.prototype.captureSoundLevel;


/**
 * Gets the current color from the video feed.
 * @method getColor
 * @return {Array} An array containing RGB values.
 */
p5.prototype.getColor = p5.prototype.captureColor;

/**
 * Gets the current light level from the video feed.
 * @method getLightLevel
 * @return {number} The brightness level.
 */
p5.prototype.getLightLevel = p5.prototype.captureLightLevel;

/**
 * Gets the current brightness level from the video feed.
 * @method getBrightness
 * @return {number} The brightness level.
 */
p5.prototype.getBrightness = p5.prototype.captureBrightness;

/**
 * Enables swipe gestures by adding event listeners for touch events.
 * Calls the specified callback function for swipe gestures.
 * @method enableSwipe
 * @param {Function} onSwipe - Callback function for swipe gesture.
 */
p5.prototype.enableSwipe = function(onSwipe) {
  let initialTouch = null;

  const handleTouchStart = (event) => {
    if (event.touches.length === 1) {
      initialTouch = event.touches[0];
    }
  };

  const handleTouchMove = (event) => {
    if (event.touches.length === 1 && initialTouch) {
      const currentTouch = event.touches[0];
      const deltaX = currentTouch.pageX - initialTouch.pageX;
      const deltaY = currentTouch.pageY - initialTouch.pageY;

      let swipeDir = '';
      if (Math.abs(deltaX) > Math.abs(deltaY)) {
        swipeDir = deltaX > 0 ? 'right' : 'left';
      } else {
        swipeDir = deltaY > 0 ? 'down' : 'up';
      }

      onSwipe(swipeDir, deltaX, deltaY);
    }
  };

  const handleTouchEnd = () => {
    initialTouch = null;
  };

  document.addEventListener('touchstart', handleTouchStart, { passive: false });
  document.addEventListener('touchmove', handleTouchMove, { passive: false });
  document.addEventListener('touchend', handleTouchEnd, { passive: false });
};

/**
 * Enables pinch gestures by adding event listeners for touch events.
 * Calls the specified callback function for pinch gestures.
 * @method enablePinch
 * @param {Function} onPinch - Callback function for pinch gesture.
 */
p5.prototype.enablePinch = function(onPinch) {
  let initialDistance = null;
  let initialScale = 1;

  const handleTouchStart = (event) => {
    if (event.touches.length === 2) {
      initialDistance = Math.hypot(
        event.touches[0].pageX - event.touches[1].pageX,
        event.touches[0].pageY - event.touches[1].pageY
      );
      initialScale = 1; // Reset the initial scale
    }
  };

  const handleTouchMove = (event) => {
    if (event.touches.length === 2 && initialDistance !== null) {
      const currentDistance = Math.hypot(
        event.touches[0].pageX - event.touches[1].pageX,
        event.touches[0].pageY - event.touches[1].pageY
      );
      const scale = (currentDistance / initialDistance) * initialScale;
      onPinch(scale);
    }
  };

  const handleTouchEnd = () => {
    initialDistance = null;
  };

  document.addEventListener('touchstart', handleTouchStart, { passive: false });
  document.addEventListener('touchmove', handleTouchMove, { passive: false });
  document.addEventListener('touchend', handleTouchEnd, { passive: false });
};