diff --git a/examples/widgets/effectwidget.py b/examples/widgets/effectwidget.py new file mode 100644 index 000000000..c173b8612 --- /dev/null +++ b/examples/widgets/effectwidget.py @@ -0,0 +1,164 @@ +''' +Example usage of the effectwidget. + +Currently highly experimental. +''' + +from kivy.app import App +from kivy.uix.effectwidget import EffectWidget +from kivy.uix.image import Image +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.scatter import Scatter +from kivy.uix.button import Button +from kivy.uix.spinner import Spinner +from kivy.uix.boxlayout import BoxLayout +from kivy.lang import Builder +from kivy.properties import ObjectProperty + +from kivy.uix.effectwidget import (MonochromeEffect, + InvertEffect, + ScanlinesEffect, + ChannelMixEffect, + ScanlinesEffect, + FXAAEffect, + PixelateEffect, + HorizontalBlurEffect, + VerticalBlurEffect) + + +class ComparisonWidget(EffectWidget): + pass + + +class ComparisonWidget(EffectWidget): + pass + + +class EffectSpinner(Spinner): + pass + + +class SpinnerRow(BoxLayout): + effectwidget = ObjectProperty() + + def update_effectwidget(self, *args): + effects = [] + for child in self.children[::-1]: + text = child.text + if text == 'none': + pass + if text == 'fxaa': + effects.append(FXAAEffect()) + if text == 'monochrome': + effects.append(MonochromeEffect()) + if text == 'invert': + effects.append(InvertEffect()) + if text == 'mix': + effects.append(ChannelMixEffect()) + if text == 'flash': + effects.append(FlashEffect()) + if text == 'blur_h': + effects.append(HorizontalBlurEffect()) + if text == 'blur_v': + effects.append(VerticalBlurEffect()) + if text == 'postprocessing': + effects.append(ScanlinesEffect()) + if text == 'pixelate': + effects.append(PixelateEffect()) + + if self.effectwidget: + self.effectwidget.effects = effects + + +example = Builder.load_string(''' +#:import Vector kivy.vector.Vector +BoxLayout: + orientation: 'vertical' + BoxLayout: + ComparisonWidget: + id: effect1 + ComparisonWidget: + id: effect2 + SpinnerRow: + effectwidget: effect1 + text: 'left effects' + SpinnerRow: + effectwidget: effect2 + text: 'right effects' + + +: + Widget: + canvas: + Color: + rgba: 1, 0, 0, 1 + Ellipse: + pos: Vector(self.pos) + 0.5*Vector(self.size) + size: 0.4*Vector(self.size) + Color: + rgba: 0, 1, 0.3, 1 + Ellipse: + pos: Vector(self.pos) + 0.1*Vector(self.size) + size: 0.6*Vector(self.size) + Color: + rgba: 0.5, 0.3, 0.8, 1 + Ellipse: + pos: Vector(self.pos) + Vector([0, 0.6])*Vector(self.size) + size: 0.4*Vector(self.size) + Color: + rgba: 1, 0.8, 0.1, 1 + Ellipse: + pos: Vector(self.pos) + Vector([0.5, 0])*Vector(self.size) + size: 0.4*Vector(self.size) + Color: + rgba: 0, 0, 0.8, 1 + Line: + points: + [self.x, self.y, + self.x + self.width, self.y + 0.3*self.height, + self.x + 0.2*self.width, self.y + 0.1*self.height, + self.x + 0.85*self.width, self.y + 0.72*self.height, + self.x + 0.31*self.width, self.y + 0.6*self.height, + self.x, self.top] + width: 1 + Color: + rgba: 0, 0.9, 0.1, 1 + Line: + points: + [self.x + self.width, self.y + self.height, + self.x + 0.35*self.width, self.y + 0.6*self.height, + self.x + 0.7*self.width, self.y + 0.15*self.height, + self.x + 0.2*self.width, self.y + 0.22*self.height, + self.x + 0.3*self.width, self.y + 0.92*self.height] + width: 2 + +: + orientation: 'horizontal' + size_hint_y: None + height: dp(40) + text: '' + Label: + text: root.text + EffectSpinner: + on_text: root.update_effectwidget() + EffectSpinner: + on_text: root.update_effectwidget() + EffectSpinner: + on_text: root.update_effectwidget() + +: + text: 'none' + values: + ['none', 'fxaa', 'monochrome', + 'invert', 'mix', + 'blur_h', 'blur_v', + 'postprocessing', 'pixelate',] +''') + + +class EffectApp(App): + def build(self): + return example + + +EffectApp().run() diff --git a/examples/widgets/effectwidget2.py b/examples/widgets/effectwidget2.py new file mode 100644 index 000000000..cd9701ba2 --- /dev/null +++ b/examples/widgets/effectwidget2.py @@ -0,0 +1,46 @@ +''' +This is an example of creating your own effect by writing a glsl string. +''' + +from kivy.base import runTouchApp +from kivy.lang import Builder +from kivy.uix.effectwidget import EffectWidget, EffectBase + + +# The effect string is glsl code defining an effect function. +effect_string = ''' +vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) +{ + // Note that time is a uniform variable that is automatically + // provided to all effects. + float red = color.x * abs(sin(time*2.0)); + float green = color.y; // No change + float blue = color.z * (1.0 - abs(sin(time*2.0))); + return vec4(red, green, blue, color.w); +} +''' + + +class DemoEffect(EffectWidget): + def __init__(self, *args, **kwargs): + self.effect_reference = EffectBase(glsl=effect_string) + super(DemoEffect, self).__init__(*args, **kwargs) + + +widget = Builder.load_string(''' +DemoEffect: + effects: [self.effect_reference] if checkbox.active else [] + orientation: 'vertical' + Button: + text: 'Some text so you can see what happens.' + BoxLayout: + size_hint_y: None + height: dp(50) + Label: + text: 'Enable effect?' + CheckBox: + id: checkbox + active: True +''') + +runTouchApp(widget) diff --git a/examples/widgets/effectwidget3_advanced.py b/examples/widgets/effectwidget3_advanced.py new file mode 100644 index 000000000..6b9e18c8f --- /dev/null +++ b/examples/widgets/effectwidget3_advanced.py @@ -0,0 +1,62 @@ +''' +This example demonstrates creating and usind an AdvancedEffectBase. In +this case, we use it to efficiently pass the touch coordinates into the shader. +''' + +from kivy.base import runTouchApp +from kivy.properties import ListProperty +from kivy.lang import Builder +from kivy.uix.effectwidget import EffectWidget, AdvancedEffectBase + + +effect_string = ''' +uniform vec2 touch; + +vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) +{ + vec2 distance = 0.025*(coords - touch); + float dist_mag = (distance.x*distance.x + distance.y*distance.y); + vec3 multiplier = vec3(abs(sin(dist_mag - time))); + return vec4(multiplier * color.xyz, 1.0); +} +''' + + +class TouchEffect(AdvancedEffectBase): + touch = ListProperty([0.0, 0.0]) + + def __init__(self, *args, **kwargs): + super(TouchEffect, self).__init__(*args, **kwargs) + self.glsl = effect_string + + self.uniforms = {'touch': [0.0, 0.0]} + + def on_touch(self, *args, **kwargs): + self.uniforms['touch'] = [float(i) for i in self.touch] + + +class TouchWidget(EffectWidget): + def __init__(self, *args, **kwargs): + super(TouchWidget, self).__init__(*args, **kwargs) + self.effect = TouchEffect() + self.effects = [self.effect] + + def on_touch_down(self, touch): + super(TouchWidget, self).on_touch_down(touch) + self.on_touch_move(touch) + + def on_touch_move(self, touch): + self.effect.touch = touch.pos + + +root = Builder.load_string(''' +TouchWidget: + Button: + text: 'Some text!' + Image: + source: 'data/logo/kivy-icon-512.png' + allow_stretch: True + keep_ratio: False +''') + +runTouchApp(root) diff --git a/kivy/data/style.kv b/kivy/data/style.kv index aab9a79cc..18e8b1950 100644 --- a/kivy/data/style.kv +++ b/kivy/data/style.kv @@ -112,6 +112,15 @@ size: self.norm_image_size pos: self.center_x - self.norm_image_size[0] / 2., self.center_y - self.norm_image_size[1] / 2. +: + canvas: + Color: + rgba: 1, 1, 1, 1 + Rectangle: + texture: self.texture + pos: self.pos + size: self.size + rows: 1 padding: 3 diff --git a/kivy/factory_registers.py b/kivy/factory_registers.py index 605a65cdc..a2d07a643 100644 --- a/kivy/factory_registers.py +++ b/kivy/factory_registers.py @@ -104,6 +104,7 @@ r('Carousel', module='kivy.uix.carousel') r('CodeInput', module='kivy.uix.codeinput') r('CheckBox', module='kivy.uix.checkbox') r('DropDown', module='kivy.uix.dropdown') +r('EffectWidget', module='kivy.uix.effectwidget') r('FloatLayout', module='kivy.uix.floatlayout') r('RelativeLayout', module='kivy.uix.relativelayout') r('ScatterLayout', module='kivy.uix.scatterlayout') diff --git a/kivy/uix/effectwidget.py b/kivy/uix/effectwidget.py new file mode 100644 index 000000000..18d58390e --- /dev/null +++ b/kivy/uix/effectwidget.py @@ -0,0 +1,738 @@ +''' +EffectWidget +============ + +.. versionadded:: 1.8.1 + +The :class:`EffectWidget` is able to apply a variety of fancy +graphical effects to +its children. It works by rendering to a series of +:class:`~kivy.graphics.Fbo` instances with custom opengl fragment shaders. +As such, effects can freely do almost anything, from inverting the +colors of the widget, to antialiasing, to emulating the appearance of a +crt monitor! + +The basic usage is as follows:: + + w = EffectWidget() + w.add_widget(Button(text='Hello!') + w.effects = [InvertEffect(), HorizontalBlurEffect(size=2.0)] + +The effects can be a list of effects of any length, and they will be +applied sequentially. + +The module comes with a range of prebuilt effects, but the interface +is designed to make it easy to create your own. Instead of writing a +full glsl shader, you provide a single function that takes +some inputs based on the screen (current pixel color, current widget +texture etc.). See the sections below for more information. + +.. note:: It is not efficient to resize an :class:`EffectWidget`, as + each :class:`~kivy.graphics.Fbo` is recreated every time. + If you need to resize frequently, consider doing things a + different way. + +.. note:: Although some effects have adjustable parameters, it is + *not* efficient to animate these, as the entire + shader is reconstructed every time. You should use glsl + uniform variables instead. The :class:`AdvancedEffectBase` + may make this easier. + +.. note:: The :class:`EffectWidget` *cannot* draw outside its own + widget area (pos -> pos + size), any child widgets + overlapping the boundary will be cut off at this point. + +Provided Effects +---------------- + +The module comes with several pre-written effects. Some have +adjustable properties (e.g. blur radius), see the individual +effect documentation for more details. + +- :class:`MonochromeEffect` - makes the widget grayscale. +- :class:`InvertEffect` - inverts the widget colors. +- :class:`ChannelMixEffect` - swaps around color channels. +- :class:`ScanlinesEffect` - displays flickering scanlines. +- :class:`PixelateEffect` - pixelates the image. +- :class:`HorizontalBlurEffect` - Gaussuan blurs horizontally. +- :class:`VerticalBlurEffect` - Gaussuan blurs vertically. +- :class:`FXAAEffect` - applies a very basic AA. + +Creating Effects +---------------- + +Effects are designed to make it easy to create and use your own +transformations. You do this by creating and using an instance of +:class:`EffectBase` with your own custom :attr:`EffectBase.glsl` +property. + +The glsl property is a string representing part of a glsl fragment +shader. You can include as many functions as you like (the string +is simply spliced into the whole shader), but it +must implement a function :code:`effect` as below:: + + vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) + { + // ... your code here + return something; // must be a vec4 representing the new color + } + +The full shader will calculate the normal pixel colour at each point, +then call your :code:`effect` function to transform it. The +parameters are: + +- **color**: The normal colour of the current pixel (i.e. texture + sampled at tex_coords). +- **texture**: The texture containing the widget's normal background. +- **tex_coords**: The normal texture_coords used to access texture. +- **coords**: The pixel indices of the current pixel. + +The shader code also has access to two useful uniform variables, +:code:`time` containing the time (in seconds) since the program start, +and :code:`resolution` containing the shape (x pixels, y pixels) of +the widget. + +For instance, the following simple string (taken from the `InvertEffect`) +would invert the input color but set alpha to 1.0:: + + vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) + { + return vec4(1.0 - color.xyz, 1.0); + } + +You can also set the glsl by automatically loading the string from a +file, simply set the :attr:`EffectBase.source` property of an effect. + +''' + +from kivy.clock import Clock +from kivy.uix.boxlayout import BoxLayout +from kivy.properties import (StringProperty, ObjectProperty, ListProperty, + NumericProperty, DictProperty) +from kivy.graphics import (RenderContext, Fbo, Color, Rectangle, + Translate, PushMatrix, PopMatrix) +from kivy.event import EventDispatcher +from kivy.base import EventLoop +from kivy.resources import resource_find + +__all__ = ('EffectWidget', 'EffectBase', 'AdvancedEffectBase', + 'MonochromeEffect', 'InvertEffect', 'ChannelMixEffect', + 'ScanlinesEffect', 'PixelateEffect', + 'HorizontalBlurEffect', 'VerticalBlurEffect', + 'FXAAEffect') + +shader_header = ''' +#ifdef GL_ES +precision highp float; +#endif + +/* Outputs from the vertex shader */ +varying vec4 frag_color; +varying vec2 tex_coord0; + +/* uniform texture samplers */ +uniform sampler2D texture0; +''' + +shader_uniforms = ''' +uniform vec2 resolution; +uniform float time; +''' + +shader_footer_trivial = ''' +void main (void){ + gl_FragColor = frag_color * texture2D(texture0, tex_coord0); +} +''' + +shader_footer_effect = ''' +void main (void){ + vec4 normal_color = frag_color * texture2D(texture0, tex_coord0); + vec4 effect_color = effect(normal_color, texture0, tex_coord0, + gl_FragCoord.xy); + gl_FragColor = effect_color; +} +''' + + +effect_trivial = ''' +vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) +{ + return color; +} +''' + +effect_monochrome = ''' +vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) +{ + float mag = 1.0/3.0 * (color.x + color.y + color.z); + return vec4(mag, mag, mag, color.w); +} +''' + +effect_invert = ''' +vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) +{ + return vec4(1.0 - color.xyz, 1.0); +} +''' + +effect_mix = ''' +vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) +{{ + return vec4(color.{}, color.{}, color.{}, 1.0); +}} +''' + +effect_blur_h = ''' +vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) +{{ + float dt = ({} / 4.0) * 1.0 / resolution.x; + vec4 sum = vec4(0.0); + sum += texture2D(texture, vec2(tex_coords.x - 4.0*dt, tex_coords.y)) + * 0.05; + sum += texture2D(texture, vec2(tex_coords.x - 3.0*dt, tex_coords.y)) + * 0.09; + sum += texture2D(texture, vec2(tex_coords.x - 2.0*dt, tex_coords.y)) + * 0.12; + sum += texture2D(texture, vec2(tex_coords.x - dt, tex_coords.y)) + * 0.15; + sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y)) + * 0.16; + sum += texture2D(texture, vec2(tex_coords.x + dt, tex_coords.y)) + * 0.15; + sum += texture2D(texture, vec2(tex_coords.x + 2.0*dt, tex_coords.y)) + * 0.12; + sum += texture2D(texture, vec2(tex_coords.x + 3.0*dt, tex_coords.y)) + * 0.09; + sum += texture2D(texture, vec2(tex_coords.x + 4.0*dt, tex_coords.y)) + * 0.05; + return sum; +}} +''' + +effect_blur_v = ''' +vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) +{{ + float dt = ({} / 4.0) + * 1.0 / resolution.x; + vec4 sum = vec4(0.0); + sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y - 4.0*dt)) + * 0.05; + sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y - 3.0*dt)) + * 0.09; + sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y - 2.0*dt)) + * 0.12; + sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y - dt)) + * 0.15; + sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y)) + * 0.16; + sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y + dt)) + * 0.15; + sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y + 2.0*dt)) + * 0.12; + sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y + 3.0*dt)) + * 0.09; + sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y + 4.0*dt)) + * 0.05; + return sum; +}} +''' + +effect_postprocessing = ''' +vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) +{ + vec2 q = tex_coords * vec2(1, -1); + vec2 uv = 0.5 + (q-0.5);//*(0.9);// + 0.1*sin(0.2*time)); + + vec3 oricol = texture2D(texture,vec2(q.x,1.0-q.y)).xyz; + vec3 col; + + col.r = texture2D(texture,vec2(uv.x+0.003,-uv.y)).x; + col.g = texture2D(texture,vec2(uv.x+0.000,-uv.y)).y; + col.b = texture2D(texture,vec2(uv.x-0.003,-uv.y)).z; + + col = clamp(col*0.5+0.5*col*col*1.2,0.0,1.0); + + //col *= 0.5 + 0.5*16.0*uv.x*uv.y*(1.0-uv.x)*(1.0-uv.y); + + col *= vec3(0.8,1.0,0.7); + + col *= 0.9+0.1*sin(10.0*time+uv.y*1000.0); + + col *= 0.97+0.03*sin(110.0*time); + + float comp = smoothstep( 0.2, 0.7, sin(time) ); + //col = mix( col, oricol, clamp(-2.0+2.0*q.x+3.0*comp,0.0,1.0) ); + + return vec4(col,1.0); +} +''' + +effect_pixelate = ''' +vec4 effect(vec4 vcolor, sampler2D texture, vec2 texcoord, vec2 pixel_coords) +{{ + vec2 pixelSize = {} / resolution; + + vec2 xy = floor(texcoord/pixelSize)*pixelSize + pixelSize/2.0; + + return texture2D(texture, xy); +}} +''' + +effect_fxaa = ''' +vec4 effect( vec4 color, sampler2D buf0, vec2 texCoords, vec2 coords) +{ + + vec2 frameBufSize = resolution; + + float FXAA_SPAN_MAX = 8.0; + float FXAA_REDUCE_MUL = 1.0/8.0; + float FXAA_REDUCE_MIN = 1.0/128.0; + + vec3 rgbNW=texture2D(buf0,texCoords+(vec2(-1.0,-1.0)/frameBufSize)).xyz; + vec3 rgbNE=texture2D(buf0,texCoords+(vec2(1.0,-1.0)/frameBufSize)).xyz; + vec3 rgbSW=texture2D(buf0,texCoords+(vec2(-1.0,1.0)/frameBufSize)).xyz; + vec3 rgbSE=texture2D(buf0,texCoords+(vec2(1.0,1.0)/frameBufSize)).xyz; + vec3 rgbM=texture2D(buf0,texCoords).xyz; + + vec3 luma=vec3(0.299, 0.587, 0.114); + float lumaNW = dot(rgbNW, luma); + float lumaNE = dot(rgbNE, luma); + float lumaSW = dot(rgbSW, luma); + float lumaSE = dot(rgbSE, luma); + float lumaM = dot(rgbM, luma); + + float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE))); + float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE))); + + vec2 dir; + dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE)); + dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE)); + + float dirReduce = max( + (lumaNW + lumaNE + lumaSW + lumaSE) * (0.25 * FXAA_REDUCE_MUL), + FXAA_REDUCE_MIN); + + float rcpDirMin = 1.0/(min(abs(dir.x), abs(dir.y)) + dirReduce); + + dir = min(vec2( FXAA_SPAN_MAX, FXAA_SPAN_MAX), + max(vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX), + dir * rcpDirMin)) / frameBufSize; + + vec3 rgbA = (1.0/2.0) * ( + texture2D(buf0, texCoords.xy + dir * (1.0/3.0 - 0.5)).xyz + + texture2D(buf0, texCoords.xy + dir * (2.0/3.0 - 0.5)).xyz); + vec3 rgbB = rgbA * (1.0/2.0) + (1.0/4.0) * ( + texture2D(buf0, texCoords.xy + dir * (0.0/3.0 - 0.5)).xyz + + texture2D(buf0, texCoords.xy + dir * (3.0/3.0 - 0.5)).xyz); + float lumaB = dot(rgbB, luma); + + vec4 return_color; + if((lumaB < lumaMin) || (lumaB > lumaMax)){ + return_color = vec4(rgbA, color.w); + }else{ + return_color = vec4(rgbB, color.w); + } + + return return_color; +} +''' + + +class EffectBase(EventDispatcher): + '''The base class for GLSL effects. It simply returns its input. + + See module documentation for more details. + + ''' + + glsl = StringProperty(effect_trivial) + '''The glsl string defining your effect function, see module + documentation for more details. + + :attr:`glsl` is a :class:`~kivy.properties.StringProperty` and + defaults to + a trivial effect that returns its input. + ''' + + source = StringProperty('') + '''The (optional) filename from which to load the :attr:`glsl` + string. + + :attr:`source` is a :class:`~kivy.properties.StringProperty` and + defaults to ''. + ''' + + fbo = ObjectProperty(None, allownone=True) + '''The fbo currently using this effect. The :class:`EffectBase` + automatically handles this. + + :attr:`fbo` is a :class:`~kivy.properties.ObjectProperty` and + defaults to None. + ''' + + def __init__(self, *args, **kwargs): + super(EffectBase, self).__init__(*args, **kwargs) + self.bind(fbo=self.set_fbo_shader) + self.bind(glsl=self.set_fbo_shader) + self.bind(source=self._load_from_source) + + def set_fbo_shader(self, *args): + '''Sets the :class:`~kivy.graphics.Fbo`'s shader by splicing + the :attr:`glsl` string into a full fragment shader. + + The full shader is made up of :code:`shader_header + + shader_uniforms + self.glsl + shader_footer_effect`. + ''' + if self.fbo is None: + return + self.fbo.set_fs(shader_header + shader_uniforms + self.glsl + + shader_footer_effect) + + def _load_from_source(self, *args): + '''(internal) Loads the glsl string from a source file.''' + source = self.source + if not source: + return + filename = resource_find(source) + if filename is None: + return Logger.error('Error reading file {filename}'. + format(filename=source)) + with open(filename) as fileh: + self.glsl = fileh.read() + + +class AdvancedEffectBase(EffectBase): + '''An :class:`EffectBase` with additional behavior to easily + set and update uniform variables in your shader. + + This class is provided for convenience if implementing your own + effects, it is not used by any of those provided with Kivy. + + In addition to your base glsl string that must be provided as + normal, the :class:`AdvancedEffectBase` has an extra property + :attr:`uniforms`, a dictionary of name-value pairs. Whenever + a value is changed, the new values for the uniform variable with + the given name are uploaded to the shader. + + You must still manually declare your uniform variables at the top + of your glsl string. + ''' + + uniforms = DictProperty({}) + '''A dictionary of uniform variable names and their values. These + are automatically uploaded to the :attr:`fbo` shader if appropriate. + + uniforms is a :class:`~kivy.properties.DictProperty` and + defaults to {}. + ''' + + def __init__(self, *args, **kwargs): + super(AdvancedEffectBase, self).__init__(*args, **kwargs) + self.bind(uniforms=self._update_uniforms) + + def _update_uniforms(self, *args): + if self.fbo is None: + return + for key, value in self.uniforms.items(): + self.fbo[key] = value + + def set_fbo_shader(self, *args): + super(AdvancedEffectBase, self).set_fbo_shader(*args) + self._update_uniforms() + + +class MonochromeEffect(EffectBase): + '''Returns its input colours in monochrome.''' + def __init__(self, *args, **kwargs): + super(MonochromeEffect, self).__init__(*args, **kwargs) + self.glsl = effect_monochrome + + +class InvertEffect(EffectBase): + '''Inverts the colours in the input.''' + def __init__(self, *args, **kwargs): + super(InvertEffect, self).__init__(*args, **kwargs) + self.glsl = effect_invert + + +class ScanlinesEffect(EffectBase): + '''Adds scanlines to the input.''' + def __init__(self, *args, **kwargs): + super(ScanlinesEffect, self).__init__(*args, **kwargs) + self.glsl = effect_postprocessing + + +class ChannelMixEffect(EffectBase): + '''Mixes the color channels of the input according to the order + property. Channels may be arbitrarily rearranged or repeated.''' + + order = ListProperty([1, 2, 0]) + '''The new sorted order of the rgb channels. + + order is a :class:`~kivy.properties.ListProperty` and defaults to + [1, 2, 0], corresponding to (g, b, r). + ''' + + def __init__(self, *args, **kwargs): + super(ChannelMixEffect, self).__init__(*args, **kwargs) + self.do_glsl() + + def on_order(self, *args): + self.do_glsl() + + def do_glsl(self): + letters = [{0: 'x', 1: 'y', 2: 'z'}[i] for i in self.order] + self.glsl = effect_mix.format(*letters) + + +class PixelateEffect(EffectBase): + '''Pixelates the input according to its + :attr:`~PixelateEffect.pixel_size`''' + + pixel_size = NumericProperty(10) + ''' + Sets the size of a new 'pixel' in the effect, in terms of number of + 'real' pixels. + + pixel_size is a :class:`~kivy.properties.NumericProperty` and + defaults to 10. + ''' + + def __init__(self, *args, **kwargs): + super(PixelateEffect, self).__init__(*args, **kwargs) + self.do_glsl() + + def on_pixel_size(self, *args): + self.do_glsl() + + def do_glsl(self): + self.glsl = effect_pixelate.format(float(self.pixel_size)) + + +class HorizontalBlurEffect(EffectBase): + '''Blurs the input horizontally, with the width given by + :attr:`~HorizontalBlurEffect.size`.''' + + size = NumericProperty(4.0) + '''The blur width in pixels. + + size is a :class:`~kivy.properties.NumericProperty` and defaults to + 4.0. + ''' + + def __init__(self, *args, **kwargs): + super(HorizontalBlurEffect, self).__init__(*args, **kwargs) + self.do_glsl() + + def on_size(self, *args): + self.do_glsl() + + def do_glsl(self): + self.glsl = effect_blur_h.format(float(self.size)) + + +class VerticalBlurEffect(EffectBase): + '''Blurs the input vertically, with the width given by + :attr:`~VerticalBlurEffect.size`.''' + + size = NumericProperty(4.0) + '''The blur width in pixels. + + size is a :class:`~kivy.properties.NumericProperty` and defaults to + 4.0. + ''' + + def __init__(self, *args, **kwargs): + super(VerticalBlurEffect, self).__init__(*args, **kwargs) + self.do_glsl() + + def on_size(self, *args): + self.do_glsl() + + def do_glsl(self): + self.glsl = effect_blur_v.format(float(self.size)) + + +class FXAAEffect(EffectBase): + '''Applies very simple antialiasing via fxaa.''' + def __init__(self, *args, **kwargs): + super(FXAAEffect, self).__init__(*args, **kwargs) + self.glsl = effect_fxaa + + +class EffectFbo(Fbo): + '''An :class:`~kivy.graphics.Fbo` with extra facility to + attempt setting a new shader, see :meth:`set_fs`. + ''' + def __init__(self, *args, **kwargs): + super(EffectFbo, self).__init__(*args, **kwargs) + self.texture_rectangle = None + + def set_fs(self, value): + '''Attempt to set the fragment shader to the given value. + If setting the shader fails, resets the old one and raises an + exception. + ''' + shader = self.shader + old_value = shader.fs + shader.fs = value + if not shader.success: + shader.fs = old_value + raise Exception('Setting new shader failed.') + + +class EffectWidget(BoxLayout): + ''' + Widget with the ability to apply a series of graphical effects to + its children. See module documentation for full information on + setting effects and creating your own. + ''' + + texture = ObjectProperty(None) + '''The output texture of our final :class:`~kivy.graphics.Fbo` after + all effects have been applied. + + texture is an :class:`~kivy.properties.ObjectProperty` and defaults + to None. + ''' + + effects = ListProperty([]) + '''List of all the effects to be applied. These should all be + instances of :class:`EffectBase`. + + effects is a :class:`ListProperty` and defaults to []. + ''' + + fbo_list = ListProperty([]) + '''(internal) list of all the fbos that are being used to apply + the effects. + + fbo_list is a :class:`ListProperty` and defaults to []. + ''' + + _bound_effects = ListProperty([]) + '''(internal) list of effect classes that have been given an fbo to + manage. This is necessary so that the fbo can be removed it the + effect is no longer in use. + + _bound_effects is a :class:`ListProperty` and defaults to []. + ''' + + def __init__(self, **kwargs): + # Make sure opengl context exists + EventLoop.ensure_window() + + self.canvas = RenderContext(use_parent_projection=True, + use_parent_modelview=True) + + with self.canvas: + self.fbo = Fbo(size=self.size) + + with self.fbo.before: + PushMatrix() + self.fbo_translation = Translate(-self.x, -self.y, 0) + with self.fbo: + Color(0, 0, 0) + self.fbo_rectangle = Rectangle(size=self.size) + with self.fbo.after: + PopMatrix() + + super(EffectWidget, self).__init__(**kwargs) + + Clock.schedule_interval(self._update_glsl, 0) + + self.bind(pos=self._update_translation, + size=self.refresh_fbo_setup, + effects=self.refresh_fbo_setup) + + self.refresh_fbo_setup() + + def _update_translation(self, *args): + '''(internal) Makes sure everything is translated correctly to + appear in the fbo.''' + self.fbo_translation.x = -self.x + self.fbo_translation.y = -self.y + + def _update_glsl(self, *largs): + '''(internal) Passes new time and resolution uniform + variables to the shader. + ''' + time = Clock.get_boottime() + resolution = [float(size) for size in self.size] + self.canvas['time'] = time + self.canvas['resolution'] = resolution + for fbo in self.fbo_list: + fbo['time'] = time + fbo['resolution'] = resolution + + def refresh_fbo_setup(self, *args): + '''(internal) Creates and assigns one :class:`~kivy.graphics.Fbo` + per effect, and makes sure all sizes etc. are correct and + consistent. + ''' + # Add/remove fbos until there is one per effect + while len(self.fbo_list) < len(self.effects): + with self.canvas: + new_fbo = EffectFbo(size=self.size) + with new_fbo: + Color(1, 1, 1, 1) + new_fbo.texture_rectangle = Rectangle( + size=self.size) + + new_fbo.texture_rectangle.size = self.size + self.fbo_list.append(new_fbo) + while len(self.fbo_list) > len(self.effects): + old_fbo = self.fbo_list.pop() + self.canvas.remove(old_fbo) + + # Remove fbos from unused effects + for effect in self._bound_effects: + if effect not in self.effects: + effect.fbo = None + self._bound_effects = self.effects + + # Do resizing etc. + self.fbo.size = self.size + self.fbo_rectangle.size = self.size + for i in range(len(self.fbo_list)): + self.fbo_list[i].size = self.size + self.fbo_list[i].texture_rectangle.size = self.size + + # If there are no effects, just draw our main fbo + if len(self.fbo_list) == 0: + self.texture = self.fbo.texture + return + + for i in range(1, len(self.fbo_list)): + fbo = self.fbo_list[i] + fbo.texture_rectangle.texture = self.fbo_list[i - 1].texture + + # Build effect shaders + for effect, fbo in zip(self.effects, self.fbo_list): + effect.fbo = fbo + + self.fbo_list[0].texture_rectangle.texture = self.fbo.texture + self.texture = self.fbo_list[-1].texture + + def add_widget(self, widget): + # Add the widget to our Fbo instead of the normal canvas + c = self.canvas + self.canvas = self.fbo + super(EffectWidget, self).add_widget(widget) + self.canvas = c + + def remove_widget(self, widget): + # Remove the widget from our Fbo instead of the normal canvas + c = self.canvas + self.canvas = self.fbo + super(EffectWidget, self).remove_widget(widget) + self.canvas = c + + def clear_widgets(self, children=None): + # Clear widgets from our Fbo instead of the normal canvas + c = self.canvas + self.canvas = self.fbo + super(EffectWidget, self).clear_widgets(children) + self.canvas = c