前言

灵感来自于 : , 因为现在有了根据图片来生成gif的工具, 而且自己当时使用的一款gifGenerator并不是很完善, 再者闲来无事, 就花了一些时间来完成这个了

问题描述

一个简单的gif生成器, 傻瓜式的, 应该基本上都会用
难点主要在于 :
1 一些swing相关的效果[比如 取消边框, 鼠标拖拽, 进度条, 更新任务进度等等]
2 robot截图之后缓冲队列的设计
3 业务操作的线程关系的协调

这里 说一说缓冲队列的设计, 缓存队列集合bufferedImgQueues是使用Map< Integer, Queue< BufferedImage>>, 这个映射key是给定的缓冲队列的索引, value是对应过的缓冲队列, 假设刷出图片的阈值为flushThreshold, 如果现在截取的图片数量超过了flushThreshold 则将当前缓存队列添加到bufferedImgQueues, 然后 启动一条线程刷出当前队列的图片, 刷出所有图片之后, 将该队列移除bufferedImgQueues[for gc], 然后新建一个缓冲队列, 周而复始

1 我曾经直接使用单线程刷出图片, 但是 太慢了, 太不能忍受了, 随着图片的增加, 用户等待时间会很长
2 再考虑直接 使用一个List, 如果使用ArrayList的话, 在多线程刷出图片的场景会存在一个问题, 就是移除已经刷出的图片开销会很大, 而且 随着缓冲的图片越来越多维护并发的正确性 也需要很多的开销
如果使用LinkedList, 在多线程刷出图片的场景同样存在问题, 读取每一个线程应该刷出的图片的效率不高, 而且在并发方便, 和上面的ArrayList存在相同的问题
3 在考虑使用BlockQueue在链表头获取, 删除元素, 使用Lock相关api维护线程的安全, 但是还存在维护并发的开销啊,, 能不能省掉这一笔开销呢?
4 就是最后决定用的Map< Integer, Queue< BufferedImage>>了, 讲各个刷出图片线程刷出的图片队列分开, 就好了嘛

其他的业务逻辑, 请详见代码

依赖于AnimatedGifEncoder, 这个是一个开源的gif的生成的工具, 大家可以自行搜索下载

思路

思路 :
1 begin按钮, 启动截图线程
2 stop按钮, 停止截图线程, 并等待刷出所有的图片成功
3 save按钮, 停止截图线程, 刷出所有图片, 生成gif文件
4 newGif按钮, 更新标志位[下一次录制的时候, 清空临时文件夹], 停止截图线程
5 close按钮, 关闭相关资源, 关闭程序

参考代码

: 依赖于AnimatedGifEncoder

1 Test12GifGenerator .java

/**
 * file name : TransparentFrame.java
 * created at : 4:45:50 PM Nov 23, 2015
 * created by
 */

package com.hx.test08;

// 录制gif的工具
// 将ToolFrame, ProgressFrame 该为继承自JDialog, 因为这两个不需要创建一个窗口在任务栏   -- 2015.11.25
public class Test12GifGenerator extends JFrame {    

    // 窗口的位置, 宽高, 工具窗口的宽高
    private static int FRAME_X = 100;
    private static int FRAME_Y = 100;
    private static int FRAME_WIDTH = 800;
    private static int FRAME_HEIGHT = 600;
    private static int TOOL_FRAMEWIDTH = 100;
    private static int TOOL_FRAMEHEIGHT = FRAME_HEIGHT;

    // 常量配置
    // 临时文件夹, 临时文件名, 临时文件后缀, GIF后缀, 临时文件格式
    // 截图延迟, 缓存的图片个数, 等待线程池结束任务的时候 检查的周期
    private final static String TMP_DIR = "C:\\Users\\970655147\\Desktop\\tmp\\gifTmp";
    private final static String TMP_NAME = "gifTmp";
    private final static String SUFFIX = Tools.PNG;
    private final static String GIF = ".gif";
    private final static String FORMAT = SUFFIX.substring(1);
    private static int delay = 100;
    private static int cachedImageSize = 60;
    private static int checkInterval = 200;
    private static int flushImgCheckInterval = 500;

    // 工具窗口, 精度条窗口
    private ToolFrame toolFrame;
    private ProgressBarFrame progressBarFrame;

