UNPKG

8.88 kBPlain TextView Raw
1/**
2 * Sigma.js Camera Class
3 * ======================
4 *
5 * Class designed to store camera information & used to update it.
6 */
7import { EventEmitter } from "events";
8
9import * as easings from "./easings";
10import { assign } from "./utils";
11
12/**
13 * Defaults.
14 */
15const ANIMATE_DEFAULTS = {
16 easing: "quadraticInOut",
17 duration: 150,
18};
19
20const DEFAULT_ZOOMING_RATIO = 1.5;
21
22// TODO: animate options = number polymorphism?
23// TODO: pan, zoom, unzoom, reset, rotate, zoomTo
24// TODO: add width / height to camera and add #.resize
25// TODO: bind camera to renderer rather than sigma
26// TODO: add #.graphToDisplay, #.displayToGraph, batch methods later
27
28export interface CameraState {
29 x?: number;
30 y?: number;
31 angle?: number;
32 ratio?: number;
33}
34/**
35 * Camera class
36 *
37 * @constructor
38 */
39export default class Camera extends EventEmitter implements CameraState {
40 x: number = 0.5;
41 y: number = 0.5;
42 angle: number = 0;
43 ratio: number = 1;
44 nextFrame: any = null;
45 previousState: CameraState;
46 enabled: boolean = true;
47
48 constructor() {
49 super();
50
51 // State
52 this.previousState = this.getState();
53 }
54
55 /**
56 * Method used to enable the camera.
57 *
58 * @return {Camera}
59 */
60 enable(): Camera {
61 this.enabled = true;
62 return this;
63 }
64
65 /**
66 * Method used to disable the camera.
67 *
68 * @return {Camera}
69 */
70 disable(): Camera {
71 this.enabled = false;
72 return this;
73 }
74
75 /**
76 * Method used to retrieve the camera's current state.
77 *
78 * @return {object}
79 */
80 getState(): CameraState {
81 return {
82 x: this.x,
83 y: this.y,
84 angle: this.angle,
85 ratio: this.ratio,
86 };
87 }
88
89 /**
90 * Method used to retrieve the camera's previous state.
91 *
92 * @return {object}
93 */
94 getPreviousState(): CameraState {
95 const state = this.previousState;
96
97 return {
98 x: state.x,
99 y: state.y,
100 angle: state.angle,
101 ratio: state.ratio,
102 };
103 }
104
105 /**
106 * Method used to check whether the camera is currently being animated.
107 *
108 * @return {boolean}
109 */
110 isAnimated(): boolean {
111 return !!this.nextFrame;
112 }
113
114 /**
115 * Method returning the coordinates of a point from the graph frame to the
116 * viewport.
117 *
118 * @param {object} dimensions - Dimensions of the viewport.
119 * @param {number} x - The X coordinate.
120 * @param {number} y - The Y coordinate.
121 * @return {object} - The point coordinates in the viewport.
122 */
123
124 // TODO: assign to gain one object
125 // TODO: angles
126 graphToViewport(dimensions: { width: number; height: number }, x: number, y: number): { x: number; y: number } {
127 const smallestDimension = Math.min(dimensions.width, dimensions.height);
128
129 const dx = smallestDimension / dimensions.width,
130 dy = smallestDimension / dimensions.height;
131
132 // TODO: we keep on the upper left corner!
133 // TODO: how to normalize sizes?
134 return {
135 x: (x - this.x + this.ratio / 2 / dx) * (smallestDimension / this.ratio),
136 y: (this.y - y + this.ratio / 2 / dy) * (smallestDimension / this.ratio),
137 };
138 }
139
140 /**
141 * Method returning the coordinates of a point from the viewport frame to the
142 * graph frame.
143 *
144 * @param {object} dimensions - Dimensions of the viewport.
145 * @param {number} x - The X coordinate.
146 * @param {number} y - The Y coordinate.
147 * @return {object} - The point coordinates in the graph frame.
148 */
149
150 // TODO: angles
151 viewportToGraph(dimensions: { width: number; height: number }, x: number, y: number): { x: number; y: number } {
152 const smallestDimension = Math.min(dimensions.width, dimensions.height);
153
154 const dx = smallestDimension / dimensions.width,
155 dy = smallestDimension / dimensions.height;
156
157 return {
158 x: (this.ratio / smallestDimension) * x + this.x - this.ratio / 2 / dx,
159 y: -((this.ratio / smallestDimension) * y - this.y - this.ratio / 2 / dy),
160 };
161 }
162
163 /**
164 * Method returning the abstract rectangle containing the graph according
165 * to the camera's state.
166 *
167 * @return {object} - The view's rectangle.
168 */
169
170 // TODO: angle
171 viewRectangle(dimensions: {
172 width: number;
173 height: number;
174 }): { x1: number; y1: number; x2: number; y2: number; height: number } {
175 // TODO: reduce relative margin?
176 const marginX = (0 * dimensions.width) / 8,
177 marginY = (0 * dimensions.height) / 8;
178
179 const p1 = this.viewportToGraph(dimensions, 0 - marginX, 0 - marginY),
180 p2 = this.viewportToGraph(dimensions, dimensions.width + marginX, 0 - marginY),
181 h = this.viewportToGraph(dimensions, 0, dimensions.height + marginY);
182
183 return {
184 x1: p1.x,
185 y1: p1.y,
186 x2: p2.x,
187 y2: p2.y,
188 height: p2.y - h.y,
189 };
190 }
191
192 /**
193 * Method used to set the camera's state.
194 *
195 * @param {object} state - New state.
196 * @return {Camera}
197 */
198 setState(state: CameraState): Camera {
199 if (!this.enabled) return this;
200
201 // TODO: validations
202 // TODO: update by function
203
204 // Keeping track of last state
205 this.previousState = this.getState();
206
207 if ("x" in state) this.x = state.x;
208
209 if ("y" in state) this.y = state.y;
210
211 if ("angle" in state) this.angle = state.angle;
212
213 if ("ratio" in state) this.ratio = state.ratio;
214
215 // Emitting
216 // TODO: don't emit if nothing changed?
217 this.emit("updated", this.getState());
218
219 return this;
220 }
221
222 /**
223 * Method used to animate the camera.
224 *
225 * @param {object} state - State to reach eventually.
226 * @param {object} options - Options:
227 * @param {number} duration - Duration of the animation.
228 * @param {function} callback - Callback
229 * @return {function} - Return a function to cancel the animation.
230 */
231 animate(state: CameraState, options?, callback?: () => void) {
232 if (!this.enabled) return this;
233
234 // TODO: validation
235
236 options = assign({}, ANIMATE_DEFAULTS, options);
237
238 const easing = typeof options.easing === "function" ? options.easing : easings[options.easing];
239
240 // Canceling previous animation if needed
241 if (this.nextFrame) cancelAnimationFrame(this.nextFrame);
242
243 // State
244 const start = Date.now(),
245 initialState = this.getState();
246
247 // Function performing the animation
248 const fn = () => {
249 const t = (Date.now() - start) / options.duration;
250
251 // The animation is over:
252 if (t >= 1) {
253 this.nextFrame = null;
254 this.setState(state);
255
256 if (typeof callback === "function") callback();
257
258 return;
259 }
260
261 const coefficient = easing(t);
262
263 const newState: CameraState = {};
264
265 if ("x" in state) newState.x = initialState.x + (state.x - initialState.x) * coefficient;
266 if ("y" in state) newState.y = initialState.y + (state.y - initialState.y) * coefficient;
267 if ("angle" in state) newState.angle = initialState.angle + (state.angle - initialState.angle) * coefficient;
268 if ("ratio" in state) newState.ratio = initialState.ratio + (state.ratio - initialState.ratio) * coefficient;
269
270 this.setState(newState);
271
272 this.nextFrame = requestAnimationFrame(fn);
273 };
274
275 if (this.nextFrame) {
276 cancelAnimationFrame(this.nextFrame);
277 this.nextFrame = requestAnimationFrame(fn);
278 } else {
279 fn();
280 }
281 }
282
283 /**
284 * Method used to zoom the camera.
285 *
286 * @param {number|object} factorOrOptions - Factor or options.
287 * @return {function}
288 */
289 animatedZoom(factorOrOptions: number | { [key: string]: any }) {
290 if (!factorOrOptions) {
291 return this.animate({ ratio: this.ratio / DEFAULT_ZOOMING_RATIO });
292 } else {
293 if (typeof factorOrOptions === "number") return this.animate({ ratio: this.ratio / factorOrOptions });
294 else
295 return this.animate(
296 {
297 ratio: this.ratio / (factorOrOptions.factor || DEFAULT_ZOOMING_RATIO),
298 },
299 factorOrOptions,
300 );
301 }
302 }
303
304 /**
305 * Method used to unzoom the camera.
306 *
307 * @param {number|object} factorOrOptions - Factor or options.
308 * @return {function}
309 */
310 animatedUnzoom(factorOrOptions: number | { [key: string]: any }) {
311 if (!factorOrOptions) {
312 return this.animate({ ratio: this.ratio * DEFAULT_ZOOMING_RATIO });
313 } else {
314 if (typeof factorOrOptions === "number") return this.animate({ ratio: this.ratio * factorOrOptions });
315 else
316 return this.animate(
317 {
318 ratio: this.ratio * (factorOrOptions.factor || DEFAULT_ZOOMING_RATIO),
319 },
320 factorOrOptions,
321 );
322 }
323 }
324
325 /**
326 * Method used to reset the camera.
327 *
328 * @param {object} options - Options.
329 * @return {function}
330 */
331 animatedReset(options: { [key: string]: any }) {
332 return this.animate(
333 {
334 x: 0.5,
335 y: 0.5,
336 ratio: 1,
337 angle: 0,
338 },
339 options,
340 );
341 }
342}