程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java:在二維動畫中使用基於圖像的路徑

Java:在二維動畫中使用基於圖像的路徑

編輯:關於JAVA

在二維(2D)動畫中,通常需要按預定義的模式(有時稱為 控制路徑)在一個 2D 區域內移動對象。這種動畫需要解決兩個問題:

如何指定對象要遵循的控制路徑。

如何沿著所選的路徑移動對象。

在本文中我們將為您展示如何用無損圖像、Swing 技術和基於 Java 的動畫引 擎解決這些問題。我們將首先繪制所需要的動畫對象軌道,然後用動畫引擎驅動 對象沿著定義的控制路徑運動。

可以容易創建和處理無損圖像(在 下面說明),而且可以根據需要對使用它 們的技術進行細致的調節。我們將利用一個示例動畫序列,介紹如何用不同的顏 色集創建復雜的運動序列。我們還將介紹如何處理圖像以提取出所需要的控制路 徑、將控制路徑與背景圖像分層、為動畫序列創建對象(Swing GUI 組件),並 驅動這些對象沿著定義的控制路徑運動以完成動畫過程。

注:本文假定讀者有 Java 一般編程、特別是 Swing GUI 構造的知識。如果 有在 Java 平台中利用 Java 2D 操縱圖像的經驗則更好。

什麼是無損?

無損圖像(lossless image)是永久保留了所有圖像像素的圖像。這種圖像必 須能夠存儲為或者恢復成與原件完全一樣的復制品。

可以使用不同的應用程序創始無損圖像,包括 Microsoft Paint、Jasc Paint Shop Pro 和一些定制的應用程序。可以將這些圖像存儲到文件中,也可以只在內 存創建它們。圖像必須是無壓縮的,或者是使用無損壓縮算法如 zip 壓縮進行壓 縮的。典型的無損圖像格式包括 Microsoft 的 Bitmap (BMP) 和 Portable Network Graphics (PNG) 格式。有損壓縮算法,比如通常用於 GIF(Graphics Interchange Format)和 JPEG(Joint Photographic Experts Group) 文件的壓 縮算法,不適用於本文所描述的動畫技術。

完全是控制問題

控制路徑 的最一般化的定義是通過任意 n 維空間時,在特定位置和時間所要 采取的行為。我們將控制路徑定義為一個或者多個對象穿過一個 2D 空間時所采 取的路徑。通過將對象的位置映射到該位置的行為來表示控制路徑。然後程序遍 歷所定義的對象、在映射中查找對象在該位置上的行為、並讓對象執行所指定的 行動。對所有控制路徑—— 除去最簡單的—— 在代碼中建立這樣一個映射都是 耗費時間和容易出錯的,因此使用一個繪圖程序更合適。

控制路徑可以是 不隨時間變化的(time invariant),在這種情況下是靜態 的,也可以是 隨時間變化的(time variable),在這種情況下是動態的。如果 無損圖像包含在一個圖像文件中,那麼它就是不隨時間變化的,或者說是靜態的 。如果無損圖像是包含在 RAM 中並直接使用的,那麼它就是隨時間變化的,或者 說是動態的。在本文中我們討論的是靜態控制路徑。使用正確的編輯程序,可以 更容易地生成靜態圖像,盡管所定義的行為類型也會在某種程度上影響這個過程 。

讓我們度過一個狂熱的夜晚!

學習動畫的一個好方法是自己動手實踐。我們將在本文其余部分使用一個動畫 的例子來闡明所討論的概念。我們的例子是一個動畫的火災逃生序列,我們將生 成控制路徑以表示幾個人物的逃生路徑。我們將使用圖 1 中的部分平面圖作為背 景圖像。可以在 圖 6中看到完整的背景圖像。

圖 1. 背景圖像的一部分

我們可以用一個數值數組生成控制映射。用一個圖像代替數組(如圖 2 所示 )使我們可以用顏色值來表示每一個位置的行為。每一種顏色值的大小(顏色位 數)取決於圖像格式。圖 2 展示了火災逃生序列的控制路徑。

圖 2. 部分控制路徑

為了看到控制路徑圖像與動畫背景的對應關系,我們可以將控制圖像覆蓋到背 景圖像上,如圖 3 所示。

