summaryrefslogtreecommitdiff
path: root/graphics/figput/javascript/main.js
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2022-07-23 03:01:02 +0000
committerNorbert Preining <norbert@preining.info>2022-07-23 03:01:02 +0000
commitfcd380d0a9f42e14bfc55d8ea92e93fe0b7b48b9 (patch)
tree7db30c9583c625a81c7aabaecdbc163379b8a39a /graphics/figput/javascript/main.js
parent72eecdf45fcf3f421668c6bdfbfe17141ce3edc1 (diff)
CTAN sync 202207230301
Diffstat (limited to 'graphics/figput/javascript/main.js')
-rw-r--r--graphics/figput/javascript/main.js833
1 files changed, 833 insertions, 0 deletions
diff --git a/graphics/figput/javascript/main.js b/graphics/figput/javascript/main.js
new file mode 100644
index 0000000000..afc7b03990
--- /dev/null
+++ b/graphics/figput/javascript/main.js
@@ -0,0 +1,833 @@
+"use strict";
+/*
+
+Main entry point for all the js code behind the browser interface.
+
+This has the initialization code, to load the pdf and the data specifying
+how figures are drawn and laid out, plus the top-level stuff to direct
+events to the proper place for handling.
+
+The same code is used for developing the document, and for serving it
+from a public-facing website, with a few changes. Look for "PUBLIC FACING"
+annotations. Note also that it would be possible to strip considerably
+more code away for the public-facing version, but there's no pressing
+reason to do it -- the savings would be tiny -- and it could lead to
+an error of some kind due to unforeseen dependencies on the deleted
+code.
+
+*/
+// In rare cases, I need to know the browser. Note it first, before doing
+// anything else. This userAgent is a longish bit of blather specifying
+// the browser, and the easiest way to deal with it is to check whether
+// certain strings appear.
+var theBrowser = function () {
+ let blah = navigator.userAgent;
+ if (blah.indexOf("Firefox") > -1)
+ return "Firefox";
+ // MS Edge is also "Chrome," at least for my purposes.
+ if (blah.indexOf("Chrome") > -1)
+ return "Chrome";
+ // Assume "Chrome" as the default.
+ console.log("MAKING DEFAULT 'CHROME' ASSUMPTION ABOUT THE BROWSWER!!!");
+ return "Chrome";
+}();
+// JS has poor to non-existent facilities for dealing with thread scheduling.
+// In a lot of JS code that doesn't matter, but the pdf.js library, on which
+// this entire thing rests, makes heavy use of Promises and workers. Fortunately,
+// there's a simple way of managing this.
+//
+// To use this, the caller says
+// await sleep(100);
+// and the "await" is crucial. This works because setTimeout() takes a
+// function to run, and some amount of time to wait before running that
+// function.
+//
+// This isn't a satisfying solution because it requires "await."
+// For that reason it can only be used in async functions.
+function sleep(ms) {
+ // ms is milliseconds, not microseconds.
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+function getAugmentedFunction(fcnName) {
+ // This converts from a function name, as specified by the user in their latex
+ // document to the internally more useful AugmentedDrawingFunction. Doing this
+ // kind of conversion feels icky, but any alternative I've come up with requires
+ // that the person using FigPut know more than the absolute minimum about what's
+ // behind the curtain.
+ //
+ // References to these functions come into the program at two points. First, when
+ // the document is loaded, a list of the relevant drawing functions is provided
+ // by latex. Second, when the user creates widgets within his drawings, these
+ // widgets must belong to a particular drawing and the function (or its name, as
+ // a string) is used as the key under which the widget is filed.
+ // BUG: This double-cast and use of 'any' is horrible, but is there
+ // anything better?
+ return window[fcnName];
+}
+// Data related to the document pages and figures. Each page
+// has one of these in PDFDocument.pageSpecs. This is all
+// static data once the document has been loaded.
+// BUG: Should this be a type? An interface?
+class PageData {
+ constructor() {
+ // Note that pageHeight is the height of a *printed* page. If the page
+ // has a figure whose height is different than the deleted height, then
+ // the page height, as rendered, will be different than this value.
+ // Also, the margins and textWidth are only valid when there is a
+ // figure to render since they come from figures.aux.
+ // For reference, 8.5 x 11 inch paper is 612 x 792 pts;
+ // A4 paper is 8.27 x 11.69 inch or 595.44 x 841.68 pts.
+ // However, it seems that latex sometimes produces slightly different
+ // dimensions of what must be A4 paper.
+ this.pageHeight = 0;
+ this.pageWidth = 0;
+ this.leftMargin = 0;
+ this.rightMargin = 0;
+ this.textWidth = 0;
+ // These arrays have one entry for each interactive figure on the page.
+ // insertPoint is the position, from the top, at which the figure is
+ // inserted. deleteHeight is how much of the latex doc is omitted, and
+ // above/belowHeight is how much room to leave in the browser window for
+ // the figure. name is the name of the function to call to draw the
+ // figure, done is whether to generate new tikz
+ this.insertPoint = [];
+ this.deleteHeight = [];
+ this.aboveHeight = [];
+ this.belowHeight = [];
+ this.name = [];
+ this.done = [];
+ // For each figure, there is a corresponding function (that will be
+ // augmented with a drawing target). These values make the name values
+ // given above conceptually redundant.
+ this.drawFcn = [];
+ }
+}
+// Everything related to the pdf document is here. Its main purpose is
+// to handle off-screen rendering. It's all static since there can only be
+// one pdf open at a time. It does include data about how to lay out the
+// figures, but none of the code that does it.
+//
+// I tried making this a module, but really no advantage over a class,
+// and definitely some disadvantages.
+class PDFDocument {
+ static async setPDF(thePDF) {
+ // Take the newly loaded pdf and note some of it's stats.
+ // Once this is loaded, thePDF never needs to be accessed again
+ // from outside this class.
+ this.pdf = thePDF;
+ this.numberOfPages = thePDF.numPages;
+ // It seems that pdfjs counts pages from one, not zero.
+ for (let i = 1; i <= this.numberOfPages; i++) {
+ await thePDF.getPage(i).then(function (page) {
+ // Note the i-1 here since I want arrays to start at zero.
+ PDFDocument.pageSpecs[i - 1] = new PageData();
+ PDFDocument.pageSpecs[i - 1].pageWidth = page.view[2];
+ PDFDocument.pageSpecs[i - 1].pageHeight = page.view[3];
+ // Make sure that the shared buffer arrays are all fully allocated.
+ PDFDocument.theCanvases[i - 1] = null;
+ PDFDocument.pageAge[i - 1] = 0;
+ });
+ }
+ }
+ static isLoaded() {
+ if (this.pdf === null)
+ return false;
+ else
+ return true;
+ }
+ static getZoom() {
+ return this.zoom;
+ }
+ static setZoom(z) {
+ this.zoom = z;
+ this.flushBuffer();
+ }
+ static getCanvas(n) {
+ // Return the canvas for page number n (counting from zero).
+ // This assumes that the canvas is available due to a previous
+ // call to render(n) -- which must have resolved by the time this
+ // method is called.
+ // BUG: This shouldn't happen, but maybe I should create a fake
+ // blank canvas anyway.
+ if (this.theCanvases[n] === null)
+ console.log("ERROR! Canvas for page missing: " + n);
+ return this.theCanvases[n];
+ }
+ static async render(n) {
+ // Render page n offscreen, where the pages are counted from zero.
+ // See if the page is already there.
+ if (PDFDocument.theCanvases[n] !== null)
+ return;
+ // Note the +1 here since pdf.js counts pages from 1, not zero.
+ let thePage = await PDFDocument.pdf.getPage(n + 1);
+ let newCanvas = document.createElement('canvas');
+ PDFDocument.theCanvases[n] = newCanvas;
+ PDFDocument.pageAge[n] = PDFDocument.renderCount;
+ ++PDFDocument.renderCount;
+ let offctx = newCanvas.getContext('2d');
+ let viewport = thePage.getViewport(PDFDocument.zoom);
+ newCanvas.width = viewport.width;
+ newCanvas.height = viewport.height;
+ await thePage.render({
+ canvasContext: offctx,
+ viewport: viewport
+ });
+ }
+ static trimBuffer() {
+ // Call this periodically, like after making a series of calls to
+ // render(), to remove excess canvases from the buffer arrays.
+ // Due to the risk of race conditions -- which I think is minimal --
+ // it's best not to call this until reaching a point at which it
+ // doesn't matter if *all* the offscreen canvases are deleted (although
+ // that outcome would be inefficient). The unlikely possiblitity of
+ // problems due to race conditions is why this is not part of render().
+ let ctot = 0;
+ for (let i = 0; i < PDFDocument.numberOfPages; i++) {
+ if (PDFDocument.theCanvases[i] !== null)
+ ++ctot;
+ }
+ // Delete excess offscreen canvases.
+ while (ctot > PDFDocument.pageBufSize) {
+ PDFDocument.removeOldestPage();
+ --ctot;
+ }
+ }
+ static removeOldestPage() {
+ // Remove the oldest single canvas from this.theCanvases.
+ let oldestIndex = -1;
+ let oldestAge = this.pageAge[0];
+ for (let i = 0; i < this.numberOfPages; i++) {
+ if (this.theCanvases[i] === null)
+ continue;
+ if (this.pageAge[i] < oldestAge) {
+ oldestAge = this.pageAge[i];
+ oldestIndex = i;
+ }
+ }
+ this.theCanvases[oldestIndex] = null;
+ }
+ static flushBuffer() {
+ // Removes all offscreen canvases in the buffer. This is needed, e.g.,
+ // when resizing so that you don't use a canvas of the wrong scale.
+ for (let i = 0; i < this.numberOfPages; i++)
+ this.theCanvases[i] = null;
+ }
+}
+// Total number of pages in the above document.
+PDFDocument.numberOfPages = 0;
+// This is the scale at which to to render the pages offscreen. Setting
+// this equal to 1 means that each px of the off-screen canvas is 1 pdf
+// point.
+PDFDocument.zoom = 1;
+// Information about the layout, one for each page.
+PDFDocument.pageSpecs = [];
+// The fields below are a temporary buffer of pages rendered offscreen.
+// This speeds up the process so that we aren't re-rendering pages.
+// My original intent was for these arrays to have no more than
+// pageBufSize entries, but the way async/promise works makes that
+// difficult (impossible?). I want render() to render a given page
+// to an offscreen canvas and store that canvas here, which means
+// that all these calls to render() share the variables below.
+// Each call to render() is, unavoidably, in a different thread, and
+// that means that they are all trying to manipulate these varaibles
+// in an order that is unpredictable. The only way that I can see to
+// make this happen in a predicatable way is for each page to have
+// its own canvas. It's OK for render() to put things into its designated
+// spot in an array, but it can't touch anything that some other invocation
+// of render might touch.
+//
+// This is wasteful, but I see no other way to do it without using
+// a mutex. It's not that bad since we're talking about a few bytes for
+// every page, but still annoying.
+//
+// Aside: JS does have the Atomics object, as of ECMA 2017, and it
+// might (?) be possible to use that somehow as a way to synchronize
+// threads, but I'd rather not mess with it.
+// The total number of pages that may be held. This needs to be at least
+// as large as the number of pages that may be visible at one time if you
+// zoom all the way out, plus a couple extra due to possible problems
+// resulting from race conditions that I can't (?) avoid.
+PDFDocument.pageBufSize = 6;
+// This holds a copy of page to be copied to the screen. Every page has
+// its own entry, although all but (at most) pageBufSize will be null.
+PDFDocument.theCanvases = [];
+// The renderCount is how many times we've rendered any page. It's used like
+// a time since I have no confidence in window.performance.now, which is
+// supposed to return the current time in nanoseconds. Every time a page
+// is asked to be rendered, this value increments, and is stored in
+// pageAge[i], where i is the index of the page rendered. It may not
+// actually be rendered if it would be a re-render, but the time is updated.
+// We need this to flush older pages from the buffer when there are too many.
+//
+// NOTE: This variable is shared by the various invocations of render()
+// and something like
+// ++renderCount
+// is not an atomic operation. The way I am using this, the fact that
+// the value of renderCount may be incorrect isn't a disaster. It can
+// only be wrong by a few steps. This is why pageBufSize is bumped up to be a
+// little larger than strictly necessary. Worst case, things are re-rendered
+// and you waste some CPU.
+PDFDocument.renderCount = 0;
+// One of these for each page, indexed by page number. Holds the value
+// or renderCount at the time when the page was rendered (or re-rendered).
+PDFDocument.pageAge = [];
+// A class for scheduling event handling.
+//
+// Everything is static because there is only one of these for all events.
+// It is used to mediate the event loop so that everything happens
+// syncrhonously, even when async functions are involved. It is used for
+// things like scroll and mouse-down events; it is also used for animations,
+// which are created by certain widgets.
+//
+// The idea is that you call getID() to get your ticket (like at the
+// butcher's). Then call await waitMyTurn() until the butcher is ready.
+// The 'await' is crucial! Then call allDone() to thank the butcher and
+// leave so that he can take the next customer.
+//
+// For this to work properly, calls to getID() must be made outside
+// of async functions. Otherwise the IDs could get out of order.
+//
+// BUG: I could use this in a DRY-er way. Define a method called
+// getInLine() that takes the function to be invoked. Have getInLine()
+// call getID(), waitMyTurn(), run the job, then call allDone().
+//
+// BUG: It may be that someone who is more expert in JS could use
+// Promises somehow to make this scheme unnecessary. OTOH, this may
+// be easier to understand and use than something more clever.
+class Events {
+ static getID() {
+ let answer = this.count;
+ this.count++;
+ return answer;
+ }
+ static async waitMyTurn(id) {
+ while (id !== Events.complete + 1)
+ await sleep(5);
+ }
+ static allDone(id) {
+ Events.complete = id;
+ }
+}
+// A count of events that have come in. Each event gets its own unique
+// ID by calling getID().
+Events.count = 0;
+// The ID number of the event whose handling was most recently completed.
+Events.complete = -1;
+async function doOpenDocument(latexName, scrollPos) {
+ // Called when the body of the page loads (onload).
+ // latexName is the base of the name of the pdf. Thus, latexName.pdf
+ // is what is to be opened. The scrollPos value is the position on the
+ // page. This is provided by doBeforeUnload(), noted by the server, and
+ // given back in the course of the reload.
+ // The "Get TikZ" button is fixed, and it is always on top.
+ // PUBLIC FACING: Comment this block of code out when running serving from a
+ // public-facing website. There's no reason to provide this "Get TikZ" button
+ // and the server won't now how to handle the resulting messages.
+ let fmenu = document.createElement("button");
+ fmenu.style.position = "fixed";
+ fmenu.style.display = "block";
+ fmenu.style.left = "0px";
+ fmenu.style.top = "0px";
+ fmenu.style.width = "80px";
+ fmenu.style.height = "18px";
+ fmenu.style.fontSize = "10px";
+ fmenu.textContent = "Get TikZ";
+ fmenu.style.zIndex = "99";
+ fmenu.onclick = doTikzClick;
+ document.body.appendChild(fmenu);
+ let theCanvas = document.getElementById("pdf_renderer");
+ theCanvas.style.position = "fixed";
+ // It might happen that the program starts with some level of zoom.
+ PDFDocument.setZoom(window.devicePixelRatio);
+ // So that the canvas exactly fills the window.
+ adjustCanvas();
+ // Mouse events are registered on the canvas rather than the document
+ // (as is done for resize and scroll), but I'm not sure there's any
+ // practical difference since the canvas is the whole document.
+ // Note that this does *not* listen for onclick events. In a few cases,
+ // they might be a more natural choice (like for buttons), but they make
+ // things like coloring a "halfway clicked" widget difficult.
+ // Note that this does not distinguish between left, right and middle
+ // buttons; a click is a click.
+ // Also, the mouseup listener is registered on the window, not the canvas,
+ // so that we hear about mouse-ups even when they happen outside the
+ // browswer window entirely.
+ theCanvas.addEventListener("mousedown", doMouseDown);
+ theCanvas.addEventListener("mousemove", doMouseMove);
+ theCanvas.addEventListener("mouseup", doMouseUp);
+ // I considered registering listeners for mouseenter/mouseleave, but
+ // they're not needed.
+ /*
+ BUG: I have not tested this much on touch devices. Everything
+ works fine on some phones, but not on others. It's a hard to imagine
+ people trying to use this on a phone, but I should test on things
+ like iPads.
+ theCanvas.addEventListener("touchstart",doTouchDown);
+ theCanvas.addEventListener("touchend",doTouchUp);
+ theCanvas.addEventListener("touchmove",doTouchMove);
+ */
+ // Open the pdf and digest figures.aux.
+ await getDocumentData(latexName);
+ // Create the various Panel objects that make up the document as a whole.
+ // This does the "page layout."
+ FullPanel.init(PDFDocument.pageSpecs);
+ // Now that page layout is done, set the range for the scroll bars on
+ // the browswer window.
+ adjustScrollBars();
+ // When reloading, we don't want things to move back to the top of
+ // the first page.
+ window.scrollTo(0, scrollPos);
+ // Render for the very first time.
+ fullRender();
+}
+async function getDocumentData(latexName) {
+ // This opens and digests the data files and the pdf itself.
+ let thePDF = await pdfjsLib.getDocument(latexName + '.pdf');
+ await PDFDocument.setPDF(thePDF);
+ // Open and digest figures.aux.
+ let fname = latexName + ".fig.aux";
+ // fetch() is the new XMLHttpRequest(), which has been deprecated.
+ // Thanks to Dan Pratt for pointing that out.
+ let fetchPromise = await fetch(fname);
+ let allText = await fetchPromise.text();
+ await getFigureInfo(allText);
+}
+async function syncLoad(scriptName) {
+ // Load the script with the given name and append to the DOM, and
+ // don't return until the load is complete. So this should typically
+ // be called with 'await'.
+ //
+ // Thanks to Dan Pratt for this tidy solution. This works because the
+ // promise can't resolve (either way) until appendChild() completes
+ // because you can't resolve or reject until the code is loaded
+ // or fails to load. Note that the 'once' argument means that the function
+ // is called once, then flushed. That's exactly what I want so that they
+ // don't hang around and consume resources.
+ let theCode = document.createElement("script");
+ theCode.type = "application/javascript";
+ theCode.src = scriptName;
+ var p = new Promise((resolve, reject) => {
+ theCode.addEventListener("load", resolve, { once: true });
+ theCode.addEventListener("error", reject, { once: true });
+ });
+ document.body.appendChild(theCode);
+ await p;
+}
+async function getFigureInfo(textBody) {
+ // textBody is the complete contents of the .aux file. Parse it and
+ // use the data to fill in PDFDocument.PageSpecs.
+ let lines = textBody.split('\n');
+ for (let i = 0; i < lines.length; i++) {
+ if (lines[i].length < 2)
+ // Last line might be blank.
+ break;
+ var parts = lines[i].split(' ');
+ if (parts[0] === "load") {
+ // Load a single .js file and move on.
+ await syncLoad(parts[1]);
+ continue;
+ }
+ var pnum = parseInt(parts[0]);
+ var innerMargin = parseFloat(parts[1]);
+ var outerMargin = parseFloat(parts[2]);
+ var textWidth = parseFloat(parts[3]);
+ var vpos = parseFloat(parts[4]);
+ var hLatex = parseFloat(parts[5]);
+ var hAbove = parseFloat(parts[6]);
+ var hBelow = parseFloat(parts[7]);
+ var name = parts[8];
+ // Careful: JS weirdness means that Boolean(parts[9]) is ALWAYS true
+ // (unless parts[9] is empty).
+ var done = (parts[9] === 'true');
+ var externLoad = (parts[10] === 'true');
+ // Page numbers should start at zero (not 1 as reported by latex).
+ pnum -= 1;
+ // Load the JS now, if necessary.
+ // NOTE: In earlier versions, up to v23, I looked to either a
+ // .js file or an .fjs file. Individual files are now assumed
+ // to be .fjs files. This is much simpler.
+ if (externLoad === true)
+ await syncLoad(name + ".fjs");
+ // NOTE: This assumes that the relevant function has been loaded,
+ // perhaps as an external file from \LoadFigureCode.
+ let augFcn = getAugmentedFunction(name);
+ if (typeof augFcn === "undefined")
+ alert(name + " not found. Is the function name correct?");
+ // Note this for the future.
+ augFcn.figurePanelClass = null;
+ PDFDocument.pageSpecs[pnum].drawFcn.push(augFcn);
+ // Copy the remaining fields over.
+ // Adjust the vertical position to be given relative to the top
+ // of the page, rather than the bottom.
+ vpos = PDFDocument.pageSpecs[pnum].pageHeight - vpos;
+ // If there are multiple figures on a particular page, then this
+ // is redundant, but harmless.
+ if (pnum % 2 === 0) {
+ PDFDocument.pageSpecs[pnum].leftMargin = innerMargin;
+ PDFDocument.pageSpecs[pnum].rightMargin = outerMargin;
+ }
+ else {
+ PDFDocument.pageSpecs[pnum].leftMargin = outerMargin;
+ PDFDocument.pageSpecs[pnum].rightMargin = innerMargin;
+ }
+ PDFDocument.pageSpecs[pnum].textWidth = textWidth;
+ // The per-figure data.
+ PDFDocument.pageSpecs[pnum].insertPoint.push(vpos);
+ PDFDocument.pageSpecs[pnum].deleteHeight.push(hLatex);
+ PDFDocument.pageSpecs[pnum].aboveHeight.push(hAbove);
+ PDFDocument.pageSpecs[pnum].belowHeight.push(hBelow);
+ PDFDocument.pageSpecs[pnum].name.push(name);
+ PDFDocument.pageSpecs[pnum].done.push(done);
+ }
+ // Make sure that all the figure names are distinct.
+ for (let i = 0; i < PDFDocument.numberOfPages; i++) {
+ // Loop over each figure on the current page.
+ for (let fig = 0; fig < PDFDocument.pageSpecs[i].name.length; fig++) {
+ let curName = PDFDocument.pageSpecs[i].name[fig];
+ // See if this name matches a name on any other page/figure.
+ // Consider the current page first.
+ for (let subfig = fig + 1; subfig < PDFDocument.pageSpecs[i].name.length; subfig++) {
+ if (curName === PDFDocument.pageSpecs[i].name[subfig]) {
+ alert("The figure name " + curName + " is used more than once.");
+ throw new Error();
+ }
+ }
+ // Continue with all the remaining pages.
+ for (let j = i + 1; j < PDFDocument.numberOfPages; j++) {
+ for (let subfig = 0; subfig < PDFDocument.pageSpecs[j].name.length; subfig++) {
+ if (curName === PDFDocument.pageSpecs[j].name[subfig]) {
+ alert("The figure name " + curName + " is used more than once.");
+ throw new Error();
+ }
+ }
+ }
+ }
+ }
+}
+function adjustScrollBars() {
+ // Based on the page size and document length, adjust the range of the
+ // browswer's scroll bars.
+ var body = document.getElementById("mainbody");
+ // I don't see any way to set the range of the scroll bar directly, so
+ // I'm fooling the browser to think that it has a body of a certain
+ // height, in px, when the body is really no bigger than the visible
+ // area of the window. What I want is a height such that, when the scroll
+ // bar is at the bottom, the lower edge of the last page is barely visible
+ // in the bottom of the window.
+ //
+ // There was some mental debate about whether it's better to let the
+ // bottom of the last page scroll up beyond the bottom of the window, but
+ // this is easier to program, and it's probably more natural for most people.
+ let totHeight = FullPanel.totalHeight();
+ let visHeight = document.documentElement.clientHeight;
+ body.style.height = totHeight + "px";
+ // The horizontal scroll bar is expressed in terms of pdf pts. This is
+ // simpler than above since I don't want to be able to scroll so that the
+ // right edge of the document is barely off the window; I want to be able
+ // to scroll just far enough so that the right edge of the document meets
+ // the right edge of the window. The size of the window becomes irrelevant
+ // (i.e., the browser deals with it).
+ // However, we do need to know whether a scroll bar is needed at all,
+ // so we can't totally ignore the visible width.
+ let visWidth = document.documentElement.clientWidth;
+ let totWidth = FullPanel.getFullWidth();
+ if (visWidth > totWidth) {
+ // No need for horizontal scroll. It seems like "0px" works, but "0 px"
+ // does not. Maybe the space causes a (silent) parse error?
+ body.style.width = "0px";
+ return;
+ }
+ body.style.width = totWidth + "px";
+}
+function ctxTopLevelAdjust() {
+ // Each function should call this before doing any drawing.
+ // Rendering is done relative to the ctx (obviously) with the t-matrix
+ // adjusted accordingly. This brings the t-matrix from being the
+ // identity to being relative to the entire document.
+ //
+ // In earlier versions, the ctx was being passed around and adjusted
+ // as the layout manager descended to a particular portion of the document.
+ // In some ways that is the cleaner and more modular way to do things,
+ // but it can be confusing because the adjustments to the t-matrix aren't
+ // done in a central place.
+ var canvas = document.getElementById("pdf_renderer");
+ let ctx = canvas.getContext('2d');
+ ctx.resetTransform();
+ // Center the document. Rather than mess with CSS to center the canvas,
+ // leave the canvas at the size of the entire window, and shift the origin
+ // so the document is rendered in the center of the canvas.
+ //
+ // However, if the window is smaller than the document, then we do
+ // NOT want to center the document. If we did center it, then it would
+ // be impossible to scroll over to the left since we can't have "negative
+ // scroll;" window.scrollX is always non-negative (unfortunately).
+ let visWidth = document.documentElement.clientWidth;
+ let totWidth = FullPanel.getFullWidth();
+ let z = PDFDocument.getZoom();
+ if (visWidth > totWidth) {
+ // No horizontal scroll bar. Center it.
+ let canvasCenter = canvas.width / 2;
+ let docCenter = FullPanel.getFullWidth() / 2;
+ docCenter *= PDFDocument.getZoom();
+ ctx.translate(canvasCenter - docCenter, 0);
+ }
+ else {
+ // Shift according to the horizontal scroll bar, leaving the document
+ // against the left edge. Note the scaling by the zoom factor so
+ // as to be consistent with the above.
+ ctx.translate(-window.scrollX * PDFDocument.getZoom(), 0);
+ }
+ return ctx;
+}
+async function fullRender() {
+ // Render all the pages of the pdf, together with the figures.
+ var canvas = document.getElementById("pdf_renderer");
+ // This shouldn't be necessary, but do it as a fail-safe.
+ var ctx = canvas.getContext('2d');
+ ctx.resetTransform();
+ let visWidth = document.documentElement.clientWidth;
+ let z = PDFDocument.getZoom();
+ // BUG: Could lead to flicker? It is necessary to clear everything
+ // because widgets could be drawn outside the page. I suppose that I could
+ // only erase the area outside the pages, which would reduce any flicker.
+ // I really don't want to double-buffer everything if it can be avoided.
+ // It may be the only solution. The problem is that, if a widget extends
+ // outside the page, and is animated (like LoopAnimWidget) so that it's
+ // drawn as part of an animation, then you must erase the entire page to
+ // avoid "leftovers," and *that* requires that you redraw the entire
+ // document (or what is visible) with every frame of an animation.
+ //
+ // BUG: So the correct solution is double-buffering everything, but I don't
+ // feel that it's pressing. For now, either restrict any widgets to
+ // draw within a page or accept that it might leave spoogey leftovers.
+ ctx.clearRect(0, 0, visWidth * z, document.documentElement.clientHeight * z);
+ await FullPanel.renderAll(canvas.height);
+}
+function adjustCanvas() {
+ // Call when the window has been zoomed or re-sized.
+ //
+ // canvas has two different dimensions: canvas.style.width and height, and
+ // canvas.width and height. The style values determine the size of the canvas
+ // on the screen -- so-called ""CSS styling." The straight values (no style)
+ // are the number of px in the canvas. By keeping the style values constant and
+ // varying the straight values, you can have more or less resolution for the
+ // canvas, EVEN WHEN ZOOMED IN.
+ //
+ // The bottom line is that if we adjust the straight pixel sizes, then
+ // we can always have one px for every physical pixel -- up to the accuracy
+ // of window.devicePixelRatio.
+ //
+ // BUG: Check for things with super high pixel density, like phones. I
+ // think the devicePixelRatio is somehow wrong for those. It may not matter.
+ var canvas = document.getElementById("pdf_renderer");
+ // Adjust the visible width to match the window size.
+ //
+ // NOTE: For debugging, it can be helpful to subtract off a bit from these
+ // values, like 1, 10 or 50, depending. In earlier versions, there was
+ // also a bit of CSS in the html that put a visible rectangle around the canvas.
+ //canvas.style.width = (document.documentElement.clientWidth - 50) + "px";
+ //canvas.style.height = (document.documentElement.clientHeight - 50) + "px";
+ canvas.style.width = document.documentElement.clientWidth + "px";
+ canvas.style.height = document.documentElement.clientHeight + "px";
+ // The visible width of the canvas has just been fixed. Now change the
+ // number of px so that the number of px per inch on the screen varies with
+ // the level of browser zoom. There seems to be no means of direct access
+ // to the phyical pixels of the monitor, but this is close, and may be
+ // exactly equivalent in many situations.
+ canvas.width = document.documentElement.clientWidth * window.devicePixelRatio;
+ canvas.height = document.documentElement.clientHeight * window.devicePixelRatio;
+}
+async function doScrollGuts(id) {
+ // Wait for any active event handler to finish and then wait until it is
+ // the turn of current id to run.
+ await Events.waitMyTurn(id);
+ await fullRender();
+ // Give your ticket to the butcher so that he can take the next customer.
+ Events.allDone(id);
+}
+function doScroll() {
+ // Scroll events are registered below to call this function.
+ // It redraws the contents of the main canvas.
+ // This appears to be called when the page is opened, before almost
+ // anything else, so return until page initialization is complete.
+ if (PDFDocument.isLoaded() == false)
+ return;
+ // Get a unique ID for this event.
+ // This must be done outside any async function to ensure proper order.
+ let id = Events.getID();
+ doScrollGuts(id);
+}
+async function doResizeGuts(id) {
+ // As with scrolling, wait for any earlier event handler to complete.
+ await Events.waitMyTurn(id);
+ // None of the buffered offscreen canvases are valid anymore.
+ // In theory, I could test for whether this was merely a window
+ // resize and not a zoom, but it's not worth fooling with.
+ PDFDocument.flushBuffer();
+ console.log("ratio: " + window.devicePixelRatio);
+ // The other thing that needs to be adjusted is the zoom level
+ // used when rendering the pdfs.
+ adjustCanvas();
+ // Here is the magic. Effectively, this value is now the level of zoom.
+ // If you take out this line, and leave PDFDocument.zoom always equal to
+ // default value of 1, then zooming does nothing, although the mismatch
+ // between the resolution of the off-screen canvas and the resolution on
+ // the screen can make things blurry or blocky.
+ PDFDocument.setZoom(window.devicePixelRatio);
+ // If the size of the window changed, then this needs to be adjusted too.
+ // This must be done after adjustCanvas().
+ adjustScrollBars();
+ // Render the pages too.
+ await fullRender();
+ // Final bit of scheduling magic.
+ Events.allDone(id);
+}
+function doResize() {
+ // This is called whenever the user zooms and also if the
+ // entire broswer window is resized.
+ let id = Events.getID();
+ doResizeGuts(id);
+}
+async function mouseDownGuts(id, x, y) {
+ // (x,y) is the location of the mouse click, in pdf points, but
+ // relative to the window.
+ await Events.waitMyTurn(id);
+ // Just as we have to tweak the t-matrix to determine where to draw things,
+ // we need to do something similar to figure out where (x,y) is on the page.
+ // The difference is that the ctx.translate() operations become adjustments
+ // to x and y.
+ let visWidth = document.documentElement.clientWidth;
+ let totWidth = FullPanel.getFullWidth();
+ let canvas = document.getElementById("pdf_renderer");
+ if (visWidth > totWidth) {
+ // No horizontal scroll bar. The document is centered.
+ let canvasCenter = canvas.width / 2;
+ let docCenter = FullPanel.getFullWidth() / 2;
+ canvasCenter = canvasCenter / PDFDocument.getZoom();
+ x = x - (canvasCenter - docCenter);
+ }
+ else {
+ // Shift according to the horizontal scroll bar.
+ x = x + window.scrollX;
+ }
+ // Fix up the y-coordinate too.
+ y = y + window.scrollY;
+ FullPanel.mouseDown(x, y);
+ // Don't forget!
+ Events.allDone(id);
+}
+function doMouseDown(e) {
+ // This is an event like any other, so the usual scheduling rigmarole.
+ let id = Events.getID();
+ mouseDownGuts(id, e.x, e.y);
+}
+async function mouseMoveGuts(id, x, y) {
+ // As above.
+ // BUG: Common code. Not DRY.
+ await Events.waitMyTurn(id);
+ // As for mouse-down.
+ let visWidth = document.documentElement.clientWidth;
+ let totWidth = FullPanel.getFullWidth();
+ let canvas = document.getElementById("pdf_renderer");
+ if (visWidth > totWidth) {
+ // No horizontal scroll bar. The document is centered.
+ let canvasCenter = canvas.width / 2;
+ let docCenter = FullPanel.getFullWidth() / 2;
+ canvasCenter = canvasCenter / PDFDocument.getZoom();
+ x = x - (canvasCenter - docCenter);
+ }
+ else {
+ // Shift according to the horizontal scroll bar.
+ x = x + window.scrollX;
+ }
+ // Fix up the y-coordinate too.
+ y = y + window.scrollY;
+ FullPanel.mouseMove(x, y);
+ // Don't forget!
+ Events.allDone(id);
+}
+function doMouseMove(e) {
+ // As above.
+ let id = Events.getID();
+ mouseMoveGuts(id, e.x, e.y);
+}
+async function mouseUpGuts(id, x, y) {
+ // As above.
+ // BUG: Common code. Not DRY.
+ await Events.waitMyTurn(id);
+ let visWidth = document.documentElement.clientWidth;
+ let totWidth = FullPanel.getFullWidth();
+ let canvas = document.getElementById("pdf_renderer");
+ if (visWidth > totWidth) {
+ // No horizontal scroll bar. The document is centered.
+ let canvasCenter = canvas.width / 2;
+ let docCenter = FullPanel.getFullWidth() / 2;
+ canvasCenter = canvasCenter / PDFDocument.getZoom();
+ x = x - (canvasCenter - docCenter);
+ }
+ else {
+ // Shift according to the horizontal scroll bar.
+ x = x + window.scrollX;
+ }
+ // Fix up the y-coordinate too.
+ y = y + window.scrollY;
+ FullPanel.mouseUp(x, y);
+ // Don't forget!
+ Events.allDone(id);
+}
+function doMouseUp(e) {
+ // As above.
+ let id = Events.getID();
+ mouseUpGuts(id, e.x, e.y);
+}
+function doTikzClick() {
+ // Tikz button was clicked. Generate output file(s).
+ //
+ // Cycle over every figure and let it generate tikz data.
+ for (let pi = 0; pi < PDFDocument.pageSpecs.length; pi++) {
+ let pd = PDFDocument.pageSpecs[pi];
+ if (pd.name.length === 0)
+ // No figures on this page.
+ continue;
+ for (let fi = 0; fi < pd.name.length; fi++) {
+ if (pd.done[fi] == true)
+ // User said to skip this one.
+ continue;
+ let theFcn = pd.drawFcn[fi];
+ if (theFcn.figurePanelClass === null)
+ // Hasn't been loaded, because was never visible.
+ continue;
+ let ctx = new CTX(pd.name[fi]);
+ theFcn(ctx);
+ ctx.close();
+ }
+ }
+}
+function doBeforeUnload(e) {
+ // This is *very* non-standard. Pass a made-up WHERE message with the position
+ // on the document.
+ // BUG: This only informs the server of the vertical scroll position.
+ // Dealing with horizontal scroll would be more painful. See fullRender()
+ // and adjustScrollBars().
+ // If *everything* were double-buffered, then this would be a bit easier.
+ let req = new XMLHttpRequest();
+ // We don't care about the second argument. In a POST this would be
+ // something like a file name. The third argument, 'false', forces the request
+ // to be handled synchronously. If it's done async, then the unload proceeds
+ // and completes before this task finishes.
+ req.open("WHERE", "bogus", false);
+ req.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
+ // Send the postion as the message. It could probaby be sent instead of
+ // 'bogus' above, and this could be blank.
+ let msg = window.scrollY.toString();
+ // BUG: Firefox generates "Uncaught DOMException: A network error occurred."
+ // on this. It might be that XMLHttpRequest is deprecated?
+ req.send(msg);
+}
+// Register additional event handlers. The main (and only) canvas had
+// listeners registered for mouse events in doOpenDocument().
+document.addEventListener('scroll', doScroll);
+window.addEventListener('resize', doResize);
+// PUBLIC FACING: Comment this out. It's doing something *very* non-standard,
+// and any normal web-server will choke on it.
+window.addEventListener('beforeunload', doBeforeUnload);