diff --git a/assets/_custom.scss b/assets/_custom.scss index 26af23d..4022a4b 100644 --- a/assets/_custom.scss +++ b/assets/_custom.scss @@ -37,11 +37,12 @@ aside nav ul li { .book-menu a.active { color: #ef0981; + text-decoration: underline; font-size: 1.3rem; } .markdown a:visited { - color: #ef0981; + // color: #ef0981; } a { diff --git a/config.toml b/config.toml index 29e970d..cd5c885 100644 --- a/config.toml +++ b/config.toml @@ -45,6 +45,7 @@ enableGitInfo = true # Used for 'Last Modified' and 'Edit this page' links. BookRepo = 'https://github.com/hydra-synth/hydra-docs-v2' + # BookTheme = 'dark' # (Optional, default 'commit') Specifies commit portion of the link to the page's last modified # commit hash for 'doc' page type. # Requires 'BookRepo' param. diff --git a/content/_index.md b/content/_index.md index f963c97..e798750 100644 --- a/content/_index.md +++ b/content/_index.md @@ -1,11 +1,26 @@ --- -title: hydra video synth portal -type: docs +title: "what is hydra" +draft: false +weight: 1 --- -WIP portal for hydra video synth. This will be a home for news and updates regarding hydra, as well as a hub for community resources and documentaiton. +# What is Hydra? +![hydra](images/hydra.png) - For the main hydra website, see the [web editor](https://hydra.ojack.xyz). - - To contribute, please [get in touch.]() +Hydra is live code-able video synth and coding environment that runs directly in the browser. It is free and open-source, and made for beginners and experts alike. You can simply open the [hydra web editor](https://hydra.ojack.xyz) on a laptop or mobile device to get started. +Hydra is written in JavaScript and compiles to WebGL under the hood. The syntax is inspired by analog modular synthesis, in which chaining or patching a set of transformations together generates a visual result. + +# About +#### Hydra can be used: +- to mix and add effects to camera feeds, screenshares, live streams, and videos +- to create generative and audio-reactive visuals, and share them online with others +- in combination with other javascript libraries such as P5.js, Tone.js, THREE.js, or gibber +- to add interactive video effects to a website +- to experiment with and learn about video feedback, fractals, and pixel operations +- to stream video between browsers and live-jam with others online + +#### Further resources and next steps +For more information and instructions, see: [Getting Started](docs/getting-started-short.md), [a list of hydra functions](https://hydra.ojack.xyz/api/), [the community database of projects and tutorials](https://hydra.ojack.xyz/garden/), [a gallery of user-generated sketches](https://twitter.com/hydra_patterns), and [the source code on github](https://github.com/hydra-synth/hydra). + +Hydra was created by [olivia jack](https://ojack.xyz) and is supported by a community of contributers. If you enjoy using Hydra, please consider [supporting continued development](https://opencollective.com/hydra-synth). diff --git a/content/docs/community.md b/content/docs/community.md index bd828fe..b2c7460 100644 --- a/content/docs/community.md +++ b/content/docs/community.md @@ -1,5 +1,5 @@ --- -weight: 3 +# weight: 3 bookFlatSection: true title: "community" bookCollapseSection: false diff --git a/content/docs/learning/getting-started-short.md b/content/docs/getting-started-short.md similarity index 67% rename from content/docs/learning/getting-started-short.md rename to content/docs/getting-started-short.md index 668c923..f22d710 100644 --- a/content/docs/learning/getting-started-short.md +++ b/content/docs/getting-started-short.md @@ -1,17 +1,29 @@ --- -title: "getting started" +title: "hello world" draft: false author: "Flor and Olivia" weight: 1 --- -# Getting started +# hello world +start writing code in hydra! to get started: +1. Go to https://hydra.ojack.xyz -## Play with gallery examples +2. Close the top window by clicking the `[x]` in the top right. + +3. Change some numbers + +4. Type "ctrl+shift+enter" to run the code (or use the triangle run button) + +5. have fun! there is no "wrong" way to code in hydra :] + + +![](https://i.imgur.com/ZfgVjJZ.gif) + + + +## Next steps +For more a more in-depth introductions see [video synth basics](../learning/video-synth-basics). For an overview of available guides and resources, see [learning](../learning). diff --git a/content/docs/learning/_index.md b/content/docs/learning/_index.md index 25c3e06..82ed33d 100644 --- a/content/docs/learning/_index.md +++ b/content/docs/learning/_index.md @@ -10,21 +10,23 @@ bookCollapseSection: true {{< columns >}} -### [getting started](getting-started-short) -### [video synth basics](video-synth-basics) -overview of hydra's [modular approach]() and main function types: [sources](), [geometry](), [color](), [blending](), and [modulation]() -### [external sources](external-sources) -using [webcams](), [images](), [videos](), [html canvas elements](), and [live streams]() inside a hydra sketch -### [sequencing and interactivity](interactivity) -Making dynamic and interactive sketches using [arrays](), [custom functions](), [audio reactivity](), [mouse input](), and [MIDI controllers](). + +## [video synth basics](video-synth-basics) +overview of hydra's [modular approach](video-synth-basics) and main function types. +## [external sources](external-sources) +using [webcams](external-sources#using-the-webcam), [images](/external-sources/#initimage), [videos](external-sources/#initvideo), [html canvas elements](external-sources/#init), and [live streams](external-sources/#initstream) inside a hydra sketch +## [sequencing and interactivity](interactivity) +Making dynamic and interactive sketches using [arrays](interactivity/#sequencing-using-arrays), [custom functions](interactivity/#custom-functions), [audio reactivity](interactivity/#audio-reactivity), [mouse input](interactivity/#mouse-interactivity), and [MIDI controllers](interactivity/#midi). +## [synth configuration]() +how to change the [speed](), [bpm](), and [resolution]() of a hydra instance, as well as write custom glsl functions <---> -### [web editor](web-editor) +## [web editor](web-editor) key commands, comments, saving sketches, loading extensions and external libraries, publishing to the gallery -### [synth configuration]() -how to change the [speed](), [bpm](), and [resolution]() of a hydra instance, as well as write custom glsl functions -### [how-to](how-to) +## [extending hydra](extending-hydra) +using hydra with other javascript libraries such as P5.js, Tone.js, strudel. Loading libraries and extensions. +## [how-to](how-to) a quick reference for common questions -### [guides](guides) +## [guides](guides) deeper dives into hydra topics written by members of the community. {{< /columns >}} diff --git a/content/docs/learning/extending-hydra.md b/content/docs/learning/extending-hydra.md new file mode 100644 index 0000000..ae7432d --- /dev/null +++ b/content/docs/learning/extending-hydra.md @@ -0,0 +1,93 @@ +--- +title: "extending hydra" +--- +# Extending hydra +Hydra is written in javascript, and compatible with many other javascript libraries. The hydra web editor executes javascript directly in the browser, so it is possible to load many other libraries and scripts directly in the browser. + +## p5.js + +### Using p5.js with the hydra web editor + +```javascript +// Initialize a new p5 instance It is only necessary to call this once +p5 = new P5() // {width: window.innerWidth, height:window.innerHeight, mode: 'P2D'} + +// draw a rectangle at point 300, 100 +p5.rect(300, 100, 100, 100) + +// Note that P5 runs in instance mode, so all functions need to start with the variable where P5 was initialized (in this case p5) +// reference for P5: https://P5js.org/reference/ +// explanation of instance mode: https://github.com/processing/P5.js/wiki/Global-and-instance-mode + +// When live coding, the "setup()" function of P5.js has basically no use; anything that you would have called in setup you can just call outside of any function. + +p5.clear() + +for(var i = 0; i < 100; i++){ + p5.fill(i*10, i%30, 255) + p5.rect(i*20, 200, 10,200) +} + +// To live code animations, you can redefine the draw function of P5 as follows: +// (a rectangle that follows the mouse) +p5.draw = () => { + p5.fill(p5.mouseX/5, p5.mouseY/5, 255, 100) + p5.rect(p5.mouseX, p5.mouseY, 30, 150) +} + +// To use P5 as an input to hydra, simply use the canvas as a source: +s0.init({src: p5.canvas}) + +// Then render the canvas +src(s0).repeat().out() +``` + +## Loading external scripts +The `await loadScript()` function lets you load other packaged javascript libraries within the hydra editor. Any javascript code can run in the hydra editor. + +## THREE.js +Here is an example using Three.js from the web editor: +```javascript +await loadScript("https://threejs.org/build/three.js") + +scene = new THREE.Scene() +camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) + +renderer = new THREE.WebGLRenderer() +renderer.setSize(width, height) +geometry = new THREE.BoxGeometry() +material = new THREE.MeshBasicMaterial({color: 0x00ff00}) +cube = new THREE.Mesh(geometry, material); +scene.add(cube) +camera.position.z = 1.5 + +// 'update' is a reserved function that will be run every time the main hydra rendering context is updated +update = () => { + cube.rotation.x += 0.01; + cube.rotation.y += 0.01; + renderer.render( scene, camera ); +} + +s0.init({ src: renderer.domElement }) + +src(s0).repeat().out() +``` + +## Tone.js +And here is an example loading the Tone.js library: +```javascript +await loadScript("https://unpkg.com/tone") + +synth = new Tone.Synth().toDestination(); +synth.triggerAttackRelease("C4", "8n"); +``` + +## Custom libraries +In the Hydra editor, you can load any external scripts, libraries or hydra-synth extensions using the following syntax at the top of your sketch: + +```javascript +await loadScript("https://www.somewebsite.com/url/to/hydra-script.js") +``` + + +You can also suffer from [CORS policy problems](./external_sources.md#cors-policy) if the script/package you're loading doesn't come from a CDN. If you want to load from a GitHub or GitLab repo, you can use special CDNs like `statically.io`. diff --git a/content/docs/learning/external-sources.md b/content/docs/learning/external-sources.md index 1aee5ab..6b7fbaf 100644 --- a/content/docs/learning/external-sources.md +++ b/content/docs/learning/external-sources.md @@ -1,14 +1,14 @@ --- title: "external sources: cameras, videos, streams" draft: false -author: "Ritchse" +author: "geikha" weight: 4 --- # External Sources - +by [geikha](https://github.com/geikha) and [olivia](https://ojack.xyz) ## Using the webcam -In addition to using sources from within hydra (such as `osc()` and `shape()`), you can use hydra to process external video sources such as a webcam. To initialize the webcam, run the following code: +In addition to using sources from within hydra (such as `osc()` and `shape()`), you can use hydra to process external video sources such as a webcam. External sources in hydra are referenced using predefined objects `s0`, `s1`, `s2`, and `s3`. To initialize the webcam in `s0`, run the following code: ```javascript s0.initCam() ``` @@ -38,7 +38,7 @@ If you have multiple webcams, you can access separate cameras by adding a number --- -## initCam +## initCam() You can use a webcam's video as such: @@ -48,22 +48,20 @@ s0.initCam() s0.initCam(2) // if you have many cameras, you can select one specifically ``` -## initScreen - -You can capture your screen or specific windows or tabs to use as a video source: -```javascript -s0.initScreen() -``` --- -## initImage +## initImage() -In order to load an image to load an image into a source object, the syntax is the following: +Load an image into a source object: ```javascript -s0.initImage("https://www.somewebpage.org/urlto/image.jpg") +// load an image into a source object +s0.initImage("https://upload.wikimedia.org/wikipedia/commons/2/25/Hydra-Foto.jpg") + +// show the image on the screen +src(s0).out() ``` When running Hydra in Atom, or any other local manner, you can load local files referring to them by URI: @@ -76,12 +74,13 @@ s0.initImage("file:///home/user/Images/image.png") You can load `.jpeg`, `.png`, and `.bmp` as well as `.gif` and `.webp` (although animation won't work). -## initVideo +## initVideo() The syntax for loading video is the same as for loading image, only changing the function to `loadVideo`: ```javascript -s0.initVideo("https://www.somewebpage.org/urlto/video.mp4") +s0.initVideo("https://media.giphy.com/media/AS9LIFttYzkc0/giphy.mp4") +src(s0).out() ``` ### Supported formats @@ -98,13 +97,43 @@ s0.src.currentTime = 10 // seek to the 10th second s0.src.loop = false // don't loop the video ``` +## initScreen() + +You can capture your screen or specific windows or tabs to use as a video source: + +```javascript +s0.initScreen() +src(s0).out() +``` --- -## initStream : streaming between Hydra sessions +## init() +`init()` is a more generic function for loading any external source into hydra. This can be especially useful when you are using an [HTML canvas element](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) as an input, or loading an existing resource as a source into hydra. Valid input types are documented in the [regl texture documentation](http://regl.party/api#textures). + +For example, the following code creates a canvas element and draws text to it, and then uses that canvas as a source in hydra: +```hydra +myCanvas = document.createElement('canvas') +ctx = myCanvas.getContext('2d') +ctx.font = "30px Arial" +ctx.fillStyle = "red"; +ctx.fillText("Hello World", 10, 50) + +s0.init({ src: myCanvas, dynamic: false }) + +src(s0).diff(osc(2, 0.1, 1.2)).out() +``` +use the `dynamic` parameter to indicate whether the source will be updated, or remain the same. + +## initStream() +{{< hint danger >}} +note: initStream() is currently broken in hydra editor due to server issues +{{< /hint >}} + +streaming between Hydra sessions Hydra (the editor) also has built-in streaming. You can stream the output of your Hydra to someone else and vice-versa. This is done in a similar fashion to using images and videos, using external sources. But there are some extra steps for streaming: -### The pb object +### The pb object On your Hydra editor, you can find a pre-defined object called `pb` (as in patch-bay). This object basically represents the connection of your Hydra editor instance to all others hosted on the same server. When you want to share your stream to someone else you'll have to give your Hydra session a name. Do this using the `pb.setName()` function and by passing in some string as the name. For example: `pb.setName('myverycoolsession')`. If you want someone else to stream to you, ask them to set a name as such and share it with you. diff --git a/content/docs/learning/interactivity.md b/content/docs/learning/interactivity.md index fa29d49..7619a8d 100644 --- a/content/docs/learning/interactivity.md +++ b/content/docs/learning/interactivity.md @@ -6,36 +6,51 @@ weight: 4 --- # Sequencing and Interactivity +by [geikha](https://github.com/geikha) and olivia --- - -If you're coding in Hydra, you're constantly trying many values to input to the sources and transforms, and it's just a matter of time until you like how more than one looks, and you want to somehow switch between them. We'll be referring to this idea of arguments whose value change over time as dynamic arguments. And there are two main ways to achieve this in Hydra: Arrays and functions. + If you're coding in Hydra, you're constantly trying many values to input to the sources and transforms, and it's just a matter of time until you like how more than one looks, and you want to somehow switch between them. We'll be referring to this idea of arguments whose value change over time as dynamic arguments. And there are two main ways to achieve this in Hydra: Arrays and functions. ## Sequencing using Arrays -#### Sequence your inputs +### Sequence your inputs + -When you send an Array as an input, Hydra will automatically switch and jump from each element from the Array to the next one. When there are no more elements, it wraps all the way back to the beginning. Let's see it in action: +When you send an Array as an input (indicated in javascript by `[]`), Hydra will automatically switch and jump from each element from the Array to the next one. When there are no more elements, it wraps all the way back to the beginning. Let's see it in action: ```hydra -osc([20,30,50,60],.1,[0,1.5]) +shape([3, 4, 5, 6, 7, 8]) .out() ``` +### changing the speed -As you can see, the fact that both these Arrays have a different amount of values doesn't matter, Hydra will take values from each element of any Array for the same amount of time by default. +The arrays in hydra have a default bpm(beats-per-minute) of 30. You can change the speed of a specific array by adding `.fast()` at the end of the array. For example `.fast(4)` will make the above array run four times faster. -#### Changing the speed of a specific Array +```hydra +shape([3, 4, 5, 6, 7, 8].fast(4)) + .out() +``` -Hydra adds a couple of methods to all Arrays to be used inside Hydra. `.fast` will control the speed at which Hydra takes elements from the Array. It receives a Number as argument, by which the global speed will be multiplied. So calling `.fast(1)` on an Array is the same as nothing. Higher values will generate faster switching, while lower than 1 values will be slower. +The speed of all arrays in a sketch can be changed using the `bpm` parameter of hydra synth. + +```javascript +bpm = 60 +``` + + + + -#### Offsetting the timing of an Array + -#### Fitting the values of an Array within a range + -#### Interpolating between values +### smooth() interpolation You can also interpolate between values instead of jumping from one to the other. That is, smoothly transition between values. For this you can use the `.smooth` method. It may take a Number argument (defaulted to 1) which controls the smoothness. ```hydra +shape([3, 4, 5, 6, 7, 8].smooth()) + .out() +``` + + -##### Easing functions + ## Custom Functions -The other main way of adding dynamic inputs to your sketches is passing functions as arguments. When Hydra takes a function as an argument, what it will do is evaluate it every time it renders a frame. The return of the function will be used as the value for that parameter during that frame render. So you can use a function to simply keep track of a value that you know will change over time, for example, mouse position (which we'll see later). - -```hydra -voronoi(5,.1,()=>Math.sin(time*4)) - .out() +Each numerical parameter in hydra can be defined as a function rather than a static variable. For example, +```javascript +osc(function(){return 100 * Math.sin(time * 0.1)}).out() ``` +modifies the oscillator frequency as a function of time. (Time is a global variable that represents the milliseconds that have passed since loading the page). -The `time` variable seen there is a variable pre-declared by Hydra, that stores how much time passed since Hydra started in seconds. +The above example can be written more concisely using es6 syntax: -Functions used in Hydra don't need to be arrow functions, any no-argument function will do! Make sure your function is returning a Number to avoid errors. - -### The time variable +```javascript +osc(() => 100 * Math.sin(time * 0.1)).out() +``` + -When you use functions that can take numerical arguments, `time` will allow you to have their values evolve through... time. If you multiply time by some value it's as if time goes faster, while dividing while act as making time go slower. For example `Math.sin(time*4)` will go 4 times faster than `Math.sin(time)`. + Custom functions are especially useful for controlling hydra parameters using external inputs, such as the microphone, mouse, or midi control. -Those users more familiar with mathematics might see this as: -* `y(t) = t` : `()=>time` -* `y(t) = A sin(f t + ph)` : `()=>amplitude*Math.sin(freq*time + phase)` - -We recommend getting familiar with some of the methods in the JS built-in `Math` object. Learn more about it [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math) - -#### Changing the global speed + -#### Note -All of the examples using mouse position to move stuff on the canvas won't work well here, since the canvas doesn't occupy the full size of the screen as in the editor. Take this into account when we use `mouse`, that the positions are relative to the full webpage and not the canvas. This also means that as you scroll down this guide the `y` value will get higher and higher. --- -## Mouse interactivity +### Mouse interactivity You can have your visuals react to the position of your mouse (or finger, in touch devices). Hydra has an object called `mouse` which stores and keeps track of the position of your mouse on the webpage. @@ -160,15 +177,19 @@ You can refer to the pixel position of your mouse by calling `mouse.x` and `mous Many times it will be most useful to use values relative to the size of the screen. And also to have values that exist between ranges more reasonable to the hydra functions you're using. For example [-0.5; 0.5] for scrollX and scrollY, [0; 2pi] for rotation, or [0; 1] for general purposes. -### Control anything with your mouse +#### Note + +All of the examples using mouse position to move stuff on the canvas won't work well here, since the canvas doesn't occupy the full size of the screen as in the editor. Take this into account when we use `mouse`, that the positions are relative to the full webpage and not the canvas. This also means that as you scroll down this guide the `y` value will get higher and higher. + +#### Control anything with your mouse On Hydra, most values used are pretty small. So it will be way more useful to have the position of the mouse as values from 0 and 1: #### Getting values from 0 to 1 ```hydra -x = () => mouse.x/innerWidth // 0→1 -y = () => mouse.y/innerHeight // 0→1 +x = () => mouse.x/width // 0→1 +y = () => mouse.y/height // 0→1 osc() .scale(()=>1+x()*2) .modulate(noise(4),()=>y()/4) @@ -177,15 +198,15 @@ osc() You can simply multiply by `2*Math.PI` to change the range to [0; 2pi] -### Make something follow your mouse +#### Make something follow your mouse On Hydra, things are placed between 0.5 and -0.5 (left to right, top to bottom). In order for anything to follow your mouse, you'll need to get the position of your mouse between that range: #### Getting values from 0 to ±0.5 from the center ```hydra -x = () => (-mouse.x/innerWidth)+.5 // 0.5→-0.5 -y = () => (-mouse.y/innerHeight)+.5 // 0.5→-0.5 +x = () => (-mouse.x/width)+.5 // 0.5→-0.5 +y = () => (-mouse.y/height)+.5 // 0.5→-0.5 solid(255) .diff( shape(4,.1) @@ -199,6 +220,43 @@ Remember you can name these functions however you prefer. --- ## Audio reactivity +FFT functionality is available via an audio object accessed via "a". The editor uses https://github.com/meyda/meyda for audio analysis. +To show the fft bins, +```javascript +a.show() +``` +Set number of fft bins: +```javascript +a.setBins(6) +``` +Access the value of the leftmost (lowest frequency) bin: +```javascript +a.fft[0] +``` +Use the value to control a variable: +```javascript +osc(10, 0, () => a.fft[0]*4) + .out() +``` +It is possible to calibrate the responsiveness by changing the minimum and maximum value detected. (Represented by blur lines over the fft). To set minimum value detected: +```javascript +a.setCutoff(4) +``` + +Setting the scale changes the range that is detected. +```javascript +a.setScale(2) +``` +The fft[] will return a value between 0 and 1, where 0 represents the cutoff and 1 corresponds to the maximum. + +You can set smoothing between audio level readings (values between 0 and 1). 0 corresponds to no smoothing (more jumpy, faster reaction time), while 1 means that the value will never change. +```javascript +a.setSmooth(0.8) +``` +To hide the audio waveform: +```javascript +a.hide() +``` ```javascript a.setBins(5) // amount of bins (bands) to separate the audio spectrum @@ -217,3 +275,82 @@ a.show() // show what hydra's listening to render(o0) ``` + +```hydra +voronoi(5,.1,()=>Math.sin(time*4)) + .out() +``` + +The `time` variable seen there is a variable pre-declared by Hydra, that stores how much time passed since Hydra started in seconds. + +Functions used in Hydra don't need to be arrow functions, any no-argument function will do! Make sure your function is returning a Number to avoid errors. + +### The time variable + +When you use functions that can take numerical arguments, `time` will allow you to have their values evolve through... time. If you multiply time by some value it's as if time goes faster, while dividing while act as making time go slower. For example `Math.sin(time*4)` will go 4 times faster than `Math.sin(time)`. + +Those users more familiar with mathematics might see this as: + +* `y(t) = t` : `()=>time` +* `y(t) = A sin(f t + ph)` : `()=>amplitude*Math.sin(freq*time + phase)` + +We recommend getting familiar with some of the methods in the JS built-in `Math` object. Learn more about it [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math) + + +## MIDI +Hydra can be used with [Web MIDI](https://webaudio.github.io/web-midi-api/) for an extra layer of control to your visuals. + +### Example script: browser console + At this time this requires some running of code on the +browser console (Press F12 in Chrome to access). This page only considers MIDI Continuous Controllers (CC) but other types of data may be accessible. + +This is a generic script that doesn't care what Midi Channel you're broadcasting on and maps a normalized value 0.0-1.0 into an array named cc. + +This portion should be ran in the console & will register Web MIDI & map the incoming CC data to a set of parameters. For simplicity, these +parameters are named to match the CC number. The CC values are normally in a range from 0-127, but we've also normalized them to be in a range of 0.0-1.0. + +```javascript +// register WebMIDI +navigator.requestMIDIAccess() + .then(onMIDISuccess, onMIDIFailure); + +function onMIDISuccess(midiAccess) { + console.log(midiAccess); + var inputs = midiAccess.inputs; + var outputs = midiAccess.outputs; + for (var input of midiAccess.inputs.values()){ + input.onmidimessage = getMIDIMessage; + } +} + +function onMIDIFailure() { + console.log('Could not access your MIDI devices.'); +} + +//create an array to hold our cc values and init to a normalized value +var cc=Array(128).fill(0.5) + +getMIDIMessage = function(midiMessage) { + var arr = midiMessage.data + var index = arr[1] + //console.log('Midi received on cc#' + index + ' value:' + arr[2]) // uncomment to monitor incoming Midi + var val = (arr[2]+1)/128.0 // normalize CC values to 0.0 - 1.0 + cc[index]=val +} +``` + +#### Hydra script +Now that these controls have been assigned to the cc[] array, we can start using them in Hydra. As we've normalized the values 0-1 we can use +as-is with most functions or quickly remap them with various math. +```javascript +// example midi mappings - Korg NanoKontrol2 CCs + +// color controls with first three knobs +noise(4).color( ()=>cc[16], ()=>cc[17], ()=>cc[18] ).out() + +// rotate & scale with first two faders +osc(10,0.2,0.5).rotate( ()=>(cc[0]*6.28)-3.14 ).scale( ()=>(cc[1]) ).out() + +``` + +### MIDI extension diff --git a/content/docs/learning/synth-configuration.md b/content/docs/learning/synth-configuration.md new file mode 100644 index 0000000..a3e0283 --- /dev/null +++ b/content/docs/learning/synth-configuration.md @@ -0,0 +1,3 @@ +--- +title: synth configuration +--- \ No newline at end of file diff --git a/content/docs/learning/video-synth-basics/_index.md b/content/docs/learning/video-synth-basics/_index.md new file mode 100644 index 0000000..104f1f3 --- /dev/null +++ b/content/docs/learning/video-synth-basics/_index.md @@ -0,0 +1,17 @@ +--- +title: video synth basics +weight: 1 +--- + +# Modular Video Synth Basics + +Hydra is inspired by [modular synthesis](https://en.wikipedia.org/wiki/Modular_synthesizer), where patching together different functions generates a visual output. + +![](https://i.imgur.com/RBRxeiL.jpg) +###### source [Sandin Image Processor](https://en.wikipedia.org/wiki/Sandin_Image_Processor) + +There are five base types of functions in hydra: source, geometry, color, blend, and modulate. +A basic hydra pattern always starts with a ***source*** (such as `osc()`, `shape()`, or `noise()`), followed by transformations to ***geometry*** and ***color*** (such as `.rotate()`, `.kaleid()`, `.pixelate()` ), and ends with `.out()` to show the result on the screen. ***blend*** and ***modulate*** functions allow combining multiple patterns and textures together. + +## getting started +For a detailed tutorial of how to use these basic function types, see [getting started](getting-started). For a complete list of functions and example usage, see: [reference](../../reference). diff --git a/content/docs/learning/video-synth-basics/getting-started-older.md b/content/docs/learning/video-synth-basics/getting-started-older.md new file mode 100644 index 0000000..e45bbc2 --- /dev/null +++ b/content/docs/learning/video-synth-basics/getting-started-older.md @@ -0,0 +1,266 @@ +--- +title: "tutorial: getting started (older)" +date: 2023-04-04T15:10:36+02:00 +# draft: true +type: post +--- +## Getting started + +Go to https://hydra.ojack.xyz + +* CTRL-Enter: run a line of code +* CTRL-Shift-Enter: run all code on screen +* ALT-Enter: run a block +* CTRL-Shift-H: hide or show code +* CTRL-Shift-F: format code using [Prettier](https://prettier.io/) +* CTRL-Shift-S: Save screenshot and download as local file +* CTRL-Shift-G: Share to twitter (if available). Shares to [@hydra_patterns](https://twitter.com/hydra_patterns) + +All code can be run either from the in-browser text editor or from the browser console. + +Check [@hydra_patterns](https://twitter.com/hydra_patterns) for patterns folks have shared as an easy way to get started. + +#### Basic functions +render an oscillator with parameters frequency, sync, and rgb offset: +```javascript +osc(20, 0.1, 0.8).out() +``` + +rotate the oscillator 0.8 radians: +```javascript +osc(20, 0.1, 0.8).rotate(0.8).out() +``` +pixelate the output of the above function: +```javascript +osc(20, 0.1, 0.8).rotate(0.8).pixelate(20, 30).out() +``` +show webcam output: +```javascript +s0.initCam() // initialize a webcam in source buffer s0 +src(s0).out() // render source buffer s0 +``` +If you have more than one camera connected, you can select the camera using an index: +```javascript +s0.initCam(1) // initialize a webcam in source buffer s0 +``` +webcam kaleidoscope: +```javascript +s0.initCam() // initialize a webcam in source buffer s0 +src(s0).kaleid(4).out() // render the webcam to a kaleidoscope +``` + +You can also composite multiple sources together: +```javascript +osc(10) + .rotate(0.5) + .diff(osc(200)) + .out() +``` + +By default, the environment contains four separate output buffers that can each render different graphics. The outputs are accessed by the variables o0, o1, o2, and o3. + +to render to output buffer o1: +```javascript +osc().out(o1) +render(o1) // render the contents of o1 +``` +If no output is specified in out(), the graphics are rendered to buffer o0. +to show all render buffers at once: +```javascript +render() +``` + +The output buffers can then be mixed and composited to produce what is shown on the screen. +```javascript +s0.initCam() // initialize a webcam in source buffer s0 +src(s0).out(o0) // set the source of o0 to render the buffer containing the webcam +osc(10, 0.2, 0.8).diff(o0).out(o1) // initialize a gradient in output buffer o1, composite with the contents of o0 +render(o1) // render o1 to the screen +``` + +The composite functions blend(), diff(), mult(), and add() perform arithmetic operations to combine the input texture color with the base texture color, similar to photoshop blend modes. + +modulate(texture, amount) uses the red and green channels of the input texture to modify the x and y coordinates of the base texture. More about modulation at: https://lumen-app.com/guide/modulation/ +```javascript +osc(21, 0).modulate(o1).out(o0) +osc(40).rotate(1.57).out(o1) +``` + +use a video as a source: +```javascript +s0.initVideo("https://media.giphy.com/media/AS9LIFttYzkc0/giphy.mp4") +src(s0).out() +``` + + +use an image as a source: +```javascript +s0.initImage("https://upload.wikimedia.org/wikipedia/commons/2/25/Hydra-Foto.jpg") +src(s0).out() +``` + +#### Passing functions as variables +Each parameter can be defined as a function rather than a static variable. For example, +```javascript +osc(function(){return 100 * Math.sin(time * 0.1)}).out() +``` +modifies the oscillator frequency as a function of time. (Time is a global variable that represents the milliseconds that have passed since loading the page). This can be written more concisely using es6 syntax: +```javascript +osc(() => (100 * Math.sin(time * 0.1))).out() +``` + +## Desktop capture +Open a dialog to select a screen tab to use as input texture: +```javascript +s0.initScreen() +src(s0).out() +``` + +## Connecting to remote streams (!!currently not working) +Any hydra instance can use other instances/windows containing hydra as input sources, as long as they are connected to the internet and not blocked by a firewall. Hydra uses webrtc (real time webstreaming) under the hood to share video streams between open windows. The included module rtc-patch-bay manages connections between connected windows, and can also be used as a standalone module to convert any website into a source within hydra. (See standalone camera source below for example.) + +To begin, open hydra simultaneously in two separate windows. +In one of the windows, set a name for the given patch-bay source: +```javascript +pb.setName("myGraphics") +``` +The title of the window should change to the name entered in setName(). + +From the other window, initiate "myGraphics" as a source stream. +```javascript +s0.initStream("myGraphics") +``` +render to screen: +```javascript +s0.initStream("myGraphics") +src(s0).out() +``` +The connections sometimes take a few seconds to be established; open the browser console to see progress. +To list available sources, type the following in the console: +```javascript +pb.list() +``` + +## Using p5.js with hydra + +```javascript +// Initialize a new p5 instance It is only necessary to call this once +p5 = new P5() // {width: window.innerWidth, height:window.innerHeight, mode: 'P2D'} + +// draw a rectangle at point 300, 100 +p5.rect(300, 100, 100, 100) + +// Note that P5 runs in instance mode, so all functions need to start with the variable where P5 was initialized (in this case p5) +// reference for P5: https://P5js.org/reference/ +// explanation of instance mode: https://github.com/processing/P5.js/wiki/Global-and-instance-mode + +// When live coding, the "setup()" function of P5.js has basically no use; anything that you would have called in setup you can just call outside of any function. + +p5.clear() + +for(var i = 0; i < 100; i++){ + p5.fill(i*10, i%30, 255) + p5.rect(i*20, 200, 10,200) +} + +// To live code animations, you can redefine the draw function of P5 as follows: +// (a rectangle that follows the mouse) +p5.draw = () => { + p5.fill(p5.mouseX/5, p5.mouseY/5, 255, 100) + p5.rect(p5.mouseX, p5.mouseY, 30, 150) +} + +// To use P5 as an input to hydra, simply use the canvas as a source: +s0.init({src: p5.canvas}) + +// Then render the canvas +src(s0).repeat().out() +``` + +## Loading external scripts +The `await loadScript()` function lets you load other packaged javascript libraries within the hydra editor. Any javascript code can run in the hydra editor. + +Here is an example using Three.js from the web editor: +```javascript +await loadScript("https://threejs.org/build/three.js") + +scene = new THREE.Scene() +camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) + +renderer = new THREE.WebGLRenderer() +renderer.setSize(width, height) +geometry = new THREE.BoxGeometry() +material = new THREE.MeshBasicMaterial({color: 0x00ff00}) +cube = new THREE.Mesh(geometry, material); +scene.add(cube) +camera.position.z = 1.5 + +// 'update' is a reserved function that will be run every time the main hydra rendering context is updated +update = () => { + cube.rotation.x += 0.01; + cube.rotation.y += 0.01; + renderer.render( scene, camera ); +} + +s0.init({ src: renderer.domElement }) + +src(s0).repeat().out() +``` + +And here is an example loading the Tone.js library: +```javascript +await loadScript("https://unpkg.com/tone") + +synth = new Tone.Synth().toDestination(); +synth.triggerAttackRelease("C4", "8n"); +``` + + +## Audio Responsiveness +FFT functionality is available via an audio object accessed via "a". The editor uses https://github.com/meyda/meyda for audio analysis. +To show the fft bins, +``` +a.show() +``` +Set number of fft bins: +``` +a.setBins(6) +``` +Access the value of the leftmost (lowest frequency) bin: +``` +a.fft[0] +``` +Use the value to control a variable: +``` +osc(10, 0, () => (a.fft[0]*4)) + .out() +``` +It is possible to calibrate the responsiveness by changing the minimum and maximum value detected. (Represented by blur lines over the fft). To set minimum value detected: +``` +a.setCutoff(4) +``` + +Setting the scale changes the range that is detected. +``` +a.setScale(2) +``` +The fft[] will return a value between 0 and 1, where 0 represents the cutoff and 1 corresponds to the maximum. + +You can set smoothing between audio level readings (values between 0 and 1). 0 corresponds to no smoothing (more jumpy, faster reaction time), while 1 means that the value will never change. +``` +a.setSmooth(0.8) +``` +To hide the audio waveform: +``` +a.hide() +``` +## MIDI (experimental) + +MIDI controllers can work with Hydra via WebMIDI an example workflow is at [/docs/midi.md](https://github.com/ojack/hydra/blob/master/docs/midi.md) . + +## API + +There is an updated list of functions at [/docs/funcs.md](https://github.com/ojack/hydra/blob/master/docs/funcs.md). + +As well as in the [source code for hydra-synth](https://github.com/ojack/hydra-synth/blob/master/src/glsl/glsl-functions.js). + diff --git a/content/docs/learning/getting-started.md b/content/docs/learning/video-synth-basics/getting-started.md similarity index 87% rename from content/docs/learning/getting-started.md rename to content/docs/learning/video-synth-basics/getting-started.md index aa8752d..9bef957 100644 --- a/content/docs/learning/getting-started.md +++ b/content/docs/learning/video-synth-basics/getting-started.md @@ -1,46 +1,10 @@ --- -title: "Getting Started" +title: "tutorial: hydra basics" date: 2023-04-04T15:10:36+02:00 -draft: true +# draft: true author: "Flor and Olivia" weight: 1 --- -# Getting started - -## Play with gallery examples -To get started, open the the [hydra web editor](https://hydra.ojack.xyz/) in a separate window. Close the top window by clicking the [x] in the top right. - -![](https://i.imgur.com/ZfgVjJZ.gif) - -You will see some colorful visuals in the background with text on top in the top left of the screen. The text is code that generates the visuals behind it. - -The easiest way to get started with hydra is to play around with the example sketches. -At the right up corner you will find a toolbar with these buttons: -![](https://i.imgur.com/iCG8Lrq.png) -1. **run all code** Runs all code on the page (same as typing *ctrl+shift+enter) -2. **upload to gallery** upload a sketch to Hydra's gallery and create a shorter URL -3. **clear all** resets the environment and clears text from the editor -4. **show random sketch**. Loads random sketch examples. Always it is a good way to learn Hydra by studying someone elses code. -5. **make random change** **dices** modify values automatically. Try it with some of the sketch examples. -6. **show info window** show overlay window with help text and links - -## Change some numbers -Change values that appears on Hydra web editor at the sketch examples to see what happens. -Values can have decimal numbers, is recommended not using big values inside the parentheses. Numbers must be **inside** the parentheses. -Every Hydra code starts by generating an input signal source and ends with an output buffer. Adding a value inside or after `.out()` **won't** work. - - -```javascript -osc(10,0.1,0.5).color(1,0.895,0.55).out() -``` - -## Type Ctrl + Shift + Enter -Evaluate the entire code with `ctrl+shift+enter` to run your sketch. - -## Have fun! -:) - - This document is an introduction to making live visuals using Hydra. It covers the basics of writing code in the browser to generate and mix live video sources. No coding or video experience is necessary! diff --git a/content/docs/learning/video-synth-basics.md b/content/docs/learning/video-synth-basics/modular-approach.md similarity index 94% rename from content/docs/learning/video-synth-basics.md rename to content/docs/learning/video-synth-basics/modular-approach.md index fa5c641..16e995c 100644 --- a/content/docs/learning/video-synth-basics.md +++ b/content/docs/learning/video-synth-basics/modular-approach.md @@ -1,17 +1,20 @@ --- title: "video synth basics" -draft: false +draft: true author: "Flor and Olivia" weight: 3 --- # Modular Video Synth Basics -Hydra is inspired by [modular synthesis](https://en.wikipedia.org/wiki/Modular_synthesizer). -Instead of connecting cables you connect different kinds of javascript functions. +Hydra is inspired by [modular synthesis](https://en.wikipedia.org/wiki/Modular_synthesizer), where patching together different functions generates a visual output. + ![](https://i.imgur.com/RBRxeiL.jpg) ###### source [Sandin Image Processor](https://en.wikipedia.org/wiki/Sandin_Image_Processor) -Press the ***run button*** drawing to run this code and update the visuals on the screen. You should see some scrolling stripes appear in the background. +A basic hydra pattern starts with a ***source*** (such as `osc()`(oscillator), `shape()`, or `noise()`), followed by transformations to ***geometry*** and ***color*** (such as `.rotate()`, `.kaleid()`, `.pixelate()` ), and in the end always ends with `.out()` to show the redult on the screen. + +For a detailed tutorial, see "Getting started" + ```hydra osc().out() diff --git a/content/docs/learning/video-synth-basics/sources.md b/content/docs/learning/video-synth-basics/sources.md new file mode 100644 index 0000000..a592611 --- /dev/null +++ b/content/docs/learning/video-synth-basics/sources.md @@ -0,0 +1,7 @@ +--- +title: "sources" +draft: true +author: "Flor and Olivia" +--- + +## hydra functions: sources diff --git a/content/docs/learning/web-editor.md b/content/docs/learning/web-editor.md index 4803acd..7054708 100644 --- a/content/docs/learning/web-editor.md +++ b/content/docs/learning/web-editor.md @@ -6,14 +6,19 @@ author: "Flor and Olivia" weight: 6 --- - -## Get to know the browser editor -To get started, open the the [hydra web editor](https://hydra.ojack.xyz/) in a separate window. Close the top window by clicking the [x] in the top right. - -![](https://i.imgur.com/ZfgVjJZ.gif) - -You will see some colorful visuals in the background with text on top in the top left of the screen. The text is code that generates the visuals behind it. - +# Web editor +basics of the browser editor at https://hydra.ojack.xyz + +## key commands +* CTRL-Enter: run a line of code +* CTRL-Shift-Enter: run all code on screen +* ALT-Enter: run a block +* CTRL-Shift-H: hide or show code +* CTRL-Shift-F: format code using [Prettier](https://prettier.io/) +* CTRL-Shift-S: Save screenshot and download as local file. The +* CTRL-Shift-G: Share to twitter (if available). Shares to [@hydra_patterns](https://twitter.com/hydra_patterns) + +## toolbar At the right up corner you will find a toolbar with these buttons: ![](https://i.imgur.com/iCG8Lrq.png) 1. **run all code** Runs all code on the page (same as typing *ctrl+shift+enter) @@ -24,9 +29,23 @@ At the right up corner you will find a toolbar with these buttons: 6. **show info window** show overlay window with help text and links -## Save your sketch on the internet - - +## share your sketch When you evaluate the entire code with the ***run button*** or with `shift + ctrl + enter`, Hydra automatically generates a URL that contains the last changes of your sketch. You can copy and paste the url from the URL bar to save it or share it with other people. You can also use the browser `back` and `forward` arrows to navigate to earlier versions of your sketch. ![](https://i.imgur.com/lV0rmoh.png) +## sharing with the code hidden +The `showCode=false` url flag makes it possible to share a sketch with the code hidden. + +For example, the following sketch +```javascript +osc(10, 0.1, 1.2).modulateScale(noise(3)).out() +``` + +is available at the URL +https://hydra.ojack.xyz/?code=b3NjKDEwJTJDJTIwMC4xJTJDJTIwMS4yKS5tb2R1bGF0ZVNjYWxlKG5vaXNlKDMpKS5vdXQoKQ%3D%3D + + +this URL will correspond to the same sketch, but with the code and toolbar hidden: +https://hydra.ojack.xyz/?code=b3NjKDEwJTJDJTIwMC4xJTJDJTIwMS4yKS5tb2R1bGF0ZVNjYWxlKG5vaXNlKDMpKS5vdXQoKQ%3D%3D&showCode=false + +pressing `Ctrl+Shift+h` will show the code again \ No newline at end of file diff --git a/content/docs/what-is-hydra.md b/content/docs/what-is-hydra.md deleted file mode 100644 index a005f8d..0000000 --- a/content/docs/what-is-hydra.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: "about" -draft: false -weight: 1 ---- - -# What is Hydra? -![hydra](https://ojack.xyz/articles/hydra/thumb.png) - -Hydra is live code-able video synth and coding environment that runs directly in the browser. It is free and open-source and made for beginners and experts alike. You can simply open the [hydra web editor](https://hydra.ojack.xyz) on a laptop or mobile device to get started. - -Hydra is written in JavaScript and compiles to WebGL under the hood. The syntax is inspired by analog modular synthesis, in which chaining or patching a set of transformations together generates a visual result. - -#### Hydra can be used: -- to mix and add effects to camera feeds, screenshares, live streams, and videos -- to create generative and audio-reactive visuals, and share them online with others -- in combination with other javascript libraries such as P5.js, Tone.js, THREE.js, or gibber -- to add interactive video effects to a website -- to experiment with and learn about video feedback, fractals, and pixel operations -- to stream video between browsers and live-jam with others online - -#### Further resources and next steps -For more information and instructions, see: [Getting Started](getting_started), [a list of hydra functions](https://hydra.ojack.xyz/api/), [the community database of projects and tutorials](https://hydra.ojack.xyz/garden/), [a gallery of user-generated sketches](https://twitter.com/hydra_patterns), and [the source code on github](https://github.com/hydra-synth/hydra). - -Hydra was created by [olivia jack](https://ojack.xyz) and is supported by a community of contributers. If you enjoy using Hydra, please consider [supporting continued development](https://opencollective.com/hydra-synth). - -Next: [Getting Started](getting_started) \ No newline at end of file diff --git a/content/images/hydra.png b/content/images/hydra.png new file mode 100644 index 0000000..3e811ce Binary files /dev/null and b/content/images/hydra.png differ diff --git a/layouts/index.html b/layouts/_old_index.html similarity index 100% rename from layouts/index.html rename to layouts/_old_index.html diff --git a/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.content b/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.content index c718928..a61dba6 100644 --- a/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.content +++ b/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.content @@ -1 +1 @@ -@charset "UTF-8";:root{--gray-100:#f8f9fa;--gray-200:#e9ecef;--gray-500:#adb5bd;--color-link:#0055bb;--color-visited-link:#8440f1;--body-background:white;--body-font-color:black;--icon-filter:none;--hint-color-info:#6bf;--hint-color-warning:#fd6;--hint-color-danger:#f66}/*!normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css*/html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}.flex{display:flex}.flex-auto{flex:auto}.flex-even{flex:1 1}.flex-wrap{flex-wrap:wrap}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.align-center{align-items:center}.mx-auto{margin:0 auto}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.hidden{display:none}input.toggle{height:0;width:0;overflow:hidden;opacity:0;position:absolute}.clearfix::after{content:"";display:table;clear:both}html{font-size:16px;scroll-behavior:smooth;touch-action:manipulation}body{min-width:20rem;color:var(--body-font-color);background:var(--body-background);letter-spacing:.33px;font-weight:400;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;box-sizing:border-box}body *{box-sizing:inherit}h1,h2,h3,h4,h5{font-weight:400}a{text-decoration:none;color:var(--color-link)}img{vertical-align:baseline}:focus{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}aside nav ul{padding:0;margin:0;list-style:none}aside nav ul li{margin:1em 0;position:relative}aside nav ul a{display:block}aside nav ul a:hover{opacity:.5}aside nav ul ul{padding-inline-start:1rem}ul.pagination{display:flex;justify-content:center;list-style-type:none;padding-inline-start:0}ul.pagination .page-item a{padding:1rem}.container{max-width:80rem;margin:0 auto}.book-icon{filter:var(--icon-filter)}.book-brand{margin-top:0;margin-bottom:1rem}.book-brand img{height:1.5em;width:1.5em;margin-inline-end:.5rem}.book-menu{flex:0 0 16rem;font-size:.875rem}.book-menu .book-menu-content{width:16rem;padding:1rem;background:var(--body-background);position:fixed;top:0;bottom:0;overflow-x:hidden;overflow-y:auto}.book-menu a,.book-menu label{color:inherit;cursor:pointer;word-wrap:break-word}.book-menu a.active{color:var(--color-link)}.book-menu input.toggle+label+ul{display:none}.book-menu input.toggle:checked+label+ul{display:block}.book-menu input.toggle+label::after{content:"▸"}.book-menu input.toggle:checked+label::after{content:"▾"}body[dir=rtl] .book-menu input.toggle+label::after{content:"◂"}body[dir=rtl] .book-menu input.toggle:checked+label::after{content:"▾"}.book-section-flat{margin:2rem 0}.book-section-flat>a,.book-section-flat>span,.book-section-flat>label{font-weight:bolder}.book-section-flat>ul{padding-inline-start:0}.book-page{min-width:20rem;flex-grow:1;padding:1rem}.book-post{margin-bottom:3rem}.book-header{display:none;margin-bottom:1rem}.book-header label{line-height:0}.book-header img.book-icon{height:1.5em;width:1.5em}.book-search{position:relative;margin:1rem 0;border-bottom:1px solid transparent}.book-search input{width:100%;padding:.5rem;border:0;border-radius:.25rem;background:var(--gray-100);color:var(--body-font-color)}.book-search input:required+.book-search-spinner{display:block}.book-search .book-search-spinner{position:absolute;top:0;margin:.5rem;margin-inline-start:calc(100% - 1.5rem);width:1rem;height:1rem;border:1px solid transparent;border-top-color:var(--body-font-color);border-radius:50%;animation:spin 1s ease infinite}@keyframes spin{100%{transform:rotate(360deg)}}.book-search small{opacity:.5}.book-toc{flex:0 0 16rem;font-size:.75rem}.book-toc .book-toc-content{width:16rem;padding:1rem;position:fixed;top:0;bottom:0;overflow-x:hidden;overflow-y:auto}.book-toc img{height:1em;width:1em}.book-toc nav>ul>li:first-child{margin-top:0}.book-footer{padding-top:1rem;font-size:.875rem}.book-footer img{height:1em;width:1em;margin-inline-end:.5rem}.book-comments{margin-top:1rem}.book-languages{margin-block-end:2rem}.book-languages .book-icon{height:1em;width:1em;margin-inline-end:.5em}.book-languages ul{padding-inline-start:1.5em}.book-menu-content,.book-toc-content,.book-page,.book-header aside,.markdown{transition:.2s ease-in-out;transition-property:transform,margin,opacity,visibility;will-change:transform,margin,opacity}@media screen and (max-width:56rem){#menu-control,#toc-control{display:inline}.book-menu{visibility:hidden;margin-inline-start:-16rem;font-size:16px;z-index:1}.book-toc{display:none}.book-header{display:block}#menu-control:focus~main label[for=menu-control]{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}#menu-control:checked~main .book-menu{visibility:initial}#menu-control:checked~main .book-menu .book-menu-content{transform:translateX(16rem);box-shadow:0 0 .5rem rgba(0,0,0,.1)}#menu-control:checked~main .book-page{opacity:.25}#menu-control:checked~main .book-menu-overlay{display:block;position:absolute;top:0;bottom:0;left:0;right:0}#toc-control:focus~main label[for=toc-control]{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}#toc-control:checked~main .book-header aside{display:block}body[dir=rtl] #menu-control:checked~main .book-menu .book-menu-content{transform:translateX(-16rem)}}@media screen and (min-width:80rem){.book-page,.book-menu .book-menu-content,.book-toc .book-toc-content{padding:2rem 1rem}}@font-face{font-family:roboto;font-style:normal;font-weight:400;font-display:swap;src:local(""),url(fonts/roboto-v27-latin-regular.woff2)format("woff2"),url(fonts/roboto-v27-latin-regular.woff)format("woff")}@font-face{font-family:roboto;font-style:normal;font-weight:700;font-display:swap;src:local(""),url(fonts/roboto-v27-latin-700.woff2)format("woff2"),url(fonts/roboto-v27-latin-700.woff)format("woff")}@font-face{font-family:roboto mono;font-style:normal;font-weight:400;font-display:swap;src:local(""),url(fonts/roboto-mono-v13-latin-regular.woff2)format("woff2"),url(fonts/roboto-mono-v13-latin-regular.woff)format("woff")}body{font-family:roboto,sans-serif}code{font-family:roboto mono,monospace}@media print{.book-menu,.book-footer,.book-toc{display:none}.book-header,.book-header aside{display:block}main{display:block!important}}.markdown{line-height:1.6}.markdown>:first-child{margin-top:0}.markdown h1,.markdown h2,.markdown h3,.markdown h4,.markdown h5,.markdown h6{font-weight:400;line-height:1;margin-top:1.5em;margin-bottom:1rem}.markdown h1 a.anchor,.markdown h2 a.anchor,.markdown h3 a.anchor,.markdown h4 a.anchor,.markdown h5 a.anchor,.markdown h6 a.anchor{opacity:0;font-size:.75em;vertical-align:middle;text-decoration:none}.markdown h1:hover a.anchor,.markdown h1 a.anchor:focus,.markdown h2:hover a.anchor,.markdown h2 a.anchor:focus,.markdown h3:hover a.anchor,.markdown h3 a.anchor:focus,.markdown h4:hover a.anchor,.markdown h4 a.anchor:focus,.markdown h5:hover a.anchor,.markdown h5 a.anchor:focus,.markdown h6:hover a.anchor,.markdown h6 a.anchor:focus{opacity:initial}.markdown h4,.markdown h5,.markdown h6{font-weight:bolder}.markdown h5{font-size:.875em}.markdown h6{font-size:.75em}.markdown b,.markdown optgroup,.markdown strong{font-weight:bolder}.markdown a{text-decoration:none}.markdown a:hover{text-decoration:underline}.markdown a:visited{color:var(--color-visited-link)}.markdown img{max-width:100%;height:auto}.markdown code{padding:0 .25rem;background:var(--gray-200);border-radius:.25rem;font-size:.875em}.markdown pre{padding:1rem;background:var(--gray-100);border-radius:.25rem;overflow-x:auto}.markdown pre code{padding:0;background:0 0}.markdown p{word-wrap:break-word}.markdown blockquote{margin:1rem 0;padding:.5rem 1rem .5rem .75rem;border-inline-start:.25rem solid var(--gray-200);border-radius:.25rem}.markdown blockquote :first-child{margin-top:0}.markdown blockquote :last-child{margin-bottom:0}.markdown table{overflow:auto;display:block;border-spacing:0;border-collapse:collapse;margin-top:1rem;margin-bottom:1rem}.markdown table tr th,.markdown table tr td{padding:.5rem 1rem;border:1px solid var(--gray-200)}.markdown table tr:nth-child(2n){background:var(--gray-100)}.markdown hr{height:1px;border:none;background:var(--gray-200)}.markdown ul,.markdown ol{padding-inline-start:2rem}.markdown dl dt{font-weight:bolder;margin-top:1rem}.markdown dl dd{margin-inline-start:0;margin-bottom:1rem}.markdown .highlight table tr td:nth-child(1) pre{margin:0;padding-inline-end:0}.markdown .highlight table tr td:nth-child(2) pre{margin:0;padding-inline-start:0}.markdown details{padding:1rem;border:1px solid var(--gray-200);border-radius:.25rem}.markdown details summary{line-height:1;padding:1rem;margin:-1rem;cursor:pointer}.markdown details[open] summary{margin-bottom:0}.markdown figure{margin:1rem 0}.markdown figure figcaption p{margin-top:0}.markdown-inner>:first-child{margin-top:0}.markdown-inner>:last-child{margin-bottom:0}.markdown .book-expand{margin-top:1rem;margin-bottom:1rem;border:1px solid var(--gray-200);border-radius:.25rem;overflow:hidden}.markdown .book-expand .book-expand-head{background:var(--gray-100);padding:.5rem 1rem;cursor:pointer}.markdown .book-expand .book-expand-content{display:none;padding:1rem}.markdown .book-expand input[type=checkbox]:checked+.book-expand-content{display:block}.markdown .book-tabs{margin-top:1rem;margin-bottom:1rem;border:1px solid var(--gray-200);border-radius:.25rem;overflow:hidden;display:flex;flex-wrap:wrap}.markdown .book-tabs label{display:inline-block;padding:.5rem 1rem;border-bottom:1px transparent;cursor:pointer}.markdown .book-tabs .book-tabs-content{order:999;width:100%;border-top:1px solid var(--gray-100);padding:1rem;display:none}.markdown .book-tabs input[type=radio]:checked+label{border-bottom:1px solid var(--color-link)}.markdown .book-tabs input[type=radio]:checked+label+.book-tabs-content{display:block}.markdown .book-tabs input[type=radio]:focus+label{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}.markdown .book-columns{margin-left:-1rem;margin-right:-1rem}.markdown .book-columns>div{margin:1rem 0;min-width:10rem;padding:0 1rem}.markdown a.book-btn{display:inline-block;font-size:.875rem;color:var(--color-link);line-height:2rem;padding:0 1rem;border:1px solid var(--color-link);border-radius:.25rem;cursor:pointer}.markdown a.book-btn:hover{text-decoration:none}.markdown .book-hint.info{border-color:#6bf;background-color:rgba(102,187,255,.1)}.markdown .book-hint.warning{border-color:#fd6;background-color:rgba(255,221,102,.1)}.markdown .book-hint.danger{border-color:#f66;background-color:rgba(255,102,102,.1)}@font-face{font-family:terminal_grotesque;src:url(fonts/terminal-grotesque-webfont.eot);src:url(fonts/terminal-grotesque-webfont.eot?#iefix)format("embedded-opentype"),url(fonts/terminal-grotesque-webfont.woff2)format("woff2"),url(fonts/terminal-grotesque-webfont.woff)format("woff"),url(fonts/terminal-grotesque-webfont.ttf)format("truetype"),url(fonts/terminal-grotesque-webfont.svg#terminal_grotesqueregular)format("svg");font-weight:400;font-style:normal}body{font-family:terminal_grotesque,sans-serif;line-height:.3 rem}aside nav ul li{margin:.1 0}.book-brand{font-size:3.6rem;line-height:1.2rem}.book-menu a{width:100%;line-height:.8}.book-menu a.active{color:#ef0981;font-size:1.3rem}.markdown a:visited{color:#ef0981}a{color:#ef0981} \ No newline at end of file +@charset "UTF-8";:root{--gray-100:#f8f9fa;--gray-200:#e9ecef;--gray-500:#adb5bd;--color-link:#0055bb;--color-visited-link:#8440f1;--body-background:white;--body-font-color:black;--icon-filter:none;--hint-color-info:#6bf;--hint-color-warning:#fd6;--hint-color-danger:#f66}/*!normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css*/html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}.flex{display:flex}.flex-auto{flex:auto}.flex-even{flex:1 1}.flex-wrap{flex-wrap:wrap}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.align-center{align-items:center}.mx-auto{margin:0 auto}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.hidden{display:none}input.toggle{height:0;width:0;overflow:hidden;opacity:0;position:absolute}.clearfix::after{content:"";display:table;clear:both}html{font-size:16px;scroll-behavior:smooth;touch-action:manipulation}body{min-width:20rem;color:var(--body-font-color);background:var(--body-background);letter-spacing:.33px;font-weight:400;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;box-sizing:border-box}body *{box-sizing:inherit}h1,h2,h3,h4,h5{font-weight:400}a{text-decoration:none;color:var(--color-link)}img{vertical-align:baseline}:focus{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}aside nav ul{padding:0;margin:0;list-style:none}aside nav ul li{margin:1em 0;position:relative}aside nav ul a{display:block}aside nav ul a:hover{opacity:.5}aside nav ul ul{padding-inline-start:1rem}ul.pagination{display:flex;justify-content:center;list-style-type:none;padding-inline-start:0}ul.pagination .page-item a{padding:1rem}.container{max-width:80rem;margin:0 auto}.book-icon{filter:var(--icon-filter)}.book-brand{margin-top:0;margin-bottom:1rem}.book-brand img{height:1.5em;width:1.5em;margin-inline-end:.5rem}.book-menu{flex:0 0 16rem;font-size:.875rem}.book-menu .book-menu-content{width:16rem;padding:1rem;background:var(--body-background);position:fixed;top:0;bottom:0;overflow-x:hidden;overflow-y:auto}.book-menu a,.book-menu label{color:inherit;cursor:pointer;word-wrap:break-word}.book-menu a.active{color:var(--color-link)}.book-menu input.toggle+label+ul{display:none}.book-menu input.toggle:checked+label+ul{display:block}.book-menu input.toggle+label::after{content:"▸"}.book-menu input.toggle:checked+label::after{content:"▾"}body[dir=rtl] .book-menu input.toggle+label::after{content:"◂"}body[dir=rtl] .book-menu input.toggle:checked+label::after{content:"▾"}.book-section-flat{margin:2rem 0}.book-section-flat>a,.book-section-flat>span,.book-section-flat>label{font-weight:bolder}.book-section-flat>ul{padding-inline-start:0}.book-page{min-width:20rem;flex-grow:1;padding:1rem}.book-post{margin-bottom:3rem}.book-header{display:none;margin-bottom:1rem}.book-header label{line-height:0}.book-header img.book-icon{height:1.5em;width:1.5em}.book-search{position:relative;margin:1rem 0;border-bottom:1px solid transparent}.book-search input{width:100%;padding:.5rem;border:0;border-radius:.25rem;background:var(--gray-100);color:var(--body-font-color)}.book-search input:required+.book-search-spinner{display:block}.book-search .book-search-spinner{position:absolute;top:0;margin:.5rem;margin-inline-start:calc(100% - 1.5rem);width:1rem;height:1rem;border:1px solid transparent;border-top-color:var(--body-font-color);border-radius:50%;animation:spin 1s ease infinite}@keyframes spin{100%{transform:rotate(360deg)}}.book-search small{opacity:.5}.book-toc{flex:0 0 16rem;font-size:.75rem}.book-toc .book-toc-content{width:16rem;padding:1rem;position:fixed;top:0;bottom:0;overflow-x:hidden;overflow-y:auto}.book-toc img{height:1em;width:1em}.book-toc nav>ul>li:first-child{margin-top:0}.book-footer{padding-top:1rem;font-size:.875rem}.book-footer img{height:1em;width:1em;margin-inline-end:.5rem}.book-comments{margin-top:1rem}.book-languages{margin-block-end:2rem}.book-languages .book-icon{height:1em;width:1em;margin-inline-end:.5em}.book-languages ul{padding-inline-start:1.5em}.book-menu-content,.book-toc-content,.book-page,.book-header aside,.markdown{transition:.2s ease-in-out;transition-property:transform,margin,opacity,visibility;will-change:transform,margin,opacity}@media screen and (max-width:56rem){#menu-control,#toc-control{display:inline}.book-menu{visibility:hidden;margin-inline-start:-16rem;font-size:16px;z-index:1}.book-toc{display:none}.book-header{display:block}#menu-control:focus~main label[for=menu-control]{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}#menu-control:checked~main .book-menu{visibility:initial}#menu-control:checked~main .book-menu .book-menu-content{transform:translateX(16rem);box-shadow:0 0 .5rem rgba(0,0,0,.1)}#menu-control:checked~main .book-page{opacity:.25}#menu-control:checked~main .book-menu-overlay{display:block;position:absolute;top:0;bottom:0;left:0;right:0}#toc-control:focus~main label[for=toc-control]{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}#toc-control:checked~main .book-header aside{display:block}body[dir=rtl] #menu-control:checked~main .book-menu .book-menu-content{transform:translateX(-16rem)}}@media screen and (min-width:80rem){.book-page,.book-menu .book-menu-content,.book-toc .book-toc-content{padding:2rem 1rem}}@font-face{font-family:roboto;font-style:normal;font-weight:400;font-display:swap;src:local(""),url(fonts/roboto-v27-latin-regular.woff2)format("woff2"),url(fonts/roboto-v27-latin-regular.woff)format("woff")}@font-face{font-family:roboto;font-style:normal;font-weight:700;font-display:swap;src:local(""),url(fonts/roboto-v27-latin-700.woff2)format("woff2"),url(fonts/roboto-v27-latin-700.woff)format("woff")}@font-face{font-family:roboto mono;font-style:normal;font-weight:400;font-display:swap;src:local(""),url(fonts/roboto-mono-v13-latin-regular.woff2)format("woff2"),url(fonts/roboto-mono-v13-latin-regular.woff)format("woff")}body{font-family:roboto,sans-serif}code{font-family:roboto mono,monospace}@media print{.book-menu,.book-footer,.book-toc{display:none}.book-header,.book-header aside{display:block}main{display:block!important}}.markdown{line-height:1.6}.markdown>:first-child{margin-top:0}.markdown h1,.markdown h2,.markdown h3,.markdown h4,.markdown h5,.markdown h6{font-weight:400;line-height:1;margin-top:1.5em;margin-bottom:1rem}.markdown h1 a.anchor,.markdown h2 a.anchor,.markdown h3 a.anchor,.markdown h4 a.anchor,.markdown h5 a.anchor,.markdown h6 a.anchor{opacity:0;font-size:.75em;vertical-align:middle;text-decoration:none}.markdown h1:hover a.anchor,.markdown h1 a.anchor:focus,.markdown h2:hover a.anchor,.markdown h2 a.anchor:focus,.markdown h3:hover a.anchor,.markdown h3 a.anchor:focus,.markdown h4:hover a.anchor,.markdown h4 a.anchor:focus,.markdown h5:hover a.anchor,.markdown h5 a.anchor:focus,.markdown h6:hover a.anchor,.markdown h6 a.anchor:focus{opacity:initial}.markdown h4,.markdown h5,.markdown h6{font-weight:bolder}.markdown h5{font-size:.875em}.markdown h6{font-size:.75em}.markdown b,.markdown optgroup,.markdown strong{font-weight:bolder}.markdown a{text-decoration:none}.markdown a:hover{text-decoration:underline}.markdown a:visited{color:var(--color-visited-link)}.markdown img{max-width:100%;height:auto}.markdown code{padding:0 .25rem;background:var(--gray-200);border-radius:.25rem;font-size:.875em}.markdown pre{padding:1rem;background:var(--gray-100);border-radius:.25rem;overflow-x:auto}.markdown pre code{padding:0;background:0 0}.markdown p{word-wrap:break-word}.markdown blockquote{margin:1rem 0;padding:.5rem 1rem .5rem .75rem;border-inline-start:.25rem solid var(--gray-200);border-radius:.25rem}.markdown blockquote :first-child{margin-top:0}.markdown blockquote :last-child{margin-bottom:0}.markdown table{overflow:auto;display:block;border-spacing:0;border-collapse:collapse;margin-top:1rem;margin-bottom:1rem}.markdown table tr th,.markdown table tr td{padding:.5rem 1rem;border:1px solid var(--gray-200)}.markdown table tr:nth-child(2n){background:var(--gray-100)}.markdown hr{height:1px;border:none;background:var(--gray-200)}.markdown ul,.markdown ol{padding-inline-start:2rem}.markdown dl dt{font-weight:bolder;margin-top:1rem}.markdown dl dd{margin-inline-start:0;margin-bottom:1rem}.markdown .highlight table tr td:nth-child(1) pre{margin:0;padding-inline-end:0}.markdown .highlight table tr td:nth-child(2) pre{margin:0;padding-inline-start:0}.markdown details{padding:1rem;border:1px solid var(--gray-200);border-radius:.25rem}.markdown details summary{line-height:1;padding:1rem;margin:-1rem;cursor:pointer}.markdown details[open] summary{margin-bottom:0}.markdown figure{margin:1rem 0}.markdown figure figcaption p{margin-top:0}.markdown-inner>:first-child{margin-top:0}.markdown-inner>:last-child{margin-bottom:0}.markdown .book-expand{margin-top:1rem;margin-bottom:1rem;border:1px solid var(--gray-200);border-radius:.25rem;overflow:hidden}.markdown .book-expand .book-expand-head{background:var(--gray-100);padding:.5rem 1rem;cursor:pointer}.markdown .book-expand .book-expand-content{display:none;padding:1rem}.markdown .book-expand input[type=checkbox]:checked+.book-expand-content{display:block}.markdown .book-tabs{margin-top:1rem;margin-bottom:1rem;border:1px solid var(--gray-200);border-radius:.25rem;overflow:hidden;display:flex;flex-wrap:wrap}.markdown .book-tabs label{display:inline-block;padding:.5rem 1rem;border-bottom:1px transparent;cursor:pointer}.markdown .book-tabs .book-tabs-content{order:999;width:100%;border-top:1px solid var(--gray-100);padding:1rem;display:none}.markdown .book-tabs input[type=radio]:checked+label{border-bottom:1px solid var(--color-link)}.markdown .book-tabs input[type=radio]:checked+label+.book-tabs-content{display:block}.markdown .book-tabs input[type=radio]:focus+label{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}.markdown .book-columns{margin-left:-1rem;margin-right:-1rem}.markdown .book-columns>div{margin:1rem 0;min-width:10rem;padding:0 1rem}.markdown a.book-btn{display:inline-block;font-size:.875rem;color:var(--color-link);line-height:2rem;padding:0 1rem;border:1px solid var(--color-link);border-radius:.25rem;cursor:pointer}.markdown a.book-btn:hover{text-decoration:none}.markdown .book-hint.info{border-color:#6bf;background-color:rgba(102,187,255,.1)}.markdown .book-hint.warning{border-color:#fd6;background-color:rgba(255,221,102,.1)}.markdown .book-hint.danger{border-color:#f66;background-color:rgba(255,102,102,.1)}@font-face{font-family:terminal_grotesque;src:url(fonts/terminal-grotesque-webfont.eot);src:url(fonts/terminal-grotesque-webfont.eot?#iefix)format("embedded-opentype"),url(fonts/terminal-grotesque-webfont.woff2)format("woff2"),url(fonts/terminal-grotesque-webfont.woff)format("woff"),url(fonts/terminal-grotesque-webfont.ttf)format("truetype"),url(fonts/terminal-grotesque-webfont.svg#terminal_grotesqueregular)format("svg");font-weight:400;font-style:normal}body{font-family:terminal_grotesque,sans-serif;line-height:.3 rem}aside nav ul li{margin:.1 0}.book-brand{font-size:3.6rem;line-height:1.2rem}.book-menu a{width:100%;line-height:.8}.book-menu a.active{color:#ef0981;text-decoration:underline;font-size:1.3rem}a{color:#ef0981} \ No newline at end of file diff --git a/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.json b/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.json index 09b688a..ee4259a 100644 --- a/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.json +++ b/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.json @@ -1 +1 @@ -{"Target":"book.min.764ef57890cd736c914487dc52ca7072395ab26399f50b08c0a94deb0a94baa9.css","MediaType":"text/css","Data":{"Integrity":"sha256-dk71eJDNc2yRRIfcUspwcjlasmOZ9QsIwKlN6wqUuqk="}} \ No newline at end of file +{"Target":"book.min.7dc3cbcfe32da8c8e8f510fcad853e3196079d1ec02ef98ab97f61816b3b9a84.css","MediaType":"text/css","Data":{"Integrity":"sha256-fcPLz+MtqMjo9RD8rYU+MZYHnR7ALvmKuX9hgWs7moQ="}} \ No newline at end of file