(WIP) "Frame 2 sprite-tileset" webapp

Talk about development tools here

Moderator: BigEvilCorporation

Post Reply
realbrucest
Interested
Posts: 27
Joined: Wed Sep 21, 2011 9:00 pm
Location: Sevilla, Spain
Contact:

(WIP) "Frame 2 sprite-tileset" webapp

Post by realbrucest » Wed Jul 11, 2012 6:18 pm

(WIP) Image to tileset webapp

Hi guys,

I've started to code an HTML5+Javascript app to transform a frame into several sprites and then converting the lot to to C data code.
Something similar to what imagenesis already does but getting focus a litte bit more on dealing with multi-sprite characters.
It has being made firstly for personal use but I've had in mind to share it as one tool that can help anyone using SGDK and even make it part of SGDK tools if I could polish it enough (and Stef aproves it).

As a coder I'm quite shitty (I'm not a coder) and because I can't put all the time I'd like into this I've decided to share it in its actual state so maybe I can get some help with it.

At the moment it is "firefox friendly only" because I couldn't find a way to avoid antialiasing when scalling the image on other browsers.
Here's the line I'm talking about:

// Disable antialiasing (Firefox only)
in_ctx.mozImageSmoothingEnabled = false;

http://beatemup1985.byethost7.com/splitter.htm

Feel free to grab the html code and play around.

TODO:

(on drawStoredSprite() function)
- Show or hide the spritebox when an item is selected (done) or unselected (ToDo)

(on removeSpriteBox() function)
- Remove sprite form "sprites" array
- Sort sprite listbox when an item is removed

(on createSpriteBox() function)

- Fix where tiles are placed into tileset canvas. From "sprite box": grab the first two tiles from the "spritebox" left column, add them to "tileset canvas" one on top of another (first and secon tiles on the left column), next two tiles from first column on "spritebox" go to the second column into "tileset canvas", and so on ...
- Fix the C code output.
--- Colors taken from original image must be converted from RGBA to a 4bit value (it is almost done, still a bit "faulty")
--- Code is not properly written
--- I'd like to add a new textarea to fill output for frame sprites initialization. (Easy one but it is not done either)
Example:
bigben01[0].x = 0; // Relative x pos on "frame space"
bigben01[0].y = 1; // Relative y pos on "frame space"
bigben01[0].width = 4; //
bigben01[0].height = 4; //

(somewhere else)

- (Maybe) Show a grid over the frame, wich can be hiden/shown using a checkbox.


... and... I think that's all I would like to achive (well, appart from a nicer looking genesis-like gui, but this is an unnecesary extra).
I'll keep working on it but if someone else is faster than me... ;)

Thanks guys for your time and once again, to Steff for his cool SGDK.

Here's the HTML code so far:

Code: Select all

<html>
<head>
<title>Megadrive Frame Splitter</title>
</head>
<body onLoad="init()" >
<!-- FRAME canvas -->
<div style="position: absolute; left: 4px; top: 4px;">
  <h3>Frame</h3>
  <canvas id="canvas-in" width="320", height="480"></canvas>
  <form><input type="file" value="gfxframe" /></form>
</div>
<!-- TILESET column: canvas and options -->
<div style="position: absolute; left: 360px; top: 4px;">
  <h3>Tileset</h3>
  <canvas id="canvas-out" width="320", height="240"></canvas>
    <form>
	<br />Tile counter: 
		<input type="text" id="num_tiles" value="0" size="4"/><br />
	<div>
	<br/>Zoom:
	<input type="radio" name="zoomfactor" value=4 onClick="zoomFrame(3);">3X
	<input type="radio" name="zoomfactor" value=4 onClick="zoomFrame(4);">4X
	<input type="radio" name="zoomfactor" value=5 onClick="zoomFrame(5);"checked>5X
	<input type="radio" name="zoomfactor" value=6 onClick="zoomFrame(6);">6X
	</div>
	<br />Current sprite size (tiles):
	<br /><input type="text" id="sprite_size" value="0" size="6"/><br />
	</form>
</div>
<!-- SPRITELIST column -->
<div style="position: absolute; left: 690px; top: 4px;">
  <h3>Sprites</h3>
    <form>
  <select name="spritelist" size="12" onChange="drawStoredSprite(this.form.spritelist)">
	<br />Sprite counter: 
	<input type="text" id="num_sprites" value=0 size="2"/><br />
	<input type="button" value="New" onClick="createSpriteBox(this.form.spritelist)"/>
	<input type="button" value="Remove" onClick="removeSpriteBox(this.form.spritelist)"/>
  </select>
    </form>
