npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

ray-visutil

v1.3.18

Published

vis utils

Downloads

222

Readme

ray-visutil

vis tools

author

ilex.h

methods

  • ColorUtils

  • SvgUtils

  • fullscreen

  • memorize 缓存方法执行结果

  • deepEqual

  • shallowEqual

  • resizeEvent [bindResize, unbindResize]

  • createInterface

const MyInterface = Interface.create('myMethodA', 'myMethodB')
// or
const MyInterface = new Interface('myMethodA', 'myMethodB')

class MyClass extends MyInterface {
  constructor () {
    super()
  }

  myMethodA () {
    // ...implementation goes here
  }
}

const instance = new MyClass()
// throws a new error with the message:
// 'The following function(s) need to be implemented for class MyClass: myMethodB'

class MySubClass extends MyClass {
  constructor () {
    super()
  }

  myMethodA () {
    // override 'myMethodA'
  }
}

const instance = new MySubClass()
// still throws an error with the message:
// 'The following function(s) need to be implemented for class MyClass: myMethodB'

isImplementedBy

const MyInterface = new Interface('myMethod')

class MyClass {
  myMethod () {
    // some implementation
  }
}

class MyOtherClass {
  myOtherMethod () {
    // some implementation
  }
}

const instanceA = new MyClass()
const instanceB = new MyOtherClass()

MyInterface.isImplementedBy(instanceA) // returns true
MyInterface.isImplementedBy(instanceB) // returns false

transition

过渡动画

移植于 @tweenjs/tween.js, or tween.js

用于简单动画的JavaScript补间引擎,结合了优化的 Robert Penner 方程。

example

import TG from 'ray-visutil/lib/transition';

function render(){
  const box = document.createElement('div');
  box.style.setProperty('background-color', '#008800');
  box.style.setProperty('width', '100px');
  box.style.setProperty('height', '100px');
  document.body.appendChild(box);

  // Setup the animation loop.
  function animate(time) {
      requestAnimationFrame(animate);
      TG.update(time);
  }
  requestAnimationFrame(animate);

  const coords = { x: 0, y: 0 }; // 起始点 (0, 0)
  const transition = new TG.Transition(coords) // 创建一个新的transition用来改变 'coords'
          .to({ x: 300, y: 200 }, 1000) // 在1s内移动至 (300, 200)
          .easing(TG.Easing.Quadratic.Out) // 使用缓动功能使的动画更加平滑
          .onUpdate(function() { // Called after TG updates 'coords'.
              // 将 'box' 移动到 'coords' 所描述的位置,配合 CSS 过渡
              box.style.setProperty('transform', 'translate(' + coords.x + 'px, ' + coords.y + 'px)');
          })
          .start(); // Start the transition immediately.
}

render();

options

TransitionGroup

import TG from 'ray-visutil/lib/transition/TransitionGroup';

用于 添加、删除、更新动画

  • getAll()
  • removeAll()
  • add(transition)
  • remove(transition)
  • update(time, preserve)

TransitionCore

import TG from 'ray-visutil/lib/transition/TransitionCore';

动画核心库,用于实现动画

  • constructor(object, group) : object 为初始属性
  • getId(): 获取动画id
  • isPlaying(): 判断是否正在执行动画
  • to(properties, duration): 从初属性转化为目标属性,duration 为耗时时间,默认 1000
  • start(time): 开启动画
  • stop(): 停止动画
  • end(): 终止动画,设置 update(time)中的,time = startTime + duration
  • stopChainedTransitions():停止 chain 动画
  • group(): 重新设置 TG
  • delay(amount): 延时时间
  • repeat(times): 重复执行次数
  • repeatDelay(amount): 设置重复延时
  • kickBack(kb): 设置回弹
  • easing(eas): 设置动画速度函数,默认为 [Easing.Linear.None]
  • interpolation(inter): 插入动画,默认为 [Interpolation.Linear]
  • chain(): 用于多个动画之间连续交换
  • onStart(callback): start 时回调
  • onUpdate(callback): 更新时回调,回调函数中的参数为constructor中具体的 object 值
  • onComplete(callback): 动画执行完成时的回调,回调函数中的参数为constructor中具体的 object 值
  • onStop(callback): 设置停止动画时的回调,回调函数中的参数为constructor中具体的 object 值
  • update(time): 更新动画

