diff options
34 files changed, 1529 insertions, 1402 deletions
diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..b6f110d --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,85 @@ +{ + "extends": "standard", + "plugins": [ "sort-class-members" ], + "rules": { + "operator-linebreak": [ "error", "before" ], + "object-curly-spacing": [ "error", "always" ], + "array-bracket-spacing": [ "error", "always" ], + "indent": [ "error", 2, { "flatTernaryExpressions": true } ], + "padded-blocks": "off", + "sort-class-members/sort-class-members": [ 2, { + "order": [ + "[static-properties]", + "[properties]", + "[conventional-private-properties]", + "constructor", + "[static-methods]", + "[methods]", + "[conventional-private-methods]", + "[accessor-pairs]", + "[getters]", + "[setters]", + "[everything-else]" + ], + "groups": { + "constructor": { + "name": "constructor", + "type": "method", + "sort": "alphabetical" + }, + "properties": { + "type": "property", + "sort": "alphabetical" + }, + "getters": { + "kind": "get", + "sort": "alphabetical" + }, + "setters": { + "kind": "set", + "sort": "alphabetical" + }, + "accessor-pairs": { + "accessorPair": true, + "sort": "alphabetical" + }, + "static-properties": { + "type": "property", + "static": true, + "sort": "alphabetical" + }, + "conventional-private-properties": { + "type": "property", + "name": "/_.+/", + "sort": "alphabetical" + }, + "arrow-function-properties": { + "propertyType": "ArrowFunctionExpression", + "sort": "alphabetical" + }, + "methods": { + "type": "method", + "sort": "alphabetical" + }, + "static-methods": { + "type": "method", + "static": true, + "sort": "alphabetical" + }, + "async-methods": { + "type": "method", + "async": true, + "sort": "alphabetical" + }, + "conventional-private-methods": { + "type": "method", + "name": "/_.+/", + "sort": "alphabetical" + }, + "everything-else": { + "sort": "alphabetical" + } + } + }] + } +}
\ No newline at end of file diff --git a/.eslintrc.yaml b/.eslintrc.yaml deleted file mode 100644 index cc037d8..0000000 --- a/.eslintrc.yaml +++ /dev/null @@ -1,10 +0,0 @@ - -extends: standard -rules: - operator-linebreak: [ error, before ] - object-curly-spacing: [ error, always ] - array-bracket-spacing: [ error, always ] - indent: [ error, 2, { - flatTernaryExpressions: true - }] - padded-blocks: off diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..2f88022 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [fuzzyma] diff --git a/CHANGELOG.md b/CHANGELOG.md index 90bd0f5..20b163c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ The document follows the conventions described in [“Keep a CHANGELOG”](http: - added possibility to pass a transform object to `PointArray.transform()` similar to Point - added `with-last` as `when` to `animate` and `schedule` to let an animation start with the start of the last one in the timeline - added lots of tests in es6 format + - added geometry and positioning methods to `A` (#1110) ### Deleted - deleted undocumented `Matrix.compose()` method which did the same as `new Matrix()` or `Matrix.transform()` @@ -2,8 +2,12 @@ [![Build Status](https://travis-ci.org/svgdotjs/svg.js.svg?branch=master)](https://travis-ci.org/svgdotjs/svg.js) [![Coverage Status](https://coveralls.io/repos/github/svgdotjs/svg.js/badge.svg?branch=master)](https://coveralls.io/github/svgdotjs/svg.js?branch=master) -[![CDNJS](https://img.shields.io/cdnjs/v/svg.js.svg)](https://cdnjs.com/libraries/svg.js) +[![Cdnjs](https://img.shields.io/cdnjs/v/svg.js.svg)](https://cdnjs.com/libraries/svg.js) +[![jsdelivr](https://badgen.net/jsdelivr/v/npm/@svgdotjs/svg.js)](https://cdn.jsdelivr.net/npm/@svgdotjs/svg.js) [![Join the chat at https://gitter.im/svgdotjs/svg.js](https://badges.gitter.im/svgdotjs/svg.js.svg)](https://gitter.im/svgdotjs/svg.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Twitter](https://img.shields.io/badge/Twitter-@svg__js-green.svg)](https://twitter.com/svg_js) + + __A lightweight library for manipulating and animating SVG, without any dependencies.__ @@ -19,11 +23,13 @@ SVG.js is licensed under the terms of the MIT License. `yarn add @svgdotjs/svg.js` -#### Cdnjs: +#### CDNs: -[https://cdnjs.com/libraries/svg.js](https://cdnjs.com/libraries/svg.js) +[https://cdnjs.com/libraries/svg.js](https://cdnjs.com/libraries/svg.js) +[https://cdn.jsdelivr.net/npm/@svgdotjs/svg.js](https://cdn.jsdelivr.net/npm/@svgdotjs/svg.js) +[https://unpkg.com/@svgdotjs/svg.js](https://unpkg.com/@svgdotjs/svg.js) ## Documentation Check [svgjs.com](https://svgjs.com/docs/3.0/) to learn more. -[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=ulima.ums%40googlemail.com&lc=US&item_name=SVG.JS¤cy_code=EUR&bn=PP-DonationsBF%3Abtn_donate_74x21.png%3ANonHostedGuest) +[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=ulima.ums%40googlemail.com&lc=US&item_name=SVG.JS¤cy_code=EUR&bn=PP-DonationsBF%3Abtn_donate_74x21.png%3ANonHostedGuest) or [![Sponsor](https://img.shields.io/badge/Sponsor-svg.js-green.svg)](https://github.com/sponsors/Fuzzyma) diff --git a/package.json b/package.json index f5e3921..5b367a2 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,10 @@ }, "github": "https://github.com/svgdotjs/svg.js", "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Fuzzyma" + }, "typings": "./svg.js.d.ts", "scripts": { "build": "npm run fix && npm run rollup", @@ -89,34 +93,34 @@ "babel-eslint": "^10.1.0", "core-js": "^3.6.5", "coveralls": "^3.1.0", - "eslint": "^6.8.0", + "eslint": "^7.0.0", "eslint-config-standard": "^14.1.1", "eslint-plugin-import": "^2.20.2", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-sort-class-members": "^1.7.0", "eslint-plugin-standard": "^4.0.1", "esm": "^3.2.25", "http-server": "^0.12.3", "jasmine": "^3.5.0", "jasmine-core": "^3.5.0", - "karma": "^5.0.4", + "karma": "^5.0.5", "karma-chrome-launcher": "^3.1.0", "karma-coverage": "^2.0.2", "karma-firefox-launcher": "^1.3.0", "karma-jasmine": "^3.1.1", "karma-sauce-launcher": "^4.1.4", - "rollup": "^2.7.6", + "rollup": "^2.9.0", "rollup-plugin-filesize": "^8.0.2", "rollup-plugin-terser": "^5.3.0", - "svgdom": "^0.1.3", "typescript": "^3.8.3", - "yargs": "^15.3.1" + "yargs": "^15.3.1", + "svgdom": "^0.1.8" }, "browserslist": [ "last 1 version", "> 0.25%", "not maintained node versions", "not dead" - ], - "dependencies": {} + ] } diff --git a/spec/spec/elements/Dom.js b/spec/spec/elements/Dom.js index bcf0ed5..2e36c92 100644 --- a/spec/spec/elements/Dom.js +++ b/spec/spec/elements/Dom.js @@ -656,7 +656,7 @@ describe('Dom.js', function () { it('works without a parent', () => { const canvas = new Svg() - expect(canvas.xml('<rect /><circle>', undefined, svg)).toBe(canvas) + expect(canvas.xml('<rect /><circle />', undefined, svg)).toBe(canvas) }) }) diff --git a/spec/spec/elements/G.js b/spec/spec/elements/G.js index 2cd73fe..58dc07d 100644 --- a/spec/spec/elements/G.js +++ b/spec/spec/elements/G.js @@ -1,8 +1,8 @@ -/* globals describe, expect, it, jasmine, spyOn, container */ +/* globals describe, expect, it, jasmine, container */ -import { Box, G, Rect, SVG } from '../../../src/main.js' +import { G, SVG } from '../../../src/main.js' -const { any, objectContaining } = jasmine +const { any } = jasmine describe('G.js', () => { @@ -26,267 +26,4 @@ describe('G.js', () => { }) }) }) - - describe('dmove()', () => { - it('moves the bbox of the group by a certain amount (1)', () => { - const canvas = SVG().addTo(container) - const g = canvas.group() - - g.add(new Rect({ width: 100, height: 120, x: 10, y: 20 })) - g.add(new Rect({ width: 70, height: 100, x: 50, y: 60 })) - - g.dmove(10, 10) - - const box = g.bbox() - expect(box).toEqual(objectContaining({ - x: 20, y: 30, width: box.width, height: box.height - })) - }) - - it('moves the bbox of the group by a certain amount (2)', () => { - const canvas = SVG().addTo(container) - const g = canvas.group() - - g.rect(400, 200).move(123, 312).rotate(34).skew(12) - g.rect(100, 50).move(11, 43).translate(123, 32).skew(-12) - g.rect(400, 200).rotate(90) - g.group().rotate(23).group().skew(32).rect(100, 40).skew(11).rotate(12) - - const oldBox = g.bbox() - - g.dmove(10, 10) - - const newBox = g.bbox() - - expect(newBox.x).toBeCloseTo(oldBox.x + 10, 4) - expect(newBox.y).toBeCloseTo(oldBox.y + 10, 4) - expect(newBox.w).toBeCloseTo(oldBox.w, 4) - expect(newBox.h).toBeCloseTo(oldBox.h, 4) - }) - }) - - describe('dx()', () => { - it('calls dmove with dy=0 and returns itself', () => { - const canvas = SVG().addTo(container) - const g = canvas.group() - const spy = spyOn(g, 'dmove').and.callThrough() - expect(g.dx(10)).toBe(g) - expect(spy).toHaveBeenCalledWith(10, 0) - }) - }) - - describe('dy()', () => { - it('calls dmove with dx=0 and returns itself', () => { - const canvas = SVG().addTo(container) - const g = canvas.group() - const spy = spyOn(g, 'dmove').and.callThrough() - expect(g.dy(10)).toBe(g) - expect(spy).toHaveBeenCalledWith(0, 10) - }) - }) - - describe('move()', () => { - it('calls dmove() with the correct difference', () => { - const canvas = SVG().addTo(container) - const g = canvas.group() - g.rect(100, 200).move(111, 223) - - spyOn(g, 'dmove') - - g.move(100, 150) - expect(g.dmove).toHaveBeenCalledWith(-11, -73) - }) - - it('defaults to x=0 and y=0', () => { - const canvas = SVG().addTo(container) - const g = canvas.group() - g.rect(100, 200).move(111, 223) - - spyOn(g, 'dmove') - - g.move() - expect(g.dmove).toHaveBeenCalledWith(-111, -223) - }) - }) - - describe('x()', () => { - it('gets the x value of the bbox', () => { - const canvas = SVG().addTo(container) - - const g = new G() - g.add(new Rect({ width: 100, height: 120, x: 10, y: 20 })) - g.add(new Rect({ width: 70, height: 100, x: 50, y: 60 })) - - g.addTo(canvas) - - expect(g.x()).toBe(g.bbox().x) - expect(g.x()).toBe(10) - }) - it('calls move with the paramater as x', () => { - const canvas = SVG().addTo(container) - const g = canvas.group() - g.rect(100, 200).move(111, 223) - - spyOn(g, 'move') - - g.x(100) - expect(g.move).toHaveBeenCalledWith(100, g.bbox().y, any(Box)) - }) - }) - - describe('y()', () => { - it('gets the y value of the bbox', () => { - const canvas = SVG().addTo(container) - - const g = new G() - g.add(new Rect({ width: 100, height: 120, x: 10, y: 20 })) - g.add(new Rect({ width: 70, height: 100, x: 50, y: 60 })) - - g.addTo(canvas) - - expect(g.y()).toBe(g.bbox().y) - expect(g.y()).toBe(20) - }) - - it('calls move with the paramater as y', () => { - const canvas = SVG().addTo(container) - const g = canvas.group() - g.rect(100, 200).move(111, 223) - - spyOn(g, 'move') - - g.y(100) - expect(g.move).toHaveBeenCalledWith(g.bbox().x, 100, any(Box)) - }) - }) - - describe('size()', () => { - it('changes the dimensions of the bbox (1)', () => { - const canvas = SVG().addTo(container) - - const g = new G() - g.add(new Rect({ width: 100, height: 120, x: 10, y: 20 })) - g.add(new Rect({ width: 70, height: 100, x: 50, y: 60 })) - - g.addTo(canvas) - - const oldBox = g.bbox() - - expect(g.size(100, 100)).toBe(g) - - const newBox = g.bbox() - - expect(newBox.x).toBeCloseTo(oldBox.x, 4) - expect(newBox.y).toBeCloseTo(oldBox.y, 4) - expect(newBox.w).toBeCloseTo(100, 4) - expect(newBox.h).toBeCloseTo(100, 4) - - const rbox1 = g.children()[0].rbox() - const rbox2 = g.children()[1].rbox() - - expect(rbox1.width).toBeCloseTo(90.9, 1) - expect(Math.floor(rbox2.width * 10) / 10).toBeCloseTo(63.6, 1) // Browsers have different opinion on this one (chrome: 63.6, ff: 63.7) - - expect(rbox1.x).toBeCloseTo(10, 1) - expect(rbox2.x).toBeCloseTo(46.4, 1) - expect(rbox1.height).toBeCloseTo(85.7, 1) - expect(rbox2.height).toBeCloseTo(71.4, 1) - expect(rbox1.y).toBeCloseTo(20, 1) - expect(rbox2.y).toBeCloseTo(48.6, 1) - }) - - it('changes the dimensions of the bbox (2)', () => { - const canvas = SVG().addTo(container) - const g = canvas.group() - - g.rect(400, 200).move(123, 312).rotate(34).skew(12) - g.rect(100, 50).move(11, 43).translate(123, 32).skew(-12) - g.rect(400, 200).rotate(90) - g.group().rotate(23).group().skew(32).rect(100, 40).skew(11).rotate(12) - - const oldBox = g.bbox() - - g.size(100, 100) - - const newBox = g.bbox() - - expect(newBox.x).toBeCloseTo(oldBox.x, 4) - expect(newBox.y).toBeCloseTo(oldBox.y, 4) - expect(newBox.w).toBeCloseTo(100, 4) - expect(newBox.h).toBeCloseTo(100, 4) - }) - - }) - - describe('width()', () => { - it('gets the width value of the bbox', () => { - const canvas = SVG().addTo(container) - - const g = new G() - g.add(new Rect({ width: 100, height: 120, x: 10, y: 20 })) - g.add(new Rect({ width: 70, height: 100, x: 50, y: 60 })) - - g.addTo(canvas) - - expect(g.width()).toBe(g.bbox().width) - expect(g.width()).toBe(110) - }) - it('sets the width value of the bbox by moving all children', () => { - const canvas = SVG().addTo(container) - - const g = new G() - g.add(new Rect({ width: 100, height: 120, x: 10, y: 20 })) - g.add(new Rect({ width: 70, height: 100, x: 50, y: 60 })) - - g.addTo(canvas) - - expect(g.width(100)).toBe(g) - expect(g.bbox().width).toBe(100) - - const rbox1 = g.children()[0].rbox() - const rbox2 = g.children()[1].rbox() - - expect(rbox1.width).toBeCloseTo(90.9, 1) - expect(Math.floor(rbox2.width * 10) / 10).toBeCloseTo(63.6, 1) // Browsers have different opinion on this one (chrome: 63.6, ff: 63.7) - - expect(rbox1.x).toBeCloseTo(10, 3) - expect(rbox2.x).toBeCloseTo(46.4, 1) - }) - }) - - describe('height()', () => { - it('gets the height value of the bbox', () => { - const canvas = SVG().addTo(container) - - const g = new G() - g.add(new Rect({ width: 100, height: 120, x: 10, y: 20 })) - g.add(new Rect({ width: 70, height: 100, x: 50, y: 60 })) - - g.addTo(canvas) - - expect(g.height()).toBe(g.bbox().height) - expect(g.height()).toBe(140) - }) - it('sets the height value of the bbox by moving all children', () => { - const canvas = SVG().addTo(container) - - const g = new G() - g.add(new Rect({ width: 100, height: 120, x: 10, y: 20 })) - g.add(new Rect({ width: 70, height: 100, x: 50, y: 60 })) - - g.addTo(canvas) - - expect(g.height(100)).toBe(g) - expect(g.bbox().height).toBeCloseTo(100, 3) - - const rbox1 = g.children()[0].rbox() - const rbox2 = g.children()[1].rbox() - - expect(rbox1.height).toBeCloseTo(85.7, 1) - expect(rbox2.height).toBeCloseTo(71.4, 1) - - expect(rbox1.y).toBeCloseTo(20, 3) - expect(rbox2.y).toBeCloseTo(48.6, 1) - }) - }) }) diff --git a/spec/spec/modules/core/circled.js b/spec/spec/modules/core/circled.js index f74332a..a6324d8 100644 --- a/spec/spec/modules/core/circled.js +++ b/spec/spec/modules/core/circled.js @@ -8,7 +8,7 @@ describe('circled.js', () => { let element beforeEach(() => { - element = new Ellipse(50, 50) + element = new Ellipse().size(50, 50) }) describe('rx()', () => { diff --git a/spec/spec/modules/core/containerGeometry.js b/spec/spec/modules/core/containerGeometry.js new file mode 100644 index 0000000..3bdb109 --- /dev/null +++ b/spec/spec/modules/core/containerGeometry.js @@ -0,0 +1,271 @@ +/* globals describe, expect, it, jasmine, spyOn, container */ + +import { Box, G, Rect, SVG } from '../../../../src/main.js' + +const { any, objectContaining } = jasmine + +describe('containerGeometry.js', () => { + + describe('dmove()', () => { + it('moves the bbox of the group by a certain amount (1)', () => { + const canvas = SVG().addTo(container) + const g = canvas.group() + + g.add(new Rect({ width: 100, height: 120, x: 10, y: 20 })) + g.add(new Rect({ width: 70, height: 100, x: 50, y: 60 })) + + g.dmove(10, 10) + + const box = g.bbox() + expect(box).toEqual(objectContaining({ + x: 20, y: 30, width: box.width, height: box.height + })) + }) + + it('moves the bbox of the group by a certain amount (2)', () => { + const canvas = SVG().addTo(container) + const g = canvas.group() + + g.rect(400, 200).move(123, 312).rotate(34).skew(12) + g.rect(100, 50).move(11, 43).translate(123, 32).skew(-12) + g.rect(400, 200).rotate(90) + g.group().rotate(23).group().skew(32).rect(100, 40).skew(11).rotate(12) + + const oldBox = g.bbox() + + g.dmove(10, 10) + + const newBox = g.bbox() + + expect(newBox.x).toBeCloseTo(oldBox.x + 10, 4) + expect(newBox.y).toBeCloseTo(oldBox.y + 10, 4) + expect(newBox.w).toBeCloseTo(oldBox.w, 4) + expect(newBox.h).toBeCloseTo(oldBox.h, 4) + }) + }) + + describe('dx()', () => { + it('calls dmove with dy=0 and returns itself', () => { + const canvas = SVG().addTo(container) + const g = canvas.group() + const spy = spyOn(g, 'dmove').and.callThrough() + expect(g.dx(10)).toBe(g) + expect(spy).toHaveBeenCalledWith(10, 0) + }) + }) + + describe('dy()', () => { + it('calls dmove with dx=0 and returns itself', () => { + const canvas = SVG().addTo(container) + const g = canvas.group() + const spy = spyOn(g, 'dmove').and.callThrough() + expect(g.dy(10)).toBe(g) + expect(spy).toHaveBeenCalledWith(0, 10) + }) + }) + + describe('move()', () => { + it('calls dmove() with the correct difference', () => { + const canvas = SVG().addTo(container) + const g = canvas.group() + g.rect(100, 200).move(111, 223) + + spyOn(g, 'dmove') + + g.move(100, 150) + expect(g.dmove).toHaveBeenCalledWith(-11, -73) + }) + + it('defaults to x=0 and y=0', () => { + const canvas = SVG().addTo(container) + const g = canvas.group() + g.rect(100, 200).move(111, 223) + + spyOn(g, 'dmove') + + g.move() + expect(g.dmove).toHaveBeenCalledWith(-111, -223) + }) + }) + + describe('x()', () => { + it('gets the x value of the bbox', () => { + const canvas = SVG().addTo(container) + + const g = new G() + g.add(new Rect({ width: 100, height: 120, x: 10, y: 20 })) + g.add(new Rect({ width: 70, height: 100, x: 50, y: 60 })) + + g.addTo(canvas) + + expect(g.x()).toBe(g.bbox().x) + expect(g.x()).toBe(10) + }) + it('calls move with the paramater as x', () => { + const canvas = SVG().addTo(container) + const g = canvas.group() + g.rect(100, 200).move(111, 223) + + spyOn(g, 'move') + + g.x(100) + expect(g.move).toHaveBeenCalledWith(100, g.bbox().y, any(Box)) + }) + }) + + describe('y()', () => { + it('gets the y value of the bbox', () => { + const canvas = SVG().addTo(container) + + const g = new G() + g.add(new Rect({ width: 100, height: 120, x: 10, y: 20 })) + g.add(new Rect({ width: 70, height: 100, x: 50, y: 60 })) + + g.addTo(canvas) + + expect(g.y()).toBe(g.bbox().y) + expect(g.y()).toBe(20) + }) + + it('calls move with the paramater as y', () => { + const canvas = SVG().addTo(container) + const g = canvas.group() + g.rect(100, 200).move(111, 223) + + spyOn(g, 'move') + + g.y(100) + expect(g.move).toHaveBeenCalledWith(g.bbox().x, 100, any(Box)) + }) + }) + + describe('size()', () => { + it('changes the dimensions of the bbox (1)', () => { + const canvas = SVG().addTo(container) + + const g = new G() + g.add(new Rect({ width: 100, height: 120, x: 10, y: 20 })) + g.add(new Rect({ width: 70, height: 100, x: 50, y: 60 })) + + g.addTo(canvas) + + const oldBox = g.bbox() + + expect(g.size(100, 100)).toBe(g) + + const newBox = g.bbox() + + expect(newBox.x).toBeCloseTo(oldBox.x, 4) + expect(newBox.y).toBeCloseTo(oldBox.y, 4) + expect(newBox.w).toBeCloseTo(100, 4) + expect(newBox.h).toBeCloseTo(100, 4) + + const rbox1 = g.children()[0].rbox() + const rbox2 = g.children()[1].rbox() + + expect(rbox1.width).toBeCloseTo(90.9, 1) + expect(Math.floor(rbox2.width * 10) / 10).toBeCloseTo(63.6, 1) // Browsers have different opinion on this one (chrome: 63.6, ff: 63.7) + + expect(rbox1.x).toBeCloseTo(10, 1) + expect(rbox2.x).toBeCloseTo(46.4, 1) + expect(rbox1.height).toBeCloseTo(85.7, 1) + expect(rbox2.height).toBeCloseTo(71.4, 1) + expect(rbox1.y).toBeCloseTo(20, 1) + expect(rbox2.y).toBeCloseTo(48.6, 1) + }) + + it('changes the dimensions of the bbox (2)', () => { + const canvas = SVG().addTo(container) + const g = canvas.group() + + g.rect(400, 200).move(123, 312).rotate(34).skew(12) + g.rect(100, 50).move(11, 43).translate(123, 32).skew(-12) + g.rect(400, 200).rotate(90) + g.group().rotate(23).group().skew(32).rect(100, 40).skew(11).rotate(12) + + const oldBox = g.bbox() + + g.size(100, 100) + + const newBox = g.bbox() + + expect(newBox.x).toBeCloseTo(oldBox.x, 4) + expect(newBox.y).toBeCloseTo(oldBox.y, 4) + expect(newBox.w).toBeCloseTo(100, 4) + expect(newBox.h).toBeCloseTo(100, 4) + }) + + }) + + describe('width()', () => { + it('gets the width value of the bbox', () => { + const canvas = SVG().addTo(container) + + const g = new G() + g.add(new Rect({ width: 100, height: 120, x: 10, y: 20 })) + g.add(new Rect({ width: 70, height: 100, x: 50, y: 60 })) + + g.addTo(canvas) + + expect(g.width()).toBe(g.bbox().width) + expect(g.width()).toBe(110) + }) + it('sets the width value of the bbox by moving all children', () => { + const canvas = SVG().addTo(container) + + const g = new G() + g.add(new Rect({ width: 100, height: 120, x: 10, y: 20 })) + g.add(new Rect({ width: 70, height: 100, x: 50, y: 60 })) + + g.addTo(canvas) + + expect(g.width(100)).toBe(g) + expect(g.bbox().width).toBe(100) + + const rbox1 = g.children()[0].rbox() + const rbox2 = g.children()[1].rbox() + + expect(rbox1.width).toBeCloseTo(90.9, 1) + expect(Math.floor(rbox2.width * 10) / 10).toBeCloseTo(63.6, 1) // Browsers have different opinion on this one (chrome: 63.6, ff: 63.7) + + expect(rbox1.x).toBeCloseTo(10, 3) + expect(rbox2.x).toBeCloseTo(46.4, 1) + }) + }) + + describe('height()', () => { + it('gets the height value of the bbox', () => { + const canvas = SVG().addTo(container) + + const g = new G() + g.add(new Rect({ width: 100, height: 120, x: 10, y: 20 })) + g.add(new Rect({ width: 70, height: 100, x: 50, y: 60 })) + + g.addTo(canvas) + + expect(g.height()).toBe(g.bbox().height) + expect(g.height()).toBe(140) + }) + it('sets the height value of the bbox by moving all children', () => { + const canvas = SVG().addTo(container) + + const g = new G() + g.add(new Rect({ width: 100, height: 120, x: 10, y: 20 })) + g.add(new Rect({ width: 70, height: 100, x: 50, y: 60 })) + + g.addTo(canvas) + + expect(g.height(100)).toBe(g) + expect(g.bbox().height).toBeCloseTo(100, 3) + + const rbox1 = g.children()[0].rbox() + const rbox2 = g.children()[1].rbox() + + expect(rbox1.height).toBeCloseTo(85.7, 1) + expect(rbox2.height).toBeCloseTo(71.4, 1) + + expect(rbox1.y).toBeCloseTo(20, 3) + expect(rbox2.y).toBeCloseTo(48.6, 1) + }) + }) +}) diff --git a/spec/spec/modules/core/selector.js b/spec/spec/modules/core/selector.js index 7a17466..1336059 100644 --- a/spec/spec/modules/core/selector.js +++ b/spec/spec/modules/core/selector.js @@ -6,9 +6,9 @@ import { getWindow } from '../../../../src/utils/window.js' describe('selector.js', () => { describe('baseFind()', () => { it('finds all elements of a selector in the document', () => { - const div = SVG('<div>', true).id('foo').addTo(container) - const span = SVG('<span>', true).addClass('bar').addTo(div) - const span2 = SVG('<span>', true).addTo(div) + const div = SVG('<div />', true).id('foo').addTo(container) + const span = SVG('<span />', true).addClass('bar').addTo(div) + const span2 = SVG('<span />', true).addTo(div) expect(find('#canvas').map(el => el.node)).toEqual([ container ]) expect(find('span')).toEqual([ span, span2 ]) @@ -17,7 +17,7 @@ describe('selector.js', () => { }) it('finds all elements of a selector scoped to an element', () => { - const div = SVG('<div>', true).id('foo').addTo(container) + const div = SVG('<div />', true).id('foo').addTo(container) expect(find('#canvas', getWindow().document)[0].node).toBe(container) expect(find('#foo', container)).toEqual([ div ]) diff --git a/spec/spec/utils/adopter.js b/spec/spec/utils/adopter.js index 5ed6713..60c1642 100644 --- a/spec/spec/utils/adopter.js +++ b/spec/spec/utils/adopter.js @@ -75,7 +75,7 @@ describe('adopter.js', () => { }) it('creates an element in the html namespace from passed html string', () => { - const div = makeInstance('<div>', true) + const div = makeInstance('<div />', true) expect(adoptSpy).toHaveBeenCalledWith(any(Node)) expect(adoptSpy).toHaveBeenCalledWith(objectContaining({ nodeName: 'DIV', namespaceURI: 'http://www.w3.org/1999/xhtml' })) diff --git a/src/animation/Controller.js b/src/animation/Controller.js index 972679e..ae49de9 100644 --- a/src/animation/Controller.js +++ b/src/animation/Controller.js @@ -128,13 +128,14 @@ export class Controller extends Stepper { this.stepper = fn } + done (c) { + return c.done + } + step (current, target, dt, c) { return this.stepper(current, target, dt, c) } - done (c) { - return c.done - } } function recalculate () { diff --git a/src/animation/Morphable.js b/src/animation/Morphable.js index 2d48e10..46dd166 100644 --- a/src/animation/Morphable.js +++ b/src/animation/Morphable.js @@ -49,6 +49,25 @@ export default class Morphable { this._morphObj = null } + at (pos) { + var _this = this + + return this._morphObj.fromArray( + this._from.map(function (i, index) { + return _this._stepper.step(i, _this._to[index], pos, _this._context[index], _this._context) + }) + ) + } + + done () { + var complete = this._context + .map(this._stepper.done) + .reduce(function (last, curr) { + return last && curr + }, true) + return complete + } + from (val) { if (val == null) { return this._from @@ -58,6 +77,12 @@ export default class Morphable { return this } + stepper (stepper) { + if (stepper == null) return this._stepper + this._stepper = stepper + return this + } + to (val) { if (val == null) { return this._to @@ -109,30 +134,6 @@ export default class Morphable { return result } - stepper (stepper) { - if (stepper == null) return this._stepper - this._stepper = stepper - return this - } - - done () { - var complete = this._context - .map(this._stepper.done) - .reduce(function (last, curr) { - return last && curr - }, true) - return complete - } - - at (pos) { - var _this = this - - return this._morphObj.fromArray( - this._from.map(function (i, index) { - return _this._stepper.step(i, _this._to[index], pos, _this._context[index], _this._context) - }) - ) - } } export class NonMorphable { @@ -146,13 +147,14 @@ export class NonMorphable { return this } + toArray () { + return [ this.value ] + } + valueOf () { return this.value } - toArray () { - return [ this.value ] - } } export class TransformBag { @@ -214,6 +216,17 @@ export class ObjectBag { this.init(...args) } + align (other) { + for (let i = 0, il = this.values.length; i < il; ++i) { + if (this.values[i] === Color) { + const space = other[i + 6] + const color = new Color(this.values.splice(i + 2, 5))[space]().toArray() + this.values.splice(i + 2, 0, ...color) + } + } + return this + } + init (objOrArr) { this.values = [] @@ -237,6 +250,10 @@ export class ObjectBag { return this } + toArray () { + return this.values + } + valueOf () { var obj = {} var arr = this.values @@ -253,20 +270,6 @@ export class ObjectBag { return obj } - toArray () { - return this.values - } - - align (other) { - for (let i = 0, il = this.values.length; i < il; ++i) { - if (this.values[i] === Color) { - const space = other[i + 6] - const color = new Color(this.values.splice(i + 2, 5))[space]().toArray() - this.values.splice(i + 2, 0, ...color) - } - } - return this - } } const morphableTypes = [ diff --git a/src/animation/Queue.js b/src/animation/Queue.js index 1858b99..e01c3d6 100644 --- a/src/animation/Queue.js +++ b/src/animation/Queue.js @@ -4,6 +4,16 @@ export default class Queue { this._last = null } + // Shows us the first item in the list + first () { + return this._first && this._first.value + } + + // Shows us the last item in the list + last () { + return this._last && this._last.value + } + push (value) { // An item stores an id and the provided value var item = typeof value.next !== 'undefined' ? value : { value: value, next: null, prev: null } @@ -22,28 +32,6 @@ export default class Queue { return item } - shift () { - // Check if we have a value - var remove = this._first - if (!remove) return null - - // If we do, remove it and relink things - this._first = remove.next - if (this._first) this._first.prev = null - this._last = this._first ? this._last : null - return remove.value - } - - // Shows us the first item in the list - first () { - return this._first && this._first.value - } - - // Shows us the last item in the list - last () { - return this._last && this._last.value - } - // Removes the item that was returned from the push remove (item) { // Relink the previous item @@ -56,4 +44,17 @@ export default class Queue { item.prev = null item.next = null } + + shift () { + // Check if we have a value + var remove = this._first + if (!remove) return null + + // If we do, remove it and relink things + this._first = remove.next + if (this._first) this._first.prev = null + this._last = this._first ? this._last : null + return remove.value + } + } diff --git a/src/animation/Runner.js b/src/animation/Runner.js index e0ac5a8..bd60915 100644 --- a/src/animation/Runner.js +++ b/src/animation/Runner.js @@ -71,27 +71,55 @@ export default class Runner extends EventTarget { this._persist = this._isDeclarative ? true : null } - /* - Runner Definitions - ================== - These methods help us define the runtime behaviour of the Runner or they - help us make new runners from the current runner - */ + static sanitise (duration, delay, when) { + // Initialise the default parameters + var times = 1 + var swing = false + var wait = 0 + duration = duration || timeline.duration + delay = delay || timeline.delay + when = when || 'last' - element (element) { - if (element == null) return this._element - this._element = element - element._prepareRunner() + // If we have an object, unpack the values + if (typeof duration === 'object' && !(duration instanceof Stepper)) { + delay = duration.delay || delay + when = duration.when || when + swing = duration.swing || swing + times = duration.times || times + wait = duration.wait || wait + duration = duration.duration || timeline.duration + } + + return { + duration: duration, + delay: delay, + swing: swing, + times: times, + wait: wait, + when: when + } + } + + active (enabled) { + if (enabled == null) return this.enabled + this.enabled = enabled return this } - timeline (timeline) { - // check explicitly for undefined so we can set the timeline to null - if (typeof timeline === 'undefined') return this._timeline - this._timeline = timeline + /* + Private Methods + =============== + Methods that shouldn't be used externally + */ + addTransform (transform, index) { + this.transforms.lmultiplyO(transform) return this } + after (fn) { + return this.on('finished', fn) + } + animate (duration, delay, when) { var o = Runner.sanitise(duration, delay, when) var runner = new Runner(o.duration) @@ -100,30 +128,54 @@ export default class Runner extends EventTarget { return runner.loop(o).schedule(o.delay, o.when) } - schedule (timeline, delay, when) { - // The user doesn't need to pass a timeline if we already have one - if (!(timeline instanceof Timeline)) { - when = delay - delay = timeline - timeline = this.timeline() - } + clearTransform () { + this.transforms = new Matrix() + return this + } - // If there is no timeline, yell at the user... - if (!timeline) { - throw Error('Runner cannot be scheduled without timeline') + // TODO: Keep track of all transformations so that deletion is faster + clearTransformsFromQueue () { + if (!this.done || !this._timeline || !this._timeline._runnerIds.includes(this.id)) { + this._queue = this._queue.filter((item) => { + return !item.isTransform + }) } + } - // Schedule the runner on the timeline provided - timeline.schedule(this, delay, when) + delay (delay) { + return this.animate(0, delay) + } + + duration () { + return this._times * (this._wait + this._duration) - this._wait + } + + during (fn) { + return this.queue(null, fn) + } + + ease (fn) { + this._stepper = new Ease(fn) return this } + /* + Runner Definitions + ================== + These methods help us define the runtime behaviour of the Runner or they + help us make new runners from the current runner + */ - unschedule () { - var timeline = this.timeline() - timeline && timeline.unschedule(this) + element (element) { + if (element == null) return this._element + this._element = element + element._prepareRunner() return this } + finish () { + return this.step(Infinity) + } + loop (times, swing, wait) { // Deal with the user passing in an object if (typeof times === 'object') { @@ -143,57 +195,6 @@ export default class Runner extends EventTarget { return this } - delay (delay) { - return this.animate(0, delay) - } - - /* - Basic Functionality - =================== - These methods allow us to attach basic functions to the runner directly - */ - - queue (initFn, runFn, retargetFn, isTransform) { - this._queue.push({ - initialiser: initFn || noop, - runner: runFn || noop, - retarget: retargetFn, - isTransform: isTransform, - initialised: false, - finished: false - }) - var timeline = this.timeline() - timeline && this.timeline()._continue() - return this - } - - during (fn) { - return this.queue(null, fn) - } - - after (fn) { - return this.on('finished', fn) - } - - /* - Runner animation methods - ======================== - Control how the animation plays - */ - - time (time) { - if (time == null) { - return this._time - } - const dt = time - this._time - this.step(dt) - return this - } - - duration () { - return this._times * (this._wait + this._duration) - this._wait - } - loops (p) { var loopDuration = this._duration + this._wait if (p == null) { @@ -264,6 +265,55 @@ export default class Runner extends EventTarget { return this.time(p * this.duration()) } + /* + Basic Functionality + =================== + These methods allow us to attach basic functions to the runner directly + */ + queue (initFn, runFn, retargetFn, isTransform) { + this._queue.push({ + initialiser: initFn || noop, + runner: runFn || noop, + retarget: retargetFn, + isTransform: isTransform, + initialised: false, + finished: false + }) + var timeline = this.timeline() + timeline && this.timeline()._continue() + return this + } + + reset () { + if (this._reseted) return this + this.time(0) + this._reseted = true + return this + } + + reverse (reverse) { + this._reverse = reverse == null ? !this._reverse : reverse + return this + } + + schedule (timeline, delay, when) { + // The user doesn't need to pass a timeline if we already have one + if (!(timeline instanceof Timeline)) { + when = delay + delay = timeline + timeline = this.timeline() + } + + // If there is no timeline, yell at the user... + if (!timeline) { + throw Error('Runner cannot be scheduled without timeline') + } + + // Schedule the runner on the timeline provided + timeline.schedule(this, delay, when) + return this + } + step (dt) { // If we are inactive, this stepper just gets skipped if (!this.enabled) return this @@ -315,38 +365,54 @@ export default class Runner extends EventTarget { return this } - reset () { - if (this._reseted) return this - this.time(0) - this._reseted = true + /* + Runner animation methods + ======================== + Control how the animation plays + */ + time (time) { + if (time == null) { + return this._time + } + const dt = time - this._time + this.step(dt) return this } - finish () { - return this.step(Infinity) - } - - reverse (reverse) { - this._reverse = reverse == null ? !this._reverse : reverse + timeline (timeline) { + // check explicitly for undefined so we can set the timeline to null + if (typeof timeline === 'undefined') return this._timeline + this._timeline = timeline return this } - ease (fn) { - this._stepper = new Ease(fn) + unschedule () { + var timeline = this.timeline() + timeline && timeline.unschedule(this) return this } - active (enabled) { - if (enabled == null) return this.enabled - this.enabled = enabled - return this - } + // Run each initialise function in the runner if required + _initialise (running) { + // If we aren't running, we shouldn't initialise when not declarative + if (!running && !this._isDeclarative) return - /* - Private Methods - =============== - Methods that shouldn't be used externally - */ + // Loop through all of the initialisers + for (var i = 0, len = this._queue.length; i < len; ++i) { + // Get the current initialiser + var current = this._queue[i] + + // Determine whether we need to initialise + var needsIt = this._isDeclarative || (!current.initialised && running) + running = !current.finished + + // Call the initialiser if we need to + if (needsIt && running) { + current.initialiser.call(this) + current.initialised = true + } + } + } // Save a morpher to the morpher list so that we can retarget it later _rememberMorpher (method, morpher) { @@ -368,6 +434,25 @@ export default class Runner extends EventTarget { } // Try to set the target for a morpher if the morpher exists, otherwise + // Run each run function for the position or dt given + _run (positionOrDt) { + // Run all of the _queue directly + var allfinished = true + for (var i = 0, len = this._queue.length; i < len; ++i) { + // Get the current function to run + var current = this._queue[i] + + // Run the function if its not finished, we keep track of the finished + // flag for the sake of declarative _queue + var converged = current.runner.call(this, positionOrDt) + current.finished = current.finished || (converged === true) + allfinished = allfinished && current.finished + } + + // We report when all of the constructors are finished + return allfinished + } + // do nothing and return false _tryRetarget (method, target, extra) { if (this._history[method]) { @@ -395,94 +480,6 @@ export default class Runner extends EventTarget { return false } - // Run each initialise function in the runner if required - _initialise (running) { - // If we aren't running, we shouldn't initialise when not declarative - if (!running && !this._isDeclarative) return - - // Loop through all of the initialisers - for (var i = 0, len = this._queue.length; i < len; ++i) { - // Get the current initialiser - var current = this._queue[i] - - // Determine whether we need to initialise - var needsIt = this._isDeclarative || (!current.initialised && running) - running = !current.finished - - // Call the initialiser if we need to - if (needsIt && running) { - current.initialiser.call(this) - current.initialised = true - } - } - } - - // Run each run function for the position or dt given - _run (positionOrDt) { - // Run all of the _queue directly - var allfinished = true - for (var i = 0, len = this._queue.length; i < len; ++i) { - // Get the current function to run - var current = this._queue[i] - - // Run the function if its not finished, we keep track of the finished - // flag for the sake of declarative _queue - var converged = current.runner.call(this, positionOrDt) - current.finished = current.finished || (converged === true) - allfinished = allfinished && current.finished - } - - // We report when all of the constructors are finished - return allfinished - } - - addTransform (transform, index) { - this.transforms.lmultiplyO(transform) - return this - } - - clearTransform () { - this.transforms = new Matrix() - return this - } - - // TODO: Keep track of all transformations so that deletion is faster - clearTransformsFromQueue () { - if (!this.done || !this._timeline || !this._timeline._runnerIds.includes(this.id)) { - this._queue = this._queue.filter((item) => { - return !item.isTransform - }) - } - } - - static sanitise (duration, delay, when) { - // Initialise the default parameters - var times = 1 - var swing = false - var wait = 0 - duration = duration || timeline.duration - delay = delay || timeline.delay - when = when || 'last' - - // If we have an object, unpack the values - if (typeof duration === 'object' && !(duration instanceof Stepper)) { - delay = duration.delay || delay - when = duration.when || when - swing = duration.swing || swing - times = duration.times || times - wait = duration.wait || wait - duration = duration.duration || timeline.duration - } - - return { - duration: duration, - delay: delay, - swing: swing, - times: times, - wait: wait, - when: when - } - } } Runner.id = 0 @@ -543,17 +540,29 @@ export class RunnerArray { return this } - getByID (id) { - return this.runners[this.ids.indexOf(id + 1)] + clearBefore (id) { + const deleteCnt = this.ids.indexOf(id + 1) || 1 + this.ids.splice(0, deleteCnt, 0) + this.runners.splice(0, deleteCnt, new FakeRunner()) + .forEach((r) => r.clearTransformsFromQueue()) + return this } - remove (id) { + edit (id, newRunner) { const index = this.ids.indexOf(id + 1) - this.ids.splice(index, 1) - this.runners.splice(index, 1) + this.ids.splice(index, 1, id + 1) + this.runners.splice(index, 1, newRunner) return this } + getByID (id) { + return this.runners[this.ids.indexOf(id + 1)] + } + + length () { + return this.ids.length + } + merge () { let lastRunner = null for (let i = 0; i < this.runners.length; ++i) { @@ -580,24 +589,13 @@ export class RunnerArray { return this } - edit (id, newRunner) { + remove (id) { const index = this.ids.indexOf(id + 1) - this.ids.splice(index, 1, id + 1) - this.runners.splice(index, 1, newRunner) + this.ids.splice(index, 1) + this.runners.splice(index, 1) return this } - length () { - return this.ids.length - } - - clearBefore (id) { - const deleteCnt = this.ids.indexOf(id + 1) || 1 - this.ids.splice(0, deleteCnt, 0) - this.runners.splice(0, deleteCnt, new FakeRunner()) - .forEach((r) => r.clearTransformsFromQueue()) - return this - } } registerMethods({ diff --git a/src/animation/Timeline.js b/src/animation/Timeline.js index d175ae6..a29d683 100644 --- a/src/animation/Timeline.js +++ b/src/animation/Timeline.js @@ -44,6 +44,62 @@ export default class Timeline extends EventTarget { this._stepImmediate = this._stepFn.bind(this, true) } + active () { + return !!this._nextFrame + } + + finish () { + // Go to end and pause + this.time(this.getEndTimeOfTimeline() + 1) + return this.pause() + } + + // Calculates the end of the timeline + getEndTime () { + var lastRunnerInfo = this.getLastRunnerInfo() + var lastDuration = lastRunnerInfo ? lastRunnerInfo.runner.duration() : 0 + var lastStartTime = lastRunnerInfo ? lastRunnerInfo.start : this._time + return lastStartTime + lastDuration + } + + getEndTimeOfTimeline () { + const endTimes = this._runners.map((i) => i.start + i.runner.duration()) + return Math.max(0, ...endTimes) + } + + getLastRunnerInfo () { + return this.getRunnerInfoById(this._lastRunnerId) + } + + getRunnerInfoById (id) { + return this._runners[this._runnerIds.indexOf(id)] || null + } + + pause () { + this._paused = true + return this._continue() + } + + persist (dtOrForever) { + if (dtOrForever == null) return this._persist + this._persist = dtOrForever + return this + } + + play () { + // Now make sure we are not paused and continue the animation + this._paused = false + return this.updateTime()._continue() + } + + reverse (yes) { + var currentSpeed = this.speed() + if (yes == null) return this.speed(-currentSpeed) + + var positive = Math.abs(currentSpeed) + return this.speed(yes ? -positive : positive) + } + // schedules a runner on the timeline schedule (runner, delay, when) { if (runner == null) { @@ -102,56 +158,20 @@ export default class Timeline extends EventTarget { return this } - // Remove the runner from this timeline - unschedule (runner) { - var index = this._runnerIds.indexOf(runner.id) - if (index < 0) return this - - this._runners.splice(index, 1) - this._runnerIds.splice(index, 1) - - runner.timeline(null) - return this - } - - getRunnerInfoById (id) { - return this._runners[this._runnerIds.indexOf(id)] || null - } - - getLastRunnerInfo () { - return this.getRunnerInfoById(this._lastRunnerId) - } - - // Calculates the end of the timeline - getEndTime () { - var lastRunnerInfo = this.getLastRunnerInfo() - var lastDuration = lastRunnerInfo ? lastRunnerInfo.runner.duration() : 0 - var lastStartTime = lastRunnerInfo ? lastRunnerInfo.start : this._time - return lastStartTime + lastDuration - } - - getEndTimeOfTimeline () { - const endTimes = this._runners.map((i) => i.start + i.runner.duration()) - return Math.max(0, ...endTimes) + seek (dt) { + return this.time(this._time + dt) } - // Makes sure, that after pausing the time doesn't jump - updateTime () { - if (!this.active()) { - this._lastSourceTime = this._timeSource() - } + source (fn) { + if (fn == null) return this._timeSource + this._timeSource = fn return this } - play () { - // Now make sure we are not paused and continue the animation - this._paused = false - return this.updateTime()._continue() - } - - pause () { - this._paused = true - return this._continue() + speed (speed) { + if (speed == null) return this._speed + this._speed = speed + return this } stop () { @@ -160,45 +180,41 @@ export default class Timeline extends EventTarget { return this.pause() } - finish () { - // Go to end and pause - this.time(this.getEndTimeOfTimeline() + 1) - return this.pause() + time (time) { + if (time == null) return this._time + this._time = time + return this._continue(true) } - speed (speed) { - if (speed == null) return this._speed - this._speed = speed - return this - } + // Remove the runner from this timeline + unschedule (runner) { + var index = this._runnerIds.indexOf(runner.id) + if (index < 0) return this - reverse (yes) { - var currentSpeed = this.speed() - if (yes == null) return this.speed(-currentSpeed) + this._runners.splice(index, 1) + this._runnerIds.splice(index, 1) - var positive = Math.abs(currentSpeed) - return this.speed(yes ? -positive : positive) + runner.timeline(null) + return this } - seek (dt) { - return this.time(this._time + dt) + // Makes sure, that after pausing the time doesn't jump + updateTime () { + if (!this.active()) { + this._lastSourceTime = this._timeSource() + } + return this } - time (time) { - if (time == null) return this._time - this._time = time - return this._continue(true) - } + // Checks if we are running and continues the animation + _continue (immediateStep = false) { + Animator.cancelFrame(this._nextFrame) + this._nextFrame = null - persist (dtOrForever) { - if (dtOrForever == null) return this._persist - this._persist = dtOrForever - return this - } + if (immediateStep) return this._stepImmediate() + if (this._paused) return this - source (fn) { - if (fn == null) return this._timeSource - this._timeSource = fn + this._nextFrame = Animator.frame(this._step) return this } @@ -303,21 +319,6 @@ export default class Timeline extends EventTarget { return this } - // Checks if we are running and continues the animation - _continue (immediateStep = false) { - Animator.cancelFrame(this._nextFrame) - this._nextFrame = null - - if (immediateStep) return this._stepImmediate() - if (this._paused) return this - - this._nextFrame = Animator.frame(this._step) - return this - } - - active () { - return !!this._nextFrame - } } registerMethods({ diff --git a/src/elements/A.js b/src/elements/A.js index 6f9bec2..478c36d 100644 --- a/src/elements/A.js +++ b/src/elements/A.js @@ -1,24 +1,28 @@ -import { nodeOrNew, register, wrapWithAttrCheck } from '../utils/adopter.js' +import { nodeOrNew, register, wrapWithAttrCheck, extend } from '../utils/adopter.js' import { registerMethods } from '../utils/methods.js' import { xlink } from '../modules/core/namespaces.js' import Container from './Container.js' +import * as containerGeometry from '../modules/core/containerGeometry.js' export default class A extends Container { constructor (node, attrs = node) { super(nodeOrNew('a', node), attrs) } + // Link target attribute + target (target) { + return this.attr('target', target) + } + // Link url to (url) { return this.attr('href', url, xlink) } - // Link target attribute - target (target) { - return this.attr('target', target) - } } +extend(A, containerGeometry) + registerMethods({ Container: { // Create a hyperlink element diff --git a/src/elements/Element.js b/src/elements/Element.js index bb11611..f39f777 100644 --- a/src/elements/Element.js +++ b/src/elements/Element.js @@ -71,12 +71,6 @@ export default class Element extends Dom { return this.y(new SVGNumber(y).plus(this.y())) } - // Get parent document - root () { - const p = this.parent(getClass(root)) - return p && p.root() - } - getEventHolder () { return this } @@ -121,6 +115,12 @@ export default class Element extends Dom { return m ? makeInstance(m[1]) : null } + // Get parent document + root () { + const p = this.parent(getClass(root)) + return p && p.root() + } + // set given data to the elements data property setData (o) { this.dom = o diff --git a/src/elements/G.js b/src/elements/G.js index 7677b92..b3a999e 100644 --- a/src/elements/G.js +++ b/src/elements/G.js @@ -1,82 +1,16 @@ -import { nodeOrNew, register, wrapWithAttrCheck } from '../utils/adopter.js' -import { proportionalSize } from '../utils/utils.js' +import { nodeOrNew, register, wrapWithAttrCheck, extend } from '../utils/adopter.js' import { registerMethods } from '../utils/methods.js' import Container from './Container.js' -import Matrix from '../types/Matrix.js' -import Point from '../types/Point.js' +import * as containerGeometry from '../modules/core/containerGeometry.js' export default class G extends Container { constructor (node, attrs = node) { super(nodeOrNew('g', node), attrs) } - - x (x, box = this.bbox()) { - if (x == null) return box.x - return this.move(x, box.y, box) - } - - y (y, box = this.bbox()) { - if (y == null) return box.y - return this.move(box.x, y, box) - } - - move (x = 0, y = 0, box = this.bbox()) { - const dx = x - box.x - const dy = y - box.y - - return this.dmove(dx, dy) - } - - dx (dx) { - return this.dmove(dx, 0) - } - - dy (dy) { - return this.dmove(0, dy) - } - - dmove (dx, dy) { - this.children().forEach((child, i) => { - // Get the childs bbox - const bbox = child.bbox() - // Get childs matrix - const m = new Matrix(child) - // Translate childs matrix by amount and - // transform it back into parents space - const matrix = m.translate(dx, dy).transform(m.inverse()) - // Calculate new x and y from old box - const p = new Point(bbox.x, bbox.y).transform(matrix) - // Move element - child.move(p.x, p.y) - }) - - return this - } - - width (width, box = this.bbox()) { - if (width == null) return box.width - return this.size(width, box.height, box) - } - - height (height, box = this.bbox()) { - if (height == null) return box.height - return this.size(box.width, height, box) - } - - size (width, height, box = this.bbox()) { - const p = proportionalSize(this, width, height, box) - const scaleX = p.width / box.width - const scaleY = p.height / box.height - - this.children().forEach((child, i) => { - const o = new Point(box).transform(new Matrix(child).inverse()) - child.scale(scaleX, scaleY, o.x, o.y) - }) - - return this - } } +extend(G, containerGeometry) + registerMethods({ Container: { // Create a group element diff --git a/src/elements/Marker.js b/src/elements/Marker.js index b3077b1..56ac3e8 100644 --- a/src/elements/Marker.js +++ b/src/elements/Marker.js @@ -8,11 +8,6 @@ export default class Marker extends Container { super(nodeOrNew('marker', node), attrs) } - // Set width of element - width (width) { - return this.attr('markerWidth', width) - } - // Set height of element height (height) { return this.attr('markerHeight', height) @@ -27,6 +22,11 @@ export default class Marker extends Container { return this.attr('refX', x).attr('refY', y) } + // Return the fill id + toString () { + return 'url(#' + this.id() + ')' + } + // Update marker update (block) { // remove all content @@ -40,10 +40,11 @@ export default class Marker extends Container { return this } - // Return the fill id - toString () { - return 'url(#' + this.id() + ')' + // Set width of element + width (width) { + return this.attr('markerWidth', width) } + } registerMethods({ diff --git a/src/elements/Text.js b/src/elements/Text.js index 2951c2f..cc8db4a 100644 --- a/src/elements/Text.js +++ b/src/elements/Text.js @@ -21,53 +21,6 @@ export default class Text extends Shape { this._build = false // disable build mode for adding multiple lines } - // Set the text content - text (text) { - // act as getter - if (text === undefined) { - var children = this.node.childNodes - var firstLine = 0 - text = '' - - for (var i = 0, len = children.length; i < len; ++i) { - // skip textPaths - they are no lines - if (children[i].nodeName === 'textPath') { - if (i === 0) firstLine = 1 - continue - } - - // add newline if its not the first child and newLined is set to true - if (i !== firstLine && children[i].nodeType !== 3 && adopt(children[i]).dom.newLined === true) { - text += '\n' - } - - // add content of this node - text += children[i].textContent - } - - return text - } - - // remove existing content - this.clear().build(true) - - if (typeof text === 'function') { - // call block - text.call(this, this) - } else { - // store text and make sure text is not blank - text = (text + '').split('\n') - - // build new lines - for (var j = 0, jl = text.length; j < jl; j++) { - this.newLine(text[j]) - } - } - - // disable build mode and rebuild lines - return this.build(false).rebuild() - } - // Set / get leading leading (value) { // act as getter @@ -124,6 +77,54 @@ export default class Text extends Shape { this.dom.leading = new SVGNumber(o.leading || 1.3) return this } + + // Set the text content + text (text) { + // act as getter + if (text === undefined) { + var children = this.node.childNodes + var firstLine = 0 + text = '' + + for (var i = 0, len = children.length; i < len; ++i) { + // skip textPaths - they are no lines + if (children[i].nodeName === 'textPath') { + if (i === 0) firstLine = 1 + continue + } + + // add newline if its not the first child and newLined is set to true + if (i !== firstLine && children[i].nodeType !== 3 && adopt(children[i]).dom.newLined === true) { + text += '\n' + } + + // add content of this node + text += children[i].textContent + } + + return text + } + + // remove existing content + this.clear().build(true) + + if (typeof text === 'function') { + // call block + text.call(this, this) + } else { + // store text and make sure text is not blank + text = (text + '').split('\n') + + // build new lines + for (var j = 0, jl = text.length; j < jl; j++) { + this.newLine(text[j]) + } + } + + // disable build mode and rebuild lines + return this.build(false).rebuild() + } + } extend(Text, textable) diff --git a/src/elements/TextPath.js b/src/elements/TextPath.js index 9ed4a71..edac399 100644 --- a/src/elements/TextPath.js +++ b/src/elements/TextPath.js @@ -93,7 +93,7 @@ registerMethods({ targets () { return baseFind('svg textPath').filter((node) => { - return node.attr('href').includes(this.id()) + return (node.attr('href') || '').includes(this.id()) }) // Does not work in IE11. Use when IE support is dropped diff --git a/src/elements/Tspan.js b/src/elements/Tspan.js index 00934ab..59860f7 100644 --- a/src/elements/Tspan.js +++ b/src/elements/Tspan.js @@ -18,21 +18,6 @@ export default class Tspan extends Shape { this._build = false // disable build mode for adding multiple lines } - // Set text content - text (text) { - if (text == null) return this.node.textContent + (this.dom.newLined ? '\n' : '') - - if (typeof text === 'function') { - this.clear().build(true) - text.call(this, this) - this.build(false) - } else { - this.plain(text) - } - - return this - } - // Shortcut dx dx (dx) { return this.attr('dx', dx) @@ -65,6 +50,22 @@ export default class Tspan extends Shape { // apply new position return this.dy(i ? dy : 0).attr('x', text.x()) } + + // Set text content + text (text) { + if (text == null) return this.node.textContent + (this.dom.newLined ? '\n' : '') + + if (typeof text === 'function') { + this.clear().build(true) + text.call(this, this) + this.build(false) + } else { + this.plain(text) + } + + return this + } + } extend(Tspan, textable) diff --git a/src/modules/core/containerGeometry.js b/src/modules/core/containerGeometry.js new file mode 100644 index 0000000..50342b8 --- /dev/null +++ b/src/modules/core/containerGeometry.js @@ -0,0 +1,69 @@ +import Matrix from '../../types/Matrix.js' +import Point from '../../types/Point.js' +import { proportionalSize } from '../../utils/utils.js' + +export function dmove (dx, dy) { + this.children().forEach((child, i) => { + // Get the childs bbox + const bbox = child.bbox() + // Get childs matrix + const m = new Matrix(child) + // Translate childs matrix by amount and + // transform it back into parents space + const matrix = m.translate(dx, dy).transform(m.inverse()) + // Calculate new x and y from old box + const p = new Point(bbox.x, bbox.y).transform(matrix) + // Move element + child.move(p.x, p.y) + }) + + return this +} + +export function dx (dx) { + return this.dmove(dx, 0) +} + +export function dy (dy) { + return this.dmove(0, dy) +} + +export function height (height, box = this.bbox()) { + if (height == null) return box.height + return this.size(box.width, height, box) +} + +export function move (x = 0, y = 0, box = this.bbox()) { + const dx = x - box.x + const dy = y - box.y + + return this.dmove(dx, dy) +} + +export function size (width, height, box = this.bbox()) { + const p = proportionalSize(this, width, height, box) + const scaleX = p.width / box.width + const scaleY = p.height / box.height + + this.children().forEach((child, i) => { + const o = new Point(box).transform(new Matrix(child).inverse()) + child.scale(scaleX, scaleY, o.x, o.y) + }) + + return this +} + +export function width (width, box = this.bbox()) { + if (width == null) return box.width + return this.size(width, box.height, box) +} + +export function x (x, box = this.bbox()) { + if (x == null) return box.x + return this.move(x, box.y, box) +} + +export function y (y, box = this.bbox()) { + if (y == null) return box.y + return this.move(box.x, y, box) +} diff --git a/src/types/Box.js b/src/types/Box.js index 3e91c35..9707b7f 100644 --- a/src/types/Box.js +++ b/src/types/Box.js @@ -26,6 +26,13 @@ export default class Box { this.init(...args) } + addOffset () { + // offset by window scroll position, because getBoundingClientRect changes when window is scrolled + this.x += globals.window.pageXOffset + this.y += globals.window.pageYOffset + return new Box(this) + } + init (source) { var base = [ 0, 0, 0, 0 ] source = typeof source === 'string' ? source.split(delimiter).map(parseFloat) @@ -49,6 +56,10 @@ export default class Box { return this } + isNulled () { + return isNulledBox(this) + } + // Merge rect box with another, return a new instance merge (box) { const x = Math.min(this.x, box.x) @@ -59,6 +70,14 @@ export default class Box { return new Box(x, y, width, height) } + toArray () { + return [ this.x, this.y, this.width, this.height ] + } + + toString () { + return this.x + ' ' + this.y + ' ' + this.width + ' ' + this.height + } + transform (m) { if (!(m instanceof Matrix)) { m = new Matrix(m) @@ -91,24 +110,6 @@ export default class Box { ) } - addOffset () { - // offset by window scroll position, because getBoundingClientRect changes when window is scrolled - this.x += globals.window.pageXOffset - this.y += globals.window.pageYOffset - return new Box(this) - } - - toString () { - return this.x + ' ' + this.y + ' ' + this.width + ' ' + this.height - } - - toArray () { - return [ this.x, this.y, this.width, this.height ] - } - - isNulled () { - return isNulledBox(this) - } } function getBox (el, getBBoxFn, retry) { diff --git a/src/types/Color.js b/src/types/Color.js index eb6168f..d5f0efa 100644 --- a/src/types/Color.js +++ b/src/types/Color.js @@ -62,6 +62,152 @@ export default class Color { this.init(...inputs) } + // Test if given value is a color + static isColor (color) { + return color && ( + color instanceof Color + || this.isRgb(color) + || this.test(color) + ) + } + + // Test if given value is an rgb object + static isRgb (color) { + return color && typeof color.r === 'number' + && typeof color.g === 'number' + && typeof color.b === 'number' + } + + /* + Generating random colors + */ + static random (mode = 'vibrant', t, u) { + + // Get the math modules + const { random, round, sin, PI: pi } = Math + + // Run the correct generator + if (mode === 'vibrant') { + + const l = (81 - 57) * random() + 57 + const c = (83 - 45) * random() + 45 + const h = 360 * random() + const color = new Color(l, c, h, 'lch') + return color + + } else if (mode === 'sine') { + + t = t == null ? random() : t + const r = round(80 * sin(2 * pi * t / 0.5 + 0.01) + 150) + const g = round(50 * sin(2 * pi * t / 0.5 + 4.6) + 200) + const b = round(100 * sin(2 * pi * t / 0.5 + 2.3) + 150) + const color = new Color(r, g, b) + return color + + } else if (mode === 'pastel') { + + const l = (94 - 86) * random() + 86 + const c = (26 - 9) * random() + 9 + const h = 360 * random() + const color = new Color(l, c, h, 'lch') + return color + + } else if (mode === 'dark') { + + const l = 10 + 10 * random() + const c = (125 - 75) * random() + 86 + const h = 360 * random() + const color = new Color(l, c, h, 'lch') + return color + + } else if (mode === 'rgb') { + + const r = 255 * random() + const g = 255 * random() + const b = 255 * random() + const color = new Color(r, g, b) + return color + + } else if (mode === 'lab') { + + const l = 100 * random() + const a = 256 * random() - 128 + const b = 256 * random() - 128 + const color = new Color(l, a, b, 'lab') + return color + + } else if (mode === 'grey') { + + const grey = 255 * random() + const color = new Color(grey, grey, grey) + return color + + } else { + + throw new Error('Unsupported random color mode') + + } + } + + // Test if given value is a color string + static test (color) { + return (typeof color === 'string') + && (isHex.test(color) || isRgb.test(color)) + } + + cmyk () { + + // Get the rgb values for the current color + const { _a, _b, _c } = this.rgb() + const [ r, g, b ] = [ _a, _b, _c ].map(v => v / 255) + + // Get the cmyk values in an unbounded format + const k = Math.min(1 - r, 1 - g, 1 - b) + + if (k === 1) { + // Catch the black case + return new Color(0, 0, 0, 1, 'cmyk') + } + + const c = (1 - r - k) / (1 - k) + const m = (1 - g - k) / (1 - k) + const y = (1 - b - k) / (1 - k) + + // Construct the new color + const color = new Color(c, m, y, k, 'cmyk') + return color + } + + hsl () { + + // Get the rgb values + const { _a, _b, _c } = this.rgb() + const [ r, g, b ] = [ _a, _b, _c ].map(v => v / 255) + + // Find the maximum and minimum values to get the lightness + const max = Math.max(r, g, b) + const min = Math.min(r, g, b) + const l = (max + min) / 2 + + // If the r, g, v values are identical then we are grey + const isGrey = max === min + + // Calculate the hue and saturation + const delta = max - min + const s = isGrey ? 0 + : l > 0.5 ? delta / (2 - max - min) + : delta / (max + min) + const h = isGrey ? 0 + : max === r ? ((g - b) / delta + (g < b ? 6 : 0)) / 6 + : max === g ? ((b - r) / delta + 2) / 6 + : max === b ? ((r - g) / delta + 4) / 6 + : 0 + + // Construct and return the new color + const color = new Color(360 * h, 100 * s, 100 * l, 'hsl') + return color + } + init (a = 0, b = 0, c = 0, d = 0, space = 'rgb') { // This catches the case when a falsy value is passed like '' a = !a ? 0 : a @@ -113,6 +259,37 @@ export default class Color { Object.assign(this, components) } + lab () { + // Get the xyz color + const { x, y, z } = this.xyz() + + // Get the lab components + const l = (116 * y) - 16 + const a = 500 * (x - y) + const b = 200 * (y - z) + + // Construct and return a new color + const color = new Color(l, a, b, 'lab') + return color + } + + lch () { + + // Get the lab color directly + const { l, a, b } = this.lab() + + // Get the chromaticity and the hue using polar coordinates + const c = Math.sqrt(a ** 2 + b ** 2) + let h = 180 * Math.atan2(b, a) / Math.PI + if (h < 0) { + h *= -1 + h = 360 - h + } + + // Make a new color and return it + const color = new Color(l, c, h, 'lch') + return color + } /* Conversion Methods */ @@ -207,18 +384,24 @@ export default class Color { } } - lab () { - // Get the xyz color - const { x, y, z } = this.xyz() + toArray () { + const { _a, _b, _c, _d, space } = this + return [ _a, _b, _c, _d, space ] + } - // Get the lab components - const l = (116 * y) - 16 - const a = 500 * (x - y) - const b = 200 * (y - z) + toHex () { + const [ r, g, b ] = this._clamped().map(componentHex) + return `#${r}${g}${b}` + } - // Construct and return a new color - const color = new Color(l, a, b, 'lab') - return color + toRgb () { + const [ rV, gV, bV ] = this._clamped() + const string = `rgb(${rV},${gV},${bV})` + return string + } + + toString () { + return this.toHex() } xyz () { @@ -247,77 +430,6 @@ export default class Color { return color } - lch () { - - // Get the lab color directly - const { l, a, b } = this.lab() - - // Get the chromaticity and the hue using polar coordinates - const c = Math.sqrt(a ** 2 + b ** 2) - let h = 180 * Math.atan2(b, a) / Math.PI - if (h < 0) { - h *= -1 - h = 360 - h - } - - // Make a new color and return it - const color = new Color(l, c, h, 'lch') - return color - } - - hsl () { - - // Get the rgb values - const { _a, _b, _c } = this.rgb() - const [ r, g, b ] = [ _a, _b, _c ].map(v => v / 255) - - // Find the maximum and minimum values to get the lightness - const max = Math.max(r, g, b) - const min = Math.min(r, g, b) - const l = (max + min) / 2 - - // If the r, g, v values are identical then we are grey - const isGrey = max === min - - // Calculate the hue and saturation - const delta = max - min - const s = isGrey ? 0 - : l > 0.5 ? delta / (2 - max - min) - : delta / (max + min) - const h = isGrey ? 0 - : max === r ? ((g - b) / delta + (g < b ? 6 : 0)) / 6 - : max === g ? ((b - r) / delta + 2) / 6 - : max === b ? ((r - g) / delta + 4) / 6 - : 0 - - // Construct and return the new color - const color = new Color(360 * h, 100 * s, 100 * l, 'hsl') - return color - } - - cmyk () { - - // Get the rgb values for the current color - const { _a, _b, _c } = this.rgb() - const [ r, g, b ] = [ _a, _b, _c ].map(v => v / 255) - - // Get the cmyk values in an unbounded format - const k = Math.min(1 - r, 1 - g, 1 - b) - - if (k === 1) { - // Catch the black case - return new Color(0, 0, 0, 1, 'cmyk') - } - - const c = (1 - r - k) / (1 - k) - const m = (1 - g - k) / (1 - k) - const y = (1 - b - k) / (1 - k) - - // Construct the new color - const color = new Color(c, m, y, k, 'cmyk') - return color - } - /* Input and Output methods */ @@ -329,121 +441,8 @@ export default class Color { return [ _a, _b, _c ].map(format) } - toHex () { - const [ r, g, b ] = this._clamped().map(componentHex) - return `#${r}${g}${b}` - } - - toString () { - return this.toHex() - } - - toRgb () { - const [ rV, gV, bV ] = this._clamped() - const string = `rgb(${rV},${gV},${bV})` - return string - } - - toArray () { - const { _a, _b, _c, _d, space } = this - return [ _a, _b, _c, _d, space ] - } - - /* - Generating random colors - */ - - static random (mode = 'vibrant', t, u) { - - // Get the math modules - const { random, round, sin, PI: pi } = Math - - // Run the correct generator - if (mode === 'vibrant') { - - const l = (81 - 57) * random() + 57 - const c = (83 - 45) * random() + 45 - const h = 360 * random() - const color = new Color(l, c, h, 'lch') - return color - - } else if (mode === 'sine') { - - t = t == null ? random() : t - const r = round(80 * sin(2 * pi * t / 0.5 + 0.01) + 150) - const g = round(50 * sin(2 * pi * t / 0.5 + 4.6) + 200) - const b = round(100 * sin(2 * pi * t / 0.5 + 2.3) + 150) - const color = new Color(r, g, b) - return color - - } else if (mode === 'pastel') { - - const l = (94 - 86) * random() + 86 - const c = (26 - 9) * random() + 9 - const h = 360 * random() - const color = new Color(l, c, h, 'lch') - return color - - } else if (mode === 'dark') { - - const l = 10 + 10 * random() - const c = (125 - 75) * random() + 86 - const h = 360 * random() - const color = new Color(l, c, h, 'lch') - return color - - } else if (mode === 'rgb') { - - const r = 255 * random() - const g = 255 * random() - const b = 255 * random() - const color = new Color(r, g, b) - return color - - } else if (mode === 'lab') { - - const l = 100 * random() - const a = 256 * random() - 128 - const b = 256 * random() - 128 - const color = new Color(l, a, b, 'lab') - return color - - } else if (mode === 'grey') { - - const grey = 255 * random() - const color = new Color(grey, grey, grey) - return color - - } else { - - throw new Error('Unsupported random color mode') - - } - } - /* Constructing colors */ - // Test if given value is a color string - static test (color) { - return (typeof color === 'string') - && (isHex.test(color) || isRgb.test(color)) - } - - // Test if given value is an rgb object - static isRgb (color) { - return color && typeof color.r === 'number' - && typeof color.g === 'number' - && typeof color.b === 'number' - } - - // Test if given value is a color - static isColor (color) { - return color && ( - color instanceof Color - || this.isRgb(color) - || this.test(color) - ) - } } diff --git a/src/types/Matrix.js b/src/types/Matrix.js index 9b783da..d4f516c 100644 --- a/src/types/Matrix.js +++ b/src/types/Matrix.js @@ -13,72 +13,99 @@ export default class Matrix { this.init(...args) } - // Initialize - init (source) { - var base = Matrix.fromArray([ 1, 0, 0, 1, 0, 0 ]) - - // ensure source as object - source = source instanceof Element ? source.matrixify() - : typeof source === 'string' ? Matrix.fromArray(source.split(delimiter).map(parseFloat)) - : Array.isArray(source) ? Matrix.fromArray(source) - : (typeof source === 'object' && Matrix.isMatrixLike(source)) ? source - : (typeof source === 'object') ? new Matrix().transform(source) - : arguments.length === 6 ? Matrix.fromArray([].slice.call(arguments)) - : base + static formatTransforms (o) { + // Get all of the parameters required to form the matrix + var flipBoth = o.flip === 'both' || o.flip === true + var flipX = o.flip && (flipBoth || o.flip === 'x') ? -1 : 1 + var flipY = o.flip && (flipBoth || o.flip === 'y') ? -1 : 1 + var skewX = o.skew && o.skew.length ? o.skew[0] + : isFinite(o.skew) ? o.skew + : isFinite(o.skewX) ? o.skewX + : 0 + var skewY = o.skew && o.skew.length ? o.skew[1] + : isFinite(o.skew) ? o.skew + : isFinite(o.skewY) ? o.skewY + : 0 + var scaleX = o.scale && o.scale.length ? o.scale[0] * flipX + : isFinite(o.scale) ? o.scale * flipX + : isFinite(o.scaleX) ? o.scaleX * flipX + : flipX + var scaleY = o.scale && o.scale.length ? o.scale[1] * flipY + : isFinite(o.scale) ? o.scale * flipY + : isFinite(o.scaleY) ? o.scaleY * flipY + : flipY + var shear = o.shear || 0 + var theta = o.rotate || o.theta || 0 + var origin = new Point(o.origin || o.around || o.ox || o.originX, o.oy || o.originY) + var ox = origin.x + var oy = origin.y + // We need Point to be invalid if nothing was passed because we cannot default to 0 here. Thats why NaN + var position = new Point(o.position || o.px || o.positionX || NaN, o.py || o.positionY || NaN) + var px = position.x + var py = position.y + var translate = new Point(o.translate || o.tx || o.translateX, o.ty || o.translateY) + var tx = translate.x + var ty = translate.y + var relative = new Point(o.relative || o.rx || o.relativeX, o.ry || o.relativeY) + var rx = relative.x + var ry = relative.y - // Merge the source matrix with the base matrix - this.a = source.a != null ? source.a : base.a - this.b = source.b != null ? source.b : base.b - this.c = source.c != null ? source.c : base.c - this.d = source.d != null ? source.d : base.d - this.e = source.e != null ? source.e : base.e - this.f = source.f != null ? source.f : base.f + // Populate all of the values + return { + scaleX, scaleY, skewX, skewY, shear, theta, rx, ry, tx, ty, ox, oy, px, py + } + } - return this + static fromArray (a) { + return { a: a[0], b: a[1], c: a[2], d: a[3], e: a[4], f: a[5] } } - // Clones this matrix - clone () { - return new Matrix(this) + static isMatrixLike (o) { + return ( + o.a != null + || o.b != null + || o.c != null + || o.d != null + || o.e != null + || o.f != null + ) } - // Transform a matrix into another matrix by manipulating the space - transform (o) { - // Check if o is a matrix and then left multiply it directly - if (Matrix.isMatrixLike(o)) { - var matrix = new Matrix(o) - return matrix.multiplyO(this) - } + // left matrix, right matrix, target matrix which is overwritten + static matrixMultiply (l, r, o) { + // Work out the product directly + var a = l.a * r.a + l.c * r.b + var b = l.b * r.a + l.d * r.b + var c = l.a * r.c + l.c * r.d + var d = l.b * r.c + l.d * r.d + var e = l.e + l.a * r.e + l.c * r.f + var f = l.f + l.b * r.e + l.d * r.f - // Get the proposed transformations and the current transformations - var t = Matrix.formatTransforms(o) - var current = this - const { x: ox, y: oy } = new Point(t.ox, t.oy).transform(current) + // make sure to use local variables because l/r and o could be the same + o.a = a + o.b = b + o.c = c + o.d = d + o.e = e + o.f = f - // Construct the resulting matrix - var transformer = new Matrix() - .translateO(t.rx, t.ry) - .lmultiplyO(current) - .translateO(-ox, -oy) - .scaleO(t.scaleX, t.scaleY) - .skewO(t.skewX, t.skewY) - .shearO(t.shear) - .rotateO(t.theta) - .translateO(ox, oy) + return o + } - // If we want the origin at a particular place, we force it there - if (isFinite(t.px) || isFinite(t.py)) { - const origin = new Point(ox, oy).transform(transformer) - // TODO: Replace t.px with isFinite(t.px) - // Doesnt work because t.px is also 0 if it wasnt passed - const dx = isFinite(t.px) ? t.px - origin.x : 0 - const dy = isFinite(t.py) ? t.py - origin.y : 0 - transformer.translateO(dx, dy) - } + around (cx, cy, matrix) { + return this.clone().aroundO(cx, cy, matrix) + } - // Translate now after positioning - transformer.translateO(t.tx, t.ty) - return transformer + // Transform around a center point + aroundO (cx, cy, matrix) { + var dx = cx || 0 + var dy = cy || 0 + return this.translateO(-dx, -dy).lmultiplyO(matrix).translateO(dx, dy) + } + + // Clones this matrix + clone () { + return new Matrix(this) } // Decomposes this matrix into its affine parameters @@ -134,32 +161,52 @@ export default class Matrix { } } - // Left multiplies by the given matrix - multiply (matrix) { - return this.clone().multiplyO(matrix) + // Check if two matrices are equal + equals (other) { + if (other === this) return true + var comp = new Matrix(other) + return closeEnough(this.a, comp.a) && closeEnough(this.b, comp.b) + && closeEnough(this.c, comp.c) && closeEnough(this.d, comp.d) + && closeEnough(this.e, comp.e) && closeEnough(this.f, comp.f) } - multiplyO (matrix) { - // Get the matrices - var l = this - var r = matrix instanceof Matrix - ? matrix - : new Matrix(matrix) - - return Matrix.matrixMultiply(l, r, this) + // Flip matrix on x or y, at a given offset + flip (axis, around) { + return this.clone().flipO(axis, around) } - lmultiply (matrix) { - return this.clone().lmultiplyO(matrix) + flipO (axis, around) { + return axis === 'x' ? this.scaleO(-1, 1, around, 0) + : axis === 'y' ? this.scaleO(1, -1, 0, around) + : this.scaleO(-1, -1, axis, around || axis) // Define an x, y flip point } - lmultiplyO (matrix) { - var r = this - var l = matrix instanceof Matrix - ? matrix - : new Matrix(matrix) + // Initialize + init (source) { + var base = Matrix.fromArray([ 1, 0, 0, 1, 0, 0 ]) - return Matrix.matrixMultiply(l, r, this) + // ensure source as object + source = source instanceof Element ? source.matrixify() + : typeof source === 'string' ? Matrix.fromArray(source.split(delimiter).map(parseFloat)) + : Array.isArray(source) ? Matrix.fromArray(source) + : (typeof source === 'object' && Matrix.isMatrixLike(source)) ? source + : (typeof source === 'object') ? new Matrix().transform(source) + : arguments.length === 6 ? Matrix.fromArray([].slice.call(arguments)) + : base + + // Merge the source matrix with the base matrix + this.a = source.a != null ? source.a : base.a + this.b = source.b != null ? source.b : base.b + this.c = source.c != null ? source.c : base.c + this.d = source.d != null ? source.d : base.d + this.e = source.e != null ? source.e : base.e + this.f = source.f != null ? source.f : base.f + + return this + } + + inverse () { + return this.clone().inverseO() } // Inverses matrix @@ -197,44 +244,32 @@ export default class Matrix { return this } - inverse () { - return this.clone().inverseO() + lmultiply (matrix) { + return this.clone().lmultiplyO(matrix) } - // Translate matrix - translate (x, y) { - return this.clone().translateO(x, y) - } + lmultiplyO (matrix) { + var r = this + var l = matrix instanceof Matrix + ? matrix + : new Matrix(matrix) - translateO (x, y) { - this.e += x || 0 - this.f += y || 0 - return this + return Matrix.matrixMultiply(l, r, this) } - // Scale matrix - scale (x, y, cx, cy) { - return this.clone().scaleO(...arguments) + // Left multiplies by the given matrix + multiply (matrix) { + return this.clone().multiplyO(matrix) } - scaleO (x, y = x, cx = 0, cy = 0) { - // Support uniform scaling - if (arguments.length === 3) { - cy = cx - cx = y - y = x - } - - const { a, b, c, d, e, f } = this - - this.a = a * x - this.b = b * y - this.c = c * x - this.d = d * y - this.e = e * x - cx * x + cx - this.f = f * y - cy * y + cy + multiplyO (matrix) { + // Get the matrices + var l = this + var r = matrix instanceof Matrix + ? matrix + : new Matrix(matrix) - return this + return Matrix.matrixMultiply(l, r, this) } // Rotate matrix @@ -261,15 +296,29 @@ export default class Matrix { return this } - // Flip matrix on x or y, at a given offset - flip (axis, around) { - return this.clone().flipO(axis, around) + // Scale matrix + scale (x, y, cx, cy) { + return this.clone().scaleO(...arguments) } - flipO (axis, around) { - return axis === 'x' ? this.scaleO(-1, 1, around, 0) - : axis === 'y' ? this.scaleO(1, -1, 0, around) - : this.scaleO(-1, -1, axis, around || axis) // Define an x, y flip point + scaleO (x, y = x, cx = 0, cy = 0) { + // Support uniform scaling + if (arguments.length === 3) { + cy = cx + cx = y + y = x + } + + const { a, b, c, d, e, f } = this + + this.a = a * x + this.b = b * y + this.c = c * x + this.d = d * y + this.e = e * x - cx * x + cx + this.f = f * y - cy * y + cy + + return this } // Shear matrix @@ -329,33 +378,63 @@ export default class Matrix { return this.skew(0, y, cx, cy) } - // Transform around a center point - aroundO (cx, cy, matrix) { - var dx = cx || 0 - var dy = cy || 0 - return this.translateO(-dx, -dy).lmultiplyO(matrix).translateO(dx, dy) + toArray () { + return [ this.a, this.b, this.c, this.d, this.e, this.f ] } - around (cx, cy, matrix) { - return this.clone().aroundO(cx, cy, matrix) + // Convert matrix to string + toString () { + return 'matrix(' + this.a + ',' + this.b + ',' + this.c + ',' + this.d + ',' + this.e + ',' + this.f + ')' } - // Check if two matrices are equal - equals (other) { - if (other === this) return true - var comp = new Matrix(other) - return closeEnough(this.a, comp.a) && closeEnough(this.b, comp.b) - && closeEnough(this.c, comp.c) && closeEnough(this.d, comp.d) - && closeEnough(this.e, comp.e) && closeEnough(this.f, comp.f) + // Transform a matrix into another matrix by manipulating the space + transform (o) { + // Check if o is a matrix and then left multiply it directly + if (Matrix.isMatrixLike(o)) { + var matrix = new Matrix(o) + return matrix.multiplyO(this) + } + + // Get the proposed transformations and the current transformations + var t = Matrix.formatTransforms(o) + var current = this + const { x: ox, y: oy } = new Point(t.ox, t.oy).transform(current) + + // Construct the resulting matrix + var transformer = new Matrix() + .translateO(t.rx, t.ry) + .lmultiplyO(current) + .translateO(-ox, -oy) + .scaleO(t.scaleX, t.scaleY) + .skewO(t.skewX, t.skewY) + .shearO(t.shear) + .rotateO(t.theta) + .translateO(ox, oy) + + // If we want the origin at a particular place, we force it there + if (isFinite(t.px) || isFinite(t.py)) { + const origin = new Point(ox, oy).transform(transformer) + // TODO: Replace t.px with isFinite(t.px) + // Doesnt work because t.px is also 0 if it wasnt passed + const dx = isFinite(t.px) ? t.px - origin.x : 0 + const dy = isFinite(t.py) ? t.py - origin.y : 0 + transformer.translateO(dx, dy) + } + + // Translate now after positioning + transformer.translateO(t.tx, t.ty) + return transformer } - // Convert matrix to string - toString () { - return 'matrix(' + this.a + ',' + this.b + ',' + this.c + ',' + this.d + ',' + this.e + ',' + this.f + ')' + // Translate matrix + translate (x, y) { + return this.clone().translateO(x, y) } - toArray () { - return [ this.a, this.b, this.c, this.d, this.e, this.f ] + translateO (x, y) { + this.e += x || 0 + this.f += y || 0 + return this } valueOf () { @@ -369,84 +448,6 @@ export default class Matrix { } } - static fromArray (a) { - return { a: a[0], b: a[1], c: a[2], d: a[3], e: a[4], f: a[5] } - } - - static isMatrixLike (o) { - return ( - o.a != null - || o.b != null - || o.c != null - || o.d != null - || o.e != null - || o.f != null - ) - } - - static formatTransforms (o) { - // Get all of the parameters required to form the matrix - var flipBoth = o.flip === 'both' || o.flip === true - var flipX = o.flip && (flipBoth || o.flip === 'x') ? -1 : 1 - var flipY = o.flip && (flipBoth || o.flip === 'y') ? -1 : 1 - var skewX = o.skew && o.skew.length ? o.skew[0] - : isFinite(o.skew) ? o.skew - : isFinite(o.skewX) ? o.skewX - : 0 - var skewY = o.skew && o.skew.length ? o.skew[1] - : isFinite(o.skew) ? o.skew - : isFinite(o.skewY) ? o.skewY - : 0 - var scaleX = o.scale && o.scale.length ? o.scale[0] * flipX - : isFinite(o.scale) ? o.scale * flipX - : isFinite(o.scaleX) ? o.scaleX * flipX - : flipX - var scaleY = o.scale && o.scale.length ? o.scale[1] * flipY - : isFinite(o.scale) ? o.scale * flipY - : isFinite(o.scaleY) ? o.scaleY * flipY - : flipY - var shear = o.shear || 0 - var theta = o.rotate || o.theta || 0 - var origin = new Point(o.origin || o.around || o.ox || o.originX, o.oy || o.originY) - var ox = origin.x - var oy = origin.y - // We need Point to be invalid if nothing was passed because we cannot default to 0 here. Thats why NaN - var position = new Point(o.position || o.px || o.positionX || NaN, o.py || o.positionY || NaN) - var px = position.x - var py = position.y - var translate = new Point(o.translate || o.tx || o.translateX, o.ty || o.translateY) - var tx = translate.x - var ty = translate.y - var relative = new Point(o.relative || o.rx || o.relativeX, o.ry || o.relativeY) - var rx = relative.x - var ry = relative.y - - // Populate all of the values - return { - scaleX, scaleY, skewX, skewY, shear, theta, rx, ry, tx, ty, ox, oy, px, py - } - } - - // left matrix, right matrix, target matrix which is overwritten - static matrixMultiply (l, r, o) { - // Work out the product directly - var a = l.a * r.a + l.c * r.b - var b = l.b * r.a + l.d * r.b - var c = l.a * r.c + l.c * r.d - var d = l.b * r.c + l.d * r.d - var e = l.e + l.a * r.e + l.c * r.f - var f = l.f + l.b * r.e + l.d * r.f - - // make sure to use local variables because l/r and o could be the same - o.a = a - o.b = b - o.c = c - o.d = d - o.e = e - o.f = f - - return o - } } export function ctm () { diff --git a/src/types/PathArray.js b/src/types/PathArray.js index d9c1eb2..b7c3c33 100644 --- a/src/types/PathArray.js +++ b/src/types/PathArray.js @@ -125,9 +125,10 @@ for (var i = 0, il = mlhvqtcsaz.length; i < il; ++i) { } export default class PathArray extends SVGArray { - // Convert array to string - toString () { - return arrayToString(this) + // Get bounding box of path + bbox () { + parser().path.setAttribute('d', this.toString()) + return new Box(parser.nodes.path.getBBox()) } // Move path string @@ -171,52 +172,6 @@ export default class PathArray extends SVGArray { return this } - // Resize path string - size (width, height) { - // get bounding box of current situation - var box = this.bbox() - var i, l - - // If the box width or height is 0 then we ignore - // transformations on the respective axis - box.width = box.width === 0 ? 1 : box.width - box.height = box.height === 0 ? 1 : box.height - - // recalculate position of all points according to new size - for (i = this.length - 1; i >= 0; i--) { - l = this[i][0] - - if (l === 'M' || l === 'L' || l === 'T') { - this[i][1] = ((this[i][1] - box.x) * width) / box.width + box.x - this[i][2] = ((this[i][2] - box.y) * height) / box.height + box.y - } else if (l === 'H') { - this[i][1] = ((this[i][1] - box.x) * width) / box.width + box.x - } else if (l === 'V') { - this[i][1] = ((this[i][1] - box.y) * height) / box.height + box.y - } else if (l === 'C' || l === 'S' || l === 'Q') { - this[i][1] = ((this[i][1] - box.x) * width) / box.width + box.x - this[i][2] = ((this[i][2] - box.y) * height) / box.height + box.y - this[i][3] = ((this[i][3] - box.x) * width) / box.width + box.x - this[i][4] = ((this[i][4] - box.y) * height) / box.height + box.y - - if (l === 'C') { - this[i][5] = ((this[i][5] - box.x) * width) / box.width + box.x - this[i][6] = ((this[i][6] - box.y) * height) / box.height + box.y - } - } else if (l === 'A') { - // resize radii - this[i][1] = (this[i][1] * width) / box.width - this[i][2] = (this[i][2] * height) / box.height - - // move position values - this[i][6] = ((this[i][6] - box.x) * width) / box.width + box.x - this[i][7] = ((this[i][7] - box.y) * height) / box.height + box.y - } - } - - return this - } - // Absolutize and parse path to array parse (array = [ 'M', 0, 0 ]) { // prepare for parsing @@ -264,9 +219,55 @@ export default class PathArray extends SVGArray { return result } - // Get bounding box of path - bbox () { - parser().path.setAttribute('d', this.toString()) - return new Box(parser.nodes.path.getBBox()) + // Resize path string + size (width, height) { + // get bounding box of current situation + var box = this.bbox() + var i, l + + // If the box width or height is 0 then we ignore + // transformations on the respective axis + box.width = box.width === 0 ? 1 : box.width + box.height = box.height === 0 ? 1 : box.height + + // recalculate position of all points according to new size + for (i = this.length - 1; i >= 0; i--) { + l = this[i][0] + + if (l === 'M' || l === 'L' || l === 'T') { + this[i][1] = ((this[i][1] - box.x) * width) / box.width + box.x + this[i][2] = ((this[i][2] - box.y) * height) / box.height + box.y + } else if (l === 'H') { + this[i][1] = ((this[i][1] - box.x) * width) / box.width + box.x + } else if (l === 'V') { + this[i][1] = ((this[i][1] - box.y) * height) / box.height + box.y + } else if (l === 'C' || l === 'S' || l === 'Q') { + this[i][1] = ((this[i][1] - box.x) * width) / box.width + box.x + this[i][2] = ((this[i][2] - box.y) * height) / box.height + box.y + this[i][3] = ((this[i][3] - box.x) * width) / box.width + box.x + this[i][4] = ((this[i][4] - box.y) * height) / box.height + box.y + + if (l === 'C') { + this[i][5] = ((this[i][5] - box.x) * width) / box.width + box.x + this[i][6] = ((this[i][6] - box.y) * height) / box.height + box.y + } + } else if (l === 'A') { + // resize radii + this[i][1] = (this[i][1] * width) / box.width + this[i][2] = (this[i][2] * height) / box.height + + // move position values + this[i][6] = ((this[i][6] - box.x) * width) / box.width + box.x + this[i][7] = ((this[i][7] - box.y) * height) / box.height + box.y + } + } + + return this } + + // Convert array to string + toString () { + return arrayToString(this) + } + } diff --git a/src/types/Point.js b/src/types/Point.js index 634ffff..cb09bf3 100644 --- a/src/types/Point.js +++ b/src/types/Point.js @@ -6,6 +6,11 @@ export default class Point { this.init(...args) } + // Clone point + clone () { + return new Point(this) + } + init (x, y) { const base = { x: 0, y: 0 } @@ -21,9 +26,8 @@ export default class Point { return this } - // Clone point - clone () { - return new Point(this) + toArray () { + return [ this.x, this.y ] } transform (m) { @@ -45,9 +49,6 @@ export default class Point { return this } - toArray () { - return [ this.x, this.y ] - } } export function point (x, y) { diff --git a/src/types/PointArray.js b/src/types/PointArray.js index 27a2076..28e649c 100644 --- a/src/types/PointArray.js +++ b/src/types/PointArray.js @@ -4,24 +4,37 @@ import Box from './Box.js' import Matrix from './Matrix.js' export default class PointArray extends SVGArray { - // Convert array to string - toString () { - // convert to a poly point string - for (var i = 0, il = this.length, array = []; i < il; i++) { - array.push(this[i].join(',')) - } - - return array.join(' ') + // Get bounding box of points + bbox () { + var maxX = -Infinity + var maxY = -Infinity + var minX = Infinity + var minY = Infinity + this.forEach(function (el) { + maxX = Math.max(el[0], maxX) + maxY = Math.max(el[1], maxY) + minX = Math.min(el[0], minX) + minY = Math.min(el[1], minY) + }) + return new Box(minX, minY, maxX - minX, maxY - minY) } - // Convert array to line object - toLine () { - return { - x1: this[0][0], - y1: this[0][1], - x2: this[1][0], - y2: this[1][1] + // Move point string + move (x, y) { + var box = this.bbox() + + // get relative offset + x -= box.x + y -= box.y + + // move every point + if (!isNaN(x) && !isNaN(y)) { + for (var i = this.length - 1; i >= 0; i--) { + this[i] = [ this[i][0] + x, this[i][1] + y ] + } } + + return this } // Parse point string and flat array @@ -48,6 +61,40 @@ export default class PointArray extends SVGArray { return points } + // Resize poly string + size (width, height) { + var i + var box = this.bbox() + + // recalculate position of all points according to new size + for (i = this.length - 1; i >= 0; i--) { + if (box.width) this[i][0] = ((this[i][0] - box.x) * width) / box.width + box.x + if (box.height) this[i][1] = ((this[i][1] - box.y) * height) / box.height + box.y + } + + return this + } + + // Convert array to line object + toLine () { + return { + x1: this[0][0], + y1: this[0][1], + x2: this[1][0], + y2: this[1][1] + } + } + + // Convert array to string + toString () { + // convert to a poly point string + for (var i = 0, il = this.length, array = []; i < il; i++) { + array.push(this[i].join(',')) + } + + return array.join(' ') + } + transform (m) { return this.clone().transformO(m) } @@ -68,50 +115,4 @@ export default class PointArray extends SVGArray { return this } - // Move point string - move (x, y) { - var box = this.bbox() - - // get relative offset - x -= box.x - y -= box.y - - // move every point - if (!isNaN(x) && !isNaN(y)) { - for (var i = this.length - 1; i >= 0; i--) { - this[i] = [ this[i][0] + x, this[i][1] + y ] - } - } - - return this - } - - // Resize poly string - size (width, height) { - var i - var box = this.bbox() - - // recalculate position of all points according to new size - for (i = this.length - 1; i >= 0; i--) { - if (box.width) this[i][0] = ((this[i][0] - box.x) * width) / box.width + box.x - if (box.height) this[i][1] = ((this[i][1] - box.y) * height) / box.height + box.y - } - - return this - } - - // Get bounding box of points - bbox () { - var maxX = -Infinity - var maxY = -Infinity - var minX = Infinity - var minY = Infinity - this.forEach(function (el) { - maxX = Math.max(el[0], maxX) - maxY = Math.max(el[1], maxY) - minX = Math.min(el[0], minX) - minY = Math.min(el[1], minY) - }) - return new Box(minX, minY, maxX - minX, maxY - minY) - } } diff --git a/src/types/SVGArray.js b/src/types/SVGArray.js index dafa2d4..6ce024a 100644 --- a/src/types/SVGArray.js +++ b/src/types/SVGArray.js @@ -6,6 +6,10 @@ export default class SVGArray extends Array { this.init(...args) } + clone () { + return new this.constructor(this) + } + init (arr) { // This catches the case, that native map tries to create an array with new Array(1) if (typeof arr === 'number') return this @@ -14,10 +18,22 @@ export default class SVGArray extends Array { return this } + // Parse whitespace separated string + parse (array = []) { + // If already is an array, no need to parse it + if (array instanceof Array) return array + + return array.trim().split(delimiter).map(parseFloat) + } + toArray () { return Array.prototype.concat.apply([], this) } + toSet () { + return new Set(this) + } + toString () { return this.join(' ') } @@ -29,19 +45,4 @@ export default class SVGArray extends Array { return ret } - // Parse whitespace separated string - parse (array = []) { - // If already is an array, no need to parse it - if (array instanceof Array) return array - - return array.trim().split(delimiter).map(parseFloat) - } - - clone () { - return new this.constructor(this) - } - - toSet () { - return new Set(this) - } } diff --git a/src/types/SVGNumber.js b/src/types/SVGNumber.js index fedb00e..914919e 100644 --- a/src/types/SVGNumber.js +++ b/src/types/SVGNumber.js @@ -7,6 +7,16 @@ export default class SVGNumber { this.init(...args) } + convert (unit) { + return new SVGNumber(this.value, unit) + } + + // Divide number + divide (number) { + number = new SVGNumber(number) + return new SVGNumber(this / number, this.unit || number.unit) + } + init (value, unit) { unit = Array.isArray(value) ? value[1] : unit value = Array.isArray(value) ? value[0] : value @@ -46,23 +56,10 @@ export default class SVGNumber { return this } - toString () { - return (this.unit === '%' ? ~~(this.value * 1e8) / 1e6 - : this.unit === 's' ? this.value / 1e3 - : this.value - ) + this.unit - } - - toJSON () { - return this.toString() - } - - toArray () { - return [ this.value, this.unit ] - } - - valueOf () { - return this.value + // Subtract number + minus (number) { + number = new SVGNumber(number) + return new SVGNumber(this - number, this.unit || number.unit) } // Add number @@ -71,25 +68,29 @@ export default class SVGNumber { return new SVGNumber(this + number, this.unit || number.unit) } - // Subtract number - minus (number) { - number = new SVGNumber(number) - return new SVGNumber(this - number, this.unit || number.unit) - } - // Multiply number times (number) { number = new SVGNumber(number) return new SVGNumber(this * number, this.unit || number.unit) } - // Divide number - divide (number) { - number = new SVGNumber(number) - return new SVGNumber(this / number, this.unit || number.unit) + toArray () { + return [ this.value, this.unit ] } - convert (unit) { - return new SVGNumber(this.value, unit) + toJSON () { + return this.toString() + } + + toString () { + return (this.unit === '%' ? ~~(this.value * 1e8) / 1e6 + : this.unit === 's' ? this.value / 1e3 + : this.value + ) + this.unit } + + valueOf () { + return this.value + } + } diff --git a/svg.js.d.ts b/svg.js.d.ts index 5b7b3fa..2264c66 100644 --- a/svg.js.d.ts +++ b/svg.js.d.ts @@ -4,6 +4,9 @@ // trick to keep reference to Array build-in type
declare class BuiltInArray<T> extends Array<T> { }
+// trick to have nice attribute list for CSS
+declare type CSSStyleName = Exclude<keyof CSSStyleDeclaration, "parentRule" | "length" >
+
declare module "@svgdotjs/svg.js" {
function SVG(): Svg;
@@ -771,11 +774,19 @@ declare module "@svgdotjs/svg.js" { }
// Timeline.js
+ interface ScheduledRunnerInfo {
+ start: number
+ duration: number
+ end: number
+ runner: Runner
+ }
+
class Timeline extends EventTarget {
constructor()
constructor(fn: Function)
- schedule(runner?: Runner, delay?: number, when?: string): this
+ schedule(runner: Runner, delay?: number, when?: string): this
+ schedule(): ScheduledRunnerInfo[]
unschedule(runner: Runner): this
getEndTime(): number
updateTime(): this
@@ -812,7 +823,6 @@ declare module "@svgdotjs/svg.js" { constructor();
constructor(options: Function);
constructor(options: number);
- constructor(options: object);
constructor(options: Controller);
static sanitise: (duration?: TimeLike, delay?: number, when?: string) => object
@@ -1006,11 +1016,11 @@ declare module "@svgdotjs/svg.js" { toggleClass(name: string): this
// prototype method register in css.js
- css(): object;
- css(style: string): string
- css(style: string[]): object;
- css(style: string, val: any): this
- css(style: object): this
+ css(): Partial<CSSStyleDeclaration>
+ css<T extends CSSStyleName>(style: T): CSSStyleDeclaration[T]
+ css<T extends CSSStyleName[]>(style: T): Partial<CSSStyleDeclaration>
+ css<T extends CSSStyleName>(style: T, val: CSSStyleDeclaration[T]): this
+ css(style: Partial<CSSStyleDeclaration>): this
show(): this
hide(): this
visible(): boolean
|