I'm finally just about finished with the storybook program I was working on over the summer. The goal was to make a fancier way to read my stories by having a book which the user would click through to different pages. You can see a working sample of it here. There's just a couple of bugs that I can't seem to fix. The biggest one is that, occasionally, a page repeats the first couple of characters from the previous page. Unfortunately, I have not been able to figure out why it happens, nor how to fix it. Here's the full code, if you need it. I'm going to pull out the relavant functions, though, so don't worry about reading it. [hide] var story; //The html text of the story. Except, only sort of html, since only <br>'s are acceptable.
var firstChar=0; //The first character of the left page.
var lastChar=0; //The last character of the right page.
//Opens the story to the storybook.
//var path: file path to pull story text from.
function openBook(path){
//Create a translucent background to dim the main site.
var back = document.createElement("div");
back.setAttribute("id", "dimmer");
back.style.backgroundColor="rgba(0,0,0,0.75)"; //Sets color to black and 75% opacity
back.style.width="100%";
back.style.height="100%";
back.style.position="fixed";
back.style.left="0px";
back.style.top="0px";
document.body.appendChild(back);
//Creates bookmark object, which will allow user to save his position in the book.
var bookmark = document.createElement("div");
bookmark.setAttribute("id","bookmark");
bookmark.onclick=bookmarkPage;
//Moves bookmark up/down to emphasize that it's clickable.
bookmark.onmouseover=function(){bookmark.style.top="-10px";}
bookmark.onmouseout=function(){bookmark.style.top="0px";}
bookmark.style.width="125px";
bookmark.style.height="600px";
bookmark.style.position="fixed";
bookmark.style.margin="auto auto";
bookmark.style.right="0px";
bookmark.style.left="125px";
bookmark.style.top="0px";
bookmark.style.bottom="90px";
bookmark.style.backgroundImage='url("http://static.tumblr.com/a14d5c1e7ef40629024ff55c737c4a7c/x7vxvm9/jjPms2aj0/tumblr_static_bookmark.png")';
bookmark.style.backgroundPositionY="top";
bookmark.style.backgroundRepeat="no-repeat";
back.appendChild(bookmark);
//Main storybook container. 786*582 px
var div = document.createElement("div");
div.setAttribute("id","storybook");
div.style.width = "786px";
div.style.height = "582px";
div.style.position="fixed";
div.style.right="0px";
div.style.left="0px";
div.style.top="0px";
div.style.bottom="0px";
div.style.margin="auto auto";
div.style.fontSize="16px";
div.style.backgroundImage='url("http://static.tumblr.com/000d4fa9d90bfcf43fce181a6bddcc7c/x7vxvm9/IDMms2ak2/tumblr_static_storybook.png")';
back.appendChild(div);
//Left and right pages, respectively. Each is 350*528, with a bit of space between.
var leftPage = document.createElement("div");
leftPage.setAttribute("id","leftPage");
leftPage.style.height="528px";
leftPage.style.width="350px";
leftPage.style.top="25px";
leftPage.style.left="12px";
leftPage.style.position="absolute";
div.insertBefore(leftPage);
var rightPage = document.createElement("div");
rightPage.setAttribute("id","rightPage");
rightPage.style.height="528px";
rightPage.style.width="350px";
rightPage.style.top="25px";
rightPage.style.right="12px";
rightPage.style.position="absolute";
div.appendChild(rightPage);
//Clicking this button will close the storybook program.
var closeButton = document.createElement("div");
closeButton.setAttribute("id","closeButton");
closeButton.onclick=closeBook;
closeButton.style.backgroundImage='url("http://static.tumblr.com/31529799c067d0559bc6ada471ada49a/x7vxvm9/okvms2aj3/tumblr_static_closebutton.png")';
closeButton.style.width="28px";
closeButton.style.height="28px";
closeButton.style.position="fixed";
closeButton.style.margin="auto auto";
closeButton.style.right="0px";
closeButton.style.left="-760px";
closeButton.style.top="-582px";
closeButton.style.bottom="0px";
closeButton.style.cursor="pointer";
back.appendChild(closeButton);
//Sits on left page, goes to previous page.
var prevButton = document.createElement("div");
prevButton.setAttribute("id","prevButton");
prevButton.onclick=prevPage;
prevButton.onmouseover=function(){this.style.backgroundImage="url('http://static.tumblr.com/0dfec6b59cfb1fdc56cb0bed3c70e8b1/x7vxvm9/3uOms2ajh/tumblr_static_left_page_turn.png')"}
prevButton.onmouseout=function(){this.style.backgroundImage="url('http://static.tumblr.com/c5e192b9d0af1287d97a19266e86b05a/x7vxvm9/XxTms2aj9/tumblr_static_left_page_arrow.png')"}
prevButton.style.width="70px";
prevButton.style.height="110px";
prevButton.style.position="absolute";
prevButton.style.bottom="0px";
prevButton.style.left="0px";
prevButton.style.backgroundImage="url('http://static.tumblr.com/c5e192b9d0af1287d97a19266e86b05a/x7vxvm9/XxTms2aj9/tumblr_static_left_page_arrow.png')";
div.appendChild(prevButton);
//Sits on right page, goes to next page.
var nextButton = document.createElement("div");
nextButton.setAttribute("id","nextButton");
nextButton.onclick=nextPage;
nextButton.onmouseover=function(){this.style.backgroundImage="url('http://static.tumblr.com/e671d833b2a84c6fc6bf0625ace14aed/x7vxvm9/y00ms2ajq/tumblr_static_right_page_turn.png')"}
nextButton.onmouseout=function(){this.style.backgroundImage="url('http://static.tumblr.com/5304bd201b95f803f192ab6263470907/x7vxvm9/Ltcms2ajm/tumblr_static_right_page_arrow.png')"}
nextButton.style.width="70px";
nextButton.style.height="110px";
nextButton.style.position="absolute";
nextButton.style.bottom="0px";
nextButton.style.right="0px";
nextButton.style.backgroundImage="url('http://static.tumblr.com/5304bd201b95f803f192ab6263470907/x7vxvm9/Ltcms2ajm/tumblr_static_right_page_arrow.png')";
div.appendChild(nextButton);
//Open the story from the given file path.
openStory(path);
//If there's a bookmark cookie, open it. This will set firstChar to whatever it was when the user bookmarked, so firstPage will open that page instead of the page starting at firstChar=0
openBookmark();
//Open the first two pages after firstChar.
firstPage();
}
//Close the book by removing the background dimmer, under which all the other elements are child nodes.
function closeBook(){
document.body.removeChild( document.getElementById("dimmer") );
}
//Save a bookmark containing the firstChar which the user was on. Expires in 30 days.
function bookmarkPage(){
var exdate=new Date();
exdate.setDate(exdate.getDate() + 30);
document.cookie="firstChar="+firstChar+"; expires="+exdate.toUTCString();
closeBook();
}
//Checks if there is a bookmark cookie. If so, firstChar gets set there.
function openBookmark(){
var fcStart, fcEnd, cookieString;
cookieString=document.cookie; //Gets page's cookies.
fcStart=cookieString.indexOf("firstChar="); //Finds where the firstChar cookie starts.
//If no firstChar cookie is there, don't continue.
if (fcStart==-1){
return null;
}
//Figure out where the string that firstChar should be set to starts and ends. Looks relative to the first instance of "firstChar=".
fcStart=cookieString.indexOf("=",fcStart)+1;
fcEnd=cookieString.indexOf(";",fcStart);
if (fcEnd==-1)
fcEnd=cookieString.length;
//Get the substring with firstChar's value, convert to an integer.
firstChar=parseInt( cookieString.substring(fcStart,fcEnd) );
}
//Opens the story and saves it to the global story.
//var path: file path to pull story text from.
function openStory(path){
xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET",path,false);
xmlhttp.send(null);
var lines = xmlhttp.responseText.split("[[zxjz]]");
story=lines[1];
}
//Open the pages which start at firstChar.
function firstPage(){
//end: last character of the first page +1. Where the second page will start.
//insertSizedString: returns the number of characters on the first page, which is how far from firstChar the second page should start.
var end=firstChar+insertSizedString( story.substring(firstChar) ,document.getElementById("leftPage") );
//If the first page is not also the last page
if (end<story.length-1)
lastChar=end+ insertSizedString( story.substring(end) , document.getElementById("rightPage") );
else{//If first page is also the last page
//Remove the button to go to the next page.
document.getElementById("nextButton").style.visibility="hidden";
lastChar=story.length-1;
//Clear previous page from the right page.
document.getElementById("rightPage").innerHTML="";
}
//If the firstChar is at the beginnning of the story, remove the button to go to the previous page.
if (firstChar<=120){
document.getElementById("prevButton").style.visibility="hidden";
firstChar=0;
}
}
//Go to the page starting at lastChar. Operates from the same principle as firstPage().
function nextPage(){
firstChar=lastChar;
var end=firstChar+insertSizedString( story.substring(firstChar) ,document.getElementById("leftPage") );
if (end<=story.length-1){
lastChar=end+insertSizedString( story.substring(end) , document.getElementById("rightPage") );
if (lastChar>=story.length-1)
document.getElementById("nextButton").style.visibility="hidden";
}else{
document.getElementById("nextButton").style.visibility="hidden";
lastChar=story.length-1;
document.getElementById("rightPage").innerHTML="";
}
if (lastChar>=story.length*.9)
alert(lastChar+"/"+story.length);
document.getElementById("prevButton").style.visibility="visible";
}
//Opens the page which ends right before firstChar.
function prevPage(){
lastChar=firstChar;
firstChar=lastChar-insertSizedStringReversed( story.substring(0,lastChar),document.getElementById("leftPage"),document.getElementById("rightPage") );
document.getElementById("nextButton").style.visibility="visible";
if (firstChar==0){
document.getElementById("prevButton").style.visibility="hidden";
}
}
function insertSizedString(str, element){
var cont=element.appendChild(document.createElement("div"));
cont.style.width=element.style.width;
cont.innerHTML=str;
while (cont.clientHeight>element.clientHeight*2){
cont.innerHTML=cont.innerHTML.substring(0, cont.innerHTML.length/2);
}
cont.innerHTML=cont.innerHTML.substring(0, cont.innerHTML.lastIndexOf(" "));
while ( cont.clientHeight>element.clientHeight ){
cont.innerHTML=cont.innerHTML.substring(0, cont.innerHTML.lastIndexOf(" "));
}
element.innerHTML=cont.innerHTML;
//alert(element.innerHTML+" "+element.innerHTML.length);
return element.innerHTML.length;
}
function insertSizedStringReversed(str,element1,element2){
var cont=element2.appendChild(document.createElement("div"));
cont.style.width=element2.style.width;
cont.innerHTML=str;
while (cont.clientHeight>(element2.clientHeight+element1.clientHeight)*2.05){
cont.innerHTML=cont.innerHTML.substring( cont.innerHTML.length/2-1 );
}
while ( cont.clientHeight>(element2.clientHeight+element1.clientHeight) ){
cont.innerHTML=cont.innerHTML.substring( cont.innerHTML.indexOf(" ")+1 );
}
var bothPages=cont.innerHTML;
while ( cont.clientHeight>element1.clientHeight ){
cont.innerHTML=cont.innerHTML.substring(0, cont.innerHTML.lastIndexOf(" "));
}
element1.innerHTML=cont.innerHTML;
element2.innerHTML=bothPages.substring(cont.innerHTML.length);
return element1.innerHTML.length+element2.innerHTML.length;
}[/hide] Essentially, the important pieces are the two functions for going to the next page. story, firstChar, and lastChar are all global variables, containing the text of the story as a string, the location in the string of the first character of the left page, and the last character of the right page, respectively. //Go to the page starting at lastChar. Operates from the same principle as firstPage().
function nextPage(){
firstChar=lastChar;
//end: last character of the first page +1. Where the second page will start.
//insertSizedString: returns the number of characters on the first page, which is how far from firstChar the second page should start.
var end=firstChar+insertSizedString( story.substring(firstChar) ,document.getElementById("leftPage") );
//If the first page is not also the last page
if (end<=story.length-1){
lastChar=end+insertSizedString( story.substring(end) , document.getElementById("rightPage") );
/*(This is tabbed in)*/if (lastChar>=story.length-1){document.getElementById("nextButton").style.visibility="hidden"; }
}else{//If first page is also the last page
//Remove the button to go to the next page.
document.getElementById("nextButton").style.visibility="hidden";
lastChar=story.length-1;
//Clear previous page from the right page.
document.getElementById("rightPage").innerHTML="";
}
//Debugging
if (lastChar>=story.length*.9)
alert(lastChar+"/"+story.length);
document.getElementById("prevButton").style.visibility="visible";
}
//Inserts a substring of str into element that is exactly as long as can fit into element without overflow.
function insertSizedString(str, element){
var cont=element.appendChild(document.createElement("div"));//Create a container under the element to test the dimensions of the text.
cont.style.width=element.style.width;
cont.innerHTML=str; //The system works backwards, from the full str until it fits into element.
//Since the function is O(n^2) otherwise, cut the problem to a more reasonable length. This isn't perfect - there's still a noticeable pause - but there's an infinite loop otherwise
while (cont.clientHeight>element.clientHeight*2){
cont.innerHTML=cont.innerHTML.substring(0, cont.innerHTML.length/2);
}
cont.innerHTML=cont.innerHTML.substring(0, cont.innerHTML.lastIndexOf(" "));
//Remove parts of the string, at word breaks, until the container is only as tall as element, at which point all the text will fit.
while ( cont.clientHeight>element.clientHeight ){
cont.innerHTML=cont.innerHTML.substring(0, cont.innerHTML.lastIndexOf(" "));
}
element.innerHTML=cont.innerHTML;
//Return the length of the inserted string, so that the system knows where to start the next page.
return element.innerHTML.length;
}
I know the problem has to do with how end is getting assigned - somehow, despite the fact that firstChar is assigned correctly and the correct length is being returned, end is not in the correct position. I thought it might have to do with the fact that I have non breaking spaces to mark tabs, but it isn't consistent between pages that start with them/have them and pages that don't. After that, I'm not sure - it's not caused by formatting marks, or anything else I can determine. On a slightly different note, this program is a little slow. There's a pause when you change pages. I'm sure it's because, as I noted in the comments, the insertSizedString() function is O(n2), because substring appears to just be a loop. Originally, insertSizedString() ran forward, adding words until cont was too tall, then removing the last word and assigning the result to element, but that came with a huge number of bugs, so I scrapped it. I can't come up with another system by which to solve this problem, so I'm running with this unless someone can show me a better way, or show me a way to make this more efficient. There's a few other bugs, but I think I'm just going to leave them be if fixing this doesn't fix those. They're not very noticeable, and they're rare, so I'm not worried about them. But this one is, and I need it to be fixed. So can anyone offer any help? edit: Oh. I broke the code when I posted. One second. >.>