TG

import TG from 'ray-visutil/lib/transition';

实质是一个 TransitionGroup 实例。

  • TG.TransitionGroup
  • TG.Transition
  • TG.Easing
  • TG.Interpolation

polylabel

Given polygon coordinates in GeoJSON-like format and precision (1.0 by default), Polylabel returns the pole of inaccessibility coordinate in [x, y] format.

  • 基本使用
import polylabel from 'ray-visutil/lib/polylabel';

const polygon = [
  [[3116, 3071], [3118, 3068], [3108, 3102], [3100, 3105]],
  [[4016, 1878], [4016, 1864], [4029, 1859], [4024, 1850], [4008, 1839]],
  [([3315, 1339], [3327, 1332], [3331, 1324], [3323, 1329])]
];

const p = polylabel(polygon, 1.0); // [3106.1875, 3098.9375]
const p = polylabel(polygon, 50); // [3108.8702290076335,3090.8778625954196]
  • test
var p = polylabel([[[0, 0], [1, 0], [2, 0], [0, 0]]]); // [0, 0]

p = polylabel([[[0, 0], [1, 0], [1, 1], [1, 0], [0, 0]]]); // [0, 0]

TinyQueue

  • 基本使用
import TinyQueue from 'ray-visutil/lib/polylabel/TinyQueue';
// create an empty priority queue
var queue = new TinyQueue();

// add some items
queue.push(7);
queue.push(5);
queue.push(10);

// remove the top item
var top = queue.pop(); // returns 7

// return the top item (without removal)
top = queue.peep(); // returns 7

// get queue length
queue.length; // returns 3

// create a priority queue from an existing array (modifies the array)
queue = new TinyQueue([7, 5, 10]);

// pass a custom item comparator as a second argument
queue = new TinyQueue([{value: 5}, {value: 7}], (a, b) => a.value - b.value);

// turn a queue into a sorted array
var array = [];
while (queue.length) {
  array.push(queue.pop());
}
  • 性能测试
import TinyQueue from 'ray-visutil/lib/polylabel/TinyQueue';

const N = 1000000;

const data = [];
for (let i = 0; i < N; i++) data[i] = {value: Math.random()};

const q = new TinyQueue(null, compare);

function compare(a, b) {
    return a.value - b.value;
}

console.time(`push ${N}`);
for (let i = 0; i < 1000000; i++) q.push(data[i]);
console.timeEnd(`push ${N}`);

console.time(`pop ${N}`);
for (let i = 0; i < 1000000; i++) q.pop();
console.timeEnd(`pop ${N}`);

Store

since 2019-01-14 v1.0.10

indexedDB 使用

还可以使用另一个比较好的开源库 localforage

中文文档库

import Store from 'ray-visutil/lib/Store';

const store = new Store({ name: 'ray-3dobj', storeName: 'models' });

store.init(() => {

});

const data = {};

store.set(data, () => {});
store.get((data) => {

});
store.clear();

tools

  • isArray(val: any): boolean;
  • isObject(val: any): boolean;
  • isString(val: any): boolean;
  • isNumber(val: any): boolean;
  • isFunction(val: any): boolean;
  • isRegExp(val: any): boolean;
  • isDom(val: any): boolean;
  • isNull(val: any): boolean; 判断 undefined 和 null
  • isBlank(val: any): boolean; 判断空白
  • isEmptyObj(val: any): boolean;
  • isEmptyArray(val: any): boolean;
  • clone(o: any): any; 克隆数据, 采用 JSON.parse 实现
  • merge(target: any, source: any, overwrite: boolean): any;
  • hasSameProperties(o1: Object, o2: Object): boolean;
  • parseValue(o: any, defaultValue: any): any; 解析数据,当 o 为空时,则取默认值,反之则是 o
  • omit(obj: Object, fields: String | String[]): Object; 删除不需要的属性
import { tools } from 'ray-visutil';

// or import tools from 'ray-visutil/lib/tools';

str2html

将 string 转化为 html

import str2html from 'ray-visutil/lib/str2html';

var ss = str2html('<li>aaaa</li>');
var ss2 = str2html('<li>aaaa</li><li>aaaa</li><li>aaaa</li>');
var ss3 = str2html('<ul><li>aaaa</li><li>aaaa</li><li>aaaa</li></ul>');

// [li]
// [li, li, li]
// [ul]

Pivot

@since 1.1.0

Pivot basic use

import Pivot from 'ray-visutil/lib/pivot';

//custom object that dispatch a `started` pivot
const myObject = {
  changed : new Pivot(),
  ended : new Pivot()
};
function onChange(param1, param2){
  alert(param1 + param2);
}

myObject.changed.add(onStarted); //add listener
myObject.changed.dispatch('foo', 'bar'); //dispatch pivot passing custom parameters
myObject.changed.remove(onStarted); //remove a single listener

Pivot Multiple Listeners

function onStop(){
  alert('ended');
}

function onStop2(){
  alert('ended listener 2');
}

myObject.ended.add(onStop);
myObject.ended.add(onStop2);
myObject.ended.dispatch();
myObject.ended.removeAll(); //remove all listeners of the `ended` pivot

Stop/Halt Propagation (method 1)

myObject.changed.add(function(){
  myObject.changed.halt(); //prevent next listeners on the queue from being executed
  // return false; //if handler returns `false` will also stop propagation
});
myObject.changed.add(function(){
  alert('second listener'); //won't be called since first listener stops propagation
});
myObject.changed.dispatch();

Set execution context of the listener handler

var foo = 'bar';
var obj = {
  foo : 10
};

function handler1(){
  alert(this.foo);
}
function handler2(){
  alert(this.foo);
}
//note that you cannot add the same handler twice to the same pivot without removing it first
myObject.changed.add(handler1); //default execution context
myObject.changed.add(handler2, obj); //set a different execution context
myObject.changed.dispatch(); //first handler will alert "bar", second will alert "10".

Set listener priority/order

var handler1 = function(){
  alert('foo');
};
var handler2 = function(){
  alert('bar');
};
myObject.changed.add(handler1); //default priority is 0
myObject.changed.add(handler2, null, 2); //setting priority to 2 will make `handler2` execute before `handler1`
myObject.changed.dispatch(); //will alert "bar" than "foo"

GIF

@since v1.1.1

import GIF from 'ray-visutil/lib/gif';

function loadGIF(url, renderGIF){
  const oReq = new window.XMLHttpRequest();
  oReq.open('GET', url, true);
  oReq.responseType = 'arraybuffer';

  oReq.onload = function(oEvent) {
    var arrayBuffer = oReq.response; // Note: not oReq.responseText
    if (arrayBuffer) {
      gif = new GIF(arrayBuffer);
      var frames = gif.decompressFrames(true);
      console.log(gif);
      // render the gif
      renderGIF(frames);
    }
  };
  oReq.send(null);
}

let loadedFrames;
let frameIndex;

loadGIF('/example/res/car.gif', frames => {
  loadedFrames = frames;
  // user canvas
  var c = document.getElementById('mycanvas');
  var ctx = c.getContext('2d');
  // gif patch canvas
  var tempCanvas = document.createElement('canvas');
  var tempCtx = tempCanvas.getContext('2d');
  // full gif canvas
  var gifCanvas = document.createElement('canvas');
  var gifCtx = gifCanvas.getContext('2d');

  c.width = frames[0].dims.width;
  c.height = frames[0].dims.height;

  gifCanvas.width = c.width;
  gifCanvas.height = c.height;

  renderFrame();
});

function renderFrame() {
  // get the frame
  var frame = loadedFrames[frameIndex];

  var start = new Date().getTime();

  gifCtx.clearRect(0, 0, c.width, c.height);

  // draw the patch
  drawPatch(frame);

  // perform manipulation
  manipulate();

  // update the frame index
  frameIndex++;
  if (frameIndex >= loadedFrames.length) {
    frameIndex = 0;
  }

  var end = new Date().getTime();
  var diff = end - start;

  if (playing) {
    // delay the next gif frame
    setTimeout(() => {
      requestAnimationFrame(renderFrame);
      //renderFrame();
    }, Math.max(0, Math.floor(frame.delay - diff)));
  }
}

