Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement a few more /icon.Blend() features #964

Merged
merged 4 commits into from
Dec 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 81 additions & 25 deletions OpenDreamRuntime/Objects/DreamIcon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Color = Robust.Shared.Maths.Color;
using ParsedDMIDescription = OpenDreamShared.Resources.DMIParser.ParsedDMIDescription;
using ParsedDMIState = OpenDreamShared.Resources.DMIParser.ParsedDMIState;
using ParsedDMIFrame = OpenDreamShared.Resources.DMIParser.ParsedDMIFrame;
Expand Down Expand Up @@ -280,7 +281,8 @@ public interface IDreamIconOperation {
public void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i bounds);
}

public sealed class DreamIconOperationBlend : IDreamIconOperation {
[Virtual]
public class DreamIconOperationBlend : IDreamIconOperation {
// With the same values as the ICON_* defines in DMStandard
public enum BlendType {
Add = 0,
Expand All @@ -294,26 +296,76 @@ public enum BlendType {

private readonly BlendType _type;
private readonly int _xOffset, _yOffset;
private readonly Image<Rgba32> _blending;
private readonly ParsedDMIDescription _blendingDescription;

public DreamIconOperationBlend(BlendType type, DreamValue blending, int xOffset, int yOffset) {
protected DreamIconOperationBlend(BlendType type, int xOffset, int yOffset) {
_type = type;
_xOffset = xOffset;
_yOffset = yOffset;

if (_type is not BlendType.Overlay and not BlendType.Underlay and not BlendType.Add and not BlendType.Subtract)
throw new NotImplementedException($"\"{_type}\" blending is not implemented");
}

public virtual void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i bounds) {
throw new NotImplementedException();
}

protected void BlendPixel(Rgba32[] pixels, int dstPixelPosition, Rgba32 src) {
Rgba32 dst = pixels[dstPixelPosition];

switch (_type) {
case BlendType.Add: {
pixels[dstPixelPosition].R = (byte)Math.Min(dst.R + src.R, byte.MaxValue);
pixels[dstPixelPosition].G = (byte)Math.Min(dst.G + src.G, byte.MaxValue);
pixels[dstPixelPosition].B = (byte)Math.Min(dst.B + src.B, byte.MaxValue);

// BYOND uses the smaller of the two alphas
pixels[dstPixelPosition].A = Math.Min(dst.A, src.A);
break;
}
case BlendType.Subtract: {
pixels[dstPixelPosition].R = (byte)Math.Max(dst.R - src.R, byte.MinValue);
pixels[dstPixelPosition].G = (byte)Math.Max(dst.G - src.G, byte.MinValue);
pixels[dstPixelPosition].B = (byte)Math.Max(dst.B - src.B, byte.MinValue);

// BYOND uses the smaller of the two alphas
pixels[dstPixelPosition].A = Math.Min(dst.A, src.A);
break;
}

case BlendType.Overlay: {
pixels[dstPixelPosition].R = (byte) (dst.R + (src.R - dst.R) * src.A / 255);
pixels[dstPixelPosition].G = (byte) (dst.G + (src.G - dst.G) * src.A / 255);
pixels[dstPixelPosition].B = (byte) (dst.B + (src.B - dst.B) * src.A / 255);

byte highAlpha = Math.Max(dst.A, src.A);
byte lowAlpha = Math.Min(dst.A, src.A);
pixels[dstPixelPosition].A = (byte) (highAlpha + (highAlpha * lowAlpha / 255));
break;
}
case BlendType.Underlay: {
// Opposite of overlay
(dst, src) = (src, dst);
goto case BlendType.Overlay;
}
}
}
}

public sealed class DreamIconOperationBlendImage : DreamIconOperationBlend {
private readonly Image<Rgba32> _blending;
private readonly ParsedDMIDescription _blendingDescription;

public DreamIconOperationBlendImage(BlendType type, int xOffset, int yOffset, DreamValue blending) : base(type, xOffset, yOffset) {
//TODO: Find a way to get rid of this!
var objectTree = IoCManager.Resolve<IDreamObjectTree>();
var resourceManager = IoCManager.Resolve<DreamResourceManager>();

(var blendingResource, _blendingDescription) = DreamMetaObjectIcon.GetIconResourceAndDescription(objectTree, resourceManager, blending);
_blending = resourceManager.LoadImage(blendingResource);

if (_type is not BlendType.Overlay and not BlendType.Underlay)
throw new NotImplementedException($"\"{_type}\" blending is not implemented");
}

public void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i bounds) {
public override void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i bounds) {
_blending.ProcessPixelRows(accessor => {
// The first frame of the source image blends with the first frame of the destination image
// The second frame blends with the second, and so on
Expand All @@ -332,23 +384,7 @@ public void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i boun
Rgba32 dst = pixels[dstPixelPosition];
Rgba32 src = row[srcFramePos.Value.X + x - bounds.Left];

switch (_type) {
case BlendType.Overlay: {
pixels[dstPixelPosition].R = (byte) (dst.R + (src.R - dst.R) * src.A / 255);
pixels[dstPixelPosition].G = (byte) (dst.G + (src.G - dst.G) * src.A / 255);
pixels[dstPixelPosition].B = (byte) (dst.B + (src.B - dst.B) * src.A / 255);

byte highAlpha = Math.Max(dst.A, src.A);
byte lowAlpha = Math.Min(dst.A, src.A);
pixels[dstPixelPosition].A = (byte) (highAlpha + (highAlpha * lowAlpha / 255));
break;
}
case BlendType.Underlay: {
// Opposite of overlay
(dst, src) = (src, dst);
goto case BlendType.Overlay;
}
}
BlendPixel(pixels, dstPixelPosition, src);
}
}
});
Expand All @@ -371,3 +407,23 @@ public void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i boun
return (column * _blendingDescription.Width, row * _blendingDescription.Height);
}
}

