画布图案偏移

时间:2013-11-27 21:14:55

标签: html5 canvas

我正在尝试修改画布图案的来源,但无法达到我想要的效果。

我需要绘制一条填充点状图案的线条。点状图案是通过createPattern创建的(为动态创建的画布元素提供)。

画布(基本上是一个红点)的创建方式如下:

function getPatternCanvas() {
  var dotWidth = 20,
      dotDistance = 5,
      patternCanvas = document.createElement('canvas'),
      patternCtx = patternCanvas.getContext('2d');

  patternCanvas.width = patternCanvas.height = dotWidth + dotDistance;

  // attempt #1:
  // patternCtx.translate(10, 10);

  patternCtx.fillStyle = 'red';
  patternCtx.beginPath();
  patternCtx.arc(dotWidth / 2, dotWidth / 2, dotWidth / 2, 0, Math.PI * 2, false);
  patternCtx.closePath();
  patternCtx.fill();

  return patternCanvas;
}

然后使用该模式(画布)绘制一条线:

var canvasEl = document.getElementById('c');
var ctx = canvasEl.getContext('2d');
var pattern = ctx.createPattern(getPatternCanvas(), 'repeat');

// attempt #2
// ctx.translate(10, 10);

ctx.strokeStyle = pattern;
ctx.lineWidth = 30;

ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(100, 100);
ctx.stroke();

所以我们得到了这个:

enter image description here

现在,我想用10px来抵消这些点的来源。翻译模式画布没有用,因为我们没有得到完整的点:

enter image description here

翻译画布本身的上下文并没有帮助,因为它抵消了线条,而不是模式本身:

enter image description here

翻译上下文似乎不会影响模式原点。

有没有办法修改模式本身的偏移?

3 个答案:

答案 0 :(得分:10)

更新由于此答案已发布,现在(自2015/02起)setTransform()个实例本身就有CanvasPattern(请参阅specs)。它可能还没有在所有浏览器中都可用(只有Firefox在编写时支持它)。

方法1

您可以偏移主画布并将增量值添加到该行的实际位置:

var offsetX = 10, offsetY = 10;
ctx.translate(offsetX, offsetY);
ctx.lineTo(x - offsetX, y - offsetY);
// ...

<强> Example

(演示只显示正在翻译的模式,但当然,通常你会将它与它一起移动)。

Line

等。这样您就可以取消该行本身的翻译。但它引入了一些开销,因为每次都需要计算坐标,除非你可以缓存结果值。

方法2

我能想到的另一种方式是创造一种自我模式。即对于图案画布重复点,这样当你将它移到边界外时,它会以相反的方向重复。

例如,这里第一个正方形是正常模式,第二个是描述为方法二的偏移模式,第三个图像使用偏移模式进行填充,显示它将起作用。

关键是这两个模式是相同大小,并且第一个模式重复偏移到第二个版本中。然后可以将第二个版本用作主要的填充。

<强> Example 2

<强> Example 3 animated

Dots

var ctx = demo.getContext('2d'),
    pattern;

// create the pattern    
ctx.fillStyle = 'red';
ctx.arc(25, 25, 22, 0, 2*Math.PI);
ctx.fill();

// offset and repeat first pattern to base for second pattern
ctx = demo2.getContext('2d');
pattern = ctx.createPattern(demo, 'repeat');
ctx.translate(25, 25);
ctx.fillStyle = pattern;
ctx.fillRect(-25, -25, 50, 50);

// use second pattern to fill main canvas
ctx = demo3.getContext('2d');
pattern = ctx.createPattern(demo2, 'repeat');
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, 200, 200);

答案 1 :(得分:6)

您可以在绘制线条/形状后简单地翻译上下文。在抚摸/填充之前抵消模式。更新了小提琴http://jsfiddle.net/28BSH/27/

android.support.v7.widget.SearchView
android-support-v7-appcompat_23.1

答案 2 :(得分:0)

我碰巧在codepen上创建了这个小工具,可能有助于创建填充模式。您可以应用多个图案层,创建线条,圆形和正方形,或应用颜色。

&#13;
&#13;
// Utils
const produce = immer.default

// Enums
const fillPatternType = {
  LINE: 'line',
  CIRCLE: 'circle',
  SQUARE: 'square'
}

