用Two.js绘制2D图形的新手指南(附实例)

505 阅读5分钟

Two.js是一个API,它使人们能够轻松地用代码创建2D形状。跟随我们的脚步,你将学会如何从JavaScript中创建和制作动画形状。

Two.js与渲染器无关,所以你可以依靠相同的API来绘制Canvas、SVG或WebGL。该库有很多方法,可以用来控制不同的形状在屏幕上的显示方式或动画效果。

安装

该库的未压缩版本的大小约为128KB,而压缩版本为50KB。如果你使用的是最新版本,你可以通过自定义构建来进一步减少库的大小。

你可以从GitHub下载该库的最小化版本,也可以直接链接到CDN托管的版本。一旦你把库添加到你的网页上,你就可以开始绘制和动画化不同的形状或物体。

创建基本形状

首先,你需要告诉Two.js你想在哪个元素上绘制和动画化你的形状。你可以向Two 构造函数传递一些参数来进行设置。

使用type 属性设置渲染器的类型。你可以指定一个值,比如svg,webgl,canvas, 等等。默认情况下,type 被设置为svg 。绘图空间的宽度和高度可以使用widthheight 参数来指定。你也可以使用fullscreen 参数将绘图空间设置为整个可用屏幕。当fullscreen 被设置为真时,widthheight 的值将被忽略。

最后,你可以告诉Two.js在布尔参数autostart 的帮助下,自动启动一个动画。

将所有需要的参数传递给构造函数后,你就可以开始绘制直线、矩形、圆形和椭圆。

你可以用two.makeLine(x1, y1, x2, y2) 来画一条线。这里,(x1, y1) 是第一个端点的坐标,(x2, y2) 是第二个端点的坐标。这个函数将返回一个Two.Line 对象,它可以被存储在一个变量中,以便以后进一步操作。

以类似的方式,你可以分别使用two.makeRectangle(x, y, width, height)two.makeRoundedRectangle(x, y, width, height, radius) 来绘制法线和圆角矩形。请记住,xy 确定矩形的中心,而不是像许多其他库那样确定其左上角坐标。widthheight 参数将决定矩形的大小。radius 参数用于指定圆角的半径值。

你也可以分别使用two.makeCircle(x, y, radius)two.makeEllipse(x, y, width, height) 在网页上渲染圆形和椭圆。就像矩形一样,xy 参数指定圆或椭圆的中心。在椭圆的情况下,将widthheight 设置为相同的值,就会像圆一样呈现。

two.makeArrow(x1, y1, x2, y2, size) 方法的帮助下,箭头的创建也很容易。x1y1 的值决定了箭尾的位置。x2y2 的值决定箭头的位置。第五个参数决定了箭头的大小。

有一个叫做two.makePolygon(x, y, radius, sides) 的方法,你可以用它来创建一个规则的多边形。x和y值决定了多边形的中心。radius 决定多边形的顶点与中心的距离,而sides 指定多边形的边数。

操纵组中的对象

Two.js中一个你会经常使用的有用方法是two.makeGroup(objects) 。你可以传递一个不同对象的列表或者传递一个对象、路径或组的数组作为这个方法的参数。它还会返回一个Two.Group 对象。在你创建了一个组之后,你可以使用该组为你提供的属性,一次性地操作它的所有子对象。

strokefill 属性可以用来为一个组中的所有子对象设置笔触和填充颜色。它们将接受所有你可以在CSS中表示颜色的有效形式。这意味着,你可以自由地使用RGB、HSL或十六进制符号。你也可以简单地使用颜色的名称,如orange,red, 或blue 。同样,你可以为所有其他属性设置值,如linewidth,opacity,miter, 和cap 。可以使用noFill()noStroke() 方法来删除一个组中所有孩子的填充和笔划。

你也可以应用其他的物理变换,如scale,rotation, 和translation 。这些变换将被应用于单个对象。使用add()remove() 等方法,向组中添加新的对象和删除它们都很容易。

这里有一个例子,它在随机位置创建了大约40个不同的矩形。然后,这些矩形被分组,这样我们就可以一次性改变它们的fillstrokelinewidth 的值:

var rects = [];

var elemWidth = document.querySelector("#draw-shapes").offsetWidth;

for (i = 0; i < 100; i++) {
  rects[i] = two.makeRectangle(
    Math.floor(Math.random() * elemWidth * 2),
    Math.floor(Math.random() * 420 * 2),
    10 + Math.floor(Math.random() * 100),
    10 + Math.floor(Math.random() * 100)
  );
}