public sealed class DreamIconOperationBlendColor : DreamIconOperationBlend {
private readonly Rgba32 _color;

public DreamIconOperationBlendColor(BlendType type, int xOffset, int yOffset, Color color) : base(type, xOffset, yOffset) {
_color = new Rgba32(color.RByte, color.GByte, color.BByte, color.AByte);
}

public override void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i bounds) {
// TODO: x & y offsets

for (int y = bounds.Top; y < bounds.Bottom; y++) {
for (int x = bounds.Left; x < bounds.Right; x++) {
int dstPixelPosition = (y * imageSpan) + x;

BlendPixel(pixels, dstPixelPosition, _color);
}
}
}
}
15 changes: 13 additions & 2 deletions OpenDreamRuntime/Objects/MetaObjects/DreamMetaObjectIcon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ public void OnObjectCreated(DreamObject dreamObject, DreamProcArguments creation
DreamValue frame = creationArguments.GetArgument(3, "frame");
DreamValue moving = creationArguments.GetArgument(4, "moving");

DreamIcon dreamIcon = new(_rscMan);
ObjectToDreamIcon.Add(dreamObject, dreamIcon);
var dreamIcon = InitializeIcon(_rscMan, dreamObject);

if (icon != DreamValue.Null) {
// TODO: Could maybe have an alternative path for /icon values so the DMI doesn't have to be generated
Expand Down Expand Up @@ -61,6 +60,18 @@ public void OnVariableSet(DreamObject dreamObject, string varName, DreamValue va
}
}

/// <summary>
/// A fast path for initializing an /icon object
/// </summary>
/// <remarks>Doesn't call any DM code</remarks>
/// <returns>The /icon's DreamIcon</returns>
public static DreamIcon InitializeIcon(DreamResourceManager rscMan, DreamObject icon) {
DreamIcon dreamIcon = new(rscMan);

ObjectToDreamIcon.Add(icon, dreamIcon);
return dreamIcon;
}

public static (DreamResource Resource, ParsedDMIDescription Description) GetIconResourceAndDescription(
IDreamObjectTree objectTree, DreamResourceManager resourceManager, DreamValue value) {
if (value.TryGetValueAsDreamObjectOfType(objectTree.Icon, out var iconObj)) {
Expand Down
18 changes: 12 additions & 6 deletions OpenDreamRuntime/Procs/DMOpcodeHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading.Tasks;
using OpenDreamRuntime.Objects;
using OpenDreamRuntime.Objects.MetaObjects;
using OpenDreamRuntime.Procs.Native;
using OpenDreamRuntime.Resources;
using OpenDreamShared.Dream;
using OpenDreamShared.Dream.Procs;
Expand Down Expand Up @@ -617,11 +618,21 @@ private static void HandleSuffixPronoun(ref StringBuilder formattedString, ReadO

return null;
} else {
throw new Exception("Invalid append operation on " + first + " and " + second);
throw new Exception($"Invalid append operation on {first} and {second}");
}
} else {
result = second;
}
} else if (first.TryGetValueAsDreamResource(out _) || first.TryGetValueAsDreamObjectOfType(state.Proc.ObjectTree.Icon, out _)) {
// Implicitly create a new /icon and ICON_ADD blend it
// Note that BYOND creates something other than an /icon, but it behaves the same as one in most reasonable interactions
DreamObject iconObj = state.Proc.ObjectTree.CreateObject(DreamPath.Icon);
var icon = DreamMetaObjectIcon.InitializeIcon(state.Proc.DreamResourceManager, iconObj);
var from = DreamMetaObjectIcon.GetIconResourceAndDescription(state.Proc.ObjectTree, state.Proc.DreamResourceManager, first);

icon.InsertStates(from.Resource, from.Description, DreamValue.Null, DreamValue.Null, DreamValue.Null);
DreamProcNativeIcon.Blend(icon, second, DreamIconOperationBlend.BlendType.Add, 0, 0);
result = new DreamValue(iconObj);
} else if (second.Value != null) {
switch (first.Type) {
case DreamValue.DreamValueType.Float when second.Type == DreamValue.DreamValueType.Float:
Expand All @@ -630,11 +641,6 @@ private static void HandleSuffixPronoun(ref StringBuilder formattedString, ReadO
case DreamValue.DreamValueType.String when second.Type == DreamValue.DreamValueType.String:
result = new DreamValue(first.GetValueAsString() + second.GetValueAsString());
break;
case DreamValue.DreamValueType.DreamResource when (second.Type == DreamValue.DreamValueType.String && first.TryGetValueAsDreamResource(out var rsc) && rsc.ResourcePath.EndsWith("dmi")):
// TODO icon += hexcolor is the same as Blend()
state.DreamManager.WriteWorldLog("Appending colors to DMIs is not implemented", LogLevel.Warning, "opendream.unimplemented");
result = first;
break;
default:
throw new Exception("Invalid append operation on " + first + " and " + second);
}
Expand Down
18 changes: 14 additions & 4 deletions OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using OpenDreamRuntime.Objects;
using OpenDreamRuntime.Objects.MetaObjects;
using OpenDreamRuntime.Resources;
using OpenDreamShared.Dream;
using BlendType = OpenDreamRuntime.Objects.DreamIconOperationBlend.BlendType;

namespace OpenDreamRuntime.Procs.Native {
static class DreamProcNativeIcon {
Expand Down Expand Up @@ -46,6 +48,17 @@ public static DreamValue NativeProc_Insert(DreamObject instance, DreamObject usr
return DreamValue.Null;
}

public static void Blend(DreamIcon icon, DreamValue blend, BlendType function, int x, int y) {
if (blend.TryGetValueAsString(out var colorStr)) {
if (!ColorHelpers.TryParseColor(colorStr, out var color))
throw new Exception($"Invalid color {colorStr}");

icon.ApplyOperation(new DreamIconOperationBlendColor(function, x, y, color));
} else {
icon.ApplyOperation(new DreamIconOperationBlendImage(function, x, y, blend));
}
}

[DreamProc("Blend")]
[DreamProcParameter("icon", Type = DreamValue.DreamValueType.DreamObject)]
[DreamProcParameter("function", Type = DreamValue.DreamValueType.Float)]
Expand All @@ -63,10 +76,7 @@ public static DreamValue NativeProc_Blend(DreamObject instance, DreamObject usr,
if (!function.TryGetValueAsInteger(out var functionValue))
throw new Exception($"Invalid 'function' argument {function}");

var blendType = (DreamIconOperationBlend.BlendType) functionValue;

DreamIcon iconObj = DreamMetaObjectIcon.ObjectToDreamIcon[instance];
iconObj.ApplyOperation(new DreamIconOperationBlend(blendType, icon, x, y));
Blend(DreamMetaObjectIcon.ObjectToDreamIcon[instance], icon, (BlendType)functionValue, x, y);
return DreamValue.Null;
}

Expand Down