Every heap ref carries its heap tag, generation checks always on, rooted async values. Splash isolates get a domain table and curated minimal preludes; system modules under `mod` expand lazily on first touch. No wall-clock script caps: isolation and force-stop instead. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
525 lines
20 KiB
Text
525 lines
20 KiB
Text
// Shared glyph/path coverage; consumers supply textures, metrics and styling.
|
|
use mod.pod.*
|
|
use mod.math.*
|
|
use mod.shader.*
|
|
use mod.glyph_coverage
|
|
|
|
// Texture-slot adapters live in the consumer; texel addressing and packed
|
|
// index decoding are shared by glyph scans and path distance queries.
|
|
mod.glyph_coverage.CurveTexels = {
|
|
fetch_curve_texel: fn(texel_idx: float) -> vec4 {
|
|
let tex_size = self.curve_tex_size()
|
|
let row = floor(texel_idx / tex_size.x)
|
|
let col = texel_idx - row * tex_size.x
|
|
let uv = vec2(
|
|
(col + 0.5) / tex_size.x,
|
|
(row + 0.5) / tex_size.y
|
|
)
|
|
return self.sample_curve_tex(uv)
|
|
}
|
|
|
|
fetch_band_texel: fn(texel_idx: float) -> vec4 {
|
|
let tex_size = self.band_tex_size()
|
|
let row = floor(texel_idx / tex_size.x)
|
|
let col = texel_idx - row * tex_size.x
|
|
let uv = vec2(
|
|
(col + 0.5) / tex_size.x,
|
|
(row + 0.5) / tex_size.y
|
|
)
|
|
return self.sample_band_tex(uv)
|
|
}
|
|
|
|
pick_channel: fn(v: vec4, channel: float) -> float {
|
|
if channel < 0.5 {
|
|
return v.x
|
|
}
|
|
if channel < 1.5 {
|
|
return v.y
|
|
}
|
|
if channel < 2.5 {
|
|
return v.z
|
|
}
|
|
return v.w
|
|
}
|
|
}
|
|
|
|
mod.glyph_coverage.SlugCoverage = {
|
|
..mod.glyph_coverage.CurveTexels
|
|
|
|
// ---- slug curve coverage (the outline's quadratic curves, scanned per pixel). The shader
|
|
// declares `curve_texture` and `band_texture`, and the accessors `slug_curve_offset()`,
|
|
// `slug_curve_count()`, `slug_band_offset()`, `slug_band_count()`, `slug_fill_flags()` (where in
|
|
// the curve texture its glyph is), `aa_pad_px` and `slug_viewport_px`.
|
|
|
|
saturate: fn(v: float) -> float {
|
|
return clamp(v, 0.0, 1.0)
|
|
}
|
|
|
|
slug_dilate: fn(pos: vec2, tex: vec2, jac: vec4, normal: vec2) -> vec4 {
|
|
let n = normalize(normal)
|
|
// The camera and list transform the GPU renders with: baked in when
|
|
// the text was drawn they were the previous paint's (identity for a
|
|
// new pass), and the first draw came out different from later ones.
|
|
let m = self.draw_pass.camera_projection * (self.draw_pass.camera_view * self.draw_list.view_transform)
|
|
let hp = m * vec4(pos.x, pos.y, 0.0, 1.0)
|
|
let hn = m * vec4(n.x, n.y, 0.0, 0.0)
|
|
let s = hp.w
|
|
let t = hn.w
|
|
|
|
let u = (s * hn.x - t * hp.x) * self.slug_viewport_px.x
|
|
let v = (s * hn.y - t * hp.y) * self.slug_viewport_px.y
|
|
|
|
let s2 = s * s
|
|
let st = s * t
|
|
let uv = u * u + v * v
|
|
let d = normal * (
|
|
s2 * (st + sqrt(uv)) / max(uv - st * st, 0.0000001)
|
|
) * self.aa_pad_px
|
|
|
|
let vpos = pos + d
|
|
let vtex = vec2(tex.x + dot(d, jac.xy), tex.y + dot(d, jac.zw))
|
|
return vec4(vtex.x, vtex.y, vpos.x, vpos.y)
|
|
}
|
|
|
|
curve_tex_size: fn() -> vec2 { return self.curve_texture.size() }
|
|
band_tex_size: fn() -> vec2 { return self.band_texture.size() }
|
|
sample_curve_tex: fn(uv: vec2) -> vec4 { return self.curve_texture.sample_nearest(uv) }
|
|
sample_band_tex: fn(uv: vec2) -> vec4 { return self.band_texture.sample_nearest(uv) }
|
|
|
|
calc_root_code: fn(y1: float, y2: float, y3: float) -> u32 {
|
|
let i1 = asuint(y1) >> u32(31)
|
|
let i2 = asuint(y2) >> u32(30)
|
|
let i3 = asuint(y3) >> u32(29)
|
|
|
|
let shift = (i1 & u32(1)) | (i2 & u32(2)) | (i3 & u32(4))
|
|
return (u32(11892) >> shift) & u32(257)
|
|
}
|
|
|
|
solve_horiz_poly: fn(p12: vec4, p3: vec2) -> vec2 {
|
|
let a = p12.xy - p12.zw * 2.0 + p3
|
|
let b = p12.xy - p12.zw
|
|
let ra = 1.0 / a.y
|
|
let rb = 0.5 / b.y
|
|
|
|
let d = sqrt(max(b.y * b.y - a.y * p12.y, 0.0))
|
|
let mut t1 = (b.y - d) * ra
|
|
let mut t2 = (b.y + d) * ra
|
|
if abs(a.y) < 1.0 / 65536.0 {
|
|
t1 = p12.y * rb
|
|
t2 = t1
|
|
}
|
|
return vec2(
|
|
(a.x * t1 - b.x * 2.0) * t1 + p12.x,
|
|
(a.x * t2 - b.x * 2.0) * t2 + p12.x
|
|
)
|
|
}
|
|
|
|
solve_vert_poly: fn(p12: vec4, p3: vec2) -> vec2 {
|
|
let a = p12.xy - p12.zw * 2.0 + p3
|
|
let b = p12.xy - p12.zw
|
|
let ra = 1.0 / a.x
|
|
let rb = 0.5 / b.x
|
|
|
|
let d = sqrt(max(b.x * b.x - a.x * p12.x, 0.0))
|
|
let mut t1 = (b.x - d) * ra
|
|
let mut t2 = (b.x + d) * ra
|
|
if abs(a.x) < 1.0 / 65536.0 {
|
|
t1 = p12.x * rb
|
|
t2 = t1
|
|
}
|
|
return vec2(
|
|
(a.y * t1 - b.y * 2.0) * t1 + p12.y,
|
|
(a.y * t2 - b.y * 2.0) * t2 + p12.y
|
|
)
|
|
}
|
|
|
|
scan_horizontal_list: fn(list_offset: float, list_count: float, sample: vec2, px_size: float) -> vec2 {
|
|
let limit = floor(list_count + 0.5)
|
|
var coverage = 0.0
|
|
var weight = 0.0
|
|
|
|
var j = 0.0
|
|
loop {
|
|
if j >= limit { break }
|
|
|
|
let packed_idx = floor(j * 0.25)
|
|
let channel = j - packed_idx * 4.0
|
|
let idx_data = self.fetch_band_texel(list_offset + packed_idx)
|
|
let curve_idx = self.pick_channel(idx_data, channel)
|
|
|
|
let p12 = self.fetch_curve_texel(curve_idx * 2.0) - vec4(sample.x, sample.y, sample.x, sample.y)
|
|
let p3 = self.fetch_curve_texel(curve_idx * 2.0 + 1.0).xy - sample
|
|
if max(max(p12.x, p12.z), p3.x) / px_size < -0.5 { break }
|
|
|
|
let code = self.calc_root_code(p12.y, p12.w, p3.y)
|
|
if code != u32(0) {
|
|
let r = self.solve_horiz_poly(p12, p3) / px_size
|
|
if (code & u32(1)) != u32(0) {
|
|
coverage = coverage + self.saturate(r.x + 0.5)
|
|
weight = max(weight, self.saturate(1.0 - abs(r.x) * 2.0))
|
|
}
|
|
if code > u32(1) {
|
|
coverage = coverage - self.saturate(r.y + 0.5)
|
|
weight = max(weight, self.saturate(1.0 - abs(r.y) * 2.0))
|
|
}
|
|
}
|
|
|
|
j = j + 1.0
|
|
}
|
|
|
|
return vec2(coverage, weight)
|
|
}
|
|
|
|
scan_vertical_list: fn(list_offset: float, list_count: float, sample: vec2, px_size: float) -> vec2 {
|
|
let limit = floor(list_count + 0.5)
|
|
var coverage = 0.0
|
|
var weight = 0.0
|
|
|
|
var j = 0.0
|
|
loop {
|
|
if j >= limit { break }
|
|
|
|
let packed_idx = floor(j * 0.25)
|
|
let channel = j - packed_idx * 4.0
|
|
let idx_data = self.fetch_band_texel(list_offset + packed_idx)
|
|
let curve_idx = self.pick_channel(idx_data, channel)
|
|
|
|
let p12 = self.fetch_curve_texel(curve_idx * 2.0) - vec4(sample.x, sample.y, sample.x, sample.y)
|
|
let p3 = self.fetch_curve_texel(curve_idx * 2.0 + 1.0).xy - sample
|
|
if max(max(p12.y, p12.w), p3.y) / px_size < -0.5 { break }
|
|
|
|
let code = self.calc_root_code(p12.x, p12.z, p3.x)
|
|
if code != u32(0) {
|
|
let r = self.solve_vert_poly(p12, p3) / px_size
|
|
if (code & u32(1)) != u32(0) {
|
|
coverage = coverage - self.saturate(r.x + 0.5)
|
|
weight = max(weight, self.saturate(1.0 - abs(r.x) * 2.0))
|
|
}
|
|
if code > u32(1) {
|
|
coverage = coverage + self.saturate(r.y + 0.5)
|
|
weight = max(weight, self.saturate(1.0 - abs(r.y) * 2.0))
|
|
}
|
|
}
|
|
|
|
j = j + 1.0
|
|
}
|
|
|
|
return vec2(coverage, weight)
|
|
}
|
|
|
|
scan_horizontal_all: fn(sample: vec2, px_size: float) -> vec2 {
|
|
let limit = floor(self.slug_curve_count() + 0.5)
|
|
var coverage = 0.0
|
|
var weight = 0.0
|
|
|
|
var i = 0.0
|
|
loop {
|
|
if i >= limit { break }
|
|
|
|
let curve_idx = self.slug_curve_offset() + i
|
|
let p12 = self.fetch_curve_texel(curve_idx * 2.0) - vec4(sample.x, sample.y, sample.x, sample.y)
|
|
let p3 = self.fetch_curve_texel(curve_idx * 2.0 + 1.0).xy - sample
|
|
let code = self.calc_root_code(p12.y, p12.w, p3.y)
|
|
if code != u32(0) {
|
|
let r = self.solve_horiz_poly(p12, p3) / px_size
|
|
if (code & u32(1)) != u32(0) {
|
|
coverage = coverage + self.saturate(r.x + 0.5)
|
|
weight = max(weight, self.saturate(1.0 - abs(r.x) * 2.0))
|
|
}
|
|
if code > u32(1) {
|
|
coverage = coverage - self.saturate(r.y + 0.5)
|
|
weight = max(weight, self.saturate(1.0 - abs(r.y) * 2.0))
|
|
}
|
|
}
|
|
|
|
i = i + 1.0
|
|
}
|
|
|
|
return vec2(coverage, weight)
|
|
}
|
|
|
|
scan_vertical_all: fn(sample: vec2, px_size: float) -> vec2 {
|
|
let limit = floor(self.slug_curve_count() + 0.5)
|
|
var coverage = 0.0
|
|
var weight = 0.0
|
|
|
|
var i = 0.0
|
|
loop {
|
|
if i >= limit { break }
|
|
|
|
let curve_idx = self.slug_curve_offset() + i
|
|
let p12 = self.fetch_curve_texel(curve_idx * 2.0) - vec4(sample.x, sample.y, sample.x, sample.y)
|
|
let p3 = self.fetch_curve_texel(curve_idx * 2.0 + 1.0).xy - sample
|
|
let code = self.calc_root_code(p12.x, p12.z, p3.x)
|
|
if code != u32(0) {
|
|
let r = self.solve_vert_poly(p12, p3) / px_size
|
|
if (code & u32(1)) != u32(0) {
|
|
coverage = coverage - self.saturate(r.x + 0.5)
|
|
weight = max(weight, self.saturate(1.0 - abs(r.x) * 2.0))
|
|
}
|
|
if code > u32(1) {
|
|
coverage = coverage + self.saturate(r.y + 0.5)
|
|
weight = max(weight, self.saturate(1.0 - abs(r.y) * 2.0))
|
|
}
|
|
}
|
|
|
|
i = i + 1.0
|
|
}
|
|
|
|
return vec2(coverage, weight)
|
|
}
|
|
|
|
calc_coverage: fn(xcov: float, ycov: float, xwgt: float, ywgt: float) -> float {
|
|
let coverage = max(
|
|
abs(xcov * xwgt + ycov * ywgt) / max(xwgt + ywgt, 1.0 / 65536.0),
|
|
min(abs(xcov), abs(ycov))
|
|
)
|
|
if self.slug_fill_flags() >= 4096.0 {
|
|
return 1.0 - abs(1.0 - fract(coverage * 0.5) * 2.0)
|
|
}
|
|
return self.saturate(coverage)
|
|
}
|
|
|
|
// Text takes the full-curve scan: the band-accelerated path has shown correctness issues in
|
|
// some GPU shader compilers, and text glyphs carry no bands. One walk over the curves serves
|
|
// both rays (each curve's two texels are fetched once, not once per direction): the same sums
|
|
// as scan_horizontal_all + scan_vertical_all, at half the texture reads.
|
|
alpha_at_scan: fn(sample: vec2, px_x: float, px_y: float) -> float {
|
|
let limit = floor(self.slug_curve_count() + 0.5)
|
|
let first = self.slug_curve_offset() * 2.0
|
|
// a curve's two texels sit side by side in one row (the row width is even)
|
|
let tex_size = self.curve_tex_size()
|
|
let texel = vec2(1.0 / tex_size.x, 1.0 / tex_size.y)
|
|
let inv_px = vec2(1.0 / px_x, 1.0 / px_y)
|
|
var xcov = 0.0
|
|
var xwgt = 0.0
|
|
var ycov = 0.0
|
|
var ywgt = 0.0
|
|
|
|
var i = 0.0
|
|
loop {
|
|
if i >= limit { break }
|
|
|
|
let at = first + i * 2.0
|
|
let row = floor(at * texel.x)
|
|
let uv = vec2((at - row * tex_size.x + 0.5) * texel.x, (row + 0.5) * texel.y)
|
|
let p12 = self.sample_curve_tex(uv) - vec4(sample.x, sample.y, sample.x, sample.y)
|
|
let p3 = self.sample_curve_tex(vec2(uv.x + texel.x, uv.y)).xy - sample
|
|
|
|
let hcode = self.calc_root_code(p12.y, p12.w, p3.y)
|
|
if hcode != u32(0) {
|
|
let r = self.solve_horiz_poly(p12, p3) * inv_px.x
|
|
if (hcode & u32(1)) != u32(0) {
|
|
xcov = xcov + self.saturate(r.x + 0.5)
|
|
xwgt = max(xwgt, self.saturate(1.0 - abs(r.x) * 2.0))
|
|
}
|
|
if hcode > u32(1) {
|
|
xcov = xcov - self.saturate(r.y + 0.5)
|
|
xwgt = max(xwgt, self.saturate(1.0 - abs(r.y) * 2.0))
|
|
}
|
|
}
|
|
|
|
let vcode = self.calc_root_code(p12.x, p12.z, p3.x)
|
|
if vcode != u32(0) {
|
|
let r = self.solve_vert_poly(p12, p3) * inv_px.y
|
|
if (vcode & u32(1)) != u32(0) {
|
|
ycov = ycov - self.saturate(r.x + 0.5)
|
|
ywgt = max(ywgt, self.saturate(1.0 - abs(r.x) * 2.0))
|
|
}
|
|
if vcode > u32(1) {
|
|
ycov = ycov + self.saturate(r.y + 0.5)
|
|
ywgt = max(ywgt, self.saturate(1.0 - abs(r.y) * 2.0))
|
|
}
|
|
}
|
|
|
|
i = i + 1.0
|
|
}
|
|
|
|
return self.calc_coverage(xcov, ycov, xwgt, ywgt)
|
|
}
|
|
|
|
// A glyph with bands scans only its band, one without scans every curve.
|
|
alpha_at_banded: fn(sample: vec2, px_x: float, px_y: float) -> float {
|
|
var coverage_x = 0.0
|
|
var coverage_y = 0.0
|
|
var weight_x = 0.0
|
|
var weight_y = 0.0
|
|
|
|
if self.slug_band_count() > 0.5 {
|
|
let num_bands = max(floor(self.slug_band_count() + 0.5), 1.0)
|
|
let h_band_idx = clamp(floor(sample.y * num_bands), 0.0, num_bands - 1.0)
|
|
let v_band_idx = clamp(floor(sample.x * num_bands), 0.0, num_bands - 1.0)
|
|
|
|
let h_band_info = self.fetch_band_texel(self.slug_band_offset() + h_band_idx)
|
|
let h_band = self.scan_horizontal_list(
|
|
floor(h_band_info.x + 0.5),
|
|
h_band_info.y,
|
|
sample,
|
|
px_x,
|
|
)
|
|
coverage_x = h_band.x
|
|
weight_x = h_band.y
|
|
|
|
let v_band_info = self.fetch_band_texel(self.slug_band_offset() + num_bands + v_band_idx)
|
|
let v_band = self.scan_vertical_list(
|
|
floor(v_band_info.x + 0.5),
|
|
v_band_info.y,
|
|
sample,
|
|
px_y,
|
|
)
|
|
coverage_y = v_band.x
|
|
weight_y = v_band.y
|
|
} else {
|
|
let x_scan = self.scan_horizontal_all(sample, px_x)
|
|
coverage_x = x_scan.x
|
|
weight_x = x_scan.y
|
|
|
|
let y_scan = self.scan_vertical_all(sample, px_y)
|
|
coverage_y = y_scan.x
|
|
weight_y = y_scan.y
|
|
}
|
|
|
|
return self.calc_coverage(coverage_x, coverage_y, weight_x, weight_y)
|
|
}
|
|
}
|
|
|
|
mod.glyph_coverage.FeatherCover = {
|
|
// Reciprocal-square-root approximation of the error function.
|
|
erf7: fn(x: float) -> float {
|
|
let s = x * 1.1283791671
|
|
let ss = s * s
|
|
let t = s + (0.24295 + (0.03395 + 0.0104 * ss) * ss) * (s * ss)
|
|
return t / sqrt(1.0 + t * t)
|
|
}
|
|
|
|
// sd is positive inside, in device pixels; sigma is Gaussian sigma in
|
|
// device pixels. Zero sigma gives the ordinary one-pixel AA ramp.
|
|
feather_cover: fn(sd: float, sigma: float, side: float) -> float {
|
|
if sigma <= 0.01 { return clamp(0.5 + sd, 0.0, 1.0) }
|
|
// Saturating the far tail avoids overflow in erf7 for distant/empty
|
|
// bands; at six erf units the Gaussian tail is already negligible.
|
|
let g = 0.5 + 0.5 * self.erf7(clamp(sd * (sqrt(0.5) / sigma), -6.0, 6.0))
|
|
// Both: Gaussian integral, 50% coverage on the path.
|
|
// Inner: nothing outside, zero on the path, rising inward.
|
|
// Outer: full coverage inside/on the path, fading outward.
|
|
if side < 0.0 { return max(2.0 * g - 1.0, 0.0) }
|
|
if side > 0.0 { return min(2.0 * g, 1.0) }
|
|
return g
|
|
}
|
|
}
|
|
|
|
mod.glyph_coverage.Feather = {
|
|
..mod.glyph_coverage.FeatherCover,
|
|
..mod.glyph_coverage.CurveTexels
|
|
|
|
curve_tex_size: fn() -> vec2 { return self.feather_curve_texture.size() }
|
|
band_tex_size: fn() -> vec2 { return self.feather_band_texture.size() }
|
|
sample_curve_tex: fn(uv: vec2) -> vec4 { return self.feather_curve_texture.sample_nearest(uv) }
|
|
sample_band_tex: fn(uv: vec2) -> vec4 { return self.feather_band_texture.sample_nearest(uv) }
|
|
|
|
// Three shape-header texels (see draw/vector/feather.rs):
|
|
// 0 = curve offset/count, band offset/count;
|
|
// 1 = expanded hull/band bbox min.xy/max.xy, in path coordinates;
|
|
// 2 = sigma, side, nonzero flag, stroke half width (-1 means fill).
|
|
// The horizontal band's list contains every segment within the feather
|
|
// reach, including horizontal segments. Walk it once for distance and
|
|
// +x-ray winding. Return (signed distance in path units, winding).
|
|
// Each line's second curve texel is (p2.xy, winding_only, 0): .z = 1
|
|
// means both sides have the same fill state, so keep its winding but
|
|
// skip its distance. Boundaries and all stroke segments have .z = 0.
|
|
band_signed_distance: fn(sample: vec2, header: float) -> vec2 {
|
|
let shape = self.fetch_band_texel(header)
|
|
let bounds = self.fetch_band_texel(header + 1.0)
|
|
let style = self.fetch_band_texel(header + 2.0)
|
|
let count = max(floor(shape.w + 0.5), 1.0)
|
|
let band = clamp(floor((sample.y - bounds.y) / max(bounds.w - bounds.y, 0.000001) * count), 0.0, count - 1.0)
|
|
let list = self.fetch_band_texel(shape.z + band)
|
|
var dist_squared = 1e30
|
|
var winding = 0.0
|
|
var j = 0.0
|
|
loop {
|
|
if j >= list.y { break }
|
|
let packed = floor(j * 0.25)
|
|
let indices = self.fetch_band_texel(list.x + packed)
|
|
let curve = self.pick_channel(indices, j - packed * 4.0)
|
|
let p0 = self.fetch_curve_texel(curve * 2.0).xy
|
|
let endpoint = self.fetch_curve_texel(curve * 2.0 + 1.0)
|
|
let p2 = endpoint.xy
|
|
let delta = p2 - p0
|
|
if endpoint.z < 0.5 {
|
|
let relative = sample - p0
|
|
let t = clamp(dot(relative, delta) / max(dot(delta, delta), 0.000000000001), 0.0, 1.0)
|
|
let nearest = relative - delta * t
|
|
dist_squared = min(dist_squared, dot(nearest, nearest))
|
|
}
|
|
// Half-open crossings count a shared endpoint once. Horizontal
|
|
// boundaries contribute distance, but never winding.
|
|
if p0.y <= sample.y && p2.y > sample.y {
|
|
if p0.x + (sample.y - p0.y) * delta.x / delta.y > sample.x {
|
|
winding = winding + 1.0
|
|
}
|
|
} else if p2.y <= sample.y && p0.y > sample.y {
|
|
if p0.x + (sample.y - p0.y) * delta.x / delta.y > sample.x {
|
|
winding = winding - 1.0
|
|
}
|
|
}
|
|
j = j + 1.0
|
|
}
|
|
let dist = sqrt(dist_squared)
|
|
if style.w >= 0.0 { return vec2(style.w - dist, winding) }
|
|
var inside = abs(winding) > 0.5
|
|
if style.z < 0.5 { inside = modf(abs(winding), 2.0) > 0.5 }
|
|
if inside { return vec2(dist, winding) }
|
|
return vec2(-dist, winding)
|
|
}
|
|
}
|
|
|
|
mod.glyph_coverage.AtlasCoverage = {
|
|
// ---- distance-field coverage from the glyph atlas. The shader declares `grayscale_texture` and
|
|
// `msdf_texture`, `atlas_plane` (which channel holds the field), `cutoff`, `radius`,
|
|
// `sdf_sharpness` and `sdf_luma_bias`.
|
|
|
|
sdf: fn(scale, p, color) {
|
|
let sampled = self.grayscale_texture.sample(p)
|
|
let s = if self.atlas_plane < 0.5 {
|
|
sampled.r
|
|
} else if self.atlas_plane < 1.5 {
|
|
sampled.g
|
|
} else if self.atlas_plane < 2.5 {
|
|
sampled.b
|
|
} else {
|
|
sampled.a
|
|
}
|
|
let safe_scale = max(scale, 0.0001)
|
|
let luma = dot(color.rgb, vec3(0.299, 0.587, 0.114))
|
|
var a = clamp(
|
|
(s - (1.0 - self.cutoff)) * self.radius / safe_scale * self.sdf_sharpness + 0.5,
|
|
0.0,
|
|
1.0,
|
|
)
|
|
let bias = (0.5 - luma) * self.sdf_luma_bias
|
|
// Light text lifts coverage by a constant: not where there is none, or
|
|
// every glyph cell shows as a faint quad over the scene.
|
|
if a > self.sdf_luma_bias * 0.5 {
|
|
a = clamp(a - bias, 0.0, 1.0)
|
|
}
|
|
return a
|
|
}
|
|
|
|
msdf: fn(scale, p, color) {
|
|
let s = self.msdf_texture.sample(p)
|
|
let dist = s.a
|
|
let safe_scale = max(scale, 0.0001)
|
|
let luma = dot(color.rgb, vec3(0.299, 0.587, 0.114))
|
|
var a = clamp(
|
|
(dist - (1.0 - self.cutoff)) * self.radius / safe_scale * self.sdf_sharpness + 0.5,
|
|
0.0,
|
|
1.0,
|
|
)
|
|
let bias = (0.5 - luma) * self.sdf_luma_bias
|
|
if a > self.sdf_luma_bias * 0.5 {
|
|
a = clamp(a - bias, 0.0, 1.0)
|
|
}
|
|
return a
|
|
}
|
|
}
|