Compare commits
113 Commits
system-swi
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
| dea8808721 | |||
| aa2eb0651d | |||
| ec68419775 | |||
| cf702fd9f1 | |||
| 5e565e3a11 | |||
| d9906857d3 | |||
| e592499711 | |||
| c13d1bb072 | |||
| d2f9376197 | |||
| 8940bf3505 | |||
| 9d7fe9e815 | |||
| 59dc42b103 | |||
| 47534a0724 | |||
| a36aa53e04 | |||
| 4db41275b4 | |||
| 8b10af555c | |||
| 2ee66cd9fb | |||
| 694b994227 | |||
| f451b81ea7 | |||
| 70b4ce714c | |||
| 7bf69a1d27 | |||
| 588934d113 | |||
| b4f2ff0fd9 | |||
| 835a30eafb | |||
| d0f62231d8 | |||
| 3314c089ed | |||
| 6dfbdbb6d4 | |||
| e5afc41fd4 | |||
| 85b0306bb6 | |||
| 61051c9e42 | |||
| 2039ee8518 | |||
| b4b507b2d6 | |||
| 3ddcc38f87 | |||
| 96ef7b04ee | |||
| 05692a61fa | |||
| 0e939a81c7 | |||
| 2ee01186f8 | |||
| 385c023446 | |||
| 1a72125a45 | |||
| 4ec0c66484 | |||
| d5d7d5ca7f | |||
| df025751fc | |||
| 0d04fb0ebd | |||
| 4913dde4a1 | |||
| fea2a3eb21 | |||
| b30fd06e7b | |||
| 2d933ae223 | |||
| 95650c5d01 | |||
| 30983147ea | |||
| 3556b180cf | |||
| 98f431f6b2 | |||
| 4421c37527 | |||
| f797434f6a | |||
| 85f0da3652 | |||
| 3afc278926 | |||
| 4e2ec4e08b | |||
| 5fc7ce3c12 | |||
| d4fed334ab | |||
| fd634ee85f | |||
| 6d1624ffd6 | |||
| 8757d7906a | |||
| 30caa07a17 | |||
| 24dabca102 | |||
| 78c0a67a77 | |||
| 36b5a07199 | |||
| 14d5c39fbe | |||
| 6f886ecc10 | |||
| b01449ee60 | |||
| 70cf7f2562 | |||
| d977f2e8f2 | |||
| b47d041a99 | |||
| 678b3305ac | |||
| facb30be3a | |||
| ae96d20b70 | |||
| 83e901491f | |||
| adf343e0bf | |||
| ecd68ae7c6 | |||
| 6f1f879f5e | |||
| b491637794 | |||
| 88ad863664 | |||
| 591332576e | |||
| a1e664eaa3 | |||
| c0cb7887c0 | |||
| 251b7ed277 | |||
| 37f8581b42 | |||
| 6ca338fac4 | |||
| d44d016357 | |||
| 024b083726 | |||
| 4254136584 | |||
| 2606b8f989 | |||
| b8e197d64a | |||
| 620ca7af59 | |||
| 6305e0ce65 | |||
| e557e5565b | |||
| 331ddab544 | |||
| f46565f5c2 | |||
| 8819a159ff | |||
| caabfc2e65 | |||
| 7a5f15b29d | |||
| 20d2e3a176 | |||
| 62ed707c59 | |||
| 0ba7ed2756 | |||
| 9458733492 | |||
| 7086682336 | |||
| 3c602dc63b | |||
| 5456410d08 | |||
| 61fb6365a0 | |||
| 11be35c677 | |||
| 122cd55ea2 | |||
| f9bce56f9e | |||
| ad6f8a5e91 | |||
| b5988d07f6 | |||
| 04142dbbbc |
3
.github/workflows/deploy.yml
vendored
@ -47,9 +47,6 @@ 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:
|
||||
|
||||
53
README.md
@ -19,52 +19,53 @@
|
||||
|
||||
---
|
||||
|
||||
Topos is a web based live coding environment. Topos is capable of many things:
|
||||
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 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/) and
|
||||
custom oscilloscopes, frequency visualizers and image sequencing capabilities
|
||||
- it can be used to sequence other MIDI devices (and soon.. OSC!)
|
||||
- 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)
|
||||
- Topos is also an emulation and personal extension of the [Monome Teletype](https://monome.org/docs/teletype/)
|
||||
|
||||
---
|
||||
|
||||

|
||||

|
||||
|
||||
## Disclaimer
|
||||
|
||||
**Topos** is still a young 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.
|
||||
**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.
|
||||
|
||||
## Installation (for devs and contributors)
|
||||
## Local 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`
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
- `yarn run build`
|
||||
- `yarn run start`
|
||||
|
||||
Always run a build before committing to check for compiler errors. The automatic deployment on the `main` branch will not accept compiler errors!
|
||||
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!
|
||||
|
||||
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:
|
||||
## 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:
|
||||
|
||||
- `yarn tauri build`
|
||||
- `yarn tauri dev`
|
||||
|
||||
The `tauri` version is only here to quickstart future developments but nothing has been done yet.
|
||||
The `tauri` version has never been fleshed out. It's a template for later developments if Topos ever wants to escape from the web :)
|
||||
|
||||
## Docker
|
||||
|
||||
### Run the application
|
||||
To run the **Docker** version, run the following command:
|
||||
|
||||
`docker run -p 8001:80 yassinsiouda/topos:latest`
|
||||
`docker run -p 8001:80 bubobubobubo/topos:latest`
|
||||
|
||||
### Build and run the prod image
|
||||
|
||||
@ -72,8 +73,7 @@ The `tauri` version is only here to quickstart future developments but nothing
|
||||
|
||||
### Build and run the dev image
|
||||
|
||||
**First installation**
|
||||
First you need to map node_modules to your local machine for your ide intellisense to work properly
|
||||
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
|
||||
@ -81,8 +81,21 @@ docker cp topos-dev:/app/node_modules .
|
||||
docker compose --profile dev down
|
||||
```
|
||||
|
||||
**Then**
|
||||
then run the following command:
|
||||
|
||||
```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 :)
|
||||
12
ToposServer/package-lock.json
generated
@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "topos-server",
|
||||
"version": "1.0.0",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "topos-server",
|
||||
"version": "1.0.0",
|
||||
"version": "0.0.1",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"osc": "^2.4.4",
|
||||
"ws": "^8.14.2"
|
||||
"ws": "^8.17.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/binding-mock": {
|
||||
@ -309,9 +309,9 @@
|
||||
"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==",
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
|
||||
@ -10,6 +10,6 @@
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"osc": "^2.4.4",
|
||||
"ws": "^8.14.2"
|
||||
"ws": "^8.17.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 590 KiB |
29
index.html
@ -1,13 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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.">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Topos</title>
|
||||
<meta name="description" content="Topos is a live coding environment inspired by the Monome Teletype.">
|
||||
<meta charset="UTF-8" />
|
||||
<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="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="mask-icon" href="favicon/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
@ -30,6 +31,18 @@
|
||||
transition: background-color 0.05s ease-in-out;
|
||||
}
|
||||
|
||||
.hydracanvas {
|
||||
position: fixed; /* ignore margins */
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%; /* fill screen */
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
overflow-y: hidden;
|
||||
z-index: -5; /* place behind everything else */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fullscreencanvas {
|
||||
position: fixed; /* ignore margins */
|
||||
top: 0px;
|
||||
@ -161,6 +174,7 @@
|
||||
<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>
|
||||
@ -237,6 +251,7 @@
|
||||
<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>
|
||||
@ -353,10 +368,12 @@
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -558,9 +575,9 @@
|
||||
<!-- Here comes the editor itself -->
|
||||
<div id="editor" class="relative flex flex-row h-screen overflow-y-hidden">
|
||||
<canvas id="scope" class="fullscreencanvas"></canvas>
|
||||
<canvas id="hydra-bg" class="fullscreencanvas"></canvas>
|
||||
<canvas id="feedback" class="fullscreencanvas"></canvas>
|
||||
<canvas id="drawings" 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>
|
||||
</div>
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -14,7 +14,7 @@
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.5",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-pwa": "^0.16.7"
|
||||
"vite-plugin-pwa": "^0.17.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.1.9",
|
||||
@ -44,7 +44,7 @@
|
||||
"tone": "^14.8.49",
|
||||
"unique-names-generator": "^4.7.1",
|
||||
"vite-plugin-markdown": "^2.1.0",
|
||||
"zifferjs": "^0.0.55",
|
||||
"zifferjs": "^0.0.62",
|
||||
"zyklus": "^0.1.4",
|
||||
"zzfx": "^1.2.0"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
46
public/favicon/favicon.svg
Normal file
@ -0,0 +1,46 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 603 B After Width: | Height: | Size: 603 B |
BIN
public/favicon/screenshot_miniature.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/favicon/topos_code.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
37
public/manifest.webmanifest
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
913
src/API.ts
@ -1,6 +1,7 @@
|
||||
import { type Editor } from "./main";
|
||||
// Basics
|
||||
import { introduction } from "./documentation/basics/welcome";
|
||||
import { atelier } from "./documentation/basics/atelier";
|
||||
import { loading_samples } from "./documentation/learning/samples/loading_samples";
|
||||
import { amplitude } from "./documentation/learning/audio_engine/amplitude";
|
||||
import { effects } from "./documentation/learning/audio_engine/effects";
|
||||
@ -17,6 +18,7 @@ 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 { visualization } from "./documentation/more/visualization";
|
||||
import { chaining } from "./documentation/patterns/chaining";
|
||||
import { interaction } from "./documentation/basics/interaction";
|
||||
import { time } from "./documentation/learning/time/time";
|
||||
@ -74,6 +76,48 @@ export const makeExampleFactory = (application: Editor): Function => {
|
||||
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.
|
||||
@ -84,6 +128,7 @@ export const documentation_factory = (application: Editor) => {
|
||||
|
||||
return {
|
||||
introduction: introduction(application),
|
||||
atelier: atelier(application),
|
||||
interface: software_interface(application),
|
||||
interaction: interaction(application),
|
||||
code: code(application),
|
||||
@ -117,6 +162,7 @@ export const documentation_factory = (application: Editor) => {
|
||||
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),
|
||||
@ -175,11 +221,14 @@ export const updateDocumentationContent = (app: Editor, bindings: any) => {
|
||||
moreStyling: true,
|
||||
backslashEscapesHTMLTags: true,
|
||||
extensions: [showdownHighlight({
|
||||
pre: true,
|
||||
pre: true,
|
||||
auto_detection: false
|
||||
}), ...bindings],
|
||||
});
|
||||
console.log(app.currentDocumentationPane);
|
||||
|
||||
if (Object.keys(app.docs).length === 0) {
|
||||
app.docs = documentation_factory(app);
|
||||
}
|
||||
|
||||
function _update_and_assign(callback: Function) {
|
||||
const converted_markdown = converter.makeHtml(
|
||||
@ -187,13 +236,13 @@ export const updateDocumentationContent = (app: Editor, bindings: any) => {
|
||||
);
|
||||
callback(converted_markdown)
|
||||
}
|
||||
_update_and_assign((e: string)=> {
|
||||
_update_and_assign((e: string) => {
|
||||
let display_content = e === undefined ? loading_message : e;
|
||||
document.getElementById("documentation-content")!.innerHTML = display_content;
|
||||
document.getElementById("documentation-content")!.innerHTML = display_content;
|
||||
})
|
||||
if (document.getElementById("documentation-content")!.innerHTML.replace(/"/g, "'") == loading_message.replace(/"/g, "'")) {
|
||||
setTimeout(() => {
|
||||
updateDocumentationContent(app, bindings);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ export const createDocumentationStyle = (app: Editor) => {
|
||||
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-white",
|
||||
a: "lg:text-2xl text-base text-brightred",
|
||||
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",
|
||||
|
||||
@ -20,7 +20,7 @@ import {
|
||||
HighlightStyle,
|
||||
} from "@codemirror/language";
|
||||
import { defaultKeymap, historyKeymap, history } from "@codemirror/commands";
|
||||
import { searchKeymap, highlightSelectionMatches } from "@codemirror/search";
|
||||
import { highlightSelectionMatches } from "@codemirror/search";
|
||||
import {
|
||||
autocompletion,
|
||||
closeBrackets,
|
||||
@ -35,112 +35,114 @@ import { inlineHoveringTips } from "./documentation/inlineHelp";
|
||||
import { toposCompletions, soundCompletions } from "./documentation/inlineHelp";
|
||||
import { javascriptLanguage } from "@codemirror/lang-javascript";
|
||||
|
||||
export const getCodeMirrorTheme = (theme: {[key: string]: string}): Extension => {
|
||||
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: background,
|
||||
backgroundColor: "transparent",
|
||||
fontSize: "24px",
|
||||
fontFamily: "IBM Plex Mono",
|
||||
},
|
||||
".cm-content": {
|
||||
caretColor: cursor,
|
||||
fontFamily: "IBM Plex Mono",
|
||||
},
|
||||
".cm-cursor, .cm-dropCursor": {
|
||||
borderLeftColor: cursor,
|
||||
},
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
|
||||
{
|
||||
backgroundColor: brightwhite,
|
||||
border: `1px solid ${brightwhite}`,
|
||||
},
|
||||
".cm-panels": {
|
||||
backgroundColor: selection_background,
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
|
||||
{
|
||||
backgroundColor: brightwhite,
|
||||
border: `1px solid ${brightwhite}`,
|
||||
},
|
||||
".cm-panels": {
|
||||
backgroundColor: selection_background,
|
||||
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-gutters": {
|
||||
//backgroundColor: base00,
|
||||
backgroundColor: "transparent",
|
||||
color: foreground,
|
||||
},
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: selection_background,
|
||||
color: selection_foreground,
|
||||
},
|
||||
|
||||
".cm-foldPlaceholder": {
|
||||
border: "none",
|
||||
color: `${blue}`,
|
||||
},
|
||||
".cm-tooltip": {
|
||||
border: "none",
|
||||
".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,
|
||||
},
|
||||
".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: background,
|
||||
},
|
||||
color: brightwhite,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ dark: true },
|
||||
);
|
||||
|
||||
@ -192,7 +194,7 @@ export const getCodeMirrorTheme = (theme: {[key: string]: string}): Extension =>
|
||||
tag: [t.heading5, t.heading6],
|
||||
color: red,
|
||||
},
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: green},
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: green },
|
||||
{
|
||||
tag: [t.processingInstruction, t.inserted],
|
||||
color: green,
|
||||
@ -201,39 +203,57 @@ export const getCodeMirrorTheme = (theme: {[key: string]: string}): Extension =>
|
||||
tag: [t.contentSeparator],
|
||||
color: green,
|
||||
},
|
||||
{ tag: t.invalid, color: red, borderBottom: `1px dotted ${red}` },
|
||||
{
|
||||
tag: [t.content],
|
||||
color: brightwhite
|
||||
},
|
||||
{
|
||||
tag: t.invalid,
|
||||
color: red,
|
||||
borderBottom: `1px dotted ${red}`
|
||||
},
|
||||
{
|
||||
tag: t.null,
|
||||
color: brightwhite,
|
||||
}
|
||||
]);
|
||||
return [ toposTheme, syntaxHighlighting(toposHighlightStyle),
|
||||
]
|
||||
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)];
|
||||
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({
|
||||
@ -259,7 +279,7 @@ export const editorSetup: Extension = (() => [
|
||||
highlightActiveLine(),
|
||||
highlightSelectionMatches(),
|
||||
keymap.of([
|
||||
...searchKeymap,
|
||||
// ...searchKeymap,
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...historyKeymap,
|
||||
|
||||
@ -81,6 +81,7 @@ export const tryEvaluate = async (
|
||||
);
|
||||
addFunctionToCache(candidateCode, newFunction);
|
||||
} else {
|
||||
application.api.logOnce("Compilation error!");
|
||||
await evaluate(application, code, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// import { tutorial_universe } from "./universes/tutorial";
|
||||
import { gzipSync, decompressSync, strFromU8 } from "fflate";
|
||||
import { examples } from "./examples/excerpts";
|
||||
// import { examples } from "./examples/excerpts";
|
||||
import { type Editor } from "./main";
|
||||
import { uniqueNamesGenerator, colors, animals } from "unique-names-generator";
|
||||
import { tryEvaluate } from "./Evaluator";
|
||||
@ -63,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;
|
||||
@ -150,7 +150,7 @@ 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(
|
||||
@ -174,7 +174,7 @@ 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;
|
||||
}
|
||||
@ -204,7 +204,7 @@ 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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -232,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));
|
||||
}
|
||||
}
|
||||
@ -245,23 +245,22 @@ 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";
|
||||
// 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)
|
||||
app.universes[app.selected_universe] =
|
||||
structuredClone(template_universe);
|
||||
} catch (error) {
|
||||
app.settings.selected_universe = "Welcome";
|
||||
app.selected_universe = app.settings.selected_universe;
|
||||
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)
|
||||
app.universes[app.selected_universe] =
|
||||
structuredClone(template_universe);
|
||||
} catch (error) {
|
||||
app.settings.selected_universe = "Welcome";
|
||||
app.selected_universe = app.settings.selected_universe;
|
||||
app.universes[app.selected_universe] = structuredClone(template_universe);
|
||||
}
|
||||
}
|
||||
(
|
||||
app.interface.universe_viewer as HTMLInputElement
|
||||
|
||||
@ -1,34 +1,34 @@
|
||||
/**
|
||||
* This code is taken from https://github.com/tidalcycles/strudel/pull/839. The logic is written by
|
||||
* 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";
|
||||
import { registerSound, onTriggerSample } from "superdough";
|
||||
|
||||
export const isAudioFile = (filename: string) => ['wav', 'mp3'].includes(filename.split('.').slice(-1)[0]);
|
||||
|
||||
interface samplesDBConfig {
|
||||
dbName: string,
|
||||
table: string,
|
||||
columns: string[],
|
||||
version: number
|
||||
dbName: string,
|
||||
table: string,
|
||||
columns: string[],
|
||||
version: number
|
||||
}
|
||||
|
||||
export const samplesDBConfig = {
|
||||
dbName: 'samples',
|
||||
table: 'usersamples',
|
||||
columns: ['data_url', 'title'],
|
||||
version: 1
|
||||
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) {
|
||||
reader.onload = function(event: Event) {
|
||||
// @ts-ignore
|
||||
resolve(event.target.result);
|
||||
};
|
||||
@ -65,7 +65,7 @@ const processFilesForIDB = async (files: FileList) => {
|
||||
};
|
||||
|
||||
|
||||
export const registerSamplesFromDB = (config: samplesDBConfig, onComplete = () => {}) => {
|
||||
export const registerSamplesFromDB = (config: samplesDBConfig, onComplete = () => { }) => {
|
||||
openDB(config, (objectStore: IDBObjectStore) => {
|
||||
let query = objectStore.getAll();
|
||||
query.onsuccess = (event: Event) => {
|
||||
@ -107,49 +107,49 @@ export const registerSamplesFromDB = (config: samplesDBConfig, onComplete = () =
|
||||
};
|
||||
|
||||
export const openDB = (config: samplesDBConfig, onOpened: Function) => {
|
||||
const { dbName, version, table, columns } = config
|
||||
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);
|
||||
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.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.")
|
||||
};
|
||||
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)
|
||||
}
|
||||
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);
|
||||
// @ts-ignore
|
||||
files.forEach((file: File) => {
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
objectStore.put(file);
|
||||
});
|
||||
};
|
||||
openDB(config, onOpened);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@ -4,6 +4,7 @@ import { type Editor } from "./main";
|
||||
import colors from "./colors.json";
|
||||
import {
|
||||
documentation_factory,
|
||||
documentation_pages,
|
||||
hideDocumentation,
|
||||
showDocumentation,
|
||||
updateDocumentationContent,
|
||||
@ -23,19 +24,11 @@ import { tryEvaluate } from "./Evaluator";
|
||||
import { inlineHoveringTips } from "./documentation/inlineHelp";
|
||||
import { lineNumbers } from "@codemirror/view";
|
||||
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;
|
||||
@ -51,8 +44,8 @@ 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
|
||||
@ -380,8 +373,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;
|
||||
|
||||
@ -509,12 +502,12 @@ export const installInterfaceLogic = (app: Editor) => {
|
||||
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.preventDefault();
|
||||
@ -537,60 +530,24 @@ export const installInterfaceLogic = (app: Editor) => {
|
||||
|
||||
tryEvaluate(app, app.universes[app.selected_universe.toString()].init);
|
||||
|
||||
[
|
||||
"introduction",
|
||||
"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",
|
||||
].forEach((e) => {
|
||||
documentation_pages.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);
|
||||
app.currentDocumentationPane = e;
|
||||
if (name !== "docs_sample_list") {
|
||||
app.currentDocumentationPane = e;
|
||||
updateDocumentationContent(app, bindings);
|
||||
updateDocumentationContent(app, app.bindings);
|
||||
} else {
|
||||
console.log("Loading samples!");
|
||||
await loadSamples().then(() => {
|
||||
app.docs = documentation_factory(app);
|
||||
app.currentDocumentationPane = e;
|
||||
updateDocumentationContent(app, bindings);
|
||||
updateDocumentationContent(app, app.bindings);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -105,6 +105,8 @@ 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);
|
||||
}
|
||||
@ -114,6 +116,7 @@ export const registerOnKeyDown = (app: Editor) => {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -95,6 +95,15 @@ export function filterObject(
|
||||
);
|
||||
}
|
||||
|
||||
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]';
|
||||
|
||||
593
src/Visuals/CanvasVisuals.ts
Normal file
@ -0,0 +1,593 @@
|
||||
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 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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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
|
||||
ctx.moveTo(points[0][0], points[0][1]);
|
||||
for (let point of points) ctx.lineTo(point[0], point[1]);
|
||||
// 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();
|
||||
};
|
||||
BIN
src/assets/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
src/assets/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
src/assets/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
src/assets/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
46
src/assets/favicon.svg
Normal file
@ -0,0 +1,46 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
BIN
src/assets/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
15
src/assets/safari-pinned-tab.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<?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="850.000000pt" height="850.000000pt" viewBox="0 0 850.000000 850.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,850.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M0 4250 l0 -3770 4250 0 4250 0 0 3770 0 3770 -4250 0 -4250 0 0
|
||||
-3770z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 603 B |
BIN
src/assets/topos_code.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 427 KiB After Width: | Height: | Size: 427 KiB |
BIN
src/assets/topos_gif.gif
Normal file
|
After Width: | Height: | Size: 4.9 MiB |
@ -521,4 +521,13 @@ export abstract class AudibleEvent extends AbstractEvent {
|
||||
this.app.api.cue(functionName);
|
||||
return this;
|
||||
}
|
||||
|
||||
runChain = (): this => {
|
||||
// chainAll is defined using all() in the API
|
||||
if("chainAll" in this && typeof this.chainAll === "function") {
|
||||
this.values = this.chainAll().values;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ export class MidiEvent extends AudibleEvent {
|
||||
return this;
|
||||
};
|
||||
|
||||
out = (): void => {
|
||||
out = (outChannel?: number|number[]): void => {
|
||||
function play(event: MidiEvent, params: MidiParams): void {
|
||||
const channel = params.channel ? params.channel : 0;
|
||||
const velocity = params.velocity ? params.velocity : 100;
|
||||
@ -141,6 +141,9 @@ export class MidiEvent extends AudibleEvent {
|
||||
);
|
||||
}
|
||||
|
||||
this.runChain();
|
||||
if(outChannel) this.channel(outChannel);
|
||||
|
||||
const events = objectWithArraysToArrayOfObjects(this.values, [
|
||||
"parsedScale",
|
||||
]) as MidiParams[];
|
||||
|
||||
@ -41,6 +41,19 @@ export class SoundEvent extends AudibleEvent {
|
||||
zrand: ["zrand", "zr"],
|
||||
curve: ["curve"],
|
||||
bank: ["bank"],
|
||||
drumMachine: function(self: SoundEvent, a: number) {
|
||||
let machines = ["AJKPercusyn", "AkaiLinn", "AkaiMPC60", "AkaiXR10", "AlesisHR16", "AlesisSR16", "BossDR110", "BossDR220", "BossDR55",
|
||||
"BossDR550", "BossDR660", "CasioRZ1", "CasioSK1", "CasioVL1", "DoepferMS404", "EmuDrumulator", "EmuModular", "EmuSP12",
|
||||
"KorgDDM110", "KorgKPR77", "KorgKR55", "KorgKRZ", "KorgM1", "KorgMinipops", "KorgPoly800", "KorgT3", "Linn9000",
|
||||
"LinnDrum", "LinnLM1", "LinnLM2", "MFB512", "MPC1000", "MoogConcertMateMG1", "OberheimDMX", "RhodesPolaris",
|
||||
"RhythmAce", "RolandCompurhythm1000", "RolandCompurhythm78", "RolandCompurhythm8000", "RolandD110", "RolandD70", "RolandDDR30",
|
||||
"RolandJD990", "RolandMC202", "RolandMC303", "RolandMT32", "RolandR8", "RolandS50", "RolandSH09", "RolandSystem100", "RolandTR505",
|
||||
"RolandTR606", "RolandTR626", "RolandTR707", "RolandTR727", "RolandTR808", "RolandTR909", "SakataDPM48", "SequentialCircuitsDrumtracks",
|
||||
"SequentialCircuitsTom", "SergeModular", "SimmonsSDS400", "SimmonsSDS5", "SoundmastersR88", "UnivoxMicroRhythmer12", "ViscoSpaceDrum",
|
||||
"XdrumLM8953", "YamahaRM50", "YamahaRX21", "YamahaRX5", "YamahaRY30", "YamahaTG33"];
|
||||
self.updateValue("bank", machines[a % machines.length]);
|
||||
return self;
|
||||
},
|
||||
slide: ["slide", "sld"],
|
||||
deltaSlide: ["deltaSlide", "dslide"],
|
||||
pitchJump: ["pitchJump", "pj"],
|
||||
@ -49,6 +62,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
znoise: ["znoise"],
|
||||
address: ["address", "add"],
|
||||
port: ["port"],
|
||||
density: ["density"],
|
||||
noise: ["noise"],
|
||||
zmod: ["zmod"],
|
||||
zcrush: ["zcrush"],
|
||||
@ -70,7 +84,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
phaserDepth: ["phaserDepth", "phasdepth"],
|
||||
phaserSweep: ["phaserSweep", "phassweep"],
|
||||
phaserCenter: ["phaserCenter", "phascenter"],
|
||||
fmadsr: function (
|
||||
fmadsr: function(
|
||||
self: SoundEvent,
|
||||
a: number,
|
||||
d: number,
|
||||
@ -83,7 +97,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
self.updateValue("fmrelease", r);
|
||||
return self;
|
||||
},
|
||||
fmad: function (self: SoundEvent, a: number, d: number) {
|
||||
fmad: function(self: SoundEvent, a: number, d: number) {
|
||||
self.updateValue("fmattack", a);
|
||||
self.updateValue("fmdecay", d);
|
||||
return self;
|
||||
@ -94,7 +108,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
decay: ["decay", "dec"],
|
||||
sustain: ["sustain", "sus"],
|
||||
release: ["release", "rel"],
|
||||
adsr: function (
|
||||
adsr: function(
|
||||
self: SoundEvent,
|
||||
a: number,
|
||||
d: number,
|
||||
@ -107,18 +121,18 @@ export class SoundEvent extends AudibleEvent {
|
||||
self.updateValue("release", r);
|
||||
return self;
|
||||
},
|
||||
ad: function (self: SoundEvent, a: number, d: number) {
|
||||
ad: function(self: SoundEvent, a: number, d: number) {
|
||||
self.updateValue("attack", a);
|
||||
self.updateValue("decay", d);
|
||||
self.updateValue("sustain", 0.0);
|
||||
self.updateValue("release", 0.0);
|
||||
return self;
|
||||
},
|
||||
scope: function (self: SoundEvent) {
|
||||
scope: function(self: SoundEvent) {
|
||||
self.updateValue("analyze", true);
|
||||
return self;
|
||||
},
|
||||
debug: function (self: SoundEvent, callback?: Function) {
|
||||
debug: function(self: SoundEvent, callback?: Function) {
|
||||
self.updateValue("debug", true);
|
||||
if (callback) {
|
||||
self.updateValue("debugFunction", callback);
|
||||
@ -130,27 +144,33 @@ export class SoundEvent extends AudibleEvent {
|
||||
lpdecay: ["lpdecay", "lpd"],
|
||||
lpsustain: ["lpsustain", "lps"],
|
||||
lprelease: ["lprelease", "lpr"],
|
||||
cutoff: function (self: SoundEvent, value: number, resonance?: number) {
|
||||
cutoff: function(self: SoundEvent, value: number, resonance?: number) {
|
||||
self.updateValue("cutoff", value);
|
||||
if (resonance) {
|
||||
self.updateValue("resonance", resonance);
|
||||
}
|
||||
return self;
|
||||
},
|
||||
lpf: function (self: SoundEvent, value: number, resonance?: number) {
|
||||
lpf: function(self: SoundEvent, value: number, resonance?: number) {
|
||||
self.updateValue("cutoff", value);
|
||||
if (resonance) {
|
||||
self.updateValue("resonance", resonance);
|
||||
}
|
||||
return self;
|
||||
},
|
||||
resonance: function (self: SoundEvent, value: number) {
|
||||
resonance: function(self: SoundEvent, value: number) {
|
||||
if (value >= 0 && value <= 1) {
|
||||
self.updateValue("resonance", 50 * value);
|
||||
}
|
||||
return self;
|
||||
},
|
||||
lpadsr: function (
|
||||
lpq: function(self: SoundEvent, value: number) {
|
||||
if (value >= 0 && value <= 1) {
|
||||
self.updateValue("resonance", 50 * value);
|
||||
}
|
||||
return self;
|
||||
},
|
||||
lpadsr: function(
|
||||
self: SoundEvent,
|
||||
depth: number,
|
||||
a: number,
|
||||
@ -165,7 +185,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
self.updateValue("lprelease", r);
|
||||
return self;
|
||||
},
|
||||
lpad: function (self: SoundEvent, depth: number, a: number, d: number) {
|
||||
lpad: function(self: SoundEvent, depth: number, a: number, d: number) {
|
||||
self.updateValue("lpenv", depth);
|
||||
self.updateValue("lpattack", a);
|
||||
self.updateValue("lpdecay", d);
|
||||
@ -178,25 +198,25 @@ export class SoundEvent extends AudibleEvent {
|
||||
hpdecay: ["hpdecay", "hpd"],
|
||||
hpsustain: ["hpsustain", "hpsus"],
|
||||
hprelease: ["hprelease", "hpr"],
|
||||
hcutoff: function (self: SoundEvent, value: number, resonance?: number) {
|
||||
hcutoff: function(self: SoundEvent, value: number, resonance?: number) {
|
||||
self.updateValue("hcutoff", value);
|
||||
if (resonance) {
|
||||
self.updateValue("hresonance", resonance);
|
||||
}
|
||||
return self;
|
||||
},
|
||||
hpf: function (self: SoundEvent, value: number, resonance?: number) {
|
||||
hpf: function(self: SoundEvent, value: number, resonance?: number) {
|
||||
self.updateValue("hcutoff", value);
|
||||
if (resonance) {
|
||||
self.updateValue("hresonance", resonance);
|
||||
self.updateValue("hresonance", resonance * 50);
|
||||
}
|
||||
return self;
|
||||
},
|
||||
hpq: function (self: SoundEvent, value: number) {
|
||||
self.updateValue("hresonance", value);
|
||||
hpq: function(self: SoundEvent, value: number) {
|
||||
self.updateValue("hresonance", value * 50);
|
||||
return self;
|
||||
},
|
||||
hpadsr: function (
|
||||
hpadsr: function(
|
||||
self: SoundEvent,
|
||||
depth: number,
|
||||
a: number,
|
||||
@ -211,7 +231,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
self.updateValue("hprelease", r);
|
||||
return self;
|
||||
},
|
||||
hpad: function (self: SoundEvent, depth: number, a: number, d: number) {
|
||||
hpad: function(self: SoundEvent, depth: number, a: number, d: number) {
|
||||
self.updateValue("hpenv", depth);
|
||||
self.updateValue("hpattack", a);
|
||||
self.updateValue("hpdecay", d);
|
||||
@ -224,22 +244,25 @@ export class SoundEvent extends AudibleEvent {
|
||||
bpdecay: ["bpdecay", "bpd"],
|
||||
bpsustain: ["bpsustain", "bps"],
|
||||
bprelease: ["bprelease", "bpr"],
|
||||
bandf: function (self: SoundEvent, value: number, resonance?: number) {
|
||||
bandf: function(self: SoundEvent, value: number, resonance?: number) {
|
||||
self.updateValue("bandf", value);
|
||||
if (resonance) {
|
||||
self.updateValue("bandq", resonance);
|
||||
}
|
||||
return self;
|
||||
},
|
||||
bpf: function (self: SoundEvent, value: number, resonance?: number) {
|
||||
bpf: function(self: SoundEvent, value: number, resonance?: number) {
|
||||
self.updateValue("bandf", value);
|
||||
if (resonance) {
|
||||
self.updateValue("bandq", resonance);
|
||||
self.updateValue("bandq", resonance * 50);
|
||||
}
|
||||
return self;
|
||||
},
|
||||
bandq: ["bandq", "bpq"],
|
||||
bpadsr: function (
|
||||
bpq: function(self: SoundEvent, value: number) {
|
||||
self.updateValue("bandq", value * 50);
|
||||
return self;
|
||||
},
|
||||
bpadsr: function(
|
||||
self: SoundEvent,
|
||||
depth: number,
|
||||
a: number,
|
||||
@ -254,7 +277,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
self.updateValue("bprelease", r);
|
||||
return self;
|
||||
},
|
||||
bpad: function (self: SoundEvent, depth: number, a: number, d: number) {
|
||||
bpad: function(self: SoundEvent, depth: number, a: number, d: number) {
|
||||
self.updateValue("bpenv", depth);
|
||||
self.updateValue("bpattack", a);
|
||||
self.updateValue("bpdecay", d);
|
||||
@ -264,7 +287,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
},
|
||||
vib: ["vib"],
|
||||
vibmod: ["vibmod"],
|
||||
fm: function (self: SoundEvent, value: number | string) {
|
||||
fm: function(self: SoundEvent, value: number | string) {
|
||||
if (typeof value === "number") {
|
||||
self.values["fmi"] = value;
|
||||
} else {
|
||||
@ -280,11 +303,11 @@ export class SoundEvent extends AudibleEvent {
|
||||
begin: ["begin"],
|
||||
end: ["end"],
|
||||
gain: ["gain"],
|
||||
dbgain: function (self: SoundEvent, value: number) {
|
||||
dbgain: function(self: SoundEvent, value: number) {
|
||||
self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
|
||||
return self;
|
||||
},
|
||||
db: function (self: SoundEvent, value: number) {
|
||||
db: function(self: SoundEvent, value: number) {
|
||||
self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
|
||||
return self;
|
||||
},
|
||||
@ -307,32 +330,32 @@ export class SoundEvent extends AudibleEvent {
|
||||
roomlp: ["roomlp", "rlp"],
|
||||
roomdim: ["roomdim", "rdim"],
|
||||
sound: ["s", "sound"],
|
||||
size: function (self: SoundEvent, value: number) {
|
||||
size: function(self: SoundEvent, value: number) {
|
||||
self.updateValue("roomsize", value);
|
||||
return self;
|
||||
},
|
||||
sz: function (self: SoundEvent, value: number) {
|
||||
sz: function(self: SoundEvent, value: number) {
|
||||
self.updateValue("roomsize", value);
|
||||
return self;
|
||||
},
|
||||
comp: ["comp","compressor", "cmp"],
|
||||
ratio: function (self: SoundEvent, value: number) {
|
||||
comp: ["comp", "compressor", "cmp"],
|
||||
ratio: function(self: SoundEvent, value: number) {
|
||||
self.updateValue("compressorRatio", value);
|
||||
return self;
|
||||
},
|
||||
knee: function (self: SoundEvent, value: number) {
|
||||
knee: function(self: SoundEvent, value: number) {
|
||||
self.updateValue("compressorKnee", value);
|
||||
return self;
|
||||
},
|
||||
compAttack: function (self: SoundEvent, value: number) {
|
||||
compAttack: function(self: SoundEvent, value: number) {
|
||||
self.updateValue("compressorAttack", value);
|
||||
return self;
|
||||
},
|
||||
compRelease: function (self: SoundEvent, value: number) {
|
||||
compRelease: function(self: SoundEvent, value: number) {
|
||||
self.updateValue("compressorRelease", value);
|
||||
return self;
|
||||
},
|
||||
stretch: function (self: SoundEvent, beat: number) {
|
||||
stretch: function(self: SoundEvent, beat: number) {
|
||||
self.updateValue("unit", "c");
|
||||
self.updateValue("speed", 1 / beat);
|
||||
self.updateValue("cut", beat);
|
||||
@ -404,7 +427,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
(soundEvent.key || "C4"),
|
||||
(soundEvent.originalPitch || soundEvent.pitch || 0),
|
||||
(soundEvent.parsedScale || soundEvent.scale || "MAJOR"),
|
||||
(soundEvent.paramOctave || 0)+(soundEvent.addedOctave || 0)
|
||||
(soundEvent.paramOctave || 0) + (soundEvent.addedOctave || 0)
|
||||
);
|
||||
soundEvent.note = resolvedPitchClass.note;
|
||||
soundEvent.freq = midiToFreq(resolvedPitchClass.note);
|
||||
@ -423,6 +446,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
};
|
||||
|
||||
out = (orbit?: number | number[]): void => {
|
||||
this.runChain();
|
||||
if (orbit) this.values["orbit"] = orbit;
|
||||
const events = objectWithArraysToArrayOfObjects(this.values, [
|
||||
"parsedScale",
|
||||
@ -438,7 +462,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
delete filteredEvent.note;
|
||||
}
|
||||
superdough(
|
||||
filteredEvent,
|
||||
filteredEvent,
|
||||
this.nudge - this.app.clock.deviation,
|
||||
filteredEvent.dur
|
||||
);
|
||||
|
||||
@ -7,6 +7,7 @@ import { MidiEvent, MidiParams } from "./MidiEvent";
|
||||
import { RestEvent } from "./RestEvent";
|
||||
import { arrayOfObjectsToObjectWithArrays, isGenerator } from "../Utils/Generic";
|
||||
import { TonnetzSpaces } from "zifferjs/src/tonnetz";
|
||||
import { safeMod } from "zifferjs/src/utils";
|
||||
|
||||
export type InputOptions = { [key: string]: string | number };
|
||||
|
||||
@ -31,6 +32,7 @@ export class Player extends AbstractEvent {
|
||||
options: InputOptions,
|
||||
public app: Editor,
|
||||
zid: string = "",
|
||||
waitTime: number = 0,
|
||||
) {
|
||||
super(app);
|
||||
this.options = options;
|
||||
@ -46,9 +48,24 @@ export class Player extends AbstractEvent {
|
||||
} else {
|
||||
throw new Error("Invalid input");
|
||||
}
|
||||
if(waitTime) this.waitTime = waitTime;
|
||||
this.zid = zid;
|
||||
}
|
||||
|
||||
updatePattern(input: string, options: InputOptions): boolean {
|
||||
const oldIndex = this.ziffers.index;
|
||||
const newPattern = new Ziffers(input, options);
|
||||
if(newPattern.values.length > 0) {
|
||||
this.ziffers = newPattern;
|
||||
this.ziffers.update();
|
||||
this.ziffers.index = oldIndex;
|
||||
this.input = input;
|
||||
this.options = options;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return this.ziffers.values.length > 0;
|
||||
}
|
||||
@ -321,18 +338,92 @@ export class Player extends AbstractEvent {
|
||||
return this;
|
||||
}
|
||||
|
||||
octaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 4) {
|
||||
if (this.atTheBeginning()) this.ziffers.octaCycle(tonnetz, repeats);
|
||||
octaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 4, components: number = 1) {
|
||||
if (this.atTheBeginning()) this.ziffers.octaCycle(tonnetz, repeats, components);
|
||||
return this;
|
||||
}
|
||||
|
||||
hexaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3) {
|
||||
if (this.atTheBeginning()) this.ziffers.hexaCycle(tonnetz, repeats);
|
||||
hexaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3, components: number = 1) {
|
||||
if (this.atTheBeginning()) this.ziffers.hexaCycle(tonnetz, repeats, components);
|
||||
return this;
|
||||
}
|
||||
|
||||
enneaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3) {
|
||||
if (this.atTheBeginning()) this.ziffers.enneaCycle(tonnetz, repeats);
|
||||
enneaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3, components: number = 1) {
|
||||
if (this.atTheBeginning()) this.ziffers.enneaCycle(tonnetz, repeats, components);
|
||||
return this;
|
||||
}
|
||||
|
||||
cubeDance(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3) {
|
||||
if (this.atTheBeginning()) this.ziffers.cubeDance(tonnetz, repeats);
|
||||
return this;
|
||||
}
|
||||
|
||||
powerTowers(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3) {
|
||||
if (this.atTheBeginning()) this.ziffers.powerTowers(tonnetz, repeats);
|
||||
return this;
|
||||
}
|
||||
powerTower = this.powerTowers;
|
||||
|
||||
octaTower(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3, components: number = 1) {
|
||||
if (this.atTheBeginning()) this.ziffers.octaTower(tonnetz, repeats, components);
|
||||
return this;
|
||||
}
|
||||
octaTowers = this.octaTower;
|
||||
|
||||
boretzRegions(tonnetz: TonnetzSpaces = [3, 4, 5]) {
|
||||
if (this.atTheBeginning()) this.ziffers.boretzRegions(tonnetz);
|
||||
return this;
|
||||
}
|
||||
boretz = this.boretzRegions;
|
||||
|
||||
weitzmannRegions(tonnetz: TonnetzSpaces = [3, 4, 5]) {
|
||||
if (this.atTheBeginning()) this.ziffers.weitzmannRegions(tonnetz);
|
||||
return this;
|
||||
}
|
||||
weitzmann = this.weitzmannRegions;
|
||||
|
||||
shuffle() {
|
||||
if (this.atTheBeginning()) this.ziffers.shuffle();
|
||||
return this;
|
||||
}
|
||||
|
||||
deal(amount: number = this.ziffers.values.length) {
|
||||
if (this.atTheBeginning()) this.ziffers.deal(amount);
|
||||
return this;
|
||||
}
|
||||
|
||||
from(value: number) {
|
||||
if (this.atTheBeginning()) this.ziffers.from(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
to(value: number) {
|
||||
if (this.atTheBeginning()) this.ziffers.to(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
between(value: number, value2: number) {
|
||||
if (this.atTheBeginning()) this.ziffers.between(value, value2+1);
|
||||
return this;
|
||||
}
|
||||
|
||||
at(value: number, ...rest: number[]) {
|
||||
if (this.atTheBeginning()) this.ziffers.at(value, ...rest);
|
||||
return this;
|
||||
}
|
||||
|
||||
keep() {
|
||||
this.ziffers.setRedo(0);
|
||||
return this;
|
||||
}
|
||||
|
||||
repeat(amount: number) {
|
||||
this.ziffers.setRedo(amount < 0 ? 0 : amount);
|
||||
return this;
|
||||
}
|
||||
|
||||
every(amount: number) {
|
||||
if (this.atTheBeginning()) this.ziffers.every(amount);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -366,6 +457,13 @@ export class Player extends AbstractEvent {
|
||||
return this;
|
||||
}
|
||||
|
||||
rotate(amount: number = 1) {
|
||||
if (this.atTheBeginning()) {
|
||||
this.ziffers.rotate(amount+safeMod(this.ziffers.cycleIndex,this.ziffers.evaluated.length));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
listen(value: string) {
|
||||
if(typeof value === "string") {
|
||||
const cueTime = this.app.api.cueTimes[value];
|
||||
@ -408,8 +506,7 @@ export class Player extends AbstractEvent {
|
||||
}
|
||||
|
||||
sync(value: string | Function, manualSync: boolean = true) {
|
||||
|
||||
if(typeof value === "string") {
|
||||
if(typeof value === "string" && manualSync) {
|
||||
if(manualSync) {
|
||||
const cueTime = this.app.api.cueTimes[value];
|
||||
if(cueTime) {
|
||||
@ -420,11 +517,11 @@ export class Player extends AbstractEvent {
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
if (this.atTheBeginning() && this.notStarted()) {
|
||||
const origin = this.app.clock.pulses_since_origin;
|
||||
if (origin > 0) {
|
||||
const syncPattern = this.app.api.patternCache.get(value.name) as Player;
|
||||
const syncName = typeof value === "function" ? value.name : value;
|
||||
const syncPattern = this.app.api.patternCache.get(syncName) as Player;
|
||||
if (syncPattern) {
|
||||
const syncPatternDuration = syncPattern.ziffers.duration;
|
||||
const syncPatternStart = syncPattern.startCallTime;
|
||||
|
||||
14406
src/colors.json
BIN
src/documentation/basics/TOPOS_COMMANDS.pdf
Normal file
193
src/documentation/basics/atelier.ts
Normal file
@ -0,0 +1,193 @@
|
||||
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)}
|
||||
|
||||
|
||||
`
|
||||
};
|
||||
|
||||
|
||||
@ -5,27 +5,27 @@ export const code = (application: Editor): string => {
|
||||
const makeExample = makeExampleFactory(application);
|
||||
return `
|
||||
# Code
|
||||
|
||||
|
||||
Topos scripts are using the [JavaScript](https://en.wikipedia.org/wiki/JavaScript) syntax. This is the language used to write pretty much anything in a web browser. JavaScript is easy to learn, and even faster to learn if you are already familiar with other programming languages. Here are some good resources if you want to learn more about it:
|
||||
|
||||
- [MDN (Mozilla Web Docs)](https://developer.mozilla.org/): it covers pretty much anything and is considered to be a reliable source to learn how the web currently works. Any web developer knows about it.
|
||||
|
||||
|
||||
- [MDN (Mozilla Web Docs)](https://developer.mozilla.org/): it covers pretty much anything and is considered to be a reliable source to learn how the web currently works. Any web developer knows about it.
|
||||
|
||||
- [Learn JS in Y Minutes](https://learnxinyminutes.com/docs/javascript/): a good tour of the language. Can be useful as a refresher.
|
||||
|
||||
|
||||
- [The Modern JavaScript Tutorial](https://javascript.info/): another well known source to learn the language.
|
||||
|
||||
**You do not need to have any prior knowledge of programming** to use Topos**.
|
||||
|
||||
|
||||
**You do not need to have any prior knowledge of programming to use Topos**.
|
||||
|
||||
# How is the code evaluated?
|
||||
|
||||
|
||||
The code you enter in any of the scripts is evaluated in strict mode. This tells your browser that the code you run can be optimized quite agressively. We need this because by default, **the global script is evaluated 48 times per beat**. It also means that you can crash at the speed of light :smile:. There are some things to keep in mind:
|
||||
|
||||
|
||||
- **about variables:** the state of your variables is not kept between iterations. If you write <ic>let a = 2</ic> and remove that value from your script, **it will crash**! Variable and state is not preserved between each run of the script. There are other ways to deal with variables and to share variables between scripts! Some variables like **iterators** can keep their state between iterations because they are saved **with the file itself**. There is also **global variables**.
|
||||
- **about errors and printing:** your code will crash! Don't worry, we do our best to make it crash in the most gracious way possible. Most errors are caught and displayed in the interface. For weirder bugs, open the dev console with ${key_shortcut(
|
||||
"Ctrl + Shift + I",
|
||||
)}. You cannot directly use <ic>console.log('hello, world')</ic> in the interface but you can use <ic>log(message)</ic> to print a one line message. You will have to open the console as well to see your messages being printed there!
|
||||
)}. You cannot directly use <ic>console.log('hello, world')</ic> in the interface but you can use <ic>log(message)</ic> to print a one line message. You will have to open the console as well to see your messages being printed there! You can also use <ic>logOnce(message)</ic> to print a message only once (or everytime you press Ctrl+Shift+Backspace).
|
||||
- **about new syntax:** sometimes, we had some fun with JavaScript's syntax in order to make it easier/faster to write on stage. <ic>&&</ic> can also be written <ic>::</ic> or <ic>-></ic> because it is faster to type or better for the eyes!
|
||||
|
||||
|
||||
# Common idioms
|
||||
|
||||
There are some techniques to keep code short and tidy. Don't try to write the shortest possible code! Use shortcuts when it makes sense. Take a look at the following examples:
|
||||
@ -38,8 +38,8 @@ beat(.75) :: snd('linnhats').n([1,4,5].beat()).out()
|
||||
beat(1) :: snd('bd').out()
|
||||
//if (true) && log('very true')
|
||||
// These two lines are the same:
|
||||
// beat(1) && snd('bd').out()
|
||||
//// beat(1) :: snd('bd').out()
|
||||
// beat(1) && snd('bd').out()
|
||||
//// beat(1) :: snd('bd').out()
|
||||
|
||||
`,
|
||||
true,
|
||||
@ -67,7 +67,7 @@ beat(4) ? snd('kick').out() : beat(2) :: snd('snare').out()
|
||||
)}
|
||||
|
||||
# About crashes and bugs
|
||||
|
||||
|
||||
Things will crash! It's part of the show! You will learn progressively to avoid mistakes and to write safer code. Do not hesitate to kill the page or to stop the transport if you feel overwhelmed by an algorithm blowing up. There is no safeguard to stop you from doing most things. This is to ensure that you have all the available possible room to write bespoke code and experiment with your ideas through code.
|
||||
|
||||
${makeExample(
|
||||
|
||||
@ -20,57 +20,57 @@ MIDI input can be enabled in the settings panel. Once you have done that, you ca
|
||||
* <ic>active_notes(channel?: number)</ic>: returns array of the active notes / pressed keys as an array of MIDI note numbers (0-127). Returns undefined if no notes are active.
|
||||
|
||||
${makeExample(
|
||||
"Play active notes as chords",
|
||||
`
|
||||
"Play active notes as chords",
|
||||
`
|
||||
beat(1) && active_notes() && sound('sine').chord(active_notes()).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Play active notes as arpeggios",
|
||||
`
|
||||
"Play active notes as arpeggios",
|
||||
`
|
||||
beat(0.25) && active_notes() && sound('juno').note(
|
||||
active_notes().beat(0.5)+[12,24].beat(0.25)
|
||||
).cutoff(300 + usine(1/4) * 2000).out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
|
||||
* <ic>sticky_notes(channel?: number)</ic>: returns array of the last pressed keys as an array of MIDI note numbers (0-127). Notes are added and removed from the list with the "Note on"-event. Returns undefined if no keys have been pressed.
|
||||
|
||||
${makeExample(
|
||||
"Play continous arpeggio with sticky notes",
|
||||
`
|
||||
"Play continous arpeggio with sticky notes",
|
||||
`
|
||||
beat(0.25) && sticky_notes() && sound('arp')
|
||||
.note(sticky_notes().palindrome().beat(0.25)).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
* <ic>last_note(channel?: number)</ic>: returns the last note that has been received. Returns 60 if no other notes have been received.
|
||||
|
||||
${makeExample(
|
||||
"Play last note",
|
||||
`
|
||||
"Play last note",
|
||||
`
|
||||
beat(0.5) && sound('sawtooth').note(last_note())
|
||||
.vib([1, 3, 5].beat(1))
|
||||
.vibmod([1,3,2,4].beat(2)).out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
* <ic>buffer()</ic>: return true if there are notes in the buffer.
|
||||
* <ic>buffer_note(channel?: number)</ic>: returns last unread note that has been received. Note is fetched and removed from start of the buffer once this is called. Returns undefined if no notes have been received.
|
||||
|
||||
${makeExample(
|
||||
"Play buffered note",
|
||||
`
|
||||
"Play buffered note",
|
||||
`
|
||||
beat(1) && buffer() && sound('sine').note(buffer_note()).out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
|
||||
|
||||
@ -80,33 +80,33 @@ ${makeExample(
|
||||
Midi CC messages can be used to control any value in Topos. MIDI input can be defined in Settings and last received CC message can be used to control any numeric value within Topos.
|
||||
|
||||
Currently supported methods for CC input are:
|
||||
* <ic>last_cc(control: number, channel?: number)</ic>: Returns last received CC value for given control number (and optional channel). By default last CC value is last value from ANY channel or 64 if no CC messages have been received.
|
||||
* <ic>ccIn(control: number, channel?: number)</ic>: Returns last received CC value for given control number (and optional channel). By default last CC value is last value from ANY channel or 64 if no CC messages have been received.
|
||||
|
||||
${makeExample(
|
||||
"Play notes with cc",
|
||||
`
|
||||
beat(0.5) && sound('arp').note(last_cc(74)).out()
|
||||
"Play notes with cc",
|
||||
`
|
||||
beat(0.5) && sound('arp').note(ccin(74)).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Control everything with CCs",
|
||||
`
|
||||
"Control everything with CCs",
|
||||
`
|
||||
beat(0.5) :: sound('sine')
|
||||
.freq(last_cc(75)*3)
|
||||
.cutoff(last_cc(76)*2*usine())
|
||||
.freq(ccIn(75)*3)
|
||||
.cutoff(ccIn(76)*2*usine())
|
||||
.sustain(1.0)
|
||||
.out()
|
||||
|
||||
beat(last_cc(74)/127*.5) :: sound('sine')
|
||||
.freq(last_cc(75)*6)
|
||||
.cutoff(last_cc(76)*3*usine())
|
||||
.sustain(last_cc(74)/127*.25)
|
||||
beat(ccIn(74)/127*.5) :: sound('sine')
|
||||
.freq(ccIn(75)*6)
|
||||
.cutoff(ccIn(76)*3*usine())
|
||||
.sustain(ccIn(74)/127*.25)
|
||||
.out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
|
||||
## Run scripts with MIDI
|
||||
@ -126,10 +126,10 @@ Topos can output scales to external keyboards lighted keys using the following f
|
||||
- <ic>show_scale(key: string, scale: string|int, channel?: number, port?: string|number, soundOff?: boolean): void</ic>: sends the scale as midi on messages to specified port and channel to light the keys of external keyboard. If soundOff is true, all sound off message will be sent after every note on message. This can be useful with some keyboards not supporting external channel for lightning or routing for the midi in to suppress the sound from incoming note on messages.
|
||||
|
||||
${makeExample(
|
||||
"Show scale on external keyboard",
|
||||
`show_scale("F","aeolian",0,4)`,
|
||||
true,
|
||||
)}
|
||||
"Show scale on external keyboard",
|
||||
`show_scale("F","aeolian",0,4)`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample("Hide scale", `hide_scale("F","aeolian",0,4)`, true)}
|
||||
|
||||
|
||||
@ -12,30 +12,30 @@ Welcome to the **Topos** documentation. You can jump here anytime by pressing ${
|
||||
)}. Press again to make the documentation disappear. Contributions are much appreciated! The documentation [lives here](https://github.com/Bubobubobubobubo/topos/tree/main/src/documentation).
|
||||
|
||||
${makeExample(
|
||||
"Welcome! Eval to get started",
|
||||
examples[Math.floor(Math.random() * examples.length)],
|
||||
true,
|
||||
)}
|
||||
"Welcome! Eval to get started",
|
||||
examples[Math.floor(Math.random() * examples.length)],
|
||||
true,
|
||||
)}
|
||||
|
||||
# What is Topos?
|
||||
|
||||
Topos is an _algorithmic_ sequencer. Topos is also a _live coding_ environment. To sum it up, think: "_making music in real time through code_". Code used as an expressive medium for musical improvisation! Topos uses small algorithms to represent musical sequences and processes.
|
||||
|
||||
${makeExample(
|
||||
"Small algorithms for direct musical expression",
|
||||
`
|
||||
"Small algorithms for direct musical expression",
|
||||
`
|
||||
rhythm(.5, 4, 8) :: sound('drum').out()
|
||||
rhythm(.25, [5, 7].beat(2), 8) :: sound(['hc', 'fikea', 'hat'].pick(1))
|
||||
.lpf([500, 4000+usine(1/2)*2000]).pan(r(0, 1)).ad(0, [1, .5])
|
||||
.db(-ir(1,8)).speed([1,[0.5, 2].pick()]).room(0.5).size(3).o(4).out()
|
||||
beat([2,0.5].dur(13.5, 0.5))::snd('fsoftsnare')
|
||||
.n(0).speed([1, 0.5]).o(4).out()`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Computer music should be immediate and intuitive",
|
||||
`
|
||||
"Computer music should be immediate and intuitive",
|
||||
`
|
||||
let chord_prog = [0, 0, 5].bar() // Chord progression
|
||||
beat(.25)::snd('sine')
|
||||
.note(chord_prog + [60, 64, 67, 71].mouseX()
|
||||
@ -47,27 +47,25 @@ beat(.25)::snd('sine')
|
||||
.delay(0.5).delayt(0.25).delayfb(0.7) // Delay
|
||||
.room(0.5).size(8) // Reverb
|
||||
.out()`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Making the web less dreadful, one beep at at time",
|
||||
`
|
||||
"Making the web less dreadful, one beep at at time",
|
||||
`
|
||||
beat(.5) :: sound('sid').n($(2))
|
||||
.room(1).speed([1,2].pick()).out()
|
||||
beat(.25) :: sound('sid').note(
|
||||
[34, 36, 41].beat(.25) + [[0,-24].pick(),12].beat())
|
||||
.room(0.9).size(0.9).n(4).out()`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
Topos is deeply inspired by the [Monome Teletype](https://monome.org/). The Teletype is/was an open source hardware module for Eurorack synthesizers. While the Teletype was initially born as an hardware module, Topos aims to be a web-browser based cousin of it! It is a sequencer, a scriptable interface, a companion for algorithmic music-making. Topos wishes to fullfill the same goal as the Teletype, keeping the same spirit alive on the web. It is free, open-source, and made to be shared and used by everyone. Learn more about live coding on [livecoding.fr](https://livecoding.fr).
|
||||
|
||||
## Demo Songs
|
||||
## Alternative documentation source (.pdf)
|
||||
|
||||
Reloading the application will get you one random song example to study every time. Press ${key_shortcut(
|
||||
"F5",
|
||||
)} and listen to them all! The demo songs are also used a bit everywhere in the documentation to illustrate some of the working principles :).
|
||||
You can also find a .pdf version listing the principal commands and functions [here](https://github.com/Bubobubobubobubo/topos/blob/main/src/documentation/basics/TOPOS_COMMANDS.pdf). This document has been generated by Chris Collis. It recaps the main sections of this documentation and can be a good companion while learning Topos.
|
||||
|
||||
## Support
|
||||
|
||||
|
||||
@ -733,8 +733,8 @@ const completionDatabase: CompletionDatabase = {
|
||||
midi: {
|
||||
name: "midi",
|
||||
category: "midi",
|
||||
description: "Send a MIDI message",
|
||||
example: "midi(144, 60, 100)",
|
||||
description: "Send a MIDI message (note, velocity, channel)",
|
||||
example: "midi(144, 60, 1)",
|
||||
},
|
||||
control_change: {
|
||||
name: "control_change",
|
||||
@ -742,6 +742,12 @@ const completionDatabase: CompletionDatabase = {
|
||||
description: "Send a MIDI control change message",
|
||||
example: "control_change({control: 1, value: 60, channel: 10})",
|
||||
},
|
||||
cc: {
|
||||
name: "cc",
|
||||
category: "midi",
|
||||
description: "Send a MIDI control change message",
|
||||
example: "cc({control: 1, value: 60, channel: 10})",
|
||||
},
|
||||
program_change: {
|
||||
name: "program_change",
|
||||
category: "midi",
|
||||
@ -782,7 +788,7 @@ const completionDatabase: CompletionDatabase = {
|
||||
name: "counter",
|
||||
category: "patterns",
|
||||
description: "Counter/iterator",
|
||||
example: "counter('my_counter_, 20, 1)",
|
||||
example: "counter('my_counter', 20, 1)",
|
||||
},
|
||||
drunk: {
|
||||
name: "drunk",
|
||||
@ -808,11 +814,17 @@ const completionDatabase: CompletionDatabase = {
|
||||
description: "Wraps (or not) of the drunk walk (boolean)",
|
||||
example: "drunk_wrap(true)",
|
||||
},
|
||||
v: {
|
||||
name: "v",
|
||||
global: {
|
||||
name: "global",
|
||||
category: "variable",
|
||||
description: "Global Variable setter or getter",
|
||||
example: "v('my_var', 10) // Sets global variable 'my_var' to 10",
|
||||
example: "global.my_var = 10; // Sets global variable 'my_var' to 10",
|
||||
},
|
||||
g: {
|
||||
name: "g",
|
||||
category: "variable",
|
||||
description: "Global Variable setter or getter",
|
||||
example: "g.my_var = 10; // Sets global variable 'my_var' to 10",
|
||||
},
|
||||
delete_variable: {
|
||||
name: "delete_variable",
|
||||
@ -953,12 +965,12 @@ export const inlineHoveringTips = hoverTooltip(
|
||||
let completion =
|
||||
completionDatabase[text.slice(start - from, end - from)] || {};
|
||||
let divContent = `
|
||||
<h1 class="text-orange-300 text-base pb-1">${completion.name} [<em class="text-white">${completion.category}</em>]</h1>
|
||||
<h1 class="text-brightwhite text-base pb-1">${completion.name} [<em class="text-white">${completion.category}</em>]</h1>
|
||||
<p class="text-base pl-4">${completion.description}</p>
|
||||
<pre class="-mt-2"><code class="pl-4 text-base">${completion.example}</code></pre></div>
|
||||
`;
|
||||
let dom = document.createElement("div");
|
||||
dom.classList.add("px-4", "py-2", "bg-neutral-700", "rounded-lg");
|
||||
dom.classList.add("px-4", "py-2", "bg-background", "rounded-lg");
|
||||
dom.innerHTML = divContent;
|
||||
return { dom };
|
||||
},
|
||||
@ -978,7 +990,7 @@ export const toposCompletions = (context: CompletionContext) => {
|
||||
info: () => {
|
||||
let div = document.createElement("div");
|
||||
div.innerHTML = `
|
||||
<h1 class="text-orange-300 text-base pb-1">${completionDatabase[key].name} [<em class="text-white">${completionDatabase[key].category}</em>]</h1>
|
||||
<h1 class="text-brightwhite text-base pb-1">${completionDatabase[key].name} [<em class="text-white">${completionDatabase[key].category}</em>]</h1>
|
||||
<p class="text-base pl-4">${completionDatabase[key].description}</p>
|
||||
<div class="overflow-hidden overflow-scroll rounded px-2 ml-4 mt-2 bg-neutral-800"><code class="text-sm">${completionDatabase[key].example}</code></div>
|
||||
`;
|
||||
|
||||
@ -13,34 +13,36 @@ Topos comes by default with a forever-increasing number of synthesis capabilitie
|
||||
The <ic>sound</ic> function can take the name of a synthesizer or waveform as first argument. This has for effect to turn the sampler we all know and love into a synthesizer. <ic>sine</ic>, <ic>sawtooth</ic>,<ic>triangle</ic>, <ic>square</ic> are the names used to select classic oscillator waveforms. Note that you can also make use of filters and envelopes to shape the sound to your liking.
|
||||
|
||||
${makeExample(
|
||||
"Listening to the different waveforms from the sweetest to the harshest",
|
||||
`
|
||||
"Listening to the different waveforms from the sweetest to the harshest",
|
||||
`
|
||||
beat(.5) && snd(['sine', 'triangle', 'sawtooth', 'square'].beat()).freq(100).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
Note that you can also use noise if you do not want to use a periodic oscillator:
|
||||
|
||||
${makeExample(
|
||||
"Listening to the different types of noise",
|
||||
`
|
||||
beat(.5) && snd(['brown', 'pink', 'white'].beat()).adsr(0,.1,0,0).out()
|
||||
"Listening to the different types of noise",
|
||||
`
|
||||
beat(.5) && snd(['brown', 'pink', 'white', 'crackle'].beat()).adsr(0,.1,0,0).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
The <ic>crackle</ic> type can be controlled using the <ic>density</ic> parameter.
|
||||
|
||||
Two functions are primarily used to control the frequency of the synthesizer:
|
||||
- <ic>freq(hz: number)</ic>: sets the frequency of the oscillator.
|
||||
- <ic>note(note: number|string)</ic>: sets the MIDI note of the oscillator (MIDI note converted to hertz).
|
||||
|
||||
${makeExample(
|
||||
"Selecting a pitch",
|
||||
`
|
||||
"Selecting a pitch",
|
||||
`
|
||||
beat(.5) && snd('triangle').freq([100,200,400].beat(2)).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Selecting a note",
|
||||
@ -55,20 +57,20 @@ Chords can also played using different parameters:
|
||||
- <ic>chord(string||number[]|...number)</ic>: parses and sets notes for the chord
|
||||
|
||||
${makeExample(
|
||||
"Playing a named chord",
|
||||
`
|
||||
"Playing a named chord",
|
||||
`
|
||||
beat(1) && snd('triangle').chord(["C","Em7","Fmaj7","Emin"].beat(2)).adsr(0,.2).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Playing a chord from a list of notes and doing inversions",
|
||||
`
|
||||
"Playing a chord from a list of notes and doing inversions",
|
||||
`
|
||||
beat(.5) && snd('triangle').chord(60,64,67,72).invert([1,-3,4,-5].pick()).adsr(0,.2).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
# Controlling amplitude
|
||||
|
||||
@ -77,16 +79,16 @@ Controlling the amplitude and duration of the sound can be done using various te
|
||||
- <ic>velocity(velocity: number)</ic>: sets the velocity of the oscillator (velocity is a multiple of gain).
|
||||
|
||||
${makeExample(
|
||||
"Setting the gain",
|
||||
`beat(0.25) :: sound('sawtooth').gain([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`,
|
||||
true,
|
||||
)}
|
||||
"Setting the gain",
|
||||
`beat(0.25) :: sound('sawtooth').gain([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Setting the velocity",
|
||||
`beat(0.25) :: sound('sawtooth').velocity([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`,
|
||||
true,
|
||||
)}
|
||||
"Setting the velocity",
|
||||
`beat(0.25) :: sound('sawtooth').velocity([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Envelopes
|
||||
|
||||
@ -101,45 +103,45 @@ ${makeExample(
|
||||
- <ic>release(release: number)</ic> / <ic>rel(rel: number)</ic>: sets the release time of the envelope.
|
||||
|
||||
${makeExample(
|
||||
"Using decay and sustain to set the ADSR envelope",
|
||||
`
|
||||
"Using decay and sustain to set the ADSR envelope",
|
||||
`
|
||||
beat(0.5) :: sound('wt_piano')
|
||||
.cutoff(1000 + usine() * 4000)
|
||||
.freq(100).decay(.2)
|
||||
.sustain([0.1,0.5].beat(4))
|
||||
.out()`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
This ADSR envelope design is important to know because it is used for other aspects of the synthesis engine such as the filters that we are now going to talk about. But wait, I've kept the best for the end. The <ic>adsr()</ic> combines all the parameters together. It is a shortcut for setting the ADSR envelope:
|
||||
|
||||
- <ic>adsr(attack: number, decay: number, sustain: number, release: number)</ic>: sets the ADSR envelope.
|
||||
|
||||
${makeExample(
|
||||
"Replacing the previous example with the adsr() method",
|
||||
`
|
||||
"Replacing the previous example with the adsr() method",
|
||||
`
|
||||
beat(0.5) :: sound('wt_piano')
|
||||
.cutoff(1000 + usine() * 4000)
|
||||
.freq(100)
|
||||
.adsr(0, .2, [0.1,0.5].beat(4), 0)
|
||||
.out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
- <ic>ad(attack: number, decay: number)</ic>: sets the attack and decay phases, setting sustain and release to <ic>0</ic>.
|
||||
|
||||
${makeExample(
|
||||
"Two segment envelope",
|
||||
`
|
||||
"Two segment envelope",
|
||||
`
|
||||
beat(0.5) :: sound('wt_piano')
|
||||
.cutoff(1000 + usine() * 4000)
|
||||
.freq(100)
|
||||
.ad(0, .2)
|
||||
.out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
## Substractive synthesis using filters
|
||||
|
||||
@ -148,36 +150,36 @@ The most basic synthesis technique used since the 1970s is called substractive s
|
||||
See the Filters page for details on lowpass, highpass and bandpass filters. I also encourage you to study these simple examples to get more familiar with the construction of basic substractive synthesizers:
|
||||
|
||||
${makeExample(
|
||||
"Filtering the high frequencies of an oscillator",
|
||||
`beat(.5) :: sound('sawtooth').cutoff(50 + usine(1/8) * 2000).out()`,
|
||||
true,
|
||||
)}
|
||||
"Filtering the high frequencies of an oscillator",
|
||||
`beat(.5) :: sound('sawtooth').cutoff(50 + usine(1/8) * 2000).out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Simple synthesizer voice with filter",
|
||||
`
|
||||
"Simple synthesizer voice with filter",
|
||||
`
|
||||
beat(.5) && snd('sawtooth')
|
||||
.cutoff([2000,500].pick() + usine(.5) * 4000)
|
||||
.resonance(0.2).freq([100,150].pick())
|
||||
.out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Blessed by the square wave",
|
||||
`
|
||||
"Blessed by the square wave",
|
||||
`
|
||||
beat(4) :: [100,101].forEach((freq) => sound('square').freq(freq).sustain(0.1).out())
|
||||
beat(.5) :: [100,101].forEach((freq) => sound('square').freq(freq*2).sustain(0.01).out())
|
||||
beat([.5, .75, 2].beat()) :: [100,101].forEach((freq) => sound('square')
|
||||
.freq(freq*4 + usquare(2) * 200).sustain(0.125).out())
|
||||
beat(.25) :: sound('square').freq(100*[1,2,4,8].beat()).sustain(0.1).out()`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Ghost carillon (move your mouse!)",
|
||||
`
|
||||
"Ghost carillon (move your mouse!)",
|
||||
`
|
||||
beat(1/8)::sound('sine')
|
||||
.velocity(rand(0.0, 1.0))
|
||||
.delay(0.75).delayt(.5)
|
||||
@ -186,16 +188,16 @@ beat(1/8)::sound('sine')
|
||||
.freq(mouseX())
|
||||
.gain(0.25)
|
||||
.out()`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
## Noise
|
||||
|
||||
A certain amount of brown noise can be added by using the <ic>.noise</ic> key:
|
||||
|
||||
${makeExample(
|
||||
"Different vibrato settings",
|
||||
`
|
||||
"Different vibrato settings",
|
||||
`
|
||||
tempo(140);
|
||||
beat(1) :: sound('triangle')
|
||||
.freq(400).release(0.2)
|
||||
@ -203,16 +205,16 @@ beat(1) :: sound('triangle')
|
||||
.vib([1/2, 1, 2, 4].beat())
|
||||
.vibmod([1,2,4,8].beat(2))
|
||||
.out()`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
## Wavetable synthesis
|
||||
|
||||
Topos can also do wavetable synthesis. Wavetable synthesis allows you to use any sound file as a source to build an oscillator. By default, Topos comes with more than 1000 waveforms thanks to the awesome [AKWF](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) pack made by Kristoffer Ekstrand. Any sample name that contains <ic>wt_</ic> as a prefix will be interpreted by the sampler as a wavetable and thus as an oscillator. See for yourself:
|
||||
|
||||
${makeExample(
|
||||
"Acidity test",
|
||||
`
|
||||
"Acidity test",
|
||||
`
|
||||
beat(.25) :: sound('wt_symetric:8').note([50,55,57,60].beat(.25) - [12,0]
|
||||
.pick()).ftype('12db').adsr(0.05/4, 1/16, 0.25/4, 0)
|
||||
.cutoff(1500 + usine(1/8) * 5000).lpadsr(16, 0.2, 0.2, 0.125/2, 0)
|
||||
@ -220,15 +222,15 @@ beat(.25) :: sound('wt_symetric:8').note([50,55,57,60].beat(.25) - [12,0]
|
||||
beat(1) :: sound('kick').n(4).out()
|
||||
beat(2) :: sound('snare').out()
|
||||
beat(.5) :: sound('hh').out()`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
|
||||
Let's explore the galaxy of possible waveforms. It can be hard to explore them all, there is a **lot** of them:
|
||||
|
||||
${makeExample(
|
||||
"Let's explore some wavetables",
|
||||
`
|
||||
"Let's explore some wavetables",
|
||||
`
|
||||
// Exploring a vast galaxy of waveforms
|
||||
let collection = [
|
||||
'wt_sinharm', 'wt_linear', 'wt_bw_sawrounded',
|
||||
@ -238,8 +240,8 @@ beat(2) :: v('selec', irand(1, 100))
|
||||
beat(2) :: v('swave', collection.pick())
|
||||
beat(0.5) :: sound(v('swave')).n(v('selec')).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
You can work with them just like with any other waveform. Having so many of them makes them also very useful for generating sound effects, percussive, sounds, etc...
|
||||
|
||||
@ -254,8 +256,8 @@ Another really useful technique to know about is FM synthesis, FM standing for _
|
||||
There is also an additional parameter, <ic>fm</ic> that combines <ic>fmi</ic> and <ic>fmh</ic> using strings: <ic>fm('2:4')</ic>. Think of it as a static shortcut for getting some timbres more quickly.
|
||||
|
||||
${makeExample(
|
||||
"80s nostalgia",
|
||||
`
|
||||
"80s nostalgia",
|
||||
`
|
||||
beat([.5, 1].beat(8)) && snd('triangle').adsr(0.02, 0.5, 0.5, 0.25)
|
||||
.fmi(2).fmh(1.5).note([60,55, 60, 63].beat() - 12)
|
||||
.pan(noise()).out()
|
||||
@ -264,23 +266,23 @@ beat(.25) && snd('triangle').adsr(0.02, 0.1, 0.1, 0.1)
|
||||
.pan(noise()).note([60,55, 60, 63].beat() + [0, 7].pick()).out()
|
||||
beat(2) :: sound('cp').room(1).sz(1).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Giving some love to ugly inharmonic sounds",
|
||||
`
|
||||
"Giving some love to ugly inharmonic sounds",
|
||||
`
|
||||
beat([.5, .25].bar()) :: sound('sine').fm('2.2183:3.18293').sustain(0.05).out()
|
||||
beat([4].bar()) :: sound('sine').fm('5.2183:4.5').sustain(0.05).out()
|
||||
beat(.5) :: sound('sine')
|
||||
.fmh([1, 1.75].beat())
|
||||
.fmi($(1) % 30).orbit(2).room(0.5).out()`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Peace and serenity through FM synthesis",
|
||||
`
|
||||
"Peace and serenity through FM synthesis",
|
||||
`
|
||||
beat(0.25) :: sound('sine')
|
||||
.note([60, 67, 70, 72, 77].beat() - [0,12].bar())
|
||||
.attack(0.2).release(0.5).gain(0.25)
|
||||
@ -289,8 +291,8 @@ beat(0.25) :: sound('sine')
|
||||
.cutoff(1500).delay(0.5).delayt(0.125)
|
||||
.delayfb(0.8).fmh(Math.floor(usine(.5) * 4))
|
||||
.out()`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
**Note:** you can also set the _modulation index_ and the _harmonic ratio_ with the <ic>fm</ic> argument. You will have to feed both as a string: <ic>fm('2:4')</ic>. If you only feed one number, only the _modulation index_ will be updated.
|
||||
|
||||
@ -301,8 +303,8 @@ There is also a more advanced set of parameters you can use to control the envel
|
||||
- <ic>fmrelease</ic> / <ic>fmrel</ic>: release time of the modulator envelope.
|
||||
|
||||
${makeExample(
|
||||
"FM Synthesis with envelope control",
|
||||
`
|
||||
"FM Synthesis with envelope control",
|
||||
`
|
||||
beat(.5) :: sound('sine')
|
||||
.note([50,53,55,57].beat(.5) - 12)
|
||||
.fmi(0.5 + usine(.25) * 1.5)
|
||||
@ -310,8 +312,8 @@ beat(.5) :: sound('sine')
|
||||
.fmwave('triangle')
|
||||
.fmsus(0).fmdec(0.2).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
## ZzFX
|
||||
|
||||
@ -320,15 +322,15 @@ beat(.5) :: sound('sine')
|
||||
ZZfX can be triggered by picking a default ZZfX waveform in the following list: <ic>z_sine</ic>, <ic>z_triangle</ic>, <ic>z_sawtooth</ic>, <ic>z_tan</ic>, <ic>z_noise</ic>.
|
||||
|
||||
${makeExample(
|
||||
"Picking a waveform",
|
||||
`
|
||||
"Picking a waveform",
|
||||
`
|
||||
beat(.5) :: sound(['z_sine', 'z_triangle', 'z_sawtooth', 'z_tan', 'z_noise'].beat()).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
${makeExample(
|
||||
"Minimalist chiptune",
|
||||
`
|
||||
"Minimalist chiptune",
|
||||
`
|
||||
beat(.5) :: sound('z_triangle')
|
||||
.note([60, 67, 72, 63, 65, 70].beat(.5))
|
||||
.zrand(0).curve([1,2,3,4].beat(1))
|
||||
@ -338,8 +340,8 @@ beat(.5) :: sound('z_triangle')
|
||||
.room(0.5).size(0.9)
|
||||
.pitchJumpTime(0.01).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
It comes with a set of parameters that can be used to tweak the sound. Don't underestimate this synth! It is very powerful for generating anything ranging from chaotic noise sources to lush pads:
|
||||
|
||||
@ -365,8 +367,8 @@ It comes with a set of parameters that can be used to tweak the sound. Don't und
|
||||
|<ic>duration</ic>|| Total sound duration (overrides envelope) |
|
||||
|
||||
${makeExample(
|
||||
"Chaotic Noise source",
|
||||
`
|
||||
"Chaotic Noise source",
|
||||
`
|
||||
beat(.25) :: sound('z_tan')
|
||||
.note(40).noise(rand(0.0, 1.0))
|
||||
.pitchJump(84).pitchJumpTime(rand(0.0, 1.0))
|
||||
@ -376,21 +378,21 @@ beat(.25) :: sound('z_tan')
|
||||
.sustain(0).decay([0.2, 0.1].pick())
|
||||
.out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
${makeExample(
|
||||
"What is happening to me?",
|
||||
`
|
||||
"What is happening to me?",
|
||||
`
|
||||
beat(1) :: snd('zzfx').zzfx([
|
||||
[4.77,,25,,.15,.2,3,.21,,2.4,,,,,,,.23,.35],
|
||||
[1.12,,97,.11,.16,.01,4,.77,,,30,.17,,,-1.9,,.01,.67,.2]
|
||||
].beat()).out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
${makeExample(
|
||||
"Les voitures dans le futur",
|
||||
`
|
||||
"Les voitures dans le futur",
|
||||
`
|
||||
beat(1) :: sound(['z_triangle', 'z_sine'].pick())
|
||||
.note([60,63,72,75].pick()).tremolo(16)
|
||||
.zmod([0, 1/2, 1/8].div(2).pick())
|
||||
@ -398,23 +400,25 @@ beat(1) :: sound(['z_triangle', 'z_sine'].pick())
|
||||
.room(0.9).size(0.9)
|
||||
.delayt(0.75).delayfb(0.5).out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
Note that you can also design sounds [on this website](https://killedbyapixel.github.io/ZzFX/) and copy the generated code in Topos. To do so, please use the <ic>zzfx</ic> method with the generated array:
|
||||
${makeExample(
|
||||
"Designing a sound on the ZzFX website",
|
||||
`
|
||||
"Designing a sound on the ZzFX website",
|
||||
`
|
||||
|
||||
beat(2) :: sound('zzfx').zzfx([3.62,,452,.16,.1,.21,,2.5,,,403,.05,.29,,,,.17,.34,.22,.68]).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
# Speech synthesis
|
||||
|
||||
Topos can also speak using the [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API). There are two ways to use speech synthesis:
|
||||
|
||||
Speech synthesis API can crash your browser if you use it too much. To avoid crashing the calls should be limited using methods like beat() or run it only once using once().
|
||||
|
||||
- <ic>speak(text: string, lang: string, voice: number, rate: number, pitch: number, volume: number)</ic>
|
||||
- <ic>text</ic>: the text you would like to synthesize (_e.g_ <ic>"Wow, Topos can speak!"</ic>).
|
||||
- <ic>lang</ic>: language code, for example <ic>en</ic> for English, <ic>fr</ic> for French or with the country code for example British English <ic>en-GB</ic>. See supported values from the [list](https://cloud.google.com/speech-to-text/docs/speech-to-text-supported-languages).
|
||||
@ -424,48 +428,48 @@ Topos can also speak using the [Web Speech API](https://developer.mozilla.org/en
|
||||
- <ic>volume(number)</ic>: speaking volume, from <ic>0.0</ic> to <ic>1.0</ic>.
|
||||
|
||||
${makeExample(
|
||||
"Hello world!",
|
||||
`
|
||||
beat(4) :: speak("Hello world!")
|
||||
"Hello world!",
|
||||
`
|
||||
once() && speak("Hello world!")
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Let's hear people talking about Topos",
|
||||
`
|
||||
beat(2) :: speak("Topos!","fr",irand(0,5))
|
||||
"Let's hear people talking about Topos",
|
||||
`
|
||||
beat(2) && speak("Topos!","fr",irand(0,5))
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
|
||||
You can also use speech by chaining methods to a string:
|
||||
|
||||
${makeExample(
|
||||
"Foobaba is the real deal",
|
||||
`
|
||||
onbeat(4) :: "Foobaba".voice(irand(0,10)).speak()
|
||||
"Foobaba is the real deal",
|
||||
`
|
||||
onbeat(4) && "Foobaba".voice(irand(0,10)).speak()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Building string and chaining",
|
||||
`
|
||||
"Building string and chaining",
|
||||
`
|
||||
const subject = ["coder","user","loser"].pick()
|
||||
const verb = ["is", "was", "isnt"].pick()
|
||||
const object = ["happy","sad","tired"].pick()
|
||||
const sentence = subject+" "+verb+" "+" "+object
|
||||
|
||||
beat(6) :: sentence.pitch(0).rate(0).voice([0,2].pick()).speak()
|
||||
beat(6) && sentence.pitch(0).rate(0).voice([0,2].pick()).speak()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Live coded poetry with array and string chaining",
|
||||
`
|
||||
"Live coded poetry with array and string chaining",
|
||||
`
|
||||
tempo(70)
|
||||
|
||||
const croissant = [
|
||||
@ -474,13 +478,13 @@ ${makeExample(
|
||||
"Flamboyant", "Cosmique", "Croissant!"
|
||||
];
|
||||
|
||||
onbeat(4) :: croissant.bar()
|
||||
onbeat(4) && croissant.bar()
|
||||
.lang("fr")
|
||||
.volume(rand(0.2,2.0))
|
||||
.rate(rand(.4,.6))
|
||||
.speak();
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
`;
|
||||
};
|
||||
|
||||
@ -65,12 +65,12 @@ On this page, you will find an exhaustive list of all the samples currently load
|
||||
A very large collection of wavetables for wavetable synthesis. This collection has been released by Kristoffer Ekstrand: [AKWF Waveforms](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/). Every sound sample that starts with <ic>wt_</ic> will be looped. Look at this demo:
|
||||
|
||||
${makeExample(
|
||||
"Wavetable synthesis made easy :)",
|
||||
`
|
||||
"Wavetable synthesis made easy :)",
|
||||
`
|
||||
beat(0.5)::sound('wt_stereo').n([0, 1].pick()).ad(0, .25).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
|
||||
Pick one folder and spend some time exploring it. There is a lot of different waveforms.
|
||||
@ -81,15 +81,16 @@ ${samples_to_markdown(application, "Waveforms")}
|
||||
|
||||
## Drum machines sample pack
|
||||
|
||||
A set of 72 classic drum machines created by **Geikha**: [Geikha Drum Machines](https://github.com/geikha/tidal-drum-machines). To use them efficiently, it is best to use the <ic>.bank()</ic> parameter like so:
|
||||
A set of 72 classic drum machines created by **Geikha**: [Geikha Drum Machines](https://github.com/geikha/tidal-drum-machines). To use them efficiently, it is best
|
||||
to use the <ic>.bank()</ic> parameter like so:
|
||||
|
||||
${makeExample(
|
||||
"Using a classic drum machine",
|
||||
`
|
||||
"Using a classic drum machine",
|
||||
`
|
||||
beat(0.5)::sound(['bd', 'cp'].pick()).bank("AkaiLinn").out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
Here is the complete list of available machines:
|
||||
|
||||
@ -98,6 +99,41 @@ Here is the complete list of available machines:
|
||||
${samples_to_markdown(application, "Machines")}
|
||||
</div>
|
||||
|
||||
In practice, using them will lead you to write short two letters long sample names, each one for a different piece of the kit:
|
||||
|
||||
| Kit Piece | Short name |
|
||||
|-----------|----------------|
|
||||
|
|
||||
| **Bass/kick drum** | <ic>bd</ic> |
|
||||
| **Snare drum** | <ic>sd</ic> |
|
||||
| **Rimshot** | <ic>rim</ic> |
|
||||
| **Clap** | <ic>cp</ic> |
|
||||
| **Closed hi-hat** | <ic>hh</ic> |
|
||||
| **Open hi-hat** | <ic>oh</ic> |
|
||||
| **Crash** | <ic>cr</ic> |
|
||||
| **Ride** | <ic>rd</ic> |
|
||||
| **Shakers (and maracas, cabasas, etc)** | <ic>sh</ic> |
|
||||
| **High tom** | <ic>ht</ic> |
|
||||
| **Medium tom** | <ic>mt</ic> |
|
||||
| **Low tom** | <ic>lt</ic> |
|
||||
| **Cowbell** | <ic>cb</ic> |
|
||||
| **Tambourine** | <ic>tb</ic> |
|
||||
| **Other percussions** | <ic>perc</ic> |
|
||||
| **Miscellaneous samples** | <ic>misc</ic> |
|
||||
| **Effects** | <ic>fx</ic> |
|
||||
|
||||
Note that there is also a <ic>drumMachine</ic> function that allows you to play a random drum machine without even typing the name.
|
||||
It takes a single argument, a number, that will pick a machine for you in the list:
|
||||
|
||||
${makeExample(
|
||||
"Using a classic drum machine",
|
||||
`
|
||||
beat(1/2)::sound(['bd', 'cp'].pick()).drumMachine(1).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
|
||||
## FoxDot sample pack
|
||||
|
||||
The default sample pack used by Ryan Kirkbride's [FoxDot](https://github.com/Qirky/FoxDot). It is a nice curated sample pack that covers all the basic sounds you could want.
|
||||
@ -119,12 +155,12 @@ ${samples_to_markdown(application, "Amiga")}
|
||||
A collection of many different amen breaks. Use <ic>.stretch()</ic> to play with these:
|
||||
|
||||
${makeExample(
|
||||
"Stretching an amen break",
|
||||
`
|
||||
"Stretching an amen break",
|
||||
`
|
||||
beat(4)::sound('amen1').stretch(4).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
The stretch should be adapted based on the length of each amen break.
|
||||
|
||||
|
||||
227
src/documentation/more/visualization.ts
Normal file
@ -0,0 +1,227 @@
|
||||
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",
|
||||
`beat(4) :: hydra.osc(3, 0.5, 2).out()`,
|
||||
false,
|
||||
)}
|
||||
|
||||
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
|
||||
`,
|
||||
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>.
|
||||
|
||||
|
||||
`;
|
||||
};
|
||||
@ -37,7 +37,7 @@ beat(1)::sound('fhh').juxrev().out()
|
||||
This is an extremely powerful construct. For example, you can use it to create synthesizer presets and reuse them later on. You can also define parameters for your registered functions. For example:
|
||||
|
||||
${makeExample(
|
||||
"Re-creating a classic Tidal function",
|
||||
"Creating synth presets",
|
||||
`
|
||||
// Registering a specific synth architecture
|
||||
register('sub', (n,x=4,y=80)=>n.ad(0, .25)
|
||||
@ -54,6 +54,26 @@ rhythm(.25, [6, 8].beat(), 12)::sound('sine')
|
||||
true,
|
||||
)}
|
||||
|
||||
## Registering chain for all events
|
||||
|
||||
The chain can also be registered automatically for all events. This is useful if you want to add a specific effect to all your events.
|
||||
|
||||
${makeExample(
|
||||
"Registering chain to all events at once",
|
||||
`
|
||||
z0("h 9 ^ <7 5 3 1>")
|
||||
.sound("sine")
|
||||
.out()
|
||||
|
||||
z1("0 4 3 2")
|
||||
.sound("sine")
|
||||
.out()
|
||||
|
||||
all(x=>x.room(1).delay(rI(0,0.5)))
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Logging values from the chain
|
||||
|
||||
You can use the <ic>log()</ic> function to print values from the current event. This can be useful to debug your code. Useful parameters to log could be **note**, **pitch**, **dur**, **octave** etc...
|
||||
@ -120,8 +140,6 @@ There is a growing collection of probability and chance methods you can use:
|
||||
| <ic>almostAlways</ic> | With a 98.5% probability. | <ic>.almostAlways(s => s.note(70))</ic> |
|
||||
| <ic>always</ic> | Always transforms the Event. | <ic>.always(s => s.note(71))</ic> |
|
||||
|
||||
|
||||
|
||||
### MIDI Chaining
|
||||
|
||||
The conditional chaining also applies to MIDI. Values can also be incremented using <ic>+=</ic> notation.
|
||||
|
||||
@ -37,6 +37,25 @@ beat(1) :: script(1, 3, 5)
|
||||
- <ic>mean(...values: number[]): number</ic>: returns the arithmetic mean of a list of numbers.
|
||||
- <ic>limit(value: number, min: number, max: number): number</ic>: Limits a value between a minimum and a maximum.
|
||||
|
||||
### Scaling functions
|
||||
|
||||
There are some very useful scaling methods taken from **SuperCollider**. You can call these on any number:
|
||||
|
||||
- <ic>.linlin(inMin: number, inMax: number, outMin: number, outMax: number)</ic>: scale linearly from one range to another.
|
||||
- <ic>.linexp(inMin: number, inMax: number, outMin: number, outMax: number)</ic>: scale a linear range to an exponential range.
|
||||
- <ic>.explin(inMin: number, inMax: number, outMin: number, outMax: number)</ic>: scale an exponential range to a linear range.
|
||||
- <ic>.expexp(inMin: number, inMax: number, outMin: number, outMax: number)</ic>: scale an exponential range to another exponential range.
|
||||
- <ic>.lincurve(inMin: number, inMax: number, outMin: number, outMax: number, curve: number)</ic>: scale a number from one range to another following a specific curve.
|
||||
- <ic>curve: number</ic>: <ic>0</ic> is linear, <ic>< 0</ic> is concave, negatively curved, <ic>> 0</ic> is convex, positively curved
|
||||
|
||||
${makeExample(
|
||||
"Scaling an LFO",
|
||||
`usine(1/2).linlin(0, 1, 0, 100)`,
|
||||
true,
|
||||
)}
|
||||
|
||||
|
||||
|
||||
## Delay functions
|
||||
|
||||
- <ic>delay(ms: number, func: Function): void</ic>: Delays the execution of a function by a given number of milliseconds.
|
||||
|
||||
@ -8,7 +8,7 @@ export const generators = (application: Editor): string => {
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@ -38,14 +38,16 @@ ${makeExample(
|
||||
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",
|
||||
`
|
||||
@ -54,16 +56,16 @@ ${makeExample(
|
||||
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)
|
||||
@ -72,9 +74,61 @@ ${makeExample(
|
||||
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.
|
||||
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.
|
||||
|
||||
@ -106,7 +160,7 @@ function* poly(x) {
|
||||
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()
|
||||
|
||||
@ -8,11 +8,10 @@ export const lfos = (application: Editor): string => {
|
||||
|
||||
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, times: number = 1, offset: number= 0): number</ic>: returns a sinusoïdal oscillation between <ic>-1</ic> and <ic>1</ic>.
|
||||
- <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>times</ic> : output value multiplier.
|
||||
- <ic>offset</ic>: linear offset.
|
||||
- <ic>usine(freq: number = 1, times: number = 1, offset: number= 0): number</ic>: returns a sinusoïdal oscillation between <ic>0</ic> and <ic>1</ic>. The <ic>u</ic> stands for _unipolar_.
|
||||
- <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",
|
||||
@ -20,8 +19,8 @@ ${makeExample(
|
||||
true,
|
||||
)};
|
||||
|
||||
- <ic>triangle(freq: number = 1, times: number = 1, offset: number= 0): number</ic>: returns a triangle oscillation between <ic>-1</ic> and <ic>1</ic>.
|
||||
- <ic>utriangle(freq: number = 1, times: number = 1, offset: number= 0): number</ic>: returns a triangle oscillation between <ic>0</ic> and <ic>1</ic>. The <ic>u</ic> stands for _unipolar_.
|
||||
- <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",
|
||||
@ -29,8 +28,8 @@ ${makeExample(
|
||||
true,
|
||||
)}
|
||||
|
||||
- <ic>saw(freq: number = 1, times: number = 1, offset: number= 0): number</ic>: returns a sawtooth-like oscillation between <ic>-1</ic> and <ic>1</ic>.
|
||||
- <ic>usaw(freq: number = 1, times: number = 1, offset: number= 0): number</ic>: returns a sawtooth-like oscillation between <ic>0</ic> and <ic>1</ic>. The <ic>u</ic> stands for _unipolar_.
|
||||
- <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",
|
||||
@ -38,8 +37,8 @@ ${makeExample(
|
||||
true,
|
||||
)}
|
||||
|
||||
- <ic>square(freq: number = 1, times: number = 1, offset: number= 0, 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, times: number = 1, offset: number= 0, 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.
|
||||
- <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",
|
||||
@ -48,6 +47,7 @@ ${makeExample(
|
||||
)};
|
||||
|
||||
- <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",
|
||||
|
||||
@ -120,6 +120,38 @@ beat(1)::sound(['kick', 'fsnare'].dur(3, 1))
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Patterning with ternary statements",
|
||||
`
|
||||
const dada = flipbar(2) ? [0,[3,5,-1].bar(3),2,3] : [9,8,9,6]
|
||||
beat(0.5) :: sound('wt_hvoice:3')
|
||||
.pitch(dada.beat(0.5))
|
||||
.scale("88.0")
|
||||
.adsr(0.05, 0.05, 0, 0)
|
||||
.cutoff(500 + usine(1/8) * 5000)
|
||||
.room(1.5)
|
||||
.resonance(0.25)
|
||||
.out()
|
||||
beat(1) :: sound('kick').n(4).out()
|
||||
onbeat([0.5,0.8].beat(1),2) :: sound('snare').out()
|
||||
onbeat(0.5,0.8,1,1.5,2,2.5,3,4) :: sound('hh').out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Iteration using a counter
|
||||
|
||||
- <ic>counter(name,limit?,step?)</ic>: return the next value on the list based on counter value. The limit is optional and defaults to the length of the list. The step is optional and defaults to 1. Setting / changing limit will reset the counter.
|
||||
- <ic>$(name,limit?,step?)</ic>: shorter alias for the counter.
|
||||
|
||||
${makeExample(
|
||||
"Using counter to iterate over a list",
|
||||
`
|
||||
beat(0.5) :: sound("bd").gain(line(0,1,0.01).$("ramp")).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Manipulating notes and scales
|
||||
|
||||
- <ic>pitch()</ic>: convert a list of integers to pitch classes
|
||||
@ -152,7 +184,7 @@ ${makeExample(
|
||||
"Play pitches from scale created from cent intervals",
|
||||
`
|
||||
rhythm([0.5,0.25].beat(1),14,16) :: sound('pluck')
|
||||
.stretch(r(1,5)).pitch(r(0,6)).key(57)
|
||||
.stretch(iR(1,5)).pitch(iR(0,6)).key(57)
|
||||
.cents(120,270,540,670,785,950,1215).out()
|
||||
`,
|
||||
true,
|
||||
|
||||
@ -54,6 +54,7 @@ By default chance operators will be evaluated 48 times within a beat. You can ch
|
||||
- <ic>frequently(beats?: number)</ic>: returns true 90% of the time in given number of beats
|
||||
- <ic>almostAlways(beats?: number)</ic>: returns true 99% of the time in given number of beats
|
||||
- <ic>always(beats?: number)</ic>: returns true. Can be handy when switching between different probabilities
|
||||
- <ic>once()</ic>: returns true once, then false until the code is force evaluated (Shift+Ctrl+Enter)
|
||||
|
||||
Examples:
|
||||
|
||||
|
||||
@ -7,20 +7,15 @@ export const variables = (application: Editor): string => {
|
||||
|
||||
# Variables
|
||||
|
||||
By default, each script is independant from each other. Scripts live in their own bubble and you cannot get or set variables affecting a script from any other script.
|
||||
By default, each script is independant from each other. The variables defined in **script 1** are not available in **script 2**, etc. Moreover, they are overriden everytime the file is evaluated. It means that you cannot store any state or share information. However, you can use global variables to make that possible.
|
||||
|
||||
**However**, everybody knows that global variables are cool and should be used everywhere. Global variables are an incredibely powerful tool to radically alter a composition in a few lines of code.
|
||||
There is a <ic>global</ic> object that you can use to store and retrieve information. It is a simple key/value store. You can store any type of data in it:
|
||||
|
||||
- <ic>variable(a: number | string, b?: any)</ic>: if only one argument is provided, the value of the variable will be returned through its name, denoted by the first argument. If a second argument is used, it will be saved as a global variable under the name of the first argument.
|
||||
- <ic>delete_variable(name: string)</ic>: deletes a global variable from storage.
|
||||
- <ic>clear_variables()</ic>: clear **ALL** variables. **This is a destructive operation**!
|
||||
|
||||
**Note:** since this example is running in the documentation, we cannot take advantage of the multiple scripts paradigm. Try to send a variable from the global file to the local file n°6.
|
||||
|
||||
${makeExample(
|
||||
"Setting a global variable",
|
||||
`
|
||||
v('my_cool_variable', 2)
|
||||
// This is script n°3
|
||||
global.my_variable = 2
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
@ -28,15 +23,16 @@ v('my_cool_variable', 2)
|
||||
${makeExample(
|
||||
"Getting that variable back and printing!",
|
||||
`
|
||||
// Note that we just use one argument
|
||||
log(v('my_cool_variable'))
|
||||
// This is script n°4
|
||||
log(global.my_variable)
|
||||
`,
|
||||
false,
|
||||
true,
|
||||
)}
|
||||
|
||||
Now your scripts can share information with each other!
|
||||
|
||||
## Counter and iterators
|
||||
|
||||
|
||||
You will often need to use iterators and/or counters to index over data structures (getting a note from a list of notes, etc...). There are functions ready to be used for this. Each script also comes with its own iterator that you can access using the <ic>i</ic> variable. **Note:** the script iteration count is **not** resetted between sessions. It will continue to increase the more you play, even if you just picked up an old project.
|
||||
|
||||
- <ic>counter(name: number | string, limit?: number, step?: number)</ic>: reads the value of the counter <ic>name</ic>. You can also call this function using the dollar symbol: <ic>$</ic>.
|
||||
|
||||
@ -8,7 +8,7 @@ export const ziffers_algorithmic = (application: Editor): string => {
|
||||
|
||||
Ziffers provides shorthands for **many** numeric and algorithimic operations such as evaluating random numbers and creating sequences using list operations:
|
||||
|
||||
* **List operations:** Cartesian operation (_e.g._ <ic>(3 2 1)+(2 5)</ic>) using the <ic>+</ic> operator. All the arithmetic operators are supported.
|
||||
* **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",
|
||||
@ -78,8 +78,20 @@ z1("s A=(0 (1,4)) B~(2 (3,8)) A B A B A")
|
||||
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
|
||||
|
||||
`;
|
||||
};
|
||||
|
||||
@ -32,6 +32,10 @@ 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.
|
||||
|
||||
@ -6,19 +6,19 @@ export const ziffers_tonnetz = (application: Editor): string => {
|
||||
return `
|
||||
# Tonnetz
|
||||
|
||||
The Riemannian Tonnetz is a geometric representation of tonal relationships for applying 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>. Live coding tonnetz implementation **combines 67 transformations** to **new explorative notation** that includes all of the traditional triad transformations (PLR functions), extended PLR* transformations, film music transformations and seventh transformations (PLRQ, PLRQ*, ST).
|
||||
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 tonal pitch spaces which are all supported in Ziffers implementation.
|
||||
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 (<ic>pl</ic>), OctaCycles (<ic>pr</ic>), Enneacycles (seventh chords), Weitzmann Regions (triad chords), Boretz Regions (triad chords) and OctaTowers (tetrachords). You can explore each of these graphs in great generality over different Tonnetz.
|
||||
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.
|
||||
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>[plrfsntq][1-9]*</ic>:
|
||||
Indexed transformations <ic>[plrfsntqNSEW][1-9]*</ic>:
|
||||
|
||||
* p: Parallel
|
||||
* l: Leading-tone exchange
|
||||
@ -29,6 +29,10 @@ Indexed transformations <ic>[plrfsntq][1-9]*</ic>:
|
||||
* 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:
|
||||
|
||||
@ -65,6 +69,16 @@ z1("024")
|
||||
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.
|
||||
@ -105,7 +119,7 @@ Therefore, you will see that paying attention to the examples will allow you to
|
||||
${makeExample(
|
||||
"Synthetic 'Morton'",
|
||||
`
|
||||
z0('3/4 0 _ q 6 h 4 3 w 2 0 3/4 ^^ 0 _q 6 h 4 3 3/4 2 5/4 0 w r')
|
||||
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()
|
||||
@ -115,7 +129,7 @@ z1('w 904')
|
||||
.tonnetz('o f l l o f l l o')
|
||||
.sound('sine').adsr(0.1, 1, 1, 1.9).out()
|
||||
|
||||
z2('w 904')
|
||||
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')
|
||||
@ -123,9 +137,10 @@ z2('w 904')
|
||||
|
||||
z3('e __ 4 s 0 e 1 2 s')
|
||||
.sound('hat').delay(0.5).delayfb(0.35).out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Different Tonnetz
|
||||
## 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.
|
||||
|
||||
@ -204,17 +219,19 @@ z1("1.0 047{10}")
|
||||
|
||||
## Cyclic methods
|
||||
|
||||
In addition to the 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:
|
||||
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)</ic>: Cycles through chords in the hexa cycle
|
||||
* <ic>octaCycle(tonnetz: number[], repeats: number = 4)</ic>: Cycles through chords in the octa cycle
|
||||
* <ic>enneaCycle(tonnetz: number[], repeats: number = 3)</ic>: Cycles through chords in the ennea cycle
|
||||
* <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
|
||||
|
||||
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.
|
||||
: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.
|
||||
|
||||
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>.
|
||||
**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.
|
||||
|
||||
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>.
|
||||
**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:
|
||||
|
||||
@ -264,20 +281,120 @@ ${makeExample(
|
||||
"HexaCycles with vitamins",
|
||||
`
|
||||
z1("0")
|
||||
.scale("chromatic")
|
||||
.hexaCycle([2,3,7],4)
|
||||
.sound("sine").out()
|
||||
.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)](https://www.jstor.org/stable/843877)
|
||||
* 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)
|
||||
|
||||
## :construction: Regions and OctaTowers
|
||||
## More traversing methods
|
||||
|
||||
TBD: Implement and write about Weitzmann Regions, Boretz Regions, OctaTowers
|
||||
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).
|
||||
|
||||
`;
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { type UserAPI } from "../API";
|
||||
import { safeScale, stepsToScale } from "zifferjs";
|
||||
export {};
|
||||
export { };
|
||||
|
||||
declare global {
|
||||
interface Array<T> {
|
||||
@ -33,6 +33,8 @@ declare global {
|
||||
gen(min: number, max: number, times: number): number[];
|
||||
sometimes(func: Function): number[];
|
||||
apply(func: Function): number[];
|
||||
counter(name: string | number, limit?: number, step?: number): number[]
|
||||
$(name: string | number, limit?: number, step?: number): number[];
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,14 +60,14 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
return this[zoneIndex];
|
||||
};
|
||||
|
||||
Array.prototype.square = function (): number[] {
|
||||
Array.prototype.square = function(): number[] {
|
||||
/**
|
||||
* @returns New array with squared values.
|
||||
*/
|
||||
return this.map((x: number) => x * x);
|
||||
};
|
||||
|
||||
Array.prototype.sometimes = function (func: Function): number[] {
|
||||
Array.prototype.sometimes = function(func: Function): number[] {
|
||||
if (api.randomGen() < 0.5) {
|
||||
return func(this);
|
||||
} else {
|
||||
@ -73,11 +75,11 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
}
|
||||
};
|
||||
|
||||
Array.prototype.apply = function (func: Function): number[] {
|
||||
Array.prototype.apply = function(func: Function): number[] {
|
||||
return func(this);
|
||||
};
|
||||
|
||||
Array.prototype.sqrt = function (): number[] {
|
||||
Array.prototype.sqrt = function(): number[] {
|
||||
/**
|
||||
* @returns New array with square roots of values. Throws if any element is negative.
|
||||
*/
|
||||
@ -86,7 +88,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
return this.map((x: number) => Math.sqrt(x));
|
||||
};
|
||||
|
||||
Array.prototype.add = function (amount: number): number[] {
|
||||
Array.prototype.add = function(amount: number): number[] {
|
||||
/**
|
||||
* @param amount - The value to add to each element in the array.
|
||||
* @returns New array with added values.
|
||||
@ -94,7 +96,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
return this.map((x: number) => x + amount);
|
||||
};
|
||||
|
||||
Array.prototype.sub = function (amount: number): number[] {
|
||||
Array.prototype.sub = function(amount: number): number[] {
|
||||
/**
|
||||
* @param amount - The value to subtract from each element in the array.
|
||||
* @returns New array with subtracted values.
|
||||
@ -102,7 +104,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
return this.map((x: number) => x - amount);
|
||||
};
|
||||
|
||||
Array.prototype.mult = function (amount: number): number[] {
|
||||
Array.prototype.mult = function(amount: number): number[] {
|
||||
/**
|
||||
* @param amount - The value to multiply with each element in the array.
|
||||
* @returns New array with multiplied values.
|
||||
@ -110,7 +112,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
return this.map((x: number) => x * amount);
|
||||
};
|
||||
|
||||
Array.prototype.div = function (amount: number): number[] {
|
||||
Array.prototype.div = function(amount: number): number[] {
|
||||
/**
|
||||
* @param amount - The value to divide each element in the array by.
|
||||
* @returns New array with divided values. Throws if division by zero.
|
||||
@ -119,7 +121,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
return this.map((x: number) => x / amount);
|
||||
};
|
||||
|
||||
Array.prototype.pick = function () {
|
||||
Array.prototype.pick = function() {
|
||||
/**
|
||||
* Returns a random element from an array.
|
||||
*
|
||||
@ -128,7 +130,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
return this[Math.floor(api.randomGen() * this.length)];
|
||||
};
|
||||
|
||||
Array.prototype.gen = function (min: number, max: number, times: number) {
|
||||
Array.prototype.gen = function(min: number, max: number, times: number) {
|
||||
/**
|
||||
* Returns an array of random numbers.
|
||||
* @param min - The minimum value of the random numbers
|
||||
@ -145,7 +147,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
);
|
||||
};
|
||||
|
||||
Array.prototype.bar = function (value: number = 1) {
|
||||
Array.prototype.bar = function(value: number = 1) {
|
||||
/**
|
||||
* Returns an element from an array based on the current bar.
|
||||
*
|
||||
@ -160,7 +162,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
}
|
||||
};
|
||||
|
||||
Array.prototype.beat = function (divisor: number = 1) {
|
||||
Array.prototype.beat = function(divisor: number = 1) {
|
||||
const chunk_size = divisor; // Get the first argument (chunk size)
|
||||
const timepos = api.app.clock.pulses_since_origin;
|
||||
const slice_count = Math.floor(
|
||||
@ -170,7 +172,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
};
|
||||
Array.prototype.b = Array.prototype.beat;
|
||||
|
||||
Array.prototype.dur = function (...durations: number[]) {
|
||||
Array.prototype.dur = function(...durations: number[]) {
|
||||
const timepos = api.app.clock.pulses_since_origin;
|
||||
const ppqn = api.ppqn();
|
||||
const adjustedDurations: number[] = this.map(
|
||||
@ -193,7 +195,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
throw new Error("Durations array does not match the pattern length.");
|
||||
};
|
||||
|
||||
Array.prototype.shuffle = function () {
|
||||
Array.prototype.shuffle = function() {
|
||||
/**
|
||||
* Shuffles the array in place.
|
||||
*
|
||||
@ -212,7 +214,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
return this;
|
||||
};
|
||||
|
||||
Array.prototype.rotate = function (steps: number) {
|
||||
Array.prototype.rotate = function(steps: number) {
|
||||
/**
|
||||
* Rotates the array in place.
|
||||
*
|
||||
@ -232,7 +234,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
return this;
|
||||
};
|
||||
|
||||
Array.prototype.unique = function () {
|
||||
Array.prototype.unique = function() {
|
||||
/**
|
||||
* Removes duplicate elements from the array in place.
|
||||
*
|
||||
@ -265,7 +267,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
if (this.length <= 1) {
|
||||
return this;
|
||||
}
|
||||
for (let i = 0; i < this.length; ) {
|
||||
for (let i = 0; i < this.length;) {
|
||||
const rand = api.randomGen() * 100;
|
||||
if (rand < amount) {
|
||||
if (this.length > 1) {
|
||||
@ -378,7 +380,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
return left_to_right.concat(right_to_left);
|
||||
};
|
||||
|
||||
Array.prototype.loop = function (index: number) {
|
||||
Array.prototype.loop = function(index: number) {
|
||||
/**
|
||||
* Returns an element from the array based on the index.
|
||||
* The index will wrap over the array.
|
||||
@ -389,7 +391,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
return this[index % this.length];
|
||||
};
|
||||
|
||||
Array.prototype.random = function () {
|
||||
Array.prototype.random = function() {
|
||||
/**
|
||||
* Returns a random element from the array.
|
||||
*
|
||||
@ -398,9 +400,32 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
return this[Math.floor(api.randomGen() * this.length)];
|
||||
};
|
||||
Array.prototype.rand = Array.prototype.random;
|
||||
|
||||
Array.prototype.counter = function(
|
||||
name: string | number,
|
||||
limit?: number,
|
||||
step?: number) {
|
||||
/**
|
||||
* @param n - Returns next item in array until the end, then returns the last value.
|
||||
*
|
||||
* @returns the shifted array
|
||||
*/
|
||||
const idx = api.counter(name, limit, step);
|
||||
if (limit) {
|
||||
return this[idx % this.length];
|
||||
} else if (idx < this.length) {
|
||||
return this[idx];
|
||||
} else {
|
||||
return this[this.length - 1];
|
||||
}
|
||||
};
|
||||
Array.prototype.$ = Array.prototype.counter;
|
||||
|
||||
};
|
||||
|
||||
Array.prototype.scale = function (
|
||||
|
||||
|
||||
Array.prototype.scale = function(
|
||||
scale: string = "major",
|
||||
base_note: number = 0,
|
||||
) {
|
||||
@ -417,14 +442,14 @@ Array.prototype.scale = function (
|
||||
return this.map((value) => {
|
||||
const octaveShift = Math.floor(value / selected_scale.length) * 12;
|
||||
return (
|
||||
selected_scale[mod(value, selected_scale.length)] +
|
||||
selected_scale[mod(Math.floor(value), selected_scale.length)] +
|
||||
base_note +
|
||||
octaveShift
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
Array.prototype.scaleArp = function (
|
||||
Array.prototype.scaleArp = function(
|
||||
scaleName: string = "major",
|
||||
boundary: number = 0,
|
||||
) {
|
||||
|
||||
@ -4,6 +4,7 @@ import { Player } from "../classes/ZPlayer";
|
||||
import { SoundEvent } from "../classes/SoundEvent";
|
||||
import { SkipEvent } from "../classes/SkipEvent";
|
||||
|
||||
|
||||
declare global {
|
||||
interface Number {
|
||||
z(): Player;
|
||||
@ -25,84 +26,132 @@ declare global {
|
||||
z15(): Player;
|
||||
z16(): Player;
|
||||
midi(): MidiEvent;
|
||||
sound(name: string): SoundEvent | SkipEvent;
|
||||
sound(name: string): SoundEvent | SkipEvent,
|
||||
linlin(a: number, b: number, c: number, d: number): number,
|
||||
linexp(a: number, b: number, c: number, d: number): number,
|
||||
explin(a: number, b: number, c: number, d: number): number,
|
||||
expexp(a: number, b: number, c: number, d: number): number,
|
||||
lincurve(inMin: number, inMax: number,
|
||||
outMin: number, outMax: number,
|
||||
curve: number): number;
|
||||
}
|
||||
}
|
||||
|
||||
export const makeNumberExtensions = (api: UserAPI) => {
|
||||
Number.prototype.z0 = function (options: { [key: string]: any } = {}) {
|
||||
|
||||
Number.prototype.linlin = function(a: number, b: number, c: number, d: number) {
|
||||
if (this.valueOf() < a) return c;
|
||||
if (this.valueOf() > b) return d;
|
||||
return (this.valueOf() - a) / (b - a) * (d - c) + c;
|
||||
};
|
||||
|
||||
Number.prototype.explin = function(a: number, b: number, c: number, d: number) {
|
||||
if (this.valueOf() <= a) return c;
|
||||
if (this.valueOf() >= b) return d;
|
||||
return (Math.log(this.valueOf() / a)) / (Math.log(b / a)) * (d - c) + c;
|
||||
};
|
||||
|
||||
Number.prototype.expexp = function(a: number, b: number, c: number, d: number) {
|
||||
if (this.valueOf() <= a) return c;
|
||||
if (this.valueOf() >= b) return d;
|
||||
return Math.pow(d / c, Math.log(this.valueOf() / a) / Math.log(b / a)) * c;
|
||||
};
|
||||
|
||||
Number.prototype.lincurve = function(
|
||||
inMin: number, inMax: number,
|
||||
outMin: number, outMax: number,
|
||||
curve: number) {
|
||||
if (this.valueOf() <= inMin) return outMin;
|
||||
if (this.valueOf() >= inMax) return outMax;
|
||||
if (Math.abs(curve) < 0.001) {
|
||||
return (this.valueOf() - inMin) / (inMax - inMin) * (outMax - outMin) + outMin;
|
||||
};
|
||||
let grow = Math.exp(curve);
|
||||
let a = outMax - outMin / (1.0 - grow);
|
||||
let b = outMin + a;
|
||||
let scaled = (this.valueOf() - inMin) / (inMax - inMin);
|
||||
return b - (a * Math.pow(grow, scaled))
|
||||
}
|
||||
|
||||
Number.prototype.linexp = function(a: number, b: number, c: number, d: number) {
|
||||
if (this.valueOf() <= a) return c;
|
||||
if (this.valueOf() >= b) return d;
|
||||
return Math.pow(d / c, (this.valueOf() - a) / (b - a)) * c;
|
||||
};
|
||||
|
||||
Number.prototype.z0 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z0(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z1 = function (options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z1 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z1(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z2 = function (options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z2 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z2(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z3 = function (options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z3 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z3(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z4 = function (options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z4 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z4(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z5 = function (options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z5 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z5(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z6 = function (options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z6 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z6(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z7 = function (options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z7 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z7(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z8 = function (options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z8 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z8(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z9 = function (options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z9 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z9(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z10 = function (options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z10 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z10(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z11 = function (options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z11 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z11(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z12 = function (options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z12 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z12(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z13 = function (options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z13 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z13(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z14 = function (options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z14 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z14(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z15 = function (options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z15 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z15(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z16 = function (options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z16 = function(options: { [key: string]: any } = {}) {
|
||||
return api.z16(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.midi = function (...kwargs: any[]) {
|
||||
Number.prototype.midi = function(...kwargs: any[]) {
|
||||
return api.midi(this.valueOf(), ...kwargs);
|
||||
};
|
||||
|
||||
Number.prototype.sound = function (name: string): SoundEvent | SkipEvent {
|
||||
Number.prototype.sound = function(name: string): SoundEvent | SkipEvent {
|
||||
if (Number.isInteger(this.valueOf())) {
|
||||
return (api.sound(name) as SoundEvent).note(this.valueOf());
|
||||
} else {
|
||||
|
||||
81
src/main.ts
@ -5,17 +5,17 @@ import { javascript } from "@codemirror/lang-javascript";
|
||||
import { markdown } from "@codemirror/lang-markdown";
|
||||
import { Extension } from "@codemirror/state";
|
||||
import { outputSocket } from "./IO/OSC";
|
||||
import { getCodeMirrorTheme } from "./EditorSetup";
|
||||
import { getCodeMirrorTheme, switchToDebugTheme } from "./EditorSetup";
|
||||
import {
|
||||
initializeSelectedUniverse,
|
||||
AppSettings,
|
||||
Universe,
|
||||
loadUniverserFromUrl,
|
||||
} from "./FileManagement";
|
||||
import { singleElements, buttonGroups, ElementMap } from "./DomElements";
|
||||
import { singleElements, buttonGroups, ElementMap, createDocumentationStyle } from "./DomElements";
|
||||
import { registerFillKeys, registerOnKeyDown } from "./KeyActions";
|
||||
import { installEditor } from "./EditorSetup";
|
||||
import { documentation_factory } from "./Documentation";
|
||||
import { documentation_factory, documentation_pages, showDocumentation, updateDocumentationContent } from "./Documentation";
|
||||
import { EditorView } from "codemirror";
|
||||
import { Clock } from "./Clock";
|
||||
import { loadSamples, UserAPI } from "./API";
|
||||
@ -31,13 +31,12 @@ import { makeStringExtensions } from "./extensions/StringExtensions";
|
||||
import { installInterfaceLogic } from "./InterfaceLogic";
|
||||
import { installWindowBehaviors } from "./WindowBehavior";
|
||||
import { makeNumberExtensions } from "./extensions/NumberExtensions";
|
||||
// @ts-ignore
|
||||
import { registerSW } from "virtual:pwa-register";
|
||||
import colors from "./colors.json";
|
||||
// @ts-ignore
|
||||
const images = import.meta.glob("./assets/*")
|
||||
|
||||
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
registerSW();
|
||||
}
|
||||
|
||||
export class Editor {
|
||||
// Universes and settings
|
||||
@ -87,6 +86,8 @@ export class Editor {
|
||||
mode: "scope",
|
||||
size: 1,
|
||||
};
|
||||
bindings: any[] = [];
|
||||
documentationStyle: any = {};
|
||||
|
||||
// UserAPI
|
||||
api: UserAPI;
|
||||
@ -212,13 +213,25 @@ export class Editor {
|
||||
loadUniverserFromUrl(this);
|
||||
|
||||
// Set the color scheme for the application
|
||||
let available_themes = Object.keys(colors);
|
||||
if (this.settings.theme in available_themes) {
|
||||
this.readTheme(this.settings.theme);
|
||||
} else {
|
||||
this.settings.theme = "Everblush";
|
||||
this.readTheme(this.settings.theme);
|
||||
this.readTheme(this.settings.theme);
|
||||
|
||||
this.documentationStyle = createDocumentationStyle(this);
|
||||
this.bindings = Object.keys(this.documentationStyle).map((key) => ({
|
||||
type: "output",
|
||||
regex: new RegExp(`<${key}([^>]*)>`, "g"),
|
||||
//@ts-ignore
|
||||
replace: (match, p1) => `<${key} class="${this.documentationStyle[key]}" ${p1}>`,
|
||||
}));
|
||||
|
||||
// Get documentation id from hash parameter
|
||||
const document_id = window.location.hash.slice(1);
|
||||
if (document_id && document_id !== "" && documentation_pages.includes(document_id)) {
|
||||
this.currentDocumentationPane = document_id
|
||||
updateDocumentationContent(this, this.bindings);
|
||||
showDocumentation(this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private getBuffer(type: string): any {
|
||||
@ -546,7 +559,7 @@ export class Editor {
|
||||
console.log("Hydra loaded successfully");
|
||||
this.initializeHydra();
|
||||
};
|
||||
script.onerror = function () {
|
||||
script.onerror = function() {
|
||||
console.error("Error loading Hydra script");
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
@ -563,8 +576,8 @@ export class Editor {
|
||||
enableStreamCapture: false,
|
||||
});
|
||||
this.hydra = this.hydra_backend.synth;
|
||||
this.hydra.setResolution(1280, 768);
|
||||
(globalThis as any).hydra = this.hydra;
|
||||
this.hydra.setResolution(1024, 768);
|
||||
}
|
||||
|
||||
private setCanvas(canvas: HTMLCanvasElement): void {
|
||||
@ -584,25 +597,25 @@ export class Editor {
|
||||
}
|
||||
}
|
||||
|
||||
private updateInterfaceTheme(selected_theme: {[key: string]: string}): void {
|
||||
function hexToRgb(hex: string): {r: number, g: number, b: number} | null {
|
||||
private updateInterfaceTheme(selected_theme: { [key: string]: string }): void {
|
||||
function hexToRgb(hex: string): { r: number, g: number, b: number } | null {
|
||||
let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
};
|
||||
for (const [key, value] of Object.entries(selected_theme)) {
|
||||
let color = hexToRgb(value);
|
||||
if (color) {
|
||||
let colorString = `${color.r} ${color.g} ${color.b}`
|
||||
document.documentElement.style.setProperty("--" + key, colorString);
|
||||
}
|
||||
}
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
};
|
||||
for (const [key, value] of Object.entries(selected_theme)) {
|
||||
let color = hexToRgb(value);
|
||||
if (color) {
|
||||
let colorString = `${color.r} ${color.g} ${color.b}`
|
||||
document.documentElement.style.setProperty("--" + key, colorString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getColorScheme(theme_name: string): {[key: string]: string} {
|
||||
getColorScheme(theme_name: string): { [key: string]: string } {
|
||||
// Check if the theme exists in colors.json
|
||||
let themes: Record<string, { [key: string]: any }> = colors;
|
||||
return themes[theme_name];
|
||||
@ -610,6 +623,10 @@ export class Editor {
|
||||
|
||||
readTheme(theme_name: string): void {
|
||||
// Check if the theme exists in colors.json
|
||||
if (theme_name == "debug") {
|
||||
switchToDebugTheme(this);
|
||||
return
|
||||
}
|
||||
let themes: Record<string, { [key: string]: any }> = colors;
|
||||
let selected_theme = themes[theme_name];
|
||||
if (selected_theme) {
|
||||
|
||||
@ -4,15 +4,16 @@ import viteCompression from "vite-plugin-compression";
|
||||
|
||||
const vitePWAconfiguration = {
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
suppressWarnings: true,
|
||||
},
|
||||
|
||||
workbox: {
|
||||
sourcemap: false,
|
||||
cleanupOutdatedCaches: false,
|
||||
maximumFileSizeToCacheInBytes: 10000000,
|
||||
globPatterns: [
|
||||
"**/*.{js,js.gz,css,html,gif,png,json,woff,woff2,json,ogg,wav,mp3,ico,png,svg}",
|
||||
"favicon/*.{js,js.gz,css,html,gif,png,json,woff,woff2,json,ogg,wav,mp3,ico,png,svg}",
|
||||
],
|
||||
runtimeCaching: [
|
||||
{
|
||||
@ -35,14 +36,9 @@ const vitePWAconfiguration = {
|
||||
},
|
||||
],
|
||||
},
|
||||
includeAssets: [
|
||||
"favicon/favicon.icon",
|
||||
"favicon/apple-touch-icon.png",
|
||||
"mask-icon.svg",
|
||||
],
|
||||
manifest: "manifest.webmanifest",
|
||||
manifest: false,
|
||||
registerType: "autoUpdate",
|
||||
injectRegister: "auto",
|
||||
injectRegister: "script-defer",
|
||||
};
|
||||
|
||||
export default defineConfig(({ command, mode, ssrBuild }) => {
|
||||
@ -54,6 +50,13 @@ export default defineConfig(({ command, mode, ssrBuild }) => {
|
||||
port: 8000,
|
||||
strictPort: true,
|
||||
},
|
||||
build: {
|
||||
outDir: "dist",
|
||||
emptyOutDir: true,
|
||||
cssCodeSplit: true,
|
||||
cssMinify: true,
|
||||
minify: true,
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
|
||||
31
yarn.lock
@ -2203,7 +2203,7 @@ fast-glob@^3.2.12:
|
||||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.4"
|
||||
|
||||
fast-glob@^3.3.1:
|
||||
fast-glob@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
|
||||
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
|
||||
@ -3609,6 +3609,13 @@ ts-interface-checker@^0.1.9:
|
||||
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
|
||||
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
|
||||
|
||||
ts-tonnetz@^0.0.84:
|
||||
version "0.0.84"
|
||||
resolved "https://registry.yarnpkg.com/ts-tonnetz/-/ts-tonnetz-0.0.84.tgz#29a87378e4b7eddd54448556213a5787eb4e915f"
|
||||
integrity sha512-s6heaLn+BRM3CA0VzTMd9UIMsgIpNTumcwjQHOgp51IgGw4EQKTnfPQXXlmoTriZByO9tn5+Ykgq1m9eHpGlxw==
|
||||
dependencies:
|
||||
typescript "5.2.2"
|
||||
|
||||
tslib@^2.3.1, tslib@^2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410"
|
||||
@ -3658,7 +3665,7 @@ typed-array-length@^1.0.4:
|
||||
for-each "^0.3.3"
|
||||
is-typed-array "^1.1.9"
|
||||
|
||||
typescript@^5.2.2:
|
||||
typescript@5.2.2, typescript@^5.2.2:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
|
||||
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
|
||||
@ -3774,13 +3781,13 @@ vite-plugin-markdown@^2.1.0:
|
||||
htmlparser2 "^6.0.0"
|
||||
markdown-it "^12.0.0"
|
||||
|
||||
vite-plugin-pwa@^0.16.7:
|
||||
version "0.16.7"
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-pwa/-/vite-plugin-pwa-0.16.7.tgz#3dcacc342766ff3598472ac7d5e0782d14e2853e"
|
||||
integrity sha512-4WMA5unuKlHs+koNoykeuCfTcqEGbiTRr8sVYUQMhc6tWxZpSRnv9Ojk4LKmqVhoPGHfBVCdGaMo8t9Qidkc1Q==
|
||||
vite-plugin-pwa@^0.17.4:
|
||||
version "0.17.4"
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-pwa/-/vite-plugin-pwa-0.17.4.tgz#be3b3714d4148681bc73e8e0b1e6ce1a71fa79f2"
|
||||
integrity sha512-j9iiyinFOYyof4Zk3Q+DtmYyDVBDAi6PuMGNGq6uGI0pw7E+LNm9e+nQ2ep9obMP/kjdWwzilqUrlfVRj9OobA==
|
||||
dependencies:
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.1"
|
||||
fast-glob "^3.3.2"
|
||||
pretty-bytes "^6.1.1"
|
||||
workbox-build "^7.0.0"
|
||||
workbox-window "^7.0.0"
|
||||
@ -4033,10 +4040,12 @@ yaml@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
|
||||
integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
|
||||
|
||||
zifferjs@^0.0.55:
|
||||
version "0.0.55"
|
||||
resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.55.tgz#ff7d08c9afde6cb78649f585b5a2c97ee4c97f22"
|
||||
integrity sha512-QO/xWN3RugMbusIYxB7H1aHSm1w8OD1leEseJcDwxBx9VxTBWZF9SrxGbNdRowFAIfFg9b4hpOYmMSQYqi87EA==
|
||||
zifferjs@^0.0.62:
|
||||
version "0.0.62"
|
||||
resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.62.tgz#77dc18076984836fd00c54bef46dfe029b834307"
|
||||
integrity sha512-OCT4Hq79kgSjP8bFiurB/T2poJtagDLgwjobIrGwdCiDixP31ereGalNoqqiHHBrjOg6Gj8m9AFjzCrup+4osA==
|
||||
dependencies:
|
||||
ts-tonnetz "^0.0.84"
|
||||
|
||||
zyklus@^0.1.4:
|
||||
version "0.1.4"
|
||||
|
||||