From 5e0d8ac7d45a8bdc5418e7ad9fead4c98d961841 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Sun, 5 Feb 2023 18:17:10 +0800 Subject: [PATCH] =?UTF-8?q?=E7=82=B9=E5=85=89=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- idea.md | 4 + public/libs/maps.js | 2 + public/libs/ui.js | 3 +- public/project/floors/MT42.js | 7 + public/project/floors/MT46.js | 108 +++++--- public/project/functions.js | 2 + public/project/plugins.js | 8 +- src/initPlugin.ts | 8 +- src/plugin/utils.ts | 3 +- src/plugin/webgl/canvas.ts | 123 +++++++++ src/plugin/webgl/gameShadow.ts | 131 +++++++++ src/plugin/webgl/martrix.ts | 160 +++++++++++ src/plugin/webgl/polygon.ts | 89 +++++++ src/plugin/webgl/shadow.ts | 468 +++++++++++++++++++++++++++++++++ src/plugin/webgl/utils.ts | 8 + src/types/map.d.ts | 2 +- src/types/util.d.ts | 7 + 17 files changed, 1082 insertions(+), 51 deletions(-) create mode 100644 src/plugin/webgl/canvas.ts create mode 100644 src/plugin/webgl/gameShadow.ts create mode 100644 src/plugin/webgl/martrix.ts create mode 100644 src/plugin/webgl/polygon.ts create mode 100644 src/plugin/webgl/shadow.ts create mode 100644 src/plugin/webgl/utils.ts diff --git a/idea.md b/idea.md index 3542621..34f9a8b 100644 --- a/idea.md +++ b/idea.md @@ -26,6 +26,10 @@ ### 第三章 战争 +#### 技能 + +闪避:每 M 回合闪避一次,减少 N%的伤害 + ## 机制 ### 通用 diff --git a/public/libs/maps.js b/public/libs/maps.js index dd32a1f..139c689 100644 --- a/public/libs/maps.js +++ b/public/libs/maps.js @@ -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 ( diff --git a/public/libs/ui.js b/public/libs/ui.js index 5b28e9b..716dd67 100644 --- a/public/libs/ui.js +++ b/public/libs/ui.js @@ -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(); }; diff --git a/public/project/floors/MT42.js b/public/project/floors/MT42.js index 910c99a..0f33595 100644 --- a/public/project/floors/MT42.js +++ b/public/project/floors/MT42.js @@ -31,6 +31,13 @@ main.floors.MT42= 5, 0 ] + }, + "8,12": { + "floorId": "MT46", + "loc": [ + 7, + 8 + ] } }, "beforeBattle": {}, diff --git a/public/project/floors/MT46.js b/public/project/floors/MT46.js index f55080e..c6b28bd 100644 --- a/public/project/floors/MT46.js +++ b/public/project/floors/MT46.js @@ -1,45 +1,71 @@ main.floors.MT46= { -"floorId": "MT46", -"title": "冰封高原", -"name": "46", -"width": 15, -"height": 15, -"canFlyTo": true, -"canFlyFrom": true, -"canUseQuickShop": true, -"cannotViewMap": false, -"images": [], -"ratio": 8, -"defaultGround": "T580", -"bgm": "winter.mp3", -"firstArrive": [], -"eachArrive": [], -"parallelDo": "", -"events": {}, -"changeFloor": {}, -"beforeBattle": {}, -"afterBattle": {}, -"afterGetItem": {}, -"afterOpenDoor": {}, -"autoEvent": {}, -"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] + "floorId": "MT46", + "title": "冰封高原", + "name": "46", + "width": 15, + "height": 15, + "canFlyTo": true, + "canFlyFrom": true, + "canUseQuickShop": true, + "cannotViewMap": false, + "images": [], + "ratio": 8, + "defaultGround": "T580", + "bgm": "winter.mp3", + "firstArrive": [], + "eachArrive": [], + "parallelDo": "", + "events": {}, + "changeFloor": {}, + "beforeBattle": {}, + "afterBattle": {}, + "afterGetItem": {}, + "afterOpenDoor": {}, + "autoEvent": {}, + "cannotMove": {}, + "cannotMoveIn": {}, + "map": [ + [ 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": [ + +] } \ No newline at end of file diff --git a/public/project/functions.js b/public/project/functions.js index 98166f4..b362973 100644 --- a/public/project/functions.js +++ b/public/project/functions.js @@ -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; diff --git a/public/project/plugins.js b/public/project/plugins.js index 3a392b9..e439faf 100644 --- a/public/project/plugins.js +++ b/public/project/plugins.js @@ -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优化,只绘制范围内的部分 diff --git a/src/initPlugin.ts b/src/initPlugin.ts index ed5a5d2..baaac03 100644 --- a/src/initPlugin.ts +++ b/src/initPlugin.ts @@ -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上 diff --git a/src/plugin/utils.ts b/src/plugin/utils.ts index 62d9e2f..5339489 100644 --- a/src/plugin/utils.ts +++ b/src/plugin/utils.ts @@ -1,3 +1,4 @@ +/// 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> { - 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> = {}; diff --git a/src/plugin/webgl/canvas.ts b/src/plugin/webgl/canvas.ts new file mode 100644 index 0000000..b4a70c2 --- /dev/null +++ b/src/plugin/webgl/canvas.ts @@ -0,0 +1,123 @@ +const glMap: Record = {}; + +/** + * 创建一个以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; +} diff --git a/src/plugin/webgl/gameShadow.ts b/src/plugin/webgl/gameShadow.ts new file mode 100644 index 0000000..e5f2e21 --- /dev/null +++ b/src/plugin/webgl/gameShadow.ts @@ -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> = { + MT46: [ + { + id: 'mt42_1', + x: 85, + y: 85, + decay: 100, + r: 300, + color: '#0000' + } + ] +}; +const backgroundInfo: Partial> = { + MT46: '#0008' +}; +const blurInfo: Partial> = { + MT46: 4 +}; +const immersionInfo: Partial> = { + MT46: 8 +}; +const shadowCache: Partial> = {}; + +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(); +} diff --git a/src/plugin/webgl/martrix.ts b/src/plugin/webgl/martrix.ts new file mode 100644 index 0000000..d7d6b41 --- /dev/null +++ b/src/plugin/webgl/martrix.ts @@ -0,0 +1,160 @@ +import { has } from '../utils'; + +export class Matrix extends Array { + 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()); + } +} diff --git a/src/plugin/webgl/polygon.ts b/src/plugin/webgl/polygon.ts new file mode 100644 index 0000000..4248458 --- /dev/null +++ b/src/plugin/webgl/polygon.ts @@ -0,0 +1,89 @@ +export class Polygon { + /** + * 多边形的节点 + */ + nodes: LocArr[]; + + private cache: Record = {}; + + 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; + } +} diff --git a/src/plugin/webgl/shadow.ts b/src/plugin/webgl/shadow.ts new file mode 100644 index 0000000..e30242e --- /dev/null +++ b/src/plugin/webgl/shadow.ts @@ -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; + /** 执行渐变的属性 */ + _transition?: Record; + /** 表示是否是代理,只有设置渐变后才会变为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 = {}; +const transitionList: Record = {}; + +/** + * 初始化阴影画布 + */ +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) { + 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] = 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>( + 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] = 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>( + 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 = { + 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] = 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] = 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); +} diff --git a/src/plugin/webgl/utils.ts b/src/plugin/webgl/utils.ts new file mode 100644 index 0000000..999219c --- /dev/null +++ b/src/plugin/webgl/utils.ts @@ -0,0 +1,8 @@ +export default function init() { + return { isWebGLSupported }; +} + +export const isWebGLSupported = (function () { + const canvas = document.createElement('canvas'); + return !!canvas.getContext('webgl'); +})(); diff --git a/src/types/map.d.ts b/src/types/map.d.ts index beefa76..43c4fee 100644 --- a/src/types/map.d.ts +++ b/src/types/map.d.ts @@ -61,7 +61,7 @@ interface Block = Exclude> { /** * 图块是否不可通行 */ - nopass: boolean; + noPass: boolean; /** * 图块高度 diff --git a/src/types/util.d.ts b/src/types/util.d.ts index 35fbc14..f3a7697 100644 --- a/src/types/util.d.ts +++ b/src/types/util.d.ts @@ -862,6 +862,11 @@ type SelectType = { [P in keyof R as R[P] extends T ? P : never]: R[P]; }; +/** + * 从一个对象中选择类型是目标属性的键名 + */ +type SelectKey = keyof SelectType; + /** * 获取一段字符串的第一个字符 */ @@ -883,3 +888,5 @@ type NonObjectOf = SelectType; * 以一个字符串结尾 */ type EndsWith = `${string}${T}`; + +type KeyExcludesUnderline = Excluede;