    // 初始化
        // 1. 配置当前窗口的属性
        // 2. 创建ProgressBarFrame, ToolFrame, 并配置
    public Test12GifGenerator() {
        this.setTitle("gif generator");
        this.setBounds(FRAME_X, FRAME_Y, FRAME_WIDTH, FRAME_HEIGHT);    
        this.setUndecorated(true);
        this.setVisible(true);
        this.setOpacity(0.2f);
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);

        progressBarFrame = new ProgressBarFrame(this);
        toolFrame = new ToolFrame(this);
        toolFrame.setLocation(FRAME_X+FRAME_WIDTH, FRAME_Y);
        MouseAdapter adater = new DragMouseAdapter(this) {
            public void mousePressed(MouseEvent e) {
                super.mousePressed(e);
                toolFrame.requestFocus();
            }
            public void mouseDragged(MouseEvent e) {
                super.mouseDragged(e);
                java.awt.Point curPos = Test12GifGenerator.this.getLocation();
                toolFrame.setLocation(curPos.x+FRAME_WIDTH, curPos.y);
            }
        };
        this.addMouseListener(adater);
        this.addMouseMotionListener(adater);
        toolFrame.addMouseListener(adater);
        toolFrame.addMouseMotionListener(adater);
    }

    // main
    public static void main(String[] args) { 

         new Test12GifGenerator();

    }    

    // 清理临时工作区间
        // 清理掉所有getScreenShotedFiles返回的文件
    private static void clearTmpDir() {
        File[] files = getScreenShotedFiles();

        String curFile = null;
        int i = -1;
        try {
            for(i=0; i<files.length; i++) {
                curFile = files[i].getName();
                files[i].delete();
            }
        } catch (Exception e) {
            Log.err("error while delete " + curFile + " !");
            e.printStackTrace();
        } finally {
            Log.log("deleted " + i + " tmp files !");
        }
    }
    // 获取截图截取到的图片
        // 这里的条件为以TMP_NAME开头, 并且以SUFFIX结尾
    public static File[] getScreenShotedFiles() {
        File tmpDir = new File(TMP_DIR );
        File[] files = tmpDir.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.startsWith(TMP_NAME) && name.endsWith(SUFFIX);
            }
        });

        return files;
    }

    // 右边的小工具栏
    static class ToolFrame extends JDialog {
        // 父窗口, 开始, 停止, 保存, 新建gif, 关闭按钮
        // 输出缓存的图片进度按钮, 生成gif进度按钮
        private Test12GifGenerator parentFrame;
        private JButton begin = new JButton("begin");
        private JButton stop = new JButton("stop");
        private JButton save = new JButton("save");
        private JButton newGif = new JButton("newGif");
        private JButton close = new JButton("close");
        private JButton flushImgProgress = new JButton("0 / 0");
        private JButton generateGifProgress = new JButton("0 / 0");

        // 是否开始, 是否开始过, 是否是创建新的gif, 是否需要将缓存的gif刷出到文件
        private boolean isStart = false;
        private boolean haveStarted = false;
        private boolean isNewGif = true;
        private boolean needFlushImgs = true;

        // robot 截图使用, 缓存的文件索引, 线程池的个数
        // 刷新图片文件的线程池, 任务线程池, 获取图片的线程
        // 刷出到磁盘的图片的个数, 刷出去的 或者正在刷的图片个数, 更新面板的进度的线程是否存在
        // 对于缓存图片, 我之前的想法是将一定量的图片缓存在一个BufferedImage[]中
            // 但是  从实际情况来说, 刷新图片的速度是满足不了获取图片的速度的, 所以就使用了一个Map<Integer, Queue<BufferedImage>>
            // 是为了方便获取现在仍在向磁盘刷出的Image
            // 如果 不需要获取在刷出的Image, 可以省略imgs这个数据结构
        private Robot robot;
        Map<Integer, Queue<BufferedImage>> imgses = new HashMap<>();
        Queue<BufferedImage> imgs = new LinkedList<>();
        private int cachedImgQueueIdx = 0;
        private int flushImgsThreads = 10;
        private int nThreads = 10;
        private ThreadPoolExecutor flushImgsThreadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(flushImgsThreads);
        private ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(nThreads);
        private Thread getImageThread = null;
        private AtomicInteger flushOnDisk = new AtomicInteger(0);
        private int flushed = 0;
        private boolean updateFlushProcessExists = false;

        // 初始化
            // 1. 初始化robot
            // 2. 配置当前JFrame
            // 3. 创建子组件, 添加listener
        public ToolFrame(Test12GifGenerator parentFrame) {
            try {
                robot = new Robot();
            } catch (AWTException e) {
                e.printStackTrace();
            }

            this.parentFrame = parentFrame;
            this.setTitle("tool");
            this.setUndecorated(true);
            this.setVisible(true);
            this.setBounds(FRAME_X, FRAME_Y, TOOL_FRAMEWIDTH, TOOL_FRAMEHEIGHT);

            this.setLayout(new GridLayout(10, 1) );
            this.add(begin );
            this.add(stop );
            this.add(save );
            this.add(newGif );
            this.add(close );
            this.add(flushImgProgress );
            this.add(generateGifProgress );
            flushImgProgress.setEnabled(false);
            generateGifProgress.setEnabled(false);
            checkStartOrStop();

            ActionListener listener = new BtnActionListener();
            begin.addActionListener(listener );
            stop.addActionListener(listener );
            close.addActionListener(listener );
            save.addActionListener(listener );
            newGif.addActionListener(listener );
        }

        // 配置begin, stop, close按钮的可用性
            // 各个按钮的可用性由isStart, haveStarted决定
        private void checkStartOrStop() {
            if(isStart) {
                begin.setEnabled(false);
                stop.setEnabled(true);
            } else {
                begin.setEnabled(true);
                stop.setEnabled(false);
            }
            if(haveStarted) {
                newGif.setEnabled(true);
                save.setEnabled(true);
            } else {
                newGif.setEnabled(false);
                save.setEnabled(false);
            }
            close.setEnabled(true);
        }

        // 刷出缓存的图片
            // 放入flushImgsThreadPool线程池中启动一个任务, 输出给定队列的图片
            // 如果 没有启动更新刷出图片进度的线程, 则启动一条刷新图片进度的线程
        private void flush(final int cachedImgQueueIdx, final Queue<BufferedImage> cachedImages) {
            flushImgsThreadPool.execute(new Runnable() {
                public void run() {
                    int size = cachedImages.size();
                    TmpGetter tmpGetter = new MyTmpGetter(TMP_DIR, TMP_NAME, flushed, SUFFIX);
                    flushed += size;
                    try {
                        while(cachedImages.size() > 0) {
                            BufferedImage curImg = cachedImages.poll();
                            if(needFlushImgs) {
                                ImageIO.write(curImg, FORMAT, new File(tmpGetter.getNextTmpPath()) );
                            }
                            flushOnDisk.incrementAndGet();
                        }
                    } catch (Exception e) {
                        Log.err("error while flush pics !");
                        e.printStackTrace();
                    } finally {
                        imgses.remove(cachedImgQueueIdx);
                        Log.log("flushed " + size + " [thread(" + cachedImgQueueIdx + ")] pics, needFlushImgs : " + needFlushImgs + " !");
                    }
                }
            });

            if(! updateFlushProcessExists) {
                updateFlushProcessExists = true;
                threadPool.execute(new Runnable() {
                    public void run() {
                        while (! flushImgsThreadPool.isShutdown() ) {
                            int queueSize = flushImgsThreadPool.getQueue().size();
                            int activeTasks = flushImgsThreadPool.getActiveCount();

                            flushImgProgress.setText(getProgressStr(flushOnDisk.get(), flushed) );
//                          Log.log(getProgressStr(flushOnDisk.get(), flushed) );
                            if((queueSize == 0) && (activeTasks == 0) ) {
                                break ;
                            }

                            Tools.sleep(flushImgCheckInterval);
                        }

                        updateFlushProcessExists = false;
                    }
                });
            }
        }

        // 根据给定的录制的图片成成gif
            // 首先 等待刷新图片的线程结束
            // 然后  在根据AnimatedGifEncoder创建gif
            // 最后  恢复按钮的可用性
        private void generateGIF() {
            threadPool.execute(new Runnable() {
                public void run() {
                    try {
                        if(getImageThread != null) {
                            getImageThread.join();
                        }
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    } finally {
                        getImageThread = null;
                        awaitTasksEnd(flushImgsThreadPool, checkInterval, false);
                    }

                    parentFrame.progressBarFrame.setVisible(true);
                    parentFrame.progressBarFrame.updateProgress("generate gif !");
                    AnimatedGifEncoder gifEncoder = new AnimatedGifEncoder();
                    TmpGetter tmpGetter = new MyTmpGetter(TMP_DIR, TMP_NAME, 0, SUFFIX);
                    String targetPath = tmpGetter.getNextTmpPath(GIF);
                    while(new File(targetPath).exists()) {
                        targetPath = tmpGetter.getNextTmpPath(GIF);
                    }

                    try {
                        gifEncoder.start(targetPath);
                        gifEncoder.setQuality(15);
                        gifEncoder.setRepeat(0);
                        gifEncoder.setDelay(delay);
                        File[] files = getScreenShotedFiles();
                        for(int i=0; i<files.length; i++) {
                            gifEncoder.addFrame(ImageIO.read(files[i]) );
                            generateGifProgress.setText(getProgressStr(i+1, files.length) );
                            parentFrame.progressBarFrame.updateProgress(i+1,  files.length);
                        }

                        gifEncoder.finish();
                    } catch (FileNotFoundException e) {
                        Log.err("have no this file : " + targetPath + " !");
                        e.printStackTrace();
                    } catch (Exception e) {
                        Log.err("error while make gif !");
                        e.printStackTrace();
                    } finally {
                        parentFrame.progressBarFrame.setVisible(false);
                        parentFrame.progressBarFrame.updateProgress(0);
                        Log.log("generate gif['" + targetPath + "'] success !");
                        checkStartOrStop();
                    }
                }
            });
        }

        // 如果获取图片的线程存在
            // 启动一条线程等待其终止, 并恢复各个按钮的可用性
            // 否则  恢复各个按钮的可用性
        private void joinIfGetImgThreadExists(final boolean isShutdown) {
            if(getImageThread != null) {
            threadPool.execute(new Runnable() {
                public void run() {
                        try {
                            getImageThread.join();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } finally {
                            getImageThread = null;
                            awaitTasksEnd(flushImgsThreadPool, checkInterval, isShutdown);
                            checkStartOrStop(); 
                        }
                    }
                });
            } else {
                checkStartOrStop(); 
            }
        }

        // 关闭当前程序
            // 配置是否需要刷出剩余缓存的图片
            // 如果需要生成gif的话, 生成gif
            // 等待两个业务线程池结束
            // 关闭三个窗口
        private void closeWindow(boolean isGenerateGif) {
            needFlushImgs = isGenerateGif;
            if(isGenerateGif) {
                generateGIF();
            }

            awaitTasksEnd(flushImgsThreadPool, checkInterval, true);
            awaitTasksEnd(threadPool, checkInterval, true);
            parentFrame.dispose();
            ToolFrame.this.dispose();
            parentFrame.progressBarFrame.dispose();
        }

        // 等待 线程池中任务结束 [并不关闭线程池]
        public static void awaitTasksEnd(ThreadPoolExecutor threadPool, int checkInterval, boolean isShutdown, boolean isLog) {
            while (! threadPool.isShutdown() ) {
                int taskInQueue = threadPool.getQueue().size();
                int activeTaskCount = threadPool.getActiveCount();
                if((taskInQueue == 0) && (activeTaskCount == 0) ) {
                    if(isShutdown) {
                        threadPool.shutdown();
                    }
                    break ;
                } else {
                    if(isLog ) {
                        Log.log("task in queue : " + taskInQueue + ", active task count : " + activeTaskCount + ", at : " + new Date().toString() + " !");
                    }
                    Tools.sleep(checkInterval);
                }
            }
        }
        public static void awaitTasksEnd(ThreadPoolExecutor threadPool, int checkInterval, boolean isShutdown) {
            awaitTasksEnd(threadPool, checkInterval, isShutdown, false);
        }

        // 获取进度字符串
        private static String getProgressStr(int now, int max) {
            return now + " / " + max;
        }

        // 响应按钮的时间
            // 如果是开始按钮, 启动截屏线程, 当缓冲队列中的图片达到一定的阈值的时候启动一条线程刷出图片
            // 如果是stop按钮, 停止截图线程, 并等待刷出所有的图片成功
            // save按钮, 停止截图线程, 刷出所有图片, 生成gif文件
            // newGif按钮, 更新标志位[下一次录制的时候, 清空临时文件夹], 停止截图线程
            // 如果是close按钮, 关闭相关资源, 关闭程序
        class BtnActionListener implements ActionListener {
            @Override
            public void actionPerformed(ActionEvent e) {
                if(e.getSource() == begin) {
                    isStart = true;
                    haveStarted = true;
                    needFlushImgs = true;
                    if(isNewGif) {
                        clearTmpDir();
                        flushOnDisk.set(0);;
                        flushed = 0;
                        isNewGif = false;
                        cachedImgQueueIdx = 0;
                    }

                    getImageThread = new Thread(new GetImageRunnable() );
                    getImageThread.start();
                    checkStartOrStop();
                } else if(e.getSource() == stop) {
                    isStart = false;
                    needFlushImgs = true;

                    setEnabled(false);
                    joinIfGetImgThreadExists(false);
                } else if(e.getSource() == save) {
                    isStart = false;
                    isNewGif = true;
                    haveStarted = false;
                    needFlushImgs = true;

                    setEnabled(false);
                    generateGIF();
                } else if(e.getSource() == newGif) {
                    isStart = false;
                    isNewGif = true;
                    haveStarted = false;
                    needFlushImgs = false;

                    setEnabled(false);
                    joinIfGetImgThreadExists(false);
                } else if(e.getSource() == close) {
                    isStart = false;
                    if(haveStarted) {
                        int res = JOptionPane.showConfirmDialog(ToolFrame.this.parentFrame, "need save gif ?");
                        if(res == JOptionPane.OK_OPTION ) {
                            closeWindow(true);
                        }else if(res ==JOptionPane.NO_OPTION ) {
                            closeWindow(false);
                        } else {

                        }
                    } else {
                        closeWindow(false);
                    }
                    checkStartOrStop();
                }

            }

            // 配置五个按钮是否可按键
            private void setEnabled(boolean isEnable) {
                begin.setEnabled(isEnable);
                stop.setEnabled(isEnable);
                save.setEnabled(isEnable);
                newGif.setEnabled(isEnable);
                close.setEnabled(isEnable);
            }
        }

        // 绘制图片的线程
        class GetImageRunnable implements Runnable {
            public void run() {
                while(isStart) {
                    imgs.add(robot.createScreenCapture(ToolFrame.this.parentFrame.getBounds()) );
//                  Log.log(imgs.size());
                    if(imgs.size() >= cachedImageSize) {
                        imgses.put(cachedImgQueueIdx, imgs);
                        flush(cachedImgQueueIdx ++, imgs);
                        imgs = new LinkedList<>();
                    }
                    Tools.sleep(delay);
                }

                if(imgs.size() > 0) {
                    imgses.put(cachedImgQueueIdx, imgs);
                    flush(cachedImgQueueIdx ++, imgs);
                    // 这里可以不用新建imgs, 因为flush之后imgs会被清空
                    imgs = new LinkedList<>();
                }
            }
        }
    }

    // 精度条的Frame
    public static class ProgressBarFrame extends JDialog {
        // 进度条
        private JProgressBar progressBar = new JProgressBar();

        // 初始化
            // 1. 配置当前Frame
            // 2. 创建子组件, 并初始化
        public ProgressBarFrame(JFrame parentFrame) {
            setSize(300, 50);
            setUndecorated(true);
            MouseAdapter adapter = new DragMouseAdapter(this);
            addMouseListener(adapter);
            addMouseMotionListener(adapter);

            progressBar.setStringPainted(true);  
            progressBar.setValue(0);
            progressBar.setMaximum(100);
            progressBar.setString("...");
            this.add(progressBar);

            java.awt.Point parentLocation = parentFrame.getLocation();
            parentLocation.x += FRAME_WIDTH >> 1;
            parentLocation.y += FRAME_HEIGHT >> 1;
            setLocation(parentLocation);
        }

        // 更新精度条
        public void updateProgress(int now, int max) {
            progressBar.setValue(now);
            progressBar.setMaximum(max);
        }
        public void updateProgress(int now) {
            progressBar.setValue(now);
        }
        public void updateProgress(String str) {
            progressBar.setString(str);
        }
    }

    // TmpGetter
    static class MyTmpGetter extends TmpGetter {
        // 初始化
        public MyTmpGetter(String tmpDir, String tmpName, int tmpIdx, String suffix) {
            super(tmpDir, tmpName, tmpIdx, suffix);
        }

        // 获取临时文件的下一个索引[生成文件名称]
        @Override
        protected String getNextTmpName() {
            int idx = tmpIdx.getAndIncrement();
            String idxStr = String.format("%06d", idx);
            return TMP_NAME + (idxStr );
        }
    }

}

