diff --git a/package-lock.json b/package-lock.json index 0b86601..121190f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "GPL", "dependencies": { + "@juggle/resize-observer": "^3.3.1", "react": "^17.0.2", "react-dom": "^17.0.2" }, @@ -143,6 +144,11 @@ "integrity": "sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==", "dev": true }, + "node_modules/@juggle/resize-observer": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz", + "integrity": "sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw==" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6803,6 +6809,11 @@ "integrity": "sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==", "dev": true }, + "@juggle/resize-observer": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz", + "integrity": "sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index d3ff9f9..2530b12 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "webpack-dev-server": "^4.7.2" }, "dependencies": { + "@juggle/resize-observer": "^3.3.1", "react": "^17.0.2", "react-dom": "^17.0.2" } diff --git a/source/Common/Emitter.ts b/source/Common/Emitter.ts new file mode 100644 index 0000000..1bfd2a7 --- /dev/null +++ b/source/Common/Emitter.ts @@ -0,0 +1,104 @@ +export type EventType = string | symbol; + +// An event handler can take an optional event argument +// and should not return a value +export type Handler = (event: T) => void; + +// An array of all currently registered event handlers for a type +export type EventHandlerList = Array>; + +// A map of event types and their corresponding event handlers. +export type EventHandlerMap> = Map< + keyof Events, + EventHandlerList +>; + +// Emitter function type +type IEmitParamType, K extends keyof E> = + E[K] extends ( undefined | void ) ? [type: K] : [type: K, evt: E[K]]; + +// Mixin to event object +export type EventMixin, B extends Record> = { + [P in (keyof A | keyof B)] : + P extends (keyof A & keyof B) ? + A[P] : + P extends keyof A ? + A[P] : + P extends keyof B ? B[P] : + never; +} + +export class Emitter> { + + /** + * A Map of event names to registered handler functions. + */ + public all: EventHandlerMap; + + public constructor() { + this.all = new Map(); + } + + public resetAll() { + this.all = new Map(); + } + + public reset(type: Key) { + this.all!.set(type, [] as EventHandlerList); + } + + /** + * Register an event handler for the given type. + * @param {string|symbol} type Type of event to listen for + * @param {Function} handler Function to call in response to given event + * @memberOf mitt + */ + public on(type: Key, handler: Handler) { + const handlers: Array> | undefined = this.all!.get(type); + if (handlers) { + handlers.push(handler); + } + else { + this.all!.set(type, [handler] as EventHandlerList); + } + } + + /** + * Remove an event handler for the given type. + * If `handler` is omitted, all handlers of the given type are removed. + * @param {string|symbol} type Type of event to unregister `handler` from + * @param {Function} [handler] Handler function to remove + * @memberOf mitt + */ + public off(type: Key, handler?: Handler) { + const handlers: Array> | undefined = this.all!.get(type); + if (handlers) { + if (handler) { + handlers.splice(handlers.indexOf(handler) >>> 0, 1); + } + else { + this.all!.set(type, []); + } + } + } + + /** + * Invoke all handlers for the given type. + * + * @param {string|symbol} type The event type to invoke + * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler + * @memberOf mitt + */ + public emit(...param: IEmitParamType): this { + const [ type, evt ] = param; + let handlers = this.all!.get(type); + if (handlers) { + (handlers as EventHandlerList) + .slice() + .map((handler) => { + handler(evt!); + }); + } + return this; + } +} \ No newline at end of file diff --git a/source/GLRender/GLCanvas.ts b/source/GLRender/GLCanvas.ts new file mode 100644 index 0000000..b95b7a7 --- /dev/null +++ b/source/GLRender/GLCanvas.ts @@ -0,0 +1,365 @@ +import { Emitter } from "../Common/Emitter"; +import { ResizeObserver as Polyfill } from '@juggle/resize-observer'; + +export { GLCanvas, GLCanvasOption}; + +/** + * GLCanvas 的设置 + */ +interface GLCanvasOption { + + /** + * 分辨率自适应 + */ + autoResize?: boolean; + + /** + * 是否监听鼠标事件 + */ + mouseEvent?: boolean; + + /** + * 调试时使用 + * 打印事件 + */ + eventLog?: boolean +} + +type GLCanvasEvent = { + mouseup: GLCanvas, + mousemove: GLCanvas, + mousedown: GLCanvas, + resize: GLCanvas, +}; + +/** + * 封装 GLCanvas + * 管理封装画布的功能属性 + * 监听画布事件 + * + * @event resize 画布缓冲区大小改变 + * @event mousemove 鼠标移动 + * @event mouseup 鼠标抬起 + * @event mousedown 鼠标按下 + * @event resize 大小变化 + */ +class GLCanvas extends Emitter { + + /** + * HTML节点 + */ + private readonly canvas:HTMLCanvasElement; + private readonly div:HTMLDivElement; + + /** + * 获取节点 + */ + public get dom(){ + return this.div; + } + + public get can(){ + return this.canvas; + } + + /** + * 像素分辨率 + */ + public pixelRatio:number = devicePixelRatio ?? 1; + + /** + * 帧缓冲区宽度 + */ + public get width():number { + return this.canvas.width + } + + /** + * 帧缓冲区高度 + */ + public get height():number { + return this.canvas.height + } + + /** + * 画布宽度 + */ + public get offsetWidth():number { + return this.canvas.offsetWidth + } + + /** + * 画布高度 + */ + public get offsetHeight():number { + return this.canvas.offsetHeight + } + + /** + * 缩放 X + */ + public get scaleX():number { + return this.canvas.width / this.canvas.offsetWidth + } + + /** + * 缩放 Y + */ + public get scaleY():number { + return this.canvas.height / this.canvas.offsetHeight + } + + /** + * 分辨率 (画布宽高比) + */ + public get ratio():number { + return this.canvas.offsetWidth / this.canvas.offsetHeight; + } + + /** + * 缓存判断是否要设置 canvas 大小 + */ + private readonly offsetFlg:[number,number] = [NaN, NaN]; + + /** + * 画布大小适应到 css 大小 + */ + public resize(){ + + if ( + this.offsetWidth !== this.offsetFlg[0] || + this.offsetHeight !== this.offsetFlg[1] + ) { + + // 缓存记录 + this.offsetFlg[0] = this.offsetWidth; + this.offsetFlg[1] = this.offsetHeight; + + // 重置缓冲区 + this.canvas.width = this.offsetWidth * this.pixelRatio; + this.canvas.height = this.offsetHeight * this.pixelRatio; + + this.emit("resize", this); + } + + } + + /** + * 鼠标 X 坐标 + */ + public mouseX:number = 0; + + /** + * 鼠标 Y 坐标 + */ + public mouseY:number = 0; + + /** + * 鼠标相对 X 坐标 + */ + public mouseUvX:number = 0; + + /** + * 鼠标相对 Y 坐标 + */ + public mouseUvY:number = 0; + + /** + * 鼠标 GLX 坐标 + */ + public mouseGlX:number = 0; + + /** + * 鼠标 GLY 坐标 + */ + public mouseGlY:number = 0; + + /** + * 鼠标 X 变化量 + */ + public mouseMotionX:number = 0; + + /** + * 鼠标 Y 变化量 + */ + public mouseMotionY:number = 0; + + /** + * 缓存鼠标位置 + */ + private readonly mouseFlg:[number, number] = [NaN, NaN]; + + /** + * 保存鼠标数据 + */ + private calcMouseData(offsetX:number, offsetY:number):boolean { + + if ( + offsetX !== this.mouseFlg[0] || + offsetY !== this.mouseFlg[1] + ){ + this.mouseX = offsetX; + this.mouseY = offsetY; + + this.mouseUvX = offsetX / this.offsetWidth; + this.mouseUvY = offsetY / this.offsetHeight; + + this.mouseGlX = this.mouseUvX * 2 - 1; + this.mouseGlY = - this.mouseUvY * 2 + 1; + + this.mouseMotionX = offsetX - this.mouseFlg[0]; + this.mouseMotionY = offsetY - this.mouseFlg[1]; + + this.mouseFlg[0] = offsetX; + this.mouseFlg[1] = offsetY; + + return true; + } + + return false; + } + + private calcMouseDataFromTouchEvent(e:TouchEvent){ + + if (e.touches.length <= 0) return; + + let offsetX = e.touches[0].clientX - this.canvas.offsetLeft; + let offsetY = e.touches[0].clientY - this.canvas.offsetTop; + + return this.calcMouseData(offsetX, offsetY); + } + + /** + * 鼠标触摸触发计数 + */ + private touchCount:number = 0; + + /** + * 鼠标是否按下 + */ + public mouseDown:boolean = false; + + /** + * 检测 canvas 变化 + */ + private readonly obs?: ResizeObserver; + + /** + * 使用 canvas 节点创建 + * 不适用节点则自动创建 + * @param ele 使用的 canvas节点 + * @param o 设置 + */ + public constructor(ele?:HTMLCanvasElement, o?:GLCanvasOption){ + + super(); + + let opt = o ?? {}; + + let autoResize = opt.autoResize ?? true; + let mouseEvent = opt.mouseEvent ?? true; + let eventLog = opt.eventLog ?? false; + + + // 获取/创建节点 + this.canvas = ele ?? document.createElement("canvas"); + + this.div = document.createElement("div"); + this.div.appendChild(this.canvas); + + this.canvas.style.width = "100%"; + this.canvas.style.height = "100%"; + + if (autoResize){ + + // 创建观察者 + this.obs = new (window.ResizeObserver || Polyfill) + ((entries:ResizeObserverEntry[])=>{ + + for (let entry of entries) { + if(entry.target === this.canvas) this.resize(); + } + + }); + + // 大小监听 + this.obs.observe(this.canvas); + + } + + if (mouseEvent) { + + this.canvas.addEventListener("mouseup",(e)=>{ + + if(this.touchCount >= 2) { + this.touchCount = 0; + return; + } + + if (eventLog) console.log("GLCanvas: mouseup"); + + this.mouseDown = false; + this.calcMouseData(e.offsetX, e.offsetY); + this.emit("mouseup", this); + + }); + + this.canvas.addEventListener("touchstart",(e)=>{ + + this.touchCount = 1; + + if (eventLog) console.log("GLCanvas: touchstart"); + + this.mouseDown = true; + this.calcMouseDataFromTouchEvent(e); + this.emit("mousedown", this); + + }); + + this.canvas.addEventListener("mousedown",(e)=>{ + + if(this.touchCount >= 2) return; + + if (eventLog) console.log("GLCanvas: mousedown"); + + this.mouseDown = true; + this.calcMouseData(e.offsetX, e.offsetY); + this.emit("mousedown", this); + + }); + + this.canvas.addEventListener("touchend",(e)=>{ + + this.touchCount ++; + + if (eventLog) console.log("GLCanvas: touchend"); + + this.mouseDown = false; + this.calcMouseDataFromTouchEvent(e); + this.emit("mouseup", this); + + }); + + this.canvas.addEventListener("mousemove",(e)=>{ + + if(this.touchCount >= 2) return; + + if (eventLog) console.log("GLCanvas: mousemove"); + + if (this.calcMouseData(e.offsetX, e.offsetY)) this.emit("mousemove", this); + + }); + + this.canvas.addEventListener("touchmove",(e)=>{ + + if (eventLog) console.log("GLCanvas: touchmove"); + + if (this.calcMouseDataFromTouchEvent(e)) this.emit("mousemove", this); + + }); + + } + + } + +} \ No newline at end of file diff --git a/source/Page/Laboratory/Laboratory.scss b/source/Page/Laboratory/Laboratory.scss index e12efda..24529af 100644 --- a/source/Page/Laboratory/Laboratory.scss +++ b/source/Page/Laboratory/Laboratory.scss @@ -1,6 +1,10 @@ -.main { - div.a { - font-size: 2.5em; - background-color: rgb(175, 204, 166); +div.main-canvas { + position: fixed; + width: 100%; + height: 100%; + + div.canvas { + width: 100%; + height: 100%; } } \ No newline at end of file diff --git a/source/Page/Laboratory/Laboratory.tsx b/source/Page/Laboratory/Laboratory.tsx index 052dd31..d2e31b1 100644 --- a/source/Page/Laboratory/Laboratory.tsx +++ b/source/Page/Laboratory/Laboratory.tsx @@ -1,17 +1,35 @@ -import { Component, ReactNode, StrictMode } from "react"; +import { Component, ReactNode, createRef } from "react"; +import { GLCanvas } from "@GLRender/GLCanvas"; import { Group } from "@Model/Group"; import { Entry } from "../Entry/Entry"; import "./Laboratory.scss"; -class Test extends Component { - render(): ReactNode { - return
-
it work
-
ok
-
+class Laboratory extends Component { + + private canvasContRef = createRef(); + + public render(): ReactNode { + return
+ } + + public componentDidMount() { + if (!this.canvasContRef.current) { + throw new Error("Laboratory: 无法获取到 Canvas 容器节点"); + } + + if (this.canvasContRef.current.getElementsByTagName("canvas").length > 0) { + throw new Error("Laboratory: 重复引用 canvas 节点"); + } + + const glCanvas = new GLCanvas(undefined, { + autoResize: true, + mouseEvent: true, + eventLog: false + }); + + glCanvas.dom.className = "canvas"; + this.canvasContRef.current.appendChild(glCanvas.dom); } } -Entry.renderComponent(Test); - -console.log(new Group); \ No newline at end of file +Entry.renderComponent(Laboratory); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 0ba5f57..5c75d7f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,8 +5,9 @@ "strict": true, "sourceMap": true, "jsx": "react-jsx", + "target": "ES5", "lib": [ - "ESNext", + "ESNext", "DOM" ], "baseUrl": "./", @@ -19,6 +20,12 @@ ], "@Model/*": [ "./source/Model/*" + ], + "@Common/*": [ + "./source/Common/*" + ], + "@GLRender/*": [ + "./source/GLRender/*" ] } },