unicornpaint

A web-based painting app for raspberry PI and pimoroni Unicorn Hat HD
Log | Files | Refs | README

Utils.js (5427B)


      1 import base64js from 'base64-js'
      2 import omggif from 'omggif'
      3 
      4 /**
      5  * Converts RGB array to named object
      6  * @param {[]} item, a length 3 array containing 8 bit RGB value
      7  */
      8 function rgb(item) {
      9     return {
     10         r: item[0],
     11         g: item[1],
     12         b: item[2]
     13     }
     14 }
     15 
     16 /**
     17  * Converts XY array to named object
     18  * @param {[]} item, a 2 length array containing x and y values
     19  */
     20 function xy(item) {
     21     return {
     22         x: item[0],
     23         y: item[1]
     24     }
     25 }
     26 
     27 /**
     28  * Compares colors. 
     29  * Colors are either 3-element arrays [r, g, b]
     30  * Or objects with .r .g .b members
     31  */
     32 function colorEqual(item1, item2) {
     33     if (!item1 || !item2) {
     34         return false
     35     }
     36     if (Array.isArray(item1)) {
     37         item1 = rgb(item1)
     38     }
     39     if (Array.isArray(item2)) {
     40         item2 = rgb(item2)
     41     }
     42     return item1.r === item2.r
     43         && item1.g === item2.g
     44         && item1.b === item2.b
     45 }
     46 
     47 /**
     48  * Compares 2 coordinate values
     49  * @param { [x, y] or {x: y:} } item1 
     50  * @param { [x, y] or {x: y:} } item2 
     51  */
     52 function coordsEqual(item1, item2) {
     53     if (!item1 || !item2) {
     54         return false
     55     }
     56 
     57     if (Array.isArray(item1)) {
     58         item1 = xy(item1)
     59     }
     60     if (Array.isArray(item2)) {
     61         item2 = xy(item2)
     62     }
     63     return item1.x === item2.x 
     64         && item1.y === item2.y
     65 }
     66 
     67 /**
     68  * If I ever want to change the pixel format...
     69  * I should really route all access to pixels through here.
     70  * @param {number} x 
     71  * @param {number} y 
     72  * @param {[][][]} pixels 
     73  */
     74 function getPixel(x, y, pixels) {
     75     let column = pixels[x]
     76     if (!column) {
     77         return
     78     }
     79     return column[y]
     80 }
     81 
     82 /**
     83  * Finds contiguous regions of pixels of the same color.
     84  * Returns an array of the x & y coordinates of every pixel in the region.
     85  * Doesn't jump corners: only pixels that share a side are considered to 
     86  * join up.
     87  * 
     88  * @param {number} x 
     89  * @param {number} y 
     90  * @param {[][]]} pixels 
     91  * @param {[]]} targetColor 
     92  * @param {[][]]} contiguousPixels 
     93  */
     94 function findContiguousPixels(x, y, pixels, targetColor = getPixel(x, y, pixels), contiguousPixels=[[x, y]]) {
     95     let adjescent = [
     96       [x-1, y],
     97       [x+1, y],
     98       [x, y-1],
     99       [x, y+1]
    100     ]
    101   
    102     adjescent.forEach((coord) => {
    103       let px = xy(coord)
    104       let pxCol = getPixel(px.x, px.y, pixels)
    105       if (!pxCol) {
    106           return
    107       }
    108 
    109       // add adjescents uniquely if they are the target color
    110       let ix = contiguousPixels.findIndex((existingCoord) => coordsEqual(coord, existingCoord))
    111       if (ix !== -1) {
    112           return 
    113       }
    114 
    115       if (!colorEqual(pxCol, targetColor)) {
    116           return
    117       }
    118       contiguousPixels.push(coord)
    119       let morePixels = findContiguousPixels(px.x, px.y, pixels, targetColor, contiguousPixels)
    120       contiguousPixels.concat(morePixels)
    121     })
    122   
    123     return contiguousPixels
    124 }
    125 
    126 function rotatePixelsCounterClock(pixels) {
    127     let rotateClock = (x, y, width, height) => {
    128         return {
    129             newx: -y + (width - 1), 
    130             newy : x
    131         }
    132     }
    133     return transformPixels(pixels, rotateClock)
    134 }
    135 
    136 function rotatePixelsClock(pixels) {
    137     let rotateClock = (x, y, width, height) => {
    138         return {
    139             newx: y, 
    140             newy : - x + (height - 1)
    141         }
    142     }
    143     return transformPixels(pixels, rotateClock)
    144 }
    145 
    146 /**
    147  * Apply an arbitrary transform to some pixels.
    148  * Does not modify the original pixels, just returns the new ones
    149  * 
    150  * @param {[][][]]} pixels 
    151  * @param {(x, y, width, height) => { newx: newy: }} transform 
    152  * @returns {[][][]} newPixels
    153  */
    154 function transformPixels(pixels, transform) {
    155     let width = pixels.length
    156     let height = pixels[0].length
    157     let newPixels = []
    158     for (let x = 0; x < width; x++) {
    159         let column = []
    160         for (var y = 0; y < height; y++) {
    161             column.push([0, 0, 0])
    162         }
    163         newPixels.push(column)
    164     }
    165     
    166     for (let x = 0; x < width; x++) {
    167       for (let y = 0; y < height; y++) {
    168         let px = getPixel(x, y, pixels)
    169         let {newx, newy} = transform(x, y, width, height)
    170         newPixels[newx][newy] = px
    171       }
    172     }
    173     return newPixels
    174 }
    175 
    176 /**
    177  * Reads all frames from a GIF image, returns them as 3d arrays (x, y, color component)
    178  * @param {string} base64GifData The GIF image encoded as base64
    179  */
    180 function readGifFrames(base64GifData) {
    181     let imageBytes = base64js.toByteArray(base64GifData)
    182     let gifReader = new omggif.GifReader(imageBytes)
    183     
    184     let { width, height } = gifReader
    185     let numFrames = gifReader.numFrames()
    186     let frames = []
    187 
    188     for (var i=0; i<numFrames; i++) {
    189       let rawPixels = new Array(width * height * 4)
    190       gifReader.decodeAndBlitFrameRGBA(i, rawPixels)
    191 
    192       // Create the x, y array upfront
    193       let pixels = new Array(width)
    194       for (let y=0; y<height; y++) {
    195         pixels[y] = new Array(height)
    196       }
    197       frames.push(pixels)
    198 
    199       // Copy pixels to out array. The data provided is provided in rows
    200       var ix = 0
    201       for (let y=0; y<height; y++) {
    202         for (let x=0; x<width; x++) {
    203           let r = rawPixels[ix++]
    204           let g = rawPixels[ix++]
    205           let b = rawPixels[ix++]
    206           ix++ // Ignore the alpha component
    207           pixels[x][y] = [r, g, b]
    208         }
    209       }
    210     }
    211 
    212     return frames
    213 }
    214 
    215 export {
    216     xy,
    217     rgb,
    218     colorEqual,
    219     coordsEqual,
    220     findContiguousPixels,
    221     getPixel,
    222     rotatePixelsClock,
    223     rotatePixelsCounterClock,
    224     readGifFrames
    225 }
    226