圖 3. 使用分層透明度結合起來的圖像

讓世界充滿色彩

生成一個圖像後,就可容易地將它轉換為所需要的映射。我們只要遍歷圖像的 顏色並為每種顏色值指定一種行為。比如我們可以使用白色 ——它通常是全為 1 的值——表示無映射或者默認行為。黑色——通常是零值——可以用於表示自定 義的行為。如果是根據我們的圖像映射的,當對象遇到一個有同樣行為(即同一 種顏色,如黑色)的位置時,它就會繼續沿著由這個位置定義的方向運行。如果 這個位置不是同樣的行為,那麼它就會找到與它有同樣行為的相鄰位置,但是不 會走回頭路。

我們可以用不同的顏色表示其他行為。沒有定義的顏色值將會被忽略。因此, 背景中的像素(如 圖 3中的淺灰色)會被忽略。

清單 1 顯示如何完成映射。先針對特定顏色值掃描該圖像,然後用每一個顏 色像素的位置定義該位置在控制狀態映射中的控制狀態。在逃生的例子中,用不 同的 STATE_xxx 常量定義了六種行為。

清單 1. 處理控制路徑圖像

Map map = new HashMap();
 :
public final static int STATE_UNKNOWN = -1;
public final static int STATE_NONE = 0;
public final static int STATE_HALLWAY = 1;
public final static int STATE_INTERSECTION = 2;
public final static int STATE_HINT = 3;
public final static int STATE_START = 4;
public final static int STATE_EXIT = 5;
 :
/** Process the control image */
void processControl(Image img, int x, int y, int w, int h)
{
  int pmap[] = new int[w * h];
  PixelGrabber pg = new PixelGrabber(img, x, y, w, h, pmap, 0, w);
  try {
    pg.grabPixels();
     if ((pg.getStatus() & ImageObserver.ABORT) != 0) {
       System.err.println("image fetch error");
    }
     else {
      Integer none = new Integer(STATE_NONE);
       Integer hall = new Integer(STATE_HALLWAY);
      Integer start = new Integer(STATE_START);
      Integer exit = new Integer(STATE_EXIT);
      Integer hint = new Integer (STATE_HINT);
      Integer inter = new Integer (STATE_INTERSECTION);
      // for each position
       for (int i = 0; i < pmap.length; i++) {
        int red   = (pmap[i] >> 16) & 0xff;
        int green = (pmap[i] >> 8) & 0xff;
        int blue = (pmap [i]   ) & 0xff;
        if   (red == 255 && green == 255 && blue == 255)
           ; // don't bother to add NONE to map
        else if (red == 0 && green == 0 && blue == 0)
           map.put(new Integer(i), hall);
        else if (red == 0 && green == 255 && blue == 0)
           map.put(new Integer(i), start);
        else if (red == 200 && green == 0 && blue == 0)
           map.put(new Integer(i), exit);
        else if (red == 255 && green == 0 && blue == 0)
           map.put(new Integer(i), hint);
        else if (red == 0 && green == 0 && blue == 255)
           map.put(new Integer(i), inter);
      }
    }
  }
  catch (InterruptedException e) {
    System.err.println ("image processing interrupted");
  }
}

變化是生活的調劑品

盡管在清單 1 中使用了具體的顏色值(即紅色是 200,綠色是 0,藍色是 0 ),但是我們可以容易地改進代碼,讓它支持顏色范圍。使用顏色范圍降低了在 繪圖程序中使用的顏色選擇的精度,因而更容易創建路徑圖像。

使用更多的顏色使我們可以定義更多狀態,還可以描述更復雜的行為。例如, 可以使用 RGB 方案中的不同顏色段創建重疊的控制路徑。如果上述每一種狀態都 是由一種顏色的不同深淺而不是由不同顏色編碼的,那麼三個獨立的控制路徑可 以彼此重疊。當然,使用一種顏色的不同深淺使得區分不同行為的細微顏色差別 變得困難了。大多數圖像編輯程序可顯示出所選像素的准確顏色值,這使這個問 題沒那麼嚴重了。

