fix: 光环显示 & 删除无用内容

This commit is contained in:
unanmed 2024-08-29 01:52:25 +08:00
parent 9597831d74
commit 470be5edba
23 changed files with 129 additions and 1752 deletions

View File

@ -142,7 +142,7 @@
<canvas class='gameCanvas draw-canvas hide' id='event2'></canvas>
<canvas class='gameCanvas draw-canvas hide' id='fg'></canvas>
<canvas class='gameCanvas hide' id='damage'></canvas>
<canvas class='gameCanvas' id='animate'></canvas>
<canvas class='gameCanvas hide' id='animate'></canvas>
<canvas class='gameCanvas hide' id='curtain'></canvas>
<canvas class='gameCanvas' id='ui'></canvas>
<canvas class='gameCanvas' id='data'>此浏览器不支持HTML5</canvas>

View File

@ -121,6 +121,7 @@ hook.once('reset', () => {
Mota.rewrite(core.control, 'loadData', 'add', () => {
if (!main.replayChecking) {
Shadow.update(true);
LayerShadowExtends.shadowList.forEach(v => v.update());
}
});
// Mota.require('var', 'hook').on('changingFloor', (floorId) => {
@ -141,7 +142,7 @@ hook.on('setBlock', () => {
})
hook.on('changingFloor', floorId => {
Shadow.clearBuffer();
Shadow.update();
Shadow.update(true);
// setCanvasFilterByFloorId(floorId);
LayerShadowExtends.shadowList.forEach(v => v.update());
})

View File

@ -17,8 +17,6 @@
// import frag from '@/plugin/fx/frag';
// import { Mota } from '.';
import * as shadow from '@/plugin/shadow/shadow';
import * as gameShadow from '@/plugin/shadow/gameShadow';
import * as fly from '@/plugin/ui/fly';
import * as chase from '@/plugin/chase/chase';
import * as completion from '@/plugin/completion';
@ -29,8 +27,6 @@ import * as gameCanvas from '@/plugin/fx/gameCanvas';
import * as smooth from '@/plugin/fx/smoothView';
import * as animateController from '@/plugin/animateController';
Mota.Plugin.register('shadow_r', shadow, shadow.init);
Mota.Plugin.register('gameShadow_r', gameShadow, gameShadow.init);
Mota.Plugin.register('fly_r', fly);
Mota.Plugin.register('chase_r', chase);
Mota.Plugin.register('completion_r', completion, completion.init);

View File

@ -8,6 +8,7 @@ import { LayerShadowExtends } from '../fx/shadow';
import { LayerGroupFilter } from '@/plugin/fx/gameCanvas';
import { LayerGroupAnimate } from './preset/animate';
import { LayerGroupPortal } from '@/plugin/fx/portal';
import { LayerGroupHalo } from '@/plugin/fx/halo';
let main: MotaRenderer;
@ -35,16 +36,19 @@ Mota.require('var', 'loading').once('loaded', () => {
const filter = new LayerGroupFilter();
const animate = new LayerGroupAnimate();
const portal = new LayerGroupPortal();
const halo = new LayerGroupHalo();
layer.extends(damage);
layer.extends(detail);
layer.extends(filter);
layer.extends(portal);
layer.extends(halo);
layer.getLayer('event')?.extends(hero);
layer.getLayer('event')?.extends(door);
layer.getLayer('event')?.extends(shadow);
layer.extends(animate);
render.appendChild(layer);
// console.log(render);
});
Mota.require('var', 'hook').on('reset', () => {

View File

@ -56,6 +56,7 @@ export class FloorDamageExtends
private create() {
if (this.sprite) return;
const sprite = new Damage();
sprite.setZIndex(80);
this.group.appendChild(sprite);
this.sprite = sprite;
}

View File

@ -83,6 +83,14 @@ export interface ILayerGroupRenderExtends {
export type FloorLayer = 'bg' | 'bg2' | 'event' | 'fg' | 'fg2';
const layerZIndex: Record<FloorLayer, number> = {
bg: 10,
bg2: 20,
event: 30,
fg: 40,
fg2: 50
};
export class LayerGroup extends Container implements IAnimateFrame {
/** 地图组列表 */
// static list: Set<LayerGroup> = new Set();
@ -190,6 +198,7 @@ export class LayerGroup extends Container implements IAnimateFrame {
const l = new Layer();
l.layer = layer;
if (l.layer) this.layers.set(l.layer, l);
l.setZIndex(layerZIndex[layer]);
this.appendChild(l);
for (const ex of this.extend.values()) {

View File

@ -1,6 +1,8 @@
import { Animation, linear, sleep } from 'mutate-animate';
import { has } from '../utils';
// todo: 移植到渲染树
interface SplittedImage {
canvas: HTMLCanvasElement;
x: number;

View File

@ -5,10 +5,6 @@ import {
LayerGroup
} from '@/core/render/preset/layer';
export default function init() {
return {};
}
const filterMap: [FloorIds[], string][] = [];
function getCanvasFilterByFloorId(floorId: FloorIds = core.status.floorId) {

106
src/plugin/fx/halo.ts Normal file
View File

@ -0,0 +1,106 @@
import { logger } from '@/core/common/logger';
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import { mainSetting } from '@/core/main/setting';
import { LayerGroupFloorBinder } from '@/core/render/preset/floor';
import {
ILayerGroupRenderExtends,
LayerGroup
} from '@/core/render/preset/layer';
import { Sprite } from '@/core/render/sprite';
import { Transform } from '@/core/render/transform';
export class LayerGroupHalo implements ILayerGroupRenderExtends {
id: string = 'halo';
group!: LayerGroup;
binder!: LayerGroupFloorBinder;
halo!: Halo;
awake(group: LayerGroup): void {
this.group = group;
const ex = group.getExtends('floor-binder');
if (ex instanceof LayerGroupFloorBinder) {
this.binder = ex;
this.halo = new Halo();
this.halo.setHD(true);
this.halo.size(group.width, group.height);
this.halo.setZIndex(75);
this.halo.binder = ex;
group.appendChild(this.halo);
} else {
logger.error(
1401,
`Halo extends needs 'floor-binder' extends as dependency.`
);
group.removeExtends('halo');
}
}
onDestroy(group: LayerGroup): void {
this.halo?.destroy();
}
}
const haloColor: Record<number, string[]> = {
21: ['cyan'],
25: ['purple'],
26: ['blue'],
27: ['red'],
29: ['#3CFF49']
};
class Halo extends Sprite {
/** 单元格大小 */
cellSize: number = 32;
/** 当前楼层,用于获取有哪些光环 */
binder!: LayerGroupFloorBinder;
constructor() {
super('static', false);
this.setRenderFn((canvas, transform) => {
this.drawHalo(canvas, transform);
});
}
drawHalo(canvas: MotaOffscreenCanvas2D, transform: Transform) {
if (!mainSetting.getValue('screen.halo', true)) return;
const floorId = this.binder.getFloor();
if (!floorId) return;
const col = core.status.maps[floorId].enemy;
if (!col) return;
const [dx, dy] = col.translation;
const list = col.haloList.concat(
Object.keys(flags[`melt_${floorId}`] ?? {}).map(v => {
const [x, y] = v.split(',').map(v => parseInt(v));
return {
type: 'square',
data: {
x: x + dx,
y: y + dy,
d: 3
},
special: 25
};
})
);
const { ctx } = canvas;
const cell = this.cellSize;
ctx.lineWidth = 1;
for (const halo of list) {
if (halo.type === 'square') {
const { x, y, d } = halo.data;
const [color, border] = haloColor[halo.special];
const r = Math.floor(d / 2);
const left = x - r;
const top = y - r;
ctx.fillStyle = color;
ctx.strokeStyle = border ?? color;
ctx.globalAlpha = 0.1;
ctx.fillRect(left * cell, top * cell, d * cell, d * cell);
ctx.globalAlpha = 0.6;
ctx.strokeRect(left * cell, top * cell, d * cell, d * cell);
}
}
}
}

View File

@ -227,7 +227,7 @@ export class FloorItemDetail implements ILayerGroupRenderExtends {
for (const [key, value] of Object.entries(diff)) {
if (!value) continue;
const color = FloorItemDetail.detailColor[key] ?? '#fff';
const text = value.toString();
const text = Math.floor(value).toString();
const renderable: DamageRenderable = {
x: x * this.sprite.cellSize + 2,
y: y * this.sprite.cellSize + 31 - n * 10,

View File

@ -1,54 +0,0 @@
export default function init() {
return { createGaussNoise };
}
interface GaussNoiseConfig {
/** 分辨率最小为1最大为画布长宽的最大值默认为画布长宽最大值的一半过大可能会卡顿 */
resolution?: number;
/** 画布宽度 */
width?: number;
/** 画布高度 */
height?: number;
/** 噪声灰度的均值,范围 0 ~ 255 */
expectation: number;
/** 噪声灰度方差 */
deviation: number;
/** 目标画布,如果不指定会创建一个新的 */
canvas?: HTMLCanvasElement;
}
/**
*
* @param config
* @returns
*/
export function createGaussNoise(config: GaussNoiseConfig): HTMLCanvasElement {
const canvas = config.canvas ?? document.createElement('canvas');
canvas.width = config.width ?? core._PX_;
canvas.height = config.height ?? core._PY_;
canvas.style.imageRendering = 'pixelated';
const ctx = canvas.getContext('2d')!;
ctx.imageSmoothingEnabled = false;
const max = Math.max(canvas.width, canvas.height);
const resolution = Math.min(max, config.resolution ?? max / 2);
const step =
max === canvas.width
? canvas.width / resolution
: canvas.height / resolution;
for (let x = 0; x < canvas.width; x += step) {
for (let y = 0; y < canvas.height; y += step) {
const random =
Math.sqrt(Math.log(Math.random()) * -2) *
Math.sin(2 * Math.PI * Math.random());
const gray = 255 - random * config.deviation - config.expectation;
ctx.fillStyle = `rgba(${gray},${gray},${gray},${gray / 255})`;
ctx.fillRect(x, y, step, step);
}
}
return canvas;
}

View File

@ -1,5 +1,4 @@
import { has, ofDir } from '@/plugin/game/utils';
import { drawHalo } from '../fx/halo';
export function init() {
// 伤害弹出

View File

@ -986,11 +986,8 @@ export function init() {
core.setHeroLoc('y', ny);
core.setHeroLoc('direction', action);
}
if (!main.replayChecking) {
setTimeout(core.replay, 100);
} else {
core.replay();
}
return true;
} else {

View File

@ -1,69 +0,0 @@
const haloColor: Record<number, string[]> = {
21: ['cyan'],
25: ['purple'],
26: ['blue'],
27: ['red'],
29: ['#3CFF49']
};
export function drawHalo(
ctx: CanvasRenderingContext2D,
onMap: boolean,
floorId: FloorIds
) {
if (main.replayChecking) return;
const setting = Mota.require('var', 'mainSetting');
if (!setting.getValue('screen.halo', true)) return;
Mota.require('fn', 'ensureFloorDamage')(floorId);
const col = core.status.maps[floorId].enemy;
const [dx, dy] = col.translation;
const list = col.haloList.concat(
Object.keys(flags[`melt_${floorId}`] ?? {}).map(v => {
const [x, y] = v.split(',').map(v => parseInt(v));
return {
type: 'square',
data: {
x: x + dx,
y: y + dy,
d: 3
},
special: 25
};
})
);
ctx.save();
for (const halo of list) {
if (halo.type === 'square') {
const { x, y, d } = halo.data;
const [color, border] = haloColor[halo.special];
const r = Math.floor(d / 2);
let left = x - r,
right = x + r,
top = y - r,
bottom = y + r;
if (onMap && core.bigmap.v2) {
left -= core.bigmap.posX;
top -= core.bigmap.posY;
right -= core.bigmap.posX;
bottom -= core.bigmap.posY;
if (
right < -1 ||
left > core._PX_ / 32 + 1 ||
top < -1 ||
bottom > core._PY_ / 32 + 1
) {
continue;
}
}
ctx.fillStyle = color;
ctx.strokeStyle = border ?? color;
ctx.lineWidth = 1;
ctx.globalAlpha = 0.1;
ctx.fillRect(left * 32, top * 32, d * 32, d * 32);
ctx.globalAlpha = 0.6;
ctx.strokeRect(left * 32, top * 32, d * 32, d * 32);
}
}
ctx.restore();
}

View File

@ -1,34 +0,0 @@
function drawHeroDetail(px: number, py: number) {
const setting = Mota.require('var', 'mainSetting');
if (!setting.getValue('screen.heroDetail', false)) return;
const { hp, atk, def } = core.status.hero;
const toDraw = {
atk: {
value: atk,
color: '#FF7A7A'
},
def: {
value: def,
color: '#00E6F1'
},
hp: {
value: hp,
color: '#F9FFFF0'
}
};
let i = 0;
for (const [key, value] of Object.entries(toDraw)) {
const ctx = core.canvas['hero'];
core.fillBoldText(
ctx,
core.formatBigNumber(value.value),
px,
py - 10 * i,
value.color
);
i++;
}
}
export { drawHeroDetail };

View File

@ -1,60 +0,0 @@
// @ts-nocheck
var heroMoving = timestamp => {
if (core.status.heroMoving <= 0) return;
if (timestamp - core.animateFrame.moveTime > core.values.moveSpeed) {
core.animateFrame.leftLeg++;
core.animateFrame.moveTime = timestamp;
}
core.drawHero(
['stop', 'leftFoot', 'midFoot', 'rightFoot'][
core.animateFrame.leftLeg % 4
],
4 * core.status.heroMoving
);
};
export function init() {
['up', 'down', 'left', 'right'].forEach(one => {
// 指定中间帧动画
core.material.icons.hero[one].midFoot = 2;
});
core.registerAnimationFrame('heroMoving', true, heroMoving);
core.events._eventMoveHero_moving = function (step, moveSteps) {
var curr = moveSteps[0];
var direction = curr[0],
x = core.getHeroLoc('x'),
y = core.getHeroLoc('y');
// ------ 前进/后退
var o = direction == 'backward' ? -1 : 1;
if (direction == 'forward' || direction == 'backward')
direction = core.getHeroLoc('direction');
var faceDirection = direction;
if (direction == 'leftup' || direction == 'leftdown')
faceDirection = 'left';
if (direction == 'rightup' || direction == 'rightdown')
faceDirection = 'right';
core.setHeroLoc('direction', direction);
if (curr[1] <= 0) {
core.setHeroLoc('direction', faceDirection);
moveSteps.shift();
return true;
}
if (step <= 4) core.drawHero('stop', 4 * o * step);
else if (step <= 8) core.drawHero('leftFoot', 4 * o * step);
else if (step <= 12) core.drawHero('midFoot', 4 * o * (step - 8));
else if (step <= 16) core.drawHero('rightFoot', 4 * o * (step - 8)); // if (step == 8) {
if (step == 8 || step == 16) {
core.setHeroLoc('x', x + o * core.utils.scan2[direction].x, true);
core.setHeroLoc('y', y + o * core.utils.scan2[direction].y, true);
core.updateFollowers();
curr[1]--;
if (curr[1] <= 0) moveSteps.shift();
core.setHeroLoc('direction', faceDirection);
return step == 16;
}
return false;
};
}

View File

@ -27,118 +27,7 @@ export function init() {
// floor.enemy.render(true);
getItemDetail(floorId, onMap); // 宝石血瓶详细信息
// getItemDetail(floorId, onMap); // 宝石血瓶详细信息
// this.drawDamage(ctx, floorId);
};
}
// 获取宝石信息 并绘制
function getItemDetail(floorId: FloorIds, onMap: boolean) {
const setting = Mota.require('var', 'mainSetting');
if (!setting.getValue('screen.itemDetail')) return;
floorId ??= core.status.thisMap.floorId;
let diff: Record<string | symbol, number | undefined> = {};
const before = core.status.hero;
const hero = core.clone(core.status.hero);
const handler: ProxyHandler<any> = {
set(target, key, v) {
diff[key] = v - (target[key] || 0);
if (!diff[key]) diff[key] = void 0;
return true;
}
};
core.status.hero = new Proxy(hero, handler);
core.status.maps[floorId].blocks.forEach(function (block) {
if (block.event.cls !== 'items' || block.disable) return;
const x = block.x,
y = block.y;
// v2优化只绘制范围内的部分
if (onMap && core.bigmap.v2) {
if (
x < core.bigmap.posX - core.bigmap.extend ||
x > core.bigmap.posX + core._PX_ + core.bigmap.extend ||
y < core.bigmap.posY - core.bigmap.extend ||
y > core.bigmap.posY + core._PY_ + core.bigmap.extend
) {
return;
}
}
diff = {};
const id = block.event.id as AllIdsOf<'items'>;
const item = core.material.items[id];
if (item.cls === 'equips') {
// 装备也显示
const diff: Record<string, any> = core.clone(
item.equip.value ?? {}
);
const per = item.equip.percentage ?? {};
for (const name in per) {
diff[name + 'per'] =
per[name as SelectKey<HeroStatus, number>].toString() + '%';
}
drawItemDetail(diff, x, y);
return;
}
// 跟数据统计原理一样 执行效果 前后比较
core.setFlag('__statistics__', true);
try {
eval(item.itemEffect!);
} catch (error) {}
drawItemDetail(diff, x, y);
});
core.status.hero = before;
window.hero = before;
window.flags = before.flags;
}
// 绘制
function drawItemDetail(diff: any, x: number, y: number) {
const px = 32 * x + 2,
py = 32 * y + 31;
let content = '';
// 获得数据和颜色
let i = 0;
for (const name in diff) {
if (!diff[name]) continue;
let color = '#fff';
if (typeof diff[name] === 'number')
content = core.formatBigNumber(Math.round(diff[name]), true);
else content = diff[name];
switch (name) {
case 'atk':
case 'atkper':
color = '#FF7A7A';
break;
case 'def':
case 'defper':
color = '#00E6F1';
break;
case 'mdef':
case 'mdefper':
color = '#6EFF83';
break;
case 'hp':
color = '#A4FF00';
break;
case 'hpmax':
case 'hpmaxper':
color = '#F9FF00';
break;
case 'manaper':
case 'mana':
color = '#c66';
break;
}
// 绘制
core.status.damage.data.push({
text: content,
px: px,
py: py - 10 * i,
color: color as Color
});
i++;
}
}

View File

@ -1,11 +1,9 @@
/* @__PURE__ */ import './dev/hotReload'; // 仅开发会用到
import * as fiveLayer from './fiveLayer';
import * as heroFourFrames from './fx/heroFourFrames';
import * as itemDetail from './fx/itemDetail';
import * as replay from './replay';
import * as ui from './ui';
import * as rewrite from './fx/rewrite';
import * as halo from './fx/halo';
import * as loopMap from './loopMap';
import * as removeMap from './removeMap';
import * as shop from './shop';
@ -29,10 +27,8 @@ Mota.Plugin.register('chase_g', chase);
Mota.Plugin.register('skill_g', skill);
Mota.Plugin.register('towerBoss_g', towerBoss);
Mota.Plugin.register('fiveLayer_g', fiveLayer, fiveLayer.init);
Mota.Plugin.register('heroFourFrames_g', heroFourFrames, heroFourFrames.init);
Mota.Plugin.register('rewrite_g', rewrite, rewrite.init);
Mota.Plugin.register('itemDetail_g', itemDetail, itemDetail.init);
Mota.Plugin.register('halo_g', halo);
// Mota.Plugin.register('study_g', study);
Mota.Plugin.register('remainEnemy_g', remainEnemy);
Mota.Plugin.register('checkBlock_g', checkBlock, checkBlock.init);

View File

@ -1,345 +0,0 @@
import { has } from '../utils';
type CanvasStyle = string | CanvasPattern | CanvasGradient;
export class Layout {
/** 画布 */
canvas: HTMLCanvasElement;
/** 绘制上下文 */
ctx: CanvasRenderingContext2D;
static readonly CLEAR: number = 1;
static readonly MASK: number = 2;
static readonly IMAGE: number = 4;
static readonly FILL: number = 1;
static readonly STROKE: number = 2;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
}
image(layout: Layout | CanvasImageSource | Path2D, type: number): this;
image(
layout: Layout | CanvasImageSource | Path2D,
type: number,
x: number,
y: number
): this;
image(
layout: Layout | CanvasImageSource | Path2D,
type: number,
x: number,
y: number,
w: number,
h: number
): this;
image(
layout: Layout | CanvasImageSource | Path2D,
type: number,
sx: number,
sy: number,
sw: number,
sh: number,
dx: number,
dy: number,
dw: number,
dh: number
): this;
image(
layout: Layout | CanvasImageSource | Path2D,
type: number,
sx: number = 0,
sy: number = 0,
sw?: number,
sh?: number,
dx?: number,
dy?: number,
dw?: number,
dh?: number
) {
const img = layout instanceof Layout ? layout.canvas : layout;
const fill = () => {
if (img instanceof Path2D) {
this.ctx.fill(img);
} else {
if (!has(sw)) {
this.ctx.drawImage(img, sx, sy);
} else if (!has(dx)) {
this.ctx.drawImage(img, sx, sy, sw, sh!);
} else {
this.ctx.drawImage(img, sx, sy, sw, sh!, dx, dy!, dw!, dh!);
}
}
};
if (type & Layout.IMAGE) {
// 绘制图片
fill();
}
if (type & Layout.CLEAR) {
// 按照图片清除一个区域
this.ctx.save();
this.ctx.globalCompositeOperation = 'destination-out';
fill();
this.ctx.restore();
}
if (type & Layout.MASK) {
// 蒙版,只显示蒙版内的东西
this.ctx.save();
this.ctx.globalCompositeOperation = 'destination-in';
fill();
this.ctx.restore();
}
return this;
}
/**
*
*/
clear(x: number, y: number, w: number, h: number): this {
this.ctx.clearRect(x, y, w, h);
return this;
}
/**
*
* @param str
* @param type FILL表示填充STROKE表示描边FILL | STROKE
* @param x
* @param y
* @param maxWidth
*/
text(
str: string,
type: number,
x: number,
y: number,
maxWidth?: number
): this {
if (type & Layout.FILL) this.ctx.fillText(str, x, y, maxWidth);
if (type & Layout.STROKE) this.ctx.strokeText(str, x, y, maxWidth);
return this;
}
/**
*
* @param path
* @param type
*/
path(path: Path2D, type: number, rule?: CanvasFillRule): this {
if (type & Layout.FILL) this.ctx.fill(path, rule);
if (type & Layout.STROKE) this.ctx.stroke(path);
return this;
}
/**
*
*/
save(): this {
this.ctx.save();
return this;
}
/**
* 退
*/
restore(): this {
this.ctx.restore();
return this;
}
/**
*
* @param style
*/
fillStyle(style: CanvasStyle): this {
this.ctx.fillStyle = style;
return this;
}
/**
*
* @param style
*/
strokeStyle(style: CanvasStyle): this {
this.ctx.strokeStyle = style;
return this;
}
/**
*
* @param align
*/
textAlign(align: CanvasTextAlign): this {
this.ctx.textAlign = align;
return this;
}
/**
* 线
* @param align 线
*/
textBaseline(align: CanvasTextBaseline): this {
this.ctx.textBaseline = align;
return this;
}
/**
*
* @param filter
*/
filter(filter: string): this {
this.ctx.filter = filter;
return this;
}
/**
*
* @param shadow
*/
shadow(shadow: Partial<CanvasShadowStyles>): this {
for (const [p, v] of Object.entries(shadow)) {
// @ts-ignore
this.ctx[p] = v;
}
return this;
}
/**
* 线
* @param width
*/
lineWidth(width: number): this {
this.ctx.lineWidth = width;
return this;
}
/**
* 线
* @param cap 线
*/
lineCap(cap: CanvasLineCap): this {
this.ctx.lineCap = cap;
return this;
}
/**
* 线
* @param join 线
*/
lineJoin(join: CanvasLineJoin): this {
this.ctx.lineJoin = join;
return this;
}
/**
*
* @param font
*/
font(font: string): this {
this.ctx.font = font;
return this;
}
/**
*
* @param alpha
*/
alpha(alpha: number): this {
this.ctx.globalAlpha = alpha;
return this;
}
/**
* 线
* @param dash 线
*/
lineDash(dash: number[]): this {
this.ctx.setLineDash(dash);
return this;
}
/**
*
* @param x
* @param y
*/
scale(x: number, y: number): this {
this.ctx.scale(x, y);
return this;
}
/**
*
* @param rad
*/
rotate(rad: number): this {
this.ctx.rotate(rad);
return this;
}
/**
*
* @param x
* @param y
*/
translate(x: number, y: number): this {
this.ctx.translate(x, y);
return this;
}
/**
*
*/
transform(): Layout;
/**
*
*
* [a c e]
* [b d f]
* [0 0 0]
* @param a
* @param b
* @param c
* @param d
* @param e
* @param f
*/
transform(
a: number,
b: number,
c: number,
d: number,
e: number,
f: number
): Layout;
transform(
a?: number,
b?: number,
c?: number,
d?: number,
e?: number,
f?: number
) {
if (!has(a)) this.ctx.resetTransform();
else this.ctx.transform(a, b!, c!, d!, e!, f!);
return this;
}
/**
* image的蒙版功能与擦除功能本质上也是通过设置混合方式实现的
* @param value
*/
composite(value: GlobalCompositeOperation): this {
this.ctx.globalCompositeOperation = value;
return this;
}
/**
*
*/
destroy() {
this.canvas.remove();
}
}

View File

@ -1,255 +0,0 @@
import { Layout } from './layout';
export class LayoutGame extends Layout {
/** 画布id */
id: string;
constructor(
id: string,
x: number,
y: number,
w: number,
h: number,
z: number
) {
const ctx = core.createCanvas(id, x, y, w, h, z);
super(ctx.canvas);
this.id = id;
}
image2(
layout: Layout | CanvasImageSource | Path2D | ImageIds,
type: number
): LayoutGame;
image2(
layout: Layout | CanvasImageSource | Path2D | ImageIds,
type: number,
x: number,
y: number
): LayoutGame;
image2(
layout: Layout | CanvasImageSource | Path2D | ImageIds,
type: number,
x: number,
y: number,
w: number,
h: number
): LayoutGame;
image2(
layout: Layout | CanvasImageSource | Path2D | ImageIds,
type: number,
sx: number,
sy: number,
sw: number,
sh: number,
dx: number,
dy: number,
dw: number,
dh: number
): LayoutGame;
image2(
layout: Layout | CanvasImageSource | Path2D | ImageIds,
type: number,
sx?: number,
sy?: number,
sw?: number,
sh?: number,
dx?: number,
dy?: number,
dw?: number,
dh?: number
): this {
const img =
typeof layout === 'string'
? core.material.images.images[layout]
: layout;
this.image(img, type, sx!, sy!, sw!, sh!, dx!, dy!, dw!, dh!);
return this;
}
/**
*
* @param id id
* @param x
* @param y
* @param w
* @param h
* @param frame
*/
icon(
id: AllIds,
x: number,
y: number,
w?: number,
h?: number,
frame?: number
): this {
core.drawIcon(this.ctx, id, x, y, w, h, frame);
return this;
}
/**
* WindowSkin
* @param direction
*/
winskin(
background: any,
x: number,
y: number,
w: number,
h: number,
direction?: 'up' | 'down',
px?: number,
py?: number
): this {
core.drawWindowSkin(
background,
this.ctx,
x,
y,
w,
h,
direction,
px,
py
);
return this;
}
/**
*
* @param x1
* @param y1
* @param x2
* @param y2
*/
arrow(x1: number, y1: number, x2: number, y2: number): this {
core.drawArrow(this.ctx, x1, y1, x2, y2);
return this;
}
/**
* 线
*/
line(x1: number, y1: number, x2: number, y2: number): this {
this.ctx.beginPath();
this.ctx.moveTo(x1, y1);
this.ctx.lineTo(x2, y2);
this.ctx.stroke();
return this;
}
/**
*
* @param type
* @param start
* @param end
* @param anticlockwise
*/
arc(
type: number,
x: number,
y: number,
r: number,
start: number,
end: number,
anticlockwise: boolean = false
): this {
this.ctx.beginPath();
this.ctx.arc(x, y, r, start, end, anticlockwise);
this.draw(type);
return this;
}
/**
*
* @param type
*/
circle(type: number, x: number, y: number, r: number): this {
return this.arc(type, x, y, r, 0, Math.PI * 2);
}
/**
*
*/
ellipse(
type: number,
x: number,
y: number,
a: number,
b: number,
rotation: number = 0,
start: number = 0,
end: number = Math.PI * 2,
anticlockwise: boolean = false
): this {
this.ctx.beginPath();
this.ctx.ellipse(x, y, a, b, rotation, start, end, anticlockwise);
this.draw(type);
return this;
}
/**
*
* @param type
* @param nodes
* @param close
*/
polygon(type: number, nodes: LocArr[], close: boolean = true): this {
this.ctx.beginPath();
this.ctx.moveTo(nodes[0][0], nodes[0][1]);
for (let i = 1; i < nodes.length; i++) {
this.ctx.lineTo(nodes[i][0], nodes[i][1]);
}
if (close) this.ctx.closePath();
this.draw(type);
return this;
}
/**
*
*/
rect(type: number, x: number, y: number, w: number, h: number): this {
if (type & Layout.FILL) this.ctx.fillRect(x, y, w, h);
if (type & Layout.STROKE) this.ctx.strokeRect(x, y, w, h);
return this;
}
/**
*
*/
roundRect(
type: number,
x: number,
y: number,
w: number,
h: number,
r: number
) {
this.ctx.beginPath();
this.ctx.moveTo(x + r, y);
this.ctx.lineTo(x + w - r, y);
this.ctx.quadraticCurveTo(x + w, y, x + w, y + r);
this.ctx.lineTo(x + w, y + h - r);
this.ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
this.ctx.lineTo(x + r, y + h);
this.ctx.quadraticCurveTo(x, y + h, x, y + h - r);
this.ctx.lineTo(x, y + r);
this.ctx.quadraticCurveTo(x, y, x + r, y);
this.ctx.closePath();
this.draw(type);
}
/**
*
*/
destroy() {
core.deleteCanvas(this.id);
}
private draw(type: number) {
if (type & Layout.FILL) this.ctx.fill();
if (type & Layout.STROKE) this.ctx.stroke();
}
}

View File

@ -1,243 +0,0 @@
import { Polygon } from './polygon';
import {
Light,
getAllLights,
initShadowCanvas,
refreshLight,
removeAllLights,
setBackground,
setBlur,
setLightList,
setShadowNodes
} from './shadow';
import { pColor } from '../utils';
import { setCanvasFilterByFloorId } from '../fx/gameCanvas';
export function init() {
// 勇士身上的光源
// Mota.rewrite(core.control, 'drawHero', 'add', () => {
// if (core.getFlag('__heroOpacity__') !== 0) {
// getAllLights().forEach(v => {
// if (!v.followHero) return;
// v._offset ??= { x: v.x, y: v.y };
// v.x = core.status.heroCenter.px + v._offset.x;
// v.y = core.status.heroCenter.py + v._offset.y;
// refreshLight();
// });
// }
// });
// // 更新地形数据
// Mota.rewrite(core.maps, 'removeBlock', 'add', success => {
// if (success && main.replayChecking) updateShadow(true);
// return success;
// });
// Mota.rewrite(core.maps, 'setBlock', 'add', () => {
// if (main.replayChecking) updateShadow(true);
// });
// Mota.rewrite(core.events, 'changingFloor', 'add', (_, floorId) => {
// if (!main.replayChecking) {
// updateShadow();
// setCanvasFilterByFloorId(floorId);
// }
// });
// // 初始化画布信息
// Mota.rewrite(core.ui, 'deleteAllCanvas', 'add', () => {
// if (main.mode === 'play' && !main.replayChecking) initShadowCanvas();
// });
}
const shadowInfo: Partial<Record<FloorIds, Light[]>> = {
MT50: [
{
id: 'mt50_1',
x: 144,
y: 144,
decay: 20,
r: 150,
color: pColor('#e953'),
noShelter: true
},
{
id: 'mt50_2',
x: 336,
y: 144,
decay: 20,
r: 150,
color: pColor('#e953'),
noShelter: true
},
{
id: 'mt50_2',
x: 336,
y: 336,
decay: 20,
r: 150,
color: pColor('#e953'),
noShelter: true
},
{
id: 'mt50_2',
x: 144,
y: 336,
decay: 20,
r: 150,
color: pColor('#e953'),
noShelter: true
}
],
MT52: [
{
id: 'mt51_1',
x: 13 * 32 + 16,
y: 6 * 32 + 16,
decay: 20,
r: 250,
color: pColor('#e953')
},
{
id: 'mt51_2',
x: 1 * 32 + 16,
y: 13 * 32 + 16,
decay: 20,
r: 350,
color: pColor('#e953')
}
]
};
const backgroundInfo: Partial<Record<FloorIds, Color>> = {
MT50: pColor('#0006'),
MT51: pColor('#0004'),
MT52: pColor('#0004')
};
const blurInfo: Partial<Record<FloorIds, number>> = {};
const immersionInfo: Partial<Record<FloorIds, number>> = {};
const shadowCache: Partial<Record<FloorIds, Polygon[]>> = {};
let calMapShadow = true;
Mota.require('var', 'loading').once('coreInit', () => {
core.floorIds.slice(61).forEach(v => {
shadowInfo[v] ??= [];
shadowInfo[v]?.push({
id: `${v}_hero`,
x: 0,
y: 0,
decay: 50,
r: 300,
color: 'transparent',
followHero: true
});
core.floors[v].map.forEach((arr, y) => {
arr.forEach((num, x) => {
if (num === 103) {
shadowInfo[v]?.push({
id: `${v}_${x}_${y}`,
x: x * 32 + 16,
y: y * 32 + 16,
decay: 20,
r: 350,
color: pColor('#e953')
});
}
});
});
});
});
export function updateShadow(nocache: boolean = false) {
// todo: 需要优化优化成bfs
const floor = core.status.floorId;
if (!shadowInfo[floor] || !backgroundInfo[floor]) {
removeAllLights();
setShadowNodes([]);
setBackground('transparent');
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) left -= immerse * 2;
if (x + 1 === w) right += immerse * 2;
if (y === 0) top -= immerse * 2;
if (y + 1 === h) bottom += immerse * 2;
if (blocks[l] && blocks[l].event.noPass) {
left -= immerse;
}
if (blocks[r] && blocks[r].event.noPass) {
right += immerse;
}
if (blocks[t] && blocks[t].event.noPass) {
top -= immerse;
}
if (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();
}

View File

@ -1,91 +0,0 @@
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.map(v => [v[0] + 32, v[1] + 32]);
}
/**
*
*/
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__) + 64;
const h = (core._PY_ ?? core.__PIXELS__) + 64;
const aspect = h / w;
const intersect = (nx: number, ny: number): LocArr => {
const k = (ny - y) / (nx - x);
if (k > aspect || k < -aspect) {
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;
}
}

View File

@ -1,468 +0,0 @@
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;
/** 是否跟随勇士 */
followHero?: boolean;
/** 正在动画的属性 */
_animating?: Record<string, boolean>;
/** 执行渐变的属性 */
_transition?: Record<string, TransitionInfo>;
/** 表示是否是代理只有设置渐变后才会变为true */
_isProxy?: boolean;
/** 跟随勇士的时候的偏移量 */
_offset?: Loc;
}
export function init() {
core.registerAnimationFrame('shadow', true, () => {
if (!needRefresh) return;
drawShadow();
});
}
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', -32, -32, w + 64, h + 64, 55);
canvas = ctx.canvas;
const s = core.domStyle.scale * devicePixelRatio;
temp1.width = (w + 64) * s;
temp1.height = (h + 64) * s;
temp2.width = (w + 64) * s;
temp2.height = (h + 64) * s;
temp3.width = (w + 64) * s;
temp3.height = (h + 64) * 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;
}
/**
*
*/
export function refreshLight() {
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__) + 64;
const h = (core._PY_ ?? core.__PIXELS__) + 64;
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];
const rx = x + 32;
const ry = y + 32;
// 绘制阴影
ct1.clearRect(0, 0, w, h);
ct2.clearRect(0, 0, w, h);
if (!noShelter) {
for (const polygon of shadowNodes) {
const area = polygon.shadowArea(rx, ry, 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(rx, ry, decay, rx, ry, r);
gra.addColorStop(0, core.arrayToRGBA(color));
gra.addColorStop(1, 'transparent');
ct2.fillStyle = gra;
ct2.beginPath();
ct2.arc(rx, ry, r, 0, Math.PI * 2);
ct2.fill();
ctx.drawImage(temp2, 0, 0, w, h);
// 再绘制ct1的阴影然后绘制到ct3叠加
ct1.globalCompositeOperation = 'source-out';
const gra2 = ct1.createRadialGradient(rx, ry, decay, rx, ry, r);
gra2.addColorStop(0, '#fff');
gra2.addColorStop(1, '#fff0');
ct1.beginPath();
ct1.arc(rx, ry, 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);
}