panel-extraction / reader.js
yms9654's picture
add kumiko files
58da40f
class Reader {
constructor(options)
{
// Mandatory options
if (!options.container || options.container.length == 0) {
console.error('no container given in options');
return;
}
this.gui = options.container;
this.gui.data('reader',this);
if (!options.images_dir) {
console.error('no images_dir given in options');
return;
}
this.images_dir = options.images_dir;
if (!options.comicsJson || typeof options.comicsJson != 'object') {
console.error('no comicsJson given in options, or not a javascript object');
return;
}
this.comic = options.comicsJson;
this.known_panels = options.known_panels ? options.known_panels : [];
// init attributes
this.currpage = 0;
this.currpanel = 0;
this.debug = this.gui.hasClass('debug') || 'debug' in this.getHashInfo();
if (this.debug)
this.gui.addClass('debug');
// add image sub-container
this.container = $('<div class="container"/>');
this.container.css({
position: 'relative',
width: '95%',
height: '95%',
margin: 'auto',
});
this.gui.append(this.container);
if (options['controls'])
this.add_controls();
window.addEventListener('orientationchange', function () {
setTimeout( function () { this.gotoPanel(this.currpanel); }.bind(this), 500); // slight delay to make it work better, not sure why :)
}.bind(this));
}
getHashInfo ()
{
var hash = window.location.hash.replace(/^#/,'').split(',');
var info = {};
for (var i in hash)
{
var h = hash[i].split('=');
info[h[0]] = h[1];
}
return info;
}
setHashInfo (newinfo)
{
var info = this.getHashInfo();
for (var i in newinfo)
info[i] = newinfo[i];
info = Object.entries(info).map(function (kv) {
if (kv[1] === null)
return '';
return kv[0] + (kv[1] ? '=' + kv[1] : '');
}).filter(function(v){return v?true:false});
window.location.hash = info.join(',');
}
start ()
{
var page = this.getHashInfo()['page'];
if (!page)
page = 0;
this.loadPage(page);
}
next()
{
if (this.container.is('.zoomed'))
this.nextPanel();
else
this.loadNextPage();
}
prev()
{
if (this.container.is('.zoomed'))
this.prevPanel();
else
this.loadPrevPage();
}
loadNextPage() { return this.loadPage(this.currpage+1); }
loadPrevPage() { return this.loadPage(this.currpage-1); }
loadPage(page=0)
{
page = parseInt(page);
// don't go to a page below 0, or above the number of pages in this comic
if (page < 0 || page >= this.comic.length)
return false;
this.currpage = page;
this.setHashInfo({page: page ? page : null});
$('.pagenb',this.gui).html('page '+(page+1)+' <small>/'+this.comic.length+'</small>')
var imginfo = this.comic[page];
if (!this.gui.hasClass('fullpage'))
this.gui.css({
width: imginfo.size[0],
height: imginfo.size[1],
padding: '2em',
});
var imgurl = this.images_dir == 'urls' ? imginfo.filename : this.images_dir + imginfo.filename.split('/').reverse()[0];
var img = $('<img class="pageimg" src="'+imgurl+'"/>');
img.css({
position: 'absolute',
'width': '100%',
'height': '100%'
});
this.container.children('img').remove();
this.container.prepend(img);
// show license
var license = this.get_license(imginfo);
if (license)
{
this.gui.append('<span class="license"/>');
this.gui.children('.license').html(license);
}
else
$('.license',this.gui).remove();
var was_zoomed = this.container.is('.zoomed');
this.drawPanels(imginfo);
this.dezoom();
if (this.currpanel == 'last')
this.currpanel = $('.panel').length-1;
else
this.currpanel = 0;
if (was_zoomed)
this.gotoPanel(this.currpanel);
return true;
}
zoomOn (panel)
{
var growth = this.container.parent().width() / panel.width();
var max = 'x';
var ygrowth = this.container.parent().height() / panel.height();
if (ygrowth < growth) {
growth = ygrowth;
max = 'y';
}
var newcss = {
top: - panel.position().top * growth,
left: - panel.position().left * growth,
height: this.container.height() * growth,
width: this.container.width() * growth
};
// center panel horizontally or vertically within container's parent
if (max == 'x')
newcss.top += (this.container.parent().height() - panel.height() * growth) / 2;
else
newcss.left += (this.container.parent().width() - panel.width() * growth) / 2;
$('.panel.zoomTarget').removeClass('zoomTarget');
panel.addClass('zoomTarget');
this.container.addClass('zoomed');
this.container.animate(newcss,300);
}
dezoom ()
{
var size = this.getImgSize();
var newcss = {
width: size.w,
height: size.h,
left: 0,
top: 0
};
this.container.removeClass('zoomed');
this.container.css(newcss);
}
getImgSize ()
{
var size = {
w: this.container.parent().width(),
h: this.container.parent().height()
};
var imgsize = this.comic[this.currpage]['size'];
var ratio = imgsize[0] / imgsize[1];
if (size.w > size.h * ratio)
size.w = size.h * ratio;
else if (size.h > size.w / ratio)
size.h = size.w / ratio;
return size;
}
gotoPanel (i)
{
if (i < 0) {
this.currpanel = 'last';
var prevpage = this.loadPrevPage();
if (!prevpage)
this.currpanel = 0;
return;
}
var newPanel = $('.panel').eq(i);
if (newPanel.length > 0)
this.zoomOn(newPanel);
else {
var nextpage = this.loadNextPage();
if (!nextpage)
this.currpanel--;
}
}
nextPanel () { this.currpanel++; this.gotoPanel(this.currpanel); }
prevPanel () { this.currpanel--; this.gotoPanel(this.currpanel); }
drawPanels (imginfo)
{
this.container.children('*:not(.pageimg)').remove();
var [imgw,imgh] = imginfo['size'];
var i = 1;
for (var p in imginfo['panels'])
{
var unknown = !this.known_panels.includes(parseInt(p));
p = imginfo['panels'][p];
var [x,y,w,h] = p;
var panel = $('<div class="panel"><!-- --></div>');
if (unknown)
panel.addClass('unknown');
var panelcss = {
top: '' + y/imgh*100 + '%',
left: '' + x/imgw*100 + '%',
height: '' + h/imgh*100 + '%',
width: '' + w/imgw*100 + '%'
};
panel.css(panelcss);
panel.append('<span class="panelnb">'+(i++)+'</span>');
panel.append('<span class="top">'+y);
panel.append('<span class="bottom">'+(y+h));
panel.append('<span class="left">'+x);
panel.append('<span class="right">'+(x+w));
this.container.append(panel);
}
}
add_controls()
{
var _reader = this;
// add controls and page info
this.gui.append('<span class="pagenb"/>');
var burger = $('<i class="burger">☰</i>');
burger.data('reader',this);
burger.on('click touch', function (e) { $(this).data('reader').showMenu(); });
this.gui.append(burger);
var btprev = $('<i class="prev">←</i>');
btprev.data('reader',this);
btprev.on('click touch', function (e) { $(this).data('reader').prev(); });
this.gui.append(btprev);
var menu = $('<div class="menu"/>');
var menuul = $('<ul/>');
menuul.append('<li><label><input type="radio" name="viewmode" value="page" autocomplete="off" />Page</label></li>');
menuul.append('<li><label><input type="radio" name="viewmode" value="panel" autocomplete="off" />Panel</label></li>');
menu.append(menuul);
var btn_debug = $('<label><input type="checkbox" class="toggleDebug" autocomplete="off" />Show panels</label>');
if (this.debug)
btn_debug.children('.toggleDebug').prop('checked',true);
btn_debug.children('.toggleDebug').on('change', function () {
_reader.gui.toggleClass('debug');
_reader.setHashInfo({'debug': _reader.gui.hasClass('debug') ? '' : null});
});
menuul.append(btn_debug);
menuul.append('<i class="exit">X</i>');
menuul.children('.exit').on('click touch', function () { _reader.showMenu(false); })
menuul.append(' \
<li>---</li> \
<li class="kbd"><span>click/tap</span><span>next page/panel</span></li> \
<li class="kbd"><span>right/down</span><span>next page/panel</span></li> \
<li class="kbd"><span>left/up</span><span>previous page/panel</span></li> \
<li class="kbd"><span>p</span><span>switch page/panel view</span></li> \
<li class="kbd"><span>m</span><span>show this menu</span></li> \
<li class="kbd"><span>d</span><span>debug (show panels)</span></li> \
');
this.gui.append(menu);
this.showMenu(false);
$(document).ready( function () {
var mode = _reader.debug ? 'page' : 'panel';
$('input[name=viewmode][value='+mode+']', _reader.gui).prop('checked',true).change();
});
}
get_license(page)
{
if (!page.license)
return '';
var html = [];
for (var i=0; i < 3; i++)
{
var k = ['title','author','license'][i];
if (k in page.license)
{
var elt = page.license;
html.push(
{title:'',author:'by ',license:''}[k] +
(elt[k+'_link'] ? '<a target="_blank" href="'+elt[k+'_link']+'">'+elt[k]+'</a>' : elt[k])
);
}
}
return html.join(', ');
}
showMenu(show=true)
{
var menu = this.gui.children('.menu');
if (show == 'toggle')
show = !menu.is(':visible');
show ? menu.show() : menu.hide();
}
}
// Change view mode
$(document).delegate( 'input[name=viewmode]', 'change', function () {
if (!$(this).is(':checked'))
return;
var reader = $(this).parents('.kumiko-reader:eq(0)').data('reader');
switch ($(this).val()) {
case 'page':
reader.dezoom();
break;
case 'panel':
reader.gotoPanel(0);
break;
}
reader.container.focus();
});
// Next panel on simple click
$(document).delegate( '.kumiko-reader', 'click touch', function (e) {
if ($(e.target).is('.panel,.kumiko-reader'))
$(this).data('reader').next();
});
// Prevent click on page when clicking on license links
$(document).delegate( '.license a', 'click touch', function (e) {
e.stopPropagation();
});
/**** KEYBOARD NAVIGATION ****/
$(document).keydown(function(e) {
if (e.altKey) // alt+left is shortcut for previous page, don't prevent it
return;
switch(e.which) {
case 37: // left
case 38: // up
reader.prev();
break;
case 39: // right
case 40: // down
reader.next();
break;
case 68: // 'd' debug (show panels)
var d = $('input.toggleDebug');
d.prop('checked',!d.is(':checked')).change();
break;
case 77: // 'm' key: toggle menu
reader.showMenu('toggle');
break;
case 80: // 'p' key: switch between page and panel reading
$('input[name=viewmode]:not(:checked)').prop('checked',true).change();
break;
default:
return; // exit this handler for other keys
}
e.preventDefault();
});