mirror of https://gitee.com/antv-l7/antv-l7
fix(merge):fix conflict
This commit is contained in:
commit
235f4d9841
|
@ -20,7 +20,7 @@
|
|||
<script>
|
||||
var scene = new L7.Scene({
|
||||
id: 'map',
|
||||
mapStyle: 'light', // 样式URL
|
||||
mapStyle: 'dark', // 样式URL
|
||||
center: [120.19382669582967, 30.258134],
|
||||
pitch: 0,
|
||||
minZoom:5,
|
||||
|
@ -47,6 +47,33 @@
|
|||
opacity: 0.9
|
||||
}).render();
|
||||
|
||||
scene.TextLayer({
|
||||
zIndex: 5
|
||||
})
|
||||
.source(data)
|
||||
.shape('name', 'text')
|
||||
.active(true)
|
||||
.filter('value', function(v) {
|
||||
return v * 1 > 10000;
|
||||
})
|
||||
.size(20)
|
||||
.color('#FFF')
|
||||
.style({
|
||||
// fontFamily: 'Monaco, monospace', // 字体
|
||||
fontWeight: 500,
|
||||
textAnchor: 'center', // 文本相对锚点的位置 center|left|right|top|bottom|top-left
|
||||
textOffset: [ 0, 0 ], // 文本相对锚点的偏移量 [水平, 垂直]
|
||||
spacing: 2, // 字符间距
|
||||
padding: [ 4, 4 ], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近
|
||||
strokeColor: 'white', // 描边颜色
|
||||
strokeWidth: 1, // 描边宽度
|
||||
opacity: 1.0
|
||||
})
|
||||
.render();
|
||||
|
||||
|
||||
|
||||
|
||||
const popup = new L7.Popup({anchor:'left'}).setText('hello world')
|
||||
const marker = new L7.Marker({color:'blue'})
|
||||
.setLnglat( [120.19382669582967, 30.258134])
|
||||
|
|
|
@ -53,23 +53,16 @@ const scene = new L7.Scene({
|
|||
window.scene = scene;
|
||||
scene.on('loaded', () => {
|
||||
$.get('https://gw.alipayobjects.com/os/rmsportal/epnZEheZeDgsiSjSPcCv.json', data => {
|
||||
console.log(data);
|
||||
const circleLayer = scene.PointLayer({
|
||||
zIndex: 0,
|
||||
})
|
||||
.source(data,{
|
||||
isCluster:true
|
||||
})
|
||||
// .shape('circle')
|
||||
.shape('point_count', [ 'circle', 'triangle', 'hexagon' ])
|
||||
// .shape('triangle')
|
||||
// .shape('square')
|
||||
// .shape('hexagon')
|
||||
// .shape('octogon')
|
||||
// .shape('hexagram')
|
||||
// .shape('pentagon')
|
||||
.shape('circle')
|
||||
.size('point_count', [ 5, 40]) // default 1
|
||||
//.size('value', [ 10, 300]) // default 1
|
||||
.active(true)
|
||||
.active(false)
|
||||
.color('point_count',["#002466","#105CB3","#2894E0","#CFF6FF","#FFF5B8","#FFAB5C","#F27049","#730D1C"])
|
||||
.style({
|
||||
stroke: 'rgb(255,255,255)',
|
||||
|
@ -77,30 +70,30 @@ scene.on('loaded', () => {
|
|||
opacity: 1
|
||||
})
|
||||
.render();
|
||||
window.circleLayer = circleLayer;
|
||||
const layerText = scene.PointLayer({
|
||||
zIndex: 3
|
||||
|
||||
scene.TextLayer({
|
||||
zIndex: 5
|
||||
})
|
||||
.source(circleLayer.layerSource)
|
||||
.source(circleLayer.layerSource)
|
||||
.shape('point_count', 'text')
|
||||
.active(false)
|
||||
.filter('point_count',(p)=>{
|
||||
return p > 50
|
||||
})
|
||||
.size('point_count', [ 5, 20]) // default 1
|
||||
.color('#fff')
|
||||
.active(true)
|
||||
.size('point_count', [ 10, 20, 24 ])
|
||||
.color('#FFF')
|
||||
.style({
|
||||
stroke: '#999',
|
||||
strokeWidth: 1,
|
||||
// fontFamily: 'Monaco, monospace', // 字体
|
||||
fontWeight: 200,
|
||||
textAnchor: 'center', // 文本相对锚点的位置 center|left|right|top|bottom|top-left
|
||||
textOffset: [ 0, 0 ], // 文本相对锚点的偏移量 [水平, 垂直]
|
||||
spacing: 2, // 字符间距
|
||||
padding: [ 4, 4 ], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近
|
||||
strokeColor: 'white', // 描边颜色
|
||||
strokeWidth: 1, // 描边宽度
|
||||
opacity: 1.0
|
||||
})
|
||||
.render();
|
||||
console.log(layerText);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -64,7 +64,6 @@ scene.on('loaded', () => {
|
|||
$.getJSON('https://gw.alipayobjects.com/os/rmsportal/JToMOWvicvJOISZFCkEI.json', city => {
|
||||
const citylayer = scene.PolygonLayer()
|
||||
.source(city)
|
||||
//.color('pm2_5_24h',["#FFF5B8","#FFDC7D","#FFAB5C","#F27049","#D42F31","#730D1C"])
|
||||
.color('pm2_5_24h',(p)=>{
|
||||
if(p>120){
|
||||
return colors[5];
|
||||
|
@ -94,8 +93,7 @@ scene.on('loaded', () => {
|
|||
.style({
|
||||
opacity: 1.0
|
||||
})
|
||||
//.render();
|
||||
|
||||
.render();
|
||||
|
||||
citylayer.on('mouseleave',(e)=>{
|
||||
console.log(e);
|
||||
|
|
|
@ -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,21 +29,42 @@ const scene = new L7.Scene({
|
|||
});
|
||||
window.scene = scene;
|
||||
scene.on('loaded', () => {
|
||||
$.get('https://gw.alipayobjects.com/os/basement_prod/abcfe339-b8bc-46ce-8ff4-c96185b6235f.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();
|
||||
.render();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<meta name="geometry" content="diagram">
|
||||
<link rel="stylesheet" href="./assets/common.css">
|
||||
<title>text layer</title>
|
||||
<style>
|
||||
#map { position:absolute; top:0; bottom:0; width:100%; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
<script src="https://webapi.amap.com/maps?v=1.4.8&key=15cd8a57710d40c9b7c0e3cc120f1200&plugin=Map3D"></script>
|
||||
<script src="./assets/jquery-3.2.1.min.js"></script>
|
||||
<script src="./assets/dat.gui.min.js"></script>
|
||||
<script src="../build/L7.js"></script>
|
||||
<script>
|
||||
|
||||
const scene = new L7.Scene({
|
||||
id: 'map',
|
||||
mapStyle: 'dark', // 样式URL
|
||||
center: [ 120.19382669582967, 30.258134 ],
|
||||
pitch: 0,
|
||||
zoom: 10
|
||||
});
|
||||
window.scene = scene;
|
||||
scene.on('loaded', () => {
|
||||
$.get('https://gw.alipayobjects.com/os/rmsportal/oVTMqfzuuRFKiDwhPSFL.json', data => {
|
||||
scene.PointLayer({
|
||||
zIndex: 3
|
||||
})
|
||||
.source(data.list, {
|
||||
parser:{
|
||||
type: 'json',
|
||||
x: 'j',
|
||||
y: 'w',
|
||||
}
|
||||
})
|
||||
.shape('circle')
|
||||
.active(true)
|
||||
.size(3)
|
||||
.color('#CFF6FF')
|
||||
.style({
|
||||
stroke: '#999',
|
||||
strokeWidth: 1,
|
||||
opacity: 1.0
|
||||
})
|
||||
.render();
|
||||
|
||||
scene.TextLayer({
|
||||
zIndex: 5
|
||||
})
|
||||
.source(data.list, {
|
||||
parser:{
|
||||
type: 'json',
|
||||
x: 'j',
|
||||
y: 'w',
|
||||
}
|
||||
})
|
||||
.shape('m', 'text')
|
||||
.active(true)
|
||||
.size('w', [ 10, 20, 24 ])
|
||||
.color('#F27049')
|
||||
.style({
|
||||
// fontFamily: 'Monaco, monospace', // 字体
|
||||
fontWeight: 200,
|
||||
textAnchor: 'center', // 文本相对锚点的位置 center|left|right|top|bottom|top-left
|
||||
textOffset: [ 0, 0 ], // 文本相对锚点的偏移量 [水平, 垂直]
|
||||
spacing: 2, // 字符间距
|
||||
padding: [ 4, 4 ], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近
|
||||
strokeColor: 'white', // 描边颜色
|
||||
strokeWidth: 1, // 描边宽度
|
||||
opacity: 1.0
|
||||
})
|
||||
.render();
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@antv/l7",
|
||||
"version": "1.2.3",
|
||||
"version": "1.3.0",
|
||||
"description": "Large-scale WebGL-powered Geospatial Data Visualization",
|
||||
"main": "build/L7.js",
|
||||
"browser": "build/L7-min.js",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import TinySDF from '@mapbox/tiny-sdf';
|
||||
import { buildMapping } from '../../util/font-util';
|
||||
import * as THREE from '../../core/three';
|
||||
import * as THREE from '../three';
|
||||
import LRUCache from '../../util/lru-cache';
|
||||
export const DEFAULT_CHAR_SET = getDefaultCharacterSet();
|
||||
export const DEFAULT_FONT_FAMILY = 'sans-serif';
|
||||
|
@ -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) {
|
||||
|
|
|
@ -87,6 +87,11 @@ export default class Mapping {
|
|||
const record = data[i];
|
||||
const newRecord = {};
|
||||
newRecord.id = data[i]._id;
|
||||
if (attrs.hasOwnProperty('filter')) {
|
||||
const attr = attrs.filter;
|
||||
const values = this._getAttrValues(attr, record);
|
||||
if (!values[0]) continue;
|
||||
}
|
||||
for (const k in attrs) {
|
||||
if (attrs.hasOwnProperty(k)) {
|
||||
const attr = attrs[k];
|
||||
|
@ -107,15 +112,6 @@ export default class Mapping {
|
|||
newRecord.coordinates = record.coordinates;
|
||||
mappedData.push(newRecord);
|
||||
}
|
||||
// 通过透明度过滤数据
|
||||
if (attrs.hasOwnProperty('filter')) {
|
||||
mappedData.forEach(item => {
|
||||
if (item.filter === false) {
|
||||
(item.color[3] = 0);
|
||||
item.id = -item.id;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.mesh.layerData = mappedData;
|
||||
}
|
||||
|
||||
|
|
|
@ -77,7 +77,6 @@ export default class Layer extends Base {
|
|||
world.add(this._object3D);
|
||||
this.layerMesh = null;
|
||||
this.layerLineMesh = null;
|
||||
// this._initEvents();
|
||||
}
|
||||
/**
|
||||
* 将图层添加加到 Object
|
||||
|
@ -145,6 +144,9 @@ export default class Layer extends Base {
|
|||
|
||||
if (data instanceof source) {
|
||||
this.layerSource = data;
|
||||
this.layerSource.on('SourceUpdate', () => {
|
||||
this.repaint();
|
||||
});
|
||||
return this;
|
||||
}
|
||||
cfg.data = data;
|
||||
|
@ -563,14 +565,16 @@ export default class Layer extends Base {
|
|||
// z-fighting
|
||||
let offset = 0;
|
||||
if (this.type === 'point') {
|
||||
offset = 3;
|
||||
this.shapeType = 'text' && (offset = 10);
|
||||
offset = 5;
|
||||
|
||||
} else if (this.type === 'polyline' || this.type === 'line') {
|
||||
offset = 2;
|
||||
} else if (this.type === 'polygon') {
|
||||
offset = 1;
|
||||
}
|
||||
if (this.type === 'text') {
|
||||
offset = 10;
|
||||
}
|
||||
this._object3D.position && (this._object3D.position.z = offset * Math.pow(2, 20 - zoom));
|
||||
if (zoom < minZoom || zoom >= maxZoom) {
|
||||
this._object3D.visible = false;
|
||||
|
|
|
@ -2,7 +2,7 @@ import Engine from './engine';
|
|||
import { LAYER_MAP } from '../layer';
|
||||
import Base from './base';
|
||||
import LoadImage from './image';
|
||||
import FontAtlasManager from '../geom/buffer/point/text/font-manager';
|
||||
import FontAtlasManager from './atlas/font-manager';
|
||||
// import { MapProvider } from '../map/AMap';
|
||||
import { getMap } from '../map/index';
|
||||
import Global from '../global';
|
||||
|
@ -24,12 +24,13 @@ export default class Scene extends Base {
|
|||
this.fontAtlasManager = new FontAtlasManager();
|
||||
this._layers = [];
|
||||
this.animateCount = 0;
|
||||
this.inited = false;
|
||||
}
|
||||
|
||||
_initEngine(mapContainer) {
|
||||
this._engine = new Engine(mapContainer, this);
|
||||
this.registerMapEvent();
|
||||
this._engine.run();
|
||||
this.registerMapEvent(); // 和高德地图同步状态
|
||||
// this._engine.run();
|
||||
compileBuiltinModules();
|
||||
}
|
||||
_initContoller() {
|
||||
|
@ -53,13 +54,13 @@ export default class Scene extends Base {
|
|||
const Map = new MapProvider(this._attrs);
|
||||
Map.mixMap(this);
|
||||
this._container = Map.container;
|
||||
this._markerContainier = Map.l7_marker_Container;
|
||||
Map.on('mapLoad', () => {
|
||||
this.map = Map.map;
|
||||
this._markerContainier = Map.l7_marker_Container;
|
||||
this._initEngine(Map.renderDom);
|
||||
Map.asyncCamera(this._engine);
|
||||
this.initLayer();
|
||||
// this._registEvents();
|
||||
this._registEvents();
|
||||
const hash = this.get('hash');
|
||||
if (hash) {
|
||||
const Ctor = getInteraction('hash');
|
||||
|
@ -174,14 +175,14 @@ export default class Scene extends Base {
|
|||
// 地图状态变化时更新可视化渲染
|
||||
registerMapEvent() {
|
||||
this._updateRender = () => this._engine.update();
|
||||
this.map.on('mousemove', this._updateRender);
|
||||
// this.map.on('mapmove', this._updateRender);
|
||||
// this.map.on('mousemove', this._updateRender);
|
||||
this.map.on('mapmove', this._updateRender);
|
||||
this.map.on('camerachange', this._updateRender);
|
||||
}
|
||||
|
||||
unRegsterMapEvent() {
|
||||
this.map.off('mousemove', this._updateRender);
|
||||
// this.map.off('mapmove', this._updateRender);
|
||||
// this.map.off('mousemove', this._updateRender);
|
||||
this.map.off('mapmove', this._updateRender);
|
||||
this.map.off('camerachange', this._updateRender);
|
||||
}
|
||||
// control
|
||||
|
|
|
@ -8,11 +8,8 @@ export default class Source extends Base {
|
|||
getDefaultCfg() {
|
||||
return {
|
||||
data: null,
|
||||
defs: {},
|
||||
parser: {},
|
||||
transforms: [],
|
||||
scaledefs: {},
|
||||
scales: {},
|
||||
options: {}
|
||||
};
|
||||
}
|
||||
|
@ -99,6 +96,7 @@ export default class Source extends Base {
|
|||
clusterCfg.bbox = bbox;
|
||||
this.set('cluster', clusterCfg);
|
||||
this._projectCoords();
|
||||
this.emit('SourceUpdate');
|
||||
}
|
||||
_projectCoords() {
|
||||
if (this.data === null) {
|
||||
|
|
|
@ -1,130 +1,163 @@
|
|||
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 {
|
||||
fontWeight,
|
||||
fontFamily
|
||||
} = options;
|
||||
const characterSet = [];
|
||||
layerData.forEach(element => {
|
||||
sourceData.forEach(element => {
|
||||
// shape 存储了 text-field
|
||||
let text = element.shape || '';
|
||||
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,
|
||||
{
|
||||
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;
|
||||
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
|
||||
);
|
||||
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;
|
||||
let indexCounter = 0;
|
||||
layerData.forEach((feature, i) => {
|
||||
const { size, coordinates } = feature;
|
||||
// 根据字段获取文本
|
||||
const text = `${layerData[i].shape || ''}`;
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
attributes.texture = texture;
|
||||
attributes.fontAtlas = fontAtlas;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1,227 +0,0 @@
|
|||
import TinySDF from '@mapbox/tiny-sdf';
|
||||
import { buildMapping } from '../../../../util/font-util';
|
||||
import * as THREE from '../../../../core/three';
|
||||
import LRUCache from './lru-cache';
|
||||
export const DEFAULT_CHAR_SET = getDefaultCharacterSet();
|
||||
export const DEFAULT_FONT_FAMILY = 'sans-serif';
|
||||
export const DEFAULT_FONT_WEIGHT = 'normal';
|
||||
export const DEFAULT_FONT_SIZE = 24;
|
||||
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 CACHE_LIMIT = 3;
|
||||
const cache = new LRUCache(CACHE_LIMIT);
|
||||
|
||||
const VALID_PROPS = [
|
||||
'fontFamily',
|
||||
'fontWeight',
|
||||
'characterSet',
|
||||
'fontSize',
|
||||
'sdf',
|
||||
'buffer',
|
||||
'cutoff',
|
||||
'radius'
|
||||
];
|
||||
|
||||
function getDefaultCharacterSet() {
|
||||
const charSet = [];
|
||||
for (let i = 32; i < 128; i++) {
|
||||
charSet.push(String.fromCharCode(i));
|
||||
}
|
||||
return charSet;
|
||||
}
|
||||
|
||||
function setTextStyle(ctx, fontFamily, fontSize, fontWeight) {
|
||||
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.textBaseline = 'baseline';
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
function getNewChars(key, characterSet) {
|
||||
const cachedFontAtlas = cache.get(key);
|
||||
if (!cachedFontAtlas) {
|
||||
return characterSet;
|
||||
}
|
||||
|
||||
const newChars = [];
|
||||
const cachedMapping = cachedFontAtlas.mapping;
|
||||
let cachedCharSet = Object.keys(cachedMapping);
|
||||
cachedCharSet = new Set(cachedCharSet);
|
||||
|
||||
let charSet = characterSet;
|
||||
if (charSet instanceof Array) {
|
||||
charSet = new Set(charSet);
|
||||
}
|
||||
|
||||
charSet.forEach(char => {
|
||||
if (!cachedCharSet.has(char)) {
|
||||
newChars.push(char);
|
||||
}
|
||||
});
|
||||
|
||||
return newChars;
|
||||
}
|
||||
|
||||
function populateAlphaChannel(alphaChannel, imageData) {
|
||||
// populate distance value from tinySDF to image alpha channel
|
||||
for (let i = 0; i < alphaChannel.length; i++) {
|
||||
imageData.data[4 * i + 3] = alphaChannel[i];
|
||||
}
|
||||
}
|
||||
|
||||
export default class FontAtlasManager {
|
||||
constructor() {
|
||||
|
||||
// font settings
|
||||
this.props = {
|
||||
fontFamily: DEFAULT_FONT_FAMILY,
|
||||
fontWeight: DEFAULT_FONT_WEIGHT,
|
||||
characterSet: DEFAULT_CHAR_SET,
|
||||
fontSize: DEFAULT_FONT_SIZE,
|
||||
buffer: DEFAULT_BUFFER,
|
||||
// sdf only props
|
||||
// https://github.com/mapbox/tiny-sdf
|
||||
sdf: true,
|
||||
cutoff: DEFAULT_CUTOFF,
|
||||
radius: DEFAULT_RADIUS
|
||||
};
|
||||
|
||||
// key is used for caching generated fontAtlas
|
||||
this._key = null;
|
||||
this._texture = new THREE.Texture();
|
||||
}
|
||||
|
||||
get texture() {
|
||||
return this._texture;
|
||||
}
|
||||
|
||||
get mapping() {
|
||||
const data = cache.get(this._key);
|
||||
return data && data.mapping;
|
||||
}
|
||||
|
||||
get scale() {
|
||||
return HEIGHT_SCALE;
|
||||
}
|
||||
|
||||
get fontAtlas() {
|
||||
return this._fontAtlas;
|
||||
}
|
||||
|
||||
setProps(props = {}) {
|
||||
VALID_PROPS.forEach(prop => {
|
||||
if (prop in props) {
|
||||
this.props[prop] = props[prop];
|
||||
}
|
||||
});
|
||||
|
||||
// update cache key
|
||||
const oldKey = this._key;
|
||||
this._key = this._getKey();
|
||||
|
||||
const charSet = getNewChars(this._key, this.props.characterSet);
|
||||
const cachedFontAtlas = cache.get(this._key);
|
||||
|
||||
// if a fontAtlas associated with the new settings is cached and
|
||||
// there are no new chars
|
||||
if (cachedFontAtlas && charSet.length === 0) {
|
||||
// update texture with cached fontAtlas
|
||||
if (this._key !== oldKey) {
|
||||
this._updateTexture(cachedFontAtlas);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// update fontAtlas with new settings
|
||||
const fontAtlas = this._generateFontAtlas(this._key, charSet, cachedFontAtlas);
|
||||
this._fontAtlas = fontAtlas;
|
||||
this._updateTexture(fontAtlas);
|
||||
|
||||
// update cache
|
||||
cache.set(this._key, fontAtlas);
|
||||
}
|
||||
|
||||
_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.flipY = false;
|
||||
this._texture.needUpdate = true;
|
||||
}
|
||||
|
||||
_generateFontAtlas(key, characterSet, cachedFontAtlas) {
|
||||
const { fontFamily, fontWeight, fontSize, buffer, sdf, radius, cutoff } = this.props;
|
||||
let canvas = cachedFontAtlas && cachedFontAtlas.data;
|
||||
if (!canvas) {
|
||||
canvas = document.createElement('canvas');
|
||||
canvas.width = MAX_CANVAS_WIDTH;
|
||||
}
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
setTextStyle(ctx, fontFamily, fontSize, fontWeight);
|
||||
|
||||
// 1. build mapping
|
||||
const { mapping, canvasHeight, xOffset, yOffset } = buildMapping(
|
||||
Object.assign(
|
||||
{
|
||||
getFontWidth: char => ctx.measureText(char).width,
|
||||
fontHeight: fontSize * HEIGHT_SCALE,
|
||||
buffer,
|
||||
characterSet,
|
||||
maxCanvasWidth: MAX_CANVAS_WIDTH
|
||||
},
|
||||
cachedFontAtlas && {
|
||||
mapping: cachedFontAtlas.mapping,
|
||||
xOffset: cachedFontAtlas.xOffset,
|
||||
yOffset: cachedFontAtlas.yOffset
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// 2. update canvas
|
||||
// copy old canvas data to new canvas only when height changed
|
||||
if (canvas.height !== canvasHeight) {
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
canvas.height = canvasHeight;
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
setTextStyle(ctx, fontFamily, fontSize, fontWeight);
|
||||
|
||||
// 3. layout characters
|
||||
if (sdf) {
|
||||
const tinySDF = new TinySDF(fontSize, buffer, radius, cutoff, fontFamily, fontWeight);
|
||||
// used to store distance values from tinySDF
|
||||
// tinySDF.size equals `fontSize + buffer * 2`
|
||||
const imageData = ctx.getImageData(0, 0, tinySDF.size, tinySDF.size);
|
||||
|
||||
for (const char of characterSet) {
|
||||
populateAlphaChannel(tinySDF.draw(char), imageData);
|
||||
ctx.putImageData(imageData, mapping[char].x - buffer, mapping[char].y - buffer);
|
||||
}
|
||||
} else {
|
||||
for (const char of characterSet) {
|
||||
ctx.fillText(char, mapping[char].x, mapping[char].y + fontSize * BASELINE_SCALE);
|
||||
}
|
||||
}
|
||||
return {
|
||||
xOffset,
|
||||
yOffset,
|
||||
mapping,
|
||||
data: canvas,
|
||||
width: canvas.width,
|
||||
height: canvas.height
|
||||
};
|
||||
}
|
||||
|
||||
_getKey() {
|
||||
const { fontFamily, fontWeight, fontSize, buffer, sdf, radius, cutoff } = this.props;
|
||||
if (sdf) {
|
||||
return `${fontFamily} ${fontWeight} ${fontSize} ${buffer} ${radius} ${cutoff}`;
|
||||
}
|
||||
return `${fontFamily} ${fontWeight} ${fontSize} ${buffer}`;
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
/**
|
||||
* LRU Cache class with limit
|
||||
*
|
||||
* Update order for each get/set operation
|
||||
* Delete oldest when reach given limit
|
||||
*/
|
||||
|
||||
export default class LRUCache {
|
||||
constructor(limit = 5) {
|
||||
this.limit = limit;
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._cache = {};
|
||||
// access/update order, first item is oldest, last item is newest
|
||||
this._order = [];
|
||||
}
|
||||
|
||||
get(key) {
|
||||
const value = this._cache[key];
|
||||
if (value) {
|
||||
// update order
|
||||
this._deleteOrder(key);
|
||||
this._appendOrder(key);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
if (!this._cache[key]) {
|
||||
// if reach limit, delete the oldest
|
||||
if (Object.keys(this._cache).length === this.limit) {
|
||||
this.delete(this._order[0]);
|
||||
}
|
||||
|
||||
this._cache[key] = value;
|
||||
this._appendOrder(key);
|
||||
} else {
|
||||
// if found in cache, delete the old one, insert new one to the first of list
|
||||
this.delete(key);
|
||||
|
||||
this._cache[key] = value;
|
||||
this._appendOrder(key);
|
||||
}
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
const value = this._cache[key];
|
||||
if (value) {
|
||||
this._deleteCache(key);
|
||||
this._deleteOrder(key);
|
||||
}
|
||||
}
|
||||
|
||||
_deleteCache(key) {
|
||||
delete this._cache[key];
|
||||
}
|
||||
|
||||
_deleteOrder(key) {
|
||||
const index = this._order.findIndex(o => o === key);
|
||||
if (index >= 0) {
|
||||
this._order.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
_appendOrder(key) {
|
||||
this._order.push(key);
|
||||
}
|
||||
}
|
|
@ -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,32 +1,31 @@
|
|||
precision mediump float;
|
||||
uniform sampler2D u_texture;
|
||||
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;
|
||||
#define SDF_PX 8.0
|
||||
#define EDGE_GAMMA 0.105 / float(DEVICE_PIXEL_RATIO)
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
#pragma include "pick"
|
||||
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;
|
||||
varying vec2 v_uv;
|
||||
varying float v_gamma_scale;
|
||||
|
||||
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;
|
||||
#pragma include "pick"
|
||||
}
|
|
@ -1,28 +1,39 @@
|
|||
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;
|
||||
varying vec4 v_color;
|
||||
varying float v_size;
|
||||
uniform float u_activeId;
|
||||
uniform vec4 u_activeColor;
|
||||
attribute float a_size;
|
||||
|
||||
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;
|
||||
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;
|
||||
|
||||
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;
|
||||
#ifdef PICK
|
||||
|
||||
#ifdef PICK
|
||||
worldId = id_toPickColor(pickingId);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
}
|
||||
|
|
|
@ -9,8 +9,10 @@ export default class Active extends Interaction {
|
|||
}
|
||||
process(ev) {
|
||||
this.layer._addActiveFeature(ev);
|
||||
this.layer.scene._engine.update();
|
||||
}
|
||||
reset() {
|
||||
this.layer._resetStyle();
|
||||
this.layer.scene._engine.update();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,5 +8,6 @@ export default class Select extends Interaction {
|
|||
}
|
||||
process(ev) {
|
||||
this.layer._addActiveFeature(ev);
|
||||
this.layer.scene._engine.update();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import HeatmapLayer from './heatmap_layer';
|
|||
import TileLayer from './tile/tile_layer';
|
||||
import ImageTileLayer from './tile/image_tile_layer';
|
||||
import VectorTileLayer from './tile/vector_tile_layer';
|
||||
import TextLayer from './text_layer';
|
||||
|
||||
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 };
|
||||
|
|
|
@ -53,4 +53,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,102 @@
|
|||
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,
|
||||
layerData,
|
||||
{
|
||||
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,27 @@
|
|||
import Layer from '../core/layer';
|
||||
import { getRender } from './render';
|
||||
import CollisionIndex from '../util/collision-index';
|
||||
export default class TextLayer extends Layer {
|
||||
shape(field, values) {
|
||||
super.shape(field, values);
|
||||
this.shape = 'text';
|
||||
|
||||
// 创建碰撞检测索引
|
||||
const { width, height } = this.scene.getSize();
|
||||
this.collisionIndex = new CollisionIndex(width, height);
|
||||
|
||||
// 相机变化,需要重新构建索引,由于文本可见性的改变,也需要重新组装顶点数据
|
||||
this.scene.on('camerachange', () => {
|
||||
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.type = 'text';
|
||||
this.add(getRender(this.type, this.shape)(this.layerData, this));
|
||||
}
|
||||
}
|
|
@ -53,7 +53,7 @@ export default class GaodeMap extends Base {
|
|||
this.container = map.getContainer();
|
||||
this.get('mapStyle') && this.map.setMapStyle(this.get('mapStyle'));
|
||||
this.addOverLayer();
|
||||
this.emit('mapLoad');
|
||||
setTimeout(() => { this.emit('mapLoad'); }, 50);
|
||||
} else {
|
||||
this.map = new AMap.Map(this.container, this._attrs);
|
||||
this.map.on('complete', () => {
|
||||
|
|
|
@ -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