diff --git a/src/core/fx/canvas2d.ts b/src/core/fx/canvas2d.ts new file mode 100644 index 0000000..18a6868 --- /dev/null +++ b/src/core/fx/canvas2d.ts @@ -0,0 +1,223 @@ +import { parseCss } from '@/plugin/utils'; +import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; +import { CSSObj } from '../interface'; + +interface OffscreenCanvasEvent extends EmitableEvent { + resize: () => void; +} + +export class MotaOffscreenCanvas2D extends EventEmitter { + static list: Set = new Set(); + + canvas: HTMLCanvasElement; + ctx: CanvasRenderingContext2D; + + width: number; + height: number; + + /** 是否自动跟随样板的core.domStyle.scale进行缩放 */ + autoScale: boolean = false; + /** 是否是高清画布 */ + highResolution: boolean = true; + + constructor() { + super(); + + this.canvas = document.createElement('canvas'); + this.ctx = this.canvas.getContext('2d')!; + this.width = this.canvas.width / devicePixelRatio; + this.height = this.canvas.height / devicePixelRatio; + + this.canvas.style.position = 'absolute'; + + MotaOffscreenCanvas2D.list.add(this); + } + + /** + * 设置画布的大小 + */ + size(width: number, height: number) { + let ratio = this.highResolution ? devicePixelRatio : 1; + if (this.autoScale && this.highResolution) { + ratio *= core.domStyle.scale; + } + this.canvas.width = width * ratio; + this.canvas.height = height * ratio; + this.width = width; + this.height = height; + this.ctx.setTransform(1, 0, 0, 1, 0, 0); + this.ctx.scale(ratio, ratio); + } + + /** + * 设置当前画布是否跟随样板的 core.domStyle.scale 一同进行缩放 + */ + withGameScale(auto: boolean) { + this.autoScale = auto; + this.size(this.width, this.height); + } + + /** + * 设置当前画布是否为高清画布 + */ + setHD(hd: boolean) { + this.highResolution = hd; + this.size(this.width, this.height); + } + + /** + * 删除这个画布 + */ + delete() { + MotaCanvas2D.list.delete(this); + } + + /** + * 复制一个离屏Canvas2D对象或Canvas2D对象,一般用于缓存等操作 + * @param canvas 被复制的MotaOffscreenCanvas2D对象 + * @returns 复制结果 + */ + static clone(canvas: MotaOffscreenCanvas2D): MotaOffscreenCanvas2D { + const newCanvas = new MotaOffscreenCanvas2D(); + newCanvas.setHD(canvas.highResolution); + newCanvas.withGameScale(canvas.autoScale); + newCanvas.size(canvas.width, canvas.height); + newCanvas.ctx.drawImage(canvas.canvas, 0, 0); + return newCanvas; + } +} + +export class MotaCanvas2D extends MotaOffscreenCanvas2D { + static map: Map = new Map(); + + id: string = ''; + + x: number = 0; + y: number = 0; + + private mounted: boolean = false; + private target!: HTMLElement; + /** 是否自动跟随样板的core.domStyle.scale进行缩放 */ + autoScale: boolean = false; + /** 是否是高清画布 */ + highResolution: boolean = true; + + constructor(id: string = '', setTarget: boolean = true) { + super(); + + this.id = id; + if (setTarget) this.target = core.dom.gameDraw; + this.canvas = document.createElement('canvas'); + this.canvas.id = id; + this.ctx = this.canvas.getContext('2d')!; + this.width = this.canvas.width / devicePixelRatio; + this.height = this.canvas.height / devicePixelRatio; + + this.canvas.style.position = 'absolute'; + + MotaCanvas2D.map.set(this.id, this); + } + + /** + * 修改画布的挂载目标,如果已经被挂载,那么会被重新挂载至新的目标元素 + * @param target 画布将被挂载的目标 + */ + setTarget(target: HTMLElement) { + this.target = target; + if (this.mounted) { + this.unmount(); + this.mount(); + } + } + + /** + * 设置画布的大小 + */ + size(width: number, height: number) { + let ratio = this.highResolution ? devicePixelRatio : 1; + if (this.autoScale) { + const scale = core.domStyle.scale; + if (this.highResolution) ratio *= scale; + this.canvas.style.width = `${width * scale}px`; + this.canvas.style.height = `${height * scale}px`; + } else { + this.canvas.style.width = `${width}px`; + this.canvas.style.height = `${height}px`; + } + this.canvas.width = width * ratio; + this.canvas.height = height * ratio; + this.width = width; + this.height = height; + this.ctx.setTransform(1, 0, 0, 1, 0, 0); + this.ctx.scale(ratio, ratio); + } + + /** + * 设置画布的位置 + */ + pos(x: number, y: number) { + this.canvas.style.left = `${x}px`; + this.canvas.style.top = `${y}px`; + this.x = x; + this.y = y; + } + + /** + * 设置画布的css + * @param css 要设置成的css + */ + css(css: string | CSSObj) { + const s = typeof css === 'string' ? parseCss(css) : css; + for (const [key, value] of Object.entries(s)) { + this.canvas.style[key as CanParseCss] = value; + } + } + + /** + * 删除这个画布 + */ + delete() { + super.delete(); + this.unmount(); + MotaCanvas2D.map.delete(this.id); + } + + /** + * 将这个画布添加至游戏画布 + */ + mount() { + if (!this.mounted) { + this.mounted = true; + this.target.appendChild(this.canvas); + } + } + + /** + * 将这个画布从页面上移除 + */ + unmount() { + if (this.mounted) { + this.mounted = false; + this.canvas.remove(); + } + } + + /** + * 类似于 Symbol.for + */ + static for(id: string, setTarget?: boolean) { + const canvas = this.map.get(id); + return canvas ?? new MotaCanvas2D(id, setTarget); + } +} + +window.addEventListener('resize', () => { + requestAnimationFrame(() => { + MotaOffscreenCanvas2D.list.forEach(v => { + if (v.autoScale) { + v.size(v.width, v.height); + v.emit('resize'); + } + }); + }); +}); diff --git a/src/core/fx/portal.ts b/src/core/fx/portal.ts new file mode 100644 index 0000000..54ca5d3 --- /dev/null +++ b/src/core/fx/portal.ts @@ -0,0 +1,241 @@ +import { Ticker } from 'mutate-animate'; +import { MotaCanvas2D } from '@/core/fx/canvas2d'; +import { MotaSettingItem, mainSetting } from '@/core/main/setting'; + +// 苍蓝殿左上角区域的传送门机制的绘制部分,传送部分看 src/game/machanism/misc.ts + +interface DrawingPortal { + color: string; + x: number; + y: number; + particles: PortalParticle[]; + /** v表示竖向,h表示横向 */ + type: 'v' | 'h'; + /** 上一次新增粒子的时间 */ + lastParticle: number; +} + +interface PortalParticle { + fx: number; + fy: number; + totalTime: number; + time: number; + tx: number; + ty: number; + r: number; +} + +const MAX_PARTICLES = 10; +const PARTICLE_LAST = 2000; +const PARTICLE_INTERVAL = PARTICLE_LAST / MAX_PARTICLES; + +const color: string[] = ['#0f0', '#ff0', '#0ff', '#fff', '#f0f']; + +const drawing: DrawingPortal[] = []; +const ticker = new Ticker(); + +let canvas: MotaCanvas2D; +let ctx: CanvasRenderingContext2D; +let particleSetting: MotaSettingItem; + +let lastTime = 0; +Mota.require('var', 'loading').once('coreInit', () => { + canvas = MotaCanvas2D.for('@portal'); + ctx = canvas.ctx; + canvas.mount(); + canvas.css(`z-index: 51`); + canvas.withGameScale(true); + canvas.pos(0, 0); + canvas.size(480, 480); + canvas.on('resize', () => { + canvas.css(`z-index: 51`); + }); + particleSetting = mainSetting.getSetting('fx.portalParticle')!; + ticker.add(tickPortal); +}); + +Mota.require('var', 'hook').on('changingFloor', id => { + drawPortals(id); +}); + +let needDraw = false; +function tickPortal(time: number) { + const last = lastTime; + lastTime = time; + const p = particleSetting.value; + + if (!core.isPlaying() || drawing.length === 0) return; + if (!p && !needDraw) return; + + needDraw = false; + + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + ctx.lineCap = 'round'; + ctx.lineWidth = 3; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + if (p) { + ctx.shadowBlur = 8; + } else { + ctx.shadowBlur = 0; + } + + drawing.forEach(v => { + const { color, x, y, type, lastParticle, particles } = v; + + ctx.strokeStyle = color; + ctx.fillStyle = color; + ctx.globalAlpha = 1; + ctx.shadowColor = color; + if (type === 'v') { + ctx.beginPath(); + ctx.moveTo(x, y - 14); + ctx.lineTo(x, y + 30); + ctx.stroke(); + } else { + ctx.beginPath(); + ctx.moveTo(x + 2, y); + ctx.lineTo(x + 30, y); + ctx.stroke(); + } + + if (p) { + // 绘制粒子效果 + let needDelete = false; + const dt = time - last; + + particles.forEach(v => { + const { fx, fy, tx, ty, time: t, totalTime, r } = v; + const progress = t / totalTime; + const nx = (tx - fx) * progress + fx; + const ny = (ty - fy) * progress + fy; + v.time += dt; + + if (progress > 1) { + needDelete = true; + return; + } else if (progress > 0.75) { + ctx.globalAlpha = (1 - progress) * 4; + } else if (progress < 0.25) { + ctx.globalAlpha = progress * 4; + } else { + ctx.globalAlpha = 1; + } + + ctx.beginPath(); + ctx.arc(nx, ny, r, 0, Math.PI * 2); + ctx.closePath(); + ctx.fill(); + }); + if (needDelete) { + particles.shift(); + } + if ( + time - lastParticle >= PARTICLE_INTERVAL && + particles.length < MAX_PARTICLES + ) { + // 添加新粒子 + const direction = Math.random(); + const k = Math.random() / 2 - 0.3; + const verticle = Math.floor(Math.random() * 8 + 8); + const r = Math.random() * 2; + v.lastParticle = time; + if (direction > 0.5) { + // 左边 | 上边 + if (type === 'h') { + const fx = Math.floor(Math.random() * 24 + x + 4); + particles.push({ + fx: fx, + fy: y - 1, + tx: verticle * k + fx + 4, + ty: -verticle + y - 1, + r: r, + time: 0, + totalTime: PARTICLE_LAST + }); + } else { + const fy = Math.floor(Math.random() * 44 + y - 14); + particles.push({ + fy: fy, + fx: x - 1, + ty: verticle * k + fy + 4, + tx: -verticle + x - 1, + r: r, + time: 0, + totalTime: PARTICLE_LAST + }); + } + } else { + // 右边 | 下边 + if (type === 'h') { + const fx = Math.floor(Math.random() * 24 + x + 4); + particles.push({ + fx: fx, + fy: y + 1, + tx: verticle * k + fx + 4, + ty: verticle + y - 1, + r: r, + time: 0, + totalTime: PARTICLE_LAST + }); + } else { + const fy = Math.floor(Math.random() * 44 + y - 14); + particles.push({ + fy: fy, + fx: x + 1, + ty: verticle * k + fy + 4, + tx: verticle + x + 1, + r: r, + time: 0, + totalTime: PARTICLE_LAST + }); + } + } + } + } + }); +} + +/** + * 绘制传送门 + * @param floorId 要绘制传送门的楼层 + */ +export function drawPortals(floorId: FloorIds) { + drawing.splice(0); + const p = Mota.require('module', 'Mechanism').BluePalace.portals[floorId]; + if (!p) return; + p.forEach((v, i) => { + const c = color[i % color.length]; + const { fx, fy, tx, ty, dir, toDir } = v; + + let x1 = fx * 32; + let y1 = fy * 32; + let x2 = tx * 32; + let y2 = ty * 32; + + if (dir === 'down') y1 += 32; + else if (dir === 'right') x1 += 32; + + if (toDir === 'down') y2 += 32; + else if (toDir === 'right') x2 += 32; + + drawing.push({ + x: x1, + y: y1, + type: dir === 'left' || dir === 'right' ? 'v' : 'h', + color: c, + particles: [], + lastParticle: lastTime + }); + + drawing.push({ + x: x2, + y: y2, + type: toDir === 'left' || toDir === 'right' ? 'v' : 'h', + color: c, + particles: [], + lastParticle: lastTime + }); + }); + needDraw = true; +}