msdf font renderen
This commit is contained in:
parent
d3845eb85f
commit
fdc2ab9421
8 changed files with 253 additions and 11 deletions
1
web/pw-visualizer/assets/res/fonts/roboto.json
Normal file
1
web/pw-visualizer/assets/res/fonts/roboto.json
Normal file
File diff suppressed because one or more lines are too long
BIN
web/pw-visualizer/assets/res/fonts/roboto.png
Normal file
BIN
web/pw-visualizer/assets/res/fonts/roboto.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
29
web/pw-visualizer/assets/shaders/frag/msdf.glsl
Normal file
29
web/pw-visualizer/assets/shaders/frag/msdf.glsl
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#extension GL_OES_standard_derivatives : enable
|
||||||
|
#ifdef GL_ES
|
||||||
|
precision mediump float;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
varying vec2 v_texCoord;
|
||||||
|
uniform sampler2D msdf;
|
||||||
|
uniform vec4 u_bgColor;
|
||||||
|
uniform vec4 u_fgColor;
|
||||||
|
|
||||||
|
uniform float u_distanceRange;
|
||||||
|
uniform float u_glyphSize;
|
||||||
|
uniform vec2 u_resolution;
|
||||||
|
uniform vec4 u_viewbox;
|
||||||
|
|
||||||
|
float median(float r, float g, float b) {
|
||||||
|
return max(min(r, g), min(max(r, g), b));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float scale = u_distanceRange / u_glyphSize * u_resolution.y / u_viewbox.w;
|
||||||
|
|
||||||
|
vec3 msd = texture2D(msdf, v_texCoord).rgb;
|
||||||
|
float sd = median(msd.r, msd.g, msd.b);
|
||||||
|
float screenPxRange = max(u_distanceRange, scale);
|
||||||
|
float screenPxDistance = screenPxRange*(sd - 0.5);
|
||||||
|
float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0);
|
||||||
|
gl_FragColor = vec4(u_fgColor.rgb, u_fgColor.a * opacity);
|
||||||
|
}
|
|
@ -9,11 +9,16 @@ export {default as shipPng} from "../assets/res/ship.png";
|
||||||
|
|
||||||
export {default as fontPng} from "../assets/res/font.png";
|
export {default as fontPng} from "../assets/res/font.png";
|
||||||
|
|
||||||
|
export {default as robotoMsdfPng} from "../assets/res/fonts/roboto.png";
|
||||||
|
export {default as robotoMsdfJson} from "../assets/res/fonts/roboto.json";
|
||||||
|
|
||||||
export {default as imageFragmentShader} from "../assets/shaders/frag/image.glsl?url";
|
export {default as imageFragmentShader} from "../assets/shaders/frag/image.glsl?url";
|
||||||
export {default as maskedImageFragmentShader} from "../assets/shaders/frag/masked_image.glsl?url";
|
export {default as maskedImageFragmentShader} from "../assets/shaders/frag/masked_image.glsl?url";
|
||||||
|
|
||||||
export {default as simpleFragmentShader} from "../assets/shaders/frag/simple.glsl?url";
|
export {default as simpleFragmentShader} from "../assets/shaders/frag/simple.glsl?url";
|
||||||
export {default as vorFragmentShader} from "../assets/shaders/frag/vor.glsl?url";
|
export {default as vorFragmentShader} from "../assets/shaders/frag/vor.glsl?url";
|
||||||
|
export {default as msdfFragmentShader} from "../assets/shaders/frag/msdf.glsl?url";
|
||||||
|
|
||||||
|
|
||||||
export {default as imageVertexShader} from "../assets/shaders/vert/image.glsl?url";
|
export {default as imageVertexShader} from "../assets/shaders/vert/image.glsl?url";
|
||||||
export {default as simpleVertexShader} from "../assets/shaders/vert/simple.glsl?url";
|
export {default as simpleVertexShader} from "../assets/shaders/vert/simple.glsl?url";
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { defaultLabelFactory, LabelFactory, Align, Label } from "./webgl/text";
|
||||||
import { VoronoiBuilder } from "./voronoi/voronoi";
|
import { VoronoiBuilder } from "./voronoi/voronoi";
|
||||||
import * as assets from "./assets";
|
import * as assets from "./assets";
|
||||||
import { loadImage, Texture } from "./webgl/texture";
|
import { loadImage, Texture } from "./webgl/texture";
|
||||||
|
import { defaultMsdfLabelFactory, MsdfLabelFactory, Label as MsdfLabel } from "./webgl/msdf_text";
|
||||||
|
|
||||||
|
|
||||||
function to_bbox(box: number[]): BBox {
|
function to_bbox(box: number[]): BBox {
|
||||||
|
@ -84,7 +85,7 @@ export function init() {
|
||||||
|
|
||||||
ms_per_frame = parseInt(ELEMENTS["speed"].value);
|
ms_per_frame = parseInt(ELEMENTS["speed"].value);
|
||||||
|
|
||||||
GL = CANVAS.getContext("webgl");
|
GL = CANVAS.getContext("webgl", { antialias: true });
|
||||||
|
|
||||||
GL.clearColor(0, 0, 0, 1);
|
GL.clearColor(0, 0, 0, 1);
|
||||||
GL.clear(GL.COLOR_BUFFER_BIT);
|
GL.clear(GL.COLOR_BUFFER_BIT);
|
||||||
|
@ -124,9 +125,12 @@ export class GameInstance {
|
||||||
image_shader: Shader;
|
image_shader: Shader;
|
||||||
masked_image_shader: Shader;
|
masked_image_shader: Shader;
|
||||||
|
|
||||||
|
msdf_shader: Shader;
|
||||||
|
|
||||||
text_factory: LabelFactory;
|
text_factory: LabelFactory;
|
||||||
planet_labels: Label[];
|
msdf_text_factory: MsdfLabelFactory;
|
||||||
ship_labels: Label[];
|
planet_labels: MsdfLabel[];
|
||||||
|
ship_labels: MsdfLabel[];
|
||||||
|
|
||||||
ship_ibo: IndexBuffer;
|
ship_ibo: IndexBuffer;
|
||||||
ship_vao: VertexArray;
|
ship_vao: VertexArray;
|
||||||
|
@ -153,6 +157,7 @@ export class GameInstance {
|
||||||
planets_textures: Texture[],
|
planets_textures: Texture[],
|
||||||
ship_texture: Texture,
|
ship_texture: Texture,
|
||||||
font_texture: Texture,
|
font_texture: Texture,
|
||||||
|
robotoMsdfTexture: Texture,
|
||||||
shaders: Dictionary<ShaderFactory>
|
shaders: Dictionary<ShaderFactory>
|
||||||
) {
|
) {
|
||||||
this.game = game;
|
this.game = game;
|
||||||
|
@ -168,7 +173,9 @@ export class GameInstance {
|
||||||
});
|
});
|
||||||
this.masked_image_shader = shaders["masked_image"].create_shader(GL);
|
this.masked_image_shader = shaders["masked_image"].create_shader(GL);
|
||||||
|
|
||||||
|
this.msdf_shader = shaders["msdf"].create_shader(GL);
|
||||||
this.text_factory = defaultLabelFactory(GL, font_texture, this.image_shader);
|
this.text_factory = defaultLabelFactory(GL, font_texture, this.image_shader);
|
||||||
|
this.msdf_text_factory = defaultMsdfLabelFactory(GL, robotoMsdfTexture, this.msdf_shader);
|
||||||
this.planet_labels = [];
|
this.planet_labels = [];
|
||||||
this.ship_labels = [];
|
this.ship_labels = [];
|
||||||
|
|
||||||
|
@ -278,11 +285,11 @@ export class GameInstance {
|
||||||
1,
|
1,
|
||||||
0,
|
0,
|
||||||
-planets[i * 3],
|
-planets[i * 3],
|
||||||
-planets[i * 3 + 1] - 1.2,
|
-planets[i * 3 + 1] - 1.171875,
|
||||||
1,
|
1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const label = this.text_factory.build(GL, transform);
|
const label = this.msdf_text_factory.build(GL, transform);
|
||||||
this.planet_labels.push(label);
|
this.planet_labels.push(label);
|
||||||
this.renderer.addRenderable(label.getRenderable(), LAYERS.planet_label);
|
this.renderer.addRenderable(label.getRenderable(), LAYERS.planet_label);
|
||||||
}
|
}
|
||||||
|
@ -330,7 +337,7 @@ export class GameInstance {
|
||||||
|
|
||||||
this.planet_labels[i].setText(
|
this.planet_labels[i].setText(
|
||||||
GL,
|
GL,
|
||||||
"*" + planet_ships[i],
|
"" + planet_ships[i],
|
||||||
Align.Middle,
|
Align.Middle,
|
||||||
Align.Begin
|
Align.Begin
|
||||||
);
|
);
|
||||||
|
@ -375,7 +382,7 @@ export class GameInstance {
|
||||||
|
|
||||||
const renderable = new DefaultRenderable(ib, vao, this.masked_image_shader, [this.ship_texture], {});
|
const renderable = new DefaultRenderable(ib, vao, this.masked_image_shader, [this.ship_texture], {});
|
||||||
this.renderer.addRenderable(renderable, LAYERS.ship);
|
this.renderer.addRenderable(renderable, LAYERS.ship);
|
||||||
const label = this.text_factory.build(GL);
|
const label = this.msdf_text_factory.build(GL);
|
||||||
|
|
||||||
this.ship_labels.push(label);
|
this.ship_labels.push(label);
|
||||||
this.renderer.addRenderable(label.getRenderable(), LAYERS.ship_label);
|
this.renderer.addRenderable(label.getRenderable(), LAYERS.ship_label);
|
||||||
|
@ -451,10 +458,11 @@ export class GameInstance {
|
||||||
this.shader,
|
this.shader,
|
||||||
this.image_shader,
|
this.image_shader,
|
||||||
this.masked_image_shader,
|
this.masked_image_shader,
|
||||||
|
this.msdf_shader,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
// If not playing, still reder with different viewbox, so people can still pan etc.
|
// If not playing, still render with different viewbox, so that panning is still possible
|
||||||
if (!this.playing) {
|
if (!this.playing) {
|
||||||
this.last_time = time;
|
this.last_time = time;
|
||||||
|
|
||||||
|
@ -595,6 +603,7 @@ export async function set_instance(source: string): Promise<GameInstance> {
|
||||||
loadImage(assets.fontPng),
|
loadImage(assets.fontPng),
|
||||||
loadImage(assets.shipPng),
|
loadImage(assets.shipPng),
|
||||||
loadImage(assets.earthPng),
|
loadImage(assets.earthPng),
|
||||||
|
loadImage(assets.robotoMsdfPng),
|
||||||
];
|
];
|
||||||
|
|
||||||
const shader_promies = [
|
const shader_promies = [
|
||||||
|
@ -630,7 +639,14 @@ export async function set_instance(source: string): Promise<GameInstance> {
|
||||||
assets.simpleVertexShader,
|
assets.simpleVertexShader,
|
||||||
),
|
),
|
||||||
])(),
|
])(),
|
||||||
|
(async () =>
|
||||||
|
<[string, ShaderFactory]>[
|
||||||
|
"msdf",
|
||||||
|
await ShaderFactory.create_factory(
|
||||||
|
assets.msdfFragmentShader,
|
||||||
|
assets.simpleVertexShader,
|
||||||
|
),
|
||||||
|
])(),
|
||||||
];
|
];
|
||||||
let shaders_array: [string, ShaderFactory][];
|
let shaders_array: [string, ShaderFactory][];
|
||||||
[texture_images, shaders_array] = await Promise.all([
|
[texture_images, shaders_array] = await Promise.all([
|
||||||
|
@ -646,12 +662,15 @@ export async function set_instance(source: string): Promise<GameInstance> {
|
||||||
const fontTexture = Texture.fromImage(GL, texture_images[0], "font");
|
const fontTexture = Texture.fromImage(GL, texture_images[0], "font");
|
||||||
const shipTexture = Texture.fromImage(GL, texture_images[1], "ship");
|
const shipTexture = Texture.fromImage(GL, texture_images[1], "ship");
|
||||||
const earthTexture = Texture.fromImage(GL, texture_images[2], "earth");
|
const earthTexture = Texture.fromImage(GL, texture_images[2], "earth");
|
||||||
|
const robotoMsdfTexture = Texture.fromImage(GL, texture_images[3], "robotoMsdf");
|
||||||
|
|
||||||
|
|
||||||
game_instance = new GameInstance(
|
game_instance = new GameInstance(
|
||||||
Game.new(source),
|
Game.new(source),
|
||||||
[earthTexture],
|
[earthTexture],
|
||||||
shipTexture,
|
shipTexture,
|
||||||
fontTexture,
|
fontTexture,
|
||||||
|
robotoMsdfTexture,
|
||||||
shaders
|
shaders
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
181
web/pw-visualizer/src/webgl/msdf_text.ts
Normal file
181
web/pw-visualizer/src/webgl/msdf_text.ts
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
import { Shader, Uniform1f, Uniform4f, UniformMatrix3fv } from "./shader";
|
||||||
|
import { Texture } from "./texture";
|
||||||
|
import { DefaultRenderable } from "./renderer";
|
||||||
|
import { IndexBuffer, VertexBuffer } from "./buffer";
|
||||||
|
import { VertexBufferLayout, VertexArray } from "./vertexBufferLayout";
|
||||||
|
import { robotoMsdfJson } from "../assets";
|
||||||
|
import { GlypInfo } from "./text";
|
||||||
|
|
||||||
|
|
||||||
|
export enum Align {
|
||||||
|
Begin,
|
||||||
|
End,
|
||||||
|
Middle,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FontAtlas = {
|
||||||
|
atlas: AtlasMeta,
|
||||||
|
metrics: Metrics,
|
||||||
|
glyphs: Glyph[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AtlasMeta = {
|
||||||
|
type: string,
|
||||||
|
distanceRange: number,
|
||||||
|
size: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
yOrigin: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Metrics = {
|
||||||
|
emSize: number,
|
||||||
|
lineHeight: number,
|
||||||
|
ascender: number,
|
||||||
|
descender: number,
|
||||||
|
underlineY: number,
|
||||||
|
underlineThickness: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type Glyph = {
|
||||||
|
unicode: number,
|
||||||
|
advance: number,
|
||||||
|
planeBounds?: Bounds,
|
||||||
|
atlasBounds?: Bounds,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Bounds = {
|
||||||
|
left: number,
|
||||||
|
bottom: number,
|
||||||
|
right: number,
|
||||||
|
top: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class MsdfLabelFactory {
|
||||||
|
texture: Texture;
|
||||||
|
font: FontAtlas;
|
||||||
|
shader: Shader;
|
||||||
|
|
||||||
|
constructor(gl: WebGLRenderingContext, fontTexture: Texture, font: FontAtlas, shader: Shader) {
|
||||||
|
this.texture = fontTexture;
|
||||||
|
this.font = font;
|
||||||
|
this.shader = shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
build(gl: WebGLRenderingContext, transform?: UniformMatrix3fv): Label {
|
||||||
|
return new Label(gl, this.shader, this.texture, this.font, transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Label {
|
||||||
|
inner: DefaultRenderable;
|
||||||
|
|
||||||
|
font: FontAtlas;
|
||||||
|
charAtlas: {[unicodeNumber: number]: Glyph};
|
||||||
|
|
||||||
|
constructor(gl: WebGLRenderingContext, shader: Shader, tex: Texture, font: FontAtlas, transform: UniformMatrix3fv) {
|
||||||
|
this.font = font;
|
||||||
|
this.charAtlas = {}
|
||||||
|
this.font.glyphs.forEach((glyph) => {
|
||||||
|
this.charAtlas[glyph.unicode] = glyph;
|
||||||
|
});
|
||||||
|
|
||||||
|
const uniforms = {
|
||||||
|
"u_trans": transform,
|
||||||
|
"u_trans_next": transform,
|
||||||
|
"u_fgColor": new Uniform4f([1.0, 1.0, 1.0, 1.0]),
|
||||||
|
"u_bgColor": new Uniform4f([0.0, 0.0, 0.0, 1.0]),
|
||||||
|
"u_distanceRange": new Uniform1f(font.atlas.distanceRange),
|
||||||
|
"u_glyphSize": new Uniform1f(font.atlas.size),
|
||||||
|
};
|
||||||
|
const ib = new IndexBuffer(gl, []);
|
||||||
|
const vb_pos = new VertexBuffer(gl, []);
|
||||||
|
const vb_tex = new VertexBuffer(gl, []);
|
||||||
|
|
||||||
|
const layout_pos = new VertexBufferLayout();
|
||||||
|
layout_pos.push(gl.FLOAT, 2, 4, "a_position");
|
||||||
|
|
||||||
|
const layout_tex = new VertexBufferLayout();
|
||||||
|
layout_tex.push(gl.FLOAT, 2, 4, "a_texCoord");
|
||||||
|
|
||||||
|
const vao = new VertexArray();
|
||||||
|
vao.addBuffer(vb_pos, layout_pos);
|
||||||
|
vao.addBuffer(vb_tex, layout_tex);
|
||||||
|
|
||||||
|
this.inner = new DefaultRenderable(ib, vao, shader, [tex], uniforms);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRenderable(): DefaultRenderable {
|
||||||
|
return this.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
setText(gl: WebGLRenderingContext, text: string, h_align = Align.Begin, v_align = Align.Begin) {
|
||||||
|
const idxs = [];
|
||||||
|
const verts_pos = [];
|
||||||
|
const verts_tex = [];
|
||||||
|
|
||||||
|
let xPos = 0;
|
||||||
|
let yPos = 0;
|
||||||
|
switch (v_align) {
|
||||||
|
case Align.Begin:
|
||||||
|
yPos = -1;
|
||||||
|
break;
|
||||||
|
case Align.End:
|
||||||
|
yPos = 0;
|
||||||
|
break;
|
||||||
|
case Align.Middle:
|
||||||
|
yPos = -0.5;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// track position in the index buffer
|
||||||
|
let bufPos = 0;
|
||||||
|
for (let charIndex = 0; charIndex < text.length; charIndex++) {
|
||||||
|
let char = this.charAtlas[text.charCodeAt(charIndex)]
|
||||||
|
if (char.atlasBounds && char.planeBounds) {
|
||||||
|
verts_pos.push(xPos + char.planeBounds.left, yPos-char.planeBounds.top);
|
||||||
|
verts_pos.push(xPos + char.planeBounds.right, yPos-char.planeBounds.top);
|
||||||
|
verts_pos.push(xPos + char.planeBounds.left, yPos-char.planeBounds.bottom);
|
||||||
|
verts_pos.push(xPos + char.planeBounds.right, yPos-char.planeBounds.bottom);
|
||||||
|
|
||||||
|
const atlasWidth = this.font.atlas.width;
|
||||||
|
const atlasHeight = this.font.atlas.height;
|
||||||
|
|
||||||
|
verts_tex.push(char.atlasBounds.left / atlasWidth, char.atlasBounds.top / atlasHeight);
|
||||||
|
verts_tex.push(char.atlasBounds.right / atlasWidth, char.atlasBounds.top / atlasHeight);
|
||||||
|
verts_tex.push(char.atlasBounds.left / atlasWidth, char.atlasBounds.bottom / atlasHeight);
|
||||||
|
verts_tex.push(char.atlasBounds.right / atlasWidth, char.atlasBounds.bottom / atlasHeight);
|
||||||
|
|
||||||
|
idxs.push(bufPos+0, bufPos+1, bufPos+2);
|
||||||
|
idxs.push(bufPos+1, bufPos+2, bufPos+3);
|
||||||
|
bufPos += 4;
|
||||||
|
}
|
||||||
|
xPos += char.advance;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shift = 0;
|
||||||
|
switch (h_align) {
|
||||||
|
case Align.End:
|
||||||
|
shift = xPos;
|
||||||
|
break;
|
||||||
|
case Align.Middle:
|
||||||
|
shift = xPos / 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < verts_pos.length; i += 2) {
|
||||||
|
verts_pos[i] -= shift;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.inner.updateIndexBuffer(gl, idxs);
|
||||||
|
this.inner.updateVAOBuffer(gl, 0, verts_pos);
|
||||||
|
this.inner.updateVAOBuffer(gl, 1, verts_tex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultMsdfLabelFactory(gl: WebGLRenderingContext, fontTexture: Texture, shader: Shader): MsdfLabelFactory {
|
||||||
|
return new MsdfLabelFactory(gl, fontTexture, robotoMsdfJson, shader);
|
||||||
|
}
|
|
@ -67,8 +67,14 @@ export class Texture {
|
||||||
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
if (name == "font") {
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
|
} else {
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA,
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA,
|
||||||
gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255]));
|
gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255]));
|
||||||
|
|
|
@ -74,6 +74,7 @@ export class Resizer {
|
||||||
mouse_pos = [0, 0];
|
mouse_pos = [0, 0];
|
||||||
last_drag = [0, 0];
|
last_drag = [0, 0];
|
||||||
|
|
||||||
|
// x, y, w, h
|
||||||
viewbox: number[];
|
viewbox: number[];
|
||||||
orig_viewbox: number[];
|
orig_viewbox: number[];
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue