web: Support wmode parameter

This commit is contained in:
Mike Welsh 2022-04-13 11:55:15 -07:00
parent 8131a4f3cc
commit 3305ac69c4
8 changed files with 199 additions and 16 deletions

View File

@ -46,7 +46,7 @@ pub use graphic::Graphic;
pub use interactive::{InteractiveObject, TInteractiveObject};
pub use morph_shape::{MorphShape, MorphShapeStatic};
pub use movie_clip::{MovieClip, Scene};
pub use stage::{Stage, StageAlign, StageDisplayState, StageQuality, StageScaleMode};
pub use stage::{Stage, StageAlign, StageDisplayState, StageQuality, StageScaleMode, WindowMode};
pub use text::Text;
pub use video::Video;

View File

@ -95,6 +95,11 @@ pub struct StageData<'gc> {
/// The bounds of the current viewport in twips, used for culling.
view_bounds: BoundingBox,
/// The window mode of the viewport.
///
/// Only used on web to control how the Flash content layers with other content on the page.
window_mode: WindowMode,
/// Whether to show default context menu items
show_menu: bool,
@ -121,6 +126,7 @@ impl<'gc> Stage<'gc> {
viewport_size: (width, height),
viewport_scale_factor: 1.0,
view_bounds: Default::default(),
window_mode: Default::default(),
show_menu: true,
avm2_object: Avm2ScriptObject::bare_object(gc_context),
},
@ -319,6 +325,22 @@ impl<'gc> Stage<'gc> {
self.build_matrices(context);
}
/// Get the stage mode.
/// This controls how the content layers with other content on the page.
/// Only used on web.
pub fn window_mode(self) -> WindowMode {
self.0.read().window_mode
}
/// Sets the window mode.
pub fn set_window_mode(
self,
context: &mut UpdateContext<'_, 'gc, '_>,
window_mode: WindowMode,
) {
self.0.write(context.gc_context).window_mode = window_mode;
}
pub fn view_bounds(self) -> BoundingBox {
self.0.read().view_bounds.clone()
}
@ -334,12 +356,13 @@ impl<'gc> Stage<'gc> {
/// Determine if we should letterbox the stage content.
fn should_letterbox(self) -> bool {
// Only enable letterbox is the default `ShowAll` scale mode.
// Only enable letterbox in the default `ShowAll` scale mode.
// If content changes the scale mode or alignment, it signals that it is size-aware.
// For example, `NoScale` is used to make responsive layouts; don't letterbox over it.
let stage = self.0.read();
stage.scale_mode == StageScaleMode::ShowAll
&& stage.align.is_empty()
&& stage.window_mode != WindowMode::Transparent
&& (stage.letterbox == Letterbox::On
|| (stage.letterbox == Letterbox::Fullscreen && self.is_fullscreen()))
}
@ -673,9 +696,13 @@ impl<'gc> TDisplayObject<'gc> for Stage<'gc> {
}
fn render(&self, context: &mut RenderContext<'_, 'gc>) {
let background_color = self
.background_color()
.unwrap_or_else(|| Color::from_rgb(0xffffff, 255));
let background_color =
if self.window_mode() != WindowMode::Transparent || self.is_fullscreen() {
self.background_color()
.unwrap_or_else(|| Color::from_rgb(0xffffff, 255))
} else {
Color::from_rgba(0)
};
context.renderer.begin_frame(background_color);
@ -1060,3 +1087,73 @@ impl FromWStr for StageQuality {
}
}
}
/// The window mode of the Ruffle player.
///
/// This setting controls how the Ruffle container is layered and rendered with other content on
/// the page. This setting is only used on web.
///
/// [Apply OBJECT and EMBED tag attributes in Adobe Flash Professional](https://helpx.adobe.com/flash/kb/flash-object-embed-tag-attributes.html)
#[derive(Clone, Collect, Copy, Debug, Eq, PartialEq)]
#[collect(require_static)]
pub enum WindowMode {
/// The Flash content is rendered in its own window and layering is done with the browser's
/// default behavior.
///
/// In Ruffle, this mode functions like `WindowMode::Opaque` and will layer the Flash content
/// together with other HTML elements.
Window,
/// The Flash content is layered together with other HTML elements, and the stage color is
/// opaque. Content can render above or below Ruffle based on CSS rendering order.
Opaque,
/// The Flash content is layered together with other HTML elements, and the stage color is
/// transparent. Content beneath Ruffle will be visible through transparent areas.
Transparent,
/// Request compositing with hardware acceleration when possible.
///
/// This mode has no effect in Ruffle and will function like `WindowMode::Opaque`.
Gpu,
/// Request a direct rendering path, bypassing browser compositing when possible.
///
/// This mode has no effect in Ruffle and will function like `WindowMode::Opaque`.
Direct,
}
impl Default for WindowMode {
fn default() -> WindowMode {
WindowMode::Window
}
}
impl Display for WindowMode {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let s = match *self {
WindowMode::Window => "window",
WindowMode::Opaque => "opaque",
WindowMode::Transparent => "transparent",
WindowMode::Direct => "direct",
WindowMode::Gpu => "gpu",
};
f.write_str(s)
}
}
impl FromStr for WindowMode {
type Err = ParseEnumError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let window_mode = match s.to_ascii_lowercase().as_str() {
"window" => WindowMode::Window,
"opaque" => WindowMode::Opaque,
"transparent" => WindowMode::Transparent,
"direct" => WindowMode::Direct,
"gpu" => WindowMode::Gpu,
_ => return Err(ParseEnumError),
};
Ok(window_mode)
}
}

