From deeec9d14b0fd811983588356880a567881fa68e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20Thom=C3=A9?= Date: Wed, 25 Jun 2014 08:23:46 +0200 Subject: [PATCH 01/10] Initial commit --- .gitattributes | 22 +++ .gitignore | 215 ++++++++++++++++++++++ EyePaint.sln | 26 +++ EyePaint/App.config | 6 + EyePaint/App.xaml | 34 ++++ EyePaint/App.xaml.cs | 82 +++++++++ EyePaint/DialogWindow.xaml | 90 +++++++++ EyePaint/DialogWindow.xaml.cs | 58 ++++++ EyePaint/EyePaint.csproj | 157 ++++++++++++++++ EyePaint/MainWindow.xaml | 96 ++++++++++ EyePaint/MainWindow.xaml.cs | 141 ++++++++++++++ EyePaint/Properties/AssemblyInfo.cs | 55 ++++++ EyePaint/Properties/Resources.Designer.cs | 73 ++++++++ EyePaint/Properties/Resources.resx | 124 +++++++++++++ EyePaint/Properties/Settings.Designer.cs | 26 +++ EyePaint/Properties/Settings.settings | 7 + EyePaint/Resources/kalkyl.png | Bin 0 -> 44867 bytes EyePaint/TobiiGazeCore32.dll | Bin 0 -> 490096 bytes 18 files changed, 1212 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 EyePaint.sln create mode 100644 EyePaint/App.config create mode 100644 EyePaint/App.xaml create mode 100644 EyePaint/App.xaml.cs create mode 100644 EyePaint/DialogWindow.xaml create mode 100644 EyePaint/DialogWindow.xaml.cs create mode 100644 EyePaint/EyePaint.csproj create mode 100644 EyePaint/MainWindow.xaml create mode 100644 EyePaint/MainWindow.xaml.cs create mode 100644 EyePaint/Properties/AssemblyInfo.cs create mode 100644 EyePaint/Properties/Resources.Designer.cs create mode 100644 EyePaint/Properties/Resources.resx create mode 100644 EyePaint/Properties/Settings.Designer.cs create mode 100644 EyePaint/Properties/Settings.settings create mode 100644 EyePaint/Resources/kalkyl.png create mode 100644 EyePaint/TobiiGazeCore32.dll diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..412eeda --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9d6bd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,215 @@ +################# +## Eclipse +################# + +*.pydevproject +.project +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + + +################# +## Visual Studio +################# + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results + +[Dd]ebug/ +[Rr]elease/ +x64/ +build/ +[Bb]in/ +[Oo]bj/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.log +*.scc + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.Publish.xml +*.pubxml + +# NuGet Packages Directory +## TODO: If you have NuGet Package Restore enabled, uncomment the next line +#packages/ + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.[Pp]ublish.xml +*.pfx +*.publishsettings + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +App_Data/*.mdf +App_Data/*.ldf + +############# +## Windows detritus +############# + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Mac crap +.DS_Store + + +############# +## Python +############# + +*.py[co] + +# Packages +*.egg +*.egg-info +dist/ +build/ +eggs/ +parts/ +var/ +sdist/ +develop-eggs/ +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg diff --git a/EyePaint.sln b/EyePaint.sln new file mode 100644 index 0000000..69bd8b9 --- /dev/null +++ b/EyePaint.sln @@ -0,0 +1,26 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Express 2013 for Windows Desktop +VisualStudioVersion = 12.0.21005.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EyePaint", "EyePaint\EyePaint.csproj", "{0A8492D9-B72D-4D5E-AF92-01119AF21737}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0A8492D9-B72D-4D5E-AF92-01119AF21737}.Debug|Any CPU.ActiveCfg = Debug|x86 + {0A8492D9-B72D-4D5E-AF92-01119AF21737}.Debug|x86.ActiveCfg = Debug|x86 + {0A8492D9-B72D-4D5E-AF92-01119AF21737}.Debug|x86.Build.0 = Debug|x86 + {0A8492D9-B72D-4D5E-AF92-01119AF21737}.Release|Any CPU.ActiveCfg = Release|x86 + {0A8492D9-B72D-4D5E-AF92-01119AF21737}.Release|x86.ActiveCfg = Release|x86 + {0A8492D9-B72D-4D5E-AF92-01119AF21737}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/EyePaint/App.config b/EyePaint/App.config new file mode 100644 index 0000000..d0feca6 --- /dev/null +++ b/EyePaint/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/EyePaint/App.xaml b/EyePaint/App.xaml new file mode 100644 index 0000000..8210754 --- /dev/null +++ b/EyePaint/App.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/EyePaint/App.xaml.cs b/EyePaint/App.xaml.cs new file mode 100644 index 0000000..5cb94e8 --- /dev/null +++ b/EyePaint/App.xaml.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media.Animation; +using Tobii.Gaze.Core; + +namespace EyePaint +{ + /// + /// Interaction logic for App.xaml. + /// + public partial class App : Application + { + IEyeTracker iet; + Queue gazePoints; + static public bool EyeTracking { get; private set; } + + [DllImport("User32.dll")] + static extern bool SetCursorPos(int X, int Y); + + public App() + { + try + { + Uri url = new EyeTrackerCoreLibrary().GetConnectedEyeTracker(); + iet = new EyeTracker(url); + iet.EyeTrackerError += onEyeTrackerError; + iet.GazeData += onGazeData; + Task.Factory.StartNew(() => iet.RunEventLoop()); + iet.Connect(); + gazePoints = new Queue(); + iet.StartTracking(); + EyeTracking = true; + } + catch (EyeTrackerException) { EyeTracking = false; } + catch (NullReferenceException) { EyeTracking = false; } + } + + void onEyeTrackerError(object s, EyeTrackerErrorEventArgs e) + { + EyeTracking = false; //TODO Retry eye tracker connection, and eventually notify the museum staff. + } + + void onGazeData(object s, GazeDataEventArgs e) + { + if (e.GazeData.TrackingStatus != TrackingStatus.BothEyesTracked) return; //TODO Implement one-eye operation. + + var previousGazePoint = Dispatcher.Invoke(() => { return Mouse.GetPosition(Application.Current.MainWindow); }); + + var newGazePoint = Dispatcher.Invoke(() => + { + return new Point( + Application.Current.MainWindow.ActualWidth * (e.GazeData.Left.GazePointOnDisplayNormalized.X + e.GazeData.Right.GazePointOnDisplayNormalized.X) / 2, + Application.Current.MainWindow.ActualHeight * (e.GazeData.Left.GazePointOnDisplayNormalized.Y + e.GazeData.Right.GazePointOnDisplayNormalized.Y) / 2 + ); + }); + gazePoints.Enqueue(newGazePoint); + + var gazePoint = new Point( + gazePoints.Average(x => x.X), + gazePoints.Average(x => x.Y) + ); + + if ((newGazePoint - gazePoint).Length > 50) + { + gazePoint = newGazePoint; + gazePoints.Clear(); + } + + var offset = gazePoint - previousGazePoint; + if (offset.Length < 100) gazePoint += offset; + + SetCursorPos((int)gazePoint.X, (int)gazePoint.Y); + } + } +} diff --git a/EyePaint/DialogWindow.xaml b/EyePaint/DialogWindow.xaml new file mode 100644 index 0000000..058aa74 --- /dev/null +++ b/EyePaint/DialogWindow.xaml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EyePaint/DialogWindow.xaml.cs b/EyePaint/DialogWindow.xaml.cs new file mode 100644 index 0000000..686ad40 --- /dev/null +++ b/EyePaint/DialogWindow.xaml.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; + +namespace EyePaint +{ + /// + /// Interaction logic for DialogWindow.xaml + /// + public partial class DialogWindow : Window + { + //TODO Remove. + [DllImport("User32.dll")] + private static extern bool SetCursorPos(int x, int y); + + public DialogWindow(string instructions, Action a = null) + { + InitializeComponent(); + Instructions.Text = instructions; + SetCursorPos(-1, -1); //TODO Remove. + + if (a != null) + { + Buttons.Visibility = Visibility.Collapsed; + FadeIn.Completed += (s, e) => { a(); Close(); }; + } + + ShowDialog(); + } + + void onConfirm(object s, EventArgs e) + { + DialogResult = true; + } + + void onCancel(object s, EventArgs e) + { + DialogResult = false; + } + + void onClose(object s, EventArgs e) + { + Close(); + } + } +} diff --git a/EyePaint/EyePaint.csproj b/EyePaint/EyePaint.csproj new file mode 100644 index 0000000..c2f5354 --- /dev/null +++ b/EyePaint/EyePaint.csproj @@ -0,0 +1,157 @@ + + + + + Debug + AnyCPU + {0A8492D9-B72D-4D5E-AF92-01119AF21737} + WinExe + Properties + EyePaint + EyePaint + v4.5.1 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + + false + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + true + + + true + bin\x86\Debug\ + DEBUG;TRACE + full + x86 + prompt + ManagedMinimumRules.ruleset + true + true + + + bin\x86\Release\ + TRACE + true + pdbonly + x86 + prompt + ManagedMinimumRules.ruleset + true + + + + + + + + + + + + 4.0 + + + False + ..\..\..\..\..\..\..\Program Files (x86)\Tobii\Tobii EyeX\Tobii.Gaze.Core.NET.dll + + + + + + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + + App.xaml + Code + + + DialogWindow.xaml + + + MainWindow.xaml + Code + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + False + Microsoft .NET Framework 4.5 %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + false + + + + + + PreserveNewest + + + + + \ No newline at end of file diff --git a/EyePaint/MainWindow.xaml b/EyePaint/MainWindow.xaml new file mode 100644 index 0000000..9ddbd38 --- /dev/null +++ b/EyePaint/MainWindow.xaml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EyePaint/EyePaint.csproj b/EyePaint/EyePaint.csproj index 501eb8e..22b8e51 100644 --- a/EyePaint/EyePaint.csproj +++ b/EyePaint/EyePaint.csproj @@ -142,13 +142,32 @@ + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EyePaint/MainWindow.xaml.cs b/EyePaint/MainWindow.xaml.cs index 5e068ae..9cfc5bb 100644 --- a/EyePaint/MainWindow.xaml.cs +++ b/EyePaint/MainWindow.xaml.cs @@ -41,17 +41,18 @@ public MainWindow() { InitializeComponent(); rng = new Random(); - (paintTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(33), DispatcherPriority.Normal, (s, e) => update(), Dispatcher)).Stop(); + (paintTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(33), DispatcherPriority.Normal, (s, e) => updateDrawing(), Dispatcher)).Stop(); tool = tools.First(); color = colors.First(); } - void Window_Loaded(object s, EventArgs e) + void onWindowLoaded(object s, EventArgs e) { bitmap = new RenderTargetBitmap((int)(Drawing.ActualWidth), (int)(Drawing.ActualHeight), 96.0, 96.0, PixelFormats.Pbgra32); + Raster.Source = bitmap; } - void start(Point p) + void startDrawing(Point p) { root = p; paintTimer.Start(); @@ -61,7 +62,7 @@ void start(Point p) parents.Add(root, root); } - void update() + void updateDrawing() { // Grow model. var newLeaves = new PointCollection(); @@ -104,14 +105,17 @@ void update() dc.DrawGeometry(null, pen, gg); // Vertices + var gg2 = new GeometryGroup(); brush = new SolidColorBrush(getRandomColor(color, tool.ColorVariety)); brush.Opacity = rng.NextDouble() * tool.VerticesOpacity; foreach (var leaf in leaves) { var r = rng.NextDouble(); var eg = new EllipseGeometry(leaf, tool.VerticesSize * (r + tool.VerticesSquash * rng.NextDouble()), tool.VerticesSize * (r + tool.VerticesSquash * rng.NextDouble())); - dc.DrawGeometry(brush, null, eg); + //dc.DrawGeometry(brush, null, eg); + gg2.Children.Add(eg); } + dc.DrawGeometry(brush, null, gg2); // Hull StreamGeometry sg = new StreamGeometry(); @@ -130,10 +134,6 @@ void update() leaves = newLeaves; parents = newParents; bitmap.Render(dv); - var image = new Image(); - image.Source = bitmap; - Drawing.Children.Clear(); //TODO Implement undo history. - Drawing.Children.Add(image); } Color getRandomColor(Color? baseColor = null, double randomness = 1) @@ -142,7 +142,7 @@ Color getRandomColor(Color? baseColor = null, double randomness = 1) return (baseColor.HasValue) ? baseColor.Value + Color.Multiply(c, (float)randomness) : c; } - void save() + void saveDrawing() { var e = new PngBitmapEncoder(); e.Frames.Add(BitmapFrame.Create(bitmap)); @@ -152,45 +152,54 @@ void save() } } - void Drawing_MouseDown(object s, MouseButtonEventArgs e) + void onDrawingMouseDown(object s, MouseButtonEventArgs e) { - start(e.GetPosition(Drawing)); + startDrawing(e.GetPosition(Drawing)); } - void Drawing_MouseUp(object s, MouseButtonEventArgs e) + void onDrawingMouseUp(object s, MouseButtonEventArgs e) { paintTimer.Stop(); } - void Drawing_MouseLeave(object s, MouseEventArgs e) + void onDrawingMouseLeave(object s, MouseEventArgs e) { paintTimer.Stop(); } - void Drawing_MouseMove(object s, MouseEventArgs e) + void onDrawingMouseMove(object s, MouseEventArgs e) { if (e.LeftButton != MouseButtonState.Pressed) { paintTimer.Stop(); return; } - if ((root - e.GetPosition(Drawing)).Length > 20) start(e.GetPosition(Drawing)); + if ((root - e.GetPosition(Drawing)).Length > 50) startDrawing(e.GetPosition(Drawing)); } - void SaveButton_Click(object s, RoutedEventArgs e) + void onStartButtonClick(object s, EventArgs e) { - if (new DialogWindow("Spara bilden?").DialogResult.Value) save(); + var b = s as Button; + b.IsEnabled = false; + b.Visibility = Visibility.Hidden; } - void ToolButton_Click(object s, RoutedEventArgs e) + void onSaveButtonClick(object s, EventArgs e) + { + if (new DialogWindow("Spara bilden?").DialogResult.Value) saveDrawing(); + } + + void onToolButtonClick(object s, EventArgs e) { tool = tools[(tools.IndexOf(tool) + 1) % tools.Count]; } - void ColorButton_Click(object s, RoutedEventArgs e) + void onColorButtonClick(object s, EventArgs e) { color = colors[(colors.IndexOf(color) + 1) % colors.Count]; + (s as Button).Background = new SolidColorBrush(color); } - void RandomButton_Click(object s, RoutedEventArgs e) + void onRandomButtonClick(object s, EventArgs e) { color = getRandomColor(); + (s as Button).Background = new SolidColorBrush(color); tool = new Tool { BranchCount = rng.Next(1, 20), @@ -213,8 +222,9 @@ void onInactivity(object s, EventArgs e) { if (new DialogWindow("Börja om?").DialogResult.Value) { - (new MainWindow()).Show(); - Close(); + var mw = new MainWindow(); + mw.Loaded += (_,__) => Close(); + mw.Show(); } } } diff --git a/EyePaint/Properties/Resources.Designer.cs b/EyePaint/Properties/Resources.Designer.cs index 5f5074a..ef64cc5 100644 --- a/EyePaint/Properties/Resources.Designer.cs +++ b/EyePaint/Properties/Resources.Designer.cs @@ -59,5 +59,75 @@ internal Resources() { resourceCulture = value; } } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap beaker { + get { + object obj = ResourceManager.GetObject("beaker", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap brush { + get { + object obj = ResourceManager.GetObject("brush", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap export { + get { + object obj = ResourceManager.GetObject("export", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap eye { + get { + object obj = ResourceManager.GetObject("eye", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap pipette { + get { + object obj = ResourceManager.GetObject("pipette", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap thumbs_down { + get { + object obj = ResourceManager.GetObject("thumbs_down", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap thumbs_up { + get { + object obj = ResourceManager.GetObject("thumbs_up", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } } } diff --git a/EyePaint/Properties/Resources.resx b/EyePaint/Properties/Resources.resx index 1af7de1..38d5166 100644 --- a/EyePaint/Properties/Resources.resx +++ b/EyePaint/Properties/Resources.resx @@ -117,4 +117,26 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\beaker.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\brush.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\export.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\eye.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\pipette.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\thumbs-down.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\thumbs-up.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + \ No newline at end of file diff --git a/EyePaint/Resources/beaker.png b/EyePaint/Resources/beaker.png new file mode 100644 index 0000000000000000000000000000000000000000..ff1a8631adf141b1680943e5ad9fdae639c924f1 GIT binary patch literal 1563 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 y{Q@smXp0a+2qAzMLPM zCymaLYsOu^CLPgpBc)F^=ZdC^bp6d7Malnx{FCL_X$bX>R{(*8c|#+tIo45*B?rNw za<(0injw;?VbDXDJ_o&zuT6A&=tgzs#8Tv@G`mh*=il1C%)FQ$$;#u_?W{I|l1uE+ zzC*3fjE&>;0+#TdU{~~*LZhE4z80iK{Xm>n^t)KMQNx?eyR#>pCN}XxZ(QJ-Z)CgWX=YDZE~S1z zb)aO?F=d<60g_-$0((i%*Q0g+>KdS}GrZ{Id!lV#Uuq|Ly?|>80EVzK=_OskN#ZBW z5nV&2bj5pG162SJiMS=XbSxR^VfeUoy+FhQ0A`^&fz9Z=sAty4ifnd<_znZmEcBbe zW+^79U|!y{`&A&tvidPF2b|(DmG&|eib75fKO}3c7(s^OU@|Vx&&X4TOq;;tS2SU&B>YxE z=74mZKi50Pl$UDbUbd~wjl16u3(Uc&S17lbQ=)xCiSkiAQ%k@5;&H!8_^c&HB_5Vx zU=sn&0r(4_l=8|KeK?VkJ}>xLUK&yT36M!r^|uLW(SiXPc*^r*w}LfD?g$;=&kl8u z&yV0hUyR0SU3vAA1N-~&V%f+fEWolw1hx0%*tV(X0VhCb-DxJW5H>@z`F9Hu$=lN3 zmGyKdgQ!;V+?d@iOciE@u>7Th3VAYsmMcN_=hmKeP}}vO&YzA*SUAF@&iSi0hmuSb ztpJUnHq^=8+>Q8b_!s1SmB6~h9wBgr*1Kq}>2tp? z%wk+Vs(249EX{XkytshfR{jy^dCu~dy-ZRBO9A=1Z6(C{^DHP}I~Xms+Zuek2+a&< zc$~n!6rgLvW)eOX1b3nSV zi3dbS$!>-%HRx`{?rBdM$FsjSwzR>oERWX6P5Rsjgl$<5271u~4RM}~a}`mdx@=Hi zVRNYKUnV{&6c5QbZ>%~l7Jtm~d*!!vv;>q)y7o4!As|`Ii=Xr3MvORp?~|%k*|)$$ zB{BRQ?Ofg__c1&fo%yHGB;Taz{N@9`GK@u~ee|;SBEJf=|Dp<)t&oyfovs$Fzpn0= zILOuzZ)8aw@q~PwHl*CXn0hLutGbs=kOwX;=+JAddd|i`m+X3Ge*ck~KsD^E0CFqI zt{ZVW6+DAWd>yhy1ljBcuYBnUFRi~h@s>e)BX6_#c^b>23|saZ*}w|~*WjbNhqEdo zhN2{jX0;M9Faf36;M#M4@$eQXl$>!Z%YS+G^lr>CrtK=ucS>I+S(%rl(pF2IKQ-Jw zT-^Irx^OunL}LL(VCUek>7!Elo2~^|PGdd!9Re%sigyRv;;7You>%*ztbMY&Lf2iE z6^XF$m+Pjw*#_}-v&Ffo`zgk9BL8@13ZT4$2`@=O;L6$M@5?!ZrCmmoPFb<6a9^o5 zyjG4-{impA_4q=#gL$T}^`aO@uUA1wuT!XMg02>a!A4UmUDKUX{t$Y6?A^WbDrNhL zx&2{l>ScOZ9`WuYvUqIewdQIhfe#nF6mldKd|SQu#%eg8?!(zP7mvO3<&vY^#Z@pD zzAhIkJ61*WJI@`of4J9d)p-8lQ1WB=Xp5;grqs^BZuSGZgS2tOMT>d-^B;BMRF%pj zlyPF*`4n09YC64}if~Vv7BLXiPt_t^b&0TJfQ1%Ak^afrr+4?VrC(>1bI-IAB49Ej zFLrZv*qsteRk6Nr{ZuAAAEa6X_yNi#xZn51U>)pdJflDAv~+J9p1=oipV3O^e!>k5 z17e4^p9^>GYp=blz*Q1h;1D5xLwF(oFUf_eaj{gQiA2LoZU&3es{*~WkZ~Yu9i7+J zYEmSp*77@OmxBReE`V(&`tG13OwT|QQayzjhNAUmBFP|45ZiUv5>>D(WI1E*nDk;sYNffnX&9*YJhq=w8NbWtv4$$bJUQD#R z@_KH(dc{1<%-?o*49n~z>-1DSjJ33D_dAXXA4+xlCbW?Dj~>Gy?Y{bd cp|`hdAYzocFHL=P@NodvWINI$3rfQO0I(pDOctw*C>(W(;gdhAB{cb<(8DPk+(T2 zYzpf+lEj#zl%bF&E$!Lsy?@|+zSlqS{eHfAUY@Qp2owSY0?D|$IiLKQ#D66T`{_^Y zLWX_@EY!o*86^G}wcU59AQ1efyR)NDO66*mU!l@zrR?4~95LSEy1b*dyj(5;tP+74FhFFd%0N89+aP_Qn<(pI zcNH25MSYo(z{9M-bAUa>6}?@3D5?|e2{HjpU?xxuBuH&~22PM5$Py3)$fx9RkA3WA z*554Dyaq+k0I;HQ3S(2}S^1(ud&y>(wCvx4HbZUlMdpWzhvSrx4EO+eKS@s%{Qj;D zq!|=;t^r5 z7oDlE9uu+ukNcNr07YWWMtVr;WHI4WY*L{qM5rKE?dzC?JdsZDJ3RqTAYKsZ+bdb} z6H2!?*soYtBK^ZYu@dzBs@KFOi3{?J5YNbH*oDP}>j+HU-1eavsPwgx_1ex|y)j>3 z(KElR`J!KwJ_t|X{26LXzAuz>a|>4I(^ywoezH5sfoX6%m>pmRngge+3l3gk{lSvM z9WYf{DyhBp;q0ziN&L4@uI?s~w0`kHy`1LG5zGp-%`Eg8B#d~2)D3=0b=p$K^yliz z4`}~b=%F$co0MCv@6esv>(iw)dj4k7RKou3(-zjeL&K^a4atGt@MA-~FMDDv+G^&j zq(7_e^eP8XZhvSvq$*@Ps+C3C{4%uN#+o&k)vHr>I5YU5eISp6n@Se_nRjr&tX{h` zWsZHK|6)iCELBH1`h=&{lGyGE$2Bbw>z8z=NEn^WVI=}TOunsq?RTTrv4q+zoT7Mo zzoRWqYDe$|CMzhFScJ>Hx2dJx*N|C)5{ks4$HliQ=BGg!wovIS@|C*TZ`1S3f$DZN z_ZAph22?Z9m#?>JJ=7Yk`^`3ZjDZCtiOa(00giJg z-!52?m(O`j2lCY+!wy?h$$aK7T{tJB8EbWMYD`nAH@f#AC?X&_!e?-3Ov>+w(G?Cu zY9FWh0q$3)l!(3SCj8AyefQr6c#SSgCSp)F&de^9?Ll@u0&}5YMc;&P*c{k~KrOOJ^x^MP1=*{DucvFL|73AVaT^FTT zO`$95X4CyCZ&+K>PCP&c|7fx^gW6qk&u>mvnHqI|4Fm8#%EV6Xaxq_w%u&BGRf3!^lI(f0>Y7^DW3s$=r`p>>HH^Eg!~@??^6Hxg^oo%%vxE8(r%s zcN3DBb*pRHHObGQ;dLwai*Lnr{PD(@EX}7eW>e(v8|_|qNG;4o{Fw$-`bB&5kG z)@bBI*tE1E(~wIxQk%Z};JJe5aRK-7!I3*3H0 ziuknKDyP`0O%~%SuRC5!j1;b4!PZk9OQw;~C0<0&VZQVAoF#SOj% zlX8>0|3YSB8o)$XAL)>zAKO5;Ee6@7D2Zp+*JW1;?IJ**uA**R@``Q|U@5In{| zM{=pxZr8k$X3BdWmGvV}bR4>8rDdC|snY7J8%D8Vc@+mT8!_SEE-`MY8MsLdmvMa8 ztwh0_G&hU9A*tTClkq-Z^_;5NEGDN%Y*5=0cGVsntkIow23&#WC16o;#IWU}2_5Ot z7`bsN+O8LSJy%^+nvyhCQ!LD7{c;|ok61QrzzkgU!*SE#rWIo!gr)W?D_06%sQK%g fegA)h=S9fRr2Zb-1!Vru-v+t6cskRM;xhgL%^tEZ literal 0 HcmV?d00001 diff --git a/EyePaint/Resources/pipette.png b/EyePaint/Resources/pipette.png new file mode 100644 index 0000000000000000000000000000000000000000..22127d533a66a8aeaa81e5ade34fcbe1b8c150d7 GIT binary patch literal 1940 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 Date: Sat, 9 Aug 2014 14:05:32 +0200 Subject: [PATCH 04/10] Prototype 3 --- EyePaint/App.xaml | 68 ++++-- EyePaint/App.xaml.cs | 35 +-- EyePaint/DialogWindow.xaml | 24 +-- EyePaint/DialogWindow.xaml.cs | 20 +- EyePaint/ErrorWindow.xaml | 11 + EyePaint/ErrorWindow.xaml.cs | 27 +++ EyePaint/EyePaint.csproj | 33 +-- EyePaint/MainWindow.xaml | 100 ++++----- EyePaint/MainWindow.xaml.cs | 202 ++++++++++-------- EyePaint/{Tool.cs => MainWindowViewModel.cs} | 73 ++++--- EyePaint/Properties/Resources.Designer.cs | 68 +----- EyePaint/Properties/Resources.resx | 22 +- EyePaint/Resources/beaker.png | Bin 1563 -> 0 bytes EyePaint/Resources/brush.png | Bin 2172 -> 0 bytes EyePaint/Resources/export.png | Bin 2207 -> 0 bytes EyePaint/Resources/eye.png | Bin 2021 -> 0 bytes .../Resources/itc_officina_sans_std_bold.ttf | Bin 0 -> 83984 bytes EyePaint/Resources/pipette.png | Bin 1940 -> 0 bytes EyePaint/Resources/thumbs-down.png | Bin 2188 -> 0 bytes EyePaint/Resources/thumbs-up.png | Bin 2202 -> 0 bytes 20 files changed, 347 insertions(+), 336 deletions(-) create mode 100644 EyePaint/ErrorWindow.xaml create mode 100644 EyePaint/ErrorWindow.xaml.cs rename EyePaint/{Tool.cs => MainWindowViewModel.cs} (70%) delete mode 100644 EyePaint/Resources/beaker.png delete mode 100644 EyePaint/Resources/brush.png delete mode 100644 EyePaint/Resources/export.png delete mode 100644 EyePaint/Resources/eye.png create mode 100644 EyePaint/Resources/itc_officina_sans_std_bold.ttf delete mode 100644 EyePaint/Resources/pipette.png delete mode 100644 EyePaint/Resources/thumbs-down.png delete mode 100644 EyePaint/Resources/thumbs-up.png diff --git a/EyePaint/App.xaml b/EyePaint/App.xaml index 1d622ca..7f3b653 100644 --- a/EyePaint/App.xaml +++ b/EyePaint/App.xaml @@ -5,56 +5,80 @@ - - + + + + + + + + + + + - - - - + + + + - - + + - + - - - + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EyePaint/App.xaml.cs b/EyePaint/App.xaml.cs index cc3bf31..3a51394 100644 --- a/EyePaint/App.xaml.cs +++ b/EyePaint/App.xaml.cs @@ -47,6 +47,7 @@ public App() void onEyeTrackerError(object s, EyeTrackerErrorEventArgs e) { EyeTracking = false; //TODO Retry eye tracker connection, and eventually notify the museum staff. + (new ErrorWindow()).Show(); } void onGazeData(object s, GazeDataEventArgs e) @@ -82,34 +83,34 @@ void onGazeData(object s, GazeDataEventArgs e) if (offset.Length < 100) gazePoint += offset; */ + // Calibrate point. + //TODO Weight by distance to calibration point. + //var distances = calibrationPoints.Select(p => (pointToCalibrate - p).Length); + //var normalizedDistances = distances.Select(d => d / distances.Average()); + foreach (var o in this.offsets) gazePoint.Offset(o.X, o.Y); + SetCursorPos((int)gazePoint.X, (int)gazePoint.Y); } + TimeSpan? time; void onGazeClick(object s, EventArgs e) { - var activeWindow = Application.Current.Windows.OfType().SingleOrDefault(x => x.IsActive); - var focusedButton = FocusManager.GetFocusedElement(activeWindow) as Button; - focusedButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); + var c = s as Clock; - if (gazePoints.Count > 0) + if (time.HasValue && c.CurrentTime.HasValue && c.CurrentTime.Value < time.Value) { + var activeWindow = Application.Current.Windows.OfType().SingleOrDefault(x => x.IsActive); + var focusedButton = FocusManager.GetFocusedElement(activeWindow) as Button; + focusedButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); + + /* TODO var expectedPoint = focusedButton.PointToScreen(new Point(focusedButton.ActualWidth / 2, focusedButton.ActualHeight / 2)); - var actualPoint = new Point( - gazePoints.Average(x => x.X), - gazePoints.Average(x => x.Y) - ); + var actualPoint = new Point(gazePoints.Average(x => x.X), gazePoints.Average(x => x.Y)); offsets.Add(actualPoint - expectedPoint); + */ } - } - Point calibrate(Point pointToCalibrate, PointCollection calibrationPoints) - { - var calibratedPoint = new Point(pointToCalibrate.X, pointToCalibrate.Y); // TODO Remove uneccessary copy. - //TODO Weight by distance to calibration point. - //var distances = calibrationPoints.Select(p => (pointToCalibrate - p).Length); - //var normalizedDistances = distances.Select(d => d / distances.Average()); - foreach (var o in offsets) calibratedPoint.Offset(o.X, o.Y); - return calibratedPoint; + time = (c.CurrentState == ClockState.Active) ? c.CurrentTime : null; } } } diff --git a/EyePaint/DialogWindow.xaml b/EyePaint/DialogWindow.xaml index b3137b8..d6d81ea 100644 --- a/EyePaint/DialogWindow.xaml +++ b/EyePaint/DialogWindow.xaml @@ -6,33 +6,27 @@ - - + + - + - + - + + + - - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EyePaint/MainWindow.xaml.cs b/EyePaint/MainWindow.xaml.cs index e5d7f8b..595f7ae 100644 --- a/EyePaint/MainWindow.xaml.cs +++ b/EyePaint/MainWindow.xaml.cs @@ -1,64 +1,234 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; +using System.Runtime.Serialization.Json; using System.Windows; using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; -using System.Windows.Media.Effects; using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; using System.Windows.Threading; +using System.Xml; namespace EyePaint { + public struct Tree + { + public Point root; + public PointCollection leaves; + public Dictionary parents; + } + + public struct Shape + { + public int maxBranches, strokeThickness; + public double branchStepLength, branchStraightness, generationRotation, colorVariety, verticesSize, verticesSquashVariety, centerSize, centerOpacity, edgesOpacity, verticesOpacity, hullOpacity; + } + + public class Presets + { + public HashSet shapes; + public HashSet colors; + + public static Presets Load() + { + using (var fs = new FileStream("Presets.json", FileMode.Open, FileAccess.Read, FileShare.None)) + { + using (var jrwf = JsonReaderWriterFactory.CreateJsonReader(fs, XmlDictionaryReaderQuotas.Max)) + { + return (Presets)(new DataContractJsonSerializer(typeof(Presets))).ReadObject(jrwf); + } + } + } + + public void Save() + { + using (var fs = new FileStream("Presets.json", FileMode.Create, FileAccess.Write, FileShare.None)) + { + using (var xdw = JsonReaderWriterFactory.CreateJsonWriter(fs)) + { + (new DataContractJsonSerializer(GetType())).WriteObject(xdw, this); + } + } + } + } + /// /// Interaction logic for MainWindow.xaml /// public partial class MainWindow : Window { Random rng; + DateTime time; + TimeSpan timePainted; DispatcherTimer paintTimer; Tree model; - Tool tool; - List tools; + Point gaze; + Shape shape; Color color; - List colors; + Presets presets; public MainWindow() { InitializeComponent(); +#if DEBUG + var p = new Presets(); + p.colors = new HashSet { Colors.Red, Colors.Blue, Colors.Green, Colors.Yellow, Colors.White, Colors.Black }; + p.shapes = new HashSet { new Shape { maxBranches = 100, strokeThickness = 1, branchStepLength = 10, branchStraightness = 1, generationRotation = 1, colorVariety = 1, verticesSize = 10, verticesSquashVariety = 0, centerSize = 100, centerOpacity = 1, edgesOpacity = 1, verticesOpacity = 1, hullOpacity = 1 } }; + p.Save(); + KeyDown += (s, e) => { presets.shapes.Add(shape); presets.Save(); }; +#endif + } + + void onContentRendered(object s, EventArgs e) + { + App.SetCursorPos(0, 0); + rng = new Random(); + presets = Presets.Load(); + color = presets.colors.ElementAt(rng.Next(presets.colors.Count)); + shape = presets.shapes.ElementAt(rng.Next(presets.shapes.Count)); + (paintTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(33), DispatcherPriority.Normal, (_, __) => updateDrawing(ref model, (RenderTargetBitmap)Raster.Source), Dispatcher)).Stop(); + clearDrawing(); + updateIcons(); + } + + void onPreviewMouseDown(object s, MouseButtonEventArgs e) + { + ((Storyboard)FindResource("InactivityAnimation")).Seek(TimeSpan.Zero); + } + + void onCanvasMouseDown(object s, MouseButtonEventArgs e) + { + gaze = e.GetPosition(s as Canvas); + startPainting(); + ((Storyboard)GazePaintMarker.FindResource("GazePaintAnimation")).Stop(); + } + + void onCanvasMouseUp(object s, MouseButtonEventArgs e) + { + stopPainting(); + ((Storyboard)GazePaintMarker.FindResource("GazePaintAnimation")).Begin(); + } + + void onCanvasMouseEnter(object s, MouseEventArgs e) + { + ((Storyboard)GazePaintMarker.FindResource("GazePaintAnimation")).Begin(); + } + + void onCanvasMouseLeave(object s, MouseEventArgs e) + { + stopPainting(); + ((Storyboard)GazePaintMarker.FindResource("GazePaintAnimation")).Stop(); + } + + void onCanvasMouseMove(object s, MouseEventArgs e) + { + var p = e.GetPosition(s as Canvas); + ((Storyboard)FindResource("InactivityAnimation")).Seek(TimeSpan.Zero); + Canvas.SetLeft(GazePaintMarker, p.X - GazePaintMarker.ActualWidth / 2); + Canvas.SetTop(GazePaintMarker, p.Y - GazePaintMarker.ActualHeight / 2); + if ((gaze - p).Length > 50) + { + gaze = p; + if (paintTimer.IsEnabled) model = createTree(gaze); + var sb = (Storyboard)GazePaintMarker.FindResource("GazePaintAnimation"); + if (sb.GetCurrentState() == ClockState.Filling) stopPainting(); + if (sb.GetCurrentState() != ClockState.Stopped) sb.Seek(TimeSpan.Zero); + } + if (!paintTimer.IsEnabled) gaze = p; + } + + void onStartButtonClick(object s, EventArgs e) + { + (s as Button).Visibility = Visibility.Hidden; + ((Storyboard)FindResource("InactivityAnimation")).Begin(); + Blur.Radius = 0; + PaintControls.IsEnabled = true; + } + + void onShapeButtonClick(object s, EventArgs e) + { + if (timePainted > TimeSpan.FromSeconds(10)) presets.shapes.Add(shape); + timePainted = TimeSpan.Zero; + var candidates = presets.shapes.Where(c => !c.Equals(shape)).ToList(); + shape = (rng.NextDouble() <= 0.5 && candidates.Count > 1) ? candidates.ElementAt(rng.Next(candidates.Count)) : new Shape + { + maxBranches = rng.Next(1, 101), + strokeThickness = (int)Math.Sqrt(rng.Next(1, 10)), + branchStepLength = Math.Sqrt(rng.Next(10, 101)), + branchStraightness = Math.Sqrt(rng.NextDouble()), + generationRotation = rng.NextDouble(), + colorVariety = rng.NextDouble(), + verticesSize = Math.Sqrt(rng.Next(1, 51)), + verticesSquashVariety = Math.Pow(rng.NextDouble(), 2), + centerSize = Math.Sqrt(rng.Next(1, 101)), + centerOpacity = Math.Pow(rng.NextDouble(), 2), + edgesOpacity = rng.NextDouble(), + verticesOpacity = rng.NextDouble(), + hullOpacity = Math.Pow(rng.NextDouble(), 2), + }; + updateIcons(); + } + + void onColorButtonClick(object s, EventArgs e) + { + if (timePainted > TimeSpan.FromSeconds(10)) presets.colors.Add(color); + timePainted = TimeSpan.Zero; + var candidates = presets.colors.Where(c => c != color).ToList(); + color = (rng.NextDouble() <= 0.5 && candidates.Count > 1) ? candidates.ElementAt(rng.Next(candidates.Count)) : createColor(); + updateIcons(); + } + + void onInactivity(object s, EventArgs e) + { + (new MainWindow()).Show(); + Close(); + } + + void onGazePaint(object s, EventArgs e) + { + startPainting(); + } + + void startPainting() + { + model = createTree(gaze); + paintTimer.Start(); + time = DateTime.Now; } - #region Drawing Functionality - Tree initializePaintStroke(Point p) + void stopPainting() + { + paintTimer.Stop(); + timePainted += DateTime.Now - time; + } + + Tree createTree(Point p) { var t = new Tree { root = p, leaves = new PointCollection(), parents = new Dictionary() }; - for (int i = 0; i < rng.Next((tool.BranchCount + 1) / 2, tool.BranchCount + 1); ++i) t.leaves.Add(t.root); + for (int i = 0; i < rng.Next((shape.maxBranches + 1) / 2, shape.maxBranches + 1); ++i) t.leaves.Add(t.root); t.parents[t.root] = t.root; return t; } - void doPaintStroke(Tree t, RenderTargetBitmap drawing) + void updateDrawing(ref Tree model, RenderTargetBitmap drawing) { // Grow model. var newLeaves = new PointCollection(); var newParents = new Dictionary(); - var rotation = tool.Rotation * rng.NextDouble() * 2 * Math.PI; - for (int i = 0; i < t.leaves.Count; ++i) + var rotation = shape.generationRotation * rng.NextDouble() * 2 * Math.PI; + for (int i = 0; i < model.leaves.Count; ++i) { - var q = (t.leaves.Count == 0) ? t.root : t.leaves[i]; - var angle = i * (2 * Math.PI / t.leaves.Count) + rotation + (1 - tool.BranchStraightness) * rng.NextDouble() * 2 * Math.PI; + var q = (model.leaves.Count == 0) ? model.root : model.leaves[i]; + var angle = + i * (2 * Math.PI / model.leaves.Count) + + rotation + + (1 - shape.branchStraightness) * rng.NextDouble() * 2 * Math.PI; var p = new Point( - q.X + tool.BranchLength * Math.Cos(angle), - q.Y + tool.BranchLength * Math.Sin(angle) + q.X + shape.branchStepLength * Math.Cos(angle), + q.Y + shape.branchStepLength * Math.Sin(angle) ); newParents[p] = q; newLeaves.Add(p); @@ -68,48 +238,50 @@ void doPaintStroke(Tree t, RenderTargetBitmap drawing) var dv = new DrawingVisual(); using (var dc = dv.RenderOpen()) { - var centerBrush = new SolidColorBrush(generateColor(color, tool.ColorVariety)); - centerBrush.Opacity = rng.NextDouble() * tool.CenterOpacityVariety; - var centerSize = rng.NextDouble() * tool.CenterSize; - dc.DrawEllipse(centerBrush, null, t.root, centerSize, centerSize); + var centerSize = rng.NextDouble() * shape.centerSize; + var centerBrush = new SolidColorBrush(createColor(color, shape.colorVariety)); + centerBrush.Opacity = rng.NextDouble() * shape.centerOpacity; + var centerPen = new Pen(new SolidColorBrush(createColor(color, shape.colorVariety)), 1); + centerPen.Brush.Opacity = rng.NextDouble() * shape.centerOpacity; + dc.DrawEllipse(centerBrush, centerPen, model.root, centerSize, centerSize); var edges = new GeometryGroup(); - var edgesBrush = new SolidColorBrush(generateColor(color, tool.ColorVariety)); - edgesBrush.Opacity = tool.EdgesOpacity; - var pen = new Pen(edgesBrush, tool.EdgesThickness); - pen.EndLineCap = pen.StartLineCap = PenLineCap.Round; - pen.LineJoin = PenLineJoin.Round; - foreach (var leaf in t.leaves) edges.Children.Add(new LineGeometry(leaf, t.parents[leaf])); - dc.DrawGeometry(null, pen, edges); + var edgesPen = new Pen(new SolidColorBrush(createColor(color, shape.colorVariety)), shape.strokeThickness); + edgesPen.Brush.Opacity = shape.edgesOpacity; + foreach (var leaf in model.leaves) edges.Children.Add(new LineGeometry(leaf, model.parents[leaf])); + dc.DrawGeometry(null, edgesPen, edges); var vertices = new GeometryGroup(); - var verticesBrush = new SolidColorBrush(generateColor(color, tool.ColorVariety)); - verticesBrush.Opacity = rng.NextDouble() * tool.VerticesOpacityVariety; - foreach (var leaf in t.leaves) + var verticesBrush = new SolidColorBrush(createColor(color, shape.colorVariety)); + verticesBrush.Opacity = rng.NextDouble() * shape.verticesOpacity; + var verticesPen = new Pen(verticesBrush, 1); + foreach (var leaf in model.leaves) { var r = rng.NextDouble(); - var eg = new EllipseGeometry(leaf, tool.VerticesSize * (r + tool.VerticesSquashVariety * rng.NextDouble()), tool.VerticesSize * (r + tool.VerticesSquashVariety * rng.NextDouble())); + var eg = new EllipseGeometry(leaf, shape.verticesSize * (r + shape.verticesSquashVariety * rng.NextDouble()), shape.verticesSize * (r + shape.verticesSquashVariety * rng.NextDouble())); vertices.Children.Add(eg); } - dc.DrawGeometry(verticesBrush, null, vertices); + dc.DrawGeometry(verticesBrush, verticesPen, vertices); var hull = new StreamGeometry(); - var hullBrush = new SolidColorBrush(generateColor(color, tool.ColorVariety)); - hullBrush.Opacity = rng.NextDouble() * tool.HullOpacityVariety; + var hullBrush = new SolidColorBrush(createColor(color, shape.colorVariety)); + hullBrush.Opacity = rng.NextDouble() * shape.hullOpacity; + var hullPen = new Pen(hullBrush, shape.strokeThickness); using (var sgc = hull.Open()) { - sgc.BeginFigure(t.leaves[0], true, true); - sgc.PolyLineTo(t.leaves, true, true); + sgc.BeginFigure(model.leaves[0], true, true); + sgc.PolyLineTo(model.leaves, true, true); } - dc.DrawGeometry(hullBrush, null, hull); + dc.DrawGeometry(hullBrush, hullPen, hull); } // Persist update. - t.leaves.Clear(); - t.parents.Clear(); - foreach (var l in newLeaves) t.leaves.Add(l); - foreach (var kvp in newParents) t.parents[kvp.Key] = kvp.Value; + model.leaves.Clear(); + model.parents.Clear(); + foreach (var l in newLeaves) model.leaves.Add(l); + foreach (var kvp in newParents) model.parents.Add(kvp.Key, kvp.Value); drawing.Render(dv); + } void saveDrawing() @@ -121,154 +293,58 @@ void saveDrawing() void clearDrawing() { - Raster.Source = new RenderTargetBitmap((int)(Drawing.ActualWidth), (int)(Drawing.ActualHeight), 96.0, 96.0, PixelFormats.Pbgra32); - } - - Color generateColor(Color? baseColor = null, double randomness = 1) - { - var c = Color.FromScRgb(1.0f, (float)rng.NextDouble(), (float)rng.NextDouble(), (float)rng.NextDouble()); - return (baseColor.HasValue) ? baseColor.Value + Color.Multiply(c, (float)randomness) : c; - } - - Image generateIcon() - { - var toolIcon = new Image(); - toolIcon.Source = new RenderTargetBitmap((int)(ToolButton.ActualWidth), (int)(ToolButton.ActualHeight), 96.0, 96.0, PixelFormats.Pbgra32); - var t = initializePaintStroke(new Point(ToolButton.ActualWidth / 2, ToolButton.ActualHeight / 2)); - for (int i = 0; i < 10; ++i) doPaintStroke(t, (RenderTargetBitmap)toolIcon.Source); - return toolIcon; - } - #endregion - - #region Event Handlers - void onWindowLoaded(object s, EventArgs e) - { - App.SetCursorPos(0, 0); - tools = new List { Tools.Splatter, Tools.Flower, Tools.Neuron, Tools.Circle, Tools.Polygon, Tools.Snowflake }; - colors = new List { Colors.Red, Colors.Green, Colors.Blue, Colors.Yellow, Colors.Black, Colors.White }; - rng = new Random(); - (paintTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(33), DispatcherPriority.Normal, (_, __) => doPaintStroke(model, (RenderTargetBitmap)Raster.Source), Dispatcher)).Stop(); - tool = tools.First(); - color = colors.First(); - clearDrawing(); - } - - void onContentRendered(object s, EventArgs e) - { - ColorButton.Background = new SolidColorBrush(color); - ToolButton.Content = generateIcon(); - } - - void onDrawingMouseDown(object s, MouseButtonEventArgs e) - { - var p = e.GetPosition(Drawing); - model = initializePaintStroke(p); - paintTimer.Start(); - } - - void onDrawingMouseUp(object s, MouseButtonEventArgs e) - { - paintTimer.Stop(); - } - - void onDrawingMouseLeave(object s, MouseEventArgs e) - { - paintTimer.Stop(); + Raster.Source = new RenderTargetBitmap((int)ActualWidth, (int)ActualHeight, 96, 96, PixelFormats.Pbgra32); } - void onDrawingMouseMove(object s, MouseEventArgs e) + Color createColor(Color? baseColor = null, double randomness = 1) { - var p = e.GetPosition(Drawing); - Canvas.SetLeft(PaintIndicator, p.X - PaintIndicator.ActualWidth / 2); - Canvas.SetTop(PaintIndicator, p.Y - PaintIndicator.ActualHeight / 2); - if (e.LeftButton != MouseButtonState.Pressed) { paintTimer.Stop(); return; } //TODO - if ((this.model.root - p).Length > 25) model = initializePaintStroke(p); - paintTimer.Start(); - } - - void onStartButtonClick(object s, EventArgs e) - { - var b = s as Button; - b.IsEnabled = false; - b.Visibility = Visibility.Hidden; - Blur.Radius = 0; - GUI.IsEnabled = true; - } + var H = rng.NextDouble(); + var S = 1.0; + var V = 1.0; + byte R, G, B; - void onSaveButtonClick(object s, EventArgs e) - { - if (new DialogWindow("Har du ritat färdigt och vill publicera bilden?", "Ja, jag är färdig.", "Nej, gå tillbaka.", false).DialogResult.Value) + if (S == 0) { - saveDrawing(); - clearDrawing(); - var mw = new MainWindow(); - mw.Loaded += (_, __) => Close(); - mw.Show(); - } else if (new DialogWindow("Vill du bara rensa bilden?", "Ja, jag vill börja om.", "Nej, jag är inte färdig.", false).DialogResult.Value) { - clearDrawing(); + R = (byte)(V * 255); + G = (byte)(V * 255); + B = (byte)(V * 255); } - } - - void onToolButtonClick(object s, EventArgs e) - { - tool = tools[(tools.IndexOf(tool) + 1) % tools.Count]; - (s as Button).Content = generateIcon(); - } - - void onColorButtonClick(object s, EventArgs e) - { - color = colors[(colors.IndexOf(color) + 1) % colors.Count]; - (s as Button).Background = new SolidColorBrush(color); - ToolButton.Content = generateIcon(); - } - - void onRandomButtonClick(object s, EventArgs e) - { - color = generateColor(); - tool = new Tool + else { - BranchCount = rng.Next(1, 100), - BranchLength = rng.Next(1, 100), - BranchStraightness = Math.Sqrt(rng.NextDouble()), - Rotation = rng.NextDouble(), - ColorVariety = rng.NextDouble(), - CenterOpacityVariety = rng.NextDouble(), - EdgesOpacity = rng.NextDouble(), - VerticesOpacityVariety = rng.NextDouble(), - HullOpacityVariety = Math.Pow(rng.NextDouble(), 2), - CenterSize = Math.Sqrt(rng.Next(1, 100)), - EdgesThickness = Math.Sqrt(rng.Next(1, 10)), - VerticesSize = rng.Next(1, 20), - VerticesSquashVariety = Math.Pow(rng.NextDouble(), 2), - }; + var h = (H * 6 == 6) ? 0 : H * 6; + var i = (int)Math.Floor(h); + var d1 = V * (1 - S); + var d2 = V * (1 - S * (h - i)); + var d3 = V * (1 - S * (1 - (h - i))); - (s as Button).Content = generateIcon(); - ToolButton.Content = generateIcon(); - ColorButton.Background = new SolidColorBrush(color); - } + double r, g, b; + switch (i) + { + case 0: r = V; g = d3; b = d1; break; + case 1: r = d2; g = V; b = d1; break; + case 2: r = d1; g = V; b = d3; break; + case 3: r = d1; g = d2; b = V; break; + case 4: r = d3; g = d1; b = V; break; + default: r = V; g = d1; b = d2; break; + } - void onInactivity(object s, EventArgs e) - { - if (new DialogWindow("Vill du rensa bilden och börja om?", "Ja, jag vill börja om.", "Nej, jag vill gå tillbaka.", true).DialogResult.Value) - { - var mw = new MainWindow(); - mw.Loaded += (_, __) => Close(); - mw.Show(); + R = (byte)(r * 255); + G = (byte)(g * 255); + B = (byte)(b * 255); } - } - void onGazePaintStart(object s, EventArgs e) - { - var p = Mouse.GetPosition(Application.Current.MainWindow); //TODO Don't use Application.Current.MainWindow. - model = initializePaintStroke(p); - paintTimer.Start(); + var c = Color.FromRgb(R, G, B); + return (baseColor.HasValue) ? baseColor.Value + Color.Multiply(c, (float)randomness) : c; } - //TODO Implement. - void onGazePaintStop(object s, EventArgs e) + void updateIcons() { - paintTimer.Stop(); + ColorButton.Background = new SolidColorBrush(color); + var toolIcon = new Image(); + toolIcon.Source = new RenderTargetBitmap((int)(ShapeButton.ActualWidth), (int)(ShapeButton.ActualHeight), 96.0, 96.0, PixelFormats.Pbgra32); + var t = createTree(new Point(ShapeButton.ActualWidth / 2, ShapeButton.ActualHeight / 2)); + for (int i = 0; i < 10; ++i) updateDrawing(ref t, (RenderTargetBitmap)toolIcon.Source); + ShapeButton.Content = toolIcon; } - #endregion } } diff --git a/EyePaint/MainWindowViewModel.cs b/EyePaint/MainWindowViewModel.cs deleted file mode 100644 index dccb241..0000000 --- a/EyePaint/MainWindowViewModel.cs +++ /dev/null @@ -1,182 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Media; - -namespace EyePaint -{ - public struct Tool - { - public int BranchCount { get; set; } - public double BranchLength { get; set; } - public double BranchStraightness { get; set; } - public double Rotation { get; set; } - public double ColorVariety { get; set; } - public double CenterOpacityVariety { get; set; } - public double EdgesOpacity { get; set; } - public double VerticesOpacityVariety { get; set; } - public double HullOpacityVariety { get; set; } - public double CenterSize { get; set; } - public double EdgesThickness { get; set; } - public double VerticesSize { get; set; } - public double VerticesSquashVariety { get; set; } - } - - // Presets - // TODO Don't initialize new objects every access. - public class Tools - { - public static Tool Splatter - { - get - { - return new Tool - { - BranchCount = 10, - BranchLength = 20, - BranchStraightness = 0.5, - Rotation = 0.5, - ColorVariety = 0.5, - CenterOpacityVariety = 0, - EdgesOpacity = 0, - VerticesOpacityVariety = 1, - HullOpacityVariety = 0, - CenterSize = 0, - EdgesThickness = 0, - VerticesSize = 20, - VerticesSquashVariety = 0, - }; - } - } - - public static Tool Flower - { - get - { - return new Tool - { - BranchCount = 50, - BranchLength = 15, - BranchStraightness = 1, - Rotation = 1, - ColorVariety = 0.25, - CenterOpacityVariety = 0.1, - EdgesOpacity = 1, - VerticesOpacityVariety = 0.75, - HullOpacityVariety = 0, - CenterSize = 25, - EdgesThickness = 2, - VerticesSize = 5, - VerticesSquashVariety = 0, - }; - } - } - - public static Tool Neuron - { - get - { - return new Tool - { - BranchCount = 100, - BranchLength = 50, - BranchStraightness = 0.8, - Rotation = 0, - ColorVariety = 0, - CenterOpacityVariety = 0.01, - EdgesOpacity = 1, - VerticesOpacityVariety = 0, - HullOpacityVariety = 0, - CenterSize = 100, - EdgesThickness = 2, - VerticesSize = 0, - VerticesSquashVariety = 0, - }; - } - } - - public static Tool Circle - { - get - { - return new Tool - { - BranchCount = 100, - BranchLength = 10, - BranchStraightness = 1, - Rotation = 0.1, - ColorVariety = 0.25, - CenterOpacityVariety = 0.25, - EdgesOpacity = 0.75, - VerticesOpacityVariety = 0, - HullOpacityVariety = 0, - CenterSize = 10, - EdgesThickness = 5, - VerticesSize = 0, - VerticesSquashVariety = 0, - }; - } - } - - public static Tool Polygon - { - get - { - return new Tool - { - BranchCount = 10, - BranchLength = 10, - BranchStraightness = 0.9, - Rotation = 0.25, - ColorVariety = 0.5, - CenterOpacityVariety = 0, - EdgesOpacity = 0, - VerticesOpacityVariety = 0, - HullOpacityVariety = 0.2, - CenterSize = 0, - EdgesThickness = 0, - VerticesSize = 0, - VerticesSquashVariety = 0, - }; - } - } - - public static Tool Snowflake - { - get - { - return new Tool - { - BranchCount = 100, - BranchLength = 25, - BranchStraightness = 1, - Rotation = 1, - ColorVariety = 0, - CenterOpacityVariety = 0, - EdgesOpacity = 1, - VerticesOpacityVariety = 0, - HullOpacityVariety = 0.5, - CenterSize = 0, - EdgesThickness = 1, - VerticesSize = 0, - VerticesSquashVariety = 0, - }; - } - } - } - - public struct Tree - { - public Point root; - public PointCollection leaves; - public Dictionary parents; - } - - class MainWindowViewModel - { - } -} diff --git a/EyePaint/Presets.json b/EyePaint/Presets.json new file mode 100644 index 0000000..ab2606c --- /dev/null +++ b/EyePaint/Presets.json @@ -0,0 +1 @@ +{"colors":[{"A":255,"B":0,"G":0,"R":255},{"A":255,"B":255,"G":0,"R":0},{"A":255,"B":0,"G":128,"R":0},{"A":255,"B":255,"G":255,"R":255},{"A":255,"B":0,"G":0,"R":0},{"A":255,"B":0,"G":37,"R":255}],"shapes":[{"branches":81,"centerOpacityVariety":0.0038610836508968303,"centerSize":9.8488578017961039,"colorVariety":0.34457415404942549,"edgesOpacity":0.82982405825975536,"hullOpacityVariety":0.17239927408090727,"rotation":0.412062909645989,"step":17,"straightness":0.93779453004143254,"strokeThickness":1,"verticesOpacityVariety":0.49084009206427265,"verticesSize":3.7416573867739413,"verticesSquashVariety":0.810641031988667},{"branches":42,"centerOpacityVariety":0.72861201536311393,"centerSize":6.7823299831252681,"colorVariety":0.84080574607514114,"edgesOpacity":0.38694085291910024,"hullOpacityVariety":0.32440319310475879,"rotation":0.53226692347427218,"step":24,"straightness":0.13494823187984151,"strokeThickness":2.6457513110645907,"verticesOpacityVariety":0.42618670893189808,"verticesSize":4,"verticesSquashVariety":0.175224644005976}]} \ No newline at end of file diff --git a/EyePaint/Properties/Resources.Designer.cs b/EyePaint/Properties/Resources.Designer.cs index 5213e9a..74656fc 100644 --- a/EyePaint/Properties/Resources.Designer.cs +++ b/EyePaint/Properties/Resources.Designer.cs @@ -69,5 +69,15 @@ internal static byte[] itc_officina_sans_std_bold { return ((byte[])(obj)); } } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] Presets { + get { + object obj = ResourceManager.GetObject("Presets", resourceCulture); + return ((byte[])(obj)); + } + } } } diff --git a/EyePaint/Properties/Resources.resx b/EyePaint/Properties/Resources.resx index 35b8dfa..f56975f 100644 --- a/EyePaint/Properties/Resources.resx +++ b/EyePaint/Properties/Resources.resx @@ -119,6 +119,9 @@ - ..\Resources\itc_officina_sans_std_bold.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ..\itc_officina_sans_std_bold.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\presets.json;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 \ No newline at end of file diff --git a/EyePaint/Resources/itc_officina_sans_std_bold.ttf b/EyePaint/itc_officina_sans_std_bold.ttf similarity index 100% rename from EyePaint/Resources/itc_officina_sans_std_bold.ttf rename to EyePaint/itc_officina_sans_std_bold.ttf From 0228f37dad85bcfef9395d78627ad3f9cfed19d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20Thom=C3=A9?= Date: Sun, 24 Aug 2014 21:17:37 +0200 Subject: [PATCH 07/10] Create README.md --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..86124d2 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +#EyePaint +## Installationsinstruktioner +1. Programmet kräver en Tobii REX eye tracker och dess tillhörande drivrutiner (http://developer.tobii.com/downloads/). + 1. Installera EyeX ramverket och följ instruktionerna. + 2. Genomför en grundkalibrering. + 3. Stäng av både ögonindikatorn och ögonstyrningen i Tobiis kontrollpanel. Måla med ögonen programmet kommunicerar direkt med eye trackern istället. +2. Hårdvaruknapparna ska kopplas enligt: + 1. Målaknappen -> Vänstermusklick + 2. Omstartsknappen -> Tangentbordsknappen 's' +3. Programmet behöver konfigureras efter installationen. Starta programmet och tryck ESC. Fyll i korrekt information i fälten och spara. Konfigurationen behöver bara göras en gång per installation. +4. Kom ihåg att stänga av Windows Update, skärmsläckare, sovlägen m.m. på operativsystemsnivå, samt se till att applikationen autostartar med Windows. +5. Applikationen kräver en internetuppkoppling för bilduppladdningen samt felhanteringsrutinen som använder epost. From 1f3dbad4ce40ba9ebf2caafcd46ee1c7356da7a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20Thom=C3=A9?= Date: Tue, 26 Aug 2014 21:54:49 +0200 Subject: [PATCH 08/10] Prototype 6 --- EyePaint/App.config | 27 ++ EyePaint/App.xaml | 24 +- EyePaint/App.xaml.cs | 192 +++++--- EyePaint/CountdownWindow.xaml | 51 ++ EyePaint/CountdownWindow.xaml.cs | 28 ++ EyePaint/DialogWindow.xaml | 44 -- EyePaint/DialogWindow.xaml.cs | 52 -- EyePaint/ErrorWindow.xaml | 18 +- EyePaint/ErrorWindow.xaml.cs | 24 +- EyePaint/EyePaint.csproj | 27 +- EyePaint/FlickrNet.chm | Bin 0 -> 4961596 bytes EyePaint/MainWindow.xaml | 114 +++-- EyePaint/MainWindow.xaml.cs | 446 +++++++++++------- EyePaint/Presets.json | 1 - EyePaint/Properties/Resources.Designer.cs | 22 +- EyePaint/Properties/Resources.resx | 7 - EyePaint/Properties/Settings.Designer.cs | 74 ++- EyePaint/Properties/Settings.settings | 27 +- EyePaint/SettingsWindow.xaml | 16 + EyePaint/SettingsWindow.xaml.cs | 23 + EyePaint/itc_officina_sans_std_bold.ttf | Bin 83984 -> 0 bytes EyePaint/packages.config | 4 + .../FlickrNet.3.14.0/FlickrNet.3.14.0.nupkg | Bin 0 -> 5000335 bytes .../FlickrNet.3.14.0/FlickrNet.3.14.0.nuspec | 16 + .../FlickrNet.3.14.0/content/FlickrNet.chm | Bin 0 -> 4961596 bytes .../FlickrNet.3.14.0/lib/net20/FlickrNet.dll | Bin 0 -> 397312 bytes packages/repositories.config | 4 + 27 files changed, 812 insertions(+), 429 deletions(-) create mode 100644 EyePaint/CountdownWindow.xaml create mode 100644 EyePaint/CountdownWindow.xaml.cs delete mode 100644 EyePaint/DialogWindow.xaml delete mode 100644 EyePaint/DialogWindow.xaml.cs create mode 100644 EyePaint/FlickrNet.chm delete mode 100644 EyePaint/Presets.json create mode 100644 EyePaint/SettingsWindow.xaml create mode 100644 EyePaint/SettingsWindow.xaml.cs delete mode 100644 EyePaint/itc_officina_sans_std_bold.ttf create mode 100644 EyePaint/packages.config create mode 100644 packages/FlickrNet.3.14.0/FlickrNet.3.14.0.nupkg create mode 100644 packages/FlickrNet.3.14.0/FlickrNet.3.14.0.nuspec create mode 100644 packages/FlickrNet.3.14.0/content/FlickrNet.chm create mode 100644 packages/FlickrNet.3.14.0/lib/net20/FlickrNet.dll create mode 100644 packages/repositories.config diff --git a/EyePaint/App.config b/EyePaint/App.config index d0feca6..46e68b4 100644 --- a/EyePaint/App.config +++ b/EyePaint/App.config @@ -1,6 +1,33 @@ + + +
+ + + + + + default@default.default + + + default.default.default + + + a81591b96318cd9799982c11663df750 + + + 05e84b538d7d4cf5 + + + 20 + + + 50 + + + diff --git a/EyePaint/App.xaml b/EyePaint/App.xaml index 6424ef0..c2c98b6 100644 --- a/EyePaint/App.xaml +++ b/EyePaint/App.xaml @@ -1,36 +1,24 @@  + Startup="onStartup"> - - - - - - - - - - + - + - + - + diff --git a/EyePaint/App.xaml.cs b/EyePaint/App.xaml.cs index 8ec392c..60b4ad9 100644 --- a/EyePaint/App.xaml.cs +++ b/EyePaint/App.xaml.cs @@ -1,45 +1,51 @@ using System; using System.Collections.Generic; -using System.Configuration; -using System.Data; using System.Linq; +using System.Net; using System.Net.Mail; using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; -using System.Windows.Media; using System.Windows.Media.Animation; using Tobii.Gaze.Core; namespace EyePaint { + public class TrackingStatusChangedEventArgs : EventArgs + { + public bool Tracking; + } + /// /// Interaction logic for App.xaml. /// public partial class App : Application { IEyeTracker iet; - List gazePoints = new List(); - Dictionary calibrationPoints = new Dictionary(); + int width, height; + List gazes; +#if DEBUG + public Dictionary offsets; +#else + Dictionary offsets; +#endif TimeSpan? time; + public event EventHandler TrackingStatusChanged; + int notTracking; [DllImport("User32.dll")] static public extern bool SetCursorPos(int X, int Y); + /// + /// Connects to the eye tracker on application startup. + /// void onStartup(object s, StartupEventArgs e) { - connect(); - } - - void onEyeTrackerError(object s, EyeTrackerErrorEventArgs e) - { - connect(); - } + gazes = new List(); + offsets = new Dictionary(); - void connect() - { try { Uri url = new EyeTrackerCoreLibrary().GetConnectedEyeTracker(); @@ -49,75 +55,98 @@ void connect() Task.Factory.StartNew(() => iet.RunEventLoop()); iet.Connect(); iet.StartTracking(); + Mouse.OverrideCursor = Cursors.None; } catch (EyeTrackerException) { - //error(); + MessageBox.Show("Kameran verkar ha gått sönder. Prova att starta om datorn."); + Application.Current.Shutdown(); } catch (NullReferenceException) { - //error(); + MessageBox.Show("Kameran verkar saknas. Säkerställ att den är inkopplad och prova att starta om datorn."); + Application.Current.Shutdown(); + } + finally + { + MainWindow = new MainWindow(); + MainWindow.Show(); + width = (int)MainWindow.ActualWidth; + height = (int)MainWindow.ActualHeight; } } - void error() - { - //TODO Implement email error report. - /* - string to = "foo@.com"; - string from = "bar@.com"; - string subject = "Test subject"; - string body = "Test message."; - MailMessage message = new MailMessage(from, to, subject, body); - SmtpClient client = new SmtpClient(server); - client.Credentials = CredentialCache.DefaultNetworkCredentials; - client.Send(message); - */ - (new ErrorWindow()).Show(); - } - - void onGazeData(object s, GazeDataEventArgs e) + /// + /// Handles error data from the eye tracker. The application will be halted, the user will see an error on the screen, and a notification email is sent to the museum's technical staff. + /// + void onEyeTrackerError(object s, EyeTrackerErrorEventArgs e) { - if (e.GazeData.TrackingStatus == TrackingStatus.NoEyesTracked) return; - var left = (e.GazeData.TrackingStatus == TrackingStatus.OneEyeTrackedProbablyLeft || e.GazeData.TrackingStatus == TrackingStatus.OnlyLeftEyeTracked || e.GazeData.TrackingStatus == TrackingStatus.OneEyeTrackedUnknownWhich) ? e.GazeData.Left.GazePointOnDisplayNormalized : new Point2D(0, 0); - var right = (e.GazeData.TrackingStatus == TrackingStatus.OneEyeTrackedProbablyRight || e.GazeData.TrackingStatus == TrackingStatus.OnlyRightEyeTracked || e.GazeData.TrackingStatus == TrackingStatus.OneEyeTrackedUnknownWhich) ? e.GazeData.Right.GazePointOnDisplayNormalized : new Point2D(0, 0); - - var newGazePoint = Dispatcher.Invoke(() => + new ErrorWindow(); + try { - return new Point( - (int)Application.Current.MainWindow.ActualWidth * (left.X + right.X) / 2, - (int)Application.Current.MainWindow.ActualHeight * (left.Y + right.Y) / 2 + var message = new MailMessage( + EyePaint.Properties.Settings.Default.AdminEmail, + EyePaint.Properties.Settings.Default.AdminEmail, + "Tekniskt problem med stationen Måla med ögonen", + "Kameran verkar ha gått sönder. Prova att starta om datorn." ); - }); - gazePoints.Add(newGazePoint); - - while (gazePoints.Count > 50) gazePoints.RemoveAt(0); - - // Reduce noise by averaging incoming gaze points. - var gazePoint = new Point( - gazePoints.Average(p => p.X), - gazePoints.Average(p => p.Y) - ); - - if ((newGazePoint - gazePoint).Length > 100) + var client = new SmtpClient(EyePaint.Properties.Settings.Default.SmtpServer); + client.Credentials = CredentialCache.DefaultNetworkCredentials; + client.Send(message); + } + catch (Exception) { - gazePoints.Clear(); - gazePoints.Add(newGazePoint); - gazePoint = newGazePoint; + MessageBox.Show("Felmeddelande kunde ej skickas via epost. Kameran verkar ha gått sönder. Prova att starta om datorn."); } + } - // Calibrate point with known offsets. - var distances = calibrationPoints.Select(kvp => (gazePoint - kvp.Key).Length); - var normalizedDistances = distances.Select(d => d / distances.Sum()); - foreach (var v in calibrationPoints.Zip(normalizedDistances, (kvp, d) => kvp.Value)) //TODO Scale by normalized distance. + /// + /// Handles data from the eye tracker. + /// + void onGazeData(object s, GazeDataEventArgs e) + { + if (e.GazeData.TrackingStatus == TrackingStatus.NoEyesTracked) { - gazePoint.Offset(v.X, v.Y); + // Determine if the user is inactive. + if (++notTracking > 20 && TrackingStatusChanged != null) TrackingStatusChanged(this, new TrackingStatusChangedEventArgs { Tracking = false }); } + else + { + // Determine if the user reactived the session. + if (notTracking > 0) + { + if (TrackingStatusChanged != null) TrackingStatusChanged(this, new TrackingStatusChangedEventArgs { Tracking = true }); + notTracking = 0; + } - SetCursorPos((int)gazePoint.X, (int)gazePoint.Y); + // Retrieve available gaze information from the eye tracker. + var left = (e.GazeData.TrackingStatus == TrackingStatus.BothEyesTracked || e.GazeData.TrackingStatus == TrackingStatus.OneEyeTrackedProbablyLeft || e.GazeData.TrackingStatus == TrackingStatus.OnlyLeftEyeTracked || e.GazeData.TrackingStatus == TrackingStatus.OneEyeTrackedUnknownWhich) ? e.GazeData.Left.GazePointOnDisplayNormalized : new Point2D(0, 0); + var right = (e.GazeData.TrackingStatus == TrackingStatus.BothEyesTracked || e.GazeData.TrackingStatus == TrackingStatus.OneEyeTrackedProbablyRight || e.GazeData.TrackingStatus == TrackingStatus.OnlyRightEyeTracked || e.GazeData.TrackingStatus == TrackingStatus.OneEyeTrackedUnknownWhich) ? e.GazeData.Right.GazePointOnDisplayNormalized : new Point2D(0, 0); + + // Determine new gaze point position on the screen. + var newGazePoint = new Point(width * (left.X + right.X) / 2, height * (left.Y + right.Y) / 2); + gazes.Add(newGazePoint); + + // Calculate average gaze point (i.e. naive noise reduction). + while (gazes.Count > EyePaint.Properties.Settings.Default.Stability) gazes.RemoveAt(0); + var gazePoint = new Point(gazes.Average(p => p.X), gazes.Average(p => p.Y)); + + // Calibrate average gaze point with known offsets. +#if !DEBUG + //TODO Fix offset interpolation bug. + if (offsets.Count == 1) gazePoint += offsets.Values.First(); + var distances = offsets.Select(kvp => (gazePoint - kvp.Key).Length); + var distancesRatios = distances.Select(d => d / distances.Sum()); + foreach (var v in offsets.Values.Zip(distancesRatios, (o, d) => (1 - d) * o)) gazePoint += v; +#endif + // Place the mouse cursor at the gaze point. + SetCursorPos((int)gazePoint.X, (int)gazePoint.Y); + } } - //TODO Fix gaze click sequence bug. + /// + /// Gaze click event for the GazeButton control template. + /// void onGazeClick(object s, EventArgs e) { var c = s as Clock; @@ -125,15 +154,15 @@ void onGazeClick(object s, EventArgs e) if (time.HasValue && c.CurrentTime.HasValue && c.CurrentTime.Value < time.Value) { // Find button. - var activeWindow = Application.Current.Windows.OfType().SingleOrDefault(x => x.IsActive); + var activeWindow = Application.Current.Windows.OfType().Single(x => x.IsActive); var focusedButton = FocusManager.GetFocusedElement(activeWindow) as Button; // Store calibration offset. - if (gazePoints.Count > 0) + if (gazes.Count > 0) { var expectedPoint = focusedButton.PointToScreen(new Point(focusedButton.ActualWidth / 2, focusedButton.ActualHeight / 2)); var actualPoint = Mouse.GetPosition(activeWindow); - calibrationPoints[actualPoint] = expectedPoint - actualPoint; + offsets[actualPoint] = expectedPoint - actualPoint; } // Raise click event. @@ -142,5 +171,36 @@ void onGazeClick(object s, EventArgs e) time = (c.CurrentState == ClockState.Active) ? c.CurrentTime : null; } + + /// + /// Clear previous gaze data. + /// + public void Clear() + { + TrackingStatusChanged = null; + gazes.Clear(); + offsets.Clear(); + time = null; + notTracking = 0; + } + + /// + /// Restart main window. + /// + public void Restart() + { + var mw = new MainWindow(); + mw.ContentRendered += (s, e) => { MainWindow.Close(); MainWindow = mw; }; + mw.Show(); + } + + /// + /// Clear previous gaze data and restart the main window. + /// + public void Reset() + { + Clear(); + Restart(); + } } } diff --git a/EyePaint/CountdownWindow.xaml b/EyePaint/CountdownWindow.xaml new file mode 100644 index 0000000..46b4df3 --- /dev/null +++ b/EyePaint/CountdownWindow.xaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EyePaint/CountdownWindow.xaml.cs b/EyePaint/CountdownWindow.xaml.cs new file mode 100644 index 0000000..2042cb8 --- /dev/null +++ b/EyePaint/CountdownWindow.xaml.cs @@ -0,0 +1,28 @@ +using System; +using System.Windows; + +namespace EyePaint +{ + /// + /// Used to display a countdown to the user before returning a dialog result. The user can abort the countdown with a keypress. + /// + public partial class CountdownWindow : Window + { + public CountdownWindow() + { + InitializeComponent(); + ShowDialog(); + } + + void onConfirm(object s, EventArgs e) + { + //TODO Fix repeated restart presses bug. + DialogResult = true; + } + + void onCancel(object s, EventArgs e) + { + DialogResult = false; + } + } +} diff --git a/EyePaint/DialogWindow.xaml b/EyePaint/DialogWindow.xaml deleted file mode 100644 index 1432557..0000000 --- a/EyePaint/DialogWindow.xaml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + diff --git a/EyePaint/MainWindow.xaml.cs b/EyePaint/MainWindow.xaml.cs index 595f7ae..9f95b30 100644 --- a/EyePaint/MainWindow.xaml.cs +++ b/EyePaint/MainWindow.xaml.cs @@ -1,190 +1,254 @@ -using System; +using FlickrNet; +using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Runtime.Serialization.Json; using System.Windows; using System.Windows.Controls; +using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Media.Imaging; using System.Windows.Threading; -using System.Xml; namespace EyePaint { - public struct Tree - { - public Point root; - public PointCollection leaves; - public Dictionary parents; - } - - public struct Shape - { - public int maxBranches, strokeThickness; - public double branchStepLength, branchStraightness, generationRotation, colorVariety, verticesSize, verticesSquashVariety, centerSize, centerOpacity, edgesOpacity, verticesOpacity, hullOpacity; - } - - public class Presets + /// + /// Model representing a paint stroke in the application. + /// + struct Tree { - public HashSet shapes; - public HashSet colors; + public Point Root; + public PointCollection Leaves; + public Dictionary Parents; - public static Presets Load() + public Tree(Point p, int maxBranches, ref Random rng) { - using (var fs = new FileStream("Presets.json", FileMode.Open, FileAccess.Read, FileShare.None)) - { - using (var jrwf = JsonReaderWriterFactory.CreateJsonReader(fs, XmlDictionaryReaderQuotas.Max)) - { - return (Presets)(new DataContractJsonSerializer(typeof(Presets))).ReadObject(jrwf); - } - } + Root = p; + Leaves = new PointCollection(); + Parents = new Dictionary(); + for (int i = 0; i < rng.Next((maxBranches + 1) / 2, maxBranches + 1); ++i) Leaves.Add(Root); + Parents[Root] = Root; } + } - public void Save() - { - using (var fs = new FileStream("Presets.json", FileMode.Create, FileAccess.Write, FileShare.None)) - { - using (var xdw = JsonReaderWriterFactory.CreateJsonWriter(fs)) - { - (new DataContractJsonSerializer(GetType())).WriteObject(xdw, this); - } - } - } + /// + /// Graphical interpretation parameters of paint strokes in the application. + /// + struct Shape + { + //TODO Add getters/setters with validation instead of using the public access modifier. + public double MaxBranches, BranchStepLength, BranchStraightness, GenerationRotation, ColorVariety, VerticesSize, VerticesSquashVariety, CenterSize, CenterOpacity, EdgesOpacity, VerticesOpacity, HullOpacity; } /// - /// Interaction logic for MainWindow.xaml + /// Lets the user draw different shapes and colors. /// public partial class MainWindow : Window { - Random rng; - DateTime time; - TimeSpan timePainted; + Random rng = new Random(); DispatcherTimer paintTimer; Tree model; Point gaze; Shape shape; Color color; - Presets presets; + DateTime time; + HashSet shapes = new HashSet(); + HashSet colors = new HashSet(); + Dictionary shapeUsage = new Dictionary(); + Dictionary colorUsage = new Dictionary(); public MainWindow() { InitializeComponent(); -#if DEBUG - var p = new Presets(); - p.colors = new HashSet { Colors.Red, Colors.Blue, Colors.Green, Colors.Yellow, Colors.White, Colors.Black }; - p.shapes = new HashSet { new Shape { maxBranches = 100, strokeThickness = 1, branchStepLength = 10, branchStraightness = 1, generationRotation = 1, colorVariety = 1, verticesSize = 10, verticesSquashVariety = 0, centerSize = 100, centerOpacity = 1, edgesOpacity = 1, verticesOpacity = 1, hullOpacity = 1 } }; - p.Save(); - KeyDown += (s, e) => { presets.shapes.Add(shape); presets.Save(); }; -#endif } void onContentRendered(object s, EventArgs e) { - App.SetCursorPos(0, 0); - rng = new Random(); - presets = Presets.Load(); - color = presets.colors.ElementAt(rng.Next(presets.colors.Count)); - shape = presets.shapes.ElementAt(rng.Next(presets.shapes.Count)); - (paintTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(33), DispatcherPriority.Normal, (_, __) => updateDrawing(ref model, (RenderTargetBitmap)Raster.Source), Dispatcher)).Stop(); - clearDrawing(); - updateIcons(); + // Choose initial shape and color by simulating a button click. + ShapeButton.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent)); + ColorButton.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent)); + + // Initialize drawing. + Raster.Source = createDrawing((int)ActualWidth, (int)ActualHeight); + + // Render clock. Note: single-threaded. + (paintTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(50), DispatcherPriority.Render, (_, __) => paint(ref this.model, Raster.Source as RenderTargetBitmap), Dispatcher)).Stop(); } - void onPreviewMouseDown(object s, MouseButtonEventArgs e) + void onFadedIn(object s, EventArgs e) { - ((Storyboard)FindResource("InactivityAnimation")).Seek(TimeSpan.Zero); +#if !DEBUG + // Disable functionality if the user isn't in front of the eye tracker. + ((App)Application.Current).TrackingStatusChanged += (_s, _e) => Dispatcher.Invoke(() => + { + if (_e.Tracking) IsEnabled = true; + }); +#else + IsEnabled = true; +#endif + } + + void onKeyDown(object s, KeyEventArgs e) + { + switch (e.Key) + { + case Key.Escape: + (new SettingsWindow()).ShowDialog(); + break; + case Key.S: + // Save drawing, but only if the user has painted enough stuff. + if (shapeUsage.Sum(sh => sh.Value.Seconds) > 5) + { + //TODO Implement yes/no gaze dialog. + if (new CountdownWindow().DialogResult.Value) + { + ((App)Application.Current).Clear(); + PaintControls.IsEnabled = false; + stopPainting(); + ((Storyboard)FindResource("SaveDrawingAnimation")).Begin(); + } + } + else + { + // Reset application for the next user. + ((App)Application.Current).Reset(); + } + + break; + } } void onCanvasMouseDown(object s, MouseButtonEventArgs e) { gaze = e.GetPosition(s as Canvas); startPainting(); - ((Storyboard)GazePaintMarker.FindResource("GazePaintAnimation")).Stop(); + ((Storyboard)GazeMarker.FindResource("GazePaintAnimation")).Stop(); } void onCanvasMouseUp(object s, MouseButtonEventArgs e) { stopPainting(); - ((Storyboard)GazePaintMarker.FindResource("GazePaintAnimation")).Begin(); + ((Storyboard)GazeMarker.FindResource("GazePaintAnimation")).Begin(); } void onCanvasMouseEnter(object s, MouseEventArgs e) { - ((Storyboard)GazePaintMarker.FindResource("GazePaintAnimation")).Begin(); + ((Storyboard)GazeMarker.FindResource("GazePaintAnimation")).Begin(); } void onCanvasMouseLeave(object s, MouseEventArgs e) { stopPainting(); - ((Storyboard)GazePaintMarker.FindResource("GazePaintAnimation")).Stop(); + ((Storyboard)GazeMarker.FindResource("GazePaintAnimation")).Stop(); } void onCanvasMouseMove(object s, MouseEventArgs e) { var p = e.GetPosition(s as Canvas); - ((Storyboard)FindResource("InactivityAnimation")).Seek(TimeSpan.Zero); - Canvas.SetLeft(GazePaintMarker, p.X - GazePaintMarker.ActualWidth / 2); - Canvas.SetTop(GazePaintMarker, p.Y - GazePaintMarker.ActualHeight / 2); - if ((gaze - p).Length > 50) + Canvas.SetLeft(GazeMarker, p.X - GazeMarker.ActualWidth / 2); + Canvas.SetTop(GazeMarker, p.Y - GazeMarker.ActualHeight / 2); + + if (paintTimer.IsEnabled && (model.Root - p).Length > EyePaint.Properties.Settings.Default.Spacing) model = new Tree(p, (int)shape.MaxBranches, ref rng); + if ((gaze - p).Length > EyePaint.Properties.Settings.Default.Spacing / 2) { - gaze = p; - if (paintTimer.IsEnabled) model = createTree(gaze); - var sb = (Storyboard)GazePaintMarker.FindResource("GazePaintAnimation"); - if (sb.GetCurrentState() == ClockState.Filling) stopPainting(); - if (sb.GetCurrentState() != ClockState.Stopped) sb.Seek(TimeSpan.Zero); + var sb = (Storyboard)GazeMarker.FindResource("GazePaintAnimation"); + switch (sb.GetCurrentState()) + { + case ClockState.Active: sb.Seek(TimeSpan.Zero); break; + case ClockState.Filling: sb.Seek(TimeSpan.Zero); stopPainting(); break; + } } - if (!paintTimer.IsEnabled) gaze = p; + gaze = p; } void onStartButtonClick(object s, EventArgs e) { (s as Button).Visibility = Visibility.Hidden; - ((Storyboard)FindResource("InactivityAnimation")).Begin(); - Blur.Radius = 0; - PaintControls.IsEnabled = true; +#if !DEBUG + ((App)Application.Current).TrackingStatusChanged += (_s, _e) => Dispatcher.Invoke(() => PaintControls.IsEnabled = _e.Tracking); +#endif } void onShapeButtonClick(object s, EventArgs e) { - if (timePainted > TimeSpan.FromSeconds(10)) presets.shapes.Add(shape); - timePainted = TimeSpan.Zero; - var candidates = presets.shapes.Where(c => !c.Equals(shape)).ToList(); - shape = (rng.NextDouble() <= 0.5 && candidates.Count > 1) ? candidates.ElementAt(rng.Next(candidates.Count)) : new Shape + // Sort shapes by usage. + shapes.OrderBy(sh => shapeUsage[sh]); + + // Remove underused shapes. + foreach (var sh in shapes.ToList()) + { + if (shapeUsage[sh].Seconds < 0.1 * shapeUsage.Max(kvp => kvp.Value).Seconds || shapeUsage[sh] == TimeSpan.Zero) + { + shapes.Remove(sh); + shapeUsage.Remove(sh); + } + } + + // Determine whether to pick a previous shape or generate a new. + if (shapes.Count > 0 && rng.NextDouble() <= 0.01 * shapes.Count - 0.1) + { + // Pick a previously used shape. + shape = shapes.ElementAt((shapes.ToList().IndexOf(shape) + 1) % shapes.Count); //TODO Verify that the index applies to the HashSet. + } + else { - maxBranches = rng.Next(1, 101), - strokeThickness = (int)Math.Sqrt(rng.Next(1, 10)), - branchStepLength = Math.Sqrt(rng.Next(10, 101)), - branchStraightness = Math.Sqrt(rng.NextDouble()), - generationRotation = rng.NextDouble(), - colorVariety = rng.NextDouble(), - verticesSize = Math.Sqrt(rng.Next(1, 51)), - verticesSquashVariety = Math.Pow(rng.NextDouble(), 2), - centerSize = Math.Sqrt(rng.Next(1, 101)), - centerOpacity = Math.Pow(rng.NextDouble(), 2), - edgesOpacity = rng.NextDouble(), - verticesOpacity = rng.NextDouble(), - hullOpacity = Math.Pow(rng.NextDouble(), 2), - }; + // Generate a new shape. + var maxBranches = rng.Next(1, 100); + var branchStepLength = Math.Sqrt(rng.Next(1, 1001)); + var branchStraightness = Math.Sqrt(rng.NextDouble()); + var generationRotation = rng.NextDouble(); + var colorVariety = rng.NextDouble(); + var verticesSize = Math.Sqrt(rng.Next(0, 101)); + var verticesSquashVariety = rng.NextDouble() * rng.NextDouble(); + var centerSize = (branchStepLength < 10) ? rng.Next(10, 101) : Math.Sqrt(rng.Next(1, 101)); + var centerOpacity = Math.Max(0, rng.NextDouble() * (rng.NextDouble() - centerSize / 100d)); + var edgesOpacity = rng.NextDouble() * rng.NextDouble() * branchStraightness; + var verticesOpacity = (verticesSize == 0) ? 0 : rng.NextDouble(); + var hullOpacity = rng.NextDouble() * branchStraightness * generationRotation; + var sumOpacity = centerOpacity + edgesOpacity + verticesOpacity + hullOpacity; + centerOpacity /= sumOpacity; edgesOpacity /= sumOpacity; verticesOpacity /= sumOpacity; hullOpacity /= sumOpacity; + shape = new Shape { MaxBranches = maxBranches, BranchStepLength = branchStepLength, BranchStraightness = branchStraightness, GenerationRotation = generationRotation, ColorVariety = colorVariety, VerticesSize = verticesSize, VerticesSquashVariety = verticesSquashVariety, CenterSize = centerSize, CenterOpacity = centerOpacity, EdgesOpacity = edgesOpacity, VerticesOpacity = verticesOpacity, HullOpacity = hullOpacity }; + } + + // Add shape. + shapes.Add(shape); + shapeUsage[shape] = TimeSpan.Zero; + + // Update GUI. updateIcons(); } void onColorButtonClick(object s, EventArgs e) { - if (timePainted > TimeSpan.FromSeconds(10)) presets.colors.Add(color); - timePainted = TimeSpan.Zero; - var candidates = presets.colors.Where(c => c != color).ToList(); - color = (rng.NextDouble() <= 0.5 && candidates.Count > 1) ? candidates.ElementAt(rng.Next(candidates.Count)) : createColor(); + // Sort colors by usage. + colors.OrderBy(c => colorUsage[c]); + + // Remove underused colors. + foreach (var c in colors.ToList()) + { + if (colorUsage[c].Seconds < 0.1 * colorUsage.Max(kvp => kvp.Value).Seconds || colorUsage[c] == TimeSpan.Zero) + { + colors.Remove(c); + colorUsage.Remove(c); + } + } + + // Either pick a previously used color or generate a new, based on how many previously used colors there are. + color = (colors.Count > 0 && rng.NextDouble() <= 0.01 * colors.Count - 0.1) ? colors.ElementAt((colors.ToList().IndexOf(color) + 1) % colors.Count) : createColor(); //TODO Verify that the index applies to the HashSet. + + // Add color. + colors.Add(color); + colorUsage[color] = TimeSpan.Zero; + + // Update GUI. updateIcons(); } void onInactivity(object s, EventArgs e) { - (new MainWindow()).Show(); - Close(); + if (shapeUsage.Sum(sh => sh.Value.Seconds) > 60) ((Storyboard)FindResource("SaveDrawingAnimation")).Begin(); + else ((App)Application.Current).Reset(); } void onGazePaint(object s, EventArgs e) @@ -192,9 +256,41 @@ void onGazePaint(object s, EventArgs e) startPainting(); } + void onPaintControlsIsEnabledChanged(object s, DependencyPropertyChangedEventArgs e) + { + /* + var sb = ((Storyboard)FindResource("InactivityAnimation")); + if (PaintControls == null || sb == null) return; + if (!PaintControls.IsEnabled) + { + stopPainting(); + sb.Begin(); + } + else sb.Stop(); + */ + } + + void onSaveDrawing(object s, EventArgs e) + { + var pbe = new PngBitmapEncoder(); + pbe.Frames.Add(BitmapFrame.Create(Raster.Source as RenderTargetBitmap)); + using (var fs = System.IO.File.OpenWrite("image.png")) pbe.Save(fs); + //TODO Backend upload. + //var f = new Flickr(EyePaint.Properties.Settings.Default.FlickrKey, EyePaint.Properties.Settings.Default.FlickrSecret); + //var requestToken = f.OAuthGetRequestToken("oob"); + //System.Diagnostics.Process.Start(f.OAuthCalculateAuthorizationUrl(requestToken.Token, AuthLevel.Write)); + //var accessToken = f.OAuthGetAccessToken(requestToken, VerifierTextBox.Text); + //f.OAuthAccessToken = ""; + //f.OAuthAccessTokenSecret = ""; + //f.UploadPicture("image.png"); + ((App)Application.Current).Restart(); + } + void startPainting() { - model = createTree(gaze); + if (!PaintControls.IsEnabled) return; + model = new Tree(gaze, (int)shape.MaxBranches, ref rng); + if (paintTimer.IsEnabled) return; paintTimer.Start(); time = DateTime.Now; } @@ -202,33 +298,32 @@ void startPainting() void stopPainting() { paintTimer.Stop(); - timePainted += DateTime.Now - time; + var t = DateTime.Now - time; + shapeUsage[shape] += t; + colorUsage[color] += t; } - Tree createTree(Point p) + RenderTargetBitmap createDrawing(int width, int height) { - var t = new Tree { root = p, leaves = new PointCollection(), parents = new Dictionary() }; - for (int i = 0; i < rng.Next((shape.maxBranches + 1) / 2, shape.maxBranches + 1); ++i) t.leaves.Add(t.root); - t.parents[t.root] = t.root; - return t; + return new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32); } - void updateDrawing(ref Tree model, RenderTargetBitmap drawing) + void paint(ref Tree model, RenderTargetBitmap drawing) { // Grow model. var newLeaves = new PointCollection(); var newParents = new Dictionary(); - var rotation = shape.generationRotation * rng.NextDouble() * 2 * Math.PI; - for (int i = 0; i < model.leaves.Count; ++i) + var rotation = shape.GenerationRotation * rng.NextDouble() * 2 * Math.PI; + for (int i = 0; i < model.Leaves.Count; ++i) { - var q = (model.leaves.Count == 0) ? model.root : model.leaves[i]; + var q = (model.Leaves.Count == 0) ? model.Root : model.Leaves[i]; var angle = - i * (2 * Math.PI / model.leaves.Count) + i * (2 * Math.PI / model.Leaves.Count) + rotation - + (1 - shape.branchStraightness) * rng.NextDouble() * 2 * Math.PI; + + (1 - shape.BranchStraightness) * rng.NextDouble() * 2 * Math.PI; var p = new Point( - q.X + shape.branchStepLength * Math.Cos(angle), - q.Y + shape.branchStepLength * Math.Sin(angle) + q.X + shape.BranchStepLength * Math.Cos(angle), + q.Y + shape.BranchStepLength * Math.Sin(angle) ); newParents[p] = q; newLeaves.Add(p); @@ -238,71 +333,94 @@ void updateDrawing(ref Tree model, RenderTargetBitmap drawing) var dv = new DrawingVisual(); using (var dc = dv.RenderOpen()) { - var centerSize = rng.NextDouble() * shape.centerSize; - var centerBrush = new SolidColorBrush(createColor(color, shape.colorVariety)); - centerBrush.Opacity = rng.NextDouble() * shape.centerOpacity; - var centerPen = new Pen(new SolidColorBrush(createColor(color, shape.colorVariety)), 1); - centerPen.Brush.Opacity = rng.NextDouble() * shape.centerOpacity; - dc.DrawEllipse(centerBrush, centerPen, model.root, centerSize, centerSize); +#if !DEBUG + var centerSize = rng.NextDouble() * shape.CenterSize; + var centerBrush = new SolidColorBrush(createColor(color, shape.ColorVariety)); + centerBrush.Opacity = rng.NextDouble() * shape.CenterOpacity; + var centerPen = new Pen(centerBrush, 1); + dc.DrawEllipse(centerBrush, centerPen, model.Root, centerSize, centerSize); var edges = new GeometryGroup(); - var edgesPen = new Pen(new SolidColorBrush(createColor(color, shape.colorVariety)), shape.strokeThickness); - edgesPen.Brush.Opacity = shape.edgesOpacity; - foreach (var leaf in model.leaves) edges.Children.Add(new LineGeometry(leaf, model.parents[leaf])); + var edgesPen = new Pen(new SolidColorBrush(createColor(color, shape.ColorVariety)), 1); + edgesPen.StartLineCap = edgesPen.EndLineCap = PenLineCap.Round; + edgesPen.LineJoin = PenLineJoin.Round; + edgesPen.Brush.Opacity = shape.EdgesOpacity; + foreach (var p in model.Leaves) edges.Children.Add(new LineGeometry(p, model.Parents[p])); dc.DrawGeometry(null, edgesPen, edges); var vertices = new GeometryGroup(); - var verticesBrush = new SolidColorBrush(createColor(color, shape.colorVariety)); - verticesBrush.Opacity = rng.NextDouble() * shape.verticesOpacity; + var verticesBrush = new SolidColorBrush(createColor(color, shape.ColorVariety)); + verticesBrush.Opacity = rng.NextDouble() * shape.VerticesOpacity; var verticesPen = new Pen(verticesBrush, 1); - foreach (var leaf in model.leaves) + foreach (var p in model.Leaves) { var r = rng.NextDouble(); - var eg = new EllipseGeometry(leaf, shape.verticesSize * (r + shape.verticesSquashVariety * rng.NextDouble()), shape.verticesSize * (r + shape.verticesSquashVariety * rng.NextDouble())); + var eg = new EllipseGeometry(p, shape.VerticesSize * (r + shape.VerticesSquashVariety * rng.NextDouble()), shape.VerticesSize * (r + shape.VerticesSquashVariety * rng.NextDouble())); vertices.Children.Add(eg); } dc.DrawGeometry(verticesBrush, verticesPen, vertices); var hull = new StreamGeometry(); - var hullBrush = new SolidColorBrush(createColor(color, shape.colorVariety)); - hullBrush.Opacity = rng.NextDouble() * shape.hullOpacity; - var hullPen = new Pen(hullBrush, shape.strokeThickness); + var hullBrush = new SolidColorBrush(createColor(color, shape.ColorVariety)); + hullBrush.Opacity = rng.NextDouble() * shape.HullOpacity; + var hullPen = new Pen(hullBrush, 1); using (var sgc = hull.Open()) { - sgc.BeginFigure(model.leaves[0], true, true); - sgc.PolyLineTo(model.leaves, true, true); + sgc.BeginFigure(model.Leaves[0], true, true); + sgc.PolyLineTo(model.Leaves, true, true); } dc.DrawGeometry(hullBrush, hullPen, hull); +#else + GazeMarker.Visibility = Visibility.Hidden; + drawing.Clear(); + foreach (var o in ((App)Application.Current).offsets) + { + var p = new Point(o.Key.X, o.Key.Y); + p.Offset(o.Value.X, o.Value.Y); + var pen = new Pen(Brushes.Yellow, 5); + pen.EndLineCap = PenLineCap.Triangle; + dc.DrawLine(pen, o.Key, p); + } + var offsets = ((App)Application.Current).offsets; + if (offsets.Count == 1) + { + var p = new Point(gaze.X, gaze.Y); + gaze += offsets.First().Value; + var pen = new Pen(Brushes.Red, 5); + pen.EndLineCap = PenLineCap.Triangle; + dc.DrawLine(pen, p, gaze); + } + else if (offsets.Count > 1) + { + var sum = offsets.Select(kvp => (kvp.Key - gaze).Length).Sum(); + var calibratedGazePoint = new Point(gaze.X, gaze.Y); + foreach (var kvp in offsets) + { + var p = new Point(calibratedGazePoint.X, calibratedGazePoint.Y); + calibratedGazePoint += (1 - (kvp.Key - gaze).Length / sum) * kvp.Value; + var pen = new Pen(Brushes.Red, 5); + pen.EndLineCap = PenLineCap.Triangle; + dc.DrawLine(pen, p, calibratedGazePoint); + } + gaze = calibratedGazePoint; + } + dc.DrawEllipse(Brushes.Green, null, gaze, 10, 10); +#endif } // Persist update. - model.leaves.Clear(); - model.parents.Clear(); - foreach (var l in newLeaves) model.leaves.Add(l); - foreach (var kvp in newParents) model.parents.Add(kvp.Key, kvp.Value); + model.Leaves = newLeaves; + model.Parents = newParents; drawing.Render(dv); - - } - - void saveDrawing() - { - var e = new PngBitmapEncoder(); - e.Frames.Add(BitmapFrame.Create((RenderTargetBitmap)Raster.Source)); - using (var fs = System.IO.File.OpenWrite("image.png")) e.Save(fs); - } - - void clearDrawing() - { - Raster.Source = new RenderTargetBitmap((int)ActualWidth, (int)ActualHeight, 96, 96, PixelFormats.Pbgra32); } Color createColor(Color? baseColor = null, double randomness = 1) { + // Generate a random color. var H = rng.NextDouble(); - var S = 1.0; - var V = 1.0; + var S = rng.NextDouble() > 0 || baseColor.HasValue ? 1.0 : 0; + var V = rng.NextDouble() > 0 || baseColor.HasValue ? 1.0 : 0; byte R, G, B; - if (S == 0) { R = (byte)(V * 255); @@ -333,17 +451,25 @@ Color createColor(Color? baseColor = null, double randomness = 1) B = (byte)(b * 255); } - var c = Color.FromRgb(R, G, B); - return (baseColor.HasValue) ? baseColor.Value + Color.Multiply(c, (float)randomness) : c; + // Mix colors if neccessary. + if (baseColor.HasValue) + { + var c1 = baseColor.Value; + var c2 = Color.Multiply(Color.FromRgb(R, G, B), (float)randomness); + var brightness = System.Drawing.Color.FromArgb(baseColor.Value.A, baseColor.Value.R, baseColor.Value.G, baseColor.Value.B).GetBrightness(); + return (brightness >= 0.5) ? c1 + c2 : c1 - c2; + } + else return Color.FromRgb(R, G, B); } void updateIcons() { - ColorButton.Background = new SolidColorBrush(color); + ColorButtonBackgroundBaseColor.Color = color; + ColorButtonBackgroundColorVariety.Color = createColor(color, shape.ColorVariety); var toolIcon = new Image(); - toolIcon.Source = new RenderTargetBitmap((int)(ShapeButton.ActualWidth), (int)(ShapeButton.ActualHeight), 96.0, 96.0, PixelFormats.Pbgra32); - var t = createTree(new Point(ShapeButton.ActualWidth / 2, ShapeButton.ActualHeight / 2)); - for (int i = 0; i < 10; ++i) updateDrawing(ref t, (RenderTargetBitmap)toolIcon.Source); + toolIcon.Source = createDrawing((int)(ShapeButton.ActualWidth), (int)(ShapeButton.ActualHeight)); + var model = new Tree(new Point(ShapeButton.ActualWidth / 2, ShapeButton.ActualHeight / 2), (int)shape.MaxBranches, ref rng); + for (int i = 0; i < 10; ++i) paint(ref model, toolIcon.Source as RenderTargetBitmap); ShapeButton.Content = toolIcon; } } diff --git a/EyePaint/Presets.json b/EyePaint/Presets.json deleted file mode 100644 index ab2606c..0000000 --- a/EyePaint/Presets.json +++ /dev/null @@ -1 +0,0 @@ -{"colors":[{"A":255,"B":0,"G":0,"R":255},{"A":255,"B":255,"G":0,"R":0},{"A":255,"B":0,"G":128,"R":0},{"A":255,"B":255,"G":255,"R":255},{"A":255,"B":0,"G":0,"R":0},{"A":255,"B":0,"G":37,"R":255}],"shapes":[{"branches":81,"centerOpacityVariety":0.0038610836508968303,"centerSize":9.8488578017961039,"colorVariety":0.34457415404942549,"edgesOpacity":0.82982405825975536,"hullOpacityVariety":0.17239927408090727,"rotation":0.412062909645989,"step":17,"straightness":0.93779453004143254,"strokeThickness":1,"verticesOpacityVariety":0.49084009206427265,"verticesSize":3.7416573867739413,"verticesSquashVariety":0.810641031988667},{"branches":42,"centerOpacityVariety":0.72861201536311393,"centerSize":6.7823299831252681,"colorVariety":0.84080574607514114,"edgesOpacity":0.38694085291910024,"hullOpacityVariety":0.32440319310475879,"rotation":0.53226692347427218,"step":24,"straightness":0.13494823187984151,"strokeThickness":2.6457513110645907,"verticesOpacityVariety":0.42618670893189808,"verticesSize":4,"verticesSquashVariety":0.175224644005976}]} \ No newline at end of file diff --git a/EyePaint/Properties/Resources.Designer.cs b/EyePaint/Properties/Resources.Designer.cs index 74656fc..09631a1 100644 --- a/EyePaint/Properties/Resources.Designer.cs +++ b/EyePaint/Properties/Resources.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.34014 +// Runtime Version:4.0.30319.18444 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -59,25 +59,5 @@ internal Resources() { resourceCulture = value; } } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] itc_officina_sans_std_bold { - get { - object obj = ResourceManager.GetObject("itc_officina_sans_std_bold", resourceCulture); - return ((byte[])(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] Presets { - get { - object obj = ResourceManager.GetObject("Presets", resourceCulture); - return ((byte[])(obj)); - } - } } } diff --git a/EyePaint/Properties/Resources.resx b/EyePaint/Properties/Resources.resx index f56975f..1af7de1 100644 --- a/EyePaint/Properties/Resources.resx +++ b/EyePaint/Properties/Resources.resx @@ -117,11 +117,4 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - ..\itc_officina_sans_std_bold.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - ..\presets.json;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - \ No newline at end of file diff --git a/EyePaint/Properties/Settings.Designer.cs b/EyePaint/Properties/Settings.Designer.cs index f661ed2..3ce8e99 100644 --- a/EyePaint/Properties/Settings.Designer.cs +++ b/EyePaint/Properties/Settings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.34014 +// Runtime Version:4.0.30319.18444 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -22,5 +22,77 @@ public static Settings Default { return defaultInstance; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("default@default.default")] + public string AdminEmail { + get { + return ((string)(this["AdminEmail"])); + } + set { + this["AdminEmail"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("default.default.default")] + public string SmtpServer { + get { + return ((string)(this["SmtpServer"])); + } + set { + this["SmtpServer"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("a81591b96318cd9799982c11663df750")] + public string FlickrKey { + get { + return ((string)(this["FlickrKey"])); + } + set { + this["FlickrKey"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("05e84b538d7d4cf5")] + public string FlickrSecret { + get { + return ((string)(this["FlickrSecret"])); + } + set { + this["FlickrSecret"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("20")] + public int Spacing { + get { + return ((int)(this["Spacing"])); + } + set { + this["Spacing"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("50")] + public int Stability { + get { + return ((int)(this["Stability"])); + } + set { + this["Stability"] = value; + } + } } } diff --git a/EyePaint/Properties/Settings.settings b/EyePaint/Properties/Settings.settings index 033d7a5..0b37048 100644 --- a/EyePaint/Properties/Settings.settings +++ b/EyePaint/Properties/Settings.settings @@ -1,7 +1,24 @@  - - - - - + + + + + default@default.default + + + default.default.default + + + a81591b96318cd9799982c11663df750 + + + 05e84b538d7d4cf5 + + + 20 + + + 50 + + \ No newline at end of file diff --git a/EyePaint/SettingsWindow.xaml b/EyePaint/SettingsWindow.xaml new file mode 100644 index 0000000..a3cf16c --- /dev/null +++ b/EyePaint/SettingsWindow.xaml @@ -0,0 +1,16 @@ + + + + + diff --git a/EyePaint/CalibrationWindow.xaml.cs b/EyePaint/CalibrationWindow.xaml.cs new file mode 100644 index 0000000..1b9cada --- /dev/null +++ b/EyePaint/CalibrationWindow.xaml.cs @@ -0,0 +1,30 @@ +using System; +using System.Windows; +using System.Windows.Media.Animation; + +namespace EyePaint +{ + /// + /// Used to reduce calibration offset errors amongst different users. + /// + public partial class CalibrationWindow : Window + { + public CalibrationWindow() + { + InitializeComponent(); + ShowDialog(); + } + + void onClick(object s, RoutedEventArgs e) + { + Close(); + } + + void onContentVisible(object s, EventArgs e) + { + //TODO + IsEnabled = ((App)Application.Current).Tracking; + ((App)Application.Current).TrackingChanged += (_s, _e) => Dispatcher.Invoke(() => IsEnabled = _e.Tracking); + } + } +} diff --git a/EyePaint/CountdownWindow.xaml b/EyePaint/CountdownWindow.xaml index 46b4df3..bd16330 100644 --- a/EyePaint/CountdownWindow.xaml +++ b/EyePaint/CountdownWindow.xaml @@ -1,8 +1,11 @@  + Style="{StaticResource GazeWindow}" + KeyDown="onCancel" + Topmost="True" + AllowsTransparency="True" + Background="#AA000000"> @@ -19,33 +22,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/EyePaint/CountdownWindow.xaml.cs b/EyePaint/CountdownWindow.xaml.cs index 2042cb8..cee1e4e 100644 --- a/EyePaint/CountdownWindow.xaml.cs +++ b/EyePaint/CountdownWindow.xaml.cs @@ -11,12 +11,13 @@ public partial class CountdownWindow : Window public CountdownWindow() { InitializeComponent(); + IsEnabled = ((App)Application.Current).Tracking; + ((App)Application.Current).TrackingChanged += (_s, _e) => Dispatcher.Invoke(() => IsEnabled = _e.Tracking); ShowDialog(); } void onConfirm(object s, EventArgs e) { - //TODO Fix repeated restart presses bug. DialogResult = true; } diff --git a/EyePaint/DialogWindow.xaml b/EyePaint/DialogWindow.xaml new file mode 100644 index 0000000..be0f032 --- /dev/null +++ b/EyePaint/DialogWindow.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + diff --git a/EyePaint/DialogWindow.xaml.cs b/EyePaint/DialogWindow.xaml.cs new file mode 100644 index 0000000..9e5cdc1 --- /dev/null +++ b/EyePaint/DialogWindow.xaml.cs @@ -0,0 +1,33 @@ +using System; +using System.Windows; + +namespace EyePaint +{ + /// + /// Used to display a yes/no dialog to the user. + /// + public partial class DialogWindow : Window + { + public DialogWindow() + { + InitializeComponent(); + ShowDialog(); + } + + void onContentVisible(object s, EventArgs e) + { + IsEnabled = ((App)Application.Current).Tracking; + ((App)Application.Current).TrackingChanged += (_s, _e) => Dispatcher.Invoke(() => IsEnabled = _e.Tracking); + } + + void onConfirm(object s, EventArgs e) + { + DialogResult = true; + } + + void onCancel(object s, EventArgs e) + { + DialogResult = false; + } + } +} diff --git a/EyePaint/ErrorWindow.xaml b/EyePaint/ErrorWindow.xaml index 4c9382d..44ff6fa 100644 --- a/EyePaint/ErrorWindow.xaml +++ b/EyePaint/ErrorWindow.xaml @@ -1,7 +1,7 @@  diff --git a/EyePaint/EyePaint.csproj b/EyePaint/EyePaint.csproj index 09ea70c..9115854 100644 --- a/EyePaint/EyePaint.csproj +++ b/EyePaint/EyePaint.csproj @@ -94,16 +94,33 @@ MSBuild:Compile Designer + + CalibrationWindow.xaml + + + CountdownWindow.xaml + + + DialogWindow.xaml + ErrorWindow.xaml SettingsWindow.xaml + + Designer + MSBuild:Compile + Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile @@ -116,9 +133,6 @@ App.xaml Code - - CountdownWindow.xaml - MainWindow.xaml Code diff --git a/EyePaint/MainWindow.xaml b/EyePaint/MainWindow.xaml index a53cf55..73c7286 100644 --- a/EyePaint/MainWindow.xaml +++ b/EyePaint/MainWindow.xaml @@ -2,118 +2,106 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" KeyDown="onKeyDown" + Loaded="onLoaded" ContentRendered="onContentRendered" - Style="{StaticResource FullscreenWindow}" - IsEnabled="False"> + Style="{StaticResource GazeWindow}" + Background="White"> - - - - - - - - + + + + + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - + diff --git a/EyePaint/MainWindow.xaml.cs b/EyePaint/MainWindow.xaml.cs index 9f95b30..73a94d9 100644 --- a/EyePaint/MainWindow.xaml.cs +++ b/EyePaint/MainWindow.xaml.cs @@ -50,6 +50,7 @@ public partial class MainWindow : Window DispatcherTimer paintTimer; Tree model; Point gaze; + List gazes = new List(); Shape shape; Color color; DateTime time; @@ -61,74 +62,65 @@ public partial class MainWindow : Window public MainWindow() { InitializeComponent(); + + // Render clock. Note: single-threaded. Approximately 20 FPS. + (paintTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(50), DispatcherPriority.Render, (_, __) => paint(ref this.model, Raster.Source as RenderTargetBitmap), Dispatcher)).Stop(); } - void onContentRendered(object s, EventArgs e) + void onLoaded(object s, RoutedEventArgs e) { // Choose initial shape and color by simulating a button click. ShapeButton.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent)); ColorButton.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent)); + } + void onContentRendered(object s, EventArgs e) + { // Initialize drawing. Raster.Source = createDrawing((int)ActualWidth, (int)ActualHeight); - // Render clock. Note: single-threaded. - (paintTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(50), DispatcherPriority.Render, (_, __) => paint(ref this.model, Raster.Source as RenderTargetBitmap), Dispatcher)).Stop(); - } + // Perform initial offset calibration. + new CalibrationWindow(); - void onFadedIn(object s, EventArgs e) - { -#if !DEBUG - // Disable functionality if the user isn't in front of the eye tracker. - ((App)Application.Current).TrackingStatusChanged += (_s, _e) => Dispatcher.Invoke(() => - { - if (_e.Tracking) IsEnabled = true; - }); -#else - IsEnabled = true; -#endif + IsEnabled = ((App)Application.Current).Tracking; + ((App)Application.Current).TrackingChanged += (_s, _e) => Dispatcher.Invoke(() => { IsEnabled = _e.Tracking; Focus(); }); } void onKeyDown(object s, KeyEventArgs e) { + if (e.IsRepeat) return; switch (e.Key) { - case Key.Escape: + case Key.Escape: // Show admin settings (new SettingsWindow()).ShowDialog(); break; - case Key.S: - // Save drawing, but only if the user has painted enough stuff. - if (shapeUsage.Sum(sh => sh.Value.Seconds) > 5) - { - //TODO Implement yes/no gaze dialog. - if (new CountdownWindow().DialogResult.Value) - { - ((App)Application.Current).Clear(); - PaintControls.IsEnabled = false; - stopPainting(); - ((Storyboard)FindResource("SaveDrawingAnimation")).Begin(); - } - } - else - { - // Reset application for the next user. - ((App)Application.Current).Reset(); - } - + case Key.C: // Change color + ColorButton.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent)); + break; + case Key.S: // Change shape + ShapeButton.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent)); + break; + case Key.P: // Publish drawing + if (new CountdownWindow().DialogResult.Value) { IsEnabled = false; ((Storyboard)FindResource("SaveDrawingAnimation")).Begin(); } + break; + case Key.R: // Reset session + if (new CountdownWindow().DialogResult.Value) { IsEnabled = false; ((App)Application.Current).Reset(); } break; } } void onCanvasMouseDown(object s, MouseButtonEventArgs e) { - gaze = e.GetPosition(s as Canvas); + gaze = calculateGaze(e.GetPosition(s as Canvas)); startPainting(); + GazeMarker.Visibility = Visibility.Hidden; ((Storyboard)GazeMarker.FindResource("GazePaintAnimation")).Stop(); } void onCanvasMouseUp(object s, MouseButtonEventArgs e) { stopPainting(); + GazeMarker.Visibility = Visibility.Visible; ((Storyboard)GazeMarker.FindResource("GazePaintAnimation")).Begin(); } @@ -145,12 +137,13 @@ void onCanvasMouseLeave(object s, MouseEventArgs e) void onCanvasMouseMove(object s, MouseEventArgs e) { - var p = e.GetPosition(s as Canvas); + var p = calculateGaze(e.GetPosition(s as Canvas)); + Canvas.SetLeft(GazeMarker, p.X - GazeMarker.ActualWidth / 2); Canvas.SetTop(GazeMarker, p.Y - GazeMarker.ActualHeight / 2); - if (paintTimer.IsEnabled && (model.Root - p).Length > EyePaint.Properties.Settings.Default.Spacing) model = new Tree(p, (int)shape.MaxBranches, ref rng); - if ((gaze - p).Length > EyePaint.Properties.Settings.Default.Spacing / 2) + if (paintTimer.IsEnabled && (model.Root - p).Length > Properties.Settings.Default.Spacing) model = new Tree(p, (int)shape.MaxBranches, ref rng); + if ((gaze - p).Length > Properties.Settings.Default.Spacing / 2) { var sb = (Storyboard)GazeMarker.FindResource("GazePaintAnimation"); switch (sb.GetCurrentState()) @@ -162,12 +155,14 @@ void onCanvasMouseMove(object s, MouseEventArgs e) gaze = p; } - void onStartButtonClick(object s, EventArgs e) + void onPublishButtonClick(object s, EventArgs e) { - (s as Button).Visibility = Visibility.Hidden; -#if !DEBUG - ((App)Application.Current).TrackingStatusChanged += (_s, _e) => Dispatcher.Invoke(() => PaintControls.IsEnabled = _e.Tracking); -#endif + if (new DialogWindow().DialogResult.Value) { IsEnabled = false; ((Storyboard)FindResource("SaveDrawingAnimation")).Begin(); } + } + + void onResetButtonClick(object s, EventArgs e) + { + if (new DialogWindow().DialogResult.Value) { IsEnabled = false; ((App)Application.Current).Reset(); } } void onShapeButtonClick(object s, EventArgs e) @@ -245,62 +240,53 @@ void onColorButtonClick(object s, EventArgs e) updateIcons(); } - void onInactivity(object s, EventArgs e) - { - if (shapeUsage.Sum(sh => sh.Value.Seconds) > 60) ((Storyboard)FindResource("SaveDrawingAnimation")).Begin(); - else ((App)Application.Current).Reset(); - } - void onGazePaint(object s, EventArgs e) { startPainting(); } - void onPaintControlsIsEnabledChanged(object s, DependencyPropertyChangedEventArgs e) - { - /* - var sb = ((Storyboard)FindResource("InactivityAnimation")); - if (PaintControls == null || sb == null) return; - if (!PaintControls.IsEnabled) - { - stopPainting(); - sb.Begin(); - } - else sb.Stop(); - */ - } - void onSaveDrawing(object s, EventArgs e) { var pbe = new PngBitmapEncoder(); pbe.Frames.Add(BitmapFrame.Create(Raster.Source as RenderTargetBitmap)); using (var fs = System.IO.File.OpenWrite("image.png")) pbe.Save(fs); //TODO Backend upload. - //var f = new Flickr(EyePaint.Properties.Settings.Default.FlickrKey, EyePaint.Properties.Settings.Default.FlickrSecret); + //var f = new Flickr(Properties.Settings.Default.FlickrKey, Properties.Settings.Default.FlickrSecret); //var requestToken = f.OAuthGetRequestToken("oob"); //System.Diagnostics.Process.Start(f.OAuthCalculateAuthorizationUrl(requestToken.Token, AuthLevel.Write)); //var accessToken = f.OAuthGetAccessToken(requestToken, VerifierTextBox.Text); //f.OAuthAccessToken = ""; //f.OAuthAccessTokenSecret = ""; //f.UploadPicture("image.png"); - ((App)Application.Current).Restart(); + ((App)Application.Current).Reset(); + } + + Point calculateGaze(Point p) + { + gazes.Add(p); + while (gazes.Count > Properties.Settings.Default.Inertia) gazes.RemoveAt(0); + return new Point(gazes.Average(_p => _p.X), gazes.Average(_p => _p.Y)); } void startPainting() { - if (!PaintControls.IsEnabled) return; - model = new Tree(gaze, (int)shape.MaxBranches, ref rng); - if (paintTimer.IsEnabled) return; - paintTimer.Start(); - time = DateTime.Now; + if (!paintTimer.IsEnabled) + { + model = new Tree(gaze, (int)shape.MaxBranches, ref rng); + time = DateTime.Now; + paintTimer.Start(); + } } void stopPainting() { - paintTimer.Stop(); - var t = DateTime.Now - time; - shapeUsage[shape] += t; - colorUsage[color] += t; + if (paintTimer.IsEnabled) + { + paintTimer.Stop(); + var t = DateTime.Now - time; + shapeUsage[shape] += t; + colorUsage[color] += t; + } } RenderTargetBitmap createDrawing(int width, int height) @@ -333,7 +319,6 @@ void paint(ref Tree model, RenderTargetBitmap drawing) var dv = new DrawingVisual(); using (var dc = dv.RenderOpen()) { -#if !DEBUG var centerSize = rng.NextDouble() * shape.CenterSize; var centerBrush = new SolidColorBrush(createColor(color, shape.ColorVariety)); centerBrush.Opacity = rng.NextDouble() * shape.CenterOpacity; @@ -370,42 +355,6 @@ void paint(ref Tree model, RenderTargetBitmap drawing) sgc.PolyLineTo(model.Leaves, true, true); } dc.DrawGeometry(hullBrush, hullPen, hull); -#else - GazeMarker.Visibility = Visibility.Hidden; - drawing.Clear(); - foreach (var o in ((App)Application.Current).offsets) - { - var p = new Point(o.Key.X, o.Key.Y); - p.Offset(o.Value.X, o.Value.Y); - var pen = new Pen(Brushes.Yellow, 5); - pen.EndLineCap = PenLineCap.Triangle; - dc.DrawLine(pen, o.Key, p); - } - var offsets = ((App)Application.Current).offsets; - if (offsets.Count == 1) - { - var p = new Point(gaze.X, gaze.Y); - gaze += offsets.First().Value; - var pen = new Pen(Brushes.Red, 5); - pen.EndLineCap = PenLineCap.Triangle; - dc.DrawLine(pen, p, gaze); - } - else if (offsets.Count > 1) - { - var sum = offsets.Select(kvp => (kvp.Key - gaze).Length).Sum(); - var calibratedGazePoint = new Point(gaze.X, gaze.Y); - foreach (var kvp in offsets) - { - var p = new Point(calibratedGazePoint.X, calibratedGazePoint.Y); - calibratedGazePoint += (1 - (kvp.Key - gaze).Length / sum) * kvp.Value; - var pen = new Pen(Brushes.Red, 5); - pen.EndLineCap = PenLineCap.Triangle; - dc.DrawLine(pen, p, calibratedGazePoint); - } - gaze = calibratedGazePoint; - } - dc.DrawEllipse(Brushes.Green, null, gaze, 10, 10); -#endif } // Persist update. diff --git a/EyePaint/Properties/Settings.Designer.cs b/EyePaint/Properties/Settings.Designer.cs index 3ce8e99..7abd926 100644 --- a/EyePaint/Properties/Settings.Designer.cs +++ b/EyePaint/Properties/Settings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.18444 +// Runtime Version:4.0.30319.34014 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -73,7 +73,7 @@ public string FlickrSecret { [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("20")] + [global::System.Configuration.DefaultSettingValueAttribute("10")] public int Spacing { get { return ((int)(this["Spacing"])); @@ -85,7 +85,7 @@ public int Spacing { [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("50")] + [global::System.Configuration.DefaultSettingValueAttribute("30")] public int Stability { get { return ((int)(this["Stability"])); @@ -94,5 +94,29 @@ public int Stability { this["Stability"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("30")] + public int Blink { + get { + return ((int)(this["Blink"])); + } + set { + this["Blink"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("100")] + public int Inertia { + get { + return ((int)(this["Inertia"])); + } + set { + this["Inertia"] = value; + } + } } } diff --git a/EyePaint/Properties/Settings.settings b/EyePaint/Properties/Settings.settings index 0b37048..42ca82c 100644 --- a/EyePaint/Properties/Settings.settings +++ b/EyePaint/Properties/Settings.settings @@ -15,10 +15,16 @@ 05e84b538d7d4cf5 - 20 + 10 - 50 + 30 + + + 30 + + + 100 \ No newline at end of file diff --git a/EyePaint/SettingsWindow.xaml b/EyePaint/SettingsWindow.xaml index a3cf16c..edc9f14 100644 --- a/EyePaint/SettingsWindow.xaml +++ b/EyePaint/SettingsWindow.xaml @@ -3,7 +3,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Topmost="True"> -