///|
priv struct ImageVertex {
x : Double
y : Double
u : Double
v : Double
opacity : Double
}
///|
priv struct NativeImagePlacement {
frame : @core.Rect
u0 : Double
v0 : Double
u1 : Double
v1 : Double
}
///|
priv struct NativeDecodedImage {
width : Int
height : Int
data : Bytes
}
///|
priv struct NativeImageResource {
width : Int
height : Int
texture : @wgpu_mbt.Texture
view : @wgpu_mbt.TextureView
bind_group : @wgpu_mbt.BindGroup
}
///|
fn calculate_image_placement(
frame : @core.Rect,
image_size : @core.Size,
fit : @core.ImageFit,
) -> NativeImagePlacement {
if image_size.width <= 0.0 ||
image_size.height <= 0.0 ||
frame.size.width <= 0.0 ||
frame.size.height <= 0.0 {
return { frame, u0: 0.0, v0: 0.0, u1: 1.0, v1: 1.0 }
}
let image_ratio = image_size.width / image_size.height
let frame_ratio = frame.size.width / frame.size.height
match fit {
@core.ImageFit::Stretch => { frame, u0: 0.0, v0: 0.0, u1: 1.0, v1: 1.0 }
@core.ImageFit::ScaleDown =>
if image_size.width <= frame.size.width &&
image_size.height <= frame.size.height {
{
frame: @core.Rect::new(
x=frame.origin.x + (frame.size.width - image_size.width) / 2.0,
y=frame.origin.y + (frame.size.height - image_size.height) / 2.0,
width=image_size.width,
height=image_size.height,
),
u0: 0.0,
v0: 0.0,
u1: 1.0,
v1: 1.0,
}
} else {
contain_image_placement(frame, image_ratio, frame_ratio)
}
@core.ImageFit::FitWidth => fit_width_image_placement(frame, image_ratio)
@core.ImageFit::FitHeight => fit_height_image_placement(frame, image_ratio)
@core.ImageFit::Contain =>
contain_image_placement(frame, image_ratio, frame_ratio)
@core.ImageFit::Cover =>
if image_ratio > frame_ratio {
let visible = frame_ratio / image_ratio
let inset = (1.0 - visible) / 2.0
{ frame, u0: inset, v0: 0.0, u1: 1.0 - inset, v1: 1.0 }
} else {
let visible = image_ratio / frame_ratio
let inset = (1.0 - visible) / 2.0
{ frame, u0: 0.0, v0: inset, u1: 1.0, v1: 1.0 - inset }
}
}
}
///|
fn fit_width_image_placement(
frame : @core.Rect,
image_ratio : Double,
) -> NativeImagePlacement {
let scaled_height = frame.size.width / image_ratio
if scaled_height <= frame.size.height {
{
frame: @core.Rect::new(
x=frame.origin.x,
y=frame.origin.y + (frame.size.height - scaled_height) / 2.0,
width=frame.size.width,
height=scaled_height,
),
u0: 0.0,
v0: 0.0,
u1: 1.0,
v1: 1.0,
}
} else {
let visible = frame.size.height / scaled_height
let inset = (1.0 - visible) / 2.0
{ frame, u0: 0.0, v0: inset, u1: 1.0, v1: 1.0 - inset }
}
}
///|
fn fit_height_image_placement(
frame : @core.Rect,
image_ratio : Double,
) -> NativeImagePlacement {
let scaled_width = frame.size.height * image_ratio
if scaled_width <= frame.size.width {
{
frame: @core.Rect::new(
x=frame.origin.x + (frame.size.width - scaled_width) / 2.0,
y=frame.origin.y,
width=scaled_width,
height=frame.size.height,
),
u0: 0.0,
v0: 0.0,
u1: 1.0,
v1: 1.0,
}
} else {
let visible = frame.size.width / scaled_width
let inset = (1.0 - visible) / 2.0
{ frame, u0: inset, v0: 0.0, u1: 1.0 - inset, v1: 1.0 }
}
}
///|
fn contain_image_placement(
frame : @core.Rect,
image_ratio : Double,
frame_ratio : Double,
) -> NativeImagePlacement {
if image_ratio > frame_ratio {
let scaled_height = frame.size.width / image_ratio
let offset_y = (frame.size.height - scaled_height) / 2.0
{
frame: @core.Rect::new(
x=frame.origin.x,
y=frame.origin.y + offset_y,
width=frame.size.width,
height=scaled_height,
),
u0: 0.0,
v0: 0.0,
u1: 1.0,
v1: 1.0,
}
} else {
let scaled_width = frame.size.height * image_ratio
let offset_x = (frame.size.width - scaled_width) / 2.0
{
frame: @core.Rect::new(
x=frame.origin.x + offset_x,
y=frame.origin.y,
width=scaled_width,
height=frame.size.height,
),
u0: 0.0,
v0: 0.0,
u1: 1.0,
v1: 1.0,
}
}
}
///|
fn generate_image_vertices(
frame : @core.Rect,
transform : @core.Transform2D,
u0 : Double,
v0 : Double,
u1 : Double,
v1 : Double,
opacity : Double,
screen_size : @core.Size,
) -> Array[ImageVertex] {
let x0 = frame.origin.x
let y0 = frame.origin.y
let x1 = frame.origin.x + frame.size.width
let y1 = frame.origin.y + frame.size.height
let vertices : Array[ImageVertex] = []
push_image_vertex(vertices, transform, x0, y0, u0, v0, opacity, screen_size)
push_image_vertex(vertices, transform, x0, y1, u0, v1, opacity, screen_size)
push_image_vertex(vertices, transform, x1, y1, u1, v1, opacity, screen_size)
push_image_vertex(vertices, transform, x0, y0, u0, v0, opacity, screen_size)
push_image_vertex(vertices, transform, x1, y1, u1, v1, opacity, screen_size)
push_image_vertex(vertices, transform, x1, y0, u1, v0, opacity, screen_size)
vertices
}
///|
fn push_image_vertex(
vertices : Array[ImageVertex],
transform : @core.Transform2D,
x : Double,
y : Double,
u : Double,
v : Double,
opacity : Double,
screen_size : @core.Size,
) -> Unit {
let point = transform_point(transform, x, y)
vertices.push({
x: visual_pixel_x_to_ndc(point.x, screen_size.width),
y: visual_pixel_y_to_ndc(point.y, screen_size.height),
u,
v,
opacity,
})
}
///|
fn load_native_image_data(source : String) -> NativeDecodedImage? {
match native_image_source_bytes(source) {
Some(bytes) => decode_native_image_bytes(bytes)
None => None
}
}
///|
pub fn native_image_load_completion(
source : String,
) -> @render.ImageResourceLoadCompletion {
match load_native_image_data(source) {
Some(decoded) => native_image_completion_from_decoded(source, decoded)
None =>
@render.ImageResourceLoadCompletion::failed(
source,
diagnostic="native image decode failed",
)
}
}
///|
pub fn native_image_load_completion_from_bytes(
source : String,
bytes : Bytes,
background_io? : Bool = false,
) -> @render.ImageResourceLoadCompletion {
match decode_native_image_bytes(bytes) {
Some(decoded) =>
native_image_completion_from_decoded(source, decoded, background_io~)
None =>
@render.ImageResourceLoadCompletion::failed(
source,
diagnostic="native image decode failed",
background_io~,
)
}
}
///|
fn native_image_completion_from_decoded(
source : String,
decoded : NativeDecodedImage,
background_io? : Bool = false,
) -> @render.ImageResourceLoadCompletion {
@render.ImageResourceLoadCompletion::ready_decoded_rgba(
source,
width=decoded.width,
height=decoded.height,
row_bytes=decoded.width * 4,
decoded.data,
background_io~,
)
}
///|
fn native_image_source_bytes(source : String) -> Bytes? {
if source.has_prefix("data:") {
native_image_data_uri_bytes(source)
} else {
None
}
}
///|
fn native_image_data_uri_bytes(source : String) -> Bytes? {
let parts = source.split(",").to_array()
if parts.length() < 2 {
return None
}
let meta = parts[0]
if !meta.has_prefix("data:") || !meta.contains(";base64") {
return None
}
Some(@base64.decode(parts[1], ignore_whitespace=true)) catch {
_ => None
}
}
///|
fn decode_native_image_bytes(bytes : Bytes) -> NativeDecodedImage? {
let decoded = if native_bytes_are_png(bytes) {
Some(@image.decode_png(bytes)) catch {
_ => None
}
} else if native_bytes_are_jpeg(bytes) {
Some(@image.decode_jpeg(bytes)) catch {
_ => None
}
} else if native_bytes_are_bmp(bytes) {
Some(@image.decode_bmp(bytes)) catch {
_ => None
}
} else {
None
}
match decoded {
Some(image) =>
Some({ width: image.width, height: image.height, data: image.data })
None => None
}
}
///|
fn native_bytes_are_png(bytes : Bytes) -> Bool {
bytes.length() >= 8 &&
bytes[0] == b'\x89' &&
bytes[1] == b'\x50' &&
bytes[2] == b'\x4E' &&
bytes[3] == b'\x47' &&
bytes[4] == b'\x0D' &&
bytes[5] == b'\x0A' &&
bytes[6] == b'\x1A' &&
bytes[7] == b'\x0A'
}
///|
fn native_bytes_are_jpeg(bytes : Bytes) -> Bool {
bytes.length() >= 3 &&
bytes[0] == b'\xFF' &&
bytes[1] == b'\xD8' &&
bytes[2] == b'\xFF'
}
///|
fn native_bytes_are_bmp(bytes : Bytes) -> Bool {
bytes.length() >= 2 && bytes[0] == b'\x42' && bytes[1] == b'\x4D'
}
///|
fn RendererHandle::ensure_native_image_resource(
self : RendererHandle,
source : String,
device : @wgpu_mbt.Device,
queue : @wgpu_mbt.Queue,
) -> NativeImageResource? {
match self.image_cache.get(source) {
Some(resource) => {
ignore(
self.image_lifecycle.mark_ready(
source,
width=resource.width,
height=resource.height,
),
)
return Some(resource)
}
None => ()
}
if self.failed_images.get(source).unwrap_or(false) {
ignore(
self.image_lifecycle.mark_failed(
source,
diagnostic="native image decode failed",
),
)
return None
}
let decoded = match self.decoded_image_cache.get(source) {
Some(image) => Some(image)
None => load_native_image_data(source)
}
guard decoded is Some(image) else {
if native_image_source_is_pending(source) {
ignore(self.image_lifecycle.mark_loading(source))
return None
}
self.failed_images[source] = true
ignore(
self.image_lifecycle.mark_failed(
source,
diagnostic="native image decode failed",
),
)
return None
}
match (self.image_bind_group_layout, self.image_sampler) {
(Some(layout), Some(sampler)) => {
let usage = @wgpu_mbt.TextureUsage::from_u64(
@wgpu_mbt.TEXTURE_USAGE_TEXTURE_BINDING |
@wgpu_mbt.TEXTURE_USAGE_COPY_DST,
)
let texture = device.create_texture_rgba8_2d_with_usage(
image.width.reinterpret_as_uint(),
image.height.reinterpret_as_uint(),
usage,
)
queue.write_texture_rgba8_2d(
texture,
image.width.reinterpret_as_uint(),
image.height.reinterpret_as_uint(),
image.data,
)
let view = texture.create_view()
let bind_group = device.create_bind_group_sampler_texture_2d(
layout, sampler, view,
)
let resource = {
width: image.width,
height: image.height,
texture,
view,
bind_group,
}
self.image_cache[source] = resource
ignore(self.decoded_image_cache.remove(source))
ignore(
self.image_lifecycle.mark_ready(
source,
width=resource.width,
height=resource.height,
),
)
Some(resource)
}
_ => {
ignore(
self.image_lifecycle.mark_failed(
source,
diagnostic="native image pipeline is not configured",
),
)
None
}
}
}
///|
fn native_image_source_is_pending(source : String) -> Bool {
!source.has_prefix("data:")
}
///|
fn RendererHandle::record_native_image_commands(
self : RendererHandle,
commands : Array[@core.DrawCommand],
) -> Unit {
for command in commands {
match command {
@core.DrawCommand::DrawImage(run) =>
match self.image_cache.get(run.source) {
Some(resource) =>
ignore(
self.image_lifecycle.mark_ready(
run.source,
width=resource.width,
height=resource.height,
),
)
None =>
if self.failed_images.get(run.source).unwrap_or(false) {
ignore(
self.image_lifecycle.mark_failed(
run.source,
diagnostic="native image decode failed",
),
)
} else {
ignore(self.image_lifecycle.mark_loading(run.source))
}
}
_ => ()
}
}
}
///|
fn RendererHandle::prepare_native_image_items(
self : RendererHandle,
device : @wgpu_mbt.Device,
queue : @wgpu_mbt.Queue,
) -> Unit {
let items : Array[NativeDrawItem] = []
self.image_vertices = []
for item in self.draw_items {
match item {
NativeDrawItem::NativeImage(_, _, scissor, run) =>
match self.ensure_native_image_resource(run.source, device, queue) {
Some(resource) => {
let start = self.image_vertices.length()
append_prepared_image_vertices(
self.image_vertices,
run,
resource,
self.size,
)
items.push(
NativeDrawItem::NativeImage(
start,
self.image_vertices.length() - start,
scissor,
run,
),
)
}
None => {
let start = self.visual_vertices.length()
append_visual_quad(
self.visual_vertices,
run.frame,
radius=0.0,
brush=native_image_placeholder_brush(run),
mode=visual_mode_fill,
stroke_width=0.0,
blur_radius=0.0,
state={
opacity: 1.0,
transform: run.transform,
clip: None,
clip_radius: 0.0,
clip_empty: false,
},
size=self.size,
)
items.push(
NativeDrawItem::NativeVisual(
start,
self.visual_vertices.length() - start,
scissor,
),
)
}
}
NativeDrawItem::NativeLayer(layer) => {
self.prepare_native_image_plan(layer.plan, device, queue)
items.push(NativeDrawItem::NativeLayer(layer))
}
NativeDrawItem::NativeShaderEffect(_) => items.push(item)
_ => items.push(item)
}
}
self.draw_items = items
}
///|
fn RendererHandle::prepare_native_image_plan(
self : RendererHandle,
plan : NativeDrawPlan,
device : @wgpu_mbt.Device,
queue : @wgpu_mbt.Queue,
) -> Unit {
let items : Array[NativeDrawItem] = []
plan.image_vertices.clear()
for item in plan.items {
match item {
NativeDrawItem::NativeImage(_, _, scissor, run) =>
match self.ensure_native_image_resource(run.source, device, queue) {
Some(resource) => {
let start = plan.image_vertices.length()
append_prepared_image_vertices(
plan.image_vertices,
run,
resource,
self.size,
)
items.push(
NativeDrawItem::NativeImage(
start,
plan.image_vertices.length() - start,
scissor,
run,
),
)
}
None => {
let start = plan.visual_vertices.length()
append_visual_quad(
plan.visual_vertices,
run.frame,
radius=0.0,
brush=native_image_placeholder_brush(run),
mode=visual_mode_fill,
stroke_width=0.0,
blur_radius=0.0,
state={
opacity: 1.0,
transform: run.transform,
clip: None,
clip_radius: 0.0,
clip_empty: false,
},
size=self.size,
)
items.push(
NativeDrawItem::NativeVisual(
start,
plan.visual_vertices.length() - start,
scissor,
),
)
}
}
NativeDrawItem::NativeLayer(layer) => {
self.prepare_native_image_plan(layer.plan, device, queue)
items.push(NativeDrawItem::NativeLayer(layer))
}
_ => items.push(item)
}
}
plan.items.clear()
for item in items {
plan.items.push(item)
}
}
///|
fn append_prepared_image_vertices(
vertices : Array[ImageVertex],
run : NativeImageRun,
resource : NativeImageResource,
screen_size : @core.Size,
) -> Unit {
let placement = calculate_image_placement(
run.frame,
@core.Size::new(
width=resource.width.to_double(),
height=resource.height.to_double(),
),
run.fit,
)
for
vertex in generate_image_vertices(
placement.frame,
run.transform,
placement.u0,
placement.v0,
placement.u1,
placement.v1,
run.opacity,
screen_size,
) {
vertices.push(vertex)
}
}
///|
fn NativeImageResource::release(self : NativeImageResource) -> Unit {
self.bind_group.release()
self.view.release()
self.texture.destroy()
self.texture.release()
}
///|
fn native_image_placeholder_brush(run : NativeImageRun) -> @core.Brush {
@core.Brush::linear_gradient(
start=run.frame.origin,
end=@core.Point::new(
x=run.frame.origin.x + run.frame.size.width,
y=run.frame.origin.y + run.frame.size.height,
),
start_color=@core.Color::rgba(r=0.72, g=0.78, b=0.86, a=run.opacity),
end_color=@core.Color::rgba(r=0.42, g=0.50, b=0.62, a=run.opacity),
)
}
///|
fn image_vertices_to_bytes(vertices : Array[ImageVertex]) -> Bytes {
let out : Array[Byte] = []
for vertex in vertices {
push_visual_f32le(out, vertex.x)
push_visual_f32le(out, vertex.y)
push_visual_f32le(out, vertex.u)
push_visual_f32le(out, vertex.v)
push_visual_f32le(out, vertex.opacity)
}
Bytes::from_array(out)
}
///|
fn image_shader_wgsl() -> String {
let shader =
#|struct VSOut {
#| @builtin(position) pos: vec4,
#| @location(0) uv: vec2,
#| @location(1) opacity: f32,
#|};
#|
#|@vertex
#|fn vs_main(
#| @location(0) pos: vec2,
#| @location(1) uv: vec2,
#| @location(2) opacity: f32,
#|) -> VSOut {
#| var out: VSOut;
#| out.pos = vec4(pos, 0.0, 1.0);
#| out.uv = uv;
#| out.opacity = opacity;
#| return out;
#|}
#|
#|@group(0) @binding(0) var image_sampler : sampler;
#|@group(0) @binding(1) var image_texture : texture_2d;
#|
#|@fragment
#|fn fs_main(in: VSOut) -> @location(0) vec4 {
#| let sample = textureSample(image_texture, image_sampler, in.uv);
#| return vec4(sample.rgb, sample.a * in.opacity);
#|}
#|
shader
}
///|
let image_vertex_stride : UInt64 = 20UL