// Utils
function rgbToHex(rgb) {
  const [r, g, b] = rgb
  return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}

function hexToRgb(hex) {
  var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result ? [
    parseInt(result[1], 16),
    parseInt(result[2], 16),
    parseInt(result[3], 16)
  ] : null;
}

const DEFAULT_FILL_PATTERN_ROTATION = 0
const DEFAULT_FILL_PATTERN_THICKNESS = 1
const DEFAULT_FILL_PATTERN_SIZE = 20
const DEFAULT_FILL_PATTERN_BACKGROUND_COLOR = '#ffffff'
class Application extends React.Component {
  constructor() {
    super()
    
    // Initialize state  
    this.state = {
      stages: [{
        patternName: 'pattern 1',
        isFocus: true
      }],
      patternMap: {
        'pattern 1': {
          size: DEFAULT_FILL_PATTERN_SIZE,
          backgroundColor: '#ffffff',
          contents: []
        }
      }
    }
    
    // Binding callbacks
    this.addNewStage = this.addNewStage.bind(this)
    this.getFillPatternCanvasesByName = this.getFillPatternCanvasesByName.bind(this)
    this.getFillPatternConfigsByName = this.getFillPatternConfigsByName.bind(this)
    this.getFillPatternCanvasesByConfigs = this.getFillPatternCanvasesByConfigs.bind(this)
    this.addPatternControl = this.addPatternControl.bind(this)
    this.toggleControl = this.toggleControl.bind(this)
    this.removeControl = this.removeControl.bind(this)
    this.setPatternConfig = this.setPatternConfig.bind(this)
    this.focusCanvasStage = this.focusCanvasStage.bind(this)
    this.setPatternSize = this.setPatternSize.bind(this)
    this.setPatternForegroundColor = this.setPatternForegroundColor.bind(this)
    this.setPatternBackgroundColor = this.setPatternBackgroundColor.bind(this)
    this.togglePatternColor = this.togglePatternColor.bind(this)
    this.downloadStages = this.downloadStages.bind(this)
  }
  
  // Add a new canvas stage
  addNewStage(evt) {
    const patternName = `pattern ${Object.keys(this.state.patternMap).length + 1}`
    const focusedPatternName = this.getFocusedPatternName()
    let newStage = {
      patternName,
      isFocus: true
    }
    
    // get an updated existing state
    this.setState(produce(this.state, (draftState) => {
      for (let stage of draftState.stages) {
        stage.isFocus = false
      }

      draftState.patternMap[patternName] = this.getFillPatternConfigsByName(focusedPatternName)
      draftState.stages.push(newStage)
    }))
  }

  downloadStages() {
    let zip = new JSZip()

    const stageCanvases = document.querySelectorAll('canvas.stage')
    const folder = zip.folder('stagePngs')
    Array.prototype.slice.call(stageCanvases).forEach((stageCanvas, i) => {
      // Using blob is better but it requires promise.all
      // For simplicity here we are just going to decode base64 string
      // stageCanvas.toBlob((blob => {
      //   folder.file(`pattern_${i + 1}.png`, blob)
      // }))

      let imgData = stageCanvas.toDataURL()
      imgData = imgData.substr(22)
      imgData = atob(imgData)

      folder.file(`pattern_${i + 1}.png`, imgData, {
        // This is needed for jszip
        // See https://stackoverflow.com/questions/37557426/put-generated-png-image-into-jszip
        binary: true
      })

      // Put the pattern config in folder as well
      folder.file(`pattern_${i + 1}.json`, JSON.stringify(this.state.patternMap[`pattern ${i + 1}`], null, 2))
    })


    folder.generateAsync({
      type: 'base64'
    })
    .then((base64) => {
      window.location = "data:application/zip;base64," + base64
    })
  }
  
  removeNewStage(evt) {
  }
  
  getFillPatternConfigsByName(patternName) {
    return this.state.patternMap[patternName]
  }
  
  getFillPatternCanvasesByName(patternName) {
    const fillPatternConfigs = this.getFillPatternConfigsByName(patternName)
    return this.getFillPatternCanvasesByConfigs(fillPatternConfigs)
  }
  
