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