wiggle = love.graphics.newShader[[ extern number seed; extern number amount; int grain = 10; //higher = less, lower = more float rand(vec2 co){ return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); } vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) { vec4 pixel_me = Texel(texture, texture_coords ); return Texel(texture, texture_coords + (rand(texture_coords * seed) - .5) * amount); /* int resolution = int(12 * 1 / zoom); vec2 final_coords = floor((resolution * texture_coords) + 0.5) / resolution; vec4 averaged_pixel = (Texel(texture, final_coords - (1 / resolution)) + Texel(texture, final_coords)) / 2 + (rand(final_coords * seed) / grain); averaged_pixel.a = 1; pixel_me = Texel(texture, final_coords); return averaged_pixel; */ } ]] glow = love.graphics.newShader[[ vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) { vec4 pixel_me = Texel(texture, texture_coords ); if (pixel_me.rgb != vec3(0,0,0)) return pixel_me; int samples = 5; float intensity = 60; float radius = .1; for (int i = 1; i < samples; i++ ) { vec4 pixel_left = Texel(texture, texture_coords + vec2(-radius * i, 0)); vec4 pixel_right = Texel(texture, texture_coords + vec2(radius * i, 0)); vec4 pixel_top = Texel(texture, texture_coords + vec2(0, radius * i)); vec4 pixel_bottom = Texel(texture, texture_coords + vec2(0, -radius * i)); vec4 pixel_top_left = Texel(texture, texture_coords + vec2(-radius * i, radius * i)); vec4 pixel_top_right = Texel(texture, texture_coords + vec2(radius * i, radius * i)); vec4 pixel_bottom_left = Texel(texture, texture_coords + vec2(-radius * i, -radius * i)); vec4 pixel_bottom_right = Texel(texture, texture_coords + vec2(radius * i, -radius * i)); pixel_me += (pixel_left + pixel_right + pixel_top + pixel_bottom + pixel_bottom_right + pixel_bottom_left + pixel_top_right + pixel_top_left) / (intensity * i); intensity -= 2.5; } return pixel_me; } ]] -- Sticky boxes reuse the authored box textures. This shader only wobbles their -- sampling; the draw helper adds a tiny squash-and-stretch bounce around center. goopShader = love.graphics.newShader[[ extern number goopTime; extern vec2 sourceSize; float goopHash(float value) { return fract(sin(value * 91.3458) * 47453.5453); } vec4 effect(vec4 color, Image texture, vec2 textureCoords, vec2 screenCoords) { float sourceY = textureCoords.y; float pixelWidth = 1.0 / sourceSize.x; float pixelHeight = 1.0 / sourceSize.y; float column = floor(textureCoords.x * sourceSize.x / 2.0); float seed = goopHash(column); // Two gentle, out-of-phase waves make the surface feel soft and viscous. // Both stay sub-pixel so the authored silhouette remains recognizable. float horizontalWave = sin(goopTime * 1.7 + sourceY * 10.0 + seed * 6.2831); float verticalWave = sin(goopTime * 1.3 + textureCoords.x * 12.0 + seed * 3.1415); float sourceX = clamp(textureCoords.x + horizontalWave * pixelWidth * 0.45, pixelWidth * 0.5, 1.0 - pixelWidth * 0.5); float wobbledY = clamp(sourceY + verticalWave * pixelHeight * 0.28, pixelHeight * 0.5, 1.0 - pixelHeight * 0.5); return Texel(texture, vec2(sourceX, wobbledY)) * color; } ]] -- Wires are drawn as primitives rather than textured sprites, but LÖVE still -- runs this shader over its default white texture. Inactive runs stay visibly -- connected at half brightness; powered runs get a travelling neon shimmer, -- occasional tube-like noise, and a sub-pixel bend rather than a global fade. wireNeonShader = love.graphics.newShader[[ extern number wirePower; extern number wireTime; extern number wirePhase; float wireHash(vec2 point) { return fract(sin(dot(point, vec2(127.1, 311.7))) * 43758.5453123); } vec4 position(mat4 transformProjection, vec4 vertexPosition) { if (wirePower < 0.5) return transformProjection * vertexPosition; // Every segment sharing an endpoint receives the same offset, so corners // stay connected while the live current gives the tube a gentle movement. float wave = sin(vertexPosition.x * 0.085 + vertexPosition.y * 0.13 + wireTime * 4.4 + wirePhase * 9.0); float wobble = wave * 0.32; vertexPosition.xy += vec2(wobble, -wobble * 0.55); return transformProjection * vertexPosition; } vec4 effect(vec4 color, Image texture, vec2 textureCoords, vec2 screenCoords) { if (wirePower < 0.5) return vec4(color.rgb * 0.50, color.a * 0.72); float travel = dot(screenCoords, vec2(0.070, 0.043)) - wireTime * 6.8 + wirePhase * 13.0; float shimmer = sin(travel) * 0.075 + sin(travel * 2.37 + 1.7) * 0.035; vec2 noiseCell = floor(screenCoords * 0.34) + floor(wireTime * 15.0) * vec2(1.0, -1.0); float noise = wireHash(noiseCell + wirePhase); float flicker = max(0.0, noise - 0.82) * 0.32; float brightness = 0.92 + shimmer + flicker; float alpha = 0.84 + shimmer * 0.80 + flicker * 0.55; return vec4(color.rgb * brightness, color.a * alpha); } ]] -- World renders a neighbouring room twice for the porous seam: once through -- its own active channels and once through the current room's channels. This -- shader only controls the alpha of that second, already-authored render, so -- it never approximates the game's literal RGB colour treatment. porousRoomBoundaryShader = love.graphics.newShader[[ extern number porousTime; extern vec4 porousRoomRect; extern vec2 porousDirection; extern number porousDepth; float porousWave(vec2 point) { float broad = sin(point.x * 0.095 + point.y * 0.071 + porousTime * 1.45); float fine = sin(point.x * 0.231 - point.y * 0.187 - porousTime * 2.20); return broad * 0.105 + fine * 0.050; } vec4 effect(vec4 color, Image texture, vec2 textureCoords, vec2 screenCoords) { vec4 alternateView = Texel(texture, textureCoords) * color; vec2 local = screenCoords - porousRoomRect.xy; vec2 size = porousRoomRect.zw; if (local.x < 0.0 || local.y < 0.0 || local.x > size.x || local.y > size.y) return vec4(0.0); float depth; if (porousDirection.x > 0.5) depth = local.x; else if (porousDirection.x < -0.5) depth = size.x - local.x; else if (porousDirection.y > 0.5) depth = local.y; else depth = size.y - local.y; float towardSeam = 1.0 - smoothstep(0.0, porousDepth, depth); // Animated fingers soften the fifty-fifty transition across the visible // overlap while preserving the pixels of the real alternate room render. float mask = smoothstep(0.18, 0.82, towardSeam + porousWave(screenCoords)); alternateView.a *= mask; return alternateView; } ]] function drawGoopySprite(sprite, x, y, scale) local w, h = sprite:getWidth(), sprite:getHeight() local time = love.timer.getTime() local bounce = math.sin(time * 2.4 + w * .17 + h * .11) * .035 local scaleX = scale * (1 + bounce) local scaleY = scale * (1 - bounce) local drawX = x - w * (scaleX - scale) / 2 local drawY = y - h * (scaleY - scale) / 2 goopShader:send("goopTime", time) goopShader:send("sourceSize", {w, h}) local previousShader = love.graphics.getShader() love.graphics.setShader(goopShader) love.graphics.draw(sprite, drawX, drawY, 0, scaleX, scaleY) love.graphics.setShader(previousShader) end -- Accessibility post-process. The puzzle's RGB channels are normally also -- its visual language, so this runs only after the entire scene has been -- composed. It leaves neutral UI/text alone and remaps saturated colours to -- a palette whose red, green, and yellow accents remain distinct for people -- with deuteranopia. accessibilityColorShader = love.graphics.newShader[[ extern number accessibilityTime; vec3 rgbToHsv(vec3 c) { float maximum = max(c.r, max(c.g, c.b)); float minimum = min(c.r, min(c.g, c.b)); float delta = maximum - minimum; float hue = 0.0; if (delta > 0.0001) { if (maximum == c.r) { hue = mod((c.g - c.b) / delta, 6.0); } else if (maximum == c.g) { hue = ((c.b - c.r) / delta) + 2.0; } else { hue = ((c.r - c.g) / delta) + 4.0; } hue = hue / 6.0; if (hue < 0.0) hue += 1.0; } float saturation = maximum <= 0.0 ? 0.0 : delta / maximum; return vec3(hue, saturation, maximum); } vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) { vec4 pixel = Texel(texture, texture_coords) * color; vec3 hsv = rgbToHsv(pixel.rgb); // Preserve greys, white (including additive RGB overlap), and near-black // background detail. Only colourful puzzle elements are recoloured. if (hsv.y < 0.22 || hsv.z < 0.06) return pixel; // Keep every additive pair in its own bucket. In particular, magenta // (red + blue) must not become the same colour as red, and cyan // (green + blue) must not become the same colour as green. vec3 replacement; float pattern = 0.0; // Advance in whole pixels so the texture stays crisp instead of swimming. float ditherStep = floor(accessibilityTime * 1.5); if (hsv.x < 0.055 || hsv.x >= 0.94) { replacement = vec3(0.93, 0.18, 0.50); // red: pink } else if (hsv.x < 0.19) { replacement = vec3(1.00, 0.78, 0.08); // red + green: gold vec2 cell = mod(floor(screen_coords + vec2(ditherStep, 0.0)), vec2(9.0)); pattern = length(cell - vec2(4.0)) < 1.6 ? 1.0 : 0.0; // dithered dots } else if (hsv.x < 0.43) { replacement = vec3(0.00, 0.72, 0.62); // green: turquoise } else if (hsv.x < 0.58) { replacement = vec3(0.12, 0.55, 0.96); // green + blue: sky blue pattern = mod(floor(screen_coords.x + screen_coords.y + ditherStep), 10.0) < 2.0 ? 1.0 : 0.0; // dithered slashes } else if (hsv.x < 0.77) { replacement = vec3(0.38, 0.32, 0.96); // blue: indigo } else { replacement = vec3(0.72, 0.25, 0.92); // red + blue: purple pattern = mod(floor((screen_coords.x + ditherStep) / 4.0) + floor(screen_coords.y / 4.0), 2.0); // dithered checker } // Secondary colours get a gently animated dither cue, so they are // identifiable even when their hue is hard to perceive. Lighting and // fades remain. replacement = mix(replacement, vec3(1.0), pattern * 0.55); return vec4(replacement * hsv.z, pixel.a); } ]]