diff --git a/package.json b/package.json index 10568b2b..884885b1 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "@types/react-reconciler": "^0.16.0", "babel-loader": "^8.0.6", "cannon": "^0.6.2", + "chroma-js": "^2.1.0", "colors": "^1.3.3", "commitizen": "^3.0.5", "coveralls": "^3.0.2", diff --git a/src/PropsHandler.ts b/src/PropsHandler.ts index a85cad55..8f9476a8 100644 --- a/src/PropsHandler.ts +++ b/src/PropsHandler.ts @@ -1,7 +1,6 @@ import { Vector3, Color3, Color4 } from '@babylonjs/core/Maths/math' import { Control } from '@babylonjs/gui/2D/controls/control' -import { Observable, FresnelParameters, BaseTexture } from '@babylonjs/core' -import { type } from 'os' +import { Observable, FresnelParameters, BaseTexture, Nullable } from '@babylonjs/core' // TODO: type/value need to be joined, as the method will have multiple. export interface PropertyUpdate { @@ -26,6 +25,121 @@ export interface HasPropsHandlers { addPropsHandler(propHandler: PropsHandler): void } +export type PropertyUpdateProcessResult = { + processed: boolean, + value: Nullable +} + +/** + * NOTE: the applyAnimatedValues from react-spring always has `oldProp` undefined, so we force set anything provided. + * Would be more efficient to only handle the props passed in. + */ +export interface ICustomPropsHandler { + /** + * Friendly name to identify the handler. + */ + readonly name: string; + + /** + * The type of prop (ie: Vector3, Color3) to register for + */ + readonly propChangeType: string; + + /** + * Can be used to influence the ordering the handler fires compared to other custom handlers. + * @todo not implemented (future enhancement) + */ + readonly order: number; + + /** + * Like a visitor, except if 'true' is returned the call chain is broken. + * So, if you want to override a 'Vector3' with a string and the type is string + * then the regular handler will be bypassed. + * @param propChangeType + */ + accept(newProp: T): boolean + + // Check old vs new and return proper value, if any. + process(oldProp: T | undefined, newProp: T): PropertyUpdateProcessResult + +} + +export class CustomPropsHandler { + + private static _registeredPropsHandlers: Record[]> = {}; + + /** + * Register a new props handler + * + * @param handler to register for props (a handler can only be registered once per ) + * @returns a reference that can be used to unregister. + */ + public static RegisterPropsHandler(propsHandler: ICustomPropsHandler): ICustomPropsHandler { + const propsChangeType: string = propsHandler.propChangeType; + + if (!Array.isArray(CustomPropsHandler._registeredPropsHandlers[propsChangeType])) { + CustomPropsHandler._registeredPropsHandlers[propsChangeType] = []; + } + + const registeredHandlers: ICustomPropsHandler[] = CustomPropsHandler._registeredPropsHandlers[propsChangeType]; + + const match = registeredHandlers.find(h => h === propsHandler); + if (match !== undefined) { + console.warn(`Handler can only be registered once per type [${propsChangeType}]`); + return match; + } + + registeredHandlers.push(propsHandler); + return propsHandler; + } + + /** + * Unregister a props handler that was previously registered. + * + * @param propsHandler + * + * @returns if the props handler was found and unregistered + */ + public static UnregisterPropsHandler(propsHandlerToUnregister: ICustomPropsHandler): boolean { + const propsChangeType: string = propsHandlerToUnregister.propChangeType; + + if (!Array.isArray(CustomPropsHandler._registeredPropsHandlers[propsChangeType])) { + console.warn(`cannot find ${propsHandlerToUnregister.name} to unregister.`) + return false; + } + + const registeredHandlers: ICustomPropsHandler[] = CustomPropsHandler._registeredPropsHandlers[propsChangeType]; + + const index: number = registeredHandlers.indexOf(propsHandlerToUnregister); + + if (index === -1) { + console.warn(`cannot find ${propsHandlerToUnregister.name} to unregister.`) + return false; + } + + CustomPropsHandler._registeredPropsHandlers[propsChangeType] = registeredHandlers.slice(index, 1); + return true; + } + + public static HandlePropsChange(propsChangeType: PropChangeType, oldProp: any, newProp: any): PropertyUpdateProcessResult { + const registeredHandlers: ICustomPropsHandler[] = CustomPropsHandler._registeredPropsHandlers[propsChangeType]; + const notProcessed: PropertyUpdateProcessResult = { processed: false, value: null}; + if (registeredHandlers === undefined) { + return notProcessed; + } + + for (const handler of registeredHandlers) { + if (handler.accept(newProp)) { + const propertyUpdatedProcessResult: PropertyUpdateProcessResult = handler.process(oldProp, newProp); + // console.log(`handler '${handler.name}'custom prop processing result:`, propertyUpdatedProcessResult); + return propertyUpdatedProcessResult; + } + } + + return notProcessed; + } +} + export enum PropChangeType { Primitive = "Primitive", Vector3 = "Vector3", @@ -40,7 +154,25 @@ export enum PropChangeType { Texture = "Texture" } +const handledCustomProp = (changeType: PropChangeType, oldProp: any, newProp: any, propertyName: string, propertyType: string, changedProps: PropertyUpdate[]): boolean => { + const processedResult = CustomPropsHandler.HandlePropsChange(changeType, oldProp, newProp); + if (processedResult.processed) { + // console.log(`handled ${PropChangeType.Color3} on ${propertyName} - bypassing built-in handler - new Value: ${JSON.stringify(processedResult.value ?? {})}`); + changedProps.push({ + propertyName, + type: propertyType, + changeType, + value: processedResult.value! + }) + } + return processedResult.processed; +} + export const checkVector3Diff = (oldProp: Vector3 | undefined, newProp: Vector3 | undefined, propertyName: string, propertyType: string, changedProps: PropertyUpdate[]): void => { + if (handledCustomProp(PropChangeType.Vector3, oldProp, newProp, propertyName, propertyType, changedProps)) { + return; + } + if (newProp && (!oldProp || !oldProp.equals(newProp))) { changedProps.push({ propertyName, @@ -52,6 +184,10 @@ export const checkVector3Diff = (oldProp: Vector3 | undefined, newProp: Vector3 } export const checkColor3Diff = (oldProp: Color3 | undefined, newProp: Color3 | undefined, propertyName: string, propertyType: string, changedProps: PropertyUpdate[]): void => { + if (handledCustomProp(PropChangeType.Color3, oldProp, newProp, propertyName, propertyType, changedProps)) { + return; + } + if (newProp && (!oldProp || !oldProp.equals(newProp))) { changedProps.push({ propertyName, @@ -63,6 +199,10 @@ export const checkColor3Diff = (oldProp: Color3 | undefined, newProp: Color3 | u } export const checkColor4Diff = (oldProp: Color4 | undefined, newProp: Color4 | undefined, propertyName: string, propertyType: string, changedProps: PropertyUpdate[]): void => { + if (handledCustomProp(PropChangeType.Color4, oldProp, newProp, propertyName, propertyType, changedProps)) { + return; + } + // Color4.equals() not added until PR #5517 if (newProp && (!oldProp || oldProp.r !== newProp.r || oldProp.g !== newProp.g || oldProp.b !== newProp.b || oldProp.a !== newProp.a)) { changedProps.push({ diff --git a/src/ReactBabylonJSHostConfig.ts b/src/ReactBabylonJSHostConfig.ts index 42b2cb33..1b089a6a 100644 --- a/src/ReactBabylonJSHostConfig.ts +++ b/src/ReactBabylonJSHostConfig.ts @@ -73,7 +73,6 @@ function createCreatedInstance>( } as CreatedInstance } - /** * remove instance's children recursively * diff --git a/src/customComponents/AdvancedDynamicTextureLifecycleListener.ts b/src/customComponents/AdvancedDynamicTextureLifecycleListener.ts index 717523e7..59c8eafa 100644 --- a/src/customComponents/AdvancedDynamicTextureLifecycleListener.ts +++ b/src/customComponents/AdvancedDynamicTextureLifecycleListener.ts @@ -7,8 +7,8 @@ import { AdvancedDynamicTexture } from "@babylonjs/gui/2D/advancedDynamicTexture export default class AdvancedDynamicTextureLifecycleListener implements LifecycleListener { - private props: FiberAdvancedDynamicTextureProps; - private scene: Scene + protected props: FiberAdvancedDynamicTextureProps; + protected scene: Scene constructor(scene: Scene, props: any) { this.scene = scene @@ -102,8 +102,7 @@ export default class AdvancedDynamicTextureLifecycleListener implements Lifecycl onUnmount(): void {/* empty */} } -export class ADTFullscreenUILifecycleListener extends AdvancedDynamicTextureLifecycleListener { - constructor(scene: Scene, props: any) { - super(scene, props); - } -} +/** + * This is attached by convention in react-reconciler HostConfig. + */ +export class ADTFullscreenUILifecycleListener extends AdvancedDynamicTextureLifecycleListener {/* empty */} diff --git a/src/hooks.ts b/src/hooks.ts index 4184e1cf..69dc4303 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -2,6 +2,7 @@ import { useContext, useEffect } from 'react'; import { Nullable, Observer, Scene, EventState } from '@babylonjs/core'; import { SceneContext } from './Scene' +import { ICustomPropsHandler, CustomPropsHandler } from './PropsHandler'; export type OnFrameRenderFn = (eventData: Scene, eventState: EventState) => void @@ -23,3 +24,14 @@ export function useBeforeRender(callback: OnFrameRenderFn, mask?: number, insert } }) } + +export function useCustomPropsHandler(propsHandler: ICustomPropsHandler/*, deps?: React.DependencyList | undefined*/): void { + // running inside useEffect is too late for initial props + CustomPropsHandler.RegisterPropsHandler(propsHandler); + useEffect(() => { + return () => { + // console.warn('de-registering on unmount', propsHandler.name); + CustomPropsHandler.UnregisterPropsHandler(propsHandler); + } + }, []) +} diff --git a/src/react-babylonjs.ts b/src/react-babylonjs.ts index 65de0723..abc01b63 100644 --- a/src/react-babylonjs.ts +++ b/src/react-babylonjs.ts @@ -1,9 +1,10 @@ -export * from "./generatedCode" -export * from "./generatedProps" -export * from "./hooks" -export * from "./customComponents" // TODO: Except for Skybox - these should not be exported. they are internal. +export * from "./generatedCode"; +export * from "./generatedProps"; +export * from "./hooks"; +export * from "./customComponents"; // TODO: Except for Skybox - these should not be exported. they are internal. +export * from "./PropsHandler"; -export { default as Engine, withBabylonJS, BabylonJSContext, useBabylonEngine, useBabylonCanvas } from "./Engine" -export { default as Scene, withScene, WithSceneContext, SceneContext, SceneEventArgs, useBabylonScene } from "./Scene" +export { default as Engine, withBabylonJS, BabylonJSContext, useBabylonEngine, useBabylonCanvas } from "./Engine"; +export { default as Scene, withScene, WithSceneContext, SceneContext, SceneEventArgs, useBabylonScene } from "./Scene"; -export { HostWithEvents } from "./customHosts" +export { HostWithEvents } from "./customHosts"; diff --git a/stories/babylonjs/1-basic/chromaJS.stories.js b/stories/babylonjs/1-basic/chromaJS.stories.js new file mode 100644 index 00000000..60daf818 --- /dev/null +++ b/stories/babylonjs/1-basic/chromaJS.stories.js @@ -0,0 +1,145 @@ +import React, { useEffect } from 'react' +import { Vector3, Color3 } from '@babylonjs/core'; +import { Control } from '@babylonjs/gui'; +import { storiesOf } from '@storybook/react' +import { Engine, Scene, PropChangeType, CustomPropsHandler } from '../../../dist/react-babylonjs' +import '../../style.css'; +import chroma, { Color } from 'chroma-js' + +class ChromajsColor3PropsHandler /* implements ICustomPropsHandler */ { + + get name() { return 'chroma-js:Color3'} + + get propChangeType() { + return PropChangeType.Color3; + } + + accept(newProp) { + return (typeof(newProp) === 'string' && chroma.valid(newProp)) || newProp instanceof Color; + } + + process(oldProp, newProp) { + let newColor; + // this doesn't work switching from 'string' <==> Color... + if (typeof(newProp) === 'string') { + if (oldProp === undefined || oldProp !== newProp) { + newColor = chroma(newProp).rgb(); + } + } else { + if (oldProp === undefined || (oldProp instanceof Color && oldProp.hex() !== newProp.hex)) { + newColor = newProp.rgb(); + } + } + + return { + processed: newColor !== undefined, + value: newColor === undefined + ? null + : Color3.FromInts(newColor[0], newColor[1], newColor[2]) + }; + } +} + +const SQUARES_PER_CIRCLE = 24; +const INNER_RADIUS = 1.5; + +/** + * Shortest distance (angular) between two angles. + * It will be in range [0, 180]. + */ +const distance = (alpha, beta) => { + const phi = Math.abs(beta - alpha) % 360; // This is either the distance or 360 - distance + const distance = phi > 180 ? 360 - phi : phi; + return distance; +} + +/** + * This is for optimizing animation when first mount application. + * But this story works well,Animation is smooth。 + */ +function WithCustomColors(props) { + // useCustomPropsHandler(new ChromajsColor3PropsHandler()); + const handlerRef = CustomPropsHandler.RegisterPropsHandler(new ChromajsColor3PropsHandler()); + useEffect(() => { + return () => { + console.error('de-registering on unmount??', handlerRef.name); + CustomPropsHandler.UnregisterPropsHandler(handlerRef); + } + }, []) + + const degreeIncrements = (360 / SQUARES_PER_CIRCLE); + + return ( + <> + + + + { + props.colors.map(colorName => { + const color = chroma(colorName); + // color.luminance() < 0.25 + const contrastColor = color.get('lab.l') > 45 ? 'black' : 'white'; + console.log('checking:', colorName, color.get('lab.l'), color.luminance()); + return ( + + + + ) + }) + } + + + + { + props.colors.map((colorName, colorIndex) => { + + const whiteColorScaleFn = chroma.scale([colorName, 'white']); + const blackColorScaleFn = chroma.scale([colorName, 'black']); + const radius = (colorIndex * 1.05) + INNER_RADIUS; + const size = 0.75 * Math.tan(degreeIncrements * Math.PI/180) * radius; + + return [...Array(SQUARES_PER_CIRCLE).keys()].map(positionIndex => { + + const degrees = positionIndex * degreeIncrements; + const topDistance = distance(degrees, 270); + const bottomDistance = distance(degrees, 90); + const useWhiteColor = topDistance <= 90;// degrees from top > 90° + + const color = useWhiteColor + ? whiteColorScaleFn(1 - (topDistance / 90)) + : blackColorScaleFn((90 - bottomDistance) / 90); + + const angleRads = degrees * Math.PI / 180; + const x = radius * Math.cos(angleRads); + const z = radius * Math.sin(angleRads); + return ( + + + + )}) + }) + } + + ) +} + +export default storiesOf('Babylon Basic', module) + .add('chroma-js Props', () => ( +
+ + + + + + + +
+ ) +)