Canvas


红宝书关于Canvas

绘制图形需要先取得绘图上下文

let drawing = document.getElementByTagName('canvas');
if(drawing.getContext) {
    //...
}

2D绘图

drawText2D上下文的坐标原点(0,0)在canvas标签的左上角,x向右增长,y向下增长

矩形

fillRectstrokeRectclearRect接收4个参数,分别为x坐标、y坐标、矩形宽度、高度

fillRect: 绘制矩形 + 填充

strokeRect: 绘制非实星填充

clearRect : 清除指定的矩形区域

if(drawing.getContext) {
    let context = drawing.getContext("2d");
    context.fillStyle = "#ff000";
    context.fillRect(10, 10, 50, 50)
}

clearRect方法可以擦除画布中某个区域,让其变得透明

路径

在绘制路径之前,需要“创建路径”,结束之后再“描画路径”

  • beginPath:创建路径(线条)
  • stroke:画路径
if(drawing.getContext) {
    let context = drawing.getContext("2d");
    context.beginPath(); //创建路径
    //..
    context.stroke();//描画路径
}

方法一:画初始点和末尾点

ctx.beginPath(); //我要开始画画了
ctx.strokeStyle = 'blue';
ctx.moveTo(80,40);  //路径先初始点的坐标
ctx.lineTo(200,40); //路径先末尾点的坐标
ctx.stroke(); //我画完了

方法二:当然也可以通过画圆的方式来画

arc(x, y, radius, startAngle, endAngle, counterclockwise)

  • x,y坐标为圆心
  • radius为半径,startAngle开始角度,endAngle结束角度
  • counterclockwise是否逆时针计算
ctx.arc(x, y, 30, 0, 2 * Math.PI); //画一个圆
ctx.arc(x, y, 30, 0, Math.PI); //画一个半圆

方法三:画一个弧

arcTo(x1,y1,x2,y2,r);

参数 描述
x1 两切线交点的横坐标。
y1 两切线交点的纵坐标。
x2 第二条切线上一点的横坐标。
y2 第二条切线上一点的纵坐标。
r 弧的半径。

变换

当然我们也可以在其中使用我们熟悉的老朋友

  • rorate(angle)
  • scale(scaleX, scaleY)
  • translate(x, y),执行这个操作之后,原本我们要仪仗的(0, 0)的原点坐标,变成了(x, y)

所有的变换,包括fillStyle、strokeStyle属性,都会一直保留在上下文中,直到再次修改他们。虽然没有办法明确地将所有之都重置为默认值,但是有两个方法可以帮我们跟踪变化:save()被调用之后,,当前时刻的所有设置会放到一个暂存栈中,之后调用 restore()方法可以取出并恢复之前保存的设置。

记住保存的是上下文的设置和变换,并没有保存上下文的内容。

绘制图像

或者可以使用 canvas.toBlob

let img = document.imges[0];
context.drawImage(img, 10, 10)

drawImage(图像, 绘制目标的x坐标, 绘制目标的y坐标)

drawImage(图像, 绘制目标的x坐标, 绘制目标的y坐标, 目标宽度, 目标高度)

toDataURL 转换图片为dataURL,一般使用为:

canvas.getContext('2d').drawImage(img, 0, 0, width, height)
const dataUrl = canvas.toDataURL('image/png');

(1)usage

在画布上定位图像:

JavaScript 语法:
context.drawImage(img,x,y); 在画布上定位图像
context.drawImage(img,x,y,width,height); 在画布上定位图像,并规定图像的宽度和高度:
context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height); 剪切图像,并在画布上定位被剪切的部分

参数值

参数 描述
img 规定要使用的图像、画布或视频。
sx 可选。开始剪切的 x 坐标位置。
sy 可选。开始剪切的 y 坐标位置。
swidth 可选。被剪切图像的宽度。
sheight 可选。被剪切图像的高度。
x 在画布上放置图像的 x 坐标位置。
y 在画布上放置图像的 y 坐标位置。
width 可选。要使用的图像的宽度(伸展或缩小图像)。
height 可选。要使用的图像的高度(伸展或缩小图像)。

(2)转化成图像

或者将当前canvas转化成一个图像

<canvas id="canvas" width="5" height="5"></canvas>

Copy to Clipboard

可以用这样的方式获取一个 data-URL

var canvas = document.getElementById("canvas");
var dataURL = canvas.toDataURL();
console.log(dataURL);
// "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNby
// blAAAADElEQVQImWNgoBMAAABpAAFEI8ARAAAAAElFTkSuQmCC"

或许你想要获取的是图像的帧数据

const imageData = ctx.getImageData(0, 0, width, height);
const pixelData = new Uint8Array(imageData.data.buffer);

此时还能用upng进行压缩

import UPNG from 'upng-js';
//....
const pngData = UPNG.encode([pixelData], width * scale, height * scale, 256);

(3)压缩图片质量

var base64 = canvas.toDataURL('image/jpeg', quality);

type 可选 图片格式,默认为 image/png

encoderOptions 可选 在指定图片格式为 image/jpeg 或 image/webp的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。 Chrome支持“image/webp”类型。

(4)获取图像信息

CanvasRenderingContext2D.getImageData() 返回一个ImageData对象,用来描述 canvas 区域隐含的像素数据,这个区域通过矩形表示,起始点为(sx, sy)、宽为sw、高为sh。

语法

ctx.getImageData(sx, sy, sw, sh);

Copy to Clipboard

参数

  • sx

    将要被提取的图像数据矩形区域的左上角 x 坐标。

  • sy

    将要被提取的图像数据矩形区域的左上角 y 坐标。

  • sw

    将要被提取的图像数据矩形区域的宽度。

  • sh

    将要被提取的图像数据矩形区域的高度。

(5)画一个圆并且塞入图像

ctx.save();
ctx.beginPath();
ctx.arc(x + radius, y + radius, radius, 0, 2 * Math.PI); // 画一个圆
ctx.closePath();
ctx.clip();

ctx.drawImage(userAvator, x, y, radius * 2, radius * 2);

(6)canvas在高清屏幕的绘制

由于 canvas 不是矢量图,而是像图片一样是位图模式的。高 dpi 显示设备意味着每平方英寸有更多的像素。也就是说二倍屏,浏览器就会以2个像素点的宽度来渲染一个像素,该 canvas 在 Retina 屏幕下相当于占据了2倍的空间,相当于图片被放大了一倍,因此绘制出来的图片文字等会变模糊。

我们可以先把所有的宽高比都设置成 * dpr

比如设置canvas宽高(style)为 320px × 400px,则

const dpr = window.devicePixelRatio || 2;
export const MODAL_WIDTH = 320;
export const MODAL_HEIGHT = 400;

myCanvas.style.width = MODAL_WIDTH + 'px';
myCanvas.style.height = MODAL_HEIGHT + 'px';

myCanvas.width = MODAL_WIDTH * ratio;
myCanvas.height = MODAL_HEIGHT * ratio;

绘制内部:

方法1:

const context = myCanvas.getContext('2d');
context.font = "36px Georgia"; //一倍屏下18px字体
context.fillStyle = "#999";
context.fillText("我是清晰的文字", 50*ratio, 50*ratio);// 坐标位置乘以像素比

方法2:

const context = myCanvas.getContext('2d');
context.scale(ratio, ratio);
context.font = "18px Georgia";
context.fillStyle = "#999";
context.fillText("我是清晰的文字", 50, 50);

小实战

画一个刮刮乐

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      #ggk {
        width: 400px;
        height: 100px;
        position: relative;
        left: 50%;
        transform: translate(-50%, 0);
      }

      .jp,
      canvas {
        position: absolute;
        width: 400px;
        height: 100px;
        left: 0;
        top: 0;
        text-align: center;
        font-size: 25px;
        line-height: 100px;
        color: deeppink;
      }
    </style>
  </head>
  <body>
    <h1 style="text-align: center">刮刮乐</h1>
    <div id="ggk">
      <div class="jp">不抽大嘴巴子</div>
      <canvas id="canvas" width="400" height="100"></canvas>
      <script>
        document.addEventListener("selectstart", function (e) {
          e.preventDefault();
        });
        let canvas = document.querySelector("#canvas");
        let ctx = canvas.getContext("2d");
        ctx.fillStyle = "darkgray";
        ctx.fillRect(0, 0, 400, 100);
        let ggkDom = document.querySelector("#ggk");
        let jp = document.querySelector(".jp");
        let isDraw = false;
        canvas.onmousedown = function () {
          isDraw = true;
        };
        canvas.onmousemove = function (e) {
          if (isDraw) {
            let x = e.pageX - ggkDom.offsetLeft + ggkDom.offsetWidth / 2;
            console.log(e.pageX, 'pageX', ggkDom.offsetLeft, 'ggkDom.offsetLeft', ggkDom.offsetWidth / 2, 'ggkDom.offsetWidth / 2');
            let y = e.pageY - ggkDom.offsetTop;
            ctx.beginPath();
            ctx.arc(x, y, 30, 0, 2 * Math.PI);
            ctx.globalCompositeOperation = "destination-out";
            ctx.fill();
            ctx.closePath();
          }
        };
        document.onmouseup = function () {
          isDraw = false;
        };
      </script>
    </div>
  </body>
