ray-visutil

vis utils

Usage no npm install needed!

<script type="module">
  import rayVisutil from 'https://cdn.skypack.dev/ray-visutil';
</script>

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

since@1.1.4

热力图

直接使用 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

  • 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

changelog

  • 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