make this code run in parallel using actors for a client viewport frame particle emitter in roblox: -- Services
local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")

-- Constants
local Constants = {
	TAGS = {
		PARTICLE = "ViewportParticle",
		EMITTER_VIEWPORT = "ParticleEmitterEnabledViewportFrame"
	},
	FLIPBOOK_TILES = {
		[Enum.ParticleFlipbookLayout.None] = 1,
		[Enum.ParticleFlipbookLayout.Grid2x2] = 4,
		[Enum.ParticleFlipbookLayout.Grid4x4] = 16,
		[Enum.ParticleFlipbookLayout.Grid8x8] = 64,
	}
}

-- Types
type ParticleData = {
	init: number,
	life: number,
	age: number,
	emitter: ParticleEmitter,
	cframe: CFrame,
	velocity: Vector3,
	rotation: number,
	rotSpeed: number,
	acceleration: Vector3,
	collidable: boolean,
	renderer: ViewportFrame,
	frames: number,
	frame: number,
	lastFrameUpdate: number,
	framerate: number,
	sizeSequence: NumberSequence,
	size: Vector2,
	color: Color3,
	transparencySequence: NumberSequence,
	transparency: number
}


-- Helper function
local function lerp(a: number, b: number, t: number): number
	return a + (b - a) * t
end

-- Utility Functions
local SequenceEvaluator = {}

