Description
When applying offsetStroke
to Paths consisting of only straight lines it will occasionally produce end caps on the outlined shape that are "inverted". In the example below, the black line is the input Path and the red fill is the outlined shape produced from the parameters PaperOffset.offsetStroke(st1, 5, { cap: 'round' })
...
The root cause of the issue appears to be the reliance on the clockwise
attribute of the input Path, which in turn relies on the area to determine the direction of the Path. The area of Path containing only a straight line should, by definition, always be zero, but floating point math makes this fact unreliable. In turn, this makes this clockwise
attribute unreliable and calling reverse()
on this Path will likely produce a new Path with the same direction as the original. The end result is a Path that is counter-clockwise and un-reverse()
able, which seems to mess with some of the logic in paperjs-offset that assumes calling reverse()
on a counter-clockwise Path will always produce a clockwise Path. The Path in the example above has an area of -1.7763568394002505e-14
.
My (incomplete) workaround is to outline straight lines manually with a much more simplistic algorithm that does not rely on on the path direction. Also currently assumes round
caps (though could probably be generalized to support other caps) and does not need to support joins because the lines are assumed to be straight...
function simpleOutlineStroke(path, offset) {
const normal = path.getNormalAt(0)
const p0 = path.firstSegment.getPoint().add(normal.multiply(offset))
const p1 = path.lastSegment.getPoint().add(normal.multiply(offset))
const p2 = path.lastSegment.getPoint().add(normal.multiply(-offset))
const p3 = path.firstSegment.getPoint().add(normal.multiply(-offset))
// draw outlined path
const offsetPath = new paper.Path({ insert: false })
offsetPath.moveTo(p0)
offsetPath.lineTo(p1)
offsetPath.arcTo(p2, true)
offsetPath.lineTo(p3)
offsetPath.arcTo(p0, true)
offsetPath.closePath()
// copy path styles
offsetPath.strokeWidth = 0;
offsetPath.fillColor = path.strokeColor;
offsetPath.shadowBlur = path.shadowBlur;
offsetPath.shadowColor = path.shadowColor;
offsetPath.shadowOffset = path.shadowOffset;
return offsetPath
}
Example used to produce the screenshot...
let canvas = document.querySelector('canvas')
paper.setup(canvas)
paper.view.center = [-0.7260725539107398, -1.2302079087572793]
let angle = Math.atan(0.9266588281199828)
let x = Math.cos(angle) * 100
let y = Math.sin(angle) * 100
let xOffset = 0.7260725539107398 - x / 2
let yOffset = 1.2302079087572793 - y / 2
let st1 = new paper.Path.Line({ from: [xOffset, yOffset], to: [x + xOffset, y + yOffset], strokeColor: 'rgba(255, 0, 0, 0.5)', strokeWidth: 1 })
let st2 = new paper.Path.Line({ from: [xOffset, yOffset], to: [x + xOffset, y + yOffset], strokeColor: '#000', strokeWidth: 1 })
PaperOffset.offsetStroke(st1, 5, { cap: 'round' })
st1.bringToFront()