還可以定義三個以上的控制路徑。如果通過一個位掩碼(bitmask)訪問每一 個顏色值,那麼將只受圖像格式的位數限制(通常是 24,如果使用 alpha 值則 為 32)。使用精確的位的路徑比使用顏色段的路徑更復雜,但是這是可以做到的 。您需要有一個可以合並各個圖像控制路徑的程序或者使用一個圖像並在上面添 加繪制。如果不需要支持重疊的路徑(即在一個位置上有多種狀態),那麼在一個 位置上可以有 2^24(或者 2^32)種狀態。還可混合這兩種方法。例如,通過位 掩碼使用紅色段,而用綠色段和藍色段表示其他狀態。

圖 4 顯示了我們的逃生模擬所使用的完整控制路徑。注意多種顏色的使用, 以及如何用顏色表示不同位置上的不同行為。

圖 4. 完整的控制路徑

圖 5 局部放大了控制路徑以看得更清楚。

圖 5. 控制路徑局部細節

逃生!

在定義了狀態映射後,就可以開始在 2D 空間中移動對象了。這個示例逃生應 用程序讓可移動對象成為 Entity 類的實例。定義了兩個主要子類: Person 和 Alarm 。 Person 可以移動,而 Alarm 是靜止的。清單 2 定義了 Entity 接口 。

清單 2. Entity 接口

interface Entity {
  void addToPanel(JPanel panel, boolean shared);
  
  void updateTick();
}

addToPanel() 方法創建一個或者多個 Swing 組件來表示對象並將它們添加到 所提供的面板中,這些組件一般是帶有圖標集的 JLabel 。面板通常是 2D 空間 的實現。它的背景顯示動畫背景。

updateTick() 方法使對象在動畫的每一周期活動。 Alarm 對象改變它們的顏 色以創建閃爍的效果。 Person 對象則移動。

Alarm 對象是簡單的閃爍對象,其實現如清單 3 所示。

清單 3. Alarm.updateTick: 閃爍

public void updateTick() {
  if (++tick % CYCLE == 0) {
     opaque = !opaque;
  }
}

Person 對象比 Alarm 復雜。它們沿著定義的控制路徑移動,如清單 4 所示 。

清單 4. Person.updateTick: 沿著路徑移動

/** Move one step along the path */
public synchronized void updateTick() {
  tick++;
  Integer tock = stops.get(new Integer(tick));
  if (tock != null) {  // adjust startTime if requested
    startTick = tick + tock.intValue();
  }
   if (tick < startTick) return;  // not my time yet
  if (isAtExit()) return;
  // Process individual movement
   Point2D location = getPosition();
  int x = (int)location.getX ();
  int y = (int)location.getY();
  switch (manager.stateAt(x, y)) {
    case BuildingManager.STATE_EXIT:
      atExit = true;
       break;
    case BuildingManager.STATE_START:
    case BuildingManager.STATE_INTERSECTION:
      // process any hints
      if   (manager.stateAt(x - 1, y) ==
            BuildingManager.STATE_HINT)
         setDirection(Person.DIR_WEST);
      else if (manager.stateAt (x + 1, y) ==
           BuildingManager.STATE_HINT)
         setDirection(Person.DIR_EAST);
      else if (manager.stateAt(x, y + 1) ==
           BuildingManager.STATE_HINT)
        setDirection (Person.DIR_SOUTH);
      else if (manager.stateAt(x, y - 1) ==
           BuildingManager.STATE_HINT)
         setDirection(Person.DIR_NORTH);
      // no hints, select a direction
      if (getDirection() == DIR_NONE) {
         if   (manager.stateAt(x - 1, y) !=
              BuildingManager.STATE_NONE)
          setDirection (Person.DIR_WEST);
        else if (manager.stateAt(x + 1, y) !=
             BuildingManager.STATE_NONE)
           setDirection(Person.DIR_EAST);
        else if (manager.stateAt(x, y + 1) !=
             BuildingManager.STATE_NONE)
          setDirection (Person.DIR_SOUTH);
        else if (manager.stateAt(x, y - 1) !=
             BuildingManager.STATE_NONE)
           setDirection(Person.DIR_NORTH);
      }
     case BuildingManager.STATE_HALLWAY:
    case BuildingManager.STATE_HINT:
      // effect motion in selected direction
      int tempX = x;
      int tempY = y;
      switch (getDirection()) {
        case DIR_EAST: x += 1; break;
        case DIR_WEST: x -= 1; break;
        case DIR_NORTH: y -= 1; break;
         case DIR_SOUTH: y += 1; break;
      }
       int check = manager.stateAt(x, y);
      if (check == manager.STATE_UNKNOWN ||
        check == manager.STATE_NONE) {
        // went off the path, backup
        x = tempX;
        y = tempY;
        if (getDirection() == DIR_EAST ||
           getDirection() == DIR_WEST) {
          if (manager.stateAt(x, y + 1) !=
             BuildingManager.STATE_NONE &&
             manager.stateAt(x, y + 1) !=
             BuildingManager.STATE_UNKNOWN) {
             setDirection(Person.DIR_SOUTH);
            y += 1;
          }
          else {
             // Only direction not checked is north
             setDirection(Person.DIR_NORTH);
            y -= 1;
          }
        }
         else {
          if (manager.stateAt(x + 1, y) !=
             BuildingManager.STATE_NONE &&
             manager.stateAt(x + 1, y) !=
             BuildingManager.STATE_UNKNOWN) {
             setDirection(Person.DIR_EAST);
            x += 1;
           }
          else {
             // Only direction not checked is south
             setDirection(Person.DIR_WEST);
            x -= 1;
          }
        }
      }
       setNextPoint(new Point(x, y));
  }
}

