Skip to content

Commit

Permalink
First attempt at a sprite editor
Browse files Browse the repository at this point in the history
  • Loading branch information
bijington committed Jul 31, 2024
1 parent 3992e62 commit e638ebb
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 67 deletions.
7 changes: 3 additions & 4 deletions games/Orbit.Studio/Orbit.Studio/AppShell.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
x:Class="Orbit.Studio.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Orbit.Studio"
Shell.FlyoutBehavior="Disabled"
xmlns:sprites="clr-namespace:Orbit.Studio.Sprites"
FlyoutBehavior="Disabled"
Title="Orbit.Studio">

<ShellContent
Title="Home"
ContentTemplate="{DataTemplate local:MainPage}"
ContentTemplate="{DataTemplate sprites:SpriteEditorPage}"
Route="MainPage" />

</Shell>
23 changes: 23 additions & 0 deletions games/Orbit.Studio/Orbit.Studio/Controls/ColorPicker.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>

<Grid xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Orbit.Studio.Controls.ColorPicker"
RowDefinitions="*,*,*,*,*"
ColumnDefinitions="*,Auto">

<Slider Minimum="0" Maximum="255" Value="255" x:Name="Red" ValueChanged="OnColorSliderValueChanged" />
<Entry Text="{Binding Value, Source={x:Reference Red}}" Grid.Column="1" />

<Slider Minimum="0" Maximum="255" Value="0" x:Name="Blue" ValueChanged="OnColorSliderValueChanged" Grid.Row="1" />
<Entry Text="{Binding Value, Source={x:Reference Blue}}" Grid.Column="1" Grid.Row="1" />

<Slider Minimum="0" Maximum="255" Value="0" x:Name="Green" ValueChanged="OnColorSliderValueChanged" Grid.Row="2" />
<Entry Text="{Binding Value, Source={x:Reference Green}}" Grid.Column="1" Grid.Row="2" />

<Slider Minimum="0" Maximum="255" Value="255" x:Name="Alpha" ValueChanged="OnColorSliderValueChanged" Grid.Row="3" />
<Entry Text="{Binding Value, Source={x:Reference Alpha}}" Grid.Column="1" Grid.Row="3" />

<BoxView x:Name="ColorPreview" WidthRequest="50" HeightRequest="50" Grid.Row="4" />

</Grid>
42 changes: 42 additions & 0 deletions games/Orbit.Studio/Orbit.Studio/Controls/ColorPicker.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Orbit.Studio.Controls;

public partial class ColorPicker : Grid
{
public ColorPicker()
{
InitializeComponent();
}

private void OnColorSliderValueChanged(object? sender, ValueChangedEventArgs e)
{
SelectedColor = Color.FromRgba(
Red.Value / 255d,
Blue.Value / 255d,
Green.Value / 255d,
Alpha.Value / 255d);
}

public static readonly BindableProperty SelectedColorProperty =
BindableProperty.Create(
nameof(SelectedColor),
typeof(Color),
typeof(ColorPicker),
Colors.Black,
propertyChanged: OnSelectedColorPropertyChanged);

private static void OnSelectedColorPropertyChanged(BindableObject sender, object oldValue, object newValue)
{
((ColorPicker)sender).UpdatePreviewColor();
}

private void UpdatePreviewColor()
{
ColorPreview.BackgroundColor = SelectedColor;
}

public Color SelectedColor
{
get => (Color)GetValue(SelectedColorProperty);
set => SetValue(SelectedColorProperty, value);
}
}
36 changes: 0 additions & 36 deletions games/Orbit.Studio/Orbit.Studio/MainPage.xaml

This file was deleted.

23 changes: 0 additions & 23 deletions games/Orbit.Studio/Orbit.Studio/MainPage.xaml.cs

This file was deleted.

7 changes: 3 additions & 4 deletions games/Orbit.Studio/Orbit.Studio/Orbit.Studio.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
<MauiImage Include="Resources\Images\*"/>
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185"/>

<EmbeddedResource Include="Resources\EmbeddedResources\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />

<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*"/>

Expand All @@ -60,10 +62,7 @@
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)"/>
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)"/>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\engine\Orbit.Engine\Orbit.Engine.csproj" />
<PackageReference Include="Microsoft.Maui.Graphics.Skia" Version="$(MauiVersion)" />
</ItemGroup>

</Project>
52 changes: 52 additions & 0 deletions games/Orbit.Studio/Orbit.Studio/Sprites/SpriteEditorPage.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Orbit.Studio.Controls"
x:Class="Orbit.Studio.Sprites.SpriteEditorPage"
Title="Sprite Editor">

<Grid ColumnDefinitions="2*,*">
<VerticalStackLayout Grid.Column="1" Spacing="10">
<HorizontalStackLayout>
<Label Text="Width" VerticalOptions="Center"/>
<Entry Keyboard="Numeric" Text="16" TextChanged="WidthEntry_OnTextChanged" />
</HorizontalStackLayout>
<HorizontalStackLayout>
<Label Text="Height" VerticalOptions="Center"/>
<Entry Keyboard="Numeric" Text="16" TextChanged="HeightEntry_OnTextChanged" />
</HorizontalStackLayout>

<Label Text="Zoom" />
<Slider
x:Name="Zoom"
Minimum="1"
Maximum="50"
Value="1"
ValueChanged="Zoom_OnValueChanged"/>