</div>
<!-- ACTUAL FRAME canvas, non visible, using for extracting tiles -->
<div style="position: absolute; left: -360px; top: 4px;">
  <h3>Actual frame</h3>
  <canvas id="canvas-original" width="160", height="320"></canvas>
</div>
<!-- TEXTAREA: output TILESET data -->
<div style="position: absolute; left: 4px; top: 640px;">
<textarea cols="120" rows="24" id="txtTileset" readonly=""></textarea>
</div>
<script type="text/javascript">
//------------------------------------------------------
// GLOBAL ONLOAD THINGIES
//------------------------------------------------------
  var ZOOM_FACTOR = 5;
  var LAST_SPRITE = 0;
  var LAST_TILE = 0;

  var new_sprite_x = 0;
  var new_sprite_y = 0;
  var new_sprite_w = 0;
  var new_sprite_h = 0;

  // Canvas data:
  var canvas_in = document.getElementById("canvas-in"); // frame
  var canvas_out = document.getElementById("canvas-out"); // tileset 
  var canvas_original = document.getElementById("canvas-original"); // Non scaled frame

  var canvas_width = canvas_in.width;
  var canvas_height = canvas_in.height;

  var in_ctx  = canvas_in.getContext("2d");
  var out_ctx = canvas_out.getContext("2d");
  var original_ctx = canvas_original.getContext("2d");

  var TILESET_ROW = canvas_out.width / 8; //Number of tiles in a "canvas-out" row

  // Rectangle drawinf stuff
  rect = {},
  drag = false; // mouse dragging

  // Array where sprite data (coords and size) will be stored
  var sprites = [];

  // Disable antialiasing (Firefox only)
  in_ctx.mozImageSmoothingEnabled = false;

  // Frame image loading stuff
  var img = new Image();
  img.onload = function(){ zoomFrame(5); original_ctx.drawImage(img,0,0,img.width,img.height); };
  img.src = "bigben01.png"; // PROVISIONAL

  //Get filename (no extension) form image.src into trim_imgsrc
  var str = img.src;
  var trim_imgsrc = str.split('\\').pop().split('/').pop().slice(0,-4);

  // Clear or initialize previous content of all text items
  document.getElementById("txtTileset").value = "";
  document.getElementById("sprite_size").value = "(0, 0)";
  document.getElementById("num_tiles").value = 0;
  document.getElementById("num_sprites").value = 0;

//------------------------------------------------------
// INIT (IN-CANVAS)
//------------------------------------------------------
function init() {
  canvas_in.addEventListener('mousedown', mouseDown, false);
  canvas_in.addEventListener('mouseup', mouseUp, false);
  canvas_in.addEventListener('mousemove', mouseMove, false);
}

//------------------------------------------------------
// MOUSEDOWN (IN-CANVAS)
//------------------------------------------------------
function mouseDown(e) {
  rect.startX = e.pageX - this.offsetLeft;
  rect.startY = e.pageY - this.offsetTop;
  drag = true;
}
//------------------------------------------------------
// MOUSEUP (IN-CANVAS)
//------------------------------------------------------
function mouseUp() {
  drag = false;
}

//------------------------------------------------------
// MOUSEMOVE (IN-CANVAS)
//------------------------------------------------------
function mouseMove(e) {
  if (drag) {
    rect.w = ((e.pageX - this.offsetLeft) - rect.startX);
    rect.h = ((e.pageY - this.offsetTop) - rect.startY);

	//Adjust position and coords to nearest 8mult
	new_sprite_x = Math.floor(rect.startX/(8*ZOOM_FACTOR))*8*ZOOM_FACTOR; 
	new_sprite_y = Math.floor(rect.startY/(8*ZOOM_FACTOR))*8*ZOOM_FACTOR;
	new_sprite_w = Math.ceil(rect.w/(8*ZOOM_FACTOR))*8*ZOOM_FACTOR;
	new_sprite_h = Math.ceil(rect.h/(8*ZOOM_FACTOR))*8*ZOOM_FACTOR;

	//Limit the sprite size
	if(new_sprite_w > (64*ZOOM_FACTOR)) new_sprite_w = 64*ZOOM_FACTOR;
	if(new_sprite_h > (64*ZOOM_FACTOR)) new_sprite_h = 64*ZOOM_FACTOR;

	//Inform about new sprite size
	document.getElementById("sprite_size").value = '(' + (new_sprite_w/(8*ZOOM_FACTOR)) + ', ' + (new_sprite_h/(8*ZOOM_FACTOR)) + ')';

	//Reload upscaled frame
	in_ctx.drawImage(img,0,0,img.width*ZOOM_FACTOR,img.height*ZOOM_FACTOR);
	
	//And finally draw the rectangle box
    draw();
  }
}

