refactor: ♻️ refactor vector add add test specs

This commit is contained in:
bubkoo
2021-04-19 15:27:19 +08:00
parent 870f69bb66
commit 03d2389c31
30 changed files with 1156 additions and 205 deletions

View File

@ -329,9 +329,9 @@ describe('Dom', () => {
set(node, value) {
if (typeof value === 'number') {
node.setAttribute('foo', value > 0 ? '1' : '-1')
return true
return false
}
return false
return value
},
})
div.attr('foo', 'bar')

View File

@ -1,6 +1,8 @@
import { Attributes } from './attributes'
export abstract class AttributesBase {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
attr(k?: any, v?: any) {
return this
return Attributes.prototype.attr.call(this, k, v)
}
}

View File

@ -204,9 +204,11 @@ export namespace Core {
) {
const hook = Hook.get(name)
if (hook && hook.set) {
if (hook.set(node, value) !== false) {
const ret = hook.set(node, value)
if (ret === false) {
return
}
value = ret // eslint-disable-line
}
const special = Special.get(name)

View File

@ -29,9 +29,9 @@ export namespace Hook {
Object.keys(attributeValue).forEach((key) =>
Style.style(node, key, attributeValue[key]),
)
return true
return false
}
return false
return attributeValue
},
})
}

View File

