Hue Adjuster in Uiua
Date: 31 Mar 2025
Words: 2400
Draft: 1 (Most recent)
1. Intro
Uiua (docs) is a glyph-based array oriented language written in Rust language created by Kai Schmidt. I found out about it through this post. I decided to do a project to make an image one hue, something that photoshop does not natively support, to learn the language. Here is what happened.
2. Method
I had an image, the lebron james pear image, that I wanted to hue shift. First I converted it into bitmap using an online converter. Using hex editor (ImHex by WerWolf), I determined where the header data stopped and the image data stopped. Bitmap files have a sign in the 9th bit position that tells where the image data starts and the rest of the header stops, and the image data is rgb hex values. (Wikipedia, BMP File Format) I wrote a function that shifts rgb by hue, and applied it to each pixel in the main function.

The starting image
The image was made with Dalle-3 with the prompt something like "Lebron james as a pear with jpeg artifacts".
Note: Uiua is read from right to left in R-P Notation.
2.1 Hue adjusting function
In the hue adjusting function, I had to
- Convert RGB to HSV
- Change the Hue value in HSV
- Convert back to RGB
2.1.1 Convert RGB to HSV
This function in Uiua was adapted from this code in Rust I got from Claude.
The first thing we do is duplicate the color array a bunch of times using the period symbol so we can preform a bunch of different operations on it. Then I had to determine if the pixel was grayscale, or had no hue. If this was the case then the remaining operations would not be necessary. To check this, on a rank one array, this operation ⟜/↧
takes the minimum of all the elements in the array and returns [1...]
if they are all equal. We use the match glyph =
, which checks if two arrays are exactly the same, to compare the output of the minimum operation to [1 1 1]
. The output of that operation is input to a switch statement ⨬
which runs declared function RR
if the output is 0 and does nothing if the output is 1.
⨬(RR)(0 0)≍ [1 1 1] = ⟜/↧ .....
A repeated bit of code we will use is getting both the minimum and maximum elements of an array into another array, so we will declare that into a function and put it at the top of the file. The on ⟜
function keeps the input of the previous function on top of the stack, and the "reduce" operator /
which is just a forwards slash applies the preceding (from right to left!) function to all elements of an array.
MNMX ← [/↧ ⟜/↥]
The function RR checks if the R value is the largest value in the pixel. If it is, it passes it to another function RRR that determines the hue for the pixel. If it is not, then it passes it to function GG that checks if the green value is the largest in the pixel. If it is then it passes it to GGG and if not it passes it to BBB as the last option. The dip ⊙
function preforms whatever function before it on the object below the index of operation on the stack the amount of times it is used in sequence; the pick ⊡
operation functions like list indexing.
GG ← ⨬(BBB)(GGG) = ⊙(⊡ 1) /↥
RR ← ⨬(GG)(RRR) = ⊙(⊡ 0) /↥ .
Next these three similar functions return the hue value by calculating the distance between the minimum and maximum value and ill fill this in later. The comma operand takes the second-to-top value in the stack where it currently is and duplicates it onto the top of the stack; the colon operand switches the places of the top two values in the stack where it is currently at. The modulus operand ◿
looks like a triangle and functions like a modulus operand in any other language.
RRR ← ◿ 360 + 360 × 60 ÷ : ⊙(/- MNMX) - ⊙(⊡ 2) ⊸⊡ 1 ,
GGG ← + 120 × 60 ÷ : ⊙(/- MNMX) - ⊙(⊡ 2) ⊸⊡ 0 ,
BBB ← + 240 × 60 ÷ : ⊙(/- MNMX) - ⊙(⊡ 0) ⊸⊡ 1 ,
The output of the hue functions is rounded ⁅
to the nearest integer.
⁅ ⨬(RR)(0 0)≍ [1 1 1] = ⟜/↧
We then determine the value. If the lowest value is 0, then the hue is 0. If not, it is the max divided by difference between the min and the max. The fork ⊃
operand applies two operations to the same value and places both results on the stack.
⨬(÷ ⊃(/↥)(/-) MNMX :)(:) = 0 /↥ ,
These statements are wrapped in an on function to avoid dipping back so far into the stack to get the pixel value again.
⟜(⨬(÷ ⊃(/↥)(/-) MNMX :)(:) = 0 /↥ , ⁅ ⨬(RR)(0 0)≍ [1 1 1] = ⟜/↧)
Finally the saturation is determined. It is the maximum value put from a scale of 0 - 255 to a scale from 0 to 1.
÷ 256 /↥
The top three values on the stack are put in an array using the join ⊂
function twice, than the resulting array is reversed ⇌
.
⊂⊂ ÷ 256 /↥ ⟜(⨬(÷ ⊃(/↥)(/-) MNMX :)(:) = 0 /↥ , ⁅ ⨬(RR)(0 0)≍ [1 1 1] = ⟜/↧) .....
2.1.2 Change the Hue value in HSV
To change the hue value in HSV we declare the hue constant H and use the under ⍜
function to put it in index 0 in the HSV array. We then remove the popped value.
⊙(◌) ⍜(⊡0) H
⊙(◌)
2.1.3 Convert back to RGB
We next duplicate the modified HSV array for stack purposes. Then we calculate the chroma value of the color, which is the distance a color is from saturationless black on the hsv slider, or, saturation times value.
× ⊃(⊡ 1)(⊡ 2) .
We then take the HSV array back to the top and take the hue to calculate how far away it is from the Red, Green, or Blue value it is closest to from a scale of 0 to 1.
fx ⊡ 0 ,
To calculate this, first we divide the hue by 60, which will give us a number between 0 and 6, than see what point of three it is past using modulus 2, then subtract 1 so we can shift the pattern to oscillate between -1 and 1, take the absolute value of that value, then subtract it from one to invert the pattern so it oscillates between 0 and 1. Here is an illustration written by Claude.
fx ← - : 1 ⌵ - 1 ◿ 2 ÷ 60
We then clone the chroma and multiply the hue-distance value by chroma to get value "x".
× , fx ⊡ 0 , × ⊃(⊡ 1)(⊡ 2) .
At this point the stack looks like this. We can see that using the ? operator.
│╴╴╴Nv╶╶╶
├╴[30 20 200]
├╴243
├╴[140 0.9 0.78125]
├╴0.703125
├╴0.23437500…01
└╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴
We need to get the multiplier "m", which is the difference between the value and chroma. To get that, we are going to dip below once, clone the top value at that position (the 0.70 chroma value), then dip below again and pull the second-to-top value from that position (the hsv array) to that position. We take index two, flip flop the values around, and take the difference between them.
⊙(⊙(- : ⊡ 2 ,) .)
We now have the values for x and for chroma, and we need to re-arrange these so that when value m is added to them it is multiplied by 256 to give the resulting rgb.
We make an array that arbitrarily has a placeholder value 0 in position 0, value x in position 1, and the chroma in position 2.
⊂ 0 ⊂
We then put the hsv array at the top of the stackagain and take the hue value from it.
⊡ 0 : ⊙(:)
We use a switch statement in function Sel
to see what permutation of orders the null, value x, and chroma should be in based on the hue. Operand ⌊
takes the floor of whatever decimal there is. We want to see which one it is past.
Sel ← ⨬([2 1 0]|[1 2 0]|[0 2 1]|[0 1 2]|[1 0 2]|[2 0 1]) ⌊ ÷ 60
Sel ⊡ 0 : ⊙(:) ⊂ 0 ⊂ ⊙(⊙(- : ⊡ 2 ,) .) × , fx ⊡ 0 , × ⊃(⊡ 1)(⊡ 2) .
To reorder the values array according to the resulting permutation array, we define function Ch
that takes the index of the input value for the array below it and takes the result from that and indexes the value array.
Ch ← ⊡ ⊡ ⊙(,,)
We call it for all indexes 0, 1, and 2, combine them, do some stack manipulation for garbage collection, add value "m" (which is still buried in the stack) to each number in the array, normalize by multiplying by 256, and round each number in the array to an integer.
⁅ × 256 + : ⊙(◌◌) ⊂⊂ ⊙⊙(Ch 2) ⊙(Ch 1) Ch 0
This returns the rgb values adjusted for hue.
2.1.4 Hue adjusting function and supporting functions code
H ← 20 # set hue here
MNMX ← [/↧ ⟜/↥]
RRR ← ◿ 360 + 360 × 60 ÷ : ⊙(/- MNMX) - ⊙(⊡ 2) ⊸⊡ 1 ,
GGG ← + 120 × 60 ÷ : ⊙(/- MNMX) - ⊙(⊡ 2) ⊸⊡ 0 ,
BBB ← + 240 × 60 ÷ : ⊙(/- MNMX) - ⊙(⊡ 0) ⊸⊡ 1 ,
GG ← ⨬(BBB)(GGG) = ⊙(⊡ 1) /↥
RR ← ⨬(GG)(RRR) = ⊙(⊡ 0) /↥ .
fx ← - : 1 ⌵ - 1 ◿ 2 ÷ 60 # this determines value x from the hue
Ch ← ⊡ ⊡ ⊙(,,)
Sel ← ⨬([2 1 0]|[1 2 0]|[0 2 1]|[0 1 2]|[1 0 2]|[2 0 1]) ⌊ ÷ 60 # this determines the permutation of values c and x
Nv ← (
⇌ ⊂⊂ ÷ 256 /↥ ⟜(⨬(÷ ⊃(/↥)(/-) MNMX :)(:) = 0 /↥ , ⁅ ⨬(RR)(0 0)≍ [1 1 1] = ⟜/↧) ..... # this turns rgb into hsv
⊙(◌) ⍜(⊡0) H # change hue
Sel ⊡ 0 : ⊙(:) ⊂ 0 ⊂ ⊙(⊙(- : ⊡ 2 ,) .) × , fx ⊡ 0 , × ⊃(⊡ 1)(⊡ 2) . # this determines chroma (s * v) and value "x"
⁅ × 256 + : ⊙(◌◌) ⊂⊂ ⊙⊙(Ch 2) ⊙(Ch 1) Ch 0 # this finds reorders one array according to the indexes in another then adds value m and multiples by 256
)
2.2 Main Function
I used the &rb ∞ &fo
to read the data to uiua. Having found where the image data started using the hex editor, in this case at bit 138, in the main function I dropped ↘
the header data at that position and left it at the bottom of the stack, and took ↙
the color field data to operate on.
⊃↘↙138 &rb ∞ &fo "lebron.bmp"
I then took the color field data, put it into a rank 2 matrix where every element was of length three for the RBG values for each pixel ↯∞_3
, and operated on each element using the rows ≡
function on the hue adjusting function Nv
.
≡Nv ↯∞_3
The resulting data was in a rank two array with many elements each three elements long, so I deshaped ♭
it, swapped the places of the top two so the header data was at the top, and joined ⊂
them.
To write to a file, a created file has to be in the position below the stack, so we put at the beginning of the stack . &fc "lesmaller3.bmp"
and duplicate it (it won't work if it's not duplicated. See here.) We then write &w
the file and close &cl
it.
So the main line looks like this.
&cl &w ? ⊂ : ♭ ≡Nv ↯∞_3 ⊃↘↙138 &rb ∞ &fo "lebron.bmp" . &fc "leoutput.bmp"
3. Results
Each of these was run by changing the hue value on the input of the code. Here are the outputs.

