1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
// suki を wasm にしたものを JS から呼ぶ。wasm とフォントは loadFont で 1 度だけ読み込み、このモジュールが持つ。
// wasm は `cargo build --lib --release --target wasm32-unknown-unknown` で作る(src/wasm.rs)。
//
// 受け渡しは UTF-8 のバイト列で行う。wasm の関数は成功なら 0、失敗なら 1 を返し、
// 結果(失敗のときはエラーの文言)を resultPointer と resultLength が指す場所に置く。
// 字形 1 つの置き方。outline は NotationTable の outline の添字である。
// 輪郭はフォントの単位なので、scaleX と scaleY を掛けると em になる。offsetX と offsetY は em である。
export interface NotationGlyph {
outline: number
scaleX: number
scaleY: number
offsetX: number
offsetY: number
}
// 分数の線や根号の横棒。左上の位置と大きさで、どれも em である。
export interface NotationRule {
offsetX: number
offsetY: number
width: number
height: number
}
// 書体にない字(\text の中の日本語など)。輪郭を持たないので文字として描く。
// size は字の大きさ(em)で、offsetY はベースラインの位置である。
export interface NotationText {
value: string
size: number
offsetX: number
offsetY: number
}
// 式 1 つ。寸法と位置は em で、y は下向きが正、ベースラインが 0 である。
export interface Notation {
width: number
height: number
depth: number
glyph: NotationGlyph[]
rule: NotationRule[]
text: NotationText[]
}
// 式の表。outline は字形の輪郭(SVG の d。y は下向き)で、式をまたいで 1 度だけ持つ。
// darken は輪郭のまわりに引く線の太さ(em)で、これを引くと SVG に書き出したものと同じ太さに見える。
export interface NotationTable {
outline: string[]
notation: Record<string, Notation>
darken: number
}
// wasm が公開するもの(src/wasm.rs)。
interface SukiExports {
memory: WebAssembly.Memory
allocate(length: number): number
deallocate(pointer: number, length: number): void
resultPointer(): number
resultLength(): number
loadFont(pointer: number, length: number): number
renderSvg(pointer: number, length: number, display: number): number
layoutNotation(pointer: number, length: number, display: number): number
glyphOutline(glyph: number): number
darken(): number
}
// layoutNotation が返す字形の置き方。字形は輪郭の添字ではなく、フォントの字形番号で指す。
interface PlacedGlyph {
glyph: number
scaleX: number
scaleY: number
offsetX: number
offsetY: number
}
// layoutNotation が返す式 1 つ。
interface PlacedNotation {
width: number
height: number
depth: number
glyph: PlacedGlyph[]
rule: NotationRule[]
text: NotationText[]
}
const encoder: TextEncoder = new TextEncoder()
const decoder: TextDecoder = new TextDecoder()
// 組み込んだ wasm。loadFont を呼ぶまでは空である。
let loadedSuki: SukiExports | null = null
function useSuki(): SukiExports {
if (loadedSuki === null) throw new Error('suki を読み込む前に呼ばれました。先に loadFont を呼びます')
return loadedSuki
}
// バイト列を wasm の中へ書き込み、その場所を返す。場所を取るとメモリが伸びて古い見方が使えなくなることがあるので、
// 書き込むための見方は場所を取ったあとで作る。
function writeBytes(suki: SukiExports, bytes: Uint8Array): number {
const pointer = suki.allocate(bytes.length) >>> 0
new Uint8Array(suki.memory.buffer, pointer, bytes.length).set(bytes)
return pointer
}
// 呼び出しの結果を読む。失敗なら、結果に入っている文言で例外にする。
function readResult(suki: SukiExports, status: number): string {
const pointer = suki.resultPointer() >>> 0
const length = suki.resultLength() >>> 0
const text = decoder.decode(new Uint8Array(suki.memory.buffer, pointer, length))
if (status !== 0) throw new Error(text)
return text
}
// wasm とフォントを読み込む。wasm は最初の 1 回だけ組み込み、2 回目からはフォントだけを読み直す。
export async function loadFont(wasmBytes: BufferSource, fontBytes: Uint8Array): Promise<void> {
if (loadedSuki === null) {
const source = await WebAssembly.instantiate(wasmBytes)
loadedSuki = source.instance.exports as unknown as SukiExports
}
const suki = useSuki()
const pointer = writeBytes(suki, fontBytes)
const status = suki.loadFont(pointer, fontBytes.length)
suki.deallocate(pointer, fontBytes.length)
readResult(suki, status)
}
// 式を輪郭の SVG にする。display が true なら独立した行の組み方にする。
// 式に囲み($…$ や \[…\])が付いていれば、そちらが組み方を決める。
export function renderSvg(tex: string, display: boolean): string {
const suki = useSuki()
const bytes = encoder.encode(tex)
const pointer = writeBytes(suki, bytes)
const status = suki.renderSvg(pointer, bytes.length, display ? 1 : 0)
suki.deallocate(pointer, bytes.length)
return readResult(suki, status)
}
function layoutNotation(suki: SukiExports, tex: string, display: boolean): PlacedNotation {
const bytes = encoder.encode(tex)
const pointer = writeBytes(suki, bytes)
const status = suki.layoutNotation(pointer, bytes.length, display ? 1 : 0)
suki.deallocate(pointer, bytes.length)
return JSON.parse(readResult(suki, status)) as PlacedNotation
}
function glyphOutline(suki: SukiExports, glyph: number): string {
return readResult(suki, suki.glyphOutline(glyph))
}
// キャンバスの式の表を作る。display の扱いは renderSvg と同じである。
// 字形の輪郭は式をまたいで重なるので、表には 1 度だけ入れ、式のほうは添字で引く。
export function buildNotationTable(texList: string[], display: boolean): NotationTable {
const suki = useSuki()
const outline: string[] = []
const outlineIndex: Map<number, number> = new Map()
const notation: Record<string, Notation> = {}
for (const tex of texList) {
const placed = layoutNotation(suki, tex, display)
const glyph: NotationGlyph[] = []
for (const item of placed.glyph) {
let index = outlineIndex.get(item.glyph)
if (index === undefined) {
index = outline.length
outlineIndex.set(item.glyph, index)
outline.push(glyphOutline(suki, item.glyph))
}
glyph.push({outline: index, scaleX: item.scaleX, scaleY: item.scaleY, offsetX: item.offsetX, offsetY: item.offsetY})
}
notation[tex] = {width: placed.width, height: placed.height, depth: placed.depth, glyph, rule: placed.rule, text: placed.text}
}
return {outline, notation, darken: suki.darken()}
}