├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── dist └── flipbook-viewer.js ├── package-lock.json ├── package.json ├── src ├── flipbookviewer.js ├── index.js └── singlepageviewer.js ├── test ├── 0.png ├── 00.png ├── 000.png ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png ├── 9.png ├── book-imgs.js ├── book-pdf.js ├── demo.gif ├── fp.pdf ├── index.html ├── pdfjs-init.js ├── test-imgs.html ├── test-imgs.js ├── test-pdf-sp.html ├── test-pdf-sp.js ├── test-pdf.html └── test-pdf.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | _tmp/ 3 | node_modules/ 4 | dist/* 5 | !dist/flipbook-viewer.js 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | _tmp/ 3 | node_modules/ 4 | test/ 5 | dist/* 6 | !dist/flipbook-viewer.js 7 | src/ 8 | webpack.config.js 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Charles Lobo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flipbook Viewer 2 | 3 | Amazing flip book component with animated pages. 4 | 5 | ![demo](./test/demo.gif) 6 | 7 | This is a tiny library that can show flip books from any source (including PDF’s, images, etc). 8 | 9 | ## Advantages 10 | 11 | 1. Tiny (18 ***Kb***). For comparison, the amazing [page-flip](./https://www.npmjs.com/package/page-flip) is 10 **Mb** (x1000 times bigger!). 12 | 2. Can use any input as a book simply by plugging in a “book provider”. An example PDF book using the amazing [pdfjs](./https://www.npmjs.com/package/pdfjs-dist) from Mozilla can be found in the test folder—[book-pdf.js](./test/book-pdf.js) (referenced usage: [test-pdf.js](./test/test-pdf.js)) 13 | 3. Supports **Panning**, **Zooming**, **Liking**, **Sharing**, along with page turning effects. 14 | 4. Raises events to track which pages are being viewed by user. 15 | 16 | ## Usage 17 | 18 | Below shows the flip `book` on the given `div` with the id `div-id`: 19 | 20 | ```js 21 | 'use strict' 22 | 23 | import { init as flipbook } from 'flipbook-viewer'; 24 | 25 | ... 26 | 27 | flipbook(book, 'div-id', (err, viewer) => { 28 | if(err) console.error(err); 29 | 30 | console.log('Number of pages: ' + viewer.page_count); 31 | viewer.on('seen', n => console.log('page number: ' + n)); 32 | 33 | next.onclick = () => viewer.flip_forward(); 34 | prev.onclick = () => viewer.flip_back(); 35 | zoom.onclick = () => viewer.zoom(); 36 | 37 | }); 38 | ``` 39 | 40 | The viewer can show *any* flip book. All you need to do is provide a book interface: 41 | 42 | ```js 43 | { 44 | numPages: () => { 45 | /* return number of pages */ 46 | }, 47 | getPage: (num, cb) => { 48 | /* return page number 'num' 49 | * in the callback 'cb' 50 | * as any CanvasImageSource: 51 | * (CSSImageValue, HTMLImageElement, 52 | * SVGImageElement, HTMLVideoElement, 53 | * HTMLCanvasElement, ImageBitmap, 54 | * OffscreenCanvas) 55 | */ 56 | } 57 | } 58 | ``` 59 | 60 | ## Options 61 | 62 | An optional `opts` parameter can be passed in to change the UI: 63 | 64 | ```js 65 | const opts = { 66 | backgroundColor: "#353535", 67 | boxColor: "#353535", 68 | width: 800, 69 | height: 600, 70 | } 71 | 72 | flipbook(book, 'div-id', opts, (err, viewer) => ... 73 | ``` 74 | 75 | ## Events 76 | 77 | You can listen on the `viewer` for which pages were seen: 78 | 79 | ```js 80 | viewer.on('seen', n => ...) 81 | ``` 82 | 83 | ## Programmatic API 84 | 85 | The returned viewer can be used to programmatically control the viewer: 86 | 87 | ```js 88 | viewer.flip_forward() 89 | viewer.flip_back() 90 | viewer.zoom() 91 | ``` 92 | 93 | ## Single Page View 94 | 95 | Finally, sometimes it makes sense to just show the book as a simple, scrollable view. To pass in only a `singlepage:true` option: 96 | 97 | ```js 98 | flipbook(book, 'div-id', {singlepage:true}, (err, viewer) => ... 99 | ``` 100 | 101 | This will generate a series of canvases with `class="flipbook__page"` and `id="flipbook__pgnum_"` that you can style using CSS. The single page view will raise the same `seen` event that the flipbook viewer does for tracking which pages the user actually flips through. 102 | 103 | The single page viewer is currently experimental and very simple. It should work for many PDF's but is not optimized for handling PDF's with a large number of pages. 104 | 105 | 106 | Enjoy! 107 | 108 | ------------ 109 | 110 | -------------------------------------------------------------------------------- /dist/flipbook-viewer.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("flipbook",[],e):"object"==typeof exports?exports.flipbook=e():t.flipbook=e()}(self,(()=>(()=>{"use strict";var t={194:t=>{function e(t){let e=document.createElement(t.tag);return n(e),e.attr(t.attr),e.add(t.children),e}function n(t){if(t)return t.c||(t.c=function(){return 1===arguments.length&&"string"==typeof arguments[0]?t.innerHTML=arguments[0]:(t.innerHTML="",arguments.length&&s(t,Array.prototype.slice.call(arguments))),t}),t.attr=e=>i(t,e),t.add=e=>s(t,e),t.rm=e=>t.removeChild(e),t.addClass=e=>function(t,e){if(t.classList&&t.classList.add)return t.classList.add(e);const n=t.getAttribute("class");n?t.setAttribute("class",`${n} ${e}`):t.setAttribute("class",e)}(t,e),t.rmClass=e=>function(t,e){if(t.classList&&t.classList.remove)return t.classList.remove(e);const n=t.getAttribute("class");if(!n)return;const o=n.split(" ").filter((t=>t!==e)).join(" ");t.setAttribute("class",o)}(t,e),t}function o(t,e,n,o){const r={tag:null,attr:{},children:[]};return i(e),i(n),i(o),r;function i(e){if(!r.tag){if(!e)return void(r.tag=t);if("string"==typeof e)return void(r.tag=e);r.tag=t}e&&(Array.isArray(e)||a(e)||"string"==typeof e||"number"==typeof e?r.children=r.children.concat(e):r.attr=Object.assign(r.attr,e))}}function r(t,e,n){n.class&&n.classes?n.class+=" "+n.classes:n.classes?n.class=n.classes:n.class||(n.class=""),delete n.classes;let o=(e=e.replace(/#/g,".#")).split(".");(e=o.shift())||(e=t);for(let t=0;t{let a=o("div",t,n,s);return a.tag=r("div",a.tag,a.attr),i.attr.class&&a.attr.class&&(a.attr.class=i.attr.class+" "+a.attr.class),i.attr.style&&a.attr.style&&typeof i.attr.style==typeof a.attr.style&&("string"==typeof i.attr.style?a.attr.style=i.attr.style+";"+a.attr.style:a.attr.style=Object.assign({},i.attr.style,a.attr.style)),a.attr=Object.assign({},i.attr,a.attr),e(a)}}t.exports={h:function(t,n,i){let s=o("div",t,n,i);return s.tag=r("div",s.tag,s.attr),e(s)},wrap:n,getH:function(t,e){return t instanceof Element?n(t):(e||(e=document),e.contentDocument&&(e=e.contentDocument),n(e.getElementById(t)))},x:c,div:c("div"),svg:function(t,e,a){let c,l=o("svg",t,e,a);if(l.tag=r("svg",l.tag,l.attr),"<"==l.tag[0]){c=document.createElementNS("http://www.w3.org/2000/svg","svg"),c.innerHTML=t;const e=c.getElementsByTagName("svg")[0];e&&(c=e)}else c=document.createElementNS("http://www.w3.org/2000/svg",l.tag),i(c,l.attr),s(c,l.children);return n(c),c}}},187:t=>{var e,n="object"==typeof Reflect?Reflect:null,o=n&&"function"==typeof n.apply?n.apply:function(t,e,n){return Function.prototype.apply.call(t,e,n)};e=n&&"function"==typeof n.ownKeys?n.ownKeys:Object.getOwnPropertySymbols?function(t){return Object.getOwnPropertyNames(t).concat(Object.getOwnPropertySymbols(t))}:function(t){return Object.getOwnPropertyNames(t)};var r=Number.isNaN||function(t){return t!=t};function i(){i.init.call(this)}t.exports=i,t.exports.once=function(t,e){return new Promise((function(n,o){function r(n){t.removeListener(e,i),o(n)}function i(){"function"==typeof t.removeListener&&t.removeListener("error",r),n([].slice.call(arguments))}g(t,e,i,{once:!0}),"error"!==e&&function(t,e,n){"function"==typeof t.on&&g(t,"error",e,{once:!0})}(t,r)}))},i.EventEmitter=i,i.prototype._events=void 0,i.prototype._eventsCount=0,i.prototype._maxListeners=void 0;var s=10;function a(t){if("function"!=typeof t)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof t)}function c(t){return void 0===t._maxListeners?i.defaultMaxListeners:t._maxListeners}function l(t,e,n,o){var r,i,s,l;if(a(n),void 0===(i=t._events)?(i=t._events=Object.create(null),t._eventsCount=0):(void 0!==i.newListener&&(t.emit("newListener",e,n.listener?n.listener:n),i=t._events),s=i[e]),void 0===s)s=i[e]=n,++t._eventsCount;else if("function"==typeof s?s=i[e]=o?[n,s]:[s,n]:o?s.unshift(n):s.push(n),(r=c(t))>0&&s.length>r&&!s.warned){s.warned=!0;var f=new Error("Possible EventEmitter memory leak detected. "+s.length+" "+String(e)+" listeners added. Use emitter.setMaxListeners() to increase limit");f.name="MaxListenersExceededWarning",f.emitter=t,f.type=e,f.count=s.length,l=f,console&&console.warn&&console.warn(l)}return t}function f(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function u(t,e,n){var o={fired:!1,wrapFn:void 0,target:t,type:e,listener:n},r=f.bind(o);return r.listener=n,o.wrapFn=r,r}function h(t,e,n){var o=t._events;if(void 0===o)return[];var r=o[e];return void 0===r?[]:"function"==typeof r?n?[r.listener||r]:[r]:n?function(t){for(var e=new Array(t.length),n=0;n0&&(s=e[0]),s instanceof Error)throw s;var a=new Error("Unhandled error."+(s?" ("+s.message+")":""));throw a.context=s,a}var c=i[t];if(void 0===c)return!1;if("function"==typeof c)o(c,this,e);else{var l=c.length,f=d(c,l);for(n=0;n=0;i--)if(n[i]===e||n[i].listener===e){s=n[i].listener,r=i;break}if(r<0)return this;0===r?n.shift():function(t,e){for(;e+1=0;o--)this.removeListener(t,e[o]);return this},i.prototype.listeners=function(t){return h(this,t,!0)},i.prototype.rawListeners=function(t){return h(this,t,!1)},i.listenerCount=function(t,e){return"function"==typeof t.listenerCount?t.listenerCount(e):p.call(t,e)},i.prototype.listenerCount=p,i.prototype.eventNames=function(){return this._eventsCount>0?e(this._events):[]}}},e={};function n(o){var r=e[o];if(void 0!==r)return r.exports;var i=e[o]={exports:{}};return t[o](i,i.exports,n),i.exports}n.d=(t,e)=>{for(var o in e)n.o(e,o)&&!n.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:e[o]})},n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),n.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var o={};return(()=>{n.r(o),n.d(o,{init:()=>d});var t=n(194),e=n(187);class r extends e{}const i=window.devicePixelRatio||1;function s(e,n){const o=new r;o.page_count=e.book.numPages(),function(e,n){const o={e:(0,t.h)("canvas")};o.ctx=o.e.getContext("2d"),o.e.width=Math.floor(e.sz.boxw*i),o.e.height=Math.floor(e.sz.boxh*i),o.e.style.width=Math.floor(e.sz.boxw)+"px",o.e.style.height=Math.floor(e.sz.boxh)+"px",e.canvas=o,n()}(e,(t=>{if(t)return n(t);!function(t,e){const n=t.sz.boxw*i,o=t.sz.boxh*i;t.book.getPage(1,((r,i)=>{if(r)return e(r);const s=1-t.sz.marginTop/100;let a=o*s;const c=1-t.sz.marginLeft/100;let l=2*i.width*(a/i.height);const f=n*c;l>f&&(l=f,a=i.height*(l/(2*i.width))),t.layout={top:(o-a)/2,left:(n-l)/2,mid:n/2,width:l,height:a},e()}))}(e,(t=>{if(t)return n(t);e.app.c(e.canvas.e),function(t,e){const n=[a(t,e)],o={};["onmouseenter","onmouseleave","onmousemove","onclick","onmousedown","onmouseup"].map((t=>{o[t]=e=>{n.map((n=>{n[t]&&n[t](e)}))}})),t.app.attr(o)}(e,o),e.zoom=0,e.showNdx=0,function(t,e){function n(t){u({draw:n=>{t.flipFrac=n.flipFrac,function(t,e){f(t,e);const n=t.canvas,o=2*t.flipNdx,r=o+1,i=l(t),s=.5-Math.abs(.5-t.flipFrac);function a(e,o){n.ctx.fillStyle=e;const r=t.sz.bx_border;n.ctx.fillRect(o.left-r,o.top-r-5,o.width+2*r,o.height+2*r+10)}function c(t,e){const n=t.sz.bx_border;return{left:e.left-n,top:e.top-n,width:e.width+2*n,height:e.height+2*n}}n.ctx.save(),t.book.getPage(o,((e,o)=>{if(e)return console.error(e);t.book.getPage(r,((e,r)=>{if(e)return console.error(e);!function(e,o,r,l){let f,u,h,p,d,g,m,v,x;if(t.showNdxn.ctx.restore()))}))}))}(t,e)},duration:1111,from:{flipFrac:0},to:{flipFrac:1},timing:t=>t*t*(3-2*t),ondone:()=>{t.showNdx=t.flipNdx,t.flipNdx=null,f(t,e)}})}e.zoom=n=>{n=Number(n),isNaN(n)&&(n=2*t.zoom+1)>4&&(n=0),n?u({draw:n=>{t.zoom=n.zoom,f(t,e)},duration:500,from:{zoom:t.zoom},to:{zoom:n},timing:t=>t*t*(3-2*t)}):(t.zoom=0,t.pan=null,f(t,e))},e.flip_forward=()=>{t.flipNdx||0===t.flipNdx||t.book.numPages()<=1||2*t.showNdx+1>=t.book.numPages()||(t.flipNdx=t.showNdx+1,n(t))},e.flip_back=()=>{t.flipNdx||0===t.flipNdx||t.book.numPages()<=1||t.showNdx&&(t.flipNdx=t.showNdx-1,n(t))}}(e,o),n(null,o),f(e,o)}))}))}function a(t,e){let n;return{onmouseleave:function(t){n=null},onmousedown:function(e){t.zoom&&(n=c(t,e),t.pan&&(n.x-=t.pan.x,n.y-=t.pan.y))},onmouseup:function(t){n=null},onmousemove:function(o){const r=c(t,o);n&&function(t,e){const n=function(t){const e=l(t);return{top:e.top,left:e.left,bottom:e.top+e.height,right:e.left+e.width}}(t);return n.top<=e.y&&n.bottom>=e.y&&n.left<=e.x&&n.right>=e.x}(t,r)?(t.pan={x:r.x-n.x,y:r.y-n.y},f(t,e)):n=null}}}function c(t,e){const n=t.app.getBoundingClientRect();return{x:e.clientX-n.x,y:e.clientY-n.y}}function l(t){let e=t.layout;if(t.zoom>0){if(e=Object.assign({},e),t.zoom){const n=.5*t.zoom;e.left=e.left-e.width*n/2,e.top=e.top-e.height*n/2,e.width=e.width*(1+n),e.height=e.height*(1+n)}t.pan&&(e.left+=t.pan.x,e.top+=t.pan.y,e.mid+=t.pan.x)}return e}function f(t,e){const n=t.canvas,o=2*t.showNdx,r=o+1;function s(t,e){n.ctx.drawImage(t.img,e.left,e.top,e.width,e.height)}n.ctx.save(),n.ctx.fillStyle=t.color.bg,n.ctx.fillRect(0,0,t.sz.boxw*i,t.sz.boxh*i),t.book.getPage(o,((i,a)=>{if(i)return console.error(i);!t.flipNdx&&0!==t.flipNdx&&a&&e.emit("seen",o),t.book.getPage(r,((o,i)=>{if(o)return console.error(o);!t.flipNdx&&0!==t.flipNdx&&i&&e.emit("seen",r),function(e,o,r){const i=l(t);0==t.zoom&&function(e){n.ctx.fillStyle=t.color.bx;const o=t.sz.bx_border;n.ctx.fillRect(e.left-o,e.top-o,e.width+2*o,e.height+2*o)}(i);const a=Object.assign({},i),c=Object.assign({},i);a.width/=2,c.width/=2,c.left=i.mid,e&&s(e,a),o&&s(o,c),n.ctx.restore()}(a,i)}))}))}function u({draw:t,duration:e,from:n,to:o,timing:r,ondone:i}){i||(i=()=>1),r||(r=t=>t);const s=Date.now();!function a(){let c=(Date.now()-s)/e;c>1&&(c=1);const l=function(t){t=r(t);const e=Object.assign({},n);for(let r in n){const i=Number(n[r]),s=Number(o[r]);e[r]=i+(s-i)*t}return e}(c);t(l),1===c?i():requestAnimationFrame(a)}()}class h extends e{}const p=e=>(0,t.h)(`canvas#flipbook__pgnum_${e}.flipbook__page`);function d(e,n,o,r){"function"==typeof o&&(r=o,o={}),o||(o={}),r||(r=()=>1);const i=(0,t.getH)(n);if(!i){const t="flipbook-viewer: Failed to find container for viewer: "+n;return console.error(t),void r(t)}if(o.singlepage)!function(t,e){const n=new h;n.page_count=t.book.numPages(),function(t,e){const n=p(0);t.app.c(n),setTimeout((()=>{t.page_width=n.getBoundingClientRect().width,t.app.rm(n),e()}))}(t,(o=>{if(o)return e(o);!function(t,e){const n=window.devicePixelRatio||1,o=t.book.pdf;t.pages=[],function r(i){if(i>=o.numPages)return e();const s=i+1;o.getPage(s).then((o=>{const a=o.getViewport({scale:1}),c=t.page_width/a.width,l=o.getViewport({scale:c}),f=p(s);f.attr({"data-flipbook-page":s}),f.width=Math.floor(l.width*n),f.height=Math.floor(l.height*n),f.style.width=Math.floor(l.width)+"px",f.style.height=Math.floor(l.height)+"px",t.pages.push(f),t.app.add(f);const u=1!==n?[n,0,0,n,0,0]:null,h={canvasContext:f.getContext("2d"),transform:u,viewport:l};o.render(h).promise.then((()=>r(i+1))).catch((t=>e(t||"Failed rendering page"+s)))})).catch((t=>e(t||"Failed getting page:"+s)))}(0)}(t,(o=>{if(o)return e(o);e(null,n),function(t,e){const n={},o=new IntersectionObserver((function(t){t.forEach((t=>{if(t.intersectionRatio)try{const o=t.target.dataset.flipbookPage;if(n[o])return;n[o]=!0,e.emit("seen",o)}catch(t){console.error(t)}}))}),{root:null,rootMargin:"0px",threshold:.25});t.pages.forEach((t=>o.observe(t)))}(t,n)}))}))}({app:i,book:e},a);else{const t={color:{bg:o.backgroundColor||"#353535"},sz:{bx_border:o.boxBorder||0,boxw:o.width||800,boxh:o.height||600},app:i,book:e},n=o.margin||1;o.marginTop||0===o.marginTop?t.sz.marginTop=o.marginTop:t.sz.marginTop=n,o.marginLeft||0===o.marginLeft?t.sz.marginLeft=o.marginLeft:t.sz.marginLeft=n,s(t,a)}function a(t,e){return o.popup&&history.pushState({},"","#"),e&&(e.version="1.6.1"),r(t,e)}}})(),o})())); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flipbook-viewer", 3 | "version": "1.6.1", 4 | "description": "Amazing Page Flip Animation for a (PDF/Image Collection) Flipbook viewer", 5 | "main": "dist/flipbook-viewer.js", 6 | "scripts": { 7 | "start": "npm run clean && webpack build --env production", 8 | "test": "webpack serve --env development --open", 9 | "clean": "npx -y del-cli dist && npx -y make-dir-cli dist" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/theproductiveprogrammer/flipbook-viewer.git" 14 | }, 15 | "keywords": [ 16 | "page-flip", "flipbook", "pdfjs", "pdf-viewer", "viewer", "canvas", "animation", 17 | "page-turning", "page-turn" 18 | ], 19 | "author": "charles.lobo@gmail.com", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/theproductiveprogrammer/flipbook-viewer/issues" 23 | }, 24 | "homepage": "https://github.com/theproductiveprogrammer/flipbook-viewer#readme", 25 | "devDependencies": { 26 | "pdfjs-dist": "^2.10.377", 27 | "svg-inline-loader": "^0.8.2", 28 | "webpack": "^5.61.0", 29 | "webpack-cli": "^4.9.1", 30 | "webpack-dev-server": "^4.4.0" 31 | }, 32 | "dependencies": { 33 | "@tpp/htm-x": "^5.8.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/flipbookviewer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import { h } from '@tpp/htm-x'; 3 | import * as EventEmitter from 'events'; 4 | 5 | class FlipbookViewer extends EventEmitter {}; 6 | 7 | const outputScale = window.devicePixelRatio || 1; // Support HiDPI-screens 8 | 9 | /* way/ 10 | * set up the canvas and the toolbar, return the viewer, 11 | * then show the first page 12 | */ 13 | export function flipbookViewer(ctx, cb) { 14 | const viewer = new FlipbookViewer(); 15 | viewer.page_count = ctx.book.numPages(); 16 | 17 | setupCanvas(ctx, err => { 18 | if(err) return cb(err); 19 | 20 | calcLayoutParameters(ctx, err => { 21 | if(err) return cb(err); 22 | 23 | ctx.app.c(ctx.canvas.e); 24 | 25 | setupMouseHandler(ctx, viewer); 26 | 27 | ctx.zoom = 0; 28 | ctx.showNdx = 0; 29 | 30 | setupControls(ctx, viewer); 31 | 32 | cb(null, viewer); 33 | 34 | showPages(ctx, viewer); 35 | 36 | }); 37 | 38 | }); 39 | 40 | } 41 | 42 | 43 | function setupControls(ctx, viewer) { 44 | 45 | viewer.zoom = zoom => { 46 | zoom = Number(zoom); 47 | if(isNaN(zoom)) { 48 | zoom = ctx.zoom * 2 + 1; 49 | if(zoom > 4) zoom = 0; 50 | } 51 | if(!zoom) { 52 | ctx.zoom = 0; 53 | ctx.pan = null; 54 | showPages(ctx, viewer); 55 | } else { 56 | animate({ 57 | draw: curr => { 58 | ctx.zoom = curr.zoom; 59 | showPages(ctx, viewer); 60 | }, 61 | duration: 500, 62 | from: { zoom: ctx.zoom }, 63 | to: { zoom }, 64 | timing: t => t * t * (3.0 - 2.0 * t), 65 | }) 66 | } 67 | } 68 | 69 | viewer.flip_forward = () => { 70 | if(ctx.flipNdx || ctx.flipNdx === 0) return; 71 | if(ctx.book.numPages() <= 1) return; 72 | if((ctx.showNdx * 2 + 1) >= ctx.book.numPages()) return; 73 | ctx.flipNdx = ctx.showNdx + 1; 74 | flip_1(ctx); 75 | } 76 | viewer.flip_back = () => { 77 | if(ctx.flipNdx || ctx.flipNdx === 0) return; 78 | if(ctx.book.numPages() <= 1) return; 79 | if(!ctx.showNdx) return; 80 | ctx.flipNdx = ctx.showNdx - 1; 81 | flip_1(ctx); 82 | } 83 | 84 | function flip_1(ctx) { 85 | animate({ 86 | draw: curr => { 87 | ctx.flipFrac = curr.flipFrac; 88 | showFlip(ctx, viewer); 89 | }, 90 | duration: 1111, 91 | from: { flipFrac: 0 }, 92 | to: { flipFrac: 1 }, 93 | timing: t => t * t * (3.0 - 2.0 * t), 94 | ondone: () => { 95 | ctx.showNdx = ctx.flipNdx; 96 | ctx.flipNdx = null; 97 | showPages(ctx, viewer); 98 | } 99 | }) 100 | } 101 | } 102 | 103 | 104 | /* way/ 105 | * set up a canvas element with some width 106 | * and height and use the first page to 107 | * calculate the display. 108 | */ 109 | function setupCanvas(ctx, cb) { 110 | const canvas = { 111 | e: h("canvas") 112 | }; 113 | 114 | canvas.ctx = canvas.e.getContext('2d'); 115 | canvas.e.width = Math.floor(ctx.sz.boxw * outputScale); 116 | canvas.e.height = Math.floor(ctx.sz.boxh * outputScale); 117 | canvas.e.style.width = Math.floor(ctx.sz.boxw) + "px"; 118 | canvas.e.style.height = Math.floor(ctx.sz.boxh) + "px"; 119 | 120 | ctx.canvas = canvas; 121 | cb(); 122 | } 123 | 124 | /* way/ 125 | * use the first page to calculate enough space 126 | * for showing a double-page view. 127 | */ 128 | function calcLayoutParameters(ctx, cb) { 129 | const w = ctx.sz.boxw * outputScale; 130 | const h = ctx.sz.boxh * outputScale; 131 | 132 | ctx.book.getPage(1, (err, pg) => { 133 | if(err) return cb(err); 134 | 135 | const usableH = 1 - (ctx.sz.marginTop/100); 136 | let height = h * usableH; 137 | const usableW = 1 - (ctx.sz.marginLeft/100); 138 | let width = (pg.width * 2) * (height / pg.height); 139 | const maxwidth = w * usableW; 140 | if(width > maxwidth) { 141 | width = maxwidth; 142 | height = (pg.height) * (width / (pg.width * 2)); 143 | } 144 | 145 | ctx.layout = { 146 | top: (h - height) / 2, 147 | left: (w - width) / 2, 148 | mid: w / 2, 149 | width: width, 150 | height, 151 | }; 152 | 153 | cb(); 154 | }); 155 | } 156 | 157 | /* way/ 158 | * capture mouse events, passing them to the 159 | * actual handlers if set up 160 | */ 161 | function setupMouseHandler(ctx, viewer) { 162 | const handlers = [ 163 | setupPanning(ctx, viewer), 164 | ]; 165 | 166 | const events = [ 167 | "onmouseenter", "onmouseleave", 168 | "onmousemove", 169 | "onclick", 170 | "onmousedown", "onmouseup", 171 | ]; 172 | 173 | const attr = {}; 174 | events.map(e => { 175 | attr[e] = evt => { 176 | handlers.map(h => { 177 | if(h[e]) h[e](evt); 178 | }) 179 | } 180 | }) 181 | 182 | ctx.app.attr(attr); 183 | } 184 | 185 | /* way/ 186 | * set up the ctx.pan offsets (only when zooming), 187 | * starting on the first mouse click and ending when 188 | * mouse up or we leave the box 189 | */ 190 | function setupPanning(ctx, viewer) { 191 | let start; 192 | 193 | function onmouseleave(evt) { 194 | start = null; 195 | } 196 | 197 | function onmousedown(evt) { 198 | if(!ctx.zoom) return; 199 | start = mousePt(ctx, evt); 200 | if(ctx.pan) { 201 | start.x -= ctx.pan.x; 202 | start.y -= ctx.pan.y; 203 | } 204 | } 205 | 206 | function onmouseup(evt) { 207 | start = null; 208 | } 209 | 210 | function onmousemove(evt) { 211 | const pt = mousePt(ctx, evt); 212 | if(start && inBox(ctx, pt)) { 213 | ctx.pan = { 214 | x: (pt.x - start.x), 215 | y: (pt.y - start.y), 216 | }; 217 | showPages(ctx, viewer); 218 | } else { 219 | start = null; 220 | } 221 | } 222 | 223 | return { 224 | onmouseleave, 225 | onmousedown, 226 | onmouseup, 227 | onmousemove, 228 | }; 229 | 230 | } 231 | 232 | /* way/ 233 | * return true if the point is in the current box 234 | */ 235 | function inBox(ctx, pt) { 236 | const rt = currBox(ctx); 237 | return (rt.top <= pt.y && rt.bottom >= pt.y && 238 | rt.left <= pt.x && rt.right >= pt.x); 239 | } 240 | 241 | /* way/ 242 | * return the location of the mouse relative to the app area 243 | */ 244 | function mousePt(ctx, evt) { 245 | const rect = ctx.app.getBoundingClientRect(); 246 | return { 247 | x: evt.clientX - rect.x, 248 | y: evt.clientY - rect.y 249 | }; 250 | } 251 | 252 | /* way/ 253 | * return the current rectangle 254 | */ 255 | function currBox(ctx) { 256 | const l = calcLayout(ctx); 257 | return { 258 | top: l.top, 259 | left: l.left, 260 | bottom: l.top + l.height, 261 | right: l.left + l.width, 262 | }; 263 | } 264 | 265 | /* understand/ 266 | * return the layout, adjusted for zoom and panning 267 | */ 268 | function calcLayout(ctx) { 269 | let layout = ctx.layout; 270 | 271 | if(ctx.zoom > 0) { 272 | layout = Object.assign({}, layout); 273 | if(ctx.zoom) { 274 | const zoom = ctx.zoom * 0.5; 275 | layout.left = layout.left - layout.width * zoom / 2; 276 | layout.top = layout.top - layout.height * zoom / 2; 277 | layout.width = layout.width * (1 + zoom); 278 | layout.height = layout.height * (1 + zoom); 279 | } 280 | if(ctx.pan) { 281 | layout.left += ctx.pan.x; 282 | layout.top += ctx.pan.y; 283 | layout.mid += ctx.pan.x; 284 | } 285 | } 286 | 287 | return layout; 288 | } 289 | 290 | /* way/ 291 | * show the background and the pages on the viewer 292 | */ 293 | function showPages(ctx, viewer) { 294 | const canvas = ctx.canvas; 295 | const left_ = ctx.showNdx * 2; 296 | const right_ = left_ + 1; 297 | canvas.ctx.save(); 298 | show_bg_1(); 299 | ctx.book.getPage(left_, (err, left) => { 300 | if(err) return console.error(err); 301 | if(!ctx.flipNdx && ctx.flipNdx !== 0 && left) viewer.emit('seen', left_); 302 | ctx.book.getPage(right_, (err, right) => { 303 | if(err) return console.error(err); 304 | if(!ctx.flipNdx && ctx.flipNdx !== 0 && right) viewer.emit('seen', right_); 305 | show_pgs_1(left, right, () => canvas.ctx.restore()); 306 | }) 307 | }) 308 | 309 | /* way/ 310 | * get the current layout and, if no zoom, show the 311 | * surrounding box. Otherwise show the left and right 312 | * pages on the correct positions 313 | */ 314 | function show_pgs_1(left, right, cb) { 315 | const layout = calcLayout(ctx); 316 | 317 | if(ctx.zoom == 0) show_bx_1(layout); 318 | 319 | const page_l = Object.assign({}, layout); 320 | const page_r = Object.assign({}, layout); 321 | page_l.width /= 2; 322 | page_r.width /= 2; 323 | page_r.left = layout.mid; 324 | if(left) show_pg_1(left, page_l); 325 | if(right) show_pg_1(right, page_r); 326 | cb(); 327 | } 328 | 329 | function show_pg_1(pg, loc) { 330 | canvas.ctx.drawImage(pg.img, loc.left, loc.top, loc.width, loc.height); 331 | } 332 | 333 | function show_bx_1(loc) { 334 | canvas.ctx.fillStyle = ctx.color.bx; 335 | const border = ctx.sz.bx_border; 336 | canvas.ctx.fillRect(loc.left - border, loc.top-border, loc.width+border*2, loc.height+2*border); 337 | } 338 | 339 | function show_bg_1() { 340 | 341 | canvas.ctx.fillStyle = ctx.color.bg; 342 | canvas.ctx.fillRect(0, 0, ctx.sz.boxw*outputScale, ctx.sz.boxh*outputScale); 343 | } 344 | } 345 | 346 | /* way/ 347 | * show the current pages, then overlay the current flip 348 | */ 349 | function showFlip(ctx, viewer) { 350 | showPages(ctx, viewer); 351 | 352 | const canvas = ctx.canvas; 353 | const left = ctx.flipNdx * 2; 354 | const right = left + 1; 355 | const layout = calcLayout(ctx); 356 | const strength = 0.5 - Math.abs(0.5 - ctx.flipFrac); 357 | canvas.ctx.save(); 358 | 359 | ctx.book.getPage(left, (err, left) => { 360 | if(err) return console.error(err); 361 | ctx.book.getPage(right, (err, right) => { 362 | if(err) return console.error(err); 363 | show_flip_1(left, right, ctx.flipFrac, () => canvas.ctx.restore()); 364 | }) 365 | }) 366 | 367 | 368 | function show_flip_1(left, right, frac, cb) { 369 | let loc, show, width, region, xloc, oheight, otop, controlpt, endpt; 370 | 371 | if(ctx.showNdx < ctx.flipNdx) { 372 | 373 | loc = Object.assign({}, layout); 374 | loc.width /= 2; 375 | loc.left = layout.mid; 376 | show = loc.left + (1 - frac) * loc.width; 377 | width = loc.width * frac; 378 | xloc = xpand_rect_1(ctx, loc); 379 | canvas.ctx.save(); 380 | region = new Path2D(); 381 | region.rect(show, xloc.top-5, width, xloc.height+10); 382 | canvas.ctx.clip(region); 383 | if(right) { 384 | canvas.ctx.drawImage(right.img, loc.left, loc.top, loc.width, loc.height); 385 | } else { 386 | show_empty_1(ctx.color.bg, xloc); 387 | } 388 | canvas.ctx.restore(); 389 | 390 | loc = Object.assign({}, layout); 391 | loc.left += (1 - frac) * loc.width; 392 | loc.width /= 2; 393 | width = loc.width * frac; 394 | 395 | oheight = loc.height; 396 | otop = loc.top; 397 | loc.height *= (1 + strength * 0.1); 398 | loc.top -= (loc.height-oheight)/2; 399 | 400 | canvas.ctx.save(); 401 | region = new Path2D(); 402 | region.moveTo(loc.left, otop); 403 | region.lineTo(loc.left, otop + oheight); 404 | controlpt = { 405 | x: loc.left + width / 2, 406 | y: loc.top + loc.height, 407 | }; 408 | endpt = { 409 | x: loc.left + width, 410 | y: otop + oheight, 411 | }; 412 | region.quadraticCurveTo(controlpt.x, controlpt.y, endpt.x, endpt.y); 413 | region.lineTo(endpt.x, otop); 414 | controlpt = { 415 | x: loc.left + width, 416 | y: loc.top 417 | }; 418 | endpt = { 419 | x: loc.left, 420 | y: otop, 421 | }; 422 | region.quadraticCurveTo(controlpt.x, controlpt.y, endpt.x, endpt.y); 423 | canvas.ctx.clip(region); 424 | canvas.ctx.drawImage(left.img, loc.left, loc.top, loc.width, loc.height); 425 | canvas.ctx.restore(); 426 | 427 | 428 | canvas.ctx.save(); 429 | const shadowsz = (loc.width / 2) * Math.max(Math.min(strength, 0.5), 0); 430 | 431 | // Draw a sharp shadow on the left side of the page 432 | canvas.ctx.strokeStyle = 'rgba(0,0,0,'+(0.1 * strength)+')'; 433 | canvas.ctx.lineWidth = 30 * strength; 434 | canvas.ctx.beginPath(); 435 | canvas.ctx.moveTo(loc.left, otop); 436 | canvas.ctx.lineTo(loc.left, otop + oheight); 437 | canvas.ctx.stroke(); 438 | 439 | // Right side drop shadow 440 | let gradient = canvas.ctx.createLinearGradient(loc.left + width, otop, loc.left+width+shadowsz, otop); 441 | gradient.addColorStop(0, 'rgba(0,0,0,'+ (0.3*strength)+')'); 442 | gradient.addColorStop(0.8, 'rgba(0,0,0,0.0)'); 443 | canvas.ctx.fillStyle = gradient; 444 | canvas.ctx.fillRect(loc.left + width, otop, width + shadowsz, oheight); 445 | 446 | canvas.ctx.restore(); 447 | 448 | } else { 449 | 450 | loc = Object.assign({}, layout); 451 | loc.width /= 2; 452 | width = loc.width * frac + ctx.sz.bx_border; 453 | xloc = xpand_rect_1(ctx, loc); 454 | canvas.ctx.save(); 455 | region = new Path2D(); 456 | region.rect(xloc.left, xloc.top-5, width, xloc.height+10); 457 | canvas.ctx.clip(region); 458 | if(left) { 459 | canvas.ctx.drawImage(left.img, loc.left, loc.top, loc.width, loc.height); 460 | } else { 461 | show_empty_1(ctx.color.bg, loc); 462 | } 463 | canvas.ctx.restore(); 464 | 465 | 466 | loc = Object.assign({}, layout); 467 | loc.width /= 2; 468 | show = loc.left + frac * loc.width; 469 | loc.left = show - (1 - frac) * loc.width; 470 | width = loc.width * frac; 471 | 472 | oheight = loc.height; 473 | otop = loc.top; 474 | loc.height *= (1 + strength * 0.1); 475 | loc.top -= (loc.height-oheight)/2; 476 | 477 | canvas.ctx.save(); 478 | region = new Path2D(); 479 | region.moveTo(show, otop); 480 | region.lineTo(show, otop + oheight); 481 | controlpt = { 482 | x: show + width / 2, 483 | y: loc.top + loc.height, 484 | }; 485 | endpt = { 486 | x: show + width, 487 | y: otop + oheight, 488 | }; 489 | region.quadraticCurveTo(controlpt.x, controlpt.y, endpt.x, endpt.y); 490 | region.lineTo(endpt.x, otop); 491 | controlpt = { 492 | x: show + width, 493 | y: loc.top 494 | }; 495 | endpt = { 496 | x: show, 497 | y: otop, 498 | }; 499 | region.quadraticCurveTo(controlpt.x, controlpt.y, endpt.x, endpt.y); 500 | canvas.ctx.clip(region); 501 | canvas.ctx.drawImage(right.img, loc.left, loc.top, loc.width, loc.height); 502 | canvas.ctx.restore(); 503 | 504 | 505 | canvas.ctx.save(); 506 | const shadowsz = (loc.width / 2) * Math.max(Math.min(strength, 0.5), 0); 507 | 508 | // Draw a sharp shadow on the right side of the page 509 | canvas.ctx.strokeStyle = 'rgba(0,0,0,'+(0.1 * strength)+')'; 510 | canvas.ctx.lineWidth = 30 * strength; 511 | canvas.ctx.beginPath(); 512 | canvas.ctx.moveTo(show + width, otop); 513 | canvas.ctx.lineTo(show + width, otop + oheight); 514 | canvas.ctx.stroke(); 515 | 516 | // Left side drop shadow 517 | let gradient = canvas.ctx.createLinearGradient(show, otop, show-shadowsz, otop); 518 | gradient.addColorStop(0, 'rgba(0,0,0,'+ (0.3*strength)+')'); 519 | gradient.addColorStop(0.8, 'rgba(0,0,0,0.0)'); 520 | canvas.ctx.fillStyle = gradient; 521 | canvas.ctx.fillRect(show-shadowsz, otop, shadowsz, oheight); 522 | 523 | canvas.ctx.restore(); 524 | 525 | } 526 | 527 | cb(); 528 | } 529 | 530 | function show_empty_1(fillStyle, loc) { 531 | canvas.ctx.fillStyle = fillStyle; 532 | const border = ctx.sz.bx_border; 533 | canvas.ctx.fillRect(loc.left - border, loc.top-border-5, loc.width+border*2, loc.height+2*border+10); 534 | } 535 | 536 | function xpand_rect_1(ctx, loc) { 537 | const border = ctx.sz.bx_border; 538 | return { 539 | left: loc.left - border, 540 | top: loc.top - border, 541 | width: loc.width + border * 2, 542 | height: loc.height + border * 2, 543 | }; 544 | } 545 | 546 | } 547 | 548 | /* understand/ 549 | * animate the properties {from -> to} , calling ondone when ends 550 | */ 551 | function animate({ draw, duration, from, to, timing, ondone }) { 552 | if(!ondone) ondone = () => 1; 553 | if(!timing) timing = t => t; 554 | 555 | const start = Date.now(); 556 | 557 | animate_1(); 558 | 559 | function animate_1() { 560 | let frac = (Date.now() - start) / duration; 561 | if(frac > 1) frac = 1; 562 | const curr = progress_1(frac); 563 | draw(curr); 564 | if(frac === 1) ondone(); 565 | else requestAnimationFrame(animate_1); 566 | } 567 | 568 | function progress_1(frac) { 569 | frac = timing(frac); 570 | const ret = Object.assign({}, from); 571 | for(let k in from) { 572 | const s = Number(from[k]); 573 | const e = Number(to[k]); 574 | ret[k] = s + (e - s) * frac; 575 | } 576 | return ret; 577 | } 578 | } 579 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import { getH } from '@tpp/htm-x'; 3 | 4 | import { flipbookViewer } from "./flipbookviewer.js"; 5 | import { singlePageViewer } from "./singlepageviewer.js"; 6 | import pkg from '../package.json'; 7 | 8 | /* way/ 9 | * set up the options and return the requested viewer 10 | */ 11 | export function init(book, id, opts, cb) { 12 | if(typeof opts === 'function') { 13 | cb = opts; 14 | opts = {}; 15 | } 16 | if(!opts) opts = {}; 17 | if(!cb) cb = () => 1; 18 | const app = getH(id); 19 | if(!app) { 20 | const emsg = ("flipbook-viewer: Failed to find container for viewer: " + id); 21 | console.error(emsg); 22 | cb(emsg); 23 | return; 24 | } 25 | 26 | if(opts.singlepage) { 27 | 28 | singlePageViewer({ app, book }, ret_1); 29 | 30 | } else { 31 | 32 | const ctx = { 33 | color: { 34 | bg: opts.backgroundColor || "#353535", 35 | }, 36 | sz: { 37 | bx_border: opts.boxBorder || 0, 38 | boxw: opts.width || 800, 39 | boxh: opts.height || 600, 40 | }, 41 | app, 42 | book, 43 | }; 44 | 45 | const margin = opts.margin || 1; 46 | if(opts.marginTop || opts.marginTop === 0) ctx.sz.marginTop = opts.marginTop; 47 | else ctx.sz.marginTop = margin; 48 | if(opts.marginLeft || opts.marginLeft === 0) ctx.sz.marginLeft = opts.marginLeft; 49 | else ctx.sz.marginLeft = margin; 50 | 51 | flipbookViewer(ctx, ret_1); 52 | } 53 | 54 | 55 | function ret_1(err, viewer) { 56 | if(opts.popup) history.pushState({}, "", "#"); 57 | if(viewer) viewer.version = pkg.version; 58 | return cb(err, viewer); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/singlepageviewer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import { h } from '@tpp/htm-x'; 3 | import * as EventEmitter from 'events'; 4 | 5 | class SinglePageViewer extends EventEmitter {}; 6 | 7 | /* way/ 8 | * Set up a viewer container, generate all 9 | * the pages within that container, and set up 10 | * the interesection observer to raise 'seen' events 11 | */ 12 | export function singlePageViewer(ctx, cb) { 13 | const viewer = new SinglePageViewer(); 14 | viewer.page_count = ctx.book.numPages(); 15 | 16 | setupCont(ctx, err => { 17 | if(err) return cb(err); 18 | 19 | generatePages(ctx, err => { 20 | if(err) return cb(err); 21 | cb(null, viewer); 22 | setupSeenEvents(ctx, viewer); 23 | }); 24 | 25 | }); 26 | } 27 | 28 | function setupSeenEvents(ctx, viewer) { 29 | const seen = {}; 30 | const observer = new IntersectionObserver(pg_seen_1, { 31 | root: null, 32 | rootMargin: "0px", 33 | threshold: 0.25, 34 | }); 35 | 36 | ctx.pages.forEach(p => observer.observe(p)); 37 | 38 | 39 | function pg_seen_1(entries) { 40 | entries.forEach(e => { 41 | if(e.intersectionRatio) { 42 | try { 43 | const page = e.target.dataset.flipbookPage; 44 | if(seen[page]) return; 45 | seen[page] = true; 46 | viewer.emit("seen", page); 47 | } catch(e) { 48 | console.error(e); 49 | } 50 | } 51 | }); 52 | } 53 | 54 | } 55 | 56 | const newpage = n => h(`canvas#flipbook__pgnum_${n}.flipbook__page`); 57 | /* way/ 58 | * Create a temporary "page" to get the 59 | * expected size (width) of the pages set 60 | * by the CSS 61 | */ 62 | function setupCont(ctx, cb) { 63 | const tmp = newpage(0); 64 | ctx.app.c(tmp); 65 | setTimeout(() => { 66 | ctx.page_width = tmp.getBoundingClientRect().width; 67 | ctx.app.rm(tmp); 68 | cb(); 69 | }); 70 | } 71 | 72 | /* way/ 73 | * generate a correctly scaled canvas for each page, add it the app 74 | * and render the pdf to it. 75 | */ 76 | function generatePages(ctx, cb) { 77 | const outputScale = window.devicePixelRatio || 1; // Support HiDPI-screens 78 | const pdf = ctx.book.pdf; 79 | 80 | ctx.pages = []; 81 | gen_pg_1(0); 82 | 83 | function gen_pg_1(ndx) { 84 | if(ndx >= pdf.numPages) return cb(); 85 | 86 | const num = ndx+1; 87 | 88 | pdf.getPage(num) 89 | .then(page => { 90 | const coresz = page.getViewport({scale:1}); 91 | const scale = ctx.page_width / coresz.width; 92 | 93 | const viewport = page.getViewport({scale}); 94 | 95 | const canvas = newpage(num); 96 | canvas.attr({ "data-flipbook-page": num }); 97 | 98 | canvas.width = Math.floor(viewport.width * outputScale); 99 | canvas.height = Math.floor(viewport.height * outputScale); 100 | canvas.style.width = Math.floor(viewport.width) + "px"; 101 | canvas.style.height = Math.floor(viewport.height) + "px"; 102 | 103 | ctx.pages.push(canvas); 104 | ctx.app.add(canvas); 105 | 106 | const transform = outputScale !== 1 107 | ? [outputScale, 0, 0, outputScale, 0, 0] 108 | : null; 109 | 110 | const context = canvas.getContext("2d") 111 | const renderContext = { 112 | canvasContext: context, 113 | transform, 114 | viewport, 115 | } 116 | page.render(renderContext).promise 117 | .then(() => gen_pg_1(ndx+1)) 118 | .catch(err => cb(err || "Failed rendering page" + num)) 119 | }) 120 | .catch(err => cb(err || "Failed getting page:" + num)) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /test/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theproductiveprogrammer/flipbook-viewer/750eea07e237a3ccd5a2dee714bafde64ae7078f/test/0.png -------------------------------------------------------------------------------- /test/00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theproductiveprogrammer/flipbook-viewer/750eea07e237a3ccd5a2dee714bafde64ae7078f/test/00.png -------------------------------------------------------------------------------- /test/000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theproductiveprogrammer/flipbook-viewer/750eea07e237a3ccd5a2dee714bafde64ae7078f/test/000.png -------------------------------------------------------------------------------- /test/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theproductiveprogrammer/flipbook-viewer/750eea07e237a3ccd5a2dee714bafde64ae7078f/test/1.png -------------------------------------------------------------------------------- /test/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theproductiveprogrammer/flipbook-viewer/750eea07e237a3ccd5a2dee714bafde64ae7078f/test/2.png -------------------------------------------------------------------------------- /test/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theproductiveprogrammer/flipbook-viewer/750eea07e237a3ccd5a2dee714bafde64ae7078f/test/3.png -------------------------------------------------------------------------------- /test/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theproductiveprogrammer/flipbook-viewer/750eea07e237a3ccd5a2dee714bafde64ae7078f/test/4.png -------------------------------------------------------------------------------- /test/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theproductiveprogrammer/flipbook-viewer/750eea07e237a3ccd5a2dee714bafde64ae7078f/test/5.png -------------------------------------------------------------------------------- /test/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theproductiveprogrammer/flipbook-viewer/750eea07e237a3ccd5a2dee714bafde64ae7078f/test/6.png -------------------------------------------------------------------------------- /test/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theproductiveprogrammer/flipbook-viewer/750eea07e237a3ccd5a2dee714bafde64ae7078f/test/7.png -------------------------------------------------------------------------------- /test/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theproductiveprogrammer/flipbook-viewer/750eea07e237a3ccd5a2dee714bafde64ae7078f/test/8.png -------------------------------------------------------------------------------- /test/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theproductiveprogrammer/flipbook-viewer/750eea07e237a3ccd5a2dee714bafde64ae7078f/test/9.png -------------------------------------------------------------------------------- /test/book-imgs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* way/ 4 | * return pages from the images, caching them once loaded 5 | */ 6 | export function init(pages, cb) { 7 | const cache = [] 8 | 9 | cb(null, { 10 | numPages: () => pages.length, 11 | getPage, 12 | }) 13 | 14 | 15 | function getPage(n, cb) { 16 | if(!n || n > pages.length) return cb() 17 | if(cache[n]) return cb(null, cache[n]) 18 | 19 | const img = new Image() 20 | img.src = pages[n-1] 21 | img.addEventListener("load", () => { 22 | cache[n] = { 23 | img, 24 | num: n, 25 | width: img.width, 26 | height: img.height, 27 | } 28 | cb(null, cache[n]) 29 | }, false) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/book-pdf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import { init as pdfjsInit, getPdfjs } from './pdfjs-init.js' 3 | import { h } from '@tpp/htm-x' 4 | 5 | export function init(pdflink, cb) { 6 | pdfjsInit() 7 | const pdfjs = getPdfjs() 8 | 9 | const cache = [] 10 | 11 | pdfjs.getDocument(pdflink).promise 12 | .then(pdf => { 13 | warm_cache_1(pdf, 1) 14 | cb(null, { 15 | pdf, 16 | numPages: () => pdf.numPages, 17 | getPage: (n, cb) => get_page_1(pdf, n, cb) 18 | }) 19 | }) 20 | .catch(err => cb(err || "pdf parsing failed")) 21 | 22 | function warm_cache_1(pdf, n) { 23 | if(n <= pdf.numPages) get_page_1(pdf, n, () => warm_cache_1(pdf, n+1)) 24 | } 25 | 26 | function get_page_1(pdf, n, cb) { 27 | if(!n || n > pdf.numPages) return cb() 28 | if(cache[n]) return cb(null, cache[n]) 29 | 30 | pdf.getPage(n) 31 | .then(page => { 32 | const scale = 1.2; 33 | const viewport = page.getViewport({scale}) 34 | // Support HiDPI-screens. 35 | const outputScale = window.devicePixelRatio || 1; 36 | 37 | const canvas = h("canvas") 38 | canvas.width = Math.floor(viewport.width * outputScale); 39 | canvas.height = Math.floor(viewport.height * outputScale); 40 | canvas.style.width = Math.floor(viewport.width) + "px"; 41 | canvas.style.height = Math.floor(viewport.height) + "px"; 42 | 43 | const transform = outputScale !== 1 44 | ? [outputScale, 0, 0, outputScale, 0, 0] 45 | : null; 46 | 47 | const context = canvas.getContext("2d") 48 | const renderContext = { 49 | canvasContext: context, 50 | transform, 51 | viewport, 52 | } 53 | page.render(renderContext).promise 54 | .then(() => { 55 | const img = new Image() 56 | img.src = canvas.toDataURL() 57 | img.addEventListener("load", () => { 58 | cache[n] = { 59 | img, 60 | num: n, 61 | width: img.width, 62 | height: img.height, 63 | } 64 | cb(null, cache[n]) 65 | }, false) 66 | }) 67 | .catch(err => cb(err)) 68 | }) 69 | .catch(err => cb(err)) 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /test/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theproductiveprogrammer/flipbook-viewer/750eea07e237a3ccd5a2dee714bafde64ae7078f/test/demo.gif -------------------------------------------------------------------------------- /test/fp.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theproductiveprogrammer/flipbook-viewer/750eea07e237a3ccd5a2dee714bafde64ae7078f/test/fp.pdf -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Flip 6 | 22 | 23 | 24 |

Flipbook Viewer

25 | 26 |
27 |

Flipbook Samples

28 | 32 |

Single-Page View Sample

33 | 36 |
 
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /test/pdfjs-init.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* understand/ 3 | * pdfjs needs a worker script to be set up. 4 | * While being served from webpack-dev-server this can 5 | * be served directly from '/pdf.worker.js' but when 6 | * we move to a production environment we will need 7 | * to adjust this parameter (TODO) 8 | */ 9 | 10 | const pdfjsLib = require("pdfjs-dist") 11 | 12 | function getPdfjs() { 13 | return pdfjsLib 14 | } 15 | 16 | function init(pfx) { 17 | pfx = pfx || "" 18 | pdfjsLib.GlobalWorkerOptions.workerSrc = `${pfx}/pdf.worker.js` 19 | } 20 | 21 | module.exports = { 22 | init, 23 | getPdfjs, 24 | } 25 | 26 | -------------------------------------------------------------------------------- /test/test-imgs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Flip 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/test-imgs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* for development/hotloading */ 4 | //import * as flipbook from '../src' 5 | 6 | import * as flipbook from '../dist/flipbook-viewer.js' 7 | 8 | import * as book from './book-imgs.js' 9 | 10 | /* understand/ 11 | * main entry point into our program 12 | */ 13 | function main() { 14 | const pages = [ 15 | '/000.png', 16 | '/1.png', 17 | '/2.png', 18 | '/6.png', 19 | '/3.png', 20 | '/8.png', 21 | '/9.png', 22 | '/4.png', 23 | '/5.png', 24 | '/7.png', 25 | ] 26 | 27 | const next = document.getElementById('next') 28 | const prev = document.getElementById('prev') 29 | const zoom = document.getElementById('zoom') 30 | 31 | book.init(pages, (err, book) => { 32 | if(err) console.error(err) 33 | else flipbook.init(book, 'app', (err, viewer) => { 34 | if(err) console.error(err) 35 | 36 | viewer.on('seen', n => console.log('page number: ' + n)) 37 | 38 | next.onclick = () => viewer.flip_forward(); 39 | prev.onclick = () => viewer.flip_back(); 40 | zoom.onclick = () => viewer.zoom(); 41 | 42 | }) 43 | }) 44 | 45 | } 46 | 47 | main() 48 | -------------------------------------------------------------------------------- /test/test-pdf-sp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | View Book 6 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/test-pdf-sp.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* for development/hotloading */ 4 | //import * as flipbook from '../src' 5 | 6 | import * as flipbook from '../dist/flipbook-viewer.js' 7 | 8 | import * as book from './book-pdf.js' 9 | 10 | /* understand/ 11 | * main entry point into our program 12 | */ 13 | function main() { 14 | const opts = { 15 | singlepage: true, 16 | }; 17 | 18 | const app = document.getElementById('app'); 19 | 20 | book.init('/fp.pdf', (err, book) => { 21 | if(err) console.error(err); 22 | else flipbook.init(book, app, opts, (err, viewer) => { 23 | if(err) return console.error(err); 24 | 25 | viewer.on('seen', n => console.log('page number: ' + n)) 26 | 27 | window.viewbook = viewer 28 | }) 29 | }) 30 | 31 | } 32 | 33 | main() 34 | -------------------------------------------------------------------------------- /test/test-pdf.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Flip 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/test-pdf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* for development/hotloading */ 4 | //import * as flipbook from '../src' 5 | 6 | import * as flipbook from '../dist/flipbook-viewer.js' 7 | 8 | import * as book from './book-pdf.js' 9 | 10 | /* understand/ 11 | * main entry point into our program 12 | */ 13 | function main() { 14 | const opts = { 15 | width: 800, 16 | height: 600, 17 | } 18 | 19 | const app = document.getElementById('app') 20 | const next = document.getElementById('next') 21 | const prev = document.getElementById('prev') 22 | const zoom = document.getElementById('zoom') 23 | 24 | book.init('/fp.pdf', (err, book) => { 25 | if(err) console.error(err) 26 | else flipbook.init(book, app, opts, (err, viewer) => { 27 | if(err) return console.error(err) 28 | 29 | viewer.on('seen', n => console.log('page number: ' + n)) 30 | 31 | next.onclick = () => viewer.flip_forward(); 32 | prev.onclick = () => viewer.flip_back(); 33 | zoom.onclick = () => viewer.zoom(); 34 | 35 | }) 36 | }) 37 | 38 | } 39 | 40 | main() 41 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | 4 | const base = { 5 | entry: { 6 | 'flipbook-viewer': { 7 | import: './src/index.js', 8 | library: { 9 | name: 'flipbook', 10 | type: 'umd', 11 | umdNamedDefine: true, 12 | }, 13 | }, 14 | 'pdf.worker': 'pdfjs-dist/build/pdf.worker.entry', 15 | }, 16 | output: { 17 | filename: '[name].js', 18 | path: path.resolve(__dirname, 'dist'), 19 | clean: true, 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.svg$/, 25 | loader: 'svg-inline-loader' 26 | } 27 | ] 28 | } 29 | } 30 | 31 | const dev = Object.assign({}, base, { 32 | mode: 'development', 33 | devtool: 'inline-source-map', 34 | devServer: { 35 | static: './test', 36 | allowedHosts: 'testing-page-flip.com', 37 | }, 38 | 39 | entry: Object.assign({ 40 | 'test-imgs': './test/test-imgs.js', 41 | 'test-pdf': './test/test-pdf.js', 42 | 'test-pdf-sp': './test/test-pdf-sp.js', 43 | }, base.entry), 44 | 45 | }) 46 | 47 | const prod = Object.assign({}, base, { 48 | mode: "production", 49 | }) 50 | 51 | module.exports = env => { 52 | if(!env.production && env.development) return dev 53 | else return prod 54 | } 55 | --------------------------------------------------------------------------------