init: 2.B

This commit is contained in:
AncTe committed 2026-02-28 20:23:34 +08:00
commit 6bf32174d8
610 files changed
+151444

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
{
"name": "@motajs/render-core",
"dependencies": {
"@motajs/common": "workspace:*"
}
}
+135
View File
@@ -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);
}
}
+119
View File
@@ -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;
}
}
+232
View File
@@ -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);
}
}
+154
View File
@@ -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
+12
View File
@@ -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
+664
View File
@@ -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());
};
+101
View File
@@ -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;
}
}
+59
View File
@@ -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;
}
}
+460
View File
@@ -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();
}
}
+6
View File
@@ -0,0 +1,6 @@
export type SizedCanvasImageSource = Exclude<
CanvasImageSource,
VideoFrame | SVGElement
>;
export type CanvasStyle = string | CanvasGradient | CanvasPattern;
+80
View File
@@ -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);
}