Skip to content

Inverted end caps when using offsetStroke with (some) straight lines #10

Open
@trun

Description

@trun

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' })...

Image 2021-09-27 at 5 10 59 PM

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions