Renderer.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. import {Pointer} from "./input/Pointer.js";
  2. import {ViewportControls} from "./controls/ViewportControls.js";
  3. import {AnimationTimer} from "./utils/AnimationTimer";
  4. import {EventManager} from "./utils/EventManager";
  5. /**
  6. * The renderer is responsible for drawing the objects structure into the canvas element and manage its rendering state.
  7. *
  8. * Object are updated by the renderer before drawing, the renderer sorts the objects by layer, checks for pointer events and draw the objects into the screen.
  9. *
  10. * Input handling is also performed by the renderer (it is also used for the event handling).
  11. *
  12. * @class
  13. * @param {Element} canvas Canvas to render the content to.
  14. * @param {Object} options Renderer canvas options.
  15. */
  16. function Renderer(canvas, options)
  17. {
  18. // Default options
  19. var defaultOptions =
  20. {
  21. alpha: true,
  22. disableContextMenu: true,
  23. imageSmoothingEnabled: true,
  24. imageSmoothingQuality: "low",
  25. globalAlpha: 1.0,
  26. // "source-over", "source-in", "source-out", "source-atop", "destination-over", "destination-in", "destination-out", "destination-atop", "lighter", "copy", "xor"
  27. globalCompositeOperation: "source-over",
  28. // "auto", "optimizeSpeed", "optimizeLegibility", "geometricPrecision"
  29. textRendering: "auto",
  30. filter: null
  31. };
  32. options = options ? Object.assign(defaultOptions, options) : defaultOptions;
  33. /**
  34. * Event manager for DOM events created by the renderer.
  35. *
  36. * Created automatically when the renderer is created. Disposed automatically when the renderer is destroyed.
  37. *
  38. * @type {EventManager}
  39. */
  40. this.manager = new EventManager();
  41. if(options.disableContextMenu) {
  42. this.manager.add(canvas, "contextmenu", function(e) {
  43. e.preventDefault();
  44. e.stopPropagation();
  45. });
  46. }
  47. this.manager.create();
  48. /**
  49. * Canvas DOM element, the user needs to manage the canvas state.
  50. *
  51. * The canvas size (width and height) should always match its actual display size (adjusted for the device pixel ratio).
  52. *
  53. * @type {Element}
  54. */
  55. this.canvas = canvas;
  56. /**
  57. * Division where DOM and SVG objects should be placed at. This division should be perfectly aligned whit the canvas element.
  58. *
  59. * If no division is defined the canvas parent element is used by default to place these objects.
  60. *
  61. * The DOM container to be used can be obtained using the getDomContainer() method.
  62. *
  63. * @type {Element}
  64. */
  65. this.container = null;
  66. /**
  67. * Canvas 2D rendering context used to draw content.
  68. *
  69. * The options passed thought the constructor are applied to the context created.
  70. *
  71. * @type {CanvasRenderingContext2D}
  72. */
  73. this.context = this.canvas.getContext("2d", {alpha: options.alpha});
  74. this.context.imageSmoothingEnabled = options.imageSmoothingEnabled;
  75. this.context.imageSmoothingQuality = options.imageSmoothingQuality;
  76. this.context.globalCompositeOperation = options.globalCompositeOperation;
  77. this.context.globalAlpha = options.globalAlpha;
  78. this.context.textRendering = options.textRendering;
  79. this.context.filter = options.filter;
  80. /**
  81. * Pointer input handler object, automatically updated by the renderer.
  82. *
  83. * The pointer is attached to the DOM window and to the canvas provided by the user.
  84. *
  85. * @type {Pointer}
  86. */
  87. this.pointer = new Pointer(window, this.canvas);
  88. /**
  89. * Indicates if the canvas should be automatically cleared before new frame is drawn.
  90. *
  91. * If set to false the user should clear the frame before drawing.
  92. *
  93. * @type {boolean}
  94. */
  95. this.autoClear = true;
  96. }
  97. /**
  98. * Get the DOM container to be used to store DOM and SVG objects.
  99. *
  100. * Can be set using the container attribute, by default the canvas parent element is used.
  101. *
  102. * @returns {Element} DOM element selected for objects.
  103. */
  104. Renderer.prototype.getDomContainer = function()
  105. {
  106. return this.container !== null ? this.container : this.canvas.parentElement;
  107. };
  108. /**
  109. * Creates a infinite render loop to render the group into a viewport each frame.
  110. *
  111. * Automatically creates a viewport controls object, used for the user to control the viewport.
  112. *
  113. * The render loop can be accessed trough the animation timer returned. Should be stopped when no longer necessary to prevent memory/code leaks.
  114. *
  115. * @param {Object2D} group Object to be rendered, alongside with all its children. Object2D can be used as a container to group objects.
  116. * @param {Viewport} viewport Viewport into the scene.
  117. * @param {Function} onUpdate Function called before rendering the frame, can be used for additional logic code. Object logic should be directly written in the update method of objects.
  118. * @return {AnimationTimer} Animation timer created for this render loop. Should be stopped when no longer necessary.
  119. */
  120. Renderer.prototype.createRenderLoop = function(group, viewport, onUpdate)
  121. {
  122. var self = this;
  123. var controls = new ViewportControls(viewport);
  124. var timer = new AnimationTimer(function()
  125. {
  126. if(onUpdate !== undefined)
  127. {
  128. onUpdate();
  129. }
  130. controls.update(self.pointer);
  131. self.update(group, viewport);
  132. });
  133. timer.start();
  134. return {timer: timer, controls: controls};
  135. };
  136. /**
  137. * Dispose the renderer object, clears the pointer events attached to the window/canvas.
  138. *
  139. * Should be called if the renderer is no longer in use to prevent code/memory leaks.
  140. */
  141. Renderer.prototype.dispose = function(group, viewport, onUpdate)
  142. {
  143. this.manager.destroy();
  144. this.pointer.dispose();
  145. };
  146. /**
  147. * Renders a object using a user defined viewport into a canvas element.
  148. *
  149. * Before rendering automatically updates the input handlers and calculates the objects/viewport transformation matrices.
  150. *
  151. * The canvas state is saved and restored for each individual object, ensuring that the code of one object does not affect another one.
  152. *
  153. * Should be called at a fixed rate preferably using the requestAnimationFrame() method, its also possible to use the createRenderLoop() method, that automatically creates a infinite render loop.
  154. *
  155. * @param object {Object2D} Object to be updated and drawn into the canvas, the Object2D should be used as a group to store all the other objects to be updated and drawn.
  156. * @param viewport {Viewport} Viewport to be updated (should be the one where the objects will be rendered after).
  157. */
  158. Renderer.prototype.update = function(object, viewport)
  159. {
  160. // Get objects to be rendered
  161. var objects = [];
  162. // Traverse object and get all objects into a list.
  163. object.traverse(function(child)
  164. {
  165. if(child.visible)
  166. {
  167. objects.push(child);
  168. }
  169. });
  170. // Sort objects by layer
  171. objects.sort(function(a, b)
  172. {
  173. if(b.layer === a.layer)
  174. {
  175. return b.level - a.level;
  176. }
  177. return b.layer - a.layer;
  178. });
  179. // Pointer object update
  180. var pointer = this.pointer;
  181. pointer.update();
  182. // Viewport transform matrix
  183. viewport.updateMatrix();
  184. // Project pointer coordinates
  185. var point = pointer.position.clone();
  186. var viewportPoint = viewport.inverseMatrix.transformPoint(point);
  187. // Object pointer events
  188. for(var i = 0; i < objects.length; i++)
  189. {
  190. var child = objects[i];
  191. //Process the object pointer events
  192. if(child.pointerEvents)
  193. {
  194. // Calculate the pointer position in the object coordinates
  195. var localPoint = child.inverseGlobalMatrix.transformPoint(child.ignoreViewport ? point : viewportPoint);
  196. // Check if the pointer pointer is inside
  197. if(child.isInside(localPoint))
  198. {
  199. // Pointer enter
  200. if(!child.pointerInside && child.onPointerEnter !== null)
  201. {
  202. child.onPointerEnter(pointer, viewport);
  203. }
  204. // Pointer over
  205. if(child.onPointerOver !== null)
  206. {
  207. child.onPointerOver(pointer, viewport);
  208. }
  209. // Double click
  210. if(pointer.buttonDoubleClicked(Pointer.LEFT) && child.onDoubleClick !== null)
  211. {
  212. child.onDoubleClick(pointer, viewport);
  213. }
  214. // Pointer pressed
  215. if(pointer.buttonPressed(Pointer.LEFT) && child.onButtonPressed !== null)
  216. {
  217. child.onButtonPressed(pointer, viewport);
  218. }
  219. // Just released
  220. if(pointer.buttonJustReleased(Pointer.LEFT) && child.onButtonUp !== null)
  221. {
  222. child.onButtonUp(pointer, viewport);
  223. }
  224. // Pointer just pressed
  225. if(pointer.buttonJustPressed(Pointer.LEFT))
  226. {
  227. if(child.onButtonDown !== null)
  228. {
  229. child.onButtonDown(pointer, viewport);
  230. }
  231. // Drag object and break to only start a drag operation on the top element.
  232. if(child.draggable)
  233. {
  234. child.beingDragged = true;
  235. if(child.onPointerDragStart !== null)
  236. {
  237. child.onPointerDragStart(pointer, viewport);
  238. }
  239. break;
  240. }
  241. }
  242. child.pointerInside = true;
  243. }
  244. else if(child.pointerInside)
  245. {
  246. // Pointer leave
  247. if(child.onPointerLeave !== null)
  248. {
  249. child.onPointerLeave(pointer, viewport);
  250. }
  251. child.pointerInside = false;
  252. }
  253. // Stop object drag
  254. if(pointer.buttonJustReleased(Pointer.LEFT))
  255. {
  256. if(child.draggable)
  257. {
  258. // On drag end callback
  259. if(child.beingDragged === true && child.onPointerDragEnd !== null)
  260. {
  261. child.onPointerDragEnd(pointer, viewport);
  262. }
  263. child.beingDragged = false;
  264. }
  265. }
  266. }
  267. }
  268. // Object drag events and update logic
  269. for(var i = 0; i < objects.length; i++)
  270. {
  271. var child = objects[i];
  272. // Pointer drag event
  273. if(child.beingDragged)
  274. {
  275. if(child.onPointerDrag !== null)
  276. {
  277. var lastPosition = pointer.position.clone();
  278. lastPosition.sub(pointer.delta);
  279. // Get position and last position in world space to calculate world pointer movement
  280. var positionWorld = viewport.inverseMatrix.transformPoint(pointer.position);
  281. var lastWorld = viewport.inverseMatrix.transformPoint(lastPosition);
  282. // Pointer movement delta in world coordinates
  283. var delta = positionWorld.clone();
  284. delta.sub(lastWorld);
  285. child.onPointerDrag(pointer, viewport, delta, positionWorld);
  286. }
  287. }
  288. // On update
  289. if(child.onUpdate !== null)
  290. {
  291. child.onUpdate();
  292. }
  293. }
  294. // Update transformation matrices
  295. object.traverse(function(child)
  296. {
  297. child.updateMatrix();
  298. });
  299. this.context.setTransform(1, 0, 0, 1, 0, 0);
  300. // Clear canvas content
  301. if(this.autoClear)
  302. {
  303. this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
  304. }
  305. // Render into the canvas
  306. for(var i = objects.length - 1; i >= 0; i--)
  307. {
  308. if(objects[i].isMask)
  309. {
  310. continue;
  311. }
  312. if(objects[i].saveContextState)
  313. {
  314. this.context.save();
  315. }
  316. // Apply all masks
  317. var masks = objects[i].masks;
  318. for(var j = 0; j < masks.length; j++)
  319. {
  320. if(!masks[j].ignoreViewport)
  321. {
  322. viewport.matrix.setContextTransform(this.context);
  323. }
  324. masks[j].transform(this.context, viewport, this.canvas, this);
  325. masks[j].clip(this.context, viewport, this.canvas);
  326. }
  327. // Set the viewport transform
  328. if(!objects[i].ignoreViewport)
  329. {
  330. viewport.matrix.setContextTransform(this.context);
  331. }
  332. else if(masks.length > 0)
  333. {
  334. this.context.setTransform(1, 0, 0, 1, 0, 0);
  335. }
  336. // Apply the object transform to the canvas context
  337. objects[i].transform(this.context, viewport, this.canvas, this);
  338. // Style the canvas context
  339. if(objects[i].style !== null)
  340. {
  341. objects[i].style(this.context, viewport, this.canvas);
  342. }
  343. // Draw content into the canvas.
  344. if(objects[i].draw !== null)
  345. {
  346. objects[i].draw(this.context, viewport, this.canvas);
  347. }
  348. if(objects[i].restoreContextState)
  349. {
  350. this.context.restore();
  351. }
  352. }
  353. };
  354. export {Renderer};