<HorizontalStackLayout>
<Label Text="Show grid lines" VerticalOptions="Center"/>
<CheckBox x:Name="ShowGridLines" CheckedChanged="ShowGridLines_OnCheckedChanged" />
</HorizontalStackLayout>

<HorizontalStackLayout>
<Label Text="Show chessboard" VerticalOptions="Center"/>
<CheckBox x:Name="ShowChessboard" CheckedChanged="ShowChessboard_OnCheckedChanged" />
</HorizontalStackLayout>

<Button Text="Undo" Clicked="OnUndoClicked" />

<controls:ColorPicker x:Name="ColorPicker" />

<Button Clicked="Button_OnClicked" Text="Export" />
</VerticalStackLayout>

<GraphicsView
x:Name="Canvas"
Drawable="{Binding}"
MoveHoverInteraction="Canvas_OnMoveHoverInteraction"
EndInteraction="GraphicsView_OnEndInteraction"/>
</Grid>

</ContentPage>
145 changes: 145 additions & 0 deletions games/Orbit.Studio/Orbit.Studio/Sprites/SpriteEditorPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using Microsoft.Maui.Graphics.Skia;

using SkiaSharp;

namespace Orbit.Studio.Sprites;

public partial class SpriteEditorPage : ContentPage, IDrawable
{
public SpriteEditorPage()
{
InitializeComponent();
BindingContext = this;
}

private int width = 16;
private int height = 16;
private IList<Pixel> pixels = [];
private float mouseX;
private float mouseY;

private void GraphicsView_OnEndInteraction(object? sender, TouchEventArgs e)
{
pixels.Add(new Pixel { Color = ColorPicker.SelectedColor, Location = new PointF(mouseX, mouseY) });
}

public void Draw(ICanvas canvas, RectF dirtyRect)
{
Render(canvas, dirtyRect, (float)Zoom.Value, ShowGridLines.IsChecked, ShowChessboard.IsChecked);
}

private void Render(ICanvas canvas, RectF bounds, float zoomFactor, bool showGridLines, bool showChessboard)
{
var renderedWidth = width * zoomFactor;
var renderedHeight = height * zoomFactor;

if (showChessboard)
{
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
var number = y % 2 == 0 ? 1 : 0;
canvas.FillColor = x % 2 == number ? Colors.White : Color.FromRgb(192 / 255d, 192 / 255d, 192 / 255d);
canvas.FillRectangle(x * zoomFactor, y * zoomFactor, 1 * zoomFactor, 1 * zoomFactor);
}
}
}

if (showGridLines)
{
var lineColor = Color.FromRgb(192 / 255d, 192 / 255d, 192 / 255d);

for (int x = 1; x < width; x++)
{
canvas.StrokeColor = lineColor;
canvas.DrawLine(x * zoomFactor, 0, x * zoomFactor, height * zoomFactor);
}

for (int y = 1; y < height; y++)
{
canvas.StrokeColor = lineColor;
canvas.DrawLine(0, y * zoomFactor, width * zoomFactor, y * zoomFactor);
}
}

foreach (var tile in pixels)
{
canvas.FillColor = tile.Color;
canvas.FillRectangle(tile.Location.X * zoomFactor, tile.Location.Y * zoomFactor, zoomFactor, zoomFactor);
}

canvas.FillColor = ColorPicker.SelectedColor;
canvas.FillRectangle(mouseX * zoomFactor, mouseY * zoomFactor, zoomFactor, zoomFactor);
}

private void Zoom_OnValueChanged(object? sender, ValueChangedEventArgs e)
{
Canvas.Invalidate();
}

private void Canvas_OnMoveHoverInteraction(object? sender, TouchEventArgs e)
{
var touch = e.Touches.First();

var zoomFactor = (float)Zoom.Value;

mouseX = MathF.Floor(touch.X / zoomFactor);
mouseY = MathF.Floor(touch.Y / zoomFactor);

Canvas.Invalidate();
}

private void Export()
{
using var canvas = new SkiaCanvas();
using var bitmap = new SKBitmap(new SKImageInfo(width, height));
canvas.Canvas = new SKCanvas(bitmap);

Render(canvas, new RectF(0, 0, width, height), 1f, false, false);

var path = Path.Combine(FileSystem.AppDataDirectory, @"sprite.png");
using var stream = File.Create(path);
bitmap.Encode(stream, SKEncodedImageFormat.Png, 100);
}

private void Button_OnClicked(object? sender, EventArgs e)
{
Export();
}

private void ShowGridLines_OnCheckedChanged(object? sender, CheckedChangedEventArgs e)
{
Canvas.Invalidate();
}

private void ShowChessboard_OnCheckedChanged(object? sender, CheckedChangedEventArgs e)
{
Canvas.Invalidate();
}

private void OnUndoClicked(object? sender, EventArgs e)
{
pixels.RemoveAt(pixels.Count - 1);
Canvas.Invalidate();
}

private void WidthEntry_OnTextChanged(object? sender, TextChangedEventArgs e)
{
int.TryParse(e.NewTextValue, out width);
Canvas.Invalidate();
}

private void HeightEntry_OnTextChanged(object? sender, TextChangedEventArgs e)
{
int.TryParse(e.NewTextValue, out height);
Canvas.Invalidate();
}
}

public class Pixel
{
public Color Color { get; init; }

public PointF Location { get; init; }
}

0 comments on commit e638ebb

Please sign in to comment.