Skip to content

Studying the masters, SVG edition

Recreating Suprematist Composition (1916) with SVG in HTML

  • code
  • svg

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:

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:

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: