mirror of
https://github.com/motajs/template.git
synced 2026-04-12 07:02:30 +08:00
refactor: 渲染系统
This commit is contained in:
parent
9ab0db9465
commit
191ba8d1db
@ -139,11 +139,11 @@ export const ConfirmBox = defineComponent<
|
||||
height.value = textHeight + pad.value * 4;
|
||||
};
|
||||
|
||||
const setYes = (_: string, width: number, height: number) => {
|
||||
const setYes = (width: number, height: number) => {
|
||||
yesSize.value = [width, height];
|
||||
};
|
||||
|
||||
const setNo = (_: string, width: number, height: number) => {
|
||||
const setNo = (width: number, height: number) => {
|
||||
noSize.value = [width, height];
|
||||
};
|
||||
|
||||
@ -190,7 +190,7 @@ export const ConfirmBox = defineComponent<
|
||||
zIndex={15}
|
||||
onClick={() => emit('yes')}
|
||||
onEnter={() => (selected.value = true)}
|
||||
onSetText={setYes}
|
||||
onResize={setYes}
|
||||
/>
|
||||
<text
|
||||
loc={noLoc.value}
|
||||
@ -201,7 +201,7 @@ export const ConfirmBox = defineComponent<
|
||||
zIndex={15}
|
||||
onClick={() => emit('no')}
|
||||
onEnter={() => (selected.value = false)}
|
||||
onSetText={setNo}
|
||||
onResize={setNo}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
@ -459,7 +459,7 @@ export const Choices = defineComponent<
|
||||
contentHeight.value = height;
|
||||
};
|
||||
|
||||
const updateTitleHeight = (_0: string, _1: number, height: number) => {
|
||||
const updateTitleHeight = (_: number, height: number) => {
|
||||
titleHeight.value = height;
|
||||
};
|
||||
|
||||
@ -519,7 +519,7 @@ export const Choices = defineComponent<
|
||||
font={props.titleFont ?? new Font(void 0, 18)}
|
||||
fillStyle={props.titleFill ?? 'gold'}
|
||||
zIndex={5}
|
||||
onSetText={updateTitleHeight}
|
||||
onResize={updateTitleHeight}
|
||||
/>
|
||||
<TextContent
|
||||
{...attrs}
|
||||
@ -556,7 +556,7 @@ export const Choices = defineComponent<
|
||||
zIndex={5}
|
||||
fillStyle={props.selFill}
|
||||
onClick={() => emit('choose', v[0])}
|
||||
onSetText={(_, width, height) =>
|
||||
onResize={(width, height) =>
|
||||
updateChoiceSize(i, width, height)
|
||||
}
|
||||
onEnter={() => (selected.value = i)}
|
||||
|
||||
@ -219,7 +219,7 @@ export const FloorSelector = defineComponent<
|
||||
lineWidth={1}
|
||||
strokeStyle="#aaa"
|
||||
/>
|
||||
<sprite
|
||||
<custom
|
||||
zIndex={20}
|
||||
loc={[0, 0, 144, SCROLL_HEIGHT]}
|
||||
nocache
|
||||
|
||||
@ -5,10 +5,11 @@ import {
|
||||
Container,
|
||||
ElementLocator,
|
||||
MotaRenderer,
|
||||
RenderItem,
|
||||
Transform,
|
||||
Font,
|
||||
RectRCircleParams
|
||||
RectRCircleParams,
|
||||
IRenderItem,
|
||||
IRenderTreeRoot
|
||||
} from '@motajs/render';
|
||||
import { transitionedColor, useKey } from '../use';
|
||||
import { linear } from 'mutate-animate';
|
||||
@ -161,9 +162,9 @@ export const Input = defineComponent<InputProps, InputEmits, keyof InputEmits>(
|
||||
if (!ele) return;
|
||||
// 计算当前绝对位置
|
||||
|
||||
const chain: RenderItem[] = [];
|
||||
let now: RenderItem | undefined = root.value;
|
||||
let renderer: MotaRenderer | undefined;
|
||||
const chain: IRenderItem[] = [];
|
||||
let now: IRenderItem | null = root.value ?? null;
|
||||
let renderer: IRenderTreeRoot | null = null;
|
||||
if (!now) return;
|
||||
while (now) {
|
||||
chain.unshift(now);
|
||||
@ -441,11 +442,11 @@ export const InputBox = defineComponent<
|
||||
emit('input', value);
|
||||
};
|
||||
|
||||
const setYes = (_: string, width: number, height: number) => {
|
||||
const setYes = (width: number, height: number) => {
|
||||
yesSize.value = [width, height];
|
||||
};
|
||||
|
||||
const setNo = (_: string, width: number, height: number) => {
|
||||
const setNo = (width: number, height: number) => {
|
||||
noSize.value = [width, height];
|
||||
};
|
||||
|
||||
@ -500,7 +501,7 @@ export const InputBox = defineComponent<
|
||||
zIndex={15}
|
||||
onClick={confirm}
|
||||
onEnter={() => (selected.value = true)}
|
||||
onSetText={setYes}
|
||||
onResize={setYes}
|
||||
/>
|
||||
<text
|
||||
loc={noLoc.value}
|
||||
@ -511,7 +512,7 @@ export const InputBox = defineComponent<
|
||||
zIndex={15}
|
||||
onClick={cancel}
|
||||
onEnter={() => (selected.value = false)}
|
||||
onSetText={setNo}
|
||||
onResize={setNo}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
import { ElementLocator, Sprite, MotaOffscreenCanvas2D } from '@motajs/render';
|
||||
import { DefaultProps, PathProps, onTick } from '@motajs/render-vue';
|
||||
import {
|
||||
ElementLocator,
|
||||
CustomRenderItem,
|
||||
MotaOffscreenCanvas2D
|
||||
} from '@motajs/render';
|
||||
import { DefaultProps, PathProps } from '@motajs/render-vue';
|
||||
import { computed, defineComponent, ref, SetupContext, watch } from 'vue';
|
||||
import { TextContent, TextContentProps } from './textbox';
|
||||
import { Scroll, ScrollExpose, ScrollProps } from './scroll';
|
||||
import { transitioned } from '../use';
|
||||
import { hyper } from 'mutate-animate';
|
||||
import { logger } from '@motajs/common';
|
||||
import { GameUI, IUIMountable, SetupComponentOptions } from '@motajs/system';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { using } from '../renderer';
|
||||
import { cosh, CurveMode } from '@motajs/animate';
|
||||
|
||||
interface ProgressProps extends DefaultProps {
|
||||
/** 进度条的位置 */
|
||||
@ -37,7 +42,7 @@ const progressProps = {
|
||||
* ```
|
||||
*/
|
||||
export const Progress = defineComponent<ProgressProps>(props => {
|
||||
const element = ref<Sprite>();
|
||||
const element = ref<CustomRenderItem>();
|
||||
|
||||
const render = (canvas: MotaOffscreenCanvas2D) => {
|
||||
const { ctx } = canvas;
|
||||
@ -67,7 +72,7 @@ export const Progress = defineComponent<ProgressProps>(props => {
|
||||
});
|
||||
|
||||
return () => {
|
||||
return <sprite ref={element} loc={props.loc} render={render}></sprite>;
|
||||
return <custom ref={element} loc={props.loc} render={render}></custom>;
|
||||
};
|
||||
}, progressProps);
|
||||
|
||||
@ -217,7 +222,7 @@ export const ScrollText = defineComponent<
|
||||
let paused = false;
|
||||
let nowScroll = 0;
|
||||
|
||||
onTick(() => {
|
||||
using.onExcitedFunc(() => {
|
||||
if (paused || !scroll.value) return;
|
||||
const now = Date.now();
|
||||
const dt = now - lastFixedTime;
|
||||
@ -296,7 +301,11 @@ const selectionProps = {
|
||||
export const Selection = defineComponent<SelectionProps>(props => {
|
||||
const minAlpha = computed(() => props.alphaRange?.[0] ?? 0.25);
|
||||
const maxAlpha = computed(() => props.alphaRange?.[1] ?? 0.55);
|
||||
const alpha = transitioned(minAlpha.value, 2000, hyper('sin', 'in-out'))!;
|
||||
const alpha = transitioned(
|
||||
minAlpha.value,
|
||||
2000,
|
||||
cosh(2, CurveMode.EaseInOut)
|
||||
)!;
|
||||
|
||||
const isWinskin = computed(() => !!props.winskin);
|
||||
const winskinImage = computed(() =>
|
||||
@ -326,7 +335,7 @@ export const Selection = defineComponent<SelectionProps>(props => {
|
||||
ctx.drawImage(image, 158, 66, 2, 28, width - 2, 2, 2, height - 4);
|
||||
};
|
||||
|
||||
onTick(() => {
|
||||
using.onExcitedFunc(() => {
|
||||
if (alpha.value === maxAlpha.value) {
|
||||
alpha.set(minAlpha.value);
|
||||
}
|
||||
@ -337,7 +346,7 @@ export const Selection = defineComponent<SelectionProps>(props => {
|
||||
|
||||
return () =>
|
||||
isWinskin.value ? (
|
||||
<sprite
|
||||
<custom
|
||||
loc={props.loc}
|
||||
render={renderWinskin}
|
||||
alpha={alpha.ref.value}
|
||||
@ -386,7 +395,7 @@ export const Background = defineComponent<BackgroundProps>(props => {
|
||||
|
||||
return () =>
|
||||
isWinskin.value ? (
|
||||
<winskin image={props.winskin!} loc={props.loc} noanti />
|
||||
<winskin imageName={props.winskin!} loc={props.loc} noanti />
|
||||
) : (
|
||||
<g-rectr
|
||||
loc={fixedLoc.value}
|
||||
@ -401,8 +410,7 @@ export const Background = defineComponent<BackgroundProps>(props => {
|
||||
}, backgroundProps);
|
||||
|
||||
export interface WaitBoxProps<T>
|
||||
extends Partial<BackgroundProps>,
|
||||
Partial<TextContentProps> {
|
||||
extends Partial<BackgroundProps>, Partial<TextContentProps> {
|
||||
loc: ElementLocator;
|
||||
width: number;
|
||||
promise?: Promise<T>;
|
||||
|
||||
@ -13,8 +13,7 @@ import {
|
||||
import {
|
||||
Container,
|
||||
ElementLocator,
|
||||
RenderItem,
|
||||
Sprite,
|
||||
CustomRenderItem,
|
||||
Transform,
|
||||
MotaOffscreenCanvas2D,
|
||||
IActionEvent,
|
||||
@ -22,9 +21,10 @@ import {
|
||||
MouseType,
|
||||
EventProgress,
|
||||
ActionEventMap,
|
||||
ContainerCustom,
|
||||
ActionType,
|
||||
CustomContainerPropagateOrigin
|
||||
CustomContainerPropagateOrigin,
|
||||
IRenderItem,
|
||||
ICustomContainer
|
||||
} from '@motajs/render';
|
||||
import { hyper, linear, Transition } from 'mutate-animate';
|
||||
import { clamp } from 'lodash-es';
|
||||
@ -110,10 +110,10 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
/** 滚动条的定位 */
|
||||
const sp = ref<ElementLocator>([0, 0, 1, 1]);
|
||||
|
||||
const listenedChild: Set<RenderItem> = new Set();
|
||||
const areaMap: Map<RenderItem, [number, number]> = new Map();
|
||||
const listenedChild: Set<IRenderItem> = new Set();
|
||||
const areaMap: Map<IRenderItem, [number, number]> = new Map();
|
||||
const content = ref<Container>();
|
||||
const scroll = ref<Sprite>();
|
||||
const scroll = ref<CustomRenderItem>();
|
||||
|
||||
const scrollAlpha = transitioned(0.5, 100, linear())!;
|
||||
|
||||
@ -187,7 +187,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
/**
|
||||
* 计算一个元素会在画面上显示的区域
|
||||
*/
|
||||
const getArea = (item: RenderItem, rect: DOMRectReadOnly) => {
|
||||
const getArea = (item: IRenderItem, rect: DOMRectReadOnly) => {
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
areaMap.set(item, [rect.left - width.value, rect.right]);
|
||||
} else {
|
||||
@ -198,7 +198,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
/**
|
||||
* 检查一个元素是否需要显示,不需要则隐藏
|
||||
*/
|
||||
const checkItem = (item: RenderItem) => {
|
||||
const checkItem = (item: IRenderItem) => {
|
||||
const area = areaMap.get(item);
|
||||
if (!area) {
|
||||
item.show();
|
||||
@ -222,7 +222,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
/**
|
||||
* 当一个元素的矩阵发生变换时执行,检查其显示区域
|
||||
*/
|
||||
const onTransform = (item: RenderItem) => {
|
||||
const onTransform = (item: IRenderItem) => {
|
||||
const rect = item.getBoundingRect();
|
||||
const pad = props.padEnd ?? 0;
|
||||
if (item.parent === content.value) {
|
||||
@ -340,7 +340,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
|
||||
const renderContent = (
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
children: RenderItem[],
|
||||
children: IRenderItem[],
|
||||
transform: Transform
|
||||
) => {
|
||||
const ctx = canvas.ctx;
|
||||
@ -367,7 +367,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
type: T,
|
||||
progress: EventProgress,
|
||||
event: ActionEventMap[T],
|
||||
_: ContainerCustom,
|
||||
_: ICustomContainer,
|
||||
origin: CustomContainerPropagateOrigin
|
||||
) => {
|
||||
if (progress === EventProgress.Capture) {
|
||||
@ -562,7 +562,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
>
|
||||
{slots.default?.()}
|
||||
</container-custom>
|
||||
<sprite
|
||||
<custom
|
||||
nocache
|
||||
hidden={props.noscroll}
|
||||
loc={sp.value}
|
||||
@ -573,7 +573,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
zIndex={10}
|
||||
onEnter={enter}
|
||||
onLeave={leave}
|
||||
></sprite>
|
||||
></custom>
|
||||
</container>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import {
|
||||
ElementLocator,
|
||||
Font,
|
||||
Sprite,
|
||||
CustomRenderItem,
|
||||
Text,
|
||||
MotaOffscreenCanvas2D
|
||||
} from '@motajs/render';
|
||||
@ -171,7 +171,7 @@ export const TextContent = defineComponent<
|
||||
|
||||
expose<TextContentExpose>({ retype, showAll, getHeight });
|
||||
|
||||
const spriteElement = shallowRef<Sprite>();
|
||||
const spriteElement = shallowRef<CustomRenderItem>();
|
||||
const renderContent = (canvas: MotaOffscreenCanvas2D) => {
|
||||
const ctx = canvas.ctx;
|
||||
ctx.textBaseline = 'top';
|
||||
@ -225,11 +225,11 @@ export const TextContent = defineComponent<
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<sprite
|
||||
<custom
|
||||
loc={loc.value}
|
||||
ref={spriteElement}
|
||||
render={renderContent}
|
||||
></sprite>
|
||||
></custom>
|
||||
);
|
||||
};
|
||||
}, textContentOptions);
|
||||
@ -493,7 +493,7 @@ export const Textbox = defineComponent<
|
||||
slots.title(data)
|
||||
) : props.winskin ? (
|
||||
<winskin
|
||||
image={props.winskin}
|
||||
imageName={props.winskin}
|
||||
loc={[0, 0, tw.value, th.value]}
|
||||
></winskin>
|
||||
) : (
|
||||
@ -514,7 +514,7 @@ export const Textbox = defineComponent<
|
||||
slots.default(data)
|
||||
) : props.winskin ? (
|
||||
<winskin
|
||||
image={props.winskin}
|
||||
imageName={props.winskin}
|
||||
loc={[0, contentY.value, data.width!, backHeight.value]}
|
||||
></winskin>
|
||||
) : (
|
||||
|
||||
@ -3,7 +3,7 @@ import { Font, MotaOffscreenCanvas2D } from '@motajs/render';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { RenderableData, AutotileRenderable, texture } from '../elements';
|
||||
import { onTick } from '@motajs/render-vue';
|
||||
import { using } from '../renderer';
|
||||
|
||||
/** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */
|
||||
const SAFE_PAD = 1;
|
||||
@ -151,8 +151,7 @@ interface ISizedTextContentBlock {
|
||||
}
|
||||
|
||||
export interface ITextContentTextBlock
|
||||
extends ITextContentBlockBase,
|
||||
ISizedTextContentBlock {
|
||||
extends ITextContentBlockBase, ISizedTextContentBlock {
|
||||
readonly type: TextContentType.Text;
|
||||
/** 文本 block 的文字内容 */
|
||||
readonly text: string;
|
||||
@ -165,8 +164,7 @@ export interface ITextContentTextBlock
|
||||
}
|
||||
|
||||
export interface ITextContentIconBlock
|
||||
extends ITextContentBlockBase,
|
||||
ISizedTextContentBlock {
|
||||
extends ITextContentBlockBase, ISizedTextContentBlock {
|
||||
readonly type: TextContentType.Icon;
|
||||
/** 图标 block 显示的图标 */
|
||||
readonly icon: AllNumbers;
|
||||
@ -200,8 +198,7 @@ export interface ITyperRenderableBase {
|
||||
}
|
||||
|
||||
export interface ITyperTextRenderable
|
||||
extends ITextContentTextBlock,
|
||||
ITyperRenderableBase {
|
||||
extends ITextContentTextBlock, ITyperRenderableBase {
|
||||
/** 文本左上角的横坐标 */
|
||||
readonly x: number;
|
||||
/** 文本左上角的纵坐标 */
|
||||
@ -211,8 +208,7 @@ export interface ITyperTextRenderable
|
||||
}
|
||||
|
||||
export interface ITyperIconRenderable
|
||||
extends ITextContentIconBlock,
|
||||
ITyperRenderableBase {
|
||||
extends ITextContentIconBlock, ITyperRenderableBase {
|
||||
/** 图标左上角的横坐标 */
|
||||
readonly x: number;
|
||||
/** 图标左上角的纵坐标 */
|
||||
@ -220,8 +216,7 @@ export interface ITyperIconRenderable
|
||||
}
|
||||
|
||||
export interface ITyperWaitRenderable
|
||||
extends ITextContentWaitBlock,
|
||||
ITyperRenderableBase {
|
||||
extends ITextContentWaitBlock, ITyperRenderableBase {
|
||||
/** 当然是否已经等待了多少个字符 */
|
||||
waited: number;
|
||||
}
|
||||
@ -314,7 +309,7 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
|
||||
this.config
|
||||
);
|
||||
|
||||
onTick(() => this.tick());
|
||||
using.onExcitedFunc(() => this.tick());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
import { ElementLocator, MotaOffscreenCanvas2D, Sprite } from '@motajs/render';
|
||||
import { SpriteProps } from '@motajs/render-vue';
|
||||
import {
|
||||
ElementLocator,
|
||||
MotaOffscreenCanvas2D,
|
||||
CustomRenderItem
|
||||
} from '@motajs/render';
|
||||
import { CustomProps } from '@motajs/render-vue';
|
||||
import { defineComponent, ref, watch } from 'vue';
|
||||
import { SetupComponentOptions } from '@motajs/system';
|
||||
|
||||
export interface ThumbnailProps extends SpriteProps {
|
||||
export interface ThumbnailProps extends CustomProps {
|
||||
/** 缩略图的位置 */
|
||||
loc: ElementLocator;
|
||||
/** 楼层 ID */
|
||||
@ -37,7 +41,7 @@ const thumbnailProps = {
|
||||
} satisfies SetupComponentOptions<ThumbnailProps>;
|
||||
|
||||
export const Thumbnail = defineComponent<ThumbnailProps>(props => {
|
||||
const spriteRef = ref<Sprite>();
|
||||
const spriteRef = ref<CustomRenderItem>();
|
||||
|
||||
const update = () => {
|
||||
spriteRef.value?.update();
|
||||
@ -75,6 +79,6 @@ export const Thumbnail = defineComponent<ThumbnailProps>(props => {
|
||||
watch(props, update);
|
||||
|
||||
return () => (
|
||||
<sprite noanti ref={spriteRef} loc={props.loc} render={drawThumbnail} />
|
||||
<custom noanti ref={spriteRef} loc={props.loc} render={drawThumbnail} />
|
||||
);
|
||||
}, thumbnailProps);
|
||||
|
||||
@ -106,7 +106,7 @@ export const Tip = defineComponent<TipProps>((props, { expose }) => {
|
||||
hide();
|
||||
};
|
||||
|
||||
const onSetText = (_: string, width: number) => {
|
||||
const onSetText = (width: number) => {
|
||||
textWidth.value = width;
|
||||
};
|
||||
|
||||
@ -137,7 +137,7 @@ export const Tip = defineComponent<TipProps>((props, { expose }) => {
|
||||
<text
|
||||
loc={textLoc.value}
|
||||
text={text.value}
|
||||
onSetText={onSetText}
|
||||
onResize={onSetText}
|
||||
font={font}
|
||||
/>
|
||||
</container>
|
||||
|
||||
@ -1,278 +0,0 @@
|
||||
import {
|
||||
RenderAdapter,
|
||||
transformCanvas,
|
||||
ERenderItemEvent,
|
||||
RenderItem,
|
||||
Transform,
|
||||
MotaOffscreenCanvas2D
|
||||
} from '@motajs/render';
|
||||
import { HeroRenderer } from './hero';
|
||||
import { ILayerGroupRenderExtends, LayerGroup } from './layer';
|
||||
|
||||
export class LayerGroupAnimate implements ILayerGroupRenderExtends {
|
||||
static animateList: Set<LayerGroupAnimate> = new Set();
|
||||
id: string = 'animate';
|
||||
|
||||
group!: LayerGroup;
|
||||
hero?: HeroRenderer;
|
||||
animate!: Animate;
|
||||
|
||||
private animation: Set<AnimateData> = new Set();
|
||||
|
||||
/**
|
||||
* 绘制一个跟随勇士的动画
|
||||
* @param name 动画id
|
||||
*/
|
||||
async drawHeroAnimate(name: AnimationIds) {
|
||||
const animate = this.animate.animate(name, 0, 0);
|
||||
this.updatePosition(animate);
|
||||
await this.animate.draw(animate);
|
||||
this.animation.delete(animate);
|
||||
}
|
||||
|
||||
private updatePosition(animate: AnimateData) {
|
||||
if (!this.checkHero()) return;
|
||||
if (!this.hero?.renderable) return;
|
||||
const { x, y } = this.hero.renderable;
|
||||
const cell = this.group.cellSize;
|
||||
const half = cell / 2;
|
||||
animate.centerX = x * cell + half;
|
||||
animate.centerY = y * cell + half;
|
||||
}
|
||||
|
||||
private onMoveTick = (x: number, y: number) => {
|
||||
const cell = this.group.cellSize;
|
||||
const half = cell / 2;
|
||||
const ax = x * cell + half;
|
||||
const ay = y * cell + half;
|
||||
this.animation.forEach(v => {
|
||||
v.centerX = ax;
|
||||
v.centerY = ay;
|
||||
});
|
||||
};
|
||||
|
||||
private listen() {
|
||||
if (this.checkHero()) {
|
||||
this.hero!.on('moveTick', this.onMoveTick);
|
||||
}
|
||||
}
|
||||
|
||||
private checkHero() {
|
||||
if (this.hero) return true;
|
||||
const ex = this.group.getLayer('event')?.getExtends('floor-hero');
|
||||
if (ex instanceof HeroRenderer) {
|
||||
this.hero = ex;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
awake(group: LayerGroup): void {
|
||||
this.group = group;
|
||||
this.animate = new Animate();
|
||||
this.animate.size(group.width, group.height);
|
||||
this.animate.setHD(true);
|
||||
this.animate.setZIndex(100);
|
||||
group.appendChild(this.animate);
|
||||
LayerGroupAnimate.animateList.add(this);
|
||||
this.listen();
|
||||
}
|
||||
|
||||
onDestroy(_group: LayerGroup): void {
|
||||
if (this.checkHero()) {
|
||||
this.hero!.off('moveTick', this.onMoveTick);
|
||||
LayerGroupAnimate.animateList.delete(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AnimateData {
|
||||
obj: globalThis.Animate;
|
||||
/** 第一帧是全局第几帧 */
|
||||
readonly start: number;
|
||||
/** 当前是第几帧 */
|
||||
index: number;
|
||||
/** 是否需要播放音频 */
|
||||
sound: boolean;
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
onEnd?: () => void;
|
||||
readonly absolute: boolean;
|
||||
}
|
||||
|
||||
export interface EAnimateEvent extends ERenderItemEvent {}
|
||||
|
||||
export class Animate extends RenderItem<EAnimateEvent> {
|
||||
/** 绝对位置的动画 */
|
||||
private absoluteAnimates: Set<AnimateData> = new Set();
|
||||
/** 静态位置的动画 */
|
||||
private staticAnimates: Set<AnimateData> = new Set();
|
||||
|
||||
private delegation: number;
|
||||
private frame: number = 0;
|
||||
private lastTime: number = 0;
|
||||
|
||||
constructor() {
|
||||
super('absolute', false, true);
|
||||
|
||||
this.delegation = this.delegateTicker(time => {
|
||||
if (time - this.lastTime < 50) return;
|
||||
this.lastTime = time;
|
||||
this.frame++;
|
||||
if (
|
||||
this.absoluteAnimates.size > 0 ||
|
||||
this.staticAnimates.size > 0
|
||||
) {
|
||||
this.update(this);
|
||||
}
|
||||
});
|
||||
|
||||
adapter.add(this);
|
||||
}
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
transform: Transform
|
||||
): void {
|
||||
if (
|
||||
this.absoluteAnimates.size === 0 &&
|
||||
this.staticAnimates.size === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.drawAnimates(this.absoluteAnimates, canvas);
|
||||
transformCanvas(canvas, transform);
|
||||
this.drawAnimates(this.staticAnimates, canvas);
|
||||
}
|
||||
|
||||
private drawAnimates(
|
||||
data: Set<AnimateData>,
|
||||
canvas: MotaOffscreenCanvas2D
|
||||
) {
|
||||
if (data.size === 0) return;
|
||||
const { ctx } = canvas;
|
||||
const toDelete = new Set<AnimateData>();
|
||||
data.forEach(v => {
|
||||
const obj = v.obj;
|
||||
const index = v.index;
|
||||
const frame = obj.frames[index];
|
||||
const ratio = obj.ratio;
|
||||
if (!v.sound) {
|
||||
const se = (index % obj.frame) + 1;
|
||||
core.playSound(v.obj.se[se], v.obj.pitch[se]);
|
||||
v.sound = true;
|
||||
}
|
||||
const centerX = v.centerX;
|
||||
const centerY = v.centerY;
|
||||
|
||||
frame.forEach(v => {
|
||||
const img = obj.images[v.index];
|
||||
if (!img) return;
|
||||
|
||||
const realWidth = (img.width * ratio * v.zoom) / 100;
|
||||
const realHeight = (img.height * ratio * v.zoom) / 100;
|
||||
ctx.globalAlpha = v.opacity / 255;
|
||||
|
||||
const cx = centerX + v.x;
|
||||
const cy = centerY + v.y;
|
||||
|
||||
const ix = -realWidth / 2;
|
||||
const iy = -realHeight / 2;
|
||||
const angle = v.angle ? (-v.angle * Math.PI) / 180 : 0;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(cx, cy);
|
||||
if (v.mirror) {
|
||||
ctx.scale(-1, 1);
|
||||
}
|
||||
ctx.rotate(angle);
|
||||
ctx.drawImage(img, ix, iy, realWidth, realHeight);
|
||||
ctx.restore();
|
||||
});
|
||||
const now = this.frame - v.start;
|
||||
if (now !== v.index) v.sound = true;
|
||||
v.index = now;
|
||||
if (v.index === v.obj.frame) {
|
||||
toDelete.add(v);
|
||||
}
|
||||
});
|
||||
toDelete.forEach(v => {
|
||||
data.delete(v);
|
||||
v.onEnd?.();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个可以被执行的动画
|
||||
* @param name 动画名称
|
||||
* @param absolute 是否是绝对定位,绝对定位不会受到transform的影响
|
||||
*/
|
||||
animate(
|
||||
name: AnimationIds,
|
||||
x: number,
|
||||
y: number,
|
||||
absolute: boolean = false
|
||||
) {
|
||||
const animate = core.material.animates[name];
|
||||
const data: AnimateData = {
|
||||
index: 0,
|
||||
start: this.frame,
|
||||
obj: animate,
|
||||
centerX: x,
|
||||
centerY: y,
|
||||
absolute,
|
||||
sound: false
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制动画,动画结束时兑现返回的Promise
|
||||
* @param animate 动画信息
|
||||
* @returns
|
||||
*/
|
||||
draw(animate: AnimateData): Promise<void> {
|
||||
return new Promise(res => {
|
||||
if (animate.absolute) {
|
||||
this.absoluteAnimates.add(animate);
|
||||
} else {
|
||||
this.staticAnimates.add(animate);
|
||||
}
|
||||
animate.onEnd = () => {
|
||||
res();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据动画名称、坐标、定位绘制动画
|
||||
* @param name 动画名称
|
||||
* @param absolute 是否是绝对定位
|
||||
*/
|
||||
drawAnimate(
|
||||
name: AnimationIds,
|
||||
x: number,
|
||||
y: number,
|
||||
absolute: boolean = false
|
||||
) {
|
||||
return this.draw(this.animate(name, x, y, absolute));
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
super.destroy();
|
||||
this.removeTicker(this.delegation);
|
||||
adapter.remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
const adapter = new RenderAdapter<Animate>('animate');
|
||||
adapter.receive('drawAnimate', (item, name, x, y, absolute) => {
|
||||
return item.drawAnimate(name, x, y, absolute);
|
||||
});
|
||||
adapter.receiveGlobal('drawHeroAnimate', name => {
|
||||
const execute: Promise<void>[] = [];
|
||||
LayerGroupAnimate.animateList.forEach(v => {
|
||||
execute.push(v.drawHeroAnimate(name));
|
||||
});
|
||||
return Promise.all(execute);
|
||||
});
|
||||
@ -1,319 +0,0 @@
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { logger } from '@motajs/common';
|
||||
import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render';
|
||||
|
||||
interface BlockCacherEvent {
|
||||
split: [];
|
||||
beforeClear: [index: number];
|
||||
}
|
||||
|
||||
interface BlockData {
|
||||
/** 横向宽度,包括rest的那一个块 */
|
||||
width: number;
|
||||
/** 纵向宽度,包括rest的那一个块 */
|
||||
height: number;
|
||||
/** 横向最后一个块的宽度 */
|
||||
restWidth: number;
|
||||
/** 纵向最后一个块的高度 */
|
||||
restHeight: number;
|
||||
}
|
||||
|
||||
export interface IBlockCacheable {
|
||||
/**
|
||||
* 摧毁这个缓存元素
|
||||
*/
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单分块缓存类,内容包含元素与分块两种,其中元素是最小单元,分块是缓存单元。
|
||||
* 拿楼层举例,假如我将楼层按照13x13划分缓存,那么元素就是每个图块,而分块就是这13x13的缓存分块。
|
||||
* 为方便区分,在相关函数的注释最后,都会有`xx -> yy`的说明,
|
||||
* 其中xx说明传入的数据是元素还是分块的数据,而yy表示其返回值或转换为的值
|
||||
*/
|
||||
export class BlockCacher<
|
||||
T extends IBlockCacheable
|
||||
> extends EventEmitter<BlockCacherEvent> {
|
||||
/** 区域宽度 */
|
||||
width: number;
|
||||
/** 区域高度 */
|
||||
height: number;
|
||||
/** 区域面积 */
|
||||
area: number = 0;
|
||||
/** 分块大小 */
|
||||
blockSize: number;
|
||||
/** 分块信息 */
|
||||
blockData: BlockData = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
restWidth: 0,
|
||||
restHeight: 0
|
||||
};
|
||||
/** 缓存深度,例如填4的时候表示每格包含4个缓存 */
|
||||
cacheDepth: number = 1;
|
||||
|
||||
/** 缓存内容,计算公式为 (x + y * width) * depth + deep */
|
||||
cache: Map<number, T> = new Map();
|
||||
|
||||
constructor(
|
||||
width: number,
|
||||
height: number,
|
||||
size: number,
|
||||
depth: number = 1
|
||||
) {
|
||||
super();
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.blockSize = size;
|
||||
this.cacheDepth = depth;
|
||||
this.split();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置区域大小
|
||||
* @param width 区域宽度
|
||||
* @param height 区域高度
|
||||
*/
|
||||
size(width: number, height: number) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.split();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置分块大小
|
||||
*/
|
||||
setBlockSize(size: number) {
|
||||
this.blockSize = size;
|
||||
this.split();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存深度,设置后会自动将旧缓存移植到新缓存中,最大值为31
|
||||
* @param depth 缓存深度
|
||||
*/
|
||||
setCacheDepth(depth: number) {
|
||||
if (depth > 31) {
|
||||
logger.error(11);
|
||||
return;
|
||||
}
|
||||
const old = this.cache;
|
||||
const before = this.cacheDepth;
|
||||
this.cache = new Map();
|
||||
old.forEach((v, k) => {
|
||||
const index = Math.floor(k / before);
|
||||
const deep = k % before;
|
||||
this.cache.set(index * depth + deep, v);
|
||||
});
|
||||
old.clear();
|
||||
this.cacheDepth = depth;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行分块
|
||||
*/
|
||||
split() {
|
||||
this.blockData = {
|
||||
width: Math.ceil(this.width / this.blockSize),
|
||||
height: Math.ceil(this.height / this.blockSize),
|
||||
restWidth: this.width % this.blockSize,
|
||||
restHeight: this.height % this.blockSize
|
||||
};
|
||||
this.area = this.blockData.width * this.blockData.height;
|
||||
this.emit('split');
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定块的索引(分块->void)
|
||||
* @param index 要清除的缓存索引
|
||||
* @param deep 清除哪些深度的缓存,至多31位二进制数,例如填0b111就是清除前三层的索引
|
||||
*/
|
||||
clearCache(index: number, deep: number) {
|
||||
const depth = this.cacheDepth;
|
||||
for (let i = 0; i < depth; i++) {
|
||||
if (deep & (1 << i)) {
|
||||
const nowIndex = index * this.cacheDepth + i;
|
||||
const item = this.cache.get(nowIndex);
|
||||
item?.destroy();
|
||||
this.cache.delete(nowIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空指定索引的缓存,与 {@link clearCache} 不同的是,这里会直接清空对应索引的缓存,而不是指定分块的缓存(元素->void)
|
||||
*/
|
||||
clearCacheByIndex(index: number) {
|
||||
const item = this.cache.get(index);
|
||||
item?.destroy();
|
||||
this.cache.delete(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有缓存
|
||||
*/
|
||||
clearAllCache() {
|
||||
this.cache.forEach(v => v.destroy());
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分块的横纵坐标获取其索引(分块->分块)
|
||||
*/
|
||||
getIndex(x: number, y: number) {
|
||||
return x + y * this.blockData.width;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据元素位置获取分块索引(注意是单个元素的位置,而不是分块的位置)(元素->分块)
|
||||
*/
|
||||
getIndexByLoc(x: number, y: number) {
|
||||
return this.getIndex(
|
||||
Math.floor(x / this.blockSize),
|
||||
Math.floor(y / this.blockSize)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据块的索引获取其位置(分块->分块)
|
||||
*/
|
||||
getBlockXYByIndex(index: number): LocArr {
|
||||
const width = this.blockData.width;
|
||||
return [index % width, Math.floor(index / width)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个元素位置所在的分块位置(即使它不在任何分块内)(元素->分块)
|
||||
*/
|
||||
getBlockXY(x: number, y: number): LocArr {
|
||||
return [Math.floor(x / this.blockSize), Math.floor(y / this.blockSize)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分块坐标与deep获取一个分块的精确索引(分块->分块)
|
||||
*/
|
||||
getPreciseIndex(x: number, y: number, deep: number) {
|
||||
return (x + y * this.blockSize) * this.cacheDepth + deep;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据元素坐标及deep获取元素所在块的精确索引(元素->分块)
|
||||
*/
|
||||
getPreciseIndexByLoc(x: number, y: number, deep: number) {
|
||||
return this.getPreciseIndex(...this.getBlockXY(x, y), deep);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定元素区域内的缓存(注意坐标是元素坐标,而非分块坐标)(元素->分块)
|
||||
* @param deep 缓存清除深度,默认全部清空
|
||||
* @returns 更新区域内的所有有关分块索引
|
||||
*/
|
||||
updateElementArea(
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
deep: number = 2 ** 31 - 1
|
||||
) {
|
||||
const [bx, by] = this.getBlockXY(x, y);
|
||||
const [ex, ey] = this.getBlockXY(x + width - 1, y + height - 1);
|
||||
|
||||
return this.updateArea(bx, by, ex - bx, ey - by, deep);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定分块区域内的缓存(注意坐标是分块坐标,而非元素坐标)(分块->分块)
|
||||
* @param deep 缓存清除深度,默认全部清空
|
||||
* @returns 更新区域内的所有分块索引
|
||||
*/
|
||||
updateArea(
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
deep: number = 2 ** 31 - 1
|
||||
) {
|
||||
const blocks = this.getIndexOf(x, y, width, height);
|
||||
|
||||
blocks.forEach(v => {
|
||||
this.clearCache(v, deep);
|
||||
});
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 传入分块坐标与范围,获取该区域内包含的所有分块索引(分块->分块)
|
||||
*/
|
||||
getIndexOf(x: number, y: number, width: number, height: number) {
|
||||
const res = new Set<number>();
|
||||
const sx = Math.max(x, 0);
|
||||
const sy = Math.max(y, 0);
|
||||
const ex = Math.min(x + width, this.blockData.width);
|
||||
const ey = Math.min(y + height, this.blockData.height);
|
||||
|
||||
for (let nx = sx; nx <= ex; nx++) {
|
||||
for (let ny = sy; ny <= ey; ny++) {
|
||||
const index = this.getIndex(nx, ny);
|
||||
res.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 传入元素坐标与范围,获取该区域内包含的所有分块索引(元素->分块)
|
||||
*/
|
||||
getIndexOfElement(x: number, y: number, width: number, height: number) {
|
||||
const [bx, by] = this.getBlockXY(x, y);
|
||||
const [ex, ey] = this.getBlockXY(x + width, y + height);
|
||||
return this.getIndexOf(bx, by, ex - bx, ey - by);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分块索引,获取这个分块所在区域的元素矩形范围(左上角横纵坐标及右下角横纵坐标)(分块->元素)
|
||||
* @param block 分块索引
|
||||
*/
|
||||
getRectOfIndex(block: number) {
|
||||
const [x, y] = this.getBlockXYByIndex(block);
|
||||
return this.getRectOfBlockXY(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分块坐标,获取这个分块所在区域的元素矩形范围(左上角横纵坐标及右下角横纵坐标)(分块->元素)
|
||||
* @param x 分块横坐标
|
||||
* @param y 分块纵坐标
|
||||
*/
|
||||
getRectOfBlockXY(x: number, y: number) {
|
||||
return [
|
||||
x * this.blockSize,
|
||||
y * this.blockSize,
|
||||
(x + 1) * this.blockSize,
|
||||
(y + 1) * this.blockSize
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 摧毁这个块缓存
|
||||
*/
|
||||
destroy() {
|
||||
this.clearAllCache();
|
||||
}
|
||||
}
|
||||
|
||||
export interface ICanvasCacheItem extends IBlockCacheable {
|
||||
readonly canvas: MotaOffscreenCanvas2D;
|
||||
symbol: number;
|
||||
}
|
||||
|
||||
export class CanvasCacheItem implements ICanvasCacheItem {
|
||||
constructor(
|
||||
public readonly canvas: MotaOffscreenCanvas2D,
|
||||
public readonly symbol: number,
|
||||
public readonly element: RenderItem<any>
|
||||
) {}
|
||||
|
||||
destroy(): void {
|
||||
this.element.deleteCanvas(this.canvas);
|
||||
}
|
||||
}
|
||||
@ -1,624 +0,0 @@
|
||||
import { Animation, TimingFn, Transition } from 'mutate-animate';
|
||||
import { RenderItem, Transform } from '@motajs/render';
|
||||
import { logger } from '@motajs/common';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
|
||||
export interface ICameraTranslate {
|
||||
readonly type: 'translate';
|
||||
readonly from: Camera;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface ICameraRotate {
|
||||
readonly type: 'rotate';
|
||||
readonly from: Camera;
|
||||
/** 旋转角,单位弧度 */
|
||||
angle: number;
|
||||
}
|
||||
|
||||
export interface ICameraScale {
|
||||
readonly type: 'scale';
|
||||
readonly from: Camera;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
type CameraOperation = ICameraTranslate | ICameraScale | ICameraRotate;
|
||||
|
||||
interface CameraEvent {
|
||||
destroy: [];
|
||||
}
|
||||
|
||||
export class Camera extends EventEmitter<CameraEvent> {
|
||||
/** 当前绑定的渲染元素 */
|
||||
readonly binded: RenderItem;
|
||||
/** 目标变换矩阵,默认与 `this.binded.transform` 同引用 */
|
||||
transform: Transform;
|
||||
|
||||
/** 委托ticker的id */
|
||||
private delegation: number;
|
||||
/** 所有的动画id */
|
||||
private animationIds: Set<number> = new Set();
|
||||
|
||||
/** 是否需要更新视角 */
|
||||
private needUpdate: boolean = false;
|
||||
/** 是否启用摄像机 */
|
||||
private enabled: boolean = true;
|
||||
|
||||
/** 变换操作列表,因为矩阵乘法跟顺序有关,因此需要把各个操作拆分成列表进行 */
|
||||
protected operation: CameraOperation[] = [];
|
||||
|
||||
/** 渲染元素到摄像机的映射 */
|
||||
private static cameraMap: Map<RenderItem, Camera> = new Map();
|
||||
|
||||
/**
|
||||
* 获取一个渲染元素的摄像机,如果不存在则为它创建一个并返回。注意使用`new Camera`创建的摄像机不在此列
|
||||
* @param item 渲染元素
|
||||
*/
|
||||
static for(item: RenderItem) {
|
||||
const camera = this.cameraMap.get(item);
|
||||
if (!camera) {
|
||||
const ca = new Camera(item);
|
||||
this.cameraMap.set(item, ca);
|
||||
return ca;
|
||||
} else {
|
||||
return camera;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(item: RenderItem) {
|
||||
super();
|
||||
|
||||
this.binded = item;
|
||||
|
||||
this.delegation = item.delegateTicker(() => this.tick());
|
||||
this.transform = item.transform;
|
||||
|
||||
item.on('destroy', () => {
|
||||
this.destroy();
|
||||
});
|
||||
|
||||
const ca = Camera.cameraMap.get(item);
|
||||
if (ca && ca.enabled) {
|
||||
logger.warn(22);
|
||||
}
|
||||
}
|
||||
|
||||
private tick = () => {
|
||||
if (!this.needUpdate || !this.enabled) return;
|
||||
const trans = this.transform;
|
||||
trans.reset();
|
||||
for (const o of this.operation) {
|
||||
if (o.type === 'translate') {
|
||||
trans.translate(-o.x, -o.y);
|
||||
} else if (o.type === 'rotate') {
|
||||
trans.rotate(o.angle);
|
||||
} else {
|
||||
trans.scale(o.x, o.y);
|
||||
}
|
||||
}
|
||||
this.binded.update(this.binded);
|
||||
this.needUpdate = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 禁用这个摄像机
|
||||
*/
|
||||
disable() {
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用这个摄像机
|
||||
*/
|
||||
enable() {
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在下一帧进行强制更新
|
||||
*/
|
||||
requestUpdate() {
|
||||
this.needUpdate = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除一个变换操作
|
||||
* @param operation 要移除的操作
|
||||
*/
|
||||
removeOperation(operation: CameraOperation) {
|
||||
const index = this.operation.indexOf(operation);
|
||||
if (index === -1) return;
|
||||
this.operation.splice(index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空变换操作列表
|
||||
*/
|
||||
clearOperation() {
|
||||
this.operation.splice(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个平移操作
|
||||
* @returns 添加的平移变换操作
|
||||
*/
|
||||
addTranslate(): ICameraTranslate {
|
||||
const item: ICameraTranslate = {
|
||||
type: 'translate',
|
||||
x: 0,
|
||||
y: 0,
|
||||
from: this
|
||||
};
|
||||
this.operation.push(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个旋转操作
|
||||
* @returns 添加的旋转变换操作
|
||||
*/
|
||||
addRotate(): ICameraRotate {
|
||||
const item: ICameraRotate = {
|
||||
type: 'rotate',
|
||||
angle: 0,
|
||||
from: this
|
||||
};
|
||||
this.operation.push(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个放缩操作
|
||||
* @returns 添加的放缩变换操作
|
||||
*/
|
||||
addScale(): ICameraScale {
|
||||
const item: ICameraScale = {
|
||||
type: 'scale',
|
||||
x: 1,
|
||||
y: 1,
|
||||
from: this
|
||||
};
|
||||
this.operation.push(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 施加动画
|
||||
* @param time 动画时长
|
||||
* @param update 每帧的更新函数
|
||||
*/
|
||||
applyAnimation(time: number, update: () => void) {
|
||||
const delegation = this.binded.delegateTicker(
|
||||
() => {
|
||||
update();
|
||||
this.needUpdate = true;
|
||||
},
|
||||
time,
|
||||
() => {
|
||||
update();
|
||||
this.needUpdate = true;
|
||||
this.animationIds.delete(delegation);
|
||||
}
|
||||
);
|
||||
this.animationIds.add(delegation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为一个平移操作实施动画
|
||||
* @param operation 平移操作
|
||||
* @param animate 动画实例
|
||||
* @param time 动画时长
|
||||
*/
|
||||
applyTranslateAnimation(
|
||||
operation: ICameraTranslate,
|
||||
animate: Animation,
|
||||
time: number
|
||||
) {
|
||||
if (operation.from !== this) {
|
||||
logger.warn(20);
|
||||
return;
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
operation.x = animate.x;
|
||||
operation.y = animate.y;
|
||||
};
|
||||
|
||||
this.applyAnimation(time, update);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为一个旋转操作实施动画
|
||||
* @param operation 旋转操作
|
||||
* @param animate 动画实例
|
||||
* @param time 动画时长
|
||||
*/
|
||||
applyRotateAnimation(
|
||||
operation: ICameraRotate,
|
||||
animate: Animation,
|
||||
time: number
|
||||
) {
|
||||
if (operation.from !== this) {
|
||||
logger.warn(20);
|
||||
return;
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
operation.angle = animate.angle;
|
||||
};
|
||||
|
||||
this.applyAnimation(time, update);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为一个缩放操作实施动画
|
||||
* @param operation 缩放操作
|
||||
* @param animate 动画实例
|
||||
* @param time 动画时长
|
||||
*/
|
||||
applyScaleAnimation(
|
||||
operation: ICameraScale,
|
||||
animate: Animation,
|
||||
time: number
|
||||
) {
|
||||
if (operation.from !== this) {
|
||||
logger.warn(20);
|
||||
return;
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
operation.x = animate.size;
|
||||
operation.y = animate.size;
|
||||
};
|
||||
|
||||
this.applyAnimation(time, update);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为一个平移操作实施渐变,使用渐变的 x,y 值,即`transition.value.x`与`transition.value.y`
|
||||
* @param operation 平移操作
|
||||
* @param animate 渐变实例
|
||||
* @param time 渐变时长
|
||||
*/
|
||||
applyTranslateTransition(
|
||||
operation: ICameraTranslate,
|
||||
animate: Transition,
|
||||
time: number
|
||||
) {
|
||||
if (operation.from !== this) {
|
||||
logger.warn(21);
|
||||
return;
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
operation.x = animate.value.x;
|
||||
operation.y = animate.value.y;
|
||||
};
|
||||
|
||||
this.applyAnimation(time, update);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为一个旋转操作实施渐变,使用渐变的 angle 值,即`transition.value.angle`
|
||||
* @param operation 旋转操作
|
||||
* @param animate 渐变实例
|
||||
* @param time 渐变时长
|
||||
*/
|
||||
applyRotateTransition(
|
||||
operation: ICameraRotate,
|
||||
animate: Transition,
|
||||
time: number
|
||||
) {
|
||||
if (operation.from !== this) {
|
||||
logger.warn(21);
|
||||
return;
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
operation.angle = animate.value.angle;
|
||||
};
|
||||
|
||||
this.applyAnimation(time, update);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为一个缩放操作实施渐变,使用渐变的 size 值,即`transition.value.size`
|
||||
* @param operation 缩放操作
|
||||
* @param animate 渐变实例
|
||||
* @param time 渐变时长
|
||||
*/
|
||||
applyScaleTransition(
|
||||
operation: ICameraScale,
|
||||
animate: Transition,
|
||||
time: number
|
||||
) {
|
||||
if (operation.from !== this) {
|
||||
logger.warn(21);
|
||||
return;
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
operation.x = animate.value.size;
|
||||
operation.y = animate.value.size;
|
||||
};
|
||||
|
||||
this.applyAnimation(time, update);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止所有动画
|
||||
*/
|
||||
stopAllAnimates() {
|
||||
this.animationIds.forEach(v => this.binded.removeTicker(v));
|
||||
}
|
||||
|
||||
/**
|
||||
* 摧毁这个摄像机,当绑定元素被摧毁之后摄像机会一并摧毁,如果这个摄像机不使用了,一定要将它摧毁
|
||||
*/
|
||||
destroy() {
|
||||
this.binded.removeTicker(this.delegation);
|
||||
this.animationIds.forEach(v => this.binded.removeTicker(v));
|
||||
Camera.cameraMap.delete(this.binded);
|
||||
this.emit('destroy');
|
||||
}
|
||||
}
|
||||
|
||||
interface CameraAnimationBase {
|
||||
type: string;
|
||||
time: number;
|
||||
start: number;
|
||||
}
|
||||
|
||||
export interface TranslateAnimation extends CameraAnimationBase {
|
||||
type: 'translate';
|
||||
timing: TimingFn;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface TranslateAsAnimation extends CameraAnimationBase {
|
||||
type: 'translateAs';
|
||||
timing: TimingFn<2>;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface RotateAnimation extends CameraAnimationBase {
|
||||
type: 'rotate';
|
||||
timing: TimingFn;
|
||||
angle: number;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface ScaleAnimation extends CameraAnimationBase {
|
||||
type: 'scale';
|
||||
timing: TimingFn;
|
||||
scale: number;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export type CameraAnimationData =
|
||||
| TranslateAnimation
|
||||
| TranslateAsAnimation
|
||||
| RotateAnimation
|
||||
| ScaleAnimation;
|
||||
|
||||
export interface CameraAnimationExecution {
|
||||
data: CameraAnimationData[];
|
||||
animation: Animation;
|
||||
}
|
||||
|
||||
interface CameraAnimationEvent {
|
||||
animate: [
|
||||
operation: CameraOperation,
|
||||
execution: CameraAnimationExecution,
|
||||
item: CameraAnimationData
|
||||
];
|
||||
}
|
||||
|
||||
export class CameraAnimation extends EventEmitter<CameraAnimationEvent> {
|
||||
camera: Camera;
|
||||
|
||||
/** 动画开始时刻 */
|
||||
private startTime: number = 0;
|
||||
/** 动画结束时刻 */
|
||||
private endTime: number = 0;
|
||||
/** 委托ticker的id */
|
||||
private delegation: number;
|
||||
/** 动画是否开始 */
|
||||
private started: boolean = false;
|
||||
|
||||
/** 每个摄像机操作的动画映射 */
|
||||
private animateMap: Map<CameraOperation, CameraAnimationExecution> =
|
||||
new Map();
|
||||
|
||||
constructor(camera: Camera) {
|
||||
super();
|
||||
|
||||
this.camera = camera;
|
||||
this.delegation = camera.binded.delegateTicker(this.tick);
|
||||
}
|
||||
|
||||
private tick = () => {
|
||||
if (!this.started) return;
|
||||
const now = Date.now();
|
||||
const time = now - this.startTime;
|
||||
if (now - this.startTime > this.endTime + 50) {
|
||||
this.destroy();
|
||||
return;
|
||||
}
|
||||
this.animateMap.forEach((exe, ope) => {
|
||||
const data = exe.data;
|
||||
if (data.length === 0) return;
|
||||
const item = data[0];
|
||||
if (item.start < time) {
|
||||
this.executeAnimate(exe, item);
|
||||
data.shift();
|
||||
this.emit('animate', ope, exe, item);
|
||||
}
|
||||
});
|
||||
this.camera.requestUpdate();
|
||||
};
|
||||
|
||||
private executeAnimate(
|
||||
execution: CameraAnimationExecution,
|
||||
animate: CameraAnimationData
|
||||
) {
|
||||
if (animate.type === 'translateAs') {
|
||||
const ani = this.ensureAnimate(execution);
|
||||
ani.time(animate.time).moveAs(animate.timing);
|
||||
} else if (animate.type === 'translate') {
|
||||
const ani = this.ensureAnimate(execution);
|
||||
const { x, y, time, timing } = animate;
|
||||
ani.mode(timing).time(time).move(x, y);
|
||||
} else if (animate.type === 'rotate') {
|
||||
const ani = this.ensureAnimate(execution);
|
||||
const { angle, time, timing } = animate;
|
||||
ani.mode(timing).time(time).rotate(angle);
|
||||
} else {
|
||||
const ani = this.ensureAnimate(execution);
|
||||
const { scale, time, timing } = animate;
|
||||
ani.mode(timing).time(time).scale(scale);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureAnimate(execution: CameraAnimationExecution) {
|
||||
if (execution.animation) return execution.animation;
|
||||
const ani = new Animation();
|
||||
execution.animation = ani;
|
||||
return ani;
|
||||
}
|
||||
|
||||
private ensureOperation(operation: CameraOperation) {
|
||||
if (!this.animateMap.has(operation)) {
|
||||
const data: CameraAnimationExecution = {
|
||||
data: [],
|
||||
animation: new Animation()
|
||||
};
|
||||
this.animateMap.set(operation, data);
|
||||
return data;
|
||||
} else {
|
||||
return this.animateMap.get(operation)!;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个平移动画
|
||||
* @param operation 摄像机操作对象
|
||||
* @param x 目标横坐标
|
||||
* @param y 目标纵坐标
|
||||
* @param time 动画时长
|
||||
* @param start 动画开始时间
|
||||
* @param timing 动画的缓动函数
|
||||
*/
|
||||
translate(
|
||||
operation: ICameraTranslate,
|
||||
x: number,
|
||||
y: number,
|
||||
time: number,
|
||||
start: number,
|
||||
timing: TimingFn
|
||||
) {
|
||||
const exe = this.ensureOperation(operation);
|
||||
const data: TranslateAnimation = {
|
||||
type: 'translate',
|
||||
timing,
|
||||
x: x * 32,
|
||||
y: y * 32,
|
||||
time,
|
||||
start
|
||||
};
|
||||
exe.data.push(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个旋转动画
|
||||
* @param operation 摄像机操作
|
||||
* @param angle 目标旋转弧度,单位弧度
|
||||
* @param time 动画时长
|
||||
* @param start 动画开始时间
|
||||
* @param timing 动画的缓动函数
|
||||
*/
|
||||
rotate(
|
||||
operation: ICameraRotate,
|
||||
angle: number,
|
||||
time: number,
|
||||
start: number,
|
||||
timing: TimingFn
|
||||
) {
|
||||
const exe = this.ensureOperation(operation);
|
||||
const data: RotateAnimation = {
|
||||
type: 'rotate',
|
||||
timing,
|
||||
angle,
|
||||
time,
|
||||
start
|
||||
};
|
||||
exe.data.push(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个缩放动画
|
||||
* @param operation 摄像机操作
|
||||
* @param scale 目标缩放倍率
|
||||
* @param time 动画时长
|
||||
* @param start 动画开始时间
|
||||
* @param timing 动画的缓动函数
|
||||
*/
|
||||
scale(
|
||||
operation: ICameraScale,
|
||||
scale: number,
|
||||
time: number,
|
||||
start: number,
|
||||
timing: TimingFn
|
||||
) {
|
||||
const exe = this.ensureOperation(operation);
|
||||
const data: ScaleAnimation = {
|
||||
type: 'scale',
|
||||
timing,
|
||||
scale,
|
||||
time,
|
||||
start
|
||||
};
|
||||
exe.data.push(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始执行这个动画
|
||||
*/
|
||||
start() {
|
||||
if (this.started) return;
|
||||
this.startTime = Date.now();
|
||||
this.started = true;
|
||||
let endTime = 0;
|
||||
this.animateMap.forEach((exe, ope) => {
|
||||
const data = exe.data;
|
||||
data.sort((a, b) => a.start - b.start);
|
||||
const end = data.at(-1);
|
||||
if (!end) return;
|
||||
const t = end.start + end.time;
|
||||
if (t > endTime) endTime = t;
|
||||
const cam = this.camera;
|
||||
|
||||
if (ope.type === 'translate') {
|
||||
cam.applyTranslateAnimation(ope, exe.animation, t + 100);
|
||||
} else if (ope.type === 'rotate') {
|
||||
cam.applyRotateAnimation(ope, exe.animation, t + 100);
|
||||
} else {
|
||||
cam.applyScaleAnimation(ope, exe.animation, t + 100);
|
||||
}
|
||||
});
|
||||
this.endTime = endTime + this.startTime;
|
||||
this.tick();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.camera.binded.removeTicker(this.delegation);
|
||||
this.camera.stopAllAnimates();
|
||||
this.animateMap.forEach(v => {
|
||||
v.animation.ticker.destroy();
|
||||
});
|
||||
this.animateMap.clear();
|
||||
}
|
||||
}
|
||||
@ -1,543 +0,0 @@
|
||||
import {
|
||||
ERenderItemEvent,
|
||||
RenderItem,
|
||||
MotaOffscreenCanvas2D,
|
||||
Transform,
|
||||
transformCanvas
|
||||
} from '@motajs/render';
|
||||
import { logger } from '@motajs/common';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { IDamageEnemy, IEnemyCollection, MapDamage } from '@motajs/types';
|
||||
import { BlockCacher, ICanvasCacheItem, CanvasCacheItem } from './block';
|
||||
import {
|
||||
ILayerGroupRenderExtends,
|
||||
LayerGroupFloorBinder,
|
||||
LayerGroup,
|
||||
Layer,
|
||||
calNeedRenderOf
|
||||
} from './layer';
|
||||
import { MAP_BLOCK_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../shared';
|
||||
|
||||
/**
|
||||
* 根据伤害大小获取颜色
|
||||
* @param damage 伤害大小
|
||||
*/
|
||||
export function getDamageColor(damage: number): string {
|
||||
if (typeof damage !== 'number') return '#f00';
|
||||
if (damage === 0) return '#2f2';
|
||||
if (damage < 0) return '#7f7';
|
||||
if (damage < core.status.hero.hp / 3) return '#fff';
|
||||
if (damage < (core.status.hero.hp * 2) / 3) return '#ff4';
|
||||
if (damage < core.status.hero.hp) return '#f93';
|
||||
return '#f22';
|
||||
}
|
||||
|
||||
interface EFloorDamageEvent {
|
||||
update: [floor: FloorIds];
|
||||
}
|
||||
|
||||
export class FloorDamageExtends
|
||||
extends EventEmitter<EFloorDamageEvent>
|
||||
implements ILayerGroupRenderExtends
|
||||
{
|
||||
id: string = 'floor-damage';
|
||||
|
||||
floorBinder!: LayerGroupFloorBinder;
|
||||
group!: LayerGroup;
|
||||
sprite!: Damage;
|
||||
|
||||
/**
|
||||
* 立刻刷新伤害渲染
|
||||
*/
|
||||
update(floor: FloorIds) {
|
||||
if (!this.sprite || !floor) return;
|
||||
const map = core.status.maps[floor];
|
||||
this.sprite.setMapSize(map.width, map.height);
|
||||
const { ensureFloorDamage } = Mota.require('@user/data-state');
|
||||
ensureFloorDamage(floor);
|
||||
const enemy = core.status.maps[floor].enemy;
|
||||
|
||||
this.sprite.updateCollection(enemy);
|
||||
this.emit('update', floor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建显伤层
|
||||
*/
|
||||
private create() {
|
||||
if (this.sprite) return;
|
||||
const sprite = new Damage();
|
||||
sprite.setZIndex(80);
|
||||
this.group.appendChild(sprite);
|
||||
this.sprite = sprite;
|
||||
}
|
||||
|
||||
private onUpdate = (floor: FloorIds) => {
|
||||
if (!this.floorBinder.bindThisFloor) {
|
||||
const { ensureFloorDamage } = Mota.require('@user/data-state');
|
||||
ensureFloorDamage(floor);
|
||||
core.status.maps[floor].enemy.calRealAttribute();
|
||||
}
|
||||
this.update(floor);
|
||||
};
|
||||
|
||||
// private onSetBlock = (x: number, y: number, floor: FloorIds) => {
|
||||
// this.sprite.enemy?.once('extract', () => {
|
||||
// if (floor !== this.sprite.enemy?.floorId) return;
|
||||
// this.sprite.updateBlocks();
|
||||
// });
|
||||
// if (!this.floorBinder.bindThisFloor) {
|
||||
// this.sprite.enemy?.extract();
|
||||
// }
|
||||
// };
|
||||
|
||||
/**
|
||||
* 进行楼层更新监听
|
||||
*/
|
||||
private listen() {
|
||||
this.floorBinder.on('update', this.onUpdate);
|
||||
// this.floorBinder.on('setBlock', this.onSetBlock);
|
||||
}
|
||||
|
||||
awake(group: LayerGroup): void {
|
||||
const ex = group.getExtends('floor-binder');
|
||||
if (ex instanceof LayerGroupFloorBinder) {
|
||||
this.floorBinder = ex;
|
||||
this.group = group;
|
||||
this.create();
|
||||
this.listen();
|
||||
} else {
|
||||
logger.warn(17);
|
||||
group.removeExtends('floor-damage');
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(_group: LayerGroup): void {
|
||||
this.floorBinder.off('update', this.onUpdate);
|
||||
// this.floorBinder.off('setBlock', this.onSetBlock);
|
||||
}
|
||||
}
|
||||
|
||||
export interface DamageRenderable {
|
||||
x: number;
|
||||
y: number;
|
||||
align: CanvasTextAlign;
|
||||
baseline: CanvasTextBaseline;
|
||||
text: string;
|
||||
color: CanvasStyle;
|
||||
font?: string;
|
||||
stroke?: CanvasStyle;
|
||||
strokeWidth?: number;
|
||||
}
|
||||
|
||||
export interface EDamageEvent extends ERenderItemEvent {
|
||||
setMapSize: [width: number, height: number];
|
||||
beforeDamageRender: [need: Set<number>, transform: Transform];
|
||||
updateBlocks: [blocks: Set<number>];
|
||||
dirtyUpdate: [block: number];
|
||||
}
|
||||
|
||||
export class Damage extends RenderItem<EDamageEvent> {
|
||||
mapWidth: number = 0;
|
||||
mapHeight: number = 0;
|
||||
|
||||
block: BlockCacher<ICanvasCacheItem>;
|
||||
/** 键表示分块索引,值表示在这个分块上的渲染信息(当然实际渲染位置可以不在这个分块上) */
|
||||
renderable: Map<number, Set<DamageRenderable>> = new Map();
|
||||
|
||||
/** 当前渲染怪物列表 */
|
||||
enemy?: IEnemyCollection;
|
||||
/** 每个分块中包含的怪物集合 */
|
||||
blockData: Map<number, Map<number, IDamageEnemy>> = new Map();
|
||||
/** 单元格大小 */
|
||||
cellSize: number = 32;
|
||||
|
||||
/** 默认伤害字体 */
|
||||
font: string = '300 9px Verdana';
|
||||
/** 默认描边样式,当伤害文字不存在描边属性时会使用此属性 */
|
||||
strokeStyle: CanvasStyle = '#000';
|
||||
/** 默认描边宽度 */
|
||||
strokeWidth: number = 2;
|
||||
|
||||
/** 要懒更新的所有分块 */
|
||||
private dirtyBlocks: Set<number> = new Set();
|
||||
|
||||
constructor() {
|
||||
super('absolute', false, true);
|
||||
|
||||
this.block = new BlockCacher(0, 0, MAP_BLOCK_WIDTH, 1);
|
||||
this.type = 'absolute';
|
||||
this.size(MAP_WIDTH, MAP_HEIGHT);
|
||||
this.setHD(true);
|
||||
this.setAntiAliasing(true);
|
||||
}
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
transform: Transform
|
||||
): void {
|
||||
this.renderDamage(canvas, transform);
|
||||
}
|
||||
|
||||
private onExtract = () => {
|
||||
if (this.enemy) this.updateCollection(this.enemy);
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置地图大小,后面应紧跟更新怪物列表
|
||||
*/
|
||||
setMapSize(width: number, height: number) {
|
||||
this.mapWidth = width;
|
||||
this.mapHeight = height;
|
||||
this.enemy = void 0;
|
||||
this.blockData.clear();
|
||||
this.renderable.clear();
|
||||
this.block.size(width, height);
|
||||
|
||||
// 预留blockData
|
||||
const w = this.block.blockData.width;
|
||||
const h = this.block.blockData.height;
|
||||
const num = w * h;
|
||||
for (let i = 0; i < num; i++) {
|
||||
this.blockData.set(i, new Map());
|
||||
this.renderable.set(i, new Set());
|
||||
this.dirtyBlocks.add(i);
|
||||
}
|
||||
|
||||
this.emit('setMapSize', width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置每个图块的大小
|
||||
*/
|
||||
setCellSize(size: number) {
|
||||
this.cellSize = size;
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新怪物列表。更新后,{@link Damage.enemy} 会丢失原来的怪物列表引用,换为传入的列表引用
|
||||
* @param enemy 怪物列表
|
||||
*/
|
||||
updateCollection(enemy: IEnemyCollection) {
|
||||
if (this.enemy !== enemy) {
|
||||
this.enemy?.off('calculated', this.onExtract);
|
||||
enemy.on('calculated', this.onExtract);
|
||||
}
|
||||
this.enemy = enemy;
|
||||
this.blockData.forEach(v => v.clear());
|
||||
this.renderable.forEach(v => v.clear());
|
||||
this.block.clearAllCache();
|
||||
const w = this.block.blockData.width;
|
||||
const h = this.block.blockData.height;
|
||||
const num = w * h;
|
||||
for (let i = 0; i < num; i++) {
|
||||
this.dirtyBlocks.add(i);
|
||||
}
|
||||
|
||||
enemy.list.forEach(v => {
|
||||
if (isNil(v.x) || isNil(v.y)) return;
|
||||
const index = this.block.getIndexByLoc(v.x, v.y);
|
||||
this.blockData.get(index)?.set(v.y * this.mapWidth + v.x, v);
|
||||
});
|
||||
// this.updateBlocks();
|
||||
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定矩形区域内的渲染信息
|
||||
* @param x 左上角横坐标
|
||||
* @param y 左上角纵坐标
|
||||
* @param width 宽度
|
||||
* @param height 高度
|
||||
*/
|
||||
updateRenderable(x: number, y: number, width: number, height: number) {
|
||||
this.updateBlocks(this.block.updateElementArea(x, y, width, height));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定分块
|
||||
* @param blocks 要更新的分块集合
|
||||
* @param map 是否更新地图伤害
|
||||
*/
|
||||
updateBlocks(blocks?: Set<number>) {
|
||||
if (blocks) {
|
||||
blocks.forEach(v => this.dirtyBlocks.add(v));
|
||||
this.emit('updateBlocks', blocks);
|
||||
} else {
|
||||
this.blockData.forEach((_v, i) => {
|
||||
this.dirtyBlocks.add(i);
|
||||
});
|
||||
this.emit('updateBlocks', new Set(this.blockData.keys()));
|
||||
}
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定位置的怪物信息
|
||||
*/
|
||||
updateEnemyOn(x: number, y: number) {
|
||||
const enemy = this.enemy?.get(x, y);
|
||||
const block = this.block.getIndexByLoc(x, y);
|
||||
const data = this.blockData.get(block);
|
||||
const index = x + y * this.mapWidth;
|
||||
if (!data) return;
|
||||
if (!enemy) {
|
||||
data.delete(index);
|
||||
} else {
|
||||
data.set(index, enemy);
|
||||
}
|
||||
|
||||
this.update(this);
|
||||
|
||||
// 渲染懒更新,优化性能表现
|
||||
this.dirtyBlocks.add(block);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单个分块
|
||||
* @param block 更新的分块
|
||||
* @param map 是否更新地图伤害
|
||||
*/
|
||||
private updateBlock(block: number, map: boolean = true) {
|
||||
const data = this.blockData.get(block);
|
||||
if (!data) return;
|
||||
|
||||
this.block.clearCache(block, 1);
|
||||
const renderable = this.renderable.get(block)!;
|
||||
|
||||
renderable.clear();
|
||||
data.forEach(v => this.extract(v, renderable));
|
||||
if (map) this.extractMapDamage(block, renderable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将怪物解析为renderable的伤害
|
||||
* @param enemy 怪物
|
||||
* @param block 怪物所属分块
|
||||
*/
|
||||
private extract(enemy: IDamageEnemy, block: Set<DamageRenderable>) {
|
||||
if (enemy.progress !== 4) return;
|
||||
const x = enemy.x!;
|
||||
const y = enemy.y!;
|
||||
const { damage } = enemy.calDamage();
|
||||
const cri = enemy.calCritical(1)[0]?.atkDelta ?? Infinity;
|
||||
|
||||
const dam1: DamageRenderable = {
|
||||
align: 'left',
|
||||
baseline: 'alphabetic',
|
||||
text: isFinite(damage) ? core.formatBigNumber(damage, true) : '???',
|
||||
color: getDamageColor(damage),
|
||||
x: x * this.cellSize + 1,
|
||||
y: y * this.cellSize + this.cellSize - 1
|
||||
};
|
||||
const dam2: DamageRenderable = {
|
||||
align: 'left',
|
||||
baseline: 'alphabetic',
|
||||
text: isFinite(cri) ? core.formatBigNumber(cri, true) : '?',
|
||||
color: '#fff',
|
||||
x: x * this.cellSize + 1,
|
||||
y: y * this.cellSize + this.cellSize - 11
|
||||
};
|
||||
block.add(dam1).add(dam2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析指定分块的地图伤害
|
||||
* @param block 分块索引
|
||||
*/
|
||||
private extractMapDamage(block: number, renderable: Set<DamageRenderable>) {
|
||||
if (!this.enemy) return;
|
||||
const damage = this.enemy.mapDamage;
|
||||
const [sx, sy, ex, ey] = this.block.getRectOfIndex(block);
|
||||
for (let x = sx; x < ex; x++) {
|
||||
for (let y = sy; y < ey; y++) {
|
||||
const loc = `${x},${y}`;
|
||||
const dam = damage[loc];
|
||||
if (!dam) continue;
|
||||
this.pushMapDamage(x, y, renderable, dam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析所有地图伤害
|
||||
*/
|
||||
private extractAllMapDamage() {
|
||||
// todo: 测试性能,这样真的会更快吗?或许能更好的优化?或者是根本不需要这个函数?
|
||||
if (!this.enemy) return;
|
||||
for (const [loc, enemy] of Object.entries(this.enemy.mapDamage)) {
|
||||
const [sx, sy] = loc.split(',');
|
||||
const x = Number(sx);
|
||||
const y = Number(sy);
|
||||
const block = this.renderable.get(this.block.getIndexByLoc(x, y))!;
|
||||
this.pushMapDamage(x, y, block, enemy);
|
||||
}
|
||||
}
|
||||
|
||||
private pushMapDamage(
|
||||
x: number,
|
||||
y: number,
|
||||
block: Set<DamageRenderable>,
|
||||
dam: MapDamage
|
||||
) {
|
||||
// todo: 这个应当可以自定义,通过地图伤害注册实现
|
||||
let text = '';
|
||||
const color = '#fa3';
|
||||
const font = '300 9px Verdana';
|
||||
if (dam.damage > 0) {
|
||||
text = core.formatBigNumber(dam.damage, true);
|
||||
} else if (dam.ambush) {
|
||||
text = `!`;
|
||||
} else if (dam.repulse) {
|
||||
text = '阻';
|
||||
}
|
||||
|
||||
const mapDam: DamageRenderable = {
|
||||
align: 'center',
|
||||
baseline: 'middle',
|
||||
text,
|
||||
color,
|
||||
font,
|
||||
x: x * this.cellSize + this.cellSize / 2,
|
||||
y: y * this.cellSize + this.cellSize / 2
|
||||
};
|
||||
block.add(mapDam);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算需要渲染哪些块
|
||||
*/
|
||||
calNeedRender(transform: Transform) {
|
||||
if (this.parent instanceof LayerGroup) {
|
||||
// 如果处于地图组中,每个地图的渲染区域应该是一样的,因此可以缓存优化
|
||||
return this.parent.cacheNeedRender(transform, this.block);
|
||||
} else if (this.parent instanceof Layer) {
|
||||
// 如果是地图的子元素,直接调用Layer的计算函数
|
||||
return this.parent.calNeedRender(transform);
|
||||
} else {
|
||||
return calNeedRenderOf(transform, this.cellSize, this.block);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染伤害层
|
||||
* @param transform 变换矩阵
|
||||
*/
|
||||
renderDamage(canvas: MotaOffscreenCanvas2D, transform: Transform) {
|
||||
// console.time('damage');
|
||||
const { ctx } = canvas;
|
||||
ctx.save();
|
||||
transformCanvas(canvas, transform);
|
||||
|
||||
const render = this.calNeedRender(transform);
|
||||
const block = this.block;
|
||||
const cell = this.cellSize;
|
||||
const size = cell * block.blockSize;
|
||||
|
||||
this.emit('beforeDamageRender', render, transform);
|
||||
|
||||
render.forEach(v => {
|
||||
const [x, y] = block.getBlockXYByIndex(v);
|
||||
const bx = x * block.blockSize;
|
||||
const by = y * block.blockSize;
|
||||
const px = bx * cell;
|
||||
const py = by * cell;
|
||||
|
||||
// todo: 是否真的需要缓存
|
||||
// 检查有没有缓存
|
||||
const cache = block.cache.get(v);
|
||||
if (cache && cache.symbol === cache.canvas.symbol) {
|
||||
ctx.drawImage(cache.canvas.canvas, px, py, size, size);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dirtyBlocks.has(v)) {
|
||||
this.updateBlock(v, true);
|
||||
}
|
||||
this.emit('dirtyUpdate', v);
|
||||
|
||||
// 否则依次渲染并写入缓存
|
||||
const temp = block.cache.get(v)?.canvas ?? this.requireCanvas();
|
||||
temp.clear();
|
||||
temp.setHD(true);
|
||||
temp.setAntiAliasing(true);
|
||||
temp.size(size, size);
|
||||
const { ctx: ct } = temp;
|
||||
|
||||
ct.translate(-px, -py);
|
||||
ct.lineJoin = 'round';
|
||||
ct.lineCap = 'round';
|
||||
|
||||
const render = this.renderable.get(v);
|
||||
|
||||
render?.forEach(v => {
|
||||
if (!v) return;
|
||||
ct.fillStyle = v.color;
|
||||
ct.textAlign = v.align;
|
||||
ct.textBaseline = v.baseline;
|
||||
ct.font = v.font ?? this.font;
|
||||
ct.strokeStyle = v.stroke ?? this.strokeStyle;
|
||||
ct.lineWidth = v.strokeWidth ?? this.strokeWidth;
|
||||
|
||||
ct.strokeText(v.text, v.x, v.y);
|
||||
ct.fillText(v.text, v.x, v.y);
|
||||
});
|
||||
|
||||
ctx.drawImage(temp.canvas, px, py, size, size);
|
||||
block.cache.set(v, new CanvasCacheItem(temp, temp.symbol, this));
|
||||
});
|
||||
ctx.restore();
|
||||
// console.timeEnd('damage');
|
||||
}
|
||||
|
||||
protected handleProps(
|
||||
key: string,
|
||||
_prevValue: any,
|
||||
nextValue: any
|
||||
): boolean {
|
||||
switch (key) {
|
||||
case 'mapWidth':
|
||||
if (!this.assertType(nextValue, 'number', key)) return false;
|
||||
this.setMapSize(nextValue, this.mapHeight);
|
||||
return true;
|
||||
case 'mapHeight':
|
||||
if (!this.assertType(nextValue, 'number', key)) return false;
|
||||
this.setMapSize(this.mapWidth, nextValue);
|
||||
return true;
|
||||
case 'cellSize':
|
||||
if (!this.assertType(nextValue, 'number', key)) return false;
|
||||
this.setCellSize(nextValue);
|
||||
return true;
|
||||
case 'enemy':
|
||||
if (!this.assertType(nextValue, 'object', key)) return false;
|
||||
this.updateCollection(nextValue);
|
||||
return true;
|
||||
case 'font':
|
||||
if (!this.assertType(nextValue, 'string', key)) return false;
|
||||
this.font = nextValue;
|
||||
this.update();
|
||||
return true;
|
||||
case 'strokeStyle':
|
||||
this.strokeStyle = nextValue;
|
||||
this.update();
|
||||
return true;
|
||||
case 'strokeWidth':
|
||||
if (!this.assertType(nextValue, 'number', key)) return false;
|
||||
this.strokeWidth = nextValue;
|
||||
this.update();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
super.destroy();
|
||||
this.block.destroy();
|
||||
this.enemy?.off('extract', this.onExtract);
|
||||
}
|
||||
}
|
||||
|
||||
// const adapter = new RenderAdapter<Damage>('damage');
|
||||
@ -1,54 +0,0 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { RenderItem } from '@motajs/render';
|
||||
|
||||
export interface IAnimateFrame {
|
||||
updateFrameAnimate(frame: number, time: number): void;
|
||||
}
|
||||
|
||||
interface RenderEvent {
|
||||
animateFrame: [frame: number, time: number];
|
||||
}
|
||||
|
||||
class RenderEmits extends EventEmitter<RenderEvent> {
|
||||
private framer: Set<IAnimateFrame> = new Set();
|
||||
|
||||
/**
|
||||
* 添加一个可更新帧动画的对象
|
||||
*/
|
||||
addFramer(framer: IAnimateFrame) {
|
||||
this.framer.add(framer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除一个可更新帧动画的对象
|
||||
*/
|
||||
removeFramer(framer: IAnimateFrame) {
|
||||
this.framer.delete(framer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有帧动画
|
||||
* @param frame 帧数
|
||||
* @param time 帧动画时刻
|
||||
*/
|
||||
emitAnimateFrame(frame: number, time: number) {
|
||||
this.framer.forEach(v => v.updateFrameAnimate(frame, time));
|
||||
this.emit('animateFrame', frame, time);
|
||||
}
|
||||
}
|
||||
|
||||
export const renderEmits = new RenderEmits();
|
||||
|
||||
export function createFrame() {
|
||||
Mota.require('@user/data-base').hook.once('reset', () => {
|
||||
let lastTime = 0;
|
||||
RenderItem.ticker.add(time => {
|
||||
if (!core.isPlaying()) return;
|
||||
if (time - lastTime > core.values.animateSpeed) {
|
||||
RenderItem.animatedFrame++;
|
||||
lastTime = time;
|
||||
renderEmits.emitAnimateFrame(RenderItem.animatedFrame, time);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -1,480 +0,0 @@
|
||||
import { SizedCanvasImageSource, RenderAdapter } from '@motajs/render';
|
||||
import { logger } from '@motajs/common';
|
||||
import { ILayerRenderExtends, Layer, LayerMovingRenderable } from './layer';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { texture } from './cache';
|
||||
import { TimingFn } from 'mutate-animate';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
// type HeroMovingStatus = 'stop' | 'moving' | 'moving-as';
|
||||
// export const enum HeroMovingStatus {}
|
||||
|
||||
interface HeroRenderEvent {
|
||||
stepEnd: [];
|
||||
moveTick: [x: number, y: number];
|
||||
append: [renderable: LayerMovingRenderable[]];
|
||||
}
|
||||
|
||||
export class HeroRenderer
|
||||
extends EventEmitter<HeroRenderEvent>
|
||||
implements ILayerRenderExtends
|
||||
{
|
||||
id: string = 'floor-hero';
|
||||
|
||||
/** 勇士的图片资源 */
|
||||
image?: SizedCanvasImageSource;
|
||||
cellWidth?: number;
|
||||
cellHeight?: number;
|
||||
|
||||
/** 勇士的渲染信息 */
|
||||
renderable?: LayerMovingRenderable;
|
||||
layer!: Layer;
|
||||
|
||||
/** 当前移动帧数 */
|
||||
movingFrame: number = 0;
|
||||
/** 是否正在移动 */
|
||||
moving: boolean = false;
|
||||
/** 是否正在播放动画,与移动分开以实现原地踏步 */
|
||||
animate: boolean = false;
|
||||
|
||||
/** 勇士移动速度 */
|
||||
speed: number = 100;
|
||||
/** 勇士移动的真正速度,经过录像修正 */
|
||||
realSpeed: number = 100;
|
||||
|
||||
/** 当前的移动方向 */
|
||||
moveDir: Dir2 = 'down';
|
||||
/** 当前移动的勇士显示方向 */
|
||||
showDir: Dir = 'down';
|
||||
/** 帧动画是否反向播放,例如后退时就应该设为true */
|
||||
animateReverse: boolean = false;
|
||||
|
||||
/** 勇士移动定时器id */
|
||||
private moveId: number = -1;
|
||||
/** 上一次帧数切换的时间 */
|
||||
private lastFrameTime: number = 0;
|
||||
/** 上一步走到格子上的时间 */
|
||||
private lastStepTime: number = 0;
|
||||
/** 执行当前步移动的Promise */
|
||||
private moveDetached?: Promise<void>;
|
||||
/** endMove的Promise */
|
||||
private moveEnding?: Promise<void>;
|
||||
/**
|
||||
* 这一步的移动方向,与{@link moveDir}不同的是,在这一步走完之前,它都不会变,
|
||||
* 当这一步走完之后,才会将其设置为{@link moveDir}的值
|
||||
*/
|
||||
stepDir: Dir2 = 'down';
|
||||
/** 每步的格子增量 */
|
||||
private stepDelta: Loc = { x: 0, y: 1 };
|
||||
|
||||
/**
|
||||
* 设置勇士所用的图片资源
|
||||
* @param image 图片资源
|
||||
*/
|
||||
setImage(image: SizedCanvasImageSource) {
|
||||
this.image = image;
|
||||
this.split();
|
||||
this.resetRenderable(true);
|
||||
this.layer.requestUpdateMoving();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置移动的移动速度
|
||||
* @param speed 移动速度
|
||||
*/
|
||||
setMoveSpeed(speed: number) {
|
||||
this.speed = speed;
|
||||
this.fixMoveSpeed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分割勇士图像,生成renderable信息
|
||||
*/
|
||||
split() {
|
||||
this.cellWidth = this.image!.width / 4;
|
||||
this.cellHeight = this.image!.height / 4;
|
||||
this.generateRenderable();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成渲染信息
|
||||
*/
|
||||
generateRenderable() {
|
||||
if (!this.image) return;
|
||||
this.renderable = {
|
||||
image: this.image,
|
||||
frame: 4,
|
||||
x: core.status.hero.loc.x,
|
||||
y: core.status.hero.loc.y,
|
||||
zIndex: core.status.hero.loc.y,
|
||||
autotile: false,
|
||||
bigImage: true,
|
||||
render: this.getRenderFromDir(this.showDir),
|
||||
animate: 0,
|
||||
alpha: 1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据方向获取勇士的裁切信息
|
||||
* @param dir 方向
|
||||
*/
|
||||
getRenderFromDir(dir: Dir): [number, number, number, number][] {
|
||||
if (!this.cellWidth || !this.cellHeight) return [];
|
||||
const index = texture.characterDirection[dir];
|
||||
const y = index * this.cellHeight;
|
||||
const res: [number, number, number, number][] = [0, 1, 2, 3].map(v => {
|
||||
return [v * this.cellWidth!, y, this.cellWidth!, this.cellHeight!];
|
||||
});
|
||||
if (this.animateReverse) return res.reverse();
|
||||
else return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始勇士帧动画
|
||||
*/
|
||||
startAnimate() {
|
||||
this.animate = true;
|
||||
this.lastFrameTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束勇士帧动画
|
||||
*/
|
||||
endAnimate() {
|
||||
this.animate = false;
|
||||
this.resetRenderable(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置帧动画是否反向播放
|
||||
* @param reverse 帧动画是否反向播放
|
||||
*/
|
||||
setAnimateReversed(reverse: boolean) {
|
||||
this.animateReverse = reverse;
|
||||
this.resetRenderable(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置勇士的动画显示朝向
|
||||
* @param dir 显示朝向
|
||||
*/
|
||||
setAnimateDir(dir: Dir) {
|
||||
if (dir !== this.showDir) {
|
||||
this.showDir = dir;
|
||||
this.resetRenderable(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置renderable状态,恢复至第一帧状态
|
||||
* @param getInfo 是否重新获取渲染信息
|
||||
*/
|
||||
resetRenderable(getInfo: boolean) {
|
||||
this.movingFrame = 0;
|
||||
|
||||
if (this.renderable) {
|
||||
this.renderable.animate = 0;
|
||||
if (getInfo) {
|
||||
this.renderable.render = this.getRenderFromDir(this.showDir);
|
||||
}
|
||||
}
|
||||
this.layer.update(this.layer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 勇士帧动画定时器
|
||||
*/
|
||||
private animateTick(time: number) {
|
||||
if (!this.animate) return;
|
||||
if (time - this.lastFrameTime > this.speed) {
|
||||
this.lastFrameTime = time;
|
||||
this.movingFrame++;
|
||||
this.movingFrame %= 4;
|
||||
if (this.renderable) this.renderable.animate = this.movingFrame;
|
||||
}
|
||||
this.layer.update(this.layer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 勇士移动定时器
|
||||
*/
|
||||
private moveTick(time: number) {
|
||||
if (!this.renderable) return;
|
||||
|
||||
if (this.moving) {
|
||||
const progress = (time - this.lastStepTime) / this.realSpeed;
|
||||
|
||||
const { x: dx, y: dy } = this.stepDelta;
|
||||
const { x, y } = core.status.hero.loc;
|
||||
if (progress >= 1) {
|
||||
this.renderable.x = x + dx;
|
||||
this.renderable.y = y + dy;
|
||||
this.fixMoveSpeed();
|
||||
this.emit('stepEnd');
|
||||
} else {
|
||||
const rx = dx * progress + x;
|
||||
const ry = dy * progress + y;
|
||||
this.renderable.x = rx;
|
||||
this.renderable.y = ry;
|
||||
}
|
||||
this.layer.update(this.layer);
|
||||
}
|
||||
this.emit('moveTick', this.renderable.x, this.renderable.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 进行下一步的移动准备,设置移动信息
|
||||
*/
|
||||
private step() {
|
||||
this.stepDir = this.moveDir;
|
||||
this.lastStepTime = Date.now();
|
||||
this.stepDelta = core.utils.scan2[this.stepDir];
|
||||
this.turn(this.stepDir);
|
||||
}
|
||||
|
||||
private fixMoveSpeed() {
|
||||
if (!core.isReplaying()) {
|
||||
this.realSpeed = this.speed;
|
||||
} else {
|
||||
const replay = core.status.replay.speed;
|
||||
this.realSpeed = replay === 24 ? 1 : this.speed / replay;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备开始移动
|
||||
*/
|
||||
readyMove() {
|
||||
this.moving = true;
|
||||
this.fixMoveSpeed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动勇士
|
||||
*/
|
||||
move(dir: Dir2): Promise<void> {
|
||||
if (!this.moving) {
|
||||
logger.error(12);
|
||||
return Promise.reject(
|
||||
'Cannot moving hero while hero not in moving!'
|
||||
);
|
||||
}
|
||||
|
||||
this.moveDir = dir;
|
||||
if (this.moveDetached) {
|
||||
return this.moveDetached;
|
||||
} else {
|
||||
this.step();
|
||||
this.moveDetached = new Promise(res => {
|
||||
this.once('stepEnd', () => {
|
||||
this.moveDetached = void 0;
|
||||
res();
|
||||
});
|
||||
});
|
||||
return this.moveDetached;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束勇士的移动过程
|
||||
*/
|
||||
endMove(): Promise<void> {
|
||||
if (!this.moving) return Promise.resolve();
|
||||
if (this.moveEnding) return this.moveEnding;
|
||||
else {
|
||||
const promise = new Promise<void>(resolve => {
|
||||
this.once('stepEnd', () => {
|
||||
this.moveEnding = void 0;
|
||||
this.moving = false;
|
||||
const { x, y } = core.status.hero.loc;
|
||||
this.setHeroLoc(x, y);
|
||||
this.render();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return (this.moveEnding = promise);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 勇士转向,不填表示顺时针转一个方向
|
||||
* @param dir 移动方向
|
||||
*/
|
||||
turn(dir?: Dir2): void {
|
||||
if (!dir) {
|
||||
const index = texture.characterTurn2.indexOf(this.stepDir);
|
||||
if (index === -1) {
|
||||
const length = texture.characterTurn.length;
|
||||
const index = texture.characterTurn.indexOf(
|
||||
this.stepDir as Dir
|
||||
);
|
||||
const next = texture.characterTurn[index % length];
|
||||
return this.turn(next);
|
||||
} else {
|
||||
return this.turn(texture.characterTurn[index]);
|
||||
}
|
||||
}
|
||||
this.moveDir = dir;
|
||||
this.stepDir = dir;
|
||||
|
||||
if (!this.renderable) return;
|
||||
this.renderable.render = this.getRenderFromDir(this.showDir);
|
||||
this.layer.update(this.layer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置勇士的坐标
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
setHeroLoc(x?: number, y?: number) {
|
||||
if (!this.renderable) return;
|
||||
if (!isNil(x)) {
|
||||
this.renderable.x = x;
|
||||
}
|
||||
if (!isNil(y)) {
|
||||
this.renderable.y = y;
|
||||
}
|
||||
this.emit('moveTick', this.renderable.x, this.renderable.y);
|
||||
this.layer.update(this.layer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按照指定函数移动勇士
|
||||
* @param x 目标横坐标
|
||||
* @param y 目标纵坐标
|
||||
* @param time 移动时间
|
||||
* @param fn 移动函数,传入一个完成度(范围0-1),返回一个三元素数组,表示横纵格子坐标,可以是小数。
|
||||
* 第三个元素表示图块纵深,一般图块的纵深就是其纵坐标,当地图上有大怪物时,此举可以辅助渲染,
|
||||
* 否则可能会导致移动过程中与大怪物的层级关系不正确,比如全在大怪物身后。注意不建议频繁改动这个值,
|
||||
* 因为此举会导致层级的重新排序,降低渲染性能。
|
||||
*/
|
||||
moveAs(x: number, y: number, time: number, fn: TimingFn<3>): Promise<void> {
|
||||
if (!this.moving) return Promise.resolve();
|
||||
if (!this.renderable) return Promise.resolve();
|
||||
const nowZIndex = fn(0)[2];
|
||||
const startTime = Date.now();
|
||||
return new Promise(res => {
|
||||
this.layer.delegateTicker(
|
||||
() => {
|
||||
if (!this.renderable) return;
|
||||
const now = Date.now();
|
||||
const progress = (now - startTime) / time;
|
||||
const [nx, ny, nz] = fn(progress);
|
||||
this.renderable.x = nx;
|
||||
this.renderable.y = ny;
|
||||
this.renderable.zIndex = nz;
|
||||
if (nz !== nowZIndex) {
|
||||
this.layer.sortMovingRenderable();
|
||||
}
|
||||
this.emit('moveTick', this.renderable.x, this.renderable.y);
|
||||
this.layer.update(this.layer);
|
||||
},
|
||||
time,
|
||||
() => {
|
||||
this.moving = false;
|
||||
if (!this.renderable) return res();
|
||||
this.renderable.x = x;
|
||||
this.renderable.y = y;
|
||||
this.emit('moveTick', this.renderable.x, this.renderable.y);
|
||||
this.layer.update(this.layer);
|
||||
res();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染勇士
|
||||
*/
|
||||
render() {
|
||||
if (!this.renderable) return;
|
||||
if (!this.animate) {
|
||||
this.renderable.animate = 0;
|
||||
} else {
|
||||
this.renderable.animate = this.movingFrame;
|
||||
}
|
||||
this.layer.update(this.layer);
|
||||
}
|
||||
|
||||
awake(layer: Layer): void {
|
||||
this.layer = layer;
|
||||
adapter.add(this);
|
||||
this.moveId = layer.delegateTicker(() => {
|
||||
const time = Date.now();
|
||||
this.animateTick(time);
|
||||
this.moveTick(time);
|
||||
});
|
||||
if (core.status.hero) {
|
||||
const image = core.status.hero.image;
|
||||
this.setImage(core.material.images.images[image]);
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(layer: Layer): void {
|
||||
adapter.remove(this);
|
||||
layer.removeTicker(this.moveId);
|
||||
}
|
||||
|
||||
onMovingUpdate(_layer: Layer, renderable: LayerMovingRenderable[]): void {
|
||||
if (this.renderable) {
|
||||
renderable.push(this.renderable);
|
||||
this.emit('append', renderable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const adapter = new RenderAdapter<HeroRenderer>('hero-adapter');
|
||||
adapter.receive('readyMove', item => {
|
||||
item.readyMove();
|
||||
return Promise.resolve();
|
||||
});
|
||||
adapter.receive('move', (item, dir: Dir) => {
|
||||
return item.move(dir);
|
||||
});
|
||||
adapter.receive('endMove', item => {
|
||||
return item.endMove();
|
||||
});
|
||||
adapter.receive(
|
||||
'moveAs',
|
||||
(item, x: number, y: number, time: number, fn: TimingFn<3>) => {
|
||||
return item.moveAs(x, y, time, fn);
|
||||
}
|
||||
);
|
||||
adapter.receive('setHeroLoc', (item, x?: number, y?: number) => {
|
||||
item.setHeroLoc(x, y);
|
||||
return Promise.resolve();
|
||||
});
|
||||
adapter.receive('turn', (item, dir: Dir2) => {
|
||||
item.turn(dir);
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
// 同步适配函数,这些函数用于同步设置信息等
|
||||
adapter.receiveSync('setImage', (item, image: SizedCanvasImageSource) => {
|
||||
item.setImage(image);
|
||||
});
|
||||
adapter.receiveSync('setMoveSpeed', (item, speed: number) => {
|
||||
item.setMoveSpeed(speed);
|
||||
});
|
||||
adapter.receiveSync('setAnimateReversed', (item, reverse: boolean) => {
|
||||
item.setAnimateReversed(reverse);
|
||||
});
|
||||
adapter.receiveSync('startAnimate', item => {
|
||||
item.startAnimate();
|
||||
});
|
||||
adapter.receiveSync('endAnimate', item => {
|
||||
item.endAnimate();
|
||||
});
|
||||
adapter.receiveSync('setAnimateDir', (item, dir: Dir) => {
|
||||
item.setAnimateDir(dir);
|
||||
});
|
||||
|
||||
// 不分同步fallback,用于适配现在的样板,之后会删除
|
||||
adapter.receiveSync('setHeroLoc', (item, x?: number, y?: number) => {
|
||||
item.setHeroLoc(x, y);
|
||||
});
|
||||
adapter.receiveSync('turn', (item, dir: Dir2) => {
|
||||
item.turn(dir);
|
||||
});
|
||||
@ -1,115 +1,47 @@
|
||||
import { standardElementNoCache, tagMap } from '@motajs/render-vue';
|
||||
import { createCache } from './cache';
|
||||
import { createFrame } from './frame';
|
||||
import { createLayer, Layer, LayerGroup } from './layer';
|
||||
import { createViewport } from './viewport';
|
||||
import { Icon, Winskin } from './misc';
|
||||
import { Animate } from './animate';
|
||||
import { createItemDetail } from './itemDetail';
|
||||
import { logger } from '@motajs/common';
|
||||
import { MapExtensionManager, MapRender, MapRenderer } from '../map';
|
||||
import { state } from '@user/data-state';
|
||||
import { materials } from '@user/client-base';
|
||||
import { MapRenderItem } from '../map';
|
||||
import { mainRenderer, tagManager } from '../renderer';
|
||||
import { createCache } from './cache';
|
||||
import { Icon, Winskin } from './misc';
|
||||
|
||||
export function createElements() {
|
||||
createCache();
|
||||
createFrame();
|
||||
createLayer();
|
||||
createViewport();
|
||||
createItemDetail();
|
||||
|
||||
// ----- 注册标签
|
||||
mainRenderer.registerElement('icon', Icon);
|
||||
mainRenderer.registerElement('winskin', Winskin);
|
||||
mainRenderer.registerElement('map-render', MapRenderItem);
|
||||
|
||||
tagMap.register('winskin', (_0, _1, props) => {
|
||||
if (!props)
|
||||
return new Winskin(core.material.images.images['winskin.png']);
|
||||
else {
|
||||
const {
|
||||
image = core.material.images.images['winskin.png'],
|
||||
type = 'static'
|
||||
} = props;
|
||||
return new Winskin(image, type);
|
||||
}
|
||||
});
|
||||
tagMap.register('layer', (_0, _1, props) => {
|
||||
if (!props) return new Layer();
|
||||
else {
|
||||
const { ex } = props;
|
||||
const l = new Layer();
|
||||
|
||||
if (ex) {
|
||||
(ex as any[]).forEach(v => {
|
||||
l.extends(v);
|
||||
});
|
||||
}
|
||||
|
||||
return l;
|
||||
}
|
||||
});
|
||||
tagMap.register('layer-group', (_0, _1, props) => {
|
||||
if (!props) return new LayerGroup();
|
||||
else {
|
||||
const { ex, layers } = props;
|
||||
const l = new LayerGroup();
|
||||
|
||||
if (ex) {
|
||||
(ex as any[]).forEach(v => {
|
||||
l.extends(v);
|
||||
});
|
||||
}
|
||||
if (layers) {
|
||||
(layers as any[]).forEach(v => {
|
||||
l.addLayer(v);
|
||||
});
|
||||
}
|
||||
|
||||
return l;
|
||||
}
|
||||
});
|
||||
tagMap.register('animation', (_0, _1, _props) => {
|
||||
return new Animate();
|
||||
});
|
||||
tagMap.register('icon', standardElementNoCache(Icon));
|
||||
tagMap.register('map-render', (_0, _1, props) => {
|
||||
tagManager.registerTag(
|
||||
'icon',
|
||||
tagManager.createStandardElement(false, Icon)
|
||||
);
|
||||
tagManager.registerTag(
|
||||
'winskin',
|
||||
tagManager.createStandardElement(false, Winskin)
|
||||
);
|
||||
tagManager.registerTag('map-render', props => {
|
||||
if (!props) {
|
||||
logger.error(42, 'layerState, renderer, extenstion');
|
||||
const renderer = new MapRenderer(materials, state.layer);
|
||||
const manager = new MapExtensionManager(renderer);
|
||||
return new MapRender(state.layer, renderer, manager);
|
||||
logger.error(42, 'layerState');
|
||||
throw new Error(`Lack of map-render property.`);
|
||||
}
|
||||
const { layerState, renderer, extension } = props;
|
||||
if (!layerState) {
|
||||
logger.error(42, 'layerState');
|
||||
const renderer = new MapRenderer(materials, state.layer);
|
||||
const manager = new MapExtensionManager(renderer);
|
||||
return new MapRender(state.layer, renderer, manager);
|
||||
throw new Error(`Lack of map-render property.`);
|
||||
}
|
||||
if (!renderer) {
|
||||
logger.error(42, 'renderer');
|
||||
const renderer = new MapRenderer(materials, state.layer);
|
||||
const manager = new MapExtensionManager(renderer);
|
||||
return new MapRender(state.layer, renderer, manager);
|
||||
throw new Error(`Lack of map-render property.`);
|
||||
}
|
||||
if (!extension) {
|
||||
logger.error(42, 'extension');
|
||||
const renderer = new MapRenderer(materials, state.layer);
|
||||
const manager = new MapExtensionManager(renderer);
|
||||
return new MapRender(state.layer, renderer, manager);
|
||||
throw new Error(`Lack of map-render property.`);
|
||||
}
|
||||
return new MapRender(layerState, renderer, extension);
|
||||
return new MapRenderItem(layerState, renderer, extension);
|
||||
});
|
||||
}
|
||||
|
||||
export * from './animate';
|
||||
export * from './block';
|
||||
export * from './cache';
|
||||
export * from './camera';
|
||||
export * from './damage';
|
||||
export * from './frame';
|
||||
export * from './hero';
|
||||
export * from './itemDetail';
|
||||
export * from './layer';
|
||||
export * from './misc';
|
||||
export * from './props';
|
||||
export * from './utils';
|
||||
export * from './viewport';
|
||||
|
||||
@ -1,270 +0,0 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { mainSetting } from '@motajs/legacy-ui';
|
||||
import { hook } from '@user/data-base';
|
||||
import { ItemState } from '@user/data-state';
|
||||
import { Damage, DamageRenderable, FloorDamageExtends } from './damage';
|
||||
import {
|
||||
ILayerGroupRenderExtends,
|
||||
LayerGroup,
|
||||
LayerGroupFloorBinder
|
||||
} from './layer';
|
||||
|
||||
interface ItemDetailData {
|
||||
x: number;
|
||||
y: number;
|
||||
diff: Record<string | symbol, number | undefined>;
|
||||
}
|
||||
|
||||
interface ItemData {
|
||||
id: AllIdsOf<'items'>;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export function createItemDetail() {
|
||||
hook.on('setBlock', (x, y, _floorId, block) => {
|
||||
FloorItemDetail.listened.forEach(v => {
|
||||
v.setBlock(block, x, y);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export class FloorItemDetail implements ILayerGroupRenderExtends {
|
||||
id: string = 'item-detail';
|
||||
|
||||
group!: LayerGroup;
|
||||
floorBinder!: LayerGroupFloorBinder;
|
||||
damage!: FloorDamageExtends;
|
||||
sprite!: Damage;
|
||||
|
||||
/** 每个分块中包含的物品信息 */
|
||||
blockData: Map<number, Map<number, ItemData>> = new Map();
|
||||
|
||||
/** 需要更新的分块 */
|
||||
private dirtyBlock: Set<number> = new Set();
|
||||
/** 道具详细信息 */
|
||||
private detailData: Map<number, Map<number, ItemDetailData>> = new Map();
|
||||
|
||||
static detailColor: Record<string, CanvasStyle> = {
|
||||
atk: '#FF7A7A',
|
||||
atkper: '#FF7A7A',
|
||||
def: '#00E6F1',
|
||||
defper: '#00E6F1',
|
||||
mdef: '#6EFF83',
|
||||
mdefper: '#6EFF83',
|
||||
hp: '#A4FF00',
|
||||
hpmax: '#F9FF00',
|
||||
hpmaxper: '#F9FF00',
|
||||
mana: '#c66',
|
||||
manaper: '#c66'
|
||||
};
|
||||
|
||||
static listened: Set<FloorItemDetail> = new Set();
|
||||
|
||||
private onBeforeDamageRender = (block: number) => {
|
||||
if (!mainSetting.getValue('screen.itemDetail')) return;
|
||||
if (this.dirtyBlock.has(block)) {
|
||||
this.sprite.block.clearCache(block, 1);
|
||||
}
|
||||
this.render(block);
|
||||
};
|
||||
|
||||
private onUpdateMapSize = (width: number, height: number) => {
|
||||
this.updateMapSize(width, height);
|
||||
};
|
||||
|
||||
private onUpdate = () => {
|
||||
this.updateItems();
|
||||
};
|
||||
|
||||
private onUpdateBlocks = (blocks: Set<number>) => {
|
||||
blocks.forEach(v => {
|
||||
this.dirtyBlock.add(v);
|
||||
});
|
||||
};
|
||||
|
||||
private listen() {
|
||||
this.sprite.on('dirtyUpdate', this.onBeforeDamageRender);
|
||||
this.sprite.on('setMapSize', this.onUpdateMapSize);
|
||||
this.sprite.on('updateBlocks', this.onUpdateBlocks);
|
||||
this.damage.on('update', this.onUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新地图大小
|
||||
*/
|
||||
updateMapSize(width: number, height: number) {
|
||||
this.blockData.clear();
|
||||
|
||||
// 预留blockData
|
||||
this.sprite.block.size(width, height);
|
||||
const data = this.sprite.block.blockData;
|
||||
const num = data.width * data.height;
|
||||
for (let i = 0; i <= num; i++) {
|
||||
this.blockData.set(i, new Map());
|
||||
this.detailData.set(i, new Map());
|
||||
this.dirtyBlock.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新全地图的物品,并进行分块存储
|
||||
*/
|
||||
updateItems() {
|
||||
const floor = this.floorBinder.getFloor();
|
||||
if (!floor) return;
|
||||
core.extractBlocks(floor);
|
||||
|
||||
core.status.maps[floor].blocks.forEach(v => {
|
||||
if (v.event.cls !== 'items' || v.disable) return;
|
||||
const id = v.event.id as AllIdsOf<'items'>;
|
||||
const item = core.material.items[id];
|
||||
if (item.cls === 'constants' || item.cls === 'tools') return;
|
||||
const x = v.x;
|
||||
const y = v.y;
|
||||
const block = this.sprite.block.getIndexByLoc(x, y);
|
||||
const index = x + y * this.sprite.mapWidth;
|
||||
const blockData = this.blockData.get(block);
|
||||
blockData?.set(index, { x, y, id });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置图块
|
||||
* @param block 图块数字
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
setBlock(block: AllNumbers, x: number, y: number) {
|
||||
const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e;
|
||||
const index = this.sprite.block.getIndexByLoc(x, y);
|
||||
const itemIndex = x + y * this.sprite.mapWidth;
|
||||
const blockData = this.blockData.get(index);
|
||||
this.dirtyBlock.add(index);
|
||||
if (block === 0) {
|
||||
blockData?.delete(itemIndex);
|
||||
return;
|
||||
}
|
||||
const cls = map[block].cls;
|
||||
if (cls !== 'items') {
|
||||
blockData?.delete(itemIndex);
|
||||
return;
|
||||
}
|
||||
const id = map[block].id;
|
||||
blockData?.set(itemIndex, { x, y, id });
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算指定分块中的物品信息
|
||||
* @param block 要计算的分块
|
||||
*/
|
||||
calAllItems(block: Set<number>) {
|
||||
const enable = mainSetting.getValue('screen.itemDetail');
|
||||
if (!core.status.thisMap || !enable) return;
|
||||
if (this.dirtyBlock.size === 0 || block.size === 0) return;
|
||||
|
||||
let diff: Record<string | symbol, number | undefined> = {};
|
||||
const before = core.status.hero;
|
||||
const hero = structuredClone(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.setFlag('__statistics__', true);
|
||||
this.dirtyBlock.forEach(v => {
|
||||
const data = this.blockData.get(v);
|
||||
const detail = this.detailData.get(v);
|
||||
detail?.clear();
|
||||
if (!data) return;
|
||||
data.forEach(v => {
|
||||
const { id, x, y } = v;
|
||||
const index = x + y * this.sprite.mapWidth;
|
||||
diff = {};
|
||||
|
||||
const item = core.material.items[id];
|
||||
if (item.cls === 'equips') {
|
||||
// 装备也显示
|
||||
const diff: Record<string, any> = {
|
||||
...(item.equip.value ?? {})
|
||||
};
|
||||
const per = item.equip.percentage ?? {};
|
||||
for (const name of Object.keys(per)) {
|
||||
const n = name as SelectKey<HeroStatus, number>;
|
||||
diff[name + 'per'] = per[n].toString() + '%';
|
||||
}
|
||||
detail?.set(index, { x, y, diff });
|
||||
return;
|
||||
}
|
||||
|
||||
ItemState.item(id)?.itemEffectFn?.();
|
||||
detail?.set(index, { x, y, diff });
|
||||
});
|
||||
});
|
||||
core.status.hero = before;
|
||||
window.hero = before;
|
||||
window.flags = before.flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算并渲染指定格子里面的物品
|
||||
* @param block 需要渲染的格子
|
||||
*/
|
||||
render(block: number) {
|
||||
if (this.dirtyBlock.has(block)) {
|
||||
this.calAllItems(new Set([block]));
|
||||
}
|
||||
const data = this.detailData;
|
||||
this.dirtyBlock.delete(block);
|
||||
const info = data.get(block);
|
||||
if (!info) return;
|
||||
info.forEach(({ x, y, diff }) => {
|
||||
let n = 0;
|
||||
for (const [key, value] of Object.entries(diff)) {
|
||||
if (!value) continue;
|
||||
const color = FloorItemDetail.detailColor[key] ?? '#fff';
|
||||
const text = core.formatBigNumber(value, 4);
|
||||
const renderable: DamageRenderable = {
|
||||
x: x * this.sprite.cellSize + 2,
|
||||
y: y * this.sprite.cellSize + 31 - n * 10,
|
||||
text,
|
||||
color,
|
||||
align: 'left',
|
||||
baseline: 'alphabetic'
|
||||
};
|
||||
this.sprite.renderable.get(block)?.add(renderable);
|
||||
n++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
awake(group: LayerGroup): void {
|
||||
this.group = group;
|
||||
|
||||
const binder = group.getExtends('floor-binder');
|
||||
const damage = group.getExtends('floor-damage');
|
||||
if (
|
||||
binder instanceof LayerGroupFloorBinder &&
|
||||
damage instanceof FloorDamageExtends
|
||||
) {
|
||||
this.floorBinder = binder;
|
||||
this.damage = damage;
|
||||
this.sprite = damage.sprite;
|
||||
this.listen();
|
||||
FloorItemDetail.listened.add(this);
|
||||
} else {
|
||||
logger.warn(1001);
|
||||
group.removeExtends('item-detail');
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(_group: LayerGroup): void {
|
||||
this.sprite.off('beforeDamageRender', this.onBeforeDamageRender);
|
||||
this.sprite.off('setMapSize', this.onUpdateMapSize);
|
||||
FloorItemDetail.listened.delete(this);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,36 +1,49 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import {
|
||||
ERenderItemEvent,
|
||||
RenderItem,
|
||||
RenderItemPosition,
|
||||
MotaOffscreenCanvas2D,
|
||||
Transform,
|
||||
SizedCanvasImageSource
|
||||
} from '@motajs/render';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { RenderableData, AutotileRenderable, texture } from './cache';
|
||||
import { IAnimateFrame, renderEmits } from './frame';
|
||||
import { IExcitable } from '@motajs/animate';
|
||||
import { IMotaIcon, IMotaWinskin } from './types';
|
||||
|
||||
export interface EIconEvent extends ERenderItemEvent {}
|
||||
|
||||
export class Icon extends RenderItem<EIconEvent> implements IAnimateFrame {
|
||||
export class Icon extends RenderItem implements IMotaIcon, IExcitable<number> {
|
||||
/** 图标id */
|
||||
icon: AllNumbers = 0;
|
||||
/** 帧数 */
|
||||
/** 渲染动画的第几帧 */
|
||||
frame: number = 0;
|
||||
/** 是否启用动画 */
|
||||
animate: boolean = false;
|
||||
/** 当前动画速度 */
|
||||
frameSpeed: number = 300;
|
||||
/** 当前帧率 */
|
||||
nowFrame: number = 0;
|
||||
|
||||
/** 图标的渲染信息 */
|
||||
private renderable?: RenderableData | AutotileRenderable;
|
||||
|
||||
private pendingIcon?: AllNumbers;
|
||||
|
||||
constructor(type: RenderItemPosition, cache?: boolean, fall?: boolean) {
|
||||
super(type, cache, fall);
|
||||
/** 委托激励对象 id,用于图标的动画展示 */
|
||||
private delegation: number = -1;
|
||||
|
||||
constructor(cache: boolean = false) {
|
||||
super(cache);
|
||||
this.setAntiAliasing(false);
|
||||
this.setHD(false);
|
||||
}
|
||||
|
||||
excited(payload: number): void {
|
||||
if (!this.renderable) return;
|
||||
const frame = Math.floor(payload / 300);
|
||||
if (frame === this.nowFrame) return;
|
||||
this.nowFrame = frame;
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
_transform: Transform
|
||||
@ -42,7 +55,7 @@ export class Icon extends RenderItem<EIconEvent> implements IAnimateFrame {
|
||||
const cw = this.width;
|
||||
const ch = this.height;
|
||||
const frame = this.animate
|
||||
? RenderItem.animatedFrame % renderable.frame
|
||||
? this.nowFrame % renderable.frame
|
||||
: this.frame;
|
||||
|
||||
if (!this.animate) {
|
||||
@ -87,6 +100,27 @@ export class Icon extends RenderItem<EIconEvent> implements IAnimateFrame {
|
||||
}
|
||||
}
|
||||
|
||||
setFrameSpeed(speed: number): void {
|
||||
this.frameSpeed = speed;
|
||||
this.update();
|
||||
}
|
||||
|
||||
setFrame(frame: number): void {
|
||||
if (frame < 0) {
|
||||
this.setAnimateStatus(true);
|
||||
return;
|
||||
}
|
||||
this.frame = frame;
|
||||
this.update();
|
||||
}
|
||||
|
||||
setAnimateStatus(animate: boolean): void {
|
||||
this.animate = animate;
|
||||
if (!animate) this.removeExcitable(this.delegation);
|
||||
else this.delegation = this.delegateExcitable(this);
|
||||
this.update();
|
||||
}
|
||||
|
||||
private setIconRenderable(num: AllNumbers) {
|
||||
const renderable = texture.getRenderable(num);
|
||||
|
||||
@ -101,15 +135,7 @@ export class Icon extends RenderItem<EIconEvent> implements IAnimateFrame {
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新动画帧
|
||||
*/
|
||||
updateFrameAnimate(): void {
|
||||
if (this.animate) this.update(this);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
renderEmits.removeFramer(this);
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
@ -124,142 +150,50 @@ export class Icon extends RenderItem<EIconEvent> implements IAnimateFrame {
|
||||
return true;
|
||||
case 'animate':
|
||||
if (!this.assertType(nextValue, 'boolean', key)) return false;
|
||||
this.animate = nextValue;
|
||||
if (nextValue) renderEmits.addFramer(this);
|
||||
else renderEmits.removeFramer(this);
|
||||
this.update();
|
||||
this.setAnimateStatus(nextValue);
|
||||
return true;
|
||||
case 'frame':
|
||||
if (!this.assertType(nextValue, 'number', key)) return false;
|
||||
this.frame = nextValue;
|
||||
this.update();
|
||||
this.setFrame(nextValue);
|
||||
return true;
|
||||
case 'speed':
|
||||
if (!this.assertType(nextValue, 'number', key)) return false;
|
||||
this.setFrameSpeed(nextValue);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
interface WinskinPatterns {
|
||||
top: CanvasPattern;
|
||||
left: CanvasPattern;
|
||||
bottom: CanvasPattern;
|
||||
right: CanvasPattern;
|
||||
}
|
||||
|
||||
export interface EWinskinEvent extends ERenderItemEvent {}
|
||||
|
||||
export class Winskin extends RenderItem<EWinskinEvent> {
|
||||
image: SizedCanvasImageSource;
|
||||
export class Winskin extends RenderItem implements IMotaWinskin {
|
||||
image: SizedCanvasImageSource | null = null;
|
||||
/** 边框宽度,32表示原始宽度 */
|
||||
borderSize: number = 32;
|
||||
/** 图片名称 */
|
||||
imageName?: string;
|
||||
imageName: string = '';
|
||||
|
||||
private pendingImage?: ImageIds;
|
||||
private patternCache?: WinskinPatterns;
|
||||
private patternTransform: DOMMatrix;
|
||||
|
||||
// todo: 跨上下文可能是未定义行为,需要上下文无关化
|
||||
private static patternMap: Map<string, WinskinPatterns> = new Map();
|
||||
|
||||
constructor(
|
||||
image: SizedCanvasImageSource,
|
||||
type: RenderItemPosition = 'static'
|
||||
) {
|
||||
super(type, false, false);
|
||||
this.image = image;
|
||||
constructor(enableCache: boolean = false) {
|
||||
super(enableCache);
|
||||
this.setAntiAliasing(false);
|
||||
|
||||
if (window.DOMMatrix) {
|
||||
this.patternTransform = new DOMMatrix();
|
||||
} else if (window.WebKitCSSMatrix) {
|
||||
this.patternTransform = new WebKitCSSMatrix();
|
||||
} else {
|
||||
this.patternTransform = new SVGMatrix();
|
||||
}
|
||||
}
|
||||
|
||||
private generatePattern() {
|
||||
const pattern = this.requireCanvas(true, false);
|
||||
pattern.setScale(1);
|
||||
protected render(canvas: MotaOffscreenCanvas2D): void {
|
||||
const img = this.image;
|
||||
pattern.size(32, 16);
|
||||
pattern.setHD(false);
|
||||
pattern.setAntiAliasing(false);
|
||||
const ctx = pattern.ctx;
|
||||
ctx.drawImage(img, 144, 0, 32, 16, 0, 0, 32, 16);
|
||||
const topPattern = ctx.createPattern(pattern.canvas, 'repeat');
|
||||
ctx.clearRect(0, 0, 32, 16);
|
||||
ctx.drawImage(img, 144, 48, 32, 16, 0, 0, 32, 16);
|
||||
const bottomPattern = ctx.createPattern(pattern.canvas, 'repeat');
|
||||
ctx.clearRect(0, 0, 32, 16);
|
||||
pattern.size(16, 32);
|
||||
ctx.drawImage(img, 128, 16, 16, 32, 0, 0, 16, 32);
|
||||
const leftPattern = ctx.createPattern(pattern.canvas, 'repeat');
|
||||
ctx.clearRect(0, 0, 16, 32);
|
||||
ctx.drawImage(img, 176, 16, 16, 32, 0, 0, 16, 32);
|
||||
const rightPattern = ctx.createPattern(pattern.canvas, 'repeat');
|
||||
if (!topPattern || !bottomPattern || !leftPattern || !rightPattern) {
|
||||
return null;
|
||||
}
|
||||
const winskinPattern: WinskinPatterns = {
|
||||
top: topPattern,
|
||||
bottom: bottomPattern,
|
||||
left: leftPattern,
|
||||
right: rightPattern
|
||||
};
|
||||
if (this.imageName) {
|
||||
Winskin.patternMap.set(this.imageName, winskinPattern);
|
||||
}
|
||||
this.patternCache = winskinPattern;
|
||||
this.deleteCanvas(pattern);
|
||||
return winskinPattern;
|
||||
}
|
||||
|
||||
private getPattern() {
|
||||
if (!this.imageName) {
|
||||
if (this.patternCache) return this.patternCache;
|
||||
return this.generatePattern();
|
||||
} else {
|
||||
const pattern = Winskin.patternMap.get(this.imageName);
|
||||
if (pattern) return pattern;
|
||||
return this.generatePattern();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
_transform: Transform
|
||||
): void {
|
||||
if (!img) return;
|
||||
const ctx = canvas.ctx;
|
||||
const img = this.image;
|
||||
const w = this.width;
|
||||
const h = this.height;
|
||||
const pad = this.borderSize / 2;
|
||||
// 背景
|
||||
ctx.drawImage(img, 0, 0, 128, 128, 2, 2, w - 4, h - 4);
|
||||
const pattern = this.getPattern();
|
||||
if (!pattern) return;
|
||||
const { top, left, right, bottom } = pattern;
|
||||
top.setTransform(this.patternTransform);
|
||||
left.setTransform(this.patternTransform);
|
||||
right.setTransform(this.patternTransform);
|
||||
bottom.setTransform(this.patternTransform);
|
||||
// 上下左右边框
|
||||
ctx.save();
|
||||
ctx.fillStyle = top;
|
||||
ctx.translate(pad, 0);
|
||||
ctx.fillRect(0, 0, w - pad * 2, pad);
|
||||
ctx.fillStyle = bottom;
|
||||
ctx.translate(0, h - pad);
|
||||
ctx.fillRect(0, 0, w - pad * 2, pad);
|
||||
ctx.fillStyle = left;
|
||||
ctx.translate(-pad, pad * 2 - h);
|
||||
ctx.fillRect(0, 0, pad, h - pad * 2);
|
||||
ctx.fillStyle = right;
|
||||
ctx.translate(w - pad, 0);
|
||||
ctx.fillRect(0, 0, pad, h - pad * 2);
|
||||
ctx.restore();
|
||||
ctx.drawImage(img, 144, 0, 32, 16, pad, 0, w - pad * 2, pad);
|
||||
ctx.drawImage(img, 144, 48, 32, 16, pad, h - pad, w - pad * 2, pad);
|
||||
ctx.drawImage(img, 128, 16, 16, 32, 0, pad, pad, h - pad * 2);
|
||||
ctx.drawImage(img, 176, 16, 16, 32, w - pad, pad, pad, h - pad * 2);
|
||||
// 四个角的边框
|
||||
ctx.drawImage(img, 128, 0, 16, 16, 0, 0, pad, pad);
|
||||
ctx.drawImage(img, 176, 0, 16, 16, w - pad, 0, pad, pad);
|
||||
@ -273,7 +207,7 @@ export class Winskin extends RenderItem<EWinskinEvent> {
|
||||
*/
|
||||
setImage(image: SizedCanvasImageSource) {
|
||||
this.image = image;
|
||||
this.patternCache = void 0;
|
||||
this.imageName = '';
|
||||
this.update();
|
||||
}
|
||||
|
||||
@ -307,8 +241,6 @@ export class Winskin extends RenderItem<EWinskinEvent> {
|
||||
*/
|
||||
setBorderSize(size: number) {
|
||||
this.borderSize = size;
|
||||
this.patternTransform.a = size / 32;
|
||||
this.patternTransform.d = size / 32;
|
||||
this.update();
|
||||
}
|
||||
|
||||
@ -319,6 +251,9 @@ export class Winskin extends RenderItem<EWinskinEvent> {
|
||||
): boolean {
|
||||
switch (key) {
|
||||
case 'image':
|
||||
this.setImage(nextValue);
|
||||
return true;
|
||||
case 'imageName':
|
||||
if (!this.assertType(nextValue, 'string', key)) return false;
|
||||
this.setImageByName(nextValue);
|
||||
return true;
|
||||
|
||||
@ -1,29 +1,7 @@
|
||||
import { BaseProps, TagDefine } from '@motajs/render-vue';
|
||||
import { ERenderItemEvent, Transform, CanvasStyle } from '@motajs/render';
|
||||
import {
|
||||
ILayerGroupRenderExtends,
|
||||
FloorLayer,
|
||||
ILayerRenderExtends,
|
||||
ELayerEvent,
|
||||
ELayerGroupEvent
|
||||
} from './layer';
|
||||
import { EAnimateEvent } from './animate';
|
||||
import { EIconEvent, EWinskinEvent } from './misc';
|
||||
import { IEnemyCollection } from '@motajs/types';
|
||||
import { ERenderItemEvent, SizedCanvasImageSource } from '@motajs/render';
|
||||
import { ILayerState } from '@user/data-state';
|
||||
import { IMapExtensionManager, IMapRenderer, IOnMapTextRenderer } from '../map';
|
||||
|
||||
export interface AnimateProps extends BaseProps {}
|
||||
|
||||
export interface DamageProps extends BaseProps {
|
||||
mapWidth?: number;
|
||||
mapHeight?: number;
|
||||
cellSize?: number;
|
||||
enemy?: IEnemyCollection;
|
||||
font?: string;
|
||||
strokeStyle?: CanvasStyle;
|
||||
strokeWidth?: number;
|
||||
}
|
||||
import { IMapExtensionManager, IMapRenderer } from '../map';
|
||||
|
||||
export interface IconProps extends BaseProps {
|
||||
/** 图标 id 或数字 */
|
||||
@ -32,50 +10,30 @@ export interface IconProps extends BaseProps {
|
||||
frame?: number;
|
||||
/** 是否开启动画,开启后 frame 参数无效 */
|
||||
animate?: boolean;
|
||||
/** 动画速度 */
|
||||
speed?: number;
|
||||
}
|
||||
|
||||
export interface WinskinProps extends BaseProps {
|
||||
/** winskin 的图片 id */
|
||||
image: ImageIds;
|
||||
/** 直接设置 winskin 图片 */
|
||||
image?: SizedCanvasImageSource;
|
||||
/** 根据图片名称设置 winskin 图片 */
|
||||
imageName?: string;
|
||||
/** 边框大小 */
|
||||
borderSize?: number;
|
||||
}
|
||||
|
||||
export interface LayerGroupProps extends BaseProps {
|
||||
cellSize?: number;
|
||||
blockSize?: number;
|
||||
floorId?: FloorIds;
|
||||
bindThisFloor?: boolean;
|
||||
camera?: Transform;
|
||||
ex?: readonly ILayerGroupRenderExtends[];
|
||||
layers?: readonly FloorLayer[];
|
||||
}
|
||||
|
||||
export interface LayerProps extends BaseProps {
|
||||
layer?: FloorLayer;
|
||||
mapWidth?: number;
|
||||
mapHeight?: number;
|
||||
cellSize?: number;
|
||||
background?: AllNumbers;
|
||||
floorImage?: FloorAnimate[];
|
||||
ex?: readonly ILayerRenderExtends[];
|
||||
}
|
||||
|
||||
export interface MapRenderProps extends BaseProps {
|
||||
layerState: ILayerState;
|
||||
renderer: IMapRenderer;
|
||||
extension: IMapExtensionManager;
|
||||
textExtension?: IOnMapTextRenderer | null;
|
||||
}
|
||||
|
||||
declare module 'vue/jsx-runtime' {
|
||||
namespace JSX {
|
||||
export interface IntrinsicElements {
|
||||
layer: TagDefine<LayerProps, ELayerEvent>;
|
||||
'layer-group': TagDefine<LayerGroupProps, ELayerGroupEvent>;
|
||||
animation: TagDefine<AnimateProps, EAnimateEvent>;
|
||||
icon: TagDefine<IconProps, EIconEvent>;
|
||||
winskin: TagDefine<WinskinProps, EWinskinEvent>;
|
||||
icon: TagDefine<IconProps, ERenderItemEvent>;
|
||||
winskin: TagDefine<WinskinProps, ERenderItemEvent>;
|
||||
'map-render': TagDefine<MapRenderProps, ERenderItemEvent>;
|
||||
}
|
||||
}
|
||||
|
||||
65
packages-user/client-modules/src/render/elements/types.ts
Normal file
65
packages-user/client-modules/src/render/elements/types.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { IRenderItem, SizedCanvasImageSource } from '@motajs/render';
|
||||
|
||||
export interface IMotaIcon extends IRenderItem {
|
||||
/** 图标id */
|
||||
readonly icon: AllNumbers;
|
||||
/** 渲染动画的第几帧 */
|
||||
readonly frame: number;
|
||||
/** 是否启用动画 */
|
||||
readonly animate: boolean;
|
||||
/** 当前动画帧数,如果没有启用动画则为 -1 */
|
||||
readonly nowFrame: number;
|
||||
/** 当前图标的动画速度,每多长时间切换至下一帧,单位毫秒 */
|
||||
readonly frameSpeed: number;
|
||||
|
||||
/**
|
||||
* 设置图标
|
||||
* @param id 图标id
|
||||
*/
|
||||
setIcon(id: AllIdsWithNone | AllNumbers): void;
|
||||
|
||||
/**
|
||||
* 设置当前图标的帧动画速度,单位毫秒
|
||||
* @param speed 帧动画速度
|
||||
*/
|
||||
setFrameSpeed(speed: number): void;
|
||||
|
||||
/**
|
||||
* 设置当前图标是否启用帧动画
|
||||
* @param animate 是否启用帧动画
|
||||
*/
|
||||
setAnimateStatus(animate: boolean): void;
|
||||
|
||||
/**
|
||||
* 设置图标显示第几帧,如果传入负数视为启用帧动画
|
||||
* @param frame 显示图标的第几帧
|
||||
*/
|
||||
setFrame(frame: number): void;
|
||||
}
|
||||
|
||||
export interface IMotaWinskin extends IRenderItem {
|
||||
/** winskin 图片源 */
|
||||
readonly image: SizedCanvasImageSource | null;
|
||||
/** 边框尺寸 */
|
||||
readonly borderSize: number;
|
||||
/** winskin 图片名称,如果不是使用名称设置的图片的话,此值为空字符串 */
|
||||
readonly imageName: string;
|
||||
|
||||
/**
|
||||
* 设置winskin图片
|
||||
* @param image winskin图片
|
||||
*/
|
||||
setImage(image: SizedCanvasImageSource): void;
|
||||
|
||||
/**
|
||||
* 通过图片名称设置winskin
|
||||
* @param name 图片名称
|
||||
*/
|
||||
setImageByName(name: ImageIds): void;
|
||||
|
||||
/**
|
||||
* 设置边框大小
|
||||
* @param size 边框大小
|
||||
*/
|
||||
setBorderSize(size: number): void;
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
import { RenderAdapter } from '@motajs/render';
|
||||
import { FloorViewport } from './viewport';
|
||||
|
||||
export function disableViewport() {
|
||||
const adapter = RenderAdapter.get<FloorViewport>('viewport');
|
||||
if (!adapter) return;
|
||||
adapter.sync('disable');
|
||||
}
|
||||
|
||||
export function enableViewport() {
|
||||
const adapter = RenderAdapter.get<FloorViewport>('viewport');
|
||||
if (!adapter) return;
|
||||
adapter.sync('enable');
|
||||
}
|
||||
@ -1,360 +0,0 @@
|
||||
import { RenderAdapter } from '@motajs/render';
|
||||
import { HeroRenderer } from './hero';
|
||||
import { ILayerGroupRenderExtends, LayerGroup } from './layer';
|
||||
import { LayerGroupFloorBinder } from './layer';
|
||||
import { hyper, TimingFn } from 'mutate-animate';
|
||||
import {
|
||||
MAP_BLOCK_HEIGHT,
|
||||
MAP_BLOCK_WIDTH,
|
||||
MAP_HEIGHT,
|
||||
MAP_WIDTH
|
||||
} from '../shared';
|
||||
|
||||
export class FloorViewport implements ILayerGroupRenderExtends {
|
||||
id: string = 'viewport';
|
||||
|
||||
group!: LayerGroup;
|
||||
hero?: HeroRenderer;
|
||||
binder?: LayerGroupFloorBinder;
|
||||
|
||||
/** 是否启用视角控制拓展 */
|
||||
enabled: boolean = true;
|
||||
/** 是否自动限定视角范围至地图范围 */
|
||||
boundX: boolean = true;
|
||||
boundY: boolean = true;
|
||||
|
||||
/** 渐变的速率曲线 */
|
||||
transitionFn: TimingFn = hyper('sin', 'out');
|
||||
/** 瞬移的速率曲线 */
|
||||
mutateFn: TimingFn = hyper('sin', 'out');
|
||||
|
||||
/** 突变时的渐变时长 */
|
||||
transitionTime: number = 600;
|
||||
|
||||
/** 当前视角位置 */
|
||||
private nx: number = 0;
|
||||
private ny: number = 0;
|
||||
/** 移动时的偏移位置 */
|
||||
private ox: number = 0;
|
||||
private oy: number = 0;
|
||||
/** 移动时的偏移最大值 */
|
||||
private maxOffset: number = 1;
|
||||
|
||||
/** 委托ticker */
|
||||
private delegation: number = -1;
|
||||
/** 渐变委托ticker */
|
||||
private transition: number = -1;
|
||||
/** 移动委托ticker */
|
||||
private moving: number = -1;
|
||||
/** 是否在渐变过程中 */
|
||||
private inTransition: boolean = false;
|
||||
/** 是否在移动过程中 */
|
||||
private inMoving: boolean = false;
|
||||
|
||||
/** 移动的监听函数 */
|
||||
private movingFramer?: () => void;
|
||||
|
||||
/**
|
||||
* 禁用自动视角控制
|
||||
*/
|
||||
disable() {
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用自动视角控制
|
||||
*/
|
||||
enable() {
|
||||
this.enabled = true;
|
||||
const { x, y } = core.status.hero.loc;
|
||||
const { x: nx, y: ny } = this.group.camera;
|
||||
const halfWidth = MAP_WIDTH / 2;
|
||||
const halfHeight = MAP_HEIGHT / 2;
|
||||
const cell = this.group.cellSize;
|
||||
const half = cell / 2;
|
||||
this.applyPosition(
|
||||
-(nx - halfWidth + half) / this.group.cellSize,
|
||||
-(ny - halfHeight + half) / this.group.cellSize
|
||||
);
|
||||
this.mutateTo(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否自动限定视角范围至地图范围
|
||||
* @param boundX 是否自动限定水平视角范围
|
||||
* @param boundY 是否自动限定竖直视角范围
|
||||
*/
|
||||
setAutoBound(boundX: boolean = this.boundX, boundY: boolean = this.boundY) {
|
||||
this.boundX = boundX;
|
||||
this.boundY = boundY;
|
||||
this.group.requestBeforeFrame(() => {
|
||||
this.setPosition(this.nx, this.ny);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 传入视角的目标位置,将其限定在地图范围内后返回
|
||||
* @param x 图格横坐标
|
||||
* @param y 图格纵坐标
|
||||
*/
|
||||
getBoundedPosition(x: number, y: number) {
|
||||
if (!this.checkDependency()) return { x, y };
|
||||
if (!this.boundX && !this.boundY) return { x, y };
|
||||
const width = MAP_BLOCK_WIDTH;
|
||||
const height = MAP_BLOCK_HEIGHT;
|
||||
const minX = (width - 1) / 2;
|
||||
const minY = (height - 1) / 2;
|
||||
const floor = core.status.maps[this.binder!.getFloor()];
|
||||
const maxX = floor.width - minX - 1;
|
||||
const maxY = floor.height - minY - 1;
|
||||
|
||||
return {
|
||||
x: this.boundX ? core.clamp(x, minX, maxX) : x,
|
||||
y: this.boundY ? core.clamp(y, minY, maxY) : y
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置视角位置
|
||||
* @param x 目标图格横坐标
|
||||
* @param y 目标图格纵坐标
|
||||
*/
|
||||
setPosition(x: number, y: number) {
|
||||
if (!this.enabled) return;
|
||||
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
|
||||
this.group.removeTicker(this.transition, false);
|
||||
this.applyPosition(nx, ny);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始移动
|
||||
*/
|
||||
startMove() {
|
||||
if (this.inMoving) return;
|
||||
this.inMoving = true;
|
||||
this.createMoveTransition();
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束移动
|
||||
*/
|
||||
endMove() {
|
||||
this.inMoving = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当勇士通过移动改变至指定位置时移动视角
|
||||
* @param x 目标图格横坐标
|
||||
* @param y 目标图格纵坐标
|
||||
*/
|
||||
moveTo(x: number, y: number, time: number = 200) {
|
||||
if (!this.enabled) return;
|
||||
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
|
||||
if (this.inTransition) {
|
||||
const distance = Math.hypot(this.nx - nx, this.ny - ny);
|
||||
const t = core.clamp(distance * time, time, time * 3);
|
||||
this.createTransition(nx, ny, t, this.transitionFn);
|
||||
}
|
||||
}
|
||||
|
||||
private createMoveTransition() {
|
||||
if (!this.checkDependency()) return;
|
||||
let xTarget: number = 0;
|
||||
let yTarget: number = 0;
|
||||
let xStart: number = this.ox;
|
||||
let yStart: number = this.oy;
|
||||
let xStartTime: number = Date.now();
|
||||
let yStartTime: number = Date.now();
|
||||
let ending: boolean = false;
|
||||
// 这个数等于 sinh(2),用这个数的话,可以正好在刚开始移动的时候达到1的斜率,效果会比较好
|
||||
const transitionTime = this.hero!.speed * 3.626860407847019;
|
||||
|
||||
const setTargetX = (x: number, time: number) => {
|
||||
if (x === xTarget) return;
|
||||
xTarget = x;
|
||||
xStartTime = time;
|
||||
xStart = this.ox;
|
||||
};
|
||||
const setTargetY = (y: number, time: number) => {
|
||||
if (y === yTarget) return;
|
||||
yTarget = y;
|
||||
yStart = this.oy;
|
||||
yStartTime = time;
|
||||
};
|
||||
|
||||
if (this.movingFramer) {
|
||||
this.hero!.off('moveTick', this.movingFramer);
|
||||
}
|
||||
this.movingFramer = () => {
|
||||
if (this.inTransition) return;
|
||||
const now = Date.now();
|
||||
if (!this.inMoving && !ending) {
|
||||
setTargetX(0, now);
|
||||
setTargetY(0, now);
|
||||
ending = true;
|
||||
}
|
||||
if (!ending) {
|
||||
const dir = this.hero!.stepDir;
|
||||
const { x, y } = core.utils.scan2[dir];
|
||||
setTargetX(-x * this.maxOffset, now);
|
||||
setTargetY(-y * this.maxOffset, now);
|
||||
}
|
||||
|
||||
if (!this.hero!.renderable) return;
|
||||
|
||||
const { x, y } = this.hero!.renderable;
|
||||
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
|
||||
this.applyPosition(nx, ny);
|
||||
|
||||
if (ending) {
|
||||
if (this.ox === xTarget && this.oy === yTarget) {
|
||||
this.hero!.off('moveTick', this.movingFramer);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// todo: 效果太差了,需要优化
|
||||
return;
|
||||
if (this.ox !== xTarget) {
|
||||
const time = transitionTime * Math.abs(xStart - xTarget);
|
||||
const progress = (now - xStartTime) / time;
|
||||
if (progress > 1) {
|
||||
this.ox = xTarget;
|
||||
} else {
|
||||
const p = this.transitionFn(progress);
|
||||
this.ox = (xTarget - xStart) * p + xStart;
|
||||
}
|
||||
}
|
||||
if (this.oy !== yTarget) {
|
||||
const time = transitionTime * Math.abs(yStart - yTarget);
|
||||
const progress = (now - yStartTime) / time;
|
||||
if (progress > 1) {
|
||||
this.oy = yTarget;
|
||||
} else {
|
||||
const p = this.transitionFn(progress);
|
||||
this.oy = (yTarget - yStart) * p + yStart;
|
||||
}
|
||||
}
|
||||
};
|
||||
this.hero!.on('moveTick', this.movingFramer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当勇士位置突变至指定位置时移动视角
|
||||
* @param x 目标图格横坐标
|
||||
* @param y 目标图格纵坐标
|
||||
*/
|
||||
mutateTo(x: number, y: number, time: number = this.transitionTime) {
|
||||
if (!this.enabled) return;
|
||||
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
|
||||
this.createTransition(nx, ny, time, this.mutateFn);
|
||||
}
|
||||
|
||||
private createTransition(x: number, y: number, time: number, fn: TimingFn) {
|
||||
const start = Date.now();
|
||||
const end = start + time;
|
||||
const sx = this.nx;
|
||||
const sy = this.ny;
|
||||
const dx = x - sx;
|
||||
const dy = y - sy;
|
||||
|
||||
this.inTransition = true;
|
||||
this.group.removeTicker(this.transition, false);
|
||||
this.transition = this.group.delegateTicker(
|
||||
() => {
|
||||
const now = Date.now();
|
||||
if (now >= end) {
|
||||
this.group.removeTicker(this.transition, true);
|
||||
return;
|
||||
}
|
||||
const progress = fn((now - start) / time);
|
||||
const tx = dx * progress;
|
||||
const ty = dy * progress;
|
||||
this.applyPosition(tx + sx, ty + sy);
|
||||
},
|
||||
time,
|
||||
() => {
|
||||
this.applyPosition(x, y);
|
||||
this.inTransition = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private applyPosition(x: number, y: number) {
|
||||
if (!this.enabled) return;
|
||||
if (x === this.nx && y === this.ny) return;
|
||||
const halfWidth = MAP_WIDTH / 2;
|
||||
const halfHeight = MAP_HEIGHT / 2;
|
||||
const cell = this.group.cellSize;
|
||||
const half = cell / 2;
|
||||
this.nx = x;
|
||||
this.ny = y;
|
||||
const { x: bx, y: by } = this.getBoundedPosition(x, y);
|
||||
const rx = bx * cell - halfWidth + half;
|
||||
const ry = by * cell - halfHeight + half;
|
||||
core.bigmap.offsetX = rx;
|
||||
core.bigmap.offsetY = ry;
|
||||
this.group.camera.setTranslate(-rx, -ry);
|
||||
this.group.update(this.group);
|
||||
}
|
||||
|
||||
private checkDependency() {
|
||||
if (this.hero && this.binder) return true;
|
||||
const group = this.group;
|
||||
const ex1 = group.getLayer('event')?.getExtends('floor-hero');
|
||||
const ex2 = group.getExtends('floor-binder');
|
||||
if (
|
||||
ex1 instanceof HeroRenderer &&
|
||||
ex2 instanceof LayerGroupFloorBinder
|
||||
) {
|
||||
this.hero = ex1;
|
||||
this.binder = ex2;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
awake(group: LayerGroup): void {
|
||||
this.group = group;
|
||||
adapter.add(this);
|
||||
}
|
||||
|
||||
onDestroy(group: LayerGroup): void {
|
||||
group.removeTicker(this.delegation);
|
||||
group.removeTicker(this.transition);
|
||||
group.removeTicker(this.moving);
|
||||
adapter.remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
const adapter = new RenderAdapter<FloorViewport>('viewport');
|
||||
adapter.receive('mutateTo', (item, x, y, time) => {
|
||||
item.mutateTo(x, y, time);
|
||||
return Promise.resolve();
|
||||
});
|
||||
adapter.receive('moveTo', (item, x, y, time) => {
|
||||
item.moveTo(x, y, time);
|
||||
return Promise.resolve();
|
||||
});
|
||||
adapter.receive('setPosition', (item, x, y) => {
|
||||
item.setPosition(x, y);
|
||||
return Promise.resolve();
|
||||
});
|
||||
adapter.receiveSync('disable', item => {
|
||||
item.disable();
|
||||
});
|
||||
adapter.receiveSync('enable', item => {
|
||||
item.enable();
|
||||
});
|
||||
adapter.receiveSync('startMove', item => {
|
||||
item.startMove();
|
||||
});
|
||||
adapter.receiveSync('endMove', item => {
|
||||
item.endMove();
|
||||
});
|
||||
|
||||
export function createViewport() {
|
||||
const { hook } = Mota.require('@user/data-base');
|
||||
hook.on('changingFloor', (_, loc) => {
|
||||
adapter.all('setPosition', loc.x, loc.y);
|
||||
});
|
||||
}
|
||||
@ -10,7 +10,7 @@ import { sceneController } from './scene';
|
||||
import { GameTitleUI } from './ui/title';
|
||||
import { createWeather } from './weather';
|
||||
import { createMainExtension } from './commonIns';
|
||||
import { createApp } from '@motajs/render-vue';
|
||||
import { createApp } from './renderer';
|
||||
|
||||
export function createGameRenderer() {
|
||||
const App = defineComponent(_props => {
|
||||
|
||||
@ -1,114 +0,0 @@
|
||||
import {
|
||||
MotaOffscreenCanvas2D,
|
||||
RenderItem,
|
||||
RenderItemPosition,
|
||||
Transform
|
||||
} from '@motajs/render';
|
||||
import { wrapInstancedComponent } from '@motajs/render-vue';
|
||||
|
||||
// 渲染端的向后兼容用,会充当两个版本间过渡的作用
|
||||
class Change extends RenderItem {
|
||||
private tips: string[] = [];
|
||||
/** 当前小贴士 */
|
||||
private usingTip: string = '';
|
||||
/** 透明度 */
|
||||
private backAlpha: number = 0;
|
||||
private title: string = '';
|
||||
|
||||
constructor(type: RenderItemPosition) {
|
||||
super(type, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置楼传过程中的小贴士
|
||||
*/
|
||||
setTips(tip: string[]) {
|
||||
this.tips = tip;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置标题
|
||||
*/
|
||||
setTitle(title: string) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示楼层切换的从透明变黑的动画
|
||||
* @param time 动画时长
|
||||
*/
|
||||
showChange(time: number) {
|
||||
const length = this.tips.length;
|
||||
const tip = this.tips[Math.floor(Math.random() * length)] ?? '';
|
||||
this.usingTip = tip;
|
||||
|
||||
return new Promise<void>(res => {
|
||||
const start = Date.now();
|
||||
const id = this.delegateTicker(
|
||||
() => {
|
||||
const dt = Date.now() - start;
|
||||
const progress = dt / time;
|
||||
if (progress > 1) {
|
||||
this.backAlpha = 1;
|
||||
this.removeTicker(id);
|
||||
} else {
|
||||
this.backAlpha = progress;
|
||||
}
|
||||
this.update();
|
||||
},
|
||||
10000,
|
||||
res
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示楼层切换从黑到透明的动画
|
||||
* @param time 动画时长
|
||||
*/
|
||||
async hideChange(time: number) {
|
||||
return new Promise<void>(res => {
|
||||
const start = Date.now();
|
||||
const id = this.delegateTicker(
|
||||
() => {
|
||||
const dt = Date.now() - start;
|
||||
const progress = dt / time;
|
||||
if (progress > 1) {
|
||||
this.removeTicker(id);
|
||||
this.backAlpha = 0;
|
||||
} else {
|
||||
this.backAlpha = 1 - progress;
|
||||
}
|
||||
this.update();
|
||||
},
|
||||
10000,
|
||||
res
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
_transform: Transform
|
||||
): void {
|
||||
if (this.backAlpha === 0) return;
|
||||
const ctx = canvas.ctx;
|
||||
ctx.globalAlpha = this.backAlpha;
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = '32px "normal"';
|
||||
ctx.fillText(this.title, canvas.width / 2, canvas.height * 0.4);
|
||||
ctx.font = '16px "normal"';
|
||||
if (this.usingTip.length > 0) {
|
||||
ctx.fillText(
|
||||
'小贴士:' + this.usingTip,
|
||||
canvas.width / 2,
|
||||
canvas.height * 0.75
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const FloorChange = wrapInstancedComponent(() => new Change('static'));
|
||||
@ -5,7 +5,7 @@ import { ElementNamespace, ComponentInternalInstance } from 'vue';
|
||||
import { CELL_HEIGHT, CELL_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../shared';
|
||||
import { IMapExtensionManager } from './extension';
|
||||
|
||||
export class MapRender extends RenderItem {
|
||||
export class MapRenderItem extends RenderItem {
|
||||
/**
|
||||
* @param layerState 地图状态对象
|
||||
* @param renderer 地图渲染器对象
|
||||
@ -15,13 +15,14 @@ export class MapRender extends RenderItem {
|
||||
readonly renderer: IMapRenderer,
|
||||
readonly exManager: IMapExtensionManager
|
||||
) {
|
||||
super('static', false, false);
|
||||
super(false);
|
||||
|
||||
renderer.setLayerState(layerState);
|
||||
renderer.setCellSize(CELL_WIDTH, CELL_HEIGHT);
|
||||
renderer.setRenderSize(MAP_WIDTH, MAP_HEIGHT);
|
||||
|
||||
this.delegateTicker(time => {
|
||||
// 元素被销毁时会自动删除所有的激励对象,所以不需要担心会内存泄漏
|
||||
this.delegateExcitable(time => {
|
||||
this.renderer.tick(time);
|
||||
if (this.renderer.needUpdate()) {
|
||||
this.update();
|
||||
|
||||
@ -997,8 +997,7 @@ export interface IMapVertexStatus {
|
||||
* 脏标记表示顶点数组的长度是否发生变化
|
||||
*/
|
||||
export interface IMapVertexGenerator
|
||||
extends IDirtyTracker<boolean>,
|
||||
IMapVertexStatus {
|
||||
extends IDirtyTracker<boolean>, IMapVertexStatus {
|
||||
/** 地图渲染器 */
|
||||
readonly renderer: IMapRenderer;
|
||||
/** 地图分块 */
|
||||
|
||||
@ -1,8 +1,46 @@
|
||||
import { MotaRenderer } from '@motajs/render';
|
||||
import { MAIN_WIDTH, MAIN_HEIGHT } from './shared';
|
||||
import {
|
||||
MAIN_WIDTH,
|
||||
MAIN_HEIGHT,
|
||||
DEBUG_VARIATOR,
|
||||
VARIATOR_DEBUG_SPEED,
|
||||
DEBUG_DIVIDER,
|
||||
DIVIDER_DEBUG_DIVIDER
|
||||
} from './shared';
|
||||
import { createRendererFor, RendererUsing } from '@motajs/render-vue';
|
||||
import {
|
||||
ExcitationDivider,
|
||||
ExcitationVariator,
|
||||
RafExcitation
|
||||
} from '@motajs/animate';
|
||||
|
||||
/** 渲染激励源 */
|
||||
export const rafExcitation = new RafExcitation();
|
||||
/** 渲染分频器 */
|
||||
export const excitationDivider = new ExcitationDivider<number>();
|
||||
|
||||
if (DEBUG_VARIATOR) {
|
||||
const variator = new ExcitationVariator();
|
||||
variator.bindExcitation(rafExcitation);
|
||||
variator.setSpeed(VARIATOR_DEBUG_SPEED);
|
||||
excitationDivider.bindExcitation(variator);
|
||||
} else {
|
||||
excitationDivider.bindExcitation(rafExcitation);
|
||||
}
|
||||
|
||||
if (DEBUG_DIVIDER) {
|
||||
excitationDivider.setDivider(DIVIDER_DEBUG_DIVIDER);
|
||||
}
|
||||
|
||||
export const mainRenderer = new MotaRenderer({
|
||||
canvas: '#render-main',
|
||||
width: MAIN_WIDTH,
|
||||
height: MAIN_HEIGHT
|
||||
height: MAIN_HEIGHT,
|
||||
// 使用分频器,用户可以在设置中调整,如果设备性能较差调高分频有助于提高性能表现
|
||||
excitaion: excitationDivider
|
||||
});
|
||||
|
||||
export const using = new RendererUsing(mainRenderer);
|
||||
|
||||
export const { createApp, render, tagManager } =
|
||||
createRendererFor(mainRenderer);
|
||||
|
||||
@ -2,6 +2,25 @@ import { ElementLocator, Font } from '@motajs/render';
|
||||
|
||||
// 本文件为 UI 配置文件,你可以修改下面的每个常量来控制 UI 的显示参数,每个常量都有注释说明
|
||||
|
||||
//#region 调试用参数
|
||||
|
||||
/**
|
||||
* 渲染器的激励源是否使用变速器,使用后可以通过 {@link VARIATOR_DEBUG_SPEED} 调整激励源速度,
|
||||
* 从而调整动画等内容的执行速度,方便调试
|
||||
*/
|
||||
export const DEBUG_VARIATOR = false;
|
||||
/** 当使用变速器作为激励源调试时,变速器的速度 */
|
||||
export const VARIATOR_DEBUG_SPEED = 0.2;
|
||||
/**
|
||||
* 是否使用分频器调试,使用后可以通过 {@link DIVIDER_DEBUG_DIVIDER} 调整分配比例,
|
||||
* 降低画面刷新频率及每帧执行函数的执行频率,方便调试
|
||||
*/
|
||||
export const DEBUG_DIVIDER = false;
|
||||
/** 当使用分频器调试时,分配比例 */
|
||||
export const DIVIDER_DEBUG_DIVIDER = 60;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 地图
|
||||
|
||||
/** 每个格子的默认宽度,现阶段用处不大 */
|
||||
|
||||
@ -2,7 +2,7 @@ import {
|
||||
Font,
|
||||
IActionEvent,
|
||||
MotaOffscreenCanvas2D,
|
||||
Sprite
|
||||
CustomRenderItem
|
||||
} from '@motajs/render';
|
||||
// import { WeatherController } from '../weather';
|
||||
import { defineComponent, onUnmounted, reactive, ref } from 'vue';
|
||||
@ -27,11 +27,10 @@ import {
|
||||
import { ReplayingStatus } from './toolbar';
|
||||
import { getHeroStatusOn, state } from '@user/data-state';
|
||||
import { hook } from '@user/data-base';
|
||||
import { FloorChange } from '../legacy/fallback';
|
||||
import { mainUIController } from './controller';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { mainMapExtension, mainMapRenderer } from '../commonIns';
|
||||
import { onTick } from '@motajs/render-vue';
|
||||
import { using } from '../renderer';
|
||||
|
||||
const MainScene = defineComponent(() => {
|
||||
//#region 基本定义
|
||||
@ -147,7 +146,7 @@ const MainScene = defineComponent(() => {
|
||||
//#region sprite 渲染
|
||||
|
||||
let lastLength = 0;
|
||||
onTick(() => {
|
||||
using.onExcitedFunc(() => {
|
||||
const len = core.status.stepPostfix?.length ?? 0;
|
||||
if (len !== lastLength) {
|
||||
mapMiscSprite.value?.update();
|
||||
@ -155,7 +154,7 @@ const MainScene = defineComponent(() => {
|
||||
}
|
||||
});
|
||||
|
||||
const mapMiscSprite = ref<Sprite>();
|
||||
const mapMiscSprite = ref<CustomRenderItem>();
|
||||
|
||||
const renderMapMisc = (canvas: MotaOffscreenCanvas2D) => {
|
||||
const step = core.status.stepPostfix;
|
||||
@ -245,7 +244,7 @@ const MainScene = defineComponent(() => {
|
||||
loc={[0, 0, MAP_WIDTH, MAP_HEIGHT]}
|
||||
/>
|
||||
<Textbox id="main-textbox" {...mainTextboxProps}></Textbox>
|
||||
<FloorChange id="floor-change" zIndex={50}></FloorChange>
|
||||
{/* <FloorChange id="floor-change" zIndex={50}></FloorChange> */}
|
||||
<Tip
|
||||
id="main-tip"
|
||||
zIndex={80}
|
||||
@ -253,7 +252,7 @@ const MainScene = defineComponent(() => {
|
||||
pad={[12, 6]}
|
||||
corner={16}
|
||||
/>
|
||||
<sprite
|
||||
<custom
|
||||
noevent
|
||||
loc={[0, 0, MAP_WIDTH, MAP_HEIGHT]}
|
||||
ref={mapMiscSprite}
|
||||
|
||||
@ -27,8 +27,7 @@ import { CENTER_LOC, FULL_LOC, MAIN_HEIGHT, POP_BOX_WIDTH } from '../shared';
|
||||
import { useKey } from '../use';
|
||||
|
||||
export interface MainSettingsProps
|
||||
extends Partial<ChoicesProps>,
|
||||
UIComponentProps {
|
||||
extends Partial<ChoicesProps>, UIComponentProps {
|
||||
loc: ElementLocator;
|
||||
}
|
||||
|
||||
|
||||
@ -27,12 +27,13 @@ import {
|
||||
transitionedColor,
|
||||
useKey
|
||||
} from '../use';
|
||||
import { hyper, linear, sleep } from 'mutate-animate';
|
||||
import { ExitFullscreen, Fullscreen, SoundVolume } from '../components';
|
||||
import { mainSetting, triggerFullscreen } from '@motajs/legacy-ui';
|
||||
import { saveLoad } from './save';
|
||||
import { MainSceneUI } from './main';
|
||||
import { adjustCover } from '../utils';
|
||||
import { cosh, CurveMode, linear } from '@motajs/animate';
|
||||
import { sleep } from '@motajs/common';
|
||||
|
||||
const enum TitleButton {
|
||||
StartGame,
|
||||
@ -106,8 +107,12 @@ export const GameTitle = defineComponent<GameTitleProps>(props => {
|
||||
color: v.color,
|
||||
name: v.name,
|
||||
hard: '',
|
||||
colorTrans: transitionedColor('#fff', 400, hyper('sin', 'out'))!,
|
||||
scale: transitioned(1, 400, hyper('sin', 'out'))!
|
||||
colorTrans: transitionedColor(
|
||||
'#fff',
|
||||
400,
|
||||
cosh(2, CurveMode.EaseOut)
|
||||
)!,
|
||||
scale: transitioned(1, 400, cosh(2, CurveMode.EaseOut))!
|
||||
};
|
||||
});
|
||||
|
||||
@ -118,8 +123,12 @@ export const GameTitle = defineComponent<GameTitleProps>(props => {
|
||||
color: core.arrayToRGBA(v.color!),
|
||||
name: v.title,
|
||||
hard: v.name,
|
||||
colorTrans: transitionedColor('#fff', 400, hyper('sin', 'out'))!,
|
||||
scale: transitioned(1, 400, hyper('sin', 'out'))!
|
||||
colorTrans: transitionedColor(
|
||||
'#fff',
|
||||
400,
|
||||
cosh(2, CurveMode.EaseOut)
|
||||
)!,
|
||||
scale: transitioned(1, 400, cosh(2, CurveMode.EaseOut))!
|
||||
};
|
||||
});
|
||||
// 返回按钮
|
||||
@ -128,11 +137,15 @@ export const GameTitle = defineComponent<GameTitleProps>(props => {
|
||||
color: '#aaa',
|
||||
name: '返回',
|
||||
hard: '',
|
||||
colorTrans: transitionedColor('#fff', 400, hyper('sin', 'out'))!
|
||||
colorTrans: transitionedColor('#fff', 400, cosh(2, CurveMode.EaseOut))!
|
||||
});
|
||||
|
||||
/** 声音设置按钮的颜色 */
|
||||
const soundColor = transitionedColor('#ddd', 400, hyper('sin', 'out'))!;
|
||||
const soundColor = transitionedColor(
|
||||
'#ddd',
|
||||
400,
|
||||
cosh(2, CurveMode.EaseOut)
|
||||
)!;
|
||||
|
||||
/** 开始界面按钮的不透明度,选择难度界面的不透明度使用 `1-buttonsAlpha` 计算 */
|
||||
const buttonsAlpha = transitioned(1, 300, linear())!;
|
||||
@ -152,7 +165,11 @@ export const GameTitle = defineComponent<GameTitleProps>(props => {
|
||||
/** 选择难度界面按钮的高度 */
|
||||
const hardHeight = (hard.length - 1) * 40 + 60;
|
||||
/** 按钮的背景框高度 */
|
||||
const rectHeight = transitioned(buttonHeight, 600, hyper('sin', 'in-out'))!;
|
||||
const rectHeight = transitioned(
|
||||
buttonHeight,
|
||||
600,
|
||||
cosh(2, CurveMode.EaseOut)
|
||||
)!;
|
||||
|
||||
//#region 按钮功能
|
||||
|
||||
|
||||
@ -24,16 +24,8 @@ import {
|
||||
watch
|
||||
} from 'vue';
|
||||
import { FloorSelector } from '../components';
|
||||
import {
|
||||
ILayerGroupRenderExtends,
|
||||
FloorDamageExtends,
|
||||
FloorItemDetail,
|
||||
LayerGroupAnimate,
|
||||
LayerGroup,
|
||||
LayerGroupFloorBinder
|
||||
} from '../elements';
|
||||
import { clamp, mean } from 'lodash-es';
|
||||
import { calculateStatisticsOne, StatisticsDataOneFloor } from './statistics';
|
||||
import { StatisticsDataOneFloor } from './statistics';
|
||||
import { Tip, TipExpose } from '../components';
|
||||
import { useKey } from '../use';
|
||||
import {
|
||||
@ -61,11 +53,11 @@ const viewMapProps = {
|
||||
export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
const nowFloorId = core.status.floorId;
|
||||
|
||||
const layerGroupExtends: ILayerGroupRenderExtends[] = [
|
||||
new FloorDamageExtends(),
|
||||
new FloorItemDetail(),
|
||||
new LayerGroupAnimate()
|
||||
];
|
||||
// const layerGroupExtends: ILayerGroupRenderExtends[] = [
|
||||
// new FloorDamageExtends(),
|
||||
// new FloorItemDetail(),
|
||||
// new LayerGroupAnimate()
|
||||
// ];
|
||||
|
||||
const restHeight = STATUS_BAR_HEIGHT - 292;
|
||||
const col = restHeight / 4;
|
||||
@ -84,7 +76,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
})
|
||||
);
|
||||
|
||||
const group = ref<LayerGroup>();
|
||||
// const group = ref<LayerGroup>();
|
||||
const tip = ref<TipExpose>();
|
||||
const statistics = shallowRef<StatisticsDataOneFloor>();
|
||||
|
||||
@ -107,7 +99,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
.realize('@viewMap_down_ten', () => changeFloor(-10))
|
||||
.realize('@viewMap_book', () => openBook())
|
||||
.realize('@viewMap_fly', () => fly())
|
||||
.realize('@viewMap_reset', () => resetCamera())
|
||||
// .realize('@viewMap_reset', () => resetCamera())
|
||||
.realize('confirm', () => close())
|
||||
.realize('exit', (_, code, assist) => {
|
||||
// 如果按键不能触发怪物手册,则关闭界面,因为怪物手册和退出默认使用同一个按键,需要特判
|
||||
@ -145,10 +137,10 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
else tip.value?.drawTip(`无法飞往${core.floors[id].title}`);
|
||||
};
|
||||
|
||||
const resetCamera = () => {
|
||||
group.value?.camera.reset();
|
||||
group.value?.update();
|
||||
};
|
||||
// const resetCamera = () => {
|
||||
// group.value?.camera.reset();
|
||||
// group.value?.update();
|
||||
// };
|
||||
|
||||
//#region 渐变渲染
|
||||
|
||||
@ -195,33 +187,33 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
|
||||
//#region 地图渲染
|
||||
|
||||
const renderLayer = (floorId: FloorIds) => {
|
||||
const binder = group.value?.getExtends(
|
||||
'floor-binder'
|
||||
) as LayerGroupFloorBinder;
|
||||
binder.bindFloor(floorId);
|
||||
group.value?.camera.reset();
|
||||
core.status.floorId = floorId;
|
||||
core.status.thisMap = core.status.maps[floorId];
|
||||
statistics.value = calculateStatisticsOne(floorId);
|
||||
};
|
||||
// const renderLayer = (floorId: FloorIds) => {
|
||||
// const binder = group.value?.getExtends(
|
||||
// 'floor-binder'
|
||||
// ) as LayerGroupFloorBinder;
|
||||
// binder.bindFloor(floorId);
|
||||
// group.value?.camera.reset();
|
||||
// core.status.floorId = floorId;
|
||||
// core.status.thisMap = core.status.maps[floorId];
|
||||
// statistics.value = calculateStatisticsOne(floorId);
|
||||
// };
|
||||
|
||||
const moveCamera = (dx: number, dy: number) => {
|
||||
const camera = group.value?.camera;
|
||||
if (!camera) return;
|
||||
camera.translate(dx / camera.scaleX, dy / camera.scaleX);
|
||||
group.value?.update();
|
||||
};
|
||||
// const moveCamera = (dx: number, dy: number) => {
|
||||
// const camera = group.value?.camera;
|
||||
// if (!camera) return;
|
||||
// camera.translate(dx / camera.scaleX, dy / camera.scaleX);
|
||||
// group.value?.update();
|
||||
// };
|
||||
|
||||
const scaleCamera = (scale: number, x: number, y: number) => {
|
||||
const camera = group.value?.camera;
|
||||
if (!camera) return;
|
||||
const [cx, cy] = camera.untransformed(x, y);
|
||||
camera.translate(cx, cy);
|
||||
camera.scale(scale);
|
||||
camera.translate(-cx, -cy);
|
||||
group.value?.update();
|
||||
};
|
||||
// const scaleCamera = (scale: number, x: number, y: number) => {
|
||||
// const camera = group.value?.camera;
|
||||
// if (!camera) return;
|
||||
// const [cx, cy] = camera.untransformed(x, y);
|
||||
// camera.translate(cx, cy);
|
||||
// camera.scale(scale);
|
||||
// camera.translate(-cx, -cy);
|
||||
// group.value?.update();
|
||||
// };
|
||||
|
||||
//#region 事件监听
|
||||
|
||||
@ -230,7 +222,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
if (ev.offsetX < col * 2) {
|
||||
changeFloor(1);
|
||||
} else {
|
||||
resetCamera();
|
||||
// resetCamera();
|
||||
}
|
||||
};
|
||||
|
||||
@ -293,7 +285,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
const dx = ev.offsetX - lastMoveX;
|
||||
const dy = ev.offsetY - lastMoveY;
|
||||
movement += Math.hypot(dx, dy);
|
||||
moveCamera(dx, dy);
|
||||
// moveCamera(dx, dy);
|
||||
}
|
||||
moved = true;
|
||||
lastMoveX = ev.offsetX;
|
||||
@ -322,7 +314,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
return;
|
||||
}
|
||||
if (!isFinite(scale) || scale === 0) return;
|
||||
scaleCamera(scale, cx, cy);
|
||||
// scaleCamera(scale, cx, cy);
|
||||
}
|
||||
} else {
|
||||
if (mouseDown) {
|
||||
@ -341,8 +333,8 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
|
||||
const wheelMap = (ev: IWheelEvent) => {
|
||||
if (ev.altKey) {
|
||||
const scale = ev.wheelY < 0 ? 1.1 : 0.9;
|
||||
scaleCamera(scale, ev.offsetX, ev.offsetY);
|
||||
// const scale = ev.wheelY < 0 ? 1.1 : 0.9;
|
||||
// scaleCamera(scale, ev.offsetX, ev.offsetY);
|
||||
} else if (ev.ctrlKey) {
|
||||
changeFloor(-Math.sign(ev.wheelY) * 10);
|
||||
} else {
|
||||
@ -362,7 +354,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
renderLayer(floorId.value);
|
||||
// renderLayer(floorId.value);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@ -371,7 +363,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
});
|
||||
|
||||
watch(floorId, value => {
|
||||
renderLayer(value);
|
||||
// renderLayer(value);
|
||||
});
|
||||
|
||||
//#region 组件树
|
||||
@ -394,7 +386,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
v-model:now={now.value}
|
||||
onClose={close}
|
||||
/>
|
||||
<layer-group
|
||||
{/* <layer-group
|
||||
ref={group}
|
||||
ex={layerGroupExtends}
|
||||
loc={[STATUS_BAR_WIDTH, 0, MAP_WIDTH, MAP_HEIGHT]}
|
||||
@ -410,7 +402,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
<layer layer="event" zIndex={30}></layer>
|
||||
<layer layer="fg" zIndex={40}></layer>
|
||||
<layer layer="fg2" zIndex={50}></layer>
|
||||
</layer-group>
|
||||
</layer-group> */}
|
||||
<Tip
|
||||
ref={tip}
|
||||
zIndex={40}
|
||||
@ -418,7 +410,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
pad={[12, 6]}
|
||||
corner={16}
|
||||
/>
|
||||
<sprite
|
||||
<custom
|
||||
loc={[STATUS_BAR_WIDTH, 0, MAP_WIDTH, 64]}
|
||||
render={renderTop}
|
||||
alpha={topAlpha.value}
|
||||
@ -428,7 +420,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
onLeave={leaveTop}
|
||||
onClick={clickTop}
|
||||
/>
|
||||
<sprite
|
||||
<custom
|
||||
loc={[STATUS_BAR_WIDTH, MAP_HEIGHT - 64, MAP_WIDTH, 64]}
|
||||
render={renderBottom}
|
||||
alpha={bottomAlpha.value}
|
||||
@ -554,7 +546,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
loc={loc3}
|
||||
anc={[0.5, 0.5]}
|
||||
cursor="pointer"
|
||||
onClick={resetCamera}
|
||||
// onClick={resetCamera}
|
||||
/>
|
||||
</container>
|
||||
</container>
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
import {
|
||||
ExcitationCurve,
|
||||
excited,
|
||||
IAnimatable,
|
||||
IExcitableController,
|
||||
ITransition,
|
||||
Transition
|
||||
} from '@motajs/animate';
|
||||
import { logger } from '@motajs/common';
|
||||
import { IRenderItem, IRenderTreeRoot } from '@motajs/render';
|
||||
import { Hotkey, gameKey } from '@motajs/system';
|
||||
import { loading } from '@user/data-base';
|
||||
import { TimingFn, Transition } from 'mutate-animate';
|
||||
import {
|
||||
ComponentInternalInstance,
|
||||
getCurrentInstance,
|
||||
@ -107,7 +116,7 @@ export interface ITransitionedController<T> {
|
||||
* 设置动画的速率曲线
|
||||
* @param timing 速率曲线
|
||||
*/
|
||||
mode(timing: TimingFn): void;
|
||||
mode(timing: ExcitationCurve): void;
|
||||
|
||||
/**
|
||||
* 设置动画的动画时长
|
||||
@ -117,37 +126,29 @@ export interface ITransitionedController<T> {
|
||||
}
|
||||
|
||||
class RenderTransition implements ITransitionedController<number> {
|
||||
private static key: number = 0;
|
||||
|
||||
private readonly key: string = `$${RenderTransition.key++}`;
|
||||
|
||||
public readonly ref: Ref<number>;
|
||||
|
||||
set value(v: number) {
|
||||
this.transition.transition(this.key, v);
|
||||
this.set(v);
|
||||
}
|
||||
get value() {
|
||||
return this.transition.value[this.key];
|
||||
return this.ref.value;
|
||||
}
|
||||
|
||||
constructor(
|
||||
value: number,
|
||||
public readonly transition: Transition,
|
||||
public readonly transition: ITransition,
|
||||
public time: number,
|
||||
public curve: TimingFn
|
||||
public curve: ExcitationCurve
|
||||
) {
|
||||
this.ref = ref(value);
|
||||
transition.value[this.key] = value;
|
||||
transition.ticker.add(() => {
|
||||
this.ref.value = transition.value[this.key];
|
||||
});
|
||||
}
|
||||
|
||||
set(value: number, time: number = this.time): void {
|
||||
this.transition.time(time).mode(this.curve).transition(this.key, value);
|
||||
this.transition.curve(this.curve).transition(this.ref).to(value, time);
|
||||
}
|
||||
|
||||
mode(timing: TimingFn): void {
|
||||
mode(timing: ExcitationCurve): void {
|
||||
this.curve = timing;
|
||||
}
|
||||
|
||||
@ -166,6 +167,13 @@ class RenderColorTransition implements ITransitionedController<string> {
|
||||
private readonly keyB: string = `$colorB${RenderColorTransition.key++}`;
|
||||
private readonly keyA: string = `$colorA${RenderColorTransition.key++}`;
|
||||
|
||||
private readonly rValue: IAnimatable;
|
||||
private readonly gValue: IAnimatable;
|
||||
private readonly bValue: IAnimatable;
|
||||
private readonly aValue: IAnimatable;
|
||||
|
||||
private readonly controller: IExcitableController<number> | null = null;
|
||||
|
||||
public readonly ref: Ref<string>;
|
||||
|
||||
set value(v: string) {
|
||||
@ -177,26 +185,32 @@ class RenderColorTransition implements ITransitionedController<string> {
|
||||
|
||||
constructor(
|
||||
value: string,
|
||||
public readonly transition: Transition,
|
||||
public readonly transition: ITransition,
|
||||
public time: number,
|
||||
public curve: TimingFn
|
||||
public curve: ExcitationCurve
|
||||
) {
|
||||
this.ref = ref(value);
|
||||
const [r, g, b, a] = this.decodeColor(value);
|
||||
transition.value[this.keyR] = r;
|
||||
transition.value[this.keyG] = g;
|
||||
transition.value[this.keyB] = b;
|
||||
transition.value[this.keyA] = a;
|
||||
transition.ticker.add(() => {
|
||||
this.ref.value = this.encodeColor();
|
||||
});
|
||||
this.rValue = { value: r };
|
||||
this.gValue = { value: g };
|
||||
this.bValue = { value: b };
|
||||
this.aValue = { value: a };
|
||||
if (!transition.excitation) {
|
||||
logger.warn(94, 'transitionedColor');
|
||||
} else {
|
||||
this.controller = transition.excitation.add(
|
||||
excited(() => {
|
||||
this.ref.value = this.encodeColor();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
set(value: string, time: number = this.time): void {
|
||||
this.transitionColor(this.decodeColor(value), time);
|
||||
}
|
||||
|
||||
mode(timing: TimingFn): void {
|
||||
mode(timing: ExcitationCurve): void {
|
||||
this.curve = timing;
|
||||
}
|
||||
|
||||
@ -206,12 +220,15 @@ class RenderColorTransition implements ITransitionedController<string> {
|
||||
|
||||
private transitionColor([r, g, b, a]: ColorRGBA, time: number) {
|
||||
this.transition
|
||||
.mode(this.curve)
|
||||
.time(time)
|
||||
.transition(this.keyR, r)
|
||||
.transition(this.keyG, g)
|
||||
.transition(this.keyB, b)
|
||||
.transition(this.keyA, a);
|
||||
.curve(this.curve)
|
||||
.transition(this.rValue)
|
||||
.to(r, time)
|
||||
.transition(this.gValue)
|
||||
.to(g, time)
|
||||
.transition(this.bValue)
|
||||
.to(b, time)
|
||||
.transition(this.aValue)
|
||||
.to(a, time);
|
||||
}
|
||||
|
||||
private decodeColor(color: string): ColorRGBA {
|
||||
@ -272,31 +289,37 @@ class RenderColorTransition implements ITransitionedController<string> {
|
||||
}
|
||||
|
||||
private encodeColor() {
|
||||
const r = this.transition.value[this.keyR];
|
||||
const g = this.transition.value[this.keyG];
|
||||
const b = this.transition.value[this.keyB];
|
||||
const a = this.transition.value[this.keyA];
|
||||
const r = this.rValue.value;
|
||||
const g = this.gValue.value;
|
||||
const b = this.bValue.value;
|
||||
const a = this.aValue.value;
|
||||
return `rgba(${r},${g},${b},${a})`;
|
||||
}
|
||||
}
|
||||
|
||||
const transitionMap = new Map<ComponentInternalInstance, Transition>();
|
||||
const transitionMap = new Map<ComponentInternalInstance, ITransition>();
|
||||
|
||||
function checkTransition() {
|
||||
const instance = getCurrentInstance();
|
||||
if (!instance) return null;
|
||||
const root = instance.root;
|
||||
if (!root) return null;
|
||||
const el = root.vnode.el as IRenderItem;
|
||||
const renderer = el.parent as IRenderTreeRoot;
|
||||
if (!renderer) return null;
|
||||
if (instance.isUnmounted) {
|
||||
const tran = transitionMap.get(instance);
|
||||
tran?.ticker.destroy();
|
||||
tran?.destroy();
|
||||
transitionMap.delete(instance);
|
||||
return null;
|
||||
}
|
||||
if (!transitionMap.has(instance)) {
|
||||
const tran = new Transition();
|
||||
tran.bindExcitation(renderer.excitation);
|
||||
transitionMap.set(instance, tran);
|
||||
onUnmounted(() => {
|
||||
transitionMap.delete(instance);
|
||||
tran.ticker.destroy();
|
||||
tran.destroy();
|
||||
});
|
||||
}
|
||||
const tran = transitionMap.get(instance);
|
||||
@ -320,7 +343,7 @@ function checkTransition() {
|
||||
export function transitioned(
|
||||
value: number,
|
||||
time: number,
|
||||
curve: TimingFn
|
||||
curve: ExcitationCurve
|
||||
): ITransitionedController<number> | null {
|
||||
const tran = checkTransition();
|
||||
if (!tran) return null;
|
||||
@ -344,7 +367,7 @@ export function transitioned(
|
||||
export function transitionedColor(
|
||||
color: string,
|
||||
time: number,
|
||||
curve: TimingFn
|
||||
curve: ExcitationCurve
|
||||
): ITransitionedController<string> | null {
|
||||
const tran = checkTransition();
|
||||
if (!tran) return null;
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { onUnmounted } from 'vue';
|
||||
import { WeatherController } from '../weather';
|
||||
import { IRenderTreeRoot } from '@motajs/render';
|
||||
|
||||
export function useWeather(): [WeatherController] {
|
||||
const weather = new WeatherController();
|
||||
export function useWeather(renderer: IRenderTreeRoot): [WeatherController] {
|
||||
const weather = new WeatherController(renderer);
|
||||
|
||||
onUnmounted(() => {
|
||||
weather.destroy();
|
||||
|
||||
@ -1,19 +1,21 @@
|
||||
import { RenderItem } from '@motajs/render';
|
||||
import { IRenderTreeRoot, RenderItem } from '@motajs/render';
|
||||
import { IWeather, IWeatherController, IWeatherInstance } from './types';
|
||||
import { logger } from '@motajs/common';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { Ticker } from 'mutate-animate';
|
||||
import { IExcitable } from '@motajs/animate';
|
||||
|
||||
type WeatherConstructor = new () => IWeather;
|
||||
|
||||
export class WeatherController implements IWeatherController {
|
||||
// todo: refactor?
|
||||
|
||||
export class WeatherController
|
||||
implements IWeatherController, IExcitable<number>
|
||||
{
|
||||
/** 暴露到全局的控制器 */
|
||||
static extern: Map<string, IWeatherController> = new Map();
|
||||
/** 注册的天气 */
|
||||
static weathers: Map<string, WeatherConstructor> = new Map();
|
||||
|
||||
private static ticker: Ticker = new Ticker();
|
||||
|
||||
/** 暴露至全局的 id */
|
||||
private externId?: string;
|
||||
/** 天气元素纵深 */
|
||||
@ -23,13 +25,13 @@ export class WeatherController implements IWeatherController {
|
||||
|
||||
container: RenderItem | null = null;
|
||||
|
||||
constructor() {
|
||||
WeatherController.ticker.add(this.tick);
|
||||
constructor(readonly renderer: IRenderTreeRoot) {
|
||||
renderer.delegateExcitable(this);
|
||||
}
|
||||
|
||||
private tick = (time: number) => {
|
||||
this.active.forEach(v => v.weather.tick(time));
|
||||
};
|
||||
excited(payload: number): void {
|
||||
this.active.forEach(v => v.weather.tick(payload));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置天气元素纵深,第一个天气会被设置为 `zIndex`,之后依次是 `zIndex+1` `zIndex+2` ...
|
||||
@ -111,7 +113,6 @@ export class WeatherController implements IWeatherController {
|
||||
|
||||
destroy() {
|
||||
this.clearWeather();
|
||||
WeatherController.ticker.remove(this.tick);
|
||||
if (!isNil(this.externId)) {
|
||||
WeatherController.extern.delete(this.externId);
|
||||
}
|
||||
@ -138,8 +139,7 @@ export class WeatherController implements IWeatherController {
|
||||
export class WeatherInstance<
|
||||
R extends RenderItem = RenderItem,
|
||||
T extends IWeather<R> = IWeather<R>
|
||||
> implements IWeatherInstance<R, T>
|
||||
{
|
||||
> implements IWeatherInstance<R, T> {
|
||||
constructor(
|
||||
readonly weather: T,
|
||||
readonly element: R
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import {
|
||||
MotaOffscreenCanvas2D,
|
||||
Sprite,
|
||||
CustomRenderItem,
|
||||
SizedCanvasImageSource
|
||||
} from '@motajs/render';
|
||||
import { Weather } from '../weather';
|
||||
|
||||
export abstract class CloudLike extends Weather<Sprite> {
|
||||
export abstract class CloudLike extends Weather<CustomRenderItem> {
|
||||
/** 不透明度 */
|
||||
private alpha: number = 0;
|
||||
/** 水平速度 */
|
||||
@ -78,8 +78,8 @@ export abstract class CloudLike extends Weather<Sprite> {
|
||||
this.cy %= this.image.height;
|
||||
}
|
||||
|
||||
createElement(level: number): Sprite {
|
||||
const element = new Sprite('static', true);
|
||||
createElement(level: number): CustomRenderItem {
|
||||
const element = new CustomRenderItem(true);
|
||||
element.setRenderFn(canvas => this.drawImage(canvas));
|
||||
this.maxSpeed = Math.sqrt(level) * 100;
|
||||
this.vx = ((Math.random() - 0.5) * this.maxSpeed) / 2;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { MotaOffscreenCanvas2D, Sprite } from '@motajs/render';
|
||||
import { MotaOffscreenCanvas2D, CustomRenderItem } from '@motajs/render';
|
||||
import { Weather } from '../weather';
|
||||
import { clamp } from 'lodash-es';
|
||||
|
||||
export class SunWeather extends Weather<Sprite> {
|
||||
export class SunWeather extends Weather<CustomRenderItem> {
|
||||
/** 阳光图片 */
|
||||
private image: HTMLImageElement | null = null;
|
||||
/** 阳光图片的不透明度 */
|
||||
@ -41,8 +41,8 @@ export class SunWeather extends Weather<Sprite> {
|
||||
}
|
||||
}
|
||||
|
||||
createElement(level: number): Sprite {
|
||||
const element = new Sprite('static', true);
|
||||
createElement(level: number): CustomRenderItem {
|
||||
const element = new CustomRenderItem(true);
|
||||
element.setRenderFn(canvas => this.drawSun(canvas));
|
||||
this.maxAlpha = level / 10;
|
||||
this.minAlpha = level / 20;
|
||||
|
||||
@ -2,15 +2,7 @@ import EventEmitter from 'eventemitter3';
|
||||
import { backDir, toDir } from './utils';
|
||||
import { loading } from '@user/data-base';
|
||||
import type { RenderAdapter } from '@motajs/render';
|
||||
import type {
|
||||
FloorLayer,
|
||||
FloorViewport,
|
||||
HeroKeyMover,
|
||||
HeroRenderer,
|
||||
Layer,
|
||||
LayerFloorBinder,
|
||||
LayerMovingRenderable
|
||||
} from '@user/client-modules';
|
||||
import type { HeroKeyMover } from '@user/client-modules';
|
||||
import { sleep } from '@motajs/common';
|
||||
import { fromDirectionString, state } from '..';
|
||||
|
||||
@ -235,176 +227,7 @@ export abstract class ObjectMoverBase extends EventEmitter<EObjectMovingEvent> {
|
||||
}
|
||||
}
|
||||
|
||||
const enum BlockMoveCode {
|
||||
Step
|
||||
}
|
||||
|
||||
export class BlockMover extends ObjectMoverBase {
|
||||
/** 楼层渲染适配器,用于显示动画 */
|
||||
static adapter?: RenderAdapter<Layer>;
|
||||
|
||||
x: number;
|
||||
y: number;
|
||||
floorId: FloorIds;
|
||||
layer: FloorLayer;
|
||||
|
||||
/** 本次移动中需要进行动画移动的楼层渲染组件 */
|
||||
private layerItems: Layer[] = [];
|
||||
/** 本次移动过程中的移动renderable实例 */
|
||||
private renderable?: LayerMovingRenderable;
|
||||
/** 本次移动的图块id */
|
||||
private blockNum: number = 0;
|
||||
|
||||
constructor(
|
||||
x: number,
|
||||
y: number,
|
||||
floorId: FloorIds,
|
||||
layer: FloorLayer,
|
||||
dir: Dir = 'down'
|
||||
) {
|
||||
super();
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.floorId = floorId;
|
||||
this.moveDir = dir;
|
||||
this.layer = layer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定移动点
|
||||
* @param x 绑定点横坐标
|
||||
* @param y 绑定点纵坐标
|
||||
* @param floorId 绑定点楼层
|
||||
* @returns 是否绑定成功,例如如果当前绑定点正在移动,那么就会绑定失败
|
||||
*/
|
||||
bind(
|
||||
x: number,
|
||||
y: number,
|
||||
floorId: FloorIds,
|
||||
layer: FloorLayer,
|
||||
dir: Dir = 'down'
|
||||
) {
|
||||
if (this.moving) return false;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.floorId = floorId;
|
||||
this.moveDir = dir;
|
||||
this.layer = layer;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async onMoveStart(_controller: IMoveController): Promise<void> {
|
||||
const adapter = BlockMover.adapter;
|
||||
if (adapter) {
|
||||
const list = adapter.items;
|
||||
const items = [...list].filter(v => {
|
||||
if (v.layer !== this.layer) return false;
|
||||
const ex = v.getExtends('floor-binder') as LayerFloorBinder;
|
||||
if (!ex) return false;
|
||||
return ex.getFloor() === core.status.floorId;
|
||||
});
|
||||
this.layerItems = items;
|
||||
}
|
||||
|
||||
let blockNum: number = 0;
|
||||
if (this.layer === 'event') {
|
||||
blockNum = core.status.maps[this.floorId].map[this.y][this.x];
|
||||
} else {
|
||||
const array = core.maps._getBgFgMapArray(this.layer, this.floorId);
|
||||
blockNum = array[this.y][this.x];
|
||||
}
|
||||
this.blockNum = blockNum;
|
||||
|
||||
Mota.r(() => {
|
||||
const { Layer } = Mota.require('@user/client-modules');
|
||||
const r = Layer.getMovingRenderable(blockNum, this.x, this.y);
|
||||
|
||||
if (r) {
|
||||
this.renderable = r;
|
||||
this.layerItems.forEach(v => {
|
||||
v.moving.add(r);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (this.layer === 'event') {
|
||||
core.removeBlock(this.x, this.y, this.floorId);
|
||||
}
|
||||
}
|
||||
|
||||
protected async onMoveEnd(_controller: IMoveController): Promise<void> {
|
||||
if (this.renderable) {
|
||||
this.layerItems.forEach(v => {
|
||||
v.moving.delete(this.renderable!);
|
||||
});
|
||||
}
|
||||
|
||||
this.layerItems = [];
|
||||
this.renderable = void 0;
|
||||
|
||||
if (this.layer === 'event') {
|
||||
core.setBlock(this.blockNum as AllNumbers, this.x, this.y);
|
||||
}
|
||||
}
|
||||
|
||||
protected async onStepStart(
|
||||
step: MoveStepDir,
|
||||
_controller: IMoveController
|
||||
): Promise<BlockMoveCode> {
|
||||
await this.moveAnimate(step);
|
||||
const { x: dx, y: dy } = core.utils.scan2[this.moveDir];
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
|
||||
return BlockMoveCode.Step;
|
||||
}
|
||||
|
||||
protected async onStepEnd(
|
||||
_step: MoveStepDir,
|
||||
_code: BlockMoveCode,
|
||||
_controller: IMoveController
|
||||
): Promise<void> {}
|
||||
|
||||
protected onSetMoveSpeed(
|
||||
_speed: number,
|
||||
_controller: IMoveController
|
||||
): void {}
|
||||
|
||||
private moveAnimate(_step: MoveStepDir) {
|
||||
const layer = this.layerItems[0];
|
||||
if (!layer) return;
|
||||
if (!this.renderable) return;
|
||||
const data = this.renderable;
|
||||
const fx = this.x;
|
||||
const fy = this.y;
|
||||
const { x: dx, y: dy } = core.utils.scan2[this.moveDir];
|
||||
const start = Date.now();
|
||||
const replay = core.status.replay.speed ?? 1;
|
||||
const time = replay === 24 ? 1 : this.moveSpeed / replay;
|
||||
|
||||
return new Promise<void>(res => {
|
||||
layer.delegateTicker(
|
||||
() => {
|
||||
const now = Date.now() - start;
|
||||
const progress = now / time;
|
||||
data.x = fx + dx * progress;
|
||||
data.y = fy + dy * progress;
|
||||
this.layerItems.forEach(v => {
|
||||
v.update(v);
|
||||
});
|
||||
},
|
||||
this.moveSpeed,
|
||||
() => {
|
||||
data.x = fx + dx;
|
||||
data.y = fy + dy;
|
||||
data.zIndex = fy + dy;
|
||||
res();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
// todo: refactor
|
||||
|
||||
interface CanMoveStatus {
|
||||
/** 由CannotIn和CannotOut计算出的信息,不可移动时不会触发触发器 */
|
||||
@ -423,11 +246,6 @@ const enum HeroMoveCode {
|
||||
}
|
||||
|
||||
export class HeroMover extends ObjectMoverBase {
|
||||
/** 勇士渲染适配器,用于等待动画等操作 */
|
||||
static adapter?: RenderAdapter<HeroRenderer>;
|
||||
/** 视角适配器 */
|
||||
static viewport?: RenderAdapter<FloorViewport>;
|
||||
|
||||
/** 当前移动是否忽略地形 */
|
||||
private ignoreTerrain: boolean = false;
|
||||
/** 当前移动是否不计入录像 */
|
||||
@ -469,8 +287,6 @@ export class HeroMover extends ObjectMoverBase {
|
||||
|
||||
protected async onMoveStart(controller: IMoveController): Promise<void> {
|
||||
this.beforeMoveSpeed = this.moveSpeed;
|
||||
const viewport = HeroMover.viewport;
|
||||
if (!viewport) return;
|
||||
if (!core.isReplaying() || core.status.replay.speed <= 12) {
|
||||
state.hero.startMove();
|
||||
}
|
||||
@ -483,21 +299,19 @@ export class HeroMover extends ObjectMoverBase {
|
||||
if (firstDir && firstDir !== 'backward' && firstDir !== 'forward') {
|
||||
const data = this.checkCanMove(x, y, toDir(firstDir as Dir));
|
||||
if (data.canMove && !data.noPass) {
|
||||
viewport.sync('startMove');
|
||||
// viewport.sync('startMove');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
viewport.sync('startMove');
|
||||
// viewport.sync('startMove');
|
||||
}
|
||||
}
|
||||
|
||||
protected async onMoveEnd(controller: IMoveController): Promise<void> {
|
||||
this.moveSpeed = this.beforeMoveSpeed;
|
||||
this.onSetMoveSpeed(this.moveSpeed, controller);
|
||||
const viewport = HeroMover.viewport;
|
||||
if (!viewport) return;
|
||||
await state.hero.endMove();
|
||||
viewport.sync('endMove');
|
||||
// viewport.sync('endMove');
|
||||
core.clearContinueAutomaticRoute();
|
||||
core.stopAutomaticRoute();
|
||||
}
|
||||
@ -620,11 +434,11 @@ export class HeroMover extends ObjectMoverBase {
|
||||
_showDir: Dir,
|
||||
moveDir: Dir2
|
||||
) {
|
||||
const viewport = HeroMover.viewport;
|
||||
if (!viewport) return;
|
||||
// const viewport = HeroMover.viewport;
|
||||
// if (!viewport) return;
|
||||
const replay = core.status.replay.speed;
|
||||
const speed = replay === 24 ? 1 : this.moveSpeed / replay;
|
||||
viewport.all('moveTo', x, y, speed * 1.6);
|
||||
// viewport.all('moveTo', x, y, speed * 1.6);
|
||||
const replaying = core.isReplaying();
|
||||
if (replaying) {
|
||||
if (core.status.replay.speed > 12) {
|
||||
@ -706,13 +520,3 @@ loading.once('coreInit', () => {
|
||||
heroMoveCollection.keyMover = keyMover;
|
||||
});
|
||||
});
|
||||
|
||||
// Adapter初始化
|
||||
loading.once('coreInit', () => {
|
||||
if (main.replayChecking || main.mode === 'editor') return;
|
||||
const Adapter = Mota.require('@motajs/render').RenderAdapter;
|
||||
const viewport = Adapter.get<FloorViewport>('viewport');
|
||||
const layerAdapter = Adapter.get<Layer>('layer');
|
||||
HeroMover.viewport = viewport;
|
||||
BlockMover.adapter = layerAdapter;
|
||||
});
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import type { RenderAdapter } from '@motajs/render';
|
||||
import type { TimingFn } from 'mutate-animate';
|
||||
import {
|
||||
BlockMover,
|
||||
fromDirectionString,
|
||||
heroMoveCollection,
|
||||
MoveStep,
|
||||
@ -9,38 +7,13 @@ import {
|
||||
} from '@user/data-state';
|
||||
import { hook, loading } from '@user/data-base';
|
||||
import { Patch, PatchClass } from '@motajs/legacy-common';
|
||||
import type {
|
||||
LayerDoorAnimate,
|
||||
LayerGroupAnimate,
|
||||
FloorViewport,
|
||||
LayerGroup
|
||||
} from '@user/client-modules';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
// 向后兼容用,会充当两个版本间过渡的作用
|
||||
|
||||
interface Adapters {
|
||||
'door-animate'?: RenderAdapter<LayerDoorAnimate>;
|
||||
animate?: RenderAdapter<LayerGroupAnimate>;
|
||||
viewport?: RenderAdapter<FloorViewport>;
|
||||
}
|
||||
|
||||
const adapters: Adapters = {};
|
||||
|
||||
export function initFallback() {
|
||||
let fallbackIds: number = 1e8;
|
||||
|
||||
if (!main.replayChecking && main.mode === 'play') {
|
||||
const Adapter = Mota.require('@motajs/render').RenderAdapter;
|
||||
const doorAnimate = Adapter.get<LayerDoorAnimate>('door-animate');
|
||||
const animate = Adapter.get<LayerGroupAnimate>('animate');
|
||||
const viewport = Adapter.get<FloorViewport>('viewport');
|
||||
|
||||
adapters['door-animate'] = doorAnimate;
|
||||
adapters['animate'] = animate;
|
||||
adapters['viewport'] = viewport;
|
||||
}
|
||||
|
||||
const { mover: heroMover } = heroMoveCollection;
|
||||
|
||||
// ----- 工具函数
|
||||
@ -86,8 +59,7 @@ export function initFallback() {
|
||||
|
||||
Mota.r(() => {
|
||||
// ----- 引入
|
||||
const { MotaRenderer: Renderer } = Mota.require('@motajs/render');
|
||||
const { Camera } = Mota.require('@user/client-modules');
|
||||
const { mainRenderer } = Mota.require('@user/client-modules');
|
||||
const Animation = Mota.require('MutateAnimate');
|
||||
|
||||
const patch = new Patch(PatchClass.Control);
|
||||
@ -177,7 +149,7 @@ export function initFallback() {
|
||||
noGather?: boolean
|
||||
) {
|
||||
if (!core.status.hero) return;
|
||||
// @ts-ignore
|
||||
// @ts-expect-error todo
|
||||
core.status.hero.loc[name] = value;
|
||||
if (name === 'direction') {
|
||||
const dir = fromDirectionString(value as Dir);
|
||||
@ -375,9 +347,9 @@ export function initFallback() {
|
||||
function (x: number, y: number, id: AllIds, callback?: () => void) {
|
||||
id = id || '';
|
||||
if (
|
||||
// @ts-ignore
|
||||
// @ts-expect-error todo
|
||||
(isNil(core.material.icons.animates[id]) &&
|
||||
// @ts-ignore
|
||||
// @ts-expect-error todo
|
||||
isNil(core.material.icons.npc48[id])) ||
|
||||
!isNil(core.getBlock(x, y))
|
||||
) {
|
||||
@ -427,10 +399,10 @@ export function initFallback() {
|
||||
name: AnimationIds,
|
||||
x: number,
|
||||
y: number,
|
||||
alignWindow?: boolean,
|
||||
_alignWindow?: boolean,
|
||||
callback?: () => void
|
||||
) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error todo
|
||||
name = core.getMappedName(name);
|
||||
|
||||
// 正在播放录像:不显示动画
|
||||
@ -444,24 +416,24 @@ export function initFallback() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
adapters.animate
|
||||
?.all(
|
||||
'drawAnimate',
|
||||
name,
|
||||
x * 32 + 16,
|
||||
y * 32 + 16,
|
||||
alignWindow ?? false
|
||||
)
|
||||
.then(() => {
|
||||
callback?.();
|
||||
});
|
||||
// adapters.animate
|
||||
// ?.all(
|
||||
// 'drawAnimate',
|
||||
// name,
|
||||
// x * 32 + 16,
|
||||
// y * 32 + 16,
|
||||
// alignWindow ?? false
|
||||
// )
|
||||
// .then(() => {
|
||||
// callback?.();
|
||||
// });
|
||||
}
|
||||
);
|
||||
|
||||
patch3.add(
|
||||
'drawHeroAnimate',
|
||||
function (name: AnimationIds, callback?: () => void) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error todo
|
||||
name = core.getMappedName(name);
|
||||
|
||||
// 正在播放录像或动画不存在:不显示动画
|
||||
@ -470,9 +442,9 @@ export function initFallback() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
adapters.animate?.global('drawHeroAnimate', name).then(() => {
|
||||
callback?.();
|
||||
});
|
||||
// adapters.animate?.global('drawHeroAnimate', name).then(() => {
|
||||
// callback?.();
|
||||
// });
|
||||
}
|
||||
);
|
||||
|
||||
@ -495,31 +467,31 @@ export function initFallback() {
|
||||
callback?.();
|
||||
return;
|
||||
}
|
||||
const mover = new BlockMover(
|
||||
x,
|
||||
y,
|
||||
core.status.floorId,
|
||||
'event'
|
||||
);
|
||||
const moveSteps = getMoveSteps(steps);
|
||||
const resolved = moveSteps.map<MoveStep>(v => {
|
||||
if (v.startsWith('speed')) {
|
||||
return { type: 'speed', value: Number(v.slice(6)) };
|
||||
} else {
|
||||
return { type: 'dir', value: v as Move2 };
|
||||
}
|
||||
});
|
||||
const start: MoveStep = { type: 'speed', value: time };
|
||||
mover.insertMove(...[start, ...resolved]);
|
||||
const controller = mover.startMove();
|
||||
// const mover = new BlockMover(
|
||||
// x,
|
||||
// y,
|
||||
// core.status.floorId,
|
||||
// 'event'
|
||||
// );
|
||||
// const moveSteps = getMoveSteps(steps);
|
||||
// const resolved = moveSteps.map<MoveStep>(v => {
|
||||
// if (v.startsWith('speed')) {
|
||||
// return { type: 'speed', value: Number(v.slice(6)) };
|
||||
// } else {
|
||||
// return { type: 'dir', value: v as Move2 };
|
||||
// }
|
||||
// });
|
||||
// const start: MoveStep = { type: 'speed', value: time };
|
||||
// mover.insertMove(...[start, ...resolved]);
|
||||
// const controller = mover.startMove();
|
||||
|
||||
if (controller) {
|
||||
await controller.onEnd;
|
||||
}
|
||||
// if (controller) {
|
||||
// await controller.onEnd;
|
||||
// }
|
||||
|
||||
if (!keep) {
|
||||
core.removeBlock(mover.x, mover.y);
|
||||
}
|
||||
// if (!keep) {
|
||||
// core.removeBlock(mover.x, mover.y);
|
||||
// }
|
||||
callback?.();
|
||||
}
|
||||
);
|
||||
@ -584,7 +556,7 @@ export function initFallback() {
|
||||
) {
|
||||
if (heroMover.moving) return;
|
||||
|
||||
adapters.viewport?.all('mutateTo', ex, ey, time);
|
||||
// adapters.viewport?.all('mutateTo', ex, ey, time);
|
||||
|
||||
const locked = core.status.lockControl;
|
||||
core.lockControl();
|
||||
@ -608,7 +580,7 @@ export function initFallback() {
|
||||
function (destX: number, destY: number, ignoreSteps: number) {
|
||||
const data = core.control.controldata;
|
||||
const success = data.moveDirectly(destX, destY, ignoreSteps);
|
||||
if (success) adapters.viewport?.all('mutateTo', destX, destY);
|
||||
// if (success) adapters.viewport?.all('mutateTo', destX, destY);
|
||||
return success;
|
||||
}
|
||||
);
|
||||
@ -622,45 +594,7 @@ export function initFallback() {
|
||||
time: number = 1,
|
||||
callback?: () => void
|
||||
) {
|
||||
const main = Renderer.get('render-main');
|
||||
const layer = main?.getElementById('layer-main') as LayerGroup;
|
||||
if (!layer) return;
|
||||
const camera = Camera.for(layer);
|
||||
camera.clearOperation();
|
||||
const translate = camera.addTranslate();
|
||||
|
||||
const animateTime =
|
||||
time / Math.max(core.status.replay.speed, 1);
|
||||
const animate = new Animation.Animation();
|
||||
animate
|
||||
.absolute()
|
||||
.time(1)
|
||||
.mode(Animation.linear())
|
||||
.move(core.bigmap.offsetX, core.bigmap.offsetY);
|
||||
animate.time(animateTime).move(x * 32, y * 32);
|
||||
|
||||
camera.applyTranslateAnimation(
|
||||
translate,
|
||||
animate,
|
||||
animateTime + 50
|
||||
);
|
||||
camera.transform = layer.camera;
|
||||
|
||||
const end = () => {
|
||||
core.bigmap.offsetX = x * 32;
|
||||
core.bigmap.offsetY = y * 32;
|
||||
camera.destroy();
|
||||
callback?.();
|
||||
};
|
||||
|
||||
const timeout = window.setTimeout(end, animateTime + 50);
|
||||
|
||||
const id = fallbackIds++;
|
||||
core.animateFrame.lastAsyncId = id;
|
||||
core.animateFrame.asyncId[id] = () => {
|
||||
end();
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
// todo
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@ -67,7 +67,7 @@ export class Animater implements IAnimater {
|
||||
private planning: boolean = false;
|
||||
|
||||
/** 当前绑定的激励源 */
|
||||
private excitation: IExcitation<number> | null = null;
|
||||
excitation: IExcitation<number> | null = null;
|
||||
/** 当前定义在绑定激励源上的可激励对象 */
|
||||
private controller: IExcitableController<number> | null = null;
|
||||
|
||||
@ -317,6 +317,22 @@ export class Animater implements IAnimater {
|
||||
return index;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.unbindExcitation();
|
||||
this.animatableStatus = null;
|
||||
this.currentAnimatable = null;
|
||||
this.planningStore.clear();
|
||||
this.groupStore.clear();
|
||||
this.whens.clear();
|
||||
this.afters.clear();
|
||||
this.executingGroupObj = null;
|
||||
this.executing.clear();
|
||||
this.executingMap.clear();
|
||||
this.planningStart.clear();
|
||||
this.whenBind = null;
|
||||
this.afterBind = null;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 动画执行
|
||||
|
||||
@ -5,7 +5,8 @@ import {
|
||||
IExcitableController,
|
||||
IExcitationVariator,
|
||||
ExcitationCurve,
|
||||
VariatorCurveMode
|
||||
VariatorCurveMode,
|
||||
IExcitationDivider
|
||||
} from './types';
|
||||
import { excited } from './utils';
|
||||
|
||||
@ -323,3 +324,63 @@ export class ExcitationVariator
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export class ExcitationDivider<T>
|
||||
extends ExcitationBase<T>
|
||||
implements IExcitationDivider<T>
|
||||
{
|
||||
divider: number = 1;
|
||||
source: IExcitation<T> | null = null;
|
||||
|
||||
/** 当前的激励对象控制器 */
|
||||
private controller: IExcitableController<T> | null = null;
|
||||
/** 当前的激励负载 */
|
||||
private nowPayload: T | null = null;
|
||||
/** 分频计数器 */
|
||||
private counter: number = 0;
|
||||
|
||||
payload(): T {
|
||||
if (!this.source) {
|
||||
logger.error(52);
|
||||
throw new Error('Expected an excitation binding');
|
||||
}
|
||||
return this.nowPayload ?? this.source.payload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 激励当前所有的激励源
|
||||
* @param payload 激励负载
|
||||
*/
|
||||
excite(payload: T) {
|
||||
this.counter++;
|
||||
if (this.counter >= this.divider) {
|
||||
this.counter = 0;
|
||||
this.nowPayload = payload;
|
||||
super.excite(payload);
|
||||
}
|
||||
}
|
||||
|
||||
bindExcitation(excitation: IExcitation<T>): void {
|
||||
this.unbindExcitation();
|
||||
this.source = excitation;
|
||||
this.divider = 1;
|
||||
this.counter = this.divider - 1;
|
||||
this.nowPayload = excitation.payload();
|
||||
const controller = excitation.add(
|
||||
excited(payload => this.excite(payload))
|
||||
);
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
unbindExcitation(): void {
|
||||
this.controller?.revoke();
|
||||
this.counter = 0;
|
||||
this.divider = 1;
|
||||
}
|
||||
|
||||
setDivider(divider: number): void {
|
||||
if (!this.source) return;
|
||||
this.divider = divider;
|
||||
this.counter = divider - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ interface ITransitionData {
|
||||
|
||||
export class Transition implements ITransition {
|
||||
/** 当前绑定的激励源 */
|
||||
private excitation: IExcitation<number> | null = null;
|
||||
excitation: IExcitation<number> | null = null;
|
||||
/** 当前定义在绑定激励源上的可激励对象 */
|
||||
private controller: IExcitableController<number> | null = null;
|
||||
|
||||
@ -64,7 +64,7 @@ export class Transition implements ITransition {
|
||||
return this;
|
||||
}
|
||||
|
||||
transite(animatable: IAnimatable): this {
|
||||
transition(animatable: IAnimatable): this {
|
||||
this.animatableStatus = animatable;
|
||||
return this;
|
||||
}
|
||||
@ -115,6 +115,12 @@ export class Transition implements ITransition {
|
||||
this.animatableStatus = null;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.controller?.revoke();
|
||||
this.unbindExcitation();
|
||||
this.animatableStatus = null;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 渐变执行
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
//#region 激励对象
|
||||
|
||||
/**
|
||||
* 动画速率曲线,输入时间完成度,输出一个值,时间完成度范围在 `0-1` 之间,而输出值没有范围限制。
|
||||
* 在不同场景下,速率曲线返回值的含义可能不同,有的可能表示动画完成度,有的可能表示绝对的坐标值。
|
||||
@ -43,6 +45,10 @@ export interface IExcitableController<T> {
|
||||
excite(payload: T): void;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 激励源
|
||||
|
||||
export interface IExcitation<T> {
|
||||
/**
|
||||
* 获取当前的激励负载
|
||||
@ -94,7 +100,7 @@ export interface IExcitationVariator extends IExcitation<number> {
|
||||
readonly source: IExcitation<number> | null;
|
||||
|
||||
/**
|
||||
* 绑定激励源对象,变速器将以此激励源为基础实施变速
|
||||
* 绑定激励源对象,变速器将以此激励源为基础实施变速。绑定后当前速度值会被设为 1。
|
||||
* @param excitation 绑定的激励源
|
||||
*/
|
||||
bindExcitation(excitation: IExcitation<number>): void;
|
||||
@ -128,6 +134,34 @@ export interface IExcitationVariator extends IExcitation<number> {
|
||||
endAllCurves(): void;
|
||||
}
|
||||
|
||||
export interface IExcitationDivider<T> extends IExcitation<T> {
|
||||
/** 分频器当前的分频比例 */
|
||||
readonly divider: number;
|
||||
/** 当前绑定的激励源 */
|
||||
readonly source: IExcitation<T> | null;
|
||||
|
||||
/**
|
||||
* 绑定激励源对象,分配器将以此激励源为基础实施分配。绑定后分频比例会设为 1。
|
||||
* @param excitation 绑定的激励源
|
||||
*/
|
||||
bindExcitation(excitation: IExcitation<T>): void;
|
||||
|
||||
/**
|
||||
* 取消绑定当前的激励源
|
||||
*/
|
||||
unbindExcitation(): void;
|
||||
|
||||
/**
|
||||
* 设置当前分频器的分配比例。设置后会在下一次激励源被激励时便触发一次激励,然后按照分配比例触发。
|
||||
* @param divider 分配比例
|
||||
*/
|
||||
setDivider(divider: number): void;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 动画类
|
||||
|
||||
export interface IAnimatable {
|
||||
/** 动画数值 */
|
||||
value: number;
|
||||
@ -173,6 +207,9 @@ export const enum EndRelation {
|
||||
}
|
||||
|
||||
export interface IAnimater extends IExcitable<number> {
|
||||
/** 渐变对象绑定的激励源 */
|
||||
readonly excitation: IExcitation<number> | null;
|
||||
|
||||
/**
|
||||
* 在动画执行器上绑定激励源
|
||||
* @param excitation 绑定的激励源
|
||||
@ -310,9 +347,17 @@ export interface IAnimater extends IExcitable<number> {
|
||||
* @param postTime 计划执行后的等待时长,等待这么长时间之后计划才真正结束
|
||||
*/
|
||||
planEnd(preTime?: number, postTime?: number): number;
|
||||
|
||||
/**
|
||||
* 销毁此动画对象
|
||||
*/
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export interface ITransition extends IExcitable<number> {
|
||||
/** 渐变对象绑定的激励源 */
|
||||
readonly excitation: IExcitation<number> | null;
|
||||
|
||||
/**
|
||||
* 在动画执行器上绑定激励源
|
||||
* @param excitation 绑定的激励源
|
||||
@ -334,7 +379,7 @@ export interface ITransition extends IExcitable<number> {
|
||||
* 绑定渐变对象,之后的渐变操作都会作用在此对象上
|
||||
* @param animatable 渐变对象
|
||||
*/
|
||||
transite(animatable: IAnimatable): this;
|
||||
transition(animatable: IAnimatable): this;
|
||||
|
||||
/**
|
||||
* 将当前绑定的值立刻缓慢渐变至目标值
|
||||
@ -351,7 +396,14 @@ export interface ITransition extends IExcitable<number> {
|
||||
wait(animatable: IAnimatable): Promise<void>;
|
||||
|
||||
/**
|
||||
* 释放当前绑定的渐变对象,防止绑定的渐变对象一直不会被垃圾回收
|
||||
* 释放当前使用 {@link transition} 绑定的渐变对象,防止当前被绑定的渐变对象一直不会被垃圾回收。
|
||||
*/
|
||||
revoke(): void;
|
||||
|
||||
/**
|
||||
* 销毁当前渐变执行器
|
||||
*/
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@ -250,15 +250,15 @@ export function stackCurve(curves: ExcitationCurve[]): GeneralExcitationCurve {
|
||||
/**
|
||||
* 对速率曲线归一化,此函数假设传入的曲线单调,会使用 `curve(0)` 和 `curve(1)` 作为最大值或最小值。
|
||||
*
|
||||
* - `f(0) > f(1)`: `g(x) = (f(x) - f(0)) / (f(0) - f(1))`
|
||||
* - `f(0) < f(1)`: `g(x) = (f(x) - f(1)) / (f(1) - f(0))`
|
||||
* - `f(0) > f(1)`: `g(x) = (f(x) - f(1)) / (f(0) - f(1))`
|
||||
* - `f(0) < f(1)`: `g(x) = (f(x) - f(0)) / (f(1) - f(0))`
|
||||
* - `f(0) = f(1)`: `g(x) = f(x)`
|
||||
* @param curve 需要归一化的曲线
|
||||
* @returns
|
||||
*/
|
||||
export function normalize(curve: ExcitationCurve): ExcitationCurve {
|
||||
const head = curve(1);
|
||||
const tail = curve(0);
|
||||
const head = curve(0);
|
||||
const tail = curve(1);
|
||||
if (head > tail) {
|
||||
const diff = head - tail;
|
||||
return p => (curve(p) - tail) / diff;
|
||||
|
||||
@ -13,8 +13,8 @@
|
||||
"11": "Cache depth cannot larger than 31.",
|
||||
"12": "Cannot move while status is not 'moving'. Call 'readyMove' first.",
|
||||
"13": "Cannot compile $1 shader. Error info: $2",
|
||||
"14": "",
|
||||
"15": "",
|
||||
"14": "Cannot register render tag, since the tag name has been used.",
|
||||
"15": "Render tag '$1' not registed.",
|
||||
"16": "Cannot find log message for $1 code $2.",
|
||||
"17": "Cannot use shader program for shader element that does not belong to it.",
|
||||
"18": "Cannot delete shader program for shader element that does not belong to it.",
|
||||
@ -51,6 +51,7 @@
|
||||
"49": "Cannot $1 on variator without excitation binding.",
|
||||
"50": "Expected a planEnd call after animation plan calling.",
|
||||
"51": "Animatable object cannot be animated by plans with the same start time.",
|
||||
"52": "To get divider payload, an excitation binding is expected.",
|
||||
"1201": "Floor-damage extension needs 'floor-binder' extension as dependency."
|
||||
},
|
||||
"warn": {
|
||||
@ -147,6 +148,7 @@
|
||||
"91": "Cannot add follower, since specified follower number $1 does not exist.",
|
||||
"92": "Followers can only be added when the last follower is not moving.",
|
||||
"93": "Followers can only be removed when the last follower is not moving.",
|
||||
"94": "Expecting an excitation binding when using '$1'",
|
||||
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
|
||||
}
|
||||
}
|
||||
|
||||
64
packages/render-vue/src/elements.ts
Normal file
64
packages/render-vue/src/elements.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { ReservedProps } from 'vue';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import {
|
||||
BezierProps,
|
||||
CirclesProps,
|
||||
CommentProps,
|
||||
ConatinerCustomProps,
|
||||
ContainerProps,
|
||||
CustomProps,
|
||||
EllipseProps,
|
||||
ImageProps,
|
||||
LineProps,
|
||||
PathProps,
|
||||
QuadraticProps,
|
||||
RectProps,
|
||||
RectRProps,
|
||||
ShaderProps,
|
||||
TextProps
|
||||
} from './props';
|
||||
import { ERenderItemEvent } from '@motajs/render';
|
||||
|
||||
export type WrapEventEmitterEvents<T extends EventEmitter.ValidEventTypes> =
|
||||
T extends string | symbol
|
||||
? T
|
||||
: {
|
||||
[P in keyof T]: T[P] extends any[]
|
||||
? (...args: T[P]) => void
|
||||
: (...args: any[]) => void;
|
||||
};
|
||||
|
||||
type MappingEvent<E extends ERenderItemEvent> = {
|
||||
[P in keyof WrapEventEmitterEvents<E> as P extends string
|
||||
? `on${Capitalize<P>}`
|
||||
: never]?: WrapEventEmitterEvents<E>[P];
|
||||
};
|
||||
|
||||
export type TagDefine<T extends object, E extends ERenderItemEvent> = T &
|
||||
MappingEvent<E> &
|
||||
ReservedProps;
|
||||
|
||||
declare module 'vue/jsx-runtime' {
|
||||
namespace JSX {
|
||||
export interface IntrinsicElements {
|
||||
container: TagDefine<ContainerProps, ERenderItemEvent>;
|
||||
'container-custom': TagDefine<
|
||||
ConatinerCustomProps,
|
||||
ERenderItemEvent
|
||||
>;
|
||||
shader: TagDefine<ShaderProps, ERenderItemEvent>;
|
||||
text: TagDefine<TextProps, ERenderItemEvent>;
|
||||
image: TagDefine<ImageProps, ERenderItemEvent>;
|
||||
comment: TagDefine<CommentProps, ERenderItemEvent>;
|
||||
custom: TagDefine<CustomProps, ERenderItemEvent>;
|
||||
'g-rect': TagDefine<RectProps, ERenderItemEvent>;
|
||||
'g-circle': TagDefine<CirclesProps, ERenderItemEvent>;
|
||||
'g-ellipse': TagDefine<EllipseProps, ERenderItemEvent>;
|
||||
'g-line': TagDefine<LineProps, ERenderItemEvent>;
|
||||
'g-bezier': TagDefine<BezierProps, ERenderItemEvent>;
|
||||
'g-quad': TagDefine<QuadraticProps, ERenderItemEvent>;
|
||||
'g-path': TagDefine<PathProps, ERenderItemEvent>;
|
||||
'g-rectr': TagDefine<RectRProps, ERenderItemEvent>;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,119 +0,0 @@
|
||||
import {
|
||||
ComponentOptionsMixin,
|
||||
defineComponent,
|
||||
DefineComponent,
|
||||
h,
|
||||
ReservedProps,
|
||||
VNodeProps
|
||||
} from 'vue';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import {
|
||||
BaseProps,
|
||||
BezierProps,
|
||||
CirclesProps,
|
||||
CommentProps,
|
||||
ConatinerCustomProps,
|
||||
ContainerProps,
|
||||
CustomProps,
|
||||
EllipseProps,
|
||||
ImageProps,
|
||||
LineProps,
|
||||
PathProps,
|
||||
QuadraticProps,
|
||||
RectProps,
|
||||
RectRProps,
|
||||
ShaderProps,
|
||||
SpriteProps,
|
||||
TextProps
|
||||
} from './props';
|
||||
import {
|
||||
ERenderItemEvent,
|
||||
RenderItem,
|
||||
ESpriteEvent,
|
||||
EContainerEvent,
|
||||
EShaderEvent,
|
||||
EImageEvent,
|
||||
ETextEvent,
|
||||
EGraphicItemEvent
|
||||
} from '@motajs/render';
|
||||
|
||||
export type WrapEventEmitterEvents<T extends EventEmitter.ValidEventTypes> =
|
||||
T extends string | symbol
|
||||
? T
|
||||
: {
|
||||
[P in keyof T]: T[P] extends any[]
|
||||
? (...args: T[P]) => void
|
||||
: (...args: any[]) => void;
|
||||
};
|
||||
|
||||
type MappingEvent<E extends ERenderItemEvent> = {
|
||||
[P in keyof WrapEventEmitterEvents<E> as P extends string
|
||||
? `on${Capitalize<P>}`
|
||||
: never]?: WrapEventEmitterEvents<E>[P];
|
||||
};
|
||||
|
||||
type _Define<P extends BaseProps, E extends ERenderItemEvent> = DefineComponent<
|
||||
P,
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
ComponentOptionsMixin,
|
||||
ComponentOptionsMixin,
|
||||
WrapEventEmitterEvents<E>,
|
||||
Exclude<keyof WrapEventEmitterEvents<E>, number | symbol>,
|
||||
VNodeProps,
|
||||
Readonly<P & MappingEvent<E>>
|
||||
>;
|
||||
|
||||
export type TagDefine<T extends object, E extends ERenderItemEvent> = T &
|
||||
MappingEvent<E> &
|
||||
ReservedProps;
|
||||
|
||||
declare module 'vue/jsx-runtime' {
|
||||
namespace JSX {
|
||||
export interface IntrinsicElements {
|
||||
sprite: TagDefine<SpriteProps, ESpriteEvent>;
|
||||
container: TagDefine<ContainerProps, EContainerEvent>;
|
||||
'container-custom': TagDefine<
|
||||
ConatinerCustomProps,
|
||||
EContainerEvent
|
||||
>;
|
||||
shader: TagDefine<ShaderProps, EShaderEvent>;
|
||||
text: TagDefine<TextProps, ETextEvent>;
|
||||
image: TagDefine<ImageProps, EImageEvent>;
|
||||
comment: TagDefine<CommentProps, ERenderItemEvent>;
|
||||
custom: TagDefine<CustomProps, ERenderItemEvent>;
|
||||
'g-rect': TagDefine<RectProps, EGraphicItemEvent>;
|
||||
'g-circle': TagDefine<CirclesProps, EGraphicItemEvent>;
|
||||
'g-ellipse': TagDefine<EllipseProps, EGraphicItemEvent>;
|
||||
'g-line': TagDefine<LineProps, EGraphicItemEvent>;
|
||||
'g-bezier': TagDefine<BezierProps, EGraphicItemEvent>;
|
||||
'g-quad': TagDefine<QuadraticProps, EGraphicItemEvent>;
|
||||
'g-path': TagDefine<PathProps, EGraphicItemEvent>;
|
||||
'g-rectr': TagDefine<RectRProps, EGraphicItemEvent>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface InstancedElementProp {
|
||||
item: RenderItem;
|
||||
}
|
||||
|
||||
export function wrapInstancedComponent<
|
||||
P extends BaseProps = BaseProps,
|
||||
E extends ERenderItemEvent = ERenderItemEvent,
|
||||
C extends RenderItem = RenderItem
|
||||
>(onCreate: (props: P) => C): _Define<P, E> {
|
||||
const Com = defineComponent((props, ctx) => {
|
||||
return () => {
|
||||
const p = {
|
||||
...props,
|
||||
...ctx.attrs,
|
||||
_item: onCreate
|
||||
};
|
||||
return h('custom', p, ctx.slots);
|
||||
};
|
||||
});
|
||||
return Com as _Define<P, E>;
|
||||
}
|
||||
@ -1,14 +1,6 @@
|
||||
import { ERenderItemEvent } from '@motajs/render';
|
||||
import { TagDefine } from './elements';
|
||||
import { BaseProps } from './props';
|
||||
|
||||
export type DefaultProps<
|
||||
P extends BaseProps = BaseProps,
|
||||
E extends ERenderItemEvent = ERenderItemEvent
|
||||
> = TagDefine<P, E>;
|
||||
|
||||
export * from './elements';
|
||||
export * from './map';
|
||||
export * from './props';
|
||||
export * from './renderer';
|
||||
export * from './tag';
|
||||
export * from './types';
|
||||
export * from './use';
|
||||
|
||||
@ -1,202 +0,0 @@
|
||||
import { ElementNamespace, VNodeProps } from 'vue';
|
||||
import { logger } from '@motajs/common';
|
||||
import {
|
||||
ERenderItemEvent,
|
||||
RenderItem,
|
||||
RenderItemPosition,
|
||||
Container,
|
||||
ContainerCustom,
|
||||
MotaRenderer,
|
||||
Sprite,
|
||||
Shader,
|
||||
Comment,
|
||||
ETextEvent,
|
||||
Image,
|
||||
Text,
|
||||
BezierCurve,
|
||||
Circle,
|
||||
Ellipse,
|
||||
Line,
|
||||
Path,
|
||||
QuadraticCurve,
|
||||
Rect,
|
||||
RectR
|
||||
} from '@motajs/render';
|
||||
|
||||
type OnItemCreate<
|
||||
E extends ERenderItemEvent = ERenderItemEvent,
|
||||
T extends RenderItem<E> = RenderItem<E>
|
||||
> = (
|
||||
namespace?: ElementNamespace,
|
||||
isCustomizedBuiltIn?: string,
|
||||
vnodeProps?: (VNodeProps & { [key: string]: any }) | null
|
||||
) => T;
|
||||
|
||||
class RenderTagMap {
|
||||
private map: Map<string, OnItemCreate> = new Map();
|
||||
|
||||
/**
|
||||
* 注册一个标签,每个标签对应一类元素,重复注册会覆盖之前的
|
||||
* @param tag 标签名称
|
||||
* @param ele 对应的元素类或其构造器
|
||||
*/
|
||||
register<E extends ERenderItemEvent, T extends RenderItem<E>>(
|
||||
tag: string,
|
||||
onCreate: OnItemCreate<E, T>
|
||||
) {
|
||||
if (this.map.has(tag)) {
|
||||
logger.warn(34, tag);
|
||||
}
|
||||
this.map.set(tag, onCreate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个标签对应的元素构造器
|
||||
* @param tag 标签名
|
||||
*/
|
||||
get<E extends ERenderItemEvent, T extends RenderItem<E>>(
|
||||
tag: string
|
||||
): OnItemCreate<E, T> | undefined {
|
||||
return this.map.get(tag) as OnItemCreate<E, T>;
|
||||
}
|
||||
}
|
||||
|
||||
export const tagMap = new RenderTagMap();
|
||||
|
||||
export const standardElement = (
|
||||
Item: new (
|
||||
type: RenderItemPosition,
|
||||
cache?: boolean,
|
||||
fall?: boolean
|
||||
) => RenderItem
|
||||
) => {
|
||||
return (_0: any, _1: any, props?: any) => {
|
||||
if (!props) return new Item('static');
|
||||
else {
|
||||
const {
|
||||
type = 'static',
|
||||
cache = true,
|
||||
fall = false,
|
||||
nocache = false
|
||||
} = props;
|
||||
return new Item(type, cache && !nocache, fall);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const standardElementNoCache = (
|
||||
Item: new (
|
||||
type: RenderItemPosition,
|
||||
cache?: boolean,
|
||||
fall?: boolean
|
||||
) => RenderItem
|
||||
) => {
|
||||
return (_0: any, _1: any, props?: any) => {
|
||||
if (!props) return new Item('static');
|
||||
else {
|
||||
const {
|
||||
type = 'static',
|
||||
cache = false,
|
||||
fall = false,
|
||||
nocache = true
|
||||
} = props;
|
||||
return new Item(type, cache && !nocache, fall);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const enum ElementState {
|
||||
None = 0,
|
||||
Cache = 1,
|
||||
Fall = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* standardElementFor
|
||||
*/
|
||||
const _se = (
|
||||
Item: new (
|
||||
type: RenderItemPosition,
|
||||
cache?: boolean,
|
||||
fall?: boolean
|
||||
) => RenderItem,
|
||||
position: RenderItemPosition,
|
||||
state: ElementState
|
||||
) => {
|
||||
const defaultCache = !!(state & ElementState.Cache);
|
||||
const defautFall = !!(state & ElementState.Fall);
|
||||
|
||||
return (_0: any, _1: any, props?: any) => {
|
||||
if (!props) return new Item('absolute');
|
||||
else {
|
||||
const {
|
||||
type = position,
|
||||
cache = defaultCache,
|
||||
fall = defautFall,
|
||||
nocache = !defaultCache
|
||||
} = props;
|
||||
return new Item(type, cache && !nocache, fall);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Default elements
|
||||
tagMap.register('container', standardElement(Container));
|
||||
tagMap.register('container-custom', standardElement(ContainerCustom));
|
||||
tagMap.register('template', standardElement(Container));
|
||||
tagMap.register('mota-renderer', (_0, _1, props) => {
|
||||
return new MotaRenderer(props?.id);
|
||||
});
|
||||
tagMap.register('sprite', standardElement(Sprite));
|
||||
tagMap.register<ETextEvent, Text>('text', (_0, _1, props) => {
|
||||
if (!props) return new Text();
|
||||
else {
|
||||
const { type = 'static', text = '' } = props;
|
||||
return new Text(text, type);
|
||||
}
|
||||
});
|
||||
const emptyImage = document.createElement('canvas');
|
||||
emptyImage.width = 1;
|
||||
emptyImage.height = 1;
|
||||
tagMap.register('image', (_0, _1, props) => {
|
||||
if (!props) return new Image(emptyImage);
|
||||
else {
|
||||
const { image = emptyImage, type = 'static' } = props;
|
||||
return new Image(image, type);
|
||||
}
|
||||
});
|
||||
tagMap.register('comment', (_0, _1, props) => {
|
||||
if (!props) return new Comment();
|
||||
else {
|
||||
const { text = '' } = props;
|
||||
return new Comment(text);
|
||||
}
|
||||
});
|
||||
tagMap.register('shader', (_0, _1, props) => {
|
||||
if (!props) return new Shader();
|
||||
else {
|
||||
const { type = 'static' } = props;
|
||||
return new Shader(type);
|
||||
}
|
||||
});
|
||||
tagMap.register('custom', (_0, _1, props) => {
|
||||
if (!props) {
|
||||
logger.error(22);
|
||||
throw new Error('Cannot create custom element.');
|
||||
} else {
|
||||
const item = props._item;
|
||||
if (!item) {
|
||||
logger.error(22);
|
||||
throw new Error('Cannot create custom element.');
|
||||
}
|
||||
return item(props);
|
||||
}
|
||||
});
|
||||
tagMap.register('g-rect', standardElementNoCache(Rect));
|
||||
tagMap.register('g-circle', standardElementNoCache(Circle));
|
||||
tagMap.register('g-ellipse', standardElementNoCache(Ellipse));
|
||||
tagMap.register('g-line', standardElementNoCache(Line));
|
||||
tagMap.register('g-bezier', standardElementNoCache(BezierCurve));
|
||||
tagMap.register('g-quad', standardElementNoCache(QuadraticCurve));
|
||||
tagMap.register('g-path', standardElementNoCache(Path));
|
||||
tagMap.register('g-rectr', standardElementNoCache(RectR));
|
||||
@ -1,29 +1,24 @@
|
||||
import {
|
||||
RenderFunction,
|
||||
RenderItem,
|
||||
RenderItemPosition,
|
||||
Transform,
|
||||
ElementAnchor,
|
||||
ElementLocator,
|
||||
ElementScale,
|
||||
CanvasStyle,
|
||||
Font,
|
||||
RenderPosition,
|
||||
CustomRenderFunction,
|
||||
CustomContainerRenderFn,
|
||||
CustomContainerPropagateFn,
|
||||
CanvasStyle,
|
||||
ILineProperty,
|
||||
BezierParams,
|
||||
CircleParams,
|
||||
EllipseParams,
|
||||
ILineProperty,
|
||||
LineParams,
|
||||
QuadParams,
|
||||
RectRCircleParams,
|
||||
RectREllipseParams,
|
||||
Font
|
||||
RectREllipseParams
|
||||
} from '@motajs/render';
|
||||
|
||||
export interface CustomProps {
|
||||
_item: (props: BaseProps) => RenderItem;
|
||||
}
|
||||
|
||||
export interface BaseProps {
|
||||
/** 元素的横坐标 */
|
||||
x?: number;
|
||||
@ -51,8 +46,8 @@ export interface BaseProps {
|
||||
hidden?: boolean;
|
||||
/** 元素的变换矩阵 */
|
||||
transform?: Transform;
|
||||
/** 元素的定位模式,static 表示常规定位,absolute 定位模式下元素位置始终处于左上角 */
|
||||
type?: RenderItemPosition;
|
||||
/** 元素的定位模式 */
|
||||
type?: RenderPosition;
|
||||
/** 是否启用缓存,用处较少,主要用于一些默认不启用缓存的元素的特殊优化 */
|
||||
cache?: boolean;
|
||||
/** 是否不启用缓存,优先级大于 cache,用处较少,主要用于一些特殊优化 */
|
||||
@ -83,9 +78,9 @@ export interface BaseProps {
|
||||
noevent?: boolean;
|
||||
}
|
||||
|
||||
export interface SpriteProps extends BaseProps {
|
||||
export interface CustomProps extends BaseProps {
|
||||
/** 自定义的渲染函数 */
|
||||
render?: RenderFunction;
|
||||
render?: CustomRenderFunction;
|
||||
}
|
||||
|
||||
export interface ContainerProps extends BaseProps {}
|
||||
|
||||
@ -1,103 +1,114 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import {
|
||||
ERenderItemEvent,
|
||||
RenderItem,
|
||||
ETextEvent,
|
||||
Text,
|
||||
Comment
|
||||
Comment,
|
||||
IRenderItem,
|
||||
IRenderTreeRoot
|
||||
} from '@motajs/render';
|
||||
import {
|
||||
ComponentInternalInstance,
|
||||
CreateAppFunction,
|
||||
createRenderer,
|
||||
ElementNamespace,
|
||||
RootRenderFunction,
|
||||
VNodeProps
|
||||
} from 'vue';
|
||||
import { tagMap } from './map';
|
||||
import { IRenderTagManager } from './types';
|
||||
import { RenderTagManager } from './tag';
|
||||
|
||||
export const { createApp, render } = createRenderer<RenderItem, RenderItem>({
|
||||
patchProp: function (
|
||||
el: RenderItem,
|
||||
key: string,
|
||||
prevValue: any,
|
||||
nextValue: any,
|
||||
namespace?: ElementNamespace,
|
||||
parentComponent?: ComponentInternalInstance | null
|
||||
): void {
|
||||
el.patchProp(key, prevValue, nextValue, namespace, parentComponent);
|
||||
},
|
||||
export interface RendererData {
|
||||
render: RootRenderFunction<IRenderItem>;
|
||||
createApp: CreateAppFunction<IRenderItem>;
|
||||
tagManager: IRenderTagManager;
|
||||
}
|
||||
|
||||
insert: function (
|
||||
el: RenderItem<ERenderItemEvent>,
|
||||
parent: RenderItem,
|
||||
_anchor?: RenderItem<ERenderItemEvent> | null
|
||||
): void {
|
||||
parent.appendChild(el);
|
||||
},
|
||||
export function createRendererFor(renderer: IRenderTreeRoot) {
|
||||
const tagManager = new RenderTagManager(renderer);
|
||||
|
||||
remove: function (el: RenderItem<ERenderItemEvent>): void {
|
||||
el.destroy();
|
||||
},
|
||||
const { createApp, render } = createRenderer<IRenderItem, IRenderItem>({
|
||||
patchProp: function (
|
||||
el: RenderItem,
|
||||
key: string,
|
||||
prevValue: any,
|
||||
nextValue: any,
|
||||
namespace?: ElementNamespace,
|
||||
parentComponent?: ComponentInternalInstance | null
|
||||
): void {
|
||||
el.patchProp(key, prevValue, nextValue, namespace, parentComponent);
|
||||
},
|
||||
|
||||
createElement: function (
|
||||
type: string,
|
||||
namespace?: ElementNamespace,
|
||||
isCustomizedBuiltIn?: string,
|
||||
vnodeProps?: (VNodeProps & { [key: string]: any }) | null
|
||||
): RenderItem {
|
||||
const onCreate = tagMap.get(type);
|
||||
if (!onCreate) {
|
||||
logger.error(20, type);
|
||||
throw new Error(`Cannot create element '${type}'`);
|
||||
insert: function (
|
||||
el: IRenderItem,
|
||||
parent: RenderItem,
|
||||
_anchor?: IRenderItem | null
|
||||
): void {
|
||||
parent.appendChild(el);
|
||||
},
|
||||
|
||||
remove: function (el: IRenderItem): void {
|
||||
el.destroy();
|
||||
},
|
||||
|
||||
createElement: function (
|
||||
type: string,
|
||||
_namespace?: ElementNamespace,
|
||||
_isCustomizedBuiltIn?: string,
|
||||
vnodeProps?: (VNodeProps & { [key: string]: any }) | null
|
||||
): IRenderItem {
|
||||
const tag = tagManager.getTag(type);
|
||||
if (!tag) {
|
||||
logger.error(20, type);
|
||||
throw new Error(`Cannot create element '${type}'`);
|
||||
}
|
||||
return tag.onCreate(vnodeProps);
|
||||
},
|
||||
|
||||
createText: function (text: string): IRenderItem {
|
||||
if (/^\s*$/.test(text)) {
|
||||
return new Comment();
|
||||
} else {
|
||||
logger.warn(38);
|
||||
}
|
||||
return new Text(text);
|
||||
},
|
||||
|
||||
createComment: function (text: string): IRenderItem {
|
||||
return renderer.createElement('comment', text);
|
||||
},
|
||||
|
||||
setText: function (node: IRenderItem, text: string): void {
|
||||
if (node instanceof Text) {
|
||||
node.setText(text);
|
||||
} else {
|
||||
logger.warn(39);
|
||||
}
|
||||
},
|
||||
|
||||
setElementText: function (node: IRenderItem, text: string): void {
|
||||
if (node instanceof Text) {
|
||||
node.setText(text);
|
||||
} else {
|
||||
logger.warn(39);
|
||||
}
|
||||
},
|
||||
|
||||
parentNode: function (node: IRenderItem): IRenderItem | null {
|
||||
return node.parent ?? null;
|
||||
},
|
||||
|
||||
nextSibling: function (node: IRenderItem): IRenderItem | null {
|
||||
if (!node) return null;
|
||||
if (!node.parent) {
|
||||
return null;
|
||||
} else {
|
||||
const parent = node.parent;
|
||||
const list = [...parent.children];
|
||||
const index = list.indexOf(node);
|
||||
return list[index] ?? null;
|
||||
}
|
||||
}
|
||||
return onCreate(namespace, isCustomizedBuiltIn, vnodeProps);
|
||||
},
|
||||
});
|
||||
|
||||
createText: function (text: string): RenderItem<ETextEvent> {
|
||||
if (/^\s*$/.test(text)) {
|
||||
return new Comment();
|
||||
} else {
|
||||
logger.warn(38);
|
||||
}
|
||||
return new Text(text);
|
||||
},
|
||||
|
||||
createComment: function (text: string): RenderItem<ERenderItemEvent> {
|
||||
return new Comment(text);
|
||||
},
|
||||
|
||||
setText: function (node: RenderItem<ERenderItemEvent>, text: string): void {
|
||||
if (node instanceof Text) {
|
||||
node.setText(text);
|
||||
} else {
|
||||
logger.warn(39);
|
||||
}
|
||||
},
|
||||
|
||||
setElementText: function (node: RenderItem, text: string): void {
|
||||
if (node instanceof Text) {
|
||||
node.setText(text);
|
||||
} else {
|
||||
logger.warn(39);
|
||||
}
|
||||
},
|
||||
|
||||
parentNode: function (
|
||||
node: RenderItem<ERenderItemEvent>
|
||||
): RenderItem | null {
|
||||
return node.parent ?? null;
|
||||
},
|
||||
|
||||
nextSibling: function (
|
||||
node: RenderItem<ERenderItemEvent>
|
||||
): RenderItem<ERenderItemEvent> | null {
|
||||
if (!node) return null;
|
||||
if (!node.parent) {
|
||||
return null;
|
||||
} else {
|
||||
const parent = node.parent;
|
||||
const list = [...parent.children];
|
||||
const index = list.indexOf(node);
|
||||
return list[index] ?? null;
|
||||
}
|
||||
}
|
||||
});
|
||||
return { tagManager, createApp, render };
|
||||
}
|
||||
|
||||
128
packages/render-vue/src/tag.ts
Normal file
128
packages/render-vue/src/tag.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import {
|
||||
BezierCurve,
|
||||
Circle,
|
||||
Comment,
|
||||
Container,
|
||||
CustomContainer,
|
||||
CustomRenderItem,
|
||||
Ellipse,
|
||||
Image,
|
||||
IRenderItem,
|
||||
IRenderTreeRoot,
|
||||
Line,
|
||||
Path,
|
||||
QuadraticCurve,
|
||||
Rect,
|
||||
RectR,
|
||||
Shader,
|
||||
Text
|
||||
} from '@motajs/render';
|
||||
import { IRenderTagInfo, IRenderTagManager, TagCreateFunction } from './types';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
export class RenderTagManager implements IRenderTagManager {
|
||||
/** 标签注册映射 */
|
||||
private readonly tagRegistry: Map<string, IRenderTagInfo> = new Map();
|
||||
/** 空图片 */
|
||||
private readonly emptyImg: HTMLCanvasElement;
|
||||
|
||||
constructor(readonly renderer: IRenderTreeRoot) {
|
||||
const emptyImage = document.createElement('canvas');
|
||||
emptyImage.width = 1;
|
||||
emptyImage.height = 1;
|
||||
this.emptyImg = emptyImage;
|
||||
|
||||
this.resgiterIntrinsicTags();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册所有的内置标签
|
||||
*/
|
||||
private resgiterIntrinsicTags() {
|
||||
this.registerTag(
|
||||
'container',
|
||||
this.createStandardElement(true, Container)
|
||||
);
|
||||
this.registerTag(
|
||||
'custom',
|
||||
this.createStandardElement(true, CustomRenderItem)
|
||||
);
|
||||
this.registerTag('text', props => {
|
||||
if (!props) return this.renderer.createElement(Text, '', false);
|
||||
const { text = '', nocache = true, cache = false } = props;
|
||||
return this.renderer.createElement(Text, text, cache && !nocache);
|
||||
});
|
||||
this.registerTag('image', props => {
|
||||
if (!props) {
|
||||
return this.renderer.createElement(Image, this.emptyImg, false);
|
||||
}
|
||||
const {
|
||||
image = this.emptyImg,
|
||||
nocache = true,
|
||||
cache = false
|
||||
} = props;
|
||||
return this.renderer.createElement(Image, image, cache && !nocache);
|
||||
});
|
||||
this.registerTag('shader', this.createNoParamElement(Shader));
|
||||
this.registerTag('comment', props => {
|
||||
if (!props) return this.renderer.createElement(Comment);
|
||||
else return this.renderer.createElement(Comment, props.text ?? '');
|
||||
});
|
||||
this.registerTag(
|
||||
'template',
|
||||
this.createStandardElement(false, Container)
|
||||
);
|
||||
this.registerTag(
|
||||
'custom-container',
|
||||
this.createStandardElement(true, CustomContainer)
|
||||
);
|
||||
this.registerTag('g-rect', this.createStandardElement(false, Rect));
|
||||
this.registerTag('g-circle', this.createStandardElement(false, Circle));
|
||||
this.registerTag(
|
||||
'g-ellipse',
|
||||
this.createStandardElement(false, Ellipse)
|
||||
);
|
||||
this.registerTag('g-line', this.createStandardElement(false, Line));
|
||||
this.registerTag(
|
||||
'g-bezier',
|
||||
this.createStandardElement(false, BezierCurve)
|
||||
);
|
||||
this.registerTag(
|
||||
'g-quad',
|
||||
this.createStandardElement(false, QuadraticCurve)
|
||||
);
|
||||
this.registerTag('g-path', this.createStandardElement(false, Path));
|
||||
this.registerTag('g-rectr', this.createStandardElement(false, RectR));
|
||||
}
|
||||
|
||||
registerTag(tag: string, onCreate: TagCreateFunction): void {
|
||||
if (this.tagRegistry.has(tag)) {
|
||||
logger.error(14, tag);
|
||||
return;
|
||||
}
|
||||
const info: IRenderTagInfo = { onCreate };
|
||||
this.tagRegistry.set(tag, info);
|
||||
}
|
||||
|
||||
getTag(tag: string): IRenderTagInfo | null {
|
||||
return this.tagRegistry.get(tag) ?? null;
|
||||
}
|
||||
|
||||
createStandardElement(
|
||||
cache: boolean,
|
||||
Cons: new (enableCache?: boolean) => IRenderItem
|
||||
): TagCreateFunction {
|
||||
const enable = cache;
|
||||
return props => {
|
||||
if (!props) {
|
||||
return this.renderer.createElement(Cons, enable);
|
||||
}
|
||||
const { nocache = !enable, cache = enable } = props;
|
||||
return this.renderer.createElement(Cons, cache && !nocache);
|
||||
};
|
||||
}
|
||||
|
||||
createNoParamElement(Cons: new () => IRenderItem): TagCreateFunction {
|
||||
return () => this.renderer.createElement(Cons);
|
||||
}
|
||||
}
|
||||
134
packages/render-vue/src/types.ts
Normal file
134
packages/render-vue/src/types.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { ERenderItemEvent, IRenderItem, IRenderTreeRoot } from '@motajs/render';
|
||||
import { VNodeProps } from 'vue';
|
||||
import { BaseProps } from './props';
|
||||
import { TagDefine } from './elements';
|
||||
import {
|
||||
IAnimater,
|
||||
IExcitable,
|
||||
IExcitation,
|
||||
ITransition
|
||||
} from '@motajs/animate';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
|
||||
//#region 标签管理
|
||||
|
||||
export type TagCreateFunction = (
|
||||
props?: (VNodeProps & { [key: string]: any }) | null
|
||||
) => IRenderItem;
|
||||
|
||||
export interface IRenderTagInfo {
|
||||
/** 标签创建函数 */
|
||||
readonly onCreate: TagCreateFunction;
|
||||
}
|
||||
|
||||
export interface IRenderTagManager {
|
||||
/** 标签管理器对应的渲染根元素 */
|
||||
readonly renderer: IRenderTreeRoot;
|
||||
|
||||
/**
|
||||
* 注册自定义标签
|
||||
* @param tag 标签名称
|
||||
* @param onCreate 创建标签的函数
|
||||
*/
|
||||
registerTag(tag: string, onCreate: TagCreateFunction): void;
|
||||
|
||||
/**
|
||||
* 获取指定标签的信息
|
||||
* @param tag 标签名称
|
||||
*/
|
||||
getTag(tag: string): IRenderTagInfo | null;
|
||||
|
||||
/**
|
||||
* 创建常规元素的创建函数
|
||||
* @param cache 创建时默认是否启用缓存
|
||||
* @param Cons 渲染元素构造器
|
||||
*/
|
||||
createStandardElement(
|
||||
cache: boolean,
|
||||
Cons: new (enableCache?: boolean) => IRenderItem
|
||||
): TagCreateFunction;
|
||||
|
||||
/**
|
||||
* 创建无创建参数的元素创建函数
|
||||
* @param Cons 渲染元素构造器
|
||||
*/
|
||||
createNoParamElement(Cons: new () => IRenderItem): TagCreateFunction;
|
||||
}
|
||||
|
||||
export type DefaultProps<
|
||||
P extends BaseProps = BaseProps,
|
||||
E extends ERenderItemEvent = ERenderItemEvent
|
||||
> = TagDefine<P, E>;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 功能接口
|
||||
|
||||
export interface IRendererUsing {
|
||||
/** Using 对象使用的渲染器 */
|
||||
readonly renderer: IRenderTreeRoot;
|
||||
|
||||
/**
|
||||
* 当渲染器的激励源触发激励时同时执行传入的可激励对象,相当于每帧执行一次。
|
||||
*
|
||||
* 在**组件内**需要每帧执行时**务必**使用此接口,否则可能会导致激励对象没有被正确删除,出现内存泄漏问题。
|
||||
*
|
||||
* 在**组件外不要**使用此接口,否则可能没有效果!
|
||||
* @param excitable 可激励对象
|
||||
*/
|
||||
onExcited(excitable: IExcitable<number>): void;
|
||||
|
||||
/**
|
||||
* 当渲染器的激励源触发激励时同时执行传入的函数,相当于每帧执行一次。
|
||||
*
|
||||
* 在**组件内**需要每帧执行时**务必**使用此接口,否则可能会导致激励对象没有被正确删除,出现内存泄漏问题。
|
||||
*
|
||||
* 在**组件外不要**使用此接口,否则可能没有效果!
|
||||
* @param excitable 可激励对象
|
||||
*/
|
||||
onExcitedFunc(fn: (payload: number) => void): void;
|
||||
|
||||
/**
|
||||
* 监听渲染元素的指定事件。
|
||||
*
|
||||
* 在组件内如果你需要监听事件,应该先考虑使用 `JSX` 的 `onXxx` 来监听事件,而不是使用此接口。
|
||||
*
|
||||
* 如果 `JSX` 的 `onXxx` 不能满足你的需求,再考虑使用此接口,在组件内**不要**直接使用 `item.on` 来监听事件,
|
||||
* 否则可能导致事件监听没有正确删除,出现内存泄漏问题。
|
||||
*
|
||||
* 在**组件外不要**使用此接口,否则可能没有效果!
|
||||
* @param item 要监听的渲染元素
|
||||
* @param key 事件类型
|
||||
* @param listener 当事件触发时执行的函数
|
||||
*/
|
||||
listenEvent<
|
||||
T extends ERenderItemEvent,
|
||||
K extends EventEmitter.EventNames<T>
|
||||
>(
|
||||
item: IRenderItem,
|
||||
key: K,
|
||||
listener: EventEmitter.EventListener<T, K>
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 创建一个动画执行器。
|
||||
*
|
||||
* 在**组件内**需要动画器时**务必**使用此接口,否则可能会动画器没有被正确销毁,出现内存泄漏问题。
|
||||
*
|
||||
* 在**组件外不要**使用此接口,否则可能没有效果!
|
||||
* @param excitation 动画执行器使用的激励源
|
||||
*/
|
||||
useAnimater(excitation: IExcitation<number>): IAnimater;
|
||||
|
||||
/**
|
||||
* 创建一个渐变执行器。
|
||||
*
|
||||
* 在**组件内**需要渐变对象时**务必**使用此接口,否则可能会渐变对象没有被正确销毁,出现内存泄漏问题。
|
||||
*
|
||||
* 在**组件外不要**使用此接口,否则可能没有效果!
|
||||
* @param excitation 渐变执行器使用的激励源
|
||||
*/
|
||||
useTransition(excitation: IExcitation<number>): ITransition;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
@ -1,54 +1,66 @@
|
||||
import { Animation, Ticker, Transition } from 'mutate-animate';
|
||||
import { ERenderItemEvent, RenderItem } from '@motajs/render';
|
||||
import { ERenderItemEvent, IRenderItem, IRenderTreeRoot } from '@motajs/render';
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { IRendererUsing } from './types';
|
||||
import {
|
||||
IExcitable,
|
||||
IAnimater,
|
||||
ITransition,
|
||||
Animater,
|
||||
IExcitation,
|
||||
Transition,
|
||||
excited
|
||||
} from '@motajs/animate';
|
||||
|
||||
const ticker = new Ticker();
|
||||
export class RendererUsing implements IRendererUsing {
|
||||
constructor(readonly renderer: IRenderTreeRoot) {}
|
||||
|
||||
/**
|
||||
* 在组件中每帧执行一次函数
|
||||
* @param fn 每帧执行的函数
|
||||
*/
|
||||
export function onTick(fn: (time: number) => void) {
|
||||
onMounted(() => {
|
||||
ticker.add(fn);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
ticker.remove(fn);
|
||||
});
|
||||
}
|
||||
|
||||
type AnimationUsing = [Animation];
|
||||
type TransitionUsing = [Transition];
|
||||
|
||||
/**
|
||||
* 在组件中创建一个动画实例
|
||||
*/
|
||||
export function useAnimation(): AnimationUsing {
|
||||
const ani = new Animation();
|
||||
onUnmounted(() => {
|
||||
ani.ticker.destroy();
|
||||
});
|
||||
return [ani];
|
||||
}
|
||||
|
||||
/**
|
||||
* 在组件中创建一个渐变实例
|
||||
*/
|
||||
export function useTransition(): TransitionUsing {
|
||||
const tran = new Transition();
|
||||
onUnmounted(() => {
|
||||
tran.ticker.destroy();
|
||||
});
|
||||
return [tran];
|
||||
}
|
||||
|
||||
export function onEvent<
|
||||
T extends ERenderItemEvent,
|
||||
K extends EventEmitter.EventNames<T>
|
||||
>(item: RenderItem<T>, key: K, listener: EventEmitter.EventListener<T, K>) {
|
||||
item.on(key, listener);
|
||||
onUnmounted(() => {
|
||||
item.off(key, listener);
|
||||
});
|
||||
onExcited(excitable: IExcitable<number>): void {
|
||||
onMounted(() => {
|
||||
this.renderer.excitation.add(excitable);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
this.renderer.excitation.remove(excitable);
|
||||
});
|
||||
}
|
||||
|
||||
onExcitedFunc(fn: (payload: number) => void): void {
|
||||
this.onExcited(excited(fn));
|
||||
}
|
||||
|
||||
listenEvent<
|
||||
T extends ERenderItemEvent,
|
||||
K extends EventEmitter.EventNames<T>
|
||||
>(
|
||||
item: IRenderItem,
|
||||
key: K,
|
||||
listener: EventEmitter.EventListener<T, K>
|
||||
): void {
|
||||
item.on(key, listener);
|
||||
onUnmounted(() => {
|
||||
item.off(key, listener);
|
||||
});
|
||||
}
|
||||
|
||||
useAnimater(excitation: IExcitation<number>): IAnimater {
|
||||
const anim = new Animater();
|
||||
onMounted(() => {
|
||||
anim.bindExcitation(excitation);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
anim.unbindExcitation();
|
||||
});
|
||||
return anim;
|
||||
}
|
||||
|
||||
useTransition(excitation: IExcitation<number>): ITransition {
|
||||
const tran = new Transition();
|
||||
onMounted(() => {
|
||||
tran.bindExcitation(excitation);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
tran.unbindExcitation();
|
||||
});
|
||||
return tran;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,44 +1,29 @@
|
||||
import { MotaOffscreenCanvas2D } from './canvas2d';
|
||||
import { ActionType, EventProgress, ActionEventMap } from './event';
|
||||
import {
|
||||
ERenderItemEvent,
|
||||
IRenderChildable,
|
||||
RenderItem,
|
||||
RenderItemPosition
|
||||
} from './item';
|
||||
import { RenderItem } from './item';
|
||||
import { Transform } from './transform';
|
||||
import {
|
||||
CustomContainerPropagateFn,
|
||||
CustomContainerRenderFn,
|
||||
IRenderItem
|
||||
} from './types';
|
||||
|
||||
export interface EContainerEvent extends ERenderItemEvent {}
|
||||
|
||||
export class Container<E extends EContainerEvent = EContainerEvent>
|
||||
extends RenderItem<E | EContainerEvent>
|
||||
implements IRenderChildable
|
||||
{
|
||||
export class Container extends RenderItem {
|
||||
sortedChildren: RenderItem[] = [];
|
||||
|
||||
private needSort: boolean = false;
|
||||
|
||||
/**
|
||||
* 创建一个容器,容器中可以包含其他渲染对象
|
||||
* @param type 渲染模式,absolute表示绝对位置,static表示跟随摄像机移动
|
||||
* @param cache 是否启用缓存机制
|
||||
*/
|
||||
constructor(
|
||||
type: RenderItemPosition = 'static',
|
||||
cache: boolean = true,
|
||||
fall: boolean = false
|
||||
) {
|
||||
super(type, cache, fall);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
transform: Transform
|
||||
_transform: Transform
|
||||
): void {
|
||||
if (this.needSort) {
|
||||
this.sortChildren();
|
||||
this.needSort = false;
|
||||
}
|
||||
this.sortedChildren.forEach(v => {
|
||||
if (v.hidden) return;
|
||||
v.renderContent(canvas, transform);
|
||||
v.renderContent(canvas);
|
||||
});
|
||||
}
|
||||
|
||||
@ -48,28 +33,22 @@ export class Container<E extends EContainerEvent = EContainerEvent>
|
||||
}
|
||||
|
||||
requestSort() {
|
||||
if (!this.needSort) {
|
||||
this.needSort = true;
|
||||
this.requestBeforeFrame(() => {
|
||||
this.needSort = false;
|
||||
this.sortChildren();
|
||||
});
|
||||
}
|
||||
this.needSort = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加子元素到这个容器上,然后在下一个tick执行更新
|
||||
* @param children 要添加的子元素
|
||||
*/
|
||||
appendChild(...children: RenderItem<any>[]) {
|
||||
appendChild(...children: IRenderItem[]) {
|
||||
children.forEach(v => {
|
||||
v.appendTo(this);
|
||||
});
|
||||
this.requestSort();
|
||||
this.update(this);
|
||||
this.update();
|
||||
}
|
||||
|
||||
removeChild(...child: RenderItem<any>[]): void {
|
||||
removeChild(...child: IRenderItem[]): void {
|
||||
let changed = false;
|
||||
child.forEach(v => {
|
||||
if (v.parent !== this) return;
|
||||
@ -79,7 +58,7 @@ export class Container<E extends EContainerEvent = EContainerEvent>
|
||||
}
|
||||
});
|
||||
if (changed) this.requestSort();
|
||||
this.update(this);
|
||||
this.update();
|
||||
}
|
||||
|
||||
appendTo(parent: RenderItem): void {
|
||||
@ -96,7 +75,7 @@ export class Container<E extends EContainerEvent = EContainerEvent>
|
||||
* 遍历这个元素中的每个子元素,并执行传入的函数
|
||||
* @param fn 对每个元素执行的函数
|
||||
*/
|
||||
forEachChild(fn: (ele: RenderItem) => void) {
|
||||
protected forEachChild(fn: (ele: RenderItem) => void) {
|
||||
const stack: RenderItem[] = [this];
|
||||
while (stack.length > 0) {
|
||||
const ele = stack.pop()!;
|
||||
@ -109,7 +88,6 @@ export class Container<E extends EContainerEvent = EContainerEvent>
|
||||
this.sortedChildren = [...this.children]
|
||||
.filter(v => !v.isComment)
|
||||
.sort((a, b) => a.zIndex - b.zIndex);
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected propagateEvent<T extends ActionType>(
|
||||
@ -145,27 +123,7 @@ export class Container<E extends EContainerEvent = EContainerEvent>
|
||||
}
|
||||
}
|
||||
|
||||
export type CustomContainerRenderFn = (
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
children: RenderItem[],
|
||||
transform: Transform
|
||||
) => void;
|
||||
|
||||
export type CustomContainerPropagateOrigin = <T extends ActionType>(
|
||||
type: T,
|
||||
progress: EventProgress,
|
||||
event: ActionEventMap[T]
|
||||
) => void;
|
||||
|
||||
export type CustomContainerPropagateFn = <T extends ActionType>(
|
||||
type: T,
|
||||
progress: EventProgress,
|
||||
event: ActionEventMap[T],
|
||||
container: ContainerCustom,
|
||||
origin: CustomContainerPropagateOrigin
|
||||
) => void;
|
||||
|
||||
export class ContainerCustom extends Container {
|
||||
export class CustomContainer extends Container {
|
||||
private renderFn?: CustomContainerRenderFn;
|
||||
private propagateFn?: CustomContainerPropagateFn;
|
||||
|
||||
|
||||
60
packages/render/src/core/custom.ts
Normal file
60
packages/render/src/core/custom.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { RenderItem } from './item';
|
||||
import { MotaOffscreenCanvas2D } from './canvas2d';
|
||||
import { CustomRenderFunction } from './types';
|
||||
import { Ref } from 'vue';
|
||||
|
||||
export class CustomRenderItem extends RenderItem {
|
||||
renderFn: CustomRenderFunction;
|
||||
|
||||
/**
|
||||
* 创建一个精灵,可以自由在上面渲染内容
|
||||
* @param type 渲染模式,absolute表示绝对位置,不会跟随自身的Transform改变
|
||||
* @param cache 是否启用缓存机制
|
||||
*/
|
||||
constructor(cache: boolean = true) {
|
||||
super(cache);
|
||||
this.renderFn = () => {};
|
||||
}
|
||||
|
||||
protected render(canvas: MotaOffscreenCanvas2D): void {
|
||||
canvas.ctx.save();
|
||||
this.renderFn(canvas);
|
||||
canvas.ctx.restore();
|
||||
}
|
||||
|
||||
setRenderFn(fn: CustomRenderFunction) {
|
||||
this.renderFn = fn;
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
protected handleProps(
|
||||
key: string,
|
||||
prevValue: any,
|
||||
nextValue: any
|
||||
): boolean {
|
||||
switch (key) {
|
||||
case 'render':
|
||||
if (!this.assertType(nextValue, 'function', key)) return false;
|
||||
this.setRenderFn(nextValue);
|
||||
return true;
|
||||
case 'bindings':
|
||||
if (!this.assertType(nextValue, Array, key)) return false;
|
||||
if (nextValue !== prevValue) {
|
||||
this.update();
|
||||
} else if (nextValue.length !== prevValue.length) {
|
||||
this.update();
|
||||
} else {
|
||||
const arr: Ref<any>[] = nextValue as Ref<any>[];
|
||||
const prev: Ref<any>[] = prevValue as Ref<any>[];
|
||||
for (let i = 0; i < nextValue.length; i++) {
|
||||
if (arr[i].value !== prev[i].value) {
|
||||
this.update();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { RenderItem } from './item';
|
||||
import { IRenderItem } from './types';
|
||||
|
||||
export const enum MouseType {
|
||||
/** 没有按键按下 */
|
||||
@ -51,7 +51,7 @@ export const enum EventProgress {
|
||||
|
||||
export interface IActionEventBase {
|
||||
/** 当前事件是监听的哪个元素 */
|
||||
target: RenderItem;
|
||||
target: IRenderItem;
|
||||
/** 是触摸操作还是鼠标操作 */
|
||||
touch: boolean;
|
||||
/**
|
||||
|
||||
@ -1,15 +1,33 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { logger } from '@motajs/common';
|
||||
import { MotaOffscreenCanvas2D } from './canvas2d';
|
||||
import { ERenderItemEvent, RenderItem, RenderItemPosition } from './item';
|
||||
import { RenderItem } from './item';
|
||||
import { Transform } from './transform';
|
||||
import { isWebGL2Supported } from './utils';
|
||||
import { SizedCanvasImageSource } from '../types';
|
||||
|
||||
export interface IGL2ProgramPrefix {
|
||||
readonly VERTEX: string;
|
||||
readonly FRAGMENT: string;
|
||||
}
|
||||
import {
|
||||
AttribSetFn,
|
||||
AttribType,
|
||||
DrawArraysInstancedParam,
|
||||
DrawArraysParam,
|
||||
DrawElementsInstancedParam,
|
||||
DrawElementsParam,
|
||||
DrawParamsMap,
|
||||
IGL2Program,
|
||||
IGL2ProgramPrefix,
|
||||
IShaderAttrib,
|
||||
IShaderAttribArray,
|
||||
IShaderIndices,
|
||||
IShaderTexture2D,
|
||||
IShaderUniform,
|
||||
IShaderUniformBlock,
|
||||
IShaderUniformMatrix,
|
||||
IWebGL2RenderItem,
|
||||
ProgramConstructor,
|
||||
RenderMode,
|
||||
UniformMatrix,
|
||||
UniformSetFn,
|
||||
UniformType
|
||||
} from './types';
|
||||
|
||||
const GL2_PREFIX: IGL2ProgramPrefix = {
|
||||
VERTEX: /* glsl */ `#version 300 es
|
||||
@ -25,78 +43,7 @@ interface CompiledShader {
|
||||
fragment: WebGLShader;
|
||||
}
|
||||
|
||||
const enum RenderMode {
|
||||
Arrays,
|
||||
Elements,
|
||||
ArraysInstanced,
|
||||
ElementsInstanced
|
||||
}
|
||||
|
||||
export const enum UniformType {
|
||||
Uniform1f,
|
||||
Uniform1fv,
|
||||
Uniform1i,
|
||||
Uniform1iv,
|
||||
Uniform1ui,
|
||||
Uniform1uiv,
|
||||
Uniform2f,
|
||||
Uniform2fv,
|
||||
Uniform2i,
|
||||
Uniform2iv,
|
||||
Uniform2ui,
|
||||
Uniform2uiv,
|
||||
Uniform3f,
|
||||
Uniform3fv,
|
||||
Uniform3i,
|
||||
Uniform3iv,
|
||||
Uniform3ui,
|
||||
Uniform3uiv,
|
||||
Uniform4f,
|
||||
Uniform4fv,
|
||||
Uniform4i,
|
||||
Uniform4iv,
|
||||
Uniform4ui,
|
||||
Uniform4uiv
|
||||
}
|
||||
|
||||
export const enum UniformMatrix {
|
||||
UMatrix2x2,
|
||||
UMatrix2x3,
|
||||
UMatrix2x4,
|
||||
UMatrix3x2,
|
||||
UMatrix3x3,
|
||||
UMatrix3x4,
|
||||
UMatrix4x2,
|
||||
UMatrix4x3,
|
||||
UMatrix4x4
|
||||
}
|
||||
|
||||
export const enum AttribType {
|
||||
Attrib1f,
|
||||
Attrib1fv,
|
||||
Attrib2f,
|
||||
Attrib2fv,
|
||||
Attrib3f,
|
||||
Attrib3fv,
|
||||
Attrib4f,
|
||||
Attrib4fv,
|
||||
AttribI4i,
|
||||
AttribI4iv,
|
||||
AttribI4ui,
|
||||
AttribI4uiv
|
||||
}
|
||||
|
||||
export type ProgramConstructor<T extends GL2Program> = new (
|
||||
gl2: GL2,
|
||||
vs?: string,
|
||||
fs?: string
|
||||
) => T;
|
||||
|
||||
export interface EGL2Event extends ERenderItemEvent {}
|
||||
|
||||
export abstract class GL2<E extends EGL2Event = EGL2Event> extends RenderItem<
|
||||
EGL2Event | E
|
||||
> {
|
||||
export abstract class GL2 extends RenderItem {
|
||||
/** 是否支持此组件 */
|
||||
static readonly support: boolean = isWebGL2Supported();
|
||||
|
||||
@ -161,14 +108,14 @@ export abstract class GL2<E extends EGL2Event = EGL2Event> extends RenderItem<
|
||||
gl: WebGL2RenderingContext;
|
||||
|
||||
/** webgl使用的程序 */
|
||||
protected program: GL2Program | null = null;
|
||||
protected program: IGL2Program | null = null;
|
||||
/** 当前渲染实例的所有着色器程序 */
|
||||
protected programs: Set<GL2Program> = new Set();
|
||||
protected programs: Set<IGL2Program> = new Set();
|
||||
/** framebuffer 映射 */
|
||||
protected framebufferMap: Map<string, WebGLFramebuffer> = new Map();
|
||||
|
||||
constructor(type: RenderItemPosition = 'static') {
|
||||
super(type, false);
|
||||
constructor() {
|
||||
super(false);
|
||||
|
||||
this.canvas = document.createElement('canvas');
|
||||
const gl = this.canvas.getContext('webgl2')!;
|
||||
@ -258,7 +205,7 @@ export abstract class GL2<E extends EGL2Event = EGL2Event> extends RenderItem<
|
||||
* @param gl 当前正在渲染的 gl2 画布
|
||||
* @param program 当前元素正在使用的着色器程序
|
||||
*/
|
||||
draw(gl: WebGL2RenderingContext, program: GL2Program) {
|
||||
protected draw(gl: WebGL2RenderingContext, program: IGL2Program) {
|
||||
const indices = program.usingIndices;
|
||||
const param = program.getDrawParams(program.renderMode);
|
||||
if (!param) return;
|
||||
@ -367,7 +314,7 @@ export abstract class GL2<E extends EGL2Event = EGL2Event> extends RenderItem<
|
||||
* 切换着色器程序
|
||||
* @param program 着色器程序
|
||||
*/
|
||||
useProgram(program: GL2Program) {
|
||||
useProgram(program: IGL2Program) {
|
||||
if (!this.gl) return;
|
||||
if (program.element !== this) {
|
||||
logger.error(17);
|
||||
@ -386,11 +333,11 @@ export abstract class GL2<E extends EGL2Event = EGL2Event> extends RenderItem<
|
||||
* @param vs 顶点着色器,可选
|
||||
* @param fs 片元着色器,可选
|
||||
*/
|
||||
createProgram<T extends GL2Program>(
|
||||
createProgram<T extends IGL2Program>(
|
||||
Program: ProgramConstructor<T>,
|
||||
vs?: string,
|
||||
fs?: string
|
||||
) {
|
||||
): T {
|
||||
const program = new Program(this, vs, fs);
|
||||
this.programs.add(program);
|
||||
return program;
|
||||
@ -400,7 +347,7 @@ export abstract class GL2<E extends EGL2Event = EGL2Event> extends RenderItem<
|
||||
* 删除一个着色器程序
|
||||
* @param program 要删除的着色器程序
|
||||
*/
|
||||
deleteProgram(program: GL2Program) {
|
||||
deleteProgram(program: IGL2Program) {
|
||||
if (program.element !== this) {
|
||||
logger.error(18);
|
||||
return;
|
||||
@ -411,319 +358,18 @@ export abstract class GL2<E extends EGL2Event = EGL2Event> extends RenderItem<
|
||||
|
||||
destroy(): void {
|
||||
this.programs.forEach(v => v.destroy());
|
||||
this.programs.clear();
|
||||
this.canvas.remove();
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
type _U1 = [x0: number];
|
||||
type _U2 = [x0: number, x1: number];
|
||||
type _U3 = [x0: number, x1: number, x2: number];
|
||||
type _U4 = [x0: number, x1: number, x2: number, x3: number];
|
||||
type _UV<T> = [data: T, srcOffset?: number, srcLength?: number];
|
||||
type _A<T> = [data: T];
|
||||
|
||||
interface UniformSetFn {
|
||||
[UniformType.Uniform1f]: _U1;
|
||||
[UniformType.Uniform1fv]: _UV<Float32List>;
|
||||
[UniformType.Uniform1i]: _U1;
|
||||
[UniformType.Uniform1iv]: _UV<Int32List>;
|
||||
[UniformType.Uniform1ui]: _U1;
|
||||
[UniformType.Uniform1uiv]: _UV<Uint32List>;
|
||||
[UniformType.Uniform2f]: _U2;
|
||||
[UniformType.Uniform2fv]: _UV<Float32List>;
|
||||
[UniformType.Uniform2i]: _U2;
|
||||
[UniformType.Uniform2iv]: _UV<Int32List>;
|
||||
[UniformType.Uniform2ui]: _U2;
|
||||
[UniformType.Uniform2uiv]: _UV<Uint32List>;
|
||||
[UniformType.Uniform3f]: _U3;
|
||||
[UniformType.Uniform3fv]: _UV<Float32List>;
|
||||
[UniformType.Uniform3i]: _U3;
|
||||
[UniformType.Uniform3iv]: _UV<Int32List>;
|
||||
[UniformType.Uniform3ui]: _U3;
|
||||
[UniformType.Uniform3uiv]: _UV<Uint32List>;
|
||||
[UniformType.Uniform4f]: _U4;
|
||||
[UniformType.Uniform4fv]: _UV<Float32List>;
|
||||
[UniformType.Uniform4i]: _U4;
|
||||
[UniformType.Uniform4iv]: _UV<Int32List>;
|
||||
[UniformType.Uniform4ui]: _U4;
|
||||
[UniformType.Uniform4uiv]: _UV<Uint32List>;
|
||||
}
|
||||
|
||||
interface AttribSetFn {
|
||||
[AttribType.Attrib1f]: _U1;
|
||||
[AttribType.Attrib1fv]: _A<Float32List>;
|
||||
[AttribType.Attrib2f]: _U2;
|
||||
[AttribType.Attrib2fv]: _A<Float32List>;
|
||||
[AttribType.Attrib3f]: _U3;
|
||||
[AttribType.Attrib3fv]: _A<Float32List>;
|
||||
[AttribType.Attrib4f]: _U4;
|
||||
[AttribType.Attrib4fv]: _A<Float32List>;
|
||||
[AttribType.AttribI4i]: _U4;
|
||||
[AttribType.AttribI4iv]: _A<Int32List>;
|
||||
[AttribType.AttribI4ui]: _U4;
|
||||
[AttribType.AttribI4uiv]: _A<Uint32List>;
|
||||
}
|
||||
|
||||
export interface IShaderUniform<T extends UniformType> {
|
||||
/** 这个 uniform 变量的内存位置 */
|
||||
readonly location: WebGLUniformLocation;
|
||||
/** 这个 uniform 变量的类型 */
|
||||
readonly type: T;
|
||||
/** 这个量所处的着色器程序 */
|
||||
readonly program: GL2Program;
|
||||
/**
|
||||
* 设置这个 uniform 变量的值,
|
||||
* 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL2RenderingContext/uniform
|
||||
* @param params 要传递的参数,例如 uniform2f 就要传递 x0 x1 两个参数等,可以参考 mdn 文档
|
||||
*/
|
||||
set(...params: UniformSetFn[T]): void;
|
||||
}
|
||||
|
||||
export interface IShaderAttrib<T extends AttribType> {
|
||||
/** 这个 attribute 常量的内存位置 */
|
||||
readonly location: number;
|
||||
/** 这个 attribute 常量的类型 */
|
||||
readonly type: T;
|
||||
/** 这个量所处的着色器程序 */
|
||||
readonly program: GL2Program;
|
||||
/**
|
||||
* 设置这个 attribute 常量的值,
|
||||
* 浮点数参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/vertexAttrib
|
||||
* 整数参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/vertexAttribI
|
||||
* @param params 要传递的参数
|
||||
*/
|
||||
set(...params: AttribSetFn[T]): void;
|
||||
}
|
||||
|
||||
export interface IShaderAttribArray {
|
||||
/** 这个 attribute 常量的内存位置 */
|
||||
readonly location: number;
|
||||
/** 这个 attribute 所用的缓冲区信息 */
|
||||
readonly data: WebGLBuffer;
|
||||
/** 这个量所处的着色器程序 */
|
||||
readonly program: GL2Program;
|
||||
/**
|
||||
* 修改缓冲区数据,会更改数据大小,重新分配内存,不更改数据大小的情况下建议使用 {@link sub} 代替。
|
||||
* 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/bufferData
|
||||
* @param data 数据
|
||||
* @param usage 用途
|
||||
*/
|
||||
buffer(data: AllowSharedBufferSource | null, usage: GLenum): void;
|
||||
/**
|
||||
* 修改缓冲区数据,会更改数据大小,重新分配内存,不更改数据大小的情况下建议使用 {@link sub} 代替。
|
||||
* 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/bufferData
|
||||
* @param data 数据
|
||||
* @param usage 用途
|
||||
* @param srcOffset 数据偏移量
|
||||
* @param length 数据长度
|
||||
*/
|
||||
buffer(
|
||||
data: ArrayBufferView,
|
||||
usage: GLenum,
|
||||
srcOffset: number,
|
||||
length?: number
|
||||
): void;
|
||||
/**
|
||||
* 修改缓冲区数据,但是不修改数据大小,不重新分配内存。
|
||||
* 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bufferSubData
|
||||
* @param dstByteOffset 数据修改的起始位置
|
||||
* @param srcData 数据
|
||||
*/
|
||||
sub(dstByteOffset: GLintptr, srcData: AllowSharedBufferSource): void;
|
||||
/**
|
||||
* 修改缓冲区数据,但是不修改数据大小,不重新分配内存。
|
||||
* 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bufferSubData
|
||||
* @param dstByteOffset 数据修改的起始位置
|
||||
* @param srcData 数据
|
||||
* @param srcOffset 数据偏移量
|
||||
* @param length 数据长度
|
||||
*/
|
||||
sub(
|
||||
dstByteOffset: GLintptr,
|
||||
srcData: ArrayBufferView,
|
||||
srcOffset: number,
|
||||
length?: GLuint
|
||||
): void;
|
||||
/**
|
||||
* 告诉 gpu 将读取此 attribute 数据
|
||||
* 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/vertexAttribPointer
|
||||
* @param size 单个数据大小
|
||||
* @param type 数据类型
|
||||
* @param normalized 是否要经过归一化处理
|
||||
* @param stride 每一部分字节偏移量
|
||||
* @param offset 第一部分字节偏移量
|
||||
*/
|
||||
pointer(
|
||||
size: GLint,
|
||||
type: GLenum,
|
||||
normalized: GLboolean,
|
||||
stride: GLsizei,
|
||||
offset: GLintptr
|
||||
): void;
|
||||
/**
|
||||
* 告诉 gpu 将由整数类型读取此 attribute 数据
|
||||
* 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/vertexAttribIPointer
|
||||
* @param size 单个数据大小
|
||||
* @param type 数据类型
|
||||
* @param stride 每一部分字节偏移量
|
||||
* @param offset 第一部分字节偏移量
|
||||
*/
|
||||
pointerI(
|
||||
size: GLint,
|
||||
type: GLenum,
|
||||
stride: GLsizei,
|
||||
offset: GLintptr
|
||||
): void;
|
||||
/**
|
||||
* 设置顶点指针更新时刻。
|
||||
* 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/vertexAttribDivisor
|
||||
* @param divisor 每多少个实例更新一次,0表示每个顶点都更新
|
||||
*/
|
||||
divisor(divisor: number): void;
|
||||
/**
|
||||
* 启用这个顶点数据
|
||||
*/
|
||||
enable(): void;
|
||||
/**
|
||||
* 禁用这个顶点数据
|
||||
*/
|
||||
disable(): void;
|
||||
}
|
||||
|
||||
export interface IShaderIndices {
|
||||
/** 这个顶点索引所用的缓冲区信息 */
|
||||
readonly data: WebGLBuffer;
|
||||
/** 这个量所处的着色器程序 */
|
||||
readonly program: GL2Program;
|
||||
/**
|
||||
* 修改缓冲区数据,会更改数据大小,重新分配内存,不更改数据大小的情况下建议使用 {@link sub} 代替。
|
||||
* 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/bufferData
|
||||
* @param data 数据
|
||||
* @param usage 用途
|
||||
*/
|
||||
buffer(data: AllowSharedBufferSource | null, usage: GLenum): void;
|
||||
/**
|
||||
* 修改缓冲区数据,会更改数据大小,重新分配内存,不更改数据大小的情况下建议使用 {@link sub} 代替。
|
||||
* 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/bufferData
|
||||
* @param data 数据
|
||||
* @param usage 用途
|
||||
* @param srcOffset 数据偏移量
|
||||
* @param length 数据长度
|
||||
*/
|
||||
buffer(
|
||||
data: ArrayBufferView,
|
||||
usage: GLenum,
|
||||
srcOffset: number,
|
||||
length?: number
|
||||
): void;
|
||||
/**
|
||||
* 修改缓冲区数据,但是不修改数据大小,不重新分配内存。
|
||||
* 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bufferSubData
|
||||
* @param dstByteOffset 数据修改的起始位置
|
||||
* @param srcData 数据
|
||||
*/
|
||||
sub(dstByteOffset: GLintptr, srcData: AllowSharedBufferSource): void;
|
||||
/**
|
||||
* 修改缓冲区数据,但是不修改数据大小,不重新分配内存。
|
||||
* 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bufferSubData
|
||||
* @param dstByteOffset 数据修改的起始位置
|
||||
* @param srcData 数据
|
||||
* @param srcOffset 数据偏移量
|
||||
* @param length 数据长度
|
||||
*/
|
||||
sub(
|
||||
dstByteOffset: GLintptr,
|
||||
srcData: ArrayBufferView,
|
||||
srcOffset: number,
|
||||
length?: GLuint
|
||||
): void;
|
||||
}
|
||||
|
||||
export interface IShaderUniformMatrix {
|
||||
/** 矩阵的内存位置 */
|
||||
readonly location: WebGLUniformLocation;
|
||||
/** 矩阵类型 */
|
||||
readonly type: UniformMatrix;
|
||||
/** 这个量所处的着色器程序 */
|
||||
readonly program: GL2Program;
|
||||
/**
|
||||
* 设置矩阵的值,参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL2RenderingContext/uniformMatrix
|
||||
* @param transpose 是否转置矩阵
|
||||
* @param data 矩阵数据,列主序
|
||||
* @param srcOffset 数据偏移量
|
||||
* @param srcLength 数据长度
|
||||
*/
|
||||
set(
|
||||
transpose: GLboolean,
|
||||
data: Float32List,
|
||||
srcOffset?: number,
|
||||
srcLength?: number
|
||||
): void;
|
||||
}
|
||||
|
||||
export interface IShaderUniformBlock {
|
||||
/** 这个 uniform block 的内存地址 */
|
||||
readonly location: GLuint;
|
||||
/** 与这个 uniform block 所绑定的缓冲区 */
|
||||
readonly buffer: WebGLBuffer;
|
||||
/** 这个 uniform block 的大小 */
|
||||
readonly size: number;
|
||||
/** 这个量所处的着色器程序 */
|
||||
readonly program: GL2Program;
|
||||
/**
|
||||
* 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL2RenderingContext/bindBufferBase
|
||||
* @param srcData 要设置为的值
|
||||
*/
|
||||
set(srcData: AllowSharedBufferSource | null): void;
|
||||
/**
|
||||
* 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL2RenderingContext/bindBufferBase
|
||||
* @param srcData 要设置为的值
|
||||
* @param srcOffset 数据偏移量
|
||||
* @param length 数据长度
|
||||
*/
|
||||
set(srcData: ArrayBufferView, srcOffset: number, length?: number): void;
|
||||
}
|
||||
|
||||
export interface IShaderTexture2D {
|
||||
/** 纹理对象 */
|
||||
readonly texture: WebGLTexture;
|
||||
/** 宽度 */
|
||||
readonly width: number;
|
||||
/** 高度 */
|
||||
readonly height: number;
|
||||
/** 纹理所属索引 */
|
||||
readonly index: number;
|
||||
/** 这个量所处的着色器程序 */
|
||||
readonly program: GL2Program;
|
||||
/**
|
||||
* 设置这个纹理的图像,不建议使用,会修改宽高
|
||||
* @param source 要设置成的图像源
|
||||
*/
|
||||
set(source: TexImageSource): void;
|
||||
/**
|
||||
* 设置纹理的一部分信息,不会修改宽高
|
||||
* @param source 要设置的图像源
|
||||
* @param x 要设置到的起始点横坐标
|
||||
* @param y 要设置到的起始点纵坐标
|
||||
* @param width 宽度
|
||||
* @param height 高度
|
||||
*/
|
||||
sub(
|
||||
source: TexImageSource,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number
|
||||
): void;
|
||||
}
|
||||
|
||||
class ShaderUniform<T extends UniformType> implements IShaderUniform<T> {
|
||||
constructor(
|
||||
readonly type: T,
|
||||
readonly location: WebGLUniformLocation,
|
||||
readonly gl: WebGL2RenderingContext,
|
||||
readonly program: GL2Program
|
||||
readonly program: IGL2Program
|
||||
) {}
|
||||
|
||||
set(...params: UniformSetFn[T]): void {
|
||||
@ -812,7 +458,7 @@ class ShaderAttrib<T extends AttribType> implements IShaderAttrib<T> {
|
||||
readonly type: T,
|
||||
readonly location: number,
|
||||
readonly gl: WebGL2RenderingContext,
|
||||
readonly program: GL2Program
|
||||
readonly program: IGL2Program
|
||||
) {}
|
||||
|
||||
set(...params: AttribSetFn[T]) {
|
||||
@ -869,7 +515,7 @@ class ShaderAttribArray implements IShaderAttribArray {
|
||||
readonly data: WebGLBuffer,
|
||||
readonly location: number,
|
||||
readonly gl: WebGL2RenderingContext,
|
||||
readonly program: GL2Program
|
||||
readonly program: IGL2Program
|
||||
) {}
|
||||
|
||||
buffer(data: AllowSharedBufferSource | null, usage: GLenum): void;
|
||||
@ -954,7 +600,7 @@ class ShaderIndices implements IShaderIndices {
|
||||
constructor(
|
||||
readonly data: WebGLBuffer,
|
||||
readonly gl: WebGL2RenderingContext,
|
||||
readonly program: GL2Program
|
||||
readonly program: IGL2Program
|
||||
) {}
|
||||
|
||||
buffer(data: AllowSharedBufferSource | null, usage: GLenum): void;
|
||||
@ -999,7 +645,7 @@ class ShaderUniformMatrix implements IShaderUniformMatrix {
|
||||
readonly type: UniformMatrix,
|
||||
readonly location: WebGLUniformLocation,
|
||||
readonly gl: WebGL2RenderingContext,
|
||||
readonly program: GL2Program
|
||||
readonly program: IGL2Program
|
||||
) {}
|
||||
|
||||
set(x2: GLboolean, x3: Float32List, x4?: number, x5?: number): void {
|
||||
@ -1043,7 +689,7 @@ class ShaderUniformBlock implements IShaderUniformBlock {
|
||||
readonly buffer: WebGLBuffer,
|
||||
readonly binding: number,
|
||||
readonly gl: WebGL2RenderingContext,
|
||||
readonly program: GL2Program
|
||||
readonly program: IGL2Program
|
||||
) {}
|
||||
|
||||
set(srcData: AllowSharedBufferSource | null): void;
|
||||
@ -1070,7 +716,7 @@ class ShaderTexture2D implements IShaderTexture2D {
|
||||
readonly index: number,
|
||||
readonly uniform: IShaderUniform<UniformType.Uniform1i>,
|
||||
readonly gl: WebGL2RenderingContext,
|
||||
readonly program: GL2Program,
|
||||
readonly program: IGL2Program,
|
||||
public width: number = 0,
|
||||
public height: number = 0
|
||||
) {
|
||||
@ -1136,47 +782,7 @@ class ShaderTexture2D implements IShaderTexture2D {
|
||||
}
|
||||
}
|
||||
|
||||
interface DrawArraysParam {
|
||||
mode: GLenum;
|
||||
first: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface DrawElementsParam {
|
||||
mode: GLenum;
|
||||
count: number;
|
||||
type: GLenum;
|
||||
offset: GLintptr;
|
||||
}
|
||||
|
||||
interface DrawArraysInstancedParam {
|
||||
mode: GLenum;
|
||||
first: number;
|
||||
count: number;
|
||||
instanceCount: number;
|
||||
}
|
||||
|
||||
interface DrawElementsInstancedParam {
|
||||
mode: GLenum;
|
||||
count: number;
|
||||
type: GLenum;
|
||||
offset: GLintptr;
|
||||
instanceCount: number;
|
||||
}
|
||||
|
||||
export interface DrawParamsMap {
|
||||
[RenderMode.Arrays]: DrawArraysParam;
|
||||
[RenderMode.ArraysInstanced]: DrawArraysInstancedParam;
|
||||
[RenderMode.Elements]: DrawElementsParam;
|
||||
[RenderMode.ElementsInstanced]: DrawElementsInstancedParam;
|
||||
}
|
||||
|
||||
interface ShaderProgramEvent {
|
||||
load: [];
|
||||
unload: [];
|
||||
}
|
||||
|
||||
export class GL2Program extends EventEmitter<ShaderProgramEvent> {
|
||||
export class GL2Program implements IGL2Program {
|
||||
/** 顶点着色器 */
|
||||
private vertex: string = '';
|
||||
/** 片元着色器 */
|
||||
@ -1184,7 +790,7 @@ export class GL2Program extends EventEmitter<ShaderProgramEvent> {
|
||||
/** webgl2上下文 */
|
||||
gl: WebGL2RenderingContext;
|
||||
/** 当前着色器程序的着色器渲染元素 */
|
||||
element: GL2;
|
||||
element: IWebGL2RenderItem;
|
||||
|
||||
/** uniform存放地址 */
|
||||
private uniform: Map<string, IShaderUniform<UniformType>> = new Map();
|
||||
@ -1220,8 +826,7 @@ export class GL2Program extends EventEmitter<ShaderProgramEvent> {
|
||||
/** 着色器代码的前缀,会在设置时自动添加至代码前 */
|
||||
protected readonly prefix: IGL2ProgramPrefix = GL2_PREFIX;
|
||||
|
||||
constructor(shader: GL2, vs?: string, fs?: string) {
|
||||
super();
|
||||
constructor(shader: IWebGL2RenderItem, vs?: string, fs?: string) {
|
||||
if (vs) this.vs(vs);
|
||||
if (fs) this.fs(fs);
|
||||
this.element = shader;
|
||||
@ -1377,6 +982,7 @@ export class GL2Program extends EventEmitter<ShaderProgramEvent> {
|
||||
/**
|
||||
* 检查当前是否需要重新编译着色器,如果需要,则重新编译
|
||||
* @param force 是否强制重新编译
|
||||
* @returns 是否执行了编译操作
|
||||
*/
|
||||
requestCompile(force: boolean = false): boolean {
|
||||
if (!force && !this.shaderDirty) return false;
|
||||
@ -1408,7 +1014,6 @@ export class GL2Program extends EventEmitter<ShaderProgramEvent> {
|
||||
this.attribArray.forEach(v => {
|
||||
v.disable();
|
||||
});
|
||||
this.emit('load');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1418,7 +1023,6 @@ export class GL2Program extends EventEmitter<ShaderProgramEvent> {
|
||||
this.attribArray.forEach(v => {
|
||||
v.enable();
|
||||
});
|
||||
this.emit('unload');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,100 +1,23 @@
|
||||
import {
|
||||
Transform,
|
||||
ERenderItemEvent,
|
||||
RenderItem,
|
||||
MotaOffscreenCanvas2D
|
||||
} from '../core';
|
||||
import { Transform, RenderItem, MotaOffscreenCanvas2D } from '.';
|
||||
import { CanvasStyle } from '../types';
|
||||
import { logger } from '@motajs/common';
|
||||
import { clamp, isEqual, isNil } from 'lodash-es';
|
||||
|
||||
export type CircleParams = [
|
||||
cx?: number,
|
||||
cy?: number,
|
||||
radius?: number,
|
||||
start?: number,
|
||||
end?: number
|
||||
];
|
||||
export type EllipseParams = [
|
||||
cx?: number,
|
||||
cy?: number,
|
||||
radiusX?: number,
|
||||
radiusY?: number,
|
||||
start?: number,
|
||||
end?: number
|
||||
];
|
||||
export type LineParams = [x1: number, y1: number, x2: number, y2: number];
|
||||
export type BezierParams = [
|
||||
sx: number,
|
||||
sy: number,
|
||||
cp1x: number,
|
||||
cp1y: number,
|
||||
cp2x: number,
|
||||
cp2y: number,
|
||||
ex: number,
|
||||
ey: number
|
||||
];
|
||||
export type QuadParams = [
|
||||
sx: number,
|
||||
sy: number,
|
||||
cpx: number,
|
||||
cpy: number,
|
||||
ex: number,
|
||||
ey: number
|
||||
];
|
||||
export type RectRCircleParams = [
|
||||
r1: number,
|
||||
r2?: number,
|
||||
r3?: number,
|
||||
r4?: number
|
||||
];
|
||||
export type RectREllipseParams = [
|
||||
rx1: number,
|
||||
ry1: number,
|
||||
rx2?: number,
|
||||
ry2?: number,
|
||||
rx3?: number,
|
||||
ry3?: number,
|
||||
rx4?: number,
|
||||
ry4?: number
|
||||
];
|
||||
|
||||
export interface ILineProperty {
|
||||
/** 线宽 */
|
||||
lineWidth: number;
|
||||
/** 线的虚线设置 */
|
||||
lineDash?: number[];
|
||||
/** 虚线偏移量 */
|
||||
lineDashOffset?: number;
|
||||
/** 线的连接样式 */
|
||||
lineJoin: CanvasLineJoin;
|
||||
/** 线的顶端样式 */
|
||||
lineCap: CanvasLineCap;
|
||||
/** 线的斜接限制,当连接为miter类型时可填,默认为10 */
|
||||
miterLimit: number;
|
||||
}
|
||||
|
||||
export interface IGraphicProperty extends ILineProperty {
|
||||
/** 渲染模式,参考 {@link GraphicMode} */
|
||||
mode: GraphicMode;
|
||||
/** 填充样式 */
|
||||
fill: CanvasStyle;
|
||||
/** 描边样式 */
|
||||
stroke: CanvasStyle;
|
||||
/** 填充算法 */
|
||||
fillRule: CanvasFillRule;
|
||||
}
|
||||
|
||||
export const enum GraphicMode {
|
||||
/** 仅填充 */
|
||||
Fill,
|
||||
/** 仅描边 */
|
||||
Stroke,
|
||||
/** 先填充,然后描边 */
|
||||
FillAndStroke,
|
||||
/** 先描边,然后填充 */
|
||||
StrokeAndFill
|
||||
}
|
||||
import {
|
||||
ILineProperty,
|
||||
GraphicMode,
|
||||
IGraphicRenderItem,
|
||||
IGraphicCircle,
|
||||
IGraphicEllipse,
|
||||
IGraphicLine,
|
||||
IGraphicBezierCurve,
|
||||
IGraphicQuadBezierCurve,
|
||||
IGraphicPath,
|
||||
RectRCorner,
|
||||
CircleParams,
|
||||
EllipseParams,
|
||||
RectRCircleParams,
|
||||
RectREllipseParams
|
||||
} from './types';
|
||||
|
||||
const enum GraphicModeProp {
|
||||
Fill,
|
||||
@ -102,11 +25,13 @@ const enum GraphicModeProp {
|
||||
StrokeAndFill
|
||||
}
|
||||
|
||||
export interface EGraphicItemEvent extends ERenderItemEvent {}
|
||||
/** 用于点击检测的画布 */
|
||||
const testCanvas = new MotaOffscreenCanvas2D(false);
|
||||
testCanvas.size(1, 1);
|
||||
|
||||
export abstract class GraphicItemBase
|
||||
extends RenderItem<EGraphicItemEvent>
|
||||
implements Required<ILineProperty>
|
||||
extends RenderItem
|
||||
implements Required<ILineProperty>, IGraphicRenderItem
|
||||
{
|
||||
mode: GraphicMode = GraphicMode.Fill;
|
||||
fill: CanvasStyle = '#ddd';
|
||||
@ -118,7 +43,6 @@ export abstract class GraphicItemBase
|
||||
lineCap: CanvasLineCap = 'butt';
|
||||
miterLimit: number = 10;
|
||||
fillRule: CanvasFillRule = 'nonzero';
|
||||
enableCache: boolean = false;
|
||||
|
||||
private propFill: boolean = true;
|
||||
private propStroke: boolean = false;
|
||||
@ -129,6 +53,10 @@ export abstract class GraphicItemBase
|
||||
private cachePath?: Path2D;
|
||||
protected pathDirty: boolean = true;
|
||||
|
||||
constructor(enableCache: boolean = false) {
|
||||
super(enableCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取这个元素的绘制路径
|
||||
*/
|
||||
@ -166,7 +94,7 @@ export abstract class GraphicItemBase
|
||||
}
|
||||
|
||||
protected isActionInElement(x: number, y: number): boolean {
|
||||
const ctx = this.cache.ctx;
|
||||
const ctx = testCanvas.ctx;
|
||||
if (this.pathDirty) {
|
||||
this.cachePath = this.getPath();
|
||||
this.pathDirty = false;
|
||||
@ -387,7 +315,7 @@ export class Rect extends GraphicItemBase {
|
||||
}
|
||||
}
|
||||
|
||||
export class Circle extends GraphicItemBase {
|
||||
export class Circle extends GraphicItemBase implements IGraphicCircle {
|
||||
radius: number = 10;
|
||||
start: number = 0;
|
||||
end: number = Math.PI * 2;
|
||||
@ -462,7 +390,7 @@ export class Circle extends GraphicItemBase {
|
||||
}
|
||||
}
|
||||
|
||||
export class Ellipse extends GraphicItemBase {
|
||||
export class Ellipse extends GraphicItemBase implements IGraphicEllipse {
|
||||
radiusX: number = 10;
|
||||
radiusY: number = 10;
|
||||
start: number = 0;
|
||||
@ -552,7 +480,7 @@ export class Ellipse extends GraphicItemBase {
|
||||
}
|
||||
}
|
||||
|
||||
export class Line extends GraphicItemBase {
|
||||
export class Line extends GraphicItemBase implements IGraphicLine {
|
||||
x1: number = 0;
|
||||
y1: number = 0;
|
||||
x2: number = 0;
|
||||
@ -633,7 +561,10 @@ export class Line extends GraphicItemBase {
|
||||
}
|
||||
}
|
||||
|
||||
export class BezierCurve extends GraphicItemBase {
|
||||
export class BezierCurve
|
||||
extends GraphicItemBase
|
||||
implements IGraphicBezierCurve
|
||||
{
|
||||
sx: number = 0;
|
||||
sy: number = 0;
|
||||
cp1x: number = 0;
|
||||
@ -700,10 +631,6 @@ export class BezierCurve extends GraphicItemBase {
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected isActionInElement(x: number, y: number): boolean {
|
||||
return x >= 0 && x < this.width && y >= 0 && y < this.height;
|
||||
}
|
||||
|
||||
private fitRect() {
|
||||
const left = Math.min(this.sx, this.cp1x, this.cp2x, this.ex);
|
||||
const top = Math.min(this.sy, this.cp1y, this.cp2y, this.ey);
|
||||
@ -767,7 +694,10 @@ export class BezierCurve extends GraphicItemBase {
|
||||
}
|
||||
}
|
||||
|
||||
export class QuadraticCurve extends GraphicItemBase {
|
||||
export class QuadraticCurve
|
||||
extends GraphicItemBase
|
||||
implements IGraphicQuadBezierCurve
|
||||
{
|
||||
sx: number = 0;
|
||||
sy: number = 0;
|
||||
cpx: number = 0;
|
||||
@ -842,10 +772,6 @@ export class QuadraticCurve extends GraphicItemBase {
|
||||
this.pathDirty = true;
|
||||
}
|
||||
|
||||
protected isActionInElement(x: number, y: number): boolean {
|
||||
return x >= 0 && x < this.width && y >= 0 && y < this.height;
|
||||
}
|
||||
|
||||
protected handleProps(
|
||||
key: string,
|
||||
prevValue: any,
|
||||
@ -890,7 +816,7 @@ export class QuadraticCurve extends GraphicItemBase {
|
||||
}
|
||||
}
|
||||
|
||||
export class Path extends GraphicItemBase {
|
||||
export class Path extends GraphicItemBase implements IGraphicPath {
|
||||
/** 路径 */
|
||||
path: Path2D = new Path2D();
|
||||
|
||||
@ -901,6 +827,14 @@ export class Path extends GraphicItemBase {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置此元素的路径
|
||||
*/
|
||||
resetPath() {
|
||||
this.path = new Path2D();
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 为路径添加路径
|
||||
* @param path 要添加的路径
|
||||
@ -911,10 +845,6 @@ export class Path extends GraphicItemBase {
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected isActionInElement(x: number, y: number): boolean {
|
||||
return x >= 0 && x < this.width && y >= 0 && y < this.height;
|
||||
}
|
||||
|
||||
protected handleProps(
|
||||
key: string,
|
||||
prevValue: any,
|
||||
@ -932,13 +862,6 @@ export class Path extends GraphicItemBase {
|
||||
}
|
||||
}
|
||||
|
||||
export const enum RectRCorner {
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomRight,
|
||||
BottomLeft
|
||||
}
|
||||
|
||||
export class RectR extends GraphicItemBase {
|
||||
/** 圆角属性,四元素数组,每个元素是一个二元素数组,表示这个角的半径,顺序为 左上,右上,右下,左下 */
|
||||
readonly corner: [radiusX: number, radiusY: number][] = [
|
||||
@ -1,11 +1,14 @@
|
||||
export * from './adapter';
|
||||
export * from './canvas2d';
|
||||
export * from './container';
|
||||
export * from './custom';
|
||||
export * from './event';
|
||||
export * from './gl2';
|
||||
export * from './graphics';
|
||||
export * from './item';
|
||||
export * from './misc';
|
||||
export * from './render';
|
||||
export * from './shader';
|
||||
export * from './sprite';
|
||||
export * from './transform';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,21 +1,12 @@
|
||||
import {
|
||||
ERenderItemEvent,
|
||||
RenderItem,
|
||||
RenderItemPosition,
|
||||
Transform,
|
||||
MotaOffscreenCanvas2D
|
||||
} from '../core';
|
||||
import { RenderItem, Transform, MotaOffscreenCanvas2D } from '.';
|
||||
import { CanvasStyle } from '../types';
|
||||
import { Font } from '../style';
|
||||
import { IRenderImage, IRenderText } from './types';
|
||||
|
||||
/** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */
|
||||
const SAFE_PAD = 1;
|
||||
|
||||
export interface ETextEvent extends ERenderItemEvent {
|
||||
setText: [text: string, width: number, height: number];
|
||||
}
|
||||
|
||||
export class Text extends RenderItem<ETextEvent> {
|
||||
export class Text extends RenderItem implements IRenderText {
|
||||
text: string;
|
||||
|
||||
fillStyle?: CanvasStyle = '#fff';
|
||||
@ -28,14 +19,13 @@ export class Text extends RenderItem<ETextEvent> {
|
||||
|
||||
private static measureCanvas = new MotaOffscreenCanvas2D();
|
||||
|
||||
constructor(text: string = '', type: RenderItemPosition = 'static') {
|
||||
super(type, false);
|
||||
constructor(text: string = '', enableCache: boolean = false) {
|
||||
super(enableCache);
|
||||
|
||||
this.text = text;
|
||||
if (text.length > 0) {
|
||||
this.requestBeforeFrame(() => {
|
||||
this.calBox();
|
||||
this.emit('setText', text, this.width, this.height);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -79,8 +69,7 @@ export class Text extends RenderItem<ETextEvent> {
|
||||
setText(text: string) {
|
||||
this.text = text;
|
||||
this.calBox();
|
||||
this.update(this);
|
||||
this.emit('setText', text, this.width, this.height);
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -157,13 +146,11 @@ export class Text extends RenderItem<ETextEvent> {
|
||||
}
|
||||
}
|
||||
|
||||
export interface EImageEvent extends ERenderItemEvent {}
|
||||
|
||||
export class Image extends RenderItem<EImageEvent> {
|
||||
export class Image extends RenderItem implements IRenderImage {
|
||||
image: CanvasImageSource;
|
||||
|
||||
constructor(image: CanvasImageSource, type: RenderItemPosition = 'static') {
|
||||
super(type, false);
|
||||
constructor(image: CanvasImageSource, enableCache: boolean = false) {
|
||||
super(enableCache);
|
||||
this.image = image;
|
||||
if (image instanceof VideoFrame || image instanceof SVGElement) {
|
||||
this.size(200, 200);
|
||||
@ -207,7 +194,7 @@ export class Comment extends RenderItem {
|
||||
readonly isComment: boolean = true;
|
||||
|
||||
constructor(public text: string = '') {
|
||||
super('static', false, false);
|
||||
super(false);
|
||||
this.hide();
|
||||
}
|
||||
|
||||
@ -219,8 +206,4 @@ export class Comment extends RenderItem {
|
||||
_canvas: MotaOffscreenCanvas2D,
|
||||
_transform: Transform
|
||||
): void {}
|
||||
|
||||
protected handleProps(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { MotaOffscreenCanvas2D } from './canvas2d';
|
||||
import { Container } from './container';
|
||||
import { Container, CustomContainer } from './container';
|
||||
import {
|
||||
ActionType,
|
||||
IActionEvent,
|
||||
@ -9,41 +9,68 @@ import {
|
||||
MouseType,
|
||||
WheelType
|
||||
} from './event';
|
||||
import { IRenderTreeRoot, RenderItem } from './item';
|
||||
import { Transform } from './transform';
|
||||
import {
|
||||
IMotaRendererConfig,
|
||||
RenderItemTags,
|
||||
IRenderItem,
|
||||
IRenderItemInstanceMap,
|
||||
IRenderItemParameterMap,
|
||||
IRenderTreeRoot,
|
||||
RenderItemConstructor,
|
||||
ExcitableDelegation
|
||||
} from './types';
|
||||
import { IExcitable, IExcitation, RafExcitation } from '@motajs/animate';
|
||||
import { CustomRenderItem } from './custom';
|
||||
import { Comment, Image, Text } from './misc';
|
||||
import { Shader } from './shader';
|
||||
import {
|
||||
BezierCurve,
|
||||
Circle,
|
||||
Ellipse,
|
||||
Line,
|
||||
Path,
|
||||
QuadraticCurve,
|
||||
Rect,
|
||||
RectR
|
||||
} from './graphics';
|
||||
|
||||
interface TouchInfo {
|
||||
/** 这次触摸在渲染系统的标识符 */
|
||||
identifier: number;
|
||||
readonly identifier: number;
|
||||
/** 浏览器的 clientX,用于判断这个触点有没有移动 */
|
||||
clientX: number;
|
||||
readonly clientX: number;
|
||||
/** 浏览器的 clientY,用于判断这个触点有没有移动 */
|
||||
clientY: number;
|
||||
readonly clientY: number;
|
||||
/** 是否覆盖在了当前元素上 */
|
||||
hovered: boolean;
|
||||
readonly hovered: boolean;
|
||||
}
|
||||
|
||||
interface MouseInfo {
|
||||
/** 这个鼠标按键的标识符 */
|
||||
identifier: number;
|
||||
readonly identifier: number;
|
||||
}
|
||||
|
||||
export interface MotaRendererConfig {
|
||||
/** 要挂载到哪个画布上,可以填 css 选择器或画布元素本身 */
|
||||
canvas: string | HTMLCanvasElement;
|
||||
/** 画布的宽度,所有渲染操作会自行适配缩放 */
|
||||
width: number;
|
||||
/** 画布的高度,所有渲染操作会自行适配缩放 */
|
||||
height: number;
|
||||
/** 是否启用不透明度通道,默认启用 */
|
||||
alpha?: boolean;
|
||||
interface DelegatedExcitable extends IExcitable<number> {
|
||||
/** 委托内容的 id */
|
||||
readonly id: number;
|
||||
/** 委托内容的原始对象 */
|
||||
readonly obj: ExcitableDelegation;
|
||||
/** 委托 excitable 是否永久执行 */
|
||||
readonly forever: boolean;
|
||||
/** 委托 excitable 的开始时刻 */
|
||||
readonly startTime: number;
|
||||
/** 委托 excitable 的持续时间 */
|
||||
readonly time: number;
|
||||
/** 委托结束时执行的函数 */
|
||||
readonly end?: () => void;
|
||||
}
|
||||
|
||||
export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
static list: Map<string, MotaRenderer> = new Map();
|
||||
|
||||
export class MotaRenderer
|
||||
extends Container
|
||||
implements IRenderTreeRoot, IExcitable<number>
|
||||
{
|
||||
/** 所有连接到此根元素的渲染元素的 id 到元素自身的映射 */
|
||||
protected idMap: Map<string, RenderItem> = new Map();
|
||||
protected readonly idMap: Map<string, IRenderItem> = new Map();
|
||||
|
||||
/** 最后一次按下的鼠标按键,用于处理鼠标移动 */
|
||||
private lastMouse: MouseType = MouseType.None;
|
||||
@ -61,16 +88,40 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
/** 根据捕获行为判断光标样式 */
|
||||
private targetCursor: string = 'auto';
|
||||
/** 当前鼠标覆盖的元素 */
|
||||
private hoveredElement: Set<RenderItem> = new Set();
|
||||
private hoveredElement: Set<IRenderItem> = new Set();
|
||||
/** 本次交互前鼠标覆盖的元素 */
|
||||
private beforeHovered: Set<RenderItem> = new Set();
|
||||
|
||||
target!: MotaOffscreenCanvas2D;
|
||||
private beforeHovered: Set<IRenderItem> = new Set();
|
||||
|
||||
/** 渲染至的目标画布 */
|
||||
readonly target!: MotaOffscreenCanvas2D;
|
||||
/** 当前元素是根元素 */
|
||||
readonly isRoot = true;
|
||||
|
||||
constructor(config: MotaRendererConfig) {
|
||||
super('static', false);
|
||||
/** 当前元素的激励源 */
|
||||
readonly excitation!: IExcitation<number>;
|
||||
|
||||
/** 下一帧之前需要执行的内容 */
|
||||
private readonly beforeFrame: Set<() => void> = new Set();
|
||||
/** 下一帧之后需要执行的内容 */
|
||||
private readonly afterFrame: Set<() => void> = new Set();
|
||||
/** 委托 excitables 的计数器 */
|
||||
private delegationCounter: number = 0;
|
||||
/** 委托执行的 excitables */
|
||||
private readonly excitables: Map<number, DelegatedExcitable> = new Map();
|
||||
/** 委托执行的 excitables 到其 id 的映射 */
|
||||
private readonly excitablesMap: Map<
|
||||
ExcitableDelegation,
|
||||
DelegatedExcitable
|
||||
> = new Map();
|
||||
/** 执行完毕需要删除的 excitables */
|
||||
private readonly toDeleteExcitables: Set<number> = new Set();
|
||||
|
||||
/** 标签注册信息 */
|
||||
private readonly tagRegistry: Map<string, RenderItemConstructor> =
|
||||
new Map();
|
||||
|
||||
constructor(config: IMotaRendererConfig) {
|
||||
super(false);
|
||||
|
||||
const canvas = this.getMountCanvas(config.canvas);
|
||||
if (!canvas) {
|
||||
@ -80,24 +131,132 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
this.target = new MotaOffscreenCanvas2D(config.alpha ?? true, canvas);
|
||||
this.size(config.width, config.height);
|
||||
this.target.setAntiAliasing(false);
|
||||
if (config.excitaion) {
|
||||
this.excitation = config.excitaion;
|
||||
} else {
|
||||
this.excitation = new RafExcitation();
|
||||
}
|
||||
|
||||
this.setAnchor(0.5, 0.5);
|
||||
|
||||
MotaRenderer.list.set(canvas.id, this);
|
||||
|
||||
const update = () => {
|
||||
this.requestRenderFrame(() => {
|
||||
this.refresh();
|
||||
update();
|
||||
});
|
||||
};
|
||||
|
||||
update();
|
||||
this.listen();
|
||||
|
||||
this.setScale(1);
|
||||
this.excited = this.excited.bind(this);
|
||||
this.excitation.add(this);
|
||||
this.registerIntrinsicTags();
|
||||
}
|
||||
|
||||
//#region 渲染相关
|
||||
|
||||
excited(payload: number): void {
|
||||
this.beforeFrame.forEach(v => v());
|
||||
this.beforeFrame.clear();
|
||||
this.refresh();
|
||||
this.afterFrame.forEach(v => v());
|
||||
this.afterFrame.clear();
|
||||
|
||||
this.excitables.forEach((ex, key) => {
|
||||
if (!ex.forever && payload - ex.startTime >= ex.time) {
|
||||
this.toDeleteExcitables.add(key);
|
||||
ex.end?.();
|
||||
} else {
|
||||
ex.excited(payload);
|
||||
}
|
||||
});
|
||||
this.toDeleteExcitables.forEach(key => this.excitables.delete(key));
|
||||
}
|
||||
|
||||
private getMountCanvas(
|
||||
canvas: string | HTMLCanvasElement
|
||||
): HTMLCanvasElement | undefined {
|
||||
if (typeof canvas === 'string') {
|
||||
return document.querySelector(canvas) as HTMLCanvasElement;
|
||||
} else {
|
||||
return canvas;
|
||||
}
|
||||
}
|
||||
|
||||
update(_item: IRenderItem = this) {
|
||||
this.cacheDirty = true;
|
||||
}
|
||||
|
||||
protected refresh(): void {
|
||||
if (!this.cacheDirty) return;
|
||||
this.target.clear();
|
||||
this.renderContent(this.target);
|
||||
}
|
||||
|
||||
getCanvas(): HTMLCanvasElement {
|
||||
return this.target.canvas;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 标签元素
|
||||
|
||||
createElement<K extends RenderItemTags>(
|
||||
tag: K,
|
||||
...params: IRenderItemParameterMap[K]
|
||||
): IRenderItemInstanceMap[K];
|
||||
createElement<P extends any[], I extends IRenderItem>(
|
||||
ele: new (...params: P) => I,
|
||||
...params: P
|
||||
): I;
|
||||
createElement(
|
||||
ele: RenderItemTags | RenderItemConstructor,
|
||||
...params: any[]
|
||||
): IRenderItem {
|
||||
if (typeof ele === 'string') {
|
||||
const Cons = this.tagRegistry.get(ele);
|
||||
if (!Cons) {
|
||||
logger.error(15, ele);
|
||||
return new Container(false);
|
||||
} else {
|
||||
return new Cons(...params);
|
||||
}
|
||||
} else {
|
||||
return new ele(...params);
|
||||
}
|
||||
}
|
||||
|
||||
registerElement(tag: string, cons: RenderItemConstructor): void {
|
||||
if (this.tagRegistry.has(tag)) {
|
||||
logger.error(14);
|
||||
return;
|
||||
} else {
|
||||
this.tagRegistry.set(tag, cons);
|
||||
}
|
||||
}
|
||||
|
||||
hasTag(tag: string): boolean {
|
||||
return this.tagRegistry.has(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册内置渲染标签
|
||||
*/
|
||||
private registerIntrinsicTags() {
|
||||
this.registerElement('container', Container);
|
||||
this.registerElement('custom', CustomRenderItem);
|
||||
this.registerElement('text', Text);
|
||||
this.registerElement('image', Image);
|
||||
this.registerElement('shader', Shader);
|
||||
this.registerElement('comment', Comment);
|
||||
this.registerElement('template', Container);
|
||||
this.registerElement('custom-container', CustomContainer);
|
||||
this.registerElement('g-rect', Rect);
|
||||
this.registerElement('g-circle', Circle);
|
||||
this.registerElement('g-ellipse', Ellipse);
|
||||
this.registerElement('g-line', Line);
|
||||
this.registerElement('g-bezier', BezierCurve);
|
||||
this.registerElement('g-quad', QuadraticCurve);
|
||||
this.registerElement('g-path', Path);
|
||||
this.registerElement('g-rectr', RectR);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 尺寸缩放
|
||||
|
||||
/**
|
||||
* 设置这个渲染器的缩放比
|
||||
* @param scale 缩放比
|
||||
@ -106,6 +265,10 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
this.onResize(scale);
|
||||
}
|
||||
|
||||
getScale() {
|
||||
return this.target.scale;
|
||||
}
|
||||
|
||||
onResize(scale: number): void {
|
||||
this.target.setScale(scale);
|
||||
const width = this.target.width * scale;
|
||||
@ -115,23 +278,20 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
super.onResize(scale);
|
||||
}
|
||||
|
||||
private getMountCanvas(canvas: string | HTMLCanvasElement) {
|
||||
if (typeof canvas === 'string') {
|
||||
return document.querySelector(canvas) as HTMLCanvasElement;
|
||||
} else {
|
||||
return canvas;
|
||||
}
|
||||
}
|
||||
|
||||
size(width: number, height: number): void {
|
||||
super.size(width, height);
|
||||
this.target.size(width, height);
|
||||
this.transform.setTranslate(width / 2, height / 2);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 事件处理
|
||||
|
||||
private listen() {
|
||||
// 画布监听
|
||||
const canvas = this.target.canvas;
|
||||
|
||||
canvas.addEventListener('mousedown', ev => {
|
||||
const mouse = this.getMouseType(ev);
|
||||
this.lastMouse = mouse;
|
||||
@ -178,47 +338,6 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
this.hoveredElement.clear();
|
||||
this.beforeHovered.clear();
|
||||
});
|
||||
document.addEventListener('touchstart', ev => {
|
||||
this.createTouchAction(ev, ActionType.Down).forEach(v => {
|
||||
this.captureEvent(ActionType.Down, v);
|
||||
});
|
||||
});
|
||||
document.addEventListener('touchend', ev => {
|
||||
this.createTouchAction(ev, ActionType.Up).forEach(v => {
|
||||
this.captureEvent(ActionType.Up, v);
|
||||
this.captureEvent(ActionType.Click, v);
|
||||
});
|
||||
[...ev.touches].forEach(v => {
|
||||
this.touchInfo.delete(v.identifier);
|
||||
});
|
||||
});
|
||||
document.addEventListener('touchcancel', ev => {
|
||||
this.createTouchAction(ev, ActionType.Up).forEach(v => {
|
||||
this.captureEvent(ActionType.Up, v);
|
||||
});
|
||||
[...ev.touches].forEach(v => {
|
||||
this.touchInfo.delete(v.identifier);
|
||||
});
|
||||
});
|
||||
document.addEventListener('touchmove', ev => {
|
||||
this.createTouchAction(ev, ActionType.Move).forEach(v => {
|
||||
const list = this.touchInfo.values();
|
||||
if (!list.some(vv => v.identifier === vv.identifier)) {
|
||||
return;
|
||||
}
|
||||
const temp = this.beforeHovered;
|
||||
temp.clear();
|
||||
this.beforeHovered = this.hoveredElement;
|
||||
this.hoveredElement = temp;
|
||||
this.captureEvent(ActionType.Move, v);
|
||||
this.checkTouchEnterLeave(
|
||||
ev,
|
||||
v,
|
||||
this.beforeHovered,
|
||||
this.hoveredElement
|
||||
);
|
||||
});
|
||||
});
|
||||
canvas.addEventListener('wheel', ev => {
|
||||
this.captureEvent(
|
||||
ActionType.Wheel,
|
||||
@ -240,6 +359,70 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
document.addEventListener('click', clear, { signal });
|
||||
document.addEventListener('mouseenter', clear, { signal });
|
||||
document.addEventListener('mouseleave', clear, { signal });
|
||||
window.addEventListener(
|
||||
'resize',
|
||||
() => {
|
||||
this.requestAfterFrame(() => this.refreshAllChildren());
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
document.addEventListener(
|
||||
'touchstart',
|
||||
ev => {
|
||||
this.createTouchAction(ev, ActionType.Down).forEach(v => {
|
||||
this.captureEvent(ActionType.Down, v);
|
||||
});
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
document.addEventListener(
|
||||
'touchend',
|
||||
ev => {
|
||||
this.createTouchAction(ev, ActionType.Up).forEach(v => {
|
||||
this.captureEvent(ActionType.Up, v);
|
||||
this.captureEvent(ActionType.Click, v);
|
||||
});
|
||||
[...ev.touches].forEach(v => {
|
||||
this.touchInfo.delete(v.identifier);
|
||||
});
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
document.addEventListener(
|
||||
'touchcancel',
|
||||
ev => {
|
||||
this.createTouchAction(ev, ActionType.Up).forEach(v => {
|
||||
this.captureEvent(ActionType.Up, v);
|
||||
});
|
||||
[...ev.touches].forEach(v => {
|
||||
this.touchInfo.delete(v.identifier);
|
||||
});
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
document.addEventListener(
|
||||
'touchmove',
|
||||
ev => {
|
||||
this.createTouchAction(ev, ActionType.Move).forEach(v => {
|
||||
const list = this.touchInfo.values();
|
||||
if (!list.some(vv => v.identifier === vv.identifier)) {
|
||||
return;
|
||||
}
|
||||
const temp = this.beforeHovered;
|
||||
temp.clear();
|
||||
this.beforeHovered = this.hoveredElement;
|
||||
this.hoveredElement = temp;
|
||||
this.captureEvent(ActionType.Move, v);
|
||||
this.checkTouchEnterLeave(
|
||||
ev,
|
||||
v,
|
||||
this.beforeHovered,
|
||||
this.hoveredElement
|
||||
);
|
||||
});
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
}
|
||||
|
||||
private isTouchInCanvas(clientX: number, clientY: number) {
|
||||
@ -312,7 +495,7 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
private createMouseActionBase(
|
||||
event: MouseEvent,
|
||||
id: number,
|
||||
target: RenderItem = this,
|
||||
target: IRenderItem = this,
|
||||
mouse: MouseType = this.getMouseType(event)
|
||||
): IActionEventBase {
|
||||
return {
|
||||
@ -331,7 +514,7 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
private createTouchActionBase(
|
||||
event: TouchEvent,
|
||||
id: number,
|
||||
target: RenderItem
|
||||
target: IRenderItem
|
||||
): IActionEventBase {
|
||||
return {
|
||||
identifier: id,
|
||||
@ -488,8 +671,8 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
private checkMouseEnterLeave(
|
||||
event: MouseEvent,
|
||||
ev: IActionEvent,
|
||||
before: Set<RenderItem>,
|
||||
now: Set<RenderItem>
|
||||
before: Set<IRenderItem>,
|
||||
now: Set<IRenderItem>
|
||||
) {
|
||||
// 先 leave,再 enter
|
||||
before.forEach(v => {
|
||||
@ -513,8 +696,8 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
private checkTouchEnterLeave(
|
||||
event: TouchEvent,
|
||||
ev: IActionEvent,
|
||||
before: Set<RenderItem>,
|
||||
now: Set<RenderItem>
|
||||
before: Set<IRenderItem>,
|
||||
now: Set<IRenderItem>
|
||||
) {
|
||||
// 先 leave,再 enter
|
||||
before.forEach(v => {
|
||||
@ -535,22 +718,16 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
});
|
||||
}
|
||||
|
||||
update(_item: RenderItem = this) {
|
||||
this.cacheDirty = true;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
protected refresh(): void {
|
||||
if (!this.cacheDirty) return;
|
||||
this.target.clear();
|
||||
this.renderContent(this.target, Transform.identity);
|
||||
}
|
||||
//#region 元素处理
|
||||
|
||||
/**
|
||||
* 根据渲染元素的id获取一个渲染元素
|
||||
* @param id 要获取的渲染元素id
|
||||
* @returns
|
||||
*/
|
||||
getElementById(id: string): RenderItem | null {
|
||||
getElementById(id: string): IRenderItem | null {
|
||||
if (id.length === 0) return null;
|
||||
const item = this.idMap.get(id);
|
||||
if (item) return item;
|
||||
@ -563,7 +740,7 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
}
|
||||
}
|
||||
|
||||
private searchElement(ele: RenderItem, id: string): RenderItem | null {
|
||||
private searchElement(ele: IRenderItem, id: string): IRenderItem | null {
|
||||
for (const child of ele.children) {
|
||||
if (child.id === id) return child;
|
||||
else {
|
||||
@ -574,7 +751,7 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
return null;
|
||||
}
|
||||
|
||||
connect(item: RenderItem): void {
|
||||
connect(item: IRenderItem): void {
|
||||
if (item.id.length === 0) return;
|
||||
const existed = this.idMap.get(item.id);
|
||||
if (existed) {
|
||||
@ -585,11 +762,11 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(item: RenderItem): void {
|
||||
disconnect(item: IRenderItem): void {
|
||||
this.idMap.delete(item.id);
|
||||
}
|
||||
|
||||
modifyId(item: RenderItem, previous: string, current: string): void {
|
||||
modifyId(item: IRenderItem, previous: string, current: string): void {
|
||||
this.idMap.delete(previous);
|
||||
if (current.length !== 0) {
|
||||
if (this.idMap.has(item.id)) {
|
||||
@ -600,24 +777,93 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
}
|
||||
}
|
||||
|
||||
getCanvas(): HTMLCanvasElement {
|
||||
return this.target.canvas;
|
||||
}
|
||||
|
||||
hoverElement(element: RenderItem): void {
|
||||
hoverElement(element: IRenderItem): void {
|
||||
if (element.cursor !== 'inherit') {
|
||||
this.targetCursor = element.cursor;
|
||||
}
|
||||
this.hoveredElement.add(element);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 渲染绑定
|
||||
|
||||
requestBeforeFrame(fn: () => void): void {
|
||||
this.beforeFrame.add(fn);
|
||||
}
|
||||
|
||||
requestAfterFrame(fn: () => void): void {
|
||||
this.afterFrame.add(fn);
|
||||
}
|
||||
|
||||
delegateExcitable(
|
||||
fn: ExcitableDelegation,
|
||||
time?: number,
|
||||
end?: () => void
|
||||
): number {
|
||||
const index = this.delegationCounter++;
|
||||
const info: DelegatedExcitable = {
|
||||
id: index,
|
||||
obj: fn,
|
||||
excited: typeof fn === 'function' ? fn : fn.excited,
|
||||
startTime: this.excitation.payload(),
|
||||
time: time ?? 0,
|
||||
forever: time === void 0,
|
||||
end
|
||||
};
|
||||
this.excitables.set(index, info);
|
||||
this.excitablesMap.set(fn, info);
|
||||
return index;
|
||||
}
|
||||
|
||||
removeExcitable(id: number, callEnd: boolean = true): boolean {
|
||||
const info = this.excitables.get(id);
|
||||
if (!info) return false;
|
||||
if (callEnd) {
|
||||
info.end?.();
|
||||
}
|
||||
this.excitables.delete(id);
|
||||
this.excitablesMap.delete(info.obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
removeExcitableObject(
|
||||
excitable: ExcitableDelegation,
|
||||
callEnd: boolean = true
|
||||
): boolean {
|
||||
const info = this.excitablesMap.get(excitable);
|
||||
if (!info) return false;
|
||||
if (callEnd) {
|
||||
info.end?.();
|
||||
}
|
||||
this.excitables.delete(info.id);
|
||||
this.excitablesMap.delete(info.obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
hasExcitable(id: number): boolean {
|
||||
return this.excitables.has(id);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 元素销毁
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
MotaRenderer.list.delete(this.id);
|
||||
this.excitation.destroy();
|
||||
this.abort?.abort();
|
||||
}
|
||||
|
||||
private toTagString(item: RenderItem, space: number, deep: number): string {
|
||||
//#endregion
|
||||
|
||||
//#region 调试功能
|
||||
|
||||
private toTagString(
|
||||
item: IRenderItem,
|
||||
space: number,
|
||||
deep: number
|
||||
): string {
|
||||
if (item.isComment) return '';
|
||||
const name = item.constructor.name;
|
||||
if (item.children.size === 0) {
|
||||
@ -647,18 +893,5 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
return this.toTagString(this, space, 0);
|
||||
}
|
||||
|
||||
static get(id: string) {
|
||||
return this.list.get(id);
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
MotaRenderer.list.forEach(v =>
|
||||
v.requestAfterFrame(() => v.refreshAllChildren())
|
||||
);
|
||||
});
|
||||
|
||||
// @ts-expect-error debug
|
||||
window.logTagTree = () => {
|
||||
console.log(MotaRenderer.get('render-main')?.toTagTree());
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { MotaOffscreenCanvas2D } from './canvas2d';
|
||||
import { EGL2Event, GL2, GL2Program, IGL2ProgramPrefix } from './gl2';
|
||||
import { RenderItemPosition } from './item';
|
||||
import { GL2, GL2Program } from './gl2';
|
||||
import { IGL2ProgramPrefix, IWebGL2RenderItem } from './types';
|
||||
|
||||
const SHADER_PREFIX: IGL2ProgramPrefix = {
|
||||
VERTEX: /* glsl */ `#version 300 es
|
||||
@ -34,15 +34,7 @@ void main() {
|
||||
}
|
||||
`;
|
||||
|
||||
export interface EShaderEvent extends EGL2Event {}
|
||||
|
||||
export class Shader<E extends EShaderEvent = EShaderEvent> extends GL2<
|
||||
EShaderEvent | E
|
||||
> {
|
||||
constructor(type: RenderItemPosition = 'static') {
|
||||
super(type);
|
||||
}
|
||||
|
||||
export class Shader extends GL2 {
|
||||
protected drawScene(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
gl: WebGL2RenderingContext
|
||||
@ -57,7 +49,7 @@ export class Shader<E extends EShaderEvent = EShaderEvent> extends GL2<
|
||||
export class ShaderProgram extends GL2Program {
|
||||
protected readonly prefix: IGL2ProgramPrefix = SHADER_PREFIX;
|
||||
|
||||
constructor(gl2: GL2, vs?: string, fs?: string) {
|
||||
constructor(gl2: IWebGL2RenderItem, vs?: string, fs?: string) {
|
||||
super(gl2, vs, fs);
|
||||
if (!vs) this.vs(DEFAULT_VS);
|
||||
if (!fs) this.fs(DEFAULT_FS);
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
import {
|
||||
ERenderItemEvent,
|
||||
RenderFunction,
|
||||
RenderItem,
|
||||
RenderItemPosition
|
||||
} from './item';
|
||||
import { MotaOffscreenCanvas2D } from './canvas2d';
|
||||
import { Transform } from './transform';
|
||||
|
||||
export interface ESpriteEvent extends ERenderItemEvent {}
|
||||
|
||||
export class Sprite<
|
||||
E extends ESpriteEvent = ESpriteEvent
|
||||
> extends RenderItem<E> {
|
||||
renderFn: RenderFunction;
|
||||
|
||||
/**
|
||||
* 创建一个精灵,可以自由在上面渲染内容
|
||||
* @param type 渲染模式,absolute表示绝对位置,不会跟随自身的Transform改变
|
||||
* @param cache 是否启用缓存机制
|
||||
*/
|
||||
constructor(
|
||||
type: RenderItemPosition = 'static',
|
||||
cache: boolean = true,
|
||||
fall: boolean = false
|
||||
) {
|
||||
super(type, cache, fall);
|
||||
this.type = type;
|
||||
this.renderFn = () => {};
|
||||
}
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
transform: Transform
|
||||
): void {
|
||||
canvas.ctx.save();
|
||||
this.renderFn(canvas, transform);
|
||||
canvas.ctx.restore();
|
||||
}
|
||||
|
||||
setRenderFn(fn: RenderFunction) {
|
||||
this.renderFn = fn;
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
protected handleProps(
|
||||
key: string,
|
||||
_prevValue: any,
|
||||
nextValue: any
|
||||
): boolean {
|
||||
switch (key) {
|
||||
case 'render':
|
||||
if (!this.assertType(nextValue, 'function', key)) return false;
|
||||
this.setRenderFn(nextValue);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
1728
packages/render/src/core/types.ts
Normal file
1728
packages/render/src/core/types.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,34 +1,6 @@
|
||||
import { TimingFn } from 'mutate-animate';
|
||||
import { JSX } from 'vue/jsx-runtime';
|
||||
import { DefineComponent, DefineSetupFnComponent } from 'vue';
|
||||
import { MotaOffscreenCanvas2D } from './canvas2d';
|
||||
import { Transform } from './transform';
|
||||
|
||||
export type Props<
|
||||
T extends
|
||||
| keyof JSX.IntrinsicElements
|
||||
| DefineSetupFnComponent<any>
|
||||
| DefineComponent
|
||||
> = T extends keyof JSX.IntrinsicElements
|
||||
? JSX.IntrinsicElements[T]
|
||||
: T extends DefineSetupFnComponent<any>
|
||||
? InstanceType<T>['$props'] & InstanceType<T>['$emits']
|
||||
: T extends DefineComponent
|
||||
? InstanceType<T>['$props'] & InstanceType<T>['$emits']
|
||||
: unknown;
|
||||
|
||||
export type ElementLocator = [
|
||||
x?: number,
|
||||
y?: number,
|
||||
width?: number,
|
||||
height?: number,
|
||||
anchorX?: number,
|
||||
anchorY?: number
|
||||
];
|
||||
|
||||
export type ElementAnchor = [x: number, y: number];
|
||||
export type ElementScale = [x: number, y: number];
|
||||
|
||||
const { gl, gl2 } = checkSupport();
|
||||
|
||||
function checkSupport() {
|
||||
@ -47,28 +19,6 @@ export function isWebGL2Supported() {
|
||||
return gl2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将两个缓动函数做加法
|
||||
*/
|
||||
export function addTiming(timing1: TimingFn, timing2: TimingFn): TimingFn {
|
||||
return (p: number) => timing1(p) + timing2(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将两个缓动函数做乘法
|
||||
*/
|
||||
export function multiplyTiming(timing1: TimingFn, timing2: TimingFn): TimingFn {
|
||||
return (p: number) => timing1(p) * timing2(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个集合是否相等
|
||||
*/
|
||||
export function isSetEqual<T>(set1: Set<T>, set2: Set<T>) {
|
||||
if (set1 === set2) return true;
|
||||
else return set1.size === set2.size && set1.isSubsetOf(set2);
|
||||
}
|
||||
|
||||
export function transformCanvas(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
transform: Transform
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
export * from './graphics';
|
||||
export * from './misc';
|
||||
@ -1,5 +1,4 @@
|
||||
export * from './assets';
|
||||
export * from './core';
|
||||
export * from './elements';
|
||||
export * from './style';
|
||||
export * from './types';
|
||||
|
||||
@ -2375,7 +2375,7 @@ ui.prototype._drawQuickShop = function () {
|
||||
};
|
||||
});
|
||||
choices.push('返回游戏');
|
||||
this.drawChoices(null, choices, void 0, true);
|
||||
this.drawChoices2(null, choices, void 0, true);
|
||||
};
|
||||
|
||||
ui.prototype._drawSyncSave = function () {
|
||||
|
||||
@ -15,10 +15,9 @@
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@motajs/*": ["packages/*/src"],
|
||||
"@user/*": ["packages-user/*/src"]
|
||||
"@motajs/*": ["./packages/*/src"],
|
||||
"@user/*": ["./packages-user/*/src"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
@ -8,12 +8,12 @@
|
||||
"strict": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"script/**/*.ts",
|
||||
"docs/**/*.ts",
|
||||
"docs/.vitepress/api.ts",
|
||||
"docs/.vitepress/config.ts",
|
||||
"docs/.vitepress/apiSidebar.ts",
|
||||
"docs/.vitepress/init.ts"
|
||||
"./vite.config.ts",
|
||||
"./script/**/*.ts",
|
||||
"./docs/**/*.ts",
|
||||
"./docs/.vitepress/api.ts",
|
||||
"./docs/.vitepress/config.ts",
|
||||
"./docs/.vitepress/apiSidebar.ts",
|
||||
"./docs/.vitepress/init.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@ -6,9 +6,8 @@ import postcssPresetEnv from 'postcss-preset-env';
|
||||
import * as glob from 'glob';
|
||||
|
||||
const custom = [
|
||||
'container', 'image', 'sprite', 'shader', 'text', 'comment', 'custom',
|
||||
'layer', 'layer-group', 'animate', 'damage', 'graphics', 'icon', 'winskin',
|
||||
'container-custom', 'map-render'
|
||||
'container', 'custom', 'text', 'image', 'shader', 'comment', 'custom-container',
|
||||
'map-render', 'animate', 'damage', 'graphics', 'icon', 'winskin',
|
||||
];
|
||||
|
||||
const aliases = glob.sync('packages/*/src').map((srcPath) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user