diff --git a/src/openfl/display/BitmapData.hx b/src/openfl/display/BitmapData.hx index d292d9dbb9..8c5b5114fd 100644 --- a/src/openfl/display/BitmapData.hx +++ b/src/openfl/display/BitmapData.hx @@ -382,12 +382,12 @@ class BitmapData implements IBitmapDrawable if (filter.__preserveObject) { - bitmapData3.copyPixels(this, rect, destPoint); + bitmapData3.copyPixels(sourceBitmapData, sourceRect, destPoint); } - var lastBitmap = filter.__applyFilter(bitmapData2, this, sourceRect, destPoint); + var lastBitmap = filter.__applyFilter(bitmapData2, sourceBitmapData, sourceRect, destPoint); - if (filter.__preserveObject) + if (filter.__preserveObject && !filter.__softwareComposite) { lastBitmap.draw(bitmapData3, null, null); } diff --git a/src/openfl/display/DisplayObjectRenderer.hx b/src/openfl/display/DisplayObjectRenderer.hx index 1ddbaa9414..8d9682bace 100644 --- a/src/openfl/display/DisplayObjectRenderer.hx +++ b/src/openfl/display/DisplayObjectRenderer.hx @@ -387,8 +387,10 @@ class DisplayObjectRenderer extends EventDispatcher filterWidth = rect.width > 0 ? Math.ceil((rect.width + 1) * pixelRatio) : 0; filterHeight = rect.height > 0 ? Math.ceil((rect.height + 1) * pixelRatio) : 0; - offsetX = rect.x > 0 ? Math.ceil(rect.x) : Math.floor(rect.x); - offsetY = rect.y > 0 ? Math.ceil(rect.y) : Math.floor(rect.y); + + // PixelRatio is adjusting for HDPI screns with fractional scaling. (1.5x, 1.25x etc) + offsetX = (rect.x > 0 ? Math.ceil(rect.x * pixelRatio) : Math.floor(rect.x * pixelRatio)) / pixelRatio; + offsetY = (rect.y > 0 ? Math.ceil(rect.y * pixelRatio) : Math.floor(rect.y * pixelRatio)) / pixelRatio; if (displayObject.__cacheBitmapData != null) { @@ -668,6 +670,10 @@ class DisplayObjectRenderer extends EventDispatcher for (filter in displayObject.__filters) { + // the cache bitmap is drawn at pixelRatio, so blur radius and + // offsets (in logical pixels) must scale to match + filter.__renderScale = pixelRatio; + if (filter.__preserveObject) { childRenderer.__setRenderTarget(bitmap3); @@ -785,6 +791,8 @@ class DisplayObjectRenderer extends EventDispatcher for (filter in displayObject.__filters) { + filter.__renderScale = pixelRatio; + if (filter.__preserveObject) { bitmap3.copyPixels(bitmap, bitmap.rect, destPoint); @@ -792,7 +800,7 @@ class DisplayObjectRenderer extends EventDispatcher lastBitmap = filter.__applyFilter(bitmap2, bitmap, bitmap.rect, destPoint); - if (filter.__preserveObject) + if (filter.__preserveObject && !filter.__softwareComposite) { lastBitmap.draw(bitmap3, null, displayObject.__objectTransform != null ? displayObject.__objectTransform.__colorTransform : null); diff --git a/src/openfl/filters/BevelFilter.hx b/src/openfl/filters/BevelFilter.hx index 50482c7b4f..fa14269a08 100644 --- a/src/openfl/filters/BevelFilter.hx +++ b/src/openfl/filters/BevelFilter.hx @@ -144,6 +144,7 @@ import lime._internal.graphics.ImageDataUtil; __needSecondBitmapData = true; __preserveObject = true; + __softwareComposite = true; __renderDirty = true; } @@ -155,15 +156,52 @@ import lime._internal.graphics.ImageDataUtil; @:noCompletion private override function __applyFilter(bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData { - #if lime - var time = Timer.stamp(); - var finalImage = ImageDataUtil.gaussianBlur(bitmapData.image, sourceBitmapData.image, sourceRect.__toLimeRectangle(), destPoint.__toLimeVector2(), - __blurX, __blurY, __quality); - var elapsed = Timer.stamp() - time; - // trace("blurX: " + __blurX + " blurY: " + __blurY + " quality: " + __quality + " elapsed: " + elapsed * 1000 + "ms"); - if (finalImage == bitmapData.image) return bitmapData; - #end - return sourceBitmapData; + var width = bitmapData.width; + var height = bitmapData.height; + + // Same Shader: blur the source alpha, then compare the mask either side of the light direction. + // The signed difference drives the highlight (light side) and the shadow (dark side). + var mask = BitmapFilter.__alphaMask(sourceBitmapData, sourceRect, destPoint, width, height); + BitmapFilter.__blurMask(mask, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); + + var rad = __angle * Math.PI / 180; + var dx = Std.int(Math.round(__distance * Math.cos(rad) * __renderScale)); + var dy = Std.int(Math.round(__distance * Math.sin(rad) * __renderScale)); + + // highlight / shadow colours, each channel premultiplied by its alpha + // the same values the shader receives as uLightColor / uShadowColor + var highlightR = (((__highlightColor >> 16) & 0xFF) / 255.0) * __highlightAlpha; + var highlightG = (((__highlightColor >> 8) & 0xFF) / 255.0) * __highlightAlpha; + var highlightB = ((__highlightColor & 0xFF) / 255.0) * __highlightAlpha; + var shadowR = (((__shadowColor >> 16) & 0xFF) / 255.0) * __shadowAlpha; + var shadowG = (((__shadowColor >> 8) & 0xFF) / 255.0) * __shadowAlpha; + var shadowB = ((__shadowColor & 0xFF) / 255.0) * __shadowAlpha; + + var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); + for (y in 0...height) + { + for (x in 0...width) + { + // Sample the blurred mask a short way along the light angle in both + // directions. Flash's `angle` is where the light comes from, so `+offset` + // points the way a shadow falls and `-offset` points toward the light. + // (called blurLeft / blurRight in BevelShader) + var maskTowardShadow = BitmapFilter.__maskAt(mask, width, height, x + dx, y + dy); + var maskTowardLight = BitmapFilter.__maskAt(mask, width, height, x - dx, y - dy); + var dist = (maskTowardShadow - maskTowardLight) * __strength; + + var highlight = dist > 1 ? 1.0 : (dist < 0 ? 0.0 : dist); + var shadow = -dist > 1 ? 1.0 : (-dist < 0 ? 0.0 : -dist); + + fxR.push(highlightR * highlight + shadowR * shadow); + fxG.push(highlightG * highlight + shadowG * shadow); + fxB.push(highlightB * highlight + shadowB * shadow); + fxA.push(__highlightAlpha * highlight + __shadowAlpha * shadow); + } + } + + var type:BitmapFilterType = (__type == "inner") ? INNER : ((__type == "outer") ? OUTER : FULL); + return BitmapFilter.__compositeEffect(bitmapData, sourceBitmapData, sourceRect, destPoint, fxR, fxG, fxB, fxA, type, __knockout); } @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader @@ -173,22 +211,12 @@ import lime._internal.graphics.ImageDataUtil; var numBlurPasses = __horizontalPasses + __verticalPasses; if (blurPass < numBlurPasses) { - var shader = BlurFilter.__blurShader; - if (pass < __horizontalPasses) - { - var scale = Math.pow(0.5, pass >> 1); - shader.uRadius.value[0] = blurX * scale; - shader.uRadius.value[1] = 0; - } - else - { - var scale = Math.pow(0.5, (pass - __horizontalPasses) >> 1); - shader.uRadius.value[0] = 0; - shader.uRadius.value[1] = blurY * scale; - } - return shader; + var horizontal = pass < __horizontalPasses; + return BlurFilter.__setupBlurShader(horizontal, (horizontal ? blurX : blurY) * __renderScale); } + // transform is scaled by __renderScale, which is not known until render time + __updateTransform(); __bevelShader.sourceBitmap.input = sourceBitmapData; #end @@ -346,12 +374,14 @@ import lime._internal.graphics.ImageDataUtil; value = value < 1 ? 1 : value; value = value > 15 ? 15 : value; - __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * (value / 4)); - __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * (value / 4)); + __horizontalPasses = (__blurX <= 0) ? 0 : value; + __verticalPasses = (__blurY <= 0) ? 0 : value; __numShaderPasses = __horizontalPasses + __verticalPasses + 1; if (value != __quality) __renderDirty = true; + __quality = value; + __updateSize(); // depends on filter's quality settings return __quality = value; } @@ -418,8 +448,8 @@ import lime._internal.graphics.ImageDataUtil; @:noCompletion private function __updateTransform():Void { var rad:Float = __angle * Math.PI / 180; - __bevelShader.uTransformX.value[0] = (__distance * Math.cos(rad)); - __bevelShader.uTransformY.value[0] = (__distance * Math.sin(rad)); + __bevelShader.uTransformX.value[0] = (__distance * Math.cos(rad)) * __renderScale; + __bevelShader.uTransformY.value[0] = (__distance * Math.sin(rad)) * __renderScale; } @:noCompletion private function __updateColors():Void @@ -445,10 +475,15 @@ import lime._internal.graphics.ImageDataUtil; { var offsetX:Int = __type != "inner" ? Math.ceil(__distance * Math.cos(__angle * Math.PI / 180)) : 0; var offsetY:Int = __type != "inner" ? Math.ceil(__distance * Math.sin(__angle * Math.PI / 180)) : 0; - __topExtension = Math.ceil((offsetY < 0 ? -offsetY : 0) + __blurY); - __bottomExtension = Math.ceil((offsetY > 0 ? offsetY : 0) + __blurY); - __leftExtension = Math.ceil((offsetX < 0 ? -offsetX : 0) + __blurX); - __rightExtension = Math.ceil((offsetX > 0 ? offsetX : 0) + __blurX); + // Box blur reach grows to ~quality*blur/2 per side (see DropShadowFilter); + // reserving the full spread so the bevel isn't clipped at high quality. + var q = (__quality > 0) ? __quality : 1; + var exX = Math.ceil(__blurX * 0.5 * q) + 4; + var exY = Math.ceil(__blurY * 0.5 * q) + 4; + __topExtension = (offsetY < 0 ? -offsetY : 0) + exY; + __bottomExtension = (offsetY > 0 ? offsetY : 0) + exY; + __leftExtension = (offsetX < 0 ? -offsetX : 0) + exX; + __rightExtension = (offsetX > 0 ? offsetX : 0) + exX; } } diff --git a/src/openfl/filters/BitmapFilter.hx b/src/openfl/filters/BitmapFilter.hx index 5e8937d7f0..fe6c712919 100644 --- a/src/openfl/filters/BitmapFilter.hx +++ b/src/openfl/filters/BitmapFilter.hx @@ -7,6 +7,7 @@ import openfl.display.DisplayObjectRenderer; import openfl.display.Shader; import openfl.geom.Point; import openfl.geom.Rectangle; +import openfl.Vector; /** The BitmapFilter class is the base class for all image filter effects. @@ -35,6 +36,25 @@ class BitmapFilter @:noCompletion private var __smooth:Bool; @:noCompletion private var __topExtension:Int; + /** + Whether `__applyFilter` composites the original object into its own result. + **/ + @:noCompletion private var __softwareComposite:Bool; + + /** + The device-pixel scale the filter is being rendered at. + + A filtered object is cached into a bitmap sized and drawn at the renderer's + pixel ratio, so on a HiDPI display the object is (say) 1.5x larger in that + bitmap. Filter distances, blur radius, and offsets are authored in + *logical* pixels, so they must be scaled to match, otherwise the effect comes + out 1/pixelRatio too small relative to its content. + + The renderer sets this before using the filter. It stays 1 for + `BitmapData.applyFilter`, which works on the bitmap's own pixels. + **/ + @:noCompletion private var __renderScale:Float; + public function new() { __bottomExtension = 0; @@ -46,6 +66,8 @@ class BitmapFilter __shaderBlendMode = NORMAL; __topExtension = 0; __smooth = true; + __renderScale = 1; + __softwareComposite = false; } /** @@ -69,6 +91,246 @@ class BitmapFilter // return renderer.__defaultShader; return null; } + + + /** + The source's alpha channel as a 0..1 mask laid out on the destination + grid, using the sourceRect/destPoint mapping `__applyFilter` is given. + Samples outside the source read 0, matching the transparent border the GL + path gets from its cache texture. + **/ + @:noCompletion private static function __alphaMask(source:BitmapData, sourceRect:Rectangle, destPoint:Point, width:Int, height:Int):Array + { + var mask = [for (i in 0...width * height) 0.0]; + + var pixels = source.getVector(sourceRect); + + var sw = Std.int(sourceRect.width); + var sh = Std.int(sourceRect.height); + + var ox = Std.int(destPoint.x); + var oy = Std.int(destPoint.y); + + for (y in 0...sh) + { + var dy = y + oy; + if (dy < 0 || dy >= height) continue; + for (x in 0...sw) + { + var dx = x + ox; + if (dx < 0 || dx >= width) continue; + mask[dy * width + dx] = ((pixels[y * sw + x] >>> 24) & 0xFF) / 255.0; + } + } + return mask; + } + + /** + Box-blur a 0..1 mask in place, matching `BoxBlurShader`: `quality` + iterations of one horizontal then one vertical pass. + **/ + @:noCompletion private static function __blurMask(mask:Array, width:Int, height:Int, blurX:Float, blurY:Float, quality:Int):Array + { + var passes = (quality > 0) ? quality : 1; + var scratch = [for (i in 0...mask.length) 0.0]; + + for (i in 0...passes) + { + __blurMaskAxis(mask, scratch, width, height, blurX, true); + __blurMaskAxis(scratch, mask, width, height, blurY, false); + } + return mask; + } + + @:noCompletion private static function __blurMaskAxis(src:Array, dest:Array, width:Int, height:Int, blur:Float, horizontal:Bool):Void + { + var fullSize = (blur > 255) ? 255.0 : blur; + + if (fullSize <= 1) + { + for (i in 0...src.length) + dest[i] = src[i]; + return; + } + + var half = fullSize * 0.5; + var n = Std.int(Math.floor(half - 0.5)); + if (n < 0) n = 0; + var frac = Math.floor((half - (n + 0.5)) * 255) / 255; + var edge = n + 1; + + for (y in 0...height) + { + for (x in 0...width) + { + var sum = __maskAt(src, width, height, x, y); + + for (i in 1...(n + 1)) + { + if (horizontal) sum += __maskAt(src, width, height, x + i, y) + __maskAt(src, width, height, x - i, y); + else sum += __maskAt(src, width, height, x, y + i) + __maskAt(src, width, height, x, y - i); + } + + if (horizontal) sum += (__maskAt(src, width, height, x + edge, y) + __maskAt(src, width, height, x - edge, y)) * frac; + else sum += (__maskAt(src, width, height, x, y + edge) + __maskAt(src, width, height, x, y - edge)) * frac; + + dest[y * width + x] = Math.floor((sum / fullSize) * 255) / 255; + } + } + } + + @:noCompletion private static inline function __maskAt(mask:Array, width:Int, height:Int, x:Int, y:Int):Float + { + return (x < 0 || x >= width || y < 0 || y >= height) ? 0.0 : mask[y * width + x]; + } + + /** + Combine an effect layer with the source and write the result into `dest`, + matching the GL combine shaders. The effect is supplied as premultiplied + 0..1 channels; `type` is INNER / OUTER / FULL. + + outer: src + fx * (1 - src.a) + inner: rgb = src.rgb * (1 - fx.a) + fx.rgb * src.a, a = src.a + full: src * (1 - fx.a) + fx + knockout: the src term is dropped (outer keeps `fx * (1 - src.a)`, + inner keeps `fx * src.a`, full keeps `fx`) + **/ + @:noCompletion private static function __compositeEffect(dest:BitmapData, source:BitmapData, sourceRect:Rectangle, destPoint:Point, fxR:Array, + fxG:Array, fxB:Array, fxA:Array, type:BitmapFilterType, knockout:Bool):BitmapData + { + var width = dest.width; + var height = dest.height; + + // source pixels on the destination grid, premultiplied + var srcR = new Array(); + var srcG = new Array(); + var srcB = new Array(); + var srcA = new Array(); + + var pixelCount = width * height; + for (i in 0...pixelCount) + { + srcR.push(0.0); + srcG.push(0.0); + srcB.push(0.0); + srcA.push(0.0); + } + + var pixels = source.getVector(sourceRect); + + var sourceWidth = Std.int(sourceRect.width); + var sourceHeight = Std.int(sourceRect.height); + var destOffsetX = Std.int(destPoint.x); + var destOffsetY = Std.int(destPoint.y); + + for (y in 0...sourceHeight) + { + var destY = y + destOffsetY; + if (destY < 0 || destY >= height) continue; + + for (x in 0...sourceWidth) + { + var destX = x + destOffsetX; + if (destX < 0 || destX >= width) continue; + + var argb = pixels[y * sourceWidth + x]; + var a = ((argb >>> 24) & 0xFF) / 255.0; + var i = destY * width + destX; + srcA[i] = a; + srcR[i] = (((argb >> 16) & 0xFF) / 255.0) * a; + srcG[i] = (((argb >> 8) & 0xFF) / 255.0) * a; + srcB[i] = ((argb & 0xFF) / 255.0) * a; + } + } + + var out = new Vector(width * height, true); + + for (i in 0...width * height) + { + var sourceR = srcR[i], sourceG = srcG[i], sourceB = srcB[i], sourceA = srcA[i]; + var effectR = fxR[i], effectG = fxG[i], effectB = fxB[i], effectA = fxA[i]; + var r:Float, g:Float, b:Float, a:Float; + + if (type == INNER) + { + // the effect confined to the shape: scaled by the source alpha + var maskedR = effectR * sourceA, maskedG = effectG * sourceA, maskedB = effectB * sourceA, maskedA = effectA * sourceA; + if (knockout) + { + r = maskedR; + g = maskedG; + b = maskedB; + a = maskedA; + } + else + { + r = sourceR * (1 - effectA) + maskedR; + g = sourceG * (1 - effectA) + maskedG; + b = sourceB * (1 - effectA) + maskedB; + a = sourceA; + } + } + else if (type == FULL) + { + if (knockout) + { + r = effectR; + g = effectG; + b = effectB; + a = effectA; + } + else + { + r = sourceR * (1 - effectA) + effectR; + g = sourceG * (1 - effectA) + effectG; + b = sourceB * (1 - effectA) + effectB; + a = sourceA * (1 - effectA) + effectA; + } + } + else // OUTER + { + var outside = 1 - sourceA; // how much of this pixel lies outside the shape + if (knockout) + { + r = effectR * outside; + g = effectG * outside; + b = effectB * outside; + a = effectA * outside; + } + else + { + r = sourceR + effectR * outside; + g = sourceG + effectG * outside; + b = sourceB + effectB * outside; + a = sourceA + effectA * outside; + } + } + + out[i] = __toStraightARGB(r, g, b, a); + } + + dest.setVector(dest.rect, out); + return dest; + } + + // premultiplied 0..1 -> straight 0xAARRGGBB + @:noCompletion private static inline function __toStraightARGB(r:Float, g:Float, b:Float, a:Float):UInt + { + if (a <= 0) return 0; + if (a > 1) a = 1; + + var ir = Std.int(__clamp01(r / a) * 255 + 0.5); + var ig = Std.int(__clamp01(g / a) * 255 + 0.5); + var ib = Std.int(__clamp01(b / a) * 255 + 0.5); + var ia = Std.int(a * 255 + 0.5); + + return (ia << 24) | (ir << 16) | (ig << 8) | ib; + } + + @:noCompletion private static inline function __clamp01(v:Float):Float + { + return v < 0 ? 0 : (v > 1 ? 1 : v); + } } #else typedef BitmapFilter = flash.filters.BitmapFilter; diff --git a/src/openfl/filters/BlurFilter.hx b/src/openfl/filters/BlurFilter.hx index f7bab3f223..892b9e53ab 100644 --- a/src/openfl/filters/BlurFilter.hx +++ b/src/openfl/filters/BlurFilter.hx @@ -69,7 +69,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:access(openfl.geom.Rectangle) @:final class BlurFilter extends BitmapFilter { - @:noCompletion private static var __blurShader:BlurShader = new BlurShader(); + @:noCompletion private static var __blurShader:BoxBlurShader = new BoxBlurShader(); /** The amount of horizontal blur. Valid values are from 0 to 255(floating @@ -184,7 +184,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO #if lime var time = Timer.stamp(); var finalImage = ImageDataUtil.gaussianBlur(bitmapData.image, sourceBitmapData.image, sourceRect.__toLimeRectangle(), destPoint.__toLimeVector2(), - __blurX, __blurY, __quality); + __blurX * __renderScale, __blurY * __renderScale, __quality); var elapsed = Timer.stamp() - time; // trace("blurX: " + __blurX + " blurY: " + __blurY + " quality: " + __quality + " elapsed: " + elapsed * 1000 + "ms"); if (finalImage == bitmapData.image) return bitmapData; @@ -195,21 +195,22 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader { #if !macro - if (pass < __horizontalPasses) - { - var scale = Math.pow(0.5, pass >> 1); - __blurShader.uRadius.value[0] = blurX * scale; - __blurShader.uRadius.value[1] = 0; - } - else - { - var scale = Math.pow(0.5, (pass - __horizontalPasses) >> 1); - __blurShader.uRadius.value[0] = 0; - __blurShader.uRadius.value[1] = blurY * scale; - } + // passes alternate horizontal / vertical, each applies one full box blur for its axis, iterated `quality` times + var horizontal = (pass % 2 == 0); + return __setupBlurShader(horizontal, (horizontal ? blurX : blurY) * __renderScale); + #else + return __blurShader; #end + } - return __blurShader; + // Configure the box-blur shader for one axis of one pass. + @:noCompletion private static function __setupBlurShader(horizontal:Bool, v:Float):BitmapFilterShader + { + var shader = __blurShader; + shader.uDir.value[0] = horizontal ? 1.0 : 0.0; + shader.uDir.value[1] = horizontal ? 0.0 : 1.0; + shader.uFullSize.value[0] = v > 255 ? 255.0 : v; + return shader; } @:noCompletion inline function __padFor(value:Float):Int @@ -270,12 +271,12 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private function set_quality(value:Int):Int { - // TODO: Quality effect with fewer passes? - __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * (value / 4)) + 1; - __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * (value / 4)) + 1; - - __numShaderPasses = __horizontalPasses + __verticalPasses; + // one horizontal + one vertical box pass per quality iteration + var passes = (value > 0) ? value : 1; + __horizontalPasses = passes; + __verticalPasses = passes; + __numShaderPasses = passes * 2; if (value != __quality) __renderDirty = true; __quality = value; @@ -289,47 +290,49 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:fileXml('tags="haxe,release"') @:noDebug #end -private class BlurShader extends BitmapFilterShader +private class BoxBlurShader extends BitmapFilterShader { - @:glFragmentSource("uniform sampler2D openfl_Texture; - - varying vec2 vBlurCoords[7]; + @:glVertexSource("#pragma header void main(void) { - vec4 sum = vec4(0.0); - sum += texture2D(openfl_Texture, vBlurCoords[0]) * 0.00443; - sum += texture2D(openfl_Texture, vBlurCoords[1]) * 0.05399; - sum += texture2D(openfl_Texture, vBlurCoords[2]) * 0.24197; - sum += texture2D(openfl_Texture, vBlurCoords[3]) * 0.39894; - sum += texture2D(openfl_Texture, vBlurCoords[4]) * 0.24197; - sum += texture2D(openfl_Texture, vBlurCoords[5]) * 0.05399; - sum += texture2D(openfl_Texture, vBlurCoords[6]) * 0.00443; - - gl_FragColor = sum; + #pragma body }") - @:glVertexSource("attribute vec4 openfl_Position; - attribute vec2 openfl_TextureCoord; - - uniform mat4 openfl_Matrix; + @:glFragmentSource("#pragma header - uniform vec2 uRadius; - varying vec2 vBlurCoords[7]; uniform vec2 uTextureSize; + uniform vec2 uDir; // blur axis: (1,0) horizontal, (0,1) vertical + uniform float uFullSize; // box width = the blur amount void main(void) { - gl_Position = openfl_Matrix * openfl_Position; - - vec2 r = uRadius / uTextureSize; - vBlurCoords[0] = openfl_TextureCoord - r; - vBlurCoords[1] = openfl_TextureCoord - r * 0.75; - vBlurCoords[2] = openfl_TextureCoord - r * 0.5; - vBlurCoords[3] = openfl_TextureCoord; - vBlurCoords[4] = openfl_TextureCoord + r * 0.5; - vBlurCoords[5] = openfl_TextureCoord + r * 0.75; - vBlurCoords[6] = openfl_TextureCoord + r; + vec2 direction = uDir / uTextureSize; + float fullSize = min(uFullSize, 255.0); + + if (fullSize <= 1.0) { + gl_FragColor = texture2D(openfl_Texture, openfl_TextureCoordv); + return; + } + + float halfW = fullSize * 0.5; + int n = int(floor(halfW - 0.5)); // full interior texels per side + float frac = halfW - (float(n) + 0.5); // fractional edge weight + frac = floor(frac * 255.0) / 255.0; // 8-bit weight (Flash fixed point) + + vec4 sum = texture2D(openfl_Texture, openfl_TextureCoordv); // centre + for (int i = 1; i <= 128; i++) { // full interior pairs + if (i > n) break; + vec2 off = float(i) * direction; + sum += texture2D(openfl_Texture, openfl_TextureCoordv + off); + sum += texture2D(openfl_Texture, openfl_TextureCoordv - off); + } + vec2 edge = float(n + 1) * direction; // fractional edges + sum += texture2D(openfl_Texture, openfl_TextureCoordv + edge) * frac; + sum += texture2D(openfl_Texture, openfl_TextureCoordv - edge) * frac; + + vec4 result = sum / fullSize; + gl_FragColor = floor(result * 255.0) / 255.0; // 8-bit round each pass }") public function new() @@ -337,7 +340,8 @@ private class BlurShader extends BitmapFilterShader super(); #if !macro - uRadius.value = [0, 0]; + uDir.value = [1.0, 0.0]; + uFullSize.value = [1.0]; #end } diff --git a/src/openfl/filters/DropShadowFilter.hx b/src/openfl/filters/DropShadowFilter.hx index 1a5225b026..ce35329965 100644 --- a/src/openfl/filters/DropShadowFilter.hx +++ b/src/openfl/filters/DropShadowFilter.hx @@ -283,6 +283,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO __needSecondBitmapData = true; __preserveObject = true; __renderDirty = true; + __softwareComposite = true; } public override function clone():BitmapFilter @@ -292,22 +293,48 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private override function __applyFilter(bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData { - // TODO: Support knockout, inner + var width = bitmapData.width; + var height = bitmapData.height; - #if lime - var r = (__color >> 16) & 0xFF; - var g = (__color >> 8) & 0xFF; - var b = __color & 0xFF; + // blur the source alpha, then read it back shifted by the shadow offset + var mask = BitmapFilter.__alphaMask(sourceBitmapData, sourceRect, destPoint, width, height); + if (__inner) + { + for (i in 0...mask.length) + mask[i] = 1 - mask[i]; + } + BitmapFilter.__blurMask(mask, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); - var point = new Point(destPoint.x + __offsetX, destPoint.y + __offsetY); + var colorR = ((__color >> 16) & 0xFF) / 255.0; + var colorG = ((__color >> 8) & 0xFF) / 255.0; + var colorB = (__color & 0xFF) / 255.0; - var finalImage = ImageDataUtil.gaussianBlur(bitmapData.image, sourceBitmapData.image, sourceRect.__toLimeRectangle(), point.__toLimeVector2(), - __blurX, __blurY, __quality, __strength); - finalImage.colorTransform(finalImage.rect, new ColorTransform(0, 0, 0, __alpha, r, g, b, 0).__toLimeColorMatrix()); + var shadowOffsetX = Std.int(__offsetX * __renderScale); + var shadowOffsetY = Std.int(__offsetY * __renderScale); - if (finalImage == bitmapData.image) return bitmapData; - #end - return sourceBitmapData; + var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); + for (y in 0...height) + { + for (x in 0...width) + { + // the shadow at this pixel is the blurred mask read from `offset` pixels back + var maskX = x - shadowOffsetX; + var maskY = y - shadowOffsetY; + var shadowCoverage = ((maskX < 0 || maskX >= width || maskY < 0 || maskY >= height) ? 0.0 : mask[maskY * width + maskX]) * __strength; + + if (shadowCoverage > 1) shadowCoverage = 1; + else if (shadowCoverage < 0) shadowCoverage = 0; + + fxR.push(colorR * shadowCoverage); + fxG.push(colorG * shadowCoverage); + fxB.push(colorB * shadowCoverage); + fxA.push(__alpha * shadowCoverage); + } + } + + // hideObject drops the object just like knockout does + return BitmapFilter.__compositeEffect(bitmapData, sourceBitmapData, sourceRect, destPoint, fxR, fxG, fxB, fxA, __inner ? INNER : OUTER, + __knockout || __hideObject); } @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader @@ -324,25 +351,9 @@ import lime._internal.graphics.ImageDataUtil; // TODO if (blurPass < numBlurPasses) { - var shader = GlowFilter.__blurAlphaShader; - if (blurPass < __horizontalPasses) - { - var scale = Math.pow(0.5, blurPass >> 1) * 0.5; - shader.uRadius.value[0] = blurX * scale; - shader.uRadius.value[1] = 0; - } - else - { - var scale = Math.pow(0.5, (blurPass - __horizontalPasses) >> 1) * 0.5; - shader.uRadius.value[0] = 0; - shader.uRadius.value[1] = blurY * scale; - } - shader.uColor.value[0] = ((color >> 16) & 0xFF) / 255; - shader.uColor.value[1] = ((color >> 8) & 0xFF) / 255; - shader.uColor.value[2] = (color & 0xFF) / 255; - shader.uColor.value[3] = alpha; - shader.uStrength.value[0] = blurPass == (numBlurPasses - 1) ? __strength : 1.0; - return shader; + var strength = blurPass == (numBlurPasses - 1) ? __strength : 1.0; + var horizontal = blurPass < __horizontalPasses; + return GlowFilter.__setupBlurAlphaShader(horizontal, (horizontal ? blurX : blurY) * __renderScale, color, alpha, strength); } if (__inner) { @@ -350,14 +361,14 @@ import lime._internal.graphics.ImageDataUtil; // TODO { var shader = GlowFilter.__innerCombineKnockoutShader; shader.sourceBitmap.input = sourceBitmapData; - shader.offset.value[0] = __offsetX; - shader.offset.value[1] = __offsetY; + shader.offset.value[0] = __offsetX * __renderScale; + shader.offset.value[1] = __offsetY * __renderScale; return shader; } var shader = GlowFilter.__innerCombineShader; shader.sourceBitmap.input = sourceBitmapData; - shader.offset.value[0] = __offsetX; - shader.offset.value[1] = __offsetY; + shader.offset.value[0] = __offsetX * __renderScale; + shader.offset.value[1] = __offsetY * __renderScale; return shader; } else @@ -366,22 +377,22 @@ import lime._internal.graphics.ImageDataUtil; // TODO { var shader = GlowFilter.__combineKnockoutShader; shader.sourceBitmap.input = sourceBitmapData; - shader.offset.value[0] = __offsetX; - shader.offset.value[1] = __offsetY; + shader.offset.value[0] = __offsetX * __renderScale; + shader.offset.value[1] = __offsetY * __renderScale; return shader; } else if (__hideObject) { var shader = __hideShader; shader.sourceBitmap.input = sourceBitmapData; - shader.offset.value[0] = __offsetX; - shader.offset.value[1] = __offsetY; + shader.offset.value[0] = __offsetX * __renderScale; + shader.offset.value[1] = __offsetY * __renderScale; return shader; } var shader = GlowFilter.__combineShader; shader.sourceBitmap.input = sourceBitmapData; - shader.offset.value[0] = __offsetX; - shader.offset.value[1] = __offsetY; + shader.offset.value[0] = __offsetX * __renderScale; + shader.offset.value[1] = __offsetY * __renderScale; return shader; } #else @@ -393,17 +404,27 @@ import lime._internal.graphics.ImageDataUtil; // TODO { __offsetX = Std.int(__distance * Math.cos(__angle * Math.PI / 180)); __offsetY = Std.int(__distance * Math.sin(__angle * Math.PI / 180)); - __topExtension = Math.ceil((__offsetY < 0 ? -__offsetY : 0) + __blurY); - __bottomExtension = Math.ceil((__offsetY > 0 ? __offsetY : 0) + __blurY); - __leftExtension = Math.ceil((__offsetX < 0 ? -__offsetX : 0) + __blurX); - __rightExtension = Math.ceil((__offsetX > 0 ? __offsetX : 0) + __blurX); + + // Box blur applies `quality` passes (low, medium, high). + // each pass widens the shadow by approx. half the blur. + // so the reach grows to approx. `quality * blur / 2`. + // If we only reserve one blur radius the shadow is hard-clipped to a rectangle at high quality. + var q = (__quality > 0) ? __quality : 1; + var exX = Math.ceil(__blurX * 0.5 * q) + 4; + var exY = Math.ceil(__blurY * 0.5 * q) + 4; + + __topExtension = Std.int((__offsetY < 0 ? -__offsetY : 0) + exY); + __bottomExtension = Std.int((__offsetY > 0 ? __offsetY : 0) + exY); + __leftExtension = Std.int((__offsetX < 0 ? -__offsetX : 0) + exX); + __rightExtension = Std.int((__offsetX > 0 ? __offsetX : 0) + exX); __calculateNumShaderPasses(); } @:noCompletion private function __calculateNumShaderPasses():Void { - __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * (__quality / 4)) + 1; - __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * (__quality / 4)) + 1; + var q = (__quality > 0) ? __quality : 1; + __horizontalPasses = (__blurX <= 0) ? 0 : q; + __verticalPasses = (__blurY <= 0) ? 0 : q; __numShaderPasses = __horizontalPasses + __verticalPasses + (__inner ? 2 : 1); } @@ -537,7 +558,12 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private function set_quality(value:Int):Int { - if (value != __quality) __renderDirty = true; + if (value != __quality) + { + __renderDirty = true; + __quality = value; + __updateSize(); // quality affects the size + } return __quality = value; } diff --git a/src/openfl/filters/GlowFilter.hx b/src/openfl/filters/GlowFilter.hx index bb9905ee06..7e82836292 100644 --- a/src/openfl/filters/GlowFilter.hx +++ b/src/openfl/filters/GlowFilter.hx @@ -68,7 +68,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:final class GlowFilter extends BitmapFilter { @:noCompletion private static var __invertAlphaShader = new InvertAlphaShader(); - @:noCompletion private static var __blurAlphaShader = new BlurAlphaShader(); + @:noCompletion private static var __blurAlphaShader = new BoxBlurAlphaShader(); @:noCompletion private static var __combineShader = new CombineShader(); @:noCompletion private static var __innerCombineShader = new InnerCombineShader(); @:noCompletion private static var __combineKnockoutShader = new CombineKnockoutShader(); @@ -246,6 +246,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO __needSecondBitmapData = true; __preserveObject = true; + __softwareComposite = true; __renderDirty = true; } @@ -256,20 +257,35 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private override function __applyFilter(bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData { - // TODO: Support knockout, inner + var width = bitmapData.width; + var height = bitmapData.height; - #if lime - var r = (__color >> 16) & 0xFF; - var g = (__color >> 8) & 0xFF; - var b = __color & 0xFF; + // blur the source alpha into the glow's coverage mask, then colourise it + var mask = BitmapFilter.__alphaMask(sourceBitmapData, sourceRect, destPoint, width, height); + if (__inner) + { + for (i in 0...mask.length) + mask[i] = 1 - mask[i]; + } + BitmapFilter.__blurMask(mask, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); - var finalImage = ImageDataUtil.gaussianBlur(bitmapData.image, sourceBitmapData.image, sourceRect.__toLimeRectangle(), destPoint.__toLimeVector2(), - __blurX, __blurY, __quality, __strength); - finalImage.colorTransform(finalImage.rect, new ColorTransform(0, 0, 0, __alpha, r, g, b, 0).__toLimeColorMatrix()); + var cr = ((__color >> 16) & 0xFF) / 255.0; + var cg = ((__color >> 8) & 0xFF) / 255.0; + var cb = (__color & 0xFF) / 255.0; - if (finalImage == bitmapData.image) return bitmapData; - #end - return sourceBitmapData; + var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); + for (i in 0...width * height) + { + var f = mask[i] * __strength; + if (f > 1) f = 1; + else if (f < 0) f = 0; + fxR.push(cr * f); + fxG.push(cg * f); + fxB.push(cb * f); + fxA.push(__alpha * f); + } + + return BitmapFilter.__compositeEffect(bitmapData, sourceBitmapData, sourceRect, destPoint, fxR, fxG, fxB, fxA, __inner ? INNER : OUTER, __knockout); } @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader @@ -286,25 +302,9 @@ import lime._internal.graphics.ImageDataUtil; // TODO if (blurPass < numBlurPasses) { - var shader = __blurAlphaShader; - if (blurPass < __horizontalPasses) - { - var scale = Math.pow(0.5, blurPass >> 1) * 0.5; - shader.uRadius.value[0] = blurX * scale; - shader.uRadius.value[1] = 0; - } - else - { - var scale = Math.pow(0.5, (blurPass - __horizontalPasses) >> 1) * 0.5; - shader.uRadius.value[0] = 0; - shader.uRadius.value[1] = blurY * scale; - } - shader.uColor.value[0] = ((color >> 16) & 0xFF) / 255; - shader.uColor.value[1] = ((color >> 8) & 0xFF) / 255; - shader.uColor.value[2] = (color & 0xFF) / 255; - shader.uColor.value[3] = alpha; - shader.uStrength.value[0] = blurPass == (numBlurPasses - 1) ? __strength : 1.0; - return shader; + var strength = blurPass == (numBlurPasses - 1) ? __strength : 1.0; + var horizontal = blurPass < __horizontalPasses; + return __setupBlurAlphaShader(horizontal, (horizontal ? blurX : blurY) * __renderScale, color, alpha, strength); } if (__inner) { @@ -345,20 +345,38 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private function __updateSize():Void { - __leftExtension = (__blurX > 0 ? Math.ceil(__blurX * 1.5) : 0); + // Box blur reach grows to ~quality*blur/2 per side (see DropShadowFilter); + var q = (__quality > 0) ? __quality : 1; + __leftExtension = (__blurX > 0 ? Math.ceil(__blurX * 0.5 * q) + 4 : 0); + __topExtension = (__blurY > 0 ? Math.ceil(__blurY * 0.5 * q) + 4 : 0); __rightExtension = __leftExtension; - __topExtension = (__blurY > 0 ? Math.ceil(__blurY * 1.5) : 0); __bottomExtension = __topExtension; __calculateNumShaderPasses(); } @:noCompletion private function __calculateNumShaderPasses():Void { - __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * (__quality / 4)) + 1; - __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * (__quality / 4)) + 1; + // one horizontal + one vertical box pass per quality iteration + var q = (__quality > 0) ? __quality : 1; + __horizontalPasses = (__blurX <= 0) ? 0 : q; + __verticalPasses = (__blurY <= 0) ? 0 : q; __numShaderPasses = __horizontalPasses + __verticalPasses + (__inner ? 2 : 1); } + @:noCompletion private static function __setupBlurAlphaShader(horizontal:Bool, v:Float, color:Int, alpha:Float, strength:Float):BitmapFilterShader + { + var shader = __blurAlphaShader; + shader.uDir.value[0] = horizontal ? 1.0 : 0.0; + shader.uDir.value[1] = horizontal ? 0.0 : 1.0; + shader.uFullSize.value[0] = v > 255 ? 255.0 : v; + shader.uColor.value[0] = ((color >> 16) & 0xFF) / 255; + shader.uColor.value[1] = ((color >> 8) & 0xFF) / 255; + shader.uColor.value[2] = (color & 0xFF) / 255; + shader.uColor.value[3] = alpha; + shader.uStrength.value[0] = strength; + return shader; + } + // Get & Set Methods @:noCompletion private function get_alpha():Float { @@ -454,7 +472,8 @@ import lime._internal.graphics.ImageDataUtil; // TODO if (value != __quality) { __renderDirty = true; - __calculateNumShaderPasses(); + __quality = value; + __updateSize(); // quality affects the size } return __quality = value; } @@ -507,72 +526,70 @@ private class InvertAlphaShader extends BitmapFilterShader @:fileXml('tags="haxe,release"') @:noDebug #end -private class BlurAlphaShader extends BitmapFilterShader +private class BoxBlurAlphaShader extends BitmapFilterShader { - @:glFragmentSource(" - uniform sampler2D openfl_Texture; + @:glFragmentSource("#pragma header + uniform vec4 uColor; uniform float uStrength; - varying vec2 vTexCoord; - varying vec2 vBlurCoords[6]; + uniform vec2 uTextureSize; + uniform vec2 uDir; // blur axis: (1,0) horizontal, (0,1) vertical + uniform float uFullSize; // box width = the blur amount void main(void) { - vec4 texel = texture2D(openfl_Texture, vTexCoord); - - vec3 contributions = vec3(0.00443, 0.05399, 0.24197); - vec3 top = vec3( - texture2D(openfl_Texture, vBlurCoords[0]).a, - texture2D(openfl_Texture, vBlurCoords[1]).a, - texture2D(openfl_Texture, vBlurCoords[2]).a - ); - vec3 bottom = vec3( - texture2D(openfl_Texture, vBlurCoords[3]).a, - texture2D(openfl_Texture, vBlurCoords[4]).a, - texture2D(openfl_Texture, vBlurCoords[5]).a - ); - - float a = texel.a * 0.39894; - a += dot(top, contributions.xyz); - a += dot(bottom, contributions.zyx); + vec2 direction = uDir / uTextureSize; + float fullSize = min(uFullSize, 255.0); + float a; + + if (fullSize <= 1.0) { + a = texture2D(openfl_Texture, openfl_TextureCoordv).a; + } else { + float halfW = fullSize * 0.5; + int n = int(floor(halfW - 0.5)); + float frac = halfW - (float(n) + 0.5); + frac = floor(frac * 255.0) / 255.0; + + float sum = texture2D(openfl_Texture, openfl_TextureCoordv).a; // centre + for (int i = 1; i <= 128; i++) { // full interior + if (i > n) break; + vec2 off = float(i) * direction; + sum += texture2D(openfl_Texture, openfl_TextureCoordv + off).a; + sum += texture2D(openfl_Texture, openfl_TextureCoordv - off).a; + } + vec2 edge = float(n + 1) * direction; // fractional edges + sum += texture2D(openfl_Texture, openfl_TextureCoordv + edge).a * frac; + sum += texture2D(openfl_Texture, openfl_TextureCoordv - edge).a * frac; + + a = sum / fullSize; + } gl_FragColor = uColor * clamp(a * uStrength, 0.0, 1.0); - } - ") - @:glVertexSource(" - attribute vec4 openfl_Position; - attribute vec2 openfl_TextureCoord; - - uniform mat4 openfl_Matrix; - uniform vec2 openfl_TextureSize; - - uniform vec2 uRadius; - varying vec2 vTexCoord; - varying vec2 vBlurCoords[6]; + }") + @:glVertexSource("#pragma header void main(void) { - gl_Position = openfl_Matrix * openfl_Position; - vTexCoord = openfl_TextureCoord; + #pragma body - vec3 offset = vec3(0.5, 0.75, 1.0); - vec2 r = uRadius / openfl_TextureSize; - vBlurCoords[0] = openfl_TextureCoord - r * offset.z; - vBlurCoords[1] = openfl_TextureCoord - r * offset.y; - vBlurCoords[2] = openfl_TextureCoord - r * offset.x; - vBlurCoords[3] = openfl_TextureCoord + r * offset.x; - vBlurCoords[4] = openfl_TextureCoord + r * offset.y; - vBlurCoords[5] = openfl_TextureCoord + r * offset.z; - } - ") + }") public function new() { super(); #if !macro - uRadius.value = [0, 0]; uColor.value = [0, 0, 0, 0]; uStrength.value = [1]; + uDir.value = [1, 0]; + uFullSize.value = [1]; + #end + } + + @:noCompletion private override function __update():Void + { + #if !macro + uTextureSize.value = [__texture.input.width, __texture.input.height]; #end + super.__update(); } } diff --git a/src/openfl/filters/GradientBevelFilter.hx b/src/openfl/filters/GradientBevelFilter.hx new file mode 100644 index 0000000000..7016dedbe5 --- /dev/null +++ b/src/openfl/filters/GradientBevelFilter.hx @@ -0,0 +1,410 @@ +package openfl.filters; + +#if !flash +import openfl.display.BitmapData; +import openfl.display.DisplayObjectRenderer; +import openfl.display.Shader; +import openfl.geom.Point; +import openfl.geom.Rectangle; + +/** + The GradientBevelFilter class lets you apply a gradient bevel effect to display objects. + The bevel's colours come from a gradient defined by `colors`/`alphas`/`ratios` instead of separate highlight/shadow colours. +**/ +#if !openfl_debug +@:fileXml('tags="haxe,release"') +@:noDebug +#end +@:access(openfl.filters.BlurFilter) +@:access(openfl.geom.Point) +@:access(openfl.geom.Rectangle) +@:final class GradientBevelFilter extends BitmapFilter +{ + @:noCompletion private static var __gradientShader = new GradientBevelShader(); + + public var distance(get, set):Float; + public var angle(get, set):Float; + public var colors(get, set):Array; + public var alphas(get, set):Array; + public var ratios(get, set):Array; + public var blurX(get, set):Float; + public var blurY(get, set):Float; + public var strength(get, set):Float; + public var quality(get, set):Int; + public var type(get, set):BitmapFilterType; + public var knockout(get, set):Bool; + + @:noCompletion private var __distance:Float; + @:noCompletion private var __angle:Float; + @:noCompletion private var __colors:Array; + @:noCompletion private var __alphas:Array; + @:noCompletion private var __ratios:Array; + @:noCompletion private var __blurX:Float; + @:noCompletion private var __blurY:Float; + @:noCompletion private var __strength:Float; + @:noCompletion private var __quality:Int; + @:noCompletion private var __type:BitmapFilterType; + @:noCompletion private var __knockout:Bool; + @:noCompletion private var __horizontalPasses:Int; + @:noCompletion private var __verticalPasses:Int; + @:noCompletion private var __ramp:BitmapData; + @:noCompletion private var __rampDirty:Bool; + + public function new(distance:Float = 4, angle:Float = 45, colors:Array = null, alphas:Array = null, ratios:Array = null, + blurX:Float = 4, blurY:Float = 4, strength:Float = 1, quality:Int = 1, type:BitmapFilterType = INNER, knockout:Bool = false) + { + super(); + + __distance = distance; + __angle = angle; + __colors = (colors != null) ? colors : [0xFFFFFF, 0x808080, 0x000000]; + __alphas = (alphas != null) ? alphas : [1, 0, 1]; + __ratios = (ratios != null) ? ratios : [0, 128, 255]; + __blurX = blurX; + __blurY = blurY; + __strength = strength; + __quality = quality; + __type = type; + __knockout = knockout; + __rampDirty = true; + + __needSecondBitmapData = true; + __preserveObject = true; + __softwareComposite = true; + __renderDirty = true; + + __updateSize(); + } + + public override function clone():BitmapFilter + { + return new GradientBevelFilter(__distance, __angle, __colors.copy(), __alphas.copy(), __ratios.copy(), __blurX, __blurY, __strength, __quality, + __type, __knockout); + } + + @:noCompletion private override function __applyFilter(bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData + { + var width = bitmapData.width; + var height = bitmapData.height; + + var mask = BitmapFilter.__alphaMask(sourceBitmapData, sourceRect, destPoint, width, height); + BitmapFilter.__blurMask(mask, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); + + if (__rampDirty) __buildRamp(); + var ramp = __rampChannels(); + + var rad = __angle * Math.PI / 180; + var offsetX = Std.int(Math.round(__distance * Math.cos(rad) * __renderScale)); + var offsetY = Std.int(Math.round(__distance * Math.sin(rad) * __renderScale)); + + var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); + for (y in 0...height) + { + for (x in 0...width) + { + // mask sampled along the light angle: +offset is the way a shadow falls, + // -offset points toward the light (Flash's `angle` is where light comes FROM) + var maskTowardShadow = BitmapFilter.__maskAt(mask, width, height, x + offsetX, y + offsetY); + var maskTowardLight = BitmapFilter.__maskAt(mask, width, height, x - offsetX, y - offsetY); + // signed edge slope -> ramp index, as GradientBevelShader does: positive on + // the edge facing the light, negative on the far edge, zero on flat areas. + // -1 is one edge, 0 the (usually transparent) middle stop, +1 the other + var edgeSlope = (maskTowardShadow - maskTowardLight) * __strength; + if (edgeSlope > 1) edgeSlope = 1; + else if (edgeSlope < -1) edgeSlope = -1; + + var i = Std.int((edgeSlope * 0.5 + 0.5) * 255 + 0.5) * 4; + fxR.push(ramp[i]); + fxG.push(ramp[i + 1]); + fxB.push(ramp[i + 2]); + fxA.push(ramp[i + 3]); + } + } + + return BitmapFilter.__compositeEffect(bitmapData, sourceBitmapData, sourceRect, destPoint, fxR, fxG, fxB, fxA, __type, __knockout); + } + + @:noCompletion private function __rampChannels():Array + { + var out = new Array(); + var pixels = __ramp.getVector(__ramp.rect); + for (i in 0...256) + { + var argb = pixels[i]; + var a = ((argb >>> 24) & 0xFF) / 255.0; + out.push((((argb >> 16) & 0xFF) / 255.0) * a); + out.push((((argb >> 8) & 0xFF) / 255.0) * a); + out.push(((argb & 0xFF) / 255.0) * a); + out.push(a); + } + return out; + } + + @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader + { + #if !macro + var numBlurPasses = __horizontalPasses + __verticalPasses; + if (pass < numBlurPasses) + { + var horizontal = pass < __horizontalPasses; + return BlurFilter.__setupBlurShader(horizontal, (horizontal ? __blurX : __blurY) * __renderScale); + } + + if (__rampDirty) __buildRamp(); + + var rad = __angle * Math.PI / 180; + var shader = __gradientShader; + shader.sourceBitmap.input = sourceBitmapData; + shader.gradientRamp.input = __ramp; + shader.uTransformX.value[0] = __distance * Math.cos(rad) * __renderScale; + shader.uTransformY.value[0] = __distance * Math.sin(rad) * __renderScale; + shader.uStrength.value[0] = __strength; + shader.uBevelType.value[0] = (__type == INNER) ? 0.0 : (__type == OUTER ? 1.0 : 2.0); + shader.uKnockout.value[0] = __knockout; + return shader; + #else + return null; + #end + } + + // 256-entry straight-ARGB gradient ramp (one texel per output index 0..255) from the (colors, alphas, ratios). + // Each index is the colour and alpha linearly interpolated between the two stops it falls between. + @:noCompletion private function __buildRamp():Void + { + if (__ramp == null) __ramp = new BitmapData(256, 1, true, 0); + var stopCount = __colors.length; + var stop = 0; // the stop at or just before the current ramp index + + for (index in 0...256) + { + // advance to the stop pair whose ratio range contains `index` + while (stop < stopCount - 1 && __ratios[stop + 1] < index) stop++; + + var colorLo = __colors[stop]; + var alphaLo = __alphas[stop]; + var r:Float, g:Float, b:Float, a:Float; + + if (stop >= stopCount - 1 || index <= __ratios[stop]) + { + // before the first stop, or past the last one: hold this stop's colour flat + r = (colorLo >> 16) & 0xFF; + g = (colorLo >> 8) & 0xFF; + b = colorLo & 0xFF; + a = alphaLo * 255; + } + else + { + var ratioLo = __ratios[stop]; + var ratioHi = __ratios[stop + 1]; + var colorHi = __colors[stop + 1]; + var alphaHi = __alphas[stop + 1]; + + // blend = how far `index` sits between the two stops (0 at the low stop, 1 at the high stop) + var blend = (ratioHi > ratioLo) ? (index - ratioLo) / (ratioHi - ratioLo) : 0.0; + + r = lerp((colorLo >> 16) & 0xFF, (colorHi >> 16) & 0xFF, blend); + g = lerp((colorLo >> 8) & 0xFF, (colorHi >> 8) & 0xFF, blend); + b = lerp(colorLo & 0xFF, colorHi & 0xFF, blend); + a = lerp(alphaLo, alphaHi, blend) * 255; + } + + var argb = (Std.int(a) << 24) | (Std.int(r) << 16) | (Std.int(g) << 8) | Std.int(b); + __ramp.setPixel32(index, 0, argb); + } + __rampDirty = false; + } + + @:noCompletion private static inline function lerp(a:Float, b:Float, t:Float):Float + { + return a + (b - a) * t; + } + + @:noCompletion private function __updateSize():Void + { + // size calculation: box-blur support ( quality * blur/2 ) + transform offset(abs(distance*cos/sin)). + + var rad = __angle * Math.PI / 180; + // per-axis offset, ceil(abs) so negative angles don't drop a pixel + var offsetX:Int = (__type != INNER) ? Math.ceil(Math.abs(__distance * Math.cos(rad))) : 0; + var offsetY:Int = (__type != INNER) ? Math.ceil(Math.abs(__distance * Math.sin(rad))) : 0; + + var q = (__quality > 0) ? __quality : 1; + var exX = Math.ceil(__blurX * 0.5 * q); + var exY = Math.ceil(__blurY * 0.5 * q); + + __leftExtension = __rightExtension = exX + offsetX; + __topExtension = __bottomExtension = exY + offsetY; + + __horizontalPasses = (__blurX <= 0) ? 0 : q; + __verticalPasses = (__blurY <= 0) ? 0 : q; + __numShaderPasses = __horizontalPasses + __verticalPasses + 1; + } + + // Getters / setters + @:noCompletion private function get_distance():Float return __distance; + + @:noCompletion private function set_distance(v:Float):Float + { + if (v != __distance) { __distance = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_angle():Float return __angle; + + @:noCompletion private function set_angle(v:Float):Float + { + if (v != __angle) { __angle = v; __renderDirty = true; } + return v; + } + + @:noCompletion private function get_colors():Array return __colors; + + @:noCompletion private function set_colors(v:Array):Array + { + __colors = v; __rampDirty = true; __renderDirty = true; + return v; + } + + @:noCompletion private function get_alphas():Array return __alphas; + + @:noCompletion private function set_alphas(v:Array):Array + { + __alphas = v; __rampDirty = true; __renderDirty = true; + return v; + } + + @:noCompletion private function get_ratios():Array return __ratios; + + @:noCompletion private function set_ratios(v:Array):Array + { + __ratios = v; __rampDirty = true; __renderDirty = true; + return v; + } + + @:noCompletion private function get_blurX():Float return __blurX; + + @:noCompletion private function set_blurX(v:Float):Float + { + if (v != __blurX) { __blurX = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_blurY():Float return __blurY; + + @:noCompletion private function set_blurY(v:Float):Float + { + if (v != __blurY) { __blurY = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_strength():Float return __strength; + + @:noCompletion private function set_strength(v:Float):Float + { + if (v != __strength) { __strength = v; __renderDirty = true; } + return v; + } + + @:noCompletion private function get_quality():Int return __quality; + + @:noCompletion private function set_quality(v:Int):Int + { + if (v != __quality) { __quality = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_type():BitmapFilterType return __type; + + @:noCompletion private function set_type(v:BitmapFilterType):BitmapFilterType + { + if (v != __type) { __type = v; __renderDirty = true; } + return v; + } + + @:noCompletion private function get_knockout():Bool return __knockout; + + @:noCompletion private function set_knockout(v:Bool):Bool + { + if (v != __knockout) { __knockout = v; __renderDirty = true; } + return v; + } +} + +#if !openfl_debug +@:fileXml('tags="haxe,release"') +@:noDebug +#end +private class GradientBevelShader extends BitmapFilterShader +{ + @:glFragmentSource("uniform sampler2D openfl_Texture; + uniform sampler2D sourceBitmap; + uniform sampler2D gradientRamp; + uniform float uBevelType; + uniform bool uKnockout; + uniform float uStrength; + varying vec2 vTextureCoord; + varying vec2 vTransform; + + void main(void) { + vec4 dest = texture2D(sourceBitmap, vTextureCoord); + vec2 uvTowardShadow = vTextureCoord + vTransform; + vec2 uvTowardLight = vTextureCoord - vTransform; + float maskTowardShadow = texture2D(openfl_Texture, uvTowardShadow).a; + float maskTowardLight = texture2D(openfl_Texture, uvTowardLight).a; + if (uvTowardShadow.x<0.0||uvTowardShadow.x>1.0||uvTowardShadow.y<0.0||uvTowardShadow.y>1.0) maskTowardShadow = 0.0; + if (uvTowardLight.x<0.0||uvTowardLight.x>1.0||uvTowardLight.y<0.0||uvTowardLight.y>1.0) maskTowardLight = 0.0; + + // signed distance field -> ramp index (-1 = one edge/ratio 0, + // 0 = base/ratio 128, +1 = other edge/ratio 255) + float sd = clamp((maskTowardShadow - maskTowardLight) * uStrength, -1.0, 1.0); + vec4 glow = texture2D(gradientRamp, vec2(sd * 0.5 + 0.5, 0.5)); + + if (uBevelType == 0.0) { + if (uKnockout) gl_FragColor = glow * dest.a; + else gl_FragColor = glow * dest.a + dest * (1.0 - glow.a); + } else if (uBevelType == 1.0) { + if (uKnockout) gl_FragColor = glow - glow * dest.a; + else gl_FragColor = dest + glow - glow * dest.a; + } else { + if (uKnockout) gl_FragColor = glow; + else gl_FragColor = dest - dest * glow.a + glow; + } + }") + + @:glVertexSource("attribute vec4 openfl_Position; + attribute vec2 openfl_TextureCoord; + uniform mat4 openfl_Matrix; + uniform vec2 uTextureSize; + uniform float uTransformX; + uniform float uTransformY; + varying vec2 vTextureCoord; + varying vec2 vTransform; + + void main(void) { + gl_Position = openfl_Matrix * openfl_Position; + vTextureCoord = openfl_TextureCoord; + vTransform = vec2(uTransformX / uTextureSize.x, uTransformY / uTextureSize.y); + }") + public function new() + { + super(); + #if !macro + uTransformX.value = [0]; + uTransformY.value = [0]; + uBevelType.value = [0.0]; + uKnockout.value = [false]; + uStrength.value = [1]; + #end + } + + @:noCompletion private override function __update():Void + { + #if !macro + uTextureSize.value = [__texture.input.width, __texture.input.height]; + #end + super.__update(); + } +} +#else +typedef GradientBevelFilter = flash.filters.GradientBevelFilter; +#end diff --git a/src/openfl/filters/GradientGlowFilter.hx b/src/openfl/filters/GradientGlowFilter.hx new file mode 100644 index 0000000000..e17f933e9b --- /dev/null +++ b/src/openfl/filters/GradientGlowFilter.hx @@ -0,0 +1,394 @@ +package openfl.filters; + +#if !flash +import openfl.display.BitmapData; +import openfl.display.DisplayObjectRenderer; +import openfl.display.Shader; +import openfl.geom.Point; +import openfl.geom.Rectangle; + +/** + The GradientGlowFilter class lets you apply a gradient glow effect to display objects. + The glow colours come from a gradient defined by `colors`/`alphas`/`ratios` instead of a single colour. +**/ +#if !openfl_debug +@:fileXml('tags="haxe,release"') +@:noDebug +#end +@:access(openfl.filters.BlurFilter) +@:access(openfl.geom.Point) +@:access(openfl.geom.Rectangle) +@:final class GradientGlowFilter extends BitmapFilter +{ + @:noCompletion private static var __gradientShader = new GradientGlowShader(); + + public var distance(get, set):Float; + public var angle(get, set):Float; + public var colors(get, set):Array; + public var alphas(get, set):Array; + public var ratios(get, set):Array; + public var blurX(get, set):Float; + public var blurY(get, set):Float; + public var strength(get, set):Float; + public var quality(get, set):Int; + public var type(get, set):BitmapFilterType; + public var knockout(get, set):Bool; + + @:noCompletion private var __distance:Float; + @:noCompletion private var __angle:Float; + @:noCompletion private var __colors:Array; + @:noCompletion private var __alphas:Array; + @:noCompletion private var __ratios:Array; + @:noCompletion private var __blurX:Float; + @:noCompletion private var __blurY:Float; + @:noCompletion private var __strength:Float; + @:noCompletion private var __quality:Int; + @:noCompletion private var __type:BitmapFilterType; + @:noCompletion private var __knockout:Bool; + @:noCompletion private var __offsetX:Int; + @:noCompletion private var __offsetY:Int; + @:noCompletion private var __horizontalPasses:Int; + @:noCompletion private var __verticalPasses:Int; + @:noCompletion private var __ramp:BitmapData; + @:noCompletion private var __rampDirty:Bool; + + public function new(distance:Float = 4, angle:Float = 45, colors:Array = null, alphas:Array = null, ratios:Array = null, + blurX:Float = 4, blurY:Float = 4, strength:Float = 1, quality:Int = 1, type:BitmapFilterType = OUTER, knockout:Bool = false) + { + super(); + + __distance = distance; + __angle = angle; + __colors = (colors != null) ? colors : [0xFFFFFF, 0xFFFFFF]; + __alphas = (alphas != null) ? alphas : [0, 1]; + __ratios = (ratios != null) ? ratios : [0, 255]; + __blurX = blurX; + __blurY = blurY; + __strength = strength; + __quality = quality; + __type = type; + __knockout = knockout; + __rampDirty = true; + + __needSecondBitmapData = true; + __preserveObject = true; + __softwareComposite = true; + __renderDirty = true; + + __updateSize(); + } + + public override function clone():BitmapFilter + { + return new GradientGlowFilter(__distance, __angle, __colors.copy(), __alphas.copy(), __ratios.copy(), __blurX, __blurY, __strength, __quality, + __type, __knockout); + } + + @:noCompletion private override function __applyFilter(bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData + { + var width = bitmapData.width; + var height = bitmapData.height; + + // blurred source alpha, read back shifted by distance/angle (as the GL path samples the mask at coord - offset) + var mask = BitmapFilter.__alphaMask(sourceBitmapData, sourceRect, destPoint, width, height); + BitmapFilter.__blurMask(mask, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); + + if (__rampDirty) __buildRamp(); + var ramp = __rampChannels(); + + var glowOffsetX = Std.int(__offsetX * __renderScale); + var glowOffsetY = Std.int(__offsetY * __renderScale); + + var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); + for (y in 0...height) + { + for (x in 0...width) + { + var glowCoverage = BitmapFilter.__maskAt(mask, width, height, x - glowOffsetX, y - glowOffsetY) * __strength; + if (glowCoverage > 1) glowCoverage = 1; + else if (glowCoverage < 0) glowCoverage = 0; + + // index the ramp by the mask, exactly as GradientGlowShader does + var i = Std.int(glowCoverage * 255 + 0.5) * 4; + fxR.push(ramp[i]); + fxG.push(ramp[i + 1]); + fxB.push(ramp[i + 2]); + fxA.push(ramp[i + 3]); + } + } + + return BitmapFilter.__compositeEffect(bitmapData, sourceBitmapData, sourceRect, destPoint, fxR, fxG, fxB, fxA, __type, __knockout); + } + + // 256-entry ramp as flat premultiplied [r,g,b,a] floats, matching how the ramp BitmapData is premultiplied when uploaded as a texture. + @:noCompletion private function __rampChannels():Array + { + var out = new Array(); + var pixels = __ramp.getVector(__ramp.rect); + for (i in 0...256) + { + var argb = pixels[i]; + var a = ((argb >>> 24) & 0xFF) / 255.0; + out.push((((argb >> 16) & 0xFF) / 255.0) * a); + out.push((((argb >> 8) & 0xFF) / 255.0) * a); + out.push(((argb & 0xFF) / 255.0) * a); + out.push(a); + } + return out; + } + + @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader + { + #if !macro + var numBlurPasses = __horizontalPasses + __verticalPasses; + if (pass < numBlurPasses) + { + // blur the object's alpha into a soft distance field (reuse BlurFilter) + var horizontal = pass < __horizontalPasses; + return BlurFilter.__setupBlurShader(horizontal, (horizontal ? __blurX : __blurY) * __renderScale); + } + + if (__rampDirty) __buildRamp(); + + var shader = __gradientShader; + shader.sourceBitmap.input = sourceBitmapData; + shader.gradientRamp.input = __ramp; + shader.offset.value[0] = __offsetX * __renderScale; + shader.offset.value[1] = __offsetY * __renderScale; + shader.uStrength.value[0] = __strength; + shader.uInner.value[0] = (__type == INNER) ? 1.0 : 0.0; + shader.uFull.value[0] = (__type == FULL) ? 1.0 : 0.0; + shader.uKnockout.value[0] = __knockout ? 1.0 : 0.0; + return shader; + #else + return null; + #end + } + + // Build the 256-entry straight-ARGB gradient ramp (one texel per output index 0..255) from the (colors, alphas, ratios). + // Each index is the colour and alpha linearly interpolated between the two stops it falls between. + @:noCompletion private function __buildRamp():Void + { + if (__ramp == null) __ramp = new BitmapData(256, 1, true, 0); + var stopCount = __colors.length; + var stop = 0; // the stop at or just before the current ramp index + + for (index in 0...256) + { + // advance to the stop pair whose ratio range contains `index` + while (stop < stopCount - 1 && __ratios[stop + 1] < index) stop++; + + var colorLo = __colors[stop]; + var alphaLo = __alphas[stop]; + var r:Float, g:Float, b:Float, a:Float; + + if (stop >= stopCount - 1 || index <= __ratios[stop]) + { + // before the first stop, or past the last one: hold this stop's colour flat + r = (colorLo >> 16) & 0xFF; + g = (colorLo >> 8) & 0xFF; + b = colorLo & 0xFF; + a = alphaLo * 255; + } + else + { + var ratioLo = __ratios[stop]; + var ratioHi = __ratios[stop + 1]; + var colorHi = __colors[stop + 1]; + var alphaHi = __alphas[stop + 1]; + + // blend = how far `index` sits between the two stops (0 at the low + // stop, 1 at the high stop) + var blend = (ratioHi > ratioLo) ? (index - ratioLo) / (ratioHi - ratioLo) : 0.0; + + r = lerp((colorLo >> 16) & 0xFF, (colorHi >> 16) & 0xFF, blend); + g = lerp((colorLo >> 8) & 0xFF, (colorHi >> 8) & 0xFF, blend); + b = lerp(colorLo & 0xFF, colorHi & 0xFF, blend); + a = lerp(alphaLo, alphaHi, blend) * 255; + } + + var argb = (Std.int(a) << 24) | (Std.int(r) << 16) | (Std.int(g) << 8) | Std.int(b); + __ramp.setPixel32(index, 0, argb); + } + __rampDirty = false; + } + + @:noCompletion private static inline function lerp(a:Float, b:Float, t:Float):Float + { + return a + (b - a) * t; + } + + @:noCompletion private function __updateSize():Void + { + __offsetX = Std.int(__distance * Math.cos(__angle * Math.PI / 180)); + __offsetY = Std.int(__distance * Math.sin(__angle * Math.PI / 180)); + // Box blur reach grows to approx. `quality * blur / 2` per side (see DropShadowFilter); + // reserve the full spread so the gradient glow isn't clipped at high quality. + var q = (__quality > 0) ? __quality : 1; + var exX = Math.ceil(__blurX * 0.5 * q) + 4; + var exY = Math.ceil(__blurY * 0.5 * q) + 4; + __topExtension = (__offsetY < 0 ? -__offsetY : 0) + exY; + __bottomExtension = (__offsetY > 0 ? __offsetY : 0) + exY; + __leftExtension = (__offsetX < 0 ? -__offsetX : 0) + exX; + __rightExtension = (__offsetX > 0 ? __offsetX : 0) + exX; + + __horizontalPasses = (__blurX <= 0) ? 0 : q; + __verticalPasses = (__blurY <= 0) ? 0 : q; + __numShaderPasses = __horizontalPasses + __verticalPasses + 1; + } + + @:noCompletion private function get_distance():Float return __distance; + + @:noCompletion private function set_distance(v:Float):Float + { + if (v != __distance) { __distance = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_angle():Float return __angle; + + @:noCompletion private function set_angle(v:Float):Float + { + if (v != __angle) { __angle = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_colors():Array return __colors; + + @:noCompletion private function set_colors(v:Array):Array + { + __colors = v; __rampDirty = true; __renderDirty = true; + return v; + } + + @:noCompletion private function get_alphas():Array return __alphas; + + @:noCompletion private function set_alphas(v:Array):Array + { + __alphas = v; __rampDirty = true; __renderDirty = true; + return v; + } + + @:noCompletion private function get_ratios():Array return __ratios; + + @:noCompletion private function set_ratios(v:Array):Array + { + __ratios = v; __rampDirty = true; __renderDirty = true; + return v; + } + + @:noCompletion private function get_blurX():Float return __blurX; + + @:noCompletion private function set_blurX(v:Float):Float + { + if (v != __blurX) { __blurX = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_blurY():Float return __blurY; + + @:noCompletion private function set_blurY(v:Float):Float + { + if (v != __blurY) { __blurY = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_strength():Float return __strength; + + @:noCompletion private function set_strength(v:Float):Float + { + if (v != __strength) { __strength = v; __renderDirty = true; } + return v; + } + + @:noCompletion private function get_quality():Int return __quality; + + @:noCompletion private function set_quality(v:Int):Int + { + if (v != __quality) { __quality = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_type():BitmapFilterType return __type; + + @:noCompletion private function set_type(v:BitmapFilterType):BitmapFilterType + { + if (v != __type) { __type = v; __renderDirty = true; } + return v; + } + + @:noCompletion private function get_knockout():Bool return __knockout; + + @:noCompletion private function set_knockout(v:Bool):Bool + { + if (v != __knockout) { __knockout = v; __renderDirty = true; } + return v; + } +} + +#if !openfl_debug +@:fileXml('tags="haxe,release"') +@:noDebug +#end +private class GradientGlowShader extends BitmapFilterShader +{ + @:glFragmentSource(" + uniform sampler2D openfl_Texture; + uniform sampler2D sourceBitmap; + uniform sampler2D gradientRamp; + uniform float uStrength; + uniform float uInner; + uniform float uFull; + uniform float uKnockout; + varying vec4 textureCoords; + + void main(void) { + vec4 src = texture2D(sourceBitmap, textureCoords.xy); + float mask = texture2D(openfl_Texture, textureCoords.zw).a; + float f = clamp(mask * uStrength, 0.0, 1.0); + + // index the ramp by the distance field (high near the shape = inner + // ratio 255, low far away = outer ratio 0), same for every type. + vec4 g = texture2D(gradientRamp, vec2(f, 0.5)); + + if (uInner > 0.5) { + vec4 inner = g * src.a; + if (uKnockout > 0.5) gl_FragColor = inner; + else gl_FragColor = src * (1.0 - inner.a) + inner; + } else if (uFull > 0.5) { + if (uKnockout > 0.5) gl_FragColor = g; + else gl_FragColor = src * (1.0 - g.a) + g; + } else { + vec4 outer = g * (1.0 - src.a); + if (uKnockout > 0.5) gl_FragColor = outer; + else gl_FragColor = src + outer; + } + } + ") + @:glVertexSource("attribute vec4 openfl_Position; + attribute vec2 openfl_TextureCoord; + uniform mat4 openfl_Matrix; + uniform vec2 openfl_TextureSize; + uniform vec2 offset; + varying vec4 textureCoords; + + void main(void) { + gl_Position = openfl_Matrix * openfl_Position; + textureCoords = vec4(openfl_TextureCoord, openfl_TextureCoord - offset / openfl_TextureSize); + } + ") + public function new() + { + super(); + #if !macro + offset.value = [0, 0]; + uStrength.value = [1]; + uInner.value = [0]; + uFull.value = [0]; + uKnockout.value = [0]; + #end + } +} +#else +typedef GradientGlowFilter = flash.filters.GradientGlowFilter; +#end