diff --git a/demo/client/src/svg-demo.ts b/demo/client/src/svg-demo.ts index 44b026b0..a8bfcba7 100644 --- a/demo/client/src/svg-demo.ts +++ b/demo/client/src/svg-demo.ts @@ -18,6 +18,7 @@ import {OrthographicCamera} from "./camera"; import {ECAAStrategy, ECAAMulticolorStrategy} from "./ecaa-strategy"; import {PathfinderMeshData} from "./meshes"; import {ShaderMap, ShaderProgramSource} from './shader-loader'; +import {SVGLoader} from './svg-loader'; import {panic, unwrapNull} from './utils'; import {PathfinderDemoView, Timings} from './view'; import SSAAStrategy from "./ssaa-strategy"; @@ -27,15 +28,10 @@ const parseColor = require('parse-color'); const SVG_NS: string = "http://www.w3.org/2000/svg"; -const PARTITION_SVG_PATHS_ENDPOINT_URL: string = "/partition-svg-paths"; - const BUILTIN_SVG_URI: string = "/svg/demo"; const DEFAULT_FILE: string = 'tiger'; -/// The minimum size of a stroke. -const HAIRLINE_STROKE_WIDTH: number = 0.25; - const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = { none: NoAAStrategy, ssaa: SSAAStrategy, @@ -59,28 +55,20 @@ interface AntialiasingStrategyTable { ecaa: typeof ECAAStrategy; } -interface PathInstance { - element: SVGPathElement; - stroke: number | 'fill'; -} - class SVGDemoController extends DemoAppController { start() { super.start(); - this.svg = document.getElementById('pf-svg') as Element as SVGSVGElement; - - this.pathInstances = []; + this.loader = new SVGLoader; this.loadInitialFile(); } 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); + this.loader.loadFile(this.fileData).then(meshes => { + this.meshes = meshes; + this.meshesReceived(); + }) } protected createView() { @@ -89,97 +77,6 @@ class SVGDemoController extends DemoAppController { unwrapNull(this.shaderSources)); } - 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.pathInstances.length = 0; - const queue: Array = [this.svg]; - let element; - while ((element = queue.pop()) != null) { - let kid = element.lastChild; - while (kid != null) { - if (kid instanceof Element) - queue.push(kid); - kid = kid.previousSibling; - } - - if (element instanceof SVGPathElement) { - const style = window.getComputedStyle(element); - if (style.fill !== 'none') - this.pathInstances.push({ element: element, stroke: 'fill' }); - if (style.stroke !== 'none') { - this.pathInstances.push({ - element: element, - stroke: parseInt(style.strokeWidth!), - }); - } - } - } - - const request: any = { paths: [] }; - let minX = 0, minY = 0, maxX = 0, maxY = 0; - - // Extract, normalize, and transform the path data. - for (const instance of this.pathInstances) { - const element = instance.element; - 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]); - - const segments = 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); - - minX = Math.min(point[0], minX); - minY = Math.min(point[1], minY); - maxX = Math.max(point[0], maxX); - maxY = Math.max(point[1], maxY); - - return [point[0], point[1]]; - }); - return { - type: segment.type, - values: newValues, - }; - }); - - let kind; - if (instance.stroke === 'fill') - kind = 'Fill'; - else - kind = { Stroke: Math.max(HAIRLINE_STROKE_WIDTH, instance.stroke) }; - - request.paths.push({ segments: segments, kind: kind }); - } - - const bounds = glmatrix.vec4.clone([minX, minY, maxX, maxY]); - - // 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 => { - 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(bounds); - }); - } - protected get builtinFileURI(): string { return BUILTIN_SVG_URI; } @@ -188,20 +85,19 @@ class SVGDemoController extends DemoAppController { return DEFAULT_FILE; } - private meshesReceived(bounds: glmatrix.vec4): void { + private meshesReceived(): void { this.view.then(view => { view.uploadPathColors(1); view.uploadPathTransforms(1); view.attachMeshes([this.meshes]); - view.camera.bounds = bounds; + view.camera.bounds = this.loader.bounds; view.camera.zoomToFit(); }) } - pathInstances: PathInstance[]; + loader: SVGLoader; - private svg: SVGSVGElement; private meshes: PathfinderMeshData; } @@ -231,7 +127,7 @@ class SVGDemoView extends PathfinderDemoView { } protected pathColorsForObject(objectIndex: number): Uint8Array { - const instances = this.appController.pathInstances; + const instances = this.appController.loader.pathInstances; const pathColors = new Uint8Array(4 * (instances.length + 1)); for (let pathIndex = 0; pathIndex < instances.length; pathIndex++) { @@ -250,7 +146,7 @@ class SVGDemoView extends PathfinderDemoView { } protected pathTransformsForObject(objectIndex: number): Float32Array { - const instances = this.appController.pathInstances; + const instances = this.appController.loader.pathInstances; const pathTransforms = new Float32Array(4 * (instances.length + 1)); for (let pathIndex = 0; pathIndex < instances.length; pathIndex++) { diff --git a/demo/client/src/svg-loader.ts b/demo/client/src/svg-loader.ts new file mode 100644 index 00000000..b572b144 --- /dev/null +++ b/demo/client/src/svg-loader.ts @@ -0,0 +1,139 @@ +// pathfinder/client/src/svg-loader.ts +// +// Copyright © 2017 The Pathfinder Project Developers. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +import * as glmatrix from 'gl-matrix'; +import * as _ from 'lodash'; + +import {panic, unwrapNull} from "./utils"; +import {PathfinderMeshData} from "./meshes"; + +const PARTITION_SVG_PATHS_ENDPOINT_URL: string = "/partition-svg-paths"; + +/// The minimum size of a stroke. +const HAIRLINE_STROKE_WIDTH: number = 0.25; + +export interface PathInstance { + element: SVGPathElement; + stroke: number | 'fill'; +} + +export class SVGLoader { + constructor() { + this.svg = unwrapNull(document.getElementById('pf-svg')) as Element as SVGSVGElement; + this.pathInstances = []; + this.bounds = glmatrix.vec4.create(); + } + + loadFile(fileData: ArrayBuffer): Promise { + this.fileData = fileData; + + 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; + return this.attachSVG(svgElement); + } + + private attachSVG(svgElement: SVGSVGElement): Promise { + // 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.pathInstances.length = 0; + const queue: Array = [this.svg]; + let element; + while ((element = queue.pop()) != null) { + let kid = element.lastChild; + while (kid != null) { + if (kid instanceof Element) + queue.push(kid); + kid = kid.previousSibling; + } + + if (element instanceof SVGPathElement) { + const style = window.getComputedStyle(element); + if (style.fill !== 'none') + this.pathInstances.push({ element: element, stroke: 'fill' }); + if (style.stroke !== 'none') { + this.pathInstances.push({ + element: element, + stroke: parseInt(style.strokeWidth!), + }); + } + } + } + + const request: any = { paths: [] }; + let minX = 0, minY = 0, maxX = 0, maxY = 0; + + // Extract, normalize, and transform the path data. + for (const instance of this.pathInstances) { + const element = instance.element; + 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]); + + const segments = 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); + + minX = Math.min(point[0], minX); + minY = Math.min(point[1], minY); + maxX = Math.max(point[0], maxX); + maxY = Math.max(point[1], maxY); + + return [point[0], point[1]]; + }); + return { + type: segment.type, + values: newValues, + }; + }); + + let kind; + if (instance.stroke === 'fill') + kind = 'Fill'; + else + kind = { Stroke: Math.max(HAIRLINE_STROKE_WIDTH, instance.stroke) }; + + request.paths.push({ segments: segments, kind: kind }); + } + + this.bounds = glmatrix.vec4.clone([minX, minY, maxX, maxY]); + + // Make the request. + return window.fetch(PARTITION_SVG_PATHS_ENDPOINT_URL, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(request), + }).then(response => response.text()).then(responseText => { + const response = JSON.parse(responseText); + if (!('Ok' in response)) + panic("Failed to partition the font!"); + const meshes = response.Ok.pathData; + return new PathfinderMeshData(meshes); + }); + } + + private svg: SVGSVGElement; + private fileData: ArrayBuffer; + + pathInstances: PathInstance[]; + bounds: glmatrix.vec4; +}