Brought over a couple more of my old BabySmash mods.

* Hardware effects option was implemented; the options checkbox for it is no longer grayed out.
* Words! When a typed sequence of letters completes a recognized word, the letters will each animate into position to
  form the word, and the last synthesized speech will be of the word rather than the last-typed letter.  This may
  breath a little more life into the app, when the child is starting to read and write; allowing them to explore keys
  and find words.  The starting dictionary just demonstrates the feature, and ought to be replaced with a longer one,
  or the WordFinder class could be modified to use the OS dictionary, etc.  Extensions should think on how to handle
  rude words that we don't want to teach here, and avoid obscure words that aren't commonly used, etc.
This commit is contained in:
karak 2015-03-15 14:06:16 -07:00
Родитель e7f6a0e94e
Коммит 50b816a7d3
10 изменённых файлов: 326 добавлений и 92 удалений

Просмотреть файл

@ -68,6 +68,7 @@
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Compile Include="WordFinder.cs" />
<Reference Include="PresentationFramework.Luna">
<RequiredTargetFramework>3.0</RequiredTargetFramework>
</Reference>
@ -346,6 +347,11 @@
<Folder Include="Resources\Voices\Female\" />
<Folder Include="Resources\Voices\Male\" />
</ItemGroup>
<ItemGroup>
<Content Include="Words.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

