diff options
Diffstat (limited to 'Master/texmf-dist/doc/latex/figput/javascript/development/main.ts')
-rw-r--r-- | Master/texmf-dist/doc/latex/figput/javascript/development/main.ts | 1086 |
1 files changed, 1086 insertions, 0 deletions
diff --git a/Master/texmf-dist/doc/latex/figput/javascript/development/main.ts b/Master/texmf-dist/doc/latex/figput/javascript/development/main.ts new file mode 100644 index 00000000000..ed7c179ed4e --- /dev/null +++ b/Master/texmf-dist/doc/latex/figput/javascript/development/main.ts @@ -0,0 +1,1086 @@ +/* + +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 : number) : Promise<unknown> { + + // ms is milliseconds, not microseconds. + return new Promise(resolve => setTimeout(resolve,ms)); +} + + +// Some semi-bogus stuff to quiet the ts compiler... + +// This makes the external library known to tsc. +declare var pdfjsLib : any; + +// And this provides the properties known to pdfjs that belong to a pdf. +// I had hoped to define a blank type, and have done with it, but we need +// access to certain fields. This provides the type information that +// pdf.js lacks -- or at least the minimal information that I need. +type LoadedPDF = { + numPages : number; + getPage : (n : number) => Promise<any>; +}; + +// This is less bogus. It's the type of the functions called to draw the figures. +// These functions have an added property that points to the +// correct FigurePanel widget. See the layout code. +// Declaring these types serves to warn the programmer what's ahead. It also +// provides tsc with the information it needs to prevent certain mistakes. +type BaseDrawingFunction = (ctx : CanvasRenderingContext2D | CTX) => void; +type AugmentedDrawingFunction = BaseDrawingFunction & { figurePanelClass : FigurePanel | null; }; + + +function getAugmentedFunction(fcnName : string ) : AugmentedDrawingFunction { + + // 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 <AugmentedDrawingFunction> <unknown> window[ fcnName as any ]; +} + + +// 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 { + + // 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. + pageHeight : number = 0; + pageWidth : number = 0; + leftMargin : number = 0; + rightMargin : number = 0; + textWidth : number = 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 + insertPoint : number[] = []; + deleteHeight : number[] = []; + aboveHeight : number[] = []; + belowHeight : number[] = []; + name : string[] = []; + done : boolean[] = []; + + // 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. + drawFcn : AugmentedDrawingFunction[] = []; +} + + +// 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 { + + // The document, as returned by pdfjsLib.getDocument. + private static pdf : LoadedPDF; + + // Total number of pages in the above document. + public static 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. + private static zoom : number = 1; + + // Information about the layout, one for each page. + public static pageSpecs : PageData[] = []; + + // 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. + private static pageBufSize : number = 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. + private static theCanvases : (HTMLCanvasElement | null)[] = []; + + // 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. + private static renderCount : number = 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). + private static pageAge : number[] = []; + + + public static async setPDF(thePDF : LoadedPDF) { + + // 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; + }); + } + } + + public static isLoaded() : boolean { + if (this.pdf === null) + return false; + else + return true; + } + + public static getZoom() : number { + return this.zoom; + } + + public static setZoom(z : number) : void { + + this.zoom = z; + this.flushBuffer(); + } + + public static getCanvas(n : number) { + + // 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]; + } + + public static async render(n : number ) { + + // 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 : HTMLCanvasElement = 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 + }); + } + + public static trimBuffer() : void { + + // 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; + } + } + + private static removeOldestPage() : void { + + // 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; + } + + public static flushBuffer() : void { + + // 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; + } +} + + +// 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 { + + // A count of events that have come in. Each event gets its own unique + // ID by calling getID(). + private static count : number = 0; + + // The ID number of the event whose handling was most recently completed. + private static complete : number = -1; + + public static getID() { + let answer = this.count; + this.count++; + return answer; + } + + public static async waitMyTurn(id : number) { + while (id !== Events.complete + 1) + await sleep(5); + } + + public static allDone(id : number) { + Events.complete = id; + } +} + + +async function doOpenDocument(latexName : string , scrollPos : number ) { + + // 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 = <HTMLCanvasElement> 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 : string) { + + // This opens and digests the data files and the pdf itself. + let thePDF : LoadedPDF = 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 : string) { + + // 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 : string) { + + // 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 : string = parts[8]; + + // Careful: JS weirdness means that Boolean(parts[9]) is ALWAYS true + // (unless parts[9] is empty). + var done : boolean = (parts[9] === 'true'); + var externLoad : boolean = (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 : AugmentedDrawingFunction = 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() : void { + + // Based on the page size and document length, adjust the range of the + // browswer's scroll bars. + var body = <HTMLElement> 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( ) : CanvasRenderingContext2D { + + // 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 = <HTMLCanvasElement> document.getElementById("pdf_renderer"); + let ctx : CanvasRenderingContext2D = 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 = <HTMLCanvasElement> document.getElementById("pdf_renderer"); + + // This shouldn't be necessary, but do it as a fail-safe. + var ctx = <CanvasRenderingContext2D> 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() : void { + + // 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 = <HTMLCanvasElement> 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 : number) { + + // 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() : void { + + // 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 : number) { + + // 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() : void { + + // 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 : number , x : number , y : number ) { + + // (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 = <HTMLCanvasElement> 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 : MouseEvent) : void { + + // 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 : number , x : number , y : number ) { + + // 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 = <HTMLCanvasElement> 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 : MouseEvent) :void { + + // As above. + let id = Events.getID(); + mouseMoveGuts(id,e.x,e.y); +} + +async function mouseUpGuts(id : number , x : number , y : number ) { + + // As above. + // BUG: Common code. Not DRY. + await Events.waitMyTurn(id); + + let visWidth = document.documentElement.clientWidth; + let totWidth = FullPanel.getFullWidth(); + let canvas = <HTMLCanvasElement> 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 : MouseEvent ) : void { + + // As above. + let id = Events.getID(); + mouseUpGuts(id,e.x,e.y); +} + +function doTikzClick() : void { + + // 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 : AugmentedDrawingFunction = 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 : Event) { + + // 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); + |