2017-08-25 23:20:45 -04:00
|
|
|
// pathfinder/client/src/svg.ts
|
|
|
|
//
|
|
|
|
// Copyright © 2017 The Pathfinder Project Developers.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
|
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
|
|
// option. This file may not be copied, modified, or distributed
|
|
|
|
// except according to those terms.
|
2017-08-27 15:43:17 -04:00
|
|
|
|
2017-08-28 19:47:27 -04:00
|
|
|
import * as glmatrix from 'gl-matrix';
|
2017-08-29 17:15:23 -04:00
|
|
|
import * as _ from 'lodash';
|
2017-08-28 19:47:27 -04:00
|
|
|
import 'path-data-polyfill.js';
|
|
|
|
|
2017-08-28 20:18:44 -04:00
|
|
|
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy";
|
|
|
|
import {ECAAStrategy, ECAAMulticolorStrategy} from "./ecaa-strategy";
|
|
|
|
import {PathfinderMeshData} from "./meshes";
|
2017-08-27 15:43:17 -04:00
|
|
|
import {ShaderMap, ShaderProgramSource} from './shader-loader';
|
|
|
|
import {panic} from './utils';
|
2017-08-28 20:18:44 -04:00
|
|
|
import {PathfinderView, Timings} from './view';
|
2017-08-27 15:43:17 -04:00
|
|
|
import AppController from './app-controller';
|
2017-08-28 20:18:44 -04:00
|
|
|
import SSAAStrategy from "./ssaa-strategy";
|
2017-08-27 15:43:17 -04:00
|
|
|
|
2017-08-29 19:04:40 -04:00
|
|
|
require('../html/svg.html');
|
|
|
|
|
2017-08-29 01:11:15 -04:00
|
|
|
const parseColor = require('parse-color');
|
|
|
|
|
2017-08-28 19:47:27 -04:00
|
|
|
const SVG_NS: string = "http://www.w3.org/2000/svg";
|
|
|
|
|
|
|
|
const PARTITION_SVG_PATHS_ENDPOINT_URL: string = "/partition-svg-paths";
|
|
|
|
|
2017-08-28 20:18:44 -04:00
|
|
|
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
|
|
|
|
none: NoAAStrategy,
|
|
|
|
ssaa: SSAAStrategy,
|
|
|
|
ecaa: ECAAMulticolorStrategy,
|
|
|
|
};
|
|
|
|
|
2017-08-28 19:47:27 -04:00
|
|
|
declare class SVGPathSegment {
|
|
|
|
type: string;
|
|
|
|
values: number[];
|
|
|
|
}
|
|
|
|
|
2017-08-29 01:11:15 -04:00
|
|
|
declare global {
|
|
|
|
interface SVGPathElement {
|
|
|
|
getPathData(settings: any): SVGPathSegment[];
|
|
|
|
}
|
2017-08-28 19:47:27 -04:00
|
|
|
}
|
|
|
|
|
2017-08-28 20:18:44 -04:00
|
|
|
interface AntialiasingStrategyTable {
|
|
|
|
none: typeof NoAAStrategy;
|
|
|
|
ssaa: typeof SSAAStrategy;
|
|
|
|
ecaa: typeof ECAAStrategy;
|
|
|
|
}
|
|
|
|
|
2017-08-27 15:43:17 -04:00
|
|
|
class SVGDemoController extends AppController<SVGDemoView> {
|
2017-08-28 19:47:27 -04:00
|
|
|
start() {
|
|
|
|
super.start();
|
|
|
|
|
|
|
|
this.svg = document.getElementById('pf-svg') as Element as SVGSVGElement;
|
|
|
|
|
|
|
|
this.pathElements = [];
|
|
|
|
|
|
|
|
this.loadFileButton = document.getElementById('pf-load-svg-button') as HTMLInputElement;
|
|
|
|
this.loadFileButton.addEventListener('change', () => this.loadFile(), false);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected fileLoaded() {
|
|
|
|
const decoder = new (window as any).TextDecoder('utf-8');
|
|
|
|
const fileStringData = decoder.decode(new DataView(this.fileData));
|
|
|
|
const svgDocument = (new DOMParser).parseFromString(fileStringData, 'image/svg+xml');
|
|
|
|
const svgElement = svgDocument.documentElement as Element as SVGSVGElement;
|
|
|
|
this.attachSVG(svgElement);
|
|
|
|
}
|
|
|
|
|
2017-08-27 15:43:17 -04:00
|
|
|
protected createView(canvas: HTMLCanvasElement,
|
|
|
|
commonShaderSource: string,
|
|
|
|
shaderSources: ShaderMap<ShaderProgramSource>) {
|
2017-08-28 19:47:27 -04:00
|
|
|
return new SVGDemoView(this, canvas, commonShaderSource, shaderSources);
|
2017-08-27 15:43:17 -04:00
|
|
|
}
|
2017-08-28 19:47:27 -04:00
|
|
|
|
|
|
|
private attachSVG(svgElement: SVGSVGElement) {
|
|
|
|
// Clear out the current document.
|
|
|
|
let kid;
|
|
|
|
while ((kid = this.svg.firstChild) != null)
|
|
|
|
this.svg.removeChild(kid);
|
|
|
|
|
|
|
|
// Add all kids of the incoming SVG document.
|
|
|
|
while ((kid = svgElement.firstChild) != null)
|
|
|
|
this.svg.appendChild(kid);
|
|
|
|
|
|
|
|
// Scan for geometry elements.
|
|
|
|
this.pathElements.length = 0;
|
|
|
|
const queue: Array<Element> = [this.svg];
|
|
|
|
let element;
|
|
|
|
while ((element = queue.pop()) != null) {
|
2017-08-29 17:15:23 -04:00
|
|
|
let kid = element.lastChild;
|
|
|
|
while (kid != null) {
|
2017-08-28 19:47:27 -04:00
|
|
|
if (kid instanceof Element)
|
|
|
|
queue.push(kid);
|
2017-08-29 17:15:23 -04:00
|
|
|
kid = kid.previousSibling;
|
2017-08-28 19:47:27 -04:00
|
|
|
}
|
2017-08-29 17:15:23 -04:00
|
|
|
|
2017-08-28 19:47:27 -04:00
|
|
|
if (element instanceof SVGPathElement)
|
|
|
|
this.pathElements.push(element);
|
|
|
|
}
|
|
|
|
|
2017-08-29 17:15:23 -04:00
|
|
|
// Extract, normalize, and transform the path data.
|
|
|
|
let pathData = [];
|
|
|
|
for (const element of this.pathElements) {
|
|
|
|
const svgCTM = element.getCTM();
|
|
|
|
const ctm = glmatrix.mat2d.fromValues(svgCTM.a, svgCTM.b,
|
|
|
|
svgCTM.c, svgCTM.d,
|
|
|
|
svgCTM.e, svgCTM.f);
|
|
|
|
glmatrix.mat2d.scale(ctm, ctm, [1.0, -1.0]);
|
|
|
|
|
|
|
|
pathData.push(element.getPathData({normalize: true}).map(segment => {
|
|
|
|
const newValues = _.flatMap(_.chunk(segment.values, 2), coords => {
|
|
|
|
const point = glmatrix.vec2.create();
|
|
|
|
glmatrix.vec2.transformMat2d(point, coords, ctm);
|
|
|
|
return [point[0], point[1]];
|
|
|
|
});
|
|
|
|
return {
|
|
|
|
type: segment.type,
|
|
|
|
values: newValues,
|
|
|
|
};
|
|
|
|
}));
|
|
|
|
}
|
2017-08-28 19:47:27 -04:00
|
|
|
|
|
|
|
// Build the partitioning request to the server.
|
|
|
|
const request = {paths: pathData.map(segments => ({segments: segments}))};
|
|
|
|
|
|
|
|
// Make the request.
|
|
|
|
window.fetch(PARTITION_SVG_PATHS_ENDPOINT_URL, {
|
|
|
|
method: 'POST',
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
|
|
|
body: JSON.stringify(request),
|
|
|
|
}).then(response => response.text()).then(responseText => {
|
2017-08-28 20:18:44 -04:00
|
|
|
const response = JSON.parse(responseText);
|
|
|
|
if (!('Ok' in response))
|
|
|
|
panic("Failed to partition the font!");
|
|
|
|
const meshes = response.Ok.pathData;
|
|
|
|
this.meshes = new PathfinderMeshData(meshes);
|
|
|
|
this.meshesReceived();
|
2017-08-28 19:47:27 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-08-28 20:18:44 -04:00
|
|
|
private meshesReceived() {
|
|
|
|
this.view.then(view => {
|
2017-08-29 01:11:15 -04:00
|
|
|
view.uploadPathData(this.pathElements);
|
2017-08-28 20:18:44 -04:00
|
|
|
view.attachMeshes(this.meshes);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-08-28 19:47:27 -04:00
|
|
|
private svg: SVGSVGElement;
|
|
|
|
private pathElements: Array<SVGPathElement>;
|
2017-08-28 20:18:44 -04:00
|
|
|
private meshes: PathfinderMeshData;
|
2017-08-27 15:43:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
class SVGDemoView extends PathfinderView {
|
|
|
|
constructor(appController: SVGDemoController,
|
|
|
|
canvas: HTMLCanvasElement,
|
|
|
|
commonShaderSource: string,
|
|
|
|
shaderSources: ShaderMap<ShaderProgramSource>) {
|
|
|
|
super(canvas, commonShaderSource, shaderSources);
|
|
|
|
|
2017-08-28 19:47:27 -04:00
|
|
|
this.appController = appController;
|
|
|
|
|
2017-08-29 01:11:15 -04:00
|
|
|
this._scale = 1.0;
|
2017-08-27 15:43:17 -04:00
|
|
|
}
|
|
|
|
|
2017-08-29 15:29:16 -04:00
|
|
|
protected resized(initialSize: boolean) {
|
|
|
|
this.antialiasingStrategy.init(this);
|
|
|
|
this.setDirty();
|
|
|
|
}
|
2017-08-28 19:47:27 -04:00
|
|
|
|
|
|
|
get destAllocatedSize(): glmatrix.vec2 {
|
|
|
|
return glmatrix.vec2.fromValues(this.canvas.width, this.canvas.height);
|
2017-08-27 15:43:17 -04:00
|
|
|
}
|
|
|
|
|
2017-08-29 01:11:15 -04:00
|
|
|
get destFramebuffer(): WebGLFramebuffer | null {
|
|
|
|
return null;
|
2017-08-27 15:43:17 -04:00
|
|
|
}
|
|
|
|
|
2017-08-28 19:47:27 -04:00
|
|
|
get destUsedSize(): glmatrix.vec2 {
|
|
|
|
return this.destAllocatedSize;
|
2017-08-27 15:43:17 -04:00
|
|
|
}
|
|
|
|
|
2017-08-29 15:29:16 -04:00
|
|
|
protected panned(): void {
|
|
|
|
this.setDirty();
|
|
|
|
}
|
2017-08-29 01:11:15 -04:00
|
|
|
|
|
|
|
uploadPathData(elements: SVGPathElement[]) {
|
|
|
|
const pathColors = new Uint8Array(4 * (elements.length + 1));
|
2017-08-29 15:29:16 -04:00
|
|
|
const pathTransforms = new Float32Array(4 * (elements.length + 1));
|
2017-08-29 01:11:15 -04:00
|
|
|
for (let pathIndex = 0; pathIndex < elements.length; pathIndex++) {
|
2017-08-29 15:29:16 -04:00
|
|
|
const startOffset = (pathIndex + 1) * 4;
|
|
|
|
|
|
|
|
// Set color.
|
2017-08-29 01:11:15 -04:00
|
|
|
const style = window.getComputedStyle(elements[pathIndex]);
|
2017-08-29 15:29:16 -04:00
|
|
|
const fillColor: number[] =
|
|
|
|
style.fill === 'none' ? [0, 0, 0, 0] : parseColor(style.fill).rgba;
|
|
|
|
pathColors.set(fillColor.slice(0, 3), startOffset);
|
|
|
|
pathColors[startOffset + 3] = fillColor[3] * 255;
|
|
|
|
|
|
|
|
// TODO(pcwalton): Set transform.
|
|
|
|
pathTransforms.set([1, 1, 0, 0], startOffset);
|
2017-08-29 01:11:15 -04:00
|
|
|
}
|
2017-08-27 15:43:17 -04:00
|
|
|
|
2017-08-29 01:11:15 -04:00
|
|
|
this.pathColorsBufferTexture.upload(this.gl, pathColors);
|
2017-08-29 15:29:16 -04:00
|
|
|
this.pathTransformBufferTexture.upload(this.gl, pathTransforms);
|
2017-08-27 15:43:17 -04:00
|
|
|
}
|
|
|
|
|
2017-08-28 20:18:44 -04:00
|
|
|
protected createAAStrategy(aaType: AntialiasingStrategyName, aaLevel: number):
|
|
|
|
AntialiasingStrategy {
|
|
|
|
return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected compositeIfNecessary(): void {}
|
|
|
|
|
|
|
|
protected updateTimings(timings: Timings) {
|
|
|
|
// TODO(pcwalton)
|
|
|
|
}
|
|
|
|
|
2017-08-29 01:11:15 -04:00
|
|
|
protected get usedSizeFactor(): glmatrix.vec2 {
|
2017-08-29 15:29:16 -04:00
|
|
|
return glmatrix.vec2.fromValues(1.0, 1.0);
|
2017-08-29 01:11:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
protected get scale(): number {
|
|
|
|
return this._scale;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected set scale(newScale: number) {
|
|
|
|
this._scale = newScale;
|
|
|
|
this.setDirty();
|
|
|
|
}
|
|
|
|
|
2017-08-29 15:29:16 -04:00
|
|
|
protected get worldTransform() {
|
|
|
|
const transform = glmatrix.mat4.create();
|
|
|
|
glmatrix.mat4.fromTranslation(transform, [this.translation[0], this.translation[1], 0]);
|
|
|
|
glmatrix.mat4.scale(transform, transform, [this.scale, this.scale, 1.0]);
|
|
|
|
return transform;
|
|
|
|
}
|
|
|
|
|
2017-08-29 01:11:15 -04:00
|
|
|
private _scale: number;
|
|
|
|
|
2017-08-28 19:47:27 -04:00
|
|
|
private appController: SVGDemoController;
|
2017-08-27 15:43:17 -04:00
|
|
|
}
|
2017-08-28 19:47:27 -04:00
|
|
|
|
|
|
|
function main() {
|
|
|
|
const controller = new SVGDemoController;
|
|
|
|
window.addEventListener('load', () => controller.start(), false);
|
|
|
|
}
|
|
|
|
|
|
|
|
main();
|