Created
February 19, 2024 11:44
-
-
Save ArvidSilverlock/4eda3c5c49cdfc619e5e57222230f018 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| local SIZE = Vector2.new(320, 320) | |
| local SEED = math.random() * 256 | |
| local MASK_SCALE = 2^-4 | |
| local COLOUR_STEP = 1 | |
| local LAYER_STEP = 1 | |
| local FRAMES_PER_FRAME = 4 | |
| local FRAME_STEP = 1/60 | |
| assert(SIZE.X <= 1024 and SIZE.X > 0, "size `x` must be within the range [0, 1024]") | |
| assert(SIZE.Y <= 1024 and SIZE.Y > 0, "size `y` must be within the range [0, 1024]") | |
| -- Maps a [0:0, 1:1] line to a [0:1, 0.5:0, 1:1] line | |
| local function zigzagify(n: number): number | |
| return math.abs(2 * n - 1) | |
| end | |
| -- Perlin noise function | |
| local function noise(x: number, y: number, offset: number) | |
| return math.clamp(math.noise( | |
| x * MASK_SCALE, | |
| y * MASK_SCALE, | |
| SEED + offset * 0.125 | |
| ) + 0.5, 0, 1) | |
| end | |
| -- Various functions that take a [0, 1] range and spit out a different [0, 1] range | |
| local LAYERS = { | |
| function(n: number) | |
| return n | |
| end, | |
| function(n: number) | |
| if n < 0.2 then | |
| return n / 0.2 | |
| elseif n < 0.8 then | |
| return 1 / (1 + math.exp(-8 * (n - 0.5))) | |
| else | |
| return 1 - (n - 0.8) / 0.2 | |
| end | |
| end, | |
| function(n: number) | |
| return math.cos(2 * math.pi * n) / 2 + 0.5 | |
| end, | |
| function(n: number) | |
| return 1 - n | |
| end, | |
| function(n: number) | |
| if n < 0.2 then | |
| return n / 0.2 | |
| elseif n < 0.5 then | |
| return (n - 0.2) / 0.3 + 1 | |
| elseif n < 0.8 then | |
| return 1 - (n - 0.5) / 0.3 | |
| else | |
| return 0 | |
| end | |
| end, | |
| function(n: number) | |
| return math.sin(2 * math.pi * n) / 2 + 0.5 | |
| end, | |
| function(n: number) | |
| return ( 2 * n - 1 )^3 / 2 + 0.5 | |
| end, | |
| function(n: number) | |
| if n < 0.2 then | |
| return n / 0.2 | |
| elseif n < 0.4 then | |
| return 1 - (n - 0.2) / 0.2 | |
| elseif n < 0.6 then | |
| return n / 0.2 - 1 | |
| elseif n < 0.8 then | |
| return 1 - (n - 0.6) / 0.2 | |
| else | |
| return n / 0.2 - 2 | |
| end | |
| end, | |
| function(n: number) | |
| return 1 / (1 + math.exp(-8 * (n - 0.5))) | |
| end, | |
| function(n: number) | |
| return 1 / (1 + math.exp(-12 * (n - 0.5))) | |
| end, | |
| function(n: number) | |
| if n < 0.2 then | |
| return n / 0.2 | |
| elseif n < 0.5 then | |
| return 1 - (n - 0.2) / 0.3 | |
| elseif n < 0.8 then | |
| return (n - 0.5) / 0.3 | |
| else | |
| return 1 - (n - 0.8) / 0.2 | |
| end | |
| end, | |
| function(n: number) | |
| return 3 * n^2 - 2 * n^3 | |
| end, | |
| function(n: number) | |
| if n < 0.2 then | |
| return n / 0.2 | |
| elseif n < 0.5 then | |
| return (n - 0.2) / 0.3 + 1 | |
| else | |
| return 1 - (n - 0.5) / 0.5 | |
| end | |
| end, | |
| function(n: number) | |
| return (1 - n)^2 * 0 + 2 * (1 - n) * n * 0.5 + n^2 * 1 | |
| end, | |
| } | |
| -- Initialise the pixel table and mask buffer | |
| local pixels = table.create(SIZE.X * SIZE.Y * 4, 1) | |
| local mask = buffer.create(SIZE.X * SIZE.Y * #LAYERS) | |
| -- How many rows are calculated per frame before we yield | |
| local frameStep = SIZE.X // FRAMES_PER_FRAME | |
| local layerCount = #LAYERS | |
| local area = SIZE.X * SIZE.Y | |
| -- Prepare the noise values in the mask buffer | |
| for x = 0, SIZE.X - 1 do | |
| for y = 0, SIZE.Y - 1 do | |
| for z = 0, layerCount - 1 do | |
| local index = x + ( y * SIZE.X ) + ( z * area ) | |
| local noise = noise(x, y, z * 256) | |
| local transform = LAYERS[z + 1] | |
| -- Calculate the value and map it from [0, 1] to [0, 255] to store as an 8 bit integer | |
| local value = math.floor(transform(noise) * 255) | |
| buffer.writeu8(mask, index, value) | |
| end | |
| end | |
| end | |
| local editableImage = Instance.new("EditableImage") | |
| editableImage.Size = SIZE | |
| editableImage.Parent = script.Parent | |
| local t = 0 | |
| while true do | |
| -- Calculate the primary and secondary colours based on `t` | |
| local hue = t % COLOUR_STEP / COLOUR_STEP | |
| local primary = Color3.fromHSV(hue, 0.5, 0.8) | |
| local secondary = Color3.fromHSV(hue, 0.125, 1) | |
| -- Calculate which layer we're on, and how far through it we are | |
| local layerIndex = t // LAYER_STEP | |
| local layerLerp = t % LAYER_STEP / LAYER_STEP | |
| -- Calculate the buffer index offsets to get the the necessary layers | |
| local layerOffsetA = ( layerIndex % layerCount ) * area | |
| local layerOffsetB = ( ( layerIndex + 1 ) % layerCount ) * area | |
| for x = 0, SIZE.X - 1 do | |
| for y = 0, SIZE.Y - 1 do | |
| local baseIndex = x + y * SIZE.X | |
| -- Calculate the mask value based on the current and next layers | |
| local a = buffer.readu8(mask, baseIndex + layerOffsetA) | |
| local b = buffer.readu8(mask, baseIndex + layerOffsetB) | |
| local maskValue = ( a + (b - a) * layerLerp ) / 255 | |
| -- Offset the mask value by `t`, re-map it to [0:1, 0.5:0, 1:1] and calculate the colour | |
| local shiftedMask = zigzagify(( maskValue + t ) % 1) | |
| local colour = primary:Lerp(secondary, shiftedMask) | |
| -- Add the RGB values to the output, ignore alpha | |
| local index = baseIndex * 4 | |
| pixels[index + 1] = colour.R | |
| pixels[index + 2] = colour.G | |
| pixels[index + 3] = colour.B | |
| end | |
| -- Prevent FPS drops | |
| if x % frameStep == 0 then | |
| task.wait() | |
| end | |
| end | |
| -- Manually increment `t` so we don't get jumps from high CPU time | |
| t += FRAME_STEP | |
| -- Write the pixels to the EditableImage | |
| editableImage:WritePixels(Vector2.zero, SIZE, pixels) | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment