diff --git a/packages/motion/README.md b/packages/motion/README.md new file mode 100644 index 000000000..4fc343f93 --- /dev/null +++ b/packages/motion/README.md @@ -0,0 +1,72 @@ +# @strudel/motion + +This package adds device motion sensing functionality to strudel Patterns. + +## Install + +```sh +npm i @strudel/motion --save +``` + +## 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 + +```js +enableMotion() //enable DeviceMotion + +setcpm(200/4) + +$_: accX.segment(16).gain().log() + +$:n("0 1 3 1 5 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") +``` + +## Setup SSL for Local Development + +`DeviceMotionEvent` only works with HTTPS, so you'll need to enable SSL for local development. +Try installing an SSL plugin for Vite. + +```sh +cd website +pnpm install -D @vitejs/plugin-basic-ssl +``` + +add the basicSsl plugin to the defineConfig block in `strudel/website/astro.config.mjs` + +```js +vite: { + plugins: [basicSsl()], + server: { + host: '0.0.0.0', // Ensures it binds to all network interfaces + // https: { + // key: '../../key.pem', // + // cert: '../../cert.pem', + // }, + }, +}, +``` + +generate an SSL certificate to avoid security warnings. + +`openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout key.pem -out cert.pem` diff --git a/packages/motion/docs/devicemotion.mdx b/packages/motion/docs/devicemotion.mdx new file mode 100644 index 000000000..9afccc890 --- /dev/null +++ b/packages/motion/docs/devicemotion.mdx @@ -0,0 +1,82 @@ +import { MiniRepl } from '../../../website/src/docs/MiniRepl'; +import { JsDoc } from '../../../website/src/docs/JsDoc'; + +# Device Motion + +Devicemotion module allows you to use your mobile device's motion sensors (accelerometer, gyroscope, and orientation sensors) to control musical parameters in real-time. This creates opportunities for expressive, movement-based musical interactions. + +## Basic Setup + +First, you need to enable device motion sensing: + + + +This will prompt the user for permission to access device motion sensors. + +## Available Motion Parameters + +You can access different types of motion data: + +| Motion | Long Names & Aliases | Description | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| Acceleration | accelerationX (accX), accelerationY (accY), accelerationZ (accZ) | Measures linear acceleration of the device, excluding gravity. Raw values are normalized from g-force. | +| Gravity | gravityX (gravX), gravityY (gravY), gravityZ (gravZ) | Indicates device's orientation relative to Earth's gravity. Raw values are normalized from ±9.81 m/s². | +| Rotation | rotationAlpha (rotA, rotZ), rotationBeta (rotB, rotX), rotationGamma (rotG, rotY) | Measures rotation rate around each axis. Raw values (±180°/s) are normalized. | +| Orientation | orientationAlpha (oriA, oriZ), orientationBeta (oriB, oriX), orientationGamma (oriG, oriY) | Relative orientation from its starting device position. Normalized from:
- Alpha: 0° to 360°
- Beta: -180° to 180°
- Gamma: -90° to 90° | +| Absolute Orientation | absoluteOrientationAlpha (absOriA, absOriZ), absoluteOrientationBeta (absOriB, absOriX), absoluteOrientationGamma (absOriG, absOriY) | **Not available for iOS**
Earth-referenced orientation using magnetometer. Same normalization as Orientation. | + +Note: + +- All motion values are normalized to a range of 0 to 1. +- Not all devices have the same sensors available + Check [DeviceMotionEvent API](https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent) for browser compatibility +- Refer to [Oritentation and motion data explained](https://developer.mozilla.org/en-US/docs/Web/API/Device_orientation_events/Orientation_and_motion_data_explained) for more details + +### Orientation vs Absolute Orientation + +The key difference between regular orientation and absolute orientation is: + +- Regular orientation (`oriX/Y/Z`) measures relative changes in device orientation from its starting position +- Absolute orientation (`absOriX/Y/Z`) measures orientation relative to Earth's magnetic field and gravity, providing consistent absolute values regardless of starting position + +For example, if you rotate your device 90 degrees clockwise and then back: + +- Regular orientation will show a change during rotation but return to initial values +- Absolute orientation will show the actual compass heading throughout + +This makes absolute orientation particularly useful for creating direction-based musical interactions - for example, performers facing north could play one melody while those facing south play another, creating spatially-aware ensemble performances. Regular orientation, on the other hand, is better suited for detecting relative motion and gestures regardless of which direction the performer is facing. + +## Basic Example + +Here's a simple example that uses device motion to control a synthesizer: + + + +## Tips for Using Motion Controls + +1. Use `.range(min, max)` to map sensor values to musically useful ranges +2. Consider using `.segment()` to smooth out rapid changes in sensor values + +## Debugging + +You can use `segment(16).log()` to see the raw values from any motion sensor: + +```javascript +$_: accX.segment(16).log(); // logs acceleration values to the console +``` + +This is helpful when calibrating your ranges and understanding how your device responds to different movements. + +Remember that device motion works best on mobile devices and may not be available on all desktop browsers. Always test your motion-controlled pieces on the target device type! diff --git a/packages/motion/index.mjs b/packages/motion/index.mjs new file mode 100644 index 000000000..b315b6173 --- /dev/null +++ b/packages/motion/index.mjs @@ -0,0 +1,3 @@ +import './motion.mjs'; + +export * from './motion.mjs'; diff --git a/packages/motion/motion.mjs b/packages/motion/motion.mjs new file mode 100644 index 000000000..875704eae --- /dev/null +++ b/packages/motion/motion.mjs @@ -0,0 +1,371 @@ +// 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 A,B,G,X,Y,Z + +export const oriA = orientationAlpha; +export const oriB = orientationBeta; +export const oriG = orientationGamma; + +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(); diff --git a/packages/motion/package.json b/packages/motion/package.json new file mode 100644 index 000000000..cc117a268 --- /dev/null +++ b/packages/motion/package.json @@ -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 ", + "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": "^6.0.11" + } +} + \ No newline at end of file diff --git a/packages/motion/vite.config.js b/packages/motion/vite.config.js new file mode 100644 index 000000000..5df3edc1b --- /dev/null +++ b/packages/motion/vite.config.js @@ -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', + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f85e3ab6e..494d04f0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -331,6 +331,16 @@ importers: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/motion: + dependencies: + '@strudel/core': + specifier: workspace:* + version: link:../core + devDependencies: + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/mqtt: dependencies: '@strudel/core': @@ -639,6 +649,9 @@ importers: '@strudel/mini': specifier: workspace:* version: link:../packages/mini + '@strudel/motion': + specifier: workspace:* + version: link:../packages/motion '@strudel/mqtt': specifier: workspace:* version: link:../packages/mqtt diff --git a/test/examples.test.mjs b/test/examples.test.mjs index 44ffdd5a9..997c1cf37 100644 --- a/test/examples.test.mjs +++ b/test/examples.test.mjs @@ -2,9 +2,30 @@ import { queryCode } from './runtime.mjs'; import { describe, it } from 'vitest'; import doc from '../doc.json'; +const skippedExamples = [ + 'absoluteOrientationGamma', + 'absoluteOrientationBeta', + 'absoluteOrientationAlpha', + 'orientationGamma', + 'orientationBeta', + 'orientationAlpha', + 'rotationGamma', + 'rotationBeta', + 'rotationAlpha', + 'gravityZ', + 'gravityY', + 'gravityX', + 'accelerationZ', + 'accelerationY', + 'accelerationX', +]; + describe('runs examples', () => { const { docs } = doc; docs.forEach(async (doc) => { + if (skippedExamples.includes(doc.name)) { + return; + } doc.examples?.forEach((example, i) => { it(`example "${doc.name}" example index ${i}`, async ({ expect }) => { const haps = await queryCode(example, 4); diff --git a/test/runtime.mjs b/test/runtime.mjs index 18d4a29e8..b0a329a47 100644 --- a/test/runtime.mjs +++ b/test/runtime.mjs @@ -74,72 +74,31 @@ const toneHelpersMocked = { highpass: mockNode, }; -strudel.Pattern.prototype.osc = function () { - return this; -}; -strudel.Pattern.prototype.csound = function () { - return this; -}; -strudel.Pattern.prototype.tone = function () { - return this; -}; -strudel.Pattern.prototype.webdirt = function () { - return this; -}; - -// draw mock -strudel.Pattern.prototype.pianoroll = function () { - return this; -}; - -// speak mock -strudel.Pattern.prototype.speak = function () { - return this; -}; - -// webaudio mock -strudel.Pattern.prototype.wave = function () { - return this; -}; -strudel.Pattern.prototype.filter = function () { - return this; -}; -strudel.Pattern.prototype.adsr = function () { - return this; -}; -strudel.Pattern.prototype.webaudio = function () { - return this; -}; -strudel.Pattern.prototype.soundfont = function () { - return this; -}; -// tune mock -strudel.Pattern.prototype.tune = function () { - return this; -}; - -strudel.Pattern.prototype.midi = function () { - return this; -}; - -strudel.Pattern.prototype._scope = function () { - return this; -}; -strudel.Pattern.prototype._spiral = function () { - return this; -}; -strudel.Pattern.prototype._pitchwheel = function () { - return this; -}; -strudel.Pattern.prototype._pianoroll = function () { - return this; -}; -strudel.Pattern.prototype._spectrum = function () { - return this; -}; -strudel.Pattern.prototype.markcss = function () { - return this; -}; +[ + 'osc', + 'csound', + 'tone', + 'webdirt', + 'pianoroll', + 'speak', + 'wave', + 'filter', + 'adsr', + 'webaudio', + 'soundfont', + 'tune', + 'midi', + '_scope', + '_spiral', + '_pitchwheel', + '_pianoroll', + '_spectrum', + 'markcss', +].forEach((mock) => { + strudel.Pattern.prototype[mock] = function () { + return this; + }; +}); const uiHelpersMocked = { backgroundImage: id, @@ -193,7 +152,6 @@ evalScope( loadcsound, setcps: id, Clock: {}, // whatever - // Tone, }, ); diff --git a/website/package.json b/website/package.json index 53a66e945..411581f8d 100644 --- a/website/package.json +++ b/website/package.json @@ -32,6 +32,7 @@ "@strudel/hydra": "workspace:*", "@strudel/midi": "workspace:*", "@strudel/mini": "workspace:*", + "@strudel/motion": "workspace:*", "@strudel/mqtt": "workspace:*", "@strudel/osc": "workspace:*", "@strudel/serial": "workspace:*", diff --git a/website/src/config.ts b/website/src/config.ts index a404f542a..fd6cc479f 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -84,6 +84,7 @@ export const SIDEBAR: Sidebar = { { text: 'Music metadata', link: 'learn/metadata' }, { text: 'CSound', link: 'learn/csound' }, { text: 'Hydra', link: 'learn/hydra' }, + { text: 'Device Motion', link: 'learn/devicemotion' }, ], 'Pattern Functions': [ { text: 'Introduction', link: 'functions/intro' }, diff --git a/website/src/pages/learn/devicemotion.mdx b/website/src/pages/learn/devicemotion.mdx new file mode 100644 index 000000000..cd54a7cf8 --- /dev/null +++ b/website/src/pages/learn/devicemotion.mdx @@ -0,0 +1,10 @@ +--- +title: Device Motion +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; +import DeviceMotion from '../../../../packages/motion/docs/devicemotion.mdx'; + + diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index b91273f97..f623e468f 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -81,6 +81,7 @@ export function loadModules() { import('@strudel/soundfonts'), import('@strudel/csound'), import('@strudel/tidal'), + import('@strudel/motion'), import('@strudel/mqtt'), ]; if (isTauri()) {