  getFillPatternCanvasesByConfigs(fillPatternConfigs) {
    return fillPatternConfigs.contents.map((fillPatternConfig) => {
      const size = fillPatternConfigs.size || DEFAULT_FILL_PATTERN_SIZE
      const backgroundColor = fillPatternConfigs.backgroundColor || DEFAULT_FILL_PATTERN_BACKGROUND_COLOR

      switch(fillPatternConfig.type) {
        case fillPatternType.LINE:
          return this.getLineFillPatternCanvases(Object.assign({}, fillPatternConfig, {
            size,
            backgroundColor
          }))

        case fillPatternType.CIRCLE:
          return this.getCircleFillPatternCanvases(Object.assign({}, fillPatternConfig, {
            size,
            backgroundColor
          }))

        case fillPatternType.SQUARE:
          return this.getSquareFillPatternCanvases(Object.assign({}, fillPatternConfig, {
            size,
            backgroundColor
          }))
          
        default:
          return this.getLineFillPatternCanvases(Object.assign({}, fillPatternConfig, {
            size,
            backgroundColor
          }))
      }
    })
  }
  
  getLineFillPatternCanvases(fillPatternConfig) {
    let {
      size,
      density,
      thickness,
      rotation,
      offsetX,
      offsetY,
      foregroundColor
    } = fillPatternConfig
    
    rotation = rotation / 360 * Math.PI * 2
    
    let canvas = document.createElement('canvas')
    canvas.width = size
    canvas.height = size

    let textureCtx = canvas.getContext('2d')
    
    // Rotate texture canvas
    textureCtx.translate(size / 2, size / 2)
    textureCtx.rotate(rotation)
    textureCtx.translate(-size / 2, -size / 2)
    
    let minY = -size * 1.3
    let maxY = size * 2.3
    let minX = -size * 1.3
    let maxX = size * 2.3

    let y = minY
    textureCtx.strokeStyle = foregroundColor
    while (y < maxY) {
      textureCtx.beginPath();
      textureCtx.lineWidth = thickness;
      textureCtx.moveTo(minX + offsetX, y + offsetY);
      textureCtx.lineTo(maxX + offsetX, y + offsetY);
      textureCtx.stroke();
      y += density;
    }

    return canvas
  }

  getCircleFillPatternCanvases(fillPatternConfig) {
    let {
      size,
      density,
      thickness,
      rotation,
      offsetX,
      offsetY,
      foregroundColor
    } = fillPatternConfig
    
    rotation = rotation / 360 * Math.PI * 2
    
    let canvas = document.createElement('canvas')
    canvas.width = size
    canvas.height = size
    
    let textureCtx = canvas.getContext('2d')
    
    // Rotate texture canvas
    textureCtx.translate(size / 2, size / 2)
    textureCtx.rotate(rotation)
    textureCtx.translate(-size / 2, -size / 2)
    
    let minY = -size * 1.3
    let maxY = size * 2.3
    let minX = -size * 1.3
    let maxX = size * 2.3

    let x
    let y
    textureCtx.fillStyle = foregroundColor
    for (y = minY; y < maxY; y += density) {
      for (x = minX; x < maxX; x += density) {
        textureCtx.beginPath();
        textureCtx.arc(x + offsetX, y + offsetY, thickness, 0, Math.PI * 2);
        textureCtx.fill();
      }
    }

    return canvas
  }

  getSquareFillPatternCanvases(fillPatternConfig) {
    let {
      size,
      density,
      thickness,
      rotation,
      offsetX,
      offsetY,
      foregroundColor
    } = fillPatternConfig
    
    rotation = rotation / 360 * Math.PI * 2
    
    let canvas = document.createElement('canvas')
    canvas.width = size
    canvas.height = size
    
    let textureCtx = canvas.getContext('2d')
    
    // Rotate texture canvas
    textureCtx.translate(size / 2, size / 2)
    textureCtx.rotate(rotation)
    textureCtx.translate(-size / 2, -size / 2)
    
    let minY = -size * 1.3
    let maxY = size * 2.3
    let minX = -size * 1.3
    let maxX = size * 2.3

    let x
    let y
    textureCtx.fillStyle = foregroundColor
    for (y = minY; y < maxY; y += density) {
      for (x = minX; x < maxX; x += density) {
        textureCtx.beginPath();
        textureCtx.rect(x + offsetX, y + offsetY, thickness, thickness);
        textureCtx.fill();
      }
    }

    return canvas
  }
  
