mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 13:48:34 +00:00
Add Device Motion package
Introduces smart-phone device's acceleration, gravity, rotation, and orientation signals for dynamic pattern creation in Strudel.
This commit is contained in:
parent
726abf76ce
commit
f33ec0e2af
66
packages/motion/README.md
Normal file
66
packages/motion/README.md
Normal file
@ -0,0 +1,66 @@
|
||||
# @strudel/motion
|
||||
|
||||
This package adds device motion sensing functionality to strudel Patterns.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm i @strudel/motion --save
|
||||
```
|
||||
|
||||
## Setup SSL for Local Development
|
||||
`DeviceMotionEvent` only work over HTTPS, so you'll need to set up SSL for local development.
|
||||
install SSL plugin for Vite
|
||||
`pnpm install -D @vitejs/plugin-basic-ssl`
|
||||
|
||||
add the basicSsl plugin to the defineConfig block in `strudel/website/astro.config.mjs`
|
||||
```
|
||||
vite: {
|
||||
plugins: [basicSsl()],
|
||||
server: {
|
||||
host: '0.0.0.0', // Ensures it binds to all network interfaces
|
||||
// https: {
|
||||
// key: '../../key.pem', //
|
||||
// cert: '../../cert.pem',
|
||||
// },
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
generate SSL cert if its necessary
|
||||
|
||||
`openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout key.pem -out cert.pem`
|
||||
|
||||
## Usage
|
||||
|
||||
| Motion | Long Names & Aliases | Description |
|
||||
|----------------------------|-----------------------------------------------------------|------------------------------------------|
|
||||
| Acceleration | accelerationX (accX), accelerationY (accY), accelerationZ (accZ) | X, Y, Z-axis acceleration values |
|
||||
| Gravity | gravityX (gravX), gravityY (gravY), gravityZ (gravZ) | X, Y, Z-axis gravity values |
|
||||
| Rotation | rotationAlpha (rotA, rotZ), rotationBeta (rotB, rotX), rotationGamma (rotG, rotY) | Rotation around alpha, beta, gamma axes and mapped to X, Y, Z |
|
||||
| Orientation | orientationAlpha (oriA, oriZ), orientationBeta (oriB, oriX), orientationGamma (oriG, oriY) | Orientation alpha, beta, gamma values and mapped to X, Y, Z |
|
||||
| Absolute Orientation | absoluteOrientationAlpha (absOriA, absOriZ), absoluteOrientationBeta (absOriB, absOriX), absoluteOrientationGamma (absOriG, absOriY) | Absolute orientation alpha, beta, gamma values and mapped to X, Y, Z |
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
enableMotion() //enable DeviceMotion
|
||||
|
||||
let tempo = 200
|
||||
|
||||
$_: accX.segment(16).gain().log()
|
||||
|
||||
$:n("[0 1 3 1 5 4]/4")
|
||||
.scale("Bb:lydian")
|
||||
.sometimesBy(0.5,sub(note(12)))
|
||||
.lpf(gravityY.range(20,1000))
|
||||
.lpq(gravityZ.range(1,30))
|
||||
.lpenv(gravityX.range(2,2))
|
||||
.gain(oriX.range(0.2,0.8))
|
||||
.room(oriZ.range(0,0.5))
|
||||
.attack(oriY.range(0,0.3))
|
||||
.delay(rotG.range(0,1))
|
||||
.decay(rotA.range(0,1))
|
||||
.attack(rotB.range(0,0.1))
|
||||
.sound("sawtooth").cpm(tempo)
|
||||
```
|
||||
3
packages/motion/index.mjs
Normal file
3
packages/motion/index.mjs
Normal file
@ -0,0 +1,3 @@
|
||||
import './motion.mjs';
|
||||
|
||||
export * from './motion.mjs';
|
||||
366
packages/motion/motion.mjs
Normal file
366
packages/motion/motion.mjs
Normal file
@ -0,0 +1,366 @@
|
||||
// motion.mjs
|
||||
|
||||
import { signal } from '../core/signal.mjs';
|
||||
|
||||
/**
|
||||
* The accelerometer's x-axis value ranges from 0 to 1.
|
||||
* @name accelerationX
|
||||
* @return {Pattern}
|
||||
* @synonyms accX
|
||||
* @example
|
||||
* n(accelerationX.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* The accelerometer's y-axis value ranges from 0 to 1.
|
||||
* @name accelerationY
|
||||
* @return {Pattern}
|
||||
* @synonyms accY
|
||||
* @example
|
||||
* n(accelerationY.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* The accelerometer's z-axis value ranges from 0 to 1.
|
||||
* @name accelerationZ
|
||||
* @return {Pattern}
|
||||
* @synonyms accZ
|
||||
* @example
|
||||
* n(accelerationZ.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* The device's gravity x-axis value ranges from 0 to 1.
|
||||
* @name gravityX
|
||||
* @return {Pattern}
|
||||
* @synonyms gravX
|
||||
* @example
|
||||
* n(gravityX.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* The device's gravity y-axis value ranges from 0 to 1.
|
||||
* @name gravityY
|
||||
* @return {Pattern}
|
||||
* @synonyms gravY
|
||||
* @example
|
||||
* n(gravityY.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* The device's gravity z-axis value ranges from 0 to 1.
|
||||
* @name gravityZ
|
||||
* @return {Pattern}
|
||||
* @synonyms gravZ
|
||||
* @example
|
||||
* n(gravityZ.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* The device's rotation around the alpha-axis value ranges from 0 to 1.
|
||||
* @name rotationAlpha
|
||||
* @return {Pattern}
|
||||
* @synonyms rotA, rotZ, rotationZ
|
||||
* @example
|
||||
* n(rotationAlpha.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* The device's rotation around the beta-axis value ranges from 0 to 1.
|
||||
* @name rotationBeta
|
||||
* @return {Pattern}
|
||||
* @synonyms rotB, rotX, rotationX
|
||||
* @example
|
||||
* n(rotationBeta.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* The device's rotation around the gamma-axis value ranges from 0 to 1.
|
||||
* @name rotationGamma
|
||||
* @return {Pattern}
|
||||
* @synonyms rotG, rotY, rotationY
|
||||
* @example
|
||||
* n(rotationGamma.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* The device's orientation alpha value ranges from 0 to 1.
|
||||
* @name orientationAlpha
|
||||
* @return {Pattern}
|
||||
* @synonyms oriA, oriZ, orientationZ
|
||||
* @example
|
||||
* n(orientationAlpha.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* The device's orientation beta value ranges from 0 to 1.
|
||||
* @name orientationBeta
|
||||
* @return {Pattern}
|
||||
* @synonyms oriB, oriX, orientationX
|
||||
* @example
|
||||
* n(orientationBeta.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* The device's orientation gamma value ranges from 0 to 1.
|
||||
* @name orientationGamma
|
||||
* @return {Pattern}
|
||||
* @synonyms oriG, oriY, orientationY
|
||||
* @example
|
||||
* n(orientationGamma.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* The device's absolute orientation alpha value ranges from 0 to 1.
|
||||
* @name absoluteOrientationAlpha
|
||||
* @return {Pattern}
|
||||
* @synonyms absOriA, absOriZ, absoluteOrientationZ
|
||||
* @example
|
||||
* n(absoluteOrientationAlpha.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* The device's absolute orientation beta value ranges from 0 to 1.
|
||||
* @name absoluteOrientationBeta
|
||||
* @return {Pattern}
|
||||
* @synonyms absOriB, absOriX, absoluteOrientationX
|
||||
* @example
|
||||
* n(absoluteOrientationBeta.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* The device's absolute orientation gamma value ranges from 0 to 1.
|
||||
* @name absoluteOrientationGamma
|
||||
* @return {Pattern}
|
||||
* @synonyms absOriG, absOriY, absoluteOrientationY
|
||||
* @example
|
||||
* n(absoluteOrientationGamma.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
|
||||
class DeviceMotionHandler {
|
||||
constructor() {
|
||||
this.GRAVITY = 9.81;
|
||||
|
||||
// Initialize sensor values
|
||||
this._acceleration = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
};
|
||||
|
||||
this._gravity = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
};
|
||||
|
||||
this._rotation = {
|
||||
alpha: 0,
|
||||
beta: 0,
|
||||
gamma: 0,
|
||||
};
|
||||
|
||||
this._orientation = {
|
||||
alpha: 0,
|
||||
beta: 0,
|
||||
gamma: 0,
|
||||
};
|
||||
|
||||
this._absoluteOrientation = {
|
||||
alpha: 0,
|
||||
beta: 0,
|
||||
gamma: 0,
|
||||
};
|
||||
|
||||
this._permissionStatus = 'unknown';
|
||||
}
|
||||
|
||||
async requestPermissions() {
|
||||
if (typeof DeviceMotionEvent?.requestPermission === 'function') {
|
||||
try {
|
||||
// iOS requires explicit permission
|
||||
const motionPermission = await DeviceMotionEvent.requestPermission();
|
||||
const orientationPermission = await DeviceOrientationEvent.requestPermission();
|
||||
|
||||
this._permissionStatus =
|
||||
motionPermission === 'granted' && orientationPermission === 'granted' ? 'granted' : 'denied';
|
||||
this.setupEventListeners();
|
||||
} catch (error) {
|
||||
console.error('Permission request failed:', error);
|
||||
this._permissionStatus = 'denied';
|
||||
}
|
||||
} else {
|
||||
this._permissionStatus = 'granted';
|
||||
this.setupEventListeners();
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
if (this._permissionStatus === 'granted') {
|
||||
// Device Motion handler
|
||||
window.addEventListener('devicemotion', this.handleDeviceMotion.bind(this), true);
|
||||
window.addEventListener('deviceorientation', this.handleDeviceOrientation.bind(this), true);
|
||||
window.addEventListener('deviceorientationabsolute', this.handleAbsoluteDeviceOrientation.bind(this), true);
|
||||
}
|
||||
}
|
||||
|
||||
handleDeviceMotion(event) {
|
||||
//console.log(event);
|
||||
if (event.acceleration) {
|
||||
// Normalize acceleration values to 0-1 range
|
||||
this._acceleration.x = (event.acceleration.x + 1) / 2;
|
||||
this._acceleration.y = (event.acceleration.y + 1) / 2;
|
||||
this._acceleration.z = (event.acceleration.z + 1) / 2;
|
||||
}
|
||||
|
||||
if (event.accelerationIncludingGravity) {
|
||||
// Normalize acceleration values to 0-1 range
|
||||
this._gravity.x = (event.accelerationIncludingGravity.x + this.GRAVITY) / (2 * this.GRAVITY);
|
||||
this._gravity.y = (event.accelerationIncludingGravity.y + this.GRAVITY) / (2 * this.GRAVITY);
|
||||
this._gravity.z = (event.accelerationIncludingGravity.z + this.GRAVITY) / (2 * this.GRAVITY);
|
||||
}
|
||||
|
||||
if (event.rotationRate) {
|
||||
// Normalize rotation values to 0-1 range
|
||||
this._rotation.alpha = (event.rotationRate.alpha + 180) / 360;
|
||||
this._rotation.beta = (event.rotationRate.beta + 180) / 360;
|
||||
this._rotation.gamma = (event.rotationRate.gamma + 180) / 360;
|
||||
}
|
||||
}
|
||||
|
||||
handleDeviceOrientation(event) {
|
||||
this._orientation.alpha = event.alpha / 360; //a(0~360)
|
||||
this._orientation.beta = (event.beta + 180) / 360; //b(-180~180)
|
||||
this._orientation.gamma = (event.gamma + 90) / 180; //g(-90~90)
|
||||
}
|
||||
|
||||
handleAbsoluteDeviceOrientation(event) {
|
||||
this._absoluteOrientation.alpha = event.alpha / 360; //a(0~360)
|
||||
this._absoluteOrientation.beta = (event.beta + 180) / 360; //b(-180~180)
|
||||
this._absoluteOrientation.gamma = (event.gamma + 90) / 180; //g(-90~90)
|
||||
}
|
||||
|
||||
// Getter methods for current values
|
||||
getAcceleration() {
|
||||
return this._acceleration;
|
||||
}
|
||||
getGravity() {
|
||||
return this._gravity;
|
||||
}
|
||||
getRotation() {
|
||||
return this._rotation;
|
||||
}
|
||||
getOrientation() {
|
||||
return this._orientation;
|
||||
}
|
||||
getAbsoluteOrientation() {
|
||||
return this._absoluteOrientation;
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
const deviceMotion = new DeviceMotionHandler();
|
||||
|
||||
// Export a function to request permission
|
||||
export async function enableMotion() {
|
||||
return deviceMotion.requestPermissions();
|
||||
}
|
||||
|
||||
// Create signals for acceleration
|
||||
export const accelerationX = signal(() => deviceMotion.getAcceleration().x);
|
||||
export const accelerationY = signal(() => deviceMotion.getAcceleration().y);
|
||||
export const accelerationZ = signal(() => deviceMotion.getAcceleration().z);
|
||||
|
||||
// Aliases for shorter names
|
||||
export const accX = accelerationX;
|
||||
export const accY = accelerationY;
|
||||
export const accZ = accelerationZ;
|
||||
|
||||
// Create signals for gravity
|
||||
export const gravityX = signal(() => deviceMotion.getGravity().x);
|
||||
export const gravityY = signal(() => deviceMotion.getGravity().y);
|
||||
export const gravityZ = signal(() => deviceMotion.getGravity().z);
|
||||
|
||||
// Aliases for shorter names
|
||||
export const gravX = gravityX;
|
||||
export const gravY = gravityY;
|
||||
export const gravZ = gravityZ;
|
||||
|
||||
// Create signals for orientation
|
||||
export const orientationAlpha = signal(() => deviceMotion.getOrientation().alpha);
|
||||
export const orientationBeta = signal(() => deviceMotion.getOrientation().beta);
|
||||
export const orientationGamma = signal(() => deviceMotion.getOrientation().gamma);
|
||||
// Aliases for shorter names
|
||||
export const orientationA = orientationAlpha;
|
||||
export const orientationB = orientationBeta;
|
||||
export const orientationG = orientationGamma;
|
||||
|
||||
// Aliases mapping to X,Y,Z coordinates
|
||||
export const orientationX = orientationBeta;
|
||||
export const orientationY = orientationGamma;
|
||||
export const orientationZ = orientationAlpha;
|
||||
|
||||
// Short aliases for X,Y,Z
|
||||
export const oriX = orientationX;
|
||||
export const oriY = orientationY;
|
||||
export const oriZ = orientationZ;
|
||||
|
||||
// Create signals for absolute orientation
|
||||
export const absoluteOrientationAlpha = signal(() => deviceMotion.getAbsoluteOrientation().alpha);
|
||||
export const absoluteOrientationBeta = signal(() => deviceMotion.getAbsoluteOrientation().beta);
|
||||
export const absoluteOrientationGamma = signal(() => deviceMotion.getAbsoluteOrientation().gamma);
|
||||
|
||||
// Aliases for shorter names
|
||||
export const absOriA = absoluteOrientationAlpha;
|
||||
export const absOriB = absoluteOrientationBeta;
|
||||
export const absOriG = absoluteOrientationGamma;
|
||||
|
||||
// Aliases mapping to X,Y,Z coordinates
|
||||
export const absoluteOrientationX = absoluteOrientationBeta;
|
||||
export const absoluteOrientationY = absoluteOrientationGamma;
|
||||
export const absoluteOrientationZ = absoluteOrientationAlpha;
|
||||
|
||||
// Short aliases for X,Y,Z
|
||||
export const absOriX = absoluteOrientationX;
|
||||
export const absOriY = absoluteOrientationY;
|
||||
export const absOriZ = absoluteOrientationZ;
|
||||
|
||||
// Create signals for rotation
|
||||
export const rotationAlpha = signal(() => deviceMotion.getRotation().alpha);
|
||||
export const rotationBeta = signal(() => deviceMotion.getRotation().beta);
|
||||
export const rotationGamma = signal(() => deviceMotion.getRotation().gamma);
|
||||
export const rotationX = rotationBeta;
|
||||
export const rotationY = rotationGamma;
|
||||
export const rotationZ = rotationAlpha;
|
||||
|
||||
// Aliases for shorter names
|
||||
export const rotA = rotationAlpha;
|
||||
export const rotB = rotationBeta;
|
||||
export const rotG = rotationGamma;
|
||||
export const rotX = rotationX;
|
||||
export const rotY = rotationY;
|
||||
export const rotZ = rotationZ;
|
||||
|
||||
// // Bipolar versions (ranging from -1 to 1 instead of 0 to 1)
|
||||
// export const accX2 = accX.toBipolar();
|
||||
// export const accY2 = accY.toBipolar();
|
||||
// export const accZ2 = accZ.toBipolar();
|
||||
|
||||
// export const rotA2 = rotA.toBipolar();
|
||||
// export const rotB2 = rotB.toBipolar();
|
||||
// export const rotG2 = rotG.toBipolar();
|
||||
38
packages/motion/package.json
Normal file
38
packages/motion/package.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@strudel/motion",
|
||||
"version": "1.1.0",
|
||||
"description": "DeviceMotion API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"titdalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Yuta Nakayama <nkymut@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10"
|
||||
}
|
||||
}
|
||||
|
||||
19
packages/motion/vite.config.js
Normal file
19
packages/motion/vite.config.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { dependencies } from './package.json';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'index.mjs'),
|
||||
formats: ['es'],
|
||||
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
@ -33,14 +33,15 @@
|
||||
"@strudel/hydra": "workspace:*",
|
||||
"@strudel/midi": "workspace:*",
|
||||
"@strudel/mini": "workspace:*",
|
||||
"@strudel/motion": "workspace:*",
|
||||
"@strudel/osc": "workspace:*",
|
||||
"@strudel/serial": "workspace:*",
|
||||
"@strudel/soundfonts": "workspace:*",
|
||||
"@strudel/tidal": "workspace:*",
|
||||
"@strudel/tonal": "workspace:*",
|
||||
"@strudel/transpiler": "workspace:*",
|
||||
"@strudel/webaudio": "workspace:*",
|
||||
"@strudel/xen": "workspace:*",
|
||||
"@strudel/tidal": "workspace:*",
|
||||
"@supabase/supabase-js": "^2.39.1",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
|
||||
@ -81,6 +81,7 @@ export function loadModules() {
|
||||
import('@strudel/soundfonts'),
|
||||
import('@strudel/csound'),
|
||||
import('@strudel/tidal'),
|
||||
import('@strudel/motion'),
|
||||
];
|
||||
if (isTauri()) {
|
||||
modules = modules.concat([
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user