</html>
  • globalCompositeOperation:合成属性

    Canvas 2D API 的 Canvas.globalCompositeOperation 属性设置要在绘制新形状时应用的合成操作的类型,其中 type 是用于标识要使用的合成或混合模式操作的字符串。

    它的每个属性值在官网里都有图文介绍:https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation

    当然,在w3c网上面也有介绍:https://www.runoob.com/w3cnote/html5-canvas-intro.html

    • source-over: 旧画布图形直接覆盖新的图形
    • source-in: 仅会出现旧画布图形和新的图形重叠的部分(展示新图像),有点像&&的感觉
    • source-out: 仅显示新图形部分,且仅仅新图形与老图形没有重叠的部分,其余部分全部透明
    • destination-over: 新图像会在老图像的下面。
    • destination-in: 仅会出现旧画布图形和新的图形重叠的部分(展示老图像),有点像&&的感觉
    • destination-out: 仅显示老图形部分,且仅仅老图形与新图形没有重叠的部分,其余部分全部透明
  • Canvas.fill() 是 Canvas 2D API 根据当前的填充样式,填充当前或已存在的路径的方法。采取非零环绕或者奇偶环绕规则。

  • Canvas.closePath() 是 Canvas 2D API 将笔点返回到当前子路径起始点的方法。它尝试从当前点到起始点绘制一条直线。如果图形已经是封闭的或者只有一个点,那么此方法不会做任何操作。

其他小案例

取自菜鸟教程

一个小太阳、一个是小时钟案例

https://www.runoob.com/w3cnote/html5-canvas-intro.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
<style>
    body {
        padding: 0;
        margin: 0;
        background-color: rgba(0, 0, 0, 0.1)
    }

    canvas {
        display: block;
        margin: 200px auto;
    }
</style>
</head>
<body>
<canvas id="solar" width="300" height="300"></canvas>
<script>
init();

function init(){
    let canvas = document.querySelector("#solar");
    let ctx = canvas.getContext("2d");
    draw(ctx);
}

function draw(ctx){
    requestAnimationFrame(function step(){
        drawDial(ctx); //绘制表盘
        drawAllHands(ctx); //绘制时分秒针
        requestAnimationFrame(step);
    });
}
/*绘制时分秒针*/
function drawAllHands(ctx){
    let time = new Date();

    let s = time.getSeconds();
    let m = time.getMinutes();
    let h = time.getHours();

    let pi = Math.PI;
    let secondAngle = pi / 180 * 6 * s;  //计算出来s针的弧度
    let minuteAngle = pi / 180 * 6 * m + secondAngle / 60;  //计算出来分针的弧度
    let hourAngle = pi / 180 * 30 * h + minuteAngle / 12;  //计算出来时针的弧度

    drawHand(hourAngle, 60, 6, "red", ctx);  //绘制时针
    drawHand(minuteAngle, 106, 4, "green", ctx);  //绘制分针
    drawHand(secondAngle, 129, 2, "blue", ctx);  //绘制秒针
}
/*绘制时针、或分针、或秒针
 * 参数1:要绘制的针的角度
 * 参数2:要绘制的针的长度
 * 参数3:要绘制的针的宽度
 * 参数4:要绘制的针的颜色
 * 参数4:ctx
 * */
function drawHand(angle, len, width, color, ctx){
    ctx.save();
    ctx.translate(150, 150); //把坐标轴的远点平移到原来的中心
    ctx.rotate(-Math.PI / 2 + angle);  //旋转坐标轴。 x轴就是针的角度
    ctx.beginPath();
    ctx.moveTo(-4, 0);
    ctx.lineTo(len, 0);  // 沿着x轴绘制针
    ctx.lineWidth = width;
    ctx.strokeStyle = color;
    ctx.lineCap = "round";
    ctx.stroke();
    ctx.closePath();
    ctx.restore();
}

/*绘制表盘*/
function drawDial(ctx){
    let pi = Math.PI;

    ctx.clearRect(0, 0, 300, 300); //清除所有内容
    ctx.save();

    ctx.translate(150, 150); //一定坐标原点到原来的中心
    ctx.beginPath();
    ctx.arc(0, 0, 148, 0, 2 * pi); //绘制圆周
    ctx.stroke();
    ctx.closePath();

    for (let i = 0; i < 60; i++){//绘制刻度。
        ctx.save();
        ctx.rotate(-pi / 2 + i * pi / 30);  //旋转坐标轴。坐标轴x的正方形从 向上开始算起
        ctx.beginPath();
        ctx.moveTo(110, 0);
        ctx.lineTo(140, 0);
        ctx.lineWidth = i % 5 ? 2 : 4;
        ctx.strokeStyle = i % 5 ? "blue" : "red";
        ctx.stroke();
        ctx.closePath();
        ctx.restore();
    }
    ctx.restore();
}
</script>
</body>
</html>

其中用到了save方法

let canvas = document.querySelector("#solar");
let ctx = canvas.getContext("2d");
ctx.save(); // Canvas 2D API 通过将当前状态放入栈中,保存 canvas 全部状态的方法;你可以理解为每一次绘制完一个针后记录下来,存到栈中,然后restore的时候返回初始状态,不影响上一个针的绘制效果
ctx.restore(); //回到save之前的状态

可以看一下这里的mdn例子加深下理解:https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/save

还有一个

ctx.clearRect(0, 0, 300, 300)

CanvasRenderingContext2D.clearRect()是 Canvas 2D API 的方法,这个方法通过把像素设置为透明以达到擦除一个矩形区域的目的。