function drawPatch(frame) {
  var dims = frame.dims;

  if (!frameImageData || dims.width !== frameImageData.width || dims.height !== frameImageData.height) {
    tempCanvas.width = dims.width;
    tempCanvas.height = dims.height;
    frameImageData = tempCtx.createImageData(dims.width, dims.height);
  }

  // set the patch data as an override
  frameImageData.data.set(frame.patch);

  // draw the patch back over the canvas
  tempCtx.putImageData(frameImageData, 0, 0);

  gifCtx.drawImage(tempCanvas, dims.left, dims.top);
}

ImageProcessor

@since v1.1.2

图片处理 颜色校正、高斯模糊

createInstance: 创建 canvas instance,包含核心方法或属性: texture, draw, update, replace, contents, getPixelArray

挂载对象 $_$: { gl, isInitialized, texture, spareTexture, flippedShader }

使用完整版 processor

import imgProcessor from 'ray-visutil/lib/imageProcessor/processorAll';

function createImg(){
  const img = new window.Image(300, 300);
  img.src = '/example/res/landscape.jpg';
  img.onload = imgOnload;
  return img;
}

function imgOnload(){
  const canvas = imgProcessor.canvas();
  const texture = canvas.texture(this);
  canvas.draw(texture).triangleBlur(50).update();

  // replace the image with the canvas
  document.body.appendChild(canvas);
}

createImg();

使用自定义滤镜

import { createInstance, wrap } from 'ray-visutil/lib/imageProcessor';

import triangleBlur from 'ray-visutil/lib/imageProcessor/filters/blur/triangleblur';

const imgProcessor = {
  canvas(){
    const canvas = createInstance();
    canvas.triangleBlur = wrap(triangleBlur);
    // 全局查看
    window.$$canvas = canvas;
    return canvas;
  }
};

// 使用 imgProcessor

screenLog

显示 log 内容。会将 console 日志打印在界面上

import screenLog from 'ray-visutil/screenLog';

// 初始化
screenLog.init('screen-log', { autoScroll: false });

screenLog.log('String: Hello world');
screenLog.log(`Numbers: ${124}`);
screenLog.log(21, 'multiple arguments');
screenLog.log('Arrays', [1, 2, 3]);
screenLog.log('Objects', { a: 3 });

console.log('console.log also gets logged.');

var i = 10;
function log() {
  console.log('Future log', Date.now());
  console.error('Future error', Date.now());
  console.warn('Future warn', Date.now());
  console.info('Future info', Date.now());
  if (--i) {
    setTimeout(log, 1000);
  }
}
log();

screenlog options 说明

  • init(parent, options) parent: 需要绑定的父节点,可以是 id选择器、或者具体的 DOM。
// 默认 options
const options = {
  bgColor: '#232323',
  logColor: 'lightgreen',
  infoColor: 'blue',
  warnColor: 'orange',
  errorColor: 'red',
  fontSize: '10px',
  freeConsole: false,
  css: `
    z-index:99;
    font-family:Helvetica,Arial,sans-serif;
    padding:5px;
    text-align:left;
    opacity:0.8;
    position:absolute;
    bottom:0;
    width:100%;
    height:100%;
    font-weight:bold;
    overflow:auto;
    box-sizing: border-box;
  `,
  autoScroll: true
};
  • log(...args) 通用日志,类似于 console.log
  • info(...args) 信息类型,类似于 console.info
  • warn(...args) 警告类型,类似于 console.warn
  • error(...args) 错误类型,类似于 console.error
  • clear() 清空日志
  • destroy() 销毁

Heatmap

[email protected]

热力图

直接使用 Heatmap

import Heatmap from 'ray-visutil/lib/heatmap';

const hm = new Heatmap({
  mapSize: 256,
  width: 12, // 宽度 单位米
  height: 8, // 长度 单位米
  minValue: 10,
  maxValue: 25,
  radius: 1, // 单个点的热力影响半径
  transparent: false // 未插值区域是否透明(默认为 false )
});