View File

@ -19,7 +19,7 @@ use crate::context::{ActionQueue, ActionType, RenderContext, UpdateContext};
use crate::context_menu::{ContextMenuCallback, ContextMenuItem, ContextMenuState};
use crate::display_object::{
EditText, InteractiveObject, MovieClip, Stage, StageAlign, StageDisplayState, StageQuality,
StageScaleMode, TInteractiveObject,
StageScaleMode, TInteractiveObject, WindowMode,
};
use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult, KeyCode, MouseButton, PlayerEvent};
use crate::external::Value as ExternalValue;
@ -813,6 +813,15 @@ impl Player {
})
}
pub fn set_window_mode(&mut self, scale_mode: &str) {
self.mutate_with_update_context(|context| {
let stage = context.stage;
if let Ok(window_mode) = WindowMode::from_str(scale_mode) {
stage.set_window_mode(context, window_mode);
}
})
}
/// Handle an event sent into the player from the external windowing system
/// or an HTML element.
///

View File

@ -230,7 +230,10 @@ impl BitmapData {
}
impl WebCanvasRenderBackend {
pub fn new(canvas: &HtmlCanvasElement) -> Result<Self, Box<dyn std::error::Error>> {
pub fn new(
canvas: &HtmlCanvasElement,
is_transparent: bool,
) -> Result<Self, Box<dyn std::error::Error>> {
// Request the CanvasRenderingContext2d.
// Disable alpha for possible speedup.
// TODO: Allow user to enable transparent background (transparent wmode in legacy Flash).
@ -238,7 +241,11 @@ impl WebCanvasRenderBackend {
let _ = js_sys::Reflect::set(
&context_options,
&"alpha".into(),
&wasm_bindgen::JsValue::FALSE,
&if is_transparent {
wasm_bindgen::JsValue::TRUE
} else {
wasm_bindgen::JsValue::FALSE
},
);
let context: CanvasRenderingContext2d = canvas
.get_context_with_context_options("2d", &context_options)
@ -636,10 +643,17 @@ impl RenderBackend for WebCanvasRenderBackend {
let width = self.canvas.width();
let height = self.canvas.height();
let color = format!("rgb({}, {}, {})", clear.r, clear.g, clear.b);
self.context.set_fill_style(&color.into());
self.context
.fill_rect(0.0, 0.0, width.into(), height.into());
if clear.a > 0 {
let color = format!("rgba({}, {}, {}, {})", clear.r, clear.g, clear.b, clear.a);
self.context.set_fill_style(&color.into());
let _ = self.context.set_global_composite_operation("copy");
self.context
.fill_rect(0.0, 0.0, width.into(), height.into());
let _ = self.context.set_global_composite_operation("source-over");
} else {
self.context
.clear_rect(0.0, 0.0, width.into(), height.into());
}
self.deactivating_mask = false;
}

View File

@ -76,6 +76,44 @@ export const enum LogLevel {
Trace = "trace",
}
/**
* The window mode of a Ruffle player.
*/
export const enum WindowMode {
/*
* The Flash content is rendered in its own window and layering is done with the browser's
* default behavior.
*
* In Ruffle, this mode functions like `WindowMode::Opaque` and will layer the Flash content
* together with other HTML elements.
*/
Window = "window",
/*
* The Flash content is layered together with other HTML elements, and the stage color is
* opaque. Content can render above or below Ruffle based on CSS rendering order.
*/
Opaque = "opaque",
/*
* The Flash content is layered together with other HTML elements, and the SWF stage color is
* transparent. Content beneath Ruffle will be visible through transparent areas.
*/
Transparent = "transparent",
/*
* Request compositing with hardware acceleration when possible.
* This mode has no effect in Ruffle and will function like `WindowMode.Opaque`.
*/
Direct = "direct",
/*
* Request a direct rendering path, bypassing browser compositing when possible.
* This mode has no effect in Ruffle and will function like `WindowMode::Opaque`.
*/
Gpu = "gpu",
}
/**
* Any options used for loading a movie.
*/
@ -227,6 +265,15 @@ export interface BaseLoadOptions {
* @default "showAll"
*/
scale?: string;
/**
* The window mode of the Ruffle player.
*
* This setting controls how the Ruffle container is layered and rendered with other content on the page.
*
* @default WindowMode.Window
*/
wmode?: WindowMode;
}
/**

View File

@ -11,6 +11,7 @@ import {
workaroundYoutubeMixedContent,
RufflePlayer,
} from "./ruffle-player";
import { WindowMode } from "./load-options";
import { registerElement } from "./register-element";
/**
@ -61,6 +62,9 @@ export class RuffleEmbed extends RufflePlayer {
this.attributes.getNamedItem("quality")?.value ?? "high",
scale:
this.attributes.getNamedItem("scale")?.value ?? "showAll",
wmode:
(this.attributes.getNamedItem("wmode")
?.value as WindowMode) ?? WindowMode.Window,
});
}
}

View File

@ -13,7 +13,7 @@ import {
RufflePlayer,
} from "./ruffle-player";
import { registerElement } from "./register-element";
import { URLLoadOptions } from "./load-options";
import { URLLoadOptions, WindowMode } from "./load-options";
import { RuffleEmbed } from "./ruffle-embed";
/**
@ -126,6 +126,7 @@ export class RuffleObject extends RufflePlayer {
const salign = findCaseInsensitive(this.params, "salign", "");
const quality = findCaseInsensitive(this.params, "quality", "high");
const scale = findCaseInsensitive(this.params, "scale", "showAll");
const wmode = findCaseInsensitive(this.params, "wmode", "window");
if (url) {
const options: URLLoadOptions = { url };
@ -152,6 +153,9 @@ export class RuffleObject extends RufflePlayer {
if (scale) {
options.scale = scale;
}
if (wmode) {
options.wmode = wmode as WindowMode;
}
// Kick off the SWF download.
this.load(options);

View File

@ -145,6 +145,8 @@ pub struct Config {
scale: Option<String>,
wmode: Option<String>,
#[serde(rename = "warnOnUnsupportedContent")]
warn_on_unsupported_content: bool,
@ -163,6 +165,7 @@ impl Default for Config {
salign: Some("".to_owned()),
quality: Some("high".to_owned()),
scale: Some("showAll".to_owned()),
wmode: Some("opaque".to_owned()),
background_color: Default::default(),
letterbox: Default::default(),
upgrade_to_https: true,
@ -465,7 +468,8 @@ impl Ruffle {
let window = web_sys::window().ok_or("Expected window")?;
let document = window.document().ok_or("Expected document")?;
let (canvas, renderer) = create_renderer(&document).await?;
let (canvas, renderer) = create_renderer(&document, &config).await?;
parent
.append_child(&canvas.clone().into())
.into_js_result()?;
@ -507,6 +511,7 @@ impl Ruffle {
core.set_stage_align(config.salign.as_deref().unwrap_or(""));
core.set_quality(config.quality.as_deref().unwrap_or("high"));
core.set_scale_mode(config.scale.as_deref().unwrap_or("showAll"));
core.set_window_mode(config.wmode.as_deref().unwrap_or("window"));
// Create the external interface.
if allow_script_access {
@ -1204,10 +1209,13 @@ fn external_to_js_value(external: ExternalValue) -> JsValue {
async fn create_renderer(
document: &web_sys::Document,
config: &Config,
) -> Result<(HtmlCanvasElement, Box<dyn RenderBackend>), Box<dyn Error>> {
#[cfg(not(any(feature = "canvas", feature = "webgl", feature = "wgpu")))]
std::compile_error!("You must enable one of the render backend features (e.g., webgl).");
let _is_transparent = config.wmode.as_deref() == Some("transparent");
// Try to create a backend, falling through to the next backend on failure.
// We must recreate the canvas each attempt, as only a single context may be created per canvas
// with `getContext`.
@ -1244,7 +1252,7 @@ async fn create_renderer(
.into_js_result()?
.dyn_into()
.map_err(|_| "Expected HtmlCanvasElement")?;
match ruffle_render_webgl::WebGlRenderBackend::new(&canvas, true) {
match ruffle_render_webgl::WebGlRenderBackend::new(&canvas, _is_transparent) {
Ok(renderer) => return Ok((canvas, Box::new(renderer))),
Err(error) => log::error!("Error creating WebGL renderer: {}", error),
}
@ -1258,7 +1266,7 @@ async fn create_renderer(
.into_js_result()?
.dyn_into()
.map_err(|_| "Expected HtmlCanvasElement")?;
match ruffle_render_canvas::WebCanvasRenderBackend::new(&canvas) {
match ruffle_render_canvas::WebCanvasRenderBackend::new(&canvas, _is_transparent) {
Ok(renderer) => return Ok((canvas, Box::new(renderer))),
Err(error) => log::error!("Error creating canvas renderer: {}", error),
}