CreateJS Radial gradient with matrix

xonorageous picture xonorageous · Feb 21, 2016 · Viewed 8.2k times · Source

I'm converting a Flash application to HTML5 Canvas. Most of the development is finished but for handling the colors there is a code like this in the flash application:

matrix = new Matrix ();
matrix.createGradientBox (600, ColorHeight * 1200, 0, 80, ColorHeight * -600);
Animation_gradient_mc.clear ();
Animation_gradient_mc.beginGradientFill (fillType, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio);

The declaration for a radial gradient in CreateJS is the following:

beginRadialGradientFill(colors, ratios, x0, y0, r0, x1, y1, r1 )

Does anyone know a method to apply a Matrix to a gradient fill?

Any help would be appreciated.

Thanks in advance

Edit

Here are some examples of the gradient I'm trying to reproduce:

Gradient start

As you can see it starts off as a standard radial gradient.

However, it can also appear stretched, I think this is where the matrix helps.

Gradient 2

I've attempted to create the same effect by creating a createjs.Graphics.Fill with a matrix but it doesn't seem to be doing anything:

var matrix = new VacpMatrix();
    matrix.createGradientBox(
        600,
        discharge_gradient.color_height * 1200,
        0,
        80,
        discharge_gradient.color_height * -600
    );

    // test_graphics.append(new createjs.Graphics.Fill('#0000ff', matrix));

    console.log('matrix', matrix);

    test_graphics.append(new createjs.Graphics.Fill('#ff0000', matrix).radialGradient(
        discharge_gradient.colors,
        discharge_gradient.ratios,
        discharge_gradient.x0,
        discharge_gradient.y0,
        discharge_gradient.r0,
        discharge_gradient.x1,
        discharge_gradient.y1,
        discharge_gradient.r1
    ));

    var discharge_shape = new createjs.Shape(test_graphics);

I extended the Matrix2d class to add a createGradientBox method using code from the openfl project:

p.createGradientBox = function (width, height, rotation, tx, ty) {
    if (_.isUndefined(rotation) || _.isNull(rotation)) {
        rotation = 0;
    }

    if (_.isUndefined(tx) || _.isNull(tx)) {
        tx = 0;
    }

    if (_.isUndefined(ty) || _.isNull(ty)) {
        ty = 0;
    }

    var a = width / 1638.4,
        d = height / 1638.4;

    // Rotation is clockwise
    if (rotation != 0) {
        var cos = math.cos(rotation),
            sin = math.sin(rotation);

        this.b = sin * d;
        this.c = -sin * a;
        this.a = a * cos;
        this.d = d * cos;
    } else {
        this.b = 0;
        this.c = 0;
    }

    this.tx = tx + width / 2;
    this.ty = ty + height / 2;
}

I hope the extra information is useful.

Answer

Kaiido picture Kaiido · Mar 3, 2016

I don't know createJS enough, nor Flash Matrix object, but to make this kind of ovalGradient with the native Canvas2d API, you will need to transform the context's matrix.

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var horizontalScale = .3;
var verticalScale = 1;

var gradient = ctx.createRadialGradient(100/horizontalScale, 100/verticalScale, 100, 100/horizontalScale,100/verticalScale,0);
gradient.addColorStop(0,"green");
gradient.addColorStop(1,"red");

// shrink the context's matrix
ctx.scale(horizontalScale, verticalScale)
// draw your gradient
ctx.fillStyle = gradient;
// stretch the rectangle which contains the gradient accordingly
ctx.fillRect(0,0, 200/horizontalScale, 200/verticalScale);
// reset the context's matrix
ctx.setTransform(1,0,0,1,0,0);
canvas{ background-color: ivory;}
<canvas id="canvas" width="200" height="200"></canvas>

So if you are planning to write some kind of a function to reproduce it, have a look at ctx.scale(), ctx.transform() and ctx.setTransform().

EDIT

As you noticed, this will also shrink your drawn shapes, also, you will have to calculate how much you should "unshrink" those at the drawing, just like I did with the fillRect. (agreed, this one was an easy one)

Here is a function that could help you with more complicated shapes. I didn't really tested it (only with the given example), so it may fail somehow, but it can also give you an idea on how to deal with it :

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

function shrinkedRadial(ctx, shapeArray, xScale, yScale, gradientOpts) {

    // scaling by 0 is like not drawing
    if (!xScale || !yScale) return;

    var gO = gradientOpts;
    // apply our scale on the gradient options we passed
    var gradient = ctx.createRadialGradient(gO.x0 / xScale, gO.y0 / yScale, gO.r0, gO.x1 / xScale, gO.y1 / yScale, gO.r1);
    gradient.addColorStop(gO.c1_pos, gO.c1_fill);
    gradient.addColorStop(gO.c2_pos, gO.c2_fill);

    // shrink the context's matrix
    ctx.scale(xScale, yScale);

    ctx.fillStyle = gradient;
    // execute the drawing operations' string
    shapeArray.forEach(function(str) {
      var val = str.split(' ');
      var op = shapesRef[val[0]];
      if (val[1]) {
        var pos = val[1].split(',').map(function(v, i) {
          // if even, it should be an y axis, otherwise an x one
          return i % 2 ? v / yScale : v / xScale;
        });
        ctx[op].apply(ctx, pos);
      } else {
        // no parameters
        ctx[op]();
      }
    });
    // apply our gradient
    ctx.fill();
    // reset the transform matrix
    ctx.setTransform(1, 0, 0, 1, 0, 0);
  }

// just for shortening our shape drawing operations
// notice how arc operations are omitted, it could be implemented but...
var shapesRef = {
  b: 'beginPath',
  fR: 'fillRect',
  m: 'moveTo',
  l: 'lineTo',
  bC: 'bezierCurveTo',
  qC: 'quadraticCurveTo',
  r: 'rect',
  c: 'closePath'
};

var gradientOpts = {
  x0: 232,
  y0: 55,
  r0: 70,
  x1: 232,
  y1: 55,
  r1: 0,
  c1_fill: 'red',
  c1_pos: 0,
  c2_fill: 'green',
  c2_pos: 1
}

var shapes = ['b', 'm 228,133', 'bC 209,121,154,76,183,43', 'bC 199,28,225,34,233,59', 'bC 239,34,270,29,280,39', 'bC 317,76,248,124,230,133']

// our shape is drawn at 150px from the right so we do move the context accordingly, but you won't have to.
ctx.translate(-150, 0);

shrinkedRadial(ctx, shapes, .3, 1, gradientOpts);

ctx.font = '15px sans-serif';
ctx.fillStyle = 'black';
ctx.fillText('shrinked radialGradient', 3, 20);

// how it looks like without scaling : 

ctx.translate(50, 0)

var gO = gradientOpts;
var gradient = ctx.createRadialGradient(gO.x0, gO.y0, gO.r0, gO.x1, gO.y1, gO.r1);
gradient.addColorStop(gO.c1_pos, gO.c1_fill);
gradient.addColorStop(gO.c2_pos, gO.c2_fill);

ctx.fillStyle = gradient;

shapes.forEach(function(str) {
  var val = str.split(' ');
  var op = shapesRef[val[0]];
  if (val[1]) {
    var pos = val[1].split(',');
    ctx[op].apply(ctx, pos);
  } else {
    ctx[op]();
  }
});
ctx.fill();
ctx.font = '15px sans-serif';
ctx.fillStyle = 'black';
ctx.fillText('normal radialGradient', 160, 20);
<canvas id="canvas" width="400" height="150"></canvas>