DEV Community

CarlyRaeJepsenStan
CarlyRaeJepsenStan

Posted on • Edited on

(Partially) Reverse Engineering Neumorphism.io

Recently, I've been using neumorphism.io a lot for my projects - I really like the soft and squishy feel buttons have.

However, as I pasted my colors into the site for the nth time, I started to wonder - how does it work? Can I calculate these shadow colors myself?

Of course, the first step was to plug in a bunch of random colors, grab the hex codes for the shadows, and then check out their RGB numbers. I used colorhexa for this.

Name R G B
Thistle 216 191 216
Thistle's pale shadow 248 220 248
Thistle's dark shadow 184 162 184
- - - -
Powderblue 176 224 230
Powderblue's pale shadow 202 255 255
Powderblue's dark shadow 150 190 196
- - - -
Peru 205 133 63
Peru's pale shadow 236 153 72
Peru's dark shadow 174 113 54

Ok, I learned to code so I could avoid typing numbers all day....😭

Anyway, now that we have the numbers, we can try seeing how they are mutated.

Name R change G change B change
Thistle pale +32 +29 +32
Thistle dark -32 -29 -32
- - - -
Powderblue pale +26 +31 +25
Powderblue dark -26 -34 -37
- - - -

Note that for powderblue pale, the green and blue maxed out at 255, so we can assume that both numbers could be 32, or something larger.
I didn't do any more math for Peru because I got sick of it. However, what we do see is assuming the variable x where x is the amount R, G, and B are changed by, then the pale shadows are r + x, g + x, b + x while the dark shadows are r - x, g - x, b - x.

x also appears to range from 26 to 37 - with more colors, one could assume it can be from 20 to 30, or possibly even larger.

At this point, I took a step back. This project was supposed to be my quick frontend puzzle for today, not a long, code-intensive and complex like my in-progress Node.js app. Just adding and subtracting, say, 25 from each rgb value would be fine enough.

Screen Shot 2020-09-22 at 4.14.44 PM
Just for fun, I checked out the code running behind neumorphism.io. There's no way I'm going to write anything with a similar function anytime soon. So for now, I'm going to prototype something that just adds and subtracts 25 to RGB values provided by the user.
(I'll address converting between hex and RGB farther down).

Now the structure looks something like this:

  • 3 inputs - R, G, and B
  • onsubmit, compose R, G, and B into a legal CSS string using a template literal like background: rgb(${r},${g}, ${b})
  • Subtract 25 from each number and set this to the positive shadow, and add 25 and set this as the negative shadow.

And I was able to spin up a working demo in about a half-hour:

Pretty cool, huh? It even looks pretty good! I'll definitely use this for my own projects in the future.

The next step: also consume hex codes. But to compute the shadow colors, we need to convert them into HEX codes! This is the part when learning how to read a RegEx comes in handy.

I found this script from this site:

function HEXtoRGB(hex) {
    var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
    hex = hex.replace(shorthandRegex, function (m, r, g, b) {
        return r + r + g + g + b + b;
    });
    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
    } : null;
}

But.....I don't really understand how it works. And I dislike copying code. And it looks scary. So let's write break apart this function and make it ourselves!

Hex Color Structure:

8adaff <- This is an example hex color. There are two parts to a hex color:

  • The optional #
  • The two digit hexadecimal numbers composed from letters or numbers or both

To match the optional hash, we can use
^#?

  • ^ marks the beginning of the string
  • # matches # (duh)
  • ? means "optional" in RegEx-ese.

Now we need to split apart the remaining 6 characters and convert them from Base 16 to Base 10.

Screen Shot 2020-09-22 at 6.29.14 PM

What we could do is validate the form like this:

  • Search the form input for # - if there is, slice the function and make it not have a # in front.
    • Slice the new string at (0,2), (2,4), and (4,6)
  • If # is not present, start slicing.

But, a more elegant solution is to use RegExs, like the complex code snippet I found does.
To match each hexadecimal number, we can do this:
a-f - hexadecimals only use letters from a to f. The full hexadecimal number line is 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F.
\d - matches any number
[] - a bracket will match only one character
{2} - will match twice - placed after the bracket, will match against any two character permutation of number or letter.
() - I learned about these today: they are "matching groups". I'll explain their function later.

Now, to match #8adaff, we can use this RegEx:
^#?([a-f\d]{2}){3}
Cool! Now we can use the RegEx method match() and mess with the numbers.

Or... can we?
Screen Shot 2020-09-22 at 6.40.07 PM

This is bad - it seems like we're only matching the whole string, and then the last two characters. How do we fix it?

The solution was actually to write the regex like
^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})
instead of using {3}:
Screen Shot 2020-09-22 at 6.42.13 PM
This is where the () matching groups come in - by making three matching groups, the regex match method will spit out three two-character strings: our hexadecimal numbers.

I found another error while putting together the final result - put i at the end of the regex, like
/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})/i
so it matches case-insensitively.

Perfect! Now we just have to parse the numbers into base 10, and we're good to go.

//assuming hex came from the form:
var num = hex.match(regex)
var r = parseInt(num[1], 16)
var g = parseInt(num[2], 16)
var b = parseInt(num[3], 16)

And because we use parseInt, the values are automatically numbers - we don't need to worry about that anymore!

And if we want to output hex codes, turns out you can't use parseInt - you have to use toString()

Anyway, here's the final product, hosted on Glitch:

Note that this app has some issues - numbers greater than 255 are not filtered out, so they become three-character hexadecimal numbers. If I were to optimize this, I would probably add a layer of validation changing variables greater than 255 or less than 0.
Additionally, the blocks of declaring variables might be better optimized with arrays and methods like .map or .forEach.

But for a project built in a few hours, this isn't too bad!

Thoughts? Ideas? Hate mail? Please leave a comment!

Top comments (0)