diff --git a/demo2/opaque.fs.glsl b/demo2/opaque.fs.glsl new file mode 100644 index 00000000..fe579b29 --- /dev/null +++ b/demo2/opaque.fs.glsl @@ -0,0 +1,21 @@ +#version 300 es + +// pathfinder/demo2/opaque.fs.glsl +// +// Copyright © 2018 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. + +precision highp float; + +in vec4 vColor; + +out vec4 oFragColor; + +void main() { + oFragColor = vColor; +} diff --git a/demo2/opaque.vs.glsl b/demo2/opaque.vs.glsl new file mode 100644 index 00000000..08e4f796 --- /dev/null +++ b/demo2/opaque.vs.glsl @@ -0,0 +1,29 @@ +#version 300 es + +// pathfinder/demo2/opaque.vs.glsl +// +// Copyright © 2018 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. + +precision highp float; + +uniform vec2 uFramebufferSize; +uniform vec2 uTileSize; + +in vec2 aTessCoord; +in vec2 aTileOrigin; +in vec4 aColor; + +out vec4 vColor; + +void main() { + vec2 position = aTileOrigin + uTileSize * aTessCoord; + vColor = aColor; + gl_Position = vec4((position / uFramebufferSize * 2.0 - 1.0) * vec2(1.0, -1.0), 0.0, 1.0); +} + diff --git a/demo2/package-lock.json b/demo2/package-lock.json index bc1fd1f7..f3548852 100644 --- a/demo2/package-lock.json +++ b/demo2/package-lock.json @@ -11,11 +11,44 @@ "commander": "^2.15.1" } }, + "@types/bezier-js": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/bezier-js/-/bezier-js-0.0.6.tgz", + "integrity": "sha1-DZdtaBY8SVUzLveYohoLPWOB0Ss=" + }, + "@types/node": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.2.tgz", + "integrity": "sha512-RO4ig5taKmcrU4Rex8ojG1gpwFkjddzug9iPQSDvbewHN9vDpcFewevkaOK+KT+w1LeZnxbgOyfXwV4pxsQ4GQ==" + }, + "@types/opentype.js": { + "version": "0.0.0", + "resolved": "http://registry.npmjs.org/@types/opentype.js/-/opentype.js-0.0.0.tgz", + "integrity": "sha1-AvZD18Y8Pr9PZG/GBa5Gg/hJrxs=" + }, + "@types/pdfkit": { + "version": "0.7.36", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.7.36.tgz", + "integrity": "sha512-9eRA6MuW+n78yU3HhoIrDxjyAX2++B5MpLDYqHOnaRTquCw+5sYXT+QN8E1eSaxvNUwlRfU3tOm4UzTeGWmBqg==", + "requires": { + "@types/node": "*" + } + }, "@types/webgl2": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.4.tgz", "integrity": "sha512-PACt1xdErJbMUOUweSrbVM7gSIYm1vTncW2hF6Os/EeWi6TXYAYMPp+8v6rzHmypE5gHrxaxZNXgMkJVIdZpHw==" }, + "bezier-js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-2.3.1.tgz", + "integrity": "sha512-nFpFL9tuayvlHfWh6xM7OHeTZvwr74+6KnzO3eNZMt0BC0cqb9lCTc9C8OVzrHBvbrNwriTw7XaF2SBsWQJLZA==" + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + }, "color-convert": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", @@ -180,6 +213,11 @@ "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", "dev": true }, + "graham_scan": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/graham_scan/-/graham_scan-1.0.4.tgz", + "integrity": "sha1-OZZR3R+DU+GID1nqjl+Uud9Mkoo=" + }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", @@ -192,6 +230,20 @@ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", "dev": true }, + "makerjs": { + "version": "0.9.93", + "resolved": "https://registry.npmjs.org/makerjs/-/makerjs-0.9.93.tgz", + "integrity": "sha512-XztS2tJEpL01dsZ5P11kGO2MitrD6MsSvkNDS3uUDVN9SVDJn33Q2j9DFruJsiRtqNVRseLK52vkkbWKtLB/tA==", + "requires": { + "@types/bezier-js": "^0.0.6", + "@types/node": "^7.0.5", + "@types/opentype.js": "^0.0.0", + "@types/pdfkit": "^0.7.34", + "bezier-js": "^2.1.0", + "clone": "^1.0.2", + "graham_scan": "^1.0.4" + } + }, "map-limit": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/map-limit/-/map-limit-0.0.1.tgz", @@ -263,6 +315,14 @@ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true }, + "svg-path-outline": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/svg-path-outline/-/svg-path-outline-1.0.1.tgz", + "integrity": "sha1-w5Zk922IdGW4voXER3jIg0Jomas=", + "requires": { + "makerjs": "^0.9.39" + } + }, "svgpath": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/svgpath/-/svgpath-2.2.1.tgz", diff --git a/demo2/package.json b/demo2/package.json index de05fd5b..3147a42f 100644 --- a/demo2/package.json +++ b/demo2/package.json @@ -7,6 +7,7 @@ "dependencies": { "@types/webgl2": "0.0.4", "parse-color": "^1.0.0", + "svg-path-outline": "^1.0.1", "svgpath": "^2.2.1" } } diff --git a/demo2/pathfinder.ts b/demo2/pathfinder.ts index 95e04c1a..69fc990c 100644 --- a/demo2/pathfinder.ts +++ b/demo2/pathfinder.ts @@ -10,6 +10,8 @@ import COVER_VERTEX_SHADER_SOURCE from "./cover.vs.glsl"; import COVER_FRAGMENT_SHADER_SOURCE from "./cover.fs.glsl"; +import OPAQUE_VERTEX_SHADER_SOURCE from "./opaque.vs.glsl"; +import OPAQUE_FRAGMENT_SHADER_SOURCE from "./opaque.fs.glsl"; import STENCIL_VERTEX_SHADER_SOURCE from "./stencil.vs.glsl"; import STENCIL_FRAGMENT_SHADER_SOURCE from "./stencil.fs.glsl"; import SVG from "../resources/svg/Ghostscript_Tiger.svg"; @@ -20,6 +22,7 @@ import {staticCast, unwrapNull} from "./util"; const SVGPath: (path: string) => SVGPath = require('svgpath'); const parseColor: (color: string) => any = require('parse-color'); +const svgPathOutline: any = require('svg-path-outline'); const SVG_NS: string = "http://www.w3.org/2000/svg"; @@ -58,6 +61,8 @@ class App { private stencilFramebuffer: WebGLFramebuffer; private stencilProgram: Program<'FramebufferSize' | 'TileSize' | 'AreaLUT', 'TessCoord' | 'From' | 'Ctrl' | 'To' | 'TileIndex'>; + private opaqueProgram: Program<'FramebufferSize' | 'TileSize', + 'TessCoord' | 'TileOrigin' | 'Color'>; private coverProgram: Program<'FramebufferSize' | 'TileSize' | 'StencilTexture' | 'StencilTextureSize', 'TessCoord' | 'TileOrigin' | 'TileIndex' | 'Color'>; @@ -65,12 +70,15 @@ class App { private stencilVertexPositionsBuffer: WebGLBuffer; private stencilVertexTileIndicesBuffer: WebGLBuffer; private stencilVertexArray: WebGLVertexArrayObject; + private opaqueVertexBuffer: WebGLBuffer; + private opaqueVertexArray: WebGLVertexArrayObject; private coverVertexBuffer: WebGLBuffer; private coverVertexArray: WebGLVertexArrayObject; private scene: Scene | null; private primitiveCount: number; private tileCount: number; + private opaqueTileCount: number; constructor(svg: XMLDocument, areaLUT: HTMLImageElement) { const canvas = staticCast(document.getElementById('canvas'), HTMLCanvasElement); @@ -135,6 +143,13 @@ class App { ['TessCoord', 'TileOrigin', 'TileIndex', 'Color']); this.coverProgram = coverProgram; + const opaqueProgram = new Program(gl, + OPAQUE_VERTEX_SHADER_SOURCE, + OPAQUE_FRAGMENT_SHADER_SOURCE, + ['FramebufferSize', 'TileSize'], + ['TessCoord', 'TileOrigin', 'Color']); + this.opaqueProgram = opaqueProgram; + const stencilProgram = new Program(gl, STENCIL_VERTEX_SHADER_SOURCE, STENCIL_FRAGMENT_SHADER_SOURCE, @@ -182,6 +197,29 @@ class App { gl.enableVertexAttribArray(stencilProgram.attributes.To); gl.enableVertexAttribArray(stencilProgram.attributes.TileIndex); + // Initialize opaque VBO. + this.opaqueVertexBuffer = unwrapNull(gl.createBuffer()); + + // Initialize opaque VAO. + this.opaqueVertexArray = unwrapNull(gl.createVertexArray()); + gl.bindVertexArray(this.opaqueVertexArray); + gl.useProgram(this.opaqueProgram.program); + gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVertexBuffer); + gl.vertexAttribPointer(opaqueProgram.attributes.TessCoord, + 2, + gl.UNSIGNED_BYTE, + false, + 0, + 0); + gl.bindBuffer(gl.ARRAY_BUFFER, this.opaqueVertexBuffer); + gl.vertexAttribPointer(opaqueProgram.attributes.TileOrigin, 2, gl.SHORT, false, 10, 0); + gl.vertexAttribDivisor(opaqueProgram.attributes.TileOrigin, 1); + gl.vertexAttribPointer(opaqueProgram.attributes.Color, 4, gl.UNSIGNED_BYTE, true, 10, 6); + gl.vertexAttribDivisor(opaqueProgram.attributes.Color, 1); + gl.enableVertexAttribArray(opaqueProgram.attributes.TessCoord); + gl.enableVertexAttribArray(opaqueProgram.attributes.TileOrigin); + gl.enableVertexAttribArray(opaqueProgram.attributes.Color); + // Initialize cover VBO. this.coverVertexBuffer = unwrapNull(gl.createBuffer()); @@ -214,6 +252,7 @@ class App { this.scene = null; this.primitiveCount = 0; this.tileCount = 0; + this.opaqueTileCount = 0; } redraw(): void { @@ -276,13 +315,23 @@ class App { //console.log(stencilData); */ - // Cover. + // Draw opaque tiles. gl.bindFramebuffer(gl.FRAMEBUFFER, null); const framebufferSize = {width: canvas.width, height: canvas.height}; gl.viewport(0, 0, framebufferSize.width, framebufferSize.height); gl.clearColor(1.0, 1.0, 1.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); + gl.bindVertexArray(this.opaqueVertexArray); + gl.useProgram(this.opaqueProgram.program); + gl.uniform2f(this.opaqueProgram.uniforms.FramebufferSize, + framebufferSize.width, + framebufferSize.height); + gl.uniform2f(this.opaqueProgram.uniforms.TileSize, TILE_SIZE.width, TILE_SIZE.height); + gl.disable(gl.BLEND); + gl.drawArraysInstanced(gl.TRIANGLE_FAN, 0, 4, this.opaqueTileCount); + + // Cover. gl.bindVertexArray(this.coverVertexArray); gl.useProgram(this.coverProgram.program); gl.uniform2f(this.coverProgram.uniforms.FramebufferSize, @@ -315,6 +364,35 @@ class App { prepare(): void { const gl = this.gl, scene = unwrapNull(this.scene); + // Construct opaque tile VBOs. + this.opaqueTileCount = 0; + const opaqueVertexData: number[] = []; + const opaqueTiles: number[][] = []; + for (let pathIndex = scene.pathTileStrips.length - 1; pathIndex >= 0; pathIndex--) { + const pathTileStrips = scene.pathTileStrips[pathIndex]; + for (const tileStrip of pathTileStrips) { + for (const tile of tileStrip.tiles) { + // TODO(pcwalton) + const color = scene.pathColors[pathIndex]; + if (!tile.isFilled()) + continue; + + if (opaqueTiles[tile.tileLeft] == null) + opaqueTiles[tile.tileLeft] = []; + if (opaqueTiles[tile.tileLeft][tileStrip.tileTop] != null) + continue; + opaqueTiles[tile.tileLeft][tileStrip.tileTop] = pathIndex; + + opaqueVertexData.push(Math.floor(tile.tileLeft), + Math.floor(tileStrip.tileTop), + 0, + color.r | (color.g << 8), + color.b | (color.a << 8)); + this.opaqueTileCount++; + } + } + } + // Construct stencil and cover VBOs. this.tileCount = 0; let primitiveCount = 0; @@ -324,10 +402,16 @@ class App { const pathTileStrips = scene.pathTileStrips[pathIndex]; for (const tileStrip of pathTileStrips) { for (const tile of tileStrip.tiles) { - // TODO(pcwalton) + const color = scene.pathColors[pathIndex]; if (tile.isFilled()) continue; + if (opaqueTiles[tile.tileLeft] != null && + opaqueTiles[tile.tileLeft][tileStrip.tileTop] != null && + pathIndex <= opaqueTiles[tile.tileLeft][tileStrip.tileTop]) { + continue; + } + for (const edge of tile.edges) { let ctrl; if (edge.ctrl == null) @@ -341,7 +425,6 @@ class App { primitiveCount++; } - const color = scene.pathColors[pathIndex]; coverVertexData.push(Math.floor(tile.tileLeft), Math.floor(tileStrip.tileTop), this.tileCount, @@ -359,6 +442,10 @@ class App { gl.bindBuffer(gl.ARRAY_BUFFER, this.stencilVertexTileIndicesBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Uint16Array(stencilVertexTileIndices), gl.STATIC_DRAW); + // Populate the opaque VBO. + gl.bindBuffer(gl.ARRAY_BUFFER, this.opaqueVertexBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Int16Array(opaqueVertexData), gl.DYNAMIC_DRAW); + // Populate the cover VBO. gl.bindBuffer(gl.ARRAY_BUFFER, this.coverVertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Int16Array(coverVertexData), gl.DYNAMIC_DRAW); @@ -382,46 +469,34 @@ class Scene { document.body.appendChild(svgElement); const pathElements = Array.from(document.getElementsByTagName('path')); - const pathColors = []; + const pathColors: any[] = []; this.pathTileStrips = []; //const tileDebugger = new TileDebugger(document); - const paths = []; + const paths: SVGPath[] = []; for (let pathElementIndex = 0; pathElementIndex < pathElements.length; pathElementIndex++) { const pathElement = pathElements[pathElementIndex]; + const pathString = unwrapNull(pathElement.getAttribute('d')); const style = window.getComputedStyle(pathElement); - let paint: string; if (style.fill != null && style.fill !== 'none') { - paint = style.fill; - /*} else if (style.stroke != null && style.stroke !== 'none') { - paint = style.stroke;*/ - } else { - continue; + this.addPath(paths, pathColors, style.fill, pathString); + } + if (style.stroke != null && style.stroke !== 'none') { + /* + const strokeWidth = + style.strokeWidth == null ? 1.0 : parseFloat(style.strokeWidth); + console.log("stroking path:", pathString, strokeWidth); + try { + const strokedPathString = svgPathOutline(pathString, strokeWidth, {joints: 1}); + this.addPath(paths, pathColors, style.stroke, strokedPathString); + } catch (e) {} + */ } - - const color = parseColor(paint).rgba; - pathColors.push({ - r: color[0], - g: color[1], - b: color[2], - a: Math.round(color[3] * 255.), - }); - - let path = SVGPath(unwrapNull(pathElement.getAttribute('d'))); - path = path.matrix([ - GLOBAL_TRANSFORM.a, GLOBAL_TRANSFORM.b, - GLOBAL_TRANSFORM.c, GLOBAL_TRANSFORM.d, - GLOBAL_TRANSFORM.tx, GLOBAL_TRANSFORM.ty, - ]); - - path = flattenPath(path); - path = canonicalizePath(path); - paths.push(path); } const startTime = window.performance.now(); @@ -474,6 +549,27 @@ class Scene { this.pathColors = pathColors; } + + private addPath(paths: SVGPath[], pathColors: any[], paint: string, pathString: string): void { + const color = parseColor(paint).rgba; + pathColors.push({ + r: color[0], + g: color[1], + b: color[2], + a: Math.round(color[3] * 255.), + }); + + let path = SVGPath(pathString); + path = path.matrix([ + GLOBAL_TRANSFORM.a, GLOBAL_TRANSFORM.b, + GLOBAL_TRANSFORM.c, GLOBAL_TRANSFORM.d, + GLOBAL_TRANSFORM.tx, GLOBAL_TRANSFORM.ty, + ]); + + path = flattenPath(path); + path = canonicalizePath(path); + paths.push(path); + } } class Program { @@ -540,6 +636,11 @@ function flattenPath(path: SVGPath): SVGPath { const ctrl = new Point2D(0.5 * (ctrl0.x + ctrl1.x), 0.5 * (ctrl0.y + ctrl1.y)); return [['Q', "" + ctrl.x, "" + ctrl.y, "" + to.x, "" + to.y]]; } + if (segment[0] === 'A') { + const to = new Point2D(parseFloat(segment[segment.length - 2]), + parseFloat(segment[segment.length - 1])); + return [['L', "" + to.x, "" + to.y]]; + } return [segment]; }); } @@ -566,23 +667,6 @@ function waitForQuery(gl: WebGL2RenderingContext, disjointTimerQueryExt: any, qu console.log(elapsed + "ms elapsed"); } -function pathIsRect(path: SVGPath, rectSize: Size2D): boolean { - let result = true; - path.iterate((segment, index) => { - if (segment.length < 3) - return; - const point = new Point2D(parseFloat(segment[1]), parseFloat(segment[2])); - result = result && - (approxEq(point.x, 0.0) || approxEq(point.x, rectSize.width)) && - (approxEq(point.y, 0.0) || approxEq(point.y, rectSize.height)); - }); - return result; -} - -function sampleBezier(from: Point2D, ctrl: Point2D, to: Point2D, t: number): Point2D { - return from.lerp(ctrl, t).lerp(ctrl.lerp(to, t), t); -} - function main(): void { window.fetch(SVG).then(svg => { svg.text().then(svgText => {