mirror of https://gitee.com/antv-l7/antv-l7
fix(merge):fix conflict
This commit is contained in:
commit
f2eadc53cc
|
@ -20,7 +20,7 @@
|
||||||
<script>
|
<script>
|
||||||
var scene = new L7.Scene({
|
var scene = new L7.Scene({
|
||||||
id: 'map',
|
id: 'map',
|
||||||
mapStyle: 'light', // 样式URL
|
mapStyle: 'dark', // 样式URL
|
||||||
center: [120.19382669582967, 30.258134],
|
center: [120.19382669582967, 30.258134],
|
||||||
pitch: 0,
|
pitch: 0,
|
||||||
minZoom:5,
|
minZoom:5,
|
||||||
|
@ -47,6 +47,33 @@
|
||||||
opacity: 0.9
|
opacity: 0.9
|
||||||
}).render();
|
}).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 popup = new L7.Popup({anchor:'left'}).setText('hello world')
|
||||||
const marker = new L7.Marker({color:'blue'})
|
const marker = new L7.Marker({color:'blue'})
|
||||||
.setLnglat( [120.19382669582967, 30.258134])
|
.setLnglat( [120.19382669582967, 30.258134])
|
||||||
|
|
|
@ -53,23 +53,16 @@ const scene = new L7.Scene({
|
||||||
window.scene = scene;
|
window.scene = scene;
|
||||||
scene.on('loaded', () => {
|
scene.on('loaded', () => {
|
||||||
$.get('https://gw.alipayobjects.com/os/rmsportal/epnZEheZeDgsiSjSPcCv.json', data => {
|
$.get('https://gw.alipayobjects.com/os/rmsportal/epnZEheZeDgsiSjSPcCv.json', data => {
|
||||||
|
console.log(data);
|
||||||
const circleLayer = scene.PointLayer({
|
const circleLayer = scene.PointLayer({
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
})
|
})
|
||||||
.source(data,{
|
.source(data,{
|
||||||
isCluster:true
|
isCluster:true
|
||||||
})
|
})
|
||||||
// .shape('circle')
|
.shape('circle')
|
||||||
.shape('point_count', [ 'circle', 'triangle', 'hexagon' ])
|
|
||||||
// .shape('triangle')
|
|
||||||
// .shape('square')
|
|
||||||
// .shape('hexagon')
|
|
||||||
// .shape('octogon')
|
|
||||||
// .shape('hexagram')
|
|
||||||
// .shape('pentagon')
|
|
||||||
.size('point_count', [ 5, 40]) // default 1
|
.size('point_count', [ 5, 40]) // default 1
|
||||||
//.size('value', [ 10, 300]) // default 1
|
.active(false)
|
||||||
.active(true)
|
|
||||||
.color('point_count',["#002466","#105CB3","#2894E0","#CFF6FF","#FFF5B8","#FFAB5C","#F27049","#730D1C"])
|
.color('point_count',["#002466","#105CB3","#2894E0","#CFF6FF","#FFF5B8","#FFAB5C","#F27049","#730D1C"])
|
||||||
.style({
|
.style({
|
||||||
stroke: 'rgb(255,255,255)',
|
stroke: 'rgb(255,255,255)',
|
||||||
|
@ -77,30 +70,30 @@ scene.on('loaded', () => {
|
||||||
opacity: 1
|
opacity: 1
|
||||||
})
|
})
|
||||||
.render();
|
.render();
|
||||||
window.circleLayer = circleLayer;
|
|
||||||
const layerText = scene.PointLayer({
|
scene.TextLayer({
|
||||||
zIndex: 3
|
zIndex: 5
|
||||||
})
|
})
|
||||||
.source(circleLayer.layerSource)
|
.source(circleLayer.layerSource)
|
||||||
.shape('point_count', 'text')
|
.shape('point_count', 'text')
|
||||||
.active(false)
|
.active(true)
|
||||||
.filter('point_count',(p)=>{
|
.size('point_count', [ 10, 20, 24 ])
|
||||||
return p > 50
|
.color('#FFF')
|
||||||
})
|
|
||||||
.size('point_count', [ 5, 20]) // default 1
|
|
||||||
.color('#fff')
|
|
||||||
.style({
|
.style({
|
||||||
stroke: '#999',
|
// fontFamily: 'Monaco, monospace', // 字体
|
||||||
strokeWidth: 1,
|
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
|
opacity: 1.0
|
||||||
})
|
})
|
||||||
.render();
|
.render();
|
||||||
console.log(layerText);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -64,7 +64,6 @@ scene.on('loaded', () => {
|
||||||
$.getJSON('https://gw.alipayobjects.com/os/rmsportal/JToMOWvicvJOISZFCkEI.json', city => {
|
$.getJSON('https://gw.alipayobjects.com/os/rmsportal/JToMOWvicvJOISZFCkEI.json', city => {
|
||||||
const citylayer = scene.PolygonLayer()
|
const citylayer = scene.PolygonLayer()
|
||||||
.source(city)
|
.source(city)
|
||||||
//.color('pm2_5_24h',["#FFF5B8","#FFDC7D","#FFAB5C","#F27049","#D42F31","#730D1C"])
|
|
||||||
.color('pm2_5_24h',(p)=>{
|
.color('pm2_5_24h',(p)=>{
|
||||||
if(p>120){
|
if(p>120){
|
||||||
return colors[5];
|
return colors[5];
|
||||||
|
@ -94,8 +93,7 @@ scene.on('loaded', () => {
|
||||||
.style({
|
.style({
|
||||||
opacity: 1.0
|
opacity: 1.0
|
||||||
})
|
})
|
||||||
//.render();
|
.render();
|
||||||
|
|
||||||
|
|
||||||
citylayer.on('mouseleave',(e)=>{
|
citylayer.on('mouseleave',(e)=>{
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
|
|
@ -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('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({
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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",
|
"name": "@antv/l7",
|
||||||
"version": "1.2.3",
|
"version": "1.3.0",
|
||||||
"description": "Large-scale WebGL-powered Geospatial Data Visualization",
|
"description": "Large-scale WebGL-powered Geospatial Data Visualization",
|
||||||
"main": "build/L7.js",
|
"main": "build/L7.js",
|
||||||
"browser": "build/L7-min.js",
|
"browser": "build/L7-min.js",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import TinySDF from '@mapbox/tiny-sdf';
|
import TinySDF from '@mapbox/tiny-sdf';
|
||||||
import { buildMapping } from '../../util/font-util';
|
import { buildMapping } from '../../util/font-util';
|
||||||
import * as THREE from '../../core/three';
|
import * as THREE from '../three';
|
||||||
import LRUCache from '../../util/lru-cache';
|
import LRUCache from '../../util/lru-cache';
|
||||||
export const DEFAULT_CHAR_SET = getDefaultCharacterSet();
|
export const DEFAULT_CHAR_SET = getDefaultCharacterSet();
|
||||||
export const DEFAULT_FONT_FAMILY = 'sans-serif';
|
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_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) {
|
||||||
|
|
|
@ -87,6 +87,11 @@ export default class Mapping {
|
||||||
const record = data[i];
|
const record = data[i];
|
||||||
const newRecord = {};
|
const newRecord = {};
|
||||||
newRecord.id = data[i]._id;
|
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) {
|
for (const k in attrs) {
|
||||||
if (attrs.hasOwnProperty(k)) {
|
if (attrs.hasOwnProperty(k)) {
|
||||||
const attr = attrs[k];
|
const attr = attrs[k];
|
||||||
|
@ -107,15 +112,6 @@ export default class Mapping {
|
||||||
newRecord.coordinates = record.coordinates;
|
newRecord.coordinates = record.coordinates;
|
||||||
mappedData.push(newRecord);
|
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;
|
this.mesh.layerData = mappedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,6 @@ export default class Layer extends Base {
|
||||||
world.add(this._object3D);
|
world.add(this._object3D);
|
||||||
this.layerMesh = null;
|
this.layerMesh = null;
|
||||||
this.layerLineMesh = null;
|
this.layerLineMesh = null;
|
||||||
// this._initEvents();
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 将图层添加加到 Object
|
* 将图层添加加到 Object
|
||||||
|
@ -145,6 +144,9 @@ export default class Layer extends Base {
|
||||||
|
|
||||||
if (data instanceof source) {
|
if (data instanceof source) {
|
||||||
this.layerSource = data;
|
this.layerSource = data;
|
||||||
|
this.layerSource.on('SourceUpdate', () => {
|
||||||
|
this.repaint();
|
||||||
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
cfg.data = data;
|
cfg.data = data;
|
||||||
|
@ -563,14 +565,16 @@ export default class Layer extends Base {
|
||||||
// z-fighting
|
// z-fighting
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
if (this.type === 'point') {
|
if (this.type === 'point') {
|
||||||
offset = 3;
|
offset = 5;
|
||||||
this.shapeType = 'text' && (offset = 10);
|
|
||||||
|
|
||||||
} else if (this.type === 'polyline' || this.type === 'line') {
|
} else if (this.type === 'polyline' || this.type === 'line') {
|
||||||
offset = 2;
|
offset = 2;
|
||||||
} else if (this.type === 'polygon') {
|
} else if (this.type === 'polygon') {
|
||||||
offset = 1;
|
offset = 1;
|
||||||
}
|
}
|
||||||
|
if (this.type === 'text') {
|
||||||
|
offset = 10;
|
||||||
|
}
|
||||||
this._object3D.position && (this._object3D.position.z = offset * Math.pow(2, 20 - zoom));
|
this._object3D.position && (this._object3D.position.z = offset * Math.pow(2, 20 - zoom));
|
||||||
if (zoom < minZoom || zoom >= maxZoom) {
|
if (zoom < minZoom || zoom >= maxZoom) {
|
||||||
this._object3D.visible = false;
|
this._object3D.visible = false;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Engine from './engine';
|
||||||
import { LAYER_MAP } from '../layer';
|
import { LAYER_MAP } from '../layer';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
import LoadImage from './image';
|
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 { MapProvider } from '../map/AMap';
|
||||||
import { getMap } from '../map/index';
|
import { getMap } from '../map/index';
|
||||||
import Global from '../global';
|
import Global from '../global';
|
||||||
|
@ -24,12 +24,13 @@ 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) {
|
||||||
this._engine = new Engine(mapContainer, this);
|
this._engine = new Engine(mapContainer, this);
|
||||||
this.registerMapEvent();
|
this.registerMapEvent(); // 和高德地图同步状态
|
||||||
this._engine.run();
|
// this._engine.run();
|
||||||
compileBuiltinModules();
|
compileBuiltinModules();
|
||||||
}
|
}
|
||||||
_initContoller() {
|
_initContoller() {
|
||||||
|
@ -53,13 +54,13 @@ export default class Scene extends Base {
|
||||||
const Map = new MapProvider(this._attrs);
|
const Map = new MapProvider(this._attrs);
|
||||||
Map.mixMap(this);
|
Map.mixMap(this);
|
||||||
this._container = Map.container;
|
this._container = Map.container;
|
||||||
this._markerContainier = Map.l7_marker_Container;
|
|
||||||
Map.on('mapLoad', () => {
|
Map.on('mapLoad', () => {
|
||||||
this.map = Map.map;
|
this.map = Map.map;
|
||||||
|
this._markerContainier = Map.l7_marker_Container;
|
||||||
this._initEngine(Map.renderDom);
|
this._initEngine(Map.renderDom);
|
||||||
Map.asyncCamera(this._engine);
|
Map.asyncCamera(this._engine);
|
||||||
this.initLayer();
|
this.initLayer();
|
||||||
// this._registEvents();
|
this._registEvents();
|
||||||
const hash = this.get('hash');
|
const hash = this.get('hash');
|
||||||
if (hash) {
|
if (hash) {
|
||||||
const Ctor = getInteraction('hash');
|
const Ctor = getInteraction('hash');
|
||||||
|
@ -174,14 +175,14 @@ export default class Scene extends Base {
|
||||||
// 地图状态变化时更新可视化渲染
|
// 地图状态变化时更新可视化渲染
|
||||||
registerMapEvent() {
|
registerMapEvent() {
|
||||||
this._updateRender = () => this._engine.update();
|
this._updateRender = () => this._engine.update();
|
||||||
this.map.on('mousemove', this._updateRender);
|
// this.map.on('mousemove', this._updateRender);
|
||||||
// this.map.on('mapmove', this._updateRender);
|
this.map.on('mapmove', this._updateRender);
|
||||||
this.map.on('camerachange', this._updateRender);
|
this.map.on('camerachange', this._updateRender);
|
||||||
}
|
}
|
||||||
|
|
||||||
unRegsterMapEvent() {
|
unRegsterMapEvent() {
|
||||||
this.map.off('mousemove', this._updateRender);
|
// this.map.off('mousemove', this._updateRender);
|
||||||
// this.map.off('mapmove', this._updateRender);
|
this.map.off('mapmove', this._updateRender);
|
||||||
this.map.off('camerachange', this._updateRender);
|
this.map.off('camerachange', this._updateRender);
|
||||||
}
|
}
|
||||||
// control
|
// control
|
||||||
|
|
|
@ -8,11 +8,8 @@ export default class Source extends Base {
|
||||||
getDefaultCfg() {
|
getDefaultCfg() {
|
||||||
return {
|
return {
|
||||||
data: null,
|
data: null,
|
||||||
defs: {},
|
|
||||||
parser: {},
|
parser: {},
|
||||||
transforms: [],
|
transforms: [],
|
||||||
scaledefs: {},
|
|
||||||
scales: {},
|
|
||||||
options: {}
|
options: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -99,6 +96,7 @@ export default class Source extends Base {
|
||||||
clusterCfg.bbox = bbox;
|
clusterCfg.bbox = bbox;
|
||||||
this.set('cluster', clusterCfg);
|
this.set('cluster', clusterCfg);
|
||||||
this._projectCoords();
|
this._projectCoords();
|
||||||
|
this.emit('SourceUpdate');
|
||||||
}
|
}
|
||||||
_projectCoords() {
|
_projectCoords() {
|
||||||
if (this.data === null) {
|
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 = [];
|
const characterSet = [];
|
||||||
layerData.forEach(element => {
|
sourceData.forEach(element => {
|
||||||
|
// shape 存储了 text-field
|
||||||
let text = element.shape || '';
|
let text = element.shape || '';
|
||||||
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,
|
||||||
|
{
|
||||||
|
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 = `${layerData[i].shape || ''}`;
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
|
@ -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 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,32 +1,31 @@
|
||||||
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;
|
||||||
}
|
|
||||||
#pragma include "pick"
|
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_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;
|
|
||||||
#ifdef PICK
|
#ifdef PICK
|
||||||
worldId = id_toPickColor(pickingId);
|
worldId = id_toPickColor(pickingId);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,10 @@ export default class Active extends Interaction {
|
||||||
}
|
}
|
||||||
process(ev) {
|
process(ev) {
|
||||||
this.layer._addActiveFeature(ev);
|
this.layer._addActiveFeature(ev);
|
||||||
|
this.layer.scene._engine.update();
|
||||||
}
|
}
|
||||||
reset() {
|
reset() {
|
||||||
this.layer._resetStyle();
|
this.layer._resetStyle();
|
||||||
|
this.layer.scene._engine.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,5 +8,6 @@ export default class Select extends Interaction {
|
||||||
}
|
}
|
||||||
process(ev) {
|
process(ev) {
|
||||||
this.layer._addActiveFeature(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 TileLayer from './tile/tile_layer';
|
||||||
import ImageTileLayer from './tile/image_tile_layer';
|
import ImageTileLayer from './tile/image_tile_layer';
|
||||||
import VectorTileLayer from './tile/vector_tile_layer';
|
import VectorTileLayer from './tile/vector_tile_layer';
|
||||||
|
import TextLayer from './text_layer';
|
||||||
|
|
||||||
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 };
|
||||||
|
|
|
@ -53,4 +53,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,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.container = map.getContainer();
|
||||||
this.get('mapStyle') && this.map.setMapStyle(this.get('mapStyle'));
|
this.get('mapStyle') && this.map.setMapStyle(this.get('mapStyle'));
|
||||||
this.addOverLayer();
|
this.addOverLayer();
|
||||||
this.emit('mapLoad');
|
setTimeout(() => { this.emit('mapLoad'); }, 50);
|
||||||
} else {
|
} else {
|
||||||
this.map = new AMap.Map(this.container, this._attrs);
|
this.map = new AMap.Map(this.container, this._attrs);
|
||||||
this.map.on('complete', () => {
|
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) {
|
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