There are also open permissions versions of the Cory Doctorow books Eastern Standard Tribe and Down and Out in the Magic Kingdom available. If you'd like to see the books in operation, and examine the code in game, you can find them on the picnic table near Louise (178,175).
It's pretty rough, I know, but perhaps someone else can get some use out of it for their own SL book making projects. I'm posting it here instead of the script library, because it's of limited applicability and still in need of development. I've stopped working on it (the voting public at the Expo preferred textured prim readers, and I've moved on to other projects), but please feel free to improve it however you'd like. This script is offered as-is, with no further support or guarantees of any kind.
Yes, I know the web prim is coming up relatively soon, thank you.
CODE
// HoloReader by Moriash Moreau, 2005
// Please feel free to use this code however you'd like.
// Credit for the original code would be nice.
// This is offered as-is.
// BaseOffset is minimum ideal position above the book.
// About 60% of the height of the page is a good start.
float Offset = 0.85;
float BaseOffset = 0.85;
// Set X equal to the desired width, Y to desired height.
// Adjust as needed to keep height-to-width ratio right.
vector PageScale = <1.0,1.4655,0>;
// Chapter page list. The first is the prologue.
list ChapterPage = [2,10,17,22,44,62,85,104,124,142,159];
// About the Author page.
integer AuthorPage = 164;
vector Height;
integer Chapters = FALSE;
string Picture;
string Description;
integer count = 0;
integer BookMark = 1;
integer PageCount = 165;
key BookUser;
key Target;
key DataKey1;
key DataKey2;
integer CommChannel;
integer Listener;
doTakeControls()
{
llInstantMessage(BookUser,"Use Page Up and Page Down to turn pages. This book is best read in Mouselook.");
llInstantMessage(BookUser,"Touch the book again to bring up the additional functions menu.");
llTakeControls(
CONTROL_UP | CONTROL_DOWN,
// These are the controls we are taking,
// if the user presses or releases any
// of these keys (or buttons) then
// the control() event will be triggered
TRUE, // accept, TRUE if we want to receive these keys,
// FALSE to release these controls (you can set this to
// FALSE to release some controls without releasing all
// controls (you would use llReleaseControls() to do that))
FALSE // pass_on, if TRUE, the avatar also receives this input
// and therefore moves. For vehicle-like scripts, set this
// to FALSE
);
}
// startup - check to see if we have PERMISSION_TAKE_CONTROLS, if we
// do, take the controls, if not, request the permission
startup()
{
integer nMyPerms;
nMyPerms = llGetPermissions();
if (nMyPerms & PERMISSION_TAKE_CONTROLS)
{
doTakeControls();
}
else
{
llRequestPermissions(BookUser, PERMISSION_TAKE_CONTROLS);
}
}
SetPage()
{
if (count == 0)
{
llSetText("Cover",<0,0,0>,1.0);
}
else
{
llSetText("Page " + (string)count,<0,0,0>,1.0);
}
Chapters = FALSE;
DataKey1 = llGetNotecardLine("pagelist", count);
}
ChapterSkip()
{
CommChannel = (integer)(llFrand(2400000) + 1);
Listener = llListen(CommChannel,"",BookUser,"");
llDialog(BookUser,"Select a Chapter.", ["8","9","10","6","7","8","3","4","5","Prologue","1","2"],CommChannel);
llListen(CommChannel, "", BookUser, ""); // listen for the dialog answer
Chapters = FALSE;
}
// Jopsy's Particle Script.
makeImage(string Picture, float Offset, vector PageScale)
{
llParticleSystem([]);
llParticleSystem( [
// Appearance Settings
PSYS_PART_START_SCALE,(vector) PageScale,// Start Size, (minimum .04, max 10.0?)
PSYS_PART_END_SCALE,(vector) PageScale, // End Size, requires *_INTERP_SCALE_MASK
PSYS_PART_START_COLOR,(vector) <1,1,1>, // Start Color, (RGB, 0 to 1)
PSYS_PART_END_COLOR,(vector) <1,1,1>, // End Color, requires *_INTERP_COLOR_MASK
PSYS_PART_START_ALPHA,(float) 1.0, // startAlpha (0 to 1),
PSYS_PART_END_ALPHA,(float) 1.0, // endAlpha (0 to 1)
PSYS_SRC_TEXTURE,(string) Picture, // name of a 'texture' in emitters inventory
// Flow Settings, keep (age/rate)*count well below 4096 !!!
PSYS_SRC_BURST_PART_COUNT,(integer) 16, // # of particles per burst
PSYS_SRC_BURST_RATE,(float) 0.1, // delay between bursts
PSYS_PART_MAX_AGE,(float) 1.2, // how long particles live
PSYS_SRC_MAX_AGE,(float) 0,//15.0*60.0, // turns emitter off after 15 minutes. (0.0 = never)
// Placement Settings
PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_ANGLE_CONE,
// _PATTERN can be: *_EXPLODE, *_DROP, *_ANGLE, *ANGLE_CONE or *_ANGLE_CONE_EMPTY
PSYS_SRC_BURST_RADIUS,(float) Offset, // How far from emitter new particles start,
PSYS_SRC_INNERANGLE,(float) 0.0, // aka 'spread' (0 to 2*PI),
PSYS_SRC_OUTERANGLE,(float) 0.0, // aka 'tilt' (0(up), PI(down) to 2*PI),
PSYS_SRC_OMEGA,(vector) <0,0,0>, // how much to rotate around x,y,z per burst,
// Movement Settings
PSYS_SRC_ACCEL,(vector) <0,0,0>, // aka gravity or push, ie <0,0,-1.0> = down
PSYS_SRC_BURST_SPEED_MIN,(float) 0, // Minimum velocity for new particles
PSYS_SRC_BURST_SPEED_MAX,(float) 0, // Maximum velocity for new particles
// PSYS_SRC_TARGET_KEY,(key) llGetOwner(), // key of a target, requires *_TARGET_POS_MASK
// for *_TARGET try llGetKey(), or llGetOwner(), or llDetectedKey(0) even. :)
PSYS_PART_FLAGS, // Remove the leading // from the options you want enabled:
//PSYS_PART_EMISSIVE_MASK | // particles glow
//PSYS_PART_BOUNCE_MASK | // particles bounce up from emitter's 'Z' altitude
//PSYS_PART_WIND_MASK | // particles get blown around by wind
//PSYS_PART_FOLLOW_VELOCITY_MASK | // particles rotate towards where they're going
//PSYS_PART_FOLLOW_SRC_MASK | // particles move as the emitter moves
//PSYS_PART_INTERP_COLOR_MASK | // particles change color depending on *_END_COLOR
//PSYS_PART_INTERP_SCALE_MASK | // particles change size using *_END_SCALE
//PSYS_PART_TARGET_POS_MASK | // particles home on *_TARGET key
0 // Unless you understand binary arithmetic, leave this 0 here. :)
] );
}
default
{
state_entry()
{
llParticleSystem([]);
llSensorRemove();
llGetNumberOfNotecardLines("pagelist");
llSetText("",<0,0,0>,1.0);
}
on_rez(integer start_param)
{
llParticleSystem([]);
llGetNumberOfNotecardLines("pagelist");
llSetText("",<0,0,0>,1.0);
}
touch_start(integer total_number)
{
BookUser = llDetectedKey(0);
Height = llDetectedPos(0) - llGetPos() + llGetAgentSize(BookUser)/2;
Offset = Height.z - BaseOffset;
if (Offset < BaseOffset)
{
Offset = BaseOffset;
}
state read;
}
dataserver(key queryid, string data)
{
PageCount = (integer)data - 1;
}
}
state read
{
state_entry()
{
llParticleSystem([]);
llSensorRepeat(llKey2Name(BookUser), BookUser, AGENT, 6, PI, 30);
startup();
count = 0;
SetPage();
}
on_rez(integer start_param)
{
state default;
}
touch_start(integer total_number)
{
if (llDetectedKey(0) == BookUser)
{
CommChannel = (integer)(llFrand(2400000) + 1);
Listener = llListen(CommChannel,"",BookUser,"");
llDialog(BookUser,"Additional Book Functions:", ["Chapters", "Mark Page", "To Mark", "Author", "License", "Close Book"],CommChannel);
llListen(CommChannel, "", BookUser, ""); // listen for the dialog answer
}
else
{
llInstantMessage(llDetectedKey(0),"This book is currently in use, but feel free to take a copy of your own.");
}
}
listen(integer channel, string name, key id, string message)
{
if (message == "Close Book")
{
llReleaseControls();
llParticleSystem([]);
llSleep(1.0);
BookUser = "";
state default;
}
else if (message == "Mark Page")
{
if (BookUser == llGetOwner())
{
BookMark = count;
llSetObjectDesc("Marked on page " + (string)BookMark + ".");
llInstantMessage(BookUser, "Bookmarking page " + (string)BookMark + ".");
}
else
{
llInstantMessage(BookUser,"Sorry, only the book owner can set a bookmark.");
llInstantMessage(BookUser,"Please feel free to take a copy of your own.");
}
}
else if (message == "To Mark")
{
Description = llGetObjectDesc();
count = (integer)llGetSubString(Description, 15, llStringLength(Description) - 2);
SetPage();
llInstantMessage(BookUser, "Jumping to marked page " + (string)count + ".");
}
else if (message == "License")
{
llGiveInventory(BookUser, "Author's Note and CC License");
}
else if (message == "Author")
{
count = AuthorPage;
SetPage();
}
else if (message == "Chapters")
{
Chapters = TRUE;
}
else if (message == "Prologue")
{
count = llList2Integer(ChapterPage, 0);
SetPage();
}
else if ((integer)message > 0)
{
count = llList2Integer(ChapterPage, (integer)message);
message = (string)0;
SetPage();
}
llListenRemove(Listener);
if (Chapters == TRUE)
{
ChapterSkip();
}
}
run_time_permissions(integer nMyPerms)
{
//llWhisper(0, "Runtime Permissions Called.");
if (nMyPerms & PERMISSION_TAKE_CONTROLS)
{
doTakeControls();
}
else
{
//llWhisper(0, "Key release shutdown.");
llParticleSystem([]);
llSleep(1.0);
state default;
}
}
control(key id, integer level, integer edge)
{
// something has happened to one of the
// keys we're watching. One or more of them was
// pressed, released or held down
//
// id is the avatar that generated the event, we are only
// capturing controls from 1 avatar, so we don't care
// about this
//
// level is the state of the key, 1 for down, 0 for up
//
// edge shows if the state of the key has changed, 1
// for changed, 0 for the same
if (level & edge & CONTROL_DOWN )
{
count = count + 1;
if (count >= PageCount + 1)
{
count = 0;
}
}
if (level & edge & CONTROL_UP )
{
count = count - 1;
if (count <= -1)
{
count = PageCount;
}
}
else
{
// boths keys are up, see if they were just released
if (edge & CONTROL_UP || edge & CONTROL_DOWN)
{
// llWhisper(0, "Keys released");
}
}
SetPage();
}
dataserver(key query_id, string data)
{
if (DataKey1 == query_id)
{
makeImage(data, Offset, PageScale);
llSetTimerEvent(1);
}
else if (DataKey2 = query_id)
{
if (data != EOF)
{
llSetTexture((string)data, 1);
}
}
}
timer()
{
DataKey2 = llGetNotecardLine("pagelist", count + 1);
llSetTimerEvent(0);
}
no_sensor()
{
llReleaseControls();
llParticleSystem([]);
llSleep(1.0);
BookUser = "";
llSensorRemove();
state default;
}
sensor(integer num_detected)
{
}
}
The pages for the book are images of preformatted text, uploaded as texture images. This reader uses a list of UUIDs for the pages, stored as a notecard (with the cover on line zero). These must be compiled by hand. If you plan on making your own line of books with something like this, I'd strongly suggest you dump the notecard setup and read the textures directly from the book inventory instead.
The particle system seems to be very client sensitive. In particular, some users seem to see the pages flicker from time to time. The only way I found to fix this is to increase the total particle count. The particle system displays what appears to be a single particle floating above the book. In actuality, this is several particles overlapping. You must keep the particle life short, so that you can turn pages in a reasonable amount of time (as it is, you must expect some brief jumping from previous to current pages as the particles expire). I currently have the total particle count at 192 particles. This appears to work flicker-free for everyone I've asked, but it can slow down your client when you are zoomed in close (ie when you are reading). A menu to vary the particle count may be in order for a more full-functioned book. Just instruct the user to select the lowest particle count he can use without flicker.
The sensor is needed to deal with people who leave the book running, or who cross the sim boundary without releasing their keys. Either case will lock out future readers. (The book locks itself so that only one reader can use it at a time.) Right now, there are three ways to shut down the reader: hit release keys, select "close book" from the touch-activated dialog menu, or move out of sensor range for more than 30 seconds.
On activation, the book checks the user's height and elevation relative to the book, and adjusts the height of the page accordingly. In most cases, this will result in the page being shown at the preset minimum set in "BaseOffset." However, if the user wants to put the book on the floor and read it flying a few meters above, he could. The page would still be shown in front of his face. This feature could almost certainly be stripped out with no consequences, but it is handy if your pages are especially small or your HoloReader is incorporated into something with an awkward viewing angle.
Well, in any case, I hope someone can make something of this.