Просмотреть файл

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Deployment.Application;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Speech.Synthesis;
using System.Windows;
@ -35,8 +36,9 @@ namespace BabySmash
private DispatcherTimer timer = new DispatcherTimer();
private Queue<Shape> ellipsesQueue = new Queue<Shape>();
private Dictionary<string, Queue<UserControl>> ellipsesUserControlQueue = new Dictionary<string, Queue<UserControl>>();
private Dictionary<string, List<UserControl>> figuresUserControlQueue = new Dictionary<string, List<UserControl>>();
private ApplicationDeployment deployment = null;
private WordFinder wordFinder = new WordFinder("Words.txt");
void deployment_CheckForUpdateCompleted(object sender, CheckForUpdateCompletedEventArgs e)
{
@ -114,22 +116,12 @@ namespace BabySmash
Name = "Window" + Number++.ToString()
};
ellipsesUserControlQueue[m.Name] = new Queue<UserControl>();
figuresUserControlQueue[m.Name] = new List<UserControl>();
m.Show();
m.MouseLeftButtonDown += HandleMouseLeftButtonDown;
m.MouseWheel += HandleMouseWheel;
#if false
m.Width = 700;
m.Height = 600;
m.Left = 900;
m.Top = 500;
#else
m.WindowState = WindowState.Maximized;
#endif
windows.Add(m);
}
@ -188,29 +180,54 @@ namespace BabySmash
return;
}
string s = e.Key.ToString();
// Handle number keys, whose values are prefixed by "D" or "NumPad" (because enum names can't start with a digit)
if (s.StartsWith("NumPad")) s = s.Substring(6);
if ((s.Length == 2) && s.StartsWith("D")) s = s.Substring(1);
AddFigure(uie, s);
char displayChar = GetDisplayChar(e.Key);
AddFigure(uie, displayChar);
}
private void AddFigure(FrameworkElement uie, string s)
private char GetDisplayChar(Key key)
{
FigureTemplate template = FigureGenerator.GenerateFigureTemplate(s);
foreach (MainWindow m in this.windows)
// If a letter was pressed, display the letter.
if (key >= Key.A && key <= Key.Z)
{
return (char)('A' + key - Key.A);
}
// If a number on the normal number track is pressed, display the number.
if (key >= Key.D0 && key <= Key.D9)
{
return (char)('0' + key - Key.D0);
}
// If a number on the numpad is pressed, display the number.
if (key >= Key.NumPad0 && key <= Key.NumPad9)
{
return (char)('0' + key - Key.NumPad0);
}
// Otherwise, display a random shape.
return '*';
}
private void AddFigure(FrameworkElement uie, char c)
{
FigureTemplate template = FigureGenerator.GenerateFigureTemplate(c);
foreach (MainWindow window in this.windows)
{
UserControl f = FigureGenerator.NewUserControlFrom(template);
m.AddFigure(f);
window.AddFigure(f);
var queue = ellipsesUserControlQueue[m.Name];
queue.Enqueue(f);
var queue = figuresUserControlQueue[window.Name];
queue.Add(f);
// Letters should already have accurate width and height, but others may them assigned.
if (double.IsNaN(f.Width) || double.IsNaN(f.Height))
{
f.Width = 300;
f.Height = 300;
Canvas.SetLeft(f, Utils.RandomBetweenTwoNumbers(0, Convert.ToInt32(m.ActualWidth - f.Width)));
Canvas.SetTop(f, Utils.RandomBetweenTwoNumbers(0, Convert.ToInt32(m.ActualHeight - f.Height)));
}
Canvas.SetLeft(f, Utils.RandomBetweenTwoNumbers(0, Convert.ToInt32(window.ActualWidth - f.Width)));
Canvas.SetTop(f, Utils.RandomBetweenTwoNumbers(0, Convert.ToInt32(window.ActualHeight - f.Height)));
Storyboard storyboard = Animation.CreateDPAnimation(uie, f,
UIElement.OpacityProperty,
@ -225,13 +242,27 @@ namespace BabySmash
if (queue.Count > Settings.Default.ClearAfter)
{
UserControl u = queue.Dequeue() as UserControl;
m.RemoveFigure(u);
window.RemoveFigure(queue[0]);
queue.RemoveAt(0);
}
}
// Find the last word typed, if applicable.
string lastWord = this.wordFinder.LastWord(figuresUserControlQueue.Values.First());
if (lastWord != null)
{
foreach (MainWindow window in this.windows)
{
this.wordFinder.AnimateLettersIntoWord(figuresUserControlQueue[window.Name], lastWord);
}
SpeakString(lastWord);
}
else
{
PlaySound(template);
}
}
//private static DoubleAnimationUsingKeyFrames ApplyZoomOut(UserControl u)
//{
@ -421,8 +452,6 @@ namespace BabySmash
if (Settings.Default.MouseDraw && main.IsMouseCaptured == false)
main.CaptureMouse();
Debug.WriteLine(String.Format("MouseMove! {0} {1} {2}", Settings.Default.MouseDraw, main.IsMouseCaptured, isOptionsDialogShown));
if (isDrawing || Settings.Default.MouseDraw)
{
MouseDraw(main, e.GetPosition(main));

Просмотреть файл

@ -793,8 +793,8 @@
IsChecked="{Binding Path=Default.ForceUppercase, Mode=TwoWay}" Content="Force Letters to UPPERCASE" />
<CheckBox Style="{DynamicResource SimpleCheckBox}" Grid.Row="5" Grid.ColumnSpan="2" Grid.Column="0" Foreground="#FFD1D1D1" FontSize="14" FontFamily="Segoe UI"
IsChecked="{Binding Path=Default.FacesOnShapes, Mode=TwoWay}" Content="Faces on Shapes" />
<CheckBox Style="{DynamicResource SimpleCheckBox}" Grid.Row="6" Grid.ColumnSpan="2" Grid.Column="0" Foreground="#4CD1D1D1" FontSize="14" FontFamily="Segoe UI"
IsChecked="{Binding Path=Default.BitmapEffects, Mode=TwoWay}" IsEnabled="False" Content="Hardware Effects (DISABLED FOR NOW)"/>
<CheckBox Style="{DynamicResource SimpleCheckBox}" Grid.Row="6" Grid.ColumnSpan="2" Grid.Column="0" Foreground="#FFD1D1D1" FontSize="14" FontFamily="Segoe UI"
IsChecked="{Binding Path=Default.UseEffects, Mode=TwoWay}" Content="Hardware Effects"/>
<CheckBox Style="{DynamicResource SimpleCheckBox}" Grid.Row="7" Grid.ColumnSpan="2" Grid.Column="0" Foreground="#FFD1D1D1" FontSize="14" FontFamily="Segoe UI"
IsChecked="{Binding Path=Default.MouseDraw, Mode=TwoWay}" Content="Clickless Mouse Drawing" />
<CheckBox Style="{DynamicResource SimpleCheckBox}" x:Name="TransparentCheckBox" Grid.Row="8" Grid.ColumnSpan="3" Grid.Column="0" Foreground="#FFD1D1D1" FontSize="14" FontFamily="Segoe UI"

6
Properties/Settings.Designer.cs сгенерированный
Просмотреть файл

@ -50,12 +50,12 @@ namespace BabySmash.Properties {
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
public bool BitmapEffects {
public bool UseEffects {
get {
return ((bool)(this["BitmapEffects"]));
return ((bool)(this["UseEffects"]));
}
set {
this["BitmapEffects"] = value;
this["UseEffects"] = value;
}
}

Просмотреть файл

@ -7,21 +7,33 @@ namespace BabySmash
{
class Animation
{
public static BitmapEffect GetRandomBitmapEffect()
public static Effect GetRandomBitmapEffect()
{
int e = Utils.RandomBetweenTwoNumbers(0, 3);
switch (e)
{
case 0:
return new BevelBitmapEffect();
// Just makes the figure blurry; maybe do this one less frequently?
return new BlurEffect
{
Radius = Utils.RandomBetweenTwoNumbers(5, 20),
RenderingBias = RenderingBias.Performance,
};
case 1:
return new DropShadowBitmapEffect();
// TODO: Maybe add a replacement for the deprecated EmbossBitmapEffect? For now, just fallthrough.
case 2:
return new EmbossBitmapEffect();
// TODO: Maybe add a replacement for the deprecated BevelBitmapEffect? For now, just fallthrough.
case 3:
return new OuterGlowBitmapEffect();
return new DropShadowEffect
{
ShadowDepth = 0,
Color = Utils.GetRandomColor(),
BlurRadius = Utils.RandomBetweenTwoNumbers(10, 50),
RenderingBias = RenderingBias.Performance,
};
}
return new BevelBitmapEffect();
return new DropShadowEffect();
}
public static void ApplyRandomAnimationEffect(FrameworkElement fe, Duration duration)

Просмотреть файл

@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Windows;
using System.Windows.Media;
@ -17,9 +18,10 @@ namespace BabySmash
this.InitializeComponent();
}
public CoolLetter(Brush x, string letter)
public CoolLetter(Brush x, char letter)
: this()
{
this.Character = letter;
this.letterPath.Fill = x;
this.letterPath.Data = MakeCharacterGeometry(GetLetterCharacter(letter));
@ -27,35 +29,33 @@ namespace BabySmash
this.Height = this.letterPath.Data.Bounds.Height + this.letterPath.Data.Bounds.Y + this.letterPath.StrokeThickness / 2;
}
private static Geometry MakeCharacterGeometry(string t)
public char Character { get; private set; }
private static Geometry MakeCharacterGeometry(char character)
{
var fText = new FormattedText(
t,
var fontFamily = new FontFamily(Settings.Default.FontFamily);
var typeface = new Typeface(fontFamily, FontStyles.Normal, FontWeights.Heavy, FontStretches.Normal);
var formattedText = new FormattedText(
character.ToString(),
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(
new FontFamily(Properties.Settings.Default.FontFamily),
FontStyles.Normal,
FontWeights.Heavy,
FontStretches.Normal),
typeface,
300,
Brushes.Black
);
return fText.BuildGeometry(new Point(0, 0)).GetAsFrozen() as Geometry;
Brushes.Black);
return formattedText.BuildGeometry(new Point(0, 0)).GetAsFrozen() as Geometry;
}
private static string GetLetterCharacter(string name)
private static char GetLetterCharacter(char name)
{
string nameToDisplay;
Debug.Assert(name == char.ToUpperInvariant(name), "Always provide uppercase character names to this method.");
if (Settings.Default.ForceUppercase)
{
nameToDisplay = name;
return name;
}
else
{
nameToDisplay = Utils.GetRandomBoolean() ? name : name.ToLowerInvariant();
}
return nameToDisplay;
// Return a random uppercase or lowercase letter.
return Utils.GetRandomBoolean() ? name : char.ToLowerInvariant(name);
}
}
}

Просмотреть файл

@ -5,6 +5,7 @@ using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Effects;
using BrushControlFunc = System.Func<System.Windows.Media.Brush, System.Windows.Controls.UserControl>;
using BabySmash.Properties;
namespace BabySmash
{
@ -13,7 +14,7 @@ namespace BabySmash
public Brush Fill { get; set; }
public Color Color { get; set; }
public BrushControlFunc GeneratorFunc { get; set; }
public BitmapEffect Effect { get; set; }
public Effect Effect { get; set; }
public string Name { get; set; }
public string Letter { get; set; }
}
@ -36,20 +37,19 @@ namespace BabySmash
public static UserControl NewUserControlFrom(FigureTemplate template)
{
UserControl retVal = null;
//We'll wait for Hardware Accelerated Shader Effects in SP1
if (template.Letter.Length == 1 && Char.IsLetterOrDigit(template.Letter[0]))
{
retVal = new CoolLetter(template.Fill.Clone(), template.Letter);
retVal = new CoolLetter(template.Fill.Clone(), template.Letter[0]);
}
else
{
retVal = template.GeneratorFunc(template.Fill.Clone());
}
Tweener.TransitionType randomTransition1 = (Tweener.TransitionType)Utils.RandomBetweenTwoNumbers(1, (int)Tweener.TransitionType.EaseOutInBounce);
var randomTransition1 = (Tweener.TransitionType)Utils.RandomBetweenTwoNumbers(1, (int)Tweener.TransitionType.EaseOutInBounce);
var ani1 = Tweener.Tween.CreateAnimation(randomTransition1, 0, 1, new TimeSpan(0, 0, 0, 1), 30);
Tweener.TransitionType randomTransition2 = (Tweener.TransitionType)Utils.RandomBetweenTwoNumbers(1, (int)Tweener.TransitionType.EaseOutInBounce);
var randomTransition2 = (Tweener.TransitionType)Utils.RandomBetweenTwoNumbers(1, (int)Tweener.TransitionType.EaseOutInBounce);
var ani2 = Tweener.Tween.CreateAnimation(randomTransition2, 360, 0, new TimeSpan(0, 0, 0, 1), 30);
retVal.RenderTransformOrigin = new Point(0.5, 0.5);
var group = new TransformGroup();
@ -60,11 +60,11 @@ namespace BabySmash
group.Children[0].BeginAnimation(ScaleTransform.ScaleYProperty, ani1);
group.Children[1].BeginAnimation(RotateTransform.AngleProperty, ani2);
//TODO: TOO SLOW! Waiting for ShaderEffects in 3.5SP1
//if (Settings.Default.BitmapEffects)
//{
// retVal.BitmapEffect = template.Effect.Clone();
//}
if (Settings.Default.UseEffects)
{
retVal.Effect = template.Effect.Clone();
}
return retVal;
}
@ -72,7 +72,7 @@ namespace BabySmash
//TODO: Should I change the height, width and stroke to be relative to the screen size?
//TODO: Where can I get REALLY complex shapes like animal vectors or custom pics? Where do I store them?
public static FigureTemplate GenerateFigureTemplate(string letter)
public static FigureTemplate GenerateFigureTemplate(char displayChar)
{
Color c = Utils.GetRandomColor();
@ -80,10 +80,10 @@ namespace BabySmash
return new FigureTemplate
{
Color = c,
Name = (letter.Length == 1 && Char.IsLetterOrDigit(letter[0])) ? letter : nameFunc.Key,
Name = Char.IsLetterOrDigit(displayChar) ? displayChar.ToString() : nameFunc.Key,
GeneratorFunc = nameFunc.Value,
Fill = Utils.GetGradientBrush(c),
Letter = letter,
Letter = displayChar.ToString(),
Effect = Animation.GetRandomBitmapEffect()
};
}

176
WordFinder.cs Normal file
Просмотреть файл

@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace BabySmash
{
public class WordFinder
{
private const int MinimumWordLength = 2, MaximumWordLength = 15;
private bool wordsReady;
private HashSet<string> words = new HashSet<string>();
public WordFinder(string wordsFilePath)
{
// File path provided should be relative to our running location, so combine for full path safety.
string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
wordsFilePath = Path.Combine(dir, wordsFilePath);
// Bail if the source word file is not found.
if (!File.Exists(wordsFilePath))
{
// Source word file was not found; place a 'words.txt' file next to BabySmash.exe to enable combining
// letters into typed words. Some common names may work too (but successful OS speech synth may vary).
return;
}
// Load up the string dictionary in the background.
Thread t = new Thread(() =>
{
// Read through the word file and create a hashtable entry for each one with some
// further parsed word data (such as various game scores, etc)
StreamReader sr = new StreamReader(wordsFilePath);
string s = sr.ReadLine();
while (s != null)
{
// Ignore invalid lines, comment lines, or words which are too short or too long.
if (!s.Contains(";") && !s.Contains("/") && !s.Contains("\\") &&
s.Length >= MinimumWordLength && s.Length <= MaximumWordLength)
{
this.words.Add(s.ToUpper());
}
s = sr.ReadLine();
}
// Store all words into separate buckets based on the last letter for faster compares.
// Mark that we're done loading so we can speak words instead of just letters.
wordsReady = true;
});
t.IsBackground = true;
t.Start();
}
public string LastWord(List<UserControl> figuresQueue)
{
// If not done loading, or could not yet form a word based on queue length, just abort.
int figuresPos = figuresQueue.Count - 1;
if (!this.wordsReady || figuresPos < MinimumWordLength - 1)
{
return null;
}
// Loop while the most recently pressed things are still letters; loop proceeds from the
// most recent letter, back towards the beginning, as we only care about the longest word
// that we JUST now finished typing.
string longestWord = null;
var stringToCheck = new StringBuilder();
int lowestIndexToCheck = Math.Max(0, figuresPos - MaximumWordLength);
while (figuresPos >= lowestIndexToCheck)
{
var lastFigure = figuresQueue[figuresPos] as CoolLetter;
if (lastFigure == null)
{
// If we encounter a non-letter, move on with the best word so far (if any).
// IE typing "o [bracket] p e n" can match word "pen" but not "open" since our
// intention back at [bracket] shows we don't necessarily mean to type "open".
break;
}
// Build up the string and check to see if it is a word so far.
stringToCheck.Insert(0, lastFigure.Character);
string s = stringToCheck.ToString();
if (this.words.Contains(stringToCheck.ToString()) && s.Length >= MinimumWordLength)
{
// Since we're progressively checking longer and longer letter combinations,
// each time we find a word, it is our new "longest" word so far.
longestWord = s;
}
figuresPos--;
}
return longestWord;
}
public void AnimateLettersIntoWord(List<UserControl> figuresQueue, string lastWord)
{
// Prepare to animate the letters into their respective positions, on each screen.
Duration duration = new Duration(TimeSpan.FromMilliseconds(1200));
int totalLetters = lastWord.Length;
Point wordCenter = this.FindWordCenter(figuresQueue, totalLetters);
Point wordSize = this.FindWordSize(figuresQueue, totalLetters);
double wordLeftEdge = wordCenter.X - wordSize.X / 2f;
// Figure out where to move each letter used in the word; find the letters used based on
// the word length; they are the last several figures in the figures queue.
for (int i = figuresQueue.Count - 1; i >= figuresQueue.Count - totalLetters; i--)
{
UserControl currentFigure = figuresQueue[i];
// Find the translation animation of this element, or make one if there is not one yet.
var transformGroup = currentFigure.RenderTransform as TransformGroup;
var transform = FindOrAddTranslationTransform(transformGroup);
// We know where we want to center the word, and the word's left edge based on figure
// sizes, and now just need to figure out how far from that left edge we need to adjust
// to make this letter move to the correct relative position to spell out the word.
double wordOffsetX = 0d;
for (int j = figuresQueue.Count - totalLetters; j < i; j++)
{
wordOffsetX += figuresQueue[j].Width;
}
// Start translating from wherever we were already translated to (or 0 if not yet
// translated) and going to the new position for this letter based for the word.
var wordTranslationX = wordLeftEdge - Canvas.GetLeft(currentFigure);
var wordTranslationY = wordCenter.Y - Canvas.GetTop(currentFigure);
var animationX = new DoubleAnimation(transform.X, wordTranslationX + wordOffsetX, duration);
var animationY = new DoubleAnimation(transform.Y, wordTranslationY, duration);
transform.BeginAnimation(TranslateTransform.XProperty, animationX);
transform.BeginAnimation(TranslateTransform.YProperty, animationY);
}
}
private Point FindWordCenter(List<UserControl> letterQueue, int letterCount)
{
// For now, target centering the word at the average position of all its letters.
var x = (from c in letterQueue select Canvas.GetLeft(c)).Reverse().Take(letterCount).Average();
var y = (from c in letterQueue select Canvas.GetTop(c)).Reverse().Take(letterCount).Average();
return new Point(x, y);
}
private Point FindWordSize(List<UserControl> letterQueue, int letterCount)
{
var x = (from c in letterQueue select c.Width).Reverse().Take(letterCount).Sum();
var y = (from c in letterQueue select c.Height).Reverse().Take(letterCount).Max();
return new Point(x, y);
}
private TranslateTransform FindOrAddTranslationTransform(TransformGroup transformGroup)
{
var translationTransform = (from t in transformGroup.Children
where t is TranslateTransform
select t).FirstOrDefault() as TranslateTransform;
if (translationTransform == null)
{
translationTransform = new TranslateTransform();
transformGroup.Children.Add(translationTransform);
}
return translationTransform;
}
}
}

11
Words.txt Normal file
Просмотреть файл

@ -0,0 +1,11 @@
BABIES
BABY
BANANA
BANANAS
FUN
SMASH
SMASHED
SMASHES
SMASHING
WORD
WORDS

Просмотреть файл

@ -29,7 +29,7 @@
<setting name="FadeAway" serializeAs="String">
<value>True</value>
</setting>
<setting name="BitmapEffects" serializeAs="String">
<setting name="UseEffects" serializeAs="String">
<value>False</value>
</setting>
<setting name="ClearAfter" serializeAs="String">