SVGRenderer.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. import {
  2. Box2,
  3. Camera,
  4. Color,
  5. Matrix3,
  6. Matrix4,
  7. Object3D,
  8. SRGBColorSpace,
  9. Vector3
  10. } from 'three';
  11. import {
  12. Projector,
  13. RenderableFace,
  14. RenderableLine,
  15. RenderableSprite
  16. } from '../renderers/Projector.js';
  17. /**
  18. * Can be used to wrap SVG elements into a 3D object.
  19. *
  20. * @augments Object3D
  21. * @three_import import { SVGObject } from 'three/addons/renderers/SVGRenderer.js';
  22. */
  23. class SVGObject extends Object3D {
  24. /**
  25. * Constructs a new SVG object.
  26. *
  27. * @param {SVGElement} node - The SVG element.
  28. */
  29. constructor( node ) {
  30. super();
  31. /**
  32. * This flag can be used for type testing.
  33. *
  34. * @type {boolean}
  35. * @readonly
  36. * @default true
  37. */
  38. this.isSVGObject = true;
  39. /**
  40. * This SVG element.
  41. *
  42. * @type {SVGElement}
  43. */
  44. this.node = node;
  45. }
  46. }
  47. /**
  48. * This renderer an be used to render geometric data using SVG. The produced vector
  49. * graphics are particular useful in the following use cases:
  50. *
  51. * - Animated logos or icons.
  52. * - Interactive 2D/3D diagrams or graphs.
  53. * - Interactive maps.
  54. * - Complex or animated user interfaces.
  55. *
  56. * `SVGRenderer` has various advantages. It produces crystal-clear and sharp output which
  57. * is independent of the actual viewport resolution.SVG elements can be styled via CSS.
  58. * And they have good accessibility since it's possible to add metadata like title or description
  59. * (useful for search engines or screen readers).
  60. *
  61. * There are, however, some important limitations:
  62. * - No advanced shading.
  63. * - No texture support.
  64. * - No shadow support.
  65. *
  66. * @three_import import { SVGRenderer } from 'three/addons/renderers/SVGRenderer.js';
  67. */
  68. class SVGRenderer {
  69. /**
  70. * Constructs a new SVG renderer.
  71. */
  72. constructor() {
  73. let _renderData, _elements, _lights,
  74. _svgWidth, _svgHeight, _svgWidthHalf, _svgHeightHalf,
  75. _v1, _v2, _v3,
  76. _svgNode,
  77. _pathCount = 0,
  78. _precision = null,
  79. _quality = 1,
  80. _currentPath, _currentStyle;
  81. const _this = this,
  82. _clipBox = new Box2(),
  83. _elemBox = new Box2(),
  84. _color = new Color(),
  85. _diffuseColor = new Color(),
  86. _ambientLight = new Color(),
  87. _directionalLights = new Color(),
  88. _pointLights = new Color(),
  89. _clearColor = new Color(),
  90. _vector3 = new Vector3(), // Needed for PointLight
  91. _centroid = new Vector3(),
  92. _normal = new Vector3(),
  93. _normalViewMatrix = new Matrix3(),
  94. _viewMatrix = new Matrix4(),
  95. _viewProjectionMatrix = new Matrix4(),
  96. _svgPathPool = [],
  97. _projector = new Projector(),
  98. _svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
  99. /**
  100. * The DOM where the renderer appends its child-elements.
  101. *
  102. * @type {DOMElement}
  103. */
  104. this.domElement = _svg;
  105. /**
  106. * Whether to automatically perform a clear before a render call or not.
  107. *
  108. * @type {boolean}
  109. * @default true
  110. */
  111. this.autoClear = true;
  112. /**
  113. * Whether to sort 3D objects or not.
  114. *
  115. * @type {boolean}
  116. * @default true
  117. */
  118. this.sortObjects = true;
  119. /**
  120. * Whether to sort elements or not.
  121. *
  122. * @type {boolean}
  123. * @default true
  124. */
  125. this.sortElements = true;
  126. /**
  127. * Number of fractional pixels to enlarge polygons in order to
  128. * prevent anti-aliasing gaps. Range is `[0,1]`.
  129. *
  130. * @type {number}
  131. * @default 0.5
  132. */
  133. this.overdraw = 0.5;
  134. /**
  135. * The output color space.
  136. *
  137. * @type {(SRGBColorSpace|LinearSRGBColorSpace)}
  138. * @default SRGBColorSpace
  139. */
  140. this.outputColorSpace = SRGBColorSpace;
  141. /**
  142. * Provides information about the number of
  143. * rendered vertices and faces.
  144. *
  145. * @type {Object}
  146. */
  147. this.info = {
  148. render: {
  149. vertices: 0,
  150. faces: 0
  151. }
  152. };
  153. /**
  154. * Sets the render quality. Setting to `high` means This value indicates that the browser
  155. * tries to improve the SVG quality over rendering speed and geometric precision.
  156. *
  157. * @param {('low'|'high')} quality - The quality.
  158. */
  159. this.setQuality = function ( quality ) {
  160. switch ( quality ) {
  161. case 'high': _quality = 1; break;
  162. case 'low': _quality = 0; break;
  163. }
  164. };
  165. /**
  166. * Sets the clear color.
  167. *
  168. * @param {(number|Color|string)} color - The clear color to set.
  169. */
  170. this.setClearColor = function ( color ) {
  171. _clearColor.set( color );
  172. };
  173. this.setPixelRatio = function () {};
  174. /**
  175. * Resizes the renderer to the given width and height.
  176. *
  177. * @param {number} width - The width of the renderer.
  178. * @param {number} height - The height of the renderer.
  179. */
  180. this.setSize = function ( width, height ) {
  181. _svgWidth = width; _svgHeight = height;
  182. _svgWidthHalf = _svgWidth / 2; _svgHeightHalf = _svgHeight / 2;
  183. _svg.setAttribute( 'viewBox', ( - _svgWidthHalf ) + ' ' + ( - _svgHeightHalf ) + ' ' + _svgWidth + ' ' + _svgHeight );
  184. _svg.setAttribute( 'width', _svgWidth );
  185. _svg.setAttribute( 'height', _svgHeight );
  186. _clipBox.min.set( - _svgWidthHalf, - _svgHeightHalf );
  187. _clipBox.max.set( _svgWidthHalf, _svgHeightHalf );
  188. };
  189. /**
  190. * Returns an object containing the width and height of the renderer.
  191. *
  192. * @return {{width:number,height:number}} The size of the renderer.
  193. */
  194. this.getSize = function () {
  195. return {
  196. width: _svgWidth,
  197. height: _svgHeight
  198. };
  199. };
  200. /**
  201. * Sets the precision of the data used to create a paths.
  202. *
  203. * @param {number} precision - The precision to set.
  204. */
  205. this.setPrecision = function ( precision ) {
  206. _precision = precision;
  207. };
  208. function removeChildNodes() {
  209. _pathCount = 0;
  210. while ( _svg.childNodes.length > 0 ) {
  211. _svg.removeChild( _svg.childNodes[ 0 ] );
  212. }
  213. }
  214. function convert( c ) {
  215. return _precision !== null ? c.toFixed( _precision ) : c;
  216. }
  217. /**
  218. * Performs a manual clear with the defined clear color.
  219. */
  220. this.clear = function () {
  221. removeChildNodes();
  222. _svg.style.backgroundColor = _clearColor.getStyle( _this.outputColorSpace );
  223. };
  224. /**
  225. * Renders the given scene using the given camera.
  226. *
  227. * @param {Object3D} scene - A scene or any other type of 3D object.
  228. * @param {Camera} camera - The camera.
  229. */
  230. this.render = function ( scene, camera ) {
  231. if ( camera instanceof Camera === false ) {
  232. console.error( 'THREE.SVGRenderer.render: camera is not an instance of Camera.' );
  233. return;
  234. }
  235. const background = scene.background;
  236. if ( background && background.isColor ) {
  237. removeChildNodes();
  238. _svg.style.backgroundColor = background.getStyle( _this.outputColorSpace );
  239. } else if ( this.autoClear === true ) {
  240. this.clear();
  241. }
  242. _this.info.render.vertices = 0;
  243. _this.info.render.faces = 0;
  244. _viewMatrix.copy( camera.matrixWorldInverse );
  245. _viewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, _viewMatrix );
  246. _renderData = _projector.projectScene( scene, camera, this.sortObjects, this.sortElements );
  247. _elements = _renderData.elements;
  248. _lights = _renderData.lights;
  249. _normalViewMatrix.getNormalMatrix( camera.matrixWorldInverse );
  250. calculateLights( _lights );
  251. // reset accumulated path
  252. _currentPath = '';
  253. _currentStyle = '';
  254. for ( let e = 0, el = _elements.length; e < el; e ++ ) {
  255. const element = _elements[ e ];
  256. const material = element.material;
  257. if ( material === undefined || material.opacity === 0 ) continue;
  258. _elemBox.makeEmpty();
  259. if ( element instanceof RenderableSprite ) {
  260. _v1 = element;
  261. _v1.x *= _svgWidthHalf; _v1.y *= - _svgHeightHalf;
  262. renderSprite( _v1, element, material );
  263. } else if ( element instanceof RenderableLine ) {
  264. _v1 = element.v1; _v2 = element.v2;
  265. _v1.positionScreen.x *= _svgWidthHalf; _v1.positionScreen.y *= - _svgHeightHalf;
  266. _v2.positionScreen.x *= _svgWidthHalf; _v2.positionScreen.y *= - _svgHeightHalf;
  267. _elemBox.setFromPoints( [ _v1.positionScreen, _v2.positionScreen ] );
  268. if ( _clipBox.intersectsBox( _elemBox ) === true ) {
  269. renderLine( _v1, _v2, material );
  270. }
  271. } else if ( element instanceof RenderableFace ) {
  272. _v1 = element.v1; _v2 = element.v2; _v3 = element.v3;
  273. if ( _v1.positionScreen.z < - 1 || _v1.positionScreen.z > 1 ) continue;
  274. if ( _v2.positionScreen.z < - 1 || _v2.positionScreen.z > 1 ) continue;
  275. if ( _v3.positionScreen.z < - 1 || _v3.positionScreen.z > 1 ) continue;
  276. _v1.positionScreen.x *= _svgWidthHalf; _v1.positionScreen.y *= - _svgHeightHalf;
  277. _v2.positionScreen.x *= _svgWidthHalf; _v2.positionScreen.y *= - _svgHeightHalf;
  278. _v3.positionScreen.x *= _svgWidthHalf; _v3.positionScreen.y *= - _svgHeightHalf;
  279. if ( this.overdraw > 0 ) {
  280. expand( _v1.positionScreen, _v2.positionScreen, this.overdraw );
  281. expand( _v2.positionScreen, _v3.positionScreen, this.overdraw );
  282. expand( _v3.positionScreen, _v1.positionScreen, this.overdraw );
  283. }
  284. _elemBox.setFromPoints( [
  285. _v1.positionScreen,
  286. _v2.positionScreen,
  287. _v3.positionScreen
  288. ] );
  289. if ( _clipBox.intersectsBox( _elemBox ) === true ) {
  290. renderFace3( _v1, _v2, _v3, element, material );
  291. }
  292. }
  293. }
  294. flushPath(); // just to flush last svg:path
  295. scene.traverseVisible( function ( object ) {
  296. if ( object.isSVGObject ) {
  297. _vector3.setFromMatrixPosition( object.matrixWorld );
  298. _vector3.applyMatrix4( _viewProjectionMatrix );
  299. if ( _vector3.z < - 1 || _vector3.z > 1 ) return;
  300. const x = _vector3.x * _svgWidthHalf;
  301. const y = - _vector3.y * _svgHeightHalf;
  302. const node = object.node;
  303. node.setAttribute( 'transform', 'translate(' + x + ',' + y + ')' );
  304. _svg.appendChild( node );
  305. }
  306. } );
  307. };
  308. function calculateLights( lights ) {
  309. _ambientLight.setRGB( 0, 0, 0 );
  310. _directionalLights.setRGB( 0, 0, 0 );
  311. _pointLights.setRGB( 0, 0, 0 );
  312. for ( let l = 0, ll = lights.length; l < ll; l ++ ) {
  313. const light = lights[ l ];
  314. const lightColor = light.color;
  315. if ( light.isAmbientLight ) {
  316. _ambientLight.r += lightColor.r;
  317. _ambientLight.g += lightColor.g;
  318. _ambientLight.b += lightColor.b;
  319. } else if ( light.isDirectionalLight ) {
  320. _directionalLights.r += lightColor.r;
  321. _directionalLights.g += lightColor.g;
  322. _directionalLights.b += lightColor.b;
  323. } else if ( light.isPointLight ) {
  324. _pointLights.r += lightColor.r;
  325. _pointLights.g += lightColor.g;
  326. _pointLights.b += lightColor.b;
  327. }
  328. }
  329. }
  330. function calculateLight( lights, position, normal, color ) {
  331. for ( let l = 0, ll = lights.length; l < ll; l ++ ) {
  332. const light = lights[ l ];
  333. const lightColor = light.color;
  334. if ( light.isDirectionalLight ) {
  335. const lightPosition = _vector3.setFromMatrixPosition( light.matrixWorld ).normalize();
  336. let amount = normal.dot( lightPosition );
  337. if ( amount <= 0 ) continue;
  338. amount *= light.intensity;
  339. color.r += lightColor.r * amount;
  340. color.g += lightColor.g * amount;
  341. color.b += lightColor.b * amount;
  342. } else if ( light.isPointLight ) {
  343. const lightPosition = _vector3.setFromMatrixPosition( light.matrixWorld );
  344. let amount = normal.dot( _vector3.subVectors( lightPosition, position ).normalize() );
  345. if ( amount <= 0 ) continue;
  346. amount *= light.distance == 0 ? 1 : 1 - Math.min( position.distanceTo( lightPosition ) / light.distance, 1 );
  347. if ( amount == 0 ) continue;
  348. amount *= light.intensity;
  349. color.r += lightColor.r * amount;
  350. color.g += lightColor.g * amount;
  351. color.b += lightColor.b * amount;
  352. }
  353. }
  354. }
  355. function renderSprite( v1, element, material ) {
  356. let scaleX = element.scale.x * _svgWidthHalf;
  357. let scaleY = element.scale.y * _svgHeightHalf;
  358. if ( material.isPointsMaterial ) {
  359. scaleX *= material.size;
  360. scaleY *= material.size;
  361. }
  362. const path = 'M' + convert( v1.x - scaleX * 0.5 ) + ',' + convert( v1.y - scaleY * 0.5 ) + 'h' + convert( scaleX ) + 'v' + convert( scaleY ) + 'h' + convert( - scaleX ) + 'z';
  363. let style = '';
  364. if ( material.isSpriteMaterial || material.isPointsMaterial ) {
  365. style = 'fill:' + material.color.getStyle( _this.outputColorSpace ) + ';fill-opacity:' + material.opacity;
  366. }
  367. addPath( style, path );
  368. }
  369. function renderLine( v1, v2, material ) {
  370. const path = 'M' + convert( v1.positionScreen.x ) + ',' + convert( v1.positionScreen.y ) + 'L' + convert( v2.positionScreen.x ) + ',' + convert( v2.positionScreen.y );
  371. if ( material.isLineBasicMaterial ) {
  372. let style = 'fill:none;stroke:' + material.color.getStyle( _this.outputColorSpace ) + ';stroke-opacity:' + material.opacity + ';stroke-width:' + material.linewidth + ';stroke-linecap:' + material.linecap;
  373. if ( material.isLineDashedMaterial ) {
  374. style = style + ';stroke-dasharray:' + material.dashSize + ',' + material.gapSize;
  375. }
  376. addPath( style, path );
  377. }
  378. }
  379. function renderFace3( v1, v2, v3, element, material ) {
  380. _this.info.render.vertices += 3;
  381. _this.info.render.faces ++;
  382. const path = 'M' + convert( v1.positionScreen.x ) + ',' + convert( v1.positionScreen.y ) + 'L' + convert( v2.positionScreen.x ) + ',' + convert( v2.positionScreen.y ) + 'L' + convert( v3.positionScreen.x ) + ',' + convert( v3.positionScreen.y ) + 'z';
  383. let style = '';
  384. if ( material.isMeshBasicMaterial ) {
  385. _color.copy( material.color );
  386. if ( material.vertexColors ) {
  387. _color.multiply( element.color );
  388. }
  389. } else if ( material.isMeshLambertMaterial || material.isMeshPhongMaterial || material.isMeshStandardMaterial ) {
  390. _diffuseColor.copy( material.color );
  391. if ( material.vertexColors ) {
  392. _diffuseColor.multiply( element.color );
  393. }
  394. _color.copy( _ambientLight );
  395. _centroid.copy( v1.positionWorld ).add( v2.positionWorld ).add( v3.positionWorld ).divideScalar( 3 );
  396. calculateLight( _lights, _centroid, element.normalModel, _color );
  397. _color.multiply( _diffuseColor ).add( material.emissive );
  398. } else if ( material.isMeshNormalMaterial ) {
  399. _normal.copy( element.normalModel ).applyMatrix3( _normalViewMatrix ).normalize();
  400. _color.setRGB( _normal.x, _normal.y, _normal.z ).multiplyScalar( 0.5 ).addScalar( 0.5 );
  401. }
  402. if ( material.wireframe ) {
  403. style = 'fill:none;stroke:' + _color.getStyle( _this.outputColorSpace ) + ';stroke-opacity:' + material.opacity + ';stroke-width:' + material.wireframeLinewidth + ';stroke-linecap:' + material.wireframeLinecap + ';stroke-linejoin:' + material.wireframeLinejoin;
  404. } else {
  405. style = 'fill:' + _color.getStyle( _this.outputColorSpace ) + ';fill-opacity:' + material.opacity;
  406. }
  407. addPath( style, path );
  408. }
  409. // Hide anti-alias gaps
  410. function expand( v1, v2, pixels ) {
  411. let x = v2.x - v1.x, y = v2.y - v1.y;
  412. const det = x * x + y * y;
  413. if ( det === 0 ) return;
  414. const idet = pixels / Math.sqrt( det );
  415. x *= idet; y *= idet;
  416. v2.x += x; v2.y += y;
  417. v1.x -= x; v1.y -= y;
  418. }
  419. function addPath( style, path ) {
  420. if ( _currentStyle === style ) {
  421. _currentPath += path;
  422. } else {
  423. flushPath();
  424. _currentStyle = style;
  425. _currentPath = path;
  426. }
  427. }
  428. function flushPath() {
  429. if ( _currentPath ) {
  430. _svgNode = getPathNode( _pathCount ++ );
  431. _svgNode.setAttribute( 'd', _currentPath );
  432. _svgNode.setAttribute( 'style', _currentStyle );
  433. _svg.appendChild( _svgNode );
  434. }
  435. _currentPath = '';
  436. _currentStyle = '';
  437. }
  438. function getPathNode( id ) {
  439. if ( _svgPathPool[ id ] == null ) {
  440. _svgPathPool[ id ] = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
  441. if ( _quality == 0 ) {
  442. _svgPathPool[ id ].setAttribute( 'shape-rendering', 'crispEdges' ); //optimizeSpeed
  443. }
  444. return _svgPathPool[ id ];
  445. }
  446. return _svgPathPool[ id ];
  447. }
  448. }
  449. }
  450. export { SVGObject, SVGRenderer };