update documentation to include reticles and vector math

It's probably not very good though.
This commit is contained in:
リキ萌え 2024-10-23 19:44:42 +02:00
parent 37c575748b
commit fd3f37d744
2 changed files with 339 additions and 170 deletions

View file

@ -86,8 +86,8 @@ If you want to draw multiple scribbles, you can wrap them into a list, which we
-- Draw two colorful dots instead of one! -- Draw two colorful dots instead of one!
withDotter \d -> withDotter \d ->
[ [
stroke 8 #F00 (vec ((vecX (d To)) + 4) (vecY (d To))) stroke 8 #F00 (d To + vec 4 0)
stroke 8 #00F (vec ((vecX (d To)) - 4) (vecY (d To))) stroke 8 #00F (d To + vec (-4) 0)
] ]
``` ```
@ -108,26 +108,23 @@ It'll draw the first inner list, which contains two scribbles, and then it'll dr
withDotter \d -> withDotter \d ->
[ [
[ [
stroke 8 #F00 (vec ((vecX (d To)) + 4) (vecY (d To))) stroke 8 #F00 (d To + vec 4 0)
stroke 8 #00F (vec ((vecX (d To)) - 4) (vecY (d To))) stroke 8 #00F (d To + vec (-4) 0)
] ]
[ [
stroke 8 #FF0 (vec (vecX (d To)) ((vecY (d To)) + 4)) stroke 8 #FF0 (d To + vec 0 4)
stroke 8 #0FF (vec (vecX (d To)) ((vecY (d To)) - 4)) stroke 8 #0FF (d To + vec 0 (-4))
] ]
] ]
``` ```
::: aside ::: aside
I know this example is kind of horrendous to read right now. Another weird thing: when negating a number, you have to put it in parentheses.
This is because haku currently does not have vector math, which means we have to disassemble and reassemble vectors manually. This is because haku does not see your spaces---`vec -4`, `vec - 4`, and `vec-4` all mean the same thing!
In this case, it will always choose the 2nd interpretation---vec minus four.
{% Another weird thing: when negating a number, you have to put it in parentheses. %} So to make it interpret our minus four as, well, _minus four_, we need to enclose it in parentheses.
{% This is because haku does not see your spaces---`vec -4`, `vec - 4`, and `vec-4` all mean the same thing! %}
{% In this case, it will always choose the 2nd interpretation---vec minus four. %}
{% So to make it interpret our minus four as, well, _minus four_, we need to enclose it in parentheses. %}
::: :::
@ -163,7 +160,7 @@ If you reorder or remove any one of them, your brush isn't going to work!
- Then, `\d ->` lets us name the data we get back from the UI. - Then, `\d ->` lets us name the data we get back from the UI.
`d` ends up containing a few useful properties, but the most useful one for us is `To`, which contains the current mouse position (where *`To`* draw). `d` ends up containing a few useful properties, but the most useful one for us is `To`, which contains the current mouse position (where *`To`* draw).
- We'll get to what all these sigils mean to haku later! - We'll get to what all these `\` and `->` sigils mean to haku later!
- On the next line we have a `stroke`. - On the next line we have a `stroke`.
`stroke` is a _function_---a recipe for producing data!\ `stroke` is a _function_---a recipe for producing data!\
@ -200,11 +197,18 @@ Likewise, negative X coordinates go leftwards, and negative Y coordinates go upw
--- ---
Going back to the example though, `vec` is yet another function, except instead of producing strokes, it produces vectors! Vectors in haku are obtained with another function---`vec`---though we don't use it in the basic example, because `d To` already is a vector.
Vectors support all the usual math operators though, so if we wanted to, we could, for example, add a vector to `d To`, thus moving the position of the dot relative to the mouse cursor:
Note how it's parenthesized though---recall that function arguments are separated with spaces, so if we didn't parenthesize the `vec`, we'd end up passing `vec`, `0`, and `0` back to `stroke`---which is far from what we want! ```haku
withDotter \d ->
stroke 8 #000 (d To + vec 10 0) -- moved 10 pixels rightwards
```
And with all that, we let haku mix all the ingredients together, and get a black dot under the cursor. Also note how the `d To` expression is parenthesized.
This is because otherwise, its individual parts would be interpreted as separate arguments to `stroke`, which is not what we want!
Anyways, with all that, we let haku mix all the ingredients together, and get a black dot under the cursor.
```haku ```haku
withDotter \d -> withDotter \d ->
@ -233,21 +237,23 @@ withDotter \d ->
We replace the singular position `d To` with a `line`. `line` expects two arguments, which are vectors defining the line's start and end points. We replace the singular position `d To` with a `line`. `line` expects two arguments, which are vectors defining the line's start and end points.
For the starting position we use a _different_ property of `d`, which is `From`---this is the _previous_ value of `To`, which allows us to draw a continuous line. For the starting position we use a _different_ property of `d`, which is `From`---this is the _previous_ value of `To`, which allows us to draw a continuous line.
```haku
[
stroke 8 #F00 (circle (-16) 0 16)
stroke 8 #00F (rect 0 (-16) 32 32)
]
```
::: aside ::: aside
In haku, by adding thickness to a point, it becomes a square. In haku, by adding thickness to a point, it becomes a circle.
In theory it could also become a circle... In theory it could also become a square... but if line caps are rounded, they connect together much more nicely!
But let's not go down that rabbit hole.
::: :::
haku also supports other kinds of shapes: circles and rectangles.
```haku
withDotter \d ->
[
stroke 8 #F00 (circle (d To + vec (-16) 0) 16)
stroke 8 #00F (rect (d To + vec 0 (-16)) 32 32)
]
```
- `circle`s are made up of an X position, Y position, and radius. - `circle`s are made up of an X position, Y position, and radius.
- `rect`s are made up of the (X and Y) position of their top-left corner, and a size (width and height).\ - `rect`s are made up of the (X and Y) position of their top-left corner, and a size (width and height).\
@ -261,21 +267,23 @@ But if describing data was all we ever wanted, we could've just used any ol' dra
Remember that example from before? Remember that example from before?
```haku ```haku
[ withDotter \d ->
stroke 8 #F00 (vec 4 0) [
stroke 8 #00F (vec (-4) 0) stroke 8 #F00 (d To + vec 4 0)
] stroke 8 #00F (d To + vec (-4) 0)
]
``` ```
It has quite a bit of repetition in it. It has quite a bit of repetition in it.
If we wanted to change the size of the points, we'd need to first update the stroke thickness... If we wanted to change the size of the points, we'd need to first update the stroke thickness...
```haku ```haku
[ withDotter \d ->
stroke 4 #F00 (vec 4 0) [
stroke 4 #00F (vec (-4) 0) stroke 4 #F00 (d To + vec 4 0)
--- stroke 4 #00F (d To + vec (-4) 0)
] ---
]
``` ```
...twice of course, because we have two scribbles. ...twice of course, because we have two scribbles.
@ -284,10 +292,10 @@ So we also have to update their positions.
```haku ```haku
[ [
stroke 4 #F00 (vec 2 0) stroke 4 #F00 (d To + vec 2 0)
--- ---
stroke 4 #00F (vec (-2) 0) stroke 4 #00F (d To + vec (-2) 0)
-- --
] ]
``` ```
@ -311,11 +319,12 @@ So we can define `thickness` to be `4`, and then use it in our scribbles.
```haku ```haku
thickness = 4 thickness = 4
[ withDotter \d ->
stroke thickness #F00 (vec 2 0) [
stroke thickness #00F (vec (-2) 0) stroke thickness #F00 (d To + vec 2 0)
--------- stroke thickness #00F (d To + vec (-2) 0)
] ---------
]
``` ```
`name = data` is a special operator in haku that tells the language "whenever we say `name`, we mean `data`." `name = data` is a special operator in haku that tells the language "whenever we say `name`, we mean `data`."
@ -327,17 +336,27 @@ To keep a consistent program structure, haku also forces all your defs to appear
You can think of the defs as a list of ingredients for the final scribble. You can think of the defs as a list of ingredients for the final scribble.
Reading the ingredients can give you context as to what you're gonna be cooking, so it's useful to have them first! Reading the ingredients can give you context as to what you're gonna be cooking, so it's useful to have them first!
If you find this hard to grasp, just know that _defs usually go before `withDotter`_.
::: aside
We cannot refer to `d` from defs, because `d` is only visible on the `withDotter` line and below.
We'll get to why soon!
:::
Anyways, we can likewise replace our `2` constants with a def: Anyways, we can likewise replace our `2` constants with a def:
```haku ```haku
thickness = 4 thickness = 4
xOffset = 2 xOffset = 2
[ withDotter \d ->
stroke thickness #F00 (vec xOffset 0) [
stroke thickness #00F (vec (-xOffset) 0) stroke thickness #F00 (d To + vec xOffset 0)
--------- stroke thickness #00F (d To + vec (-xOffset) 0)
] ---------
]
``` ```
Note how in haku, names may not contain spaces. Note how in haku, names may not contain spaces.
@ -347,9 +366,13 @@ This naming convention is known as `camelCase`, and is used everywhere throughou
::: aside ::: aside
Of note is that haku names also cannot start with an uppercase letter. Of note is that haku names also cannot start with an uppercase letter.
It's reserved syntax for the future. Uppercase names are special values we call _tags_.
Right now the only names that start with an uppercase letter are the two booleans, `True` and `False`. Tags are values which represent names.
For example, the `To` in `d To` is a tag.
It represents the name of the piece of data we're extracting from `d`.
There are also two special tags, `True` and `False`, which represent [Boolean](https://en.wikipedia.org/wiki/Boolean_algebra) truth and falsehood.
::: :::
@ -361,10 +384,11 @@ thickness = 8
--- ---
xOffset = 2 xOffset = 2
[ withDotter \d ->
stroke thickness #F00 (vec xOffset 0) [
stroke thickness #00F (vec (-xOffset) 0) stroke thickness #F00 (d To + vec xOffset 0)
] stroke thickness #00F (d To + vec (-xOffset) 0)
]
``` ```
So we'll make our `xOffset` calculated dynamically from the `thickness`, to not have to update it every time. So we'll make our `xOffset` calculated dynamically from the `thickness`, to not have to update it every time.
@ -374,10 +398,11 @@ thickness = 8
xOffset = thickness / 2 xOffset = thickness / 2
------------- -------------
[ withDotter \d ->
stroke thickness #F00 (vec xOffset 0) [
stroke thickness #00F (vec (-xOffset) 0) stroke thickness #F00 (d To + vec xOffset 0)
] stroke thickness #00F (d To + vec (-xOffset) 0)
]
``` ```
Try playing with the `thickness` now! Try playing with the `thickness` now!
@ -389,14 +414,16 @@ So far we've only been dealing with strokes.
So why not switch it up a little and _fill in_ a shape? So why not switch it up a little and _fill in_ a shape?
```haku ```haku
fill #000 (circle 0 0 16) withDotter \d ->
fill #000 (circle (d To) 16)
``` ```
How about... some transparency? How about... some transparency?
Recall that colors can have an alpha component, so let's try using that! Recall that colors can have an alpha component, so let's try using that!
```haku ```haku
fill #0001 (circle 0 0 16) withDotter \d ->
fill #0001 (circle (d To) 16)
``` ```
If you play around with this brush, you'll notice how the circles blend together really nicely. If you play around with this brush, you'll notice how the circles blend together really nicely.
@ -405,21 +432,23 @@ That's the power of Alpha!
Now let's see what happens if we draw two such circles on top of each other---one bigger, one smaller. Now let's see what happens if we draw two such circles on top of each other---one bigger, one smaller.
```haku ```haku
[ withDotter \d ->
fill #0001 (circle 0 0 16) [
fill #0001 (circle 0 0 32) fill #0001 (circle (d To) 16)
] fill #0001 (circle (d To) 32)
]
``` ```
How about four? How about four?
```haku ```haku
[ withDotter \d ->
fill #0001 (circle 0 0 8) [
fill #0001 (circle 0 0 16) fill #0001 (circle (d To) 8)
fill #0001 (circle 0 0 24) fill #0001 (circle (d To) 16)
fill #0001 (circle 0 0 32) fill #0001 (circle (d To) 24)
] fill #0001 (circle (d To) 32)
]
``` ```
Okay, this is starting to look interesting, but it's also getting super unwieldy code-wise! Okay, this is starting to look interesting, but it's also getting super unwieldy code-wise!
@ -455,15 +484,16 @@ That'll need fixing!
Either way, let's define a function that'll make us those circles! Either way, let's define a function that'll make us those circles!
```haku ```haku
splat = \radius -> splat = \d, radius ->
fill #0001 (circle 0 0 radius) fill #0001 (circle (d To) radius)
[ withDotter \d ->
splat 8 [
splat 16 splat d 8
splat 24 splat d 16
splat 32 splat d 24
] splat d 32
]
``` ```
That's a lot nicer, isn't it---a template for our circles is neatly defined in a single place, and all we do is reuse it, each time with a different `radius`. That's a lot nicer, isn't it---a template for our circles is neatly defined in a single place, and all we do is reuse it, each time with a different `radius`.
@ -481,6 +511,10 @@ To dismantle that weird `\` syntax...
A function can have an arbitrary number of parameters listed, separated by commas, and that many parameters _must_ be passed to it. A function can have an arbitrary number of parameters listed, separated by commas, and that many parameters _must_ be passed to it.
Otherwise your brush will fail with an error! Otherwise your brush will fail with an error!
- Note how also pass `d` into `splat`.
If you're a keen-eyed observer, you will have already noticed that the `\d ->` after `withDotter` is a function, too!
And the values a function uses, such as `d To`, have to come from somewhere.
- And lastly, after an arrow `->`, we have the function's result. - And lastly, after an arrow `->`, we have the function's result.
Note that a function can only have _one_ result, just like a brush can only have one scribble. Note that a function can only have _one_ result, just like a brush can only have one scribble.
@ -500,19 +534,20 @@ haku limits the use of overloading to system functions for simplicity---adding o
Since these transparent circles are so much easier to draw now, let's make a few more of them! Since these transparent circles are so much easier to draw now, let's make a few more of them!
```haku ```haku
splat = \radius -> splat = \d, radius ->
fill #0001 (circle 0 0 radius) fill #0001 (circle (d To) radius)
[ withDotter \d ->
splat 8 [
splat 16 splat d 8
splat 24 splat d 16
splat 32 splat d 24
splat 40 splat d 32
splat 48 splat d 40
splat 56 splat d 48
splat 64 splat d 56
] splat d 64
]
``` ```
Okay, I'll admit this is getting kind of dumb. Okay, I'll admit this is getting kind of dumb.
@ -545,16 +580,17 @@ Until some threshold is reached, in which case we just make a single circle.
The first part is easy to do: haku allows us to define a function that calls itself without making any fuss. The first part is easy to do: haku allows us to define a function that calls itself without making any fuss.
```haku ```haku
splat = \radius -> splat = \d, radius ->
fill #0001 (circle 0 0 radius) fill #0001 (circle (d To) radius)
airbrush = \size -> airbrush = \d, size ->
[ [
splat size splat d size
airbrush (size - 8) airbrush d (size - 8)
] ]
airbrush 64 -- sounds like some Nintendo 64 game about graffiti, lol. withDotter \d ->
airbrush d 64
``` ```
But... But...
@ -588,34 +624,37 @@ color =
else else
#F00 #F00
fill color (circle 0 0 radius) withDotter \d ->
fill color (circle (d To) radius)
``` ```
- `<` is a function that produces `true` if the second argument is a smaller number than the first argument. - `<` is a function that produces `true` if the second argument is a smaller number than the first argument.
Truth and falsehood are data too, and are represented with the values `true` and `false`. Truth and falsehood are data too, and are represented with the values `True` and `False`.
- We need three arguments to execute an `if`: the condition, the data to use when the condition is `true`, and the data to use when the condition is `false`. - We need three arguments to execute an `if`: the condition, the data to use when the condition is `True`, and the data to use when the condition is `False`.
What's magical about an `if` is that _only one branch is executed_. What's magical about an `if` is that _only one branch is executed_.
In a function call, all arguments will always be calculated. In a function call, all arguments will always be calculated.
An `if` only calculates the argument it needs to produce the result. An `if` only calculates the argument it needs to produce the result.
This allows us to use it to prevent unbounded recursion in our `airbrush` example. This allows us to use it to prevent unbounded recursion in our `airbrush` example.
```haku ```haku
splat = \radius -> splat = \d, radius ->
fill #0001 (circle 0 0 radius) fill #0001 (circle (d To) radius)
airbrush = \size -> airbrush = \d, size ->
if (size > 0) if (size > 0)
[ [
splat size splat d size
airbrush (size - 8) airbrush d (size - 8)
] ]
else else
[] []
airbrush 64 withDotter \d ->
airbrush d 64
``` ```
Neat! Neat!
@ -628,39 +667,41 @@ But the airbrush still looks super primitive.
Let's try increasing the fidelity by doing smaller steps! Let's try increasing the fidelity by doing smaller steps!
```haku ```haku
splat = \radius -> splat = \d, radius ->
fill #0001 (circle 0 0 radius) fill #0001 (circle (d To) radius)
airbrush = \size -> airbrush = \d, size ->
if (size > 0) if (size > 0)
[ [
splat size splat d size
airbrush (size - 1) airbrush d (size - 1)
--- ---
] ]
else else
[] []
airbrush 64 withDotter \d ->
airbrush d 64
``` ```
Well... sure, that's just a black blob with a slight gradient on the outer edge, so let's decrease the opacity. Well... sure, that's just a black blob with a slight gradient on the outer edge, so let's decrease the opacity.
```haku ```haku
splat = \radius -> splat = \d, radius ->
fill #00000004 (circle 0 0 radius) fill #00000004 (circle (d To) radius)
--------- ---------
airbrush = \size -> airbrush = \d, size ->
if (size > 0) if (size > 0)
[ [
splat size splat d size
airbrush (size - 1) airbrush d (size - 1)
] ]
else else
[] []
airbrush 64 withDotter \d ->
airbrush d 64
``` ```
Looks good as a single dot, but if you try drawing with it... it's gray?? Looks good as a single dot, but if you try drawing with it... it's gray??
@ -698,8 +739,9 @@ Most commonly, colors are blended using _linear interpolation_---which is essent
Mathematically, linear interpolation is defined using this formula: Mathematically, linear interpolation is defined using this formula:
``` ```haku
lerp(a, b, t) = a + (b - a) * t lerp = \a, b, t ->
a + (b - a) * t
``` ```
What we're doing when blending colors, is mixing between a _source_ color (the wall), and a _destination_ color (the brush) on each channel. What we're doing when blending colors, is mixing between a _source_ color (the wall), and a _destination_ color (the brush) on each channel.
@ -710,8 +752,8 @@ But due to this reduced precision on the wall, we have to convert from a real nu
Consider that we're drawing circles of opacity 0.01 every single time. Consider that we're drawing circles of opacity 0.01 every single time.
Now let's look what happens when we try to blend each circle on top of a single pixel... Now let's look what happens when we try to blend each circle on top of a single pixel...
``` ```haku
lerp(0, 255, 0.01) = 0 + (255 - 0) * 0.01 = 255 * 0.01 = 2.55 lerp 0 255 0.01 = 0 + (255 - 0) * 0.01 = 255 * 0.01 = 2.55
``` ```
That's one circle. That's one circle.
@ -721,39 +763,39 @@ But remember that we have to convert that down to an integer between 0 to 255---
This is known as _truncation_. This is known as _truncation_.
It is not the same as rounding! It is not the same as rounding!
For negative results, it gives different results: `floor(-1.5)` would be `-2`, while `trunc(-1.5)` is `-1`. For negative results, it gives different results: `floor (-1.5)` would be `-2`, while `trunc (-1.5)` is `-1`.
::: :::
So for the next step, we'll be interpolating from `2`, and not `2.55`... So for the next step, we'll be interpolating from `2`, and not `2.55`...
``` ```haku
lerp(2, 255, 0.01) = 4.53 lerp 2 255 0.01 = 4.53
lerp(4, 255, 0.01) = 6.51 lerp 4 255 0.01 = 6.51
lerp(6, 255, 0.01) = 8.49 lerp 6 255 0.01 = 8.49
lerp(8, 255, 0.01) = 10.47 lerp 8 255 0.01 = 10.47
... ...
``` ```
I think you can see the pattern here. I think you can see the pattern here.
This continues until around 52, where the decimal point finally goes below zero, and now we're incrementing by one instead. This continues until around 52, where the decimal point finally goes below zero, and now we're incrementing by one instead.
``` ```haku
... ...
lerp(52, 255, 0.01) = 54.03 lerp 52 255 0.01 = 54.03
lerp(54, 255, 0.01) = 56.01 lerp 54 255 0.01 = 56.01
lerp(56, 255, 0.01) = 57.99 -- !! lerp 56 255 0.01 = 57.99 -- !!
lerp(57, 255, 0.01) = 58.98 lerp 57 255 0.01 = 58.98
... ...
``` ```
...and at one point, we get to this: ...and at one point, we get to this:
``` ```haku
lerp(153, 255, 0.01) = 154.02 lerp 153 255 0.01 = 154.02
lerp(154, 255, 0.01) = 155.01 lerp 154 255 0.01 = 155.01
lerp(155, 255, 0.01) = 156 lerp 155 255 0.01 = 156
lerp(156, 255, 0.01) = 156.99 -- !! lerp 156 255 0.01 = 156.99 -- !!
``` ```
Truncating 156.99 will get us to 156 again, which means we're stuck! Truncating 156.99 will get us to 156 again, which means we're stuck!
@ -762,7 +804,7 @@ This precision limitation is quite unfortunate, but I don't have a solution for
Maybe one day. Maybe one day.
For now you'll have to construct your brushes with this in mind. For now you'll have to construct your brushes with this in mind.
## And more limits ### And more limits
There are more limits on top of this, which stem from haku's design. There are more limits on top of this, which stem from haku's design.
Since it's running _your_ code on _my_ server, it has some arbitrary limits set to prevent it from causing much harm. Since it's running _your_ code on _my_ server, it has some arbitrary limits set to prevent it from causing much harm.
@ -771,14 +813,108 @@ haku code cannot be too long, and it cannot execute too long.
It cannot consume too much memory---you cannot have too many definitions, or too many temporary values at once. It cannot consume too much memory---you cannot have too many definitions, or too many temporary values at once.
There are also memory usage limits on "heavyweight" data, such as functions or lists. There are also memory usage limits on "heavyweight" data, such as functions or lists.
Basically, don't DoS me with it ^^'
I'm not specifying the precise limits here, because the app will show these to you in the future. ## Reticles
There's no point in documenting them if you can't inspect your brush's resource usage easily.
Having basic knowledge of functions and scribbles, you may be wondering: _what does that `withDotter` function do?_
It surely looks a bit magical, conjuring that `d` parameter from nowhere; and `d` contains everything we need to draw!
Put simply, `withDotter` is what rakugaki calls a *reticle*.
Reticles are pieces of data representing _interactions with the wall_.
A reticle is usually composed of two parts: the reticle data, defining how the interaction is meant to be initiated, and a _continuation function_.
When your brush gives rakugaki a reticle, it will take the reticle data, and let the user perform an interaction.
Once the interaction is performed, it will give whatever user input it has gathered as an argument to the continuation function, which can return a scribble to draw on the wall, or another reticle.
rakugaki will continue performing interactions until the brush gives back a scribble.
`withDotter` is the simplest, most direct reticle for interacting with the wall.
It allows you to paint freely, and gives the brush information on your current mouse position, previous mouse position, and _number of steps_ performed thus far.
You've already seen the former two properties---those are `d To` and `d From`.
The number of steps is available as `d Num`.
This is an integer starting at 0, incremented by 1 with each execution of the brush, until you release your mouse cursor.
This allows you to _animate_ your brushes over time!
For example, this brush draws a rainbow.
```haku
colorCurve = \n ->
abs (cos n)
pi = 3.14159265
l = 0.1 -- wavelength
withDotter \d ->
let r = colorCurve (d Num * l)
let g = colorCurve (d Num * l + pi/3)
let b = colorCurve (d Num * l + 2*pi/3)
let color = rgba r g b 1
stroke 8 color (line (d From) (d To))
```
Currently, `withDotter` is the only reticle available in rakugaki, and it cannot be chained due to its immediate nature:
`withDotter` continues executing immediately after you move your mouse by the _tiniest_ bit, so it's unclear how to even continue after that!
In the future rakugaki might get reticles that let you select lines, rectangles, ellipses, curves... but today is not that day.
### What's that, `let`?
I mentioned before that you cannot have defs inside functions.
What you _can_ have though, is `let`s, which define _variables_.
Unlike defs, which are constant and cannot vary, variables' values can depend on function parameters---and a function can be called with a different set of parameters each time, thus making them variable!
A `let` always takes the following form.
```haku
let name = value
then
```
It's very similar to a def, with one major difference.
Because a `let` by itself only _names a value_ and does not have a result, it must be followed by another expression on the following line---and that expression determines the result.
The magic is that this continuing expression can refer to the `name` we had previously assigned in the `let` expression.
::: aside
Here's a bit of trivia: the variable defined by a `let` is exactly the same as a function parameter.
The `let` above is equivalent to applying the argument `value` to a function taking in the parameter `name`, and returning `then` as the result.
```haku
(\name -> then) value
```
This basic little trick with immediately applying a function stands the basis of a formal mathematical system called [_lambda calculus_](https://en.wikipedia.org/wiki/Lambda_calculus).
It underpins a large number of functional programming languages, including haku, and more famously [Haskell](https://www.haskell.org/)!
That's right. haku is a cute little Haskell for artists.
:::
`let`s aren't only useful for reusability---they're also helpful for breaking your brushes into smaller, more digestible pieces!
Compare the above version of the rainbow brush to this version, where all the `let`s are written inline:
```haku
colorCurve = \n ->
abs (cos n)
pi = 3.14159265
l = 0.1 -- wavelength
withDotter \d ->
stroke 8 (rgba (colorCurve (d Num * l)) (colorCurve (d Num * l + pi/3)) (colorCurve (d Num * l + 2*pi/3)) 1) (line (d From) (d To))
```
That's one hard to read beast of a `stroke`!
Generally, if a line is so long it wraps around rakugaki's narrow little text editor, it's probably a good idea to split it into variables.
## Have fun ## Have fun
With that said, I hope you can have fun with rakugaki despite its flaws. With that said, I hope you can have fun with rakugaki despite it being in its infancy!
You may want to check out the [system library reference](/docs/system.html) now, to know what else you can do with the language---this little introduction barely even scratched the surface of what's possible! You may want to check out the [system library reference](/docs/system.html) now, to know what else you can do with the language---this little introduction barely even scratched the surface of what's possible!

View file

@ -63,45 +63,96 @@ Additionally, the syntax `a | b` may be used to signify that one of the listed t
- -
a : number a : number
-> number -> number
-
a : vector
-> vector
``` ```
`-`, when used in its unary form `-x`, returns the number `x` with the opposite sign. `-`, when used in its unary form `-x`, returns the number `x` with the opposite sign.
When used on vectors, returns the same vector facing the reverse direction (the individual components are negated.)
This operation is not defined for colors, because it doesn't make sense to have a color with negative RGBA values.
```haku ```haku
+ +
a : number a : number
b : number b : number
-> number -> number
+
a : vector
b : vector
-> vector
+
a : rgba
b : rgba
-> rgba
``` ```
`+` adds two numbers together. `+` adds two numbers, vectors, or colors together.
```haku ```haku
- -
a : number a : number
b : number b : number
-> number -> number
-
a : vector
b : vector
-> vector
-
a : rgba
b : rgba
-> rgba
``` ```
`-`, when used in its binary form `x - y`, subtracts two numbers from one another. `-`, when used in its binary form `x - y`, subtracts two numbers, vectors, or colors from one another.
```haku ```haku
* *
a : number a : number
b : number b : number
-> number -> number
*
a : vector
b : vector
-> vector
*
a : rgba
b : rgba
-> rgba
``` ```
`*` multiplies two numbers together. `*` multiplies two numbers together.
When used on vectors, it scales them component-wise.
Likewise for colors.
```haku ```haku
/ /
a : number a : number
b : number b : number
-> number -> number
/
a : vector
b : vector
-> vector
/
a : rgba
b : rgba
-> rgba
``` ```
`/` divides a number by another number. `/` divides a number by another number.
When used on vectors, it divides them component-wise.
Likewise for colors.
```haku ```haku
floor floor
@ -324,7 +375,7 @@ The following functions are used to compare values and work with `boolean`s.
-> boolean -> boolean
``` ```
If `b` is `()` or `False`, `not` returns `true`. If `b` is `()` or `False`, `not` returns `True`.
Otherwise it returns `False`. Otherwise it returns `False`.
```haku ```haku
@ -459,25 +510,6 @@ vecW
--- ---
Note that mathematical operations are currently not defined for vectors.
You may define your own vector operations like so:
```haku
-- Vector addition
addv = \a, b ->
vec (vecX a + vecX b) (vecY a + vecY b) (vecZ a + vecZ b) (vecW a + vecW b)
-- Likewise for subtraction, multiplication, and division.
```
Note that haku-defined vector operations like these are more costly the more components they operate on.
Therefore, it's recommended to only define them for two dimensions, unless you really need more.
```haku
addv2 = \a, b ->
vec (vecX a + vecX b) (vecY a + vecY b)
```
## Colors ## Colors
```haku ```haku
@ -520,6 +552,7 @@ For example, consider multiplicatively blending two colors.
```haku ```haku
-- This is how you can multiply two colors together. -- This is how you can multiply two colors together.
-- Note that the `*` operator works for colors, so you don't need to define this in your brushes.
mulRgba = \a, b -> mulRgba = \a, b ->
rgba (rgbaR a * rgbaR b) (rgbaG a * rgbaG b) (rgbaB a * rgbaB b) (rgbaA a * rgbaA b) rgba (rgbaR a * rgbaR b) (rgbaG a * rgbaG b) (rgbaB a * rgbaB b) (rgbaA a * rgbaA b)
``` ```
@ -539,12 +572,12 @@ mulRgba = \a, b ->
Note that haku does not clamp colors to the `0` to `1` range. Note that haku does not clamp colors to the `0` to `1` range.
It is perfectly valid to have a color that is out of range or even `NaN`, but when drawing scribbles: It is perfectly valid to have a color that is out of range or even `NaN`, but when drawing scribbles:
- any number less than `0` is clamped to `0`.
- any number greater than `1` is clamped to `1`.
- `∞` is clamped back to `1`. - `∞` is clamped back to `1`.
- `-∞` is clamped back to `0`. - `-∞` is clamped back to `0`.
- any scribble with a `NaN` color is ignored. - any scribble with a `NaN` color is ignored.
Note that just like vectors, arithmetic operations on colors are currently not defined.
Before scribbles are drawn to the wall, colors are converted to 8-bit integers for more efficient rasterization and storage. Before scribbles are drawn to the wall, colors are converted to 8-bit integers for more efficient rasterization and storage.
This means some loss of precision will happen, which may cause issues with brushes like this one: This means some loss of precision will happen, which may cause issues with brushes like this one:
@ -625,7 +658,7 @@ stroke
Creates a stroke scribble, which outlines the provided shape with a stroke of the given thickness and color. Creates a stroke scribble, which outlines the provided shape with a stroke of the given thickness and color.
Point shapes are drawn as squares, and `line` shapes have square caps at the line's endpoints. Point shapes are drawn as circles, and `line` shapes have round caps at the line's endpoints.
```haku ```haku
fill fill