解决 HTML Canvas 元素在高像素密度/高分辨率屏幕上显示模糊的问题

最近在使用 HTML Canvas 元素时发现通过 fillText() 绘制的文字明显发虚,像是以低分辨率渲染后放大所致,再仔细一检查,发现事实上包括 fillRect() 在内的其他图案也都存在相同的问题。

初步推测和显示设备使用了较高的 DPI 有关,随进行了一番搜索,在 Stackoverflow 、Medium 等站点都有一些提问和回答。然而意外的是,自己能找到的一些解答,至少在自己这边并不能解决问题。

经过摸索,找到了能够解决自己问题的办法,分享于此,以便后人。

以下为 Angular component 代码片段,语言为 TypeScript。

@ViewChild('canvas', { static: true }) canvas: ElementRef; // Canvas 元素
@ViewChild('canvasDiv', { static: true }) canvasDiv: ElementRef; // Canvas 元素的父元素,一个 div
private ctx: CanvasRenderingContext2D; // Canvas context

private dpiRatio: number; // DPI 缩放比例
private canvasDivWidth: number;  // 父元素宽度

……

this.dpiRatio = window.devicePixelRatio;

this.canvasDivWidth = this.canvasDiv.nativeElement.offsetWidth;

// 分别设置 canvas 的宽度和样式宽度,canvas 的显示尺寸将会减半
this.ctx = this.canvas.nativeElement.getContext('2d');
this.ctx.canvas.width = this.canvasWidth * this.dpiRatio;
this.ctx.canvas.style.width = this.canvasWidth + 'px';

// 在绘制时考虑缩放因素
this.ctx.font = 12 * this.dpiRatio + 'px Roboto';
this.ctx.fillText('HIDPI文字', x  * this.dpiRatio, y * this.dpiRatio);

一个悬而未决的问题是,canvas context 本身的 transfer 函数似乎不能做到整体的放大,因此这里作为临时方案暂时在绘制每一个图形时都乘以了缩放倍率。这里应该有更好的办法,不过限于时间,暂且搁置。

Angular 9+ 升级小记 —— 应付 MSAL-Angular 与 ngx-restangular 的兼容性问题

之前几次升级 Angular 版本的经历总体还算顺利,因此原本对 Angular 8 升级到 9 的过程也比较乐观。虽然 Angular 9 开始将默认启动新的 Angular Ivy,但考虑到已经正式发布数月,甚至 Android 10 都已问世,自己的项目又没有用特别冷门的依赖,依然没有担心会遇到问题。可惜墨菲定律无处不在,最后还是花了比想象中更多的时间完成升级。

为此,本文简单介绍自己在从 Angular 8 升级到 9 最终到 10 时遇到的问题及相应的解决方法,仅供参考。

首先,Angular 官方文档提供了详细的升级说明,建议在升级项目前首先通览相关文档,了解可能存在的问题。可以从下面的链接找到升级到 Angular 10 的最新信息。

https://angular.io/guide/updating-to-version-10

下面是自己在升级时实际操作的几个主要步骤:

  • 更新 Angular 8 相关依赖至最新版
    • ng update @angular/core@8 @angular/cli@8
    • ng build –prod –aot
    • 确认变更
  • 更新 @azure/msal-angular 至最新版 1.0.0
    • 安装 1.0.0 版并为新版的接口变化修改代码(1)
    • ng build –prod –aot
    • 确认变更
  • 升级至 Angular 9,检查并乎略一些依赖版本问题
    • ng update @angular/core@9 @angular/cli@9 –force(2)
    • ng build –prod –aot
    • 确认变更
    • 升级 Angular Material
      • ng update @angular/material@9
      • 检查 Angular Material 引用问题(3)
    • 增加 ngcc 作为 postinstall 脚本(4)
    • ng build –prod –aot
    • 确认变更
    • ng add @angular/localize (5)
    • ng serve –ssl 启动并检查应用基本功能
  • 升级至 Angular 10,检查并乎略一些依赖版本问题
    • ng update @angular/core @angular/cli –force
    • 临时手段解决 ngx-restangular 编译问题(6)
    • ng build –prod –aot
    • 确认变更
    • ng update @angular/material
    • ng build –prod –aot
    • 确认变更
    • ng serve –ssl 启动并检查应用基本功能
  • 大功告成

以下具体说明上述步骤中的一些关键操作。

(1)MSAL Angular 代码更改

在升级 Angular 9 之前,自己的项目使用 MSAL Angular 0.1.4 实现对 Microsoft 账户或 ADD 的验证。当时该库的文档和范例代码质量就差强人意,缺少说明和链接失效比比皆是。更重要的是,该库并不兼容 Angular Ivy,这也是没有更早升级 Angular 9 的原因之一。等着 MSAL Angular 花了几个月的时间终于正式发布 1.0.0 版本才开始升级,依然遇到几个问题:

  • 方法接口变化,getUser() 改为 getAccount(),displayableId 成员取消
  • 服务初始化方式变化,结构虽然较过去清晰,但文档和示例代码不足,甚至还有语法拼写错误
  • 请求验证成功后返回的 payload 格式发生变化,token 变量改为 rawIdToken
  • loginRedirect() 似乎不再正常工作,仍需进一步排查原因

无论如何,能够兼容 Angular 9 已经不易,总比 ngx-restangular 那样根本无法成功编译要好些。MSAL Angular 已经两个月没有新版本发布,不知今后的更新是否能解决一些问题。

(2)升级至 Angular 9

这步本身没有什么问题,不过需要检查一些报警的依赖版本冲突。自己的项目中主要是 tslib 已经到了 2.0 而 Angular 9 要求 1.10.0,以及 Angular http 停在了大版本7。确认没有问题后就以 –force 参数强制升级。

(3)检查 Angular Material 引用问题

新版本 Angular Material 要求每个引用都明确指定具体组件,如:

 import { MatSpinner } from '@angular/material/progress-spinner';

而不允许在一句引用中同时引入多个组件,如:

import { MatSpinner, MatSnackBar } from '@angular/material';

正常情况下 ng update 会自动更正代码,不过自己在操作时似乎遇到一些问题,没有顺利完成,只好收到批量做了一些处理。

(4)增加 ngcc 作为 postinstall 脚本

在 package.json 中增加 “postinstall”: “ngcc” 有助于避免可能的库兼容问题。参见 ngcc 的和 ModuleWithProviders 迁移的相关说明。

(5)增加 @angular/localize

如果项目中使用了 Angular 自带的 i18n 功能,可能需要执行 ng add @angular/localize 添加必要的依赖。不过这并非必要步骤,可以在遇到提示时才采取行动。

(6)解决 ngx-restangular 5.0.0 编译问题

很可惜,ngcc 不是银弹。至少,它无法解决 ngx-restangular 的兼容问题。

ngx-restangular 是自己项目中使用的主要第三方库之一,帮助使用 REST API 的使用。这个项目本身算不上太活跃,但基本功能还算充分。可惜的是,半年没有更新的它,果然无法在 Ivy 下正确编译,报错如下:

ERROR in node_modules/ngx-restangular/lib/ngx-restangular.module.d.ts:8:97 - error TS2314: Generic type 'ModuleWithProviders<T>' requires 1 type argument(s).

static forRoot(providers?: any[], configFunction?: (provider: any, ...arg: any[]) => void): ModuleWithProviders;

搜索尝试一些方法无果后,不得已手动修改库文件,增加类型 <RestangularModule> 在 node_modules/ngx-restangular/lib/ngx-restangular.module.d.ts 第八行末尾:

static forRoot(providers?: any[], configFunction?: (provider: any, ...arg: any[]) => void): ModuleWithProviders<RestangularModule>;

在 CI 管道中,则暂时每次都在 ng build 之前以 sed 命令完成代码更改:

sed -i 's/ModuleWithProviders;/ModuleWithProviders<RestangularModule>;/g' node_modules/ngx-restangular/lib/ngx-restangular.module.d.ts

于是,暂时绕过了编译错误。ngx-restangular 的功能本身倒似乎没有问题,可以正常工作。

经过功能确认和测试后,一个 Angular 8 应用终于顺利升级为了 Angular 10 应用。尽管并非一帆风顺,但 Ivy 确实明显提升了编译性能并减小了体积,总管没有白费功夫。如有需要,还可以参考 AngularGo GitHub 地址),了解升级过程中的配置与代码更改。