docomo 用の 2 次元描画クラス

docomo でアフィン変換を掛けて描画するためのクラスを作ってみた。
com.nttdocomo.opt.ui.Graphics2 に対応してることが前提で作ってる。
対応してない場合はアルファとか加算合成とか使うのは無理だけど、DoJa-3.5 以降ならアフィン変換掛けて描画するのはできるはず。

public final class AffineTransform
{
    private float[] matrix = new float[6];

    AffineTransform() { clear(); }
    AffineTransform(float[] v)
    {
        matrix[0] = v[0]; matrix[1] = v[1]; matrix[2] = v[2];
        matrix[3] = v[3]; matrix[4] = v[4]; matrix[5] = v[5];
    }
    AffineTransform(AffineTransform rhs)
    {
        float[] v = rhs.matrix;
        matrix[0] = v[0]; matrix[1] = v[1]; matrix[2] = v[2];
        matrix[3] = v[3]; matrix[4] = v[4]; matrix[5] = v[5];
    }

    public float[] get()
    {
        return matrix;
    }

    public void clear()
    {
        clear(matrix);
    }
    public void translate(float x, float y)
    {
        translate(matrix, x, y);
    }
    public void rotate(float degree)
    {
        rotate(matrix, degree);
    }
    public void scale(float sx, float sy)
    {
        scale(matrix, sx, sy);
    }

    void getIntegerMatrix(int[] m)
    {
        m[0] = roundup(matrix[0] * 4096);
        m[1] = roundup(matrix[1] * 4096);
        m[2] = roundup(matrix[2] * 4096);
        m[3] = roundup(matrix[3] * 4096);
        m[4] = roundup(matrix[4] * 4096);
        m[5] = roundup(matrix[5] * 4096);
    }

    private static void clear(float[] v)
    {
        v[0] = 1; v[1] = 0; v[2] = 0;
        v[3] = 0; v[4] = 1; v[5] = 0;
    }
    private static void translate(float[] v, float x, float y)
    {
        v[2] += v[0] * x + v[1] * y;
        v[5] += v[3] * x + v[4] * y;
    }

    private static final float fromDegree = 1024.0f / 360.0f;
    private static void rotate(float[] v, float degree)
    {
        // MathUtil#sin(), MathUtil#cos() は 1024 を一周として、4096 倍された値を返す
        int s = MathUtil.sin((int)(rotate * fromDegree));
        int c = MathUtil.cos((int)(rotate * fromDegree));
        float t = v[0];
        v[0] = (t    * c + v[1] * s) / 4096;
        v[1] = (v[1] * c - t    * s) / 4096;
        t = v[3];
        v[3] = (t    * c + v[4] * s) / 4096;
        v[4] = (v[4] * c - t    * s) / 4096;
    }
    private static void scale(float[] v, float sx, float sy)
    {
        v[0] = v[0] * sx;
        v[1] = v[1] * sy;
        v[3] = v[3] * sx;
        v[4] = v[4] * sy;
    }
    private static int roundup(float v)
    {
        if (v < 0) return (int)(v - 0.99);
        return (int)(v + 0.99);
    }
}
import java.io.InputStream;
import com.nttdocomo.ui.MediaImage;
import com.nttdocomo.ui.MediaManager;
import com.nttdocomo.io.ConnectionException;

public final class Image
{
    private final com.nttdocomo.ui.Image image;

    public Image(InputStream is)
        throws ConnectionException
    {
        MediaImage mi = MediaManager.getImage(is);
        mi.use();
        image = mi.getImage();
    }
    public Image(com.nttdocomo.ui.Image image)
    {
        this.image = image;
    }

    public int getWidth() { return image.getWidth(); }
    public int getHeight() { return image.getHeight(); }

    com.nttdocomo.ui.Image getInternalImage() { return image; }
}
public final class SpriteImage
{
    private Image image;
    private int x, y, w, h;

    public SpriteImage()
    {
    }

    public SpriteImage(Image image)
    {
        reset(image);
    }

    public SpriteImage(Image image, int x, int y, int w, int h)
    {
        reset(image, x, y, w, h);
    }

    public Image getImage() { return image; }
    public int getX() { return x; }
    public int getY() { return y; }
    public int getWidth() { return w; }
    public int getHeight() { return h; }

    public SpriteImage reset(Image image)
    {
        this.image = image;
        setRectangle(0, 0, image.getWidth(), image.getHeight());
        return this;
    }
    public SpriteImage reset(Image image, int x, int y, int w, int h)
    {
        this.image = image;
        setRectangle(x, y, w, h);
        return this;
    }
    public void setImage(Image image) { this.image = image; }
    public void setRectangle(int x, int y, int w, int h)
    {
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
    }
    public void setX(int x) { this.x = x; }
    public void setY(int y) { this.y = y; }
    public void setWidth(int w) { this.w = w; }
    public void setHeight(int h) { this.h = h; }
}
import com.nttdocomo.opt.ui.Graphics2;
import com.nttdocomo.ui.Font;

public final class Graphics
{
    public static final int OP_ALPHA = 0;
    public static final int OP_ADD = 1;
    public static final int OP_SUB = 2;

    private static AffineTransform at = new AffineTransform();
    private static int[] matrix = new int[6];

    private Graphics2 g;

    public Graphics()
    {
    }

    public Graphics(Graphics2 g)
    {
        setGraphics(g);
    }

    public void setGraphics(Graphics2 g)
    {
        this.g = g;
    }