var group = two.makeGroup(...rects);

group.noFill();
group.stroke = "black";
group.linewidth = 6;

two.update();

你可以点击div内的任何地方来改变矩形的位置。实际上,我们将设置组的位置。由于矩形是组的一部分,它们会自动移动。

为了练习,你应该尝试修改代码,把矩形分成两个相等的组。对每一组应用不同的linewidthstroke 颜色值,创造你自己独特的几何艺术作品。

定义梯度和书写文本

你可以在Two.js中定义线性和径向渐变。定义梯度并不意味着它将自动呈现在屏幕上,但它将供你在设置各种对象的fillstroke 值时使用。

你可以用two.makeLinearGradient(x1, y1, x2, y2, stops) 来定义一个线性梯度。值x1y1 决定梯度的起始坐标。同样地,值x2y2 决定梯度的终点坐标。stops 参数是一个Two.Stop 实例的数组。这些定义了数组中每个部分的颜色,以及每个颜色过渡到下一个颜色的位置。它们可以用new Two.Stop(offset, color, opacity) 来定义,其中offset 决定梯度上该特定颜色必须完全呈现的点。color 参数决定了梯度在特定点上的颜色。你可以使用任何有效的CSS颜色表示作为它的值。最后,opacity 参数决定了颜色的不透明度。不透明度是可选的,它可以有0到1之间的任何值。

你可以使用two.makeRadialGradient(x, y, radius, stops, fx, fy) ,以类似的方式定义径向渐变。在这种情况下,值xy 决定梯度的中心。radius 参数指定梯度应该延伸多远。你也可以向这个方法传递一个停止数组,以便设置梯度的颜色组成。参数fxfy 是可选的,它们可以用来指定梯度的焦点位置。

在下面的CodePen中查看一些梯度的类型和它们的代码。

记住,xy 梯度的位置是相对于它们要填充的形状的原点而言的。例如,一个径向梯度应该从中心开始填充一个形状,它的xy 总是设置为零。

Two.js还允许你在绘图区写文字,并在以后根据你的需要更新它。这需要使用方法two.makeText(message, x, y, styles) 。从参数的名称中可能可以看出,message 是你要写的实际文本。参数xy 是点的坐标,它将作为写入文本的中心。styles 参数是一个对象,可以用来设置一大批属性的值。

你可以使用样式来设置诸如字体family,size, 和alignment 的属性值。你也可以指定诸如fill,stroke,opacity,rotation,scale, 和translation 的属性值。

创建一个Two.js项目

在了解了所有这些方法和属性之后,现在是时候将它们应用于一个项目了。在本教程中,我将向你展示我们如何使用Two.js来渲染周期表的前十个元素,让电子围绕原子核旋转。核子也会有一些轻微的运动,以提高我们的表现形式的视觉吸引力。

我们首先定义了一些变量和函数,这些变量和函数将在后面使用:

var centerX = window.innerWidth / 2;
var centerY = window.innerHeight / 2;

var elem = document.getElementById("atoms");

var elementNames = [
  "",
  "Hydrogen",
  "Helium",
  "Lithium",
  "Beryllium",
  "Boron",
  "Carbon",
  "Nitrogen",
  "Oxygen",
  "Fluorine",
  "Neon"
];

var styles = {
  alignment: "center",
  size: 36,
  family: "Lato"
};

var nucleusCount = 10;
var nucleusArray = Array();

var electronCount = 10;
var electronArray = Array();

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

上面的代码将我们窗口中心的坐标存储在变量centerXcenterY 。这些将在后面用于将我们的原子放在中心位置。elementNames 数组包含周期表前十个元素的名称。每个名称的索引对应于该元素的电子和质子数量,并且以空字符串开始。styles 对象包含了用于对文本对象进行造型的属性。

我们还定义了一个函数intRange() ,以获得一个在给定极值内的随机整数值:

var two = new Two({ fullscreen: true }).appendTo(elem);

var protonColor = two.makeRadialGradient(
  0,
  0,
  15,
  new Two.Stop(0, "red", 1),
  new Two.Stop(1, "black", 1)
);

var neutronColor = two.makeRadialGradient(
  0,
  0,
  15,
  new Two.Stop(0, "blue", 1),
  new Two.Stop(1, "black", 1)
);

for (i = 0; i < nucleusCount; i++) {
  nucleusArray.push(two.makeCircle(intRange(-10, 10), intRange(-10, 10), 8));
}

nucleusArray.forEach(function(nucleus, index) {
  if (index % 2 == 0) {
    nucleus.fill = protonColor;
  }
  if (index % 2 == 1) {
    nucleus.fill = neutronColor;
  }
  nucleus.noStroke();
});

这创建了一个 Two 的实例,并定义了两个径向梯度。红色/黑色的径向梯度将代表质子,而蓝色/黑色的梯度将代表中子。

我们使用intRange() 函数将所有这些中子和质子放在相互之间的20个像素范围内。makeCircle() 方法还将这些质子和中子的半径设置为10像素。之后,我们在nucleusArray ,用不同的梯度交替填充每个圆:

for (var i = 0; i < 10; i++) {
  if (i < 2) {
    var shellRadius = 50;
    var angle = i * Math.PI;
    electronArray.push(
      two.makeCircle(
        Math.cos(angle) * shellRadius,
        Math.sin(angle) * shellRadius,
        5
      )
    );
  }
  if (i >= 2 && i < 10) {
    var shellRadius = 80;
    var angle = (i - 2) * Math.PI / 4;
    electronArray.push(
      two.makeCircle(
        Math.cos(angle) * shellRadius,
        Math.sin(angle) * shellRadius,
        5
      )
    );
  }
}

在原子核内放置中子和质子很容易。然而,正确地将电子放置在一个统一的距离上,则需要一点数学运算。我们使用shellRadius 变量来指定不同电子壳与原子核的距离。一个整圆覆盖的角度等于2个PI弧度。我们可以通过将2个PI弧度平均分配给不同的电子,来均匀地放置这些电子。

Math.cos()Math.sin() 函数被用来根据不同电子的角度来分离其位置向量的垂直和水平分量:

var orbitA = two.makeCircle(centerX, centerY, 50);
orbitA.fill = "transparent";
orbitA.linewidth = 2;
orbitA.stroke = "rgba(0, 0, 0, 0.1)";

var orbitB = two.makeCircle(centerX, centerY, 80);
orbitB.fill = "transparent";
orbitB.linewidth = 2;
orbitB.stroke = "rgba(0, 0, 0, 0.1)";

var groupElectronA = two.makeGroup(electronArray.slice(0, 2));
groupElectronA.translation.set(centerX, centerY);
groupElectronA.fill = "orange";
groupElectronA.linewidth = 1;

var groupElectronB = two.makeGroup(electronArray.slice(2, 10));
groupElectronB.translation.set(centerX, centerY);
groupElectronB.fill = "yellow";
groupElectronB.linewidth = 1;

var groupNucleus = two.makeGroup(nucleusArray);
groupNucleus.translation.set(centerX, centerY);

代码的这一部分将来自不同外壳的电子以及中子和质子放在它们自己独立的组中。它还为特定轨道上的所有电子一次性设置了填充颜色:

two
  .bind("update", function(frameCount) {
    groupElectronA.rotation += 0.025 * Math.PI;
    groupElectronB.rotation += 0.005 * Math.PI;
    groupNucleus.rotation -= 0.05;
  })
  .play();

var text = two.makeText("", centerX, 100, styles);

nucleusArray.forEach(function(nucleus, index) {
  nucleus.opacity = 0;
});

electronArray.forEach(function(electron, index) {
  electron.opacity = 0;
});

这部分代码将单个电子和质子的不透明度设为零。它还告诉Two.js以特定速度旋转电子和质子:

visible = 0;

document.addEventListener("click", function(event) {
  if (visible < nucleusArray.length) {
    nucleusArray[visible].opacity = 1;
    electronArray[visible].opacity = 1;
    visible++;
    text.value = elementNames[visible];
  }
  else {
    nucleusArray.forEach(el => el.opacity=0);
    electronArray.forEach(el => el.opacity=0);
    visible = 0;
    text.value = elementNames[0];
  }
});         


代码的最后部分允许我们通过点击鼠标或轻敲来迭代元素。为了加载下一个元素,我们再让一个电子和一个质子或中子可见,并更新元素名称。点击最后一个元素后,所有的粒子又被隐藏起来,所以我们可以重新开始。visible 这个变量记录了当前可见的原子粒子的数量,这样我们就可以相应地显示或隐藏它们。

最后的思考

本教程开始时,我们简单介绍了Two.js库,以及如何用它来绘制矩形、圆形和椭圆等形状。之后,我们讨论了如何对不同的对象进行分组,以便一次性地对它们进行操作。我们用这种能力来分组元素,使它们同步平移和旋转。这些工具在我们的元素周期表中前十种元素的原子动画中都有体现。