public SimpleRGB ToRGB() { // Algorithm from https://www.rapidtables.com/convert/color/hsv-to-rgb.html double c = this.V * this.S; double x = c * (1 - Math.Abs((this.H / 60.0) % 2 - 1)); double m = this.V - c; double rr = 0.0; double gg = 0.0; double bb = 0.0; if (this.H < 60.0) { rr = c; gg = x; } else if (this.H < 120.0) { rr = x; gg = c; } else if (this.H < 180.0) { gg = c; bb = x; } else if (this.H < 240.0) { gg = x; bb = c; } else if (this.H < 300.0) { rr = x; bb = c; } else { rr = c; bb = x; } SimpleRGB converted = new SimpleRGB(); converted.R = Math.Min(255.0, Math.Max(0.0, 255.0 * (rr + m))); converted.G = Math.Min(255.0, Math.Max(0.0, 255.0 * (gg + m))); converted.B = Math.Min(255.0, Math.Max(0.0, 255.0 * (bb + m))); return(converted); }
public override void Entry(IModHelper helper) { Instance = this; Config = helper.ReadConfig <PondPainterConfig>(); helper.Events.GameLoop.DayStarted += GameLoop_DayStarted; if (Config.Enable_Animations) { helper.Events.GameLoop.UpdateTicked += GameLoop_UpdateTicked; } foreach (IContentPack contentPack in this.Helper.ContentPacks.GetOwned()) { this.Monitor.Log($"Reading content pack: {contentPack.Manifest.Name} {contentPack.Manifest.Version} from {contentPack.DirectoryPath}", LogLevel.Trace); // We are assuming that we will receive the packs in proper dependency order. // However, dependencies are traditionally "whoever updates last wins" but tag matching is usually the opposite. // So we will try to do the following. If we get pack A, B, C we save the data as C entries, B entries, A entries. int entryIndex = 0; if (contentPack.HasFile(ContentPackFile)) { PondPainterPackData packData = contentPack.ReadJsonFile <PondPainterPackData>(ContentPackFile); if (packData.EmptyPondColor != null) { Data.EmptyPondColor = ColorLookup.FromName(packData.EmptyPondColor); } int index = 0; foreach (PondPainterPackEntry entry in packData.Entries) { index++; string LogName = String.Format("Entry {0}", index); if (entry.LogName != null && !entry.LogName.Equals("")) { LogName = entry.LogName; } //this.Monitor.Log($"Found an entry called \"{LogName}\" and will now try to parse it.", LogLevel.Debug); if (entry.Tags.Count == 0) { this.Monitor.Log($"Entry \"{LogName}\" has an empty Tags list and will be skipped.", LogLevel.Warn); continue; } int cindex = 0; PondPainterDataEntry DataEntry = new PondPainterDataEntry(contentPack.Manifest.UniqueID, LogName, entry.Tags); foreach (PondPainterPackColor c in entry.Colors) { cindex++; Color?theColor = null; if (c.ColorName != null) { theColor = ColorLookup.FromName(c.ColorName); } if (theColor == null) { this.Monitor.Log($"Entry \"{LogName}\" color definition {cindex} does not have a ColorName defined and will be skipped.", LogLevel.Warn); continue; } // We've passed the null check so it is time to get rid of the damned nullable type Color theRealColor = (Color)theColor; bool HasAnimation = false; // Some defaults; note the range is a double even though the content pack only takes ints // This is because the internal ColorMine properties are all doubles int AnimationFrameDelay = 10; double AnimationRange = 20.0f; // Of course I had to make this configurable too, which means I have to sanity-check it. // For other options I could delay the sanity checks until I was certain we actually had a properly defined type, but this one // needs to be checked before I try to construct the animations. This means some meaningless error messages will be logged if // there is an animation type of "none" and something wrong with this value. // The total amount of steps is really twice this variable +1 since we go from base + steps to base - steps int AnimationSteps = 30; if (c.AnimationTotalFrames != null) { AnimationSteps = Math.Abs((int)c.AnimationTotalFrames); if (c.AnimationTotalFrames > -2 && c.AnimationTotalFrames < 2) { this.Monitor.Log($"Entry \"{LogName}\" color definition {cindex} has animation total frames set to ({c.AnimationTotalFrames}). The minimum useful value is 2 and that will be used instead.", LogLevel.Warn); AnimationSteps = 2; } else if (c.AnimationTotalFrames < 0) { this.Monitor.Log($"Entry \"{LogName}\" color definition {cindex} has animation total frames set to ({c.AnimationTotalFrames}). This value should be positive and will be changed to {AnimationSteps}.", LogLevel.Warn); } } List <Color> AnimationColors = new List <Color>(); if (c.AnimationType != null && !c.AnimationType.Equals("none")) { // An AnimationType was given, and each type is processed a bit differently. // The range will be interpreted differently for various types, but it should // never be 0 since that represents an animation that does nothing. // Note that null right now is still ok; it is only an explicit 0 being excluded if (c.AnimationRange == null || c.AnimationRange != 0) { if (c.AnimationType.Equals("hue")) { if (c.AnimationRange != null) { // restrict range to (0, 180] if (c.AnimationRange < 0 || c.AnimationRange > 180) { AnimationRange = Math.Min(180.0f, Math.Abs((int)c.AnimationRange)); this.Monitor.Log($"Entry \"{LogName}\" color definition {cindex} has an animation range of ({c.AnimationRange}). Hue animations must have a positive range <= 180 so this will be interpreted as {AnimationRange}.", LogLevel.Warn); } else { AnimationRange = (double)c.AnimationRange; } } else { AnimationRange = 20.0f; this.Monitor.Log($"Entry \"{LogName}\" color definition {cindex} had no animation range listed; the default of {AnimationRange} will be used.", LogLevel.Debug); } HasAnimation = true; // Calculating the animation frames, in HSV via ColorMine // We use a sine model with the animation "range" as amplitude and a period of the number of steps. SimpleRGB BaseRGB = new SimpleRGB(theRealColor.R, theRealColor.G, theRealColor.B); SimpleHSV BaseHSV = BaseRGB.ToHSV(); this.Monitor.Log($"Entry \"{LogName}\" color definition {cindex} Tracing HUE animation with base of {BaseHSV.H} and a range of {AnimationRange} in {AnimationSteps} steps.", LogLevel.Trace); for (int i = 0; i <= AnimationSteps; i++) { // We can't do the simpler NewHSV = BaseHSV because that is not a true copy SimpleHSV NewHSV = BaseRGB.ToHSV(); double hue = (360 + BaseHSV.H + AnimationRange * Math.Sin(2 * i * Math.PI / AnimationSteps)) % 360; NewHSV.H = hue; //this.Monitor.Log($"** Animation trace step {i}: hue {hue}. Base {BaseHSV.H}", LogLevel.Trace); SimpleRGB NewRGB = NewHSV.ToRGB(); AnimationColors.Add(new Color((int)NewRGB.R, (int)NewRGB.G, (int)NewRGB.B)); } } else if (c.AnimationType.Equals("value")) { if (c.AnimationRange != null) { // restrict range to (0, 100] (will be converted to (0, 1] later) if (c.AnimationRange < 0 || c.AnimationRange > 100) { AnimationRange = Math.Min(100, Math.Abs((int)c.AnimationRange)); this.Monitor.Log($"Entry \"{LogName}\" color definition {cindex} has an animation range of {c.AnimationRange}. Value animations must have a positive range <= 100 so this will be interpreted as {AnimationRange}.", LogLevel.Warn); } else { AnimationRange = (double)c.AnimationRange; } } else { AnimationRange = 20.0f; this.Monitor.Log($"Entry \"{LogName}\" color definition {cindex} had no animation range listed; the default of {AnimationRange} will be used.", LogLevel.Info); } AnimationRange /= 100.0f; HasAnimation = true; // Calculating the animation frames, in HSV via ColorMine // We use a sine model with the animation "range" as amplitude and a period of the number of steps. SimpleRGB BaseRGB = new SimpleRGB(theRealColor.R, theRealColor.G, theRealColor.B); SimpleHSV BaseHSV = BaseRGB.ToHSV(); //this.Monitor.Log($"Entry \"{LogName}\" color definition {cindex} Tracing VALUE animation with base of {BaseHSV.V} and a range of {AnimationRange} in {AnimationSteps} steps.", LogLevel.Trace); for (int i = 0; i <= AnimationSteps; i++) { // We can't do the simpler NewHSV = BaseHSV because that is not a true copy SimpleHSV NewHSV = BaseRGB.ToHSV(); double val = Math.Min(Math.Max(BaseHSV.V + AnimationRange * Math.Sin(2 * i * Math.PI / AnimationSteps), 0), 1); NewHSV.V = val; this.Monitor.Log($"** Animation trace step {i}: value {val}. Base {BaseHSV.V}", LogLevel.Trace); SimpleRGB NewRGB = NewHSV.ToRGB(); AnimationColors.Add(new Color((int)NewRGB.R, (int)NewRGB.G, (int)NewRGB.B)); } } else { this.Monitor.Log($"Entry \"{LogName}\" color definition {cindex} has an unknown animation type ({c.AnimationType}). A static color will be used instead.", LogLevel.Warn); } } else // AnimationRange was 0 { this.Monitor.Log($"Entry \"{LogName}\" color definition {cindex} has an animation range of zero. A static color will be used instead.", LogLevel.Warn); } } if (HasAnimation) { // To get here, we had a valid animation type with an appropriate range. // One final sanity check is on the timing. if (c.AnimationFrameDelay == null || c.AnimationFrameDelay == 0) { this.Monitor.Log($"Entry \"{LogName}\" color definition {cindex} had no animation frame delay listed; the default of {AnimationFrameDelay} frames will be used.", LogLevel.Info); } else { AnimationFrameDelay = Math.Abs((int)c.AnimationFrameDelay); if (c.AnimationFrameDelay < 0) { this.Monitor.Log($"Entry \"{LogName}\" color definition {cindex} had an animation frame delay of {c.AnimationFrameDelay}; timings must be positive so this will be interpreted as {AnimationFrameDelay}.", LogLevel.Warn); } } DataEntry.Colors.Add(c.MinPopulationForColor, new PondPainterDataColorDef(AnimationColors, AnimationFrameDelay)); this.Monitor.Log($"Entry \"{LogName}\" color definition {cindex} successfully added as a new animation.", LogLevel.Trace); } else { DataEntry.Colors.Add(c.MinPopulationForColor, new PondPainterDataColorDef((Color)theColor)); this.Monitor.Log($"Entry \"{LogName}\" color definition {cindex} successfully added as a new static color.", LogLevel.Trace); } } Data.Entries.Insert(entryIndex++, DataEntry); //this.Monitor.Log($"Entry \"{LogName}\" complete entry added to internal data.", LogLevel.Trace); } } else { this.Monitor.Log($"Unable to load content pack {contentPack.Manifest.Name} {contentPack.Manifest.Version} because no {ContentPackFile} file was found.", LogLevel.Warn); } } this.Monitor.Log($"Finished loading content packs. Data has {Data.Entries.Count} entries.", LogLevel.Trace); }