Skip to content

Commit

Permalink
fix(gradients): support for 'transparent' as a color stop (#65)
Browse files Browse the repository at this point in the history
Thank you very much for all your effort. I will slate a release for this very soon. By Tuesday evening, eastern standard time.
  • Loading branch information
evanoc3 committed Apr 11, 2020
1 parent 80f24aa commit 90fc2bf
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 67 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG

## Version 2.3.0

- Switched to `moo-color` for color parsing
- Added contributors to markdown document

## Version 2.2.0

- Bug: Slice canvas transform value when pushing (#50)
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ canvas.toDataURL.mockReturnValueOnce(
);
```

## Contributors

* [@hustcc](https://github.com/hustcc)
* [@jtenner](https://github.com/jtenner)
* [@evanoc0](https://github.com/evanoc0)

## License

MIT@[hustcc](https://github.com/hustcc).
MIT@[hustcc](https://github.com/hustcc).
30 changes: 27 additions & 3 deletions __tests__/classes/CanvasGradient.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,39 @@ describe('CanvasGradient', () => {
test('CanvasGradient should throw if offset is ' + value, () => {
expect(() => {
var grd = ctx.createLinearGradient(1, 2, 3, 4);
grd.addColorStop(value, "blue");
grd.addColorStop(value, 'blue');
}).toThrow(DOMException);
});
});

test('should accept all valid CSS colors as a color stop', () => {
const grd = ctx.createLinearGradient(0, 0, 100, 100);
expect(() => {
grd.addColorStop(0.1, 'blue');
grd.addColorStop(0.2, '#f008');
grd.addColorStop(0.2, '#f0f0f0');
grd.addColorStop(0.3, '#f0f0f0f0');
grd.addColorStop(0.3, 'rgb(10, 10, 10)');
grd.addColorStop(0.4, 'rgb(100%, 25%, 25%)');
grd.addColorStop(0.3, 'rgba(100, 100, 100, 0.5)');
grd.addColorStop(0.4, 'hsl(180, 100%, 50%)');
grd.addColorStop(0.5, 'hsla(180, 100%, 50%, 0.5)');
grd.addColorStop(0.0, 'transparent');
}).not.toThrow(SyntaxError);
});

test('CanvasGradient should throw if color cannot be parsed', () => {
const grd = ctx.createLinearGradient(1, 2, 3, 4);
expect(() => {
grd.addColorStop(0.5, 'invalid');
}).toThrow(SyntaxError);

expect(() => {
grd.addColorStop(0.5, 'rgb(50%, 0, 50%)');
}).toThrow(SyntaxError);

expect(() => {
var grd = ctx.createLinearGradient(1, 2, 3, 4);
grd.addColorStop(1, "badcolor");
grd.addColorStop(0.5, 'hsl(180, 50%, 50)');
}).toThrow(SyntaxError);
});
});
8 changes: 4 additions & 4 deletions __tests__/classes/CanvasRenderingContext2D.fillStyle.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ beforeEach(() => {
describe('fillStyle', () => {
it('should parse a css color string \'blue\'', () => {
ctx.fillStyle = 'blue';
expect(ctx.fillStyle).toBe('#00f');
expect(ctx.fillStyle).toBe('#0000ff');
});

it('should not parse invalid colors', () => {
ctx.fillStyle = 'invalid!';
expect(ctx.fillStyle).toBe('#000');
expect(ctx.fillStyle).toBe('#000000');
});

it('should parse css colors with alpha values', () => {
Expand All @@ -28,7 +28,7 @@ describe('fillStyle', () => {
ctx.fillStyle = 'green';
ctx.save();
ctx.fillStyle = 'red';
expect(ctx.fillStyle).toBe('#f00');
expect(ctx.fillStyle).toBe('#ff0000');
ctx.restore();
expect(ctx.fillStyle).toBe('#008000');
});
Expand All @@ -49,6 +49,6 @@ describe('fillStyle', () => {

it('should ignore invalid fillStyle values', () => {
ctx.fillStyle = null;
expect(ctx.fillStyle).toBe('#000');
expect(ctx.fillStyle).toBe('#000000');
});
});
4 changes: 2 additions & 2 deletions __tests__/classes/CanvasRenderingContext2D.shadowColor.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ beforeEach(() => {
describe('shadowColor', () => {
it('should parse a css color string \'blue\'', () => {
ctx.shadowColor = 'blue';
expect(ctx.shadowColor).toBe('#00f');
expect(ctx.shadowColor).toBe('#0000ff');
});

it('should not parse invalid colors', () => {
Expand All @@ -28,7 +28,7 @@ describe('shadowColor', () => {
ctx.shadowColor = 'green';
ctx.save();
ctx.shadowColor = 'red';
expect(ctx.shadowColor).toBe('#f00');
expect(ctx.shadowColor).toBe('#ff0000');
ctx.restore();
expect(ctx.shadowColor).toBe('#008000');
});
Expand Down
8 changes: 4 additions & 4 deletions __tests__/classes/CanvasRenderingContext2D.strokeStyle.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ beforeEach(() => {
describe('strokeStyle', () => {
it('should parse a css color string \'blue\'', () => {
ctx.strokeStyle = 'blue';
expect(ctx.strokeStyle).toBe('#00f');
expect(ctx.strokeStyle).toBe('#0000ff');
});

it('should not parse invalid colors', () => {
ctx.strokeStyle = 'invalid!';
expect(ctx.strokeStyle).toBe('#000');
expect(ctx.strokeStyle).toBe('#000000');
});

it('should parse css colors with alpha values', () => {
Expand All @@ -28,7 +28,7 @@ describe('strokeStyle', () => {
ctx.strokeStyle = 'green';
ctx.save();
ctx.strokeStyle = 'red';
expect(ctx.strokeStyle).toBe('#f00');
expect(ctx.strokeStyle).toBe('#ff0000');
ctx.restore();
expect(ctx.strokeStyle).toBe('#008000');
});
Expand All @@ -49,6 +49,6 @@ describe('strokeStyle', () => {

it('should ignore invalid strokeStyle values', () => {
ctx.strokeStyle = null;
expect(ctx.strokeStyle).toBe('#000');
expect(ctx.strokeStyle).toBe('#000000');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1010,7 +1010,7 @@ exports[`__getEvents should have an event when fillStyle is set 1`] = `
Array [
Object {
"props": Object {
"value": "#00f",
"value": "#0000ff",
},
"transform": Array [
1,
Expand Down Expand Up @@ -1874,7 +1874,7 @@ exports[`__getEvents should have an event when the shadowColor is valid 1`] = `
Array [
Object {
"props": Object {
"value": "#f00",
"value": "#ff0000",
},
"transform": Array [
1,
Expand Down Expand Up @@ -1931,7 +1931,7 @@ exports[`__getEvents should have an event when the strokeStyle is set 1`] = `
Array [
Object {
"props": Object {
"value": "#00f",
"value": "#0000ff",
},
"transform": Array [
1,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"dependencies": {
"cssfontparser": "^1.2.1",
"parse-color": "^1.0.0"
"moo-color": "^1.0.2"
},
"devDependencies": {
"@babel/cli": "^7.7.0",
Expand Down
11 changes: 6 additions & 5 deletions src/classes/CanvasGradient.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import parseColor from 'parse-color';
import { MooColor } from 'moo-color';

export default class CanvasGradient {
constructor() {
this.addColorStop = jest.fn(this.addColorStop.bind(this));
}

addColorStop(offset, color) {
const numoffset = Number(offset);
const colorstr = String(color);
if (!Number.isFinite(numoffset) || numoffset < 0 || numoffset > 1) {
throw new DOMException('IndexSizeError', 'Failed to execute \'addColorStop\' on \'CanvasGradient\': The provided value (\'' + numoffset + '\') is outside the range (0.0, 1.0)');
}
const output = parseColor(colorstr);
if (!output.hex) {
throw new SyntaxError('Failed to execute \'addColorStop\' on \'CanvasGradient\': The value provided (\'' + color + '\') could not be parsed as a color.')
try {
new MooColor(color);
} catch(e) {
throw new SyntaxError('Failed to execute \'addColorStop\' on \'CanvasGradient\': The value provided (\'' + color + '\') could not be parsed as a color.');
}
}
}
80 changes: 36 additions & 44 deletions src/classes/CanvasRenderingContext2D.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,11 @@
import DOMMatrix from './DOMMatrix';
import CanvasPattern from './CanvasPattern';
import parseColor from 'parse-color';
import cssfontparser from 'cssfontparser';
import TextMetrics from './TextMetrics';
import createCanvasEvent from '../mock/createCanvasEvent';
import Path2D from "./Path2D";
import { MooColor } from 'moo-color';

function parseCSSColor(value) {
const result = parseColor(value);

if (result.rgba && result.rgba[3] !== 1) {
return 'rgba(' + result.rgba.join(', ') + ')';
}

if (result.hex) {
const hex = result.hex;

// shorthand #ABC
if (hex[1] === hex[2] && hex[3] === hex[4] && hex[5] === hex[6]) {
return '#' + hex[1] + hex[3] + hex[5];
}
return result.hex;
}

return void 0;
}

const testFuncs = ['setLineDash', 'getLineDash', 'setTransform', 'getTransform', 'getImageData', 'save', 'restore', 'createPattern', 'createRadialGradient', 'addHitRegion', 'arc', 'arcTo', 'beginPath', 'clip', 'closePath', 'scale', 'stroke', 'clearHitRegions', 'clearRect', 'fillRect', 'strokeRect', 'rect', 'resetTransform', 'translate', 'moveTo', 'lineTo', 'bezierCurveTo', 'createLinearGradient', 'ellipse', 'measureText', 'rotate', 'drawImage', 'drawFocusIfNeeded', 'isPointInPath', 'isPointInStroke', 'putImageData', 'strokeText', 'fillText', 'quadraticCurveTo', 'removeHitRegion', 'fill', 'transform', 'scrollPathIntoView', 'createImageData'];
const compositeOperations = ['source-over', 'source-in', 'source-out', 'source-atop', 'destination-over', 'destination-in', 'destination-out', 'destination-atop', 'lighter', 'copy', 'xor', 'multiply', 'screen', 'overlay', 'darken', 'lighten', 'color-dodge', 'color-burn', 'hard-light', 'soft-light', 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity'];
Expand All @@ -33,6 +14,15 @@ function getTransformSlice(ctx) {
return ctx._transformStack[ctx._stackIndex].slice();
}

/**
* Returns the string serialization of a CSS color, according to https://www.w3.org/TR/2dcontext/#serialization-of-a-color
*/
function serializeColor(value) {
return (value.getAlpha() === 1)
? value.toHex()
: value.toRgb();
}

export default class CanvasRenderingContext2D {
/**
* Every time a function call would result in a drawing operation, it should be added to this array.
Expand Down Expand Up @@ -97,7 +87,7 @@ export default class CanvasRenderingContext2D {
}

_directionStack = ['inherit'];
_fillStyleStack = ['#000'];
_fillStyleStack = [ '#000000' ];
_filterStack = ['none'];
_fontStack = ['10px sans-serif'];
_globalAlphaStack = [1.0];
Expand All @@ -111,11 +101,11 @@ export default class CanvasRenderingContext2D {
_lineWidthStack = [1];
_miterLimitStack = [10];
_shadowBlurStack = [0];
_shadowColorStack = ['rgba(0, 0, 0, 0)'];
_shadowColorStack = [ 'rgba(0, 0, 0, 0)' ];
_shadowOffsetXStack = [0];
_shadowOffsetYStack = [0];
_stackIndex = 0;
_strokeStyleStack = ['#000'];
_strokeStyleStack = [ '#000000' ];
_textAlignStack = ['start'];
_textBaselineStack = ['alphabetic'];
_transformStack = [[1, 0, 0, 1, 0, 0]];
Expand Down Expand Up @@ -637,13 +627,14 @@ export default class CanvasRenderingContext2D {
set fillStyle(value) {
let valid = false;
if (typeof value === 'string') {
const result = parseCSSColor(value);

if (result) {
try {
const result = new MooColor(value);
valid = true;
value = this._fillStyleStack[this._stackIndex] = result;
value = this._fillStyleStack[this._stackIndex] = serializeColor(result);
}
} else if (value instanceof CanvasGradient || value instanceof CanvasPattern) {
catch(e) { return; }
}
else if (value instanceof CanvasGradient || value instanceof CanvasPattern) {
valid = true;
this._fillStyleStack[this._stackIndex] = value;
}
Expand Down Expand Up @@ -1282,17 +1273,17 @@ export default class CanvasRenderingContext2D {

set shadowColor(value) {
if (typeof value === 'string') {
const result = parseCSSColor(value);

if (result) {
this._shadowColorStack[this._stackIndex] = result;
const event = createCanvasEvent(
'shadowColor',
getTransformSlice(this),
{ value: result },
);
this._events.push(event);
}
try {
const result = new MooColor(value);
value = this._shadowColorStack[this._stackIndex] = serializeColor(result);
} catch (e) { return; }

const event = createCanvasEvent(
'shadowColor',
getTransformSlice(this),
{ value },
);
this._events.push(event);
}
}

Expand Down Expand Up @@ -1373,13 +1364,14 @@ export default class CanvasRenderingContext2D {
set strokeStyle(value) {
let valid = false;
if (typeof value === 'string') {
const result = parseCSSColor(value);

if (result) {
try {
const result = new MooColor(value);
valid = true;
value = this._strokeStyleStack[this._stackIndex] = result;
value = this._strokeStyleStack[this._stackIndex] = serializeColor(result);
}
} else if (value instanceof CanvasGradient || value instanceof CanvasPattern) {
catch(e) { return; }
}
else if (value instanceof CanvasGradient || value instanceof CanvasPattern) {
valid = true;
this._strokeStyleStack[this._stackIndex] = value;
}
Expand Down

0 comments on commit 90fc2bf

Please sign in to comment.