CSS art is fun. SVGs are neat. Animation? Good enough for Disney; good enough for me. Time to learn a little of all of the above. The first in a series.
I figured a fun, easy learning project would be to recreate the Malevich painting on which I based my site’s theme using both SVG and pure CSS. It’s possible to animate both using native features and using JavaScript, so that’s a further learning opportunity once the main canvas is complete. Speaking of: I should do this in HTML canvas, too.
I’ve no idea how Malevich composed his painting, but for the browser, any simple model of how to position each block in space will do. I elected to position all of the rectangles from their top-left corners since that’s how SVG’s coordinate system works, and to make that the rotation origin.
I could have eyeballed the coordinates, but for the sake of speed and precision, I decided to bring the model image into PhotoShop (one could equally use or GIMP or Pixlr or even good ol’ Microsoft Paint whatever) and use the mouse and measure tools to grab some details about each of the shapes from the Info panel:
x
: distance from the left edge of the SVG, which is the same as the normal Cartesian planey
: distance from the top edge of the SVG, which differs from the normal Cartesian planewidth
: from left to right, a directed edge emerging from the originheight
: from left to right, a directed edge terminating at the origin
For perceptually representative colours, I used the Magic Wand tool and
PhotoShop’s handy Filter > Blur > Average
feature fill each shape and the
canvas with a single value, and copied the hex values directly using the
Eyedropper tool.
It took a few minutes, but the repetitive nature of grabbing these values makes for easy going. Only a handful of angles go measured with any precision; I eyeballed the rest. I stored the relevant bits for each item in a JavaScript array of objects:
rects = [
// using Θ, theta, for the angle, because Greek letters are neat
{ x: 539, y: 9, w: 65, h: 470, Θ: 32, hex: '282a29' },
{ x: 564, y: 82, w: 67, h: 490, Θ: 32, hex: '282a29' },
{ x: 357, y: 575, w: 107, h: 530, Θ: 35, hex: '2b2c2c' },
...
]
Copying the data out in the shorthand was preferable to writing the SVG elements manually. They’re not especially complicated, but there’s some repititon, and XML is quite verbose:
<rect x='1312' y='716' width='800' height='880' transform='rotate(33 1312 716)' fill='#051c5a'></rect>
<!-- └───────╫───────────────────────────────────────────────────┘ ║
║ repetition! ║
╚═══════════════════════════════════════════════════════╝ -->
I wrote the following small JavaScript function to create the <rect>
elements
in this Codepen while I tinkered
with positioning, then copied the source of the generated SVG. It’s a tiny bit
of JavaScript, but why ship JavaScript to the client if you don’t need to?
function drawMalevich1916(obj) {
const ns = 'http://www.w3.org/2000/svg'
const rect = document.createElementNS(ns, 'rect')
// destructuring oh so fancy
const { x, y, w, h, Θ, hex } = obj
// don't need *NS functions for setting these attributes; one
// could equally use rect.setAttributeNS(null, 'x', x) etc.
rect.setAttribute('x', x)
rect.setAttribute('y', y)
rect.setAttribute('width', w)
rect.setAttribute('height', h)
rect.setAttribute('transform', `rotate(${Θ} ${x} ${y})`)
rect.setAttribute('fill', `#${hex}`)
// special handling for the pink quadrilateral
if (hex === 'd4a7b5') rect.setAttribute('clip-path', 'polygon(0 0, 100% 0, 68% 100%, 0 100%)')
return rect
}
The lone pink box in Malevich’s painting is also the only one that is obviously
not a rectangle, so I handled that separately. Many polygon
configurations of
the clip-path
attribute are actually really easy to reason about, even without
a slick tool like this one, if you have a
the ability to imagine geometric space.I do, and I’m grateful for the fact. Not everyone does, mind; some people don’t have a “mind’s eye” at all.
I could have set the same styling in the CSS using an attribute selector, as
I’ve done in my explanation of polygon
below. Making the SVG standalone seemed
more in keeping with my SVG-centric goal here.
In a nutshell:
- All values are expressed relative to the width and height of the containing box.When it comes to web layout, everything is a box. Google it. Plenty of resources on the box model.
- Coordinates are given as percentage-based
x y
pairs, where x
andy
are separated by spaces, and- pairs are separated by commas.
- The last item in the
polygon
tacitly connects to the first one- (like the
Z
(closepath) command in thed
property of SVG’spath
element).
- (like the
- The closed region defines what area of the box remains visible.
Here’s one way to think about it:
rect[fill='#d4a7b5']:
polygon(
clip-path:
// x y ↓ straight-line walking directions ↓
0 0, // call this top left point your START
100% 0, // go along the top edge (0) all the the way to the right edge (100%)
68% 100%, // go to the bottom (100%), about 2/3 of the way from the left edge (68%)
0 100% // go back to the left edge (0) along the bottom (100%)
) // go back to START (implied)
If you’re still confused, Diane Ensey has a fuller explanation with pictures over at beyondpaper.
Setting the viewBox
attribute to the pixel size of the image I used saved some
math and guarantees that the SVG version of the painting will scale
proportionally. Making it repsonsive is pretty straightforward: I used
position: absolute
on the <svg>
, which I placed inside using a wrapper
<div>
that scales to the device viewport using nifty viewport-relative
units.
(There are probably other ways of doing this.) Setting the height based on vmin
and using calc
to scale the wrapper’s width at the proportions of the original
painting lets the browser handle the heavy lifting, and the beauty of SVG is
that it looks sharp at every resolution.
Here’s the finished product: