Canvas pattern offset

前端 未结 3 441
执念已碎
执念已碎 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:45

    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);
    
    0 讨论(0)
  • 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 (
          <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>

    0 讨论(0)
  • 2020-12-20 17:01

    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();
    
    0 讨论(0)
提交回复
热议问题