//------------------------------------------------------
// ZOOMFRAME
//------------------------------------------------------
function zoomFrame(factor)
{
  ZOOM_FACTOR = factor;
  //Clearing the canvas
  in_ctx.fillStyle = "rgba(255, 255, 255, 255)";  
  in_ctx.fillRect (0, 0, canvas_width, canvas_height);
  //Drawing the upscaled image
  in_ctx.drawImage(img,0,0,img.width*ZOOM_FACTOR,img.height*ZOOM_FACTOR);
}

//------------------------------------------------------
// DRAW
//------------------------------------------------------
function draw()
{
  in_ctx.fillStyle = "rgba(0, 0, 200, 0.5)";
  in_ctx.fillRect(new_sprite_x, new_sprite_y, new_sprite_w, new_sprite_h);
}

//------------------------------------------------------
// DRAWSTOREDSPRITE
//------------------------------------------------------
function drawStoredSprite(theSel) {

  var i = theSel.options[theSel.selectedIndex].value;
  //sprites[i].selected = 1;

  //Get a random color
  var color = Math.floor(Math.random()*4);

  switch(color){
	case 0: in_ctx.fillStyle = "rgba(200, 200, 200, 0.5)"; break;
	case 1: in_ctx.fillStyle = "rgba(200, 0, 200, 0.5)"; break;
	case 2: in_ctx.fillStyle = "rgba(200, 200, 0, 0.5)"; break;
	case 3: in_ctx.fillStyle = "rgba(120, 120, 120, 0.5)"; break;
  }//end switch

  in_ctx.strokeStyle = "#f00";
  in_ctx.lineWidth = 2;
  in_ctx.strokeRect(sprites[i].x*ZOOM_FACTOR*8, sprites[i].y*ZOOM_FACTOR*8, sprites[i].w*ZOOM_FACTOR*8, sprites[i].h*ZOOM_FACTOR*8);
  in_ctx.fillRect(sprites[i].x*ZOOM_FACTOR*8, sprites[i].y*ZOOM_FACTOR*8, sprites[i].w*ZOOM_FACTOR*8, sprites[i].h*ZOOM_FACTOR*8);
}

