UNPKG

@eva/fiber-eva

Version:

React EVA是一个让开发同学能够在React技术体系下,利用EVA JS的游戏研发能力,开发动画、游戏类场景的框架。它可以让开发同学用熟悉的JSX和Hooks语法编写动画、游戏场景的代码。

1,044 lines (918 loc) 25.2 kB
import ReactReconciler from 'react-reconciler'; import {GameObject, Game, Component, System} from '@eva/eva.js'; import {RendererSystem} from '@eva/plugin-renderer'; import {Render, RenderSystem} from '@eva/plugin-renderer-render'; import {Text, TextSystem} from '@eva/plugin-renderer-text'; import {Event, EventSystem} from '@eva/plugin-renderer-event'; let _driver, _debug, _options, _root: HTMLDivElement, _canvas: HTMLCanvasElement, _hud: HTMLDivElement, _background: HTMLDivElement, _game: Game, _textMap = {}, _counter = { eva: 0, scene: 0, gameobject: 0, empty: 0, textMap: 0, }, _firstRender = true; function debugLog(debug, level, ...message) { if (debug) { console[level](...message); } } function useEvaDriver(filter, level = 'debug') { return (target, methodName, descriptor) => { const oldValue = descriptor.value; descriptor.value = function (...args) { let returnValue; let driverName; if (filter(...args)) { driverName = 'Eva Driver'; returnValue = oldValue.apply(this, args); } else { driverName = 'React Driver'; returnValue = _driver[methodName](...args); } debugLog( _debug, level, `[${driverName}]`, methodName, '(', args, ')', '=>', returnValue, ); return returnValue; }; }; } const EvaPropName = '__$eva$__'; const EvaRootAttrName = 'eva-root'; const EvaHudAttrName = 'eva-hud'; const EvaBgAttrName = 'eva-bg'; const EvaCanvasAttrName = 'eva-canvas'; const EvaElementTags = ['eva', 'scene', 'gameobject', 'hud', 'background']; const TextStyleKeys = [ 'align', 'breakWords', 'dropShadow', 'dropShadowAlpha', 'dropShadowAngle', 'dropShadowBlur', 'dropShadowColor', 'dropShadowDistance', 'fill', 'fillGradientType', 'fillGradientStops', 'fontFamily', 'fontSize', 'fontStyle', 'fontVariant', 'fontWeight', 'letterSpacing', 'lineHeight', 'lineJoin', 'miterLimit', 'padding', 'stroke', 'strokeThickness', 'textBaseline', 'trim', 'whiteSpace', 'wordWrap', 'wordWrapWidth', 'leading', ]; const TransformKeys = [ 'position', 'x', 'y', 'size', 'width', 'height', 'anchor', 'anchorX', 'anchorY', 'origin', 'originX', 'originY', 'scale', 'scaleX', 'scaleY', 'rotation', 'rotationX', 'rotationY', ]; const TransformKeyRegExp = /^(anchor|origin|scale|rotation)([A-Z]+)$/; const EventKeys = [/^on[A-Z]/]; const EventMap = { click: 'tap', }; const CantRemoveComponents = ['Transform', 'Event', 'Render']; const ListeningProps: any[] = [ ['alpha', 'zIndex', 'visible', 'sortableChildren'], Render, // Render Component & It's filter ]; const ListeningPropsCached: Record<string, typeof Component> = {}; function findComponent(key): typeof Component { if (ListeningPropsCached[key]) { return ListeningPropsCached[key]; } for (let i = 0; i < ListeningProps.length; i += 2) { const filter = ListeningProps[i]; const ComponentClass = ListeningProps[i + 1]; if ( (filter instanceof Array && filter.indexOf(key) > -1) || (filter instanceof RegExp && filter.test(key)) || (filter instanceof Function && filter(key)) ) { ListeningPropsCached[key] = ComponentClass; return ComponentClass; } } return undefined; } function processListeningProps(gameObject: GameObject, restProps) { const map: Record< string, {ComponentClass: typeof Component; InitParams: Record<string, any>} > = {}; for (const key in restProps) { const ComponentClass = findComponent(key); if (ComponentClass) { const options = map[ComponentClass.componentName] || { ComponentClass, InitParams: {}, }; options.InitParams[key] = restProps[key]; map[ComponentClass.componentName] = options; } } for (const componentName in map) { const options = map[componentName]; const component = gameObject.getComponent(options.ComponentClass); if (!component) { gameObject.addComponent(new options.ComponentClass(options.InitParams)); } else { for (const key in options.InitParams) { component[key] = options.InitParams[key]; } } } } function yeep() { return true; } function noop() { return false; } function isGameObject(node) { return node instanceof GameObject; } function getTransformKey(key) { if (key === 'x' || key === 'y') { return ['position', key]; } else if (key === 'width' || key === 'height') { return ['size', key]; } else { const matches = key.match(TransformKeyRegExp); if (matches) { return [matches[1], matches[2].toLowerCase()]; } else { return [key]; } } } function getTransformProps(props) { const result = {}; for (const key of TransformKeys) { if ( typeof key === 'string' && props.hasOwnProperty(key) && typeof props[key] !== 'undefined' ) { const value = props[key]; const [mainKey, subKey] = getTransformKey(key); if (subKey === undefined) { result[mainKey] = value; } else { if (result[mainKey] === undefined) { result[mainKey] = {}; } result[mainKey][subKey.toLowerCase()] = value; } } } return result; } function getTextStyleProps(props) { const result = {}; for (const key of TextStyleKeys) { if ( typeof key === 'string' && props.hasOwnProperty(key) && typeof props[key] !== 'undefined' ) { result[key] = props[key]; } } return result; } function getEventProps(props) { const result = {}; for (const key of EventKeys) { for (const propKey in props) { if (key.test(propKey)) { result[propKey] = props[propKey]; } } } return result; } function setEvaElement(el) { if ( !( el instanceof GameObject || el instanceof TextNode || el instanceof EmptyNode ) ) { Object.defineProperty(el, EvaPropName, { writable: false, configurable: true, enumerable: false, value: true, }); } } function isEvaElement(el) { if ( el instanceof GameObject || el instanceof TextNode || el instanceof EmptyNode ) { return true; } else { return el[EvaPropName] === true; } } function setEvaHud(el) { if (el instanceof Element) { el.setAttribute(EvaHudAttrName, 'true'); } } function isEvaHud(el) { if (el instanceof Element) { return el.getAttribute(EvaHudAttrName) === 'true'; } return false; } function isGameObjectTree(node, parent) { return isGameObject(node) && isGameObject(parent); } function isSceneNode(node, parentNode) { return ( isGameObject(node) && parentNode?.gameInstance instanceof Game // (node._r?.type === 'scene' || // || // (node.parent != null && node.parent === node.scene)) ); } function isBgNode(node) { return ( node instanceof HTMLElement && node.getAttribute(EvaBgAttrName) === 'true' ); } function isHudNode(node) { return ( node instanceof HTMLElement && node.getAttribute(EvaHudAttrName) === 'true' ); } function isEvaNode(node) { return ( node instanceof HTMLElement && node.getAttribute(EvaRootAttrName) === 'true' ); } function isEventName(propName) { return ( propName.startsWith('on') && window.hasOwnProperty(propName.toLowerCase()) ); } class EmptyNode extends GameObject { constructor(name) { super(name, {}); } } class TextNode { index: number; name: string; textComponent: Text; constructor(name, textComponent, index) { this.index = index; this.name = name; this.textComponent = textComponent; } } function setStyle(domElement, styles) { Object.keys(styles).forEach(name => { const rawValue = styles[name]; const isEmpty = rawValue === null || typeof rawValue === 'boolean' || rawValue === ''; // Unset the style to its default values using an empty string if (isEmpty) domElement.style[name] = ''; else { const value = typeof rawValue === 'number' ? `${rawValue}px` : rawValue; domElement.style[name] = value; } }); } function addEventListener(node, eventName, eventHandler) { eventName = EventMap[eventName] || eventName; let eventComponent = node.getComponent(Event); if (!eventComponent) { eventComponent = node.addComponent(new Event()); } return eventComponent.on(eventName, eventHandler); } const NO_CONTEXT = {}; function appendChild(parent, node) { // if (node instanceof TextNode) return; if (isGameObjectTree(node, parent)) { parent.addChild(node); } else if (isSceneNode(node, parent)) { _game.scene.addChild(node); } else { parent.appendChild(node); } } function removeChild(parent, node) { if (isGameObject(node) || isSceneNode(node, parent)) { // gameobject // parent.removeChild(node); node.destroy(); } else if (isBgNode(node)) { parent.removeChild(node); } else if (isHudNode(node)) { parent.removeChild(node); } else if (isEvaNode(node)) { parent.removeChild(node); _destroyGame(); } else { parent.removeChild(node); } } function insertBefore(parent, node, before) { parent = parent || before.parent || before.parentNode; if (isGameObjectTree(node, parent)) { const index = parent.transform.children.indexOf(before.transform); if (index > -1) { parent.addChild(node); const transform = parent.transform.children.pop(); if (index === 0) { parent.transform.children.unshift(transform); } else { parent.transform.children.splice(index, -1, transform); } } } else if (isSceneNode(node, parent)) { parent.scene.addChild(node); } // else if (isBgNode(node)) { // this._driver.insertBefore(node, this._canvas); // } else if (isHudNode(node)) { // this._driver.insertBefore(node, this._canvas); // } else { // this._driver.insertBefore(node, before, parent); // } } // get diff between 2 objects function shallowDiff(oldObj, newObj) { // Return a diff between the new and the old object const uniqueProps = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]); const changedProps = Array.from(uniqueProps).filter( propName => oldObj[propName] !== newObj[propName], ); return changedProps; } function _updateTextComponent(textComponent) { const textId = textComponent._textId; textComponent.text = _textMap[textId].join(''); } /** attr */ function _setTransform(gameObject, propKey, propValue) { const [mainKey, subKey] = getTransformKey(propKey); if (subKey === undefined) { gameObject.transform[mainKey] = propValue; } else { if (gameObject.transform[mainKey] === undefined) { gameObject.transform[mainKey] = {}; } gameObject.transform[mainKey][subKey] = propValue; } } function _setTextStyle(gameObject, propKey, propValue) { const textComponent = gameObject.getComponent(Text); if (textComponent) { textComponent.style[propKey] = propValue; } } function _findChangedComponents(node, propValue) { const newTypes = propValue.map(item => item); const changedComponents = []; node.components.forEach(component => { const index = newTypes.indexOf(component); if (index > -1) { newTypes[index] = false; } else if (CantRemoveComponents.indexOf(component.name) < 0) { changedComponents.push([component, true]); } }); newTypes.forEach((item, index) => { if (item !== false) { changedComponents.push([propValue[index], false]); } }); return changedComponents; } function _setComponent(gameObject: GameObject, component, isRemove = false) { const has = gameObject.getComponent(component.name); if (!has && !isRemove) { gameObject.addComponent(component); } else if (has && isRemove) { gameObject.removeComponent(component.name); } } function setAttribute(node, propKey, propValue) { if (propKey === 'name') { node.name = propValue; } else if (propKey === 'components') { const changedComponents = _findChangedComponents(node, propValue); for (const [component, isRemove] of changedComponents) { _setComponent(node, component, isRemove); } } else if (TransformKeys.indexOf(propKey) > -1) { _setTransform(node, propKey, propValue); } else if (TextStyleKeys.indexOf(propKey) > -1) { _setTextStyle(node, propKey, propValue); } else { processListeningProps(node, {[propKey]: propValue}); } } function _destroyGame() { try { _game.pause(); (_game as any).destroy?.(); } catch (e) {} } function _createGame( options = { width: 100, height: 100, }, ) { _options = {...options}; const { frameRate = 60, preventScroll = false, transparent = true, renderType = 0, backgroundColor = 0x000000, HiRes = true, LowRes, width, height, style = {}, systems = [], listeningProps = [], component, ...props } = _options; if (_options.resolution === undefined && HiRes === true) { _options.resolution = 2; } else if (_options.resolution === undefined && LowRes === true) { _options.resolution = 1; } _root = document.createElement('div', { ...props, [EvaRootAttrName]: 'true', component, }); _root.setAttribute(EvaRootAttrName, 'true'); _canvas = document.createElement('canvas', { ...props, [EvaCanvasAttrName]: 'true', _parent: _root, }); _canvas.setAttribute(EvaCanvasAttrName, 'true'); setStyle(_root, { width, height, position: 'relative', ...style, }); setStyle(_canvas, { position: 'absolute', left: '0', top: '0', zIndex: '1', }); _root.appendChild(_canvas); const systemCached = {}; let canvasWidth = width + ''; let canvasHeight = height + ''; if (canvasWidth.match(/[\d\.]+%/)) { canvasWidth = (750 * parseFloat(canvasWidth)) / 100 + ''; } if (canvasHeight.match(/[\d\.]+%/)) { canvasHeight = ((window.innerHeight / window.innerWidth) * 750 * parseFloat(canvasHeight)) / 100 + ''; } _game = new Game({ frameRate, autoStart: true, systems: [ new RendererSystem({ canvas: _canvas, width: Number(canvasWidth), height: Number(canvasHeight), transparent, preventScroll, renderType, backgroundColor, resolution: _options.resolution / 2, }), new RenderSystem(), new TextSystem(), new EventSystem(), ...systems .map(system => { if (system instanceof System) { return system; } else if (typeof system === 'function') { return system(props); } }) .filter(system => { const systemName = system.constructor.systemName; if (!systemName || systemCached[systemName] !== true) { systemName && (systemCached[systemName] = true); return true; } return false; }), ], }); _game.scene.transform.size.width = Number(canvasWidth); _game.scene.transform.size.height = Number(canvasHeight); Object.defineProperty(_root, 'gameInstance', { value: _game, writable: false, enumerable: false, configurable: true, }); ListeningProps.push(...listeningProps); Object.defineProperty(_root, 'listeningProps', { value: ListeningProps, writable: false, enumerable: false, configurable: true, }); setEvaElement(_root); console.log(_root); return _root; } function _createHUD({style, ...props}) { _hud = document.createElement('div', { ...props, }); _hud.setAttribute(EvaHudAttrName, 'true'); setStyle(_hud, { width: _options.width, height: _options.height, position: 'absolute', left: '0', top: '0', pointerEvents: 'none', zIndex: '2', ...style, }); setEvaElement(_hud); return _hud; } function _createBackground({style, ...props}) { _background = document.createElement('div', { ...props, }); _background.setAttribute(EvaBgAttrName, 'true'); setStyle(_background, { width: _options.width, height: _options.height, position: 'absolute', left: '0', top: '0', pointerEvents: 'none', zIndex: '0', ...style, }); setEvaElement(_background); return _background; } const HostConfig = { getRootHostContext(rootContainerInstance) { return NO_CONTEXT; }, getChildHostContext(parentHostContext, type, rootContainerInstance) { return NO_CONTEXT; }, getChildHostContextForEventComponent(parentHostContext) { return parentHostContext; }, getPublicInstance(instance) { return instance; }, prepareForCommit() { // noop return null; }, resetAfterCommit() { // noop }, createInstance: ( type, props, rootContainerInstance, hostContext, internalInstanceHandle, ) => { if (type == 'eva') { return _root; } else if (type === 'hud') { return _createHUD(props); } else if (type === 'background') { return _createBackground(props); } if (EvaElementTags.indexOf(type) > -1) { const { name = `${type}${_counter[type]++}`, components = [], children = '', ...restProps } = props; const transform = getTransformProps(restProps); const gameObject = new GameObject(name, transform); const events = getEventProps(restProps); for (const name in events) { addEventListener(gameObject, name.slice(2).toLowerCase(), events[name]); } if (type === 'scene') { const design = restProps.design || 750; const realSize = _game.scene.transform.size; const scale = realSize.width / design; const designSize = { width: design, height: realSize.height / scale, }; gameObject.transform.size.width = designSize.width; gameObject.transform.size.height = designSize.height; gameObject.transform.scale.x = scale; gameObject.transform.scale.y = scale; } else if (type === 'gameobject') { if (components) { for (const component of components) { gameObject.addComponent(component); } if ( children && (typeof children === 'string' || typeof children === 'number') ) { let textComponent = gameObject.getComponent(Text) as any; if (!textComponent) { const style = getTextStyleProps({...restProps}); textComponent = gameObject.addComponent( new Text({ text: props.children, style, }), ); } } } } processListeningProps(gameObject, restProps); return gameObject; } else { //normal react element const domElement = document.createElement(type, props); Object.keys(props).forEach(propName => { const propValue = props[propName]; if (propName === 'style') { setStyle(domElement, propValue); } else if (propName === 'children') { if (typeof propValue === 'string' || typeof propValue === 'number') { domElement.textContent = propValue; } } else if (propName === 'className') { domElement.setAttribute('class', propValue); } else if (isEventName(propName)) { const eventName = propName.toLowerCase().replace('on', ''); domElement.addEventListener(eventName, propValue); } else { domElement.setAttribute(propName, propValue); } }); return domElement; } }, hideInstance(instance) { instance.visible = false; }, unhideInstance(instance, props) { const visible = props !== undefined && props !== null && props.hasOwnProperty('visible') ? props.visible : true; instance.visible = visible; }, finalizeInitialChildren(wordElement, type, props) { return false; }, prepareUpdate( instance, type, oldProps, newProps, rootContainerInstance, hostContext, ) { if (instance instanceof Game) { return true; } else if (instance instanceof GameObject) { return true; } else { return shallowDiff(oldProps, newProps); } }, commitUpdate(instance, updatePayload, type, oldProps, newProps) { // noop if (instance instanceof Game) { } else if (instance instanceof GameObject) { for (let prop in newProps) { const value = newProps[prop]; const lastValue = oldProps != null ? oldProps[prop] : undefined; if ( !newProps.hasOwnProperty(prop) || value === lastValue || (value == null && lastValue == null) ) { continue; } if (prop === 'children') continue; if (value != null) { if (prop === 'style') { setStyle(instance, value); } else if (isEventName(prop)) { addEventListener(instance, prop.slice(2).toLowerCase(), value); } else { setAttribute(instance, prop, value); } } } if (instance instanceof TextNode) { const {textComponent, index} = instance; const textId = (textComponent as any)._textId; _textMap[textId][index] = newProps.children; _updateTextComponent(textComponent); } else { // updateText(node, newProps.children ); } } else { updatePayload.forEach(propName => { // children changes is done by the other methods like `commitTextUpdate` if (propName === 'children') { const propValue = newProps[propName]; if (typeof propValue === 'string' || typeof propValue === 'number') { instance.textContent = propValue; } return; } if (propName === 'style') { // Return a diff between the new and the old styles const styleDiffs = shallowDiff(oldProps.style, newProps.style); const finalStyles = styleDiffs.reduce((acc, styleName) => { // Style marked to be unset if (!newProps.style[styleName]) acc[styleName] = ''; else acc[styleName] = newProps.style[styleName]; return acc; }, {}); setStyle(instance, finalStyles); } else if ( newProps[propName] || typeof newProps[propName] === 'number' ) { if (isEventName(propName)) { const eventName = propName.toLowerCase().replace('on', ''); instance.removeEventListener(eventName, oldProps[propName]); instance.addEventListener(eventName, newProps[propName]); } else { instance.setAttribute(propName, newProps[propName]); } } else { if (isEventName(propName)) { const eventName = propName.toLowerCase().replace('on', ''); instance.removeEventListener(eventName, oldProps[propName]); } else { instance.removeAttribute(propName); } } }); } }, shouldDeprioritizeSubtree(type, props) { // noop }, shouldSetTextContent(type, props) { return ( typeof props.children === 'string' || typeof props.children === 'number' ); }, createTextInstance(text, rootContainerInstance, internalInstanceHandler) { return document.createTextNode(text); }, unhideTextInstance(textInstance, text) { // noop }, mountEventComponent() { // noop }, updateEventComponent() { // noop }, handleEventTarget() { // noop }, scheduleTimeout: setTimeout, cancelTimeout: clearTimeout, noTimeout: -1, warnsIfNotActing: false, now: Date.now, isPrimaryRenderer: false, supportsMutation: true, supportsPersistence: false, supportsHydration: false, /** * ------------------------------------------- * Mutation * ------------------------------------------- */ appendInitialChild: appendChild, appendChild, appendChildToContainer: (parent, child) => { parent.appendChild(child); }, removeChild: removeChild, removeChildFromContainer: removeChild, insertBefore, insertInContainerBefore(...args) { // noop return false; }, commitMount(instance, updatePayload, type, oldProps, newProps) { // noop }, commitTextUpdate(textInstance, oldText, newText) { textInstance.nodeValue = newText; }, resetTextContent(evaElement) { // noop }, clearContainer(container) { // TODO implement this // container.removeAllChildrenRecursive(); }, beforeActiveInstanceBlur(internalInstanceHandle) { // noop }, afterActiveInstanceBlur() { // noop }, preparePortalMount(portalInstance) { // noop }, }; function createRenderer() { const reconciler = ReactReconciler(HostConfig); return {reconciler, createInstance: _createGame}; } export default createRenderer;