ue-too
Version:
pan, zoom, and rotate your html canvas
1 lines • 438 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","sources":["../src/being/interfaces.ts","../src/board-camera/utils/zoom.ts","../src/board-camera/utils/matrix.ts","../src/board-camera/utils/coordinate-conversion.ts","../src/board-camera/utils/position.ts","../src/board-camera/utils/rotation.ts","../src/utils/observable.ts","../src/board-camera/camera-update-publisher.ts","../src/board-camera/base-camera.ts","../src/board-camera/board-camera-v2.ts","../src/board-camera/alt-camera.ts","../src/utils/handler-pipeline.ts","../src/board-camera/camera-rig/pan-handler.ts","../src/board-camera/camera-rig/zoom-handler.ts","../src/board-camera/camera-rig/rotation-handler.ts","../src/board-camera/batcher/camera-position-update.ts","../src/board-camera/batcher/camera-zoom-update.ts","../src/board-camera/batcher/camera-rotation-update.ts","../src/board-camera/camera-rig/camera-rig.ts","../src/input-interpretation/kmt-event-parser/vanilla-kmt-event-parser.ts","../src/input-interpretation/touch-event-parser/vanilla-touch-event-parser.ts","../src/boardify/utils/zoomlevel-adjustment.ts","../src/utils/ruler.ts","../src/utils/drawing.ts","../src/boardify/utils/drawing-utils.ts","../src/boardify/utils/canvas-position-dimension.ts","../src/boardify/utils/canvas.ts","../src/camera-mux/animation-and-lock/pan-control-state-machine.ts","../src/camera-mux/animation-and-lock/zoom-control-state-machine.ts","../src/camera-mux/animation-and-lock/rotate-control-state-machine.ts","../src/camera-mux/animation-and-lock/animation-and-lock.ts","../src/camera-mux/relay.ts","../src/input-interpretation/raw-input-publisher/raw-input-publisher.ts","../src/input-interpretation/input-state-machine/kmt-input-context.ts","../src/input-interpretation/input-state-machine/kmt-input-state-machine.ts","../src/input-interpretation/input-state-machine/touch-input-state-machine.ts","../src/input-interpretation/input-state-machine/touch-input-context.ts","../src/boardify/board.ts","../src/drawing-engine/driver.ts","../src/drawing-engine/selection-box.ts"],"sourcesContent":["export interface BaseContext {\n setup(): void;\n cleanup(): void;\n}\n\ntype NOOP = () => void;\n\nexport const NO_OP: NOOP = ()=>{};\n\n/**\n * @description This is the interface for the state machine. The interface takes in a few generic parameters.\n * \n * Generic parameters:\n * - EventPayloadMapping: A mapping of events to their payloads.\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * \n * You can probably get by using the TemplateStateMachine class.\n * The naming is that an event would \"happen\" and the state of the state machine would \"handle\" it.\n *\n * @see {@link TemplateStateMachine}\n * @see {@link KmtInputStateMachine}\n * \n * @category being\n */\nexport interface StateMachine<EventPayloadMapping, Context extends BaseContext, States extends string = 'IDLE'> {\n switchTo(state: States): void;\n happens<K extends keyof EventPayloadMapping>(event: K, payload: EventPayloadMapping[K]): States | undefined;\n setContext(context: Context): void;\n states: Record<States, State<EventPayloadMapping, Context, string extends States ? string : States>>;\n onStateChange(callback: StateChangeCallback<States>): void;\n possibleStates: States[];\n onHappens(callback: (event: keyof EventPayloadMapping, payload: EventPayloadMapping[keyof EventPayloadMapping], context: Context) => void): void;\n}\n\n/**\n * @description This is the type for the callback that is called when the state changes.\n *\n * @category being\n */\nexport type StateChangeCallback<States extends string = 'IDLE'> = (currentState: States, nextState: States) => void;\n\n/**\n * @description This is the interface for the state. The interface takes in a few generic parameters:\n * You can probably get by extending the TemplateState class. \n *\n * Generic parameters:\n * - EventPayloadMapping: A mapping of events to their payloads.\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * \n * A state's all possible states can be only a subset of the possible states of the state machine. (a state only needs to know what states it can transition to)\n * This allows for a state to be reusable across different state machines.\n *\n * @see {@link TemplateState}\n * \n * @category being\n */\nexport interface State<EventPayloadMapping, Context extends BaseContext, States extends string = 'IDLE'> { \n uponEnter(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States>, from: States): void;\n beforeExit(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States>, to: States): void;\n handles<K extends keyof Partial<EventPayloadMapping>>(event: K, payload: EventPayloadMapping[K], context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States>): States | undefined;\n eventReactions: EventReactions<EventPayloadMapping, Context, States>;\n guards: Guard<Context>;\n eventGuards: Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>>;\n delay: Delay<Context, EventPayloadMapping, States> | undefined;\n}\n\n/**\n * @description This is the type for the event reactions of a state.\n * \n * Generic parameters:\n * - EventPayloadMapping: A mapping of events to their payloads.\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * \n * @category being\n */\nexport type EventReactions<EventPayloadMapping, Context extends BaseContext, States extends string> = {\n [K in keyof Partial<EventPayloadMapping>]: { \n action: (context: Context, event: EventPayloadMapping[K], stateMachine: StateMachine<EventPayloadMapping, Context, States>) => void;\n defaultTargetState: States;\n };\n};\n\n/**\n * @description This is the type for the guard evaluation when a state transition is happening.\n * \n * Guard evaluations are evaluated after the state has handled the event with the action.\n * Guard evaluations can be defined in an array and the first guard that evaluates to true will be used to determine the next state.\n * \n * Generic parameters:\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * \n * @category being\n */\nexport type GuardEvaluation<Context extends BaseContext> = (context: Context) => boolean;\n\n/**\n * @description This is the type for the guard of a state.\n * \n * guard is an object that maps a key to a guard evaluation.\n * K is all the possible keys that can be used to evaluate the guard.\n * K is optional but if it is not provided, typescript won't be able to type guard in the EventGuards type.\n * \n * @category being\n */\nexport type Guard<Context extends BaseContext, K extends string = string> = {\n [P in K]: GuardEvaluation<Context>;\n}\n\nexport type Action<Context extends BaseContext, EventPayloadMapping, States extends string> = {\n action: (context: Context, event: EventPayloadMapping[keyof EventPayloadMapping], stateMachine: StateMachine<EventPayloadMapping, Context, States>) => void;\n defaultTargetState: States;\n}\n\nexport type Delay<Context extends BaseContext, EventPayloadMapping, States extends string> = {\n time: number;\n action: Action<Context, EventPayloadMapping, States>;\n}\n\n/**\n * @description This is a mapping of a guard to a target state.\n * \n * Generic parameters:\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - G: The guard type.\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * \n * You probably don't need to use this type directly.\n * \n * @see {@link TemplateState['eventGuards']}\n * \n * @category being\n */\nexport type GuardMapping<Context extends BaseContext, G, States extends string> = {\n guard: G extends Guard<Context, infer K> ? K : never;\n target: States;\n}\n\n/**\n * @description This is a mapping of an event to a guard evaluation.\n * \n * Generic parameters:\n * - EventPayloadMapping: A mapping of events to their payloads.\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - T: The guard type.\n * \n * You probably don't need to use this type directly.\n * This is a mapping of an event to a guard evaluation.\n * \n * @see {@link TemplateState['eventGuards']}\n * \n * @category being\n */\nexport type EventGuards<EventPayloadMapping, States extends string, Context extends BaseContext, T extends Guard<Context>> = {\n [K in keyof EventPayloadMapping]: GuardMapping<Context, T, States>[];\n}\n\n/**\n * @description This is the template for the state machine.\n * \n * You can use this class to create a state machine. Usually this is all you need for the state machine. Unless you need extra functionality.\n * To create a state machine, just instantiate this class and pass in the states, initial state and context.\n * \n * @see {@link createKmtInputStateMachine} for an example of how to create a state machine.\n * \n * @category being\n */\nexport class TemplateStateMachine<EventPayloadMapping, Context extends BaseContext, States extends string = 'IDLE'> implements StateMachine<EventPayloadMapping, Context, States> {\n\n protected _currentState: States;\n protected _states: Record<States, State<EventPayloadMapping, Context, States>>;\n protected _context: Context;\n protected _statesArray: States[];\n protected _stateChangeCallbacks: StateChangeCallback<States>[];\n protected _happensCallbacks: ((event: keyof EventPayloadMapping, payload: EventPayloadMapping[keyof EventPayloadMapping], context: Context) => void)[];\n protected _timeouts: ReturnType<typeof setTimeout> | undefined = undefined;\n\n constructor(states: Record<States, State<EventPayloadMapping, Context, States>>, initialState: States, context: Context){\n this._states = states;\n this._currentState = initialState;\n this._context = context;\n this._statesArray = Object.keys(states) as States[];\n this._stateChangeCallbacks = [];\n this._happensCallbacks = [];\n }\n\n switchTo(state: States): void {\n this._currentState = state;\n }\n \n happens<K extends keyof EventPayloadMapping>(event: K, payload: EventPayloadMapping[K]): States | undefined {\n if(this._timeouts){\n clearTimeout(this._timeouts);\n }\n this._happensCallbacks.forEach(callback => callback(event, payload, this._context));\n const nextState = this._states[this._currentState].handles(event, payload, this._context, this);\n if(nextState !== undefined && nextState !== this._currentState){\n const originalState = this._currentState;\n this._states[this._currentState].beforeExit(this._context, this, nextState);\n this.switchTo(nextState);\n this._states[this._currentState].uponEnter(this._context, this, originalState);\n this._stateChangeCallbacks.forEach(callback => callback(originalState, this._currentState));\n }\n return nextState;\n }\n\n onStateChange(callback: StateChangeCallback<States>): void {\n this._stateChangeCallbacks.push(callback);\n }\n\n onHappens(callback: (event: keyof EventPayloadMapping, payload: EventPayloadMapping[keyof EventPayloadMapping], context: Context) => void): void {\n this._happensCallbacks.push(callback);\n }\n\n get currentState(): States {\n return this._currentState;\n }\n\n setContext(context: Context): void {\n this._context = context;\n }\n\n get possibleStates(): States[] {\n return this._statesArray;\n }\n\n get states(): Record<States, State<EventPayloadMapping, Context, States>> {\n return this._states;\n }\n}\n\n/**\n * @description This is the template for the state.\n * \n * This is a base template that you can extend to create a state.\n * Unlike the TemplateStateMachine, this class is abstract. You need to implement the specific methods that you need.\n * The core part off the state is the event reactions in which you would define how to handle each event in a state.\n * You can define an eventReactions object that maps only the events that you need. If this state does not need to handle a specific event, you can just not define it in the eventReactions object.\n * \n * @category being\n */\nexport abstract class TemplateState<EventPayloadMapping, Context extends BaseContext, States extends string = 'IDLE'> implements State<EventPayloadMapping, Context, States> {\n\n public abstract eventReactions: EventReactions<EventPayloadMapping, Context, States>;\n protected _guards: Guard<Context> = {} as Guard<Context>;\n protected _eventGuards: Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>> = {} as Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>>;\n protected _delay: Delay<Context, EventPayloadMapping, States> | undefined = undefined;\n\n get guards(): Guard<Context> {\n return this._guards;\n }\n\n get eventGuards(): Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>> {\n return this._eventGuards;\n }\n\n get delay(): Delay<Context, EventPayloadMapping, States> | undefined {\n return this._delay;\n }\n\n uponEnter(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States>, from: States): void {\n // console.log(\"enter\");\n }\n\n beforeExit(context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States>, to: States): void {\n // console.log('leave');\n }\n\n handles<K extends keyof EventPayloadMapping>(event: K, payload: EventPayloadMapping[K], context: Context, stateMachine: StateMachine<EventPayloadMapping, Context, States>): States | undefined {\n if (this.eventReactions[event]) {\n this.eventReactions[event].action(context, payload, stateMachine);\n const targetState = this.eventReactions[event].defaultTargetState;\n const guardToEvaluate = this._eventGuards[event];\n if(guardToEvaluate){\n const target = guardToEvaluate.find((guard)=>{\n if(this.guards[guard.guard]){\n return this.guards[guard.guard](context);\n }\n return false;\n });\n return target ? target.target : targetState;\n }\n return targetState;\n }\n return undefined;\n }\n}\n","/**\n * @description The limits of the zoom level.\n * \n * @category Camera\n */\nexport type ZoomLevelLimits = {min?: number, max?: number};\n\n/**\n * @description Checks if the zoom level limits are valid.\n */\nexport function isValidZoomLevelLimits(zoomLevelLimits: ZoomLevelLimits | undefined): boolean{\n if(zoomLevelLimits === undefined){\n return true;\n }\n if(zoomLevelLimits.min !== undefined && zoomLevelLimits.max !== undefined && zoomLevelLimits.min > zoomLevelLimits.max){\n return false;\n }\n return true;\n}\n\n/**\n * @description Clamps the zoom level within the limits.\n * \n * @category Camera\n */\nexport function clampZoomLevel(zoomLevel: number, zoomLevelLimits?: ZoomLevelLimits): number{\n if(zoomLevelWithinLimits(zoomLevel, zoomLevelLimits) || zoomLevelLimits === undefined){\n return zoomLevel;\n }\n if(zoomLevelLimits.max){\n zoomLevel = Math.min(zoomLevelLimits.max, zoomLevel);\n }\n if(zoomLevelLimits.min){\n zoomLevel = Math.max(zoomLevelLimits.min, zoomLevel);\n }\n return zoomLevel;\n}\n\n/**\n * @description Checks if the zoom level is within the limits.\n * \n * @category Camera\n */\nexport function zoomLevelWithinLimits(zoomLevel: number, zoomLevelLimits?: ZoomLevelLimits): boolean{\n if(zoomLevelLimits === undefined){\n return true;\n }\n if(zoomLevel <= 0 || (zoomLevelLimits !== undefined && \n ((zoomLevelLimits.max !== undefined && zoomLevelLimits.max < zoomLevel) || \n (zoomLevelLimits.min !== undefined && zoomLevelLimits.min > zoomLevel)\n ))){\n return false;\n }\n return true;\n}\n","\n/**\n * @description The transform matrix for the camera.\n * It's in the format like this:\n * ```\n * | a c e |\n * | b d f |\n * | 0 0 1 |\n * ```\n * \n * @category Camera\n */\nexport type TransformationMatrix = {\n a: number;\n b: number;\n c: number;\n d: number;\n e: number;\n f: number;\n};\n\n/**\n * Decomposes a camera transformation matrix back to camera parameters\n * \n * Transformation order:\n * 1. Scale by device pixel ratio\n * 2. Translate to canvas center\n * 3. Rotate by -camera.rotation\n * 4. Scale by zoom level\n * 5. Translate by -camera.position\n * \n * Final matrix: M = S1 * T1 * R * S2 * T2\n */\n\nexport function decomposeCameraMatrix(transformMatrix: TransformationMatrix, devicePixelRatio: number, canvasWidth: number, canvasHeight: number) {\n // Extract matrix elements (assuming 2D transformation matrix)\n // [a c tx] [m00 m02 m04]\n // [b d ty] = [m01 m03 m05]\n // [0 0 1 ] [0 0 1 ]\n \n const a = transformMatrix.a; // m00\n const b = transformMatrix.b; // m01 \n const c = transformMatrix.c; // m02\n const d = transformMatrix.d; // m03\n const tx = transformMatrix.e; // m04\n const ty = transformMatrix.f; // m05\n \n // Step 1: Extract rotation\n // The rotation is preserved in the orientation of the transformation\n const rotation = -Math.atan2(b, a); // Negative because we applied -camera.rotation\n \n // Step 2: Extract total scale and zoom\n const totalScale = Math.sqrt(a * a + b * b);\n const zoom = totalScale / devicePixelRatio;\n \n // Step 3: Extract camera position\n // We need to reverse the transformation chain:\n // Final translation = DPR * (center + R * Z * (-camera_position))\n \n // Start with the matrix translation\n let reverse = [tx, ty];\n \n // Remove DPR scaling\n reverse = [reverse[0] / devicePixelRatio, reverse[1] / devicePixelRatio];\n \n // Remove canvas center translation\n reverse = [reverse[0] - canvasWidth/2, reverse[1] - canvasHeight/2];\n \n // Apply inverse rotation (rotate by positive camera rotation)\n const cos_r = Math.cos(rotation); // Note: positive for inverse\n const sin_r = Math.sin(rotation);\n reverse = [\n cos_r * reverse[0] - sin_r * reverse[1],\n sin_r * reverse[0] + cos_r * reverse[1]\n ];\n \n // Apply inverse zoom scaling\n reverse = [reverse[0] / zoom, reverse[1] / zoom];\n \n // Negate to get original camera position (since we applied -camera.position)\n const cameraX = -reverse[0];\n const cameraY = -reverse[1];\n \n return {\n position: { x: cameraX, y: cameraY },\n zoom: zoom,\n rotation: rotation\n };\n}\n\n// Alternative implementation using matrix operations for clarity\nfunction decomposeCameraMatrixVerbose(transformMatrix: TransformationMatrix, devicePixelRatio: number, canvasWidth: number, canvasHeight: number) {\n const a = transformMatrix.a;\n const b = transformMatrix.b;\n const c = transformMatrix.c;\n const d = transformMatrix.d;\n const tx = transformMatrix.e;\n const ty = transformMatrix.f;\n \n console.log('Input matrix:');\n console.log(`[${a.toFixed(3)}, ${c.toFixed(3)}, ${tx.toFixed(3)}]`);\n console.log(`[${b.toFixed(3)}, ${d.toFixed(3)}, ${ty.toFixed(3)}]`);\n console.log('[0.000, 0.000, 1.000]');\n \n // Extract rotation\n const rotation = -Math.atan2(b, a);\n console.log(`\\nExtracted rotation: ${(rotation * 180 / Math.PI).toFixed(2)}°`);\n \n // Extract zoom\n const totalScale = Math.sqrt(a * a + b * b);\n const zoom = totalScale / devicePixelRatio;\n console.log(`Extracted zoom: ${zoom.toFixed(3)}`);\n \n // Extract camera position\n const centerX = canvasWidth / 2;\n const centerY = canvasHeight / 2;\n \n // First remove DPR scaling from the final translation\n const unscaledTx = tx / devicePixelRatio;\n const unscaledTy = ty / devicePixelRatio;\n console.log(`After removing DPR: [${unscaledTx.toFixed(3)}, ${unscaledTy.toFixed(3)}]`);\n \n // Then remove canvas center offset\n const adjustedTx = unscaledTx - centerX;\n const adjustedTy = unscaledTy - centerY;\n console.log(`After removing canvas center: [${adjustedTx.toFixed(3)}, ${adjustedTy.toFixed(3)}]`);\n \n // Reverse rotation\n const cos_r = Math.cos(-rotation);\n const sin_r = Math.sin(-rotation);\n const rotatedBackX = cos_r * adjustedTx + sin_r * adjustedTy;\n const rotatedBackY = -sin_r * adjustedTx + cos_r * adjustedTy;\n console.log(`After inverse rotation: [${rotatedBackX.toFixed(3)}, ${rotatedBackY.toFixed(3)}]`);\n \n // Reverse zoom scaling and negate (because we applied -camera.position)\n const cameraX = -rotatedBackX / zoom;\n const cameraY = -rotatedBackY / zoom;\n console.log(`Final camera position: [${cameraX.toFixed(3)}, ${cameraY.toFixed(3)}]`);\n \n return {\n position: { x: cameraX, y: cameraY },\n zoom: zoom,\n rotation: rotation\n };\n}\n\n// Helper function to create the transformation matrix from camera parameters\nexport function createCameraMatrix(cameraPos: {x: number, y: number}, zoom: number, rotation: number, devicePixelRatio: number, canvasWidth: number, canvasHeight: number) {\n // Step 1: Scale by device pixel ratio\n let matrix: TransformationMatrix = {\n a: devicePixelRatio,\n b: 0,\n c: 0,\n d: devicePixelRatio,\n e: 0,\n f: 0\n };\n \n // Step 2: Translate to canvas center\n const multipliedMatrix = multiplyMatrix(matrix, {\n a: 1,\n b: 0,\n c: 0,\n d: 1,\n e: canvasWidth/2,\n f: canvasHeight/2\n });\n \n // Step 3: Rotate (negative camera rotation)\n const cos_r = Math.cos(-rotation);\n const sin_r = Math.sin(-rotation);\n const rotatedMatrix = multiplyMatrix(multipliedMatrix, {\n a: cos_r,\n b: sin_r,\n c: -sin_r,\n d: cos_r,\n e: 0,\n f: 0\n });\n \n // Step 4: Scale by zoom\n const zoomedMatrix = multiplyMatrix(rotatedMatrix, {\n a: zoom,\n b: 0,\n c: 0,\n d: zoom,\n e: 0,\n f: 0\n });\n \n // Step 5: Translate by negative camera position\n const translatedMatrix = multiplyMatrix(zoomedMatrix, {\n a: 1,\n b: 0,\n c: 0,\n d: 1,\n e: -cameraPos.x,\n f: -cameraPos.y\n });\n return translatedMatrix;\n}\n\n// Matrix multiplication helper (2D transformation matrices)\nexport function multiplyMatrix(m1: TransformationMatrix, m2: TransformationMatrix) {\n const a1 = m1.a;\n const b1 = m1.b;\n const c1 = m1.c;\n const d1 = m1.d;\n const tx1 = m1.e;\n const ty1 = m1.f;\n\n const a2 = m2.a;\n const b2 = m2.b;\n const c2 = m2.c;\n const d2 = m2.d;\n const tx2 = m2.e;\n const ty2 = m2.f;\n \n return {\n a: a1 * a2 + c1 * b2, // a\n b: b1 * a2 + d1 * b2, // b\n c: a1 * c2 + c1 * d2, // c\n d: b1 * c2 + d1 * d2, // d\n e: a1 * tx2 + c1 * ty2 + tx1, // tx\n f: b1 * tx2 + d1 * ty2 + ty1 // ty\n };\n}\n\n// Example usage and test\nfunction testDecomposition() {\n // Test parameters\n const originalCamera = {\n position: { x: 100, y: 50 },\n zoom: 2.0,\n rotation: Math.PI / 6 // 30 degrees\n };\n const devicePixelRatio = 1.5;\n const canvasWidth = 800;\n const canvasHeight = 600;\n \n console.log('=== Testing Camera Matrix Decomposition ===');\n console.log('Original camera parameters:');\n console.log(`Position: (${originalCamera.position.x}, ${originalCamera.position.y})`);\n console.log(`Zoom: ${originalCamera.zoom}`);\n console.log(`Rotation: ${(originalCamera.rotation * 180 / Math.PI).toFixed(2)}°`);\n console.log(`Device Pixel Ratio: ${devicePixelRatio}`);\n console.log(`Canvas: ${canvasWidth}x${canvasHeight}`);\n \n // Create transformation matrix\n const matrix = createCameraMatrix(\n originalCamera.position, \n originalCamera.zoom, \n originalCamera.rotation, \n devicePixelRatio, \n canvasWidth, \n canvasHeight\n );\n \n console.log('\\n=== Decomposition Process ===');\n \n // Decompose the matrix\n const decomposed = decomposeCameraMatrixVerbose(\n matrix, \n devicePixelRatio, \n canvasWidth, \n canvasHeight\n );\n \n console.log('\\n=== Results ===');\n console.log('Decomposed camera parameters:');\n console.log(`Position: (${decomposed.position.x.toFixed(3)}, ${decomposed.position.y.toFixed(3)})`);\n console.log(`Zoom: ${decomposed.zoom.toFixed(3)}`);\n console.log(`Rotation: ${(decomposed.rotation * 180 / Math.PI).toFixed(2)}°`);\n \n // Check accuracy\n const posError = Math.sqrt(\n Math.pow(originalCamera.position.x - decomposed.position.x, 2) + \n Math.pow(originalCamera.position.y - decomposed.position.y, 2)\n );\n const zoomError = Math.abs(originalCamera.zoom - decomposed.zoom);\n const rotError = Math.abs(originalCamera.rotation - decomposed.rotation);\n \n console.log('\\n=== Accuracy Check ===');\n console.log(`Position error: ${posError.toFixed(6)}`);\n console.log(`Zoom error: ${zoomError.toFixed(6)}`);\n console.log(`Rotation error: ${rotError.toFixed(6)} radians`);\n}\n\n// Run the test\n// testDecomposition();\n","import { Point } from \"src/utils/misc\";\nimport { PointCal } from \"point2point\";\nimport { multiplyMatrix, TransformationMatrix } from \"src/board-camera/utils/matrix\";\n\n/**\n * @description Finds the world space coordinate of the interest point if the camera is at target position.\n * The target position is the \"would be\" position of the camera in world space.\n * The interest point is the point in view port space where the \"bottom left\" corner is the origin.\n * \n * @category Camera\n */\nexport function convert2WorldSpaceWRT(targetPosition: Point, interestPoint: Point, viewPortWidth: number, viewPortHeight: number, cameraZoomLevel: number, cameraRotation: number): Point{\n let cameraFrameCenter = {x: viewPortWidth / 2, y: viewPortHeight / 2};\n let delta2Point = PointCal.subVector(interestPoint, cameraFrameCenter);\n delta2Point = PointCal.multiplyVectorByScalar(delta2Point, 1 / cameraZoomLevel);\n delta2Point = PointCal.rotatePoint(delta2Point, cameraRotation);\n return PointCal.addVector(targetPosition, delta2Point);\n}\n\n/**\n * @description Converts the point to world space.\n * The point is in the viewport space where the \"bottom left\" corner is the origin.\n * Camera position is the position of the camera in world space.\n * \n * @category Camera\n */\nexport function convert2WorldSpace(point: Point, viewPortWidth: number, viewPortHeight: number, cameraPosition: Point, cameraZoomLevel: number, cameraRotation: number): Point{\n let cameraFrameCenter = {x: viewPortWidth / 2, y: viewPortHeight / 2};\n let delta2Point = PointCal.subVector(point, cameraFrameCenter);\n delta2Point = PointCal.multiplyVectorByScalar(delta2Point, 1 / cameraZoomLevel);\n delta2Point = PointCal.rotatePoint(delta2Point, cameraRotation);\n return PointCal.addVector(cameraPosition, delta2Point);\n}\n\n/**\n * @description Converts the point to world space.\n * The point is in the viewport space where the origin is at the center of the viewport.\n * Camera position is the position of the camera in world space.\n * \n * @category Camera\n */\nexport function convert2WorldSpaceAnchorAtCenter(point: Point, cameraPosition: Point, cameraZoomLevel: number, cameraRotation: number): Point{\n const scaledBack = PointCal.multiplyVectorByScalar(point, 1 / cameraZoomLevel);\n const rotatedBack = PointCal.rotatePoint(scaledBack, cameraRotation);\n const withOffset = PointCal.addVector(rotatedBack, cameraPosition);\n return withOffset;\n}\n\n/**\n * @description Converts a point in \"stage/context/world\" space to view port space.\n * The origin of the viewport is at the center of the viewport.\n * The point is in world space.\n * The camera position is the position of the camera in world space.\n * \n * @category Camera\n */\nexport function convert2ViewPortSpaceAnchorAtCenter(point: Point, cameraPosition: Point, cameraZoomLevel: number, cameraRotation: number): Point{\n const withOffset = PointCal.subVector(point, cameraPosition);\n const scaled = PointCal.multiplyVectorByScalar(withOffset, cameraZoomLevel);\n const rotated = PointCal.rotatePoint(scaled, -cameraRotation);\n return rotated;\n}\n\n/**\n * @description Converts a point in \"stage/context/world\" space to view port space.\n * The origin of the view port is at the bottom left corner.\n * The point is in world space.\n * The camera position is the position of the camera in world space.\n * \n * @category Camera\n */\nexport function invertFromWorldSpace(point: Point, viewPortWidth: number, viewPortHeight: number, cameraPosition: Point, cameraZoomLevel: number, cameraRotation: number): Point{\n let cameraFrameCenter = {x: viewPortWidth / 2, y: viewPortHeight / 2};\n let delta2Point = PointCal.subVector(point, cameraPosition);\n delta2Point = PointCal.rotatePoint(delta2Point, -cameraRotation);\n delta2Point = PointCal.multiplyVectorByScalar(delta2Point, cameraZoomLevel);\n return PointCal.addVector(cameraFrameCenter, delta2Point);\n}\n\n/**\n * @description Checks if a point is in the view port.\n * The point is in world space.\n * The camera position is the position of the camera in world space.\n * \n * @category Camera\n */\nexport function pointIsInViewPort(point: Point, viewPortWidth: number, viewPortHeight: number, cameraPosition: Point, cameraZoomLevel: number, cameraRotation: number): boolean{\n const pointInCameraFrame = invertFromWorldSpace(point, viewPortWidth, viewPortHeight, cameraPosition, cameraZoomLevel, cameraRotation);\n if(pointInCameraFrame.x < 0 || pointInCameraFrame.x > viewPortWidth || pointInCameraFrame.y < 0 || pointInCameraFrame.y > viewPortHeight){\n return false;\n }\n return true;\n}\n\n/**\n * @description Converts a delta in view port space to world space.\n * The delta is in view port space.\n * \n * @category Camera\n */\nexport function convertDeltaInViewPortToWorldSpace(delta: Point, cameraZoomLevel: number, cameraRotation: number): Point{\n return PointCal.multiplyVectorByScalar(PointCal.rotatePoint(delta, cameraRotation), 1 / cameraZoomLevel);\n}\n\n/**\n * @description Converts a delta in world space to view port space.\n * The delta is in world space.\n * \n * @category Camera\n */\nexport function convertDeltaInWorldToViewPortSpace(delta: Point, cameraZoomLevel: number, cameraRotation: number): Point{\n return PointCal.multiplyVectorByScalar(PointCal.rotatePoint(delta, -cameraRotation), cameraZoomLevel);\n}\n\n/**\n * @description Calculates the camera position to get a point in \"stage/context/world\" space to be at a certain point in view port space.\n * This is useful to coordinate camera pan and zoom at the same time.\n * \n * @category Camera\n */\nexport function cameraPositionToGet(pointInWorld: Point, toPointInViewPort: Point, cameraZoomLevel: number, cameraRotation: number): Point {\n const scaled = PointCal.multiplyVectorByScalar(toPointInViewPort, 1 / cameraZoomLevel);\n const rotated = PointCal.rotatePoint(scaled, cameraRotation);\n return PointCal.subVector(pointInWorld, rotated);\n}\n\nexport function transformationMatrixFromCamera(cameraPosition: Point, cameraZoomLevel: number, cameraRotation: number): TransformationMatrix{\n const cos = Math.cos(cameraRotation);\n const sin = Math.sin(cameraRotation);\n const trMatrix = multiplyMatrix({\n a: 1,\n b: 0,\n c: 0,\n d: 1,\n e: cameraPosition.x,\n f: cameraPosition.y\n }, {\n a: cos,\n b: sin,\n c: -sin,\n d: cos,\n e: 0,\n f: 0\n });\n const trsMatrix = multiplyMatrix(trMatrix, {\n a: 1 / cameraZoomLevel,\n b: 0,\n c: 0,\n d: 1 / cameraZoomLevel,\n e: 0,\n f: 0\n });\n return trsMatrix;\n}\n\nexport function convert2WorldSpaceWithTransformationMatrix(point: Point, transformationMatrix: TransformationMatrix): Point{\n return {\n x: point.x * transformationMatrix.a + point.y * transformationMatrix.c + transformationMatrix.e,\n y: point.x * transformationMatrix.b + point.y * transformationMatrix.d + transformationMatrix.f\n }\n}\n","import { Point } from \"src/utils/misc\";\nimport { PointCal } from \"point2point\";\n\nimport { convert2WorldSpaceWRT } from \"./coordinate-conversion\";\n\n/**\n * @description The boundaries of a camera.\n * The x and y are in world space.\n * \n * @category Camera\n */\nexport type Boundaries = {\n min?: {x?: number, y?: number};\n max?: {x?: number, y?: number};\n}\n\n/**\n * @description Checks if a point is within the boundaries.\n * \n * @category Camera\n */\nexport function withinBoundaries(point: Point, boundaries: Boundaries | undefined): boolean{\n if(boundaries == undefined){\n // no boundaries \n return true;\n }\n let leftSide = false;\n let rightSide = false;\n let topSide = false;\n let bottomSide = false;\n // check within boundaries horizontally\n if(boundaries.max == undefined || boundaries.max.x == undefined || point.x <= boundaries.max.x){\n rightSide = true;\n }\n if(boundaries.min == undefined || boundaries.min.x == undefined || point.x >= boundaries.min.x){\n leftSide = true;\n }\n if(boundaries.max == undefined || boundaries.max.y == undefined || point.y <= boundaries.max.y){\n topSide = true;\n }\n if(boundaries.min == undefined || boundaries.min.y == undefined || point.y >= boundaries.min.y){\n bottomSide = true;\n }\n return leftSide && rightSide && topSide && bottomSide;\n}\n\n/**\n * @description Checks if the boundaries are valid.\n * \n * @category Camera\n */\nexport function isValidBoundaries(boundaries: Boundaries | undefined): boolean{\n if(boundaries == undefined){\n return true;\n }\n const minX = boundaries.min?.x;\n const maxX = boundaries.max?.x;\n if (minX != undefined && maxX != undefined && minX >= maxX){\n return false;\n }\n const minY = boundaries.min?.y;\n const maxY = boundaries.max?.y;\n if (minY != undefined && maxY != undefined && minY >= maxY){\n return false;\n }\n return true;\n}\n\n/**\n * @description Checks if the boundaries are fully defined.\n * \n * @category Camera\n */\nexport function boundariesFullyDefined(boundaries: Boundaries | undefined): boolean{\n if(boundaries == undefined){\n return false;\n }\n if(boundaries.max == undefined || boundaries.min == undefined){\n return false;\n }\n if(boundaries.max.x == undefined || boundaries.max.y == undefined || boundaries.min.x == undefined || boundaries.min.y == undefined){\n return false;\n }\n return true;\n}\n\n/**\n * @description Clamps a point to the boundaries.\n * \n * @category Camera\n */\nexport function clampPoint(point: Point, boundaries: Boundaries | undefined): Point{\n if(withinBoundaries(point, boundaries) || boundaries == undefined){\n return point;\n }\n let manipulatePoint = {x: point.x, y: point.y};\n let limit = boundaries.min;\n if (limit != undefined){\n if(limit.x != undefined){\n manipulatePoint.x = Math.max(manipulatePoint.x, limit.x);\n }\n if(limit.y != undefined){\n manipulatePoint.y = Math.max(manipulatePoint.y, limit.y);\n }\n }\n limit = boundaries.max;\n if(limit != undefined){\n if(limit.x != undefined){\n manipulatePoint.x = Math.min(manipulatePoint.x, limit.x);\n }\n if(limit.y != undefined){\n manipulatePoint.y = Math.min(manipulatePoint.y, limit.y);\n }\n }\n return manipulatePoint;\n}\n\n/**\n * @description Gets the translation width of the boundaries.\n * \n * @category Camera\n */\nexport function translationWidthOf(boundaries: Boundaries | undefined): number | undefined{\n if(boundaries == undefined || boundaries.min == undefined || boundaries.max == undefined || boundaries.min.x == undefined || boundaries.max.x == undefined){\n return undefined;\n }\n return boundaries.max.x - boundaries.min.x;\n}\n\n/**\n * @description Gets the half translation width of the boundaries.\n * \n * @category Camera\n */\nexport function halfTranslationWidthOf(boundaries: Boundaries): number | undefined{\n const translationWidth = translationWidthOf(boundaries);\n return translationWidth != undefined ? translationWidth / 2 : undefined;\n}\n\n/**\n * @description Gets the translation height of the boundaries.\n * \n * @category Camera\n */\nexport function translationHeightOf(boundaries: Boundaries | undefined): number | undefined{\n if(boundaries == undefined || boundaries.min == undefined || boundaries.max == undefined || boundaries.min.y == undefined || boundaries.max.y == undefined){\n return undefined;\n }\n return boundaries.max.y - boundaries.min.y;\n}\n\n/**\n * @description Gets the half translation height of the boundaries.\n * \n * @category Camera\n */\nexport function halfTranslationHeightOf(boundaries: Boundaries): number | undefined{\n const translationHeight = translationHeightOf(boundaries);\n return translationHeight != undefined ? translationHeight / 2 : undefined;\n}\n\n/**\n * @description Clamps the entire viewport within the boundaries\n * \n * @category Camera\n */\nexport function clampPointEntireViewPort(point: Point, viewPortWidth: number, viewPortHeight: number, boundaries: Boundaries | undefined, cameraZoomLevel: number, cameraRotation: number): Point{\n if(boundaries == undefined){\n return point;\n }\n let topLeftCorner = convert2WorldSpaceWRT(point, {x: 0, y: viewPortHeight}, viewPortWidth, viewPortHeight, cameraZoomLevel, cameraRotation);\n let bottomLeftCorner = convert2WorldSpaceWRT(point, {x: 0, y: 0}, viewPortWidth, viewPortHeight, cameraZoomLevel, cameraRotation);\n let topRightCorner = convert2WorldSpaceWRT(point, {x: viewPortWidth, y: viewPortHeight}, viewPortWidth, viewPortHeight, cameraZoomLevel, cameraRotation);\n let bottomRightCorner = convert2WorldSpaceWRT(point, {x: viewPortWidth, y: 0}, viewPortWidth, viewPortHeight, cameraZoomLevel, cameraRotation);\n let topLeftCornerClamped = clampPoint(topLeftCorner, boundaries);\n let topRightCornerClamped = clampPoint(topRightCorner, boundaries);\n let bottomLeftCornerClamped = clampPoint(bottomLeftCorner, boundaries);\n let bottomRightCornerClamped = clampPoint(bottomRightCorner, boundaries);\n let topLeftCornerDiff = PointCal.subVector(topLeftCornerClamped, topLeftCorner);\n let topRightCornerDiff = PointCal.subVector(topRightCornerClamped, topRightCorner);\n let bottomLeftCornerDiff = PointCal.subVector(bottomLeftCornerClamped, bottomLeftCorner);\n let bottomRightCornerDiff = PointCal.subVector(bottomRightCornerClamped, bottomRightCorner);\n let diffs = [topLeftCornerDiff, topRightCornerDiff, bottomLeftCornerDiff, bottomRightCornerDiff];\n let maxXDiff = Math.abs(diffs[0].x);\n let maxYDiff = Math.abs(diffs[0].y);\n let delta = diffs[0];\n diffs.forEach((diff)=>{\n if(Math.abs(diff.x) > maxXDiff){\n maxXDiff = Math.abs(diff.x);\n delta.x = diff.x;\n }\n if(Math.abs(diff.y) > maxYDiff){\n maxYDiff = Math.abs(diff.y);\n delta.y = diff.y;\n }\n });\n return PointCal.addVector(point, delta);\n}\n","/**\n * @description The limits of the rotation.\n * \n * @category Camera\n */\nexport type RotationLimits = {start: number, end: number, ccw: boolean, startAsTieBreaker: boolean};\n\n/**\n * @description The boundary of the rotation. (experimental)\n * \n * @category Camera\n */\nexport type RotationBoundary = {start: number, end: number, positiveDirection: boolean, startAsTieBreaker: boolean};\n\n/**\n * @description Clamps the rotation within the limits.\n * \n * @category Camera\n */\nexport function clampRotation(rotation: number, rotationLimits?: RotationLimits): number{\n if(rotationWithinLimits(rotation, rotationLimits) || rotationLimits === undefined){\n return rotation;\n }\n rotation = normalizeAngleZero2TwoPI(rotation);\n const angleSpanFromStart = angleSpan(rotationLimits.start, rotation);\n const angleSpanFromEnd = angleSpan(rotationLimits.end, rotation);\n if((rotationLimits.ccw && (angleSpanFromStart < 0 || angleSpanFromEnd > 0)) || (!rotationLimits.ccw && (angleSpanFromStart > 0 || angleSpanFromEnd < 0))){\n // ccw out of bounds\n if(Math.abs(angleSpanFromStart) === Math.abs(angleSpanFromEnd)){\n // console.log(\"tie\", \"start:\", rotationLimits.start, \"end:\", rotationLimits.end, \"rotation:\", rotation);\n return rotationLimits.startAsTieBreaker ? rotationLimits.start : rotationLimits.end;\n }\n const closerToStart = Math.abs(angleSpanFromStart) < Math.abs(angleSpanFromEnd);\n return closerToStart ? rotationLimits.start : rotationLimits.end;\n }\n return rotation;\n}\n\n/**\n * @description Checks if the rotation is within the limits.\n * \n * @category Camera\n */\nexport function rotationWithinLimits(rotation: number, rotationLimits?: RotationLimits): boolean{\n if(rotationLimits === undefined){\n return true;\n }\n if(normalizeAngleZero2TwoPI(rotationLimits.start) === normalizeAngleZero2TwoPI(rotationLimits.end)){\n return true;\n }\n if(normalizeAngleZero2TwoPI(rotationLimits.start + 0.01) === normalizeAngleZero2TwoPI(rotationLimits.end + 0.01)){\n return true;\n }\n const normalizedRotation = normalizeAngleZero2TwoPI(rotation);\n const angleSpanFromStart = angleSpan(rotationLimits.start, normalizedRotation);\n const angleSpanFromEnd = angleSpan(rotationLimits.end, normalizedRotation);\n if((rotationLimits.ccw && (angleSpanFromStart < 0 || angleSpanFromEnd > 0)) || (!rotationLimits.ccw && (angleSpanFromStart > 0 || angleSpanFromEnd < 0))){\n return false;\n }\n return true;\n}\n\n/**\n * @description Checks if the rotation is within the boundary. (experimental)\n * \n * @category Camera\n */\nexport function rotationWithinBoundary(rotation: number, rotationBoundary: RotationBoundary): boolean {\n if(normalizeAngleZero2TwoPI(rotationBoundary.start) === normalizeAngleZero2TwoPI(rotationBoundary.end)){\n return true;\n }\n if(normalizeAngleZero2TwoPI(rotationBoundary.start + 0.01) === normalizeAngleZero2TwoPI(rotationBoundary.end + 0.01)){\n return true;\n }\n const normalizedRotation = normalizeAngleZero2TwoPI(rotation);\n\n let angleFromStart = normalizedRotation - normalizeAngleZero2TwoPI(rotationBoundary.start);\n if (angleFromStart < 0){\n angleFromStart += (Math.PI * 2);\n }\n if (!rotationBoundary.positiveDirection && angleFromStart > 0){\n angleFromStart = Math.PI * 2 - angleFromStart;\n }\n\n let angleRange = normalizeAngleZero2TwoPI(rotationBoundary.end) - normalizeAngleZero2TwoPI(rotationBoundary.start);\n if(angleRange < 0){\n angleRange += (Math.PI * 2);\n }\n if(!rotationBoundary.positiveDirection && angleRange > 0){\n angleRange = Math.PI * 2 - angleRange;\n }\n\n return angleRange >= angleFromStart;\n}\n\n/**\n * @description Normalizes the angle to be between 0 and 2π.\n * \n * @category Camera\n */\nexport function normalizeAngleZero2TwoPI(angle: number){\n // reduce the angle \n angle = angle % (Math.PI * 2);\n\n // force it to be the positive remainder, so that 0 <= angle < 2 * Math.PI \n angle = (angle + Math.PI * 2) % (Math.PI * 2); \n return angle;\n}\n\n/**\n * @description Gets the smaller angle span between two angles. (in radians)\n * \n * @category Camera\n */\nexport function angleSpan(from: number, to: number): number{\n // in radians\n from = normalizeAngleZero2TwoPI(from);\n to = normalizeAngleZero2TwoPI(to);\n let angleDiff = to - from;\n \n if(angleDiff > Math.PI){\n angleDiff = - (Math.PI * 2 - angleDiff);\n }\n\n if(angleDiff < -Math.PI){\n angleDiff += (Math.PI * 2);\n }\n return angleDiff;\n}\n\n/**\n * @description Converts degrees to radians.\n * \n * @category Camera\n */\nexport function deg2rad(deg: number): number{\n return deg * Math.PI / 180;\n}\n\n/**\n * @description Converts radians to degrees.\n * \n * @category Camera\n */\nexport function rad2deg(rad: number): number{\n return rad * 180 / Math.PI;\n}\n","export type Observer<T extends any[]> = (...data: T) => void;\n\nexport interface SubscriptionOptions {\n signal?: AbortSignal;\n}\n\nexport class Observable<T extends any[]> {\n private observers: Observer<T>[] = [];\n\n subscribe(observer: Observer<T>, options?: SubscriptionOptions): () => void {\n this.observers.push(observer);\n\n // Handle AbortSignal\n if (options?.signal) {\n // If signal is already aborted, don't add the observer\n if (options.signal.aborted) {\n this.observers = this.observers.filter(o => o !== observer);\n return () => {};\n }\n\n // Add abort handler\n const abortHandler = () => {\n this.observers = this.observers.filter(o => o !== observer);\n options.signal?.removeEventListener('abort', abortHandler);\n };\n\n options.signal.addEventListener('abort', abortHandler);\n }\n\n // Return unsubscribe function\n return () => {\n this.observers = this.observers.filter(o => o !== observer);\n };\n }\n\n notify(...data: T): void {\n this.observers.forEach(observer => queueMicrotask(() => observer(...data)));\n }\n}\n\n// Usage example\n// const observable = new Observable<[string]>();\n\n// Create an AbortController\n// const controller = new AbortController();\n\n// Subscribe with AbortSignal\n// const unsubscribe = observable.subscribe(\n// (data) => console.log('Received:', data),\n// { signal: controller.signal }\n// );\n\n// Example notifications\n// observable.notify('Hello!'); // Observer will receive this\n\n// Abort the subscription\n// controller.abort();\n\n// Observer won't receive this notification\n// observable.notify('World!');\n\n// Alternative way to unsubscribe using the returned function\n// unsubscribe();","import { Point } from \"src/utils/misc\";\nimport { Observable, Observer, SubscriptionOptions } from \"../utils/observable\";\n\n/**\n * @description The payload for the pan event.\n * \n * @category Camera\n */\nexport type CameraPanEventPayload = {\n diff: Point;\n}\n\n/**\n * @description The payload for the zoom event.\n * \n * @category Camera\n */\nexport type CameraZoomEventPayload = {\n deltaZoomAmount: number;\n}\n\n/**\n * @description The payload for the rotate event.\n * \n * @category Camera\n */\nexport type CameraRotateEventPayload = {\n deltaRotation: number;\n}\n\n/**\n * @description The mapping of the camera events.\n * This is primarily used for type inference.\n * \n * @category Camera\n */\nexport type CameraEventMap = {\n \"pan\": CameraPanEventPayload,\n \"zoom\": CameraZoomEventPayload,\n \"rotate\": CameraRotateEventPayload,\n \"all\": AllCameraEventPayload,\n}\n\n/**\n * @description The type of the camera rotate event.\n * The type is for discriminating the event type when the all event is triggered.\n * \n * @category Camera\n */\nexport type CameraRotateEvent = {\n type: \"rotate\",\n} & CameraRotateEventPayload;\n\n/**\n * @description The type of the camera pan event.\n * The type is for discriminating the event type when the all event is triggered.\n * \n * @category Camera\n */\nexport type CameraPanEvent = {\n type: \"pan\",\n} & CameraPanEventPayload;\n\n/**\n * @description The type of the camera zoom event.\n * The type is for discriminating the event type when the all event is triggered.\n * \n * @category Camera\n */\nexport type CameraZoomEvent = {\n type: \"zoom\",\n} & CameraZoomEventPayload;\n\n/**\n * @description The type of