一个使用 Leafer UI 实现的游戏王卡片渲染工具

游戏王卡片 Yugioh Card

前言

我曾在 3 年前制作了一个游戏王卡片生成器的网站,当时用的技术是 dom + css 渲染卡片。在不同浏览器渲染的结果差异挺大(需要精确到像素级)。其实一开始我有考虑过 Canvas 去实现,奈何当时遇到的问题比较多,也没啥经验。文本渲染需要处理非常复杂的情况,最终还是觉得 dom 实现比较简单。

3 年过去了,我想应该考虑用 Canvas 重构一下,就当练练手。正巧遇到了 Leafer UI 这个库,让我有了行动力。

项目展示

image.png

左侧是卡片,右侧是调试工具。直接修改右侧的 JSON 数据就能动态渲染了。具体的数据内容不是本文的重点,感兴趣的小伙伴可以自行研究。

开发流程

我设计每个卡片拥有自己的属性,且能够动态改变卡片内容,于是就采用了面向对象的思想,使用类的实现方式。

目前项目包含 5 种卡片,每个卡片都有基础属性与方法是重复的,重复地复制粘贴不是一个好方法,我自然地想到了类的继承。我定义了一个 Card 类,每个卡片都需要继承它,它基础结构如下:

class Card {
  constructor(data = {}) {

    this.leafer = null; // Leafer 实例
    this.cardWidth = 100; // 卡片宽度,需要重写
    this.cardHeight = 100;  // 卡片高度,需要重写
    this.key = 0; // 类似与 vue 中的 key,用于强制重新渲染
    this.data = {}; // 卡片数据
    this.defaultData = {}; // 默认卡片数据,需要重写
    
    this.view = data.view; // 渲染容器
    this.resourcePath = data.resourcePath; // 静态资源路径
    
    loadCSS(); // 加载字体样式
  }
  setData() { ... } // 更新数据
  loadFont() { ... } // 加载字体
  initData() { ... } // 初始化数据
  initLeafer() { ... } // 初始化 Leafer
  initDraw() { ... } // 绘制卡片,需要重写
  updateScale() { ... } // 更新 Leafer 的尺寸
}

接着我们定义一个 YugiohCard 子类:

class YugiohCard extends Card {
  constructor(data = {}) {

    super(data); // 把参数传递给父类
    // 以下是 Leafer 子元素
    this.cardLeaf = null;
    this.nameLeaf = null;
    // 此处省略,根据卡片内容而定
    this.defaultData = {}; // 卡片默认值
    this.initData(data); // 初始化数据
    this.initLeafer(); // 初始化 Leafer
    this.initDraw(); // 初始化绘制
    this.loadFont(); // 加载字体
  }

  initDraw() {
    this.drawCard();
    this.drawName();
    // 以下省略
    this.updateScale(); // 更新卡片尺寸
  }

  drawCard() { ... } // 绘制卡片背景
  drawName() { ... } // 绘制卡名
}

在 Canvas 中加载字体可以用 webfontloader 等库,由于卡片包含的字体数量过多,不适合一次性加载。于是我采用引入 css 动态加载字体的方式。对于 dom 很容易,字体触发加载后页面文本会自动更新。但是在 Canvas 中第一次渲染时,字体还没加载完成,等字体加载完后 Canvas 又结束渲染了。所以正确的动态加载流程是这样的:

首次渲染文本

触发字体加载

再次渲染文本

当卡片数据更新时,要调用父类的 setData 方法,我们来看看是怎么实现的:

setData(data = {}) {
  data = cloneDeep(data);
  let needDraw = false;
  let needLoadFont = false;
  Object.keys(data).forEach(key => {
    const value = data[key] ?? this.defaultData[key];
    if (JSON.stringify(this.data[key]) !== JSON.stringify(value)) {
      this.data[key] = value;
      if (['language', 'font'].includes(key)) {
        needLoadFont = true;
      }
      needDraw = true;
    }
  });
  if (needDraw) {
    this.initDraw();
  }
  // 先触发绘制,再触发字体加载
  if (needLoadFont) {
    this.loadFont();
  }
}

首先把数据深拷贝一份,以防对象类型数据被篡改。循环传进来的对象,通过空值合并运算符得出参数值。我使用 JSON.stringify 去对比数据是考虑到数组、对象的值不能直接比较是否相同。如果改变的参数涉及到字体改动,则需要去触发一次字体加载监听,我们再来看看父类中 loadFont 的实现:

loadFont() {
  document.fonts.ready.finally(() => {
    this.key++;
    this.initDraw();
  });
}

代码很少,通过 document.fonts.ready 去监听字体加载,不管成功失败都去初始化渲染,因为可能存在多个字体同时加载,某个字体失败的情况。有注意到这里改变了 key 吗?key 是用来强制更新文本渲染的,用 drawName 举例:

drawName() {
  const { name } = this.style;

  if (!this.nameLeaf) {
    this.nameLeaf = new CompressText();
    this.leafer.add(this.nameLeaf);
  }
  this.nameLeaf.set({
    text: this.data.name,
    fontFamily: name.fontFamily,
    fontSize: name.fontSize,
    letterSpacing: name.letterSpacing || 0,
    wordSpacing: name.wordSpacing || 0,
    textAlign: this.data.align || 'left',
    color: this.data.color || this.autoNameColor,
    gradient: this.data.gradient,
    gradientColor1: this.data.gradientColor1,
    gradientColor2: this.data.gradientColor2,
    rtFontSize: name.rtFontSize,
    rtTop: name.rtTop,
    rtColor: this.autoNameColor,
    width: this.showAttribute ? 1033 : 1161,
    height: 200,
    x: 116,
    y: name.top,
    key: this.key,
    zIndex: 10,
  });
}

每个复杂的文本渲染都用到 CompressText 类,关于这个类的实现,可以看我上一篇文章。为什么要用 key 强制渲染,虽然 CompressText 内部也会监听字体加载动态更新。我们再来看看渲染文本的流程:

如需加载

如需加载

Card.initDraw

CompressText 渲染文本

CompressText 字体加载

Card.loadFont

流程看着没有问题,但是 CompressText.set 方法,如果传入的新值和旧值相同,则不会去重新渲染文本。所以我们引入了一个 key 变量,能够手动触发重新渲染。

有细心的小伙伴可能注意到,每次数据更新都会重复执行 initDraw 方法,那么这不会影响性能吗?答案是不会的,看它内部是怎么写的:

drawXxx() {
  if (!this.xxxLeaf) {
    this.xxxLeaf = new Image();
    this.leafer.add(this.xxxLeaf);
  }
  this.xxxLeaf.set({
    ...
  });
}

首先会判断 xxxLeaf 有没有生成过实例,然后再调用 set 方法,而我们知道参数相同的情况下是不会重复渲染的。无需考虑参数是什么,从内部实现了局部更新。

以上就是整个卡片类的流程介绍,具体每个 drawXxx 的内容就不一一介绍了,感兴趣可以直接参考源码。

总结

我不知道这样的文章算不算纯粹的技术分享,但是我想写就写了O(∩_∩)O~。也许这里也有喜欢游戏王的牌佬呢(我自己不会打牌,就是单纯喜欢卡片)。

项目地址

项目源码:github.com/kooriookami…

在线演示:kooriookami.github.io/yugioh-card…

最佳实践:ygo.ygosgs.com/

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYRbMSxJ' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片