From 19d17afdb81f8bf208e2b3617f3cddf49323ffad Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Mon, 26 Nov 2018 08:24:09 -0800 Subject: [PATCH] WIP --- demo2/pathfinder.ts | 148 ++++++++++++++++++++++++++++++------------ demo2/stencil.vs.glsl | 9 +-- 2 files changed, 112 insertions(+), 45 deletions(-) diff --git a/demo2/pathfinder.ts b/demo2/pathfinder.ts index 116d9dcd..01a4b4ad 100644 --- a/demo2/pathfinder.ts +++ b/demo2/pathfinder.ts @@ -52,6 +52,10 @@ class Point2D { approxEq(other: Point2D): boolean { return approxEq(this.x, other.x) && approxEq(this.y, other.y); } + + lerp(other: Point2D, t: number): Point2D { + return new Point2D(lerp(this.x, other.x, t), lerp(this.y, other.y, t)); + } } interface Size2D { @@ -104,7 +108,7 @@ class App { private stencilTexture: WebGLTexture; private stencilFramebuffer: WebGLFramebuffer; private stencilProgram: Program<'FramebufferSize' | 'TileSize' | 'AreaLUT', - 'TessCoord' | 'From' | 'To' | 'TileIndex'>; + 'TessCoord' | 'From' | 'Ctrl' | 'To' | 'TileIndex'>; private coverProgram: Program<'FramebufferSize' | 'TileSize' | 'StencilTexture' | 'StencilTextureSize', 'TessCoord' | 'TileOrigin' | 'TileIndex' | 'Color'>; @@ -185,7 +189,7 @@ class App { STENCIL_VERTEX_SHADER_SOURCE, STENCIL_FRAGMENT_SHADER_SOURCE, ['FramebufferSize', 'TileSize', 'AreaLUT'], - ['TessCoord', 'From', 'To', 'TileIndex']); + ['TessCoord', 'From', 'Ctrl', 'To', 'TileIndex']); this.stencilProgram = stencilProgram; // Initialize quad VBO. @@ -209,9 +213,11 @@ class App { 0, 0); gl.bindBuffer(gl.ARRAY_BUFFER, this.stencilVertexPositionsBuffer); - gl.vertexAttribPointer(stencilProgram.attributes.From, 2, gl.FLOAT, false, 16, 0); + gl.vertexAttribPointer(stencilProgram.attributes.From, 2, gl.FLOAT, false, 24, 0); gl.vertexAttribDivisor(stencilProgram.attributes.From, 1); - gl.vertexAttribPointer(stencilProgram.attributes.To, 2, gl.FLOAT, false, 16, 8); + gl.vertexAttribPointer(stencilProgram.attributes.Ctrl, 2, gl.FLOAT, false, 24, 8); + gl.vertexAttribDivisor(stencilProgram.attributes.Ctrl, 1); + gl.vertexAttribPointer(stencilProgram.attributes.To, 2, gl.FLOAT, false, 24, 16); gl.vertexAttribDivisor(stencilProgram.attributes.To, 1); gl.bindBuffer(gl.ARRAY_BUFFER, this.stencilVertexTileIndicesBuffer); gl.vertexAttribIPointer(stencilProgram.attributes.TileIndex, @@ -222,6 +228,7 @@ class App { gl.vertexAttribDivisor(stencilProgram.attributes.TileIndex, 1); gl.enableVertexAttribArray(stencilProgram.attributes.TessCoord); gl.enableVertexAttribArray(stencilProgram.attributes.From); + gl.enableVertexAttribArray(stencilProgram.attributes.Ctrl); gl.enableVertexAttribArray(stencilProgram.attributes.To); gl.enableVertexAttribArray(stencilProgram.attributes.TileIndex); @@ -364,7 +371,7 @@ class App { const primitiveCountHistogram: number[] = []; for (let tileIndex = 0; tileIndex < scene.tiles.length; tileIndex++) { const tile = scene.tiles[tileIndex]; - let firstPoint = {x: 0.0, y: 0.0}, lastPoint = {x: 0.0, y: 0.0}; + let firstPoint = new Point2D(0.0, 0.0), lastPoint = new Point2D(0.0, 0.0); let primitiveCountForThisTile = 0; tile.path.iterate(segment => { /*if (primitiveCountForThisTile > 0) @@ -374,10 +381,8 @@ class App { if (segment[0] === 'Z') { point = firstPoint; } else { - point = { - x: parseFloat(segment[segment.length - 2]), - y: parseFloat(segment[segment.length - 1]), - }; + point = new Point2D(parseFloat(segment[segment.length - 2]), + parseFloat(segment[segment.length - 1])); } /* @@ -393,8 +398,20 @@ class App { if (segment[0] === 'M') { firstPoint = point; + } else if (segment[0] === 'Q') { + const ctrl = new Point2D(parseFloat(segment[segment.length - 4]), + parseFloat(segment[segment.length - 3])); + stencilVertexPositions.push(lastPoint.x, lastPoint.y, + ctrl.x, ctrl.y, + point.x, point.y); + stencilVertexTileIndices.push(tileIndex); + primitiveCount++; + primitiveCountForThisTile++; } else { - stencilVertexPositions.push(lastPoint.x, lastPoint.y, point.x, point.y); + const ctrl = lastPoint.lerp(point, 0.5); + stencilVertexPositions.push(lastPoint.x, lastPoint.y, + ctrl.x, ctrl.y, + point.x, point.y); stencilVertexTileIndices.push(tileIndex); primitiveCount++; primitiveCountForThisTile++; @@ -587,7 +604,7 @@ class Scene { let output: string[][] = []; input.iterate(segment => { const event = segment[0]; - let to; + let ctrl: Point2D | null = null, to; switch (event) { case 'M': from = new Point2D(parseFloat(segment[segment.length - 2]), @@ -600,6 +617,10 @@ class Scene { return; to = pathStart; break; + case 'Q': + ctrl = new Point2D(parseFloat(segment[segment.length - 4]), + parseFloat(segment[segment.length - 3])); + // fallthrough default: to = new Point2D(parseFloat(segment[segment.length - 2]), parseFloat(segment[segment.length - 1])); @@ -608,17 +629,14 @@ class Scene { if (this.pointIsInside(edge, edgePos, to)) { if (!this.pointIsInside(edge, edgePos, from)) { - this.addLine(this.computeLineIntersection(edge, edgePos, from, to), - output, - firstPoint); + this.addClippedLine(from, ctrl, to, edge, edgePos, output, firstPoint); firstPoint = false; } + // FIXME(pcwalton): Is this right? this.addLine(to, output, firstPoint); firstPoint = false; } else if (this.pointIsInside(edge, edgePos, from)) { - this.addLine(this.computeLineIntersection(edge, edgePos, from, to), - output, - firstPoint); + this.addClippedLine(from, ctrl, to, edge, edgePos, output, firstPoint); firstPoint = false; } @@ -633,6 +651,47 @@ class Scene { return SVGPath(output.map(segment => segment.join(" ")).join(" ")); } + private addClippedLine(from: Point2D, + ctrl: Point2D | null, + to: Point2D, + edge: Edge, + edgePos: number, + output: string[][], + firstPoint: boolean): + void { + if (ctrl == null) { + if (edge === 'left' || edge === 'right') + to = this.computeLineIntersectionX(edgePos, from, to); + else + to = this.computeLineIntersectionY(edgePos, from, to); + } else { + let minT = 0.0, maxT = 1.0; + while (maxT - minT > 1e-3) { + const midT = lerp(minT, maxT, 0.5); + const point = sampleBezier(from, ctrl, to, midT); + const diff = ((edge === 'left' || edge === 'right') ? point.x : point.y) - edgePos; + if (diff < 0.0) + maxT = midT; + else if (diff > 0.0) + minT = midT; + else + break; + } + + const midT = lerp(minT, maxT, 0.5); + const newCtrl = from.lerp(ctrl, midT); + to = sampleBezier(from, ctrl, to, midT); + ctrl = newCtrl; + } + + if (firstPoint) + output.push(['M', "" + to.x, "" + to.y]); + else if (ctrl == null) + output.push(['L', "" + to.x, "" + to.y]); + else + output.push(['Q', "" + ctrl.x, "" + ctrl.y, "" + to.x, "" + to.y]); + } + private addLine(to: Point2D, output: string[][], firstPoint: boolean) { if (firstPoint) output.push(['M', "" + to.x, "" + to.y]); @@ -649,29 +708,27 @@ class Scene { } } - private computeLineIntersection(edge: Edge, - edgePos: number, - startPoint: Point2D, - endpoint: Point2D): - Point2D { + private computeLineIntersectionX(x: number, + startPoint: Point2D, + endpoint: Point2D): + Point2D { const start = {x: startPoint.x, y: startPoint.y, z: 1.0}; const end = {x: endpoint.x, y: endpoint.y, z: 1.0}; - - let edgeVector: Vector3D; - switch (edge) { - case 'left': - case 'right': - edgeVector = {x: 1.0, y: 0.0, z: -edgePos}; - break; - default: - edgeVector = {x: 0.0, y: 1.0, z: -edgePos}; - break; - } - + let edgeVector: Vector3D = {x: 1.0, y: 0.0, z: -x}; const intersection = cross(cross(start, end), edgeVector); return new Point2D(intersection.x / intersection.z, intersection.y / intersection.z); } + private computeLineIntersectionY(y: number, + startPoint: Point2D, + endpoint: Point2D): + Point2D { + const start = {x: startPoint.x, y: startPoint.y, z: 1.0}; + const end = {x: endpoint.x, y: endpoint.y, z: 1.0}; + let edgeVector: Vector3D = {x: 0.0, y: 1.0, z: -y}; + const intersection = cross(cross(start, end), edgeVector); + return new Point2D(intersection.x / intersection.z, intersection.y / intersection.z); + } } class Tile { @@ -753,14 +810,15 @@ class PathSegment { function flattenPath(path: SVGPath): SVGPath { return path.abs().iterate(segment => { - if (segment[0] === 'Q') - return [['L', segment[1], segment[2]], ['L', segment[3], segment[4]]]; if (segment[0] === 'C') { - return [ - ['L', segment[1], segment[2]], - ['L', segment[3], segment[4]], - ['L', segment[5], segment[6]], - ]; + const ctrl0 = new Point2D(parseFloat(segment[segment.length - 6]), + parseFloat(segment[segment.length - 5])); + const ctrl1 = new Point2D(parseFloat(segment[segment.length - 4]), + parseFloat(segment[segment.length - 3])); + const to = new Point2D(parseFloat(segment[segment.length - 2]), + parseFloat(segment[segment.length - 1])); + 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]]; } return [segment]; }); @@ -813,6 +871,14 @@ function pathIsSquare(path: SVGPath, squareLength: number): boolean { return result; } +function lerp(a: number, b: number, t: number): number { + return a + (b - a) * t; +} + +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 => { diff --git a/demo2/stencil.vs.glsl b/demo2/stencil.vs.glsl index ea6d402f..98f21515 100644 --- a/demo2/stencil.vs.glsl +++ b/demo2/stencil.vs.glsl @@ -17,6 +17,7 @@ uniform vec2 uTileSize; in vec2 aTessCoord; in vec2 aFrom; +in vec2 aCtrl; in vec2 aTo; in uint aTileIndex; @@ -33,7 +34,7 @@ vec2 computeTileOffset(uint tileIndex, float stencilTextureWidth) { void main() { vec2 tileOrigin = computeTileOffset(aTileIndex, uFramebufferSize.x); - vec2 from = aFrom, ctrl = mix(aFrom, aTo, 0.5), to = aTo; + vec2 from = aFrom, ctrl = aCtrl, to = aTo; vec2 dilation, position; bool zeroArea = abs(from.x - to.x) < 0.01; @@ -53,9 +54,9 @@ void main() { } position += dilation; - vFrom = aFrom - position; - vCtrl = mix(aFrom, aTo, 0.5) - position; - vTo = aTo - position; + vFrom = from - position; + vCtrl = ctrl - position; + vTo = to - position; gl_Position = vec4((tileOrigin + position) / uFramebufferSize * 2.0 - 1.0, 0.0, 1.0); }