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