mirror of https://gitee.com/antv-l7/antv-l7
Shihui (#996)
* docs: add keywords * feat: 升级部分官网 demo,增加 bloom 效果 * feat: 增加 pointLayer fillImage 模式的 demo * style: lint style * docs: 补充后处理相关文档 * style: lint style * docs: 完善图层基础方法
This commit is contained in:
parent
7af21ea83e
commit
cf326c047c
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: MultiPass
|
||||
order: 10
|
||||
---
|
||||
|
||||
`markdown:docs/api/pass.zh.md`
|
|
@ -0,0 +1,348 @@
|
|||
---
|
||||
title: 后处理模块
|
||||
order: 10
|
||||
---
|
||||
|
||||
后处理(Post-Process Effect)是 3D 渲染常见的处理效果,是一种对渲染之后的画面进行再加工的技术,一般用于实现各种特效。L7 的后处理模块为用户提供了一些常见的后处理效果,同时也提供了标准规范,允许用户自定义后处理效果。
|
||||
|
||||
🌟 需要注意的是,使用后处理通常会产生额外的性能消耗,用户应该根据项目的实际情况合理使用后处理。
|
||||
|
||||
## 使用
|
||||
|
||||
```jsx
|
||||
const layer = new LineLayer({
|
||||
enableMultiPassRenderer: true,
|
||||
passes: [
|
||||
[
|
||||
'bloom',
|
||||
{
|
||||
bloomBaseRadio: 0.8,
|
||||
bloomRadius: 2,
|
||||
bloomIntensity: 1
|
||||
}
|
||||
]
|
||||
]
|
||||
}).source(data)
|
||||
.size('ELEV', h => [ h % 50 === 0 ? 1.0 : 0.5, (h - 1300) * 0.2 ])
|
||||
.shape('line')
|
||||
.scale('ELEV', {
|
||||
type: 'quantize'
|
||||
})
|
||||
.color('ELEV', [
|
||||
'#094D4A',
|
||||
...
|
||||
]);
|
||||
scene.addLayer(layer);
|
||||
```
|
||||
|
||||
<img width="60%" style="display: block;margin: 0 auto;" alt="案例" src='https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*PIXmQ6m1C10AAAAAAAAAAAAAARQnAQ' />
|
||||
|
||||
[在线案例](../../examples/line/isoline#ele_dark)
|
||||
|
||||
### 开启后处理
|
||||
|
||||
为了开启图层的后处理能力,我们需要在初始化图层的时候配置 enableMultiPassRenderer 为 true,同时传入该图层作用的处理效果配置。
|
||||
|
||||
```javascript
|
||||
let pointLayer = new PointLayer({
|
||||
zIndex: 1,
|
||||
enableMultiPassRenderer: false,
|
||||
passes: [
|
||||
[
|
||||
'bloom',
|
||||
{
|
||||
bloomBaseRadio: 0.95,
|
||||
bloomRadius: 4,
|
||||
bloomIntensity: 1.1,
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
- enableMultiPassRenderer 配置该图层是否开始后处理能力
|
||||
- passes 后处理配置列表
|
||||
🌟 passes 需要根据一定的规则配置
|
||||
|
||||
### 单图层后处理
|
||||
|
||||
传统的后处理渲染往往会对场景中所有的对象做统一的后处理,而许多时候我们只需要对场景中的一部分内容做后处理。L7 的后处理模块天然支持以图层为单位进行后处理,这使的用户对 L7 场景内容的处理有更高的自由度。
|
||||
|
||||
### update pass options
|
||||
|
||||
用户在初始化完图层对象之后,若想调整后处理效果的参数,可以直接使用 style 方法
|
||||
|
||||
```javascript
|
||||
layer.style({
|
||||
passes: [
|
||||
[
|
||||
'colorHalftone',
|
||||
{
|
||||
// 更新 cenrter 的位置
|
||||
center: [newX, newY],
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
scene.render();
|
||||
```
|
||||
|
||||
### setMultiPass(enableMultiPassRenderer: boolean, passes?: pass[])
|
||||
|
||||
为了方便用户切换后处理的状态(开启、关闭后处理),我们为用户提供了专门的方法
|
||||
|
||||
```javascript
|
||||
// 当前图层存在 multiPass,我们需要关闭时
|
||||
// 直接关闭
|
||||
layer.setMultiPass(false);
|
||||
// 关闭的同时清除 passes
|
||||
layer.setMultiPass(false, []);
|
||||
|
||||
// 当前图层不存在 multiPass,我们需要开启时
|
||||
// 图层初始化时已经传入 passes
|
||||
const layer = new PolygonLayer({
|
||||
zIndex: 0,
|
||||
enableMultiPassRenderer: false,
|
||||
passes: [
|
||||
[
|
||||
'bloom',
|
||||
{
|
||||
bloomBaseRadio: 0.5,
|
||||
bloomRadius: 20,
|
||||
bloomIntensity: 1,
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
layer.setMultiPass(true);
|
||||
|
||||
// 图层初始化时没有传入 passes
|
||||
layer.setMultiPass(true, [
|
||||
[
|
||||
'bloom',
|
||||
{
|
||||
bloomRadius: 10,
|
||||
bloomIntensity: 1,
|
||||
},
|
||||
],
|
||||
]);
|
||||
```
|
||||
|
||||
### 后处理链路
|
||||
|
||||
passes 可以传入多种后处理,普通渲染的结果是第一个后处理的输入,前一种后处理的输出是后一个后处理的输入,最后的结果输出到屏幕。
|
||||
|
||||
### 预制的后处理
|
||||
|
||||
L7 的后处理模块预置了几种后处理效果,因此用户可以直接在 passes 中配置使用。
|
||||
|
||||
#### bloom
|
||||
|
||||
<img width="40%" style="display: block;margin: 0 auto;" alt="案例" src='https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*I3FCSo-gZR4AAAAAAAAAAAAAARQnAQ' />
|
||||
|
||||
```javascript
|
||||
const bloomPass = [
|
||||
'bloom',
|
||||
{
|
||||
bloomBaseRadio: 0.5,
|
||||
bloomRadius: 20,
|
||||
bloomIntensity: 1,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
辉光后处理
|
||||
|
||||
- bloomBaseRadio
|
||||
设置保持图形原本样式的比例,值在 0 - 1 之间,值为 1 时完全保存本身的样式
|
||||
- bloomRadius
|
||||
设置 bloom 的半径,值越大,bloom 范围越大
|
||||
- bloomIntensity
|
||||
设置 bloom 的强度,值越大,辉光越强
|
||||
|
||||
#### blurV/blurH
|
||||
|
||||
<img width="40%" style="display: block;margin: 0 auto;" alt="案例" src='https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*NrNGSIQuZ4oAAAAAAAAAAAAAARQnAQ' />
|
||||
|
||||
垂直方向模糊/水平方向模糊
|
||||
|
||||
```javascript
|
||||
const blurVPass = [
|
||||
'blurV',
|
||||
{
|
||||
blurRadius: 5,
|
||||
},
|
||||
];
|
||||
const blurHPass = [
|
||||
'blurH',
|
||||
{
|
||||
blurRadius: 5,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
- blurRadius
|
||||
设置模糊半径
|
||||
|
||||
#### colorHalftone
|
||||
|
||||
<img width="40%" style="display: block;margin: 0 auto;" alt="案例" src='https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*QstwSr4dj20AAAAAAAAAAAAAARQnAQ' />
|
||||
|
||||
colorHalftone
|
||||
|
||||
```javascript
|
||||
const colorHalftonePass = [
|
||||
'colorHalftone',
|
||||
{
|
||||
angle: 0,
|
||||
size: 8,
|
||||
centerX: 0.5,
|
||||
centerY: 0.5,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
- angle
|
||||
设置角度
|
||||
- size
|
||||
设置大小
|
||||
- centerX
|
||||
设置中心点 X
|
||||
- centerY
|
||||
设置中心点 Y
|
||||
|
||||
#### hexagonalPixelate
|
||||
|
||||
<img width="40%" style="display: block;margin: 0 auto;" alt="案例" src='https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*IQOMQrbDQ5IAAAAAAAAAAAAAARQnAQ' />
|
||||
|
||||
六边形像素
|
||||
|
||||
```javascript
|
||||
const hexagonalPixelatePass = [
|
||||
'hexagonalPixelate',
|
||||
{
|
||||
scale: 10,
|
||||
centerX: 0.5,
|
||||
centerY: 0.5,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
- scale
|
||||
设置缩放
|
||||
- centerX
|
||||
设置中心点 X
|
||||
- centerY
|
||||
设置中心点 Y
|
||||
|
||||
#### ink
|
||||
|
||||
<img width="40%" style="display: block;margin: 0 auto;" alt="案例" src='https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*IpogQbe-5K4AAAAAAAAAAAAAARQnAQ' />
|
||||
|
||||
ink
|
||||
|
||||
```javascript
|
||||
const inkPass = [
|
||||
'ink',
|
||||
{
|
||||
strength: 1,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
- strength
|
||||
设置强度
|
||||
|
||||
#### noise
|
||||
|
||||
<img width="40%" style="display: block;margin: 0 auto;" alt="案例" src='https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*6lcVS7YFrvUAAAAAAAAAAAAAARQnAQ' />
|
||||
|
||||
噪声
|
||||
|
||||
```javascript
|
||||
const noisePass = [
|
||||
'noise',
|
||||
{
|
||||
amount: 1,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
- amount
|
||||
设置噪点数量
|
||||
|
||||
### 自定义后处理
|
||||
|
||||
用户通过 L7 定义的标准可以轻松的自定义后处理效果。
|
||||
|
||||
```javascript
|
||||
import { BasePostProcessingPass, PolygonLayer, Scene } from '@antv/l7';
|
||||
|
||||
interface IDotScreenEffectConfig {
|
||||
center: [number, number]; // pattern 圆心
|
||||
angle: number; // dot 旋转角度
|
||||
size: number; // dot 尺寸
|
||||
}
|
||||
|
||||
class DotScreenEffect extends BasePostProcessingPass<IDotScreenEffectConfig> {
|
||||
protected setupShaders() {
|
||||
this.shaderModuleService.registerModule('dotScreenEffect', {
|
||||
vs: this.quad,
|
||||
fs: `
|
||||
varying vec2 v_UV;
|
||||
|
||||
uniform sampler2D u_Texture;
|
||||
uniform vec2 u_ViewportSize : [1.0, 1.0];
|
||||
uniform vec2 u_Center : [0.5, 0.5];
|
||||
uniform float u_Angle : 1;
|
||||
uniform float u_Size : 3;
|
||||
|
||||
float pattern(vec2 texSize, vec2 texCoord) {
|
||||
float scale = 3.1415 / u_Size;
|
||||
float s = sin(u_Angle), c = cos(u_Angle);
|
||||
vec2 tex = texCoord * texSize - u_Center * texSize;
|
||||
vec2 point = vec2(
|
||||
c * tex.x - s * tex.y,
|
||||
s * tex.x + c * tex.y
|
||||
) * scale;
|
||||
return (sin(point.x) * sin(point.y)) * 4.0;
|
||||
}
|
||||
vec4 dotScreen_filterColor(vec4 color, vec2 texSize, vec2 texCoord) {
|
||||
float average = (color.r + color.g + color.b) / 3.0;
|
||||
return vec4(vec3(average * 10.0 - 5.0 + pattern(texSize, texCoord)), color.a);
|
||||
}
|
||||
|
||||
void main() {
|
||||
gl_FragColor = vec4(texture2D(u_Texture, v_UV));
|
||||
gl_FragColor = dotScreen_filterColor(gl_FragColor, u_ViewportSize, v_UV);
|
||||
}
|
||||
`,
|
||||
});
|
||||
const { vs, fs, uniforms } = this.shaderModuleService.getModule('dotScreenEffect');
|
||||
const { width, height } = this.rendererService.getViewportSize();
|
||||
return {
|
||||
vs,
|
||||
fs,
|
||||
uniforms: {
|
||||
...uniforms,
|
||||
u_ViewportSize: [width, height],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 注册自定义后处理效果
|
||||
scene.registerPostProcessingPass(DotScreenEffect, 'dotScreenEffect');
|
||||
const layer = new PolygonLayer({
|
||||
enableMultiPassRenderer: true,
|
||||
passes: [
|
||||
[
|
||||
'dotScreenEffect',
|
||||
{
|
||||
size: 8,
|
||||
angle: 1,
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
```
|
|
@ -54,4 +54,51 @@ const scatter = new PointLayer()
|
|||
|
||||
[在线案例](../../../examples/point/image#image)
|
||||
|
||||
### layerType = "fillImage"
|
||||
|
||||
🌟 默认通过 PointLayer 实例化的 image 本质上是精灵贴图,因此有始终面向相机的特性,同时贴图的大小也收到设备的限制。
|
||||
🌟 由于精灵始终面向相机,因此我们也无法自定义配置 image 的旋转角度
|
||||
|
||||
为了解决上述的两个问题(1. 大小受限,2. 无法自定义旋转角度),我们单独提供了 fillimage 的模式。
|
||||
只需要在初始化图层的时候提前指定 layerType 为 fillImage,其他使用与普通的 image 完全相同。
|
||||
|
||||
```javascript
|
||||
const imageLayer = new PointLayer({ layerType: 'fillImage' })
|
||||
.source(data, {
|
||||
parser: {
|
||||
type: 'json',
|
||||
x: 'longitude',
|
||||
y: 'latitude',
|
||||
},
|
||||
})
|
||||
.shape('name', ['00', '01', '02'])
|
||||
.style({
|
||||
rotation: 0,
|
||||
})
|
||||
.active({
|
||||
color: '#0ff',
|
||||
mix: 0.5,
|
||||
})
|
||||
.size(45);
|
||||
scene.addLayer(imageLayer);
|
||||
|
||||
let r = 0;
|
||||
rotate();
|
||||
function rotate() {
|
||||
r += 0.2;
|
||||
imageLayer.style({
|
||||
rotation: r,
|
||||
});
|
||||
scene.render();
|
||||
requestAnimationFrame(rotate);
|
||||
}
|
||||
```
|
||||
|
||||
- rotation: number|undefined
|
||||
我们支持使用 rotation 自定义配置图标的旋转角度(顺时针方向、角度制)
|
||||
|
||||
<img width="60%" style="display: block;margin: 0 auto;" alt="案例" src='https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*1kBZTaains4AAAAAAAAAAAAAARQnAQ'>
|
||||
|
||||
[在线案例](../../../examples/point/image#fillimage)
|
||||
|
||||
`markdown:docs/common/layer/base.md`
|
||||
|
|
|
@ -78,6 +78,58 @@ layer.select(false);
|
|||
layer.setSelect(featureId);
|
||||
```
|
||||
|
||||
### setAutoFit(autoFit: boolean)
|
||||
让用户可以主动设置图层的 autoFit 参数
|
||||
🌟 设置完该方法后会在图层发生更新的时候生效,如在 setData 之后触发
|
||||
|
||||
```javascript
|
||||
// 使用方法
|
||||
layer.setAutoFit(true);
|
||||
// 内部实现
|
||||
public setAutoFit(autoFit: boolean): ILayer {
|
||||
this.updateLayerConfig({
|
||||
autoFit,
|
||||
});
|
||||
return this;
|
||||
}
|
||||
```
|
||||
|
||||
### getScale(attr: string)
|
||||
支持单独获取某个图形经过 scale 计算后的值, 满足用户获取图层某些 feature 值的需求。
|
||||
- attr scale 的属性值
|
||||
|
||||
```javascript
|
||||
const data = [
|
||||
{lng: 120, lat: 30, name: 'n1'},
|
||||
{lng: 120, lat: 30, name: 'n2'}
|
||||
]
|
||||
const layer = new PointLayer()
|
||||
.source(data, {
|
||||
parser: {
|
||||
x: 'lng',
|
||||
y: 'lat',
|
||||
type: 'json'
|
||||
}
|
||||
})
|
||||
.shape('circle')
|
||||
.color('name', ['#f00', '#ff0'])
|
||||
.size('name', [20, 40])
|
||||
|
||||
scene.addLayer(layer)
|
||||
|
||||
|
||||
// 此时在 scene 上绘制两个点
|
||||
// 一个颜色为黄色,大小为 40 的点,对应 name 为 n1
|
||||
// 一个颜色为红色,大小为 20 的点,对应 name 为 n2
|
||||
|
||||
const colorScale = layer.getScale('color'); // 获取 color 方法产生的 scale
|
||||
const color1 = colorScale('n1'); // '#ff0'
|
||||
const color1 = colorScale('n2'); // '#f00'
|
||||
|
||||
const sizeScale = layer.getScale('size'); // 获取 size 方法产生的 scale
|
||||
const size1 = sizeScale('n1'); // 40
|
||||
const size2 = sizeScale('n2'); // 20
|
||||
```
|
||||
### getLegendItems(type: string)
|
||||
|
||||
获取图例配置
|
||||
|
|
|
@ -91,7 +91,21 @@ scene.on('loaded', () => {
|
|||
.style({
|
||||
opacity: 1.0
|
||||
});
|
||||
const flyLine = new LineLayer({ blend: 'additive', zIndex: 2 })
|
||||
const flyLine = new LineLayer({
|
||||
blend: 'additive',
|
||||
zIndex: 2,
|
||||
enableMultiPassRenderer: true,
|
||||
passes: [
|
||||
[
|
||||
'bloom',
|
||||
{
|
||||
bloomBaseRadio: 0.8,
|
||||
bloomRadius: 2,
|
||||
bloomIntensity: 1
|
||||
}
|
||||
]
|
||||
]
|
||||
})
|
||||
.source(flydata, {
|
||||
parser: {
|
||||
type: 'json',
|
||||
|
|
|
@ -40,7 +40,7 @@ scene.on('loaded', () => {
|
|||
duration: 5
|
||||
})
|
||||
.style({
|
||||
opacity: 1
|
||||
opacity: 0.5
|
||||
});
|
||||
scene.addLayer(layer);
|
||||
});
|
||||
|
|
|
@ -16,7 +16,18 @@ scene.on('loaded', () => {
|
|||
.then(res => res.text())
|
||||
.then(data => {
|
||||
const layer = new LineLayer({
|
||||
blend: 'normal'
|
||||
blend: 'normal',
|
||||
enableMultiPassRenderer: true,
|
||||
passes: [
|
||||
[
|
||||
'bloom',
|
||||
{
|
||||
bloomBaseRadio: 0.8,
|
||||
bloomRadius: 2,
|
||||
bloomIntensity: 1
|
||||
}
|
||||
]
|
||||
]
|
||||
})
|
||||
.source(data, {
|
||||
parser: {
|
||||
|
|
|
@ -16,7 +16,19 @@ scene.on('loaded', () => {
|
|||
fetch('https://gw.alipayobjects.com/os/rmsportal/ZVfOvhVCzwBkISNsuKCc.json')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const layer = new LineLayer({})
|
||||
const layer = new LineLayer({
|
||||
enableMultiPassRenderer: true,
|
||||
passes: [
|
||||
[
|
||||
'bloom',
|
||||
{
|
||||
bloomBaseRadio: 0.8,
|
||||
bloomRadius: 2,
|
||||
bloomIntensity: 1
|
||||
}
|
||||
]
|
||||
]
|
||||
})
|
||||
.source(data)
|
||||
.size('ELEV', h => {
|
||||
return [ h % 50 === 0 ? 1.0 : 0.5, (h - 1300) * 0.2 ];
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { Scene, PointLayer } from '@antv/l7';
|
||||
import { GaodeMap } from '@antv/l7-maps';
|
||||
|
||||
const scene = new Scene({
|
||||
id: 'map',
|
||||
map: new GaodeMap({
|
||||
pitch: 0,
|
||||
style: 'light',
|
||||
center: [ 121.434765, 31.256735 ],
|
||||
zoom: 14.83
|
||||
})
|
||||
});
|
||||
scene.on('loaded', () => {
|
||||
fetch(
|
||||
'https://gw.alipayobjects.com/os/basement_prod/893d1d5f-11d9-45f3-8322-ee9140d288ae.json'
|
||||
)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
scene.addImage(
|
||||
'00',
|
||||
'https://gw.alipayobjects.com/zos/basement_prod/604b5e7f-309e-40db-b95b-4fac746c5153.svg'
|
||||
);
|
||||
scene.addImage(
|
||||
'01',
|
||||
'https://gw.alipayobjects.com/zos/basement_prod/30580bc9-506f-4438-8c1a-744e082054ec.svg'
|
||||
);
|
||||
scene.addImage(
|
||||
'02',
|
||||
'https://gw.alipayobjects.com/zos/basement_prod/7aa1f460-9f9f-499f-afdf-13424aa26bbf.svg'
|
||||
);
|
||||
const imageLayer = new PointLayer({ layerType: 'fillImage' })
|
||||
.source(data, {
|
||||
parser: {
|
||||
type: 'json',
|
||||
x: 'longitude',
|
||||
y: 'latitude'
|
||||
}
|
||||
})
|
||||
.shape('name', [ '00', '01', '02' ])
|
||||
.active({
|
||||
color: '#0ff',
|
||||
mix: 0.5
|
||||
})
|
||||
.size(45);
|
||||
scene.addLayer(imageLayer);
|
||||
|
||||
let r = 0;
|
||||
rotate();
|
||||
function rotate() {
|
||||
r += 0.2;
|
||||
imageLayer.style({
|
||||
rotation: r
|
||||
});
|
||||
scene.render();
|
||||
requestAnimationFrame(rotate);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -9,6 +9,11 @@
|
|||
"title": "符号图",
|
||||
"screenshot": "https://gw.alipayobjects.com/mdn/antv_site/afts/img/A*oVyHT5S3sv0AAAAAAAAAAABkARQnAQ"
|
||||
},
|
||||
{
|
||||
"filename": "fillimage.js",
|
||||
"title": "贴地符号图",
|
||||
"screenshot": "https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*1kBZTaains4AAAAAAAAAAAAAARQnAQ"
|
||||
},
|
||||
{
|
||||
"filename": "locate.js",
|
||||
"title": "精确符号",
|
||||
|
|
|
@ -14,6 +14,7 @@ module.exports = {
|
|||
'Large-scale WebGL-powered Geospatial data visualization analysis framework',
|
||||
siteUrl: 'https://l7.antv.vision',
|
||||
githubUrl: 'https://github.com/antvis/L7',
|
||||
keywords: 'l7, L7, antv/l7, 地理, 空间可视化, Webgl, webgl, 地图, webgis, 3d, GIS, gis, Mapbox, deckgl, g2, g6, antv,',
|
||||
showChartResize: true, // 是否在demo页展示图表视图切换
|
||||
showAPIDoc: true, // 是否在demo页展示API文档
|
||||
navs: [
|
||||
|
@ -229,6 +230,14 @@ module.exports = {
|
|||
},
|
||||
order: 9,
|
||||
},
|
||||
{
|
||||
slug: 'api/pass',
|
||||
title: {
|
||||
zh: '后处理模块',
|
||||
en: 'MultiPass',
|
||||
},
|
||||
order: 10,
|
||||
},
|
||||
{
|
||||
slug: 'api/district',
|
||||
title: {
|
||||
|
|
Loading…
Reference in New Issue