// 更新热图数值
setInterval(() => {
  // 数据格式为
  // [x坐标,y坐标,热力值]
  // 坐标系以热力图平面中心为原点
  hm.randomData();
}, 1000);


// document.body.appendChild(hm.getCanvas());
document.body.appendChild(hm.canvas);

使用 BaseHeatmap

import { BaseHeatmap } from 'ray-visutil/lib/heatmap';

function randomFloat(min, max) {
  return Math.random() * (max - min) + min;
}

function randomData(){
  return [
    [-0.91, -1.07, randomFloat(24.0, 28.0)],
    [-2.11, -1.07, randomFloat(24.0, 28.0)],
    [0, 0, randomFloat(24.0, 28.0)],
    [-2.71, -1.07, randomFloat(24.0, 28.0)],
    [-0.86, 1.13, randomFloat(24.0, 28.0)],
    [-0.31, -1.07, randomFloat(24.0, 28.0)],
    [2, -2, randomFloat(24.0, 28.0)],
    [3, 3, randomFloat(24.0, 28.0)]
  ];
}

class HM {
  constructor(param) {
    this._shm = new BaseHeatmap();
    this._params = {};
    this.initParams(param);
  }

  initParams(param = {}) {
    const params = this._params;
    const { width = 10, height = 10, mapSize = 256, transparent, gradient, minValue = 10, maxValue = 50, radius = 0.8, blur = 0.8, data = [] } = param;

    params.areaWidth = width; // area width
    params.areaHeight = height;
    params.mapSize = mapSize;
    params.transparent = !!transparent;
    params.gradient = gradient || {
      0.4: 'blue',
      0.6: 'cyan',
      0.7: 'lime',
      0.8: 'yellow',
      1.0: 'red'
    };

    params.minValue = minValue;
    params.maxValue = maxValue;
    params.radius = radius;
    params.blur = blur;
    params.data = data;

    this._refresh();
  }

  setData(data) {
    this._params.data = data;
    this._refresh();
  }

  _refresh() {
    const params = this._params;

    let rateX = 1;
    let rateY = 1;
    let rateRadius = params.mapSize / params.areaWidth;
    if (params.areaWidth !== params.areaHeight) {
      if (params.areaWidth > params.areaHeight) {
        rateX = 1;
        rateY = params.areaHeight / params.areaWidth;
      } else {
        rateX = params.areaWidth / params.areaHeight;
        rateY = 1;

        rateRadius = params.mapSize / params.areaHeight;
      }
    }

    let scaler = 1;
    if (params.radius * rateRadius < 1) {
      scaler = (1 / params.radius) * rateRadius + 0.0001;
    }

    const data = params.data.map(item => {
      return [item[0] * scaler, item[1] * scaler, item[2]];
    });

    this._shm
      .setCanvasSize(Math.floor(params.mapSize * rateX), Math.floor(params.mapSize * rateY))
      .setAreaSize(params.areaWidth * scaler, params.areaHeight * scaler)
      .setMin(params.minValue)
      .setMax(params.maxValue)
      .setTransparent(params.transparent)
      .setGradient(params.gradient)
      .setRadius(Math.floor(params.radius * rateRadius * scaler), Math.floor(params.blur * rateRadius * scaler))
      .data(data)
      .draw();
  }

  get canvas(){
    return this._shm.getCanvas();
  }
}

// 创建热图
const heatMap01 = new HM({
  mapSize: 512,
  width: 12, // 宽度 单位米
  height: 8, // 长度 单位米
  minValue: 10,
  maxValue: 25,
  radius: 1, // 单个点的热力影响半径
  transparent: false // 未插值区域是否透明(默认为 false )
});

// 更新热图数值
setInterval(() => {
  const data = randomData();

  heatMap01.setData(data);
}, 1000);

document.body.appendChild(hm.canvas);

UndoPool

前进/后退 池

直接使用:

const undoPool = new UndoPool();
undoPool.add({
  undo() {
    // ...
  },
  redo() {
    // ...
  }
});

使用案例

const undoPool = new UndoPool();
const people = {};

function addPerson(id, name) {
  people[id] = name;
};

