regl-worldview
Advanced tools
Comparing version 0.5.0 to 0.6.0
{ | ||
"name": "regl-worldview", | ||
"version": "0.5.0", | ||
"version": "0.6.0", | ||
"description": "A reusable component for rendering 2D and 3D views using regl", | ||
@@ -5,0 +5,0 @@ "license": "Apache-2.0", |
@@ -81,2 +81,10 @@ // @flow | ||
}, | ||
isPerspective(context, props) { | ||
return this.cameraState.perspective; | ||
}, | ||
fovy(context, props) { | ||
return this.cameraState.fovy; | ||
}, | ||
}, | ||
@@ -83,0 +91,0 @@ |
@@ -5,7 +5,7 @@ // @flow | ||
import memoizeOne from "memoize-one"; | ||
import React, { useState, useContext } from "react"; | ||
import React, { useState } from "react"; | ||
import type { Color } from "../types"; | ||
import { defaultBlend, defaultDepth } from "../utils/commandUtils"; | ||
import WorldviewReactContext from "../WorldviewReactContext"; | ||
import { defaultBlend, defaultDepth, toColor } from "../utils/commandUtils"; | ||
import { createInstancedGetChildrenForHitmap } from "../utils/getChildrenForHitmapDefaults"; | ||
import Command, { type CommonCommandProps } from "./Command"; | ||
@@ -29,3 +29,2 @@ import { isColorDark, type TextMarker } from "./Text"; | ||
// ============================ | ||
// - Add hitmap support. | ||
// - Allow customization of font style, maybe highlight ranges. | ||
@@ -138,2 +137,6 @@ // - Consider a solid rectangular background instead of an outline. This is challenging because the | ||
uniform float scaleInvariantSize; | ||
uniform float viewportHeight; | ||
uniform float viewportWidth; | ||
uniform bool isPerspective; | ||
uniform float cameraFovY; | ||
@@ -187,14 +190,40 @@ // per-vertex attributes | ||
void main () { | ||
// Scale invariance only works for billboards | ||
bool scaleInvariantEnabled = scaleInvariant && billboard == 1.0; | ||
vec2 srcSize = vec2(srcWidth, fontSize); | ||
vec3 markerSpacePos = scale * vec3((destOffset + position * srcSize + alignmentOffset) / fontSize, 0); | ||
gl_Position = computeVertexPosition(markerSpacePos); | ||
vec3 markerSpacePos = vec3((destOffset + position * srcSize + alignmentOffset) / fontSize, 0); | ||
if (scaleInvariant && billboard == 1.0) { | ||
// Scale invariance only works for billboards | ||
float w = gl_Position.w; | ||
w *= scaleInvariantSize; | ||
markerSpacePos *= w; | ||
gl_Position = computeVertexPosition(markerSpacePos); | ||
if (!scaleInvariantEnabled) { | ||
// Apply marker scale only when scale invariance is disabled | ||
markerSpacePos *= scale; | ||
} else { | ||
// If scale invariance is enabled, the text will be rendered at a constant | ||
// scale regardless of the zoom level. | ||
// The given scaleInvariantSize is in pixels. We need to scale it based on | ||
// the current canvas resolution to get the proper dimensions later in NDC | ||
float scaleInvariantFactor = scaleInvariantSize / viewportHeight; | ||
if (isPerspective) { | ||
// When using a perspective projection, the effect is achieved by using | ||
// the w-component for scaling, which is obtained by first projecting | ||
// the marker position into clip space. | ||
gl_Position = computeVertexPosition(markerSpacePos); | ||
scaleInvariantFactor *= gl_Position.w; | ||
// We also need to take into account the camera's half vertical FOV | ||
scaleInvariantFactor *= cameraFovY; | ||
} else { | ||
// Compute inverse aspect ratio | ||
float invAspect = viewportHeight / viewportWidth; | ||
// When using orthographic projection, the scaling factor is obtain from | ||
// the camera projection itself. | ||
// We also need applied the inverse aspect ratio | ||
scaleInvariantFactor *= 2.0 * invAspect / length(projection[0].xyz); | ||
} | ||
// Apply scale invariant factor | ||
markerSpacePos *= scaleInvariantFactor; | ||
} | ||
// Compute final vertex position | ||
gl_Position = computeVertexPosition(markerSpacePos); | ||
vTexCoord = (srcOffset + texCoord * srcSize) / atlasSize; | ||
@@ -218,2 +247,3 @@ vEnableBackground = enableBackground; | ||
uniform float scaleInvariantSize; | ||
uniform bool isHitmap; | ||
@@ -236,12 +266,17 @@ varying vec2 vTexCoord; | ||
if (scaleInvariant && vBillboard == 1.0 && scaleInvariantSize < 0.03) { | ||
// If scale invariant is enabled and scaleInvariantSize is "too small", do not interpolate | ||
// the raw distance value since at such small scale, the SDF approach causes some | ||
if (scaleInvariant && vBillboard == 1.0 && scaleInvariantSize <= 20.0) { | ||
// If scale invariant is enabled and scaleInvariantSize is "too small", do not interpolate | ||
// the raw distance value since at such small scale, the SDF approach causes some | ||
// visual artifacts. | ||
// The value used for checking if scaleInvariantSize is "too small" is arbitrary and | ||
// was defined after some experimentation. | ||
// The value used for checking if scaleInvariantSize is "too small" is arbitrary and | ||
// was defined after some experimentation. | ||
edgeStep = dist; | ||
} | ||
if (vEnableHighlight > 0.5) { | ||
if (isHitmap) { | ||
// When rendering for the hitmap buffer, we draw flat polygons using the foreground color | ||
// instead of the actual glyphs. This way we increase the selection range and provide a | ||
// better user experience. | ||
gl_FragColor = vForegroundColor; | ||
} else if (vEnableHighlight > 0.5) { | ||
gl_FragColor = mix(vHighlightColor, vec4(0, 0, 0, 1), edgeStep); | ||
@@ -269,38 +304,45 @@ } else if (vEnableBackground > 0.5) { | ||
const atlasTexture = regl.texture(); | ||
const drawText = regl({ | ||
depth: defaultDepth, | ||
blend: defaultBlend, | ||
primitive: "triangle strip", | ||
vert, | ||
frag, | ||
uniforms: { | ||
atlas: atlasTexture, | ||
atlasSize: () => [atlasTexture.width, atlasTexture.height], | ||
fontSize: command.resolution, | ||
cutoff: CUTOFF, | ||
scaleInvariant: command.scaleInvariant, | ||
scaleInvariantSize: command.scaleInvariantSize, | ||
}, | ||
instances: regl.prop("instances"), | ||
count: 4, | ||
attributes: { | ||
position: [[0, 0], [0, -1], [1, 0], [1, -1]], | ||
texCoord: [[0, 0], [0, 1], [1, 0], [1, 1]], // flipped | ||
srcOffset: (ctx, props) => ({ buffer: props.srcOffsets, divisor: 1 }), | ||
destOffset: (ctx, props) => ({ buffer: props.destOffsets, divisor: 1 }), | ||
srcWidth: (ctx, props) => ({ buffer: props.srcWidths, divisor: 1 }), | ||
scale: (ctx, props) => ({ buffer: props.scale, divisor: 1 }), | ||
alignmentOffset: (ctx, props) => ({ buffer: props.alignmentOffset, divisor: 1 }), | ||
billboard: (ctx, props) => ({ buffer: props.billboard, divisor: 1 }), | ||
foregroundColor: (ctx, props) => ({ buffer: props.foregroundColor, divisor: 1 }), | ||
backgroundColor: (ctx, props) => ({ buffer: props.backgroundColor, divisor: 1 }), | ||
highlightColor: (ctx, props) => ({ buffer: props.highlightColor, divisor: 1 }), | ||
enableBackground: (ctx, props) => ({ buffer: props.enableBackground, divisor: 1 }), | ||
enableHighlight: (ctx, props) => ({ buffer: props.enableHighlight, divisor: 1 }), | ||
posePosition: (ctx, props) => ({ buffer: props.posePosition, divisor: 1 }), | ||
poseOrientation: (ctx, props) => ({ buffer: props.poseOrientation, divisor: 1 }), | ||
}, | ||
}); | ||
const makeDrawText = (isHitmap: boolean) => { | ||
return regl({ | ||
depth: defaultDepth, | ||
blend: defaultBlend, | ||
primitive: "triangle strip", | ||
vert, | ||
frag, | ||
uniforms: { | ||
atlas: atlasTexture, | ||
atlasSize: () => [atlasTexture.width, atlasTexture.height], | ||
fontSize: command.resolution, | ||
cutoff: CUTOFF, | ||
scaleInvariant: command.scaleInvariant, | ||
scaleInvariantSize: command.scaleInvariantSize, | ||
isHitmap: !!isHitmap, | ||
viewportHeight: regl.context("viewportHeight"), | ||
viewportWidth: regl.context("viewportWidth"), | ||
isPerspective: regl.context("isPerspective"), | ||
cameraFovY: regl.context("fovy"), | ||
}, | ||
instances: regl.prop("instances"), | ||
count: 4, | ||
attributes: { | ||
position: [[0, 0], [0, -1], [1, 0], [1, -1]], | ||
texCoord: [[0, 0], [0, 1], [1, 0], [1, 1]], // flipped | ||
srcOffset: (ctx, props) => ({ buffer: props.srcOffsets, divisor: 1 }), | ||
destOffset: (ctx, props) => ({ buffer: props.destOffsets, divisor: 1 }), | ||
srcWidth: (ctx, props) => ({ buffer: props.srcWidths, divisor: 1 }), | ||
scale: (ctx, props) => ({ buffer: props.scale, divisor: 1 }), | ||
alignmentOffset: (ctx, props) => ({ buffer: props.alignmentOffset, divisor: 1 }), | ||
billboard: (ctx, props) => ({ buffer: props.billboard, divisor: 1 }), | ||
foregroundColor: (ctx, props) => ({ buffer: props.foregroundColor, divisor: 1 }), | ||
backgroundColor: (ctx, props) => ({ buffer: props.backgroundColor, divisor: 1 }), | ||
highlightColor: (ctx, props) => ({ buffer: props.highlightColor, divisor: 1 }), | ||
enableBackground: (ctx, props) => ({ buffer: props.enableBackground, divisor: 1 }), | ||
enableHighlight: (ctx, props) => ({ buffer: props.enableHighlight, divisor: 1 }), | ||
posePosition: (ctx, props) => ({ buffer: props.posePosition, divisor: 1 }), | ||
poseOrientation: (ctx, props) => ({ buffer: props.poseOrientation, divisor: 1 }), | ||
}, | ||
}); | ||
}; | ||
return (props: $ReadOnlyArray<TextMarkerProps>) => { | ||
return (props: $ReadOnlyArray<TextMarkerProps>, isHitmap: boolean) => { | ||
let estimatedInstances = 0; | ||
@@ -364,6 +406,12 @@ const prevNumChars = charSet.size; | ||
const fgColor = marker.colors?.[0] || marker.color || BG_COLOR_LIGHT; | ||
// If we need to render text for hitmap framebuffer, we only render the polygons using | ||
// the foreground color (which needs to be converted to RGBA since it's a vec4). | ||
// See comment on fragment shader above | ||
const fgColor = toColor( | ||
isHitmap ? marker.color || [0, 0, 0, 1] : marker.colors?.[0] || marker.color || BG_COLOR_LIGHT | ||
); | ||
const outline = marker.colors?.[1] != null || command.autoBackgroundColor; | ||
const bgColor = | ||
marker.colors?.[1] || (command.autoBackgroundColor && isColorDark(fgColor) ? BG_COLOR_LIGHT : BG_COLOR_DARK); | ||
const bgColor = toColor( | ||
marker.colors?.[1] || (command.autoBackgroundColor && isColorDark(fgColor) ? BG_COLOR_LIGHT : BG_COLOR_DARK) | ||
); | ||
const hlColor = marker?.highlightColor || { r: 1, b: 0, g: 1, a: 1 }; | ||
@@ -385,3 +433,6 @@ | ||
srcOffsets[2 * index + 0] = info.x + BUFFER; | ||
srcOffsets[2 * index + 1] = info.y + BUFFER; | ||
// In order to make sure there's enough room for glyphs' descenders (i.e. 'g'), | ||
// we need to apply an extra offset based on the font resolution. | ||
// The value used to compute the offset is a result of experimentation. | ||
srcOffsets[2 * index + 1] = info.y + BUFFER + 0.05 * command.resolution; | ||
srcWidths[index] = info.width; | ||
@@ -441,3 +492,3 @@ | ||
drawText({ | ||
makeDrawText(isHitmap)({ | ||
instances: totalInstances, | ||
@@ -469,5 +520,2 @@ | ||
export default function GLText(props: Props) { | ||
const context = useContext(WorldviewReactContext); | ||
const { dimension } = context; | ||
const [command] = useState(() => makeTextCommand(props.alphabet)); | ||
@@ -477,14 +525,8 @@ // HACK: Worldview doesn't provide an easy way to pass a command-level prop into the regl commands, | ||
command.autoBackgroundColor = props.autoBackgroundColor; | ||
command.resolution = Math.max(MIN_RESOLUTION, props.resolution || DEFAULT_RESOLUTION); | ||
command.scaleInvariant = props.scaleInvariantFontSize != null; | ||
// Compute the actual size for the text object in NDC coordinates (from -1 to 1) | ||
// In order to make sure the text is always shown at the same size regardless of | ||
// the canvas dimensions (as Text does), we need to scale it based on both the desired | ||
// font size and the current worldview resolution. Otherwise, the text will be | ||
// displayed at different sizes depending on the worlview canvas dimension. | ||
command.scaleInvariantSize = (props.scaleInvariantFontSize ?? 0) / dimension.height; | ||
command.scaleInvariantSize = props.scaleInvariantFontSize ?? 0; | ||
const getChildrenForHitmap = createInstancedGetChildrenForHitmap(1); | ||
return <Command reglCommand={command} {...props} />; | ||
return <Command getChildrenForHitmap={getChildrenForHitmap} reglCommand={command} {...props} />; | ||
} |
@@ -11,4 +11,4 @@ // @flow | ||
import type { Point, CameraCommand, Dimensions, Color, Pose, Scale } from "../types"; | ||
import { getCSSColor } from "../utils/commandUtils"; | ||
import type { Point, CameraCommand, Dimensions, Color, Pose, Scale, Vec4 } from "../types"; | ||
import { getCSSColor, toColor } from "../utils/commandUtils"; | ||
import { type WorldviewContextType } from "../WorldviewContext"; | ||
@@ -27,4 +27,4 @@ import WorldviewReactContext from "../WorldviewReactContext"; | ||
scale: Scale, | ||
color?: Color, | ||
colors?: Color[], | ||
color?: Color | Vec4, | ||
colors?: (Color | Vec4)[], | ||
text: string, | ||
@@ -95,5 +95,6 @@ }; | ||
const hasBgColor = colors.length >= 2; | ||
const textColor = hasBgColor ? colors[0] : color; | ||
const textColor = toColor(hasBgColor ? colors[0] : color || [0, 0, 0, 1]); | ||
if (textColor) { | ||
const backgroundColor = toColor(colors[1]); | ||
if (!isColorEqual(this._prevTextColor, textColor)) { | ||
@@ -118,6 +119,6 @@ this._prevTextColor = textColor; | ||
this._inner.style.background = hexBgColor; | ||
} else if (hasBgColor && this._prevBgColor && !isColorEqual(colors[1], this._prevBgColor)) { | ||
} else if (hasBgColor && this._prevBgColor && !isColorEqual(backgroundColor, this._prevBgColor)) { | ||
// update background color with colors[1] data | ||
this._prevBgColor = colors[1]; | ||
this._inner.style.background = getCSSColor(colors[1]); | ||
this._prevBgColor = backgroundColor; | ||
this._inner.style.background = getCSSColor(backgroundColor); | ||
} | ||
@@ -124,0 +125,0 @@ } |
@@ -14,2 +14,3 @@ // @flow | ||
import Container from "./Container"; | ||
import { rng } from "./util"; | ||
@@ -22,9 +23,21 @@ import { GLText } from ".."; | ||
background = true, | ||
}: { | ||
randomScale = false, | ||
}: {| | ||
text: string, | ||
billboard?: ?boolean, | ||
background?: ?boolean, | ||
}) { | ||
randomScale?: ?boolean, | ||
|}) { | ||
const radius = 10; | ||
const count = 10; | ||
const scale = (i: number) => { | ||
if (!randomScale) { | ||
return { x: 1, y: 1, z: 1 }; | ||
} | ||
return { | ||
x: 0.5 + 2.0 * rng(), | ||
y: 0.5 + 2.0 * rng(), | ||
z: 0.5 + 2.0 * rng(), | ||
}; | ||
}; | ||
return new Array(count).fill().map((_, i) => { | ||
@@ -39,3 +52,3 @@ const angle = (2 * Math.PI * i) / count; | ||
}, | ||
scale: { x: 1, y: 1, z: 1 }, | ||
scale: scale(i), | ||
color, | ||
@@ -95,2 +108,26 @@ colors: background && i % 4 === 0 ? [color, { r: 1, g: 1, b: 0, a: 1 }] : undefined, | ||
}) | ||
.add("glyph ascent and descent", () => { | ||
const markers = textMarkers({ text: "BDFGHJKLPQTY\nbdfghjklpqty", billboard: true }); | ||
const target = markers[9].pose.position; | ||
return ( | ||
<Container | ||
cameraState={{ | ||
target: [target.x, target.y + 2, target.z], | ||
perspective: true, | ||
distance: 8, | ||
}}> | ||
<GLText>{markers}</GLText> | ||
<Axes /> | ||
</Container> | ||
); | ||
}) | ||
.add("random marker scale", () => { | ||
const markers = textMarkers({ text: "Hello\nWorldview", billboard: true, randomScale: true }); | ||
return ( | ||
<Container cameraState={{ perspective: true, distance: 25 }}> | ||
<GLText resolution={40}>{markers}</GLText> | ||
<Axes /> | ||
</Container> | ||
); | ||
}) | ||
.add("scaleInvariant", () => { | ||
@@ -117,3 +154,3 @@ const markers = textMarkers({ text: "Hello\nWorldview", billboard: true }); | ||
return ( | ||
<Container cameraState={{ perspective: true, distance: 25 }}> | ||
<Container cameraState={{ perspective: true, distance: 25 }} backgroundColor={[0.2, 0.2, 0.4, 1]}> | ||
<GLText scaleInvariantFontSize={40}>{markers}</GLText> | ||
@@ -124,2 +161,11 @@ <Axes /> | ||
}) | ||
.add("scaleInvariant - perspective: false", () => { | ||
const markers = textMarkers({ text: "Hello\nWorldview", billboard: true }); | ||
return ( | ||
<Container cameraState={{ perspective: false, distance: 25 }} backgroundColor={[0.4, 0.2, 0.2, 1]}> | ||
<GLText scaleInvariantFontSize={40}>{markers}</GLText> | ||
<Axes /> | ||
</Container> | ||
); | ||
}) | ||
.add("scaleInvariant resize", () => { | ||
@@ -147,2 +193,11 @@ function Example() { | ||
}) | ||
.add("scaleInvariant - ignore scale", () => { | ||
const markers = textMarkers({ text: "Hello\nWorldview", billboard: true, randomScale: true }); | ||
return ( | ||
<Container cameraState={{ perspective: true, distance: 25 }}> | ||
<GLText scaleInvariantFontSize={30}>{markers}</GLText> | ||
<Axes /> | ||
</Container> | ||
); | ||
}) | ||
.add("with alphabet", () => { | ||
@@ -149,0 +204,0 @@ const markers = textMarkers({ text: "Hello\nWorldview", billboard: true }); |
@@ -57,2 +57,4 @@ // @flow | ||
export const toColor = (val: Color | Vec4): Color => (Array.isArray(val) ? vec4ToRGBA(val) : val); | ||
export function getCSSColor(color: Color = DEFAULT_TEXT_COLOR) { | ||
@@ -59,0 +61,0 @@ const { r, g, b, a } = color; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 3 instances in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 3 instances in 1 package
2896213
84
18952
4