From 370303f75d1d6fd91aaf595ba5b1fe44bb5b0b9d Mon Sep 17 00:00:00 2001 From: Thea Flowers Date: Tue, 27 Feb 2024 13:44:31 -0500 Subject: [PATCH] user guide: update to Wintersong theme --- user_guide/docs/build.md | 184 +++--- user_guide/docs/index.md | 142 ++-- user_guide/docs/scripts/forms.js | 505 +++++++++++++++ user_guide/docs/scripts/gemini.js | 4 +- user_guide/docs/scripts/layered.js | 42 -- user_guide/docs/scripts/settings.js | 88 +-- user_guide/docs/scripts/svgmap.js | 128 ---- user_guide/docs/scripts/utils.js | 169 +++++ user_guide/docs/scripts/waveforms.js | 4 +- user_guide/docs/settings.md | 610 +++++++----------- user_guide/docs/styles/io-legend-svgmap.css | 107 --- .../docs/styles/kit-contents-svgmap.css | 37 -- user_guide/docs/styles/layered.css | 51 -- user_guide/docs/styles/settings.css | 20 - user_guide/docs/styles/svgmap.css | 9 - user_guide/docs/styles/waveforms.css | 85 +-- user_guide/mkdocs.yml | 47 +- user_guide/requirements.txt | 7 +- 18 files changed, 1151 insertions(+), 1088 deletions(-) create mode 100644 user_guide/docs/scripts/forms.js delete mode 100644 user_guide/docs/scripts/layered.js delete mode 100644 user_guide/docs/scripts/svgmap.js create mode 100644 user_guide/docs/scripts/utils.js delete mode 100644 user_guide/docs/styles/io-legend-svgmap.css delete mode 100644 user_guide/docs/styles/kit-contents-svgmap.css delete mode 100644 user_guide/docs/styles/layered.css delete mode 100644 user_guide/docs/styles/settings.css delete mode 100644 user_guide/docs/styles/svgmap.css diff --git a/user_guide/docs/build.md b/user_guide/docs/build.md index d15211cb..fa678cb2 100644 --- a/user_guide/docs/build.md +++ b/user_guide/docs/build.md @@ -34,44 +34,25 @@ Before jumping in, make sure you have: Your kit should contain the following items. If any are missing please email us at support@winterbloom.com. - - - - - - - - - - - - - - - - - - - - - - - + + + Faceplate + Mainboard + Expander board + Expander faceplate + Rubber bands (2) + Expander cable + Tactile switch + Tactile switch cap + Eurorack power header + Nuts for 1/8" jacks (13) + 1/8" jacks (13) + Small knobs (4) + Big knobs (2) + Tall trimmer pots (6) + 9mm pots (6) + Washers and nuts for 9mm pots (6) + - (1) Mainboard - (1) Faceplate @@ -102,11 +83,11 @@ The power connector goes on the **back** side of the board. When placing **note Once placed, make sure to push it flush against the board and then solder the 10 pins on the front side of the board. Be careful here and avoid touching the small components near the pins with your iron. -
- - - -
+ + + + + !!! warning "Watch out for those LEDs!" Avoid touching the LEDs with your iron- they really don't like being melted and they're very hard to replace. @@ -125,14 +106,14 @@ One of the six pots has been modified - its pins are shorter and bent outwards. Place five of the six pots onto the mainboard in the spots labeled `pitch`, `duty`, and `lfo`. You may need to bend or straighten the mounting legs on the pots to get them in place. -
- + + -
+ Take the remaining, modified pot and place it on the spot labeled `crossfade`. This pot will not be as secure as the others, but make sure the pins line up with the pads on the as board shown below. @@ -140,35 +121,35 @@ Take the remaining, modified pot and place it on the spot labeled `crossfade`. T Next, take one of the 1/8" jacks and place it into the spot at the center bottom edge of the board labeled `mix`: -
- + + -
+ Next, carefully place the faceplate onto the front of the module. Make sure all of the pots and the jack are aligned and resting in their respective holes. -
- + + -
+ Take one of the rubber bands and wrap it around the board and faceplate twice to hold the faceplate in place during the next few steps. -
- + + -
+ Flip the whole thing upside-down and solder the pots and jack in place. -
- + + -
+ !!! warning Take extra care not to hit any of the surface mount components with your iron. @@ -179,11 +160,11 @@ Next, flip the assembly rightside-up and remove the rubber bands and faceplate. Finally, solder the legs of the modified pot to the pads on the board. Take care not to accidentally bridge the legs together. If you find that the legs are misaligned, you can flip the board back over and melt the solder on the mounting legs and twist it back into alignment. -
- + + -
+ ## Tactile switch @@ -197,10 +178,10 @@ Place the tactile switch on the spot labeled `btn` on the mainboard. Make sure i Next, place the cap on top of the tactile switch and press in firmly to completely seat it on the switch. -
- + + -
+ ## Jacks and trimpots @@ -210,38 +191,38 @@ Next up is all six trimpots and six of the 1/8" jacks. Start from the **right** side of the board and place the pots and jacks for `ramp`, `pulse`, `duty`, `sub`, `pitch`, and `out`. -
- + + -
+ Continue on the left side with the pots and jacks for `pulse`, `ramp`, `sub`, `duty`, `out`, and `pitch`. -
- + + -
+ Next, place the faceplate and rubber band just as you did earlier. -
- + + -
+ Flip the module upside-down and solder the pots and jacks into place. -
- + + -
+ ## Nuts @@ -251,19 +232,19 @@ Now that everything is soldered in place the next step is to secure the faceplat Place a washer on the shaft of each of the six 9mm pots. -
- + + -
+ With the washers in place, place the nuts onto the shafts and tighten them in place. -
- + + -
+ Next up is the seven hex nuts for the 1/8 jacks. @@ -271,11 +252,11 @@ Next up is the seven hex nuts for the 1/8 jacks. Place the nuts on each jack and tighten them in place. -
- + + -
+ ## Knobs @@ -290,11 +271,11 @@ Start by loosening the set screw on each of the knobs using a small flat head sc Next, turn all of the potentiometers fully counterclockwise. Place the two large knobs on the top two shafts with the indicator line at the 7 o' clock position. Tighten the set screws to secure the knobs in place. -
- + + -
+ Repeat the same process for the four smaller knobs. @@ -316,31 +297,31 @@ To assemble the expander, you'll need the expander faceplate, six 1/8" jacks, si Start by placing all six of the 1/8" jacks onto the front side of the expander board. -
- + + -
+ Next, place the faceplate onto the jacks and temporarily hold it in place using a rubber band. -
- + + -
+ Flip it upside-down and solder all of the jacks into place. -
- + + -
+ Flip it rightside-up and secure the faceplate using the hex nuts. -
- + + -
+ & you're finished! @@ -349,6 +330,3 @@ Flip it rightside-up and secure the faceplate using the hex nuts. Congrats on building your very own Castor & Pollux, we hope you had a lovely time! Don't forget to go check out the [User's Guide](/). We'd love to see your work, feel free to tag us on social media - we're `@wntrblm` on [Twitter](https://twitter.com/wntrblm) and [Instagram](https://instagram.com/wntrblm). If you have any feedback or ran into any issues, feel free to drop us an email at support@winterbloom.com or file a issue on [GitHub](https://github.com/wntrblm/Castor_and_Pollux). - - - diff --git a/user_guide/docs/index.md b/user_guide/docs/index.md index ae9883b6..3c7cfa2f 100644 --- a/user_guide/docs/index.md +++ b/user_guide/docs/index.md @@ -35,7 +35,7 @@ We want you to have a wonderful experience with your module. If you need help or ## Version differences -![Illustration of Castor & Pollux I and Castor & Pollux II](images/V1%20vs%20V2.svg){.dark-invert} +![Illustration of Castor & Pollux I and Castor & Pollux II](images/V1%20vs%20V2.svg) Castor & Pollux has two different versions in the wild with significant changes between them. Fortunately, both versions are fundamentally the same brains with different user interfaces. Changes made by Castor & Pollux II include: @@ -54,7 +54,7 @@ Both versions use the same firmware and behave the same way. This manual applies ## Installation -![Illustration of power connection](images/Power%20connection.svg){.dark-invert} +![Illustration of power connection](images/Power%20connection.svg) To install this into your Eurorack setup, connect a Eurorack power cable from your power supply to the back of the module. **Note that even though there's a keyed power connector on the module, double check that the red stripe is on the side labeled `red stripe`!** Once you've connected the power cable, secure your module to your rack rails using screws. @@ -62,40 +62,40 @@ To install this into your Eurorack setup, connect a Eurorack power cable from yo Castor & Pollux has two separate but _intertwined_ oscillators. It's possible to control each oscillator independently, but they truly shine when used together. Castor & Pollux's design is focused around the connection between these two oscillators and modulating their parameters using the internal LFO. -![Illustration of Castor & Pollux's interface with elements for the separate oscillators highlighted](images/1%20-%20Overview.svg){.dark-invert} +![Illustration of Castor & Pollux's interface with elements for the separate oscillators highlighted](images/1%20-%20Overview.svg) Castor & Pollux's front panel is arranged so that Castor's inputs, outputs, and controls are on the **left** whereas the corresponding elements are mirrored on the **right** for Pollux. The center contains controls for the internal LFO and crossfade mixer: -Each oscillator, `⍺` and `β`, is controlled by its associated knobs and CV input jacks: +Each oscillator, ++"⍺"++ and ++"β"++, is controlled by its associated knobs and CV input jacks: -![Illustration of the knobs and input jacks](images/2%20-%20Osc%20controls.svg){.dark-invert} +![Illustration of the knobs and input jacks](images/2%20-%20Osc%20controls.svg) - The large pitch knob and pitch CV input control the oscillator's **pitch** (frequency). - The smaller pulse width knob and pulse width CV input control the oscillator's **pulse width**. You can learn more about these inputs in the [pitch behavior](#pitch-behavior) section. -Each oscillator has a single output jack, `★`, and corresponding mixing knobs: +Each oscillator has a single output jack, ++"★"++, and corresponding mixing knobs: -![Illustration of the oscillator's outputs](images/3%20-%20Osc%20outputs.svg){.dark-invert} +![Illustration of the oscillator's outputs](images/3%20-%20Osc%20outputs.svg) Each oscillator generates three waveshapes: _saw_, _pulse_, and _sub_. The three mixing knobs control how much of each waveshape is present in the oscillator's output. You can read more about the sound of each waveshape in the [waveshapes](#waveshapes) section. Next, the _crossfade mixer_ combines the output of the two oscillators together: -![Illustration of the crossfade mixer and its output jack](images/4%20-%20Crossfader.svg){.dark-invert} +![Illustration of the crossfade mixer and its output jack](images/4%20-%20Crossfader.svg) -The crossfader's knob, `Σ`, determines which oscillator is more prominent in the mix at the crossfade output, `♊️`. The [oscillator stacking](#oscillator-stacking) section has more details and sound samples of combining the oscillators together. +The crossfader's knob, ++"Σ"++, determines which oscillator is more prominent in the mix at the crossfade output, `♊️`. The [oscillator stacking](#oscillator-stacking) section has more details and sound samples of combining the oscillators together. -Next up, in the very middle there's the LFO knob, `φ`: +Next up, in the very middle there's the LFO knob, ++"φ"++: -![Illustration of the LFO knob](images/5%20-%20LFO.svg){.dark-invert} +![Illustration of the LFO knob](images/5%20-%20LFO.svg) This [internal LFO](#internal-low-frequency-oscillator) can be used to modulate several parameters in interesting ways depending on the [mode](#modes--tweaking). Last, but not least, there is a single button in the center that's used for changing modes and enabling the tweak overlay: -![Illustration of the button](images/6%20-%20Button.svg){.dark-invert} +![Illustration of the button](images/6%20-%20Button.svg) You can learn more about the different modes and tweaking in the [modes & tweaking section](#modes--tweaking). @@ -107,7 +107,7 @@ If you're not sure where to start with Castor & Pollux this section has a few pa First, dip your toes in by putting together this patch: -![Illustration of the first patch](images/7%20-%20Patch%201.svg){.dark-invert} +![Illustration of the first patch](images/7%20-%20Patch%201.svg) - Turn all the knobs and trimpots fully counter-clockwise - Turn the ramp mix trimpot for Castor fully clockwise @@ -121,7 +121,7 @@ You can keep playing around with this patch by moving the mix trimpots for each Ready for more? Try out this patch: -![Illustration of the second patch](images/8%20-%20Patch%202.svg){.dark-invert} +![Illustration of the second patch](images/8%20-%20Patch%202.svg) - Turn Castor's pitch knob and pulse knob to 12 o' clock - Turn at least one of Castor's mix trimpots clockwise @@ -134,7 +134,7 @@ Castor should now be playing notes based on the CV you're sending it. You can tu Okay, one last patch and you'll be ready for anything: -![Illustration of the third patch](images/9%20-%20Patch%203.svg){.dark-invert} +![Illustration of the third patch](images/9%20-%20Patch%203.svg) - Turn Castor's pitch knob and pulse knob to 12 o' clock - Turn Pollux's pitch knob and pulse knob to 12 o' clock @@ -158,13 +158,13 @@ Due to the intertwined nature of Castor & Pollux's oscillators, the effect of th ### Coarse -![Illustration of coarse pitch behavior](images/8%20-%20Coarse.svg){.dark-invert} +![Illustration of coarse pitch behavior](images/8%20-%20Coarse.svg) When [nothing](#jack-detection) is patched into Castor's pitch CV jack, `Coarse` behavior is used. Castor's pitch is determined by its pitch knob which sweeps through six octaves and quantizes to the nearest semitone. ### Fine -![Illustration of fine pitch behavior](images/9%20-%20Fine.svg){.dark-invert} +![Illustration of fine pitch behavior](images/9%20-%20Fine.svg) If there is a signal patched into Castor's pitch CV jack, `Fine` behavior is used. The input CV should be between `0 V` and `6 V`. The pitch knob offsets the input CV by `±1 octave`. The pitch knob has a sort of ["virtual notch"](#tuning) at the 12 o' clock position to help you dial in the frequency you're looking for. @@ -172,7 +172,7 @@ Pollux also uses `Fine` behavior but _follows_ Castor if nothing is patched into ### Multiply -![Illustration of multiply pitch behavior](images/10%20-%20multiply.svg){.dark-invert} +![Illustration of multiply pitch behavior](images/10%20-%20multiply.svg) Finally, Pollux uses `Multiply` behavior when in [Hard Sync](#hard-sync) mode. In this case, Pollux follows Castor and the knob adds up to three octaves. @@ -196,17 +196,15 @@ To make tuning easier, Castor & Pollux provides two ways of using the pitch knob 440 Hz
-
- - -
+ +
First, the pitch knobs have a "virtual notch" because of their **non-linear** response: they're less sensitive in the middle of their range than the edges. It's usually easier to understand this visually, so try out the little illustration above and notice that with the non-linear response it's much easier to tune to frequencies around `440 Hz`. This non-linear response only happens when the oscillator is using the `Follow` [pitch behavior](#pitch-behavior). You can configure how strong this effect is using the [settings editor](#editing-module-settings). -![Illustration of tweak mode pitch tuning](images/11%20-%20extra%20fine.svg){.dark-invert} +![Illustration of tweak mode pitch tuning](images/11%20-%20extra%20fine.svg) Second, the [tweak](#modes--tweaking) overlay allows extra-fine control over tuning. Holding down the button and turning the pitch knob allows you to apply an additional `±2.5 semitone` offset. This offset is applied to the oscillator regardless of the [pitch behavior](#pitch-behavior) or the current [mode](#modes--tweaking). @@ -228,24 +226,18 @@ click the image to start and stop the animation The ramp sounds like this: -
- -
+ -
- -
+ The pulse wave depends on the pulse width CV and knob. You can vary the pulse width in this animation to see how it affects the waveshape:
-
- - -
+ +
@@ -253,17 +245,11 @@ You can also use [internal low-frequency oscillator](#internal-low-frequency-osc Here are some examples of the pulse wave's sounds: -
- -
+ -
- -
+ -
- -
+ Finally, there's the sub waveshape. It's a square wave that's one octave lower. Here's what it looks like: @@ -273,13 +259,9 @@ Finally, there's the sub waveshape. It's a square wave that's one octave lower. And here are some sound samples of the sub waveshape: -
- -
+ -
- -
+ These waveshapes can be mixed together to produce much more complex and interesting variants - try playing with the sliders under this animation to see how it affects the waveshape: @@ -311,17 +293,11 @@ These waveshapes can be mixed together to produce much more complex and interest Here are some sound samples of various mixes: -
- -
+ -
- -
+ -
- -
+ ## Oscillator stacking @@ -347,17 +323,11 @@ You can play around with this interactive animation to see how detuning and mixi This animation just uses the ramp waveshape, but the crossfader takes the mix from each oscillator's mixer, so you can combine many different waveshapes. Here are some sound samples of oscillator stacking: -
- -
+ -
- -
+ -
- -
+ ## Internal low-frequency oscillator @@ -376,11 +346,11 @@ Castor & Pollux has four different _modes_ that change the module's overall fun - [LFO FM](#lfo-fm-mode) mode uses the internal LFO to modulate both oscillator's frequency. - [Hard Sync](#hard-sync-mode) mode produces metallic sounds by syncing Pollux's ramp core to Castor's. -![Illustration of tapping the button](images/12%20-%20tap%20button.svg){.dark-invert} +![Illustration of tapping the button](images/12%20-%20tap%20button.svg) To cycle between modes, **tap** the button in the middle and the module will play a short animation to show that it has switched modes. -![Illustration of holding the button](images/13%20-%20hold%20button.svg){.dark-invert} +![Illustration of holding the button](images/13%20-%20hold%20button.svg) On the other hand, **holding** the button turns on the tweak overlay. This gives you access to additional parameters depending on the mode. When moving in and out of the tweak overlay, the knobs get "latched" so that they don't immediately cause changes - similar how many synthesizers work when loading patches. The parameter only starts changing once you've moved the knob. In all modes, the pitch knobs control the extra-fine [tuning](#tuning). @@ -388,11 +358,11 @@ On the other hand, **holding** the button turns on the tweak overlay. This gives Castor & Pollux's default mode is the _Chorus_ mode. This mode is inspired by the original Juno's analog chorus circuit, however, instead of applying the chorus affect _after_ sound generation, Castor & Pollux's chorusing works by varying the frequency of the second oscillator using its [internal low-frequency oscillator](#internal-low-frequency-oscillator). This means you have to [use both oscillators](#oscillator-stacking) to hear this effect and it works best if Pollux is _following_ Castor's pitch. -![Illustration of chorus controls](images/14%20-%20lfo%20controls.svg){.dark-invert} +![Illustration of chorus controls](images/14%20-%20lfo%20controls.svg) The LFO knob, `φ`, determines the intensity of chorusing from none when fully counter-clockwise to its maximum at fully clockwise. The crossfade mixer, `Σ`, also has an impact on the intensity of the chorus. -![Illustration of chorus tweak controls](images/15%20-%20lfo%20tweaks.svg){.dark-invert} +![Illustration of chorus tweak controls](images/15%20-%20lfo%20tweaks.svg) When holding the tweak button, the LFO knob, `φ`, controls the LFO's frequency. @@ -411,19 +381,17 @@ You can play around with this interactive animation to see how the chorusing amo Here are some sound samples of chorusing: -
- -
+ ### LFO PWM LFO PWM mode uses the internal LFO to modulate the _pulse width_ of each oscillator. This only affects the _pulse_ [waveshape](#waveshapes). -![Illustration of the LFO PWM controls](images/16%20-%20lfo%20pwm.svg){.dark-invert} +![Illustration of the LFO PWM controls](images/16%20-%20lfo%20pwm.svg) The LFO knob, `φ`, determines the _frequency_ of the internal LFO. Meanwhile, each oscillator's pulse width knob controls the depth of modulation from none when fully counter-clockwise to its maximum at fully clockwise. Any signal patched into the pulse width jack is summed with the knob. -![Illustration of the LFO PWM tweak controls](images/17%20-%20lfo%20pwm%20tweaks.svg){.dark-invert} +![Illustration of the LFO PWM tweak controls](images/17%20-%20lfo%20pwm%20tweaks.svg) When holding the tweak button, each oscillator's pulse width knob controls the _center_ of the pulse width modulation. @@ -431,13 +399,13 @@ When holding the tweak button, each oscillator's pulse width knob controls the _ LFO FM mode uses the internal LFO to modulate the _pitch_ for each oscillator. This is similar to the [Chorus mode](#chorus-mode), except it applies to both oscillators instead of just Pollux. -![Illustration of LFO FM controls](images/18%20-%20lfo%20fm.svg){.dark-invert} +![Illustration of LFO FM controls](images/18%20-%20lfo%20fm.svg) The LFO knob, `φ`, determines the _frequency_ of the internal LFO. Meanwhile, each oscillator's pulse width knob controls the depth of pitch modulation from none when fully counter-clockwise to its maximum at fully clockwise. Unlike the LFO PWM mode, the pulse width jack has no impact on modulation. -![Illustration of LFO FM tweak controls](images/19%20-%20lfo%20pwm%20tweaks.svg){.dark-invert} +![Illustration of LFO FM tweak controls](images/19%20-%20lfo%20pwm%20tweaks.svg) When holding the tweak button, each oscillator's pulse width knob controls the oscillator's pulse width. @@ -453,9 +421,7 @@ Since hard sync only affects Pollux, you'll have to use either Pollux's output o Here are some sound samples of hard sync: -
- -
+ Hard sync mode's controls are the same as [chorus mode](#chorus-mode) except that Pollux's pitch knob uses the [Multiply behavior](#pitch-behavior): @@ -466,11 +432,11 @@ Hard sync mode's controls are the same as [chorus mode](#chorus-mode) except tha ## Expander -![Illustration of C&P & expander next to each other](images/22%20-%20expander.svg){.dark-invert} +![Illustration of C&P & expander next to each other](images/22%20-%20expander.svg) Castor & Pollux II includes a small expander that provides individual output jacks for each oscillator's [waveshapes](#waveshapes). -![Illustration of connecting the expander to C&P](images/Expander%20connection.svg){.dark-invert} +![Illustration of connecting the expander to C&P](images/Expander%20connection.svg) To use the expander, connect the small ribbon cable to the back of Castor & Pollux in the header labeled `Expander`. Connect the other end to the matching header on the back of the expander. Secure the expander to your case using screws. @@ -484,7 +450,7 @@ To use the expander, connect the small ribbon cable to the back of Castor & Poll You can connect Castor & Pollux to your computer using a standard micro USB cable, which lets you [edit settings](#editing-module-settings) and [update the firmware](#updating-the-firmware). -![Illustration of connecting Castor & Pollux to USB](images/USB%20connection.svg){.dark-invert} +![Illustration of connecting Castor & Pollux to USB](images/USB%20connection.svg) The micro USB port is located on the backside of the module. Once you've connected a cable, be careful not to put too much stress on the connector as it's possible to damage the connector with enough force. @@ -513,7 +479,7 @@ Castor & Pollux is completely open source and hacking is encouraged. - The [firmware][firmware source] is available under the [MIT License]. Note that the firmware uses some third-party libraries that are under different, but compatible terms. Read the full text of the license for more details. - The [hardware designs][hardware source] are available under the permissive [CERN-OHL-P v2] license, and is designed using [KiCAD], which is also free and open source. You can open the hardware files using KiCAD, or you can download a PDF of the [schematics]. -![Open Source Hardware Association mark](images/oshw.svg){class=oshw} Castor & Pollux is [certified open source hardware][oshwa certification]. +![Open Source Hardware Association mark](images/oshw.svg){:.small .inline} Castor & Pollux is [certified open source hardware][oshwa certification]. [firmware source]: https://github.com/wntrblm/Castor_and_Pollux/tree/main/firmware [hardware source]: https://github.com/wntrblm/Castor_and_Pollux/tree/main/hardware @@ -575,7 +541,3 @@ Castor & Pollux would not be possible without the help of the Adafruit, support - - - - diff --git a/user_guide/docs/scripts/forms.js b/user_guide/docs/scripts/forms.js new file mode 100644 index 00000000..f6b26939 --- /dev/null +++ b/user_guide/docs/scripts/forms.js @@ -0,0 +1,505 @@ +/* + Copyright (c) 2021 Alethea Katherine Flowers. + Published under the standard MIT License. + Full text available at: https://opensource.org/licenses/MIT +*/ + +import { DOM } from "/winter.js"; +import { $on, ObjectHelpers } from "./utils.js"; + +const $ = DOM.$; + +/* + Two-way databinding for a form input. + + Whenever the input changes, `data[key]` is updated with the value from the + form. + + To go the other direction - update the form when `data[key]` changes, call + `update_value()`. + + This can also display validation messages. It looks for an element with + data-validation-message-for="${id|name}" within the containing
. +*/ +export class InputBinding { + constructor(elem, data, key = undefined) { + this.elem = $(elem); + this.name = this.elem.name || this.elem.id; + this.data = data; + + if (key === undefined) { + key = this.elem.name; + } + + this.key = key; + + this.bind(); + + this.setup_validation(); + } + + bind() { + this.update_value(); + + $on(this.elem, "input", () => { + this.update_data(); + }); + } + + value_to_data(value) { + return value; + } + + data_to_value(value) { + return value; + } + + update_value() { + const value = ObjectHelpers.get_property_by_path( + this.data, + this.key, + false + ); + if (value === undefined) { + return; + } + this.elem.value = this.data_to_value(value); + this.update_validation(); + this.elem.dispatchEvent( + new Event("winterjs:update", { bubbles: false }) + ); + } + + update_data() { + ObjectHelpers.set_property_by_path( + this.data, + this.key, + this.value_to_data(this.elem.value), + false + ); + } + + get validation_enabled() { + return this.elem.classList.contains("is-validation-enabled"); + } + + set validation_enabled(value) { + if (value) { + this.elem.classList.add("is-validation-enabled"); + } else { + this.elem.classList.remove("is-validation-enabled"); + } + } + + setup_validation() { + this.validation_enabled = false; + const form = this.elem.closest("form"); + if (form !== null) { + this.validation_message = + form.querySelector( + `[data-validation-message-for="${this.elem.id}"]` + ) || + form.querySelector( + `[data-validation-message-for="${this.elem.name}"]` + ); + } + + $on(this.elem, "blur", () => { + this.validation_enabled = true; + this.set_backend_error(""); + this.update_validation(this.elem.checkValidity()); + }); + + $on(this.elem, "invalid", () => { + this.update_validation(false); + }); + } + + set_backend_error(message) { + this.validation_enabled = true; + this.elem.setCustomValidity(message); + this.update_validation(false); + } + + update_validation(valid) { + if (!this.validation_message || !this.validation_enabled) { + return; + } + + if (valid === undefined) { + valid = this.elem.checkValidity(); + } + + if (valid) { + this.validation_message.innerText = ""; + } else { + this.validation_message.innerText = this.elem.validationMessage; + } + } +} + +/* Two-way databinding for plain-o-text fields */ +export class TextInputBinding extends InputBinding {} + +/* Two-way databinding for number inputs with a min & max property. */ +export class MixMaxInputBinding extends InputBinding { + update_data() { + const min = parseFloat(this.elem.min); + const max = parseFloat(this.elem.max); + + if (!isNaN(min) && !isNaN(max)) { + const value = this.elem.valueAsNumber; + if (value < min) { + this.elem.value = min; + } + if (value > max) { + this.elem.value = max; + } + } + + ObjectHelpers.set_property_by_path( + this.data, + this.key, + this.value_to_data(this.elem.value), + false + ); + } +} + +export class IntInputBinding extends MixMaxInputBinding { + value_to_data(value) { + return parseInt(value); + } +} + +export class FloatInputBinding extends MixMaxInputBinding { + constructor(elem, data, key, precision) { + super(elem, data, key); + this.precision = parseInt(precision || 2); + this.update_value(); + } + + value_to_data(value) { + return parseFloat(value); + } + + data_to_value(value) { + return value.toFixed(this.precision); + } +} + +/* Two-way databinding for checkbox inputs. */ +export class CheckboxInputBinding extends InputBinding { + update_value() { + this.elem.checked = ObjectHelpers.get_property_by_path( + this.data, + this.key + ) + ? true + : false; + this.elem.dispatchEvent( + new Event("winterjs:update", { bubbles: false }) + ); + } + + update_data() { + ObjectHelpers.set_property_by_path( + this.data, + this.key, + this.elem.checked + ); + } +} + +/* Two-day databinding for select fields */ +export class SelectInputBinding extends InputBinding { + constructor(elem, data, options) { + super(elem, data); + this.type = elem.dataset.bindType; + this.placeholder = this.elem.getAttribute("placeholder"); + + if (options === undefined) { + const options_key = elem.dataset.bindOptions; + if (options_key !== undefined) { + this.options = ObjectHelpers.get_property_by_path( + data, + options_key + ); + this.update_options(); + } + } else { + this.options = options; + this.update_options(); + } + + this.update_value(); + this.setup_placeholder(); + } + + update_options() { + DOM.removeAllChildren(this.elem); + if (this.placeholder) { + const option = document.createElement("option"); + option.value = ""; + option.disabled = true; + option.selected = true; + option.hidden = true; + option.innerText = this.placeholder; + this.elem.appendChild(option); + } + + for (const [val, name] of Object.entries(this.options)) { + const option = document.createElement("option"); + option.value = val; + option.innerText = name; + this.elem.appendChild(option); + } + } + + /* God, placeholders for + + percent + + + `data-display-format` can be: + + - `float`, with `data-display-precision` controlling rounding. + - `percent` + + If advanced formatting is needed, use `data-display-formatter`: + + data-display-formatter="input.valueAsNumber * 2" +*/ +export class ValueDisplay { + constructor(display_elem, formatter = null, target_elem = null) { + this.display_elem = $(display_elem); + + if (target_elem) { + target_elem = $(target_elem); + } else { + const target_id = this.display_elem.dataset.displayValueFor; + target_elem = + $(target_id) || document.querySelector(`[name="${target_id}"]`); + } + if (!target_elem) { + console.error( + "Could not find target element for ValueDisplay", + this.display_elem + ); + } + this.target_elem = target_elem; + + this.setup_formatter(formatter); + + $on(target_elem, "input", () => this.update()); + $on(target_elem, "winterjs:update", () => this.update()); + this.update(); + } + + setup_formatter(formatter) { + if (formatter) { + this.formatter = formatter; + return; + } + switch (this.display_elem.dataset.displayFormat) { + case "float": { + let precision = + parseInt(this.display_elem.dataset.displayPrecision, 10) || + 2; + this.formatter = (input) => + input.valueAsNumber.toFixed(precision); + break; + } + case "percent": + this.formatter = (input) => Math.round(input.value * 100); + break; + default: + this.formatter = (input) => input.value; + break; + } + if (this.display_elem.dataset.displayFormatter) { + this.formatter = new Function( + "input", + `"use strict"; return ${this.display_elem.dataset.displayFormatter}` + ); + } + } + + update() { + this.display_elem.innerText = this.formatter(this.target_elem); + } +} + +/* + Bind the controls in the given `form` to the given `data`. + + The form controls must have a `data-bind` attribute to be wired up. For + input type="text" the `data-bind-type` attribute can be set to `text`, `int`, + or `float`. + + Also binds any value displays (elements with `data-display-value-for`). +*/ +export class Form { + constructor(form_elem, data) { + this.elem = $(form_elem); + this.submit_btn = this.elem.querySelector("button[type=submit]"); + this.bindings = []; + this.fields = {}; + + this._setup_submit(); + this._bind_all(data); + this._bind_all_displays(); + } + + _setup_submit() { + $on(this.elem, "submit", (e) => { + if (this.elem.dataset.noSubmit !== undefined) { + e.preventDefault(); + } + if (this.submit_btn !== null) { + this.submit_btn.classList.add("is-validation-enabled"); + } + for (const binding of this.bindings) { + binding.validation_enabled = true; + } + }); + } + + _bind_all(data) { + for (const elem of this.elem.querySelectorAll( + "input[data-bind], select[data-bind], textarea[data-bind]" + )) { + this.bind_one(elem, data); + } + } + + bind_one(elem, data) { + let binding = null; + + switch (elem.tagName) { + case "INPUT": { + if (elem.type === "checkbox") { + binding = new CheckboxInputBinding(elem, data); + } else { + switch (elem.dataset.bindType) { + case "int": + binding = new IntInputBinding(elem, data); + break; + case "float": + binding = new FloatInputBinding(elem, data); + break; + default: + binding = new TextInputBinding(elem, data); + break; + } + } + break; + } + + case "TEXTAREA": { + binding = new TextInputBinding(elem, data); + break; + } + + case "SELECT": { + binding = new SelectInputBinding(elem, data); + break; + } + + default: + } + + if (binding === null) { + console.error(`Unimplemented databinding for element`, elem); + return; + } + + this.bindings.push(binding); + this.fields[binding.name] = binding; + } + + _bind_all_displays() { + for (const elem of this.elem.querySelectorAll( + "[data-display-value-for]" + )) { + new ValueDisplay(elem); + } + } + + /* + Call this to update the form's fields whenever modifying the bound + `data`. + */ + update() { + for (const binding of this.bindings) { + binding.update_value(); + } + this.elem.dispatchEvent(new Event("change", { bubbles: true })); + } + + get valid() { + return this.elem.checkValidity(); + } + + set custom_validity(val) { + if (val) { + this.elem.classList.remove("is-invalid"); + } else { + this.elem.classList.add("is-invalid"); + } + } + + get custom_validity() { + return !this.elem.classList.contains("is-invalid"); + } + + get classList() { + return this.elem.classList; + } + + addEventListener(event, callback) { + this.elem.addEventListener(event, callback); + } +} diff --git a/user_guide/docs/scripts/gemini.js b/user_guide/docs/scripts/gemini.js index 19959dd5..776778b6 100644 --- a/user_guide/docs/scripts/gemini.js +++ b/user_guide/docs/scripts/gemini.js @@ -1,5 +1,5 @@ -import * as Teeth from "../winterjs/teeth.js"; -import { Uint8Array_to_hex } from "../winterjs/utils.js"; +import { Teeth } from "/winter.js"; +import { Uint8Array_to_hex } from "./utils.js"; import GemSettings from "./gem_settings.js"; import GemMonitorUpdate from "./gem_monitor_update.js"; import _struct from "./struct.mjs"; diff --git a/user_guide/docs/scripts/layered.js b/user_guide/docs/scripts/layered.js deleted file mode 100644 index f714e3ff..00000000 --- a/user_guide/docs/scripts/layered.js +++ /dev/null @@ -1,42 +0,0 @@ -document.addEventListener("DOMContentLoaded", () => { - for (const fig of document.querySelectorAll("figure[data-layered]")) { - const images = fig.querySelectorAll("img"); - const img_to_btn = new Map(); - - const show_image = (which) => { - for (const img of images) { - if (img == which) { - img.classList.add("active"); - img_to_btn.get(img).classList.add("active"); - } else { - img.classList.remove("active"); - img_to_btn.get(img).classList.remove("active"); - } - } - }; - - const btn_div = document.createElement("div"); - btn_div.classList.add("buttons"); - - let counter = 1; - - for (const img of images) { - const btn = document.createElement("button"); - btn_div.append(btn); - - btn.type = "button"; - btn.innerText = img.title || `${counter}`; - btn.addEventListener("click", () => { - show_image(img); - }); - - img_to_btn.set(img, btn); - - counter++; - } - - show_image(images[0]); - - fig.append(btn_div); - } -}); diff --git a/user_guide/docs/scripts/settings.js b/user_guide/docs/scripts/settings.js index 41b2ba0f..a93546a8 100644 --- a/user_guide/docs/scripts/settings.js +++ b/user_guide/docs/scripts/settings.js @@ -4,9 +4,9 @@ Full text available at: https://opensource.org/licenses/MIT */ -import { $e, $on } from "../winterjs/utils.js"; -import * as forms from "../winterjs/forms.js"; -import MIDI from "../winterjs/midi.js"; +import { MIDI } from "/winter.js"; +import { $e, $on } from "./utils.js"; +import * as forms from "./forms.js"; import GemSettings from "./gem_settings.js"; import Gemini from "./gemini.js"; import GitHub from "./github.js"; @@ -16,10 +16,9 @@ const ui = { info_section: $e("info_section"), settings_section: $e("settings_section"), save_btn: $e("save_button"), - dangerous_fields: [ - ...document.querySelectorAll("#settings_editor .is-dangerous"), - ], allow_danger: $e("allow_danger"), + danger_section: $e("danger_section"), + dangerous_section_content: $e("danger_section_content"), connect_btn: $e("connect"), connect_info: $e("connect_info"), firmware_version: $e("firmware_version"), @@ -53,8 +52,6 @@ const ui = { const midi = new MIDI("Gemini"); const gemini = new Gemini(midi); const settings = new GemSettings(); -/* The lowest compatible firmware version */ -const minimum_firmware_version = new Date(2023, 3, 21); let gemini_firmware_version = null; let gemini_serial_number = null; let gemini_hardware_revision = null; @@ -126,24 +123,6 @@ async function restore_backup_calibration() { ui.settings_form.update(); } -function check_firmware_version() { - let [year, month, day] = gemini_firmware_version - .split(" ")[0] - .split(".") - .map((x) => parseInt(x, 10)); - - let version_date = new Date(year, month - 1, day); - - console.log(year, month, day, version_date, minimum_firmware_version); - - if (version_date < minimum_firmware_version) { - ui.firmware_incompatible.classList.remove("hidden"); - return false; - } - - return true; -} - async function check_for_new_firmware() { let gh = new GitHub(); let release_info = null; @@ -154,31 +133,35 @@ async function check_for_new_firmware() { ); } catch (e) { console.log("Error while fetching latest firmware: ", e); - return; + return false; } if (gemini_firmware_version.includes(release_info.tag_name)) { - return; + return false; } - let link = ui.firmware_outdated.querySelector("a"); - link.href = release_info.html_url; - link.innerText = `${release_info.name} (${release_info.tag_name})`; - ui.firmware_outdated.classList.remove("hidden"); + return true; } $on(ui.connect_btn, "click", async function () { - ui.connect_info.classList.remove("is-danger", "hidden"); - ui.connect_info.innerText = "Connecting"; + const connect_info_p = ui.connect_info.querySelector("p"); + + ui.connect_btn.innerText = "Connecting"; + ui.connect_btn.disabled = true; + connect_info_p.innerText = ""; + ui.connect_info.hidden = true; try { await midi.connect(); } catch (err) { console.log(err); - ui.connect_info.classList.add("is-danger"); - ui.connect_info.innerText = + connect_info_p.innerText = "Couldn't connect, check connection and power and try again?"; + ui.connect_info.hidden = false; return; + } finally { + ui.connect_btn.innerText = "Connect"; + ui.connect_btn.disabled = false; } gemini_firmware_version = await gemini.get_version(); @@ -190,16 +173,14 @@ $on(ui.connect_btn, "click", async function () { ui.serial_number.value = `${gemini_serial_number}`; ui.hardware_revision.value = `v${gemini_hardware_revision}`; - ui.info_section.classList.remove("hidden"); + ui.info_section.hidden = false; - if (!check_firmware_version()) { + if (await check_for_new_firmware()) { console.log("Firmware too old, bailing. :("); - ui.connect_info.classList.add("hidden"); + ui.firmware_incompatible.hidden = false; return; } - check_for_new_firmware(); - /* Load settings & update the form. */ let loaded_settings = false; /* WebMIDI can sometimes inexplicably mess up SysEx messages, so try this a few times. */ @@ -215,8 +196,7 @@ $on(ui.connect_btn, "click", async function () { } if (!loaded_settings) { - ui.connect_info.classList.add("is-danger"); - ui.connect_info.innerText = + connect_info_p.innerText = "Couldn't load settings, check connection, power, and try resetting the module."; return; } @@ -224,13 +204,11 @@ $on(ui.connect_btn, "click", async function () { ui.settings_form.update(); draw_lfo_waveform(); - ui.connect_btn.classList.remove("is-primary"); - ui.connect_btn.classList.add("is-success"); + ui.connect_btn.disabled = true; ui.connect_btn.innerText = "Connected"; - ui.connect_btn.classList.add("hidden"); - ui.connect_info.classList.add("hidden"); - ui.connect_info.innerText = ""; - ui.settings_section.classList.remove("hidden"); + ui.connect_info.hidden = true; + ui.settings_section.hidden = false; + ui.danger_section.hidden = false; check_for_backups(); }); @@ -263,13 +241,7 @@ $on(ui.restore_adc_calibration_btn, "click", async function () { Enable/disable dangerous settings. */ $on(ui.allow_danger, "change", function () { - for (const elem of ui.dangerous_fields) { - if (elem.type === "range") { - elem.disabled = !ui.allow_danger.checked; - } else { - elem.readOnly = !ui.allow_danger.checked; - } - } + ui.dangerous_section_content.hidden = !ui.allow_danger.checked; }); /* @@ -412,7 +384,7 @@ $on(ui.tuning.save, "click", function () { Monitoring */ if (window.location.hash == "#monitor") { - ui.monitoring.section.classList.remove("hidden"); + ui.monitoring.section.hidden = false; } $on(ui.monitoring.enable, "click", function () { gemini.enable_monitoring((msg) => { @@ -424,7 +396,7 @@ $on(ui.monitoring.enable, "click", function () { Ramp calibration swap */ if (window.location.hash == "#ramp") { - ui.ramp.section.classList.remove("hidden"); + ui.ramp.section.hidden = false; } $on(ui.ramp.swap_btn, "click", async function () { console.log(ramp_calibration_backup); diff --git a/user_guide/docs/scripts/svgmap.js b/user_guide/docs/scripts/svgmap.js deleted file mode 100644 index 888a2555..00000000 --- a/user_guide/docs/scripts/svgmap.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - An advanced imagemap using SVG. - - element attributes: - - data-is-svg-map: attaches this functionality. - - data-list: element ID of a containing that map the - IDs of SVG areas to descriptions. - - data-stylesheet: href to stylesheet to apply inside of the SVG. - - The stylesheet should have styles for .hoverable, .hoverable:hover, and - .info-text. -*/ -class SVGMap { - constructor(object_elem) { - this.object_elem = object_elem; - this.svg_doc = object_elem.contentDocument; - - this.svg_doc = object_elem.contentDocument; - this.svg_elem = this.svg_doc.querySelector("svg"); - apply_safari_img_in_svg_fix(this.svg_elem); - - this.insert_stylesheet().then(() => { - this.insert_help_text(); - this.insert_info_text(); - this.attach_event_listeners(); - }); - } - - insert_help_text() { - const help_text_elem = document.createElement("p"); - help_text_elem.innerText = "Hover or tap an item"; - help_text_elem.classList.add("svgmap-help-text"); - this.object_elem.parentNode.insertBefore( - help_text_elem, - this.object_elem.nextSibling - ); - } - - async insert_stylesheet() { - const resp = await fetch(this.object_elem.dataset.stylesheet); - const style_elem = this.svg_doc.createElementNS( - "http://www.w3.org/2000/svg", - "style" - ); - style_elem.setAttribute("type", "text/css"); - style_elem.textContent = await resp.text(); - this.svg_elem.appendChild(style_elem); - } - - insert_info_text() { - const info_text_template = document.getElementById( - this.object_elem.dataset.infoTextTemplate - ); - let info_text_elem = null; - - if (info_text_template !== null) { - /* Translate the HTML template into and SVG defs so the namespace is correct. */ - const svg_defs = this.svg_doc.createElementNS( - "http://www.w3.org/2000/svg", - "defs" - ); - svg_defs.innerHTML = info_text_template.innerHTML; - info_text_elem = svg_defs.firstElementChild; - } else { - info_text_elem = this.svg_doc.createElementNS( - "http://www.w3.org/2000/svg", - "text" - ); - info_text_elem.setAttribute("id", "info-text"); - } - this.svg_elem.appendChild(info_text_elem); - - /* We only need the text element from this point on. */ - this.info_text_container = info_text_elem; - this.info_text = this.svg_doc.getElementById("info-text"); - this.info_text.textContent = ""; - - /* Check if there's a rect that's set to follow the text's size. */ - this.rect = this.svg_doc.querySelector("rect[data-size-to]"); - } - - update_bounding_rect() { - if (this.rect === null) return; - const bb = this.info_text.getBBox(); - this.rect.setAttribute("x", bb.x); - this.rect.setAttribute("y", bb.y); - this.rect.setAttribute("width", bb.width); - this.rect.setAttribute("height", bb.height); - } - - attach_event_listeners() { - const datalist = document.getElementById(this.object_elem.dataset.list); - for (let item of datalist.options) { - const item_elem = this.svg_doc.getElementById(item.value); - item_elem.classList.add("hoverable"); - item_elem.addEventListener("mouseenter", () => { - this.info_text.textContent = item.label; - this.info_text_container.classList.add("visible"); - this.info_text_container.dataset.value = item.value; - this.update_bounding_rect(); - }); - item_elem.addEventListener("mouseleave", () => { - this.info_text_container.classList.remove("visible"); - }); - } - } -} - -window.addEventListener("load", () => { - const svg_maps = document.querySelectorAll("object[data-is-svg-map]"); - for (const elem of svg_maps) { - new SVGMap(elem); - } -}); - -const is_safari = /apple/i.test(navigator.vendor); - -function apply_safari_img_in_svg_fix(svg) { - /* Fix for safari, which continues to be the most obnoxious browser. */ - if (is_safari) { - for (let elem of svg.querySelectorAll("[*|href]")) { - elem.setAttribute( - "href", - elem.getAttributeNS("http://www.w3.org/1999/xlink", "href") - ); - } - } -} diff --git a/user_guide/docs/scripts/utils.js b/user_guide/docs/scripts/utils.js new file mode 100644 index 00000000..02793179 --- /dev/null +++ b/user_guide/docs/scripts/utils.js @@ -0,0 +1,169 @@ +/* + Copyright (c) 2021 Alethea Katherine Flowers. + Published under the standard MIT License. + Full text available at: https://opensource.org/licenses/MIT +*/ + +import { DOM } from "/winter.js"; + +export function Uint8Array_to_hex(buf) { + return Array.prototype.map + .call(buf, (x) => ("00" + x.toString(16)).slice(-2)) + .join(""); +} + +export const $e = DOM.$; +export const $s = DOM.$$; + +export function $make(tag_name, properties = {}) { + const elem = document.createElement(tag_name); + for (const [name, value] of Object.entries(properties)) { + if (name === "children") { + for (const child of value) { + elem.appendChild(child); + } + continue; + } + if (name === "innerText") { + elem.innerText = value; + continue; + } + elem.setAttribute(name, value); + } + return elem; +} + +/* Adds an event listener. */ +export function $on(elem, event, callback, strict = true) { + if (!strict && (elem === null || elem === undefined)) { + return; + } + elem.addEventListener(event, callback); +} + +/* Helper for working with template elements with simple interpolation. */ +export class TemplateElement { + constructor(id) { + this._elem = $e(id); + this._parent = this._elem.parentNode; + } + + hide() { + this._parent.classList.add("hidden"); + } + + show() { + this._parent.classList.remove("hidden"); + } + + render(ctx) { + let content = this._elem.innerHTML; + content = content.replace(/\${(.*?)}/g, (_, g) => { + return ObjectHelpers.get_property_by_path(ctx, g) || ""; + }); + const temp = document.createElement("template"); + temp.innerHTML = content; + return temp.content.cloneNode(true); + } + + render_to(elem, ctx) { + DOM.removeAllChildren($e(elem)); + $e(elem).appendChild(this.render(ctx)); + } + + render_to_parent(ctx) { + DOM.removeAllChildren(this._parent); + this._parent.appendChild(this.render(ctx)); + } + + render_all_to_parent(ctxes) { + DOM.removeAllChildren(this._parent); + for (const [n, ctx] of ctxes.entries()) { + ctx.$index = n; + this._parent.appendChild(this.render(ctx)); + } + } +} + +export const ObjectHelpers = { + /* Why is it so hard to check if something is a string in JavaScript? */ + is_string(val) { + return typeof val === "string" || val instanceof String; + }, + + /* + Like Object.assign, but only updates existing properties in target- it + does not add any new ones. + */ + assign_only_existing_properties: (target, source) => { + for (const prop of Object.getOwnPropertyNames(target)) { + if (Object.prototype.hasOwnProperty.call(source, prop)) { + target[prop] = source[prop]; + } + } + }, + /* Access nested object properties using a string, e.g. "order.email" */ + get_property_by_path: (obj, key, strict = true) => { + const key_parts = key.split("."); + let value = obj; + for (const part of key_parts) { + if (!strict && (value === undefined || value == null)) { + return undefined; + } + value = value[part]; + } + return value; + }, + + set_property_by_path: (obj, key, value, strict = true) => { + const key_parts = key.split("."); + let current_obj = obj; + for (const part of key_parts.slice(0, -1)) { + if (!strict && (part === undefined || part == null)) { + return; + } + current_obj = current_obj[part]; + } + + current_obj[key_parts.pop()] = value; + }, +}; + +/* + Handles re-constructing native objects, like Number and Date objects + from plain-o-JSON objects with sentinel keys like __decimal__ and + __datetime__ fields. +*/ +export class ObjectReviver { + constructor() { + this._revivers = {}; + } + + add(sentinel_key, reviver) { + this._revivers[sentinel_key] = reviver; + } + + revive() { + return (key, value) => { + if ( + value === null || + value === undefined || + typeof value !== "object" + ) { + return value; + } + for (const [sentinel_key, revive] of Object.entries( + this._revivers + )) { + if (Object.prototype.hasOwnProperty.call(value, sentinel_key)) { + return revive(value); + } + } + return value; + }; + } +} + +ObjectReviver.default = new ObjectReviver(); +ObjectReviver.default.add("__decimal__", (val) => new Number(val.value)); +ObjectReviver.default.add("__datetime__", (val) => new Date(val.value)); diff --git a/user_guide/docs/scripts/waveforms.js b/user_guide/docs/scripts/waveforms.js index 8314ffa8..d9cf820b 100644 --- a/user_guide/docs/scripts/waveforms.js +++ b/user_guide/docs/scripts/waveforms.js @@ -4,8 +4,8 @@ Full text available at: https://opensource.org/licenses/MIT */ -import { $e, $on } from "../winterjs/utils.js"; -import * as forms from "../winterjs/forms.js"; +import { $e, $on } from "./utils.js"; +import * as forms from "./forms.js"; /* Global parameters for every waveform display. */ const frequency = 3.0; diff --git a/user_guide/docs/settings.md b/user_guide/docs/settings.md index 5fbf83c0..9bec623b 100644 --- a/user_guide/docs/settings.md +++ b/user_guide/docs/settings.md @@ -2,9 +2,9 @@ To use this editor: 1. Connect your Castor & Pollux to your Eurorack power supply. The module **must** be powered by the Eurorack power supply. 1. Connect a USB cable from your computer to Castor & Pollux's USB port. It's located on the right side of the module on the bottom circuit board. -1. Click the [connect button](#connect), and when prompted, allow the page access to your MIDI devices. +1. Click the ++"connect"++ button below, and when prompted, allow the page access to your MIDI devices. 1. Once connected the editor will appear and load the current settings from your device. -1. Once you're done editing, click the save button at the bottom of the editor. +1. Once you're done editing, click the ++"save & restart"++ button at the bottom of the editor. 1. Saving should automatically reset your module, but, if you don't see the changes take effect then try restarting your module. You can do this by turning it off and back on or by pressing the small reset button that's right beside the USB port. !!! Note @@ -12,386 +12,252 @@ To use this editor: If you run into issues, feel free to [reach out](mailto:support@winterbloom.com). -
- - + + + + + + - - - -