├── README.md ├── glitch_ren.py ├── sample_chroma.png ├── sample_nochroma.png └── sample_squares.png /README.md: -------------------------------------------------------------------------------- 1 | # renpy-ChromaGlitch 2 | A way to display images (or other displayables, even animated) with a DDLC-like glitch effect offsetting slices of the image laterally and optionally adding chromatic aberration effects on the glitched slices. 3 | 4 | These effects were featured in [this YouTube video](https://www.youtube.com/watch?v=H2eg010UozE) by Visual Novel Design. Thanks to him ! 5 | 6 | ![](sample_nochroma.png) 7 | ![](sample_chroma.png) 8 | 9 | The `glitch.rpy` file contains the code itself. 10 | 11 | ## Howto glitch 12 | `glitch` is a transform (because it takes a displayable and returns a displayable). 13 | Its 6 parameters are : 14 | - `child` : the displayable (~= image) on which the effect is applied. This is the only required argument, all others are keyword-only. 15 | - `randomkey` : the key given to the random object used to generate the slices heights and offsets. This must match [the random module's specifications](https://docs.python.org/3/library/random.html#random.seed). A given image glitched with a given non-None key will always, always, be glitched the same way. A glitched image with a None key will look differently every time Ren'Py renders it - and re-renders can be triggered by a whole lot of things. Use this to make the glitching reliable (in an animation for example). Defaults to generating a random key at the first call and then sticking with it. 16 | - `chroma` : boolean indicating whether or not to apply chromatic aberration effects to the glitched tranches. Defaults to True. 17 | - `minbandheight` : minimum height of a slice, in pixels. Defaults to 1. 18 | - `offset` : a positive integer. The actual offset given to a glitched slice will be comprised between -offset and +offset. Defaults to 30. 19 | - `crop` : boolean indicating whether or not to crop the resulting image to make it fit the size of the original image. Defaults to False. 20 | 21 | Then, you can directly show it using `show expression glitch("eileen happy") as beautifulcharacter` (I strongly recommand using the `as` clause). 22 | You can also apply it as a transform, with `show eileen at glitch` or `show eileen at functools.partial(glitch, chroma=False)` or even `show layer master at glitch`. 23 | 24 | It is also possible to define it directly as an image, simply using `image eileen glitched = glitch("eileen", offset=20)` 25 | (it was not possible in previous versions of ChromaGlitch, but now it is). 26 | 27 | Example : 28 | ```rpy 29 | image eileen glitched: 30 | glitch("eileen happy") # reliable slicing 31 | pause 1.0 32 | glitch("eileen happy", offset=60, randomkey=None) # bigger and always-random slicing 33 | pause 0.1 34 | repeat 35 | ``` 36 | 37 | However, this last example using ATL will work poorly if and when "eileen happy" is itself an animation. This is because at the two lines starting with `glitch`, the child of the ATL animation is set again, and its animation timebase begins anew. This is (sadly for us) the expected behavior of ATL. 38 | Luckily for us, there is... 39 | 40 | ## animated_glitch 41 | `animated_glitch` is a transform working much like `glitch`, except that it overhauls the `randomkey` mechanism. That parameter is changed and updated at parameterizable intervals of time, alternating (though that can be disabled) between vanilla (non-glitched) and glitched versions of the transformed image. Given that it's the same image, same displayable all the time, it doesn't reset anything like the ATL solution above did, which makes it perfectly suitable for use over animations. 42 | It takes the same parameters `glitch` does, plus the following ones : 43 | - `timeout_base` : the time in seconds between two randomkey changes. Can either be a single float (or integer) value, or a tuple if two values between which the actual periods of time will be randomly chosen following a uniform distribution (which is `randomkey`-wise deterministic). Defaults to .1 second. 44 | - `timeout_vanilla` : the same, for the periods of time during which the child image is shown without any glitching effect. If passed False, deactivates it - the image will always be glitched. If a `timeout_base` is passed, it default to that value, otherwise it defaults to `(1, 3)` (meaning a random duration each time, chosen randomly between 1 and 3 seconds). 45 | 46 | The glitchless ("vanilla") phases, unless deactivated of course, are generated this way : 47 | - the initial period is vanilla 48 | - if one period was vanilla, the next is glitched 49 | - if one period was glitched, the next has a 30% chance of being vanilla, otherwise it's glitched 50 | 51 | ## Squares glitch 52 | 53 | ![](sample_squares.png) 54 | 55 | This is a second type of glitch, which cuts the image into squares and does things to them. 56 | It takes `child` and `randomkey`, which are the same as for the preceding glitch, and three aditional parameters. 57 | - `squareside` : the size, in pixels, of the side of the squares the image will be cut to. This will be adjusted so that all the squares have the same size. Defaults to 20. 58 | - `chroma` : the probability (0. - 1.) for each square to get a chromatic glitch. Defaults to .25. 59 | - `permutes` : the percentage (0. - 1.) of squares which will change places with other squares. Defaults to a random percentage between 10% and 40%. 60 | 61 | ## Terms of use 62 | Use it freely in any project. If you liked it, you can drop my name in the credits with a link to this repo 🥰 63 | -------------------------------------------------------------------------------- /glitch_ren.py: -------------------------------------------------------------------------------- 1 | """renpy 2 | init python: 3 | """ 4 | class glitch(renpy.Displayable): 5 | """ 6 | `randomkey` 7 | Follows the rules of the random modume's seed function. 8 | If not set, a random seed is generated when the transform is applied, 9 | and stays the same afterwards. 10 | If you want the effect to be random for each render operation, set to None. 11 | 12 | `chroma` 13 | Boolean, whether to apply the chromatic aberration effect. 14 | 15 | `minbandheight` 16 | Minimum height of each slice. 17 | 18 | `offset` 19 | The offset of each slice will be between -offset and offset pixels. 20 | 21 | `nslices` 22 | Number of slicings to do (the number of slices will be nslices + 1). 23 | Setting this to 0 is not supported. 24 | None (the default) makes it random. 25 | """ 26 | 27 | NotSet = object() 28 | 29 | def __init__(self, child, *, randomkey=NotSet, chroma=True, minbandheight=1, offset=30, nslices=None, **properties): 30 | super().__init__(**properties) 31 | self.child = renpy.displayable(child) 32 | if randomkey is self.NotSet: 33 | randomkey = renpy.random.random() 34 | self.randomkey = randomkey 35 | self.chroma = chroma 36 | self.minbandheight = minbandheight 37 | self.offset = offset 38 | self.nslices = nslices 39 | 40 | def render(self, width, height, st, at): 41 | child = self.child 42 | child_render = renpy.render(child, width, height, st, at) 43 | cwidth, cheight = child_render.get_size() 44 | if not (cwidth and cheight): 45 | return child_render 46 | render = renpy.Render(cwidth, cheight) 47 | randomobj = renpy.random.Random(self.randomkey) 48 | chroma = self.chroma and renpy.display.render.models 49 | offset = self.offset 50 | minbandheight = self.minbandheight 51 | nslices = self.nslices 52 | if nslices is None: 53 | nslices = min(int(cheight/minbandheight), randomobj.randrange(10, 21)) 54 | 55 | theights = sorted(randomobj.randrange(int(cheight)+1) for k in range(nslices)) # y coordinates demarcating all the strips 56 | offt = 0 # next strip's lateral offset 57 | fheight = 0 # sum of the size of all the strips added this far 58 | while fheight= self.timeout: 134 | randomkey = self.randomkey 135 | randomobj = renpy.random.Random(randomkey) 136 | self.randomkey = randomobj.random() 137 | 138 | # determine whether to show vanilla or not 139 | if vanilla or (self.timeout_vanilla is False): 140 | # if we were showing it or if showing it is disabled 141 | vanilla = False 142 | else: 143 | vanilla = (randomobj.random() < .3) 144 | 145 | self.set_timeout(vanilla, st) 146 | 147 | renpy.redraw(self, st-self.timeout) 148 | 149 | if vanilla: 150 | return renpy.render(self.child, width, height, st, at) 151 | else: 152 | return super().render(width, height, st, at) 153 | 154 | class squares_glitch(renpy.Displayable): 155 | """ 156 | `squareside` 157 | The size, in pixels, of the side of the squares the child image will be cut to. This will 158 | be adjusted so that all the "squares" (rectangles, really) have the same width and the 159 | same height, and that none is cut at the borders of the image. Defaults to 20 pixels. 160 | 161 | `chroma` 162 | The probability for each square to get a chromatic effect. Defaults to .25. 163 | 164 | `permutes` 165 | The percentage of squares which will be moved to another square's place. If not passed, 166 | defaults to a random value between .1 and .4. 167 | """ 168 | 169 | NotSet = object() 170 | 171 | def __init__(self, child, *args, randomkey=NotSet, **kwargs): 172 | super().__init__() 173 | self.child = renpy.displayable(child) 174 | self.args = args 175 | if randomkey is self.NotSet: 176 | randomkey = renpy.random.random() 177 | self.randomkey = randomkey 178 | self.kwargs = kwargs 179 | 180 | def render(self, width, height, st, at): 181 | cwidth, cheight = renpy.render(self.child, width, height, st, at).get_size() 182 | return renpy.render(self.glitch(self.child, 183 | cwidth, cheight, renpy.random.Random(self.randomkey), 184 | *self.args, **self.kwargs), 185 | width, height, 186 | st, at) 187 | 188 | @staticmethod 189 | def glitch(child, cwidth, cheight, randomobj, squareside=20, chroma=.25, permutes=None): 190 | if not renpy.display.render.models: 191 | chroma = False 192 | if not (cwidth and cheight): 193 | return child 194 | 195 | ncols = round(cwidth/squareside) 196 | nrows = round(cheight/squareside) 197 | square_width = absolute(cwidth/ncols) 198 | square_height = absolute(cheight/nrows) 199 | 200 | lizt = [] 201 | for y in range(nrows): 202 | for x in range(ncols): 203 | lizt.append(Transform(child, 204 | crop=(absolute(x*square_width), absolute(y*square_height), square_width, square_height), 205 | subpixel=True, 206 | )) 207 | 208 | if permutes is None: 209 | permutes = randomobj.randrange(10, 40)/100 # between 10% and 40% 210 | permutes = round(permutes*ncols*nrows) 211 | permute_a = randomobj.sample(range(ncols*nrows), permutes) 212 | permute_b = randomobj.sample(range(ncols*nrows), permutes) 213 | 214 | for a, b in zip(permute_a, permute_b): 215 | lizt[a], lizt[b] = lizt[b], lizt[a] 216 | 217 | for k, el in enumerate(lizt): 218 | if randomobj.random() < chroma: 219 | lizt[k] = Transform(el, 220 | gl_color_mask=(randomobj.random()<.33, randomobj.random()<.33, randomobj.random()<.33, True), 221 | # matrixcolor=HueMatrix(randomobj.random()*360), 222 | ) 223 | 224 | return Grid(ncols, nrows, *lizt) 225 | 226 | def __eq__(self, other): 227 | return (type(self) == type(other)) and (self.args == other.args) and (self.kwargs == other.kwargs) 228 | -------------------------------------------------------------------------------- /sample_chroma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gouvernathor/renpy-ChromaGlitch/db2fc053b92ba309ed3bd0db0f33e96b587b5503/sample_chroma.png -------------------------------------------------------------------------------- /sample_nochroma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gouvernathor/renpy-ChromaGlitch/db2fc053b92ba309ed3bd0db0f33e96b587b5503/sample_nochroma.png -------------------------------------------------------------------------------- /sample_squares.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gouvernathor/renpy-ChromaGlitch/db2fc053b92ba309ed3bd0db0f33e96b587b5503/sample_squares.png --------------------------------------------------------------------------------