Remove the Pathfinder 2 demo

This commit is contained in:
Patrick Walton 2019-02-08 14:39:35 -08:00
parent 2a118c3af0
commit 4062f824fd
47 changed files with 0 additions and 11449 deletions

2
demo/.gitignore vendored
View File

@ -1,2 +0,0 @@
node_modules

View File

@ -1,46 +0,0 @@
const {spawn} = require('child_process');
const process = require('process');
class RustdocPlugin {
constructor(options) {
this.options = Object.assign({directories: [], flags: {}}, options);
}
apply(compiler) {
let rustdocFlags = [];
for (let key in this.options.flags) {
if (this.options.flags.hasOwnProperty(key))
rustdocFlags.push("--" + key + "=" + this.options.flags[key]);
}
rustdocFlags = rustdocFlags.join(" ");
compiler.plugin('after-compile', (compilation, done) => {
let directoriesLeft = this.options.directories.length;
for (const directory of this.options.directories) {
console.log("Building documentation for `" + directory + "`...");
const cargo = spawn("cargo", ["doc", "--no-deps"], {
cwd: directory,
env: Object.assign({RUSTDOCFLAGS: rustdocFlags}, process.env),
});
cargo.stdout.setEncoding('utf8');
cargo.stderr.setEncoding('utf8');
cargo.stdout.on('data', data => console.log(data));
cargo.stderr.on('data', data => console.log(data));
cargo.on('close', code => {
if (code !== 0) {
const message = "Failed to build documentation for `" + directory + "`!";
console.error(message);
throw new Error(message);
}
console.log("Built documentation for `" + directory + "`.");
directoriesLeft--;
if (directoriesLeft === 0)
done();
});
}
});
}
}
module.exports = RustdocPlugin;

View File

@ -1,11 +0,0 @@
{
"name": "rustdoc-webpack-plugin",
"version": "0.1.0",
"description": "Webpack plugin for building Rust package documentation",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Patrick Walton",
"license": "MIT OR Apache-2.0"
}

View File

@ -1,43 +0,0 @@
body {
padding: 0 !important;
}
h1,
h2,
h3
h4,
.sidebar,
a.source,
.search-input,
.content table :not(code) > a,
.collapse-toggle,
ul.item-list > li > .out-of-band {
font-family: inherit !important;
}
.content {
padding: 0 15px 0 0 !important;
}
nav.navbar {
max-width: inherit !important;
border: none !important;
margin: 0 !important;
}
nav.sidebar {
top: auto !important;
}
img {
max-width: inherit !important;
}
h4 > span.invisible,
.structfield > span.invisible,
.variant > span.invisible {
visibility: visible !important;
}
/*
* Hide `generate_gamma_lut` and `pathfinder_server`.
* FIXME(pcwalton): Is there a better way to do this?
*/
.sidebar-elems > .block.crate > ul > li:first-child,
.sidebar-elems > .block.crate > ul > li:last-child {
display: none;
}

View File

@ -1,406 +0,0 @@
/*
* Fonts
*/
@font-face {
font-family: "Inter UI";
font-weight: 400;
src: url("/woff2/inter-ui/Inter-UI-Regular.woff2") format('woff2');
}
@font-face {
font-family: "Inter UI";
font-weight: 700;
src: url("/woff2/inter-ui/Inter-UI-Bold.woff2") format('woff2');
}
@font-face {
font-family: "Inter UI";
font-weight: 400;
font-style: italic;
src: url("/woff2/inter-ui/Inter-UI-RegularItalic.woff2") format('woff2');
}
@font-face {
font-family: "Inter UI";
font-weight: 700;
font-style: italic;
src: url("/woff2/inter-ui/Inter-UI-BoldItalic.woff2") format('woff2');
}
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url("/woff2/material-icons/MaterialIcons-Regular.woff2") format('woff2');
}
/*
* Core
*/
body {
font-family: "Inter UI", sans-serif;
}
body.pf-unscrollable {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
nav {
-moz-user-select: none;
user-select: none;
}
#pf-load-font-button-label,
#pf-load-svg-button-label {
left: 1em;
margin: 0;
}
#pf-settings-container, #pf-rotate-slider-container {
position: absolute;
right: 1rem;
bottom: 2rem;
pointer-events: none;
text-align: right;
}
#pf-toolbar {
margin-right: 1rem;
}
#pf-settings, #pf-rotate-slider-card {
text-align: initial;
user-select: none;
-moz-user-select: none;
opacity: 1.0;
transition: opacity 300ms, transform 300ms, visibility 300ms;
transform: translateY(0);
}
#pf-settings:not(.pf-invisible), #pf-rotate-slider-card:not(.pf-invisible) {
pointer-events: auto;
}
.pf-toolbar-button {
pointer-events: auto;
}
#pf-test-pane {
pointer-events: auto;
}
.pf-toolbar-button.btn-outline-secondary:not(:hover) {
background: rgba(255, 255, 255, 0.75);
}
#pf-file-select {
position: absolute;
visibility: hidden;
}
#pf-settings.pf-invisible, #pf-rotate-slider-card.pf-invisible {
opacity: 0.0;
transform: translateY(1em);
visibility: hidden;
}
button > svg {
width: 1.25em;
display: block;
}
button > svg path {
fill: currentColor;
}
.pf-pointer-events-none {
pointer-events: none;
}
.pf-pointer-events-auto {
pointer-events: auto;
}
#pf-rendering-options-group {
right: 1em;
}
.pf-maximized-canvas {
display: block;
position: absolute;
touch-action: none;
}
#pf-canvas.pf-draggable {
cursor: grab;
cursor: -webkit-grab;
}
#pf-canvas.pf-draggable.pf-grabbing {
cursor: grabbing;
cursor: -webkit-grabbing;
}
#pf-fps-label {
color: white;
background: rgba(0, 0, 0, 0.75);
width: 17em;
}
#pf-svg, #pf-svg * {
visibility: hidden !important;
}
#pf-outer-container {
display: flex;
flex-direction: column;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#pf-inner-container {
overflow: scroll;
}
.pf-spinner {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.pf-spinner-hidden {
display: none !important;
}
.pf-display-none {
display: none !important;
}
#pf-benchmark-results-modal .modal-body {
overflow: scroll;
max-height: 75vh;
}
.pf-benchmark-results-global {
padding: 0.75rem;
}
.pf-benchmark-results-label {
font-weight: bold;
}
.pf-material-icons {
font-family: "Material Icons";
display: block;
font-size: 18px;
margin: -3px 0 -3px;
}
#pf-rotate-slider {
margin: 2px 0 0;
}
label.pf-disabled {
color: gray;
}
.btn-group input[type="radio"] {
-moz-appearance: none;
}
/*
* Arrow
*
* http://www.cssarrowplease.com/
*/
#pf-settings > .pf-arrow-box:after, #pf-settings .pf-arrow-box:before {
right: 22px;
}
#pf-rotate-slider-card > .pf-arrow-box:after, #pf-rotate-slider-card .pf-arrow-box:before {
right: 166px;
}
.pf-arrow-box:after, .pf-arrow-box:before {
top: 100%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
.pf-arrow-box:after {
border-color: rgba(0, 0, 0, 0);
border-top-color: white;
border-width: 14px;
margin-right: -14px;
}
.pf-arrow-box:before {
border-color: rgba(0, 0, 0, 0);
border-top-color: rgba(0, 0, 0, 0.125);
border-width: 15px;
margin-right: -15px;
}
#pf-masthead-logo {
width: 30vw;
}
.github-corner {
display: block;
position: absolute;
right: 0;
top: 0;
height: 100%;
text-align: right;
}
.github-corner > svg {
fill: #151513;
color: #fff;
height: 100%;
width: auto;
}
.github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
@keyframes octocat-wave{
0%, 100% {
transform: rotate(0);
}
20%, 60% {
transform: rotate(-25deg);
}
40%, 80% {
transform: rotate(10deg);
}
}
@media (max-width: 500px) {
.github-corner:hover .octo-arm {
animation: none;
}
.github-corner .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
}
/*
* Spinner
*
* http://tobiasahlin.com/spinkit/
*/
.sk-fading-circle {
margin: auto;
width: 40px;
height: 40px;
}
.sk-fading-circle .sk-circle {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
.sk-fading-circle .sk-circle:before {
content: '';
display: block;
margin: 0 auto;
width: 15%;
height: 15%;
background-color: #333;
border-radius: 100%;
-webkit-animation: sk-circleFadeDelay 1.2s infinite ease-in-out both;
animation: sk-circleFadeDelay 1.2s infinite ease-in-out both;
}
.sk-fading-circle .sk-circle2 {
-webkit-transform: rotate(30deg);
-ms-transform: rotate(30deg);
transform: rotate(30deg);
}
.sk-fading-circle .sk-circle3 {
-webkit-transform: rotate(60deg);
-ms-transform: rotate(60deg);
transform: rotate(60deg);
}
.sk-fading-circle .sk-circle4 {
-webkit-transform: rotate(90deg);
-ms-transform: rotate(90deg);
transform: rotate(90deg);
}
.sk-fading-circle .sk-circle5 {
-webkit-transform: rotate(120deg);
-ms-transform: rotate(120deg);
transform: rotate(120deg);
}
.sk-fading-circle .sk-circle6 {
-webkit-transform: rotate(150deg);
-ms-transform: rotate(150deg);
transform: rotate(150deg);
}
.sk-fading-circle .sk-circle7 {
-webkit-transform: rotate(180deg);
-ms-transform: rotate(180deg);
transform: rotate(180deg);
}
.sk-fading-circle .sk-circle8 {
-webkit-transform: rotate(210deg);
-ms-transform: rotate(210deg);
transform: rotate(210deg);
}
.sk-fading-circle .sk-circle9 {
-webkit-transform: rotate(240deg);
-ms-transform: rotate(240deg);
transform: rotate(240deg);
}
.sk-fading-circle .sk-circle10 {
-webkit-transform: rotate(270deg);
-ms-transform: rotate(270deg);
transform: rotate(270deg);
}
.sk-fading-circle .sk-circle11 {
-webkit-transform: rotate(300deg);
-ms-transform: rotate(300deg);
transform: rotate(300deg);
}
.sk-fading-circle .sk-circle12 {
-webkit-transform: rotate(330deg);
-ms-transform: rotate(330deg);
transform: rotate(330deg);
}
.sk-fading-circle .sk-circle2:before {
-webkit-animation-delay: -1.1s;
animation-delay: -1.1s;
}
.sk-fading-circle .sk-circle3:before {
-webkit-animation-delay: -1s;
animation-delay: -1s;
}
.sk-fading-circle .sk-circle4:before {
-webkit-animation-delay: -0.9s;
animation-delay: -0.9s;
}
.sk-fading-circle .sk-circle5:before {
-webkit-animation-delay: -0.8s;
animation-delay: -0.8s;
}
.sk-fading-circle .sk-circle6:before {
-webkit-animation-delay: -0.7s;
animation-delay: -0.7s;
}
.sk-fading-circle .sk-circle7:before {
-webkit-animation-delay: -0.6s;
animation-delay: -0.6s;
}
.sk-fading-circle .sk-circle8:before {
-webkit-animation-delay: -0.5s;
animation-delay: -0.5s;
}
.sk-fading-circle .sk-circle9:before {
-webkit-animation-delay: -0.4s;
animation-delay: -0.4s;
}
.sk-fading-circle .sk-circle10:before {
-webkit-animation-delay: -0.3s;
animation-delay: -0.3s;
}
.sk-fading-circle .sk-circle11:before {
-webkit-animation-delay: -0.2s;
animation-delay: -0.2s;
}
.sk-fading-circle .sk-circle12:before {
-webkit-animation-delay: -0.1s;
animation-delay: -0.1s;
}
@-webkit-keyframes sk-circleFadeDelay {
0%, 39%, 100% { opacity: 0; }
40% { opacity: 1; }
}
@keyframes sk-circleFadeDelay {
0%, 39%, 100% { opacity: 0; }
40% { opacity: 1; }
}

View File

@ -1,58 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>3D Demo &mdash; Pathfinder</title>
<meta charset="utf-8">
{{>partials/header.html}}
<script type="text/javascript" src="/js/pathfinder/3d-demo.js"></script>
</head>
<body class="pf-unscrollable">
{{>partials/navbar.html isDemo=true}}
<canvas id="pf-canvas" class="pf-maximized-canvas" width="400" height="300"></canvas>
<div class="fixed-bottom mb-3 d-flex justify-content-between align-items-end pf-pointer-events-none">
<div class="rounded invisible container py-1 px-3 ml-3" id="pf-fps-label"></div>
<div id="pf-toolbar">
<button id="pf-screenshot-button" type="button"
class="btn btn-outline-secondary pf-toolbar-button">
<span class="pf-material-icons">photo_camera</span>
</button>
<div id="pf-settings-container">
<div id="pf-settings" class="card mb-4 pf-invisible">
<div class="card-body">
<button id="pf-settings-close-button" type="button" class="close"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<form>
<div class="form-group">
<label for="pf-aa-level-select">Antialiasing</label>
<select id="pf-aa-level-select" class="form-control custom-select">
<option value="none">None</option>
<option value="ssaa-2">2&times;SSAA</option>
<option value="ssaa-4" selected>4&times;SSAA</option>
</select>
</div>
</form>
</div>
<div class="pf-arrow-box"></div>
</div>
</div>
<button id="pf-settings-button" type="button"
class="btn btn-outline-secondary pf-toolbar-button"
aria-expanded="false" aria-controls="#pf-settings">
<span class="pf-material-icons">settings</span>
</button>
<button id="pf-vr" type="button"
class="btn btn-outline-secondary pf-toolbar-button"
aria-expanded="false" aria-controls="#pf-vr"
style="display: none">
<!-- https://materialdesignicons.com/icon/virtual-reality -->
<svg style="height: 18px" viewBox="0 0 24 24">
<path fill="#000000" d="M5,3C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3H5M6,9H7.5L8.5,12.43L9.5,9H11L9.25,15H7.75L6,9M13,9H16.5C17.35,9 18,9.65 18,10.5V11.5C18,12.1 17.6,12.65 17.1,12.9L18,15H16.5L15.65,13H14.5V15H13V9M14.5,10.5V11.5H16.5V10.5H14.5Z" />
</svg>
</button>
</div>
</div>
{{>partials/spinner.html}}
</body>
</html>

View File

@ -1,122 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Benchmark &mdash; Pathfinder</title>
<meta charset="utf-8">
{{>partials/header.html}}
<script type="text/javascript" src="/js/pathfinder/benchmark.js"></script>
</head>
<body class="pf-unscrollable">
{{>partials/navbar.html isTool=true}}
<canvas id="pf-canvas" class="pf-maximized-canvas" width="400" height="300"></canvas>
<svg id="pf-svg" xmlns="http://www.w3.org/2000/svg" width="400" height="300"></svg>
<div class="modal fade" id="pf-benchmark-modal" tabindex="-1" role="dialog"
aria-labelledby="pf-benchmark-label" aria-hidden="false">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="pf-benchmark-label">Benchmark</h5>
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="pf-benchmark-text-tab"
data-toggle="tab" href="#pf-benchmark-text" role="tab"
aria-controls="pf-benchmark-text" aria-selected="true">Text</a>
</li>
<li class="nav-item">
<a class="nav-link" id="pf-benchmark-svg-tab" data-toggle="tab"
href="#pf-benchmark-svg" role="tab"
aria-controls="pf-benchmark-svg" aria-selected="false">SVG</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane show active pt-3" id="pf-benchmark-text"
role="tabpanel" aria-labelledby="pf-benchmark-text-tab">
<form class="form" id="pf-benchmark-text-form">
<div class="form-group" id="pf-aa-level-form-group">
<label for="pf-aa-level-select"
class="col-form-label mr-sm-2">Antialiasing</label>
<select id="pf-aa-level-select" autocomplete="off"
class="form-control custom-select mr-sm-3">
<option value="none" selected>None</option>
<option value="ssaa-2">2&times;SSAA</option>
<option value="ssaa-4">4&times;SSAA</option>
<option value="ssaa-8">8&times;SSAA</option>
<option value="ssaa-16">16&times;SSAA</option>
<option value="xcaa">XCAA</option>
</select>
</div>
<div class="form-group">
<label for="pf-subpixel-aa-select">Subpixel AA</label>
<select id="pf-subpixel-aa-select"
class="form-control custom-select">
<option value="none">None</option>
<option value="freetype" selected>
FreeType-style
</option>
<option value="core-graphics">
Core Graphics-style
</option>
</select>
</div>
</form>
</div>
<div class="tab-pane pt-3" id="pf-benchmark-svg"
role="tabpanel" aria-labelledby="pf-benchmark-svg-tab">
<form class="form" id="pf-benchmark-svg-form">
</form>
</div>
</div>
<div class="d-flex justify-content-end">
<button id="pf-run-benchmark-button" type="button"
class="btn btn-primary">
Run
</button>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="pf-benchmark-results-modal" tabindex="-1" role="dialog"
aria-labelledby="pf-benchmark-results-label" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="pf-benchmark-result-label">
Benchmark Results
</h5>
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="pf-benchmark-results-global">
<span class="pf-benchmark-results-label">Partitioning:</span>
&nbsp;
<span id="pf-benchmark-results-partitioning-time">0</span> µs/glyph
</div>
<table class="table table-striped">
<thead class="thead-default" id="pf-benchmark-results-table-header">
<tr><th>Font size (px)</th><th>GPU time per glyph (µs)</th></tr>
</thead>
<tbody id="pf-benchmark-results-table-body"></tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
id="pf-benchmark-results-save-csv-button">Save CSV&hellip;</button>
<button type="button" class="btn btn-primary"
id="pf-benchmark-results-close-button">Close</button>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1 +0,0 @@
{{>partials/navbar.html isDoc=true}}

View File

@ -1,2 +0,0 @@
{{>partials/header.html}}
<link rel="stylesheet" href="/css/pathfinder-doc.css">

View File

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Pathfinder</title>
<meta charset="utf-8">
{{>partials/header.html}}
</head>
<body>
{{>partials/navbar.html}}
<main id="content" class="mt-5 mb-5">
<div class="container d-flex flex-row">
<div><img alt="" src="/svg/demo/logo" id="pf-masthead-logo"></div>
<div class="flex-column d-flex justify-content-center ml-5">
<h1 class="display-4">Pathfinder</h1>
<p class="lead">
A fast, high-quality, open source text and vector graphics renderer for
GPUs.
</p>
</div>
</div>
</main>
</body>
</html>

View File

@ -1,73 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Mesh Debugger &mdash; Pathfinder</title>
<meta charset="utf-8">
{{>partials/header.html}}
<script type="text/javascript" src="/js/pathfinder/mesh-debugger.js"></script>
</head>
<body class="pf-unscrollable">
{{>partials/navbar.html isTool=true}}
<canvas id="pf-canvas" class="pf-maximized-canvas" width="400" height="300"></canvas>
<svg id="pf-svg" xmlns="http://www.w3.org/2000/svg" width="400" height="300"></svg>
<div class="fixed-bottom mb-3 d-flex justify-content-end align-items-end pf-pointer-events-none">
<div id="pf-toolbar">
<button id="pf-open-button" type="button"
class="btn btn-outline-secondary pf-toolbar-button">
Open…
</button>
</div>
</div>
<div class="modal fade" id="pf-open-modal" tabindex="-1" role="dialog"
aria-labelledby="pf-open-label" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="pf-open-label">Open…</h5>
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<label for="pf-open-file-label">File</label>
<select id="pf-open-file-select"
class="form-control custom-select">
<option value="font-eb-garamond" selected>
Font: EB Garamond
</option>
<option value="font-open-sans">Font: Open Sans</option>
<option value="font-nimbus-sans">Font: Nimbus Sans</option>
<option value="font-inter-ui">Font: Inter UI</option>
<option value="font-custom">Font: Custom…</option>
<option value="svg-tiger">SVG: Ghostscript Tiger</option>
<option value="svg-logo">SVG: Pathfinder Logo</option>
<option value="svg-custom">SVG: Custom…</option>
</select>
<input id="pf-file-select" type="file">
</div>
<div class="form-group" id="pf-font-path-select-group">
<label for="pf-font-path-label">Path</label>
<select id="pf-font-path-select"
class="form-control custom-select">
<option value="65" selected>A</option>
<option value="66">B</option>
<option value="67">C</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
data-dismiss="modal">Cancel</button>
<button type="button"
class="btn btn-primary" id="pf-open-ok-button">Open</button>
</div>
</div>
</div>
</div>
{{>partials/spinner.html}}
</body>
</html>

View File

@ -1,6 +0,0 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/css/bootstrap/bootstrap.css">
<link rel="stylesheet" href="/css/pathfinder.css">
<script type="text/javascript" src="/js/jquery/jquery.js"></script>
<script type="text/javascript" src="/js/popper.js/popper.js"></script>
<script type="text/javascript" src="/js/bootstrap/bootstrap.js"></script>

View File

@ -1,42 +0,0 @@
<nav class="navbar sticky-top navbar-expand-sm navbar-dark bg-dark">
<a class="navbar-brand" href="/">
<img id="pf-navbar-logo" alt="" src="/svg/demo/logo-bw" width="30" class="mr-2">
Pathfinder
</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav mr-auto">
<li class="nav-item dropdown {{#if isDemo}}active{{/if}}">
<a class="nav-link dropdown-toggle" id="pf-demos-menu" data-toggle="dropdown"
ref="/" aria-haspopup="true" aria-expanded="false">Demos</a>
<div class="dropdown-menu" aria-labelledby="pf-demos-menu">
<a class="dropdown-item" href="/demo/text">Text</a>
<a class="dropdown-item" href="/demo/svg">SVG</a>
<a class="dropdown-item" href="/demo/3d">3D</a>
</div>
</li>
<li class="nav-item {{#if isDoc}}active{{/if}}">
<a class="nav-link" href="/doc/api">Documentation</a>
</li>
<li class="nav-item dropdown {{#if isTool}}active{{/if}}">
<a class="nav-link dropdown-toggle" id="pf-tools-menu" data-toggle="dropdown"
ref="/" aria-haspopup="true" aria-expanded="false">Tools</a>
<div class="dropdown-menu" aria-labelledby="pf-tools-menu">
<a class="dropdown-item" href="/tools/benchmark">Benchmark</a>
<a class="dropdown-item" href="/tools/reference-test">Reference Test</a>
<a class="dropdown-item" href="/tools/mesh-debugger">Mesh Debugger</a>
</div>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/pcwalton/pathfinder">Code</a>
</li>
</ul>
</div>
<a href="https://github.com/pcwalton/pathfinder" class="github-corner"
aria-label="View source on Github">
<svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="true">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path>
</svg>
</a>
</nav>

View File

@ -1,18 +0,0 @@
<div id="pf-rotate-slider-container">
<div id="pf-rotate-slider-card" class="card mb-4 pf-invisible">
<div class="card-body d-flex flex-row">
<form class="mr-3">
<input id="pf-rotate-slider" type="range" min="-3.14159" max="3.14159"
step="0.006136" value="0.0" autocomplete="off" class="form-control">
</form>
<button id="pf-rotate-close-button" type="button" class="close" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="pf-arrow-box"></div>
</div>
</div>
<button id="pf-rotate-button" type="button" title="Rotate"
class="btn btn-outline-secondary pf-toolbar-button">
<span class="pf-material-icons">rotate_left</span>
</button>

View File

@ -1,15 +0,0 @@
<!-- http://tobiasahlin.com/spinkit/ -->
<div class="sk-fading-circle pf-spinner-hidden pf-spinner">
<div class="sk-circle1 sk-circle"></div>
<div class="sk-circle2 sk-circle"></div>
<div class="sk-circle3 sk-circle"></div>
<div class="sk-circle4 sk-circle"></div>
<div class="sk-circle5 sk-circle"></div>
<div class="sk-circle6 sk-circle"></div>
<div class="sk-circle7 sk-circle"></div>
<div class="sk-circle8 sk-circle"></div>
<div class="sk-circle9 sk-circle"></div>
<div class="sk-circle10 sk-circle"></div>
<div class="sk-circle11 sk-circle"></div>
<div class="sk-circle12 sk-circle"></div>
</div>

View File

@ -1,9 +0,0 @@
<label for="{{id}}-select-on" class="col-form-label col-auto">{{title}}</label>
<div class="col-auto btn-group" id="{{id}}-buttons" data-toggle="buttons">
<label class="btn btn-outline-secondary active">
<input type="radio" name="{{id}}" id="{{id}}-select-on" autocomplete="off" checked>On
</label>
<label class="btn btn-outline-secondary">
<input type="radio" name="{{id}}" autocomplete="off" id="{{id}}-select-off">Off
</label>
</div>

View File

@ -1,188 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Reference Test &mdash; Pathfinder</title>
<meta charset="utf-8">
{{>partials/header.html}}
<script type="text/javascript" src="/js/pathfinder/reference-test.js"></script>
</head>
<body>
<svg id="pf-svg" xmlns="http://www.w3.org/2000/svg" width="400" height="300"></svg>
<div id="pf-outer-container" class="w-100">
<div class="w-100">
{{>partials/navbar.html isTool=true}}
</div>
<div id="pf-inner-container" class="container-fluid">
<div class="row">
<div id="pf-reference-test-sidebar" class="col p-3">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link" id="pf-font-test-suite-tab" data-toggle="tab"
href="#pf-font-test-suite" role="tab"
aria-controls="pf-font-test-suite" aria-selected="false">
Font Test Suite
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="pf-svg-test-suite-tab" data-toggle="tab"
href="#pf-svg-test-suite" role="tab"
aria-controls="pf-svg-test-suite" aria-selected="false">
SVG Test Suite
</a>
</li>
<li class="nav-item">
<a class="nav-link active" id="pf-font-custom-test-tab"
data-toggle="tab" href="#pf-font-custom-test" role="tab"
aria-controls="pf-font-custom-test"
aria-selected="true">
Custom Font Test
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="pf-svg-custom-test-tab"
data-toggle="tab" href="#pf-svg-custom-test" role="tab"
aria-controls="pf-svg-custom-test">
Custom SVG Test
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane pt-3" id="pf-font-test-suite" role="tabpanel"
aria-labelledby="pf-font-test-suite-tab">
<button id="pf-run-font-tests-button" class="btn btn-primary m-2">
Run Tests
</button>
<table id="pf-font-results-table"
class="table table-sm table-striped">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">Font</th>
<th scope="col">Char.</th>
<th scope="col">Size</th>
<th scope="col">AA</th>
<th scope="col">Subpixel</th>
<th scope="col">Reference</th>
<th scope="col">Expected SSIM</th>
<th scope="col">Actual SSIM</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="tab-pane pt-3" id="pf-svg-test-suite" role="tabpanel"
aria-labelledby="pf-svg-test-suite-tab">
<button id="pf-run-svg-tests-button" class="btn btn-primary m-2">
Run Tests
</button>
<table id="pf-svg-results-table"
class="table table-sm table-striped">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">SVG</th>
<th scope="col">AA</th>
<th scope="col">Reference</th>
<th scope="col">Expected SSIM</th>
<th scope="col">Actual SSIM</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="tab-pane show active p-3" id="pf-font-custom-test"
role="tabpanel" aria-labelledby="pf-font-custom-test-tab">
<form id="pf-font-custom-form">
<div id="pf-font-select-file-group" class="form-group">
<label for="pf-select-file">Font</label>
<select id="pf-select-file"
class="form-control custom-select">
</select>
</div>
<div class="form-group">
<label for="pf-font-size">Font Size</label>
<div class="input-group">
<input id="pf-font-size" type="number"
class="form-control" value="32">
<div class="input-group-addon">px</div>
</div>
</div>
<div class="form-group">
<label for="pf-character">Character</label>
<input id="pf-character" type="text" maxlength="1" class="form-control"
value="B">
</div>
<div class="form-group">
<label for="pf-font-reference-renderer">
Reference Renderer
</label>
<select id="pf-font-reference-renderer"
class="form-control custom-select">
<option value="freetype">FreeType</option>
<option value="core-graphics">Core Graphics</option>
</select>
</div>
<div class="form-group row justify-content-between">
{{>partials/switch.html id="pf-subpixel-aa"
title="Subpixel AA"}}
</div>
<div id="pf-aa-level-group" class="form-group">
<label for="pf-aa-level-select">Antialiasing</label>
<select id="pf-aa-level-select"
class="form-control custom-select">
<option value="none">None</option>
<option value="ssaa-4">4&times;SSAA</option>
<option value="xcaa" selected>XCAA</option>
</select>
</div>
<div id="pf-font-ssim-group" class="form-group">
<label for="pf-font-ssim-label">SSIM</label>
<div id="pf-font-ssim-label">&mdash;</div>
</div>
</form>
</div>
<div class="tab-pane p-3" id="pf-svg-custom-test"
role="tabpanel" aria-labelledby="pf-svg-custom-test-tab">
<form id="pf-svg-custom-form">
<div id="pf-svg-select-file-group" class="form-group">
<label for="pf-select-file">SVG</label>
</div>
<div class="form-group">
<label for="pf-svg-reference-renderer">
Reference Renderer
</label>
<select id="pf-svg-reference-renderer"
class="form-control custom-select">
<option value="pixman">Cairo/Pixman</option>
</select>
</div>
<div id="pf-svg-ssim-group" class="form-group">
<label for="pf-svg-ssim-label">SSIM</label>
<div id="pf-svg-ssim-label">&mdash;</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-auto d-flex flex-column">
<div class="row">
<canvas id="pf-reference-canvas"
class="border border-top-0 border-right-0"
width="250" height="200"></canvas>
</div>
<div class="row">
<canvas id="pf-canvas"
class="pf-no-autoresize border border-top-0 border-right-0"
width="250" height="200"></canvas>
</div>
<div class="row">
<canvas id="pf-difference-canvas"
class="border border-top-0 border-right-0"
width="250" height="200"></canvas>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,78 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>SVG Demo &mdash; Pathfinder</title>
<meta charset="utf-8">
{{>partials/header.html}}
<script type="text/javascript" src="/js/pathfinder/svg-demo.js"></script>
</head>
<body class="pf-unscrollable">
{{>partials/navbar.html isDemo=true}}
<canvas id="pf-canvas" class="pf-maximized-canvas" width="400" height="300"></canvas>
<svg id="pf-svg" xmlns="http://www.w3.org/2000/svg" width="400" height="300"></svg>
<div class="fixed-bottom mb-3 d-flex justify-content-between align-items-end pf-pointer-events-none">
<div class="rounded invisible container py-1 px-3 ml-3" id="pf-fps-label"></div>
<div id="pf-toolbar">
<div class="btn-group" role="group" aria-label="Zoom">
<button id="pf-zoom-out-button" type="button"
class="btn btn-outline-secondary pf-toolbar-button">
<span class="pf-material-icons">remove</span>
</button>
<button id="pf-zoom-in-button" type="button"
class="btn btn-outline-secondary pf-toolbar-button">
<span class="pf-material-icons">add</span>
</button>
</div>
{{>partials/rotate.html}}
<button id="pf-zoom-pulse-button" type="button" title="Pulse Zoom"
class="btn btn-outline-secondary pf-toolbar-button">
<span class="pf-material-icons">play_arrow</span>
</button>
<button id="pf-screenshot-button" type="button"
class="btn btn-outline-secondary pf-toolbar-button">
<span class="pf-material-icons">photo_camera</span>
</button>
<div id="pf-settings-container">
<div id="pf-settings" class="card mb-4 pf-invisible">
<div class="card-body">
<button id="pf-settings-close-button" type="button" class="close"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<form>
<div class="form-group">
<label for="pf-select-file">SVG document</label>
<select id="pf-select-file" class="form-control custom-select">
<option value="tiger" selected>Ghostscript Tiger</option>
<option value="logo">Pathfinder Logo</option>
<option value="icons">Material Design Icons</option>
<option value="load-custom">Load File&hellip;</option>
</select>
<input id="pf-file-select" type="file">
</div>
<div class="form-group">
<label for="pf-aa-level-select">Antialiasing</label>
<select id="pf-aa-level-select" class="form-control custom-select">
<option value="none">None</option>
<option value="ssaa-2">2&times;SSAA</option>
<option value="ssaa-4">4&times;SSAA</option>
<option value="ssaa-8">8&times;SSAA</option>
<option value="ssaa-16">16&times;SSAA</option>
<option value="xcaa" selected>XCAA</option>
</select>
</div>
</form>
</div>
<div class="pf-arrow-box"></div>
</div>
</div>
<button id="pf-settings-button" type="button"
class="btn btn-outline-secondary pf-toolbar-button" aria-expanded="false"
aria-controls="#pf-settings">
<span class="pf-material-icons">settings</span>
</button>
</div>
</div>
{{>partials/spinner.html}}
</body>
</html>

View File

@ -1,133 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Text Demo &mdash; Pathfinder</title>
<meta charset="utf-8">
{{>partials/header.html}}
<script type="text/javascript" src="/js/pathfinder/text-demo.js"></script>
</head>
<body class="pf-unscrollable">
{{>partials/navbar.html isDemo=true}}
<canvas id="pf-canvas" class="pf-maximized-canvas" width="400" height="300"></canvas>
<div class="fixed-bottom mb-3 d-flex justify-content-between align-items-end pf-pointer-events-none">
<div class="rounded invisible container py-1 px-3 ml-3" id="pf-fps-label"></div>
<div id="pf-toolbar">
<div class="btn-group" role="group" aria-label="Zoom">
<button id="pf-zoom-out-button" type="button"
class="btn btn-outline-secondary pf-toolbar-button">
<span class="pf-material-icons">remove</span>
</button>
<button id="pf-zoom-in-button" type="button"
class="btn btn-outline-secondary pf-toolbar-button">
<span class="pf-material-icons">add</span>
</button>
</div>
{{>partials/rotate.html}}
<button id="pf-zoom-pulse-button" type="button" title="Pulse Zoom"
class="btn btn-outline-secondary pf-toolbar-button">
<span class="pf-material-icons">play_arrow</span>
</button>
<button id="pf-screenshot-button" type="button"
class="btn btn-outline-secondary pf-toolbar-button">
<span class="pf-material-icons">photo_camera</span>
</button>
<div id="pf-settings-container">
<div id="pf-settings" class="card mb-4 pf-invisible">
<div class="card-body">
<button id="pf-settings-close-button" type="button" class="close"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<form>
<div class="form-group">
<label for="pf-select-file">Font</label>
<select id="pf-select-file" class="form-control custom-select">
<option value="eb-garamond" selected>EB Garamond</option>
<option value="open-sans">Open Sans</option>
<option value="nimbus-sans">Nimbus Sans</option>
<option value="inter-ui">Inter UI</option>
<option value="load-custom">Load File&hellip;</option>
</select>
<input id="pf-file-select" type="file">
</div>
<div class="form-group">
<label for="pf-aa-level-select">Antialiasing</label>
<select id="pf-aa-level-select" class="form-control custom-select">
<option value="none">None</option>
<option value="ssaa-2">2&times;SSAA</option>
<option value="ssaa-4">4&times;SSAA</option>
<option value="xcaa" selected>XCAA</option>
</select>
</div>
<div class="form-group">
<label for="pf-hinting-select">Hinting</label>
<select id="pf-hinting-select"
class="form-control custom-select">
<option value="none">None</option>
<option value="slight" selected>Slight</option>
</select>
</div>
<div class="form-group">
<label for="pf-subpixel-aa-select">Subpixel AA</label>
<select id="pf-subpixel-aa-select"
class="form-control custom-select">
<option value="none">None</option>
<option value="freetype" selected>FreeType-style</option>
<option value="core-graphics">Core Graphics-style</option>
</select>
</div>
<div class="form-group row justify-content-between">
{{>partials/switch.html id="pf-gamma-correction"
title="Gamma Correction"}}
</div>
<div class="form-group row justify-content-between">
{{>partials/switch.html id="pf-stem-darkening"
title="Stem Darkening"}}
</div>
<div class="form-group">
<label for="pf-embolden">Faux Bold</label>
<input class="form-control" id="pf-embolden" type="range"
min="-0.015" max="0.015" step="0.0005" value="0.0"
autocomplete="off">
</div>
</form>
</div>
<div class="pf-arrow-box"></div>
</div>
</div>
<button id="pf-settings-button" type="button"
class="btn btn-outline-secondary pf-toolbar-button" aria-expanded="false"
aria-controls="#pf-settings">
<span class="pf-material-icons">settings</span>
</button>
</div>
</div>
<div class="modal fade" id="pf-edit-text-modal" tabindex="-1" role="dialog"
aria-labelledby="pf-edit-text-label" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="pf-edit-text-label">Edit Text</h5>
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form>
<textarea id="pf-edit-text-area" class="form-control" rows="16">
</textarea>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
data-dismiss="modal">Cancel</button>
<button type="button"
class="btn btn-primary" id="pf-edit-text-ok-button">OK</button>
</div>
</div>
</div>
</div>
{{>partials/spinner.html}}
</body>
</html>

View File

@ -1,44 +0,0 @@
{
"name": "pathfinder-demo",
"version": "0.1.0",
"description": "Demo for Pathfinder 2",
"scripts": {
"build": "webpack",
"build-watch": "webpack --watch",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Patrick Walton <pcwalton@mimiga.net>",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@types/base64-js": "^1.2.5",
"@types/gl-matrix": "^2.4.0",
"@types/lodash": "^4.14.104",
"@types/node": "^8.9.4",
"@types/opentype.js": "0.0.0",
"@types/papaparse": "^4.1.33",
"@types/webgl-ext": "0.0.29",
"base64-js": "^1.2.3",
"bootstrap": "^4.0.0",
"gl-matrix": "^2.4.0",
"handlebars": "^4.0.11",
"handlebars-loader": "^1.6.0",
"handlebars-webpack-plugin": "^1.3.2",
"html-loader": "^0.5.5",
"image-ssim": "^0.2.0",
"jquery": "^3.3.1",
"lodash": "^4.17.5",
"opentype.js": "^0.7.3",
"papaparse": "^4.3.7",
"parse-color": "^1.0.0",
"path-data-polyfill.js": "^1.0.2",
"popper.js": "^1.13.0",
"rustdoc-webpack-plugin": "file:./build/rustdoc-webpack-plugin",
"ts-loader": "^2.3.7",
"typescript": "^2.7.2"
},
"devDependencies": {
"tslint": "^5.9.1",
"tslint-loader": "^3.5.3",
"webpack": "^3.11.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,167 +0,0 @@
// pathfinder/client/src/aa-strategy.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.
import * as glmatrix from 'gl-matrix';
import {createFramebuffer, createFramebufferColorTexture, UniformMap} from './gl-utils';
import {createFramebufferDepthTexture} from './gl-utils';
import {Renderer} from './renderer';
import {unwrapNull} from './utils';
import {DemoView} from './view';
const SUBPIXEL_AA_KERNELS: {readonly [kind in SubpixelAAType]: glmatrix.vec4} = {
// These intentionally do not precisely match what Core Graphics does (a Lanczos function),
// because we don't want any ringing artefacts.
'core-graphics': glmatrix.vec4.clone([0.033165660, 0.102074051, 0.221434336, 0.286651906]),
'freetype': glmatrix.vec4.clone([0.0, 0.031372549, 0.301960784, 0.337254902]),
'none': glmatrix.vec4.clone([0.0, 0.0, 0.0, 1.0]),
};
export type AntialiasingStrategyName = 'none' | 'ssaa' | 'xcaa';
export type DirectRenderingMode = 'none' | 'conservative' | 'color';
export type SubpixelAAType = 'none' | 'freetype' | 'core-graphics';
export type GammaCorrectionMode = 'off' | 'on';
export type StemDarkeningMode = 'none' | 'dark';
export interface TileInfo {
size: glmatrix.vec2;
position: glmatrix.vec2;
}
export abstract class AntialiasingStrategy {
// The type of direct rendering that should occur, if any.
abstract readonly directRenderingMode: DirectRenderingMode;
// How many rendering passes this AA strategy requires.
abstract readonly passCount: number;
protected subpixelAA: SubpixelAAType;
constructor(subpixelAA: SubpixelAAType) {
this.subpixelAA = subpixelAA;
}
// Prepares any OpenGL data. This is only called on startup and canvas resize.
init(renderer: Renderer): void {
this.setFramebufferSize(renderer);
}
// Uploads any mesh data. This is called whenever a new set of meshes is supplied.
abstract attachMeshes(renderer: Renderer): void;
// This is called whenever the framebuffer has changed.
abstract setFramebufferSize(renderer: Renderer): void;
// Returns the transformation matrix that should be applied when directly rendering.
abstract get transform(): glmatrix.mat4;
// Called before rendering.
//
// Typically, this redirects rendering to a framebuffer of some sort.
abstract prepareForRendering(renderer: Renderer): void;
// Called before directly rendering.
//
// Typically, this redirects rendering to a framebuffer of some sort.
abstract prepareForDirectRendering(renderer: Renderer): void;
// Called before directly rendering a single object.
abstract prepareToRenderObject(renderer: Renderer, objectIndex: number): void;
abstract finishDirectlyRenderingObject(renderer: Renderer, objectIndex: number): void;
// Called after direct rendering.
//
// This usually performs the actual antialiasing.
abstract antialiasObject(renderer: Renderer, objectIndex: number): void;
// Called after antialiasing each object.
abstract finishAntialiasingObject(renderer: Renderer, objectIndex: number): void;
// Called before rendering each object directly.
abstract resolveAAForObject(renderer: Renderer, objectIndex: number): void;
// Called after antialiasing.
//
// This usually blits to the real framebuffer.
abstract resolve(pass: number, renderer: Renderer): void;
setSubpixelAAKernelUniform(renderer: Renderer, uniforms: UniformMap): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
const kernel = SUBPIXEL_AA_KERNELS[this.subpixelAA];
gl.uniform4f(uniforms.uKernel, kernel[0], kernel[1], kernel[2], kernel[3]);
}
worldTransformForPass(renderer: Renderer, pass: number): glmatrix.mat4 {
return glmatrix.mat4.create();
}
}
export class NoAAStrategy extends AntialiasingStrategy {
framebufferSize: glmatrix.vec2;
get passCount(): number {
return 1;
}
constructor(level: number, subpixelAA: SubpixelAAType) {
super(subpixelAA);
this.framebufferSize = glmatrix.vec2.create();
}
attachMeshes(renderer: Renderer) {}
setFramebufferSize(renderer: Renderer) {
this.framebufferSize = renderer.destAllocatedSize;
}
get transform(): glmatrix.mat4 {
return glmatrix.mat4.create();
}
prepareForRendering(renderer: Renderer): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
gl.bindFramebuffer(gl.FRAMEBUFFER, renderer.destFramebuffer);
renderer.setDrawViewport();
gl.disable(gl.SCISSOR_TEST);
}
prepareForDirectRendering(renderer: Renderer): void {}
prepareToRenderObject(renderer: Renderer, objectIndex: number): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
gl.bindFramebuffer(gl.FRAMEBUFFER, renderer.destFramebuffer);
renderer.setDrawViewport();
gl.disable(gl.SCISSOR_TEST);
}
finishDirectlyRenderingObject(renderer: Renderer, objectIndex: number): void {}
antialiasObject(renderer: Renderer, objectIndex: number): void {}
finishAntialiasingObject(renderer: Renderer, objectIndex: number): void {}
resolveAAForObject(renderer: Renderer): void {}
resolve(pass: number, renderer: Renderer): void {}
get directRenderingMode(): DirectRenderingMode {
return 'color';
}
}

View File

@ -1,398 +0,0 @@
// pathfinder/client/src/app-controller.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.
import {AntialiasingStrategyName, GammaCorrectionMode, StemDarkeningMode} from "./aa-strategy";
import {SubpixelAAType} from "./aa-strategy";
import {FilePickerView} from "./file-picker";
import {ShaderLoader, ShaderMap, ShaderProgramSource} from './shader-loader';
import {expectNotNull, unwrapNull, unwrapUndef} from './utils';
import {DemoView, Timings, TIMINGS} from "./view";
const AREA_LUT_URI: string = "/textures/area-lut.png";
const GAMMA_LUT_URI: string = "/textures/gamma-lut.png";
const SWITCHES: SwitchMap = {
gammaCorrection: {
defaultValue: 'on',
id: 'pf-gamma-correction',
offValue: 'off',
onValue: 'on',
switchInputsName: 'gammaCorrectionSwitchInputs',
},
stemDarkening: {
defaultValue: 'dark',
id: 'pf-stem-darkening',
offValue: 'none',
onValue: 'dark',
switchInputsName: 'stemDarkeningSwitchInputs',
},
};
interface SwitchDescriptor {
id: string;
switchInputsName: keyof Switches;
onValue: string;
offValue: string;
defaultValue: string;
}
interface SwitchMap {
gammaCorrection: SwitchDescriptor;
stemDarkening: SwitchDescriptor;
}
export interface AAOptions {
gammaCorrection: GammaCorrectionMode;
stemDarkening: StemDarkeningMode;
subpixelAA: SubpixelAAType;
}
export interface SwitchInputs {
on: HTMLInputElement;
off: HTMLInputElement;
}
interface Switches {
gammaCorrectionSwitchInputs: SwitchInputs | null;
stemDarkeningSwitchInputs: SwitchInputs | null;
}
export abstract class AppController {
protected canvas!: HTMLCanvasElement;
protected selectFileElement: HTMLSelectElement | null = null;
protected screenshotButton: HTMLButtonElement | null = null;
start(): void {
this.selectFileElement = document.getElementById('pf-select-file') as HTMLSelectElement |
null;
}
protected loadInitialFile(builtinFileURI: string): void {
if (this.selectFileElement != null) {
const selectedOption = this.selectFileElement.selectedOptions[0] as HTMLOptionElement;
this.fetchFile(selectedOption.value, builtinFileURI);
} else {
this.fetchFile(this.defaultFile, builtinFileURI);
}
}
protected fetchFile(file: string, builtinFileURI: string): Promise<void> {
return new Promise(resolve => {
window.fetch(`${builtinFileURI}/${file}`)
.then(response => response.arrayBuffer())
.then(data => {
this.fileLoaded(data, file);
resolve();
});
});
}
protected abstract fileLoaded(data: ArrayBuffer, builtinName: string | null): void;
protected abstract get defaultFile(): string;
}
export abstract class DemoAppController<View extends DemoView> extends AppController
implements Switches {
view!: Promise<View>;
gammaCorrectionSwitchInputs: SwitchInputs | null = null;
stemDarkeningSwitchInputs: SwitchInputs | null = null;
protected abstract readonly builtinFileURI: string;
protected filePickerView: FilePickerView | null = null;
protected aaLevelSelect: HTMLSelectElement | null = null;
protected subpixelAASelect: HTMLSelectElement | null = null;
private fpsLabel: HTMLElement | null = null;
constructor() {
super();
}
start() {
super.start();
this.initPopup('pf-settings', 'pf-settings-button', 'pf-settings-close-button');
this.initPopup('pf-rotate-slider-card', 'pf-rotate-button', 'pf-rotate-close-button');
const screenshotButton = document.getElementById('pf-screenshot-button') as
HTMLButtonElement | null;
if (screenshotButton != null) {
screenshotButton.addEventListener('click', () => {
this.view.then(view => view.queueScreenshot());
}, false);
}
const zoomInButton = document.getElementById('pf-zoom-in-button') as HTMLButtonElement |
null;
if (zoomInButton != null) {
zoomInButton.addEventListener('click', () => {
this.view.then(view => view.zoomIn());
}, false);
}
const zoomOutButton = document.getElementById('pf-zoom-out-button') as HTMLButtonElement |
null;
if (zoomOutButton != null) {
zoomOutButton.addEventListener('click', () => {
this.view.then(view => view.zoomOut());
}, false);
}
const zoomPulseButton = document.getElementById('pf-zoom-pulse-button') as
HTMLButtonElement | null;
if (zoomPulseButton != null) {
zoomPulseButton.addEventListener('click', () => {
this.view.then(view => view.zoomPulse());
}, false);
}
const rotateSlider = document.getElementById('pf-rotate-slider') as HTMLInputElement |
null;
if (rotateSlider != null) {
rotateSlider.addEventListener('input', event => {
this.view.then(view => {
view.rotate((event.target as HTMLInputElement).valueAsNumber);
});
}, false);
}
const vrButton = document.getElementById('pf-vr') as HTMLInputElement |
null;
if (vrButton != null) {
vrButton.addEventListener('click', event => {
this.view.then(view => {
view.enterVR();
});
}, false);
}
this.filePickerView = FilePickerView.create();
if (this.filePickerView != null) {
this.filePickerView.onFileLoaded = fileData => this.fileLoaded(fileData, null);
}
if (this.selectFileElement != null) {
this.selectFileElement
.addEventListener('click', event => this.fileSelectionChanged(event), false);
}
this.fpsLabel = document.getElementById('pf-fps-label');
const shaderLoader = new ShaderLoader;
shaderLoader.load();
const areaLUTPromise = this.loadTexture(AREA_LUT_URI);
const gammaLUTPromise = this.loadTexture(GAMMA_LUT_URI);
const promises: any[] = [
areaLUTPromise,
gammaLUTPromise,
shaderLoader.common,
shaderLoader.shaders,
];
this.view = Promise.all(promises).then(assets => {
return this.createView(assets[0], assets[1], assets[2], assets[3]);
});
this.aaLevelSelect = document.getElementById('pf-aa-level-select') as
(HTMLSelectElement | null);
if (this.aaLevelSelect != null)
this.aaLevelSelect.addEventListener('change', () => this.updateAALevel(), false);
this.subpixelAASelect = document.getElementById('pf-subpixel-aa-select') as
(HTMLSelectElement | null);
if (this.subpixelAASelect != null)
this.subpixelAASelect.addEventListener('change', () => this.updateAALevel(), false);
// The event listeners here use `window.setTimeout()` because jQuery won't fire the "live"
// click listener that Bootstrap sets up until the event bubbles up to the document. This
// click listener is what toggles the `checked` attribute, so we have to wait until it
// fires before updating the antialiasing settings.
for (const switchName of Object.keys(SWITCHES) as Array<keyof SwitchMap>) {
const switchInputsName = SWITCHES[switchName].switchInputsName;
const switchID = SWITCHES[switchName].id;
const switchOnInput = document.getElementById(`${switchID}-select-on`);
const switchOffInput = document.getElementById(`${switchID}-select-off`);
if (switchOnInput != null && switchOffInput != null) {
this[switchInputsName] = {
off: switchOffInput as HTMLInputElement,
on: switchOnInput as HTMLInputElement,
};
} else {
this[switchInputsName] = null;
}
const buttons = document.getElementById(`${switchID}-buttons`) as HTMLElement | null;
if (buttons == null)
continue;
buttons.addEventListener('click', () => {
window.setTimeout(() => this.updateAALevel(), 0);
}, false);
}
this.updateAALevel();
}
newTimingsReceived(timings: Partial<Timings>) {
if (this.fpsLabel == null)
return;
while (this.fpsLabel.lastChild != null)
this.fpsLabel.removeChild(this.fpsLabel.lastChild);
for (const timing of Object.keys(timings) as Array<keyof Timings>) {
const tr = document.createElement('div');
tr.classList.add('row');
const keyTD = document.createElement('div');
const valueTD = document.createElement('div');
keyTD.classList.add('col');
valueTD.classList.add('col');
keyTD.appendChild(document.createTextNode(TIMINGS[timing]));
valueTD.appendChild(document.createTextNode(timings[timing] + " ms"));
tr.appendChild(keyTD);
tr.appendChild(valueTD);
this.fpsLabel.appendChild(tr);
}
this.fpsLabel.classList.remove('invisible');
}
protected updateAALevel(): Promise<void> {
let aaType: AntialiasingStrategyName, aaLevel: number;
if (this.aaLevelSelect != null) {
const selectedOption = this.aaLevelSelect.selectedOptions[0];
const aaValues = unwrapNull(/^([a-z-]+)(?:-([0-9]+))?$/.exec(selectedOption.value));
aaType = aaValues[1] as AntialiasingStrategyName;
aaLevel = aaValues[2] === "" ? 1 : parseInt(aaValues[2], 10);
} else {
aaType = 'none';
aaLevel = 0;
}
this.updateUIForAALevelChange(aaType, aaLevel);
const aaOptions: Partial<AAOptions> = {};
for (const switchName of Object.keys(SWITCHES) as Array<keyof SwitchMap>) {
const switchDescriptor = SWITCHES[switchName];
const switchInputsName = switchDescriptor.switchInputsName;
const switchInputs = this[switchInputsName];
if (switchInputs == null)
aaOptions[switchName] = switchDescriptor.defaultValue as any;
else if (switchInputs.on.checked && !switchInputs.on.disabled)
aaOptions[switchName] = switchDescriptor.onValue as any;
else
aaOptions[switchName] = switchDescriptor.offValue as any;
}
if (this.subpixelAASelect != null) {
const selectedOption = this.subpixelAASelect.selectedOptions[0];
aaOptions.subpixelAA = selectedOption.value as SubpixelAAType;
} else {
aaOptions.subpixelAA = 'none';
}
return this.view.then(view => {
view.setAntialiasingOptions(aaType, aaLevel, aaOptions as AAOptions);
});
}
protected updateUIForAALevelChange(aaType: AntialiasingStrategyName, aaLevel: number): void {
// Overridden by subclasses.
}
protected abstract createView(areaLUT: HTMLImageElement,
gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>):
View;
private initPopup(cardID: string, popupButtonID: string, closeButtonID: string): void {
const card = document.getElementById(cardID) as HTMLElement | null;
const button = document.getElementById(popupButtonID) as HTMLButtonElement | null;
const closeButton = document.getElementById(closeButtonID) as HTMLButtonElement | null;
if (button != null) {
button.addEventListener('click', event => {
event.stopPropagation();
unwrapNull(card).classList.toggle('pf-invisible');
}, false);
}
if (closeButton != null) {
closeButton.addEventListener('click', () => {
unwrapNull(card).classList.add('pf-invisible');
}, false);
}
if (card == null)
return;
document.body.addEventListener('click', event => {
let element = event.target as Element | null;
while (element != null) {
if (element === card)
return;
element = element.parentElement;
}
card.classList.add('pf-invisible');
}, false);
}
private loadTexture(uri: string): Promise<HTMLImageElement> {
return window.fetch(uri)
.then(response => response.blob())
.then(blob => {
const imgElement = document.createElement('img');
imgElement.src = URL.createObjectURL(blob);
const promise: Promise<HTMLImageElement> = new Promise(resolve => {
imgElement.addEventListener('load', () => {
resolve(imgElement);
}, false);
});
return promise;
});
}
private fileSelectionChanged(event: Event): void {
const selectFileElement = event.currentTarget as HTMLSelectElement;
const selectedOption = selectFileElement.selectedOptions[0] as HTMLOptionElement;
if (selectedOption.value === 'load-custom' && this.filePickerView != null) {
this.filePickerView.open();
const oldSelectedIndex = selectFileElement.selectedIndex;
const newOption = document.createElement('option');
newOption.id = 'pf-custom-option-placeholder';
newOption.appendChild(document.createTextNode("Custom"));
selectFileElement.insertBefore(newOption, selectedOption);
selectFileElement.selectedIndex = oldSelectedIndex;
return;
}
// Remove the "Custom…" placeholder if it exists.
const placeholder = document.getElementById('pf-custom-option-placeholder');
if (placeholder != null)
selectFileElement.removeChild(placeholder);
// Fetch the file.
this.fetchFile(selectedOption.value, this.builtinFileURI);
}
}
export function setSwitchInputsValue(switchInputs: SwitchInputs, on: boolean): void {
switchInputs.on.checked = on;
switchInputs.off.checked = !on;
}

View File

@ -1,160 +0,0 @@
// pathfinder/client/src/atlas.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.
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import {setTextureParameters} from './gl-utils';
import {calculatePixelDescent, calculatePixelRectForGlyph, calculatePixelXMin, Hint} from './text';
import {PathfinderFont, UnitMetrics} from './text';
import {unwrapNull} from './utils';
import {RenderContext} from './view';
export const SUBPIXEL_GRANULARITY: number = 4;
export const ATLAS_SIZE: glmatrix.vec2 = glmatrix.vec2.fromValues(2048, 4096);
export class Atlas {
private _texture: WebGLTexture | null;
private _usedSize: glmatrix.vec2;
constructor() {
this._texture = null;
this._usedSize = glmatrix.vec2.create();
}
layoutGlyphs(glyphs: AtlasGlyph[],
font: PathfinderFont,
pixelsPerUnit: number,
rotationAngle: number,
hint: Hint,
emboldenAmount: glmatrix.vec2):
void {
let nextOrigin = glmatrix.vec2.fromValues(1.0, 1.0);
let shelfBottom = 2.0;
for (const glyph of glyphs) {
// Place the glyph, and advance the origin.
const metrics = font.metricsForGlyph(glyph.glyphKey.id);
if (metrics == null)
continue;
const unitMetrics = new UnitMetrics(metrics, rotationAngle, emboldenAmount);
glyph.setPixelLowerLeft(nextOrigin, unitMetrics, pixelsPerUnit);
let pixelOrigin = glyph.calculateSubpixelOrigin(pixelsPerUnit);
nextOrigin[0] = calculatePixelRectForGlyph(unitMetrics,
pixelOrigin,
pixelsPerUnit,
hint)[2] + 1.0;
// If the glyph overflowed the shelf, make a new one and reposition the glyph.
if (nextOrigin[0] > ATLAS_SIZE[0]) {
nextOrigin = glmatrix.vec2.clone([1.0, shelfBottom + 1.0]);
glyph.setPixelLowerLeft(nextOrigin, unitMetrics, pixelsPerUnit);
pixelOrigin = glyph.calculateSubpixelOrigin(pixelsPerUnit);
nextOrigin[0] = calculatePixelRectForGlyph(unitMetrics,
pixelOrigin,
pixelsPerUnit,
hint)[2] + 1.0;
}
// Grow the shelf as necessary.
const glyphBottom = calculatePixelRectForGlyph(unitMetrics,
pixelOrigin,
pixelsPerUnit,
hint)[3];
shelfBottom = Math.max(shelfBottom, glyphBottom + 1.0);
}
// FIXME(pcwalton): Could be more precise if we don't have a full row.
this._usedSize = glmatrix.vec2.clone([ATLAS_SIZE[0], shelfBottom]);
}
ensureTexture(renderContext: RenderContext): WebGLTexture {
if (this._texture != null)
return this._texture;
const gl = renderContext.gl;
const texture = unwrapNull(gl.createTexture());
this._texture = texture;
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D,
0,
gl.RGBA,
ATLAS_SIZE[0],
ATLAS_SIZE[1],
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
null);
setTextureParameters(gl, gl.NEAREST);
return texture;
}
get usedSize(): glmatrix.vec2 {
return this._usedSize;
}
}
export class AtlasGlyph {
readonly glyphStoreIndex: number;
readonly glyphKey: GlyphKey;
readonly origin: glmatrix.vec2;
constructor(glyphStoreIndex: number, glyphKey: GlyphKey) {
this.glyphStoreIndex = glyphStoreIndex;
this.glyphKey = glyphKey;
this.origin = glmatrix.vec2.create();
}
calculateSubpixelOrigin(pixelsPerUnit: number): glmatrix.vec2 {
const pixelOrigin = glmatrix.vec2.create();
glmatrix.vec2.scale(pixelOrigin, this.origin, pixelsPerUnit);
glmatrix.vec2.round(pixelOrigin, pixelOrigin);
if (this.glyphKey.subpixel != null)
pixelOrigin[0] += this.glyphKey.subpixel / SUBPIXEL_GRANULARITY;
return pixelOrigin;
}
setPixelLowerLeft(pixelLowerLeft: glmatrix.vec2, metrics: UnitMetrics, pixelsPerUnit: number):
void {
const pixelXMin = calculatePixelXMin(metrics, pixelsPerUnit);
const pixelDescent = calculatePixelDescent(metrics, pixelsPerUnit);
const pixelOrigin = glmatrix.vec2.clone([pixelLowerLeft[0] - pixelXMin,
pixelLowerLeft[1] - pixelDescent]);
this.setPixelOrigin(pixelOrigin, pixelsPerUnit);
}
private setPixelOrigin(pixelOrigin: glmatrix.vec2, pixelsPerUnit: number): void {
glmatrix.vec2.scale(this.origin, pixelOrigin, 1.0 / pixelsPerUnit);
}
get pathID(): number {
if (this.glyphKey.subpixel == null)
return this.glyphStoreIndex + 1;
return this.glyphStoreIndex * SUBPIXEL_GRANULARITY + this.glyphKey.subpixel + 1;
}
}
export class GlyphKey {
readonly id: number;
readonly subpixel: number | null;
constructor(id: number, subpixel: number | null) {
this.id = id;
this.subpixel = subpixel;
}
get sortKey(): number {
return this.subpixel == null ? this.id : this.id * SUBPIXEL_GRANULARITY + this.subpixel;
}
}

View File

@ -1,653 +0,0 @@
// pathfinder/client/src/benchmark.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.
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import * as opentype from "opentype.js";
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy";
import {SubpixelAAType} from "./aa-strategy";
import {AppController, DemoAppController, setSwitchInputsValue} from "./app-controller";
import PathfinderBufferTexture from './buffer-texture';
import {OrthographicCamera} from './camera';
import {UniformMap} from './gl-utils';
import {PathfinderMeshPack, PathfinderPackedMeshes} from "./meshes";
import {PathTransformBuffers, Renderer} from './renderer';
import {ShaderMap, ShaderProgramSource} from "./shader-loader";
import SSAAStrategy from './ssaa-strategy';
import {BUILTIN_SVG_URI, SVGLoader} from './svg-loader';
import {SVGRenderer} from './svg-renderer';
import {BUILTIN_FONT_URI, ExpandedMeshData, GlyphStore, PathfinderFont, TextFrame} from "./text";
import {computeStemDarkeningAmount, TextRun} from "./text";
import {assert, lerp, PathfinderError, unwrapNull, unwrapUndef} from "./utils";
import {DemoView, Timings} from "./view";
import {AdaptiveStencilMeshAAAStrategy} from './xcaa-strategy';
const STRING: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const DEFAULT_FONT: string = 'nimbus-sans';
const DEFAULT_SVG_FILE: string = 'tiger';
const TEXT_COLOR: number[] = [0, 0, 0, 255];
// In milliseconds.
const MIN_RUNTIME: number = 100;
const MAX_RUNTIME: number = 3000;
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
none: NoAAStrategy,
ssaa: SSAAStrategy,
xcaa: AdaptiveStencilMeshAAAStrategy,
};
interface BenchmarkModeMap<T> {
text: T;
svg: T;
}
type BenchmarkMode = 'text' | 'svg';
interface AntialiasingStrategyTable {
none: typeof NoAAStrategy;
ssaa: typeof SSAAStrategy;
xcaa: typeof AdaptiveStencilMeshAAAStrategy;
}
interface TestParameter {
start: number;
stop: number;
step: number;
}
const DISPLAY_HEADER_LABELS: BenchmarkModeMap<string[]> = {
svg: ["Size (px)", "GPU time (ms)"],
text: ["Font size (px)", "GPU time per glyph (µs)"],
};
const TEST_SIZES: BenchmarkModeMap<TestParameter> = {
svg: { start: 64, stop: 2048, step: 16 },
text: { start: 6, stop: 200, step: 1 },
};
class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
font: PathfinderFont | null = null;
textRun: TextRun | null = null;
svgLoader!: SVGLoader;
mode!: BenchmarkMode;
protected get defaultFile(): string {
if (this.mode === 'text')
return DEFAULT_FONT;
return DEFAULT_SVG_FILE;
}
protected get builtinFileURI(): string {
if (this.mode === 'text')
return BUILTIN_FONT_URI;
return BUILTIN_SVG_URI;
}
private optionsModal!: HTMLDivElement;
private resultsModal!: HTMLDivElement;
private resultsTableHeader!: HTMLTableSectionElement;
private resultsTableBody!: HTMLTableSectionElement;
private resultsPartitioningTimeLabel!: HTMLSpanElement;
private glyphStore!: GlyphStore;
private baseMeshes!: PathfinderMeshPack;
private expandedMeshes!: ExpandedMeshData;
private size!: number;
private currentRun!: number;
private startTime!: number;
private elapsedTimes!: ElapsedTime[];
private partitionTime!: number;
start(): void {
super.start();
this.mode = 'text';
this.optionsModal = unwrapNull(document.getElementById('pf-benchmark-modal')) as
HTMLDivElement;
this.resultsModal = unwrapNull(document.getElementById('pf-benchmark-results-modal')) as
HTMLDivElement;
this.resultsTableHeader =
unwrapNull(document.getElementById('pf-benchmark-results-table-header')) as
HTMLTableSectionElement;
this.resultsTableBody =
unwrapNull(document.getElementById('pf-benchmark-results-table-body')) as
HTMLTableSectionElement;
this.resultsPartitioningTimeLabel =
unwrapNull(document.getElementById('pf-benchmark-results-partitioning-time')) as
HTMLSpanElement;
const resultsSaveCSVButton =
unwrapNull(document.getElementById('pf-benchmark-results-save-csv-button'));
resultsSaveCSVButton.addEventListener('click', () => this.saveCSV(), false);
const resultsCloseButton =
unwrapNull(document.getElementById('pf-benchmark-results-close-button'));
resultsCloseButton.addEventListener('click', () => {
window.jQuery(this.resultsModal).modal('hide');
}, false);
const runBenchmarkButton = unwrapNull(document.getElementById('pf-run-benchmark-button'));
runBenchmarkButton.addEventListener('click', () => this.runBenchmark(), false);
const aaLevelFormGroup = unwrapNull(document.getElementById('pf-aa-level-form-group')) as
HTMLDivElement;
const benchmarkTextForm = unwrapNull(document.getElementById('pf-benchmark-text-form')) as
HTMLFormElement;
const benchmarkSVGForm = unwrapNull(document.getElementById('pf-benchmark-svg-form')) as
HTMLFormElement;
window.jQuery(this.optionsModal).modal();
const benchmarkTextTab = document.getElementById('pf-benchmark-text-tab') as
HTMLAnchorElement;
const benchmarkSVGTab = document.getElementById('pf-benchmark-svg-tab') as
HTMLAnchorElement;
window.jQuery(benchmarkTextTab).on('shown.bs.tab', event => {
this.mode = 'text';
if (aaLevelFormGroup.parentElement != null)
aaLevelFormGroup.parentElement.removeChild(aaLevelFormGroup);
benchmarkTextForm.insertBefore(aaLevelFormGroup, benchmarkTextForm.firstChild);
this.modeChanged();
});
window.jQuery(benchmarkSVGTab).on('shown.bs.tab', event => {
this.mode = 'svg';
if (aaLevelFormGroup.parentElement != null)
aaLevelFormGroup.parentElement.removeChild(aaLevelFormGroup);
benchmarkSVGForm.insertBefore(aaLevelFormGroup, benchmarkSVGForm.firstChild);
this.modeChanged();
});
this.loadInitialFile(this.builtinFileURI);
}
protected fileLoaded(fileData: ArrayBuffer, builtinName: string | null): void {
switch (this.mode) {
case 'text':
this.textFileLoaded(fileData, builtinName);
return;
case 'svg':
this.svgFileLoaded(fileData, builtinName);
return;
}
}
protected createView(areaLUT: HTMLImageElement,
gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>):
BenchmarkTestView {
return new BenchmarkTestView(this, areaLUT, gammaLUT, commonShaderSource, shaderSources);
}
private modeChanged(): void {
this.loadInitialFile(this.builtinFileURI);
if (this.aaLevelSelect != null)
this.aaLevelSelect.selectedIndex = 0;
if (this.subpixelAASelect != null)
this.subpixelAASelect.selectedIndex = 0;
this.updateAALevel();
}
private textFileLoaded(fileData: ArrayBuffer, builtinName: string | null): void {
const font = new PathfinderFont(fileData, builtinName);
this.font = font;
const textRun = new TextRun(STRING, [0, 0], font);
textRun.layout();
this.textRun = textRun;
const textFrame = new TextFrame([textRun], font);
const glyphIDs = textFrame.allGlyphIDs;
glyphIDs.sort((a, b) => a - b);
this.glyphStore = new GlyphStore(font, glyphIDs);
this.glyphStore.partition().then(result => {
this.baseMeshes = result.meshes;
const partitionTime = result.time / this.glyphStore.glyphIDs.length * 1e6;
const timeLabel = this.resultsPartitioningTimeLabel;
while (timeLabel.firstChild != null)
timeLabel.removeChild(timeLabel.firstChild);
timeLabel.appendChild(document.createTextNode("" + partitionTime));
const expandedMeshes = textFrame.expandMeshes(this.baseMeshes, glyphIDs);
this.expandedMeshes = expandedMeshes;
this.view.then(view => {
view.recreateRenderer();
view.attachMeshes([expandedMeshes.meshes]);
});
});
}
private svgFileLoaded(fileData: ArrayBuffer, builtinName: string | null): void {
this.svgLoader = new SVGLoader;
this.svgLoader.loadFile(fileData);
this.svgLoader.partition().then(meshes => {
this.view.then(view => {
view.recreateRenderer();
view.attachMeshes([new PathfinderPackedMeshes(meshes)]);
view.initCameraBounds(this.svgLoader.svgViewBox);
});
});
}
private reset(): void {
this.currentRun = 0;
this.startTime = Date.now();
}
private runBenchmark(): void {
window.jQuery(this.optionsModal).modal('hide');
this.reset();
this.elapsedTimes = [];
this.size = TEST_SIZES[this.mode].start;
this.view.then(view => this.runOneBenchmarkTest(view));
}
private runDone(): boolean {
const totalElapsedTime = Date.now() - this.startTime;
if (totalElapsedTime < MIN_RUNTIME)
return false;
if (totalElapsedTime >= MAX_RUNTIME)
return true;
// Compute median absolute devation.
const elapsedTime = unwrapUndef(_.last(this.elapsedTimes));
elapsedTime.times.sort((a, b) => a - b);
const median = unwrapNull(computeMedian(elapsedTime.times));
const absoluteDeviations = elapsedTime.times.map(time => Math.abs(time - median));
absoluteDeviations.sort((a, b) => a - b);
const medianAbsoluteDeviation = unwrapNull(computeMedian(absoluteDeviations));
const medianAbsoluteDeviationFraction = medianAbsoluteDeviation / median;
return medianAbsoluteDeviationFraction <= 0.01;
}
private runOneBenchmarkTest(view: BenchmarkTestView): void {
const renderedPromise = new Promise<number>((resolve, reject) => {
view.renderingPromiseCallback = resolve;
view.size = this.size;
});
renderedPromise.then(elapsedTime => {
if (this.currentRun === 0)
this.elapsedTimes.push(new ElapsedTime(this.size));
unwrapUndef(_.last(this.elapsedTimes)).times.push(elapsedTime);
this.currentRun++;
if (this.runDone()) {
this.reset();
if (this.size >= TEST_SIZES[this.mode].stop) {
this.showResults();
return;
}
this.size += TEST_SIZES[this.mode].step;
}
this.runOneBenchmarkTest(view);
});
}
private showResults(): void {
while (this.resultsTableHeader.lastChild != null)
this.resultsTableHeader.removeChild(this.resultsTableHeader.lastChild);
while (this.resultsTableBody.lastChild != null)
this.resultsTableBody.removeChild(this.resultsTableBody.lastChild);
const tr = document.createElement('tr');
for (const headerLabel of DISPLAY_HEADER_LABELS[this.mode]) {
const th = document.createElement('th');
th.appendChild(document.createTextNode(headerLabel));
tr.appendChild(th);
}
this.resultsTableHeader.appendChild(tr);
for (const elapsedTime of this.elapsedTimes) {
const tr = document.createElement('tr');
const sizeTH = document.createElement('th');
const timeTD = document.createElement('td');
sizeTH.appendChild(document.createTextNode("" + elapsedTime.size));
const time = this.mode === 'svg' ? elapsedTime.timeInMS : elapsedTime.time;
timeTD.appendChild(document.createTextNode("" + time));
sizeTH.scope = 'row';
tr.appendChild(sizeTH);
tr.appendChild(timeTD);
this.resultsTableBody.appendChild(tr);
}
window.jQuery(this.resultsModal).modal();
}
private saveCSV(): void {
let output = "Size,Time\n";
for (const elapsedTime of this.elapsedTimes) {
const time = this.mode === 'svg' ? elapsedTime.timeInMS : elapsedTime.time;
output += `${elapsedTime.size},${time}\n`;
}
// https://stackoverflow.com/a/30832210
const file = new Blob([output], {type: 'text/csv'});
const a = document.createElement('a');
const url = URL.createObjectURL(file);
a.href = url;
a.download = "pathfinder-benchmark-results.csv";
document.body.appendChild(a);
a.click();
window.setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 0);
}
}
class BenchmarkTestView extends DemoView {
renderer!: BenchmarkTextRenderer | BenchmarkSVGRenderer;
readonly appController: BenchmarkAppController;
renderingPromiseCallback: ((time: number) => void) | null = null;
get camera(): OrthographicCamera {
return this.renderer.camera;
}
set size(newSize: number) {
if (this.renderer instanceof BenchmarkTextRenderer) {
this.renderer.pixelsPerEm = newSize;
} else if (this.renderer instanceof BenchmarkSVGRenderer) {
const camera = this.renderer.camera;
camera.zoomToSize(newSize);
camera.center();
}
}
constructor(appController: BenchmarkAppController,
areaLUT: HTMLImageElement,
gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>) {
super(areaLUT, gammaLUT, commonShaderSource, shaderSources);
this.appController = appController;
this.recreateRenderer();
this.resizeToFit(true);
}
recreateRenderer(): void {
switch (this.appController.mode) {
case 'svg':
this.renderer = new BenchmarkSVGRenderer(this);
break;
case 'text':
this.renderer = new BenchmarkTextRenderer(this);
break;
}
}
initCameraBounds(viewBox: glmatrix.vec4): void {
if (this.renderer instanceof BenchmarkSVGRenderer)
this.renderer.initCameraBounds(viewBox);
}
protected renderingFinished(): void {
if (this.renderingPromiseCallback == null)
return;
const appController = this.appController;
let time = this.renderer.lastTimings.rendering * 1000.0;
if (appController.mode === 'text')
time /= unwrapNull(appController.textRun).glyphIDs.length;
this.renderingPromiseCallback(time);
}
}
class BenchmarkTextRenderer extends Renderer {
renderContext!: BenchmarkTestView;
camera: OrthographicCamera;
needsStencil: boolean = false;
isMulticolor: boolean = false;
get destFramebuffer(): WebGLFramebuffer | null {
return null;
}
get bgColor(): glmatrix.vec4 {
return glmatrix.vec4.clone([1.0, 1.0, 1.0, 0.0]);
}
get fgColor(): glmatrix.vec4 {
return glmatrix.vec4.clone([0.0, 0.0, 0.0, 1.0]);
}
get destAllocatedSize(): glmatrix.vec2 {
const canvas = this.renderContext.canvas;
return glmatrix.vec2.clone([canvas.width, canvas.height]);
}
get destUsedSize(): glmatrix.vec2 {
return this.destAllocatedSize;
}
get allowSubpixelAA(): boolean {
return true;
}
get emboldenAmount(): glmatrix.vec2 {
return this.stemDarkeningAmount;
}
get pixelsPerEm(): number {
return this._pixelsPerEm;
}
set pixelsPerEm(newPixelsPerEm: number) {
this._pixelsPerEm = newPixelsPerEm;
this.uploadPathTransforms(1);
this.renderContext.setDirty();
}
protected get usedSizeFactor(): glmatrix.vec2 {
return glmatrix.vec2.clone([1.0, 1.0]);
}
protected get worldTransform(): glmatrix.mat4 {
const canvas = this.renderContext.canvas;
const transform = glmatrix.mat4.create();
const translation = this.camera.translation;
glmatrix.mat4.translate(transform, transform, [-1.0, -1.0, 0.0]);
glmatrix.mat4.scale(transform, transform, [2.0 / canvas.width, 2.0 / canvas.height, 1.0]);
glmatrix.mat4.translate(transform, transform, [translation[0], translation[1], 0]);
glmatrix.mat4.scale(transform, transform, [this.camera.scale, this.camera.scale, 1.0]);
const pixelsPerUnit = this.pixelsPerUnit;
glmatrix.mat4.scale(transform, transform, [pixelsPerUnit, pixelsPerUnit, 1.0]);
return transform;
}
protected get objectCount(): number {
return this.meshBuffers == null ? 0 : this.meshBuffers.length;
}
private _pixelsPerEm: number = 32.0;
private get pixelsPerUnit(): number {
const font = unwrapNull(this.renderContext.appController.font);
return this._pixelsPerEm / font.opentypeFont.unitsPerEm;
}
private get stemDarkeningAmount(): glmatrix.vec2 {
return computeStemDarkeningAmount(this._pixelsPerEm, this.pixelsPerUnit);
}
constructor(renderContext: BenchmarkTestView) {
super(renderContext);
this.camera = new OrthographicCamera(renderContext.canvas, { fixed: true });
this.camera.onPan = () => renderContext.setDirty();
this.camera.onZoom = () => renderContext.setDirty();
}
attachMeshes(meshes: PathfinderPackedMeshes[]): void {
super.attachMeshes(meshes);
this.uploadPathColors(1);
this.uploadPathTransforms(1);
}
pathCountForObject(objectIndex: number): number {
return STRING.length;
}
pathBoundingRects(objectIndex: number): Float32Array {
const appController = this.renderContext.appController;
const font = unwrapNull(appController.font);
const boundingRects = new Float32Array((STRING.length + 1) * 4);
for (let glyphIndex = 0; glyphIndex < STRING.length; glyphIndex++) {
const glyphID = unwrapNull(appController.textRun).glyphIDs[glyphIndex];
const metrics = font.metricsForGlyph(glyphID);
if (metrics == null)
continue;
boundingRects[(glyphIndex + 1) * 4 + 0] = metrics.xMin;
boundingRects[(glyphIndex + 1) * 4 + 1] = metrics.yMin;
boundingRects[(glyphIndex + 1) * 4 + 2] = metrics.xMax;
boundingRects[(glyphIndex + 1) * 4 + 3] = metrics.yMax;
}
return boundingRects;
}
setHintsUniform(uniforms: UniformMap): void {
this.renderContext.gl.uniform4f(uniforms.uHints, 0, 0, 0, 0);
}
pathTransformsForObject(objectIndex: number): PathTransformBuffers<Float32Array> {
const appController = this.renderContext.appController;
const canvas = this.renderContext.canvas;
const font = unwrapNull(appController.font);
const pathTransforms = this.createPathTransformBuffers(STRING.length);
let currentX = 0, currentY = 0;
const availableWidth = canvas.width / this.pixelsPerUnit;
const lineHeight = font.opentypeFont.lineHeight();
for (let glyphIndex = 0; glyphIndex < STRING.length; glyphIndex++) {
const glyphID = unwrapNull(appController.textRun).glyphIDs[glyphIndex];
pathTransforms.st.set([1, 1, currentX, currentY], (glyphIndex + 1) * 4);
currentX += font.opentypeFont.glyphs.get(glyphID).advanceWidth;
if (currentX > availableWidth) {
currentX = 0;
currentY += lineHeight;
}
}
return pathTransforms;
}
protected createAAStrategy(aaType: AntialiasingStrategyName,
aaLevel: number,
subpixelAA: SubpixelAAType):
AntialiasingStrategy {
return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA);
}
protected compositeIfNecessary(): void {}
protected updateTimings(timings: Timings): void {
// TODO(pcwalton)
}
protected pathColorsForObject(objectIndex: number): Uint8Array {
const pathColors = new Uint8Array(4 * (STRING.length + 1));
for (let pathIndex = 0; pathIndex < STRING.length; pathIndex++)
pathColors.set(TEXT_COLOR, (pathIndex + 1) * 4);
return pathColors;
}
protected directCurveProgramName(): keyof ShaderMap<void> {
return 'directCurve';
}
protected directInteriorProgramName(): keyof ShaderMap<void> {
return 'directInterior';
}
}
class BenchmarkSVGRenderer extends SVGRenderer {
renderContext!: BenchmarkTestView;
protected get loader(): SVGLoader {
return this.renderContext.appController.svgLoader;
}
protected get canvas(): HTMLCanvasElement {
return this.renderContext.canvas;
}
constructor(renderContext: BenchmarkTestView) {
super(renderContext, {sizeToFit: false});
}
}
function computeMedian(values: number[]): number | null {
if (values.length === 0)
return null;
const mid = values.length / 2;
if (values.length % 2 === 1)
return values[Math.floor(mid)];
return lerp(values[mid - 1], values[mid], 0.5);
}
class ElapsedTime {
readonly size: number;
readonly times: number[];
constructor(size: number) {
this.size = size;
this.times = [];
}
get time(): number {
const median = computeMedian(this.times);
return median == null ? 0.0 : median;
}
get timeInMS(): number {
return this.time / 1000.0;
}
}
function main() {
const controller = new BenchmarkAppController;
window.addEventListener('load', () => controller.start(), false);
}
main();

View File

@ -1,137 +0,0 @@
// pathfinder/client/src/buffer-texture.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.
import * as glmatrix from 'gl-matrix';
import {setTextureParameters, UniformMap} from './gl-utils';
import {assert, expectNotNull} from './utils';
export default class PathfinderBufferTexture {
readonly texture: WebGLTexture;
readonly uniformName: string;
private size: glmatrix.vec2;
private glType: number;
private destroyed: boolean;
constructor(gl: WebGLRenderingContext, uniformName: string) {
this.texture = expectNotNull(gl.createTexture(), "Failed to create buffer texture!");
this.size = glmatrix.vec2.create();
this.uniformName = uniformName;
this.glType = 0;
this.destroyed = false;
}
destroy(gl: WebGLRenderingContext): void {
assert(!this.destroyed, "Buffer texture destroyed!");
gl.deleteTexture(this.texture);
this.destroyed = true;
}
upload(gl: WebGLRenderingContext, data: Float32Array | Uint8Array): void {
assert(!this.destroyed, "Buffer texture destroyed!");
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.texture);
const glType = data instanceof Float32Array ? gl.FLOAT : gl.UNSIGNED_BYTE;
const areaNeeded = Math.ceil(data.length / 4);
if (glType !== this.glType || areaNeeded > this.area) {
const sideLength = nextPowerOfTwo(Math.ceil(Math.sqrt(areaNeeded)));
this.size = glmatrix.vec2.clone([sideLength, sideLength]);
this.glType = glType;
gl.texImage2D(gl.TEXTURE_2D,
0,
gl.RGBA,
sideLength,
sideLength,
0,
gl.RGBA,
glType,
null);
setTextureParameters(gl, gl.NEAREST);
}
const mainDimensions = glmatrix.vec4.clone([
0,
0,
this.size[0],
(areaNeeded / this.size[0]) | 0,
]);
const remainderDimensions = glmatrix.vec4.clone([
0,
mainDimensions[3],
areaNeeded % this.size[0],
1,
]);
const splitIndex = mainDimensions[2] * mainDimensions[3] * 4;
if (mainDimensions[2] > 0 && mainDimensions[3] > 0) {
gl.texSubImage2D(gl.TEXTURE_2D,
0,
mainDimensions[0],
mainDimensions[1],
mainDimensions[2],
mainDimensions[3],
gl.RGBA,
this.glType,
data.slice(0, splitIndex));
}
if (remainderDimensions[2] > 0) {
// Round data up to a multiple of 4 elements if necessary.
let remainderLength = data.length - splitIndex;
let remainder: Float32Array | Uint8Array;
if (remainderLength % 4 === 0) {
remainder = data.slice(splitIndex);
} else {
remainderLength += 4 - remainderLength % 4;
remainder = new (data.constructor as any)(remainderLength);
remainder.set(data.slice(splitIndex));
}
gl.texSubImage2D(gl.TEXTURE_2D,
0,
remainderDimensions[0],
remainderDimensions[1],
remainderDimensions[2],
remainderDimensions[3],
gl.RGBA,
this.glType,
remainder);
}
}
bind(gl: WebGLRenderingContext, uniforms: UniformMap, textureUnit: number): void {
assert(!this.destroyed, "Buffer texture destroyed!");
gl.activeTexture(gl.TEXTURE0 + textureUnit);
gl.bindTexture(gl.TEXTURE_2D, this.texture);
gl.uniform2i(uniforms[`${this.uniformName}Dimensions`], this.size[0], this.size[1]);
gl.uniform1i(uniforms[this.uniformName], textureUnit);
}
private get area(): number {
assert(!this.destroyed, "Buffer texture destroyed!");
return this.size[0] * this.size[1];
}
}
/// https://stackoverflow.com/a/466242
function nextPowerOfTwo(n: number): number {
n--;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
return n + 1;
}

View File

@ -1,506 +0,0 @@
// pathfinder/client/src/camera.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.
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import {EPSILON, unwrapNull} from "./utils";
import {PathfinderView} from "./view";
const PIXELS_PER_LINE: number = 16.0;
const ORTHOGRAPHIC_ZOOM_SPEED: number = 1.0 / 100.0;
const ORTHOGRAPHIC_ZOOM_IN_FACTOR: number = 1.2;
const ORTHOGRAPHIC_ZOOM_OUT_FACTOR: number = 1.0 / ORTHOGRAPHIC_ZOOM_IN_FACTOR;
const ORTHOGRAPHIC_DEFAULT_MIN_SCALE: number = 0.01;
const ORTHOGRAPHIC_DEFAULT_MAX_SCALE: number = 1000.0;
const PERSPECTIVE_MOVEMENT_SPEED: number = 10.0;
const PERSPECTIVE_ROTATION_SPEED: number = 1.0 / 300.0;
const PERSPECTIVE_MOVEMENT_VECTORS: PerspectiveMovementVectors = _.fromPairs([
['W'.charCodeAt(0), glmatrix.vec3.fromValues(0, 0, PERSPECTIVE_MOVEMENT_SPEED)],
['A'.charCodeAt(0), glmatrix.vec3.fromValues(PERSPECTIVE_MOVEMENT_SPEED, 0, 0)],
['S'.charCodeAt(0), glmatrix.vec3.fromValues(0, 0, -PERSPECTIVE_MOVEMENT_SPEED)],
['D'.charCodeAt(0), glmatrix.vec3.fromValues(-PERSPECTIVE_MOVEMENT_SPEED, 0, 0)],
]);
const PERSPECTIVE_MOVEMENT_INTERVAL_DELAY: number = 10;
const PERSPECTIVE_INITIAL_TRANSLATION: glmatrix.vec3 =
glmatrix.vec3.clone([1750.0, 700.0, -1750.0]);
const PERSPECTIVE_INITIAL_ROTATION: glmatrix.vec2 = glmatrix.vec2.clone([Math.PI * 0.25, 0.0]);
const PERSPECTIVE_OUTER_COLLISION_EXTENT: number = 3000.0;
const PERSPECTIVE_HITBOX_RADIUS: number = 1.0;
const KEYCODES = ["W", "A", "S", "D"].map(x => x.charCodeAt(0));
export interface OrthographicCameraOptions {
fixed?: boolean;
minScale?: number;
maxScale?: number;
scaleBounds?: boolean;
ignoreBounds?: boolean;
}
export interface PerspectiveCameraOptions {
innerCollisionExtent?: number;
}
interface PerspectiveMovementVectors {
[keyCode: number]: glmatrix.vec3;
}
interface PerspectiveMovementKeys {
[keyCode: number]: boolean;
}
export interface CameraView {
readonly width: number;
readonly height: number;
readonly classList: DOMTokenList | null;
addEventListener<K extends keyof HTMLElementEventMap>(type: K,
listener: (this: HTMLCanvasElement,
ev: HTMLElementEventMap[K]) =>
any,
useCapture?: boolean): void;
getBoundingClientRect(): ClientRect;
}
export abstract class Camera {
protected canvas: CameraView;
constructor(canvas: CameraView) {
this.canvas = canvas;
}
abstract zoom(scale: number): void;
abstract zoomIn(): void;
abstract zoomOut(): void;
abstract rotate(newAngle: number): void;
}
export class OrthographicCamera extends Camera {
onPan: (() => void) | null;
onZoom: (() => void) | null;
onRotate: (() => void) | null;
translation!: glmatrix.vec2;
scale!: number;
rotationAngle!: number;
private _bounds: glmatrix.vec4;
private readonly fixed: boolean;
private readonly minScale: number;
private readonly maxScale: number;
private readonly scaleBounds: boolean;
private readonly ignoreBounds: boolean;
constructor(canvas: CameraView, options?: OrthographicCameraOptions) {
super(canvas);
if (options == null)
options = {};
this.fixed = !!options.fixed;
this.minScale = _.defaultTo(options.minScale, ORTHOGRAPHIC_DEFAULT_MIN_SCALE);
this.maxScale = _.defaultTo(options.maxScale, ORTHOGRAPHIC_DEFAULT_MAX_SCALE);
this.scaleBounds = !!options.scaleBounds;
this.ignoreBounds = !!options.ignoreBounds;
this.reset();
this._bounds = glmatrix.vec4.create();
if (!this.fixed) {
this.canvas.addEventListener('wheel', event => this.onWheel(event), false);
this.canvas.addEventListener('mousedown', event => this.onMouseDown(event), false);
this.canvas.addEventListener('mouseup', event => this.onMouseUp(event), false);
this.canvas.addEventListener('mousemove', event => this.onMouseMove(event), false);
if (this.canvas.classList != null)
this.canvas.classList.add('pf-draggable');
} else {
if (this.canvas.classList != null)
this.canvas.classList.remove('pf-draggable');
}
this.onPan = null;
this.onZoom = null;
this.onRotate = null;
}
onWheel(event: MouseWheelEvent): void {
if (this.canvas == null)
throw new Error("onWheel() with no canvas?!");
event.preventDefault();
if (!event.ctrlKey) {
const delta = glmatrix.vec2.fromValues(-event.deltaX, event.deltaY);
if (event.deltaMode === event.DOM_DELTA_LINE)
glmatrix.vec2.scale(delta, delta, PIXELS_PER_LINE);
this.pan(delta);
return;
}
// Zoom event: see https://developer.mozilla.org/en-US/docs/Web/Events/wheel
const mouseLocation = glmatrix.vec2.fromValues(event.clientX, event.clientY);
const canvasLocation = this.canvas.getBoundingClientRect();
mouseLocation[0] -= canvasLocation.left;
mouseLocation[1] = canvasLocation.bottom - mouseLocation[1];
glmatrix.vec2.scale(mouseLocation, mouseLocation, window.devicePixelRatio);
const scale = 1.0 - event.deltaY * window.devicePixelRatio * ORTHOGRAPHIC_ZOOM_SPEED;
this.doZoom(scale, mouseLocation);
}
zoomToFit(): void {
const size = this.objectSize();
this.scale = Math.min(this.canvas.width / size[0], this.canvas.height / size[1]);
this.center();
}
center(): void {
const upperLeft = glmatrix.vec2.clone([this._bounds[0], this._bounds[1]]);
const lowerRight = glmatrix.vec2.clone([this._bounds[2], this._bounds[3]]);
this.translation = glmatrix.vec2.create();
glmatrix.vec2.lerp(this.translation, upperLeft, lowerRight, 0.5);
glmatrix.vec2.scale(this.translation, this.translation, -this.scale);
this.translation[0] += this.canvas.width * 0.5;
this.translation[1] += this.canvas.height * 0.5;
}
zoomToSize(newSize: number): void {
this.reset();
const size = this.objectSize();
const length = Math.max(size[0], size[1]);
this.zoom(newSize / length);
}
zoom(scale: number): void {
this.doZoom(scale, this.centerPoint);
}
zoomIn(): void {
this.doZoom(ORTHOGRAPHIC_ZOOM_IN_FACTOR, this.centerPoint);
}
zoomOut(): void {
this.doZoom(ORTHOGRAPHIC_ZOOM_OUT_FACTOR, this.centerPoint);
}
rotate(newAngle: number): void {
this.rotationAngle = newAngle;
if (this.onRotate != null)
this.onRotate();
}
private objectSize(): glmatrix.vec2 {
const upperLeft = glmatrix.vec2.clone([this._bounds[0], this._bounds[1]]);
const lowerRight = glmatrix.vec2.clone([this._bounds[2], this._bounds[3]]);
const width = this._bounds[2] - this._bounds[0];
const height = Math.abs(this._bounds[1] - this._bounds[3]);
return glmatrix.vec2.clone([width, height]);
}
private reset(): void {
this.translation = glmatrix.vec2.create();
this.scale = 1.0;
this.rotationAngle = 0.0;
}
private onMouseDown(event: MouseEvent): void {
if (this.canvas.classList != null)
this.canvas.classList.add('pf-grabbing');
}
private onMouseUp(event: MouseEvent): void {
if (this.canvas.classList != null)
this.canvas.classList.remove('pf-grabbing');
}
private onMouseMove(event: MouseEvent): void {
if ((event.buttons & 1) !== 0)
this.pan(glmatrix.vec2.fromValues(event.movementX, -event.movementY));
}
private pan(delta: glmatrix.vec2): void {
// Pan event.
glmatrix.vec2.scale(delta, delta, window.devicePixelRatio);
glmatrix.vec2.add(this.translation, this.translation, delta);
this.clampViewport();
if (this.onPan != null)
this.onPan();
}
private clampViewport() {
if (this.ignoreBounds)
return;
const bounds = glmatrix.vec4.clone(this.bounds);
if (!this.scaleBounds)
glmatrix.vec4.scale(bounds, bounds, this.scale);
for (let axis = 0; axis < 2; axis++) {
const viewportLength = axis === 0 ? this.canvas.width : this.canvas.height;
const axisBounds = [bounds[axis + 0], bounds[axis + 2]];
const boundsLength = axisBounds[1] - axisBounds[0];
if (viewportLength < boundsLength) {
// Viewport must be inside bounds.
this.translation[axis] = _.clamp(this.translation[axis],
viewportLength - axisBounds[1],
-axisBounds[0]);
} else {
// Bounds must be inside viewport.
this.translation[axis] = _.clamp(this.translation[axis],
-axisBounds[0],
viewportLength - axisBounds[1]);
}
}
}
private doZoom(scale: number, point: glmatrix.vec2): void {
const absoluteTranslation = glmatrix.vec2.create();
glmatrix.vec2.sub(absoluteTranslation, this.translation, point);
glmatrix.vec2.scale(absoluteTranslation, absoluteTranslation, 1.0 / this.scale);
this.scale = _.clamp(this.scale * scale, this.minScale, this.maxScale);
glmatrix.vec2.scale(absoluteTranslation, absoluteTranslation, this.scale);
glmatrix.vec2.add(this.translation, absoluteTranslation, point);
this.clampViewport();
if (this.onZoom != null)
this.onZoom();
}
private get centerPoint(): glmatrix.vec2 {
return glmatrix.vec2.clone([this.canvas.width * 0.5, this.canvas.height * 0.5]);
}
get bounds(): glmatrix.vec4 {
const bounds = glmatrix.vec4.clone(this._bounds);
if (this.scaleBounds)
glmatrix.vec4.scale(bounds, bounds, this.scale);
return bounds;
}
set bounds(newBounds: glmatrix.vec4) {
this._bounds = glmatrix.vec4.clone(newBounds);
}
}
export class PerspectiveCamera extends Camera {
canvas!: HTMLCanvasElement;
onChange: (() => void) | null;
translation: glmatrix.vec3;
/// Yaw and pitch Euler angles.
rotation: glmatrix.vec2;
private movementDelta: glmatrix.vec3;
// If W, A, S, D are pressed
private wasdPress: PerspectiveMovementKeys;
private movementInterval: number | null;
private readonly innerCollisionExtent: number;
private vrRotationMatrix: glmatrix.mat4 | null;
constructor(canvas: HTMLCanvasElement, options?: PerspectiveCameraOptions) {
super(canvas);
if (options == null)
options = {};
this.innerCollisionExtent = options.innerCollisionExtent || 0.0;
this.translation = glmatrix.vec3.clone(PERSPECTIVE_INITIAL_TRANSLATION);
this.rotation = glmatrix.vec2.clone(PERSPECTIVE_INITIAL_ROTATION);
this.movementDelta = glmatrix.vec3.create();
this.movementInterval = null;
this.canvas.addEventListener('mousedown', event => this.onMouseDown(event), false);
this.canvas.addEventListener('mouseup', event => this.onMouseUp(event), false);
this.canvas.addEventListener('mousemove', event => this.onMouseMove(event), false);
window.addEventListener('keydown', event => this.onKeyDown(event), false);
window.addEventListener('keyup', event => this.onKeyUp(event), false);
this.onChange = null;
this.vrRotationMatrix = null;
this.wasdPress = _.fromPairs([
['W'.charCodeAt(0), false],
['A'.charCodeAt(0), false],
['S'.charCodeAt(0), false],
['D'.charCodeAt(0), false],
]);
}
zoom(scale: number): void {
// TODO(pcwalton)
}
zoomIn(): void {
// TODO(pcwalton)
}
zoomOut(): void {
// TODO(pcwalton)
}
rotate(newAngle: number): void {
// TODO(pcwalton)
}
setView(rotation: glmatrix.mat4, pose: VRPose): void {
this.vrRotationMatrix = rotation;
}
private onMouseDown(event: MouseEvent): void {
if (document.pointerLockElement !== this.canvas) {
this.canvas.requestPointerLock();
return;
}
this.movementDelta = glmatrix.vec3.fromValues(0.0, 0.0, PERSPECTIVE_MOVEMENT_SPEED);
if (event.button !== 1)
this.movementDelta[0] = -this.movementDelta[0];
this.startMoving();
}
private onMouseUp(event: MouseEvent): void {
this.stopMoving();
}
private onMouseMove(event: MouseEvent): void {
if (document.pointerLockElement !== this.canvas)
return;
this.rotation[0] += event.movementX * PERSPECTIVE_ROTATION_SPEED;
const trialYRotation = this.rotation[1] + event.movementY * PERSPECTIVE_ROTATION_SPEED;
this.rotation[1] = _.clamp(trialYRotation, -Math.PI * 0.5, Math.PI * 0.5);
if (this.onChange != null)
this.onChange();
}
private onKeyDown(event: KeyboardEvent): void {
if (PERSPECTIVE_MOVEMENT_VECTORS.hasOwnProperty(event.keyCode)) {
// keyDown will be repeated on prolonged holds of the key,
// don't do extra computation in that case
if (this.wasdPress[event.keyCode])
return;
this.wasdPress[event.keyCode] = true;
this.updateMovementDelta();
this.startMoving();
}
}
private onKeyUp(event: KeyboardEvent): void {
if (PERSPECTIVE_MOVEMENT_VECTORS.hasOwnProperty(event.keyCode)) {
this.wasdPress[event.keyCode] = false;
if (this.updateMovementDelta()) {
this.stopMoving();
}
}
}
// Updates the movementDelta vector based on what keys are currently pressed
// Returns true if the vector is now empty
private updateMovementDelta(): boolean {
this.movementDelta = glmatrix.vec3.create();
let empty = true;
for (const key of KEYCODES) {
if (this.wasdPress[key]) {
glmatrix.vec3.add(this.movementDelta,
this.movementDelta,
PERSPECTIVE_MOVEMENT_VECTORS[key]);
empty = false;
}
}
return empty;
}
private startMoving(): void {
if (this.movementInterval == null)
this.movementInterval = window.setInterval(() => this.move(),
PERSPECTIVE_MOVEMENT_INTERVAL_DELAY);
}
private stopMoving(): void {
if (this.movementInterval != null) {
window.clearInterval(this.movementInterval);
this.movementInterval = null;
this.movementDelta = glmatrix.vec3.create();
}
}
private move() {
const invRotationMatrix = glmatrix.mat4.create();
glmatrix.mat4.invert(invRotationMatrix, this.rotationMatrix);
const delta = glmatrix.vec3.clone(this.movementDelta);
glmatrix.vec3.transformMat4(delta, delta, invRotationMatrix);
const trialTranslation = glmatrix.vec3.create();
glmatrix.vec3.add(trialTranslation, this.translation, delta);
// TODO(pcwalton): Sliding…
const absoluteTrialTranslationX = Math.abs(trialTranslation[0]);
const absoluteTrialTranslationZ = Math.abs(trialTranslation[2]);
if (absoluteTrialTranslationX < this.innerCollisionExtent + PERSPECTIVE_HITBOX_RADIUS &&
absoluteTrialTranslationZ < this.innerCollisionExtent + PERSPECTIVE_HITBOX_RADIUS) {
return;
}
if (absoluteTrialTranslationX > PERSPECTIVE_OUTER_COLLISION_EXTENT -
PERSPECTIVE_HITBOX_RADIUS) {
trialTranslation[0] = Math.sign(trialTranslation[0]) *
(PERSPECTIVE_OUTER_COLLISION_EXTENT - PERSPECTIVE_HITBOX_RADIUS);
}
if (absoluteTrialTranslationZ > PERSPECTIVE_OUTER_COLLISION_EXTENT -
PERSPECTIVE_HITBOX_RADIUS) {
trialTranslation[2] = Math.sign(trialTranslation[2]) *
(PERSPECTIVE_OUTER_COLLISION_EXTENT - PERSPECTIVE_HITBOX_RADIUS);
}
this.translation = trialTranslation;
if (this.onChange != null)
this.onChange();
}
get rotationMatrix(): glmatrix.mat4 {
if (this.vrRotationMatrix != null) {
// This actually is a rotation + translation matrix, but it works
return this.vrRotationMatrix;
}
const matrix = glmatrix.mat4.create();
glmatrix.mat4.fromXRotation(matrix, this.rotation[1]);
glmatrix.mat4.rotateY(matrix, matrix, this.rotation[0]);
return matrix;
}
}

View File

@ -1,43 +0,0 @@
// pathfinder/client/src/file-picker.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.
import {expectNotNull} from "./utils";
export class FilePickerView {
static create(): FilePickerView | null {
const element = document.getElementById('pf-file-select') as (HTMLInputElement | null);
return element == null ? null : new FilePickerView(element);
}
onFileLoaded: ((fileData: ArrayBuffer) => void) | null;
private readonly element: HTMLInputElement;
private constructor(element: HTMLInputElement) {
this.element = element;
this.onFileLoaded = null;
element.addEventListener('change', event => this.loadFile(event), false);
}
open() {
this.element.click();
}
private loadFile(event: Event) {
const element = event.target as HTMLInputElement;
const file = expectNotNull(element.files, "No file selected!")[0];
const reader = new FileReader;
reader.addEventListener('loadend', () => {
if (this.onFileLoaded != null)
this.onFileLoaded(reader.result);
}, false);
reader.readAsArrayBuffer(file);
}
}

View File

@ -1,133 +0,0 @@
// pathfinder/client/src/gl-utils.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.
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import {assert, UINT32_SIZE, unwrapNull} from './utils';
import {ColorAlphaFormat, DemoView} from './view';
export type WebGLVertexArrayObject = any;
export interface AttributeMap {
[attributeName: string]: number;
}
export interface UniformMap {
[uniformName: string]: WebGLUniformLocation;
}
export interface EXTDisjointTimerQuery {
readonly QUERY_COUNTER_BITS_EXT: GLenum;
readonly CURRENT_QUERY_EXT: GLenum;
readonly QUERY_RESULT_EXT: GLenum;
readonly QUERY_RESULT_AVAILABLE_EXT: GLenum;
readonly TIME_ELAPSED_EXT: GLenum;
readonly TIMESTAMP_EXT: GLenum;
readonly GPU_DISJOINT_EXT: GLenum;
createQueryEXT(): WebGLQuery;
deleteQueryEXT(query: WebGLQuery): void;
isQueryEXT(query: any): GLboolean;
beginQueryEXT(target: GLenum, query: WebGLQuery): void;
endQueryEXT(target: GLenum): void;
queryCounterEXT(query: WebGLQuery, target: GLenum): void;
getQueryEXT(target: GLenum, pname: GLenum): any;
getQueryObjectEXT(query: WebGLQuery, pname: GLenum): any;
}
export class WebGLQuery {}
export const QUAD_ELEMENTS: Uint8Array = new Uint8Array([0, 1, 2, 1, 3, 2]);
export function createFramebufferColorTexture(gl: WebGLRenderingContext,
size: glmatrix.vec2,
colorAlphaFormat: ColorAlphaFormat,
filter?: number):
WebGLTexture {
// Firefox seems to have a bug whereby textures don't get marked as initialized when cleared
// if they're anything other than the first attachment of an FBO. To work around this, supply
// zero data explicitly when initializing the texture.
let format, type, internalFormat, bitDepth, zeroes;
if (colorAlphaFormat === 'RGBA8') {
format = internalFormat = gl.RGBA;
type = gl.UNSIGNED_BYTE;
bitDepth = 32;
zeroes = new Uint8Array(size[0] * size[1] * 4);
} else {
format = internalFormat = gl.RGBA;
type = gl.UNSIGNED_SHORT_5_5_5_1;
bitDepth = 16;
zeroes = new Uint16Array(size[0] * size[1] * 4);
}
const texture = unwrapNull(gl.createTexture());
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, size[0], size[1], 0, format, type, zeroes);
setTextureParameters(gl, _.defaultTo(filter, gl.NEAREST));
return texture;
}
export function createFramebufferDepthTexture(gl: WebGLRenderingContext, size: glmatrix.vec2):
WebGLTexture {
const texture = unwrapNull(gl.createTexture());
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D,
0,
gl.DEPTH_COMPONENT,
size[0],
size[1],
0,
gl.DEPTH_COMPONENT,
gl.UNSIGNED_INT,
null);
setTextureParameters(gl, gl.NEAREST);
return texture;
}
export function setTextureParameters(gl: WebGLRenderingContext, filter: number) {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
}
export function createFramebuffer(gl: WebGLRenderingContext,
colorAttachment: WebGLTexture,
depthAttachment: WebGLTexture | null):
WebGLFramebuffer {
const framebuffer = unwrapNull(gl.createFramebuffer());
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
colorAttachment,
0);
if (depthAttachment != null) {
gl.framebufferTexture2D(gl.FRAMEBUFFER,
gl.DEPTH_ATTACHMENT,
gl.TEXTURE_2D,
depthAttachment,
0);
assert(gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER,
gl.DEPTH_ATTACHMENT,
gl.FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE) ===
gl.TEXTURE,
"Failed to attach depth texture!");
}
assert(gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE,
"Framebuffer was incomplete!");
return framebuffer;
}

View File

@ -1,564 +0,0 @@
// pathfinder/client/src/mesh-debugger.ts
//
// Copyright © 2018 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.
import * as glmatrix from 'gl-matrix';
import * as opentype from "opentype.js";
import {Font} from 'opentype.js';
import {AppController} from "./app-controller";
import {OrthographicCamera} from "./camera";
import {FilePickerView} from './file-picker';
import {B_QUAD_UPPER_RIGHT_VERTEX_OFFSET, PathfinderMeshPack} from "./meshes";
import {B_QUAD_LOWER_LEFT_VERTEX_OFFSET, B_QUAD_UPPER_CONTROL_POINT_VERTEX_OFFSET} from "./meshes";
import {B_QUAD_LOWER_RIGHT_VERTEX_OFFSET} from "./meshes";
import {B_QUAD_LOWER_CONTROL_POINT_VERTEX_OFFSET, PathfinderPackedMeshes} from "./meshes";
import {B_QUAD_SIZE, B_QUAD_UPPER_LEFT_VERTEX_OFFSET} from "./meshes";
import {BUILTIN_SVG_URI, SVGLoader} from './svg-loader';
import {BUILTIN_FONT_URI, TextRun} from "./text";
import {GlyphStore, PathfinderFont, TextFrame} from "./text";
import {assert, UINT32_MAX, UINT32_SIZE, unwrapNull} from "./utils";
import {PathfinderView} from "./view";
const CHARACTER: string = 'A';
const FONT: string = 'eb-garamond';
const POINT_LABEL_FONT: string = "sans-serif";
const POINT_LABEL_FONT_SIZE: number = 12.0;
const POINT_LABEL_OFFSET: glmatrix.vec2 = glmatrix.vec2.fromValues(12.0, 12.0);
const POINT_RADIUS: number = 2.0;
const SEGMENT_POINT_RADIUS: number = 3.0;
const SEGMENT_STROKE_WIDTH: number = 1.0;
const SEGMENT_CONTROL_POINT_STROKE_WIDTH: number = 1.0;
const NORMAL_LENGTHS: NormalStyleParameter<number> = {
bVertex: 10.0,
edge: 14.0,
};
const NORMAL_ARROWHEAD_LENGTH: number = 4.0;
const NORMAL_ARROWHEAD_ANGLE: number = Math.PI * 5.0 / 6.0;
const SEGMENT_POINT_FILL_STYLE: string = "rgb(0, 0, 128)";
const LIGHT_STROKE_STYLE: string = "rgb(192, 192, 192)";
const LINE_STROKE_STYLE: string = "rgb(0, 128, 0)";
const CURVE_STROKE_STYLE: string = "rgb(128, 0, 0)";
const SEGMENT_LINE_STROKE_STYLE: string = "rgb(128, 192, 128)";
const SEGMENT_CONTROL_POINT_FILL_STYLE: string = "rgb(255, 255, 255)";
const SEGMENT_CONTROL_POINT_STROKE_STYLE: string = "rgb(0, 0, 128)";
const SEGMENT_CONTROL_POINT_HULL_STROKE_STYLE: string = "rgba(128, 128, 128, 0.5)";
const NORMAL_STROKE_STYLES: NormalStyleParameter<string> = {
bVertex: '#e6aa00',
edge: '#cc5500',
};
const BUILTIN_URIS = {
font: BUILTIN_FONT_URI,
svg: BUILTIN_SVG_URI,
};
const SVG_SCALE: number = 1.0;
type FileType = 'font' | 'svg';
type NormalType = 'edge' | 'bVertex';
interface NormalStyleParameter<T> {
edge: T;
bVertex: T;
}
interface NormalsTable<T> {
lowerCurve: T;
lowerLine: T;
upperCurve: T;
upperLine: T;
}
class MeshDebuggerAppController extends AppController {
meshes: PathfinderPackedMeshes | null = null;
protected readonly defaultFile: string = FONT;
private file: PathfinderFont | SVGLoader | null = null;
private fileType!: FileType;
private fileData: ArrayBuffer | null = null;
private openModal!: HTMLElement;
private openFileSelect!: HTMLSelectElement;
private fontPathSelectGroup!: HTMLElement;
private fontPathSelect!: HTMLSelectElement;
private filePicker!: FilePickerView;
private view!: MeshDebuggerView;
start() {
super.start();
this.fileType = 'font';
this.view = new MeshDebuggerView(this);
this.filePicker = unwrapNull(FilePickerView.create());
this.filePicker.onFileLoaded = fileData => this.fileLoaded(fileData, null);
this.openModal = unwrapNull(document.getElementById('pf-open-modal'));
this.fontPathSelectGroup =
unwrapNull(document.getElementById('pf-font-path-select-group'));
this.fontPathSelect = unwrapNull(document.getElementById('pf-font-path-select')) as
HTMLSelectElement;
this.openFileSelect = unwrapNull(document.getElementById('pf-open-file-select')) as
HTMLSelectElement;
this.openFileSelect.addEventListener('click', () => this.openSelectedFile(), false);
const openButton = unwrapNull(document.getElementById('pf-open-button'));
openButton.addEventListener('click', () => this.showOpenDialog(), false);
const openOKButton = unwrapNull(document.getElementById('pf-open-ok-button'));
openOKButton.addEventListener('click', () => this.loadPath(), false);
this.loadInitialFile(BUILTIN_FONT_URI);
}
protected fileLoaded(fileData: ArrayBuffer, builtinName: string | null): void {
while (this.fontPathSelect.lastChild != null)
this.fontPathSelect.removeChild(this.fontPathSelect.lastChild);
this.fontPathSelectGroup.classList.remove('pf-display-none');
if (this.fileType === 'font')
this.fontLoaded(fileData, builtinName);
else if (this.fileType === 'svg')
this.svgLoaded(fileData);
}
protected loadPath(opentypeGlyph?: opentype.Glyph | null) {
window.jQuery(this.openModal).modal('hide');
let promise: Promise<PathfinderMeshPack>;
if (this.file instanceof PathfinderFont && this.fileData != null) {
if (opentypeGlyph == null) {
const glyphIndex = parseInt(this.fontPathSelect.selectedOptions[0].value, 10);
opentypeGlyph = this.file.opentypeFont.glyphs.get(glyphIndex);
}
const glyphStorage = new GlyphStore(this.file, [(opentypeGlyph as any).index]);
promise = glyphStorage.partition().then(result => result.meshes);
} else if (this.file instanceof SVGLoader) {
promise = this.file.partition(this.fontPathSelect.selectedIndex);
} else {
return;
}
promise.then(meshes => {
this.meshes = new PathfinderPackedMeshes(meshes);
this.view.attachMeshes();
});
}
private showOpenDialog(): void {
window.jQuery(this.openModal).modal();
}
private openSelectedFile(): void {
const selectedOption = this.openFileSelect.selectedOptions[0] as HTMLOptionElement;
const optionValue = selectedOption.value;
this.fontPathSelectGroup.classList.add('pf-display-none');
const results = unwrapNull(/^([a-z]+)-(.*)$/.exec(optionValue));
this.fileType = results[1] as FileType;
const filename = results[2];
if (filename === 'custom')
this.filePicker.open();
else
this.fetchFile(results[2], BUILTIN_URIS[this.fileType]);
}
private fontLoaded(fileData: ArrayBuffer, builtinName: string | null): void {
this.file = new PathfinderFont(fileData, builtinName);
this.fileData = fileData;
const glyphCount = this.file.opentypeFont.numGlyphs;
for (let glyphIndex = 1; glyphIndex < glyphCount; glyphIndex++) {
const newOption = document.createElement('option');
newOption.value = "" + glyphIndex;
const glyphName = this.file.opentypeFont.glyphIndexToName(glyphIndex);
newOption.appendChild(document.createTextNode(glyphName));
this.fontPathSelect.appendChild(newOption);
}
// Automatically load a path if this is the initial pageload.
if (this.meshes == null)
this.loadPath(this.file.opentypeFont.charToGlyph(CHARACTER));
}
private svgLoaded(fileData: ArrayBuffer): void {
this.file = new SVGLoader;
this.file.scale = SVG_SCALE;
this.file.loadFile(fileData);
const pathCount = this.file.pathInstances.length;
for (let pathIndex = 0; pathIndex < pathCount; pathIndex++) {
const newOption = document.createElement('option');
newOption.value = "" + pathIndex;
newOption.appendChild(document.createTextNode(`Path ${pathIndex}`));
this.fontPathSelect.appendChild(newOption);
}
}
}
class MeshDebuggerView extends PathfinderView {
camera: OrthographicCamera;
private appController: MeshDebuggerAppController;
private drawControl: boolean = true;
private drawNormals: boolean = true;
private drawVertices: boolean = true;
private drawSegments: boolean = false;
constructor(appController: MeshDebuggerAppController) {
super();
this.appController = appController;
this.camera = new OrthographicCamera(this.canvas, { ignoreBounds: true });
this.camera.onPan = () => this.setDirty();
this.camera.onZoom = () => this.setDirty();
window.addEventListener('keypress', event => this.onKeyPress(event), false);
this.resizeToFit(true);
}
attachMeshes() {
this.setDirty();
}
redraw() {
super.redraw();
const meshes = this.appController.meshes;
if (meshes == null)
return;
const context = unwrapNull(this.canvas.getContext('2d'));
context.clearRect(0, 0, this.canvas.width, this.canvas.height);
context.save();
context.translate(this.camera.translation[0],
this.canvas.height - this.camera.translation[1]);
context.scale(this.camera.scale, this.camera.scale);
const invScaleFactor = window.devicePixelRatio / this.camera.scale;
context.font = `12px ${POINT_LABEL_FONT}`;
context.lineWidth = invScaleFactor;
const bQuadVertexPositions = new Float32Array(meshes.bQuadVertexPositions);
const normals: NormalsTable<Float32Array> = {
lowerCurve: new Float32Array(0),
lowerLine: new Float32Array(0),
upperCurve: new Float32Array(0),
upperLine: new Float32Array(0),
};
// Draw B-quads.
for (let bQuadIndex = 0; bQuadIndex < meshes.bQuadVertexPositions.length; bQuadIndex++) {
const bQuadStartOffset = (B_QUAD_SIZE * bQuadIndex) / UINT32_SIZE;
const upperLeftPosition = getPosition(bQuadVertexPositions, bQuadIndex, 0);
const upperControlPointPosition = getPosition(bQuadVertexPositions, bQuadIndex, 1);
const upperRightPosition = getPosition(bQuadVertexPositions, bQuadIndex, 2);
const lowerRightPosition = getPosition(bQuadVertexPositions, bQuadIndex, 3);
const lowerControlPointPosition = getPosition(bQuadVertexPositions, bQuadIndex, 4);
const lowerLeftPosition = getPosition(bQuadVertexPositions, bQuadIndex, 5);
if (this.drawVertices) {
drawVertexIfNecessary(context, upperLeftPosition, invScaleFactor);
drawVertexIfNecessary(context, upperRightPosition, invScaleFactor);
drawVertexIfNecessary(context, lowerLeftPosition, invScaleFactor);
drawVertexIfNecessary(context, lowerRightPosition, invScaleFactor);
}
context.beginPath();
context.moveTo(upperLeftPosition[0], -upperLeftPosition[1]);
if (upperControlPointPosition != null) {
context.strokeStyle = CURVE_STROKE_STYLE;
context.quadraticCurveTo(upperControlPointPosition[0],
-upperControlPointPosition[1],
upperRightPosition[0],
-upperRightPosition[1]);
} else {
context.strokeStyle = LINE_STROKE_STYLE;
context.lineTo(upperRightPosition[0], -upperRightPosition[1]);
}
context.stroke();
context.strokeStyle = LIGHT_STROKE_STYLE;
context.beginPath();
context.moveTo(upperRightPosition[0], -upperRightPosition[1]);
context.lineTo(lowerRightPosition[0], -lowerRightPosition[1]);
context.stroke();
context.beginPath();
context.moveTo(lowerRightPosition[0], -lowerRightPosition[1]);
if (lowerControlPointPosition != null) {
context.strokeStyle = CURVE_STROKE_STYLE;
context.quadraticCurveTo(lowerControlPointPosition[0],
-lowerControlPointPosition[1],
lowerLeftPosition[0],
-lowerLeftPosition[1]);
} else {
context.strokeStyle = LINE_STROKE_STYLE;
context.lineTo(lowerLeftPosition[0], -lowerLeftPosition[1]);
}
context.stroke();
context.strokeStyle = LIGHT_STROKE_STYLE;
context.beginPath();
context.moveTo(lowerLeftPosition[0], -lowerLeftPosition[1]);
context.lineTo(upperLeftPosition[0], -upperLeftPosition[1]);
context.stroke();
}
// Draw segments.
if (this.drawVertices) {
drawSegmentVertices(context,
new Float32Array(meshes.stencilSegments),
new Float32Array(meshes.stencilNormals),
meshes.count('stencilSegments'),
[0, 2],
1,
3,
invScaleFactor,
this.drawControl,
this.drawNormals,
this.drawSegments);
}
context.restore();
}
private onKeyPress(event: KeyboardEvent): void {
if (event.key === "c") {
this.drawControl = !this.drawControl;
} else if (event.key === "n") {
this.drawNormals = !this.drawNormals;
} else if (event.key === "v") {
this.drawVertices = !this.drawVertices;
} else if (event.key === "r") {
// Reset
this.drawControl = true;
this.drawNormals = true;
this.drawVertices = true;
}
this.setDirty();
}
}
function getPosition(positions: Float32Array, bQuadIndex: number, vertexIndex: number):
glmatrix.vec2 {
return glmatrix.vec2.clone([
positions[(bQuadIndex * 6 + vertexIndex) * 2 + 0],
positions[(bQuadIndex * 6 + vertexIndex) * 2 + 1],
]);
}
function getNormal(normals: Float32Array, bQuadIndex: number, vertexIndex: number): number {
return normals[bQuadIndex * 6 + vertexIndex];
}
function getNormals(normals: NormalsTable<Float32Array>,
normalIndices: NormalsTable<number>,
isCurve: boolean,
side: 'upper' | 'lower'):
{ left: number, right: number } {
const key: keyof NormalsTable<void> = (side + (isCurve ? 'Curve' : 'Line')) as keyof
NormalsTable<void>;
const startOffset = normalIndices[key];
normalIndices[key]++;
return {
left: normals[key][startOffset * 2 + 0],
right: normals[key][startOffset * 2 + 1],
};
}
function drawSegmentVertices(context: CanvasRenderingContext2D,
segments: Float32Array,
normals: Float32Array,
segmentCount: number,
endpointOffsets: number[],
controlPointOffset: number | null,
segmentSize: number,
invScaleFactor: number,
drawControl: boolean,
drawNormals: boolean,
drawSegments: boolean) {
for (let segmentIndex = 0; segmentIndex < segmentCount; segmentIndex++) {
const positionStartOffset = segmentSize * 2 * segmentIndex;
const normalStartOffset = segmentSize * 2 * segmentIndex;
const position0 =
glmatrix.vec2.clone([segments[positionStartOffset + endpointOffsets[0] * 2 + 0],
segments[positionStartOffset + endpointOffsets[0] * 2 + 1]]);
const position1 =
glmatrix.vec2.clone([segments[positionStartOffset + endpointOffsets[1] * 2 + 0],
segments[positionStartOffset + endpointOffsets[1] * 2 + 1]]);
let controlPoint: glmatrix.vec2 | null;
if (controlPointOffset != null) {
controlPoint =
glmatrix.vec2.clone([segments[positionStartOffset + controlPointOffset * 2 + 0],
segments[positionStartOffset + controlPointOffset * 2 + 1]]);
} else {
controlPoint = null;
}
const normal0 =
glmatrix.vec2.clone([normals[normalStartOffset + endpointOffsets[0] * 2 + 0],
normals[normalStartOffset + endpointOffsets[0] * 2 + 1]]);
const normal1 =
glmatrix.vec2.clone([normals[normalStartOffset + endpointOffsets[1] * 2 + 0],
normals[normalStartOffset + endpointOffsets[1] * 2 + 1]]);
let normalControlPoint: glmatrix.vec2 | null;
if (controlPointOffset != null) {
normalControlPoint =
glmatrix.vec2.clone([normals[normalStartOffset + controlPointOffset * 2 + 0],
normals[normalStartOffset + controlPointOffset * 2 + 1]]);
} else {
normalControlPoint = null;
}
if (drawNormals) {
drawNormal(context, position0, normal0, invScaleFactor, 'edge');
drawNormal(context, position1, normal1, invScaleFactor, 'edge');
if (drawControl && controlPoint != null && normalControlPoint != null)
drawNormal(context, controlPoint, normalControlPoint, invScaleFactor, 'edge');
}
drawSegmentVertex(context, position0, invScaleFactor);
drawSegmentVertex(context, position1, invScaleFactor);
if (drawControl && controlPoint != null) {
context.save();
context.strokeStyle = SEGMENT_CONTROL_POINT_HULL_STROKE_STYLE;
context.setLineDash([2 * invScaleFactor, 2 * invScaleFactor]);
context.beginPath();
context.moveTo(position0[0], -position0[1]);
context.lineTo(controlPoint[0], -controlPoint[1]);
context.lineTo(position1[0], -position1[1]);
context.stroke();
context.restore();
drawSegmentControlPoint(context, controlPoint, invScaleFactor);
}
// TODO(pcwalton): Draw the curves too.
if (drawSegments) {
context.save();
context.strokeStyle = SEGMENT_LINE_STROKE_STYLE;
context.beginPath();
context.moveTo(position0[0], -position0[1]);
context.lineTo(position1[0], -position1[1]);
context.stroke();
context.restore();
}
}
}
function drawVertexIfNecessary(context: CanvasRenderingContext2D,
position: Float32Array,
invScaleFactor: number) {
context.beginPath();
context.moveTo(position[0], -position[1]);
context.arc(position[0], -position[1], POINT_RADIUS * invScaleFactor, 0, 2.0 * Math.PI);
context.fill();
}
function drawSegmentVertex(context: CanvasRenderingContext2D,
position: glmatrix.vec2,
invScaleFactor: number) {
context.save();
context.fillStyle = SEGMENT_POINT_FILL_STYLE;
context.lineWidth = invScaleFactor * SEGMENT_STROKE_WIDTH;
context.beginPath();
context.arc(position[0],
-position[1],
SEGMENT_POINT_RADIUS * invScaleFactor,
0,
2.0 * Math.PI);
context.fill();
context.restore();
}
function drawSegmentControlPoint(context: CanvasRenderingContext2D,
position: glmatrix.vec2,
invScaleFactor: number) {
context.save();
context.strokeStyle = SEGMENT_CONTROL_POINT_STROKE_STYLE;
context.fillStyle = SEGMENT_CONTROL_POINT_FILL_STYLE;
context.lineWidth = invScaleFactor * SEGMENT_CONTROL_POINT_STROKE_WIDTH;
context.beginPath();
context.arc(position[0],
-position[1],
SEGMENT_POINT_RADIUS * invScaleFactor,
0,
2.0 * Math.PI);
context.fill();
context.stroke();
context.restore();
}
function drawNormal(context: CanvasRenderingContext2D,
position: glmatrix.vec2,
normalVector: glmatrix.vec2,
invScaleFactor: number,
normalType: NormalType) {
const length = invScaleFactor * NORMAL_LENGTHS[normalType];
const arrowheadLength = invScaleFactor * NORMAL_ARROWHEAD_LENGTH;
const endpoint = glmatrix.vec2.clone([position[0] + length * normalVector[0],
-position[1] + length * -normalVector[1]]);
context.save();
context.strokeStyle = NORMAL_STROKE_STYLES[normalType];
context.beginPath();
context.moveTo(position[0], -position[1]);
context.lineTo(endpoint[0], endpoint[1]);
context.lineTo(endpoint[0] + arrowheadLength *
(Math.cos(NORMAL_ARROWHEAD_ANGLE) * normalVector[0] +
Math.sin(NORMAL_ARROWHEAD_ANGLE) * normalVector[1]),
endpoint[1] + arrowheadLength *
(Math.sin(NORMAL_ARROWHEAD_ANGLE) * normalVector[0] -
Math.cos(NORMAL_ARROWHEAD_ANGLE) * normalVector[1]));
context.stroke();
context.beginPath();
context.moveTo(endpoint[0], endpoint[1]);
context.lineTo(endpoint[0] + arrowheadLength *
(Math.cos(NORMAL_ARROWHEAD_ANGLE) * normalVector[0] -
Math.sin(NORMAL_ARROWHEAD_ANGLE) * normalVector[1]),
endpoint[1] - arrowheadLength *
(Math.cos(NORMAL_ARROWHEAD_ANGLE) * normalVector[1] +
Math.sin(NORMAL_ARROWHEAD_ANGLE) * normalVector[0]));
context.stroke();
context.restore();
}
function main() {
const controller = new MeshDebuggerAppController;
window.addEventListener('load', () => controller.start(), false);
}
main();

View File

@ -1,391 +0,0 @@
// pathfinder/client/src/meshes.ts
//
// Copyright © 2018 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.
import * as base64js from 'base64-js';
import * as _ from 'lodash';
import {expectNotNull, FLOAT32_SIZE, panic, PathfinderError, Range, UINT16_SIZE} from './utils';
import {UINT32_MAX, UINT32_SIZE, UINT8_SIZE, unwrapNull, unwrapUndef} from './utils';
interface BufferTypeFourCCTable {
[fourCC: string]: keyof MeshLike<void>;
}
interface PathRangeTypeFourCCTable {
[fourCC: string]: keyof PathRanges;
}
interface RangeToCountTable {
[rangeKey: string]: keyof MeshDataCounts;
}
type PathIDBufferTable = Partial<MeshLike<PackedMeshBufferType>>;
interface ArrayLike {
readonly length: number;
}
interface VertexCopyResult {
originalStartIndex: number;
originalEndIndex: number;
expandedStartIndex: number;
expandedEndIndex: number;
}
type PrimitiveType = 'Uint16' | 'Uint32' | 'Float32';
type PrimitiveTypeArray = Float32Array | Uint16Array | Uint32Array;
type MeshBufferType = keyof MeshLike<void>;
type PackedMeshBufferType = keyof PackedMeshLike<void>;
interface MeshBufferTypeDescriptor {
type: PrimitiveType;
size: number;
}
const PRIMITIVE_TYPE_ARRAY_CONSTRUCTORS = {
Float32: Float32Array,
Uint16: Uint16Array,
Uint32: Uint32Array,
};
export const B_QUAD_SIZE: number = 4 * 8;
export const B_QUAD_UPPER_LEFT_VERTEX_OFFSET: number = 4 * 0;
export const B_QUAD_UPPER_RIGHT_VERTEX_OFFSET: number = 4 * 1;
export const B_QUAD_UPPER_CONTROL_POINT_VERTEX_OFFSET: number = 4 * 2;
export const B_QUAD_LOWER_LEFT_VERTEX_OFFSET: number = 4 * 4;
export const B_QUAD_LOWER_RIGHT_VERTEX_OFFSET: number = 4 * 5;
export const B_QUAD_LOWER_CONTROL_POINT_VERTEX_OFFSET: number = 4 * 6;
export const B_QUAD_UPPER_INDICES_OFFSET: number = B_QUAD_UPPER_LEFT_VERTEX_OFFSET;
export const B_QUAD_LOWER_INDICES_OFFSET: number = B_QUAD_LOWER_LEFT_VERTEX_OFFSET;
const B_QUAD_FIELD_COUNT: number = B_QUAD_SIZE / UINT32_SIZE;
// FIXME(pcwalton): This duplicates information below in `MESH_TYPES`.
const INDEX_SIZE: number = 4;
const B_QUAD_VERTEX_POSITION_SIZE: number = 12 * 4;
const B_VERTEX_POSITION_SIZE: number = 4 * 2;
const MESH_TYPES: PackedMeshLike<MeshBufferTypeDescriptor> = {
bBoxPathIDs: { type: 'Uint16', size: 1 },
bBoxes: { type: 'Float32', size: 20 },
bQuadVertexInteriorIndices: { type: 'Uint32', size: 1 },
bQuadVertexPositionPathIDs: { type: 'Uint16', size: 1 },
bQuadVertexPositions: { type: 'Float32', size: 2 },
stencilNormals: { type: 'Float32', size: 6 },
stencilSegmentPathIDs: { type: 'Uint16', size: 1 },
stencilSegments: { type: 'Float32', size: 6 },
};
const BUFFER_TYPES: PackedMeshLike<BufferType> = {
bBoxPathIDs: 'ARRAY_BUFFER',
bBoxes: 'ARRAY_BUFFER',
bQuadVertexInteriorIndices: 'ELEMENT_ARRAY_BUFFER',
bQuadVertexPositionPathIDs: 'ARRAY_BUFFER',
bQuadVertexPositions: 'ARRAY_BUFFER',
stencilNormals: 'ARRAY_BUFFER',
stencilSegmentPathIDs: 'ARRAY_BUFFER',
stencilSegments: 'ARRAY_BUFFER',
};
const EDGE_BUFFER_NAMES = ['UpperLine', 'UpperCurve', 'LowerLine', 'LowerCurve'];
const RIFF_FOURCC: string = 'RIFF';
const MESH_PACK_FOURCC: string = 'PFMP';
const MESH_FOURCC: string = 'mesh';
// Must match the FourCCs in `pathfinder_partitioner::mesh_library::MeshLibrary::serialize_into()`.
const BUFFER_TYPE_FOURCCS: BufferTypeFourCCTable = {
bbox: 'bBoxes',
bqii: 'bQuadVertexInteriorIndices',
bqvp: 'bQuadVertexPositions',
snor: 'stencilNormals',
sseg: 'stencilSegments',
};
const RANGE_TO_COUNT_TABLE: RangeToCountTable = {
bBoxPathRanges: 'bBoxCount',
bQuadVertexInteriorIndexPathRanges: 'bQuadVertexInteriorIndexCount',
bQuadVertexPositionPathRanges: 'bQuadVertexPositionCount',
stencilSegmentPathRanges: 'stencilSegmentCount',
};
const INDEX_TYPE_DESCRIPTOR_TABLE: {[P in MeshBufferType]?: IndexTypeDescriptor} = {
bQuadVertexInteriorIndices: {
bufferType: 'bQuadVertexPositions',
},
};
const PATH_ID_BUFFER_TABLE: PathIDBufferTable = {
bBoxes: 'bBoxPathIDs',
bQuadVertexPositions: 'bQuadVertexPositionPathIDs',
stencilSegments: 'stencilSegmentPathIDs',
};
const PATH_RANGE_TO_BUFFER_TYPE_TABLE: {[P in keyof PathRanges]: MeshBufferType} = {
bBoxPathRanges: 'bBoxes',
bQuadVertexInteriorIndexPathRanges: 'bQuadVertexInteriorIndices',
bQuadVertexPositionPathRanges: 'bQuadVertexPositions',
stencilSegmentPathRanges: 'stencilSegments',
};
type BufferType = 'ARRAY_BUFFER' | 'ELEMENT_ARRAY_BUFFER';
export interface MeshBuilder<T> {
bQuadVertexPositions: T;
bQuadVertexInteriorIndices: T;
bBoxes: T;
stencilSegments: T;
stencilNormals: T;
}
export interface PackedMeshBuilder<T> extends MeshBuilder<T> {
bBoxPathIDs: T;
bQuadVertexPositionPathIDs: T;
stencilSegmentPathIDs: T;
}
export type MeshLike<T> = {
readonly [P in keyof MeshBuilder<void>]: T;
};
export type PackedMeshLike<T> = {
readonly [P in keyof PackedMeshBuilder<void>]: T;
};
interface PathRanges {
readonly bBoxPathRanges: Range[];
readonly bQuadVertexInteriorIndexPathRanges: Range[];
readonly bQuadVertexPositionPathRanges: Range[];
readonly stencilSegmentPathRanges: Range[];
}
interface MeshDataCounts {
readonly bQuadVertexPositionCount: number;
readonly bQuadVertexInteriorIndexCount: number;
readonly bBoxCount: number;
readonly stencilSegmentCount: number;
}
interface IndexTypeDescriptor {
bufferType: MeshBufferType;
}
export class PathfinderMeshPack {
meshes: PathfinderMesh[];
constructor(meshes: ArrayBuffer) {
this.meshes = [];
// RIFF encoded data.
if (toFourCC(meshes, 0) !== RIFF_FOURCC)
panic("Supplied array buffer is not a mesh library (no RIFF header)!");
if (toFourCC(meshes, 8) !== MESH_PACK_FOURCC)
panic("Supplied array buffer is not a mesh library (no PFMP header)!");
let offset = 12;
while (offset < meshes.byteLength) {
const fourCC = toFourCC(meshes, offset);
const chunkLength = readUInt32(meshes, offset + 4);
const startOffset = offset + 8;
const endOffset = startOffset + chunkLength;
if (fourCC === MESH_FOURCC)
this.meshes.push(new PathfinderMesh(meshes.slice(startOffset, endOffset)));
offset = endOffset;
}
}
}
export class PathfinderMesh implements MeshLike<ArrayBuffer> {
bQuadVertexPositions!: ArrayBuffer;
bQuadVertexInteriorIndices!: ArrayBuffer;
bBoxes!: ArrayBuffer;
stencilSegments!: ArrayBuffer;
stencilNormals!: ArrayBuffer;
constructor(data: ArrayBuffer) {
let offset = 0;
while (offset < data.byteLength) {
const fourCC = toFourCC(data, offset);
const chunkLength = readUInt32(data, offset + 4);
const startOffset = offset + 8;
const endOffset = startOffset + chunkLength;
if (BUFFER_TYPE_FOURCCS.hasOwnProperty(fourCC))
this[BUFFER_TYPE_FOURCCS[fourCC]] = data.slice(startOffset, endOffset);
offset = endOffset;
}
for (const type of Object.keys(BUFFER_TYPE_FOURCCS) as Array<keyof MeshLike<void>>) {
if (this[type] == null)
this[type] = new ArrayBuffer(0);
}
}
}
export class PathfinderPackedMeshes implements PackedMeshLike<PrimitiveTypeArray>, PathRanges {
readonly bBoxes!: Float32Array;
readonly bQuadVertexInteriorIndices!: Uint32Array;
readonly bQuadVertexPositions!: Float32Array;
readonly stencilSegments!: Float32Array;
readonly stencilNormals!: Float32Array;
readonly bBoxPathIDs!: Uint16Array;
readonly bQuadVertexPositionPathIDs!: Uint16Array;
readonly stencilSegmentPathIDs!: Uint16Array;
readonly bBoxPathRanges!: Range[];
readonly bQuadVertexInteriorIndexPathRanges!: Range[];
readonly bQuadVertexPositionPathRanges!: Range[];
readonly stencilSegmentPathRanges!: Range[];
/// NB: Mesh indices are 1-indexed.
constructor(meshPack: PathfinderMeshPack, meshIndices?: number[]) {
if (meshIndices == null)
meshIndices = meshPack.meshes.map((value, index) => index + 1);
const meshData: PackedMeshBuilder<number[]> = {
bBoxPathIDs: [],
bBoxes: [],
bQuadVertexInteriorIndices: [],
bQuadVertexPositionPathIDs: [],
bQuadVertexPositions: [],
stencilNormals: [],
stencilSegmentPathIDs: [],
stencilSegments: [],
};
const pathRanges: PathRanges = {
bBoxPathRanges: [],
bQuadVertexInteriorIndexPathRanges: [],
bQuadVertexPositionPathRanges: [],
stencilSegmentPathRanges: [],
};
for (let destMeshIndex = 0; destMeshIndex < meshIndices.length; destMeshIndex++) {
const srcMeshIndex = meshIndices[destMeshIndex];
const mesh = meshPack.meshes[srcMeshIndex - 1];
for (const pathRangeType of Object.keys(pathRanges) as Array<keyof PathRanges>) {
const bufferType = PATH_RANGE_TO_BUFFER_TYPE_TABLE[pathRangeType];
const startIndex = bufferCount(meshData, bufferType);
pathRanges[pathRangeType].push(new Range(startIndex, startIndex));
}
for (const indexType of Object.keys(BUFFER_TYPES) as MeshBufferType[]) {
if (BUFFER_TYPES[indexType] !== 'ELEMENT_ARRAY_BUFFER')
continue;
const indexTypeDescriptor = unwrapUndef(INDEX_TYPE_DESCRIPTOR_TABLE[indexType]);
const offset = bufferCount(meshData, indexTypeDescriptor.bufferType);
for (const index of new Uint32Array(mesh[indexType]))
meshData[indexType].push(index + offset);
}
for (const bufferType of Object.keys(BUFFER_TYPES) as MeshBufferType[]) {
if (BUFFER_TYPES[bufferType] !== 'ARRAY_BUFFER')
continue;
meshData[bufferType].push(...new Float32Array(mesh[bufferType]));
const pathIDBufferType = PATH_ID_BUFFER_TABLE[bufferType];
if (pathIDBufferType != null) {
const length = bufferCount(meshData, bufferType);
while (meshData[pathIDBufferType].length < length)
meshData[pathIDBufferType].push(destMeshIndex + 1);
}
}
for (const pathRangeType of Object.keys(PATH_RANGE_TO_BUFFER_TYPE_TABLE) as
Array<keyof PathRanges>) {
const bufferType = PATH_RANGE_TO_BUFFER_TYPE_TABLE[pathRangeType];
const endIndex = bufferCount(meshData, bufferType);
unwrapUndef(_.last(pathRanges[pathRangeType])).end = endIndex;
}
}
for (const bufferType of Object.keys(BUFFER_TYPES) as PackedMeshBufferType[]) {
const arrayCtor = PRIMITIVE_TYPE_ARRAY_CONSTRUCTORS[MESH_TYPES[bufferType].type];
this[bufferType] = (new arrayCtor(meshData[bufferType])) as any;
}
_.assign(this, pathRanges);
}
count(bufferType: MeshBufferType): number {
return bufferCount(this, bufferType);
}
}
export class PathfinderPackedMeshBuffers implements PackedMeshLike<WebGLBuffer>, PathRanges {
readonly bBoxes!: WebGLBuffer;
readonly bQuadVertexInteriorIndices!: WebGLBuffer;
readonly bQuadVertexPositions!: WebGLBuffer;
readonly stencilSegments!: WebGLBuffer;
readonly stencilNormals!: WebGLBuffer;
readonly bBoxPathIDs!: WebGLBuffer;
readonly bQuadVertexPositionPathIDs!: WebGLBuffer;
readonly stencilSegmentPathIDs!: WebGLBuffer;
readonly bBoxPathRanges!: Range[];
readonly bQuadVertexInteriorIndexPathRanges!: Range[];
readonly bQuadVertexPositionPathRanges!: Range[];
readonly stencilSegmentPathRanges!: Range[];
constructor(gl: WebGLRenderingContext, packedMeshes: PathfinderPackedMeshes) {
for (const bufferName of Object.keys(BUFFER_TYPES) as PackedMeshBufferType[]) {
const bufferType = gl[BUFFER_TYPES[bufferName]];
const buffer = expectNotNull(gl.createBuffer(), "Failed to create buffer!");
gl.bindBuffer(bufferType, buffer);
gl.bufferData(bufferType, packedMeshes[bufferName], gl.STATIC_DRAW);
this[bufferName] = buffer;
}
for (const rangeName of Object.keys(PATH_RANGE_TO_BUFFER_TYPE_TABLE) as
Array<keyof PathRanges>) {
this[rangeName] = packedMeshes[rangeName];
}
}
}
function bufferCount(mesh: MeshLike<ArrayLike>, bufferType: MeshBufferType): number {
return mesh[bufferType].length / MESH_TYPES[bufferType].size;
}
function sizeOfPrimitive(primitiveType: PrimitiveType): number {
switch (primitiveType) {
case 'Uint16': return UINT16_SIZE;
case 'Uint32': return UINT32_SIZE;
case 'Float32': return FLOAT32_SIZE;
}
}
function toFourCC(buffer: ArrayBuffer, position: number): string {
let result = "";
const bytes = new Uint8Array(buffer, position, 4);
for (const byte of bytes)
result += String.fromCharCode(byte);
return result;
}
export function parseServerTiming(headers: Headers): number {
if (!headers.has('Server-Timing'))
return 0.0;
const timing = headers.get('Server-Timing')!;
const matches = /^Partitioning\s*=\s*([0-9.]+)$/.exec(timing);
return matches != null ? parseFloat(matches[1]) / 1000.0 : 0.0;
}
function readUInt32(buffer: ArrayBuffer, offset: number): number {
return (new Uint32Array(buffer.slice(offset, offset + 4)))[0];
}

View File

@ -1,917 +0,0 @@
// pathfinder/client/src/reference-test.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.
import * as glmatrix from 'gl-matrix';
import * as imageSSIM from 'image-ssim';
import * as _ from 'lodash';
import * as papaparse from 'papaparse';
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from './aa-strategy';
import {SubpixelAAType} from './aa-strategy';
import {DemoAppController, setSwitchInputsValue} from "./app-controller";
import {SUBPIXEL_GRANULARITY} from './atlas';
import {OrthographicCamera} from './camera';
import {UniformMap} from './gl-utils';
import {B_QUAD_UPPER_CONTROL_POINT_VERTEX_OFFSET, PathfinderMeshPack} from './meshes';
import {PathfinderPackedMeshBuffers, PathfinderPackedMeshes} from './meshes';
import {PathTransformBuffers, Renderer} from "./renderer";
import {ShaderMap, ShaderProgramSource} from "./shader-loader";
import SSAAStrategy from './ssaa-strategy';
import {BUILTIN_SVG_URI, SVGLoader} from './svg-loader';
import {SVGRenderer} from './svg-renderer';
import {BUILTIN_FONT_URI, computeStemDarkeningAmount, ExpandedMeshData, GlyphStore} from "./text";
import {Hint} from "./text";
import {PathfinderFont, TextFrame, TextRun} from "./text";
import {MAX_SUBPIXEL_AA_FONT_SIZE} from './text-renderer';
import {unwrapNull} from "./utils";
import {DemoView} from "./view";
import {AdaptiveStencilMeshAAAStrategy} from './xcaa-strategy';
const FONT: string = 'open-sans';
const TEXT_COLOR: number[] = [0, 0, 0, 255];
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
none: NoAAStrategy,
ssaa: SSAAStrategy,
xcaa: AdaptiveStencilMeshAAAStrategy,
};
const RENDER_REFERENCE_URIS: PerTestType<string> = {
font: "/render-reference/text",
svg: "/render-reference/svg",
};
const TEST_DATA_URI: string = "/test-data/reference-test-text.csv";
const SSIM_TOLERANCE: number = 0.01;
const SSIM_WINDOW_SIZE: number = 8;
const FILES: PerTestType<File[]> = {
font: [
{ id: 'open-sans', title: "Open Sans" },
{ id: 'eb-garamond', title: "EB Garamond" },
{ id: 'nimbus-sans', title: "Nimbus Sans" },
],
svg: [
{ id: 'tiger', title: "Ghostscript Tiger" },
],
};
interface ReferenceTestGroup {
font: string;
tests: ReferenceTestCase[];
}
interface ReferenceTestCase {
size: number;
character: string;
aaMode: keyof AntialiasingStrategyTable;
subpixel: boolean;
referenceRenderer: ReferenceRenderer;
expectedSSIM: number;
}
interface PerTestType<T> {
font: T;
svg: T;
}
interface File {
id: string;
title: string;
}
type ReferenceRenderer = 'core-graphics' | 'freetype';
interface AntialiasingStrategyTable {
none: typeof NoAAStrategy;
ssaa: typeof SSAAStrategy;
xcaa: typeof AdaptiveStencilMeshAAAStrategy;
}
class ReferenceTestAppController extends DemoAppController<ReferenceTestView> {
font: PathfinderFont | null = null;
textRun: TextRun | null = null;
svgLoader!: SVGLoader;
builtinSvgName: string | null = null;
referenceCanvas!: HTMLCanvasElement;
tests!: Promise<ReferenceTestGroup[]>;
currentTestType!: 'font' | 'svg';
protected readonly defaultFile: string = FONT;
protected get builtinFileURI(): string {
if (this.currentTestType === 'font')
return BUILTIN_FONT_URI;
return BUILTIN_SVG_URI;
}
private glyphStore!: GlyphStore;
private baseMeshes!: PathfinderMeshPack;
private expandedMeshes!: ExpandedMeshData;
private fontSizeInput!: HTMLInputElement;
private characterInput!: HTMLInputElement;
private referenceRendererSelect!: HTMLSelectElement;
private differenceCanvas!: HTMLCanvasElement;
private aaLevelGroup!: HTMLElement;
private customTabs!: PerTestType<HTMLElement>;
private customTestForms!: PerTestType<HTMLFormElement>;
private selectFileGroups!: PerTestType<HTMLElement>;
private runTestsButtons!: PerTestType<HTMLButtonElement>;
private ssimGroups!: PerTestType<HTMLElement>;
private ssimLabels!: PerTestType<HTMLElement>;
private resultsTables!: PerTestType<HTMLTableElement>;
private currentTestGroupIndex: number | null = null;
private currentTestCaseIndex: number | null = null;
private currentGlobalTestCaseIndex: number | null = null;
get currentFontSize(): number {
return parseInt(this.fontSizeInput.value, 10);
}
set currentFontSize(newFontSize: number) {
this.fontSizeInput.value = "" + newFontSize;
}
get currentCharacter(): string {
return this.characterInput.value;
}
set currentCharacter(newCharacter: string) {
this.characterInput.value = newCharacter;
}
get currentReferenceRenderer(): ReferenceRenderer {
return this.referenceRendererSelect.value as ReferenceRenderer;
}
set currentReferenceRenderer(newReferenceRenderer: ReferenceRenderer) {
this.referenceRendererSelect.value = newReferenceRenderer;
}
start(): void {
this.referenceRendererSelect =
unwrapNull(document.getElementById('pf-font-reference-renderer')) as HTMLSelectElement;
this.referenceRendererSelect.addEventListener('change', () => {
this.view.then(view => this.runSingleTest(view));
}, false);
super.start();
this.currentTestGroupIndex = null;
this.currentTestCaseIndex = null;
this.currentGlobalTestCaseIndex = null;
this.referenceCanvas = unwrapNull(document.getElementById('pf-reference-canvas')) as
HTMLCanvasElement;
this.fontSizeInput = unwrapNull(document.getElementById('pf-font-size')) as
HTMLInputElement;
this.fontSizeInput.addEventListener('change', () => {
this.view.then(view => this.runSingleTest(view));
}, false);
this.characterInput = unwrapNull(document.getElementById('pf-character')) as
HTMLInputElement;
this.characterInput.addEventListener('change', () => {
this.view.then(view => this.runSingleTest(view));
}, false);
this.aaLevelGroup = unwrapNull(document.getElementById('pf-aa-level-group')) as
HTMLElement;
this.differenceCanvas = unwrapNull(document.getElementById('pf-difference-canvas')) as
HTMLCanvasElement;
this.customTabs = {
font: unwrapNull(document.getElementById('pf-font-custom-test-tab')) as HTMLElement,
svg: unwrapNull(document.getElementById('pf-svg-custom-test-tab')) as HTMLElement,
};
this.customTestForms = {
font: unwrapNull(document.getElementById('pf-font-custom-form')) as HTMLFormElement,
svg: unwrapNull(document.getElementById('pf-svg-custom-form')) as HTMLFormElement,
};
this.selectFileGroups = {
font: unwrapNull(document.getElementById('pf-font-select-file-group')) as HTMLElement,
svg: unwrapNull(document.getElementById('pf-svg-select-file-group')) as HTMLElement,
};
this.runTestsButtons = {
font: unwrapNull(document.getElementById('pf-run-font-tests-button')) as
HTMLButtonElement,
svg: unwrapNull(document.getElementById('pf-run-svg-tests-button')) as
HTMLButtonElement,
};
this.ssimGroups = {
font: unwrapNull(document.getElementById('pf-font-ssim-group')) as HTMLElement,
svg: unwrapNull(document.getElementById('pf-svg-ssim-group')) as HTMLElement,
};
this.ssimLabels = {
font: unwrapNull(document.getElementById('pf-font-ssim-label')) as HTMLElement,
svg: unwrapNull(document.getElementById('pf-svg-ssim-label')) as HTMLElement,
};
this.resultsTables = {
font: unwrapNull(document.getElementById('pf-font-results-table')) as HTMLTableElement,
svg: unwrapNull(document.getElementById('pf-svg-results-table')) as HTMLTableElement,
};
this.customTabs.font.addEventListener('click',
() => this.showCustomTabPane('font'),
false);
this.customTabs.svg.addEventListener('click', () => this.showCustomTabPane('svg'), false);
this.runTestsButtons.font.addEventListener('click', () => {
this.view.then(view => this.runTests());
}, false);
this.runTestsButtons.svg.addEventListener('click', () => {
this.view.then(view => this.runTests());
}, false);
this.currentTestType = 'font';
this.loadTestData();
this.populateResultsTable();
this.populateFilesSelect();
this.loadInitialFile(this.builtinFileURI);
}
runNextTestIfNecessary(view: ReferenceTestView, tests: ReferenceTestGroup[]): void {
if (this.currentTestGroupIndex == null || this.currentTestCaseIndex == null ||
this.currentGlobalTestCaseIndex == null) {
return;
}
this.currentTestCaseIndex++;
this.currentGlobalTestCaseIndex++;
if (this.currentTestCaseIndex === tests[this.currentTestGroupIndex].tests.length) {
this.currentTestCaseIndex = 0;
this.currentTestGroupIndex++;
if (this.currentTestGroupIndex === tests.length) {
// Done running tests.
this.currentTestCaseIndex = null;
this.currentTestGroupIndex = null;
this.currentGlobalTestCaseIndex = null;
this.view.then(view => view.suppressAutomaticRedraw = false);
return;
}
}
this.loadFontForTestGroupIfNecessary(tests).then(() => {
this.setOptionsForCurrentTest(tests).then(() => this.runSingleTest(view));
});
}
recordSSIMResult(tests: ReferenceTestGroup[], ssimResult: imageSSIM.IResult): void {
const formattedSSIM: string = "" + (Math.round(ssimResult.ssim * 1000.0) / 1000.0);
this.ssimLabels[this.currentTestType].textContent = formattedSSIM;
if (this.currentTestGroupIndex == null || this.currentTestCaseIndex == null ||
this.currentGlobalTestCaseIndex == null) {
return;
}
const testGroup = tests[this.currentTestGroupIndex];
const expectedSSIM = testGroup.tests[this.currentTestCaseIndex].expectedSSIM;
const passed = Math.abs(expectedSSIM - ssimResult.ssim) <= SSIM_TOLERANCE;
const resultsBody: Element = unwrapNull(this.resultsTables[this.currentTestType]
.lastElementChild);
let resultsRow = unwrapNull(resultsBody.firstElementChild);
for (let rowIndex = 0; rowIndex < this.currentGlobalTestCaseIndex; rowIndex++)
resultsRow = unwrapNull(resultsRow.nextElementSibling);
const passCell = unwrapNull(resultsRow.firstElementChild);
const resultsCell = unwrapNull(resultsRow.lastElementChild);
resultsCell.textContent = formattedSSIM;
passCell.textContent = passed ? "✓" : "✗";
resultsRow.classList.remove('table-success', 'table-danger');
resultsRow.classList.add(passed ? 'table-success' : 'table-danger');
}
drawDifferenceImage(differenceImage: imageSSIM.IImage): void {
const canvas = this.differenceCanvas;
const context = unwrapNull(canvas.getContext('2d'));
context.fillStyle = 'white';
context.fillRect(0, 0, canvas.width, canvas.height);
const data = new Uint8ClampedArray(differenceImage.data);
const imageData = new ImageData(data, differenceImage.width, differenceImage.height);
context.putImageData(imageData, 0, 0);
}
protected createView(areaLUT: HTMLImageElement,
gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>):
ReferenceTestView {
return new ReferenceTestView(this, areaLUT, gammaLUT, commonShaderSource, shaderSources);
}
protected fileLoaded(fileData: ArrayBuffer, builtinName: string | null): void {
switch (this.currentTestType) {
case 'font':
this.textFileLoaded(fileData, builtinName);
break;
case 'svg':
this.svgFileLoaded(fileData, builtinName);
break;
}
// Don't automatically run the test unless this is a custom test.
if (this.currentGlobalTestCaseIndex == null)
this.view.then(view => this.runSingleTest(view));
}
private textFileLoaded(fileData: ArrayBuffer, builtinName: string | null): void {
const font = new PathfinderFont(fileData, builtinName);
this.font = font;
}
private svgFileLoaded(fileData: ArrayBuffer, builtinName: string | null): void {
this.builtinSvgName = builtinName;
this.svgLoader = new SVGLoader;
this.svgLoader.loadFile(fileData);
}
private populateFilesSelect(): void {
const selectFileElement = unwrapNull(this.selectFileElement);
while (selectFileElement.lastChild != null)
selectFileElement.removeChild(selectFileElement.lastChild);
for (const file of FILES[this.currentTestType]) {
const option = document.createElement('option');
option.value = file.id;
option.appendChild(document.createTextNode(file.title));
selectFileElement.appendChild(option);
}
}
private loadTestData(): void {
this.tests = window.fetch(TEST_DATA_URI)
.then(response => response.text())
.then(testDataText => {
const fontNames = [];
const groups: {[font: string]: ReferenceTestCase[]} = {};
const testData = papaparse.parse(testDataText, {
comments: "#",
header: true,
skipEmptyLines: true,
});
for (const row of testData.data) {
if (!groups.hasOwnProperty(row.Font)) {
fontNames.push(row.Font);
groups[row.Font] = [];
}
groups[row.Font].push({
aaMode: row['AA Mode'] as keyof AntialiasingStrategyTable,
character: row.Character,
expectedSSIM: parseFloat(row['Expected SSIM']),
referenceRenderer: row['Reference Renderer'],
size: parseInt(row.Size, 10),
subpixel: !!row.Subpixel,
});
}
return fontNames.map(fontName => {
return {
font: fontName,
tests: groups[fontName],
};
});
});
}
private populateResultsTable(): void {
this.tests.then(tests => {
const resultsBody: Element = unwrapNull(this.resultsTables[this.currentTestType]
.lastElementChild);
for (const testGroup of tests) {
for (const test of testGroup.tests) {
const row = document.createElement('tr');
addCell(row, "");
addCell(row, testGroup.font);
addCell(row, test.character);
addCell(row, "" + test.size);
addCell(row, "" + test.aaMode);
addCell(row, test.subpixel ? "Y" : "N");
addCell(row, test.referenceRenderer);
addCell(row, "" + test.expectedSSIM);
addCell(row, "");
resultsBody.appendChild(row);
}
}
});
}
private runSingleTest(view: ReferenceTestView): void {
if (this.currentTestType === 'font')
this.setUpTextRun();
this.loadReference(view).then(() => this.loadRendering());
}
private runTests(): void {
this.view.then(view => {
view.suppressAutomaticRedraw = true;
this.tests.then(tests => {
this.currentTestGroupIndex = 0;
this.currentTestCaseIndex = 0;
this.currentGlobalTestCaseIndex = 0;
this.loadFontForTestGroupIfNecessary(tests).then(() => {
this.setOptionsForCurrentTest(tests).then(() => this.runSingleTest(view));
});
});
});
}
private loadFontForTestGroupIfNecessary(tests: ReferenceTestGroup[]): Promise<void> {
return new Promise(resolve => {
if (this.currentTestGroupIndex == null) {
resolve();
return;
}
this.fetchFile(tests[this.currentTestGroupIndex].font, BUILTIN_FONT_URI).then(() => {
resolve();
});
});
}
private setOptionsForCurrentTest(tests: ReferenceTestGroup[]): Promise<void> {
if (this.currentTestGroupIndex == null || this.currentTestCaseIndex == null)
return new Promise(resolve => resolve());
const currentTestCase = tests[this.currentTestGroupIndex].tests[this.currentTestCaseIndex];
this.currentFontSize = currentTestCase.size;
this.currentCharacter = currentTestCase.character;
this.currentReferenceRenderer = currentTestCase.referenceRenderer;
const aaLevelSelect = unwrapNull(this.aaLevelSelect);
aaLevelSelect.selectedIndex = _.findIndex(aaLevelSelect.options, option => {
return option.value.startsWith(currentTestCase.aaMode);
});
const subpixelAASelect = unwrapNull(this.subpixelAASelect);
subpixelAASelect.selectedIndex = currentTestCase.subpixel ? 1 : 0;
return this.updateAALevel();
}
private setUpTextRun(): void {
const font = unwrapNull(this.font);
const textRun = new TextRun(this.currentCharacter, [0, 0], font);
textRun.layout();
this.textRun = textRun;
this.glyphStore = new GlyphStore(font, [textRun.glyphIDs[0]]);
}
private loadRendering(): void {
switch (this.currentTestType) {
case 'font':
this.loadTextRendering();
break;
case 'svg':
this.loadSVGRendering();
break;
}
}
private loadTextRendering(): void {
this.glyphStore.partition().then(result => {
const textRun = unwrapNull(this.textRun);
this.baseMeshes = result.meshes;
const textFrame = new TextFrame([textRun], unwrapNull(this.font));
const expandedMeshes = textFrame.expandMeshes(this.baseMeshes, [textRun.glyphIDs[0]]);
this.expandedMeshes = expandedMeshes;
this.view.then(view => {
view.recreateRenderer();
view.attachMeshes([expandedMeshes.meshes]);
view.redraw();
});
});
}
private loadSVGRendering(): void {
this.svgLoader.partition().then(meshes => {
this.view.then(view => {
view.recreateRenderer();
view.attachMeshes([new PathfinderPackedMeshes(meshes)]);
view.initCameraBounds(this.svgLoader.svgViewBox);
});
});
}
private loadReference(view: ReferenceTestView): Promise<void> {
let request;
switch (this.currentTestType) {
case 'font':
request = {
face: {
Builtin: unwrapNull(this.font).builtinFontName,
},
fontIndex: 0,
glyph: this.glyphStore.glyphIDs[0],
pointSize: this.currentFontSize,
renderer: this.currentReferenceRenderer,
};
break;
case 'svg':
// TODO(pcwalton): Custom SVGs.
request = {
name: unwrapNull(this.builtinSvgName),
renderer: 'pixman',
scale: 1.0,
};
break;
}
return window.fetch(RENDER_REFERENCE_URIS[this.currentTestType], {
body: JSON.stringify(request),
headers: {'Content-Type': 'application/json'} as any,
method: 'POST',
}).then(response => response.blob()).then(blob => {
const imgElement = document.createElement('img');
imgElement.src = URL.createObjectURL(blob);
imgElement.addEventListener('load', () => {
const canvas = this.referenceCanvas;
const context = unwrapNull(canvas.getContext('2d'));
context.fillStyle = 'white';
context.fillRect(0, 0, canvas.width, canvas.height);
context.drawImage(imgElement, 0, 0);
}, false);
});
}
private showCustomTabPane(testType: 'font' | 'svg'): void {
this.currentTestType = testType;
const selectFileElement = unwrapNull(this.selectFileElement);
const aaLevelGroup = unwrapNull(this.aaLevelGroup);
const customTestForm = this.customTestForms[testType];
const selectFileGroup = this.selectFileGroups[testType];
const ssimGroup = this.ssimGroups[testType];
unwrapNull(selectFileElement.parentNode).removeChild(selectFileElement);
unwrapNull(aaLevelGroup.parentNode).removeChild(aaLevelGroup);
selectFileGroup.appendChild(selectFileElement);
customTestForm.insertBefore(aaLevelGroup, ssimGroup);
this.populateFilesSelect();
}
}
class ReferenceTestView extends DemoView {
renderer!: ReferenceTestTextRenderer | ReferenceTestSVGRenderer;
readonly appController: ReferenceTestAppController;
get camera(): OrthographicCamera {
return this.renderer.camera;
}
constructor(appController: ReferenceTestAppController,
areaLUT: HTMLImageElement,
gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>) {
super(areaLUT, gammaLUT, commonShaderSource, shaderSources);
this.appController = appController;
this.recreateRenderer();
this.resizeToFit(true);
}
recreateRenderer(): void {
switch (this.appController.currentTestType) {
case 'svg':
this.renderer = new ReferenceTestSVGRenderer(this);
break;
case 'font':
this.renderer = new ReferenceTestTextRenderer(this);
break;
}
}
initCameraBounds(viewBox: glmatrix.vec4): void {
if (this.renderer instanceof ReferenceTestSVGRenderer)
this.renderer.initCameraBounds(viewBox);
}
protected renderingFinished(): void {
const gl = this.renderContext.gl;
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// TODO(pcwalton): Get the SVG pixel rect.
let pixelRect: glmatrix.vec4 = glmatrix.vec4.create();
if (this.renderer instanceof ReferenceTestTextRenderer)
pixelRect = this.renderer.getPixelRectForGlyphAt(0);
const canvasHeight = this.canvas.height;
const width = pixelRect[2] - pixelRect[0], height = pixelRect[3] - pixelRect[1];
const originY = Math.max(canvasHeight - height, 0);
const flippedBuffer = new Uint8Array(width * height * 4);
gl.readPixels(0, originY, width, height, gl.RGBA, gl.UNSIGNED_BYTE, flippedBuffer);
const buffer = new Uint8Array(width * height * 4);
for (let y = 0; y < height; y++) {
const destRowStart = y * width * 4;
const srcRowStart = (height - y - 1) * width * 4;
buffer.set(flippedBuffer.slice(srcRowStart, srcRowStart + width * 4),
destRowStart);
}
const renderedImage = createSSIMImage(buffer, pixelRect);
this.appController.tests.then(tests => {
const referenceImage = createSSIMImage(this.appController.referenceCanvas,
pixelRect);
const ssimResult = imageSSIM.compare(referenceImage, renderedImage, SSIM_WINDOW_SIZE);
const differenceImage = generateDifferenceImage(referenceImage, renderedImage);
this.appController.recordSSIMResult(tests, ssimResult);
this.appController.drawDifferenceImage(differenceImage);
this.appController.runNextTestIfNecessary(this, tests);
});
}
}
class ReferenceTestTextRenderer extends Renderer {
renderContext!: ReferenceTestView;
camera: OrthographicCamera;
needsStencil: boolean = false;
isMulticolor: boolean = false;
get destFramebuffer(): WebGLFramebuffer | null {
return null;
}
get bgColor(): glmatrix.vec4 {
return glmatrix.vec4.clone([1.0, 1.0, 1.0, 1.0]);
}
get fgColor(): glmatrix.vec4 {
return glmatrix.vec4.clone([0.0, 0.0, 0.0, 1.0]);
}
get destAllocatedSize(): glmatrix.vec2 {
const canvas = this.renderContext.canvas;
return glmatrix.vec2.clone([canvas.width, canvas.height]);
}
get destUsedSize(): glmatrix.vec2 {
return this.destAllocatedSize;
}
get emboldenAmount(): glmatrix.vec2 {
return this.stemDarkeningAmount;
}
get allowSubpixelAA(): boolean {
const appController = this.renderContext.appController;
return appController.currentFontSize <= MAX_SUBPIXEL_AA_FONT_SIZE;
}
protected get objectCount(): number {
return this.meshBuffers == null ? 0 : this.meshBuffers.length;
}
protected get usedSizeFactor(): glmatrix.vec2 {
return glmatrix.vec2.clone([1.0, 1.0]);
}
protected get worldTransform(): glmatrix.mat4 {
const canvas = this.renderContext.canvas;
const transform = glmatrix.mat4.create();
const translation = this.camera.translation;
glmatrix.mat4.translate(transform, transform, [-1.0, -1.0, 0.0]);
glmatrix.mat4.scale(transform, transform, [2.0 / canvas.width, 2.0 / canvas.height, 1.0]);
glmatrix.mat4.translate(transform, transform, [translation[0], translation[1], 0]);
glmatrix.mat4.scale(transform, transform, [this.camera.scale, this.camera.scale, 1.0]);
const pixelsPerUnit = this.pixelsPerUnit;
glmatrix.mat4.scale(transform, transform, [pixelsPerUnit, pixelsPerUnit, 1.0]);
return transform;
}
private get pixelsPerUnit(): number {
const appController = this.renderContext.appController;
const font = unwrapNull(appController.font);
return appController.currentFontSize / font.opentypeFont.unitsPerEm;
}
private get stemDarkeningAmount(): glmatrix.vec2 {
const appController = this.renderContext.appController;
return computeStemDarkeningAmount(appController.currentFontSize, this.pixelsPerUnit);
}
constructor(renderContext: ReferenceTestView) {
super(renderContext);
this.camera = new OrthographicCamera(renderContext.canvas, { fixed: true });
this.camera.onPan = () => renderContext.setDirty();
this.camera.onZoom = () => renderContext.setDirty();
}
attachMeshes(meshes: PathfinderPackedMeshes[]): void {
super.attachMeshes(meshes);
this.uploadPathColors(1);
this.uploadPathTransforms(1);
}
pathCountForObject(objectIndex: number): number {
return 1;
}
pathBoundingRects(objectIndex: number): Float32Array {
const appController = this.renderContext.appController;
const font = unwrapNull(appController.font);
const boundingRects = new Float32Array(2 * 4);
const glyphID = unwrapNull(appController.textRun).glyphIDs[0];
const metrics = unwrapNull(font.metricsForGlyph(glyphID));
boundingRects[4 + 0] = metrics.xMin;
boundingRects[4 + 1] = metrics.yMin;
boundingRects[4 + 2] = metrics.xMax;
boundingRects[4 + 3] = metrics.yMax;
return boundingRects;
}
setHintsUniform(uniforms: UniformMap): void {
this.renderContext.gl.uniform4f(uniforms.uHints, 0, 0, 0, 0);
}
getPixelRectForGlyphAt(glyphIndex: number): glmatrix.vec4 {
const textRun = unwrapNull(this.renderContext.appController.textRun);
return textRun.pixelRectForGlyphAt(glyphIndex);
}
pathTransformsForObject(objectIndex: number): PathTransformBuffers<Float32Array> {
const appController = this.renderContext.appController;
const canvas = this.renderContext.canvas;
const font = unwrapNull(appController.font);
const hint = new Hint(font, this.pixelsPerUnit, true);
const pathTransforms = this.createPathTransformBuffers(1);
const textRun = unwrapNull(appController.textRun);
const glyphID = textRun.glyphIDs[0];
textRun.recalculatePixelRects(this.pixelsPerUnit,
0.0,
hint,
glmatrix.vec2.create(),
SUBPIXEL_GRANULARITY,
glmatrix.vec4.create());
const pixelRect = textRun.pixelRectForGlyphAt(0);
const x = -pixelRect[0] / this.pixelsPerUnit;
const y = (canvas.height - (pixelRect[3] - pixelRect[1])) / this.pixelsPerUnit;
pathTransforms.st.set([1, 1, x, y], 1 * 4);
return pathTransforms;
}
protected createAAStrategy(aaType: AntialiasingStrategyName,
aaLevel: number,
subpixelAA: SubpixelAAType):
AntialiasingStrategy {
return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA);
}
protected compositeIfNecessary(): void {}
protected pathColorsForObject(objectIndex: number): Uint8Array {
const pathColors = new Uint8Array(4 * 2);
pathColors.set(TEXT_COLOR, 1 * 4);
return pathColors;
}
protected directCurveProgramName(): keyof ShaderMap<void> {
return 'directCurve';
}
protected directInteriorProgramName(): keyof ShaderMap<void> {
return 'directInterior';
}
}
class ReferenceTestSVGRenderer extends SVGRenderer {
renderContext!: ReferenceTestView;
protected get loader(): SVGLoader {
return this.renderContext.appController.svgLoader;
}
protected get canvas(): HTMLCanvasElement {
return this.renderContext.canvas;
}
constructor(renderContext: ReferenceTestView) {
super(renderContext, { sizeToFit: false, fixed: true });
}
}
function createSSIMImage(image: HTMLCanvasElement | Uint8Array, rect: glmatrix.vec4):
imageSSIM.IImage {
const size = glmatrix.vec2.clone([rect[2] - rect[0], rect[3] - rect[1]]);
let data;
if (image instanceof HTMLCanvasElement) {
const context = unwrapNull(image.getContext('2d'));
data = new Uint8Array(context.getImageData(0, 0, size[0], size[1]).data);
} else {
data = image;
}
return {
channels: imageSSIM.Channels.RGBAlpha,
data: data,
height: size[1],
width: size[0],
};
}
function generateDifferenceImage(referenceImage: imageSSIM.IImage,
renderedImage: imageSSIM.IImage):
imageSSIM.IImage {
const differenceImage = new Uint8Array(referenceImage.width * referenceImage.height * 4);
for (let y = 0; y < referenceImage.height; y++) {
const rowStart = y * referenceImage.width * 4;
for (let x = 0; x < referenceImage.width; x++) {
const pixelStart = rowStart + x * 4;
let differenceSum = 0;
for (let channel = 0; channel < 3; channel++) {
differenceSum += Math.abs(referenceImage.data[pixelStart + channel] -
renderedImage.data[pixelStart + channel]);
}
if (differenceSum === 0) {
// Lighten to indicate no difference.
for (let channel = 0; channel < 4; channel++) {
differenceImage[pixelStart + channel] =
Math.floor(referenceImage.data[pixelStart + channel] / 2) + 128;
}
continue;
}
// Draw differences in red.
const differenceMean = differenceSum / 3;
differenceImage[pixelStart + 0] = 127 + Math.round(differenceMean / 2);
differenceImage[pixelStart + 1] = differenceImage[pixelStart + 2] = 0;
differenceImage[pixelStart + 3] = 255;
}
}
return {
channels: referenceImage.channels,
data: differenceImage,
height: referenceImage.height,
width: referenceImage.width,
};
}
function addCell(row: HTMLTableRowElement, text: string): void {
const tableCell = document.createElement('td');
tableCell.textContent = text;
row.appendChild(tableCell);
}
function main() {
const controller = new ReferenceTestAppController;
window.addEventListener('load', () => controller.start(), false);
}
main();

View File

@ -1,847 +0,0 @@
// pathfinder/client/src/renderer.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.
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import {AntialiasingStrategy, AntialiasingStrategyName, DirectRenderingMode} from './aa-strategy';
import {GammaCorrectionMode} from './aa-strategy';
import {TileInfo} from './aa-strategy';
import {NoAAStrategy, StemDarkeningMode, SubpixelAAType} from './aa-strategy';
import {AAOptions} from './app-controller';
import PathfinderBufferTexture from "./buffer-texture";
import {UniformMap, WebGLQuery} from './gl-utils';
import {PathfinderPackedMeshBuffers, PathfinderPackedMeshes} from "./meshes";
import {ShaderMap} from './shader-loader';
import {FLOAT32_SIZE, Range, UINT16_SIZE, UINT32_SIZE, unwrapNull, unwrapUndef} from './utils';
import {RenderContext, Timings} from "./view";
const MAX_PATHS: number = 65535;
const MAX_VERTICES: number = 4 * 1024 * 1024;
const TIME_INTERVAL_DELAY: number = 32;
const B_LOOP_BLINN_DATA_SIZE: number = 4;
const B_LOOP_BLINN_DATA_TEX_COORD_OFFSET: number = 0;
const B_LOOP_BLINN_DATA_SIGN_OFFSET: number = 2;
export interface PathTransformBuffers<T> {
st: T;
ext: T;
}
export abstract class Renderer {
readonly renderContext: RenderContext;
readonly pathTransformBufferTextures: Array<PathTransformBuffers<PathfinderBufferTexture>>;
meshBuffers: PathfinderPackedMeshBuffers[] | null;
meshes: PathfinderPackedMeshes[] | null;
lastTimings: Timings;
inVR: boolean = false;
get emboldenAmount(): glmatrix.vec2 {
return glmatrix.vec2.create();
}
get bgColor(): glmatrix.vec4 {
return glmatrix.vec4.clone([1.0, 1.0, 1.0, 1.0]);
}
get fgColor(): glmatrix.vec4 | null {
return null;
}
get backgroundColor(): glmatrix.vec4 {
return glmatrix.vec4.clone([1.0, 1.0, 1.0, 1.0]);
}
get meshesAttached(): boolean {
return this.meshBuffers != null && this.meshes != null;
}
abstract get isMulticolor(): boolean;
abstract get needsStencil(): boolean;
abstract get allowSubpixelAA(): boolean;
abstract get destFramebuffer(): WebGLFramebuffer | null;
abstract get destAllocatedSize(): glmatrix.vec2;
abstract get destUsedSize(): glmatrix.vec2;
protected antialiasingStrategy: AntialiasingStrategy | null;
protected pathColorsBufferTextures: PathfinderBufferTexture[];
protected gammaCorrectionMode: GammaCorrectionMode;
protected get pathIDsAreInstanced(): boolean {
return false;
}
protected abstract get objectCount(): number;
protected abstract get usedSizeFactor(): glmatrix.vec2;
protected abstract get worldTransform(): glmatrix.mat4;
private implicitCoverInteriorVAO: WebGLVertexArrayObjectOES | null = null;
private implicitCoverCurveVAO: WebGLVertexArrayObjectOES | null = null;
private gammaLUTTexture: WebGLTexture | null = null;
private areaLUTTexture: WebGLTexture | null = null;
private instancedPathIDVBO: WebGLBuffer | null = null;
private vertexIDVBO: WebGLBuffer | null = null;
private timerQueryPollInterval: number | null = null;
constructor(renderContext: RenderContext) {
this.renderContext = renderContext;
this.meshes = null;
this.meshBuffers = null;
this.lastTimings = { rendering: 0, compositing: 0 };
this.gammaCorrectionMode = 'on';
this.pathTransformBufferTextures = [];
this.pathColorsBufferTextures = [];
if (this.pathIDsAreInstanced)
this.initInstancedPathIDVBO();
this.initVertexIDVBO();
this.initLUTTexture('gammaLUT', 'gammaLUTTexture');
this.initLUTTexture('areaLUT', 'areaLUTTexture');
this.antialiasingStrategy = new NoAAStrategy(0, 'none');
this.antialiasingStrategy.init(this);
this.antialiasingStrategy.setFramebufferSize(this);
}
attachMeshes(meshes: PathfinderPackedMeshes[]): void {
const renderContext = this.renderContext;
this.meshes = meshes;
this.meshBuffers = meshes.map(meshes => {
return new PathfinderPackedMeshBuffers(renderContext.gl, meshes);
});
unwrapNull(this.antialiasingStrategy).attachMeshes(this);
}
abstract pathBoundingRects(objectIndex: number): Float32Array;
abstract setHintsUniform(uniforms: UniformMap): void;
abstract pathTransformsForObject(objectIndex: number): PathTransformBuffers<Float32Array>;
redrawVR(frame: VRFrameData): void {
this.redraw();
}
setDrawViewport() {
const renderContext = this.renderContext;
const gl = renderContext.gl;
gl.viewport(0, 0, this.destAllocatedSize[0], this.destAllocatedSize[1]);
}
setClipPlanes(display: VRDisplay) {
}
redraw(): void {
const renderContext = this.renderContext;
if (this.meshBuffers == null)
return;
this.clearDestFramebuffer(false);
// Start timing rendering.
if (this.timerQueryPollInterval == null &&
renderContext.timerQueryExt != null &&
renderContext.atlasRenderingTimerQuery != null) {
renderContext.timerQueryExt.beginQueryEXT(renderContext.timerQueryExt.TIME_ELAPSED_EXT,
renderContext.atlasRenderingTimerQuery);
}
const antialiasingStrategy = unwrapNull(this.antialiasingStrategy);
antialiasingStrategy.prepareForRendering(this);
// Draw "scenery" (used in the 3D view).
this.drawSceneryIfNecessary();
const passCount = antialiasingStrategy.passCount;
for (let pass = 0; pass < passCount; pass++) {
if (antialiasingStrategy.directRenderingMode !== 'none')
antialiasingStrategy.prepareForDirectRendering(this);
const objectCount = this.objectCount;
for (let objectIndex = 0; objectIndex < objectCount; objectIndex++) {
if (antialiasingStrategy.directRenderingMode !== 'none') {
// Prepare for direct rendering.
antialiasingStrategy.prepareToRenderObject(this, objectIndex);
// Clear.
this.clearForDirectRendering(objectIndex);
// Perform direct rendering (Loop-Blinn).
this.directlyRenderObject(pass, objectIndex);
}
// Antialias.
antialiasingStrategy.antialiasObject(this, objectIndex);
// End the timer, and start a new one.
// FIXME(pcwalton): This is kinda bogus for multipass.
if (this.timerQueryPollInterval == null &&
objectIndex === objectCount - 1 &&
pass === passCount - 1 &&
renderContext.timerQueryExt != null &&
renderContext.compositingTimerQuery != null) {
renderContext.timerQueryExt
.endQueryEXT(renderContext.timerQueryExt.TIME_ELAPSED_EXT);
renderContext.timerQueryExt
.beginQueryEXT(renderContext.timerQueryExt.TIME_ELAPSED_EXT,
renderContext.compositingTimerQuery);
}
// Perform post-antialiasing tasks.
antialiasingStrategy.finishAntialiasingObject(this, objectIndex);
antialiasingStrategy.resolveAAForObject(this, objectIndex);
}
antialiasingStrategy.resolve(pass, this);
}
// Draw the glyphs with the resolved atlas to the default framebuffer.
this.compositeIfNecessary();
// Finish timing.
this.finishTiming();
}
setAntialiasingOptions(aaType: AntialiasingStrategyName,
aaLevel: number,
aaOptions: AAOptions):
void {
this.gammaCorrectionMode = aaOptions.gammaCorrection;
this.antialiasingStrategy = this.createAAStrategy(aaType,
aaLevel,
aaOptions.subpixelAA,
aaOptions.stemDarkening);
this.antialiasingStrategy.init(this);
if (this.meshes != null)
this.antialiasingStrategy.attachMeshes(this);
this.antialiasingStrategy.setFramebufferSize(this);
this.renderContext.setDirty();
}
canvasResized(): void {
if (this.antialiasingStrategy != null)
this.antialiasingStrategy.init(this);
}
setFramebufferSizeUniform(uniforms: UniformMap): void {
const gl = this.renderContext.gl;
gl.uniform2i(uniforms.uFramebufferSize,
this.destAllocatedSize[0],
this.destAllocatedSize[1]);
}
setTransformAndTexScaleUniformsForDest(uniforms: UniformMap, tileInfo?: TileInfo): void {
const renderContext = this.renderContext;
const usedSize = this.usedSizeFactor;
let tileSize, tilePosition;
if (tileInfo == null) {
tileSize = glmatrix.vec2.clone([1.0, 1.0]);
tilePosition = glmatrix.vec2.create();
} else {
tileSize = tileInfo.size;
tilePosition = tileInfo.position;
}
const transform = glmatrix.mat4.create();
glmatrix.mat4.fromTranslation(transform, [
-1.0 + tilePosition[0] / tileSize[0] * 2.0,
-1.0 + tilePosition[1] / tileSize[1] * 2.0,
0.0,
]);
glmatrix.mat4.scale(transform, transform, [2.0 * usedSize[0], 2.0 * usedSize[1], 1.0]);
glmatrix.mat4.scale(transform, transform, [1.0 / tileSize[0], 1.0 / tileSize[1], 1.0]);
renderContext.gl.uniformMatrix4fv(uniforms.uTransform, false, transform);
renderContext.gl.uniform2f(uniforms.uTexScale, usedSize[0], usedSize[1]);
}
setTransformSTAndTexScaleUniformsForDest(uniforms: UniformMap): void {
const renderContext = this.renderContext;
const gl = renderContext.gl;
const usedSize = this.usedSizeFactor;
gl.uniform4f(uniforms.uTransformST, 2.0 * usedSize[0], 2.0 * usedSize[1], -1.0, -1.0);
gl.uniform2f(uniforms.uTexScale, usedSize[0], usedSize[1]);
}
setTransformUniform(uniforms: UniformMap, pass: number, objectIndex: number): void {
const transform = this.computeTransform(pass, objectIndex);
this.renderContext.gl.uniformMatrix4fv(uniforms.uTransform, false, transform);
}
setTransformSTUniform(uniforms: UniformMap, objectIndex: number): void {
// FIXME(pcwalton): Lossy conversion from a 4x4 matrix to an ST matrix is ugly and fragile.
// Refactor.
const renderContext = this.renderContext;
const gl = renderContext.gl;
const transform = this.computeTransform(0, objectIndex);
gl.uniform4f(uniforms.uTransformST,
transform[0],
transform[5],
transform[12],
transform[13]);
}
affineTransform(objectIndex: number): glmatrix.mat2d {
// FIXME(pcwalton): Lossy conversion from a 4x4 matrix to an affine matrix is ugly and
// fragile. Refactor.
const transform = this.computeTransform(0, objectIndex);
return glmatrix.mat2d.fromValues(transform[0], transform[1],
transform[4], transform[5],
transform[12], transform[13]);
}
setTransformAffineUniforms(uniforms: UniformMap, objectIndex: number): void {
const renderContext = this.renderContext;
const gl = renderContext.gl;
const transform = this.affineTransform(objectIndex);
gl.uniform4f(uniforms.uTransformST,
transform[0],
transform[3],
transform[4],
transform[5]);
gl.uniform2f(uniforms.uTransformExt, transform[1], transform[2]);
}
uploadPathColors(objectCount: number): void {
const renderContext = this.renderContext;
for (let objectIndex = 0; objectIndex < objectCount; objectIndex++) {
const pathColors = this.pathColorsForObject(objectIndex);
let pathColorsBufferTexture;
if (objectIndex >= this.pathColorsBufferTextures.length) {
pathColorsBufferTexture = new PathfinderBufferTexture(renderContext.gl,
'uPathColors');
this.pathColorsBufferTextures[objectIndex] = pathColorsBufferTexture;
} else {
pathColorsBufferTexture = this.pathColorsBufferTextures[objectIndex];
}
pathColorsBufferTexture.upload(renderContext.gl, pathColors);
}
}
uploadPathTransforms(objectCount: number): void {
const renderContext = this.renderContext;
const gl = renderContext.gl;
for (let objectIndex = 0; objectIndex < objectCount; objectIndex++) {
const pathTransforms = this.pathTransformsForObject(objectIndex);
let pathTransformBufferTextures;
if (objectIndex >= this.pathTransformBufferTextures.length) {
pathTransformBufferTextures = {
ext: new PathfinderBufferTexture(gl, 'uPathTransformExt'),
st: new PathfinderBufferTexture(gl, 'uPathTransformST'),
};
this.pathTransformBufferTextures[objectIndex] = pathTransformBufferTextures;
} else {
pathTransformBufferTextures = this.pathTransformBufferTextures[objectIndex];
}
pathTransformBufferTextures.st.upload(gl, pathTransforms.st);
pathTransformBufferTextures.ext.upload(gl, pathTransforms.ext);
}
}
setPathColorsUniform(objectIndex: number, uniforms: UniformMap, textureUnit: number): void {
const gl = this.renderContext.gl;
const meshIndex = this.meshIndexForObject(objectIndex);
this.pathColorsBufferTextures[meshIndex].bind(gl, uniforms, textureUnit);
}
setEmboldenAmountUniform(objectIndex: number, uniforms: UniformMap): void {
const gl = this.renderContext.gl;
const emboldenAmount = this.emboldenAmount;
gl.uniform2f(uniforms.uEmboldenAmount, emboldenAmount[0], emboldenAmount[1]);
}
meshIndexForObject(objectIndex: number): number {
return objectIndex;
}
pathRangeForObject(objectIndex: number): Range {
if (this.meshBuffers == null)
return new Range(0, 0);
const bVertexPathRanges = this.meshBuffers[objectIndex].bQuadVertexPositionPathRanges;
return new Range(1, bVertexPathRanges.length + 1);
}
bindAreaLUT(textureUnit: number, uniforms: UniformMap): void {
const renderContext = this.renderContext;
const gl = renderContext.gl;
gl.activeTexture(gl.TEXTURE0 + textureUnit);
gl.bindTexture(gl.TEXTURE_2D, this.areaLUTTexture);
gl.uniform1i(uniforms.uAreaLUT, textureUnit);
}
protected clearColorForObject(objectIndex: number): glmatrix.vec4 | null {
return null;
}
protected bindGammaLUT(bgColor: glmatrix.vec3, textureUnit: number, uniforms: UniformMap):
void {
const renderContext = this.renderContext;
const gl = renderContext.gl;
gl.activeTexture(gl.TEXTURE0 + textureUnit);
gl.bindTexture(gl.TEXTURE_2D, this.gammaLUTTexture);
gl.uniform1i(uniforms.uGammaLUT, textureUnit);
gl.uniform3f(uniforms.uBGColor, bgColor[0], bgColor[1], bgColor[2]);
}
protected abstract createAAStrategy(aaType: AntialiasingStrategyName,
aaLevel: number,
subpixelAA: SubpixelAAType,
stemDarkening: StemDarkeningMode):
AntialiasingStrategy;
protected abstract compositeIfNecessary(): void;
protected abstract pathColorsForObject(objectIndex: number): Uint8Array;
protected abstract directCurveProgramName(): keyof ShaderMap<void>;
protected abstract directInteriorProgramName(renderingMode: DirectRenderingMode):
keyof ShaderMap<void>;
protected drawSceneryIfNecessary(): void {}
protected clearDestFramebuffer(force: boolean): void {
const renderContext = this.renderContext;
const gl = renderContext.gl;
const clearColor = this.backgroundColor;
gl.bindFramebuffer(gl.FRAMEBUFFER, this.destFramebuffer);
gl.depthMask(true);
gl.viewport(0, 0, this.destAllocatedSize[0], this.destAllocatedSize[1]);
gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
gl.clearDepth(0.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
protected clearForDirectRendering(objectIndex: number): void {
const renderingMode = unwrapNull(this.antialiasingStrategy).directRenderingMode;
const renderContext = this.renderContext;
const gl = renderContext.gl;
const clearColor = this.clearColorForObject(objectIndex);
if (clearColor == null)
return;
gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
gl.clearDepth(0.0);
gl.depthMask(true);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
protected getModelviewTransform(pathIndex: number): glmatrix.mat4 {
return glmatrix.mat4.create();
}
/// If non-instanced, returns instance 0. An empty range skips rendering the object entirely.
protected instanceRangeForObject(objectIndex: number): Range {
return new Range(0, 1);
}
/// Called whenever new GPU timing statistics are available.
protected newTimingsReceived(): void {}
protected createPathTransformBuffers(pathCount: number): PathTransformBuffers<Float32Array> {
pathCount += 1;
return {
ext: new Float32Array((pathCount + (pathCount & 1)) * 2),
st: new Float32Array(pathCount * 4),
};
}
private directlyRenderObject(pass: number, objectIndex: number): void {
if (this.meshBuffers == null || this.meshes == null)
return;
const renderContext = this.renderContext;
const gl = renderContext.gl;
const antialiasingStrategy = unwrapNull(this.antialiasingStrategy);
const renderingMode = antialiasingStrategy.directRenderingMode;
const objectCount = this.objectCount;
const instanceRange = this.instanceRangeForObject(objectIndex);
if (instanceRange.isEmpty)
return;
const pathRange = this.pathRangeForObject(objectIndex);
const meshIndex = this.meshIndexForObject(objectIndex);
const meshes = this.meshBuffers![meshIndex];
const meshData = this.meshes![meshIndex];
// Set up implicit cover state.
gl.depthFunc(gl.GREATER);
gl.depthMask(true);
gl.enable(gl.DEPTH_TEST);
gl.disable(gl.BLEND);
gl.cullFace(gl.BACK);
gl.frontFace(gl.CCW);
gl.enable(gl.CULL_FACE);
// Set up the implicit cover interior VAO.
const directInteriorProgramName = this.directInteriorProgramName(renderingMode);
const directInteriorProgram = renderContext.shaderPrograms[directInteriorProgramName];
if (this.implicitCoverInteriorVAO == null) {
this.implicitCoverInteriorVAO = renderContext.vertexArrayObjectExt
.createVertexArrayOES();
}
renderContext.vertexArrayObjectExt.bindVertexArrayOES(this.implicitCoverInteriorVAO);
this.initImplicitCoverInteriorVAO(objectIndex, instanceRange, renderingMode);
// Draw direct interior parts.
if (renderingMode === 'conservative')
this.setTransformAffineUniforms(directInteriorProgram.uniforms, objectIndex);
else
this.setTransformUniform(directInteriorProgram.uniforms, pass, objectIndex);
this.setFramebufferSizeUniform(directInteriorProgram.uniforms);
this.setHintsUniform(directInteriorProgram.uniforms);
this.setPathColorsUniform(objectIndex, directInteriorProgram.uniforms, 0);
this.setEmboldenAmountUniform(objectIndex, directInteriorProgram.uniforms);
this.pathTransformBufferTextures[meshIndex].st.bind(gl, directInteriorProgram.uniforms, 1);
this.pathTransformBufferTextures[meshIndex]
.ext
.bind(gl, directInteriorProgram.uniforms, 2);
const bQuadInteriorRange = getMeshIndexRange(meshes.bQuadVertexInteriorIndexPathRanges,
pathRange);
if (!this.pathIDsAreInstanced) {
gl.drawElements(gl.TRIANGLES,
bQuadInteriorRange.length,
gl.UNSIGNED_INT,
bQuadInteriorRange.start * UINT32_SIZE);
} else {
renderContext.instancedArraysExt
.drawElementsInstancedANGLE(gl.TRIANGLES,
bQuadInteriorRange.length,
gl.UNSIGNED_INT,
0,
instanceRange.length);
}
gl.disable(gl.CULL_FACE);
// Render curves, if applicable.
if (renderingMode !== 'conservative') {
// Set up direct curve state.
gl.depthMask(false);
gl.enable(gl.BLEND);
gl.blendEquation(gl.FUNC_ADD);
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE);
// Set up the direct curve VAO.
//
// TODO(pcwalton): Cache these.
const directCurveProgramName = this.directCurveProgramName();
const directCurveProgram = renderContext.shaderPrograms[directCurveProgramName];
if (this.implicitCoverCurveVAO == null) {
this.implicitCoverCurveVAO = renderContext.vertexArrayObjectExt
.createVertexArrayOES();
}
renderContext.vertexArrayObjectExt.bindVertexArrayOES(this.implicitCoverCurveVAO);
this.initImplicitCoverCurveVAO(objectIndex, instanceRange);
// Draw direct curve parts.
this.setTransformUniform(directCurveProgram.uniforms, pass, objectIndex);
this.setFramebufferSizeUniform(directCurveProgram.uniforms);
this.setHintsUniform(directCurveProgram.uniforms);
this.setPathColorsUniform(objectIndex, directCurveProgram.uniforms, 0);
this.setEmboldenAmountUniform(objectIndex, directCurveProgram.uniforms);
this.pathTransformBufferTextures[meshIndex]
.st
.bind(gl, directCurveProgram.uniforms, 1);
this.pathTransformBufferTextures[meshIndex]
.ext
.bind(gl, directCurveProgram.uniforms, 2);
const coverCurveRange = getMeshIndexRange(meshes.bQuadVertexPositionPathRanges,
pathRange);
if (!this.pathIDsAreInstanced) {
gl.drawArrays(gl.TRIANGLES, coverCurveRange.start * 6, coverCurveRange.length * 6);
} else {
renderContext.instancedArraysExt
.drawArraysInstancedANGLE(gl.TRIANGLES,
0,
coverCurveRange.length * 6,
instanceRange.length);
}
}
renderContext.vertexArrayObjectExt.bindVertexArrayOES(null);
// Finish direct rendering. Right now, this performs compositing if necessary.
antialiasingStrategy.finishDirectlyRenderingObject(this, objectIndex);
}
private finishTiming(): void {
const renderContext = this.renderContext;
if (this.timerQueryPollInterval != null ||
renderContext.timerQueryExt == null ||
renderContext.atlasRenderingTimerQuery == null ||
renderContext.compositingTimerQuery == null) {
return;
}
renderContext.timerQueryExt.endQueryEXT(renderContext.timerQueryExt.TIME_ELAPSED_EXT);
this.timerQueryPollInterval = window.setInterval(() => {
if (renderContext.timerQueryExt == null ||
renderContext.atlasRenderingTimerQuery == null ||
renderContext.compositingTimerQuery == null) {
return;
}
for (const queryName of ['atlasRenderingTimerQuery', 'compositingTimerQuery'] as
Array<'atlasRenderingTimerQuery' | 'compositingTimerQuery'>) {
if (renderContext.timerQueryExt
.getQueryObjectEXT(renderContext[queryName] as WebGLQuery,
renderContext.timerQueryExt
.QUERY_RESULT_AVAILABLE_EXT) ===
0) {
return;
}
}
const atlasRenderingTime =
renderContext.timerQueryExt
.getQueryObjectEXT(renderContext.atlasRenderingTimerQuery,
renderContext.timerQueryExt.QUERY_RESULT_EXT);
const compositingTime =
renderContext.timerQueryExt
.getQueryObjectEXT(renderContext.compositingTimerQuery,
renderContext.timerQueryExt.QUERY_RESULT_EXT);
this.lastTimings = {
compositing: compositingTime / 1000000.0,
rendering: atlasRenderingTime / 1000000.0,
};
this.newTimingsReceived();
window.clearInterval(this.timerQueryPollInterval!);
this.timerQueryPollInterval = null;
}, TIME_INTERVAL_DELAY);
}
private initLUTTexture(imageName: 'gammaLUT' | 'areaLUT',
textureName: 'gammaLUTTexture' | 'areaLUTTexture'):
void {
const renderContext = this.renderContext;
const gl = renderContext.gl;
const image = renderContext[imageName];
const texture = unwrapNull(gl.createTexture());
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, gl.LUMINANCE, gl.UNSIGNED_BYTE, image);
const filter = imageName === 'gammaLUT' ? gl.NEAREST : gl.LINEAR;
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
this[textureName] = texture;
}
private initImplicitCoverCurveVAO(objectIndex: number, instanceRange: Range): void {
if (this.meshBuffers == null)
return;
const renderContext = this.renderContext;
const gl = renderContext.gl;
const meshIndex = this.meshIndexForObject(objectIndex);
const meshes = this.meshBuffers[meshIndex];
const meshData = unwrapNull(this.meshes)[meshIndex];
const directCurveProgramName = this.directCurveProgramName();
const directCurveProgram = renderContext.shaderPrograms[directCurveProgramName];
gl.useProgram(directCurveProgram.program);
gl.bindBuffer(gl.ARRAY_BUFFER, meshes.bQuadVertexPositions);
gl.vertexAttribPointer(directCurveProgram.attributes.aPosition, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexIDVBO);
gl.vertexAttribPointer(directCurveProgram.attributes.aVertexID, 1, gl.FLOAT, false, 0, 0);
if (this.pathIDsAreInstanced)
gl.bindBuffer(gl.ARRAY_BUFFER, this.instancedPathIDVBO);
else
gl.bindBuffer(gl.ARRAY_BUFFER, meshes.bQuadVertexPositionPathIDs);
gl.vertexAttribPointer(directCurveProgram.attributes.aPathID,
1,
gl.UNSIGNED_SHORT,
false,
0,
instanceRange.start * UINT16_SIZE);
if (this.pathIDsAreInstanced) {
renderContext.instancedArraysExt
.vertexAttribDivisorANGLE(directCurveProgram.attributes.aPathID, 1);
}
gl.enableVertexAttribArray(directCurveProgram.attributes.aPosition);
gl.enableVertexAttribArray(directCurveProgram.attributes.aVertexID);
gl.enableVertexAttribArray(directCurveProgram.attributes.aPathID);
}
private initImplicitCoverInteriorVAO(objectIndex: number,
instanceRange: Range,
renderingMode: DirectRenderingMode):
void {
if (this.meshBuffers == null)
return;
const renderContext = this.renderContext;
const gl = renderContext.gl;
const meshIndex = this.meshIndexForObject(objectIndex);
const meshes = this.meshBuffers[meshIndex];
const directInteriorProgramName = this.directInteriorProgramName(renderingMode);
const directInteriorProgram = renderContext.shaderPrograms[directInteriorProgramName];
gl.useProgram(directInteriorProgram.program);
gl.bindBuffer(gl.ARRAY_BUFFER, meshes.bQuadVertexPositions);
gl.vertexAttribPointer(directInteriorProgram.attributes.aPosition,
2,
gl.FLOAT,
false,
0,
0);
if (this.pathIDsAreInstanced)
gl.bindBuffer(gl.ARRAY_BUFFER, this.instancedPathIDVBO);
else
gl.bindBuffer(gl.ARRAY_BUFFER, meshes.bQuadVertexPositionPathIDs);
gl.vertexAttribPointer(directInteriorProgram.attributes.aPathID,
1,
gl.UNSIGNED_SHORT,
false,
0,
instanceRange.start * UINT16_SIZE);
if (this.pathIDsAreInstanced) {
renderContext.instancedArraysExt
.vertexAttribDivisorANGLE(directInteriorProgram.attributes.aPathID, 1);
}
if (directInteriorProgramName === 'conservativeInterior') {
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexIDVBO);
gl.vertexAttribPointer(directInteriorProgram.attributes.aVertexID,
1,
gl.FLOAT,
false,
0,
0);
}
gl.enableVertexAttribArray(directInteriorProgram.attributes.aPosition);
gl.enableVertexAttribArray(directInteriorProgram.attributes.aPathID);
if (directInteriorProgramName === 'conservativeInterior')
gl.enableVertexAttribArray(directInteriorProgram.attributes.aVertexID);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, meshes.bQuadVertexInteriorIndices);
}
private initInstancedPathIDVBO(): void {
const renderContext = this.renderContext;
const gl = renderContext.gl;
const pathIDs = new Uint16Array(MAX_PATHS);
for (let pathIndex = 0; pathIndex < MAX_PATHS; pathIndex++)
pathIDs[pathIndex] = pathIndex + 1;
this.instancedPathIDVBO = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.instancedPathIDVBO);
gl.bufferData(gl.ARRAY_BUFFER, pathIDs, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
}
private initVertexIDVBO(): void {
const renderContext = this.renderContext;
const gl = renderContext.gl;
const vertexIDs = new Float32Array(MAX_VERTICES);
for (let vertexID = 0; vertexID < MAX_VERTICES; vertexID++)
vertexIDs[vertexID] = vertexID;
this.vertexIDVBO = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexIDVBO);
gl.bufferData(gl.ARRAY_BUFFER, vertexIDs, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
}
private computeTransform(pass: number, objectIndex: number): glmatrix.mat4 {
let transform;
if (this.antialiasingStrategy == null)
transform = glmatrix.mat4.create();
else
transform = this.antialiasingStrategy.worldTransformForPass(this, pass);
glmatrix.mat4.mul(transform, transform, this.worldTransform);
glmatrix.mat4.mul(transform, transform, this.getModelviewTransform(objectIndex));
return transform;
}
}
function getMeshIndexRange(indexRanges: Range[], pathRange: Range): Range {
if (indexRanges.length === 0)
return new Range(0, 0);
const lastIndexRange = unwrapUndef(_.last(indexRanges));
const descending = indexRanges[0].start > lastIndexRange.start;
pathRange = new Range(pathRange.start - 1, pathRange.end - 1);
let startIndex;
if (pathRange.start >= indexRanges.length)
startIndex = lastIndexRange.end;
else if (!descending)
startIndex = indexRanges[pathRange.start].start;
else
startIndex = indexRanges[pathRange.start].end;
let endIndex;
if (descending)
endIndex = indexRanges[pathRange.end - 1].start;
else if (pathRange.end >= indexRanges.length)
endIndex = lastIndexRange.end;
else
endIndex = indexRanges[pathRange.end].start;
if (descending) {
const tmp = startIndex;
startIndex = endIndex;
endIndex = tmp;
}
return new Range(startIndex, endIndex);
}

View File

@ -1,189 +0,0 @@
// pathfinder/demo/client/src/shader-loader.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.
import {AttributeMap, UniformMap} from './gl-utils';
import {expectNotNull, PathfinderError, unwrapNull} from './utils';
export interface ShaderMap<T> {
blitLinear: T;
blitGamma: T;
conservativeInterior: T;
demo3DDistantGlyph: T;
demo3DMonument: T;
directCurve: T;
directInterior: T;
direct3DCurve: T;
direct3DInterior: T;
mcaa: T;
ssaaSubpixelResolve: T;
stencilAAA: T;
xcaaMonoResolve: T;
xcaaMonoSubpixelResolve: T;
}
export interface UnlinkedShaderProgram {
vertex: WebGLShader;
fragment: WebGLShader;
}
const COMMON_SHADER_URL: string = '/glsl/gles2/common.inc.glsl';
export const SHADER_NAMES: Array<keyof ShaderMap<void>> = [
'blitLinear',
'blitGamma',
'conservativeInterior',
'directCurve',
'directInterior',
'direct3DCurve',
'direct3DInterior',
'ssaaSubpixelResolve',
'mcaa',
'stencilAAA',
'xcaaMonoResolve',
'xcaaMonoSubpixelResolve',
'demo3DDistantGlyph',
'demo3DMonument',
];
const SHADER_URLS: ShaderMap<ShaderProgramURLs> = {
blitGamma: {
fragment: "/glsl/gles2/blit-gamma.fs.glsl",
vertex: "/glsl/gles2/blit.vs.glsl",
},
blitLinear: {
fragment: "/glsl/gles2/blit-linear.fs.glsl",
vertex: "/glsl/gles2/blit.vs.glsl",
},
conservativeInterior: {
fragment: "/glsl/gles2/direct-interior.fs.glsl",
vertex: "/glsl/gles2/conservative-interior.vs.glsl",
},
demo3DDistantGlyph: {
fragment: "/glsl/gles2/demo-3d-distant-glyph.fs.glsl",
vertex: "/glsl/gles2/demo-3d-distant-glyph.vs.glsl",
},
demo3DMonument: {
fragment: "/glsl/gles2/demo-3d-monument.fs.glsl",
vertex: "/glsl/gles2/demo-3d-monument.vs.glsl",
},
direct3DCurve: {
fragment: "/glsl/gles2/direct-curve.fs.glsl",
vertex: "/glsl/gles2/direct-3d-curve.vs.glsl",
},
direct3DInterior: {
fragment: "/glsl/gles2/direct-interior.fs.glsl",
vertex: "/glsl/gles2/direct-3d-interior.vs.glsl",
},
directCurve: {
fragment: "/glsl/gles2/direct-curve.fs.glsl",
vertex: "/glsl/gles2/direct-curve.vs.glsl",
},
directInterior: {
fragment: "/glsl/gles2/direct-interior.fs.glsl",
vertex: "/glsl/gles2/direct-interior.vs.glsl",
},
mcaa: {
fragment: "/glsl/gles2/mcaa.fs.glsl",
vertex: "/glsl/gles2/mcaa.vs.glsl",
},
ssaaSubpixelResolve: {
fragment: "/glsl/gles2/ssaa-subpixel-resolve.fs.glsl",
vertex: "/glsl/gles2/blit.vs.glsl",
},
stencilAAA: {
fragment: "/glsl/gles2/stencil-aaa.fs.glsl",
vertex: "/glsl/gles2/stencil-aaa.vs.glsl",
},
xcaaMonoResolve: {
fragment: "/glsl/gles2/xcaa-mono-resolve.fs.glsl",
vertex: "/glsl/gles2/xcaa-mono-resolve.vs.glsl",
},
xcaaMonoSubpixelResolve: {
fragment: "/glsl/gles2/xcaa-mono-subpixel-resolve.fs.glsl",
vertex: "/glsl/gles2/xcaa-mono-subpixel-resolve.vs.glsl",
},
};
export interface ShaderProgramSource {
vertex: string;
fragment: string;
}
interface ShaderProgramURLs {
vertex: string;
fragment: string;
}
export class ShaderLoader {
common!: Promise<string>;
shaders!: Promise<ShaderMap<ShaderProgramSource>>;
load(): void {
this.common = window.fetch(COMMON_SHADER_URL).then(response => response.text());
const shaderKeys = Object.keys(SHADER_URLS) as Array<keyof ShaderMap<string>>;
const promises = [];
for (const shaderKey of shaderKeys) {
promises.push(Promise.all([
window.fetch(SHADER_URLS[shaderKey].vertex).then(response => response.text()),
window.fetch(SHADER_URLS[shaderKey].fragment).then(response => response.text()),
]).then(results => ({ vertex: results[0], fragment: results[1] })));
}
this.shaders = Promise.all(promises).then(promises => {
const shaderMap: Partial<ShaderMap<ShaderProgramSource>> = {};
for (let keyIndex = 0; keyIndex < shaderKeys.length; keyIndex++)
shaderMap[shaderKeys[keyIndex]] = promises[keyIndex];
return shaderMap as ShaderMap<ShaderProgramSource>;
});
}
}
export class PathfinderShaderProgram {
readonly uniforms: UniformMap;
readonly attributes: AttributeMap;
readonly program: WebGLProgram;
readonly programName: string;
constructor(gl: WebGLRenderingContext,
programName: string,
unlinkedShaderProgram: UnlinkedShaderProgram) {
this.programName = programName;
this.program = expectNotNull(gl.createProgram(), "Failed to create shader program!");
for (const compiledShader of Object.values(unlinkedShaderProgram))
gl.attachShader(this.program, compiledShader);
gl.linkProgram(this.program);
if (gl.getProgramParameter(this.program, gl.LINK_STATUS) === 0) {
const infoLog = gl.getProgramInfoLog(this.program);
throw new PathfinderError(`Failed to link program "${programName}":\n${infoLog}`);
}
const uniformCount = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS);
const attributeCount = gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES);
const uniforms: UniformMap = {};
const attributes: AttributeMap = {};
for (let uniformIndex = 0; uniformIndex < uniformCount; uniformIndex++) {
const uniformName = unwrapNull(gl.getActiveUniform(this.program, uniformIndex)).name;
uniforms[uniformName] = expectNotNull(gl.getUniformLocation(this.program, uniformName),
`Didn't find uniform "${uniformName}"!`);
}
for (let attributeIndex = 0; attributeIndex < attributeCount; attributeIndex++) {
const attributeName = unwrapNull(gl.getActiveAttrib(this.program, attributeIndex)).name;
attributes[attributeName] = attributeIndex;
}
this.uniforms = uniforms;
this.attributes = attributes;
}
}

View File

@ -1,202 +0,0 @@
// pathfinder/demo/client/src/ssaa-strategy.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.
import * as glmatrix from 'gl-matrix';
import {AntialiasingStrategy, DirectRenderingMode, SubpixelAAType, TileInfo} from './aa-strategy';
import {createFramebuffer, createFramebufferColorTexture} from './gl-utils';
import {createFramebufferDepthTexture, setTextureParameters} from './gl-utils';
import {Renderer} from './renderer';
import {unwrapNull} from './utils';
import {DemoView} from './view';
export default class SSAAStrategy extends AntialiasingStrategy {
get passCount(): number {
switch (this.level) {
case 16:
return 4;
case 8:
return 2;
}
return 1;
}
private level: number;
private destFramebufferSize: glmatrix.vec2;
private supersampledFramebufferSize: glmatrix.vec2;
private supersampledColorTexture!: WebGLTexture;
private supersampledDepthTexture!: WebGLTexture;
private supersampledFramebuffer!: WebGLFramebuffer;
constructor(level: number, subpixelAA: SubpixelAAType) {
super(subpixelAA);
this.level = level;
this.subpixelAA = subpixelAA;
this.destFramebufferSize = glmatrix.vec2.create();
this.supersampledFramebufferSize = glmatrix.vec2.create();
}
attachMeshes(renderer: Renderer): void {}
setFramebufferSize(renderer: Renderer): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
this.destFramebufferSize = glmatrix.vec2.clone(renderer.destAllocatedSize);
this.supersampledFramebufferSize = glmatrix.vec2.create();
glmatrix.vec2.mul(this.supersampledFramebufferSize,
this.destFramebufferSize,
this.supersampleScale);
this.supersampledColorTexture =
createFramebufferColorTexture(gl,
this.supersampledFramebufferSize,
renderContext.colorAlphaFormat,
gl.LINEAR);
this.supersampledDepthTexture =
createFramebufferDepthTexture(gl, this.supersampledFramebufferSize);
this.supersampledFramebuffer = createFramebuffer(gl,
this.supersampledColorTexture,
this.supersampledDepthTexture);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
get transform(): glmatrix.mat4 {
const scale = glmatrix.vec2.create();
glmatrix.vec2.div(scale, this.supersampledFramebufferSize, this.destFramebufferSize);
const transform = glmatrix.mat4.create();
glmatrix.mat4.fromScaling(transform, [scale[0], scale[1], 1.0]);
return transform;
}
prepareForRendering(renderer: Renderer): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
const framebufferSize = this.supersampledFramebufferSize;
const usedSize = this.usedSupersampledFramebufferSize(renderer);
gl.bindFramebuffer(gl.FRAMEBUFFER, this.supersampledFramebuffer);
gl.viewport(0, 0, framebufferSize[0], framebufferSize[1]);
gl.scissor(0, 0, usedSize[0], usedSize[1]);
gl.enable(gl.SCISSOR_TEST);
const clearColor = renderer.backgroundColor;
gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
gl.clearDepth(0.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
prepareForDirectRendering(renderer: Renderer): void {}
prepareToRenderObject(renderer: Renderer, objectIndex: number): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
gl.bindFramebuffer(gl.FRAMEBUFFER, this.supersampledFramebuffer);
gl.viewport(0,
0,
this.supersampledFramebufferSize[0],
this.supersampledFramebufferSize[1]);
gl.disable(gl.SCISSOR_TEST);
}
finishDirectlyRenderingObject(renderer: Renderer, objectIndex: number): void {}
antialiasObject(renderer: Renderer): void {}
finishAntialiasingObject(renderer: Renderer, objectIndex: number): void {}
resolveAAForObject(renderer: Renderer): void {}
resolve(pass: number, renderer: Renderer): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
gl.bindFramebuffer(gl.FRAMEBUFFER, renderer.destFramebuffer);
renderer.setDrawViewport();
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.BLEND);
// Set up the blit program VAO.
let resolveProgram;
if (this.subpixelAA !== 'none')
resolveProgram = renderContext.shaderPrograms.ssaaSubpixelResolve;
else
resolveProgram = renderContext.shaderPrograms.blitLinear;
gl.useProgram(resolveProgram.program);
renderContext.initQuadVAO(resolveProgram.attributes);
// Resolve framebuffer.
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.supersampledColorTexture);
gl.uniform1i(resolveProgram.uniforms.uSource, 0);
gl.uniform2i(resolveProgram.uniforms.uSourceDimensions,
this.supersampledFramebufferSize[0],
this.supersampledFramebufferSize[1]);
const tileInfo = this.tileInfoForPass(pass);
renderer.setTransformAndTexScaleUniformsForDest(resolveProgram.uniforms, tileInfo);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, renderContext.quadElementsBuffer);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0);
}
worldTransformForPass(renderer: Renderer, pass: number): glmatrix.mat4 {
const tileInfo = this.tileInfoForPass(pass);
const usedSize = renderer.destUsedSize;
const transform = glmatrix.mat4.create();
glmatrix.mat4.fromTranslation(transform, [-1.0, -1.0, 0.0]);
glmatrix.mat4.scale(transform, transform, [tileInfo.size[0], tileInfo.size[1], 1.0]);
glmatrix.mat4.translate(transform, transform, [
-tileInfo.position[0] / tileInfo.size[0] * 2.0,
-tileInfo.position[1] / tileInfo.size[1] * 2.0,
0.0,
]);
glmatrix.mat4.translate(transform, transform, [1.0, 1.0, 0.0]);
return transform;
}
private tileInfoForPass(pass: number): TileInfo {
const tileSize = this.tileSize;
return {
position: glmatrix.vec2.clone([pass % tileSize[0], Math.floor(pass / tileSize[0])]),
size: tileSize,
};
}
get directRenderingMode(): DirectRenderingMode {
return 'color';
}
private get supersampleScale(): glmatrix.vec2 {
return glmatrix.vec2.clone([this.subpixelAA !== 'none' ? 3 : 2, this.level === 2 ? 1 : 2]);
}
private get tileSize(): glmatrix.vec2 {
switch (this.level) {
case 16:
return glmatrix.vec2.clone([2.0, 2.0]);
case 8:
return glmatrix.vec2.clone([2.0, 1.0]);
}
return glmatrix.vec2.clone([1.0, 1.0]);
}
private usedSupersampledFramebufferSize(renderer: Renderer): glmatrix.vec2 {
const result = glmatrix.vec2.create();
glmatrix.vec2.mul(result, renderer.destUsedSize, this.supersampleScale);
return result;
}
}

View File

@ -1,120 +0,0 @@
// pathfinder/demo/client/src/svg-demo.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.
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import {DemoAppController} from './app-controller';
import {OrthographicCamera} from "./camera";
import {PathfinderPackedMeshes} from "./meshes";
import {ShaderMap, ShaderProgramSource} from './shader-loader';
import {BUILTIN_SVG_URI, SVGLoader} from './svg-loader';
import {SVGRenderer} from './svg-renderer';
import {DemoView} from './view';
const parseColor = require('parse-color');
const SVG_NS: string = "http://www.w3.org/2000/svg";
const DEFAULT_FILE: string = 'tiger';
class SVGDemoController extends DemoAppController<SVGDemoView> {
loader!: SVGLoader;
protected readonly builtinFileURI: string = BUILTIN_SVG_URI;
private meshes!: PathfinderPackedMeshes;
start() {
super.start();
this.loader = new SVGLoader;
this.loadInitialFile(this.builtinFileURI);
}
protected fileLoaded(fileData: ArrayBuffer) {
this.loader.loadFile(fileData);
this.loader.partition().then(meshes => {
this.meshes = new PathfinderPackedMeshes(meshes);
this.meshesReceived();
});
}
protected createView(areaLUT: HTMLImageElement,
gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>):
SVGDemoView {
return new SVGDemoView(this, areaLUT, gammaLUT, commonShaderSource, shaderSources);
}
protected get defaultFile(): string {
return DEFAULT_FILE;
}
private meshesReceived(): void {
this.view.then(view => {
view.attachMeshes([this.meshes]);
view.initCameraBounds(this.loader.svgViewBox);
});
}
}
class SVGDemoView extends DemoView {
renderer: SVGDemoRenderer;
appController: SVGDemoController;
get camera(): OrthographicCamera {
return this.renderer.camera;
}
constructor(appController: SVGDemoController,
areaLUT: HTMLImageElement,
gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>) {
super(areaLUT, gammaLUT, commonShaderSource, shaderSources);
this.appController = appController;
this.renderer = new SVGDemoRenderer(this, {sizeToFit: true});
this.resizeToFit(true);
}
initCameraBounds(viewBox: glmatrix.vec4): void {
this.renderer.initCameraBounds(viewBox);
}
}
class SVGDemoRenderer extends SVGRenderer {
renderContext!: SVGDemoView;
needsStencil: boolean = false;
protected get loader(): SVGLoader {
return this.renderContext.appController.loader;
}
protected get canvas(): HTMLCanvasElement {
return this.renderContext.canvas;
}
protected newTimingsReceived(): void {
this.renderContext.appController.newTimingsReceived(this.lastTimings);
}
}
function main() {
const controller = new SVGDemoController;
window.addEventListener('load', () => controller.start(), false);
}
main();

View File

@ -1,254 +0,0 @@
// pathfinder/client/src/svg-loader.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.
import * as base64js from 'base64-js';
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import 'path-data-polyfill.js';
import {parseServerTiming, PathfinderMeshPack, PathfinderPackedMeshes} from "./meshes";
import {lerp, panic, Range, unwrapNull, unwrapUndef} from "./utils";
export const BUILTIN_SVG_URI: string = "/svg/demo";
const parseColor = require('parse-color');
const PARTITION_SVG_PATHS_ENDPOINT_URL: string = "/partition-svg-paths";
declare class SVGPathSegment {
type: string;
values: number[];
}
declare global {
interface SVGPathElement {
getPathData(settings: any): SVGPathSegment[];
}
}
type FillRule = 'evenodd' | 'winding';
export abstract class SVGPath {
element: SVGPathElement;
color: glmatrix.vec4;
constructor(element: SVGPathElement, colorProperty: keyof CSSStyleDeclaration) {
this.element = element;
const style = window.getComputedStyle(element);
this.color = unwrapNull(colorFromStyle(style[colorProperty]));
}
}
export class SVGFill extends SVGPath {
fillRule: FillRule;
get pathfinderFillRule(): string {
return { evenodd: 'EvenOdd', winding: 'Winding' }[this.fillRule];
}
constructor(element: SVGPathElement) {
super(element, 'fill');
const style = window.getComputedStyle(element);
this.fillRule = style.fillRule === 'evenodd' ? 'evenodd' : 'winding';
}
}
export class SVGStroke extends SVGPath {
width: number;
constructor(element: SVGPathElement) {
super(element, 'stroke');
const style = window.getComputedStyle(element);
const ctm = unwrapNull(element.getCTM());
const strokeWidthString = unwrapNull(style.strokeWidth);
const matches = /^(\d+\.?\d*)(.*)$/.exec(strokeWidthString);
let strokeWidth: number;
if (matches == null) {
strokeWidth = 0.0;
} else {
strokeWidth = unwrapNull(parseFloat(matches[1]));
if (matches[2] === 'px')
strokeWidth *= lerp(ctm.a, ctm.d, 0.5);
}
this.width = strokeWidth;
}
}
interface ClipPathIDTable {
[id: string]: number;
}
export class SVGLoader {
pathInstances: SVGPath[];
scale: number;
pathBounds: glmatrix.vec4[];
svgBounds: glmatrix.vec4;
svgViewBox: glmatrix.vec4;
isMonochrome: boolean;
private svg: SVGSVGElement;
private fileData!: ArrayBuffer;
private paths: any[];
private clipPathIDs: ClipPathIDTable;
constructor() {
this.scale = 1.0;
this.pathInstances = [];
this.pathBounds = [];
this.paths = [];
this.clipPathIDs = {};
this.svgBounds = glmatrix.vec4.create();
this.svgViewBox = glmatrix.vec4.create();
this.svg = unwrapNull(document.getElementById('pf-svg')) as Element as SVGSVGElement;
this.isMonochrome = false;
}
loadFile(fileData: ArrayBuffer) {
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;
this.attachSVG(svgElement);
}
partition(pathIndex?: number | undefined): Promise<PathfinderMeshPack> {
// Make the request.
const paths = pathIndex == null ? this.paths : [this.paths[pathIndex]];
let time = 0;
return window.fetch(PARTITION_SVG_PATHS_ENDPOINT_URL, {
body: JSON.stringify({
paths: paths,
viewBoxHeight: this.svgViewBox[3] - this.svgViewBox[1],
viewBoxWidth: this.svgViewBox[2] - this.svgViewBox[0],
}),
headers: {'Content-Type': 'application/json'} as any,
method: 'POST',
}).then(response => {
time = parseServerTiming(response.headers);
return response.arrayBuffer();
}).then(buffer => new PathfinderMeshPack(buffer));
}
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);
const viewBox = svgElement.viewBox.baseVal;
this.svg.setAttribute('width', "" + viewBox.width);
this.svg.setAttribute('height', "" + viewBox.height);
// Scan for geometry elements.
this.pathInstances.length = 0;
this.clipPathIDs = {};
this.scanElement(this.svg);
this.paths = [];
for (const instance of this.pathInstances) {
const element = instance.element;
const svgCTM = unwrapNull(element.getCTM());
const ctm = glmatrix.mat2d.fromValues(svgCTM.a, -svgCTM.b,
svgCTM.c, -svgCTM.d,
svgCTM.e, viewBox.height - svgCTM.f);
glmatrix.mat2d.scale(ctm, ctm, [this.scale, this.scale]);
const bottomLeft = glmatrix.vec2.create();
const topRight = glmatrix.vec2.create();
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);
glmatrix.vec2.min(bottomLeft, bottomLeft, point);
glmatrix.vec2.max(topRight, topRight, point);
return [point[0], point[1]];
});
return {
type: segment.type,
values: newValues,
};
});
const pathBounds = glmatrix.vec4.clone([
bottomLeft[0], bottomLeft[1],
topRight[0], topRight[1],
]);
if (instance instanceof SVGFill) {
this.paths.push({
kind: { Fill: instance.pathfinderFillRule },
segments: segments,
});
this.pathBounds.push(pathBounds);
} else if (instance instanceof SVGStroke) {
this.paths.push({ kind: { Stroke: instance.width }, segments: segments });
this.pathBounds.push(pathBounds);
}
}
this.isMonochrome = this.pathInstances.every(pathInstance => {
return glmatrix.vec4.equals(pathInstance.color, this.pathInstances[0].color);
});
this.svgViewBox = glmatrix.vec4.clone([
viewBox.x, viewBox.y, viewBox.x + viewBox.width, viewBox.y + viewBox.height,
]);
}
private scanElement(element: Element): void {
const style = window.getComputedStyle(element);
if (element instanceof SVGPathElement) {
if (colorFromStyle(style.fill) != null)
this.addPathInstance(new SVGFill(element));
if (colorFromStyle(style.stroke) != null)
this.addPathInstance(new SVGStroke(element));
}
for (const kid of element.childNodes) {
if (kid instanceof Element)
this.scanElement(kid);
}
}
private addPathInstance(pathInstance: SVGPath): void {
this.pathInstances.push(pathInstance);
}
}
function colorFromStyle(style: string | null): glmatrix.vec4 | null {
if (style == null || style === 'none')
return null;
// TODO(pcwalton): Gradients?
const color = parseColor(style);
if (color.rgba == null)
return glmatrix.vec4.clone([0.0, 0.0, 0.0, 1.0]);
if (color.rgba[3] === 0.0)
return null;
return glmatrix.vec4.clone(color.rgba);
}

View File

@ -1,217 +0,0 @@
// pathfinder/demo/client/src/svg-renderer.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.
import * as glmatrix from 'gl-matrix';
import {AntialiasingStrategy, AntialiasingStrategyName, DirectRenderingMode} from './aa-strategy';
import {NoAAStrategy} from './aa-strategy';
import {SubpixelAAType} from './aa-strategy';
import {OrthographicCamera} from "./camera";
import {UniformMap} from './gl-utils';
import {PathfinderPackedMeshes} from './meshes';
import {PathTransformBuffers, Renderer} from "./renderer";
import {ShaderMap} from './shader-loader';
import SSAAStrategy from './ssaa-strategy';
import {SVGLoader} from './svg-loader';
import {Range} from './utils';
import {RenderContext} from './view';
import {MCAAStrategy, XCAAStrategy} from './xcaa-strategy';
interface AntialiasingStrategyTable {
none: typeof NoAAStrategy;
ssaa: typeof SSAAStrategy;
xcaa: typeof MCAAStrategy;
}
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
none: NoAAStrategy,
ssaa: SSAAStrategy,
xcaa: MCAAStrategy,
};
export interface SVGRendererOptions {
sizeToFit?: boolean;
fixed?: boolean;
}
export abstract class SVGRenderer extends Renderer {
renderContext!: RenderContext;
camera: OrthographicCamera;
needsStencil: boolean = false;
private options: SVGRendererOptions;
get isMulticolor(): boolean {
return !this.loader.isMonochrome;
}
get bgColor(): glmatrix.vec4 {
return glmatrix.vec4.clone([1.0, 1.0, 1.0, 1.0]);
}
get fgColor(): glmatrix.vec4 {
return glmatrix.vec4.clone([0.0, 0.0, 0.0, 1.0]);
}
get destAllocatedSize(): glmatrix.vec2 {
const canvas = this.canvas;
return glmatrix.vec2.clone([canvas.width, canvas.height]);
}
get destFramebuffer(): WebGLFramebuffer | null {
return null;
}
get destUsedSize(): glmatrix.vec2 {
return this.destAllocatedSize;
}
get backgroundColor(): glmatrix.vec4 {
return glmatrix.vec4.clone([1.0, 1.0, 1.0, 1.0]);
}
get allowSubpixelAA(): boolean {
return false;
}
protected get objectCount(): number {
return 1;
}
protected abstract get loader(): SVGLoader;
protected abstract get canvas(): HTMLCanvasElement;
constructor(renderContext: RenderContext, options: SVGRendererOptions) {
super(renderContext);
this.options = options;
// FIXME(pcwalton): Get the canvas a better way?
this.camera = new OrthographicCamera((this as any).canvas, {
fixed: !!this.options.fixed,
scaleBounds: true,
});
this.camera.onPan = () => this.renderContext.setDirty();
this.camera.onZoom = () => this.renderContext.setDirty();
this.camera.onRotate = () => this.renderContext.setDirty();
}
setHintsUniform(uniforms: UniformMap): void {
this.renderContext.gl.uniform4f(uniforms.uHints, 0, 0, 0, 0);
}
pathBoundingRects(objectIndex: number): Float32Array {
const loader = this.loader;
const boundingRectsBuffer = new Float32Array((loader.pathBounds.length + 1) * 4);
for (let pathIndex = 0; pathIndex < loader.pathBounds.length; pathIndex++)
boundingRectsBuffer.set(loader.pathBounds[pathIndex], (pathIndex + 1) * 4);
return boundingRectsBuffer;
}
attachMeshes(meshes: PathfinderPackedMeshes[]): void {
super.attachMeshes(meshes);
this.uploadPathColors(1);
this.uploadPathTransforms(1);
}
initCameraBounds(svgViewBox: glmatrix.vec4): void {
// The SVG origin is in the upper left, but the camera origin is in the lower left.
this.camera.bounds = svgViewBox;
if (this.options.sizeToFit)
this.camera.zoomToFit();
}
meshIndexForObject(objectIndex: number): number {
return 0;
}
pathRangeForObject(objectIndex: number): Range {
return new Range(1, this.loader.pathInstances.length + 1);
}
pathTransformsForObject(objectIndex: number): PathTransformBuffers<Float32Array> {
const instances = this.loader.pathInstances;
const pathTransforms = this.createPathTransformBuffers(instances.length);
for (let pathIndex = 0; pathIndex < instances.length; pathIndex++) {
// TODO(pcwalton): Set transform.
const startOffset = (pathIndex + 1) * 4;
pathTransforms.st.set([1, 1, 0, 0], startOffset);
}
return pathTransforms;
}
protected get usedSizeFactor(): glmatrix.vec2 {
return glmatrix.vec2.clone([1.0, 1.0]);
}
protected get worldTransform(): glmatrix.mat4 {
const canvas = this.canvas;
const transform = glmatrix.mat4.create();
glmatrix.mat4.translate(transform, transform, [-1.0, -1.0, 0.0]);
glmatrix.mat4.scale(transform, transform, [2.0 / canvas.width, 2.0 / canvas.height, 1.0]);
const centerPoint = glmatrix.vec3.clone([canvas.width * 0.5, canvas.height * 0.5, 0.0]);
glmatrix.mat4.translate(transform, transform, centerPoint);
glmatrix.mat4.rotateZ(transform, transform, this.camera.rotationAngle);
glmatrix.vec3.negate(centerPoint, centerPoint);
glmatrix.mat4.translate(transform, transform, centerPoint);
const translation = this.camera.translation;
glmatrix.mat4.translate(transform, transform, [translation[0], translation[1], 0]);
glmatrix.mat4.scale(transform, transform, [this.camera.scale, this.camera.scale, 1.0]);
return transform;
}
protected clearColorForObject(objectIndex: number): glmatrix.vec4 | null {
return glmatrix.vec4.create();
}
protected directCurveProgramName(): keyof ShaderMap<void> {
return 'directCurve';
}
protected directInteriorProgramName(renderingMode: DirectRenderingMode):
keyof ShaderMap<void> {
return renderingMode === 'conservative' ? 'conservativeInterior' : 'directInterior';
}
protected pathColorsForObject(objectIndex: number): Uint8Array {
const instances = this.loader.pathInstances;
const pathColors = new Uint8Array(4 * (instances.length + 1));
for (let pathIndex = 0; pathIndex < instances.length; pathIndex++) {
const startOffset = (pathIndex + 1) * 4;
// Set color.
const color: ArrayLike<number> = instances[pathIndex].color;
pathColors.set(instances[pathIndex].color, startOffset);
pathColors[startOffset + 3] = color[3] * 255;
}
return pathColors;
}
protected createAAStrategy(aaType: AntialiasingStrategyName,
aaLevel: number,
subpixelAA: SubpixelAAType):
AntialiasingStrategy {
return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA);
}
protected compositeIfNecessary(): void {}
}

View File

@ -1,470 +0,0 @@
// pathfinder/client/src/text-demo.ts
//
// Copyright © 2018 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.
import * as base64js from 'base64-js';
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import * as opentype from 'opentype.js';
import {Metrics} from 'opentype.js';
import {StemDarkeningMode, SubpixelAAType} from './aa-strategy';
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy";
import {AAOptions, DemoAppController, setSwitchInputsValue, SwitchInputs} from './app-controller';
import {Atlas, ATLAS_SIZE, AtlasGlyph, GlyphKey, SUBPIXEL_GRANULARITY} from './atlas';
import PathfinderBufferTexture from './buffer-texture';
import {CameraView, OrthographicCamera} from "./camera";
import {createFramebuffer, createFramebufferColorTexture} from './gl-utils';
import {createFramebufferDepthTexture, QUAD_ELEMENTS, setTextureParameters} from './gl-utils';
import {UniformMap} from './gl-utils';
import {PathfinderMeshPack, PathfinderPackedMeshBuffers, PathfinderPackedMeshes} from './meshes';
import {PathfinderShaderProgram, ShaderMap, ShaderProgramSource} from './shader-loader';
import SSAAStrategy from './ssaa-strategy';
import {calculatePixelRectForGlyph, PathfinderFont} from "./text";
import {BUILTIN_FONT_URI, calculatePixelXMin, computeStemDarkeningAmount} from "./text";
import {GlyphStore, Hint, SimpleTextLayout, UnitMetrics} from "./text";
import {SimpleTextLayoutRenderContext, SimpleTextLayoutRenderer} from './text-renderer';
import {TextRenderContext, TextRenderer} from './text-renderer';
import {assert, expectNotNull, panic, PathfinderError, scaleRect, UINT32_SIZE} from './utils';
import {unwrapNull} from './utils';
import {DemoView, RenderContext, Timings, TIMINGS} from './view';
const DEFAULT_TEXT: string =
`Twas brillig, and the slithy toves
Did gyre and gimble in the wabe;
All mimsy were the borogoves,
And the mome raths outgrabe.
Beware the Jabberwock, my son!
The jaws that bite, the claws that catch!
Beware the Jubjub bird, and shun
The frumious Bandersnatch!
He took his vorpal sword in hand:
Long time the manxome foe he sought
So rested he by the Tumtum tree,
And stood awhile in thought.
And as in uffish thought he stood,
The Jabberwock, with eyes of flame,
Came whiffling through the tulgey wood,
And burbled as it came!
One, two! One, two! And through and through
The vorpal blade went snicker-snack!
He left it dead, and with its head
He went galumphing back.
And hast thou slain the Jabberwock?
Come to my arms, my beamish boy!
O frabjous day! Callooh! Callay!
He chortled in his joy.
Twas brillig, and the slithy toves
Did gyre and gimble in the wabe;
All mimsy were the borogoves,
And the mome raths outgrabe.`;
const INITIAL_FONT_SIZE: number = 72.0;
const DEFAULT_FONT: string = 'open-sans';
const B_POSITION_SIZE: number = 8;
const B_PATH_INDEX_SIZE: number = 2;
declare global {
interface Window {
jQuery(element: HTMLElement): JQuerySubset;
}
}
interface TabShownEvent {
target: EventTarget;
relatedTarget: EventTarget;
}
interface JQuerySubset {
modal(options?: any): void;
on(name: 'shown.bs.tab', handler: (event: TabShownEvent) => void): void;
}
type Matrix4D = Float32Array;
type Rect = glmatrix.vec4;
interface Point2D {
x: number;
y: number;
}
type Size2D = glmatrix.vec2;
type ShaderType = number;
// `opentype.js` monkey patches
declare module 'opentype.js' {
interface Font {
isSupported(): boolean;
lineHeight(): number;
}
interface Glyph {
getIndex(): number;
}
}
class TextDemoController extends DemoAppController<TextDemoView> {
font!: PathfinderFont;
layout!: SimpleTextLayout;
glyphStore!: GlyphStore;
atlasGlyphs!: AtlasGlyph[];
private hintingSelect!: HTMLSelectElement;
private emboldenInput!: HTMLInputElement;
private editTextModal!: HTMLElement;
private editTextArea!: HTMLTextAreaElement;
private _atlas: Atlas;
private meshes!: PathfinderPackedMeshes;
private _fontSize!: number;
private _rotationAngle!: number;
private _emboldenAmount!: number;
private text: string;
constructor() {
super();
this.text = DEFAULT_TEXT;
this._atlas = new Atlas;
}
start(): void {
this._fontSize = INITIAL_FONT_SIZE;
this._rotationAngle = 0.0;
this._emboldenAmount = 0.0;
this.hintingSelect = unwrapNull(document.getElementById('pf-hinting-select')) as
HTMLSelectElement;
this.hintingSelect.addEventListener('change', () => this.hintingChanged(), false);
this.emboldenInput = unwrapNull(document.getElementById('pf-embolden')) as
HTMLInputElement;
this.emboldenInput.addEventListener('input', () => this.emboldenAmountChanged(), false);
this.editTextModal = unwrapNull(document.getElementById('pf-edit-text-modal'));
this.editTextArea = unwrapNull(document.getElementById('pf-edit-text-area')) as
HTMLTextAreaElement;
const editTextOkButton = unwrapNull(document.getElementById('pf-edit-text-ok-button'));
editTextOkButton.addEventListener('click', () => this.updateText(), false);
super.start();
this.loadInitialFile(this.builtinFileURI);
}
showTextEditor(): void {
this.editTextArea.value = this.text;
window.jQuery(this.editTextModal).modal();
}
get emboldenAmount(): number {
return this._emboldenAmount;
}
protected updateUIForAALevelChange(aaType: AntialiasingStrategyName, aaLevel: number): void {
const gammaCorrectionSwitchInputs = unwrapNull(this.gammaCorrectionSwitchInputs);
const stemDarkeningSwitchInputs = unwrapNull(this.stemDarkeningSwitchInputs);
const emboldenInput = unwrapNull(this.emboldenInput);
const emboldenLabel = getLabelFor(emboldenInput);
switch (aaType) {
case 'none':
case 'ssaa':
enableSwitchInputs(gammaCorrectionSwitchInputs, false);
enableSwitchInputs(stemDarkeningSwitchInputs, false);
emboldenInput.value = "0";
emboldenInput.disabled = true;
enableLabel(emboldenLabel, false);
break;
case 'xcaa':
enableSwitchInputs(gammaCorrectionSwitchInputs, true);
enableSwitchInputs(stemDarkeningSwitchInputs, true);
emboldenInput.value = "0";
emboldenInput.disabled = false;
enableLabel(emboldenLabel, true);
break;
}
function enableSwitchInputs(switchInputs: SwitchInputs, enabled: boolean): void {
switchInputs.off.disabled = switchInputs.on.disabled = !enabled;
enableLabel(getLabelFor(switchInputs.on), enabled);
}
function enableLabel(label: HTMLLabelElement | null, enabled: boolean): void {
if (label == null)
return;
if (enabled)
label.classList.remove('pf-disabled');
else
label.classList.add('pf-disabled');
}
function getLabelFor(element: HTMLElement): HTMLLabelElement | null {
return document.querySelector(`label[for="${element.id}"]`) as HTMLLabelElement | null;
}
}
protected createView(areaLUT: HTMLImageElement,
gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>):
TextDemoView {
return new TextDemoView(this, areaLUT, gammaLUT, commonShaderSource, shaderSources);
}
protected fileLoaded(fileData: ArrayBuffer, builtinName: string | null) {
const font = new PathfinderFont(fileData, builtinName);
this.recreateLayout(font);
}
private hintingChanged(): void {
this.view.then(view => view.renderer.updateHinting());
}
private emboldenAmountChanged(): void {
this._emboldenAmount = parseFloat(this.emboldenInput.value);
this.view.then(view => view.renderer.updateEmboldenAmount());
}
private updateText(): void {
this.text = this.editTextArea.value;
window.jQuery(this.editTextModal).modal('hide');
this.recreateLayout(this.font);
}
private recreateLayout(font: PathfinderFont) {
const newLayout = new SimpleTextLayout(font, this.text);
let uniqueGlyphIDs = newLayout.textFrame.allGlyphIDs;
uniqueGlyphIDs.sort((a, b) => a - b);
uniqueGlyphIDs = _.sortedUniq(uniqueGlyphIDs);
const glyphStore = new GlyphStore(font, uniqueGlyphIDs);
glyphStore.partition().then(result => {
const meshes = this.expandMeshes(result.meshes, uniqueGlyphIDs.length);
this.view.then(view => {
this.font = font;
this.layout = newLayout;
this.glyphStore = glyphStore;
this.meshes = meshes;
view.attachText();
view.attachMeshes([this.meshes]);
});
});
}
private expandMeshes(meshes: PathfinderMeshPack, glyphCount: number): PathfinderPackedMeshes {
const pathIDs = [];
for (let glyphIndex = 0; glyphIndex < glyphCount; glyphIndex++) {
for (let subpixel = 0; subpixel < SUBPIXEL_GRANULARITY; subpixel++)
pathIDs.push(glyphIndex + 1);
}
return new PathfinderPackedMeshes(meshes, pathIDs);
}
get atlas(): Atlas {
return this._atlas;
}
/// The font size in pixels per em.
get fontSize(): number {
return this._fontSize;
}
/// The font size in pixels per em.
set fontSize(newFontSize: number) {
this._fontSize = newFontSize;
this.view.then(view => view.renderer.relayoutText());
}
get rotationAngle(): number {
return this._rotationAngle;
}
set rotationAngle(newRotationAngle: number) {
this._rotationAngle = newRotationAngle;
this.view.then(view => view.renderer.relayoutText());
}
get unitsPerEm(): number {
return this.font.opentypeFont.unitsPerEm;
}
get pixelsPerUnit(): number {
return this._fontSize / this.unitsPerEm;
}
get useHinting(): boolean {
return this.hintingSelect.selectedIndex !== 0;
}
get pathCount(): number {
return this.glyphStore.glyphIDs.length * SUBPIXEL_GRANULARITY;
}
protected get builtinFileURI(): string {
return BUILTIN_FONT_URI;
}
protected get defaultFile(): string {
return DEFAULT_FONT;
}
}
class TextDemoView extends DemoView implements SimpleTextLayoutRenderContext {
renderer: TextDemoRenderer;
appController: TextDemoController;
get cameraView(): CameraView {
return this.canvas;
}
get atlasGlyphs(): AtlasGlyph[] {
return this.appController.atlasGlyphs;
}
set atlasGlyphs(newAtlasGlyphs: AtlasGlyph[]) {
this.appController.atlasGlyphs = newAtlasGlyphs;
}
get atlas(): Atlas {
return this.appController.atlas;
}
get glyphStore(): GlyphStore {
return this.appController.glyphStore;
}
get font(): PathfinderFont {
return this.appController.font;
}
get fontSize(): number {
return this.appController.fontSize;
}
get pathCount(): number {
return this.appController.pathCount;
}
get pixelsPerUnit(): number {
return this.appController.pixelsPerUnit;
}
get useHinting(): boolean {
return this.appController.useHinting;
}
get layout(): SimpleTextLayout {
return this.appController.layout;
}
get rotationAngle(): number {
return this.appController.rotationAngle;
}
get emboldenAmount(): number {
return this.appController.emboldenAmount;
}
get unitsPerEm(): number {
return this.appController.unitsPerEm;
}
protected get camera(): OrthographicCamera {
return this.renderer.camera;
}
constructor(appController: TextDemoController,
areaLUT: HTMLImageElement,
gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>) {
super(areaLUT, gammaLUT, commonShaderSource, shaderSources);
this.appController = appController;
this.renderer = new TextDemoRenderer(this);
this.canvas.addEventListener('dblclick', () => this.appController.showTextEditor(), false);
this.resizeToFit(true);
}
attachText() {
this.panZoomEventsEnabled = false;
this.renderer.prepareToAttachText();
this.renderer.camera.zoomToFit();
this.appController.fontSize = this.renderer.camera.scale *
this.appController.font.opentypeFont.unitsPerEm;
this.renderer.finishAttachingText();
this.panZoomEventsEnabled = true;
}
newTimingsReceived(newTimings: Timings) {
this.appController.newTimingsReceived(newTimings);
}
protected onPan(): void {
this.renderer.viewPanned();
}
protected onZoom(): void {
this.appController.fontSize = this.renderer.camera.scale *
this.appController.font.opentypeFont.unitsPerEm;
}
protected onRotate(): void {
this.appController.rotationAngle = this.renderer.camera.rotationAngle;
}
private set panZoomEventsEnabled(flag: boolean) {
if (flag) {
this.renderer.camera.onPan = () => this.onPan();
this.renderer.camera.onZoom = () => this.onZoom();
this.renderer.camera.onRotate = () => this.onRotate();
} else {
this.renderer.camera.onPan = null;
this.renderer.camera.onZoom = null;
this.renderer.camera.onRotate = null;
}
}
}
class TextDemoRenderer extends SimpleTextLayoutRenderer {
renderContext!: TextDemoView;
}
function main(): void {
const controller = new TextDemoController;
window.addEventListener('load', () => controller.start(), false);
}
main();

View File

@ -1,649 +0,0 @@
// pathfinder/client/src/text-renderer.ts
//
// Copyright © 2018 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.
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from './aa-strategy';
import {StemDarkeningMode, SubpixelAAType} from './aa-strategy';
import {AAOptions} from './app-controller';
import {Atlas, ATLAS_SIZE, AtlasGlyph, GlyphKey, SUBPIXEL_GRANULARITY} from './atlas';
import {CameraView, OrthographicCamera} from './camera';
import {createFramebuffer, createFramebufferDepthTexture, QUAD_ELEMENTS} from './gl-utils';
import {UniformMap} from './gl-utils';
import {PathTransformBuffers, Renderer} from './renderer';
import {ShaderMap} from './shader-loader';
import SSAAStrategy from './ssaa-strategy';
import {calculatePixelRectForGlyph, computeStemDarkeningAmount, GlyphStore, Hint} from "./text";
import {MAX_STEM_DARKENING_PIXELS_PER_EM, PathfinderFont, SimpleTextLayout} from "./text";
import {UnitMetrics} from "./text";
import {unwrapNull} from './utils';
import {RenderContext, Timings} from "./view";
import {StencilAAAStrategy} from './xcaa-strategy';
interface AntialiasingStrategyTable {
none: typeof NoAAStrategy;
ssaa: typeof SSAAStrategy;
xcaa: typeof StencilAAAStrategy;
}
const SQRT_1_2: number = 1.0 / Math.sqrt(2.0);
const MIN_SCALE: number = 0.0025;
const MAX_SCALE: number = 0.5;
export const MAX_SUBPIXEL_AA_FONT_SIZE: number = 48.0;
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
none: NoAAStrategy,
ssaa: SSAAStrategy,
xcaa: StencilAAAStrategy,
};
export interface TextRenderContext extends RenderContext {
atlasGlyphs: AtlasGlyph[];
readonly cameraView: CameraView;
readonly atlas: Atlas;
readonly glyphStore: GlyphStore;
readonly font: PathfinderFont;
readonly fontSize: number;
readonly useHinting: boolean;
newTimingsReceived(timings: Timings): void;
}
export abstract class TextRenderer extends Renderer {
renderContext!: TextRenderContext;
camera: OrthographicCamera;
atlasFramebuffer!: WebGLFramebuffer;
atlasDepthTexture!: WebGLTexture;
get isMulticolor(): boolean {
return false;
}
get needsStencil(): boolean {
return this.renderContext.fontSize <= MAX_STEM_DARKENING_PIXELS_PER_EM;
}
get destFramebuffer(): WebGLFramebuffer {
return this.atlasFramebuffer;
}
get destAllocatedSize(): glmatrix.vec2 {
return ATLAS_SIZE;
}
get destUsedSize(): glmatrix.vec2 {
return this.renderContext.atlas.usedSize;
}
get emboldenAmount(): glmatrix.vec2 {
const emboldenAmount = glmatrix.vec2.create();
glmatrix.vec2.add(emboldenAmount, this.extraEmboldenAmount, this.stemDarkeningAmount);
return emboldenAmount;
}
get bgColor(): glmatrix.vec4 {
return glmatrix.vec4.clone([0.0, 0.0, 0.0, 1.0]);
}
get fgColor(): glmatrix.vec4 {
return glmatrix.vec4.clone([1.0, 1.0, 1.0, 1.0]);
}
get rotationAngle(): number {
return 0.0;
}
get allowSubpixelAA(): boolean {
return this.renderContext.fontSize <= MAX_SUBPIXEL_AA_FONT_SIZE;
}
protected get pixelsPerUnit(): number {
return this.renderContext.fontSize / this.renderContext.font.opentypeFont.unitsPerEm;
}
protected get worldTransform(): glmatrix.mat4 {
const transform = glmatrix.mat4.create();
glmatrix.mat4.translate(transform, transform, [-1.0, -1.0, 0.0]);
glmatrix.mat4.scale(transform, transform, [2.0 / ATLAS_SIZE[0], 2.0 / ATLAS_SIZE[1], 1.0]);
return transform;
}
protected get stemDarkeningAmount(): glmatrix.vec2 {
if (this.stemDarkening === 'dark')
return computeStemDarkeningAmount(this.renderContext.fontSize, this.pixelsPerUnit);
return glmatrix.vec2.create();
}
protected get usedSizeFactor(): glmatrix.vec2 {
const usedSize = glmatrix.vec2.create();
glmatrix.vec2.div(usedSize, this.renderContext.atlas.usedSize, ATLAS_SIZE);
return usedSize;
}
private get pathCount(): number {
return this.renderContext.glyphStore.glyphIDs.length * SUBPIXEL_GRANULARITY;
}
protected get objectCount(): number {
return this.meshBuffers == null ? 0 : this.meshBuffers.length;
}
protected get extraEmboldenAmount(): glmatrix.vec2 {
return glmatrix.vec2.create();
}
private stemDarkening!: StemDarkeningMode;
private subpixelAA!: SubpixelAAType;
constructor(renderContext: TextRenderContext) {
super(renderContext);
this.camera = new OrthographicCamera(this.renderContext.cameraView, {
maxScale: MAX_SCALE,
minScale: MIN_SCALE,
});
}
setHintsUniform(uniforms: UniformMap): void {
const renderContext = this.renderContext;
const gl = renderContext.gl;
const hint = this.createHint();
gl.uniform4f(uniforms.uHints,
hint.xHeight,
hint.hintedXHeight,
hint.stemHeight,
hint.hintedStemHeight);
}
pathBoundingRects(objectIndex: number): Float32Array {
const pathCount = this.pathCount;
const atlasGlyphs = this.renderContext.atlasGlyphs;
const pixelsPerUnit = this.pixelsPerUnit;
const rotationAngle = this.rotationAngle;
const font = this.renderContext.font;
const hint = this.createHint();
const boundingRects = new Float32Array((pathCount + 1) * 4);
for (const glyph of atlasGlyphs) {
const atlasGlyphMetrics = font.metricsForGlyph(glyph.glyphKey.id);
if (atlasGlyphMetrics == null)
continue;
const atlasUnitMetrics = new UnitMetrics(atlasGlyphMetrics, 0.0, this.emboldenAmount);
const pathID = glyph.pathID;
boundingRects[pathID * 4 + 0] = atlasUnitMetrics.left;
boundingRects[pathID * 4 + 1] = atlasUnitMetrics.descent;
boundingRects[pathID * 4 + 2] = atlasUnitMetrics.right;
boundingRects[pathID * 4 + 3] = atlasUnitMetrics.ascent;
}
return boundingRects;
}
pathTransformsForObject(objectIndex: number): PathTransformBuffers<Float32Array> {
const pathCount = this.pathCount;
const atlasGlyphs = this.renderContext.atlasGlyphs;
const pixelsPerUnit = this.pixelsPerUnit;
const rotationAngle = this.rotationAngle;
// FIXME(pcwalton): This is a hack that tries to preserve the vertical extents of the glyph
// after stem darkening. It's better than nothing, but we should really do better.
//
// This hack seems to produce *better* results than what macOS does on sans-serif fonts;
// the ascenders and x-heights of the glyphs are pixel snapped, while they aren't on macOS.
// But we should really figure out what macOS does…
const ascender = this.renderContext.font.opentypeFont.ascender;
const emboldenAmount = this.emboldenAmount;
const stemDarkeningYScale = (ascender + emboldenAmount[1]) / ascender;
const stemDarkeningOffset = glmatrix.vec2.clone(emboldenAmount);
glmatrix.vec2.scale(stemDarkeningOffset, stemDarkeningOffset, pixelsPerUnit);
glmatrix.vec2.scale(stemDarkeningOffset, stemDarkeningOffset, SQRT_1_2);
glmatrix.vec2.mul(stemDarkeningOffset, stemDarkeningOffset, [1, stemDarkeningYScale]);
const transform = glmatrix.mat2d.create();
const transforms = this.createPathTransformBuffers(pathCount);
for (const glyph of atlasGlyphs) {
const pathID = glyph.pathID;
const atlasOrigin = glyph.calculateSubpixelOrigin(pixelsPerUnit);
glmatrix.mat2d.identity(transform);
glmatrix.mat2d.translate(transform, transform, atlasOrigin);
glmatrix.mat2d.translate(transform, transform, stemDarkeningOffset);
glmatrix.mat2d.rotate(transform, transform, rotationAngle);
glmatrix.mat2d.scale(transform,
transform,
[pixelsPerUnit, pixelsPerUnit * stemDarkeningYScale]);
transforms.st[pathID * 4 + 0] = transform[0];
transforms.st[pathID * 4 + 1] = transform[3];
transforms.st[pathID * 4 + 2] = transform[4];
transforms.st[pathID * 4 + 3] = transform[5];
transforms.ext[pathID * 2 + 0] = transform[1];
transforms.ext[pathID * 2 + 1] = transform[2];
}
return transforms;
}
protected createAtlasFramebuffer(): void {
const atlasColorTexture = this.renderContext.atlas.ensureTexture(this.renderContext);
this.atlasDepthTexture = createFramebufferDepthTexture(this.renderContext.gl, ATLAS_SIZE);
this.atlasFramebuffer = createFramebuffer(this.renderContext.gl,
atlasColorTexture,
this.atlasDepthTexture);
// Allow the antialiasing strategy to set up framebuffers as necessary.
if (this.antialiasingStrategy != null)
this.antialiasingStrategy.setFramebufferSize(this);
}
protected createAAStrategy(aaType: AntialiasingStrategyName,
aaLevel: number,
subpixelAA: SubpixelAAType,
stemDarkening: StemDarkeningMode):
AntialiasingStrategy {
this.subpixelAA = subpixelAA;
this.stemDarkening = stemDarkening;
return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA);
}
protected clearForDirectRendering(): void {}
protected buildAtlasGlyphs(atlasGlyphs: AtlasGlyph[]): void {
const renderContext = this.renderContext;
const font = renderContext.font;
const pixelsPerUnit = this.pixelsPerUnit;
const rotationAngle = this.rotationAngle;
const hint = this.createHint();
atlasGlyphs.sort((a, b) => a.glyphKey.sortKey - b.glyphKey.sortKey);
atlasGlyphs = _.sortedUniqBy(atlasGlyphs, glyph => glyph.glyphKey.sortKey);
if (atlasGlyphs.length === 0)
return;
renderContext.atlasGlyphs = atlasGlyphs;
renderContext.atlas.layoutGlyphs(atlasGlyphs,
font,
pixelsPerUnit,
rotationAngle,
hint,
this.emboldenAmount);
this.uploadPathTransforms(1);
this.uploadPathColors(1);
}
protected pathColorsForObject(objectIndex: number): Uint8Array {
const pathCount = this.pathCount;
const pathColors = new Uint8Array(4 * (pathCount + 1));
for (let pathIndex = 0; pathIndex < pathCount; pathIndex++) {
for (let channel = 0; channel < 3; channel++)
pathColors[(pathIndex + 1) * 4 + channel] = 0xff; // RGB
pathColors[(pathIndex + 1) * 4 + 3] = 0xff; // alpha
}
return pathColors;
}
protected newTimingsReceived(): void {
this.renderContext.newTimingsReceived(this.lastTimings);
}
protected createHint(): Hint {
return new Hint(this.renderContext.font,
this.pixelsPerUnit,
this.renderContext.useHinting);
}
protected directCurveProgramName(): keyof ShaderMap<void> {
return 'directCurve';
}
protected directInteriorProgramName(): keyof ShaderMap<void> {
return 'directInterior';
}
}
export interface SimpleTextLayoutRenderContext extends TextRenderContext {
readonly layout: SimpleTextLayout;
readonly rotationAngle: number;
readonly emboldenAmount: number;
readonly unitsPerEm: number;
}
export abstract class SimpleTextLayoutRenderer extends TextRenderer {
abstract get renderContext(): SimpleTextLayoutRenderContext;
glyphPositionsBuffer!: WebGLBuffer;
glyphTexCoordsBuffer!: WebGLBuffer;
glyphElementsBuffer!: WebGLBuffer;
private glyphBounds!: Float32Array;
get layout(): SimpleTextLayout {
return this.renderContext.layout;
}
get backgroundColor(): glmatrix.vec4 {
return glmatrix.vec4.create();
}
get rotationAngle(): number {
return this.renderContext.rotationAngle;
}
protected get extraEmboldenAmount(): glmatrix.vec2 {
const emboldenLength = this.renderContext.emboldenAmount * this.renderContext.unitsPerEm;
return glmatrix.vec2.clone([emboldenLength, emboldenLength]);
}
prepareToAttachText(): void {
if (this.atlasFramebuffer == null)
this.createAtlasFramebuffer();
this.layoutText();
}
finishAttachingText(): void {
this.buildGlyphs();
this.renderContext.setDirty();
}
setAntialiasingOptions(aaType: AntialiasingStrategyName,
aaLevel: number,
aaOptions: AAOptions):
void {
super.setAntialiasingOptions(aaType, aaLevel, aaOptions);
// Need to relayout because changing AA options can cause font dilation to change...
this.layoutText();
this.buildGlyphs();
this.renderContext.setDirty();
}
relayoutText(): void {
this.layoutText();
this.buildGlyphs();
this.renderContext.setDirty();
}
updateHinting(): void {
// Need to relayout the text because the pixel bounds of the glyphs can change from this...
this.layoutText();
this.buildGlyphs();
this.renderContext.setDirty();
}
updateEmboldenAmount(): void {
// Likewise, need to relayout the text.
this.layoutText();
this.buildGlyphs();
this.renderContext.setDirty();
}
viewPanned(): void {
this.buildGlyphs();
this.renderContext.setDirty();
}
protected compositeIfNecessary(): void {
const renderContext = this.renderContext;
const gl = renderContext.gl;
// Set up composite state.
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, renderContext.cameraView.width, renderContext.cameraView.height);
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.SCISSOR_TEST);
gl.blendEquation(gl.FUNC_REVERSE_SUBTRACT);
gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ZERO, gl.ONE);
gl.enable(gl.BLEND);
// Clear.
gl.clearColor(1.0, 1.0, 1.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// Set the appropriate program.
const programName = this.gammaCorrectionMode === 'off' ? 'blitLinear' : 'blitGamma';
const blitProgram = this.renderContext.shaderPrograms[programName];
// Set up the composite VAO.
const attributes = blitProgram.attributes;
gl.useProgram(blitProgram.program);
gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphPositionsBuffer);
gl.vertexAttribPointer(attributes.aPosition, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphTexCoordsBuffer);
gl.vertexAttribPointer(attributes.aTexCoord, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(attributes.aPosition);
gl.enableVertexAttribArray(attributes.aTexCoord);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glyphElementsBuffer);
// Create the transform.
const transform = glmatrix.mat4.create();
glmatrix.mat4.fromTranslation(transform, [-1.0, -1.0, 0.0]);
glmatrix.mat4.scale(transform, transform, [
2.0 / this.renderContext.cameraView.width,
2.0 / this.renderContext.cameraView.height,
1.0,
]);
glmatrix.mat4.translate(transform,
transform,
[this.camera.translation[0], this.camera.translation[1], 0.0]);
// Blit.
gl.uniformMatrix4fv(blitProgram.uniforms.uTransform, false, transform);
gl.activeTexture(gl.TEXTURE0);
const destTexture = this.renderContext
.atlas
.ensureTexture(this.renderContext);
gl.bindTexture(gl.TEXTURE_2D, destTexture);
gl.uniform1i(blitProgram.uniforms.uSource, 0);
this.setIdentityTexScaleUniform(blitProgram.uniforms);
this.bindGammaLUT(glmatrix.vec3.clone([1.0, 1.0, 1.0]), 1, blitProgram.uniforms);
const totalGlyphCount = this.layout.textFrame.totalGlyphCount;
gl.drawElements(gl.TRIANGLES, totalGlyphCount * 6, gl.UNSIGNED_INT, 0);
}
private layoutText(): void {
const renderContext = this.renderContext;
const gl = renderContext.gl;
this.layout.layoutRuns();
const textBounds = this.layout.textFrame.bounds;
this.camera.bounds = textBounds;
const totalGlyphCount = this.layout.textFrame.totalGlyphCount;
const glyphPositions = new Float32Array(totalGlyphCount * 8);
const glyphIndices = new Uint32Array(totalGlyphCount * 6);
const hint = this.createHint();
const pixelsPerUnit = this.pixelsPerUnit;
const rotationAngle = this.rotationAngle;
let globalGlyphIndex = 0;
for (const run of this.layout.textFrame.runs) {
run.recalculatePixelRects(pixelsPerUnit,
rotationAngle,
hint,
this.emboldenAmount,
SUBPIXEL_GRANULARITY,
textBounds);
for (let glyphIndex = 0;
glyphIndex < run.glyphIDs.length;
glyphIndex++, globalGlyphIndex++) {
const rect = run.pixelRectForGlyphAt(glyphIndex);
glyphPositions.set([
rect[0], rect[3],
rect[2], rect[3],
rect[0], rect[1],
rect[2], rect[1],
], globalGlyphIndex * 8);
for (let glyphIndexIndex = 0;
glyphIndexIndex < QUAD_ELEMENTS.length;
glyphIndexIndex++) {
glyphIndices[glyphIndexIndex + globalGlyphIndex * 6] =
QUAD_ELEMENTS[glyphIndexIndex] + 4 * globalGlyphIndex;
}
}
}
this.glyphPositionsBuffer = unwrapNull(gl.createBuffer());
gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphPositionsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, glyphPositions, gl.STATIC_DRAW);
this.glyphElementsBuffer = unwrapNull(gl.createBuffer());
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glyphElementsBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, glyphIndices, gl.STATIC_DRAW);
}
private buildGlyphs(): void {
const font = this.renderContext.font;
const glyphStore = this.renderContext.glyphStore;
const pixelsPerUnit = this.pixelsPerUnit;
const rotationAngle = this.rotationAngle;
const textFrame = this.layout.textFrame;
const textBounds = textFrame.bounds;
const hint = this.createHint();
// Only build glyphs in view.
const translation = this.camera.translation;
const canvasRect = glmatrix.vec4.clone([
-translation[0],
-translation[1],
-translation[0] + this.renderContext.cameraView.width,
-translation[1] + this.renderContext.cameraView.height,
]);
const atlasGlyphs = [];
for (const run of textFrame.runs) {
for (let glyphIndex = 0; glyphIndex < run.glyphIDs.length; glyphIndex++) {
const pixelRect = run.pixelRectForGlyphAt(glyphIndex);
if (!rectsIntersect(pixelRect, canvasRect))
continue;
const glyphID = run.glyphIDs[glyphIndex];
const glyphStoreIndex = glyphStore.indexOfGlyphWithID(glyphID);
if (glyphStoreIndex == null)
continue;
const subpixel = run.subpixelForGlyphAt(glyphIndex,
pixelsPerUnit,
rotationAngle,
hint,
SUBPIXEL_GRANULARITY,
textBounds);
const glyphKey = new GlyphKey(glyphID, subpixel);
atlasGlyphs.push(new AtlasGlyph(glyphStoreIndex, glyphKey));
}
}
this.buildAtlasGlyphs(atlasGlyphs);
// TODO(pcwalton): Regenerate the IBOs to include only the glyphs we care about.
this.setGlyphTexCoords();
}
private setGlyphTexCoords(): void {
const gl = this.renderContext.gl;
const textFrame = this.layout.textFrame;
const textBounds = textFrame.bounds;
const font = this.renderContext.font;
const atlasGlyphs = this.renderContext.atlasGlyphs;
const hint = this.createHint();
const pixelsPerUnit = this.pixelsPerUnit;
const rotationAngle = this.rotationAngle;
const atlasGlyphKeys = atlasGlyphs.map(atlasGlyph => atlasGlyph.glyphKey.sortKey);
this.glyphBounds = new Float32Array(textFrame.totalGlyphCount * 8);
let globalGlyphIndex = 0;
for (const run of textFrame.runs) {
for (let glyphIndex = 0;
glyphIndex < run.glyphIDs.length;
glyphIndex++, globalGlyphIndex++) {
const textGlyphID = run.glyphIDs[glyphIndex];
const subpixel = run.subpixelForGlyphAt(glyphIndex,
pixelsPerUnit,
rotationAngle,
hint,
SUBPIXEL_GRANULARITY,
textBounds);
const glyphKey = new GlyphKey(textGlyphID, subpixel);
const atlasGlyphIndex = _.sortedIndexOf(atlasGlyphKeys, glyphKey.sortKey);
if (atlasGlyphIndex < 0)
continue;
// Set texture coordinates.
const atlasGlyph = atlasGlyphs[atlasGlyphIndex];
const atlasGlyphMetrics = font.metricsForGlyph(atlasGlyph.glyphKey.id);
if (atlasGlyphMetrics == null)
continue;
const atlasGlyphUnitMetrics = new UnitMetrics(atlasGlyphMetrics,
rotationAngle,
this.emboldenAmount);
const atlasGlyphPixelOrigin =
atlasGlyph.calculateSubpixelOrigin(pixelsPerUnit);
const atlasGlyphRect = calculatePixelRectForGlyph(atlasGlyphUnitMetrics,
atlasGlyphPixelOrigin,
pixelsPerUnit,
hint);
const atlasGlyphBL = atlasGlyphRect.slice(0, 2) as glmatrix.vec2;
const atlasGlyphTR = atlasGlyphRect.slice(2, 4) as glmatrix.vec2;
glmatrix.vec2.div(atlasGlyphBL, atlasGlyphBL, ATLAS_SIZE);
glmatrix.vec2.div(atlasGlyphTR, atlasGlyphTR, ATLAS_SIZE);
this.glyphBounds.set([
atlasGlyphBL[0], atlasGlyphTR[1],
atlasGlyphTR[0], atlasGlyphTR[1],
atlasGlyphBL[0], atlasGlyphBL[1],
atlasGlyphTR[0], atlasGlyphBL[1],
], globalGlyphIndex * 8);
}
}
this.glyphTexCoordsBuffer = unwrapNull(gl.createBuffer());
gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphTexCoordsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, this.glyphBounds, gl.STATIC_DRAW);
}
private setIdentityTexScaleUniform(uniforms: UniformMap): void {
this.renderContext.gl.uniform2f(uniforms.uTexScale, 1.0, 1.0);
}
}
/// The separating axis theorem.
function rectsIntersect(a: glmatrix.vec4, b: glmatrix.vec4): boolean {
return a[2] > b[0] && a[3] > b[1] && a[0] < b[2] && a[1] < b[3];
}

View File

@ -1,453 +0,0 @@
// pathfinder/client/src/text.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.
import * as base64js from 'base64-js';
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import * as opentype from "opentype.js";
import {Metrics} from 'opentype.js';
import {B_QUAD_SIZE, parseServerTiming, PathfinderMeshPack} from "./meshes";
import {PathfinderPackedMeshes} from "./meshes";
import {assert, lerp, panic, UINT32_MAX, UINT32_SIZE, unwrapNull} from "./utils";
export const BUILTIN_FONT_URI: string = "/otf/demo";
const SQRT_2: number = Math.sqrt(2.0);
// Should match macOS 10.13 High Sierra.
//
// We multiply by sqrt(2) to compensate for the fact that dilation amounts are relative to the
// pixel square on macOS and relative to the vertex normal in Pathfinder.
const STEM_DARKENING_FACTORS: glmatrix.vec2 = glmatrix.vec2.clone([
0.0121 * SQRT_2,
0.0121 * 1.25 * SQRT_2,
]);
// Likewise.
const MAX_STEM_DARKENING_AMOUNT: glmatrix.vec2 = glmatrix.vec2.clone([0.3 * SQRT_2, 0.3 * SQRT_2]);
// This value is a subjective cutoff. Above this ppem value, no stem darkening is performed.
export const MAX_STEM_DARKENING_PIXELS_PER_EM: number = 72.0;
const PARTITION_FONT_ENDPOINT_URI: string = "/partition-font";
export interface ExpandedMeshData {
meshes: PathfinderPackedMeshes;
}
export interface PartitionResult {
meshes: PathfinderMeshPack;
time: number;
}
export interface PixelMetrics {
left: number;
right: number;
ascent: number;
descent: number;
}
opentype.Font.prototype.isSupported = function() {
return (this as any).supported;
};
opentype.Font.prototype.lineHeight = function() {
const os2Table = this.tables.os2;
return os2Table.sTypoAscender - os2Table.sTypoDescender + os2Table.sTypoLineGap;
};
export class PathfinderFont {
readonly opentypeFont: opentype.Font;
readonly data: ArrayBuffer;
readonly builtinFontName: string | null;
private metricsCache: Metrics[];
constructor(data: ArrayBuffer, builtinFontName: string | null) {
this.data = data;
this.builtinFontName = builtinFontName != null ? builtinFontName : null;
this.opentypeFont = opentype.parse(data);
if (!this.opentypeFont.isSupported())
panic("Unsupported font!");
this.metricsCache = [];
}
metricsForGlyph(glyphID: number): Metrics | null {
if (this.metricsCache[glyphID] == null)
this.metricsCache[glyphID] = this.opentypeFont.glyphs.get(glyphID).getMetrics();
return this.metricsCache[glyphID];
}
}
export class TextRun {
readonly glyphIDs: number[];
advances: number[];
readonly origin: number[];
private readonly font: PathfinderFont;
private pixelRects: glmatrix.vec4[];
constructor(text: number[] | string, origin: number[], font: PathfinderFont) {
if (typeof(text) === 'string') {
this.glyphIDs = font.opentypeFont
.stringToGlyphs(text)
.map(glyph => (glyph as any).index);
} else {
this.glyphIDs = text;
}
this.origin = origin;
this.advances = [];
this.font = font;
this.pixelRects = [];
}
layout() {
this.advances = [];
let currentX = 0;
for (const glyphID of this.glyphIDs) {
this.advances.push(currentX);
currentX += this.font.opentypeFont.glyphs.get(glyphID).advanceWidth;
}
}
calculatePixelOriginForGlyphAt(index: number,
pixelsPerUnit: number,
rotationAngle: number,
hint: Hint,
textFrameBounds: glmatrix.vec4):
glmatrix.vec2 {
const textFrameCenter = glmatrix.vec2.clone([
0.5 * (textFrameBounds[0] + textFrameBounds[2]),
0.5 * (textFrameBounds[1] + textFrameBounds[3]),
]);
const transform = glmatrix.mat2d.create();
glmatrix.mat2d.fromTranslation(transform, textFrameCenter);
glmatrix.mat2d.rotate(transform, transform, -rotationAngle);
glmatrix.vec2.negate(textFrameCenter, textFrameCenter);
glmatrix.mat2d.translate(transform, transform, textFrameCenter);
const textGlyphOrigin = glmatrix.vec2.create();
glmatrix.vec2.add(textGlyphOrigin, [this.advances[index], 0.0], this.origin);
glmatrix.vec2.transformMat2d(textGlyphOrigin, textGlyphOrigin, transform);
glmatrix.vec2.scale(textGlyphOrigin, textGlyphOrigin, pixelsPerUnit);
return textGlyphOrigin;
}
pixelRectForGlyphAt(index: number): glmatrix.vec4 {
return this.pixelRects[index];
}
subpixelForGlyphAt(index: number,
pixelsPerUnit: number,
rotationAngle: number,
hint: Hint,
subpixelGranularity: number,
textFrameBounds: glmatrix.vec4):
number {
const textGlyphOrigin = this.calculatePixelOriginForGlyphAt(index,
pixelsPerUnit,
rotationAngle,
hint,
textFrameBounds)[0];
return Math.abs(Math.round(textGlyphOrigin * subpixelGranularity) % subpixelGranularity);
}
recalculatePixelRects(pixelsPerUnit: number,
rotationAngle: number,
hint: Hint,
emboldenAmount: glmatrix.vec2,
subpixelGranularity: number,
textFrameBounds: glmatrix.vec4):
void {
for (let index = 0; index < this.glyphIDs.length; index++) {
const metrics = unwrapNull(this.font.metricsForGlyph(this.glyphIDs[index]));
const unitMetrics = new UnitMetrics(metrics, rotationAngle, emboldenAmount);
const textGlyphOrigin = this.calculatePixelOriginForGlyphAt(index,
pixelsPerUnit,
rotationAngle,
hint,
textFrameBounds);
textGlyphOrigin[0] *= subpixelGranularity;
glmatrix.vec2.round(textGlyphOrigin, textGlyphOrigin);
textGlyphOrigin[0] /= subpixelGranularity;
const pixelRect = calculatePixelRectForGlyph(unitMetrics,
textGlyphOrigin,
pixelsPerUnit,
hint);
this.pixelRects[index] = pixelRect;
}
}
get measure(): number {
const lastGlyphID = _.last(this.glyphIDs), lastAdvance = _.last(this.advances);
if (lastGlyphID == null || lastAdvance == null)
return 0.0;
return lastAdvance + this.font.opentypeFont.glyphs.get(lastGlyphID).advanceWidth;
}
}
export class TextFrame {
readonly runs: TextRun[];
readonly origin: glmatrix.vec3;
private readonly font: PathfinderFont;
constructor(runs: TextRun[], font: PathfinderFont) {
this.runs = runs;
this.origin = glmatrix.vec3.create();
this.font = font;
}
expandMeshes(meshes: PathfinderMeshPack, glyphIDs: number[]): ExpandedMeshData {
const pathIDs = [];
for (const textRun of this.runs) {
for (const glyphID of textRun.glyphIDs) {
if (glyphID === 0)
continue;
const pathID = _.sortedIndexOf(glyphIDs, glyphID);
pathIDs.push(pathID + 1);
}
}
return {
meshes: new PathfinderPackedMeshes(meshes, pathIDs),
};
}
get bounds(): glmatrix.vec4 {
if (this.runs.length === 0)
return glmatrix.vec4.create();
const upperLeft = glmatrix.vec2.clone(this.runs[0].origin);
const lowerRight = glmatrix.vec2.clone(_.last(this.runs)!.origin);
const lowerLeft = glmatrix.vec2.clone([upperLeft[0], lowerRight[1]]);
const upperRight = glmatrix.vec2.clone([lowerRight[0], upperLeft[1]]);
const lineHeight = this.font.opentypeFont.lineHeight();
lowerLeft[1] -= lineHeight;
upperRight[1] += lineHeight * 2.0;
upperRight[0] = _.defaultTo<number>(_.max(this.runs.map(run => run.measure)), 0.0);
return glmatrix.vec4.clone([lowerLeft[0], lowerLeft[1], upperRight[0], upperRight[1]]);
}
get totalGlyphCount(): number {
return _.sumBy(this.runs, run => run.glyphIDs.length);
}
get allGlyphIDs(): number[] {
const glyphIDs = [];
for (const run of this.runs)
glyphIDs.push(...run.glyphIDs);
return glyphIDs;
}
}
/// Stores one copy of each glyph.
export class GlyphStore {
readonly font: PathfinderFont;
readonly glyphIDs: number[];
constructor(font: PathfinderFont, glyphIDs: number[]) {
this.font = font;
this.glyphIDs = glyphIDs;
}
partition(): Promise<PartitionResult> {
// Build the partitioning request to the server.
let fontFace;
if (this.font.builtinFontName != null)
fontFace = { Builtin: this.font.builtinFontName };
else
fontFace = { Custom: base64js.fromByteArray(new Uint8Array(this.font.data)) };
const request = {
face: fontFace,
fontIndex: 0,
glyphs: this.glyphIDs.map(id => ({ id: id, transform: [1, 0, 0, 1, 0, 0] })),
pointSize: this.font.opentypeFont.unitsPerEm,
};
// Make the request.
let time = 0;
return window.fetch(PARTITION_FONT_ENDPOINT_URI, {
body: JSON.stringify(request),
headers: {'Content-Type': 'application/json'} as any,
method: 'POST',
}).then(response => {
time = parseServerTiming(response.headers);
return response.arrayBuffer();
}).then(buffer => {
return {
meshes: new PathfinderMeshPack(buffer),
time: time,
};
});
}
indexOfGlyphWithID(glyphID: number): number | null {
const index = _.sortedIndexOf(this.glyphIDs, glyphID);
return index >= 0 ? index : null;
}
}
export class SimpleTextLayout {
readonly textFrame: TextFrame;
constructor(font: PathfinderFont, text: string) {
const lineHeight = font.opentypeFont.lineHeight();
const textRuns: TextRun[] = text.split("\n").map((line, lineNumber) => {
return new TextRun(line, [0.0, -lineHeight * lineNumber], font);
});
this.textFrame = new TextFrame(textRuns, font);
}
layoutRuns() {
this.textFrame.runs.forEach(textRun => textRun.layout());
}
}
export class Hint {
readonly xHeight: number;
readonly hintedXHeight: number;
readonly stemHeight: number;
readonly hintedStemHeight: number;
private useHinting: boolean;
constructor(font: PathfinderFont, pixelsPerUnit: number, useHinting: boolean) {
useHinting = false;
this.useHinting = useHinting;
const os2Table = font.opentypeFont.tables.os2;
this.xHeight = os2Table.sxHeight != null ? os2Table.sxHeight : 0;
this.stemHeight = os2Table.sCapHeight != null ? os2Table.sCapHeight : 0;
if (!useHinting) {
this.hintedXHeight = this.xHeight;
this.hintedStemHeight = this.stemHeight;
} else {
this.hintedXHeight = Math.round(Math.round(this.xHeight * pixelsPerUnit) /
pixelsPerUnit);
this.hintedStemHeight = Math.round(Math.round(this.stemHeight * pixelsPerUnit) /
pixelsPerUnit);
}
}
/// NB: This must match `hintPosition()` in `common.inc.glsl`.
hintPosition(position: glmatrix.vec2): glmatrix.vec2 {
if (!this.useHinting)
return position;
if (position[1] >= this.stemHeight) {
const y = position[1] - this.stemHeight + this.hintedStemHeight;
return glmatrix.vec2.clone([position[0], y]);
}
if (position[1] >= this.xHeight) {
const y = lerp(this.hintedXHeight, this.hintedStemHeight,
(position[1] - this.xHeight) / (this.stemHeight - this.xHeight));
return glmatrix.vec2.clone([position[0], y]);
}
if (position[1] >= 0.0) {
const y = lerp(0.0, this.hintedXHeight, position[1] / this.xHeight);
return glmatrix.vec2.clone([position[0], y]);
}
return position;
}
}
export class UnitMetrics {
left: number;
right: number;
ascent: number;
descent: number;
constructor(metrics: Metrics, rotationAngle: number, emboldenAmount: glmatrix.vec2) {
const left = metrics.xMin;
const bottom = metrics.yMin;
const right = metrics.xMax + emboldenAmount[0] * 2;
const top = metrics.yMax + emboldenAmount[1] * 2;
const transform = glmatrix.mat2.create();
glmatrix.mat2.fromRotation(transform, -rotationAngle);
const lowerLeft = glmatrix.vec2.clone([Infinity, Infinity]);
const upperRight = glmatrix.vec2.clone([-Infinity, -Infinity]);
const points = [[left, bottom], [left, top], [right, top], [right, bottom]];
const transformedPoint = glmatrix.vec2.create();
for (const point of points) {
glmatrix.vec2.transformMat2(transformedPoint, point, transform);
glmatrix.vec2.min(lowerLeft, lowerLeft, transformedPoint);
glmatrix.vec2.max(upperRight, upperRight, transformedPoint);
}
this.left = lowerLeft[0];
this.right = upperRight[0];
this.ascent = upperRight[1];
this.descent = lowerLeft[1];
}
}
export function calculatePixelXMin(metrics: UnitMetrics, pixelsPerUnit: number): number {
return Math.floor(metrics.left * pixelsPerUnit);
}
export function calculatePixelDescent(metrics: UnitMetrics, pixelsPerUnit: number): number {
return Math.floor(metrics.descent * pixelsPerUnit);
}
function calculateSubpixelMetricsForGlyph(metrics: UnitMetrics, pixelsPerUnit: number, hint: Hint):
PixelMetrics {
const ascent = hint.hintPosition(glmatrix.vec2.fromValues(0, metrics.ascent))[1];
return {
ascent: ascent * pixelsPerUnit,
descent: metrics.descent * pixelsPerUnit,
left: metrics.left * pixelsPerUnit,
right: metrics.right * pixelsPerUnit,
};
}
export function calculatePixelRectForGlyph(metrics: UnitMetrics,
subpixelOrigin: glmatrix.vec2,
pixelsPerUnit: number,
hint: Hint):
glmatrix.vec4 {
const pixelMetrics = calculateSubpixelMetricsForGlyph(metrics, pixelsPerUnit, hint);
return glmatrix.vec4.clone([Math.floor(subpixelOrigin[0] + pixelMetrics.left),
Math.floor(subpixelOrigin[1] + pixelMetrics.descent),
Math.ceil(subpixelOrigin[0] + pixelMetrics.right),
Math.ceil(subpixelOrigin[1] + pixelMetrics.ascent)]);
}
export function computeStemDarkeningAmount(pixelsPerEm: number, pixelsPerUnit: number):
glmatrix.vec2 {
const amount = glmatrix.vec2.create();
if (pixelsPerEm > MAX_STEM_DARKENING_PIXELS_PER_EM)
return amount;
glmatrix.vec2.scale(amount, STEM_DARKENING_FACTORS, pixelsPerEm);
glmatrix.vec2.min(amount, amount, MAX_STEM_DARKENING_AMOUNT);
glmatrix.vec2.scale(amount, amount, 1.0 / pixelsPerUnit);
return amount;
}

View File

@ -1,85 +0,0 @@
// pathfinder/client/src/utils.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.
import * as glmatrix from 'gl-matrix';
export const FLOAT32_SIZE: number = 4;
export const UINT16_SIZE: number = 2;
export const UINT8_SIZE: number = 1;
export const UINT32_MAX: number = 0xffffffff;
export const UINT32_SIZE: number = 4;
export const EPSILON: number = 0.001;
export function panic(message: string): never {
throw new PathfinderError(message);
}
export function assert(value: boolean, message: string) {
if (!value)
panic(message);
}
export function expectNotNull<T>(value: T | null, message: string): T {
if (value === null)
throw new PathfinderError(message);
return value;
}
function expectNotUndef<T>(value: T | undefined, message: string): T {
if (value === undefined)
throw new PathfinderError(message);
return value;
}
export function unwrapNull<T>(value: T | null): T {
return expectNotNull(value, "Unexpected null!");
}
export function unwrapUndef<T>(value: T | undefined): T {
return expectNotUndef(value, "Unexpected `undefined`!");
}
export function scaleRect(rect: glmatrix.vec4, scale: number): glmatrix.vec4 {
const upperLeft = glmatrix.vec2.clone([rect[0], rect[1]]);
const lowerRight = glmatrix.vec2.clone([rect[2], rect[3]]);
glmatrix.vec2.scale(upperLeft, upperLeft, scale);
glmatrix.vec2.scale(lowerRight, lowerRight, scale);
return glmatrix.vec4.clone([upperLeft[0], upperLeft[1], lowerRight[0], lowerRight[1]]);
}
export function lerp(a: number, b: number, t: number): number {
return a + (b - a) * t;
}
export class PathfinderError extends Error {
constructor(message?: string | undefined) {
super(message);
}
}
export class Range {
start: number;
end: number;
get isEmpty(): boolean {
return this.start >= this.end;
}
get length(): number {
return this.end - this.start;
}
constructor(start: number, end: number) {
this.start = start;
this.end = end;
}
}

View File

@ -1,427 +0,0 @@
// pathfinder/client/src/view.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.
import * as glmatrix from 'gl-matrix';
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy";
import {StemDarkeningMode, SubpixelAAType} from "./aa-strategy";
import {AAOptions} from './app-controller';
import PathfinderBufferTexture from './buffer-texture';
import {Camera} from "./camera";
import {EXTDisjointTimerQuery, QUAD_ELEMENTS, UniformMap} from './gl-utils';
import {PathfinderPackedMeshBuffers, PathfinderPackedMeshes} from './meshes';
import {Renderer} from './renderer';
import {PathfinderShaderProgram, SHADER_NAMES, ShaderMap} from './shader-loader';
import {ShaderProgramSource, UnlinkedShaderProgram} from './shader-loader';
import {expectNotNull, PathfinderError, UINT32_SIZE, unwrapNull} from './utils';
const QUAD_POSITIONS: Float32Array = new Float32Array([
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
1.0, 1.0,
]);
const QUAD_TEX_COORDS: Float32Array = new Float32Array([
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
1.0, 1.0,
]);
export const TIMINGS: {[name: string]: string} = {
compositing: "Compositing",
rendering: "Rendering",
};
export interface Timings {
compositing: number;
rendering: number;
}
export type ColorAlphaFormat = 'RGBA8' | 'RGB5_A1';
declare class WebGLQuery {}
export abstract class PathfinderView {
canvas: HTMLCanvasElement;
suppressAutomaticRedraw: boolean;
vrDisplayWidth: number | null;
vrDisplayHeight: number | null;
protected abstract get camera(): Camera;
private dirty: boolean;
private pulseHandle: number;
constructor() {
this.dirty = false;
this.pulseHandle = 0;
this.suppressAutomaticRedraw = false;
this.canvas = unwrapNull(document.getElementById('pf-canvas')) as HTMLCanvasElement;
window.addEventListener('resize', () => this.resizeToFit(false), false);
this.vrDisplayHeight = null;
this.vrDisplayWidth = null;
}
setDirty(): void {
if (this.dirty || this.suppressAutomaticRedraw)
return;
this.dirty = true;
window.requestAnimationFrame(() => this.redraw());
}
zoomIn(): void {
this.camera.zoomIn();
}
zoomOut(): void {
this.camera.zoomOut();
}
zoomPulse(): void {
if (this.pulseHandle) {
window.cancelAnimationFrame(this.pulseHandle);
this.pulseHandle = 0;
return;
}
let c = 0;
let d = 0.005;
const self = this;
function tick() {
self.camera.zoom(1 + d);
if (c++ % 200 === 0) {
d *= -1;
}
self.pulseHandle = window.requestAnimationFrame(tick);
}
this.pulseHandle = window.requestAnimationFrame(tick);
}
rotate(newAngle: number): void {
this.camera.rotate(newAngle);
}
redraw(): void {
this.dirty = false;
}
protected resized(): void {
this.setDirty();
}
protected inVR(): boolean {
return false;
}
protected resizeToFit(initialSize: boolean): void {
if (!this.canvas.classList.contains('pf-no-autoresize')) {
if (this.inVR()) {
const width = unwrapNull(this.vrDisplayWidth);
const height = unwrapNull(this.vrDisplayHeight);
// these are already in device pixel units, no need to multiply
this.canvas.style.width = width + 'px';
this.canvas.style.height = height + 'px';
this.canvas.width = width;
this.canvas.height = height;
} else {
const width = window.innerWidth;
const canvasTop = this.canvas.getBoundingClientRect().top;
const height = window.scrollY + window.innerHeight - canvasTop;
const devicePixelRatio = window.devicePixelRatio;
const canvasSize = new Float32Array([width, height]) as glmatrix.vec2;
glmatrix.vec2.scale(canvasSize, canvasSize, devicePixelRatio);
glmatrix.vec2.round(canvasSize, canvasSize);
this.canvas.style.width = width + 'px';
this.canvas.style.height = height + 'px';
this.canvas.width = canvasSize[0];
this.canvas.height = canvasSize[1];
}
}
this.resized();
}
}
export abstract class DemoView extends PathfinderView implements RenderContext {
readonly renderer!: Renderer;
gl!: WebGLRenderingContext;
shaderPrograms: ShaderMap<PathfinderShaderProgram>;
areaLUT: HTMLImageElement;
gammaLUT: HTMLImageElement;
instancedArraysExt!: ANGLE_instanced_arrays;
textureHalfFloatExt!: OESTextureHalfFloat;
timerQueryExt!: EXTDisjointTimerQuery | null;
vertexArrayObjectExt!: OESVertexArrayObject;
quadPositionsBuffer!: WebGLBuffer;
quadTexCoordsBuffer!: WebGLBuffer;
quadElementsBuffer!: WebGLBuffer;
atlasRenderingTimerQuery!: WebGLQuery | null;
compositingTimerQuery!: WebGLQuery | null;
meshes: PathfinderPackedMeshBuffers[];
meshData: PathfinderPackedMeshes[];
get colorAlphaFormat(): ColorAlphaFormat {
// On macOS, RGBA framebuffers seem to cause driver stalls when switching between rendering
// and texturing. Work around this by using RGB5A1 instead.
return navigator.platform === 'MacIntel' ? 'RGB5_A1' : 'RGBA8';
}
get renderContext(): RenderContext {
return this;
}
protected colorBufferHalfFloatExt: any;
private wantsScreenshot: boolean;
/// NB: All subclasses are responsible for creating a renderer in their constructors.
constructor(areaLUT: HTMLImageElement,
gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>) {
super();
this.meshes = [];
this.meshData = [];
this.initContext();
const shaderSource = this.compileShaders(commonShaderSource, shaderSources);
this.shaderPrograms = this.linkShaders(shaderSource);
this.areaLUT = areaLUT;
this.gammaLUT = gammaLUT;
this.wantsScreenshot = false;
}
attachMeshes(meshes: PathfinderPackedMeshes[]): void {
this.renderer.attachMeshes(meshes);
this.setDirty();
}
initQuadVAO(attributes: any): void {
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadPositionsBuffer);
this.gl.vertexAttribPointer(attributes.aPosition, 2, this.gl.FLOAT, false, 0, 0);
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadTexCoordsBuffer);
this.gl.vertexAttribPointer(attributes.aTexCoord, 2, this.gl.FLOAT, false, 0, 0);
this.gl.enableVertexAttribArray(attributes.aPosition);
this.gl.enableVertexAttribArray(attributes.aTexCoord);
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.quadElementsBuffer);
}
queueScreenshot(): void {
this.wantsScreenshot = true;
this.setDirty();
}
setAntialiasingOptions(aaType: AntialiasingStrategyName,
aaLevel: number,
aaOptions: AAOptions):
void {
this.renderer.setAntialiasingOptions(aaType, aaLevel, aaOptions);
}
snapshot(rect: glmatrix.vec4): Uint8Array {
const gl = this.renderContext.gl;
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
const canvasHeight = this.canvas.height;
const width = rect[2] - rect[0], height = rect[3] - rect[1];
const originX = Math.max(rect[0], 0);
const originY = Math.max(canvasHeight - height, 0);
const flippedBuffer = new Uint8Array(width * height * 4);
gl.readPixels(originX, originY, width, height, gl.RGBA, gl.UNSIGNED_BYTE, flippedBuffer);
const buffer = new Uint8Array(width * height * 4);
for (let y = 0; y < height; y++) {
const destRowStart = y * width * 4;
const srcRowStart = (height - y - 1) * width * 4;
buffer.set(flippedBuffer.slice(srcRowStart, srcRowStart + width * 4),
destRowStart);
}
return buffer;
}
enterVR(): void {}
redrawVR(): void {
this.renderer.redraw();
}
redraw(): void {
super.redraw();
if (!this.renderer.meshesAttached)
return;
if (!this.renderer.inVR) {
this.renderer.redraw();
} else {
this.redrawVR();
}
// Invoke the post-render hook.
this.renderingFinished();
// Take a screenshot if desired.
if (this.wantsScreenshot) {
this.wantsScreenshot = false;
this.takeScreenshot();
}
}
protected resized(): void {
super.resized();
this.renderer.canvasResized();
}
protected inVR(): boolean {
return this.renderer.inVR;
}
protected initContext(): void {
// Initialize the OpenGL context.
this.gl = expectNotNull(this.canvas.getContext('webgl', { antialias: false, depth: true }),
"Failed to initialize WebGL! Check that your browser supports it.");
this.colorBufferHalfFloatExt = this.gl.getExtension('EXT_color_buffer_half_float');
this.instancedArraysExt = unwrapNull(this.gl.getExtension('ANGLE_instanced_arrays'));
this.textureHalfFloatExt = unwrapNull(this.gl.getExtension('OES_texture_half_float'));
this.timerQueryExt = this.gl.getExtension('EXT_disjoint_timer_query');
this.vertexArrayObjectExt = unwrapNull(this.gl.getExtension('OES_vertex_array_object'));
this.gl.getExtension('EXT_frag_depth');
this.gl.getExtension('OES_element_index_uint');
this.gl.getExtension('OES_standard_derivatives');
this.gl.getExtension('OES_texture_float');
this.gl.getExtension('WEBGL_depth_texture');
// Upload quad buffers.
this.quadPositionsBuffer = unwrapNull(this.gl.createBuffer());
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadPositionsBuffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, QUAD_POSITIONS, this.gl.STATIC_DRAW);
this.quadTexCoordsBuffer = unwrapNull(this.gl.createBuffer());
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadTexCoordsBuffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, QUAD_TEX_COORDS, this.gl.STATIC_DRAW);
this.quadElementsBuffer = unwrapNull(this.gl.createBuffer());
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.quadElementsBuffer);
this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, QUAD_ELEMENTS, this.gl.STATIC_DRAW);
// Set up our timer queries for profiling.
if (this.timerQueryExt != null) {
this.atlasRenderingTimerQuery = this.timerQueryExt.createQueryEXT();
this.compositingTimerQuery = this.timerQueryExt.createQueryEXT();
} else {
this.atlasRenderingTimerQuery = null;
this.compositingTimerQuery = null;
}
}
protected renderingFinished(): void {}
private compileShaders(commonSource: string, shaderSources: ShaderMap<ShaderProgramSource>):
ShaderMap<UnlinkedShaderProgram> {
const shaders: Partial<ShaderMap<Partial<UnlinkedShaderProgram>>> = {};
for (const shaderKey of SHADER_NAMES) {
for (const typeName of ['vertex', 'fragment'] as Array<'vertex' | 'fragment'>) {
const type = {
fragment: this.gl.FRAGMENT_SHADER,
vertex: this.gl.VERTEX_SHADER,
}[typeName];
const source = shaderSources[shaderKey][typeName];
const shader = this.gl.createShader(type);
if (shader == null)
throw new PathfinderError("Failed to create shader!");
this.gl.shaderSource(shader, commonSource + "\n#line 1\n" + source);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
const infoLog = this.gl.getShaderInfoLog(shader);
throw new PathfinderError(`Failed to compile ${typeName} shader ` +
`"${shaderKey}":\n${infoLog}`);
}
if (shaders[shaderKey] == null)
shaders[shaderKey] = {};
shaders[shaderKey]![typeName] = shader;
}
}
return shaders as ShaderMap<UnlinkedShaderProgram>;
}
private linkShaders(shaders: ShaderMap<UnlinkedShaderProgram>):
ShaderMap<PathfinderShaderProgram> {
const shaderProgramMap: Partial<ShaderMap<PathfinderShaderProgram>> = {};
for (const shaderName of Object.keys(shaders) as Array<keyof ShaderMap<string>>) {
shaderProgramMap[shaderName] = new PathfinderShaderProgram(this.gl,
shaderName,
shaders[shaderName]);
}
return shaderProgramMap as ShaderMap<PathfinderShaderProgram>;
}
private takeScreenshot(): void {
const width = this.canvas.width, height = this.canvas.height;
const scratchCanvas = document.createElement('canvas');
scratchCanvas.width = width;
scratchCanvas.height = height;
const scratch2DContext = unwrapNull(scratchCanvas.getContext('2d'));
scratch2DContext.drawImage(this.canvas, 0, 0, width, height);
const scratchLink = document.createElement('a');
scratchLink.download = 'pathfinder-screenshot.png';
scratchLink.href = scratchCanvas.toDataURL();
scratchLink.style.position = 'absolute';
document.body.appendChild(scratchLink);
scratchLink.click();
document.body.removeChild(scratchLink);
}
}
export interface RenderContext {
/// The OpenGL context.
readonly gl: WebGLRenderingContext;
readonly instancedArraysExt: ANGLEInstancedArrays;
readonly textureHalfFloatExt: OESTextureHalfFloat;
readonly timerQueryExt: EXTDisjointTimerQuery | null;
readonly vertexArrayObjectExt: OESVertexArrayObject;
readonly colorAlphaFormat: ColorAlphaFormat;
readonly shaderPrograms: ShaderMap<PathfinderShaderProgram>;
readonly areaLUT: HTMLImageElement;
readonly gammaLUT: HTMLImageElement;
readonly quadPositionsBuffer: WebGLBuffer;
readonly quadElementsBuffer: WebGLBuffer;
readonly atlasRenderingTimerQuery: WebGLQuery | null;
readonly compositingTimerQuery: WebGLQuery | null;
initQuadVAO(attributes: any): void;
setDirty(): void;
}

View File

@ -1,886 +0,0 @@
// pathfinder/demo/client/src/xcaa-strategy.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.
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import {AntialiasingStrategy, DirectRenderingMode, SubpixelAAType} from './aa-strategy';
import PathfinderBufferTexture from './buffer-texture';
import {createFramebuffer, createFramebufferColorTexture} from './gl-utils';
import {createFramebufferDepthTexture, setTextureParameters, UniformMap} from './gl-utils';
import {WebGLVertexArrayObject} from './gl-utils';
import {B_QUAD_LOWER_INDICES_OFFSET, B_QUAD_SIZE, B_QUAD_UPPER_INDICES_OFFSET} from './meshes';
import {Renderer} from './renderer';
import {PathfinderShaderProgram, ShaderMap} from './shader-loader';
import {computeStemDarkeningAmount} from './text';
import {assert, FLOAT32_SIZE, lerp, Range, UINT16_SIZE, UINT32_SIZE, unwrapNull} from './utils';
import {unwrapUndef} from './utils';
import {RenderContext} from './view';
interface FastEdgeVAOs {
upper: WebGLVertexArrayObject;
lower: WebGLVertexArrayObject;
}
type Direction = 'upper' | 'lower';
const DIRECTIONS: Direction[] = ['upper', 'lower'];
const PATCH_VERTICES: Float32Array = new Float32Array([
0.0, 0.0,
0.5, 0.0,
1.0, 0.0,
0.0, 1.0,
0.5, 1.0,
1.0, 1.0,
]);
const MCAA_PATCH_INDICES: Uint8Array = new Uint8Array([0, 1, 2, 1, 3, 2]);
export type TransformType = 'affine' | '3d';
export abstract class XCAAStrategy extends AntialiasingStrategy {
abstract readonly directRenderingMode: DirectRenderingMode;
protected patchVertexBuffer: WebGLBuffer | null = null;
protected patchIndexBuffer: WebGLBuffer | null = null;
get passCount(): number {
return 1;
}
protected abstract get transformType(): TransformType;
protected abstract get patchIndices(): Uint8Array;
protected pathBoundsBufferTextures: PathfinderBufferTexture[];
protected supersampledFramebufferSize: glmatrix.vec2;
protected destFramebufferSize: glmatrix.vec2;
protected resolveVAO: WebGLVertexArrayObject | null;
protected aaAlphaTexture: WebGLTexture | null = null;
protected aaDepthTexture: WebGLTexture | null = null;
protected aaFramebuffer: WebGLFramebuffer | null = null;
protected abstract get mightUseAAFramebuffer(): boolean;
constructor(level: number, subpixelAA: SubpixelAAType) {
super(subpixelAA);
this.supersampledFramebufferSize = glmatrix.vec2.create();
this.destFramebufferSize = glmatrix.vec2.create();
this.pathBoundsBufferTextures = [];
}
init(renderer: Renderer): void {
super.init(renderer);
}
attachMeshes(renderer: Renderer): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
this.createResolveVAO(renderer);
this.pathBoundsBufferTextures = [];
this.patchVertexBuffer = unwrapNull(gl.createBuffer());
gl.bindBuffer(gl.ARRAY_BUFFER, this.patchVertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, PATCH_VERTICES.buffer as ArrayBuffer, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
this.patchIndexBuffer = unwrapNull(gl.createBuffer());
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.patchIndexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,
this.patchIndices.buffer as ArrayBuffer,
gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
}
setFramebufferSize(renderer: Renderer): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
this.destFramebufferSize = glmatrix.vec2.clone(renderer.destAllocatedSize);
glmatrix.vec2.mul(this.supersampledFramebufferSize,
this.destFramebufferSize,
this.supersampleScale);
this.initAAAlphaFramebuffer(renderer);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
prepareForRendering(renderer: Renderer): void {}
prepareForDirectRendering(renderer: Renderer): void {}
finishAntialiasingObject(renderer: Renderer, objectIndex: number): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
this.initResolveFramebufferForObject(renderer, objectIndex);
if (!this.usesAAFramebuffer(renderer))
return;
const usedSize = this.supersampledUsedSize(renderer);
gl.scissor(0, 0, usedSize[0], usedSize[1]);
gl.enable(gl.SCISSOR_TEST);
// Clear out the color and depth textures.
gl.clearColor(1.0, 1.0, 1.0, 1.0);
gl.clearDepth(0.0);
gl.depthMask(true);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
prepareToRenderObject(renderer: Renderer, objectIndex: number): void {}
finishDirectlyRenderingObject(renderer: Renderer, objectIndex: number): void {
// TODO(pcwalton)
}
antialiasObject(renderer: Renderer, objectIndex: number): void {
// Perform early preparations.
this.createPathBoundsBufferTextureForObjectIfNecessary(renderer, objectIndex);
// Set up antialiasing.
this.prepareAA(renderer);
// Clear.
this.clearForAA(renderer);
}
resolveAAForObject(renderer: Renderer, objectIndex: number): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
if (!this.usesAAFramebuffer(renderer))
return;
const resolveProgram = this.getResolveProgram(renderer);
if (resolveProgram == null)
return;
// Set state for XCAA resolve.
const usedSize = renderer.destUsedSize;
gl.scissor(0, 0, usedSize[0], usedSize[1]);
gl.enable(gl.SCISSOR_TEST);
this.setDepthAndBlendModeForResolve(renderContext);
// Clear out the resolve buffer, if necessary.
this.clearForResolve(renderer);
// Resolve.
gl.useProgram(resolveProgram.program);
renderContext.vertexArrayObjectExt.bindVertexArrayOES(this.resolveVAO);
gl.uniform2i(resolveProgram.uniforms.uFramebufferSize,
this.destFramebufferSize[0],
this.destFramebufferSize[1]);
gl.activeTexture(renderContext.gl.TEXTURE0);
gl.bindTexture(renderContext.gl.TEXTURE_2D, this.aaAlphaTexture);
gl.uniform1i(resolveProgram.uniforms.uAAAlpha, 0);
gl.uniform2i(resolveProgram.uniforms.uAAAlphaDimensions,
this.supersampledFramebufferSize[0],
this.supersampledFramebufferSize[1]);
if (renderer.bgColor != null)
gl.uniform4fv(resolveProgram.uniforms.uBGColor, renderer.bgColor);
if (renderer.fgColor != null)
gl.uniform4fv(resolveProgram.uniforms.uFGColor, renderer.fgColor);
renderer.setTransformSTAndTexScaleUniformsForDest(resolveProgram.uniforms);
this.setSubpixelAAKernelUniform(renderer, resolveProgram.uniforms);
this.setAdditionalStateForResolveIfNecessary(renderer, resolveProgram, 1);
gl.drawElements(renderContext.gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0);
renderContext.vertexArrayObjectExt.bindVertexArrayOES(null);
}
resolve(pass: number, renderer: Renderer): void {}
get transform(): glmatrix.mat4 {
return glmatrix.mat4.create();
}
protected abstract usesAAFramebuffer(renderer: Renderer): boolean;
protected supersampledUsedSize(renderer: Renderer): glmatrix.vec2 {
const usedSize = glmatrix.vec2.create();
glmatrix.vec2.mul(usedSize, renderer.destUsedSize, this.supersampleScale);
return usedSize;
}
protected prepareAA(renderer: Renderer): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
// Set state for antialiasing.
const usedSize = this.supersampledUsedSize(renderer);
if (this.usesAAFramebuffer(renderer))
gl.bindFramebuffer(gl.FRAMEBUFFER, this.aaFramebuffer);
gl.viewport(0,
0,
this.supersampledFramebufferSize[0],
this.supersampledFramebufferSize[1]);
gl.scissor(0, 0, usedSize[0], usedSize[1]);
gl.enable(gl.SCISSOR_TEST);
}
protected setAAState(renderer: Renderer): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
const usedSize = this.supersampledUsedSize(renderer);
if (this.usesAAFramebuffer(renderer))
gl.bindFramebuffer(gl.FRAMEBUFFER, this.aaFramebuffer);
gl.viewport(0,
0,
this.supersampledFramebufferSize[0],
this.supersampledFramebufferSize[1]);
gl.scissor(0, 0, usedSize[0], usedSize[1]);
gl.enable(gl.SCISSOR_TEST);
this.setAADepthState(renderer);
}
protected setAAUniforms(renderer: Renderer, uniforms: UniformMap, objectIndex: number): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
switch (this.transformType) {
case 'affine':
renderer.setTransformAffineUniforms(uniforms, 0);
break;
case '3d':
renderer.setTransformUniform(uniforms, 0, 0);
break;
}
gl.uniform2i(uniforms.uFramebufferSize,
this.supersampledFramebufferSize[0],
this.supersampledFramebufferSize[1]);
renderer.pathTransformBufferTextures[0].ext.bind(gl, uniforms, 0);
renderer.pathTransformBufferTextures[0].st.bind(gl, uniforms, 1);
this.pathBoundsBufferTextures[objectIndex].bind(gl, uniforms, 2);
renderer.setHintsUniform(uniforms);
renderer.bindAreaLUT(4, uniforms);
}
protected setDepthAndBlendModeForResolve(renderContext: RenderContext): void {
const gl = renderContext.gl;
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.BLEND);
}
protected setAdditionalStateForResolveIfNecessary(renderer: Renderer,
resolveProgram: PathfinderShaderProgram,
firstFreeTextureUnit: number):
void {}
protected abstract clearForAA(renderer: Renderer): void;
protected abstract getResolveProgram(renderer: Renderer): PathfinderShaderProgram | null;
protected abstract setAADepthState(renderer: Renderer): void;
protected abstract clearForResolve(renderer: Renderer): void;
private initResolveFramebufferForObject(renderer: Renderer, objectIndex: number): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
gl.bindFramebuffer(gl.FRAMEBUFFER, renderer.destFramebuffer);
renderer.setDrawViewport();
gl.disable(gl.SCISSOR_TEST);
}
private initAAAlphaFramebuffer(renderer: Renderer): void {
if (!this.mightUseAAFramebuffer) {
this.aaAlphaTexture = null;
this.aaDepthTexture = null;
this.aaFramebuffer = null;
return;
}
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
this.aaAlphaTexture = unwrapNull(gl.createTexture());
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.aaAlphaTexture);
gl.texImage2D(gl.TEXTURE_2D,
0,
gl.RGB,
this.supersampledFramebufferSize[0],
this.supersampledFramebufferSize[1],
0,
gl.RGB,
renderContext.textureHalfFloatExt.HALF_FLOAT_OES,
null);
setTextureParameters(gl, gl.NEAREST);
this.aaDepthTexture = createFramebufferDepthTexture(gl, this.supersampledFramebufferSize);
this.aaFramebuffer = createFramebuffer(gl, this.aaAlphaTexture, this.aaDepthTexture);
}
private createPathBoundsBufferTextureForObjectIfNecessary(renderer: Renderer,
objectIndex: number):
void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
const pathBounds = renderer.pathBoundingRects(objectIndex);
if (this.pathBoundsBufferTextures[objectIndex] == null) {
this.pathBoundsBufferTextures[objectIndex] =
new PathfinderBufferTexture(gl, 'uPathBounds');
}
this.pathBoundsBufferTextures[objectIndex].upload(gl, pathBounds);
}
private createResolveVAO(renderer: Renderer): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
const resolveProgram = this.getResolveProgram(renderer);
if (resolveProgram == null)
return;
this.resolveVAO = renderContext.vertexArrayObjectExt.createVertexArrayOES();
renderContext.vertexArrayObjectExt.bindVertexArrayOES(this.resolveVAO);
gl.useProgram(resolveProgram.program);
renderContext.initQuadVAO(resolveProgram.attributes);
renderContext.vertexArrayObjectExt.bindVertexArrayOES(null);
}
protected get directDepthTexture(): WebGLTexture | null {
return null;
}
protected get supersampleScale(): glmatrix.vec2 {
return glmatrix.vec2.fromValues(this.subpixelAA !== 'none' ? 3.0 : 1.0, 1.0);
}
}
export class MCAAStrategy extends XCAAStrategy {
protected vao: WebGLVertexArrayObject | null;
protected get patchIndices(): Uint8Array {
return MCAA_PATCH_INDICES;
}
protected get transformType(): TransformType {
return 'affine';
}
protected get mightUseAAFramebuffer(): boolean {
return true;
}
attachMeshes(renderer: Renderer): void {
super.attachMeshes(renderer);
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
this.vao = renderContext.vertexArrayObjectExt.createVertexArrayOES();
}
antialiasObject(renderer: Renderer, objectIndex: number): void {
super.antialiasObject(renderer, objectIndex);
const shaderProgram = this.edgeProgram(renderer);
this.antialiasEdgesOfObjectWithProgram(renderer, objectIndex, shaderProgram);
}
protected usesAAFramebuffer(renderer: Renderer): boolean {
return !renderer.isMulticolor;
}
protected getResolveProgram(renderer: Renderer): PathfinderShaderProgram | null {
const renderContext = renderer.renderContext;
if (renderer.isMulticolor)
return null;
if (this.subpixelAA !== 'none' && renderer.allowSubpixelAA)
return renderContext.shaderPrograms.xcaaMonoSubpixelResolve;
return renderContext.shaderPrograms.xcaaMonoResolve;
}
protected clearForAA(renderer: Renderer): void {
if (!this.usesAAFramebuffer(renderer))
return;
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
gl.clearColor(0.0, 0.0, 0.0, 0.0);
gl.clearDepth(0.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
protected setAADepthState(renderer: Renderer): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
if (this.directRenderingMode !== 'conservative') {
gl.disable(gl.DEPTH_TEST);
return;
}
gl.depthFunc(gl.GREATER);
gl.depthMask(false);
gl.enable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
}
protected clearForResolve(renderer: Renderer): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
if (!renderer.isMulticolor) {
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
}
}
protected setBlendModeForAA(renderer: Renderer): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
if (renderer.isMulticolor)
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE);
else
gl.blendFunc(gl.ONE, gl.ONE);
gl.blendEquation(gl.FUNC_ADD);
gl.enable(gl.BLEND);
}
protected prepareAA(renderer: Renderer): void {
super.prepareAA(renderer);
this.setBlendModeForAA(renderer);
}
protected initVAOForObject(renderer: Renderer, objectIndex: number): void {
if (renderer.meshBuffers == null || renderer.meshes == null)
return;
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
const pathRange = renderer.pathRangeForObject(objectIndex);
const meshIndex = renderer.meshIndexForObject(objectIndex);
const shaderProgram = this.edgeProgram(renderer);
const attributes = shaderProgram.attributes;
// FIXME(pcwalton): Refactor.
const vao = this.vao;
renderContext.vertexArrayObjectExt.bindVertexArrayOES(vao);
const bBoxRanges = renderer.meshes[meshIndex].bBoxPathRanges;
const offset = calculateStartFromIndexRanges(pathRange, bBoxRanges);
gl.useProgram(shaderProgram.program);
gl.bindBuffer(gl.ARRAY_BUFFER, renderer.renderContext.quadPositionsBuffer);
gl.vertexAttribPointer(attributes.aTessCoord, 2, gl.FLOAT, false, FLOAT32_SIZE * 2, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, renderer.meshBuffers[meshIndex].bBoxes);
gl.vertexAttribPointer(attributes.aRect,
4,
gl.FLOAT,
false,
FLOAT32_SIZE * 20,
FLOAT32_SIZE * 0 + offset * FLOAT32_SIZE * 20);
gl.vertexAttribPointer(attributes.aUV,
4,
gl.FLOAT,
false,
FLOAT32_SIZE * 20,
FLOAT32_SIZE * 4 + offset * FLOAT32_SIZE * 20);
gl.vertexAttribPointer(attributes.aDUVDX,
4,
gl.FLOAT,
false,
FLOAT32_SIZE * 20,
FLOAT32_SIZE * 8 + offset * FLOAT32_SIZE * 20);
gl.vertexAttribPointer(attributes.aDUVDY,
4,
gl.FLOAT,
false,
FLOAT32_SIZE * 20,
FLOAT32_SIZE * 12 + offset * FLOAT32_SIZE * 20);
gl.vertexAttribPointer(attributes.aSignMode,
4,
gl.FLOAT,
false,
FLOAT32_SIZE * 20,
FLOAT32_SIZE * 16 + offset * FLOAT32_SIZE * 20);
gl.enableVertexAttribArray(attributes.aTessCoord);
gl.enableVertexAttribArray(attributes.aRect);
gl.enableVertexAttribArray(attributes.aUV);
gl.enableVertexAttribArray(attributes.aDUVDX);
gl.enableVertexAttribArray(attributes.aDUVDY);
gl.enableVertexAttribArray(attributes.aSignMode);
renderContext.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aRect, 1);
renderContext.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aUV, 1);
renderContext.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aDUVDX, 1);
renderContext.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aDUVDY, 1);
renderContext.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aSignMode, 1);
gl.bindBuffer(gl.ARRAY_BUFFER, renderer.meshBuffers[meshIndex].bBoxPathIDs);
gl.vertexAttribPointer(attributes.aPathID,
1,
gl.UNSIGNED_SHORT,
false,
UINT16_SIZE,
offset * UINT16_SIZE);
gl.enableVertexAttribArray(attributes.aPathID);
renderContext.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aPathID, 1);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, renderer.renderContext.quadElementsBuffer);
renderContext.vertexArrayObjectExt.bindVertexArrayOES(null);
}
protected edgeProgram(renderer: Renderer): PathfinderShaderProgram {
return renderer.renderContext.shaderPrograms.mcaa;
}
protected antialiasEdgesOfObjectWithProgram(renderer: Renderer,
objectIndex: number,
shaderProgram: PathfinderShaderProgram):
void {
if (renderer.meshBuffers == null || renderer.meshes == null)
return;
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
const pathRange = renderer.pathRangeForObject(objectIndex);
const meshIndex = renderer.meshIndexForObject(objectIndex);
this.initVAOForObject(renderer, objectIndex);
gl.useProgram(shaderProgram.program);
const uniforms = shaderProgram.uniforms;
this.setAAUniforms(renderer, uniforms, objectIndex);
// FIXME(pcwalton): Refactor.
const vao = this.vao;
renderContext.vertexArrayObjectExt.bindVertexArrayOES(vao);
this.setBlendModeForAA(renderer);
this.setAADepthState(renderer);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, renderContext.quadElementsBuffer);
const bBoxRanges = renderer.meshes[meshIndex].bBoxPathRanges;
const count = calculateCountFromIndexRanges(pathRange, bBoxRanges);
renderContext.instancedArraysExt
.drawElementsInstancedANGLE(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, count);
renderContext.vertexArrayObjectExt.bindVertexArrayOES(null);
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
}
get directRenderingMode(): DirectRenderingMode {
// FIXME(pcwalton): Only in multicolor mode?
return 'conservative';
}
protected setAAUniforms(renderer: Renderer, uniforms: UniformMap, objectIndex: number): void {
super.setAAUniforms(renderer, uniforms, objectIndex);
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
renderer.setPathColorsUniform(0, uniforms, 3);
gl.uniform1i(uniforms.uMulticolor, renderer.isMulticolor ? 1 : 0);
}
}
export class StencilAAAStrategy extends XCAAStrategy {
directRenderingMode: DirectRenderingMode = 'none';
protected transformType: TransformType = 'affine';
protected patchIndices: Uint8Array = MCAA_PATCH_INDICES;
protected mightUseAAFramebuffer: boolean = true;
private vao: WebGLVertexArrayObject;
attachMeshes(renderer: Renderer): void {
super.attachMeshes(renderer);
this.createVAO(renderer);
}
antialiasObject(renderer: Renderer, objectIndex: number): void {
super.antialiasObject(renderer, objectIndex);
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
if (renderer.meshes == null)
return;
// Antialias.
const shaderPrograms = renderer.renderContext.shaderPrograms;
this.setAAState(renderer);
this.setBlendModeForAA(renderer);
const program = renderContext.shaderPrograms.stencilAAA;
gl.useProgram(program.program);
const uniforms = program.uniforms;
this.setAAUniforms(renderer, uniforms, objectIndex);
renderContext.vertexArrayObjectExt.bindVertexArrayOES(this.vao);
// FIXME(pcwalton): Only render the appropriate instances.
const count = renderer.meshes[0].count('stencilSegments');
for (let side = 0; side < 2; side++) {
gl.uniform1i(uniforms.uSide, side);
renderContext.instancedArraysExt
.drawElementsInstancedANGLE(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, count);
}
renderContext.vertexArrayObjectExt.bindVertexArrayOES(null);
}
protected usesAAFramebuffer(renderer: Renderer): boolean {
return true;
}
protected clearForAA(renderer: Renderer): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
gl.clearColor(0.0, 0.0, 0.0, 0.0);
gl.clearDepth(0.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
protected getResolveProgram(renderer: Renderer): PathfinderShaderProgram | null {
const renderContext = renderer.renderContext;
if (this.subpixelAA !== 'none' && renderer.allowSubpixelAA)
return renderContext.shaderPrograms.xcaaMonoSubpixelResolve;
return renderContext.shaderPrograms.xcaaMonoResolve;
}
protected setAADepthState(renderer: Renderer): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
}
protected setAAUniforms(renderer: Renderer, uniforms: UniformMap, objectIndex: number):
void {
super.setAAUniforms(renderer, uniforms, objectIndex);
renderer.setEmboldenAmountUniform(objectIndex, uniforms);
}
protected clearForResolve(renderer: Renderer): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
gl.clearColor(0.0, 0.0, 0.0, 0.0);
gl.clear(gl.COLOR_BUFFER_BIT);
}
private createVAO(renderer: Renderer): void {
if (renderer.meshBuffers == null || renderer.meshes == null)
return;
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
const program = renderContext.shaderPrograms.stencilAAA;
const attributes = program.attributes;
this.vao = renderContext.vertexArrayObjectExt.createVertexArrayOES();
renderContext.vertexArrayObjectExt.bindVertexArrayOES(this.vao);
const vertexPositionsBuffer = renderer.meshBuffers[0].stencilSegments;
const vertexNormalsBuffer = renderer.meshBuffers[0].stencilNormals;
const pathIDsBuffer = renderer.meshBuffers[0].stencilSegmentPathIDs;
gl.useProgram(program.program);
gl.bindBuffer(gl.ARRAY_BUFFER, renderContext.quadPositionsBuffer);
gl.vertexAttribPointer(attributes.aTessCoord, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexPositionsBuffer);
gl.vertexAttribPointer(attributes.aFromPosition, 2, gl.FLOAT, false, FLOAT32_SIZE * 6, 0);
gl.vertexAttribPointer(attributes.aCtrlPosition,
2,
gl.FLOAT,
false,
FLOAT32_SIZE * 6,
FLOAT32_SIZE * 2);
gl.vertexAttribPointer(attributes.aToPosition,
2,
gl.FLOAT,
false,
FLOAT32_SIZE * 6,
FLOAT32_SIZE * 4);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexNormalsBuffer);
gl.vertexAttribPointer(attributes.aFromNormal, 2, gl.FLOAT, false, FLOAT32_SIZE * 6, 0);
gl.vertexAttribPointer(attributes.aCtrlNormal,
2,
gl.FLOAT,
false,
FLOAT32_SIZE * 6,
FLOAT32_SIZE * 2);
gl.vertexAttribPointer(attributes.aToNormal,
2,
gl.FLOAT,
false,
FLOAT32_SIZE * 6,
FLOAT32_SIZE * 4);
gl.bindBuffer(gl.ARRAY_BUFFER, pathIDsBuffer);
gl.vertexAttribPointer(attributes.aPathID, 1, gl.UNSIGNED_SHORT, false, 0, 0);
gl.enableVertexAttribArray(attributes.aTessCoord);
gl.enableVertexAttribArray(attributes.aFromPosition);
gl.enableVertexAttribArray(attributes.aCtrlPosition);
gl.enableVertexAttribArray(attributes.aToPosition);
gl.enableVertexAttribArray(attributes.aFromNormal);
gl.enableVertexAttribArray(attributes.aCtrlNormal);
gl.enableVertexAttribArray(attributes.aToNormal);
gl.enableVertexAttribArray(attributes.aPathID);
renderContext.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aFromPosition, 1);
renderContext.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aCtrlPosition, 1);
renderContext.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aToPosition, 1);
renderContext.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aFromNormal, 1);
renderContext.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aCtrlNormal, 1);
renderContext.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aToNormal, 1);
renderContext.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aPathID, 1);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, renderContext.quadElementsBuffer);
renderContext.vertexArrayObjectExt.bindVertexArrayOES(null);
}
private setBlendModeForAA(renderer: Renderer): void {
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
gl.blendEquation(gl.FUNC_ADD);
gl.blendFunc(gl.ONE, gl.ONE);
gl.enable(gl.BLEND);
}
}
/// Switches between mesh-based and stencil-based analytic antialiasing depending on whether stem
/// darkening is enabled.
///
/// FIXME(pcwalton): Share textures and FBOs between the two strategies.
export class AdaptiveStencilMeshAAAStrategy extends AntialiasingStrategy {
private meshStrategy: MCAAStrategy;
private stencilStrategy: StencilAAAStrategy;
get directRenderingMode(): DirectRenderingMode {
return 'none';
}
get passCount(): number {
return 1;
}
constructor(level: number, subpixelAA: SubpixelAAType) {
super(subpixelAA);
this.meshStrategy = new MCAAStrategy(level, subpixelAA);
this.stencilStrategy = new StencilAAAStrategy(level, subpixelAA);
}
init(renderer: Renderer): void {
this.meshStrategy.init(renderer);
this.stencilStrategy.init(renderer);
}
attachMeshes(renderer: Renderer): void {
this.meshStrategy.attachMeshes(renderer);
this.stencilStrategy.attachMeshes(renderer);
}
setFramebufferSize(renderer: Renderer): void {
this.meshStrategy.setFramebufferSize(renderer);
this.stencilStrategy.setFramebufferSize(renderer);
}
get transform(): glmatrix.mat4 {
return this.meshStrategy.transform;
}
prepareForRendering(renderer: Renderer): void {
this.getAppropriateStrategy(renderer).prepareForRendering(renderer);
}
prepareForDirectRendering(renderer: Renderer): void {
this.getAppropriateStrategy(renderer).prepareForDirectRendering(renderer);
}
finishAntialiasingObject(renderer: Renderer, objectIndex: number): void {
this.getAppropriateStrategy(renderer).finishAntialiasingObject(renderer, objectIndex);
}
prepareToRenderObject(renderer: Renderer, objectIndex: number): void {
this.getAppropriateStrategy(renderer).prepareToRenderObject(renderer, objectIndex);
}
finishDirectlyRenderingObject(renderer: Renderer, objectIndex: number): void {
this.getAppropriateStrategy(renderer).finishDirectlyRenderingObject(renderer, objectIndex);
}
antialiasObject(renderer: Renderer, objectIndex: number): void {
this.getAppropriateStrategy(renderer).antialiasObject(renderer, objectIndex);
}
resolveAAForObject(renderer: Renderer, objectIndex: number): void {
this.getAppropriateStrategy(renderer).resolveAAForObject(renderer, objectIndex);
}
resolve(pass: number, renderer: Renderer): void {
this.getAppropriateStrategy(renderer).resolve(pass, renderer);
}
worldTransformForPass(renderer: Renderer, pass: number): glmatrix.mat4 {
return glmatrix.mat4.create();
}
private getAppropriateStrategy(renderer: Renderer): AntialiasingStrategy {
return renderer.needsStencil ? this.stencilStrategy : this.meshStrategy;
}
}
function calculateStartFromIndexRanges(pathRange: Range, indexRanges: Range[]): number {
return indexRanges.length === 0 ? 0 : indexRanges[pathRange.start - 1].start;
}
function calculateCountFromIndexRanges(pathRange: Range, indexRanges: Range[]): number {
if (indexRanges.length === 0)
return 0;
let lastIndex;
if (pathRange.end - 1 >= indexRanges.length)
lastIndex = unwrapUndef(_.last(indexRanges)).end;
else
lastIndex = indexRanges[pathRange.end - 1].start;
const firstIndex = indexRanges[pathRange.start - 1].start;
return lastIndex - firstIndex;
}

View File

@ -1,26 +0,0 @@
{
"compilerOptions": {
"target": "ES2017",
"module": "commonjs",
"types": [
"base64-js",
"gl-matrix",
"lodash",
"node",
"opentype.js",
"webgl-ext"
],
"typeRoots": [
"node_modules/@types"
],
"sourceMap": true,
"strict": true,
"allowUnreachableCode": true
},
"include": [
"src"
],
"exclude": [
"node_modules"
]
}

View File

@ -1,28 +0,0 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {
"quotemark": false
},
"rules": {
"quotemark": false,
"interface-name": false,
"curly": false,
"member-access": false,
"max-classes-per-file": false,
"arrow-parens": false,
"one-variable-per-declaration": false,
"object-literal-shorthand": false,
"no-empty": false,
"variable-name": false,
"no-bitwise": false,
"new-parens": false,
"no-conditional-assignment": false,
"no-shadowed-variable": false,
"no-var-requires": false,
"max-line-length": [true, 100]
},
"rulesDirectory": []
}

View File

@ -1,64 +0,0 @@
const Handlebars = require('handlebars');
const HandlebarsPlugin = require('handlebars-webpack-plugin');
const RustdocPlugin = require('rustdoc-webpack-plugin');
const fs = require('fs');
const path = require('path');
const cwd = fs.realpathSync(".");
module.exports = {
devtool: 'inline-source-map',
entry: {
'3d-demo': "./src/3d-demo.ts",
'svg-demo': "./src/svg-demo.ts",
'text-demo': "./src/text-demo.ts",
'reference-test': "./src/reference-test.ts",
'benchmark': "./src/benchmark.ts",
'mesh-debugger': "./src/mesh-debugger.ts",
},
module: {
rules: [
{
test: /src(\/|\\)[a-zA-Z0-9_-]+\.tsx?$/,
enforce: 'pre',
loader: 'tslint-loader',
exclude: /node_modules/,
options: {
configFile: "tslint.json",
},
},
{
test: /src(\/|\\)[a-zA-Z0-9_-]+\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
]
},
resolve: {
extensions: [".tsx", ".ts", ".html", ".js"],
},
output: {
filename: "[name].js",
path: __dirname,
},
plugins: [
new HandlebarsPlugin({
entry: "html/*.hbs",
output: "./[name]",
partials: ["html/partials/*.hbs"],
helpers: {
octicon: function(iconName) {
const svg = fs.readFileSync(`node_modules/octicons/build/svg/${iconName}.svg`);
return new Handlebars.SafeString(svg);
}
},
}),
new RustdocPlugin({
directories: [fs.realpathSync("../..")],
flags: {
'html-in-header': path.join(cwd, "doc-header.html"),
'html-before-content': path.join(cwd, "doc-before-content.html"),
},
}),
]
}