Fork me on GitHub

Colouroscope: Another Colour Picker

Colouroscope is a fully-frontend, serverless colour picker web application and more, built with React Redux on HTML5 & canvas.

Try it here: https://colouroscope.github.io

Introduction

Whenever I needed to find out the values of colours, I either use the Digital Colour Meter on my Mac or load images in Photoshop.

Often enough, I also need to preview differently-coloured versions of posters, webpages, designs, etc.

So I made Colouroscope:

Summary

By using virtual DOM image tags and canvas methods in Javascript, we can convert images to an array of pixel RGBA(Red, Green, Blue, Alpha) values, and vice versa.

Since we can manipulate the image data from the front-end, we can make a server-less image editor. Loading images by drag-and-drop can also be done int a server-less way using the HTML5 File loader Api.

We can also use React Redux to manage the application state and UI changes, this means we can code the final image transformation as a pure function. Also, this makes managing the interactions between forms, canvas inputs (drags, clicks) and outputs (rendered image and position) simple and easy.

After eliminating side-effects in the rendering process, we can serialize and persist a minimum amount of information into localstorage, then “rehydrate” the store when reopen the webapp.

All of this can be achieved without requiring any server-side processing.

Colouroscope is a quick way to compile a colour palette, and lets you preview changes in colour schemes instantly.

Built on React, Redux, using create-react-app as the boilerplate. As for styling, Bootstrap 4 is used to do most of the heavy lifting.

Things you can do:

  1. Drop images into the window for quick loading
  2. Position images by dragging them around within the canvas
  3. Preview colours by clicking on the canvas, or by inputting the various RGB/Hex/HSL values
  4. Add images to collection to make a colour palette
  5. Create colour substitutions to preview colour changes immediately

HTML5 Bonuses

The Canvas and Localstorage are HTML5 features that have been supported for a very long time now (7+ years).

The HTML5 File Api lets you load images without having to upload to a server. This is a new HTML5 addition to browsers, It’s only been supported since September 2016 on Safari, and IE and Edge only provides partial support so far.

Image Format Merry-go-round

The key idea to editing images in-browser is to convert it to a pixel array, and back (this can be expensive for large images).

It is just a few steps, but the transformations are asynchronous.

In Redux, the redux-thunk middleware is necessary to support asynchronous actions. To get a Base64-encoded image to show on the canvas. An image tag needs to be created, then this instance is provided after the onload callback is triggered.

const getBase64FromSrc = (src) => (
    new Promise(resolve) {
        const image = new window.Image()
        image.onload = (imgEvent) => {
            const img = imgEvent.target
            const { width, height } = img
            // Load the image in a canvas element
            const canvas = document.createElement('canvas')
            canvas.width = width
            canvas.height = height
            // Export the canvas into base64
            const base64Image = canvas.toDataURL("image/png")
            // Using the canvas context, get the image pixel data
            const ctx = canvas.getContext('2d')
            ctx.drawImage(img, 0, 0)
            const imageData = ctx.getImageData(0, 0, width, height)
            const data = imageData.data
            resolve({
                base64Image,
                data,
                height,
                width,
            })
        }
        image.src = src
    }
)

When an Image gets uploaded, the HTML5 File api provides the file that’s usable as the src attribute. Then the Base64-encoded image is obtained which is persisted locally.

To get pixel data for images, they have to be placed in a canvas, then we can call getImageData() to get the array of pixel values. This can be converted back into an image by using the putimageData() method that the canvas context provides.

Fuzzy Substitutions with Colour Delta

What is just a barely noticeable difference? The RGB (red, green, blue) colour model has values from 0-255 for each channel.

Fuzziness is defined as the largest euclidean distance possible for a substitution to happen.

A better way to handle colour deltas might be to conver into a LAB colour space, which is designed to be perspectively uniform to our eyes. A just-noticable difference is a delta (or distance) of 2.3.

React with Side Effects

A trouble with React apps is making the app fully functional. It’s not just a matter of being pedantic, bugs tend to originate from side effects. Most React/ Redux methods to handle side effects revolve around modelling or wrapping side effects to keep the state changes pure.

Some examples of side effects include: making asynchronous requests, drag-and-drop DOM events, resizing window which changes how CSS renders the view.

Canvas

The react-konva library replaces canvas with a <Stage></Stage>, <Layer></Layer> and <Image/> representation, which is great.

Drawing canvases are full of side-effects. The React way could have made us discard and redo each time the props change. Now, we don’t have to call the canvas context method drawImage whenever we need to re-position the image.

Using the library reduces the complexity so that it is now just as simple as an <img/> tag.

<Stage width={containerWidth} height={height}>
    <Layer>
        <Image
            image={display}
            x={position.x}
            y={position.y}
            onClick={onClickImage}
            draggable="true"
            onDragEnd={onDrag}
        />
    </Layer>
</Stage>

Responsive

Unfortunately, the canvas height and width needs to be provided at render time, which can be frustrating if you try to make it responsive.

Wrapping the react-dimensions library around the canvas gives us height and width values that lets us resize the canvas. Specifically, it turns the canvas into a higher-order component(HOC) that divines it’s size whenever it renders.

As a result, the render function that sizes the canvas can remain pure - simply draw the canvas based on props that were provided.

import Dimensions from 'react-dimensions'

class PV extends Component {
    render() {
        // this.props.dimensions provided by react-dimensions
        const { dimensions } = this.props
        // ...
    }
}
let PictureViewer = Dimensions()(PV)

export default PictureViewer 

File Drop

To get drag-and-drop to work in React, a wrapper component wraps the entire app. This doesn’t get in the way of rendering, but also adds event listeners when files are dropped.

The libary that does this work is react-dropzone.

Redux and Persisting images

A great feature about React and Redux apps, is that the idea of the whole application can be viewed in one file.

const appReducer = combineReducers({
    canvas, // Blacklisted from persistence
    collection,
    dropzone,
    editor,
    picker,
    picture,
    substitutions,
})

Using redux-persist, it is just an extra few lines.

// ... other imports
import { persistStore, autoRehydrate } from 'redux-persist'

const store = createStore(colourApp, composeEnhancers(
  applyMiddleware(
    thunkMiddleware,
    loggerMiddleware,
  ),
  autoRehydrate()
))

render (
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

persistStore(store, {blacklist: ['canvas']}, () => {
  store.dispatch(fetchPictureIfNeeded())
})

Persisting image pixel arrays is quite space and time prohibitive, especially if you use large images. We can blacklist reducers from being saved, then regenerate them using the callback on rehydrate, which we can configure persistStore to do.

By writing the substitutions as a pure function, we can simply discard and redo the transformations every time we load an image, and it does not matter if it’s a new image or a one loaded from localstorage.

Conclusion

Drag-and-drop, Canvas, and Localstorage are easily tamed even when using Redux, with the addition of the right libraries.

When the UI and state interactions are uncomplicated, then we can really focus on the core feature - image manipulation.