mirror of https://gitee.com/antv-l7/antv-l7
feat: 增加对于点要素的自动标注
This commit is contained in:
parent
6c06e4676d
commit
98383a34e0
|
@ -6,7 +6,7 @@
|
|||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<meta name="geometry" content="diagram">
|
||||
<link rel="stylesheet" href="./assets/common.css">
|
||||
<title>point_circle</title>
|
||||
<title>text layer</title>
|
||||
<style>
|
||||
#map { position:absolute; top:0; bottom:0; width:100%; }
|
||||
</style>
|
||||
|
@ -29,18 +29,39 @@ const scene = new L7.Scene({
|
|||
});
|
||||
window.scene = scene;
|
||||
scene.on('loaded', () => {
|
||||
$.get('./data/provincePoint.json', data => {
|
||||
$.get('https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_regions_points.geojson', data => {
|
||||
scene.PointLayer({
|
||||
zIndex: 2
|
||||
zIndex: 3
|
||||
})
|
||||
.source(data)
|
||||
.shape('circle')
|
||||
.active(true)
|
||||
.size(4)
|
||||
.color('#fff')
|
||||
.style({
|
||||
stroke: '#999',
|
||||
strokeWidth: 1,
|
||||
opacity: 1.0
|
||||
})
|
||||
.render();
|
||||
|
||||
scene.TextLayer({
|
||||
zIndex: 4
|
||||
})
|
||||
.source(data)
|
||||
.shape('name', 'text')
|
||||
.active(true)
|
||||
.size(12) // default 1
|
||||
.color('name')
|
||||
.size('scalerank', [ 10, 20, 24 ])
|
||||
.color('scalerank', [ 'red', 'blue', 'black' ])
|
||||
.style({
|
||||
stroke: '#999',
|
||||
strokeWidth: 0,
|
||||
// fontFamily: 'Monaco, monospace', // 字体
|
||||
fontWeight: 400,
|
||||
textAnchor: 'center', // 文本相对锚点的位置 center|left|right|top|bottom|top-left
|
||||
textOffset: [ 0, 0 ], // 文本相对锚点的偏移量 [水平, 垂直]
|
||||
spacing: 2, // 字符间距
|
||||
padding: [ 4, 4 ], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近
|
||||
strokeColor: 'white', // 描边颜色
|
||||
strokeWidth: 2, // 描边宽度
|
||||
opacity: 1.0
|
||||
})
|
||||
.render();
|
||||
|
|
|
@ -21,6 +21,7 @@ export default class Scene extends Base {
|
|||
this.fontAtlasManager = new FontAtlasManager();
|
||||
this._layers = [];
|
||||
this.animateCount = 0;
|
||||
this.inited = false;
|
||||
}
|
||||
|
||||
_initEngine(mapContainer) {
|
||||
|
@ -44,6 +45,10 @@ export default class Scene extends Base {
|
|||
this.map = Map.map;
|
||||
this._initEngine(Map.renderDom);
|
||||
Map.asyncCamera(this._engine);
|
||||
|
||||
// 等待相机同步之后再进行首次渲染
|
||||
Map.on('cameraloaded', () => {
|
||||
if (!this.inited) {
|
||||
this.initLayer();
|
||||
this._registEvents();
|
||||
const hash = this.get('hash');
|
||||
|
@ -53,7 +58,11 @@ export default class Scene extends Base {
|
|||
interaction._onHashChange();
|
||||
}
|
||||
this.emit('loaded');
|
||||
this.inited = true;
|
||||
}
|
||||
this._engine.update();
|
||||
this.emit('cameraloaded');
|
||||
});
|
||||
});
|
||||
}
|
||||
initLayer() {
|
||||
|
|
|
@ -1,130 +1,165 @@
|
|||
export default function TextBuffer(layerData, fontAtlasManager) {
|
||||
/**
|
||||
* 为文本构建顶点数据,仅支持点要素自动标注。
|
||||
* @see https://zhuanlan.zhihu.com/p/72222549
|
||||
* @see https://zhuanlan.zhihu.com/p/74373214
|
||||
*/
|
||||
import { shapeText, getGlyphQuads } from '../../../util/symbol-layout';
|
||||
|
||||
export default function TextBuffer(
|
||||
layerData,
|
||||
sourceData,
|
||||
options,
|
||||
fontAtlasManager,
|
||||
collisionIndex,
|
||||
mvpMatrix
|
||||
) {
|
||||
const {
|
||||
textField,
|
||||
fontWeight,
|
||||
fontFamily
|
||||
} = options;
|
||||
const characterSet = [];
|
||||
layerData.forEach(element => {
|
||||
let text = element.shape || '';
|
||||
sourceData.forEach(element => {
|
||||
// shape 存储了 text-field
|
||||
let text = element[textField] || '';
|
||||
text = text.toString();
|
||||
for (let j = 0; j < text.length; j++) {
|
||||
// 去重
|
||||
if (characterSet.indexOf(text[j]) === -1) {
|
||||
characterSet.push(text[j]);
|
||||
}
|
||||
}
|
||||
});
|
||||
fontAtlasManager.setProps({
|
||||
characterSet
|
||||
characterSet,
|
||||
fontFamily,
|
||||
fontWeight
|
||||
});
|
||||
const attr = drawGlyph(layerData, fontAtlasManager);
|
||||
return attr;
|
||||
return drawGlyph(layerData, sourceData, options, fontAtlasManager, collisionIndex, mvpMatrix);
|
||||
}
|
||||
function drawGlyph(layerData, fontAtlasManager) {
|
||||
|
||||
function drawGlyph(
|
||||
layerData, sourceData,
|
||||
{
|
||||
textField,
|
||||
spacing = 2,
|
||||
textAnchor = 'center',
|
||||
textOffset = [ 0, 0 ],
|
||||
padding = [ 4, 4 ]
|
||||
},
|
||||
fontAtlasManager,
|
||||
collisionIndex,
|
||||
mvpMatrix
|
||||
) {
|
||||
const { texture, fontAtlas, mapping } = fontAtlasManager;
|
||||
|
||||
const attributes = {
|
||||
originPoints: [],
|
||||
textSizes: [],
|
||||
textOffsets: [],
|
||||
fontAtlas,
|
||||
texture,
|
||||
positions: [],
|
||||
colors: [],
|
||||
textureElements: [],
|
||||
pickingIds: []
|
||||
pickingIds: [],
|
||||
textUVs: [],
|
||||
textOffsets: [],
|
||||
textSizes: [],
|
||||
index: []
|
||||
};
|
||||
const { texture, fontAtlas, mapping, scale } = fontAtlasManager;
|
||||
layerData.forEach(function(element) {
|
||||
const size = element.size;
|
||||
const pos = element.coordinates;
|
||||
let text = element.shape || '';
|
||||
text = text.toString();
|
||||
const pen = {
|
||||
x: (-text.length * size) / 2,
|
||||
y: 0
|
||||
};
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const metric = mapping[text[i]];
|
||||
const { x, y, width, height } = metric;
|
||||
const color = element.color;
|
||||
const offsetX = pen.x;
|
||||
const offsetY = pen.y;
|
||||
let indexCounter = 0;
|
||||
layerData.forEach((feature, i) => {
|
||||
const { size, coordinates } = feature;
|
||||
// 根据字段获取文本
|
||||
const text = `${sourceData[i][textField] || ''}`;
|
||||
// sdf 中默认字号为 24
|
||||
const fontScale = size / 24;
|
||||
|
||||
// 1. 计算每个字符相对锚点的位置
|
||||
const shaping = shapeText(text, mapping, 24, textAnchor, 'center', spacing, textOffset);
|
||||
|
||||
if (shaping) {
|
||||
// 2. 尝试加入空间索引,获取碰撞检测结果
|
||||
// TODO:按照 feature 中指定字段排序,确定插入权重,保证优先级高的文本优先展示
|
||||
const { box } = collisionIndex.placeCollisionBox({
|
||||
x1: shaping.left * fontScale - padding[0],
|
||||
x2: shaping.right * fontScale + padding[0],
|
||||
y1: shaping.top * fontScale - padding[1],
|
||||
y2: shaping.bottom * fontScale + padding[1],
|
||||
// 点要素锚点就是当前点位置
|
||||
anchorPointX: coordinates[0],
|
||||
anchorPointY: coordinates[1]
|
||||
}, mvpMatrix);
|
||||
|
||||
// 无碰撞则加入空间索引
|
||||
if (box && box.length) {
|
||||
// TODO:featureIndex
|
||||
collisionIndex.insertCollisionBox(box, 0);
|
||||
|
||||
// 3. 计算可供渲染的文本块,其中每个字符都包含纹理坐标
|
||||
const glyphQuads = getGlyphQuads(shaping, textOffset, false);
|
||||
|
||||
// 4. 构建顶点数据,四个顶点组成一个 quad
|
||||
indexCounter = addAttributeForFeature(feature, attributes, glyphQuads, indexCounter);
|
||||
}
|
||||
}
|
||||
});
|
||||
return attributes;
|
||||
}
|
||||
|
||||
function addAttributeForFeature(feature, attributes, glyphQuads, indexCounter) {
|
||||
const { id, size, color, coordinates } = feature;
|
||||
glyphQuads.forEach(quad => {
|
||||
|
||||
attributes.pickingIds.push(
|
||||
element.id,
|
||||
element.id,
|
||||
element.id,
|
||||
element.id,
|
||||
element.id,
|
||||
element.id
|
||||
);
|
||||
attributes.textOffsets.push(
|
||||
// 文字在词语的偏移量
|
||||
offsetX,
|
||||
offsetY,
|
||||
offsetX,
|
||||
offsetY,
|
||||
offsetX,
|
||||
offsetY,
|
||||
offsetX,
|
||||
offsetY,
|
||||
offsetX,
|
||||
offsetY,
|
||||
offsetX,
|
||||
offsetY
|
||||
);
|
||||
attributes.originPoints.push(
|
||||
// 词语的经纬度坐标
|
||||
pos[0],
|
||||
pos[1],
|
||||
0,
|
||||
pos[0],
|
||||
pos[1],
|
||||
0,
|
||||
pos[0],
|
||||
pos[1],
|
||||
0,
|
||||
pos[0],
|
||||
pos[1],
|
||||
0,
|
||||
pos[0],
|
||||
pos[1],
|
||||
0,
|
||||
pos[0],
|
||||
pos[1],
|
||||
0
|
||||
);
|
||||
attributes.textSizes.push(
|
||||
size,
|
||||
size * scale,
|
||||
0,
|
||||
size * scale,
|
||||
0,
|
||||
0,
|
||||
size,
|
||||
size * scale,
|
||||
0,
|
||||
0,
|
||||
size,
|
||||
0
|
||||
id,
|
||||
id,
|
||||
id,
|
||||
id
|
||||
);
|
||||
|
||||
attributes.colors.push(
|
||||
...color,
|
||||
...color,
|
||||
...color,
|
||||
...color,
|
||||
...color,
|
||||
...color
|
||||
);
|
||||
attributes.textureElements.push(
|
||||
// 文字纹理坐标
|
||||
x + width,
|
||||
y,
|
||||
x,
|
||||
y,
|
||||
x,
|
||||
y + height,
|
||||
x + width,
|
||||
y,
|
||||
x,
|
||||
y + height,
|
||||
x + width,
|
||||
y + height
|
||||
|
||||
attributes.positions.push(
|
||||
coordinates[0], coordinates[1],
|
||||
coordinates[0], coordinates[1],
|
||||
coordinates[0], coordinates[1],
|
||||
coordinates[0], coordinates[1]
|
||||
);
|
||||
pen.x = pen.x + size;
|
||||
}
|
||||
|
||||
attributes.textUVs.push(
|
||||
quad.tex.x, quad.tex.y + quad.tex.height,
|
||||
quad.tex.x + quad.tex.width, quad.tex.y + quad.tex.height,
|
||||
quad.tex.x + quad.tex.width, quad.tex.y,
|
||||
quad.tex.x, quad.tex.y,
|
||||
);
|
||||
|
||||
attributes.textOffsets.push(
|
||||
quad.tl.x, quad.tl.y,
|
||||
quad.tr.x, quad.tr.y,
|
||||
quad.br.x, quad.br.y,
|
||||
quad.bl.x, quad.bl.y
|
||||
);
|
||||
|
||||
attributes.textSizes.push(
|
||||
size,
|
||||
size,
|
||||
size,
|
||||
size
|
||||
);
|
||||
|
||||
attributes.index.push(
|
||||
0 + indexCounter,
|
||||
1 + indexCounter,
|
||||
2 + indexCounter,
|
||||
2 + indexCounter,
|
||||
3 + indexCounter,
|
||||
0 + indexCounter
|
||||
);
|
||||
indexCounter += 4;
|
||||
});
|
||||
attributes.texture = texture;
|
||||
attributes.fontAtlas = fontAtlas;
|
||||
return attributes;
|
||||
|
||||
return indexCounter;
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ export const DEFAULT_BUFFER = 3;
|
|||
export const DEFAULT_CUTOFF = 0.25;
|
||||
export const DEFAULT_RADIUS = 8;
|
||||
const MAX_CANVAS_WIDTH = 1024;
|
||||
const BASELINE_SCALE = 0.9;
|
||||
const HEIGHT_SCALE = 1.2;
|
||||
const BASELINE_SCALE = 1.0;
|
||||
const HEIGHT_SCALE = 1.0;
|
||||
const CACHE_LIMIT = 3;
|
||||
const cache = new LRUCache(CACHE_LIMIT);
|
||||
|
||||
|
@ -36,9 +36,9 @@ function getDefaultCharacterSet() {
|
|||
|
||||
function setTextStyle(ctx, fontFamily, fontSize, fontWeight) {
|
||||
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.textBaseline = 'baseline';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.textBaseline = 'middle';
|
||||
// ctx.textAlign = 'left';
|
||||
}
|
||||
function getNewChars(key, characterSet) {
|
||||
const cachedFontAtlas = cache.get(key);
|
||||
|
@ -146,10 +146,10 @@ export default class FontAtlasManager {
|
|||
|
||||
_updateTexture({ data: canvas }) {
|
||||
this._texture = new THREE.CanvasTexture(canvas);
|
||||
this._texture.wrapS = THREE.ClampToEdgeWrapping;
|
||||
this._texture.wrapT = THREE.ClampToEdgeWrapping;
|
||||
this._texture.minFilter = THREE.LinearFilter;
|
||||
this._texture.magFilter = THREE.LinearFilter;
|
||||
this._texture.flipY = false;
|
||||
this._texture.format = THREE.AlphaFormat;
|
||||
this._texture.needUpdate = true;
|
||||
}
|
||||
|
||||
|
@ -200,7 +200,8 @@ export default class FontAtlasManager {
|
|||
|
||||
for (const char of characterSet) {
|
||||
populateAlphaChannel(tinySDF.draw(char), imageData);
|
||||
ctx.putImageData(imageData, mapping[char].x - buffer, mapping[char].y - buffer);
|
||||
// 考虑到描边,需要保留 sdf 的 buffer,不能像 deck.gl 一样直接减去
|
||||
ctx.putImageData(imageData, mapping[char].x, mapping[char].y);
|
||||
}
|
||||
} else {
|
||||
for (const char of characterSet) {
|
||||
|
|
|
@ -1,23 +1,14 @@
|
|||
import Material from './material';
|
||||
import { getModule } from '../../util/shaderModule';
|
||||
import { getModule, wrapUniforms } from '../../util/shaderModule';
|
||||
import merge from '@antv/util/lib/deep-mix';
|
||||
|
||||
export default function TextMaterial(options) {
|
||||
const { vs, fs } = getModule('text');
|
||||
export default function TextMaterial(_uniforms) {
|
||||
const { vs, fs, uniforms } = getModule('text');
|
||||
const material = new Material({
|
||||
uniforms: {
|
||||
u_opacity: { value: options.u_opacity || 1.0 },
|
||||
u_texture: { value: options.u_texture },
|
||||
u_strokeWidth: { value: options.u_strokeWidth },
|
||||
u_stroke: { value: options.u_stroke },
|
||||
u_textTextureSize: { value: options.u_textTextureSize },
|
||||
u_scale: { value: options.u_scale },
|
||||
u_gamma: { value: options.u_gamma },
|
||||
u_buffer: { value: options.u_buffer },
|
||||
u_glSize: { value: options.u_glSize },
|
||||
u_activeId: { value: options.u_activeId || 0 },
|
||||
u_activeColor: { value: options.u_activeColor }
|
||||
|
||||
defines: {
|
||||
DEVICE_PIXEL_RATIO: window.devicePixelRatio
|
||||
},
|
||||
uniforms: wrapUniforms(merge(uniforms, _uniforms)),
|
||||
vertexShader: vs,
|
||||
fragmentShader: fs,
|
||||
transparent: true
|
||||
|
|
|
@ -1,31 +1,30 @@
|
|||
precision mediump float;
|
||||
uniform sampler2D u_texture;
|
||||
#define SDF_PX 8.0
|
||||
#define EDGE_GAMMA 0.105 / float(DEVICE_PIXEL_RATIO)
|
||||
|
||||
uniform sampler2D u_sdf_map;
|
||||
uniform float u_gamma_scale : 0.5;
|
||||
uniform float u_font_size : 24;
|
||||
uniform float u_font_opacity : 1.0;
|
||||
uniform vec4 u_halo_color : [0, 0, 0, 1];
|
||||
uniform float u_halo_width : 2.0;
|
||||
uniform float u_halo_blur : 0.5;
|
||||
|
||||
varying vec4 v_color;
|
||||
uniform vec4 u_stroke;
|
||||
uniform float u_strokeWidth;
|
||||
uniform float u_buffer;
|
||||
uniform float u_gamma;
|
||||
uniform float u_opacity;
|
||||
varying vec2 v_texcoord;
|
||||
varying float v_size;
|
||||
void main(){
|
||||
float dist=texture2D(u_texture,vec2(v_texcoord.x,v_texcoord.y)).a;
|
||||
float alpha;
|
||||
varying vec2 v_uv;
|
||||
varying float v_gamma_scale;
|
||||
|
||||
if(u_strokeWidth==0.){
|
||||
alpha=smoothstep(u_buffer-u_gamma,u_buffer+u_gamma,dist);
|
||||
gl_FragColor=vec4(v_color.rgb,alpha*v_color.a);
|
||||
}else{
|
||||
if(dist<=u_buffer-u_gamma){
|
||||
alpha=smoothstep(u_strokeWidth-u_gamma,u_strokeWidth+u_gamma,dist);
|
||||
gl_FragColor=vec4(u_stroke.rgb,alpha*u_stroke.a);
|
||||
}else if(dist<u_buffer){
|
||||
alpha=smoothstep(u_buffer-u_gamma,u_buffer+u_gamma,dist);
|
||||
gl_FragColor=vec4(alpha*v_color.rgb+(1.-alpha)*u_stroke.rgb,1.*v_color.a*alpha+(1.-alpha)*u_stroke.a);
|
||||
}else{
|
||||
alpha=1.;
|
||||
gl_FragColor=vec4(v_color.rgb,alpha*v_color.a);
|
||||
}
|
||||
void main() {
|
||||
// get sdf from atlas
|
||||
float dist = texture2D(u_sdf_map, v_uv).a;
|
||||
|
||||
}
|
||||
float fontScale = u_font_size / 24.0;
|
||||
|
||||
lowp float buff = (6.0 - u_halo_width / fontScale) / SDF_PX;
|
||||
highp float gamma = (u_halo_blur * 1.19 / SDF_PX + EDGE_GAMMA) / (fontScale * u_gamma_scale);
|
||||
|
||||
highp float gamma_scaled = gamma * v_gamma_scale;
|
||||
|
||||
highp float alpha = smoothstep(buff - gamma_scaled, buff + gamma_scaled, dist);
|
||||
|
||||
gl_FragColor = mix(v_color * u_font_opacity, u_halo_color, smoothstep(0., .5, 1. - dist)) * alpha;
|
||||
}
|
|
@ -1,26 +1,35 @@
|
|||
precision mediump float;
|
||||
attribute vec2 a_txtsize;
|
||||
attribute vec2 a_txtOffsets;
|
||||
uniform float u_opacity;
|
||||
attribute vec2 a_pos;
|
||||
attribute vec2 a_tex;
|
||||
attribute vec2 a_offset;
|
||||
attribute vec4 a_color;
|
||||
uniform vec2 u_textTextureSize;// 纹理大小
|
||||
uniform vec2 u_glSize;
|
||||
varying vec2 v_texcoord;
|
||||
attribute float a_size;
|
||||
|
||||
uniform vec2 u_sdf_map_size;
|
||||
uniform vec2 u_viewport_size;
|
||||
|
||||
uniform float u_activeId : 0;
|
||||
uniform vec4 u_activeColor : [1.0, 0.0, 0.0, 1.0];
|
||||
|
||||
varying vec2 v_uv;
|
||||
varying float v_gamma_scale;
|
||||
varying vec4 v_color;
|
||||
varying float v_size;
|
||||
uniform float u_activeId;
|
||||
uniform vec4 u_activeColor;
|
||||
|
||||
void main(){
|
||||
mat4 matModelViewProjection=projectionMatrix*modelViewMatrix;
|
||||
vec4 cur_position=matModelViewProjection*vec4(position.xy,0,1);
|
||||
v_size = 12. / u_glSize.x;
|
||||
gl_Position=cur_position/cur_position.w+vec4((a_txtOffsets+a_txtsize)/u_glSize*2.,0.,0.);
|
||||
v_color=vec4(a_color.rgb,a_color.a*u_opacity);
|
||||
if(pickingId==u_activeId){
|
||||
v_color=u_activeColor;
|
||||
void main() {
|
||||
v_color = a_color;
|
||||
v_uv = a_tex / u_sdf_map_size;
|
||||
|
||||
// 文本缩放比例
|
||||
float fontScale = a_size / 24.;
|
||||
|
||||
// 投影到屏幕空间 + 偏移量
|
||||
vec4 projected_position = projectionMatrix * modelViewMatrix * vec4(a_pos, 0., 1.);
|
||||
gl_Position = vec4(projected_position.xy / projected_position.w
|
||||
+ a_offset * fontScale / u_viewport_size * 2., 0.0, 1.0);
|
||||
|
||||
v_gamma_scale = gl_Position.w;
|
||||
|
||||
if (pickingId == u_activeId) {
|
||||
v_color = u_activeColor;
|
||||
}
|
||||
v_texcoord=uv/u_textTextureSize;
|
||||
worldId=id_toPickColor(pickingId);
|
||||
|
||||
worldId = id_toPickColor(pickingId);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import HeatmapLayer from './heatmapLayer';
|
|||
import TileLayer from './tile/tileLayer';
|
||||
import ImageTileLayer from './tile/imageTileLayer';
|
||||
import VectorTileLayer from './tile/VectorTileLayer';
|
||||
import TextLayer from './textLayer';
|
||||
|
||||
registerLayer('PolygonLayer', PolygonLayer);
|
||||
registerLayer('PointLayer', PointLayer);
|
||||
|
@ -18,6 +19,7 @@ registerLayer('HeatmapLayer', HeatmapLayer);
|
|||
registerLayer('TileLayer', TileLayer);
|
||||
registerLayer('ImageTileLayer', ImageTileLayer);
|
||||
registerLayer('VectorTileLayer', VectorTileLayer);
|
||||
registerLayer('TextLayer', TextLayer);
|
||||
|
||||
export { LAYER_MAP, getLayer } from './factory';
|
||||
export { registerLayer };
|
||||
|
|
|
@ -48,4 +48,10 @@ import DrawImage from './image/drawImage';
|
|||
|
||||
registerRender('image', 'image', DrawImage);
|
||||
|
||||
// image
|
||||
|
||||
import DrawText from './text/drawText';
|
||||
|
||||
registerRender('text', 'text', DrawText);
|
||||
|
||||
export { getRender };
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
import * as THREE from '../../../core/three';
|
||||
import TextMaterial from '../../../geom/material/textMaterial';
|
||||
import TextBuffer from '../../../geom/buffer/point/text';
|
||||
import ColorUtil from '../../../attr/color-util';
|
||||
|
||||
export default function DrawText(layerData, layer, updateGeometry = false) {
|
||||
const style = layer.get('styleOptions');
|
||||
const activeOption = layer.get('activedOptions');
|
||||
|
||||
const { strokeWidth, strokeColor, opacity,
|
||||
fontFamily, fontWeight, spacing, textAnchor, textOffset, padding } = style;
|
||||
|
||||
const { width, height } = layer.scene.getSize();
|
||||
const { _camera: { projectionMatrix, matrixWorldInverse } } = layer.scene._engine;
|
||||
|
||||
// 计算 MVP 矩阵
|
||||
const mvpMatrix = new THREE.Matrix4()
|
||||
.copy(projectionMatrix)
|
||||
.multiply(matrixWorldInverse);
|
||||
|
||||
const {
|
||||
index,
|
||||
positions,
|
||||
pickingIds,
|
||||
texture,
|
||||
colors,
|
||||
textUVs,
|
||||
textOffsets,
|
||||
textSizes,
|
||||
fontAtlas
|
||||
} = new TextBuffer(
|
||||
layerData,
|
||||
layer.layerSource.data.dataArray,
|
||||
{
|
||||
textField: layer.textField,
|
||||
fontFamily,
|
||||
fontWeight,
|
||||
spacing,
|
||||
textAnchor,
|
||||
textOffset,
|
||||
padding
|
||||
},
|
||||
layer.scene.fontAtlasManager,
|
||||
layer.collisionIndex,
|
||||
mvpMatrix
|
||||
);
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setIndex(index);
|
||||
geometry.addAttribute(
|
||||
'a_pos',
|
||||
new THREE.Float32BufferAttribute(positions, 2)
|
||||
);
|
||||
geometry.addAttribute(
|
||||
'pickingId',
|
||||
new THREE.Float32BufferAttribute(pickingIds, 1)
|
||||
);
|
||||
geometry.addAttribute(
|
||||
'a_color',
|
||||
new THREE.Float32BufferAttribute(colors, 4)
|
||||
);
|
||||
geometry.addAttribute(
|
||||
'a_tex',
|
||||
new THREE.Float32BufferAttribute(textUVs, 2)
|
||||
);
|
||||
geometry.addAttribute(
|
||||
'a_offset',
|
||||
new THREE.Float32BufferAttribute(textOffsets, 2)
|
||||
);
|
||||
geometry.addAttribute(
|
||||
'a_size',
|
||||
new THREE.Float32BufferAttribute(textSizes, 1)
|
||||
);
|
||||
|
||||
// 只需要更新顶点数据
|
||||
if (updateGeometry) {
|
||||
return geometry;
|
||||
}
|
||||
|
||||
const material = new TextMaterial({
|
||||
name: layer.layerId,
|
||||
u_sdf_map: texture,
|
||||
u_halo_color: ColorUtil.toRGB(strokeColor).map(e => e / 255),
|
||||
u_halo_width: strokeWidth,
|
||||
u_halo_blur: 0.5,
|
||||
u_font_opacity: opacity,
|
||||
u_sdf_map_size: [ fontAtlas.width, fontAtlas.height ],
|
||||
u_viewport_size: [ width, height ],
|
||||
u_activeColor: activeOption.fill
|
||||
});
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
// 更新 viewport
|
||||
window.addEventListener('resize', () => {
|
||||
const { width, height } = layer.scene.getSize();
|
||||
material.uniforms.u_viewport_size.value = [ width, height ];
|
||||
material.uniforms.needsUpdate = true;
|
||||
}, false);
|
||||
|
||||
// 关闭视锥裁剪
|
||||
mesh.frustumCulled = false;
|
||||
return mesh;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import Layer from '../core/layer';
|
||||
import { getRender } from './render/';
|
||||
import CollisionIndex from '../util/collision-index';
|
||||
export default class TextLayer extends Layer {
|
||||
shape(textField, shape = 'text') {
|
||||
this.textField = textField;
|
||||
this.shape = shape;
|
||||
|
||||
// 创建碰撞检测索引
|
||||
const { width, height } = this.scene.getSize();
|
||||
this.collisionIndex = new CollisionIndex(width, height);
|
||||
|
||||
// 相机变化,需要重新构建索引,由于文本可见性的改变,也需要重新组装顶点数据
|
||||
this.scene.on('cameraloaded', () => {
|
||||
this.collisionIndex = new CollisionIndex(width, height);
|
||||
|
||||
this.layerMesh.geometry = getRender(this.type, this.shape)(this.layerData, this, true);
|
||||
this.layerMesh.geometry.needsUpdate = true;
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
draw() {
|
||||
this.init();
|
||||
this.type = 'text';
|
||||
this.add(getRender(this.type, this.shape)(this.layerData, this));
|
||||
}
|
||||
}
|
|
@ -82,7 +82,9 @@ export default class GaodeMap extends Base {
|
|||
camera.lookAt(0, 0, 0);
|
||||
camera.position.x += e.camera.position.x;
|
||||
camera.position.y += -e.camera.position.y;
|
||||
this._engine.update();
|
||||
|
||||
// 相机同步成功,通知 scene 开始渲染
|
||||
this.emit('cameraloaded');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
// import GridIndex from 'grid-index';
|
||||
// @mapbox/grid-index 并没有类似 hitTest 的单纯获取碰撞检测结果的方法,query 将导致计算大量多余的包围盒结果,因此使用改良版
|
||||
import GridIndex from '../util/grid-index';
|
||||
import { Vector4 } from '../core/three';
|
||||
|
||||
// 为 viewport 加上 buffer,避免边缘处的文本无法显示
|
||||
const viewportPadding = 100;
|
||||
|
||||
/**
|
||||
* 基于网格实现文本避让,大幅提升包围盒碰撞检测效率
|
||||
* @see https://zhuanlan.zhihu.com/p/74373214
|
||||
*/
|
||||
export default class CollisionIndex {
|
||||
constructor(width, height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
// 创建网格索引
|
||||
this.grid = new GridIndex(width + 2 * viewportPadding, height + 2 * viewportPadding, 25);
|
||||
|
||||
this.screenRightBoundary = width + viewportPadding;
|
||||
this.screenBottomBoundary = height + viewportPadding;
|
||||
this.gridRightBoundary = width + 2 * viewportPadding;
|
||||
this.gridBottomBoundary = height + 2 * viewportPadding;
|
||||
}
|
||||
|
||||
placeCollisionBox(collisionBox, mvpMatrix) {
|
||||
const projectedPoint = this.project(mvpMatrix, collisionBox.anchorPointX, collisionBox.anchorPointY);
|
||||
|
||||
const tlX = collisionBox.x1 + projectedPoint.x;
|
||||
const tlY = collisionBox.y1 + projectedPoint.y;
|
||||
const brX = collisionBox.x2 + projectedPoint.x;
|
||||
const brY = collisionBox.y2 + projectedPoint.y;
|
||||
|
||||
if (!this.isInsideGrid(tlX, tlY, brX, brY) ||
|
||||
this.grid.hitTest(tlX, tlY, brX, brY)
|
||||
) {
|
||||
return {
|
||||
box: []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
box: [ tlX, tlY, brX, brY ]
|
||||
};
|
||||
}
|
||||
|
||||
insertCollisionBox(collisionBox, featureIndex) {
|
||||
const key = { featureIndex };
|
||||
this.grid.insert(key, collisionBox[0], collisionBox[1], collisionBox[2], collisionBox[3]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 后续碰撞检测都需要投影到 viewport 坐标系
|
||||
* @param {THREE.Matrix4} mvpMatrix mvp矩阵
|
||||
* @param {number} x P20 平面坐标X
|
||||
* @param {number} y P20 平面坐标Y
|
||||
* @return {Point} projectedPoint
|
||||
*/
|
||||
project(mvpMatrix, x, y) {
|
||||
const p = new Vector4(x, y, 0, 1)
|
||||
.applyMatrix4(mvpMatrix);
|
||||
|
||||
// GL 坐标系[-1, 1] -> viewport 坐标系[width, height]
|
||||
return {
|
||||
x: (((p.x / p.w + 1) / 2) * this.width) + viewportPadding,
|
||||
y: (((-p.y / p.w + 1) / 2) * this.height) + viewportPadding
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断包围盒是否在整个网格内,需要加上 buffer
|
||||
* @param {number} x1 x1
|
||||
* @param {number} y1 y1
|
||||
* @param {number} x2 x2
|
||||
* @param {number} y2 y2
|
||||
* @return {Point} isInside
|
||||
*/
|
||||
isInsideGrid(x1, y1, x2, y2) {
|
||||
return x2 >= 0 && x1 < this.gridRightBoundary && y2 >= 0 && y1 < this.gridBottomBoundary;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,8 @@
|
|||
/**
|
||||
* tiny-sdf 中每个 glyph 的宽度(加上 buffer 24 + 3 + 3 = 30)
|
||||
*/
|
||||
const glyphSizeInSDF = 30;
|
||||
|
||||
export function nextPowOfTwo(number) {
|
||||
return Math.pow(2, Math.ceil(Math.log2(number)));
|
||||
}
|
||||
|
@ -17,18 +22,23 @@ export function buildMapping({
|
|||
Array.from(characterSet).forEach((char, i) => {
|
||||
if (!mapping[char]) {
|
||||
const width = getFontWidth(char, i);
|
||||
if (x + width + buffer * 2 > maxCanvasWidth) {
|
||||
if (x + glyphSizeInSDF > maxCanvasWidth) {
|
||||
// if (x + width + buffer * 2 > maxCanvasWidth) {
|
||||
x = 0;
|
||||
row++;
|
||||
}
|
||||
mapping[char] = {
|
||||
x: x + buffer,
|
||||
y: yOffset + row * (fontHeight + buffer * 2) + buffer,
|
||||
width,
|
||||
height: fontHeight,
|
||||
mask: true
|
||||
// x: x + buffer,
|
||||
x,
|
||||
y: yOffset + row * glyphSizeInSDF,
|
||||
// y: yOffset + row * (fontHeight + buffer * 2) + buffer,
|
||||
width: glyphSizeInSDF,
|
||||
// height: fontHeight,
|
||||
height: glyphSizeInSDF,
|
||||
advance: width
|
||||
};
|
||||
x += width + buffer * 2;
|
||||
// x += width + buffer * 2;
|
||||
x += glyphSizeInSDF;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
/**
|
||||
* 网格索引,相比 @mapbox/grid-index,在简单计算碰撞检测结果时效率更高
|
||||
* @see https://zhuanlan.zhihu.com/p/74373214
|
||||
*/
|
||||
class GridIndex {
|
||||
constructor(width, height, cellSize) {
|
||||
const boxCells = this.boxCells = [];
|
||||
|
||||
this.xCellCount = Math.ceil(width / cellSize);
|
||||
this.yCellCount = Math.ceil(height / cellSize);
|
||||
|
||||
for (let i = 0; i < this.xCellCount * this.yCellCount; i++) {
|
||||
boxCells.push([]);
|
||||
}
|
||||
this.boxKeys = [];
|
||||
this.bboxes = [];
|
||||
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.xScale = this.xCellCount / width;
|
||||
this.yScale = this.yCellCount / height;
|
||||
this.boxUid = 0;
|
||||
}
|
||||
|
||||
insert(key, x1, y1, x2, y2) {
|
||||
this._forEachCell(x1, y1, x2, y2, this._insertBoxCell, this.boxUid++);
|
||||
this.boxKeys.push(key);
|
||||
this.bboxes.push(x1);
|
||||
this.bboxes.push(y1);
|
||||
this.bboxes.push(x2);
|
||||
this.bboxes.push(y2);
|
||||
}
|
||||
|
||||
_insertBoxCell(x1, y1, x2, y2, cellIndex, uid) {
|
||||
this.boxCells[cellIndex].push(uid);
|
||||
}
|
||||
|
||||
_query(x1, y1, x2, y2, hitTest, predicate) {
|
||||
if (x2 < 0 || x1 > this.width || y2 < 0 || y1 > this.height) {
|
||||
return hitTest ? false : [];
|
||||
}
|
||||
const result = [];
|
||||
if (x1 <= 0 && y1 <= 0 && this.width <= x2 && this.height <= y2) {
|
||||
// 这一步是高效的关键,后续精确碰撞检测结果在计算文本可见性时并不需要
|
||||
if (hitTest) {
|
||||
return true;
|
||||
}
|
||||
for (let boxUid = 0; boxUid < this.boxKeys.length; boxUid++) {
|
||||
result.push({
|
||||
key: this.boxKeys[boxUid],
|
||||
x1: this.bboxes[boxUid * 4],
|
||||
y1: this.bboxes[boxUid * 4 + 1],
|
||||
x2: this.bboxes[boxUid * 4 + 2],
|
||||
y2: this.bboxes[boxUid * 4 + 3]
|
||||
});
|
||||
}
|
||||
return predicate ? result.filter(predicate) : result;
|
||||
}
|
||||
|
||||
const queryArgs = {
|
||||
hitTest,
|
||||
seenUids: { box: {}, circle: {} }
|
||||
};
|
||||
this._forEachCell(x1, y1, x2, y2, this._queryCell, result, queryArgs, predicate);
|
||||
return hitTest ? result.length > 0 : result;
|
||||
}
|
||||
|
||||
query(x1, y1, x2, y2, predicate) {
|
||||
return this._query(x1, y1, x2, y2, false, predicate);
|
||||
}
|
||||
|
||||
hitTest(x1, y1, x2, y2, predicate) {
|
||||
return this._query(x1, y1, x2, y2, true, predicate);
|
||||
}
|
||||
|
||||
_queryCell(x1, y1, x2, y2, cellIndex, result, queryArgs, predicate) {
|
||||
const seenUids = queryArgs.seenUids;
|
||||
const boxCell = this.boxCells[cellIndex];
|
||||
if (boxCell !== null) {
|
||||
const bboxes = this.bboxes;
|
||||
for (const boxUid of boxCell) {
|
||||
if (!seenUids.box[boxUid]) {
|
||||
seenUids.box[boxUid] = true;
|
||||
const offset = boxUid * 4;
|
||||
if ((x1 <= bboxes[offset + 2]) &&
|
||||
(y1 <= bboxes[offset + 3]) &&
|
||||
(x2 >= bboxes[offset + 0]) &&
|
||||
(y2 >= bboxes[offset + 1]) &&
|
||||
(!predicate || predicate(this.boxKeys[boxUid]))) {
|
||||
if (queryArgs.hitTest) {
|
||||
result.push(true);
|
||||
return true;
|
||||
}
|
||||
result.push({
|
||||
key: this.boxKeys[boxUid],
|
||||
x1: bboxes[offset],
|
||||
y1: bboxes[offset + 1],
|
||||
x2: bboxes[offset + 2],
|
||||
y2: bboxes[offset + 3]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_forEachCell(x1, y1, x2, y2, fn, arg1, arg2, predicate) {
|
||||
const cx1 = this._convertToXCellCoord(x1);
|
||||
const cy1 = this._convertToYCellCoord(y1);
|
||||
const cx2 = this._convertToXCellCoord(x2);
|
||||
const cy2 = this._convertToYCellCoord(y2);
|
||||
|
||||
for (let x = cx1; x <= cx2; x++) {
|
||||
for (let y = cy1; y <= cy2; y++) {
|
||||
const cellIndex = this.xCellCount * y + x;
|
||||
if (fn.call(this, x1, y1, x2, y2, cellIndex, arg1, arg2, predicate)) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_convertToXCellCoord(x) {
|
||||
return Math.max(0, Math.min(this.xCellCount - 1, Math.floor(x * this.xScale)));
|
||||
}
|
||||
|
||||
_convertToYCellCoord(y) {
|
||||
return Math.max(0, Math.min(this.yCellCount - 1, Math.floor(y * this.yScale)));
|
||||
}
|
||||
}
|
||||
|
||||
export default GridIndex;
|
|
@ -0,0 +1,234 @@
|
|||
/**
|
||||
* 返回文本相对锚点位置
|
||||
* @param {string} anchor 锚点位置
|
||||
* @return {alignment} alignment
|
||||
*/
|
||||
function getAnchorAlignment(anchor) {
|
||||
let horizontalAlign = 0.5;
|
||||
let verticalAlign = 0.5;
|
||||
|
||||
switch (anchor) {
|
||||
case 'right':
|
||||
case 'top-right':
|
||||
case 'bottom-right':
|
||||
horizontalAlign = 1;
|
||||
break;
|
||||
case 'left':
|
||||
case 'top-left':
|
||||
case 'bottom-left':
|
||||
horizontalAlign = 0;
|
||||
break;
|
||||
default:
|
||||
horizontalAlign = 0.5;
|
||||
}
|
||||
|
||||
switch (anchor) {
|
||||
case 'bottom':
|
||||
case 'bottom-right':
|
||||
case 'bottom-left':
|
||||
verticalAlign = 1;
|
||||
break;
|
||||
case 'top':
|
||||
case 'top-right':
|
||||
case 'top-left':
|
||||
verticalAlign = 0;
|
||||
break;
|
||||
default:
|
||||
verticalAlign = 0.5;
|
||||
}
|
||||
|
||||
return { horizontalAlign, verticalAlign };
|
||||
}
|
||||
|
||||
// justify right = 1, left = 0, center = 0.5
|
||||
function justifyLine(
|
||||
positionedGlyphs,
|
||||
glyphMap,
|
||||
start,
|
||||
end,
|
||||
justify) {
|
||||
if (!justify) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastPositionedGlyph = positionedGlyphs[end];
|
||||
const glyph = lastPositionedGlyph.glyph;
|
||||
if (glyph) {
|
||||
const lastAdvance = glyphMap[glyph].advance * lastPositionedGlyph.scale;
|
||||
const lineIndent = (positionedGlyphs[end].x + lastAdvance) * justify;
|
||||
|
||||
for (let j = start; j <= end; j++) {
|
||||
positionedGlyphs[j].x -= lineIndent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// justify right=1 left=0 center=0.5
|
||||
// horizontalAlign right=1 left=0 center=0.5
|
||||
// verticalAlign right=1 left=0 center=0.5
|
||||
function align(
|
||||
positionedGlyphs,
|
||||
justify,
|
||||
horizontalAlign,
|
||||
verticalAlign,
|
||||
maxLineLength,
|
||||
lineHeight,
|
||||
lineCount
|
||||
) {
|
||||
const shiftX = (justify - horizontalAlign) * maxLineLength;
|
||||
const shiftY = (-verticalAlign * lineCount + 0.5) * lineHeight;
|
||||
|
||||
for (let j = 0; j < positionedGlyphs.length; j++) {
|
||||
positionedGlyphs[j].x += shiftX;
|
||||
positionedGlyphs[j].y += shiftY;
|
||||
}
|
||||
}
|
||||
|
||||
function shapeLines(
|
||||
shaping,
|
||||
glyphMap,
|
||||
lines,
|
||||
lineHeight,
|
||||
textAnchor,
|
||||
textJustify,
|
||||
spacing
|
||||
) {
|
||||
// buffer 为 4
|
||||
const yOffset = -8;
|
||||
|
||||
let x = 0;
|
||||
let y = yOffset;
|
||||
|
||||
let maxLineLength = 0;
|
||||
const positionedGlyphs = shaping.positionedGlyphs;
|
||||
|
||||
const justify =
|
||||
textJustify === 'right' ? 1 :
|
||||
textJustify === 'left' ? 0 : 0.5;
|
||||
|
||||
const lineStartIndex = positionedGlyphs.length;
|
||||
lines.forEach(line => {
|
||||
line.split('').forEach(char => {
|
||||
const glyph = glyphMap[char];
|
||||
const baselineOffset = 0;
|
||||
|
||||
if (glyph) {
|
||||
positionedGlyphs.push({
|
||||
glyph: char,
|
||||
x,
|
||||
y: y + baselineOffset,
|
||||
vertical: false, // TODO:目前只支持水平方向
|
||||
scale: 1,
|
||||
metrics: glyph
|
||||
});
|
||||
x += glyph.advance + spacing;
|
||||
}
|
||||
});
|
||||
|
||||
// 左右对齐
|
||||
if (positionedGlyphs.length !== lineStartIndex) {
|
||||
const lineLength = x - spacing;
|
||||
maxLineLength = Math.max(lineLength, maxLineLength);
|
||||
justifyLine(positionedGlyphs, glyphMap, lineStartIndex, positionedGlyphs.length - 1, justify);
|
||||
}
|
||||
|
||||
x = 0;
|
||||
y += lineHeight;
|
||||
});
|
||||
|
||||
const { horizontalAlign, verticalAlign } = getAnchorAlignment(textAnchor);
|
||||
align(positionedGlyphs, justify, horizontalAlign, verticalAlign, maxLineLength, lineHeight, lines.length);
|
||||
|
||||
// 计算包围盒
|
||||
const height = y - yOffset;
|
||||
|
||||
shaping.top += -verticalAlign * height;
|
||||
shaping.bottom = shaping.top + height;
|
||||
shaping.left += -horizontalAlign * maxLineLength;
|
||||
shaping.right = shaping.left + maxLineLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算文本中每个独立字符相对锚点的位置
|
||||
*
|
||||
* @param {string} text 原始文本
|
||||
* @param {*} glyphs mapping
|
||||
* @param {number} lineHeight 行高
|
||||
* @param {string} textAnchor 文本相对于锚点的位置
|
||||
* @param {string} textJustify 左右对齐
|
||||
* @param {number} spacing 字符间距
|
||||
* @param {[number, number]} translate 文本水平 & 垂直偏移量
|
||||
* @return {boolean|shaping} 每个字符相对于锚点的位置
|
||||
*/
|
||||
export function shapeText(
|
||||
text,
|
||||
glyphs,
|
||||
lineHeight,
|
||||
textAnchor,
|
||||
textJustify,
|
||||
spacing,
|
||||
translate
|
||||
) {
|
||||
|
||||
// TODO:处理换行
|
||||
const lines = text.split('\n');
|
||||
|
||||
const positionedGlyphs = [];
|
||||
const shaping = {
|
||||
positionedGlyphs,
|
||||
top: translate[1],
|
||||
bottom: translate[1],
|
||||
left: translate[0],
|
||||
right: translate[0],
|
||||
lineCount: lines.length,
|
||||
text
|
||||
};
|
||||
|
||||
shapeLines(shaping, glyphs, lines, lineHeight, textAnchor, textJustify, spacing);
|
||||
if (!positionedGlyphs.length) return false;
|
||||
|
||||
return shaping;
|
||||
}
|
||||
|
||||
export function getGlyphQuads(
|
||||
shaping,
|
||||
textOffset,
|
||||
alongLine
|
||||
) {
|
||||
const { positionedGlyphs } = shaping;
|
||||
const quads = [];
|
||||
|
||||
for (let k = 0; k < positionedGlyphs.length; k++) {
|
||||
const positionedGlyph = positionedGlyphs[k];
|
||||
const rect = positionedGlyph.metrics;
|
||||
|
||||
// The rects have an addditional buffer that is not included in their size.
|
||||
const rectBuffer = 4;
|
||||
|
||||
const halfAdvance = rect.advance * positionedGlyph.scale / 2;
|
||||
|
||||
const glyphOffset = alongLine ?
|
||||
[ positionedGlyph.x + halfAdvance, positionedGlyph.y ] :
|
||||
[ 0, 0 ];
|
||||
|
||||
const builtInOffset = alongLine ?
|
||||
[ 0, 0 ] :
|
||||
[ positionedGlyph.x + halfAdvance + textOffset[0], positionedGlyph.y + textOffset[1] ];
|
||||
|
||||
const x1 = (0 - rectBuffer) * positionedGlyph.scale - halfAdvance + builtInOffset[0];
|
||||
const y1 = (0 - rectBuffer) * positionedGlyph.scale + builtInOffset[1];
|
||||
const x2 = x1 + rect.width * positionedGlyph.scale;
|
||||
const y2 = y1 + rect.height * positionedGlyph.scale;
|
||||
|
||||
const tl = { x: x1, y: y1 };
|
||||
const tr = { x: x2, y: y1 };
|
||||
const bl = { x: x1, y: y2 };
|
||||
const br = { x: x2, y: y2 };
|
||||
|
||||
// TODO:处理字符旋转的情况
|
||||
|
||||
quads.push({ tl, tr, bl, br, tex: rect, glyphOffset });
|
||||
}
|
||||
|
||||
return quads;
|
||||
}
|
Loading…
Reference in New Issue