mirror of
https://github.com/motajs/template.git
synced 2026-11-02 23:14:34 +08:00
init: 2.B
This commit is contained in:
commit
6bf32174d8
610 files changed
+151444
No files matched your search
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@motajs/render-core",
|
||||
"dependencies": {
|
||||
"@motajs/common": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
type AdapterFunction<T> = (item: T, ...params: any[]) => Promise<any>;
|
||||
type SyncAdapterFunction<T> = (item: T, ...params: any[]) => any;
|
||||
type GlobalAdapterFunction = (...params: any[]) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 渲染适配器,用作渲染层与数据层沟通的桥梁,用于在数据层等待渲染层执行,常用与动画等。
|
||||
* 例如移动图块,移动图块操作先由数据层执行,删除原有图块,然后通知渲染层进行移动动画,
|
||||
* 而移动动画是耗时的,因此需要通过本类进行适配,实现等待移动动画执行完毕,然后进行后续操作
|
||||
*/
|
||||
export class RenderAdapter<T> {
|
||||
static adapters: Map<string, RenderAdapter<any>> = new Map();
|
||||
|
||||
/** 所有元素的集合 */
|
||||
items: Set<T> = new Set();
|
||||
/** 适配器的id */
|
||||
id: string;
|
||||
|
||||
private execute: Map<string, AdapterFunction<T>> = new Map();
|
||||
private syncExecutes: Map<string, SyncAdapterFunction<T>> = new Map();
|
||||
private globalExecutes: Map<string, GlobalAdapterFunction> = new Map();
|
||||
|
||||
constructor(id: string) {
|
||||
this.id = id;
|
||||
RenderAdapter.adapters.set(id, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个元素
|
||||
*/
|
||||
add(item: T) {
|
||||
this.items.add(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除一个元素
|
||||
*/
|
||||
remove(item: T) {
|
||||
this.items.delete(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义一个类似于静态函数的函数,只会调用一次,不会对每个元素执行
|
||||
* @param id 函数名称
|
||||
* @param fn 执行的函数
|
||||
*/
|
||||
receiveGlobal(id: string, fn: GlobalAdapterFunction) {
|
||||
this.globalExecutes.set(id, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置执行函数
|
||||
* @param fn 对于每个元素执行的函数
|
||||
*/
|
||||
receive(id: string, fn: AdapterFunction<T>): void {
|
||||
this.execute.set(id, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置同步执行函数
|
||||
* @param fn 对于每个元素执行的函数
|
||||
*/
|
||||
receiveSync(id: string, fn: SyncAdapterFunction<T>): void {
|
||||
this.syncExecutes.set(id, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对所有元素执行函数,当所有元素都运行完毕后兑现,类似于Promise.all
|
||||
* @returns 包含每个元素运行结果的数组
|
||||
*/
|
||||
all<R = any>(fn: string, ...params: any[]): Promise<R[]> {
|
||||
const execute = this.execute.get(fn);
|
||||
|
||||
if (!execute) {
|
||||
return Promise.reject();
|
||||
} else {
|
||||
return Promise.all(
|
||||
[...this.items].map(v => execute!(v, ...params))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对所有元素执行函数,当任意一个元素运行完毕后兑现,类似于Promise.any
|
||||
* @returns 最先运行完毕的元素的结果
|
||||
*/
|
||||
any<R = any>(fn: string, ...params: any[]): Promise<R> {
|
||||
const execute = this.execute.get(fn);
|
||||
if (!execute) {
|
||||
return Promise.reject();
|
||||
} else {
|
||||
return Promise.any(
|
||||
[...this.items].map(v => execute!(v, ...params))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对所有元素执行同步函数
|
||||
* @returns 包含每个元素运行结果的数组
|
||||
*/
|
||||
sync<R = any>(fn: string, ...params: any[]): R[] {
|
||||
const execute = this.syncExecutes.get(fn);
|
||||
if (!execute) {
|
||||
return [];
|
||||
} else {
|
||||
return [...this.items].map(v => execute!(v, ...params));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用一个全局函数
|
||||
*/
|
||||
global<R = any>(id: string, ...params: any[]): Promise<R> {
|
||||
const execute = this.globalExecutes.get(id);
|
||||
if (!execute) {
|
||||
return Promise.reject();
|
||||
} else {
|
||||
return execute(...params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁这个adapter
|
||||
*/
|
||||
destroy() {
|
||||
RenderAdapter.adapters.delete(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取适配器
|
||||
*/
|
||||
static get<T>(id: string): RenderAdapter<T> | undefined {
|
||||
return this.adapters.get(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
|
||||
interface OffscreenCanvasEvent {
|
||||
/** 当被动触发resize时(例如窗口大小变化)时触发,使用size函数并不会触发 */
|
||||
resize: [];
|
||||
}
|
||||
|
||||
export class MotaOffscreenCanvas2D extends EventEmitter<OffscreenCanvasEvent> {
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
/** 是否是高清画布 */
|
||||
highResolution: boolean = true;
|
||||
/** 是否启用抗锯齿 */
|
||||
antiAliasing: boolean = true;
|
||||
|
||||
scale: number = 1;
|
||||
|
||||
/** 更新标识符,如果发生变化则说明画布被动清空 */
|
||||
symbol: number = 0;
|
||||
|
||||
/**
|
||||
* 创建一个新的离屏画布\
|
||||
* **注意**:如果你在自定义渲染元素中使用,请避免使用此构造函数,而应该使用 `RenderItem.requireCanvas`,
|
||||
* 之后如果不使用,再使用 `RenderItem.deleteCanvas` 删除。
|
||||
* @param alpha 是否启用透明度通道
|
||||
* @param canvas 指定画布,不指定时会自动创建一个新画布
|
||||
*/
|
||||
constructor(alpha: boolean = true, canvas?: HTMLCanvasElement) {
|
||||
super();
|
||||
this.canvas = canvas ?? document.createElement('canvas');
|
||||
this.ctx = this.canvas.getContext('2d', { alpha })!;
|
||||
this.width = this.canvas.width / devicePixelRatio;
|
||||
this.height = this.canvas.height / devicePixelRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置画布的缩放比
|
||||
* @param scale 缩放比
|
||||
*/
|
||||
setScale(scale: number) {
|
||||
// if (scale === this.scale) {
|
||||
// this.clear();
|
||||
// return;
|
||||
// }
|
||||
this.scale = scale;
|
||||
let ratio = this.highResolution ? devicePixelRatio : 1;
|
||||
ratio *= this.scale;
|
||||
this.canvas.width = this.width * ratio;
|
||||
this.canvas.height = this.height * ratio;
|
||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
this.ctx.scale(ratio, ratio);
|
||||
this.ctx.imageSmoothingEnabled = this.antiAliasing;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置画布的大小
|
||||
*/
|
||||
size(width: number, height: number) {
|
||||
const w = Math.max(width, 1);
|
||||
const h = Math.max(height, 1);
|
||||
let ratio = this.highResolution ? devicePixelRatio : 1;
|
||||
ratio *= this.scale;
|
||||
this.canvas.width = w * ratio;
|
||||
this.canvas.height = h * ratio;
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
this.ctx.scale(ratio, ratio);
|
||||
this.ctx.imageSmoothingEnabled = this.antiAliasing;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前画布是否为高清画布
|
||||
*/
|
||||
setHD(hd: boolean) {
|
||||
this.highResolution = hd;
|
||||
this.size(this.width, this.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前画布的抗锯齿设置
|
||||
*/
|
||||
setAntiAliasing(anti: boolean) {
|
||||
this.antiAliasing = anti;
|
||||
this.ctx.imageSmoothingEnabled = anti;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空画布
|
||||
*/
|
||||
clear() {
|
||||
this.ctx.save();
|
||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制一个离屏Canvas2D对象,一般用于缓存等操作
|
||||
* @param canvas 被复制的MotaOffscreenCanvas2D对象
|
||||
*/
|
||||
static clone(canvas: MotaOffscreenCanvas2D): MotaOffscreenCanvas2D {
|
||||
const newCanvas = new MotaOffscreenCanvas2D();
|
||||
newCanvas.setHD(canvas.highResolution);
|
||||
newCanvas.size(canvas.width, canvas.height);
|
||||
newCanvas.ctx.drawImage(
|
||||
canvas.canvas,
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
return newCanvas;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import { MotaOffscreenCanvas2D } from './canvas2d';
|
||||
import { ActionType, EventProgress, ActionEventMap } from './event';
|
||||
import {
|
||||
ERenderItemEvent,
|
||||
IRenderChildable,
|
||||
RenderItem,
|
||||
RenderItemPosition
|
||||
} from './item';
|
||||
import { Transform } from './transform';
|
||||
|
||||
export interface EContainerEvent extends ERenderItemEvent {}
|
||||
|
||||
export class Container<E extends EContainerEvent = EContainerEvent>
|
||||
extends RenderItem<E | EContainerEvent>
|
||||
implements IRenderChildable
|
||||
{
|
||||
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
|
||||
): void {
|
||||
this.sortedChildren.forEach(v => {
|
||||
if (v.hidden) return;
|
||||
v.renderContent(canvas, transform);
|
||||
});
|
||||
}
|
||||
|
||||
onResize(scale: number): void {
|
||||
this.children.forEach(v => v.onResize(scale));
|
||||
super.onResize(scale);
|
||||
}
|
||||
|
||||
requestSort() {
|
||||
if (!this.needSort) {
|
||||
this.needSort = true;
|
||||
this.requestBeforeFrame(() => {
|
||||
this.needSort = false;
|
||||
this.sortChildren();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加子元素到这个容器上,然后在下一个tick执行更新
|
||||
* @param children 要添加的子元素
|
||||
*/
|
||||
appendChild(...children: RenderItem<any>[]) {
|
||||
children.forEach(v => {
|
||||
v.appendTo(this);
|
||||
});
|
||||
this.requestSort();
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
removeChild(...child: RenderItem<any>[]): void {
|
||||
let changed = false;
|
||||
child.forEach(v => {
|
||||
if (v.parent !== this) return;
|
||||
const success = v.remove();
|
||||
if (success) {
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
if (changed) this.requestSort();
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
appendTo(parent: RenderItem): void {
|
||||
super.appendTo(parent);
|
||||
if (this.root) {
|
||||
const root = this.root;
|
||||
this.forEachChild(ele => {
|
||||
ele.setRoot(root);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历这个元素中的每个子元素,并执行传入的函数
|
||||
* @param fn 对每个元素执行的函数
|
||||
*/
|
||||
forEachChild(fn: (ele: RenderItem) => void) {
|
||||
const stack: RenderItem[] = [this];
|
||||
while (stack.length > 0) {
|
||||
const ele = stack.pop()!;
|
||||
stack.push(...ele.children);
|
||||
fn(ele);
|
||||
}
|
||||
}
|
||||
|
||||
private sortChildren() {
|
||||
this.sortedChildren = [...this.children]
|
||||
.filter(v => !v.isComment)
|
||||
.sort((a, b) => a.zIndex - b.zIndex);
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected propagateEvent<T extends ActionType>(
|
||||
type: T,
|
||||
progress: EventProgress,
|
||||
event: ActionEventMap[T]
|
||||
): void {
|
||||
const len = this.sortedChildren.length;
|
||||
if (progress === EventProgress.Capture) {
|
||||
let success = false;
|
||||
for (let i = len - 1; i >= 0; i--) {
|
||||
const child = this.sortedChildren[i];
|
||||
if (child.hidden) continue;
|
||||
if (child.captureEvent(type, event)) {
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 如果没有子元素能够触发,那么自身触发冒泡
|
||||
if (!success) {
|
||||
this.bubbleEvent(type, event);
|
||||
}
|
||||
} else {
|
||||
this.parent?.bubbleEvent(type, event);
|
||||
}
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
super.destroy();
|
||||
this.children.forEach(v => {
|
||||
v.destroy();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
private renderFn?: CustomContainerRenderFn;
|
||||
private propagateFn?: CustomContainerPropagateFn;
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
transform: Transform
|
||||
): void {
|
||||
if (!this.renderFn) {
|
||||
super.render(canvas, transform);
|
||||
} else {
|
||||
this.renderFn(canvas, this.sortedChildren, transform);
|
||||
}
|
||||
}
|
||||
|
||||
protected propagateEvent<T extends ActionType>(
|
||||
type: T,
|
||||
progress: EventProgress,
|
||||
event: ActionEventMap[T]
|
||||
): void {
|
||||
if (this.propagateFn) {
|
||||
this.propagateFn(type, progress, event, this, () => {
|
||||
super.propagateEvent(type, progress, event);
|
||||
});
|
||||
} else {
|
||||
super.propagateEvent(type, progress, event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置这个自定义容器的渲染函数
|
||||
* @param render 渲染函数
|
||||
*/
|
||||
setRenderFn(render?: CustomContainerRenderFn) {
|
||||
this.renderFn = render;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置这个自定义容器的事件传递函数
|
||||
* @param propagate 事件传递函数
|
||||
*/
|
||||
setPropagateFn(propagate: CustomContainerPropagateFn) {
|
||||
this.propagateFn = propagate;
|
||||
}
|
||||
|
||||
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 'propagate': {
|
||||
if (!this.assertType(nextValue, 'function', key)) return false;
|
||||
this.setPropagateFn(nextValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.handleProps(key, prevValue, nextValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import type { RenderItem } from './item';
|
||||
|
||||
export const enum MouseType {
|
||||
/** 没有按键按下 */
|
||||
None = 0,
|
||||
/** 左键 */
|
||||
Left = 1 << 0,
|
||||
/** 中键,即按下滚轮 */
|
||||
Middle = 1 << 1,
|
||||
/** 右键 */
|
||||
Right = 1 << 2,
|
||||
/** 侧键后退 */
|
||||
Back = 1 << 3,
|
||||
/** 侧键前进 */
|
||||
Forward = 1 << 4
|
||||
}
|
||||
|
||||
export const enum WheelType {
|
||||
None,
|
||||
/** 以像素为单位 */
|
||||
Pixel,
|
||||
/** 以行为单位,每行长度视浏览器设置而定,约为 1rem */
|
||||
Line,
|
||||
/** 以页为单位,一般为一个屏幕高度 */
|
||||
Page
|
||||
}
|
||||
|
||||
export const enum ActionType {
|
||||
/** 点击事件,即按下与抬起都在该元素上时触发 */
|
||||
Click,
|
||||
/** 鼠标或手指按下事件 */
|
||||
Down,
|
||||
/** 鼠标或手指移动事件 */
|
||||
Move,
|
||||
/** 鼠标或手指抬起事件 */
|
||||
Up,
|
||||
/** 鼠标或手指移动入该元素时触发的事件 */
|
||||
Enter,
|
||||
/** 鼠标或手指移出该元素时触发的事件 */
|
||||
Leave,
|
||||
/** 鼠标在该元素上滚轮时触发的事件 */
|
||||
Wheel
|
||||
}
|
||||
|
||||
export const enum EventProgress {
|
||||
/** 捕获阶段 */
|
||||
Capture,
|
||||
/** 冒泡阶段 */
|
||||
Bubble
|
||||
}
|
||||
|
||||
export interface IActionEventBase {
|
||||
/** 当前事件是监听的哪个元素 */
|
||||
target: RenderItem;
|
||||
/** 是触摸操作还是鼠标操作 */
|
||||
touch: boolean;
|
||||
/**
|
||||
* 触发的按键种类,会出现在点击、按下、抬起三个事件中,而其他的如移动等该值只会是 {@link MouseType.None},
|
||||
* 电脑端可以有左键、中键、右键等,手机只会触发左键,每一项的值参考 {@link MouseType}
|
||||
*/
|
||||
type: MouseType;
|
||||
/**
|
||||
* 当前按下了哪些按键。该值是一个数字,可以通过位运算判断是否按下了某个按键。
|
||||
* 例如通过 `buttons & MouseType.Left` 来判断是否按下了左键。
|
||||
* 注意在鼠标抬起或鼠标点击事件中,并不会包含触发的那个按键
|
||||
*/
|
||||
buttons: number;
|
||||
/** 触发时是否按下了 alt 键 */
|
||||
altKey: boolean;
|
||||
/** 触发时是否按下了 shift 键 */
|
||||
shiftKey: boolean;
|
||||
/** 触发时是否按下了 ctrl 键 */
|
||||
ctrlKey: boolean;
|
||||
/** 触发时是否按下了 Windows(Windows) / Command(Mac) 键 */
|
||||
metaKey: boolean;
|
||||
/** 这次操作的标识符,在按下、移动、抬起阶段中保持不变 */
|
||||
identifier: number;
|
||||
}
|
||||
|
||||
export interface IActionEvent extends IActionEventBase {
|
||||
/** 相对于触发元素左上角的横坐标 */
|
||||
offsetX: number;
|
||||
/** 相对于触发元素左上角的纵坐标 */
|
||||
offsetY: number;
|
||||
/** 相对于整个画布左上角的横坐标 */
|
||||
absoluteX: number;
|
||||
/** 相对于整个画布左上角的纵坐标 */
|
||||
absoluteY: number;
|
||||
|
||||
/**
|
||||
* 调用后将停止事件的继续传播。
|
||||
* 在捕获阶段,将会阻止捕获的进一步进行,在冒泡阶段,将会阻止冒泡的进一步进行。
|
||||
* 如果当前元素有很多监听器,该方法并不会阻止其他监听器的执行。
|
||||
*/
|
||||
stopPropagation(): void;
|
||||
}
|
||||
|
||||
export interface IWheelEvent extends IActionEvent {
|
||||
/** 滚轮事件的鼠标横向滚动量 */
|
||||
wheelX: number;
|
||||
/** 滚轮事件的鼠标纵向滚动量 */
|
||||
wheelY: number;
|
||||
/** 滚轮事件的鼠标垂直屏幕的滚动量 */
|
||||
wheelZ: number;
|
||||
/** 滚轮事件的滚轮类型,表示了对应值的单位 */
|
||||
wheelType: WheelType;
|
||||
}
|
||||
|
||||
export interface ERenderItemActionEvent {
|
||||
/** 当这个元素被点击时的捕获阶段触发 */
|
||||
clickCapture: [ev: Readonly<IActionEvent>];
|
||||
/** 当这个元素被点击时的冒泡阶段触发 */
|
||||
click: [ev: Readonly<IActionEvent>];
|
||||
/** 当鼠标或手指在该元素上按下的捕获阶段触发 */
|
||||
downCapture: [ev: Readonly<IActionEvent>];
|
||||
/** 当鼠标或手指在该元素上按下的冒泡阶段触发 */
|
||||
down: [ev: Readonly<IActionEvent>];
|
||||
/** 当鼠标或手指在该元素上移动的捕获阶段触发 */
|
||||
moveCapture: [ev: Readonly<IActionEvent>];
|
||||
/** 当鼠标或手指在该元素上移动的冒泡阶段触发 */
|
||||
move: [ev: Readonly<IActionEvent>];
|
||||
/** 当鼠标或手指在该元素上抬起的捕获阶段触发 */
|
||||
upCapture: [ev: Readonly<IActionEvent>];
|
||||
/** 当鼠标或手指在该元素上抬起的冒泡阶段触发 */
|
||||
up: [ev: Readonly<IActionEvent>];
|
||||
/** 当鼠标或手指进入该元素时触发 */
|
||||
enter: [ev: Readonly<IActionEventBase>];
|
||||
/** 当鼠标或手指离开该元素时触发 */
|
||||
leave: [ev: Readonly<IActionEventBase>];
|
||||
/** 当鼠标滚轮时的捕获阶段触发 */
|
||||
wheelCapture: [ev: Readonly<IWheelEvent>];
|
||||
/** 当鼠标滚轮时的冒泡阶段触发 */
|
||||
wheel: [ev: Readonly<IWheelEvent>];
|
||||
}
|
||||
|
||||
export interface ActionEventMap {
|
||||
[ActionType.Click]: IActionEvent;
|
||||
[ActionType.Down]: IActionEvent;
|
||||
[ActionType.Enter]: IActionEvent;
|
||||
[ActionType.Leave]: IActionEvent;
|
||||
[ActionType.Move]: IActionEvent;
|
||||
[ActionType.Up]: IActionEvent;
|
||||
[ActionType.Wheel]: IWheelEvent;
|
||||
}
|
||||
|
||||
export const eventNameMap: Record<ActionType, keyof ERenderItemActionEvent> = {
|
||||
[ActionType.Click]: 'click',
|
||||
[ActionType.Down]: 'down',
|
||||
[ActionType.Move]: 'move',
|
||||
[ActionType.Up]: 'up',
|
||||
[ActionType.Enter]: 'enter',
|
||||
[ActionType.Leave]: 'leave',
|
||||
[ActionType.Wheel]: 'wheel'
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,12 @@
|
||||
export * from './adapter';
|
||||
export * from './canvas2d';
|
||||
export * from './container';
|
||||
export * from './event';
|
||||
export * from './gl2';
|
||||
export * from './item';
|
||||
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
@@ -0,0 +1,664 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { MotaOffscreenCanvas2D } from './canvas2d';
|
||||
import { Container } from './container';
|
||||
import {
|
||||
ActionType,
|
||||
IActionEvent,
|
||||
IActionEventBase,
|
||||
IWheelEvent,
|
||||
MouseType,
|
||||
WheelType
|
||||
} from './event';
|
||||
import { IRenderTreeRoot, RenderItem } from './item';
|
||||
import { Transform } from './transform';
|
||||
|
||||
interface TouchInfo {
|
||||
/** 这次触摸在渲染系统的标识符 */
|
||||
identifier: number;
|
||||
/** 浏览器的 clientX,用于判断这个触点有没有移动 */
|
||||
clientX: number;
|
||||
/** 浏览器的 clientY,用于判断这个触点有没有移动 */
|
||||
clientY: number;
|
||||
/** 是否覆盖在了当前元素上 */
|
||||
hovered: boolean;
|
||||
}
|
||||
|
||||
interface MouseInfo {
|
||||
/** 这个鼠标按键的标识符 */
|
||||
identifier: number;
|
||||
}
|
||||
|
||||
export interface MotaRendererConfig {
|
||||
/** 要挂载到哪个画布上,可以填 css 选择器或画布元素本身 */
|
||||
canvas: string | HTMLCanvasElement;
|
||||
/** 画布的宽度,所有渲染操作会自行适配缩放 */
|
||||
width: number;
|
||||
/** 画布的高度,所有渲染操作会自行适配缩放 */
|
||||
height: number;
|
||||
/** 是否启用不透明度通道,默认启用 */
|
||||
alpha?: boolean;
|
||||
}
|
||||
|
||||
export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
static list: Map<string, MotaRenderer> = new Map();
|
||||
|
||||
/** 所有连接到此根元素的渲染元素的 id 到元素自身的映射 */
|
||||
protected idMap: Map<string, RenderItem> = new Map();
|
||||
|
||||
/** 最后一次按下的鼠标按键,用于处理鼠标移动 */
|
||||
private lastMouse: MouseType = MouseType.None;
|
||||
/** 每个触点的信息 */
|
||||
private touchInfo: Map<number, TouchInfo> = new Map();
|
||||
/** 触点列表 */
|
||||
private touchList: Map<number, Touch> = new Map();
|
||||
/** 每个鼠标按键的信息 */
|
||||
private mouseInfo: Map<MouseType, MouseInfo> = new Map();
|
||||
/** 操作的标识符 */
|
||||
private actionIdentifier: number = 0;
|
||||
|
||||
/** 用于终止 document 上的监听 */
|
||||
private abort?: AbortController;
|
||||
/** 根据捕获行为判断光标样式 */
|
||||
private targetCursor: string = 'auto';
|
||||
/** 当前鼠标覆盖的元素 */
|
||||
private hoveredElement: Set<RenderItem> = new Set();
|
||||
/** 本次交互前鼠标覆盖的元素 */
|
||||
private beforeHovered: Set<RenderItem> = new Set();
|
||||
|
||||
target!: MotaOffscreenCanvas2D;
|
||||
|
||||
readonly isRoot = true;
|
||||
|
||||
constructor(config: MotaRendererConfig) {
|
||||
super('static', false);
|
||||
|
||||
const canvas = this.getMountCanvas(config.canvas);
|
||||
if (!canvas) {
|
||||
logger.error(19);
|
||||
return;
|
||||
}
|
||||
this.target = new MotaOffscreenCanvas2D(config.alpha ?? true, canvas);
|
||||
this.size(config.width, config.height);
|
||||
this.target.setAntiAliasing(false);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置这个渲染器的缩放比
|
||||
* @param scale 缩放比
|
||||
*/
|
||||
setScale(scale: number) {
|
||||
this.onResize(scale);
|
||||
}
|
||||
|
||||
onResize(scale: number): void {
|
||||
this.target.setScale(scale);
|
||||
const width = this.target.width * scale;
|
||||
const height = this.target.height * scale;
|
||||
this.target.canvas.style.width = `${width}px`;
|
||||
this.target.canvas.style.height = `${height}px`;
|
||||
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);
|
||||
}
|
||||
|
||||
private listen() {
|
||||
// 画布监听
|
||||
const canvas = this.target.canvas;
|
||||
canvas.addEventListener('mousedown', ev => {
|
||||
const mouse = this.getMouseType(ev);
|
||||
this.lastMouse = mouse;
|
||||
this.captureEvent(
|
||||
ActionType.Down,
|
||||
this.createMouseAction(ev, ActionType.Down, mouse)
|
||||
);
|
||||
});
|
||||
canvas.addEventListener('mouseup', ev => {
|
||||
const event = this.createMouseAction(ev, ActionType.Up);
|
||||
this.captureEvent(ActionType.Up, event);
|
||||
this.captureEvent(ActionType.Click, event);
|
||||
});
|
||||
canvas.addEventListener('mousemove', ev => {
|
||||
const event = this.createMouseAction(
|
||||
ev,
|
||||
ActionType.Move,
|
||||
this.lastMouse
|
||||
);
|
||||
this.targetCursor = 'auto';
|
||||
const temp = this.beforeHovered;
|
||||
temp.clear();
|
||||
this.beforeHovered = this.hoveredElement;
|
||||
this.hoveredElement = temp;
|
||||
this.captureEvent(ActionType.Move, event);
|
||||
if (this.targetCursor !== this.target.canvas.style.cursor) {
|
||||
this.target.canvas.style.cursor = this.targetCursor;
|
||||
}
|
||||
this.checkMouseEnterLeave(
|
||||
ev,
|
||||
event,
|
||||
this.beforeHovered,
|
||||
this.hoveredElement
|
||||
);
|
||||
});
|
||||
canvas.addEventListener('mouseleave', ev => {
|
||||
const id = this.getMouseIdentifier(
|
||||
ActionType.Leave,
|
||||
this.getMouseType(ev)
|
||||
);
|
||||
this.hoveredElement.forEach(v => {
|
||||
v.emit('leave', this.createMouseActionBase(ev, id, v));
|
||||
});
|
||||
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,
|
||||
this.createWheelAction(ev, ActionType.Wheel)
|
||||
);
|
||||
});
|
||||
// 文档监听
|
||||
const abort = new AbortController();
|
||||
const signal = abort.signal;
|
||||
this.abort = abort;
|
||||
const clear = (ev: MouseEvent) => {
|
||||
const mouse = this.getMouseButtons(ev);
|
||||
for (const button of this.mouseInfo.keys()) {
|
||||
if (!(mouse & button)) {
|
||||
this.mouseInfo.delete(button);
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', clear, { signal });
|
||||
document.addEventListener('mouseenter', clear, { signal });
|
||||
document.addEventListener('mouseleave', clear, { signal });
|
||||
}
|
||||
|
||||
private isTouchInCanvas(clientX: number, clientY: number) {
|
||||
const rect = this.target.canvas.getBoundingClientRect();
|
||||
const { left, right, top, bottom } = rect;
|
||||
const x = clientX;
|
||||
const y = clientY;
|
||||
return x >= left && x <= right && y >= top && y <= bottom;
|
||||
}
|
||||
|
||||
private getMouseType(ev: MouseEvent): MouseType {
|
||||
switch (ev.button) {
|
||||
case 0:
|
||||
return MouseType.Left;
|
||||
case 1:
|
||||
return MouseType.Middle;
|
||||
case 2:
|
||||
return MouseType.Right;
|
||||
case 3:
|
||||
return MouseType.Back;
|
||||
case 4:
|
||||
return MouseType.Forward;
|
||||
}
|
||||
return MouseType.None;
|
||||
}
|
||||
|
||||
private getActiveMouseIdentifier(mouse: MouseType) {
|
||||
if (this.lastMouse === MouseType.None) {
|
||||
return -1;
|
||||
} else {
|
||||
const info = this.mouseInfo.get(mouse);
|
||||
if (!info) return -1;
|
||||
else return info.identifier;
|
||||
}
|
||||
}
|
||||
|
||||
private getMouseIdentifier(type: ActionType, mouse: MouseType): number {
|
||||
switch (type) {
|
||||
case ActionType.Down: {
|
||||
const id = this.actionIdentifier++;
|
||||
this.mouseInfo.set(mouse, { identifier: id });
|
||||
return id;
|
||||
}
|
||||
case ActionType.Move:
|
||||
case ActionType.Enter:
|
||||
case ActionType.Leave:
|
||||
case ActionType.Wheel: {
|
||||
return this.getActiveMouseIdentifier(mouse);
|
||||
}
|
||||
case ActionType.Up:
|
||||
case ActionType.Click: {
|
||||
const id = this.getActiveMouseIdentifier(mouse);
|
||||
this.mouseInfo.delete(mouse);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getMouseButtons(event: MouseEvent): number {
|
||||
if (event.buttons === 0) return MouseType.None;
|
||||
let buttons = 0;
|
||||
if (event.buttons & 0b1) buttons |= MouseType.Left;
|
||||
if (event.buttons & 0b10) buttons |= MouseType.Right;
|
||||
if (event.buttons & 0b100) buttons |= MouseType.Middle;
|
||||
if (event.buttons & 0b1000) buttons |= MouseType.Back;
|
||||
if (event.buttons & 0b10000) buttons |= MouseType.Forward;
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private createMouseActionBase(
|
||||
event: MouseEvent,
|
||||
id: number,
|
||||
target: RenderItem = this,
|
||||
mouse: MouseType = this.getMouseType(event)
|
||||
): IActionEventBase {
|
||||
return {
|
||||
identifier: id,
|
||||
target: target,
|
||||
touch: false,
|
||||
type: mouse,
|
||||
buttons: this.getMouseButtons(event),
|
||||
altKey: event.altKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
shiftKey: event.shiftKey,
|
||||
metaKey: event.metaKey
|
||||
};
|
||||
}
|
||||
|
||||
private createTouchActionBase(
|
||||
event: TouchEvent,
|
||||
id: number,
|
||||
target: RenderItem
|
||||
): IActionEventBase {
|
||||
return {
|
||||
identifier: id,
|
||||
target: target,
|
||||
touch: false,
|
||||
type: MouseType.Left,
|
||||
buttons: MouseType.Left,
|
||||
altKey: event.altKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
shiftKey: event.shiftKey,
|
||||
metaKey: event.metaKey
|
||||
};
|
||||
}
|
||||
|
||||
private createMouseAction(
|
||||
event: MouseEvent,
|
||||
type: ActionType,
|
||||
mouse: MouseType = this.getMouseType(event)
|
||||
): IActionEvent {
|
||||
const id = this.getMouseIdentifier(type, mouse);
|
||||
const x = event.offsetX / this.scale;
|
||||
const y = event.offsetY / this.scale;
|
||||
|
||||
return {
|
||||
target: this,
|
||||
identifier: id,
|
||||
touch: false,
|
||||
offsetX: x,
|
||||
offsetY: y,
|
||||
absoluteX: x,
|
||||
absoluteY: y,
|
||||
type: mouse,
|
||||
buttons: this.getMouseButtons(event),
|
||||
altKey: event.altKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
shiftKey: event.shiftKey,
|
||||
metaKey: event.metaKey,
|
||||
stopPropagation: () => {
|
||||
this.propagationStoped.set(type, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private createWheelAction(
|
||||
event: WheelEvent,
|
||||
type: ActionType,
|
||||
mouse: MouseType = this.getMouseType(event)
|
||||
): IWheelEvent {
|
||||
const ev = this.createMouseAction(event, type, mouse) as IWheelEvent;
|
||||
ev.wheelX = event.deltaX;
|
||||
ev.wheelY = event.deltaY;
|
||||
ev.wheelZ = event.deltaZ;
|
||||
switch (event.deltaMode) {
|
||||
case 0x00:
|
||||
ev.wheelType = WheelType.Pixel;
|
||||
break;
|
||||
case 0x01:
|
||||
ev.wheelType = WheelType.Line;
|
||||
break;
|
||||
case 0x02:
|
||||
ev.wheelType = WheelType.Page;
|
||||
break;
|
||||
default:
|
||||
ev.wheelType = WheelType.None;
|
||||
break;
|
||||
}
|
||||
return ev;
|
||||
}
|
||||
|
||||
private getTouchIdentifier(touch: Touch, type: ActionType) {
|
||||
if (type === ActionType.Down) {
|
||||
const id = this.actionIdentifier++;
|
||||
this.touchInfo.set(touch.identifier, {
|
||||
identifier: id,
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
hovered: this.isTouchInCanvas(touch.clientX, touch.clientY)
|
||||
});
|
||||
return id;
|
||||
}
|
||||
const info = this.touchInfo.get(touch.identifier);
|
||||
if (!info) return -1;
|
||||
return info.identifier;
|
||||
}
|
||||
|
||||
private createTouch(
|
||||
touch: Touch,
|
||||
type: ActionType,
|
||||
event: TouchEvent,
|
||||
rect: DOMRect
|
||||
): IActionEvent {
|
||||
const x = (touch.clientX - rect.left) / this.scale;
|
||||
const y = (touch.clientY - rect.top) / this.scale;
|
||||
return {
|
||||
target: this,
|
||||
identifier: this.getTouchIdentifier(touch, type),
|
||||
touch: true,
|
||||
offsetX: x,
|
||||
offsetY: y,
|
||||
absoluteX: x,
|
||||
absoluteY: y,
|
||||
type: MouseType.Left,
|
||||
buttons: MouseType.Left,
|
||||
altKey: event.altKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
shiftKey: event.shiftKey,
|
||||
metaKey: event.metaKey,
|
||||
stopPropagation: () => {
|
||||
this.propagationStoped.set(type, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private createTouchAction(
|
||||
event: TouchEvent,
|
||||
type: ActionType
|
||||
): IActionEvent[] {
|
||||
const list: IActionEvent[] = [];
|
||||
const rect = this.target.canvas.getBoundingClientRect();
|
||||
if (type === ActionType.Up) {
|
||||
// 抬起是一个需要特殊处理的东西,因为 touches 不会包含这个内容,所以需要特殊处理
|
||||
const touches = Array.from(event.touches).map(v => v.identifier);
|
||||
for (const [id, touch] of this.touchList) {
|
||||
if (!touches.includes(id)) {
|
||||
// 如果不包含,才需要触发
|
||||
if (this.isTouchInCanvas(touch.clientX, touch.clientY)) {
|
||||
const ev = this.createTouch(touch, type, event, rect);
|
||||
list.push(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Array.from(event.touches).forEach(v => {
|
||||
const ev = this.createTouch(v, type, event, rect);
|
||||
if (type === ActionType.Move) {
|
||||
const touch = this.touchInfo.get(v.identifier);
|
||||
if (!touch) return;
|
||||
const moveX = touch.clientX - v.clientX;
|
||||
const moveY = touch.clientY - v.clientY;
|
||||
if (moveX !== 0 || moveY !== 0) {
|
||||
list.push(ev);
|
||||
}
|
||||
} else if (type === ActionType.Down) {
|
||||
this.touchList.set(v.identifier, v);
|
||||
if (this.isTouchInCanvas(v.clientX, v.clientY)) {
|
||||
list.push(ev);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private checkMouseEnterLeave(
|
||||
event: MouseEvent,
|
||||
ev: IActionEvent,
|
||||
before: Set<RenderItem>,
|
||||
now: Set<RenderItem>
|
||||
) {
|
||||
// 先 leave,再 enter
|
||||
before.forEach(v => {
|
||||
if (!now.has(v)) {
|
||||
v.emit(
|
||||
'leave',
|
||||
this.createMouseActionBase(event, ev.identifier, v)
|
||||
);
|
||||
}
|
||||
});
|
||||
now.forEach(v => {
|
||||
if (!before.has(v)) {
|
||||
v.emit(
|
||||
'enter',
|
||||
this.createMouseActionBase(event, ev.identifier, v)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private checkTouchEnterLeave(
|
||||
event: TouchEvent,
|
||||
ev: IActionEvent,
|
||||
before: Set<RenderItem>,
|
||||
now: Set<RenderItem>
|
||||
) {
|
||||
// 先 leave,再 enter
|
||||
before.forEach(v => {
|
||||
if (!now.has(v)) {
|
||||
v.emit(
|
||||
'leave',
|
||||
this.createTouchActionBase(event, ev.identifier, v)
|
||||
);
|
||||
}
|
||||
});
|
||||
now.forEach(v => {
|
||||
if (!before.has(v)) {
|
||||
v.emit(
|
||||
'enter',
|
||||
this.createTouchActionBase(event, ev.identifier, v)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update(_item: RenderItem = this) {
|
||||
this.cacheDirty = true;
|
||||
}
|
||||
|
||||
protected refresh(): void {
|
||||
if (!this.cacheDirty) return;
|
||||
this.target.clear();
|
||||
this.renderContent(this.target, Transform.identity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据渲染元素的id获取一个渲染元素
|
||||
* @param id 要获取的渲染元素id
|
||||
* @returns
|
||||
*/
|
||||
getElementById(id: string): RenderItem | null {
|
||||
if (id.length === 0) return null;
|
||||
const item = this.idMap.get(id);
|
||||
if (item) return item;
|
||||
else {
|
||||
const item = this.searchElement(this, id);
|
||||
if (item) {
|
||||
this.idMap.set(id, item);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
private searchElement(ele: RenderItem, id: string): RenderItem | null {
|
||||
for (const child of ele.children) {
|
||||
if (child.id === id) return child;
|
||||
else {
|
||||
const ele = this.searchElement(child, id);
|
||||
if (ele) return ele;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
connect(item: RenderItem): void {
|
||||
if (item.id.length === 0) return;
|
||||
const existed = this.idMap.get(item.id);
|
||||
if (existed) {
|
||||
if (existed === item) return;
|
||||
else logger.warn(23, item.id);
|
||||
} else {
|
||||
this.idMap.set(item.id, item);
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(item: RenderItem): void {
|
||||
this.idMap.delete(item.id);
|
||||
}
|
||||
|
||||
modifyId(item: RenderItem, previous: string, current: string): void {
|
||||
this.idMap.delete(previous);
|
||||
if (current.length !== 0) {
|
||||
if (this.idMap.has(item.id)) {
|
||||
logger.warn(23, item.id);
|
||||
} else {
|
||||
this.idMap.set(item.id, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getCanvas(): HTMLCanvasElement {
|
||||
return this.target.canvas;
|
||||
}
|
||||
|
||||
hoverElement(element: RenderItem): void {
|
||||
if (element.cursor !== 'inherit') {
|
||||
this.targetCursor = element.cursor;
|
||||
}
|
||||
this.hoveredElement.add(element);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
MotaRenderer.list.delete(this.id);
|
||||
this.abort?.abort();
|
||||
}
|
||||
|
||||
private toTagString(item: RenderItem, space: number, deep: number): string {
|
||||
if (item.isComment) return '';
|
||||
const name = item.constructor.name;
|
||||
if (item.children.size === 0) {
|
||||
return `${' '.repeat(deep * space)}<${name} ${
|
||||
item.id ? `id="${item.id}" ` : ''
|
||||
}uid="${item.uid}"${item.hidden ? ' hidden' : ''} />\n`;
|
||||
} else {
|
||||
return (
|
||||
`${' '.repeat(deep * space)}<${name} ${
|
||||
item.id ? `${item.id} ` : ''
|
||||
}uid="${item.uid}" ${item.hidden ? 'hidden' : ''}>\n` +
|
||||
`${[...item.children]
|
||||
.filter(v => !v.isComment)
|
||||
.map(v => this.toTagString(v, space, deep + 1))
|
||||
.join('')}` +
|
||||
`${' '.repeat(deep * space)}</${name}>\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试功能,将渲染树输出为 XML 标签形式,只包含渲染元素类名,以及元素 id 等基础属性,不包含属性值等
|
||||
* @param space 缩进空格数
|
||||
*/
|
||||
toTagTree(space: number = 4) {
|
||||
if (!import.meta.env.DEV) return '';
|
||||
return this.toTagString(this, space, 0);
|
||||
}
|
||||
|
||||
static get(id: string) {
|
||||
return this.list.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
MotaRenderer.list.forEach(v =>
|
||||
v.requestAfterFrame(() => v.refreshAllChildren())
|
||||
);
|
||||
});
|
||||
|
||||
// @ts-expect-error debug
|
||||
window.logTagTree = () => {
|
||||
console.log(MotaRenderer.get('render-main')?.toTagTree());
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
import { MotaOffscreenCanvas2D } from './canvas2d';
|
||||
import { EGL2Event, GL2, GL2Program, IGL2ProgramPrefix } from './gl2';
|
||||
import { RenderItemPosition } from './item';
|
||||
|
||||
const SHADER_PREFIX: IGL2ProgramPrefix = {
|
||||
VERTEX: /* glsl */ `#version 300 es
|
||||
precision highp float;
|
||||
|
||||
in vec4 a_position;
|
||||
in vec2 a_texCoord;
|
||||
|
||||
out vec2 v_texCoord;
|
||||
`,
|
||||
FRAGMENT: /* glsl */ `#version 300 es
|
||||
precision highp float;
|
||||
|
||||
in vec2 v_texCoord;
|
||||
|
||||
uniform sampler2D u_sampler;
|
||||
`
|
||||
};
|
||||
|
||||
const DEFAULT_VS = /* glsl */ `
|
||||
void main() {
|
||||
v_texCoord = a_texCoord;
|
||||
gl_Position = a_position;
|
||||
}
|
||||
`;
|
||||
const DEFAULT_FS = /* glsl */ `
|
||||
out vec4 color;
|
||||
|
||||
void main() {
|
||||
color = texture(u_sampler, v_texCoord);
|
||||
}
|
||||
`;
|
||||
|
||||
export interface EShaderEvent extends EGL2Event {}
|
||||
|
||||
export class Shader<E extends EShaderEvent = EShaderEvent> extends GL2<
|
||||
EShaderEvent | E
|
||||
> {
|
||||
constructor(type: RenderItemPosition = 'static') {
|
||||
super(type);
|
||||
}
|
||||
|
||||
protected drawScene(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
gl: WebGL2RenderingContext
|
||||
): void {
|
||||
const program = this.program;
|
||||
if (!program) return;
|
||||
program.texTexture('u_sampler', canvas.canvas);
|
||||
this.draw(gl, program);
|
||||
}
|
||||
}
|
||||
|
||||
export class ShaderProgram extends GL2Program {
|
||||
protected readonly prefix: IGL2ProgramPrefix = SHADER_PREFIX;
|
||||
|
||||
constructor(gl2: GL2, vs?: string, fs?: string) {
|
||||
super(gl2, vs, fs);
|
||||
if (!vs) this.vs(DEFAULT_VS);
|
||||
if (!fs) this.fs(DEFAULT_FS);
|
||||
}
|
||||
|
||||
ready(): boolean {
|
||||
this.useIndices('defaultIndices');
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override compile() {
|
||||
const success = super.compile();
|
||||
if (!success) return false;
|
||||
const shader = this.element;
|
||||
const gl = shader.gl;
|
||||
if (!gl) return false;
|
||||
|
||||
const tex = this.defineAttribArray('a_texCoord');
|
||||
const position = this.defineAttribArray('a_position');
|
||||
const sampler = this.defineTexture('u_sampler', 0);
|
||||
const indices = this.defineIndices('defaultIndices');
|
||||
if (!tex || !position || !sampler || !indices) {
|
||||
return true;
|
||||
}
|
||||
position.buffer(
|
||||
new Float32Array([1, -1, -1, -1, 1, 1, -1, 1]),
|
||||
gl.STATIC_DRAW
|
||||
);
|
||||
position.pointer(2, gl.FLOAT, false, 0, 0);
|
||||
position.enable();
|
||||
tex.buffer(new Float32Array([1, 1, 0, 1, 1, 0, 0, 0]), gl.STATIC_DRAW);
|
||||
tex.pointer(2, gl.FLOAT, false, 0, 0);
|
||||
tex.enable();
|
||||
indices.buffer(new Uint16Array([0, 1, 2, 2, 3, 1]), gl.STATIC_DRAW);
|
||||
this.useIndices(indices);
|
||||
this.mode(this.element.DRAW_ELEMENTS);
|
||||
this.paramElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
import { mat3, mat4, ReadonlyMat3, ReadonlyVec3, vec2, vec3 } from 'gl-matrix';
|
||||
|
||||
export interface ITransformUpdatable {
|
||||
updateTransform?(): void;
|
||||
}
|
||||
|
||||
export class Transform {
|
||||
mat: mat3 = mat3.create();
|
||||
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
scaleX: number = 1;
|
||||
scaleY: number = 1;
|
||||
rad: number = 0;
|
||||
|
||||
/** 有没有对这个Transform进行过修改,用于优化常规表现 */
|
||||
private modified: boolean = false;
|
||||
|
||||
/** 绑定的可更新元素 */
|
||||
bindedObject?: ITransformUpdatable;
|
||||
|
||||
/**
|
||||
* 对这个变换实例添加绑定对象,当矩阵变换时,自动调用其 update 函数
|
||||
* @param obj 要绑定的对象
|
||||
*/
|
||||
bind(obj?: ITransformUpdatable) {
|
||||
this.bindedObject = obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重设所有参数
|
||||
*/
|
||||
reset() {
|
||||
mat3.identity(this.mat);
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.scaleX = 1;
|
||||
this.scaleY = 1;
|
||||
this.rad = 0;
|
||||
this.modified = false;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改缩放,叠加关系
|
||||
*/
|
||||
scale(x: number, y: number = x): this {
|
||||
mat3.scale(this.mat, this.mat, [x, y]);
|
||||
this.scaleX *= x;
|
||||
this.scaleY *= y;
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动,叠加关系
|
||||
*/
|
||||
translate(x: number, y: number): this {
|
||||
mat3.translate(this.mat, this.mat, [x, y]);
|
||||
this.x += x;
|
||||
this.y += y;
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转,叠加关系
|
||||
*/
|
||||
rotate(rad: number): this {
|
||||
mat3.rotate(this.mat, this.mat, rad);
|
||||
this.rad += rad;
|
||||
if (this.rad >= Math.PI * 2) {
|
||||
const n = Math.floor(this.rad / Math.PI / 2);
|
||||
this.rad -= n * Math.PI * 2;
|
||||
}
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缩放,非叠加关系
|
||||
*/
|
||||
setScale(x: number, y: number = x): this {
|
||||
mat3.scale(this.mat, this.mat, [x / this.scaleX, y / this.scaleY]);
|
||||
this.scaleX = x;
|
||||
this.scaleY = y;
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置位置,非叠加关系
|
||||
*/
|
||||
setTranslate(x: number, y: number): this {
|
||||
mat3.translate(this.mat, this.mat, [x - this.x, y - this.y]);
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置旋转,非叠加关系
|
||||
*/
|
||||
setRotate(rad: number): this {
|
||||
mat3.rotate(this.mat, this.mat, rad - this.rad);
|
||||
this.rad = rad;
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置变换矩阵,叠加模式
|
||||
* @param a 水平缩放
|
||||
* @param b 垂直倾斜
|
||||
* @param c 水平倾斜
|
||||
* @param d 垂直缩放
|
||||
* @param e 水平移动
|
||||
* @param f 垂直移动
|
||||
*/
|
||||
transform(
|
||||
a: number,
|
||||
b: number,
|
||||
c: number,
|
||||
d: number,
|
||||
e: number,
|
||||
f: number
|
||||
): this {
|
||||
mat3.multiply(
|
||||
this.mat,
|
||||
this.mat,
|
||||
mat3.fromValues(a, b, 0, c, d, 0, e, f, 1)
|
||||
);
|
||||
this.calAttributes();
|
||||
this.bindedObject?.updateTransform?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置变换矩阵,非叠加模式
|
||||
* @param a 水平缩放
|
||||
* @param b 垂直倾斜
|
||||
* @param c 水平倾斜
|
||||
* @param d 垂直缩放
|
||||
* @param e 水平移动
|
||||
* @param f 垂直移动
|
||||
*/
|
||||
setTransform(
|
||||
a: number,
|
||||
b: number,
|
||||
c: number,
|
||||
d: number,
|
||||
e: number,
|
||||
f: number
|
||||
): this {
|
||||
mat3.set(this.mat, a, b, 0, c, d, 0, e, f, 1);
|
||||
this.calAttributes();
|
||||
this.bindedObject?.updateTransform?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新计算 translation scaling rotation
|
||||
*/
|
||||
calAttributes() {
|
||||
const [x, y] = getTranslation(this.mat);
|
||||
const [scaleX, scaleY] = getScaling(this.mat);
|
||||
const rad = getRotation(this.mat);
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.scaleX = scaleX;
|
||||
this.scaleY = scaleY;
|
||||
this.rad = rad;
|
||||
if (x === 0 && y === 0 && scaleX === 1 && scaleY === 1 && rad === 0) {
|
||||
this.modified = false;
|
||||
} else {
|
||||
this.modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 与一个矩阵相乘,返回其计算结果(不改变原矩阵)
|
||||
* @param transform 变换矩阵
|
||||
*/
|
||||
multiply(transform: Transform): Transform {
|
||||
if (this.modified) {
|
||||
const result = new Transform();
|
||||
mat3.multiply(result.mat, this.mat, transform.mat);
|
||||
result.modified = true;
|
||||
return result;
|
||||
} else {
|
||||
return transform.clone();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制这个变换矩阵
|
||||
*/
|
||||
clone() {
|
||||
const transform = new Transform();
|
||||
transform.mat = mat3.clone(this.mat);
|
||||
return transform;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变换矩阵的信息,将一个点转换为计算后的位置
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
transformed(x: number, y: number): vec3 {
|
||||
if (!this.modified) return [x, y, 1];
|
||||
return multiplyVec3(this.mat, [x, y, 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变换矩阵的信息,将一个计算后的位置逆转换为原位置
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
untransformed(x: number, y: number): vec3 {
|
||||
if (!this.modified) return [x, y, 1];
|
||||
const invert = mat3.create();
|
||||
mat3.invert(invert, this.mat);
|
||||
return multiplyVec3(invert, [x, y, 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变换矩阵的信息,将一个点转换为计算后的位置
|
||||
* @param transform 变换矩阵
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
static transformed(transform: Transform, x: number, y: number) {
|
||||
return multiplyVec3(transform.mat, [x, y, 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变换矩阵的信息,将一个计算后的位置逆转换为原位置
|
||||
* @param transform 变换矩阵
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
static untransformed(transform: Transform, x: number, y: number) {
|
||||
const invert = mat3.create();
|
||||
mat3.invert(invert, transform.mat);
|
||||
return multiplyVec3(invert, [x, y, 1]);
|
||||
}
|
||||
|
||||
/** 单位矩阵 */
|
||||
static get identity() {
|
||||
return new Transform();
|
||||
}
|
||||
}
|
||||
|
||||
function multiplyVec3(mat: ReadonlyMat3, vec: ReadonlyVec3): vec3 {
|
||||
const v0 = vec[0];
|
||||
const v1 = vec[1];
|
||||
const v2 = vec[2];
|
||||
return [
|
||||
mat[0] * v0 + mat[3] * v1 + mat[6] * v2,
|
||||
mat[1] * v0 + mat[4] * v1 + mat[7] * v2,
|
||||
mat[2] * v0 + mat[5] * v1 + mat[8] * v2
|
||||
];
|
||||
}
|
||||
|
||||
function getTranslation(mat: ReadonlyMat3): vec2 {
|
||||
return [mat[6], mat[7]];
|
||||
}
|
||||
|
||||
function getScaling(mat: ReadonlyMat3): vec2 {
|
||||
return [Math.hypot(mat[0], mat[3]), Math.hypot(mat[1], mat[4])];
|
||||
}
|
||||
|
||||
function getRotation(mat: ReadonlyMat3): number {
|
||||
return Math.atan2(mat[3], mat[0]);
|
||||
}
|
||||
|
||||
export class Transform3D {
|
||||
mat: mat4 = mat4.create();
|
||||
|
||||
/** 绑定的可更新元素 */
|
||||
bindedObject?: ITransformUpdatable;
|
||||
|
||||
/**
|
||||
* 绑定可更新对象
|
||||
* @param obj 要绑定的对象
|
||||
*/
|
||||
bind(obj?: ITransformUpdatable) {
|
||||
this.bindedObject = obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置为单位矩阵
|
||||
*/
|
||||
reset(): this {
|
||||
mat4.identity(this.mat);
|
||||
this.bindedObject?.updateTransform?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用缩放变换
|
||||
* @param x X轴缩放
|
||||
* @param y Y轴缩放
|
||||
* @param z Z轴缩放
|
||||
*/
|
||||
scale(x: number, y: number, z: number): this {
|
||||
mat4.scale(this.mat, this.mat, [x, y, z]);
|
||||
this.bindedObject?.updateTransform?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用平移变换
|
||||
* @param x X轴平移
|
||||
* @param y Y轴平移
|
||||
* @param z Z轴平移
|
||||
*/
|
||||
translate(x: number, y: number, z: number): this {
|
||||
mat4.translate(this.mat, this.mat, [x, y, z]);
|
||||
this.bindedObject?.updateTransform?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用旋转变换
|
||||
* @param rad 旋转角度(弧度)
|
||||
* @param axis 旋转轴
|
||||
*/
|
||||
rotate(rad: number, axis: vec3): this {
|
||||
mat4.rotate(this.mat, this.mat, rad, axis);
|
||||
this.bindedObject?.updateTransform?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用绕X轴旋转
|
||||
* @param rad 旋转角度(弧度)
|
||||
*/
|
||||
rotateX(rad: number): this {
|
||||
return this.rotate(rad, [1, 0, 0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用绕Y轴旋转
|
||||
* @param rad 旋转角度(弧度)
|
||||
*/
|
||||
rotateY(rad: number): this {
|
||||
return this.rotate(rad, [0, 1, 0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用绕Z轴旋转
|
||||
* @param rad 旋转角度(弧度)
|
||||
*/
|
||||
rotateZ(rad: number): this {
|
||||
return this.rotate(rad, [0, 0, 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置视图矩阵
|
||||
* @param eye 摄像机位置
|
||||
* @param center 目标位置
|
||||
* @param up 上方向量
|
||||
*/
|
||||
lookAt(eye: vec3, center: vec3, up: vec3): this {
|
||||
mat4.lookAt(this.mat, eye, center, up);
|
||||
this.bindedObject?.updateTransform?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置透视投影矩阵
|
||||
* @param fovy 垂直视野角度(弧度)
|
||||
* @param aspect 宽高比
|
||||
* @param near 近平面
|
||||
* @param far 远平面
|
||||
*/
|
||||
perspective(fovy: number, aspect: number, near: number, far: number): this {
|
||||
mat4.perspective(this.mat, fovy, aspect, near, far);
|
||||
this.bindedObject?.updateTransform?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置正交投影矩阵
|
||||
* @param left 左平面
|
||||
* @param right 右平面
|
||||
* @param bottom 底平面
|
||||
* @param top 顶平面
|
||||
* @param near 近平面
|
||||
* @param far 远平面
|
||||
*/
|
||||
ortho(
|
||||
left: number,
|
||||
right: number,
|
||||
bottom: number,
|
||||
top: number,
|
||||
near: number,
|
||||
far: number
|
||||
): this {
|
||||
mat4.ortho(this.mat, left, right, bottom, top, near, far);
|
||||
this.bindedObject?.updateTransform?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 与另一个变换矩阵相乘
|
||||
* @param transform 另一个变换矩阵
|
||||
*/
|
||||
multiply(transform: Transform3D): Transform3D {
|
||||
const result = new Transform3D();
|
||||
mat4.multiply(result.mat, this.mat, transform.mat);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆当前变换矩阵
|
||||
*/
|
||||
clone(): Transform3D {
|
||||
const clone = new Transform3D();
|
||||
mat4.copy(clone.mat, this.mat);
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将点应用变换
|
||||
* @param point 要变换的点
|
||||
*/
|
||||
transformed(point: vec3): vec3 {
|
||||
const result = vec3.create();
|
||||
vec3.transformMat4(result, point, this.mat);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将点应用逆变换
|
||||
* @param point 要逆变换的点
|
||||
*/
|
||||
untransformed(point: vec3): vec3 {
|
||||
const inverse = mat4.create();
|
||||
mat4.invert(inverse, this.mat);
|
||||
const result = vec3.create();
|
||||
vec3.transformMat4(result, point, inverse);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态方法:单位矩阵
|
||||
*/
|
||||
static get identity(): Transform3D {
|
||||
return new Transform3D();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export type SizedCanvasImageSource = Exclude<
|
||||
CanvasImageSource,
|
||||
VideoFrame | SVGElement
|
||||
>;
|
||||
|
||||
export type CanvasStyle = string | CanvasGradient | CanvasPattern;
|
||||
@@ -0,0 +1,80 @@
|
||||
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() {
|
||||
const canvas = document.createElement('canvas');
|
||||
const canvas2 = document.createElement('canvas');
|
||||
const gl = canvas.getContext('webgl');
|
||||
const gl2 = canvas2.getContext('webgl2');
|
||||
return { gl: !!gl, gl2: !!gl2 };
|
||||
}
|
||||
|
||||
export function isWebGLSupported() {
|
||||
return gl;
|
||||
}
|
||||
|
||||
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
|
||||
) {
|
||||
const { ctx } = canvas;
|
||||
const mat = transform.mat;
|
||||
const [a, b, , c, d, , e, f] = mat;
|
||||
ctx.transform(a, b, c, d, e, f);
|
||||
}
|
||||
Reference in new issue
Block a user