threejs-align-html-elements-to-3d.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. 'use strict';
  2. /* global dat */
  3. {
  4. function outlineText(ctx, msg, x, y) {
  5. ctx.strokeText(msg, x, y);
  6. ctx.fillText(msg, x, y);
  7. }
  8. function arrow(ctx, x1, y1, x2, y2, start, end, size) {
  9. size = size || 1;
  10. const dx = x1 - x2;
  11. const dy = y1 - y2;
  12. const rot = -Math.atan2(dx, dy);
  13. const len = Math.sqrt(dx * dx + dy * dy);
  14. ctx.save();
  15. {
  16. ctx.translate(x1, y1);
  17. ctx.rotate(rot);
  18. ctx.beginPath();
  19. ctx.moveTo(0, 0);
  20. ctx.lineTo(0, -(len - 10 * size));
  21. ctx.stroke();
  22. }
  23. ctx.restore();
  24. if (start) {
  25. arrowHead(ctx, x1, y1, rot, size);
  26. }
  27. if (end) {
  28. arrowHead(ctx, x2, y2, rot + Math.PI, size);
  29. }
  30. }
  31. function arrowHead(ctx, x, y, rot, size) {
  32. ctx.save();
  33. {
  34. ctx.translate(x, y);
  35. ctx.rotate(rot);
  36. ctx.scale(size, size);
  37. ctx.translate(0, -10);
  38. ctx.beginPath();
  39. ctx.moveTo(0, 0);
  40. ctx.lineTo(-5, -2);
  41. ctx.lineTo(0, 10);
  42. ctx.lineTo(5, -2);
  43. ctx.closePath();
  44. ctx.fill();
  45. }
  46. ctx.restore();
  47. }
  48. const THREE = {
  49. Math: {
  50. radToDeg(rad) {
  51. return rad * 180 / Math.PI;
  52. },
  53. degToRad(deg) {
  54. return deg * Math.PI / 180;
  55. },
  56. },
  57. };
  58. class DegRadHelper {
  59. constructor(obj, prop) {
  60. this.obj = obj;
  61. this.prop = prop;
  62. }
  63. get value() {
  64. return THREE.Math.radToDeg(this.obj[this.prop]);
  65. }
  66. set value(v) {
  67. this.obj[this.prop] = THREE.Math.degToRad(v);
  68. }
  69. }
  70. function dot(x1, y1, x2, y2) {
  71. return x1 * x2 + y1 * y2;
  72. }
  73. function distance(x1, y1, x2, y2) {
  74. const dx = x1 - x2;
  75. const dy = y1 - y2;
  76. return Math.sqrt(dx * dx + dy * dy);
  77. }
  78. function normalize(x, y) {
  79. const l = distance(0, 0, x, y);
  80. if (l > 0.00001) {
  81. return [x / l, y / l];
  82. } else {
  83. return [0, 0];
  84. }
  85. }
  86. function resizeCanvasToDisplaySize(canvas, pixelRatio = 1) {
  87. const width = canvas.clientWidth * pixelRatio | 0;
  88. const height = canvas.clientHeight * pixelRatio | 0;
  89. const needResize = canvas.width !== width || canvas.height !== height;
  90. if (needResize) {
  91. canvas.width = width;
  92. canvas.height = height;
  93. }
  94. return needResize;
  95. }
  96. const diagrams = {
  97. dotProduct: {
  98. create(info) {
  99. const {elem} = info;
  100. const div = document.createElement('div');
  101. div.style.position = 'relative';
  102. div.style.width = '100%';
  103. div.style.height = '100%';
  104. elem.appendChild(div);
  105. const ctx = document.createElement('canvas').getContext('2d');
  106. div.appendChild(ctx.canvas);
  107. const settings = {
  108. rotation: 0.3,
  109. };
  110. const gui = new dat.GUI({autoPlace: false});
  111. gui.add(new DegRadHelper(settings, 'rotation'), 'value', -180, 180).name('rotation').onChange(render);
  112. gui.domElement.style.position = 'absolute';
  113. gui.domElement.style.top = '0';
  114. gui.domElement.style.right = '0';
  115. div.appendChild(gui.domElement);
  116. const darkColors = {
  117. globe: 'green',
  118. camera: '#AAA',
  119. base: '#DDD',
  120. label: '#0FF',
  121. };
  122. const lightColors = {
  123. globe: '#0C0',
  124. camera: 'black',
  125. base: '#000',
  126. label: 'blue',
  127. };
  128. const darkMatcher = window.matchMedia('(prefers-color-scheme: dark)');
  129. darkMatcher.addEventListener('change', render);
  130. function render() {
  131. const {rotation} = settings;
  132. const isDarkMode = darkMatcher.matches;
  133. const colors = isDarkMode ? darkColors : lightColors;
  134. const pixelRatio = window.devicePixelRatio;
  135. resizeCanvasToDisplaySize(ctx.canvas, pixelRatio);
  136. ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  137. ctx.save();
  138. {
  139. const width = ctx.canvas.width / pixelRatio;
  140. const height = ctx.canvas.height / pixelRatio;
  141. const min = Math.min(width, height);
  142. const half = min / 2;
  143. const r = half * 0.4;
  144. const x = r * Math.sin(-rotation);
  145. const y = r * Math.cos(-rotation);
  146. const camDX = x - 0;
  147. const camDY = y - (half - 40);
  148. const labelDir = normalize(x, y);
  149. const camToLabelDir = normalize(camDX, camDY);
  150. const dp = dot(...camToLabelDir, ...labelDir);
  151. ctx.scale(pixelRatio, pixelRatio);
  152. ctx.save();
  153. {
  154. {
  155. ctx.translate(width / 2, height / 2);
  156. ctx.beginPath();
  157. ctx.arc(0, 0, half * 0.4, 0, Math.PI * 2);
  158. ctx.fillStyle = colors.globe;
  159. ctx.fill();
  160. ctx.save();
  161. {
  162. ctx.fillStyle = colors.camera;
  163. ctx.translate(0, half);
  164. ctx.fillRect(-15, -30, 30, 30);
  165. ctx.beginPath();
  166. ctx.moveTo(0, -25);
  167. ctx.lineTo(-25, -50);
  168. ctx.lineTo( 25, -50);
  169. ctx.closePath();
  170. ctx.fill();
  171. }
  172. ctx.restore();
  173. ctx.save();
  174. {
  175. ctx.lineWidth = 4;
  176. ctx.strokeStyle = colors.camera;
  177. ctx.fillStyle = colors.camera;
  178. arrow(ctx, 0, half - 40, x, y, false, true, 2);
  179. ctx.save();
  180. {
  181. ctx.strokeStyle = colors.label;
  182. ctx.fillStyle = colors.label;
  183. arrow(ctx, 0, 0, x, y, false, true, 2);
  184. }
  185. ctx.restore();
  186. {
  187. ctx.lineWidth = 3;
  188. ctx.strokeStyle = 'black';
  189. ctx.fillStyle = dp < 0 ? 'white' : 'red';
  190. ctx.font = '20px sans-serif';
  191. ctx.textAlign = 'center';
  192. ctx.textBaseline = 'middle';
  193. outlineText(ctx, 'label', x, y);
  194. }
  195. }
  196. ctx.restore();
  197. }
  198. ctx.restore();
  199. }
  200. ctx.lineWidth = 3;
  201. ctx.font = '24px sans-serif';
  202. ctx.strokeStyle = 'black';
  203. ctx.textAlign = 'left';
  204. ctx.textBaseline = 'middle';
  205. ctx.save();
  206. {
  207. ctx.translate(width / 4, 80);
  208. const textColor = dp < 0 ? colors.base : 'red';
  209. advanceText(ctx, textColor, 'dot( ');
  210. ctx.save();
  211. {
  212. ctx.fillStyle = colors.camera;
  213. ctx.strokeStyle = colors.camera;
  214. ctx.rotate(Math.atan2(camDY, camDX));
  215. arrow(ctx, -8, 0, 8, 0, false, true, 1);
  216. }
  217. ctx.restore();
  218. advanceText(ctx, textColor, ' , ');
  219. ctx.save();
  220. {
  221. ctx.fillStyle = colors.label;
  222. ctx.strokeStyle = colors.label;
  223. ctx.rotate(rotation + Math.PI * 0.5);
  224. arrow(ctx, -8, 0, 8, 0, false, true, 1);
  225. }
  226. ctx.restore();
  227. advanceText(ctx, textColor, ` ) = ${dp.toFixed(2)}`);
  228. }
  229. ctx.restore();
  230. }
  231. ctx.restore();
  232. }
  233. render();
  234. window.addEventListener('resize', render);
  235. },
  236. },
  237. };
  238. function advanceText(ctx, color, str) {
  239. ctx.fillStyle = color;
  240. ctx.fillText(str, 0, 0);
  241. ctx.translate(ctx.measureText(str).width, 0);
  242. }
  243. [...document.querySelectorAll('[data-diagram]')].forEach(createDiagram);
  244. function createDiagram(base) {
  245. const name = base.dataset.diagram;
  246. const info = diagrams[name];
  247. if (!info) {
  248. throw new Error(`no diagram ${name}`);
  249. }
  250. info.create({elem:base});
  251. }
  252. }