這個相當復雜的方法主要是檢查當前位置的映射。然後選擇要去的最佳位置。 它試圖盡可能地沿同一個方向走。注意 hints是標志,提供了優先選擇的方向。 它們通常用於開始位置和交叉路口。

Person 可以過早地(也就是在達到路徑終點之前)停止,也可以設定為經過 固定的時間後才開始移動。 Person 對象還可以留下逐漸消失的圖像痕跡(或者 歷史)以描繪出它們的運動,如圖 6 所示。

圖 6. 一個帶有歷史痕跡的 person 移動

每個人都有機會

清單 5 顯示了用於移動實體的邏輯。這個過程在每個動畫周期中執行一次。

清單 5. 移動所有實體

/** Move the entities around the pattern */
public void moveEntities() {
  // update (move) the people
  for (Iterator iter = people.iterator(); iter.hasNext();) {
     Object next = iter.next();
    if (next instanceof Person) {
      ((Person)next).updateTick();
    }
  }
   // update the other entities
  for (Iterator iter = entities.iterator(); iter.hasNext();) {
    Object next = iter.next();
    if (next instanceof Entity) {
       ((Entity)next).updateTick();
    }
  }
}

動畫中的每個新幀(frame)都是由清單 6 所顯示的過程創建的。注意實體將 它們自己添加到幀中。

清單 6. 在當前位置添加實體

/** Advance the animation */
public void prepareNextFrame (boolean update, boolean shared) {
  setBackground (Color.black);
  if (update) {
    manager.moveEntities ();
  }
  mainPanel.setBounds(getBounds());
   mainPanel.removeAll();
  // add the people
  for (Iterator iter = manager.getPeople().iterator();
     iter.hasNext();) {
    Person person = (Person)iter.next();
     person.addToPanel(mainPanel, shared);
  }
  // add the entities
  for (Iterator iter = manager.getEntities().iterator ();
     iter.hasNext();) {
    Entity entity = (Entity) iter.next();
    entity.addToPanel(mainPanel, shared);
  }
}

我們用清單 7 中的代碼實現連續的動畫。 delay 值控制動畫運行的快慢以及 它占用 CPU 時間的多少。

清單 7. 動畫周期

public void runUpdates(int delay) {
  TimerTask task = new TimerTask() {
    public void run() {
       SwingUtilities.invokeLater(new Runnable() {
           public void run() {
            prepareNextFrame();
            repaint();
            if (manager.getPeople().size() == 0) {
               cancel();
            }
          }
       });
    }
  };
  (new Timer (true)).scheduleAtFixedRate(task, 0, delay);
}

下面是我們動畫序列的幾個屏幕快照。圖 7 顯示了在行動之前的逃生模擬( 完成了全部過程的大約 10%)。

圖 7. 剛開始時的逃生序列

圖 8 顯示在完成了大約 50% 時的動畫序列。

圖 8. 數次更新後的逃生序列