2 DragMouseAdapter.java

/**
 * file name : DragMouseListener.java
 * created at : 9:29:40 PM Nov 24, 2015
 * created by 
 */

package com.hx.util;

// 拖拽的MouseListener
public class DragMouseAdapter extends MouseAdapter {

    // 操作的frame, 上一次点击按钮的时候的x
    protected Component component;
    private int lastX = -1;
    private int lastY = -1;

    // 初始化
    public DragMouseAdapter(Component component) {
        super();
        this.component = component;
    }

    // 记录拖拽之前点击的坐标
    @Override
    public void mousePressed(MouseEvent e) {
        lastX = e.getX();
        lastY = e.getY();
    }

    // 更新给定过的Component的位置
    @Override
    public void mouseDragged(MouseEvent e) {
        if(lastX != -1) {
            java.awt.Point point = component.getLocation();
            point.x += e.getX() - lastX;
            point.y += e.getY() - lastY;
            component.setLocation(point );
        }
    }

}

3 TmpGetter .java

/**
 * file name : TmpGetter.java
 * created at : 9:31:46 PM Nov 24, 2015
 * created by 
 */

package com.hx.util;

// 获取临时文件相关的数据
public class TmpGetter {

    // 临时文件夹, 临时文件名, 临时文件索引, 默认的后缀
    public final String tmpDir;
    public final String tmpName;
    public final AtomicInteger tmpIdx;
    public final String suffix;