function removePerson(id) {
  delete people[id];
};

function createPerson(id, name) {
  // first creation
  addPerson(id, name);

  // make undo-able
  undoPool.add({
    undo() {
      removePerson(id)
    },
    redo() {
      addPerson(id, name);
    }
  });
}

createPerson(101, "John");
createPerson(102, "Mary");

console.log("people", people); // {101: "John", 102: "Mary"}

undoPool.undo();
console.log("people", people); // {101: "John"}

undoPool.undo();
console.log("people", people); // {}

undoPool.redo();
console.log("people", people); // {101: "John"}

UndoPool methods

  • new UndoPool($limit?: Number, cb?: Function): create undopool.
  • undoPool.undo();: Performs the undo action.
  • undoPool.redo();: Performs the redo action.
  • undoPool.clear();: Clears all stored states.
  • undoPool.setLimit(limit);: Set the maximum number of undo steps. Default: 0 (unlimited).
  • var hasUndo = undoPool.hasUndo();: Tests if any undo actions exist.
  • var hasRedo = undoPool.hasRedo();: Tests if any redo actions exist.
  • undoPool.setCallback(myCallback);: Get notified on changes.
  • var index = undoPool.getIndex();: Returns the index of the actions list.

toImage

  • imageUtils
  • toVizBlob(node, options)
  • base64DataToBlob(data)
  • base64DataToFile(data, filename)

imageUtils

  • toSvg(node, options)
  • toPng(node, options)
  • toJpeg(node, options)
  • toBlob(node, options)
  • toPixelData(node, options)

options:

  • filter: func 过滤node节点,如 (node) => node.tagName !== 'i'
  • bgcolor: background color, valid CSS color value.
  • height, width: 设置 node 的宽高
  • twidth, theight: 设置裁剪后自定义宽高
  • style: 设置 node 样式
  • quality: JPEG 时,设置 quality, 0 and 1,(e.g. 0.92 => 92%)
  • cacheBust: 启用缓存
  • imagePlaceholder: 获取图像失败时的默认url

cached 使用教程

import registerCache from 'ray-visutil/lib/LoaderCache';

/** 启用 cached,默认会拦截所有的 XMLHttpRequest */
// 注意: 该方法仅执行一次,确保在入口处执行即可
registerCache({
  dbName: '__br3d',
  onRequest(url){
    let extension = '';
    if (url){
      const strs = url.split('.');
      if (strs.length > 1){
        const ext = strs.pop();
        const index = ext.indexOf('?');
        extension = index !== -1 ? ext.substring(0, index) : ext;
      }
    }
    return ['glb', 'gltf', 'bin', 'json', 'jpg', 'png', 'obj', 'mtl'].includes(extension.toLowerCase());
  }
});

changelog

  • 2024-04-30 v1.3.17

    add Clock and LibGif

  • 2024-04-23 v1.3.15

    add Svg Convert

  • 2024-02-23 v1.3.14

    fix resize bugs

  • 2024-01-15 v1.3.13

    修复 typing 数据中断无法继续执行问题

  • 2021-12-22 v1.3.7

    修复 imageUtils 无法截取 svg use symbol 问题

  • 2021-5-7 v1.3.1

    add toos#utf8ArrayToStr

  • 2021-2-23 v1.2.0

    add memorize and memoFastStringify

  • 2021-2-23 v1.1.15

    add UndoPoll ts support and UndoPool($limit?: Number, cb?: Function) constructor

  • 2020-8-12 v1.1.12

    add imageUtils

  • 2020-7-7 v1.1.10

    add QRTypes

  • 2020-7-6 v1.1.9

    add VisCache

  • 2020-5-25 v1.1.8

    add fastStringify

  • 2020-5-9 v1.1.7

    add uuid、shortid

  • 2020-5-8 v1.1.6

    add UndoPool

  • 2020-4-27 v1.1.5

    modify tools

  • 2019-12-31 v1.1.3

    add screen log

  • 2019-12-9 v1.1.2

    add imageProcessor

  • 2018-12-19 v1.0.9

    add tools

  • 2018-12-18 v1.0.8

    add Transition

License

MIT