diff --git a/index.js b/index.js index 80adf8175ec..dc074638330 100644 --- a/index.js +++ b/index.js @@ -5,11 +5,6 @@ import './src/mixins/collection.mixin'; import './src/mixins/shared_methods.mixin'; import './src/util/misc/misc'; // import './src/util/named_accessors.mixin'; i would imagine dead forever or proper setters/getters -import './src/util/lang_class'; -import './src/util/dom_misc'; -import './src/util/animate'; // optional animation -import './src/util/animate_color'; // optional animation -import './src/util/anim_ease'; // optional easing import './src/parser'; // optional parser import './src/point.class'; import './src/intersection.class'; diff --git a/src/cache.ts b/src/cache.ts index ff56089cbf8..6abad7709bb 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -1,3 +1,4 @@ +import { config } from './config'; export class Cache { /** @@ -44,6 +45,21 @@ export class Cache { } } + /** + * Given current aspect ratio, determines the max width and height that can + * respect the total allowed area for the cache. + * @memberOf fabric.util + * @param {number} ar aspect ratio + * @return {number[]} Limited dimensions X and Y + */ + limitDimsByArea(ar: number) { + const { perfLimitSizeTotal } = config; + const roughWidth = Math.sqrt(perfLimitSizeTotal * ar); + // we are not returning a point on purpose, to avoid circular dependencies + // this is an internal utility + return [Math.floor(roughWidth), Math.floor(perfLimitSizeTotal / roughWidth)]; + } + /** * This object contains the result of arc to bezier conversion for faster retrieving if the same arc needs to be converted again. * It was an internal variable, is accessible since version 2.3.4 diff --git a/src/canvas.class.ts b/src/canvas.class.ts index 5ad7f28a555..980ac09a6c4 100644 --- a/src/canvas.class.ts +++ b/src/canvas.class.ts @@ -1044,19 +1044,20 @@ import { Point } from './point.class'; * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized */ _createUpperCanvas: function () { - var lowerCanvasClass = this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/, ''), - lowerCanvasEl = this.lowerCanvasEl, upperCanvasEl = this.upperCanvasEl; + var lowerCanvasEl = this.lowerCanvasEl, upperCanvasEl = this.upperCanvasEl; - // there is no need to create a new upperCanvas element if we have already one. - if (upperCanvasEl) { - upperCanvasEl.className = ''; - } - else { + // if there is no upperCanvas (most common case) we create one. + if (!upperCanvasEl) { upperCanvasEl = this._createCanvasElement(); this.upperCanvasEl = upperCanvasEl; } - fabric.util.addClass(upperCanvasEl, 'upper-canvas ' + lowerCanvasClass); - this.upperCanvasEl.setAttribute('data-fabric', 'top'); + // we assign the same classname of the lowerCanvas + upperCanvasEl.className = lowerCanvasEl.className; + // but then we remove the lower-canvas specific className + upperCanvasEl.classList.remove('lower-canvas'); + // we add the specific upper-canvas class + upperCanvasEl.classList.add('upper-canvas'); + upperCanvasEl.setAttribute('data-fabric', 'top'); this.wrapperEl.appendChild(upperCanvasEl); this._copyCanvasStyle(lowerCanvasEl, upperCanvasEl); @@ -1082,9 +1083,9 @@ import { Point } from './point.class'; if (this.wrapperEl) { return; } - this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', { - 'class': this.containerClass - }); + const container = fabric.document.createElement('div'); + container.classList.add(this.containerClass); + this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, container); this.wrapperEl.setAttribute('data-fabric', 'wrapper'); fabric.util.setStyle(this.wrapperEl, { width: this.width + 'px', diff --git a/src/constants.ts b/src/constants.ts index f6c28147cb5..59ac5f12ba0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,8 +1,9 @@ import { TMat2D } from "./typedefs"; export { version as VERSION } from '../package.json'; -export const noop = () => {}; +export function noop() {}; export const halfPI = Math.PI / 2; +export const twoMathPi = Math.PI * 2; export const PiBy180 = Math.PI / 180; export const iMatrix = Object.freeze([1, 0, 0, 1, 0, 0]) as TMat2D; export const DEFAULT_SVG_FONT_SIZE = 16; diff --git a/src/parser/parseSVGDocument.ts b/src/parser/parseSVGDocument.ts index b65751a7b86..e6eb9e9ed1a 100644 --- a/src/parser/parseSVGDocument.ts +++ b/src/parser/parseSVGDocument.ts @@ -31,7 +31,7 @@ export function parseSVGDocument(doc, callback, reviver, parsingOptions) { } parseUseDirectives(doc); - let svgUid = fabric.Object.__uid++, i, len, options = applyViewboxTransform(doc), descendants = fabric.util.toArray(doc.getElementsByTagName('*')); + let svgUid = fabric.Object.__uid++, i, len, options = applyViewboxTransform(doc), descendants = Array.from(doc.getElementsByTagName('*')); options.crossOrigin = parsingOptions && parsingOptions.crossOrigin; options.svgUid = svgUid; options.signal = parsingOptions && parsingOptions.signal; @@ -61,7 +61,7 @@ export function parseSVGDocument(doc, callback, reviver, parsingOptions) { return el.nodeName.replace('svg:', '') === 'clipPath'; }).forEach(function (el) { const id = el.getAttribute('id'); - localClipPaths[id] = fabric.util.toArray(el.getElementsByTagName('*')).filter(function (el) { + localClipPaths[id] = Array.from(el.getElementsByTagName('*')).filter(function (el) { return svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', '')); }); }); diff --git a/src/point.class.ts b/src/point.class.ts index c868a5e368a..a9a3c82220d 100644 --- a/src/point.class.ts +++ b/src/point.class.ts @@ -1,5 +1,7 @@ import { fabric } from '../HEADER'; import { TMat2D, TRadian } from './typedefs'; +import { sin } from './util/misc/sin'; +import { cos } from './util/misc/cos'; export interface IPoint { x: number @@ -350,16 +352,22 @@ export class Point { /** * Rotates `point` around `origin` with `radians` - * WARNING: this is probably a source of circular dependency. - * evaluate what to do when importing rotateVector directly from the file * @static * @memberOf fabric.util * @param {Point} origin The origin of the rotation * @param {TRadian} radians The radians of the angle for the rotation * @return {Point} The new rotated point */ - rotate(origin: Point, radians: TRadian): Point { - return fabric.util.rotateVector(this.subtract(origin), radians).add(origin); + rotate(radians: TRadian, origin: Point = originZero): Point { + // TODO benchmark and verify the add and subtract how much cost + // and then in case early return if no origin is passed + const sinus = sin(radians), cosinus = cos(radians); + const p = this.subtract(origin); + const rotated = new Point( + p.x * cosinus - p.y * sinus, + p.x * sinus + p.y * cosinus, + ); + return rotated.add(origin); } /** @@ -378,4 +386,6 @@ export class Point { } } +const originZero = new Point(0, 0); + fabric.Point = Point; diff --git a/src/shadow.class.ts b/src/shadow.class.ts index 3c641573d1e..10bad11f110 100644 --- a/src/shadow.class.ts +++ b/src/shadow.class.ts @@ -2,6 +2,7 @@ import { Color } from "./color"; import { config } from "./config"; +import { Point } from "./point.class"; (function(global) { var fabric = global.fabric || (global.fabric = { }), @@ -119,7 +120,7 @@ import { config } from "./config"; toSVG: function(object) { var fBoxX = 40, fBoxY = 40, NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS, offset = fabric.util.rotateVector( - { x: this.offsetX, y: this.offsetY }, + new Point(this.offsetX, this.offsetY), fabric.util.degreesToRadians(-object.angle)), BLUR_BOX = 20, color = new Color(this.color); diff --git a/src/shapes/image.class.ts b/src/shapes/image.class.ts index 8962a6f1bfc..99743e8693c 100644 --- a/src/shapes/image.class.ts +++ b/src/shapes/image.class.ts @@ -163,6 +163,7 @@ this._element = element; this._originalElement = element; this._initConfig(options); + element.classList.add(fabric.Image.CSS_CANVAS); if (this.filters.length !== 0) { this.applyFilters(); } @@ -477,7 +478,7 @@ * @param {CanvasRenderingContext2D} ctx Context to render on */ _render: function(ctx) { - fabric.util.setImageSmoothing(ctx, this.imageSmoothing); + ctx.imageSmoothingEnabled = this.imageSmoothing; if (this.isMoving !== true && this.resizeFilter && this._needsResize()) { this.applyResizeFilters(); } @@ -491,7 +492,7 @@ * @param {CanvasRenderingContext2D} ctx Context to render on */ drawCacheOnCanvas: function(ctx) { - fabric.util.setImageSmoothing(ctx, this.imageSmoothing); + ctx.imageSmoothingEnabled = this.imageSmoothing; fabric.Object.prototype.drawCacheOnCanvas.call(this, ctx); }, @@ -557,8 +558,7 @@ * @param {Object} [options] Options object */ _initElement: function(element, options) { - this.setElement(fabric.util.getById(element), options); - fabric.util.addClass(this.getElement(), fabric.Image.CSS_CANVAS); + this.setElement(fabric.document.getElementById(element) || element, options); }, /** diff --git a/src/shapes/object.class.ts b/src/shapes/object.class.ts index 8370146e374..7f95ed976ff 100644 --- a/src/shapes/object.class.ts +++ b/src/shapes/object.class.ts @@ -1,10 +1,11 @@ //@ts-nocheck - +import { cache } from '../cache'; import { config } from '../config'; import { VERSION } from '../constants'; import { Point } from '../point.class'; import { capValue } from '../util/misc/capValue'; import { pick } from '../util/misc/pick'; +import { runningAnimations } from '../util/animation_registry'; (function(global) { var fabric = global.fabric || (global.fabric = { }), @@ -684,10 +685,9 @@ import { pick } from '../util/misc/pick'; * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache */ _limitCacheSize: function(dims) { - var perfLimitSizeTotal = config.perfLimitSizeTotal, - width = dims.width, height = dims.height, + var width = dims.width, height = dims.height, max = config.maxCacheSideLimit, min = config.minCacheSideLimit; - if (width <= max && height <= max && width * height <= perfLimitSizeTotal) { + if (width <= max && height <= max && width * height <= config.perfLimitSizeTotal) { if (width < min) { dims.width = min; } @@ -696,9 +696,9 @@ import { pick } from '../util/misc/pick'; } return dims; } - var ar = width / height, limitedDims = fabric.util.limitDimsByArea(ar, perfLimitSizeTotal), - x = capValue(min, limitedDims.x, max), - y = capValue(min, limitedDims.y, max); + var ar = width / height, [limX, limY] = cache.limitDimsByArea(ar), + x = capValue(min, limX, max), + y = capValue(min, limY, max); if (width > x) { dims.zoomX /= width / x; dims.width = x; @@ -1904,8 +1904,10 @@ import { pick } from '../util/misc/pick'; * override if necessary to dispose artifacts such as `clipPath` */ dispose: function () { - if (fabric.runningAnimations) { - fabric.runningAnimations.cancelByTarget(this); + // todo verify this. + // runningAnimations is always truthy + if (runningAnimations) { + runningAnimations.cancelByTarget(this); } } }); diff --git a/src/static_canvas.class.ts b/src/static_canvas.class.ts index a2b65cc3ae9..ba2a0e934cb 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -310,14 +310,14 @@ import { pick } from './util/misc/pick'; this.lowerCanvasEl = canvasEl; } else { - this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(); + this.lowerCanvasEl = fabric.document.getElementById(canvasEl) || canvasEl || this._createCanvasElement(); } if (this.lowerCanvasEl.hasAttribute('data-fabric')) { /* _DEV_MODE_START_ */ throw new Error('fabric.js: trying to initialize a canvas that has already been initialized'); /* _DEV_MODE_END_ */ } - fabric.util.addClass(this.lowerCanvasEl, 'lower-canvas'); + this.lowerCanvasEl.classList.add('lower-canvas'); this.lowerCanvasEl.setAttribute('data-fabric', 'main'); if (this.interactive) { this._originalCanvasStyle = this.lowerCanvasEl.style.cssText; @@ -752,7 +752,7 @@ import { pick } from './util/misc/pick'; this.cancelRequestedRender(); this.calcViewportBoundaries(); this.clearContext(ctx); - fabric.util.setImageSmoothing(ctx, this.imageSmoothingEnabled); + ctx.imageSmoothingEnabled = this.imageSmoothingEnabled; ctx.patternQuality = 'best'; this.fire('before:render', { ctx: ctx, }); this._renderBackground(ctx); diff --git a/src/util/anim_ease.ts b/src/util/anim_ease.ts index 52d9c5187c5..0361060e644 100644 --- a/src/util/anim_ease.ts +++ b/src/util/anim_ease.ts @@ -1,399 +1,328 @@ -//@ts-nocheck -(function(global) { - var fabric = global.fabric; - function normalize(a, c, p, s) { - if (a < Math.abs(c)) { - a = c; - s = p / 4; +import { twoMathPi, halfPI } from "../constants"; + +type TEasingFunction = ( + currentTime: number, + startValue: number, + byValue: number, + duration: number) => number; + +/** + * Easing functions + * See Easing Equations by Robert Penner + * @namespace fabric.util.ease + */ + +const normalize = (a: number, c: number, p: number, s: number) => { + if (a < Math.abs(c)) { + a = c; + s = p / 4; + } + else { + //handle the 0/0 case: + if (c === 0 && a === 0) { + s = p / twoMathPi * Math.asin(1); } else { - //handle the 0/0 case: - if (c === 0 && a === 0) { - s = p / (2 * Math.PI) * Math.asin(1); - } - else { - s = p / (2 * Math.PI) * Math.asin(c / a); - } + s = p / twoMathPi * Math.asin(c / a); } - return { a: a, c: c, p: p, s: s }; - } - - function elastic(opts, t, d) { - return opts.a * - Math.pow(2, 10 * (t -= 1)) * - Math.sin( (t * d - opts.s) * (2 * Math.PI) / opts.p ); } - - /** - * Cubic easing out - * @memberOf fabric.util.ease - */ - function easeOutCubic(t, b, c, d) { - return c * ((t = t / d - 1) * t * t + 1) + b; + return { a, c, p, s }; +} + +const elastic = (a: number, s: number, p: number, t: number, d: number): number => a * + Math.pow(2, 10 * (t -= 1)) * + Math.sin((t * d - s) * twoMathPi / p); + +/** + * Cubic easing out + * @memberOf fabric.util.ease + */ +export const easeOutCubic:TEasingFunction = (t, b, c, d) => c * ((t /= d - 1) * t ** 2 + 1) + b; + +/** + * Cubic easing in and out + * @memberOf fabric.util.ease + */ +export const easeInOutCubic:TEasingFunction = (t, b, c, d) => { + t /= d / 2; + if (t < 1) { + return c / 2 * t ** 3 + b; } - - /** - * Cubic easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutCubic(t, b, c, d) { - t /= d / 2; - if (t < 1) { - return c / 2 * t * t * t + b; - } - return c / 2 * ((t -= 2) * t * t + 2) + b; + return c / 2 * ((t -= 2) * t ** 2 + 2) + b; +} + +/** + * Quartic easing in + * @memberOf fabric.util.ease + */ +export const easeInQuart:TEasingFunction = (t, b, c, d) => c * (t /= d) * t ** 3 + b; + +/** + * Quartic easing out + * @memberOf fabric.util.ease + */ +export const easeOutQuart:TEasingFunction = (t, b, c, d) => -c * ((t = t / d - 1) * t ** 3 - 1) + b; + +/** + * Quartic easing in and out + * @memberOf fabric.util.ease + */ +export const easeInOutQuart:TEasingFunction = (t, b, c, d) => { + t /= d / 2; + if (t < 1) { + return c / 2 * t ** 4 + b; } - - /** - * Quartic easing in - * @memberOf fabric.util.ease - */ - function easeInQuart(t, b, c, d) { - return c * (t /= d) * t * t * t + b; + return -c / 2 * ((t -= 2) * t ** 3 - 2) + b; +} + +/** + * Quintic easing in + * @memberOf fabric.util.ease + */ +export const easeInQuint:TEasingFunction = (t, b, c, d) => c * (t /= d) * t ** 4 + b; + +/** + * Quintic easing out + * @memberOf fabric.util.ease + */ +export const easeOutQuint:TEasingFunction = (t, b, c, d) => c * ((t /= d - 1) * t ** 4 + 1) + b; + +/** + * Quintic easing in and out + * @memberOf fabric.util.ease + */ +export const easeInOutQuint:TEasingFunction = (t, b, c, d) => { + t /= d / 2; + if (t < 1) { + return c / 2 * t ** 5 + b; } - - /** - * Quartic easing out - * @memberOf fabric.util.ease - */ - function easeOutQuart(t, b, c, d) { - return -c * ((t = t / d - 1) * t * t * t - 1) + b; - } - - /** - * Quartic easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutQuart(t, b, c, d) { - t /= d / 2; - if (t < 1) { - return c / 2 * t * t * t * t + b; - } - return -c / 2 * ((t -= 2) * t * t * t - 2) + b; - } - - /** - * Quintic easing in - * @memberOf fabric.util.ease - */ - function easeInQuint(t, b, c, d) { - return c * (t /= d) * t * t * t * t + b; - } - - /** - * Quintic easing out - * @memberOf fabric.util.ease - */ - function easeOutQuint(t, b, c, d) { - return c * ((t = t / d - 1) * t * t * t * t + 1) + b; + return c / 2 * ((t -= 2) * t ** 4 + 2) + b; +} + +/** + * Sinusoidal easing in + * @memberOf fabric.util.ease + */ +export const easeInSine:TEasingFunction = (t, b, c, d) => -c * Math.cos(t / d * halfPI) + c + b; + +/** + * Sinusoidal easing out + * @memberOf fabric.util.ease + */ +export const easeOutSine:TEasingFunction = (t, b, c, d) => c * Math.sin(t / d * halfPI) + b; + +/** + * Sinusoidal easing in and out + * @memberOf fabric.util.ease + */ +export const easeInOutSine:TEasingFunction = (t, b, c, d) => -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b; + +/** + * Exponential easing in + * @memberOf fabric.util.ease + */ +export const easeInExpo:TEasingFunction = (t, b, c, d) => (t === 0) ? b : c * 2 ** (10 * (t / d - 1)) + b; + +/** + * Exponential easing out + * @memberOf fabric.util.ease + */ +export const easeOutExpo:TEasingFunction = (t, b, c, d) => (t === d) ? b + c : c * -(2 ** (-10 * t / d) + 1) + b; + +/** + * Exponential easing in and out + * @memberOf fabric.util.ease + */ +export const easeInOutExpo:TEasingFunction = (t, b, c, d) => { + if (t === 0) { + return b; } - - /** - * Quintic easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutQuint(t, b, c, d) { - t /= d / 2; - if (t < 1) { - return c / 2 * t * t * t * t * t + b; - } - return c / 2 * ((t -= 2) * t * t * t * t + 2) + b; + if (t === d) { + return b + c; } - - /** - * Sinusoidal easing in - * @memberOf fabric.util.ease - */ - function easeInSine(t, b, c, d) { - return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; + t /= d / 2; + if (t < 1) { + return c / 2 * 2 ** (10 * (t - 1)) + b; } - - /** - * Sinusoidal easing out - * @memberOf fabric.util.ease - */ - function easeOutSine(t, b, c, d) { - return c * Math.sin(t / d * (Math.PI / 2)) + b; + return c / 2 * -(2 ** (-10 * --t) + 2) + b; +} + +/** + * Circular easing in + * @memberOf fabric.util.ease + */ +export const easeInCirc:TEasingFunction = (t, b, c, d) => -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b; + +/** + * Circular easing out + * @memberOf fabric.util.ease + */ +export const easeOutCirc:TEasingFunction = (t, b, c, d) => c * Math.sqrt(1 - (t = t / d - 1) * t) + b; + +/** + * Circular easing in and out + * @memberOf fabric.util.ease + */ +export const easeInOutCirc:TEasingFunction = (t, b, c, d) => { + t /= d / 2; + if (t < 1) { + return -c / 2 * (Math.sqrt(1 - t ** 2) - 1) + b; } - - /** - * Sinusoidal easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutSine(t, b, c, d) { - return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b; + return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b; +} + +/** + * Elastic easing in + * @memberOf fabric.util.ease + */ +export const easeInElastic:TEasingFunction = (t, b, c, d) => { + const s = 1.70158, a = c; + let p = 0; + if (t === 0) { + return b; } - - /** - * Exponential easing in - * @memberOf fabric.util.ease - */ - function easeInExpo(t, b, c, d) { - return (t === 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b; + t /= d; + if (t === 1) { + return b + c; } - - /** - * Exponential easing out - * @memberOf fabric.util.ease - */ - function easeOutExpo(t, b, c, d) { - return (t === d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b; + if (!p) { + p = d * 0.3; } - - /** - * Exponential easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutExpo(t, b, c, d) { - if (t === 0) { - return b; - } - if (t === d) { - return b + c; - } - t /= d / 2; - if (t < 1) { - return c / 2 * Math.pow(2, 10 * (t - 1)) + b; - } - return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b; + const { a: normA, s: normS, p: normP } = normalize(a, c, p, s); + return -elastic(normA, normS, normP, t, d) + b; +} + +/** + * Elastic easing out + * @memberOf fabric.util.ease + */ +export const easeOutElastic:TEasingFunction = (t, b, c, d) => { + const s = 1.70158, a = c; + let p = 0; + if (t === 0) { + return b; } - - /** - * Circular easing in - * @memberOf fabric.util.ease - */ - function easeInCirc(t, b, c, d) { - return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b; + t /= d; + if (t === 1) { + return b + c; } - - /** - * Circular easing out - * @memberOf fabric.util.ease - */ - function easeOutCirc(t, b, c, d) { - return c * Math.sqrt(1 - (t = t / d - 1) * t) + b; + if (!p) { + p = d * 0.3; } - - /** - * Circular easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutCirc(t, b, c, d) { - t /= d / 2; - if (t < 1) { - return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b; - } - return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b; + const { a: normA, s: normS, p: normP, c: normC } = normalize(a, c, p, s); + return normA * 2 ** (-10 * t) * Math.sin((t * d - normS) * (twoMathPi) / normP ) + normC + b; +} + +/** + * Elastic easing in and out + * @memberOf fabric.util.ease + */ +export const easeInOutElastic:TEasingFunction = (t, b, c, d) => { + const s = 1.70158, a = c; + let p = 0; + if (t === 0) { + return b; } - - /** - * Elastic easing in - * @memberOf fabric.util.ease - */ - function easeInElastic(t, b, c, d) { - var s = 1.70158, p = 0, a = c; - if (t === 0) { - return b; - } - t /= d; - if (t === 1) { - return b + c; - } - if (!p) { - p = d * 0.3; - } - var opts = normalize(a, c, p, s); - return -elastic(opts, t, d) + b; + t /= d / 2; + if (t === 2) { + return b + c; } - - /** - * Elastic easing out - * @memberOf fabric.util.ease - */ - function easeOutElastic(t, b, c, d) { - var s = 1.70158, p = 0, a = c; - if (t === 0) { - return b; - } - t /= d; - if (t === 1) { - return b + c; - } - if (!p) { - p = d * 0.3; - } - var opts = normalize(a, c, p, s); - return opts.a * Math.pow(2, -10 * t) * Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p ) + opts.c + b; + if (!p) { + p = d * (0.3 * 1.5); } - - /** - * Elastic easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutElastic(t, b, c, d) { - var s = 1.70158, p = 0, a = c; - if (t === 0) { - return b; - } - t /= d / 2; - if (t === 2) { - return b + c; - } - if (!p) { - p = d * (0.3 * 1.5); - } - var opts = normalize(a, c, p, s); - if (t < 1) { - return -0.5 * elastic(opts, t, d) + b; - } - return opts.a * Math.pow(2, -10 * (t -= 1)) * - Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p ) * 0.5 + opts.c + b; + const { a: normA, s: normS, p: normP, c: normC } = normalize(a, c, p, s); + if (t < 1) { + return -0.5 * elastic(normA, normS, normP, t, d) + b; } - - /** - * Backwards easing in - * @memberOf fabric.util.ease - */ - function easeInBack(t, b, c, d, s) { - if (s === undefined) { - s = 1.70158; - } - return c * (t /= d) * t * ((s + 1) * t - s) + b; + return normA * Math.pow(2, -10 * (t -= 1)) * + Math.sin((t * d - normS) * (twoMathPi) / normP ) * 0.5 + normC + b; +} + +/** + * Backwards easing in + * @memberOf fabric.util.ease + */ +export const easeInBack:TEasingFunction = (t, b, c, d, s = 1.70158) => c * (t /= d) * t * ((s + 1) * t - s) + b; + +/** + * Backwards easing out + * @memberOf fabric.util.ease + */ +export const easeOutBack:TEasingFunction = (t, b, c, d, s = 1.70158) => c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; + +/** + * Backwards easing in and out + * @memberOf fabric.util.ease + */ +export const easeInOutBack:TEasingFunction = (t, b, c, d, s = 1.70158) => { + t /= d / 2; + if (t < 1) { + return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b; } - - /** - * Backwards easing out - * @memberOf fabric.util.ease - */ - function easeOutBack(t, b, c, d, s) { - if (s === undefined) { - s = 1.70158; - } - return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; + return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b; +} + +/** + * Bouncing easing out + * @memberOf fabric.util.ease + */ +export const easeOutBounce:TEasingFunction = (t, b, c, d) => { + if ((t /= d) < (1 / 2.75)) { + return c * (7.5625 * t * t) + b; } - - /** - * Backwards easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutBack(t, b, c, d, s) { - if (s === undefined) { - s = 1.70158; - } - t /= d / 2; - if (t < 1) { - return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b; - } - return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b; + else if (t < (2 / 2.75)) { + return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b; } - - /** - * Bouncing easing in - * @memberOf fabric.util.ease - */ - function easeInBounce(t, b, c, d) { - return c - easeOutBounce (d - t, 0, c, d) + b; + else if (t < (2.5 / 2.75)) { + return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b; } - - /** - * Bouncing easing out - * @memberOf fabric.util.ease - */ - function easeOutBounce(t, b, c, d) { - if ((t /= d) < (1 / 2.75)) { - return c * (7.5625 * t * t) + b; - } - else if (t < (2 / 2.75)) { - return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b; - } - else if (t < (2.5 / 2.75)) { - return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b; - } - else { - return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b; - } + else { + return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b; } - - /** - * Bouncing easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutBounce(t, b, c, d) { - if (t < d / 2) { - return easeInBounce (t * 2, 0, c, d) * 0.5 + b; - } - return easeOutBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b; +} + +/** + * Bouncing easing in + * @memberOf fabric.util.ease + */ + export const easeInBounce:TEasingFunction = (t, b, c, d) => c - easeOutBounce(d - t, 0, c, d) + b; + +/** + * Bouncing easing in and out + * @memberOf fabric.util.ease + */ +export const easeInOutBounce:TEasingFunction = (t, b, c, d) => + t < d / 2 ? + easeInBounce(t * 2, 0, c, d) * 0.5 + b : + easeOutBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b; + + +/** + * Quadratic easing in + * @memberOf fabric.util.ease + */ +export const easeInQuad:TEasingFunction = (t, b, c, d) => c * (t /= d) * t + b; + +/** + * Quadratic easing out + * @memberOf fabric.util.ease + */ +export const easeOutQuad:TEasingFunction = (t, b, c, d) => -c * (t /= d) * (t - 2) + b; + +/** + * Quadratic easing in and out + * @memberOf fabric.util.ease + */ +export const easeInOutQuad:TEasingFunction = (t, b, c, d) => { + t /= (d / 2); + if (t < 1) { + return c / 2 * t ** 2 + b; } - - /** - * Easing functions - * See Easing Equations by Robert Penner - * @namespace fabric.util.ease - */ - fabric.util.ease = { - - /** - * Quadratic easing in - * @memberOf fabric.util.ease - */ - easeInQuad: function(t, b, c, d) { - return c * (t /= d) * t + b; - }, - - /** - * Quadratic easing out - * @memberOf fabric.util.ease - */ - easeOutQuad: function(t, b, c, d) { - return -c * (t /= d) * (t - 2) + b; - }, - - /** - * Quadratic easing in and out - * @memberOf fabric.util.ease - */ - easeInOutQuad: function(t, b, c, d) { - t /= (d / 2); - if (t < 1) { - return c / 2 * t * t + b; - } - return -c / 2 * ((--t) * (t - 2) - 1) + b; - }, - - /** - * Cubic easing in - * @memberOf fabric.util.ease - */ - easeInCubic: function(t, b, c, d) { - return c * (t /= d) * t * t + b; - }, - - easeOutCubic: easeOutCubic, - easeInOutCubic: easeInOutCubic, - easeInQuart: easeInQuart, - easeOutQuart: easeOutQuart, - easeInOutQuart: easeInOutQuart, - easeInQuint: easeInQuint, - easeOutQuint: easeOutQuint, - easeInOutQuint: easeInOutQuint, - easeInSine: easeInSine, - easeOutSine: easeOutSine, - easeInOutSine: easeInOutSine, - easeInExpo: easeInExpo, - easeOutExpo: easeOutExpo, - easeInOutExpo: easeInOutExpo, - easeInCirc: easeInCirc, - easeOutCirc: easeOutCirc, - easeInOutCirc: easeInOutCirc, - easeInElastic: easeInElastic, - easeOutElastic: easeOutElastic, - easeInOutElastic: easeInOutElastic, - easeInBack: easeInBack, - easeOutBack: easeOutBack, - easeInOutBack: easeInOutBack, - easeInBounce: easeInBounce, - easeOutBounce: easeOutBounce, - easeInOutBounce: easeInOutBounce - }; - -})(typeof exports !== 'undefined' ? exports : window); + return -c / 2 * ((--t) * (t - 2) - 1) + b; +}; + +/** + * Cubic easing in + * @memberOf fabric.util.ease + */ +export const easeInCubic:TEasingFunction = (t, b, c, d) => c * (t /= d) * t * t + b; diff --git a/src/util/animate.ts b/src/util/animate.ts index c8ecf56efef..4bb4d0b7a73 100644 --- a/src/util/animate.ts +++ b/src/util/animate.ts @@ -1,267 +1,174 @@ //@ts-nocheck -import { extend } from './lang_object'; - -(function (global) { - /** - * - * @typedef {Object} AnimationOptions - * Animation of a value or list of values. - * @property {Function} [onChange] Callback; invoked on every value change - * @property {Function} [onComplete] Callback; invoked when value change is completed - * @property {number | number[]} [startValue=0] Starting value - * @property {number | number[]} [endValue=100] Ending value - * @property {number | number[]} [byValue=100] Value to modify the property by - * @property {Function} [easing] Easing function - * @property {number} [duration=500] Duration of change (in ms) - * @property {Function} [abort] Additional function with logic. If returns true, animation aborts. - * @property {number} [delay] Delay of animation start (in ms) - * - * @typedef {() => void} CancelFunction - * - * @typedef {Object} AnimationCurrentState - * @property {number | number[]} currentValue value in range [`startValue`, `endValue`] - * @property {number} completionRate value in range [0, 1] - * @property {number} durationRate value in range [0, 1] - * - * @typedef {(AnimationOptions & AnimationCurrentState & { cancel: CancelFunction }} AnimationContext - */ - - /** - * Array holding all running animations - * @memberof fabric - * @type {AnimationContext[]} - */ - var fabric = global.fabric, RUNNING_ANIMATIONS = []; - extend(RUNNING_ANIMATIONS, { - - /** - * cancel all running animations at the next requestAnimFrame - * @returns {AnimationContext[]} - */ - cancelAll: function () { - var animations = this.splice(0); - animations.forEach(function (animation) { - animation.cancel(); - }); - return animations; - }, - - /** - * cancel all running animations attached to canvas at the next requestAnimFrame - * @param {fabric.Canvas} canvas - * @returns {AnimationContext[]} - */ - cancelByCanvas: function (canvas) { - if (!canvas) { - return []; +import { fabric } from '../../HEADER'; +import { runningAnimations } from './animation_registry'; +import { noop } from '../constants'; + +/** + * + * @typedef {Object} AnimationOptions + * Animation of a value or list of values. + * @property {Function} [onChange] Callback; invoked on every value change + * @property {Function} [onComplete] Callback; invoked when value change is completed + * @property {number | number[]} [startValue=0] Starting value + * @property {number | number[]} [endValue=100] Ending value + * @property {number | number[]} [byValue=100] Value to modify the property by + * @property {Function} [easing] Easing function + * @property {number} [duration=500] Duration of change (in ms) + * @property {Function} [abort] Additional function with logic. If returns true, animation aborts. + * @property {number} [delay] Delay of animation start (in ms) + * + * @typedef {() => void} CancelFunction + * + * @typedef {Object} AnimationCurrentState + * @property {number | number[]} currentValue value in range [`startValue`, `endValue`] + * @property {number} completionRate value in range [0, 1] + * @property {number} durationRate value in range [0, 1] + * + * @typedef {(AnimationOptions & AnimationCurrentState & { cancel: CancelFunction }} AnimationContext + */ + +const defaultEasing = (t, b, c, d) => -c * Math.cos(t / d * (Math.PI / 2)) + c + b; + +/** + * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. + * @memberOf fabric.util + * @param {AnimationOptions} [options] Animation options + * When using lists, think of something like this: + * @example + * fabric.util.animate({ + * startValue: [1, 2, 3], + * endValue: [2, 4, 6], + * onChange: function([x, y, zoom]) { + * canvas.zoomToPoint(new Point(x, y), zoom); + * canvas.requestRenderAll(); + * } + * }); + * + * @example + * fabric.util.animate({ + * startValue: 1, + * endValue: 0, + * onChange: function(v) { + * obj.set('opacity', v); + * canvas.requestRenderAll(); + * } + * }); + * + * @returns {CancelFunction} cancel function + */ +export function animate(options = {}) { + let cancel = false; + + const { + startValue = 0, + duration = 500, + easing = defaultEasing, + onChange = noop, + abort = noop, + onComplete = noop, + endValue = 100, + delay = 0, + } = options; + + const context = { + ...options, + currentValue: startValue, + completionRate: 0, + durationRate: 0 + }; + + const removeFromRegistry = () => { + const index = runningAnimations.indexOf(context); + return index > -1 && runningAnimations.splice(index, 1)[0]; + }; + + context.cancel = function () { + cancel = true; + return removeFromRegistry(); + }; + runningAnimations.push(context); + + const runner = function (timestamp) { + const start = timestamp || +new Date(), + finish = start + duration, + isMany = Array.isArray(startValue), + byValue = options.byValue || ( + isMany ? + startValue.map((value, i) => endValue[i] - value) + : endValue - startValue + ); + + options.onStart && options.onStart(); + + (function tick(ticktime) { + const time = ticktime || +new Date(); + const currentTime = time > finish ? duration : (time - start), + timePerc = currentTime / duration, + current = isMany ? + startValue.map( + (_value, i) => easing(currentTime, _value, byValue[i], duration) + ) : easing(currentTime, startValue, byValue, duration), + valuePerc = isMany ? Math.abs((current[0] - startValue[0]) / byValue[0]) + : Math.abs((current - startValue) / byValue); + // update context + context.currentValue = isMany ? current.slice() : current; + context.completionRate = valuePerc; + context.durationRate = timePerc; + + if (cancel) { + return; } - var cancelled = this.filter(function (animation) { - return typeof animation.target === 'object' && animation.target.canvas === canvas; - }); - cancelled.forEach(function (animation) { - animation.cancel(); - }); - return cancelled; - }, - - /** - * cancel all running animations for target at the next requestAnimFrame - * @param {*} target - * @returns {AnimationContext[]} - */ - cancelByTarget: function (target) { - var cancelled = this.findAnimationsByTarget(target); - cancelled.forEach(function (animation) { - animation.cancel(); - }); - return cancelled; - }, - - /** - * - * @param {CancelFunction} cancelFunc the function returned by animate - * @returns {number} - */ - findAnimationIndex: function (cancelFunc) { - return this.indexOf(this.findAnimation(cancelFunc)); - }, - - /** - * - * @param {CancelFunction} cancelFunc the function returned by animate - * @returns {AnimationContext | undefined} animation's options object - */ - findAnimation: function (cancelFunc) { - return this.find(function (animation) { - return animation.cancel === cancelFunc; - }); - }, - - /** - * - * @param {*} target the object that is assigned to the target property of the animation context - * @returns {AnimationContext[]} array of animation options object associated with target - */ - findAnimationsByTarget: function (target) { - if (!target) { - return []; + if (abort(current, valuePerc, timePerc)) { + removeFromRegistry(); + return; } - return this.filter(function (animation) { - return animation.target === target; - }); - } - }); - - function noop() { - return false; - } - - function defaultEasing(t, b, c, d) { - return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; - } - - /** - * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. - * @memberOf fabric.util - * @param {AnimationOptions} [options] Animation options - * When using lists, think of something like this: - * @example - * fabric.util.animate({ - * startValue: [1, 2, 3], - * endValue: [2, 4, 6], - * onChange: function([x, y, zoom]) { - * canvas.zoomToPoint(new Point(x, y), zoom); - * canvas.requestRenderAll(); - * } - * }); - * - * @example - * fabric.util.animate({ - * startValue: 1, - * endValue: 0, - * onChange: function(v) { - * obj.set('opacity', v); - * canvas.requestRenderAll(); - * } - * }); - * - * @returns {CancelFunction} cancel function - */ - function animate(options) { - options || (options = {}); - var cancel = false, - context, - removeFromRegistry = function () { - var index = fabric.runningAnimations.indexOf(context); - return index > -1 && fabric.runningAnimations.splice(index, 1)[0]; - }; - - context = Object.assign({}, options, { - cancel: function () { - cancel = true; - return removeFromRegistry(); - }, - currentValue: 'startValue' in options ? options.startValue : 0, - completionRate: 0, - durationRate: 0 - }); - fabric.runningAnimations.push(context); - - var runner = function (timestamp) { - var start = timestamp || +new Date(), - duration = options.duration || 500, - finish = start + duration, time, - onChange = options.onChange || noop, - abort = options.abort || noop, - onComplete = options.onComplete || noop, - easing = options.easing || defaultEasing, - isMany = 'startValue' in options ? options.startValue.length > 0 : false, - startValue = 'startValue' in options ? options.startValue : 0, - endValue = 'endValue' in options ? options.endValue : 100, - byValue = options.byValue || (isMany ? startValue.map(function(value, i) { - return endValue[i] - startValue[i]; - }) : endValue - startValue); - - options.onStart && options.onStart(); - - (function tick(ticktime) { - time = ticktime || +new Date(); - var currentTime = time > finish ? duration : (time - start), - timePerc = currentTime / duration, - current = isMany ? startValue.map(function(_value, i) { - return easing(currentTime, startValue[i], byValue[i], duration); - }) : easing(currentTime, startValue, byValue, duration), - valuePerc = isMany ? Math.abs((current[0] - startValue[0]) / byValue[0]) - : Math.abs((current - startValue) / byValue); + if (time > finish) { // update context - context.currentValue = isMany ? current.slice() : current; - context.completionRate = valuePerc; - context.durationRate = timePerc; - if (cancel) { - return; - } - if (abort(current, valuePerc, timePerc)) { - removeFromRegistry(); - return; - } - if (time > finish) { - // update context - context.currentValue = isMany ? endValue.slice() : endValue; - context.completionRate = 1; - context.durationRate = 1; - // execute callbacks - onChange(isMany ? endValue.slice() : endValue, 1, 1); - onComplete(endValue, 1, 1); - removeFromRegistry(); - return; - } - else { - onChange(current, valuePerc, timePerc); - requestAnimFrame(tick); - } - })(start); - }; - - if (options.delay) { - setTimeout(function () { - requestAnimFrame(runner); - }, options.delay); - } - else { - requestAnimFrame(runner); - } - - return context.cancel; - } - - var _requestAnimFrame = fabric.window.requestAnimationFrame || - fabric.window.webkitRequestAnimationFrame || - fabric.window.mozRequestAnimationFrame || - fabric.window.oRequestAnimationFrame || - fabric.window.msRequestAnimationFrame || - function(callback) { - return fabric.window.setTimeout(callback, 1000 / 60); - }; - - var _cancelAnimFrame = fabric.window.cancelAnimationFrame || fabric.window.clearTimeout; - - /** - * requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/ - * In order to get a precise start time, `requestAnimFrame` should be called as an entry into the method - * @memberOf fabric.util - * @param {Function} callback Callback to invoke - * @param {DOMElement} element optional Element to associate with animation - */ - function requestAnimFrame() { - return _requestAnimFrame.apply(fabric.window, arguments); - } + context.currentValue = isMany ? endValue.slice() : endValue; + context.completionRate = 1; + context.durationRate = 1; + // execute callbacks + onChange(isMany ? endValue.slice() : endValue, 1, 1); + onComplete(endValue, 1, 1); + removeFromRegistry(); + return; + } + else { + onChange(current, valuePerc, timePerc); + requestAnimFrame(tick); + } + })(start); + }; - function cancelAnimFrame() { - return _cancelAnimFrame.apply(fabric.window, arguments); + if (delay > 0 ) { + setTimeout(() => requestAnimFrame(runner), delay); + } else { + requestAnimFrame(runner); } - fabric.util.animate = animate; - fabric.util.requestAnimFrame = requestAnimFrame; - fabric.util.cancelAnimFrame = cancelAnimFrame; - fabric.runningAnimations = RUNNING_ANIMATIONS; -})(typeof exports !== 'undefined' ? exports : window); + return context.cancel; +} + +const _requestAnimFrame = + fabric.window.requestAnimationFrame || + function(callback) { + return fabric.window.setTimeout(callback, 1000 / 60); + }; + +const _cancelAnimFrame = + fabric.window.cancelAnimationFrame || fabric.window.clearTimeout; + +/** + * requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + * In order to get a precise start time, `requestAnimFrame` should be called as an entry into the method + * @memberOf fabric.util + * @param {Function} callback Callback to invoke + * @param {DOMElement} element optional Element to associate with animation + */ +export function requestAnimFrame(...args) { + return _requestAnimFrame.apply(fabric.window, args); +} + +export function cancelAnimFrame(...args) { + return _cancelAnimFrame.apply(fabric.window, args); +} diff --git a/src/util/animate_color.ts b/src/util/animate_color.ts index 9fc25124630..92772b2da81 100644 --- a/src/util/animate_color.ts +++ b/src/util/animate_color.ts @@ -1,79 +1,84 @@ //@ts-nocheck - import { Color } from "../color"; +import { animate } from './animate'; -(function(global) { - var fabric = global.fabric; - // Calculate an in-between color. Returns a "rgba()" string. - // Credit: Edwin Martin - // http://www.bitstorm.org/jquery/color-animation/jquery.animate-colors.js - function calculateColor(begin, end, pos) { - var color = 'rgba(' - + parseInt((begin[0] + pos * (end[0] - begin[0])), 10) + ',' - + parseInt((begin[1] + pos * (end[1] - begin[1])), 10) + ',' - + parseInt((begin[2] + pos * (end[2] - begin[2])), 10); +// Calculate an in-between color. Returns a "rgba()" string. +// Credit: Edwin Martin +// http://www.bitstorm.org/jquery/color-animation/jquery.animate-colors.js +// const calculateColor = (begin: number[], end: number[], pos) => { +// const [r, g, b, _a] = begin.map((beg, index) => beg + pos * (end[index] - beg)); +// const a = begin && end ? parseFloat(_a) : 1; +// return `rgba(${parseInt(r, 10)},${parseInt(g, 10)},${parseInt(b, 10)},${a})`; +// } - color += ',' + (begin && end ? parseFloat(begin[3] + pos * (end[3] - begin[3])) : 1); - color += ')'; - return color; - } +// color animation is broken. This function pass the tests for some reasons +// but begin and end aren't array anymore since we improved animate function +// to handler arrays internally. +function calculateColor(begin, end, pos) { + let color = 'rgba(' + + parseInt((begin[0] + pos * (end[0] - begin[0])), 10) + ',' + + parseInt((begin[1] + pos * (end[1] - begin[1])), 10) + ',' + + parseInt((begin[2] + pos * (end[2] - begin[2])), 10); - /** - * Changes the color from one to another within certain period of time, invoking callbacks as value is being changed. - * @memberOf fabric.util - * @param {String} fromColor The starting color in hex or rgb(a) format. - * @param {String} toColor The starting color in hex or rgb(a) format. - * @param {Number} [duration] Duration of change (in ms). - * @param {Object} [options] Animation options - * @param {Function} [options.onChange] Callback; invoked on every value change - * @param {Function} [options.onComplete] Callback; invoked when value change is completed - * @param {Function} [options.colorEasing] Easing function. Note that this function only take two arguments (currentTime, duration). Thus the regular animation easing functions cannot be used. - * @param {Function} [options.abort] Additional function with logic. If returns true, onComplete is called. - * @returns {Function} abort function - */ - function animateColor(fromColor, toColor, duration, options) { - var startColor = new Color(fromColor).getSource(), - endColor = new Color(toColor).getSource(), - originalOnComplete = options.onComplete, - originalOnChange = options.onChange; - options = options || {}; + color += ',' + (begin && end ? parseFloat(begin[3] + pos * (end[3] - begin[3])) : 1); + color += ')'; + return color; +} + +const defaultColorEasing = (currentTime, duration) => 1 - Math.cos(currentTime / duration * (Math.PI / 2)); - return fabric.util.animate(Object.assign(options, { - duration: duration || 500, - startValue: startColor, - endValue: endColor, - byValue: endColor, - easing: function (currentTime, startValue, byValue, duration) { - var posValue = options.colorEasing - ? options.colorEasing(currentTime, duration) - : 1 - Math.cos(currentTime / duration * (Math.PI / 2)); - return calculateColor(startValue, byValue, posValue); - }, - // has to take in account for color restoring; - onComplete: function(current, valuePerc, timePerc) { - if (originalOnComplete) { - return originalOnComplete( - calculateColor(endColor, endColor, 0), +/** + * Changes the color from one to another within certain period of time, invoking callbacks as value is being changed. + * @memberOf fabric.util + * @param {String} fromColor The starting color in hex or rgb(a) format. + * @param {String} toColor The starting color in hex or rgb(a) format. + * @param {Number} [duration] Duration of change (in ms). + * @param {Object} [options] Animation options + * @param {Function} [options.onChange] Callback; invoked on every value change + * @param {Function} [options.onComplete] Callback; invoked when value change is completed + * @param {Function} [options.colorEasing] Easing function. Note that this function only take two arguments (currentTime, duration). Thus the regular animation easing functions cannot be used. + * @param {Function} [options.abort] Additional function with logic. If returns true, onComplete is called. + * @returns {Function} abort function + */ +export function animateColor( + fromColor, + toColor, + duration = 500, + { + colorEasing = defaultColorEasing, + onComplete, + onChange, + ...restOfOptions + } = {} +) { + const startColor = new Color(fromColor).getSource(), + endColor = new Color(toColor).getSource(), + return animate({ + ...restOfOptions, + duration, + startValue: startColor, + endValue: endColor, + byValue: endColor, + easing: (currentTime, startValue, byValue, duration) => + calculateColor(startValue, byValue, colorEasing(currentTime, duration)), + // has to take in account for color restoring; + onComplete: (current, valuePerc, timePerc) => onComplete?.( + calculateColor(endColor, endColor, 0), + valuePerc, + timePerc + ), + onChange: (current, valuePerc, timePerc) => { + if (onChange) { + if (Array.isArray(current)) { + return onChange( + calculateColor(current, current, 0), valuePerc, timePerc ); } - }, - onChange: function(current, valuePerc, timePerc) { - if (originalOnChange) { - if (Array.isArray(current)) { - return originalOnChange( - calculateColor(current, current, 0), - valuePerc, - timePerc - ); - } - originalOnChange(current, valuePerc, timePerc); - } + onChange(current, valuePerc, timePerc); } - })); - } - - fabric.util.animateColor = animateColor; + } + }); +} -})(typeof exports !== 'undefined' ? exports : window); diff --git a/src/util/animation_registry.ts b/src/util/animation_registry.ts new file mode 100644 index 00000000000..215eefdc487 --- /dev/null +++ b/src/util/animation_registry.ts @@ -0,0 +1,81 @@ +//@ts-nocheck +import { fabric } from '../../HEADER'; + +/** + * Array holding all running animations + * @memberof fabric + * @type {AnimationContext[]} + */ +class RunningAnimations extends Array { + /** + * cancel all running animations at the next requestAnimFrame + * @returns {AnimationContext[]} + */ + cancelAll(): any[] { + const animations = this.splice(0); + animations.forEach((animation) => animation.cancel()); + return animations; + } + + /** + * cancel all running animations attached to canvas at the next requestAnimFrame + * @param {fabric.Canvas} canvas + * @returns {AnimationContext[]} + */ + cancelByCanvas(canvas: any) { + if (!canvas) { + return []; + } + const cancelled = this.filter( + (animation) => typeof animation.target === 'object' && animation.target.canvas === canvas + ); + cancelled.forEach((animation) => animation.cancel()); + return cancelled; + } + + /** + * cancel all running animations for target at the next requestAnimFrame + * @param {*} target + * @returns {AnimationContext[]} + */ + cancelByTarget(target) { + const cancelled = this.findAnimationsByTarget(target); + cancelled.forEach((animation) => animation.cancel()); + return cancelled; + } + + /** + * + * @param {CancelFunction} cancelFunc the function returned by animate + * @returns {number} + */ + findAnimationIndex(cancelFunc) { + return this.indexOf(this.findAnimation(cancelFunc)); + } + + /** + * + * @param {CancelFunction} cancelFunc the function returned by animate + * @returns {AnimationContext | undefined} animation's options object + */ + findAnimation(cancelFunc) { + return this.find((animation) => animation.cancel === cancelFunc); + } + + /** + * + * @param {*} target the object that is assigned to the target property of the animation context + * @returns {AnimationContext[]} array of animation options object associated with target + */ + findAnimationsByTarget(target) { + if (!target) { + return []; + } + return this.filter((animation) => animation.target === target); + } +} + +export const runningAnimations = new RunningAnimations(); + +fabric.runningAnimations = runningAnimations; + diff --git a/src/util/dom_misc.ts b/src/util/dom_misc.ts index 2b9f2b3ed86..cd6ac1cddf3 100644 --- a/src/util/dom_misc.ts +++ b/src/util/dom_misc.ts @@ -1,152 +1,72 @@ //@ts-nocheck -(function(global) { - - var fabric = global.fabric, _slice = Array.prototype.slice; - - /** - * Takes id and returns an element with that id (if one exists in a document) - * @memberOf fabric.util - * @param {String|HTMLElement} id - * @return {HTMLElement|null} - */ - function getById(id) { - return typeof id === 'string' ? fabric.document.getElementById(id) : id; - } - - var sliceCanConvertNodelists, - /** - * Converts an array-like object (e.g. arguments or NodeList) to an array - * @memberOf fabric.util - * @param {Object} arrayLike - * @return {Array} - */ - toArray = function(arrayLike) { - return _slice.call(arrayLike, 0); - }; - - try { - sliceCanConvertNodelists = toArray(fabric.document.childNodes) instanceof Array; - } - catch (err) { } - - if (!sliceCanConvertNodelists) { - toArray = function(arrayLike) { - var arr = new Array(arrayLike.length), i = arrayLike.length; - while (i--) { - arr[i] = arrayLike[i]; - } - return arr; - }; - } - - /** - * Creates specified element with specified attributes - * @memberOf fabric.util - * @param {String} tagName Type of an element to create - * @param {Object} [attributes] Attributes to set on an element - * @return {HTMLElement} Newly created element - */ - function makeElement(tagName, attributes) { - var el = fabric.document.createElement(tagName); - for (var prop in attributes) { - if (prop === 'class') { - el.className = attributes[prop]; - } - else if (prop === 'for') { - el.htmlFor = attributes[prop]; - } - else { - el.setAttribute(prop, attributes[prop]); - } - } - return el; - } - - /** - * Adds class to an element - * @memberOf fabric.util - * @param {HTMLElement} element Element to add class to - * @param {String} className Class to add to an element - */ - function addClass(element, className) { - if (element && (' ' + element.className + ' ').indexOf(' ' + className + ' ') === -1) { - element.className += (element.className ? ' ' : '') + className; - } - } - - /** - * Wraps element with another element - * @memberOf fabric.util - * @param {HTMLElement} element Element to wrap - * @param {HTMLElement|String} wrapper Element to wrap with - * @param {Object} [attributes] Attributes to set on a wrapper - * @return {HTMLElement} wrapper - */ - function wrapElement(element, wrapper, attributes) { - if (typeof wrapper === 'string') { - wrapper = makeElement(wrapper, attributes); - } - if (element.parentNode) { - element.parentNode.replaceChild(wrapper, element); - } - wrapper.appendChild(element); - return wrapper; +/** + * Wraps element with another element + * @memberOf fabric.util + * @param {HTMLElement} element Element to wrap + * @param {HTMLElement|String} wrapper Element to wrap with + * @param {Object} [attributes] Attributes to set on a wrapper + * @return {HTMLElement} wrapper + */ +export function wrapElement(element, wrapper) { + if (element.parentNode) { + element.parentNode.replaceChild(wrapper, element); } - - /** - * Returns element scroll offsets - * @memberOf fabric.util - * @param {HTMLElement} element Element to operate on - * @return {Object} Object with left/top values - */ - function getScrollLeftTop(element) { - - var left = 0, - top = 0, - docElement = fabric.document.documentElement, + wrapper.appendChild(element); + return wrapper; +} + +/** + * Returns element scroll offsets + * @memberOf fabric.util + * @param {HTMLElement} element Element to operate on + * @return {Object} Object with left/top values + */ +export function getScrollLeftTop(element) { + + let left = 0, + top = 0; + + const docElement = fabric.document.documentElement, body = fabric.document.body || { scrollLeft: 0, scrollTop: 0 }; - - // While loop checks (and then sets element to) .parentNode OR .host - // to account for ShadowDOM. We still want to traverse up out of ShadowDOM, - // but the .parentNode of a root ShadowDOM node will always be null, instead - // it should be accessed through .host. See http://stackoverflow.com/a/24765528/4383938 - while (element && (element.parentNode || element.host)) { - - // Set element to element parent, or 'host' in case of ShadowDOM - element = element.parentNode || element.host; - - if (element === fabric.document) { - left = body.scrollLeft || docElement.scrollLeft || 0; - top = body.scrollTop || docElement.scrollTop || 0; - } - else { - left += element.scrollLeft || 0; - top += element.scrollTop || 0; - } - - if (element.nodeType === 1 && element.style.position === 'fixed') { - break; - } + // While loop checks (and then sets element to) .parentNode OR .host + // to account for ShadowDOM. We still want to traverse up out of ShadowDOM, + // but the .parentNode of a root ShadowDOM node will always be null, instead + // it should be accessed through .host. See http://stackoverflow.com/a/24765528/4383938 + while (element && (element.parentNode || element.host)) { + + // Set element to element parent, or 'host' in case of ShadowDOM + element = element.parentNode || element.host; + + if (element === fabric.document) { + left = body.scrollLeft || docElement.scrollLeft || 0; + top = body.scrollTop || docElement.scrollTop || 0; + } + else { + left += element.scrollLeft || 0; + top += element.scrollTop || 0; } - return { left: left, top: top }; + if (element.nodeType === 1 && element.style.position === 'fixed') { + break; + } } - /** - * Returns offset for a given element - * @function - * @memberOf fabric.util - * @param {HTMLElement} element Element to get offset for - * @return {Object} Object with "left" and "top" properties - */ - function getElementOffset(element) { - var docElem, - doc = element && element.ownerDocument, - box = { left: 0, top: 0 }, + return { left, top }; +} + +/** + * Returns offset for a given element + * @function + * @memberOf fabric.util + * @param {HTMLElement} element Element to get offset for + * @return {Object} Object with "left" and "top" properties + */ +export function getElementOffset(element) { + let box = { left: 0, top: 0 }; + const doc = element && element.ownerDocument, offset = { left: 0, top: 0 }, - scrollLeftTop, offsetAttributes = { borderLeftWidth: 'left', borderTopWidth: 'top', @@ -154,148 +74,71 @@ paddingTop: 'top' }; - if (!doc) { - return offset; - } - - for (var attr in offsetAttributes) { - offset[offsetAttributes[attr]] += parseInt(getElementStyle(element, attr), 10) || 0; - } - - docElem = doc.documentElement; - if ( typeof element.getBoundingClientRect !== 'undefined' ) { - box = element.getBoundingClientRect(); - } - - scrollLeftTop = getScrollLeftTop(element); - - return { - left: box.left + scrollLeftTop.left - (docElem.clientLeft || 0) + offset.left, - top: box.top + scrollLeftTop.top - (docElem.clientTop || 0) + offset.top - }; - } - - /** - * Returns style attribute value of a given element - * @memberOf fabric.util - * @param {HTMLElement} element Element to get style attribute for - * @param {String} attr Style attribute to get for element - * @return {String} Style attribute value of the given element. - */ - var getElementStyle; - if (fabric.document.defaultView && fabric.document.defaultView.getComputedStyle) { - getElementStyle = function(element, attr) { - var style = fabric.document.defaultView.getComputedStyle(element, null); - return style ? style[attr] : undefined; - }; + if (!doc) { + return offset; } - else { - getElementStyle = function(element, attr) { - var value = element.style[attr]; - if (!value && element.currentStyle) { - value = element.currentStyle[attr]; - } - return value; - }; + const elemStyle = fabric.document.defaultView.getComputedStyle(element, null) + for (const attr in offsetAttributes) { + offset[offsetAttributes[attr]] += parseInt(elemStyle[attr], 10) || 0; } - (function () { - var style = fabric.document.documentElement.style, - selectProp = 'userSelect' in style - ? 'userSelect' - : 'MozUserSelect' in style - ? 'MozUserSelect' - : 'WebkitUserSelect' in style - ? 'WebkitUserSelect' - : 'KhtmlUserSelect' in style - ? 'KhtmlUserSelect' - : ''; - - /** - * Makes element unselectable - * @memberOf fabric.util - * @param {HTMLElement} element Element to make unselectable - * @return {HTMLElement} Element that was passed in - */ - function makeElementUnselectable(element) { - if (typeof element.onselectstart !== 'undefined') { - element.onselectstart = () => false; - } - if (selectProp) { - element.style[selectProp] = 'none'; - } - else if (typeof element.unselectable === 'string') { - element.unselectable = 'on'; - } - return element; - } - - /** - * Makes element selectable - * @memberOf fabric.util - * @param {HTMLElement} element Element to make selectable - * @return {HTMLElement} Element that was passed in - */ - function makeElementSelectable(element) { - if (typeof element.onselectstart !== 'undefined') { - element.onselectstart = null; - } - if (selectProp) { - element.style[selectProp] = ''; - } - else if (typeof element.unselectable === 'string') { - element.unselectable = ''; - } - return element; - } + const docElem = doc.documentElement; + if ( typeof element.getBoundingClientRect !== 'undefined' ) { + box = element.getBoundingClientRect(); + } - fabric.util.makeElementUnselectable = makeElementUnselectable; - fabric.util.makeElementSelectable = makeElementSelectable; - })(typeof exports !== 'undefined' ? exports : window); + const scrollLeftTop = getScrollLeftTop(element); - function getNodeCanvas(element) { - var impl = fabric.jsdomImplForWrapper(element); - return impl._canvas || impl._image; + return { + left: box.left + scrollLeftTop.left - (docElem.clientLeft || 0) + offset.left, + top: box.top + scrollLeftTop.top - (docElem.clientTop || 0) + offset.top }; - - function cleanUpJsdomNode(element) { - if (!fabric.isLikelyNode) { - return; - } - var impl = fabric.jsdomImplForWrapper(element); - if (impl) { - impl._image = null; - impl._canvas = null; - // unsure if necessary - impl._currentSrc = null; - impl._attributes = null; - impl._classList = null; - } +} + +/** + * Makes element unselectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make unselectable + * @return {HTMLElement} Element that was passed in + */ +export function makeElementUnselectable(element) { + if (typeof element.onselectstart !== 'undefined') { + element.onselectstart = () => false; } - - function setImageSmoothing(ctx, value) { - ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled || ctx.webkitImageSmoothingEnabled - || ctx.mozImageSmoothingEnabled || ctx.msImageSmoothingEnabled || ctx.oImageSmoothingEnabled; - ctx.imageSmoothingEnabled = value; + element.style.userSelect = 'none'; + return element; +} + +/** + * Makes element selectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make selectable + * @return {HTMLElement} Element that was passed in + */ +export function makeElementSelectable(element) { + if (typeof element.onselectstart !== 'undefined') { + element.onselectstart = null; } - - /** - * setImageSmoothing sets the context imageSmoothingEnabled property. - * Used by canvas and by ImageObject. - * @memberOf fabric.util - * @since 4.0.0 - * @param {HTMLRenderingContext2D} ctx to set on - * @param {Boolean} value true or false - */ - fabric.util.setImageSmoothing = setImageSmoothing; - fabric.util.getById = getById; - fabric.util.toArray = toArray; - fabric.util.addClass = addClass; - fabric.util.makeElement = makeElement; - fabric.util.wrapElement = wrapElement; - fabric.util.getScrollLeftTop = getScrollLeftTop; - fabric.util.getElementOffset = getElementOffset; - fabric.util.getNodeCanvas = getNodeCanvas; - fabric.util.cleanUpJsdomNode = cleanUpJsdomNode; - -})(typeof exports !== 'undefined' ? exports : window); + element.style.userSelect = ''; + return element; +} + +export function getNodeCanvas(element) { + const impl = fabric.jsdomImplForWrapper(element); + return impl._canvas || impl._image; +}; + +export function cleanUpJsdomNode(element) { + if (!fabric.isLikelyNode) { + return; + } + const impl = fabric.jsdomImplForWrapper(element); + if (impl) { + impl._image = null; + impl._canvas = null; + // unsure if necessary + impl._currentSrc = null; + impl._attributes = null; + impl._classList = null; + } +} diff --git a/src/util/lang_class.ts b/src/util/lang_class.ts index 43c1067a002..27fe7839e93 100644 --- a/src/util/lang_class.ts +++ b/src/util/lang_class.ts @@ -1,97 +1,90 @@ //@ts-nocheck -(function(global) { +import { noop } from '../constants'; - var fabric = global.fabric, slice = Array.prototype.slice, emptyFunction = function() { }, - /** @ignore */ - addMethods = function(klass, source, parent) { - for (var property in source) { +function addMethods(klass, source, parent) { + for (var property in source) { - if (property in klass.prototype && - typeof klass.prototype[property] === 'function' && - (source[property] + '').indexOf('callSuper') > -1) { + if (property in klass.prototype && + typeof klass.prototype[property] === 'function' && + (source[property] + '').indexOf('callSuper') > -1) { - klass.prototype[property] = (function(property) { - return function() { + klass.prototype[property] = (function(property) { + return function(...args) { - var superclass = this.constructor.superclass; - this.constructor.superclass = parent; - var returnValue = source[property].apply(this, arguments); - this.constructor.superclass = superclass; + var superclass = this.constructor.superclass; + this.constructor.superclass = parent; + var returnValue = source[property].call(this, ...args); + this.constructor.superclass = superclass; - if (property !== 'initialize') { - return returnValue; - } - }; - })(property); + if (property !== 'initialize') { + return returnValue; } - else { - klass.prototype[property] = source[property]; - } - } - }; + }; + })(property); + } + else { + klass.prototype[property] = source[property]; + } + } +}; - function Subclass() { } +function Subclass() { } - function callSuper(methodName) { - var parentMethod = null, - _this = this; +function callSuper(methodName, ...args) { + var parentMethod = null, + _this = this; - // climb prototype chain to find method not equal to callee's method - while (_this.constructor.superclass) { - var superClassMethod = _this.constructor.superclass.prototype[methodName]; - if (_this[methodName] !== superClassMethod) { - parentMethod = superClassMethod; - break; - } - // eslint-disable-next-line - _this = _this.constructor.superclass.prototype; + // climb prototype chain to find method not equal to callee's method + while (_this.constructor.superclass) { + var superClassMethod = _this.constructor.superclass.prototype[methodName]; + if (_this[methodName] !== superClassMethod) { + parentMethod = superClassMethod; + break; } - - if (!parentMethod) { - return console.log('tried to callSuper ' + methodName + ', method not found in prototype chain', this); - } - - return (arguments.length > 1) - ? parentMethod.apply(this, slice.call(arguments, 1)) - : parentMethod.call(this); + // eslint-disable-next-line + _this = _this.constructor.superclass.prototype; } - /** - * Helper for creation of "classes". - * @memberOf fabric.util - * @param {Function} [parent] optional "Class" to inherit from - * @param {Object} [properties] Properties shared by all instances of this class - * (be careful modifying objects defined here as this would affect all instances) - */ - function createClass() { - var parent = null, - properties = slice.call(arguments, 0); + if (!parentMethod) { + return console.log('tried to callSuper ' + methodName + ', method not found in prototype chain', this); + } - if (typeof properties[0] === 'function') { - parent = properties.shift(); - } - function klass() { - this.initialize.apply(this, arguments); - } + return parentMethod.call(this, ...args); +} - klass.superclass = parent; - klass.subclasses = []; +/** + * Helper for creation of "classes". + * @memberOf fabric.util + * @param {Function} [parent] optional "Class" to inherit from + * @param {Object} [properties] Properties shared by all instances of this class + * (be careful modifying objects defined here as this would affect all instances) + */ +export function createClass(...args) { + var parent = null, + properties = [...args]; - if (parent) { - Subclass.prototype = parent.prototype; - klass.prototype = new Subclass(); - parent.subclasses.push(klass); - } - for (var i = 0, length = properties.length; i < length; i++) { - addMethods(klass, properties[i], parent); - } - if (!klass.prototype.initialize) { - klass.prototype.initialize = emptyFunction; - } - klass.prototype.constructor = klass; - klass.prototype.callSuper = callSuper; - return klass; + if (typeof args[0] === 'function') { + parent = properties.shift(); + } + function klass(...klassArgs) { + this.initialize.call(this, ...klassArgs); } - fabric.util.createClass = createClass; -})(typeof exports !== 'undefined' ? exports : window); + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + Subclass.prototype = parent.prototype; + klass.prototype = new Subclass(); + parent.subclasses.push(klass); + } + for (var i = 0, length = properties.length; i < length; i++) { + addMethods(klass, properties[i], parent); + } + if (!klass.prototype.initialize) { + klass.prototype.initialize = noop; + } + klass.prototype.constructor = klass; + klass.prototype.callSuper = callSuper; + return klass; +} diff --git a/src/util/misc/index.ts b/src/util/misc/index.ts deleted file mode 100644 index e9a90a88ffd..00000000000 --- a/src/util/misc/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { cos } from './cos'; -export { sin } from './sin'; -export * from './radiansDegreesConversion'; -export * from './vectors'; -export { rotatePoint } from './rotatePoint'; diff --git a/src/util/misc/isTransparent.ts b/src/util/misc/isTransparent.ts new file mode 100644 index 00000000000..34cd9932421 --- /dev/null +++ b/src/util/misc/isTransparent.ts @@ -0,0 +1,44 @@ +/** + * Returns true if context has transparent pixel + * at specified location (taking tolerance into account) + * @param {CanvasRenderingContext2D} ctx context + * @param {Number} x x coordinate in canvasElementCoordinate, not fabric space + * @param {Number} y y coordinate in canvasElementCoordinate, not fabric space + * @param {Number} tolerance Tolerance pixels around the point, not alpha tolerance + * @return {boolean} true if transparent + */ +export const isTransparent = (ctx: CanvasRenderingContext2D, x: number, y: number, tolerance: number): boolean => { + + // If tolerance is > 0 adjust start coords to take into account. + // If moves off Canvas fix to 0 + if (tolerance > 0) { + if (x > tolerance) { + x -= tolerance; + } + else { + x = 0; + } + if (y > tolerance) { + y -= tolerance; + } + else { + y = 0; + } + } + + let _isTransparent = true; + const { data } = ctx.getImageData(x, y, (tolerance * 2) || 1, (tolerance * 2) || 1); + const l = data.length; + + // Split image data - for tolerance > 1, pixelDataSize = 4; + for (let i = 3; i < l; i += 4) { + const alphaChannel = data[i]; + if (alphaChannel > 0) { + // Stop if colour found + _isTransparent = false; + break; + } + } + + return _isTransparent; +}; diff --git a/src/util/misc/matrix.ts b/src/util/misc/matrix.ts index 4186db24804..4ee725f4c34 100644 --- a/src/util/misc/matrix.ts +++ b/src/util/misc/matrix.ts @@ -14,7 +14,7 @@ type TTranslateMatrixArgs = { translateY?: number; } -type TScaleMatrixArgs = { +export type TScaleMatrixArgs = { scaleX?: number; scaleY?: number; flipX?: boolean; diff --git a/src/util/misc/mergeClipPaths.ts b/src/util/misc/mergeClipPaths.ts new file mode 100644 index 00000000000..3cee36fa365 --- /dev/null +++ b/src/util/misc/mergeClipPaths.ts @@ -0,0 +1,45 @@ +import { fabric } from '../../../HEADER'; +import { applyTransformToObject } from './objectTransforms'; +import { multiplyTransformMatrices, invertTransform } from './matrix'; +/** + * Merges 2 clip paths into one visually equal clip path + * + * **IMPORTANT**:\ + * Does **NOT** clone the arguments, clone them proir if necessary. + * + * Creates a wrapper (group) that contains one clip path and is clipped by the other so content is kept where both overlap. + * Use this method if both the clip paths may have nested clip paths of their own, so assigning one to the other's clip path property is not possible. + * + * In order to handle the `inverted` property we follow logic described in the following cases:\ + * **(1)** both clip paths are inverted - the clip paths pass the inverted prop to the wrapper and loose it themselves.\ + * **(2)** one is inverted and the other isn't - the wrapper shouldn't become inverted and the inverted clip path must clip the non inverted one to produce an identical visual effect.\ + * **(3)** both clip paths are not inverted - wrapper and clip paths remain unchanged. + * + * @memberOf fabric.util + * @param {fabric.Object} c1 + * @param {fabric.Object} c2 + * @returns {fabric.Object} merged clip path + */ +export const mergeClipPaths = (c1: any, c2: any) => { + let a = c1, b = c2; + if (a.inverted && !b.inverted) { + // case (2) + a = c2; + b = c1; + } + // `b` becomes `a`'s clip path so we transform `b` to `a` coordinate plane + applyTransformToObject( + b, + multiplyTransformMatrices( + invertTransform(a.calcTransformMatrix()), + b.calcTransformMatrix() + ) + ); + // assign the `inverted` prop to the wrapping group + const inverted = a.inverted && b.inverted; + if (inverted) { + // case (1) + a.inverted = b.inverted = false; + } + return new fabric.Group([a], { clipPath: b, inverted }); +}; diff --git a/src/util/misc/misc.ts b/src/util/misc/misc.ts index 342a9dd0803..163e7e9df4c 100644 --- a/src/util/misc/misc.ts +++ b/src/util/misc/misc.ts @@ -1,6 +1,5 @@ //@ts-nocheck import { fabric } from '../../../HEADER'; -import { Point } from '../../point.class'; import { cos } from './cos'; import { sin } from './sin'; import { rotateVector, createVector, calcAngleBetweenVectors, getHatVector, getBisector } from './vectors'; @@ -36,6 +35,7 @@ import { addTransformToObject, applyTransformToObject, removeTransformFromObject, + sizeAfterTransform, } from './objectTransforms'; import { makeBoundingBoxFromPoints } from './boundingBoxFromPoints'; import { @@ -79,233 +79,125 @@ import { removeListener, addListener, } from '../dom_event'; - - /** - * @namespace fabric.util - */ - fabric.util = { - cos, - sin, - rotateVector, - createVector, - calcAngleBetweenVectors, - getHatVector, - getBisector, - degreesToRadians, - radiansToDegrees, - rotatePoint, - // probably we should stop exposing this from the interface - getRandomInt, - removeFromArray, - projectStrokeOnPoints, - // matrix.ts file - transformPoint, - invertTransform, - composeMatrix, - qrDecompose, - calcDimensionsMatrix, - calcRotateMatrix, - multiplyTransformMatrices, - // textStyles.ts file - stylesFromArray, - stylesToArray, - hasStyleChanged, - object: { - clone, - extend, - }, - createCanvasElement, - createImage, - copyCanvasElement, - toDataURL, - toFixed, - matrixToSVG, - parsePreserveAspectRatioAttribute, - groupSVGElements, - parseUnit, - getSvgAttributes, - findScaleToFit, - findScaleToCover, - capValue, - saveObjectTransform, - resetObjectTransform, - addTransformToObject, - applyTransformToObject, - removeTransformFromObject, - makeBoundingBoxFromPoints, - sendPointToPlane, - transformPointRelativeToCanvas, - sendObjectToPlane, - string: { - camelize, - capitalize, - escapeXml, - graphemeSplit, - }, - getKlass, - loadImage, - enlivenObjects, - enlivenObjectEnlivables, - array: { - min, - max, - }, - pick, - joinPath, - parsePath, - makePathSimpler, - getSmoothPathFromPoints, - getPathSegmentsInfo, - getBoundsOfCurve, - getPointOnPath, - transformPath, - getRegularPolygonPath, - request, - setStyle, - isTouchEvent, - getPointer, - removeListener, - addListener, - - /** - * Returns true if context has transparent pixel - * at specified location (taking tolerance into account) - * @param {CanvasRenderingContext2D} ctx context - * @param {Number} x x coordinate - * @param {Number} y y coordinate - * @param {Number} tolerance Tolerance - */ - isTransparent: function(ctx, x, y, tolerance) { - - // If tolerance is > 0 adjust start coords to take into account. - // If moves off Canvas fix to 0 - if (tolerance > 0) { - if (x > tolerance) { - x -= tolerance; - } - else { - x = 0; - } - if (y > tolerance) { - y -= tolerance; - } - else { - y = 0; - } - } - - var _isTransparent = true, i, temp, - imageData = ctx.getImageData(x, y, (tolerance * 2) || 1, (tolerance * 2) || 1), - l = imageData.data.length; - - // Split image data - for tolerance > 1, pixelDataSize = 4; - for (i = 3; i < l; i += 4) { - temp = imageData.data[i]; - _isTransparent = temp <= 0; - if (_isTransparent === false) { - break; // Stop if colour found - } - } - - imageData = null; - - return _isTransparent; - }, - - /** - * Given current aspect ratio, determines the max width and height that can - * respect the total allowed area for the cache. - * @memberOf fabric.util - * @param {Number} ar aspect ratio - * @param {Number} maximumArea Maximum area you want to achieve - * @return {Object.x} Limited dimensions by X - * @return {Object.y} Limited dimensions by Y - */ - limitDimsByArea: function(ar, maximumArea) { - var roughWidth = Math.sqrt(maximumArea * ar), - perfLimitSizeY = Math.floor(maximumArea / roughWidth); - return { x: Math.floor(roughWidth), y: perfLimitSizeY }; - }, - - /** - * given a width and height, return the size of the bounding box - * that can contains the box with width/height with applied transform - * described in options. - * Use to calculate the boxes around objects for controls. - * @memberOf fabric.util - * @param {Number} width - * @param {Number} height - * @param {Object} options - * @param {Number} options.scaleX - * @param {Number} options.scaleY - * @param {Number} options.skewX - * @param {Number} options.skewY - * @returns {Point} size - */ - sizeAfterTransform: function(width, height, options) { - var dimX = width / 2, dimY = height / 2, - points = [ - { - x: -dimX, - y: -dimY - }, - { - x: dimX, - y: -dimY - }, - { - x: -dimX, - y: dimY - }, - { - x: dimX, - y: dimY - }], - transformMatrix = fabric.util.calcDimensionsMatrix(options), - bbox = fabric.util.makeBoundingBoxFromPoints(points, transformMatrix); - return new Point(bbox.width, bbox.height); - }, - - /** - * Merges 2 clip paths into one visually equal clip path - * - * **IMPORTANT**:\ - * Does **NOT** clone the arguments, clone them proir if necessary. - * - * Creates a wrapper (group) that contains one clip path and is clipped by the other so content is kept where both overlap. - * Use this method if both the clip paths may have nested clip paths of their own, so assigning one to the other's clip path property is not possible. - * - * In order to handle the `inverted` property we follow logic described in the following cases:\ - * **(1)** both clip paths are inverted - the clip paths pass the inverted prop to the wrapper and loose it themselves.\ - * **(2)** one is inverted and the other isn't - the wrapper shouldn't become inverted and the inverted clip path must clip the non inverted one to produce an identical visual effect.\ - * **(3)** both clip paths are not inverted - wrapper and clip paths remain unchanged. - * - * @memberOf fabric.util - * @param {fabric.Object} c1 - * @param {fabric.Object} c2 - * @returns {fabric.Object} merged clip path - */ - mergeClipPaths: function (c1, c2) { - var a = c1, b = c2; - if (a.inverted && !b.inverted) { - // case (2) - a = c2; - b = c1; - } - // `b` becomes `a`'s clip path so we transform `b` to `a` coordinate plane - fabric.util.applyTransformToObject( - b, - fabric.util.multiplyTransformMatrices( - fabric.util.invertTransform(a.calcTransformMatrix()), - b.calcTransformMatrix() - ) - ); - // assign the `inverted` prop to the wrapping group - var inverted = a.inverted && b.inverted; - if (inverted) { - // case (1) - a.inverted = b.inverted = false; - } - return new fabric.Group([a], { clipPath: b, inverted: inverted }); - }, - }; +import { + wrapElement, + getScrollLeftTop, + getElementOffset, + getNodeCanvas, + cleanUpJsdomNode, + makeElementUnselectable, + makeElementSelectable, +} from '../dom_misc'; +import { isTransparent } from './isTransparent'; +import { mergeClipPaths } from './mergeClipPaths'; +import * as ease from '../anim_ease'; +import { animateColor } from '../animate_color'; +import { + animate, + requestAnimFrame, + cancelAnimFrame, +} from '../animate'; +import { createClass } from '../lang_class'; +/** + * @namespace fabric.util + */ +fabric.util = { + cos, + sin, + rotateVector, + createVector, + calcAngleBetweenVectors, + getHatVector, + getBisector, + degreesToRadians, + radiansToDegrees, + rotatePoint, + // probably we should stop exposing this from the interface + getRandomInt, + removeFromArray, + projectStrokeOnPoints, + // matrix.ts file + transformPoint, + invertTransform, + composeMatrix, + qrDecompose, + calcDimensionsMatrix, + calcRotateMatrix, + multiplyTransformMatrices, + // textStyles.ts file + stylesFromArray, + stylesToArray, + hasStyleChanged, + object: { + clone, + extend, + }, + createCanvasElement, + createImage, + copyCanvasElement, + toDataURL, + toFixed, + matrixToSVG, + parsePreserveAspectRatioAttribute, + groupSVGElements, + parseUnit, + getSvgAttributes, + findScaleToFit, + findScaleToCover, + capValue, + saveObjectTransform, + resetObjectTransform, + addTransformToObject, + applyTransformToObject, + removeTransformFromObject, + makeBoundingBoxFromPoints, + sendPointToPlane, + transformPointRelativeToCanvas, + sendObjectToPlane, + string: { + camelize, + capitalize, + escapeXml, + graphemeSplit, + }, + getKlass, + loadImage, + enlivenObjects, + enlivenObjectEnlivables, + array: { + min, + max, + }, + pick, + joinPath, + parsePath, + makePathSimpler, + getSmoothPathFromPoints, + getPathSegmentsInfo, + getBoundsOfCurve, + getPointOnPath, + transformPath, + getRegularPolygonPath, + request, + setStyle, + isTouchEvent, + getPointer, + removeListener, + addListener, + wrapElement, + getScrollLeftTop, + getElementOffset, + getNodeCanvas, + cleanUpJsdomNode, + makeElementUnselectable, + makeElementSelectable, + isTransparent, + sizeAfterTransform, + mergeClipPaths, + ease, + animateColor, + animate, + requestAnimFrame, + cancelAnimFrame, + createClass, +}; diff --git a/src/util/misc/objectTransforms.ts b/src/util/misc/objectTransforms.ts index a804c71dd3d..daf0fd4eab5 100644 --- a/src/util/misc/objectTransforms.ts +++ b/src/util/misc/objectTransforms.ts @@ -1,7 +1,8 @@ import { Point } from "../../point.class"; import { TMat2D } from "../../typedefs"; -import { invertTransform, multiplyTransformMatrices, qrDecompose } from "./matrix"; -import type { TComposeMatrixArgs } from './matrix'; +import { makeBoundingBoxFromPoints } from './boundingBoxFromPoints'; +import { invertTransform, multiplyTransformMatrices, qrDecompose, calcDimensionsMatrix } from "./matrix"; +import type { TComposeMatrixArgs, TScaleMatrixArgs } from './matrix'; type FabricObject = any; @@ -89,3 +90,31 @@ export const saveObjectTransform = ( flipY: target.flipY, top: target.top }); + +/** + * given a width and height, return the size of the bounding box + * that can contains the box with width/height with applied transform + * described in options. + * Use to calculate the boxes around objects for controls. + * @memberOf fabric.util + * @param {Number} width + * @param {Number} height + * @param {Object} options + * @param {Number} options.scaleX + * @param {Number} options.scaleY + * @param {Number} options.skewX + * @param {Number} options.skewY + * @returns {Point} size + */ +export const sizeAfterTransform = (width: number, height: number, options: TScaleMatrixArgs) => { + const dimX = width / 2, dimY = height / 2, + points = [ + new Point(-dimX, -dimY), + new Point(dimX, -dimY), + new Point(-dimX, dimY), + new Point(dimX, dimY), + ], + transformMatrix = calcDimensionsMatrix(options), + bbox = makeBoundingBoxFromPoints(points, transformMatrix); + return new Point(bbox.width, bbox.height); +}; diff --git a/src/util/misc/rotatePoint.ts b/src/util/misc/rotatePoint.ts index 4f103795ddc..f87db19a153 100644 --- a/src/util/misc/rotatePoint.ts +++ b/src/util/misc/rotatePoint.ts @@ -10,4 +10,4 @@ import type { TRadian } from '../../typedefs'; * @param {TRadian} radians The radians of the angle for the rotation * @return {Point} The new rotated point */ -export const rotatePoint = (point: Point, origin: Point, radians: TRadian): Point => point.rotate(origin, radians); +export const rotatePoint = (point: Point, origin: Point, radians: TRadian): Point => point.rotate(radians, origin); diff --git a/src/util/misc/vectors.ts b/src/util/misc/vectors.ts index 4f61c0b0794..d5751fa86c2 100644 --- a/src/util/misc/vectors.ts +++ b/src/util/misc/vectors.ts @@ -1,5 +1,3 @@ -import { sin } from './sin'; -import { cos } from './cos'; import { IPoint, Point } from '../../point.class'; import { TRadian } from '../../typedefs'; @@ -11,14 +9,7 @@ import { TRadian } from '../../typedefs'; * @param {Number} radians The radians of the angle for the rotation * @return {Point} The new rotated point */ -export const rotateVector = (vector: Point, radians: TRadian) => { - const sinus = sin(radians), - cosinus = cos(radians); - return new Point( - vector.x * cosinus - vector.y * sinus, - vector.x * sinus + vector.y * cosinus, - ); -}; +export const rotateVector = (vector: Point, radians: TRadian) => vector.rotate(radians); /** * Creates a vetor from points represented as a point diff --git a/test/unit/cache.js b/test/unit/cache.js new file mode 100644 index 00000000000..f98e0c5dad4 --- /dev/null +++ b/test/unit/cache.js @@ -0,0 +1,30 @@ +(function() { + const perfLimit = fabric.config.perfLimitSizeTotal; + QUnit.module('Cache', { + beforeEach: function() { + fabric.config.perfLimitSizeTotal = 10000; + }, + afterEach: function() { + fabric.config.perfLimitSizeTotal = perfLimit; + } + }); + + QUnit.test('Cache.limitDimsByArea', function(assert) { + assert.ok(typeof fabric.cache.limitDimsByArea === 'function'); + var [x, y] = fabric.cache.limitDimsByArea(1); + assert.equal(x, 100); + assert.equal(y, 100); + }); + + QUnit.test('Cache.limitDimsByArea ar > 1', function(assert) { + var [x , y] = fabric.cache.limitDimsByArea(3); + assert.equal(x, 173); + assert.equal(y, 57); + }); + + QUnit.test('Cache.limitDimsByArea ar < 1', function(assert) { + var [x, y] = fabric.cache.limitDimsByArea(1 / 3); + assert.equal(x, 57); + assert.equal(y, 173); + }); +})(); diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index cd85ef7de7e..f8f46ec7c5f 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -127,6 +127,10 @@ return _createImageObject(IMG_WIDTH, IMG_HEIGHT, callback); } + function createImageStub() { + return new fabric.Image(_createImageElement(), { width: 0, height: 0 }); + } + function setSrc(img, src, callback) { img.onload = callback; img.src = src; @@ -878,7 +882,7 @@ text = new fabric.Text('Text'), group = new fabric.Group([text, line]), ellipse = new fabric.Ellipse(), - image = new fabric.Image({width: 0, height: 0}), + image = createImageStub(), path2 = new fabric.Path('M 0 0 L 200 100 L 200 300 z'), path3 = new fabric.Path('M 50 50 L 100 300 L 400 400 z'), pathGroup = new fabric.Group([path2, path3]); @@ -913,9 +917,9 @@ text = new fabric.Text('Text'), group = new fabric.Group([text, line]), ellipse = new fabric.Ellipse(), - image = new fabric.Image({width: 0, height: 0}), - imageBG = new fabric.Image({width: 0, height: 0}), - imageOL = new fabric.Image({width: 0, height: 0}), + image = createImageStub(), + imageBG = createImageStub(), + imageOL = createImageStub(), path2 = new fabric.Path('M 0 0 L 200 100 L 200 300 z'), path3 = new fabric.Path('M 50 50 L 100 300 L 400 400 z'), pathGroup = new fabric.Group([path2, path3]); @@ -953,7 +957,7 @@ text = new fabric.Text('Text'), group = new fabric.Group([text, line]), ellipse = new fabric.Ellipse(), - image = new fabric.Image({width: 0, height: 0}), + image = createImageStub(), path2 = new fabric.Path('M 0 0 L 200 100 L 200 300 z'), path3 = new fabric.Path('M 50 50 L 100 300 L 400 400 z'), pathGroup = new fabric.Group([path2, path3]); @@ -983,10 +987,7 @@ }); QUnit.test('toSVG with exclude from export background', function(assert) { - var image = fabric.document.createElement('img'), - imageBG = new fabric.Image(image, {width: 0, height: 0}), - imageOL = new fabric.Image(image, {width: 0, height: 0}); - + const imageBG = createImageStub(), imageOL = createImageStub(); canvas.renderOnAddRemove = false; canvas.backgroundImage = imageBG; canvas.overlayImage = imageOL; diff --git a/test/unit/parser.js b/test/unit/parser.js index d14d072e2ea..36047d9a701 100644 --- a/test/unit/parser.js +++ b/test/unit/parser.js @@ -125,6 +125,22 @@ var done = assert.async(); assert.ok(typeof fabric.parseElements === 'function'); + function makeElement(tagName, attributes) { + var el = fabric.document.createElement(tagName); + for (var prop in attributes) { + if (prop === 'class') { + el.className = attributes[prop]; + } + else if (prop === 'for') { + el.htmlFor = attributes[prop]; + } + else { + el.setAttribute(prop, attributes[prop]); + } + } + return el; + } + function getOptions(options) { return fabric.util.object.extend(fabric.util.object.clone({ left: 10, top: 20, width: 30, height: 40 @@ -132,10 +148,10 @@ } var elements = [ - fabric.util.makeElement('rect', getOptions()), - fabric.util.makeElement('circle', getOptions({ r: 14 })), - fabric.util.makeElement('path', getOptions({ d: 'M 100 100 L 300 100 L 200 300 z' })), - fabric.util.makeElement('inexistent', getOptions()) + makeElement('rect', getOptions()), + makeElement('circle', getOptions({ r: 14 })), + makeElement('path', getOptions({ d: 'M 100 100 L 300 100 L 200 300 z' })), + makeElement('inexistent', getOptions()) ]; fabric.parseElements(elements, function(parsedElements) { assert.ok(parsedElements[0] instanceof fabric.Rect); diff --git a/test/unit/point.js b/test/unit/point.js index fbb42e61634..0096de3d8b3 100644 --- a/test/unit/point.js +++ b/test/unit/point.js @@ -445,4 +445,18 @@ assert.equal(returned.y, point.y, 'y coords should be same'); }); + QUnit.test('rotate', function(assert) { + var point = new fabric.Point(5, 1); + var rotated = point.rotate(Math.PI); + assert.equal(rotated.x, -5, 'rotated x'); + assert.equal(rotated.y, -1, 'rotated y'); + }); + + QUnit.test('rotate with origin point', function(assert) { + var point = new fabric.Point(5, 1); + var rotated = point.rotate(Math.PI, new fabric.Point(4, 1)); + assert.equal(rotated.x, 3, 'rotated x around 4'); + assert.equal(rotated.y, 1, 'rotated y around 1'); + }); + })(); diff --git a/test/unit/util.js b/test/unit/util.js index 81d9c85fc17..7e3b65df963 100644 --- a/test/unit/util.js +++ b/test/unit/util.js @@ -273,92 +273,22 @@ // assert.ok(axisPoint instanceof YAxisPoint); <-- fails }); - QUnit.test('fabric.util.getById', function(assert) { - assert.ok(typeof fabric.util.getById === 'function'); - - var el = fabric.document.createElement('div'); - el.id = 'foobarbaz'; - fabric.document.body.appendChild(el); - - assert.equal(el, fabric.util.getById(el)); - assert.equal(el, fabric.util.getById('foobarbaz')); - assert.equal(null, fabric.util.getById('likely-non-existent-id')); - }); - - QUnit.test('fabric.util.toArray', function(assert) { - assert.ok(typeof fabric.util.toArray === 'function'); - - assert.deepEqual(['x', 'y'], fabric.util.toArray({ 0: 'x', 1: 'y', length: 2 })); - assert.deepEqual([1, 3], fabric.util.toArray((function(){ return arguments; })(1, 3))); - - var nodelist = fabric.document.getElementsByTagName('div'), - converted = fabric.util.toArray(nodelist); - - assert.ok(converted instanceof Array); - assert.equal(nodelist.length, converted.length); - assert.equal(nodelist[0], converted[0]); - assert.equal(nodelist[1], converted[1]); - }); - - QUnit.test('fabric.util.makeElement', function(assert) { - var makeElement = fabric.util.makeElement; - assert.ok(typeof makeElement === 'function'); - - var el = makeElement('div'); - - assert.equal(el.tagName.toLowerCase(), 'div'); - assert.equal(el.nodeType, 1); - - el = makeElement('p', { 'class': 'blah', 'for': 'boo_hoo', 'some_random-attribute': 'woot' }); - - assert.equal(el.tagName.toLowerCase(), 'p'); - assert.equal(el.nodeType, 1); - assert.equal(el.className, 'blah'); - assert.equal(el.htmlFor, 'boo_hoo'); - assert.equal(el.getAttribute('some_random-attribute'), 'woot'); - }); - - QUnit.test('fabric.util.addClass', function(assert) { - var addClass = fabric.util.addClass; - assert.ok(typeof addClass === 'function'); - - var el = fabric.document.createElement('div'); - addClass(el, 'foo'); - assert.equal(el.className, 'foo'); - - addClass(el, 'bar'); - assert.equal(el.className, 'foo bar'); - - addClass(el, 'baz qux'); - assert.equal(el.className, 'foo bar baz qux'); - - addClass(el, 'foo'); - assert.equal(el.className, 'foo bar baz qux'); - }); - QUnit.test('fabric.util.wrapElement', function(assert) { var wrapElement = fabric.util.wrapElement; assert.ok(typeof wrapElement === 'function'); - + var wrapper = fabric.document.createElement('div'); var el = fabric.document.createElement('p'); - var wrapper = wrapElement(el, 'div'); - - assert.equal(wrapper.tagName.toLowerCase(), 'div'); - assert.equal(wrapper.firstChild, el); - - el = fabric.document.createElement('p'); - wrapper = wrapElement(el, 'div', { 'class': 'foo' }); + var wrapper = wrapElement(el, wrapper); assert.equal(wrapper.tagName.toLowerCase(), 'div'); assert.equal(wrapper.firstChild, el); - assert.equal(wrapper.className, 'foo'); var childEl = fabric.document.createElement('span'); var parentEl = fabric.document.createElement('p'); parentEl.appendChild(childEl); - wrapper = wrapElement(childEl, 'strong'); + wrapper = wrapElement(childEl, fabric.document.createElement('strong')); // wrapper is now in between parent and child assert.equal(wrapper.parentNode, parentEl); @@ -1058,25 +988,6 @@ assert.deepEqual(matrix, fabric.iMatrix, 'default is identity matrix'); }); - QUnit.test('fabric.util.limitDimsByArea', function(assert) { - assert.ok(typeof fabric.util.limitDimsByArea === 'function'); - var dims = fabric.util.limitDimsByArea(1, 10000); - assert.equal(dims.x, 100); - assert.equal(dims.y, 100); - }); - - QUnit.test('fabric.util.limitDimsByArea ar > 1', function(assert) { - var dims = fabric.util.limitDimsByArea(3, 10000); - assert.equal(dims.x, 173); - assert.equal(dims.y, 57); - }); - - QUnit.test('fabric.util.limitDimsByArea ar < 1', function(assert) { - var dims = fabric.util.limitDimsByArea(1 / 3, 10000); - assert.equal(dims.x, 57); - assert.equal(dims.y, 173); - }); - QUnit.test('fabric.util.capValue ar < 1', function(assert) { assert.ok(typeof fabric.util.capValue === 'function'); var val = fabric.util.capValue(3, 10, 70);