Skip to content

Commit

Permalink
Extract Canvas2D batch renderers to their own file
Browse files Browse the repository at this point in the history
  • Loading branch information
jlfwong committed Aug 3, 2020
1 parent 39b18df commit f1ea14c
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 72 deletions.
69 changes: 69 additions & 0 deletions src/lib/canvas-2d-batch-renderers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// This file contains a collection of classes which make it easier to perform
// batch rendering of Canvas2D primitives. The advantage of this over just doing
// ctx.beginPath() ... ctx.rect(...) ... ctx.endPath() is that you can construct
// several different batch renderers are the same time, then decide on their
// paint order at the end.
//
// See FlamechartPanZoomView.renderOverlays for an example of how this is used.

export interface TextArgs {
text: string
x: number
y: number
}

export class BatchCanvasTextRenderer {
private argsBatch: TextArgs[] = []

text(args: TextArgs) {
this.argsBatch.push(args)
}

fill(ctx: CanvasRenderingContext2D, color: string) {
if (this.argsBatch.length === 0) return
ctx.fillStyle = color
for (let args of this.argsBatch) {
ctx.fillText(args.text, args.x, args.y)
}
this.argsBatch = []
}
}

export interface RectArgs {
x: number
y: number
w: number
h: number
}

export class BatchCanvasRectRenderer {
private argsBatch: RectArgs[] = []

rect(args: RectArgs) {
this.argsBatch.push(args)
}

private drawPath(ctx: CanvasRenderingContext2D) {
ctx.beginPath()
for (let args of this.argsBatch) {
ctx.rect(args.x, args.y, args.w, args.h)
}
ctx.closePath()
this.argsBatch = []
}

fill(ctx: CanvasRenderingContext2D, color: string) {
if (this.argsBatch.length === 0) return
ctx.fillStyle = color
this.drawPath(ctx)
ctx.fill()
}

stroke(ctx: CanvasRenderingContext2D, color: string, lineWidth: number) {
if (this.argsBatch.length === 0) return
ctx.strokeStyle = color
ctx.lineWidth = lineWidth
this.drawPath(ctx)
ctx.stroke()
}
}
142 changes: 70 additions & 72 deletions src/views/flamechart-pan-zoom-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {style} from './flamechart-style'
import {h, Component} from 'preact'
import {css} from 'aphrodite'
import {ProfileSearchResults} from '../lib/profile-search'
import {BatchCanvasTextRenderer, BatchCanvasRectRenderer} from '../lib/canvas-2d-batch-renderers'