备注: 如果没有依照 绘制路径 的步骤,使用 clearRect() 会导致意想之外的结果。请确保在调用 clearRect()之后绘制新内容前调用beginPath()

可以理解为一个命令行中 clear的操作

参考:

使用Canvas制作刮刮乐,看看你能刮出什么奖品来?

pixijs

1.pixi基础

使用 PixiJS ,我们首先应该创建一个 Pixi 应用,并且添加到当前的节点处

// 创建一个Pixi 应用
let app = new PIXI.Application({width: 256, height: 256});
// 把 Pixi 应用中创建出来的 canvas 添加到页面上
document.body.appendChild(app.view);

容器(container)
容器是用来装载多个显示对象的, 它可以用 PIXI.Container() 方法来创建,可以把它看成一个小组件,里面可以包裹多个精灵,或者其他容器等

精灵(spirite)
精灵是可以放在容器里的特殊图像对象, 它可以用 PIXI.Sprite() 方法来创建。精灵是你能用代码控制图像的基础。你能够控制他们的位置,大小,和许多其他有用的属性来产生交互和动画,我自己感觉spirite对于pixi就像dom对于document一样

纹理(Texture)

因为 Pixi 用 WebGL 和 GPU 去渲染图像,所以图像需要转化成 GPU 可以处理的格式。可以被 GPU 处理的图像被称作 纹理 。在你让精灵显示图片之前,需要将普通的图片转化成 WebGL 纹理。

为了让所有工作执行的快速有效率,Pixi会自动使用 纹理缓存 来存储和引用所有你的精灵需要的纹理。纹理的名称字符串就是图像的地址。这意味着如果你有从"images/cat.png" 加载的图像(注意是图像,不会把json也给你存进来),我们可以在纹理缓存中这样找到他:

PIXI.utils.TextureCache["images/logo.png"];

那该如何将它转化成纹理?那就是是用Pixi已经构建好的loader对象(下方有loader的使用),通过loader加载过的图片,都可以在PIXI.utils.TextureCache中找到

PIXI.Application 会自动选择使用 Canvas 或者是 WebGL 来渲染图形,这取决于您的浏览器支持情况

我们通常使用Pixi提供的Application方法来创建一个实例应用(app),它能自动创建我们等下讲到的renderer,ticker 和container、loader

注意:pixi从v3到v5,再到v7,关于loader的语法变动很多,大家需要注意网上学习的语法是哪个版本的,比如v5是没有的loader是没有on来监听事件的,v7甚至没有loader

Application option:

autoStart 布尔值 default 选修的构建完成后自动开始渲染。 注意:将此参数设置为 false 不会停止共享代码,即使您将 options.sharedTicker 设置为 true 以防它已经启动。自己阻止吧。
width 数字 选修的渲染器视图的宽度。
height 数字 选修的渲染器视图的高度。
view HTML画布元素 选修的用作视图的画布,可选。
transparent 布尔值 False 选修的如果渲染视图是透明的。
autoDensity 布尔值 False 选修的以 CSS 像素为单位调整渲染器视图的大小,以允许 1 以外的分辨率。
antialias 布尔值 False 选修的设置抗锯齿
preserveDrawingBuffer 布尔值 False 选修的启用绘图缓冲区保存,如果您需要在 WebGL 上下文中调用 toDataUrl,请启用此功能。
resolution 数字 1 选修的渲染器的分辨率/设备像素比,视网膜将为 2,移动端一般要根据window.devicePixelRatio 来。
forceCanvas 布尔值 False 选修的阻止选择 WebGL 渲染器,即使存在,此选项仅在使用pixi.js-legacy@pixi/canvas-renderer模块时可用,否则将被忽略。
backgroundColor 数字 0x000000 选修的渲染区域的背景颜色(如果不透明则显示)。
clearBeforeRender 布尔值 True 选修的这会设置渲染器是否会在新的渲染通道之前清除画布。
powerPreference 细绳 选修的传递给 webgl 上下文的参数,对于具有双显卡的设备设置为“高性能”。(仅限 WebGL)
sharedTicker 布尔值 False 选修的true使用 PIXI.Ticker.shared,false创建新的代码。如果设置为 false,则您无法将处理程序注册为在共享代码上运行的任何内容之前发生。系统代码将始终在共享代码和应用程序代码之前运行。
sharedLoader 布尔值 False 选修的true使用 PIXI.Loader.shared,false创建新的 Loader。
resizeTo 窗口 | HTML元素

application还有个销毁的方法:

  • (removeView,stageOptions)
removeView 布尔值 <可选> 错误的 自动从 DOM 中移除画布。
stageOptions 对象 | 布尔值 <可选> 选项参数。布尔值将表现得好像所有选项都已设置为该值
stageOptions.children 布尔值 <可选> 错误的 如果设置为 true,所有的孩子也将调用他们的 destroy 方法。’stageOptions’ 将传递给那些调用。
stageOptions.texture 布尔值 <可选> 错误的 如果 stageOptions.children 设置为 true,则仅用于子 Sprites。它是否应该破坏子精灵的纹理
stageOptions.baseTexture 布尔值 <可选> 错误的 如果 stageOptions.children 设置为 true,则仅用于子 Sprites。它是否应该破坏子精灵的基础纹理
app.destroy(true, {});

