Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat use harfbuzzjs #131

Merged
merged 4 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading