function CanvasRenderingContext2D(skcanvas) {
  this._canvas = skcanvas;
  this._paint = new CanvasKit.Paint();
  this._paint.setAntiAlias(true);

  this._paint.setStrokeMiter(10);
  this._paint.setStrokeCap(CanvasKit.StrokeCap.Butt);
  this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Miter);
  this._fontString = '10px monospace';

  this._font = new CanvasKit.Font(CanvasKit.Typeface.GetDefault(), 10);
  this._font.setSubpixel(true);

  this._strokeStyle    = CanvasKit.BLACK;
  this._fillStyle      = CanvasKit.BLACK;
  this._shadowBlur     = 0;
  this._shadowColor    = CanvasKit.TRANSPARENT;
  this._shadowOffsetX  = 0;
  this._shadowOffsetY  = 0;
  this._globalAlpha    = 1;
  this._strokeWidth    = 1;
  this._lineDashOffset = 0;
  this._lineDashList   = [];
  // aka BlendMode
  this._globalCompositeOperation = CanvasKit.BlendMode.SrcOver;

  this._paint.setStrokeWidth(this._strokeWidth);
  this._paint.setBlendMode(this._globalCompositeOperation);

  this._currentPath = new CanvasKit.Path();
  this._currentTransform = CanvasKit.Matrix.identity();

  // Use this for save/restore
  this._canvasStateStack = [];
  // Keep a reference to all the effects (e.g. gradients, patterns)
  // that were allocated for cleanup in _dispose.
  this._toCleanUp = [];

  this._dispose = function() {
    this._currentPath.delete();
    this._paint.delete();
    this._font.delete();
    this._toCleanUp.forEach(function(c) {
      c._dispose();
    });
    // Don't delete this._canvas as it will be disposed
    // by the surface of which it is based.
  };

  // This always accepts DOMMatrix/SVGMatrix or any other
  // object that has properties a,b,c,d,e,f defined.
  // Returns a DOM-Matrix like dictionary
  Object.defineProperty(this, 'currentTransform', {
    enumerable: true,
    get: function() {
      return {
        'a' : this._currentTransform[0],
        'c' : this._currentTransform[1],
        'e' : this._currentTransform[2],
        'b' : this._currentTransform[3],
        'd' : this._currentTransform[4],
        'f' : this._currentTransform[5],
      };
    },
    // @param {DOMMatrix} matrix
    set: function(matrix) {
      if (matrix.a) {
        // if we see a property named 'a', guess that b-f will
        // also be there.
        this.setTransform(matrix.a, matrix.b, matrix.c,
                          matrix.d, matrix.e, matrix.f);
      }
    }
  });

  Object.defineProperty(this, 'fillStyle', {
    enumerable: true,
    get: function() {
      if (isCanvasKitColor(this._fillStyle)) {
        return colorToString(this._fillStyle);
      }
      return this._fillStyle;
    },
    set: function(newStyle) {
      if (typeof newStyle === 'string') {
        this._fillStyle = parseColor(newStyle);
      } else if (newStyle._getShader) {
        // It's an effect that has a shader.
        this._fillStyle = newStyle
      }
    }
  });

  Object.defineProperty(this, 'font', {
    enumerable: true,
    get: function() {
      return this._fontString;
    },
    set: function(newFont) {
      var tf = getTypeface(newFont);
      if (tf) {
        // tf is a "dict" according to closure, that is, the field
        // names are not minified. Thus, we need to access it via
        // bracket notation to tell closure not to minify these names.
        this._font.setSize(tf['sizePx']);
        this._font.setTypeface(tf['typeface']);
        this._fontString = newFont;
      }
    }
  });

  Object.defineProperty(this, 'globalAlpha', {
    enumerable: true,
    get: function() {
      return this._globalAlpha;
    },
    set: function(newAlpha) {
      // ignore invalid values, as per the spec
      if (!isFinite(newAlpha) || newAlpha < 0 || newAlpha > 1) {
        return;
      }
      this._globalAlpha = newAlpha;
    }
  });

  Object.defineProperty(this, 'globalCompositeOperation', {
    enumerable: true,
    get: function() {
      switch (this._globalCompositeOperation) {
        // composite-mode
        case CanvasKit.BlendMode.SrcOver:
          return 'source-over';
        case CanvasKit.BlendMode.DstOver:
          return 'destination-over';
        case CanvasKit.BlendMode.Src:
          return 'copy';
        case CanvasKit.BlendMode.Dst:
          return 'destination';
        case CanvasKit.BlendMode.Clear:
          return 'clear';
        case CanvasKit.BlendMode.SrcIn:
          return 'source-in';
        case CanvasKit.BlendMode.DstIn:
          return 'destination-in';
        case CanvasKit.BlendMode.SrcOut:
          return 'source-out';
        case CanvasKit.BlendMode.DstOut:
          return 'destination-out';
        case CanvasKit.BlendMode.SrcATop:
          return 'source-atop';
        case CanvasKit.BlendMode.DstATop:
          return 'destination-atop';
        case CanvasKit.BlendMode.Xor:
          return 'xor';
        case CanvasKit.BlendMode.Plus:
          return 'lighter';

        case CanvasKit.BlendMode.Multiply:
          return 'multiply';
        case CanvasKit.BlendMode.Screen:
          return 'screen';
        case CanvasKit.BlendMode.Overlay:
          return 'overlay';
        case CanvasKit.BlendMode.Darken:
          return 'darken';
        case CanvasKit.BlendMode.Lighten:
          return 'lighten';
        case CanvasKit.BlendMode.ColorDodge:
          return 'color-dodge';
        case CanvasKit.BlendMode.ColorBurn:
          return 'color-burn';
        case CanvasKit.BlendMode.HardLight:
          return 'hard-light';
        case CanvasKit.BlendMode.SoftLight:
          return 'soft-light';
        case CanvasKit.BlendMode.Difference:
          return 'difference';
        case CanvasKit.BlendMode.Exclusion:
          return 'exclusion';
        case CanvasKit.BlendMode.Hue:
          return 'hue';
        case CanvasKit.BlendMode.Saturation:
          return 'saturation';
        case CanvasKit.BlendMode.Color:
          return 'color';
        case CanvasKit.BlendMode.Luminosity:
          return 'luminosity';
      }
    },
    set: function(newMode) {
      switch (newMode) {
        // composite-mode
        case 'source-over':
          this._globalCompositeOperation = CanvasKit.BlendMode.SrcOver;
          break;
        case 'destination-over':
          this._globalCompositeOperation = CanvasKit.BlendMode.DstOver;
          break;
        case 'copy':
          this._globalCompositeOperation = CanvasKit.BlendMode.Src;
          break;
        case 'destination':
          this._globalCompositeOperation = CanvasKit.BlendMode.Dst;
          break;
        case 'clear':
          this._globalCompositeOperation = CanvasKit.BlendMode.Clear;
          break;
        case 'source-in':
          this._globalCompositeOperation = CanvasKit.BlendMode.SrcIn;
          break;
        case 'destination-in':
          this._globalCompositeOperation = CanvasKit.BlendMode.DstIn;
          break;
        case 'source-out':
          this._globalCompositeOperation = CanvasKit.BlendMode.SrcOut;
          break;
        case 'destination-out':
          this._globalCompositeOperation = CanvasKit.BlendMode.DstOut;
          break;
        case 'source-atop':
          this._globalCompositeOperation = CanvasKit.BlendMode.SrcATop;
          break;
        case 'destination-atop':
          this._globalCompositeOperation = CanvasKit.BlendMode.DstATop;
          break;
        case 'xor':
          this._globalCompositeOperation = CanvasKit.BlendMode.Xor;
          break;
        case 'lighter':
          this._globalCompositeOperation = CanvasKit.BlendMode.Plus;
          break;
        case 'plus-lighter':
          this._globalCompositeOperation = CanvasKit.BlendMode.Plus;
          break;
        case 'plus-darker':
          throw 'plus-darker is not supported';

        // blend-mode
        case 'multiply':
          this._globalCompositeOperation = CanvasKit.BlendMode.Multiply;
          break;
        case 'screen':
          this._globalCompositeOperation = CanvasKit.BlendMode.Screen;
          break;
        case 'overlay':
          this._globalCompositeOperation = CanvasKit.BlendMode.Overlay;
          break;
        case 'darken':
          this._globalCompositeOperation = CanvasKit.BlendMode.Darken;
          break;
        case 'lighten':
          this._globalCompositeOperation = CanvasKit.BlendMode.Lighten;
          break;
        case 'color-dodge':
          this._globalCompositeOperation = CanvasKit.BlendMode.ColorDodge;
          break;
        case 'color-burn':
          this._globalCompositeOperation = CanvasKit.BlendMode.ColorBurn;
          break;
        case 'hard-light':
          this._globalCompositeOperation = CanvasKit.BlendMode.HardLight;
          break;
        case 'soft-light':
          this._globalCompositeOperation = CanvasKit.BlendMode.SoftLight;
          break;
        case 'difference':
          this._globalCompositeOperation = CanvasKit.BlendMode.Difference;
          break;
        case 'exclusion':
          this._globalCompositeOperation = CanvasKit.BlendMode.Exclusion;
          break;
        case 'hue':
          this._globalCompositeOperation = CanvasKit.BlendMode.Hue;
          break;
        case 'saturation':
          this._globalCompositeOperation = CanvasKit.BlendMode.Saturation;
          break;
        case 'color':
          this._globalCompositeOperation = CanvasKit.BlendMode.Color;
          break;
        case 'luminosity':
          this._globalCompositeOperation = CanvasKit.BlendMode.Luminosity;
          break;
        default:
          return;
      }
      this._paint.setBlendMode(this._globalCompositeOperation);
    }
  });

  Object.defineProperty(this, 'imageSmoothingEnabled', {
    enumerable: true,
    get: function() {
      return true;
    },
    set: function(a) {
      // ignored, we always use high quality image smoothing.
    }
  });

  Object.defineProperty(this, 'imageSmoothingQuality', {
    enumerable: true,
    get: function() {
          return 'high';
    },
    set: function(a) {
      // ignored, we always use high quality image smoothing.
    }
  });

  Object.defineProperty(this, 'lineCap', {
    enumerable: true,
    get: function() {
      switch (this._paint.getStrokeCap()) {
        case CanvasKit.StrokeCap.Butt:
          return 'butt';
        case CanvasKit.StrokeCap.Round:
          return 'round';
        case CanvasKit.StrokeCap.Square:
          return 'square';
      }
    },
    set: function(newCap) {
      switch (newCap) {
        case 'butt':
          this._paint.setStrokeCap(CanvasKit.StrokeCap.Butt);
          return;
        case 'round':
          this._paint.setStrokeCap(CanvasKit.StrokeCap.Round);
          return;
        case 'square':
          this._paint.setStrokeCap(CanvasKit.StrokeCap.Square);
          return;
      }
    }
  });

  Object.defineProperty(this, 'lineDashOffset', {
    enumerable: true,
    get: function() {
      return this._lineDashOffset;
    },
    set: function(newOffset) {
      if (!isFinite(newOffset)) {
        return;
      }
      this._lineDashOffset = newOffset;
    }
  });

  Object.defineProperty(this, 'lineJoin', {
    enumerable: true,
    get: function() {
      switch (this._paint.getStrokeJoin()) {
        case CanvasKit.StrokeJoin.Miter:
          return 'miter';
        case CanvasKit.StrokeJoin.Round:
          return 'round';
        case CanvasKit.StrokeJoin.Bevel:
          return 'bevel';
      }
    },
    set: function(newJoin) {
      switch (newJoin) {
        case 'miter':
          this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Miter);
          return;
        case 'round':
          this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Round);
          return;
        case 'bevel':
          this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Bevel);
          return;
      }
    }
  });

  Object.defineProperty(this, 'lineWidth', {
    enumerable: true,
    get: function() {
      return this._paint.getStrokeWidth();
    },
    set: function(newWidth) {
      if (newWidth <= 0 || !newWidth) {
        // Spec says to ignore NaN/Inf/0/negative values
        return;
      }
      this._strokeWidth = newWidth;
      this._paint.setStrokeWidth(newWidth);
    }
  });

  Object.defineProperty(this, 'miterLimit', {
    enumerable: true,
    get: function() {
      return this._paint.getStrokeMiter();
    },
    set: function(newLimit) {
      if (newLimit <= 0 || !newLimit) {
        // Spec says to ignore NaN/Inf/0/negative values
        return;
      }
      this._paint.setStrokeMiter(newLimit);
    }
  });

  Object.defineProperty(this, 'shadowBlur', {
    enumerable: true,
    get: function() {
      return this._shadowBlur;
    },
    set: function(newBlur) {
      // ignore negative, inf and NAN (but not 0) as per the spec.
      if (newBlur < 0 || !isFinite(newBlur)) {
        return;
      }
      this._shadowBlur = newBlur;
    }
  });

  Object.defineProperty(this, 'shadowColor', {
    enumerable: true,
    get: function() {
      return colorToString(this._shadowColor);
    },
    set: function(newColor) {
      this._shadowColor = parseColor(newColor);
    }
  });

  Object.defineProperty(this, 'shadowOffsetX', {
    enumerable: true,
    get: function() {
      return this._shadowOffsetX;
    },
    set: function(newOffset) {
      if (!isFinite(newOffset)) {
        return;
      }
      this._shadowOffsetX = newOffset;
    }
  });

  Object.defineProperty(this, 'shadowOffsetY', {
    enumerable: true,
    get: function() {
      return this._shadowOffsetY;
    },
    set: function(newOffset) {
      if (!isFinite(newOffset)) {
        return;
      }
      this._shadowOffsetY = newOffset;
    }
  });

  Object.defineProperty(this, 'strokeStyle', {
    enumerable: true,
    get: function() {
      return colorToString(this._strokeStyle);
    },
    set: function(newStyle) {
      if (typeof newStyle === 'string') {
        this._strokeStyle = parseColor(newStyle);
      } else if (newStyle._getShader) {
        // It's probably an effect.
        this._strokeStyle = newStyle
      }
    }
  });

  this.arc = function(x, y, radius, startAngle, endAngle, ccw) {
    arc(this._currentPath, x, y, radius, startAngle, endAngle, ccw);
  };

  this.arcTo = function(x1, y1, x2, y2, radius) {
    arcTo(this._currentPath, x1, y1, x2, y2, radius);
  };

  // As per the spec this doesn't begin any paths, it only
  // clears out any previous paths.
  this.beginPath = function() {
    this._currentPath.delete();
    this._currentPath = new CanvasKit.Path();
  };

  this.bezierCurveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {
    bezierCurveTo(this._currentPath, cp1x, cp1y, cp2x, cp2y, x, y);
  };

  this.clearRect = function(x, y, width, height) {
    this._paint.setStyle(CanvasKit.PaintStyle.Fill);
    this._paint.setBlendMode(CanvasKit.BlendMode.Clear);
    this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), this._paint);
    this._paint.setBlendMode(this._globalCompositeOperation);
  };

  this.clip = function(path, fillRule) {
    if (typeof path === 'string') {
      // shift the args if a Path2D is supplied
      fillRule = path;
      path = this._currentPath;
    } else if (path && path._getPath) {
      path = path._getPath();
    }
    if (!path) {
      path = this._currentPath;
    }

    var clip = path.copy();
    if (fillRule && fillRule.toLowerCase() === 'evenodd') {
      clip.setFillType(CanvasKit.FillType.EvenOdd);
    } else {
      clip.setFillType(CanvasKit.FillType.Winding);
    }
    this._canvas.clipPath(clip, CanvasKit.ClipOp.Intersect, true);
    clip.delete();
  };

  this.closePath = function() {
    closePath(this._currentPath);
  };

  this.createImageData = function() {
    // either takes in 1 or 2 arguments:
    //  - imagedata on which to copy *width* and *height* only
    //  - width, height
    if (arguments.length === 1) {
      var oldData = arguments[0];
      var byteLength = 4 * oldData.width * oldData.height;
      return new ImageData(new Uint8ClampedArray(byteLength),
                           oldData.width, oldData.height);
    } else if (arguments.length === 2) {
      var width = arguments[0];
      var height = arguments[1];
      var byteLength = 4 * width * height;
      return new ImageData(new Uint8ClampedArray(byteLength),
                           width, height);
    } else {
      throw 'createImageData expects 1 or 2 arguments, got '+arguments.length;
    }
  };

  this.createLinearGradient = function(x1, y1, x2, y2) {
    if (!allAreFinite(arguments)) {
      return;
    }
    var lcg = new LinearCanvasGradient(x1, y1, x2, y2);
    this._toCleanUp.push(lcg);
    return lcg;
  };

  this.createPattern = function(image, repetition) {
    var cp = new CanvasPattern(image, repetition);
    this._toCleanUp.push(cp);
    return cp;
  };

  this.createRadialGradient = function(x1, y1, r1, x2, y2, r2) {
    if (!allAreFinite(arguments)) {
      return;
    }
    var rcg = new RadialCanvasGradient(x1, y1, r1, x2, y2, r2);
    this._toCleanUp.push(rcg);
    return rcg;
  };

  this.drawImage = function(img) {
    // 3 potential sets of arguments
    // - image, dx, dy
    // - image, dx, dy, dWidth, dHeight
    // - image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight
    // use the fillPaint, which has the globalAlpha in it
    // which drawImageRect will use.
    if (img instanceof HTMLImage) {
      img = img.getSkImage();
    }
    var iPaint = this._fillPaint();
    if (arguments.length === 3 || arguments.length === 5) {
      var destRect = CanvasKit.XYWHRect(arguments[1], arguments[2],
                        arguments[3] || img.width(), arguments[4] || img.height());
      var srcRect = CanvasKit.XYWHRect(0, 0, img.width(), img.height());
    } else if (arguments.length === 9){
      var destRect = CanvasKit.XYWHRect(arguments[5], arguments[6],
                                        arguments[7], arguments[8]);
      var srcRect = CanvasKit.XYWHRect(arguments[1], arguments[2],
                                       arguments[3], arguments[4]);
    } else {
      throw 'invalid number of args for drawImage, need 3, 5, or 9; got '+ arguments.length;
    }
    this._canvas.drawImageRect(img, srcRect, destRect, iPaint, false);

    iPaint.dispose();
  };

  this.ellipse = function(x, y, radiusX, radiusY, rotation,
                          startAngle, endAngle, ccw) {
    ellipse(this._currentPath, x, y, radiusX, radiusY, rotation,
            startAngle, endAngle, ccw);
  };

  // A helper to copy the current paint, ready for filling
  // This applies the global alpha.
  // Call dispose() after to clean up.
  this._fillPaint = function() {
    var paint = this._paint.copy();
    paint.setStyle(CanvasKit.PaintStyle.Fill);
    if (isCanvasKitColor(this._fillStyle)) {
      var alphaColor = CanvasKit.multiplyByAlpha(this._fillStyle, this._globalAlpha);
      paint.setColor(alphaColor);
    } else {
      var shader = this._fillStyle._getShader(this._currentTransform);
      paint.setColor(CanvasKit.Color(0,0,0, this._globalAlpha));
      paint.setShader(shader);
    }

    paint.dispose = function() {
      // If there are some helper effects in the future, clean them up
      // here. In any case, we have .dispose() to make _fillPaint behave
      // like _strokePaint and _shadowPaint.
      this.delete();
    };
    return paint;
  };

  this.fill = function(path, fillRule) {
    if (typeof path === 'string') {
      // shift the args if a Path2D is supplied
      fillRule = path;
      path = this._currentPath;
    } else if (path && path._getPath) {
      path = path._getPath();
    }
    if (fillRule === 'evenodd') {
      this._currentPath.setFillType(CanvasKit.FillType.EvenOdd);
    } else if (fillRule === 'nonzero' || !fillRule) {
      this._currentPath.setFillType(CanvasKit.FillType.Winding);
    } else {
      throw 'invalid fill rule';
    }
    if (!path) {
      path = this._currentPath;
    }

    var fillPaint = this._fillPaint();

    var shadowPaint = this._shadowPaint(fillPaint);
    if (shadowPaint) {
      this._canvas.save();
      this._applyShadowOffsetMatrix();
      this._canvas.drawPath(path, shadowPaint);
      this._canvas.restore();
      shadowPaint.dispose();
    }
    this._canvas.drawPath(path, fillPaint);
    fillPaint.dispose();
  };

  this.fillRect = function(x, y, width, height) {
    var fillPaint = this._fillPaint();

    var shadowPaint = this._shadowPaint(fillPaint);
    if (shadowPaint) {
      this._canvas.save();
      this._applyShadowOffsetMatrix();
      this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), shadowPaint);
      this._canvas.restore();
      shadowPaint.dispose();
    }

    this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), fillPaint);
    fillPaint.dispose();
  };

  this.fillText = function(text, x, y, maxWidth) {
    // TODO do something with maxWidth, probably involving measure
    var fillPaint = this._fillPaint();
    var blob = CanvasKit.TextBlob.MakeFromText(text, this._font);

    var shadowPaint = this._shadowPaint(fillPaint);
    if (shadowPaint) {
      this._canvas.save();
      this._applyShadowOffsetMatrix();
      this._canvas.drawTextBlob(blob, x, y, shadowPaint);
      this._canvas.restore();
      shadowPaint.dispose();
    }
    this._canvas.drawTextBlob(blob, x, y, fillPaint);
    blob.delete();
    fillPaint.dispose();
  };

  this.getImageData = function(x, y, w, h) {
    var pixels = this._canvas.readPixels(x, y, {
        'width': w,
        'height': h,
        'colorType': CanvasKit.ColorType.RGBA_8888,
        'alphaType': CanvasKit.AlphaType.Unpremul,
        'colorSpace': CanvasKit.ColorSpace.SRGB,
    });
    if (!pixels) {
      return null;
    }
    // This essentially re-wraps the pixels from a Uint8Array to
    // a Uint8ClampedArray (without making a copy of pixels).
    return new ImageData(
      new Uint8ClampedArray(pixels.buffer),
      w, h);
  };

  this.getLineDash = function() {
    return this._lineDashList.slice();
  };

  this._mapToLocalCoordinates = function(pts) {
    var inverted = CanvasKit.Matrix.invert(this._currentTransform);
    CanvasKit.Matrix.mapPoints(inverted, pts);
    return pts;
  };

  this.isPointInPath = function(x, y, fillmode) {
    var args = arguments;
    if (args.length === 3) {
      var path = this._currentPath;
    } else if (args.length === 4) {
      var path = args[0];
      x = args[1];
      y = args[2];
      fillmode = args[3];
    } else {
      throw 'invalid arg count, need 3 or 4, got ' + args.length;
    }
    if (!isFinite(x) || !isFinite(y)) {
      return false;
    }
    fillmode = fillmode || 'nonzero';
    if (!(fillmode === 'nonzero' || fillmode === 'evenodd')) {
      return false;
    }
    // x and y are in canvas coordinates (i.e. unaffected by CTM)
    var pts = this._mapToLocalCoordinates([x, y]);
    x = pts[0];
    y = pts[1];
    path.setFillType(fillmode === 'nonzero' ?
                                  CanvasKit.FillType.Winding :
                                  CanvasKit.FillType.EvenOdd);
    return path.contains(x, y);
  };

  this.isPointInStroke = function(x, y) {
    var args = arguments;
    if (args.length === 2) {
      var path = this._currentPath;
    } else if (args.length === 3) {
      var path = args[0];
      x = args[1];
      y = args[2];
    } else {
      throw 'invalid arg count, need 2 or 3, got ' + args.length;
    }
    if (!isFinite(x) || !isFinite(y)) {
      return false;
    }
    var pts = this._mapToLocalCoordinates([x, y]);
    x = pts[0];
    y = pts[1];
    var temp = path.copy();
    // fillmode is always nonzero
    temp.setFillType(CanvasKit.FillType.Winding);
    temp.stroke({'width': this.lineWidth, 'miter_limit': this.miterLimit,
                 'cap': this._paint.getStrokeCap(), 'join': this._paint.getStrokeJoin(),
                 'precision': 0.3, // this is what Chrome uses to compute this
                });
    var retVal = temp.contains(x, y);
    temp.delete();
    return retVal;
  };

  this.lineTo = function(x, y) {
    lineTo(this._currentPath, x, y);
  };

  this.measureText = function(text) {
    const ids = this._font.getGlyphIDs(text);
    const widths = this._font.getGlyphWidths(ids);
    let totalWidth = 0;
    for (const w of widths) {
      totalWidth += w;
    }
    return {
      "width": totalWidth,
    };
  };

  this.moveTo = function(x, y) {
    moveTo(this._currentPath, x, y);
  };

  this.putImageData = function(imageData, x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight) {
    if (!allAreFinite([x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight])) {
      return;
    }
    if (dirtyX === undefined) {
      // fast, simple path for basic call
      this._canvas.writePixels(imageData.data, imageData.width, imageData.height, x, y);
      return;
    }
    dirtyX = dirtyX || 0;
    dirtyY = dirtyY || 0;
    dirtyWidth = dirtyWidth || imageData.width;
    dirtyHeight = dirtyHeight || imageData.height;

    // as per https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-putimagedata
    if (dirtyWidth < 0) {
      dirtyX = dirtyX+dirtyWidth;
      dirtyWidth = Math.abs(dirtyWidth);
    }
    if (dirtyHeight < 0) {
      dirtyY = dirtyY+dirtyHeight;
      dirtyHeight = Math.abs(dirtyHeight);
    }
    if (dirtyX < 0) {
      dirtyWidth = dirtyWidth + dirtyX;
      dirtyX = 0;
    }
    if (dirtyY < 0) {
      dirtyHeight = dirtyHeight + dirtyY;
      dirtyY = 0;
    }
    if (dirtyWidth <= 0 || dirtyHeight <= 0) {
      return;
    }
    var img = CanvasKit.MakeImage({
      'width': imageData.width,
      'height': imageData.height,
      'alphaType': CanvasKit.AlphaType.Unpremul,
      'colorType': CanvasKit.ColorType.RGBA_8888,
      'colorSpace': CanvasKit.ColorSpace.SRGB
    }, imageData.data, 4 * imageData.width);
    var src = CanvasKit.XYWHRect(dirtyX, dirtyY, dirtyWidth, dirtyHeight);
    var dst = CanvasKit.XYWHRect(x+dirtyX, y+dirtyY, dirtyWidth, dirtyHeight);
    var inverted = CanvasKit.Matrix.invert(this._currentTransform);
    this._canvas.save();
    // putImageData() operates in device space.
    this._canvas.concat(inverted);
    this._canvas.drawImageRect(img, src, dst, null, false);
    this._canvas.restore();
    img.delete();
  };

  this.quadraticCurveTo = function(cpx, cpy, x, y) {
    quadraticCurveTo(this._currentPath, cpx, cpy, x, y);
  };

  this.rect = function(x, y, width, height) {
    rect(this._currentPath, x, y, width, height);
  };

  this.resetTransform = function() {
    // Apply the current transform to the path and then reset
    // to the identity. Essentially "commit" the transform.
    this._currentPath.transform(this._currentTransform);
    var inverted = CanvasKit.Matrix.invert(this._currentTransform);
    this._canvas.concat(inverted);
    // This should be identity, modulo floating point drift.
    this._currentTransform = this._canvas.getTotalMatrix();
  };

  this.restore = function() {
    var newState = this._canvasStateStack.pop();
    if (!newState) {
      return;
    }
    // "commit" the current transform. We pop, then apply the inverse of the
    // popped state, which has the effect of applying just the delta of
    // transforms between old and new.
    var combined = CanvasKit.Matrix.multiply(
      this._currentTransform,
      CanvasKit.Matrix.invert(newState.ctm)
    );
    this._currentPath.transform(combined);
    this._paint.delete();
    this._paint = newState.paint;

    this._lineDashList = newState.ldl;
    this._strokeWidth = newState.sw;
    this._strokeStyle = newState.ss;
    this._fillStyle = newState.fs;
    this._shadowOffsetX = newState.sox;
    this._shadowOffsetY = newState.soy;
    this._shadowBlur = newState.sb;
    this._shadowColor = newState.shc;
    this._globalAlpha = newState.ga;
    this._globalCompositeOperation = newState.gco;
    this._lineDashOffset = newState.ldo;
    this._fontString = newState.fontstr;

    //TODO: textAlign, textBaseline

    // restores the clip and ctm
    this._canvas.restore();
    this._currentTransform = this._canvas.getTotalMatrix();
  };

  this.rotate = function(radians) {
    if (!isFinite(radians)) {
      return;
    }
    // retroactively apply the inverse of this transform to the previous
    // path so it cancels out when we apply the transform at draw time.
    var inverted = CanvasKit.Matrix.rotated(-radians);
    this._currentPath.transform(inverted);
    this._canvas.rotate(radiansToDegrees(radians), 0, 0);
    this._currentTransform = this._canvas.getTotalMatrix();
  };

  this.save = function() {
    if (this._fillStyle._copy) {
      var fs = this._fillStyle._copy();
      this._toCleanUp.push(fs);
    } else {
      var fs = this._fillStyle;
    }

    if (this._strokeStyle._copy) {
      var ss = this._strokeStyle._copy();
      this._toCleanUp.push(ss);
    } else {
      var ss = this._strokeStyle;
    }

    this._canvasStateStack.push({
      ctm:     this._currentTransform.slice(),
      ldl:     this._lineDashList.slice(),
      sw:      this._strokeWidth,
      ss:      ss,
      fs:      fs,
      sox:     this._shadowOffsetX,
      soy:     this._shadowOffsetY,
      sb:      this._shadowBlur,
      shc:     this._shadowColor,
      ga:      this._globalAlpha,
      ldo:     this._lineDashOffset,
      gco:     this._globalCompositeOperation,
      paint:   this._paint.copy(),
      fontstr: this._fontString,
      //TODO: textAlign, textBaseline
    });
    // Saves the clip
    this._canvas.save();
  };

  this.scale = function(sx, sy) {
    if (!allAreFinite(arguments)) {
      return;
    }
    // retroactively apply the inverse of this transform to the previous
    // path so it cancels out when we apply the transform at draw time.
    var inverted = CanvasKit.Matrix.scaled(1/sx, 1/sy);
    this._currentPath.transform(inverted);
    this._canvas.scale(sx, sy);
    this._currentTransform = this._canvas.getTotalMatrix();
  };

  this.setLineDash = function(dashes) {
    for (var i = 0; i < dashes.length; i++) {
      if (!isFinite(dashes[i]) || dashes[i] < 0) {
        Debug('dash list must have positive, finite values');
        return;
      }
    }
    if (dashes.length % 2 === 1) {
      // as per the spec, concatenate 2 copies of dashes
      // to give it an even number of elements.
      Array.prototype.push.apply(dashes, dashes);
    }
    this._lineDashList = dashes;
  };

  this.setTransform = function(a, b, c, d, e, f) {
    if (!(allAreFinite(arguments))) {
      return;
    }
    this.resetTransform();
    this.transform(a, b, c, d, e, f);
  };

  // We need to apply the shadowOffsets on the device coordinates, so we undo
  // the CTM, apply the offsets, then re-apply the CTM.
  this._applyShadowOffsetMatrix = function() {
    var inverted = CanvasKit.Matrix.invert(this._currentTransform);
    this._canvas.concat(inverted);
    this._canvas.concat(CanvasKit.Matrix.translated(this._shadowOffsetX, this._shadowOffsetY));
    this._canvas.concat(this._currentTransform);
  };

  // Returns the shadow paint for the current settings or null if there
  // should be no shadow. This ends up being a copy of the given
  // paint with a blur maskfilter and the correct color.
  this._shadowPaint = function(basePaint) {
    // multiply first to see if the alpha channel goes to 0 after multiplication.
    var alphaColor = CanvasKit.multiplyByAlpha(this._shadowColor, this._globalAlpha);
    // if alpha is zero, no shadows
    if (!CanvasKit.getColorComponents(alphaColor)[3]) {
      return null;
    }
    // one of these must also be non-zero (otherwise the shadow is
    // completely hidden.  And the spec says so).
    if (!(this._shadowBlur || this._shadowOffsetY || this._shadowOffsetX)) {
      return null;
    }
    var shadowPaint = basePaint.copy();
    shadowPaint.setColor(alphaColor);
    var blurEffect = CanvasKit.MaskFilter.MakeBlur(CanvasKit.BlurStyle.Normal,
      BlurRadiusToSigma(this._shadowBlur),
      false);
    shadowPaint.setMaskFilter(blurEffect);

    // hack up a "destructor" which also cleans up the blurEffect. Otherwise,
    // we leak the blurEffect (since smart pointers don't help us in JS land).
    shadowPaint.dispose = function() {
      blurEffect.delete();
      this.delete();
    };
    return shadowPaint;
  };

  // A helper to get a copy of the current paint, ready for stroking.
  // This applies the global alpha and the dashedness.
  // Call dispose() after to clean up.
  this._strokePaint = function() {
    var paint = this._paint.copy();
    paint.setStyle(CanvasKit.PaintStyle.Stroke);
    if (isCanvasKitColor(this._strokeStyle)) {
      var alphaColor = CanvasKit.multiplyByAlpha(this._strokeStyle, this._globalAlpha);
      paint.setColor(alphaColor);
    } else {
      var shader = this._strokeStyle._getShader(this._currentTransform);
      paint.setColor(CanvasKit.Color(0,0,0, this._globalAlpha));
      paint.setShader(shader);
    }

    paint.setStrokeWidth(this._strokeWidth);

    if (this._lineDashList.length) {
      var dashedEffect = CanvasKit.PathEffect.MakeDash(this._lineDashList, this._lineDashOffset);
      paint.setPathEffect(dashedEffect);
    }

    paint.dispose = function() {
      dashedEffect && dashedEffect.delete();
      this.delete();
    };
    return paint;
  };

  this.stroke = function(path) {
    path = path ? path._getPath() : this._currentPath;
    var strokePaint = this._strokePaint();

    var shadowPaint = this._shadowPaint(strokePaint);
    if (shadowPaint) {
      this._canvas.save();
      this._applyShadowOffsetMatrix();
      this._canvas.drawPath(path, shadowPaint);
      this._canvas.restore();
      shadowPaint.dispose();
    }

    this._canvas.drawPath(path, strokePaint);
    strokePaint.dispose();
  };

  this.strokeRect = function(x, y, width, height) {
    var strokePaint = this._strokePaint();

    var shadowPaint = this._shadowPaint(strokePaint);
    if (shadowPaint) {
      this._canvas.save();
      this._applyShadowOffsetMatrix();
      this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), shadowPaint);
      this._canvas.restore();
      shadowPaint.dispose();
    }
    this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), strokePaint);
    strokePaint.dispose();
  };

  this.strokeText = function(text, x, y, maxWidth) {
    // TODO do something with maxWidth, probably involving measure
    var strokePaint = this._strokePaint();
    var blob = CanvasKit.TextBlob.MakeFromText(text, this._font);

    var shadowPaint = this._shadowPaint(strokePaint);
    if (shadowPaint) {
      this._canvas.save();
      this._applyShadowOffsetMatrix();
      this._canvas.drawTextBlob(blob, x, y, shadowPaint);
      this._canvas.restore();
      shadowPaint.dispose();
    }
    this._canvas.drawTextBlob(blob, x, y, strokePaint);
    blob.delete();
    strokePaint.dispose();
  };

  this.translate = function(dx, dy) {
    if (!allAreFinite(arguments)) {
      return;
    }
    // retroactively apply the inverse of this transform to the previous
    // path so it cancels out when we apply the transform at draw time.
    var inverted = CanvasKit.Matrix.translated(-dx, -dy);
    this._currentPath.transform(inverted);
    this._canvas.translate(dx, dy);
    this._currentTransform = this._canvas.getTotalMatrix();
  };

  this.transform = function(a, b, c, d, e, f) {
    var newTransform = [a, c, e,
                        b, d, f,
                        0, 0, 1];
    // retroactively apply the inverse of this transform to the previous
    // path so it cancels out when we apply the transform at draw time.
    var inverted = CanvasKit.Matrix.invert(newTransform);
    this._currentPath.transform(inverted);
    this._canvas.concat(newTransform);
    this._currentTransform = this._canvas.getTotalMatrix();
  };

  // Not supported operations (e.g. for Web only)
  this.addHitRegion = function() {};
  this.clearHitRegions = function() {};
  this.drawFocusIfNeeded = function() {};
  this.removeHitRegion = function() {};
  this.scrollPathIntoView = function() {};

  Object.defineProperty(this, 'canvas', {
    value: null,
    writable: false
  });
}

function BlurRadiusToSigma(radius) {
  // Blink (Chrome) does the following, for legacy reasons, even though it
  // is against the spec. https://bugs.chromium.org/p/chromium/issues/detail?id=179006
  // This may change in future releases.
  // This code is staying here in case any clients are interested in using it
  // to match Blink "exactly".
  // if (radius <= 0)
  //   return 0;
  // return 0.288675 * radius + 0.5;
  //
  // This is what the spec says, which is how Firefox and others operate.
  return radius/2;
}
