Renderer.js 9.2 KB

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