  getFocusedPatternName() {
    return this.state.stages.filter(stage => stage.isFocus)[0].patternName
  }
  
  addPatternControl(patternName, patternControl) {
    this.setState(produce(this.state, (draftState) => {
      draftState.patternMap[patternName].contents.unshift(Object.assign({}, {
        density: 5,
        thickness: 1,
        rotation: 0,
        offsetX: 0,
        offsetY: 0,
        foregroundColor: '#000000'
      }, patternControl))
    }))
  }
  
  toggleControl(patternName, patternConfigIndex) {
    this.setState(produce(this.state, (draftState) => {
      draftState.patternMap[patternName].contents[patternConfigIndex].showDetails = !draftState.patternMap[patternName].contents[patternConfigIndex].showDetails
    }))
  }
  
  removeControl(patternName, patternConfigIndex) {
    this.setState(produce(this.state, (draftState) => {
      draftState.patternMap[patternName].contents.splice(patternConfigIndex, 1)
    }))
  }
  
  setPatternConfig(patternName, patternConfigIndex, changeset) {
    this.setState(produce(this.state, (draftState) => {
      Object.assign(draftState.patternMap[patternName].contents[patternConfigIndex], changeset)
    }))
  }

  setPatternSize(patternName, patternSize) {
    this.setState(produce(this.state, (draftState) => {
      draftState.patternMap[patternName].size = patternSize
    }))
  }

  setPatternForegroundColor(patternName, patternConfigIndex, color) {
    this.setState(produce(this.state, (draftState) => {
      draftState.patternMap[patternName].contents[patternConfigIndex].foregroundColor = color
    }))
  }

  setPatternBackgroundColor(patternName, color) {
    this.setState(produce(this.state, (draftState) => {
      draftState.patternMap[patternName].backgroundColor = color
    }))
  }

  togglePatternColor(patternName,  patternConfigIndex) {
    this.setState(produce(this.state, (draftState) => {
      let foregroundColor = draftState.patternMap[patternName].contents[patternConfigIndex].foregroundColor
      draftState.patternMap[patternName].contents[patternConfigIndex].foregroundColor = draftState.patternMap[patternName].backgroundColor
      draftState.patternMap[patternName].backgroundColor = foregroundColor
    }))
  }

  focusCanvasStage(stageIndex) {
    this.setState(produce(this.state, (draftState) => {
      for (let stage of draftState.stages) {
        stage.isFocus = false
      }

      draftState.stages[stageIndex].isFocus = true
    }))
  }
  
  render() {
    return (
      <div>
        <CanvasStageContainer stages={this.state.stages}
          addNewStageCallback={this.addNewStage}
          downloadStages={this.downloadStages}
          getFillPatternCanvasesByName={this.getFillPatternCanvasesByName}
          getFillPatternConfigsByName={this.getFillPatternConfigsByName}
          focusCanvasStage={this.focusCanvasStage}></CanvasStageContainer>
        
        <hr />
        
        <PatternContainer focusPatternName={this.getFocusedPatternName()}
          getFillPatternCanvasesByName={this.getFillPatternCanvasesByName}
          getFillPatternConfigsByName={this.getFillPatternConfigsByName}
          getFillPatternCanvasesByConfigs={this.getFillPatternCanvasesByConfigs}
          addPatternControl={this.addPatternControl}
          toggleControl={this.toggleControl}
          removeControl={this.removeControl}
          setPatternConfig={this.setPatternConfig}
          setPatternSize={this.setPatternSize}
          setPatternForegroundColor={this.setPatternForegroundColor}
          setPatternBackgroundColor={this.setPatternBackgroundColor}
          togglePatternColor={this.togglePatternColor}></PatternContainer>
      </div>
    )
  }
}

// CanvasStageContainer component
const CanvasStageContainer = (props) => {
  let stages = props.stages.map((stage, i) => (
    <CanvasStage key={i} 
      patternName={stage.patternName} 
      isFocus={stage.isFocus}
      getFillPatternCanvasesByName={props.getFillPatternCanvasesByName}
      getFillPatternConfigsByName={props.getFillPatternConfigsByName}
      focusCanvasStage={() => props.focusCanvasStage(i)}></CanvasStage>
  ))
  
  return (
    <div className="stage-container">
      <div className="stage-wrapper">
        {stages}
      </div>
      <button className="btn new-target" 
        onClick={props.addNewStageCallback}>Add new stage</button>
      <button className="btn" 
        onClick={props.downloadStages}>Download stages</button>
    </div>
  )
}

// CanvasStage component
// Need to use stateful component as we need to get ref to canvas
const DEFAULT_CANVAS_WIDTH = 150
const DEFAULT_CANVAS_HEIGHT = 150
class CanvasStage extends React.Component {
  componentDidMount() {
    this.updateCanvas();
  }
  
  componentDidUpdate() {
    this.updateCanvas();
  }
    
  updateCanvas() {
    const canvas = this.refs.canvas
    const context = canvas.getContext('2d')

    const patternName = this.props.patternName
    const fillPatternCanvases = this.props.getFillPatternCanvasesByName(patternName)
    const fillPatternConfigs = this.props.getFillPatternConfigsByName(patternName)
    
    canvas.width = DEFAULT_CANVAS_WIDTH
    canvas.height = DEFAULT_CANVAS_HEIGHT

    context.clearRect(0, 0, canvas.width, canvas.height)
    context.fillStyle = fillPatternConfigs.backgroundColor
    context.fillRect(0, 0, canvas.width, canvas.height)

    for (let fillPatternCanvas of fillPatternCanvases) {
      context.fillStyle = context.createPattern(fillPatternCanvas, 'repeat')
      context.fillRect(0, 0, canvas.width, canvas.height)
    }
  }
  
  render() {
    return (
      <div className='canvas-stage'>
        <canvas width={this.props.width || DEFAULT_CANVAS_WIDTH}
          height={this.props.height || DEFAULT_CANVAS_HEIGHT}
          className="stage main"
          onClick={this.props.focusCanvasStage}
          ref="canvas"></canvas>
        <span>{this.props.patternName}</span>
      </div>
      
    )
  }
}

const PatternContainer = (props) => {
  return (
    <div className="pattern-container">
      <PatternPanel patternName={props.focusPatternName} {...props}></PatternPanel>
    </div>
  )
}

class PatternPanel extends React.Component {
  componentDidMount() {
    this.updateCanvas();
  }
  
  componentDidUpdate() {
    this.updateCanvas();
  }
    
  updateCanvas() {
    const canvas = this.refs.patternCanvas
    const context = canvas.getContext('2d')

    const patternName = this.props.patternName
    const patternConfigs = this.props.getFillPatternConfigsByName(patternName)
    const fillPatternCanvases = this.props.getFillPatternCanvasesByConfigs(patternConfigs)
    
    canvas.width = patternConfigs.size
    canvas.height = patternConfigs.size
  
    context.clearRect(0, 0, canvas.width, canvas.height)
    for (let fillPatternCanvas of fillPatternCanvases) {
      context.drawImage(fillPatternCanvas, 0, 0, canvas.width, canvas.height)
    }
  }
  
  render() {
    const patternName = this.props.patternName
    const patternConfigs = this.props.getFillPatternConfigsByName(patternName)
    const patternControls = patternConfigs.contents.map((patternConfig, i) => (
      <PatternControl key={i}
        {...patternConfig}
        patternSize={patternConfigs.size}
        backgroundColor={patternConfigs.backgroundColor}
        toggleControl={() => this.props.toggleControl(patternName, i)}
        removeControl={() => this.props.removeControl(patternName, i)}
        setPatternConfig={(changeSet) => this.props.setPatternConfig(patternName, i, changeSet)}
        setPatternSize={(size) => this.props.setPatternSize(patternName, size)}
        setPatternForegroundColor={(color) => this.props.setPatternForegroundColor(patternName, i, color)}
        setPatternBackgroundColor={(color) => this.props.setPatternBackgroundColor(patternName, color)}
        togglePatternColor={() => this.props.togglePatternColor(patternName, i)}></PatternControl>
    ))
    
    return (
      <div className='pattern-panel'>
        <div className='pattern-canvas-wrapper'>
          <strong>{patternName}</strong>
          <canvas className='pattern-canvas'
            ref='patternCanvas'></canvas>
        </div>
        
        <div className='pattern-panel-control-wrapper'>  
          <button className='pattern-panel-add-control' 
            onClick={() => this.props.addPatternControl(patternName, {
              type: fillPatternType.LINE
            })}>Add line</button>
          <button className='pattern-panel-add-control' 
            onClick={() => this.props.addPatternControl(patternName, {
              type: fillPatternType.CIRCLE
            })}>Add circle</button>
          <button className='pattern-panel-add-control' 
            onClick={() => this.props.addPatternControl(patternName, {
              type: fillPatternType.SQUARE
            })}>Add square</button>
          <ul className='pattern-controls'>
            {patternControls}
          </ul>
        </div>
      </div>
    )
  }
}

