Canvas pattern offset

前端 未结 3 442
执念已碎
执念已碎 2020-12-20 16:03

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 create

3条回答
  •  轮回少年
    2020-12-20 16:53

    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 (
          

    ) } } // CanvasStageContainer component const CanvasStageContainer = (props) => { let stages = props.stages.map((stage, i) => ( props.focusCanvasStage(i)}> )) return (
    {stages}
    ) } // 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 (
    {this.props.patternName}
    ) } } const PatternContainer = (props) => { return (
    ) } 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) => ( 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)}> )) return (
    {patternName}
      {patternControls}
    ) } } // Pattern control component const PatternControl = (props) => { return (
    {props.type}
    size {props.patternSize} px
    density {props.density} px
    thickness {props.thickness} px
    rotation {props.rotation} degree
    X offset {props.offsetX} px
    Y offset {props.offsetY} px
    foreground color {props.foregroundColor}
    background color {props.backgroundColor}
    {props.showDetails ? (
    size ({props.patternSize} px) props.setPatternSize(parseInt(evt.target.value))}>
    density ({props.density} px) props.setPatternConfig({ density: parseInt(evt.target.value) })}>
    thickness ({props.thickness} px) props.setPatternConfig({ thickness: parseInt(evt.target.value) })}>
    rotation ({props.rotation} px) props.setPatternConfig({ rotation: parseInt(evt.target.value) })}>
    X offset ({props.offsetX} px) props.setPatternConfig({ offsetX: parseInt(evt.target.value) })}>
    Y offset ({props.offsetY} px) props.setPatternConfig({ offsetY: parseInt(evt.target.value) })}>
    foreground color RED ({hexToRgb(props.foregroundColor)[0]}) { props.setPatternForegroundColor( rgbToHex([parseInt(evt.target.value)].concat(hexToRgb(props.foregroundColor).slice(1)))) }}>
    foreground color GREEN ({hexToRgb(props.foregroundColor)[1]}) { let rgb = hexToRgb(props.foregroundColor) props.setPatternForegroundColor( rgbToHex([rgb[0], parseInt(evt.target.value), rgb[2]])) }}>
    foreground color BLUE ({hexToRgb(props.foregroundColor)[2]}) { props.setPatternForegroundColor( rgbToHex(hexToRgb(props.foregroundColor).slice(0, 2).concat([parseInt(evt.target.value)]))) }}>
    ) : null}
    ) } /* * Render the above component into the div#app */ ReactDOM.render(, 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;
    }
    
    
    
    
    
    

提交回复
热议问题