diff options
Diffstat (limited to 'graphics/figput/javascript/main.js')
-rw-r--r-- | graphics/figput/javascript/main.js | 833 |
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); |