点光源

This commit is contained in:
unanmed 2023-02-05 18:17:10 +08:00
parent 3c7b5906f6
commit 5e0d8ac7d4
17 changed files with 1082 additions and 51 deletions

View File

@ -26,6 +26,10 @@
### 第三章 战争 ### 第三章 战争
#### 技能
闪避:每 M 回合闪避一次,减少 N%的伤害
## 机制 ## 机制
### 通用 ### 通用

View File

@ -3202,6 +3202,7 @@ maps.prototype.removeBlock = function (x, y, floorId) {
const block = blocks[i]; const block = blocks[i];
this.removeBlockByIndex(i, floorId); this.removeBlockByIndex(i, floorId);
this._removeBlockFromMap(floorId, block); this._removeBlockFromMap(floorId, block);
core.updateShadow(true);
return true; return true;
} }
return false; return false;
@ -3364,6 +3365,7 @@ maps.prototype.setBlock = function (number, x, y, floorId, noredraw) {
} }
} }
} }
core.updateShadow(true);
}; };
maps.prototype.animateSetBlock = function ( maps.prototype.animateSetBlock = function (

View File

@ -4226,7 +4226,8 @@ ui.prototype.deleteCanvas = function (name) {
////// 删除所有动态canvas ////// ////// 删除所有动态canvas //////
ui.prototype.deleteAllCanvas = function () { ui.prototype.deleteAllCanvas = function () {
return this.deleteCanvas(function () { this.deleteCanvas(function () {
return true; return true;
}); });
if (main.mode === 'play' && !core.isReplaying()) core.initShadowCanvas();
}; };

View File

@ -31,6 +31,13 @@ main.floors.MT42=
5, 5,
0 0
] ]
},
"8,12": {
"floorId": "MT46",
"loc": [
7,
8
]
} }
}, },
"beforeBattle": {}, "beforeBattle": {},

View File

@ -1,45 +1,71 @@
main.floors.MT46= main.floors.MT46=
{ {
"floorId": "MT46", "floorId": "MT46",
"title": "冰封高原", "title": "冰封高原",
"name": "46", "name": "46",
"width": 15, "width": 15,
"height": 15, "height": 15,
"canFlyTo": true, "canFlyTo": true,
"canFlyFrom": true, "canFlyFrom": true,
"canUseQuickShop": true, "canUseQuickShop": true,
"cannotViewMap": false, "cannotViewMap": false,
"images": [], "images": [],
"ratio": 8, "ratio": 8,
"defaultGround": "T580", "defaultGround": "T580",
"bgm": "winter.mp3", "bgm": "winter.mp3",
"firstArrive": [], "firstArrive": [],
"eachArrive": [], "eachArrive": [],
"parallelDo": "", "parallelDo": "",
"events": {}, "events": {},
"changeFloor": {}, "changeFloor": {},
"beforeBattle": {}, "beforeBattle": {},
"afterBattle": {}, "afterBattle": {},
"afterGetItem": {}, "afterGetItem": {},
"afterOpenDoor": {}, "afterOpenDoor": {},
"autoEvent": {}, "autoEvent": {},
"cannotMove": {}, "cannotMove": {},
"cannotMoveIn": {}, "cannotMoveIn": {},
"map": [ "map": [
[ 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],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 0, 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": [
]
} }

View File

@ -150,6 +150,8 @@ var functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a = {
// ---------- 重绘新地图这一步将会设置core.status.floorId ---------- // // ---------- 重绘新地图这一步将会设置core.status.floorId ---------- //
core.drawMap(floorId); core.drawMap(floorId);
core.updateShadow();
// 切换楼层BGM // 切换楼层BGM
if (core.status.maps[floorId].bgm) { if (core.status.maps[floorId].bgm) {
var bgm = core.status.maps[floorId].bgm; var bgm = core.status.maps[floorId].bgm;

View File

@ -2,6 +2,7 @@
var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = { var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = {
init: function () { init: function () {
// 只看插件没用插件是与vite样板高度融合的所以要看的话就在游戏内的百科全书-关于游戏内点那个开源地址吧
this._afterLoadResources = function () { this._afterLoadResources = function () {
if (!main.replayChecking && main.mode === 'play') { if (!main.replayChecking && main.mode === 'play') {
main.forward(); main.forward();
@ -770,12 +771,7 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = {
core.status.hero = new Proxy(hero, handler); core.status.hero = new Proxy(hero, handler);
core.status.maps[floorId].blocks.forEach(function (block) { core.status.maps[floorId].blocks.forEach(function (block) {
if ( if (block.event.cls !== 'items' || block.disable) return;
block.event.cls !== 'items' ||
block.event.id === 'superPotion' ||
block.disable
)
return;
const x = block.x, const x = block.x,
y = block.y; y = block.y;
// v2优化只绘制范围内的部分 // v2优化只绘制范围内的部分

View File

@ -11,6 +11,9 @@ import chapter from './plugin/ui/chapter';
import fly from './plugin/ui/fly'; import fly from './plugin/ui/fly';
import chase from './plugin/chase/chase'; import chase from './plugin/chase/chase';
import fixed from './plugin/ui/fixed'; 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() { function forward() {
// 每个引入的插件都要在这里执行,否则不会被转发 // 每个引入的插件都要在这里执行,否则不会被转发
@ -26,7 +29,10 @@ function forward() {
chapter(), chapter(),
fly(), fly(),
chase(), chase(),
fixed() fixed(),
webglUtils(),
shadow(),
gameShadow()
]; ];
// 初始化所有插件并转发到core上 // 初始化所有插件并转发到core上

View File

@ -1,3 +1,4 @@
/// <reference path="../types/core.d.ts" />
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { MessageApi } from 'ant-design-vue/lib/message'; import { MessageApi } from 'ant-design-vue/lib/message';
import { isNil } from 'lodash'; import { isNil } from 'lodash';
@ -69,7 +70,7 @@ export function keycode(key: number) {
* @param css css字符串 * @param css css字符串
*/ */
export function parseCss(css: string): Partial<Record<CanParseCss, string>> { 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 styles = str.split(';');
const res: Partial<Record<CanParseCss, string>> = {}; const res: Partial<Record<CanParseCss, string>> = {};

123
src/plugin/webgl/canvas.ts Normal file
View 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;
}

View 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
View 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;
}
/**
* Float32Arraywebgl
*/
toWebGLFloat32Array(): Float32Array {
return new Float32Array(this.transpose().flat());
}
}

View 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
View 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);
}

View 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
View File

@ -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
View File

@ -862,6 +862,11 @@ type SelectType<R, T> = {
[P in keyof R as R[P] extends T ? P : never]: R[P]; [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 EndsWith<T extends string> = `${string}${T}`;
type KeyExcludesUnderline<T> = Excluede<keyof T, `_${string}`>;