Skip to content

Support Path2D API #1116

@akira-cn

Description

@akira-cn

https://developer.mozilla.org/en-US/docs/Web/API/Path2D

The Path2D interface of the Canvas 2D API is used to declare paths that are then later used on CanvasRenderingContext2D objects. The path methods of the CanvasRenderingContext2D interface are present on this interface as well and are allowing you to create paths that you can retain and replay as required on a canvas.


This is my polyfill:

const {CanvasRenderingContext2D, createCanvas, Image} = require('canvas')
const {parseSVG, makeAbsolute} = require('svg-path-parser')

const MAX_SIZE = 2048

function createPathSVG(d, lineWidth, lineCap, lineJoin, strokeColor, fillColor) {
  const tpl = `
    <svg width="${MAX_SIZE}" height="${MAX_SIZE}" xmlns="http://www.w3.org/2000/svg">
      <path d="${d}" 
        stroke="${strokeColor || 'black'}" 
        fill="${fillColor || 'transparent'}"
        stroke-width="${lineWidth || 1}"
        stroke-linecap="${lineCap || 'butt'}"
        stroke-linejoin="${lineJoin || 'miter'}"
      ></path>
    </svg>
  `

  const img = new Image()
  img.src = Buffer.from(tpl, 'utf8')

  return img
}

class Path2D {
  constructor(d) {
    this.footprint = []
    this.commands = []
    if(d instanceof Path2D) {
      this.addPath(d)
    } else if(typeof d === 'string') {
      // svg path
      const commands = makeAbsolute(parseSVG(d))
      if(commands[0] && commands[0].code === 'M') {
        this.footprint.push([commands[0].x, commands[0].y])
      }
      this.commands.push(['path', d])
    }
  }
  addPath(path) {
    this.footprint.push(...path.footprint)
    this.commands.push(...path.commands)
  }
  closePath() {
    const point = this.footprint[0]
    if(point) {
      this.moveTo(...point)
    }
  }
  moveTo(x, y) {
    this.footprint.push([x, y])
    this.commands.push(['moveTo', x, y])
  }
  lineTo(x, y) {
    this.footprint.push([x, y])
    this.commands.push(['lineTo', x, y])
  }
  bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
    this.footprint.push([x, y])
    this.commands.push(['bezierCurveTo', cp1x, cp1y, cp2x, cp2y, x, y])
  }
  quadraticCurveTo(cpx, cpy, x, y) {
    this.footprint.push([x, y])
    this.commands.push('quadraticCurveTo', cpx, cpy, x, y)
  }
  arc(x, y, ...rest) {
    this.footprint.push([x, y])
    this.commands.push('arc', x, y, ...rest)
  }
  arcTo(x1, y1, x2, y2, radius) {
    this.commands.push('artTo', x1, y1, x2, y2, radius)
  }
  ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise) {
    this.footprint.push([x, y])
    this.commands.push('ellipse', x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)
  }
  rect(x, y, width, height) {
    this.footprint.push([x, y])
    this.commands.push('rect', x, y, width, height)
  }
  draw(context, type = 'stroke') {
    const {lineWidth, lineCap, lineJoin, strokeStyle, fillStyle} = context
    context.save()
    context.beginPath()
    this.commands.forEach((command) => {
      const [cmd, ...args] = command
      if(cmd === 'path') {
        const svg = createPathSVG(...args, lineWidth, lineCap, lineJoin, strokeStyle, type === 'stroke' ? null : fillStyle)
        context.drawImage(svg, 0, 0)
      } else {
        context[cmd](...args)
        context[type]()
      }
    })
    context.restore()
  }
}

const _stroke = CanvasRenderingContext2D.prototype.stroke
Object.defineProperty(CanvasRenderingContext2D.prototype, 'stroke', {
  value(p) {
    if(p instanceof Path2D) {
      return p.draw(this, 'stroke')
    }
    return _stroke.call(this)
  },
})

const _fill = CanvasRenderingContext2D.prototype.fill
Object.defineProperty(CanvasRenderingContext2D.prototype, 'fill', {
  value(p) {
    if(p instanceof Path2D) {
      return p.draw(this, 'fill')
    }
    return _fill.call(this)
  },
})

const p = new Path2D('M10,80 q100,120 120,20 q140,-50 160,0')
const canvas = createCanvas(800, 600)
const context = canvas.getContext('2d')
context.strokeStyle = 'red'
context.stroke(p)

require('fs').writeFileSync('out.png', canvas.toBuffer())

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions