Skip to content

Commit

Permalink
Feat use harfbuzzjs (#131)
Browse files Browse the repository at this point in the history
* feat: use harfbuzz to generate Glyph

* test: adapt test to harfbuzz

* chore: remove unsed parentSize on renderer & fix tests

* fix: fix issue with top alignement when text contains spaces
  • Loading branch information
romgere authored May 30, 2024
1 parent e14b496 commit 79d3c34
Show file tree
Hide file tree
Showing 18 changed files with 386 additions and 225 deletions.
14 changes: 5 additions & 9 deletions app/components/three-preview.hbs
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
{{yield (hash
renderer=(component 'three-preview/renderer'
mesh=@mesh
parentSize=@parentSize
nearCamera=@nearCamera
{{yield
(hash
renderer=(component 'three-preview/renderer' mesh=@mesh nearCamera=@nearCamera)
size=(component 'three-preview/size' mesh=@mesh)
)
size=(component 'three-preview/size'
mesh=@mesh
)
)}}
}}
5 changes: 1 addition & 4 deletions app/components/three-preview/renderer.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
<canvas
...attributes
{{three-renderer @mesh parentSize=@parentSize nearCamera=@nearCamera}}
>
<canvas ...attributes {{three-renderer @mesh nearCamera=@nearCamera}}>
</canvas>
20 changes: 5 additions & 15 deletions app/modifiers/three-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ const {
} = config;

type namedArgs = {
parentSize?: boolean;
nearCamera?: boolean;
};

Expand Down Expand Up @@ -79,11 +78,8 @@ export default class ThreeRendererModifier extends Modifier<ThreeRendererModifie
preserveDrawingBuffer: true, // use this to allow creation of image from canvas
});
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(
// for parent sizing set size to 1x1 & let the renderFrame adapt the size accordingly later...
this.namedArgs.parentSize ? 1 : 1024,
this.namedArgs.parentSize ? 1 : 768,
);
// set size to 1x1 & let the renderFrame adapt the size accordingly later...
this.renderer.setSize(1, 1);
this.renderer.setClearColor(0xffffff, 1);
}

Expand Down Expand Up @@ -182,9 +178,7 @@ export default class ThreeRendererModifier extends Modifier<ThreeRendererModifie

this.animationFrameRequestID = requestAnimationFrame(() => this.renderFrame());

const { offsetWidth: width, offsetHeight: height } = this.namedArgs.parentSize
? this.canvas?.parentElement ?? this.canvas
: this.canvas;
const { offsetWidth: width, offsetHeight: height } = this.canvas?.parentElement ?? this.canvas;

if (this.rendererSize.width !== width || this.rendererSize.height !== height) {
this.renderer.setSize(width, height);
Expand All @@ -201,12 +195,8 @@ export default class ThreeRendererModifier extends Modifier<ThreeRendererModifie
this.renderer.render(this.scene, this.camera);
}

modify(
element: HTMLCanvasElement,
[mesh]: [THREE.Mesh | undefined],
{ parentSize, nearCamera }: namedArgs,
) {
this.namedArgs = { parentSize, nearCamera };
modify(element: HTMLCanvasElement, [mesh]: [THREE.Mesh | undefined], { nearCamera }: namedArgs) {
this.namedArgs = { nearCamera };

if (!this.canvas) {
this.canvas = element;
Expand Down
8 changes: 5 additions & 3 deletions app/routes/app.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import type IntlService from 'ember-intl/services/intl';
import type FontManagerService from 'text2stl/services/font-manager';

import { Registry as Services } from '@ember/service';

import type IntlService from 'ember-intl/services/intl';
import type FontManagerService from 'text2stl/services/font-manager';
import type HarfbuzzService from 'text2stl/services/harfbuzz';
import type RouterService from '@ember/routing/router-service';

type Transition = ReturnType<RouterService['transitionTo']>;
Expand All @@ -13,6 +13,7 @@ export default class AppRoute extends Route {
@service declare intl: IntlService;
@service declare router: Services['router'];
@service declare fontManager: FontManagerService;
@service declare harfbuzz: HarfbuzzService;

constructor(props: object | undefined) {
super(props);
Expand All @@ -23,6 +24,7 @@ export default class AppRoute extends Route {
this.intl.locale = locale === 'en-us' ? locale : [locale, 'en-us'];
// No await here, let's the loading happen & await for it in generator route
this.fontManager.loadFont();
this.harfbuzz.loadWASM();
}

afterModel() {
Expand Down
9 changes: 6 additions & 3 deletions app/routes/app/generator.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import type FontManagerService from 'text2stl/services/font-manager';
import TextMakerSettings from 'text2stl/models/text-maker-settings';
import config from 'text2stl/config/environment';

import type FontManagerService from 'text2stl/services/font-manager';
import type HarfbuzzService from 'text2stl/services/harfbuzz';

const {
APP: { textMakerDefault },
} = config;

export default class GeneratorRoute extends Route {
@service declare fontManager: FontManagerService;
@service declare harfbuzz: HarfbuzzService;

queryParams = {
modelSettings: {
Expand All @@ -31,12 +34,12 @@ export default class GeneratorRoute extends Route {

// No custom font via QP
model.customFont = undefined;
model.fontName = textMakerDefault.fontName;
model.variantName = textMakerDefault.variantName;
}

// Ensure font list is fully load
await this.fontManager.loadFont();
// Ensure harfbuzzJS is fully load (WASM loaded & lib instance created)
await this.harfbuzz.loadWASM();

return model;
}
Expand Down
33 changes: 25 additions & 8 deletions app/services/font-manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Service from '@ember/service';
import * as opentype from 'opentype.js';
import config from 'text2stl/config/environment';
import { inject as service } from '@ember/service';

import type HarfbuzzService from 'text2stl/services/harfbuzz';
import type { HBFont, HBFace } from 'harfbuzzjs/hbjs';

export type Category = 'sans-serif' | 'serif' | 'display' | 'handwriting' | 'monospace';
export type Script =
Expand Down Expand Up @@ -81,7 +84,14 @@ type GoogleFontApiResponse = {
}[];
};

export interface FaceAndFont {
font: HBFont;
face: HBFace;
}

export default class FontManagerService extends Service {
@service declare harfbuzz: HarfbuzzService;

availableFontScript: Script[] = [
'arabic',
'bengali',
Expand Down Expand Up @@ -120,9 +130,7 @@ export default class FontManagerService extends Service {

fontList: Map<string, Font> = new Map();

fontCache: Record<string, opentype.Font> = {};

opentype = opentype; // For easy mock
fontCache: Record<string, FaceAndFont> = {};

// For easy mock
fetch(input: RequestInfo, init?: RequestInit | undefined): Promise<Response> {
Expand Down Expand Up @@ -216,7 +224,16 @@ export default class FontManagerService extends Service {
document.adoptedStyleSheets.push(await stylesheet.replace(style));
}

async fetchFont(fontName: string, variantName?: Variant): Promise<opentype.Font> {
private openHBFont(buffer: ArrayBuffer): FaceAndFont {
const blob = this.harfbuzz.hb.createBlob(buffer);
const face = this.harfbuzz.hb.createFace(blob, 0);
return {
font: this.harfbuzz.hb.createFont(face),
face,
};
}

async fetchFont(fontName: string, variantName?: Variant): Promise<FaceAndFont> {
const font = this.fontList.get(fontName);
if (!font) {
throw `Unknown font name ${fontName}`;
Expand All @@ -238,15 +255,15 @@ export default class FontManagerService extends Service {
if (!this.fontCache[cacheName]) {
const res = await this.fetch(url.replace('http:', 'https:'));
const fontData = await res.arrayBuffer();
this.fontCache[cacheName] = this.opentype.parse(fontData);
this.fontCache[cacheName] = this.openHBFont(fontData);
}

return this.fontCache[cacheName];
}

async loadCustomFont(fontTTFFile: Blob): Promise<opentype.Font> {
async loadCustomFont(fontTTFFile: Blob): Promise<FaceAndFont> {
const fontAsBuffer = await fontTTFFile.arrayBuffer();
return this.opentype.parse(fontAsBuffer);
return this.openHBFont(fontAsBuffer);
}

private chunk<T>(array: T[], chunkSize: number) {
Expand Down
28 changes: 28 additions & 0 deletions app/services/harfbuzz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Service from '@ember/service';
import hb from 'harfbuzzjs/hbjs';
import type { HBInstance } from 'harfbuzzjs/hbjs';

export default class HarfbuzzService extends Service {
declare hb: HBInstance;

loadWASMPromise: undefined | Promise<void> = undefined;

async loadWASM() {
if (!this.loadWASMPromise) {
this.loadWASMPromise = this._loadWASM();
}

await this.loadWASMPromise;
}

async _loadWASM() {
const result = await WebAssembly.instantiateStreaming(fetch('/hb.wasm'));
this.hb = hb(result.instance);
}
}

declare module '@ember/service' {
interface Registry {
harfbuzz: HarfbuzzService;
}
}
Loading

0 comments on commit 79d3c34

Please sign in to comment.