圖 9 顯示接近完成時的序列(盡管序列實際上是無限循環運行的)。

圖 9. 接近完成時的逃生序列

在圖 10 中可以看到實體是如何沿著控制路徑移動的,它應當可以讓您更好地 理解運行是如何實現的。

圖 10. 實體沿著控制路徑移動

繪制舞台

類 BuildingViewer 創建讓對象在其中移動的容器。 paintChldren() 方法首 先繪制背景圖像,然後是報警消息,最後是表示不同實體的子組件。

清單 8. BuildingViewer.paintChildren

private int paintCount;
private static final Color evacColor = new Color(255, 0, 0, 128);
 :
/** draw the background, massage and entities */
public void paintChildren(Graphics g) {
   paintCount++;
  if (background != null) {
    Graphics g2 = g.create();
    try {
      // draw the background
      g2.drawImage(background.getImage(),
              (int)getLocation().getX(),
             (int) getLocation().getY(),
             (int)getLocation ().getX() + getWidth(),
             (int)getLocation ().getY() + getHeight(),
             0, 0,
              background.getIconWidth(),
             background.getIconHeight(),
             Color.black, null);
      // draw the alert message (if any)
       if (alertMessage != null) {
        if (paintCount % alertPeriod >= (alertPeriod / 2)) {
          Font f = g2.getFont();
          Font f2 = f.deriveFont((float) alertSize);
          FontMetrics fm = Toolkit.getDefaultToolkit().
            getFontMetrics (f2);
          int fHeight = fm.getHeight(),
             fAscent = fm.getAscent();
          int sWidth = fm.stringWidth(alertMessage);
           g2.setFont(f2);
          Graphics2D g2d = (Graphics2D) g2;
          g2d.setStroke(new BasicStroke(10));
           g2.setColor(evacColor);
           g2.drawString(alertMessage,
                  (getWidth() - sWidth) / 2,
                  (getHeight() - fHeight) / 2 +
                    Ascent);
        }
      }
    } finally {
      g2.dispose();
    }
  }
   super.paintChildren(g);
}

要創建有效的動畫,需要移動的對象。清單 9 顯示的代碼根據所提供的輸入 創建一系列同樣類型(即殘疾的、非殘疾的、消防員等)的 Person 實體。

清單 9. 創建相同類型的實體

/** initialize paths */
protected static void initLoop (BuildingManager manager, ImageIcon icon,
                 int[] locs, String[] names,
                int [] starts, int[] appear, int[][][] stops)
{
  LinkedList startPts = (LinkedList)manager.
             getAvailableStartingPoints();
  // for all specified locations - create a Person
  for (int i = 0; i < locs.length; i++) {
     JLabel label = new JLabel(names[i], icon, JLabel.CENTER);
     label.setFont(new Font(label.getFont().getName(),
                 label.getFont().getStyle(), 20));
    Person person = new Person(manager, label,
                   (Point2D)startPts.get(
                   Math.min(startPts.size() - 1, locs[i])),
                   starts[i]);
    person.setAppearTick(appear[i]);
     // defines stop locations for each Person
    for (int j = 0; j < stops[i].length; j++) {
      person.addStop(stops [i][j][0], stops[i][j][1]);
    }
    manager.addEntity (person);
  }
}

清單 10 用清單 9 中的 initLoop 代碼定義了一組 Person 實體。這段代碼 使用幾個平行的數組(根據 locs 數組的長度)提供有關要創建的對象的信息。 locs 數組為用控制路徑提供的一組定義好的開始位置提供了索引。 starts 值指 定在什麼時間讓 Person 開始移動。 appear 值定義了什麼時候應當讓 Person 變為可見(通常是在開始移動之前)。 stops 值指定每個 Person 可以有的停止 點(可能有多個)。

盡管下面顯示的代碼是以手工鍵入值,但是也可以通過增加表示位置實體狀態 的新顏色來從控制路徑獲得大多數這些輸入值。這種增強可以簡化這些值的輸入 ,並減少控制路徑改變時出錯的可能性。

清單 10. 創建所有 Person 實體