Hue 20

Hue 140

Hue 260
4. Potential Improvements and Viability
The first thing that is wrong is that, all of the hues are shifted by 120 degrees! I don't know what went wrong and as of publishing I am too tired of this project to fix it.
The code could be made more succinct in some parts, particularly with stack management, and perhaps there is a clever way to merge the RRR
, GGG
, and BBB
functions, although it's not too necessary.
I could add some checks for if the color values are out of range (if they're over 255). If the color is monotone, I need to make it skip over the remaining steps and just keep it's value.
Finally I could add finding the position of the starting bits itself instead of having to look inside the file for each one for modularity.
Running this code on a 1024x1024 image took 40 seconds each time on my laptop's AMD A12-9720P RADEON R7, 12 COMPUTE CORES 4C+8G processor. I thought an array language would be good for image manipulation tasks tasks but this was not very fast. Perhaps I could modify the uiua source code in rust to take advantage of multithreading or find some other optimizations.
5. Final thoughts on the Uiua language
Uiua is a fun language to work with. Once you are familiar with what the glyphs do and learn some common patterns, it is pretty easy to model in your head what to do if you are already familiar with assembly instruction. That being said, I am not sure if it will find a place for being useful. Although being implemented in Rust, it is not as fast as I would have liked it to be. I imagined an array language would be fast where the exact same type of calculation happens over and over again, like with image manipulation. There were also some features that I felt were not completely true to an array language, such as being able to dip below the top element of a stack to change the index, being able to clone the second-to-top element, or being able to swap the top two. These rely on storing elements in memory instead of just on a stack, so I would not say it is purely an array language. It's main strength, however, is it is extremely idiomatic. Writing this code in C or Rust would have taken at least 80 lines compared to under 20 in Uiua. And, it just looks cool. I don't see myself using this language regularly anytime soon but I think it would be fun to come back to again sometime.