    private void setColor(int c)
    {
        // c は AARRGGBB 形式
        g.setColor(com.nttdocomo.ui.Graphics.getColorOfRGB(
            (c >> 16) & 0xff,
            (c >>  8) & 0xff,
            (c >>  0) & 0xff,
            (c >> 24) & 0xff));
    }

    public void fillRect(int x, int y, int w, int h, int c)
    {
        setColor(c);
        g.fillRect(x, y, w, h);
    }

    public void drawRect(int x, int y, int w, int h, int c)
    {
        setColor(c);
        g.drawRect(x, y, w, h);
    }

    public void drawLine(int x1, int y1, int x2, int y2, int c)
    {
        setColor(c);
        g.drawLine(x1, y1, x2, y2);
    }

    public void drawString(Font f, String str, int x, int y, int c)
    {
        g.setFont(f);
        setColor(c);
        g.drawString(str, x, y + f.getAscent());
    }

    public void drawImage(SpriteImage img, float x, float y, int anchor)
    {
        drawAffineImage(img, anchor, 1, 1, 0, x, y, OP_ALPHA, 255);
    }

    public void drawImage(SpriteImage img, float x, float y, int anchor, int op, int alpha)
    {
        drawAffineImage(img, anchor, 1, 1, 0, x, y, op, alpha);
    }

    public void drawScaleImage(SpriteImage img, float x, float y, float scalex, float scaley, int anchor)
    {
        drawAffineImage(img, anchor, scalex, scaley, 0, x, y, OP_ALPHA, 255);
    }

    public void drawScaleImage(SpriteImage img, float x, float y, float scalex, float scaley, int anchor, int op, int alpha)
    {
        drawAffineImage(img, anchor, scalex, scaley, 0, x, y, op, alpha);
    }

    public void drawDestScaleImage(SpriteImage img, float dstX, float dstY, float dstW, float dstH)
    {
        drawAffineImage(img, 0, dstW / img.getWidth(), dstH / img.getHeight(), 0, dstX, dstY, OP_ALPHA, 255);
    }

    public void drawDestScaleImage(SpriteImage img, float dstX, float dstY, float dstW, float dstH, int op, int alpha)
    {
        drawAffineImage(img, 0, dstW / img.getWidth(), dstH / img.getHeight(), 0, dstX, dstY, op, alpha);
    }

    public void drawRotateImage(SpriteImage img, float x, float y, float rotate, int anchor)
    {
        drawAffineImage(img, anchor, 1, 1, rotate, x, y, OP_ALPHA, 255);
    }

    public void drawRotateImage(SpriteImage img, float x, float y, float rotate, int anchor, int op, int alpha)
    {
        drawAffineImage(img, anchor, 1, 1, rotate, x, y, op, alpha);
    }


    public void drawAffineImage(SpriteImage img, int anchor, float scalex, float scaley, float rotate, float x, float y)
    {
        drawAffineImage(img, anchor, scalex, scaley, rotate, x, y, OP_ALPHA, 255);
    }

    public void drawAffineImage(SpriteImage img,
        int anchor,                         // このアンカーで指定された場所に配置して
        float scalex, float scaley,         // (原点を中心に)スケーリングして
        float rotate,                       // (原点を中心に)回転して
        float x, float y,                   // 移動させる
        int op, int alpha)
    {
        at.clear();

        at.translate(x, y);
        at.rotate(rotate);
        at.scale(scalex, scaley);
        at.translate(
            -(anchor % 3) * (float)img.getWidth() / 2,
            -(anchor / 3) * (float)img.getHeight() / 2);

        drawAffineImage(img, at, op, alpha);
    }

    public void drawAffineImage(SpriteImage img, AffineTransform affine)
    {
        drawAffineImage(img, affine, OP_ALPHA, 255);
    }

    public void drawAffineImage(SpriteImage img, AffineTransform affine, int op, int alpha)
    {
        switch (op)
        {
        case OP_ALPHA:
            if (alpha == 0) return;
            else if (alpha == 255) g.setRenderMode(Graphics2.OP_REPL, 255, 0);
            else g.setRenderMode(Graphics2.OP_ADD, alpha, 255 - alpha);
            break;
        case OP_ADD:        g.setRenderMode(Graphics2.OP_ADD, alpha, 255); break;
        case OP_SUB:        g.setRenderMode(Graphics2.OP_SUB, alpha, 255); break;
        }
        affine.getIntegerMatrix(matrix);
        g.drawImage(img.getImage().getInternalImage(), matrix, img.getX(), img.getY(), img.getWidth(), img.getHeight());
    }
}

AffineTransform を int に変換したときに roundup() で小数点を繰り上げている(実装は適当だけど)理由は、例えば 10x10 のイメージを 12x12 で描画したい場合、

at.scale(12.0f / 10.0f, 12.0f / 10.0f);

とか設定するんだけど、こうすると行列の中身は

| 1.2 |   0 |   0 |
|   0 | 1.2 |   0 }

となり、Graphics#drawImage() に渡すためにそれぞれを 4096 倍すると 1.2 の部分は 4915.2 になる。もし四捨五入したり切り捨てたりすると 4915 で渡すことになるんだけど、(渡した行列の扱いは実装依存なので各機種毎に違うけど)普通に計算すると 10x10 のイメージの幅は 10*4915 / 4096 = 11.99951171875... となり、内部の計算で切り捨てられた場合は 11 になってしまう。実際 11x11 になってしまう機種があった。
なので確実に 12 になるようにするために繰り上げて 4916 にしてしまえば 10*4916 / 4096 = 12.001953125... となって、内部の計算で切り捨てられても無事に 12x12 で描画されることになる。