// Pattern control component
const PatternControl = (props) => {
  return (
    <div className='pattern-control'>
      <div className='pattern-control-summary'>
        <span className='pattern-control-summary-title'>{props.type}</span>
        <div className='pattern-control-items'
          onClick={props.toggleControl}>
          <div className='pattern-control-item'>
            <strong>size</strong>
            <span>{props.patternSize} px</span>
          </div>
          <div className='pattern-control-item'>
            <strong>density</strong>
            <span>{props.density} px</span>
          </div>
          <div className='pattern-control-item'>
            <strong>thickness</strong>
            <span>{props.thickness} px</span>
          </div>
          <div className='pattern-control-item'>
            <strong>rotation</strong>
            <span>{props.rotation} degree</span>
          </div>
          <div className='pattern-control-item'>
            <strong>X offset</strong>
            <span>{props.offsetX} px</span>
          </div>
          <div className='pattern-control-item'>
            <strong>Y offset</strong>
            <span>{props.offsetY} px</span>
          </div>
          <div className='pattern-control-item'>
            <strong>foreground color</strong>
            <span>{props.foregroundColor}</span>
          </div>
          <div className='pattern-control-item'>
            <strong>background color</strong>
            <span>{props.backgroundColor}</span>
          </div>
        </div>
        
        <div className='pattern-control-buttons'>
          <button onClick={props.togglePatternColor}>Toggle color</button>
          <button onClick={props.removeControl}>Remove</button>
        </div>
      </div>
      
      {props.showDetails ? (
        <div className='pattern-control-body'>
          <div className='pattern-control-row'>
            <span>size ({props.patternSize} px)</span>
            <input type='range' min='16' max='64' step='4' value={props.patternSize}
              onChange={(evt) => props.setPatternSize(parseInt(evt.target.value))}></input>
          </div>

          <div className='pattern-control-row'>
            <span>density ({props.density} px)</span>
            <input type='range' min='1' max='20' step='1' value={props.density}
              onChange={(evt) => props.setPatternConfig({
                density: parseInt(evt.target.value)
              })}></input>
          </div>
          
          <div className='pattern-control-row'>
            <span>thickness ({props.thickness} px)</span>
            <input type='range' min='1' max='10' step='1' value={props.thickness}
              onChange={(evt) => props.setPatternConfig({
                thickness: parseInt(evt.target.value)
              })}></input>
          </div>
          
          <div className='pattern-control-row'>
            <span>rotation ({props.rotation} px)</span>
            <input type='range' min='0' max='360' step='1' value={props.rotation}
              onChange={(evt) => props.setPatternConfig({
                rotation: parseInt(evt.target.value)
              })}></input>
          </div>
          
          <div className='pattern-control-row'>
            <span>X offset ({props.offsetX} px)</span>
            <input type='range' min='0' max={props.patternSize} step='1' value={props.offsetX}
              onChange={(evt) => props.setPatternConfig({
                offsetX: parseInt(evt.target.value)
              })}></input>
          </div>
          
          <div className='pattern-control-row'>
            <span>Y offset ({props.offsetY} px)</span>
            <input type='range' min='0' max={props.patternSize} step='1' value={props.offsetY}
              onChange={(evt) => props.setPatternConfig({
                offsetY: parseInt(evt.target.value)
              })}></input>
          </div>

          <div className='pattern-control-row'>
            <span>foreground color RED ({hexToRgb(props.foregroundColor)[0]})</span>
            <input  type='range' min='0' max='255' step='1' value={hexToRgb(props.foregroundColor)[0]}
              onChange={(evt) => {
                props.setPatternForegroundColor(
                  rgbToHex([parseInt(evt.target.value)].concat(hexToRgb(props.foregroundColor).slice(1))))
              }}></input>
          </div>

          <div className='pattern-control-row'>
            <span>foreground color GREEN ({hexToRgb(props.foregroundColor)[1]})</span>
            <input type='range' min='0' max='255' step='1' value={hexToRgb(props.foregroundColor)[1]}
              onChange={(evt) => {
                let rgb = hexToRgb(props.foregroundColor)
                props.setPatternForegroundColor(
                  rgbToHex([rgb[0], parseInt(evt.target.value), rgb[2]]))
              }}></input>
          </div>

          <div className='pattern-control-row'>
            <span>foreground color BLUE ({hexToRgb(props.foregroundColor)[2]})</span>
            <input type='range' min='0' max='255' step='1' value={hexToRgb(props.foregroundColor)[2]}
              onChange={(evt) => {
                props.setPatternForegroundColor(
                  rgbToHex(hexToRgb(props.foregroundColor).slice(0, 2).concat([parseInt(evt.target.value)])))
              }}></input>
          </div>
        </div>
      ) : null}
      
    </div>
  )
}

/*
 * Render the above component into the div#app
 */
ReactDOM.render(<Application />, document.getElementById('app'));
&#13;
html,
body {
  height: 100%;
  font-family: sans-serif;
}

button {
  cursor: pointer;
}

ul {
  padding: 0;
}

#app {
  min-width: 1100px;
}

canvas.stage {
  border: 1px solid #c1c1c1;
  cursor: pointer;
}

canvas.pattern-canvas {
  border: 1px solid #c1c1c1;
  margin: 20px;
}

button.btn {
  height: 25px;
  background-color: white;
  margin-right: 10px;
}

.stage-container {
  padding: 10px;
}

.stage-wrapper {
  display: flex;
  flex-wrap: wrap;
  margin-bottom: 20px;
}

.canvas-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-right: 10px;
  margin-bottom: 10px;
}

.pattern-container {
  padding: 10px;
}

.pattern-panel {
  display: flex;
  border: 1px solid #c1c1c1;
  padding: 10px;
}
.pattern-panel .pattern-canvas-wrapper {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-right: 20px;
}
.pattern-panel .pattern-panel-control-wrapper {
  flex-grow: 1;
}
.pattern-panel .pattern-panel-add-control {
  margin-right: 10px;
}

.pattern-control {
  border: 1px solid #c1c1c1;
  margin-top: 10px;
}
.pattern-control .pattern-control-summary {
  display: flex;
  align-items: center;
  height: 40px;
  border-bottom: 1px solid #c1c1c1;
}
.pattern-control .pattern-control-summary .pattern-control-summary-title {
  height: 100%;
  box-sizing: border-box;
  padding: 10px;
  border-right: 1px solid #c1c1c1;
  flex-shrink: 0;
}
.pattern-control .pattern-control-summary .pattern-control-buttons {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  height: 100%;
  border-left: 1px solid #c1c1c1;
}
.pattern-control .pattern-control-summary .pattern-control-buttons button {
  margin: 5px;
}
.pattern-control .pattern-control-summary .pattern-control-items {
  display: flex;
  justify-content: space-around;
  flex-grow: 1;
  cursor: pointer;
}
.pattern-control .pattern-control-summary .pattern-control-item {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.pattern-control .pattern-control-summary .pattern-control-item strong {
  font-weight: bold;
}
.pattern-control .pattern-control-row {
  display: flex;
  align-items: center;
  justify-content: space-around;
  margin: 5px;
}
.pattern-control .pattern-control-row span {
  width: 25%;
  text-align: right;
}
.pattern-control .pattern-control-row input {
  flex-grow: 1;
}
&#13;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.0/umd/react-dom.development.js"></script>
<script src="https://cdn.jsdelivr.net/npm/immer/dist/immer.umd.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.js"></script>

<div id="app"></app>
&#13;
&#13;
&#13;