    // 初始化
    public TmpGetter(String tmpDir, String tmpName, int tmpIdx, String suffix) {
        super();
        this.tmpDir = tmpDir;
        this.tmpName = tmpName;
        this.tmpIdx = new AtomicInteger(tmpIdx);
        this.suffix = suffix;
    }

    // 获取临时路径的下一个路径[返回文件路径]
    public String getNextTmpPath() {
        return tmpDir + "\\" + getNextTmpName() + suffix;
    }
    public String getNextTmpPath(String suffix) {
        return tmpDir + "\\" + getNextTmpName() + suffix;
    }
    public String getNextTmpPath(String fileName, String suffix) {
        return tmpDir + "\\" + fileName + suffix;
    }
    public String getTmpPath(int idx) {
        return tmpDir + "\\" + tmpName + idx + suffix;
    }
    public String getTmpPath(int idx, String suffix) {
        return tmpDir + "\\" + tmpName + idx + suffix;
    }
    public String getTmpPath(String name) {
        return tmpDir + "\\" + name + suffix;
    }
    public String getTmpPath(String name, String suffix) {
        return tmpDir + "\\" + name + suffix;
    }
    public String getNextTmpDir() {
        return tmpDir + "\\" + getNextTmpName();
    }
    public String getTmpDir(int idx) {
        return tmpDir + "\\" + tmpName + idx;
    }
    public String getTmpDir(String name) {
        return tmpDir + "\\" + name;
    }
    public void setTmpIdx(int tmpIdx) {
        this.tmpIdx.set(tmpIdx);
    }

    // 获取临时文件的下一个索引[生成文件名称]
    protected String getNextTmpName() {
        return tmpName + (String.valueOf(tmpIdx.getAndIncrement()) );
    }
}

效果截图

13 gifGenerator_工具

newGif

13 gifGenerator_gif_02

总结

测试 下来, 这个 是可以用, 但是 可能相关api优化的不够吧, 导致gif占用的空间非常的大, 所以 也就只能当做写着玩玩了
还有一个功能在于, 可以截屏, 截出一系列的图片, 然后使用专业的gif生成工具来完成gif的制作, 这样的话, 这个工具就相当于是一个录屏工具

当然 这种功能完全可以通过gifGenerator等等其他可视化工具实现, 这个不过是瞎扯淡, 写着玩..

参考 :

注 : 因为作者的水平有限,必然可能出现一些bug, 所以请大家指出!