Browse Source

Merge pull request #37 from Tencent/feature/vap_java_tool

vaptool java
hexleo 5 years ago
parent
commit
70d4e6c558
100 changed files with 1394 additions and 285 deletions
  1. 1 1
      Android/PlayerProj/animplayer/publish.gradle
  2. 6 2
      Android/PlayerProj/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimConfigManager.kt
  3. 3 1
      Android/PlayerProj/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimPlayer.kt
  4. 6 0
      Android/PlayerProj/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimView.kt
  5. 106 60
      Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/AnimTool.java
  6. 30 13
      Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/CommonArg.java
  7. 103 26
      Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/CommonArgTool.java
  8. 53 45
      Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/GetAlphaFrame.java
  9. 82 8
      Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/Main.java
  10. 19 0
      Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/TLog.java
  11. 24 0
      Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/data/PointRect.java
  12. 154 24
      Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/ui/ToolUI.java
  13. 288 0
      Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/ui/VapxUI.java
  14. 79 0
      Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/vapx/FrameSet.java
  15. 207 0
      Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/vapx/GetMaskFrame.java
  16. 90 0
      Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/vapx/SrcSet.java
  17. BIN
      Android/aar/vap_2.0.6.aar
  18. 52 0
      tool/Mac_Tool.md
  19. 91 34
      tool/README.md
  20. BIN
      tool/Release/VapxTool.dmg
  21. 0 71
      tool/VapTool_Java.md
  22. BIN
      tool/images/4.png
  23. BIN
      tool/images/vaptool_java_01.png
  24. BIN
      tool/images/vaptool_java_02.png
  25. 0 0
      tool/simple_demo/000.png
  26. 0 0
      tool/simple_demo/001.png
  27. 0 0
      tool/simple_demo/002.png
  28. 0 0
      tool/simple_demo/003.png
  29. 0 0
      tool/simple_demo/004.png
  30. 0 0
      tool/simple_demo/005.png
  31. 0 0
      tool/simple_demo/006.png
  32. 0 0
      tool/simple_demo/007.png
  33. 0 0
      tool/simple_demo/008.png
  34. 0 0
      tool/simple_demo/009.png
  35. 0 0
      tool/simple_demo/010.png
  36. 0 0
      tool/simple_demo/011.png
  37. 0 0
      tool/simple_demo/012.png
  38. 0 0
      tool/simple_demo/013.png
  39. 0 0
      tool/simple_demo/014.png
  40. 0 0
      tool/simple_demo/015.png
  41. 0 0
      tool/simple_demo/016.png
  42. 0 0
      tool/simple_demo/017.png
  43. 0 0
      tool/simple_demo/018.png
  44. 0 0
      tool/simple_demo/019.png
  45. 0 0
      tool/simple_demo/020.png
  46. 0 0
      tool/simple_demo/021.png
  47. 0 0
      tool/simple_demo/022.png
  48. 0 0
      tool/simple_demo/023.png
  49. 0 0
      tool/simple_demo/024.png
  50. 0 0
      tool/simple_demo/025.png
  51. 0 0
      tool/simple_demo/026.png
  52. 0 0
      tool/simple_demo/027.png
  53. 0 0
      tool/simple_demo/028.png
  54. 0 0
      tool/simple_demo/029.png
  55. 0 0
      tool/simple_demo/030.png
  56. 0 0
      tool/simple_demo/031.png
  57. 0 0
      tool/simple_demo/032.png
  58. 0 0
      tool/simple_demo/033.png
  59. 0 0
      tool/simple_demo/034.png
  60. 0 0
      tool/simple_demo/035.png
  61. 0 0
      tool/simple_demo/036.png
  62. 0 0
      tool/simple_demo/037.png
  63. 0 0
      tool/simple_demo/038.png
  64. 0 0
      tool/simple_demo/039.png
  65. 0 0
      tool/simple_demo/040.png
  66. 0 0
      tool/simple_demo/041.png
  67. 0 0
      tool/simple_demo/042.png
  68. 0 0
      tool/simple_demo/043.png
  69. 0 0
      tool/simple_demo/044.png
  70. 0 0
      tool/simple_demo/045.png
  71. 0 0
      tool/simple_demo/046.png
  72. 0 0
      tool/simple_demo/047.png
  73. 0 0
      tool/simple_demo/048.png
  74. 0 0
      tool/simple_demo/049.png
  75. 0 0
      tool/simple_demo/050.png
  76. 0 0
      tool/simple_demo/051.png
  77. 0 0
      tool/simple_demo/052.png
  78. 0 0
      tool/simple_demo/053.png
  79. 0 0
      tool/simple_demo/054.png
  80. 0 0
      tool/simple_demo/055.png
  81. 0 0
      tool/simple_demo/056.png
  82. 0 0
      tool/simple_demo/057.png
  83. 0 0
      tool/simple_demo/058.png
  84. 0 0
      tool/simple_demo/059.png
  85. 0 0
      tool/simple_demo/060.png
  86. 0 0
      tool/simple_demo/061.png
  87. 0 0
      tool/simple_demo/062.png
  88. 0 0
      tool/simple_demo/063.png
  89. 0 0
      tool/simple_demo/064.png
  90. 0 0
      tool/simple_demo/065.png
  91. 0 0
      tool/simple_demo/066.png
  92. 0 0
      tool/simple_demo/067.png
  93. 0 0
      tool/simple_demo/068.png
  94. 0 0
      tool/simple_demo/069.png
  95. 0 0
      tool/simple_demo/070.png
  96. 0 0
      tool/simple_demo/071.png
  97. 0 0
      tool/simple_demo/072.png
  98. 0 0
      tool/simple_demo/073.png
  99. 0 0
      tool/simple_demo/074.png
  100. 0 0
      tool/simple_demo/075.png

+ 1 - 1
Android/PlayerProj/animplayer/publish.gradle

