mirror of https://gitee.com/antv-l7/antv-l7
feat: 增加对于点要素的自动标注
This commit is contained in:
parent
821e180a74
commit
4ec14dfda5
|
@ -6,7 +6,7 @@
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
<meta name="geometry" content="diagram">
|
<meta name="geometry" content="diagram">
|
||||||
<link rel="stylesheet" href="./assets/common.css">
|
<link rel="stylesheet" href="./assets/common.css">
|
||||||
<title>point_circle</title>
|
<title>text layer</title>
|
||||||
<style>
|
<style>
|
||||||
#map { position:absolute; top:0; bottom:0; width:100%; }
|
#map { position:absolute; top:0; bottom:0; width:100%; }
|
||||||
</style>
|
</style>
|
||||||
|
@ -29,21 +29,42 @@ const scene = new L7.Scene({
|
||||||
});
|
});
|
||||||
window.scene = scene;
|
window.scene = scene;
|
||||||
scene.on('loaded', () => {
|
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({
|
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)
|
.source(data)
|
||||||
.shape('name', 'text')
|
.shape('name', 'text')
|
||||||
.active(true)
|
.active(true)
|
||||||
.size(12) // default 1
|
.size('scalerank', [ 10, 20, 24 ])
|
||||||
.color('name')
|
.color('scalerank', [ 'red', 'blue', 'black' ])
|
||||||
.style({
|
.style({
|
||||||
stroke: '#999',
|
// fontFamily: 'Monaco, monospace', // 字体
|
||||||
strokeWidth: 0,
|
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
|
opacity: 1.0
|
||||||
})
|
})
|
||||||
.render();
|
.render();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ export default class Scene extends Base {
|
||||||
this.fontAtlasManager = new FontAtlasManager();
|
this.fontAtlasManager = new FontAtlasManager();
|
||||||
this._layers = [];
|
this._layers = [];
|
||||||
this.animateCount = 0;
|
this.animateCount = 0;
|
||||||
|
this.inited = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_initEngine(mapContainer) {
|
_initEngine(mapContainer) {
|
||||||
|
@ -44,16 +45,24 @@ export default class Scene extends Base {
|
||||||
this.map = Map.map;
|
this.map = Map.map;
|
||||||
this._initEngine(Map.renderDom);
|
this._initEngine(Map.renderDom);
|
||||||
Map.asyncCamera(this._engine);
|
Map.asyncCamera(this._engine);
|
||||||
this.initLayer();
|
|
||||||
this._registEvents();
|
// 等待相机同步之后再进行首次渲染
|
||||||
const hash = this.get('hash');
|
Map.on('cameraloaded', () => {
|
||||||
if (hash) {
|
if (!this.inited) {
|
||||||
const Ctor = getInteraction('hash');
|
this.initLayer();
|
||||||
const interaction = new Ctor({ layer: this });
|
this._registEvents();
|
||||||
interaction._onHashChange();
|
const hash = this.get('hash');
|
||||||
}
|
if (hash) {
|
||||||
this.emit('loaded');
|
const Ctor = getInteraction('hash');
|
||||||
this._engine.update();
|
const interaction = new Ctor({ layer: this });
|
||||||
|
interaction._onHashChange();
|
||||||
|
}
|
||||||
|
this.emit('loaded');
|
||||||
|
this.inited = true;
|
||||||
|
}
|
||||||
|
this._engine.update();
|
||||||
|
this.emit('cameraloaded');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
initLayer() {
|
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 = [];
|
const characterSet = [];
|
||||||
layerData.forEach(element => {
|
sourceData.forEach(element => {
|
||||||
let text = element.shape || '';
|
// shape 存储了 text-field
|
||||||
|
let text = element[textField] || '';
|
||||||
text = text.toString();
|
text = text.toString();
|
||||||
for (let j = 0; j < text.length; j++) {
|
for (let j = 0; j < text.length; j++) {
|
||||||
|
// 去重
|
||||||
if (characterSet.indexOf(text[j]) === -1) {
|
if (characterSet.indexOf(text[j]) === -1) {
|
||||||
characterSet.push(text[j]);
|
characterSet.push(text[j]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
fontAtlasManager.setProps({
|
fontAtlasManager.setProps({
|
||||||
characterSet
|
characterSet,
|
||||||
|
fontFamily,
|
||||||
|
fontWeight
|
||||||
});
|
});
|
||||||
const attr = drawGlyph(layerData, fontAtlasManager);
|
return drawGlyph(layerData, sourceData, options, fontAtlasManager, collisionIndex, mvpMatrix);
|
||||||
return attr;
|
|
||||||
}
|
}
|
||||||
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 = {
|
const attributes = {
|
||||||
originPoints: [],
|
fontAtlas,
|
||||||
textSizes: [],
|
texture,
|
||||||
textOffsets: [],
|
positions: [],
|
||||||
colors: [],
|
colors: [],
|
||||||
textureElements: [],
|
pickingIds: [],
|
||||||
pickingIds: []
|
textUVs: [],
|
||||||
|
textOffsets: [],
|
||||||
|
textSizes: [],
|
||||||
|
index: []
|
||||||
};
|
};
|
||||||
const { texture, fontAtlas, mapping, scale } = fontAtlasManager;
|
let indexCounter = 0;
|
||||||
layerData.forEach(function(element) {
|
layerData.forEach((feature, i) => {
|
||||||
const size = element.size;
|
const { size, coordinates } = feature;
|
||||||
const pos = element.coordinates;
|
// 根据字段获取文本
|
||||||
let text = element.shape || '';
|
const text = `${sourceData[i][textField] || ''}`;
|
||||||
text = text.toString();
|
// sdf 中默认字号为 24
|
||||||
const pen = {
|
const fontScale = size / 24;
|
||||||
x: (-text.length * size) / 2,
|
|
||||||
y: 0
|
// 1. 计算每个字符相对锚点的位置
|
||||||
};
|
const shaping = shapeText(text, mapping, 24, textAnchor, 'center', spacing, textOffset);
|
||||||
for (let i = 0; i < text.length; i++) {
|
|
||||||
const metric = mapping[text[i]];
|
if (shaping) {
|
||||||
const { x, y, width, height } = metric;
|
// 2. 尝试加入空间索引,获取碰撞检测结果
|
||||||
const color = element.color;
|
// TODO:按照 feature 中指定字段排序,确定插入权重,保证优先级高的文本优先展示
|
||||||
const offsetX = pen.x;
|
const { box } = collisionIndex.placeCollisionBox({
|
||||||
const offsetY = pen.y;
|
x1: shaping.left * fontScale - padding[0],
|
||||||
attributes.pickingIds.push(
|
x2: shaping.right * fontScale + padding[0],
|
||||||
element.id,
|
y1: shaping.top * fontScale - padding[1],
|
||||||
element.id,
|
y2: shaping.bottom * fontScale + padding[1],
|
||||||
element.id,
|
// 点要素锚点就是当前点位置
|
||||||
element.id,
|
anchorPointX: coordinates[0],
|
||||||
element.id,
|
anchorPointY: coordinates[1]
|
||||||
element.id
|
}, mvpMatrix);
|
||||||
);
|
|
||||||
attributes.textOffsets.push(
|
// 无碰撞则加入空间索引
|
||||||
// 文字在词语的偏移量
|
if (box && box.length) {
|
||||||
offsetX,
|
// TODO:featureIndex
|
||||||
offsetY,
|
collisionIndex.insertCollisionBox(box, 0);
|
||||||
offsetX,
|
|
||||||
offsetY,
|
// 3. 计算可供渲染的文本块,其中每个字符都包含纹理坐标
|
||||||
offsetX,
|
const glyphQuads = getGlyphQuads(shaping, textOffset, false);
|
||||||
offsetY,
|
|
||||||
offsetX,
|
// 4. 构建顶点数据,四个顶点组成一个 quad
|
||||||
offsetY,
|
indexCounter = addAttributeForFeature(feature, attributes, glyphQuads, indexCounter);
|
||||||
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
|
|
||||||
);
|
|
||||||
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
|
|
||||||
);
|
|
||||||
pen.x = pen.x + size;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
attributes.texture = texture;
|
|
||||||
attributes.fontAtlas = fontAtlas;
|
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addAttributeForFeature(feature, attributes, glyphQuads, indexCounter) {
|
||||||
|
const { id, size, color, coordinates } = feature;
|
||||||
|
glyphQuads.forEach(quad => {
|
||||||
|
|
||||||
|
attributes.pickingIds.push(
|
||||||
|
id,
|
||||||
|
id,
|
||||||
|
id,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
|
||||||
|
attributes.colors.push(
|
||||||
|
...color,
|
||||||
|
...color,
|
||||||
|
...color,
|
||||||
|
...color
|
||||||
|
);
|
||||||
|
|
||||||
|
attributes.positions.push(
|
||||||
|
coordinates[0], coordinates[1],
|
||||||
|
coordinates[0], coordinates[1],
|
||||||
|
coordinates[0], coordinates[1],
|
||||||
|
coordinates[0], coordinates[1]
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
return indexCounter;
|
||||||
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ export const DEFAULT_BUFFER = 3;
|
||||||
export const DEFAULT_CUTOFF = 0.25;
|
export const DEFAULT_CUTOFF = 0.25;
|
||||||
export const DEFAULT_RADIUS = 8;
|
export const DEFAULT_RADIUS = 8;
|
||||||
const MAX_CANVAS_WIDTH = 1024;
|
const MAX_CANVAS_WIDTH = 1024;
|
||||||
const BASELINE_SCALE = 0.9;
|
const BASELINE_SCALE = 1.0;
|
||||||
const HEIGHT_SCALE = 1.2;
|
const HEIGHT_SCALE = 1.0;
|
||||||
const CACHE_LIMIT = 3;
|
const CACHE_LIMIT = 3;
|
||||||
const cache = new LRUCache(CACHE_LIMIT);
|
const cache = new LRUCache(CACHE_LIMIT);
|
||||||
|
|
||||||
|
@ -36,9 +36,9 @@ function getDefaultCharacterSet() {
|
||||||
|
|
||||||
function setTextStyle(ctx, fontFamily, fontSize, fontWeight) {
|
function setTextStyle(ctx, fontFamily, fontSize, fontWeight) {
|
||||||
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
||||||
ctx.fillStyle = '#000';
|
ctx.fillStyle = 'black';
|
||||||
ctx.textBaseline = 'baseline';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.textAlign = 'left';
|
// ctx.textAlign = 'left';
|
||||||
}
|
}
|
||||||
function getNewChars(key, characterSet) {
|
function getNewChars(key, characterSet) {
|
||||||
const cachedFontAtlas = cache.get(key);
|
const cachedFontAtlas = cache.get(key);
|
||||||
|
@ -146,10 +146,10 @@ export default class FontAtlasManager {
|
||||||
|
|
||||||
_updateTexture({ data: canvas }) {
|
_updateTexture({ data: canvas }) {
|
||||||
this._texture = new THREE.CanvasTexture(canvas);
|
this._texture = new THREE.CanvasTexture(canvas);
|
||||||
this._texture.wrapS = THREE.ClampToEdgeWrapping;
|
|
||||||
this._texture.wrapT = THREE.ClampToEdgeWrapping;
|
|
||||||
this._texture.minFilter = THREE.LinearFilter;
|
this._texture.minFilter = THREE.LinearFilter;
|
||||||
|
this._texture.magFilter = THREE.LinearFilter;
|
||||||
this._texture.flipY = false;
|
this._texture.flipY = false;
|
||||||
|
this._texture.format = THREE.AlphaFormat;
|
||||||
this._texture.needUpdate = true;
|
this._texture.needUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,7 +200,8 @@ export default class FontAtlasManager {
|
||||||
|
|
||||||
for (const char of characterSet) {
|
for (const char of characterSet) {
|
||||||
populateAlphaChannel(tinySDF.draw(char), imageData);
|
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 {
|
} else {
|
||||||
for (const char of characterSet) {
|
for (const char of characterSet) {
|
||||||
|
|
|
@ -1,23 +1,14 @@
|
||||||
import Material from './material';
|
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) {
|
export default function TextMaterial(_uniforms) {
|
||||||
const { vs, fs } = getModule('text');
|
const { vs, fs, uniforms } = getModule('text');
|
||||||
const material = new Material({
|
const material = new Material({
|
||||||
uniforms: {
|
defines: {
|
||||||
u_opacity: { value: options.u_opacity || 1.0 },
|
DEVICE_PIXEL_RATIO: window.devicePixelRatio
|
||||||
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 }
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
uniforms: wrapUniforms(merge(uniforms, _uniforms)),
|
||||||
vertexShader: vs,
|
vertexShader: vs,
|
||||||
fragmentShader: fs,
|
fragmentShader: fs,
|
||||||
transparent: true
|
transparent: true
|
||||||
|
|
|
@ -1,31 +1,30 @@
|
||||||
precision mediump float;
|
#define SDF_PX 8.0
|
||||||
uniform sampler2D u_texture;
|
#define EDGE_GAMMA 0.105 / float(DEVICE_PIXEL_RATIO)
|
||||||
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;
|
|
||||||
|
|
||||||
if(u_strokeWidth==0.){
|
uniform sampler2D u_sdf_map;
|
||||||
alpha=smoothstep(u_buffer-u_gamma,u_buffer+u_gamma,dist);
|
uniform float u_gamma_scale : 0.5;
|
||||||
gl_FragColor=vec4(v_color.rgb,alpha*v_color.a);
|
uniform float u_font_size : 24;
|
||||||
}else{
|
uniform float u_font_opacity : 1.0;
|
||||||
if(dist<=u_buffer-u_gamma){
|
uniform vec4 u_halo_color : [0, 0, 0, 1];
|
||||||
alpha=smoothstep(u_strokeWidth-u_gamma,u_strokeWidth+u_gamma,dist);
|
uniform float u_halo_width : 2.0;
|
||||||
gl_FragColor=vec4(u_stroke.rgb,alpha*u_stroke.a);
|
uniform float u_halo_blur : 0.5;
|
||||||
}else if(dist<u_buffer){
|
|
||||||
alpha=smoothstep(u_buffer-u_gamma,u_buffer+u_gamma,dist);
|
varying vec4 v_color;
|
||||||
gl_FragColor=vec4(alpha*v_color.rgb+(1.-alpha)*u_stroke.rgb,1.*v_color.a*alpha+(1.-alpha)*u_stroke.a);
|
varying vec2 v_uv;
|
||||||
}else{
|
varying float v_gamma_scale;
|
||||||
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_pos;
|
||||||
attribute vec2 a_txtsize;
|
attribute vec2 a_tex;
|
||||||
attribute vec2 a_txtOffsets;
|
attribute vec2 a_offset;
|
||||||
uniform float u_opacity;
|
|
||||||
attribute vec4 a_color;
|
attribute vec4 a_color;
|
||||||
uniform vec2 u_textTextureSize;// 纹理大小
|
attribute float a_size;
|
||||||
uniform vec2 u_glSize;
|
|
||||||
varying vec2 v_texcoord;
|
|
||||||
varying vec4 v_color;
|
|
||||||
varying float v_size;
|
|
||||||
uniform float u_activeId;
|
|
||||||
uniform vec4 u_activeColor;
|
|
||||||
|
|
||||||
void main(){
|
uniform vec2 u_sdf_map_size;
|
||||||
mat4 matModelViewProjection=projectionMatrix*modelViewMatrix;
|
uniform vec2 u_viewport_size;
|
||||||
vec4 cur_position=matModelViewProjection*vec4(position.xy,0,1);
|
|
||||||
v_size = 12. / u_glSize.x;
|
uniform float u_activeId : 0;
|
||||||
gl_Position=cur_position/cur_position.w+vec4((a_txtOffsets+a_txtsize)/u_glSize*2.,0.,0.);
|
uniform vec4 u_activeColor : [1.0, 0.0, 0.0, 1.0];
|
||||||
v_color=vec4(a_color.rgb,a_color.a*u_opacity);
|
|
||||||
if(pickingId==u_activeId){
|
varying vec2 v_uv;
|
||||||
v_color=u_activeColor;
|
varying float v_gamma_scale;
|
||||||
|
varying vec4 v_color;
|
||||||
|
|
||||||
|
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 TileLayer from './tile/tileLayer';
|
||||||
import ImageTileLayer from './tile/imageTileLayer';
|
import ImageTileLayer from './tile/imageTileLayer';
|
||||||
import VectorTileLayer from './tile/VectorTileLayer';
|
import VectorTileLayer from './tile/VectorTileLayer';
|
||||||
|
import TextLayer from './textLayer';
|
||||||
|
|
||||||
registerLayer('PolygonLayer', PolygonLayer);
|
registerLayer('PolygonLayer', PolygonLayer);
|
||||||
registerLayer('PointLayer', PointLayer);
|
registerLayer('PointLayer', PointLayer);
|
||||||
|
@ -18,6 +19,7 @@ registerLayer('HeatmapLayer', HeatmapLayer);
|
||||||
registerLayer('TileLayer', TileLayer);
|
registerLayer('TileLayer', TileLayer);
|
||||||
registerLayer('ImageTileLayer', ImageTileLayer);
|
registerLayer('ImageTileLayer', ImageTileLayer);
|
||||||
registerLayer('VectorTileLayer', VectorTileLayer);
|
registerLayer('VectorTileLayer', VectorTileLayer);
|
||||||
|
registerLayer('TextLayer', TextLayer);
|
||||||
|
|
||||||
export { LAYER_MAP, getLayer } from './factory';
|
export { LAYER_MAP, getLayer } from './factory';
|
||||||
export { registerLayer };
|
export { registerLayer };
|
||||||
|
|
|
@ -48,4 +48,10 @@ import DrawImage from './image/drawImage';
|
||||||
|
|
||||||
registerRender('image', 'image', DrawImage);
|
registerRender('image', 'image', DrawImage);
|
||||||
|
|
||||||
|
// image
|
||||||
|
|
||||||
|
import DrawText from './text/drawText';
|
||||||
|
|
||||||
|
registerRender('text', 'text', DrawText);
|
||||||
|
|
||||||
export { getRender };
|
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.lookAt(0, 0, 0);
|
||||||
camera.position.x += e.camera.position.x;
|
camera.position.x += e.camera.position.x;
|
||||||
camera.position.y += -e.camera.position.y;
|
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) {
|
export function nextPowOfTwo(number) {
|
||||||
return Math.pow(2, Math.ceil(Math.log2(number)));
|
return Math.pow(2, Math.ceil(Math.log2(number)));
|
||||||
}
|
}
|
||||||
|
@ -17,18 +22,23 @@ export function buildMapping({
|
||||||
Array.from(characterSet).forEach((char, i) => {
|
Array.from(characterSet).forEach((char, i) => {
|
||||||
if (!mapping[char]) {
|
if (!mapping[char]) {
|
||||||
const width = getFontWidth(char, i);
|
const width = getFontWidth(char, i);
|
||||||
if (x + width + buffer * 2 > maxCanvasWidth) {
|
if (x + glyphSizeInSDF > maxCanvasWidth) {
|
||||||
|
// if (x + width + buffer * 2 > maxCanvasWidth) {
|
||||||
x = 0;
|
x = 0;
|
||||||
row++;
|
row++;
|
||||||
}
|
}
|
||||||
mapping[char] = {
|
mapping[char] = {
|
||||||
x: x + buffer,
|
// x: x + buffer,
|
||||||
y: yOffset + row * (fontHeight + buffer * 2) + buffer,
|
x,
|
||||||
width,
|
y: yOffset + row * glyphSizeInSDF,
|
||||||
height: fontHeight,
|
// y: yOffset + row * (fontHeight + buffer * 2) + buffer,
|
||||||
mask: true
|
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