10

I'm trying to modify the origin of canvas pattern but can't achieve quite what I want.

I need to draw a line filled with a dotted pattern. Dotted pattern is created via createPattern (feeding it dynamically created canvas element).

The canvas (essentially a red dot) is created like so:

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;
}

Then a line is drawn using that pattern (canvas):

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();

So we get this:

enter image description here

Now, I'd like to offset the origin of those dots, say, by 10px. Translating pattern canvas doesn't help as then we don't get full dots:

enter image description here

And translating context of canvas itself doesn't help as that offsets the line, not the pattern itself:

enter image description here

Translating context doesn't seem to affect pattern origin.

Is there a way to modify offset of pattern itself?

kangax
  • 38,898
  • 13
  • 99
  • 135

3 Answers3

13

Update Since this answer was posted there is now (since 2015/02) a local setTransform() on the CanvasPattern instance itself (see specs). It may not be available in all browsers yet (only Firefox supports it when this was written).

Method 1

You could offset the main canvas and add a delta value to the actual position of the line:

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

Example

(the demo only shows the pattern being translated, but of course, normally you would move the line together with it).

Line

etc. this way you cancel the translation for the line itself. But it introduces some overhead as the coordinates needs to be calculated each time unless you can cache the resulting value.

Method 2

The other way I can think of is to sort of create a pattern of the pattern it self. I.e. for the pattern canvas repeat the dot so that when you move it outside its boundary it is repeated in the opposite direction.

For example here the first square is the normal pattern, the second is the offset pattern described as method two and the third image uses the offset pattern for fill showing it will work.

The key is that the two patterns are of the same size and that the first pattern is repeated offset into this second version. The second version can then be used as fill on the main.

Example 2 (links broken)

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);
Community
  • 1
  • 1
  • 1
    Thanks Ken, both sound great. I think I'll experiment a bit more with 1st version. – kangax Dec 05 '13 at 13:44
  • 1
    I'm very sorry, I accidentally downvoted instead of upvoting and since more than 5 hours have passed the vote is locked. This answer was actually useful to me. – Fabio Iotti Sep 23 '17 at 10:15
  • 1
    @bruce965 lol, no worries, it happens. I edited to unlock the vote in case you want to change it. In any case I'm glad it was useful :) –  Sep 24 '17 at 03:04
10

You can simply translate the context after drawing the line/shape & before stroking/filling to offset the pattern. Updated fiddle http://jsfiddle.net/28BSH/27/

ctx.fillStyle = somePattern;
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(180, 180);
ctx.save();
ctx.translate(offset, offset);
ctx.stroke();
ctx.restore();
Shadaez
  • 412
  • 3
  • 13
0

I happen to create this little tool on codepen that maybe helpful to create fill patterns. You can apply multiple layers of pattern, create lines, circles and squares, or apply colors.

// 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'));
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;
}
<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>
Xin Chen
  • 263
  • 2
  • 11