OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / IFSEngine / Model / IFS.cs

235 lines · csharp ref:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;

using IFSEngine.Animation;
using IFSEngine.Utility;

namespace IFSEngine.Model;

/// <summary>
/// Iterated Function System.
/// The model is similar to flam3, with additional IFSRenderer parameters, like effects.
/// </summary>
public class IFS
{
    public string Title { get; set; } = "Untitled";
    public IReadOnlyList<Author> Authors => authors;
    public IReadOnlySet<Iterator> Iterators => iterators;
    /// <summary>
    /// List of post-processing effects applied to the rendered image in order.
    /// </summary>
    public IReadOnlyList<EffectLayer> PostEffects => postEffects;

    /// <summary>
    /// Entropy is the probability to reset on each iteration.
    /// </summary>
    /// <remarks>
    /// Based on zy0rg's description, but it applies to the whole system for now.
    /// This replaces the constant 10 000 iteration depth in Flame. Defaults to 0.01.
    /// </remarks>
    public double Entropy { get; set; } = 0.01;

    /// <summary>
    /// Number of iterations to skip plotting after reset.
    /// </summary>
    /// <remarks>
    /// This is needed to avoid seeing the starting random points.
    /// Also known as "fuse count". This is 20 in Flame. Defaults to 0.
    /// </remarks>
    public int Warmup { get; set; } = 0;
    public double Brightness { get; set; } = 1.0;
    public double Gamma { get; set; } = 1.0;
    public double GammaThreshold { get; set; } = 0.0;
    public double Vibrancy { get; set; } = 1.0;
    public double FogEffect { get; set; } = 0.0;
    public Camera Camera { get; set; } = new Camera();
    public Color BackgroundColor { get; set; } = Color.Black;
    public Size ImageResolution { get; set; } = new Size(1920, 1080);
    public ColorPalette Palette { get; set; } = ColorPalette.Default;
    public Dopesheet Dopesheet { get; set; } = new Dopesheet();//null;

    /// <summary>
    /// Defines the number of iterations that has to be performed for the image to be considered "done".
    /// The formula is <code> log2(1+iters/(w*h)) > target </code>
    /// Type is double to allow animation.
    /// </summary>
    public double TargetIterationLevel { get; set; } = 15;

    [Obsolete("Use Node(int iteratorId) instead. This is kept for backward compatibility.")]
    public Iterator this[int iteratorId] => Iterators.First(i => i.Id == iteratorId);


    /// <summary>
    /// Indexer for iterator by ID. Used by the animation system via reflection.
    /// </summary>
    public Iterator Node(int iteratorId) => Iterators.First(i => i.Id == iteratorId);

    /// <summary>
    /// Indexer for post-fx effect layers by ID. Used by the animation system via reflection.
    /// </summary>
    public EffectLayer PostFx(int effectLayerId) => PostEffects.First(l => l.Id == effectLayerId);

    protected List<Author> authors = [];
    protected HashSet<Iterator> iterators = [];
    protected List<EffectLayer> postEffects = [];

    /// <param name="connect">Whether to connect the new <see cref="Iterator"/> to existing ones.</param>
    public void AddIterator(Iterator newIterator, bool connect)
    {
        if(iterators.Contains(newIterator))
            throw new InvalidOperationException($"Unable to add Iterator (Id: {newIterator.Id}) to IFS, it already exists.");
        iterators.Add(newIterator);
        double weight = connect ? 1.0 : 0.0;
        foreach (var it in iterators)
        {
            newIterator.WeightTo[it] = weight;
            it.WeightTo[newIterator] = weight;
        }
    }

