You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
247 lines
7.2 KiB
247 lines
7.2 KiB
namespace SvgDrawing { |
|
export interface Rect { |
|
x: number; |
|
y: number; |
|
width: number; |
|
height: number; |
|
} |
|
|
|
export interface Point { |
|
x: number; |
|
y: number; |
|
} |
|
|
|
export class Point { |
|
public static add(a: Point, b: Point, out_result: Point) { |
|
out_result.x = a.x + b.x; |
|
out_result.y = a.y + b.y; |
|
} |
|
|
|
public static subtract(a: Point, b: Point, out_result: Point) { |
|
out_result.x = a.x - b.x; |
|
out_result.y = a.y - b.y; |
|
} |
|
|
|
public static normalize(v: Point) { |
|
let invN = 1.0 / Math.sqrt(v.x*v.x + v.y*v.y); |
|
v.x *= invN; |
|
v.y *= invN; |
|
} |
|
|
|
public static dot(a: Point, b: Point) { |
|
return a.x*b.x + a.y*b.y; |
|
} |
|
|
|
public static normalizedDot(a: Point, b: Point) { |
|
let NA = Math.sqrt(a.x*a.x + a.y*a.y); |
|
let NB = Math.sqrt(b.x*b.x + b.y*b.y); |
|
return (a.x*b.x + a.y*b.y) / NA / NB; |
|
} |
|
} |
|
|
|
export class Viewport { |
|
private invDataW: number = 0; |
|
private invDataH: number = 0; |
|
|
|
constructor(private data: Rect, private view: Rect) { this.update(); } |
|
|
|
setData(r: Rect) { this.data = r; this.update(); } |
|
setView(r: Rect) { this.view = r; this.update(); } |
|
|
|
xDataToView(x: number) { return (x - this.data.x) / this.data.width * this.view.width + this.view.x; } |
|
yDataToView(y: number) { return (y - this.data.y) / this.data.height * this.view.height + this.view.y; } |
|
|
|
dataToView(p: Point, out_point: Point) { |
|
out_point.x = (p.x - this.data.x) * this.invDataW * this.view.width + this.view.x; |
|
out_point.y = (p.y - this.data.y) * this.invDataH * this.view.height + this.view.y; |
|
} |
|
|
|
private update() { |
|
this.invDataW = 1.0 / this.data.width; |
|
this.invDataH = 1.0 / this.data.height; |
|
} |
|
} |
|
|
|
export interface LineStyle { |
|
className: string; |
|
} |
|
|
|
export class SvgElement { |
|
public viewport: Viewport; |
|
public width: number; |
|
public height: number; |
|
|
|
constructor(private htmlElement: HTMLElement) { |
|
let viewBox = htmlElement.getAttribute('viewBox').split(' '); |
|
let r: Rect = { x: Number(viewBox[0]), y: Number(viewBox[1]), width: Number(viewBox[2]), height: Number(viewBox[3]) }; |
|
this.viewport = new Viewport(r, { x: r.x, y: r.y + r.height, width: r.width, height: -r.height }); |
|
this.width = r.width; |
|
this.height = r.height; |
|
} |
|
|
|
clear() { |
|
this.htmlElement.querySelectorAll('*').forEach(elt => elt.remove()); |
|
} |
|
|
|
line(start: Point, end: Point): SVGLineElement { |
|
let line = document.createElementNS('http://www.w3.org/2000/svg','line'); |
|
line.setAttribute('x1', this.viewport.xDataToView(start.x).toString()); |
|
line.setAttribute('y1', this.viewport.yDataToView(start.y).toString()); |
|
line.setAttribute('x2', this.viewport.xDataToView(end.x).toString()); |
|
line.setAttribute('y2', this.viewport.yDataToView(end.y).toString()); |
|
this.htmlElement.append(line); |
|
return line; |
|
} |
|
|
|
text(position: Point, value: string, anchor?: string, verticalAlign?: number): SVGTextElement { |
|
let text = document.createElementNS('http://www.w3.org/2000/svg','text'); |
|
text.setAttribute('x', this.viewport.xDataToView(position.x).toString()); |
|
text.setAttribute('y', this.viewport.yDataToView(position.y).toString()); |
|
if(anchor !== undefined) text.setAttribute('text-anchor', anchor); |
|
if(verticalAlign !== undefined) text.setAttribute('dy', (1.0-verticalAlign) + 'em'); |
|
text.appendChild(document.createTextNode(value)); |
|
this.htmlElement.append(text); |
|
return text; |
|
} |
|
|
|
graph(y: number[]): SVGPathElement[]; |
|
graph(x: number[], y: number[]): SVGPathElement[]; |
|
graph(y: number[], indices: number[], styles: LineStyle[]): SVGPathElement[]; |
|
graph(x: number[], y: number[], indices: number[], styles: LineStyle[]): SVGPathElement[]; |
|
graph(arg1: number[], arg2?: number[], arg3?: number[] | LineStyle[], arg4?: LineStyle[]): SVGPathElement[] { |
|
let indices: number[] | null = null; |
|
let styles: LineStyle[] | null = null; |
|
if(arg3) { |
|
if(arg4) { |
|
indices = <number[]>arg3; |
|
styles = arg4; |
|
} |
|
else { |
|
indices = arg2; |
|
styles = <LineStyle[]>arg3; |
|
} |
|
} |
|
|
|
if(styles == null) { |
|
styles = [{className: ''}]; |
|
} |
|
|
|
let read = (idx: number, out_point: Point) => { |
|
out_point.x = arg1[idx]; |
|
out_point.y = arg2[idx]; |
|
return true; |
|
}; |
|
|
|
let reset = (idx: number) => {}; |
|
|
|
if(!arg2 || (arg3 && typeof(arg3[0]) !== 'number')) { |
|
read = (idx: number, out_point: Point) => { |
|
out_point.x = idx; |
|
out_point.y = arg1[idx]; |
|
return true; |
|
}; |
|
} |
|
|
|
let num = arg1.length; |
|
console.assert(!arg2 || num == arg2.length); |
|
|
|
if(num <= 1) return null; |
|
|
|
let optimizeCurveDist = 5; |
|
let optimizeCurveAngle = 45.0; |
|
if(optimizeCurveDist > 0 || optimizeCurveAngle < 0.0) { |
|
let rawRead = read; |
|
|
|
let dp = Math.cos(optimizeCurveAngle/180*Math.PI); |
|
|
|
let lastDrawnPoint: Point = { x: 0, y: 0 }; |
|
|
|
let nextPoint: Point = { x: 0, y: 0 }; |
|
let dir: Point = { x: 0, y: 0 }; |
|
let nextDir: Point = { x: 0, y: 0 }; |
|
let nextSegDir: Point = { x: 0, y: 0 }; |
|
let perp: Point = { x: 0, y: 0 }; |
|
|
|
let rawReset = reset; |
|
reset = (idx: number) => { |
|
rawReset(idx); |
|
read(idx, lastDrawnPoint); |
|
}; |
|
|
|
read = (idx: number, out_point: Point) => { |
|
rawRead(idx, out_point); |
|
if(idx == 0 || idx == num - 1) return true; |
|
|
|
rawRead(idx + 1, nextPoint); |
|
Point.subtract(out_point, lastDrawnPoint, dir); |
|
Point.subtract(nextPoint, out_point, nextSegDir); |
|
|
|
if(Point.normalizedDot(dir, nextSegDir) < dp) { |
|
lastDrawnPoint = { ...out_point }; |
|
return true; |
|
} |
|
|
|
Point.subtract(nextPoint, lastDrawnPoint, nextDir); |
|
|
|
perp.x = -nextDir.y; |
|
perp.y = nextDir.x; |
|
Point.normalize(perp); |
|
|
|
let d = Math.abs(Point.dot(perp, dir)); |
|
if(d > optimizeCurveDist) { |
|
lastDrawnPoint = { ...out_point }; |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
} |
|
|
|
let startTime = performance.now(); |
|
let count = 0; |
|
|
|
let paths = []; |
|
|
|
for(let styleIdx = 0; styleIdx < styles.length; ++styleIdx) { |
|
let dataPoint: Point = { x: 0, y: 0 }; |
|
let viewPoint: Point = { x: 0, y: 0 }; |
|
|
|
let coordinates = ''; |
|
|
|
let open = false; |
|
for(let idx = 0; idx < num; ++idx) { |
|
let includePoint = !indices || indices[idx] == styleIdx; |
|
|
|
if(!open && includePoint) { |
|
reset(idx); |
|
read(idx, dataPoint); |
|
this.viewport.dataToView(dataPoint, viewPoint); |
|
let space = coordinates == '' ? '' : ' '; |
|
coordinates += space+'M'+Math.round(viewPoint.x)+','+Math.round(viewPoint.y) + ' L'; |
|
count += 1; |
|
} |
|
else if(open && (read(idx, dataPoint) || !includePoint)) { |
|
this.viewport.dataToView(dataPoint, viewPoint); |
|
coordinates += ' '+Math.round(viewPoint.x)+','+Math.round(viewPoint.y); |
|
count += 1; |
|
} |
|
|
|
open = includePoint; |
|
} |
|
|
|
let style = styles[styleIdx]; |
|
let path = document.createElementNS('http://www.w3.org/2000/svg','path'); |
|
path.setAttribute('class', 'graph' + (style.className == '' ? '' : ' ' + style.className)); |
|
path.setAttribute('d', coordinates); |
|
this.htmlElement.append(path); |
|
|
|
paths.push(path); |
|
} |
|
|
|
let endTime = performance.now(); |
|
console.log("graph: " + count + " points, " + (endTime - startTime) + "ms"); |
|
|
|
return paths; |
|
} |
|
} |
|
}
|
|
|