//------------------------------------------------------
// CREATESPRITEBOX
//------------------------------------------------------
function createSpriteBox(theSel)
{
  // Local data _______________________________
  var i = 0;
  var ii = 0;
  var r, g, b;
  var h, v;
  var tile;
  var tileset_h = 0;
  var tileset_v = 0;
  var fake_last_tile = LAST_TILE;

  var color; // integer value sum (concat) of r+g+b
  var palette = new Array(); // 16 colors values
  var palcol = 0; // index for "palette" array
  var pixelcounter  = 0;
  var tilecounter   = 0;
  var spritecounter = 0;

  // Process __________________________________

  // Initialize palette array
  for(i=0; i<16; i++) palette[i] = -1;

  //Save relative position and size (in tile units)
  sprites[LAST_SPRITE] = new Object();
  sprites[LAST_SPRITE].x = new_sprite_x / (8 * ZOOM_FACTOR);
  sprites[LAST_SPRITE].y = new_sprite_y / (8 * ZOOM_FACTOR);
  sprites[LAST_SPRITE].w = new_sprite_w / (8 * ZOOM_FACTOR);
  sprites[LAST_SPRITE].h = new_sprite_h / (8 * ZOOM_FACTOR);
  //sprites[LAST_SPRITE].selected = -1;

  //Add a new item to the sprite listbox ....................................
  if (theSel.length == 0) {
    var newOpt1 = new Option(trim_imgsrc+'['+LAST_SPRITE+']'+ ', X=' + sprites[LAST_SPRITE].x + ', Y=' + sprites[LAST_SPRITE].y + ', W=' + sprites[LAST_SPRITE].w + ', H=' + sprites[LAST_SPRITE].h, LAST_SPRITE);
    theSel.options[0] = newOpt1;
    theSel.selectedIndex = 0;
  } else if (theSel.selectedIndex != -1) {
    var selText = new Array();
    var selValues = new Array();
    var selIsSel = new Array();
    var newCount = -1;
    var newSelected = -1;
    var i;

    for(i=0; i<theSel.length; i++)
    {
      newCount++;
      selText[newCount] = theSel.options[i].text;
      selValues[newCount] = theSel.options[i].value;
      selIsSel[newCount] = theSel.options[i].selected;
      
      if (newCount == theSel.selectedIndex) {
        newCount++;
        selText[newCount] = trim_imgsrc+'['+LAST_SPRITE+']'+ ', X=' + sprites[LAST_SPRITE].x + ', Y=' + sprites[LAST_SPRITE].y + ', W=' + sprites[LAST_SPRITE].w + ', H=' + sprites[LAST_SPRITE].h;
        selValues[newCount] = LAST_SPRITE;
        selIsSel[newCount] = false;
        newSelected = newCount - 1;
      }
    }
    for(i=0; i<=newCount; i++)
    {
      var newOpt = new Option(selText[i], selValues[i]);
      theSel.options[i] = newOpt;
      theSel.options[i].selected = selIsSel[i];
    }
  }

  //Add the tiles to tile canvas and data to textarea ................................
  for(h=0; h<(sprites[LAST_SPRITE].w*8); h+=8) // sprite tiles loops (Hor & Vert)
  {
    for(v=0; v<(sprites[LAST_SPRITE].h*8); v+=8)
	  {
		//Load fake_last_tile (for every two tiles downside placement) (DOESN'T WORK YET :S)
		if(LAST_TILE % 2 == 0)
		  fake_last_tile = Math.ceil(LAST_TILE/2);
		else
		  fake_last_tile = TILESET_ROW + Math.floor(LAST_TILE/2);
		
		//Load tile area into "tile" buffer
		var tile = original_ctx.getImageData((sprites[LAST_SPRITE].x*8)+h, (sprites[LAST_SPRITE].y*8)+v, 8, 8);

		//Write into tile_canvas
		tileset_v = Math.floor(fake_last_tile/TILESET_ROW)*8;
		tileset_h = (fake_last_tile % TILESET_ROW)*8;
		out_ctx.putImageData(tile, tileset_h, tileset_v);

		// Write sprite header into textarea
		if(tilecounter % (sprites[LAST_SPRITE].w * sprites[LAST_SPRITE].h) == 0)document.getElementById("txtTileset").value += "}\n" + "u32 " + trim_imgsrc +"[" + LAST_SPRITE + "][" + (sprites[LAST_SPRITE].w * sprites[LAST_SPRITE].h) + "] = {\n  ";

		//Loop through pixel data and write data to textarea .......................
 		for(i=0; i<256; i+=4)
  		{
			//Save pixel RGB values
			r = tile.data[i];
			g = tile.data[i+1];
			b = tile.data[i+2];

			//Store RGB value as an integer
			color = parseInt(r.toString() + g.toString() + b.toString());

			//If not already saved we store the color value to palette
			while((palette[palcol]!=-1) && (palcol<15)) palcol++;
			if((palcol<15) && (palette[palcol]!==color)) palette[palcol] = color;

			// Make 8 consecutive pixels into an u32 value
			document.getElementById("txtTileset").value += "0x";
			for(ii=0; ii<8; ii++)
			{
				document.getElementById("txtTileset").value += palcol.toString(16);
				i++;
			    pixelcounter++;
			}

			document.getElementById("txtTileset").value += ", ";

			if(pixelcounter % 64 == 0) //Check for TILE CHANGE (64 pixels = 1 tile)
			{
				tilecounter++;
				LAST_TILE++;
	  			document.getElementById("txtTileset").value += "// TILE " + Math.floor(tilecounter) + "\n  ";
			}

			if(tilecounter == sprites[LAST_SPRITE].w * sprites[LAST_SPRITE].h) //Check for SPRITE CHANGE
			{
  	  			tilecounter = 0; // reset
	  			spritecounter++;
				LAST_SPRITE++; //Update sprite counter
			}

  		}//end for(tile pixel data)
		
	  }//end for
  }//end for

  //Update the infobox
  document.getElementById("num_tiles").value = LAST_TILE;
  document.getElementById("num_sprites").value = LAST_SPRITE;

}

//------------------------------------------------------
// REMOVESPRITEBOX
//------------------------------------------------------
function removeSpriteBox(theSel)
{
  var selIndex = theSel.selectedIndex;
  if (selIndex != -1) {
    for(i=theSel.length-1; i>=0; i--)
    {
      if(theSel.options[i].selected)
      {
        theSel.options[i] = null;
      }
    }
    if (theSel.length > 0) {
      theSel.selectedIndex = selIndex == 0 ? 0 : selIndex - 1;
    }
  }

  LAST_SPRITE--;

  //Update the infobox
  document.getElementById("num_sprites").value = LAST_SPRITE;

}
</script>
</body>
</html>

Post Reply