use std::sync::Arc;
use valo_dl::{
BlendMode, ColorFilter, DisplayList, FocalCircle, Image, Paint, Sampling, Shader, SpreadMode,
TileMode, MAX_GRADIENT_STOPS,
};
use valo_geometry::{Color, FillRule, Matrix, Point, Rect};
use crate::frame::Step;
use crate::glyphs::{GlyphStore, PageRef};
use crate::host_buffer::{HostBuffer, VertexSlot};
use crate::images::ImageStore;
use crate::pipelines::{
advanced_mode_id, blend_filter_id, blur_style_id, Frag, PipelineCache, PipelineKey,
PipelineKind, TextMode,
};
use crate::ramps::RampCache;
use crate::raster::{ListRasterCache, RasterVerdict};
use super::layers::PassFrame;
const PAYLOAD_RECT: usize = 0;
pub(super) const PAYLOAD_GEOM: usize = 1;
pub(super) const PAYLOAD_MISC: usize = 2;
const PAYLOAD_OFFSETS: usize = 3; const PAYLOAD_RADII: usize = 3; const PAYLOAD_DECAL: usize = 3; const PAYLOAD_COLORS: usize = 5; const PAYLOAD_LOCAL: usize = 13; const PAYLOAD_CONICAL: usize = 15; const PAYLOAD_CONICAL_FLAGS: usize = 16; const PAYLOAD_COLOR_MATRIX: usize = 17;
pub(super) struct StepEmitter<'a> {
host: &'a mut HostBuffer,
device: &'a wgpu::Device,
queue: &'a wgpu::Queue,
pipelines: &'a PipelineCache,
images: &'a mut ImageStore,
ramps: &'a mut RampCache,
sampler: wgpu::Sampler,
format: wgpu::TextureFormat,
}
pub(super) struct UniformRecord {
bytes: [u8; crate::host_buffer::UNIFORM_SIZE as usize],
}
impl UniformRecord {
fn new(mvp: [f32; 16], color: [f32; 4]) -> Self {
let mut bytes = [0u8; crate::host_buffer::UNIFORM_SIZE as usize];
bytes[0..64].copy_from_slice(bytemuck::cast_slice(&mvp));
bytes[64..80].copy_from_slice(bytemuck::cast_slice(&color));
Self { bytes }
}
pub(super) fn set_payload(&mut self, index: usize, v: [f32; 4]) {
let start = 80 + index * 16;
self.bytes[start..start + 16].copy_from_slice(bytemuck::cast_slice(&v));
}
fn set_local_rect(&mut self, r: &Rect) {
self.set_payload(PAYLOAD_RECT, [r.x, r.y, r.width, r.height]);
}
}
impl<'a> StepEmitter<'a> {
#[expect(
clippy::too_many_arguments,
reason = "one-shot wiring of the GPU-facing services"
)]
pub fn new(
host: &'a mut HostBuffer,
device: &'a wgpu::Device,
queue: &'a wgpu::Queue,
pipelines: &'a PipelineCache,
images: &'a mut ImageStore,
ramps: &'a mut RampCache,
sampler: wgpu::Sampler,
format: wgpu::TextureFormat,
) -> Self {
Self {
host,
device,
queue,
pipelines,
images,
ramps,
sampler,
format,
}
}
#[expect(
clippy::too_many_arguments,
reason = "the draw's full resolved inputs, passed explicitly by design"
)]
pub fn paint_quad(
&mut self,
frame: &mut PassFrame,
group_alpha: f32,
kind: PipelineKind,
quad: &Rect,
paint: &Paint,
current: &Matrix,
z: f32,
) {
let model = current.then(&rect_to_unit(quad));
let tint = tinted(paint, group_alpha);
let mut record = UniformRecord::new(ortho(frame, &model, z), tint);
record.set_local_rect(quad);
let bind = self.shader_payload(&mut record, paint);
let kind = promote_opaque(kind, paint, group_alpha);
self.push_step(frame, kind, paint.blend_mode, record, bind, None, z);
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors paint_quad plus the blend inputs"
)]
pub fn blend_solid_quad(
&mut self,
frame: &mut PassFrame,
group_alpha: f32,
kind: PipelineKind,
quad: &Rect,
paint: &Paint,
current: &Matrix,
z: f32,
mode: BlendMode,
snapshot: &wgpu::TextureView,
) {
let model = current.then(&rect_to_unit(quad));
let tint = scaled_premul(paint.color, group_alpha);
let mut record = UniformRecord::new(ortho(frame, &model, z), tint);
record.set_local_rect(quad);
self.set_blend_misc(frame, &mut record, mode);
let bind = self.texture_bind(snapshot);
self.push_step(frame, kind, BlendMode::SrcOver, record, Some(bind), None, z);
}
pub fn strip_step(
&mut self,
frame: &mut PassFrame,
tint: [f32; 4],
paint: &Paint,
current: &Matrix,
mesh: (VertexSlot, u32),
z: f32,
) {
let mut record = UniformRecord::new(ortho(frame, current, z), tint);
let bind = self.shader_payload(&mut record, paint);
self.push_step(
frame,
PipelineKind::Strip(paint_frag(paint)),
paint.blend_mode,
record,
bind,
Some(mesh),
z,
);
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors the DrawImage op's fields 1:1"
)]
pub fn image_step(
&mut self,
frame: &mut PassFrame,
group_alpha: f32,
image: &Image,
src: &Rect,
dst: &Rect,
sampling: Sampling,
paint: &Paint,
current: &Matrix,
z: f32,
) {
let model = current.then(&rect_to_unit(dst));
let tint = alpha_tint(paint.color.a * group_alpha);
let mut record = UniformRecord::new(ortho(frame, &model, z), tint);
record.set_local_rect(dst);
record.set_payload(PAYLOAD_GEOM, uv_mapping(image, src, dst));
record.set_payload(PAYLOAD_DECAL, decal_flags(sampling));
let fragment = match paint.color_filter {
None => Frag::Image,
Some(filter) => match encode_color_filter(&mut record, filter) {
EncodedColorFilter::Matrix => Frag::ImageMatrix,
EncodedColorFilter::Blend => Frag::ImageBlend,
},
};
let bind = self
.images
.bind_group(self.pipelines.texture_bind_layout(), image, sampling);
self.push_step(
frame,
PipelineKind::Draw(fragment),
paint.blend_mode,
record,
Some(bind),
None,
z,
);
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors the RRectBlur op's fields 1:1"
)]
pub fn rrect_blur_step(
&mut self,
frame: &mut PassFrame,
group_alpha: f32,
rect: &Rect,
radii: [f32; 4],
paint: &Paint,
current: &Matrix,
z: f32,
) {
let mask = paint.mask_blur.expect("recorded with mask_blur");
let quad = rect.expand(paint.mask_padding());
let model = current.then(&rect_to_unit(&quad));
let tint = scaled_premul(paint.color, group_alpha);
let mut record = UniformRecord::new(ortho(frame, &model, z), tint);
record.set_local_rect(&quad);
record.set_payload(
PAYLOAD_GEOM,
[rect.x, rect.y, rect.x + rect.width, rect.y + rect.height],
);
record.set_payload(
PAYLOAD_MISC,
[
mask.sigma.max(0.05),
blur_style_id(mask.style) as f32,
0.0,
0.0,
],
);
record.set_payload(PAYLOAD_RADII, radii);
self.push_step(
frame,
PipelineKind::Draw(Frag::RRectBlur),
paint.blend_mode,
record,
None,
None,
z,
);
}
#[expect(
clippy::too_many_arguments,
reason = "one batch's full resolved inputs, passed explicitly by design"
)]
pub fn text_step(
&mut self,
frame: &mut PassFrame,
mode: TextMode,
tint: [f32; 4],
blend: BlendMode,
model: &Matrix,
mesh: (VertexSlot, u32),
page: wgpu::BindGroup,
z: f32,
) {
let record = UniformRecord::new(ortho(frame, model, z), tint);
self.push_step(
frame,
PipelineKind::Text { mode },
blend,
record,
Some(page),
Some(mesh),
z,
);
}
pub fn atlas_bind(&self, glyphs: &mut GlyphStore, page: PageRef) -> wgpu::BindGroup {
glyphs.bind_group(self.pipelines.texture_bind_layout(), page)
}
pub fn raster_verdict(
&self,
rasters: &mut ListRasterCache,
list: &Arc<DisplayList>,
needed_scale: f32,
) -> RasterVerdict {
rasters.resolve(
self.device,
self.format,
list,
needed_scale,
self.device.limits().max_texture_dimension_2d,
)
}
pub fn raster_quad_step(
&mut self,
frame: &mut PassFrame,
dest: &Rect,
extent: [f32; 2],
view: &wgpu::TextureView,
z: f32,
) {
let mut record = self.quad_record(frame, dest, [1.0, 1.0, 1.0, 1.0], z);
let sample = Rect::new(dest.x, dest.y, extent[0], extent[1]);
record.set_payload(PAYLOAD_GEOM, full_rect_uv(&sample));
let bind = self.texture_bind(view);
self.push_step(
frame,
PipelineKind::Draw(Frag::Image),
BlendMode::SrcOver,
record,
Some(bind),
None,
z,
);
}
pub fn clip_cover_step(
&mut self,
frame: &mut PassFrame,
bounds: &Rect,
current: &Matrix,
z: f32,
) {
let model = current.then(&rect_to_unit(bounds));
let record = UniformRecord::new(ortho(frame, &model, z), [0.0; 4]);
self.push_step(
frame,
PipelineKind::ClipCover { difference: true },
BlendMode::SrcOver,
record,
None,
None,
z,
);
}
pub fn clip_ceiling_step(&mut self, frame: &mut PassFrame, z: f32) {
let viewport = Rect::new(0.0, 0.0, frame.size[0] as f32, frame.size[1] as f32);
let record =
UniformRecord::new(ortho_mvp(&rect_to_unit(&viewport), frame.size, z), [0.0; 4]);
self.push_step(
frame,
PipelineKind::ClipCover { difference: false },
BlendMode::SrcOver,
record,
None,
None,
z,
);
}
pub fn push_fan(
&mut self,
frame: &mut PassFrame,
rule: FillRule,
current: &Matrix,
mesh: (VertexSlot, u32),
z: f32,
) {
let record = UniformRecord::new(ortho(frame, current, 0.0), [0.0; 4]);
self.push_step(
frame,
fan_kind(rule),
BlendMode::SrcOver,
record,
None,
Some(mesh),
z,
);
}
pub fn alloc_mesh(&mut self, vertices: &[f32]) -> (VertexSlot, u32) {
let slot = self.host.alloc_vertices(bytemuck::cast_slice(vertices));
(slot, (vertices.len() / 2) as u32)
}
pub fn alloc_text_mesh(&mut self, vertices: &[f32]) -> (VertexSlot, u32) {
let slot = self.host.alloc_vertices(bytemuck::cast_slice(vertices));
(slot, (vertices.len() / 4) as u32)
}
pub fn quad_record(
&self,
frame: &PassFrame,
rect: &Rect,
tint: [f32; 4],
z: f32,
) -> UniformRecord {
let mut record = UniformRecord::new(ortho(frame, &rect_to_unit(rect), z), tint);
record.set_local_rect(rect);
record
}
pub fn texture_bind(&self, view: &wgpu::TextureView) -> wgpu::BindGroup {
self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("valo.step.texture"),
layout: self.pipelines.texture_bind_layout(),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
})
}
pub fn blend_bind(&self, dst: &wgpu::TextureView, src: &wgpu::TextureView) -> wgpu::BindGroup {
self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("valo.step.blend"),
layout: self.pipelines.blend_bind_layout(),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(dst),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(src),
},
],
})
}
pub fn set_blend_misc(&self, frame: &PassFrame, record: &mut UniformRecord, mode: BlendMode) {
record.set_payload(
PAYLOAD_MISC,
[
advanced_mode_id(mode) as f32,
0.0,
frame.size[0] as f32,
frame.size[1] as f32,
],
);
}
pub fn filter_step(
&mut self,
target_format: wgpu::TextureFormat,
frag: Frag,
record: UniformRecord,
bind: wgpu::BindGroup,
) -> Step {
let uniforms = self.host.alloc_uniform(&record.bytes);
let key = PipelineKey::new(
target_format,
BlendMode::SrcOver,
PipelineKind::Filter(frag),
);
Step {
key,
uniforms,
texture: Some(bind),
mesh: None,
sort_z: 0.0,
}
}
pub fn filtered_image_entry(&mut self, source: &Image, filter: ColorFilter) -> (Image, bool) {
self.images.filtered_image(source, filter)
}
fn shader_payload(
&mut self,
record: &mut UniformRecord,
paint: &Paint,
) -> Option<wgpu::BindGroup> {
match paint.shader.as_ref()? {
Shader::Image {
image,
sampling,
local,
} => {
fill_pattern_payload(record, image, *sampling, local);
Some(
self.images
.bind_group(self.pipelines.texture_bind_layout(), image, *sampling),
)
}
shader => {
let ramp = (shader.stops().len() > MAX_GRADIENT_STOPS).then(|| {
let (view, texels) = self.ramps.ensure(self.device, self.queue, shader.stops());
(self.texture_bind(&view), texels)
});
fill_gradient_payload(record, shader, ramp.as_ref().map(|(_, n)| *n));
ramp.map(|(bind, _)| bind)
}
}
}
#[expect(
clippy::too_many_arguments,
reason = "the one funnel every draw goes through"
)]
pub fn push_step(
&mut self,
frame: &mut PassFrame,
kind: PipelineKind,
blend: BlendMode,
record: UniformRecord,
texture: Option<wgpu::BindGroup>,
mesh: Option<(VertexSlot, u32)>,
z: f32,
) {
let uniforms = self.host.alloc_uniform(&record.bytes);
let key = PipelineKey::new(self.format, blend, kind);
frame.steps.push(Step {
key,
uniforms,
texture,
mesh,
sort_z: z,
});
}
}
fn promote_opaque(kind: PipelineKind, paint: &Paint, group_alpha: f32) -> PipelineKind {
if group_alpha < 1.0 || !is_opaque_paint(paint) {
return kind;
}
match kind {
PipelineKind::Draw(f) => PipelineKind::OpaqueDraw(f),
PipelineKind::Cover(f) => PipelineKind::OpaqueCover(f),
other => other,
}
}
fn ortho(frame: &PassFrame, m: &Matrix, z: f32) -> [f32; 16] {
let o = frame.origin;
let shifted = Matrix::translation(-o.x, -o.y).then(m);
ortho_mvp(&shifted, frame.size, z)
}
fn rect_to_unit(r: &Rect) -> Matrix {
Matrix::from_affine(r.width, 0.0, 0.0, r.height, r.x, r.y)
}
pub(super) fn tinted(paint: &Paint, extra: f32) -> [f32; 4] {
if paint.shader.is_none() {
scaled_premul(paint.color, extra)
} else {
alpha_tint(paint.color.a * extra)
}
}
pub(super) fn scaled_premul(color: Color, alpha: f32) -> [f32; 4] {
let [r, g, b, a] = color.premultiplied();
[r * alpha, g * alpha, b * alpha, a * alpha]
}
pub(super) fn alpha_tint(a: f32) -> [f32; 4] {
[a, a, a, a]
}
pub(super) fn full_rect_uv(rect: &Rect) -> [f32; 4] {
let sx = 1.0 / rect.width;
let sy = 1.0 / rect.height;
[sx, sy, -rect.x * sx, -rect.y * sy]
}
pub(super) fn paint_frag(paint: &Paint) -> Frag {
let ramp = paint
.shader
.as_ref()
.is_some_and(|s| s.stops().len() > MAX_GRADIENT_STOPS);
match &paint.shader {
None => Frag::Solid,
Some(Shader::Linear { .. }) if ramp => Frag::LinearRamp,
Some(Shader::Radial { .. }) if ramp => Frag::RadialRamp,
Some(Shader::Sweep { .. }) if ramp => Frag::SweepRamp,
Some(Shader::Linear { .. }) => Frag::Linear,
Some(Shader::Radial { .. }) => Frag::Radial,
Some(Shader::Sweep { .. }) => Frag::Sweep,
Some(Shader::Image { .. }) => Frag::Pattern,
}
}
fn fan_kind(rule: FillRule) -> PipelineKind {
PipelineKind::StencilFan {
even_odd: rule == FillRule::EvenOdd,
}
}
fn is_opaque_paint(paint: &Paint) -> bool {
let solid_blend = matches!(paint.blend_mode, BlendMode::SrcOver | BlendMode::Src);
solid_blend
&& paint.mask_blur.is_none()
&& paint.effective_image_filter().is_none()
&& paint.color.a >= 1.0
&& paint.shader.as_ref().is_none_or(shader_opaque)
}
fn shader_opaque(shader: &Shader) -> bool {
if let Shader::Radial {
center,
focus: Some(circle),
..
} = shader
{
if circle.radius > 0.0 || circle.center != *center {
return false;
}
}
let stops = match shader {
Shader::Linear { stops, .. }
| Shader::Radial { stops, .. }
| Shader::Sweep { stops, .. } => stops,
Shader::Image { .. } => return false,
};
stops.iter().all(|stop| stop.color.a >= 1.0)
}
pub(super) fn filter_quad_record(quad: &Rect, extent: [u32; 2]) -> UniformRecord {
let mut record = UniformRecord::new(ortho_mvp(&rect_to_unit(quad), extent, 0.0), [0.0; 4]);
record.set_local_rect(quad);
record
}
pub(super) enum EncodedColorFilter {
Matrix,
Blend,
}
pub(super) fn encode_color_filter(
record: &mut UniformRecord,
filter: ColorFilter,
) -> EncodedColorFilter {
match filter {
ColorFilter::Matrix(matrix) => {
for row in 0..4 {
let start = row * 5;
record.set_payload(
PAYLOAD_COLOR_MATRIX + row,
[
matrix[start],
matrix[start + 1],
matrix[start + 2],
matrix[start + 3],
],
);
}
record.set_payload(
PAYLOAD_COLOR_MATRIX + 4,
[matrix[4], matrix[9], matrix[14], matrix[19]],
);
EncodedColorFilter::Matrix
}
ColorFilter::Blend(color, mode) => {
record.set_payload(PAYLOAD_COLOR_MATRIX, color.premultiplied());
record.set_payload(PAYLOAD_MISC, [blend_filter_id(mode) as f32, 0.0, 0.0, 0.0]);
EncodedColorFilter::Blend
}
}
}
fn fill_gradient_payload(record: &mut UniformRecord, shader: &Shader, ramp_texels: Option<u32>) {
let (geom, angle, misc_w) = match shader {
Shader::Linear { start, end, .. } => ([start.x, start.y, end.x, end.y], 0.0, 0.0),
Shader::Radial {
center,
radius,
focus,
..
} => {
let f = focus.map_or(*center, |circle| circle.center);
([center.x, center.y, *radius, f.x], 0.0, f.y)
}
Shader::Sweep {
center,
start_angle,
..
} => ([center.x, center.y, 0.0, 0.0], *start_angle, 0.0),
Shader::Image { .. } => unreachable!("patterns fill their own payload"),
};
record.set_payload(PAYLOAD_GEOM, geom);
let (Shader::Linear { local, .. }
| Shader::Radial { local, .. }
| Shader::Sweep { local, .. }
| Shader::Image { local, .. }) = shader;
let mut inverse = local
.invert()
.unwrap_or(Matrix::from_affine(0.0, 0.0, 0.0, 0.0, 0.0, 0.0));
let conical = match shader {
Shader::Radial {
center,
radius,
focus,
..
} => ConicalSetup::solve(*center, *radius, *focus),
_ => ConicalSetup::UNUSED,
};
if let Some(focal_map) = conical.focal_map {
inverse = focal_map.then(&inverse);
}
record.set_payload(PAYLOAD_CONICAL, conical.constants);
record.set_payload(PAYLOAD_CONICAL_FLAGS, conical.flags);
let [a, b, c, d, tx, ty] = inverse.to_affine();
record.set_payload(PAYLOAD_LOCAL, [a, b, c, d]);
record.set_payload(PAYLOAD_LOCAL + 1, [tx, ty, 0.0, 0.0]);
let spread = match shader {
Shader::Linear { spread, .. } | Shader::Radial { spread, .. } => *spread,
Shader::Sweep { .. } | Shader::Image { .. } => SpreadMode::Pad,
};
let stops = shader.stops();
let count = stops.len().min(MAX_GRADIENT_STOPS);
let count_lane = match ramp_texels {
Some(texels) => texels as f32,
None => count as f32,
};
record.set_payload(
PAYLOAD_MISC,
[count_lane, angle, spread as u8 as f32, misc_w],
);
if ramp_texels.is_some() {
return;
}
let mut offsets = [0.0f32; MAX_GRADIENT_STOPS];
for (i, stop) in stops.iter().take(count).enumerate() {
offsets[i] = stop.offset;
record.set_payload(PAYLOAD_COLORS + i, stop.color.components());
}
record.set_payload(
PAYLOAD_OFFSETS,
[offsets[0], offsets[1], offsets[2], offsets[3]],
);
record.set_payload(
PAYLOAD_OFFSETS + 1,
[offsets[4], offsets[5], offsets[6], offsets[7]],
);
}
fn fill_pattern_payload(
record: &mut UniformRecord,
image: &Image,
sampling: Sampling,
local: &Matrix,
) {
let size = image.size();
record.set_payload(
PAYLOAD_GEOM,
[1.0 / size[0] as f32, 1.0 / size[1] as f32, 0.0, 0.0],
);
record.set_payload(PAYLOAD_DECAL, decal_flags(sampling));
let inverse = local
.invert()
.unwrap_or(Matrix::from_affine(0.0, 0.0, 0.0, 0.0, 0.0, 0.0));
let [a, b, c, d, tx, ty] = inverse.to_affine();
record.set_payload(PAYLOAD_LOCAL, [a, b, c, d]);
record.set_payload(PAYLOAD_LOCAL + 1, [tx, ty, 0.0, 0.0]);
}
fn uv_mapping(image: &Image, src: &Rect, dst: &Rect) -> [f32; 4] {
let (tw, th) = (image.width(), image.height());
let sx = src.width / (dst.width * tw);
let sy = src.height / (dst.height * th);
[sx, sy, src.x / tw - dst.x * sx, src.y / th - dst.y * sy]
}
fn decal_flags(sampling: Sampling) -> [f32; 4] {
[
f32::from(sampling.tile_x == TileMode::Decal),
f32::from(sampling.tile_y == TileMode::Decal),
0.0,
0.0,
]
}
struct ConicalSetup {
constants: [f32; 4],
flags: [f32; 4],
focal_map: Option<Matrix>,
}
const CONICAL_CONCENTRIC: f32 = 0.0;
const CONICAL_GENERAL: f32 = 1.0;
const CONICAL_EMPTY: f32 = 2.0;
const CONICAL_STRIP: f32 = 3.0;
impl ConicalSetup {
const UNUSED: Self = Self {
constants: [CONICAL_CONCENTRIC, 0.0, 0.0, 0.0],
flags: [0.0; 4],
focal_map: None,
};
fn solve(center: Point, radius: f32, focus: Option<FocalCircle>) -> Self {
const CASE_EPSILON: f32 = 1.0e-3;
const NEARLY_ZERO: f32 = 1.0 / (1 << 12) as f32;
let start = focus.unwrap_or(FocalCircle::point(center));
let separation = (center.x - start.center.x).hypot(center.y - start.center.y);
if separation < CASE_EPSILON {
if (radius - start.radius).abs() < CASE_EPSILON {
return Self {
constants: [CONICAL_EMPTY, 0.0, 0.0, 0.0],
..Self::UNUSED
};
}
return Self {
constants: [CONICAL_CONCENTRIC, start.radius, radius, 0.0],
flags: [0.0; 4],
focal_map: None,
};
}
if (radius - start.radius).abs() < CASE_EPSILON {
let radius_in_unit_space = start.radius / separation;
return Self {
constants: [
CONICAL_STRIP,
radius_in_unit_space * radius_in_unit_space,
0.0,
0.0,
],
flags: [0.0; 4],
focal_map: Some(map_to_unit_x(start.center, center)),
};
}
let (mut first, mut second) = (start.center, center);
let mut focal = start.radius / (start.radius - radius);
let is_swapped = (focal - 1.0).abs() < NEARLY_ZERO;
if is_swapped {
std::mem::swap(&mut first, &mut second);
focal = 0.0f32;
}
let focal_center = Point::new(
first.x * (1.0 - focal) + second.x * focal,
first.y * (1.0 - focal) + second.y * focal,
);
let radius_in_unit_space = (radius - start.radius).abs() / separation;
let is_focal_on_circle = (radius_in_unit_space - 1.0).abs() < NEARLY_ZERO;
let span = (1.0 - focal).abs();
let (scale_x, scale_y) = if is_focal_on_circle {
(span * 0.5, span * 0.5)
} else {
let squared = radius_in_unit_space * radius_in_unit_space;
(
span * radius_in_unit_space / (squared - 1.0),
span / (squared - 1.0).abs().sqrt(),
)
};
let is_well_behaved = !is_focal_on_circle && radius_in_unit_space > 1.0;
Self {
constants: [
CONICAL_GENERAL,
radius_in_unit_space,
focal,
(1.0 - focal).signum(),
],
flags: [
is_swapped as u32 as f32,
is_focal_on_circle as u32 as f32,
is_well_behaved as u32 as f32,
0.0,
],
focal_map: Some(scale_after(
map_to_unit_x(focal_center, second),
scale_x,
scale_y,
)),
}
}
}
fn map_to_unit_x(from: Point, to: Point) -> Matrix {
let (dx, dy) = (to.x - from.x, to.y - from.y);
let length = dx.hypot(dy);
let (ux, uy) = (dx / length, dy / length);
Matrix::from_affine(
ux / length,
-uy / length,
uy / length,
ux / length,
-(ux * from.x + uy * from.y) / length,
(uy * from.x - ux * from.y) / length,
)
}
fn scale_after(matrix: Matrix, x: f32, y: f32) -> Matrix {
let [a, b, c, d, tx, ty] = matrix.to_affine();
Matrix::from_affine(a * x, b * y, c * x, d * y, tx * x, ty * y)
}
#[rustfmt::skip]
fn ortho_mvp(m: &Matrix, size: [u32; 2], z: f32) -> [f32; 16] {
let (w, h) = (size[0] as f32, size[1] as f32);
let projection = glam::Mat4::from_cols_array(&[
2.0 / w, 0.0, 0.0, 0.0,
0.0, -2.0 / h, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
-1.0, 1.0, 0.0, 1.0,
]);
let mut mvp = projection * m.to_mat4();
mvp.x_axis.z = z * mvp.x_axis.w;
mvp.y_axis.z = z * mvp.y_axis.w;
mvp.z_axis.z = z * mvp.z_axis.w;
mvp.w_axis.z = z * mvp.w_axis.w;
mvp.to_cols_array()
}