avm2: Implement BitmapData.hitTest

This commit is contained in:
Nathan Adams 2023-03-16 09:09:48 +01:00
parent a1176afcce
commit a010bd0f7a
7 changed files with 417 additions and 3 deletions

View File

@ -21,6 +21,7 @@ use std::str::FromStr;
pub use crate::avm2::object::bitmap_data_allocator;
use crate::avm2::parameters::{null_parameter_error, ParametersExt};
use crate::display_object::TDisplayObject;
/// Copy the static data from a given Bitmap into a new BitmapData.
///
@ -667,10 +668,131 @@ pub fn unlock<'gc>(
pub fn hit_test<'gc>(
activation: &mut Activation<'_, 'gc>,
_this: Option<Object<'gc>>,
_args: &[Value<'gc>],
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
avm2_stub_method!(activation, "flash.display.BitmapData", "hitTest");
if let Some(bitmap_data) = this.and_then(|t| t.as_bitmap_data()) {
if !bitmap_data.read().disposed() {
let first_point = args.get_object(activation, 0, "firstPoint")?;
let top_left = (
first_point
.get_public_property("x", activation)?
.coerce_to_i32(activation)?,
first_point
.get_public_property("y", activation)?
.coerce_to_i32(activation)?,
);
let source_threshold = args.get_u32(activation, 1)?;
let compare_object = args.get_object(activation, 2, "secondObject")?;
let point_class = activation.avm2().classes().point;
let rectangle_class = activation.avm2().classes().rectangle;
if compare_object.is_of_type(point_class, activation) {
let test_point = (
compare_object
.get_public_property("x", activation)?
.coerce_to_i32(activation)?
- top_left.0,
compare_object
.get_public_property("y", activation)?
.coerce_to_i32(activation)?
- top_left.1,
);
return Ok(Value::Bool(
bitmap_data
.read()
.hit_test_point(source_threshold, test_point),
));
} else if compare_object.is_of_type(rectangle_class, activation) {
let test_point = (
compare_object
.get_public_property("x", activation)?
.coerce_to_i32(activation)?
- top_left.0,
compare_object
.get_public_property("y", activation)?
.coerce_to_i32(activation)?
- top_left.1,
);
let size = (
compare_object
.get_public_property("width", activation)?
.coerce_to_i32(activation)?,
compare_object
.get_public_property("height", activation)?
.coerce_to_i32(activation)?,
);
return Ok(Value::Bool(bitmap_data.read().hit_test_rectangle(
source_threshold,
test_point,
size,
)));
} else if let Some(other_bmd) = compare_object.as_bitmap_data() {
other_bmd.read().check_valid(activation)?;
let second_point = args.get_object(activation, 3, "secondBitmapDataPoint")?;
let second_point = (
second_point
.get_public_property("x", activation)?
.coerce_to_i32(activation)?,
second_point
.get_public_property("y", activation)?
.coerce_to_i32(activation)?,
);
let second_threshold = args.get_u32(activation, 4)?;
let result = if GcCell::ptr_eq(bitmap_data, other_bmd) {
bitmap_data.read().hit_test_bitmapdata(
top_left,
source_threshold,
None,
second_point,
second_threshold,
)
} else {
bitmap_data.read().hit_test_bitmapdata(
top_left,
source_threshold,
Some(&other_bmd.read()),
second_point,
second_threshold,
)
};
return Ok(Value::Bool(result));
} else if let Some(bitmap) = compare_object
.as_display_object()
.and_then(|dobj| dobj.as_bitmap())
{
let other_bmd = bitmap.bitmap_data_wrapper().sync();
other_bmd.read().check_valid(activation)?;
let second_point = args.get_object(activation, 3, "secondBitmapDataPoint")?;
let second_point = (
second_point
.get_public_property("x", activation)?
.coerce_to_i32(activation)?,
second_point
.get_public_property("y", activation)?
.coerce_to_i32(activation)?,
);
let second_threshold = args.get_u32(activation, 4)?;
return Ok(Value::Bool(bitmap_data.read().hit_test_bitmapdata(
top_left,
source_threshold,
Some(&other_bmd.read()),
second_point,
second_threshold,
)));
} else {
// This is the error message Flash Player produces. Even though it's misleading.
return Err(Error::AvmError(argument_error(
activation,
"Parameter 0 is of the incorrect type. Should be type BitmapData.",
2005,
)?));
}
}
}
Ok(false.into())
}

View File

@ -1362,6 +1362,72 @@ impl<'gc> BitmapData<'gc> {
}
}
pub fn hit_test_point(&self, alpha_threshold: u32, test_point: (i32, i32)) -> bool {
self.get_pixel32(test_point.0, test_point.1).alpha() as u32 >= alpha_threshold
}
pub fn hit_test_rectangle(
&self,
alpha_threshold: u32,
top_left: (i32, i32),
size: (i32, i32),
) -> bool {
for x in 0..size.0 {
for y in 0..size.1 {
if self.get_pixel32(top_left.0 + x, top_left.1 + y).alpha() as u32
>= alpha_threshold
{
return true;
}
}
}
false
}
pub fn hit_test_bitmapdata(
&self,
self_point: (i32, i32),
self_threshold: u32,
test: Option<&BitmapData>,
test_point: (i32, i32),
test_threshold: u32,
) -> bool {
let xd = test_point.0 - self_point.0;
let yd = test_point.1 - self_point.1;
let self_width = self.width as i32;
let self_height = self.height as i32;
let (test_width, test_height) = if let Some(test) = test {
(test.width as i32, test.height as i32)
} else {
(self_width, self_height)
};
let (self_x0, test_x0, width) = if xd < 0 {
(0, -xd, self_width.min(test_width + xd))
} else {
(xd, 0, test_width.min(self_width - xd))
};
let (self_y0, test_y0, height) = if yd < 0 {
(0, -yd, self_height.min(test_height + yd))
} else {
(yd, 0, test_height.min(self_height - yd))
};
for x in 0..width {
for y in 0..height {
let self_is_opaque =
self.hit_test_point(self_threshold, (self_x0 + x, self_y0 + y));
let test_is_opaque = if let Some(test) = test {
test.hit_test_point(test_threshold, (test_x0 + x, test_y0 + y))
} else {
self.hit_test_point(test_threshold, (test_x0 + x, test_y0 + y))
};
if self_is_opaque && test_is_opaque {
return true;
}
}
}
false
}
#[allow(clippy::too_many_arguments)]
pub fn draw(
&mut self,

View File

@ -0,0 +1,138 @@
package {
import flash.display.MovieClip;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.geom.Point;
import flash.geom.Rectangle;
public class Test extends MovieClip {
public function Test() {
var bmd: BitmapData = createImage();
var otherBmd: BitmapData = createImage();
var bitmap: Bitmap = new Bitmap(bmd);
// Testing bmd against bmd, aligns both images so both points overlap and checks for any opaque overlap
trace("/// hitTest with bmd");
test(bmd, new Point(0, 0), 0, bmd, new Point(0, 0), 0);
test(bmd, new Point(1, 1), 0xFF, bmd, new Point(3, 3), 0xA0);
test(bmd, new Point(2, 1), 0xA0, bmd, new Point(1, 3), 0xA0);
test(bmd, new Point(3, 1), 0xA0, bmd, new Point(1, 2), 0xFF);
test(bmd, new Point(0, 0), 0xA0, bmd, new Point(1, 0), 0xFF);
test(bmd, new Point(1, 1), 0xFF, bmd, new Point(1, 1), 0xFF);
trace("");
trace("/// hitTest with other bmd");
test(bmd, new Point(0, 0), 0, otherBmd, new Point(0, 0), 0);
test(bmd, new Point(1, 1), 0xFF, otherBmd, new Point(3, 3), 0xA0);
test(bmd, new Point(2, 1), 0xA0, otherBmd, new Point(1, 3), 0xA0);
test(bmd, new Point(3, 1), 0xA0, otherBmd, new Point(1, 2), 0xFF);
test(bmd, new Point(0, 0), 0xA0, otherBmd, new Point(1, 0), 0xFF);
test(bmd, new Point(1, 1), 0xFF, otherBmd, new Point(1, 1), 0xFF);
trace("");
// Testing bmd against bitmap, same as above
trace("/// hitTest with bitmap");
test(bmd, new Point(0, 0), 0, bitmap, new Point(0, 0), 0);
test(bmd, new Point(1, 1), 0xFF, bitmap, new Point(3, 3), 0xA0);
test(bmd, new Point(2, 1), 0xA0, bitmap, new Point(1, 3), 0xA0);
test(bmd, new Point(3, 1), 0xA0, bitmap, new Point(1, 2), 0xFF);
trace("");
// Testing bmd against rect, offsets the rect by -firstPoint and then looks for any opaque pixel inside rect
trace("/// hitTest with rect");
test(bmd, new Point(0, 0), 0xA0, new Rectangle(2, 2, 2, 2));
test(bmd, new Point(0, 0), 0xFF, new Rectangle(0, 0, 3, 4));
test(bmd, new Point(0, 0), 0xFF, new Rectangle(2, 2, 1, 1));
test(bmd, new Point(2, 2), 0xFF, new Rectangle(4, 4, 1, 1));
trace("");
// Testing bmd against point, offsets the point by -firstPoint and then checks if that pixel is opaque
trace("/// hitTest with point");
test(bmd, new Point(0, 0), 0xA0, new Point(2, 2));
test(bmd, new Point(0, 0), 0xFF, new Point(0, 0));
test(bmd, new Point(0, 0), 0xFF, new Point(2, 2));
test(bmd, new Point(2, 2), 0xFF, new Point(4, 4));
trace("");
trace("/// Error cases")
try {
test(bmd, new Point(0, 0), 0x00, bmd, null);
} catch (error: Error) {
trace("- Error " + error.errorID);
}
try {
test(bmd, new Point(0, 0), 0x00, {});
} catch (error: Error) {
trace("- Error " + error.errorID);
}
}
// BMD looks like: ('-' is no alpha, 'x' is 0xA0, 'X' is 0xFF)
/* 0 1 2 3 4
* 0 - - - - -
* 1 - x x x -
* 2 - x X x -
* 3 - x x x -
* 4 - - - - -
*/
function createImage():BitmapData {
var bmd: BitmapData = new BitmapData(5, 5, true, 0);
for (var x = 1; x <= 3; x++) {
for (var y = 1; y <= 3; y++) {
bmd.setPixel32(x, y, 0xA0FFFFFF);
}
}
bmd.setPixel32(2, 2, 0xFFFFFFFF);
return bmd;
}
function formatPoint(point: Point): String {
if (point) {
return "new Point(" + point.x + ", " + point.y + ")";
} else {
return "null";
}
}
function formatRectangle(rect: Rectangle): String {
if (rect) {
return "new Rectangle(" + rect.x + ", " + rect.y + ", " + rect.width + ", " + rect.height + ")";
} else {
return "null";
}
}
function formatObject(bmd: BitmapData, object: Object): String {
if (object === bmd) {
return "bmd";
} else if (object is Point) {
return formatPoint(object as Point);
} else if (object is Rectangle) {
return formatRectangle(object as Rectangle);
} else if (object is BitmapData) {
return "otherBitmapData";
} else if (object is Bitmap) {
return "otherBitmap";
} else if (object === null) {
return "null";
} else {
return "{}";
}
}
function test(bmd: BitmapData, firstPoint:Point, firstAlphaThreshold:uint, secondObject:Object, secondBitmapDataPoint:Point = null, secondAlphaThreshold:uint = 1) {
trace("// bmd.hitTest(" + formatPoint(firstPoint) + ", " + firstAlphaThreshold + ", " + formatObject(bmd, secondObject) + ", " + formatPoint(secondBitmapDataPoint) + ", " + secondAlphaThreshold + ")");
trace(bmd.hitTest(firstPoint, firstAlphaThreshold, secondObject, secondBitmapDataPoint, secondAlphaThreshold));
trace("");
}
}
}

View File

@ -0,0 +1,87 @@
/// hitTest with bmd
// bmd.hitTest(new Point(0, 0), 0, bmd, new Point(0, 0), 0)
true
// bmd.hitTest(new Point(1, 1), 255, bmd, new Point(3, 3), 160)
false
// bmd.hitTest(new Point(2, 1), 160, bmd, new Point(1, 3), 160)
true
// bmd.hitTest(new Point(3, 1), 160, bmd, new Point(1, 2), 255)
false
// bmd.hitTest(new Point(0, 0), 160, bmd, new Point(1, 0), 255)
true
// bmd.hitTest(new Point(1, 1), 255, bmd, new Point(1, 1), 255)
true
/// hitTest with other bmd
// bmd.hitTest(new Point(0, 0), 0, otherBitmapData, new Point(0, 0), 0)
true
// bmd.hitTest(new Point(1, 1), 255, otherBitmapData, new Point(3, 3), 160)
false
// bmd.hitTest(new Point(2, 1), 160, otherBitmapData, new Point(1, 3), 160)
true
// bmd.hitTest(new Point(3, 1), 160, otherBitmapData, new Point(1, 2), 255)
false
// bmd.hitTest(new Point(0, 0), 160, otherBitmapData, new Point(1, 0), 255)
true
// bmd.hitTest(new Point(1, 1), 255, otherBitmapData, new Point(1, 1), 255)
true
/// hitTest with bitmap
// bmd.hitTest(new Point(0, 0), 0, otherBitmap, new Point(0, 0), 0)
true
// bmd.hitTest(new Point(1, 1), 255, otherBitmap, new Point(3, 3), 160)
false
// bmd.hitTest(new Point(2, 1), 160, otherBitmap, new Point(1, 3), 160)
true
// bmd.hitTest(new Point(3, 1), 160, otherBitmap, new Point(1, 2), 255)
false
/// hitTest with rect
// bmd.hitTest(new Point(0, 0), 160, new Rectangle(2, 2, 2, 2), null, 1)
true
// bmd.hitTest(new Point(0, 0), 255, new Rectangle(0, 0, 3, 4), null, 1)
true
// bmd.hitTest(new Point(0, 0), 255, new Rectangle(2, 2, 1, 1), null, 1)
true
// bmd.hitTest(new Point(2, 2), 255, new Rectangle(4, 4, 1, 1), null, 1)
true
/// hitTest with point
// bmd.hitTest(new Point(0, 0), 160, new Point(2, 2), null, 1)
true
// bmd.hitTest(new Point(0, 0), 255, new Point(0, 0), null, 1)
false
// bmd.hitTest(new Point(0, 0), 255, new Point(2, 2), null, 1)
true
// bmd.hitTest(new Point(2, 2), 255, new Point(4, 4), null, 1)
true
/// Error cases
// bmd.hitTest(new Point(0, 0), 0, bmd, null, 1)
- Error 2007
// bmd.hitTest(new Point(0, 0), 0, {}, null, 1)
- Error 2005

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
num_frames = 1