application滑动问题:

// 可滑动
app.renderer.view.style.touchAction = 'auto';
app.renderer.plugins.interaction.autoPreventDefault = false;

2.loader

类似于threejs的loader,threejs的loader种类繁多,但是pixijs的loader只有一种,是一个用于加载资源的东西,由 Chad Engler 从 Resource Loader 派生而来,用于加载资源,传入2个参数则可以设置别名

import * as PIXI from 'pixi.js';
const loader = new PIXI.Loader();
loader.add('bunny', 'data/bunny.png')
  .add('images/logo.png')
  .add('spaceship', 'assets.json')
  .add("logo", "images/logo.png")
  .load(init);

function init((loader, resources)) {
  // 以指定名稱的方式去取用 Texture Cache
  var sprite = new PIXI.Sprite(loader.resources.logo.texture);
   var sprite = new PIXI.Sprite(resources.logo.texture);
  //或者这样
  var sprite2 = new PIXI.Sprite(loader.resources['spaceship'].texture);
}

add也可以传入数组

let app = new PIXI.Application({ width: 256, height: 256 });
app.loader
  .add([
  "images/imageOne.png",
  "images/imageTwo.png",
  "images/imageThree.png"
])
  .load((loader, res) => {
  console.log(loader === app.loader) //true
});

关于loader还有一些监听事件

//可取得下載進度
loader.onProgress.add((e) => {
});
//載入檔案錯誤時
loader.onError.add((target, e, error) => {
});
//每一次加载的回调
loader.onLoad.add((e, target) => {
});
//全部加载完成后回调
loader.onComplete.add(() => {
});
V7 and loader

pixi团队一直想要删除loader,因为它的遗留方法(例如,XMLHttpRequest),是从 resource-loader 衍生出来的,Loader 最初的设计灵感主要是由 Flash/AS3 驱动的,现在看来已经过时了。我们希望从新的迭代中获得一些东西:静态加载、使用 Workers 加载、后台加载、基于 Promise、更少的缓存层。这是一个简单的例子,说明这将如何改变:

import { Loader, Sprite } from 'pixi.js';

const loader = new Loader();
loader.add('background', 'path/to/assets/background.jpg');
loader.load((loader, resources) => {
  const image = Sprite.from(resources.background.texture);
});

现在变成:

import { Assets, Sprite } from 'pixi.js';

const texture = await Assets.load('path/to/assets/background.jpg');
const image = Sprite.from(texture);
删除loader缓存资源

正常我们将资源从stage移除之后,utils.TextureCache依然会存在loader加载过的资源缓存

通过

sprite.destroy()

资源依然存在。

真正删除需要:

sprite.destroy({texture: true, baseTexture: true});

移除TextureCacheBaseTextureCache 纹理,并且会从stage消失

还有个children选项,若 children 设定为 true 時,會將 destroy() 的其他屬性如 texture: truebaseTexture: true 再傳入子物件、子孙物件

PIXI.BaseTexture
纹理存储表示图像的信息。所有纹理都有一个基础纹理。

PIXI.Texture
纹理存储表示图像或图像的一部分的信息。它不能直接添加到显示列表中。而是将其用作 Sprite 的纹理。如果没有提供框架,则使用整个图像。

PIXI.Texture 是对 PIXI.BaseTexture 的引用

sprite.destroy - 将销毁精灵,使 PIXI.Texture 和 PIXI.BaseTexture 保持不变
sprite.destroy(true); - 将破坏精灵和 PIXI.Texture;PIXI.BaseTexture 保持不变
sprite.destroy(true, true); - 将销毁精灵、PIXI.Texture 和 PIXI.BaseTexture

通常,您不希望每次调用 addSprite() 时都创建 PIXI.Texture 并在 removeSprite() 中销毁它。即使这是在 Pixi 内部处理的并且没有创建重复的纹理。
提前创建你的纹理,将它们存储到数组中并在需要时选择一个。在没有其他精灵使用它时销毁 PIXI.Texture,并在完全完成后销毁 PIXI.BaseTexture。

并且通常不太建议销毁 Texture,因为:销毁 PIXI.Texture 不会释放内存,它只会使纹理无效并将其从图像缓存中删除,因此其他精灵将无法使用它。我真的不知道什么时候必须调用它:)

销毁 PIXI.BaseTexture 会释放绑定到它的 WebGL 对象。为您使用的动态纹理或不再需要的一些静态调用它。

3.设置精灵属性

比如我上面加载了一个sprite,可以设置大小、缩放、位置

sprite.position.set(20, 20)
sprite.x = sprite.x + 10  // 可以对 x、y 的某一项单独设置

// 缩放
sprite.scale.set(num, num)
// 大小
sprite.width = sprite.width + 10
// 旋转
sprite.rotation += 0.1

