mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
refactor: split effects and masks into dedicated rust crates, introduce MediaTime and FrameRate
This commit is contained in:
+10
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "opencut-wasm"
|
||||
version = "0.1.3"
|
||||
version = "0.2.3"
|
||||
edition = "2024"
|
||||
description = "Shared video editor logic compiled to WebAssembly"
|
||||
repository = "https://github.com/opencut/opencut"
|
||||
@@ -11,11 +11,19 @@ path = "src/wasm.rs"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
gpu = { version = "0.1.0", path = "../crates/gpu" }
|
||||
bridge = { version = "0.1.0", path = "../crates/bridge" }
|
||||
effects = { version = "0.1.0", path = "../crates/effects" }
|
||||
gpu = { version = "0.1.0", path = "../crates/gpu", features = ["wasm"] }
|
||||
js-sys = "0.3.93"
|
||||
masks = { version = "0.1.0", path = "../crates/masks" }
|
||||
num-traits = "0.2.19"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde-wasm-bindgen = "0.6.5"
|
||||
time = { version = "0.1.0", path = "../crates/time", features = ["wasm"] }
|
||||
wasm-bindgen = "0.2.116"
|
||||
wasm-bindgen-futures = "0.4.66"
|
||||
web-sys = { version = "0.3.93", features = ["OffscreenCanvas", "HtmlCanvasElement", "CanvasRenderingContext2d", "Document", "Window"] }
|
||||
|
||||
[features]
|
||||
default = ["wasm"]
|
||||
wasm = []
|
||||
|
||||
+4
-1
@@ -11,7 +11,10 @@ npm install opencut-wasm
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { formatTimeCode } from "opencut-wasm";
|
||||
import { formatTimecode, mediaTimeFromSeconds } from "opencut-wasm";
|
||||
|
||||
const ticks = mediaTimeFromSeconds(1.5);
|
||||
const label = formatTimecode({ ticks });
|
||||
```
|
||||
|
||||
All exports are documented in the [TypeScript definitions](./opencut_wasm.d.ts).
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
|
||||
use effects::{ApplyEffectsOptions, EffectPass, UniformValue};
|
||||
use gpu::wgpu;
|
||||
use js_sys::Object;
|
||||
use serde::Deserialize;
|
||||
use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen};
|
||||
|
||||
use crate::gpu::{
|
||||
import_canvas_texture, read_offscreen_canvas_property, read_serde_property, read_u32_property,
|
||||
render_texture_to_canvas, with_gpu_runtime,
|
||||
};
|
||||
|
||||
struct ApplyEffectPassesOptions {
|
||||
source: wgpu::web_sys::OffscreenCanvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
passes: Vec<EffectPassInput>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct EffectPassInput {
|
||||
shader: String,
|
||||
uniforms: Vec<EffectUniformInput>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct EffectUniformInput {
|
||||
name: String,
|
||||
value: Vec<f32>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = applyEffectPasses)]
|
||||
pub fn apply_effect_passes(options: JsValue) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> {
|
||||
let ApplyEffectPassesOptions {
|
||||
source,
|
||||
width,
|
||||
height,
|
||||
passes,
|
||||
} = parse_apply_effect_passes_options(options)?;
|
||||
|
||||
with_gpu_runtime(|runtime| {
|
||||
let source_texture = import_canvas_texture(
|
||||
&runtime.context,
|
||||
&source,
|
||||
width,
|
||||
height,
|
||||
"effects-input-texture",
|
||||
);
|
||||
let effect_passes = map_effect_passes(passes);
|
||||
let result_texture = runtime
|
||||
.effects
|
||||
.apply(
|
||||
&runtime.context,
|
||||
ApplyEffectsOptions {
|
||||
source: &source_texture,
|
||||
width,
|
||||
height,
|
||||
passes: &effect_passes,
|
||||
},
|
||||
)
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||
render_texture_to_canvas(&runtime.context, &result_texture, width, height)
|
||||
})
|
||||
}
|
||||
|
||||
fn map_effect_passes(effect_passes: Vec<EffectPassInput>) -> Vec<EffectPass> {
|
||||
effect_passes
|
||||
.into_iter()
|
||||
.map(|pass| EffectPass {
|
||||
shader: pass.shader,
|
||||
uniforms: pass
|
||||
.uniforms
|
||||
.into_iter()
|
||||
.map(|uniform| {
|
||||
let value = if uniform.value.len() == 1 {
|
||||
UniformValue::Number(uniform.value[0])
|
||||
} else {
|
||||
UniformValue::Vector(uniform.value)
|
||||
};
|
||||
(uniform.name, value)
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_apply_effect_passes_options(value: JsValue) -> Result<ApplyEffectPassesOptions, JsValue> {
|
||||
let object: Object = value
|
||||
.dyn_into()
|
||||
.map_err(|_| JsValue::from_str("applyEffectPasses expects an options object"))?;
|
||||
|
||||
Ok(ApplyEffectPassesOptions {
|
||||
source: read_offscreen_canvas_property(&object, "source")?,
|
||||
width: read_u32_property(&object, "width")?,
|
||||
height: read_u32_property(&object, "height")?,
|
||||
passes: read_serde_property(&object, "passes")?,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
use effects::EffectPipeline;
|
||||
use gpu::{GpuContext, wgpu};
|
||||
use js_sys::{Object, Reflect};
|
||||
use masks::MaskFeatherPipeline;
|
||||
use serde::Deserialize;
|
||||
use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen};
|
||||
|
||||
pub(crate) struct GpuRuntime {
|
||||
pub(crate) context: GpuContext,
|
||||
pub(crate) effects: EffectPipeline,
|
||||
pub(crate) masks: MaskFeatherPipeline,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static GPU_RUNTIME: RefCell<Option<GpuRuntime>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = initializeGpu)]
|
||||
pub async fn initialize_gpu() -> Result<(), JsValue> {
|
||||
if GPU_RUNTIME.with(|runtime| runtime.borrow().is_some()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let context = GpuContext::new()
|
||||
.await
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||
let effects = EffectPipeline::new(&context);
|
||||
let masks = MaskFeatherPipeline::new(&context);
|
||||
|
||||
GPU_RUNTIME.with(|runtime| {
|
||||
runtime.replace(Some(GpuRuntime {
|
||||
context,
|
||||
effects,
|
||||
masks,
|
||||
}));
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn with_gpu_runtime<T>(
|
||||
action: impl FnOnce(&GpuRuntime) -> Result<T, JsValue>,
|
||||
) -> Result<T, JsValue> {
|
||||
GPU_RUNTIME.with(|runtime| {
|
||||
let borrow = runtime.borrow();
|
||||
let Some(gpu_runtime) = borrow.as_ref() else {
|
||||
return Err(JsValue::from_str(
|
||||
"GPU context not initialized. Call initializeGpu() first.",
|
||||
));
|
||||
};
|
||||
action(gpu_runtime)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn import_canvas_texture(
|
||||
context: &GpuContext,
|
||||
canvas: &wgpu::web_sys::OffscreenCanvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
label: &'static str,
|
||||
) -> wgpu::Texture {
|
||||
context.import_offscreen_canvas_texture(canvas, width, height, label)
|
||||
}
|
||||
|
||||
pub(crate) fn render_texture_to_canvas(
|
||||
context: &GpuContext,
|
||||
texture: &wgpu::Texture,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> {
|
||||
let canvas = wgpu::web_sys::OffscreenCanvas::new(width, height)?;
|
||||
context
|
||||
.render_texture_to_offscreen_canvas(texture, &canvas, width, height)
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||
Ok(canvas)
|
||||
}
|
||||
|
||||
pub(crate) fn read_property(object: &Object, name: &str) -> Result<JsValue, JsValue> {
|
||||
Reflect::get(object, &JsValue::from_str(name))
|
||||
.map_err(|_| JsValue::from_str(&format!("Missing property '{name}'")))
|
||||
}
|
||||
|
||||
pub(crate) fn read_offscreen_canvas_property(
|
||||
object: &Object,
|
||||
name: &str,
|
||||
) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> {
|
||||
read_property(object, name)?
|
||||
.dyn_into::<wgpu::web_sys::OffscreenCanvas>()
|
||||
.map_err(|_| JsValue::from_str(&format!("Property '{name}' must be an OffscreenCanvas")))
|
||||
}
|
||||
|
||||
pub(crate) fn read_u32_property(object: &Object, name: &str) -> Result<u32, JsValue> {
|
||||
let value = read_property(object, name)?;
|
||||
let Some(number) = value.as_f64() else {
|
||||
return Err(JsValue::from_str(&format!(
|
||||
"Property '{name}' must be a number"
|
||||
)));
|
||||
};
|
||||
Ok(number as u32)
|
||||
}
|
||||
|
||||
pub(crate) fn read_f32_property(object: &Object, name: &str) -> Result<f32, JsValue> {
|
||||
let value = read_property(object, name)?;
|
||||
let Some(number) = value.as_f64() else {
|
||||
return Err(JsValue::from_str(&format!(
|
||||
"Property '{name}' must be a number"
|
||||
)));
|
||||
};
|
||||
Ok(number as f32)
|
||||
}
|
||||
|
||||
pub(crate) fn read_serde_property<T>(object: &Object, name: &str) -> Result<T, JsValue>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let value = read_property(object, name)?;
|
||||
serde_wasm_bindgen::from_value(value)
|
||||
.map_err(|error| JsValue::from_str(&format!("Invalid property '{name}': {error}")))
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
use gpu::{EffectPass, GpuContext, UniformValue, wgpu};
|
||||
use js_sys::{Object, Reflect};
|
||||
use serde::Deserialize;
|
||||
use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen};
|
||||
|
||||
thread_local! {
|
||||
static GPU_CONTEXT: RefCell<Option<GpuContext>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
struct ApplyEffectPassesOptions {
|
||||
source: wgpu::web_sys::OffscreenCanvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
passes: Vec<EffectPassInput>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct EffectPassInput {
|
||||
shader: String,
|
||||
uniforms: Vec<EffectUniformInput>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct EffectUniformInput {
|
||||
name: String,
|
||||
value: Vec<f32>,
|
||||
}
|
||||
|
||||
struct ApplyMaskFeatherOptions {
|
||||
mask: wgpu::web_sys::OffscreenCanvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
feather: f32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = initializeGpu)]
|
||||
pub async fn initialize_gpu() -> Result<(), JsValue> {
|
||||
if GPU_CONTEXT.with(|context| context.borrow().is_some()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let context = GpuContext::new()
|
||||
.await
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||
GPU_CONTEXT.with(|gpu_context| {
|
||||
gpu_context.replace(Some(context));
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = applyEffectPasses)]
|
||||
pub fn apply_effect_passes(
|
||||
options: JsValue,
|
||||
) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> {
|
||||
let ApplyEffectPassesOptions {
|
||||
source,
|
||||
width,
|
||||
height,
|
||||
passes,
|
||||
} = parse_apply_effect_passes_options(options)?;
|
||||
|
||||
with_gpu_context(|context| {
|
||||
let source_texture = import_canvas_texture(context, &source, width, height)?;
|
||||
let effect_passes = map_effect_passes(passes);
|
||||
let result_texture = context
|
||||
.apply_effects(gpu::ApplyEffectsOptions {
|
||||
source: &source_texture,
|
||||
width,
|
||||
height,
|
||||
passes: &effect_passes,
|
||||
})
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||
let output_canvas = wgpu::web_sys::OffscreenCanvas::new(width, height)?;
|
||||
let surface = context
|
||||
.instance()
|
||||
.create_surface(wgpu::SurfaceTarget::OffscreenCanvas(output_canvas.clone()))
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||
|
||||
context
|
||||
.render_texture_to_surface(&result_texture, &surface, width, height)
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||
Ok(output_canvas)
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = applyMaskFeather)]
|
||||
pub fn apply_mask_feather(
|
||||
options: JsValue,
|
||||
) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> {
|
||||
let ApplyMaskFeatherOptions {
|
||||
mask,
|
||||
width,
|
||||
height,
|
||||
feather,
|
||||
} = parse_apply_mask_feather_options(options)?;
|
||||
|
||||
with_gpu_context(|context| {
|
||||
let mask_texture = import_canvas_texture(context, &mask, width, height)?;
|
||||
let result_texture = context.apply_mask_feather(gpu::ApplyMaskFeatherOptions {
|
||||
mask: &mask_texture,
|
||||
width,
|
||||
height,
|
||||
feather,
|
||||
});
|
||||
let output_canvas = wgpu::web_sys::OffscreenCanvas::new(width, height)?;
|
||||
let surface = context
|
||||
.instance()
|
||||
.create_surface(wgpu::SurfaceTarget::OffscreenCanvas(output_canvas.clone()))
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||
|
||||
context
|
||||
.render_texture_to_surface(&result_texture, &surface, width, height)
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||
Ok(output_canvas)
|
||||
})
|
||||
}
|
||||
|
||||
fn with_gpu_context<T>(
|
||||
action: impl FnOnce(&GpuContext) -> Result<T, JsValue>,
|
||||
) -> Result<T, JsValue> {
|
||||
GPU_CONTEXT.with(|context| {
|
||||
let borrow = context.borrow();
|
||||
let Some(gpu_context) = borrow.as_ref() else {
|
||||
return Err(JsValue::from_str(
|
||||
"GPU context not initialized. Call initializeGpu() first.",
|
||||
));
|
||||
};
|
||||
action(gpu_context)
|
||||
})
|
||||
}
|
||||
|
||||
fn import_canvas_texture(
|
||||
context: &GpuContext,
|
||||
canvas: &wgpu::web_sys::OffscreenCanvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<wgpu::Texture, JsValue> {
|
||||
let texture = context.create_render_texture(width, height, "gpu-bridge-input-texture");
|
||||
context.queue().copy_external_image_to_texture(
|
||||
&wgpu::CopyExternalImageSourceInfo {
|
||||
source: wgpu::ExternalImageSource::OffscreenCanvas(canvas.clone()),
|
||||
origin: wgpu::Origin2d::ZERO,
|
||||
flip_y: true,
|
||||
},
|
||||
wgpu::CopyExternalImageDestInfo {
|
||||
texture: &texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
color_space: wgpu::PredefinedColorSpace::Srgb,
|
||||
premultiplied_alpha: false,
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
fn map_effect_passes(effect_passes: Vec<EffectPassInput>) -> Vec<EffectPass> {
|
||||
effect_passes
|
||||
.into_iter()
|
||||
.map(|pass| EffectPass {
|
||||
shader: pass.shader,
|
||||
uniforms: pass
|
||||
.uniforms
|
||||
.into_iter()
|
||||
.map(|uniform| {
|
||||
let value = if uniform.value.len() == 1 {
|
||||
UniformValue::Number(uniform.value[0])
|
||||
} else {
|
||||
UniformValue::Vector(uniform.value)
|
||||
};
|
||||
(uniform.name, value)
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_apply_effect_passes_options(value: JsValue) -> Result<ApplyEffectPassesOptions, JsValue> {
|
||||
let object: Object = value
|
||||
.dyn_into()
|
||||
.map_err(|_| JsValue::from_str("applyEffectPasses expects an options object"))?;
|
||||
|
||||
Ok(ApplyEffectPassesOptions {
|
||||
source: read_offscreen_canvas_property(&object, "source")?,
|
||||
width: read_u32_property(&object, "width")?,
|
||||
height: read_u32_property(&object, "height")?,
|
||||
passes: read_serde_property(&object, "passes")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_apply_mask_feather_options(value: JsValue) -> Result<ApplyMaskFeatherOptions, JsValue> {
|
||||
let object: Object = value
|
||||
.dyn_into()
|
||||
.map_err(|_| JsValue::from_str("applyMaskFeather expects an options object"))?;
|
||||
|
||||
Ok(ApplyMaskFeatherOptions {
|
||||
mask: read_offscreen_canvas_property(&object, "mask")?,
|
||||
width: read_u32_property(&object, "width")?,
|
||||
height: read_u32_property(&object, "height")?,
|
||||
feather: read_f32_property(&object, "feather")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_property(object: &Object, name: &str) -> Result<JsValue, JsValue> {
|
||||
Reflect::get(object, &JsValue::from_str(name))
|
||||
.map_err(|_| JsValue::from_str(&format!("Missing property '{name}'")))
|
||||
}
|
||||
|
||||
fn read_offscreen_canvas_property(
|
||||
object: &Object,
|
||||
name: &str,
|
||||
) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> {
|
||||
read_property(object, name)?
|
||||
.dyn_into::<wgpu::web_sys::OffscreenCanvas>()
|
||||
.map_err(|_| JsValue::from_str(&format!("Property '{name}' must be an OffscreenCanvas")))
|
||||
}
|
||||
|
||||
fn read_u32_property(object: &Object, name: &str) -> Result<u32, JsValue> {
|
||||
let value = read_property(object, name)?;
|
||||
let Some(number) = value.as_f64() else {
|
||||
return Err(JsValue::from_str(&format!(
|
||||
"Property '{name}' must be a number"
|
||||
)));
|
||||
};
|
||||
Ok(number as u32)
|
||||
}
|
||||
|
||||
fn read_f32_property(object: &Object, name: &str) -> Result<f32, JsValue> {
|
||||
let value = read_property(object, name)?;
|
||||
let Some(number) = value.as_f64() else {
|
||||
return Err(JsValue::from_str(&format!(
|
||||
"Property '{name}' must be a number"
|
||||
)));
|
||||
};
|
||||
Ok(number as f32)
|
||||
}
|
||||
|
||||
fn read_serde_property<T>(object: &Object, name: &str) -> Result<T, JsValue>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let value = read_property(object, name)?;
|
||||
serde_wasm_bindgen::from_value(value)
|
||||
.map_err(|error| JsValue::from_str(&format!("Invalid property '{name}': {error}")))
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
|
||||
use gpu::wgpu;
|
||||
use js_sys::Object;
|
||||
use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen};
|
||||
|
||||
use crate::gpu::{
|
||||
import_canvas_texture, read_f32_property, read_offscreen_canvas_property, read_u32_property,
|
||||
render_texture_to_canvas, with_gpu_runtime,
|
||||
};
|
||||
|
||||
struct ApplyMaskFeatherOptions {
|
||||
mask: wgpu::web_sys::OffscreenCanvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
feather: f32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = applyMaskFeather)]
|
||||
pub fn apply_mask_feather(options: JsValue) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> {
|
||||
let ApplyMaskFeatherOptions {
|
||||
mask,
|
||||
width,
|
||||
height,
|
||||
feather,
|
||||
} = parse_apply_mask_feather_options(options)?;
|
||||
|
||||
with_gpu_runtime(|runtime| {
|
||||
let mask_texture = import_canvas_texture(
|
||||
&runtime.context,
|
||||
&mask,
|
||||
width,
|
||||
height,
|
||||
"masks-input-texture",
|
||||
);
|
||||
let result_texture = runtime.masks.apply_mask_feather(
|
||||
&runtime.context,
|
||||
masks::ApplyMaskFeatherOptions {
|
||||
mask: &mask_texture,
|
||||
width,
|
||||
height,
|
||||
feather,
|
||||
},
|
||||
);
|
||||
render_texture_to_canvas(&runtime.context, &result_texture, width, height)
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_apply_mask_feather_options(value: JsValue) -> Result<ApplyMaskFeatherOptions, JsValue> {
|
||||
let object: Object = value
|
||||
.dyn_into()
|
||||
.map_err(|_| JsValue::from_str("applyMaskFeather expects an options object"))?;
|
||||
|
||||
Ok(ApplyMaskFeatherOptions {
|
||||
mask: read_offscreen_canvas_property(&object, "mask")?,
|
||||
width: read_u32_property(&object, "width")?,
|
||||
height: read_u32_property(&object, "height")?,
|
||||
feather: read_f32_property(&object, "feather")?,
|
||||
})
|
||||
}
|
||||
+10
-2
@@ -1,6 +1,14 @@
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod gpu_bridge;
|
||||
mod effects;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod gpu;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod masks;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use gpu_bridge::*;
|
||||
pub use effects::*;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use gpu::*;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use masks::*;
|
||||
pub use time::*;
|
||||
|
||||
Reference in New Issue
Block a user