From b134cb2bc6eadb23256bdd648831a8b4e4f6bcb1 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Tue, 22 Oct 2024 18:49:36 +0200 Subject: [PATCH] Add `sdf` sprite support with `/sdf_sprite/...` endpoint (#1492) Given a set of SVGs as a sprite source X, Martin will now generate two endpoints: `/sprite/X` and `/sdf_sprite/X`, with the second endpoint serving [signed distance field](https://en.wikipedia.org/wiki/Signed_distance_function) images. Closes #1075 --- docs/src/sources-sprites.md | 16 ++++++- docs/src/using.md | 25 +++++----- martin/src/sprites/mod.rs | 44 ++++++++++++------ martin/src/srv/server.rs | 4 +- martin/src/srv/sprites.rs | 37 +++++++++++++-- tests/expected/configured/sdf_spr_cmp.json | 34 ++++++++++++++ tests/expected/configured/sdf_spr_cmp.png | Bin 0 -> 945 bytes tests/expected/configured/sdf_spr_cmp.png.txt | 1 + tests/expected/configured/sdf_spr_cmp_2.json | 34 ++++++++++++++ tests/expected/configured/sdf_spr_cmp_2.png | Bin 0 -> 1863 bytes .../expected/configured/sdf_spr_cmp_2.png.txt | 1 + tests/expected/configured/sdf_spr_mysrc.json | 10 ++++ tests/expected/configured/sdf_spr_mysrc.png | Bin 0 -> 675 bytes .../expected/configured/sdf_spr_mysrc.png.txt | 1 + tests/expected/configured/sdf_spr_src1.json | 26 +++++++++++ tests/expected/configured/sdf_spr_src1.png | Bin 0 -> 900 bytes .../expected/configured/sdf_spr_src1.png.txt | 1 + tests/expected/configured/sdf_spr_src1_.json | 26 +++++++++++ tests/expected/configured/sdf_spr_src1_.png | Bin 0 -> 1699 bytes .../expected/configured/sdf_spr_src1_.png.txt | 1 + .../fixtures/sprites/expected/all_1_sdf.json | 34 ++++++++++++++ tests/fixtures/sprites/expected/all_1_sdf.png | Bin 0 -> 945 bytes .../fixtures/sprites/expected/all_2_sdf.json | 34 ++++++++++++++ tests/fixtures/sprites/expected/all_2_sdf.png | Bin 0 -> 1863 bytes .../fixtures/sprites/expected/src1_1_sdf.json | 26 +++++++++++ .../fixtures/sprites/expected/src1_1_sdf.png | Bin 0 -> 900 bytes .../fixtures/sprites/expected/src1_2_sdf.json | 26 +++++++++++ .../fixtures/sprites/expected/src1_2_sdf.png | Bin 0 -> 1699 bytes .../fixtures/sprites/expected/src2_1_sdf.json | 10 ++++ .../fixtures/sprites/expected/src2_1_sdf.png | Bin 0 -> 377 bytes .../fixtures/sprites/expected/src2_2_sdf.json | 10 ++++ .../fixtures/sprites/expected/src2_2_sdf.png | Bin 0 -> 675 bytes tests/test.sh | 36 +++++++++----- 33 files changed, 394 insertions(+), 43 deletions(-) create mode 100644 tests/expected/configured/sdf_spr_cmp.json create mode 100644 tests/expected/configured/sdf_spr_cmp.png create mode 100644 tests/expected/configured/sdf_spr_cmp.png.txt create mode 100644 tests/expected/configured/sdf_spr_cmp_2.json create mode 100644 tests/expected/configured/sdf_spr_cmp_2.png create mode 100644 tests/expected/configured/sdf_spr_cmp_2.png.txt create mode 100644 tests/expected/configured/sdf_spr_mysrc.json create mode 100644 tests/expected/configured/sdf_spr_mysrc.png create mode 100644 tests/expected/configured/sdf_spr_mysrc.png.txt create mode 100644 tests/expected/configured/sdf_spr_src1.json create mode 100644 tests/expected/configured/sdf_spr_src1.png create mode 100644 tests/expected/configured/sdf_spr_src1.png.txt create mode 100644 tests/expected/configured/sdf_spr_src1_.json create mode 100644 tests/expected/configured/sdf_spr_src1_.png create mode 100644 tests/expected/configured/sdf_spr_src1_.png.txt create mode 100644 tests/fixtures/sprites/expected/all_1_sdf.json create mode 100644 tests/fixtures/sprites/expected/all_1_sdf.png create mode 100644 tests/fixtures/sprites/expected/all_2_sdf.json create mode 100644 tests/fixtures/sprites/expected/all_2_sdf.png create mode 100644 tests/fixtures/sprites/expected/src1_1_sdf.json create mode 100644 tests/fixtures/sprites/expected/src1_1_sdf.png create mode 100644 tests/fixtures/sprites/expected/src1_2_sdf.json create mode 100644 tests/fixtures/sprites/expected/src1_2_sdf.png create mode 100644 tests/fixtures/sprites/expected/src2_1_sdf.json create mode 100644 tests/fixtures/sprites/expected/src2_1_sdf.png create mode 100644 tests/fixtures/sprites/expected/src2_2_sdf.json create mode 100644 tests/fixtures/sprites/expected/src2_2_sdf.png diff --git a/docs/src/sources-sprites.md b/docs/src/sources-sprites.md index 5b24cb8e1..ac865b12c 100644 --- a/docs/src/sources-sprites.md +++ b/docs/src/sources-sprites.md @@ -1,6 +1,7 @@ ## Sprite Sources -Given a directory with SVG images, Martin will generate a sprite -- a JSON index and a PNG image, for both low and highresolution displays. The SVG filenames without extension will be used as the sprites' image IDs (remember that one sprite and thus `sprite_id` contains multiple images). +Given a directory with SVG images, Martin will generate a sprite -- a JSON index and a PNG image, for both low and highresolution displays. +The SVG filenames without extension will be used as the sprites' image IDs (remember that one sprite and thus `sprite_id` contains multiple images). The images are searched recursively in the given directory, so subdirectory names will be used as prefixes for the image IDs. For example `icons/bicycle.svg` will be available as `icons/bicycle` sprite image. @@ -40,6 +41,19 @@ the PNG, there is a high DPI version available at `/sprite/@2x.json`. } ``` +##### Coloring at runtime via Signed Distance Fields (SDFs) + +If you want to set the color of a sprite at runtime, you will need use the [Signed Distance Fields (SDFs)](https://steamcdn-a.akamaihd.net/apps/valve/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf)-endpoints. +For example, maplibre does support the image being modified via the [`icon-color`](https://maplibre.org/maplibre-style-spec/layers/#icon-color) and [`icon-halo-color`](https://maplibre.org/maplibre-style-spec/layers/#icon-halo-color) properties if using SDFs. + +SDFs have the significant **downside of only allowing one color**. +If you want multiple colors, you will need to layer icons on top of each other. + +The following APIs are available: + +- `/sdf_sprite/.json` for getting a sprite index as SDF and +- `/sdf_sprite/.png` for getting sprite PNGs as SDF + #### Combining Multiple Sprites Multiple `sprite_id` values can be combined into one sprite with the same pattern as for tile diff --git a/docs/src/using.md b/docs/src/using.md index 82c11ee41..3e5c40567 100644 --- a/docs/src/using.md +++ b/docs/src/using.md @@ -2,18 +2,19 @@ Martin data is available via the HTTP `GET` endpoints: -| URL | Description | -|-----------------------------------------|------------------------------------------------| -| `/` | Web UI | -| `/catalog` | [List of all sources](#catalog) | -| `/{sourceID}` | [Source TileJSON](#source-tilejson) | -| `/{sourceID}/{z}/{x}/{y}` | Map Tiles | -| `/{source1},…,{sourceN}` | [Composite Source TileJSON](#source-tilejson) | -| `/{source1},…,{sourceN}/{z}/{x}/{y}` | [Composite Source Tiles](sources-composite.md) | -| `/sprite/{spriteID}[@2x].{json,png}` | [Sprite sources](sources-sprites.md) | -| `/font/{font}/{start}-{end}` | [Font source](sources-fonts.md) | -| `/font/{font1},…,{fontN}/{start}-{end}` | [Composite Font source](sources-fonts.md) | -| `/health` | Martin server health check: returns 200 `OK` | +| URL | Description | +|------------------------------------------|------------------------------------------------| +| `/` | Web UI | +| `/catalog` | [List of all sources](#catalog) | +| `/{sourceID}` | [Source TileJSON](#source-tilejson) | +| `/{sourceID}/{z}/{x}/{y}` | Map Tiles | +| `/{source1},…,{sourceN}` | [Composite Source TileJSON](#source-tilejson) | +| `/{source1},…,{sourceN}/{z}/{x}/{y}` | [Composite Source Tiles](sources-composite.md) | +| `/sprite/{spriteID}[@2x].{json,png}` | [Sprite sources](sources-sprites.md) | +| `/sdf_sprite/{spriteID}[@2x].{json,png}` | [SDF Sprite sources](sources-sprites.md) | +| `/font/{font}/{start}-{end}` | [Font source](sources-fonts.md) | +| `/font/{font1},…,{fontN}/{start}-{end}` | [Composite Font source](sources-fonts.md) | +| `/health` | Martin server health check: returns 200 `OK` | ### Duplicate Source ID diff --git a/martin/src/sprites/mod.rs b/martin/src/sprites/mod.rs index 07e1c769a..033f9631f 100644 --- a/martin/src/sprites/mod.rs +++ b/martin/src/sprites/mod.rs @@ -146,7 +146,7 @@ impl SpriteSources { /// Given a list of IDs in a format "id1,id2,id3", return a spritesheet with them all. /// `ids` may optionally end with "@2x" to request a high-DPI spritesheet. - pub async fn get_sprites(&self, ids: &str) -> SpriteResult { + pub async fn get_sprites(&self, ids: &str, as_sdf: bool) -> SpriteResult { let (ids, dpi) = if let Some(ids) = ids.strip_suffix("@2x") { (ids, 2) } else { @@ -162,7 +162,7 @@ impl SpriteSources { }) .collect::>>()?; - get_spritesheet(sprite_ids.into_iter(), dpi).await + get_spritesheet(sprite_ids.into_iter(), dpi, as_sdf).await } } @@ -175,6 +175,7 @@ async fn parse_sprite( name: String, path: PathBuf, pixel_ratio: u8, + as_sdf: bool, ) -> SpriteResult<(String, Sprite)> { let on_err = |e| SpriteError::IoError(e, path.clone()); @@ -186,7 +187,12 @@ async fn parse_sprite( let tree = Tree::from_data(&buffer, &Options::default()) .map_err(|e| SpriteParsingError(e, path.clone()))?; - let sprite = Sprite::new(tree, pixel_ratio).ok_or_else(|| SpriteInstError(path.clone()))?; + let sprite = if as_sdf { + Sprite::new_sdf(tree, pixel_ratio) + } else { + Sprite::new(tree, pixel_ratio) + }; + let sprite = sprite.ok_or_else(|| SpriteInstError(path.clone()))?; Ok((name, sprite)) } @@ -194,6 +200,7 @@ async fn parse_sprite( pub async fn get_spritesheet( sources: impl Iterator, pixel_ratio: u8, + as_sdf: bool, ) -> SpriteResult { // Asynchronously load all SVG files from the given sources let mut futures = Vec::new(); @@ -203,11 +210,14 @@ pub async fn get_spritesheet( for path in paths { let name = sprite_name(&path, &source.path) .map_err(|e| SpriteProcessingError(e, source.path.clone()))?; - futures.push(parse_sprite(name, path, pixel_ratio)); + futures.push(parse_sprite(name, path, pixel_ratio, as_sdf)); } } let sprites = try_join_all(futures).await?; let mut builder = SpritesheetBuilder::new(); + if as_sdf { + builder.make_sdf(); + } builder.sprites(sprites.into_iter().collect()); // TODO: decide if this is needed and/or configurable @@ -234,24 +244,32 @@ mod tests { let sprites = SpriteSources::resolve(&mut cfg).unwrap().0; assert_eq!(sprites.len(), 2); - test_src(sprites.values(), 1, "all_1").await; - test_src(sprites.values(), 2, "all_2").await; + //.sdf => generate sdf from png, add sdf == true + //- => does not generate sdf, omits sdf == true + for extension in ["_sdf", ""] { + test_src(sprites.values(), 1, "all_1", extension).await; + test_src(sprites.values(), 2, "all_2", extension).await; - test_src(sprites.get("src1").into_iter(), 1, "src1_1").await; - test_src(sprites.get("src1").into_iter(), 2, "src1_2").await; + test_src(sprites.get("src1").into_iter(), 1, "src1_1", extension).await; + test_src(sprites.get("src1").into_iter(), 2, "src1_2", extension).await; - test_src(sprites.get("src2").into_iter(), 1, "src2_1").await; - test_src(sprites.get("src2").into_iter(), 2, "src2_2").await; + test_src(sprites.get("src2").into_iter(), 1, "src2_1", extension).await; + test_src(sprites.get("src2").into_iter(), 2, "src2_2", extension).await; + } } async fn test_src( sources: impl Iterator, pixel_ratio: u8, filename: &str, + extension: &str, ) { - let path = PathBuf::from(format!("../tests/fixtures/sprites/expected/{filename}")); - - let sprites = get_spritesheet(sources, pixel_ratio).await.unwrap(); + let path = PathBuf::from(format!( + "../tests/fixtures/sprites/expected/{filename}{extension}" + )); + let sprites = get_spritesheet(sources, pixel_ratio, extension == "_sdf") + .await + .unwrap(); let mut json = serde_json::to_string_pretty(sprites.get_index()).unwrap(); json.push('\n'); let png = sprites.encode_png().unwrap(); diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index c4f3e0f3a..75af0e6b2 100755 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -114,7 +114,9 @@ pub fn router(cfg: &mut web::ServiceConfig, #[allow(unused_variables)] usr_cfg: .service(get_tile); #[cfg(feature = "sprites")] - cfg.service(crate::srv::sprites::get_sprite_json) + cfg.service(crate::srv::sprites::get_sprite_sdf_json) + .service(crate::srv::sprites::get_sprite_json) + .service(crate::srv::sprites::get_sprite_sdf_png) .service(crate::srv::sprites::get_sprite_png); #[cfg(feature = "fonts")] diff --git a/martin/src/srv/sprites.rs b/martin/src/srv/sprites.rs index 55a61a81b..d9d4c2845 100644 --- a/martin/src/srv/sprites.rs +++ b/martin/src/srv/sprites.rs @@ -15,7 +15,18 @@ async fn get_sprite_png( path: Path, sprites: Data, ) -> ActixResult { - let sheet = get_sprite(&path, &sprites).await?; + let sheet = get_sprite(&path, &sprites, false).await?; + Ok(HttpResponse::Ok() + .content_type(ContentType::png()) + .body(sheet.encode_png().map_err(map_internal_error)?)) +} + +#[route("/sdf_sprite/{source_ids}.png", method = "GET", method = "HEAD")] +async fn get_sprite_sdf_png( + path: Path, + sprites: Data, +) -> ActixResult { + let sheet = get_sprite(&path, &sprites, true).await?; Ok(HttpResponse::Ok() .content_type(ContentType::png()) .body(sheet.encode_png().map_err(map_internal_error)?)) @@ -31,13 +42,31 @@ async fn get_sprite_json( path: Path, sprites: Data, ) -> ActixResult { - let sheet = get_sprite(&path, &sprites).await?; + let sheet = get_sprite(&path, &sprites, false).await?; + Ok(HttpResponse::Ok().json(sheet.get_index())) +} + +#[route( + "/sdf_sprite/{source_ids}.json", + method = "GET", + method = "HEAD", + wrap = "middleware::Compress::default()" +)] +async fn get_sprite_sdf_json( + path: Path, + sprites: Data, +) -> ActixResult { + let sheet = get_sprite(&path, &sprites, true).await?; Ok(HttpResponse::Ok().json(sheet.get_index())) } -async fn get_sprite(path: &SourceIDsRequest, sprites: &SpriteSources) -> ActixResult { +async fn get_sprite( + path: &SourceIDsRequest, + sprites: &SpriteSources, + as_sdf: bool, +) -> ActixResult { sprites - .get_sprites(&path.source_ids) + .get_sprites(&path.source_ids, as_sdf) .await .map_err(|e| match e { SpriteError::SpriteNotFound(_) => ErrorNotFound(e.to_string()), diff --git a/tests/expected/configured/sdf_spr_cmp.json b/tests/expected/configured/sdf_spr_cmp.json new file mode 100644 index 000000000..7a179b175 --- /dev/null +++ b/tests/expected/configured/sdf_spr_cmp.json @@ -0,0 +1,34 @@ +{ + "another_bicycle": { + "height": 21, + "pixelRatio": 1, + "width": 21, + "x": 26, + "y": 22, + "sdf": true + }, + "bear": { + "height": 22, + "pixelRatio": 1, + "width": 22, + "x": 26, + "y": 0, + "sdf": true + }, + "bicycle": { + "height": 21, + "pixelRatio": 1, + "width": 21, + "x": 0, + "y": 26, + "sdf": true + }, + "sub/circle": { + "height": 26, + "pixelRatio": 1, + "width": 26, + "x": 0, + "y": 0, + "sdf": true + } +} diff --git a/tests/expected/configured/sdf_spr_cmp.png b/tests/expected/configured/sdf_spr_cmp.png new file mode 100644 index 0000000000000000000000000000000000000000..0575b9f7d803d2775d1637e87ef2b5623f1cc860 GIT binary patch literal 945 zcmV;i15W&jP)5PcUex#o)GU&xI}KJWqL2jW9~A-@n#rN5AHPV8?$RfOCWMF>@{Yv?bm;T-sd zb~GCA){L-Nr0jsSYtPK{yq%p{$hbg&D}=Z~g!n)rdK%K(02iX&Yv|qK76bH21~j{C z*!8S?F}lYHUk_x2p(qOk&4+M$V?1EUE{8N5H|%a|_t)0w328$TJXz&yfrx1RghVu6 z@QATMJ<`hAjSxBa_txkQOO!Aq12b z?k7l*iR%q^Bo)s~2AMDf6U6{2R;*Pa$17&YNM zA?iv13~O%5m2pC=Ej``C)BpoAa_&FqrIrscgs>V4K*8U-$Cm!0DXT3itZIN;Jm8U; zZ@SkI8HiPm;SOVF`xh#u$uY^!j7gJzU+|16)*RY#*`TaruphO6(V9%p94To$kfwzD z4ugFwz|O`HgJ@-|oFmX;utdty4r+iB8>M#4EJh!3D?o+ZYBn;WT`CmT%vzleATQfV z`rHUKz!K&NneSH9<+L|N31;$PDy40b_Tj$8W0OO0xf7ChD$a#X(y#`2YavOe=@r*r zK0lm)|DWUvzin9=kR~E26r_0aK?@LCyB}PA+$mmTCIAsa1OSyf<}>ps6+vU0wao2a z1%TsoadNTix(JTQ#A<^16y?Q<<@%DXYC0p9%v; z0DSp`jv$pUcI(7N;KgUl9*~Y&vyB&@N3$qrTeaq>efiYq!0wZ*)xP=gsZX+%ky>-+ zdGV=gQPe2udfh|d&8IGoBB5II>G$SSmv@ma^t$8Ar|0nJ({uRq=`;NKJlFX@vN3Um T+6cww00000NkvXXu0mjfF6G3o literal 0 HcmV?d00001 diff --git a/tests/expected/configured/sdf_spr_cmp.png.txt b/tests/expected/configured/sdf_spr_cmp.png.txt new file mode 100644 index 000000000..8f3852018 --- /dev/null +++ b/tests/expected/configured/sdf_spr_cmp.png.txt @@ -0,0 +1 @@ +tests/output/configured/sdf_spr_cmp.png: PNG image data, 48 x 47, 8-bit gray+alpha, non-interlaced diff --git a/tests/expected/configured/sdf_spr_cmp_2.json b/tests/expected/configured/sdf_spr_cmp_2.json new file mode 100644 index 000000000..7da6e3238 --- /dev/null +++ b/tests/expected/configured/sdf_spr_cmp_2.json @@ -0,0 +1,34 @@ +{ + "another_bicycle": { + "height": 36, + "pixelRatio": 2, + "width": 36, + "x": 84, + "y": 0, + "sdf": true + }, + "bear": { + "height": 38, + "pixelRatio": 2, + "width": 38, + "x": 46, + "y": 0, + "sdf": true + }, + "bicycle": { + "height": 36, + "pixelRatio": 2, + "width": 36, + "x": 84, + "y": 36, + "sdf": true + }, + "sub/circle": { + "height": 46, + "pixelRatio": 2, + "width": 46, + "x": 0, + "y": 0, + "sdf": true + } +} diff --git a/tests/expected/configured/sdf_spr_cmp_2.png b/tests/expected/configured/sdf_spr_cmp_2.png new file mode 100644 index 0000000000000000000000000000000000000000..8f370ed6dd5cce870bb272483466b87a21264e78 GIT binary patch literal 1863 zcmV-N2e|l&P)6Q$ zFq+V>3RRW6iYliys?lUDK+bx78IO$245qXNmRMnxHP+c+lP23!Ws?mWtg*@pODvFW zpIG4wU)f6Bx5ZaJv%;s$wV$V6`$gBzseQs=g}TEYha63jL-yIBY1jsx{V3t?vY)+z!Ars)1D&doY?2@v18C#oe|@n;`g;U5pp9by7q(? ze)VaLoYxjvVmUE*%Pg_@QQWVBJ?6|_+ToTy_dF1pJkY1hrLd1VU{5+ro3`Wr;Dv|* zKlv6TZ>MBO*M8$C10tUJfg=~&WW&X6t_W9VCD;qPJPGraSH`?E=2gEu(W66~GmfRR z^lP?+@JPgn2_uFK_)d%Cscv#in_C79b#=s}khgL*`sKI2zgUbbvohFcB8G;Ul`%uZ zzT-kV+eM}wYk8X^({2<$6$Kg+~d-t%asj|;Yvolq<|NvX*3xpVyjqV z`D3^}hbQou{BW;JT>xhIl5=$-~Ed7;X1 zb>puh&BST}rXkysfH?c$O!dj^!vPHr+fjEcL92 zzqs(cERh8^v7jY9z0-!^l4ph*_#-7Z&15Dh{VFz3W&)mQyrpA4$H?u{gZaw6JxMVk zl?BRxW!N|J;Tt7HX5?{l~AfuN)P7P4b$AiXA-5n zk{c{&NnsH^8TEzwl?lekHfBfihWFV;q3g(uTEG>b9&x3B)ksd9mIXd2K6mZ!C4D04 zx^4eIX-4k4V;C}936q;$hCE3#n>cW<67Y>rFPcfra4idT`Nhb7dFIm6(<@^cs@F2X z7yz*eKWF(Sn+FVKVK6@lhE|5{* z>O(^UH{+SaG;caP(*O(=In>WBT?MX|UEz_uiGNT);C^^uSBx?`3tb9SPa0(J?+|Ga zEa~)T30NK+M9jSA1^j?QnQ2pjit9L1-^+a4^wIq?^M?d*Fb|d&&kH!tq6Du%)wciI z-mk3j$?iYV?G7OQ z+Vi+^<@tP#<#f=}X6|7kDCPDifggCCL(p3GOueKRPdsDOQgh0*WWH*Uns)O0eOgjX z3qLJstsR@yxWc-t$+|7$<0Y&o9+p_#dty=4KmWvI(N^0^UX>Gl)G507s%sh0CWRp> zwM?guMg2-G&Brb^{y|gAc*|`^e*Cf7-V#OW*LOjEsoB!vA+tCr=-1+5Q{(@{F! zAicmZ>Yx7x04!)*#y-O==?_EB4_4nLSV-29NF77k(B{5RrL8g7>q*Koj=J!U2 zn5Nt}H`&CL0!?#d19{I(k4a43pUtAi#%~0b}>o!Y`M-AJoH6Ar< zGuC+24SD7okGdg;s`02z3svJ$n-;3Zqc$y6jYn--s2Y#jv`{r3wP`cgc+?Ykh8mCm z0&roCNBx0I*Lc+3q)XR$)Hi6&P~%b0X{mCJM_m(-(ls7+Pc2K=c+`FIP`bvW?#sgp zH6Hce0H{>sQRgk5N;Mw!-o&d^<5BNz+W%Ym_BV#=4rJQV9dQ5v002ovPDHLkV1jL* BqW%B? literal 0 HcmV?d00001 diff --git a/tests/expected/configured/sdf_spr_cmp_2.png.txt b/tests/expected/configured/sdf_spr_cmp_2.png.txt new file mode 100644 index 000000000..803547c0b --- /dev/null +++ b/tests/expected/configured/sdf_spr_cmp_2.png.txt @@ -0,0 +1 @@ +tests/output/configured/sdf_spr_cmp_2.png: PNG image data, 120 x 72, 8-bit gray+alpha, non-interlaced diff --git a/tests/expected/configured/sdf_spr_mysrc.json b/tests/expected/configured/sdf_spr_mysrc.json new file mode 100644 index 000000000..03636083d --- /dev/null +++ b/tests/expected/configured/sdf_spr_mysrc.json @@ -0,0 +1,10 @@ +{ + "bicycle": { + "height": 36, + "pixelRatio": 2, + "width": 36, + "x": 0, + "y": 0, + "sdf": true + } +} diff --git a/tests/expected/configured/sdf_spr_mysrc.png b/tests/expected/configured/sdf_spr_mysrc.png new file mode 100644 index 0000000000000000000000000000000000000000..78f861308997171ea138243ab0f386c11d099fa1 GIT binary patch literal 675 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 literal 0 HcmV?d00001 diff --git a/tests/expected/configured/sdf_spr_src1_.png.txt b/tests/expected/configured/sdf_spr_src1_.png.txt new file mode 100644 index 000000000..b1c908d2d --- /dev/null +++ b/tests/expected/configured/sdf_spr_src1_.png.txt @@ -0,0 +1 @@ +tests/output/configured/sdf_spr_src1_.png: PNG image data, 120 x 46, 8-bit gray+alpha, non-interlaced diff --git a/tests/fixtures/sprites/expected/all_1_sdf.json b/tests/fixtures/sprites/expected/all_1_sdf.json new file mode 100644 index 000000000..7a179b175 --- /dev/null +++ b/tests/fixtures/sprites/expected/all_1_sdf.json @@ -0,0 +1,34 @@ +{ + "another_bicycle": { + "height": 21, + "pixelRatio": 1, + "width": 21, + "x": 26, + "y": 22, + "sdf": true + }, + "bear": { + "height": 22, + "pixelRatio": 1, + "width": 22, + "x": 26, + "y": 0, + "sdf": true + }, + "bicycle": { + "height": 21, + "pixelRatio": 1, + "width": 21, + "x": 0, + "y": 26, + "sdf": true + }, + "sub/circle": { + "height": 26, + "pixelRatio": 1, + "width": 26, + "x": 0, + "y": 0, + "sdf": true + } +} diff --git a/tests/fixtures/sprites/expected/all_1_sdf.png b/tests/fixtures/sprites/expected/all_1_sdf.png new file mode 100644 index 0000000000000000000000000000000000000000..0575b9f7d803d2775d1637e87ef2b5623f1cc860 GIT binary patch literal 945 zcmV;i15W&jP)5PcUex#o)GU&xI}KJWqL2jW9~A-@n#rN5AHPV8?$RfOCWMF>@{Yv?bm;T-sd zb~GCA){L-Nr0jsSYtPK{yq%p{$hbg&D}=Z~g!n)rdK%K(02iX&Yv|qK76bH21~j{C z*!8S?F}lYHUk_x2p(qOk&4+M$V?1EUE{8N5H|%a|_t)0w328$TJXz&yfrx1RghVu6 z@QATMJ<`hAjSxBa_txkQOO!Aq12b z?k7l*iR%q^Bo)s~2AMDf6U6{2R;*Pa$17&YNM zA?iv13~O%5m2pC=Ej``C)BpoAa_&FqrIrscgs>V4K*8U-$Cm!0DXT3itZIN;Jm8U; zZ@SkI8HiPm;SOVF`xh#u$uY^!j7gJzU+|16)*RY#*`TaruphO6(V9%p94To$kfwzD z4ugFwz|O`HgJ@-|oFmX;utdty4r+iB8>M#4EJh!3D?o+ZYBn;WT`CmT%vzleATQfV z`rHUKz!K&NneSH9<+L|N31;$PDy40b_Tj$8W0OO0xf7ChD$a#X(y#`2YavOe=@r*r zK0lm)|DWUvzin9=kR~E26r_0aK?@LCyB}PA+$mmTCIAsa1OSyf<}>ps6+vU0wao2a z1%TsoadNTix(JTQ#A<^16y?Q<<@%DXYC0p9%v; z0DSp`jv$pUcI(7N;KgUl9*~Y&vyB&@N3$qrTeaq>efiYq!0wZ*)xP=gsZX+%ky>-+ zdGV=gQPe2udfh|d&8IGoBB5II>G$SSmv@ma^t$8Ar|0nJ({uRq=`;NKJlFX@vN3Um T+6cww00000NkvXXu0mjfF6G3o literal 0 HcmV?d00001 diff --git a/tests/fixtures/sprites/expected/all_2_sdf.json b/tests/fixtures/sprites/expected/all_2_sdf.json new file mode 100644 index 000000000..7da6e3238 --- /dev/null +++ b/tests/fixtures/sprites/expected/all_2_sdf.json @@ -0,0 +1,34 @@ +{ + "another_bicycle": { + "height": 36, + "pixelRatio": 2, + "width": 36, + "x": 84, + "y": 0, + "sdf": true + }, + "bear": { + "height": 38, + "pixelRatio": 2, + "width": 38, + "x": 46, + "y": 0, + "sdf": true + }, + "bicycle": { + "height": 36, + "pixelRatio": 2, + "width": 36, + "x": 84, + "y": 36, + "sdf": true + }, + "sub/circle": { + "height": 46, + "pixelRatio": 2, + "width": 46, + "x": 0, + "y": 0, + "sdf": true + } +} diff --git a/tests/fixtures/sprites/expected/all_2_sdf.png b/tests/fixtures/sprites/expected/all_2_sdf.png new file mode 100644 index 0000000000000000000000000000000000000000..8f370ed6dd5cce870bb272483466b87a21264e78 GIT binary patch literal 1863 zcmV-N2e|l&P)6Q$ zFq+V>3RRW6iYliys?lUDK+bx78IO$245qXNmRMnxHP+c+lP23!Ws?mWtg*@pODvFW zpIG4wU)f6Bx5ZaJv%;s$wV$V6`$gBzseQs=g}TEYha63jL-yIBY1jsx{V3t?vY)+z!Ars)1D&doY?2@v18C#oe|@n;`g;U5pp9by7q(? ze)VaLoYxjvVmUE*%Pg_@QQWVBJ?6|_+ToTy_dF1pJkY1hrLd1VU{5+ro3`Wr;Dv|* zKlv6TZ>MBO*M8$C10tUJfg=~&WW&X6t_W9VCD;qPJPGraSH`?E=2gEu(W66~GmfRR z^lP?+@JPgn2_uFK_)d%Cscv#in_C79b#=s}khgL*`sKI2zgUbbvohFcB8G;Ul`%uZ zzT-kV+eM}wYk8X^({2<$6$Kg+~d-t%asj|;Yvolq<|NvX*3xpVyjqV z`D3^}hbQou{BW;JT>xhIl5=$-~Ed7;X1 zb>puh&BST}rXkysfH?c$O!dj^!vPHr+fjEcL92 zzqs(cERh8^v7jY9z0-!^l4ph*_#-7Z&15Dh{VFz3W&)mQyrpA4$H?u{gZaw6JxMVk zl?BRxW!N|J;Tt7HX5?{l~AfuN)P7P4b$AiXA-5n zk{c{&NnsH^8TEzwl?lekHfBfihWFV;q3g(uTEG>b9&x3B)ksd9mIXd2K6mZ!C4D04 zx^4eIX-4k4V;C}936q;$hCE3#n>cW<67Y>rFPcfra4idT`Nhb7dFIm6(<@^cs@F2X z7yz*eKWF(Sn+FVKVK6@lhE|5{* z>O(^UH{+SaG;caP(*O(=In>WBT?MX|UEz_uiGNT);C^^uSBx?`3tb9SPa0(J?+|Ga zEa~)T30NK+M9jSA1^j?QnQ2pjit9L1-^+a4^wIq?^M?d*Fb|d&&kH!tq6Du%)wciI z-mk3j$?iYV?G7OQ z+Vi+^<@tP#<#f=}X6|7kDCPDifggCCL(p3GOueKRPdsDOQgh0*WWH*Uns)O0eOgjX z3qLJstsR@yxWc-t$+|7$<0Y&o9+p_#dty=4KmWvI(N^0^UX>Gl)G507s%sh0CWRp> zwM?guMg2-G&Brb^{y|gAc*|`^e*Cf7-V#OW*LOjEsoB!vA+tCr=-1+5Q{(@{F! zAicmZ>Yx7x04!)*#y-O==?_EB4_4nLSV-29NF77k(B{5RrL8g7>q*Koj=J!U2 zn5Nt}H`&CL0!?#d19{I(k4a43pUtAi#%~0b}>o!Y`M-AJoH6Ar< zGuC+24SD7okGdg;s`02z3svJ$n-;3Zqc$y6jYn--s2Y#jv`{r3wP`cgc+?Ykh8mCm z0&roCNBx0I*Lc+3q)XR$)Hi6&P~%b0X{mCJM_m(-(ls7+Pc2K=c+`FIP`bvW?#sgp zH6Hce0H{>sQRgk5N;Mw!-o&d^<5BNz+W%Ym_BV#=4rJQV9dQ5v002ovPDHLkV1jL* BqW%B? literal 0 HcmV?d00001 diff --git a/tests/fixtures/sprites/expected/src1_1_sdf.json b/tests/fixtures/sprites/expected/src1_1_sdf.json new file mode 100644 index 000000000..d90ee2d88 --- /dev/null +++ b/tests/fixtures/sprites/expected/src1_1_sdf.json @@ -0,0 +1,26 @@ +{ + "another_bicycle": { + "height": 21, + "pixelRatio": 1, + "width": 21, + "x": 26, + "y": 22, + "sdf": true + }, + "bear": { + "height": 22, + "pixelRatio": 1, + "width": 22, + "x": 26, + "y": 0, + "sdf": true + }, + "sub/circle": { + "height": 26, + "pixelRatio": 1, + "width": 26, + "x": 0, + "y": 0, + "sdf": true + } +} diff --git a/tests/fixtures/sprites/expected/src1_1_sdf.png b/tests/fixtures/sprites/expected/src1_1_sdf.png new file mode 100644 index 0000000000000000000000000000000000000000..77717779ff2c6b09c4c56116630818ca92081691 GIT binary patch literal 900 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/sprites/expected/src2_1_sdf.json b/tests/fixtures/sprites/expected/src2_1_sdf.json new file mode 100644 index 000000000..bf3f7792a --- /dev/null +++ b/tests/fixtures/sprites/expected/src2_1_sdf.json @@ -0,0 +1,10 @@ +{ + "bicycle": { + "height": 21, + "pixelRatio": 1, + "width": 21, + "x": 0, + "y": 0, + "sdf": true + } +} diff --git a/tests/fixtures/sprites/expected/src2_1_sdf.png b/tests/fixtures/sprites/expected/src2_1_sdf.png new file mode 100644 index 0000000000000000000000000000000000000000..1fd1735ffda869dcdc0118e255eeb423d6942529 GIT binary patch literal 377 zcmV-<0fzpGP)ZrE5Jg`@la|&JS8!)b6gfaR!8U1}D}<6O#I7TA28zhCEGvZ-A%r*sSIO6C;>{9; zuU+=cB9B54kY62yNh|D*a4b(q~G%_>k-mv2-=3KC$(#DM=qvNyM8?T=8Mx22= zYTx+A#lVZuj(5*_XRw2Nj)t5W)PFM3bG4Zxdxr@Qp0zkLsC}vLgwImX+2H`n(!fbg zQ4Q@Z`pC)VPg-xp`3ns;@u7tqO2M$J#;xtvM%3Qd9=M>fx6Z4f@!zPeM~VKJL``$` z+?HPGvI4y=BtBLkt*h73eOlM}weG5;Prp{7&VN8xSb77CrsN*w!@OcDoR}ySiRiy{az72OBpSV2Dp%xEgTx%F(Cc2%nL7;cCR){o^dXQrY=Y^{rQ(iX2#WBDr^nl{f;3V0@aVJB< zOD~Qg@0o@kbb$Ww69=LyLH5Rj1_mWLd7#U5_W~f^j~$!>EIL46jBZsGdSSl0q180{ zDROg^SmC>XN~1fW-21BXFt-?Ef*Fi#nH}+n3JZgN;_5f|wpg1_8D)kEt!@P+n3T#n zjTDFid=^K~=FLID7CN=S+`<$TVU+d)6-2Cz8=Jit%!`71gp*XX$Xh7E7&kmrW3PPG z_Wgt%PV&v!{Gf`ed3ueqg~FJVTmc-R#4B&FwsRz$qtS-5S7XYAt8EK41}2!JXnELS zW1el%;1~Y#vu8*vgAo9A4jXw&C|b{nf*BU5XnIrtyaSbhdAq?#j~}Q-_*y5FS)!;2 zbpT5k)qqM2fMo8sY6N!P_=-jlyu$$ynj;(FJO&R-hu-+01qH zYXd)+ajE^wKqA;5^!ifz04NAeZ*7I5<9euH2B8IHKUn!cbjgh91#~X`