unicornpaint

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

App.js (7879B)


      1 import React, { Component } from 'react'
      2 import './App.css'
      3 import { rgb, xy, findContiguousPixels, getPixel, rotatePixelsClock, rotatePixelsCounterClock, readGifFrames } from './Utils'
      4 import { setPixel, clear, noop, save, load, addFrame, removeFrame } from './Actions'
      5 import Palette from './Palette'
      6 import PaintArea from './PaintArea'
      7 import Toolkit from './Toolkit'
      8 import ColorIndicator from './ColorIndicator'
      9 import ConnectedIndicator from './ConnectedIndicator'
     10 import LoadDialog from './LoadDialog'
     11 import SaveDialog from './SaveDialog'
     12 import FrameControl from './FrameControl'
     13 import '@fortawesome/fontawesome-free-webfonts/css/fontawesome.css'
     14 import '@fortawesome/fontawesome-free-webfonts/css/fa-solid.css'
     15 import '@fortawesome/fontawesome-free-webfonts/css/fa-regular.css'
     16 import '@fortawesome/fontawesome-free-webfonts/css/fa-brands.css'
     17 
     18 const tools = [
     19   {
     20     name: "paint",
     21     icon: "fas fa-pencil-alt",
     22     action: function (x, y, frame) {
     23       let { r, g, b } = rgb(this.state.selectedColor)
     24       setPixel(this._websocket, x, y, r, g, b, frame)
     25     }
     26   },
     27   {
     28     name: "fill",
     29     icon: "fab fa-bitbucket",
     30     action: function (x, y, frame) {
     31       let pixelsToColor = findContiguousPixels(x, y, this.state.frames[frame])
     32       pixelsToColor.forEach((coord) => {
     33         let px = { ...xy(coord), ...rgb(this.state.selectedColor) }
     34         setPixel(this._websocket, px.x, px.y, px.r, px.g, px.b, frame)
     35       })
     36     }
     37   },
     38   {
     39     name: "erase",
     40     icon: "fas fa-eraser",
     41     action: function (x, y, frame) {
     42       setPixel(this._websocket, x, y, 0, 0, 0, frame)
     43     },
     44   },
     45   {
     46     name: "pick",
     47     icon: "fas fa-eye-dropper",
     48     action: function (x, y, frame) {
     49       let pixels = this.state.frames[frame]
     50       let color = getPixel(x, y, pixels)
     51       this.setState({ selectedColor: color })
     52     },
     53   },
     54   {
     55     name: "add frame",
     56     icon: "far fa-plus-square",
     57     onSelect: function () {
     58       addFrame(this._websocket, this.state.selectedFrame+1, 50)
     59     }
     60   },
     61   {
     62     name: "remove frame",
     63     icon: "far fa-minus-square",
     64     onSelect: function () {
     65       removeFrame(this._websocket, this.state.selectedFrame)
     66     }
     67   },
     68   {
     69     name: "rotate-clockwise",
     70     icon: "fas fa-redo",
     71     onSelect: function () {
     72       let pixels = this.state.frames[this.state.selectedFrame]
     73       let newPixels = rotatePixelsClock(pixels)
     74       this._setAllPixels(newPixels)
     75     }
     76   },
     77   {
     78     name: "rotate-anticlockwise",
     79     icon: "fas fa-undo",
     80     onSelect: function () {
     81       let pixels = this.state.frames[this.state.selectedFrame]
     82       let newPixels = rotatePixelsCounterClock(pixels)
     83       this._setAllPixels(newPixels)
     84     }
     85   },
     86   {
     87     name: "save",
     88     icon: "fas fa-save",
     89     onSelect: function () {
     90       this.setState({ showingSave: true })
     91     }
     92   },
     93   {
     94     name: "load",
     95     icon: "fas fa-save",
     96     onSelect: function () {
     97       this.setState({ showingLoad: true })
     98     }
     99   },
    100   {
    101     name: "trash",
    102     icon: "fas fa-trash",
    103     onSelect: function () {
    104       clear(this._websocket)
    105     }
    106   },
    107 ]
    108 
    109 class App extends Component {
    110   constructor(props) {
    111     super(props)
    112 
    113     this.state = {
    114       connected: false,
    115       // Data from server
    116       frames: [],
    117       saves: [],
    118       imageData: "",
    119 
    120       // Local data
    121       selectedFrame: 0,
    122       selectedColor: [0, 0, 0],
    123       selectedTool: tools[0],
    124       showingSave: false,
    125       showingLoad: false,
    126     }
    127 
    128 
    129     this._applyTool = this._applyTool.bind(this)
    130     this._selectTool = this._selectTool.bind(this)
    131     this._connectWebsocket = this._connectWebsocket.bind(this)
    132     this._onMessage = this._onMessage.bind(this)
    133     this._onOpen = this._onOpen.bind(this)
    134     this._onClose = this._onClose.bind(this)
    135     this._onError = this._onError.bind(this)
    136     this._loadDrawing = this._loadDrawing.bind(this)
    137     this._saveDrawing = this._saveDrawing.bind(this)
    138     this._connectWebsocket()
    139   }
    140 
    141   _onMessage({ data }) {
    142     let { imageData, saves } = JSON.parse(data)
    143     let frames = readGifFrames(imageData)
    144 
    145     let selectedFrame = this.state.selectedFrame >= frames.length ? 
    146       frames.length - 1 :
    147       this.state.selectedFrame
    148     
    149     this.setState({
    150       frames: frames,
    151       saves: saves,
    152       selectedFrame: selectedFrame,
    153       imageData: imageData,
    154     })
    155   }
    156 
    157   _onOpen() {
    158     this.setState({ connected: true })
    159     noop(this._websocket)
    160   }
    161 
    162   _onClose() {
    163     this.setState({ connected: false })
    164     this._connectWebsocket()
    165   }
    166 
    167   _onError() {
    168     this.setState({ connected: false })
    169     this._connectWebsocket()
    170   }
    171 
    172   _connectWebsocket() {
    173     let webSocketProto = window.location.protocol === "https:" ? "wss:" : "ws:"
    174     var host = window.location.host
    175     // if (true) {
    176     //   console.log("Dev mode overriding port to 3001")
    177     //   host = window.location.hostname + ":3001"
    178     // }
    179     this._websocket = new WebSocket(`${webSocketProto}//${host}/ws`)
    180     this._websocket.onmessage = this._onMessage
    181     this._websocket.onopen = this._onOpen
    182     this._websocket.onclose = this._onClose
    183     this._websocket.onerror = this._onError
    184   }
    185 
    186   _applyTool(x, y) {
    187     let tool = this.state.selectedTool
    188     if (!tool) {
    189       return
    190     }
    191     let action = tool.action
    192     if (!action) {
    193       return
    194     }
    195     let selectedFrame = this.state.selectedFrame
    196     action.bind(this)(x, y, selectedFrame)
    197   }
    198 
    199   _selectTool(tool) {
    200     let selectAction = tool.onSelect
    201     if (selectAction) {
    202       selectAction.bind(this)()
    203     } else {
    204       this.setState({ selectedTool: tool })
    205     }
    206   }
    207 
    208   _loadDrawing(name) {
    209     load(this._websocket, name)
    210     this.setState({ showingLoad: false })
    211   }
    212 
    213   _saveDrawing(name) {
    214     save(this._websocket, name)
    215     this.setState({ showingSave: false })
    216   }
    217 
    218   _setAllPixels(newPixels) {
    219     let width = newPixels.length
    220     let height = newPixels[0].length
    221     for (var x = 0; x < width; x++) {
    222       for (var y = 0; y < height; y++) {
    223         let px = getPixel(x, y, newPixels)
    224         let { r, g, b } = rgb(px)
    225         let selectedFrame = this.state.selectedFrame
    226         setPixel(this._websocket, x, y, r, g, b, selectedFrame)
    227       }
    228     }
    229   }
    230 
    231   render() {
    232     let { selectedFrame, 
    233       frames, 
    234       imageData,
    235       connected, 
    236       selectedTool, 
    237       selectedColor,
    238       saves,
    239       showingLoad,
    240       showingSave } = this.state
    241 
    242     let pixels = frames[selectedFrame] || []
    243 
    244     return (
    245       <div className="container">
    246         <div className="header">
    247           <h1>Unicorn Paint!</h1>
    248           <ConnectedIndicator connected={connected} />
    249         </div>
    250         <div className="framePaintContainer">
    251           <div className="paintContainer">
    252             <PaintArea
    253               data={pixels}
    254               onTool={this._applyTool} />
    255             <Toolkit
    256               tools={tools}
    257               selectedTool={selectedTool}
    258               onSelectTool={this._selectTool} />
    259             <Palette
    260               selectedColor={selectedColor}
    261               onSelectColor={(color) => this.setState({ selectedColor: color })} />  
    262             <ColorIndicator color={selectedColor} />
    263           </div>
    264           <FrameControl 
    265               frames={frames} 
    266               selectedFrame={selectedFrame}
    267               onFrameSelected={ frame => this.setState({selectedFrame: frame}) }
    268               imageData={imageData}/>
    269         </div>
    270         <div>
    271           {
    272             showingLoad
    273             && <LoadDialog
    274               saves={saves}
    275               onLoad={(drawing) => this._loadDrawing(drawing)}
    276               onClose={() => this.setState({ showingLoad: false })} />
    277           }
    278         </div>
    279         <div>
    280           {
    281             showingSave
    282             && <SaveDialog
    283               saves={saves}
    284               onSave={(name) => this._saveDrawing(name)}
    285               onClose={() => this.setState({ showingSave: false })} />
    286           }
    287         </div>
    288       </div>
    289     );
    290   }
    291 }
    292 
    293 export default App;