精灵 Sprite

在spritejs中,Sprite是最基础的一个类,一个Sprite对象相当于DOM中的一个元素Element。同Element在文档流中一样,Sprite在Canvas画布上占有一个特定的区域。一个Sprite有自己的盒模型,通过设置它的基本属性,可以让它在Canvas画布上呈现出不同的背景颜色、边框,也可以让它出现在不同的坐标位置。

设置边框和大小

spritejs里,一个不具有textures的Sprite元素默认大小为0,即使将它添加到指定Layer,也是不会被显示出来的。我们给元素设定大小,再设定一个边框,就能让它在画布上呈现出来。

元素 - 图1

不同线宽、大小、圆角的border:

  1. const scene = new Scene('#border-and-size', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. const box1 = new Sprite({
  4. size: [100, 100],
  5. pos: [100, 100],
  6. border: [2, '#f77'],
  7. });
  8. const box2 = new Sprite({
  9. size: [200, 200],
  10. pos: [300, 100],
  11. border: [4, '#7c7'],
  12. borderRadius: 20,
  13. });
  14. const box3 = new Sprite({
  15. size: [300, 300],
  16. pos: [600, 100],
  17. border: [6, '#77c'],
  18. borderRadius: 50,
  19. });
  20. const box4 = new Sprite({
  21. size: [400, 400],
  22. pos: [1000, 100],
  23. border: [8, '#c7c'],
  24. borderRadius: 200,
  25. });
  26. layer.append(box1, box2, box3, box4);

填充背景色

spritejs背景色填充只需要设置bgcolor属性,支持所有css允许的颜色类型,最常用的是rgb和rgba。

元素 - 图2

  1. const scene = new Scene('#bgcolor', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. const box1 = new Sprite({
  4. size: [100, 100],
  5. pos: [100, 100],
  6. bgcolor: 'red',
  7. });
  8. const box2 = new Sprite({
  9. size: [200, 200],
  10. pos: [300, 100],
  11. bgcolor: '#7c7',
  12. borderRadius: 20,
  13. });
  14. const box3 = new Sprite({
  15. size: [300, 300],
  16. pos: [600, 100],
  17. bgcolor: 'rgba(192, 128, 192, 0.5)',
  18. borderRadius: 50,
  19. });
  20. const box4 = new Sprite({
  21. size: [400, 400],
  22. pos: [1000, 100],
  23. bgcolor: 'hsl(180,50%,50%)',
  24. borderRadius: 200,
  25. });
  26. layer.append(box1, box2, box3, box4);

加载图片

在spritejs中,一个sprite元素可以加载一张或多张图片,加载图片最简单的方式就是直接设置Sprite元素的textures属性,将其中的内容设置为对应的图片的URL。

元素 - 图3

如果指定图片不设置元素大小,sprite元素默认宽高为图片宽高(微信小程序版除外),如果设置元素大小,sprite图片默认大小被缩放为元素大小。此外,我们可以通过指定rect,来控制图片被加载到元素的哪个位置,这样我们可以平铺加载多张图片。最后,我们还可以通过指定srcRect来裁剪要加载的图片。以上是精灵图片的基本用法,更多加载图片相关的内容,参考高级用法:资源加载与雪碧图

  1. const scene = new Scene('#textures', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. const texture = 'https://p5.ssl.qhimg.com/t01a2bd87890397464a.png';
  4. const s1 = new Sprite(texture);
  5. s1.attr({
  6. pos: [100, 20],
  7. border: [2, 'grey'],
  8. });
  9. const s2 = new Sprite(texture);
  10. s2.attr({
  11. size: [190, 269],
  12. border: [2, 'grey'],
  13. pos: [500, 20],
  14. });
  15. const s3 = new Sprite();
  16. s3.attr({
  17. textures: [{src: texture, rect: [0, 0, 190, 268]}, {src: texture, rect: [192, 0, 190, 268]}],
  18. border: [2, 'grey'],
  19. pos: [500, 300],
  20. });
  21. const s4 = new Sprite();
  22. s4.attr({
  23. textures: [
  24. {src: texture, rect: [0, 0, 190, 268], srcRect: [0, 0, 190, 268]},
  25. {src: texture, rect: [200, 278, 190, 268], srcRect: [191, 269, 190, 268]},
  26. {src: texture, rect: [0, 278, 190, 268], srcRect: [0, 269, 190, 268]},
  27. {src: texture, rect: [200, 0, 190, 268], srcRect: [191, 0, 190, 268]},
  28. ],
  29. border: [2, 'grey'],
  30. pos: [1000, 20],
  31. });
  32. layer.append(s1, s2, s3, s4);

文字 Label

Label是用来显示文字的元素,可以显示单行或多行文字。通过Label的font属性可以改变字体样式、大小等,支持css font字符串。与加载图片的精灵元素类似,如果Label不指定宽高,可以自适应宽高。文字可以通过设置textAlign属性修改对齐方式,默认是居左对齐,可以支持居中和居右对齐。文字还可以支持行高lineHeight属性,如果不设置这个属性,默认行高是font指定字体像素大小的1.2倍。通过设置padding属性能够让文字周围保留一定的空白。

元素 - 图4

  1. const scene = new Scene('#label-text', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer('fglayer');
  3. const text1 = new Label('SpriteJS.org');
  4. text1.attr({
  5. pos: [100, 40],
  6. fillColor: '#707',
  7. font: 'oblique small-caps bold 56px Arial',
  8. border: [2.5, '#ccc'],
  9. });
  10. layer.append(text1);
  11. const text2 = new Label('从前有座\n灵剑山');
  12. text2.attr({
  13. pos: [500, 40],
  14. fillColor: '#077',
  15. font: '64px "宋体"',
  16. lineHeight: 112,
  17. textAlign: 'center',
  18. padding: [0, 30],
  19. border: [2.5, '#ccc'],
  20. });
  21. layer.append(text2);
  22. const text3 = new Label('Hello');
  23. text3.attr({
  24. pos: [100, 240],
  25. strokeColor: '#fc7',
  26. font: 'bold italic 70px Microsoft Yahei',
  27. textAlign: 'center',
  28. padding: [0, 30],
  29. border: [2.5, '#ccc'],
  30. });
  31. layer.append(text3);
  32. function createClockTexts(text, x, y) {
  33. const len = text.length;
  34. for(let i = 0; i < len; i++) {
  35. const char = text.charAt(i);
  36. const label = new Label(char);
  37. label.attr({
  38. anchor: [0.5, 4.5],
  39. pos: [x, y],
  40. font: 'bold 44px Arial',
  41. fillColor: '#37c',
  42. rotate: i * 360 / len,
  43. });
  44. layer.append(label);
  45. }
  46. }
  47. createClockTexts('Sprite.js JavaScript Canvas...', 1200, 300);

label能够自适应大小,但是对于指定大小的Label,超出大小的部分文字将被遮挡,目前无法做到自动换行、撑开box等高级功能。这块内容后续可以通过为spritejs开发专门的文字类扩展库来实现。

路径 Path

前面的例子里我们已经见过Path的使用,这是一个强大的用来绘制矢量图形的基础类。

Path支持SVG的Path路径,我们可以使用它来绘制复杂的几何图形。

元素 - 图5

  1. const scene = new Scene('#svgpath', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer('fglayer');
  3. const p1 = new Path();
  4. p1.attr({
  5. path: {
  6. d: 'M280,250A200,200,0,1,1,680,250A200,200,0,1,1,280,250Z',
  7. transform: {
  8. scale: 0.5,
  9. },
  10. trim: true,
  11. },
  12. strokeColor: '#033',
  13. fillColor: '#839',
  14. lineWidth: 12,
  15. pos: [100, 50],
  16. });
  17. layer.appendChild(p1);
  18. const p2 = new Path();
  19. p2.attr({
  20. path: {
  21. d: 'M480,50L423.8,182.6L280,194.8L389.2,289.4L356.4,430L480,355.4L480,355.4L603.6,430L570.8,289.4L680,194.8L536.2,182.6Z',
  22. transform: {
  23. rotate: 45,
  24. },
  25. trim: true,
  26. },
  27. fillColor: '#ed8',
  28. pos: [450, 100],
  29. });
  30. layer.appendChild(p2);
  31. const p3 = new Path();
  32. p3.attr({
  33. path: {
  34. d: 'M480,437l-29-26.4c-103-93.4-171-155-171-230.6c0-61.6,48.4-110,110-110c34.8,0,68.2,16.2,90,41.8C501.8,86.2,535.2,70,570,70c61.6,0,110,48.4,110,110c0,75.6-68,137.2-171,230.8L480,437z',
  35. trim: true,
  36. },
  37. strokeColor: '#f37',
  38. lineWidth: 20,
  39. lineJoin: 'round',
  40. lineCap: 'round',
  41. pos: [1000, 100],
  42. });
  43. layer.appendChild(p3);

Path对象的path属性是一个非常重要而且强大的属性,通过它能够指定SVG Path的“d”属性,绘制一个矢量图形。path属性还包括transform,对矢量图进行变换。与精灵元素本身的transform不同的是,path的transform直接变换的是矢量路径,所以在进行缩放的时候能够保真。

元素 - 图6

可以看到左边的爱心在放大的时候会变模糊,右边则不会。因为右边是在放大的时候通过路径的transform重新生成的路径,这样可以保真,当然代价是运算量比较大,因此有利有弊,分场合使用。

  1. const scene = new Scene('#svgpath-transform', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer('fglayer');
  3. const d = 'M23.6,0c-3.4,0-6.3,2.7-7.6,5.6C14.7,2.7,11.8,0,8.4,0C3.8,0,0,3.8,0,8.4c0,9.4,9.5,11.9,16,21.2 c6.1-9.3,16-12.1,16-21.2C32,3.8,28.2,0,23.6,0z';
  4. const heart1 = new Path();
  5. heart1.attr({
  6. anchor: [0.5, 0.5],
  7. path: {
  8. d,
  9. transform: {
  10. rotate: 45,
  11. },
  12. trim: true,
  13. },
  14. fillColor: '#f33',
  15. pos: [300, 300],
  16. });
  17. layer.appendChild(heart1);
  18. heart1.animate([
  19. {scale: 1},
  20. {scale: 10},
  21. ], {
  22. duration: 5000,
  23. iterations: Infinity,
  24. direction: 'alternate',
  25. });
  26. const heart2 = new Path();
  27. heart2.attr({
  28. anchor: [0.5, 0.5],
  29. path: {
  30. d,
  31. transform: {
  32. rotate: 45,
  33. },
  34. trim: true,
  35. },
  36. fillColor: '#f33',
  37. pos: [900, 300],
  38. });
  39. heart2.animate([
  40. {path: {d, trim: true, transform: {rotate: 45, scale: 1}}},
  41. {path: {d, trim: true, transform: {rotate: 45, scale: 10}}},
  42. ], {
  43. duration: 5000,
  44. iterations: Infinity,
  45. direction: 'alternate',
  46. });
  47. layer.appendChild(heart2);

如上所见,使用Path对象可以绘制复杂的矢量图,不过我们需要对SVG的Path熟悉才会比较好用。spritejs本身不提供基础的简单图形的绘制,但我们可以使用自定义绘图或者通过创建自定义的精灵类型来解决这样的需求。在未来,我们考虑提供专业的绘图扩展库,来解决各种图形相关的问题。

分组 Group

就像DOM元素可以嵌套一样,当我们要批量操作多个元素时,我们可以使用Group元素将其他元素放到Group元素下。

元素 - 图7

对于Group元素的操作就像操作普通精灵那样,因此当我们把几个不同的元素放进Group之后,直接操作Group就可以让Group中的所有元素一起随着Group运动。

  1. const scene = new Scene('#group', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer('fglayer');
  3. const group = new Group();
  4. const arcD = 'M0 0L 50 0A50 50 0 0 1 43.3 25z';
  5. group.attr({
  6. size: [300, 300],
  7. pos: [770, 300],
  8. anchor: [0.5, 0.5],
  9. });
  10. layer.append(group);
  11. for(let i = 0; i < 6; i++) {
  12. const arc = new Path();
  13. arc.attr({
  14. path: {
  15. d: arcD,
  16. transform: {scale: 3, rotate: -15},
  17. trim: true,
  18. },
  19. pos: [150, 150],
  20. anchor: [0, 0.5],
  21. strokeColor: 'red',
  22. rotate: i * 60,
  23. });
  24. arc.attr('fillColor', `rgb(${i * 139 % 255}, 0, 0)`);
  25. group.append(arc);
  26. }
  27. group.animate([
  28. {rotate: 0},
  29. {rotate: 360},
  30. ], {
  31. duration: 3000,
  32. iterations: Infinity,
  33. });

Group除了分组元素外,还有一个特别好的功能,那就是创建clip剪裁区域。

元素 - 图8

  1. ;(async function () {
  2. const imgUrl = 'https://p4.ssl.qhimg.com/t01423053c4cb748581.jpg';
  3. const scene = new Scene('#group-clip', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  4. await scene.preload({id: 'beauty', src: imgUrl});
  5. const layer = scene.layer('fglayer');
  6. const group = new Group();
  7. group.attr({
  8. pos: [770, 300],
  9. anchor: [0.5, 0.5],
  10. clip: {d: 'M23.6,0c-3.4,0-6.3,2.7-7.6,5.6C14.7,2.7,11.8,0,8.4,0C3.8,0,0,3.8,0,8.4c0,9.4,9.5,11.9,16,21.2 c6.1-9.3,16-12.1,16-21.2C32,3.8,28.2,0,23.6,0z', transform: {scale: 15}},
  11. });
  12. layer.append(group);
  13. const sprite = new Sprite('beauty');
  14. sprite.attr({
  15. pos: [-10, 0],
  16. scale: 0.75,
  17. });
  18. group.append(sprite);
  19. }())

Group的clip属性和Path的path属性一样,可以设置d,表示剪裁区域,并且设置transform和trim。