interface FlamechartFrameLabel {
configSpaceBounds: Rect
Expand Down Expand Up @@ -171,24 +172,6 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,

ctx.clearRect(0, 0, physicalViewSize.x, physicalViewSize.y)

if (this.hoveredLabel) {
let color = Colors.DARK_GRAY
if (this.props.selectedNode === this.hoveredLabel.node) {
color = Colors.DARK_BLUE
}

ctx.lineWidth = 2 * devicePixelRatio
ctx.strokeStyle = color

const physicalViewBounds = configToPhysical.transformRect(this.hoveredLabel.configSpaceBounds)
ctx.strokeRect(
Math.round(physicalViewBounds.left()),
Math.round(physicalViewBounds.top()),
Math.round(Math.max(0, physicalViewBounds.width())),
Math.round(Math.max(0, physicalViewBounds.height())),
)
}

ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${FontFamily.MONOSPACE}`
ctx.textBaseline = 'alphabetic'

Expand All @@ -199,6 +182,13 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,

const LABEL_PADDING_PX = 5 * window.devicePixelRatio

const labelBatch = new BatchCanvasTextRenderer()
const fadedLabelBatch = new BatchCanvasTextRenderer()
const matchedTextHighlightBatch = new BatchCanvasRectRenderer()
const directlySelectedOutlineBatch = new BatchCanvasRectRenderer()
const indirectlySelectedOutlineBatch = new BatchCanvasRectRenderer()
const matchedFrameBatch = new BatchCanvasRectRenderer()

const renderFrameLabelAndChildren = (frame: FlamechartFrame, depth = 0) => {
const width = frame.end - frame.start
const y = this.props.renderInverted ? this.configSpaceSize().y - 1 - depth : depth
Expand Down Expand Up @@ -251,44 +241,40 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
// actually do the highlighting.
let lastEndIndex = 0
let left = physicalLabelBounds.left() + LABEL_PADDING_PX
ctx.fillStyle = Colors.YELLOW

ctx.beginPath()
const padding = (physicalViewSpaceFrameHeight - physicalViewSpaceFontSize) / 2 - 2
for (let [startIndex, endIndex] of rangesToHighlightInTrimmedText) {
left += ctx.measureText(trimmedText.trimmedString.substring(lastEndIndex, startIndex))
.width
const highlightWidth = ctx.measureText(
left += cachedMeasureTextWidth(
ctx,
trimmedText.trimmedString.substring(lastEndIndex, startIndex),
)
const highlightWidth = cachedMeasureTextWidth(
ctx,
trimmedText.trimmedString.substring(startIndex, endIndex),
).width
ctx.rect(
left,
physicalLabelBounds.top() + padding,
highlightWidth,
physicalViewSpaceFrameHeight - 2 * padding,
)
matchedTextHighlightBatch.rect({
x: left,
y: physicalLabelBounds.top() + padding,
w: highlightWidth,
h: physicalViewSpaceFrameHeight - 2 * padding,
})

left += highlightWidth
lastEndIndex = endIndex
}
ctx.fill()
}

// Note that this is specifying the position of the starting text
// baseline.
if (this.props.searchResults != null && !match) {
ctx.fillStyle = Colors.LIGHT_GRAY
} else {
ctx.fillStyle = Colors.DARK_GRAY
}
ctx.fillText(
trimmedText.trimmedString,
physicalLabelBounds.left() + LABEL_PADDING_PX,
Math.round(
const batch = this.props.searchResults != null && !match ? fadedLabelBatch : labelBatch
batch.text({
text: trimmedText.trimmedString,

// This is specifying the position of the starting text baseline.
x: physicalLabelBounds.left() + LABEL_PADDING_PX,
y: Math.round(
physicalLabelBounds.bottom() -
(physicalViewSpaceFrameHeight - physicalViewSpaceFontSize) / 2,
),
)
})
}
}
for (let child of frame.children) {
Expand All @@ -298,7 +284,6 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,

const frameOutlineWidth = 2 * window.devicePixelRatio
ctx.strokeStyle = Colors.PALE_DARK_BLUE
ctx.lineWidth = frameOutlineWidth
const minConfigSpaceWidthToRenderOutline = (
configToPhysical.inverseTransformVector(new Vec2(1, 0)) || new Vec2(0, 0)
).x
Expand All @@ -315,56 +300,69 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
if (configSpaceBounds.top() > this.props.configSpaceViewportRect.bottom()) return

if (configSpaceBounds.hasIntersectionWith(this.props.configSpaceViewportRect)) {
let outlineColor: string | null = null

if (this.props.searchResults?.getMatchForFrame(frame.node.frame)) {
ctx.fillStyle = Colors.ORANGE

// TODO(jlfwong): This is really inefficient. Fix it!
const physicalRectBounds = configToPhysical.transformRect(configSpaceBounds)
ctx.fillRect(
Math.round(physicalRectBounds.left() + frameOutlineWidth / 2),
Math.round(physicalRectBounds.top() + frameOutlineWidth / 2),
Math.round(Math.max(0, physicalRectBounds.width() - frameOutlineWidth)),
Math.round(Math.max(0, physicalRectBounds.height() - frameOutlineWidth)),
)
matchedFrameBatch.rect({
x: Math.round(physicalRectBounds.left() + frameOutlineWidth / 2),
y: Math.round(physicalRectBounds.top() + frameOutlineWidth / 2),
w: Math.round(Math.max(0, physicalRectBounds.width() - frameOutlineWidth)),
h: Math.round(Math.max(0, physicalRectBounds.height() - frameOutlineWidth)),
})
}

if (this.props.selectedNode != null && frame.node.frame === this.props.selectedNode.frame) {
if (frame.node === this.props.selectedNode) {
outlineColor = Colors.DARK_BLUE
} else if (ctx.strokeStyle !== Colors.PALE_DARK_BLUE) {
outlineColor = Colors.PALE_DARK_BLUE
}
let batch =
frame.node === this.props.selectedNode
? directlySelectedOutlineBatch
: indirectlySelectedOutlineBatch

if (outlineColor != null) {
// TODO(jlfwong): This is really inefficient. Fix it!
const physicalRectBounds = configToPhysical.transformRect(configSpaceBounds)
ctx.strokeStyle = outlineColor
ctx.strokeRect(
Math.round(physicalRectBounds.left() + 1 + frameOutlineWidth / 2),
Math.round(physicalRectBounds.top() + 1 + frameOutlineWidth / 2),
Math.round(Math.max(0, physicalRectBounds.width() - 2 - frameOutlineWidth)),
Math.round(Math.max(0, physicalRectBounds.height() - 2 - frameOutlineWidth)),
)
}
const physicalRectBounds = configToPhysical.transformRect(configSpaceBounds)
batch.rect({
x: Math.round(physicalRectBounds.left() + 1 + frameOutlineWidth / 2),
y: Math.round(physicalRectBounds.top() + 1 + frameOutlineWidth / 2),
w: Math.round(Math.max(0, physicalRectBounds.width() - 2 - frameOutlineWidth)),
h: Math.round(Math.max(0, physicalRectBounds.height() - 2 - frameOutlineWidth)),
})
}
}
for (let child of frame.children) {
renderSpecialFrameOutlines(child, depth + 1)
}
}

ctx.beginPath()
for (let frame of this.props.flamechart.getLayers()[0] || []) {
renderSpecialFrameOutlines(frame)
}
ctx.stroke()

for (let frame of this.props.flamechart.getLayers()[0] || []) {
renderFrameLabelAndChildren(frame)
}

matchedFrameBatch.fill(ctx, Colors.ORANGE)
matchedTextHighlightBatch.fill(ctx, Colors.YELLOW)
fadedLabelBatch.fill(ctx, Colors.LIGHT_GRAY)
labelBatch.fill(ctx, Colors.BLACK)
indirectlySelectedOutlineBatch.stroke(ctx, Colors.PALE_DARK_BLUE, frameOutlineWidth)
directlySelectedOutlineBatch.stroke(ctx, Colors.DARK_BLUE, frameOutlineWidth)

if (this.hoveredLabel) {
let color = Colors.DARK_GRAY
if (this.props.selectedNode === this.hoveredLabel.node) {
color = Colors.DARK_BLUE
}

ctx.lineWidth = 2 * devicePixelRatio
ctx.strokeStyle = color

const physicalViewBounds = configToPhysical.transformRect(this.hoveredLabel.configSpaceBounds)
ctx.strokeRect(
Math.round(physicalViewBounds.left()),
Math.round(physicalViewBounds.top()),
Math.round(Math.max(0, physicalViewBounds.width())),
Math.round(Math.max(0, physicalViewBounds.height())),
)
}

this.renderTimeIndicators()
}

Expand Down

0 comments on commit f1ea14c

Please sign in to comment.