mask

为 displayObject 设置掩码。蒙版是一种对象,它将对象的可见性限制为应用于它的蒙版的形状。在 PixiJS 中,常规掩码必须是 PIXI.GraphicsPIXI.Sprite对象。这允许在画布中更快地进行遮罩,因为它使用形状剪裁。

你可以直接理解为就是蒙版。裁剪的时候用到它

 import { Graphics, Sprite } from 'pixi.js';

 const graphics = new Graphics();
 graphics.beginFill(0xFF3300);
 graphics.drawRect(50, 250, 100, 100);
 graphics.endFill();

 const sprite = new Sprite(texture);
 sprite.mask = graphics;

设置文本 + 文本局中 + 气泡外框

// 创建文本对象
const text = new Text('您好,这是一条弹幕消息!', { fontSize: 16, fill: 0xffffff });
text.anchor.set(0.5);

// 计算文本的长度
const textWidth = text.width;

// 创建弹幕气泡对象
const bubble = new Graphics();
bubble.beginFill(0x000000, 0.8);
bubble.drawRoundedRect(0, 0, textWidth + 20, 30, 15);
bubble.endFill();
text.position.set(bubble.width / 2, bubble.height / 2);

4.pixijs的事件

pixijs的两种渲染模式,都不是以dom结构为基础(可以联想到canvas),所以需要使用它内置的事件:

  • pointerdown:类似于 mousedown。
  • pointerup:类似于 mouseup。
  • pointerover:类似于 mouseover。
  • pointerout:类似于 mouseout。
  • pointermove:类似于 mousemove。

现在我们给sprite添加事件

// 第一步:设置元素为可交互的    
cat.interactive = true;
// 第二部:监听对应的事件
cat.addListener('pointerdown', (e) => {
    cat.alpha = 0.5 //类似CSS3滤镜filter
});

我们也可以通过代码来触发事件

cat.emit('pointerdown')

5.pixijs绘制图形

熟悉canvas的同学都知道,canvas可以利用 fillRectmoveTolineTo等随喜所欲画一些简单的矢量图形,pixijs也不例外

其中beginFillendFill可以理解为canvas的 beginPathstroke是一个道理

//画一个矩形
let rectangle = new PIXI.Graphics();
// 外边框的颜色
rectangle.lineStyle(4, 0xFF3300, 1);
// 给矩形的内部填充颜色
rectangle.beginFill(0x66CCcc);
// 绘制矩形。它的四个参数是 x, y, width, height
rectangle.drawRect(0, 0, 64, 64);
// 结束绘制
rectangle.endFill();
// 创建一个半径为32px的圆
const circle = new Graphics();
circle.beginFill(0xfb6a8f);
circle.drawCircle(0, 0, 32); //x\y\半径
circle.endFill();

6.给容器添加元素

和threejs一样(threejs是往scene里添加东西),我们做好的元素,要往PIXI的容器(container)里面加,然后再通过,才能显示到屏幕上

app.stage是一个pixijsContainer的实例,作为最底层的舞台(stage),所有要渲染的图形都应放在它的内部

我们可以选择直接添加在pixi的app.stage上,也可以创建一个自定义的container,然后再添加到app.stage上(自定义container好处在于,比如说修改container的透明度,位于其中的子节点,都会受到影响)

// 自定义Container
const myContainer = new Container();
// 相对于根节点偏移
myContainer.position.set(40, 40);
myContainer.addChild(rectangle);
app.stage.addChild(myContainer);
document.body.appendChild(app.view);

实际上这里的app.view就是app.renderer.view,打印出来是一个canvas

7.渲染器Renderer

app.renderer是一个Renderer的实例,熟悉threejs的同学应该不陌生,如果WebGL 可以用,那么application自带的就是一个Renderer,否则为一个CanvasRenderer,它将场景及其所有内容绘制到支持 WebGL 的画布上。每当我们的元素变动,就需要重新渲染,然后我们就可以看到动画效果

renderer PIXI.Renderer | PIXI.CanvasRenderer

// 把画布重新渲染为500*500大小
app.renderer.resize(500, 500);

// 渲染一个容器
const container = new Container();
app.renderer.render(container);

通过打印renderer的type属性可以查看他的类型

console.log(app.renderer.type);
渲染模式 取值
UNKNOWN 0
WEBGL 1
CANVAS 2

改变容器(渲染器)的底色

app.renderer.backgroundColor = 0x061639;

要更改画布的大小,请使用rendererresize方法,并提供任何新的widthheight值。但是,为了确保画布的大小调整到与分辨率匹配,请将autoResize设置为true

app.renderer.autoResize = true;
app.renderer.resize(512, 512);

8.Ticker

Ticker有点类似前端的requestAnimationFrame,当然大部分情况,我们也可以用 requestAnimationFrame来替代Ticker

在pixi源码可以看到,我们在注册application的时候,application会在初始化时注入ticker插件(Application.registerPlugin(TickerPlugin);

他还会默认帮我们把autoStart干成true

// 自定义ticker
const ticker = new Ticker();
// 每次屏幕刷新重新渲染,否则只会渲染第一帧
ticker.add(() => {
  chanziAnimate.x += 1;
});
function gameLoop() {
  chanziAnimate.x += 1;
  renderer.render(stage); // 重新渲染,如果是使用application初始化,可以忽略这一行,因为application.ticker在不断执行
  requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);

尝试一下吧~https://codesandbox.io/s/elated-wind-5j60fu?file=/src/MoveDemo.js

Ticker可以实现简单的动画,但如果我们希望实现一些复杂效果,则需要自己编写很多代码,这时就可以选择一个兼容pixi的动画库。市面上比较常见的动画库有:Tween.jsTweenMax(它包括了GreenSock动画平台的大部分核心功能)

chatGPT:

总的来说,TweenMax 更加强大,而 TweenJS 更加轻量级。TweenMax 的学习曲线可能会比 TweenJS 更陡峭,但是它可以提供更多的控制和功能。如果您需要一个全面的动画库,或者需要创建复杂的动画效果,那么 TweenMax 可能是更好的选择。如果您只需要简单的 Tween 功能,并且希望使用更轻量级的库,那么 TweenJS 可能更适合您。

如果你不存在需要持续动画渲染的功能,可以选择关闭app.ticker的启动:

app.ticker.stop();
//等到需要时再渲染:
app.render();

其他

图片合成

前端有时会把多张图片合并成一张图片(雪碧图),而不是一张一张去加载。。通过设置background-position来显示不同的图片。pixi.js也有类似的技术,我们可以利用Texture Packer软件,把多张图片合并成一张图片,合并的同时,软件会生成一份json配置文件,记录了每张图片的相对位置。

具体教程见这里

首先下载 -> 拖动所需图片文件至texure packer,他将会自动生成

制作动画:

复选右边的图片,然后点击预览动画就可以看到图片连续起来的动画效果

然后点击发布精灵表

就会对应生成一个json文件(记录每个图像名字、大小、位置)和一个png文件(雪碧图)

spritesheet

Sritesheetshttps://pixijs.io/guides/basics/sprite-sheets.html

一般spritesheet用于是一张大的雪碧图(png),并且配合上一个json文件,json文件记录了雪碧图每个图片的位置以及其他信息

import { Application, Container, Sprite, Graphics, Loader, Spritesheet } from 'pixi.js';

// myjson记录了每张图片的相对位置
import myjson from './assets/treasureHunter.json';
// mypng里面有多张图片
import mypng from './assets/treasureHunter.png';

const loader = Loader.shared;

const app = new Application({
  width: 300,
  height: 300,
  antialias: true,
  transparent: false,
  resolution: 1,
  backgroundColor: 0x1d9ce0
});

document.body.appendChild(app.view);

loader
.add('mypng', mypng)
.load(setup)

function setup() {
  const texture = loader.resources["mypng"].texture.baseTexture;
  const sheet = new Spritesheet(texture, myjson);
}

需要注意的是,通过spritesheet加载完成后,有一些属性是为空的,所以所以我们需要等待加载完成去执行一些操作的话,需要等待spritesheet加载完成后(异步),在进行操作,这时需要用的哦它的prase方法

sheet.parse((textures) => {
  // mypng里面的一张叫treasure.png的图片
  const treasure = new Sprite(textures["treasure.png"]);
  treasure.position.set(0, 0);

  // mypng里面的一张叫blob.png的图片
  const blob = new Sprite(textures["blob.png"]);
  blob.position.set(100, 100);

  app.stage.addChild(treasure);
  app.stage.addChild(blob);
});

Texture Packer要收费的,免费版可能有水印(刚开始会送你7天pro),这里有个免费版的Texture Packer可以看一下:

http://free-tex-packer.com/download/

AnimatedSprite

可以看成一个会动的Sprite,png -> apng;Sprite -> AnimatedSprite,AnimatedSprite接受一个图像数组,按照数组顺序播放图像,然后我们就可以看到“动起来的效果”

传入2个参数

  • textures
  • autoUpdate,默认为true,采用PIXI.Ticker来更新动画时间
import { AnimatedSprite, Texture } from 'pixi.js';

const alienImages = [
    'image_sequence_01.png',
    'image_sequence_02.png',
    'image_sequence_03.png',
    'image_sequence_04.png',
];
const textureArray = [];

for (let i = 0; i < 4; i++)
{
    const texture = Texture.from(alienImages[i]);
    textureArray.push(texture);
}

const animatedSprite = new AnimatedSprite(textureArray);

当然,你也可以选择在图片合成工具的texurePacker里,设置图片名称,让他自动帮你生成动画:

此时通过spritesheet实例化生成的精灵表中,可以看到有animations属性(一个图片集数组),我们直接把它放到AnimatedSprite,就是一个动画精灵了

app.loader.add("chanziPng", chanziPng).load(async (loader, resources) => {
  const chanziSheet = new PIXI.Spritesheet(
    resources["chanziPng"].texture,
    chanzi
  );
  await chanziSheet.parse(() => {});
  // 创建动画雪碧
  const chanziAnimate = new PIXI.AnimatedSprite(
    chanziSheet.animations["ani"]
  );

  // 设置动画时间
  chanziAnimate.animationSpeed = 0.05;
  chanziAnimate.play();
  app.stage.addChild(chanziAnimate);
});

尝试一下:https://codesandbox.io/s/elated-wind-5j60fu?file=/src/Component.js

z-index

有时候我们需要对一些元素层级来定位,决定元素的层次优先级,我们可以先开启

PIXI.settings.SORTABLE_CHILDREN:设置容器属性“sortableChildren”的默认值。如果设置为 true,容器将在调用 updateTransform() 时按 zIndex 值对其子项进行排序,或者在调用 sortChildren() 时手动排序。

PIXI.settings.SORTABLE_CHILDREN = true;

此时我们的 sprite.zIndex = xx;就可以生效了

但是目前更新版本已废弃⚠️

使用:sortableChildren

app.stage.sortableChildren = true;

或者

const container = new Container();
container.sortableChildren = true;

设置容器属性 sortableChildren 的默认值。如果设置为 true,则容器将在调用时 updateTransform() 按 zIndex 值对其子级进行排序,如果 sortChildren() 被调用,则手动对子项进行排序。

这实际上改变了数组中元素的顺序,因此应被视为与其他解决方案(例如 PixiJS 层)相比性能不佳的基本解决方案。
另请注意,这可能不适用于该 addChildAt() 函数,因为排序可能会导致子项自动 zIndex 排序到另一个位置。

  • Default Value: 默认值:

    false 假

pixi Text资源没有销毁问题

通过react -> useEffect 创建pixi对象时,此时创建Text,Text会保存在utils中,组件销毁后重新创建,此时useEffect会重新执行,而Text它不像普通图片资源等,普通图片资源可以通过链接的形式有utils id 对应,而不会在utils中重复创建,而Text会重复创建。

如果反复执行(来回切换tab而不断创建, 销毁react组件)useEffect,此时内存会囤积大量Text的texture,像这样:

解决方法:

方法1: useEffect,return时通过 app.destroy(true, true),把所有缓存销毁

方法2: 在constant中全局静态创建文字

方法3: 文字通过图片的形式加载,不实用Text方法

方法4(最好):

通过app.destroy(true, { children: true })的形式销毁,因为直接传true的话会把所有children下的baseTexture销毁(把图片资源也销毁了)

image-20231108205107393

低帧数低端机动画速率问题

pixi的ticker使用的是requestAnimationFrame 向下兼容 setinterval,但是 requestAnimationFrame 的执行速率取决于手机的帧率,如果在不同刷新率的手机,会出现不一样速率的效果

此时我们可以用Tweenjs做缓动动画,但是对于动画种类,动画数量比较多的情况,还得疯狂嵌套tweenjs动画,通过 TweenA.chain(TweenB) TweenB.chain(TweenC) 来执行动画,套娃起来可能有点稍微麻烦

我们看下Tweenjs源码,实际上他们都是依赖 performance.now() 去换算执行时间,而不是直接通过ticker的执行速率

所以我们也可以通过每次执行ticker拿到performance.now去做timestamp的换算,去做动画

无法滑动问题

在手机上使用时,需要可以滚动页面。但是在游戏画布上滑动时并不能滚动页面,原因是pixi在默认情况下,会阻止所有事件的冒泡,并且会把画布的touch-action设置为none。

解决方法

app.view.style.touchAction = 'auto';
一些pixi例子

Run Pixie Run

WASTE INVADERS

宝可梦实战(通过TexturePacker实现动画)

碰撞检测案例(飞机大战小游戏)

另一个飞机大战github

一些资源网站:

爱给网 (精灵图,场景图)

参考文章

PixiJs

PixiJS - 最快、最灵活的 2D WebGL 渲染引擎

PixiJS基础教程

Pixi v7迁移

一个老版本的pixi教程

fabricjs

Fabric.js 是一个强大的H5 canvas框架,在原生canvas之上提供了交互式对象模型,通过简洁的api就可以在画布上进行丰富的操作。它也是一个SVG-to-canvas 解析器


文章作者: Hello
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Hello !
 上一篇
CSS渲染深究 CSS渲染深究
CSS动画和JS动画的对比CSS动画优点: 动画流畅(以每一帧的间隔保证恰当的时间刷新UI) 性能较优 动画效果对帧速不好的低版本浏览器 代码简单,调优方向固定 缺点: 运行过程控制较弱,无法附加事件绑定回调函数 代码冗长 JS动画
2020-07-31
下一篇 
finished gitee finished gitee
原来是个人邮箱的设置必须得是公开的,我一直是设置为private,搞得一直错,终于搞出来了。 但是发现了gitee的一个缺点,每次部署完后都要去码云更新一次。。。
2020-07-07 Hello
  目录