From 3b1042395521f14cae4df5f9cc6134d62ac36411 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Sun, 16 Oct 2022 19:30:02 +0000 Subject: [PATCH] Add hue/saturation/lightness shift support to AFP renderer to fix neo generator seven boss animation rendering. --- bemani/format/afp/blend/blend.py | 39 ++++++- bemani/format/afp/blend/blendcpp.pyi | 4 +- bemani/format/afp/blend/blendcpp.pyx | 14 ++- bemani/format/afp/blend/blendcppimpl.cxx | 136 ++++++++++++++++++++++- bemani/format/afp/render.py | 39 +++++++ bemani/format/afp/swf.py | 30 ++++- bemani/format/afp/types/__init__.py | 3 +- bemani/format/afp/types/generic.py | 60 ++++++++++ verifylint | 2 +- 9 files changed, 315 insertions(+), 12 deletions(-) diff --git a/bemani/format/afp/blend/blend.py b/bemani/format/afp/blend/blend.py index c30bf59..6fd509e 100644 --- a/bemani/format/afp/blend/blend.py +++ b/bemani/format/afp/blend/blend.py @@ -3,7 +3,7 @@ import signal from PIL import Image # type: ignore from typing import Any, Callable, List, Optional, Sequence, Union -from ..types import Color, Matrix, Point, AAMode +from ..types import Color, HSL, Matrix, Point, AAMode from .perspective import perspective_calculate @@ -186,6 +186,7 @@ def blend_mask_combine( def blend_point( add_color: Color, mult_color: Color, + hsl_shift: HSL, # This should be a sequence of exactly 4 values, either bytes or a tuple. src_color: Sequence[int], # This should be a sequence of exactly 4 values, either bytes or a tuple. @@ -200,6 +201,23 @@ def blend_point( clamp((src_color[3] * mult_color.a) + (255 * add_color.a)), ) + # Only add in HSL shift effects if they exist, since its expensive to + # convert and shift. Also I'm not sure if this should be done before or + # after the add and multiply. + if not hsl_shift.is_identity: + hslcolor = Color( + src_color[0] / 255, src_color[1] / 255, src_color[2] / 255, 1.0 + ).as_hsl() + hslcolor = hslcolor.add(hsl_shift) + newcolor = hslcolor.as_rgb() + + src_color = ( + clamp(newcolor.r * 255), + clamp(newcolor.g * 255), + clamp(newcolor.b * 255), + src_color[3], + ) + if blendfunc == 3: return blend_multiply(dest_color, src_color) # TODO: blend mode 4, which is "screen" blending according to SWF references. I've only seen this @@ -240,6 +258,7 @@ def pixel_renderer( callback: Callable[[Point], Optional[Point]], add_color: Color, mult_color: Color, + hsl_shift: HSL, blendfunc: int, imgbytes: Union[bytes, bytearray], texbytes: Union[bytes, bytearray], @@ -422,7 +441,12 @@ def pixel_renderer( # Finally, blend it with the destination. return blend_point( - add_color, mult_color, average, imgbytes[imgoff : (imgoff + 4)], blendfunc + add_color, + mult_color, + hsl_shift, + average, + imgbytes[imgoff : (imgoff + 4)], + blendfunc, ) else: # Calculate what texture pixel data goes here. @@ -441,6 +465,7 @@ def pixel_renderer( return blend_point( add_color, mult_color, + hsl_shift, texbytes[texoff : (texoff + 4)], imgbytes[imgoff : (imgoff + 4)], blendfunc, @@ -459,6 +484,7 @@ def affine_line_renderer( inverse: Matrix, add_color: Color, mult_color: Color, + hsl_shift: HSL, blendfunc: int, imgbytes: Union[bytes, bytearray], texbytes: Union[bytes, bytearray], @@ -491,6 +517,7 @@ def affine_line_renderer( lambda point: inverse.multiply_point(point), add_color, mult_color, + hsl_shift, blendfunc, imgbytes, texbytes, @@ -505,6 +532,7 @@ def affine_composite( img: Image.Image, add_color: Color, mult_color: Color, + hsl_shift: HSL, transform: Matrix, mask: Optional[Image.Image], blendfunc: int, @@ -577,6 +605,7 @@ def affine_composite( lambda point: inverse.multiply_point(point), add_color, mult_color, + hsl_shift, blendfunc, imgbytes, texbytes, @@ -623,6 +652,7 @@ def affine_composite( inverse, add_color, mult_color, + hsl_shift, blendfunc, imgbytes, texbytes, @@ -676,6 +706,7 @@ def perspective_line_renderer( inverse: Matrix, add_color: Color, mult_color: Color, + hsl_shift: HSL, blendfunc: int, imgbytes: Union[bytes, bytearray], texbytes: Union[bytes, bytearray], @@ -716,6 +747,7 @@ def perspective_line_renderer( perspective_inverse, add_color, mult_color, + hsl_shift, blendfunc, imgbytes, texbytes, @@ -730,6 +762,7 @@ def perspective_composite( img: Image.Image, add_color: Color, mult_color: Color, + hsl_shift: HSL, transform: Matrix, camera: Point, focal_length: float, @@ -804,6 +837,7 @@ def perspective_composite( perspective_inverse, add_color, mult_color, + hsl_shift, blendfunc, imgbytes, texbytes, @@ -852,6 +886,7 @@ def perspective_composite( inverse_matrix, add_color, mult_color, + hsl_shift, blendfunc, imgbytes, texbytes, diff --git a/bemani/format/afp/blend/blendcpp.pyi b/bemani/format/afp/blend/blendcpp.pyi index 29b3715..e78d5cc 100644 --- a/bemani/format/afp/blend/blendcpp.pyi +++ b/bemani/format/afp/blend/blendcpp.pyi @@ -1,13 +1,14 @@ from PIL import Image # type: ignore from typing import Optional -from ..types import Color, Point, Matrix +from ..types import Color, HSL, Point, Matrix def affine_composite( img: Image.Image, add_color: Color, mult_color: Color, + hsl_shift: HSL, transform: Matrix, mask: Optional[Image.Image], blendfunc: int, @@ -22,6 +23,7 @@ def perspective_composite( img: Image.Image, add_color: Color, mult_color: Color, + hsl_shift: HSL, transform: Matrix, camera: Point, focal_length: float, diff --git a/bemani/format/afp/blend/blendcpp.pyx b/bemani/format/afp/blend/blendcpp.pyx index 707d476..d7b2cd3 100644 --- a/bemani/format/afp/blend/blendcpp.pyx +++ b/bemani/format/afp/blend/blendcpp.pyx @@ -2,7 +2,7 @@ import multiprocessing from PIL import Image # type: ignore from typing import Optional, Tuple -from ..types import Color, Matrix, Point, AAMode +from ..types import Color, HSL, Matrix, Point, AAMode from .perspective import perspective_calculate cdef extern struct floatcolor_t: @@ -11,6 +11,11 @@ cdef extern struct floatcolor_t: double b; double a; +cdef extern struct hslcolor_t: + double h; + double s; + double l; + cdef extern struct matrix_t: double a11; double a12; @@ -36,6 +41,7 @@ cdef extern int composite_fast( unsigned int maxy, floatcolor_t add_color, floatcolor_t mult_color, + hslcolor_t hsl_shift, double xscale, double yscale, matrix_t inverse, @@ -52,6 +58,7 @@ def affine_composite( img: Image.Image, add_color: Color, mult_color: Color, + hsl_shift: HSL, transform: Matrix, mask: Optional[Image.Image], blendfunc: int, @@ -112,6 +119,7 @@ def affine_composite( # Convert classes to C structs. cdef floatcolor_t c_addcolor = floatcolor_t(r=add_color.r, g=add_color.g, b=add_color.b, a=add_color.a) cdef floatcolor_t c_multcolor = floatcolor_t(r=mult_color.r, g=mult_color.g, b=mult_color.b, a=mult_color.a) + cdef hslcolor_t c_hslcolor = hslcolor_t(h=hsl_shift.h, s=hsl_shift.s, l=hsl_shift.l) cdef matrix_t c_inverse = matrix_t( a11=inverse.a11, a12=inverse.a12, a13=inverse.a13, a21=inverse.a21, a22=inverse.a22, a23=inverse.a23, @@ -132,6 +140,7 @@ def affine_composite( maxy, c_addcolor, c_multcolor, + c_hslcolor, transform.xscale, transform.yscale, c_inverse, @@ -157,6 +166,7 @@ def perspective_composite( img: Image.Image, add_color: Color, mult_color: Color, + hsl_shift: HSL, transform: Matrix, camera: Point, focal_length: float, @@ -200,6 +210,7 @@ def perspective_composite( # Convert classes to C structs. cdef floatcolor_t c_addcolor = floatcolor_t(r=add_color.r, g=add_color.g, b=add_color.b, a=add_color.a) cdef floatcolor_t c_multcolor = floatcolor_t(r=mult_color.r, g=mult_color.g, b=mult_color.b, a=mult_color.a) + cdef hslcolor_t c_hslcolor = hslcolor_t(h=hsl_shift.h, s=hsl_shift.s, l=hsl_shift.l) cdef matrix_t c_inverse = matrix_t( a11=inverse_matrix.a11, a12=inverse_matrix.a12, a13=inverse_matrix.a13, a21=inverse_matrix.a21, a22=inverse_matrix.a22, a23=inverse_matrix.a23, @@ -220,6 +231,7 @@ def perspective_composite( maxy, c_addcolor, c_multcolor, + c_hslcolor, transform.xscale, transform.yscale, c_inverse, diff --git a/bemani/format/afp/blend/blendcppimpl.cxx b/bemani/format/afp/blend/blendcppimpl.cxx index 8212c40..e2b7dc6 100644 --- a/bemani/format/afp/blend/blendcppimpl.cxx +++ b/bemani/format/afp/blend/blendcppimpl.cxx @@ -26,6 +26,12 @@ extern "C" double a; } floatcolor_t; + typedef struct hslcolor { + double h; + double s; + double l; + } hslcolor_t; + typedef struct point { double x; double y; @@ -81,6 +87,7 @@ extern "C" int use_perspective; floatcolor_t add_color; floatcolor_t mult_color; + hslcolor_t hsl_shift; int blendfunc; pthread_t *thread; int aa_mode; @@ -90,6 +97,101 @@ extern "C" return fmin(fmax(0.0, roundf(color)), 255.0); } + inline unsigned int min(unsigned int x, unsigned int y) { + return x < y ? x : y; + } + + inline unsigned int max(unsigned int x, unsigned int y) { + return x > y ? x : y; + } + + void rgb_to_hsl(int r, int g, int b, double *h, double *s, double *l) { + int cmax = max(max(r, g), b); + int cmin = min(min(r, g), b); + double sum = (double)(cmin + cmax); + + // First, calculate luminance, which is the sum divided by 2. We + // also need to scale down by 255 since RGB values are integers! + *l = sum / (2.0 * 255.0); + if (cmax == cmin) { + // No point in calculating anything else, its just luminance. + *h = 0.0; + *s = 0.0; + return; + } + + // Second, calculate saturation. + double delta = (double)(cmax - cmin); + if (*l <= 0.5) { + // 255 scaling appears on both sides, so no need to handle it. + *s = delta / sum; + } else { + // We need to remember to scale by 255 here, so let's factor it out. + *s = delta / ((2.0 * 255) - sum); + } + + // Finaly, calculate hue. This can theoretically go above 1.0 or below + // 0.0 and most equations show it being clamped, but we need to clamp + // again when converting back so don't bother wasting time. + if (r == cmax) { + *h = ((double)(g - b) / 6.0) / delta; + } else if (g == cmax) { + *h = (1.0 / 3.0) + ((double)(b - r) / 6.0) / delta; + } else { + *h = (2.0 / 3.0) + ((double)(r - g) / 6.0) / delta; + } + } + + inline double hue_to_rgb(double v1, double v2, double vh) { + // Clamp hue value to 0.0/1.0, respecting the fact that 361 degrees is + // equivalent to 1 degree, and negative 1 degree is equivalent to 359. + if (vh < 0.0) { + vh += 1.0; + } + if (vh >= 1.0) { + vh -= 1.0; + } + + // Split back into 3 quadrants since RGB isn't linear with in these, + // there's a step function where at some point the slope goes from positive + // to negative non-continuously. + if ((6.0 * vh) < 1.0) { + return v1 + ((v2 - v1) * 6.0 * vh); + } + if ((2.0 * vh) < 1.0) { + return v2; + } + if ((3.0 * vh) < 2.0) { + return v1 + ((v2 - v1) * ((2.0 / 3.0) - vh) * 6.0); + } + + return v1; + } + + void hsl_to_rgb(double h, double s, double l, unsigned char *r, unsigned char *g, unsigned char *b) { + // Clamp hue value to 0.0/1.0, respecting the fact that 361 degrees is + // equivalent to 1 degree, and negative 1 degree is equivalent to 359. + while (h < 0.0) { + h += 1.0; + } + while (h >= 1.0) { + h -= 1.0; + } + s = fmin(fmax(s, 0.0), 1.0); + l = fmin(fmax(l, 0.0), 1.0); + + if (s == 0.0) { + *r = *g = *b = (int)(l * 255.0); + } else { + double v2 = (l < 0.5) ? (l * (1.0 + s)) : ((l + s) - (l * s)); + double v1 = (2.0 * l) - v2; + + *r = (unsigned char)(255.0 * hue_to_rgb(v1, v2, h + (1.0 / 3.0))); + *g = (unsigned char)(255.0 * hue_to_rgb(v1, v2, h)); + *b = (unsigned char)(255.0 * hue_to_rgb(v1, v2, h - (1.0 / 3.0))); + } + } + intcolor_t blend_normal( intcolor_t dest, intcolor_t src @@ -242,6 +344,7 @@ extern "C" intcolor_t blend_point( floatcolor_t add_color, floatcolor_t mult_color, + hslcolor_t hsl_shift, intcolor_t src_color, intcolor_t dest_color, int blendfunc @@ -254,6 +357,32 @@ extern "C" clamp((src_color.a * mult_color.a) + (255 * add_color.a)), }; + // Add in hsl shift if there is anything to do. + if (hsl_shift.h != 0.0 || hsl_shift.s != 0.0 || hsl_shift.l != 0.0) { + hslcolor_t hslcolor; + rgb_to_hsl( + src_color.r, + src_color.g, + src_color.b, + &hslcolor.h, + &hslcolor.s, + &hslcolor.l + ); + + hslcolor.h += hsl_shift.h; + hslcolor.s += hsl_shift.s; + hslcolor.l += hsl_shift.l; + + hsl_to_rgb( + hslcolor.h, + hslcolor.s, + hslcolor.l, + &src_color.r, + &src_color.g, + &src_color.b + ); + } + if (blendfunc == 3) { return blend_multiply(dest_color, src_color); } @@ -485,7 +614,7 @@ extern "C" } // Blend it. - work->imgdata[imgoff] = blend_point(work->add_color, work->mult_color, average, work->imgdata[imgoff], work->blendfunc); + work->imgdata[imgoff] = blend_point(work->add_color, work->mult_color, work->hsl_shift, average, work->imgdata[imgoff], work->blendfunc); } else { // Grab the center of the pixel to get the color. int texx = -1; @@ -510,7 +639,7 @@ extern "C" // Blend it. unsigned int texoff = texx + (texy * work->texwidth); - work->imgdata[imgoff] = blend_point(work->add_color, work->mult_color, work->texdata[texoff], work->imgdata[imgoff], work->blendfunc); + work->imgdata[imgoff] = blend_point(work->add_color, work->mult_color, work->hsl_shift, work->texdata[texoff], work->imgdata[imgoff], work->blendfunc); } } } @@ -533,6 +662,7 @@ extern "C" unsigned int maxy, floatcolor_t add_color, floatcolor_t mult_color, + hslcolor_t hsl_shift, double xscale, double yscale, matrix_t inverse, @@ -567,6 +697,7 @@ extern "C" work.inverse = inverse; work.add_color = add_color; work.mult_color = mult_color; + work.hsl_shift = hsl_shift; work.blendfunc = blendfunc; work.aa_mode = aa_mode; work.use_perspective = use_perspective; @@ -614,6 +745,7 @@ extern "C" work->inverse = inverse; work->add_color = add_color; work->mult_color = mult_color; + work->hsl_shift = hsl_shift; work->blendfunc = blendfunc; work->thread = thread; work->aa_mode = aa_mode; diff --git a/bemani/format/afp/render.py b/bemani/format/afp/render.py index f5a4968..5d1534c 100644 --- a/bemani/format/afp/render.py +++ b/bemani/format/afp/render.py @@ -20,6 +20,7 @@ from .swf import ( from .decompile import ByteCode from .types import ( Color, + HSL, Matrix, Point, Rectangle, @@ -120,6 +121,7 @@ class PlacedObject: projection: int, mult_color: Color, add_color: Color, + hsl_shift: HSL, blend: int, mask: Optional[Mask], ) -> None: @@ -130,6 +132,7 @@ class PlacedObject: self.projection = projection self.mult_color = mult_color self.add_color = add_color + self.hsl_shift = hsl_shift self.blend = blend self.mask = mask self.visible: bool = True @@ -164,6 +167,7 @@ class PlacedShape(PlacedObject): projection: int, mult_color: Color, add_color: Color, + hsl_shift: HSL, blend: int, mask: Optional[Mask], source: RegisteredShape, @@ -176,6 +180,7 @@ class PlacedShape(PlacedObject): projection, mult_color, add_color, + hsl_shift, blend, mask, ) @@ -201,6 +206,7 @@ class PlacedClip(PlacedObject): projection: int, mult_color: Color, add_color: Color, + hsl_shift: HSL, blend: int, mask: Optional[Mask], source: RegisteredClip, @@ -213,6 +219,7 @@ class PlacedClip(PlacedObject): projection, mult_color, add_color, + hsl_shift, blend, mask, ) @@ -375,6 +382,7 @@ class PlacedImage(PlacedObject): projection: int, mult_color: Color, add_color: Color, + hsl_shift: HSL, blend: int, mask: Optional[Mask], source: RegisteredImage, @@ -387,6 +395,7 @@ class PlacedImage(PlacedObject): projection, mult_color, add_color, + hsl_shift, blend, mask, ) @@ -411,6 +420,7 @@ class PlacedDummy(PlacedObject): projection: int, mult_color: Color, add_color: Color, + hsl_shift: HSL, blend: int, mask: Optional[Mask], source: RegisteredDummy, @@ -423,6 +433,7 @@ class PlacedDummy(PlacedObject): projection, mult_color, add_color, + hsl_shift, blend, mask, ) @@ -1059,6 +1070,7 @@ class AFPRenderer(VerboseOutput): if obj.object_id == tag.object_id and obj.depth == tag.depth: new_mult_color = tag.mult_color or obj.mult_color new_add_color = tag.add_color or obj.add_color + new_hsl_shift = tag.hsl_shift or obj.hsl_shift new_transform = ( obj.transform.update(tag.transform) if tag.transform is not None @@ -1092,6 +1104,7 @@ class AFPRenderer(VerboseOutput): new_projection, new_mult_color, new_add_color, + new_hsl_shift, new_blend, obj.mask, newobj, @@ -1108,6 +1121,7 @@ class AFPRenderer(VerboseOutput): new_projection, new_mult_color, new_add_color, + new_hsl_shift, new_blend, obj.mask, newobj, @@ -1124,6 +1138,7 @@ class AFPRenderer(VerboseOutput): new_projection, new_mult_color, new_add_color, + new_hsl_shift, new_blend, obj.mask, newobj, @@ -1141,6 +1156,7 @@ class AFPRenderer(VerboseOutput): new_projection, new_mult_color, new_add_color, + new_hsl_shift, new_blend, obj.mask, newobj, @@ -1160,6 +1176,7 @@ class AFPRenderer(VerboseOutput): ) obj.mult_color = new_mult_color obj.add_color = new_add_color + obj.hsl_shift = new_hsl_shift obj.transform = new_transform obj.rotation_origin = new_rotation_origin obj.projection = new_projection @@ -1194,6 +1211,7 @@ class AFPRenderer(VerboseOutput): tag.projection, tag.mult_color or Color(1.0, 1.0, 1.0, 1.0), tag.add_color or Color(0.0, 0.0, 0.0, 0.0), + tag.hsl_shift or HSL(0.0, 0.0, 0.0), tag.blend or 0, None, newobj, @@ -1212,6 +1230,7 @@ class AFPRenderer(VerboseOutput): tag.projection, tag.mult_color or Color(1.0, 1.0, 1.0, 1.0), tag.add_color or Color(0.0, 0.0, 0.0, 0.0), + tag.hsl_shift or HSL(0.0, 0.0, 0.0), tag.blend or 0, None, newobj, @@ -1229,6 +1248,7 @@ class AFPRenderer(VerboseOutput): tag.projection, tag.mult_color or Color(1.0, 1.0, 1.0, 1.0), tag.add_color or Color(0.0, 0.0, 0.0, 0.0), + tag.hsl_shift or HSL(0.0, 0.0, 0.0), tag.blend or 0, None, newobj, @@ -1258,6 +1278,7 @@ class AFPRenderer(VerboseOutput): tag.projection, tag.mult_color or Color(1.0, 1.0, 1.0, 1.0), tag.add_color or Color(0.0, 0.0, 0.0, 0.0), + tag.hsl_shift or HSL(0.0, 0.0, 0.0), tag.blend or 0, None, newobj, @@ -1386,6 +1407,7 @@ class AFPRenderer(VerboseOutput): ), Color(0.0, 0.0, 0.0, 0.0), Color(1.0, 1.0, 1.0, 1.0), + HSL(0.0, 0.0, 0.0), Matrix.identity().translate(Point(mask.bounds.left, mask.bounds.top)), None, 0, @@ -1406,6 +1428,7 @@ class AFPRenderer(VerboseOutput): ), Color(0.0, 0.0, 0.0, 0.0), Color(1.0, 1.0, 1.0, 1.0), + HSL(0.0, 0.0, 0.0), transform, None, 257, @@ -1424,6 +1447,7 @@ class AFPRenderer(VerboseOutput): ), Color(0.0, 0.0, 0.0, 0.0), Color(1.0, 1.0, 1.0, 1.0), + HSL(0.0, 0.0, 0.0), transform, None, 257, @@ -1438,6 +1462,7 @@ class AFPRenderer(VerboseOutput): ), Color(0.0, 0.0, 0.0, 0.0), Color(1.0, 1.0, 1.0, 1.0), + HSL(0.0, 0.0, 0.0), transform, self.__camera.center, self.__camera.focal_length, @@ -1453,6 +1478,7 @@ class AFPRenderer(VerboseOutput): parent_mask.copy(), Color(0.0, 0.0, 0.0, 0.0), Color(1.0, 1.0, 1.0, 1.0), + HSL(0.0, 0.0, 0.0), Matrix.identity(), None, 256, @@ -1470,6 +1496,7 @@ class AFPRenderer(VerboseOutput): parent_mask: Image.Image, parent_mult_color: Color, parent_add_color: Color, + parent_hsl_shift: HSL, parent_blend: int, only_depths: Optional[List[int]] = None, prefix: str = "", @@ -1505,6 +1532,7 @@ class AFPRenderer(VerboseOutput): .multiply(parent_mult_color) .add(parent_add_color) ) + hsl_shift = renderable.hsl_shift or HSL(0.0, 0.0, 0.0).add(parent_hsl_shift) blend = renderable.blend or 0 if parent_blend not in {0, 1, 2} and blend in {0, 1, 2}: blend = parent_blend @@ -1541,6 +1569,7 @@ class AFPRenderer(VerboseOutput): mask, mult_color, add_color, + hsl_shift, blend, only_depths=new_only_depths, prefix=prefix + " ", @@ -1631,6 +1660,7 @@ class AFPRenderer(VerboseOutput): img, add_color, mult_color, + hsl_shift, transform, mask, blend, @@ -1656,6 +1686,7 @@ class AFPRenderer(VerboseOutput): img, add_color, mult_color, + hsl_shift, transform, mask, blend, @@ -1677,6 +1708,7 @@ class AFPRenderer(VerboseOutput): img, add_color, mult_color, + hsl_shift, transform, self.__camera.center, self.__camera.focal_length, @@ -1699,6 +1731,7 @@ class AFPRenderer(VerboseOutput): img, add_color, mult_color, + hsl_shift, transform, mask, blend, @@ -1717,6 +1750,7 @@ class AFPRenderer(VerboseOutput): img, add_color, mult_color, + hsl_shift, transform, mask, blend, @@ -1731,6 +1765,7 @@ class AFPRenderer(VerboseOutput): img, add_color, mult_color, + hsl_shift, transform, self.__camera.center, self.__camera.focal_length, @@ -2051,6 +2086,7 @@ class AFPRenderer(VerboseOutput): AP2PlaceObjectTag.PROJECTION_AFFINE, Color(1.0, 1.0, 1.0, 1.0), Color(0.0, 0.0, 0.0, 0.0), + HSL(0.0, 0.0, 0.0), 0, None, RegisteredClip( @@ -2106,6 +2142,7 @@ class AFPRenderer(VerboseOutput): AP2PlaceObjectTag.PROJECTION_AFFINE, Color(1.0, 1.0, 1.0, 1.0), Color(0.0, 0.0, 0.0, 0.0), + HSL(0.0, 0.0, 0.0), 0, None, background_object, @@ -2120,6 +2157,7 @@ class AFPRenderer(VerboseOutput): # These could possibly be overwritten from an external source of we wanted. actual_mult_color = Color(1.0, 1.0, 1.0, 1.0) actual_add_color = Color(0.0, 0.0, 0.0, 0.0) + actual_hsl_shift = HSL(0.0, 0.0, 0.0) actual_blend = 0 max_frame: Optional[int] = None @@ -2206,6 +2244,7 @@ class AFPRenderer(VerboseOutput): movie_mask, actual_mult_color, actual_add_color, + actual_hsl_shift, actual_blend, only_depths=only_depths, ) diff --git a/bemani/format/afp/swf.py b/bemani/format/afp/swf.py index 4a8cba0..a55a696 100644 --- a/bemani/format/afp/swf.py +++ b/bemani/format/afp/swf.py @@ -8,6 +8,7 @@ from .decompile import ByteCode from .types import ( Matrix, Color, + HSL, Point, Rectangle, AP2Action, @@ -323,6 +324,7 @@ class AP2PlaceObjectTag(Tag): projection: int, mult_color: Optional[Color], add_color: Optional[Color], + hsl_shift: Optional[HSL], triggers: Dict[int, List[ByteCode]], unrecognized_options: bool, ) -> None: @@ -364,6 +366,9 @@ class AP2PlaceObjectTag(Tag): # If there is a color to add with the sprite/shape when drawing. self.add_color = add_color + # If there is a hue/saturation/lightness shift effect when drawing. + self.hsl_shift = hsl_shift + # List of triggers for this object, and their respective bytecodes to execute when the trigger # fires. self.triggers = triggers @@ -398,6 +403,9 @@ class AP2PlaceObjectTag(Tag): "add_color": self.add_color.as_dict(*args, **kwargs) if self.add_color is not None else None, + "hsl_shift": self.hsl_shift.as_dict(*args, **kwargs) + if self.hsl_shift + else None, "triggers": { i: [b.as_dict(*args, **kwargs) for b in t] for (i, t) in self.triggers.items() @@ -2007,18 +2015,29 @@ class SWF(VerboseOutput, TrackedCoverage): component="tags", ) + # HSL shift data. + hue: Optional[int] = None + saturation: Optional[int] = None + lightness: Optional[int] = None + if flags & 0x20000000: - # TODO: Again, absolutely no idea, gets passed into a function and I don't see how its used. + # Looks like Hue/Lightness/Saturation shift, matching after effects in the limits. + # First value is degrees to shift the hue, second and third values I'm not sure if + # its saturation then lightness or lightness then saturation but both have limits of + # -100 to 100 in after effects and the file that I found with this option chooses + # 0 for each. unhandled_flags &= ~0x20000000 - unk_a, unk_b, unk_c = struct.unpack( + hue, saturation, lightness = struct.unpack( " "HSL": + h, l, s = colorsys.rgb_to_hls(self.r, self.g, self.b) + return HSL(h, s, l) + def as_tuple(self) -> Tuple[int, int, int, int]: return ( int(self.r * 255), @@ -49,6 +54,61 @@ class Color: return f"r: {round(self.r, 5)}, g: {round(self.g, 5)}, b: {round(self.b, 5)}, a: {round(self.a, 5)}" +class HSL: + # A hue/saturation/lightness color shift, represented as a series of floats between + # -1.0 and 1.0. The hue represents a percentage along the polar coordinates, + # 0.0 being 0 degrees, -1.0 being -360 degrees and 1.0 being 360 degrees. The + # saturation and lightness values representing actual normalized percentages where + # a lightness of 100 would be written as 1.0. + def __init__(self, h: float, s: float, l: float) -> None: + self.h = h + self.s = s + self.l = l + + @property + def is_identity(self) -> bool: + return self.h == 0.0 and self.s == 0.0 and self.l == 0.0 + + def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: + return { + "h": self.h, + "s": self.s, + "l": self.l, + } + + def add(self, other: "HSL") -> "HSL": + # Not entirely sure this is correct, but we don't have any animations to compare to. + # Basically, not sure if HSL colorspace is linear in this way, but as long as no + # animations try to stack multiple HSL shift effects this shouldn't matter. + return HSL(h=self.h + other.h, s=self.s + other.s, l=self.l + other.l) + + def as_rgb(self) -> "Color": + h = self.h + while h < 0.0: + h += 1.0 + while h > 1.0: + h -= 1.0 + + s = min(max(self.s, 0.0), 1.0) + l = min(max(self.l, 0.0), 1.0) + r, g, b = colorsys.hls_to_rgb(h, l, s) + return Color(r, g, b, 1.0) + + def as_tuple(self) -> Tuple[int, int, int]: + h = int(self.h * 360) + while h < 0: + h += 360 + while h > 360: + h -= 360 + + s = min(max(int(self.s), -100), 100) + l = min(max(int(self.l), -100), 100) + return (h, s, l) + + def __repr__(self) -> str: + return f"h: {round(self.h, 5)}, s: {round(self.s, 5)}, l: {round(self.l, 5)}" + + class Point: # A simple 3D point. For ease of construction, the Z can be left out # at which point it is assumed to be zero. diff --git a/verifylint b/verifylint index 978674e..a24189b 100755 --- a/verifylint +++ b/verifylint @@ -1,3 +1,3 @@ #! /bin/bash -flake8 bemani/ --ignore E203,E501,E252,W503,W504,B006,B008,B009 | grep -v "migrations\/" +flake8 bemani/ --ignore E203,E501,E252,E741,W503,W504,B006,B008,B009 | grep -v "migrations\/"