A level is made up of sprites and sprites come from somewhere. In our editor, we are going to allow the user to “upload” multiple image files containing sprite sheets. However, are server is not required and that is going to require a bit of work. Also, we are going to need some form of UI where users can upload the spritesheet, without cluttering our main UI too much, so we will implement it as a modal dialog box.
Well, let’s get to it. First lets create a data type for holding our sprite sheet collection. For now, a spritesheet is simply an image, the dimensions of each sprite and a name. In your models folder create a new file named spriteSheet.js
spriteSheet.js
YUI.add('spriteSheet',function(Y){ Y.SpriteSheet = Y.Base.create('spriteSheet', Y.Model, [],{ count:function(){ return this.get('spritesheets').length; }, add:function(name,width,height,img){ this.get('spritesheets').push({name:name,width:width,height:height,img:img}); } },{ ATTRS:{ spritesheets: { value: [] } } } ); }, '0.0.1', { requires: ['model']});
Nothing really special. Our spritesheets attribute is just an empty array for now. We also included a pair of methods, add, for adding a new spritesheet and count for getting the current count of spritesheets already declared. Everything else here should already be familiar at this point.
Now we want to create a dialog that will be displayed when the user wants to add a spritesheet. As a bit of a spoiler, here is what we are going to create:
This isn’t a View and it isn’t a model, so we create a new folder called classess and create the long-winded file named AddSpriteSheetDialog.js
AddSpriteSheetDialog.js
YUI.add('addSpriteSheetDialog', function(Y){ Y.AddSpriteSheetDialog = new Y.Base(); var spriteSheets = null; Y.AddSpriteSheetDialog.show = function(ss,onComplete){ spriteSheets = ss; var panel = new Y.Panel({ width:500, height:300, centered:true, visible:true, modal:true, headerContent:'Select the image file containing your sprite sheet', bodyContent:Y.Node.create( "<DIV> <input type=file id=spritesheet /> <br /> <div id=imgName style='padding-top:25px;padding-bottom:25px'> Click above to select a file to download</div> <br />Sheet name:<input type=Text id=name size=30 value=''> <br />Sprite Width:<input type=Text id=width size=4 value=32> Sprite Height:<input type=Text id=height size=4 value=32> <br /><input type=button id=done value=done /> </DIV> " ), render:true }); var fileUpload = Y.one("#spritesheet"); fileUpload.on("change", Y.AddSpriteSheetDialog._fileUploaded); var buttonDone = Y.one("#done"); buttonDone.on("click", function(){ panel.hide(); onComplete(); }) panel.show(); }; Y.AddSpriteSheetDialog._fileUploaded = function(e){ if(!e.target._node.files[0].type.match(/image.*/)){ alert("NOT AN IMAGE!"); return; } var selectedFile = e.target._node.files[0]; var fileReader = new FileReader(); var that=this; fileReader.onload = (function(file){ return function(e){ if(e.target.readyState == 2) { var imgData = e.target.result; var img = new Image(); img.onload = function(){ Y.one('#imgName').set('innerHTML',selectedFile.name + " selected"); var name = Y.one('#name').get('value'); var width = Y.one('#width').get('value'); var height = Y.one('#height').get('value'); spriteSheets.add(name,width,height,img); } img.src = imgData; } }; })(selectedFile); fileReader.readAsDataURL(selectedFile); }; },'0.0.1', {requires:['node','spriteSheet','panel']});
The editorView owns the spritesheet collection, and passes it in to the show() method of AddSpriteSheetDialog. We also pass in a callback function that will be called when we are done.
We start off creating the panel which is a Y.Panel. Most of the properties should be pretty straight forward, headerContent is the title and bodyContent is either the ID of the object to render the panel in, or in our case, we actually create a new node with our dialog HTML. We then wire up a change handler on our file upload button, this will fire when a file is uploaded and call the _fileUploaded function. We then wire up the Done button’s on click handler to hide the panel then call the callback function that was passed in. Finally we display the panel.
When the user clicks the Choose File button, _fileUploaded is called. First thing we check to make sure it is an image that is uploaded and error out if it isn’t. We then want to read the selected file, which we do with the FileReader api. Word of warning, this isn’t completely supported in every browser… frankly though, I don’t care about supporting IE in a project like this, cross browser support takes all of the fun out of web app development!
Next is well… JavaScript at it’s most confusing. We are registering an onload event that will be fired once the file has been loaded, which in turn fires off an anonymous method. It checks the readystate of the file to make sure it is ready and if so, our “uploaded” file will be in e.target.result. We then create an Image object, then register yet another onload handler, this one for when the image has completed loading. Once the user has uploaded the file, its finished loading and populated in our newly create Image, we then get the width, height name and our newly populated image and at it to the screenSheets object we passed in during show(). Yes, this is a bit screwy of an interface, in that you need to populate the text fields before uploading the interview. I will ultimately clean that up ( and add edit ability ), but it would needlessly complicate the code for now. Finally, no that our fileReader.onload() event is done, we actually read the file now with readAsDataUrl() the file that was chosen, which fires off the whole onload event handler in the first place. Welcome to asynchronous JavaScript programming! Don’t worry, if this is new to you, thinking async will come naturally soon enough…
So, that is how you can create a modal dialog to edit app data. Now we wire it up and deal with a bit of a gotcha.
The gotcha first… the Panel dialog requires a parent HTML element in the DOM to have a YUI skin CSS class declared. At the bottom on the render function in editor.View.js add the following code:
Y.one('body').setStyle("margin",0); Y.one('body').setStyle("overflow","hidden"); // The below needs to be added as some controls, such as our add sprite dialog, require a parent container // to have the YUI skin defined already Y.one('body').setAttribute("class","yui3-skin-sam"); return this;
This adds the yui3-skin-sam class to the page’s body, which brings in all the styling for the Panel ( and other YUI widgets ).
While we are in editor.View.js, we wire up a menu handler for when the user clicks the add spritesheet button ( we will add in a second ). That handler is basically the same as the menu:fileExit handler we created earlier. Right below that handler in the initializer function, add the following:
var that = this; Y.Global.on('menu:fileAddSpriteSheet',function(e){ var dialog = Y.AddSpriteSheetDialog.show(that.spriteSheets,function(){ var sheet = that.spriteSheets.get("spritesheets")[0]; console.log(sheet); }); });
There is the that=this hack again, there are alternatives ( you can pass the context in to the Y.Global.on event handler ), but this is a fair bit easier at the end of the day, as we would lose this again when the callback is called. Otherwise, when the menu:fileAddSpriteSheet event is received, we simply call AddSpriteSheetDialog.show(), passing in our spritesheet and the function that is called when the panel is complete. For now we simply log the spritesheet out to the console to prove something changed.
We also need to add the SpriteSheet to our editor.View.js, like so:
Y.EditorView = Y.Base.create('editorView', Y.View, [], { spriteSheets:new Y.SpriteSheet(), initializer:function(){
Now we need to add the menu item. First add it to the template mainMenu.Template,like so:
<ul> <li class="yui3-menuitem" id="menuFileAddSpriteSheet"> <a class="yui3-menuitem-content" href="#">Add SpriteSheet</a> </li> <li class="yui3-menuitem" id="menuFileExit"> <a class="yui3-menuitem-content" href="#">Exit</a> </li> </ul
And we wire it up in the mainMenu.View.js, add the bottom of render() add the following code:
var menuFileAddSpriteSheet = container.one('#menuFileAddSpriteSheet'); menuFileAddSpriteSheet.on("click", function(e){ Y.Global.fire('menu:fileAddSpriteSheet', {msg:null}); });
Oh, and our newly added script AddSpriteSheetDialog.js is added to index.html to guarantee it gets loaded and evaluated.
And done. We now added a dialog for adding sprite sheet images, and can store the image results locally without requiring any server interaction at all.
Here is the end result, select File->Add Spritesheet to bring up the newly created dialog:
You can download the entire updated source code here.
One step closer to a full web based game editor, one very tiny step.
Programming General