From 84a9193e3a8a311bb52bbedabc8847eabba7dc9a Mon Sep 17 00:00:00 2001 From: thinkinggis Date: Thu, 26 Dec 2019 22:36:50 +0800 Subject: [PATCH] feat(layer): pointLayer add text model --- .../coordinate/CoordinateSystemService.ts | 5 + .../coordinate/ICoordinateSystemService.ts | 1 + .../core/src/services/layer/ILayerService.ts | 2 + .../services/layer/IStyleAttributeService.ts | 2 +- .../core/src/services/scene/SceneService.ts | 5 + packages/layers/src/core/BaseLayer.ts | 4 +- packages/layers/src/core/BaseModel.ts | 7 + packages/layers/src/index.ts | 9 + packages/layers/src/line/dash.ts | 211 -------------- packages/layers/src/line/index.ts | 1 - packages/layers/src/line/models/arc.ts | 8 +- packages/layers/src/line/models/arc_3d.ts | 7 +- packages/layers/src/line/models/dash.ts | 23 -- .../layers/src/line/models/great_circle.ts | 11 +- packages/layers/src/line/models/line.ts | 7 +- .../layers/src/line/shaders/line_frag.glsl | 4 +- .../layers/src/line/shaders/line_vert.glsl | 2 + .../layers/src/plugins/FeatureScalePlugin.ts | 28 +- .../src/plugins/LayerAnimateStylePlugin.ts | 32 +++ .../layers/src/plugins/ShaderUniformPlugin.ts | 1 - packages/layers/src/point/models/fill.ts | 9 + packages/layers/src/point/models/index.ts | 3 +- packages/layers/src/point/models/text.ts | 259 +++++++++++++++++- .../layers/src/point/shaders/fill_frag.glsl | 7 + .../layers/src/point/shaders/text_frag.glsl | 5 +- .../layers/src/point/shaders/text_vert.glsl | 19 +- packages/layers/src/point/text.ts | 177 ------------ packages/layers/src/utils/collision-index.ts | 109 ++++++++ packages/layers/src/utils/grid-index.ts | 210 ++++++++++++++ packages/layers/src/utils/symbol-layout.ts | 29 +- stories/Layers/Layers.stories.tsx | 2 + stories/Layers/components/Line.tsx | 40 +-- stories/Layers/components/Point.tsx | 1 + stories/Layers/components/Text.tsx | 95 ++++--- 34 files changed, 809 insertions(+), 526 deletions(-) delete mode 100644 packages/layers/src/line/dash.ts delete mode 100644 packages/layers/src/line/models/dash.ts create mode 100644 packages/layers/src/plugins/LayerAnimateStylePlugin.ts delete mode 100644 packages/layers/src/point/text.ts create mode 100644 packages/layers/src/utils/collision-index.ts create mode 100644 packages/layers/src/utils/grid-index.ts diff --git a/packages/core/src/services/coordinate/CoordinateSystemService.ts b/packages/core/src/services/coordinate/CoordinateSystemService.ts index 227aa8c7ad..8bf04809dc 100644 --- a/packages/core/src/services/coordinate/CoordinateSystemService.ts +++ b/packages/core/src/services/coordinate/CoordinateSystemService.ts @@ -13,6 +13,7 @@ const VECTOR_TO_POINT_MATRIX = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]; @injectable() export default class CoordinateSystemService implements ICoordinateSystemService { + public needRefresh: boolean = true; @inject(TYPES.ICameraService) private readonly cameraService: ICameraService; @@ -59,6 +60,9 @@ export default class CoordinateSystemService * TODO: 使用 memoize 缓存参数以及计算结果 */ public refresh(): void { + // if (!this.needRefresh) { + // return; + // } const zoom = this.cameraService.getZoom(); const zoomScale = this.cameraService.getZoomScale(); const center = this.cameraService.getCenter(); @@ -86,6 +90,7 @@ export default class CoordinateSystemService } else if (this.coordinateSystem === CoordinateSystem.P20_OFFSET) { this.calculateLnglatOffset(center, zoom, zoomScale, true); } + this.needRefresh = false; // TODO: 判断是否应用瓦片 & 常规坐标系 } diff --git a/packages/core/src/services/coordinate/ICoordinateSystemService.ts b/packages/core/src/services/coordinate/ICoordinateSystemService.ts index 96b763777b..8bbc211970 100644 --- a/packages/core/src/services/coordinate/ICoordinateSystemService.ts +++ b/packages/core/src/services/coordinate/ICoordinateSystemService.ts @@ -30,6 +30,7 @@ export const CoordinateUniform = { }; export interface ICoordinateSystemService { + needRefresh: boolean; refresh(): void; getCoordinateSystem(): CoordinateSystem; setCoordinateSystem(coordinateSystem: CoordinateSystem): void; diff --git a/packages/core/src/services/layer/ILayerService.ts b/packages/core/src/services/layer/ILayerService.ts index 3cad0f053c..71fb3f9dc7 100644 --- a/packages/core/src/services/layer/ILayerService.ts +++ b/packages/core/src/services/layer/ILayerService.ts @@ -53,6 +53,7 @@ export interface ILayerModel { render(): void; getUninforms(): IModelUniform; getDefaultStyle(): unknown; + getAnimateUniforms(): IModelUniform; buildModels(): IModel[]; } export interface IModelUniform { @@ -78,6 +79,7 @@ export interface ILayer { zIndex: number; plugins: ILayerPlugin[]; layerModelNeedUpdate: boolean; + layerModel: ILayerModel; dataState: IDataState; // 数据流状态 pickedFeatureID: number; hooks: { diff --git a/packages/core/src/services/layer/IStyleAttributeService.ts b/packages/core/src/services/layer/IStyleAttributeService.ts index 9396ef0b9c..2f00b415dc 100644 --- a/packages/core/src/services/layer/IStyleAttributeService.ts +++ b/packages/core/src/services/layer/IStyleAttributeService.ts @@ -119,7 +119,7 @@ export interface IStyleAttributeInitializationOptions { type: AttributeType; scale?: { field: StyleAttributeField; - values: unknown[]; + values: unknown[] | string; names: string[]; type: StyleScaleType; callback?: (...args: any[]) => []; diff --git a/packages/core/src/services/scene/SceneService.ts b/packages/core/src/services/scene/SceneService.ts index 8013c5dd02..1803cf8e4a 100644 --- a/packages/core/src/services/scene/SceneService.ts +++ b/packages/core/src/services/scene/SceneService.ts @@ -11,6 +11,7 @@ import { ICameraService, IViewport } from '../camera/ICameraService'; import { IControlService } from '../component/IControlService'; import { IMarkerService } from '../component/IMarkerService'; import { IGlobalConfigService, ISceneConfig } from '../config/IConfigService'; +import { ICoordinateSystemService } from '../coordinate/ICoordinateSystemService'; import { IInteractionService } from '../interaction/IInteractionService'; import { ILayer, ILayerService } from '../layer/ILayerService'; import { ILogService } from '../log/ILogService'; @@ -47,6 +48,9 @@ export default class Scene extends EventEmitter implements ISceneService { @inject(TYPES.IMapService) private readonly map: IMapService; + @inject(TYPES.ICoordinateSystemService) + private readonly coordinateSystemService: ICoordinateSystemService; + @inject(TYPES.IRendererService) private readonly rendererService: IRendererService; @@ -249,6 +253,7 @@ export default class Scene extends EventEmitter implements ISceneService { }); // 触发 Map, canvas DOM.triggerResize(); + this.coordinateSystemService.needRefresh = true; // repaint layers this.render(); } diff --git a/packages/layers/src/core/BaseLayer.ts b/packages/layers/src/core/BaseLayer.ts index 251469c731..cec96c3d14 100644 --- a/packages/layers/src/core/BaseLayer.ts +++ b/packages/layers/src/core/BaseLayer.ts @@ -103,6 +103,8 @@ export default class BaseLayer extends EventEmitter options?: ISourceCFG; }; + public layerModel: ILayerModel; + @lazyInject(TYPES.ILogService) protected readonly logger: ILogService; @@ -133,8 +135,6 @@ export default class BaseLayer extends EventEmitter ) => IPostProcessingPass; protected normalPassFactory: (name: string) => IPass; - protected layerModel: ILayerModel; - protected animateOptions: IAnimateOption = { enable: false }; /** diff --git a/packages/layers/src/core/BaseModel.ts b/packages/layers/src/core/BaseModel.ts index fdacc33722..732354af42 100644 --- a/packages/layers/src/core/BaseModel.ts +++ b/packages/layers/src/core/BaseModel.ts @@ -57,7 +57,10 @@ export default class BaseModel this.cameraService = layer .getContainer() .get(TYPES.ICameraService); + // 注册 Attribute this.registerBuiltinAttributes(); + // 开启动画 + this.startModelAnimate(); } public getBlend(): IBlendOptions { const { blend = 'normal' } = this.layer.getLayerConfig(); @@ -70,6 +73,10 @@ export default class BaseModel throw new Error('Method not implemented.'); } + public getAnimateUniforms(): IModelUniform { + return {}; + } + public buildModels(): IModel[] { throw new Error('Method not implemented.'); } diff --git a/packages/layers/src/index.ts b/packages/layers/src/index.ts index 2a47c1029f..3f95eaa2e8 100644 --- a/packages/layers/src/index.ts +++ b/packages/layers/src/index.ts @@ -13,6 +13,7 @@ import ConfigSchemaValidationPlugin from './plugins/ConfigSchemaValidationPlugin import DataMappingPlugin from './plugins/DataMappingPlugin'; import DataSourcePlugin from './plugins/DataSourcePlugin'; import FeatureScalePlugin from './plugins/FeatureScalePlugin'; +import LayerAnimateStylePlugin from './plugins/LayerAnimateStylePlugin'; import LayerStylePlugin from './plugins/LayerStylePlugin'; import LightingPlugin from './plugins/LightingPlugin'; import MultiPassRendererPlugin from './plugins/MultiPassRendererPlugin'; @@ -87,6 +88,14 @@ container .bind(TYPES.ILayerPlugin) .to(ShaderUniformPlugin) .inRequestScope(); + +/** + * 传入动画参数 + */ +container + .bind(TYPES.ILayerPlugin) + .to(LayerAnimateStylePlugin) + .inRequestScope(); /** * 传入光照相关参数 */ diff --git a/packages/layers/src/line/dash.ts b/packages/layers/src/line/dash.ts deleted file mode 100644 index 7eca2e28f0..0000000000 --- a/packages/layers/src/line/dash.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { AttributeType, gl, IEncodeFeature } from '@antv/l7-core'; -import BaseLayer from '../core/BaseLayer'; -import { LineTriangulation } from '../core/triangulation'; -import line_dash_frag from './shaders/line_dash_frag.glsl'; -import line_dash_vert from './shaders/line_dash_vert.glsl'; -interface IDashLineLayerStyleOptions { - opacity: number; - dashArray: [number, number]; - lineType: string; -} -export default class DashLineLayer extends BaseLayer< - IDashLineLayerStyleOptions -> { - public type: string = 'LineLayer'; - - protected getConfigSchema() { - return { - properties: { - opacity: { - type: 'number', - minimum: 0, - maximum: 1, - }, - }, - }; - } - - protected renderModels() { - const { - opacity, - dashArray = [10, 5], - lineType = 'dash', - } = this.getLayerConfig(); - this.models.forEach((model) => - model.draw({ - uniforms: { - u_opacity: opacity || 1.0, - u_dash_array: dashArray, - }, - }), - ); - return this; - } - - protected buildModels() { - this.registerBuiltinAttributes(); - this.models = [ - this.buildLayerModel({ - moduleName: 'line_dash', - vertexShader: line_dash_vert, - fragmentShader: line_dash_frag, - triangulation: LineTriangulation, - blend: { - enable: true, - func: { - srcRGB: gl.SRC_ALPHA, - srcAlpha: 1, - dstRGB: gl.ONE_MINUS_SRC_ALPHA, - dstAlpha: 1, - }, - }, - }), - ]; - } - - private registerBuiltinAttributes() { - // point layer size; - this.styleAttributeService.registerStyleAttribute({ - name: 'size', - type: AttributeType.Attribute, - descriptor: { - name: 'a_Size', - buffer: { - // give the WebGL driver a hint that this buffer may change - usage: gl.DYNAMIC_DRAW, - data: [], - type: gl.FLOAT, - }, - size: 1, - update: ( - feature: IEncodeFeature, - featureIdx: number, - vertex: number[], - attributeIdx: number, - ) => { - const { size } = feature; - return Array.isArray(size) ? [size[0]] : [size as number]; - }, - }, - }); - - // point layer size; - this.styleAttributeService.registerStyleAttribute({ - name: 'normal', - type: AttributeType.Attribute, - descriptor: { - name: 'a_Normal', - buffer: { - // give the WebGL driver a hint that this buffer may change - usage: gl.STATIC_DRAW, - data: [], - type: gl.FLOAT, - }, - size: 3, - update: ( - feature: IEncodeFeature, - featureIdx: number, - vertex: number[], - attributeIdx: number, - normal: number[], - ) => { - return normal; - }, - }, - }); - - this.styleAttributeService.registerStyleAttribute({ - name: 'miter', - type: AttributeType.Attribute, - descriptor: { - name: 'a_Miter', - buffer: { - // give the WebGL driver a hint that this buffer may change - usage: gl.DYNAMIC_DRAW, - data: [], - type: gl.FLOAT, - }, - size: 1, - update: ( - feature: IEncodeFeature, - featureIdx: number, - vertex: number[], - attributeIdx: number, - ) => { - return [vertex[4]]; - }, - }, - }); - - // this.styleAttributeService.registerStyleAttribute({ - // name: 'startPos', - // type: AttributeType.Attribute, - // descriptor: { - // name: 'a_StartPos', - // buffer: { - // // give the WebGL driver a hint that this buffer may change - // usage: gl.DYNAMIC_DRAW, - // data: [], - // type: gl.FLOAT, - // }, - // size: 3, - // update: ( - // feature: IEncodeFeature, - // featureIdx: number, - // vertex: number[], - // attributeIdx: number, - // ) => { - // const coordinates = feature.coordinates as number[][]; - // const coord = coordinates[0]; - // return coord.length === 3 ? coord : [...coord, 0.0]; - // }, - // }, - // }); - - this.styleAttributeService.registerStyleAttribute({ - name: 'distance', - type: AttributeType.Attribute, - descriptor: { - name: 'a_Distance', - buffer: { - // give the WebGL driver a hint that this buffer may change - usage: gl.DYNAMIC_DRAW, - data: [], - type: gl.FLOAT, - }, - size: 1, - update: ( - feature: IEncodeFeature, - featureIdx: number, - vertex: number[], - attributeIdx: number, - ) => { - return [vertex[3]]; - }, - }, - }); - - this.styleAttributeService.registerStyleAttribute({ - name: 'total_distance', - type: AttributeType.Attribute, - descriptor: { - name: 'a_Total_Distance', - buffer: { - // give the WebGL driver a hint that this buffer may change - usage: gl.DYNAMIC_DRAW, - data: [], - type: gl.FLOAT, - }, - size: 1, - update: ( - feature: IEncodeFeature, - featureIdx: number, - vertex: number[], - attributeIdx: number, - ) => { - return [vertex[5]]; - }, - }, - }); - } -} diff --git a/packages/layers/src/line/index.ts b/packages/layers/src/line/index.ts index d076a1f956..dbad4d7ac7 100644 --- a/packages/layers/src/line/index.ts +++ b/packages/layers/src/line/index.ts @@ -29,7 +29,6 @@ export default class LineLayer extends BaseLayer { return defaultConfig[type]; } protected renderModels() { - // console.log(this.layerModel.getUninforms()); this.models.forEach((model) => model.draw({ uniforms: this.layerModel.getUninforms(), diff --git a/packages/layers/src/line/models/arc.ts b/packages/layers/src/line/models/arc.ts index 75ecff1138..9963a24a36 100644 --- a/packages/layers/src/line/models/arc.ts +++ b/packages/layers/src/line/models/arc.ts @@ -24,19 +24,23 @@ export default class ArcModel extends BaseModel { lineType = 'solid', dashArray = [10, 5], } = this.layer.getLayerConfig() as ILineLayerStyleOptions; - const { animateOption } = this.layer.getLayerConfig() as ILayerConfig; return { u_opacity: opacity || 1, segmentNumber: 30, u_line_type: lineStyleObj[lineType || 'solid'], u_dash_array: dashArray, + }; + } + + public getAnimateUniforms(): IModelUniform { + const { animateOption } = this.layer.getLayerConfig() as ILayerConfig; + return { u_aimate: this.animateOption2Array(animateOption as IAnimateOption), u_time: this.layer.getLayerAnimateTime(), }; } public buildModels(): IModel[] { - this.startModelAnimate(); return [ this.layer.buildLayerModel({ moduleName: 'arc2dline', diff --git a/packages/layers/src/line/models/arc_3d.ts b/packages/layers/src/line/models/arc_3d.ts index 12c4c258d4..4110ae6c7b 100644 --- a/packages/layers/src/line/models/arc_3d.ts +++ b/packages/layers/src/line/models/arc_3d.ts @@ -23,19 +23,22 @@ export default class Arc3DModel extends BaseModel { lineType = 'solid', dashArray = [10, 5], } = this.layer.getLayerConfig() as ILineLayerStyleOptions; - const { animateOption } = this.layer.getLayerConfig() as ILayerConfig; return { u_opacity: opacity || 1, segmentNumber: 30, u_line_type: lineStyleObj[lineType as string] || 0.0, u_dash_array: dashArray, + }; + } + public getAnimateUniforms(): IModelUniform { + const { animateOption } = this.layer.getLayerConfig() as ILayerConfig; + return { u_aimate: this.animateOption2Array(animateOption as IAnimateOption), u_time: this.layer.getLayerAnimateTime(), }; } public buildModels(): IModel[] { - this.startModelAnimate(); return [ this.layer.buildLayerModel({ moduleName: 'arc3Dline', diff --git a/packages/layers/src/line/models/dash.ts b/packages/layers/src/line/models/dash.ts deleted file mode 100644 index ee37772a4f..0000000000 --- a/packages/layers/src/line/models/dash.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { - AttributeType, - gl, - IEncodeFeature, - ILayer, - ILayerModel, - IModel, -} from '@antv/l7-core'; - -import BaseModel from '../../core/BaseModel'; -export default class ArcModel extends BaseModel { - public getUninforms() { - return {}; - } - - public buildModels(): IModel[] { - throw new Error('Method not implemented.'); - } - - protected registerBuiltinAttributes() { - // - } -} diff --git a/packages/layers/src/line/models/great_circle.ts b/packages/layers/src/line/models/great_circle.ts index 35d28ce1f4..16ce7d75af 100644 --- a/packages/layers/src/line/models/great_circle.ts +++ b/packages/layers/src/line/models/great_circle.ts @@ -25,22 +25,22 @@ export default class GreatCircleModel extends BaseModel { lineType = 'solid', dashArray = [10, 5], } = this.layer.getLayerConfig() as Partial; - const { animateOption } = this.layer.getLayerConfig() as ILayerConfig; return { u_opacity: opacity || 1, segmentNumber: 30, u_line_type: lineStyleObj[lineType as string] || 0.0, u_dash_array: dashArray, + }; + } + public getAnimateUniforms(): IModelUniform { + const { animateOption } = this.layer.getLayerConfig() as ILayerConfig; + return { u_aimate: this.animateOption2Array(animateOption as IAnimateOption), u_time: this.layer.getLayerAnimateTime(), }; } public buildModels(): IModel[] { - const { animateOption } = this.layer.getLayerConfig() as ILayerConfig; - if (animateOption.enable) { - this.layer.setAnimateStartTime(); - } return [ this.layer.buildLayerModel({ moduleName: 'arc2dline', @@ -53,7 +53,6 @@ export default class GreatCircleModel extends BaseModel { ]; } protected registerBuiltinAttributes() { - // point layer size; this.styleAttributeService.registerStyleAttribute({ name: 'size', type: AttributeType.Attribute, diff --git a/packages/layers/src/line/models/line.ts b/packages/layers/src/line/models/line.ts index 6e11d53950..287112a0bf 100644 --- a/packages/layers/src/line/models/line.ts +++ b/packages/layers/src/line/models/line.ts @@ -24,18 +24,21 @@ export default class LineModel extends BaseModel { lineType = lineStyleType.solid, dashArray = [10, 5], } = this.layer.getLayerConfig() as ILineLayerStyleOptions; - const { animateOption } = this.layer.getLayerConfig() as ILayerConfig; return { u_opacity: opacity || 1.0, u_line_type: lineStyleObj[lineType], u_dash_array: dashArray, + }; + } + public getAnimateUniforms(): IModelUniform { + const { animateOption } = this.layer.getLayerConfig() as ILayerConfig; + return { u_aimate: this.animateOption2Array(animateOption as IAnimateOption), u_time: this.layer.getLayerAnimateTime(), }; } public buildModels(): IModel[] { - this.startModelAnimate(); return [ this.layer.buildLayerModel({ moduleName: 'line', diff --git a/packages/layers/src/line/shaders/line_frag.glsl b/packages/layers/src/line/shaders/line_frag.glsl index 60d1cd60e5..0303e815b2 100644 --- a/packages/layers/src/line/shaders/line_frag.glsl +++ b/packages/layers/src/line/shaders/line_frag.glsl @@ -13,6 +13,7 @@ uniform float u_dash_offset : 0.0; uniform float u_dash_ratio : 0.1; varying float v_distance_ratio; varying vec2 v_dash_array; +varying float v_side; #pragma include "picking" @@ -30,7 +31,8 @@ void main() { float alpha =1.0 - fract( mod(1.0- v_distance_ratio, u_aimate.z)* (1.0/ u_aimate.z) + u_time / u_aimate.y); alpha = (alpha + u_aimate.w -1.0) / u_aimate.w; alpha = smoothstep(0., 1., alpha); - gl_FragColor.a *= alpha * blur; + float alpha2 = exp(-abs(v_side)); + gl_FragColor.a *= alpha * blur * alpha2; // gl_FragColor.a = fract(u_time); } // dash line diff --git a/packages/layers/src/line/shaders/line_vert.glsl b/packages/layers/src/line/shaders/line_vert.glsl index 138db7c8b1..c3036680b9 100644 --- a/packages/layers/src/line/shaders/line_vert.glsl +++ b/packages/layers/src/line/shaders/line_vert.glsl @@ -24,6 +24,7 @@ varying vec4 v_color; varying vec2 v_dash_array; varying vec2 v_normal; varying float v_distance_ratio; +varying float v_side; void main() { @@ -39,6 +40,7 @@ void main() { v_color = a_Color; vec3 size = a_Miter * a_Size.x * reverse_offset_normal(a_Normal); //v_normal * vec3(1., -1., 1.0); vec2 offset = project_pixel(size.xy); + v_side = a_Miter * a_Size.x; vec4 project_pos = project_position(vec4(a_Position.xy, 0, 1.0)); gl_Position = project_common_position_to_clipspace(vec4(project_pos.xy + offset, a_Size.y, 1.0)); diff --git a/packages/layers/src/plugins/FeatureScalePlugin.ts b/packages/layers/src/plugins/FeatureScalePlugin.ts index 2bcfdb0901..955f904afd 100644 --- a/packages/layers/src/plugins/FeatureScalePlugin.ts +++ b/packages/layers/src/plugins/FeatureScalePlugin.ts @@ -118,7 +118,7 @@ export default class FeatureScalePlugin implements ILayerPlugin { scales.forEach((scale) => { // 如果设置了回调, 这不需要设置让range if (!attributeScale.callback) { - if (attributeScale.values) { + if (attributeScale.values && attributeScale.values !== 'text') { if ( scale.option?.type === 'linear' && attributeScale.values.length > 2 @@ -131,6 +131,7 @@ export default class FeatureScalePlugin implements ILayerPlugin { scale.scale.range(attributeScale.values); } else if (scale.option?.type === 'cat') { // 如果没有设置初值且 类型为cat,range ==domain; + scale.scale.range(scale.option.domain); } } @@ -159,20 +160,13 @@ export default class FeatureScalePlugin implements ILayerPlugin { dataArray: IParseDataItem[], ) { const scalekey = [field, attribute.name].join('_'); + const values = attribute.scale?.values; if (this.scaleCache[scalekey]) { return this.scaleCache[scalekey]; } - const styleScale = this.createScale(field, dataArray); + const styleScale = this.createScale(field, values, dataArray); this.scaleCache[scalekey] = styleScale; - // if ( - // styleScale.type === StyleScaleType.VARIABLE && - // attribute.scale?.values && - // attribute.scale?.values.length > 0 - // ) { // 只有变量初始化range - // styleScale.scale.range(attribute.scale?.values); - // } - return this.scaleCache[scalekey]; } @@ -191,7 +185,11 @@ export default class FeatureScalePlugin implements ILayerPlugin { return [field]; } - private createScale(field: string, data?: IParseDataItem[]): IStyleScale { + private createScale( + field: string, + values: unknown[] | string | undefined, + data?: IParseDataItem[], + ): IStyleScale { // 首先查找全局默认配置例如 color const scaleOption: IScale | undefined = this.scaleOptions[field]; const styleScale: IStyleScale = { @@ -200,6 +198,7 @@ export default class FeatureScalePlugin implements ILayerPlugin { type: StyleScaleType.VARIABLE, option: scaleOption, }; + if (!data || !data.length) { if (scaleOption && scaleOption.type) { styleScale.scale = this.createDefaultScale(scaleOption); @@ -216,9 +215,12 @@ export default class FeatureScalePlugin implements ILayerPlugin { styleScale.type = StyleScaleType.CONSTANT; } else { // 根据数据类型判断 默认等分位,时间,和枚举类型 - const type = + let type = (scaleOption && scaleOption.type) || this.getDefaultType(firstValue); - + if (values === 'text') { + // text 为内置变 如果是文本则为cat + type = ScaleTypes.CAT; + } const cfg = this.createDefaultScaleConfig(type, field, data); Object.assign(cfg, scaleOption); styleScale.scale = this.createDefaultScale(cfg); diff --git a/packages/layers/src/plugins/LayerAnimateStylePlugin.ts b/packages/layers/src/plugins/LayerAnimateStylePlugin.ts new file mode 100644 index 0000000000..db766a6e35 --- /dev/null +++ b/packages/layers/src/plugins/LayerAnimateStylePlugin.ts @@ -0,0 +1,32 @@ +import { + CameraUniform, + CoordinateUniform, + ICameraService, + ICoordinateSystemService, + ILayer, + ILayerPlugin, + IModel, + IRendererService, + TYPES, +} from '@antv/l7-core'; +import { inject, injectable } from 'inversify'; + +@injectable() +export default class LayerAnimateStylePlugin implements ILayerPlugin { + @inject(TYPES.ICameraService) + private readonly cameraService: ICameraService; + + @inject(TYPES.IRendererService) + private readonly rendererService: IRendererService; + + public apply(layer: ILayer) { + layer.hooks.beforeRender.tap('LayerAnimateStylePlugin', () => { + // 重新计算坐标系参数 + layer.models.forEach((model: IModel) => { + model.addUniforms({ + ...layer.layerModel.getAnimateUniforms(), + }); + }); + }); + } +} diff --git a/packages/layers/src/plugins/ShaderUniformPlugin.ts b/packages/layers/src/plugins/ShaderUniformPlugin.ts index a075e0db0e..4e488b7754 100644 --- a/packages/layers/src/plugins/ShaderUniformPlugin.ts +++ b/packages/layers/src/plugins/ShaderUniformPlugin.ts @@ -31,7 +31,6 @@ export default class ShaderUniformPlugin implements ILayerPlugin { public apply(layer: ILayer) { layer.hooks.beforeRender.tap('ShaderUniformPlugin', () => { // 重新计算坐标系参数 - this.coordinateSystemService.refresh(); const { width, height } = this.rendererService.getViewportSize(); diff --git a/packages/layers/src/point/models/fill.ts b/packages/layers/src/point/models/fill.ts index b4f8fc548e..61a01b11fa 100644 --- a/packages/layers/src/point/models/fill.ts +++ b/packages/layers/src/point/models/fill.ts @@ -1,9 +1,11 @@ import { AttributeType, gl, + IAnimateOption, IAttribute, IElements, IEncodeFeature, + ILayerConfig, IModel, IModelUniform, } from '@antv/l7-core'; @@ -30,6 +32,13 @@ export default class FillModel extends BaseModel { u_stroke_color: rgb2arr(stroke), }; } + public getAnimateUniforms(): IModelUniform { + const { animateOption } = this.layer.getLayerConfig() as ILayerConfig; + return { + u_aimate: this.animateOption2Array(animateOption as IAnimateOption), + u_time: this.layer.getLayerAnimateTime(), + }; + } public getAttribute(): { attributes: { diff --git a/packages/layers/src/point/models/index.ts b/packages/layers/src/point/models/index.ts index e0b6ef8c87..ea4c8b9647 100644 --- a/packages/layers/src/point/models/index.ts +++ b/packages/layers/src/point/models/index.ts @@ -3,6 +3,7 @@ import ExtrudeModel from './extrude'; import FillModel from './fill'; import IMageModel from './image'; import NormalModel from './normal'; +import TextModel from './text'; export type PointType = 'fill' | 'image' | 'normal' | 'extrude' | 'text'; @@ -11,7 +12,7 @@ const PointModels: { [key in PointType]: any } = { image: IMageModel, normal: NormalModel, extrude: ExtrudeModel, - text: null, + text: TextModel, }; export default PointModels; diff --git a/packages/layers/src/point/models/text.ts b/packages/layers/src/point/models/text.ts index 4b643b340c..085c1edec3 100644 --- a/packages/layers/src/point/models/text.ts +++ b/packages/layers/src/point/models/text.ts @@ -1,15 +1,264 @@ -import { IModel, IModelUniform } from '@antv/l7-core'; +import { + AttributeType, + BlendType, + gl, + IEncodeFeature, + ILayerConfig, + IModel, + IModelUniform, + ITexture2D, +} from '@antv/l7-core'; +import { rgb2arr } from '@antv/l7-utils'; import BaseModel from '../../core/BaseModel'; +import { PointFillTriangulation } from '../../core/triangulation'; +import { + getGlyphQuads, + IGlyphQuad, + shapeText, +} from '../../utils/symbol-layout'; +import textFrag from '../shaders/text_frag.glsl'; +import textVert from '../shaders/text_vert.glsl'; +interface IPointTextLayerStyleOptions { + opacity: number; + textAnchor: string; + spacing: number; + padding: [number, number]; + stroke: string; + strokeWidth: number; + strokeOpacity: number; + fontWeight: string; + fontFamily: string; + textOffset: [number, number]; + textAllowOverlap: boolean; +} +export function TextTriangulation(feature: IEncodeFeature) { + const coordinates = feature.coordinates as number[]; + const { glyphQuads } = feature; + const vertices: number[] = []; + const indices: number[] = []; + const coord = + coordinates.length === 2 + ? [coordinates[0], coordinates[1], 0] + : coordinates; + glyphQuads.forEach((quad: IGlyphQuad, index: number) => { + vertices.push( + ...coord, + quad.tex.x, + quad.tex.y + quad.tex.height, + quad.tl.x, + quad.tl.y, + ...coord, + quad.tex.x + quad.tex.width, + quad.tex.y + quad.tex.height, + quad.tr.x, + quad.tr.y, + ...coord, + quad.tex.x + quad.tex.width, + quad.tex.y, + quad.br.x, + quad.br.y, + ...coord, + quad.tex.x, + quad.tex.y, + quad.bl.x, + quad.bl.y, + ); + indices.push( + 0 + index * 4, + 1 + index * 4, + 2 + index * 4, + 2 + index * 4, + 3 + index * 4, + 0 + index * 4, + ); + }); + return { + vertices, // [ x, y, z, tex.x,tex.y, offset.x. offset.y] + indices, + size: 7, + }; +} -export default class ExtrudeModel extends BaseModel { +export default class TextModel extends BaseModel { + private texture: ITexture2D; public getUninforms(): IModelUniform { - throw new Error('Method not implemented.'); + const { + fontWeight = 'normal', + fontFamily, + stroke, + strokeWidth, + } = this.layer.getLayerConfig() as IPointTextLayerStyleOptions; + const { canvas, fontAtlas, mapping } = this.fontService; + return { + u_opacity: 1.0, + u_sdf_map: this.texture, + u_stroke: rgb2arr(stroke), + u_halo_blur: 0.5, + u_sdf_map_size: [canvas.width, canvas.height], + u_strokeWidth: strokeWidth, + }; } public buildModels(): IModel[] { - throw new Error('Method not implemented.'); + this.initTextFont(); + this.generateGlyphLayout(); + this.registerBuiltinAttributes(); + this.updateTexture(); + return [ + this.layer.buildLayerModel({ + moduleName: 'pointText', + vertexShader: textVert, + fragmentShader: textFrag, + triangulation: TextTriangulation, + depth: { enable: false }, + blend: this.getBlend(), + }), + ]; } + protected registerBuiltinAttributes() { - throw new Error('Method not implemented.'); + const viewProjection = this.cameraService.getViewProjectionMatrix(); + this.styleAttributeService.registerStyleAttribute({ + name: 'textOffsets', + type: AttributeType.Attribute, + descriptor: { + name: 'a_textOffsets', + buffer: { + // give the WebGL driver a hint that this buffer may change + usage: gl.STATIC_DRAW, + data: [], + type: gl.FLOAT, + }, + size: 2, + update: ( + feature: IEncodeFeature, + featureIdx: number, + vertex: number[], + attributeIdx: number, + ) => { + return [vertex[5], vertex[6]]; + }, + }, + }); + + // point layer size; + this.styleAttributeService.registerStyleAttribute({ + name: 'size', + type: AttributeType.Attribute, + descriptor: { + name: 'a_Size', + buffer: { + // give the WebGL driver a hint that this buffer may change + usage: gl.DYNAMIC_DRAW, + data: [], + type: gl.FLOAT, + }, + size: 1, + update: ( + feature: IEncodeFeature, + featureIdx: number, + vertex: number[], + attributeIdx: number, + ) => { + const { size } = feature; + return Array.isArray(size) ? [size[0]] : [size as number]; + }, + }, + }); + + // point layer size; + this.styleAttributeService.registerStyleAttribute({ + name: 'textUv', + type: AttributeType.Attribute, + descriptor: { + name: 'a_tex', + buffer: { + // give the WebGL driver a hint that this buffer may change + usage: gl.DYNAMIC_DRAW, + data: [], + type: gl.FLOAT, + }, + size: 2, + update: ( + feature: IEncodeFeature, + featureIdx: number, + vertex: number[], + attributeIdx: number, + ) => { + return [vertex[3], vertex[4]]; + }, + }, + }); + } + private initTextFont() { + const { + fontWeight = 'normal', + fontFamily, + } = this.layer.getLayerConfig() as IPointTextLayerStyleOptions; + const data = this.layer.getEncodedData(); + const characterSet: string[] = []; + data.forEach((item: IEncodeFeature) => { + let { shape = '' } = item; + shape = shape.toString(); + for (const char of shape) { + // 去重 + if (characterSet.indexOf(char) === -1) { + characterSet.push(char); + } + } + }); + this.fontService.setFontOptions({ + characterSet, + fontWeight, + fontFamily, + }); + } + private generateGlyphLayout() { + const { canvas, fontAtlas, mapping } = this.fontService; + const { + spacing = 2, + textAnchor = 'center', + textOffset, + padding = [4, 4], + textAllowOverlap, + } = this.layer.getLayerConfig() as IPointTextLayerStyleOptions; + const data = this.layer.getEncodedData(); + data.forEach((feature: IEncodeFeature) => { + const { coordinates, shape = '' } = feature; + const size = feature.size as number; + const fontScale = size / 24; + const shaping = shapeText( + shape.toString(), + mapping, + 24, + textAnchor, + 'center', + spacing, + textOffset, + ); + const glyphQuads = getGlyphQuads(shaping, textOffset, false); + feature.shaping = shaping; + feature.glyphQuads = glyphQuads; + }); + } + + private drawGlyph() { + const { + spacing = 2, + textAnchor = 'center', + textOffset = [0, 0], + padding = [4, 4], + textAllowOverlap, + } = this.layer.getLayerConfig() as IPointTextLayerStyleOptions; + const viewProjection = this.cameraService.getViewProjectionMatrix(); + } + private updateTexture() { + const { createTexture2D } = this.rendererService; + const { canvas } = this.fontService; + this.texture = createTexture2D({ + data: canvas, + width: canvas.width, + height: canvas.height, + }); } } diff --git a/packages/layers/src/point/shaders/fill_frag.glsl b/packages/layers/src/point/shaders/fill_frag.glsl index b4c4b86e10..c66d8ba62f 100644 --- a/packages/layers/src/point/shaders/fill_frag.glsl +++ b/packages/layers/src/point/shaders/fill_frag.glsl @@ -7,6 +7,7 @@ uniform float u_stroke_opacity : 1; varying vec4 v_data; varying vec4 v_color; varying float v_radius; +uniform float u_time; #pragma include "sdf_2d" #pragma include "picking" @@ -61,8 +62,14 @@ void main() { inner_df ); vec4 strokeColor = u_stroke_color == vec4(0) ? v_color : u_stroke_color; + float PI = 3.14159; + float N_RINGS = 3.0; + float FREQ = 1.0; + // float intensity = 1.0; + float intensity = clamp(cos(r * PI), 0.0, 1.0) * clamp(cos(2.0 * PI * (r * 2.0 * N_RINGS - FREQ * u_time / 1000.)), 0.0, 1.0); gl_FragColor = opacity_t * mix(vec4(v_color.rgb, v_color.a * u_opacity), strokeColor * u_stroke_opacity, color_t); + // gl_FragColor = vec4(gl_FragColor.xyz * intensity, intensity); gl_FragColor = filterColor(gl_FragColor); } diff --git a/packages/layers/src/point/shaders/text_frag.glsl b/packages/layers/src/point/shaders/text_frag.glsl index d3dfa93034..11b8520478 100644 --- a/packages/layers/src/point/shaders/text_frag.glsl +++ b/packages/layers/src/point/shaders/text_frag.glsl @@ -1,3 +1,5 @@ +#define SDF_PX 8.0 +#define EDGE_GAMMA 0.105 uniform sampler2D u_sdf_map; uniform float u_gamma_scale : 0.5; uniform float u_font_size : 24; @@ -5,6 +7,7 @@ uniform float u_opacity : 1.0; uniform vec4 u_stroke : [0, 0, 0, 1]; uniform float u_strokeWidth : 2.0; uniform float u_halo_blur : 0.5; +uniform float u_DevicePixelRatio; varying vec4 v_color; varying vec2 v_uv; @@ -17,7 +20,7 @@ void main() { float fontScale = u_font_size / 24.0; lowp float buff = (6.0 - u_strokeWidth / fontScale) / SDF_PX; - highp float gamma = (u_halo_blur * 1.19 / SDF_PX + EDGE_GAMMA) / (fontScale * u_gamma_scale); + highp float gamma = (u_halo_blur * 1.19 / SDF_PX + EDGE_GAMMA) / (fontScale * u_gamma_scale) / 1.0; highp float gamma_scaled = gamma * v_gamma_scale; diff --git a/packages/layers/src/point/shaders/text_vert.glsl b/packages/layers/src/point/shaders/text_vert.glsl index 496a5180c2..729b446105 100644 --- a/packages/layers/src/point/shaders/text_vert.glsl +++ b/packages/layers/src/point/shaders/text_vert.glsl @@ -1,14 +1,13 @@ +#define SDF_PX 8.0 +#define EDGE_GAMMA 0.105 attribute vec3 a_Position; attribute vec2 a_tex; -attribute vec2 a_offset; -attribute vec4 a_color; -attribute float a_size; +attribute vec2 a_textOffsets; +attribute vec4 a_Color; +attribute float a_Size; uniform vec2 u_sdf_map_size; -uniform vec2 u_viewport_size; - -uniform float u_activeId : 0; -uniform vec4 u_activeColor : [1.0, 0.0, 0.0, 1.0]; +uniform mat4 u_ModelMatrix; varying vec2 v_uv; varying float v_gamma_scale; @@ -17,18 +16,18 @@ varying vec4 v_color; #pragma include "projection" void main() { - v_color = a_color; + v_color = a_Color; v_uv = a_tex / u_sdf_map_size; // 文本缩放比例 - float fontScale = a_size / 24.; + float fontScale = a_Size / 24.; vec4 project_pos = project_position(vec4(a_Position, 1.0)); vec4 projected_position = project_common_position_to_clipspace(vec4(project_pos.xyz, 1.0)); gl_Position = vec4(projected_position.xy / projected_position.w - + a_offset * fontScale / u_viewport_size * 2., 0.0, 1.0); + + a_textOffsets * fontScale / u_ViewportSize * 2., 0.0, 1.0); v_gamma_scale = gl_Position.w; diff --git a/packages/layers/src/point/text.ts b/packages/layers/src/point/text.ts deleted file mode 100644 index becc151b72..0000000000 --- a/packages/layers/src/point/text.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { AttributeType, gl, IEncodeFeature } from '@antv/l7-core'; -import BaseLayer from '../core/BaseLayer'; -import { getGlyphQuads, shapeText } from '../utils/symbol-layout'; -import textFrag from './shaders/text_frag.glsl'; -import textVert from './shaders/text_vert.glsl'; -interface IPointTextLayerStyleOptions { - opacity: number; - textAnchor: string; - textOffset: [number, number]; - spacing: number; - padding: [number, number]; - stroke: string; - strokeWidth: number; - strokeOpacity: number; - fontWeight: string; - fontFamily: string; - - textAllowOverlap: boolean; -} -export function PointTriangulation(feature: IEncodeFeature) { - const coordinates = feature.coordinates as number[]; - return { - vertices: [...coordinates, ...coordinates, ...coordinates, ...coordinates], - indices: [0, 1, 2, 2, 3, 0], - size: coordinates.length, - }; -} -export default class TextLayer extends BaseLayer { - public type: string = 'PointLayer'; - - protected getConfigSchema() { - return { - properties: { - opacity: { - type: 'number', - minimum: 0, - maximum: 1, - }, - }, - }; - } - - protected renderModels() { - const { opacity } = this.getLayerConfig(); - this.models.forEach((model) => - model.draw({ - uniforms: { - u_opacity: opacity || 1.0, - }, - }), - ); - return this; - } - - protected buildModels() { - this.registerBuiltinAttributes(); - this.models = [ - this.buildLayerModel({ - moduleName: 'pointText', - vertexShader: textVert, - fragmentShader: textFrag, - triangulation: PointTriangulation, - depth: { enable: false }, - blend: { - enable: true, - func: { - srcRGB: gl.SRC_ALPHA, - srcAlpha: 1, - dstRGB: gl.ONE_MINUS_SRC_ALPHA, - dstAlpha: 1, - }, - }, - }), - ]; - } - - private registerBuiltinAttributes() { - this.styleAttributeService.registerStyleAttribute({ - name: 'textOffsets', - type: AttributeType.Attribute, - descriptor: { - name: 'a_textOffsets', - buffer: { - // give the WebGL driver a hint that this buffer may change - usage: gl.STATIC_DRAW, - data: [], - type: gl.FLOAT, - }, - size: 2, - update: ( - feature: IEncodeFeature, - featureIdx: number, - vertex: number[], - attributeIdx: number, - ) => { - const extrude = [-1, -1, 1, -1, 1, 1, -1, 1]; - const extrudeIndex = (attributeIdx % 4) * 2; - return [extrude[extrudeIndex], extrude[extrudeIndex + 1]]; - }, - }, - }); - - // point layer size; - this.styleAttributeService.registerStyleAttribute({ - name: 'size', - type: AttributeType.Attribute, - descriptor: { - name: 'a_Size', - buffer: { - // give the WebGL driver a hint that this buffer may change - usage: gl.DYNAMIC_DRAW, - data: [], - type: gl.FLOAT, - }, - size: 1, - update: ( - feature: IEncodeFeature, - featureIdx: number, - vertex: number[], - attributeIdx: number, - ) => { - const { size } = feature; - return Array.isArray(size) ? [size[0]] : [size as number]; - }, - }, - }); - - // point layer size; - this.styleAttributeService.registerStyleAttribute({ - name: 'shape', - type: AttributeType.Attribute, - descriptor: { - name: 'a_Shape', - buffer: { - // give the WebGL driver a hint that this buffer may change - usage: gl.DYNAMIC_DRAW, - data: [], - type: gl.FLOAT, - }, - size: 1, - update: ( - feature: IEncodeFeature, - featureIdx: number, - vertex: number[], - attributeIdx: number, - ) => { - const { shape = 2 } = feature; - const shape2d = this.getLayerConfig().shape2d as string[]; - const shapeIndex = shape2d.indexOf(shape as string); - return [shapeIndex]; - }, - }, - }); - } - - private initTextFont() { - const { fontWeight = 'normal', fontFamily } = this.getLayerConfig(); - const data = this.getEncodedData(); - const characterSet: string[] = []; - data.forEach((item: IEncodeFeature) => { - let { shape = '' } = item; - shape = shape.toString(); - for (const char of shape) { - // 去重 - if (characterSet.indexOf(char) === -1) { - characterSet.push(char); - } - } - }); - - this.fontService.setFontOptions({ - characterSet, - fontWeight, - fontFamily, - }); - } -} diff --git a/packages/layers/src/utils/collision-index.ts b/packages/layers/src/utils/collision-index.ts new file mode 100644 index 0000000000..21e8873938 --- /dev/null +++ b/packages/layers/src/utils/collision-index.ts @@ -0,0 +1,109 @@ +export interface ICollisionBox { + x1: number; + y1: number; + x2: number; + y2: number; + anchorPointX: number; + anchorPointY: number; +} +// @mapbox/grid-index 并没有类似 hitTest 的单纯获取碰撞检测结果的方法,query 将导致计算大量多余的包围盒结果,因此使用改良版 +import { mat4, vec4 } from 'gl-matrix'; +import GridIndex from './grid-index'; + +// 为 viewport 加上 buffer,避免边缘处的文本无法显示 +const viewportPadding = 100; + +/** + * 基于网格实现文本避让,大幅提升包围盒碰撞检测效率 + * @see https://zhuanlan.zhihu.com/p/74373214 + */ +export default class CollisionIndex { + private width: number; + private height: number; + private grid: GridIndex; + private screenRightBoundary: number; + private screenBottomBoundary: number; + private gridRightBoundary: number; + private gridBottomBoundary: number; + constructor(width: number, height: number) { + 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; + } + + public placeCollisionBox(collisionBox: ICollisionBox, mvpMatrix: mat4) { + 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], + }; + } + + public insertCollisionBox(box: number[], featureIndex: number) { + const key = { featureIndex }; + this.grid.insert(key, box[0], box[1], box[2], box[3]); + } + + /** + * 后续碰撞检测都需要投影到 viewport 坐标系 + * @param {THREE.Matrix4} mvpMatrix mvp矩阵 + * @param {number} x P20 平面坐标X + * @param {number} y P20 平面坐标Y + * @return {Point} projectedPoint + */ + public project(mvpMatrix: mat4, x: number, y: number) { + const point = vec4.fromValues(x, y, 0, 1); + const out = vec4.create(); + vec4.transformMat4(out, point, mvpMatrix); + // GL 坐标系[-1, 1] -> viewport 坐标系[width, height] + return { + x: ((out[0] / out[3] + 1) / 2) * this.width + viewportPadding, + y: ((-out[1] / out[3] + 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 + */ + public isInsideGrid(x1: number, y1: number, x2: number, y2: number) { + return ( + x2 >= 0 && + x1 < this.gridRightBoundary && + y2 >= 0 && + y1 < this.gridBottomBoundary + ); + } +} diff --git a/packages/layers/src/utils/grid-index.ts b/packages/layers/src/utils/grid-index.ts new file mode 100644 index 0000000000..4f56a11b58 --- /dev/null +++ b/packages/layers/src/utils/grid-index.ts @@ -0,0 +1,210 @@ +interface IQueryArgs { + hitTest: boolean; + seenUids: { box: any; circle: any }; +} +type CallBack = (...args: any[]) => any; +/** + * 网格索引,相比 @mapbox/grid-index,在简单计算碰撞检测结果时效率更高 + * @see https://zhuanlan.zhihu.com/p/74373214 + */ +class GridIndex { + private boxCells: number[][]; + private xCellCount: number; + private yCellCount: number; + private boxKeys: string[]; + private bboxes: number[]; + private width: number; + private height: number; + private xScale: number; + private yScale: number; + private boxUid: number; + + constructor(width: number, height: number, cellSize: number) { + 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; + } + + public insert(key: any, x1: number, y1: number, x2: number, y2: number) { + 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); + } + + public query( + x1: number, + y1: number, + x2: number, + y2: number, + predicate?: CallBack, + ) { + return this.queryHitTest(x1, y1, x2, y2, false, predicate); + } + + public hitTest( + x1: number, + y1: number, + x2: number, + y2: number, + predicate?: CallBack, + ) { + return this.queryHitTest(x1, y1, x2, y2, true, predicate); + } + + private insertBoxCell( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + uid: number, + ) { + this.boxCells[cellIndex].push(uid); + } + + private queryHitTest( + x1: number, + y1: number, + x2: number, + y2: number, + hitTest: boolean, + predicate?: CallBack, + ) { + if (x2 < 0 || x1 > this.width || y2 < 0 || y1 > this.height) { + return hitTest ? false : []; + } + const result: any[] = []; + 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; + } + + private queryCell( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + result: any[], + queryArgs?: any, + predicate?: CallBack, + ) { + 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; + } + + private forEachCell( + x1: number, + y1: number, + x2: number, + y2: number, + fn: CallBack, + arg1: any[] | number, + arg2?: IQueryArgs, + predicate?: CallBack, + ) { + 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; + } + } + } + } + + private convertToXCellCoord(x: number) { + return Math.max( + 0, + Math.min(this.xCellCount - 1, Math.floor(x * this.xScale)), + ); + } + + private convertToYCellCoord(y: number) { + return Math.max( + 0, + Math.min(this.yCellCount - 1, Math.floor(y * this.yScale)), + ); + } +} + +export default GridIndex; diff --git a/packages/layers/src/utils/symbol-layout.ts b/packages/layers/src/utils/symbol-layout.ts index 204e192395..4ccf46d2d9 100644 --- a/packages/layers/src/utils/symbol-layout.ts +++ b/packages/layers/src/utils/symbol-layout.ts @@ -1,3 +1,22 @@ +interface IPoint { + x: number; + y: number; +} +export interface IGlyphQuad { + tr: IPoint; + tl: IPoint; + bl: IPoint; + br: IPoint; + tex: { + x: number; + y: number; + height: number; + width: number; + advance: number; + }; + glyphOffset: [number, number]; +} + /** * 返回文本相对锚点位置 * @param {string} anchor 锚点位置 @@ -181,7 +200,7 @@ export function shapeText( textAnchor: string, textJustify: string, spacing: number, - translate: [number, number], + translate: [number, number] = [0, 0], ) { // TODO:处理换行 const lines = text.split('\n'); @@ -215,11 +234,11 @@ export function shapeText( export function getGlyphQuads( shaping: any, - textOffset: [number, number], + textOffset: [number, number] = [0, 0], alongLine: boolean, -) { +): IGlyphQuad[] { const { positionedGlyphs } = shaping; - const quads = []; + const quads: IGlyphQuad[] = []; for (const positionedGlyph of positionedGlyphs) { const rect = positionedGlyph.metrics; @@ -229,7 +248,7 @@ export function getGlyphQuads( const halfAdvance = (rect.advance * positionedGlyph.scale) / 2; - const glyphOffset = alongLine + const glyphOffset: [number, number] = alongLine ? [positionedGlyph.x + halfAdvance, positionedGlyph.y] : [0, 0]; diff --git a/stories/Layers/Layers.stories.tsx b/stories/Layers/Layers.stories.tsx index 2e0ee5fda8..b86b68a2fc 100644 --- a/stories/Layers/Layers.stories.tsx +++ b/stories/Layers/Layers.stories.tsx @@ -15,6 +15,7 @@ import PointImage from './components/PointImage'; import Polygon3D from './components/Polygon3D'; import ImageLayerDemo from './components/RasterImage'; import RasterLayerDemo from './components/RasterLayer'; +import TextLayerDemo from './components/Text'; // @ts-ignore storiesOf('图层', module) @@ -22,6 +23,7 @@ storiesOf('图层', module) .add('数据更新', () => ) .add('亮度图', () => ) .add('3D点', () => ) + .add('文字', () => ) .add('Column', () => ) .add('图片标注', () => ) .add('面3d图层', () => ) diff --git a/stories/Layers/components/Line.tsx b/stories/Layers/components/Line.tsx index f414e011c5..c70aefa5fc 100644 --- a/stories/Layers/components/Line.tsx +++ b/stories/Layers/components/Line.tsx @@ -12,40 +12,30 @@ export default class LineDemo extends React.Component { public async componentDidMount() { const response = await fetch( - 'https://gw.alipayobjects.com/os/rmsportal/ZVfOvhVCzwBkISNsuKCc.json', + 'https://arcgis.github.io/arcgis-samples-javascript/sample-data/custom-gl-animated-lines/lines.json', ); const scene = new Scene({ id: 'map', map: new Mapbox({ - center: [102.602992, 23.107329], - pitch: 0, - style: 'mapbox://styles/mapbox/dark-v9', - zoom: 13, + center: [-74.006, 40.7128], + zoom: 15, + style: 'dark', }), }); const lineLayer = new LineLayer() - .source(await response.json()) - .size(1) + .source(await response.json(), { + parser: { + type: 'json', + coordinates: 'path', + }, + }) + .size(3) .shape('line') - .color( - 'ELEV', - [ - '#E8FCFF', - '#CFF6FF', - '#A1E9ff', - '#65CEF7', - '#3CB1F0', - '#2894E0', - '#1772c2', - '#105CB3', - '#0D408C', - '#002466', - ].reverse(), - ) + .color('red') .animate({ - interval: 0.1, - trailLength: 0.05, - duration: 2, + interval: 0.5, + trailLength: 0.2, + duration: 4, }) .style({ lineType: 'solid', diff --git a/stories/Layers/components/Point.tsx b/stories/Layers/components/Point.tsx index 4b092dba1a..d0f41040a8 100644 --- a/stories/Layers/components/Point.tsx +++ b/stories/Layers/components/Point.tsx @@ -36,6 +36,7 @@ export default class Point3D extends React.Component { type: 'quantile', }) .size('point_count', [5, 10, 15, 20, 25]) + .animate(false) .color('yellow') .style({ opacity: 0.5, diff --git a/stories/Layers/components/Text.tsx b/stories/Layers/components/Text.tsx index fca96db54f..12bc4d9c21 100644 --- a/stories/Layers/components/Text.tsx +++ b/stories/Layers/components/Text.tsx @@ -1,61 +1,82 @@ import { PointLayer, Scene } from '@antv/l7'; +import { GaodeMap, Mapbox } from '@antv/l7-maps'; import * as React from 'react'; +// @ts-ignore import data from '../data/data.json'; -export default class Point3D extends React.Component { +export default class TextLayerDemo extends React.Component { + // @ts-ignore private scene: Scene; public componentWillUnmount() { this.scene.destroy(); } - public componentDidMount() { - const scene = new Scene({ - center: [120.19382669582967, 30.258134], - id: 'map', - pitch: 0, - type: 'mapbox', - style: 'mapbox://styles/mapbox/streets-v9', - zoom: 1, - }); - const pointLayer = new PointLayer({}); - const p1 = { + public async componentDidMount() { + const data = { type: 'FeatureCollection', features: [ { type: 'Feature', - properties: {}, + properties: { + name: '中华人民共和国', + }, geometry: { type: 'Point', - coordinates: [83.671875, 44.84029065139799], + coordinates: [103.0078125, 36.03133177633187], + }, + }, + { + type: 'Feature', + properties: { + name: '中华人民共和国', + }, + geometry: { + type: 'Point', + coordinates: [122.6953125, 10.833305983642491], }, }, ], }; - pointLayer - .source(data) - .color('name', [ - '#FFF5B8', - '#FFDC7D', - '#FFAB5C', - '#F27049', - '#D42F31', - '#730D1C', - ]) - .shape('subregion', [ - 'circle', - 'triangle', - 'square', - 'pentagon', - 'hexagon', - 'octogon', - 'hexagram', - 'rhombus', - 'vesica', - ]) - .size('scalerank', [2, 4, 6, 8, 10]); + const response = await fetch( + 'https://gw.alipayobjects.com/os/rmsportal/oVTMqfzuuRFKiDwhPSFL.json', + ); + const pointsData = await response.json(); + + const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + center: [120.19382669582967, 30.258134], + pitch: 0, + style: 'dark', + zoom: 3, + }), + }); + // scene.on('loaded', () => { + const pointLayer = new PointLayer({}) + .source(pointsData.list, { + parser: { + type: 'json', + x: 'j', + y: 'w', + }, + }) + .shape('m', 'text') + .size(24) + .color('#fff') + .style({ + fontWeight: 800, + textAnchor: 'center', // 文本相对锚点的位置 center|left|right|top|bottom|top-left + textOffset: [0, 0], // 文本相对锚点的偏移量 [水平, 垂直] + spacing: 2, // 字符间距 + padding: [4, 4], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近 + strokeColor: 'white', // 描边颜色 + strokeWidth: 4, // 描边宽度 + strokeOpacity: 1.0, + }); scene.addLayer(pointLayer); - scene.render(); + this.scene = scene; + // }); } public render() {