Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17b344376d | ||
|
|
0ed4e44878 | ||
|
|
b42c1832f0 | ||
|
|
058534ba67 | ||
|
|
204dcd6498 | ||
|
|
2c846c0db9 | ||
|
|
2faeb044e0 | ||
|
|
09c8e4f779 | ||
|
|
6994fa6be8 | ||
|
|
cc7beb7670 | ||
|
|
510653732d | ||
|
|
93e8178d67 | ||
|
|
cebc4864cc | ||
|
|
6ad0449376 | ||
|
|
c33c977326 | ||
|
|
f0299d365a | ||
|
|
6ecdca73f5 | ||
|
|
af6a709b2c | ||
|
|
d5c27450ef | ||
|
|
d10269fb07 | ||
|
|
53d987476e | ||
|
|
8b7866d37f | ||
|
|
bb529729b6 | ||
|
|
b7735d9ba8 | ||
|
|
ce744e2b84 | ||
|
|
631c92da3f | ||
|
|
b7063804e9 | ||
|
|
75d47c8419 | ||
|
|
114fb05e80 |
@@ -1,5 +1,12 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.12.0
|
||||
|
||||
- 支持皮肤列表 (仅 3.8.x 及以上支持)
|
||||
- 支持多轨道动画
|
||||
- 动画和皮肤列表多选时改为取并集
|
||||
- 修复导出时没有正确处理预乘像素的问题
|
||||
|
||||
## v0.11.5
|
||||
|
||||
- 导出格式全面支持
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
|
||||
|
||||
[](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
|
||||
[中文](README.md) | [English](README.en.md)
|
||||
|
||||
@@ -10,6 +12,10 @@
|
||||
|
||||
---
|
||||
|
||||
:sparkles: v0.12.x New Feature: Support for multi-track animations and multi-skin list management :sparkles:
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Head over to the [Release](https://github.com/ww-rm/SpineViewer/releases) page to download the zip package.
|
||||
@@ -23,7 +29,7 @@ Exporting video formats such as GIF requires that ffmpeg is installed locally an
|
||||
## Supported Export Formats
|
||||
|
||||
| Export Format | Suitable for Scenario |
|
||||
| :------------: | :------------------------------------------------------------------------------------:|
|
||||
| ------------ | ------------------------------------------------------------------------------------|
|
||||
| Single Frame | Supports generating high-definition model snapshots; you can manually adjust the frame. |
|
||||
| Frame Sequence | Supports png sequence output with transparency and lossless compression. |
|
||||
| GIF | Ideal for generating preview animations. |
|
||||
|
||||
24
README.md
24
README.md
@@ -1,6 +1,8 @@
|
||||
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
|
||||
|
||||
[](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
|
||||
[中文](README.md) | [English](README.en.md)
|
||||
|
||||
@@ -10,6 +12,10 @@
|
||||
|
||||
---
|
||||
|
||||
:sparkles: v0.12.x 新增功能: 支持多轨道动画以及多皮肤列表管理 :sparkles:
|
||||
|
||||
---
|
||||
|
||||
## 安装
|
||||
|
||||
前往 [Release](https://github.com/ww-rm/SpineViewer/releases) 界面下载压缩包.
|
||||
@@ -22,15 +28,15 @@
|
||||
|
||||
## 导出格式支持
|
||||
|
||||
| 导出格式 | 适用场景 |
|
||||
| :---: | :---: |
|
||||
| 单帧画面 | 支持生成高清模型画面图像, 可手动调节需要的一帧. |
|
||||
| 帧序列 | 支持 png 格式帧序列, 可保留透明通道且无损压缩. |
|
||||
| GIF | 适合生成预览动图. |
|
||||
| MP4 | 最常见的视频格式, 兼容性最好. |
|
||||
| WebM | 适合浏览器在线播放格式, 支持透明背景. |
|
||||
| MKV | 适合折腾. |
|
||||
| MOV | 适合折腾. |
|
||||
| 导出格式 | 适用场景 |
|
||||
| --- | --- |
|
||||
| 单帧画面 | 支持生成高清模型画面图像, 可手动调节需要的一帧. |
|
||||
| 帧序列 | 支持 png 格式帧序列, 可保留透明通道且无损压缩. |
|
||||
| GIF | 适合生成预览动图. |
|
||||
| MP4 | 最常见的视频格式, 兼容性最好. |
|
||||
| WebM | 适合浏览器在线播放格式, 支持透明背景. |
|
||||
| MKV | 适合折腾. |
|
||||
| MOV | 适合折腾. |
|
||||
| 自定义导出 | 除上述预设方案, 支持提供任意 FFmpeg 参数进行导出, 满足自定义复杂需求. |
|
||||
|
||||
## Spine 版本支持
|
||||
|
||||
@@ -41,8 +41,9 @@ namespace SpineRuntime21 {
|
||||
|
||||
public AnimationStateData Data { get { return data; } }
|
||||
public float TimeScale { get { return timeScale; } set { timeScale = value; } }
|
||||
public List<TrackEntry> Tracks => tracks;
|
||||
|
||||
public delegate void StartEndDelegate(AnimationState state, int trackIndex);
|
||||
public delegate void StartEndDelegate(AnimationState state, int trackIndex);
|
||||
public event StartEndDelegate Start;
|
||||
public event StartEndDelegate End;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ using System.Reflection;
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Specialized;
|
||||
using NLog;
|
||||
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
public partial class SpineListView : UserControl
|
||||
|
||||
@@ -120,7 +120,7 @@ namespace SpineViewer.Controls
|
||||
RenderWindow.Size = new((uint)sizeX, (uint)sizeY);
|
||||
|
||||
// 将 view 的大小设置成于 resolution 相同的大小, 其余属性都不变
|
||||
var view = RenderWindow.GetView();
|
||||
using var view = RenderWindow.GetView();
|
||||
var signX = Math.Sign(view.Size.X);
|
||||
var signY = Math.Sign(view.Size.Y);
|
||||
view.Size = new(value.Width * signX, value.Height * signY);
|
||||
@@ -140,12 +140,13 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
get
|
||||
{
|
||||
var center = RenderWindow.GetView().Center;
|
||||
using var view = RenderWindow.GetView();
|
||||
var center = view.Center;
|
||||
return new(center.X, center.Y);
|
||||
}
|
||||
set
|
||||
{
|
||||
var view = RenderWindow.GetView();
|
||||
using var view = RenderWindow.GetView();
|
||||
view.Center = new(value.X, value.Y);
|
||||
RenderWindow.SetView(view);
|
||||
}
|
||||
@@ -158,11 +159,15 @@ namespace SpineViewer.Controls
|
||||
[Browsable(false)]
|
||||
public float Zoom
|
||||
{
|
||||
get => resolution.Width / Math.Abs(RenderWindow.GetView().Size.X);
|
||||
get
|
||||
{
|
||||
using var view = RenderWindow.GetView();
|
||||
return resolution.Width / Math.Abs(view.Size.X);
|
||||
}
|
||||
set
|
||||
{
|
||||
value = Math.Clamp(value, ZOOM_MIN, ZOOM_MAX);
|
||||
var view = RenderWindow.GetView();
|
||||
using var view = RenderWindow.GetView();
|
||||
var signX = Math.Sign(view.Size.X);
|
||||
var signY = Math.Sign(view.Size.Y);
|
||||
view.Size = new(resolution.Width / value * signX, resolution.Height / value * signY);
|
||||
@@ -177,10 +182,14 @@ namespace SpineViewer.Controls
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
public float Rotation
|
||||
{
|
||||
get => RenderWindow.GetView().Rotation;
|
||||
get
|
||||
{
|
||||
using var view = RenderWindow.GetView();
|
||||
return view.Rotation;
|
||||
}
|
||||
set
|
||||
{
|
||||
var view = RenderWindow.GetView();
|
||||
using var view = RenderWindow.GetView();
|
||||
view.Rotation = value;
|
||||
RenderWindow.SetView(view);
|
||||
}
|
||||
@@ -193,10 +202,14 @@ namespace SpineViewer.Controls
|
||||
[Browsable(false)]
|
||||
public bool FlipX
|
||||
{
|
||||
get => RenderWindow.GetView().Size.X < 0;
|
||||
get
|
||||
{
|
||||
using var view = RenderWindow.GetView();
|
||||
return view.Size.X < 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
var view = RenderWindow.GetView();
|
||||
using var view = RenderWindow.GetView();
|
||||
var size = view.Size;
|
||||
if (size.X > 0 && value || size.X < 0 && !value)
|
||||
size.X *= -1;
|
||||
@@ -212,10 +225,14 @@ namespace SpineViewer.Controls
|
||||
[Browsable(false)]
|
||||
public bool FlipY
|
||||
{
|
||||
get => RenderWindow.GetView().Size.Y < 0;
|
||||
get
|
||||
{
|
||||
using var view = RenderWindow.GetView();
|
||||
return view.Size.Y < 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
var view = RenderWindow.GetView();
|
||||
using var view = RenderWindow.GetView();
|
||||
var size = view.Size;
|
||||
if (size.Y > 0 && value || size.Y < 0 && !value)
|
||||
size.Y *= -1;
|
||||
@@ -619,7 +636,7 @@ namespace SpineViewer.Controls
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
foreach (var spine in SpineListView.Spines)
|
||||
spine.Track0Animation = spine.Track0Animation; // TODO: 多轨道重置
|
||||
spine.ResetAnimationsTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -631,7 +648,7 @@ namespace SpineViewer.Controls
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
foreach (var spine in SpineListView.Spines)
|
||||
spine.Track0Animation = spine.Track0Animation; // TODO: 多轨道重置
|
||||
spine.ResetAnimationsTime();
|
||||
}
|
||||
}
|
||||
IsUpdating = true;
|
||||
|
||||
139
SpineViewer/Dialogs/AnimationTracksEditorDialog.Designer.cs
generated
Normal file
139
SpineViewer/Dialogs/AnimationTracksEditorDialog.Designer.cs
generated
Normal file
@@ -0,0 +1,139 @@
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
partial class AnimationTracksEditorDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
panel = new Panel();
|
||||
tableLayoutPanel1 = new TableLayoutPanel();
|
||||
flowLayoutPanel1 = new FlowLayoutPanel();
|
||||
button_Add = new Button();
|
||||
button_Delete = new Button();
|
||||
propertyGrid_AnimationTracks = new PropertyGrid();
|
||||
panel.SuspendLayout();
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
flowLayoutPanel1.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// panel
|
||||
//
|
||||
panel.Controls.Add(tableLayoutPanel1);
|
||||
panel.Dock = DockStyle.Fill;
|
||||
panel.Location = new Point(0, 0);
|
||||
panel.Name = "panel";
|
||||
panel.Padding = new Padding(50, 15, 50, 10);
|
||||
panel.Size = new Size(666, 483);
|
||||
panel.TabIndex = 0;
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
//
|
||||
tableLayoutPanel1.ColumnCount = 1;
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.Controls.Add(flowLayoutPanel1, 0, 1);
|
||||
tableLayoutPanel1.Controls.Add(propertyGrid_AnimationTracks, 0, 0);
|
||||
tableLayoutPanel1.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel1.Location = new Point(50, 15);
|
||||
tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||
tableLayoutPanel1.RowCount = 2;
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.Size = new Size(566, 458);
|
||||
tableLayoutPanel1.TabIndex = 0;
|
||||
//
|
||||
// flowLayoutPanel1
|
||||
//
|
||||
flowLayoutPanel1.AutoSize = true;
|
||||
flowLayoutPanel1.Controls.Add(button_Add);
|
||||
flowLayoutPanel1.Controls.Add(button_Delete);
|
||||
flowLayoutPanel1.Dock = DockStyle.Fill;
|
||||
flowLayoutPanel1.Location = new Point(3, 415);
|
||||
flowLayoutPanel1.Name = "flowLayoutPanel1";
|
||||
flowLayoutPanel1.Size = new Size(560, 40);
|
||||
flowLayoutPanel1.TabIndex = 2;
|
||||
//
|
||||
// button_Add
|
||||
//
|
||||
button_Add.Location = new Point(3, 3);
|
||||
button_Add.Name = "button_Add";
|
||||
button_Add.Size = new Size(112, 34);
|
||||
button_Add.TabIndex = 0;
|
||||
button_Add.Text = "添加";
|
||||
button_Add.UseVisualStyleBackColor = true;
|
||||
button_Add.Click += button_Add_Click;
|
||||
//
|
||||
// button_Delete
|
||||
//
|
||||
button_Delete.Location = new Point(121, 3);
|
||||
button_Delete.Name = "button_Delete";
|
||||
button_Delete.Size = new Size(112, 34);
|
||||
button_Delete.TabIndex = 1;
|
||||
button_Delete.Text = "删除";
|
||||
button_Delete.UseVisualStyleBackColor = true;
|
||||
button_Delete.Click += button_Delete_Click;
|
||||
//
|
||||
// propertyGrid_AnimationTracks
|
||||
//
|
||||
propertyGrid_AnimationTracks.Dock = DockStyle.Fill;
|
||||
propertyGrid_AnimationTracks.HelpVisible = false;
|
||||
propertyGrid_AnimationTracks.Location = new Point(3, 3);
|
||||
propertyGrid_AnimationTracks.Name = "propertyGrid_AnimationTracks";
|
||||
propertyGrid_AnimationTracks.PropertySort = PropertySort.NoSort;
|
||||
propertyGrid_AnimationTracks.Size = new Size(560, 406);
|
||||
propertyGrid_AnimationTracks.TabIndex = 1;
|
||||
propertyGrid_AnimationTracks.ToolbarVisible = false;
|
||||
//
|
||||
// AnimationTracksEditorDialog
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(666, 483);
|
||||
Controls.Add(panel);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "AnimationTracksEditorDialog";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
Text = "多轨道动画实时编辑器";
|
||||
panel.ResumeLayout(false);
|
||||
tableLayoutPanel1.ResumeLayout(false);
|
||||
tableLayoutPanel1.PerformLayout();
|
||||
flowLayoutPanel1.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Panel panel;
|
||||
private TableLayoutPanel tableLayoutPanel1;
|
||||
private FlowLayoutPanel flowLayoutPanel1;
|
||||
private Button button_Add;
|
||||
private Button button_Delete;
|
||||
private PropertyGrid propertyGrid_AnimationTracks;
|
||||
}
|
||||
}
|
||||
43
SpineViewer/Dialogs/AnimationTracksEditorDialog.cs
Normal file
43
SpineViewer/Dialogs/AnimationTracksEditorDialog.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using SpineViewer.Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
public partial class AnimationTracksEditorDialog : Form
|
||||
{
|
||||
private readonly Spine.Spine spine;
|
||||
public AnimationTracksEditorDialog(Spine.Spine spine)
|
||||
{
|
||||
InitializeComponent();
|
||||
this.spine = spine;
|
||||
propertyGrid_AnimationTracks.SelectedObject = spine.AnimationTracks;
|
||||
}
|
||||
|
||||
private void button_Add_Click(object sender, EventArgs e)
|
||||
{
|
||||
spine.SetAnimation(spine.GetTrackIndices().Max() + 1, spine.AnimationNames[0]);
|
||||
propertyGrid_AnimationTracks.Refresh();
|
||||
}
|
||||
|
||||
private void button_Delete_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (propertyGrid_AnimationTracks.SelectedGridItem?.Value is TrackWrapper tr)
|
||||
{
|
||||
if (tr.Index == 0)
|
||||
MessageBox.Info("必须保留轨道 0");
|
||||
else
|
||||
spine.ClearTrack(tr.Index);
|
||||
}
|
||||
propertyGrid_AnimationTracks.Refresh();
|
||||
propertyGrid_AnimationTracks.SelectedGridItem = propertyGrid_AnimationTracks.SelectedGridItem?.Parent?.GridItems?.Cast<GridItem>().Last();
|
||||
}
|
||||
}
|
||||
}
|
||||
120
SpineViewer/Dialogs/AnimationTracksEditorDialog.resx
Normal file
120
SpineViewer/Dialogs/AnimationTracksEditorDialog.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
139
SpineViewer/Dialogs/SkinManagerEditorDialog.Designer.cs
generated
Normal file
139
SpineViewer/Dialogs/SkinManagerEditorDialog.Designer.cs
generated
Normal file
@@ -0,0 +1,139 @@
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
partial class SkinManagerEditorDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
panel = new Panel();
|
||||
tableLayoutPanel1 = new TableLayoutPanel();
|
||||
flowLayoutPanel1 = new FlowLayoutPanel();
|
||||
button_Add = new Button();
|
||||
button_Delete = new Button();
|
||||
propertyGrid_SkinManager = new PropertyGrid();
|
||||
panel.SuspendLayout();
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
flowLayoutPanel1.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// panel
|
||||
//
|
||||
panel.Controls.Add(tableLayoutPanel1);
|
||||
panel.Dock = DockStyle.Fill;
|
||||
panel.Location = new Point(0, 0);
|
||||
panel.Name = "panel";
|
||||
panel.Padding = new Padding(50, 15, 50, 10);
|
||||
panel.Size = new Size(666, 483);
|
||||
panel.TabIndex = 0;
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
//
|
||||
tableLayoutPanel1.ColumnCount = 1;
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.Controls.Add(flowLayoutPanel1, 0, 1);
|
||||
tableLayoutPanel1.Controls.Add(propertyGrid_SkinManager, 0, 0);
|
||||
tableLayoutPanel1.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel1.Location = new Point(50, 15);
|
||||
tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||
tableLayoutPanel1.RowCount = 2;
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.Size = new Size(566, 458);
|
||||
tableLayoutPanel1.TabIndex = 0;
|
||||
//
|
||||
// flowLayoutPanel1
|
||||
//
|
||||
flowLayoutPanel1.AutoSize = true;
|
||||
flowLayoutPanel1.Controls.Add(button_Add);
|
||||
flowLayoutPanel1.Controls.Add(button_Delete);
|
||||
flowLayoutPanel1.Dock = DockStyle.Fill;
|
||||
flowLayoutPanel1.Location = new Point(3, 415);
|
||||
flowLayoutPanel1.Name = "flowLayoutPanel1";
|
||||
flowLayoutPanel1.Size = new Size(560, 40);
|
||||
flowLayoutPanel1.TabIndex = 2;
|
||||
//
|
||||
// button_Add
|
||||
//
|
||||
button_Add.Location = new Point(3, 3);
|
||||
button_Add.Name = "button_Add";
|
||||
button_Add.Size = new Size(112, 34);
|
||||
button_Add.TabIndex = 0;
|
||||
button_Add.Text = "添加";
|
||||
button_Add.UseVisualStyleBackColor = true;
|
||||
button_Add.Click += button_Add_Click;
|
||||
//
|
||||
// button_Delete
|
||||
//
|
||||
button_Delete.Location = new Point(121, 3);
|
||||
button_Delete.Name = "button_Delete";
|
||||
button_Delete.Size = new Size(112, 34);
|
||||
button_Delete.TabIndex = 1;
|
||||
button_Delete.Text = "删除";
|
||||
button_Delete.UseVisualStyleBackColor = true;
|
||||
button_Delete.Click += button_Delete_Click;
|
||||
//
|
||||
// propertyGrid_SkinManager
|
||||
//
|
||||
propertyGrid_SkinManager.Dock = DockStyle.Fill;
|
||||
propertyGrid_SkinManager.HelpVisible = false;
|
||||
propertyGrid_SkinManager.Location = new Point(3, 3);
|
||||
propertyGrid_SkinManager.Name = "propertyGrid_SkinManager";
|
||||
propertyGrid_SkinManager.PropertySort = PropertySort.NoSort;
|
||||
propertyGrid_SkinManager.Size = new Size(560, 406);
|
||||
propertyGrid_SkinManager.TabIndex = 1;
|
||||
propertyGrid_SkinManager.ToolbarVisible = false;
|
||||
//
|
||||
// SkinManagerEditorDialog
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(666, 483);
|
||||
Controls.Add(panel);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "SkinManagerEditorDialog";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
Text = "皮肤列表实时编辑器";
|
||||
panel.ResumeLayout(false);
|
||||
tableLayoutPanel1.ResumeLayout(false);
|
||||
tableLayoutPanel1.PerformLayout();
|
||||
flowLayoutPanel1.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Panel panel;
|
||||
private TableLayoutPanel tableLayoutPanel1;
|
||||
private FlowLayoutPanel flowLayoutPanel1;
|
||||
private Button button_Add;
|
||||
private Button button_Delete;
|
||||
private PropertyGrid propertyGrid_SkinManager;
|
||||
}
|
||||
}
|
||||
45
SpineViewer/Dialogs/SkinManagerEditorDialog.cs
Normal file
45
SpineViewer/Dialogs/SkinManagerEditorDialog.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using SpineViewer.Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
public partial class SkinManagerEditorDialog : Form
|
||||
{
|
||||
private readonly Spine.Spine spine;
|
||||
public SkinManagerEditorDialog(Spine.Spine spine)
|
||||
{
|
||||
InitializeComponent();
|
||||
this.spine = spine;
|
||||
propertyGrid_SkinManager.SelectedObject = spine.SkinManager;
|
||||
}
|
||||
|
||||
private void button_Add_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (spine.SkinNames.Count <= 0)
|
||||
{
|
||||
MessageBox.Info($"{spine.Name} 没有可用的皮肤");
|
||||
return;
|
||||
}
|
||||
spine.LoadSkin(spine.SkinNames[0]);
|
||||
propertyGrid_SkinManager.Refresh();
|
||||
}
|
||||
|
||||
private void button_Delete_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (propertyGrid_SkinManager.SelectedGridItem?.Value is SkinWrapper sk)
|
||||
spine.UnloadSkin(sk.Index);
|
||||
propertyGrid_SkinManager.Refresh();
|
||||
|
||||
if (propertyGrid_SkinManager.SelectedGridItem?.Parent?.GridItems?.Cast<GridItem>().Last() is GridItem gt)
|
||||
propertyGrid_SkinManager.SelectedGridItem = gt;
|
||||
}
|
||||
}
|
||||
}
|
||||
120
SpineViewer/Dialogs/SkinManagerEditorDialog.resx
Normal file
120
SpineViewer/Dialogs/SkinManagerEditorDialog.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -8,13 +8,14 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出参数基类
|
||||
/// </summary>
|
||||
public abstract class ExportArgs : ImplementationResolver<ExportArgs, ExportImplementationAttribute, ExportType>
|
||||
public abstract class ExportArgs : ImplementationResolver<ExportArgs, ExportImplementationAttribute, ExportType>, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建指定类型导出参数
|
||||
@@ -34,6 +35,10 @@ namespace SpineViewer.Exporter
|
||||
RenderSelectedOnly = renderSelectedOnly;
|
||||
}
|
||||
|
||||
~ExportArgs() { Dispose(false); }
|
||||
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
|
||||
protected virtual void Dispose(bool disposing) { View?.Dispose(); }
|
||||
|
||||
/// <summary>
|
||||
/// 输出文件夹
|
||||
/// </summary>
|
||||
@@ -72,7 +77,27 @@ namespace SpineViewer.Exporter
|
||||
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
|
||||
[TypeConverter(typeof(SFMLColorConverter))]
|
||||
[Category("[0] 导出"), DisplayName("背景颜色"), Description("要使用的背景色, 格式为 #RRGGBBAA")]
|
||||
public SFML.Graphics.Color BackgroundColor { get; set; } = SFML.Graphics.Color.Transparent;
|
||||
public SFML.Graphics.Color BackgroundColor
|
||||
{
|
||||
get => backgroundColor;
|
||||
set
|
||||
{
|
||||
backgroundColor = value;
|
||||
var bcPma = value;
|
||||
var a = bcPma.A / 255f;
|
||||
bcPma.R = (byte)(bcPma.R * a);
|
||||
bcPma.G = (byte)(bcPma.G * a);
|
||||
bcPma.B = (byte)(bcPma.B * a);
|
||||
BackgroundColorPma = bcPma;
|
||||
}
|
||||
}
|
||||
private SFML.Graphics.Color backgroundColor = SFML.Graphics.Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 预乘后的背景颜色
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public SFML.Graphics.Color BackgroundColorPma { get; private set; } = SFML.Graphics.Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace SpineViewer.Exporter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SFML.Graphics.Image 帧对象包装类
|
||||
/// SFML.Graphics.Image 帧对象包装类, 将接管给定的 image 对象生命周期
|
||||
/// </summary>
|
||||
public class SFMLImageVideoFrame(SFML.Graphics.Image image) : IVideoFrame, IDisposable
|
||||
{
|
||||
@@ -64,12 +64,7 @@ namespace SpineViewer.Exporter
|
||||
/// <summary>
|
||||
/// 获取 Winforms Bitmap 对象
|
||||
/// </summary>
|
||||
public Bitmap CopyToBitmap()
|
||||
{
|
||||
image.SaveToMemory(out var imgBuffer, "bmp");
|
||||
using var stream = new MemoryStream(imgBuffer);
|
||||
return new(new Bitmap(stream)); // 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
|
||||
}
|
||||
public Bitmap CopyToBitmap() => image.CopyToBitmap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -86,51 +81,5 @@ namespace SpineViewer.Exporter
|
||||
else if (imageFormat == ImageFormat.Exif) return ".jpeg";
|
||||
else return $".{imageFormat.ToString().ToLower()}";
|
||||
}
|
||||
|
||||
#region 包围盒辅助函数
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个包围盒下合适的视图
|
||||
/// </summary>
|
||||
public static SFML.Graphics.View GetView(this RectangleF bounds, Size resolution, Padding padding)
|
||||
=> bounds.GetView((uint)resolution.Width, (uint)resolution.Height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom);
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个包围盒下合适的视图
|
||||
/// </summary>
|
||||
public static SFML.Graphics.View GetView(this RectangleF bounds, uint width, uint height, Padding padding)
|
||||
=> bounds.GetView(width, height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom);
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个包围盒下合适的视图
|
||||
/// </summary>
|
||||
public static SFML.Graphics.View GetView(this RectangleF bounds, Size resolution, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1)
|
||||
=> bounds.GetView((uint)resolution.Width, (uint)resolution.Height, paddingL, paddingR, paddingT, paddingB);
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个包围盒下合适的视图
|
||||
/// </summary>
|
||||
public static SFML.Graphics.View GetView(this RectangleF bounds, uint width, uint height, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1)
|
||||
{
|
||||
float sizeX = bounds.Width;
|
||||
float sizeY = bounds.Height;
|
||||
float innerW = width - paddingL - paddingR;
|
||||
float innerH = height - paddingT - paddingB;
|
||||
|
||||
float scale = 1;
|
||||
if (sizeY / sizeX < innerH / innerW)
|
||||
scale = sizeX / innerW; // 相同的 X, 视窗 Y 更大
|
||||
else
|
||||
scale = sizeY / innerH; // 相同的 Y, 视窗 X 更大
|
||||
|
||||
var x = bounds.X + bounds.Width / 2 + (paddingL - (float)paddingR) * scale;
|
||||
var y = bounds.Y + bounds.Height / 2 + (paddingT - (float)paddingB) * scale;
|
||||
var viewX = width * scale;
|
||||
var viewY = height * scale;
|
||||
|
||||
return new(new(x, y), new(viewX, -viewY));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using NLog;
|
||||
using SpineViewer.Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -52,27 +51,54 @@ namespace SpineViewer.Exporter
|
||||
/// <summary>
|
||||
/// 获取单个模型的单帧画面
|
||||
/// </summary>
|
||||
protected SFMLImageVideoFrame GetFrame(Spine.Spine spine)
|
||||
{
|
||||
// tex 必须临时创建, 随用随取, 防止出现跨线程的情况
|
||||
using var tex = GetRenderTexture();
|
||||
tex.Clear(ExportArgs.BackgroundColor);
|
||||
tex.Draw(spine);
|
||||
tex.Display();
|
||||
return new(tex.Texture.CopyToImage());
|
||||
}
|
||||
protected SFMLImageVideoFrame GetFrame(Spine.Spine spine) => GetFrame([spine]);
|
||||
|
||||
/// <summary>
|
||||
/// 获取模型列表的单帧画面
|
||||
/// </summary>
|
||||
protected SFMLImageVideoFrame GetFrame(Spine.Spine[] spinesToRender)
|
||||
{
|
||||
// tex 必须临时创建, 随用随取, 防止出现跨线程的情况
|
||||
using var tex = GetRenderTexture();
|
||||
tex.Clear(ExportArgs.BackgroundColor);
|
||||
foreach (var spine in spinesToRender) tex.Draw(spine);
|
||||
tex.Display();
|
||||
return new(tex.Texture.CopyToImage());
|
||||
// RenderTexture 必须临时创建, 随用随取, 防止出现跨线程的情况
|
||||
using var texPma = GetRenderTexture();
|
||||
|
||||
// 先将预乘结果准确绘制出来, 注意背景色也应当是预乘的
|
||||
texPma.Clear(ExportArgs.BackgroundColorPma);
|
||||
foreach (var spine in spinesToRender) texPma.Draw(spine);
|
||||
texPma.Display();
|
||||
|
||||
// 背景色透明度不为 1 时需要处理反预乘, 否则直接就是结果
|
||||
if (ExportArgs.BackgroundColor.A < 255)
|
||||
{
|
||||
// 从预乘结果构造渲染对象, 并正确设置变换
|
||||
using var view = texPma.GetView();
|
||||
using var img = texPma.Texture.CopyToImage();
|
||||
using var texSprite = new SFML.Graphics.Texture(img);
|
||||
using var sp = new SFML.Graphics.Sprite(texSprite)
|
||||
{
|
||||
Origin = new(texPma.Size.X / 2f, texPma.Size.Y / 2f),
|
||||
Position = new(view.Center.X, view.Center.Y),
|
||||
Scale = new(view.Size.X / texPma.Size.X, view.Size.Y / texPma.Size.Y),
|
||||
Rotation = view.Rotation
|
||||
};
|
||||
|
||||
// 混合模式用直接覆盖的方式, 保证得到的图像区域是反预乘的颜色和透明度, 同时使用反预乘着色器
|
||||
var st = SFML.Graphics.RenderStates.Default;
|
||||
st.BlendMode = new(SFML.Graphics.BlendMode.Factor.One, SFML.Graphics.BlendMode.Factor.Zero); // 用源的颜色和透明度直接覆盖
|
||||
st.Shader = Shader.InversePma;
|
||||
|
||||
// 在最终结果上二次渲染非预乘画面
|
||||
using var tex = GetRenderTexture();
|
||||
|
||||
// 将非预乘结果覆盖式绘制在目标对象上, 注意背景色应该用非预乘的
|
||||
tex.Clear(ExportArgs.BackgroundColor);
|
||||
tex.Draw(sp, st);
|
||||
tex.Display();
|
||||
return new(tex.Texture.CopyToImage());
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(texPma.Texture.CopyToImage());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using FFMpegCore.Enums;
|
||||
using FFMpegCore;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
/// <summary>
|
||||
/// 文件格式
|
||||
/// </summary>
|
||||
[Category("[2] FFmpeg 基本参数"), DisplayName("文件格式"), Description("文件格式")]
|
||||
[Category("[2] FFmpeg 基本参数"), DisplayName("文件格式"), Description("-f, 文件格式")]
|
||||
public abstract string Format { get; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -16,9 +16,6 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
public GifExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
|
||||
{
|
||||
// 给一个纯白的背景
|
||||
BackgroundColor = new(255, 255, 255, 0);
|
||||
|
||||
// GIF 的帧率不能太高, 超过 50 帧反而会变慢
|
||||
FPS = 12;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
public MkvExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
|
||||
{
|
||||
BackgroundColor = new(0, 255, 0, 0);
|
||||
BackgroundColor = new(0, 255, 0);
|
||||
}
|
||||
|
||||
public override string Format => "matroska";
|
||||
@@ -28,7 +28,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("libx264", "libx265", "libvpx-vp9", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("编码器"), Description("要使用的编码器")]
|
||||
[Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
|
||||
public string Codec { get; set; } = "libx265";
|
||||
|
||||
/// <summary>
|
||||
@@ -43,7 +43,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("像素格式"), Description("要使用的像素格式")]
|
||||
[Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")]
|
||||
public string PixelFormat { get; set; } = "yuv444p";
|
||||
|
||||
public override void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
public MovExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
|
||||
{
|
||||
BackgroundColor = new(0, 255, 0, 0);
|
||||
BackgroundColor = new(0, 255, 0);
|
||||
}
|
||||
|
||||
public override string Format => "mov";
|
||||
@@ -28,13 +28,13 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("prores_ks", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("编码器"), Description("要使用的编码器")]
|
||||
[Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
|
||||
public string Codec { get; set; } = "prores_ks";
|
||||
|
||||
/// <summary>
|
||||
/// 预设
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("auto", "proxy", "lt", "standard", "hq", "4444", "444xq")]
|
||||
[StringEnumConverter.StandardValues("auto", "proxy", "lt", "standard", "hq", "4444", "4444xq")]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("预设"), Description("-profile, 预设配置")]
|
||||
public string Profile { get; set; } = "auto";
|
||||
@@ -44,7 +44,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("yuv422p10le", "yuv444p10le", "yuva444p10le", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("像素格式"), Description("要使用的像素格式")]
|
||||
[Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")]
|
||||
public string PixelFormat { get; set; } = "yuva444p10le";
|
||||
|
||||
public override void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
public Mp4ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
|
||||
{
|
||||
BackgroundColor = new(0, 255, 0, 0);
|
||||
BackgroundColor = new(0, 255, 0);
|
||||
}
|
||||
|
||||
public override string Format => "mp4";
|
||||
@@ -28,7 +28,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("libx264", "libx265", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("编码器"), Description("要使用的编码器")]
|
||||
[Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
|
||||
public string Codec { get; set; } = "libx264";
|
||||
|
||||
/// <summary>
|
||||
@@ -43,7 +43,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("像素格式"), Description("要使用的像素格式")]
|
||||
[Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")]
|
||||
public string PixelFormat { get; set; } = "yuv444p";
|
||||
|
||||
public override void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using FFMpegCore.Enums;
|
||||
using FFMpegCore;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
@@ -19,7 +17,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
/// <summary>
|
||||
/// 导出时长
|
||||
/// </summary>
|
||||
[Category("[1] 视频参数"), DisplayName("时长"), Description("可以从模型列表查看动画时长, 如果小于 0, 则在逐个导出时每个模型使用各自的当前动画时长")]
|
||||
[Category("[1] 视频参数"), DisplayName("时长"), Description("可以从模型列表查看动画时长, 如果小于 0, 则在逐个导出时每个模型使用各自的所有轨道动画时长最大值")]
|
||||
public float Duration
|
||||
{
|
||||
get => duration;
|
||||
|
||||
@@ -29,13 +29,13 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("libvpx-vp9", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("编码器"), Description("要使用的编码器")]
|
||||
[Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
|
||||
public string Codec { get; set; } = "libvpx-vp9";
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
[Category("[3] 格式参数"), DisplayName("CRF"), Description("Constant Rate Factor, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")]
|
||||
[Category("[3] 格式参数"), DisplayName("CRF"), Description("-crf, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")]
|
||||
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
|
||||
private int crf = 23;
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("像素格式"), Description("要使用的像素格式")]
|
||||
[Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")]
|
||||
public string PixelFormat { get; set; } = "yuva420p";
|
||||
|
||||
public override void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FFMpegCore.Arguments;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
|
||||
@@ -23,9 +23,9 @@ namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
{
|
||||
var args = (VideoExportArgs)ExportArgs;
|
||||
|
||||
// 独立导出时如果 args.Duration 小于 0 则使用 Track0 的动画时长
|
||||
// 独立导出时如果 args.Duration 小于 0 则使用所有轨道上动画时长最大值
|
||||
var duration = args.Duration;
|
||||
if (duration < 0) duration = spine.GetAnimationDuration(spine.Track0Animation); // TODO: 也许可以使用所有轨道的最大值
|
||||
if (duration < 0) duration = spine.GetTrackIndices().Select(i => spine.GetAnimationDuration(spine.GetAnimation(i))).Max();
|
||||
|
||||
float delta = 1f / args.FPS;
|
||||
int total = Math.Max(1, (int)(duration * args.FPS)); // 至少导出 1 帧
|
||||
@@ -75,7 +75,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
public override void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
|
||||
{
|
||||
// 导出视频格式需要把模型时间都重置到 0
|
||||
foreach (var spine in spines) spine.Track0Animation = spine.Track0Animation; // TODO: 多轨道重置
|
||||
foreach (var spine in spines) spine.ResetAnimationsTime();
|
||||
base.Export(spines, worker);
|
||||
}
|
||||
}
|
||||
|
||||
66
SpineViewer/MainForm.Designer.cs
generated
66
SpineViewer/MainForm.Designer.cs
generated
@@ -39,10 +39,10 @@
|
||||
toolStripMenuItem_ExportFrame = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportFrameSequence = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportGif = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportMkv = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportMp4 = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportMov = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportWebm = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportMkv = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportMov = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportCustom = new ToolStripMenuItem();
|
||||
toolStripSeparator2 = new ToolStripSeparator();
|
||||
toolStripMenuItem_Exit = new ToolStripMenuItem();
|
||||
@@ -115,27 +115,27 @@
|
||||
//
|
||||
toolStripMenuItem_Open.Name = "toolStripMenuItem_Open";
|
||||
toolStripMenuItem_Open.ShortcutKeys = Keys.Control | Keys.O;
|
||||
toolStripMenuItem_Open.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Open.Size = new Size(254, 34);
|
||||
toolStripMenuItem_Open.Text = "打开(&O)...";
|
||||
toolStripMenuItem_Open.Click += toolStripMenuItem_Open_Click;
|
||||
//
|
||||
// toolStripMenuItem_BatchOpen
|
||||
//
|
||||
toolStripMenuItem_BatchOpen.Name = "toolStripMenuItem_BatchOpen";
|
||||
toolStripMenuItem_BatchOpen.Size = new Size(270, 34);
|
||||
toolStripMenuItem_BatchOpen.Size = new Size(254, 34);
|
||||
toolStripMenuItem_BatchOpen.Text = "批量打开(&B)...";
|
||||
toolStripMenuItem_BatchOpen.Click += toolStripMenuItem_BatchOpen_Click;
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
toolStripSeparator1.Size = new Size(267, 6);
|
||||
toolStripSeparator1.Size = new Size(251, 6);
|
||||
//
|
||||
// toolStripMenuItem_Export
|
||||
//
|
||||
toolStripMenuItem_Export.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ExportFrame, toolStripMenuItem_ExportFrameSequence, toolStripMenuItem_ExportGif, toolStripMenuItem_ExportMp4, toolStripMenuItem_ExportWebm, toolStripMenuItem_ExportMkv, toolStripMenuItem_ExportMov, toolStripMenuItem_ExportCustom });
|
||||
toolStripMenuItem_Export.Name = "toolStripMenuItem_Export";
|
||||
toolStripMenuItem_Export.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Export.Size = new Size(254, 34);
|
||||
toolStripMenuItem_Export.Text = "导出(&E)";
|
||||
//
|
||||
// toolStripMenuItem_ExportFrame
|
||||
@@ -159,13 +159,6 @@
|
||||
toolStripMenuItem_ExportGif.Text = "GIF...";
|
||||
toolStripMenuItem_ExportGif.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportMkv
|
||||
//
|
||||
toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv";
|
||||
toolStripMenuItem_ExportMkv.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportMkv.Text = "MKV...";
|
||||
toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportMp4
|
||||
//
|
||||
toolStripMenuItem_ExportMp4.Name = "toolStripMenuItem_ExportMp4";
|
||||
@@ -173,13 +166,6 @@
|
||||
toolStripMenuItem_ExportMp4.Text = "MP4...";
|
||||
toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportMov
|
||||
//
|
||||
toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov";
|
||||
toolStripMenuItem_ExportMov.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportMov.Text = "MOV...";
|
||||
toolStripMenuItem_ExportMov.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportWebm
|
||||
//
|
||||
toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm";
|
||||
@@ -187,6 +173,20 @@
|
||||
toolStripMenuItem_ExportWebm.Text = "WebM...";
|
||||
toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportMkv
|
||||
//
|
||||
toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv";
|
||||
toolStripMenuItem_ExportMkv.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportMkv.Text = "MKV...";
|
||||
toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportMov
|
||||
//
|
||||
toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov";
|
||||
toolStripMenuItem_ExportMov.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportMov.Text = "MOV...";
|
||||
toolStripMenuItem_ExportMov.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportCustom
|
||||
//
|
||||
toolStripMenuItem_ExportCustom.Name = "toolStripMenuItem_ExportCustom";
|
||||
@@ -197,13 +197,13 @@
|
||||
// toolStripSeparator2
|
||||
//
|
||||
toolStripSeparator2.Name = "toolStripSeparator2";
|
||||
toolStripSeparator2.Size = new Size(267, 6);
|
||||
toolStripSeparator2.Size = new Size(251, 6);
|
||||
//
|
||||
// toolStripMenuItem_Exit
|
||||
//
|
||||
toolStripMenuItem_Exit.Name = "toolStripMenuItem_Exit";
|
||||
toolStripMenuItem_Exit.ShortcutKeys = Keys.Alt | Keys.F4;
|
||||
toolStripMenuItem_Exit.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Exit.Size = new Size(254, 34);
|
||||
toolStripMenuItem_Exit.Text = "退出(&X)";
|
||||
toolStripMenuItem_Exit.Click += toolStripMenuItem_Exit_Click;
|
||||
//
|
||||
@@ -271,7 +271,7 @@
|
||||
rtbLog.Margin = new Padding(3, 2, 3, 2);
|
||||
rtbLog.Name = "rtbLog";
|
||||
rtbLog.ReadOnly = true;
|
||||
rtbLog.Size = new Size(1758, 146);
|
||||
rtbLog.Size = new Size(1758, 120);
|
||||
rtbLog.TabIndex = 0;
|
||||
rtbLog.Text = "";
|
||||
rtbLog.WordWrap = false;
|
||||
@@ -295,7 +295,7 @@
|
||||
splitContainer_MainForm.Panel2.Controls.Add(rtbLog);
|
||||
splitContainer_MainForm.Panel2.Cursor = Cursors.Default;
|
||||
splitContainer_MainForm.Size = new Size(1758, 1097);
|
||||
splitContainer_MainForm.SplitterDistance = 943;
|
||||
splitContainer_MainForm.SplitterDistance = 969;
|
||||
splitContainer_MainForm.SplitterWidth = 8;
|
||||
splitContainer_MainForm.TabIndex = 3;
|
||||
splitContainer_MainForm.TabStop = false;
|
||||
@@ -319,7 +319,7 @@
|
||||
//
|
||||
splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview);
|
||||
splitContainer_Functional.Panel2.Cursor = Cursors.Default;
|
||||
splitContainer_Functional.Size = new Size(1758, 943);
|
||||
splitContainer_Functional.Size = new Size(1758, 969);
|
||||
splitContainer_Functional.SplitterDistance = 759;
|
||||
splitContainer_Functional.SplitterWidth = 8;
|
||||
splitContainer_Functional.TabIndex = 2;
|
||||
@@ -343,7 +343,7 @@
|
||||
//
|
||||
splitContainer_Information.Panel2.Controls.Add(splitContainer_Config);
|
||||
splitContainer_Information.Panel2.Cursor = Cursors.Default;
|
||||
splitContainer_Information.Size = new Size(759, 943);
|
||||
splitContainer_Information.Size = new Size(759, 969);
|
||||
splitContainer_Information.SplitterDistance = 354;
|
||||
splitContainer_Information.SplitterWidth = 8;
|
||||
splitContainer_Information.TabIndex = 1;
|
||||
@@ -357,7 +357,7 @@
|
||||
groupBox_SkelList.Dock = DockStyle.Fill;
|
||||
groupBox_SkelList.Location = new Point(0, 0);
|
||||
groupBox_SkelList.Name = "groupBox_SkelList";
|
||||
groupBox_SkelList.Size = new Size(354, 943);
|
||||
groupBox_SkelList.Size = new Size(354, 969);
|
||||
groupBox_SkelList.TabIndex = 0;
|
||||
groupBox_SkelList.TabStop = false;
|
||||
groupBox_SkelList.Text = "模型列表";
|
||||
@@ -368,7 +368,7 @@
|
||||
spineListView.Location = new Point(3, 26);
|
||||
spineListView.Name = "spineListView";
|
||||
spineListView.PropertyGrid = propertyGrid_Spine;
|
||||
spineListView.Size = new Size(348, 914);
|
||||
spineListView.Size = new Size(348, 940);
|
||||
spineListView.TabIndex = 0;
|
||||
//
|
||||
// propertyGrid_Spine
|
||||
@@ -377,7 +377,7 @@
|
||||
propertyGrid_Spine.HelpVisible = false;
|
||||
propertyGrid_Spine.Location = new Point(3, 26);
|
||||
propertyGrid_Spine.Name = "propertyGrid_Spine";
|
||||
propertyGrid_Spine.Size = new Size(391, 580);
|
||||
propertyGrid_Spine.Size = new Size(391, 606);
|
||||
propertyGrid_Spine.TabIndex = 0;
|
||||
propertyGrid_Spine.ToolbarVisible = false;
|
||||
propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged;
|
||||
@@ -400,7 +400,7 @@
|
||||
//
|
||||
splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig);
|
||||
splitContainer_Config.Panel2.Cursor = Cursors.Default;
|
||||
splitContainer_Config.Size = new Size(397, 943);
|
||||
splitContainer_Config.Size = new Size(397, 969);
|
||||
splitContainer_Config.SplitterDistance = 326;
|
||||
splitContainer_Config.SplitterWidth = 8;
|
||||
splitContainer_Config.TabIndex = 0;
|
||||
@@ -436,7 +436,7 @@
|
||||
groupBox_SkelConfig.Dock = DockStyle.Fill;
|
||||
groupBox_SkelConfig.Location = new Point(0, 0);
|
||||
groupBox_SkelConfig.Name = "groupBox_SkelConfig";
|
||||
groupBox_SkelConfig.Size = new Size(397, 609);
|
||||
groupBox_SkelConfig.Size = new Size(397, 635);
|
||||
groupBox_SkelConfig.TabIndex = 0;
|
||||
groupBox_SkelConfig.TabStop = false;
|
||||
groupBox_SkelConfig.Text = "模型参数";
|
||||
@@ -447,7 +447,7 @@
|
||||
groupBox_Preview.Dock = DockStyle.Fill;
|
||||
groupBox_Preview.Location = new Point(0, 0);
|
||||
groupBox_Preview.Name = "groupBox_Preview";
|
||||
groupBox_Preview.Size = new Size(991, 943);
|
||||
groupBox_Preview.Size = new Size(991, 969);
|
||||
groupBox_Preview.TabIndex = 1;
|
||||
groupBox_Preview.TabStop = false;
|
||||
groupBox_Preview.Text = "预览画面";
|
||||
@@ -458,7 +458,7 @@
|
||||
spinePreviewer.Location = new Point(3, 26);
|
||||
spinePreviewer.Name = "spinePreviewer";
|
||||
spinePreviewer.PropertyGrid = propertyGrid_Previewer;
|
||||
spinePreviewer.Size = new Size(985, 914);
|
||||
spinePreviewer.Size = new Size(985, 940);
|
||||
spinePreviewer.SpineListView = spineListView;
|
||||
spinePreviewer.TabIndex = 0;
|
||||
//
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
using FFMpegCore.Pipes;
|
||||
using FFMpegCore;
|
||||
using NLog;
|
||||
using SFML.System;
|
||||
using NLog;
|
||||
using SpineViewer.Spine;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using FFMpegCore.Enums;
|
||||
using SpineViewer.Exporter;
|
||||
|
||||
namespace SpineViewer
|
||||
@@ -34,7 +28,7 @@ namespace SpineViewer
|
||||
// 执行一些初始化工作
|
||||
try
|
||||
{
|
||||
Spine.Shader.Init();
|
||||
Shader.Init();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -133,9 +127,47 @@ namespace SpineViewer
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
//private System.Windows.Forms.Timer timer = new();
|
||||
//private PetForm pet = new PetForm();
|
||||
//private IntPtr screenDC;
|
||||
//private IntPtr memDC;
|
||||
private void toolStripMenuItem_ManageResource_Click(object sender, EventArgs e)
|
||||
{
|
||||
// screenDC = Win32.GetDC(IntPtr.Zero);
|
||||
// memDC = Win32.CreateCompatibleDC(screenDC);
|
||||
// pet.Show();
|
||||
// timer.Tick += Timer_Tick;
|
||||
// timer.Enabled = true;
|
||||
// timer.Interval = 50;
|
||||
// timer.Start();
|
||||
//}
|
||||
|
||||
//private void Timer_Tick(object? sender, EventArgs e)
|
||||
//{
|
||||
// using var tex = new SFML.Graphics.RenderTexture((uint)pet.Width, (uint)pet.Height);
|
||||
// var v = spinePreviewer.GetView();
|
||||
// tex.SetView(v);
|
||||
// tex.Clear(new SFML.Graphics.Color(0, 0, 0, 0));
|
||||
// lock (spineListView.Spines)
|
||||
// {
|
||||
// foreach (var sp in spineListView.Spines)
|
||||
// tex.Draw(sp);
|
||||
// }
|
||||
// tex.Display();
|
||||
// using var frame = new SFMLImageVideoFrame(tex.Texture.CopyToImage());
|
||||
// using var bitmap = frame.CopyToBitmap();
|
||||
|
||||
// var newBitmap = bitmap.GetHbitmap(Color.FromArgb(0));
|
||||
// var oldBitmap = Win32.SelectObject(memDC, newBitmap);
|
||||
|
||||
// Win32.SIZE size = new Win32.SIZE { cx = pet.Width, cy = pet.Height };
|
||||
// Win32.POINT srcPos = new Win32.POINT { x = 0, y = 0 };
|
||||
// Win32.BLENDFUNCTION blend = new Win32.BLENDFUNCTION { BlendOp = 0, BlendFlags = 0, SourceConstantAlpha = 255, AlphaFormat = Win32.AC_SRC_ALPHA };
|
||||
|
||||
// Win32.UpdateLayeredWindow(pet.Handle, screenDC, IntPtr.Zero, ref size, memDC, ref srcPos, 0, ref blend, Win32.ULW_ALPHA);
|
||||
|
||||
// Win32.SelectObject(memDC, oldBitmap);
|
||||
// Win32.DeleteObject(newBitmap);
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_About_Click(object sender, EventArgs e)
|
||||
|
||||
73
SpineViewer/SFMLExtension.cs
Normal file
73
SpineViewer/SFMLExtension.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
public static class SFMLExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取 Winforms Bitmap 对象, 需要使用 Dispose 释放对象
|
||||
/// </summary>
|
||||
public static Bitmap CopyToBitmap(this SFML.Graphics.Image image)
|
||||
{
|
||||
image.SaveToMemory(out var imgBuffer, "bmp");
|
||||
using var stream = new MemoryStream(imgBuffer);
|
||||
using var bitmap = new Bitmap(stream);
|
||||
return new(bitmap); // 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Winforms Bitmap 对象, 需要使用 Dispose 释放对象
|
||||
/// </summary>
|
||||
public static Bitmap CopyToBitmap(this SFML.Graphics.Texture texture)
|
||||
{
|
||||
using var image = texture.CopyToImage();
|
||||
return CopyToBitmap(image);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个包围盒下合适的视图
|
||||
/// </summary>
|
||||
public static SFML.Graphics.View GetView(this RectangleF bounds, Size resolution, Padding padding)
|
||||
=> bounds.GetView((uint)resolution.Width, (uint)resolution.Height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom);
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个包围盒下合适的视图
|
||||
/// </summary>
|
||||
public static SFML.Graphics.View GetView(this RectangleF bounds, uint width, uint height, Padding padding)
|
||||
=> bounds.GetView(width, height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom);
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个包围盒下合适的视图
|
||||
/// </summary>
|
||||
public static SFML.Graphics.View GetView(this RectangleF bounds, Size resolution, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1)
|
||||
=> bounds.GetView((uint)resolution.Width, (uint)resolution.Height, paddingL, paddingR, paddingT, paddingB);
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个包围盒下合适的视图
|
||||
/// </summary>
|
||||
public static SFML.Graphics.View GetView(this RectangleF bounds, uint width, uint height, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1)
|
||||
{
|
||||
float sizeX = bounds.Width;
|
||||
float sizeY = bounds.Height;
|
||||
float innerW = width - paddingL - paddingR;
|
||||
float innerH = height - paddingT - paddingB;
|
||||
|
||||
float scale = 1;
|
||||
if (sizeY / sizeX < innerH / innerW)
|
||||
scale = sizeX / innerW; // 相同的 X, 视窗 Y 更大
|
||||
else
|
||||
scale = sizeY / innerH; // 相同的 Y, 视窗 X 更大
|
||||
|
||||
var x = bounds.X + bounds.Width / 2 + (paddingL - (float)paddingR) * scale;
|
||||
var y = bounds.Y + bounds.Height / 2 + (paddingT - (float)paddingB) * scale;
|
||||
var viewX = width * scale;
|
||||
var viewY = height * scale;
|
||||
|
||||
return new(new(x, y), new(viewX, -viewY));
|
||||
}
|
||||
}
|
||||
}
|
||||
80
SpineViewer/Shader.cs
Normal file
80
SpineViewer/Shader.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
public static class Shader
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于非预乘纹理的 fragment shader, 乘上了插值后的透明度用于实现透明度变化(插值预乘), 并且输出预乘后的像素值
|
||||
/// </summary>
|
||||
private const string FRAGMENT_VertexAlpha =
|
||||
"uniform sampler2D t;" +
|
||||
"void main() { vec4 p = texture(t, gl_TexCoord[0].xy);" +
|
||||
"p.rgb *= p.a * gl_Color.a;" +
|
||||
"gl_FragColor = gl_Color * p; }"
|
||||
;
|
||||
|
||||
/// <summary>
|
||||
/// 用于预乘纹理的 fragment shader, 乘上了插值后的透明度用于实现透明度变化(插值预乘)
|
||||
/// </summary>
|
||||
private const string FRAGMENT_VertexAlphaPma =
|
||||
"uniform sampler2D t;" +
|
||||
"void main() { vec4 p = texture(t, gl_TexCoord[0].xy);" +
|
||||
"p.rgb *= gl_Color.a;" +
|
||||
"gl_FragColor = gl_Color * p; }"
|
||||
;
|
||||
|
||||
/// <summary>
|
||||
/// 预乘转非预乘 fragment shader
|
||||
/// </summary>
|
||||
private const string FRAGMENT_InvPma =
|
||||
"uniform sampler2D t;" +
|
||||
"void main() { vec4 p = texture(t, gl_TexCoord[0].xy);" +
|
||||
"if (p.a > 0) p.rgb /= max(max(max(p.r, p.g), p.b), p.a);" +
|
||||
"gl_FragColor = p; }"
|
||||
;
|
||||
|
||||
/// <summary>
|
||||
/// 考虑了顶点透明度变化的着色器, 输入是非预乘纹理像素, 输出是预乘像素
|
||||
/// </summary>
|
||||
private static SFML.Graphics.Shader? VertexAlpha = null;
|
||||
|
||||
/// <summary>
|
||||
/// 考虑了顶点透明度变化的着色器, 输入和输出均是预乘像素值
|
||||
/// </summary>
|
||||
private static SFML.Graphics.Shader? VertexAlphaPma = null;
|
||||
|
||||
/// <summary>
|
||||
/// 反预乘着色器, 用于得到正确透明度的非预乘画面
|
||||
/// </summary>
|
||||
public static SFML.Graphics.Shader? InversePma { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 加载 Shader, 可能会存在异常导致着色器加载失败
|
||||
/// </summary>
|
||||
/// <exception cref="SFML.LoadingFailedException"></exception>
|
||||
public static void Init()
|
||||
{
|
||||
VertexAlpha = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_VertexAlpha);
|
||||
VertexAlphaPma = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_VertexAlphaPma);
|
||||
InversePma = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_InvPma);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取绘制 Spine 的着色器
|
||||
/// </summary>
|
||||
/// <param name="pma">纹理是否是预乘的</param>
|
||||
/// <param name="twoColor">是否是双色着色的(TODO)</param>
|
||||
public static SFML.Graphics.Shader? GetSpineShader(bool pma, bool twoColor = false)
|
||||
{
|
||||
if (pma)
|
||||
return VertexAlphaPma;
|
||||
else
|
||||
return VertexAlpha;
|
||||
}
|
||||
}
|
||||
}
|
||||
124
SpineViewer/Spine/AnimationTracks.cs
Normal file
124
SpineViewer/Spine/AnimationTracks.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// 对轨道索引的包装类, 能够在面板上显示例如时长的属性, 但是处理该属性时按字符串去处理, 例如 ToString 和判断对象相等都是用动画名称实现逻辑
|
||||
/// </summary>
|
||||
/// <param name="spine"></param>
|
||||
/// <param name="i"></param>
|
||||
[TypeConverter(typeof(TrackWrapperConverter))]
|
||||
public class TrackWrapper(Spine spine, int i)
|
||||
{
|
||||
private readonly Spine spine = spine;
|
||||
|
||||
[Browsable(false)]
|
||||
public int Index { get; } = i;
|
||||
|
||||
[DisplayName("时长")]
|
||||
public float Duration => spine.GetAnimationDuration(spine.GetAnimation(Index));
|
||||
|
||||
/// <summary>
|
||||
/// 实现了默认的转为字符串的方式
|
||||
/// </summary>
|
||||
public override string ToString() => spine.GetAnimation(Index);
|
||||
|
||||
/// <summary>
|
||||
/// 影响了属性面板的判断, 当动画名称相同的时候认为两个对象是相同的, 这样属性面板可以在多选的时候正确显示相同取值的内容
|
||||
/// </summary>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is TrackWrapper) return ToString() == obj.ToString();
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 哈希码需要和 Equals 行为类似
|
||||
/// </summary>
|
||||
public override int GetHashCode() => (typeof(TrackWrapper).FullName + ToString()).GetHashCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轨道属性描述符, 实现对属性的读取和赋值
|
||||
/// </summary>
|
||||
/// <param name="spine">关联的 Spine 对象</param>
|
||||
/// <param name="i">轨道索引</param>
|
||||
public class TrackWrapperPropertyDescriptor(Spine spine, int i) : PropertyDescriptor($"Track{i}", [new DisplayNameAttribute($"轨道 {i}")])
|
||||
{
|
||||
private readonly Spine spine = spine;
|
||||
private readonly int idx = i;
|
||||
|
||||
public override Type ComponentType => typeof(AnimationTracksType);
|
||||
public override bool IsReadOnly => false;
|
||||
public override Type PropertyType => typeof(TrackWrapper);
|
||||
public override bool CanResetValue(object component) => false;
|
||||
public override void ResetValue(object component) { }
|
||||
public override bool ShouldSerializeValue(object component) => false;
|
||||
|
||||
/// <summary>
|
||||
/// 得到一个轨道包装类, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
|
||||
/// </summary>
|
||||
public override object? GetValue(object? component) => new TrackWrapper(spine, idx);
|
||||
|
||||
/// <summary>
|
||||
/// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理
|
||||
/// </summary>
|
||||
public override void SetValue(object? component, object? value)
|
||||
{
|
||||
if (value is string s) spine.SetAnimation(idx, s);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AnimationTracks 动态类型包装类, 用于提供对 Spine 对象多轨道动画的访问能力, 不同轨道将动态生成属性
|
||||
/// </summary>
|
||||
/// <param name="spine">关联的 Spine 对象</param>
|
||||
public class AnimationTracksType(Spine spine) : ICustomTypeDescriptor
|
||||
{
|
||||
private readonly Dictionary<int, TrackWrapperPropertyDescriptor> pdCache = [];
|
||||
public Spine Spine { get; } = spine;
|
||||
|
||||
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
|
||||
|
||||
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
|
||||
public string? GetClassName() => TypeDescriptor.GetClassName(this, true);
|
||||
public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true);
|
||||
public TypeConverter? GetConverter() => TypeDescriptor.GetConverter(this, true);
|
||||
public EventDescriptor? GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true);
|
||||
public PropertyDescriptor? GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true);
|
||||
public object? GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true);
|
||||
public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(this, true);
|
||||
public EventDescriptorCollection GetEvents(Attribute[]? attributes) => TypeDescriptor.GetEvents(this, attributes, true);
|
||||
public object? GetPropertyOwner(PropertyDescriptor? pd) => this;
|
||||
public PropertyDescriptorCollection GetProperties() => GetProperties(null);
|
||||
public PropertyDescriptorCollection GetProperties(Attribute[]? attributes)
|
||||
{
|
||||
var props = new List<TrackWrapperPropertyDescriptor>();
|
||||
foreach (var i in Spine.GetTrackIndices())
|
||||
{
|
||||
if (!pdCache.ContainsKey(i))
|
||||
pdCache[i] = new TrackWrapperPropertyDescriptor(Spine, i);
|
||||
props.Add(pdCache[i]);
|
||||
}
|
||||
return new PropertyDescriptorCollection(props.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在属性面板悬停可以按轨道顺序显示动画名称
|
||||
/// </summary>
|
||||
public override string ToString() => $"[{string.Join(", ", Spine.GetTrackIndices().Select(Spine.GetAnimation))}]";
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is AnimationTracksType tracks) return ToString() == tracks.ToString();
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => (typeof(AnimationTracksType).FullName + ToString()).GetHashCode();
|
||||
}
|
||||
}
|
||||
@@ -7,36 +7,86 @@ using System.Threading.Tasks;
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// SFML 混合模式
|
||||
/// SFML 混合模式, 预乘模式下输入和输出的像素值都是预乘的
|
||||
/// </summary>
|
||||
public static class BlendModeSFML
|
||||
{
|
||||
/// <summary>
|
||||
/// Alpha Blend
|
||||
/// <code>
|
||||
/// res.c = src.c * src.a + dst.c * (1 - src.a)
|
||||
/// res.a = src.a * 1 + dst.a * (1 - src.a)
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public static SFML.Graphics.BlendMode Normal = SFML.Graphics.BlendMode.Alpha;
|
||||
///// <summary>
|
||||
///// Normal Blend, 无预乘, 仅在 dst.a 是 1 时得到正确结果, 其余情况是有偏结果
|
||||
///// <para>当 <c>src.c < dst.c</c> 时, 结果偏大, 例如 src 是半透明纯黑, dst 是全透明纯白</para>
|
||||
///// <para>当 <c>src.c > dst.c</c> 时, 结果偏小, 例如 src 是半透明纯白, dst 是全透明纯黑</para>
|
||||
///// <code>
|
||||
///// res.c = src.c * src.a + dst.c * (1 - src.a)
|
||||
///// res.a = src.a * 1 + dst.a * (1 - src.a)
|
||||
///// </code>
|
||||
///// </summary>
|
||||
//public static readonly SFML.Graphics.BlendMode Normal = new(
|
||||
// SFML.Graphics.BlendMode.Factor.SrcAlpha,
|
||||
// SFML.Graphics.BlendMode.Factor.OneMinusSrcAlpha,
|
||||
// SFML.Graphics.BlendMode.Equation.Add,
|
||||
// SFML.Graphics.BlendMode.Factor.One,
|
||||
// SFML.Graphics.BlendMode.Factor.OneMinusSrcAlpha,
|
||||
// SFML.Graphics.BlendMode.Equation.Add
|
||||
//);
|
||||
|
||||
///// <summary>
|
||||
///// Additive Blend, 无预乘, 仅在 dst.a 是 1 时得到正确结果, 其余情况是有偏结果
|
||||
///// <para>当 <c>src.a + dst.a >= 1</c> 时, 结果偏大, 例如 src 是不透明纯黑, dst 是全透明纯白</para>
|
||||
///// <para>当 <c>src.a + dst.a < 1</c> 时, 结果偏差方式类似 <see cref="Normal"/>, 均可假设 dst 是全透明纯白进行判断</para>
|
||||
///// <code>
|
||||
///// res.c = src.c * src.a + dst.c * 1
|
||||
///// res.a = src.a * 1 + dst.a * 1
|
||||
///// </code>
|
||||
///// </summary>
|
||||
//public static readonly SFML.Graphics.BlendMode Additive = new(
|
||||
// SFML.Graphics.BlendMode.Factor.SrcAlpha,
|
||||
// SFML.Graphics.BlendMode.Factor.One,
|
||||
// SFML.Graphics.BlendMode.Equation.Add,
|
||||
// SFML.Graphics.BlendMode.Factor.One,
|
||||
// SFML.Graphics.BlendMode.Factor.One,
|
||||
// SFML.Graphics.BlendMode.Equation.Add
|
||||
//);
|
||||
|
||||
/// <summary>
|
||||
/// Additive Blend
|
||||
/// Normal Blend with PremultipliedAlpha
|
||||
/// <code>
|
||||
/// res.c = src.c * src.a + dst.c * 1
|
||||
/// res.a = src.a * 1 + dst.a * 1
|
||||
/// [res.c * res.a] = [src.c * src.a] * 1 + [dst.c * dst.a] * (1 - src.a)
|
||||
/// res.a = src.a * 1 + dst.a * (1 - src.a)
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public static SFML.Graphics.BlendMode Additive = SFML.Graphics.BlendMode.Add;
|
||||
public static readonly SFML.Graphics.BlendMode NormalPma = new(
|
||||
SFML.Graphics.BlendMode.Factor.One,
|
||||
SFML.Graphics.BlendMode.Factor.OneMinusSrcAlpha,
|
||||
SFML.Graphics.BlendMode.Equation.Add,
|
||||
SFML.Graphics.BlendMode.Factor.One,
|
||||
SFML.Graphics.BlendMode.Factor.OneMinusSrcAlpha,
|
||||
SFML.Graphics.BlendMode.Equation.Add
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Multiply Blend (PremultipliedAlpha Only)
|
||||
/// Additive Blend with PremultipliedAlpha
|
||||
/// <code>
|
||||
/// [res.c * res.a] = [src.c * src.a] * 1 + [dst.c * dst.a] * 1
|
||||
/// res.a = src.a * 1 + dst.a * 1
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public static readonly SFML.Graphics.BlendMode AdditivePma = new(
|
||||
SFML.Graphics.BlendMode.Factor.One,
|
||||
SFML.Graphics.BlendMode.Factor.One,
|
||||
SFML.Graphics.BlendMode.Equation.Add,
|
||||
SFML.Graphics.BlendMode.Factor.One,
|
||||
SFML.Graphics.BlendMode.Factor.One,
|
||||
SFML.Graphics.BlendMode.Equation.Add
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Multiply Blend with PremultipliedAlpha
|
||||
/// <code>
|
||||
/// res.c = src.c * dst.c + dst.c * (1 - src.a)
|
||||
/// res.a = src.a * 1 + dst.a * (1 - src.a)
|
||||
/// res.a = src.a * 1 + dst.a * (1 - src.a)
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public static SFML.Graphics.BlendMode Multiply = new(
|
||||
public static readonly SFML.Graphics.BlendMode MultiplyPma = new(
|
||||
SFML.Graphics.BlendMode.Factor.DstColor,
|
||||
SFML.Graphics.BlendMode.Factor.OneMinusSrcAlpha,
|
||||
SFML.Graphics.BlendMode.Equation.Add,
|
||||
@@ -46,13 +96,13 @@ namespace SpineViewer.Spine
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Screen Blend (PremultipliedAlpha Only)
|
||||
/// Screen Blend with PremultipliedAlpha Only
|
||||
/// <code>
|
||||
/// res.c = src.c * 1 + dst.c * (1 - src.c) = 1 - [(1 - src.c)(1 - dst.c)]
|
||||
/// res.a = src.a * 1 + dst.a * (1 - src.a)
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public static SFML.Graphics.BlendMode Screen = new(
|
||||
public static readonly SFML.Graphics.BlendMode ScreenPma = new(
|
||||
SFML.Graphics.BlendMode.Factor.One,
|
||||
SFML.Graphics.BlendMode.Factor.OneMinusSrcColor,
|
||||
SFML.Graphics.BlendMode.Equation.Add,
|
||||
|
||||
@@ -109,8 +109,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
var pos = position;
|
||||
var fX = flipX;
|
||||
var fY = flipY;
|
||||
var animation = track0Animation; // TODO: 适配多轨道
|
||||
var sk = skin;
|
||||
var animations = animationState.Tracks.Where(te => te is not null).Select(te => te.Animation.Name).ToArray();
|
||||
|
||||
var val = Math.Max(value, SCALE_MIN);
|
||||
if (skeletonBinary is not null)
|
||||
@@ -133,8 +132,8 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
position = pos;
|
||||
flipX = fX;
|
||||
flipY = fY;
|
||||
track0Animation = animation; // TODO: 适配多轨道
|
||||
skin = sk;
|
||||
foreach (var s in loadedSkins) addSkin(s);
|
||||
for (int i = 0; i < animations.Length; i++) setAnimation(i, animations[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,29 +159,35 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
set => skeleton.FlipY = value;
|
||||
}
|
||||
|
||||
protected override string skin
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
if (!skinNames.Contains(name)) return;
|
||||
skeleton.SetSkin(name); // XXX: 3.7 及以下不支持 AddSkin
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override string track0Animation
|
||||
protected override void clearSkin()
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
set
|
||||
{
|
||||
if (value == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(0, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(value))
|
||||
animationState.SetAnimation(0, value, true);
|
||||
}
|
||||
skeleton.SetSkin(skeletonData.DefaultSkin);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF bounds
|
||||
{
|
||||
get
|
||||
@@ -233,8 +238,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override void update(float delta)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
@@ -259,6 +262,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
vertexArray.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
@@ -320,18 +324,13 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
|
||||
// 似乎 2.1.x 也没有 BlendMode
|
||||
SFML.Graphics.BlendMode blendMode = slot.Data.AdditiveBlending ? BlendModeSFML.Additive : BlendModeSFML.Normal;
|
||||
SFML.Graphics.BlendMode blendMode = slot.Data.AdditiveBlending ? BlendModeSFML.AdditivePma : BlendModeSFML.NormalPma;
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
@@ -375,11 +374,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
//clipping.ClipEnd(slot);
|
||||
}
|
||||
|
||||
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
//clipping.ClipEnd();
|
||||
|
||||
// 调试纹理
|
||||
|
||||
@@ -108,8 +108,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
var pos = position;
|
||||
var fX = flipX;
|
||||
var fY = flipY;
|
||||
var animation = track0Animation; // TODO: 适配多轨道
|
||||
var sk = skin;
|
||||
var animations = animationState.Tracks.Where(te => te is not null).Select(te => te.Animation.Name).ToArray();
|
||||
|
||||
var val = Math.Max(value, SCALE_MIN);
|
||||
if (skeletonBinary is not null)
|
||||
@@ -132,8 +131,8 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
position = pos;
|
||||
flipX = fX;
|
||||
flipY = fY;
|
||||
track0Animation = animation; // TODO: 适配多轨道
|
||||
skin = sk;
|
||||
foreach (var s in loadedSkins) addSkin(s);
|
||||
for (int i = 0; i < animations.Length; i++) setAnimation(i, animations[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,29 +158,35 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
set => skeleton.FlipY = value;
|
||||
}
|
||||
|
||||
protected override string skin
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
if (!skinNames.Contains(name)) return;
|
||||
skeleton.SetSkin(name); // XXX: 3.7 及以下不支持 AddSkin
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override string track0Animation
|
||||
protected override void clearSkin()
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
set
|
||||
{
|
||||
if (value == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(0, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(value))
|
||||
animationState.SetAnimation(0, value, true);
|
||||
}
|
||||
skeleton.SetSkin(skeletonData.DefaultSkin);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF bounds
|
||||
{
|
||||
get
|
||||
@@ -192,8 +197,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override void update(float delta)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
@@ -206,10 +209,10 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
return spineBlendMode switch
|
||||
{
|
||||
BlendMode.Normal => BlendModeSFML.Normal,
|
||||
BlendMode.Additive => BlendModeSFML.Additive,
|
||||
BlendMode.Multiply => BlendModeSFML.Multiply,
|
||||
BlendMode.Screen => BlendModeSFML.Screen,
|
||||
BlendMode.Normal => BlendModeSFML.NormalPma,
|
||||
BlendMode.Additive => BlendModeSFML.AdditivePma,
|
||||
BlendMode.Multiply => BlendModeSFML.MultiplyPma,
|
||||
BlendMode.Screen => BlendModeSFML.ScreenPma,
|
||||
_ => throw new NotImplementedException($"{spineBlendMode}"),
|
||||
};
|
||||
}
|
||||
@@ -218,6 +221,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
vertexArray.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
@@ -284,11 +288,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
@@ -334,11 +333,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
@@ -129,29 +129,35 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
protected override string skin
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
if (!skinNames.Contains(name)) return;
|
||||
skeleton.SetSkin(name); // XXX: 3.7 及以下不支持 AddSkin
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override string track0Animation
|
||||
protected override void clearSkin()
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
set
|
||||
{
|
||||
if (value == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(0, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(value))
|
||||
animationState.SetAnimation(0, value, true);
|
||||
}
|
||||
skeleton.SetSkin(skeletonData.DefaultSkin);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF bounds
|
||||
{
|
||||
get
|
||||
@@ -162,8 +168,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override void update(float delta)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
@@ -176,10 +180,10 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
return spineBlendMode switch
|
||||
{
|
||||
BlendMode.Normal => BlendModeSFML.Normal,
|
||||
BlendMode.Additive => BlendModeSFML.Additive,
|
||||
BlendMode.Multiply => BlendModeSFML.Multiply,
|
||||
BlendMode.Screen => BlendModeSFML.Screen,
|
||||
BlendMode.Normal => BlendModeSFML.NormalPma,
|
||||
BlendMode.Additive => BlendModeSFML.AdditivePma,
|
||||
BlendMode.Multiply => BlendModeSFML.MultiplyPma,
|
||||
BlendMode.Screen => BlendModeSFML.ScreenPma,
|
||||
_ => throw new NotImplementedException($"{spineBlendMode}"),
|
||||
};
|
||||
}
|
||||
@@ -188,6 +192,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
vertexArray.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
@@ -254,12 +259,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
// XXX: 实测不用设置 sampler2D 的值也正确
|
||||
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
@@ -305,11 +304,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
@@ -135,29 +135,35 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
protected override string skin
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
if (!skinNames.Contains(name)) return;
|
||||
skeleton.Skin.AddSkin(skeletonData.FindSkin(name));
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override string track0Animation
|
||||
protected override void clearSkin()
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
set
|
||||
{
|
||||
if (value == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(0, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(value))
|
||||
animationState.SetAnimation(0, value, true);
|
||||
}
|
||||
skeleton.Skin.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF bounds
|
||||
{
|
||||
get
|
||||
@@ -168,8 +174,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override void update(float delta)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
@@ -182,10 +186,10 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
return spineBlendMode switch
|
||||
{
|
||||
BlendMode.Normal => BlendModeSFML.Normal,
|
||||
BlendMode.Additive => BlendModeSFML.Additive,
|
||||
BlendMode.Multiply => BlendModeSFML.Multiply,
|
||||
BlendMode.Screen => BlendModeSFML.Screen,
|
||||
BlendMode.Normal => BlendModeSFML.NormalPma,
|
||||
BlendMode.Additive => BlendModeSFML.AdditivePma,
|
||||
BlendMode.Multiply => BlendModeSFML.MultiplyPma,
|
||||
BlendMode.Screen => BlendModeSFML.ScreenPma,
|
||||
_ => throw new NotImplementedException($"{spineBlendMode}"),
|
||||
};
|
||||
}
|
||||
@@ -194,6 +198,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
vertexArray.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
@@ -260,14 +265,8 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
// XXX: 实测不用设置 sampler2D 的值也正确
|
||||
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
vertexArray.Clear();
|
||||
@@ -311,11 +310,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
@@ -131,29 +131,35 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
protected override string skin
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
if (!skinNames.Contains(name)) return;
|
||||
skeleton.Skin.AddSkin(skeletonData.FindSkin(name));
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override string track0Animation
|
||||
protected override void clearSkin()
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
set
|
||||
{
|
||||
if (value == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(0, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(value))
|
||||
animationState.SetAnimation(0, value, true);
|
||||
}
|
||||
skeleton.Skin.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF bounds
|
||||
{
|
||||
get
|
||||
@@ -164,8 +170,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override void update(float delta)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
@@ -178,10 +182,10 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
return spineBlendMode switch
|
||||
{
|
||||
BlendMode.Normal => BlendModeSFML.Normal,
|
||||
BlendMode.Additive => BlendModeSFML.Additive,
|
||||
BlendMode.Multiply => BlendModeSFML.Multiply,
|
||||
BlendMode.Screen => BlendModeSFML.Screen,
|
||||
BlendMode.Normal => BlendModeSFML.NormalPma,
|
||||
BlendMode.Additive => BlendModeSFML.AdditivePma,
|
||||
BlendMode.Multiply => BlendModeSFML.MultiplyPma,
|
||||
BlendMode.Screen => BlendModeSFML.ScreenPma,
|
||||
_ => throw new NotImplementedException($"{spineBlendMode}"),
|
||||
};
|
||||
}
|
||||
@@ -190,6 +194,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
vertexArray.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
@@ -256,12 +261,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
// XXX: 实测不用设置 sampler2D 的值也正确
|
||||
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
@@ -307,11 +306,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
@@ -131,29 +131,35 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
protected override string skin
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
if (!skinNames.Contains(name)) return;
|
||||
skeleton.Skin.AddSkin(skeletonData.FindSkin(name));
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override string track0Animation
|
||||
protected override void clearSkin()
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
set
|
||||
{
|
||||
if (value == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(0, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(value))
|
||||
animationState.SetAnimation(0, value, true);
|
||||
}
|
||||
skeleton.Skin.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF bounds
|
||||
{
|
||||
get
|
||||
@@ -164,8 +170,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override void update(float delta)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
@@ -178,10 +182,10 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
return spineBlendMode switch
|
||||
{
|
||||
BlendMode.Normal => BlendModeSFML.Normal,
|
||||
BlendMode.Additive => BlendModeSFML.Additive,
|
||||
BlendMode.Multiply => BlendModeSFML.Multiply,
|
||||
BlendMode.Screen => BlendModeSFML.Screen,
|
||||
BlendMode.Normal => BlendModeSFML.NormalPma,
|
||||
BlendMode.Additive => BlendModeSFML.AdditivePma,
|
||||
BlendMode.Multiply => BlendModeSFML.MultiplyPma,
|
||||
BlendMode.Screen => BlendModeSFML.ScreenPma,
|
||||
_ => throw new NotImplementedException($"{spineBlendMode}"),
|
||||
};
|
||||
}
|
||||
@@ -190,6 +194,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
vertexArray.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
@@ -256,12 +261,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
// XXX: 实测不用设置 sampler2D 的值也正确
|
||||
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
@@ -307,11 +306,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
@@ -131,29 +131,35 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
protected override string skin
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
if (!skinNames.Contains(name)) return;
|
||||
skeleton.Skin.AddSkin(skeletonData.FindSkin(name));
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override string track0Animation
|
||||
protected override void clearSkin()
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
set
|
||||
{
|
||||
if (value == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(0, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(value))
|
||||
animationState.SetAnimation(0, value, true);
|
||||
}
|
||||
skeleton.Skin.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF bounds
|
||||
{
|
||||
get
|
||||
@@ -164,8 +170,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override void update(float delta)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
@@ -178,10 +182,10 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
return spineBlendMode switch
|
||||
{
|
||||
BlendMode.Normal => BlendModeSFML.Normal,
|
||||
BlendMode.Additive => BlendModeSFML.Additive,
|
||||
BlendMode.Multiply => BlendModeSFML.Multiply,
|
||||
BlendMode.Screen => BlendModeSFML.Screen,
|
||||
BlendMode.Normal => BlendModeSFML.NormalPma,
|
||||
BlendMode.Additive => BlendModeSFML.AdditivePma,
|
||||
BlendMode.Multiply => BlendModeSFML.MultiplyPma,
|
||||
BlendMode.Screen => BlendModeSFML.ScreenPma,
|
||||
_ => throw new NotImplementedException($"{spineBlendMode}"),
|
||||
};
|
||||
}
|
||||
@@ -190,6 +194,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
vertexArray.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
@@ -256,12 +261,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
// XXX: 实测不用设置 sampler2D 的值也正确
|
||||
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
@@ -307,11 +306,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
public static class Shader
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于解决 PMA 和渐变动画问题的片段着色器
|
||||
/// </summary>
|
||||
private const string FRAGMENT_SHADER = (
|
||||
"uniform sampler2D t;" +
|
||||
"void main() { vec4 p = texture2D(t, gl_TexCoord[0].xy);" +
|
||||
"if (p.a > 0) p.rgb /= max(max(max(p.r, p.g), p.b), p.a);" +
|
||||
"gl_FragColor = gl_Color * p; }"
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 针对预乘 Alpha 通道的片段着色器
|
||||
/// </summary>
|
||||
public static SFML.Graphics.Shader? FragmentShader { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 加载 Shader, 可能会存在异常导致着色器加载失败
|
||||
/// </summary>
|
||||
/// <exception cref="SFML.LoadingFailedException"></exception>
|
||||
public static void Init()
|
||||
{
|
||||
FragmentShader = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_SHADER);
|
||||
}
|
||||
}
|
||||
}
|
||||
115
SpineViewer/Spine/SkinManager.cs
Normal file
115
SpineViewer/Spine/SkinManager.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// 对皮肤的包装类
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SkinWrapperConverter))]
|
||||
public class SkinWrapper(Spine spine, int i)
|
||||
{
|
||||
private readonly Spine spine = spine;
|
||||
|
||||
[Browsable(false)]
|
||||
public int Index { get; } = i;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var loadedSkins = spine.GetLoadedSkins();
|
||||
if (Index >= 0 && Index < loadedSkins.Length)
|
||||
return loadedSkins[Index];
|
||||
return "!NULL"; // XXX: 预期应该不会发生
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is SkinWrapper) return ToString() == obj.ToString();
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => (typeof(SkinWrapper).FullName + ToString()).GetHashCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 皮肤属性描述符, 实现对属性的读取和赋值
|
||||
/// </summary>
|
||||
/// <param name="spine">关联的 Spine 对象</param>
|
||||
public class SkinWrapperPropertyDescriptor(Spine spine, int i) : PropertyDescriptor($"Skin{i}", [new DisplayNameAttribute($"皮肤 {i}")])
|
||||
{
|
||||
private readonly Spine spine = spine;
|
||||
private readonly int idx = i;
|
||||
|
||||
public override Type ComponentType => typeof(SkinManager);
|
||||
public override bool IsReadOnly => false;
|
||||
public override Type PropertyType => typeof(SkinWrapper);
|
||||
public override bool CanResetValue(object component) => false;
|
||||
public override void ResetValue(object component) { }
|
||||
public override bool ShouldSerializeValue(object component) => false;
|
||||
|
||||
/// <summary>
|
||||
/// 得到一个 SkinWrapper, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
|
||||
/// </summary>
|
||||
public override object? GetValue(object? component) => new SkinWrapper(spine, idx);
|
||||
|
||||
/// <summary>
|
||||
/// 允许通过字符串赋值修改该位置的皮肤
|
||||
/// </summary>
|
||||
public override void SetValue(object? component, object? value)
|
||||
{
|
||||
if (value is string s) spine.ReplaceSkin(idx, s);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SkinManager 动态类型包装类, 用于提供对 Spine 皮肤列表的管理能力
|
||||
/// </summary>
|
||||
/// <param name="spine">关联的 Spine 对象</param>
|
||||
public class SkinManager(Spine spine) : ICustomTypeDescriptor
|
||||
{
|
||||
private readonly Dictionary<int, SkinWrapperPropertyDescriptor> pdCache = [];
|
||||
public Spine Spine { get; } = spine;
|
||||
|
||||
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
|
||||
|
||||
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
|
||||
public string? GetClassName() => TypeDescriptor.GetClassName(this, true);
|
||||
public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true);
|
||||
public TypeConverter? GetConverter() => TypeDescriptor.GetConverter(this, true);
|
||||
public EventDescriptor? GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true);
|
||||
public PropertyDescriptor? GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true);
|
||||
public object? GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true);
|
||||
public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(this, true);
|
||||
public EventDescriptorCollection GetEvents(Attribute[]? attributes) => TypeDescriptor.GetEvents(this, attributes, true);
|
||||
public object? GetPropertyOwner(PropertyDescriptor? pd) => this;
|
||||
public PropertyDescriptorCollection GetProperties() => GetProperties(null);
|
||||
public PropertyDescriptorCollection GetProperties(Attribute[]? attributes)
|
||||
{
|
||||
var props = new List<SkinWrapperPropertyDescriptor>();
|
||||
for (var i = 0; i < Spine.GetLoadedSkins().Length; i++)
|
||||
{
|
||||
if (!pdCache.ContainsKey(i))
|
||||
pdCache[i] = new SkinWrapperPropertyDescriptor(Spine, i);
|
||||
props.Add(pdCache[i]);
|
||||
}
|
||||
return new PropertyDescriptorCollection(props.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在属性面板悬停可以显示已加载的皮肤列表
|
||||
/// </summary>
|
||||
public override string ToString() => $"[{string.Join(", ", Spine.GetLoadedSkins())}]";
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is SkinManager manager) return ToString() == manager.ToString();
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => (typeof(SkinManager).FullName + ToString()).GetHashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Numerics;
|
||||
using System.Collections;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Collections.Immutable;
|
||||
using SpineViewer.Exporter;
|
||||
using System.Drawing.Design;
|
||||
using NLog;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
@@ -23,6 +12,9 @@ namespace SpineViewer.Spine
|
||||
/// </summary>
|
||||
public abstract class Spine : ImplementationResolver<Spine, SpineImplementationAttribute, SpineVersion>, SFML.Graphics.Drawable, IDisposable
|
||||
{
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private bool skinLoggerWarned = false;
|
||||
|
||||
/// <summary>
|
||||
/// 空动画标记
|
||||
/// </summary>
|
||||
@@ -76,7 +68,9 @@ namespace SpineViewer.Spine
|
||||
private Spine PostInit()
|
||||
{
|
||||
SkinNames = skinNames.AsReadOnly();
|
||||
SkinManager = new(this);
|
||||
AnimationNames = animationNames.AsReadOnly();
|
||||
AnimationTracks = new(this);
|
||||
|
||||
// 必须 Update 一次否则包围盒还没有值
|
||||
update(0);
|
||||
@@ -87,25 +81,15 @@ namespace SpineViewer.Spine
|
||||
// 除此之外, 似乎还和 tex 的 Dispose 有关
|
||||
// 如果不对 tex 进行 Dispose, 那么不管是否 Draw 都正常不会死锁
|
||||
var tex = new SFML.Graphics.RenderTexture(PREVIEW_WIDTH, PREVIEW_HEIGHT);
|
||||
tex.SetView(bounds.GetView(PREVIEW_WIDTH, PREVIEW_HEIGHT));
|
||||
using var view = bounds.GetView(PREVIEW_WIDTH, PREVIEW_HEIGHT);
|
||||
tex.SetView(view);
|
||||
tex.Clear(SFML.Graphics.Color.Transparent);
|
||||
tex.Draw(this);
|
||||
tex.Display();
|
||||
|
||||
using (var img = tex.Texture.CopyToImage())
|
||||
{
|
||||
if (img.SaveToMemory(out var imgBuffer, "bmp"))
|
||||
{
|
||||
// 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
|
||||
using var stream = new MemoryStream(imgBuffer);
|
||||
using var bitmap = new Bitmap(stream);
|
||||
Preview = new Bitmap(bitmap);
|
||||
}
|
||||
}
|
||||
Preview = tex.Texture.CopyToBitmap();
|
||||
|
||||
// 取最后一个作为初始, 尽可能去显示非默认的内容
|
||||
skin = SkinNames.Last();
|
||||
track0Animation = AnimationNames.Last();
|
||||
setAnimation(0, AnimationNames.Last());
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -177,7 +161,7 @@ namespace SpineViewer.Spine
|
||||
get { lock (_lock) return usePremultipliedAlpha; }
|
||||
set { lock (_lock) usePremultipliedAlpha = value; }
|
||||
}
|
||||
protected bool usePremultipliedAlpha = true;
|
||||
protected bool usePremultipliedAlpha = false;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -199,7 +183,7 @@ namespace SpineViewer.Spine
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(PointFConverter))]
|
||||
[Category("[2] 变换"), DisplayName("位置")]
|
||||
public PointF Position
|
||||
public PointF Position
|
||||
{
|
||||
get { lock (_lock) return position; }
|
||||
set { lock (_lock) { position = value; update(0); } }
|
||||
@@ -232,49 +216,164 @@ namespace SpineViewer.Spine
|
||||
|
||||
#region 属性 | [3] 动画
|
||||
|
||||
/// <summary>
|
||||
/// 已加载皮肤列表
|
||||
/// </summary>
|
||||
[Editor(typeof(SkinManagerEditor), typeof(UITypeEditor))]
|
||||
[TypeConverter(typeof(ExpandableObjectConverter))]
|
||||
[Category("[3] 动画"), DisplayName("已加载皮肤列表")]
|
||||
public SkinManager SkinManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认轨道动画名称, 如果设置的动画不存在则忽略
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(AnimationConverter))]
|
||||
[Category("[3] 动画"), DisplayName("轨道 0 动画")]
|
||||
public string Track0Animation
|
||||
{
|
||||
get { lock (_lock) return getAnimation(0); }
|
||||
set { lock (_lock) { setAnimation(0, value); update(0); } }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 默认轨道动画时长
|
||||
/// </summary>
|
||||
[Category("[3] 动画"), DisplayName("轨道 0 动画时长")]
|
||||
public float Track0AnimationDuration => GetAnimationDuration(Track0Animation);
|
||||
|
||||
/// <summary>
|
||||
/// 默认轨道动画时长
|
||||
/// </summary>
|
||||
[Editor(typeof(AnimationTracksEditor), typeof(UITypeEditor))]
|
||||
[TypeConverter(typeof(ExpandableObjectConverter))]
|
||||
[Category("[3] 动画"), DisplayName("多轨道动画管理")]
|
||||
public AnimationTracksType AnimationTracks { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 包含的所有皮肤名称
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public ReadOnlyCollection<string> SkinNames { get; private set; }
|
||||
protected List<string> skinNames = [];
|
||||
protected readonly List<string> skinNames = [];
|
||||
|
||||
/// <summary>
|
||||
/// 使用的皮肤名称, 如果设置的皮肤不存在则忽略
|
||||
/// 获取已加载的皮肤列表快照, 允许出现重复值
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SkinConverter))]
|
||||
[Category("[3] 动画"), DisplayName("皮肤")]
|
||||
public string Skin
|
||||
public string[] GetLoadedSkins() { lock (_lock) return loadedSkins.ToArray(); }
|
||||
protected readonly List<string> loadedSkins = [];
|
||||
|
||||
/// <summary>
|
||||
/// 加载指定皮肤, 添加至列表末尾, 如果不存在则忽略, 允许加载重复的值
|
||||
/// </summary>
|
||||
public void LoadSkin(string name)
|
||||
{
|
||||
get { lock (_lock) return skin; }
|
||||
set { lock (_lock) { skin = value; update(0); } }
|
||||
lock (_lock)
|
||||
{
|
||||
if (skinNames.Contains(name))
|
||||
{
|
||||
loadedSkins.Add(name);
|
||||
reloadSkins();
|
||||
|
||||
if (!skinLoggerWarned && Version <= SpineVersion.V37 && loadedSkins.Count > 1)
|
||||
{
|
||||
logger.Warn($"Multiplt skins not supported in SpineVersion {Version.GetName()}");
|
||||
skinLoggerWarned = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
protected abstract string skin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 卸载列表指定位置皮肤, 如果超出范围则忽略
|
||||
/// </summary>
|
||||
public void UnloadSkin(int idx)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (idx >= 0 && idx < loadedSkins.Count)
|
||||
{
|
||||
loadedSkins.RemoveAt(idx);
|
||||
reloadSkins();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 替换皮肤列表指定位置皮肤, 超出范围或者皮肤不存在则忽略
|
||||
/// </summary>
|
||||
public void ReplaceSkin(int idx, string name)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (idx >= 0 && idx < loadedSkins.Count && skinNames.Contains(name))
|
||||
{
|
||||
loadedSkins[idx] = name;
|
||||
reloadSkins();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重新加载现有皮肤列表, 用于刷新等操作
|
||||
/// </summary>
|
||||
public void ReloadSkins() { lock (_lock) reloadSkins(); }
|
||||
private void reloadSkins()
|
||||
{
|
||||
clearSkin();
|
||||
foreach (var s in loadedSkins.Distinct()) addSkin(s);
|
||||
update(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载皮肤, 之后需要使用 <see cref="setSlotsToSetupPose"/> 来复位
|
||||
/// </summary>
|
||||
protected abstract void addSkin(string name);
|
||||
|
||||
/// <summary>
|
||||
/// 清空加载的所有皮肤
|
||||
/// </summary>
|
||||
protected abstract void clearSkin();
|
||||
|
||||
/// <summary>
|
||||
/// 包含的所有动画名称
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public ReadOnlyCollection<string> AnimationNames { get; private set; }
|
||||
protected List<string> animationNames = [EMPTY_ANIMATION];
|
||||
protected readonly List<string> animationNames = [EMPTY_ANIMATION];
|
||||
|
||||
/// <summary>
|
||||
/// 默认轨道动画名称, 如果设置的动画不存在则忽略
|
||||
/// 获取所有非 null 的轨道索引快照
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(AnimationConverter))]
|
||||
[Category("[3] 动画"), DisplayName("默认轨道动画")]
|
||||
public string Track0Animation
|
||||
{
|
||||
get { lock (_lock) return track0Animation; }
|
||||
set { lock (_lock) { track0Animation = value; update(0); } }
|
||||
}
|
||||
protected abstract string track0Animation { get; set; }
|
||||
public int[] GetTrackIndices() { lock (_lock) return getTrackIndices(); }
|
||||
protected abstract int[] getTrackIndices();
|
||||
|
||||
/// <summary>
|
||||
/// 默认轨道动画时长
|
||||
/// 获取指定轨道的当前动画, 如果没有, 应当返回空动画名称
|
||||
/// </summary>
|
||||
[Category("[3] 动画"), DisplayName("默认轨道动画时长")]
|
||||
public float Track0AnimationDuration { get => GetAnimationDuration(Track0Animation); } // TODO: 动画时长变成伪属性在面板显示
|
||||
public string GetAnimation(int track) { lock (_lock) return getAnimation(track); }
|
||||
protected abstract string getAnimation(int track);
|
||||
|
||||
/// <summary>
|
||||
/// 设置某个轨道动画
|
||||
/// </summary>
|
||||
public void SetAnimation(int track, string name) { lock (_lock) { setAnimation(track, name); update(0); } }
|
||||
protected abstract void setAnimation(int track, string name);
|
||||
|
||||
/// <summary>
|
||||
/// 清除某个轨道, 与设置空动画不同, 是彻底删除轨道内的东西
|
||||
/// </summary>
|
||||
public void ClearTrack(int i) { lock (_lock) { clearTrack(i); update(0); } }
|
||||
protected abstract void clearTrack(int i); // XXX: 清除轨道之后被加载的附件还是会保留, 不会自动卸下, 除非使用 SetSlotsToSetupPose
|
||||
|
||||
/// <summary>
|
||||
/// 获取动画时长, 如果动画不存在则返回 0
|
||||
/// </summary>
|
||||
public abstract float GetAnimationDuration(string name);
|
||||
|
||||
/// <summary>
|
||||
/// 重置所有轨道上的动画时间
|
||||
/// </summary>
|
||||
public void ResetAnimationsTime() { lock (_lock) { foreach (var i in getTrackIndices()) setAnimation(i, getAnimation(i)); update(0); } }
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -284,8 +383,8 @@ namespace SpineViewer.Spine
|
||||
/// 显示调试
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public bool IsDebug
|
||||
{
|
||||
public bool IsDebug
|
||||
{
|
||||
get { lock (_lock) return isDebug; }
|
||||
set { lock (_lock) isDebug = value; }
|
||||
}
|
||||
@@ -350,16 +449,11 @@ namespace SpineViewer.Spine
|
||||
protected abstract RectangleF bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 骨骼预览图
|
||||
/// 骨骼预览图, 并没有去除预乘, 画面可能偏暗
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public Image Preview { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取动画时长, 如果动画不存在则返回 0
|
||||
/// </summary>
|
||||
public abstract float GetAnimationDuration(string name);
|
||||
|
||||
/// <summary>
|
||||
/// 更新内部状态
|
||||
/// </summary>
|
||||
@@ -390,10 +484,18 @@ namespace SpineViewer.Spine
|
||||
|
||||
/// <summary>
|
||||
/// SFML.Graphics.Drawable 接口实现
|
||||
/// <para>这个渲染实现绘制出来的像素将是预乘的, 当渲染的背景透明度是 1 时, 则等价于非预乘的结果, 即正常画面, 否则画面偏暗</para>
|
||||
/// <para>可以用于 <see cref="SFML.Graphics.RenderWindow"/> 的渲染, 因为直接在窗口上绘制时窗口始终是不透明的</para>
|
||||
/// </summary>
|
||||
public void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states) { lock (_lock) draw(target, states); }
|
||||
|
||||
/// <summary>
|
||||
/// 这个渲染实现绘制出来的像素将是预乘的, 当渲染的背景透明度是 1 时, 则等价于非预乘的结果, 即正常画面, 否则画面偏暗
|
||||
/// <para>可以用于 <see cref="SFML.Graphics.RenderWindow"/> 的渲染, 因为直接在窗口上绘制时窗口始终是不透明的</para>
|
||||
/// </summary>
|
||||
protected abstract void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states);
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
@@ -21,32 +22,6 @@ namespace SpineViewer.Spine
|
||||
}
|
||||
}
|
||||
|
||||
public class AnimationConverter : StringConverter
|
||||
{
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
|
||||
{
|
||||
if (context?.Instance is Spine obj)
|
||||
{
|
||||
return new StandardValuesCollection(obj.AnimationNames);
|
||||
}
|
||||
else if (context?.Instance is Spine[] spines)
|
||||
{
|
||||
if (spines.Length > 0)
|
||||
{
|
||||
IEnumerable<string> common = spines[0].AnimationNames;
|
||||
foreach (var spine in spines.Skip(1))
|
||||
common = common.Intersect(spine.AnimationNames);
|
||||
return new StandardValuesCollection(common.ToArray());
|
||||
}
|
||||
}
|
||||
return base.GetStandardValues(context);
|
||||
}
|
||||
}
|
||||
|
||||
public class SkinConverter : StringConverter
|
||||
{
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
@@ -65,7 +40,100 @@ namespace SpineViewer.Spine
|
||||
{
|
||||
IEnumerable<string> common = spines[0].SkinNames;
|
||||
foreach (var spine in spines.Skip(1))
|
||||
common = common.Intersect(spine.SkinNames);
|
||||
common = common.Union(spine.SkinNames);
|
||||
return new StandardValuesCollection(common.ToArray());
|
||||
}
|
||||
}
|
||||
return base.GetStandardValues(context);
|
||||
}
|
||||
}
|
||||
|
||||
public class AnimationConverter : StringConverter
|
||||
{
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
|
||||
{
|
||||
if (context?.Instance is Spine obj)
|
||||
{
|
||||
return new StandardValuesCollection(obj.AnimationNames);
|
||||
}
|
||||
else if (context?.Instance is Spine[] spines)
|
||||
{
|
||||
if (spines.Length > 0)
|
||||
{
|
||||
IEnumerable<string> common = spines[0].AnimationNames;
|
||||
foreach (var spine in spines.Skip(1))
|
||||
common = common.Union(spine.AnimationNames);
|
||||
return new StandardValuesCollection(common.ToArray());
|
||||
}
|
||||
}
|
||||
return base.GetStandardValues(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轨道索引包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力
|
||||
/// </summary>
|
||||
public class TrackWrapperConverter : ExpandableObjectConverter
|
||||
{
|
||||
// NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转
|
||||
// ToString 实现了 ConvertTo
|
||||
// SetValue 实现了从字符串设置属性
|
||||
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
|
||||
{
|
||||
if (context.Instance is AnimationTracksType animTrack)
|
||||
{
|
||||
return new StandardValuesCollection(animTrack.Spine.AnimationNames);
|
||||
}
|
||||
else if (context.Instance is object[] instances && instances.All(x => x is AnimationTracksType))
|
||||
{
|
||||
// XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的 AnimationTracksType[] 类型
|
||||
var animTracks = instances.Cast<AnimationTracksType>().ToArray();
|
||||
if (animTracks.Length > 0)
|
||||
{
|
||||
IEnumerable<string> common = animTracks[0].Spine.AnimationNames;
|
||||
foreach (var t in animTracks.Skip(1))
|
||||
common = common.Union(t.Spine.AnimationNames);
|
||||
return new StandardValuesCollection(common.ToArray());
|
||||
}
|
||||
}
|
||||
return base.GetStandardValues(context);
|
||||
}
|
||||
}
|
||||
|
||||
public class SkinWrapperConverter : StringConverter
|
||||
{
|
||||
// NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转
|
||||
// ToString 实现了 ConvertTo
|
||||
// SetValue 实现了从字符串设置属性
|
||||
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
|
||||
{
|
||||
if (context.Instance is SkinManager manager)
|
||||
{
|
||||
return new StandardValuesCollection(manager.Spine.SkinNames);
|
||||
}
|
||||
else if (context.Instance is object[] instances && instances.All(x => x is SkinManager))
|
||||
{
|
||||
// XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的 SkinManager[] 类型
|
||||
var managers = instances.Cast<SkinManager>().ToArray();
|
||||
if (managers.Length > 0)
|
||||
{
|
||||
IEnumerable<string> common = managers[0].Spine.SkinNames;
|
||||
foreach (var t in managers.Skip(1))
|
||||
common = common.Union(t.Spine.SkinNames);
|
||||
return new StandardValuesCollection(common.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using SpineViewer.Dialogs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Design;
|
||||
@@ -36,4 +37,52 @@ namespace SpineViewer.Spine
|
||||
openFileDialog.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多轨道动画编辑器
|
||||
/// </summary>
|
||||
public class AnimationTracksEditor : UITypeEditor
|
||||
{
|
||||
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context) => UITypeEditorEditStyle.Modal;
|
||||
|
||||
public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value)
|
||||
{
|
||||
if (provider == null || context == null || context.Instance is not Spine)
|
||||
return value;
|
||||
|
||||
IWindowsFormsEditorService editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
|
||||
if (editorService == null)
|
||||
return value;
|
||||
|
||||
using (var dialog = new AnimationTracksEditorDialog((Spine)context.Instance))
|
||||
editorService.ShowDialog(dialog);
|
||||
|
||||
TypeDescriptor.Refresh(context.Instance);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多轨道动画编辑器
|
||||
/// </summary>
|
||||
public class SkinManagerEditor : UITypeEditor
|
||||
{
|
||||
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context) => UITypeEditorEditStyle.Modal;
|
||||
|
||||
public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value)
|
||||
{
|
||||
if (provider == null || context == null || context.Instance is not Spine)
|
||||
return value;
|
||||
|
||||
IWindowsFormsEditorService editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
|
||||
if (editorService == null)
|
||||
return value;
|
||||
|
||||
using (var dialog = new SkinManagerEditorDialog((Spine)context.Instance))
|
||||
editorService.ShowDialog(dialog);
|
||||
|
||||
TypeDescriptor.Refresh(context.Instance);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>0.11.5</Version>
|
||||
<Version>0.12.0</Version>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>appicon.ico</ApplicationIcon>
|
||||
|
||||
177
SpineViewer/Win32.cs
Normal file
177
SpineViewer/Win32.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
/// <summary>
|
||||
/// Win32 Sdk 包装类
|
||||
/// </summary>
|
||||
public static class Win32
|
||||
{
|
||||
public const int GWL_STYLE = -16;
|
||||
public const int WS_SIZEBOX = 0x40000;
|
||||
public const int WS_BORDER = 0x800000;
|
||||
public const int WS_POPUP = unchecked((int)0x80000000);
|
||||
|
||||
public const int GWL_EXSTYLE = -20;
|
||||
public const int WS_EX_TOPMOST = 0x8;
|
||||
public const int WS_EX_TRANSPARENT = 0x20;
|
||||
public const int WS_EX_TOOLWINDOW = 0x80;
|
||||
public const int WS_EX_WINDOWEDGE = 0x100;
|
||||
public const int WS_EX_CLIENTEDGE = 0x200;
|
||||
public const int WS_EX_LAYERED = 0x80000;
|
||||
public const int WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE;
|
||||
|
||||
public const uint LWA_COLORKEY = 0x1;
|
||||
public const uint LWA_ALPHA = 0x2;
|
||||
|
||||
public const byte AC_SRC_OVER = 0x00;
|
||||
public const byte AC_SRC_ALPHA = 0x01;
|
||||
|
||||
public const int ULW_COLORKEY = 0x00000001;
|
||||
public const int ULW_ALPHA = 0x00000002;
|
||||
public const int ULW_OPAQUE = 0x00000004;
|
||||
|
||||
public const IntPtr HWND_TOPMOST = -1;
|
||||
|
||||
public const uint SWP_NOSIZE = 0x0001;
|
||||
public const uint SWP_NOMOVE = 0x0002;
|
||||
public const uint SWP_NOZORDER = 0x0004;
|
||||
public const uint SWP_FRAMECHANGED = 0x0020;
|
||||
public const uint SWP_REFRESHLONG = SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_FRAMECHANGED;
|
||||
|
||||
public const int WM_SPAWN_WORKER = 0x052C; // 一个未公开的神秘消息
|
||||
|
||||
public const uint SMTO_NORMAL = 0x0000;
|
||||
public const uint SMTO_BLOCK = 0x0001;
|
||||
public const uint SMTO_ABORTIFHUNG = 0x0002;
|
||||
public const uint SMTO_NOTIMEOUTIFNOTHUNG = 0x0008;
|
||||
|
||||
public const uint GA_PARENT = 1;
|
||||
public const uint GW_OWNER = 4;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct POINT
|
||||
{
|
||||
public int x;
|
||||
public int y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct SIZE
|
||||
{
|
||||
public int cx;
|
||||
public int cy;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct BLENDFUNCTION
|
||||
{
|
||||
public byte BlendOp;
|
||||
public byte BlendFlags;
|
||||
public byte SourceConstantAlpha;
|
||||
public byte AlphaFormat;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct LASTINPUTINFO
|
||||
{
|
||||
public uint cbSize;
|
||||
public uint dwTime;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern IntPtr GetDC(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool GetLayeredWindowAttributes(IntPtr hWnd, ref uint crKey, ref byte bAlpha, ref uint dwFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool SetLayeredWindowAttributes(IntPtr hWnd, uint pcrKey, byte pbAlpha, uint pdwFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool UpdateLayeredWindow(IntPtr hWnd, IntPtr hdcDst, IntPtr pptDst, ref SIZE psize, IntPtr hdcSrc, ref POINT pptSrc, int crKey, ref BLENDFUNCTION pblend, int dwFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern uint GetDoubleClickTime();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam, uint fuFlags, uint uTimeout, out IntPtr lpdwResult);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string className, string windowTitle);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern IntPtr GetParent(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern IntPtr GetAncestor(IntPtr hWnd, uint gaFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);
|
||||
|
||||
[DllImport("gdi32.dll", SetLastError = true)]
|
||||
public static extern IntPtr CreateCompatibleDC(IntPtr hdc);
|
||||
|
||||
[DllImport("gdi32.dll", SetLastError = true)]
|
||||
public static extern bool DeleteDC(IntPtr hdc);
|
||||
|
||||
[DllImport("gdi32.dll", SetLastError = true)]
|
||||
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
|
||||
|
||||
[DllImport("gdi32.dll", SetLastError = true)]
|
||||
public static extern bool DeleteObject(IntPtr hObject);
|
||||
|
||||
public static TimeSpan GetLastInputElapsedTime()
|
||||
{
|
||||
LASTINPUTINFO lastInputInfo = new();
|
||||
lastInputInfo.cbSize = (uint)Marshal.SizeOf(lastInputInfo);
|
||||
|
||||
uint idleTimeMillis = 1000;
|
||||
if (GetLastInputInfo(ref lastInputInfo))
|
||||
{
|
||||
uint tickCount = (uint)Environment.TickCount;
|
||||
uint lastInputTick = lastInputInfo.dwTime;
|
||||
idleTimeMillis = tickCount - lastInputTick;
|
||||
}
|
||||
return TimeSpan.FromMilliseconds(idleTimeMillis);
|
||||
}
|
||||
|
||||
public static IntPtr GetWorkerW()
|
||||
{
|
||||
var progman = FindWindow("Progman", null);
|
||||
if (progman == IntPtr.Zero)
|
||||
return IntPtr.Zero;
|
||||
IntPtr hWnd = FindWindowEx(progman, 0, "WorkerW", null);
|
||||
Debug.WriteLine($"{hWnd:x8}");
|
||||
return hWnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
img/preview.webp
BIN
img/preview.webp
Binary file not shown.
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 126 KiB |
Reference in New Issue
Block a user