smoosic
Version:
<sub>[Github site](https://github.com/Smoosic/smoosic) | [source documentation](https://smoosic.github.io/Smoosic/release/docs/modules.html) | [change notes](https://aarondavidnewman.github.io/Smoosic/changes.html) | [application](https://smoosic.github.i
526 lines (492 loc) • 18.2 kB
text/typescript
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)
// Copyright (c) Aaron David Newman 2021.
import { Transposable, SvgBox, SvgPoint, ElementLike } from '../../smo/data/common';
import { SvgPage } from './svgPageMap';
declare var $: any;
/**
* @internal
*/
export interface StrokeInfo {
strokeName: string,
stroke: string,
strokeWidth: string | number,
strokeDasharray: string | number,
fill: string,
opacity: number
}
/**
* @internal
*/
export interface OutlineInfo {
stroke: StrokeInfo,
classes: string,
box: SvgBox | SvgBox[],
scroll: SvgPoint,
context: SvgPage,
timeOff: number,
timer?: number,
element?: ElementLike
}
/**
* @internal
*/
export interface GradientInfo {
color: string, offset: string, opacity: number
}
/**
* @internal
*/
export interface Boxable {
box: SvgBox
}
/**
* @internal
*/
export class SvgBuilder {
e: Element;
constructor(el: string) {
const ns = SvgHelpers.namespace;
this.e = document.createElementNS(ns, el);
}
classes(cl: string): SvgBuilder {
this.e.setAttributeNS('', 'class', cl);
return this;
}
attr(name: string, value: string): SvgBuilder {
this.e.setAttributeNS('', name, value);
return this;
}
text(x: number | string, y: number | string, classes: string, text: string): SvgBuilder {
x = typeof (x) == 'string' ? x : x.toString();
y = typeof (y) == 'string' ? y : y.toString();
this.e.setAttributeNS('', 'class', classes);
this.e.setAttributeNS('', 'x', x);
this.e.setAttributeNS('', 'y', y);
this.e.textContent = text;
return this;
}
rect(x: number | string, y: number | string, width: number | string, height: number | string, classes: string): SvgBuilder {
x = typeof (x) == 'string' ? x : x.toString();
y = typeof (y) == 'string' ? y : y.toString();
width = typeof (width) == 'string' ? width : width.toString();
height = typeof (height) == 'string' ? height : height.toString();
this.e.setAttributeNS('', 'x', x);
this.e.setAttributeNS('', 'y', y);
this.e.setAttributeNS('', 'width', width);
this.e.setAttributeNS('', 'height', height);
if (classes) {
this.e.setAttributeNS('', 'class', classes);
}
return this;
}
line(x1: number | string, y1: number | string, x2: number | string, y2: number | string, classes: string): SvgBuilder {
x1 = typeof (x1) == 'string' ? x1 : x1.toString();
y1 = typeof (y1) == 'string' ? y1 : y1.toString();
x2 = typeof (x2) == 'string' ? x2 : x2.toString();
y2 = typeof (y2) == 'string' ? y2 : y2.toString();
this.e.setAttributeNS('', 'x1', x1);
this.e.setAttributeNS('', 'y1', y1);
this.e.setAttributeNS('', 'x2', x2);
this.e.setAttributeNS('', 'y2', y2);
if (classes) {
this.e.setAttributeNS('', 'class', classes);
}
return this;
}
append(el: any): SvgBuilder {
this.e.appendChild(el.e);
return this;
}
dom(): Element {
return this.e;
}
static b(element: string): SvgBuilder {
return new SvgBuilder(element);
}
}
/**
* Mostly utilities for converting coordinate spaces based on transforms, etc.
* @internal
*/
export class SvgHelpers {
static get namespace(): string {
return "http://www.w3.org/2000/svg";
}
// ### gradient
// Create an svg linear gradient.
// Stops look like this:
// `[{color:"#eee", offset:"0%",opacity:0.5}]`
// orientation is horizontal or vertical
static gradient(svg: SVGSVGElement, id: string, orientation: string, stops: GradientInfo[]) {
var ns = SvgHelpers.namespace;
var x2 = orientation === 'vertical' ? 0 : 1;
var y2 = orientation === 'vertical' ? 1 : 0;
var e = document.createElementNS(ns, 'linearGradient');
e.setAttributeNS('', 'id', id);
e.setAttributeNS('', 'x1', '0');
e.setAttributeNS('', 'x2', x2.toString());
e.setAttributeNS('', 'y1', '0');
e.setAttributeNS('', 'y2', y2.toString());
stops.forEach((stop) => {
var s = document.createElementNS(ns, 'stop');
s.setAttributeNS('', 'stop-opacity', stop.opacity.toString());
s.setAttributeNS('', 'stop-color', stop.color);
s.setAttributeNS('', 'offset', stop.offset);
e.appendChild(s);
});
svg.appendChild(e);
}
static renderCursor(svg: ElementLike, x: number, y: number, height: number) {
if (svg === null) {
throw("invalid svg in renderCursor");
}
var ns = SvgHelpers.namespace;
const width = height * 0.4;
x = x - (width / 2);
var mcmd = (d: string, x: number, y: number) => {
return d + 'M ' + x.toString() + ' ' + y.toString() + ' ';
};
var qcmd = (d: string, x1: number, y1: number, x2: number, y2: number) => {
return d + 'q ' + x1.toString() + ' ' + y1.toString() + ' ' + x2.toString() + ' ' + y2.toString() + ' ';
};
var lcmd = (d: string, x: number, y: number) => {
return d + 'L ' + x.toString() + ' ' + y.toString() + ' ';
};
var x1 = (width / 2) * .333;
var y1 = -1 * (x1 / 4);
var x2 = (width / 2);
var y2 = x2 / 4;
var ns = SvgHelpers.namespace;
var e = document.createElementNS(ns, 'path');
var d = '';
d = mcmd(d, x, y);
d = qcmd(d, x1, y1, x2, y2);
d = lcmd(d, x + (width / 2), y + height - (width / 8));
d = mcmd(d, x + width, y);
d = qcmd(d, -1 * x1, y1, -1 * x2, y2);
d = mcmd(d, x, y + height);
d = qcmd(d, x1, -1 * y1, x2, -1 * y2);
d = mcmd(d, x + width, y + height);
d = qcmd(d, -1 * x1, -1 * y1, -1 * x2, -1 * y2);
e.setAttributeNS('', 'd', d);
e.setAttributeNS('', 'stroke-width', '1');
e.setAttributeNS('', 'stroke', '#555');
e.setAttributeNS('', 'fill', 'none');
svg.appendChild(e);
}
// ### boxNote
// update the note geometry based on current viewbox conditions.
// This may not be the appropriate place for this...maybe in layout
static updateArtifactBox(context: SvgPage, element: ElementLike, artifact: Transposable) {
if (!element) {
console.log('updateArtifactBox: undefined element!');
return;
}
artifact.logicalBox = context.offsetBbox(element);
}
// ### eraseOutline
// Erases old outlineRects.
static eraseOutline(params: OutlineInfo) {
// Hack: Assume a stroke style, should just take a stroke param.
if (params.element) {
params.element.remove();
params.element = undefined;
}
}
static outlineRect(params: OutlineInfo) {
const context = params.context;
if (params.element && params.timer) {
clearTimeout(params.timer);
params.timer = undefined;
params.element.remove();
params.element = undefined;
}
if (params.timeOff) {
params.timer = window.setTimeout(() => {
if (params.element) {
params.element.remove();
params.element = undefined;
params.timer = undefined;
}
}, params.timeOff);
}
// Don't highlight in print mode.
if ($('body').hasClass('printing')) {
return;
}
const classes = params.classes.length > 0 ? params.classes + ' ' + params.stroke.strokeName : params.stroke.strokeName;
var grp = context.getContext().openGroup(classes, classes + '-outline');
params.element = grp;
const boxes = Array.isArray(params.box) ? params.box : [params.box];
boxes.forEach((box: SvgBox) => {
if (box) {
var strokeObj:any = params.stroke;
strokeObj['stroke-width'] = params.stroke.strokeWidth;
var margin = 5;
/* if (params.clientCoordinates === true) {
box = SvgHelpers.smoBox(SvgHelpers.clientToLogical(context.svg, SvgHelpers.smoBox(SvgHelpers.adjustScroll(box, scroll))));
} */
context.getContext().rect(box.x - margin, box.y - margin, box.width + margin * 2, box.height + margin * 2, strokeObj);
}
});
context.getContext().closeGroup();
}
static setSvgStyle(element: Element, attrs: StrokeInfo) {
element.setAttributeNS('', 'stroke', attrs.stroke);
if (attrs.strokeDasharray) {
element.setAttributeNS('', 'stroke-dasharray', attrs.strokeDasharray.toString());
}
if (attrs.strokeWidth) {
element.setAttributeNS('', 'stroke-width', attrs.strokeWidth.toString());
}
if (attrs.fill) {
element.setAttributeNS('', 'fill', attrs.fill);
}
}
static rect(svg: Document, box: SvgBox, attrs: StrokeInfo, classes: string) {
var rect = document.createElementNS(SvgHelpers.namespace, 'rect');
SvgHelpers.setSvgStyle(rect, attrs);
if (classes) {
rect.setAttributeNS('', 'class', classes);
}
svg.appendChild(rect);
return rect;
}
static line(svg: SVGSVGElement, x1: number | string, y1: number | string, x2: number | string, y2: number | string, attrs: StrokeInfo, classes: string) {
var line = document.createElementNS(SvgHelpers.namespace, 'line');
x1 = typeof (x1) == 'string' ? x1 : x1.toString();
y1 = typeof (y1) == 'string' ? y1 : y1.toString();
x2 = typeof (x2) == 'string' ? x2 : x2.toString();
y2 = typeof (y2) == 'string' ? y2 : y2.toString();
line.setAttributeNS('', 'x1', x1);
line.setAttributeNS('', 'y1', y1);
line.setAttributeNS('', 'x2', x2);
line.setAttributeNS('', 'y2', y2);
SvgHelpers.setSvgStyle(line, attrs);
if (classes) {
line.setAttributeNS('', 'class', classes);
}
svg.appendChild(line);
}
static arrowDown(svg: SVGSVGElement, box: SvgBox) {
const arrowStroke: StrokeInfo = { strokeName: 'arrow-stroke', stroke: '#321', strokeWidth: '2', strokeDasharray: '4,1', fill: 'none', opacity: 1.0 };
SvgHelpers.line(svg, box.x + box.width / 2, box.y, box.x + box.width / 2, box.y + box.height, arrowStroke, '');
var arrowY = box.y + box.height / 4;
SvgHelpers.line(svg, box.x, arrowY, box.x + box.width / 2, box.y + box.height, arrowStroke, '');
SvgHelpers.line(svg, box.x + box.width, arrowY, box.x + box.width / 2, box.y + box.height, arrowStroke, '');
}
static debugBox(svg: SVGSVGElement, box: SvgBox | null, classes: string, voffset: number) {
voffset = voffset ?? 0;
classes = classes ?? '';
if (!box)
return;
classes += ' svg-debug-box';
var b = SvgBuilder.b;
var mid = box.x + box.width / 2;
var xtext = 'x1: ' + Math.round(box.x);
var wtext = 'x2: ' + Math.round(box.width + box.x);
var ytext = 'y1: ' + Math.round(box.y);
var htext = 'y2: ' + Math.round(box.height + box.y);
var ytextp = Math.round(box.y + box.height);
var ytextp2 = Math.round(box.y + box.height - 30);
var r = b('g').classes(classes)
.append(
b('text').text(box.x + 20, box.y - 14 + voffset, 'svg-debug-text', xtext))
.append(
b('text').text(mid - 20, box.y - 14 + voffset, 'svg-debug-text', wtext))
.append(
b('line').line(box.x, box.y - 2, box.x + box.width, box.y - 2, ''))
.append(
b('line').line(box.x, box.y - 8, box.x, box.y + 5, ''))
.append(
b('line').line(box.x + box.width, box.y - 8, box.x + box.width, box.y + 5, ''))
.append(
b('text').text(Math.round(box.x - 14 + voffset), ytextp, 'svg-vdebug-text', ytext)
.attr('transform', 'rotate(-90,' + Math.round(box.x - 14 + voffset) + ',' + ytextp + ')'));
if (box.height > 2) {
r.append(
b('text').text(Math.round(box.x - 14 + voffset), ytextp2, 'svg-vdebug-text', htext)
.attr('transform', 'rotate(-90,' + Math.round(box.x - 14 + voffset) + ',' + (ytextp2) + ')'))
.append(
b('line').line(Math.round(box.x - 2), Math.round(box.y + box.height), box.x - 2, box.y, ''))
.append(
b('line').line(Math.round(box.x - 8), Math.round(box.y + box.height), box.x + 6, Math.round(box.y + box.height), ''))
.append(
b('line').line(Math.round(box.x - 8), Math.round(box.y), Math.round(box.x + 6), Math.round(box.y),''));
}
svg.appendChild(r.dom());
}
static debugBoxNoText(svg: SVGSVGElement, box: SvgBox | null, classes: string, voffset: number) {
voffset = voffset ?? 0;
classes = classes ?? '';
if (!box)
return;
classes += ' svg-debug-box';
var b = SvgBuilder.b;
var r = b('g').classes(classes)
.append(
b('line').line(box.x, box.y - 2, box.x + box.width, box.y - 2, ''))
.append(
b('line').line(box.x, box.y - 8, box.x, box.y + 5, ''))
.append(
b('line').line(box.x + box.width, box.y - 8, box.x + box.width, box.y + 5, ''));
if (box.height > 2) {
r.append(
b('line').line(Math.round(box.x - 2), Math.round(box.y + box.height), box.x - 2, box.y, ''))
.append(
b('line').line(Math.round(box.x - 8), Math.round(box.y + box.height), box.x + 6, Math.round(box.y + box.height), ''))
.append(
b('line').line(Math.round(box.x - 8), Math.round(box.y), Math.round(box.x + 6), Math.round(box.y),''));
}
svg.appendChild(r.dom());
}
static placeSvgText(svg: SVGSVGElement, attributes: Record<string | number, string | number>[], classes: string, text: string): SVGSVGElement {
var ns = SvgHelpers.namespace;
var e = document.createElementNS(ns, 'text');
attributes.forEach((attr) => {
var key: string = Object.keys(attr)[0];
e.setAttributeNS('', key, attr[key].toString());
})
if (classes) {
e.setAttributeNS('', 'class', classes);
}
var tn = document.createTextNode(text);
e.appendChild(tn);
svg.appendChild(e);
return (e as any);
}
static doesBox1ContainBox2(box1?: SvgBox, box2?: SvgBox): boolean {
if (!box1 || !box2) {
return false;
}
const i1 = box2.x - box1.x;
const i2 = box2.y - box1.y;
return (i1 > 0 && i1 < box1.width && i2 > 0 && i2 < box1.height);
}
// ### findIntersectionArtifact
// find all object that intersect with the rectangle
static findIntersectingArtifact(clientBox: SvgBox, objects: Boxable[]): Boxable[] {
var box = SvgHelpers.smoBox(clientBox); //svgHelpers.untransformSvgPoint(this.context.svg,clientBox);
// box.y = box.y - this.renderElement.offsetTop;
// box.x = box.x - this.renderElement.offsetLeft;
var rv: Boxable[] = [];
objects.forEach((object) => {
// Measure has been updated, but not drawn.
if (!object.box) {
// console.log('there is no box');
} else {
var obox = SvgHelpers.smoBox(object.box);
if (SvgHelpers.doesBox1ContainBox2(obox, box)) {
rv.push(object);
}
}
});
return rv;
}
static findSmallestIntersection(clientBox: SvgBox, objects: Boxable[]) {
var ar = SvgHelpers.findIntersectingArtifact(clientBox, objects);
if (!ar.length) {
return null;
}
var rv = ar[0];
var min = ar[0].box.width * ar[0].box.height;
ar.forEach((obj) => {
var tst = obj.box.width * obj.box.height;
if (tst < min) {
rv = obj;
min = tst;
}
});
return rv;
}
static translateElement(g: SVGSVGElement, x: number | string, y: number | string) {
g.setAttributeNS('', 'transform', 'translate(' + x + ' ' + y + ')');
}
static stringify(box: SvgBox): string {
if (box['width']) {
return JSON.stringify({
x: box.x,
y: box.y,
width: box.width,
height: box.height
}, null, ' ');
} else {
return JSON.stringify({
x: box.x,
y: box.y
}, null, ' ');
}
}
static log(box: SvgBox) {
if (box['width']) {
console.log(JSON.stringify({
x: box.x,
y: box.y,
width: box.width,
height: box.height
}, null, ' '));
} else {
console.log('{}');
}
}
// ### smoBox:
// return a simple box object that can be serialized, copied
// (from svg DOM box)
static smoBox(box: any) {
if (typeof (box) === "undefined" || box === null) {
return SvgBox.default;
}
let testBox = box;
if (Array.isArray(box)) {
testBox = box[0];
}
const hround = (f: number): number => {
return Math.round((f + Number.EPSILON) * 100) / 100;
}
const x = typeof (testBox.x) == 'undefined' ? hround(testBox.left) : hround(testBox.x);
const y = typeof (testBox.y) == 'undefined' ? hround(testBox.top) : hround(testBox.y);
return ({
x: hround(x),
y: hround(y),
width: hround(testBox.width),
height: hround(testBox.height)
});
}
// ### unionRect
// grow the bounding box two objects to include both.
static unionRect(b1: SvgBox, b2: SvgBox): SvgBox {
const x = Math.min(b1.x, b2.x);
const y = Math.min(b1.y, b2.y);
const width = Math.max(b1.x + b1.width, b2.x + b2.width) - x;
const height = Math.max(b1.y + b1.height, b2.y + b2.height) - y;
return {
x: x,
y: y,
width: width,
height: height
};
}
static boxPoints(x: number, y: number, w: number, h: number): SvgBox {
return ({
x: x,
y: y,
width: w,
height: h
});
}
// ### svgViewport
// set `svg` element to `width`,`height` and viewport `scale`
static svgViewport(svg: SVGSVGElement, xOffset: number, yOffset: Number, width: number, height: number, scale: number) {
svg.setAttributeNS('', 'width', '' + width);
svg.setAttributeNS('', 'height', '' + height);
svg.setAttributeNS('', 'viewBox', '' + xOffset + ' ' + yOffset + ' ' + Math.round(width / scale) + ' ' +
Math.round(height / scale));
}
static removeElementsByClass(svg: SVGSVGElement, className: string) {
const els = svg.getElementsByClassName(className);
const ellength = els.length
for (var xxx = 0; xxx < ellength; ++xxx) {
els[0].remove();
}
}
}