MaSVG - simple svg level loader class

Any community contributions to libgdx go here! Some may get included in the core API when permission is granted.

MaSVG - simple svg level loader class

Postby Magnesus » Sun Nov 11, 2012 8:21 pm

I've just put all my SVG loader code in one class. It's very simple, feel free to use it, modify it, be inspired by it or look for some tips on how to deal with SVGs (I've put a lot of work into reading x, y and rotation from SVG into values for Actors in scene2d).


EDIT: Newest version here: viewtopic.php?f=17&t=21333

It's main purpose is using Inkscape as a level editor.

How to use it?
- create a new project in Inkscape with the same width and height as your stage,
- create a class that extends MaSVG and define constructor (which must set screenheight to the height of your stage), newText and newImage methods,
- drag and drop images in Inkscape and put some text (configuration lines for level or labels to be put on screen) - save all to SVG,
- in newText decide what to do with text (put them on stage as labels, use as configuration for example "tosave: 5" might mean that user needs to save 5 creatures in that level),
- in newImage decide what to do with images (put them on stage as images, use name to decide what textureregion to use).
- use load("nameofsvg.svg") to load your SVG.

All images that you put on the SVG in Inkscape you should also put on some Atlas, load it and take TextureRegions from it - the name parameter in newImage will be the same as the name of the TextureRegion (for example "logo.png").

What works?
- images, text and rectangles
- rotation and position,
- flipx and flipy (they will give you width<0 or height<0 - use it to flip during drawing)
- width and height,
- you can put images, text in groups and layers, but putting groups i groups is risky - I didn't test what happens with transformation then
- you can use el parameter in newImage to get subelements - useful for getting for example description of image (I use description for setting properties of the actor for example speed, weight, onload, onclick etc.) - or read some attributes.

In newText you can also put configuration - for example I put "tosave: 4" to indicate that user has to save 4 creatures.
In newImage set x, y, width, height, rotation of Actor to values from parameters. The name is the name of the image. Remember to set originX to width/2 and originY to height/2 (might now place actors properly without it when they are rotated, it's a limitation, I know).

It's version 1.1. Use it for inspiration - the code should be easy to understand and modify.

Planned for later
Loading polygons, circles - to use as paths, defining regions etc.

Code: Select all
package magory.lib;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.utils.XmlReader;
import com.badlogic.gdx.utils.XmlReader.Element;

public abstract class MaSVG
{
   private String file;
   protected float screenheight;
   protected String lastGroupLabel;
   
   public MaSVG(float screenheight)
   {
      this.screenheight = screenheight; // for transforming y
   }
   
   public void load(String name)
   {
      XmlReader xr = new XmlReader();
      Element lev = new Element("a", null);
      file = name;
      try
      {
         lev = xr.parse(Gdx.files.internal(file));
      }
      catch(Exception e)
      {
         Gdx.app.log("test", "Error loading "+file);
      }

      loadElement(lev, 0, 0, 0, 1, 1);      
   }
   
   // the rest
   
   public String getImageName(String trans)
   {
      // /media/amon/praca/android/fluffy/topack-game/tile-grass.png
      String name = "";
      try
      {
         int en = trans.indexOf(".png");
         int st = trans.lastIndexOf('/');
         name = trans.substring(st+1, en);
      }
      catch(Exception e)
      {
         Gdx.app.log("test", "Error getting filename: "+trans);
      }
      return name;
   }
   
   public void loadElement(Element el, float x, float y, float r, float sx, float sy)
   {
      String elname = el.getName();
      float[] fl = new float[1]; // ugly, I know
      int count = el.getChildCount();
      if(elname.equals("g")) // allows getting the name of the group (for example: layer name!)
      {
         String s = el.getAttribute("inkscape:label", "empty");
         if(!s.equals("empty"))
         {
            lastGroupLabel = s;
         }
      }
      
      int sign = 0;
      float r2 = 0;
         
      // magic for transforms and matrixes, don't touch unless you know what you are doing
      String trans = el.getAttribute("transform", "");
      if(!trans.equals(""))
      {
         if(trans.contains("translate("))
         {
            // sprawdź czy jest translate
            fl = getTwoFloats(trans, "translate(");
            x+=fl[0];
            y+=fl[1];
         }
         else if(trans.contains("matrix("))
         {
            fl = getSixFloats(trans, "matrix(");
            x+=fl[4];
            y+=fl[5];
            
            // new version
            if(fl[1]!=0||fl[0]!=0)
               r+=(float)(Math.toDegrees(Math.atan2(fl[1], fl[0])));            
            else
               r+=90;
            sign = ((fl[0]*fl[3] - fl[2]*fl[1])<0) ? -1 : 1;
            r2 = (float)(Math.toDegrees(Math.atan2(fl[3], fl[2])));

            float oldsx = sx;
            float oldsy = sy;

            if(sign<0)
            {
               if(r2<0&&r<0)
               {
                  sx*=(fl[0])/Math.cos( Math.toRadians(r) );
                  sy*=(fl[3])/Math.cos( Math.toRadians(r) );
                  sx=1/sx;
                  sy=1/sy;

                                                if(sy<0)
                  {
                     sy=1/sy;
                     sx=1/sx;
                  }
               }
               else
               {
                  sx*=(fl[0])/Math.cos( Math.toRadians(r) );
                  sy*=(fl[3])/Math.cos( Math.toRadians(r) );

                  if(sx==0||sy==0) // necessary for some situation when fl[0] and/or fl[3] are 0
                  {
                     sx=(float) (oldsx*(fl[1])/Math.sin( Math.toRadians(r) ));
                     sy=(float) (oldsy*-(fl[2])/Math.sin( Math.toRadians(r) ));
                  }
               }
            }
            else if(fl[0]!=0||fl[3]!=0)
            {
               sx*=fl[0]/Math.cos( Math.toRadians(r) );
               sy*=fl[3]/Math.cos( Math.toRadians(r) );

               if(sx==0||sy==0) // might not be necessary
               {
                  sx=(float) (oldsx*(fl[1])/Math.sin( Math.toRadians(r) ));
                  sy=(float) (oldsy*-(fl[2])/Math.sin( Math.toRadians(r) ));
               }
            }
            else // might not be necessary
            {
               sx=(float) (oldsx*(fl[1])/Math.sin( Math.toRadians(r) ));
               sy=(float) (oldsy*-(fl[2])/Math.sin( Math.toRadians(r) ));
            }
         }
      }
      
      if(count!=0)
         for(int i=0; i<count; i++)
         {
            loadElement(el.getChild(i), x, y, r, sx, sy);
         }
      
      // important magic for width,height and x,y
      float xx = getFloat(el.getAttribute("x", "0"))*sx;
      float yy = getFloat(el.getAttribute("y", "0"))*sy;
      float width = getFloat(el.getAttribute("width", ""))*sx;
      float height = getFloat(el.getAttribute("height", ""))*sy;
      
      float yyy = screenheight-(y+yy)-height;
      float xxx = x+xx;
      float rr = r;
      float originX = width/2;
      float originY = height/2;
      
      if(fl.length>2) // matrix
      {
         xx+=originX; // it assumes you set originX and originY as width/2 and height/2
         yy+=originY;         
         xxx = (float)(x + xx * Math.cos( Math.toRadians(rr) )
               - yy * Math.sin( Math.toRadians(rr) )) - originX;
         yyy =  screenheight-(float)(y + xx * Math.sin( Math.toRadians(rr) )
               + yy * Math.cos( Math.toRadians(rr) )) - originY;
         
         if(sign<0)
         {
            xx = getFloat(el.getAttribute("x", "0"))*(1/sx);
            yy = getFloat(el.getAttribute("y", "0"))*(1/sy);
            xx+=originX;
            yy+=originY;
            xxx = (float)(x + xx * Math.cos( Math.toRadians(rr) )
                  - yy * Math.sin( Math.toRadians(rr) )) - originX;
            yyy =  screenheight-(float)(y + xx * Math.sin( Math.toRadians(rr) )
                  + yy * Math.cos( Math.toRadians(rr) )) + originY;
         }
         rr = -rr;         
      }
      
      if(elname.equals("tspan")) // tekst
      {
         String text = el.getText();
         newText(text, el, xxx, yyy, width, height, rr);
      }
      else if(elname.equals("image")) // obraz
      {
         String name = getImageName(el.getAttribute("xlink:href", ""));
         
         newImage(name, el, xxx, yyy, width, height, rr);         
      }
      else if(elname.equals("rect")) // obraz
      {
         Element title = el.getChildByName("title");
         if(title!=null)
            newRect(title.getText(), el, xxx, yyy, width, height, rr);
         else
            newRect("", el, xxx, yyy, width, height, rr);
      }
   }
   
   // actions to override
   
   abstract public void newImage(String name, Element el, float xxx, float yyy,
         float width, float height, float rr);
   
   abstract public void newRect(String name, Element el, float xxx, float yyy,
         float width, float height, float rr);

   abstract public void newText(String text, Element el, float xxx, float yyy,
         float width, float height, float rr);

   // statics
   
   public static float[] getSixFloats(String trans, String search)
   {
      int st = trans.indexOf(search);
      float[] fl = new float[6];
      float xx = 0;
      float yy = 0;
      
      float zz = 0;
      float x2 = 0;
      float y2 = 0;
      float z2 = 0;
      
      if(st!=-1) // jest translate
      {
         int comma = trans.indexOf(",", st);
         int bracket = trans.indexOf(")", st);
         int comma2 = trans.indexOf(",", comma+1);
         try
         {
            
            xx = new Float(trans.substring(st+search.length(), comma));
            yy = new Float(trans.substring(comma+1, comma2));
            comma = comma2;
            comma2 = trans.indexOf(",", comma+1);
            zz = new Float(trans.substring(comma+1, comma2));
            
            comma = comma2;
            comma2 = trans.indexOf(",", comma+1);
            x2 = new Float(trans.substring(comma+1, comma2));
            
            comma = comma2;
            comma2 = trans.indexOf(",", comma+1);
            y2 = new Float(trans.substring(comma+1, comma2));
            
            comma = comma2;
            z2 = new Float(trans.substring(comma+1, bracket));
            
         }
         catch(Exception e)
         {
            
         }
      }
      fl[0] = xx;
      fl[1] = yy;
      fl[2] = zz;
      fl[3] = x2;
      fl[4] = y2;
      fl[5] = z2;
      return fl;
   }
   
   public static float[] getTwoFloats(String trans, String search)
   {
      int st = trans.indexOf(search);
      float[] fl = new float[2];
      float xx = 0;
      float yy = 0;
      if(st!=-1) // jest translate
      {
         int comma = trans.indexOf(",", st);
         int bracket = trans.indexOf(")", st);
         try
         {
            
            xx = new Float(trans.substring(st+search.length(), comma));
            yy = new Float(trans.substring(comma+1, bracket));
         }
         catch(Exception e)
         {
            
         }
      }
      fl[0] = xx;
      fl[1] = yy;
      return fl;
   }
   
   public int getInt(String t, String beg)
   {
      return getInt(t.substring(beg.length()));
   }
   
   public int getInt(String trans)
   {
      try
      {
         return new Integer(trans);
      }
      catch(Exception e)
      {
      }
      return 0;
   }
   
   public float getFloat(String t, String beg)
   {
      return getFloat(t.substring(beg.length()));
   }
   
   public float getFloat(String trans)
   {
      try
      {
         return new Float(trans);
      }
      catch(Exception e)
      {
      }
      return 0;
   }
}
Last edited by Magnesus on Wed Dec 16, 2015 10:20 pm, edited 9 times in total.
Magnesus
 
Posts: 1515
Joined: Sun Sep 25, 2011 3:50 pm

Re: MaSVG - simple svg level loader class

Postby bosoni » Sat Dec 08, 2012 7:50 pm

This might come in handy, must test. Thanks!
bosoni
 
Posts: 127
Joined: Sun Oct 14, 2012 7:04 pm
Location: Fi

Re: MaSVG - simple svg level loader class

Postby Magnesus » Tue Dec 18, 2012 9:34 pm

I updated the class because there was a bug in calculating y coordinate - instead of screenheight it was using constant 800 (I haven't noticed it before because I mostly use 1280x800 stage). There is also some problem with calculating coordinates when images are scaled without keeping aspect ratio that I have to investigate - as long as you always scale elements with keeping aspect ratio you will be fine.
Magnesus
 
Posts: 1515
Joined: Sun Sep 25, 2011 3:50 pm

Re: MaSVG - simple svg level loader class

Postby Magnesus » Tue Jan 15, 2013 1:42 pm

New version also loads rectangles - newRect. Parameter "name" in case of rectangles is the title you can set in Inkscape in object properties. I personally use rectangles for text (title set in Inkscape decides what text to display, rectangle width and height define text bounds) and intent on using them for boundaries in 2D platformer (where monster can walk, where are ladders, walls etc.).

I am also using MaSVG right now to load all user interface in my game - it sped up my development process tenfolds.

I'm also using attributes you can set in Inkscape - onfocus, onload etc. You can read them using el.getAttribute("onload", "") and do whatever you want with it (for example I set "onload" as "fadein" or "scalein" to decide how to animate the item when the screen is shown).
Magnesus
 
Posts: 1515
Joined: Sun Sep 25, 2011 3:50 pm

Re: MaSVG - simple svg level loader class

Postby Magnesus » Sat Jan 19, 2013 9:12 pm

EDIT: I fixed it. Now all rotations work. Fixed version in first post!
Magnesus
 
Posts: 1515
Joined: Sun Sep 25, 2011 3:50 pm

Re: MaSVG - simple svg level loader class

Postby Magnesus » Tue Feb 12, 2013 10:02 pm

I've just spend half the day trying to implement flip - my trigonometry sucks. :) If someone has any ideas, let me know - current version returns height below zero when the object is flipped horizontally (width should be below zero then) and behaves very badly when sth is stretched and flipped at the same time.
Magnesus
 
Posts: 1515
Joined: Sun Sep 25, 2011 3:50 pm

Re: MaSVG - simple svg level loader class

Postby Magnesus » Wed Feb 13, 2013 2:50 pm

I've looked into source code of other SVG loaders and found that:
- SVG Loader Extension for AndEngine works worse than mine with skew and flip... :( so I'm not the only one that has problems with it,
- most just draw using the matrix without resolving it (not useful for game use, only for drawing static SVGs),
- I had a small bug with object rotated by exactly -90 degrees (probably also 270) - it is fixed now (I'll modify the first post with updated version when I manage to fight off the flip curse ;) ),
Magnesus
 
Posts: 1515
Joined: Sun Sep 25, 2011 3:50 pm

Re: MaSVG - simple svg level loader class

Postby Magnesus » Wed Feb 13, 2013 11:49 pm

OK. I've just solved it. New version in the first post. It works with flipx, flipy.
Magnesus
 
Posts: 1515
Joined: Sun Sep 25, 2011 3:50 pm

Re: MaSVG - simple svg level loader class

Postby Magnesus » Wed May 22, 2013 8:11 pm

A small update: for special cases when fl[0] and fl[3] are 0 (happens on some unfortunate flip and rotate mix). It might not cover all cases, I will update when I find more problems.
Magnesus
 
Posts: 1515
Joined: Sun Sep 25, 2011 3:50 pm

Re: MaSVG - simple svg level loader class

Postby siondream » Thu May 23, 2013 7:24 am

There's still no way of specifying relative paths in Inkscape for textures, is there?
siondream
 
Posts: 364
Joined: Tue Apr 03, 2012 11:59 pm

Next

Return to Libgdx Contributions

Who is online

Users browsing this forum: No registered users and 2 guests