Home Reference Source

src/dialogs/color.js

/*
 * OS.js - JavaScript Cloud/Web Desktop Platform
 *
 * Copyright (c) Anders Evenrud <andersevenrud@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author  Anders Evenrud <andersevenrud@gmail.com>
 * @licence Simplified BSD License
 */

import {h, app} from 'hyperapp';
import Dialog from '../dialog';
import {
  Box,
  BoxContainer,
  TextField,
  RangeField
} from '@osjs/gui';

/*
 * Creates a palette canvas
 */
const createPalette = (width, height) => {
  let gradient;

  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;

  const ctx = canvas.getContext('2d');

  gradient = ctx.createLinearGradient(0, 0, ctx.canvas.width, 0);
  gradient.addColorStop(0,    'rgb(255,   0,   0)');
  gradient.addColorStop(0.15, 'rgb(255,   0, 255)');
  gradient.addColorStop(0.33, 'rgb(0,     0, 255)');
  gradient.addColorStop(0.49, 'rgb(0,   255, 255)');
  gradient.addColorStop(0.67, 'rgb(0,   255,   0)');
  gradient.addColorStop(0.84, 'rgb(255, 255,   0)');
  gradient.addColorStop(1,    'rgb(255,   0,   0)');

  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

  gradient = ctx.createLinearGradient(0, 0, 0, ctx.canvas.height);
  gradient.addColorStop(0,   'rgba(255, 255, 255, 1)');
  gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0)');
  gradient.addColorStop(0.5, 'rgba(0,     0,   0, 0)');
  gradient.addColorStop(1,   'rgba(0,     0,   0, 1)');

  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

  return canvas;
};

/*
 * Converts hex to its component values
 */
const hexToComponent = hex => {
  const rgb = parseInt(hex.replace('#', ''), 16);
  const val = {};
  val.r = (rgb & (255 << 16)) >> 16;
  val.g = (rgb & (255 << 8)) >> 8;
  val.b = (rgb & 255);
  return val;
};

/*
 * Convert component values into hex
 */
const componentToHex = ({r, g, b}) => {
  const hex = [
    parseInt(r, 10).toString(16),
    parseInt(g, 10).toString(16),
    parseInt(b, 10).toString(16)
  ].map(i => String(i).length === 1 ? '0' + String(i) : i);

  return '#' + hex.join('').toUpperCase();
};

/*
 * Gets the color of a clicked palette area
 */
const colorFromClick = (ev, canvas) => {
  const {clientX, clientY} = ev;
  const box = canvas.getBoundingClientRect();
  const cx = clientX - box.x;
  const cy = clientY - box.y;
  const ctx = canvas.getContext('2d');
  const {data} = ctx.getImageData(cx, cy, 1, 1);
  const [r, g, b] = data;
  const hex = componentToHex({r, g, b});
  return {r, g, b, hex};
};

/**
 * Default OS.js Color Dialog
 */
export default class ColorDialog extends Dialog {

  /**
   * Constructor
   * @param {Core} core OS.js Core reference
   * @param {Object} args Arguments given from service creation
   * @param {String} [args.title] Dialog title
   * @param {Function} callback The callback function
   */
  constructor(core, args, callback) {
    super(core, args, {
      className: 'color',
      buttons: ['ok', 'cancel'],
      window: {
        title: args.title || 'Select Color',
        attributes: {
          minDimension: {
            width: 500,
            height: 260
          }
        }
      }
    }, callback);

    this.value = {r: 0, g: 0, b: 0, hex: '#000000'};

    let color = args.color;
    if (color) {
      if (typeof color === 'string') {
        if (color.charAt(0) !== '#') {
          color = '#' + color;
        }

        this.value = Object.assign({}, this.value, hexToComponent(args.color));
        this.value.hex = args.color;
      } else {
        this.value = Object.assign({}, this.value, args.color);
        this.value.hex = componentToHex(this.value);
      }
    }
  }

  render(options) {
    super.render(options, ($content) => {
      const canvas = createPalette(98, 98);
      const initialState = Object.assign({}, this.value);
      const initialActions = {
        setColor: color => state => color,
        setComponent: ({color, value}) => state => {
          this.value[color] = value;
          return {[color]: value};
        },
        updateHex: () => state => {
          const hex = componentToHex(state);
          this.value.hex = hex;
          return {hex};
        }
      };

      const rangeContainer = (c, v, actions) =>
        h(Box, {orientation: 'vertical', align: 'center', padding: false}, [
          h(Box, {shrink: 1}, h('div', {}, c.toUpperCase())),
          h(RangeField, {
            box: {grow: 1},
            min: 0,
            max: 255,
            value: v,
            oncreate: el => (el.value = v),
            oninput: (ev, value) => {
              actions.setComponent({color: c, value});
              actions.updateHex();
            }
          }),
          h(TextField, {
            box: {shrink: 1, basis: '5em'},
            value: String(v),
            oninput: (ev, value) => {
              actions.setComponent({color: c, value});
              actions.updateHex();
            }
          })
        ]);

      const a = app(initialState, initialActions, (state, actions) => this.createView([
        h(Box, {orientation: 'vertical', grow: 1, shrink: 1}, [
          h(BoxContainer, {orientation: 'horizontal'}, [
            h('div', {
              class: 'osjs-gui-border',
              style: {display: 'inline-block'},
              oncreate: el => el.appendChild(canvas)
            }),
            h(TextField, {
              value: state.hex,
              style: {width: '100px', color: state.hex}
            })
          ]),
          h(Box, {padding: false, grow: 1, shrink: 1}, [
            rangeContainer('r', state.r, actions),
            rangeContainer('g', state.g, actions),
            rangeContainer('b', state.b, actions)
          ])
        ])
      ]), $content);

      canvas.addEventListener('click', ev => {
        const color = colorFromClick(ev, canvas);
        if (color) {
          a.setColor(color);
          a.updateHex();
        }
      });
    });
  }

}