Logo Turtle Graphics in WPF
Logo is a programming language that somehow involves a turtle. Imagine a turtle on a large piece of paper on the floor. The turtle has a pen. You can command the turtle to go forward, turn right, then repeat 4 times to draw a square.
The last time I wrote about Logo (in 2006) I hadn’t read the current Wikipedia article on it, where it says Logo was invented at BB&N.
I was surprised, because I too worked at BBN, in the early 80’s, designing computer systems to analyze sounds, writing convolutions and Fourier Transforms using Fortran and Macro-11 on PDP 11s and array processors.
I found my first implementation of Logo while digging through some old code. It was written using DeSmet C in 1984. The coordinates for the Turtle below are taken from that code. I remember using my Cartoon animation program to draw the turtle using MSDos. I had to write my own mouse driver to listen via RS-232 serial port to my VisiOn mouse.
The DeSmet compiler is now available in full source code and, because it’s simple, can be a good learning tool. I remember back then I reverse engineered it, dumping out many pages of hex, trying to figure out how it worked.
In 2006, I rewrote my Logo in Foxpro and Visual Basic.Net Winform versions. They both still work in the latest versions of Visual FoxPro and VB. The WPF version below seems to go much faster. I’d expect this because it’s doing direct GDI calls
Follow the directions from this post: DPI Aware Sample to create the base project from which we can inherit some immediate mode graphics functionality.
We’ll now add a project to your hWndHost Solution: Right click on the Solution node in Solution Explorer, choose Add New Project->C# WPF Application. Name it “Logo”
In the new project:
Rename MainWindow.Xaml.cs to “LogoWindow.Xaml.cs”
In App.Xaml, change
StartupUri="MainWindow.xaml">
To
StartupUri="LogoWindow.xaml">
Right click on the Solution->Logo project and “Set As Startup”, which says ,when you hit F5, or Ctrl-F5 to GO, the project to run is the Logo project.
Hit F5 to test: you should see a blank window. Close it.
Right click on the Logo->References->Add reference, add a reference to the Solution->Projects->hWndHost project.
Paste in the code below to replace LogoWindow.Xaml.cs
The current command list is set to draw repeated spirals. The commands are “fr+cd”. “f” means move forward 1 step. “r” means turn right the current angle (defaults to 90 degrees). The “+” means increment the stride length. “c” means change the color a little bit. “d” means delay a little (by the amount on the right slider: the turtle is very fast).
__currentCommandList = "fr+cd"
The code sends a keyboard command “.” which repeats the commands, incrementing the default angle each time.
Try experimenting with the available commands, add new ones. Try the left slider, which changes the pen width.
See also
How to filter out unwanted sounds via Fourier Transform
More fun with the Fast Fourier Transform
<LogoCode>
using hWndHost;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Xml;
namespace Logo
{
public class LogoControl : MyHwndHost
{
public Turtle _Turtle;
public const string strLegalCommands = "flrbhpalrcdn+-";
private string __currentCommandList = "fr+cd";
public string _currentCmdList
{
get
{
return __currentCommandList;
}
set
{
__currentCommandList = value;
UpdateTbxCmdList(__currentCommandList);
}
}
private IntPtr _hBgd;
private bool _fIsRunningScript;
private Point _boundSize;
MainWindow _mainWindow;
public LogoControl(IntPtr hbgd, MainWindow mainWindow)
: base(hbgd)
{
_hBgd = hbgd;
_mainWindow = mainWindow;
this._Turtle = new Turtle(this);
UpdateTbxCmdList(_currentCmdList);
_mainWindow._tbxCmdList.TextChanged += (ot, et) =>
{
{
__currentCommandList = _mainWindow._tbxCmdList.Text;
}
};
}
public void GotTextInput(TextCompositionEventArgs e)
{
if (_fIsRunningScript)
{ //any key will stop running script
_fIsRunningScript = false;
e.Handled = true;
}
else
{
var cmdChar = e.Text.ToLower();
switch (cmdChar)
{
case "q":
App.Current.Shutdown();
break;
case ".":
var nTimes = 10000000;
System.Threading.ThreadPool.QueueUserWorkItem(
o =>
{
_fIsRunningScript = true;
_Turtle._fShowTurtle = false;
PlayBack(_currentCmdList, nTimes);
_fIsRunningScript = false;
_Turtle._fShowTurtle = true;
}
);
cmdChar = string.Empty;
break;
case "e": // erase/reset
EraseRect();
_currentCmdList = string.Empty;
_Turtle.InitVals();
_Turtle._fShowTurtle = true;
_Turtle.Draw();
cmdChar = string.Empty;
break;
}
if (!string.IsNullOrEmpty(cmdChar))
{
_currentCmdList += cmdChar;
_Turtle.DoTurtleDrawCommand(cmdChar);
}
}
}
public void UpdateTbxCmdList(string strCmd)
{
_mainWindow._tbxCmdList.Dispatcher.Invoke(
() =>
{
_mainWindow._tbxCmdList.Text = strCmd;
}
);
}
public void UpdateTbxStatus(string stat)
{
_mainWindow._tbxStatus.Dispatcher.Invoke(
() =>
{
_mainWindow._tbxStatus.Text = stat;
}
);
}
internal void OnKey(KeyEventArgs ek)
{
switch (ek.Key)
{
case Key.Back: // backspace
if (!string.IsNullOrEmpty(_currentCmdList))
{
_currentCmdList = _currentCmdList.Substring(0, _currentCmdList.Length - 1);
}
ek.Handled = true;
break;
case Key.Escape:
App.Current.Shutdown();
break;
}
}
void PlayBack(string currentCmdList, int nTimes)
{
int nReptCnt = 0;
var sw = new System.Diagnostics.Stopwatch();
sw.Start();
for (int i = 0; i < nTimes; i++)
{
nReptCnt++;
if (_Turtle._position.X < 0 ||
_Turtle._position.X > _boundSize.X ||
_Turtle._position.Y < 0 ||
_Turtle._position.Y > _boundSize.Y ||
nReptCnt == 20000
)
{
EraseRect();
nReptCnt = 0;
int nangleStepSave = _Turtle._nAngleStep;
int nStepSizeSave = _Turtle._nStepSize;
_Turtle.InitVals();
_Turtle._nStepSize = (nStepSizeSave + 1) % 20;
_Turtle._nAngleStep = (nangleStepSave + 1) % 360;
UpdateTbxStatus("Angle= " + _Turtle._nAngleStep.ToString());
}
for (int j = 0; j < currentCmdList.Length; j++)
{
_Turtle.DoTurtleDrawCommand(currentCmdList[j].ToString());
if (!_fIsRunningScript)
{
i = nTimes;
break;
}
}
}
sw.Stop();
UpdateTbxStatus(string.Format("Done in {0} secs", sw.Elapsed.TotalSeconds));
}
internal void OnSizeChanged()
{
_boundSize = new Point(
(this.ActualWidth * xScale),
(this.ActualHeight * yScale));
}
public override void OnReady(IntPtr hwnd)
{
if (_boundSize.X == 0)
{
_boundSize = new Point(
(this.ActualWidth * xScale),
(this.ActualHeight * yScale)
);
// send a "." command to start the default program
Keyboard.FocusedElement.RaiseEvent(
new TextCompositionEventArgs(
InputManager.Current.PrimaryKeyboardDevice,
new TextComposition(InputManager.Current, Keyboard.FocusedElement, ".")
) { RoutedEvent = TextCompositionManager.TextInputEvent }
);
}
_Turtle.OnReady(hwnd);
}
public int _Delay { get; set; }
}
public class Turtle
{
IntPtr _hwnd { get { return _logoControl._hwnd; } }
LogoControl _logoControl;
int[,] _turtle = {{0,0}, {1,1}, {1,3}, { 4,5}, { 7,3}, { 7,4}, {5,7}, {7,11},
{7,15}, {6,17}, {7,20}, {4,19}, {1,20}, {2,27}, {0,30} };
public Point _position;
const int _nTurtleSize = 2;
public IntPtr _colorTurtle;
public int _turtleDrawingColor = 0xffffff; // turtle pen color
public IntPtr _penTurtleDrawingPen; // pen of turtle (CreatePen)
public int _nAngleStep;
public double _nAngle; //direction turtle is facing
public bool _fPenDown;
public int _penWidth = 1; // the width of the turtle's pen
public int _nStepSize;
public bool _fShowTurtle = true;
const double piOver180 = Math.PI / 180;
public int _nInput = 0;// user input integer
NativeMethods.WinPoint _prevPos = new NativeMethods.WinPoint(0, 0);
public Turtle(LogoControl logoControl)
{
_logoControl = logoControl;
_colorTurtle = NativeMethods.CreatePen(0, 0, (IntPtr)0x8f00);
InitVals();
}
public void InitVals()
{
_nStepSize = 20;
_nAngle = -90;
_nAngleStep = 90;
_fPenDown = true;
_nInput = 0;
SetColor(0x0);
var rect = new NativeMethods.WinRect();
NativeMethods.GetClientRect(_hwnd, ref rect);
_position = new Point((rect.Right - rect.Left) / 2,
(rect.Bottom - rect.Top) / 2);
}
public void Draw()
{
if (_fShowTurtle)
{
IntPtr hdc = NativeMethods.GetDC(_hwnd);
NativeMethods.SelectObject(hdc, _colorTurtle);
var cosT = Math.Cos((90 + _nAngle) * piOver180); // cos(Theta)
var sinT = Math.Sin((90 + _nAngle) * piOver180); // sin(Theta)
NativeMethods.SetROP2(hdc, NativeMethods.RasterOps.R2_NOTXORPEN);
NativeMethods.MoveToEx(hdc, (int)_position.X, (int)_position.Y, ref _prevPos);
for (int i = 0; i < _turtle.Length / 2; i++)
{
var x1 = _nTurtleSize * _turtle[i, 0];
var y1 = _nTurtleSize * _turtle[i, 1];
NativeMethods.LineTo(
hdc,
(int)(_position.X + x1 * cosT + y1 * sinT),
(int)(_position.Y + x1 * sinT - y1 * cosT)
);
}
// now the other half of the turtle
NativeMethods.MoveToEx(hdc, (int)_position.X, (int)_position.Y, ref _prevPos);
for (int i = 0; i < _turtle.Length / 2; i++)
{
var x1 = -_nTurtleSize * _turtle[i, 0];
var y1 = -_nTurtleSize * _turtle[i, 1];
NativeMethods.LineTo(
hdc,
(int)(_position.X + x1 * cosT - y1 * sinT),
(int)(_position.Y + x1 * sinT + y1 * cosT)
);
}
var rect = new NativeMethods.WinRect();
NativeMethods.GetClientRect(_hwnd, ref rect);
NativeMethods.ValidateRect(_hwnd, ref rect);
NativeMethods.SetROP2(hdc, NativeMethods.RasterOps.R2_COPYPEN);
NativeMethods.ReleaseDC(_hwnd, hdc);
}
}
public void DoTurtleDrawCommand(string cmd)
{
int nInputParam = _nInput;
if (char.IsDigit(cmd[0]))
{
_nInput = _nInput * 10 + cmd[0] - 48;
}
else
{
_nInput = 0; // reset
if (nInputParam == 0)
{
nInputParam = 1;
}
Draw(); // hide turtle
Point newpos;
switch (cmd[0])
{
case 'f': // forward
newpos = new Point(
(_position.X +
Math.Cos(_nAngle * piOver180) *
_nStepSize *
nInputParam
),
(_position.Y +
Math.Sin(_nAngle * piOver180) *
_nStepSize *
nInputParam
)
);
if (_fPenDown)
{
var hdc = NativeMethods.GetDC(_hwnd);
if (_penTurtleDrawingPen == IntPtr.Zero)
{
SetColor(_turtleDrawingColor);
}
var old = NativeMethods.SelectObject(hdc, _penTurtleDrawingPen);
NativeMethods.MoveToEx(hdc, (int)_position.X, (int)_position.Y, ref _prevPos);
NativeMethods.LineTo(hdc, (int)newpos.X, (int)newpos.Y);
NativeMethods.ReleaseDC(_hwnd, hdc);
}
_position = newpos;
break;
case 'l': //left
_nAngle = (_nAngle - _nAngleStep) % 360;
break;
case 'r': //right
_nAngle = (_nAngle + _nAngleStep) % 360;
break;
case 'a': // angle
if (nInputParam == 99)
{
_nAngleStep = (new Random()).Next(100);
}
else
{
_nAngleStep += nInputParam;
}
break;
case 'p': // pen up/down
_fPenDown = !_fPenDown;
break;
case '+': // increment step size
if (nInputParam == 99)
{
_nStepSize = (new Random()).Next(100) - 50;
}
else
{
_nStepSize += nInputParam;
}
break;
case '-': // decrement step size
_nStepSize -= nInputParam;
break;
case 'd': // delay
// System.Threading.Thread.Sleep(nInputParam);
var delay = _logoControl._Delay;
if (delay == 0)
{
delay = nInputParam;
}
var sw = new System.Diagnostics.Stopwatch();
sw.Start();
while (sw.ElapsedTicks < delay)
{
}
break;
case 'c': // color
if (nInputParam == 1)
{
nInputParam = 140; // a little more pronounced
}
SetColor((int)((_turtleDrawingColor + nInputParam) & 0xffffff));
break;
case 'h': // hide
_fShowTurtle = !_fShowTurtle;
break;
}
Draw();
}
}
void SetColor(int nColor)
{
{
_turtleDrawingColor = nColor;
if (_penTurtleDrawingPen != null)
{
NativeMethods.DeleteObject(_penTurtleDrawingPen);
}
_penTurtleDrawingPen = NativeMethods.CreatePen(
0, // pen Style
_penWidth,
(IntPtr)_turtleDrawingColor);
}
}
internal void OnReady(IntPtr hwnd)
{
_fShowTurtle = true;
InitVals();
Draw();
}
public override string ToString()
{
return string.Format("{0}", _position);
}
internal void SetPenWidth(int p)
{
_penWidth = p;
SetColor(_turtleDrawingColor);
}
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public LogoControl _logoControl;
public TextBox _tbxCmdList;
public TextBox _tbxStatus;
public MainWindow()
{
InitializeComponent();
this.WindowState = WindowState.Maximized;
this.Title = "Logo";
this.Loaded += (o, e) =>
{
this.Top = 0;
this.Left = 0;
this.Width = 800;
this.Height = 800;
try
{
//there are a lot of quotes in XAML
//and the C# string requires quotes to be doubled
var strxaml = @" <Grid
xmlns=""https://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x=""https://schemas.microsoft.com/winfx/2006/xaml""
Name=""HwndTest"" Margin=""5,5,5,5"">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height=""25""></RowDefinition>
</Grid.RowDefinitions>
<DockPanel Grid.Row=""0"">
<UserControl Name=""MyUserControl""></UserControl>
</DockPanel>
<DockPanel Grid.Row=""1"">
<TextBox
Name=""tbxStatus""
HorizontalAlignment=""Left""
Height=""23""
Margin=""10,2,0,0""
IsReadOnly=""True""
TextWrapping=""Wrap""
VerticalAlignment=""Top""
Width=""120""/>
<TextBox
Name=""tbxCmdList""
HorizontalAlignment=""Left""
Height=""23""
Margin=""10,2,0,0""
TextWrapping=""Wrap""
VerticalAlignment=""Top""
Width=""320""/>
<Slider
Name=""sldPenWidth""
HorizontalAlignment=""Left""
Margin=""12,2,0,0""
VerticalAlignment=""Top""
Width=""100""
ToolTip=""Change the pen width""
/>
<Slider
Name=""sldDelay""
HorizontalAlignment=""Left""
Margin=""12,2,0,0""
VerticalAlignment=""Top""
ToolTip=""Change the delay""
Width=""100""/>
<Button
Name=""btnQuit""
Content=""_Quit""
HorizontalAlignment=""Left""
Margin=""10,2,0,0""
VerticalAlignment=""Top""
Width=""55""/>
</DockPanel>
</Grid>
";
var strReader = new System.IO.StringReader(strxaml);
var xamlreader = XmlReader.Create(strReader);
var grid = (Grid)(System.Windows.Markup.XamlReader.Load(xamlreader));
var btnQuit = (Button)grid.FindName("btnQuit");
btnQuit.Click += (ob, eb) =>
{
App.Current.Shutdown();
};
_tbxCmdList = (TextBox)grid.FindName("tbxCmdList");
_tbxStatus = (TextBox)grid.FindName("tbxStatus");
var userCtrl = (UserControl)grid.FindName("MyUserControl");
var bgd = NativeMethods.CreateSolidBrush(new IntPtr(0xffffff));
_logoControl = new LogoControl(bgd, this);
userCtrl.Content = _logoControl;
userCtrl.Focusable = true;
userCtrl.IsTabStop = true;
this.Content = grid;
var sldPenWidth = (Slider)grid.FindName("sldPenWidth");
sldPenWidth.Minimum = 0;
sldPenWidth.Maximum = 400;
sldPenWidth.ValueChanged += (os, es) =>
{
_logoControl._Turtle.SetPenWidth((int)sldPenWidth.Value);
};
var sldDelay = (Slider)grid.FindName("sldDelay");
sldDelay.Minimum = 0;
sldDelay.Maximum = 100000;
sldDelay.ValueChanged += (os, es) =>
{
_logoControl._Delay = (int)sldDelay.Value;
};
this.TextInput += (ot, et) =>
{
_logoControl.GotTextInput(et);
};
userCtrl.KeyUp += (ok, ek) =>
{
_logoControl.OnKey(ek);
};
this.SizeChanged += (os, es) =>
{
_logoControl.OnSizeChanged();
};
btnQuit.ToolTip = @"Logo Program by Calvin Hsia
Logo Drawing Program by Calvin Hsia
based on Seymour Papert's Logo
f = Move Forward the Stepsize (*)
r = turn Right
l = turn Left
p = Pen Up (off the paper) or down
h = Hide turtle
e = Erase and start over (prerecorded programs survive)
+ = Increase the turtle's step size for Forward
- = Decrease the step size
a = Increase the angle for left/right
. = Repeat current command indefinitely
q = Quit out of program
c = Change Color (*) InputValue
d = Delay (*)
n = Number for User Parameter. Used for 'x'
s = Store (*) cmd. Inputvalue indicates which storage cell (1-9)
x = Execute stored program (*) User parameter times
[0-9] = input integer into Input value (defaults to 1)
[any char] while executing will stop turtle
* = command will use the Input value for parameter
";
}
catch (Exception ex)
{
this.Content = ex.ToString();
}
};
}
}
}
</LogoCode>
Comments
Anonymous
August 29, 2014
chương trình cho cái gì vậy ta giaitrimobile24h.blogspot.com/.../tai-game-ngoc-rong-online.htmlAnonymous
August 30, 2014
Also see Logo inspired Turtle in MS Small Basic teaching language for beginners http://smallbasic.com/doc.aspx