I work with oil paints a lot, mostly creating urban landscapes. An experiment crossed my mind to try to recreate these paintings using only CSS. Surprisingly, the process of painting with CSS is very similar to painting with oil; start out in broad strokes working down to more detail, then refactor again and again.
Working from the F train photograph shown above, I’ll run through some of the important steps I learned along the way to recreate this image in CSS. Here is the finished product for reference.
Setup
I set up a .paperclass to contain all of the pieces of the painting.
<div class="paper"></div>
\* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
height: 100vh;
width: 100vw;
background: #fff;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-family: 'Nanum Gothic Coding', monospace;
font-size: 11px;
}
.paper {
height: 50vmin;
width: 70vmin;
background: #cc0000;
position: relative;
z-index: -1000;
}
For the body, I used viewport units to set the height and width to 100vh and 100vw, which is relative to the percentage width of the browser window. I used flexbox to center the paper.
I set the height and width of .paper to a landscape size, set the z-index to ensure it’s always the bottom element, and set its position to relative so the paper becomes the baseline where all the painting pieces attach themselves.
Finally, I gave the background a red color so I would know the difference between what is the background and what is not (since there will be a lot of different colors). You can also add an overflow: hidden so any excess elements don’t come off the paper, but I opted to create everything to fit.
Approach
I used SCSS variables to make life easier. Since I was working from a photograph, I used Mac’s Digital Color Meter to create the color variables. I tried to name all of the colors to represent the physical object, like $floor and $seat instead of just $grey and $red. This is very important, especially as your painting grows in size.
Start painting from background to foreground, otherwise, your head will explode from reordering divs and tinkering with z-index. This may not always be possible, and you’ll inevitably end up having to shuffle some pieces around, but keep the back to front approach in mind.
I approached the CSS the same way I approach a painting. I break down everything into modules — floor, floor shadow, seat, wall, etc. Think in terms of only using one color per piece, the way a silk screener or block printer works. Where it made sense, I grouped pieces together as parent/children, for example, I had a back-wall-container and within it, all of the back wall pieces.
Painting
The basic approach looks like this:
<div class="paper">
<div class="floor"></div>
</div>
And then the CSS:
.floor {
position: absolute;
height: 4vmin;
width: 70vmin;
background: linear-gradient(to right, #222, #222, $floor);
right: 0vmin;
bottom: 0vmin;
z-index: -2;
}
I set the position to be absolute, so it’s using the paper as its guide. For each piece, I started off giving it an arbitrary background color (something easy to see, like green) then eyeballing the size based on the image. I used vmin, a percentage of the width or height, whichever one is smaller. In this case, to me, the floor looked about 4vmin high which ran the entire width of the painting.
Next, I positioned the floor on the paper. In this case to the bottom right. Most of these are trial and error, and you end up moving the pieces around like a game of Tetris until they fit into the right spot. Finally, I used my $floor color variable along with a linear-gradient in this case to create a little shadow.
Before and after
I used :before and :after to add highlights or shadows to some elements. The nice thing about pseudo-element selectors is that they allow positioning another element relative to itself. For the “Do not lean on door” sign on the subway door (which I’ve never seen anyone actually follow), I created the sign background, then used :after to create the white top border line. I opted to not add the sign text. Here is the CSS:
.door-sign {
position: absolute;
width: 10vmin;
height: 1.25vmin;
background: $door-sign;
right: 8.75vmin;
top: 22vmin;
}
.door-sign:after {
content: ' ';
position: absolute;
width: 9vmin;
height: .15vmin;
background: $door-sign-border;
left: .5vmin;
top: .2vmin;
opacity: .5;
}
I set the door sign like all the other elements with height, width, background, and positioned it on the paper. Next, I used :after, setting the content to an empty string (we don’t want to add any content, just a shape). Then I positioned the white border relative to the door sign. This makes painting certain elements easier because you don’t have to rethink where the element needs to be positioned on the entire page because it inherits from its parent.
Complicated shapes
Obviously, everything can’t be a square, rectangle or circle, so I had to create all kinds of different shapes using borders and transforms. Here is a great resource from CSS-Tricks to understand how to create complicated shapes, using before/after and borders.
For the more complicated shapes, for example, the top section of the middle seat, I had to use other shapes and layer them on top to create the desired result.
This section of the seat was a rectangle with a border-radius at the top and right. To create the tapering effect at the top left and right, I created a triangle “shim” and made it the same background as the seat base:
.seat-tri-1 {
position: absolute;
width: 0;
height: 0;
border-left: .75vmin solid transparent;
border-right: .75vmin solid transparent;
border-top: 10vmin solid $seat-base;
top: 25.5vmin;
left: 13.95vmin;
z-index: 10;
}
To create the triangle, I set the width and height to 0, then three of the borders, two of which will be transparent, one will be the color, depending on the direction of the triangle. Finally, I positioned the shim between seats one and two. If you have trouble finding the shim, change the border-top color to something visible, like green.
Refactoring
We could refactor the painting to make the code more reusable using a mixin:
@mixin paint($height, $width, $background, $left: auto, $right: auto, $top: auto, $bottom: auto) {
height: $height;
width: $width;
background: $background;
left: $left;
right: $right;
top: $top;
bottom: $bottom;
}
Then use this mixin like this:
.door-sign {
@include paint(1.25vmin, 10vmin, $door-sign, right: 8.75vmin, top: 22vmin);
}
The paint mixin uses the height and width, background color, and positioning to create a paint element. To avoid a syntax error, I had to set the left, right, top, and bottom to auto.
At first, this approach seemed better, but as I went along, I found it too abstract and decided I liked having each element explicitly stated, especially since I had to constantly go back and refer to other parts of the painting. Normally, refactoring is always good (although too much can be bad) but in this case, I was more interested in the end result than writing clean code.
Issues
I ran into a few small issues with viewport lengths. First, the smaller the vmin, the less accurate it seemed to be. Try to avoid using numbers like 4.15vmin and instead try to stick close to quarter widths, like 4.25vmin, or 4.75vmin instead.
Second, occasionally when using vmin, the more precise measurements seemed to change slightly when the page was made larger or smaller. I’m not sure if this is browser specific or if it happens when smaller viewport lengths are used, but this seems to be related to the first issue I mentioned above. Overall though, these were the only issues I ran into.
Conclusion
This exercise gave me a broader understanding of CSS. After completing a few of these paintings, I’ve noticed that I approach CSS a little differently, taking a bit more of an artistic approach, but also really thinking about each piece on the page as its own module (instead of a whole page). This has made my CSS much better.
If you create something cool, leave a comment and let me know what you learned in the process.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single page apps.
Top comments (0)