    /// <param name="splitWeights">Whether to adjust the base weights of the original iterator and its duplicate in order to keep a similar look of the fractal. "False" results in the duplicate operation known from flame fractals.</param>
    /// <returns>The created duplicate.</returns>
    public Iterator DuplicateIterator(Iterator original, bool splitWeights)
    {
        //clone first
        Iterator dupe = new(original.Transform)
        {
            BaseWeight = original.BaseWeight,
            ColorIndex = original.ColorIndex,
            ColorSpeed = original.ColorSpeed,
            Opacity = original.Opacity,
            ShadingMode = original.ShadingMode,
            StartWeight = original.StartWeight,
            Mix = original.Mix,
            Add = original.Add,
            Name = original.Name
        };
        //copy parameter values
        foreach (var tv in original.RealParams)
            dupe.RealParams[tv.Key] = tv.Value;
        foreach (var tv in original.Vec3Params)
            dupe.Vec3Params[tv.Key] = tv.Value;
        //add to the ifs
        AddIterator(dupe, false);
        //copy connection weights
        foreach (Iterator it in Iterators)
        {//'from' weights
            if (it.WeightTo.TryGetValue(original, out double w))
                it.WeightTo[dupe] = w;
        }
        foreach (var w in original.WeightTo)
        {//'to' weights
            if (w.Key == original)//if self-connected, do the same on the duplicate
                dupe.WeightTo[dupe] = w.Value;
            else
                dupe.WeightTo[w.Key] = w.Value;
        }

        if (splitWeights)
        {//split base weights, no connection between the two
            original.BaseWeight /= 2;
            dupe.BaseWeight = original.BaseWeight;
            dupe.WeightTo[original] = 0.0;
            original.WeightTo[dupe] = 0.0;
        }
        else
        {//connect the original with the duplicate
            dupe.WeightTo[original] = 1.0;
            original.WeightTo[dupe] = 1.0;
        }
        return dupe;
    }

    public void RemoveIterator(Iterator it1)
    {
        if (!iterators.Contains(it1))
            throw new InvalidOperationException($"Unable to remove Iterator (Id: {it1.Id}) from IFS.");
        RemoveAnimationChannels(it1.Id);
        //removing connecting weights
        foreach (var it in iterators)
        {
            it1.WeightTo.Remove(it);
            it.WeightTo.Remove(it1);
        }
        iterators.Remove(it1);
    }

    public void AddPostEffectLayer(EffectLayer layer)
    {
        if (postEffects.Contains(layer))
            throw new InvalidOperationException($"Unable to add PostEffect layer (Id: {layer.Id}) to IFS, it already exists.");
        postEffects.Add(layer);
    }

    public void RemovePostEffectLayer(EffectLayer layer)
    {
        if (!postEffects.Contains(layer))
            throw new InvalidOperationException($"Unable to remove PostEffect layer (Id: {layer.Id}) from IFS.");
        RemoveAnimationChannels(layer.Id);
        postEffects.Remove(layer);
    }

    /// <summary>
    /// Moves a post effect to a new index in the layer list.
    /// </summary>
    public void MovePostEffectLayer(EffectLayer layer, int newIndex)
    {
        int oldIndex = postEffects.IndexOf(layer);
        if (oldIndex < 0 || newIndex < 0 || newIndex >= postEffects.Count || oldIndex == newIndex)
            return;
        postEffects.RemoveAt(oldIndex);
        postEffects.Insert(newIndex, layer);
    }

    private void RemoveAnimationChannels(int sourceId)
    {
        var channels = Dopesheet.Channels.Where(c => c.Key.Contains(sourceId.ToString())).ToList();
        foreach (var channel in channels)
            Dopesheet.Channels.Remove(channel.Key);
    }

    public void ReloadPlugins(IEnumerable<TransformPlugin> transforms, IEnumerable<EffectPlugin> effects)
    {
        foreach (var iterator in iterators.ToList())
        {
            //ignore plugin version checking here so they are updated.
            var newtf = transforms.FirstOrDefault(tf => tf.Name == iterator.Transform.Name);
            if (newtf != null)
                iterator.SetTransform(newtf);
            //leave old plugin if a newer version is not found
        }

        foreach (var layer in PostEffects.ToList())
        {
            //ignore plugin version checking here so they are updated.
            var newfx = effects.FirstOrDefault(fx => fx.Name == layer.Effect.Name);
            if (newfx != null)
                layer.SetEffect(newfx);
            //leave old plugin if a newer version is not found
        }
    }

    public void AddAuthor(Author author)
    {
        if (!authors.Contains(author))
            authors.Add(author);
    }

    public void EvaluateAt(TimeOnly t)
    {

        foreach (var (path, channel) in Dopesheet.Channels)
        {
            var val = channel.EvaluateAt(t.ToTimeSpan() / TimeSpan.FromSeconds(1));

            NestedReflectionHelper.SetMemberValueByPath(this, path, val);

        }
    }

    public static readonly IFS Default = new();

}