Re-stroking paths creates unexpected fills
FossPrime opened this issue · comments
Expected Behaviour
In this code:
const ctx = canvas.getContext('2d');
ctx.beginPath() // Only needed in pureimage :/
// First sub-path
ctx.lineWidth = 26;
ctx.strokeStyle = 'rgba(255,165,0,0.5)'
ctx.moveTo(20, 20);
ctx.lineTo(160, 20);
ctx.stroke();
// Second sub-path
ctx.lineWidth = 14;
ctx.strokeStyle = 'rgba(0,255,0,0.5)'
ctx.moveTo(20, 80);
ctx.lineTo(220, 80);
ctx.stroke();
// Third sub-path
ctx.lineWidth = 4;
ctx.strokeStyle = 'rgba(255,192,203,0.5)'
ctx.moveTo(20, 140);
ctx.lineTo(280, 140);
ctx.stroke();
- Lines should be parallel
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/stroke#re-stroking_paths
Actual Behaviour
- lines are mostly parallel, with lots of diagonal fills
Steps To Reproduce
- Minimal: https://stackblitz.com/edit/koa-starter-yrc77r?file=index.js
- Workaround https://stackblitz.com/edit/koa-starter-zvulkw?file=index.js
- Where I had to workaround this bug: https://stackblitz.com/edit/feathersjs-7kqlyt?file=server.js
Any Relevant Code Snippets
Full koa server for testing:
import Koa from 'koa'
import { make, encodePNGToStream } from 'pureimage'
import { createWriteStream } from 'fs'
import logger from 'koa-logger'
import Router from '@koa/router'
import serve from 'koa-static'
const filepath = '/tmp/output.png'
function stroker(canvas) {
const ctx = canvas.getContext('2d')
ctx.beginPath()
// First sub-path
ctx.lineWidth = 26
ctx.strokeStyle = 'rgba(255,165,0,0.5)'
ctx.moveTo(20, 20)
ctx.lineTo(160, 20)
ctx.stroke()
// Second sub-path
ctx.lineWidth = 14
ctx.strokeStyle = 'rgba(0,255,0,0.5)'
ctx.moveTo(20, 80)
ctx.lineTo(220, 80)
ctx.stroke()
// Third sub-path
ctx.lineWidth = 4
ctx.strokeStyle = 'rgba(255,192,203,0.5)'
ctx.moveTo(20, 140)
ctx.lineTo(280, 140)
ctx.stroke()
}
const webCanvas = (ctx) => {
ctx.body = `
<h3>Expected Behavior:</h3>
<canvas id="canvas"></canvas>
<h3>Actual Behavior:</h3>
<img src="output.png" />
<script>
${stroker.toString()}
stroker(document.getElementById('canvas'))
</script>
`
}
const app = new Koa()
// middlewares
app.use(logger())
const router = Router()
router.get('/', webCanvas)
app.use(serve('/tmp'))
app.use(router.routes())
// Run canvas code on server
try {
const canvas = make(300, 150)
stroker(canvas)
await encodePNGToStream(canvas, createWriteStream(filepath))
console.log('done writing to ', filepath)
app.listen(3030)
} catch (err) {
// When a request is aborted - err is an AbortError
console.error(err)
}
My current understanding:
- if you preface each set of lines with beginPath() then this won't happen. No re-stroking happens so it looks fine.
- If you don't preface the later sets of lines with beginPath() then the later calls should re-stroke, but without the extra lines.
The beginPath
function resets the path object, to prevent re-stroking. This is the correct behavior. When beginPath is not called the stroke
function will convert the path to flat lines, and then to a new polygon representing the stroked path of the lines. This is where the bug lies. It doesn't work right if there are multiple sub-paths. This is the function path_to_stroked_path
in context.js
.
This would happen even without the missing beginPath. You can simply try to draw two lines as separate sub-paths and this will happen. Stroking doesn't work with multiple subpaths. That's the root bug.
The fix is to divide the path into distinct sub-paths, then handle them separately, then combine the results.
I just pushed back a fix. Can you see if the latest on MAIN fixes it? If so I'll push out a release.
That's a whole lot better! I think I have to change the blendmode on the browser version to screen and it should look exactly like what pure image produces.
https://stackblitz.com/edit/koa-starter-firdac?file=package.json,index.js
I've pushed the changes back and released a patch update on NPM. I'm going to mark this bug fixed. The remaining issues you see I think are due to color blending bugs (perhaps linear vs non-linear?).