Rework vector spaces for super sharp rendering!
This commit is contained in:
251
flamechart.tsx
251
flamechart.tsx
@@ -187,13 +187,16 @@ const DEVICE_PIXEL_RATIO = window.devicePixelRatio
|
|||||||
|
|
||||||
export class FlamechartView extends Component<FlamechartViewProps, void> {
|
export class FlamechartView extends Component<FlamechartViewProps, void> {
|
||||||
renderer: ReglCommand<RectangleBatchRendererProps> | null = null
|
renderer: ReglCommand<RectangleBatchRendererProps> | null = null
|
||||||
|
|
||||||
ctx: WebGLRenderingContext | null = null
|
ctx: WebGLRenderingContext | null = null
|
||||||
canvas: HTMLCanvasElement | null = null
|
canvas: HTMLCanvasElement | null = null
|
||||||
|
|
||||||
overlayCanvas: HTMLCanvasElement | null = null
|
overlayCanvas: HTMLCanvasElement | null = null
|
||||||
overlayCtx: CanvasRenderingContext2D | null = null
|
overlayCtx: CanvasRenderingContext2D | null = null
|
||||||
|
|
||||||
worldSpaceViewportRect = new Rect()
|
configSpaceViewportRect = new Rect()
|
||||||
labels: FlamechartFrameLabel[] = []
|
labels: FlamechartFrameLabel[] = []
|
||||||
|
hoveredLabel: FlamechartFrameLabel | null = null
|
||||||
|
|
||||||
private preprocess() {
|
private preprocess() {
|
||||||
if (!this.canvas) return
|
if (!this.canvas) return
|
||||||
@@ -206,7 +209,6 @@ export class FlamechartView extends Component<FlamechartViewProps, void> {
|
|||||||
const duration = flamechart.getDuration()
|
const duration = flamechart.getDuration()
|
||||||
const maxStackHeight = layers.length
|
const maxStackHeight = layers.length
|
||||||
|
|
||||||
const configSpaceToWorldSpace = this.configSpaceToWorldSpace()
|
|
||||||
const frameColors = flamechart.getFrameColors()
|
const frameColors = flamechart.getFrameColors()
|
||||||
|
|
||||||
for (let i = 0; i < layers.length; i++) {
|
for (let i = 0; i < layers.length; i++) {
|
||||||
@@ -226,11 +228,6 @@ export class FlamechartView extends Component<FlamechartViewProps, void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.worldSpaceViewportRect = new Rect(
|
|
||||||
new Vec2(0, 0),
|
|
||||||
new Vec2(this.viewSpaceViewportWidth(), this.viewSpaceViewportHeight())
|
|
||||||
)
|
|
||||||
|
|
||||||
this.ctx = this.canvas.getContext('webgl')!
|
this.ctx = this.canvas.getContext('webgl')!
|
||||||
this.renderer = rectangleBatchRenderer(this.ctx, configSpaceRects, colors)
|
this.renderer = rectangleBatchRenderer(this.ctx, configSpaceRects, colors)
|
||||||
}
|
}
|
||||||
@@ -256,53 +253,42 @@ export class FlamechartView extends Component<FlamechartViewProps, void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private configSpaceWidth() { return this.props.flamechart.getDuration() }
|
private configSpaceSize() {
|
||||||
private configSpaceHeight() { return this.props.flamechart.getLayers().length }
|
return new Vec2(
|
||||||
private configSpaceSize() { return new Vec2(this.configSpaceWidth(), this.configSpaceHeight()) }
|
this.props.flamechart.getDuration(),
|
||||||
|
this.props.flamechart.getLayers().length
|
||||||
private WORLD_SPACE_FRAME_HEIGHT = 16
|
|
||||||
private WORLD_SPACE_LABEL_FONT_SIZE = 12
|
|
||||||
|
|
||||||
private viewSpaceViewportWidth() { return this.canvas ? this.canvas.width : 0 }
|
|
||||||
private viewSpaceViewportHeight() { return this.canvas ? this.canvas.height : 0 }
|
|
||||||
private viewSpaceViewportSize() { return new Vec2(this.viewSpaceViewportWidth(), this.viewSpaceViewportHeight()) }
|
|
||||||
|
|
||||||
private configSpaceToWorldSpace() {
|
|
||||||
return AffineTransform.withScale(new Vec2(
|
|
||||||
this.viewSpaceViewportWidth() / this.configSpaceWidth(),
|
|
||||||
this.WORLD_SPACE_FRAME_HEIGHT
|
|
||||||
))
|
|
||||||
}
|
|
||||||
private worldSpaceSize() {
|
|
||||||
return this.configSpaceToWorldSpace().transformVector(this.configSpaceSize())
|
|
||||||
}
|
|
||||||
|
|
||||||
private worldSpaceToViewSpace() {
|
|
||||||
return AffineTransform.betweenRects(
|
|
||||||
this.worldSpaceViewportRect,
|
|
||||||
new Rect(new Vec2(0, 0), this.viewSpaceViewportSize())
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private viewSpaceToNDC() {
|
private physicalViewSize() {
|
||||||
|
return new Vec2(
|
||||||
|
this.canvas ? this.canvas.width : 0,
|
||||||
|
this.canvas ? this.canvas.height : 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private LOGICAL_VIEW_SPACE_FRAME_HEIGHT = 16
|
||||||
|
private LOGICAL_VIEW_SPACE_LABEL_FONT_SIZE = 12
|
||||||
|
|
||||||
|
private configSpaceToPhysicalViewSpace() {
|
||||||
|
return AffineTransform.betweenRects(
|
||||||
|
this.configSpaceViewportRect,
|
||||||
|
new Rect(new Vec2(0, 0), this.physicalViewSize())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private physicalViewSpaceToNDC() {
|
||||||
return AffineTransform.withScale(new Vec2(1, -1)).times(
|
return AffineTransform.withScale(new Vec2(1, -1)).times(
|
||||||
AffineTransform.betweenRects(
|
AffineTransform.betweenRects(
|
||||||
new Rect(new Vec2(0, 0), this.viewSpaceViewportSize()),
|
new Rect(new Vec2(0, 0), this.physicalViewSize()),
|
||||||
new Rect(new Vec2(-1, -1), new Vec2(2, 2))
|
new Rect(new Vec2(-1, -1), new Vec2(2, 2))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private viewSpaceToOverlaySpace() {
|
private logicalToPhysicalViewSpace() {
|
||||||
return AffineTransform.withScale(new Vec2(DEVICE_PIXEL_RATIO, DEVICE_PIXEL_RATIO))
|
return AffineTransform.withScale(new Vec2(DEVICE_PIXEL_RATIO, DEVICE_PIXEL_RATIO))
|
||||||
}
|
}
|
||||||
|
|
||||||
private configSpaceToNDC() {
|
|
||||||
return this.viewSpaceToNDC()
|
|
||||||
.times(this.worldSpaceToViewSpace())
|
|
||||||
.times(this.configSpaceToWorldSpace())
|
|
||||||
}
|
|
||||||
|
|
||||||
private resizeOverlayCanvasIfNeeded() {
|
private resizeOverlayCanvasIfNeeded() {
|
||||||
if (!this.overlayCanvas) return
|
if (!this.overlayCanvas) return
|
||||||
let {width, height} = this.overlayCanvas.getBoundingClientRect()
|
let {width, height} = this.overlayCanvas.getBoundingClientRect()
|
||||||
@@ -332,55 +318,66 @@ export class FlamechartView extends Component<FlamechartViewProps, void> {
|
|||||||
if (!ctx) return
|
if (!ctx) return
|
||||||
this.resizeOverlayCanvasIfNeeded()
|
this.resizeOverlayCanvasIfNeeded()
|
||||||
|
|
||||||
const configSpaceToOverlaySpace = this.viewSpaceToOverlaySpace()
|
const configToPhysical = this.configSpaceToPhysicalViewSpace()
|
||||||
.times(this.worldSpaceToViewSpace())
|
|
||||||
.times(this.configSpaceToWorldSpace())
|
|
||||||
|
|
||||||
const overlaySpaceFontSize = this.WORLD_SPACE_LABEL_FONT_SIZE * DEVICE_PIXEL_RATIO
|
const physicalViewSpaceFontSize = this.LOGICAL_VIEW_SPACE_LABEL_FONT_SIZE * DEVICE_PIXEL_RATIO
|
||||||
const overlaySpaceFrameHeight = this.WORLD_SPACE_FRAME_HEIGHT * DEVICE_PIXEL_RATIO
|
const physicalViewSpaceFrameHeight = this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT * DEVICE_PIXEL_RATIO
|
||||||
|
|
||||||
ctx.font = `${overlaySpaceFontSize}px/${overlaySpaceFrameHeight}px Courier, monospace`
|
const minWidthToRender = cachedMeasureTextWidth(ctx, 'M' + ELLIPSIS + 'M')
|
||||||
|
const physicalViewSize = this.physicalViewSize()
|
||||||
|
const physicalViewBounds = new Rect(new Vec2(0, 0), physicalViewSize)
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, physicalViewSize.x, physicalViewSize.y)
|
||||||
|
|
||||||
|
ctx.strokeStyle = 'rgba(15, 10, 5, 0.5)'
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
|
||||||
|
if (this.hoveredLabel) {
|
||||||
|
const physicalViewBounds = configToPhysical.transformRect(this.hoveredLabel.configSpaceBounds)
|
||||||
|
ctx.strokeRect(
|
||||||
|
Math.floor(physicalViewBounds.left()), Math.floor(physicalViewBounds.top()),
|
||||||
|
Math.floor(physicalViewBounds.width()), Math.floor(physicalViewBounds.height())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px Courier, monospace`
|
||||||
ctx.fillStyle = 'rgba(15, 10, 5, 1)'
|
ctx.fillStyle = 'rgba(15, 10, 5, 1)'
|
||||||
ctx.textBaseline = 'top'
|
ctx.textBaseline = 'top'
|
||||||
|
|
||||||
const minWidthToRender = cachedMeasureTextWidth(ctx, 'M' + ELLIPSIS + 'M')
|
|
||||||
const overlaySpaceViewportRect = this.viewSpaceToOverlaySpace().transformRect(new Rect(new Vec2(0, 0), this.viewSpaceViewportSize()))
|
|
||||||
|
|
||||||
ctx.clearRect(overlaySpaceViewportRect.left(), overlaySpaceViewportRect.top(), overlaySpaceViewportRect.width(), overlaySpaceViewportRect.height())
|
|
||||||
|
|
||||||
for (let label of this.labels) {
|
for (let label of this.labels) {
|
||||||
const LABEL_PADDING_PX = 2 * DEVICE_PIXEL_RATIO
|
const LABEL_PADDING_PX = 2 * DEVICE_PIXEL_RATIO
|
||||||
let overlaySpaceBounds = configSpaceToOverlaySpace.transformRect(label.configSpaceBounds)
|
let physicalLabelBounds = configToPhysical.transformRect(label.configSpaceBounds)
|
||||||
|
|
||||||
overlaySpaceBounds = overlaySpaceBounds
|
physicalLabelBounds = physicalLabelBounds
|
||||||
.withOrigin(overlaySpaceBounds.origin.plus(new Vec2(LABEL_PADDING_PX, LABEL_PADDING_PX)))
|
.withOrigin(physicalLabelBounds.origin.plus(new Vec2(LABEL_PADDING_PX, LABEL_PADDING_PX)))
|
||||||
.withSize(overlaySpaceBounds.size.minus(new Vec2(2 * LABEL_PADDING_PX, 2 * LABEL_PADDING_PX)))
|
.withSize(physicalLabelBounds.size.minus(new Vec2(2 * LABEL_PADDING_PX, 2 * LABEL_PADDING_PX)))
|
||||||
.intersectWith(new Rect(
|
.intersectWith(new Rect(
|
||||||
new Vec2(LABEL_PADDING_PX + overlaySpaceViewportRect.origin.x, -Infinity),
|
new Vec2(LABEL_PADDING_PX, -Infinity),
|
||||||
new Vec2(overlaySpaceViewportRect.size.x, Infinity)
|
new Vec2(physicalViewSize.x, Infinity)
|
||||||
))
|
))
|
||||||
|
|
||||||
if (overlaySpaceBounds.width() < minWidthToRender) continue
|
if (physicalLabelBounds.width() < minWidthToRender) continue
|
||||||
|
|
||||||
// Cull text outside the viewport
|
// Cull text outside the viewport
|
||||||
if (overlaySpaceViewportRect.intersectWith(overlaySpaceBounds).isEmpty()) continue
|
if (physicalViewBounds.intersectWith(physicalLabelBounds).isEmpty()) continue
|
||||||
|
|
||||||
if (overlaySpaceBounds.origin.x < 0) {
|
if (physicalLabelBounds.origin.x < 0) {
|
||||||
overlaySpaceBounds = overlaySpaceBounds.withOrigin(
|
physicalLabelBounds = physicalLabelBounds.withOrigin(
|
||||||
new Vec2(0, overlaySpaceBounds.origin.y)
|
new Vec2(0, physicalLabelBounds.origin.y)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const trimmedText = trimTextMid(ctx, label.frame.name, overlaySpaceBounds.width())
|
const trimmedText = trimTextMid(ctx, label.frame.name, physicalLabelBounds.width())
|
||||||
ctx.fillText(trimmedText, overlaySpaceBounds.left(), overlaySpaceBounds.top())
|
ctx.fillText(trimmedText, physicalLabelBounds.left(), physicalLabelBounds.top())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private resizeCanvasIfNeeded() {
|
private resizeCanvasIfNeeded() {
|
||||||
if (!this.canvas || !this.ctx) return
|
if (!this.canvas || !this.ctx) return
|
||||||
let {width, height} = this.canvas.getBoundingClientRect()
|
let { width, height } = this.canvas.getBoundingClientRect()
|
||||||
width = Math.floor(width)
|
const logicalHeight = height
|
||||||
height = Math.floor(height)
|
width = Math.floor(width) * DEVICE_PIXEL_RATIO
|
||||||
|
height = Math.floor(height) * DEVICE_PIXEL_RATIO
|
||||||
|
|
||||||
// Still initializing: don't resize yet
|
// Still initializing: don't resize yet
|
||||||
if (width === 0 || height === 0) return
|
if (width === 0 || height === 0) return
|
||||||
@@ -393,14 +390,21 @@ export class FlamechartView extends Component<FlamechartViewProps, void> {
|
|||||||
this.canvas.width = width
|
this.canvas.width = width
|
||||||
this.canvas.height = height
|
this.canvas.height = height
|
||||||
|
|
||||||
// Resize the world-space viewport rectangle to match the
|
if (this.configSpaceViewportRect.isEmpty()) {
|
||||||
// window size.
|
this.configSpaceViewportRect = new Rect(
|
||||||
this.worldSpaceViewportRect = this.worldSpaceViewportRect.withSize(
|
new Vec2(0, 0),
|
||||||
this.worldSpaceViewportRect.size.timesPointwise(new Vec2(
|
new Vec2(this.configSpaceSize().x, logicalHeight / this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT)
|
||||||
width / oldWidth,
|
)
|
||||||
height / oldHeight
|
} else {
|
||||||
))
|
// Resize the viewport rectangle to match the window size aspect
|
||||||
)
|
// ratio.
|
||||||
|
this.configSpaceViewportRect = this.configSpaceViewportRect.withSize(
|
||||||
|
this.configSpaceViewportRect.size.timesPointwise(new Vec2(
|
||||||
|
width / oldWidth,
|
||||||
|
height / oldHeight
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
this.ctx.viewport(0, 0, width, height)
|
this.ctx.viewport(0, 0, width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,8 +412,11 @@ export class FlamechartView extends Component<FlamechartViewProps, void> {
|
|||||||
if (!this.renderer || !this.canvas) return
|
if (!this.renderer || !this.canvas) return
|
||||||
this.resizeCanvasIfNeeded()
|
this.resizeCanvasIfNeeded()
|
||||||
|
|
||||||
|
const configSpaceToNDC = this.physicalViewSpaceToNDC().times(this.configSpaceToPhysicalViewSpace())
|
||||||
|
|
||||||
this.renderer({
|
this.renderer({
|
||||||
configSpaceToNDC: this.configSpaceToNDC()
|
configSpaceToNDC: configSpaceToNDC,
|
||||||
|
physicalSize: this.physicalViewSize()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,41 +432,68 @@ export class FlamechartView extends Component<FlamechartViewProps, void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private pan(viewSpaceDelta: Vec2) {
|
private transformViewport(transform: AffineTransform) {
|
||||||
const worldSpaceDelta = this.worldSpaceToViewSpace().inverseTransformVector(viewSpaceDelta)
|
const viewportRect = transform.transformRect(this.configSpaceViewportRect)
|
||||||
if (!worldSpaceDelta) return
|
|
||||||
this.transformViewport(AffineTransform.withTranslation(worldSpaceDelta))
|
const configSpaceOriginBounds = new Rect(
|
||||||
|
new Vec2(0, 0),
|
||||||
|
this.configSpaceSize().minus(viewportRect.size)
|
||||||
|
)
|
||||||
|
|
||||||
|
const configSpaceSizeBounds = new Rect(
|
||||||
|
new Vec2(1, viewportRect.height()),
|
||||||
|
new Vec2(this.configSpaceSize().x, viewportRect.height())
|
||||||
|
)
|
||||||
|
|
||||||
|
this.configSpaceViewportRect = new Rect(
|
||||||
|
configSpaceOriginBounds.closestPointTo(viewportRect.origin),
|
||||||
|
configSpaceSizeBounds.closestPointTo(viewportRect.size)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private zoom(viewSpaceZoomCenter: Vec2, multiplier: number) {
|
private pan(logicalViewSpaceDelta: Vec2) {
|
||||||
const worldSpaceZoomCenter = this.worldSpaceToViewSpace().inverseTransformPosition(viewSpaceZoomCenter)
|
const physicalDelta = this.logicalToPhysicalViewSpace().transformVector(logicalViewSpaceDelta)
|
||||||
if (!worldSpaceZoomCenter) return
|
const configDelta = this.configSpaceToPhysicalViewSpace().inverseTransformVector(physicalDelta)
|
||||||
|
|
||||||
|
if (!configDelta) return
|
||||||
|
this.transformViewport(AffineTransform.withTranslation(configDelta))
|
||||||
|
}
|
||||||
|
|
||||||
|
private zoom(logicalViewSpaceCenter: Vec2, multiplier: number) {
|
||||||
|
const physicalCenter = this.logicalToPhysicalViewSpace().transformPosition(logicalViewSpaceCenter)
|
||||||
|
const configSpaceCenter = this.configSpaceToPhysicalViewSpace().inverseTransformPosition(physicalCenter)
|
||||||
|
|
||||||
|
if (!configSpaceCenter) return
|
||||||
|
|
||||||
const zoomTransform = AffineTransform
|
const zoomTransform = AffineTransform
|
||||||
.withTranslation(worldSpaceZoomCenter.times(-1))
|
.withTranslation(configSpaceCenter.times(-1))
|
||||||
.scaledBy(new Vec2(multiplier, 1))
|
.scaledBy(new Vec2(multiplier, 1))
|
||||||
.translatedBy(worldSpaceZoomCenter)
|
.translatedBy(configSpaceCenter)
|
||||||
|
|
||||||
this.transformViewport(zoomTransform)
|
this.transformViewport(zoomTransform)
|
||||||
}
|
}
|
||||||
|
|
||||||
private transformViewport(transform: AffineTransform) {
|
private onMouseMove = (ev: MouseEvent) => {
|
||||||
const viewportRect = transform.transformRect(this.worldSpaceViewportRect)
|
this.hoveredLabel = null
|
||||||
|
const logicalViewSpaceMouse = new Vec2(ev.offsetX, ev.offsetY)
|
||||||
|
const physicalViewSpaceMouse = this.logicalToPhysicalViewSpace().transformPosition(logicalViewSpaceMouse)
|
||||||
|
const configSpaceMouse = this.configSpaceToPhysicalViewSpace().inverseTransformPosition(physicalViewSpaceMouse)
|
||||||
|
|
||||||
const worldSpaceOriginBounds = new Rect(
|
if (!configSpaceMouse) return
|
||||||
new Vec2(0, 0),
|
|
||||||
this.worldSpaceSize().minus(viewportRect.size)
|
|
||||||
)
|
|
||||||
|
|
||||||
const worldSpaceSizeBounds = new Rect(
|
let labelUnderMouse: FlamechartFrameLabel | null = null
|
||||||
new Vec2(1, viewportRect.height()),
|
|
||||||
new Vec2(this.worldSpaceSize().x, viewportRect.height())
|
|
||||||
)
|
|
||||||
|
|
||||||
this.worldSpaceViewportRect = new Rect(
|
// This could be sped up significantly
|
||||||
worldSpaceOriginBounds.closestPointTo(viewportRect.origin),
|
for (let label of this.labels) {
|
||||||
worldSpaceSizeBounds.closestPointTo(viewportRect.size)
|
if (label.configSpaceBounds.contains(configSpaceMouse)) {
|
||||||
)
|
this.hoveredLabel = label
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(jlfwong): Change this to do the more correct thing:
|
||||||
|
// debounce to rendering at most once a frame.
|
||||||
|
requestAnimationFrame(() => this.renderCanvas())
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWheel = (ev: WheelEvent) => {
|
private onWheel = (ev: WheelEvent) => {
|
||||||
@@ -490,6 +524,7 @@ export class FlamechartView extends Component<FlamechartViewProps, void> {
|
|||||||
// TODO(jlfwong): Change this to do the more correct thing:
|
// TODO(jlfwong): Change this to do the more correct thing:
|
||||||
// debounce to rendering at most once a frame.
|
// debounce to rendering at most once a frame.
|
||||||
requestAnimationFrame(() => this.renderCanvas())
|
requestAnimationFrame(() => this.renderCanvas())
|
||||||
|
this.forceUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() { this.renderCanvas() }
|
componentDidUpdate() { this.renderCanvas() }
|
||||||
@@ -501,16 +536,12 @@ export class FlamechartView extends Component<FlamechartViewProps, void> {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={css(style.fill)}
|
className={css(style.fill)}
|
||||||
|
onMouseMove={this.onMouseMove}
|
||||||
onWheel={this.onWheel}>
|
onWheel={this.onWheel}>
|
||||||
<canvas
|
<canvas
|
||||||
width={1} height={1}
|
width={1} height={1}
|
||||||
ref={this.canvasRef}
|
ref={this.canvasRef}
|
||||||
className={css(style.fill)} />
|
className={css(style.fill)} />
|
||||||
{/*
|
|
||||||
We render text at a higher resolution then scale down to
|
|
||||||
ensure we're rendering at 1:1 device pixel ratio.
|
|
||||||
This ensures our text is rendered crisply.
|
|
||||||
*/}
|
|
||||||
<canvas
|
<canvas
|
||||||
width={1} height={1}
|
width={1} height={1}
|
||||||
ref={this.overlayCanvasRef}
|
ref={this.overlayCanvasRef}
|
||||||
@@ -533,6 +564,7 @@ const style = StyleSheet.create({
|
|||||||
|
|
||||||
interface RectangleBatchRendererProps {
|
interface RectangleBatchRendererProps {
|
||||||
configSpaceToNDC: AffineTransform
|
configSpaceToNDC: AffineTransform
|
||||||
|
physicalSize: Vec2
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rectangleBatchRenderer = (ctx: WebGLRenderingContext, rects: Rect[], colors: vec3[]) => {
|
export const rectangleBatchRenderer = (ctx: WebGLRenderingContext, rects: Rect[], colors: vec3[]) => {
|
||||||
@@ -561,12 +593,16 @@ export const rectangleBatchRenderer = (ctx: WebGLRenderingContext, rects: Rect[]
|
|||||||
return regl(ctx)<RectangleBatchRendererProps>({
|
return regl(ctx)<RectangleBatchRendererProps>({
|
||||||
vert: `
|
vert: `
|
||||||
uniform mat3 configSpaceToNDC;
|
uniform mat3 configSpaceToNDC;
|
||||||
|
uniform vec2 physicalSize;
|
||||||
attribute vec2 position;
|
attribute vec2 position;
|
||||||
attribute vec3 color;
|
attribute vec3 color;
|
||||||
varying vec3 vColor;
|
varying vec3 vColor;
|
||||||
void main() {
|
void main() {
|
||||||
vColor = color;
|
vColor = color;
|
||||||
gl_Position = vec4((configSpaceToNDC * vec3(position, 1)).xy, 0, 1);
|
vec2 roundedPosition = (configSpaceToNDC * vec3(position, 1)).xy;
|
||||||
|
vec2 halfSize = physicalSize / 2.0;
|
||||||
|
roundedPosition = floor(roundedPosition * halfSize) / halfSize;
|
||||||
|
gl_Position = vec4(roundedPosition, 0, 1);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|
||||||
@@ -586,6 +622,9 @@ export const rectangleBatchRenderer = (ctx: WebGLRenderingContext, rects: Rect[]
|
|||||||
uniforms: {
|
uniforms: {
|
||||||
configSpaceToNDC: (context, props) => {
|
configSpaceToNDC: (context, props) => {
|
||||||
return props.configSpaceToNDC.flatten()
|
return props.configSpaceToNDC.flatten()
|
||||||
|
},
|
||||||
|
physicalSize: (context, props) => {
|
||||||
|
return props.physicalSize.flatten()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
8
math.ts
8
math.ts
@@ -208,6 +208,14 @@ export class Rect {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
distanceFrom(p: Vec2) {
|
||||||
|
return p.minus(this.closestPointTo(p)).length()
|
||||||
|
}
|
||||||
|
|
||||||
|
contains(p: Vec2) {
|
||||||
|
return this.distanceFrom(p) === 0
|
||||||
|
}
|
||||||
|
|
||||||
intersectWith(other: Rect) {
|
intersectWith(other: Rect) {
|
||||||
const topLeft = Vec2.max(this.topLeft(), other.topLeft())
|
const topLeft = Vec2.max(this.topLeft(), other.topLeft())
|
||||||
const bottomRight = Vec2.max(topLeft, Vec2.min(this.bottomRight(), other.bottomRight()))
|
const bottomRight = Vec2.max(topLeft, Vec2.min(this.bottomRight(), other.bottomRight()))
|
||||||
|
|||||||
Reference in New Issue
Block a user