8 Commits

Author SHA1 Message Date
fbf32d7946 Added check for negative deviation 2023-11-25 09:10:58 +02:00
69c24f6a15 Changed clock to use performance.now() 2023-11-24 22:22:17 +02:00
f3ddb39ab6 Reintroduce nudge 2023-11-20 11:12:16 +01:00
8b744e3d90 Delete TransportNode and TransportProcessor 2023-11-20 11:04:15 +01:00
424adeebc0 Cleaning Clock file 2023-11-20 11:03:46 +01:00
ed8bc21713 work in progress 2023-11-20 02:29:47 +01:00
9b7f980027 working on clock 2023-11-20 01:42:44 +01:00
05382bab6e tiny correction 2023-11-19 23:43:18 +01:00
195 changed files with 7369 additions and 27944 deletions

View File

@ -3,23 +3,28 @@ name: Build and Push Docker Images
on:
push:
branches:
- "main"
- 'main'
jobs:
topos:
runs-on: ubuntu-latest
steps:
- name: Checkout
-
name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
-
name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
-
name: Build and push
uses: docker/build-push-action@v5
with:
push: true

View File

@ -47,6 +47,9 @@ jobs:
with:
path: "main"
- name: Copy favicon folder
run: cp -r main/favicon ./dist/
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:

View File

@ -1 +1,2 @@
{}
{
}

View File

@ -2,8 +2,8 @@
<p align="center"> |
<a href="https://discord.gg/aPgV7mSFZh">Discord</a> |
<a href="https://raphaelforment.fr/">BuboBubo</a> |
<a href="https://github.com/amiika">Amiika</a> |
<a href="https://raphaelforment.fr/">BuboBubo</a> | 
<a href="about:blank">Amiika</a> |
<a href="https://toplap.org/">About Live Coding</a> |
<br><br>
<h2 align="center"><b>Contributors</b></h2>
@ -12,90 +12,57 @@
<img src="https://contrib.rocks/image?repo=bubobubobubobubo/Topos" />
</a>
</p>
<p align="center">
<a href='https://ko-fi.com/I2I2RSBHF' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi3.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
</p>
</p>
---
Topos is a web-based live coding environment. It lives [here](https://topos.live). Documentation is directly embedded in the application itself. Topos is an emulation and extension of the [Monome Teletype](https://monome.org/docs/teletype/) that gradually evolved into something a bit more personal.
Topos is a web based live coding environment designed to be installation-free, independant and fun. Topos is loosely based on the [Monome Teletype](https://monome.org/docs/teletype/). The application follows the same operating principle, but adapts it to the rich multimedia context offered by web browsers. Topos is capable of many things:
- it is a generative/algorithmic music sequencer made for **improvisation** and **composition** alike
- it is a synthesizer capable of _additive_, _substractive_, _FM_ and _wavetable
synthesis_, backed up by a [powerful web based audio engine](https://www.npmjs.com/package/superdough)
- it can also generate video thanks to [Hydra](https://hydra.ojack.xyz/),
oscilloscopes, frequency visualizers and image/canvas sequencing capabilities
- it can be used to sequence other MIDI and OSC devices (the latter using a **NodeJS** script)
- it is made to be used without the need of installing anything, always ready at
[https://topos.live](https://topos.live)
---
![Screenshot](https://github.com/Bubobubobubobubo/Topos/blob/main/src/assets/topos_gif.gif)
![Screenshot](https://github.com/Bubobubobubobubo/Topos/blob/main/img/topos_gif.gif)
## Disclaimer
**Topos** is still a young and experimental project developed by two hobbyists :) Contributions are welcome! We wish to be as inclusive and welcoming as possible to your ideas and suggestions! The software is working quite well and we are continuously striving to improve it. Note that most features are rather experimental and that we don't really have any classical training in web development.
**Topos** is a fairly young project developed by two part time hobbyists :) Do not expect stable features and/or user support in the initial development stage. Contributors and curious people are welcome! The software is working quite well and we are continuously striving to improve it.
## Local Installation (for devs and contributors)
## Installation (for devs and contributors)
To run the application, you will need to install [Node.js](https://nodejs.org/en/) and [Yarn](https://yarnpkg.com/en/). Then, clone the repository and run:
- `yarn install`
- `yarn run dev`
You are good to go. The application will update itself automatically with every change to the codebase. To test the production version of the applicationn, you will need to install [Node.js](https://nodejs.org/en/) and [Yarn](https://yarnpkg.com/en/). Then, clone the repository and run:
To build the application for production, you will need to install [Node.js](https://nodejs.org/en/) and [Yarn](https://yarnpkg.com/en/). Then, clone the repository and run:
- `yarn run build`
- `yarn run start`
If the build passes, you can be sure that it will also pass our **CI** pipeline that deploys the application to [https://topos.live](https://topos.live). Always run a build before committing to check for compiler errors. The automatic deployment on the `main` branch will not accept compiler errors!
Always run a build before committing to check for compiler errors. The automatic deployment on the `main` branch will not accept compiler errors!
## Tauri version
Topos can also be compiled as a standalone application using [Tauri](https://tauri.app/). You will need [Node.js](https://nodejs.org/en/), [Yarn](https://yarnpkg.com/en/) and [Rust](https://www.rust-lang.org/) to be installed on your computer. Then, clone the repository and run:
To build a standalone browser application using [Tauri](https://tauri.app/), you will need to have [Node.js](https://nodejs.org/en/), [Yarn](https://yarnpkg.com/en/) and [Rust](https://www.rust-lang.org/) installed. Then, clone the repository and run:
- `yarn tauri build`
- `yarn tauri dev`
The `tauri` version has never been fleshed out. It's a template for later developments if Topos ever wants to escape from the web :)
The `tauri` version is only here to quickstart future developments but nothing has been done yet.
## Docker
To run the **Docker** version, run the following command:
`docker run -p 8001:80 bubobubobubo/topos:latest`
### Run the application
`docker run -p 8001:80 yassinsiouda/topos:latest`
### Build and run the prod image
`docker compose --profile prod up`
### Build and run the dev image
First you need to map `node_modules` to your local machine for your IDE IntelliSense to work properly :
**First installation**
First you need to map node_modules to your local machine for your ide intellisense to work properly
```bash
docker compose --profile dev up -d
docker cp topos-dev:/app/node_modules .
docker compose --profile dev down
```
then run the following command:
**Then**
```bash
docker compose --profile dev up
```
Note that a Docker version of Topos is automatically generated everytime a commit is done on the `main` branch.
## Credits
- Felix Roos for the [SuperDough](https://www.npmjs.com/package/superdough) audio engine.
- Frank Force for the [ZzFX](https://github.com/KilledByAPixel/ZzFX) synthesizer.
- Kristoffer Ekstrand for the [AKWF](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) waveforms.
- Ryan Kirkbride for some of the audio samples in the [Dough-Fox](https://github.com/Bubobubobubobubo/Dough-Fox) sample pack, taken from [here](https://github.com/Qirky/FoxDot/tree/master/FoxDot/snd).
- Adel Faure for the [JGS](https://adelfaure.net/https://adelfaure.net/) font.
- Raphaël Bastide for the [Steps Mono](https://github.com/raphaelbastide/steps-mono/) font.
Many thanks to all the contributors and folks who tried the software already :)

View File

@ -1,36 +0,0 @@
const WebSocket = require("ws");
const osc = require("osc");
const cleanIncomingOSC = (oscMsg) => {
let data = oscMsg.args;
// Remove information about type of data
data = data.map((item) => {
return item.value;
});
return { data: data, address: oscMsg.address };
};
// ==============================================
// Receiving and forwarding OSC UDP messages
// Create an osc.js UDP Port listening on port 57121.
console.log("> OSC Input: 127.0.0.1:30000");
const wss = new WebSocket.Server({ port: 3001 });
var udpPort = new osc.UDPPort({
localAddress: "0.0.0.0",
localPort: 30000,
metadata: true,
});
udpPort.on("message", function (oscMsg, timeTag, info) {
console.log(
`> Incoming OSC to ${oscMsg.address}:`,
oscMsg.args.map((item) => {
return item.value;
}),
);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(cleanIncomingOSC(oscMsg)));
}
});
});
udpPort.open();

View File

@ -1,83 +0,0 @@
const WebSocket = require("ws");
const osc = require("osc");
// Listening to WebSocket messages
const wss = new WebSocket.Server({ port: 3000 });
// Setting up for message broadcasting
wss.on("connection", function (ws) {
console.log("> Client connected");
ws.on("message", function (data) {
try {
const message = JSON.parse(data);
sendOscMessage(
formatAndTypeMessage(message),
message.address,
message.port,
);
console.log(
`> Message sent to ${message.address}:${message.port}: ${JSON.stringify(
message.args,
)}`,
);
} catch (error) {
console.error("> Error processing message:", error);
}
});
});
wss.on("error", function (error) {
console.error("> Server error:", error);
});
wss.on("close", function () {
// Close the websocket server
wss.close();
console.log("> Closing websocket server");
});
let udpPort = new osc.UDPPort({
localAddress: "0.0.0.0",
localPort: 3000,
metadata: true,
remoteAddress: "0.0.0.0",
remotePort: 57120,
});
udpPort.on("error", function (error) {
console.error("> UDP Port error:", error);
});
udpPort.on("ready", function () {
//console.log(`> UDP Receive: ${udpPort.options.localPort}`);
console.log("> WebSocket server: 127.0.0.1:3000");
});
udpPort.open();
function sendOscMessage(message, address, port) {
try {
udpPort.options.remotePort = port;
message.address = address;
udpPort.send(message);
} catch (error) {
console.error("> Error sending OSC message:", error);
}
}
const formatAndTypeMessage = (message) => {
let newMessage = {};
delete message.args["address"];
delete message.args["port"];
newMessage.address = message.address;
newMessage.timestamp = osc.timeTag(message.timetag);
args = [...Object.entries(message.args)].flat().map((arg) => {
if (typeof arg === "string") return { type: "s", value: arg };
if (typeof arg === "number") return { type: "f", value: arg };
if (typeof arg === "boolean")
return value ? { type: "s", value: 1 } : { type: "s", value: 0 };
});
newMessage.args = args;
return newMessage;
};

View File

@ -1,14 +0,0 @@
var pjson = require("./package.json");
let banner = `
┏┳┓ ┏┓┏┓┏┓
┃ ┏┓┏┓┏┓┏ ┃┃┗┓┃
┻ ┗┛┣┛┗┛┛ ┗┛┗┛┗┛
${pjson.version}\n`;
function greet() {
console.log(banner);
}
module.exports = {
greet: greet,
};

View File

@ -1,332 +0,0 @@
{
"name": "topos-server",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "topos-server",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"dependencies": {
"osc": "^2.4.4",
"ws": "^8.14.2"
}
},
"node_modules/@serialport/binding-mock": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz",
"integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==",
"optional": true,
"dependencies": {
"@serialport/bindings-interface": "^1.2.1",
"debug": "^4.3.3"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@serialport/bindings-cpp": {
"version": "10.8.0",
"resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-10.8.0.tgz",
"integrity": "sha512-OMQNJz5kJblbmZN5UgJXLwi2XNtVLxSKmq5VyWuXQVsUIJD4l9UGHnLPqM5LD9u3HPZgDI5w7iYN7gxkQNZJUw==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"@serialport/bindings-interface": "1.2.2",
"@serialport/parser-readline": "^10.2.1",
"debug": "^4.3.2",
"node-addon-api": "^5.0.0",
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=12.17.0 <13.0 || >=14.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/bindings-interface": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz",
"integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==",
"optional": true,
"engines": {
"node": "^12.22 || ^14.13 || >=16"
}
},
"node_modules/@serialport/parser-byte-length": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-10.5.0.tgz",
"integrity": "sha512-eHhr4lHKboq1OagyaXAqkemQ1XyoqbLQC8XJbvccm95o476TmEdW5d7AElwZV28kWprPW68ZXdGF2VXCkJgS2w==",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-cctalk": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-10.5.0.tgz",
"integrity": "sha512-Iwsdr03xmCKAiibLSr7b3w6ZUTBNiS+PwbDQXdKU/clutXjuoex83XvsOtYVcNZmwJlVNhAUbkG+FJzWwIa4DA==",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-delimiter": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-10.5.0.tgz",
"integrity": "sha512-/uR/yT3jmrcwnl2FJU/2ySvwgo5+XpksDUR4NF/nwTS5i3CcuKS+FKi/tLzy1k8F+rCx5JzpiK+koqPqOUWArA==",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-inter-byte-timeout": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-10.5.0.tgz",
"integrity": "sha512-WPvVlSx98HmmUF9jjK6y9mMp3Wnv6JQA0cUxLeZBgS74TibOuYG3fuUxUWGJALgAXotOYMxfXSezJ/vSnQrkhQ==",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-packet-length": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-10.5.0.tgz",
"integrity": "sha512-jkpC/8w4/gUBRa2Teyn7URv1D7T//0lGj27/4u9AojpDVXsR6dtdcTG7b7dNirXDlOrSLvvN7aS5/GNaRlEByw==",
"optional": true,
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/@serialport/parser-readline": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-10.5.0.tgz",
"integrity": "sha512-0aXJknodcl94W9zSjvU+sLdXiyEG2rqjQmvBWZCr8wJZjWEtv3RgrnYiWq4i2OTOyC8C/oPK8ZjpBjQptRsoJQ==",
"optional": true,
"dependencies": {
"@serialport/parser-delimiter": "10.5.0"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-ready": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-10.5.0.tgz",
"integrity": "sha512-QIf65LTvUoxqWWHBpgYOL+soldLIIyD1bwuWelukem2yDZVWwEjR288cLQ558BgYxH4U+jLAQahhqoyN1I7BaA==",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-regex": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-10.5.0.tgz",
"integrity": "sha512-9jnr9+PCxRoLjtGs7uxwsFqvho+rxuJlW6ZWSB7oqfzshEZWXtTJgJRgac/RuLft4hRlrmRz5XU40i3uoL4HKw==",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-slip-encoder": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-10.5.0.tgz",
"integrity": "sha512-wP8m+uXQdkWSa//3n+VvfjLthlabwd9NiG6kegf0fYweLWio8j4pJRL7t9eTh2Lbc7zdxuO0r8ducFzO0m8CQw==",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-spacepacket": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-10.5.0.tgz",
"integrity": "sha512-BEZ/HAEMwOd8xfuJSeI/823IR/jtnThovh7ils90rXD4DPL1ZmrP4abAIEktwe42RobZjIPfA4PaVfyO0Fjfhg==",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/stream": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-10.5.0.tgz",
"integrity": "sha512-gbcUdvq9Kyv2HsnywS7QjnEB28g+6OGB5Z8TLP7X+UPpoMIWoUsoQIq5Kt0ZTgMoWn3JGM2lqwTsSHF+1qhniA==",
"optional": true,
"dependencies": {
"@serialport/bindings-interface": "1.2.2",
"debug": "^4.3.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"optional": true,
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"optional": true
},
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"optional": true
},
"node_modules/node-gyp-build": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.7.0.tgz",
"integrity": "sha512-PbZERfeFdrHQOOXiAKOY0VPbykZy90ndPKk0d+CFDegTKmWp1VgOTz2xACVbr1BjCWxrQp68CXtvNsveFhqDJg==",
"optional": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/osc": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/osc/-/osc-2.4.4.tgz",
"integrity": "sha512-YJr2bUCQMc9BIaq1LXgqYpt5Ii7wNy2n0e0BkQiCSziMNrrsYHhH5OlExNBgCrQsum60EgXZ32lFsvR4aUf+ew==",
"dependencies": {
"long": "4.0.0",
"slip": "1.0.2",
"wolfy87-eventemitter": "5.2.9",
"ws": "8.13.0"
},
"optionalDependencies": {
"serialport": "10.5.0"
}
},
"node_modules/osc/node_modules/ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/serialport": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/serialport/-/serialport-10.5.0.tgz",
"integrity": "sha512-7OYLDsu5i6bbv3lU81pGy076xe0JwpK6b49G6RjNvGibstUqQkI+I3/X491yBGtf4gaqUdOgoU1/5KZ/XxL4dw==",
"optional": true,
"dependencies": {
"@serialport/binding-mock": "10.2.2",
"@serialport/bindings-cpp": "10.8.0",
"@serialport/parser-byte-length": "10.5.0",
"@serialport/parser-cctalk": "10.5.0",
"@serialport/parser-delimiter": "10.5.0",
"@serialport/parser-inter-byte-timeout": "10.5.0",
"@serialport/parser-packet-length": "10.5.0",
"@serialport/parser-readline": "10.5.0",
"@serialport/parser-ready": "10.5.0",
"@serialport/parser-regex": "10.5.0",
"@serialport/parser-slip-encoder": "10.5.0",
"@serialport/parser-spacepacket": "10.5.0",
"@serialport/stream": "10.5.0",
"debug": "^4.3.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/slip": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/slip/-/slip-1.0.2.tgz",
"integrity": "sha512-XrcHe3NAcyD3wO+O4I13RcS4/3AF+S9RvGNj9JhJeS02HyImwD2E3QWLrmn9hBfL+fB6yapagwxRkeyYzhk98g=="
},
"node_modules/wolfy87-eventemitter": {
"version": "5.2.9",
"resolved": "https://registry.npmjs.org/wolfy87-eventemitter/-/wolfy87-eventemitter-5.2.9.tgz",
"integrity": "sha512-P+6vtWyuDw+MB01X7UeF8TaHBvbCovf4HPEMF/SV7BdDc1SMTiBy13SRD71lQh4ExFTG1d/WNzDGDCyOKSMblw=="
},
"node_modules/ws": {
"version": "8.14.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
"integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@ -1,15 +0,0 @@
{
"name": "topos-server",
"version": "0.0.1",
"description": "Topos OSC Server",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Raphaël Forment",
"license": "GPL-3.0-or-later",
"dependencies": {
"osc": "^2.4.4",
"ws": "^8.14.2"
}
}

View File

@ -1,8 +0,0 @@
const WebSocket = require("ws");
const osc = require("osc");
require("./banner").greet();
// Topos to OSC
require("./ToposToOSC");
// OSC to Topos
require("./OSCtoTopos");

View File

@ -1,4 +1,4 @@
version: "3.7"
version: '3.7'
services:
topos-dev:
container_name: topos-dev

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 121 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 603 B

After

Width:  |  Height:  |  Size: 603 B

19
favicon/site.webmanifest Normal file
View File

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@ -1,7 +1,6 @@
@font-face {
font-family: "IBM Plex Mono";
src:
url("woff2/IBMPlexMono-Regular.woff2") format("woff2"),
src: url("woff2/IBMPlexMono-Regular.woff2") format("woff2"),
url("woff/IBMPlexMono-Regular.woff") format("woff");
font-weight: 400;
font-style: normal;
@ -10,8 +9,7 @@
@font-face {
font-family: "IBM PLex Mono";
src:
url("woff2/IBMPlexMono-Italic.woff2") format("woff2"),
src: url("woff2/IBMPlexMono-Italic.woff2") format("woff2"),
url("woff/IBMPlexMono-Italic.woff") format("woff");
font-weight: 400;
font-style: italic;
@ -20,8 +18,7 @@
@font-face {
font-family: "IBM PLex Mono";
src:
url("woff2/IBMPlexMono-Bold.woff2") format("woff2"),
src: url("woff2/IBMPlexMono-Bold.woff2") format("woff2"),
url("woff/IBMPlexMono-Bold.woff") format("woff");
font-weight: 700;
font-style: normal;
@ -30,8 +27,7 @@
@font-face {
font-family: "IBM Plex Mono";
src:
url("woff2/IBMPlexMono-BoldItalic.woff2") format("woff2"),
src: url("woff2/IBMPlexMono-BoldItalic.woff2") format("woff2"),
url("woff/IBMPlexMono-BoldItalic.woff") format("woff");
font-weight: 700;
font-style: italic;
@ -41,84 +37,83 @@
@font-face {
font-family: "Comic Mono";
font-weight: normal;
src:
url(./woff/ComicMono.woff) format("woff"),
src: url(./woff/ComicMono.woff) format("woff"),
url(./woff2/ComicMono.woff2) format("wooff2");
}
@font-face {
font-family: "Comic Mono";
font-weight: bold;
src:
url(./woff/ComicMono-Bold.woff) format("woff"),
url(./woff/ComicMono-Bold.woff2) format("woff2");
src: url(./woff/ComicMono-Bold.woff) format("woff"),
url(./woff/ComicMono-Bold.woff2) format("woff2"),
}
@font-face {
font-family: "jgs7";
src:
url("./woff2/jgs7.woff2") format("woff2"),
url("./woff/jgs7.woff") format("woff");
font-family: 'jgs7';
src: url('./woff2/jgs7.woff2') format('woff2'),
url('./woff/jgs7.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "jgs5";
src:
url("./woff2/jgs5.woff2") format("woff2"),
url("./woff/jgs5.woff") format("woff");
font-family: 'jgs5';
src: url('./woff2/jgs5.woff2') format('woff2'),
url('./woff/jgs5.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "jgs9";
src:
url("./woff2/jgs9.woff2") format("woff2"),
url("./woff/jgs9.woff") format("woff");
font-family: 'jgs9';
src: url('./woff2/jgs9.woff2') format('woff2'),
url('./woff/jgs9.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'jgs_vecto';
src: url('./woff2/jgs_vecto.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "jgs_vecto";
src: url("./woff2/jgs_vecto.woff2") format("woff2");
font-family: 'Steps Mono';
src: url('./woff2/Steps-Mono.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Steps Mono";
src: url("./woff2/Steps-Mono.woff2") format("woff2");
font-family: 'Steps Mono Thin';
src: url('./woff2/Steps-Mono-Thin.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Steps Mono Thin";
src: url("./woff2/Steps-Mono-Thin.woff2") format("woff2");
font-family: 'Jet Brains';
src: url('./woff2/JetBrainsMono-Regular.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Jet Brains";
src: url("./woff2/JetBrainsMono-Regular.woff2") format("woff2");
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Jet Brains";
src: url("./woff2/JetBrainsMono-Bold.woff2") format("woff2");
font-family: 'Jet Brains';
src: url('./woff2/JetBrainsMono-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;

2
global.d.ts vendored
View File

@ -1,3 +1 @@
/// <reference types="vite-plugin-pwa/client" />

BIN
img/screnshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 KiB

View File

@ -1,24 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Topos</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Topos is a live coding environment inspired by the Monome Teletype.">
<title>Topos</title>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon/favicon.ico" sizes="48x48" ><!-- REVISED (Aug 11, 2023)! -->
<link rel="icon" href="/favicon/favicon.svg" sizes="any" type="image/svg+xml"><!-- REVISED (Aug 11, 2023)! -->
<link rel="apple-touch-icon" href="/favicon/apple-touch-icon.png"/>
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicon/favicon-16x16.png">
<link rel="mask-icon" href="favicon/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href='/fonts/index.css' >
<link rel="stylesheet" href="/src/output.css" />
<link rel="stylesheet" href='/fonts/index.css' >
<script src="https://unpkg.com/hydra-synth"></script>
</head>
<style>
body {
font-family: "Arial";
background-color: #111827;
overflow: hidden;
position: fixed;
width: 100vw;
@ -27,27 +28,20 @@
padding: 0;
}
.fluid-transition {
.fluid-bg-transition {
transition: background-color 0.05s ease-in-out;
}
.hydracanvas {
position: fixed;
top: 0px; left: 0px;
width: 100%; height: 100%;
background-size: cover;
overflow-y: hidden;
z-index: -5;
display: block;
}
.fullscreencanvas {
position: fixed;
top: 0px; left: 0px;
width: 100%; height: 100%;
position: fixed; /* ignore margins */
top: 0px;
left: 0px;
width: 100%; /* fill screen */
height: 100%;
background-size: cover;
overflow-y: hidden;
z-index: -1;
z-index: -1; /* place behind everything else */
display: block;
}
@ -72,220 +66,183 @@
z-index: 0;
}
.bar_button {
@apply mx-2 px-2 py-2 flex inline rounded-lg bg-background text-foreground hover:bg-foreground hover:text-background
}
.side_button {
@apply px-2 py-2 bg-background text-foreground rounded-lg hover:bg-foreground hover:text-background
}
.subtitle {
@apply bg-selection_foreground text-sm lg:text-xl border-b py-4 text-foreground
}
.tab_panel {
@apply inline-block lg:px-4 px-8 py-1 text-brightwhite
}
.doc_header {
@apply pl-2 pr-2 lg:text-xl text-sm py-1 my-1 rounded-lg text-white hover:text-brightwhite hover:bg-brightblack
}
.doc_subheader {
@apply pl-2 pr-2 lg:text-xl text-sm ml-6 py-1 my-1 rounded-lg text-white hover:text-brightwhite hover:bg-brightblack
}
</style>
<body id="all" class="z-0 bg-neutral-800 overflow-y-hidden">
<body id="all" class="z-0 overflow-y-hidden bg-black">
<!-- The header is hidden on smaller devices -->
<header class="py-0 block">
<div id="topbar" class="mx-auto flex flex-wrap pl-2 py-1 flex-row items-center bg-background">
<a class="flex title-font font-medium items-center mb-0">
<img id="topos_logo" src="topos_frog.svg" class="w-12 h-12 text-selection_foreground p-2 rounded-full bg-foreground" alt="Topos Frog Logo"/>
<input id="universe-viewer" class="hidden transparent xl:block ml-4 text-2xl bg-background text-brightwhite placeholder-brightwhite" id="renamer" type="text" placeholder="Topos">
<header class="py-0 block text-white bg-neutral-900">
<div class="mx-auto flex flex-wrap pl-2 py-1 flex-row items-center">
<a class="flex title-font font-medium items-center text-black mb-0">
<img id="topos-logo" src="topos_frog.svg" class="w-12 h-12 text-black p-2 bg-white rounded-full" alt="Topos Frog Logo" />
<input id="universe-viewer" class="hidden bg-transparent xl:block ml-4 text-2xl text-white placeholder-white" id="renamer" type="text" placeholder="Topos">
</a>
<nav class="py-2 flex flex-wrap items-center text-base absolute right-0">
<!-- Play Button -->
<a title="Play button (Ctrl+P)" id="play-button" class="bar_button">
<a title="Play button (Ctrl+P)" id="play-button-1" class="flex flex-row mr-2 hover:bg-gray-800 px-2 py-2 rounded-lg">
<svg id="play-icon" class="w-7 h-7" fill="currentColor" viewBox="0 0 14 16">
<path d="M0 .984v14.032a1 1 0 0 0 1.506.845l12.006-7.016a.974.974 0 0 0 0-1.69L1.506.139A1 1 0 0 0 0 .984Z"/>
</svg>
<svg id="pause-icon" class="hidden w-7 h-7" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<svg id="pause-icon" class="hidden w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9 13a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0v6Zm4 0a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0v6Z"/>
</svg>
<p id="play-label" class="hidden lg:block text-xl pl-2 inline-block">Play</p>
<p id="play-label" class="hidden lg:block text-xl pl-2 text-white inline-block">Play</p>
</a>
<!-- Stop button -->
<a title="Stop button (Ctrl+R)" id="stop-button" class="bar_button">
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<a title="Stop button (Ctrl+R)" id="stop-button-1" class="flex flex-row mr-2 hover:bg-gray-800 px-2 py-2 rounded-lg">
<svg class="w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Z"/>
<rect x="6.5" y="6.5" width="7" height="7" fill="selection_background" rx="1" ry="1"/>
<rect x="6.5" y="6.5" width="7" height="7" fill="black" rx="1" ry="1"/>
</svg>
<p class="hidden lg:block text-xl pl-2 inline-block">Stop</p>
<p class="hidden lg:block text-xl pl-2 text-white inline-block">Stop</p>
</a>
<!-- Eval button -->
<a title="Eval button (Ctrl+Enter)" id="eval-button-1" class="bar_button">
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
<a title="Eval button (Ctrl+Enter)" id="eval-button-1" class="flex flex-row mr-2 hover:text-gray-900 hover:bg-gray-800 px-2 py-2 rounded-lg">
<svg class="w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 1v5h-5M2 19v-5h5m10-4a8 8 0 0 1-14.947 3.97M1 10a8 8 0 0 1 14.947-3.97"/>
</svg>
<p class="hidden lg:block text-xl pl-2 inline-block">Eval</p>
<p class="hidden lg:block text-xl pl-2 text-white inline-block">Eval</p>
</a>
<a title="Share button" id="share-button" class="bar_button">
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 19 19">
<a title="Clear button" id="clear-button-1" class="flex flex-row mr-2 hover:text-gray-900 hover:bg-gray-800 px-2 py-2 rounded-lg">
<svg class="w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 20">
<path d="M17 4h-4V2a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v2H1a1 1 0 0 0 0 2h1v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1a1 1 0 1 0 0-2ZM7 2h4v2H7V2Zm1 14a1 1 0 1 1-2 0V8a1 1 0 0 1 2 0v8Zm4 0a1 1 0 0 1-2 0V8a1 1 0 0 1 2 0v8Z"/>
</svg>
<p class="hidden lg:block text-xl pl-2 text-white inline-block">Clear</p>
</a>
<a title="Share button" id="share-button" class="flex flex-row mr-2 hover:text-gray-900 hover:bg-gray-800 px-2 py-2 rounded-lg">
<svg class="w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 19 19">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.013 7.962a3.519 3.519 0 0 0-4.975 0l-3.554 3.554a3.518 3.518 0 0 0 4.975 4.975l.461-.46m-.461-4.515a3.518 3.518 0 0 0 4.975 0l3.553-3.554a3.518 3.518 0 0 0-4.974-4.975L10.3 3.7"/>
</svg>
<p class="hidden lg:block text-xl pl-2 inline-block">Share</p>
<p class="hidden lg:block text-xl pl-2 text-white inline-block">Share</p>
</a>
<a title="Open Documentation (Ctrl+D)" id="doc-button-1" class="bar_button">
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<a title="Open Documentation (Ctrl+D)" id="doc-button-1" class="flex flex-row hover:text-gray-900 hover:bg-gray-800 px-2 py-2 rounded-lg">
<svg class="w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
</svg>
<p class="hidden lg:block text-xl pl-2 inline-block">Docs</p>
<p class="hidden lg:block text-xl pl-2 text-white inline-block">Docs</p>
</a>
<div id="transport_viewer" class="pr-2 text-selection_background"></div>
</nav>
</nav>
</div>
</header>
<div id="documentation" class="hidden">
<div id="documentation-page" class="flex flex-row transparent">
<aside class="w-1/8 flex-shrink-0 h-screen overflow-y-auto p-1 lg:p-6 bg-background">
<nav class="text-xl sm:text-sm overflow-y-scroll mb-24 bg-background">
<details class="" open>
<div id="documentation-page" class="flex flex-row">
<aside class="w-1/8 flex-shrink-0 h-screen overflow-y-auto p-1 lg:p-6 bg-neutral-900 text-white">
<nav class="text-xl sm:text-sm overflow-y-scroll mb-24">
<details class="" open=true>
<summary class="font-semibold lg:text-xl text-orange-300">Basics</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_introduction" class="doc_header">Welcome </p>
<p rel="noopener noreferrer" id="docs_atelier" class="doc_header">Atelier (FR)</p>
<p rel="noopener noreferrer" id="docs_interface" class="doc_header">Interface</p>
<p rel="noopener noreferrer" id="docs_interaction" class="doc_header">Interaction</p>
<p rel="noopener noreferrer" id="docs_shortcuts" class="doc_header">Keyboard</p>
<p rel="noopener noreferrer" id="docs_mouse" class="doc_header">Mouse</p>
<p rel="noopener noreferrer" id="docs_code" class="doc_header">Coding</p>
<p rel="noopener noreferrer" id="docs_introduction" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Welcome </p>
<p rel="noopener noreferrer" id="docs_interface" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Interface</p>
<p rel="noopener noreferrer" id="docs_interaction" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Interaction</p>
<p rel="noopener noreferrer" id="docs_shortcuts" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Keyboard</p>
<p rel="noopener noreferrer" id="docs_mouse" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Mouse</p>
<p rel="noopener noreferrer" id="docs_code" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Coding</p>
</div>
</details>
<details class="space-y-2" open>
<details class="space-y-2" open=true>
<summary class="font-semibold lg:text-xl pb-1 pt-1 text-orange-300">Learning</summary>
<div class="flex flex-col">
<!-- Time -->
<details class="space-y-2">
<summary class="ml-2 lg:text-xl pb-1 pt-1 doc_header">Time</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_time" class="doc_subheader">Dealing with time</p>
<p rel="noopener noreferrer" id="docs_linear" class="doc_subheader">Time & Transport</p>
<p rel="noopener noreferrer" id="docs_cyclic" class="doc_subheader">Time & Cycles</p>
<p rel="noopener noreferrer" id="docs_longform" class="doc_subheader">Time & Structure</p>
</div>
</details>
<!-- Audio Engine -->
<details class="space-y-2">
<summary class="ml-2 lg:text-xl pb-1 pt-1 doc_header">Audio Engine</summary>
<!-- Time -->
<details class="space-y-2" open=false>
<summary class="ml-2 lg:text-xl pb-1 pt-1 text-white">Time</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_audio_basics" class="doc_subheader">Playing a sound</p>
<p rel="noopener noreferrer" id="docs_amplitude" class="doc_subheader">Amplitude</p>
<p rel="noopener noreferrer" id="docs_sampler" class="doc_subheader">Sampler</p>
<p rel="noopener noreferrer" id="docs_synths" class="doc_subheader">Synths</p>
<p rel="noopener noreferrer" id="docs_filters" class="doc_subheader">Filters</p>
<p rel="noopener noreferrer" id="docs_effects" class="doc_subheader">Effects</p>
<p rel="noopener noreferrer" id="docs_time" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Dealing with time</p>
<p rel="noopener noreferrer" id="docs_linear" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Time & Transport</p>
<p rel="noopener noreferrer" id="docs_cyclic" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Time & Cycles</p>
<p rel="noopener noreferrer" id="docs_longform" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Time & Structure</p>
</div>
</details>
<!-- Samples -->
<details class="space-y-2">
<summary class="ml-2 lg:text-xl pb-1 pt-1 doc_header ">Samples</summary>
<details class="space-y-2" open=false>
<summary class="ml-2 lg:text-xl pb-1 pt-1 text-white">Audio Engine</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_sample_list" class="doc_subheader">List of samples</p>
<p rel="noopener noreferrer" id="docs_loading_samples" class="doc_subheader">External samples</p>
<p rel="noopener noreferrer" id="docs_audio_basics" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Playing a sound</p>
<p rel="noopener noreferrer" id="docs_amplitude" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Amplitude</p>
<p rel="noopener noreferrer" id="docs_sampler" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Sampler</p>
<p rel="noopener noreferrer" id="docs_synths" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Synths</p>
<p rel="noopener noreferrer" id="docs_reverb_delay" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Effects</p>
</div>
</details>
<p rel="noopener noreferrer" id="docs_midi" class="doc_header">MIDI</p>
<p rel="noopener noreferrer" id="docs_osc" class="doc_header">OSC</p>
<!-- Audio Engine -->
<details class="space-y-2" open=false>
<summary class="ml-2 lg:text-xl pb-1 pt-1 text-white">Samples</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_sample_list" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">List of samples</p>
<p rel="noopener noreferrer" id="docs_loading_samples" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Loading Samples</p>
</div>
</details>
<details class="space-y-2" open>
<p rel="noopener noreferrer" id="docs_patterns" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Patterns</p>
<p rel="noopener noreferrer" id="docs_midi" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">MIDI</p>
</div>
</details>
<details class="space-y-2" open=true>
<summary class="font-semibold lg:text-xl pb-1 pt-1 text-orange-300">Patterns</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_patterns" class="pl-2 pr-2 lg:text-xl text-sm hover:neutral-800 py-1 my-1 rounded-lg doc_header">Array patterns</p>
<!-- Ziffers -->
<details class="space-y-2">
<summary class="doc_header">Ziffers</summary>
<p rel="noopener noreferrer" id="docs_variables" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Global Variables</p>
<p rel="noopener noreferrer" id="docs_lfos" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Low Freq Oscs.</p>
<p rel="noopener noreferrer" id="docs_probabilities" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Probabilities</p>
<p rel="noopener noreferrer" id="docs_chaining" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Chaining</p>
<p rel="noopener noreferrer" id="docs_functions" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Functions</p>
<p rel="noopener noreferrer" id="docs_ziffers" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Ziffers</p>
<!--
<p rel="noopener noreferrer" id="docs_reference" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Reference</p>
-->
</div>
</details>
<details class="space-y-2" open=true>
<summary class="font-semibold lg:text-xl text-orange-300">More</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_ziffers_basics" class="doc_subheader">Basics</p>
<p rel="noopener noreferrer" id="docs_ziffers_scales" class="doc_subheader">Scales</p>
<p rel="noopener noreferrer" id="docs_ziffers_rhythm" class="doc_subheader">Rhythm</p>
<p rel="noopener noreferrer" id="docs_ziffers_algorithmic" class="doc_subheader">Algorithmic</p>
<p rel="noopener noreferrer" id="docs_ziffers_tonnetz" class="doc_subheader">Tonnetz</p>
<p rel="noopener noreferrer" id="docs_ziffers_syncing" class="doc_subheader">Syncing</p>
<a rel="noopener noreferrer" id="docs_synchronisation" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Synchronisation</a>
<a rel="noopener noreferrer" id="docs_oscilloscope" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Oscilloscope</a>
<a rel="noopener noreferrer" id="docs_bonus" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Bonus/Trivia</a>
<a rel="noopener noreferrer" id="docs_about" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">About Topos</a>
</div>
</details>
<p rel="noopener noreferrer" id="docs_variables" class="doc_header">Global Variables</p>
<p rel="noopener noreferrer" id="docs_lfos" class="doc_header">Low Freq Oscs.</p>
<p rel="noopener noreferrer" id="docs_probabilities" class="doc_header">Probabilities</p>
<p rel="noopener noreferrer" id="docs_chaining" class="doc_header">Chaining</p>
<p rel="noopener noreferrer" id="docs_functions" class="doc_header">Functions</p>
<p rel="noopener noreferrer" id="docs_generators" class="doc_header">Generators</p>
</div>
</details>
<details class="space-y-2" open>
<summary class="font-semibold lg:text-xl doc_header">More</summary>
<div class="flex flex-col">
<a rel="noopener noreferrer" id="docs_synchronisation" class="doc_subheader">Synchronisation</a>
<a rel="noopener noreferrer" id="docs_oscilloscope" class="doc_subheader">Oscilloscope</a>
<a rel="noopener noreferrer" id="docs_visualization" class="doc_header">Visualization</a>
<a rel="noopener noreferrer" id="docs_bonus" class="doc_header">Bonus/Trivia</a>
<a rel="noopener noreferrer" id="docs_about" class="doc_header">About Topos</a>
</div>
</details>
<details class="" open>
<details class="" open=true>
<summary class="font-semibold lg:text-xl text-orange-300">Community</summary>
<form action="https://github.com/Bubobubobubobubo/topos">
<input rel="noopener noreferrer" id="github_link" class="doc_header" type="submit" value="GitHub" />
<input rel="noopener noreferrer" id="docs_introduction" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg" type="submit" value="GitHub" />
</form>
<form action="https://discord.gg/6T67DqBNNT">
<input rel="noopener noreferrer" id="discord_link" class="doc_header" type="submit" value="Discord" />
</form>
<form action="https://ko-fi.com/raphaelbubo">
<input rel="noopener noreferrer" id="support_link" class="doc_header" type="submit" value="Support" />
<input rel="noopener noreferrer" id="docs_introduction" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg" type="submit" value="Discord" />
</form>
</details>
</nav>
</aside>
<div id="documentation-content" class="w-full flex-grow-1 h-screen overflow-y-scroll lg:px-12 mx-2 my-2 break-words pb-32 transparent"></div>
<div id="documentation-content" class="w-full flex-grow-1 h-screen overflow-y-scroll lg:px-12 mx-2 my-2 break-words pb-32">
</div>
</div>
</div>
<div id="app">
<!-- This modal is used for switching between buffers -->
<div id="modal-buffers" class="invisible flex justify-center items-center absolute top-0 right-0 bottom-0 left-0">
<div id="start-button" class="lg:px-16 px-4 lg:pt-4 lg:pb-4 pt-2 pb-2 rounded-md text-center bg-foreground">
<p class="text-semibold lg:text-2xl text-sm pb-4 text-selection_foreground">Known universes</p>
<div id="modal-buffers" class="invisible bg-gray-900 bg-opacity-50 flex justify-center items-center absolute top-0 right-0 bottom-0 left-0">
<div id="start-button" class="lg:px-16 px-4 lg:pt-4 lg:pb-4 pt-2 pb-2 rounded-md text-center bg-white">
<p class="text-semibold lg:text-2xl text-sm pb-4">Known universes</p>
<p id="existing-universes" class="text-normal lg:h-auto h-48 overflow-y-auto mb-2"></p>
<div id="disclaimer" class="pb-4">
<form id="universe-creator">
<label for="search" class="mb-2 text-sm font-medium sr-only ">Search</label>
<label for="search" class="mb-2 text-sm font-medium text-gray-900 sr-only text-white">Search</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<svg class="w-4 h-4 text-gray-500 text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
</svg>
</div>
<input name="universe" minlength="2" autocomplete="off" type="text" id="buffer-search" class="block w-full p-4 pl-10 text-sm border border-neutral-800 outline-0 rounded-lg neutral-800 " placeholder="Buffer..." required>
<button id="load-universe-button" class="bg-background text-selection_background absolute right-2.5 bottom-2.5 focus:outline-none font-medium rounded-lg text-sm px-4 py-2">Go</button>
<input name="universe" minlength="2" autocomplete="off" type="text" id="buffer-search" class="block w-full p-4 pl-10 text-sm border border-neutral-800 outline-0 rounded-lg bg-neutral-800 text-white" placeholder="Buffer..." required>
<button id="load-universe-button" class="text-black absolute right-2.5 bottom-2.5 bg-white hover:bg-white focus:outline-none font-medium rounded-lg text-sm px-4 py-2">Go</button>
</div>
</form>
<div class="mt-2 flex space-x-6 border-t rounded-b border-spacing-y-4">
<button id="close-universes-button" data-modal-hide="defaultModal" type="button" class="mt-2 focus:ring-4 font-medium rounded-lg text-sm px-5 py-2.5 text-center bg-background text-selection_background">Close</button>
<div class="mt-2 flex space-x-6 border-t border-gray-200 rounded-b dark:border-gray-600 border-spacing-y-4">
<button id="close-universes-button" data-modal-hide="defaultModal" type="button" class="mt-2 hover:bg-neutral-700 bg-neutral-800 text-white focus:ring-4 font-medium rounded-lg text-sm px-5 py-2.5 text-center">Close</button>
</div>
</div>
@ -300,20 +257,23 @@
md:top-0 md:bottom-0 h-screen w-full"
>
<div class="grid w-full grid-col-3">
<div class="white rounded-lg lg:mx-48 mx-0 lg:space-y-8 space-y-4 lg:px-8 bg-foreground">
<h1 class="lg:mt-12 mt-6 font-semibold rounded-lg justify-center lg:text-center lg:pl-0 pl-8 mx-4 subtitle">Topos Application Settings</h1>
<div class="bg-white rounded-lg lg:mx-48 mx-0 lg:space-y-8 space-y-4 lg:px-8">
<h1 class="lg:mt-12 mt-6 font-semibold rounded-lg
bg-gray-800 justify-center lg:text-center lg:pl-0 pl-8 text-white mx-4
text-sm lg:text-xl border-b border-gray-300 py-4">Topos Application Settings</h1>
<div class="flex lg:flex-row flex-col mr-4 ml-4">
<!-- Font Size Selection -->
<div class="rounded-lg ml-0 lg:w-1/3 w-full pt-2 pb-1 mb-2 bg-selection_foreground">
<p class="font-bold lg:text-xl text-sm ml-4 lg:pb-4 pb-2 underline underline-offset-4 text-selection_background">Theme Settings</p>
<div class="bg-gray-200 rounded-lg ml-0 lg:w-1/3 w-full pt-2 pb-1 mb-2">
<p class="font-bold lg:text-xl text-sm ml-4 lg:pb-4 pb-2 underline underline-offset-4">Font Settings</p>
<div class="mb-6 mx-4 font-semibold">
<label for="default-input" class="block mb-2 ml-1 font-normal sd:text-sm text-foreground">Size:</label>
<input type="text" id="font-size-input" type="number" class="border
text-sm rounded-lg focus:border-blue-500 block w-full p-2.5 focus:border-blue-500">
<label for="default-input" class="block mb-2 ml-1 font-normal sd:text-sm">Size:</label>
<input type="text" id="font-size-input" type="number" class="bg-gray-50 border border-gray-300 text-gray-900
text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700
dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
</div>
<label for="font" class="block ml-5 mb-2 font-medium sd:text-sm text-foreground">Font:</label>
<select id="font-family" class=" ml-4 border mb-2
text-sm rounded-lg focus:border-blue-500 block p-2.5">
<label for="font" class="block ml-5 mb-2 font-medium sd:text-sm">Font:</label>
<select id="font-family" class="bg-gray-50 ml-4 border border-gray-300 mb-2
text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
<option value="IBM Plex Mono">IBM Plex Mono</option>
<option value="Jet Brains">Jet Brains</option>
<option value="Courier">Courier</option>
@ -325,200 +285,186 @@
<option value="Steps Mono">Steps Mono</option>
<option value="Steps Mono Thin">Steps Mono Thin</option>
</select>
<div class="rounded-lg ml-0 lg:w-1/3 w-full pt-2 pb-1 mb-2">
<label for="theme" class="block ml-5 mb-2 font-medium sd:text-sm text-foreground">Theme:</label>
<select id="theme-selector" class="ml-4 border mb-2
text-sm rounded-lg block p-2.5">
</select>
</div>
</div>
<!-- Editor mode selection -->
<div class="rounded-lg lg:ml-4 lg:w-1/3 w-full pt-2 pb-1 mb-2 bg-selection_foreground">
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4 text-selection_background">Editor options</p>
<div class="bg-gray-200 rounded-lg lg:ml-4 lg:w-1/3 w-full pt-2 pb-1 mb-2">
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4">Editor options</p>
<!-- Checkboxes -->
<div class="pr-4">
<div class="flex items-center mb-4 ml-5">
<input id="vim-mode" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-selection_background">Vim Mode</label>
<input id="vim-mode" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Vim Mode</label>
</div>
<div class="flex items-center mb-4 ml-5">
<input id="show-line-numbers" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600 focus:ring-2">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Show Line Numbers</label>
<input id="show-line-numbers" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Show Line Numbers</label>
</div>
<div class="flex items-center mb-4 ml-5">
<input id="show-time-position" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600 focus:ring-2">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Show Time Position</label>
<input id="show-time-position" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Show Time Position</label>
</div>
<div class="flex items-center mb-4 ml-5">
<input id="show-tips" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600 focus:ring-2">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Show Hovering Tips</label>
<input id="show-tips" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Show Hovering Tips</label>
</div>
<div class="flex items-center mb-4 ml-5">
<input id="show-completions" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600 focus:ring-2">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Show Completions</label>
<input id="show-completions" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Show Completions</label>
</div>
<!--
<div class="flex items-center mb-4 ml-5">
<input id="load-demo-songs" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Load Demo Song</label>
<input id="load-demo-songs" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Load Demo Song</label>
</div>
-->
</div>
</div>
<div class="rounded-lg lg:ml-4 lg:w-1/3 w-full pt-2 pb-1 mb-2 bg-selection_foreground">
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4 text-selection_background">File Management</p>
<div class="bg-gray-200 rounded-lg lg:ml-4 lg:w-1/3 w-full pt-2 pb-1 mb-2">
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4">File Management</p>
<div class="flex flex-col space-y-2 pb-2">
<button id="download-universes" class="bg-brightwhite font-bold lg:py-4 lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4 text-selection_background">
<button id="download-universes" class="bg-gray-800 hover:bg-gray-900 text-white font-bold lg:py-4 lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4">
<svg class="fill-current w-4 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/></svg>
<span class="text-selection_foreground">Download universes</span>
<span>Download universes</span>
</button>
<button id="upload-universes" class="bg-brightwhite font-bold lg:py-4 lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4 text-selection_background">
<button id="upload-universes" class="bg-gray-800 hover:bg-gray-900 text-white font-bold lg:py-4 lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4">
<svg class="rotate-180 fill-current w-4 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/></svg>
<span class="text-selection_foreground">Upload universes</span>
<span>Upload universes</span>
</button>
<button id="destroy-universes" class="bg-brightwhite font-bold lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4">
<button id="destroy-universes" class="bg-red-800 hover:bg-red-900 text-white font-bold lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-6 mr-2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
<span class="text-selection_foreground">Destroy universes</span>
<span>Destroy universes</span>
</button>
<!-- Upload audio samples -->
<p class="font-bold lg:text-xl text-sm ml-4 pb-2 pt-2 underline underline-offset-4 text-selection_background">Audio samples</p>
<label class="bg-brightwhite font-bold lg:py-4 lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4 text-selection_background">
<svg class="rotate-180 fill-current w-4 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/></svg>
<input id="upload-samples" type="file" class="hidden" accept="file" webkitdirectory directory multiple>
<span id="sample-indicator" class="text-selection_foreground">Import samples</span>
</label>
</div>
</div>
</div>
<!-- Midi settings -->
<div id="midi-settings-container" class="rounded-lg flex lg:flex-row flex-col mx-4 my-4 pt-4 bg-color bg-selection_foreground">
<div id="midi-settings-container" class="bg-gray-200 rounded-lg flex lg:flex-row flex-col mx-4 my-4 pt-4">
<div class="lg:flex lg:flex-row w-fit">
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4 text-selection_background">MIDI I/O Settings</p>
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4">MIDI I/O Settings</p>
<div class="flex items-center mb-4 ml-6">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">MIDI Clock:&nbsp;</label>
<select id="midi-clock-input" class="w-32 h-8 text-sm font-medium text-black rounded focus:ring-blue-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">MIDI Clock:&nbsp;</label>
<select id="midi-clock-input" class="w-32 h-8 text-sm font-medium text-black bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2">
<option value="-1">Internal</option>
</select>
</div>
<div class="lg:flex block items-center mb-4 ml-6">
<label for="default-checkbox" class="ml-2 mr-2 text-sm font-medium text-foreground">Clock PPQN:&nbsp;</label>
<select id="midi-clock-ppqn-input" class="w-32 h-8 text-sm font-medium text-black rounded focus:ring-blue-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Clock PPQN:&nbsp;</label>
<select id="midi-clock-ppqn-input" class="w-32 h-8 text-sm font-medium text-black bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2">
<option value="24">24</option>
<option value="48">48</option>
</select>
</div>
<div class="lg:flex block items-center mb-4 ml-6">
<input id="send-midi-clock" type="checkbox" value="" class="lg:w-8 lg:h-8 h-4 w-4 text-blue-600 rounded focus:ring-blue-600 focus:ring-2">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Send MIDI Clock</label>
<input id="send-midi-clock" type="checkbox" value="" class="lg:w-8 lg:h-8 h-4 w-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Send MIDI Clock</label>
</div>
</div>
<div class="lg:flex block flex-row">
<div class="flex items-center mb-4 ml-6">
<label for="default-checkbox" class="ml-2 mr-2 text-sm font-medium text-foreground">MIDI input:&nbsp;</label>
<select id="default-midi-input" class="w-32 h-8 text-sm font-medium text-black rounded focus:ring-blue-600 focus:ring-2">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">MIDI input:&nbsp;</label>
<select id="default-midi-input" class="w-32 h-8 text-sm font-medium text-black bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2">
<option value="-1">None</option>
</select>
</div>
<div class="lg:flex block items-center mb-4 ml-6">
<input id="midi-channels-scripts" type="checkbox" value="" class="lg:w-8 lg:h-8 h-4 w-4 text-blue-600 rounded focus:ring-blue-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Route channels to scripts</label>
<input id="midi-channels-scripts" type="checkbox" value="" class="lg:w-8 lg:h-8 h-4 w-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Route channels to scripts</label>
</div>
</div>
</div>
<!-- Audio nudge slider -->
<div id="midi-settings-container" class="rounded-lg flex flex-col mx-4 my-4 pt-4 pb-2 bg-selection_foreground">
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4 text-selection_background">Audio/Event Nudging</p> <div class="flex flex-column pb-2">
<p class="pt-0.5 ml-4 text-foreground">Clock:</p>
<div id="midi-settings-container" class="bg-gray-200 rounded-lg flex flex-col mx-4 my-4 pt-4 pb-2">
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4">Audio/Event Nudging</p>
<div class="flex flex-column pb-2">
<p class="pt-0.5 ml-4">Clock:</p>
<input
type="range" id="audio_nudge"
name="audiorangeInput"
min="-200" max="200"
value="0"
class="w-full ml-4 text-red"
class="w-full ml-4"
oninput="nudgenumber.value=audio_nudge.value"
>
<output
name="nudgenumber"
id="nudgenumber"
for="audiorangeInput"
class="rounded-lg ml-2 mr-4 px-4 py-1 text-foreground"
class="bg-gray-500 rounded-lg ml-2 mr-4 px-4 py-1 text-white"
>0</output>
</div>
<div class="flex flex-column">
<p class="pt-0.5 ml-4 text-foreground">Audio:</p>
<p class="pt-0.5 ml-4">Audio:</p>
<input
type="range" id="dough_nudge"
name="doughrangeInput"
min="0" max="100"
value="0"
class="w-full ml-4 text-foreground"
class="w-full ml-4"
oninput="doughnumber.value=dough_nudge.value"
>
<output
name="doughnumber"
id="doughnumber"
for="doughrangeInput"
class="rounded-lg ml-2 mr-4 px-4 py-1 text-foreground"
class="bg-gray-500 rounded-lg ml-2 mr-4 px-4 py-1 text-white"
>0</output>
</div>
</div>
<div class="flex space-x-6 border-t rounded-b mx-4 border-spacing-y-4 pb-36 lg:pb-0">
<div class="flex space-x-6 border-t border-gray-200 rounded-b dark:border-gray-600 mx-4 border-spacing-y-4 pb-36 lg:pb-0">
<button id="close-settings-button" data-modal-hide="defaultModal" type="button" class="
hover:bg-background bg-background mt-4 mb-4 focus:ring-4
font-medium rounded-lg text-sm px-5 py-2.5 text-center text-selection_background">OK</button>
hover:bg-gray-700 bg-gray-800 mt-4 mb-4 text-white focus:ring-4
font-medium rounded-lg text-sm px-5 py-2.5 text-center">OK</button>
</div>
</div>
</div>
</div>
<div class="flex flex-row max-h-fit">
<!-- This is a lateral bar that will inherit the header buttons if the window is too small. -->
<aside id="sidebar" class="
<aside class="
flex flex-col items-center w-14
h-screen py-2 border-r
rtl:border-l max-h-fit
rtl:border-r-0 bg-background
border-neutral-700 border-none"
rtl:border-r-0 bg-neutral-900
dark:border-neutral-700 border-none"
>
<nav class="flex flex-col space-y-6">
<a title="Local Scripts (Ctrl + L)" id="local-button" class="side_button">
<svg class="w-8 h-8 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 18">
<a title="Local Scripts (Ctrl + L)" id="local-button" class="pl-2 p-1.5 focus:outline-nones transition-colors duration-200 rounded-lg text-white hover:bg-gray-800">
<svg class="w-8 h-8 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 18">
<path d="M18 5H0v11a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5Zm-7.258-2L9.092.8a2.009 2.009 0 0 0-1.6-.8H2.049a2 2 0 0 0-2 2v1h10.693Z"/>
</svg>
</svg>
</a>
<a title="Global Script (Ctrl + G)" id="global-button" class="side_button">
<svg class="w-8 h-8 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 16">
<a title="Global Script (Ctrl + G)" id="global-button" class="pl-2 p-1.5 text-white focus:outline-nones transition-colors duration-200 rounded-lg hover:bg-gray-800">
<svg class="w-8 h-8 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 16">
<path d="M14.316.051A1 1 0 0 0 13 1v8.473A4.49 4.49 0 0 0 11 9c-2.206 0-4 1.525-4 3.4s1.794 3.4 4 3.4 4-1.526 4-3.4a2.945 2.945 0 0 0-.067-.566c.041-.107.064-.22.067-.334V2.763A2.974 2.974 0 0 1 16 5a1 1 0 0 0 2 0C18 1.322 14.467.1 14.316.051ZM10 3H1a1 1 0 0 1 0-2h9a1 1 0 1 1 0 2Z"/>
<path d="M10 7H1a1 1 0 0 1 0-2h9a1 1 0 1 1 0 2Zm-5 4H1a1 1 0 0 1 0-2h4a1 1 0 1 1 0 2Z"/>
</svg>
</a>
<a title="Initialisation Script (Ctrl + I)" id="init-button" class="side_button">
<svg class="w-8 h-8 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 14">
<a title="Initialisation Script (Ctrl + I)" id="init-button" class="pl-2 p-1.5 focus:outline-nones transition-colors duration-200 rounded-lg text-white hover:bg-gray-800">
<svg class="w-8 h-8 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1v12m0 0 4-4m-4 4L1 9"/>
</svg>
</a>
<a title="Project notes (Ctrl + N)" id="note-button" class="side_button">
<svg class="w-8 h-8 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<a title="Project notes (Ctrl + N)" id="note-button" class="pl-2 p-1.5 text-white focus:outline-nones transition-colors duration-200 rounded-lg text-white hover:bg-gray-800">
<svg class="w-8 h-8 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="m13.835 7.578-.005.007-7.137 7.137 2.139 2.138 7.143-7.142-2.14-2.14Zm-10.696 3.59 2.139 2.14 7.138-7.137.007-.005-2.141-2.141-7.143 7.143Zm1.433 4.261L2 12.852.051 18.684a1 1 0 0 0 1.265 1.264L7.147 18l-2.575-2.571Zm14.249-14.25a4.03 4.03 0 0 0-5.693 0L11.7 2.611 17.389 8.3l1.432-1.432a4.029 4.029 0 0 0 0-5.689Z"/>
</svg>
</a>
<a title="Application Settings" id="settings-button" class="side_button">
<svg class="w-8 h-8 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<a title="Application Settings" id="settings-button" class="pl-2 p-1.5 text-white focus:outline-nones transition-colors duration-200 rounded-lg text-white hover:bg-gray-800">
<svg class="w-8 h-8 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
@ -532,53 +478,55 @@
<!-- Tabs for local files -->
<div class="min-w-screen flex grow flex-col">
<ul id="local-script-tabs" class=" flex text-xl font-medium text-center bg-background space-x-1 lg:space-x-8">
<ul id="local-script-tabs" class=" flex text-xl font-medium text-center text-white bg-neutral-900 space-x-1 lg:space-x-8">
<li class="pl-5">
<a title="Local Script 1 (F1)" id="tab-1" class="tab_panel">1</a>
<a title="Local Script 1 (F1)" id="tab-1" class="bg-orange-300 inline-block lg:px-4 px-2 py-1 text-white hover:bg-gray-800">1</a>
</li>
<li class="">
<a title="Local Script 2 (F2)" id="tab-2" class="tab_panel">2</a>
<a title="Local Script 2 (F2)" id="tab-2" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">2</a>
</li>
<li class="">
<a title="Local Script 3 (F3)" id="tab-3" class="tab_panel">3</a>
<a title="Local Script 3 (F3)" id="tab-3" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">3</a>
</li>
<li class="">
<a title="Local Script 4 (F4)" id="tab-4" class="tab_panel">4</a>
<a title="Local Script 4 (F4)" id="tab-4" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">4</a>
</li>
<li class="">
<a title="Local Script 5 (F5)" id="tab-5" class="tab_panel">5</a>
<a title="Local Script 5 (F5)" id="tab-5" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">5</a>
</li>
<li class="">
<a title="Local Script 6 (F6)" id="tab-6" class="tab_panel">6</a>
<a title="Local Script 6 (F6)" id="tab-6" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">6</a>
</li>
<li class="">
<a title="Local Script 7 (F7)" id="tab-7" class="tab_panel">7</a>
<a title="Local Script 7 (F7)" id="tab-7" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">7</a>
</li>
<li class="">
<a title="Local Script 8 (F8)" id="tab-8" class="tab_panel">8</a>
<a title="Local Script 8 (F8)" id="tab-8" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">8</a>
</li>
<li class="">
<a title="Local Script 9 (F9)" id="tab-9" class="tab_panel">9</a>
<a title="Local Script 9 (F9)" id="tab-9" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">9</a>
</li>
</ul>
<!-- Here comes the editor itself -->
<div id="editor" class="relative flex flex-row h-screen overflow-y-hidden">
<canvas id="hydra-bg" class="fullscreencanvas"></canvas>
<canvas id="scope" class="fullscreencanvas"></canvas>
<canvas id="feedback" class="fullscreencanvas"></canvas>
<canvas id="hydra-bg" class="hydracanvas"></canvas>
</div>
<p id="error_line" class="hidden w-screen bg-background font-mono absolute bottom-0 pl-2 py-2">Hello kids</p>
<p id="error_line" class="hidden text-red-400 w-screen bg-neutral-900 font-mono absolute bottom-0 pl-2 py-2">Hello kids</p>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
<template id="ui-known-universe-item-template">
<!-- A known universe button in "opening" interface -->
<li class="py-2 px-4 flex justify-between text-brightwhite hover:bg-selection_background hover:text-selection_foreground">
<li class="hover:fill-black hover:bg-white py-2 hover:text-black flex justify-between px-4">
<button class="universe-name load-universe" title="Load this universe">Universe Name</button>
<button class="delete-universe" title="Delete this universe">🗑</button>
</li>
</template>
</body>
<p id="fillviewer" class="invisible rounded-lg px-2 py-2 font-bold cursor-textpointer-events-none select-none text-sm absolute right-2 bottom-12 bg-foreground text-background">/////// Fill ///////</p>
<p id="timeviewer" class="rounded-lg px-2 py-2 font-bold bg-white cursor-textpointer-events-none select-none text-black text-sm absolute bottom-2 right-2"></p>
<p id="fillviewer" class="invisible rounded-lg px-2 py-2 font-bold bg-white cursor-textpointer-events-none select-none text-black text-sm absolute right-2 bottom-12">/////// Fill ///////</p>
</html>

24
manifest.webmanifest Normal file
View File

@ -0,0 +1,24 @@
{
"name": "Topos",
"short_name": "Topos",
"description": "Live coding environment",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone",
"scope": "/",
"start_url": "/",
"icons": [
{
"src": "./favicon/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "./favicon/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}

View File

@ -14,7 +14,7 @@
"typescript": "^5.2.2",
"vite": "^4.4.5",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-pwa": "^0.17.4"
"vite-plugin-pwa": "^0.16.7"
},
"dependencies": {
"@codemirror/lang-javascript": "^6.1.9",
@ -30,22 +30,19 @@
"autoprefixer": "^10.4.14",
"codemirror": "^6.0.1",
"fflate": "^0.8.0",
"highlight.js": "^11.9.0",
"jisg": "^0.9.7",
"lru-cache": "^10.2.0",
"lru-cache": "^10.0.1",
"marked": "^7.0.3",
"osc": "^2.4.4",
"postcss": "^8.4.27",
"showdown": "^2.1.0",
"showdown-highlight": "^3.1.0",
"superdough": "^0.9.12",
"superdough": "^0.9.11",
"tailwind-highlightjs": "^2.0.1",
"tailwindcss": "^3.3.3",
"tone": "^14.8.49",
"unique-names-generator": "^4.7.1",
"vite-plugin-markdown": "^2.1.0",
"zifferjs": "^0.0.62",
"zyklus": "^0.1.4",
"zifferjs": "^0.0.39",
"zzfx": "^1.2.0"
}
}

View File

@ -3,4 +3,4 @@ export default {
tailwindcss: {},
autoprefixer: {},
},
};
}

View File

@ -1,46 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="48.000000pt" height="48.000000pt" viewBox="0 0 48.000000 48.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,48.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M163 470 c-13 -5 -23 -12 -23 -15 0 -3 46 -5 102 -5 80 0 99 3 90 12
-15 15 -139 21 -169 8z"/>
<path d="M91 391 c-16 -16 -19 -32 -18 -84 1 -53 -2 -69 -18 -87 -15 -16 -20
-38 -22 -83 0 -33 2 -56 5 -50 9 13 44 13 39 -1 -2 -6 -18 -11 -34 -11 -39 0
-50 -10 -33 -30 8 -10 30 -15 60 -15 26 0 64 -7 83 -15 43 -18 125 -20 164 -3
15 6 57 14 93 17 73 7 87 24 51 60 -12 12 -21 17 -21 13 0 -4 5 -13 12 -20 8
-8 8 -12 1 -12 -18 0 -25 34 -9 45 10 7 12 16 6 25 -5 8 -7 24 -4 35 3 13 -2
28 -15 39 -12 11 -21 27 -21 36 0 9 -5 21 -11 27 -8 8 -8 17 0 31 17 32 13 56
-12 80 -31 29 -63 28 -91 -3 -16 -17 -34 -25 -56 -25 -22 0 -40 8 -56 25 -28
30 -67 32 -93 6z m89 -16 c19 -23 5 -29 -24 -10 -16 10 -33 14 -47 10 -14 -5
-19 -4 -15 4 10 16 72 13 86 -4z m211 -7 c12 -22 11 -22 -8 -5 -25 21 -41 22
-65 0 -22 -19 -36 -10 -18 12 20 24 77 19 91 -7z m-264 -30 c-3 -7 -5 -2 -5
12 0 14 2 19 5 13 2 -7 2 -19 0 -25z m229 -5 c-11 -11 -19 6 -11 24 8 17 8 17
12 0 3 -10 2 -21 -1 -24z m-80 -8 c4 -8 10 -12 15 -9 5 3 9 0 9 -6 0 -14 60
-40 77 -33 7 3 13 -2 13 -11 0 -13 -6 -15 -27 -9 -36 9 -210 9 -245 0 -22 -6
-28 -4 -28 9 0 8 6 14 13 11 6 -2 25 2 40 10 15 8 32 11 37 8 6 -4 7 1 3 11
-4 12 -3 15 5 10 7 -4 15 -1 18 8 8 21 63 21 70 1z m83 -91 c10 -9 -37 -33
-77 -39 -55 -8 -117 2 -155 26 l-30 18 54 4 c56 4 201 -2 208 -9z m-286 -30
c-15 -7 21 -44 42 -44 8 0 15 -4 15 -10 0 -16 -33 -11 -59 9 -25 19 -24 52 1
50 9 0 10 -2 1 -5z m355 -21 c2 -14 -4 -23 -17 -28 -12 -3 -21 -13 -21 -21 0
-19 -16 -18 -24 1 -5 15 -31 16 -53 2 -8 -5 -13 -4 -13 2 0 6 5 12 10 12 6 1
15 3 20 4 6 1 18 3 28 4 25 1 66 37 52 44 -6 3 -5 4 2 3 7 -1 14 -12 16 -23z
m-255 -37 c4 -10 1 -13 -9 -9 -7 3 -14 9 -14 14 0 14 17 10 23 -5z m42 -6 c3
-5 1 -10 -4 -10 -6 0 -11 5 -11 10 0 6 2 10 4 10 3 0 8 -4 11 -10z m35 0 c0
-5 -4 -10 -10 -10 -5 0 -10 5 -10 10 0 6 5 10 10 10 6 0 10 -4 10 -10z m-170
-10 c0 -5 -5 -10 -11 -10 -5 0 -7 5 -4 10 3 6 8 10 11 10 2 0 4 -4 4 -10z
m215 -39 c-6 -5 -25 10 -25 20 0 5 6 4 14 -3 8 -7 12 -15 11 -17z m45 8 c0
-14 -16 -11 -29 5 -10 12 -8 13 8 9 12 -3 21 -9 21 -14z m-220 1 c0 -5 -5 -10
-11 -10 -5 0 -7 5 -4 10 3 6 8 10 11 10 2 0 4 -4 4 -10z m40 0 c0 -5 -4 -10
-10 -10 -5 0 -10 5 -10 10 0 6 5 10 10 10 6 0 10 -4 10 -10z m215 0 c3 -5 2
-10 -4 -10 -5 0 -13 5 -16 10 -3 6 -2 10 4 10 5 0 13 -4 16 -10z m43 -10 c7
-11 10 -20 6 -20 -7 0 -34 27 -34 34 0 13 16 5 28 -14z m-160 -17 c-10 -2 -26
-2 -35 0 -10 3 -2 5 17 5 19 0 27 -2 18 -5z m99 1 c-3 -3 -12 -4 -19 -1 -8 3
-5 6 6 6 11 1 17 -2 13 -5z m40 0 c-3 -3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17
-2 13 -5z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

@ -1,37 +0,0 @@
{
"name": "Topos",
"short_name": "Topos",
"icons": [
{
"src": "favicon/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "favicon/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"display": "standalone",
"start_url": "/",
"scope": "/",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"description": "Topos is a web based live coding platform",
"screenshots": [
{
"src": "favicon/screenshot_miniature.png",
"sizes": "640x320",
"type": "image/gif",
"form_factor": "wide",
"label": "Topos application"
},
{
"src": "favicon/topos_code.png",
"sizes": "1280x768",
"type": "image/gif",
"label": "Topos code"
}
]
}

2147
src/API.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,630 +0,0 @@
import * as Transport from './Time/Transport';
import * as Mouse from './DOM/Mouse';
import * as Theme from './DOM/Theme';
import * as Canvas from './DOM/Canvas';
import * as Cache from './Cache';
import * as Script from './Script';
import * as Drunk from './Drunk';
import * as Warp from './Time/Warp';
import * as Mathematics from './Math';
import * as Ziffers from './Ziffers';
import * as Filters from './Time/Filters';
import * as LFO from './LFO';
import * as Probability from './Probabilities';
import * as OSC from './IO/OSC';
import * as Randomness from './Randomness';
import * as Counter from './Counter';
import * as Sound from './Sound';
import * as Console from './DOM/Console';
import { type SoundEvent } from '../Classes/SoundEvent';
import { type SkipEvent } from '../Classes/SkipEvent';
import { OscilloscopeConfig } from "../DOM/Visuals/Oscilloscope";
import { Player } from "../Classes/ZPlayer";
import { InputOptions } from "../Classes/ZPlayer";
import { type ShapeObject } from "../API/DOM/Canvas";
import { nearScales } from "zifferjs";
import { MidiConnection } from "../IO/MidiConnection";
import { DrunkWalk } from "../Utils/Drunk";
import { Editor } from "../main";
import { LRUCache } from "lru-cache";
import {
loadUniverse,
openUniverseModal,
} from "../Editor/FileManagement";
import {
samples,
initAudioOnFirstClick,
registerSynthSounds,
registerZZFXSounds,
soundMap,
// @ts-ignore
} from "superdough";
import { getScaleNotes } from "zifferjs";
import drums from "../tidal-drum-machines.json";
import { updatePlayPauseIcon } from '../DOM/UILogic';
export async function loadSamples() {
return Promise.all([
initAudioOnFirstClick(),
samples("github:tidalcycles/Dirt-Samples/master", undefined, {
tag: "Tidal",
}).then(() => registerSynthSounds()),
registerZZFXSounds(),
samples(drums, "github:ritchse/tidal-drum-machines/main/machines/", {
tag: "Machines",
}),
samples("github:Bubobubobubobubo/Dough-Fox/main", undefined, {
tag: "FoxDot",
}),
samples("github:Bubobubobubobubo/Dough-Samples/main", undefined, {
tag: "Pack",
}),
samples("github:Bubobubobubobubo/Dough-Amiga/main", undefined, {
tag: "Amiga",
}),
samples("github:Bubobubobubobubo/Dough-Juj/main", undefined, {
tag: "Juliette",
}),
samples("github:Bubobubobubobubo/Dough-Amen/main", undefined, {
tag: "Amen",
}),
samples("github:Bubobubobubobubo/Dough-Waveforms/main", undefined, {
tag: "Waveforms",
}),
]);
}
export class UserAPI {
/**
* The UserAPI class is the interface between the user's code and the backend. It provides
* access to the AudioContext, to the MIDI Interface, to internal variables, mouse position,
* useful functions, etc... This class is exposed to the user's action and any function
* destined to the user should be placed here.
*/
public codeExamples: { [key: string]: string } = {};
public counters: { [key: string]: any } = {};
//@ts-ignore
public _drunk: DrunkWalk = new DrunkWalk(-100, 100, false);
public randomGen = Math.random;
public currentSeed: string | undefined = undefined;
public localSeeds = new Map<string, Function>();
public patternCache = new LRUCache({ max: 10000, ttl: 10000 * 60 * 5 });
public invalidPatterns: { [key: string]: boolean } = {};
public cueTimes: { [key: string]: number } = {};
private errorTimeoutID: number = 0;
private printTimeoutID: number = 0;
public MidiConnection: MidiConnection;
public scale_aid: string | number | undefined = undefined;
public hydra: any;
public onceEvaluator: boolean = true;
public forceEvaluator: boolean = false;
load: samples;
public global: { [key: string]: any };
time: () => number;
play: () => void;
pause: () => void;
stop: () => void;
silence: () => void;
mouseX: () => number;
mouseY: () => number;
noteX: () => number;
noteY: () => number;
tempo: (n?: number | undefined) => number;
ppqn: (n?: number | undefined) => number;
time_signature: (numerator: number, denominator: number) => void;
theme: (color_scheme: string) => void;
themeName: () => string;
randomTheme: () => void;
nextTheme: () => void;
getThemes: () => string[];
pulseLocation: () => number;
clear: () => boolean;
loadHydra: () => void;
w: () => number;
h: () => number;
hc: () => number;
wc: () => number;
background: (color: string | number, ...gb: number[]) => boolean;
linearGradient: (x1: number, y1: number, x2: number, y2: number, ...stops: (number | string)[]) => CanvasGradient;
radialGradient: (x1: number, y1: number, r1: number, x2: number, y2: number, r2: number, ...stops: (number | string)[]) => CanvasGradient;
conicGradient: (x: number, y: number, angle: number, ...stops: (number | string)[]) => CanvasGradient;
draw: (func: Function) => boolean;
balloid: (curves: number | ShapeObject, radius: number, curve: number, fillStyle: string, secondary: string, x: number, y: number) => boolean;
equilateral: (radius: number | ShapeObject, fillStyle: string, rotation: number, x: number, y: number) => boolean;
triangular: (width: number | ShapeObject, height: number, fillStyle: string, rotation: number, x: number, y: number) => boolean;
ball: (radius: number | ShapeObject, fillStyle: string, x: number, y: number) => boolean;
circle: (radius: number | ShapeObject, fillStyle: string, x: number, y: number) => boolean;
donut: (slices: number | ShapeObject, eaten: number, radius: number, hole: number, fillStyle: string, secondary: string, stroke: string, rotation: number, x: number, y: number) => boolean;
pie: (slices: number | ShapeObject, eaten: number, radius: number, fillStyle: string, secondary: string, stroke: string, rotation: number, x: number, y: number) => boolean;
star: (points: number | ShapeObject, radius: number, fillStyle: string, rotation: number, outerRadius: number, x: number, y: number) => boolean;
stroke: (width: number | ShapeObject, strokeStyle: string, rotation: number, x1: number, y1: number, x2: number, y2: number) => boolean;
box: (width: number | ShapeObject, height: number, fillStyle: string, rotation: number, x: number, y: number) => boolean;
smiley: (happiness: number | ShapeObject, radius: number, eyeSize: number, fillStyle: string, rotation: number, x: number, y: number) => boolean;
text: (text: string | ShapeObject, fontSize: number, rotation: number, font: string, x: number, y: number, fillStyle: string, filter: string) => boolean;
image: (url: string | ShapeObject, width: number, height: number, rotation: number, x: number, y: number, filter: string) => boolean;
randomChar: (length: number, min: number, max: number) => string;
randomFromRange: (min: number, max: number) => string;
emoji: (n: number) => string;
food: (n: number) => string;
animals: (n: number) => string;
expressions: (n: number) => string;
generateCacheKey: (...args: any[]) => string;
resetAllFromCache: () => void;
clearPatternCache: () => void;
removePatternFromCache: (id: string) => void;
script: (...args: number[]) => void;
s: (...args: number[]) => void;
delete_script: (script: number) => void;
cs: (script: number) => void;
copy_script: (from: number, to: number) => void;
cps: (from: number, to: number) => void;
copy_universe: (from: string, to: string) => void;
delete_universe: (universe: string) => void;
big_bang: () => void;
drunk: (n?: number | undefined) => number;
drunk_max: (max: number) => void;
drunk_min: (min: number) => void;
drunk_wrap: (wrap: boolean) => void;
warp: (n: number) => void;
beat_warp: (beat: number) => void;
min: (...values: number[]) => number;
max: (...values: number[]) => number;
mean: (...values: number[]) => number;
limit: (value: number, min: number, max: number) => number;
abs: (value: number) => number;
z: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
fullseq: (sequence: string, duration: number) => boolean | boolean[];
seq: (expr: string, duration?: number) => boolean;
beat: (n?: number | number[], nudge?: number) => boolean;
bar: (n?: number | number[], nudge?: number) => boolean;
pulse: (n?: number | number[], nudge?: number) => boolean;
tick: (tick: number | number[], offset?: number) => boolean;
dur: (n: number | number[]) => boolean;
flip: (chunk: number, ratio?: number) => boolean;
flipbar: (chunk?: number) => boolean;
onbar: (bars: number | number[], n?: number) => boolean;
onbeat: (...beat: number[]) => boolean;
oncount: (beats: number | number[], count: number) => boolean;
oneuclid: (pulses: number, length: number, rotate?: number) => boolean;
euclid: (iterator: number, pulses: number, length: number, rotate?: number) => boolean;
ec: any;
rhythm: (div: number, pulses: number, length: number, rotate?: number) => boolean;
ry: any;
nrhythm: (div: number, pulses: number, length: number, rotate?: number) => boolean;
nry: any;
bin: (iterator: number, n: number) => boolean;
binrhythm: (div: number, n: number) => boolean;
bry: any;
line: any;
sine: any;
usine: any;
saw: any;
usaw: any;
triangle: any;
utriangle: any;
square: any;
usquare: any;
noise: any;
unoise: any;
prob: (p: number) => boolean;
toss: () => boolean;
odds: (n: number, beats?: number) => boolean;
never: (beats?: number) => boolean;
almostNever: (beats?: number) => boolean;
rarely: (beats?: number) => boolean;
scarcely: (beats?: number) => boolean;
sometimes: (beats?: number) => boolean;
often: (beats?: number) => boolean;
frequently: (beats?: number) => boolean;
almostAlways: (beats?: number) => boolean;
always: (beats?: number) => boolean;
dice: (sides: number) => number;
osc: (address: string, port: number, ...args: any[]) => void;
getOSC: (address?: string | undefined) => any[];
gif: (options: any) => void;
scope: (config: OscilloscopeConfig) => void;
randI: any;
rand: any;
ir: any;
irand: any;
r: any;
seed: any;
localSeededRandom: any;
clearLocalSeed: any;
once: () => boolean;
counter: (name: string | number, limit?: number | undefined, step?: number | undefined) => number;
$: any;
count: any;
i: (n?: number | undefined) => any;
sound: (sound: string | string[] | null | undefined) => SoundEvent | SkipEvent;
snd: any;
log: (message: any) => void;
logOnce: (message: any) => void;
speak: (text: string, lang?: string, voiceIndex?: number, rate?: number, pitch?: number) => void;
cbar: () => number;
ctick: () => number;
cpulse: () => number;
cbeat: () => number;
ebeat: () => number;
epulse: () => number;
nominator: () => number;
meter: () => number;
denominator: () => number;
pulsesForBar: () => number;
z0!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
z1!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
z2!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
z3!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
z4!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
z5!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
z6!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
z7!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
z8!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
z9!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
z10!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
z11!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
z12!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
z13!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
z14!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
z15!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
z16!: (input: string | Generator<number>, options: InputOptions, id: number | string) => Player;
constructor(public app: Editor) {
this.MidiConnection = new MidiConnection(this, app.settings);
this.global = {};
this.g = this.global;
this.time = Transport.time(this);
this.play = Transport.play(this);
this.pause = Transport.pause(this);
this.stop = Transport.stop(this);
this.silence = Transport.silence(this);
this.tempo = Transport.tempo(this.app);
this.ppqn = Transport.ppqn(this.app);
this.time_signature = Transport.time_signature(this.app);
this.mouseX = Mouse.mouseX(this.app);
this.mouseY = Mouse.mouseY(this.app);
this.noteX = Mouse.noteX(this.app);
this.noteY = Mouse.noteY(this.app);
this.theme = Theme.theme(this.app);
this.themeName = Theme.themeName(this.app);
this.randomTheme = Theme.randomTheme(this.app);
this.nextTheme = Theme.nextTheme(this.app);
this.getThemes = Theme.getThemes();
this.pulseLocation = Canvas.pulseLocation(this.app);
this.loadHydra = Canvas.loadHydra(this.app);
this.clear = Canvas.clear(this.app);
this.w = Canvas.w(this.app);
this.h = Canvas.h(this.app);
this.hc = Canvas.hc(this.app);
this.wc = Canvas.wc(this.app);
this.background = Canvas.background(this.app);
this.linearGradient = Canvas.linearGradient(this.app);
this.radialGradient = Canvas.radialGradient(this.app);
this.conicGradient = Canvas.conicGradient(this.app);
this.draw = Canvas.draw(this.app);
this.balloid = Canvas.balloid(this.app);
this.equilateral = Canvas.equilateral(this.app);
this.triangular = Canvas.triangular(this.app);
this.ball = Canvas.ball(this.app);
this.circle = Canvas.circle(this.app);
this.donut = Canvas.donut(this.app);
this.pie = Canvas.pie(this.app);
this.star = Canvas.star(this.app);
this.stroke = Canvas.stroke(this.app);
this.box = Canvas.box(this.app);
this.smiley = Canvas.smiley(this.app);
this.text = Canvas.text(this.app);
this.image = Canvas.image(this.app);
this.randomChar = Canvas.randomChar();
this.randomFromRange = Canvas.randomFromRange();
this.emoji = Canvas.emoji();
this.food = Canvas.food();
this.animals = Canvas.animals();
this.expressions = Canvas.expressions();
this.generateCacheKey = Cache.generateCacheKey();
this.resetAllFromCache = Cache.resetAllFromCache(this);
this.clearPatternCache = Cache.clearPatternCache(this);
this.removePatternFromCache = Cache.removePatternFromCache(this);
this.script = Script.script(this.app);
this.s = this.script;
this.delete_script = Script.delete_script(this.app);
this.cs = this.delete_script;
this.copy_script = Script.copy_script(this.app);
this.cps = this.copy_script;
this.copy_universe = Script.copy_universe(this.app);
this.delete_universe = Script.delete_universe(this.app);
this.big_bang = Script.big_bang(this.app);
this.drunk = Drunk.drunk(this);
this.drunk_max = Drunk.drunk_max(this);
this.drunk_min = Drunk.drunk_min(this);
this.drunk_wrap = Drunk.drunk_wrap(this);
this.warp = Warp.warp(this.app);
this.beat_warp = Warp.beat_warp(this.app);
this.min = Mathematics.min();
this.max = Mathematics.max();
this.mean = Mathematics.mean();
this.limit = Mathematics.limit();
this.abs = Mathematics.abs();
this.z = Ziffers.z(this);
Object.assign(this, Ziffers.generateZFunctions(this));
this.fullseq = Filters.fullseq();
this.seq = Filters.seq(this.app);
this.beat = Filters.beat(this.app);
this.bar = Filters.bar(this.app);
this.pulse = Filters.pulse(this.app);
this.tick = Filters.tick(this.app);
this.dur = Filters.dur(this.app);
this.flip = Filters.flip(this.app);
this.flipbar = Filters.flipbar(this.app);
this.onbar = Filters.onbar(this.app);
this.onbeat = Filters.onbeat(this);
this.oncount = Filters.oncount(this.app);
this.oneuclid = Filters.oneuclid(this.app);
this.euclid = Filters.euclid();
this.ec = this.euclid;
this.rhythm = Filters.rhythm(this.app);
this.ry = this.rhythm;
this.nrhythm = Filters.nrhythm(this.app);
this.nry = this.nrhythm;
this.bin = Filters.bin();
this.binrhythm = Filters.binrhythm(this.app);
this.bry = this.binrhythm;
this.line = LFO.line();
this.sine = LFO.sine(this.app);
this.usine = LFO.usine(this.app);
this.saw = LFO.saw(this.app);
this.usaw = LFO.usaw(this.app);
this.triangle = LFO.triangle(this.app);
this.utriangle = LFO.utriangle(this.app);
this.square = LFO.square(this.app);
this.usquare = LFO.usquare(this.app);
this.noise = LFO.noise(this);
this.unoise = LFO.unoise(this);
this.prob = Probability.prob(this);
this.toss = Probability.toss(this);
this.odds = Probability.odds(this);
this.never = Probability.never();
this.almostNever = Probability.almostNever(this);
this.rarely = Probability.rarely(this);
this.scarcely = Probability.scarcely(this);
this.sometimes = Probability.sometimes(this);
this.often = Probability.often(this);
this.frequently = Probability.frequently(this);
this.almostAlways = Probability.almostAlways(this);
this.always = Probability.always();
this.dice = Probability.dice(this);
this.osc = OSC.osc(this.app);
this.getOSC = OSC.getOSC();
this.gif = Canvas.gif(this.app);
this.scope = Canvas.scope(this.app);
this.randI = Randomness.randI(this);
this.ir = this.randI;
this.irand = this.randI;
this.rand = Randomness.rand(this);
this.r = this.rand;
this.seed = Randomness.seed(this);
this.localSeededRandom = Randomness.localSeededRandom(this);
this.clearLocalSeed = Randomness.clearLocalSeed(this);
this.once = Counter.once(this);
this.counter = Counter.counter(this);
this.$ = this.counter;
this.count = this.counter;
this.i = Counter.i(this.app);
this.sound = Sound.sound(this.app);
this.snd = this.sound;
this.speak = Sound.speak();
this.log = Console.log(this);
this.logOnce = Console.logOnce(this);
this.cbar = Transport.cbar(this.app);
this.ctick = Transport.ctick(this.app);
this.cpulse = Transport.cpulse(this.app);
this.cbeat = Transport.cbeat(this.app);
this.ebeat = Transport.ebeat(this.app);
this.epulse = Transport.epulse(this.app);
this.nominator = Transport.nominator(this.app);
this.meter = Transport.meter(this.app);
this.denominator = Transport.denominator(this.app);
this.pulsesForBar = Transport.pulsesForBar(this.app);
}
public g: any;
_loadUniverseFromInterface = (universe: string) => {
this.app.selected_universe = universe.trim();
this.app.settings.selected_universe = universe.trim();
loadUniverse(this.app, universe as string);
openUniverseModal();
};
_deleteUniverseFromInterface = (universe: string) => {
delete this.app.universes[universe];
if (this.app.settings.selected_universe === universe) {
this.app.settings.selected_universe = "Welcome";
this.app.selected_universe = "Welcome";
}
this.app.settings.saveApplicationToLocalStorage(
this.app.universes,
this.app.settings,
);
this.app.updateKnownUniversesView();
};
_playDocExample = (code?: string) => {
/**
* Play an example from the documentation. The example is going
* to be stored in the example buffer belonging to the universe.
* This buffer is going to be cleaned everytime the user press
* pause or leaves the documentation window.
*
* @param code - The code example to play (identifier)
*/
let current_universe = this.app.universes[this.app.selected_universe]!;
this.app.exampleIsPlaying = true;
if (!current_universe.example) {
current_universe.example = {
candidate: "",
committed: "",
evaluations: 0,
};
current_universe.example.candidate! = code
? code
: (this.app.selectedExample as string);
} else {
current_universe.example.candidate! = code
? code
: (this.app.selectedExample as string);
}
this.patternCache.clear();
if (this.app.isPlaying) {
} else {
this.app.clock.resume();
updatePlayPauseIcon(this.app, "play");
this.app.api.MidiConnection.sendStartMessage();
}
// this.app.clock.start();
};
_stopDocExample = () => {
let current_universe = this.app.universes[this.app.selected_universe];
if (current_universe?.example !== undefined) {
this.app.exampleIsPlaying = false;
current_universe.example.candidate! = "";
current_universe.example.committed! = "";
}
this.clearPatternCache();
this.stop();
};
_playDocExampleOnce = (code?: string) => {
let current_universe = this.app.universes[this.app.selected_universe];
if (current_universe?.example !== undefined) {
current_universe.example.candidate! = "";
current_universe.example.committed! = "";
}
this.clearPatternCache();
this.stop();
this.play();
this.app.exampleIsPlaying = true;
evaluateOnce(this.app, code as string);
};
_all_samples = (): object => {
return soundMap.get();
};
_reportError = (error: any): void => {
const extractLineAndColumn = (error: Error) => {
const stackLines = error.stack?.split("\n");
if (stackLines) {
for (const line of stackLines) {
if (line.includes("<anonymous>")) {
const match = line.match(/<anonymous>:(\d+):(\d+)/);
if (match as RegExpMatchArray)
return {
// @ts-ignore
line: parseInt(match[1], 10),
// @ts-ignore
column: parseInt(match[2]!, 10),
};
}
}
}
return { line: null, column: null };
};
const { line, column } = extractLineAndColumn(error);
const errorMessage =
line && column
? `${error.message} (Line: ${line - 2}, Column: ${column})`
: error.message;
clearTimeout(this.errorTimeoutID);
clearTimeout(this.printTimeoutID);
this.app.interface.error_line.innerHTML = errorMessage;
this.app.interface.error_line.style.color = "red";
this.app.interface.error_line.classList.remove("hidden");
// @ts-ignore
this.errorTimeoutID = setTimeout(
() => this.app.interface.error_line.classList.add("hidden"),
2000,
);
};
_logMessage = (message: any, error: boolean = false): void => {
console.log(message);
clearTimeout(this.printTimeoutID);
clearTimeout(this.errorTimeoutID);
this.app.interface.error_line.innerHTML = message as string;
this.app.interface.error_line.style.color = error ? "red" : "white";
this.app.interface.error_line.classList.remove("hidden");
// @ts-ignore
this.printTimeoutID = setTimeout(
() => this.app.interface.error_line.classList.add("hidden"),
4000,
);
};
// =============================================================
// Quantification functions
// =============================================================
public quantize = (value: number, quantization: number[]): number => {
/**
* Returns the closest value in an array to a given value.
*
* @param value - The value to quantize
* @param quantization - The array of values to quantize to
* @returns The closest value in the array to the given value
*/
if (quantization.length === 0) {
return value;
}
let closest: number | undefined = quantization[0];
quantization.forEach((q) => {
if (Math.abs(q - value) < Math.abs(closest! - value)) {
closest = q;
}
});
return closest!;
};
quant = this.quantize;
public clamp = (value: number, min: number, max: number): number => {
/**
* Returns a value clamped between min and max.
*
* @param value - The value to clamp
* @param min - The minimum value of the clamped value
* @param max - The maximum value of the clamped value
* @returns A value clamped between min and max
*/
return Math.min(Math.max(value, min), max);
};
cmp = this.clamp;
// =============================================================
// Time markers
// =============================================================
// =============================================================
// Fill
// =============================================================
public fill = (): boolean => this.app.fill;
scale = getScaleNotes;
nearScales = nearScales;
public cue = (functionName: string | Function): void => {
functionName = typeof functionName === "function" ? functionName.name : functionName;
this.cueTimes[functionName] = this.app.clock.grain;
};
onmousemove = (e: MouseEvent) => {
this.app._mouseX = e.pageX;
this.app._mouseY = e.pageY;
};
}

View File

@ -1,61 +0,0 @@
import { isGenerator, isGeneratorFunction, maybeToNumber } from "../Utils/Generic";
import { type Player } from "../Classes/ZPlayer";
import { type UserAPI } from "./API";
export const generateCacheKey = () => (...args: any[]): string => {
return args.map((arg) => JSON.stringify(arg)).join(",");
};
export const resetAllFromCache = (api: UserAPI) => (): void => {
api.patternCache.forEach((player) => (player as Player).reset());
};
export const clearPatternCache = (api: UserAPI) => (): void => {
api.patternCache.clear();
};
export const removePatternFromCache = (api: UserAPI) => (id: string): void => {
api.patternCache.delete(id);
};
export const cache = (api: UserAPI) => (key: string, value: any) => {
if (value !== undefined) {
if (isGenerator(value)) {
if (api.patternCache.has(key)) {
const cachedValue = (api.patternCache.get(key) as Generator<any>).next().value;
if (cachedValue !== 0 && !cachedValue) {
const generator = value as unknown as Generator<any>;
api.patternCache.set(key, generator);
return maybeToNumber(generator.next().value);
}
return maybeToNumber(cachedValue);
} else {
const generator = value as unknown as Generator<any>;
api.patternCache.set(key, generator);
return maybeToNumber(generator.next().value);
}
} else if (isGeneratorFunction(value)) {
if (api.patternCache.has(key)) {
const cachedValue = (api.patternCache.get(key) as Generator<any>).next().value;
if (cachedValue || cachedValue === 0 || cachedValue === 0n) {
return maybeToNumber(cachedValue);
} else {
const generator = value();
api.patternCache.set(key, generator);
return maybeToNumber(generator.next().value);
}
} else {
const generator = value();
api.patternCache.set(key, generator);
return maybeToNumber(generator.next().value);
}
} else {
api.patternCache.set(key, value);
return maybeToNumber(value);
}
} else {
return maybeToNumber(api.patternCache.get(key));
}
};

View File

@ -1,43 +0,0 @@
import { type UserAPI } from "./API";
import { type Editor } from "../main";
export const once = (api: UserAPI) => (): boolean => {
const firstTime = api.onceEvaluator;
api.onceEvaluator = false;
return firstTime;
};
export const counter = (api: UserAPI) => (name: string | number, limit?: number, step?: number): number => {
if (!(name in api.counters)) {
api.counters[name] = {
value: 0,
step: step ?? 1,
limit,
};
} else {
if (api.counters[name].limit !== limit) {
api.counters[name].value = 0;
api.counters[name].limit = limit;
}
if (api.counters[name].step !== step) {
api.counters[name].step = step ?? api.counters[name].step;
}
api.counters[name].value += api.counters[name].step;
if (api.counters[name].limit !== undefined && api.counters[name].value > api.counters[name].limit) {
api.counters[name].value = 0;
}
}
return api.counters[name].value;
};
export const i = (app: Editor) => (n?: number) => {
if (n !== undefined) {
app.universes[app.selected_universe]!.global.evaluations = n;
return app.universes[app.selected_universe];
}
return app.universes[app.selected_universe]!.global.evaluations as number;
};

View File

@ -1,452 +0,0 @@
import { OscilloscopeConfig } from "../../DOM/Visuals/Oscilloscope";
import { createConicGradient, createLinearGradient, createRadialGradient, drawBackground, drawBox, drawBall, drawBalloid, drawDonut, drawEquilateral, drawImage, drawPie, drawSmiley, drawStar, drawStroke, drawText, drawTriangular } from "../../DOM/Visuals/CanvasVisuals";
import { Editor } from "../../main";
export type ShapeObject = {
x: number;
y: number;
x1: number;
y1: number;
x2: number;
y2: number;
radius: number;
width: number;
height: number;
fillStyle: string;
secondary: string;
strokeStyle: string;
rotation: number;
points: number;
outerRadius: number;
eyeSize: number;
happiness: number;
slices: number;
gap: number;
font: string;
fontSize: number;
text: string;
filter: string;
url: string;
curve: number;
curves: number;
stroke: string;
eaten: number;
hole: number;
};
export const loadHydra = (app: Editor) => (): void => {
app.api.log("Hydra is now loaded!")
app.ensureHydraLoaded()
}
export const w = (app: Editor) => (): number => {
const canvas: HTMLCanvasElement = app.interface["feedback"] as HTMLCanvasElement;
return canvas.clientWidth;
};
export const pulseLocation = (app: Editor) => (): number => {
return ((app.api.epulse() / app.api.pulsesForBar()) * w(app)()) % w(app)();
};
export const clear = (app: Editor) => (): boolean => {
const canvas: HTMLCanvasElement = app.interface["feedback"] as HTMLCanvasElement;
const ctx = canvas.getContext("2d")!;
ctx.clearRect(0, 0, canvas.width, canvas.height);
return true;
};
export const h = (app: Editor) => (): number => {
const canvas: HTMLCanvasElement = app.interface["feedback"] as HTMLCanvasElement;
return canvas.clientHeight;
};
export const hc = (app: Editor) => (): number => {
return h(app)() / 2;
};
export const wc = (app: Editor) => (): number => {
return w(app)() / 2;
};
export const background = (app: Editor) => (color: string | number, ...gb: number[]): boolean => {
drawBackground(app.interface["feedback"] as HTMLCanvasElement, color, ...gb);
return true;
};
export const bg = background;
export const linearGradient = (app: Editor) => (x1: number, y1: number, x2: number, y2: number, ...stops: (number | string)[]): CanvasGradient => {
return createLinearGradient(app.interface["feedback"] as HTMLCanvasElement, x1, y1, x2, y2, ...stops);
};
export const radialGradient = (app: Editor) => (x1: number, y1: number, r1: number, x2: number, y2: number, r2: number, ...stops: (number | string)[]) => {
return createRadialGradient(app.interface["feedback"] as HTMLCanvasElement, x1, y1, r1, x2, y2, r2, ...stops);
};
export const conicGradient = (app: Editor) => (x: number, y: number, angle: number, ...stops: (number | string)[]) => {
return createConicGradient(app.interface["feedback"] as HTMLCanvasElement, x, y, angle, ...stops);
};
export const draw = (app: Editor) => (func: Function): boolean => {
if (typeof func === "string") {
drawText(app.interface["feedback"] as HTMLCanvasElement, func, 24, 0, "Arial", wc(app)(), hc(app)(), "white", "none");
} else {
const canvas: HTMLCanvasElement = app.interface["feedback"] as HTMLCanvasElement;
const ctx = canvas.getContext("2d")!;
func(ctx);
}
return true;
};
// Additional drawing and utility functions in canvas.ts
export const balloid = (app: Editor) => (
curves: number | ShapeObject = 6,
radius: number = hc(app)() / 2,
curve: number = 1.5,
fillStyle: string = "white",
secondary: string = "black",
x: number = wc(app)(),
y: number = hc(app)(),
): boolean => {
if (typeof curves === "object") {
fillStyle = curves.fillStyle || "white";
x = curves.x || wc(app)();
y = curves.y || hc(app)();
curve = curves.curve || 1.5;
radius = curves.radius || hc(app)() / 2;
curves = curves.curves || 6;
}
drawBalloid(app.interface["feedback"] as HTMLCanvasElement, curves, radius, curve, fillStyle, secondary, x, y);
return true;
};
export const equilateral = (app: Editor) => (
radius: number | ShapeObject = hc(app)() / 3,
fillStyle: string = "white",
rotation: number = 0,
x: number = wc(app)(),
y: number = hc(app)(),
): boolean => {
if (typeof radius === "object") {
fillStyle = radius.fillStyle || "white";
x = radius.x || wc(app)();
y = radius.y || hc(app)();
rotation = radius.rotation || 0;
radius = radius.radius || hc(app)() / 3;
}
drawEquilateral(app.interface["feedback"] as HTMLCanvasElement, radius, fillStyle, rotation, x, y);
return true;
};
export const triangular = (app: Editor) => (
width: number | ShapeObject = hc(app)() / 3,
height: number = hc(app)() / 3,
fillStyle: string = "white",
rotation: number = 0,
x: number = wc(app)(),
y: number = hc(app)(),
): boolean => {
if (typeof width === "object") {
fillStyle = width.fillStyle || "white";
x = width.x || wc(app)();
y = width.y || hc(app)();
rotation = width.rotation || 0;
height = width.height || hc(app)() / 3;
width = width.width || hc(app)() / 3;
}
drawTriangular(app.interface['feedback'] as HTMLCanvasElement, width, height, fillStyle, rotation, x, y);
return true;
};
export const pointy = triangular;
export const ball = (app: Editor) => (
radius: number | ShapeObject = hc(app)() / 3,
fillStyle: string = "white",
x: number = wc(app)(),
y: number = hc(app)(),
): boolean => {
if (typeof radius === "object") {
fillStyle = radius.fillStyle || "white";
x = radius.x || wc(app)();
y = radius.y || hc(app)();
radius = radius.radius || hc(app)() / 3;
}
drawBall(app.interface['feedback'] as HTMLCanvasElement, radius, fillStyle, x, y);
return true;
};
export const circle = ball;
export const donut = (app: Editor) => (
slices: number | ShapeObject = 3,
eaten: number = 0,
radius: number = hc(app)() / 3,
hole: number = hc(app)() / 12,
fillStyle: string = "white",
secondary: string = "black",
stroke: string = "black",
rotation: number = 0,
x: number = wc(app)(),
y: number = hc(app)(),
): boolean => {
if (typeof slices === "object") {
fillStyle = slices.fillStyle || "white";
x = slices.x || wc(app)();
y = slices.y || hc(app)();
rotation = slices.rotation || 0;
radius = slices.radius || hc(app)() / 3;
eaten = slices.eaten || 0;
hole = slices.hole || hc(app)() / 12;
secondary = slices.secondary || "black";
stroke = slices.stroke || "black";
slices = slices.slices || 3;
}
drawDonut(app.interface['feedback'] as HTMLCanvasElement, slices, eaten, radius, hole, fillStyle, secondary, stroke, rotation, x, y);
return true;
};
export const pie = (app: Editor) => (
slices: number | ShapeObject = 3,
eaten: number = 0,
radius: number = hc(app)() / 3,
fillStyle: string = "white",
secondary: string = "black",
stroke: string = "black",
rotation: number = 0,
x: number = wc(app)(),
y: number = hc(app)(),
): boolean => {
if (typeof slices === "object") {
fillStyle = slices.fillStyle || "white";
x = slices.x || wc(app)();
y = slices.y || hc(app)();
rotation = slices.rotation || 0;
radius = slices.radius || hc(app)() / 3;
secondary = slices.secondary || "black";
stroke = slices.stroke || "black";
eaten = slices.eaten || 0;
slices = slices.slices || 3;
}
drawPie(app.interface['feedback'] as HTMLCanvasElement, slices, eaten, radius, fillStyle, secondary, stroke, rotation, x, y);
return true;
};
export const star = (app: Editor) => (
points: number | ShapeObject = 5,
radius: number = hc(app)() / 3,
fillStyle: string = "white",
rotation: number = 0,
outerRadius: number = radius / 100,
x: number = wc(app)(),
y: number = hc(app)(),
): boolean => {
if (typeof points === "object") {
radius = points.radius || hc(app)() / 3;
fillStyle = points.fillStyle || "white";
x = points.x || wc(app)();
y = points.y || hc(app)();
rotation = points.rotation || 0;
outerRadius = points.outerRadius || radius / 100;
points = points.points || 5;
}
drawStar(app.interface['feedback'] as HTMLCanvasElement, points, radius, fillStyle, rotation, outerRadius, x, y);
return true;
};
export const stroke = (app: Editor) => (
width: number | ShapeObject = 1,
strokeStyle: string = "white",
rotation: number = 0,
x1: number = wc(app)() - wc(app)() / 10,
y1: number = hc(app)(),
x2: number = wc(app)() + wc(app)() / 5,
y2: number = hc(app)(),
): boolean => {
if (typeof width === "object") {
strokeStyle = width.strokeStyle || "white";
x1 = width.x1 || wc(app)() - wc(app)() / 10;
y1 = width.y1 || hc(app)();
x2 = width.x2 || wc(app)() + wc(app)() / 5;
y2 = width.y2 || hc(app)();
rotation = width.rotation || 0;
width = width.width || 1;
}
drawStroke(app.interface['feedback'] as HTMLCanvasElement, width, strokeStyle, rotation, x1, y1, x2, y2);
return true;
};
export const box = (app: Editor) => (
width: number | ShapeObject = wc(app)() / 4,
height: number = wc(app)() / 4,
fillStyle: string = "white",
rotation: number = 0,
x: number = wc(app)() - wc(app)() / 8,
y: number = hc(app)() - hc(app)() / 8,
): boolean => {
if (typeof width === "object") {
fillStyle = width.fillStyle || "white";
x = width.x || wc(app)() - wc(app)() / 4;
y = width.y || hc(app)() - hc(app)() / 2;
rotation = width.rotation || 0;
height = width.height || wc(app)() / 4;
width = width.width || wc(app)() / 4;
}
drawBox(app.interface['feedback'] as HTMLCanvasElement, width, height, fillStyle, rotation, x, y);
return true;
};
export const smiley = (app: Editor) => (
happiness: number | ShapeObject = 0,
radius: number = hc(app)() / 3,
eyeSize: number = 3.0,
fillStyle: string = "yellow",
rotation: number = 0,
x: number = wc(app)(),
y: number = hc(app)(),
): boolean => {
if (typeof happiness === "object") {
fillStyle = happiness.fillStyle || "yellow";
x = happiness.x || wc(app)();
y = happiness.y || hc(app)();
rotation = happiness.rotation || 0;
eyeSize = happiness.eyeSize || 3.0;
radius = happiness.radius || hc(app)() / 3;
happiness = happiness.happiness || 0;
}
drawSmiley(app.interface['feedback'] as HTMLCanvasElement, happiness, radius, eyeSize, fillStyle, rotation, x, y);
return true;
};
export const text = (app: Editor) => (
text: string | ShapeObject,
fontSize: number = 24,
rotation: number = 0,
font: string = "Arial",
x: number = wc(app)(),
y: number = hc(app)(),
fillStyle: string = "white",
filter: string = "none",
): boolean => {
if (typeof text === "object") {
fillStyle = text.fillStyle || "white";
x = text.x || wc(app)();
y = text.y || hc(app)();
rotation = text.rotation || 0;
font = text.font || "Arial";
fontSize = text.fontSize || 24;
filter = text.filter || "none";
text = text.text || "";
}
drawText(app.interface['feedback'] as HTMLCanvasElement, text, fontSize, rotation, font, x, y, fillStyle, filter);
return true;
};
export const image = (app: Editor) => (
url: string | ShapeObject,
width: number = wc(app)() / 2,
height: number = hc(app)() / 2,
rotation: number = 0,
x: number = wc(app)(),
y: number = hc(app)(),
filter: string = "none",
): boolean => {
if (typeof url === "object") {
if (!url.url) return true;
x = url.x || wc(app)();
y = url.y || hc(app)();
rotation = url.rotation || 0;
width = url.width || 100;
height = url.height || 100;
filter = url.filter || "none";
url = url.url || "";
}
drawImage(app.interface['feedback'] as HTMLCanvasElement, url, width, height, rotation, x, y, filter);
return true;
};
export const randomChar = () => (length: number = 1, min: number = 0, max: number = 65536): string => {
return Array.from(
{ length }, () => String.fromCodePoint(Math.floor(Math.random() * (max - min) + min))
).join('');
};
export const randomFromRange = () => (min: number, max: number): string => {
const codePoint = Math.floor(Math.random() * (max - min) + min);
return String.fromCodePoint(codePoint);
};
export const emoji = () => (n: number = 1): string => {
return randomChar()(n, 0x1f600, 0x1f64f);
};
export const food = () => (n: number = 1): string => {
return randomChar()(n, 0x1f32d, 0x1f37f);
};
export const animals = () => (n: number = 1): string => {
return randomChar()(n, 0x1f400, 0x1f4d3);
};
export const expressions = () => (n: number = 1): string => {
return randomChar()(n, 0x1f910, 0x1f92f);
};
export const gif = (app: Editor) => (options: any): void => {
const {
url,
posX = 0,
posY = 0,
opacity = 1,
size = "auto",
center = false,
rotation = 0,
filter = 'none',
duration = 10
} = options;
let real_duration = duration * app.clock.time_position.tick_duration * app.clock.ppqn;
let fadeOutDuration = real_duration * 0.1;
let visibilityDuration = real_duration - fadeOutDuration;
const gifElement = document.createElement("img");
gifElement.src = url;
gifElement.style.position = "fixed";
gifElement.style.left = center ? "50%" : `${posX}px`;
gifElement.style.top = center ? "50%" : `${posY}px`;
gifElement.style.opacity = `${opacity}`;
gifElement.style.zIndex = "1000"; // Ensure it's on top, fixed zIndex
if (size !== "auto") {
gifElement.style.width = size;
gifElement.style.height = size;
}
const transformRules = [`rotate(${rotation}deg)`];
if (center) {
transformRules.unshift("translate(-50%, -50%)");
}
gifElement.style.transform = transformRules.join(" ");
gifElement.style.filter = filter;
gifElement.style.transition = `opacity ${fadeOutDuration}s ease`;
document.body.appendChild(gifElement);
// Start the fade-out at the end of the visibility duration
setTimeout(() => {
gifElement.style.opacity = "0";
}, visibilityDuration * 1000);
// Remove the GIF from the DOM after the fade-out duration
setTimeout(() => {
if (document.body.contains(gifElement)) {
document.body.removeChild(gifElement);
}
}, real_duration * 1000);
};
export const scope = (app: Editor) => (config: OscilloscopeConfig): void => {
/**
* Configures the oscilloscope.
* @param config - The configuration object for the oscilloscope.
*/
app.osc = {
...app.osc,
...config,
};
};

View File

@ -1,22 +0,0 @@
import { type UserAPI } from "../API";
export const log = (api: UserAPI) => (message: any) => {
/**
* Logs a message to the console and app-specific logger.
* @param message - The message to log.
*/
console.log(message);
api._logMessage(message, false);
};
export const logOnce = (api: UserAPI) => (message: any) => {
/**
* Logs a message to the console and app-specific logger, but only once.
* @param message - The message to log.
*/
if (api.onceEvaluator) {
console.log(message);
api._logMessage(message, false);
api.onceEvaluator = false;
}
};

View File

@ -1,29 +0,0 @@
import { Editor } from "../../main";
export const mouseX = (app: Editor) => (): number => {
/**
* @returns The current x position of the mouse
*/
return app._mouseX;
};
export const mouseY = (app: Editor) => (): number => {
/**
* @returns The current y position of the mouse
*/
return app._mouseY;
};
export const noteX = (app: Editor) => (): number => {
/**
* @returns The current x position scaled to 0-127 using screen width
*/
return Math.floor((app._mouseX / document.body.clientWidth) * 127);
};
export const noteY = (app: Editor) => (): number => {
/**
* @returns The current y position scaled to 0-127 using screen height
*/
return Math.floor((app._mouseY / document.body.clientHeight) * 127);
};

View File

@ -1,32 +0,0 @@
import { type Editor } from '../../main';
import colorschemes from "../../Editor/colors.json";
export const theme = (app: Editor) => (color_scheme: string): void => {
app.readTheme(color_scheme);
};
export const themeName = (app: Editor) => (): string => {
return app.currentThemeName;
};
export const randomTheme = (app: Editor) => (): void => {
let theme_names = getThemes()();
let selected_theme = theme_names[Math.floor(Math.random() * theme_names.length)];
if (selected_theme) {
app.readTheme(selected_theme);
}
};
export const nextTheme = (app: Editor) => (): void => {
let theme_names = getThemes()();
let current_theme = themeName(app)();
let current_theme_idx = theme_names.indexOf(current_theme);
let next_theme_idx = (current_theme_idx + 1) % theme_names.length;
let next_theme = theme_names[next_theme_idx];
app.readTheme(next_theme!);
app.api.log(next_theme);
};
export const getThemes = () => (): string[] => {
return Object.keys(colorschemes);
};

View File

@ -1,39 +0,0 @@
import { type UserAPI } from './API';
export const drunk = (api: UserAPI) => (n?: number): number => {
/**
* This function sets or returns the current drunk mechanism's value.
* @param n - [optional] The value to set the drunk mechanism to
* @returns The current value of the drunk mechanism
*/
if (n !== undefined) {
api._drunk.position = n;
return api._drunk.getPosition();
}
api._drunk.step();
return api._drunk.getPosition();
};
export const drunk_max = (api: UserAPI) => (max: number): void => {
/**
* Sets the maximum value of the drunk mechanism.
* @param max - The maximum value of the drunk mechanism
*/
api._drunk.max = max;
};
export const drunk_min = (api: UserAPI) => (min: number): void => {
/**
* Sets the minimum value of the drunk mechanism.
* @param min - The minimum value of the drunk mechanism
*/
api._drunk.min = min;
};
export const drunk_wrap = (api: UserAPI) => (wrap: boolean): void => {
/**
* Sets whether the drunk mechanism should wrap around
* @param wrap - Whether the drunk mechanism should wrap around
*/
api._drunk.toggleWrap(wrap);
};

View File

@ -1,198 +0,0 @@
import { getAllScaleNotes } from 'zifferjs';
import {
MidiCCEvent,
MidiNoteEvent,
} from "../../IO/MidiConnection";
import { MidiEvent, MidiParams } from "../../Classes/MidiEvent";
import { UserAPI } from '../API';
import { Editor } from '../../main';
interface ControlChange {
channel: number;
control: number;
value: number;
}
export const midi_outputs = (api: UserAPI) => (): void => {
api._logMessage(api.MidiConnection.listMidiOutputs(), false);
};
export const midi_output = (api: UserAPI) => (outputName: string): void => {
if (!outputName) {
console.log(api.MidiConnection.getCurrentMidiPort());
} else {
api.MidiConnection.switchMidiOutput(outputName);
}
};
export const midi = (app: Editor) => (
value: number | number[] = 60,
velocity?: number | number[],
channel?: number | number[],
port?: number | string | number[] | string[],
): MidiEvent => {
const event = { note: value, velocity, channel, port } as MidiParams;
return new MidiEvent(event, app);
};
export const sysex = (api: UserAPI) => (data: Array<number>): void => {
api.MidiConnection.sendSysExMessage(data);
};
export const pitch_bend = (api: UserAPI) => (value: number, channel: number): void => {
api.MidiConnection.sendPitchBend(value, channel);
};
export const program_change = (api: UserAPI) => (program: number, channel: number): void => {
api.MidiConnection.sendProgramChange(program, channel);
};
export const midi_clock = (api: UserAPI) => (): void => {
api.MidiConnection.sendMidiClock();
};
export const control_change = (api: UserAPI) => ({
control = 20,
value = 0,
channel = 0,
}: ControlChange): void => {
api.MidiConnection.sendMidiControlChange(control, value, channel);
};
export const cc = control_change;
export const midi_panic = (api: UserAPI) => (): void => {
api.MidiConnection.panic();
};
export const active_note_events = (api: UserAPI) => (
channel?: number,
): MidiNoteEvent[] | undefined => {
let events;
if (channel) {
events = api.MidiConnection.activeNotesFromChannel(channel);
} else {
events = api.MidiConnection.activeNotes;
}
if (events.length > 0) return events;
else return undefined;
};
export const transmission = (api: UserAPI) => (): boolean => {
return api.MidiConnection.activeNotes.length > 0;
};
export const active_notes = (api: UserAPI) => (channel?: number): number[] | undefined => {
const events = active_note_events(api)(channel);
if (events && events.length > 0) return events.map((e) => e.note);
else return undefined;
};
export const kill_active_notes = (api: UserAPI) => (): void => {
api.MidiConnection.activeNotes = [];
};
export const sticky_notes = (api: UserAPI) => (channel?: number): number[] | undefined => {
let notes;
if (channel) notes = api.MidiConnection.stickyNotesFromChannel(channel);
else notes = api.MidiConnection.stickyNotes;
if (notes.length > 0) return notes.map((e: any) => e.note);
else return undefined;
};
export const kill_sticky_notes = (api: UserAPI) => (): void => {
api.MidiConnection.stickyNotes = [];
};
export const buffer = (api: UserAPI) => (channel?: number): boolean => {
if (channel)
return (
api.MidiConnection.findNoteFromBufferInChannel(channel) !== undefined
);
else return api.MidiConnection.noteInputBuffer.length > 0;
};
export const buffer_event = (api: UserAPI) => (channel?: number): MidiNoteEvent | undefined => {
if (channel)
return api.MidiConnection.findNoteFromBufferInChannel(channel);
else return api.MidiConnection.noteInputBuffer.shift();
};
export const buffer_note = (api: UserAPI) => (channel?: number): number | undefined => {
const note = buffer_event(api)(channel);
return note ? note.note : undefined;
};
export const last_note_event = (api: UserAPI) => (channel?: number): MidiNoteEvent | undefined => {
if (channel) return api.MidiConnection.lastNoteInChannel[channel];
else return api.MidiConnection.lastNote;
};
export const last_note = (api: UserAPI) => (channel?: number): number => {
const note = last_note_event(api)(channel);
return note ? note.note : 60;
};
export const ccIn = (api: UserAPI) => (control: number, channel?: number): number => {
if (channel) {
if (api.MidiConnection.lastCCInChannel[channel]) {
return api.MidiConnection.lastCCInChannel[channel]?.[control] ?? 0;
} else return 0;
} else return api.MidiConnection.lastCC[control] ?? 0;
};
export const has_cc = (api: UserAPI) => (channel?: number): boolean => {
if (channel)
return (
api.MidiConnection.findCCFromBufferInChannel(channel) !== undefined
);
else return api.MidiConnection.ccInputBuffer.length > 0;
};
export const buffer_cc = (api: UserAPI) => (channel?: number): MidiCCEvent | undefined => {
if (channel) return api.MidiConnection.findCCFromBufferInChannel(channel);
else return api.MidiConnection.ccInputBuffer.shift();
};
export const show_scale = (api: UserAPI) => (
root: number | string,
scale: number | string,
channel: number = 0,
port: number | string = api.MidiConnection.currentOutputIndex || 0,
soundOff: boolean = false,
): void => {
if (!api.scale_aid || scale !== api.scale_aid) {
hide_scale(api)(channel, port);
const scaleNotes = getAllScaleNotes(scale, root);
scaleNotes.forEach((note) => {
api.MidiConnection.sendMidiOn(note, channel, 1, port);
if (soundOff) api.MidiConnection.sendAllSoundOff(channel, port);
});
api.scale_aid = scale;
}
};
export const hide_scale = (api: UserAPI) => (
channel: number = 0,
port: number | string = api.MidiConnection.currentOutputIndex || 0,
): void => {
const allNotes = Array.from(Array(128).keys());
allNotes.forEach((note) => {
api.MidiConnection.sendMidiOff(note, channel, port);
});
api.scale_aid = undefined;
};
export const midi_notes_off = (api: UserAPI) => (
channel: number = 0,
port: number | string = api.MidiConnection.currentOutputIndex || 0,
): void => {
api.MidiConnection.sendAllNotesOff(channel, port);
};
export const midi_sound_off = (api: UserAPI) => (
channel: number = 0,
port: number | string = api.MidiConnection.currentOutputIndex || 0,
): void => {
api.MidiConnection.sendAllSoundOff(channel, port);
};

View File

@ -1,28 +0,0 @@
import { sendToServer, type OSCMessage } from "../../IO/OSC";
import { oscMessages } from "../../IO/OSC";
import { type Editor } from "../../main";
export const osc = (app: Editor) => (address: string, port: number, ...args: any[]): void => {
/**
* Sends an OSC message to the server.
*/
sendToServer({
address: address,
port: port,
args: args,
timetag: Math.round(Date.now() - app.clock.getTimeDeviation()),
} as OSCMessage);
};
export const getOSC = () => (address?: string): any[] => {
/**
* Retrieves incoming OSC messages. Filters by address if provided.
*/
if (address) {
let messages = oscMessages.filter((msg: { address: string; }) => msg.address === address);
messages = messages.map((msg: { data: any; }) => msg.data);
return messages;
} else {
return oscMessages;
}
};

View File

@ -1,68 +0,0 @@
import { Editor } from "../main";
import { UserAPI } from "./API";
export const line = () => (start: number, end: number, step: number = 1): number[] => {
const countPlaces = (num: number) => {
var text = num.toString();
var index = text.indexOf(".");
return index == -1 ? 0 : (text.length - index - 1);
};
const result: number[] = [];
if ((end > start && step > 0) || (end < start && step < 0)) {
for (let value = start; value <= end; value += step) {
result.push(value);
}
} else if ((end > start && step < 0) || (end < start && step > 0)) {
for (let value = start; value >= end; value -= step) {
result.push(parseFloat(value.toFixed(countPlaces(step))));
}
} else {
console.error("Invalid range or step provided.");
}
return result;
};
export const sine = (app: Editor) => (freq: number = 1, phase: number = 0): number => {
return Math.sin(2 * Math.PI * freq * (app.clock.ctx.currentTime - phase));
};
export const usine = (app: Editor) => (freq: number = 1, phase: number = 0): number => {
return ((sine(app)(freq, phase) + 1) / 2);
};
export const saw = (app: Editor) => (freq: number = 1, phase: number = 0): number => {
return (((app.clock.ctx.currentTime * freq + phase) % 1) * 2 - 1);
};
export const usaw = (app: Editor) => (freq: number = 1, phase: number = 0): number => {
return ((saw(app)(freq, phase) + 1) / 2);
};
export const triangle = (app: Editor) => (freq: number = 1, phase: number = 0): number => {
return (Math.abs(saw(app)(freq, phase)) * 2 - 1);
};
export const utriangle = (app: Editor) => (freq: number = 1, phase: number = 0): number => {
return ((triangle(app)(freq, phase) + 1) / 2);
};
export const square = (app: Editor) => (freq: number = 1, duty: number = 0.5): number => {
const period = 1 / freq;
const t = (app.clock.ctx.currentTime % period);
return (t / period < duty ? 1 : -1);
};
export const usquare = (app: Editor) => (freq: number = 1, duty: number = 0.5): number => {
return ((square(app)(freq, duty) + 1) / 2);
};
export const noise = (api: UserAPI) => (): number => {
return (api.randomGen() * 2 - 1); // Assuming randomGen() is defined in the app context
};
export const unoise = (api: UserAPI) => (): number => {
return ((noise(api)() + 1) / 2);
};

View File

@ -1,36 +0,0 @@
// mathFunctions.ts
export const min = () => (...values: number[]): number => {
/**
* Returns the minimum value of a list of numbers.
*/
return Math.min(...values);
};
export const max = () => (...values: number[]): number => {
/**
* Returns the maximum value of a list of numbers.
*/
return Math.max(...values);
};
export const mean = () => (...values: number[]): number => {
/**
* Returns the mean of a list of numbers.
*/
const sum = values.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
return values.length > 0 ? sum / values.length : 0;
};
export const limit = () => (value: number, min: number, max: number): number => {
/**
* Limits a value between a minimum and a maximum.
*/
return Math.min(Math.max(value, min), max);
};
export const abs = () => (value: number): number => {
/**
* Returns the absolute value of a number.
*/
return Math.abs(value);
};

View File

@ -1,53 +0,0 @@
import { type UserAPI } from "./API";
export const prob = (api: UserAPI) => (p: number): boolean => {
return api.randomGen() * 100 < p;
};
export const toss = (api: UserAPI) => (): boolean => {
return api.randomGen() > 0.5;
};
export const odds = (api: UserAPI) => (n: number, beats: number = 1): boolean => {
return api.randomGen() < (n * api.ppqn()) / (api.ppqn() * beats);
};
export const never = () => (): boolean => {
return false;
};
export const almostNever = (api: UserAPI) => (beats: number = 1): boolean => {
return api.randomGen() < (0.025 * api.ppqn()) / (api.ppqn() * beats);
};
export const rarely = (api: UserAPI) => (beats: number = 1): boolean => {
return api.randomGen() < (0.1 * api.ppqn()) / (api.ppqn() * beats);
};
export const scarcely = (api: UserAPI) => (beats: number = 1): boolean => {
return api.randomGen() < (0.25 * api.ppqn()) / (api.ppqn() * beats);
};
export const sometimes = (api: UserAPI) => (beats: number = 1): boolean => {
return api.randomGen() < (0.5 * api.ppqn()) / (api.ppqn() * beats);
};
export const often = (api: UserAPI) => (beats: number = 1): boolean => {
return api.randomGen() < (0.75 * api.ppqn()) / (api.ppqn() * beats);
};
export const frequently = (api: UserAPI) => (beats: number = 1): boolean => {
return api.randomGen() < (0.9 * api.ppqn()) / (api.ppqn() * beats);
};
export const almostAlways = (api: UserAPI) => (beats: number = 1): boolean => {
return api.randomGen() < (0.985 * api.ppqn()) / (api.ppqn() * beats);
};
export const always = () => (): boolean => {
return true;
};
export const dice = (api: UserAPI) => (sides: number): number => {
return Math.floor(api.randomGen() * sides) + 1;
};

View File

@ -1,35 +0,0 @@
import { seededRandom } from "zifferjs";
import { UserAPI } from "./API";
export const randI = (api: UserAPI) => (min: number, max: number): number => {
return Math.floor(api.randomGen() * (max - min + 1)) + min;
};
export const rand = (api: UserAPI) => (min: number, max: number): number => {
return api.randomGen() * (max - min) + min;
};
export const r = rand
export const seed = (api: UserAPI) => (seed: string | number): void => {
if (typeof seed === "number") seed = seed.toString();
if (api.currentSeed !== seed) {
api.currentSeed = seed;
api.randomGen = seededRandom(seed);
}
};
export const localSeededRandom = (api: UserAPI) => (seed: string | number): Function => {
if (typeof seed === "number") seed = seed.toString();
if (api.localSeeds.has(seed)) return api.localSeeds.get(seed) as Function;
const newSeededRandom = seededRandom(seed);
api.localSeeds.set(seed, newSeededRandom);
return newSeededRandom;
};
export const clearLocalSeed = (api: UserAPI) => (seed: string | number | undefined = undefined): void => {
if (seed) {
api.localSeeds.delete(seed.toString());
} else {
api.localSeeds.clear();
}
};

View File

@ -1,64 +0,0 @@
import { tryEvaluate } from "../Evaluator";
import { blinkScript } from "../DOM/Visuals/Blinkers";
import { template_universes } from "../Editor/FileManagement";
import { Editor } from "../main";
export const script = (app: Editor) => (...args: number[]): void => {
args.forEach((arg) => {
if (arg >= 1 && arg <= 9) {
blinkScript(app, "local", arg);
tryEvaluate(
app,
app.universes[app.selected_universe]!.locals[arg]!,
);
}
});
};
export const s = script;
export const delete_script = (app: Editor) => (script: number): void => {
app.universes[app.selected_universe]!.locals[script] = {
candidate: "",
committed: "",
evaluations: 0,
};
};
export const copy_script = (app: Editor) => (from: number, to: number): void => {
//@ts-ignore
app.universes[app.selected_universe].locals[to] = {
...app.universes[app.selected_universe]!.locals[from],
};
};
export const copy_universe = (app: Editor) => (from: string, to: string): void => {
//@ts-ignore
app.universes[to] = { ...app.universes[from], };
};
export const delete_universe = (app: Editor) => (universe: string): void => {
if (app.selected_universe === universe) {
app.selected_universe = "Default";
}
delete app.universes[universe];
app.settings.saveApplicationToLocalStorage(
app.universes,
app.settings,
);
app.updateKnownUniversesView();
};
export const big_bang = (app: Editor) => (): void => {
if (confirm("Are you sure you want to delete all universes?")) {
app.universes = {
...template_universes, // Assuming template_universes is defined elsewhere
};
app.settings.saveApplicationToLocalStorage(
app.universes,
app.settings,
);
}
app.selected_universe = "Default";
app.updateKnownUniversesView();
};

View File

@ -1,44 +0,0 @@
import { SoundEvent } from "../Classes/SoundEvent";
import { SkipEvent } from "../Classes/SkipEvent";
import { Editor } from "../main";
export const sound = (app: Editor) => (sound: string | string[] | null | undefined) => {
/**
* Creates a sound event if a sound is specified, otherwise returns a skip event.
* @param sound - The sound identifier or array of identifiers to play.
* @returns SoundEvent if sound is defined, otherwise SkipEvent.
*/
if (sound) return new SoundEvent(sound, app);
else return new SkipEvent();
};
export const snd = sound;
export const speak = () => (text: string, lang: string = "en-US", voiceIndex: number = 0, rate: number = 1, pitch: number = 1): void => {
/**
* Speaks the given text using the browser's speech synthesis API.
* @param text - The text to speak.
* @param lang - The language code (e.g., "en-US").
* @param voiceIndex - The index of the voice to use from the speechSynthesis voice list.
* @param rate - The rate at which to speak the text.
* @param pitch - The pitch at which to speak the text.
*/
const msg = new SpeechSynthesisUtterance(text);
msg.lang = lang;
msg.rate = rate;
msg.pitch = pitch;
// Set the voice using a provided index
const voices = window.speechSynthesis.getVoices();
msg.voice = voices[voiceIndex] || null;
window.speechSynthesis.speak(msg);
msg.onend = () => {
console.log("Finished speaking:", text);
};
msg.onerror = (event) => {
console.error("Speech synthesis error:", event);
};
};

View File

@ -1,215 +0,0 @@
import { type Editor } from "../../main";
import { UserAPI } from "../API";
const _euclidean_cycle = (
pulses: number,
length: number,
rotate: number = 0,
): boolean[] => {
if (pulses == length) return Array.from({ length }, () => true);
function startsDescent(list: number[], i: number): boolean {
const length = list.length;
const nextIndex = (i + 1) % length;
return list[i]! > list[nextIndex]!? true : false;
}
if (pulses >= length) return [true];
const resList = Array.from(
{ length },
(_, i) => (((pulses * (i - 1)) % length) + length) % length,
);
let cycle = resList.map((_, i) => startsDescent(resList, i));
if (rotate != 0) {
cycle = cycle.slice(rotate).concat(cycle.slice(0, rotate));
}
return cycle;
}
export const fullseq = () => (sequence: string, duration: number): boolean | Array<boolean> => {
if (sequence.split("").every((c) => c === "x" || c === "o")) {
return [...sequence].map((c) => c === "x").beat(duration);
} else {
return false;
}
};
export const seq = (app: any) => (expr: string, duration: number = 0.5): boolean => {
let len = expr.length * duration;
let output: number[] = [];
for (let i = 1; i <= len + 1; i += duration) {
output.push(Math.floor(i * 10) / 10);
}
output.pop();
output = output.filter((_, idx) => {
const exprIdx = idx % expr.length;
return expr[exprIdx] === "x";
});
return oncount(app)(output, len);
};
export const beat = (app: Editor) => (n: number | number[] = 1, nudge: number = 0): boolean => {
const nArray = Array.isArray(n) ? n : [n];
const results: boolean[] = nArray.map(
(value) =>
(app.clock.time_position.grain - Math.round(nudge * app.clock.time_position.ppqn)) %
Math.round(value * app.clock.time_position.ppqn) === 0,
);
return results.some((value) => value === true);
};
// export const beat = (app: Editor) => (n: number | number[] = 1, nudge: number = 0): boolean => {
// const nArray = !Array.isArray(n) ? [n] : n;
// return nArray.some(
// (value) =>
// !((app.clock.time_position.grain - nudge * app.clock.time_position.ppqn) % Math.floor(value * app.clock.time_position.ppqn))
// );
// };
export const bar = (app: Editor) => (n: number | number[] = 1, nudge: number = 0): boolean => {
const nArray = Array.isArray(n) ? n : [n];
const barLength = app.clock.time_position.num * app.clock.ppqn;
const nudgeInPulses = Math.floor(nudge * barLength);
const results: boolean[] = nArray.map(
(value) =>
(app.clock.grain - nudgeInPulses) %
Math.floor(value * barLength) === 0,
);
return results.some((value) => value === true);
};
export const pulse = (app: Editor) => (n: number | number[] = 1, nudge: number = 0): boolean => {
const nArray = Array.isArray(n) ? n : [n];
const results: boolean[] = nArray.map(
(value) => (app.clock.grain - nudge) % value === 0,
);
return results.some((value) => value === true);
};
export const tick = (app: Editor) => (tick: number | number[], offset: number = 0): boolean => {
const nArray = Array.isArray(tick) ? tick : [tick];
const results: boolean[] = nArray.map(
(value) => app.clock.time_position.tick === value + offset,
);
return results.some((value) => value === true);
};
export const dur = (app: Editor) => (n: number | number[]): boolean => {
let nums: number[] = Array.isArray(n) ? n : [n];
return beat(app)(nums.dur(...nums));
};
export const flip = (app: Editor) => (chunk: number, ratio: number = 50): boolean => {
let realChunk = chunk * 2;
const time_pos = app.clock.grain;
const full_chunk = Math.floor(realChunk * app.clock.ppqn);
const threshold = Math.floor((ratio / 100) * full_chunk);
const pos_within_chunk = time_pos % full_chunk;
return pos_within_chunk < threshold;
};
export const flipbar = (app: Editor) => (chunk: number = 1): boolean => {
let realFlip = chunk;
const time_pos = app.clock.time_position.bar;
const current_chunk = Math.floor(time_pos / realFlip);
return current_chunk % 2 === 0;
};
export const onbar = (app: Editor) => (
bars: number[] | number,
n: number = app.clock.time_position.num,
): boolean => {
let current_bar = (app.clock.time_position.bar % n) + 1;
return typeof bars === "number"
? bars === current_bar
: bars.some((b) => b === current_bar);
};
export const onbeat = (api: UserAPI) => (...beat: number[]): boolean => {
let final_pulses: boolean[] = [];
beat.forEach((b) => {
let beatNumber = b % api.nominator() || api.nominator();
let integral_part = Math.floor(beatNumber);
integral_part = integral_part === 0 ? api.nominator() : integral_part;
let decimal_part = Math.floor((beatNumber - integral_part) * api.app.clock.ppqn + 1);
if (decimal_part <= 0)
decimal_part += api.app.clock.ppqn * api.nominator();
final_pulses.push(
integral_part === api.cbeat() && api.cpulse() === decimal_part,
);
});
return final_pulses.some((p) => p === true);
};
export const oncount = (app: Editor) => (beats: number[] | number, count: number): boolean => {
if (typeof beats === "number") beats = [beats];
const origin = app.clock.grain;
let final_pulses: boolean[] = [];
beats.forEach((b) => {
b = b < 1 ? 0 : b - 1;
const beatInTicks = Math.ceil(b * app.clock.ppqn);
const meterPosition = origin % (app.clock.ppqn * count);
final_pulses.push(meterPosition === beatInTicks);
});
return final_pulses.some((p) => p === true);
};
export const oneuclid = (app: Editor) => (pulses: number, length: number, rotate: number = 0): boolean => {
const cycle = _euclidean_cycle(pulses, length, rotate);
const beats = cycle.reduce((acc: number[], x: boolean, i: number) => {
if (x) acc.push(i + 1);
return acc;
}, []);
return oncount(app)(beats, length);
};
export const euclid = () => (iterator: number, pulses: number, length: number, rotate: number = 0): boolean => {
/**
* Returns a Euclidean cycle of size length, with n pulses, rotated or not.
*/
const cycle = _euclidean_cycle(pulses, length, rotate);
return cycle && cycle[iterator % length] === true;
};
export const ec = euclid;
export const rhythm = (app: Editor) => (div: number, pulses: number, length: number, rotate: number = 0): boolean => {
/**
* Returns a rhythm based on Euclidean cycle.
*/
return (
beat(app)(div) && _euclidean_cycle(pulses, length, rotate).beat(div)
);
};
export const ry = rhythm;
export const nrhythm = (app: Editor) => (div: number, pulses: number, length: number, rotate: number = 0): boolean => {
/**
* Returns a negated rhythm based on Euclidean cycle.
*/
let rhythm = _euclidean_cycle(pulses, length, rotate).map((n: any) => !n);
return (
beat(app)(div) && rhythm.beat(div)
);
};
export const nry = nrhythm;
export const bin = () => (iterator: number, n: number): boolean => {
/**
* Returns a binary cycle of size n.
*/
let convert: string = n.toString(2);
let tobin: boolean[] = convert.split("").map((x: string) => x === "1");
return tobin[iterator % tobin.length] || false;
};
export const binrhythm = (app: Editor) => (div: number, n: number): boolean => {
/**
* Returns a binary rhythm based on division and binary cycle.
*/
let convert: string = n.toString(2);
let tobin: boolean[] = convert.split("").map((x: string) => x === "1");
return beat(app)(div) && tobin.beat(div);
};
export const bry = binrhythm;

View File

@ -1,103 +0,0 @@
import { type UserAPI } from "../API";
import { type Editor } from "../../main";
export const time = (api: UserAPI) => (): number => {
return api.app.audioContext.currentTime;
};
export const play = (api: UserAPI) => (): void => {
api.app.setButtonHighlighting("play", true);
api.MidiConnection.sendStartMessage();
api.app.clock.start();
};
export const pause = (api: UserAPI) => (): void => {
api.app.setButtonHighlighting("pause", true);
api.app.clock.pause();
};
export const stop = (api: UserAPI) => (): void => {
api.app.setButtonHighlighting("stop", true);
api.app.clock.stop();
};
export const silence = (api: UserAPI) => (): void => {
return stop(api)();
};
export const tempo = (app: Editor) => (n?: number): number => {
/**
* Sets or returns the current bpm.
*/
if (n === undefined) return app.clock.bpm;
if (n >= 1 && n <= 500) {
app.clock.bpm = n;
} else {
console.error("BPM out of acceptable range (1-500).");
}
return n;
};
export const ppqn = (app: Editor) => (n?: number): number => {
/**
* Sets or returns the number of pulses per quarter note.
*/
if (n === undefined) return app.clock.ppqn;
if (n >= 1) {
app.clock.ppqn = n;
} else {
console.error("Pulses per quarter note must be at least 1.");
}
return n;
};
export const time_signature = (app: Editor) => (numerator: number, denominator: number): void => {
/**
* Sets the time signature.
*/
if (numerator < 1 || denominator < 1) {
console.error("Time signature values must be at least 1.");
} else {
app.clock.setSignature(numerator, denominator);
}
};
export const cbar = (app: Editor) => (): number => {
return app.clock.time_position.bar + 1;
};
export const ctick = (app: Editor) => (): number => {
return app.clock.grain + 1;
};
export const cpulse = (app: Editor) => (): number => {
return app.clock.time_position.tick + 1;
};
export const cbeat = (app: Editor) => (): number => {
return app.clock.time_position.beat + 1;
};
export const ebeat = (app: Editor) => (): number => {
return app.clock.beats_since_origin + 1;
};
export const epulse = (app: Editor) => (): number => {
return app.clock.grain + 1;
};
export const nominator = (app: Editor) => (): number => {
return app.clock.time_position.num;
};
export const meter = (app: Editor) => (): number => {
return app.clock.time_position.den;
};
export const denominator = meter;
export const pulsesForBar = (app: Editor) => (): number => {
return (app.clock.bpm * app.clock.ppqn * nominator(app)()) / 60;
};

View File

@ -1,18 +0,0 @@
import { Editor } from "../../main";
export const warp = (app: Editor) => (n: number): void => {
/**
* Time-warp the clock by using the tick you wish to jump to.
*/
app.clock.time_position.tick = n;
app.clock.time_position = app.clock.convertTicksToTimeposition(n);
};
export const beat_warp = (app: Editor) => (beat: number): void => {
/**
* Time-warp the clock by using the tick you wish to jump to.
*/
const ticks = beat * app.clock.ppqn;
app.clock.time_position.tick = ticks;
app.clock.time_position = app.clock.convertTicksToTimeposition(ticks);
};

View File

@ -1,72 +0,0 @@
import { InputOptions, Player } from "../Classes/ZPlayer";
import { UserAPI } from "./API";
import { generateCacheKey, removePatternFromCache } from "./Cache"
export const z = (api: UserAPI) => (input: string | Generator<number>, options: InputOptions = {}, id: number | string = ""): Player => {
const zid = "z" + id.toString();
const key = id === "" ? generateCacheKey()(input, options) : zid;
const validSyntax = typeof input === "string" && !api.invalidPatterns[input]
let player;
let replace = false;
if (api.patternCache.has(key)) {
player = api.patternCache.get(key) as Player;
if (typeof input === "string" &&
player.input !== input &&
(player.atTheBeginning() || api.forceEvaluator)) {
replace = true;
}
}
if ((typeof input !== "string" || validSyntax) && (!player || replace)) {
if (typeof input === "string" && player && api.forceEvaluator) {
if (!player.updatePattern(input, options)) {
api.logOnce(`Invalid syntax: ${input}`);
};
api.forceEvaluator = false;
} else {
const newPlayer = player ? new Player(input, options, api.app, zid, player.nextEndTime()) : new Player(input, options, api.app, zid);
if (newPlayer.isValid()) {
player = newPlayer;
api.patternCache.set(key, player);
} else if (typeof input === "string") {
api.invalidPatterns[input] = true;
}
}
}
if (player) {
if (player.atTheBeginning()) {
if (typeof input === "string" && !validSyntax) api.log(`Invalid syntax: ${input}`);
}
if (player.ziffers.generator && player.ziffers.generatorDone) {
removePatternFromCache(api)(key);
}
if (typeof id === "number") player.zid = zid;
player.updateLastCallTime();
if (id !== "" && zid !== "z0") {
// Sync named patterns to z0 by default
player.sync("z0", false);
}
return player;
} else {
throw new Error(`Invalid syntax: ${input}`);
}
};
// Generating numbered functions dynamically
export const generateZFunctions = (api: UserAPI) => {
const zFunctions: { [key: string]: (input: string, opts: InputOptions) => Player } = {};
for (let i = 0; i <= 16; i++) {
zFunctions[`z${i}`] = (input: string, opts: InputOptions = {}) => z(api)(input, opts, i);
}
return zFunctions;
};

View File

@ -1,6 +1,122 @@
// @ts-ignore
import { getAnalyser } from "superdough";
import { Editor } from "../../main";
import { type Editor } from "./main";
/**
* Draw a circle at a specific position on the canvas.
* @param {number} x - The x-coordinate of the circle's center.
* @param {number} y - The y-coordinate of the circle's center.
* @param {number} radius - The radius of the circle.
* @param {string} color - The fill color of the circle.
*/
export const drawCircle = (
app: Editor,
x: number,
y: number,
radius: number,
color: string
): void => {
// @ts-ignore
const canvas: HTMLCanvasElement = app.interface.feedback;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
};
/**
* Blinks a script indicator circle.
* @param script - The type of script.
* @param no - The shift amount multiplier.
*/
export const blinkScript = (
app: Editor,
script: "local" | "global" | "init",
no?: number
) => {
if (no !== undefined && no < 1 && no > 9) return;
const blinkDuration =
(app.clock.bpm / 60 / app.clock.time_signature[1]) * 200;
// @ts-ignore
const ctx = app.interface.feedback.getContext("2d"); // Assuming a canvas context
/**
* Draws a circle at a given shift.
* @param shift - The pixel distance from the origin.
*/
const _drawBlinker = (shift: number) => {
const horizontalOffset = 50;
drawCircle(
app,
horizontalOffset + shift,
app.interface.feedback.clientHeight - 15,
8,
"#fdba74"
);
};
/**
* Clears the circle at a given shift.
* @param shift - The pixel distance from the origin.
*/
const _clearBlinker = (shift: number) => {
const x = 50 + shift;
const y = app.interface.feedback.clientHeight - 15;
const radius = 8;
ctx.clearRect(x - radius, y - radius, radius * 2, radius * 2);
};
if (script === "local" && no !== undefined) {
const shiftAmount = no * 25;
// Clear existing timeout if any
if (app.blinkTimeouts[shiftAmount]) {
clearTimeout(app.blinkTimeouts[shiftAmount]);
}
_drawBlinker(shiftAmount);
// Save timeout ID for later clearing
// @ts-ignore
app.blinkTimeouts[shiftAmount] = setTimeout(() => {
_clearBlinker(shiftAmount);
// Clear the canvas before drawing new blinkers
(app.interface.feedback as HTMLCanvasElement)
.getContext("2d")!
.clearRect(
0,
0,
(app.interface.feedback as HTMLCanvasElement).width,
(app.interface.feedback as HTMLCanvasElement).height
);
}, blinkDuration);
}
};
/**
* Manages animation updates using requestAnimationFrame.
* @param app - The Editor application context.
*/
export const scriptBlinkers = () => {
let lastFrameTime = Date.now();
const frameRate = 10;
const minFrameDelay = 1000 / frameRate;
const update = () => {
const now = Date.now();
const timeSinceLastFrame = now - lastFrameTime;
if (timeSinceLastFrame >= minFrameDelay) {
lastFrameTime = now;
}
requestAnimationFrame(update);
};
requestAnimationFrame(update);
};
export interface OscilloscopeConfig {
enabled: boolean;
@ -18,16 +134,15 @@ export interface OscilloscopeConfig {
let lastZeroCrossingType: string | null = null; // 'negToPos' or 'posToNeg'
let lastRenderTime: number = 0;
/**
* Initializes and runs an oscilloscope using an AnalyzerNode.
* @param {HTMLCanvasElement} canvas - The canvas element to draw the oscilloscope.
* @param {OscilloscopeConfig} config - Configuration for the oscilloscope's appearance and behavior.
*/
export const runOscilloscope = (
canvas: HTMLCanvasElement,
app: Editor,
app: Editor
): void => {
/**
* Runs the oscilloscope visualization on the provided canvas element.
*
* @param canvas - The HTMLCanvasElement on which to render the visualization.
* @param app - The Editor object containing the configuration for the oscilloscope.
*/
let config = app.osc;
let analyzer = getAnalyser(config.fftSize);
let dataArray = new Float32Array(analyzer.frequencyBinCount);
@ -40,7 +155,7 @@ export const runOscilloscope = (
width: number,
height: number,
offset_height: number,
offset_width: number,
offset_width: number
) {
const maxFPS = 30;
const now = performance.now();
@ -54,12 +169,10 @@ export const runOscilloscope = (
canvasCtx.clearRect(0, 0, width, height);
const performanceFactor = 1;
const reducedDataSize = Math.floor(
freqDataArray.length * performanceFactor,
);
const reducedDataSize = Math.floor(freqDataArray.length * performanceFactor);
const numBars = Math.min(
reducedDataSize,
app.osc.orientation === "horizontal" ? width : height,
app.osc.orientation === "horizontal" ? width : height
);
const barWidth =
app.osc.orientation === "horizontal" ? width / numBars : height / numBars;
@ -71,8 +184,7 @@ export const runOscilloscope = (
for (let i = 0; i < numBars; i++) {
barHeight = Math.floor(
freqDataArray[Math.floor((i * freqDataArray.length) / numBars)]! *
((height / 256) * app.osc.size),
freqDataArray[Math.floor(i * freqDataArray.length / numBars)] * ((height / 256) * app.osc.size)
);
if (app.osc.orientation === "horizontal") {
@ -80,7 +192,7 @@ export const runOscilloscope = (
x + offset_width,
(height - barHeight) / 2 + offset_height,
barWidth + 1,
barHeight,
barHeight
);
x += barWidth;
} else {
@ -88,13 +200,14 @@ export const runOscilloscope = (
(width - barHeight) / 2 + offset_width,
y + offset_height,
barHeight,
barWidth + 1,
barWidth + 1
);
y += barWidth;
}
}
}
function draw() {
// Update the canvas position on each cycle
const WIDTH = canvas.width;
@ -117,19 +230,12 @@ export const runOscilloscope = (
-OFFSET_WIDTH,
-OFFSET_HEIGHT,
WIDTH + 2 * OFFSET_WIDTH,
HEIGHT + 2 * OFFSET_HEIGHT,
HEIGHT + 2 * OFFSET_HEIGHT
);
return;
}
if (analyzer.fftSize !== app.osc.fftSize) {
// Disconnect and release the old analyzer if it exists
if (analyzer) {
analyzer.disconnect();
analyzer = null; // Release the reference for garbage collection
}
// Create a new analyzer with the updated FFT size
analyzer = getAnalyser(app.osc.fftSize);
dataArray = new Float32Array(analyzer.frequencyBinCount);
}
@ -139,25 +245,25 @@ export const runOscilloscope = (
canvasCtx.fillStyle = "rgba(0, 0, 0, 0)";
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
if (app.clock.time_position.tick % app.osc.refresh == 0) {
if (app.clock.time_position.pulse % app.osc.refresh == 0) {
canvasCtx.clearRect(
-OFFSET_WIDTH,
-OFFSET_HEIGHT,
WIDTH + 2 * OFFSET_WIDTH,
HEIGHT + 2 * OFFSET_HEIGHT,
HEIGHT + 2 * OFFSET_HEIGHT
);
}
canvasCtx.lineWidth = app.osc.thickness;
if (app.osc.color === "random") {
if (app.clock.time_position.tick % 16 === 0) {
if (app.clock.time_position.pulse % 16 === 0) {
canvasCtx.strokeStyle = `hsl(${Math.random() * 360}, 100%, 50%)`;
}
} else {
canvasCtx.strokeStyle = app.osc.color;
}
const remainingRefreshTime =
app.clock.time_position.tick % app.osc.refresh;
app.clock.time_position.pulse % app.osc.refresh;
const opacityRatio = 1 - remainingRefreshTime / app.osc.refresh;
canvasCtx.globalAlpha = opacityRatio;
canvasCtx.beginPath();
@ -165,9 +271,9 @@ export const runOscilloscope = (
let startIndex = 0;
for (let i = 1; i < dataArray.length; ++i) {
let currentType = null;
if (dataArray[i]! >= 0 && dataArray[i - 1]! < 0) {
if (dataArray[i] >= 0 && dataArray[i - 1] < 0) {
currentType = "negToPos";
} else if (dataArray[i]! < 0 && dataArray[i - 1]! >= 0) {
} else if (dataArray[i] < 0 && dataArray[i - 1] >= 0) {
currentType = "posToNeg";
}
@ -187,8 +293,8 @@ export const runOscilloscope = (
drawFrequencyScope(WIDTH, HEIGHT, OFFSET_HEIGHT, OFFSET_WIDTH);
} else if (app.osc.mode === "3D") {
for (let i = startIndex; i < dataArray.length; i += 2) {
const x = (dataArray[i]! * WIDTH * app.osc.size) / 2 + WIDTH / 4;
const y = (dataArray[i + 1]! * HEIGHT * app.osc.size) / 2 + HEIGHT / 4;
const x = (dataArray[i] * WIDTH * app.osc.size) / 2 + WIDTH / 4;
const y = (dataArray[i + 1] * HEIGHT * app.osc.size) / 2 + HEIGHT / 4;
i === startIndex ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y);
}
} else if (
@ -199,7 +305,7 @@ export const runOscilloscope = (
const yOffset = HEIGHT / 4;
let x = 0;
for (let i = startIndex; i < dataArray.length; i++) {
const v = dataArray[i]! * 0.5 * HEIGHT * app.osc.size;
const v = dataArray[i] * 0.5 * HEIGHT * app.osc.size;
const y = v + yOffset;
i === startIndex ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y);
x += sliceWidth;
@ -210,7 +316,7 @@ export const runOscilloscope = (
const xOffset = WIDTH / 4;
let y = 0;
for (let i = startIndex; i < dataArray.length; i++) {
const v = dataArray[i]! * 0.5 * WIDTH * app.osc.size;
const v = dataArray[i] * 0.5 * WIDTH * app.osc.size;
const x = v + xOffset;
i === startIndex ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y);
y += sliceHeight;

394
src/Clock.ts Normal file
View File

@ -0,0 +1,394 @@
// @ts-ignore
import { Editor } from "./main";
import { tryEvaluate } from "./Evaluator";
const zeroPad = (num: number, places: number) =>
String(num).padStart(places, "0");
export interface TimePosition {
/**
* A position in time.
*
* @param bar - The bar number
* @param beat - The beat number
* @param pulse - The pulse number
*/
bar: number;
beat: number;
pulse: number;
}
export class Clock {
/**
* The Clock Class is responsible for keeping track of the current time.
* It is also responsible for starting and stopping the Clock TransportNode.
*
* @param app - The main application instance
* @param ctx - The current AudioContext used by app
* @param bpm - The current beats per minute value
* @param time_signature - The time signature
* @param time_position - The current time position
* @param ppqn - The pulses per quarter note
* @param tick - The current tick since origin
* @param running - Is the clock running?
* @param lastPauseTime - The last time the clock was paused
* @param lastPlayPressTime - The last time the clock was started
* @param totalPauseTime - The total time the clock has been paused / stopped
* @param _nudge - The current nudge value
*/
lastPulseAt: number;
afterEvaluation: number;
private _bpm: number;
time_signature: number[];
time_position: TimePosition;
private _ppqn: number;
tick: number;
running: boolean;
private timerWorker: Worker | null = null;
_nudge: number;
timeviewer: HTMLElement;
constructor(public app: Editor) {
this.timeviewer = document.getElementById("timeviewer")!;
this.time_position = { bar: 0, beat: 0, pulse: 0 };
this.time_signature = [4, 4];
this.lastPulseAt = 0;
this.afterEvaluation = 0;
this.tick = 0;
this._bpm = 120;
this._ppqn = 48;
this._nudge = 0;
this.running = false;
}
private initializeWorker(): void {
/**
* Initializes the worker responsible for sending clock pulses. The worker
* is responsible for sending clock pulses at a regular interval. The
* interval is set by the `setWorkerInterval` function. The worker is
* restarted when the BPM is changed. The worker is terminated when the
* clock is stopped.
*
* @returns void
*/
const workerScript =
"onmessage = (e) => { setInterval(() => { postMessage(true) }, e.data)}";
const blob = new Blob([workerScript], { type: "text/javascript" });
this.timerWorker = new Worker(URL.createObjectURL(blob));
this.timerWorker.onmessage = () => {
this.run();
};
}
private setWorkerInterval(): void {
/**
* Sets the interval for the worker responsible for sending clock pulses.
* The interval is set by calculating the duration of one pulse. The
* duration of one pulse is calculated by dividing the duration of one beat
* by the number of pulses per quarter note.
*
* @remark The BPM is off constantly by 3~5 BPM.
* @returns void
*/
const beatDurationMs = 60000 / this._bpm;
const pulseDurationMs = beatDurationMs / this._ppqn;
this.timerWorker?.postMessage(pulseDurationMs);
}
private run = () => {
/**
* This function is called by the worker responsible for sending clock
* pulses. It is called at a regular interval. The interval is set by the
* `setWorkerInterval` function. This function is responsible for updating
* the time position and sending MIDI clock messages. It is also responsible
* for evaluating the global buffer. The global buffer is evaluated at the
* beginning of each pulse.
*
* @returns void
*/
if (this.running) {
this.lastPulseAt = performance.now();
const futureTimeStamp = this.convertTicksToTimeposition(this.tick);
this.time_position = futureTimeStamp;
if (this.app.settings.send_clock) {
this.app.api.MidiConnection.sendMidiClock();
}
if (futureTimeStamp.pulse % this.app.clock.ppqn == 0) {
this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${
futureTimeStamp.beat + 1
} / ${this.bpm}`;
}
if (this.app.exampleIsPlaying) {
tryEvaluate(this.app, this.app.example_buffer);
} else {
tryEvaluate(this.app, this.app.global_buffer);
}
this.afterEvaluation = performance.now();
console.log("DEVIATION", this.deviation);
this.tick++;
}
};
convertTicksToTimeposition(ticks: number): TimePosition {
/**
* This function converts a number of ticks to a time position.
* @param ticks - number of ticks
* @returns time position
*/
const beatsPerBar = this.app.clock.time_signature[0];
const ppqnPosition = ticks % this.app.clock.ppqn;
const beatNumber = Math.floor(ticks / this.app.clock.ppqn);
const barNumber = Math.floor(beatNumber / beatsPerBar);
const beatWithinBar = Math.floor(beatNumber % beatsPerBar);
return { bar: barNumber, beat: beatWithinBar, pulse: ppqnPosition };
}
get ticks_before_new_bar(): number {
/**
* This function returns the number of ticks separating the current moment
* from the beginning of the next bar.
*
* @returns number of ticks until next bar
*/
const ticskMissingFromBeat = this.ppqn - this.time_position.pulse;
const beatsMissingFromBar = this.beats_per_bar - this.time_position.beat;
return beatsMissingFromBar * this.ppqn + ticskMissingFromBeat;
}
get next_beat_in_ticks(): number {
/**
* This function returns the number of ticks separating the current moment
* from the beginning of the next beat.
*
* @returns number of ticks until next beat
*/
return this.app.clock.pulses_since_origin + this.time_position.pulse;
}
get beats_per_bar(): number {
/**
* Returns the number of beats per bar.
*
* @returns number of beats per bar
*/
return this.time_signature[0];
}
get beats_since_origin(): number {
/**
* Returns the number of beats since the origin.
*
* @returns number of beats since origin
*/
return Math.floor(this.tick / this.ppqn);
}
get pulses_since_origin(): number {
/**
* Returns the number of pulses since the origin.
*
* @returns number of pulses since origin
*/
return this.tick;
}
get pulse_duration(): number {
/**
* Returns the duration of a pulse in seconds.
*
* @returns duration of a pulse in seconds
*/
return 60 / this._bpm / this.ppqn;
}
public pulse_duration_at_bpm(bpm: number = this.bpm): number {
/**
* Returns the duration of a pulse in seconds at a specific bpm.
*
* @param bpm - beats per minute
* @returns duration of a pulse in seconds
*/
return 60 / bpm / this.ppqn;
}
get bpm(): number {
/**
* Returns the current BPM.
*
* @returns current BPM
*/
return this._bpm;
}
set nudge(nudge: number) {
/**
* Sets the nudge.
*
* @param nudge - nudge in seconds
* @returns void
*/
this._nudge = nudge;
}
get nudge(): number {
/**
* Returns the current nudge.
*
* @returns current nudge
*/
return this._nudge;
}
set bpm(bpm: number) {
/**
* Sets the BPM.
*
* @param bpm - beats per minute
* @returns void
*/
if (bpm > 0 && this._bpm !== bpm) {
this._bpm = bpm;
// Restart the worker with the new BPM if the clock is running
if (this.running) {
this.restartWorker();
}
}
}
private restartWorker(): void {
/**
* Restarts the worker responsible for sending clock pulses.
*
* @returns void
*/
if (this.timerWorker) {
this.timerWorker.terminate();
}
this.initializeWorker();
this.setWorkerInterval();
}
get ppqn(): number {
/**
* Returns the current PPQN.
*
* @returns current PPQN
*/
return this._ppqn;
}
get realTime(): number {
/**
* Returns the current time of the audio context.
*
* @returns current time of the audio context
* @remark This is the time of the audio context, not the time of the clock.
*/
return this.lastPulseAt;
}
get deviation(): number {
/**
* Returns the deviation between the logical time and the real time.
*
* @returns deviation between the logical time and the real time
*/
if(this.afterEvaluation<this.lastPulseAt) return 0;
return (this.afterEvaluation - this.lastPulseAt) / 1000;
}
set ppqn(ppqn: number) {
/**
* Sets the PPQN.
*
* @param ppqn - pulses per quarter note
* @returns void
*/
if (ppqn > 0 && this._ppqn !== ppqn) {
this._ppqn = ppqn;
}
}
public nextTickFrom(time: number, nudge: number): number {
/**
* Compute the time remaining before the next clock tick.
* @param time - audio context currentTime
* @param nudge - nudge in the future (in seconds)
* @returns remainingTime
*/
const pulseDuration = this.pulse_duration;
const nudgedTime = time + nudge;
const nextTickTime = Math.ceil(nudgedTime / pulseDuration) * pulseDuration;
const remainingTime = nextTickTime - nudgedTime;
return remainingTime;
}
public convertPulseToSecond(n: number): number {
/**
* Converts a number of pulses to a number of seconds.
*
* @param n - number of pulses
* @returns number of seconds
*/
return n * this.pulse_duration;
}
public start(): void {
/**
* This function starts the worker.
*
* @remark also sends a MIDI message if a port is declared
* @returns void
*/
if (this.running) {
return;
}
this.running = true;
this.app.api.MidiConnection.sendStartMessage();
this.lastPulseAt = 0;
this.afterEvaluation = 0;
if (!this.timerWorker) {
this.initializeWorker();
}
this.setWorkerInterval();
}
public pause(): void {
/**
* Pauses the Transport worker.
*
* @remark also sends a MIDI message if a port is declared
* @returns void
*/
this.running = false;
this.app.api.MidiConnection.sendStopMessage();
if (this.timerWorker) {
this.timerWorker.terminate();
this.timerWorker = null;
}
}
public stop(): void {
/**
* Stops the Transport worker and resets the tick to 0. The time position
* is also reset to 0. The clock is stopped by terminating the worker
* responsible for sending clock pulses.
*
* @remark also sends a MIDI message if a port is declared
* @returns void
*/
this.running = false;
this.tick = 0;
this.time_position = { bar: 0, beat: 0, pulse: 0 };
this.app.api.MidiConnection.sendStopMessage();
if (this.timerWorker) {
this.timerWorker.terminate();
this.timerWorker = null;
}
}
}

View File

@ -1,122 +0,0 @@
import { type Editor } from "../../main";
const HORIZONTALOFFSETPERCENT = 0.025;
const VERTICALOFFSETPERCENT = 0.025;
const RADIUSPERCENT = 0.010;
const SHIFTPERCENT = 0.025;
export const drawCircle = (
/**
* Draw a circle at a specific position on the canvas.
* @param {number} x - The x-coordinate of the circle's center.
* @param {number} y - The y-coordinate of the circle's center.
* @param {number} radius - The radius of the circle.
* @param {string} color - The fill color of the circle.
*/
app: Editor,
x: number,
y: number,
radius: number,
color: string,
): void => {
// @ts-ignore
const canvas: HTMLCanvasElement = app.interface.feedback;
const ctx = canvas.getContext("2d");
console.log(`Canvas size: ${canvas.width}x${canvas.height}`);
if (!ctx) return;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
};
export const blinkScript = (
/**
* Blinks a script indicator circle.
* @param script - The type of script.
* @param no - The shift amount multiplier.
*/
app: Editor,
script: "local" | "global" | "init",
no?: number,
) => {
if (no !== undefined && no < 1 && no > 9) return;
const blinkDuration =
(app.clock.bpm / 60 / app.clock.time_position.num) * 200;
// @ts-ignore
const ctx = app.interface.feedback.getContext("2d");
const _drawBlinker = (shift: number) => {
const horizontalOffsetPercent = HORIZONTALOFFSETPERCENT;
const verticalOffsetPercent = VERTICALOFFSETPERCENT;
const radiusPercent = RADIUSPERCENT;
drawCircle(
app,
(app.interface.feedback as HTMLCanvasElement).width * horizontalOffsetPercent + shift,
(app.interface.feedback as HTMLCanvasElement).height * (1 - verticalOffsetPercent),
(app.interface.feedback as HTMLCanvasElement).width * radiusPercent,
"#fdba74",
);
};
const _clearBlinker = (shift: number) => {
const horizontalOffsetPercent = HORIZONTALOFFSETPERCENT;
const verticalOffsetPercent = VERTICALOFFSETPERCENT;
const radiusPercent = RADIUSPERCENT;
const x = (app.interface.feedback as HTMLCanvasElement).width * horizontalOffsetPercent + shift;
const y = (app.interface.feedback as HTMLCanvasElement).height * (1 - verticalOffsetPercent);
const radius = (app.interface.feedback as HTMLCanvasElement).width * radiusPercent;
ctx.clearRect(x - radius, y - radius, radius * 2, radius * 2);
};
if (script === "local" && no !== undefined) {
const shiftPercent = SHIFTPERCENT;
const shiftAmount = no * (app.interface.feedback as HTMLCanvasElement).width * shiftPercent;
// Clear existing timeout if any
if (app.blinkTimeouts[shiftAmount]) {
clearTimeout(app.blinkTimeouts[shiftAmount]);
}
_drawBlinker(shiftAmount);
// Save timeout ID for later clearing
// @ts-ignore
app.blinkTimeouts[shiftAmount] = setTimeout(() => {
_clearBlinker(shiftAmount);
// Clear the canvas before drawing new blinkers
(app.interface.feedback as HTMLCanvasElement)
.getContext("2d")!
.clearRect(
0,
0,
(app.interface.feedback as HTMLCanvasElement).width,
(app.interface.feedback as HTMLCanvasElement).height,
);
}, blinkDuration);
}
};
export const scriptBlinkers = () => {
/**
* Manages animation updates using requestAnimationFrame.
* @param app - The Editor application context.
*/
let lastFrameTime = Date.now();
const frameRate = 10;
const minFrameDelay = 1000 / frameRate;
const update = () => {
const now = Date.now();
const timeSinceLastFrame = now - lastFrameTime;
if (timeSinceLastFrame >= minFrameDelay) {
lastFrameTime = now;
}
requestAnimationFrame(update);
};
requestAnimationFrame(update);
};

View File

@ -1,562 +0,0 @@
export const drawBackground = (
canvas: HTMLCanvasElement,
color: string | number,
...gb: number[]
): void => {
/**
* Set background color of the canvas.
* @param color - The color to set. String or 3 numbers representing RGB values.
*/
const ctx = canvas.getContext("2d")!;
if (typeof color === "number") color = `rgb(${color},${gb[0]},${gb[1]})`;
ctx.fillStyle = color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
};
export const createLinearGradient = (
canvas: HTMLCanvasElement,
x1: number,
y1: number,
x2: number,
y2: number,
...stops: (number | string)[]
): CanvasGradient => {
const ctx = canvas.getContext("2d")!;
const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
// Parse pairs of values from stops
for (let i = 0; i < stops.length; i += 2) {
let color = stops[i + 1];
if (typeof color === "number")
color = `rgb(${color},${stops[i + 2]},${stops[i + 3]})`;
gradient.addColorStop(stops[i] as number, color as string);
}
return gradient;
};
export const createRadialGradient = (
canvas: HTMLCanvasElement,
x1: number,
y1: number,
r1: number,
x2: number,
y2: number,
r2: number,
...stops: (number | string)[]
) => {
/**
* Set radial gradient on the canvas.
* @param x1 - The x-coordinate of the start circle
* @param y1 - The y-coordinate of the start circle
* @param r1 - The radius of the start circle
* @param x2 - The x-coordinate of the end circle
* @param y2 - The y-coordinate of the end circle
* @param r2 - The radius of the end circle
* @param stops - The stops to set. Pairs of numbers representing the position and color of the stop.
*/
const ctx = canvas.getContext("2d")!;
const gradient = ctx.createRadialGradient(x1, y1, r1, x2, y2, r2);
for (let i = 0; i < stops.length; i += 2) {
let color = stops[i + 1];
if (typeof color === "number")
color = `rgb(${color},${stops[i + 2]},${stops[i + 3]})`;
gradient.addColorStop(stops[i] as number, color as string);
}
return gradient;
};
export const createConicGradient = (
canvas: HTMLCanvasElement,
x: number,
y: number,
angle: number,
...stops: (number | string)[]
) => {
/**
* Set conic gradient on the canvas.
* @param x - The x-coordinate of the center of the gradient
* @param y - The y-coordinate of the center of the gradient
* @param angle - The angle of the gradient, in radians
* @param stops - The stops to set. Pairs of numbers representing the position and color of the stop.
*/
const ctx = canvas.getContext("2d")!;
const gradient = ctx.createConicGradient(x, y, angle);
for (let i = 0; i < stops.length; i += 2) {
let color = stops[i + 1];
if (typeof color === "number")
color = `rgb(${color},${stops[i + 2]},${stops[i + 3]})`;
gradient.addColorStop(stops[i] as number, color as string);
}
return gradient;
};
export const drawGradientImage = (
canvas: HTMLCanvasElement,
time: number = 666
) => {
/* TODO: This works but is really resource heavy. Should do method for requestAnimationFrame? */
const context = canvas.getContext("2d")!;
const { width, height } = context.canvas;
const imageData = context.getImageData(0, 0, width, height);
for (let p = 0; p < imageData.data.length; p += 4) {
const i = p / 4;
const x = i % width;
const y = (i / width) >>> 0;
const red = 64 + (128 * x) / width + 64 * Math.sin(time / 1000);
const green = 64 + (128 * y) / height + 64 * Math.cos(time / 1000);
const blue = 128;
imageData.data[p + 0] = red;
imageData.data[p + 1] = green;
imageData.data[p + 2] = blue;
imageData.data[p + 3] = 255;
}
context.putImageData(imageData, 0, 0);
return true;
};
export const drawBalloid = (
canvas: HTMLCanvasElement,
curves: number,
radius: number,
curve: number,
fillStyle: string,
secondary: string,
x: number,
y: number
): void => {
const ctx = canvas.getContext("2d")!;
// Draw the shape using quadratic Bézier curves
ctx.beginPath();
ctx.fillStyle = fillStyle;
if (curves === 0) {
// Draw a circle if curves = 0
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
} else if (curves === 1) {
// Draw a single curve (ellipse) if curves = 1
ctx.ellipse(x, y, radius * 0.8, radius * curve * 0.7, 0, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
} else if (curves === 2) {
// Draw a shape with two symmetric curves starting from the top and meeting at the bottom
ctx.moveTo(x, y - radius);
// First curve
ctx.quadraticCurveTo(x + radius * curve, y, x, y + radius);
// Second symmetric curve
ctx.quadraticCurveTo(x - radius * curve, y, x, y - radius);
ctx.closePath();
ctx.fill();
} else {
// Draw the curved shape with the specified number of curves
ctx.moveTo(x, y - radius);
let points = [];
for (let i = 0; i < curves; i++) {
const startAngle = (i / curves) * 2 * Math.PI;
const endAngle = startAngle + (2 * Math.PI) / curves;
const controlX =
x + radius * curve * Math.cos(startAngle + Math.PI / curves);
const controlY =
y + radius * curve * Math.sin(startAngle + Math.PI / curves);
points.push([
x + radius * Math.cos(startAngle),
y + radius * Math.sin(startAngle),
]);
ctx.moveTo(
x + radius * Math.cos(startAngle),
y + radius * Math.sin(startAngle)
);
ctx.quadraticCurveTo(
controlX,
controlY,
x + radius * Math.cos(endAngle),
y + radius * Math.sin(endAngle)
);
}
ctx.closePath();
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.fillStyle = secondary;
// Form the shape from points with straight lines and fill it
if (points[0]) {
ctx.moveTo(points[0][0] as number, points[0][1] as number);
for (let point of points) ctx.lineTo(point[0] as number, point[1] as number);
}
// Close and fill
ctx.closePath();
ctx.fill();
}
};
export const drawEquilateral = (
canvas: HTMLCanvasElement,
radius: number,
fillStyle: string,
rotation: number,
x: number,
y: number
): void => {
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
ctx.beginPath();
ctx.moveTo(0, -radius);
ctx.lineTo(radius, radius);
ctx.lineTo(-radius, radius);
ctx.closePath();
ctx.fillStyle = fillStyle;
ctx.fill();
ctx.restore();
};
export const drawTriangular = (
canvas: HTMLCanvasElement,
width: number,
height: number,
fillStyle: string,
rotation: number,
x: number,
y: number
): void => {
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
ctx.beginPath();
ctx.moveTo(0, -height);
ctx.lineTo(width, height);
ctx.lineTo(-width, height);
ctx.closePath();
ctx.fillStyle = fillStyle;
ctx.fill();
ctx.restore();
};
export const drawBall = (
canvas: HTMLCanvasElement,
radius: number,
fillStyle: string,
x: number,
y: number
): void => {
const ctx = canvas.getContext("2d")!;
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fillStyle = fillStyle;
ctx.fill();
ctx.closePath();
};
export const drawDonut = (
canvas: HTMLCanvasElement,
slices: number,
eaten: number,
radius: number,
hole: number,
fillStyle: string,
secondary: string,
stroke: string,
rotation: number,
x: number,
y: number
): void => {
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
if (slices < 2) {
ctx.beginPath();
ctx.arc(0, 0, radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.fillStyle = slices < 1 ? secondary : fillStyle;
ctx.fill();
ctx.beginPath();
ctx.arc(0, 0, hole, 0, 2 * Math.PI);
ctx.closePath();
ctx.fillStyle = secondary;
ctx.fill();
ctx.restore();
}
// Draw slices as arcs
const totalSlices = slices;
const sliceAngle = (2 * Math.PI) / totalSlices;
for (let i = 0; i < totalSlices; i++) {
const startAngle = i * sliceAngle;
const endAngle = (i + 1) * sliceAngle;
// Calculate the position of the outer arc
const outerStartX = hole * Math.cos(startAngle);
const outerStartY = hole * Math.sin(startAngle);
ctx.beginPath();
ctx.moveTo(outerStartX, outerStartY);
ctx.arc(0, 0, radius, startAngle, endAngle);
ctx.arc(0, 0, hole, endAngle, startAngle, true);
ctx.closePath();
// Fill and stroke the slices with the specified fill style
if (i < slices - eaten) {
// Regular slices are white
ctx.fillStyle = fillStyle;
} else {
// Missing slices are black
ctx.fillStyle = secondary;
}
ctx.lineWidth = 2;
ctx.fill();
ctx.strokeStyle = stroke;
ctx.stroke();
}
ctx.restore();
};
export const drawPie = (
canvas: HTMLCanvasElement,
slices: number,
eaten: number,
radius: number,
fillStyle: string,
secondary: string,
stroke: string,
rotation: number,
x: number,
y: number
): void => {
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
if (slices < 2) {
ctx.beginPath();
ctx.arc(0, 0, radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.fillStyle = slices < 1 ? secondary : fillStyle;
ctx.fill();
ctx.restore();
}
// Draw slices as arcs
const totalSlices = slices;
const sliceAngle = (2 * Math.PI) / totalSlices;
for (let i = 0; i < totalSlices; i++) {
const startAngle = i * sliceAngle;
const endAngle = (i + 1) * sliceAngle;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(0, 0, radius, startAngle, endAngle);
ctx.lineTo(0, 0); // Connect to center
ctx.closePath();
// Fill and stroke the slices with the specified fill style
if (i < slices - eaten) {
// Regular slices are white
ctx.fillStyle = fillStyle;
} else {
// Missing slices are black
ctx.fillStyle = secondary;
}
ctx.lineWidth = 2;
ctx.fill();
ctx.strokeStyle = stroke;
ctx.stroke();
}
ctx.restore();
};
export const drawStar = (
canvas: HTMLCanvasElement,
points: number,
radius: number,
fillStyle: string,
rotation: number,
outerRadius: number,
x: number,
y: number
): void => {
if (points < 1) return drawBall(canvas, radius, fillStyle, x, y);
if (points == 1) return drawEquilateral(canvas, radius, fillStyle, 0, x, y);
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
ctx.beginPath();
ctx.moveTo(0, -radius);
for (let i = 0; i < points; i++) {
ctx.rotate(Math.PI / points);
ctx.lineTo(0, -(radius * outerRadius));
ctx.rotate(Math.PI / points);
ctx.lineTo(0, -radius);
}
ctx.closePath();
ctx.fillStyle = fillStyle;
ctx.fill();
ctx.restore();
};
export const drawStroke = (
canvas: HTMLCanvasElement,
width: number,
strokeStyle: string,
rotation: number = 0,
x1: number,
y1: number,
x2: number,
y2: number
): void => {
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x1, y1);
ctx.rotate((rotation * Math.PI) / 180);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(x2 - x1, y2 - y1);
ctx.lineWidth = width;
ctx.strokeStyle = strokeStyle;
ctx.stroke();
ctx.restore();
};
export const drawBox = (
canvas: HTMLCanvasElement,
width: number,
height: number,
fillStyle: string,
rotation: number,
x: number,
y: number
): void => {
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
ctx.fillStyle = fillStyle;
ctx.fillRect(0, 0, width, height);
ctx.restore();
};
export const drawSmiley = (
canvas: HTMLCanvasElement,
happiness: number,
radius: number,
eyeSize: number,
fillStyle: string,
rotation: number,
x: number,
y: number
): void => {
const ctx = canvas.getContext("2d")!;
// Map the rotation value to an angle within the range of -PI to PI
const rotationAngle = (rotation / 100) * Math.PI;
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotationAngle);
// Draw face
ctx.beginPath();
ctx.arc(0, 0, radius, 0, 2 * Math.PI);
ctx.fillStyle = fillStyle;
ctx.fill();
ctx.lineWidth = radius / 20;
ctx.strokeStyle = "black";
ctx.stroke();
// Draw eyes
const eyeY = -radius / 5;
const eyeXOffset = radius / 2.5;
const eyeRadiusX = radius / 8;
const eyeRadiusY = (eyeSize * radius) / 10;
ctx.beginPath();
ctx.ellipse(-eyeXOffset, eyeY, eyeRadiusX, eyeRadiusY, 0, 0, 2 * Math.PI);
ctx.fillStyle = "black";
ctx.fill();
ctx.beginPath();
ctx.ellipse(eyeXOffset, eyeY, eyeRadiusX, eyeRadiusY, 0, 0, 2 * Math.PI);
ctx.fillStyle = "black";
ctx.fill();
// Draw mouth with happiness number -1.0 to 1.0. 0.0 Should be a straight line.
const mouthY = radius / 2;
const mouthLength = radius * 0.9;
const smileFactor = 0.25; // Adjust for the smile curvature
let controlPointX = 0;
let controlPointY = 0;
if (happiness >= 0) {
controlPointY = mouthY + (happiness * smileFactor * radius) / 2;
} else {
controlPointY = mouthY + (happiness * smileFactor * radius) / 2;
}
ctx.beginPath();
ctx.moveTo(-mouthLength / 2, mouthY);
ctx.quadraticCurveTo(controlPointX, controlPointY, mouthLength / 2, mouthY);
ctx.lineWidth = 10;
ctx.strokeStyle = "black";
ctx.stroke();
ctx.restore();
};
export const drawText = (
canvas: HTMLCanvasElement,
text: string,
fontSize: number,
rotation: number,
font: string,
x: number,
y: number,
fillStyle: string,
filter: string
): void => {
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
ctx.filter = filter;
ctx.font = `${fontSize}px ${font}`;
ctx.fillStyle = fillStyle;
ctx.fillText(text, 0, 0);
ctx.restore();
};
export const drawImage = (
canvas: HTMLCanvasElement,
url: string,
width: number,
height: number,
rotation: number,
x: number,
y: number,
filter: string = "none"
): void => {
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
ctx.filter = filter;
const image = new Image();
image.src = url;
ctx.drawImage(image, -width / 2, -height / 2, width, height);
ctx.restore();
};

View File

@ -1,265 +0,0 @@
import { Editor } from "../main";
import { introduction, atelier, software_interface, shortcuts, code, mouse, interaction } from "./basics";
import { amplitude, effects, sampler, synths, filters, audio_basics } from "./learning/audio_engine";
import { lfos, functions, generators, variables, probabilities } from './patterns';
import { ziffers_basics, ziffers_scales, ziffers_rhythm, ziffers_algorithmic, ziffers_tonnetz, ziffers_syncing } from "./patterns/ziffers";
import { loading_samples } from "./learning/samples/loading_samples";
import { sample_banks } from "./learning/samples/sample_banks";
import { sample_list } from "./learning/samples/sample_list";
import { oscilloscope } from "./more/oscilloscope";
import { synchronisation } from "./more/synchronisation";
import { about } from "./more/about";
import { bonus } from "./more/bonus";
import { visualization } from "./more/visualization";
import { chaining } from "./patterns/chaining";
import { time } from "./learning/time/time";
import { linear_time } from "./learning/time/linear_time";
import { cyclical_time } from "./learning/time/cyclical_time";
import { long_forms } from "./learning/time/long_forms";
import { midi } from "./learning/midi";
import { osc } from "./learning/osc";
import { patterns } from "./patterns/patterns";
// Setting up the Markdown converter with syntax highlighting
import showdown from "showdown";
import showdownHighlight from "showdown-highlight";
import "highlight.js/styles/atom-one-dark-reasonable.min.css";
import { createDocumentationStyle } from "../DOM/DomElements";
showdown.setFlavor("github");
type StyleBinding = {
type: string;
regex: RegExp;
replace: (match: string, p1: string) => string;
};
export const key_shortcut = (shortcut: string): string => {
return `<kbd class="lg:px-2 lg:py-1.5 px-1 py-1 lg:text-sm text-xs font-semibold text-brightwhite bg-brightblack border border-black rounded-lg">${shortcut}</kbd>`;
};
export const makeExampleFactory = (application: Editor): Function => {
const make_example = (
description: string,
code: string,
open: boolean = false,
) => {
const codeId = `codeExample${application.exampleCounter++}`;
// Store the code snippet in the data structure
application.api.codeExamples[codeId] = code;
return `
<details ${open ? "open" : ""}>
<summary >${description}
<button class="ml-4 py-1 align-top text-base px-4 hover:bg-brightgreen bg-green inline-block text-selection_foreground" onclick="app.api._playDocExample(app.api.codeExamples['${codeId}'])">▶️ Play</button>
<button class="py-1 text-base px-4 hover:brightyellow bg-yellow text-selection_foreground inline-block" onclick="app.api._stopDocExample()">&#x23f8;&#xFE0F; Pause</button>
<button class="py-1 text-base px-4 hover:bg-brightmagenta bg-magenta text-selection_foreground inline-block" onclick="navigator.clipboard.writeText(app.api.codeExamples['${codeId}'])">📎 Copy</button>
</summary>
<pre><code class="hljs language-javascript">${code.trim()}</code></pre>
</details> `;
};
return make_example;
};
export const documentation_pages = [
"introduction",
"atelier",
"sampler",
"amplitude",
"audio_basics",
"filters",
"effects",
"interface",
"interaction",
"code",
"time",
"linear",
"cyclic",
"longform",
"synths",
"chaining",
"patterns",
"ziffers_basics",
"ziffers_scales",
"ziffers_rhythm",
"ziffers_algorithmic",
"ziffers_tonnetz",
"ziffers_syncing",
"midi",
"osc",
"functions",
"generators",
"lfos",
"probabilities",
"variables",
"synchronisation",
"mouse",
"shortcuts",
"about",
"bonus",
"oscilloscope",
"sample_list",
"loading_samples",
"visualization"
];
export const documentation_factory = (application: Editor) => {
/**
* Creates the documentation for the given application.
* @param application The editor application.
* @returns An object containing various documentation sections.
*/
application.api.codeExamples = {};
return {
introduction: introduction(application),
atelier: atelier(application),
interface: software_interface(application),
interaction: interaction(application),
code: code(application),
time: time(application),
linear: linear_time(application),
cyclic: cyclical_time(application),
longform: long_forms(application),
synths: synths(application),
filters: filters(application),
chaining: chaining(application),
patterns: patterns(application),
ziffers_basics: ziffers_basics(application),
ziffers_scales: ziffers_scales(application),
ziffers_algorithmic: ziffers_algorithmic(application),
ziffers_rhythm: ziffers_rhythm(application),
ziffers_tonnetz: ziffers_tonnetz(application),
ziffers_syncing: ziffers_syncing(application),
midi: midi(application),
osc: osc(application),
lfos: lfos(application),
variables: variables(application),
probabilities: probabilities(application),
functions: functions(application),
generators: generators(application),
shortcuts: shortcuts(application),
amplitude: amplitude(application),
effects: effects(application),
sampler: sampler(application),
mouse: mouse(application),
oscilloscope: oscilloscope(application),
audio_basics: audio_basics(application),
synchronisation: synchronisation(application),
bonus: bonus(application),
visualization: visualization(application),
sample_list: sample_list(application),
sample_banks: sample_banks(application),
loading_samples: loading_samples(application),
about: about(),
};
};
export const showDocumentation = (app: Editor): void => {
const toggleElementVisibility = (elementId: string, shouldHide: boolean): void => {
const element = document.getElementById(elementId);
if (element) {
element.classList.toggle("hidden", shouldHide);
}
};
const applyStyleBindings = (style: Record<string, string>, updateContent: (bindings: StyleBinding[]) => void): void => {
const bindings: StyleBinding[] = Object.keys(style).map((key) => ({
type: "output",
regex: new RegExp(`<${key}([^>]*)>`, "g"),
replace: (_, p1) => `<${key} class="${style[key]}" ${p1}>`
}));
updateContent(bindings);
};
const appHidden = document.getElementById("app")?.classList.contains("hidden");
if (appHidden) {
toggleElementVisibility("app", false);
toggleElementVisibility("documentation", true);
app.exampleIsPlaying = false;
} else {
toggleElementVisibility("app", true);
toggleElementVisibility("documentation", false);
const style = createDocumentationStyle(app);
applyStyleBindings(style, (bindings: StyleBinding[]) => updateDocumentationContent(app, bindings));
}
// Reset the URL to the base URL
window.history.pushState({}, '', '/');
};
// export const showDocumentation = (app: Editor) => {
// /**
// * Shows or hides the documentation based on the current state of the app.
// * @param app - The Editor instance.
// */
// if (document.getElementById("app")?.classList.contains("hidden")) {
// document.getElementById("app")?.classList.remove("hidden");
// document.getElementById("documentation")?.classList.add("hidden");
// app.exampleIsPlaying = false;
// } else {
// document.getElementById("app")?.classList.add("hidden");
// document.getElementById("documentation")?.classList.remove("hidden");
// // Load and convert Markdown content from the documentation file
// let style = createDocumentationStyle(app);
// function update_and_assign(callback: Function) {
// let bindings = Object.keys(style).map((key) => ({
// type: "output",
// regex: new RegExp(`<${key}([^>]*)>`, "g"),
// //@ts-ignore
// replace: (match, p1) => `<${key} class="${style[key]}" ${p1}>`,
// }));
// callback(bindings)
// }
// update_and_assign((e: Object) => updateDocumentationContent(app, e));
// }
// };
export const hideDocumentation = () => {
/**
* Hides the documentation section and shows the main application.
*/
if (document.getElementById("app")?.classList.contains("hidden")) {
document.getElementById("app")?.classList.remove("hidden");
document.getElementById("documentation")?.classList.add("hidden");
}
};
export const updateDocumentationContent = (app: Editor, bindings: any) => {
/**
* Updates the content of the documentation pane with the converted markdown.
*
* @param app - The editor application.
* @param bindings - Additional bindings for the showdown converter.
*/
let loading_message: string = "<h1 class='border-4 py-2 px-2 mx-48 mt-48 text-center text-2xl text-brightwhite'>Loading! <b class='text-red'>Clic to refresh!</b></h1>";
const converter = new showdown.Converter({
emoji: true,
moreStyling: true,
backslashEscapesHTMLTags: true,
extensions: [showdownHighlight({
pre: true,
auto_detection: false
}), ...bindings],
});
if (Object.keys(app.docs).length === 0) {
app.docs = documentation_factory(app);
}
function _update_and_assign(callback: Function) {
const converted_markdown = converter.makeHtml(
app.docs[app.currentDocumentationPane]!,
);
callback(converted_markdown)
}
_update_and_assign((e: string) => {
let display_content = e === undefined ? loading_message : e;
document.getElementById("documentation-content")!.innerHTML = display_content;
})
if (document.getElementById("documentation-content")!.innerHTML.replace(/"/g, "'") == loading_message.replace(/"/g, "'")) {
setTimeout(() => {
updateDocumentationContent(app, bindings);
}, 100);
}
}

Binary file not shown.

View File

@ -1,193 +0,0 @@
import { type Editor } from "../../main";
import { makeExampleFactory } from "../Documentation";
export const atelier = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Atelier (06 mars 2024)
Bonjour tout le monde ! Nous sommes :
- [Rémi Georges](https://remigeorges.fr) : musicien, réalisateur en informatique musicale.
- [Agathe Herrou](https://www.youtube.com/@th4music) : musicienne, chercheuse.
- [Raphaël Forment](https://raphaelforment.fr) : musicien, doctorant.
Nous pratiquons le [live coding](https://livecoding.fr). Nous utilisons notre ordinateur comme un instrument de musique, nous programmons de la musique devant notre public. Nous pouvons faire plein de choses comme :
- créer des instruments de musique, des synthétiseurs, des boîtes à rythme.
- jouer des échantillons, charger des images, des vidéos, créer des animations.
- contrôler d'autres instruments, jouer avec d'autres musiciens.
Topos est un instrument de musique. On peut l'utiliser depuis n'importe quel ordinateur, sans avoir à installer quoi que ce soit. Nous l'avons fabriqué pour que tout le monde puisse jouer facilement de la musique.
## Découverte
<br>
${makeExample(
"Percussions", `
tempo(120) // Changer le tempo
beat(1)::sound('kick').out()
beat(2)::sound('snare').out()
beat(.5)::sound('hh').out()
`, true,)}
<br>
- Qu'est-ce qu'il se passe si je change un nombre ?
- Qu'est-ce qu'il se passe si je change un nom ?
- Essayez par exemple <ic>"sid"</ic> ou <ic>"trump"</ic>.
- Qu'est-ce qu'il se passe si j'enlève <ic>.out()</ic> ?
- Est-il possible de jouer un rythme très rapide ou très lent ?
### Ajout d'une basse
<br>
${makeExample(
"Une basse", `
// Aucun changement dans le code
beat(1)::sound('kick').out()
beat(2)::sound('snare').out()
beat(.5)::sound('hh').out()
// Une nouvelle partie
beat([0.25,0.5].beat(1))::sound("pluck")
.note([40,45].beat(2)).out()
`, true,)}
<br>
- Qu'est-ce que le son <ic>"pluck"</ic> ?
- Que signifie <ic>.note([40,45].beat(2))</ic> ?
- Que se passe-t-il si je change la valeur dans <ic>.beat(2)</ic> ?
- Que se passe-t-il lorsque j'ajoute de nouveaux nombres dans <ic>[40, 45]</ic> ?
### Ajout d'une mélodie
<br>
${makeExample(
"Le morceau complet", `
// Aucun changement dans le code
beat(1)::sound('kick').out()
beat(2)::sound('snare').out()
beat(.5)::sound('hh').out()
beat([0.25,0.5].beat(1))::sound("pluck")
.note([40,45].beat(2)).out()
// Nouvelle partie mélodique
beat([0.25,0.5].beat())::sound("pluck")
.note([0,7,5,8,2,9,0].scale("Major",60).beat(1))
.vib(8).vibmod(1/4)
.delay(0.5).room(1.5).size(0.5)
.out()
`, true,)}
<br>
Ici, on ajoute une nouvelle mélodie mais il s'agit aussi d'un nouvel instrument. C'est pour cela que le code est plus long. Quand on fait du <em>live coding</em>, on code tout en même temps : notes, rythmes, mélodies, sons. C'est beaucoup de choses ! C'est pour cela que le code est court, on essaie de tout taper très vite en jouant !
- Que signifie selon vous <ic>vib</ic>, <ic>delay</ic>, <ic>room</ic> ou <ic>size</ic> ?
- Que se passe-t-il si je change les valeurs dans <ic>vib</ic>, <ic>delay</ic>, <ic>room</ic> ou <ic>size</ic> ?
<br>
**Exercices :**
- Transformer <ic>vib(8)</ic> en <ic>vib([2,4,8].beat(1))</ic>.
- Transformer <ic>"pluck"</ic> en <ic>["pluck", "clap"].beat(1)</ic>.
Vous pouvez aussi utiliser la fonction <ic>rhythm</ic> pour jouer rapidement des rythmes.
${makeExample(
"Rythmes rythmes rythmes", `
rhythm(0.5, 3, 8)::sound('bd').out()
rhythm(0.5, 3, 8)::sound('clap').out()
rhythm(0.5, 6, 8)::sound('hat').out()
rhythm(0.25, 6, 8)::sound('hat')
.vel(0.3).speed(2).out()
rhythm(0.5, 2, 8)::sound('sd').out()
`, true)};
## Créer un instrument
<br>
Nous allons créer un nouvel instrument à partir d'un son de base. Voici un premier son :
${makeExample("Notre son de base", `beat(2)::sound('sine').note(50).ad(0, .5).out()`, true)}
<br>
Ce son est assez ennuyeux. Nous allons ajouter quelques paramètres :
${makeExample("Beaucoup mieux !", `beat(2)::sound('sine').note(50).fmi(2).fmh(2).ad(0, .5).out()`, true)}
<br>
Nous allons aussi ajouter quelques effets intéressants :
${makeExample("Ajout d'un écho", `beat(2)::sound('sine').note(50)
.fmi(2).fmh(2).ad(1/16, 1.5)
.delay(0.5).delayt(0.75).out()`,
true)}
<br>
Nous pouvons utiliser plusieurs techniques pour rendre le son plus dynamique :
- générer des valeurs aléatoires pour les paramètres
- utiliser des générateurs de valeurs (comme <ic>usine</ic>)
- utiliser la souris ou un autre contrôleur pour changer les valeurs en temps réel
${makeExample("Plus dynamique encore", `
beat(2)::sound('sine').note([50,55,57,62,66, 69, 74].mouseX())
.fmi(usine(1/4)).fmh([1,2,0.5].beat())
.ad(1/16, 1.5).delay(0.5).delayt(0.75)
.out()`, true)}
<br>
Un exemple final, le plus complexe jusqu'à présent :
${makeExample("Un instrument de musique complet", `
beat(2)::sound('triangle')
.note([50,55,57,62,66, 69, 74].mouseX())
.fmi(usine(1/4)).fmh([1,2,0.5].beat())
.ad(1/16, 1.5).delay(0.5).delayt(0.75)
.room(0.5).size(8).lpf(usine(1/3)*4000).out()`, true)}
## Compléments
${makeExample("Quelques échantillons", `
ab ade ades2 ades3 ades4 alex alphabet amencutup armora arp arpy auto
baa baa2 bass bass0 bass1 bass2 bass3 bassdm bassfoo battles bd bend
bev bin birds birds3 bleep blip blue bottle breaks125 breaks152
breaks157 breaks165 breath bubble can casio cb cc chin circus clak
click clubkick co coins control cosmicg cp cr crow d db diphone
diphone2 dist dork2 dorkbot dr dr2 dr55 dr_few drum drumtraks e east
electro1 em2 erk f feel feelfx fest fire flick fm foo future gab
gabba gabbaloud gabbalouder glasstap glitch glitch2 gretsch gtr h
hand hardcore hardkick haw hc hh hh27 hit hmm ho hoover house ht if
ifdrums incoming industrial insect invaders jazz jungbass jungle juno
jvbass kicklinn koy kurt latibro led less lighter linnhats lt made
made2 mash mash2 metal miniyeah monsterb moog mouth mp3 msg mt mute
newnotes noise noise2 notes numbers oc off outdoor pad padlong pebbles
perc peri pluck popkick print proc procshort psr rave rave2 ravemono
realclaps reverbkick rm rs sax sd seawolf sequential sf sheffield
short sid sine sitar sn space speakspell speech speechless speedupdown
stab stomp subroc3d sugar sundance tabla tabla2 tablex tacscan tech
techno tink tok toys trump ul ulgab uxay v voodoo wind wobble world
xmas yeah`, true)}
`
};

View File

@ -1,7 +0,0 @@
export { introduction } from './welcome';
export { atelier } from './atelier';
export { software_interface } from './interface';
export { shortcuts } from './keyboard';
export { code } from './code';
export { mouse } from './mouse';
export { interaction } from './interaction';

View File

@ -1,145 +0,0 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../Documentation";
export const filters = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Filters
Filters can be applied to both synthesizers and samples. They are used to shape the sound by removing or emphasizing certain frequencies. They are also used to create movement in the sound by modulating the cutoff frequency of the filter over time.
- **lowpass filter**: filters the high frequencies, keeping the low frequencies.
- **highpass filter**: filtering the low frequencies, keeping the high frequencies.
- **bandpass filter**: filters the low and high frequencies around a frequency band, keeping what's in the middle.
${makeExample(
"Filtering the high frequencies of an oscillator",
`beat(.5) :: sound('sawtooth').cutoff(50 + usine(1/8) * 2000).out()`,
true,
)}
These filters all come with their own set of parameters. Note that we are describing the parameters of the three different filter types here. Choose the right parameters depending on the filter type you are using:
### Lowpass filter
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>cutoff</ic> | <ic>lpf</ic> | cutoff frequency of the lowpass filter |
| <ic>resonance</ic> | <ic>lpq</ic> | resonance of the lowpass filter (0-1) |
${makeExample(
"Filtering a bass",
`beat(.5) :: sound('jvbass').lpf([250,1000,8000].beat()).out()`,
true,
)}
### Highpass filter
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>hcutoff</ic> | <ic>hpf</ic> | cutoff frequency of the highpass filter |
| <ic>hresonance</ic> | <ic>hpq</ic> | resonance of the highpass filter (0-1) |
${makeExample(
"Filtering a noise source",
`beat(.5) :: sound('gtr').hpf([250,1000, 2000, 3000, 4000].beat()).end(0.5).out()`,
true,
)}
### Bandpass filter
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>bandf</ic> | <ic>bpf</ic> | cutoff frequency of the bandpass filter |
| <ic>bandq</ic> | <ic>bpq</ic> | resonance of the bandpass filter (0-1) |
${makeExample(
"Sweeping the filter on the same guitar sample",
`beat(.5) :: sound('gtr').bandf(100 + usine(1/8) * 4000).end(0.5).out()`,
true,
)}
Alternatively, <ic>lpf</ic>, <ic>hpf</ic> and <ic>bpf</ic> can take a second argument, the **resonance**.
## Filter order (type)
You can also use the <ic>ftype</ic> method to change the filter type (order). There are two types by default, <ic>12db</ic> for a gentle slope or <ic>24db</ic> for a really steep filtering slope. The <ic>24db</ic> type is particularly useful for substractive synthesis if you are trying to emulate some of the Moog or Prophet sounds:
- <ic>ftype(type: string)</ic>: sets the filter type (order), either <ic>12db</ic> or <ic>24db</ic>.
${makeExample(
"Filtering a bass",
`beat(.5) :: sound('jvbass').ftype(['12db', '24db'].beat(4)).lpf([250,1000,8000].beat()).out()`,
true,
)}
## Filter envelopes
The examples we have studied so far are static. They filter the sound around a fixed cutoff frequency. To make the sound more interesting, you can use the ADSR filter envelopes to shape the filter cutoff frequency over time. You will always find amplitude and filter envelopes on most commercial synthesizers. This is done using the following methods:
### Lowpass envelope
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>lpenv</ic> | <ic>lpe</ic> | lowpass frequency modulation amount (negative or positive) |
| <ic>lpattack</ic> | <ic>lpa</ic> | attack of the lowpass filter |
| <ic>lpdecay</ic> | <ic>lpd</ic> | decay of the lowpass filter |
| <ic>lpsustain</ic> | <ic>lps</ic> | sustain of the lowpass filter |
| <ic>lprelease</ic> | <ic>lpr</ic> | release of the lowpass filter |
| <ic>lpadsr</ic> | | (**takes five arguments**) set all the parameters |
${makeExample(
"Filtering a sawtooth wave dynamically",
`beat(.5) :: sound('sawtooth').note([48,60].beat())
.cutoff(5000).lpa([0.05, 0.25, 0.5].beat(2))
.lpenv(-8).lpq(10).out()`,
true,
)}
### Highpass envelope
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>hpenv</ic> | <ic>hpe</ic> | highpass frequency modulation amount (negative or positive) |
| <ic>hpattack</ic> | <ic>hpa</ic> | attack of the highpass filter |
| <ic>hpdecay</ic> | <ic>hpd</ic> | decay of the highpass filter |
| <ic>hpsustain</ic> | <ic>hps</ic> | sustain of the highpass filter |
| <ic>hprelease</ic> | <ic>hpr</ic> | release of the highpass filter |
| <ic>hpadsr</ic> | | (**takes five arguments**) set all the parameters |
${makeExample(
"Let's use another filter using the same example",
`beat(.5) :: sound('sawtooth').note([48,60].beat())
.hcutoff(1000).hpa([0.05, 0.25, 0.5].beat(2))
.hpenv(8).hpq(10).out()`,
true,
)}
### Bandpass envelope
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>bpenv</ic> | <ic>bpe</ic> | bandpass frequency modulation amount (negative or positive) |
| <ic>bpattack</ic> | <ic>bpa</ic> | attack of the bandpass filter |
| <ic>bpdecay</ic> | <ic>bpd</ic> | decay of the bandpass filter |
| <ic>bpsustain</ic> | <ic>bps</ic> | sustain of the bandpass filter |
| <ic>bprelease</ic> | <ic>bpr</ic> | release of the bandpass filter |
| <ic>bpadsr</ic> | | (**takes five arguments**) set all the parameters |
${makeExample(
"And the bandpass filter, just for fun",
`beat(.5) :: sound('sawtooth').note([48,60].beat())
.bandf([500,1000,2000].beat(2))
.bpa([0.25, 0.125, 0.5].beat(2) * 4)
.bpenv(-4).release(2).out()
`,
true,
)}
`;
};

View File

@ -1,6 +0,0 @@
export { amplitude } from './amplitude';
export { effects } from './effects';
export { sampler } from './sampler';
export { synths } from './synths';
export { filters } from './filters';
export { audio_basics } from './audio_basics';

View File

@ -1,75 +0,0 @@
import { type Editor } from "../../main";
import { makeExampleFactory } from "../Documentation";
export const osc = (application: Editor): string => {
// @ts-ignore
const makeExample = makeExampleFactory(application);
return `
# Open Sound Control
Topos is a sandboxed web application. It cannot speak with your computer directly or only through a secure connexion. You can use the [Open Sound Control](https://en.wikipedia.org/wiki/Open_Sound_Control) protocol to send and receive data from your computer. This protocol is used by many softwares and hardware devices. You can use it to control your favorite DAW, your favorite synthesizer, your favorite robot, or anything really! To use **OSC** with Topos, you will need to download the <ic>ToposServer</ic> by [following this link](https://github.com/Bubobubobubobubo/Topos). You can download everything as a zip file or clone the project if you know what you are doing. Here is a quick guide to get you started:
- 1) Download <ic>Topos</ic> and navigate to the <ic>ToposServer</ic> folder.
- 2) Install the dependencies using <ic>npm install</ic>. Start the server using <ic>npm start</ic>.
- 3) Open the <ic>Topos</ic> application in your web browser (server first, then application).
The <ic>ToposServer</ic> server is used both for **OSC** _input_ and _output_.
## Input
Send an **OSC** message to the server from another application or device at the address <ic>localhost:30000</ic>. Topos will store the last 1000 messages in a queue. You can access this queue using the <ic>getOsc()</ic> function.
### Unfiltered messages
You can access the last 1000 messages using the <ic>getOsc()</ic> function without any argument. This is raw data, you will need to parse it yourself:
${makeExample(
"Reading the last OSC messages",
`
beat(1)::getOsc()
// 0 : {data: Array(2), address: '/lala'}
// 1 : {data: Array(2), address: '/lala'}
// 2 : {data: Array(2), address: '/lala'}`,
true,
)}
### Filtered messages
The <ic>getOsc()</ic> can receive an address filter as an argument. This will return only the messages that match the filter:
${makeExample(
"Reading the last OSC messages (filtered)",
`
beat(1)::getOsc("/lala")
// 0 : (2) [89, 'bob']
// 1 : (2) [84, 'bob']
// 2 : (2) [82, 'bob']
`,
true,
)}
## Output
Once the server is loaded, you are ready to send an **OSC** message:
${makeExample(
"Sending a simple OSC message",
`
beat(1)::sound('cp').speed(2).vel(0.5).osc()
`,
true,
)}
This is a simple **OSC** message that will inherit all the properties of the sound. You can also send customized OSC messages using the <ic>osc()</ic> function:
${makeExample(
"Sending a customized OSC message",
`
// osc(address, port, ...message)
osc('/my/osc/address', 5000, 1, 2, 3)
`,
true,
)}
`;
};

View File

@ -1,3 +0,0 @@
export { loading_samples } from './loading_samples';
export { sample_banks } from './sample_banks';
export { sample_list } from './sample_list';

View File

@ -1,106 +0,0 @@
import { type Editor } from "../../main";
import { key_shortcut, makeExampleFactory } from "../Documentation";
export const bonus = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Bonus features
Some features have been included as a bonus. These features are often about patterning over things that are not directly related to sound: pictures, video, etc.
## Editor theme configuration
The editor theme can be changed using the <ic>theme</ic> and <ic>randomTheme</ic> functions. The following example will use a random color scheme for every beat:
${makeExample(
"Random theme on each beat",
`
beat(1)::randomTheme()
`, true)}
You can also pick a theme using the <ic>theme</ic> function with a string as only argument:
${makeExample(
"Picking a theme",
`
beat(1)::theme("Batman")
`, true)}
## Hydra Visual Live Coding
<div class="mx-12 bg-neutral-600 rounded-lg flex flex-col items-center justify-center">
<warning>⚠️ This feature can generate flashing images that could trigger photosensitivity or epileptic seizures. ⚠️ </warning>
</div>
[Hydra](https://hydra.ojack.xyz/?sketch_id=mahalia_1) is a popular live-codable video synthesizer developed by [Olivia Jack](https://ojack.xyz/) and other contributors. It follows an analog synthesizer patching metaphor to encourage live coding complex shaders. Being very easy to use, extremely powerful and also very rewarding to use, Hydra has become a popular choice for adding visuals into a live code performance.
${makeExample(
"Hydra integration",
`beat(4) :: hydra.osc(3, 0.5, 2).out()`,
true,
)}
Close the documentation to see the effect: ${key_shortcut(
"Ctrl+D",
)}! **Boom, all shiny!**
Be careful not to call <ic>hydra</ic> too often as it can impact performances. You can use any rhythmical function like <ic>beat()</ic> function to limit the number of function calls. You can write any Topos code like <ic>[1,2,3].beat()</ic> to bring some life and movement in your Hydra sketches.
Stopping **Hydra** is simple:
${makeExample(
"Stopping Hydra",
`
beat(4) :: stop_hydra() // this one
beat(4) :: hydra.hush() // or this one
`,
true,
)}
### Changing the resolution
You can change Hydra resolution using this simple method:
${makeExample(
"Changing Hydra resolution",
`hydra.setResolution(1024, 768)`,
true,
)}
### Documentation
I won't teach Hydra. You can find some great resources directly on the [Hydra website](https://hydra.ojack.xyz/):
- [Hydra interactive documentation](https://hydra.ojack.xyz/docs/)
- [List of Hydra Functions](https://hydra.ojack.xyz/api/)
- [Source code on GitHub](https://github.com/hydra-synth/hydra)
### The Hydra namespace
In comparison with the basic Hydra editor, please note that you have to prefix all Hydra functions with <ic>hydra.</ic> to avoid conflicts with Topos functions. For example, <ic>osc()</ic> becomes <ic>hydra.osc()</ic>.
${makeExample("Hydra namespace", `hydra.voronoi(20).out()`, true)}
## GIF player
Topos embeds a small <ic>.gif</ic> picture player with a small API. GIFs are automatically fading out after the given duration. Look at the following example:
${makeExample(
"Playing many gifs",
`
beat(0.25)::gif({
url:v('gif')[$(1)%6], // Any URL will do!
opacity: r(0.5, 1), // Opacity (0-1)
size:"300px", // CSS size property
center:false, // Centering on the screen?
filter:'none', // CSS Filter
dur: 2, // In beats (Topos unit)
rotation: ir(1, 360), // Rotation (in degrees)
posX: ir(1,1200), // CSS Horizontal Position
posY: ir(1, 800), // CSS Vertical Position
`,
true,
)}
`;
};

View File

@ -1,5 +0,0 @@
export { about } from './about';
export { bonus } from './bonus';
export { oscilloscope } from './oscilloscope';
export { synchronisation } from './synchronisation';
export { visualization } from './visualization.ts';

View File

@ -1,229 +0,0 @@
import { type Editor } from "../../main";
import { key_shortcut, makeExampleFactory } from "../Documentation";
export const visualization = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Vizualisation
While Topos is mainly being developed as a live coding environment for algorithmic music composition, it also includes some features for live code visualizations. This section will introduce you to these features.
## Hydra Visual Live Coding
<div class="mx-12 bg-neutral-600 rounded-lg flex flex-col items-center justify-center">
<warning>⚠️ This feature can generate flashing images that could trigger photosensitivity or epileptic seizures. ⚠️ </warning>
</div>
[Hydra](https://hydra.ojack.xyz/?sketch_id=mahalia_1) is a popular live-codable video synthesizer developed by [Olivia Jack](https://ojack.xyz/) and other contributors. It follows an analog synthesizer patching metaphor to encourage live coding complex shaders. Being very easy to use, extremely powerful and also very rewarding to use, Hydra has become a popular choice for adding visuals into a live code performance.
${makeExample(
"Hydra integration",
`
loadHydra() // Load Hydra first!
beat(4) :: hydra.osc(3, 0.5, 2).out()
`,
true,
)}
Close the documentation to see the effect: ${key_shortcut(
"Ctrl+D",
)}! **Boom, all shiny!**
Be careful not to call <ic>hydra</ic> too often as it can impact performances. You can use any rhythmical function like <ic>beat()</ic> function to limit the number of function calls. You can write any Topos code like <ic>[1,2,3].beat()</ic> to bring some life and movement in your Hydra sketches.
Stopping **Hydra** is simple:
${makeExample(
"Stopping Hydra",
`
beat(4) :: hydra.hush() // or this one
`,
false,
)}
### Changing the resolution
You can change Hydra resolution using this simple method:
${makeExample(
"Changing Hydra resolution",
`hydra.setResolution(1024, 768)`,
false,
)}
### Hydra documentation
I won't teach Hydra. You can find some great resources directly on the [Hydra website](https://hydra.ojack.xyz/):
- [Hydra interactive documentation](https://hydra.ojack.xyz/docs/)
- [List of Hydra Functions](https://hydra.ojack.xyz/api/)
- [Source code on GitHub](https://github.com/hydra-synth/hydra)
### The Hydra namespace
In comparison with the basic Hydra editor, please note that you have to prefix all Hydra functions with <ic>hydra.</ic> to avoid conflicts with Topos functions. For example, <ic>osc()</ic> becomes <ic>hydra.osc()</ic>.
${makeExample("Hydra namespace", `hydra.voronoi(20).out()`, true)}
## GIF player
Topos embeds a small <ic>.gif</ic> picture player with a small API. GIFs are automatically fading out after the given duration. Look at the following example:
${makeExample(
"Playing many gifs",
`
beat(0.25)::gif({
url:v('gif')[$(1)%6], // Any URL will do!
opacity: r(0.5, 1), // Opacity (0-1)
size:"300px", // CSS size property
center:false, // Centering on the screen?
filter:'none', // CSS Filter
dur: 2, // In beats (Topos unit)
rotation: ir(1, 360), // Rotation (in degrees)
posX: ir(1,1200), // CSS Horizontal Position
posY: ir(1, 800), // CSS Vertical Position
`,
false,
)}
## Canvas live coding
Documentation in progress! Copy the example and run it separately (Showing visualization examples in the documentation not implemented yet).
Canvas live coding is a feature that allows you to draw musical events to the canvas. Canvas can be used to create complex visualizations. The feature is based on the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API" target="_blank">Canvas API</a> and the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D" target="_blank">CanvasRenderingContext2D</a> interface. The feature is still in development and more functions will be added in the future.
In addition to the standard Canvas API, Topos also includes some pre-defined shapes for convenience. See the Shapes section below for more info.
* <ic>draw(f: Function)</ic> - Draws to a canvas with the given function.
${makeExample(
"Drawing to canvas",
`
beat(0.5) && clear() && draw(context => {
context.fillStyle = 'red';
// Begin the path for the heart shape
context.beginPath();
const x = wc();
const y = hc();
context.fillStyle = 'red';
// Begin the path for the heart shape
context.beginPath();
context.moveTo(x + 125, y + 50);
context.bezierCurveTo(x + 75, y, x, y + 75, x + 125, y + 175);
context.bezierCurveTo(x + 250, y + 75, x + 175, y, x + 125, y + 50);
// Fill the heart with red color
context.fill();
})
`,
false,
)}
${makeExample(
"Using draw with events and shapes",
`
beat(0.25) && sound("bass1:5").pitch(rI(1,6)).draw(x => {
donut(x.pitch)
}).out()
`,
false,
)}
${makeExample(
"Using draw with ziffers and shapes",
`
z1("1/8 (0 2 1 4)+(2 1)").sound("sine").ad(0.05,.25).clear()
.draw(x => {
pie({slices:7,eaten:(7-x.pitch-1),fillStyle:"green", rotate: 250})
}).log("pitch").out()
`,
false,
)}
* <ic<image(url, x, y, width, height, rotation)</ic> - Draws an image to a canvas.
${makeExample(
"Image to canvas",
`
beat(0.5) && clear() && image("http://localhost:8000/topos_frog.svg",200,200+epulse()%15)
`,
false,
)}
* <ic>clear()</ic> - Clears the canvas.
* <ic>background(fill: string)</ic> - Sets the background color, image or gradient.
* <ic>w()</ic> - Returns the canvas width.
* <ic>h()</ic> - Returns the canvas height.
* <ic>wc()</ic> - Returns the center of the canvas width.
* <ic>hc()</ic> - Returns the center of the canvas height.
### Text to canvas
Text can be drawn to canvas using the <ic>drawText()</ic> function. The function can take any unicode characters including emojis. The function can also be used to draw random characters from a given unicode range. Different filters can also be applied using the **filter** parameter. See filter in <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter" target="_blank">canvas documentation</a> for more info.
* <ic>drawText(text, fontSize, rotation, font, x, y)</ic> - Draws text to a canvas.
${makeExample(
"Writing to canvas",
`
beat(0.5) && clear() && drawText("Hello world!", 100, 0, "Arial", 100, 100)
`,
false,
)}
* <ic>randomChar(number, min, max)</ic> - Returns a number of random characters from given unicode range.
${makeExample(
"Drawing random characters to canvas",
`
beat(0.5) && clear() && drawText(randomChar(10,1000,2000),30)
`,
false,
)}
* <ic>emoji(size)</ic> - Returns a random emojis as text.
* <ic>animals(size)</ic> - Returns a random animal emojis as text.
* <ic>food(size)</ic> - Returns a random food emojis as text.
${makeExample(
"Drawing food emojis to canvas",
`
beat(0.5) && clear() && drawText({x: 10, y: epulse()%700, text: food(50)})
`,
false,
)}
* <ic>expression(size)</ic> - Returns a random expression emojis as text.
### Shapes
In addition to supporting drawing to canvas directly, Topos also include some pre-defined shapes for convenience. Every shape can be defined by either by inputting one object as parameter or by inputting the parameters separately.
The predefined shapes are:
* <ic>smiley(happiness, radius, eyes, fill, rotate, x, y)</ic>
* <ic>ball(radius,fill,x,y)</ic>
* <ic>box(width, height, fill, rotate)</ic>
* <ic>pointy(width, height, fill, rotate, x, y)</ic>
* <ic>equilateral(radius, fill, rotate, x, y)</ic>
* <ic>star(points, radius, fill rotate, outerRadius, x, y</ic>
* <ic>pie(slices, eaten, radius, fill, secondary, stroke, rotate, x, y</ic>
* <ic>donut(slices, eaten, radius, hole, fill, secondary, stroke, rotate, x, y</ic>
* <ic>balloid(petals, radius, curve, fill, secondary, x, y)</ic>
* <ic>stroke(width, stroke, rotate, x1, y1, x2, y2)</ic>
### Gradients
* <ic>linearGradient(x1, y1, x2, y2, ...stops)</ic> - Creates a <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createLinearGradient">linear gradient</a>.
* <ic>radialGradient(x1, y1, r1, x2, y2, r2, ...stops)</ic> - Creates a <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createRadialGradient">radial gradient</a>.
* <ic>conicGradient(x, y, angle, ...stops)</ic> - Creates a <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createConicGradient">conic gradient</a>.
`;
};

View File

@ -1,173 +0,0 @@
import { type Editor } from "../../main";
import { makeExampleFactory } from "../Documentation";
export const generators = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Generator functions
JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator" target="_blank">generators</a> are powerful functions for generating value sequences. They can be used to generate melodies, rhythms or control parameters.
In Topos generator functions should be called using the <ic>cache(key, function)</ic> function to store the current state of the generator. This function takes two arguments: the name for the cache and the generator instance.
Once the generator is cached the values will be returned from the named cache even if the generator function is modified. To clear the current cache and to re-evaluate the modified generator use the **Shift+Ctrl+Backspace** shortcut. Alternatively you can cache the modified generator using a different name.
The resulted values can be played using either <ic>pitch()</ic> or <ic>freq()</ic> or as Ziffers patterns. When playing the values using <ic>pitch()</ic> different scales and chained methods can be used to alter the result, for example <ic>mod(value: number)</ic> to limit the integer range or <ic>scale(name: string)</ic> etc. to change the resulting note.
${makeExample(
"Simple looping generator function",
`
function* simple() {
let x = 0;
while (x < 12) {
yield x+x;
x+=1;
}
}
beat(.25) && sound("triangle").pitch(cache("simple",simple())).scale("minor").out()
`,
true,
)};
${makeExample(
"Infinite frequency generator",
`
function* poly(x=0) {
while (true) {
const s = Math.tan(x/10)+Math.sin(x/20);
yield 2 * Math.pow(s, 3) - 6 * Math.pow(s, 2) + 5 * s + 200;
x++;
}
}
beat(.125) && sound("triangle").freq(cache("mathyshit",poly())).out()
`,
true,
)};
When you want to dance with a dynamical system in controlled musical chaos, Topos is waiting for you:
${makeExample(
"Truly scale free chaos inspired by Lorentz attractor",
`
function* strange(x = 0.1, y = 0, z = 0, rho = 28, beta = 8 / 3, zeta = 10) {
while (true) {
const dx = 10 * (y - x);
const dy = x * (rho - z) - y;
const dz = x * y - beta * z;
x += dx * 0.01;
y += dy * 0.01;
z += dz * 0.01;
const value = 300 + 30 * (Math.sin(x) + Math.tan(y) + Math.cos(z))
yield value;
}
}
beat(0.25) :: sound("triangle")
.freq(cache("stranger",strange(3,5,2)))
.adsr(.15,.1,.1,.1)
.log("freq").out()
`,
true,
)};
${makeExample(
"Henon and his discrete music",
`
function* henonMap(x = 0, y = 0, a = 1.4, b = 0.3) {
while (true) {
const newX = 1 - a * x ** 2 + y;
const newY = b * x;
const fusionPoint = newX + newY
yield fusionPoint * 300;
[x, y] = [newX, newY]
}
}
beat(0.25) :: sound("sawtooth")
.semitones(1,1,2,2,2,1,2,1)
.freq(cache("Hénon Synth", henonMap()))
.adsr(0, 0.1, 0.1, 0.5).out()
z0('1 {-2}').octave(-2).sound('bd').out()
z1('e. 1 s 3!2 e 3!2 s 9 8 1')
.sound('dr').gain(0.3).octave(-5).out()
`,
true,
)};
${makeExample(
"1970s fractal dream",
`
function* rossler(x = 0.1, y = 0.1, z = 0.1, a = 0.2, b = 0.2, c = 5.7) {
while (true) {
const dx = - y - z;
const dy = x + (a * y);
const dz = b + (x * z) - (c * z);
x += dx * 0.01;
y += dy * 0.01;
z += dz * 0.01;
const value = 250 * (Math.cosh(x*z) + Math.sinh(y*z))
yield value % 120 + 100;
}
}
beat(0.25) :: sound("triangle")
.freq(cache("Rössler attractor", rossler(3,4,1)))
.adsr(0,.1,.1,.1)
.log("freq").out()
`,
true,
)};
## OEIS integer sequences
To find some inspiration - or to enter into the void - one can visit <a href="https://oeis.org/" target="_blank">The On-Line Encyclopedia of Integer Sequences (OEIS)</a> to find some interesting integer sequences.
Many of the sequences are implemented by <a href="https://github.com/acerix/jisg/tree/main/src/oeis" target="_blank">JISG</a> (Javascript Integer Sequence Generators) project. Those sequences can be referenced directly with the identifiers using the cache function.
One of these implemented generators is the Inventory sequence <a href="https://github.com/acerix/jisg/blob/main/src/oeis/A342585.ts" target="_blank">A342585</a> made famous by <a href="https://www.youtube.com/watch?v=rBU9E-ZOZAI" target="_blank">Neil Sloane</a>.
${makeExample(
"Inventory sequence",
`
rhythm(0.5,[8,7,5,6].bar(4),9) :: sound("triangle")
.pitch(cache("Inventory",A342585))
.mod(11).scale("minor")
.adsr(.25,.05,.5,.5)
.room(2.0).size(0.5)
.gain(1).out()
`,
true,
)};
## Using generators with Ziffers
Alternatively generators can be used with Ziffers to generate longer patterns. In this case the generator function should be passed as an argument to the ziffers function. Ziffers patterns are cached separately so there is no need for using **cache()** function. Ziffers expects values from the generators to be integers or strings in ziffers syntax.
${makeExample(
"Ziffers patterns using a generator functions",
`
function* poly(x) {
while (true) {
yield 64 * Math.pow(x, 6) - 480 * Math.pow(x, 4) + 720 * Math.pow(x, 2);
x++;
}
}
z0(poly(1)).noteLength(0.5).semitones(2,2,3,2,2,2).sound("sine").out()
z1(poly(8)).noteLength(0.25).semitones(2,1,2,1,2,2).sound("sine").out()
z2(poly(-3)).noteLength(1.0).semitones(2,2,2,1,3,2).sound("sine").out()
`,
true,
)};
`
};

View File

@ -1,7 +0,0 @@
export { chaining } from './chaining';
export { functions } from './functions';
export { generators } from './generators';
export { lfos } from './lfos';
export { patterns } from './patterns';
export { probabilities } from './probabilities';
export { variables } from './variables';

View File

@ -1,59 +0,0 @@
import { type Editor } from "../../main";
import { makeExampleFactory } from "../Documentation";
export const lfos = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Low Frequency Oscillators
Low Frequency Oscillators (_LFOs_) are an important piece in any digital audio workstation or synthesizer. Topos implements some basic waveforms you can play with to automatically modulate your paremeters.
- <ic>sine(freq: number = 1, phase: number = 0): number</ic>: returns a sinusoïdal oscillation between <ic>-1</ic> and <ic>1</ic>.
- <ic>freq</ic> : frequency in hertz.
- <ic>phase</ic> : phase amount (adds or substract from current time point).
- <ic>usine(freq: number = 1, phase: number = 0): number</ic>: returns a sinusoïdal oscillation between <ic>0</ic> and <ic>1</ic>. The <ic>u</ic> stands for _unipolar_.
${makeExample(
"Modulating the speed of a sample player using a sine LFO",
`beat(.25) && snd('cp').speed(1 + usine(0.25) * 2).out()`,
true,
)};
- <ic>triangle(freq: number = 1, phase: number = 0): number</ic>: returns a triangle oscillation between <ic>-1</ic> and <ic>1</ic>.
- <ic>utriangle(freq: number = 1, phase: number = 0): number</ic>: returns a triangle oscillation between <ic>0</ic> and <ic>1</ic>. The <ic>u</ic> stands for _unipolar_.
${makeExample(
"Modulating the speed of a sample player using a triangle LFO",
`beat(.25) && snd('cp').speed(1 + utriangle(0.25) * 2).out()`,
true,
)}
- <ic>saw(freq: number = 1, phase: number = 0): number</ic>: returns a sawtooth-like oscillation between <ic>-1</ic> and <ic>1</ic>.
- <ic>usaw(freq: number = 1, phase: number = 0): number</ic>: returns a sawtooth-like oscillation between <ic>0</ic> and <ic>1</ic>. The <ic>u</ic> stands for _unipolar_.
${makeExample(
"Modulating the speed of a sample player using a saw LFO",
`beat(.25) && snd('cp').speed(1 + usaw(0.25) * 2).out()`,
true,
)}
- <ic>square(freq: number = 1, duty: number = .5): number</ic>: returns a square wave oscillation between <ic>-1</ic> and <ic>1</ic>. You can also control the duty cycle using the <ic>duty</ic> parameter.
- <ic>usquare(freq: number = 1, duty: number = .5): number</ic>: returns a square wave oscillation between <ic>0</ic> and <ic>1</ic>. The <ic>u</ic> stands for _unipolar_. You can also control the duty cycle using the <ic>duty</ic> parameter.
${makeExample(
"Modulating the speed of a sample player using a square LFO",
`beat(.25) && snd('cp').speed(1 + usquare(0.25, 0, 0.25) * 2).out()`,
true,
)};
- <ic>noise(times: number = 1)</ic>: returns a random value between -1 and 1.
- <ic>unoise(times: number = 1)</ic>: returns a random value between 0 and 1.
${makeExample(
"Modulating the speed of a sample player using noise",
`beat(.25) && snd('cp').speed(1 + noise() * 2).out()`,
true,
)};
`;
};

View File

@ -1,6 +0,0 @@
export { ziffers_basics } from './ziffers_basics';
export { ziffers_scales } from './ziffers_scales';
export { ziffers_rhythm } from './ziffers_rhythm';
export { ziffers_algorithmic } from './ziffers_algorithmic';
export { ziffers_tonnetz } from './ziffers_tonnetz';
export { ziffers_syncing } from './ziffers_syncing';

View File

@ -1,97 +0,0 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../Documentation";
export const ziffers_algorithmic = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Algorithmic operations
Ziffers provides shorthands for **many** numeric and algorithimic operations such as evaluating random numbers and creating sequences using list operations:
* **List operations:** Element-wise operation (_e.g._ <ic>(3 2 1)+(2 5)</ic>) using the <ic>+</ic> operator. All the arithmetic operators are supported.
${makeExample(
"Element-wise operations for melodic generation",
`
z1("1/8 _ 0 (0 1 3)+(1 2) 0 (2 3 5)-(1 2)").sound('sine')
.scale('pentatonic').fmi([0.25,0.5].beat(2)).fmh([2,4].beat(2))
.room(0.9).size(0.9).sustain(0.1).delay(0.125)
.delayfb(0.25).out();
`,
true,
)}
${makeExample(
"List operations",
`
z1('q (0 3 1 5)+(2 5) e (0 5 2)*(2 1) (0 5 2)%(2 3)')
.sound('sine')
.room(.5).size(1.0).adsr(.15,.15,.25,.1)
.scale("Bebop major")
.out()
`,
true,
)}
* **Random numbers:** <ic>(4,6)</ic> Random number between 4 and 6
${makeExample(
"Random numbers, true computer music at last!",
`
z1("s _ (0,8) 0 0 (0,5) 0 0").sound('sine')
.adsr(0, .1, 0, 0).scale('minor')
.fmdec(0.25).fmi(2).fmh([0.5, 0.25].beat(2))
.room(0.9).size(0.5).sustain(0.1) .delay(0.5)
.delay(0.125).delayfb(0.25).out();
beat(.5) :: snd(['kick', 'hat'].beat(.5)).out()
`,
true,
)}
## Variables
* <ic>A=(0 2 3 (1,4))</ic> Assign pre-evaluated list to a variable
* <ic>B~(0 2 3 (1,4))</ic> Assign list with operations to a variable
${makeExample(
"Assign lists to variables",
`
z1("s A=(0 (1,4)) B~(2 (3,8)) A B A B A")
.scale("modimic")
.sound("triangle")
.adsr(0.01,0.15,0.25,0)
.gain(0.5)
.out()
`,
true,
)}
${makeExample(
"Combine variables into lists and do operations",
`
z1("s A=(0 3) B=(3 8) C=(((A+B)+A)*B) D=(C-B) A A+C D")
.sound("sawtooth")
.ad(0.05,1.0)
.gain(0.5)
.out()
`,
true,
)}
## Generative functions
* <ic>at(index: number, ...args?: number[])</ic> Get event(s) at given index
* <ic>repeat(amount: number)</ic> Repeat the generated pattern without re-evaluating random patterns
* <ic>keep()</ic> Keep the generated pattern without re-evaluating random patterns. Same as repeat(0).
* <ic>shuffle()</ic> Shuffle the pattern
* <ic>deal(amount: number): Shuffle the generated pattern and deal given number of elements
* <ic>retrograde()</ic> Reverse the generated pattern
* <ic>invert()</ic> Invert the generated pattern
* <ic>rotate(amount: number)</ic> Rotate the generated pattern by given amount
* <ic>between(start: number, end: number)</ic> Select a range of elements from the generated pattern
* <ic>from(start: number)</ic> Select a range of elements from the start index to the end of the pattern
* <ic>to(end: number)</ic> Select a range of elements from the beginning of the pattern to the end index
* <ic>every(amount: number)</ic> Select every n-th element from the pattern
`;
};

View File

@ -1,300 +0,0 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../Documentation";
export const ziffers_basics = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Ziffers
Ziffers is a **musical number based notation** tuned for _live coding_. It is a very powerful and flexible notation for describing musical patterns in very few characters. Number based musical notation has a long history and has been used for centuries as a shorthand technique for music notation. Amiika has written [papers](https://zenodo.org/record/7841945) and other documents describing his system. It is currently implemented for many live coding platforms including [Sardine](https://sardine.raphaelforment.fr) (Raphaël Forment) and [Sonic Pi](https://sonic-pi.net/) (Sam Aaron). Ziffers can be used for:
- composing melodies using using **classical music notation and concepts**.
- exploring **generative / aleatoric / stochastic** melodies and applying them to sounds and synths.
- embracing a different mindset and approach to time and **patterning**.
${makeExample(
"Super Fancy Ziffers example",
`
z1('1/8 024!3 035 024 0124').sound('wt_stereo')
.adsr(0, .4, 0.5, .4).gain(0.1)
.lpadsr(4, 0, .2, 0, 0)
.cutoff(5000 + usine(1/2) * 2000)
.n([1,2,4].beat(4)).out()
z2('<1/8 1/16> __ 0 <(^) (^ ^)> (0,8)').sound('wt_stereo')
.adsr(0, .5, 0.5, .4).gain(0.2)
.lpadsr(4, 0, .2, 0, 0).n(14)
.cutoff(200 + usine(1/2) * 4000)
.n([1,2,4].beat(4)).o(2).room(0.9).out()
let osci = 1500 + usine(1/2) * 2000;
z3('can can:2').sound().gain(1).cutoff(osci).out()
z4('1/4 kick kick snare kick').sound().gain(1).cutoff(osci).out()
`,
true,
)}
## Evaluation
Evaluation of live coded Ziffers patterns can be done in 3 different ways. Normal evaluation using <ic>Ctrl+Enter</ic> updates the pattern after the current cycle is finished. Evaluation using <ic>Ctrl+Shift+Enter</ic> updates the pattern immediately keeping the current position, which enables to modify future events even within the current cycle. Evaluation using <ic>Ctrl+Shift+Backspace</ic> resets the current pattern and starts from the beginning immediately.
## Notation
The basic Ziffer notation is entirely written in JavaScript strings (_e.g_ <ic>"0 1 2"</ic>). It consists mostly of numbers and letters. The whitespace character is used as a separator. Instead of note names, Ziffer is using numbers to represent musical pitch and letters to represent musical durations. Alternatively, _floating point numbers_ can also be used to represent durations.
| Syntax | Symbol | Description |
|------------ |--------|------------------------|
| **Pitches** | <ic>0-9</ic> <ic>{10 11 21}</ic> | Numbers or escaped numbers in curly brackets |
| **Duration** | <ic>0.25</ic>, <ic>0.5</ic> | Floating point numbers can also be used as durations |
| **Duration** | <ic>1/4</ic>, <ic>1/16</ic> | Fractions can be used as durations |
| **Subdivision** | <ic>[1 [2 3]]</ic> | Durations can be subdivided using square brackets |
| **Cycles** | <ic>1 <2 4></ic> | Cycle values within the pattern |
| **Octave** | <ic>^ _</ic> | <ic>^</ic> for octave up and <ic>_</ic> for octave down |
| **Accidentals** | <ic># b</ic> | Sharp and flats, just like with regular music notation :smile: |
| **Rest** | <ic>r</ic> | Rest / silences |
| **Repeat** | <ic>!1-9</ic> | Repeat the item 1 to 9 times |
| **Chords** | <ic>[1-9]+ / [iv]+ / [AG]+name</ic> | Multiple pitches grouped together, roman numerals or named chords |
| **Samples** | <ic>[a-z0-9_]+</ic> | Samples can be used pitched or unpitched |
| **Index/Channel** | <ic>[a-z0-9]+:[0-9]*</ic> | Samples or midi channel can be changed using a colon |
**Note:** Some features are experimental and some are still unsupported. For full / prior syntax see article about <a href="https://zenodo.org/record/7841945" target="_blank">Ziffers</a>.
${makeExample(
"Pitches from 0 to 9",
`
z1('0.25 0 1 2 3 4 5 6 7 8 9').sound('wt_stereo')
.adsr(0, .1, 0, 0).out()`,
true,
)}
${makeExample(
"Escaped pitches using curly brackets",
`z1('_ _ 0 {9 10 11} 4 {12 13 14}')
.sound('wt_05').pan(r(0,1))
.cutoff(usaw(1/2) * 4000)
.room(0.9).size(0.9).out()`,
false,
)}
${makeExample(
"Durations using fractions and floating point numbers",
`
z1('1/8 0 2 4 0 2 4 1/4 0 3 5 0.25 _ 0 7 0 7')
.sound('square').delay(0.5).delayt(1/8)
.adsr(0, .1, 0, 0).delayfb(0.45).out()
`,
false,
)}
${makeExample(
"Disco was invented thanks to Ziffers",
`
z1('e _ _ 0 ^ 0 _ 0 ^ 0').sound('jvbass').out()
beat(1)::snd('bd').out(); beat(2)::snd('sd').out()
beat(3) :: snd('cp').room(0.5).size(0.5).orbit(2).out()
`,
false,
)}
${makeExample(
"Accidentals and rests for nice melodies",
`
z1('^ 1/8 0 1 b2 3 4 _ 4 b5 4 3 b2 1 0')
.scale('major').sound('triangle')
.cutoff(500).lpadsr(5, 0, 1/12, 0, 0)
.fmi(0.5).fmh(2).delay(0.5).delayt(1/3)
.adsr(0, .1, 0, 0).out()
`,
false,
)}
${makeExample(
"Repeat items n-times",
`
z1('1/8 _ _ 0!4 3!4 4!4 3!4')
.scale('major').sound('wt_oboe')
.shape(0.2).sustain(0.1).out()
z2('1/8 _ 0!4 5!4 4!2 7!2')
.scale('major').sound('wt_oboe')
.shape(0.2).sustain(0.1).out()
`,
false,
)}
${makeExample(
"Subdivided durations",
`
z1('w [0 [5 [3 7]]] h [0 4]')
.scale('major').sound('sine')
.fmi(usine(.5)).fmh(2).out()
`,
false,
)}
## Rests
${makeExample(
"Rest and octaves",
`
z1('q 0 ^ e0 r _ 0 _ r 4 ^4 4')
.sound('sine').scale("godian").out()
`,
true,
)}
${makeExample(
"Rests with durations",
`
z1('q 0 4 e^r 3 e3 0.5^r h4 1/4^r e 5 r 0.125^r 0')
.sound('sine').scale("aeryptian").out()
`,
true,
)}
## Chords
Chords can be build by grouping pitches or using roman numeral notation, or by using named chords.
${makeExample(
"Chords from pitches",
`
z1('1.0 024 045 058 046 014')
.sound('sine').adsr(0.5, 1, 0, 0)
.room(0.5).size(0.9)
.scale("minor").out()
`,
true
)}
${makeExample(
"Chords from roman numerals",
`
z1('2/4 i vi ii v')
.sound('triangle').adsr(0.2, 0.3, 0, 0)
.room(0.5).size(0.9).scale("major").out()
`,
true
)}
${makeExample(
"Named chords with repeats",
`
z1('0.25 Bmaj7!2 D7!2 _ Gmaj7!2 Bb7!2 ^ Ebmaj7!2')
.sound('square').room(0.5).cutoff(500)
.lpadsr(4, 0, .4, 0, 0).size(0.9)
.scale("major").out()
`,
true
)}
${makeExample(
"Transposing chords",
`
z1('q Amin!2').key(["A2", "E2"].beat(4))
.sound('sawtooth').cutoff(500)
.lpadsr(2, 0, .5, 0, 0, 0).out()`,
)}
${makeExample(
"Chord inversions with roman numerals",
`
z1('i i v%-4 v%-2 vi%-5 vi%-3 iv%-2 iv%-1')
.sound('triangle').adsr(1/16, 1/5, 0.1, 0)
.delay(0.5).delayt([1/8, 1/4].beat(4))
.delayfb(0.5).out()
beat(4) :: sound('breaks165').stretch(4).out()
`,
)}
${makeExample(
"Chord inversion with named chords",
`
z1('1/4 Cmin!3 Fmin!3 Fmin%-1 Fmin%-2 Fmin%-1')
.sound("sine").bpf(500 + usine(1/4) * 2000)
.out()
`,
)}
${makeExample(
"Programmatic inversions",
`
z1('1/6 i v 1/3 vi iv').invert([1,-1,-2,0].beat(4))
.sound("sawtooth").cutoff(1000)
.lpadsr(2, 0, .2, 0, 0).out()
`,
)}
## Arpeggios
Chords can be arpeggiated using the @-character within the ziffers notation or by using <ic>arpeggio</ic> method.
${makeExample(
"Arpeggio using the mini-notation",
`
z1("(i v vi%-3 iv%-2)@(s 0 2 0 1 2 1 0 2)")
.sound("sine").out()
`,
)}
${makeExample(
"Arpeggio from named chords with durations",
`
z1("_ Gm7 ^ C9 D7 Gm7")
.arpeggio("e 0 2 q 3 e 1 2")
.sound("sine").out()
`,
)}
${makeExample(
"Arpeggio from roman chords with inversions",
`
z1("i v%-1 vi%-1 iv%-2")
.arpeggio(0,2,1,2)
.noteLength(0.125)
.sound("sine").out()
`,
)}
## Chaining
- Basic notation
${makeExample(
"Simple method chaining",
`
z1('0 1 2 3').key('G3')
.scale('minor').sound('sine').out()
`,
true,
)}
${makeExample(
"More complex chaining",
`
z1('0 1 2 3 4').key('G3').scale('minor').sound('sine').often(n => n.pitch+=3).rarely(s => s.delay(0.5)).out()
`,
true,
)}
${makeExample(
"Alternative way for inputting options",
`
z1('0 3 2 4',{key: 'D3', scale: 'minor pentatonic'}).sound('sine').out()
`,
true,
)}
## String prototypes
You can also use string prototypes as an alternative syntax for creating Ziffers patterns
${makeExample(
"String prototypes",
`
"q 0 e 5 2 6 2 q 3".z0().sound('sine').out()
"q 2 7 8 6".z1().octave(-1).sound('sine').out()
"q 2 7 8 6".z2({key: "C2", scale: "aeolian"}).sound('sine').scale("minor").out()
`,
true,
)}
`;
};

View File

@ -1,157 +0,0 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../Documentation";
export const ziffers_rhythm = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Rhythm
Ziffers combines rhythmic and melodic notation into a single pattern language. This means that you can use the same pattern to describe both the rhythm and the melody of a musical phrase similarly to the way it is done in traditional music notation.
${makeExample(
"Duration chars",
`
z1('q 0 0 4 4 5 5 h4 q 3 3 2 2 1 1 h0').sound('sine').out()
`,
true,
)}
${makeExample(
"Fraction durations",
`
z1('1/4 0 0 4 4 5 5 2/4 4 1/4 3 3 2 2 1 1 2/4 0')
.sound('sine').out()
`,
true,
)}
${makeExample(
"Decimal durations",
`
z1('0.25 5 1 2 6 0.125 3 8 0.5 4 1.0 0')
.sound('sine').scale("galian").out()
`,
true,
)}
## List of all duration characters
Ziffers maps the following duration characters to the corresponding note lengths.
| Character | Fraction | Duration | Name (US) | Name (UK) |
| ----- | ----- | ------- | ----- |
| m.. | 14/1 | 14.0 | Double dotted maxima | Double dotted Large
| m. | 12/1 | 12.0 | Dotted maxima | Dotted Large |
| m | 8/1 | 8.0 | Maxima | Large |
| l.. | 7/1 | 7.0 | Double dotted long note | Double dotted longa |
| l. | 6/1 | 6.0 | Long dotted note | Longa dotted |
| l | 4/1 | 4.0 | Long | Longa |
| d.. | 7/2 | 3.5 | Double dotted long note | Double dotted breve |
| d. | 3/3 | 3.0 | Dotted whole note | Double breve |
| n | 8/3 | 2.6666 | Triplet Long | Triplet longa |
| d | 2/1 | 2.0 | Double whole note | Breve |
| w.. | 7/4 | 1.75 | Double dotted whole note | Double dotted breve |
| w. | 3/2 | 1.5 | Dotted whole note | Dotted breve |
| k | 4/3 | 1.3333 | Triplet double whole | Triplet breve |
| w | 1/1 | 1.0 | Whole note | Semibreve |
| h.. | 7/8 | 0.875 | Double dotted half note | Double dotted minim |
| h. | 3/4 | 0.75 | Dotted half note | Dotted minim |
| c | 2/3 | 0.6666 | Triplet whole | Triplet semibreve |
| h | 1/2 | 0.5 | Half note  | Minim |
| p | 1/3 | 0.3333 | Triplet half | Triplet minim |
| q.. | 7/16 | 0.4375 | Double dotted quarter note | Double dotted crotchet |
| q. | 3/8 | 0.375 | Dotted quarter note | Dotted crotchet |
| q | 1/4 | 0.25 | Quarter note | Crotchet |
| e.. | 7/32 | 0.2187 | Double dotted 8th | Double dotted quaver |
| e. | 3/16 | 0.1875 | Dotted 8th | Dotted quaver |
| g | 1/6 | 0.1666 | Triplet quarter | Triplet crochet  |
| e | 1/8 | 0.125 | 8th note | Quaver |
| s.. | 7/64 | 0.1093 | Double dotted 16th | Double dotted semiquaver |
| a | 1/12 | 0.0833 | Triplet 8th | Triplet quaver |
| s. | 3/32 | 0.0937 | Dotted 16th | Dotted semiquaver |
| s | 1/16 | 0.0625 | 16th note | Semiquaver |
| t.. | 7/128 | 0.0546 | Double dotted 32th | Double dotted demisemiquaver |
| t. | 3/64 | 0.0468 | Dotted 32th | Dotted demisemiquaver |
| f | 1/24 | 0.0416 | Triplet 16th | Triplet semiquaver |
| t | 1/32 | 0.0312 | 32th note | Demisemiquaver |
| u.. | 7/256 | 0.0273 | Double dotted 64th | Double dotted hemidemisemiquaver |
| u. | 3/128 | 0.0234 | Dotted 64th | Dotted hemidemisemiquaver |
| x | 1/48 | 0.0208 | Triplet 32th | Triplet demi-semiquaver |
| u | 1/64 | 0.0156 | 64th note | Hemidemisemiquaver |
| o.. | 7/512 | 0.0136 | Double dotted 128th note | Double dotted semihemidemisemiquaver |
| y | 1/96 | 0.0104 | Triplet 64th | Triplet hemidemisemiquaver |
| o. | 3/256 | 0.0117 | Dotted 128th note | Dotted semihemidemisemiquaver |
| o | 1/128 | 0.0078 | 128th note | Semihemidemisemiquaver |
| j | 1/192 | 0.0052 | Triplet 128th | Triplet semihemidemisemiquaver |
| z | 0/1 | 0.0 | No length | No length |
## Samples
Samples can be patterned using the sample names or using <c>@</c>-operator for assigning sample to a pitch. Sample index can be changed using the <c>:</c> operator.
${makeExample(
"Sampled drums",
`
z1('bd [hh hh]').octave(-2).sound('sine').out()
`,
true,
)}
${makeExample(
"More complex pattern",
`
z1('bd [hh <hh <cp cp:2>>]').octave(-2).sound('sine').out()
`,
true,
)}
${makeExample(
"Pitched samples",
`
z1('0@sax 3@sax 2@sax 6@sax')
.octave(-1).sound()
.adsr(0.25,0.125,0.125,0.25).out()
`,
true,
)}
${makeExample(
"Pitched samples from list operation",
`
z1('e (0 3 -1 4)+(-1 0 2 1)@sine')
.key('G4')
.scale('110 220 320 450')
.sound().out()
`,
true,
)}
${makeExample(
"Pitched samples with list notation",
`
z1('e (0 2 6 3 5 -2)@sax (0 2 6 3 5 -2)@arp')
.octave(-1).sound()
.adsr(0.25,0.125,0.125,0.25).out()
`,
true,
)}
${makeExample(
"Sample indices",
`
z1('e 1:2 4:3 6:2')
.octave(-1).sound("east").out()
`,
true,
)}
${makeExample(
"Pitched samples with sample indices",
`
z1('_e 1@east:2 4@bd:3 6@arp:2 9@baa').sound().out()
`,
true,
)}
`;
};

View File

@ -1,101 +0,0 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../Documentation";
export const ziffers_scales = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Scales
Ziffers supports all the keys and scales. Keys can be defined by using [scientific pitch notation](https://en.wikipedia.org/wiki/Scientific_pitch_notation), for example <ic>F3</ic>. Western style (1490 scales) can be with scale names named after greek modes and extended by <a href="https://allthescales.org/intro.php" target="_blank">William Zeitler</a>. You will never really run out of scales to play with using Ziffers. Here is a short list of some possible scales that you can play with:
| Scale name | Intervals |
|------------|------------------------|
| Lydian | <ic>2221221</ic> |
| Mixolydian | <ic>2212212</ic> |
| Aeolian | <ic>2122122</ic> |
| Locrian | <ic>1221222</ic> |
| Ionian | <ic>2212221</ic> |
| Dorian | <ic>2122212</ic> |
| Phrygian | <ic>1222122</ic> |
| Soryllic | <ic>11122122</ic>|
| Modimic | <ic>412122</ic> |
| Ionalian | <ic>1312122</ic> |
| ... | And it goes on for <a href="https://ianring.com/musictheory/scales/traditions/zeitler" target="_blank">**1490** scales (See full list here)</a>. |
${makeExample(
"What the hell is the Modimic scale?",
`
z1("s (0,8) 0 0 (0,5) 0 0").sound('sine')
.scale('modimic').fmi(2).fmh(2).room(0.5)
.size(0.5).sustain(0.1) .delay(0.5)
.delay(0.125).delayfb(0.25).out();
beat(.5) :: snd(['kick', 'hat'].beat(.5)).out()
`,
true,
)}
You can also use more traditional <a href="https://ianring.com/musictheory/scales/traditions/western" target="_blank">western names</a>:
| Scale name | Intervals |
|------------|------------------------|
| Major | <ic>2212221</ic> |
| Minor | <ic>2122122</ic> |
| Minor pentatonic | <ic>32232</ic> |
| Harmonic minor | <ic>2122131</ic>|
| Harmonic major | <ic>2212131</ic>|
| Melodic minor | <ic>2122221</ic>|
| Melodic major | <ic>2212122</ic>|
| Whole | <ic>222222</ic> |
| Blues minor | <ic>321132</ic> |
| Blues major | <ic>211323</ic> |
${makeExample(
"Let's fall back to a classic blues minor scale",
`
z1("s (0,8) 0 0 (0,5) 0 0").sound('sine')
.scale('blues minor').fmi(2).fmh(2).room(0.5)
.size(0.5).sustain(0.25).delay(0.25)
.delay(0.25).delayfb(0.5).out();
beat(1, 1.75) :: snd(['kick', 'hat'].beat(1)).out()
`,
true,
)}
Microtonal scales can be defined using <a href="https://www.huygens-fokker.org/scala/scl_format.html" target="_blank">Scala format</a> or by extended notation defined by Sevish <a href="https://sevish.com/scaleworkshop/" target="_blank">Scale workshop</a>, for example:
- **Young:** 106. 198. 306.2 400.1 502. 604. 697.9 806.1 898.1 1004.1 1102. 1200.
- **Wendy carlos:** 17/16 9/8 6/5 5/4 4/3 11/8 3/2 13/8 5/3 7/4 15/8 2/1
${makeExample(
"Wendy Carlos, here we go!",
`
z1("s ^ (0,8) 0 0 _ (0,5) 0 0").sound('sine')
.scale('17/16 9/8 6/5 5/4 4/3 11/8 3/2 13/8 5/3 7/4 15/8 2/1').fmi(2).fmh(2).room(0.5)
.size(0.5).sustain(0.15).delay(0.1)
.delay(0.25).delayfb(0.5).out();
beat(1, 1.75) :: snd(['kick', 'hat'].beat(1)).out()
`,
true,
)}
${makeExample(
"Werckmeister scale in Scala format",
`
const werckmeister = "107.82 203.91 311.72 401.955 503.91 605.865 701.955 809.775 900. 1007.82 1103.91 1200."
z0('s (0,3) ^ 0 3 ^ 0 (3,6) 0 _ (3,5) 0 _ 3 ^ 0 (3,5) ^ 0 6 0 _ 3 0')
.key('C3')
.scale(werckmeister)
.sound('sine')
.fmi(1 + usine(0.5) * irand(1,10))
.cutoff(100 + usine(.5) * 100)
.out()
onbeat(1,1.5,3) :: sound('bd').cutoff(100 + usine(.25) * 1000).out()
`,
true,
)}
`;
};

View File

@ -1,112 +0,0 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../Documentation";
export const ziffers_syncing = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Synchronization
Ziffers patterns can be synced to any event by using **cue**, **sync**, **wait** and **listen** methods.
## Sync with cue
The <ic>cue(name: string)</ic> methods can be used to send cue messages for ziffers patterns. The <ic>wait(name: string)</ic> method is used to wait for the cue message to be received before starting the next cycle.
${makeExample(
"Sending cue from event and wait",
`
beat(4.0) :: sound("bd").cue("foo").out()
z1("e 0 3 2 1 2 1").wait("foo").sound("sine").out()
`,
true,
)}
The <ic>sync(name: string)</ic> method is used to sync the ziffers pattern to the cue message.
${makeExample(
"Delayed start using individual cue",
`
register('christmas', n=>n.room(0.25).size(2).speed([0.5, 0.25, 0.125])
.delay(0.5).delayt(1/3).delayfb(0.5).bpf(200+usine(1/3)*500).out())
onbar(1) :: cue("bar")
onbar(2) :: cue('baz')
z1("<0.25 0.125> 0 4 2 -2").sync("bar").sound("ST40:25").christmas()
z2("<0.25 0.125> 0 6 4 -4").sync("baz").sound("ST40:25").christmas()
`,
true,
)}
The <ic>listen(name: string)</ic> method can be used to listen for the cue messages and play one event from the pattern for every cue.
${makeExample(
"Delayed start using individual cue",
`
beat(1.0) :: cue("boom")
z1("bd <hh ho>").listen("boom")
.sound().out()
`,
true,
)}
## Sync with beat
Patterns can also be synced using beat and setting the note length of events to zero using **z** duration character or <ic>noteLength(number)</ic> method.
${makeExample(
"Syncing with beat",
`
beat(.5) :: z1("<bd sn:3> hh:5").noteLength(0)
.sound().out()
beat([2.0,0.5,1.5].bar(1)) ::
z2("z _ 0 0 <2 1>").sound("bass:5")
.dur(0.5).out()
`,
true,
)}
## Automatic sync for ziffers patterns
Numbered methods **(z0-z16)** are synced automatically to **z0** method if it exsists. Syncing can also be done manually by using either the <ic>wait</ic> method, which will always wait for the current pattern to finish before starting the next cycle, or the <ic>sync</ic> method will only wait for the synced pattern to finish on the first time.
${makeExample(
"Automatic sync to z0",
`
z0('w 0 8').sound('peri').out()
z1('e 0 4 5 9').sound('bell').out()
`,
true,
)}
## Syncing patterns to each other
Patterns can also be synced together using the <ic>sync(name: Function)</ic> method. This will sync the pattern to the start of the referenced pattern. Copy this example and first run z1 and then z2 at random position.
${makeExample(
"Sync on first run",
`
z1('w __ 0 5 9 3').sound('bin').out()
z2('q __ 4 2 e 6 3 q 6').sync(z1).sound('east').out()
`,
true,
)}
## Sync with wait
Syncing can also be done using <ic>wait(name: Function)</ic> method. This will wait for the referenced pattern to finish before starting the next cycle.
${makeExample(
"Sync with wait",
`
z1('w 0 5').sound('pluck').release(0.1).sustain(0.25).out()
z2('q 6 3').wait(z1).sound('sine').release(0.16).sustain(0.55).out()
`,
true,
)}
`;
};

View File

@ -1,403 +0,0 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../Documentation";
export const ziffers_tonnetz = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Tonnetz
The Riemannian Tonnetz is a geometric representation of pitches where we apply mathematical operations to analyze harmonic and melodic relationships in tonal music. Ziffers includes an implementation of live coding Tonnetz developed together with <a href="https://github.com/edelveart/TypeScriptTonnetz" target="_blank">Edgar Delgado Vega</a>. Nevertheless, our implementation allows you to play in different chord complexes and **combine 67 transformations** with **new exploratory notation**. You have at your disposal the sets: traditional PLR, film music, extended PLR* and functions for seventh chords PLRQ, PLRQ*, ST.
Tonnetz can be visualized as an <a href="https://numeric-tonnetz-ziffers-6f7c9299bb4e1292f6891b9aceba16d81409236.gitlab.io/" target="_blank">numeric lattice</a> that represents the twelve pitch classes of the chromatic scale. The numeric visualization is a fork of <a href="https://hal.science/hal-03250334/" target="_blank">Web tonnetz</a> by Corentin Guichaou et al. (2021). The lattice can be arranged into multiple pitch spaces which are all supported in Ziffers implementation.
In addition, we have included common graphs and cycles in Neo-Riemmanian theory: HexaCycles, OctaCycles, Enneacycles, Weitzmann Regions, Boretz Regions, OctaTowers, Cube Dance and Power Towers. You can explore each of these graphs in great generality over different Tonnetz.
## Explorative notation
Ziffers implements explorative live coding notation that indexes all of the transformations for triad and seventh chords. For more detailed transformations see Triad and Tetra chapters. Explorative transformations also include cardinal direction transformations (North, South, East, West) as visualized by the <a href="https://numeric-tonnetz-ziffers-6f7c9299bb4e1292f6891b9aceba16d81409236.gitlab.io/" target="_blank">numerical Tonnetz</a> and correspond to different Neo-Riemannian operations depending on the chord type (Major or Minor).
Transformations are applied by grouping operations into a **parameter string** which applies the **transformations** to the chord. The parameter string is a **sequence** of transformations **separated by whitespace**, for example <ic>plr rl2 p3lr</ic>. The numbers after the characters defines the **index for the operation**, as there can be multiple operations of the same type.
Indexed transformations <ic>[plrfsntqNSEW][1-9]*</ic>:
* p: Parallel
* l: Leading-tone exchange
* r: Relative
* f: Film transformation - Far-fifth
* n: Film transformation - Near-fifth (Nebenverdwandt) or PLRQ* transformation
* s: Film transformation - Slide
* h: Film transformation - Hexatonic Pole
* t: Film transformation - Tritone transposition
* q: PLR* transformation or PLRQ* transformation
* N: North transformation
* S: South transformation
* E: East transformation
* W: West transformation
### Examples:
${makeExample(
"Explorative transformations with roman chords",
`
z1('i i7').tonnetz("p1 p2 plr2")
.sound('wt_stereo')
.adsr(0, .1, 0, 0)
.out()`,
true,
)}
${makeExample(
"Arpeggiated explorative transformations",
`
z1("i7")
.tonnetz("p l2 r3 rp4l")
.arpeggio("e _ 0 1 s ^ 0 2 1 3 h _ 012 s ^ 2 1")
.sound("sine")
.out()`,
true,
)}
${makeExample(
"Arpeggios and note lengths with parameters",
`
z1("024")
.tonnetz("p lr rp lrp")
.arpeggio(0,2,1,2)
.noteLength(1/16,1/8)
.sound("sine")
.out()`,
true,
)}
${makeExample(
"Explorative transformations with cardinal directions",
`
z1("1/4 i")
.tonnetz("p Np N2p N3p plr N3plr E EE EEE E6 NSE3W2")
.sound("sine")
.out()
`
)}
## Triad transformations
Triad transformations can be defined explicitly using the <ic>triadTonnetz(transformation: string, tonnetz: number[])</ic> method. This method will only apply specific transformations to triad chords.
In the table below, we write the transformations types available for triads followed by the **transposition in semitones (+/-)** that we must perform to the **root of the chord**: <ic>0,4,3,7,5,1,8,6</ic>.
Second, you should know that the numbers next to the names of the transformations represent the **chord types to be exchanged**: <ic>1 = major, 2 = minor, 3 = diminished, 4 = augmented</ic>.
In fact, the functions <ic>p,l,r</ic> and the so-called [**film music transformations**](https://alpof.wordpress.com/2021/10/09/neo-riemannian-examples-in-music/), could have the numbers <ic>p12, l12, r12, f12 </ic> and so on, since they transform major and minor chords with their respective root transpositions. It must be clarified that the <ic>t6</ic> function is the only one among all that maintains the same type of chord.
Therefore, you will see that paying attention to the examples will allow you to infer whether you should **raise or lower the root** depending on the **type of chord**.
| Function | Function type | Root transposition | Example |
| :------: | :-------------------------------: | :----------------: | :-----------: |
| p | Parallel | 0 | C <-> Cm |
| l | Leading-tone | 4 | C <-> Em |
| r | Relative | 3 | C <-> Am |
| f | Film music: Far-fifth | 7 | C <-> Gm |
| n | Film music: Near-fifth | 5 | C <-> Fm |
| s | Film music: Slide | 1 | C <-> C#m |
| h | Film music: Hexatonic pole | 8 | C <-> G#m |
| t6 | Film music: Tritone Transposition | 6 | C <-> F# |
| p32 | Parallel | 0 | Cdim <-> Cm |
| p41 | Parallel | 0 | Caug <-> C |
| lt13 | Leading-tone | 4 | C <-> Edim |
| l41 | Leading-tone | 4 | Caug <-> Edim |
| l14 | Leading-tone | 4 | C <-> Eaug |
| rt23 | Relative | 3 | Cm <-> Adim |
| rt42 | Relative | 3 | Caug <-> Am |
| q13 | PLR* | 1 | C <-> C#dim |
| q42 | PLR* | 1 | Caug <-> C#m |
| n42 | PLR* | 5 | Caug <-> Fm |
* Remark A: We add <ic>t</ic> to <ic>l.13</ic>, <ic>r.23</ic>, <ic>r.42</ic> because we have similar syntax for sevenths transformations <ic>l13</ic>, <ic>r23</ic> and <ic>r42</ic>, although the meaning of the numbers (chord types) is different. See in the next section.
* Remark B: For those curious about mathematics, what we have implemented at Ziffers is the group called **PLR\*** [(Cannas, 2018, pp. 93-100)](https://publication-theses.unistra.fr/public/theses_doctorat/2018/CANNAS_Sonia_2018_ED269.pdf).
### Examples:
${makeExample(
"Synthetic 'Morton'",
`
z0('h. 0 q _6 h _4 _3 w _2 _0 h. ^0 q 6 h 4 3 3/4 2 5/4 0 w r')
.scale("minor").sound('sawtooth').key("A")
.room(0.9).size(9).phaser(0.25).phaserDepth(0.8)
.vib(4).vibmod(0.15).out()
z1('w 904')
.scale("chromatic")
.tonnetz('o f l l o f l l o')
.sound('sine').adsr(0.1, 1, 1, 1.9).out()
z2('904')
.scale("chromatic")
.tonnetz('o f l l o f l l o')
.arpeggio('s 0 2 1 0 1 2 1 0 2 1 0 1 2 0 1 0')
.sound('sine').pan(rand(1, 0)).adsr(0, 0.125, 0.15, 0.25).out()
z3('e __ 4 s 0 e 1 2 s')
.sound('hat').delay(0.5).delayfb(0.35).out()`,
true,
)}
## Different Tonnetz, Chord Complexes
At Ziffers we have strived to have fun and inspire you by exploring new sounds that Neo-Riemannian functions can offer you by changing only one parameter: The Tonnetz in which your chords move. By default, the Tonnetz has this form: <ic>[3, 4, 5]</ic>. Let's try an example as it will clarify this idea for us.
The <ic>Cm</ic> chord has the tone classes: <ic>037</ic>. Notice that the distance between the third of the chord and the root of the chord is <ic>3</ic> <ic>(3-0)</ic>. In turn, the distance of the fifth from the third is <ic>4</ic> semitones <ic>(7-3)</ic>. Finally, the distance left to get from the fifth to the root is <ic>5</ic> <ic>(7+5=0)</ic>. These distances are known as **intervalic structure**. In this regard, the array <ic>[x = 3, y = 4, z = 5]</ic> of a Tonnetz tells us the intervallic structure of the chords to which we apply the Neo-Riemannian functions.
:warning: To have a geometric intuition of the chords that we are going to describe, we suggest you see the <a href="https://numeric-tonnetz-ziffers-6f7c9299bb4e1292f6891b9aceba16d81409236.gitlab.io/" target="_blank">numerical Tonnetz.</a>
In the next three Tonnetz we consider that we go from a minor chord to a major one by inversion (we change <ic>x</ic> and <ic>y</ic>).
* For the Tonnetz <ic>[3, 4, 5]</ic> we have minor chords <ic>037</ic> and major chords <ic>047</ic>
* For a Tonnetz <ic>[2, 3, 7]</ic> we have the "minor" chords <ic>025</ic> and the "major" chords <ic>035</ic>
* For a Tonnetz <ic>[1, 4, 7]</ic> we have the "minor" chords <ic>015</ic> and the "major" chords <ic>045</ic>
Are those all the Tonnetz? In fact, there are <ic>12</ic> spaces that comply with symmetries by **transposition and inversion**:
<ic>[3, 4, 5], [1, 1, 10], [1, 2, 9], [1, 3, 8], [1, 4, 7], [1, 5, 6], [ 2, 3, 7], [ 2, 5, 5]</ic>
<ic>[2, 4, 6], [2, 2, 8], [3, 3, 6], [4, 4, 4]</ic>
What do augmented chords or seventh chords sound like on a Tonnetz <ic>[1,5,6]</ic>? It is up to you to **explore all the transformations in different spaces**.
What if I want to place another type of Tonnetz that is not on the list? No problem, everyone is invited to the party.
* Remark C: If you want to know more about the topology and mathematics behind Tonnetz, you can refer to [Bigo (2013)](https://theses.hal.science/tel-01326827).
## Tetra transformations
Did you want to experiment with more functions? At Ziffers we have brought you Neo-Riemannian functions for seventh chords. The possibilities of sound exploration increase considerably, even more so if you **include different Tonnetz**.
Tetra transformations can be applied to seventh chords using the <ic>tetraTonnetz(transformation: string, tonnetz: number[])</ic> method. This method will apply specific transformations to certain type of chords. If the **chord is not the correct type**, the **transformation will not be applied**.
:warning: If you are here without having read the **triad chords transformations section**, we highly suggest you skip to it. The ideas and notation shown in this section are nothing more than an extension of what was developed above.
First, here we will deal with **9 interchangeable chord types** to which we will assign a number:
<ic>1 = 7, 2 = m7, 3 = hdim7, 4 = maj7, 5 = dim7, 6 = minMaj7, 7 = maj7#5, 8 = 7#5, 9 = 7b5</ic>.
Second, the **transpositions** that carry out the functions <ic>p = 0, l = 4, r = 3, q = 1, n = 5</ic>, raise or lower the **root of the chord** in semitones in equal measure. However, there are two new types of functions whose transpositions are the following: <ic>rr = 6</ic> and <ic>qq = 2</ic>.
You are ready, these have been all the requirements. Now a couple of examples will be enough for you to know how these functions operate.
* The <ic>p14</ic> function **does not move the root (0 semitones)** and changes a dominant 7th chord to a major 7th chord. For example: <ic>C7 <-> Cmaj7</ic>.
* The <ic>rr39</ic> function **moves the root 6 semitones** and swaps an hdim7 chord for a 7b5 chord. For example: <ic>Cm7b5 <-> F#7b5</ic>.
So that you can incorporate this new musical machinery into your game, all the possible transformations according to the type of seventh chord are listed below. You already know what each one will do.
* Remark D: For those curious about the mathematics behind it, we have implemented a group called **PLRQ** and another group called **PLRQ\*** extended [(Cannas, 2018, pp. 71-92)](https://publication-theses.unistra.fr/public/theses_doctorat/2018/CANNAS_Sonia_2018_ED269.pdf).
| Chord type | P functions | L functions | R functions | Q functions | N functions |
| :--------: | :----------------: | :-----------: | :----------------------------: | :---------: | :---------: |
| 7 | p12, p14, p18, p19 | l13, l15, l71 | r12, rr19 | q15, qq51 | n51 |
| m7 | p12, p23, p26 | l42 | r12, r23, r42 | q62 | |
| hdim7 | p23, p35, p39 | l13 | r23, r35, r53, r63, rr35, rr39 | q43, qq38 | |
| maj7 | p14, p47, p64 | l42 | r42 | q43 | |
| dim7 | p35 | l15 | r35, r53 | q15, qq51 | n51 |
| minMaj7 | p26, p64 | | r63, r76, r86 | q62, q76 | |
| maj7#5 | p47, p87 | l71 | r76 | q76 | |
| dom7#5 | p18, p87, p98 | l89 | r86, rr98 | qq38, qq98 | |
| dom7b5 | p19, p39, p98 | l89 | rr19, rr39, rr98 | qq98 | |
### Examples
${makeExample(
"Transform seventh chord from chromatic scale",
`
z1("1.0 047{10}")
.scale('chromatic')
.tetraTonnetz("o p18 q15 p14p47r76 l13 n51 x p19rr39 r12")
.sound("sawtooth")
.cutoff(500 + usine(1/8) * 2000)
.adsr(.5,0.05,0.25,0.5)
.dur(2.0)
.log("pitch")
.out()`,
true,
)}
It is quite convenient to observe the resulting chords using <ic>log("pitch")</ic> when you have many operations. As with functions for triads, the way to **compose functions** is to write them **without spaces**. Note that text strings that are not functions operate like the **identity transformation**. Only enabled functions will alter the final result.
## Cyclic methods
In addition to the traditional tonnetz transformations, Ziffers implements cyclic methods that can be used to cycle through the tonnetz space. Cyclic methods turns individual pitch classes to chords using the tonnetz. The cyclic methods are:
* <ic>hexaCycle(tonnetz: number[], repeats: number = 3, components: number = 1)</ic>: Cycles through chords via hexatonic cycles
* <ic>octaCycle(tonnetz: number[], repeats: number = 4, components: number = 1)</ic>: Cycles through chords via octatonic cycles
* <ic>enneaCycle(tonnetz: number[], repeats: number = 3, components: number = 1)</ic>: Cycles through chords via enneatonic cycles
:warning: By default, the number of graph <ic>components</ic> is set to <ic>1</ic>. Therefore, these methods produce a single hexatonic, octatonic, and enneatonic cycle, respectively. OctaTowers were implemented in the same way, so it generates a single octatonic tower. Try increasing the number of components to obtain different graphs.
**HexaCycles** are sequences of major and minor triads generated by the <ic>p</ic> and <ic>l</ic> transformations. Let's take the following example starting with a <ic>C</ic> chord: <ic>C -> Cm -> Ab -> Abm -> E -> Em</ic>. You can start on the chord of your choice.
**OctaCycles** are sequences of major and minor triads generated using <ic>p</ic> and <ic>r</ic> transformations. Starting at <ic>C</ic>, we have the following sequence: <ic>C -> Cm -> Eb -> Ebm -> F# -> F#m -> A -> Am</ic>.
Unlike HexaCycles and OctaCycles, **EnneaCycles** are four-note chord sequences. Considering the functions implemented for tetrachords in Ziffers, we can interpret these sequences as generated by <ic>p12, p23, and l13</ic> transformations repeatedly: <ic>C7 -> Cm7 -> Cm7b5 -> Ab7 -> Abm7 -> Abm7b5 -> E7 -> Em7 -> Em7b5</ic>.
### Examples:
${makeExample(
"Arpeggio with ennea cycle",
`
z1("0 2 -1 3")
.enneaCycle()
.arpeggio(0,2,1)
.scale("modimic")
.noteLength(0.15,0.05,0.05,0.25)
.sound("sine")
.adsr(0.1,0.15,0.25,0.1)
.out()`,
true,
)}
${makeExample(
"Variating arpeggios",
`
z1("s 0 3 2 1")
.octaCycle()
.arpeggio([0,[0,2],[1,0],[0,1,2]].beat(0.15))
.sound("triangle")
.adsr(0.1,0.1,0.13,0.15)
.out()`,
true,
)}
## Cycles with vitamins and repetitions
Finally, cyclic methods in Ziffers can also be vitaminized with doses of different Tonnetz. However, this opens the way to different behavior with cycles.
We have the Tonnetz <ic>[2, 3, 7]</ic>, so <ic>hexaCycle([2, 3, 7])</ic>. The generated chords we hear are:
<ic>035 -> 025 -> 902 -> 9{11}2 -> 69{11} -> 68{11} </ic>
Apparently, everything operates as we expect: six chords and we return to the first. However, here comes the unexpected and perhaps somewhat obscure question:
* If we look at the graphs of the [numeric Tonnetz](https://numeric-tonnetz-ziffers-6f7c9299bb4e1292f6891b9aceba16d81409236.gitlab.io/), our hexaCycle over <ic>[2, 3, 7]</ic> which starts with the chord <ic>035</ic> goes through all the intermediate chords generated by <ic>p</ic> and <ic>l</ic> functions until reaching <ic>035</ic> again?
As you can verify it manually, you will see that this is not the case. Upon reaching the <ic>68{11} </ic> chord, the cycle makes a jump of two chords (<ic>368 358</ic>) towards the <ic>035</ic> chord. This does not happen with the cycles in the Tonnetz <ic>[3, 4, 5]</ic>, since all the intermediate chords are played there.
To play the chords without jumps in our hexaCycle (although the prefix "hexa" would no longer have a precise meaning), we add a number of repetitions.
${makeExample(
"HexaCycles with vitamins",
`
z1("0")
.scale("chromatic")
.hexaCycle([2,3,7],4)
.sound("sine").out()
`,
true
)}
By default hexaCycles and enneaCycles have <ic>3</ic> repetitions, while octaCycles has <ic>4</ic> repetitions. We have specified a **chromatic scale** although this is the **default scale**. Try changing the **repeats and scales** when playing with different Tonnetz.
* Remark E: These cycles in Tonnetz <ic>[3, 4, 5]</ic> are implemented based on the work of [Douthett & Steinbach (1998, pp. 245-247, 253-256)](https://www.jstor.org/stable/843877)
## More traversing methods
In addition to the cyclical traversing methods, Ziffers implements traversing methods that traverse the Tonnetz in different ways. These methods are:
* <ic>weitzmannRegions(tonnetz: number[])</ic>: Cycles through chords in a Weitzmann region
* <ic>boretzRegions(tonnetz: number[])</ic>: Cycles through chords in a Boretz region
* <ic>octaTowers(tonnetz: number[], repeats: number = 3, components: number = 1)</ic>: Cycles through chords using the octaTowers
* <ic>cubeDance(tonnetz: number[], repeats: number = 3)</ic>: Cycles through chords in a Cube Dance
* <ic>powerTowers(tonnetz: number[], repeats: number = 3)</ic>: Cycles through chords using the Power Towers
**Weitzmann Regions** is composed only of three-note chords. Following Richard Cohn's **Weitzmann water bug** graph, the region consists of an augmented chord (body), three major chords, and three minor chords (feet). The latter related to the central chord by a minimal parsimonious movement. A cyclic order of **Nebenverdwandt / R** transformations proposed by Carl Weitzmann himself has been chosen.
**Boretz Regions** is the four-note analogue of the Weitzmann regions. Richard Cohn draws them in **Boretz Spiders**, a graph consisting of 8 feet between 7th and half-diminished 7th chords. The body (prosoma-opisthosoma) is a <ic>dim7</ic> chord, related to the others by a semitonal movement.
**OctaTowers** generates a graph composed of **12** chords, whose types are <ic>halfdim7, m7 and 7</ic>. A reading from left to right in an ascending diagonal has been chosen. Note that changing the number of components to <ic>3</ic> will obtain the complete graph (**36** chords).
**Cube Dance** is another graph of **28** chords that is built primarily with HexaCycles (4 hexatonic cycles), except that it adds <ic>augmented</ic> triads as assemblers. As with Power Towers, one possible path has been selected.
**Power Towers** use **39** four-note chords (<ic>halfdim7, m7 and 7</ic>). As you can notice, it is composed of OctaTowers (3 octatonic towers) assembled by <ic>dim7</ic> type chords. One of the many paths for succession has been chosen.
As you have noticed, all these graphs usually have many chords, so sometimes it will be convenient to slice up fragments of the cycles. We encourage you to explore these methods and their different parameters. The tonnetz traversing methods can be used in combination with the Ziffers generative methods to sequence, arpeggiate and to randomize the chords in different ways.
${makeExample(
"Cube Dance swing",
`
z1("0").cubeDance([3,4,5])
.sound("sine")
.ad(r(0.1,0.5),0.1)
.out()
`,
true,
)}
${makeExample(
"Selecting subset of chords from the cube dance",
`
z1("1/2 0")
.cubeDance([3,4,5],4)
.at(0,8,2,rI(9,14))
.sound("triangle")
.ad(0.05,0.15)
.delay(2)
.out()
`,
true
)}
${makeExample(
"Power Towers with pulse",
`
z1("1/4 2").powerTowers([2,3,7])
.between(5,11)
.arpeggio("e 0 3 1 2")
.sound("sine")
.adsr(0.01,0.1,0.1,0.9)
.out()
`,
true,
)}
${makeExample(
"Between an OctaTower",
`
z1("s. 0")
.octaTower()
.between(2,8)
.arpeggio(3,2,1,rI(1,5))
.sound("sawtooth")
.adsr(0.1,0.15,0,0.1)
.out()
`,
true
)}
${makeExample(
"Selecting chords from the weitzmann region",
`
z1("1/8 0")
.weitzmannRegions()
.at(1,rI(0,7),4,6)
.arpeggio(0,2,1,rI(0,2))
.sound("sine")
.ad(0.15,0.15)
.out()
`,
true
)}
${makeExample(
"Boretz Spider",
`
z1("1/16 0")
.boretzRegions([1,4,7])
.at(2,rI(3,7),4,6)
.arpeggio(1,0,2,rI(1,4))
.sound("square")
.adsr(0.1,0.1,0.1,0.2)
.out()
`,
true
)}
* Remark F: You can find more details about Weitzmann and Boretz regions in chapters 4 and 7 of Richard Cohn's book [Audacious Euphony: Chromatic Harmony and the Triad's Second Nature (2012)](https://books.google.com.pe/books?id=rZxZCMRiO9EC&pg=PA59&hl=es&source=gbs_toc_r&cad=2#v=onepage&q&f=false).
`;
};

150
src/Documentation.ts Normal file
View File

@ -0,0 +1,150 @@
import { type Editor } from "./main";
// Basics
import { introduction } from "./documentation/basics/welcome";
import { loading_samples } from "./documentation/samples/loading_samples";
import { amplitude } from "./documentation/audio_engine/amplitude";
import { reverb } from "./documentation/audio_engine/reverb_delay";
import { sampler } from "./documentation/audio_engine/sampler";
import { sample_banks } from "./documentation/samples/sample_banks";
import { audio_basics } from "./documentation/audio_engine/audio_basics";
import { sample_list } from "./documentation/samples/sample_list";
import { software_interface } from "./documentation/basics/interface";
import { shortcuts } from "./documentation/basics/keyboard";
import { code } from "./documentation/basics/code";
import { mouse } from "./documentation/basics/mouse";
// More
import { oscilloscope } from "./documentation/more/oscilloscope";
import { synchronisation } from "./documentation/more/synchronisation";
import { about } from "./documentation/more/about";
import { bonus } from "./documentation/more/bonus";
import { chaining } from "./documentation/chaining";
import { interaction } from "./documentation/interaction";
import { time } from "./documentation/time/time";
import { linear_time } from "./documentation/time/linear_time";
import { cyclical_time } from "./documentation/time/cyclical_time";
import { long_forms } from "./documentation/long_forms";
import { midi } from "./documentation/midi";
import { sound } from "./documentation/engine";
import { patterns } from "./documentation/patterns";
import { functions } from "./documentation/functions";
import { variables } from "./documentation/variables";
import { probabilities } from "./documentation/probabilities";
import { lfos } from "./documentation/lfos";
import { ziffers } from "./documentation/ziffers";
import { synths } from "./documentation/synths";
// Setting up the Markdown converter with syntax highlighting
import showdown from "showdown";
import showdownHighlight from "showdown-highlight";
import { createDocumentationStyle } from "./DomElements";
showdown.setFlavor("github");
export const key_shortcut = (shortcut: string): string => {
return `<kbd class="lg:px-2 lg:py-1.5 px-1 py-1 lg:text-sm text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-600 dark:text-gray-100 dark:border-gray-500">${shortcut}</kbd>`;
};
export const makeExampleFactory = (application: Editor): Function => {
const make_example = (
description: string,
code: string,
open: boolean = false
) => {
const codeId = `codeExample${application.exampleCounter++}`;
// Store the code snippet in the data structure
application.api.codeExamples[codeId] = code;
return `
<details ${open ? "open" : ""}>
<summary >${description}
<button class="ml-4 py-1 align-top text-base px-4 hover:bg-green-700 bg-emerald-600 inline-block" onclick="app.api._playDocExample(app.api.codeExamples['${codeId}'])">▶️ Play</button>
<button class="py-1 text-base px-4 hover:bg-neutral-600 bg-neutral-500 inline-block " onclick="app.api._stopDocExample()">&#x23f8;&#xFE0F; Pause</button>
<button class="py-1 text-base px-4 hover:bg-neutral-900 bg-neutral-800 inline-block " onclick="navigator.clipboard.writeText(app.api.codeExamples['${codeId}'])">📎 Copy</button>
</summary>
\`\`\`javascript
${code}
\`\`\`
</details>
`;
};
return make_example;
};
export const documentation_factory = (application: Editor) => {
// Initialize a data structure to store code examples by their unique IDs
application.api.codeExamples = {};
return {
introduction: introduction(application),
interface: software_interface(application),
interaction: interaction(application),
code: code(application),
time: time(application),
linear: linear_time(application),
cyclic: cyclical_time(application),
longform: long_forms(application),
sound: sound(application),
synths: synths(application),
chaining: chaining(application),
patterns: patterns(application),
ziffers: ziffers(application),
midi: midi(application),
lfos: lfos(application),
variables: variables(application),
probabilities: probabilities(application),
functions: functions(application),
shortcuts: shortcuts(application),
amplitude: amplitude(application),
reverb_delay: reverb(application),
sampler: sampler(application),
mouse: mouse(application),
oscilloscope: oscilloscope(application),
audio_basics: audio_basics(application),
synchronisation: synchronisation(application),
bonus: bonus(application),
sample_list: sample_list(application),
sample_banks: sample_banks(application),
loading_samples: loading_samples(application),
about: about(),
};
};
export const showDocumentation = (app: Editor) => {
if (document.getElementById("app")?.classList.contains("hidden")) {
document.getElementById("app")?.classList.remove("hidden");
document.getElementById("documentation")?.classList.add("hidden");
app.exampleIsPlaying = false;
} else {
document.getElementById("app")?.classList.add("hidden");
document.getElementById("documentation")?.classList.remove("hidden");
// Load and convert Markdown content from the documentation file
let style = createDocumentationStyle(app);
let bindings = Object.keys(style).map((key) => ({
type: "output",
regex: new RegExp(`<${key}([^>]*)>`, "g"),
//@ts-ignore
replace: (match, p1) => `<${key} class="${style[key]}" ${p1}>`,
}));
updateDocumentationContent(app, bindings);
}
};
export const hideDocumentation = () => {
if (document.getElementById("app")?.classList.contains("hidden")) {
document.getElementById("app")?.classList.remove("hidden");
document.getElementById("documentation")?.classList.add("hidden");
}
};
export const updateDocumentationContent = (app: Editor, bindings: any) => {
const converter = new showdown.Converter({
emoji: true,
moreStyling: true,
backslashEscapesHTMLTags: true,
extensions: [showdownHighlight({ auto_detection: true }), ...bindings],
});
const converted_markdown = converter.makeHtml(
app.docs[app.currentDocumentationPane]
);
document.getElementById("documentation-content")!.innerHTML =
converted_markdown;
};

View File

@ -1,13 +1,25 @@
import { type Editor } from "../main";
import { type Editor } from "./main";
export type ElementMap = {
[key: string]:
| HTMLElement
| HTMLButtonElement
| HTMLDivElement
| HTMLInputElement
| HTMLSelectElement
| HTMLCanvasElement
| HTMLFormElement
| HTMLInputElement
;
};
export const singleElements = {
logo: "topos_logo",
topos_logo: "topos-logo",
fill_viewer: "fillviewer",
load_universe_button: "load-universe-button",
download_universe_button: "download-universes",
upload_universe_button: "upload-universes",
upload_samples_button: "upload-samples",
sample_indicator: "sample-indicator",
destroy_universes_button: "destroy-universes",
documentation_button: "doc-button-1",
eval_button: "eval-button-1",
@ -29,12 +41,10 @@ export const singleElements = {
line_numbers_checkbox: "show-line-numbers",
time_position_checkbox: "show-time-position",
tips_checkbox: "show-tips",
transport_viewer: "transport_viewer",
completion_checkbox: "show-completions",
midi_clock_checkbox: "send-midi-clock",
midi_channels_scripts: "midi-channels-scripts",
midi_clock_ppqn: "midi-clock-ppqn-input",
theme_selector: "theme-selector",
load_demo_songs: "load-demo-songs",
normal_mode_button: "normal-mode",
vim_mode_button: "vim-mode",
@ -45,60 +55,43 @@ export const singleElements = {
hydra_canvas: "hydra-bg",
feedback: "feedback",
scope: "scope",
play_button: "play-button",
play_label: "play-label",
stop_button: "stop-button",
play_icon: "play-icon",
pause_icon: "pause-icon",
} as const;
};
export type SingleElementsKeys = keyof typeof singleElements;
export type ElementMap = {
[K in SingleElementsKeys]:
| HTMLElement
| HTMLButtonElement
| HTMLDivElement
| HTMLInputElement
| HTMLSelectElement
| HTMLCanvasElement
| HTMLFormElement
| HTMLInputElement;
export const buttonGroups = {
play_buttons: ["play-button-1"],
stop_buttons: ["stop-button-1"],
clear_buttons: ["clear-button-1"],
};
//@ts-ignore
export const createDocumentationStyle = (app: Editor) => {
/**
* Creates a documentation style object.
* @param {Editor} app - The editor object.
* @returns {Object} - The documentation style object.
*/
return {
h1: "text-brightwhite lg:text-4xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 border-b-4 pt-4 pb-3 px-2",
h2: "text-brightwhite lg:text-3xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 border-b-2 pt-12 pb-3 px-2",
h3: "text-brightwhite lg:text-2xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 border-l-2 border-b-2 lg:mb-4 mb-4 pb-2 px-2 lg:mt-16",
h1: "text-white lg:text-4xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 border-b-4 pt-4 pb-3 px-2",
h2: "text-white lg:text-3xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 border-b-2 pt-12 pb-3 px-2",
h3: "text-white lg:text-2xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 border-l-2 border-b-2 lg:mb-4 mb-4 pb-2 px-2 lg:mt-16",
ul: "text-underline ml-12",
li: "list-disc lg:text-2xl text-base text-white lg:mx-4 mx-2 my-4 my-2 leading-normal",
p: "lg:text-2xl text-base text-white lg:mx-6 mx-2 my-4 leading-normal",
warning:
"animate-pulse lg:text-2xl font-bold text-brightred lg:mx-6 mx-2 my-4 leading-normal",
a: "lg:text-2xl text-base text-brightred",
"animate-pulse lg:text-2xl font-bold text-rose-600 lg:mx-6 mx-2 my-4 leading-normal",
a: "lg:text-2xl text-base text-orange-300",
code: `lg:my-4 sm:my-1 text-base lg:text-xl block whitespace-pre overflow-x-hidden`,
icode:
"lg:my-1 my-1 lg:text-xl sm:text-xs text-brightwhite font-mono bg-brightblack",
ic: "lg:my-1 my-1 lg:text-xl sm:text-xs text-brightwhite font-mono bg-brightblack",
blockquote: "text-brightwhite border-l-4 border-white pl-4 my-4 mx-4",
"lg:my-1 my-1 lg:text-xl sm:text-xs text-white font-mono bg-neutral-600",
ic: "lg:my-1 my-1 lg:text-xl sm:text-xs text-white font-mono bg-neutral-600",
blockquote: "text-neutral-200 border-l-4 border-neutral-500 pl-4 my-4 mx-4",
details:
"lg:mx-20 py-2 px-6 lg:text-2xl text-white border-l-8 box-border bg-selection_foreground",
"lg:mx-20 py-2 px-6 lg:text-2xl text-white border-l-8 box-border bg-neutral-900",
summary: "font-semibold text-xl",
table:
"justify-center lg:my-12 my-2 lg:mx-12 mx-2 lg:text-2xl text-base w-full text-left text-white border-collapse",
thead:
"text-xs text-gray-700 uppercase",
"text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400",
th: "",
td: "",
tr: "",
box: "border bg-red",
};
box: "border bg-red-500",
};
}

View File

@ -1,370 +0,0 @@
import { Prec } from "@codemirror/state";
import { indentWithTab } from "@codemirror/commands";
import { tags as t } from "@lezer/highlight";
import {
keymap,
lineNumbers,
highlightSpecialChars,
drawSelection,
highlightActiveLine,
dropCursor,
highlightActiveLineGutter,
} from "@codemirror/view";
import { Extension, EditorState } from "@codemirror/state";
import { vim } from "@replit/codemirror-vim";
import {
defaultHighlightStyle,
syntaxHighlighting,
indentOnInput,
bracketMatching,
HighlightStyle,
} from "@codemirror/language";
import { defaultKeymap, historyKeymap, history } from "@codemirror/commands";
import { highlightSelectionMatches } from "@codemirror/search";
import {
autocompletion,
closeBrackets,
closeBracketsKeymap,
} from "@codemirror/autocomplete";
import { lintKeymap } from "@codemirror/lint";
import { Compartment } from "@codemirror/state";
import { Editor } from "../main";
import { EditorView } from "codemirror";
import { javascript } from "@codemirror/lang-javascript";
import { inlineHoveringTips, toposCompletions, soundCompletions } from "../Docs/inlineHelp";
import { javascriptLanguage } from "@codemirror/lang-javascript";
export const getCodeMirrorTheme = (theme: { [key: string]: string }): Extension => {
// @ts-ignore
const black = theme["black"],
red = theme["red"],
green = theme["green"],
yellow = theme["yellow"],
blue = theme["blue"],
magenta = theme["magenta"],
cyan = theme["cyan"],
white = theme["white"],
// @ts-ignore
brightblack = theme["brightblack"],
// @ts-ignore
brightred = theme["brightred"],
brightgreen = theme["brightgreen"],
// @ts-ignore
brightyellow = theme["brightyellow"],
// @ts-ignore
brightblue = theme["brightblue"],
// @ts-ignore
brightmagenta = theme["brightmagenta"],
// @ts-ignore
brightcyan = theme["brightcyan"],
brightwhite = theme["brightwhite"],
background = theme["background"],
selection_foreground = theme["selection_foreground"],
cursor = theme["cursor"],
foreground = theme["foreground"],
selection_background = theme["selection_background"];
const toposTheme = EditorView.theme({
"&": {
color: background || "",
backgroundColor: "transparent",
fontSize: "24px",
fontFamily: "IBM Plex Mono",
},
".cm-content": {
caretColor: cursor || '',
fontFamily: "IBM Plex Mono",
},
".cm-line": {
color: `${brightwhite}`,
},
".cm-cursor, .cm-dropCursor": {
borderLeftColor: cursor || 'white',
},
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
{
backgroundColor: brightwhite || 'black',
border: `1px solid ${brightwhite}`,
},
".cm-panels": {
backgroundColor: selection_background || 'gray',
color: red || '',
},
".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" },
".cm-search.cm-panel": { backgroundColor: "transparent" },
".cm-searchMatch": {
outline: `1px solid ${magenta}`,
},
".cm-searchMatch.cm-searchMatch-selected": {
backgroundColor: red || '',
},
".cm-activeLine": {
backgroundColor: `rgba(${(parseInt(selection_background!.slice(1, 3), 16))}, ${(parseInt(selection_background!.slice(3, 5), 16))}, ${(parseInt(selection_background!.slice(5, 7), 16))}, 0.25)`,
},
".cm-selectionMatch": {
backgroundColor: `rgba(${(parseInt(selection_background!.slice(1, 3), 16))}, ${(parseInt(selection_background!.slice(3, 5), 16))}, ${(parseInt(selection_background!.slice(5, 7), 16))}, 0.25)`,
outline: `1px solid ${brightwhite}`,
},
"&.cm-focused .cm-matchingBracket": {
color: `rgba(${(parseInt(selection_background!.slice(1, 3), 16))}, ${(parseInt(selection_background!.slice(3, 5), 16))}, ${(parseInt(selection_background!.slice(5, 7), 16))}, 0.25)`,
},
"&.cm-focused .cm-nonmatchingBracket": {
color: yellow || '',
},
".cm-gutters": {
//backgroundColor: base00,
backgroundColor: "transparent",
color: foreground || '',
},
".cm-activeLineGutter": {
backgroundColor: selection_background || '',
color: selection_foreground || '',
},
".cm-foldPlaceholder": {
border: "none",
color: `${brightwhite}`,
},
".cm-tooltip": {
border: "none",
backgroundColor: background || '',
},
".cm-tooltip .cm-tooltip-arrow:before": {},
".cm-tooltip .cm-tooltip-arrow:after": {
borderTopColor: background || '',
borderBottomColor: background || '',
},
".cm-tooltip-autocomplete": {
"& > ul > li[aria-selected]": {
backgroundColor: background || '',
color: brightwhite || '',
},
},
},
{ dark: true },
);
let toposHighlightStyle = HighlightStyle.define([
{ tag: t.paren, color: brightwhite },
{ tag: [t.propertyName, t.punctuation, t.variableName], color: brightwhite },
{ tag: t.keyword, color: yellow },
{ tag: [t.name, t.deleted, t.character, t.macroName], color: red, },
{ tag: [t.function(t.variableName)], color: blue },
{ tag: [t.labelName], color: brightwhite },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: cyan, },
{ tag: [t.definition(t.name), t.separator], color: brightwhite },
{ tag: [t.brace], color: white },
{ tag: [t.annotation], color: blue, },
{ tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: yellow, },
{ tag: [t.typeName, t.className], color: magenta, },
{ tag: [t.operator, t.operatorKeyword], color: blue, },
{ tag: [t.tagName], color: blue, },
{ tag: [t.squareBracket], color: blue, },
{ tag: [t.angleBracket], color: blue, },
{ tag: [t.attributeName], color: red, },
{ tag: [t.regexp], color: brightgreen, },
{ tag: [t.quote], color: green, },
{ tag: [t.string], color: green },
{
tag: t.link,
color: green,
textDecoration: "underline",
textUnderlinePosition: "under",
},
{
tag: [t.url, t.escape, t.special(t.string)],
color: green,
},
{ tag: [t.meta], color: brightwhite },
{ tag: [t.comment], color: brightwhite, fontStyle: "italic" },
{ tag: t.monospace, color: brightwhite },
{ tag: t.strong, fontWeight: "bold", color: white },
{ tag: t.emphasis, fontStyle: "italic", color: white },
{ tag: t.strikethrough, textDecoration: "line-through" },
{ tag: t.heading, fontWeight: "bold", color: white },
{ tag: t.heading1, fontWeight: "bold", color: white },
{
tag: [t.heading2, t.heading3, t.heading4],
fontWeight: "bold",
color: yellow,
},
{
tag: [t.heading5, t.heading6],
color: red,
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: green },
{
tag: [t.processingInstruction, t.inserted],
color: green,
},
{
tag: [t.contentSeparator],
color: green,
},
{
tag: [t.content],
color: brightwhite
},
{
tag: t.invalid,
color: red,
borderBottom: `1px dotted ${red}`
},
{
tag: t.null,
color: brightwhite,
}
]);
return [toposTheme, syntaxHighlighting(toposHighlightStyle),
]
}
const debugTheme = EditorView.theme({
".cm-line span": {
position: "relative",
},
".cm-line span:hover::after": {
position: "absolute",
bottom: "100%",
left: 0,
background: "black",
color: "white",
border: "solid 2px",
borderRadius: "5px",
content: "var(--tags)",
width: `max-content`,
padding: "1px 4px",
zIndex: 10,
pointerEvents: "none",
},
});
const debugHighlightStyle = HighlightStyle.define(
// @ts-ignore
Object.entries(t).map(([key, value]) => {
return { tag: value, "--tags": `"tag.${key}"` };
})
);
const debug = [debugTheme, syntaxHighlighting(debugHighlightStyle)];
export const switchToDebugTheme = (app: Editor) => {
app.view.dispatch({
effects: app.themeCompartment.reconfigure(debug),
});
}
export const jsCompletions = javascriptLanguage.data.of({
autocomplete: toposCompletions,
});
export const toposSoundCompletions = javascriptLanguage.data.of({
autocomplete: soundCompletions,
});
export const editorSetup: Extension = (() => [
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
bracketMatching(),
closeBrackets(),
autocompletion(),
highlightActiveLine(),
highlightSelectionMatches(),
keymap.of([
// ...searchKeymap,
...closeBracketsKeymap,
...defaultKeymap,
...historyKeymap,
...lintKeymap,
]),
])();
export const installEditor = (app: Editor) => {
app.vimModeCompartment = new Compartment();
app.hoveringCompartment = new Compartment();
app.themeCompartment = new Compartment();
app.completionsCompartment = new Compartment();
app.withLineNumbers = new Compartment();
app.chosenLanguage = new Compartment();
app.fontSize = new Compartment();
const vimPlugin = app.settings.vimMode ? vim() : [];
const lines = app.settings.line_numbers ? lineNumbers() : [];
const fontModif = EditorView.theme({
"&": {
fontSize: `${app.settings.font_size}px`,
},
$content: {
fontFamily: `${app.settings.font}`,
fontSize: `${app.settings.font_size}px`,
},
".cm-gutters": {
fontSize: `${app.settings.font_size}px`,
},
});
app.editorExtensions = [
app.vimModeCompartment.of(vimPlugin),
app.withLineNumbers.of(lines),
app.fontSize.of(fontModif),
app.hoveringCompartment.of(app.settings.tips ? inlineHoveringTips : []),
app.completionsCompartment.of(
app.settings.completions ? [jsCompletions, toposSoundCompletions] : [],
),
editorSetup,
app.themeCompartment.of(
getCodeMirrorTheme(app.getColorScheme("Batman")),
// debug
),
app.chosenLanguage.of(javascript()),
];
app.dynamicPlugins = new Compartment();
app.state = EditorState.create({
extensions: [
...app.editorExtensions,
EditorView.lineWrapping,
app.dynamicPlugins.of(app.userPlugins),
Prec.highest(
keymap.of([
{
key: "Ctrl-Enter",
run: () => {
return true;
},
},
]),
),
keymap.of([indentWithTab]),
],
doc: app.universes[app.selected_universe]!.global.candidate,
});
app.view = new EditorView({
parent: document.getElementById("editor") as HTMLElement,
state: app.state,
});
// Re-apply font size and font family change
app.view.dispatch({
effects: app.fontSize.reconfigure(
EditorView.theme({
"&": {
fontSize: `${app.settings.font_size}px`,
},
$content: {
fontFamily: `${app.settings.font}`,
fontSize: `${app.settings.font_size}px`,
},
".cm-gutters": {
fontSize: `${app.settings.font_size}px`,
},
}),
),
});
};

File diff suppressed because it is too large Load Diff

145
src/EditorSetup.ts Normal file
View File

@ -0,0 +1,145 @@
import { Prec } from "@codemirror/state";
import { indentWithTab } from "@codemirror/commands";
import {
keymap,
lineNumbers,
highlightSpecialChars,
drawSelection,
highlightActiveLine,
dropCursor,
// rectangularSelection,
// crosshairCursor,
highlightActiveLineGutter,
} from "@codemirror/view";
import { Extension, EditorState } from "@codemirror/state";
import { vim } from "@replit/codemirror-vim";
import {
defaultHighlightStyle,
syntaxHighlighting,
indentOnInput,
bracketMatching,
} from "@codemirror/language";
import { defaultKeymap, historyKeymap, history } from "@codemirror/commands";
import { searchKeymap, highlightSelectionMatches } from "@codemirror/search"
import {
autocompletion,
closeBrackets,
closeBracketsKeymap,
} from "@codemirror/autocomplete";
import { lintKeymap } from "@codemirror/lint";
import { Compartment } from "@codemirror/state";
import { Editor } from "./main";
import { EditorView } from "codemirror";
import { toposTheme } from "./themes/toposTheme";
import { javascript } from "@codemirror/lang-javascript";
import { inlineHoveringTips } from "./documentation/inlineHelp";
import { toposCompletions, soundCompletions } from "./documentation/inlineHelp";
import { javascriptLanguage } from "@codemirror/lang-javascript"
export const jsCompletions = javascriptLanguage.data.of({
autocomplete: toposCompletions
})
export const toposSoundCompletions = javascriptLanguage.data.of({
autocomplete: soundCompletions
})
export const editorSetup: Extension = (() => [
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
bracketMatching(),
closeBrackets(),
autocompletion(),
highlightActiveLine(),
highlightSelectionMatches(),
keymap.of([
...searchKeymap,
...closeBracketsKeymap,
...defaultKeymap,
...historyKeymap,
...lintKeymap,
]),
])();
export const installEditor = (app: Editor) => {
app.vimModeCompartment = new Compartment();
app.hoveringCompartment = new Compartment();
app.completionsCompartment = new Compartment();
app.withLineNumbers = new Compartment();
app.chosenLanguage = new Compartment();
app.fontSize = new Compartment();
const vimPlugin = app.settings.vimMode ? vim() : [];
const lines = app.settings.line_numbers ? lineNumbers() : [];
const fontModif = EditorView.theme({
"&": {
fontSize: `${app.settings.font_size}px`,
},
$content: {
fontFamily: `${app.settings.font}`,
fontSize: `${app.settings.font_size}px`,
},
".cm-gutters": {
fontSize: `${app.settings.font_size}px`,
},
});
app.editorExtensions = [
app.vimModeCompartment.of(vimPlugin),
app.withLineNumbers.of(lines),
app.fontSize.of(fontModif),
app.hoveringCompartment.of(app.settings.tips ? inlineHoveringTips : []),
app.completionsCompartment.of(app.settings.completions ? [jsCompletions, toposSoundCompletions] : []),
editorSetup,
toposTheme,
app.chosenLanguage.of(javascript()),
];
app.dynamicPlugins = new Compartment();
app.state = EditorState.create({
extensions: [
...app.editorExtensions,
EditorView.lineWrapping,
app.dynamicPlugins.of(app.userPlugins),
Prec.highest(
keymap.of([
{
key: "Ctrl-Enter",
run: () => {
return true;
},
},
])
),
keymap.of([indentWithTab]),
],
doc: app.universes[app.selected_universe].global.candidate,
});
app.view = new EditorView({
parent: document.getElementById("editor") as HTMLElement,
state: app.state,
});
// Re-apply font size and font family change
app.view.dispatch({
effects: app.fontSize.reconfigure(
EditorView.theme({
"&": {
fontSize: `${app.settings.font_size}px`,
},
$content: {
fontFamily: `${app.settings.font}`,
fontSize: `${app.settings.font_size}px`,
},
".cm-gutters": {
fontSize: `${app.settings.font_size}px`,
},
})
),
});
};

View File

@ -1,32 +1,111 @@
import type { Editor } from "./main";
import type { File } from "./Editor/FileManagement";
import type { File } from "./FileManagement";
async function tryCatchWrapper(application: Editor, code: string): Promise<boolean> {
const delay = (ms: number) =>
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Operation took too long")), ms)
);
const codeReplace = (code: string): string => {
let new_code = code.replace(/->/g, "&&").replace(/::/g, "&&");
return new_code;
};
const tryCatchWrapper = (
application: Editor,
code: string
): Promise<boolean> => {
return new Promise((resolve, _) => {
try {
await new Function(`"use strict"; ${code}`).call(application.api);
return true;
Function(
`"use strict";try{
${codeReplace(code)}; /* break block comments */;
} catch (e) {console.log(e); _reportError(e);};`
).call(application.api);
resolve(true);
} catch (error) {
console.error(error);
return false;
}
application.interface.error_line.innerHTML = error as string;
application.api._reportError(error as string)
resolve(false);
}
});
};
export async function tryEvaluate(application: Editor, code: File): Promise<void> {
const wrappedCode = `let i = ${code.evaluations}; ${code.candidate}`;
const isCodeValid = await tryCatchWrapper(application, wrappedCode);
const cache = new Map<string, Function>();
const MAX_CACHE_SIZE = 20;
const addFunctionToCache = (code: string, fn: Function) => {
if (cache.size >= MAX_CACHE_SIZE) {
// Delete the first item if cache size exceeds max size
cache.delete(cache.keys().next().value);
}
cache.set(code, fn);
};
export const tryEvaluate = async (
application: Editor,
code: File,
timeout = 5000
): Promise<void> => {
try {
code.evaluations!++;
const candidateCode = code.candidate;
if (cache.has(candidateCode)) {
// If the code is already in cache, use it
cache.get(candidateCode)!.call(application.api);
} else {
const wrappedCode = `let i = ${code.evaluations};` + candidateCode;
// Otherwise, evaluate the code and if valid, add it to the cache
const isCodeValid = await Promise.race([
tryCatchWrapper(application, wrappedCode as string),
delay(timeout),
]);
if (isCodeValid) {
code.committed = code.candidate;
const newFunction = new Function(
`"use strict";try{${codeReplace(
wrappedCode
)}} catch (e) {console.log(e); _reportError(e);};`
);
addFunctionToCache(candidateCode, newFunction);
} else {
console.error("Compilation error!");
await evaluate(application, code, timeout);
}
}
} catch (error) {
application.interface.error_line.innerHTML = error as string;
application.api._reportError(error as string)
}
};
export async function evaluateOnce(application: Editor, code: File): Promise<void> {
const wrappedCode = `let i = ${code.evaluations}; ${code.candidate}`;
const isCodeValid = await tryCatchWrapper(application, wrappedCode);
if (isCodeValid) {
code.committed = code.candidate;
} else {
console.error("Compilation error!");
}
export const evaluate = async (
application: Editor,
code: File,
timeout = 1000
): Promise<void> => {
try {
await Promise.race([
tryCatchWrapper(application, code.committed as string),
delay(timeout),
]);
if (code.evaluations) code.evaluations++;
} catch (error) {
application.interface.error_line.innerHTML = error as string;
console.log(error);
}
};
export const evaluateOnce = async (
application: Editor,
code: string
): Promise<void> => {
/**
* Evaluates the code once without any caching or error-handling mechanisms besides the tryCatchWrapper.
*
* @param application - The application object that contains the Editor API.
* @param code - The code to be evaluated.
* @returns A promise that resolves when the code has been evaluated.
*/
await tryCatchWrapper(application, code);
};

View File

@ -1,7 +1,9 @@
// import { tutorial_universe } from "./universes/tutorial";
import { gzipSync, decompressSync, strFromU8 } from "fflate";
import { type Editor } from "../main";
import { examples } from "./examples/excerpts";
import { type Editor } from "./main";
import { uniqueNamesGenerator, colors, animals } from "unique-names-generator";
import { tryEvaluate } from "../Evaluator";
import { tryEvaluate } from "./Evaluator";
export type Universes = { [key: string]: Universe };
export interface Universe {
@ -61,7 +63,7 @@ export interface Settings {
selected_universe: string;
line_numbers: boolean;
time_position: boolean;
// load_demo_songs: boolean;
load_demo_songs: boolean;
tips: boolean;
completions: boolean;
send_clock: boolean;
@ -134,7 +136,7 @@ export class AppSettings {
*/
public vimMode: boolean = false;
public theme: string = "Everblush";
public theme: string = "toposTheme";
public font: string = "IBM Plex Mono";
public font_size: number = 24;
public universes: Universes;
@ -148,11 +150,11 @@ export class AppSettings {
public midi_clock_input: string | undefined = undefined;
public default_midi_input: string | undefined = undefined;
public midi_clock_ppqn: number = 24;
// public load_demo_songs: boolean = true;
public load_demo_songs: boolean = true;
constructor() {
const settingsFromStorage = JSON.parse(
localStorage.getItem("topos") || "{}",
localStorage.getItem("topos") || "{}"
);
if (settingsFromStorage && Object.keys(settingsFromStorage).length !== 0) {
@ -172,14 +174,14 @@ export class AppSettings {
this.midi_clock_input = settingsFromStorage.midi_clock_input;
this.midi_clock_ppqn = settingsFromStorage.midi_clock_ppqn || 24;
this.default_midi_input = settingsFromStorage.default_midi_input;
// this.load_demo_songs = settingsFromStorage.load_demo_songs;
this.load_demo_songs = settingsFromStorage.load_demo_songs;
} else {
this.universes = template_universes;
}
}
get_universe() {
this.universes["universe_name"];
this.universes.universe_name;
}
get data(): Settings {
@ -202,13 +204,13 @@ export class AppSettings {
midi_clock_input: this.midi_clock_input,
midi_clock_ppqn: this.midi_clock_ppqn,
default_midi_input: this.default_midi_input,
// load_demo_songs: this.load_demo_songs,
load_demo_songs: this.load_demo_songs,
};
}
saveApplicationToLocalStorage(
universes: Universes,
settings: Settings,
settings: Settings
): void {
/**
* Main method to store the application to local storage.
@ -230,7 +232,7 @@ export class AppSettings {
this.midi_clock_input = settings.midi_clock_input;
this.midi_clock_ppqn = settings.midi_clock_ppqn;
this.default_midi_input = settings.default_midi_input;
// this.load_demo_songs = settings.load_demo_songs;
this.load_demo_songs = settings.load_demo_songs;
localStorage.setItem("topos", JSON.stringify(this.data));
}
}
@ -243,13 +245,13 @@ export const initializeSelectedUniverse = (app: Editor): void => {
* @param app - The main application
* @returns void
*/
// if (app.settings.load_demo_songs) {
// let random_example = examples[Math.floor(Math.random() * examples.length)];
// app.selected_universe = "Demo";
// app.universes[app.selected_universe] = structuredClone(template_universe);
// app.universes[app.selected_universe].global.committed = random_example;
// app.universes[app.selected_universe].global.candidate = random_example;
// } else {
if (app.settings.load_demo_songs) {
let random_example = examples[Math.floor(Math.random() * examples.length)];
app.selected_universe = "Demo";
app.universes[app.selected_universe] = structuredClone(template_universe);
app.universes[app.selected_universe].global.committed = random_example;
app.universes[app.selected_universe].global.candidate = random_example;
} else {
try {
app.selected_universe = app.settings.selected_universe;
if (app.universes[app.selected_universe] === undefined)
@ -260,9 +262,8 @@ export const initializeSelectedUniverse = (app: Editor): void => {
app.selected_universe = app.settings.selected_universe;
app.universes[app.selected_universe] = structuredClone(template_universe);
}
(
app.interface.universe_viewer as HTMLInputElement
).placeholder! = `${app.selected_universe}`;
}
(app.interface.universe_viewer as HTMLInputElement).placeholder! = `${app.selected_universe}`;
};
export const emptyUrl = () => {
@ -270,11 +271,6 @@ export const emptyUrl = () => {
};
export const share = async (app: Editor) => {
/**
* Shares the current state of the app by generating a URL with encoded data and copying it to the clipboard.
* @param app - The Editor instance representing the app.
* @returns A Promise that resolves to void.
*/
async function bufferToBase64(buffer: Uint8Array) {
const base64url: string = await new Promise((r) => {
const reader = new FileReader();
@ -325,20 +321,8 @@ export const loadUniverserFromUrl = (app: Editor): void => {
export const loadUniverse = (
app: Editor,
universeName: string,
universe: Universe = template_universe,
universe: Universe = template_universe
): void => {
/**
* Loads a universe into the application.
* If the universe does not exist, a fresh clone of the template universe is created and added to the application.
* The references to the selected universe are updated in the application settings.
* The editor view is updated to reflect the selected universe.
* The initialization script for the selected universe is evaluated.
*
* @param app - The Editor application instance.
* @param universeName - The name of the universe to load.
* @param universe - The template universe to clone if the specified universe does not exist.
*/
let selectedUniverse = universeName.trim();
if (app.universes[selectedUniverse] === undefined) {
// Pushing a freshly cloned template universe to:
@ -350,21 +334,15 @@ export const loadUniverse = (
// Updating references to the currently selected universe
app.settings.selected_universe = selectedUniverse;
app.selected_universe = selectedUniverse;
(
app.interface.universe_viewer as HTMLInputElement
).placeholder! = `${selectedUniverse}`;
(app.interface.universe_viewer as HTMLInputElement).placeholder! = `${selectedUniverse}`;
// Updating the editor View to reflect the selected universe
app.updateEditorView();
// Evaluating the initialisation script for the selected universe
tryEvaluate(app, app.universes[app.selected_universe.toString()]!.init);
tryEvaluate(app, app.universes[app.selected_universe.toString()].init);
};
export const openUniverseModal = (): void => {
/**
* Opens the universe modal.
* If the modal is hidden, it unhides it and hides the editor.
* If the modal is already visible, it closes the modal.
*/
// If the modal is hidden, unhide it and hide the editor
if (
document.getElementById("modal-buffers")!.classList.contains("invisible")
) {
@ -377,9 +355,6 @@ export const openUniverseModal = (): void => {
};
export const closeUniverseModal = (): void => {
/**
* Closes the universe modal and performs necessary actions.
*/
// @ts-ignore
document.getElementById("buffer-search")!.value = "";
document.getElementById("editor")!.classList.remove("invisible");
@ -387,9 +362,6 @@ export const closeUniverseModal = (): void => {
};
export const openSettingsModal = (): void => {
/**
* Opens the settings modal.
*/
if (
document.getElementById("modal-settings")!.classList.contains("invisible")
) {
@ -401,9 +373,6 @@ export const openSettingsModal = (): void => {
};
export const closeSettingsModal = (): void => {
/**
* Closes the settings modal and performs necessary actions.
*/
document.getElementById("editor")!.classList.remove("invisible");
document.getElementById("modal-settings")!.classList.add("invisible");
};

View File

@ -1,6 +1,5 @@
import { UserAPI } from "../API/API";
import { MidiEvent } from "../Classes/MidiEvent";
import { AppSettings } from "../Editor/FileManagement";
import { UserAPI } from "../API";
import { AppSettings } from "../FileManagement";
export type MidiNoteEvent = {
note: number;
@ -65,7 +64,7 @@ export class MidiConnection {
constructor(api: UserAPI, settings: AppSettings) {
this.api = api;
this.settings = settings;
this.lastBPM = api.app.clock.bpm;
this.lastBPM = api.tempo();
this.roundedBPM = this.lastBPM;
this.initializeMidiAccess();
}
@ -105,13 +104,7 @@ export class MidiConnection {
this.currentOutputIndex >= 0 &&
this.currentOutputIndex < this.midiOutputs.length
) {
const output = this.midiOutputs[this.currentOutputIndex];
if (output) {
return output.name;
} else {
console.error("MIDI output is undefined.");
return null;
}
return this.midiOutputs[this.currentOutputIndex].name;
} else {
console.error("No MIDI output selected or available.");
return null;
@ -182,10 +175,10 @@ export class MidiConnection {
*/
if (this.midiInputs.length > 0) {
const midiClockSelect = document.getElementById(
"midi-clock-input",
"midi-clock-input"
) as HTMLSelectElement;
const midiInputSelect = document.getElementById(
"default-midi-input",
"default-midi-input"
) as HTMLSelectElement;
midiClockSelect.innerHTML = "";
@ -214,7 +207,7 @@ export class MidiConnection {
if (this.settings.midi_clock_input) {
const clockMidiInputIndex = this.getMidiInputIndex(
this.settings.midi_clock_input,
this.settings.midi_clock_input
);
midiClockSelect.value = clockMidiInputIndex.toString();
if (clockMidiInputIndex > 0) {
@ -227,7 +220,7 @@ export class MidiConnection {
if (this.settings.default_midi_input) {
const defaultMidiInputIndex = this.getMidiInputIndex(
this.settings.default_midi_input,
this.settings.default_midi_input
);
midiInputSelect.value = defaultMidiInputIndex.toString();
if (defaultMidiInputIndex > 0) {
@ -260,7 +253,7 @@ export class MidiConnection {
this.midiClockInput = this.midiInputs[clockInputIndex];
this.registerMidiInputListener(clockInputIndex);
this.settings.midi_clock_input =
this.midiClockInput?.name ?? undefined;
this.midiClockInput.name || undefined;
}
});
@ -284,7 +277,7 @@ export class MidiConnection {
this.currentInputIndex = parseInt(value);
this.registerMidiInputListener(this.currentInputIndex);
this.settings.default_midi_input =
this.midiInputs[this.currentInputIndex]?.name || undefined;
this.midiInputs[this.currentInputIndex].name || undefined;
}
});
}
@ -298,37 +291,36 @@ export class MidiConnection {
const input = this.midiInputs[inputIndex];
if (input && !input.onmidimessage) {
input.onmidimessage = (event: Event) => {
// @ts-ignore
const message: MidiEvent = event as MIDIMessageEvent;
const message = event as MIDIMessageEvent;
/* MIDI CLOCK */
if (input.name === this.settings.midi_clock_input) {
if (message['data'][0] === 0xf8) {
if (message.data[0] === 0xf8) {
if (this.skipOnError > 0) {
this.skipOnError -= 1;
} else {
this.onMidiClock(event.timeStamp);
}
} else if (message["data"]![0] === 0xfa) {
} else if (message.data[0] === 0xfa) {
console.log("MIDI start received");
this.api.stop();
this.api.play();
} else if (message["data"]![0] === 0xfc) {
} else if (message.data[0] === 0xfc) {
console.log("MIDI stop received");
this.api.pause();
} else if (message["data"]![0] === 0xfb) {
} else if (message.data[0] === 0xfb) {
console.log("MIDI continue received");
this.api.play();
} else if (message["data"]![0] === 0xfe) {
} else if (message.data[0] === 0xfe) {
console.log("MIDI active sensing received");
}
}
/* DEFAULT MIDI INPUT */
if (input.name === this.settings.default_midi_input) {
// If message is one of note ons
if (message["data"][0] >= 0x90 && message["data"]![0] <= 0x9f) {
const channel = message["data"]![0] - 0x90 + 1;
const note = message["data"]![1];
const velocity = message["data"]![2];
if (message.data[0] >= 0x90 && message.data[0] <= 0x9f) {
const channel = message.data[0] - 0x90 + 1;
const note = message.data[1];
const velocity = message.data[2];
this.lastNote = {
note,
@ -369,24 +361,24 @@ export class MidiConnection {
}
// If note off
if (message["data"]![0] >= 0x80 && message["data"]![0] <= 0x8f) {
const channel = message["data"]![0] - 0x80 + 1;
const note = message["data"]![1];
if (message.data[0] >= 0x80 && message.data[0] <= 0x8f) {
const channel = message.data[0] - 0x80 + 1;
const note = message.data[1];
this.removeFromActiveNotes(note, channel);
}
// If message is one of CCs
if (message["data"]![0] >= 0xb0 && message["data"]![0] <= 0xbf) {
const channel = message["data"]![0] - 0xb0 + 1;
const control = message["data"]![1];
const value = message["data"]![2];
if (message.data[0] >= 0xb0 && message.data[0] <= 0xbf) {
const channel = message.data[0] - 0xb0 + 1;
const control = message.data[1];
const value = message.data[2];
this.lastCC[control] = value;
if (this.lastCCInChannel[channel]) {
this.lastCCInChannel[channel]![control] = value;
this.lastCCInChannel[channel][control] = value;
} else {
this.lastCCInChannel[channel] = {};
this.lastCCInChannel[channel]![control] = value;
this.lastCCInChannel[channel][control] = value;
}
//console.log(`CC: ${control} VALUE: ${value} CHANNEL: ${channel}`);
@ -408,14 +400,14 @@ export class MidiConnection {
public removeFromActiveNotes(note: number, channel: number): void {
const index = this.activeNotes.findIndex(
(e) => e.note === note && e.channel === channel,
(e) => e.note === note && e.channel === channel
);
if (index >= 0) this.activeNotes.splice(index, 1);
}
public removeFromStickyNotes(note: number, channel: number): boolean {
const index = this.stickyNotes.findIndex(
(e) => e.note === note && e.channel === channel,
(e) => e.note === note && e.channel === channel
);
if (index >= 0) {
this.stickyNotes.splice(index, 1);
@ -587,7 +579,7 @@ export class MidiConnection {
if (output < 0 || output >= this.midiOutputs.length) {
console.error(
`Invalid MIDI output index. Index must be in the range 0-${this.midiOutputs.length - 1
}.`,
}.`
);
return this.currentOutputIndex;
} else {
@ -616,7 +608,7 @@ export class MidiConnection {
if (input < 0 || input >= this.midiInputs.length) {
console.error(
`Invalid MIDI input index. Index must be in the range 0-${this.midiInputs.length - 1
}.`,
}.`
);
return -1;
} else {
@ -650,7 +642,7 @@ export class MidiConnection {
velocity: number,
duration: number,
port: number | string = this.currentOutputIndex,
bend: number | undefined = undefined,
bend: number | undefined = undefined
): void {
/**
* Sending a MIDI Note on/off message with the same note number and channel. Automatically manages
@ -676,14 +668,11 @@ export class MidiConnection {
if (bend) this.sendPitchBend(bend, channel, port);
// Schedule Note Off
const timeoutId = setTimeout(
() => {
const timeoutId = setTimeout(() => {
output.send(noteOffMessage);
if (bend) this.sendPitchBend(8192, channel, port);
delete this.scheduledNotes[noteNumber];
},
(duration - 0.02) * 1000,
);
}, (duration - 0.02) * 1000);
// @ts-ignore
this.scheduledNotes[noteNumber] = timeoutId;
@ -696,7 +685,7 @@ export class MidiConnection {
note: number,
channel: number,
velocity: number,
port: number | string = this.currentOutputIndex,
port: number | string = this.currentOutputIndex
) {
/**
* Sending Midi Note on message
@ -715,7 +704,7 @@ export class MidiConnection {
sendMidiOff(
note: number,
channel: number,
port: number | string = this.currentOutputIndex,
port: number | string = this.currentOutputIndex
) {
/**
* Sending Midi Note off message
@ -733,7 +722,7 @@ export class MidiConnection {
sendAllNotesOff(
channel: number,
port: number | string = this.currentOutputIndex,
port: number | string = this.currentOutputIndex
) {
/**
* Sending Midi Note off message
@ -750,7 +739,7 @@ export class MidiConnection {
sendAllSoundOff(
channel: number,
port: number | string = this.currentOutputIndex,
port: number | string = this.currentOutputIndex
) {
/**
* Sending all sound off
@ -786,7 +775,7 @@ export class MidiConnection {
public sendPitchBend(
value: number,
channel: number,
port: number | string = this.currentOutputIndex,
port: number | string = this.currentOutputIndex
): void {
/**
* Sends a MIDI Pitch Bend message to the currently selected MIDI output.
@ -797,7 +786,7 @@ export class MidiConnection {
*/
if (value < 0 || value > 16383) {
console.error(
"Invalid pitch bend value. Value must be in the range 0-16383.",
"Invalid pitch bend value. Value must be in the range 0-16383."
);
}
if (channel < 0 || channel > 15) {
@ -836,7 +825,7 @@ export class MidiConnection {
public sendMidiControlChange(
controlNumber: number,
value: number,
channel: number,
channel: number
): void {
/**
* Sends a MIDI Control Change message to the currently selected MIDI output.

View File

@ -1,73 +0,0 @@
export interface OSCMessage {
address: string;
port: number;
args: object;
timetag: number;
}
// Send/receive messages from websocket
export let outputSocket = new WebSocket("ws://localhost:3000");
export let inputSocket = new WebSocket("ws://localhost:3001");
outputSocket.onerror = (error: Event) => {
console.log("[Topos] Failed to connect to OSC daemon:", error.type);
console.log("[Topos] Note: the daemon must be started before Topos");
};
inputSocket.onerror = (error: Event) => {
console.log("[Topos] Failed to connect to OSC daemon:", error.type);
console.log("[Topos] Note: the daemon must be started before Topos");
};
export let oscMessages: any[] = [];
inputSocket.addEventListener("message", (event) => {
let data = JSON.parse(event.data);
if (oscMessages.length >= 1000) {
oscMessages.shift();
}
oscMessages.push(data);
});
// @ts-ignore
outputSocket.onopen = function(event) {
console.log("Connected to WebSocket Server");
// Send an OSC-like message
outputSocket.send(
JSON.stringify({
address: "/successful_connexion",
port: 3000,
args: {},
}),
);
outputSocket.onerror = function(error) {
console.log("Websocket Error:", error);
};
outputSocket.onmessage = function(event) {
console.log("Received: ", event.data);
};
};
export function sendToServer(message: OSCMessage) {
if (outputSocket.readyState === WebSocket.OPEN) {
outputSocket.send(JSON.stringify(message));
} else {
console.log("WebSocket is not open. Attempting to reconnect...");
if (
outputSocket.readyState === WebSocket.CONNECTING ||
outputSocket.readyState === WebSocket.OPEN
) {
outputSocket.close();
}
// Create a new WebSocket connection
outputSocket = new WebSocket("ws://localhost:3000");
// Send the message once the socket is open
outputSocket.onopen = () => {
outputSocket.send(JSON.stringify(message));
};
}
}

0
src/IO/OSCConnection.ts Normal file
View File

View File

@ -1,158 +0,0 @@
/**
* This code is taken from https://github.com/tidalcycles/strudel/pull/839. The logic is written by
* daslyfe (Jade Rose Rowland). I have tweaked it a bit to fit the needs of this project (TypeScript),
* etc... Many thanks for this piece of code! This code is initially part of the Strudel project:
* https://github.com/tidalcycles/strudel.
*/
// @ts-ignore
import { registerSound, onTriggerSample } from "superdough";
export const isAudioFile = (filename: string) => {
const extension = filename.split('.').slice(-1)[0];
return extension !== undefined && ['wav', 'mp3'].includes(extension);
};
interface samplesDBConfig {
dbName: string,
table: string,
columns: string[],
version: number
}
export const samplesDBConfig = {
dbName: 'samples',
table: 'usersamples',
columns: ['data_url', 'title'],
version: 1
}
async function bufferToDataUrl(buf: Buffer) {
return new Promise((resolve) => {
var blob = new Blob([buf], { type: 'application/octet-binary' });
var reader = new FileReader();
reader.onload = function(event: Event) {
// @ts-ignore
resolve(event.target.result);
};
reader.readAsDataURL(blob);
});
}
const processFilesForIDB = async (files: FileList) => {
return await Promise.all(
Array.from(files)
.map(async (s: File) => {
const title = s.name;
if (!isAudioFile(title)) {
return;
}
//create obscured url to file system that can be fetched
const sUrl = URL.createObjectURL(s);
//fetch the sound and turn it into a buffer array
const buf = await fetch(sUrl).then((res) => res.arrayBuffer());
//create a url blob containing all of the buffer data
// @ts-ignore
// TODO: conversion to do here, remove ts-ignore
const base64 = await bufferToDataUrl(buf);
return {
title,
blob: base64,
id: s.webkitRelativePath,
};
})
.filter(Boolean),
).catch((error) => {
console.log('Something went wrong while processing uploaded files', error);
});
};
export const registerSamplesFromDB = (config: samplesDBConfig, onComplete = () => { }) => {
openDB(config, (objectStore: IDBObjectStore) => {
let query = objectStore.getAll();
query.onsuccess = (event: Event) => {
// @ts-ignore
const soundFiles = event.target.result;
if (!soundFiles?.length) {
return;
}
const sounds = new Map();
[...soundFiles]
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
.forEach((soundFile) => {
const title = soundFile.title;
if (!isAudioFile(title)) {
return;
}
const splitRelativePath = soundFile.id?.split('/');
const parentDirectory = splitRelativePath[splitRelativePath.length - 2];
const soundPath = soundFile.blob;
const soundPaths = sounds.get(parentDirectory) ?? new Set();
soundPaths.add(soundPath);
sounds.set(parentDirectory, soundPaths);
});
sounds.forEach((soundPaths, key) => {
const value = Array.from(soundPaths);
// @ts-ignore
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
samples: value,
baseUrl: undefined,
prebake: false,
tag: "user",
});
});
onComplete();
};
});
};
export const openDB = (config: samplesDBConfig, onOpened: Function) => {
const { dbName, version, table, columns } = config
if (!('indexedDB' in window)) {
console.log('This browser doesn\'t support IndexedDB')
return
}
const dbOpen = indexedDB.open(dbName, version);
dbOpen.onupgradeneeded = (_event) => {
const db = dbOpen.result;
const objectStore = db.createObjectStore(table, { keyPath: 'id', autoIncrement: false });
columns.forEach((c: any) => {
objectStore.createIndex(c, c, { unique: false });
});
};
dbOpen.onerror = function(err: Event) {
console.log('Error opening DB: ', (err.target as IDBOpenDBRequest).error);
}
dbOpen.onsuccess = function(_event: Event) {
const db = dbOpen.result;
db.onversionchange = function() {
db.close();
alert("Database is outdated, please reload the page.")
};
const writeTransaction = db.transaction([table], 'readwrite'),
objectStore = writeTransaction.objectStore(table);
// Writing in the database here!
onOpened(objectStore)
}
}
export const uploadSamplesToDB = async (config: samplesDBConfig, files: FileList) => {
await processFilesForIDB(files).then((files) => {
const onOpened = (objectStore: IDBObjectStore, _db: IDBDatabase) => {
// @ts-ignore
files.forEach((file: File) => {
if (file == null) {
return;
}
objectStore.put(file);
});
};
openDB(config, onOpened);
});
};

View File

@ -1,14 +1,12 @@
import { EditorView } from "codemirror";
import { vim } from "@replit/codemirror-vim";
import { type Editor } from "../main";
import colors from "../Editor/colors.json";
import { type Editor } from "./main";
import {
documentation_factory,
documentation_pages,
hideDocumentation,
showDocumentation,
updateDocumentationContent,
} from "../Docs/Documentation";
} from "./Documentation";
import {
type Universe,
template_universe,
@ -18,17 +16,24 @@ import {
share,
closeUniverseModal,
openUniverseModal,
} from "../Editor/FileManagement";
import { loadSamples } from "../API/API";
import { tryEvaluate } from "../Evaluator";
import { inlineHoveringTips } from "../Docs/inlineHelp";
} from "./FileManagement";
import { loadSamples } from "./API";
import { tryEvaluate } from "./Evaluator";
import { inlineHoveringTips } from "./documentation/inlineHelp";
import { lineNumbers } from "@codemirror/view";
import { jsCompletions } from "../Editor/EditorSetup";
import { jsCompletions } from "./EditorSetup";
import { createDocumentationStyle } from "./DomElements";
import { saveState } from "./WindowBehavior";
import { registerSamplesFromDB, samplesDBConfig, uploadSamplesToDB } from "../IO/SampleLoading";
export const installInterfaceLogic = (app: Editor) => {
// Initialize style
const documentationStyle = createDocumentationStyle(app);
const bindings = Object.keys(documentationStyle).map((key) => ({
type: "output",
regex: new RegExp(`<${key}([^>]*)>`, "g"),
//@ts-ignore
replace: (match, p1) => `<${key} class="${documentationStyle[key]}" ${p1}>`,
}));
(app.interface.line_numbers_checkbox as HTMLInputElement).checked =
app.settings.line_numbers;
@ -44,50 +49,59 @@ export const installInterfaceLogic = (app: Editor) => {
app.settings.midi_channels_scripts;
(app.interface.midi_clock_ppqn as HTMLInputElement).value =
app.settings.midi_clock_ppqn.toString();
// (app.interface.load_demo_songs as HTMLInputElement).checked =
// app.settings.load_demo_songs;
(app.interface.load_demo_songs as HTMLInputElement).checked =
app.settings.load_demo_songs;
const tabs = document.querySelectorAll('[id^="tab-"]');
// Iterate over the tabs with an index
for (let i = 0; i < tabs.length; i++) {
tabs[i]!.addEventListener("click", (event) => {
tabs[i].addEventListener("click", (event) => {
// Updating the CSS accordingly
tabs[i]!.classList.add("bg-foreground");
tabs[i]!.classList.add("text-selection_foreground");
tabs[i].classList.add("bg-orange-300");
for (let j = 0; j < tabs.length; j++) {
if (j != i) tabs[j]!.classList.remove("bg-foreground");
if (j != i) tabs[j]!.classList.remove("text-selection_foreground");
if (j != i) tabs[j].classList.remove("bg-orange-300");
}
app.currentFile().candidate = app.view.state.doc.toString();
let tab = event.target as HTMLElement;
let tab_id = tab.id.split("-")[1];
app.local_index = parseInt(tab_id!);
app.local_index = parseInt(tab_id);
app.updateEditorView();
});
}
app.interface['logo'].addEventListener("click", () => {
app.interface.topos_logo.addEventListener("click", () => {
hideDocumentation();
app.updateKnownUniversesView();
openUniverseModal();
});
app.interface['play_button'].addEventListener("click", () => {
app.buttonElements.play_buttons.forEach((button) => {
button.addEventListener("click", () => {
if (app.isPlaying) {
app.setButtonHighlighting("pause", true);
app.isPlaying = !app.isPlaying;
app.clock.pause();
app.api.MidiConnection.sendStopMessage();
} else {
app.clock.resume()
app.setButtonHighlighting("play", true);
app.isPlaying = !app.isPlaying;
app.clock.start();
app.api.MidiConnection.sendStartMessage();
}
updatePlayButton(app);
});
});
app.interface['stop_button'].addEventListener("click", () => {
app.isPlaying = false;
app.clock.stop();
updatePlayButton(app);
app.buttonElements.clear_buttons.forEach((button) => {
button.addEventListener("click", () => {
app.setButtonHighlighting("clear", true);
if (confirm("Do you want to reset the current universe?")) {
app.universes[app.selected_universe] =
structuredClone(template_universe);
app.updateEditorView();
}
});
});
app.interface.documentation_button.addEventListener("click", () => {
showDocumentation(app);
@ -104,52 +118,35 @@ export const installInterfaceLogic = (app: Editor) => {
app.interface.universe_viewer.addEventListener("keydown", (event: any) => {
if (event.key === "Enter") {
let content = (
app.interface.universe_viewer as HTMLInputElement
).value.trim();
let content = (app.interface.universe_viewer as HTMLInputElement).value.trim();
if (content.length > 2 && content.length < 40) {
if (content !== app.selected_universe) {
Object.defineProperty(
app.universes,
content,
Object.defineProperty(app.universes, content,
// @ts-ignore
Object.getOwnPropertyDescriptor(
app.universes,
app.selected_universe,
),
);
Object.getOwnPropertyDescriptor(app.universes, app.selected_universe));
delete app.universes[app.selected_universe];
}
app.selected_universe = content;
loadUniverse(app, app.selected_universe);
(app.interface.universe_viewer as HTMLInputElement).placeholder =
content;
(app.interface.universe_viewer as HTMLInputElement).value = "";
(app.interface.universe_viewer as HTMLInputElement).placeholder = content;
(app.interface.universe_viewer as HTMLInputElement).value = '';
}
}
});
app.interface.audio_nudge_range.addEventListener("input", () => {
app.clock.nudge = parseInt(
(app.interface.audio_nudge_range as HTMLInputElement).value
);
});
app.interface.dough_nudge_range.addEventListener("input", () => {
app.dough_nudge = parseInt(
(app.interface.dough_nudge_range as HTMLInputElement).value,
(app.interface.dough_nudge_range as HTMLInputElement).value
);
});
app.interface.upload_samples_button.addEventListener("input", async (event: Event) => {
let fileInput = event.target as HTMLInputElement;
if (!fileInput.files?.length) {
return;
}
app.interface.sample_indicator.innerText = "Loading...";
app.interface.sample_indicator.classList.add("animate-pulse");
await uploadSamplesToDB(samplesDBConfig, fileInput.files).then(() => {
registerSamplesFromDB(samplesDBConfig, () => {
app.interface.sample_indicator.innerText = "Import samples";
app.interface.sample_indicator.classList.remove("animate-pulse");
});
});
});
app.interface.upload_universe_button.addEventListener("click", () => {
const fileInput = document.createElement("input");
fileInput.type = "file";
@ -221,17 +218,25 @@ export const installInterfaceLogic = (app: Editor) => {
app.flashBackground("#404040", 200);
});
app.buttonElements.stop_buttons.forEach((button) => {
button.addEventListener("click", () => {
app.setButtonHighlighting("stop", true);
app.isPlaying = false;
app.clock.stop();
});
});
app.interface.local_button.addEventListener("click", () =>
app.changeModeFromInterface("local"),
app.changeModeFromInterface("local")
);
app.interface.global_button.addEventListener("click", () =>
app.changeModeFromInterface("global"),
app.changeModeFromInterface("global")
);
app.interface.init_button.addEventListener("click", () =>
app.changeModeFromInterface("init"),
app.changeModeFromInterface("init")
);
app.interface.note_button.addEventListener("click", () =>
app.changeModeFromInterface("notes"),
app.changeModeFromInterface("notes")
);
app.interface.font_family_selector.addEventListener("change", () => {
@ -250,7 +255,7 @@ export const installInterfaceLogic = (app: Editor) => {
fontSize: app.settings.font_size + "px",
},
".cm-gutters": { fontSize: app.settings.font_size + "px" },
}),
})
),
});
});
@ -270,51 +275,26 @@ export const installInterfaceLogic = (app: Editor) => {
fontSize: app.settings.font_size + "px",
},
".cm-gutters": { fontSize: app.settings.font_size + "px" },
}),
})
),
});
});
app.interface.theme_selector.addEventListener("change", () => {
app.settings.theme = (app.interface.theme_selector as HTMLSelectElement).value;
app.readTheme(app.settings.theme);
// @ts-ignore
let selected_theme = colors[app.settings.theme as string];
let theme_preview = "";
for (const [key, _] of Object.entries(selected_theme)) {
theme_preview += `<p class="inline text-${key} bg-${key}">█</div>`;
}
});
app.interface.settings_button.addEventListener("click", () => {
// Populate the font selector
const fontFamilySelect = document.getElementById(
"font-family",
"font-family"
) as HTMLSelectElement | null;
if (fontFamilySelect) {
fontFamilySelect.value = app.settings.font;
}
app.interface.theme_selector.innerHTML = "";
let all_themes = Object.keys(colors);
all_themes.sort((a, b) => {
return a.toLowerCase().localeCompare(b.toLowerCase());
});
app.interface.theme_selector.innerHTML = all_themes.map((color) => {
return `<option value="${color}">${color}</option>`
}).join("");
// Set the selected theme in the selector to app.settings.theme
// @ts-ignore
app.interface.theme_selector.value = app.settings.theme;
// @ts-ignore
let selected_theme = colors[app.settings.theme as string];
// Populate the font family selector
const doughNudgeRange = app.interface.dough_nudge_range as HTMLInputElement;
doughNudgeRange.value = app.dough_nudge.toString();
// @ts-ignore
const doughNumber = document.getElementById(
"doughnumber",
"doughnumber"
) as HTMLInputElement;
doughNumber.value = app.dough_nudge.toString();
if (app.settings.font_size === null) {
@ -340,8 +320,8 @@ export const installInterfaceLogic = (app: Editor) => {
midiChannelsScripts.checked = app.settings.midi_channels_scripts;
const midiClockPpqn = app.interface.midi_clock_ppqn as HTMLInputElement;
midiClockPpqn.value = app.settings.midi_clock_ppqn.toString();
// const loadDemoSongs = app.interface.load_demo_songs as HTMLInputElement;
// loadDemoSongs.checked = app.settings.load_demo_songs;
const loadDemoSongs = app.interface.load_demo_songs as HTMLInputElement;
loadDemoSongs.checked = app.settings.load_demo_songs;
const vimModeCheckbox = app.interface.vim_mode_checkbox as HTMLInputElement;
vimModeCheckbox.checked = app.settings.vimMode;
@ -370,7 +350,7 @@ export const installInterfaceLogic = (app: Editor) => {
fontSize: app.settings.font_size + "px",
},
".cm-gutters": { fontSize: app.settings.font_size + "px" },
}),
})
),
});
});
@ -409,6 +389,18 @@ export const installInterfaceLogic = (app: Editor) => {
});
});
app.interface.time_position_checkbox.addEventListener("change", () => {
let timeviewer = document.getElementById("timeviewer") as HTMLElement;
let checked = (app.interface.time_position_checkbox as HTMLInputElement)
.checked
? true
: false;
app.settings.time_position = checked;
checked
? timeviewer.classList.remove("hidden")
: timeviewer.classList.add("hidden");
});
app.interface.tips_checkbox.addEventListener("change", () => {
let checked = (app.interface.tips_checkbox as HTMLInputElement).checked
? true
@ -416,7 +408,7 @@ export const installInterfaceLogic = (app: Editor) => {
app.settings.tips = checked;
app.view.dispatch({
effects: app.hoveringCompartment.reconfigure(
checked ? inlineHoveringTips : [],
checked ? inlineHoveringTips : []
),
});
});
@ -429,7 +421,7 @@ export const installInterfaceLogic = (app: Editor) => {
app.settings.completions = checked;
app.view.dispatch({
effects: app.completionsCompartment.reconfigure(
checked ? jsCompletions : [],
checked ? jsCompletions : []
),
});
});
@ -452,19 +444,19 @@ export const installInterfaceLogic = (app: Editor) => {
app.interface.midi_clock_ppqn.addEventListener("change", () => {
let value = parseInt(
(app.interface.midi_clock_ppqn as HTMLInputElement).value,
(app.interface.midi_clock_ppqn as HTMLInputElement).value
);
app.settings.midi_clock_ppqn = value;
});
// app.interface.load_demo_songs.addEventListener("change", () => {
// let checked = (app.interface.load_demo_songs as HTMLInputElement).checked
// ? true
// : false;
// app.settings.load_demo_songs = checked;
// });
app.interface.load_demo_songs.addEventListener("change", () => {
let checked = (app.interface.load_demo_songs as HTMLInputElement).checked
? true
: false;
app.settings.load_demo_songs = checked;
});
app.interface.universe_creator.addEventListener("submit", (event: Event) => {
app.interface.universe_creator.addEventListener("submit", (event) => {
event.preventDefault();
let data = new FormData(app.interface.universe_creator as HTMLFormElement);
@ -483,71 +475,53 @@ export const installInterfaceLogic = (app: Editor) => {
}
});
tryEvaluate(app, app.universes[app.selected_universe.toString()]!.init);
tryEvaluate(app, app.universes[app.selected_universe.toString()].init);
documentation_pages.forEach((e) => {
[
"introduction",
"sampler",
"amplitude",
"audio_basics",
"reverb_delay",
"interface",
"interaction",
"code",
"time",
"linear",
"cyclic",
"longform",
// "sound",
"synths",
"chaining",
"patterns",
"ziffers",
"midi",
"functions",
"lfos",
"probabilities",
"variables",
"synchronisation",
"mouse",
"shortcuts",
"about",
"bonus",
"oscilloscope",
"sample_list",
"loading_samples",
].forEach((e) => {
let name = `docs_` + e;
// Check if the element exists
let element = document.getElementById(name);
if (element) {
element.addEventListener("click", async () => {
// Clear query params & set id as hash paremeter for uri
window.history.replaceState({}, "", window.location.pathname);
window.location.hash = e;
app.docs = documentation_factory(app);
document.getElementById(name)!.addEventListener("click", async () => {
if (name !== "docs_samples") {
app.currentDocumentationPane = e;
if (name !== "docs_sample_list") {
updateDocumentationContent(app, app.bindings);
updateDocumentationContent(app, bindings);
} else {
console.log("Loading samples!");
await loadSamples().then(() => {
updateDocumentationContent(app, app.bindings);
app.docs = documentation_factory(app);
app.currentDocumentationPane = e;
updateDocumentationContent(app, bindings);
});
}
});
} else {
console.log("Could not find element " + name);
}
});
};
export const updatePlayButton = (app: Editor) => {
switch (app.clock.state) {
case 'stopped':
app.interface.play_label.innerText = "Play";
updatePlayPauseIcon(app, "play");
break;
case 'paused':
app.interface.play_label.innerText = "Resume";
updatePlayPauseIcon(app, "play");
break;
case 'running':
app.interface.play_label.innerText = "Pause";
updatePlayPauseIcon(app, "pause");
break;
}
}
export const updatePlayPauseIcon = (app: Editor, state: "play" | "pause"): void => {
const { play_icon, pause_icon } = app.interface;
const isPlayIconHidden = play_icon.classList.contains("hidden");
const isPauseIconHidden = pause_icon.classList.contains("hidden");
if (state === "play" && isPlayIconHidden) {
play_icon.classList.remove("hidden");
pause_icon.classList.add("hidden");
} else if (state === "pause" && isPauseIconHidden) {
play_icon.classList.add("hidden");
pause_icon.classList.remove("hidden");
}
}
export const resetTransportView = (app: Editor) => {
requestAnimationFrame(() => {
app.interface.transport_viewer.innerHTML = `<span class="text-xl text-neutral">00:00:00</span>`;
});
}

View File

@ -1,9 +1,8 @@
import { type Editor } from "../main";
import { type Editor } from "./main";
import { vim } from "@replit/codemirror-vim";
import { tryEvaluate } from "../Evaluator";
import { hideDocumentation, showDocumentation } from "../Docs/Documentation";
import { openSettingsModal, openUniverseModal } from "../Editor/FileManagement";
import { resetTransportView, updatePlayButton } from "./UILogic";
import { tryEvaluate } from "./Evaluator";
import { hideDocumentation, showDocumentation } from "./Documentation";
import { openSettingsModal, openUniverseModal } from "./FileManagement";
export const registerFillKeys = (app: Editor) => {
document.addEventListener("keydown", (event) => {
@ -27,48 +26,23 @@ export const registerOnKeyDown = (app: Editor) => {
event.preventDefault();
}
if (event.ctrlKey && event.key === "m") {
event.preventDefault();
let topbar = document.getElementById("topbar");
let sidebar = document.getElementById("sidebar");
console.log("oui ok");
if (app.hidden_interface) {
// Sidebar
sidebar?.classList.remove("flex");
sidebar?.classList.remove("flex-col");
sidebar?.classList.add("hidden");
// Topbar
topbar?.classList.add("hidden");
topbar?.classList.remove("flex");
} else {
// Sidebar
sidebar?.classList.remove("hidden");
sidebar?.classList.add("flex");
sidebar?.classList.add("flex-col");
// Topbar
topbar?.classList.remove("hidden");
topbar?.classList.add("flex");
}
app.hidden_interface = !app.hidden_interface;
}
if (event.ctrlKey && event.key === "s") {
event.preventDefault();
app.flashBackground("#404040", 200);
requestAnimationFrame (() => {
updatePlayButton(app);
resetTransportView(app);
});
app.clock.stop()
app.setButtonHighlighting("stop", true);
app.clock.stop();
}
if (event.ctrlKey && event.key === "p") {
event.preventDefault();
app.flashBackground("#404040", 200);
requestAnimationFrame(() => {
updatePlayButton(app);
});
app.clock.resume()
if (app.isPlaying) {
app.isPlaying = false;
app.setButtonHighlighting("pause", true);
app.clock.pause();
} else {
app.isPlaying = true;
app.setButtonHighlighting("play", true);
app.clock.start();
}
}
// Ctrl + Shift + V: Vim Mode
@ -106,18 +80,6 @@ export const registerOnKeyDown = (app: Editor) => {
if (event.key === "Enter" && event.shiftKey && event.ctrlKey) {
event.preventDefault();
app.currentFile().candidate = app.view.state.doc.toString();
app.api.onceEvaluator = true;
app.api.forceEvaluator = true;
tryEvaluate(app, app.currentFile());
app.flashBackground("#404040", 200);
}
// Force eval with clearing cache
if (event.ctrlKey && event.shiftKey && (event.key === "Backspace" || event.key === "Delete")) {
event.preventDefault();
app.api.clearPatternCache();
app.currentFile().candidate = app.view.state.doc.toString();
app.api.forceEvaluator = true;
tryEvaluate(app, app.currentFile());
app.flashBackground("#404040", 200);
}

View File

@ -1,7 +1,3 @@
export function objectWithArraysToArrayOfObjects(
input: Record<string, any>,
arraysToArrays: string[],
): Record<string, any>[] {
/*
* Transforms object with arrays into array of objects
*
@ -10,42 +6,37 @@ export function objectWithArraysToArrayOfObjects(
* @returns {Record<string, any>[]} Array of objects
*
*/
const inputCopy = { ...input };
export function objectWithArraysToArrayOfObjects(input: Record<string, any>, arraysToArrays: string[]): Record<string, any>[] {
arraysToArrays.forEach((k) => {
if (Array.isArray(inputCopy[k]) && !Array.isArray(inputCopy[k][0])) {
inputCopy[k] = [inputCopy[k]];
// Transform single array to array of arrays and keep array of arrays as is
if (Array.isArray(input[k]) && !Array.isArray(input[k][0])) {
input[k] = [input[k]];
}
});
const keys = Object.keys(input);
const keysAndLengths = Object.entries(inputCopy).reduce(
(acc, [key, value]) => {
const length = Array.isArray(value) ? (value as any[]).length : 1;
acc.maxLength = Math.max(acc.maxLength, length);
acc.keys.push(key);
return acc;
},
{ keys: [] as string[], maxLength: 0 },
const maxLength = Math.max(
...keys.map((k) =>
Array.isArray(input[k]) ? (input[k] as any[]).length : 1
)
);
const output: Record<string, any>[] = [];
for (let i = 0; i < keysAndLengths.maxLength; i++) {
for (let i = 0; i < maxLength; i++) {
const event: Record<string, any> = {};
for (const k of keysAndLengths.keys) {
if (Array.isArray(inputCopy[k])) {
event[k] = (inputCopy[k] as any[])[i % (inputCopy[k] as any[]).length];
for (const k of keys) {
if (Array.isArray(input[k])) {
event[k] = (input[k] as any[])[i % (input[k] as any[]).length];
} else {
event[k] = inputCopy[k];
event[k] = input[k];
}
}
output.push(event);
}
return output;
}
};
export function arrayOfObjectsToObjectWithArrays<T extends Record<string, any>>(
array: T[],
mergeObject: Record<string, any> = {},
): Record<string, any> {
/*
* Transforms array of objects into object with arrays
*
@ -54,34 +45,21 @@ export function arrayOfObjectsToObjectWithArrays<T extends Record<string, any>>(
* @returns {object} Merged object with arrays
*
*/
return array.reduce(
(acc, obj) => {
const mergedObj = { ...obj, ...mergeObject };
Object.keys(mergedObj).forEach((key) => {
if (!acc[key]) {
acc[key] = [];
export function arrayOfObjectsToObjectWithArrays<T extends Record<string, any>>(array: T[], mergeObject: Record<string, any> = {}): Record<string, any> {
return array.reduce((acc, obj) => {
Object.keys(mergeObject).forEach((key) => {
obj[key as keyof T] = mergeObject[key];
});
Object.keys(obj).forEach((key) => {
if (!acc[key as keyof T]) {
acc[key as keyof T] = [];
}
acc[key].push(mergedObj[key]);
(acc[key as keyof T] as unknown[]).push(obj[key]);
});
return acc;
},
{} as Record<string, any>,
);
}, {} as Record<keyof T, any[]>);
}
export function maybeAtomic<T>(value: T): T | T[] {
/*
* Returns first value of array if array of length 1, otherwise returns value
* @param {any} value - Value to check
* @returns {any} Value or array
*/
return Array.isArray(value) && value.length === 1 ? value[0] : value;
}
export function filterObject(
obj: Record<string, any>,
filter: string[],
): Record<string, any> {
/*
* Filter certain keys from object
*
@ -90,21 +68,6 @@ export function filterObject(
* @returns {object} Filtered object
*
*/
return Object.fromEntries(
Object.entries(obj).filter(([key]) => filter.includes(key)),
);
export function filterObject(obj: Record<string, any>, filter: string[]): Record<string, any> {
return Object.fromEntries(Object.entries(obj).filter(([key]) => filter.includes(key)));
}
export const maybeToNumber = (something: any): number | any => {
// If something is BigInt
if (typeof something === "bigint") {
return Number(something);
} else {
return something;
}
}
export const GeneratorType = (function*(){yield undefined;}).constructor;
export const GeneratorIteratorType = (function*(){yield undefined;}).prototype.constructor;
export const isGenerator = (v:any) => Object.prototype.toString.call(v) === '[object Generator]';
export const isGeneratorFunction = (v:any) => Object.prototype.toString.call(v) === '[object GeneratorFunction]';

View File

@ -1,5 +1,4 @@
import { type Editor } from "../main";
import { outputSocket, inputSocket } from "../IO/OSC";
import { type Editor } from "./main";
const handleResize = (canvas: HTMLCanvasElement) => {
if (!canvas) return;
@ -27,49 +26,46 @@ export const saveBeforeExit = (app: Editor): null => {
app.currentFile().candidate = app.view.state.doc.toString();
app.currentFile().committed = app.view.state.doc.toString();
app.settings.saveApplicationToLocalStorage(app.universes, app.settings);
// Close the websocket
inputSocket.close();
outputSocket.close();
return null;
};
export const installWindowBehaviors = (
app: Editor,
window: Window,
preventMultipleTabs: boolean = false,
preventMultipleTabs: boolean = false
) => {
window.addEventListener("resize", () =>
handleResize(app.interface.feedback as HTMLCanvasElement),
handleResize(app.interface.scope as HTMLCanvasElement)
);
window.addEventListener("resize", () =>
handleResize(app.interface.scope as HTMLCanvasElement),
handleResize(app.interface.feedback as HTMLCanvasElement)
);
// window.addEventListener("beforeunload", (event) => {
// event.preventDefault();
// saveBeforeExit(app);
// });
window.addEventListener("beforeunload", (event) => {
event.preventDefault();
saveBeforeExit(app);
});
window.addEventListener("visibilitychange", (event) => {
event.preventDefault();
saveState(app);
});
if (preventMultipleTabs) {
localStorage["openpages"] = Date.now();
localStorage.openpages = Date.now();
window.addEventListener(
"storage",
function (e) {
if (e.key == "openpages") {
// Listen if anybody else is opening the same page!
localStorage["page_available"] = Date.now();
localStorage.page_available = Date.now();
}
if (e.key == "page_available") {
document.getElementById("all")!.classList.add("invisible");
alert(
"Topos is already opened in another tab. Close this tab now to prevent data loss.",
"Topos is already opened in another tab. Close this tab now to prevent data loss."
);
}
},
false,
false
);
}
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,46 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="48.000000pt" height="48.000000pt" viewBox="0 0 48.000000 48.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,48.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M163 470 c-13 -5 -23 -12 -23 -15 0 -3 46 -5 102 -5 80 0 99 3 90 12
-15 15 -139 21 -169 8z"/>
<path d="M91 391 c-16 -16 -19 -32 -18 -84 1 -53 -2 -69 -18 -87 -15 -16 -20
-38 -22 -83 0 -33 2 -56 5 -50 9 13 44 13 39 -1 -2 -6 -18 -11 -34 -11 -39 0
-50 -10 -33 -30 8 -10 30 -15 60 -15 26 0 64 -7 83 -15 43 -18 125 -20 164 -3
15 6 57 14 93 17 73 7 87 24 51 60 -12 12 -21 17 -21 13 0 -4 5 -13 12 -20 8
-8 8 -12 1 -12 -18 0 -25 34 -9 45 10 7 12 16 6 25 -5 8 -7 24 -4 35 3 13 -2
28 -15 39 -12 11 -21 27 -21 36 0 9 -5 21 -11 27 -8 8 -8 17 0 31 17 32 13 56
-12 80 -31 29 -63 28 -91 -3 -16 -17 -34 -25 -56 -25 -22 0 -40 8 -56 25 -28
30 -67 32 -93 6z m89 -16 c19 -23 5 -29 -24 -10 -16 10 -33 14 -47 10 -14 -5
-19 -4 -15 4 10 16 72 13 86 -4z m211 -7 c12 -22 11 -22 -8 -5 -25 21 -41 22
-65 0 -22 -19 -36 -10 -18 12 20 24 77 19 91 -7z m-264 -30 c-3 -7 -5 -2 -5
12 0 14 2 19 5 13 2 -7 2 -19 0 -25z m229 -5 c-11 -11 -19 6 -11 24 8 17 8 17
12 0 3 -10 2 -21 -1 -24z m-80 -8 c4 -8 10 -12 15 -9 5 3 9 0 9 -6 0 -14 60
-40 77 -33 7 3 13 -2 13 -11 0 -13 -6 -15 -27 -9 -36 9 -210 9 -245 0 -22 -6
-28 -4 -28 9 0 8 6 14 13 11 6 -2 25 2 40 10 15 8 32 11 37 8 6 -4 7 1 3 11
-4 12 -3 15 5 10 7 -4 15 -1 18 8 8 21 63 21 70 1z m83 -91 c10 -9 -37 -33
-77 -39 -55 -8 -117 2 -155 26 l-30 18 54 4 c56 4 201 -2 208 -9z m-286 -30
c-15 -7 21 -44 42 -44 8 0 15 -4 15 -10 0 -16 -33 -11 -59 9 -25 19 -24 52 1
50 9 0 10 -2 1 -5z m355 -21 c2 -14 -4 -23 -17 -28 -12 -3 -21 -13 -21 -21 0
-19 -16 -18 -24 1 -5 15 -31 16 -53 2 -8 -5 -13 -4 -13 2 0 6 5 12 10 12 6 1
15 3 20 4 6 1 18 3 28 4 25 1 66 37 52 44 -6 3 -5 4 2 3 7 -1 14 -12 16 -23z
m-255 -37 c4 -10 1 -13 -9 -9 -7 3 -14 9 -14 14 0 14 17 10 23 -5z m42 -6 c3
-5 1 -10 -4 -10 -6 0 -11 5 -11 10 0 6 2 10 4 10 3 0 8 -4 11 -10z m35 0 c0
-5 -4 -10 -10 -10 -5 0 -10 5 -10 10 0 6 5 10 10 10 6 0 10 -4 10 -10z m-170
-10 c0 -5 -5 -10 -11 -10 -5 0 -7 5 -4 10 3 6 8 10 11 10 2 0 4 -4 4 -10z
m215 -39 c-6 -5 -25 10 -25 20 0 5 6 4 14 -3 8 -7 12 -15 11 -17z m45 8 c0
-14 -16 -11 -29 5 -10 12 -8 13 8 9 12 -3 21 -9 21 -14z m-220 1 c0 -5 -5 -10
-11 -10 -5 0 -7 5 -4 10 3 6 8 10 11 10 2 0 4 -4 4 -10z m40 0 c0 -5 -4 -10
-10 -10 -5 0 -10 5 -10 10 0 6 5 10 10 10 6 0 10 -4 10 -10z m215 0 c3 -5 2
-10 -4 -10 -5 0 -13 5 -16 10 -3 6 -2 10 4 10 5 0 13 -4 16 -10z m43 -10 c7
-11 10 -20 6 -20 -7 0 -34 27 -34 34 0 13 16 5 28 -14z m-160 -17 c-10 -2 -26
-2 -35 0 -10 3 -2 5 17 5 19 0 27 -2 18 -5z m99 1 c-3 -3 -12 -4 -19 -1 -8 3
-5 6 6 6 11 1 17 -2 13 -5z m40 0 c-3 -3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17
-2 13 -5z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

Some files were not shown because too many files have changed in this diff Show More