@ -5,29 +5,29 @@ import { Dom } from './dom'
import { Fragment } from '../vector/fragment/fragment'
describe('Dom', () => {
describe('first()', () => {
describe('firstChild()', () => {
it('should return the first child', () => {
const g = new G()
const rect = g.rect()
g.circle(100)
expect(g.first()).toBe(rect)
expect(g.firstChild()).toBe(rect)
})
it('should return `null` if no first child exists', () => {
expect(new G().first()).toBe(null)
expect(new G().firstChild()).toBe(null)
})
})
describe('last()', () => {
describe('lastChild()', () => {
it('should return the last child of the element', () => {
const g = new G()
g.rect()
const rect = g.rect()
expect(g.last()).toBe(rect)
expect(g.lastChild()).toBe(rect)
})
it('should return `null` if no last child exists', () => {
expect(new G().last()).toBe(null)
expect(new G().lastChild()).toBe(null)
})
})

View File

@ -14,14 +14,14 @@ export class Dom<TElement extends Element = Element> extends Primer<TElement> {
/**
* Returns the first child of the element.
*/
first<T extends Dom = Dom>(): T | null {
firstChild<T extends Dom = Dom>(): T | null {
return Dom.adopt<T>(this.node.firstChild)
}
/**
* Returns the last child of the element.
*/
last<T extends Dom = Dom>(): T | null {
lastChild<T extends Dom = Dom>(): T | null {
return Dom.adopt<T>(this.node.lastChild)
}

View File

@ -77,7 +77,7 @@ describe('Dom', () => {
div.add(span)
const div2 = div.clone(true)
const span2 = div2.first()!
const span2 = div2.firstChild()!
expect(div2.attr(Affix.PERSIST_ATTR_NAME)).toEqual('{"foo":"bar"}')
expect(span2.attr(Affix.PERSIST_ATTR_NAME)).toEqual('{"a":1}')

View File

@ -0,0 +1,286 @@
import sinon from 'sinon'
import { Matrix } from '../../struct/matrix'
import { Dom } from '../dom'
describe('Dom', () => {
describe('transform()', () => {
it('should act as full getter with no argument', () => {
const dom = new Dom().attr('transform', 'translate(10, 20) rotate(45)')
const actual = dom.transform()
const expected = new Matrix().rotate(45).translate(10, 20).decompose()
expect(actual).toEqual(expected)
})
it('should return a single transformation value when string was passed', () => {
const dom = new Dom().attr('transform', 'translate(10, 20) rotate(45)')
expect(dom.transform('rotate')).toBe(45)
expect(dom.transform('translateX')).toBe(10)
expect(dom.transform('translateY')).toBe(20)
})
it('should set the transformation with an object', () => {
const dom = new Dom().transform({ rotate: 45, translate: [10, 20] })
expect(dom.transform('rotate')).toBe(45)
expect(dom.transform('translateX')).toBe(10)
expect(dom.transform('translateY')).toBe(20)
})
it('should perform a relative transformation', () => {
const dom = new Dom()
.transform({ rotate: 45, translate: [10, 20] })
.transform({ rotate: 10 }, true)
expect(dom.transform('rotate')).toBeCloseTo(55, 5) // rounding errors
expect(dom.transform('translateX')).toBe(10)
expect(dom.transform('translateY')).toBe(20)
})
it('should perform a relative transformation with other matrix', () => {
const dom = new Dom()
.transform({ rotate: 45, translate: [10, 20] })
.transform({ rotate: 10 }, new Matrix().rotate(30))
expect(dom.transform('rotate')).toBeCloseTo(40, 5) // rounding errors
expect(dom.transform('translateX')).toBe(0)
expect(dom.transform('translateY')).toBe(0)
})
it('should perform a relative transformation with other element', () => {
const ref = new Dom().transform({ rotate: 30 })
const dom = new Dom()
.transform({ rotate: 45, translate: [10, 20] })
.transform({ rotate: 10 }, ref)
expect(dom.transform('rotate')).toBeCloseTo(40, 5) // rounding errors
expect(dom.transform('translateX')).toBe(0)
expect(dom.transform('translateY')).toBe(0)
})
})
describe('untransform()', () => {
it('should return itself', () => {
const dom = new Dom()
expect(dom.untransform()).toBe(dom)
})
it('should delete the transform attribute', () => {
const dom = new Dom()
expect(dom.untransform().attr('transform') as any).toBe(undefined)
})
})
describe('matrixify()', () => {
it('should get an empty matrix when there is not transformations', () => {
expect(new Dom().matrixify()).toEqual(new Matrix())
})
it('should reduce all transformations of the transform list into one matrix [1]', () => {
const dom = new Dom().attr('transform', 'matrix(1, 0, 1, 1, 0, 1)')
expect(dom.matrixify()).toEqual(new Matrix(1, 0, 1, 1, 0, 1))
})
it('should reduce all transformations of the transform list into one matrix [2]', () => {
const dom = new Dom().attr('transform', 'translate(10, 20) rotate(45)')
expect(dom.matrixify()).toEqual(new Matrix().rotate(45).translate(10, 20))
})
it('should reduce all transformations of the transform list into one matrix [3]', () => {
const dom = new Dom().attr(
'transform',
'translate(10, 20) rotate(45) skew(1,2) skewX(10) skewY(20)',
)
expect(dom.matrixify()).toEqual(
new Matrix()
.skewY(20)
.skewX(10)
.skew(1, 2)
.rotate(45)
.translate(10, 20),
)
})
})
describe('matrix()', () => {
it('should get transform as a matrix', () => {
expect(new Dom().matrixify()).toEqual(new Matrix())
const dom = new Dom().transform({ rotate: 45, translate: [10, 20] })
expect(dom.matrix()).toEqual(new Matrix().rotate(45).translate(10, 20))
})
it('should transform element with matrix', () => {
expect(
new Dom().matrix(new Matrix().translate(10, 20)).attr('transform'),
).toEqual('matrix(1,0,0,1,10,20)')
expect(new Dom().matrix(1, 0, 0, 1, 10, 20).attr('transform')).toEqual(
'matrix(1,0,0,1,10,20)',
)
})
})
describe('rotate()', () => {
it('should rotate element', () => {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.rotate(1, 2, 3)
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([{ rotate: 1, ox: 2, oy: 3 }, true])
})
})
describe('skew()', () => {
it('should skew element with no argument', function () {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.skew()
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([
{ skew: undefined, ox: undefined, oy: undefined },
true,
])
})
it('should skew element with one argument', function () {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.skew(5)
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([
{ skew: 5, ox: undefined, oy: undefined },
true,
])
})
it('should skew element with two argument', function () {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.skew(5, 6)
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([
{ skew: [5, 6], ox: undefined, oy: undefined },
true,
])
})
it('should skew element with three arguments', function () {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.skew(5, 6, 7)
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([{ skew: 5, ox: 6, oy: 7 }, true])
})
it('should skew element with four arguments', function () {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.skew(5, 6, 7, 8)
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([{ skew: [5, 6], ox: 7, oy: 8 }, true])
})
})
describe('shear()', () => {
it('should shear element', function () {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.shear(1, 2, 3)
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([{ shear: 1, ox: 2, oy: 3 }, true])
})
})
describe('scale()', () => {
it('should scale element with no argument', function () {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.scale()
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([
{ scale: undefined, ox: undefined, oy: undefined },
true,
])
})
it('should scale element with one argument', function () {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.scale(5)
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([
{ scale: 5, ox: undefined, oy: undefined },
true,
])
})
it('should scale element with two argument', function () {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.scale(5, 6)
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([
{ scale: [5, 6], ox: undefined, oy: undefined },
true,
])
})
it('should scale element with three arguments', function () {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.scale(5, 6, 7)
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([{ scale: 5, ox: 6, oy: 7 }, true])
})
it('should scale element with four arguments', function () {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.scale(5, 6, 7, 8)
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([{ scale: [5, 6], ox: 7, oy: 8 }, true])
})
})
describe('translate()', () => {
it('should translate element', function () {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.translate(1, 2)
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([{ translate: [1, 2] }, true])
})
})
describe('relative()', () => {
it('should relative element', function () {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.relative(1, 2)
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([{ relative: [1, 2] }, true])
})
})
describe('flip()', () => {
it('should flip element', function () {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.flip('x', 2)
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([{ flip: 'x', origin: 2 }, true])
})
it('should sets flip to "both" when calling without anything', function () {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.flip()
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([{ flip: 'both', origin: 'center' }, true])
})
it('should set flip to both and origin to number when called with origin only', function () {
const dom = new Dom()
const spy = sinon.spy(dom, 'transform')
dom.flip(5)
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([{ flip: 'both', origin: 5 }, true])
})
})
})

View File

@ -72,9 +72,10 @@ export class Transform<TElement extends Element>
}
matrix(): Matrix
matrix(m: Matrix | Matrix.MatrixLike): this
matrix(a: number, b: number, c: number, d: number, e: number, f: number): this
matrix(
a?: number,
a?: Matrix | Matrix.MatrixLike | number,
b?: number,
c?: number,
d?: number,
@ -85,17 +86,19 @@ export class Transform<TElement extends Element>
return new Matrix(this)
}
return this.attr(
'transform',
new Matrix(
a,
b as number,
c as number,
d as number,
e as number,
f as number,
).toString(),
)
const m =
typeof a === 'number'
? new Matrix(
a,
b as number,
c as number,
d as number,
e as number,
f as number,
)
: new Matrix(a)
return this.attr('transform', m.toString())
}
rotate(angle: number): this
@ -104,12 +107,22 @@ export class Transform<TElement extends Element>
return this.transform({ rotate: angle, ox: cx, oy: cy }, true)
}
skew(): this
skew(s: number): this
skew(x: number, y: number): this
skew(s: number, cx: number, cy: number): this
skew(x: number, y: number, cx: number, cy: number): this
skew(x: number, y: number, cx?: number, cy?: number) {
skew(x?: number, y?: number, cx?: number, cy?: number) {
return arguments.length === 1 || arguments.length === 3
? this.transform({ skew: x, ox: y, oy: cx }, true)
: this.transform({ skew: [x, y], ox: cx, oy: cy }, true)
: this.transform(
{
skew: typeof x === 'undefined' ? undefined : [x, y as number],
ox: cx,
oy: cy,
},
true,
)
}
shear(lam: number): this
@ -118,12 +131,22 @@ export class Transform<TElement extends Element>
return this.transform({ shear: lam, ox: cx, oy: cy }, true)
}
scale(): this
scale(s: number): this
scale(x: number, y: number): this
scale(x: number, y: number, cx: number, cy: number): this
scale(x: number, y: number, cx?: number, cy?: number) {
scale(s: number, cx: number, cy: number): this
scale(x?: number, y?: number, cx?: number, cy?: number) {
return arguments.length === 1 || arguments.length === 3
? this.transform({ scale: x, ox: y, oy: cx }, true)
: this.transform({ scale: [x, y], ox: cx, oy: cy }, true)
: this.transform(
{
scale: typeof x === 'undefined' ? undefined : [x, y as number],
ox: cx,
oy: cy,
},
true,
)
}
translate(x: number, y: number) {
@ -137,7 +160,7 @@ export class Transform<TElement extends Element>
flip(origin?: number | [number, number] | Point.PointLike): this
flip(
direction: 'both' | 'x' | 'y',
origin?: number | [number, number] | Point.PointLike,
origin?: number | [number, number] | Point.PointLike | 'center',
): this
flip(
direction:
@ -147,7 +170,7 @@ export class Transform<TElement extends Element>
| number
| [number, number]
| Point.PointLike = 'both',
origin?: number | [number, number] | Point.PointLike,
origin: number | [number, number] | Point.PointLike | 'center' = 'center',
) {
if (typeof direction !== 'string') {
origin = direction // eslint-disable-line

View File

@ -1,4 +1,3 @@
import { Global } from '../global'
import { Box } from './box'
import { Matrix } from './matrix'
@ -88,26 +87,6 @@ describe('Box', () => {
})
})
describe('addOffset()', () => {
it('should return a new instance', () => {
Global.withWindow({ pageXOffset: 50, pageYOffset: 25 } as any, () => {
const box = new Box(100, 100, 100, 100)
const box2 = box.addOffset()
expect(box2).toBeInstanceOf(Box)
expect(box2).not.toBe(box)
})
})
it('should add the current page offset to the box', () => {
Global.withWindow({ pageXOffset: 50, pageYOffset: 25 } as any, () => {
const box = new Box(100, 100, 100, 100).addOffset()
expect(box.toArray()).toEqual([150, 125, 100, 100])
})
})
})
describe('merge()', () => {
it('should merge various bounding boxes', () => {
const box1 = new Box(50, 50, 100, 100)

View File

@ -1,6 +1,5 @@
import { Global } from '../global'
import { Matrix } from './matrix'
import { Point } from './point'
import { Matrix } from './matrix'
export class Box implements Box.BoxLike {
x: number
@ -67,13 +66,6 @@ export class Box implements Box.BoxLike {
return this
}
addOffset() {
// offset by window scroll position, because getBoundingClientRect changes when window is scrolled
this.x += Global.window.pageXOffset
this.y += Global.window.pageYOffset
return new Box(this)
}
isNull() {
return Box.isNull(this)
}

View File

@ -21,7 +21,7 @@ export class Matrix implements Matrix.MatrixLike {
constructor(element: Matrix.Matrixifiable | null)
constructor(array: Matrix.MatrixArray)
constructor(matrix: Matrix.MatrixLike)
constructor(options: Matrix.TransformOptions)
constructor(options: Matrix.TransformOptionsStrict)
constructor(
a?:
| number
@ -44,7 +44,7 @@ export class Matrix implements Matrix.MatrixLike {
| Matrix.Matrixifiable
| Matrix.MatrixArray
| Matrix.MatrixLike
| Matrix.TransformOptions
| Matrix.TransformOptionsStrict
| null,
b?: number,
c?: number,
@ -67,7 +67,7 @@ export class Matrix implements Matrix.MatrixLike {
: typeof a === 'object' && Matrix.isMatrixLike(a)
? a
: typeof a === 'object'
? new Matrix().transform(a as Matrix.TransformOptions)
? new Matrix().transform(a as Matrix.TransformOptionsStrict)
: typeof a === 'number'
? Matrix.toMatrixLike([
a,
@ -380,7 +380,7 @@ export class Matrix implements Matrix.MatrixLike {
return this.skew(0, y, cx, cy)
}
transform(o: Matrix.MatrixLike | Matrix.TransformOptions) {
transform(o: Matrix.MatrixLike | Matrix.TransformOptionsStrict) {
if (Matrix.isMatrixLike(o)) {
const matrix = new Matrix(o)
return matrix.multiplyO(this)
@ -452,7 +452,7 @@ export namespace Matrix {
}
export namespace Matrix {
export interface TransformOptions {
export interface TransformOptionsStrict {
flip?: 'both' | 'x' | 'y' | boolean
skew?: number | [number, number]
skewX?: number
@ -491,9 +491,14 @@ export namespace Matrix {
relativeY?: number
}
export interface TransformOptions
extends Omit<TransformOptionsStrict, 'origin'> {
origin?: number | [number, number] | { x: number; y: number } | 'center'
}
export type Transform = ReturnType<typeof Matrix.prototype.decompose>
export function formatTransforms(o: TransformOptions) {
export function formatTransforms(o: TransformOptionsStrict) {
const flipBoth = o.flip === 'both' || o.flip === true
const flipX = o.flip && (flipBoth || o.flip === 'x') ? -1 : 1
const flipY = o.flip && (flipBoth || o.flip === 'y') ? -1 : 1

View File

@ -14,7 +14,18 @@ export class ContainerExtension<
text?: string | Attributes | null,
attrs?: Attributes | null,
) {
return Text.create(text, attrs).appendTo(this)
const instance = new Text().appendTo(this)
if (text) {
if (typeof text === 'string') {
instance.text(text)
if (attrs) {
instance.attr(attrs)
}
} else {
instance.attr(text)
}
}
return instance
}
plain<Attributes extends SVGTextAttributes>(attrs?: Attributes | null): Text
@ -26,17 +37,17 @@ export class ContainerExtension<
text?: string | Attributes | null,
attrs?: Attributes,
) {
const t = Text.create()
const instance = new Text().appendTo(this)
if (text) {
if (typeof text === 'string') {
t.plain(text)
instance.plain(text)
if (attrs) {
t.attr(attrs)
instance.attr(attrs)
}
} else {
t.attr(text)
instance.attr(text)
}
}
return t
return instance
}
}

View File

@ -1,25 +1,30 @@
import { UnitNumber } from '../../struct/unit-number'
import { AttributesBase } from '../../dom/attributes'
export class AttrOverride extends AttributesBase {
export class Overrides extends AttributesBase {
attr(attr: any, value: any) {
if (attr === 'leading') {
return this.leading(value)
if (typeof value === 'undefined') {
return this.leading()
}
this.leading(value)
}
const ret = super.attr(attr, value)
if (attr === 'font-size' || attr === 'x') {
this.rebuild()
if (typeof value !== 'undefined') {
if (attr === 'font-size' || attr === 'x') {
this.rebuild()
}
}
return ret
}
}
export interface AttrOverride extends AttrOverride.Depends {}
export interface Overrides extends Overrides.Depends {}
export namespace AttrOverride {
export namespace Overrides {
export interface Depends {
leading(): number
leading(value: UnitNumber.Raw): this

View File

@ -3,16 +3,15 @@ import { UnitNumber } from '../../struct/unit-number'
import { Adopter } from '../../dom/common/adopter'
import { TSpan } from '../tspan/tspan'
import { TextBase } from './base'
import { AttrOverride } from './override'
import { SVGTextAttributes } from './types'
import { Overrides } from './overrides'
@Text.mixin(AttrOverride)
@Text.mixin(Overrides)
@Text.register('Text')
export class Text<
TSVGTextElement extends SVGTextElement | SVGTextPathElement = SVGTextElement
>
extends TextBase<TSVGTextElement>
implements AttrOverride.Depends {
implements Overrides.Depends {
public affixes: Record<string | number, any> & {
leading: number
}
@ -21,8 +20,8 @@ export class Text<
restoreAffix() {
super.restoreAffix()
if (this.affixes.leading == null) {
this.affixes.leading = 1.3
if (this.leading() == null) {
this.leading(1.3)
}
return this
}
@ -31,10 +30,10 @@ export class Text<
leading(value: UnitNumber.Raw): this
leading(value?: UnitNumber.Raw) {
if (value == null) {
return this.affixes.leading
return this.affix('leading')
}
this.affixes.leading = UnitNumber.create(value).valueOf()
this.affix('leading', UnitNumber.create(value).valueOf())
return this.rebuild()
}
@ -123,34 +122,3 @@ export class Text<
return this.tspan(text).newLine()
}
}
export namespace Text {
export function create<Attributes extends SVGTextAttributes>(
attrs?: Attributes | null,
): Text
export function create<Attributes extends SVGTextAttributes>(
text: string,
attrs?: Attributes | null,
): Text
export function create<Attributes extends SVGTextAttributes>(
text?: string | Attributes | null,
attrs?: Attributes | null,
): Text
export function create<Attributes extends SVGTextAttributes>(
text?: string | Attributes | null,
attrs?: Attributes | null,
) {
const t = new Text()
if (text) {
if (typeof text === 'string') {
t.text(text)
if (attrs) {
t.attr(attrs)
}
} else {
t.attr(text)
}
}
return t
}
}

View File

@ -14,7 +14,7 @@ export class ContainerExtension<
path: string | Path,
attrs?: Attributes,
) {
const instance = text instanceof Text ? text : Text.create(text)
const instance = text instanceof Text ? text : new Text().text(text)
const textPath = instance.path(path)
if (attrs) {
textPath.attr(attrs)
@ -74,7 +74,7 @@ export class PathExtension<
text: string | Text,
attrs?: Attributes,
) {
const instance = text instanceof Text ? text : Text.create(text)
const instance = text instanceof Text ? text : new Text().text(text)
if (!instance.parent()) {
this.after(instance)
}

View File

@ -9,12 +9,22 @@ export class TextExtension<
text = '',
attrs?: Attributes | null,
) {
const tspan = TSpan.create(text, attrs)
const tspan = new TSpan().appendTo(this)
if (text != null) {
if (typeof text === 'string') {
tspan.text(text)
if (attrs) {
tspan.attr(attrs)
}
} else {
tspan.attr(text)
}
}
if (!this.building) {
this.clear()
}
return tspan.appendTo(this)
return tspan
}
}

View File

@ -1,7 +1,6 @@
import { Global } from '../../global'
import { Text } from '../text/text'
import { TextBase } from '../text/base'
import { SVGTSpanAttributes } from './types'
@TSpan.register('Tspan')
export class TSpan extends TextBase<SVGTSpanElement> {
@ -30,7 +29,7 @@ export class TSpan extends TextBase<SVGTSpanElement> {
const fontSize = Global.window
.getComputedStyle(this.node)
.getPropertyValue('font-size')
const dy = text.affixes.leading * Number.parseFloat(fontSize)
const dy = text.leading() * Number.parseFloat(fontSize)
return this.dy(index ? dy : 0).attr('x', text.x())
}
@ -54,31 +53,3 @@ export class TSpan extends TextBase<SVGTSpanElement> {
return this
}
}
export namespace TSpan {
export function create(): TSpan
export function create<Attributes extends SVGTSpanAttributes>(
attrs: Attributes | null,
): TSpan
export function create<Attributes extends SVGTSpanAttributes>(
text: string,
attrs?: Attributes | null,
): TSpan
export function create<Attributes extends SVGTSpanAttributes>(
text?: string | Attributes | null,
attrs?: Attributes | null,
) {
const tspan = new TSpan()
if (text != null) {
if (typeof text === 'string') {
tspan.text(text)
if (attrs) {
tspan.attr(attrs)
}
} else {
tspan.attr(text)
}
}
return tspan
}
}

View File

@ -0,0 +1,92 @@
import { Global } from '../../global'
import { Box } from '../../struct/box'
import { Matrix } from '../../struct/matrix'
import { Rect } from '../rect/rect'
import { Svg } from '../svg/svg'
describe('Vector', () => {
describe('bbox()', () => {
it('should returns the bounding box of the element', () => {
const svg = new Svg().appendTo(document.body)
const rect = svg.rect().size(100, 200).move(20, 30)
expect(rect.bbox()).toBeInstanceOf(Box)
expect(rect.bbox().toArray()).toEqual([20, 30, 100, 200])
svg.remove()
})
it('should return the bounding box of the element even if the node is not in the dom', () => {
const rect = new Rect().size(100, 200).move(20, 30)
expect(rect.bbox().toArray()).toEqual([20, 30, 100, 200])
})
it('should throw error when it is not possible to get a bbox', () => {
const spy = spyOn(
Global.window.SVGGraphicsElement.prototype,
'getBBox',
).and.callFake(() => {
throw new Error('No BBox for you')
})
const rect = new Rect()
expect(() => rect.bbox()).toThrow()
expect(spy).toHaveBeenCalled()
})
})
describe('rbox()', () => {
it('should return the BoundingClientRect of the element', () => {
document.body.style.margin = '0px'
document.body.style.padding = '0px'
const svg = new Svg().appendTo(document.body)
const rect = new Rect()
.size(100, 200)
.move(20, 30)
.addTo(svg)
.attr(
'transform',
new Matrix({ scale: 2, translate: [40, 50] }).toString(),
)
expect(rect.rbox()).toBeInstanceOf(Box)
expect(rect.rbox().toArray()).toEqual([80, 110, 200, 400])
svg.remove()
document.body.style.margin = ''
document.body.style.padding = ''
})
it('should return the rbox box of the element in the coordinate system of the passed element', () => {
const svg = new Svg().appendTo(document.body)
const group = svg.group().translate(1, 1)
const rect = new Rect()
.size(100, 200)
.move(20, 30)
.addTo(svg)
.attr(
'transform',
new Matrix({ scale: 2, translate: [40, 50] }).toString(),
)
expect(rect.rbox(group)).toBeInstanceOf(Box)
expect(rect.rbox(group).toArray()).toEqual([79, 109, 200, 400])
svg.remove()
})
it('should throw error when element is not in dom', () => {
expect(() => new Rect().rbox()).toThrow()
})
})
describe('containsPoint()', () => {
it('checks if a point is in the elements borders', () => {
const svg = new Svg()
const rect = svg.rect(100, 100)
expect(rect.containsPoint(50, 50)).toBe(true)
expect(rect.containsPoint({ x: 50, y: 50 })).toBe(true)
expect(rect.containsPoint(101, 101)).toBe(false)
})
})
})

View File

@ -2,6 +2,8 @@ import { withSvgContext } from '../../util'
import { Box } from '../../struct/box'
import { Base } from '../common/base'
import { Transform } from './transform'
import { Global } from '../../global'
import { Point } from '../../struct/point'
export class BBox<
TSVGElement extends SVGElement = SVGElement
@ -50,11 +52,18 @@ export class BBox<
// Else we want it in absolute screen coordinates
// Therefore we need to add the scrollOffset
return rbox.addOffset()
rbox.x += Global.window.pageXOffset
rbox.y += Global.window.pageYOffset
return rbox
}
inside(x: number, y: number) {
containsPoint(p: Point.PointLike): boolean
containsPoint(x: number, y: number): boolean
containsPoint(arg1: number | Point.PointLike, arg2?: number) {
const box = this.bbox()
const x = typeof arg1 === 'number' ? arg1 : arg1.x
const y = typeof arg1 === 'number' ? (arg2 as number) : arg1.y
return (
x > box.x && y > box.y && x < box.x + box.width && y < box.y + box.height
)

View File

@ -0,0 +1,162 @@
import { Pattern } from '../pattern/pattern'
import { Rect } from '../rect/rect'
import { Svg } from '../svg/svg'
describe('Vector', () => {
describe('fill()', () => {
describe('setter', () => {
it('should return itself', () => {
const rect = new Rect()
expect(rect.fill('black')).toBe(rect)
})
it('should set a fill color', () => {
const rect = new Rect()
expect(rect.fill('black').attr('fill')).toBe('black')
})
it('should remove fill when pass `null`', () => {
const rect = new Rect()
expect(rect.fill('black').attr('fill')).toBe('black')
expect(rect.fill(null).attr('fill')).toEqual('#000000')
})
it('should set fill with color object', () => {
const rect = new Rect()
expect(rect.fill({ r: 1, g: 1, b: 1 }).attr('fill')).toBe(
'rgba(1,1,1,1)',
)
})
it('should set a fill pattern when pattern given', () => {
const svg = new Svg()
const pattern = svg.pattern()
const rect = svg.rect(100, 100)
expect(rect.fill(pattern).attr('fill')).toBe(pattern.url())
})
it('should set a fill pattern when image given', () => {
const svg = new Svg()
const image = svg.image('http://via.placeholder.com/120x80')
const rect = svg.rect(100, 100)
expect(rect.fill(image).attr('fill')).toBe(
image.parent<Pattern>()!.url(),
)
})
it('should set a fill pattern when image url given', () => {
const svg = new Svg()
const rect = svg.rect()
rect.fill(
'https://www.centerforempathy.org/wp-content/uploads/2019/11/placeholder.png',
)
const defs = svg.defs()
const pattern = defs.firstChild<Pattern>()!
expect(rect.fill()).toEqual(pattern.url())
})
it('should set an object of fill properties', () => {
const rect = new Rect()
expect(
rect
.fill({
color: 'black',
opacity: 0.5,
rule: 'evenodd',
})
.attr(),
).toEqual({
fill: 'black',
fillOpacity: 0.5,
fillRule: 'evenodd',
} as any)
})
})
describe('getter', () => {
it('should return fill color', () => {
const rect = new Rect().fill('black')
expect(rect.fill()).toBe('black')
})
})
})
describe('stroke()', () => {
describe('setter', () => {
it('should return itself', () => {
const rect = new Rect()
expect(rect.stroke('black')).toBe(rect)
})
it('should set a stroke color', () => {
const rect = new Rect()
expect(rect.stroke('black').attr('stroke')).toBe('black')
})
it('should sets a stroke pattern when pattern given', () => {
const svg = new Svg()
const pattern = svg.pattern()
const rect = svg.rect(100, 100)
expect(rect.stroke(pattern).attr('stroke')).toBe(pattern.url())
})
it('should set a stroke pattern when image given', () => {
const svg = new Svg()
const image = svg.image('http://via.placeholder.com/120x80')
const rect = svg.rect(100, 100)
expect(rect.stroke(image).attr('stroke')).toBe(
image.parent<Pattern>()!.url(),
)
})
it('should set an object of stroke properties', () => {
const rect = new Rect()
expect(
rect
.stroke({
color: 'black',
width: 2,
opacity: 0.5,
linecap: 'butt',
linejoin: 'miter',
miterlimit: 10,
dasharray: '2 2',
dashoffset: 15,
})
.attr(),
).toEqual({
stroke: 'black',
strokeWidth: 2,
strokeOpacity: 0.5,
strokeLinecap: 'butt',
strokeLinejoin: 'miter',
strokeMiterlimit: 10,
strokeDasharray: '2 2',
strokeDashoffset: 15,
} as any)
})
it('should set stroke dasharray with array passed', () => {
const rect = new Rect().stroke({ dasharray: [2, 2] })
expect(rect.attr()).toEqual({ strokeDasharray: '2 2' } as any)
})
})
describe('getter', () => {
it('should return stroke color', () => {
const rect = new Rect().stroke('black')
expect(rect.stroke()).toBe('black')
})
})
})
describe('opacity()', () => {
it('should get/set opacity', () => {
const rect = new Rect()
expect(rect.opacity()).toEqual(1)
rect.opacity(0.5)
expect(rect.opacity()).toEqual(0.5)
})
})
})

View File

@ -1,11 +1,13 @@
import { Color } from '../../struct/color'
import { Base } from '../common/base'
import { Image } from '../image/image'
import { Vector } from './vector'
export class FillStroke<
TSVGElement extends SVGElement = SVGElement
> extends Base<TSVGElement> {
fill(): string
fill(color: string | Color | Color.RGBALike | Base | null): this
fill(color: string | Color | Color.RGBALike | Vector | null): this
fill(attrs: {
color?: string
opacity?: number
@ -17,7 +19,7 @@ export class FillStroke<
| string
| Color
| Color.RGBALike
| Base
| Vector
| {
color?: string
opacity?: number
@ -33,7 +35,7 @@ export class FillStroke<
}
stroke(): string
stroke(color: string | Color | Color.RGBALike | Base | null): this
stroke(color: string | Color | Color.RGBALike | Vector | null): this
stroke(attrs: {
color?: string
width?: number
@ -41,8 +43,8 @@ export class FillStroke<
linecap?: 'butt' | 'round' | 'square'
linejoin?: 'arcs' | 'bevel' | 'miter' | 'miter-clip' | 'round'
miterlimit?: number
dasharray?: string
dashoffset?: string
dasharray?: string | number[]
dashoffset?: string | number
}): this
stroke(
value?:
@ -50,7 +52,7 @@ export class FillStroke<
| string
| Color
| Color.RGBALike
| Base
| Vector
| {
color?: string
width?: number
@ -58,8 +60,8 @@ export class FillStroke<
linecap?: 'butt' | 'round' | 'square'
linejoin?: 'arcs' | 'bevel' | 'miter' | 'miter-clip' | 'round'
miterlimit?: number
dasharray?: string
dashoffset?: string
dasharray?: string | number[]
dashoffset?: string | number
},
) {
if (typeof value === 'undefined') {
@ -101,14 +103,17 @@ export namespace FillStroke {
| string
| Color
| Color.RGBALike
| Base<T>
| Record<string, string | number>
| Vector
| Record<string, string | number | number[]>
| null,
) {
if (value === null) {
elem.attr(type, null)
} else if (typeof value === 'string' || value instanceof Base) {
elem.attr(type, value.toString())
} else if (typeof value === 'string' || value instanceof Vector) {
elem.attr(
type,
value instanceof Image ? (value as any) : value.toString(),
)
} else if (value instanceof Color || Color.isRgbLike(value)) {
const color = new Color(value)
elem.attr(type, color.toString())
@ -118,7 +123,7 @@ export namespace FillStroke {
const k = names[i]
const v = value[k]
if (v != null) {
elem.attr(prefix(type, k), v)
elem.attr(prefix(type, k), Array.isArray(v) ? v.join(' ') : v)
}
}
}

View File

@ -0,0 +1,72 @@
import sinon from 'sinon'
import { Svg } from '../svg/svg'
import { Text } from '../text/text'
describe('Vector', () => {
describe('font()', () => {
let svg: Svg
let txt: Text
beforeEach(() => {
svg = new Svg().appendTo(document.body)
txt = svg.text('Some text')
})
afterEach(() => {
svg.remove()
})
it('should set leading when given', function () {
const spy = spyOn(txt, 'leading')
txt.font({ leading: 3 })
expect(spy).toHaveBeenCalledWith(3)
})
it('should sets text-anchor when anchor given', function () {
const spy = sinon.spy(txt, 'attr')
txt.font({ anchor: 'start' })
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual(['text-anchor', 'start'])
})
it('should set all font properties via attr()', function () {
const spy = spyOn(txt, 'attr')
txt.font({
size: 20,
family: 'Verdana',
weight: 'bold',
stretch: 'wider',
variant: 'small-caps',
style: 'italic',
})
expect(spy).toHaveBeenCalledWith('font-size', 20)
expect(spy).toHaveBeenCalledWith('font-family', 'Verdana')
expect(spy).toHaveBeenCalledWith('font-weight', 'bold')
expect(spy).toHaveBeenCalledWith('font-stretch', 'wider')
expect(spy).toHaveBeenCalledWith('font-variant', 'small-caps')
expect(spy).toHaveBeenCalledWith('font-style', 'italic')
})
it('should redirect all other stuff directly to attr()', function () {
const spy = spyOn(txt, 'attr')
txt.font({
foo: 'bar',
bar: 'baz',
} as any)
expect(spy).toHaveBeenCalledWith('foo', 'bar')
expect(spy).toHaveBeenCalledWith('bar', 'baz')
})
it('should set key value pair', function () {
const spy = spyOn(txt, 'attr')
txt.font('size', 20)
expect(spy).toHaveBeenCalledWith('font-size', 20)
})
it('should get value if called with one parameter', function () {
const spy = spyOn(txt, 'attr')
txt.font('size')
expect(spy).toHaveBeenCalledWith('font-size', undefined)
})
})
})

View File

@ -0,0 +1,49 @@
import { Base } from '../common/base'
import { Text } from '../text/text'
export class FontStyle<
TSVGElement extends SVGElement = SVGElement
> extends Base<TSVGElement> {
font(key: string): string | number
font(attrs: FontStyle.Attributes): this
font(key: string, value: string | number | null | undefined): this
font(a: FontStyle.Attributes | string, v?: string | number) {
if (typeof a === 'object') {
Object.keys(a).forEach((key: keyof FontStyle.Attributes) =>
this.font(key, a[key]),
)
return this
}
if (a === 'leading') {
const text = (this as any) as Text // eslint-disable-line
if (text.leading) {
return text.leading(v)
}
}
return a === 'anchor'
? this.attr('text-anchor', v)
: a === 'size' ||
a === 'family' ||
a === 'weight' ||
a === 'stretch' ||
a === 'variant' ||
a === 'style'
? this.attr(`font-${a}`, v)
: this.attr(a, v)
}
}
export namespace FontStyle {
export interface Attributes {
leading?: number
anchor?: string | number | null
size?: string | number | null
family?: string | null
weight?: string | number | null
stretch?: string | null
variant?: string | null
style?: string | null
}
}

View File

@ -2,9 +2,11 @@ import { applyMixins } from '../../util'
import { ElementExtension as StyleExtension } from '../style/exts'
import { ElementExtension as MaskExtension } from '../mask/exts'
import { ElementExtension as ClipPathExtension } from '../clippath/exts'
import { Vector } from './vector'
import { Base } from '../common/base'
import { Vector } from './vector'
import { Overrides } from './overrides'
import { BBox } from './bbox'
import { FontStyle } from './font'
import { Transform } from './transform'
import { FillStroke } from './fillstroke'
@ -13,6 +15,7 @@ declare module './vector' {
extends Base<TSVGElement>,
FillStroke<TSVGElement>,
BBox<TSVGElement>,
FontStyle<TSVGElement>,
Transform<TSVGElement>,
MaskExtension<TSVGElement>,
StyleExtension<TSVGElement>,
@ -22,7 +25,9 @@ declare module './vector' {
applyMixins(
Vector,
Base,
Overrides,
BBox,
FontStyle,
Transform,
FillStroke,
MaskExtension,

View File

@ -0,0 +1,55 @@
import { Util } from '../../dom/attributes/util'
import { AttributesBase } from '../../dom/attributes/base'
export const defaultAttributes = {
// fill and stroke
fillOpacity: 1,
strokeOpacity: 1,
strokeWidth: 0,
strokeLinejoin: 'miter',
strokeLinecap: 'butt',
fill: '#000000',
stroke: '#000000',
opacity: 1,
// position
x: 0,
y: 0,
cx: 0,
cy: 0,
// size
width: 0,
height: 0,
// radius
r: 0,
rx: 0,
ry: 0,
// gradient
offset: 0,
stopOpacity: 1,
stopColor: '#000000',
// text
textAnchor: 'start',
}
export class Overrides extends AttributesBase {
attr(attributeName: any, attributeValue: any) {
if (
typeof attributeName === 'string' &&
typeof attributeValue === 'undefined'
) {
const val = super.attr(attributeName)
if (typeof val === 'undefined') {
return defaultAttributes[
Util.camelCase(attributeName) as keyof typeof defaultAttributes
]
}
return val
}
return super.attr(attributeName, attributeValue)
}
}

View File

@ -0,0 +1,124 @@
import sinon from 'sinon'
import { Global } from '../../global'
import { Matrix } from '../../struct/matrix'
import { Point } from '../../struct/point'
import { G } from '../g/g'
import { Rect } from '../rect/rect'
import { Svg } from '../svg/svg'
describe('Vector', () => {
describe('toParent()', () => {
it('should return itself', () => {
const svg = new Svg()
const g = svg.group()
const rect = g.rect(100, 100)
expect(rect.toParent(svg)).toBe(rect)
})
it('should do nothing if the parent is the the current element', () => {
const svg = new Svg().appendTo(document.body)
const g = svg.group()
const parent = g.parent()
const index = g.index()
g.toParent(g)
expect(g.parent()).toBe(parent)
expect(g.index()).toBe(index)
svg.remove()
})
it('should move the element to a different container without changing its visual representation [1]', () => {
const svg = new Svg().appendTo(document.body)
const g = svg.group().matrix(1, 0, 1, 1, 0, 1)
const rect = g.rect(100, 100)
rect.toParent(svg)
expect(rect.matrix()).toEqual(new Matrix(1, 0, 1, 1, 0, 1))
expect(rect.parent()).toBe(svg)
svg.remove()
})
it('should move the element to a different container without changing its visual representation [2]', () => {
const svg = new Svg().appendTo(document.body)
const g = svg.group().translate(10, 20)
const rect = g.rect(100, 100)
const g2 = svg.group().rotate(10)
rect.toParent(g2)
const actual = rect.matrix()
const expected = new Matrix().translate(10, 20).rotate(-10)
const factors = 'abcdef'.split('')
// funny enough the dom seems to shorten the floats and precision gets lost
factors.forEach((prop: 'a') =>
expect(actual[prop]).toBeCloseTo(expected[prop], 5),
)
svg.remove()
})
it('should insert the element at the specified position', () => {
const svg = new Svg()
const g = svg.group()
const rect = g.rect(100, 100)
svg.rect(100, 100)
svg.rect(100, 100)
expect(rect.toParent(svg, 2).index()).toBe(2)
})
})
describe('toRoot()', () => {
it('should call `toParent()` with root node', () => {
const svg = new Svg()
const g = svg.group().matrix(1, 0, 1, 1, 0, 1)
const rect = g.rect(100, 100)
const spy = sinon.spy(rect, 'toParent')
rect.toRoot(3)
expect(spy.callCount).toEqual(1)
expect(spy.args[0]).toEqual([svg, 3])
})
it('should do nothing when the element do not have a root', () => {
const g = new G()
const rect = g.rect()
rect.toRoot()
expect(rect.parent()).toBe(g)
})
})
describe('toLocalPoint()', () => {
it('should transform a screen point into the coordinate system of the element', () => {
const rect = new Rect()
spyOn(rect, 'screenCTM').and.callFake(
() => new Matrix(1, 0, 0, 1, 20, 20),
)
expect(rect.toLocalPoint({ x: 10, y: 10 })).toEqual(new Point(-10, -10))
expect(rect.toLocalPoint(10, 10)).toEqual(new Point(-10, -10))
})
})
describe('ctm()', () => {
it('should return the native ctm wrapped into a matrix', () => {
const rect = new Rect()
const spy = sinon.spy(rect.node, 'getCTM')
rect.ctm()
expect(spy.callCount).toEqual(1)
})
})
describe('screenCTM()', () => {
it('should return the native screenCTM wrapped into a matrix for a normal element', () => {
const rect = new Rect()
const spy = sinon.spy(rect.node, 'getScreenCTM')
rect.screenCTM()
expect(spy.callCount).toEqual(1)
})
it('should do extra work for nested svgs because firefox needs it', () => {
const spy = sinon.spy(
Global.window.SVGGraphicsElement.prototype,
'getScreenCTM',
)
const svg = new Svg().nested()
svg.screenCTM()
expect(spy.callCount).toEqual(1)
})
})
})

View File

@ -6,6 +6,11 @@ import { Base } from '../common/base'
export class Transform<
TSVGElement extends SVGElement = SVGElement
> extends Base<TSVGElement> {
/**
* Moves an element to a different parent (similar to addTo), but without
* changing its visual representation. All transformations are merged and
* applied to the element.
*/
toParent(parent: Transform, index?: number): this {
if (this !== parent) {
const ctm = this.screenCTM()
@ -17,6 +22,11 @@ export class Transform<
return this
}
/**
* Moves an element to the root svg (similar to addTo), but without
* changing its visual representation. All transformations are merged and
* applied to the element.
*/
toRoot(index?: number): this {
const root = this.root()
if (root) {
@ -25,8 +35,14 @@ export class Transform<
return this
}
point(x: number, y: number) {
return new Point(x, y).transform(this.screenCTM().inverse())
/**
* Transforms a point from screen coordinates to the elements coordinate system.
*/
toLocalPoint(p: Point.PointLike): Point
toLocalPoint(x: number, y: number): Point
toLocalPoint(x: number | Point.PointLike, y?: number) {
const p = typeof x === 'number' ? new Point(x, y) : new Point(x)
return p.transform(this.screenCTM().inverse())
}
ctm() {

View File

@ -0,0 +1,141 @@
import { Rect } from '../rect/rect'
describe('Vector', () => {
let rect: Rect
beforeEach(() => {
rect = new Rect()
})
describe('x()', () => {
it('should call attr with x', () => {
const spy = spyOn(rect, 'attr')
rect.x(123)
expect(spy).toHaveBeenCalledWith('x', 123)
})
})
describe('y()', () => {
it('should call attr with y', () => {
const spy = spyOn(rect, 'attr')
rect.y(123)
expect(spy).toHaveBeenCalledWith('y', 123)
})
})
describe('move()', () => {
it('should call `x()` and `y()` with passed parameters and returns itself', () => {
const spyx = spyOn(rect, 'x').and.callThrough()
const spyy = spyOn(rect, 'y').and.callThrough()
expect(rect.move(1, 2)).toBe(rect)
expect(spyx).toHaveBeenCalledWith(1)
expect(spyy).toHaveBeenCalledWith(2)
})
})
describe('cx()', () => {
it('should return the elements center along the x axis', () => {
rect.attr({ x: 10, width: 100 })
expect(rect.cx()).toBe(60)
})
it('should center the element along the x axis and returns itself', () => {
rect.attr({ x: 10, width: 100 })
expect(rect.cx(100)).toBe(rect)
expect(rect.attr('x')).toBe(50)
})
})
describe('cy()', () => {
it('should return the elements center along the y axis', () => {
rect.attr({ y: 10, height: 100 })
expect(rect.cy()).toBe(60)
})
it('should center the element along the y axis and returns itself', () => {
rect.attr({ y: 10, height: 100 })
expect(rect.cy(100)).toBe(rect)
expect(rect.attr('y')).toBe(50)
})
})
describe('center()', () => {
it('should call `cx()` and `cy()` with passed parameters and returns itself', () => {
const spyCx = spyOn(rect, 'cx').and.callThrough()
const spyCy = spyOn(rect, 'cy').and.callThrough()
expect(rect.center(1, 2)).toBe(rect)
expect(spyCx).toHaveBeenCalledWith(1)
expect(spyCy).toHaveBeenCalledWith(2)
})
})
describe('dx()', () => {
it('should move the element along the x axis relatively and returns itself', () => {
rect.attr({ x: 10, width: 100 })
expect(rect.dx(100)).toBe(rect)
expect(rect.attr('x')).toBe(110)
})
})
describe('dy()', () => {
it('should move the element along the x axis relatively and returns itself', () => {
rect.attr({ y: 10, height: 100 })
expect(rect.dy(100)).toBe(rect)
expect(rect.attr('y')).toBe(110)
})
})
describe('dmove()', () => {
it('should call `dx()` and `dy()` with passed parameters and returns itself', () => {
const spyDx = spyOn(rect, 'dx').and.callThrough()
const spyDy = spyOn(rect, 'dy').and.callThrough()
expect(rect.dmove(1, 2)).toBe(rect)
expect(spyDx).toHaveBeenCalledWith(1)
expect(spyDy).toHaveBeenCalledWith(2)
})
})
describe('width()', () => {
it('should call attr with width', () => {
const spy = spyOn(rect, 'attr')
rect.width(123)
expect(spy).toHaveBeenCalledWith('width', 123)
})
})
describe('height()', () => {
it('should call attr with height', () => {
const spy = spyOn(rect, 'attr')
rect.height(123)
expect(spy).toHaveBeenCalledWith('height', 123)
})
})
describe('size()', () => {
it('should call `width()` and `height()` with passed parameters and returns itself', () => {
const spyWidth = spyOn(rect, 'width').and.callThrough()
const spyHeight = spyOn(rect, 'height').and.callThrough()
expect(rect.size(1, 2)).toBe(rect)
expect(spyWidth).toHaveBeenCalledWith(1)
expect(spyHeight).toHaveBeenCalledWith(2)
})
it('should change height proportionally if null', () => {
const rect = Rect.create(100, 100)
const spyWidth = spyOn(rect, 'width').and.callThrough()
const spyHeight = spyOn(rect, 'height').and.callThrough()
expect(rect.size(200, null)).toBe(rect)
expect(spyWidth).toHaveBeenCalledWith(200)
expect(spyHeight).toHaveBeenCalledWith(200)
})
it('should change width proportionally if null', () => {
const rect = Rect.create(100, 100)
const spyWidth = spyOn(rect, 'width').and.callThrough()
const spyHeight = spyOn(rect, 'height').and.callThrough()
expect(rect.size(null, 200)).toBe(rect)
expect(spyWidth).toHaveBeenCalledWith(200)
expect(spyHeight).toHaveBeenCalledWith(200)
})
})
})

View File

@ -1,4 +1,3 @@
import type { Text } from '../text/text'
import { UnitNumber } from '../../struct/unit-number'
import { Size } from '../common/size'
import { Dom } from '../../dom'
@ -80,36 +79,4 @@ export class Vector<
dmove(x: string | number = 0, y: string | number = 0) {
return this.dx(x).dy(y)
}
// #region Font
font(attrs: Record<string, string | number>): this
font(key: string, value: string | number): this
font(a: Record<string, string | number> | string, v?: string | number) {
if (typeof a === 'object') {
Object.keys(a).forEach((key) => this.font(key, a[key]))
return this
}
if (a === 'leading') {
const text = (this as any) as Text // eslint-disable-line
if (text.leading) {
text.leading(v)
}
return this
}
return a === 'anchor'
? this.attr('text-anchor', v)
: a === 'size' ||
a === 'family' ||
a === 'weight' ||
a === 'stretch' ||
a === 'variant' ||
a === 'style'
? this.attr(`font-${a}`, v)
: this.attr(a, v)
}
// #endregion
}