/** Make some demo people */
static public void createPeople (BuildingManager manager,
                ImageIcon employIcon,
                ImageIcon fireIcon,
                ImageIcon disabledIcon)
{
  // Main character - ALEX
  int locs[] = new int[] {42};
  String names[] = new String[] {"Alex"};
  int starts[] = new int[] { 300 };
  int appear[] = new int[] { 0 };
  int stops[][][] = new int[][][] {{}};
  initLoop(manager, employIcon, locs, names, starts, appear, stops);
  // Some disabled people
  locs = new int[] { 39, 45 };
  names = new String[] { "Karen", "Mike" };
  starts = new int[] { 0, 0};
  appear = new int[] { 0, 0 };
  stops = new int[][][] {{{1, 164}, {560, 20}},
               {{1, 141}, {460, 30}}};
  initLoop(manager, disabledIcon, locs, names, starts, appear, stops);
  // Some Assisters
  locs = new int[] {44, 49, 37, 46};
  names = new String[] { "Tom", "Joe", "Cathy", "Larry" };
  starts = new int[] { 0, 0, 0, 0};
  appear = new int[] { 0, 0, 0, 0 };
  stops = new int [][][] {{{120, 52}, {560, 20}},
               {{155, 24}, {560, 20}},
              {{122, 27}, {460, 30}},
              {{100, 59}, {460, 30}}};
  initLoop(manager, employIcon, locs, names, starts, appear, stops);
  // A firemen
  locs = new int[] { 25 };
   names = new String[] { "FD", "FD 2", "FD 3" };
  starts = new int [] { 400, 400, 400};
  appear = new int[] { 400, 400, 400 };
   stops = new int [][][] {{},{}, {}};
  initLoop(manager, fireIcon, locs, names, starts, appear, stops);
   :
   :  **** many additional definition sets omitted ****
   :
}

最後,清單 11 顯示了如何創建一個 Alarm 實體。顯然,我們可以容易地根 據需要增加更多的報警。

清單 11. 創建報警

/** Make some demo alarms */
static public void createAlarms (BuildingManager manager) {
  final int alarms[] = { 12 };
   LinkedList startPts = (LinkedList)manager.
                getAvailableStartingPoints();
  for (int i = 0; i < alarms.length; i++) {
    manager.addEntity(
      new Alarm(manager, (Point2D)startPts.get(alarms[i])));
  }
}

結束語

在本文中,我們展示了如何用無損圖像、Swing 技術和自定義的動畫引擎來生 成 2D 動畫中的運動路徑。這種方法使我們可以用控制路徑以一種快速和可預言 的方式可視化地創建動畫。這種技術的優點如下:

易於使用 大多數編輯程序都有幾種生成直線、圓弧和其他形狀的方法。這些 選擇使我們可以手工迅速生成一些路徑,同時減少錯誤。這對於某些行為是非常 有用的。

引用圖像 當動畫相對於背景圖像運動時(如在 圖 1中),我們可能希望讓對 象在圖像中的某些區域內運動,如讓對象保持在走廊中。許多圖像編輯程序可以 讓我們使用半透明的圖層在背景圖上面生成控制圖像。這樣我們就可以容易地創 建與背景圖相匹配的控制路徑,因為在生成控制圖像時可以同時看到疊加在一起 的兩幅圖像。

增加繪制 通過混合顏色,我們可以在一個位置上編碼多種行為。例如,使用 RGB 顏色時,可以用紅色 (0xFF0000) 表示一個對象所要走的路徑,綠色 (0x00FF00) 編碼另一個對象要走的路徑。使用增加繪制時,在路徑交叉的點將會 是黃色 (0xFFFF00)。

使用這種增加繪制模型時,當使用比如說一個 32 位顏色模型時,可以從一個 給定位置以位掩碼的形式提取出 32 種不同的行為。盡管我們只描述了一種簡單 的行為,但是可以編碼的行為的數量只受在該圖像格式中每種顏色所具有的位數 的限制。

我們還給出並描述了沿著一組路徑移動對象的一個簡單動畫引擎。在例子中, 每一個對象稱為 Entity 並實現為 JLabel ,它們被驅動周期性地更新其位置和/ 或者外觀。使用一個長期運行的計時器線程驅動這個過程。用一個 JPanel 作為 所有對象的容器,並作為繪制背景的方法。

代碼下載: http://download.boulder.ibm.com/ibmdl/pub/software/dw/library/j-animat -source.zip

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved