Remove the Pathfinder 2 demo
This commit is contained in:
parent
2a118c3af0
commit
4062f824fd
|
@ -1,2 +0,0 @@
|
|||
node_modules
|
||||
|
|
@ -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;
|
|
@ -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"
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>3D Demo — 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">×</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×SSAA</option>
|
||||
<option value="ssaa-4" selected>4×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>
|
|
@ -1,122 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Benchmark — 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">×</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×SSAA</option>
|
||||
<option value="ssaa-4">4×SSAA</option>
|
||||
<option value="ssaa-8">8×SSAA</option>
|
||||
<option value="ssaa-16">16×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">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="pf-benchmark-results-global">
|
||||
<span class="pf-benchmark-results-label">Partitioning:</span>
|
||||
|
||||
<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…</button>
|
||||
<button type="button" class="btn btn-primary"
|
||||
id="pf-benchmark-results-close-button">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1 +0,0 @@
|
|||
{{>partials/navbar.html isDoc=true}}
|
|
@ -1,2 +0,0 @@
|
|||
{{>partials/header.html}}
|
||||
<link rel="stylesheet" href="/css/pathfinder-doc.css">
|
|
@ -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>
|
|
@ -1,73 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Mesh Debugger — 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">×</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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">×</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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,188 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Reference Test — 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×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">—</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">—</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>
|
|
@ -1,78 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>SVG Demo — 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">×</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…</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×SSAA</option>
|
||||
<option value="ssaa-4">4×SSAA</option>
|
||||
<option value="ssaa-8">8×SSAA</option>
|
||||
<option value="ssaa-16">16×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>
|
|
@ -1,133 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Text Demo — 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">×</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…</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×SSAA</option>
|
||||
<option value="ssaa-4">4×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">×</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>
|
|
@ -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
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
|
@ -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];
|
||||
}
|
|
@ -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();
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -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);
|
||||
}
|
|
@ -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 {}
|
||||
}
|
|
@ -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();
|
|
@ -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];
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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": []
|
||||
}
|
|
@ -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"),
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue