Pixi.js为线段添加交互能力

Pixi.js中线段需要使用 PIXI.Graphics 进行绘制。而由于某些因素,Pixi.js在使用Graphics绘制线段时并未对线段添加hitArea,因此即使启用该Graphics对象interactive属性并添加事件监听器也无法起作用。

如果想为线段添加事件则需要为该线段所属Graphics对象添加hitArea,而一些比较简单的使用场景下只需要使用一个PIXI.Rectangle将整个线段覆盖即可,但对于一些比较精细的事件监听则需要使用线段所属PIXI.Graphics.geometry 中的points构建一个polygon,而后hitArea指向该多边形。

PIXI.Graphics.geometry是Graphics进行图形绘制时存储的几何数据模型,Grapphics在渲染时通过该模型进行点计算和渲染。而渲染计算后的图形顶点集则存储在geometry.points中(geometry.points只有在经过renderer渲染后才会存在)。

我们的目标是为线段添加交互能力。理所应当的,线段所属的Graphics仅用于该线段的绘制,而不包含其他图形绘制。这样Graphics.geometry中就只包含了绘制该线段所需要的几何模型,而渲染出来的geometry.points中也只包含了线段顶点。

我们需要知道的是,在计算机图形渲染中,由于效率等原因,几乎所有的图形都是又由一个个小三角形构成,无论是圆、直线还是方形。有了这个基础我们就可以理解为什么geometry.points中的点并不是沿着线段边沿顶点围绕一周分布,而是从起始点开始左右交替直到线段终点。我们可以简单写一个辅助线验证一下:

辅助线绘制
渲染结果

可以看到points列表中点分布是线段左右依次分布,而我们需要的实际上是:

我们所需的points顺序,用于创建hitArea Polygon

因此我们只需要写一个转换points列表的函数即可,函数也很简单:

function lineToPolygon(points) {
  //the number of points
	const numPoints = points.length / 2;
  //the last points' array index
  const pointsLastIndex = points.length-1;
  //how many pairs in the geometry
  const pair = numPoints/2;
  //result
  const output = new Array(points.length);
  for (let x = 0; x < pair; x++) {
    const i = x*4;
    //fulfill start side's points
    output[x*2] = points[i];
    output[x*2+1] = points[i+1];
    //fulfill opposite side's points
    output[pointsLastIndex-x*2-1]=points[i+2];
    output[pointsLastIndex-x*2]=points[i+3];
  }
  //close path
  output.push(output[0], output[1]);
  //gen new polygon
  return new PIXI.Polygon(output);
}

而使用也很简单,在进行线段绘制的时候调用Graphics的render并传入Stage的renderer以立即渲染获得渲染后的points列表,然后将该函数创建的polygon设置为graphics的hitArea即可:

graphics.current.render(r as PIXI.Renderer);
graphics.current.interactive = true;
const shape = lineToPolygon(hitAreaWith,graphics.current.geometry.points);
graphics.current.hitArea = shape;

此处在我的应用场景下有一个优化点,我的线段使用GreenSock库制作了一些动画效果,如果在每个Graphics每次图形绘制中都单独调用render会有一定性能亏损,而我并不需要在执行动画的过程中对该线段进行交互,因此我设置了一个isAnimating状态,标志是否Graphics处于动画执行状态。只有isAnimating变更为false时,才执行hitArea填充(代码使用React):

useEffect(()=>{
    if(!isAnimating){
      graphics.current.render(r as PIXI.Renderer);
      graphics.current.interactive = true;
      const shape = lineToPolygon(hitAreaWith,graphics.current.geometry.points);
      graphics.current.hitArea = shape;
    }
    else{
      graphics.current.interactive=false;
    }
  },[isAnimating]);

这样就可以减少计算和设置hitArea带来的性能损耗。


v1.0 wep 2022/01/12 文章基本内容

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注