1// motion.mjs 2 3import { signal } from '../core/signal.mjs'; 4 5/** 6 * The accelerometer's x-axis value ranges from 0 to 1. 7 * @name accelerationX 8 * @return {Pattern} 9 * @synonyms accX 10 * @tags external_io 11 * @example 12 * n(accelerationX.segment(4).range(0,7)).scale("C:minor") 13 * 14 */ 15 16/** 17 * The accelerometer's y-axis value ranges from 0 to 1. 18 * @name accelerationY 19 * @return {Pattern} 20 * @synonyms accY 21 * @tags external_io 22 * @example 23 * n(accelerationY.segment(4).range(0,7)).scale("C:minor") 24 * 25 */ 26 27/** 28 * The accelerometer's z-axis value ranges from 0 to 1. 29 * @name accelerationZ 30 * @return {Pattern} 31 * @synonyms accZ 32 * @tags external_io 33 * @example 34 * n(accelerationZ.segment(4).range(0,7)).scale("C:minor") 35 * 36 */ 37 38/** 39 * The device's gravity x-axis value ranges from 0 to 1. 40 * @name gravityX 41 * @return {Pattern} 42 * @synonyms gravX 43 * @tags external_io 44 * @example 45 * n(gravityX.segment(4).range(0,7)).scale("C:minor") 46 * 47 */ 48 49/** 50 * The device's gravity y-axis value ranges from 0 to 1. 51 * @name gravityY 52 * @return {Pattern} 53 * @synonyms gravY 54 * @tags external_io 55 * @example 56 * n(gravityY.segment(4).range(0,7)).scale("C:minor") 57 * 58 */ 59 60/** 61 * The device's gravity z-axis value ranges from 0 to 1. 62 * @name gravityZ 63 * @return {Pattern} 64 * @synonyms gravZ 65 * @tags external_io 66 * @example 67 * n(gravityZ.segment(4).range(0,7)).scale("C:minor") 68 * 69 */ 70 71/** 72 * The device's rotation around the alpha-axis value ranges from 0 to 1. 73 * @name rotationAlpha 74 * @return {Pattern} 75 * @synonyms rotA, rotZ, rotationZ 76 * @tags external_io 77 * @example 78 * n(rotationAlpha.segment(4).range(0,7)).scale("C:minor") 79 * 80 */ 81 82/** 83 * The device's rotation around the beta-axis value ranges from 0 to 1. 84 * @name rotationBeta 85 * @return {Pattern} 86 * @synonyms rotB, rotX, rotationX 87 * @tags external_io 88 * @example 89 * n(rotationBeta.segment(4).range(0,7)).scale("C:minor") 90 * 91 */ 92 93/** 94 * The device's rotation around the gamma-axis value ranges from 0 to 1. 95 * @name rotationGamma 96 * @return {Pattern} 97 * @synonyms rotG, rotY, rotationY 98 * @tags external_io 99 * @example 100 * n(rotationGamma.segment(4).range(0,7)).scale("C:minor") 101 * 102 */ 103 104/** 105 * The device's orientation alpha value ranges from 0 to 1. 106 * @name orientationAlpha 107 * @return {Pattern} 108 * @synonyms oriA, oriZ, orientationZ 109 * @tags external_io 110 * @example 111 * n(orientationAlpha.segment(4).range(0,7)).scale("C:minor") 112 * 113 */ 114 115/** 116 * The device's orientation beta value ranges from 0 to 1. 117 * @name orientationBeta 118 * @return {Pattern} 119 * @synonyms oriB, oriX, orientationX 120 * @tags external_io 121 * @example 122 * n(orientationBeta.segment(4).range(0,7)).scale("C:minor") 123 * 124 */ 125 126/** 127 * The device's orientation gamma value ranges from 0 to 1. 128 * @name orientationGamma 129 * @return {Pattern} 130 * @synonyms oriG, oriY, orientationY 131 * @tags external_io 132 * @example 133 * n(orientationGamma.segment(4).range(0,7)).scale("C:minor") 134 * 135 */ 136 137/** 138 * The device's absolute orientation alpha value ranges from 0 to 1. 139 * @name absoluteOrientationAlpha 140 * @return {Pattern} 141 * @synonyms absOriA, absOriZ, absoluteOrientationZ 142 * @tags external_io 143 * @example 144 * n(absoluteOrientationAlpha.segment(4).range(0,7)).scale("C:minor") 145 * 146 */ 147 148/** 149 * The device's absolute orientation beta value ranges from 0 to 1. 150 * @name absoluteOrientationBeta 151 * @return {Pattern} 152 * @synonyms absOriB, absOriX, absoluteOrientationX 153 * @tags external_io 154 * @example 155 * n(absoluteOrientationBeta.segment(4).range(0,7)).scale("C:minor") 156 * 157 */ 158 159/** 160 * The device's absolute orientation gamma value ranges from 0 to 1. 161 * @name absoluteOrientationGamma 162 * @return {Pattern} 163 * @synonyms absOriG, absOriY, absoluteOrientationY 164 * @tags external_io 165 * @example 166 * n(absoluteOrientationGamma.segment(4).range(0,7)).scale("C:minor") 167 * 168 */ 169 170class DeviceMotionHandler { 171 constructor() { 172 this.GRAVITY = 9.81; 173 174 // Initialize sensor values 175 this._acceleration = { 176 x: 0, 177 y: 0, 178 z: 0, 179 }; 180 181 this._gravity = { 182 x: 0, 183 y: 0, 184 z: 0, 185 }; 186 187 this._rotation = { 188 alpha: 0, 189 beta: 0, 190 gamma: 0, 191 }; 192 193 this._orientation = { 194 alpha: 0, 195 beta: 0, 196 gamma: 0, 197 }; 198 199 this._absoluteOrientation = { 200 alpha: 0, 201 beta: 0, 202 gamma: 0, 203 }; 204 205 this._permissionStatus = 'unknown'; 206 } 207 208 async requestPermissions() { 209 if (typeof DeviceMotionEvent?.requestPermission === 'function') { 210 try { 211 // iOS requires explicit permission 212 const motionPermission = await DeviceMotionEvent.requestPermission(); 213 const orientationPermission = await DeviceOrientationEvent.requestPermission(); 214 215 this._permissionStatus = 216 motionPermission === 'granted' && orientationPermission === 'granted' ? 'granted' : 'denied'; 217 this.setupEventListeners(); 218 } catch (error) { 219 console.error('Permission request failed:', error); 220 this._permissionStatus = 'denied'; 221 } 222 } else { 223 this._permissionStatus = 'granted'; 224 this.setupEventListeners(); 225 } 226 } 227 228 setupEventListeners() { 229 if (this._permissionStatus === 'granted') { 230 // Device Motion handler 231 window.addEventListener('devicemotion', this.handleDeviceMotion.bind(this), true); 232 window.addEventListener('deviceorientation', this.handleDeviceOrientation.bind(this), true); 233 window.addEventListener('deviceorientationabsolute', this.handleAbsoluteDeviceOrientation.bind(this), true); 234 } 235 } 236 237 handleDeviceMotion(event) { 238 //console.log(event); 239 if (event.acceleration) { 240 // Normalize acceleration values to 0-1 range 241 this._acceleration.x = (event.acceleration.x + 1) / 2; 242 this._acceleration.y = (event.acceleration.y + 1) / 2; 243 this._acceleration.z = (event.acceleration.z + 1) / 2; 244 } 245 246 if (event.accelerationIncludingGravity) { 247 // Normalize acceleration values to 0-1 range 248 this._gravity.x = (event.accelerationIncludingGravity.x + this.GRAVITY) / (2 * this.GRAVITY); 249 this._gravity.y = (event.accelerationIncludingGravity.y + this.GRAVITY) / (2 * this.GRAVITY); 250 this._gravity.z = (event.accelerationIncludingGravity.z + this.GRAVITY) / (2 * this.GRAVITY); 251 } 252 253 if (event.rotationRate) { 254 // Normalize rotation values to 0-1 range 255 this._rotation.alpha = (event.rotationRate.alpha + 180) / 360; 256 this._rotation.beta = (event.rotationRate.beta + 180) / 360; 257 this._rotation.gamma = (event.rotationRate.gamma + 180) / 360; 258 } 259 } 260 261 handleDeviceOrientation(event) { 262 this._orientation.alpha = event.alpha / 360; //a(0~360) 263 this._orientation.beta = (event.beta + 180) / 360; //b(-180~180) 264 this._orientation.gamma = (event.gamma + 90) / 180; //g(-90~90) 265 } 266 267 handleAbsoluteDeviceOrientation(event) { 268 this._absoluteOrientation.alpha = event.alpha / 360; //a(0~360) 269 this._absoluteOrientation.beta = (event.beta + 180) / 360; //b(-180~180) 270 this._absoluteOrientation.gamma = (event.gamma + 90) / 180; //g(-90~90) 271 } 272 273 // Getter methods for current values 274 getAcceleration() { 275 return this._acceleration; 276 } 277 getGravity() { 278 return this._gravity; 279 } 280 getRotation() { 281 return this._rotation; 282 } 283 getOrientation() { 284 return this._orientation; 285 } 286 getAbsoluteOrientation() { 287 return this._absoluteOrientation; 288 } 289} 290 291// Create singleton instance 292const deviceMotion = new DeviceMotionHandler(); 293 294// Export a function to request permission 295export async function enableMotion() { 296 return deviceMotion.requestPermissions(); 297} 298 299// Create signals for acceleration 300export const accelerationX = signal(() => deviceMotion.getAcceleration().x); 301export const accelerationY = signal(() => deviceMotion.getAcceleration().y); 302export const accelerationZ = signal(() => deviceMotion.getAcceleration().z); 303 304// Aliases for shorter names 305export const accX = accelerationX; 306export const accY = accelerationY; 307export const accZ = accelerationZ; 308 309// Create signals for gravity 310export const gravityX = signal(() => deviceMotion.getGravity().x); 311export const gravityY = signal(() => deviceMotion.getGravity().y); 312export const gravityZ = signal(() => deviceMotion.getGravity().z); 313 314// Aliases for shorter names 315export const gravX = gravityX; 316export const gravY = gravityY; 317export const gravZ = gravityZ; 318 319// Create signals for orientation 320export const orientationAlpha = signal(() => deviceMotion.getOrientation().alpha); 321export const orientationBeta = signal(() => deviceMotion.getOrientation().beta); 322export const orientationGamma = signal(() => deviceMotion.getOrientation().gamma); 323// Aliases for shorter names 324export const orientationA = orientationAlpha; 325export const orientationB = orientationBeta; 326export const orientationG = orientationGamma; 327 328// Aliases mapping to X,Y,Z coordinates 329export const orientationX = orientationBeta; 330export const orientationY = orientationGamma; 331export const orientationZ = orientationAlpha; 332 333// Short aliases for A,B,G,X,Y,Z 334 335export const oriA = orientationAlpha; 336export const oriB = orientationBeta; 337export const oriG = orientationGamma; 338 339export const oriX = orientationX; 340export const oriY = orientationY; 341export const oriZ = orientationZ; 342 343// Create signals for absolute orientation 344export const absoluteOrientationAlpha = signal(() => deviceMotion.getAbsoluteOrientation().alpha); 345export const absoluteOrientationBeta = signal(() => deviceMotion.getAbsoluteOrientation().beta); 346export const absoluteOrientationGamma = signal(() => deviceMotion.getAbsoluteOrientation().gamma); 347 348// Aliases for shorter names 349export const absOriA = absoluteOrientationAlpha; 350export const absOriB = absoluteOrientationBeta; 351export const absOriG = absoluteOrientationGamma; 352 353// Aliases mapping to X,Y,Z coordinates 354export const absoluteOrientationX = absoluteOrientationBeta; 355export const absoluteOrientationY = absoluteOrientationGamma; 356export const absoluteOrientationZ = absoluteOrientationAlpha; 357 358// Short aliases for X,Y,Z 359export const absOriX = absoluteOrientationX; 360export const absOriY = absoluteOrientationY; 361export const absOriZ = absoluteOrientationZ; 362 363// Create signals for rotation 364export const rotationAlpha = signal(() => deviceMotion.getRotation().alpha); 365export const rotationBeta = signal(() => deviceMotion.getRotation().beta); 366export const rotationGamma = signal(() => deviceMotion.getRotation().gamma); 367export const rotationX = rotationBeta; 368export const rotationY = rotationGamma; 369export const rotationZ = rotationAlpha; 370 371// Aliases for shorter names 372export const rotA = rotationAlpha; 373export const rotB = rotationBeta; 374export const rotG = rotationGamma; 375export const rotX = rotationX; 376export const rotY = rotationY; 377export const rotZ = rotationZ; 378 379// // Bipolar versions (ranging from -1 to 1 instead of 0 to 1) 380// export const accX2 = accX.toBipolar(); 381// export const accY2 = accY.toBipolar(); 382// export const accZ2 = accZ.toBipolar(); 383 384// export const rotA2 = rotA.toBipolar(); 385// export const rotB2 = rotB.toBipolar(); 386// export const rotG2 = rotG.toBipolar();