From 2d8e30085c1ac965b2765d3b68c750caa7c439bc Mon Sep 17 00:00:00 2001 From: iaosee Date: Mon, 19 Dec 2022 23:46:01 +0800 Subject: [PATCH 1/4] feat: :sparkles: The Image adds rounded corner support --- src/Util.ts | 49 +++++++++++++++++++++++++++++++++++++ src/shapes/Image.ts | 53 ++++++++++++++++++++++++++++++++++------- src/shapes/Rect.ts | 52 ++++------------------------------------ test/sandbox.html | 32 +++++++++++++++++++++++++ test/unit/Image-test.ts | 5 ++-- 5 files changed, 134 insertions(+), 57 deletions(-) diff --git a/src/Util.ts b/src/Util.ts index 29063929..19da8d5a 100644 --- a/src/Util.ts +++ b/src/Util.ts @@ -1,4 +1,5 @@ import { Konva } from './Global'; +import { Context } from './Context'; import { IRect, RGB, RGBA, Vector2d } from './types'; /* @@ -988,5 +989,53 @@ export const Util = { c.width = 0; c.height = 0; }) + }, + drawRoundedRectPath(context: Context, width: number, height: number, cornerRadius: number | number[]) { + let topLeft = 0; + let topRight = 0; + let bottomLeft = 0; + let bottomRight = 0; + if (typeof cornerRadius === 'number') { + topLeft = topRight = bottomLeft = bottomRight = Math.min( + cornerRadius, + width / 2, + height / 2 + ); + } else { + topLeft = Math.min(cornerRadius[0] || 0, width / 2, height / 2); + topRight = Math.min(cornerRadius[1] || 0, width / 2, height / 2); + bottomRight = Math.min(cornerRadius[2] || 0, width / 2, height / 2); + bottomLeft = Math.min(cornerRadius[3] || 0, width / 2, height / 2); + } + context.moveTo(topLeft, 0); + context.lineTo(width - topRight, 0); + context.arc( + width - topRight, + topRight, + topRight, + (Math.PI * 3) / 2, + 0, + false + ); + context.lineTo(width, height - bottomRight); + context.arc( + width - bottomRight, + height - bottomRight, + bottomRight, + 0, + Math.PI / 2, + false + ); + context.lineTo(bottomLeft, height); + context.arc( + bottomLeft, + height - bottomLeft, + bottomLeft, + Math.PI / 2, + Math.PI, + false + ); + context.lineTo(0, topLeft); + context.arc(topLeft, topLeft, topLeft, Math.PI, (Math.PI * 3) / 2, false); } }; diff --git a/src/shapes/Image.ts b/src/shapes/Image.ts index 7032d7d4..1bc59aa8 100644 --- a/src/shapes/Image.ts +++ b/src/shapes/Image.ts @@ -1,8 +1,8 @@ import { Util } from '../Util'; import { Factory } from '../Factory'; import { Shape, ShapeConfig } from '../Shape'; -import { getNumberValidator } from '../Validators'; import { _registerNode } from '../Global'; +import { getNumberOrArrayOfNumbersValidator, getNumberValidator } from '../Validators'; import { GetSet, IRect } from '../types'; import { Context } from '../Context'; @@ -10,6 +10,7 @@ import { Context } from '../Context'; export interface ImageConfig extends ShapeConfig { image: CanvasImageSource | undefined; crop?: IRect; + cornerRadius?: number | number[]; } /** @@ -66,6 +67,7 @@ export class Image extends Shape { _sceneFunc(context: Context) { const width = this.getWidth(); const height = this.getHeight(); + const cornerRadius = this.cornerRadius(); const image = this.attrs.image; let params; @@ -89,23 +91,32 @@ export class Image extends Shape { } } - if (this.hasFill() || this.hasStroke()) { - context.beginPath(); + context.beginPath(); + if (!cornerRadius) { context.rect(0, 0, width, height); - context.closePath(); - context.fillStrokeShape(this); + } else { + Util.drawRoundedRectPath(context, width, height, cornerRadius); } - if (image) { + context.save(); + cornerRadius && context.clip(); context.drawImage.apply(context, params); + context.restore(); } + context.closePath(); + context.fillStrokeShape(this); } _hitFunc(context) { var width = this.width(), - height = this.height(); + height = this.height(), + cornerRadius = this.cornerRadius(); context.beginPath(); - context.rect(0, 0, width, height); + if (!cornerRadius) { + context.rect(0, 0, width, height); + } else { + Util.drawRoundedRectPath(context, width, height, cornerRadius); + } context.closePath(); context.fillStrokeShape(this); } @@ -149,10 +160,36 @@ export class Image extends Shape { cropY: GetSet; cropWidth: GetSet; cropHeight: GetSet; + cornerRadius: GetSet; } Image.prototype.className = 'Image'; _registerNode(Image); + +/** + * get/set corner radius + * @method + * @name Konva.Image#cornerRadius + * @param {Number} cornerRadius + * @returns {Number} + * @example + * // get corner radius + * var cornerRadius = image.cornerRadius(); + * + * // set corner radius + * image.cornerRadius(10); + * + * // set different corner radius values + * // top-left, top-right, bottom-right, bottom-left + * image.cornerRadius([0, 10, 20, 30]); + */ +Factory.addGetterSetter( + Image, + 'cornerRadius', + 0, + getNumberOrArrayOfNumbersValidator(4) +); + /** * get/set image source. It can be image, canvas or video element * @name Konva.Image#image diff --git a/src/shapes/Rect.ts b/src/shapes/Rect.ts index 27549e7f..4b1dcddc 100644 --- a/src/shapes/Rect.ts +++ b/src/shapes/Rect.ts @@ -2,8 +2,11 @@ import { Factory } from '../Factory'; import { Shape, ShapeConfig } from '../Shape'; import { _registerNode } from '../Global'; +import { Util } from '../Util'; import { GetSet } from '../types'; +import { Context } from '../Context'; import { getNumberOrArrayOfNumbersValidator } from '../Validators'; + export interface RectConfig extends ShapeConfig { cornerRadius?: number | number[]; } @@ -27,7 +30,7 @@ export interface RectConfig extends ShapeConfig { * }); */ export class Rect extends Shape { - _sceneFunc(context) { + _sceneFunc(context: Context) { var cornerRadius = this.cornerRadius(), width = this.width(), height = this.height(); @@ -38,52 +41,7 @@ export class Rect extends Shape { // simple rect - don't bother doing all that complicated maths stuff. context.rect(0, 0, width, height); } else { - let topLeft = 0; - let topRight = 0; - let bottomLeft = 0; - let bottomRight = 0; - if (typeof cornerRadius === 'number') { - topLeft = topRight = bottomLeft = bottomRight = Math.min( - cornerRadius, - width / 2, - height / 2 - ); - } else { - topLeft = Math.min(cornerRadius[0] || 0, width / 2, height / 2); - topRight = Math.min(cornerRadius[1] || 0, width / 2, height / 2); - bottomRight = Math.min(cornerRadius[2] || 0, width / 2, height / 2); - bottomLeft = Math.min(cornerRadius[3] || 0, width / 2, height / 2); - } - context.moveTo(topLeft, 0); - context.lineTo(width - topRight, 0); - context.arc( - width - topRight, - topRight, - topRight, - (Math.PI * 3) / 2, - 0, - false - ); - context.lineTo(width, height - bottomRight); - context.arc( - width - bottomRight, - height - bottomRight, - bottomRight, - 0, - Math.PI / 2, - false - ); - context.lineTo(bottomLeft, height); - context.arc( - bottomLeft, - height - bottomLeft, - bottomLeft, - Math.PI / 2, - Math.PI, - false - ); - context.lineTo(0, topLeft); - context.arc(topLeft, topLeft, topLeft, Math.PI, (Math.PI * 3) / 2, false); + Util.drawRoundedRectPath(context, width, height, cornerRadius); } context.closePath(); context.fillStrokeShape(this); diff --git a/test/sandbox.html b/test/sandbox.html index 38276fcb..a136b90e 100644 --- a/test/sandbox.html +++ b/test/sandbox.html @@ -52,6 +52,38 @@ stage.on('contextmenu', (e) => { console.log('click'); }); + + var imageObj = new Image(); + imageObj.onload = function () { + var yoda = new Konva.Image({ + x: 150, + y: 50, + image: imageObj, + width: 200, + height: 200, + draggable: true, + strokeWidth: 4, + stroke: 'blue', + cornerRadius: [10, 20, 30, 40], + }); + layer.add(yoda); + }; + imageObj.src = 'https://konvajs.org/assets/yoda.jpg'; + + Konva.Image.fromURL('https://konvajs.org/assets/darth-vader.jpg', function (darthNode) { + darthNode.setAttrs({ + x: 300, + y: 50, + // scaleX: 0.5, + // scaleY: 0.5, + draggable: true, + strokeEnabled: true, + stroke: 'green', + strokeWidth: 2, + cornerRadius: 30, + }); + layer.add(darthNode); + }); diff --git a/test/unit/Image-test.ts b/test/unit/Image-test.ts index 4cf4fa43..0376433d 100644 --- a/test/unit/Image-test.ts +++ b/test/unit/Image-test.ts @@ -26,6 +26,7 @@ describe('Image', function () { offset: { x: 50, y: 30 }, crop: { x: 135, y: 7, width: 167, height: 134 }, draggable: true, + cornerRadius: 15, }); layer.add(darth); @@ -43,9 +44,9 @@ describe('Image', function () { assert.equal(darth.getHeight(), 100); assert.equal(darth.offset().x, 50); assert.equal(darth.offset().y, 30); + assert.equal(darth.cornerRadius(), 15); - var crop = null; - crop = darth.crop(); + var crop = darth.crop(); assert.equal(crop.x, 135); assert.equal(crop.y, 7); From e04cabfb459ce5e026ab8b9e5af3d5d2ce1bd333 Mon Sep 17 00:00:00 2001 From: iaosee Date: Wed, 21 Dec 2022 23:57:42 +0800 Subject: [PATCH 2/4] fix: :bug: fix test failed --- src/shapes/Image.ts | 25 +++++++++++++++---------- test/unit/Image-test.ts | 4 ++-- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/shapes/Image.ts b/src/shapes/Image.ts index 1bc59aa8..8363b9d2 100644 --- a/src/shapes/Image.ts +++ b/src/shapes/Image.ts @@ -91,20 +91,25 @@ export class Image extends Shape { } } - context.beginPath(); - if (!cornerRadius) { - context.rect(0, 0, width, height); - } else { - Util.drawRoundedRectPath(context, width, height, cornerRadius); + if (this.hasFill() || this.hasStroke()) { + context.beginPath(); + cornerRadius + ? Util.drawRoundedRectPath(context, width, height, cornerRadius) + : context.rect(0, 0, width, height); + context.closePath(); + context.fillStrokeShape(this); } + if (image) { - context.save(); - cornerRadius && context.clip(); + // context.save(); + if (cornerRadius) { + // Util.drawRoundedRectPath(context, width, height, cornerRadius); + context.clip(); + } context.drawImage.apply(context, params); - context.restore(); + // context.restore(); } - context.closePath(); - context.fillStrokeShape(this); + // If you need to draw later, you need to execute save/restore } _hitFunc(context) { var width = this.width(), diff --git a/test/unit/Image-test.ts b/test/unit/Image-test.ts index 0376433d..bebc7990 100644 --- a/test/unit/Image-test.ts +++ b/test/unit/Image-test.ts @@ -26,7 +26,6 @@ describe('Image', function () { offset: { x: 50, y: 30 }, crop: { x: 135, y: 7, width: 167, height: 134 }, draggable: true, - cornerRadius: 15, }); layer.add(darth); @@ -44,7 +43,6 @@ describe('Image', function () { assert.equal(darth.getHeight(), 100); assert.equal(darth.offset().x, 50); assert.equal(darth.offset().y, 30); - assert.equal(darth.cornerRadius(), 15); var crop = darth.crop(); @@ -169,6 +167,7 @@ describe('Image', function () { crop: { x: 186, y: 211, width: 106, height: 74 }, draggable: true, scale: { x: 0.5, y: 0.5 }, + cornerRadius: 15, }); layer.add(darth); @@ -210,6 +209,7 @@ describe('Image', function () { assert.equal(darth.cropY(), 6); assert.equal(darth.cropWidth(), 7); assert.equal(darth.cropHeight(), 8); + assert.equal(darth.cornerRadius(), 15); done(); }); From e3f1d1c95b4ed3456189b9888e2a94f68948eb3a Mon Sep 17 00:00:00 2001 From: Anton Lavrenov Date: Thu, 5 Jan 2023 23:43:35 -0500 Subject: [PATCH 3/4] fix incorrect display when no fill or stroke --- src/shapes/Image.ts | 10 +++++----- test/unit/Image-test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/shapes/Image.ts b/src/shapes/Image.ts index 8363b9d2..12791d3e 100644 --- a/src/shapes/Image.ts +++ b/src/shapes/Image.ts @@ -2,7 +2,10 @@ import { Util } from '../Util'; import { Factory } from '../Factory'; import { Shape, ShapeConfig } from '../Shape'; import { _registerNode } from '../Global'; -import { getNumberOrArrayOfNumbersValidator, getNumberValidator } from '../Validators'; +import { + getNumberOrArrayOfNumbersValidator, + getNumberValidator, +} from '../Validators'; import { GetSet, IRect } from '../types'; import { Context } from '../Context'; @@ -91,7 +94,7 @@ export class Image extends Shape { } } - if (this.hasFill() || this.hasStroke()) { + if (this.hasFill() || this.hasStroke() || cornerRadius) { context.beginPath(); cornerRadius ? Util.drawRoundedRectPath(context, width, height, cornerRadius) @@ -101,13 +104,10 @@ export class Image extends Shape { } if (image) { - // context.save(); if (cornerRadius) { - // Util.drawRoundedRectPath(context, width, height, cornerRadius); context.clip(); } context.drawImage.apply(context, params); - // context.restore(); } // If you need to draw later, you need to execute save/restore } diff --git a/test/unit/Image-test.ts b/test/unit/Image-test.ts index bebc7990..fb743cb1 100644 --- a/test/unit/Image-test.ts +++ b/test/unit/Image-test.ts @@ -385,4 +385,32 @@ describe('Image', function () { done(); }); }); + + it.only('corner radius', function (done) { + loadImage('darth-vader.jpg', (imageObj) => { + var stage = addStage(); + + var layer = new Konva.Layer(); + var darth = new Konva.Image({ + x: 20, + y: 20, + image: imageObj, + cornerRadius: 10, + draggable: true, + stroke: 'red', + strokeWidth: 100, + strokeEnabled: false, + }); + + layer.add(darth); + stage.add(layer); + + assert.equal( + layer.getContext().getTrace(true), + 'clearRect();save();transform();beginPath();moveTo();lineTo();arc();lineTo();arc();lineTo();arc();lineTo();arc();closePath();clip();drawImage();restore();' + ); + + done(); + }); + }); }); From a17eab69cb87e082695e62732efbdc79ad0dd95e Mon Sep 17 00:00:00 2001 From: Anton Lavrenov Date: Thu, 5 Jan 2023 23:43:48 -0500 Subject: [PATCH 4/4] enable tests --- test/unit/Image-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/Image-test.ts b/test/unit/Image-test.ts index fb743cb1..1f666ed3 100644 --- a/test/unit/Image-test.ts +++ b/test/unit/Image-test.ts @@ -386,7 +386,7 @@ describe('Image', function () { }); }); - it.only('corner radius', function (done) { + it('corner radius', function (done) { loadImage('darth-vader.jpg', (imageObj) => { var stage = addStage();