From: Ulrich-Matthias Schäfer Date: Tue, 19 May 2020 23:36:17 +0000 (+1000) Subject: added geometry and positioning methods to `A` (#1110) X-Git-Tag: 3.1.0~22 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=0890c4d55fd39356347a5e7a2bb37c9436d2555c;p=svg.js.git added geometry and positioning methods to `A` (#1110) --- 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()` 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/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/src/elements/A.js b/src/elements/A.js index 0388314..478c36d 100644 --- a/src/elements/A.js +++ b/src/elements/A.js @@ -1,7 +1,8 @@ -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) { @@ -20,6 +21,8 @@ export default class A extends Container { } +extend(A, containerGeometry) + registerMethods({ Container: { // Create a hyperlink element diff --git a/src/elements/G.js b/src/elements/G.js index b460269..b3a999e 100644 --- a/src/elements/G.js +++ b/src/elements/G.js @@ -1,83 +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) } - - 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 - } - - dx (dx) { - return this.dmove(dx, 0) - } - - dy (dy) { - return this.dmove(0, dy) - } - - height (height, box = this.bbox()) { - if (height == null) return box.height - return this.size(box.width, height, box) - } - - move (x = 0, y = 0, box = this.bbox()) { - const dx = x - box.x - const dy = y - box.y - - return this.dmove(dx, dy) - } - - 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 - } - - width (width, box = this.bbox()) { - if (width == null) return box.width - return this.size(width, box.height, box) - } - - 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) - } - } +extend(G, containerGeometry) + registerMethods({ Container: { // Create a group element 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) +}