├── .gitignore
├── .nojekyll
├── CNAME
├── images
├── cover.png
└── color-remapping-animation.gif
├── symbols
└── external.png
├── embed.md
├── layers.md
├── _sidebar.md
├── LICENSE
├── feedback.md
├── performing.md
├── textures.md
├── style.css
├── javascript.md
├── README.md
├── arithmetic.md
├── code-as-state.md
├── geometry.md
├── motions.md
├── glsl.md
├── thoughts-on-modulation.md
├── index.html
├── modulation.md
├── colors.md
├── script.js
└── 100questions.md
/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | hydra-book.glitches.me
--------------------------------------------------------------------------------
/images/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micuat/hydra-book/HEAD/images/cover.png
--------------------------------------------------------------------------------
/symbols/external.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micuat/hydra-book/HEAD/symbols/external.png
--------------------------------------------------------------------------------
/images/color-remapping-animation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micuat/hydra-book/HEAD/images/color-remapping-animation.gif
--------------------------------------------------------------------------------
/embed.md:
--------------------------------------------------------------------------------
1 | Embedding Hydra in a Webpage
2 | ========
3 |
4 | example: https://hydra-long-webpage.glitch.me/
5 |
6 | example: https://hydra-scrollable-webpage.glitch.me/
7 |
--------------------------------------------------------------------------------
/layers.md:
--------------------------------------------------------------------------------
1 | Layers
2 | ========
3 |
4 | Video Tutorial
5 | --------
6 |
7 |
8 |
--------------------------------------------------------------------------------
/_sidebar.md:
--------------------------------------------------------------------------------
1 | * [Hydra Book](/)
2 |
3 | ---
4 |
5 | * [Textures](textures)
6 | * [Geometry](geometry)
7 | * [Modulation](modulation)
8 | * [Colors](colors)
9 | * [Layers](layers)
10 | * [Arithmetic](arithmetic)
11 | * [Motions](motions)
12 | * [Feedback](feedback)
13 | * [Custom GLSL](glsl)
14 |
15 | ---
16 |
17 | * [JavaScript Tips](javascript)
18 | * [Embed Hydra in Webpage](embed)
19 | * [Performing with Hydra](performing)
20 | * [100 Questions](100questions)
21 |
22 | ---
23 |
24 | * [Thoughts on modulation](thoughts-on-modulation)
25 | * [Code as state](code-as-state)
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/feedback.md:
--------------------------------------------------------------------------------
1 | Feedback
2 | ========
3 |
4 | Video Tutorial
5 | --------
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Layering
15 | --------
16 |
17 | Putting a layer on top (e.g., `mask(shape(4,0.3,0)`) of the previous frame (`src(s0)`) is a good starting point to work with feedback. For example, moving the previous frame by `scroll`:
18 |
19 | ```hydra
20 | src(o0)
21 | .scroll(.003,.006)
22 | .layer(
23 | osc(30,0.1,1.5).mask(shape(4,0.3,0.01))
24 | ).out(o0)
25 | ```
26 |
27 | Or you can use other functions like `modulateScale` to modify the image. Notice that always keep the parameters small so that the feedback is under "control":
28 |
29 | ```hydra
30 | src(o0)
31 | .modulateScale(osc(6,.5),0.01)
32 | .layer(
33 | osc(30,0.1,1.5).mask(shape(4,0.3,0))
34 | ).out(o0)
35 | ```
36 |
37 | And `modulateHue` with `src(o0)` as modulator - the source is scaled slightly so that the pixels are not stuck in one place:
38 |
39 | ```hydra
40 | src(o0)
41 | .modulateHue(src(o0).scale(1.01),1)
42 | .layer(
43 | osc(30,0.1,1.5).mask(shape(4,0.3,0))
44 | ).out(o0)
45 | ```
46 |
47 | Using [remapping](colors?id=color-remapping) technique, you can move pixels in a more unpredictable, or fluid-like manner. Note that the modulator `osc`'s color is adjusted to -0.5 to 0.5 using `brightness(-0.5)`:
48 |
49 | ```hydra
50 | src(o0)
51 | .modulate(
52 | osc(6,0,1.5).modulate(noise(3).sub(gradient()),1)
53 | .brightness(-0.5)
54 | ,0.003)
55 | .layer(
56 | osc(30,0.1,1.5).mask(shape(4,0.3,0))
57 | ).out(o0)
58 | ```
59 |
60 | with `voronoi` (note that the color palette `osc`'s parameter is changed from 6 to 12 - this is because `noise`'s output range is -1 to 1 but `voronoi` is 0 to 1):
61 |
62 | ```hydra
63 | src(o0)
64 | .modulate(
65 | osc(12,0,1.5).modulate(voronoi(6).sub(gradient()),1)
66 | .brightness(-0.5)
67 | ,0.003)
68 | .layer(
69 | osc(30,0.1,1.5).mask(shape(4,0.3,0))
70 | ).out(o0)
71 | ```
72 |
73 | Scaling
74 | --------
75 |
76 | `scale` function with slightly bigger number than 1.0 also goes well with layering:
77 |
78 | ```hydra
79 | src(o0)
80 | .scale(1.1)
81 | .layer(
82 | osc(30,0.1,1.5).mask(shape(4,0.3,0))
83 | ).out(o0)
84 | ```
85 |
86 | Another technique is to use `gradient` as a modulator. This is not intuitive, but makes a crisp effect compared to normal `scale` function:
87 |
88 | ```hydra
89 | src(o0)
90 | .modulate(gradient().pixelate(2,2).brightness(-0.5)
91 | ,-0.1)
92 | .layer(
93 | osc(30,0.1,1.5).mask(shape(4,0.3,0))
94 | ).out(o0)
95 | ```
96 |
--------------------------------------------------------------------------------
/performing.md:
--------------------------------------------------------------------------------
1 | Performing with Hydra
2 | ========
3 |
4 | Flok
5 | --------
6 |
7 | Performing with live-coding sounds intimidating at first. Nevertheless, the live-coding community is inclusive and there is no "fail" in live coding. A first step may be to do a jam with friends on [Flok](https://flok.clic.cf/), for example, to get used to the interactive and improvisational nature of coding. Enter "hydra" in the text box and press "Create Session" for opening one editor, or you can create multiple editors, e.g., "hydra,hydra,hydra,hydra" to open 4 editors. Enter your name, and make sure "Enable Hydra" is checked. Share the url with friends, or on Facebook group, Telegram group or toplap chat, to invite others.
8 |
9 | The editor is almost identical to Hydra, but you can press control+enter to evaluate the block of code. If you are using four editors, you may want to use `render()` to split the screen into four. Beware that the order is slightly different; Flok shows editor 1 on top right and 2 on bottom left, but Hydra shows buffer 1 on bottom left and buffer 2 on top right.
10 |
11 | Pre-coded or from Scratch
12 | --------
13 |
14 | There is no strict definition of live coding. [Toplap Manifesto](https://toplap.org/wiki/ManifestoDraft) can be a starting point, but you can create your own manifesto. Given that, starting from a pre-coded script or from a blank editor is up to you. Or you can even add sensors or other tools to generate videos.
15 |
16 | Preparing a code has an advantage of making a set or a narrative; let's take a look at an example:
17 |
18 | (note that the current embedded editor does not support "execute by line/block" - please open it in the full editor to try out)
19 |
20 |
21 | ```hydra
22 | // 1
23 | osc(60,0.1,1.5).modulate(o1).out(o0)
24 | solid().out(o1)
25 |
26 | // 2
27 | src(o1).blend(noise(3),0.1).out(o1)
28 |
29 | // 3
30 | src(o0).blend(osc(20,0.1).kaleid(4).kaleid(4),0.1).out(o0)
31 | src(o0).blend(osc(20,0.1).kaleid(4).kaleid(4),0.3).out(o0)
32 | src(o0).blend(osc(20,0.1).kaleid(4).kaleid(4),1).out(o0)
33 | ```
34 |
35 | First, evaluate the first block of `osc` and `solid`. At this point, `modulate` does not affect anything because `o1` is blank. Then, by evaluating the second block, the texture is modulated by the noise texture. `blend` with feedback makes the transition smooth. Next, evaluate the first line of the last block will make the texture crossfade to a distorted oscillator. However, because of `blend`, the texture look blurry. This can be fixed by gradually changing the `blend` parameter to 1. In the example, there are three lines in the third block, but in a performance, you may want to keep only the first line and simply edit the parameter.
36 |
37 | Starting from a blank editor, or from scratch, is both simpler and more difficult. The author prefers this style as it always brings surprise. Advice is to get used to some patterns as a repertoire. You do not have to remember it exactly by heart; but knowing a combination of functions that you like, performance becomes easier as you can continue by changing colors and parameters. Another tip is not to be afraid of clearing the editor. At one point in a performance, the code becomes gigantic or the texture becomes too beautiful that you do not want to edit. If this happens, make a copy if the session is not recorded, and clear the editor. You will soon find another inspiration and the transition gives a good surprise to the audience.
38 |
--------------------------------------------------------------------------------
/textures.md:
--------------------------------------------------------------------------------
1 | Textures
2 | ========
3 |
4 | In this chapter, we discuss textures or patterns, separately from colors or movements. Most of the snippets have low saturation in order to separate textures from other effects.
5 |
6 | Oscillator
7 | --------
8 |
9 | `osc(freq,sync,offset)` is one of the basic sources to create a texture. The first argument determines the frequency (i.e., how packed the stripes are), the second for the sync (i.e., the scroll speed), and the third for the offset, which adds color to the pattern. One cycle of an oscillator in the screen space can be achieved by `osc(Math.PI * 2)`; thus the following example shows 10 cycles:
10 |
11 | ```hydra
12 | osc(Math.PI*2*10,0).out(o0)
13 | ```
14 |
15 | For simplicity, natural numbers are often used as `freq` or the first argument (e.g., `osc(40,0)`). The sync parameter is multiplied with `time` and `freq`; thus even if `sync` is unchanged, the larger the frequency, the faster the scroll speed (discussed in [motions](motions#low-frequency-oscillator)). `offset` cycles from 0 to `PI*2`, which shifts the [color](colors#oscillator).
16 |
17 | By adding `thresh()` or `posterize()`, the oscillator pattern becomes clear stripes. `thresh(threshold)` literally thresholds the grayscale value; if the pixel's grayscale is brighter than `threshold`, returns white and else returns black (alpha is preserved). `posterize(bins,gamma)` thresholds with multiple steps, similar to histogram. `pixelate()` achieves a similar effect; however, the offset between the bumps of the oscillator and the pixelation bins can create artifacts.
18 |
19 | (`render()` displays four buffers; `o0` on top left, `o1` on bottom left, `o2` on top right and `o3` on bottom right)
20 |
21 | ```hydra
22 | osc(40,0).out(o0)
23 | src(o0).thresh().out(o1)
24 | src(o0).posterize(3,1).out(o2)
25 | src(o0).pixelate(20, 20).out(o3)
26 | render()
27 | ```
28 |
29 | `kaleid()` with a large number creates circles,
30 |
31 | ```hydra
32 | osc(200, 0).kaleid(99).out(o0)
33 | ```
34 |
35 | 99 is a magic number; to save character counts (which is essential for live coding), 99 is big enough and only takes 2 characters. However, depending on the effect you want to create, you might need to set a higher number, such as 999, or 1e4.
36 |
37 | You might have noticed that this sketch is stretched if the window is not square. `scale(amount,x,y)` can correct the scaling; it scales `amount*x` to x-axis and `amount*y` to y-axis. Therefore, `scale(1,1,16/9)` fits the sketch to 16:9 window, and in general,
38 |
39 | ```javascript
40 | scale(1,1,()=>window.innerWidth/window.innerHeight)
41 | ```
42 |
43 | adapts the sketch to any size of the window. Notice `()=>`, which is an arrow function. If a value is passed to a hydra function (e.g., `scale(1,1,window.innerWidth/window.innerHeight)`), it will be evaluated only once when `ctrl+enter` or `ctrl+shift+enter` is pressed. However, an arrow function is evaluated every frame; thus, it becomes responsive to the window size change. In the rest of the book, a square window is assumed for simplicity. Note that `width` and `height` global variables are set when the hydra canvas is initialized, and they will not change according to window resizing.
44 |
45 | `kaleid` with a small number creates a geometric shape (in the example, an oscillator is combined with `kaleid` and `thresh`).
46 |
47 | ```hydra
48 | osc(40,0).thresh().kaleid(3).out(o0)
49 | ```
50 |
51 | Noise
52 | --------
53 |
54 | `noise()` is another basic function as a source. A texture is generated based on a variant of Perlin Noise.
55 |
56 | ```hydra
57 | noise(10, 0).out(o0)
58 | ```
59 |
60 | We will look more into detail in the modulator and [arithmetic](arithmetic#normalization) sections.
61 |
62 | Voronoi
63 | --------
64 |
65 | `voronoi()` is a source to generate a Voronoi diagram.
66 |
67 | ```hydra
68 | voronoi(10, 0).out(o0)
69 | ```
70 |
71 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Chivo', sans-serif;
3 | font-weight: 400;
4 | }
5 |
6 | main {
7 | height: fit-content;
8 | }
9 |
10 | body, .sidebar {
11 | background-color: black;
12 | color: white;
13 | }
14 |
15 | .CodeMirror {
16 | position: relative;
17 | margin: 0;
18 | padding: 0;
19 | box-sizing: border-box;
20 | width: 100%;
21 | height: 100%;
22 | background-color: black;
23 | }
24 |
25 | .cm-s-paraiso-dark.CodeMirror {
26 | background-color: #333;
27 | }
28 |
29 | /* docsify */
30 |
31 | .external {
32 | width: 12pt;
33 | height: 12pt;
34 | margin-left: 4px;
35 | margin-right: 4px;
36 | }
37 |
38 | p > img {
39 | display: block;
40 | margin-left: auto;
41 | margin-right: auto;
42 | }
43 |
44 | .markdown-section em {
45 | color: #cccccc;
46 | }
47 |
48 | .markdown-section h1, .markdown-section h2, .markdown-section h3, .markdown-section h4, .markdown-section strong {
49 | color: white;
50 | font-weight: 700;
51 | }
52 |
53 | .markdown-section h1, .markdown-section h2, .markdown-section h3, .markdown-section h4 {
54 | text-transform: uppercase;
55 | }
56 |
57 | .sidebar ul li a {
58 | color: white;
59 | }
60 |
61 | .anchor span {
62 | color: white;
63 | }
64 |
65 | .markdown-section pre[data-lang=javascript],
66 | .markdown-section pre[data-lang=javascript] > * {
67 | background-color: #151515;
68 | }
69 |
70 | .markdown-section pre[data-lang=javascript].hydra-code {
71 | margin-top: 512px;
72 | }
73 |
74 | .markdown-section code, .markdown-section pre {
75 | background-color: black;
76 | font-family: monospace,courier;
77 | border: solid white 2px;
78 | color: #99cc99;
79 | }
80 |
81 | .markdown-section pre > code {
82 | color: #99cc99;
83 | background-color: black;
84 | border: none;
85 | }
86 |
87 | .token.function {
88 | color: #99cc99;
89 | }
90 | .token.number {
91 | color: #a16a94;
92 | }
93 | .token.punctuation {
94 | color: #cccccc;
95 | }
96 | .token.operator {
97 | color: #cccccc;
98 | }
99 | .token.parameter {
100 | color: #f99157;
101 | }
102 | .token.keyword {
103 | color: #f2777a;
104 | }
105 | .token.string {
106 | color: #ffcc66;
107 | }
108 |
109 |
110 |
111 | /* body {
112 | font-family: 'Chivo', sans-serif;
113 | font-weight: 300;
114 | }
115 |
116 | body, .sidebar {
117 | background-color: black;
118 | color: white;
119 | }
120 |
121 | .external {
122 | width: 12pt;
123 | height: 12pt;
124 | margin-left: 4px;
125 | margin-right: 4px;
126 | }
127 |
128 | .openin {
129 | text-align: right;
130 | width: 100%;
131 | display: block;
132 | }
133 |
134 | p > img {
135 | display: block;
136 | margin-left: auto;
137 | margin-right: auto;
138 | }
139 |
140 | .markdown-section em {
141 | color: #cccccc;
142 | }
143 |
144 | .markdown-section h1, .markdown-section h2, .markdown-section h3, .markdown-section h4, .markdown-section strong {
145 | color: white;
146 | font-weight: 700;
147 | }
148 |
149 | .markdown-section h1, .markdown-section h2, .markdown-section h3, .markdown-section h4 {
150 | text-transform: uppercase;
151 | }
152 |
153 | .sidebar ul li a {
154 | color: white;
155 | }
156 |
157 | .anchor span {
158 | color: white;
159 | }
160 |
161 | .markdown-section code, .markdown-section pre {
162 | background-color: black;
163 | font-family: monospace,courier;
164 | border: solid white 2px;
165 | color: #99cc99;
166 | }
167 |
168 | .markdown-section pre > code {
169 | color: #99cc99;
170 | background-color: black;
171 | border: none;
172 | }
173 |
174 | .token.function {
175 | color: #99cc99;
176 | }
177 | .token.number {
178 | color: #a16a94;
179 | }
180 | .token.punctuation {
181 | color: #cccccc;
182 | }
183 | .token.operator {
184 | color: #cccccc;
185 | }
186 | .token.parameter {
187 | color: #f99157;
188 | }
189 | .token.keyword {
190 | color: #f2777a;
191 | }
192 | .token.string {
193 | color: #ffcc66;
194 | }
195 |
196 | .hydracontainer {
197 | left: 50%;
198 | position: relative;
199 | transform: translate(-50%, 0%);
200 | } */
201 |
202 |
--------------------------------------------------------------------------------
/javascript.md:
--------------------------------------------------------------------------------
1 | JavaScript Tips
2 | ========
3 |
4 | While Hydra is made easy to start without deep understanding of JavaScript, knowledge of JavaScript definitely helps your creation. In this chapter, several JavaScript features useful for Hydra are described.
5 |
6 | Arrow Function
7 | --------
8 |
9 | ### Function Alias
10 |
11 | [Arrow function](https://developer.cdn.mozilla.net/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) `=>` is an expression to define a function. It is similar to the traditional `function`; however, there are differences explained in the MDN docs in the link above. Arrow functions are often preferred in live coding because they are shorter, but in the examples below, traditional `function` can be used as well. An arrow function can be used mainly in two ways in Hydra. The first example is to use it as an alias:
12 |
13 | ```hydra
14 | var cnoise = ()=>noise(3,0).mult(gradient().r())
15 | // same as
16 | // cnoise = ()=>{return noise(3,0).mult(gradient())}
17 | // or
18 | // cnoise = function () {return noise(3,0).mult(gradient())}
19 | cnoise().colorama(0.01).out(o0)
20 | ```
21 |
22 | Here, `cnoise` is treated as an alias of `noise(3,0).mult(gradient().r())`. As commented in the example, if the content is not wrapped by curly brackets, `return` can be omitted. Note that, after running the code, editing and evaluating only the `cnoise` line will **not** affect the output because `o0` buffer's shader code is already generated, and it has no connection to `cnoise` anymore. Therefore, you need to evaluate the last line to update the texture.
23 |
24 | You might wonder if there is a difference between the code above and the example below:
25 |
26 | ```hydra
27 | noise(3,0).mult(gradient().r()).out(o1)
28 | src(o1).colorama(0.01).out(o0)
29 | ```
30 |
31 | The answer is yes. An obvious difference is that the first example with `=>` only requires one buffer. Another difference is that, when a texture is drawn on a buffer, color values lower than 0 and higher than 1 are trimmed to 0 and 1, respectively; thus, the resulting textures differ.
32 |
33 | Therefore, the arrow function has an upside of using the full range of color. The downside is that, as mentioned previously, evaluating the code does not immediatly affect the rendering. Another minor downside is that the internal shader code can become longer. The example above runs without any issues, but if you chain arrow functions too many times, there may be a possibility that generating a shader code fails.
34 |
35 | ### Dynamic Parameter
36 |
37 | In a totally different context, an arrow function can be used to define a dynamically changing parameter. The following example seems plausible as `time` is the seconds since the page is loaded:
38 |
39 | ```hydra
40 | shape(3).color(Math.sin(time),Math.cos(time),0).out(o0)
41 | ```
42 |
43 | However, the texture will be static, and the color only changes only at the moment when the line is evaluated. This is because `time` variable is evaluated as a value in the shader code. Let's say, if `time = 10.2`, JavaScript cannot distinguish the difference between `osc(time)` and `osc(10.2)`.
44 |
45 | > For example, in p5.js, you will not encounter such a problem in the following code:
46 |
47 | > ```clike
48 | function draw() {
49 | let time = millis() * 0.001;
50 | background(sin(time) * 100 + 100);
51 | }
52 |
53 | > This is because that `draw` function is called in a loop. Hydra also has an animation loop, but the content of the `draw` is already "compiled". An analogy in p5.js is to assign `time` in `setup` function. This way, `time` holds the same value even though `draw` is run every animation frame.
54 |
55 | To make the variable dynamic, an arrow function can be used:
56 |
57 | ```hydra
58 | var v0 = ()=>Math.sin(time)
59 | var v1 = ()=>Math.cos(time)
60 | shape(3).color(v0,v1,0).out(o0)
61 |
62 | // or,
63 | // shape(3).color(()=>Math.sin(time),()=>Math.cos(time),0).out(o0)
64 | ```
65 |
66 | This way, Hydra understands the parameters of `color` as functions instead of static values, and it keeps the reference to the function. As a result, Hydra evaluates the function every animation frame, which returns a corresponding time value.
67 |
68 | Array
69 | --------
70 |
71 | Under the hood, Hydra overrides `Array.prototype` to add a few functions.
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Hydra Book
2 | ========
3 |
4 |
5 |
6 | ```hydra
7 | shape(100,0.35,0.25).out(o0)
8 | osc(40,0.1,1).hue(-0.1).modulate(noise(1,0),0.5).modulateRotate(osc(12,0).kaleid(100),4).out(o1)
9 | src(o2).modulateHue(o1,4).blend(o0,0.03).out(o2)
10 | src(o2).contrast(2).mult(src(o1)).out(o3)
11 | render()
12 | ```
13 |
14 | Important Notes
15 | --------
16 |
17 | June 24, 2025: Since glitch is retiring, I moved this online book to GitHub. The new URL is https://hydra-book.glitches.me/ (this website) and it is hosted by GitHub: https://github.com/micuat/hydra-book.
18 |
19 | You are redirected to [hydra-book.glitch.me](https://hydra-book.glitch.me/), which is the latest version of Hydra Book. While I'm sloppy about updating the Github repository, if you find any problems feel free to report on [Github issue page](https://github.com/micuat/hydra-book/issues).
20 |
21 | *NEW*: you can edit the code in the embedded editor and press `shift+enter`, `ctrl+enter` or `ctrl+shift+enter` to evaluate the code. Currently evaluate line/block is not supported.
22 |
23 | Preface
24 | --------
25 |
26 | Hydra is an analog-synth-like coding environment for real-time visuals. It is created by Olivia Jack and is [open-source](https://github.com/hydra-synth). You can simply open [Hydra editor](https://hydra.ojack.xyz) to start coding.
27 |
28 |
29 | ### For those who just started
30 |
31 | There are a few resources besides this book:
32 |
33 | * [The new official documentation](https://hydra.ojack.xyz/docs/#/) is a good resource to get started,
34 | * [Hydra Functions](https://hydra.ojack.xyz/functions/) is an interactive webpage to see functions and its usages,
35 | * [Hydra Garden](https://hydra.ojack.xyz/garden/) is a place for inspirations,
36 | * and [Hydra Patterns on Twitter](https://twitter.com/hydra_patterns) is a way to find sketches from other artists.
37 |
38 | Also Flor and Naoto gave an introductory workshop, which can be found here:
39 |
40 |
41 |
42 | And the workshop material can be found [here](https://ccfest-2021-glitchme.glitch.me/).
43 |
44 | ### How to Read
45 |
46 | This is a work-in-progress online book to collect Hydra snippets. Thanks to its live-coding nature, simply improvising by chaining different functions may lead to an unexpected pattern; nevertheless, studying Hydra in a structured, systematic way can reveal its potential. Thus, the goal of this book is not only to accumulate frequently-used techniques to make coding easier but also to research the theory of Hydra to discover new images.
47 |
48 | If you are new to Hydra, I recommend you to skim through the book and find patterns you like, and try the code by pressing "open in editor" link. On the editor, you can change some parameters and press `ctrl+shift+enter` to re-evaluate the sketch.
49 |
50 | If you are already familiar with Hydra, I hope reading this book gives you some insight not only about "how" to make a pattern but also "why" a pattern emerges.
51 |
52 |
53 | Table of Contents
54 | --------
55 |
56 | Understanding Hydra:
57 |
58 | * [Textures](textures)
59 | * [Geometry](geometry)
60 | * [Modulation](modulation)
61 | * [Colors](colors)
62 | * [Layers](layers)
63 | * [Arithmetic](arithmetic)
64 | * [Motions](motions)
65 | * [Feedback](feedback)
66 | * [Custom GLSL](glsl)
67 |
68 | JavaScript and applications:
69 |
70 | * [JavaScript Tips](javascript)
71 | * [Embed Hydra in Webpage](embed)
72 | * [Performing with Hydra](performing)
73 | * [100 Questions](100questions)
74 |
75 | Essays:
76 |
77 | * [Thoughts on modulation](thoughts-on-modulation)
78 | * [Code as state](code-as-state)
79 |
80 | License
81 | --------
82 |
83 | The code snippets are under public domain, meaning that all the code snippets can be used freely without any restrictions. Nevertheless, I appreciate it if you cite this book or simply let [me](https://naotohieda.com) know when you write about any ideas developed from this book! The explanations and essays are licensed under [CC Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0/).
84 |
--------------------------------------------------------------------------------
/arithmetic.md:
--------------------------------------------------------------------------------
1 | Arithmetic
2 | ========
3 |
4 | Arithmetic is not the most exciting topic; nevertheless, you might encounter undesired blending effects and wonder how to fix it. The output range of the sources are not all the same.
5 |
6 | Normalization
7 | --------
8 |
9 | In this example, `func`'s negative value is clipped by `luma` and overlaid on a red solid texture. If `func` is normalized from 0 to 1, the resulting texture is the same as `func` as it is not affected by `luma`. However, if `func` is normalized from -1 to 1, the negative values are clipped and the magenta texture appears. `osc`, `gradient` and `voronoi` are the former (0 to 1) and `noise` is the latter (-1 to 1) as seen in the image below.
10 |
11 | ```hydra
12 | var epsilon=0.001
13 | var func = () => noise(4,0.1)
14 | solid(1,0,1).layer(func().luma(-epsilon,0)).out(o0)
15 | ```
16 |
17 | `noise` can be normalized to 0-1 by the following method:
18 |
19 | ```hydra
20 | noise(4,0.1).add(solid(1,1,1),0.5).out(o0)
21 | ```
22 |
23 | Note that writing to a buffer clips negative values to 0. In fact, also values above 1 are clipped to 1. In this example, `o1` (bottom left) and `o2` (top right) show different results as `func` outputs values outside 0 to 1, but `src(o0)` only outputs values from 0 to 1; thus in the latter, the values only range from 0.5 to 1.
24 |
25 | ```hydra
26 | var epsilon=0.001
27 | var func = () => noise(4,0.1)
28 | func().out(o0)
29 | func().add(solid(1,1,1),0.5).out(o1)
30 | src(o0).add(solid(1,1,1),0.5).out(o2)
31 | render()
32 | ```
33 |
34 | Blending
35 | --------
36 |
37 | This example shows the difference between `add` and `diff`. `add(oX, -1)` might seem to be identical to `diff(oX)`. Although `add` simply adds the texture (the first argument) multiplied by a scalar (the second argument), `diff` first takes a difference of two textures and returns absolute values. Note that `diff` only takes one argument, and the resulting alpha value (transparency) is the maximum value between two values.
38 |
39 | ```clike
40 | vec4 diff(vec4 c0, vec4 c1){
41 | return vec4(abs(c0.rgb-c1.rgb), max(c0.a, c1.a));
42 | }
43 | ```
44 |
45 | In this example, a gray solid texture is subtracted by `osc` using two different functions. Notice the difference; `diff` (top) returns absolute values therefore continuous, and `add` (bottom) keeps negative values which appears black.
46 |
47 | ```hydra
48 | solid(0.5,0.5,0.5).diff(osc(40,0,1)).out(o0)
49 | solid(0.5,0.5,0.5).add(osc(40,0,1),-1).out(o1)
50 | render()
51 | ```
52 |
53 | Another confusing blending functions are `mult` and `mask`. On Hydra interface, the result might appear the same; however, they treat the alpha channel differently. First, `mult` simply multiplies the color values of two textures. Each channel, R, G, B and A are treated independently. Therefore, the alpha channel of the resulting image in the example below remains 1 (note that both `osc` and `shape` return opaque textures), and the texture underneath cannot be seen.
54 |
55 | ```hydra
56 | osc(10,0,1).hue(0.5).layer(osc(10,0,1).mult(shape(4,0.5,0.001))).out()
57 | ```
58 |
59 | Contrarily, `mask` only uses the luminance of the mask texture. The returned texture is not only the multiplication of the masked texture and the luminance of mask, the alpha channel is overwritten by the luminance of mask. Therefore, the returned texture can be overlaid on another texture by `layer`.
60 |
61 | ```hydra
62 | osc(10,0,1).hue(0.5).layer(osc(10,0,1).mask(shape(4,0.5,0.001))).out()
63 | ```
64 |
65 | With `mult`, a similar effect can be obtained by using `luma` to modify the alpha channel. In this example, the resulting image is the same; however, with a grayscale texture, the result depends on the arguments of `luma`.
66 |
67 | ```hydra
68 | osc(10,0,1).hue(0.5).layer(osc(10,0,1).mult(shape(4,0.5,0.001).luma(0.5,0.001))).out()
69 | ```
70 |
71 |
--------------------------------------------------------------------------------
/code-as-state.md:
--------------------------------------------------------------------------------
1 | The syntax of Hydra is an analogy to modular synthesizers, specifically referring to video synthesizers by [Dan Sandin](https://en.wikipedia.org/wiki/Daniel_J._Sandin).
2 |
3 | Like modular synthesizers whose output signal is connected to an input of another module by physically wiring them (cover photo [^1]), Hydra uses a chain of functions. For example, the code below uses an oscillator (`osc()`) as a source, changes the color and outputs to the screen.
4 |
5 | ```javascript
6 | osc().color(1,0,0.5).out()
7 | ```
8 |
9 | You can blend (or mix) several sources, too.
10 |
11 | ```javascript
12 | osc().blend(noise()).out()
13 | ```
14 |
15 | While the other chapters are about technical explanations of Hydra, here I want to discuss how this design contributes to live-coding practices. Let's say, there is an imaginary coding environment that achieves the same functions as Hydra but has a "classical" design, for example, node based similar to Web Audio API. The first Hydra code above can be written as:
16 |
17 | ```
18 | var source0 = osc()
19 | var color0 = color(1, 0, 0.5)
20 | source0.connect(color0)
21 | color0.connect(out)
22 | ```
23 |
24 | While it is wordy, this notation makes the structure more explicit. It is optimized for a development, when you edit and execute the whole code at once because the execution order is defined. First, the oscillator and color operator are generated and they are connected to respective outputs. You will not expect that the line 3 is evaluated first and the code aborts since `source0` is undefined.
25 |
26 | Nevertheless, in live coding when you can edit the code and evaluate line by line, this type of error occurs and thus using variables can add confusion. In fact you can use variables in Hydra and this is a valid code:
27 |
28 | ```javascript
29 | var source0 = osc()
30 | var color0 = source0.color(1, 0, 0.5)
31 | color0.out()
32 | ```
33 |
34 | Even though it is valid, use of variables can become problematic when, for example, you want to change `osc()` to `noise()`. First, you change the line 1 to `noise()` and evaluate it, but nothing will change. Actually, the rest of the code has to be evaluated too because assigning another value to the variable `source0` will not affect the state of `color0` [^2]. As a high level explanation, when variables are used in a live-coding setup in which the execution order is not defined, the state of the code cannot be determined from the code itself. **The design of Hydra, being free of variables, lets the code be the *state* and vice versa** [^3].
35 |
36 | As the code represents its state, there is no blackbox when coding with Hydra. Everything is visible and editable on the editor [^4]. While this is due to the fact that Hydra is inspired by modular synthesis, this design makes Hydra unique and helps the coders relate code to their body: the embodiment of code.
37 |
38 | This design can be abused to create an ambiguous state:
39 |
40 | ```javascript
41 | osc().color(1,0,0.5).out()
42 | solid(1,0.2,0.2).out()
43 | ```
44 |
45 | If the entire code is evaluated, the last line (`solid()`) will take over and the output will be covered by salmon color. But when the cursor is focused on the first line and only the line is evaluated, the output becomes the oscillator with magenta color. The mouse and keyboard can toggle the effect, which can be explained as the modular synth analogy of plugging and unplugging cables, but I argue that this case is different because both lines have "potential" to be connected to `out()`. What determines the output is the cursor; this is when the cursor becomes an extension of the body to be digitized, or in other words, the cursor becomes an extension of the code to be materialized.
46 |
47 | ---
48 |
49 | Footnotes:
50 |
51 | [^1]: https://naotohieda.com/blog/code-as-state-en/ The photo is taken at a show at Gibney Dance in 2018 with a permission of the artist, but I forgot the name of the artist and the show.
52 |
53 | [^2]: Of course, you can imagine an environment where you can change the properties through `source0`'s member variables (e.g., `source0.type = "noise"`), but in any case this design is wordy and not suitable for live coding.
54 |
55 | [^3]: In fact, the most important data of Hydra is stored in variables: output buffers. To be more precise, the code represents the state as long as the code is not deleted.
56 |
57 | [^4]: What about [TidalCycles](https://tidalcycles.org/Welcome)? Besides the recently introduced counters, the same statement can be made for TidalCycles that the code is interchangable with the state. However, it depends on the definition of *state*; as everything relies on time in TidalCycles, strictly speaking, you cannot determine the state without the clock of the interpreter, for example `every 3 (slow 2) $ s "bd*4"`. Hydra also uses `time` for animations, but unlike TidalCycles, in which the sense of time is discrete and dictates the state, the design principle of Hydra still stands without the sense of time.
--------------------------------------------------------------------------------
/geometry.md:
--------------------------------------------------------------------------------
1 | Geometry
2 | ========
3 |
4 | Shapes
5 | --------
6 |
7 | `shape(sides,radius,smoothing)` generates a polygon with a number of sides set by `sides`. Nevertheless, it is more than just a polygon - `radius` changes the size of the shape, and most importantly, `smoothing` sets gradient of the shape; 1 for fuzzy borders and close to 0 for sharp edges (however, setting to 0 does not work in recent versions). For example, `shape(2)` is a thick line, which can be scaled to make a thin line.
8 |
9 | ```javascript
10 | shape(2).scale(0.01).out(o0)
11 | ```
12 |
13 | or simply,
14 |
15 | ```hydra
16 | shape(2,0.01,0).out(o0)
17 | ```
18 |
19 | `shape(4)` is a square:
20 |
21 | ```hydra
22 | shape(4).out(o0)
23 | ```
24 |
25 | and large number creates a (almost) circle:
26 |
27 | ```hydra
28 | shape(999).out(o0)
29 | ```
30 |
31 | By repeating `shape(4)` and overlapping them, it gives a grid-like pattern. Basically, first `a()` makes a sparse pattern, and `a().scroll(0.5/n,0.5/n)` is the same pattern but shifted. For convenience, a parameter and a function are stored in JavaScript variables.
32 |
33 | ```hydra
34 | var n = 4
35 | var a = () => shape(4,0.4).repeat(n,n)
36 | a().add(a().scroll(0.5/n,0.5/n)).out()
37 | ```
38 |
39 | Here is an elegant (and tricky) way: `shape` is first scaled to stretch in Y direction, and repeated in a strange way. This works because the 3rd parameter is to scroll every even-th pattern in the X direction (and the 4th parameter for Y direction).
40 |
41 | ```hydra
42 | shape(4,0.4).scale(1,1,2).repeat(4,8,.5).out()
43 | ```
44 |
45 | By tweaking the example above, it generates a Polka dot pattern.
46 |
47 | ```hydra
48 | shape(999,0.4).scale(1,1,2).repeat(4,8,.5).out()
49 | ```
50 |
51 | and here is another approach for Polka dot:
52 |
53 | ```hydra
54 | shape(999,0.4).repeat(8,8).rotate(Math.PI/4).out()
55 | ```
56 |
57 | This tiling technique can be used to create a RGB pixel filter. In this example, `func` is decomposed into R, G, and B channels and overlaid on top of each other.
58 |
59 | ```hydra
60 | var n = 50;
61 | var func = () => osc(30,0.1,1).modulate(noise(4,0.1))
62 | var pix = () => shape(4,0.3).scale(1,1,3).repeat(n,n)
63 | pix().mult(func().color(1,0,0).pixelate(n,n)).out(o1)
64 | pix().mult(func().color(0,1,0).pixelate(n,n)).scrollX(1/n/3).out(o2)
65 | pix().mult(func().color(0,0,1).pixelate(n,n)).scrollX(2/n/3).out(o3)
66 |
67 | solid().add(src(o1),1).add(src(o2),1).add(src(o3),1).out(o0)
68 | ```
69 |
70 | Kaleid
71 | --------
72 |
73 | We already saw [`kaleid`](textures?id=oscillator) ealier, but let's try to understand it further. Take a look at this example:
74 |
75 | ```hydra
76 | gradient().pixelate(8,8).kaleid(4).out()
77 | //gradient().out()
78 | ```
79 |
80 | You can see that the red color (and black color) is dominant, which is the color appears mostly at the top of `gradient()`. We can say that the way how `kaleid` works is that it chops off the top part of the image, duplicates it and aligns them like a triangle fan.
81 |
82 | ```hydra
83 | var k = 8
84 | var d = Math.PI/k
85 | shape(2,d,0).scrollY(d/2).rotate(Math.atan2(d,1))
86 | .scrollY(-d/2-.5)
87 | .mask(shape(1,0,0).invert().rotate(Math.PI/4)) // for demo purpose... not compatible with k<4
88 | .out()
89 | ```
90 |
91 | Indeed, the thin top part indicated in white is the only part that appears after `kaleid` (in this case with argument k = 16. Try with different k values!).
92 |
93 | Here's an approach to make rays from the center:
94 |
95 |
96 | ```hydra
97 | var k = 16
98 | var d = Math.PI/2/k
99 | shape(2,d,0).scrollY(d/2).rotate(Math.atan2(d,1))
100 | .scrollY(-d/2-.5)
101 | .kaleid(k).out()
102 | ```
103 |
104 | Scaling and Feedback
105 | --------
106 |
107 | Scaling and difference can also create a periodic texture.
108 |
109 | ```hydra
110 | shape(4,0.8).diff(src(o0).scale(0.9)).out(o0)
111 | ```
112 |
113 | This technique can also be applied to a complex texture.
114 |
115 | ```hydra
116 | voronoi(10,0).diff(src(o0).scale(0.9)).out(o0)
117 | ```
118 |
119 | The effect can be enhanced by `thresh` and setting the third argument of `voronoi` to 0, to have sharp edges. However, a naive implementation will end up in a complete noise.
120 |
121 | ```hydra
122 | voronoi(10,0,0).thresh(0.5,0).diff(src(o0).scale(0.9)).out(o0)
123 | ```
124 |
125 | To have a desired effect, apply a square mask (before trying the next example, apply `solid().out(o0)` to clear the buffer).
126 |
127 | ```hydra
128 | voronoi(10,0,0).thresh(0.5,0).mask(shape(4,0.8,0.0)).diff(src(o0).scale(0.9)).out(o0)
129 | ```
130 |
131 | This example can be used together with rotation.
132 |
133 | ```hydra
134 | shape(4,0.9,0).diff(src(o0).scale(0.9).mask(shape(4,0.9,0.0)).rotate(0.1)).out(o0)
135 | ```
136 |
137 | Or, instead of `scale`, scrolling functions (`scrollX` and `scrollY`) can be used with a feedback loop.
138 |
139 | ```hydra
140 | shape(4,0.7,0).diff(src(o0).scrollX(0.01).mask(shape(4,0.7,0))).out(o0)
141 | ```
142 |
--------------------------------------------------------------------------------
/motions.md:
--------------------------------------------------------------------------------
1 | Motions
2 | ========
3 |
4 | Low Frequency Oscillator
5 | --------
6 |
7 | In audiovisual synthesis, a term low frequency oscillator (LFO) is often used. According to [Wikipedia](https://en.wikipedia.org/wiki/Low-frequency_oscillation), an oscillator with a frequency below 20 Hz is usually considered as an LFO; nevertheless, the definition depends on the application, and here, I would not define the frequency (in fact, most LCDs support up to 60 Hz, and effectively, what can be displayed on an LCD is LFO). The important point is that, in this section, we strictly look at oscillators in the time domain. In the previous chapters, oscillators are explained in the spatial domain, i.e., the pixel space. If the second argument of `osc` is set to a non-zero value, the pattern starts to "move."
8 |
9 | ```hydra
10 | osc(60,0.1,1).out(o0)
11 | ```
12 |
13 | The result seems to be scrolling stripes due to the human perception. If we look at an oscillator with a smaller spatial frequency (i.e., to set the first argument small), and take an average of the whole pixels by `pixelate(1,1)`, the color change in time becomes recognizable. This example flickers at 1 second interval:
14 |
15 | ```hydra
16 | osc(Math.PI*2,1,0).pixelate(1,1).out(o0)
17 | ```
18 |
19 | and it can be colorized:
20 |
21 | ```hydra
22 | osc(Math.PI*2,1,Math.PI/2).pixelate(1,1).out(o0)
23 | ```
24 |
25 | These are not particularly interesting examples. Yet, it is important to separate the characteristics in the time and spatial domains. For instance, the sine wave oscillator in the example above can be used as a fader to mix two images:
26 |
27 | ```hydra
28 | var lfo = () => osc(Math.PI*1,1,0).pixelate(1,1)
29 | shape(3).color(1,0,0).mult(lfo())
30 | .add(shape(4).color(0,0,1).mult(lfo().invert()),1)
31 | .out(o0)
32 | ```
33 |
34 | The same effect can be achieved by an arrow function:
35 |
36 | ```hydra
37 | shape(3).color(1,0,0)
38 | .blend(shape(4).color(0,0,1), () => Math.sin(time*Math.PI) * 0.5 + 0.5)
39 | .out(o0)
40 | ```
41 |
42 | Visually, both examples crossfade the two shapes: a red triangle and a blue square. The key is to understand the difference between these two examples. In the first code, the two shapes are multiplied by `lfo` and `lfoInvert`, which is the inverted texture of `lfo`. This can be thought as an analogy of a layer mask with a uniform transparency in Photoshop. In the second code, an arrow function with `Math.sin` is attached to the second argument of `blend`. This is similar to setting a global opacity of the layer in Photoshop. The latter is more concise and easier to understand. However, it is spatially less flexible because the single transparency is applied to the blending operation of all the pixels. The former can be modified to add spatial oscillation, i.e., a layer mask.
43 |
44 | ```hydra
45 | var lfo = () => osc(Math.PI*1,1,0).pixelate(10,1)
46 | shape(3).color(1,0,0).mult(lfo())
47 | .add(shape(4).color(0,0,1).mult(lfo().invert()),1)
48 | .out(o0)
49 | ```
50 |
51 | Beyond image blending, LFOs can be used for other several operations. An example is `pixelate`. To change the argument of `pixelate` in time, one might use an arrow function:
52 |
53 | ```hydra
54 | var lfo = () => (Math.sin(time*Math.PI) * 0.5 + 0.5) * 4 + 4
55 | osc(10,0,1).pixelate(lfo,lfo).out(o0)
56 | ```
57 |
58 | Note that `lfo` function itself is passed to `pixelate`, not `lfo()` function call. When `lfo()` is passed, it is only evaluated once and you will not see any change in the image. A similar texture can be generated using `modulatePixelate`:
59 |
60 | ```hydra
61 | osc(10,0,1)
62 | .modulatePixelate(osc(Math.PI*1,1,0).pixelate(1,1).color(4,4),1)
63 | .out(o0)
64 | ```
65 |
66 | Again, the difference of the two example is the flexibility in the spatial domain. For example, by multiplying a `shape`, you can apply `modulatePixelate` partially in the texture.
67 |
68 | ```hydra
69 | osc(10,0,1)
70 | .modulatePixelate(osc(Math.PI*1,1,0).pixelate(1,1).color(4,4).mult(shape(4,0.5,0.001)),1)
71 | .out(o0)
72 | ```
73 |
74 | The downside of the `osc.pixelate` LFO compared to an arrow-function LFO is that arithmetic operations are cumbersome and less readable. To add a value X, one needs to write
75 |
76 | ```javascript
77 | ...brightness(X)
78 | ```
79 |
80 | or
81 |
82 | ```javascript
83 | ...add(solid(1,1,1),X)
84 | ```
85 |
86 | Note that default multiplier of `add()` is 1. And to multiply by Y,
87 |
88 | ```javascript
89 | ...color(Y,Y,Y)
90 | ```
91 |
92 | Also, discretization can be achieved by `thresh()` for binary and `posterize()` for histogram (whose second argument sets gamma, so set it to `1` to achieve binning in a linear space) :
93 |
94 | ```hydra
95 | osc(10,0,1).modulatePixelate(osc(1,1,0).pixelate(1,1).posterize(16,1).color(4,4),1).out(o0)
96 | ```
97 |
98 | An analogy in math functions is `Math.floor`:
99 |
100 | ```hydra
101 | var lfo = () => Math.floor((Math.sin(time) * 0.5 + 0.5) * 8) + 1
102 | osc(10,0,1).pixelate(lfo,lfo).out(o0)
103 | ```
104 |
105 | and similarly achieved by the JavaScript array extension in Hydra:
106 |
107 | ```hydra
108 | var lfo = [1,2,3,4].fast(2)
109 | osc(10,0,1).pixelate(lfo,lfo).out(o0)
110 | ```
111 |
--------------------------------------------------------------------------------
/glsl.md:
--------------------------------------------------------------------------------
1 | Custom GLSL
2 | ========
3 |
4 | setFunction
5 | --------
6 |
7 | A custom GLSL function can be defined using `setFunction()`. The structure follows the format of [builtin functions](https://github.com/ojack/hydra-synth/blob/master/src/glsl/glsl-functions.js). Defining a custom GLSL function does not mean that you are allowed to design an arbitrary function; the function has to have specific inputs and an output, based on its type. The types are `src` `color` `combine` `combineCoords`.
8 |
9 |
10 | Scale and Rotate
11 | --------
12 |
13 | Let's take a look at this example, trying to scale a pattern and then rotate using `modulateScale` and `modulateRotate`.
14 |
15 |
16 | ```hydra
17 | osc(60,.01)
18 | .modulateScale(noise(3).thresh(0,0), 1,1)
19 | .modulateRotate(noise(3).thresh(0,0), Math.PI/2)
20 | .out(o0)
21 | ```
22 |
23 | While both modulateXX use `noise` as a modulator, you can find that the result has weird boundaries. To apply scaling and rotation altogether, you need a custom function:
24 |
25 | ```hydra
26 | setFunction({
27 | name: 'modulateSR',type: 'combineCoord',
28 | inputs: [
29 | {
30 | type: 'float',name: 'multiple',default: 1,
31 | },
32 | {
33 | type: 'float',name: 'offset',default: 1,
34 | },
35 | {
36 | type: 'float',name: 'rotateMultiple',default: 1,
37 | },
38 | {
39 | type: 'float',name: 'rotateOffset',default: 1,
40 | }
41 | ],
42 | glsl:
43 | ` vec2 xy = _st - vec2(0.5);
44 | float angle = rotateOffset + _c0.z * rotateMultiple;
45 | xy = mat2(cos(angle),-sin(angle), sin(angle),cos(angle))*xy;
46 | xy*=(1.0/vec2(offset + multiple*_c0.r, offset + multiple*_c0.g));
47 | xy+=vec2(0.5);
48 | return xy;`
49 | })
50 | osc(60,.01)
51 | .modulateSR(noise(3).thresh(0,0), 1,1,Math.PI/2)
52 | .out(o0)
53 | ```
54 |
55 | Chroma Key
56 | --------
57 |
58 | This example modifies `color` to replace green background with transparency (i.e., chroma keying). The GLSL code is ported from [Inigo Quilez's example](https://www.shadertoy.com/view/XsfGzn).
59 |
60 | ```hydra
61 | setFunction({
62 | name: 'chroma',
63 | type: 'color',
64 | inputs: [
65 | ],
66 | glsl: `
67 | float maxrb = max( _c0.r, _c0.b );
68 | float k = clamp( (_c0.g-maxrb)*5.0, 0.0, 1.0 );
69 | float dg = _c0.g;
70 | _c0.g = min( _c0.g, maxrb*0.8 );
71 | _c0 += vec4(dg - _c0.g);
72 | return vec4(_c0.rgb, 1.0 - k);
73 | `})
74 |
75 | // s0.initCam()
76 | // src(s0).out(o0)
77 | solid(0,1,0).layer(shape(5,0.3,0.3).luma()).out(o0)
78 | osc(30, 0, 1).layer(src(o0).chroma()).out(o1)
79 | render()
80 | ```
81 |
82 | Here's a "minified" version:
83 |
84 | ```hydra
85 | setFunction({name:'chroma',type:'color',inputs:[],glsl:`float m=max(_c0.r,_c0.b);float d=_c0.g;_c0.g=min(_c0.g,m*.8);_c0+=vec4(d-_c0.g);return vec4(_c0.rgb, 1.-clamp((_c0.g-m)*5.,.0,1.));`})
86 |
87 | // s0.initCam()
88 | // src(s0).out(o0)
89 | solid(0,1,0).layer(shape(5,0.3,0.3).luma()).out(o0)
90 | osc(30, 0, 1).layer(src(o0).chroma()).out(o1)
91 | render()
92 | ```
93 |
94 | And another version that returns a mask:
95 |
96 | ```hydra
97 | setFunction({name:'chr',type:'color',inputs:[],glsl:`float m=max(_c0.r,_c0.b);float d=_c0.g;_c0.g=min(_c0.g,m*.8);_c0+=vec4(d-_c0.g);return vec4(1.-clamp((_c0.g-m)*5.,.0,1.));`})
98 | // s0.initCam()
99 | // src(s0).out(o0)
100 | solid(0,1,0).layer(shape(5,0.3,0.3).luma()).out(o0)
101 | osc(30, 0, 1).layer(src(o0).mask(src(o0).chr().thresh(.5,0))).out(o1)
102 | render()
103 | ```
104 |
105 | Ray Marching
106 | --------
107 |
108 | In this example, a simple ray marching is implemented as a `coord` function, which literally modifies the coordinates, to map incoming texture to a sphere.
109 |
110 | ```hydra
111 | setFunction({
112 | name: 'sphere',type: 'coord',
113 | inputs: [
114 | {name: 'radius', type: 'float', default: 4.0},
115 | {name: 'rot', type: 'float', default: 0.0}
116 | ],
117 | glsl: `
118 | vec2 pos = _st-0.5;
119 | vec3 rpos = vec3(0.0, 0.0, -10.0);
120 | vec3 rdir = normalize(vec3(pos * 3.0, 1.0));
121 | float d = 0.0;
122 | for(int i = 0; i < 16; ++i){
123 | d = length(rpos) - radius;
124 | rpos += d * rdir;
125 | if (abs(d) < 0.001)break;
126 | }
127 | return vec2(atan(rpos.z, rpos.x)+rot, atan(length(rpos.xz), rpos.y));
128 | `})
129 |
130 | osc(10,0,2).sphere(6.0).out()
131 | ```
132 |
133 | A noise input can be added as an argument to displace the sphere surface. This can be done with `combineCoord` type, which is used by `modulate*` family.
134 |
135 | ```hydra
136 | setFunction({
137 | name: 'sphereDisplacement',type: 'combineCoord',
138 | inputs: [
139 | {name: 'radius', type: 'float', default: 4.0},
140 | {name: 'rot', type: 'float', default: 0.0}
141 | ],
142 | glsl: `
143 | vec2 pos = _st-0.5;
144 | vec3 rpos = vec3(0.0, 0.0, -10.0);
145 | vec3 rdir = normalize(vec3(pos * 3.0, 1.0));
146 | float d = 0.0;
147 | for(int i = 0; i < 16; ++i){
148 | float height = length(_c0);
149 | d = length(rpos) - (radius+height);
150 | rpos += d * rdir;
151 | if (abs(d) < 0.001)break;
152 | }
153 | return vec2(atan(rpos.z, rpos.x)+rot, atan(length(rpos.xz), rpos.y));
154 | `})
155 |
156 | osc(10,0,2).sphereDisplacement(noise(2,0,3).color(0.5,0.5,0.5), 6.0).out()
157 | ```
158 |
159 | It can be hacked to set the background image.
160 |
161 | ```hydra
162 | setFunction({
163 | name: 'sphereDisplacement2',
164 | type: 'combineCoord',
165 | inputs: [
166 | {name: 'radius', type: 'float', default: 4.0},
167 | {name: 'rot', type: 'float', default: 0.0}
168 | ],
169 | glsl: `
170 | vec2 pos = _st-0.5;
171 | vec3 rpos = vec3(0.0, 0.0, -10.0);
172 | vec3 rdir = normalize(vec3(pos * 3.0, 1.0));
173 | float d = 0.0;
174 | for(int i = 0; i < 16; ++i){
175 | float height = length(_c0);
176 | d = length(rpos) - (radius+height);
177 | rpos += d * rdir;
178 | if (abs(d) < 0.001)break;
179 | }
180 | if(d > 0.5) return vec2(0.5,0.5);
181 | else return vec2(atan(rpos.z, rpos.x)+rot, atan(length(rpos.xz), rpos.y));
182 | `})
183 |
184 | osc(10,0,2).layer(shape().luma().color(0,0,0)).sphereDisplacement2(noise(2,0,3).color(0.5,0.5,0.5), 6.0).out()
185 | ```
186 |
--------------------------------------------------------------------------------
/thoughts-on-modulation.md:
--------------------------------------------------------------------------------
1 | Thoughts on modulation
2 | ========
3 |
4 | One of the key function of Hydra is `modulate`. As referenced in [Hydra README](https://github.com/ojack/hydra), [Lumen](https://lumen-app.com/) is a commercial software that also uses modulation, and it has a [documentation](https://lumen-app.com/guide/modulation/) about modulation.
5 |
6 | ```javascript
7 | osc(20,0.1,2)
8 | .modulate(noise(), () => time%1*0.1, 0.1)
9 | .out()
10 | ```
11 |
12 | I usually think modulation as a way to *push* pixels in x and y directions based on the red and green channels of the input texture. In the example above, the original texture (rainbow oscillator) is pushed by an input texture (noise). Let's take a look another example.
13 |
14 | ```javascript
15 | shape(4).modulate(noise(), () => time%1*0.1, 0.1).out()
16 | ```
17 |
18 | Here, the square shape is modulated in both x, y directions; in fact, `noise` returns grayscale values, so pixels are pushed equally in x and y directions. Effectively, pixels are pushed in a diagonal direction. To modulate only in x direction, green channel should be discarded (there is also `modulateScrollX` which specifically modulates in x direction, but currently there is a bug that the function is currently broken):
19 |
20 | ```javascript
21 | shape(4)
22 | .modulate(noise().color(1,0),
23 | () => time%1*0.1, 0.1)
24 | .out()
25 | ```
26 |
27 | Notice that while the effect is similar, the top and bottom edges of the square are preserved because modulation only applies in the x (horizontal) direction. You might wonder why I used `color(1,0)` instead of `color(1,0,0)`. The reason is that modulate only looks at the red and green channels, so the value in the blue channel does not matter. In `modulate` family, the following functions use only 1 channel (red):
28 |
29 | * `modulateRepeatX`
30 | * `modulateRepeatY`
31 | * `modulateScrollX`
32 | * `modulateScrollY`
33 | * `modulateKaleid`
34 | * `modulateRotate`
35 |
36 | note that all the functions samples red channel; for example, `modulateScrollY` uses red channel to shift pixels in y direction while `modulate` uses red for x and green channel for y direction.
37 |
38 | The following functions use 2 channels (red and green):
39 |
40 | * `modulate`
41 | * `modulateRepeat`
42 | * `modulateScale`
43 | * `modulatePixelate`
44 |
45 | At last, `modulateHue` uses 3 channels (red, green and blue).
46 |
47 | Therefore, blue channel will not affect the results of `modulate`. For example, `gradient` takes an argument to "animate" the texture.
48 |
49 | ```javascript
50 | gradient(2).out()
51 | ```
52 |
53 | Using `gradient` animation as an input does not animate the texture because the animation only happens in the blue channel:
54 |
55 | ```javascript
56 | shape(4)
57 | .modulate(gradient(2), 0.1)
58 | .out()
59 | ```
60 |
61 | How can we use the animation of `gradient`? Although swapping color channels can be confusing in Hydra, you can simply use `hue` to effectively shift values between channels:
62 |
63 | ```javascript
64 | shape(4)
65 | .modulate(gradient(2).hue(), 0.1)
66 | .out()
67 | ```
68 |
69 | This is where things get confusing. `hue` is supposed to change *color* but in the code above, what is changed is the direction of pixels to push. Let's look at an even more confusing example:
70 |
71 | ```javascript
72 | render()
73 |
74 | osc(30,0,2)
75 | .modulateScale(shape(99,0.5),0.5,1)
76 | .out(o0)
77 | osc(30,0,2)
78 | .modulateScale(shape(99,0.5).color(0.5,0.5),1,1)
79 | .out(o1)
80 | osc(30,0,2)
81 | .modulateScale(shape(99,0.5).brightness(2),0.5,0)
82 | .out(o2)
83 | osc(30,0,2)
84 | .modulateScale(shape(99,0.5).invert(),-0.5,1.5)
85 | .out(o3)
86 | ```
87 |
88 | While they all have different operations using `color`, `brightness` and `invert`, with an appropriate arguments of `modulateScale`, they all yield the same texture. This is because that the color operations are simply arithmetic operations (`color` for `*`, `brightness` for `+` and `invert` for `-`).
89 |
90 | By understanding such operations, animations can be coded without array objects or arrow functions:
91 |
92 | ```javascript
93 | shape(4)
94 | .modulateScale(gradient().r()
95 | .scrollX(0,1).pixelate(1,1),1,1)
96 | .out()
97 | ```
98 |
99 | which is identical to
100 |
101 | ```
102 | shape(4).scale(()=>time%1+1).out()
103 | ```
104 |
105 | (`r()` copies the red channel to green and blue channels; thus scaling is applied equally to x and y directions). I prefer the *color* operations inside modulation because they give more freedom to work in the spatial domain:
106 |
107 | ```javascript
108 | shape(4)
109 | .modulateScale(gradient().r()
110 | .scrollX(0,1).pixelate(8,1),1,1)
111 | .out()
112 | ```
113 |
114 | Nevertheless, I am hesitant to tell people about these tricks because simply they are not intuitive. As a design question, I would love to ask people what would be an alias of `color` inside modulation to actually multiply the values for modifying the texture coordinates? `scale` may sound like a good idea, but unfortunately it is already taken by *real* scaling function that stretches the texture in the spatial domain.
115 |
116 | This is not totally a hydra *geek* talk. In the following example of naive uses of modulation family functions, the pairs look similar to each other:
117 |
118 | ```javascript
119 | render()
120 |
121 | osc(30,0,0)
122 | .modulate(noise(3))
123 | .out(o0)
124 | osc(30,0,0)
125 | .modulateRotate(noise(3))
126 | .out(o1)
127 | osc(30,0,0)
128 | .modulatePixelate(noise(3))
129 | .out(o2)
130 | osc(30,0,0)
131 | .modulateKaleid(noise(3))
132 | .out(o3)
133 | ```
134 |
135 | Based on the observations above, these modulators become controllable by adding a few color operations and by picking the right parameters:
136 |
137 | ```javascript
138 | render()
139 |
140 | osc(30,0,0)
141 | .modulate(noise(3))
142 | .out(o0)
143 | osc(30,0,0)
144 | .modulateRotate(noise(3).mult(shape(99,0.0,0.7)),Math.PI*2,0)
145 | .out(o1)
146 | osc(30,0,0)
147 | .modulatePixelate(noise(3).pixelate(8,8),1024,8)
148 | .out(o2)
149 | osc(30,0,0)
150 | .modulateKaleid(noise(3).color(0.1),4)
151 | .out(o3)
152 | ```
153 |
154 | I would love to contribute to Hydra (or specifically hydra-synth) to make modulators more accessible. However, as written above, it is not a simple engineering problem but it needs to be closely discussed by community members to make it accessible.
155 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
70 |
71 |
72 |
73 |
74 |
181 |
182 |
--------------------------------------------------------------------------------
/modulation.md:
--------------------------------------------------------------------------------
1 | Modulation
2 | ========
3 |
4 | Video Tutorial
5 | --------
6 |
7 |
8 |
9 | Modulators are the key component in Hydra. Let's look at this example; the modulated function `osc()` is on top left, the modulating function `noise()` is on bottom left, and the result is on top right.
10 |
11 | ```hydra
12 | osc(40,0,1).out(o0)
13 | noise(3,0).out(o1)
14 | osc(40,0,1).modulate(noise(3,0)).out(o2)
15 | render()
16 | ```
17 |
18 | We can see this from two perspectives. First, the color of the original image (or modulated image, `osc(40,0,1)`) is preserved. Second, the oscillator is distorted to resemble the pattern of the modulating texture, `noise(3,0)`. Modulators can be seen from two different perspectives. On the one hand, a modulator literally modulates (or distorts) the chained function (`osc` in this example). In this section, we cover this aspect to explore the distortion. On the other hand, it can be seen as a way to paint the modulator function (`noise` in this example). For example, `noise` itself is grayscale, but using it as an argument of a modulator, the noise pattern is painted with, for example, an oscillator or a gradient.
19 |
20 | modulate
21 | --------
22 |
23 | Here is a pseudocode of `A.modulate(B, amount)` producing `ANew`. This might be helpful if you are already familiar with coding environments such as Processing and openFrameworks.
24 |
25 | ```clike
26 | Pixel[][] A;
27 | Pixel[][] B;
28 | Pixel[][] ANew;
29 | for(int y = 0; y < height; y++) {
30 | for(int x = 0; x < width; x++) {
31 | Pixel b = B[y][x];
32 | ANew[y][x] = A[y + b.green * amount][x + b.red * amount];
33 | }
34 | }
35 | ```
36 |
37 | ### luma
38 |
39 | `luma(threshold,tolerance)` cuts off pixels with intensity less than `threshold` (explained in [color](colors#luma)). This is useful to make a conditional modulation; let's take a look at the example below:
40 |
41 | ```hydra
42 | osc(40,0,1).modulate(noise(3,0).luma(0.5,0.5)).out()
43 | ```
44 |
45 | In this example, `luma` is applied to the modulator; thus, only certain area (with intensity more than 0.5) have `modulate` applied. This is particularly useful to emphasize the effect of modulation.
46 |
47 |
48 | ### Feedback
49 |
50 | A modulator with a feedback loop keeps *pushing* pixels based on its brightness. In this example, a noise is modulated by itself in a feedback loop. As a result, bright pixels are pushed further and further, creating a smooth, 3D-like effect.
51 |
52 | ```hydra
53 | noise(10, 0).modulate(o0).blend(o0,0.9).out(o0)
54 | ```
55 |
56 | This example uses the same technique on a Voronoi diagram. Similar to above, the resulting image has a fake 3D look.
57 |
58 | ```hydra
59 | voronoi(10, 0).modulate(o0).blend(o0,0.9).out(o0)
60 | ```
61 |
62 | ### x and y
63 |
64 | In the examples above, modulating functions are grayscale, so pixels are always pushed to left-up direction with varying amplitude (positive direction is right and down for x and y, respectively, but *pushing* happens to the other direction as modulation is look-up). By adding color channels, and by remapping color values from `[0, 1]` to, for example, `[-1, 1]`, pixels are pushed to all directions. This can be achieved by `add(solid(1,1),-0.5)` (notice that only red and green are selected because blue channel is ignored by a modulator).
65 |
66 | ```hydra
67 | shape(4,0.5).out(o0)
68 | osc(10,0,1).modulate(noise(2,0),0.5).out(o1)
69 | src(o2).modulate(src(o1).add(solid(1,1),-0.5),0.01).blend(o0,0.01).out(o2)
70 | render()
71 | ```
72 |
73 | This texture can be further developed by modifying the modulating buffer using `luma()`
74 |
75 | ```hydra
76 | shape(4,0.5).out(o0)
77 | osc(10,0,1).modulate(noise(2,0),0.5).luma(0.7).out(o1)
78 | src(o2).modulate(src(o1).add(solid(1,1),-0.5),0.01).blend(o0,0.01).out(o2)
79 | render()
80 | ```
81 |
82 | or `posterize()`
83 |
84 | ```hydra
85 | shape(4,0.5).out(o0)
86 | osc(10,0,1).modulate(noise(2,0),0.5).posterize(4).out(o1)
87 | src(o2).modulate(src(o1).add(solid(1,1),-0.5),0.01).blend(o0,0.01).out(o2)
88 | render()
89 | ```
90 |
91 | modulateHue
92 | --------
93 |
94 | `modulateHue(src,amount)` behaves similar to `modulate()`; however, the pixel shift can happen to negative directions and also it uses blue channel (however, it does not use hue values as the name suggests):
95 |
96 | ```clike
97 | return _st + (vec2(_c0.g - _c0.r, _c0.b - _c0.g) * amount * 1.0/resolution);
98 | ```
99 |
100 | ```hydra
101 | shape(4,0.5).out(o0)
102 | osc(10,0,1).modulate(noise(2,0),0.5).hue(-0.1).out(o1)
103 | src(o2).modulateHue(o1,8).blend(o0,0.01).out(o2)
104 | src(o2).luma(0.3,0.3).mult(o1).out(o3)
105 | render()
106 | ```
107 |
108 | modulateScale
109 | --------
110 |
111 | ### Basics
112 |
113 | `modulateScale` is a variant of `modulate`. The original `modulate` translates the texture coordinate by `(r, g)` which is the color of modulating texture; `modulateScale` scales the pixel position by `(r, g)`. Simply applying `modulateScale` can create huge distortion, which is pleasant as it is, but you can extend your repertoire by understanding the behavior of `modulateScale`. For example, modulating a high frequency oscillator by a low frequency oscillator can create the following distortion. Note that `modulateScrollX` achieves a similar effect; nevertheless, scrolling involve texture wrapping which creates a discontinuity unlike scaling.
114 |
115 | ```hydra
116 | osc(60,0).modulateScale(osc(8,0)).out(o0)
117 | ```
118 |
119 | `kaleid` can be added to create a ripple or breathing effect towards or from the center.
120 |
121 | ```hydra
122 | osc(60,0).modulateScale(osc(8,0)).kaleid(400).out(o0)
123 | ```
124 |
125 | This breathing or ripple texture can be further used for modulating another texture.
126 |
127 | ```hydra
128 | shape(400,0.5).repeat(40,40).modulate(osc(60,0).modulateScale(osc(8,0)).kaleid(400),0.02).out(o0)
129 | ```
130 |
131 | `modulateScale`-ing a `shape` is not that interesting because it ends up similar to `modulate`:
132 |
133 | ```hydra
134 | shape(4).modulateScale(noise(8,0)).out(o0)
135 | ```
136 |
137 | To make the effect clearer, use `pixelate` to the "modulator" function (inside `modulateScale`):
138 |
139 | ```hydra
140 | shape(4).modulateScale(noise(8,0).pixelate(2,2)).out(o0)
141 | ```
142 |
143 | ### Application for Repetition
144 |
145 | Scaling with a value smaller than 1 will shrink the texture, and if it is combined with `repeat`, the texture ends up with repetition.
146 |
147 | ```hydra
148 | shape(999).repeat(1,1).modulateScale(noise(8,0).pixelate(8,8).add(solid(1,1)).color(.5,.5).posterize(4,1),-1.3,1).out(o0)
149 | ```
150 |
151 | The idea is that `repeat(1,1)` makes sure that the shape does not end up with a lonely small shape when shrunk, but will be tiled. Inside `modulateScale`, there is a long function but the point is to normalize `noise` value from [-1, 1] to [0, 1] then pass it to `posterize()`. `posterize()` always returns a value less than 1: in this case, [0, 0.75]. So the scaling factor here is -1.3, slightly smaller than -1 to emphasize the scaling effect.
152 |
153 | modulatePixelate
154 | --------
155 |
156 | `modulatePixelate(multiple,offset)` applies pixelation based on the modulator texture. At a glance, it is not so different from `modulate` and "pixelated" texture cannot be observed:
157 |
158 | ```hydra
159 | osc(40,0,2).out(o0)
160 | noise(3,0).out(o1)
161 | osc(40,0,2).modulatePixelate(noise(3,0)).out(o2)
162 | render()
163 | ```
164 |
165 | How can we create a clearer pixelation effect? A strategy is, similar to the example in `modulateScale`, to apply `pixelate` to the "modulator" function (inside `modulatePixelate`).
166 |
167 | ```hydra
168 | osc(40,0,2).out(o0)
169 | noise(3,0).pixelate(16,16).out(o1)
170 | osc(40,0,2).modulatePixelate(noise(3,0).pixelate(16,16),1024,16).out(o2)
171 | render()
172 | ```
173 |
174 | ```hydra
175 | noise(3,0).out(o0)
176 | noise(3,0).pixelate(16,16).out(o1)
177 | noise(3,0).modulatePixelate(noise(3,0).pixelate(16,16),1024,16).out(o2)
178 | render()
179 | ```
180 |
--------------------------------------------------------------------------------
/colors.md:
--------------------------------------------------------------------------------
1 | Colors
2 | ========
3 |
4 | Video Tutorial
5 | --------
6 |
7 |
8 |
9 | Gradient
10 | --------
11 |
12 | `gradient()` is one of the sources to generate a gradient texture. The first argument determines the speed of the color change.
13 |
14 | ```hydra
15 | gradient(0).out(o0)
16 | ```
17 |
18 | Oscillator
19 | --------
20 |
21 | With the third argument `offset` of `osc()`, an oscillator generates a colored texture. The parameter cycles from 0 to `PI*2`, which shifts the phase of R, G, B channels. A rainbow effect can be most visible at `offset=Math.PI/2`:
22 |
23 | ```hydra
24 | osc(30,0,Math.PI/2).out(o0)
25 | ```
26 |
27 | At `offset=Math.PI`, the red and blue channels have the same phase:
28 |
29 | ```hydra
30 | osc(30,0,Math.PI).out(o0)
31 | ```
32 |
33 | Color Operations
34 | --------
35 |
36 | ### hue
37 |
38 | Although not documented, `hue(hue)` is a useful function to shift the hue in HSV (hue, saturation, value) color space. The saturation and brightness of the color are preserved, and only the hue is affected.
39 |
40 | ```hydra
41 | osc(30,0,1).hue(0.5).out(o0)
42 | ```
43 |
44 | `hue()` is often useful to combine with feedback. In this example, `o0` is gradually modulated and its hue is shifted at the same time.
45 |
46 | ```hydra
47 | src(o0).modulateRotate(noise(2,0),0.03).hue(0.003).layer(shape(2,0.125).luma().color(0,0,1)).out(o0)
48 | ```
49 |
50 | ### colorama
51 |
52 | In contrast, `colorama()` shifts all H, S and V values, implemented as follows:
53 |
54 | ```clike
55 | vec4 colorama(vec4 c0, float amount){
56 | vec3 c = _rgbToHsv(c0.rgb);
57 | c += vec3(amount);
58 | c = _hsvToRgb(c);
59 | c = fract(c);
60 | return vec4(c, c0.a);
61 | }
62 | ```
63 |
64 | Therefore, the resulting image is rather unpredictable (for explanation, the top part shows the original image (oscillator) and the bottom shows colorama-ed result).
65 |
66 | ```hydra
67 | osc(30,0,1).out(o0)
68 | osc(30,0,1).colorama(0.01).out(o1)
69 | render()
70 | ```
71 |
72 | This unpredictability is due to the following reasons. In the GLSL snippet above, first, HSV values are increased by `amount`, and after converting back to RGB, the `fract` value is returned. Since `fract` returns the fraction of the value (equivalent to `x % 1` in JavaScript), any values exceeding 1 will wrap to 0, which causes the discontinuity and unpredictable colors. Therefore, one way to make `colorama` effect less harsh is to set negative value as an argument:
73 |
74 | ```hydra
75 | osc(30,0,1).colorama(-0.1).out(o0)
76 | ```
77 |
78 | ### luma
79 |
80 | `luma(threshold,tolerance)` masks an image based on the luminosity. Similar to `thresh()`, however, the color of the bright part of the image is preserved. `threshold` is for the threshold, and `tolerance` sets the blurriness (with bigger tolerance, the boundary becomes blurrier, and same as `threshold`, the value cannot be 0).
81 |
82 | ```hydra
83 | osc(30,0,1).luma(0.5,0.01).out(o0)
84 | ```
85 |
86 | Importantly, `luma()` returns an image with transparency. Therefore, the image can be overlayed to another image.
87 |
88 | ```hydra
89 | osc(200,0,1).rotate(1).layer(osc(30,0,1).luma(0.5,0.01)).out(o0)
90 | ```
91 |
92 | With the second argument of `luma`, a shadow-like effect can be created. First, turn the texture to grayscale by `saturate(0)`, then use `luma(0.2,0.2)` to create blurred boundaries, and finally `color(0,0,0,1)` to convert grayscale to an alpha mask with black color. In the example, foreground texture `f()` is defined for convenience to avoid duplication for shadow generation and foreground rendering. The shadow texture is overlaid on the background texture `osc(200,0,1)` and then the foreground texture `f()` is overlaid on the shadow texture.
93 |
94 | ```hydra
95 | var f=()=>osc(30,0,1)
96 | osc(200,0,1).rotate(1).layer(f().saturate(0).luma(0.2,0.2).color(0,0,0,1)).layer(f().luma(0.5,0.01)).out(o0)
97 | ```
98 |
99 | Color Remapping
100 | --------
101 |
102 | The above examples give "video synthesizer" like colors. But what if you want to use colors from a palette, for example, specified by RGB hexadecimal numbers? In the next example, a grayscale texture is re-colored in several methods.
103 |
104 | ### Mapping intensity to palette
105 |
106 | The first method attempts to map grayscale (intensity) texture to a palette, or a color scheme.
107 |
108 | ```hydra
109 | osc(Math.PI*2,0,2).modulate(noise(3,0).add(gradient(),-1),1).out(o0)
110 | ```
111 |
112 | Let's break this one-liner to four buffers for explanation.
113 |
114 | ```hydra
115 | noise(3,0).out(o0)
116 | src(o0).add(gradient(),-1).out(o1)
117 | osc(Math.PI*2,0,2).out(o2)
118 | src(o2).modulate(noise(3,0).add(gradient(),-1),1).out(o3)
119 | render()
120 | ```
121 |
122 | The first buffer `o0` is the grayscale image to be remapped (in fact, as explained earlier, `noise` outputs [-1 1] instead of [0 1]). As the second buffer is complicated, we skip this; the third buffer `o2` (top right) is the new palette. In this example, an oscillator is used to make a smooth gradient. Black color in `o0` will be mapped to the pixels on the left, and white to the pixels in the right. The fourth buffer is the result.
123 |
124 | The second buffer is used as the "modulator". In the screenshot (bottom left), it appears blank. You may wonder why add gradient, and with the second argument -1. Let's take a look at the code snippet from [modulation](modulation#modulation) chapter.
125 |
126 | ```clike
127 | Pixel[][] A; // palette
128 | Pixel[][] B; // intensity
129 | Pixel[][] ANew; // remapped texture
130 | for(int y = 0; y < height; y++) {
131 | for(int x = 0; x < width; x++) {
132 | Pixel b = B[y][x];
133 | ANew[y][x] = A[y + b.green][x + b.red];
134 | }
135 | }
136 | ```
137 |
138 | For simplicity, modulation `amount` is set to 1. For color remapping, we need to map `B`'s color value (0 - 1) to `A`'s x value (0 - 1). Therefore,
139 |
140 | ```clike
141 | ANew[y][x] = A[0][b.red];
142 | ```
143 |
144 | assuming that b.red == b.green == b.blue. To do so, we need to cancel out `x` and `y` in the array indices; we need the modulator `B`'s values to be
145 |
146 | ```clike
147 | b.red = -x + b.red;
148 | b.green = -y;
149 | ```
150 |
151 | `gradient` with default parameter directly maps the pixel position to red and green. Given that Hydra uses normalized coordinates (x=0, y=0 at the top left, x=1, y=1 at the bottom right), `add(gradient(), -1)` will result in `-x` and `-y`; therefore, with modulation `amount` 1, we can achieve the second code snippet.
152 |
153 | Technically, the example above will sample the palette diagonally from the top left to the bottom right. This becomes a problem if the palette texture has variation in y direction, such as `gradient()`:
154 |
155 | ```hydra
156 | gradient().modulate(noise(3,0).add(gradient(),-1),1).out(o0)
157 | ```
158 |
159 | If you want to want to sample in the center of the palette at y=0.5, one way is to do arithmetic operations:
160 |
161 | ```hydra
162 | gradient().modulate(noise(3,0).color(1,0,0).add(solid(0,0.5)).add(gradient(),-1),1).out(o0)
163 | ```
164 |
165 | However, an easier way is to stretch the palette in the y axis using `scale()`:
166 |
167 | ```hydra
168 | gradient().scale(1,1,1000).modulate(noise(3,0).add(gradient(),-1),1).out(o0)
169 | ```
170 |
171 | In some environments, you may need to add `color(0.99,0.99,0.99)` to the intensity texture to avoid texture wrapping.
172 |
173 | ### Using coolors
174 |
175 | (Although this method is *deprecated*, this section is kept as a reference)
176 |
177 | Here, we use a palette taken from [coolors.co](https://coolors.co).
178 |
179 | ```hydra
180 | var DD=0.01
181 | var b=(o,u,i,y,z)=>o().add(solid(1,1,1),DD).thresh(i*0.2*(z-y)+y,0.0001).luma(0.5,0.0001).color(c(u,i,0),c(u,i,1),c(u,i,2))
182 | var c=(u,i,j)=>{
183 | let cc = u.split("/"), cs = cc[cc.length - 1].split("-")
184 | return parseInt("0x" + cs[i].substring(2*j, 2+2*j))/255
185 | }
186 | var colorize=(x,u,y=0,z=1)=>b(x,u,0,y,z).layer(b(x,u,1,y,z)).layer(b(x,u,2,y,z)).layer(b(x,u,3,y,z)).layer(b(x,u,4,y,z))
187 |
188 | var url='https://coolors.co/bbdef0-f08700-f49f0a-efca08-00a6a6'
189 | var func=()=>osc(20,0,0).modulate(noise(4,0))
190 | colorize(func,url).out()
191 | ```
192 |
193 | While the example code is long, in a nutshell, the input grayscale texture defined by `func` is separated into 5 layers based on the intensity, and each layer is recolored by the hexadecimal number specified in coolors URL. The GIF animation below shows each layer recolored for explanation. At the end, these layers are overlaid on top of each other to produce the final texture (above).
194 |
195 | 
196 |
197 |
207 |
208 |
--------------------------------------------------------------------------------
/script.js:
--------------------------------------------------------------------------------
1 | /* global Torus jdom css */
2 | /* global Hydra */
3 | /* global CodeMirror */
4 |
5 | class HydraApp extends Torus.StyledComponent {
6 | init() {
7 | this.canvas = document.createElement("CANVAS");
8 | this.canvas.width = 512;
9 | this.canvas.height = 512;
10 | this.hydra = new Hydra({
11 | canvas: this.canvas,
12 | detectAudio: false,
13 | enableStreamCapture: false,
14 | width: 512,
15 | height: 512,
16 | });
17 | }
18 | styles() {
19 | return css`
20 | position: relative;
21 | `;
22 | }
23 | compose() {
24 | return jdom`