utils.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  1. import {useLayoutEffect, useState} from 'react';
  2. import Fuse from 'fuse.js';
  3. import {mat3, vec2} from 'gl-matrix';
  4. import {CanvasView} from 'sentry/utils/profiling/canvasView';
  5. import {ColorChannels} from 'sentry/utils/profiling/flamegraph/flamegraphTheme';
  6. import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
  7. import {FlamegraphRenderer} from 'sentry/utils/profiling/renderers/flamegraphRenderer';
  8. import {CanvasPoolManager} from '../canvasScheduler';
  9. import {clamp, colorComponentsToRGBA} from '../colors/utils';
  10. import {FlamegraphCanvas} from '../flamegraphCanvas';
  11. import {SpanChartRenderer2D} from '../renderers/spansRenderer';
  12. import {SpanChartNode} from '../spanChart';
  13. import {Rect} from '../speedscope';
  14. export function createShader(
  15. gl: WebGLRenderingContext,
  16. type: WebGLRenderingContext['VERTEX_SHADER'] | WebGLRenderingContext['FRAGMENT_SHADER'],
  17. source: string
  18. ): WebGLShader {
  19. const shader = gl.createShader(type);
  20. if (!shader) {
  21. throw new Error('Could not create shader');
  22. }
  23. gl.shaderSource(shader, source);
  24. gl.compileShader(shader);
  25. const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  26. if (success) {
  27. return shader;
  28. }
  29. gl.deleteShader(shader);
  30. throw new Error(`Failed to compile ${type} shader`);
  31. }
  32. export function createProgram(
  33. gl: WebGLRenderingContext,
  34. vertexShader: WebGLShader,
  35. fragmentShader: WebGLShader
  36. ): WebGLProgram {
  37. const program = gl.createProgram();
  38. if (!program) {
  39. throw new Error('Could not create program');
  40. }
  41. gl.attachShader(program, vertexShader);
  42. gl.attachShader(program, fragmentShader);
  43. gl.linkProgram(program);
  44. const success = gl.getProgramParameter(program, gl.LINK_STATUS);
  45. if (success) {
  46. return program;
  47. }
  48. gl.deleteProgram(program);
  49. throw new Error('Failed to create program');
  50. }
  51. export function getUniform(
  52. gl: WebGLRenderingContext,
  53. program: WebGLProgram,
  54. name: string
  55. ): WebGLUniformLocation {
  56. const uniform = gl.getUniformLocation(program, name);
  57. if (!uniform) {
  58. throw new Error(`Could not locate uniform ${name} in shader`);
  59. }
  60. return uniform;
  61. }
  62. export function getAttribute(
  63. gl: WebGLRenderingContext,
  64. program: WebGLProgram,
  65. name: string
  66. ): number {
  67. const attribute = gl.getAttribLocation(program, name);
  68. if (attribute === -1) {
  69. throw new Error(`Could not locate attribute ${name} in shader`);
  70. }
  71. return attribute;
  72. }
  73. export function createAndBindBuffer(
  74. gl: WebGLRenderingContext,
  75. data: ArrayBufferView,
  76. usage: number
  77. ): WebGLBuffer {
  78. const buffer = gl.createBuffer();
  79. if (!buffer) {
  80. throw new Error('Could not create buffer');
  81. }
  82. gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  83. gl.bufferData(gl.ARRAY_BUFFER, data, usage);
  84. return buffer;
  85. }
  86. export function pointToAndEnableVertexAttribute(
  87. gl: WebGLRenderingContext,
  88. attribute: number,
  89. attributeInfo: {
  90. normalized: boolean;
  91. offset: number;
  92. size: number;
  93. stride: number;
  94. type: number;
  95. }
  96. ) {
  97. gl.vertexAttribPointer(
  98. attribute,
  99. attributeInfo.size,
  100. attributeInfo.type,
  101. attributeInfo.normalized,
  102. attributeInfo.stride,
  103. attributeInfo.offset
  104. );
  105. gl.enableVertexAttribArray(attribute);
  106. }
  107. // Create a projection matrix with origins at 0,0 in top left corner, scaled to width/height
  108. export function makeProjectionMatrix(width: number, height: number): mat3 {
  109. const projectionMatrix = mat3.create();
  110. mat3.translate(projectionMatrix, projectionMatrix, vec2.fromValues(-1, 1));
  111. mat3.scale(
  112. projectionMatrix,
  113. projectionMatrix,
  114. vec2.divide(vec2.create(), vec2.fromValues(2, -2), vec2.fromValues(width, height))
  115. );
  116. return projectionMatrix;
  117. }
  118. const canvasToDisplaySizeMap = new Map<HTMLCanvasElement, [number, number]>();
  119. function onResize(entries: ResizeObserverEntry[]) {
  120. for (const entry of entries) {
  121. let width;
  122. let height;
  123. let dpr = window.devicePixelRatio;
  124. if (entry.devicePixelContentBoxSize) {
  125. // NOTE: Only this path gives the correct answer
  126. // The other paths are imperfect fallbacks
  127. // for browsers that don't provide anyway to do this
  128. width = entry.devicePixelContentBoxSize[0].inlineSize;
  129. height = entry.devicePixelContentBoxSize[0].blockSize;
  130. dpr = 1; // it's already in width and height
  131. } else if (entry.contentBoxSize) {
  132. if (entry.contentBoxSize[0]) {
  133. width = entry.contentBoxSize[0].inlineSize;
  134. height = entry.contentBoxSize[0].blockSize;
  135. } else {
  136. // @ts-expect-error
  137. width = entry.contentBoxSize.inlineSize;
  138. // @ts-expect-error
  139. height = entry.contentBoxSize.blockSize;
  140. }
  141. } else {
  142. width = entry.contentRect.width;
  143. height = entry.contentRect.height;
  144. }
  145. const displayWidth = Math.round(width * dpr);
  146. const displayHeight = Math.round(height * dpr);
  147. canvasToDisplaySizeMap.set(entry.target as HTMLCanvasElement, [
  148. displayWidth,
  149. displayHeight,
  150. ]);
  151. resizeCanvasToDisplaySize(entry.target as HTMLCanvasElement);
  152. }
  153. }
  154. export const watchForResize = (
  155. canvas: HTMLCanvasElement[],
  156. callback?: (entries: ResizeObserverEntry[], observer: ResizeObserver) => void
  157. ): ResizeObserver => {
  158. const handler: ResizeObserverCallback = (entries, observer) => {
  159. onResize(entries);
  160. callback?.(entries, observer);
  161. };
  162. for (const c of canvas) {
  163. canvasToDisplaySizeMap.set(c, [c.width, c.height]);
  164. }
  165. const resizeObserver = new ResizeObserver(handler);
  166. try {
  167. // only call us of the number of device pixels changed
  168. canvas.forEach(c => {
  169. resizeObserver.observe(c, {box: 'device-pixel-content-box'});
  170. });
  171. } catch (ex) {
  172. // device-pixel-content-box is not supported so fallback to this
  173. canvas.forEach(c => {
  174. resizeObserver.observe(c, {box: 'content-box'});
  175. });
  176. }
  177. return resizeObserver;
  178. };
  179. export function resizeCanvasToDisplaySize(canvas: HTMLCanvasElement): boolean {
  180. // Get the size the browser is displaying the canvas in device pixels.
  181. const size = canvasToDisplaySizeMap.get(canvas);
  182. if (!size) {
  183. const displayWidth = canvas.clientWidth * window.devicePixelRatio;
  184. const displayHeight = canvas.clientHeight * window.devicePixelRatio;
  185. canvas.width = displayWidth;
  186. canvas.height = displayHeight;
  187. return false;
  188. }
  189. const [displayWidth, displayHeight] = size;
  190. // Check if the canvas is not the same size.
  191. const needResize = canvas.width !== displayWidth || canvas.height !== displayHeight;
  192. if (needResize) {
  193. // Make the canvas the same size
  194. canvas.width = displayWidth;
  195. canvas.height = displayHeight;
  196. }
  197. return needResize;
  198. }
  199. export function transformMatrixBetweenRect(from: Rect, to: Rect): mat3 {
  200. return mat3.fromValues(
  201. to.width / from.width,
  202. 0,
  203. 0,
  204. 0,
  205. to.height / from.height,
  206. 0,
  207. to.x - from.x * (to.width / from.width),
  208. to.y - from.y * (to.height / from.height),
  209. 1
  210. );
  211. }
  212. function getContext(canvas: HTMLCanvasElement, context: '2d'): CanvasRenderingContext2D;
  213. function getContext(canvas: HTMLCanvasElement, context: 'webgl'): WebGLRenderingContext;
  214. function getContext(canvas: HTMLCanvasElement, context: string): RenderingContext {
  215. const ctx =
  216. context === 'webgl'
  217. ? canvas.getContext(context, {antialias: false})
  218. : canvas.getContext(context);
  219. if (!ctx) {
  220. throw new Error(`Could not get context ${context}`);
  221. }
  222. return ctx;
  223. }
  224. // Exported separately as writing export function for each overload as
  225. // breaks the line width rules and makes it harder to read.
  226. export {getContext};
  227. export const ELLIPSIS = '\u2026';
  228. export function measureText(string: string, ctx?: CanvasRenderingContext2D): Rect {
  229. if (!string) {
  230. return Rect.Empty();
  231. }
  232. const context = ctx || getContext(document.createElement('canvas'), '2d');
  233. const measures = context.measureText(string);
  234. return new Rect(
  235. 0,
  236. 0,
  237. measures.width,
  238. // https://stackoverflow.com/questions/1134586/how-can-you-find-the-height-of-text-on-an-html-canvas
  239. measures.actualBoundingBoxAscent + measures.actualBoundingBoxDescent
  240. );
  241. }
  242. /**
  243. * Returns first index of value in array where value.start < target
  244. * Example: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], target = 5, returns 4 which points to value 3
  245. * @param target {number}
  246. * @param values {Array<T> | ReadonlyArray<T>}
  247. * @returns number
  248. */
  249. export function upperBound<T extends {end: number; start: number}>(
  250. target: number,
  251. values: Array<T> | ReadonlyArray<T>
  252. ): number;
  253. export function upperBound<T>(
  254. target: number,
  255. values: Array<T> | ReadonlyArray<T>,
  256. getValue: (value: T) => number
  257. ): number;
  258. export function upperBound<T extends {end: number; start: number} | {x: number}>(
  259. target: number,
  260. values: Array<T> | ReadonlyArray<T> | Record<any, any>,
  261. getValue?: (value: T) => number
  262. ) {
  263. let low = 0;
  264. let high = values.length;
  265. if (high === 0) {
  266. return 0;
  267. }
  268. if (high === 1) {
  269. return getValue
  270. ? getValue(values[0]) < target
  271. ? 1
  272. : 0
  273. : values[0].start < target
  274. ? 1
  275. : 0;
  276. }
  277. while (low !== high) {
  278. const mid = low + Math.floor((high - low) / 2);
  279. const value = getValue ? getValue(values[mid]) : values[mid].start;
  280. if (value < target) {
  281. low = mid + 1;
  282. } else {
  283. high = mid;
  284. }
  285. }
  286. return low;
  287. }
  288. /**
  289. * Returns first index of value in array where value.end < target
  290. * Example: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], target = 5, returns 3 which points to value 4
  291. * @param target {number}
  292. * @param values {Array<T> | ReadonlyArray<T>}
  293. * @returns number
  294. */
  295. export function lowerBound<T extends {end: number; start: number}>(
  296. target: number,
  297. values: Array<T> | ReadonlyArray<T>
  298. ): number;
  299. export function lowerBound<T>(
  300. target: number,
  301. values: Array<T> | ReadonlyArray<T>,
  302. getValue: (value: T) => number
  303. ): number;
  304. export function lowerBound<T extends {end: number; start: number}>(
  305. target: number,
  306. values: Array<T> | ReadonlyArray<T>,
  307. getValue?: (value: T) => number
  308. ): number {
  309. let low = 0;
  310. let high = values.length;
  311. if (high === 0) {
  312. return 0;
  313. }
  314. if (high === 1) {
  315. return getValue
  316. ? getValue(values[0]) < target
  317. ? 1
  318. : 0
  319. : values[0].end < target
  320. ? 1
  321. : 0;
  322. }
  323. while (low !== high) {
  324. const mid = low + Math.floor((high - low) / 2);
  325. const value = getValue ? getValue(values[mid]) : values[mid].end;
  326. if (value < target) {
  327. low = mid + 1;
  328. } else {
  329. high = mid;
  330. }
  331. }
  332. return low;
  333. }
  334. export function formatColorForSpan(
  335. frame: SpanChartNode,
  336. renderer: SpanChartRenderer2D
  337. ): string {
  338. const color = renderer.getColorForFrame(frame);
  339. if (Array.isArray(color)) {
  340. return colorComponentsToRGBA(color);
  341. }
  342. return '';
  343. }
  344. export function formatColorForFrame(frame: FlamegraphFrame, color: ColorChannels): string;
  345. export function formatColorForFrame(
  346. frame: FlamegraphFrame,
  347. color: FlamegraphRenderer
  348. ): string;
  349. export function formatColorForFrame(
  350. frame: FlamegraphFrame,
  351. rendererOrColor: FlamegraphRenderer | ColorChannels
  352. ): string {
  353. if (Array.isArray(rendererOrColor)) {
  354. return colorComponentsToRGBA(rendererOrColor);
  355. }
  356. const color = rendererOrColor.getColorForFrame(frame);
  357. if (color.length === 4) {
  358. return `rgba(${color
  359. .slice(0, 3)
  360. .map(n => n * 255)
  361. .join(',')}, ${color[3]})`;
  362. }
  363. return `rgba(${color.map(n => n * 255).join(',')}, 1.0)`;
  364. }
  365. export interface TrimTextCenter {
  366. end: number;
  367. length: number;
  368. start: number;
  369. text: string;
  370. }
  371. export function hexToColorChannels(color: string, alpha: number): ColorChannels {
  372. return [
  373. parseInt(color.slice(1, 3), 16) / 255,
  374. parseInt(color.slice(3, 5), 16) / 255,
  375. parseInt(color.slice(5, 7), 16) / 255,
  376. alpha,
  377. ];
  378. }
  379. // Utility function to compute a clamped view. This is essentially a bounds check
  380. // to ensure that zoomed viewports stays in the bounds and does not escape the view.
  381. export function computeClampedConfigView(
  382. newConfigView: Rect,
  383. {width, height}: {height: {max: number; min: number}; width: {max: number; min: number}}
  384. ) {
  385. if (!newConfigView.isValid()) {
  386. throw new Error(newConfigView.toString());
  387. }
  388. const clampedWidth = clamp(newConfigView.width, width.min, width.max);
  389. const clampedHeight = clamp(newConfigView.height, height.min, height.max);
  390. const maxX = width.max - clampedWidth;
  391. const maxY = Math.max(height.max - clampedHeight, 0);
  392. const clampedX = clamp(newConfigView.x, 0, maxX);
  393. const clampedY = clamp(newConfigView.y, 0, maxY);
  394. return new Rect(clampedX, clampedY, clampedWidth, clampedHeight);
  395. }
  396. /**
  397. * computeHighlightedBounds determines if a supplied boundary should be reduced in size
  398. * or shifted based on the results of a trim operation
  399. */
  400. export function computeHighlightedBounds(
  401. bounds: Fuse.RangeTuple,
  402. trim: TrimTextCenter
  403. ): Fuse.RangeTuple {
  404. if (!trim.length) {
  405. return bounds;
  406. }
  407. const isStartBetweenTrim = bounds[0] >= trim.start && bounds[0] <= trim.end;
  408. const isEndBetweenTrim = bounds[1] >= trim.start && bounds[1] <= trim.end;
  409. const isFullyTruncated = isStartBetweenTrim && isEndBetweenTrim;
  410. // example:
  411. // -[UIScrollView _smoothScrollDisplayLink:]
  412. // "smooth" in "-[UIScrollView _…ScrollDisplayLink:]"
  413. // ^^
  414. if (isFullyTruncated) {
  415. return [trim.start, trim.start + 1];
  416. }
  417. if (bounds[0] < trim.start) {
  418. // "ScrollView" in '-[UIScrollView _sm…rollDisplayLink:]'
  419. // ^--------^
  420. if (bounds[1] < trim.start) {
  421. return [bounds[0], bounds[1]];
  422. }
  423. // "smoothScroll" in -[UIScrollView _smooth…DisplayLink:]'
  424. // ^-----^
  425. if (isEndBetweenTrim) {
  426. return [bounds[0], trim.start + 1];
  427. }
  428. // "smoothScroll" in -[UIScrollView _sm…llDisplayLink:]'
  429. // ^---^
  430. if (bounds[1] > trim.end) {
  431. return [bounds[0], bounds[1] - trim.length + 1];
  432. }
  433. }
  434. // "smoothScroll" in -[UIScrollView _…scrollDisplayLink:]'
  435. // ^-----^
  436. if (isStartBetweenTrim && bounds[1] > trim.end) {
  437. return [trim.start, bounds[1] - trim.length + 1];
  438. }
  439. // "display" in -[UIScrollView _…scrollDisplayLink:]'
  440. // ^-----^
  441. if (bounds[0] > trim.end) {
  442. return [bounds[0] - trim.length + 1, bounds[1] - trim.length + 1];
  443. }
  444. throw new Error(`Unhandled case: ${JSON.stringify(bounds)} ${trim}`);
  445. }
  446. // Utility function to allow zooming into frames using a specific strategy. Supports
  447. // min zooming and exact strategy. Min zooming means we will zoom into a frame by doing
  448. // the minimal number of moves to get a frame into view - for example, if the view is large
  449. // enough and the frame we are zooming to is just outside of the viewport to the right,
  450. // we will only move the viewport to the right until the frame is in view. Exact strategy
  451. // means we will zoom into the frame by moving the viewport to the exact location of the frame
  452. // and setting the width of the view to that of the frame.
  453. export function computeConfigViewWithStrategy(
  454. strategy: 'min' | 'exact',
  455. view: Rect,
  456. frame: Rect
  457. ): Rect {
  458. if (strategy === 'exact') {
  459. return frame.withHeight(view.height);
  460. }
  461. if (strategy === 'min') {
  462. // If frame is in view, do nothing
  463. if (view.containsRect(frame)) {
  464. return view;
  465. }
  466. // If view width <= frame width, we need to zoom out, so the behavior is the
  467. // same as if we were using 'exact'
  468. if (view.width <= frame.width) {
  469. return frame.withHeight(view.height);
  470. }
  471. // If frame is to the left of the view, translate it left
  472. // to frame.x so that start of the frame is in the view
  473. let offset = view.clone();
  474. if (frame.left < view.left) {
  475. offset = offset.withX(frame.x);
  476. } else if (frame.right > view.right) {
  477. // If the right boundary of a frame is outside of the view, translate the view
  478. // by the difference between the right edge of the frame and the right edge of the view
  479. offset = view.withX(offset.x + frame.right - offset.right);
  480. }
  481. // If frame is above the view, translate view to top of frame
  482. if (frame.bottom < view.top) {
  483. offset = offset.withY(frame.top);
  484. } else if (frame.bottom > view.bottom) {
  485. // If frame is below the view, translate view by the difference
  486. // of the bottom edge of the frame and the view
  487. offset = offset.translateY(offset.y + frame.bottom - offset.bottom);
  488. }
  489. return offset;
  490. }
  491. return frame.withHeight(view.height);
  492. }
  493. export function computeMinZoomConfigViewForFrames(view: Rect, frames: Rect[]): Rect {
  494. if (!frames.length) {
  495. return view;
  496. }
  497. if (frames.length === 1) {
  498. return new Rect(frames[0].x, frames[0].y, frames[0].width, view.height);
  499. }
  500. const frame = frames.reduce(
  501. (min, f) => {
  502. return {
  503. x: Math.min(min.x, f.x),
  504. y: Math.min(min.y, f.y),
  505. right: Math.max(min.right, f.right),
  506. bottom: 0,
  507. };
  508. },
  509. {x: Number.MAX_SAFE_INTEGER, y: Number.MAX_SAFE_INTEGER, right: 0, bottom: 0}
  510. );
  511. return computeConfigViewWithStrategy(
  512. 'exact',
  513. view,
  514. new Rect(frame.x, frame.y, frame.right - frame.x, view.height)
  515. );
  516. }
  517. // Compute the X and Y position based on offset and canvas resolution
  518. export function getPhysicalSpacePositionFromOffset(offsetX: number, offsetY: number) {
  519. const logicalMousePos = vec2.fromValues(offsetX, offsetY);
  520. return vec2.scale(vec2.create(), logicalMousePos, window.devicePixelRatio);
  521. }
  522. export function getCenterScaleMatrixFromConfigPosition(scale: vec2, center: vec2) {
  523. const invertedConfigCenter = vec2.fromValues(-center[0], -center[1]);
  524. const centerScaleMatrix = mat3.create();
  525. mat3.fromTranslation(centerScaleMatrix, center);
  526. mat3.scale(centerScaleMatrix, centerScaleMatrix, scale);
  527. mat3.translate(centerScaleMatrix, centerScaleMatrix, invertedConfigCenter);
  528. return centerScaleMatrix;
  529. }
  530. // Translates the offsetX and offsetY into a config space position to find the center
  531. // and apply the scaling transformation from there
  532. export function getCenterScaleMatrixFromMousePosition(
  533. scale: number,
  534. cursor: vec2,
  535. view: CanvasView<any>,
  536. canvas: FlamegraphCanvas
  537. ): mat3 {
  538. const configSpaceMouse = view.getConfigViewCursor(cursor, canvas);
  539. const configCenter = vec2.fromValues(configSpaceMouse[0], view.configView.y);
  540. return getCenterScaleMatrixFromConfigPosition(vec2.fromValues(scale, 1), configCenter);
  541. }
  542. export function getTranslationMatrixFromConfigSpace(deltaX: number, deltaY: number) {
  543. const configDelta = vec2.fromValues(deltaX, deltaY);
  544. return mat3.fromTranslation(mat3.create(), configDelta);
  545. }
  546. // Translates the offsetX and offsetY into a config space units and return a translation
  547. // matrix that can be applied to the view
  548. export function getTranslationMatrixFromPhysicalSpace(
  549. deltaX: number,
  550. deltaY: number,
  551. view: CanvasView<any>,
  552. canvas: FlamegraphCanvas,
  553. multiplierX: number = 0.8,
  554. multiplierY: number = 1
  555. ) {
  556. const physicalDelta = vec2.fromValues(deltaX * multiplierX, deltaY * multiplierY);
  557. const physicalToConfig = mat3.invert(
  558. mat3.create(),
  559. view.fromConfigView(canvas.physicalSpace)
  560. );
  561. const [m00, m01, m02, m10, m11, m12] = physicalToConfig;
  562. const configDelta = vec2.transformMat3(vec2.create(), physicalDelta, [
  563. m00,
  564. m01,
  565. m02,
  566. m10,
  567. m11,
  568. m12,
  569. 0,
  570. 0,
  571. 0,
  572. ]);
  573. return getTranslationMatrixFromConfigSpace(configDelta[0], configDelta[1]);
  574. }
  575. export function getConfigViewTranslationBetweenVectors(
  576. offsetX: number,
  577. offsetY: number,
  578. start: vec2,
  579. view: CanvasView<any>,
  580. canvas: FlamegraphCanvas,
  581. invert?: boolean
  582. ): mat3 | null {
  583. const physicalMousePos = getPhysicalSpacePositionFromOffset(offsetX, offsetY);
  584. const physicalDelta = invert
  585. ? vec2.subtract(vec2.create(), physicalMousePos, start)
  586. : vec2.subtract(vec2.create(), start, physicalMousePos);
  587. if (physicalDelta[0] === 0 && physicalDelta[1] === 0) {
  588. return null;
  589. }
  590. const physicalToConfig = mat3.invert(
  591. mat3.create(),
  592. view.fromConfigView(canvas.physicalSpace)
  593. );
  594. const [m00, m01, m02, m10, m11, m12] = physicalToConfig;
  595. const configDelta = vec2.transformMat3(vec2.create(), physicalDelta, [
  596. m00,
  597. m01,
  598. m02,
  599. m10,
  600. m11,
  601. m12,
  602. 0,
  603. 0,
  604. 0,
  605. ]);
  606. return mat3.fromTranslation(mat3.create(), configDelta);
  607. }
  608. export function getConfigSpaceTranslationBetweenVectors(
  609. offsetX: number,
  610. offsetY: number,
  611. start: vec2,
  612. view: CanvasView<any>,
  613. canvas: FlamegraphCanvas,
  614. invert?: boolean
  615. ): mat3 | null {
  616. const physicalMousePos = getPhysicalSpacePositionFromOffset(offsetX, offsetY);
  617. const physicalDelta = invert
  618. ? vec2.subtract(vec2.create(), physicalMousePos, start)
  619. : vec2.subtract(vec2.create(), start, physicalMousePos);
  620. if (physicalDelta[0] === 0 && physicalDelta[1] === 0) {
  621. return null;
  622. }
  623. const physicalToConfig = mat3.invert(
  624. mat3.create(),
  625. view.fromConfigSpace(canvas.physicalSpace)
  626. );
  627. const [m00, m01, m02, m10, m11, m12] = physicalToConfig;
  628. const configDelta = vec2.transformMat3(vec2.create(), physicalDelta, [
  629. m00,
  630. m01,
  631. m02,
  632. m10,
  633. m11,
  634. m12,
  635. 0,
  636. 0,
  637. 0,
  638. ]);
  639. return mat3.fromTranslation(mat3.create(), configDelta);
  640. }
  641. export function getMinimapCanvasCursor(
  642. configView: Rect | undefined,
  643. configSpaceCursor: vec2 | null,
  644. borderWidth: number
  645. ) {
  646. if (!configView || !configSpaceCursor) {
  647. return 'col-resize';
  648. }
  649. const nearestEdge = Math.min(
  650. Math.abs(configView.left - configSpaceCursor[0]),
  651. Math.abs(configView.right - configSpaceCursor[0])
  652. );
  653. const isWithinBorderSize = nearestEdge <= borderWidth;
  654. if (isWithinBorderSize) {
  655. return 'ew-resize';
  656. }
  657. if (configView.contains(configSpaceCursor)) {
  658. return 'grab';
  659. }
  660. return 'col-resize';
  661. }
  662. export function useResizeCanvasObserver(
  663. canvases: (HTMLCanvasElement | null)[],
  664. canvasPoolManager: CanvasPoolManager,
  665. canvas: FlamegraphCanvas | null,
  666. view: CanvasView<any> | null
  667. ): Rect {
  668. const [bounds, setCanvasBounds] = useState<Rect>(Rect.Empty());
  669. useLayoutEffect(() => {
  670. if (!canvas || !canvases.length) {
  671. return undefined;
  672. }
  673. if (canvases.some(c => c === null)) {
  674. return undefined;
  675. }
  676. const observer = watchForResize(canvases as HTMLCanvasElement[], entries => {
  677. const contentRect =
  678. entries[0].contentRect ?? entries[0].target.getBoundingClientRect();
  679. setCanvasBounds(
  680. new Rect(contentRect.x, contentRect.y, contentRect.width, contentRect.height)
  681. );
  682. canvas.initPhysicalSpace();
  683. if (view) {
  684. view.resizeConfigSpace(canvas);
  685. }
  686. canvasPoolManager.drawSync();
  687. });
  688. return () => {
  689. observer.disconnect();
  690. };
  691. }, [canvases, canvas, view, canvasPoolManager]);
  692. return bounds;
  693. }