mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-01-19 04:19:30 +08:00
点光源
This commit is contained in:
parent
3c7b5906f6
commit
5e0d8ac7d4
@ -3202,6 +3202,7 @@ maps.prototype.removeBlock = function (x, y, floorId) {
|
||||
const block = blocks[i];
|
||||
this.removeBlockByIndex(i, floorId);
|
||||
this._removeBlockFromMap(floorId, block);
|
||||
core.updateShadow(true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -3364,6 +3365,7 @@ maps.prototype.setBlock = function (number, x, y, floorId, noredraw) {
|
||||
}
|
||||
}
|
||||
}
|
||||
core.updateShadow(true);
|
||||
};
|
||||
|
||||
maps.prototype.animateSetBlock = function (
|
||||
|
@ -4226,7 +4226,8 @@ ui.prototype.deleteCanvas = function (name) {
|
||||
|
||||
////// 删除所有动态canvas //////
|
||||
ui.prototype.deleteAllCanvas = function () {
|
||||
return this.deleteCanvas(function () {
|
||||
this.deleteCanvas(function () {
|
||||
return true;
|
||||
});
|
||||
if (main.mode === 'play' && !core.isReplaying()) core.initShadowCanvas();
|
||||
};
|
||||
|
@ -31,6 +31,13 @@ main.floors.MT42=
|
||||
5,
|
||||
0
|
||||
]
|
||||
},
|
||||
"8,12": {
|
||||
"floorId": "MT46",
|
||||
"loc": [
|
||||
7,
|
||||
8
|
||||
]
|
||||
}
|
||||
},
|
||||
"beforeBattle": {},
|
||||
|
@ -26,20 +26,46 @@ main.floors.MT46=
|
||||
"cannotMove": {},
|
||||
"cannotMoveIn": {},
|
||||
"map": [
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
[ 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
|
||||
[ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0],
|
||||
[ 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
[ 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
[ 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0],
|
||||
[ 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1],
|
||||
[ 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0],
|
||||
[ 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0],
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
|
||||
[ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
|
||||
[ 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
|
||||
[ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0],
|
||||
[ 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
|
||||
[ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
|
||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0]
|
||||
],
|
||||
"bgmap": [
|
||||
[300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
|
||||
[300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
|
||||
[300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
|
||||
[300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
|
||||
[300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
|
||||
[300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
|
||||
[300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
|
||||
[300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
|
||||
[300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
|
||||
[300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
|
||||
[300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
|
||||
[300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
|
||||
[300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
|
||||
[300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
|
||||
[300,300,300,300,300,300,300,300,300,300,300,300,300,300,300]
|
||||
],
|
||||
"fgmap": [
|
||||
|
||||
],
|
||||
"bg2map": [
|
||||
|
||||
],
|
||||
"fg2map": [
|
||||
|
||||
]
|
||||
}
|
@ -150,6 +150,8 @@ var functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a = {
|
||||
// ---------- 重绘新地图;这一步将会设置core.status.floorId ---------- //
|
||||
core.drawMap(floorId);
|
||||
|
||||
core.updateShadow();
|
||||
|
||||
// 切换楼层BGM
|
||||
if (core.status.maps[floorId].bgm) {
|
||||
var bgm = core.status.maps[floorId].bgm;
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = {
|
||||
init: function () {
|
||||
// 只看插件没用,插件是与vite样板高度融合的,所以要看的话就在游戏内的百科全书-关于游戏内点那个开源地址吧
|
||||
this._afterLoadResources = function () {
|
||||
if (!main.replayChecking && main.mode === 'play') {
|
||||
main.forward();
|
||||
@ -770,12 +771,7 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = {
|
||||
core.status.hero = new Proxy(hero, handler);
|
||||
|
||||
core.status.maps[floorId].blocks.forEach(function (block) {
|
||||
if (
|
||||
block.event.cls !== 'items' ||
|
||||
block.event.id === 'superPotion' ||
|
||||
block.disable
|
||||
)
|
||||
return;
|
||||
if (block.event.cls !== 'items' || block.disable) return;
|
||||
const x = block.x,
|
||||
y = block.y;
|
||||
// v2优化,只绘制范围内的部分
|
||||
|
@ -11,6 +11,9 @@ import chapter from './plugin/ui/chapter';
|
||||
import fly from './plugin/ui/fly';
|
||||
import chase from './plugin/chase/chase';
|
||||
import fixed from './plugin/ui/fixed';
|
||||
import webglUtils from './plugin/webgl/utils';
|
||||
import shadow from './plugin/webgl/shadow';
|
||||
import gameShadow from './plugin/webgl/gameShadow';
|
||||
|
||||
function forward() {
|
||||
// 每个引入的插件都要在这里执行,否则不会被转发
|
||||
@ -26,7 +29,10 @@ function forward() {
|
||||
chapter(),
|
||||
fly(),
|
||||
chase(),
|
||||
fixed()
|
||||
fixed(),
|
||||
webglUtils(),
|
||||
shadow(),
|
||||
gameShadow()
|
||||
];
|
||||
|
||||
// 初始化所有插件,并转发到core上
|
||||
|
@ -1,3 +1,4 @@
|
||||
/// <reference path="../types/core.d.ts" />
|
||||
import { message } from 'ant-design-vue';
|
||||
import { MessageApi } from 'ant-design-vue/lib/message';
|
||||
import { isNil } from 'lodash';
|
||||
@ -69,7 +70,7 @@ export function keycode(key: number) {
|
||||
* @param css 要解析的css字符串
|
||||
*/
|
||||
export function parseCss(css: string): Partial<Record<CanParseCss, string>> {
|
||||
const str = css.replace(/[\n\s\t]*/g, '').replace(/[;,]*/g, ';');
|
||||
const str = css.replace(/[\n\s\t]*/g, '').replace(/;*/g, ';');
|
||||
const styles = str.split(';');
|
||||
const res: Partial<Record<CanParseCss, string>> = {};
|
||||
|
||||
|
123
src/plugin/webgl/canvas.ts
Normal file
123
src/plugin/webgl/canvas.ts
Normal file
@ -0,0 +1,123 @@
|
||||
const glMap: Record<string, WebGLRenderingContext> = {};
|
||||
|
||||
/**
|
||||
* 创建一个以webgl为绘制上下文的画布
|
||||
* @param id 画布id
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
* @param w 宽度
|
||||
* @param h 高度
|
||||
* @param z 纵深
|
||||
*/
|
||||
export function createWebGLCanvas(
|
||||
id: string,
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
z: number
|
||||
) {
|
||||
if (id in glMap) {
|
||||
deleteWebGLCanvas(id);
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
const gl = canvas.getContext('webgl')!;
|
||||
const s = core.domStyle.scale;
|
||||
canvas.style.left = `${x * s}px`;
|
||||
canvas.style.top = `${y * s}px`;
|
||||
canvas.style.width = `${w * s}px`;
|
||||
canvas.style.height = `${h * s}px`;
|
||||
canvas.style.zIndex = `${z}`;
|
||||
canvas.width = w * s * devicePixelRatio;
|
||||
canvas.height = h * s * devicePixelRatio;
|
||||
core.dom.gameDraw.appendChild(canvas);
|
||||
return gl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一个webgl画布
|
||||
* @param id 画布id
|
||||
*/
|
||||
export function deleteWebGLCanvas(id: string) {
|
||||
const gl = glMap[id];
|
||||
if (!gl) return;
|
||||
const canvas = gl.canvas as HTMLCanvasElement;
|
||||
canvas.remove();
|
||||
delete glMap[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取webgl画布上下文
|
||||
* @param id 画布id
|
||||
*/
|
||||
export function getWebGLCanvas(id: string): WebGLRenderingContext | null {
|
||||
return glMap[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建webgl程序对象
|
||||
* @param gl 画布webgl上下文
|
||||
* @param vshader 顶点着色器
|
||||
* @param fshader 片元着色器
|
||||
*/
|
||||
export function createProgram(
|
||||
gl: WebGLRenderingContext,
|
||||
vshader: string,
|
||||
fshader: string
|
||||
) {
|
||||
// 创建着色器
|
||||
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
|
||||
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
|
||||
|
||||
// 创建program
|
||||
const program = gl.createProgram();
|
||||
if (!program) {
|
||||
throw new Error(`Create webgl program fail!`);
|
||||
}
|
||||
|
||||
// 分配和连接program
|
||||
gl.attachShader(program, vertexShader);
|
||||
gl.attachShader(program, fragmentShader);
|
||||
gl.linkProgram(program);
|
||||
|
||||
// 检查连接是否成功
|
||||
const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
|
||||
if (!linked) {
|
||||
const err = gl.getProgramInfoLog(program);
|
||||
throw new Error(`Program link fail: ${err}`);
|
||||
}
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载着色器
|
||||
* @param gl 画布的webgl上下文
|
||||
* @param type 着色器类型,顶点着色器还是片元着色器
|
||||
* @param source 着色器源码
|
||||
*/
|
||||
export function loadShader(
|
||||
gl: WebGLRenderingContext,
|
||||
type: number,
|
||||
source: string
|
||||
) {
|
||||
// 创建着色器
|
||||
const shader = gl.createShader(type);
|
||||
if (!shader) {
|
||||
throw new ReferenceError(
|
||||
`Your device or browser does not support webgl!`
|
||||
);
|
||||
}
|
||||
// 引入并编译着色器
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
|
||||
// 检查是否编译成功
|
||||
const compiled = gl.getShaderParameter(gl, gl.COMPILE_STATUS);
|
||||
if (!compiled) {
|
||||
const err = gl.getShaderInfoLog(shader);
|
||||
throw new Error(`Shader compile fail: ${err}`);
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
131
src/plugin/webgl/gameShadow.ts
Normal file
131
src/plugin/webgl/gameShadow.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import { Polygon } from './polygon';
|
||||
import {
|
||||
Light,
|
||||
removeAllLights,
|
||||
setBackground,
|
||||
setBlur,
|
||||
setLightList,
|
||||
setShadowNodes
|
||||
} from './shadow';
|
||||
|
||||
export default function init() {
|
||||
return { updateShadow, clearShadowCache, setCalShadow };
|
||||
}
|
||||
|
||||
const shadowInfo: Partial<Record<FloorIds, Light[]>> = {
|
||||
MT46: [
|
||||
{
|
||||
id: 'mt42_1',
|
||||
x: 85,
|
||||
y: 85,
|
||||
decay: 100,
|
||||
r: 300,
|
||||
color: '#0000'
|
||||
}
|
||||
]
|
||||
};
|
||||
const backgroundInfo: Partial<Record<FloorIds, Color>> = {
|
||||
MT46: '#0008'
|
||||
};
|
||||
const blurInfo: Partial<Record<FloorIds, number>> = {
|
||||
MT46: 4
|
||||
};
|
||||
const immersionInfo: Partial<Record<FloorIds, number>> = {
|
||||
MT46: 8
|
||||
};
|
||||
const shadowCache: Partial<Record<FloorIds, Polygon[]>> = {};
|
||||
|
||||
let calMapShadow = true;
|
||||
|
||||
export function updateShadow(nocache: boolean = false) {
|
||||
// 需要优化,优化成bfs
|
||||
const floor = core.status.floorId;
|
||||
if (!shadowInfo[floor] || !backgroundInfo[floor]) {
|
||||
removeAllLights();
|
||||
setShadowNodes([]);
|
||||
setBackground('#0000');
|
||||
return;
|
||||
}
|
||||
const f = core.status.thisMap;
|
||||
const w = f.width;
|
||||
const h = f.height;
|
||||
const nodes: Polygon[] = [];
|
||||
if (calMapShadow) {
|
||||
if (shadowCache[floor] && !nocache) {
|
||||
setShadowNodes(shadowCache[floor]!);
|
||||
} else {
|
||||
core.extractBlocks();
|
||||
const blocks = core.getMapBlocksObj();
|
||||
core.status.maps[floor].blocks.forEach(v => {
|
||||
if (
|
||||
!['terrains', 'autotile', 'tileset', 'animates'].includes(
|
||||
v.event.cls
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (v.event.noPass) {
|
||||
const immerse = immersionInfo[floor] ?? 4;
|
||||
const x = v.x;
|
||||
const y = v.y;
|
||||
let left = x * 32 + immerse;
|
||||
let top = y * 32 + immerse;
|
||||
let right = left + 32 - immerse * 2;
|
||||
let bottom = top + 32 - immerse * 2;
|
||||
const l: LocString = `${x - 1},${y}`;
|
||||
const r: LocString = `${x + 1},${y}`;
|
||||
const t: LocString = `${x},${y - 1}`;
|
||||
const b: LocString = `${x},${y + 1}`;
|
||||
|
||||
if (x === 0 || (blocks[l] && blocks[l].event.noPass)) {
|
||||
left -= immerse;
|
||||
}
|
||||
if (x + 1 === w || (blocks[r] && blocks[r].event.noPass)) {
|
||||
right += immerse;
|
||||
}
|
||||
if (y === 0 || (blocks[t] && blocks[t].event.noPass)) {
|
||||
top -= immerse;
|
||||
}
|
||||
if (y + 1 === h || (blocks[b] && blocks[b].event.noPass)) {
|
||||
bottom += immerse;
|
||||
}
|
||||
nodes.push(
|
||||
new Polygon([
|
||||
[left, top],
|
||||
[right, top],
|
||||
[right, bottom],
|
||||
[left, bottom]
|
||||
])
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
shadowCache[floor] = nodes;
|
||||
setShadowNodes(nodes);
|
||||
}
|
||||
} else {
|
||||
setShadowNodes([]);
|
||||
setBlur(0);
|
||||
}
|
||||
setLightList(shadowInfo[floor]!);
|
||||
setBackground(backgroundInfo[floor]!);
|
||||
setBlur(blurInfo[floor] ?? 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除某一层的墙壁缓存
|
||||
* @param floorId 楼层id
|
||||
*/
|
||||
export function clearShadowCache(floorId: FloorIds) {
|
||||
delete shadowCache[floorId];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否不计算墙壁遮挡,对所有灯光有效
|
||||
* @param n 目标值
|
||||
*/
|
||||
export function setCalShadow(n: boolean) {
|
||||
calMapShadow = n;
|
||||
updateShadow();
|
||||
}
|
160
src/plugin/webgl/martrix.ts
Normal file
160
src/plugin/webgl/martrix.ts
Normal file
@ -0,0 +1,160 @@
|
||||
import { has } from '../utils';
|
||||
|
||||
export class Matrix extends Array<number[]> {
|
||||
constructor(...n: number[][]) {
|
||||
if (n.length !== n[0]?.length) {
|
||||
throw new TypeError(
|
||||
`The array delivered to Matrix must has the same length of its item and itself.`
|
||||
);
|
||||
}
|
||||
super(...n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加上某个方阵
|
||||
* @param matrix 要加上的方阵
|
||||
*/
|
||||
add(matrix: number[][]): Matrix {
|
||||
if (matrix.length !== this.length) {
|
||||
throw new TypeError(
|
||||
`To add a martrix, the be-added-matrix's size must equal to the to-add-matrix's.`
|
||||
);
|
||||
}
|
||||
const length = matrix.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
for (let j = 0; j < length; j++) {
|
||||
this[i][j] += matrix[i][j];
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 让该方阵与另一个方阵相乘
|
||||
* @param matrix 要相乘的方阵
|
||||
*/
|
||||
multipy(matrix: number[][]): Matrix {
|
||||
if (matrix.length !== this.length) {
|
||||
throw new TypeError(
|
||||
`To multipy a martrix, the be-multipied-matrix's size must equal to the to-multipy-matrix's.`
|
||||
);
|
||||
}
|
||||
const n = this.length;
|
||||
const arr = this.map(v => v.slice());
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let j = 0; j < n; j++) {
|
||||
for (let k = 0; k < n; k++) {
|
||||
this[i][j] = arr[i][k] * matrix[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class Matrix4 extends Matrix {
|
||||
constructor(...n: number[][]) {
|
||||
n ??= [
|
||||
[1, 0, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 0, 1]
|
||||
];
|
||||
if (n.length !== 4) {
|
||||
throw new TypeError(`The length of delivered array must be 4.`);
|
||||
}
|
||||
super(...n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 平移变换
|
||||
* @param x 平移横坐标
|
||||
* @param y 平移纵坐标
|
||||
* @param z 平移竖坐标
|
||||
*/
|
||||
translation(x: number, y: number, z: number) {
|
||||
this.multipy([
|
||||
[1, 0, 0, x],
|
||||
[0, 1, 0, y],
|
||||
[0, 0, 1, z],
|
||||
[0, 0, 0, 1]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缩放变换
|
||||
* @param x 沿x轴的缩放比例
|
||||
* @param y 沿y轴的缩放比例
|
||||
* @param z 沿z轴的缩放比例
|
||||
*/
|
||||
scale(x: number, y: number, z: number) {
|
||||
this.multipy([
|
||||
[x, 0, 0, 0],
|
||||
[0, y, 0, 0],
|
||||
[0, 0, z, 0],
|
||||
[0, 0, 0, 1]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转变换
|
||||
* @param x 绕x轴的旋转角度
|
||||
* @param y 绕y轴的旋转角度
|
||||
* @param z 绕z轴的旋转角度
|
||||
*/
|
||||
rotate(x?: number, y?: number, z?: number): Matrix4 {
|
||||
if (has(x) && x !== 0) {
|
||||
const sin = Math.sin(x);
|
||||
const cos = Math.cos(x);
|
||||
this.multipy([
|
||||
[1, 0, 0, 0],
|
||||
[0, cos, sin, 0],
|
||||
[0, -sin, cos, 0],
|
||||
[0, 0, 0, 1]
|
||||
]);
|
||||
}
|
||||
if (has(y) && y !== 0) {
|
||||
const sin = Math.sin(y);
|
||||
const cos = Math.cos(y);
|
||||
this.multipy([
|
||||
[cos, 0, -sin, 0],
|
||||
[0, 1, 0, 0],
|
||||
[sin, 0, cos, 0],
|
||||
[0, 0, 0, 1]
|
||||
]);
|
||||
}
|
||||
if (has(z) && z !== 0) {
|
||||
const sin = Math.sin(z);
|
||||
const cos = Math.cos(z);
|
||||
this.multipy([
|
||||
[cos, sin, 0, 0],
|
||||
[-sin, cos, 0, 0],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 0, 1]
|
||||
]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转置矩阵
|
||||
* @param target 转置目标,是赋给原矩阵还是新建一个矩阵
|
||||
*/
|
||||
transpose(target: 'this' | 'new' = 'new'): Matrix4 {
|
||||
const t = target === 'this' ? this : new Matrix4();
|
||||
const arr = this.map(v => v.slice());
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
t[i][j] = arr[j][i];
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换成列主序的Float32Array,用于webgl
|
||||
*/
|
||||
toWebGLFloat32Array(): Float32Array {
|
||||
return new Float32Array(this.transpose().flat());
|
||||
}
|
||||
}
|
89
src/plugin/webgl/polygon.ts
Normal file
89
src/plugin/webgl/polygon.ts
Normal file
@ -0,0 +1,89 @@
|
||||
export class Polygon {
|
||||
/**
|
||||
* 多边形的节点
|
||||
*/
|
||||
nodes: LocArr[];
|
||||
|
||||
private cache: Record<string, LocArr[][]> = {};
|
||||
|
||||
static from(...polygons: LocArr[][]) {
|
||||
return polygons.map(v => new Polygon(v));
|
||||
}
|
||||
|
||||
constructor(nodes: LocArr[]) {
|
||||
if (nodes.length < 3) {
|
||||
throw new Error(`Nodes number delivered is less than 3!`);
|
||||
}
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个点光源下的阴影
|
||||
*/
|
||||
shadowArea(x: number, y: number, r: number): LocArr[][] {
|
||||
const id = `${x},${y}`;
|
||||
if (this.cache[id]) return this.cache[id];
|
||||
const res: LocArr[][] = [];
|
||||
const w = core._PX_ ?? core.__PIXELS__;
|
||||
const h = core._PY_ ?? core.__PIXELS__;
|
||||
|
||||
const intersect = (nx: number, ny: number): LocArr => {
|
||||
const k = (ny - y) / (nx - x);
|
||||
if (k > 1 || k < -1) {
|
||||
if (ny < y) {
|
||||
const ix = x + y / k;
|
||||
return [2 * x - ix, 0];
|
||||
} else {
|
||||
const ix = x + (h - y) / k;
|
||||
return [ix, h];
|
||||
}
|
||||
} else {
|
||||
if (nx < x) {
|
||||
const iy = y + k * x;
|
||||
return [0, 2 * y - iy];
|
||||
} else {
|
||||
const iy = y + k * (w - x);
|
||||
return [w, iy];
|
||||
}
|
||||
}
|
||||
};
|
||||
const l = this.nodes.length;
|
||||
let now = intersect(...this.nodes[0]);
|
||||
for (let i = 0; i < l; i++) {
|
||||
const next = (i + 1) % l;
|
||||
const nextInter = intersect(...this.nodes[next]);
|
||||
const start = [this.nodes[i], now];
|
||||
const end = [nextInter, this.nodes[next]];
|
||||
let path: LocArr[];
|
||||
if (
|
||||
(now[0] === 0 && nextInter[1] === 0) ||
|
||||
(now[1] === 0 && nextInter[0] === 0)
|
||||
) {
|
||||
path = [...start, [0, 0], ...end];
|
||||
} else if (
|
||||
(now[0] === 0 && nextInter[1] === h) ||
|
||||
(now[1] === h && nextInter[0] === 0)
|
||||
) {
|
||||
path = [...start, [0, h], ...end];
|
||||
} else if (
|
||||
(now[0] === w && nextInter[1] === 0) ||
|
||||
(now[1] === 0 && nextInter[0] === w)
|
||||
) {
|
||||
path = [...start, [w, 0], ...end];
|
||||
} else if (
|
||||
(now[0] === w && nextInter[1] === h) ||
|
||||
(now[1] === h && nextInter[0] === w)
|
||||
) {
|
||||
path = [...start, [w, h], ...end];
|
||||
} else {
|
||||
path = [...start, ...end];
|
||||
}
|
||||
res.push(path);
|
||||
now = nextInter;
|
||||
}
|
||||
|
||||
this.cache[id] = res;
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
468
src/plugin/webgl/shadow.ts
Normal file
468
src/plugin/webgl/shadow.ts
Normal file
@ -0,0 +1,468 @@
|
||||
import {
|
||||
Animation,
|
||||
linear,
|
||||
PathFn,
|
||||
TimingFn,
|
||||
Transition
|
||||
} from 'mutate-animate';
|
||||
import { has } from '../utils';
|
||||
import { Polygon } from './polygon';
|
||||
|
||||
interface TransitionInfo {
|
||||
time: number;
|
||||
mode: TimingFn;
|
||||
}
|
||||
|
||||
export interface Light {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
r: number;
|
||||
/** 衰减开始半径 */
|
||||
decay: number;
|
||||
/** 颜色,每个值的范围0.0~1.0 */
|
||||
color: Color;
|
||||
/** 是否可以被物体遮挡 */
|
||||
noShelter?: boolean;
|
||||
/** 正在动画的属性 */
|
||||
_animating?: Record<string, boolean>;
|
||||
/** 执行渐变的属性 */
|
||||
_transition?: Record<string, TransitionInfo>;
|
||||
/** 表示是否是代理,只有设置渐变后才会变为true */
|
||||
_isProxy?: boolean;
|
||||
}
|
||||
|
||||
export default function init() {
|
||||
core.registerAnimationFrame('shadow', true, () => {
|
||||
if (!needRefresh) return;
|
||||
drawShadow();
|
||||
});
|
||||
|
||||
return {
|
||||
initShadowCanvas,
|
||||
drawShadow,
|
||||
addLight,
|
||||
removeLight,
|
||||
setLight,
|
||||
setShadowNodes,
|
||||
setBackground,
|
||||
animateLight,
|
||||
transitionLight,
|
||||
moveLightAs,
|
||||
getAllLights
|
||||
};
|
||||
}
|
||||
|
||||
let canvas: HTMLCanvasElement;
|
||||
let ctx: CanvasRenderingContext2D;
|
||||
let lights: Light[] = [];
|
||||
let needRefresh = false;
|
||||
let shadowNodes: Polygon[] = [];
|
||||
let background: Color;
|
||||
let blur = 3;
|
||||
const temp1 = document.createElement('canvas');
|
||||
const temp2 = document.createElement('canvas');
|
||||
const temp3 = document.createElement('canvas');
|
||||
const ct1 = temp1.getContext('2d')!;
|
||||
const ct2 = temp2.getContext('2d')!;
|
||||
const ct3 = temp3.getContext('2d')!;
|
||||
|
||||
const animationList: Record<string, Animation> = {};
|
||||
const transitionList: Record<string, Transition> = {};
|
||||
|
||||
/**
|
||||
* 初始化阴影画布
|
||||
*/
|
||||
export function initShadowCanvas() {
|
||||
const w = core._PX_ ?? core.__PIXELS__;
|
||||
const h = core._PY_ ?? core.__PIXELS__;
|
||||
ctx = core.createCanvas('shadow', 0, 0, w, h, 55);
|
||||
canvas = ctx.canvas;
|
||||
const s = core.domStyle.scale * devicePixelRatio;
|
||||
temp1.width = w * s;
|
||||
temp1.height = h * s;
|
||||
temp2.width = w * s;
|
||||
temp2.height = h * s;
|
||||
temp3.width = w * s;
|
||||
temp3.height = h * s;
|
||||
ct1.scale(s, s);
|
||||
ct2.scale(s, s);
|
||||
ct3.scale(s, s);
|
||||
canvas.style.filter = `blur(${blur}px)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个光源
|
||||
* @param info 光源信息
|
||||
*/
|
||||
export function addLight(info: Light) {
|
||||
lights.push(info);
|
||||
needRefresh = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除一个光源
|
||||
* @param id 光源id
|
||||
*/
|
||||
export function removeLight(id: string) {
|
||||
const index = lights.findIndex(v => v.id === id);
|
||||
if (index === -1) {
|
||||
throw new ReferenceError(`You are going to remove nonexistent light!`);
|
||||
}
|
||||
lights.splice(index, 1);
|
||||
needRefresh = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置一个光源的信息
|
||||
* @param id 光源id
|
||||
* @param info 光源信息
|
||||
*/
|
||||
export function setLight(id: string, info: Partial<Light>) {
|
||||
if (has(info.id)) delete info.id;
|
||||
const light = lights.find(v => v.id === id);
|
||||
if (!light) {
|
||||
throw new ReferenceError(`You are going to set nonexistent light!`);
|
||||
}
|
||||
for (const [p, v] of Object.entries(info)) {
|
||||
light[p as SelectKey<Light, number>] = v as number;
|
||||
}
|
||||
needRefresh = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前的光源列表
|
||||
* @param list 光源列表
|
||||
*/
|
||||
export function setLightList(list: Light[]) {
|
||||
lights = list;
|
||||
needRefresh = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 去除所有的光源
|
||||
*/
|
||||
export function removeAllLights() {
|
||||
lights = [];
|
||||
needRefresh = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个灯光
|
||||
* @param id 灯光id
|
||||
*/
|
||||
export function getLight(id: string) {
|
||||
return lights.find(v => v.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有灯光
|
||||
*/
|
||||
export function getAllLights() {
|
||||
return lights;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置背景色
|
||||
* @param color 背景色
|
||||
*/
|
||||
export function setBackground(color: Color) {
|
||||
background = color;
|
||||
needRefresh = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 动画改变一个属性的值
|
||||
* @param id 灯光id
|
||||
* @param key 动画属性,x,y,r,decay,颜色请使用animateLightColor(下个版本会加)
|
||||
* @param n 目标值
|
||||
* @param time 动画时间
|
||||
* @param mode 动画方式,渐变函数,高级动画提供了大量内置的渐变函数
|
||||
* @param relative 相对方式,是绝对还是相对
|
||||
*/
|
||||
export function animateLight<K extends Exclude<keyof Light, 'id'>>(
|
||||
id: string,
|
||||
key: K,
|
||||
n: Light[K],
|
||||
time: number = 1000,
|
||||
mode: TimingFn = linear(),
|
||||
relative: boolean = false
|
||||
) {
|
||||
const light = getLight(id);
|
||||
if (!has(light)) {
|
||||
throw new ReferenceError(`You are going to animate nonexistent light`);
|
||||
}
|
||||
if (typeof n !== 'number') {
|
||||
light[key] = n;
|
||||
}
|
||||
const ani = animationList[id] ?? (animationList[id] = new Animation());
|
||||
if (typeof ani.value[key] !== 'number') {
|
||||
ani.register(key, light[key] as number);
|
||||
} else {
|
||||
ani.time(0)
|
||||
.mode(linear())
|
||||
.absolute()
|
||||
.apply(key, light[key] as number);
|
||||
}
|
||||
ani.time(time)
|
||||
.mode(mode)
|
||||
[relative ? 'relative' : 'absolute']()
|
||||
.apply(key, n as number);
|
||||
const start = Date.now();
|
||||
const fn = () => {
|
||||
if (Date.now() - start > time + 50) {
|
||||
ani.ticker.remove(fn);
|
||||
light._animating![key] = false;
|
||||
}
|
||||
needRefresh = true;
|
||||
light[key as SelectKey<Light, number>] = ani.value[key];
|
||||
};
|
||||
ani.ticker.add(fn);
|
||||
light._animating ??= {};
|
||||
light._animating[key] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把一个属性设置为渐变模式
|
||||
* @param id 灯光id
|
||||
* @param key 渐变的属性
|
||||
* @param time 渐变时长
|
||||
* @param mode 渐变方式,渐变函数,高级动画提供了大量内置的渐变函数
|
||||
*/
|
||||
export function transitionLight<K extends Exclude<keyof Light, 'id'>>(
|
||||
id: string,
|
||||
key: K,
|
||||
time: number = 1000,
|
||||
mode: TimingFn = linear()
|
||||
) {
|
||||
const index = lights.findIndex(v => v.id === id);
|
||||
if (index === -1) {
|
||||
throw new ReferenceError(`You are going to transite nonexistent light`);
|
||||
}
|
||||
const light = lights[index];
|
||||
if (typeof light[key] !== 'number') return;
|
||||
light._transition ??= {};
|
||||
light._transition[key] = { time, mode };
|
||||
const tran = transitionList[id] ?? (transitionList[id] = new Transition());
|
||||
tran.value[key] = light[key] as number;
|
||||
if (!light._isProxy) {
|
||||
const handler: ProxyHandler<Light> = {
|
||||
set(t, p, v) {
|
||||
if (typeof p === 'symbol') return false;
|
||||
const start = Date.now();
|
||||
if (
|
||||
!light._transition![p] ||
|
||||
light._animating?.[key] ||
|
||||
typeof v !== 'number'
|
||||
) {
|
||||
t[p as SelectKey<Light, number>] = v;
|
||||
return true;
|
||||
}
|
||||
// @ts-ignore
|
||||
t[p] = light[p];
|
||||
const info = light._transition![p];
|
||||
tran.mode(info.mode).time(info.time);
|
||||
const fn = () => {
|
||||
if (Date.now() - start > info.time + 50) {
|
||||
tran.ticker.remove(fn);
|
||||
}
|
||||
needRefresh = true;
|
||||
t[p as SelectKey<Light, number>] = tran.value[key];
|
||||
};
|
||||
tran.ticker.add(fn);
|
||||
tran.transition(p, v);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
lights[index] = new Proxy(light, handler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动一个灯光
|
||||
* @param id 灯光id
|
||||
* @param x 目标横坐标
|
||||
* @param y 目标纵坐标
|
||||
* @param time 移动时间
|
||||
* @param mode 移动方式,渐变函数
|
||||
* @param relative 相对模式,相对还是绝对
|
||||
*/
|
||||
export function moveLight(
|
||||
id: string,
|
||||
x: number,
|
||||
y: number,
|
||||
time: number = 1000,
|
||||
mode: TimingFn = linear(),
|
||||
relative: boolean = false
|
||||
) {
|
||||
animateLight(id, 'x', x, time, mode, relative);
|
||||
animateLight(id, 'y', y, time, mode, relative);
|
||||
}
|
||||
|
||||
/**
|
||||
* 以一个路径移动光源
|
||||
* @param id 灯光id
|
||||
* @param time 移动时长
|
||||
* @param path 移动路径
|
||||
* @param mode 移动方式,渐变函数,表示移动的完成度
|
||||
* @param relative 相对模式,相对还是绝对
|
||||
*/
|
||||
export function moveLightAs(
|
||||
id: string,
|
||||
time: number,
|
||||
path: PathFn,
|
||||
mode: TimingFn = linear(),
|
||||
relative: boolean = true
|
||||
) {
|
||||
const light = getLight(id);
|
||||
if (!has(light)) {
|
||||
throw new ReferenceError(`You are going to animate nonexistent light`);
|
||||
}
|
||||
const ani = animationList[id] ?? (animationList[id] = new Animation());
|
||||
ani.mode(linear()).time(0).move(light.x, light.y);
|
||||
ani.time(time)
|
||||
.mode(mode)
|
||||
[relative ? 'relative' : 'absolute']()
|
||||
.moveAs(path);
|
||||
const start = Date.now();
|
||||
const fn = () => {
|
||||
if (Date.now() - start > time + 50) {
|
||||
ani.ticker.remove(fn);
|
||||
light._animating!.x = false;
|
||||
light._animating!.y = false;
|
||||
}
|
||||
needRefresh = true;
|
||||
light.x = ani.x;
|
||||
light.y = ani.y;
|
||||
};
|
||||
ani.ticker.add(fn);
|
||||
light._animating ??= {};
|
||||
light._animating.x = true;
|
||||
light._animating.y = true;
|
||||
}
|
||||
|
||||
export function animateLightColor(
|
||||
id: string,
|
||||
target: Color,
|
||||
time: number = 1000,
|
||||
mode: TimingFn = linear()
|
||||
) {
|
||||
// todo
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据坐标数组设置物体节点
|
||||
* @param nodes 坐标数组
|
||||
*/
|
||||
export function setShadowNodes(nodes: LocArr[][]): void;
|
||||
/**
|
||||
* 根据多边形数组设置物体节点
|
||||
* @param nodes 多边形数组
|
||||
*/
|
||||
export function setShadowNodes(nodes: Polygon[]): void;
|
||||
export function setShadowNodes(nodes: LocArr[][] | Polygon[]) {
|
||||
if (nodes.length === 0) {
|
||||
shadowNodes = [];
|
||||
needRefresh = true;
|
||||
}
|
||||
if (nodes[0] instanceof Polygon) shadowNodes = nodes as Polygon[];
|
||||
else shadowNodes = Polygon.from(...(nodes as LocArr[][]));
|
||||
needRefresh = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据坐标数组添加物体节点
|
||||
* @param polygons 坐标数组
|
||||
*/
|
||||
export function addPolygon(...polygons: LocArr[][]): void;
|
||||
/**
|
||||
* 根据多边形数组添加物体节点
|
||||
* @param polygons 多边形数组
|
||||
*/
|
||||
export function addPolygon(...polygons: Polygon[]): void;
|
||||
export function addPolygon(...polygons: Polygon[] | LocArr[][]) {
|
||||
if (polygons.length === 0) return;
|
||||
if (polygons[0] instanceof Polygon)
|
||||
shadowNodes.push(...(polygons as Polygon[]));
|
||||
else shadowNodes.push(...Polygon.from(...(polygons as LocArr[][])));
|
||||
needRefresh = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置光源的虚化程度
|
||||
* @param n 虚化程度
|
||||
*/
|
||||
export function setBlur(n: number) {
|
||||
blur = n;
|
||||
canvas.style.filter = `blur(${n}px)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制阴影
|
||||
*/
|
||||
export function drawShadow() {
|
||||
const w = core._PX_ ?? core.__PIXELS__;
|
||||
const h = core._PY_ ?? core.__PIXELS__;
|
||||
needRefresh = false;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ct1.clearRect(0, 0, w, h);
|
||||
ct2.clearRect(0, 0, w, h);
|
||||
ct3.clearRect(0, 0, w, h);
|
||||
|
||||
const b = core.arrayToRGBA(background);
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ct3.globalCompositeOperation = 'source-over';
|
||||
|
||||
// 绘制阴影,一个光源一个光源地绘制,然后source-out获得光,然后把光叠加,再source-out获得最终阴影
|
||||
for (let i = 0; i < lights.length; i++) {
|
||||
const { x, y, r, decay, color, noShelter } = lights[i];
|
||||
// 绘制阴影
|
||||
ct1.clearRect(0, 0, w, h);
|
||||
ct2.clearRect(0, 0, w, h);
|
||||
if (!noShelter) {
|
||||
for (const polygon of shadowNodes) {
|
||||
const area = polygon.shadowArea(x, y, r);
|
||||
area.forEach(v => {
|
||||
ct1.beginPath();
|
||||
ct1.moveTo(v[0][0], v[0][1]);
|
||||
for (let i = 1; i < v.length; i++) {
|
||||
ct1.lineTo(v[i][0], v[i][1]);
|
||||
}
|
||||
ct1.closePath();
|
||||
ct1.fillStyle = '#000';
|
||||
ct1.globalCompositeOperation = 'source-over';
|
||||
ct1.fill();
|
||||
});
|
||||
}
|
||||
}
|
||||
// 存入ct2,用于绘制真实阴影
|
||||
ct2.globalCompositeOperation = 'source-over';
|
||||
ct2.drawImage(temp1, 0, 0, w, h);
|
||||
ct2.globalCompositeOperation = 'source-out';
|
||||
const gra = ct2.createRadialGradient(x, y, decay, x, y, r);
|
||||
gra.addColorStop(0, core.arrayToRGBA(color));
|
||||
gra.addColorStop(1, 'transparent');
|
||||
ct2.fillStyle = gra;
|
||||
ct2.beginPath();
|
||||
ct2.arc(x, y, r, 0, Math.PI * 2);
|
||||
ct2.fill();
|
||||
ctx.drawImage(temp2, 0, 0, w, h);
|
||||
// 再绘制ct1的阴影,然后绘制到ct3叠加
|
||||
ct1.globalCompositeOperation = 'source-out';
|
||||
const gra2 = ct1.createRadialGradient(x, y, decay, x, y, r);
|
||||
gra2.addColorStop(0, '#fff');
|
||||
gra2.addColorStop(1, '#fff0');
|
||||
ct1.beginPath();
|
||||
ct1.arc(x, y, r, 0, Math.PI * 2);
|
||||
ct1.fillStyle = gra2;
|
||||
ct1.fill();
|
||||
// 绘制到ct3上
|
||||
ct3.drawImage(temp1, 0, 0, w, h);
|
||||
}
|
||||
// 绘制真实阴影
|
||||
ct3.globalCompositeOperation = 'source-out';
|
||||
ct3.fillStyle = b;
|
||||
ct3.fillRect(0, 0, w, h);
|
||||
ctx.globalCompositeOperation = 'destination-over';
|
||||
ctx.drawImage(temp3, 0, 0, w, h);
|
||||
}
|
8
src/plugin/webgl/utils.ts
Normal file
8
src/plugin/webgl/utils.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export default function init() {
|
||||
return { isWebGLSupported };
|
||||
}
|
||||
|
||||
export const isWebGLSupported = (function () {
|
||||
const canvas = document.createElement('canvas');
|
||||
return !!canvas.getContext('webgl');
|
||||
})();
|
2
src/types/map.d.ts
vendored
2
src/types/map.d.ts
vendored
@ -61,7 +61,7 @@ interface Block<N extends Exclude<AllNumbers, 0> = Exclude<AllNumbers, 0>> {
|
||||
/**
|
||||
* 图块是否不可通行
|
||||
*/
|
||||
nopass: boolean;
|
||||
noPass: boolean;
|
||||
|
||||
/**
|
||||
* 图块高度
|
||||
|
7
src/types/util.d.ts
vendored
7
src/types/util.d.ts
vendored
@ -862,6 +862,11 @@ type SelectType<R, T> = {
|
||||
[P in keyof R as R[P] extends T ? P : never]: R[P];
|
||||
};
|
||||
|
||||
/**
|
||||
* 从一个对象中选择类型是目标属性的键名
|
||||
*/
|
||||
type SelectKey<R, T> = keyof SelectType<R, T>;
|
||||
|
||||
/**
|
||||
* 获取一段字符串的第一个字符
|
||||
*/
|
||||
@ -883,3 +888,5 @@ type NonObjectOf<T> = SelectType<T, NonObject>;
|
||||
* 以一个字符串结尾
|
||||
*/
|
||||
type EndsWith<T extends string> = `${string}${T}`;
|
||||
|
||||
type KeyExcludesUnderline<T> = Excluede<keyof T, `_${string}`>;
|
||||
|
Loading…
Reference in New Issue
Block a user