// TODO have a look at rxjs v5 (chainable operators) or v6 (pipeable operators) // (could give some hint at how to do a builder-like pattern) // TODO units (px, rem) etc (set svg default? per primitive? mix'n'match?) import { SVGRenderer } from './render' // TODO deprecated class based in favor of functional interface // TODO split in DOM (createElement, setAttribute) and Primitive object shizzle (pos, getPos, drag) // TODO support all svg (2.0? mention version in docs) elements on https://developer.mozilla.org/en-US/docs/Web/SVG/Element // TODO support all attributes on https://developer.mozilla.org/nl/docs/Web/SVG/Attribute // TODO make interactive using https://www.petercollingridge.co.uk/tools/interactivesvgjs/ and // https://www.petercollingridge.co.uk/blog/3d-svg/rotating-3d-svg-cube/ // and https://www.petercollingridge.co.uk/blog/3d-svg/3d-animation-svg/ // and https://github.com/petercollingridge/InteractiveSVG.js/blob/master/interactiveSVG.js // and https://www.petercollingridge.co.uk/tutorials/svg/interactive/ // TODO get inspiration from snapsvg, raphael svg, gsap svg (and mention credits!) // TODO esveegee.ts, miter.ts, path.ts // TODO what if other units? rem, px etc? .units(SVG.Rem)? // TODO something from macbook -> i dunno... can't remember (some draw thingie? Patchwork? Hatches? patterns?) ) // TODO with no fill for the childs, group is not draggable! is this a common bugs for non-filled elements? // show light or outline as visual indicator? // TODO document: individual elements can be draggable as well (now go make your paint program!) // TODO note that svg signification digits are 2-3 at best,s so don't blow up your 10x10 to full screen :-) // TODO how to access elements adjust their properties? some query language? import { applyMixins } from './mixins.js' import { drag } from './draggable.js' // import { size } from './sizeable.js' export const elementer = (svg: SVGElement) => ({ circle: (radius: number) => circle(svg, radius), // TODO overload with line(length) which can be positioned with x and y line: (x1: number, y1: number, x2: number, y2: number) => line(svg, x1, y1, x2, y2), // can be sized with length(10) or bounds(100, 200) rect: (width: number, height: number) => rect(svg, width, height), text: (s: string) => text(svg, s), __element: svg // TODO DOC use at own risk! // TODO cache (profile first!) (how, elements have different properties and the draw methods need a reference to an element) }) // TODO combine sizer and positioner (and painter? what can be painted, can be sized? is already an exception though) into dimensioner const positionerXY = (svg: SVGElement, element: SVGElement) => ({ position: (x: number, y: number) => positionXY(svg, element, x, y) }) const positionerX1Y1 = (svg: SVGElement, element: SVGElement) => ({ position: (x1: number, y1: number) => positionX1Y1(svg, element, x1, y1) }) const positionXY = (svg: SVGElement, element: SVGElement, x: number, y: number) => { // TODO svg elements have different methods of positing (line has x1, y1, x2, y2), g (and others) have transform, rect has x and y // needs some form of polymorphism setAttributes(element, { x, y }) return { ...elementer(svg), ...painter(svg, element), ...converter(svg) } // TODO function union / function sum type possible? } const positionX1Y1 = (svg: SVGElement, element: SVGElement, x1: number, y1: number) => { // TODO svg elements have different methods of positing (line has x1, y1, x2, y2), g has transform, rect has x and y // needs some form of polymorphism setAttributes(element, { x1, y1 }) return { ...elementer(svg), ...painter(svg, element), ...converter(svg) } // TODO function union / function sum type possible? } // composition experiment fluent API // maybe solve the PositionXY / PositionTransform / PositionX1Y1 with a strategy set by the line(), circle(), rect() primitives? // or always use transform="translate(x, y)" for consistency? const NS = 'http://www.w3.org/2000/svg' export const SVG2 = (width = 300, height = 150, document: Document = window.document) => { const svg = document!.createElementNS(NS, 'svg') document.body.append(svg) return all(svg) } const circle = (svg: SVGElement, radius: number) => { const element = document!.createElementNS(NS, 'circle') element.setAttribute('r', radius.toString()) svg.append(element) return { ...elementer(svg), ...painter(svg, element), ...positionerXY(svg, element), ...positionerX1Y1(svg, element), ...sizer(svg, element), ...converter(svg), } } const line = (svg: SVGElement, x1: number, y1: number, x2: number, y2: number) => { const element = document!.createElementNS(NS, 'line') setAttributes(element, { x1, y1, x2, y2 }) svg.append(element) return { ...elementer(svg), ...painter(svg, element), ...converter(svg) } } const rect = (svg: SVGElement, width: number, height: number) => { const element = document!.createElementNS(NS, 'rect') svg.append(element) setAttributes(element, { height, width }) return { ...elementer(svg), ...positionerXY(svg, element) } } const text = (svg: SVGElement, s: string) => { const element = document!.createElementNS(NS, 'text') svg.append(element) return { ...(elementer(svg)), ...(painter(svg, element)), ...(texter(svg, element)) } } // TODO idea? derive from base which has toString() => outputs the svg XML // TODO attributes = map or object? const setAttributes = (element: Element, attributes: object) => { for (const [name, value] of Object.entries(attributes)) { element.setAttribute(name, value) } } interface Positionable { position(svg: SVGElement, element: SVGElement, x: number, y: number): void } const sizer = (svg: SVGElement, element: SVGElement) => ({ size: (width: number, height: number) => size(svg, element, width, height) }) const size = (svg: SVGElement, element: SVGElement, width: number, height: number) => { element.setAttribute('width', width.toString()) element.setAttribute('height', height.toString()) return { ...(elementer(svg)), ...(painter(svg, element)) } } const converter = (svg: SVGElement) => ({ toSVG: () => toSVG(svg) }) const toSVG = (svg: SVGElement) => { return svg.outerHTML } // const dragger = {} const painter = (svg: SVGElement, element: SVGElement) => ({ fill: (color: string) => fill(svg, element, color), stroke: (color: string) => stroke(svg, element, color) }) const texter = (svg: SVGElement, element: SVGElement) => ({ align: (alignment: string) => align(svg, element, alignment) // TODO follow SVG attribute names, don't make up ourselves // vertical align }) const all = (svg: SVGElement) => ({ ...elementer(svg), ...painter(svg, svg), ...texter(svg, svg), ...positionerXY(svg, svg), ...sizer(svg, svg), }) // no worky (not needed as well possible) const stringer = { toString: () => `toString()` } const align = (svg: SVGElement, element: SVGElement, alignment: string) => { element.setAttribute('base-asdalskaslfs', alignment) return { ...(elementer(svg)), ...(painter(svg, element)), ...(texter(svg, element)) } } const fill = (svg: SVGElement, element: SVGElement, color: string) => { element.setAttribute('fill', color) return { ...elementer(svg), ...painter(svg, element) } // TODO this should cached! performance loss: spread } const stroke = (svg: SVGElement, element: SVGElement, color: string) => { element.setAttribute('stroke', color) // TODO which positioner (XY or X1Y1) to return? it depends on the element being drawn // strokeXY, strokeX1Y1 :-( console.log(element.tagName) return { ...elementer(svg), ...painter(svg, element), ...converter(svg), ...positionerXY(svg, element), ...positionerX1Y1(svg, element) } } // const figure1 = SVG().rect(10, 20).line(10, 10, 150, 200).stroke('red').circle(10).fill('blue') // const figure2 = SVG().text('sjaak').align('left') // you can call align on text(), but not on line() // const s1 = SVG().align('left') // alignment use for whole svg // const circleElement = SVG(300, 300).circle(10) // console.log(s1.toString()) // output xml (doesn't call toString() :-()) // svg(100, 200) interface IPositionable { position(x: number, y: number): void getPosition(): { x: number, y: number } } interface ISizeable { size(width: number, height: number): void } const elements = new Map() export abstract class Primitive implements IPositionable { // TODO deprecate in favor of SVGRenderer.createEle protected static createElement(qualifiedName: string, owner: Document = document): SVGElement { // TODO use owner if available // TODO can we skip document dependency? can it be avoided (by resolving it up to the root of the container?) // (how do node.js projects do this?) // const document = this.resolveOwner(this.svg) return document!.createElementNS('http://www.w3.org/2000/svg', qualifiedName) } protected static resolveOwner(element: Element) { if (element.ownerDocument) { return element.ownerDocument } else { return document } } // TODO not in use? public constructor(protected parent: Element) { elements.set(parent, this) } // TODO expose directly in class SVG so every possible element is suppooted? public primitive(tagName: string, attributes: object = {}, namespace: string = ''): Element { // TODO ISimpleMap or Map? const element = namespace === '' ? this.createElement(tagName) : this.createElementNS(namespace, tagName) for (const [name, value] of Object.entries(attributes)) { if (value) { element.setAttribute(name, value) } } return element } public attribute(name: string, value: string) { this.parent.setAttribute(name, value) } public position(x: number, y: number) { this.parent.setAttribute('x', x.toString()) this.parent.setAttribute('y', y.toString()) return this } public getPosition(): { x: number, y: number } { return { x: Number(this.parent.getAttribute('x')), y: Number(this.parent.getAttribute('y')) } } public remove() { // TODO buggy group().circle().remove().rect() removes everything in the group elements.delete(this.parent) this.parent.remove() return this } public title(s: string) { SVG.createElement('title') this.parent.append(s) return this } // Group implements FillableParent // sets fill for childs (groups itself cannot have fill) // Group returns itself // Other primitives return their parent (because new primitives are drawn adjacent to them) public group(x: number = 0, y: number = 0) { const group = SVG.createElement('g') group.setAttribute('transform', `translate(${x}, ${y})`) this.add(group) return new Group(group) } public groupEnd() { // TODO close group (return parent Element?) } public circle(x: number, y: number, radius: number) { const circle = this.primitive('circle', { cx: x, cy: y, r: radius }) this.add(circle) return new Circle(circle) } public line(x1: number, y1: number, x2: number, y2: number) { const line = this.primitive('line', { x1, y1, x2, y2 }) this.add(line) return new Line(line) } // TODO data structure, not 'd' public path(d: string) { const path = this.primitive('path', { d }) this.add(path) return new Path(path) } // TODO support other formats as well (array of coordinates {x, y}, string? ) public polygon(...numbers: number[]) { const polygon = this.primitive('polygon', { points: numbers.join(' ') }) this.add(polygon) return new Polygon(polygon) } public polyline(...numbers: number[]) { const polyline = this.primitive('polyline', { points: numbers.join(' ') }) this.add(polyline) return new Polyline(polyline) } public rect(x: number, y: number, width: number, height: number) { const rect = this.primitive('rect', { x, y, width, height }) this.add(rect) return new Rect(rect) } public text(caption: string, x: number, y: number) { // sensible text colors // TODO bug: color set to null, must get color from root SVG const color = this.parent.getAttribute('stroke') const text = this.primitive('text', { x, y }) text.textContent = caption text.setAttribute('fill', color!) text.setAttribute('stroke', 'none') this.add(text) return new Text(text) } // FIXME audio, canvas, iframe, video no worky (something namespace related) // see http://xn--dahlstrm-t4a.net/svg/audio/html5-audio-in-svg.svg for a static example that works public audio(src: string) { const audio = this.primitive('html:audio', { src, controls: true }, 'html') this.add(audio) return new Audio(audio) } public canvas() { const canvas = this.primitive('html:canvas', {}, 'html') this.add(canvas) return new Canvas(canvas) } public iframe(src: string) { const iframe = this.primitive('html:iframe', {}, 'html') this.add(iframe) return new IFrame(iframe) } public video(src: string) { const video = this.primitive('video', { src, controls: true, height: 200 }, 'http://www.w3.org/1999/xhtml') this.add(video) return new Video(video) } public html(s: string, x: number, y: number, width: number = 200, height: number = 150) { const html = this.primitive('foreignObject', { x, y, width, height }) html.innerHTML = s this.add(html) return new HTML(html) } // helpers public grid(xSpacing: number = 16, ySpacing: number = 16, xOffset: number = 0, yOffset: number = 0) { // TODO use pattern (so sizing doesn't affect the grid) const group = this.group() // .stroke('lightgray') for (let i = 0; i < 20; i++) { group.line(0, i * ySpacing + yOffset, 300, i * ySpacing + yOffset) group.line(i * xSpacing + xOffset, 0, i * xSpacing + xOffset, 300) } return new Grid(group.parent) } // TODO merge createElement and createElementNS (return types differ!) protected createElement(qualifiedName: string, owner: Document = document): SVGElement { // TODO use owner if available // TODO can we skip document dependency? can it be avoided (by resolving it up to the root of the container?) // (how do node.js projects do this?) // const document = this.resolveOwner(this.svg) return document!.createElementNS('http://www.w3.org/2000/svg', qualifiedName) } protected createElementNS(namespace: string, qualifiedName: string, owner: Document = document): Element { return document!.createElementNS(namespace, qualifiedName) } protected add(element: Element) { // TODO instance of Container or Containable? if (this.parent.tagName === 'svg' || this.parent.tagName === 'g') { this.parent.appendChild(element) } else { this.parent.insertAdjacentElement('afterend', element) } // return element } } export enum SVGColors { None = 'none', Transparent = 'transparent' } // TODO combine Fillable and Strokable? they exist always together (Paintable? Stylable?) class Fillable extends Primitive { public fill(color: Color = SVGColors.None) { this.parent.setAttribute('fill', color) return this } public class(name: string) { this.parent.setAttribute('class', name) return this } public focusable(enable: boolean = true) { if (enable) { this.parent.setAttribute('tabindex', '0') } else { this.parent.removeAttribute('tabindex') } return this } } class Strokable extends Primitive { public stroke(color: Color = SVGColors.None) { this.parent.setAttribute('stroke', color) return this } public class(name: string) { this.parent.setAttribute('class', name) return this } // TODO width have some values like 'vector-resize'bla' for harlines // TODO call it size() but see applyMixins for diamond problem public width(size: number | string) { this.parent.setAttribute('stroke-width', size.toString()) return this } } class Filter extends Primitive { public blend(x: number | string, y: number | string, width: number | string, height: number | string) { const filterUnits = 'objectBoundingBox' const primitiveUnits = '"userSpaceOnUse' const blend = this.primitive('feBlend', { x, y, width, height, filterUnits, primitiveUnits }) this.add(blend) // TODO add to defs (or apply filters after drawing primitive? cannot reuse) return new Filter(blend) } } class Filterable extends Primitive { public use() { return this } } export const enum Cap { Butt = 'butt', Round = 'round', Square = 'square' } class Cappable extends Primitive { public cap(type: Cap) { this.parent.setAttribute('stroke-linecap', type.toString()) } } export enum Join { Arcs = 'arcs', Bevel = 'bevel', Miter = 'miter', MiterClip = 'miter-clip', Round = 'round' } // TODO only on , , , , , , , , and class Miterable extends Primitive { public miterlimit(n: number) { this.parent.setAttribute('stroke-miterlimit', n.toString()) } public join(type: Join) { this.parent.setAttribute('stroke-linejoin', type.toString()) } } export enum Direction { NorthSouth = 'ns-resize', EastWest = 'ew-resize' } // TODO class Sizable (to resize elements using the mouse/touch) // TODO Handable (sizeable, draggeabe, nameable) // TODO draggable implies that all nodes are draggable as well? // line().draggable() -> you can drag as a whole + drag nodes // rect().draggable() -> you can drag as a whole + drag nodes // maybe introducte .editable()? class Draggable extends Primitive { // TODO draggable: mouseleave -> do not move element, but keep capture (feels more intuitive maybe, // see how in inkscape, gimp etc do it) // TODO draggable().cursor(Cursor.EastWest) or draggable(Drag.HORIZONTAL) // TODO east-west only cursor: ew-resize draggable(Direction.EastWest) public draggable(enable: boolean = true) { // TODO dataset instead... if (enable) { this.parent.classList.add('draggable') } else { this.parent.classList.remove('draggable') } return this } public connectable(enable: boolean = true) { // TODO dataset instead... if (enable) { this.parent.classList.add('connectable') } else { this.parent.classList.remove('connectable') } return this } public sizeable(enable: boolean = true) { // TODO dataset instead... if (enable) { this.parent.classList.add('sizeable') } else { this.parent.classList.remove('sizeable') } return this } public id(id: string) { // TODO doc: please use sensible id's (so prevent using things that can // be interpreted as valid SVG/XSS, az09 should be fine) this.parent.setAttribute('id', id) } } // TODO replace with defs(arrows(), ...) export enum Marker { None, Start, End } // TODO how to use? line().markers().start().end()? export type Color = string export interface ISimpleMap { [key: string]: string } // TODO separate Graph and SVG? // the name refers to Graphic and Graph class Graph { // type Coordinates = (x1: number, y1: number, x2: number, y2: number) // TODO separate createElement and creating of objects // public line(Coordinates | { Coordinate }) { // return { // tagName: 'line', // element: new Line(0, 0, 0, 0), // // these can be set as attributes directly, params could be a member of Line // params: { x1, y1, x2, y2 } // } } /* // TODO like path(penDown().penUp().move(0, 0).line(10, 20).cubicBezier(12, 15).quadBezier(15, 20).arc(...).close() TODO autodetect quad or cubic bezier q = relative Q = absolute bezier({}) C x1 y1, x2 y2, x y (two control points) Q x1 y1, x y (one control point) (there is also S command, interesting!) */ // TODO attributes stroke-width, font, fill-opacity, stroke-opacity, opacity etc // TODO how to deal with coorddinages? you really should draw on 25.5 for example if you want 1px line (not 25) // or document this? research. Or is it sane to do? // TODO sane defaults (e.g. pattern UseOnSpace and set widdt/height or you won't see anything) // TODO patterns, gradients, images as fill and stroke /* getters could be useful as well? not very consistent image.rect(x, y, width, height).scale(2, 2) image.rect(x, y, { width: 200, height: 100, fill: 'red' }) image.rect(x, y).style({ border: '1px solid red' }) // TODO CSP?! paper.js interessant? canvas, maar leuke API */ // TODO Draggable/pannable as well (i think this calls for a root element // that is not exported and can have things like draggable) export class SVG extends Primitive implements ISizeable { public static on(element: SVGElement | Element | string) { const root = SVG.createSVG(element) const svg = new SVG(root) if (!element) { throw new Error(`cannot bind to ${element}`) } root.setAttribute('version', '1.1') // FIXME 2.0? // FIXME problaby not the cleanest way to set a namespace root.setAttribute('xmlns:html', 'http://www.w3.org/1999/xhtml') // TODO this.size(width, height) // listen to size of original element (svg, element or string) root.setAttribute('fill', 'none') root.setAttribute('stroke', 'black') // TODO svg.appendChild(arrowDefs()) return svg } // element can be a DOMElement, a SVGElement ( or primitive) or a CSS selector protected static createSVG(element: SVGElement | Element | string) { // TODO resolveElement() if (element instanceof SVGElement) { return element } else { const svg = SVG.createElement('svg') if (element instanceof Element) { element.prepend(svg) } else if (typeof element === 'string') { const container = document.querySelector(element)! container.prepend(svg) } return svg } } // TODO expose this for advanced users? protected static setAttributes(element: Element, attributes: object) { for (const [name, value] of Object.entries(attributes)) { element.setAttribute(name, value) } } private svg: Element // TODO SVGElement? (from parent?) // TODO class Dragger? private selected: Primitive | undefined = undefined private offset: { x: number, y: number } = { x: 0, y: 0 } // TODO constructor without being dependent on the DOM // TODO parent is not a good name, it is about the accompanying SVG element constructor(protected parent: SVGElement) { super(parent) this.svg = parent // TODO not that if you implement this from https://www.petercollingridge.co.uk/tutorials/svg/interactive/dragging/ // there is a undefined bug somewhere (which is solved here -> notify peter) parent.addEventListener('mousedown', this.startDrag) parent.addEventListener('mousemove', this.drag) parent.addEventListener('mouseup', this.endDrag) parent.addEventListener('mouseleave', this.endDrag) // TODO touch events // parent.addEventListener('touchstart', this.startDrag) // parent.addEventListener('touchmove', this.drag) // parent.addEventListener('touchend', this.endDrag) // parent.addEventListener('touchleave', this.endDrag) // parent.addEventListener('touchcancel', this.endDrag) } public pattern(o: object) { // TODO add some pattern definition to for later use return this } // public get size() { // return { width: -9000, height: -9000 } // } // public size(): { width: number, height: number } // TODO complete viewPort (x,y origin + dimensions) // TODO overload public size(dimensions: { width: number = 0, height: number = 0 }): any public size(width: number = 0, height: number = 0): any { this.parent.setAttribute('width', width.toString()) this.parent.setAttribute('height', height.toString()) this.parent.setAttribute('viewBox', `0 0 ${width} ${height}`) // not needed if 0 0 width height is same as svg (or for responsive?) // TODO find out ratio preserving this.parent.setAttribute('preserveAspectRatio', 'xMinYMin meet') // TODO fix type crap if (width && height) { return this // } else { // return { width: 1, height: 2 } } else { return this } } // TODO correct type public viewBox(xorigin: number, yorigin: number, width: number, height: number): any public viewBox(width: number, height: number): any { // TODO setAttributes(this.parent) } // TODO mixin public stroke(color: Color = SVGColors.None) { this.parent.setAttribute('stroke', color) return this } protected resolveElement(element: SVGElement | Element | string) { // TODO resolve element to SVGElement } private startDrag(event: MouseEvent) { // TODO cursor = 'grabbing' const target = event.target if (target.classList.contains('draggable')) { event.preventDefault() this.selected = elements.get(target) // TODO to getMousePosition (like in drag.html) const svg = target.ownerSVGElement const ctm = svg.getScreenCTM() this.offset = { x: (event.clientX - ctm!.e) / ctm!.a, y: (event.clientY - ctm!.f) / ctm!.d } const position = this.selected?.getPosition() this.offset.x -= position!.x this.offset.y -= position!.y } } // TODO see http://www.petercollingridge.co.uk/tutorials/svg/interactive/dragging/ // TODO draggable on svg document, bubble down (like in relations.js) -> only one draggable object needed // TODO how to drag groups (add thin-line rect? use one element to drag everything in the gruop?) // TODO bug: if you try to grab the edge of an element and move away from the element, // the element gets lost and fails with 'esveegee.ts:586 Uncaught TypeError: Cannot read property 'getScreenCTM' of null // at SVGSVGElement.drag (esveegee.ts:586)' private drag(event: MouseEvent) { // TODO bug: if another element is in the way, event.target changes to that one and the dragging stops const target = event.target if (this.selected) { event.preventDefault() const svg = target.ownerSVGElement const ctm = svg.getScreenCTM() // FIXME null when element is 'lost' // (a*x + e, d*y + f) const x = (event.clientX - ctm!.e) / ctm!.a const y = (event.clientY - ctm!.f) / ctm!.d this.selected.position(x - this.offset.x, y - this.offset.y) } } private endDrag(event: MouseEvent) { if (this.selected) { event.preventDefault() this.selected = undefined } } // TODO support external images (x:href bla) // TODO support (external) svg patterns // TODO responsive private filterDefs(id: string) { // TODO const filter = SVG.createELement('filter') } // TODO deliver some default hatches (striped, rotated) private arrowDefs() { // create const defs = SVG.createElement('defs') const marker = SVG.createElement('marker') const [arrow] = [SVG.createElement('path')] // configure marker.setAttribute('id', 'arrow') marker.setAttribute('markerWidth', '25') marker.setAttribute('markerHeight', '25') marker.setAttribute('refX', '15') marker.setAttribute('refY', '6') marker.setAttribute('orient', 'auto') marker.setAttribute('markerUnits', 'strokeWidth') // triangle 9x6 (will showup as strokeWidth * 9 &* 7) arrow.setAttribute('d', 'M0 0 L0 12 L18 6 z') arrow.setAttribute('fill', 'black') arrow.setAttribute('stroke', 'none') // connect defs.appendChild(marker) marker.appendChild(arrow) return defs } } // TODO note that const circle = 'document.cre'ateElementNS(null, 'circle') is // valid if you are in a SVG already (no need to specify namespace; createElement() does not work though) class Circle extends Primitive { // TODO real tooltips (what's the use on touch devices? maybe tap) public position(x: number, y: number) { const r = Number(this.parent.getAttribute('r')) this.parent.setAttribute('cx', x.toString()) this.parent.setAttribute('cy', y.toString()) // this.parent.setAttribute('cx', (x + r / 2).toString()) // this.parent.setAttribute('cy', (y + r / 2).toString()) return this } public getPosition(): { x: number, y: number } { return { x: Number(this.parent.getAttribute('cx')), y: Number(this.parent.getAttribute('cy')) } } } class Line extends Primitive { public position(x: number, y: number) { this.parent.setAttribute('x1', x.toString()) this.parent.setAttribute('y1', y.toString()) // TODO calc new line end // this.parent.setAttribute('x2', x.toString()) // this.parent.setAttribute('y2', y.toString()) return this } } class Path extends Primitive { private isAbsolute = false // TODO act on it (z or Z) public absolute() { this.isAbsolute = true return this } public relative() { this.isAbsolute = false return this } public bezier(x: number, y: number) { this.parent.setAttribute('d', '${x}, ${y}') return this } public control(x1: number, y1: number, x2: number, y2: number): Path // cubic public control(x: number, y: number) { // quadratic const d = this.parent.getAttribute('d') // TODO cubic/quad this.parent.setAttribute('d', `${d} ${x} ${y}`) return this } public smooth(x1: number, y1: number, x2: number, y2: number): Path // cubic public smooth(x: number, y: number) { // quadratic const d = this.parent.getAttribute('d') // TODO cubic/quad this.parent.setAttribute('d', `${d} ${x} ${y}`) return this } } class Polygon extends Primitive { } class Polyline extends Primitive { } class Media extends Primitive { } class Audio extends Media { } class Canvas extends Media { } class IFrame extends Media { } class Video extends Media { } class Group extends Primitive { public position(x: number, y: number) { this.parent.setAttribute('transform', `translate(${x} ${y})`) return this } } class Rect extends Primitive implements ISizeable { public round(radiusX: number, radiusY: number) { this.parent.setAttribute('rx', radiusX.toString()) this.parent.setAttribute('ry', radiusY.toString()) return this } public size(width: number, height: number) { this.parent.setAttribute('width', width.toString()) this.parent.setAttribute('height', height.toString()) return this } } class Grid extends Primitive { } export enum Align { Start = 'start', Middle = 'middle', End = 'end' } export enum VerticalAlign { Auto = 'auto', Baseline = 'baseline', BeforeEdge = 'before-edge', TextBeforeEdge = 'text-before-edge', Middle = 'middle', Central = 'central', AfterEdge = 'after-edge', TextAfterEdge = 'text-after-edge', Ideographic = 'ideographic', Alphabetic = 'alphabetic', Hanging = 'hanging', Mathematical = 'mathematical', Top = 'top', Center = 'center', Bottom = 'bottom' } class Text extends Primitive { public align(align: Align | string) { this.parent.setAttribute('text-anchor', align) return this } // TODO use font().size() (so it can be used on and as well to set it once for a lot of elements?) public size(size: number | string) { if (typeof size === 'string') { this.parent.setAttribute('font-size', size) } else { this.parent.setAttribute('font-size', `${size}px`) } return this } public verticalAlign(align: VerticalAlign | string) { // FIXME Safari doesn't this.parent.setAttribute('dominant-baseline', align) return this } } class HTML extends Primitive { } // TODO not so primitive! // Draggable // --------- // line: end points + segment // svg: geen width/height, well viewBox // preserveAspectRatio="xMidYMin slice" // // kleine icons: wel width/height // // hairline: vector-effect: non-scaling-stroke; // two way binding? // image.line(10, 0, 10, 200).draggable(true, element => callback).followX() // TODO blog about this fluent api using multiple inheritance (mixins) // advantage: text().fill().textAlign() but not rect().fill().textAlign() // TODO is multiple inheritance the way to go? or should classes of classes be created? // (can you do without classes and create a fluent api? return a labmda?) // const line = (x1, y1) => return something that adheres to an interface? const line2 = () => { return drawers } const move2 = () => { return drawers } const drawers = { line2, move2 } const Builder = () => { return drawers } // TODO split line in move(x, y), line(x, y) and line (x1, y1, x2, y2) // interface SVG extends Fillable, Strokable {} // TODO in ts 4.x this can be simplified https://www.typescriptlang.org/docs/handbook/mixins.html interface Circle extends Fillable, Strokable, Draggable { } interface Group extends Fillable, Strokable, Miterable, Cappable, Draggable { } interface Line extends Fillable, Strokable, Miterable, Cappable, Draggable { } interface Polygon extends Fillable, Strokable, Miterable, Cappable, Draggable { } interface Polyline extends Fillable, Strokable, Miterable, Cappable, Draggable { } interface Rect extends Fillable, Strokable, Miterable, Draggable { } interface Text extends Fillable, Strokable, Miterable, Cappable, Draggable { } interface Group extends Draggable { } interface HTML extends Draggable { } applyMixins(Circle, [Fillable, Strokable, Draggable]) applyMixins(Group, [Fillable, Strokable, Cappable, Draggable]) applyMixins(Line, [Fillable, Strokable, Cappable, Draggable]) applyMixins(Polygon, [Fillable, Strokable, Miterable, Cappable, Draggable]) applyMixins(Polyline, [Fillable, Strokable, Miterable, Cappable, Draggable]) applyMixins(Rect, [Fillable, Strokable, Cappable, Draggable]) applyMixins(Text, [Fillable, Strokable, Draggable]) applyMixins(Group, [Draggable]) applyMixins(HTML, [Draggable])