@@ -23,7 +23,7 @@ ext {
     // library artifact(单个module一般就填写library name)
     artifact = 'animplayer'
     libraryName = 'animplayer'
-    libraryVersion = '2.0.9'
+    libraryVersion = '2.0.10'
     libraryDescription = ''
     // bintrayName 是你在网页Repository页面能看到的名称
     bintrayName = 'vap'

+ 6 - 2
Android/PlayerProj/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimConfigManager.kt

@@ -36,17 +36,21 @@ class AnimConfigManager(val player: AnimPlayer) {
      * 解析配置
      * @return true 解析成功 false 解析失败
      */
-    fun parseConfig(fileContainer: FileContainer, defaultVideoMode: Int, defaultFps: Int): Int {
+    fun parseConfig(fileContainer: FileContainer, enableVersion1: Boolean, defaultVideoMode: Int, defaultFps: Int): Int {
         try {
             isParsingConfig = true
             // 解析vapc
             val time = SystemClock.elapsedRealtime()
             val result = parse(fileContainer, defaultVideoMode, defaultFps)
-            ALog.i(TAG, "parseConfig cost=${SystemClock.elapsedRealtime() - time}ms")
+            ALog.i(TAG, "parseConfig cost=${SystemClock.elapsedRealtime() - time}ms enableVersion1=$enableVersion1 result=$result")
             if (!result) {
                 isParsingConfig = false
                 return Constant.REPORT_ERROR_TYPE_PARSE_CONFIG
             }
+            if (config?.isDefaultConfig == true && !enableVersion1) {
+                isParsingConfig = false
+                return Constant.REPORT_ERROR_TYPE_PARSE_CONFIG
+            }
             // 插件解析配置
             val resultCode = config?.let {
                 player.pluginManager.onConfigCreate(it)

+ 3 - 1
Android/PlayerProj/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimPlayer.kt

@@ -42,6 +42,8 @@ class AnimPlayer(val animView: AnimView) {
         }
     var supportMaskBoolean : Boolean = false
     var maskEdgeBlurBoolean : Boolean = false
+    // 是否兼容老版本 默认不兼容
+    var enableVersion1 : Boolean = false
     // 视频模式
     var videoMode: Int = Constant.VIDEO_MODE_SPLIT_HORIZONTAL
     var isDetachedFromWindow = false
@@ -79,7 +81,7 @@ class AnimPlayer(val animView: AnimView) {
         }
         // 在线程中解析配置
         decoder?.renderThread?.handler?.post {
-            val result = configManager.parseConfig(fileContainer, videoMode, fps)
+            val result = configManager.parseConfig(fileContainer, enableVersion1, videoMode, fps)
             if (result != Constant.OK) {
                 decoder?.onFailed(result, Constant.getErrorMsg(result))
                 isStartRunning = false

+ 6 - 0
Android/PlayerProj/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimView.kt

@@ -188,6 +188,12 @@ open class AnimView @JvmOverloads constructor(context: Context, attrs: Attribute
         player?.updateMaskConfig(maskConfig)
     }
 
+
+    @Deprecated("Compatible older version mp4, default false")
+    fun enableVersion1(enable: Boolean) {
+        player?.enableVersion1 = enable
+    }
+
     // 兼容老版本视频模式
     @Deprecated("Compatible older version mp4")
     fun setVideoMode(mode: Int) {

+ 106 - 60
Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/AnimTool.java

@@ -15,15 +15,16 @@
  */
 package com.tencent.qgame.playerproj.animtool;
 
+import com.tencent.qgame.playerproj.animtool.vapx.FrameSet;
+import com.tencent.qgame.playerproj.animtool.vapx.GetMaskFrame;
+import com.tencent.qgame.playerproj.animtool.vapx.SrcSet;
+
 import javax.imageio.ImageIO;
 import java.awt.image.BufferedImage;
-import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileWriter;
 import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.concurrent.TimeUnit;
 
 public class AnimTool {
 
@@ -32,7 +33,8 @@ public class AnimTool {
     public static final String OUTPUT_DIR = "output"+ File.separator;
     public static final String FRAME_IMAGE_DIR = "frames"+ File.separator;
     public static final String VIDEO_FILE = "video.mp4";
-    public static final String TEM_VIDEO_FILE = "tmp_video.mp4";
+    public static final String TEMP_VIDEO_FILE = "tmp_video.mp4";
+    public static final String TEMP_VIDEO_AUDIO_FILE = "tmp_video_audio.mp4";
     public static final String VAPC_BIN_FILE = "vapc.bin";
     public static final String VAPC_JSON_FILE = "vapc.json";
 
@@ -42,6 +44,7 @@ public class AnimTool {
     private volatile int finishThreadCount = 0;
     private long time;
     private GetAlphaFrame getAlphaFrame = new GetAlphaFrame();
+    private GetMaskFrame getMaskFrame = new GetMaskFrame();
     private IToolListener toolListener;
 
     public void setToolListener(IToolListener toolListener) {
@@ -57,7 +60,7 @@ public class AnimTool {
         createAllFrameImage(commonArg, new Runnable() {
             @Override
             public void run() {
-                if (needVideo) {
+                if (finalCheck(commonArg) && needVideo) {
                     // 最终生成视频文件
                     createVideo(commonArg);
                 }
@@ -71,7 +74,23 @@ public class AnimTool {
      * @return
      */
     private boolean checkCommonArg(CommonArg commonArg) throws Exception {
-        return CommonArgTool.autoFillAndCheck(commonArg);
+        return CommonArgTool.autoFillAndCheck(commonArg, toolListener);
+    }
+
+    private boolean finalCheck(CommonArg commonArg) {
+        if (commonArg.isVapx) {
+            if (commonArg.srcSet.srcs.isEmpty()) {
+                TLog.i(TAG, "vapx error: src is empty");
+                return false;
+            }
+            for (SrcSet.Src src : commonArg.srcSet.srcs) {
+                if (src.w <=0 || src.h <= 0) {
+                    TLog.i(TAG, "vapx error: src.id=" + src.srcId + ",src.w=" + src.w + ",src.h=" + src.h);
+                    return false;
+                }
+            }
+        }
+        return true;
     }
 
     private void createAllFrameImage(final CommonArg commonArg, final Runnable finishRunnable) throws Exception{
@@ -107,6 +126,11 @@ public class AnimTool {
                 @Override
                 public void run() {
                     for(int i = threadIndexSet[k][0]; i<threadIndexSet[k][1]; i++) {
+                        try {
+                            createFrame(commonArg, i);
+                        } catch (Exception e) {
+                            e.printStackTrace();
+                        }
                         synchronized (AnimTool.class) {
                             totalP++;
                             float progress = totalP * 1.0f / commonArg.totalFrame;
@@ -116,11 +140,6 @@ public class AnimTool {
                                 TLog.i(TAG, "progress " + progress);
                             }
                         }
-                        try {
-                            createFrame(commonArg, i);
-                        } catch (Exception e) {
-                            e.printStackTrace();
-                        }
                     }
                     synchronized (AnimTool.class) {
                         finishThreadCount++;
@@ -141,18 +160,21 @@ public class AnimTool {
     }
 
     private void createFrame(CommonArg commonArg, int frameIndex) throws Exception {
-        int w = commonArg.videoW;
-        int h = commonArg.videoH;
         File inputFile = new File(commonArg.inputPath + String.format("%03d", frameIndex)+".png");
-        GetAlphaFrame.AlphaFrameOut videoFrame = getAlphaFrame.createFrame(commonArg.orin, w, h,
-                commonArg.gap, commonArg.wFill, commonArg.hFill, inputFile);
+        GetAlphaFrame.AlphaFrameOut videoFrame = getAlphaFrame.createFrame(commonArg, inputFile);
+        if (commonArg.isVapx) {
+            FrameSet.FrameObj frameObj = getMaskFrame.getFrameObj(frameIndex, commonArg, videoFrame.argb);
+            if (frameObj != null) {
+                commonArg.frameSet.frameObjs.add(frameObj);
+            }
+        }
         if (videoFrame == null) {
             TLog.i(TAG, "frameIndex="+frameIndex +" is empty");
             return;
         }
         // 最后保存图片
-        BufferedImage outBuf = new BufferedImage(videoFrame.outW, videoFrame.outH, BufferedImage.TYPE_INT_ARGB);
-        outBuf.setRGB(0,0, videoFrame.outW, videoFrame.outH, videoFrame.argb, 0, videoFrame.outW);
+        BufferedImage outBuf = new BufferedImage(commonArg.outputW, commonArg.outputH, BufferedImage.TYPE_INT_ARGB);
+        outBuf.setRGB(0,0, commonArg.outputW, commonArg.outputH, videoFrame.argb, 0, commonArg.outputW);
 
         File outputFile = new File(commonArg.frameOutputPath + String.format("%03d", frameIndex) +".png");
         ImageIO.write(outBuf, "PNG", outputFile);
@@ -180,17 +202,30 @@ public class AnimTool {
                 TLog.i(TAG, "createMp4 fail");
                 return;
             }
+            String tempVideoName = TEMP_VIDEO_FILE;
+            if (commonArg.needAudio) {
+                result = mergeAudio2Mp4(commonArg, tempVideoName);
+                if (!result) {
+                    TLog.i(TAG, "mergeAudio2Mp4 fail");
+                    return;
+                }
+                tempVideoName = TEMP_VIDEO_AUDIO_FILE;
+            }
+
             String input = commonArg.outputPath + VAPC_JSON_FILE;
             // 由json变为bin文件
             String vapcBinPath = mp4BoxTool(input, commonArg.outputPath);
             // 将bin文件合并到mp4里
-            result = mergeBin2Mp4(commonArg, vapcBinPath, commonArg.outputPath);
+            result = mergeBin2Mp4(commonArg, vapcBinPath, tempVideoName, commonArg.outputPath);
             if (!result) {
                 TLog.i(TAG, "mergeBin2Mp4 fail");
                 return;
             }
             // 删除临时视频文件
-            new File(commonArg.outputPath + TEM_VIDEO_FILE).delete();
+            new File(commonArg.outputPath + TEMP_VIDEO_FILE).delete();
+            if (commonArg.needAudio) {
+                new File(commonArg.outputPath + TEMP_VIDEO_AUDIO_FILE).delete();
+            }
             new File(commonArg.outputPath + VAPC_BIN_FILE).delete();
             // 计算文件md5
             String md5 = new Md5Util().getFileMD5(new File(commonArg.outputPath + VIDEO_FILE), commonArg.outputPath);
@@ -205,36 +240,34 @@ public class AnimTool {
      * @param commonArg
      */
     private void createVapcJson(CommonArg commonArg) {
-        String json = "{\"info\":{\"v\":$(v),\"f\":$(f),\"w\":$(w),\"h\":$(h),\"videoW\":$(videoW),\"videoH\":$(videoH),\"orien\":0,\"fps\":$(fps),\"isVapx\":0,\"aFrame\":$(aFrame),\"rgbFrame\":$(rgbFrame)}}";
-        json = json.replace("$(v)", String.valueOf(commonArg.version));
-        json = json.replace("$(f)", String.valueOf(commonArg.totalFrame));
-        json = json.replace("$(w)", String.valueOf(commonArg.videoW));
-        json = json.replace("$(h)", String.valueOf(commonArg.videoH));
-        json = json.replace("$(fps)", String.valueOf(commonArg.fps));
-        int realW = 0;
-        int realH = 0;
-        int cx, cy;
-        String aFrame = "[0,0,"+commonArg.videoW+","+commonArg.videoH+"]";
-        String rgbFrame = "[0,0,0,0]";
-        if (commonArg.orin == CommonArg.ORIN_H) { // 水平对齐
-            realW = 2 * commonArg.videoW + commonArg.gap;
-            realH = commonArg.videoH;
-            cx = commonArg.videoW + commonArg.gap;
-            cy = 0;
-        } else { // 上下对齐
-            realW = commonArg.videoW;
-            realH = 2 * commonArg.videoH + commonArg.gap;
-            cx = 0;
-            cy = commonArg.videoH + commonArg.gap;
+
+        String json = "\"info\":{" +
+                "\"v\":" + commonArg.version + "," +
+                "\"f\":" + commonArg.totalFrame + "," +
+                "\"w\":" + commonArg.rgbPoint.w + "," +
+                "\"h\":" + commonArg.rgbPoint.h + "," +
+                "\"fps\":" + commonArg.fps + "," +
+                "\"videoW\":" + commonArg.outputW + "," +
+                "\"videoH\":" + commonArg.outputH + "," +
+                "\"aFrame\":" + commonArg.alphaPoint.toString() + "," +
+                "\"rgbFrame\":" + commonArg.rgbPoint.toString() + "," +
+                "\"isVapx\":" + (commonArg.isVapx ? 1 : 0) + "," +
+                "\"orien\":" + 0 +
+                "}";
+        TLog.i(TAG, "{" + json + "}");
+
+        StringBuilder sb = new StringBuilder();
+        sb.append("{");
+        sb.append(json);
+        if (commonArg.isVapx) {
+            sb.append(",");
+            sb.append(commonArg.srcSet.toString());
+            sb.append(",");
+            sb.append(commonArg.frameSet.toString());
         }
-        rgbFrame = "["+cx+","+cy+","+commonArg.videoW+","+commonArg.videoH+"]";
-
-        realW += commonArg.wFill;
-        realH += commonArg.hFill;
-        json = json.replace("$(videoW)", String.valueOf(realW));
-        json = json.replace("$(videoH)", String.valueOf(realH));
-        json = json.replace("$(aFrame)", aFrame);
-        json = json.replace("$(rgbFrame)", rgbFrame);
+        sb.append("}");
+        json = sb.toString();
+
         try {
             BufferedWriter writer = new BufferedWriter(new FileWriter(commonArg.outputPath + VAPC_JSON_FILE));
             writer.write(json);
@@ -244,8 +277,6 @@ public class AnimTool {
             e.printStackTrace();
             throw new RuntimeException();
         }
-        TLog.i(TAG,json);
-
     }
 
 
@@ -254,8 +285,6 @@ public class AnimTool {
 
     /**
      * 创建mp4
-     * @param commonArg
-     * @throws Exception
      */
     private boolean createMp4(CommonArg commonArg, String videoPath, String frameImagePath) throws Exception {
         String[] cmd = null;
@@ -269,17 +298,18 @@ public class AnimTool {
                     "-level", "4.0",
                     "-tag:v", "hvc1",
                     "-bufsize", "2000k",
-                    "-y", videoPath + TEM_VIDEO_FILE};
+                    "-y", videoPath + TEMP_VIDEO_FILE};
         } else {
             cmd = new String[]{commonArg.ffmpegCmd, "-r", String.valueOf(commonArg.fps),
                     "-i", frameImagePath + "%03d.png",
                     "-pix_fmt", "yuv420p",
                     "-vcodec", "libx264",
                     "-b:v", "3000k",
-                    "-profile:v", "baseline",
-                    "-level", "3.0",
+                    "-profile:v", "main",
+                    "-level", "4.0",
                     "-bf", "0",
-                    "-y", videoPath + TEM_VIDEO_FILE};
+                    "-bufsize", "3000k",
+                    "-y", videoPath + TEMP_VIDEO_FILE};
         }
 
         TLog.i(TAG, "run createMp4");
@@ -288,13 +318,28 @@ public class AnimTool {
         return result == 0;
     }
 
+    /**
+     * 合并音频文件
+     */
+    private boolean mergeAudio2Mp4(CommonArg commonArg, String tempVideoFile) throws Exception {
+        String[] cmd = new String[] {commonArg.ffmpegCmd,
+                "-i", commonArg.audioPath,
+                "-i", commonArg.outputPath + tempVideoFile,
+                "-c:v", "copy",
+                "-c:a", "aac",
+                "-y", commonArg.outputPath + TEMP_VIDEO_AUDIO_FILE};
+        TLog.i(TAG, "run mergeAudio2Mp4");
+        int result = ProcessUtil.run(cmd);
+        TLog.i(TAG, "mergeAudio2Mp4 result=" + (result == 0? "success" : "fail"));
+        return result == 0;
+    }
+
+
     /**
      * 合并vapc.bin到mp4里
-     * @param inputFile
-     * @throws Exception
      */
-    private boolean mergeBin2Mp4(CommonArg commonArg, String inputFile, String videoPath) throws Exception{
-        String[] cmd = new String[] {commonArg.mp4editCmd, "--insert", ":"+inputFile+":1", videoPath + TEM_VIDEO_FILE, videoPath + VIDEO_FILE};
+    private boolean mergeBin2Mp4(CommonArg commonArg, String inputFile, String tempVideoFile, String videoPath) throws Exception{
+        String[] cmd = new String[] {commonArg.mp4editCmd, "--insert", ":"+inputFile+":1", videoPath + tempVideoFile, videoPath + VIDEO_FILE};
         TLog.i(TAG, "run mergeBin2Mp4");
         int result = ProcessUtil.run(cmd);
         TLog.i(TAG, "mergeBin2Mp4 result=" + (result == 0? "success" : "fail"));
@@ -313,6 +358,7 @@ public class AnimTool {
 
     public interface IToolListener {
         void onProgress(float progress);
+        void onWarning(String msg);
         void onError();
         void onComplete();
     }

+ 30 - 13
Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/CommonArg.java

@@ -15,10 +15,11 @@
  */
 package com.tencent.qgame.playerproj.animtool;
 
-public class CommonArg {
+import com.tencent.qgame.playerproj.animtool.data.PointRect;
+import com.tencent.qgame.playerproj.animtool.vapx.FrameSet;
+import com.tencent.qgame.playerproj.animtool.vapx.SrcSet;
 
-    public static final int ORIN_H = 1; // 左右对齐
-    public static final int ORIN_V = 2; // 上下对齐
+public class CommonArg {
 
     public String ffmpegCmd = "ffmpeg"; // ffmpeg 命令地址
 
@@ -26,14 +27,14 @@ public class CommonArg {
 
     public boolean enableH265 = false; // 是否开启h265
 
-    public int fps = 0;
+    public int fps = 24;
 
     public String inputPath; // 输入帧文件地址
 
-
+    public float scale = 0.5f; // alpha 区域缩放大小
 
     /**
-     * 无需手动配置
+     * 自动填充参数配置
      */
     public String outputPath; // 输出地址
 
@@ -41,19 +42,32 @@ public class CommonArg {
 
     public int version = 2;
 
-    public int orin = ORIN_H;
+    public int gap; // rgb 与 alpha 之间间隔距离
 
-    public int videoW;
+    public int totalFrame;
 
-    public int videoH;
+    public PointRect rgbPoint = new PointRect(); // rgb 区域 原始图像区域
 
-    public int gap; // rgb 与 alpha 之间间隔距离
+    public PointRect alphaPoint = new PointRect();  // alpha 区域
 
-    public int wFill; // 宽度填充
+    public boolean isVLayout = false; // 是否为垂直布局
 
-    public int hFill; // 高度填充
+    public int outputW = 0; // 输出最终视频的宽高
 
-    public int totalFrame;
+    public int outputH = 0;
+
+    public boolean needAudio = false;
+
+    public String audioPath; // 音频地址
+
+    /**
+     * 融合动画相关参数
+     */
+
+    public boolean isVapx = false;
+
+    public SrcSet srcSet = new SrcSet();
+    public FrameSet frameSet = new FrameSet();
 
     @Override
     public String toString() {
@@ -62,7 +76,10 @@ public class CommonArg {
                 ", mp4editCmd='" + mp4editCmd + '\'' +
                 ", enableH265=" + enableH265 +
                 ", fps=" + fps +
+                ", scale=" + scale +
                 ", inputPath='" + inputPath + '\'' +
+                ", needAudio=" + needAudio + '\'' +
+                ", audioPath='" + audioPath + '\'' +
                 '}';
     }
 }

+ 103 - 26
Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/CommonArgTool.java

@@ -1,5 +1,7 @@
 package com.tencent.qgame.playerproj.animtool;
 
+import com.tencent.qgame.playerproj.animtool.vapx.SrcSet;
+
 import java.awt.image.BufferedImage;
 import java.io.File;
 
@@ -17,59 +19,138 @@ class CommonArgTool {
      * 参数自动填充
      * @param commonArg
      */
-    static boolean autoFillAndCheck(CommonArg commonArg) throws Exception {
+    static boolean autoFillAndCheck(CommonArg commonArg, AnimTool.IToolListener toolListener) throws Exception {
 
         String os = System.getProperty("os.name");
         TLog.i(TAG, os);
 
-        if (commonArg.inputPath == null && commonArg.inputPath == "") {
-            TLog.i(TAG, "error: input path invalid");
+        if (commonArg.inputPath == null && "".equals(commonArg.inputPath)) {
+            TLog.e(TAG, "input path invalid");
             return false;
         }
 
         //  路径检查
         File input = new File(commonArg.inputPath);
         if (!input.exists()) {
-            TLog.i(TAG, "error: input path invalid " + commonArg.inputPath);
+            TLog.e(TAG, "input path invalid " + commonArg.inputPath);
             return false;
         }
 
         if (!File.separator.equals(commonArg.inputPath.substring(commonArg.inputPath.length() - 1))) {
             commonArg.inputPath = commonArg.inputPath + File.separator;
         }
+
+        // 检查音频文件是否存在
+        if (commonArg.needAudio) {
+            File audio = new File(commonArg.audioPath);
+            if (!audio.exists() || commonArg.audioPath == null || commonArg.audioPath.length() < 3) {
+                TLog.e(TAG , "audio file not exists " + commonArg.audioPath);
+                return false;
+            }
+            String type = commonArg.audioPath.substring(commonArg.audioPath.length() - 3).toLowerCase();
+            if (!"mp3".equals(type)) {
+                TLog.e(TAG , "audio file must be mp3 file " + commonArg.audioPath);
+                return false;
+            }
+        }
+
         // output path
         commonArg.outputPath = commonArg.inputPath + AnimTool.OUTPUT_DIR;
 
         // 帧图片生成路径
         commonArg.frameOutputPath = commonArg.outputPath + AnimTool.FRAME_IMAGE_DIR;
 
+        // srcId自动生成 & 融合动画路径检查 & z序
+        if (commonArg.isVapx) {
+            // vapx 强制缩小
+            commonArg.scale = 0.5f;
+            int size = commonArg.srcSet.srcs.size();
+            SrcSet.Src src;
+            for (int i=0; i<size; i++) {
+                src = commonArg.srcSet.srcs.get(i);
+                src.srcId = String.valueOf(i);
+                src.z = i;
+                File srcPath = new File(src.srcPath);
+                if (!srcPath.exists()) {
+                    TLog.e(TAG, "src="+ src.srcId+",path invalid " + src.srcPath);
+                    continue;
+                }
+                if (!File.separator.equals(src.srcPath.substring(src.srcPath.length() - 1))) {
+                    src.srcPath = src.srcPath + File.separator;
+                }
+            }
+        }
+
+        // 限定scale的值
+        if (commonArg.scale < 0.5f) {
+            commonArg.scale = 0.5f;
+        }
+
+        if (commonArg.scale > 1f) {
+            commonArg.scale = 1f;
+        }
+
         // 检查第一帧
         File firstFrame = new File(commonArg.inputPath + "000.png");
         if (!firstFrame.exists()) {
-            TLog.i(TAG, "error: first frame 000.png does not exist");
+            TLog.e(TAG, "first frame 000.png does not exist");
             return false;
         }
         // 获取视频高度
         BufferedImage inputBuf = ImageIO.read(firstFrame);
-        commonArg.videoW = inputBuf.getWidth();
-        commonArg.videoH = inputBuf.getHeight();
-        if (commonArg.videoW <= 0 || commonArg.videoH <= 0) {
-            TLog.i(TAG, "error: video size " + commonArg.videoW + "x" + commonArg.videoH);
+        commonArg.rgbPoint.w = inputBuf.getWidth();
+        commonArg.rgbPoint.h = inputBuf.getHeight();
+        if (commonArg.rgbPoint.w <= 0 || commonArg.rgbPoint.h <= 0) {
+            TLog.e(TAG, "video size " + commonArg.rgbPoint.w + "x" + commonArg.rgbPoint.h);
             return false;
         }
 
-
-        // 计算视频最佳方向
-        commonArg.orin = commonArg.videoW >= commonArg.videoH ? CommonArg.ORIN_V : CommonArg.ORIN_H;
-
         // 设置元素之间宽度
         commonArg.gap = MIN_GAP;
 
-        // 计算出 16倍数的视频
-        int[] size = calSizeFill(commonArg.orin, commonArg.gap, commonArg.videoW, commonArg.videoH, 0, 0);
-        commonArg.wFill = size[0];
-        commonArg.hFill = size[1];
+        // 计算alpha区域大小
+        commonArg.alphaPoint.w = (int) (commonArg.rgbPoint.w * commonArg.scale);
+        commonArg.alphaPoint.h = (int) (commonArg.rgbPoint.h * commonArg.scale);
+
+        // 计算视频最佳方向 (最长边最小原则)
+        int hW = commonArg.rgbPoint.w + commonArg.gap + commonArg.alphaPoint.w;
+        int hH = commonArg.rgbPoint.h;
+        int hMaxLen = Math.max(hW, hH);
+
+        int vW = commonArg.rgbPoint.w;
+        int vH = commonArg.rgbPoint.h + commonArg.gap + commonArg.alphaPoint.h;
+        int vMaxLen = Math.max(vW, vH);
+
+        if (hMaxLen > vMaxLen) { // 竖直布局
+            commonArg.isVLayout = true;
+            commonArg.alphaPoint.x = 0;
+            commonArg.alphaPoint.y = commonArg.rgbPoint.h + commonArg.gap;
+
+            commonArg.outputW = commonArg.rgbPoint.w;
+            commonArg.outputH = commonArg.rgbPoint.h + commonArg.gap + commonArg.alphaPoint.h;
+        } else { // 水平布局
+            commonArg.isVLayout = false;
+            commonArg.alphaPoint.x = commonArg.rgbPoint.w + commonArg.gap;
+            commonArg.alphaPoint.y = 0;
+
+            commonArg.outputW = commonArg.rgbPoint.w + commonArg.gap + commonArg.alphaPoint.w;
+            commonArg.outputH = commonArg.rgbPoint.h;
+        }
 
+        // 计算出 16倍数的视频
+        int[] size = calSizeFill(commonArg.outputW, commonArg.outputH, 0, 0);
+        // 得到最终视频宽高
+        commonArg.outputW += size[0];
+        commonArg.outputH += size[1];
+
+        if (commonArg.outputW > 1504 || commonArg.outputH > 1504) {
+            String msg = "[Warning] Output video width:" + commonArg.outputW + " or height:" + commonArg.outputH
+                    + " is over 1504. Some devices will display exception, like video turn green!";
+            TLog.w(TAG, msg);
+            if (toolListener != null) {
+                toolListener.onWarning(msg);
+            }
+        }
 
         // 获取总帧数
         commonArg.totalFrame = 0;
@@ -84,30 +165,26 @@ class CommonArgTool {
 
 
         if (commonArg.totalFrame <= 0) {
-            TLog.i(TAG, "error: totalFrame=" + commonArg.totalFrame);
+            TLog.e(TAG, "totalFrame=" + commonArg.totalFrame);
             return false;
         }
 
-
-
         return true;
     }
 
     /**
      * 寻找最小wFill & hFill情况下 整个视频宽高能被16整除
      */
-    private static int[] calSizeFill(int orin, int gap, int w, int h, int wFill, int hFill) {
-        int outW = (orin == CommonArg.ORIN_H ? (w * 2 + gap) : w) + wFill;
-        int outH = (orin == CommonArg.ORIN_H ? h : (h * 2 + gap)) + hFill;
+    private static int[] calSizeFill(int outW, int outH, int wFill, int hFill) {
+        boolean wCheck = (outW + wFill)% 16 == 0;
+        boolean hCheck = (outH + hFill) % 16 == 0;
 
-        boolean wCheck = outW % 16 == 0;
-        boolean hCheck = outH % 16 == 0;
         if (wCheck && hCheck) {
             return new int[]{wFill, hFill};
         }
 
         // 递归计算
-        return calSizeFill(orin, gap, w, h, wCheck? wFill : wFill + 1, hCheck? hFill : hFill + 1);
+        return calSizeFill(outW, outH, wCheck? wFill : wFill + 1, hCheck? hFill : hFill + 1);
     }
 
 

+ 53 - 45
Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/GetAlphaFrame.java

@@ -15,7 +15,12 @@
  */
 package com.tencent.qgame.playerproj.animtool;
 
+import com.tencent.qgame.playerproj.animtool.data.PointRect;
+
 import javax.imageio.ImageIO;
+
+import java.awt.geom.AffineTransform;
+import java.awt.image.AffineTransformOp;
 import java.awt.image.BufferedImage;
 import java.io.File;
 import java.io.IOException;
@@ -23,52 +28,26 @@ import java.util.Arrays;
 
 public class GetAlphaFrame {
 
-    public static final int ORIN_H = 1; // 左右对齐
-    public static final int ORIN_V = 2; // 上下对齐
-
-
     public static class AlphaFrameOut {
 
-
-        public int orin;
         public int[] argb;
-        public int w;
-        public int h;
-        public int outW;
-        public int outH;
-        public int gap;
 
-
-        public AlphaFrameOut(int orin, int[] argb, int w, int h, int outW, int outH, int gap) {
-            this.orin = orin;
+        public AlphaFrameOut(int[] argb) {
             this.argb = argb;
-            this.w = w;
-            this.h = h;
-            this.outW = outW;
-            this.outH = outH;
-            this.gap = gap;
         }
 
     }
 
-    /**
-     *
-     * @param orin
-     * @param w 原图像宽
-     * @param h 原图像高
-     * @param gap rgb 与 alpha 之间间隔距离
-     * @param inputFile
-     * @return
-     * @throws IOException
-     */
-    public AlphaFrameOut createFrame(int orin, int w, int h, int gap, int wFill, int hFill, File inputFile) throws IOException {
+    public AlphaFrameOut createFrame(CommonArg commonArg, File inputFile) throws IOException {
 
         if (!inputFile.exists()) {
             return null;
         }
 
-        int outW = (orin == ORIN_H ? (w * 2 + gap) : w) + wFill;
-        int outH = (orin == ORIN_H ? h : (h * 2 + gap)) + hFill;
+        int w = commonArg.rgbPoint.w;
+        int h = commonArg.rgbPoint.h;
+        int outW = commonArg.outputW;
+        int outH = commonArg.outputH;
 
         BufferedImage inputBuf = ImageIO.read(inputFile);
         int[] inputArgb = inputBuf.getRGB(0, 0, w, h, null, 0, w);
@@ -76,23 +55,52 @@ public class GetAlphaFrame {
         int[] outputArgb = new int[outW * outH];
         Arrays.fill(outputArgb, 0xff000000);
 
-        for (int k=0; k<2; k++) {
-            for (int x = 0; x < w; x++) {
-                for (int y = 0; y < h; y++) {
-                    int outPoint = orin == ORIN_H ? k * (w + gap) + x + y * outW : k * outW * (h + gap) + x + y * outW;
-                    if (k == 0) {
-                        int alpha = inputArgb[x + y * w] >>> 24;
-                        // r = g = b
-                        outputArgb[outPoint] = 0xff000000 + (alpha << 16) + (alpha << 8) + alpha;
-                    } else {
-                        outputArgb[outPoint] = blendBg(inputArgb[x + y * w], 0xff000000);
-                    }
-                }
+        BufferedImage alphaBuf = inputBuf;
+        int[] alphaArgb = inputArgb;
+
+        if (commonArg.scale < 1f) {
+            AffineTransform at = new AffineTransform();
+            at.scale(commonArg.scale, commonArg.scale);
+
+            alphaBuf = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
+            AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
+            alphaBuf = scaleOp.filter(inputBuf, alphaBuf);
+
+            alphaArgb = alphaBuf.getRGB(0, 0, w, h, null, 0, w);
+        }
+
+        // rgb 区域
+        fillColor(outputArgb, outW, commonArg.rgbPoint, false, inputArgb, w);
+
+        // alpha 区域
+        fillColor(outputArgb, outW, commonArg.alphaPoint, true, alphaArgb, w);
+
+        return new AlphaFrameOut(outputArgb);
+
+    }
+
+
+    private void fillColor(int[] outputArgb, int outputW, PointRect point, boolean isAlpha, int[] inputArgb, int inputW) {
+        int outX = 0;
+        int outY = 0;
+        for (int y = 0; y < point.h ; y++) {
+            outY = point.y + y;
+            for (int x = 0; x < point.w ; x++) {
+                outX = point.x + x;
+                int color = inputArgb[x + y * inputW];
+                outputArgb[outX + outY * outputW] = isAlpha ? getAlpha(color) : getColor(color);
             }
         }
+    }
 
-        return new AlphaFrameOut(orin, outputArgb, w, h, outW, outH, gap);
+    private int getColor(int color) {
+        return blendBg(color, 0xff000000);
+    }
 
+    private int getAlpha(int color) {
+        int alpha = color >>> 24;
+        // r = g = b
+        return 0xff000000 + (alpha << 16) + (alpha << 8) + alpha;
     }
 
     private int blendBg(int color, int colorBg) {

+ 82 - 8
Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/Main.java

@@ -17,6 +17,7 @@ package com.tencent.qgame.playerproj.animtool;
 
 
 import com.tencent.qgame.playerproj.animtool.ui.ToolUI;
+import com.tencent.qgame.playerproj.animtool.vapx.SrcSet;
 
 public class Main {
 
@@ -24,8 +25,12 @@ public class Main {
     public static void main(String[] args) throws Exception {
         // 启动UI界面
         new ToolUI().run();
-        // java工具
+
+        // java工具普通动画
         // animTool();
+
+        // java工具融合动画
+        // animVapxTool();
     }
 
 
@@ -36,13 +41,12 @@ public class Main {
      * 生成图片的工具
      * step 1 填写如下参数,运行后生成中间图片
      * step 2 进入outputPath目录,运行如下ffmpeg命令(需要预先安装ffmpeng)
-     * ffmpeg -r 24 -i "%03d.png" -pix_fmt yuv420p -vcodec libx264 -b:v 3000k -profile:v baseline -level 3.0 -bf 0 -y demo.mp4
      *
+     * h264
+     * ffmpeg -r 24 -i "%03d.png" -pix_fmt yuv420p -vcodec libx264 -b:v 3000k -profile:v main -level 4.0 -bf 0 -bufsize 3000k -y demo.mp4
      *
-     * -vcodec libx264 h264编码
-     * -b:v 3000K 表示码率为3000K,可以改变码率调节文件大小和视频清晰度
-     * -bf 0 没有B帧
-     * -profile:v baseline baseline模式
+     * h265
+     * ffmpeg -r 24 -i "%03d.png" -pix_fmt yuv420p -vcodec libx265 -b:v 2000k -profile:v main -level 4.0 -bf 0 -bufsize 2000k -tag:v hvc1 -y demo.mp4
      *
      * 使用固定码率能使文件更小,但会损失清晰度
      * 使用-crf 参数可以提高清晰度但文件大小不可控(会变大),推荐值 29(0 最好 51 最差)
@@ -58,21 +62,91 @@ public class Main {
         commonArg.mp4editCmd = "mp4edit";
 
         /*
-         * 是否开启h265(默认关闭)
+         * 是否开启h265
          * 优点:压缩率更高,视频更清晰
          * 缺点:Android 4.x系统 & 极少部分低端机 无法播放265视频
          */
-        commonArg.enableH265 = true;
+        commonArg.enableH265 = false;
         // fps
         commonArg.fps = 24;
         // 素材文件路径
         commonArg.inputPath = "/path/to/your/demo";
+        // alpha 区域缩放大小  (0.5 - 1)
+        commonArg.scale = 0.5f;
+
+        // 开始运行
+        AnimTool animTool = new AnimTool();
+        // needVideo true 直接生成video false 生成帧图片,由用户手动生成最终视频文件
+        animTool.create(commonArg, true);
+    }
+
+
+    /**
+     * 融合动画 demo
+     */
+    public static void animVapxTool() throws Exception {
+        final CommonArg commonArg = new CommonArg();
+        // ffmpeg 命令路径
+        commonArg.ffmpegCmd = "ffmpeg";
+        // bento4 mp4edit 命令路径
+        commonArg.mp4editCmd = "mp4edit";
+
+        String path = "/path/to/your/demo";
+
+        commonArg.enableH265 = false;
+        // fps
+        commonArg.fps = 24;
+        // 素材文件路径
+        commonArg.inputPath = path + "video";
+        // 启动融合动画
+        commonArg.isVapx = true;
+        if (commonArg.isVapx) {
+            // 融合动画默认需要缩放0.5f 空出区域
+            commonArg.scale = 0.5f;
+        }
+        // src 设置
+        commonArg.srcSet = getSrcSet(path);
+
 
         // 开始运行
         AnimTool animTool = new AnimTool();
         // needVideo true 直接生成video false 生成帧图片,由用户手动生成最终视频文件
         animTool.create(commonArg, true);
     }
+
+
+    private static SrcSet getSrcSet(String path) {
+        SrcSet srcSet = new SrcSet();
+
+        {
+            SrcSet.Src src = new SrcSet.Src();
+            src.srcPath = path + "mask1";
+            src.srcId = "1";
+            src.srcType = SrcSet.Src.SRC_TYPE_IMG;
+            src.srcTag = "head1";
+            src.fitType = SrcSet.Src.FIT_TYPE_CF;
+            srcSet.srcs.add(src);
+        }
+
+
+        {
+            SrcSet.Src src = new SrcSet.Src();
+            src.srcPath = path + "mask2";
+            src.srcId = "2";
+            src.srcType = SrcSet.Src.SRC_TYPE_TXT;
+            src.srcTag = "text1";
+            src.fitType = SrcSet.Src.FIT_TYPE_FITXY;
+            src.color = "#0000ff";
+            src.style = SrcSet.Src.TEXT_STYLE_BOLD;
+            srcSet.srcs.add(src);
+        }
+
+
+
+
+        return srcSet;
+    }
+
     /**
      * 生成对应的box bin
      * 执行 mp4edit --insert :vapc.bin:1 demo_origin.mp4 demo_output.mp4 插入对应box

+ 19 - 0
Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/TLog.java

@@ -27,7 +27,26 @@ public class TLog {
         }
     }
 
+    public static void e(String tag, String msg) {
+        if (logger != null) {
+            logger.e(tag, msg);
+        } else {
+            System.out.println(tag + "\tError:" + msg);
+        }
+    }
+
+    public static void w(String tag, String msg) {
+        if (logger != null) {
+            logger.w(tag, msg);
+        } else {
+            System.out.println(tag + "\tWarning:" + msg);
+        }
+    }
+
+
     public interface ITLog {
         void i(String tag, String  msg);
+        void e(String tag, String  msg);
+        void w(String tag, String  msg);
     }
 }

+ 24 - 0
Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/data/PointRect.java

@@ -0,0 +1,24 @@
+package com.tencent.qgame.playerproj.animtool.data;
+
+public class PointRect {
+
+    public int x = 0;
+    public int y = 0;
+    public int w = 0;
+    public int h = 0;
+
+    public PointRect() {
+    }
+
+    public PointRect(int x, int y, int w, int h) {
+        this.x = x;
+        this.y = y;
+        this.w = w;
+        this.h = h;
+    }
+
+    @Override
+    public String toString() {
+        return "["+ x +","+ y +","+ w +","+ h +"]";
+    }
+}

+ 154 - 24
Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/ui/ToolUI.java

@@ -22,9 +22,11 @@ import java.util.Properties;
 import javax.swing.BoxLayout;
 import javax.swing.ButtonGroup;
 import javax.swing.JButton;
+import javax.swing.JComboBox;
 import javax.swing.JFileChooser;
 import javax.swing.JFrame;
 import javax.swing.JLabel;
+import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.JRadioButton;
 import javax.swing.JScrollPane;
@@ -38,30 +40,55 @@ public class ToolUI {
 
     private static final String TAG = "ToolUI";
     private static final String PROPERTIES_FILE = "setting.properties";
-    private final int WIDTH = 550;
-    private final int HEIGHT = 350;
+    public static final int WIDTH = 900;
+    public static final int HEIGHT = 700;
 
+    private final JFrame frame = new JFrame("VAP tool");
     private final ButtonGroup group = new ButtonGroup();
-    private final JRadioButton btnH265 = new JRadioButton("h265");
     private final JRadioButton btnH264 = new JRadioButton("h264");
+    private final JRadioButton btnH265 = new JRadioButton("h265");
     private final SpinnerModel modelFps = new SpinnerNumberModel(24, 1, 60, 1);
+    private final Float[] scaleArray = new Float[]{0.5f, 1f};
+    private final JComboBox<Float> boxScale = new JComboBox<>(scaleArray);
     private final JTextField textInputPath = new JTextField();
     private final JButton btnCreate = new JButton("create VAP");
     private final JTextArea txtAreaLog = new JTextArea();
+    private final JTextField textAudioPath = new JTextField();
+    private final JPanel panelAudioPath = new JPanel();
+
     private final JLabel labelOutInfo = new JLabel();
     private final Dimension labelSize = new Dimension(100, 20);
     private final Properties props = new Properties();
+    private final VapxUI vapxUI = new VapxUI();
 
+    private boolean needAudio = false;
 
-
-    public void run() {
+    public ToolUI() {
         TLog.logger = new TLog.ITLog() {
             @Override
             public void i(String tag, String msg) {
                 log(tag, msg);
             }
+
+            @Override
+            public void e(String tag, String msg) {
+                log(tag, "Error:" + msg);
+            }
+
+            @Override
+            public void w(String tag, String msg) {
+                log(tag, "Warning:" + msg);
+            }
         };
+    }
+
+
+    public void run() {
         createUI();
+        loadProperties();
+    }
+
+    private void loadProperties() {
         try {
             File file = new File(PROPERTIES_FILE);
             if (!file.exists()) {
@@ -72,12 +99,19 @@ public class ToolUI {
             group.setSelected(commonArg.enableH265 ? btnH265.getModel() : btnH264.getModel(), true);
             modelFps.setValue(commonArg.fps);
             textInputPath.setText(commonArg.inputPath);
+            textAudioPath.setText(commonArg.audioPath);
+            float scale = commonArg.scale;
+            for (int i=0; i<scaleArray.length ; i++) {
+                if (scaleArray[i] == scale) {
+                    boxScale.setSelectedIndex(i);
+                    break;
+                }
+            }
         } catch (Exception e) {
-            TLog.i(TAG, "ERROR -> " + e.getMessage());
+            TLog.e(TAG, e.getMessage());
         }
     }
 
-
     private void runTool() {
         txtAreaLog.setText("");
         new Thread(new Runnable() {
@@ -86,7 +120,7 @@ public class ToolUI {
                 try {
                     runAnimTool();
                 } catch (Exception e) {
-                    TLog.i(TAG, "ERROR -> " + e.getMessage());
+                    TLog.e(TAG, e.getMessage());
                     btnCreate.setEnabled(true);
                 }
             }
@@ -112,6 +146,20 @@ public class ToolUI {
         commonArg.enableH265 = group.isSelected(btnH265.getModel());
         commonArg.fps = (Integer)modelFps.getValue();
         commonArg.inputPath = textInputPath.getText();
+        commonArg.scale = scaleArray[boxScale.getSelectedIndex()];
+        if (needAudio) {
+            commonArg.needAudio = true;
+            commonArg.audioPath = textAudioPath.getText();
+        }
+
+        if (vapxUI.isVapxEnable()) {
+            commonArg.isVapx = true;
+            commonArg.srcSet = vapxUI.getSrcSet();
+            if (commonArg.srcSet == null) {
+                return;
+            }
+        }
+
         TLog.i(TAG, commonArg.toString());
 
         AnimTool animTool = new AnimTool();
@@ -122,6 +170,11 @@ public class ToolUI {
                 labelOutInfo.setText((Math.min(p, 99)) + "%");
             }
 
+            @Override
+            public void onWarning(String msg) {
+                JOptionPane.showMessageDialog(frame, msg, "Warning", JOptionPane.WARNING_MESSAGE);
+            }
+
             @Override
             public void onError() {
                 btnCreate.setEnabled(true);
@@ -135,7 +188,7 @@ public class ToolUI {
                     setProperties(commonArg);
                     Desktop.getDesktop().open(new File(commonArg.outputPath));
                 } catch (IOException e) {
-                    TLog.i(TAG, "ERROR -> " + e.getMessage());
+                    TLog.e(TAG, e.getMessage());
                 }
             }
         });
@@ -145,7 +198,6 @@ public class ToolUI {
     }
 
     private void createUI() {
-        JFrame frame = new JFrame("VAP tool");
         frame.setSize(WIDTH, HEIGHT);
         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
@@ -166,8 +218,14 @@ public class ToolUI {
         panel.add(getCodecLayout());
         // fps
         panel.add(getFpsLayout());
+        // scale
+        panel.add(getScaleLayout());
         // path
         panel.add(getPathLayout());
+        // audio path
+        panel.add(getAudioPathLayout());
+        // vapx
+        panel.add(vapxUI.createUI());
         // create
         panel.add(getCreateLayout());
         // log
@@ -187,11 +245,11 @@ public class ToolUI {
 
         JPanel panelRadio = new JPanel();
         panelRadio.setLayout(new GridLayout(1, 2));
-        panelRadio.add(btnH265);
         panelRadio.add(btnH264);
-        group.add(btnH265);
+        panelRadio.add(btnH265);
         group.add(btnH264);
-        group.setSelected(btnH265.getModel(), true);
+        group.add(btnH265);
+        group.setSelected(btnH264.getModel(), true);
         panel.add(panelRadio);
 
         return panel;
@@ -209,11 +267,22 @@ public class ToolUI {
         return panel;
     }
 
+    private JPanel getScaleLayout() {
+        JPanel panel = new JPanel();
+        panel.setLayout(new FlowLayout(FlowLayout.LEFT));
+        JLabel label = new JLabel("alpha scale");
+        label.setPreferredSize(labelSize);
+        panel.add(label);
+
+        panel.add(boxScale);
+        return panel;
+    }
+
     private JPanel getPathLayout() {
         JPanel panel = new JPanel();
 
         panel.setLayout(new FlowLayout(FlowLayout.LEFT));
-        JLabel label = new JLabel("input path");
+        JLabel label = new JLabel("frames path");
         label.setPreferredSize(labelSize);
         panel.add(label);
         JPanel gPanel = new JPanel();
@@ -222,7 +291,7 @@ public class ToolUI {
         BoxLayout layout = new BoxLayout(gPanel, BoxLayout.LINE_AXIS);
         gPanel.setLayout(layout);
 
-        textInputPath.setPreferredSize(new Dimension(300,20));
+        textInputPath.setPreferredSize(new Dimension(400,20));
         gPanel.add(textInputPath);
 
         JButton btnInputPath = new JButton("choose");
@@ -244,6 +313,55 @@ public class ToolUI {
         return panel;
     }
 
+
+    private JPanel getAudioPathLayout() {
+        JPanel panel = new JPanel();
+
+        panel.setLayout(new FlowLayout(FlowLayout.LEFT));
+        JLabel label = new JLabel("audio(mp3)");
+        label.setPreferredSize(labelSize);
+        panel.add(label);
+        panel.add(panelAudioPath);
+        final JLabel labelAudioAction = new JLabel("+");
+        panel.add(labelAudioAction);
+        labelAudioAction.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseClicked(MouseEvent mouseEvent) {
+                needAudio = !needAudio;
+                panelAudioPath.setVisible(needAudio);
+                labelAudioAction.setText(needAudio ? "x" : "+");
+            }
+        });
+
+        BoxLayout layout = new BoxLayout(panelAudioPath, BoxLayout.LINE_AXIS);
+        panelAudioPath.setLayout(layout);
+
+        textAudioPath.setPreferredSize(new Dimension(400,20));
+        panelAudioPath.add(textAudioPath);
+
+        JButton btnInputPath = new JButton("choose");
+        panelAudioPath.add(btnInputPath);
+        btnInputPath.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent actionEvent) {
+                JFileChooser fileChooser = new JFileChooser();
+                fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
+                int returnVal = fileChooser.showOpenDialog(fileChooser);
+                if(returnVal == JFileChooser.APPROVE_OPTION) {
+                    // 文件夹路径
+                    String filePath = fileChooser.getSelectedFile().getAbsolutePath();
+                    textAudioPath.setText(filePath);
+                }
+            }
+        });
+
+        if (!needAudio) {
+            panelAudioPath.setVisible(false);
+        }
+
+        return panel;
+    }
+
     private void setOutput(final String path) {
         labelOutInfo.setText("<html><font color='blue'>open output</font></html>");
         labelOutInfo.addMouseListener(new MouseAdapter() {
@@ -252,7 +370,7 @@ public class ToolUI {
                 try {
                     Desktop.getDesktop().open(new File(path));
                 } catch (IOException e) {
-                    TLog.i(TAG, "ERROR -> " + e.getMessage());
+                    TLog.e(TAG, e.getMessage());
                 }
             }
         });
@@ -286,7 +404,8 @@ public class ToolUI {
         JScrollPane areaScrollPane = new JScrollPane(txtAreaLog);
         areaScrollPane.setVerticalScrollBarPolicy(
                 JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
-        areaScrollPane.setPreferredSize(new Dimension(WIDTH, 200));
+        areaScrollPane.setPreferredSize(new Dimension(WIDTH, 100));
+        areaScrollPane.setMinimumSize(new Dimension(WIDTH, 100));
 
         panel.add(areaScrollPane);
         panel.setPreferredSize(new Dimension(WIDTH, HEIGHT));
@@ -317,25 +436,36 @@ public class ToolUI {
 
     private CommonArg getProperties() {
         CommonArg commonArg = new CommonArg();
-        String enableH265 = props.getProperty("enableH265", Boolean.TRUE.toString());
-        String fps = props.getProperty("fps", "24");
-        String inputPath = props.getProperty("inputPath", "");
-
-        commonArg.enableH265 = Boolean.TRUE.toString().equals(enableH265);
         try {
+            String version = props.getProperty("version", "0");
+            String enableH265 = props.getProperty("enableH265", Boolean.toString(commonArg.enableH265));
+            String fps = props.getProperty("fps", String.valueOf(commonArg.fps));
+            String inputPath = props.getProperty("inputPath", "");
+            String scale = props.getProperty("scale", String.valueOf(scaleArray[0]));
+            String audioPath = props.getProperty("audioPath", "");
+
+            int v = Integer.parseInt(version);
+            // 版本不符直接返回默认值
+            if (v != commonArg.version) return commonArg;
             commonArg.fps = Integer.parseInt(fps);
+            commonArg.scale = Float.parseFloat(scale);
+            commonArg.enableH265 = Boolean.TRUE.toString().equals(enableH265);
+            commonArg.inputPath = inputPath;
+            commonArg.audioPath = audioPath;
         } catch (Exception e) {
-            commonArg.fps = 24;
+            TLog.e(TAG, "getProperties error:" + e.getMessage());
         }
-        commonArg.inputPath = inputPath;
         return commonArg;
     }
 
 
     private void setProperties(CommonArg commonArg) throws IOException {
+        props.setProperty("version", commonArg.version + "");
         props.setProperty("enableH265", commonArg.enableH265? Boolean.TRUE.toString() : Boolean.FALSE.toString());
         props.setProperty("fps", commonArg.fps + "");
         props.setProperty("inputPath", commonArg.inputPath == null ? "" : commonArg.inputPath);
+        props.setProperty("audioPath", commonArg.audioPath == null ? "" : commonArg.audioPath);
+        props.setProperty("scale", commonArg.scale + "");
         props.store(new FileOutputStream(PROPERTIES_FILE), "");
     }
 

+ 288 - 0
Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/ui/VapxUI.java

@@ -0,0 +1,288 @@
+package com.tencent.qgame.playerproj.animtool.ui;
+
+import com.tencent.qgame.playerproj.animtool.TLog;
+import com.tencent.qgame.playerproj.animtool.vapx.SrcSet;
+
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSeparator;
+import javax.swing.JTextField;
+
+public class VapxUI {
+
+    private static final String TAG = "VapxUI";
+
+    private final Dimension labelSize = new Dimension(100, 20);
+    private final JPanel controlPanel = new JPanel();
+    private final List<MaskUI> maskUiList = new ArrayList<>();
+    private int index = 0;
+    private final IMaskUIListener listener = new IMaskUIListener() {
+        @Override
+        public void onDelete(MaskUI maskUI) {
+            controlPanel.remove(maskUI.getPanel());
+            maskUiList.remove(maskUI);
+            controlPanel.revalidate();
+        }
+    };
+
+    public JPanel createUI() {
+        JPanel panel = new JPanel();
+        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
+        panel.setPreferredSize(new Dimension(ToolUI.WIDTH, 300));
+        panel.setMinimumSize(new Dimension(ToolUI.WIDTH, 300));
+        controlPanel.setLayout(new BoxLayout(controlPanel, BoxLayout.PAGE_AXIS));
+        controlPanel.add(getAddLayout());
+        JScrollPane areaScrollPane = new JScrollPane(controlPanel);
+        panel.add(areaScrollPane);
+        return panel;
+    }
+
+    public boolean isVapxEnable() {
+        return !maskUiList.isEmpty();
+    }
+
+    public SrcSet getSrcSet() {
+        if (maskUiList.isEmpty()) return null;
+        SrcSet srcSet = new SrcSet();
+
+        SrcSet.Src src;
+        for (MaskUI maskUI : maskUiList) {
+            src = maskUI.getSrc();
+            if (src == null) return null;
+            srcSet.srcs.add(src);
+        }
+
+        return srcSet;
+    }
+
+
+    private JPanel getAddLayout() {
+        JPanel panel = new JPanel();
+        panel.setLayout(new FlowLayout(FlowLayout.LEFT));
+        JLabel label = new JLabel("add source");
+        label.setPreferredSize(labelSize);
+
+        JButton btnAdd = new JButton("add");
+        btnAdd.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent actionEvent) {
+                createMaskUI();
+            }
+        });
+
+        panel.add(label);
+        panel.add(btnAdd);
+        panel.add(new JLabel("(simple video don't need add source)"));
+        return panel;
+    }
+
+    private void createMaskUI() {
+        MaskUI maskUI = new MaskUI(++index, listener);
+        controlPanel.add(maskUI.getPanel());
+        maskUiList.add(maskUI);
+        controlPanel.revalidate();
+    }
+
+    private static class MaskUI {
+        public IMaskUIListener listener;
+        public int index;
+        public String maskPath;
+        public JPanel panel = new JPanel();
+
+        private final JLabel labelIndex = new JLabel();
+        private final JTextField textSrcTag = new JTextField();
+        // image -> SrcSet.Src.SRC_TYPE_IMG text -> SrcSet.Src.SRC_TYPE_TXT
+        private final String[] srcTypeArray = new String[]{"image", "text"};
+        private final JComboBox<String> boxSrcType = new JComboBox<>(srcTypeArray);
+
+        // centerCrop -> SrcSet.Src.FIT_TYPE_CF
+        private final String[] fitTypeArray = new String[]{"fitXY", "centerCrop"};
+        private final JComboBox<String> boxFitType = new JComboBox<>(fitTypeArray);
+
+        private final JPanel txtPanel = new JPanel();
+        private final JTextField textTxtColor = new JTextField();
+        private final JCheckBox checkTxtBold = new JCheckBox("text Bold");
+
+        final JLabel labelMaskPathState = new JLabel();
+
+        public MaskUI(int index, IMaskUIListener listener) {
+            this.index = index;
+            this.listener = listener;
+            createUI();
+        }
+
+        public JPanel getPanel() {
+            return panel;
+        }
+
+        public SrcSet.Src getSrc() {
+            SrcSet.Src src = new SrcSet.Src();
+            src.srcId = String.valueOf(index);
+            src.srcTag = textSrcTag.getText().trim();
+
+            src.srcType = SrcSet.Src.SRC_TYPE_IMG;
+            if (boxSrcType.getSelectedIndex() == 1) {
+                src.srcType = SrcSet.Src.SRC_TYPE_TXT;
+            }
+
+            src.fitType = SrcSet.Src.FIT_TYPE_FITXY;
+            if (boxFitType.getSelectedIndex() == 1) {
+                src.fitType = SrcSet.Src.FIT_TYPE_CF;
+            }
+
+            src.srcPath = maskPath;
+
+            if (SrcSet.Src.SRC_TYPE_TXT.equals(src.srcType)) {
+                src.color = textTxtColor.getText().trim();
+                if (checkTxtBold.isSelected()) {
+                    src.style = SrcSet.Src.TEXT_STYLE_BOLD;
+                }
+            }
+
+            if (src.srcTag == null || "".equals(src.srcTag)) {
+                String msg = "id:" + index + " source tag is empty";
+                TLog.e(TAG, msg);
+                JOptionPane.showMessageDialog(panel, msg, "Error", JOptionPane.ERROR_MESSAGE);
+                return null;
+            }
+
+            if (src.srcPath == null || "".equals(src.srcPath)) {
+                String msg = "id:" + index + " mask path is empty";
+                TLog.e(TAG, msg);
+                JOptionPane.showMessageDialog(panel, msg, "Error", JOptionPane.ERROR_MESSAGE);
+                return null;
+            }
+            return src;
+        }
+
+        private void createUI() {
+            panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
+            setMaskPath();
+            panel.add(new JSeparator());
+            panel.add(part1Layout());
+            panel.add(part2Layout());
+            panel.add(part3Layout());
+
+        }
+
+        private void setMaskPath() {
+            String text = maskPath == null? "<html><font color='red'>empty</font></html>" : maskPath;
+            labelMaskPathState.setText(text);
+        }
+
+        public JPanel part1Layout() {
+            JPanel panel = new JPanel();
+            panel.setLayout(new FlowLayout(FlowLayout.LEFT));
+
+            // index
+            labelIndex.setText("id:" + index);
+            panel.add(labelIndex);
+
+            // srcTag
+            panel.add(new JLabel(" source tag:"));
+            textSrcTag.setPreferredSize(new Dimension(50, 20));
+            textSrcTag.setText("tag" + index);
+            panel.add(textSrcTag);
+
+            // srcType
+            panel.add(new JLabel(" source type:"));
+            boxSrcType.setSelectedIndex(0);
+            panel.add(boxSrcType);
+            boxSrcType.addItemListener(new ItemListener() {
+                @Override
+                public void itemStateChanged(ItemEvent itemEvent) {
+                    txtPanel.setVisible(srcTypeArray[1].equals(itemEvent.getItem()));
+                }
+            });
+            // fitType
+            panel.add(new JLabel(" fit type:"));
+            boxFitType.setSelectedIndex(0);
+            panel.add(boxFitType);
+
+
+
+
+            // delete
+            JLabel labelDelete = new JLabel("<html><font color='red'>delete</font></html>");
+            panel.add(labelDelete);
+            labelDelete.addMouseListener(new MouseAdapter() {
+                @Override
+                public void mouseClicked(MouseEvent mouseEvent) {
+                    if (listener != null) {
+                        listener.onDelete(MaskUI.this);
+                    }
+                }
+            });
+
+            return panel;
+        }
+
+
+        private JPanel part2Layout() {
+            JPanel panel = txtPanel;
+            panel.setLayout(new FlowLayout(FlowLayout.LEFT));
+
+            panel.add(new JLabel(" text color:"));
+            textTxtColor.setPreferredSize(new Dimension(100, 20));
+            textTxtColor.setText("#000000");
+            panel.add(textTxtColor);
+
+            panel.add(checkTxtBold);
+            panel.setVisible(false);
+            return panel;
+        }
+
+
+        private JPanel part3Layout() {
+            JPanel panel = new JPanel();
+            panel.setLayout(new FlowLayout(FlowLayout.LEFT));
+
+
+            // mask path
+            panel.add(new JLabel(" mask path:"));
+            JButton btnMaskPath = new JButton("choose");
+            panel.add(btnMaskPath);
+            btnMaskPath.addActionListener(new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent actionEvent) {
+                    JFileChooser fileChooser = new JFileChooser();
+                    fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+                    int returnVal = fileChooser.showOpenDialog(fileChooser);
+                    if(returnVal == JFileChooser.APPROVE_OPTION) {
+                        // 文件夹路径
+                        maskPath = fileChooser.getSelectedFile().getAbsolutePath();
+                        setMaskPath();
+                    }
+                }
+            });
+
+
+            panel.add(labelMaskPathState);
+
+            return panel;
+        }
+    }
+
+    private interface IMaskUIListener {
+        void onDelete(MaskUI maskUI);
+    }
+
+}

+ 79 - 0
Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/vapx/FrameSet.java

@@ -0,0 +1,79 @@
+package com.tencent.qgame.playerproj.animtool.vapx;
+
+import com.tencent.qgame.playerproj.animtool.data.PointRect;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Vector;
+
+public class FrameSet {
+
+    // for sync
+    public Vector<FrameObj> frameObjs = new Vector<>();
+
+    @Override
+    public String toString() {
+        StringBuilder json = new StringBuilder();
+
+        json.append("\"frame\":[");
+        FrameSet.FrameObj frameObj;
+        for (int i=0; i<frameObjs.size(); i++) {
+            frameObj = frameObjs.get(i);
+            json.append(frameObj.toString());
+            if (i != frameObjs.size() - 1) {
+                json.append(",");
+            }
+        }
+
+        json.append("]");
+
+        return json.toString();
+    }
+
+    public static class FrameObj {
+        public List<Frame> frames = new ArrayList<>();
+        public int frameIndex = 0;
+
+        @Override
+        public String toString() {
+            StringBuilder json = new StringBuilder();
+
+            json.append("{");
+            json.append("\"i\":").append(frameIndex).append(",");
+            json.append("\"obj\":[");
+            FrameSet.Frame frame;
+            for (int i=0; i<frames.size(); i++) {
+                frame = frames.get(i);
+                json.append(frame.toString());
+                if (i != frames.size() - 1) {
+                    json.append(",");
+                }
+            }
+            json.append("]");
+            json.append("}");
+
+            return json.toString();
+        }
+    }
+
+    public static class Frame {
+        public String srcId = "";
+        public int z = 0;
+        public int mt = 0; // 旋转角度 目前只支持0
+        public PointRect frame = new PointRect(); // src位置
+        public PointRect mFrame = new PointRect(); // 遮罩区域
+
+
+        @Override
+        public String toString() {
+
+            return "{" +
+                    "\"srcId\":" + "\"" + srcId + "\"," +
+                    "\"z\":" + z + "," +
+                    "\"frame\":" + frame.toString() + "," +
+                    "\"mFrame\":" + mFrame.toString() + "," +
+                    "\"mt\":" + mt +
+                    "}";
+        }
+    }
+}

+ 207 - 0
Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/vapx/GetMaskFrame.java

@@ -0,0 +1,207 @@
+package com.tencent.qgame.playerproj.animtool.vapx;
+
+import com.tencent.qgame.playerproj.animtool.CommonArg;
+import com.tencent.qgame.playerproj.animtool.TLog;
+import com.tencent.qgame.playerproj.animtool.data.PointRect;
+
+import java.awt.geom.AffineTransform;
+import java.awt.image.AffineTransformOp;
+import java.awt.image.BufferedImage;
+import java.io.File;
+
+import javax.imageio.ImageIO;
+
+/**
+ * 获取融合动画遮罩
+ */
+public class GetMaskFrame {
+
+    private static final String TAG = "GetMaskFrame";
+
+    public FrameSet.FrameObj getFrameObj(int frameIndex, CommonArg commonArg, int[] outputArgb) throws Exception {
+
+        FrameSet.FrameObj frameObj = new FrameSet.FrameObj();
+        frameObj.frameIndex = frameIndex;
+
+        FrameSet.Frame frame;
+        // 需要放置的位置
+        int x;
+        int y;
+        int gap = commonArg.gap;
+        if (commonArg.isVLayout) {
+            x = commonArg.alphaPoint.w + gap;
+            y = commonArg.alphaPoint.y;
+        } else {
+            x = commonArg.alphaPoint.x;
+            y = commonArg.alphaPoint.h + gap;
+        }
+        int startX = x;
+        int lastMaxY = y;
+        for (int i=0; i<commonArg.srcSet.srcs.size(); i++) {
+            frame = getFrame(frameIndex, commonArg.srcSet.srcs.get(i), outputArgb, commonArg.outputW, commonArg.outputH, x, y, startX, lastMaxY);
+            if (frame == null) continue;
+            // 计算下一个遮罩起点
+            x = frame.mFrame.x + frame.mFrame.w + gap;
+            y = frame.mFrame.y;
+            int newY = frame.mFrame.y + frame.mFrame.h + gap;
+            if (newY > lastMaxY) {
+                lastMaxY = newY;
+            }
+
+            frameObj.frames.add(frame);
+        }
+
+        if (frameObj.frames.isEmpty()) {
+            return null;
+        }
+        return frameObj;
+    }
+
+
+    private FrameSet.Frame getFrame(int frameIndex, SrcSet.Src src, int[] outputArgb, int outW, int outH, int x, int y, int startX, int lastMaxY) throws Exception {
+        File inputFile = new File(src.srcPath  + String.format("%03d", frameIndex)+".png");
+        if (!inputFile.exists()) {
+            return null;
+        }
+
+        BufferedImage inputBuf = ImageIO.read(inputFile);
+        int maskW = inputBuf.getWidth();
+        int maskH = inputBuf.getHeight();
+        int[] maskArgb = inputBuf.getRGB(0, 0, maskW, maskH, null, 0, maskW);
+
+        FrameSet.Frame frame = new FrameSet.Frame();
+        frame.srcId = src.srcId;
+        frame.z = src.z;
+
+        frame.frame = getSrcFramePoint(maskArgb, maskW, maskH);
+        if (frame.frame == null) {
+            // 有文件,但内容是空
+            return null;
+        }
+
+        PointRect maskPoint = new PointRect(
+            frame.frame.x,
+            frame.frame.y,
+            frame.frame.w,
+            frame.frame.h
+        );
+
+        PointRect mFrame = new PointRect(x, y, frame.frame.w, frame.frame.h);
+        // 计算是否能放下遮罩
+        if (mFrame.x + mFrame.w > outW) { // 超宽换行
+            mFrame.x = startX;
+            mFrame.y = lastMaxY;
+            if (mFrame.x + mFrame.w > outW) {
+
+                // 超长后缩放mask
+                float scale = (outW - mFrame.x) * 1f / mFrame.w;
+
+                mFrame.w = outW - mFrame.x;
+                mFrame.h = (int) (mFrame.h * scale);
+
+                maskPoint.x = (int) (maskPoint.x * scale);
+                maskPoint.y = (int) (maskPoint.y * scale);
+
+                maskArgb = scaleMask(scale, inputBuf);
+
+                TLog.w(TAG, "frameIndex=" + frameIndex + ",src=" + src.srcId + ", no more space for(w)" + mFrame + ",scale=" + scale);
+            }
+        }
+        if (mFrame.y + mFrame.h > outH) { // 高度不够直接错误
+            TLog.e(TAG, "frameIndex=" + frameIndex + ",src=" + src.srcId + ", no more space(h)" + mFrame);
+            return null;
+        }
+        frame.mFrame = mFrame;
+
+        fillMaskToOutput(outputArgb, outW, maskArgb, maskW, maskPoint, frame.mFrame, SrcSet.Src.SRC_TYPE_TXT.equals(src.srcType));
+
+        // 设置src的w,h 取所有遮罩里最大值
+        synchronized (GetMaskFrame.class) {
+            // 只按宽度进行判断防止横跳
+            if (frame.frame.w > src.w) {
+                src.w = frame.frame.w;
+                src.h = frame.mFrame.h;
+            }
+        }
+        return frame;
+    }
+
+    /**
+     * 缩放遮罩
+     */
+    private int[] scaleMask(float scale, BufferedImage inputBuf) {
+        AffineTransform at = new AffineTransform();
+        at.scale(scale, scale);
+
+        int w = inputBuf.getWidth();
+        int h = inputBuf.getHeight();
+        BufferedImage alphaBuf = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
+        AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
+        alphaBuf = scaleOp.filter(inputBuf, alphaBuf);
+
+        return alphaBuf.getRGB(0, 0, w, h, null, 0, w);
+    }
+
+    /**
+     * 获取遮罩位置信息 并转换为黑白
+     */
+    private PointRect getSrcFramePoint(int[] maskArgb, int w, int h) {
+
+        PointRect point = new PointRect();
+
+        int minX = Integer.MAX_VALUE;
+        int minY = Integer.MAX_VALUE;
+        int maxX = 0;
+        int maxY = 0;
+        for (int y=0; y<h; y++) {
+            for (int x = 0; x < w; x++) {
+                int alpha = maskArgb[x + y*w] >>> 24;
+                if (alpha > 0) {
+                    if (x < minX) minX = x;
+                    if (y < minY) minY = y;
+                    if (x > maxX) maxX = x;
+                    if (y > maxY) maxY = y;
+                }
+            }
+        }
+
+        point.x = minX;
+        point.y = minY;
+        point.w = maxX - minX;
+        point.h = maxY - minY;
+        if (point.w <=0 || point.h <= 0) return null;
+
+        return point;
+
+    }
+
+
+    private void fillMaskToOutput(int[] outputArgb, int outW,
+                                  int[] maskArgb, int maskW,
+                                  PointRect frame,
+                                  PointRect mFrame,
+                                  boolean isTxtMask) {
+        for (int y=0; y < frame.h; y++) {
+            for (int x=0; x < frame.w; x++) {
+                int maskXOffset = frame.x;
+                int maskYOffset = frame.y;
+                // 先从遮罩 maskArgb 取色
+                int maskColor = maskArgb[x + maskXOffset + (y + maskYOffset) * maskW];
+                // 黑色部分不遮挡,红色部分被遮挡
+                int alpha = maskColor >>> 24;
+                int maskRed = (maskColor & 0x00ff0000) >>> 16;
+                int redAlpha = 255 - maskRed; // 红色部分算遮挡
+                alpha = (int) ((redAlpha / 255f) * (alpha / 255f) * 255f);
+                // 最终color
+                int color = 0xff000000 + (alpha << 16) + (alpha << 8) + alpha;
+
+                // 将遮罩颜色放置到视频中对应区域
+                int outputXOffset = mFrame.x;
+                int outputYOffset = mFrame.y;
+                outputArgb[x + outputXOffset + (y + outputYOffset) * outW] = color;
+
+            }
+        }
+    }
+
+}

+ 90 - 0
Android/PlayerProj/animtool/src/main/java/com/tencent/qgame/playerproj/animtool/vapx/SrcSet.java

@@ -0,0 +1,90 @@
+package com.tencent.qgame.playerproj.animtool.vapx;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SrcSet {
+
+
+    public List<Src> srcs = new ArrayList<>();
+
+
+    public static class Src {
+
+        public static final String SRC_TYPE_IMG = "img";
+        public static final String SRC_TYPE_TXT = "txt";
+
+        public static final String LOAD_TYPE_NET = "net";
+        public static final String LOAD_TYPE_LOC = "local";
+
+        public static final String TEXT_STYLE_DEFAULT = "";
+        public static final String TEXT_STYLE_BOLD = "b";
+
+        public static final String FIT_TYPE_FITXY = "fitXY";
+        public static final String FIT_TYPE_CF = "centerFull"; // 同centerCrop
+
+        /**
+         * src 配置
+         */
+        public String srcId = "";
+        public String srcType = SRC_TYPE_IMG;
+        public String loadType = LOAD_TYPE_NET;
+        public String srcTag = "";
+        public String color = "#000000";
+        public String style = TEXT_STYLE_DEFAULT;
+        public int w = 0;
+        public int h = 0;
+        public String fitType = FIT_TYPE_FITXY;
+
+        /**
+         * src 辅助信息
+         */
+        public String srcPath = "";
+        public int z = 0; // 渲染层级 与输入顺序相关
+
+        @Override
+        public String toString() {
+            StringBuilder json = new StringBuilder();
+            json.append("{");
+            json.append("\"srcId\":").append("\"").append(srcId).append("\",");
+            json.append("\"srcType\":").append("\"").append(srcType).append("\",");
+            json.append("\"srcTag\":").append("\"").append(srcTag.trim()).append("\",");
+            if (SRC_TYPE_TXT.equals(srcType)) {
+                if (color != null && color != null) {
+                    json.append("\"color\":").append("\"").append(color.trim()).append("\",");
+                }
+                json.append("\"style\":").append("\"").append(style).append("\",");
+                json.append("\"loadType\":").append("\"").append(LOAD_TYPE_LOC).append("\",");
+            } else {
+                json.append("\"loadType\":").append("\"").append(loadType).append("\",");
+            }
+
+
+            json.append("\"fitType\":").append("\"").append(fitType).append("\",");
+            json.append("\"w\":").append(w).append(",");
+            json.append("\"h\":").append(h);
+            json.append("}");
+
+            return json.toString();
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder json = new StringBuilder();
+
+        json.append("\"src\":[");
+        Src src;
+        for (int i=0; i<srcs.size(); i++) {
+            src = srcs.get(i);
+            json.append(src.toString());
+            if (i != srcs.size() - 1) {
+                json.append(",");
+            }
+        }
+
+        json.append("]");
+
+        return json.toString();
+    }
+}

BIN
Android/aar/vap_2.0.6.aar


+ 52 - 0
tool/Mac_Tool.md

@@ -0,0 +1,52 @@
+# VAPXTool 
+
+## 简介
+vapxTool是专门为vap组件打造的素材生成工具
+
+Mac安装包下载:[download](https://github.com/Tencent/vap/releases/download/iOS1.0.3/VapxTool.dmg)
+
+Java版本(支持Windows & Mac): [VapTool_Java.md](./README.md)
+
+## 功能介绍
+
+### 基本操作
+![](./images/1.png)
+
+1. 版本号 - 默认为2,一般情况不需要改
+2. fps - 定义素材播放的帧率
+3. 码率 - 定义mp4中h.264 的码率 同一份素材,码率越高清晰度越高,文件大小也会越大
+4. alpha通道缩放 - 对素材中alpha数据进行压缩,以减少素材大小和显存占用。建议值0.5,数值越低质量越低
+5. 视频帧 - !!!!重要!!!!特效的帧素材,命名必须以固定长度的数字递增例如(000.png, 001.png, ...)
+6. 音频文件 - 可选,特效素材可以支持音频播放
+7. 融合信息 - 可选,增加融合效果,可以对静态素材增加网络图片或者自定义文字等。详细见 增加融合信息。
+8. 生成按钮 -  点击开始生成
+9. 输出目录 - 点击打开输出文件,mp4文件即为vap文件,使用这个文件即可;vapc.json文件是给前端用的,实际上就是已经编码在mp4的vap配置信息
+10. 经典模式开关 - 为企鹅电竞旧版本所需的非融合素材提供的兼容模式,外部团队可以不需要关注。具体区别在于打开兼容模式后- (1).帧素材的长或宽必须是16的倍数,否则直接失败;(2).alpha通道不能缩放,系数锁定为1 (3).无法增加融合信息 (4).alpha和rgb纹理之间没有缝隙,紧密排布。
+
+-------- 如果不需要融合信息的话 不需要关注以下操作 ------------
+
+### 增加融合信息
+![](./images/2.png)
+
+1. 占位符 - 用以标示该融合资源。例如图片的话建议命名为[img0],[img1],[img2],... 文字则建议命名为[txt0],[txt1],...;这都不固定,与开发约定好就可以了。
+2. 资源类型 - 目前支持1.网络图片 2.文字 两种类型
+3. 适配类型 - 铺满:资源会按照容器大小(在遮罩中指定)拉伸 等比适配 - 默认按资源尺寸展示,如果资源尺寸小于遮罩,则等比放大至可填满
+4. 宽高尺寸 - 宽高像素值,这里定义的是资源的宽高大小并不是最终展示的大小;影响像素质量。
+5. 上传遮罩 - !!!!重要!!!!命名要求与视频帧一样!且可以不连续但是命名一定是对应的帧数值。例 003.png指的是第四帧对应的遮罩。以下补充遮罩文件的说明:
+
+#### 遮罩文件说明
+例:
+![](./images/3.png)
+图中 1 2标示两组遮罩(1是头像遮罩 2.标示文字遮罩)中的150帧的遮罩,3表示对应的视频帧内容
+注意点:
+1. 遮罩帧的尺寸必须与视频帧一致
+2. 黑色表示显示,红色表示遮挡;
+
+效果图:
+![](./images/4.png)
+
+
+
+### 额外说明
+1. 为了提高在Android设备上的兼容性,对生成的素材长宽进行16倍率处理,即生成的素材长宽必然是16整数倍
+2. 为了消除视频编码算法对遮罩/原视频边缘的影响,遮罩/原视频周边会预留4像素,即遮罩/原视频与边缘间隔4像素,遮罩/原视频与遮罩/原视频间隔8像素;特别地,当遮罩/原视频宽度不小于最大长度(1504)时左右间距为0;当遮罩/原视频高度不小于最大长度(1504)时上下间距为0

+ 91 - 34
tool/README.md

@@ -1,52 +1,109 @@
-# VAPXTool 
+# VapTool
 
-## 简介
-vapxTool是专门为vap组件打造的素材生成工具
+VapTool Java版本支持 Windows 与Mac
 
-Mac安装包下载:[download](https://github.com/Tencent/vap/releases/download/iOS1.0.3/VapxTool.dmg)
+os|download|description
+---|---|---
+Windows|[VapTool\_Java\_Win\_Full.zip](https://github.com/Tencent/vap/releases/download/v1.0.0/VapTool_Java_Win_Full.zip)|包含jre 可直接运行
+Mac|[VapTool\_Java\_Mac\_without\_jre.zip](https://github.com/Tencent/vap/releases/download/v1.0.0/VapTool_Java_Mac_without_jre.zip)|未含jre 但Mac基本自带java 
 
-Java版本(支持Windows & Mac): [VapTool_Java.md](./VapTool_Java.md)
 
-## 功能介绍
+## Windows
 
-### 基本操作
-![](./images/1.png)
+双击此文件运行
 
-1. 版本号 - 默认为2,一般情况不需要改
-2. fps - 定义素材播放的帧率
-3. 码率 - 定义mp4中h.264 的码率 同一份素材,码率越高清晰度越高,文件大小也会越大
-4. alpha通道缩放 - 对素材中alpha数据进行压缩,以减少素材大小和显存占用。建议值0.5,数值越低质量越低
-5. 视频帧 - !!!!重要!!!!特效的帧素材,命名必须以固定长度的数字递增例如(000.png, 001.png, ...)
-6. 音频文件 - 可选,特效素材可以支持音频播放
-7. 融合信息 - 可选,增加融合效果,可以对静态素材增加网络图片或者自定义文字等。详细见 增加融合信息。
-8. 生成按钮 -  点击开始生成
-9. 输出目录 - 点击打开输出文件,mp4文件即为vap文件,使用这个文件即可;vapc.json文件是给前端用的,实际上就是已经编码在mp4的vap配置信息
-10. 经典模式开关 - 为企鹅电竞旧版本所需的非融合素材提供的兼容模式,外部团队可以不需要关注。具体区别在于打开兼容模式后- (1).帧素材的长或宽必须是16的倍数,否则直接失败;(2).alpha通道不能缩放,系数锁定为1 (3).无法增加融合信息 (4).alpha和rgb纹理之间没有缝隙,紧密排布。
+```sh
+win_start.bat
+```
 
--------- 如果不需要融合信息的话 不需要关注以下操作 ------------
+## Mac
 
-### 增加融合信息
-![](./images/2.png)
+需要打开终端命令行,进入到工具目录执行以下命令
 
-1. 占位符 - 用以标示该融合资源。例如图片的话建议命名为[img0],[img1],[img2],... 文字则建议命名为[txt0],[txt1],...;这都不固定,与开发约定好就可以了。
-2. 资源类型 - 目前支持1.网络图片 2.文字 两种类型
-3. 适配类型 - 铺满:资源会按照容器大小(在遮罩中指定)拉伸 等比适配 - 默认按资源尺寸展示,如果资源尺寸小于遮罩,则等比放大至可填满
-4. 宽高尺寸 - 宽高像素值,这里定义的是资源的宽高大小并不是最终展示的大小;影响像素质量。
-5. 上传遮罩 - !!!!重要!!!!命名要求与视频帧一样!且可以不连续但是命名一定是对应的帧数值。例 003.png指的是第四帧对应的遮罩。以下补充遮罩文件的说明:
+```sh
+// 先检查是否已经安装java
+// 如未有版本信息输出,请先安装java
+java -version
 
-#### 遮罩文件说明
+// 赋予脚步可执行权限(只需要执行一次)
+chmod +x mac_start.sh
+
+// 启动工具
+./mac_start.sh
+
+```
+
+## 工具说明
+
+![](images/vaptool_java_01.png)
+
+* codec: 编码类型(默认h264)
+ 	* h264: **优点**:兼容性好,几乎所有机器都能播放;**缺点**:压缩率没有h265高
+	* h265: **优点**:压缩率更高,画面更清晰;**缺点**:Android 4.x 版本无法播放,部分低端机器兼容性差; Web端浏览器可能不支持h265
+
+* fps: 每秒播放多少帧
+* alpha scale: 视频alpha区域是否缩放(默认缩放0.5),目前可选: 缩放0.5;不缩放1. 缩放视频能最终减小视频分辨率,提高兼容性
+* frames path: 视频帧存放的位置
+	* 视频帧命名方法 **000.png 001.png ... 099.png**。第一帧一定是**000.png**不然无法正常生成,可以参考"simple_demo"
+* audio: 需要集成到视频里的声音文件(目前支持mp3文件)
+
+普通VAP视频完成以上配置即可。
+ 
+点击"create VAP" 开始生成视频。
+
+
+### 文件输出
+* video.mp4: 最终生成的视频文件
+* vapc.json: web端配置文件
+* md5.txt: video.mp4的md5,可以做文件校验
+* frames: 临时图片文件
+
+## 进阶:融合动画
+ps:普通动画不需要此设置
+
+![](images/vaptool_java_02.png)
+
+* 点击"add"按钮添加融合动画遮罩信息
+
+* (1) source tag: 占位符标示,相当于当前资源的一个字符串标志,播放融合动画时,根据不同的tag 返回不同 bitmap 用于显示;
+
+* (2) source type: 表示属性类型,目前支持两种 image(图片), text(文字);
+
+* (3) fit type: 图片显示时的方式,目前支持两种 fitXY(图片平铺 default), centerCrop(比例缩放),这几个概念与Android里图片对其方式概念相同;
+
+* (4) mask path: 遮罩图片路径;
+
+* (5) text color: 如果是文字类型,设置文字颜色,格式: #000000
+
+* (6) text Bold: 如果是文字类型,设置文字是否为粗体
+
+### 遮罩图片说明
 例:
+
 ![](./images/3.png)
+
 图中 1 2标示两组遮罩(1是头像遮罩 2.标示文字遮罩)中的150帧的遮罩,3表示对应的视频帧内容
-注意点:
-1. 遮罩帧的尺寸必须与视频帧一致
-2. 黑色表示显示,红色表示遮挡;
+注意点:
+
+1. 遮罩文件命名规则,与视频帧相同**000.png 001.png ... 099.png**,文件名表示当前遮罩属于哪一帧;
+
+2. 遮罩帧的尺寸必须与视频帧一致;
+
+3. 遮罩内容: 黑色区域表示图片(文字)需要显示的位置,其它区域透明度必须为0 (黑色块内的红色表示遮挡区域);
+
+可以参考"vapx_demo"
+
+效果图:
 
-效果图:
 ![](./images/4.png)
 
+## 工具说明
+
+欢迎大家一起来完善Java版本功能. 
+
+Java工具源码路径:Android/PlayerProj  项目:[animtool](https://github.com/Tencent/vap/tree/master/Android/PlayerProj)
+
+原Mac工具说明[Mac tool](./Mac_Tool.md)
 
+VAP json配置信息字段说明[Image](images/vap_field_info.png).
 
-### 额外说明
-1. 为了提高在Android设备上的兼容性,对生成的素材长宽进行16倍率处理,即生成的素材长宽必然是16整数倍
-2. 为了消除视频编码算法对遮罩/原视频边缘的影响,遮罩/原视频周边会预留4像素,即遮罩/原视频与边缘间隔4像素,遮罩/原视频与遮罩/原视频间隔8像素;特别地,当遮罩/原视频宽度不小于最大长度(1504)时左右间距为0;当遮罩/原视频高度不小于最大长度(1504)时上下间距为0

BIN
tool/Release/VapxTool.dmg


+ 0 - 71
tool/VapTool_Java.md

@@ -1,71 +0,0 @@
-# VapTool
-
-VapTool Java版本支持 Windows 与Mac
-
-OS|download
----|---
-Windows|[VapTool\_Java\_Win\_Full.zip](https://github.com/Tencent/vap/releases/download/v1.0.0/VapTool_Java_Win_Full.zip)
-Mac|[VapTool\_Java\_Mac\_without\_jre.zip](https://github.com/Tencent/vap/releases/download/v1.0.0/VapTool_Java_Mac_without_jre.zip)
-
-### 注意
-Java版本支持能力:
-
-1. 基础透明视频输出;
-2. 支持H265视频输出;
-
-未支持能力:
-
-1. 不支持融合动画;
-2. 不支持音频合成;
-3. 不支持alpha区域按比例缩放;
-
-## Windows
-
-双击此文件运行
-
-```sh
-win_full_start.bat
-```
-
-## Mac
-
-需要打开终端命令行,进入到工具目录执行以下命令
-
-```sh
-// 先检查是否已经安装java
-// 如未有版本信息输出,请先安装java
-java -version
-
-// 赋予脚步可执行权限(只需要执行一次)
-chmod +x mac_start.sh
-
-// 启动工具
-./mac_start.sh
-
-```
-
-## 工具说明
-
-![](images/vaptool_java_01.png)
-
-* codec: 编码类型(默认h265)
-	* h265: 优点:压缩率更高,画面更清晰;缺点:Android 4.x 版本无法播放,部分低端机器兼容性差
-	* h264: 优点:兼容性好,几乎所有机器都能播放;缺点:压缩率没有h265高
-* fps: 每秒播放多少帧
-* input path: 视频帧存放的位置
-	* 视频帧命名方法 **000.png 001.png ... 099.png**。第一帧一定是**000.png**不然无法正常生成,可以参考"非融合特效demo"
-
-点击"create VAP" 开始生成视频。
-
-### 文件输出
-* video.mp4: 最终生成的视频文件
-* vapc.json: web端配置文件
-* md5.txt: video.mp4的md5,可以做文件校验
-* frames: 临时图片文件
-
-## VAP字段说明
-
-欢迎大家一起来完善Java版本功能,下表为VAP json配置信息字段说明。
-
-![](images/vap_field_info.png)
-

BIN
tool/images/4.png


BIN
tool/images/vaptool_java_01.png


BIN
tool/images/vaptool_java_02.png


+ 0 - 0
tool/非融合特效demo/000.png → tool/simple_demo/000.png


+ 0 - 0
tool/非融合特效demo/001.png → tool/simple_demo/001.png


+ 0 - 0
tool/非融合特效demo/002.png → tool/simple_demo/002.png


+ 0 - 0
tool/非融合特效demo/003.png → tool/simple_demo/003.png


+ 0 - 0
tool/非融合特效demo/004.png → tool/simple_demo/004.png


+ 0 - 0
tool/非融合特效demo/005.png → tool/simple_demo/005.png


+ 0 - 0
tool/非融合特效demo/006.png → tool/simple_demo/006.png


+ 0 - 0
tool/非融合特效demo/007.png → tool/simple_demo/007.png


+ 0 - 0
tool/非融合特效demo/008.png → tool/simple_demo/008.png


+ 0 - 0
tool/非融合特效demo/009.png → tool/simple_demo/009.png


+ 0 - 0
tool/非融合特效demo/010.png → tool/simple_demo/010.png


+ 0 - 0
tool/非融合特效demo/011.png → tool/simple_demo/011.png


+ 0 - 0
tool/非融合特效demo/012.png → tool/simple_demo/012.png


+ 0 - 0
tool/非融合特效demo/013.png → tool/simple_demo/013.png


+ 0 - 0
tool/非融合特效demo/014.png → tool/simple_demo/014.png


+ 0 - 0
tool/非融合特效demo/015.png → tool/simple_demo/015.png


+ 0 - 0
tool/非融合特效demo/016.png → tool/simple_demo/016.png


+ 0 - 0
tool/非融合特效demo/017.png → tool/simple_demo/017.png


+ 0 - 0
tool/非融合特效demo/018.png → tool/simple_demo/018.png


+ 0 - 0
tool/非融合特效demo/019.png → tool/simple_demo/019.png


+ 0 - 0
tool/非融合特效demo/020.png → tool/simple_demo/020.png


+ 0 - 0
tool/非融合特效demo/021.png → tool/simple_demo/021.png


+ 0 - 0
tool/非融合特效demo/022.png → tool/simple_demo/022.png


+ 0 - 0
tool/非融合特效demo/023.png → tool/simple_demo/023.png


+ 0 - 0
tool/非融合特效demo/024.png → tool/simple_demo/024.png


+ 0 - 0
tool/非融合特效demo/025.png → tool/simple_demo/025.png


+ 0 - 0
tool/非融合特效demo/026.png → tool/simple_demo/026.png


+ 0 - 0
tool/非融合特效demo/027.png → tool/simple_demo/027.png


+ 0 - 0
tool/非融合特效demo/028.png → tool/simple_demo/028.png


+ 0 - 0
tool/非融合特效demo/029.png → tool/simple_demo/029.png


+ 0 - 0
tool/非融合特效demo/030.png → tool/simple_demo/030.png


+ 0 - 0
tool/非融合特效demo/031.png → tool/simple_demo/031.png


+ 0 - 0
tool/非融合特效demo/032.png → tool/simple_demo/032.png


+ 0 - 0
tool/非融合特效demo/033.png → tool/simple_demo/033.png


+ 0 - 0
tool/非融合特效demo/034.png → tool/simple_demo/034.png


+ 0 - 0
tool/非融合特效demo/035.png → tool/simple_demo/035.png


+ 0 - 0
tool/非融合特效demo/036.png → tool/simple_demo/036.png


+ 0 - 0
tool/非融合特效demo/037.png → tool/simple_demo/037.png


+ 0 - 0
tool/非融合特效demo/038.png → tool/simple_demo/038.png


+ 0 - 0
tool/非融合特效demo/039.png → tool/simple_demo/039.png


+ 0 - 0
tool/非融合特效demo/040.png → tool/simple_demo/040.png


+ 0 - 0
tool/非融合特效demo/041.png → tool/simple_demo/041.png


+ 0 - 0
tool/非融合特效demo/042.png → tool/simple_demo/042.png


+ 0 - 0
tool/非融合特效demo/043.png → tool/simple_demo/043.png


+ 0 - 0
tool/非融合特效demo/044.png → tool/simple_demo/044.png


+ 0 - 0
tool/非融合特效demo/045.png → tool/simple_demo/045.png


+ 0 - 0
tool/非融合特效demo/046.png → tool/simple_demo/046.png


+ 0 - 0
tool/非融合特效demo/047.png → tool/simple_demo/047.png


+ 0 - 0
tool/非融合特效demo/048.png → tool/simple_demo/048.png


+ 0 - 0
tool/非融合特效demo/049.png → tool/simple_demo/049.png


+ 0 - 0
tool/非融合特效demo/050.png → tool/simple_demo/050.png


+ 0 - 0
tool/非融合特效demo/051.png → tool/simple_demo/051.png


+ 0 - 0
tool/非融合特效demo/052.png → tool/simple_demo/052.png


+ 0 - 0
tool/非融合特效demo/053.png → tool/simple_demo/053.png


+ 0 - 0
tool/非融合特效demo/054.png → tool/simple_demo/054.png


+ 0 - 0
tool/非融合特效demo/055.png → tool/simple_demo/055.png


+ 0 - 0
tool/非融合特效demo/056.png → tool/simple_demo/056.png


+ 0 - 0
tool/非融合特效demo/057.png → tool/simple_demo/057.png


+ 0 - 0
tool/非融合特效demo/058.png → tool/simple_demo/058.png


+ 0 - 0
tool/非融合特效demo/059.png → tool/simple_demo/059.png


+ 0 - 0
tool/非融合特效demo/060.png → tool/simple_demo/060.png


+ 0 - 0
tool/非融合特效demo/061.png → tool/simple_demo/061.png


+ 0 - 0
tool/非融合特效demo/062.png → tool/simple_demo/062.png


+ 0 - 0
tool/非融合特效demo/063.png → tool/simple_demo/063.png


+ 0 - 0
tool/非融合特效demo/064.png → tool/simple_demo/064.png


+ 0 - 0
tool/非融合特效demo/065.png → tool/simple_demo/065.png


+ 0 - 0
tool/非融合特效demo/066.png → tool/simple_demo/066.png


+ 0 - 0
tool/非融合特效demo/067.png → tool/simple_demo/067.png


+ 0 - 0
tool/非融合特效demo/068.png → tool/simple_demo/068.png


+ 0 - 0
tool/非融合特效demo/069.png → tool/simple_demo/069.png


+ 0 - 0
tool/非融合特效demo/070.png → tool/simple_demo/070.png


+ 0 - 0
tool/非融合特效demo/071.png → tool/simple_demo/071.png


+ 0 - 0
tool/非融合特效demo/072.png → tool/simple_demo/072.png


+ 0 - 0
tool/非融合特效demo/073.png → tool/simple_demo/073.png


+ 0 - 0
tool/非融合特效demo/074.png → tool/simple_demo/074.png


+ 0 - 0
tool/非融合特效demo/075.png → tool/simple_demo/075.png


Some files were not shown because too many files changed in this diff