function SequenceEvaluator.evalColorSequence(sequence: ColorSequence, time: number): Color3
	if time <= 0 then return sequence.Keypoints[1].Value end
	if time >= 1 then return sequence.Keypoints[#sequence.Keypoints].Value end

	for i = 1, #sequence.Keypoints - 1 do
		local thisKeypoint = sequence.Keypoints[i]
		local nextKeypoint = sequence.Keypoints[i + 1]

		if time >= thisKeypoint.Time and time < nextKeypoint.Time then
			local alpha = (time - thisKeypoint.Time) / (nextKeypoint.Time - thisKeypoint.Time)
			return Color3.new(
				lerp(thisKeypoint.Value.R, nextKeypoint.Value.R, alpha),
				lerp(thisKeypoint.Value.G, nextKeypoint.Value.G, alpha),
				lerp(thisKeypoint.Value.B, nextKeypoint.Value.B, alpha)
			)
		end
	end
	return sequence.Keypoints[#sequence.Keypoints].Value
end

function SequenceEvaluator.evalNumberSequence(sequence: NumberSequence, time: number): number
	if time <= 0 then return sequence.Keypoints[1].Value end
	if time >= 1 then return sequence.Keypoints[#sequence.Keypoints].Value end

	for i = 1, #sequence.Keypoints - 1 do
		local currKeypoint = sequence.Keypoints[i]
		local nextKeypoint = sequence.Keypoints[i + 1]

		if time >= currKeypoint.Time and time < nextKeypoint.Time then
			local alpha = (time - currKeypoint.Time) / (nextKeypoint.Time - currKeypoint.Time)
			return lerp(currKeypoint.Value, nextKeypoint.Value, alpha)
		end
	end
	return sequence.Keypoints[#sequence.Keypoints].Value
end

-- Particle Manager
local ParticleManager = {
	_particles = {},
	_emitters = {},
	_emitterLastSpawns = {},
	_viewportFrames = {},
	_prefabParticle = nil
}

function ParticleManager.init(prefabParticle: Instance)
	ParticleManager._prefabParticle = prefabParticle
	ParticleManager._setupViewportFrames()
	ParticleManager._bindToRenderStep()
end

function ParticleManager._setupViewportFrames()
	local function registerViewportFrame(instance: ViewportFrame)
		if not instance:IsA("ViewportFrame") or ParticleManager._viewportFrames[instance] then
			return
		end

		ParticleManager._viewportFrames[instance] = true

		for _, value in ipairs(instance:GetChildren()) do
			if not (value:IsA("ObjectValue") and value:GetAttribute("Enabled") and value.Value) then
				continue
			end

			local parent = value.Value
			if not (parent:IsA("BasePart") or parent:IsA("Attachment")) then
				continue
			end

			for _, emitter in ipairs(parent:GetChildren()) do
				if emitter:IsA("ParticleEmitter") then
					ParticleManager._emitters[emitter] = instance
				end
			end
		end
	end

	for _, value in ipairs(CollectionService:GetTagged(Constants.TAGS.EMITTER_VIEWPORT)) do
		registerViewportFrame(value)
	end

	CollectionService:GetInstanceAddedSignal(Constants.TAGS.EMITTER_VIEWPORT):Connect(registerViewportFrame)
end

function ParticleManager._spawnParticle(emitter: ParticleEmitter)
	local viewportFrame = ParticleManager._emitters[emitter]
	if not viewportFrame then return end

	local particle = ParticleManager._prefabParticle:Clone()
	particle.Archivable = false

	-- Initialize particle properties
	local data = ParticleManager._initializeParticleData(emitter, particle, viewportFrame)
	ParticleManager._particles[particle] = data

	-- Set initial appearance
	ParticleManager._updateParticleAppearance(particle, data)

	particle.Parent = viewportFrame
end

function ParticleManager._initializeParticleData(emitter: ParticleEmitter, particle: Instance, viewportFrame: ViewportFrame): ParticleData
	-- Set up particle textures
	for _, texture in ipairs(particle:GetChildren()) do
		texture.Texture = emitter.Texture
	end

	local startingCFrame = emitter.Parent:IsA("Attachment") 
		and emitter.Parent.WorldCFrame 
		or emitter.Parent.CFrame

	-- Initialize sequences with random envelope values
	local sizes = table.create(#emitter.Size.Keypoints)
	local transparencies = table.create(#emitter.Transparency.Keypoints)

	for i, value in ipairs(emitter.Size.Keypoints) do
		sizes[i] = NumberSequenceKeypoint.new(
			value.Time, 
			value.Value + math.random(-value.Envelope, value.Envelope), 
			0
		)
	end

	for i, value in ipairs(emitter.Transparency.Keypoints) do
		transparencies[i] = NumberSequenceKeypoint.new(
			value.Time, 
			value.Value + math.random(-value.Envelope, value.Envelope), 
			0
		)
	end

	-- Return complete particle data
	return {
		init = tick(),
		life = math.random(emitter.Lifetime.Min, emitter.Lifetime.Max),
		age = 0,
		emitter = emitter,
		cframe = startingCFrame,
		velocity = ParticleManager._calculateInitialVelocity(emitter, startingCFrame),
		rotation = math.random(emitter.Rotation.Min, emitter.Rotation.Max),
		rotSpeed = math.random(emitter.RotSpeed.Min, emitter.RotSpeed.Max),
		acceleration = emitter.Acceleration,
		collidable = emitter:GetAttribute("CanCollide"),
		renderer = viewportFrame,
		frames = Constants.FLIPBOOK_TILES[emitter.FlipbookLayout],
		frame = emitter.FlipbookStartRandom and math.random(1, Constants.FLIPBOOK_TILES[emitter.FlipbookLayout]) or 1,
		lastFrameUpdate = tick(),
		framerate = math.random(emitter.FlipbookFramerate.Min, emitter.FlipbookFramerate.Max),
		sizeSequence = NumberSequence.new(sizes),
		size = Vector2.new(0, 0),
		color = Color3.new(0, 0, 0),
		transparencySequence = NumberSequence.new(transparencies),
		transparency = 1
	}
end

function ParticleManager._calculateInitialVelocity(emitter: ParticleEmitter, startingCFrame: CFrame): Vector3
	local speed = math.random(emitter.Speed.Min, emitter.Speed.Max)
	return (startingCFrame - startingCFrame.Position)
		* CFrame.Angles(
			math.rad(math.random(-emitter.SpreadAngle.X, emitter.SpreadAngle.X)),
			0,
			math.rad(math.random(-emitter.SpreadAngle.Y, emitter.SpreadAngle.Y))
		)
		* (CFrame.new(Vector3.FromNormalId(emitter.EmissionDirection)).UpVector * speed)
end

function ParticleManager._updateParticle(particle: Instance, data: ParticleData, deltaTime: number, camera: Camera)
	local age = (1 / data.life) * (tick() - data.init)
	data.age = age

	if age >= 1 then
		ParticleManager._destroyParticle(particle)
		return
	end

	-- Update particle properties
	ParticleManager._updateParticlePhysics(data, deltaTime, camera)
	ParticleManager._updateParticleAppearance(particle, data)
	ParticleManager._updateFlipbook(data, deltaTime)
end

function ParticleManager._updateParticlePhysics(data: ParticleData, deltaTime: number, camera: Camera)
	-- Update velocity and position
	data.velocity = data.velocity + (data.acceleration * deltaTime)
	local origin = CFrame.new(data.cframe.Position) * CFrame.new(data.velocity * deltaTime)
	local goal = CFrame.new(origin.Position, camera.CFrame.Position)

	-- Update rotation
	data.rotation = data.rotation + (data.rotSpeed * deltaTime)
	data.cframe = goal * CFrame.Angles(0, 0, math.rad(data.rotation))
end

function ParticleManager._updateParticleAppearance(particle: Instance, data: ParticleData)
	-- Update size
	local size = SequenceEvaluator.evalNumberSequence(data.sizeSequence, data.age) * 2
	local squash = SequenceEvaluator.evalNumberSequence(data.emitter.Squash, data.age)

	local xSize = size - (squash > 0 and squash or 0) + (squash < 0 and -squash or 0)
	local ySize = size - (squash < 0 and -squash or 0) + (squash > 0 and squash or 0)

	data.size = Vector2.new(xSize, ySize)
	particle.Size = Vector3.new(xSize, ySize, 0)

	-- Calculate flipbook offset
	local frame = math.floor(data.frame)
	local sqrtFrames = math.sqrt(data.frames)
	local uOffset = math.floor(frame / sqrtFrames)
	local vOffset = frame % sqrtFrames

	-- Update textures
	for _, texture in ipairs(particle:GetChildren()) do
		texture.Transparency = SequenceEvaluator.evalNumberSequence(data.transparencySequence, data.age)
		texture.Color3 = SequenceEvaluator.evalColorSequence(data.emitter.Color, data.age)

		-- Update flipbook properties
		texture.StudsPerTileU = data.size.X * sqrtFrames
		texture.StudsPerTileV = data.size.Y * sqrtFrames
		texture.OffsetStudsU = data.frames > 1 and (vOffset * data.size.X) or 0
		texture.OffsetStudsV = data.frames > 1 and (uOffset * data.size.Y) or 0
	end
end

function ParticleManager._updateFlipbook(data: ParticleData, deltaTime: number)
	if tick() - data.lastFrameUpdate > (1 / data.framerate) then
		data.frame = (data.frame % data.frames) + (data.framerate * deltaTime)
		data.lastFrameUpdate = tick()
	end
end

function ParticleManager._destroyParticle(particle: Instance)
	ParticleManager._particles[particle] = nil
	particle:Destroy()
end

function ParticleManager._bindToRenderStep()
	RunService:BindToRenderStep(
		"ViewportParticles",
		Enum.RenderPriority.Camera.Value + 1,
		function(deltaTime)
			local bulkMoveToParts = table.create(50)
			local bulkMoveToCFrames = table.create(50)

			-- Update existing particles
			for particle, data in pairs(ParticleManager._particles) do
				local camera = data.renderer.CurrentCamera
				if camera then
					ParticleManager._updateParticle(particle, data, deltaTime, camera)

					-- Add to bulk move tables
					table.insert(bulkMoveToParts, particle)
					table.insert(bulkMoveToCFrames, data.cframe)
				end
			end

			-- Perform bulk move
			if #bulkMoveToParts > 0 then
				workspace:BulkMoveTo(bulkMoveToParts, bulkMoveToCFrames, Enum.BulkMoveMode.FireCFrameChanged)
			end

			-- Spawn new particles from emitters
			for emitter, viewportFrame in pairs(ParticleManager._emitters) do
				local timeBetweenCreation = 1 / emitter.Rate
				local lastSpawn = ParticleManager._emitterLastSpawns[emitter] or 0

				if tick() - lastSpawn > timeBetweenCreation then
					local count = math.ceil(deltaTime / timeBetweenCreation)
					ParticleManager._emitterLastSpawns[emitter] = tick()

					for _ = 1, count do
						ParticleManager._spawnParticle(emitter)
					end
				end
			end
		end
	)
end


return ParticleManager