Commits

Herbert Breunung committed 9f567c8 Draft

version 0.89

Comments (0)

Files changed (13)

bin/harmonograph.pl

 #!/usr/bin/perl
 use v5.12;
 use warnings;
+use Wx;
+use Wx::Perl::RadioGroup;
+use Wx::Perl::TextSlider;
+use YAML::Tiny;
+use Math::Trig;
+
 
 package Harmonograph;
-our $VERSION = '0.75';
+our $VERSION = '0.78';
 use base qw(Wx::App);
-use Wx qw/ :everything /;
-use Math::Trig;
-my %app;
-my $maltafelgroesse = 450;
+my $maltafelgroesse = 500;
 my $midoffset = $maltafelgroesse / 2;
-my $amp = 200;
-my @label = qw(X Y X Y Betrag Reibung Dauer Zoom Farbe);
-my @name = qw(freqx freqy ampx ampy rotation friction length zoom color);
-my @ranges = (
-	[1,1,27],[1,1,27], [0,$amp, $amp*2], [0,$amp, $amp*2],
-	[0,1,20],[0,0,100],[0,12,100], [-10,0,10], [0, 0, 20],
+my $amp = 220;
+my %range_defaults = ( # label, min, max, init
+    freqx => ['X',      1, 1,    27], freqy    => ['Y',      1, 1,     27],
+    ampx  => ['X',   $amp, 0,$amp*2], ampy     => ['Y',   $amp, 0, $amp*2],
+    rota  => ['Betrag', 1, 0,    20], friction => ['Reibung',0, 0,    100],
+ 'length' => ['Dauer', 12, 0,   100],
+     zoom => ['Zoom',   0,-10,   10], color    => ['Farbe',  0, 0,     30],
 );
 
 
 sub OnInit {
 	my $app = shift;
 	my $frame = Wx::Frame->new(
-		undef,-1, __PACKAGE__." $VERSION", [-1,-1],[750,465],
-		&Wx::wxCAPTION | &Wx::wxCLOSE_BOX | &Wx::wxFRAME_TOOL_WINDOW | &Wx::wxSYSTEM_MENU
+		undef, -1, __PACKAGE__." $VERSION", [-1,-1],[700,-1],
+        &Wx::wxCAPTION | &Wx::wxCLOSE_BOX | &Wx::wxFRAME_TOOL_WINDOW | &Wx::wxSYSTEM_MENU | &Wx::wxRESIZE_BORDER
 	);
 	$frame->SetIcon( Wx::GetWxPerlIcon() );
 
-	$app{'maltafel'} = Wx::StaticBitmap->new($frame, -1, &Wx::wxNullBitmap);
-	$app{'bmp'}   = Wx::Bitmap->new( $maltafelgroesse, $maltafelgroesse);
-	$app{'dc'}  = Wx::MemoryDC->new();
-	$app{'dc'}->SelectObject( $app{'bmp'} );
-	$app{'panel'} = create_knob_panel($frame);
+	$app->{'maltafel'} = Wx::StaticBitmap->new($frame, -1, &Wx::wxNullBitmap);
+	$app->{'maltafel'}->SetMinSize([$maltafelgroesse, $maltafelgroesse]);
+	$app->{'bmp'}   = Wx::Bitmap->new( $maltafelgroesse, $maltafelgroesse);
+	$app->{'dc'}  = Wx::MemoryDC->new();
+	$app->{'dc'}->SelectObject( $app->{'bmp'} );
+
+	my $panel = $app->{'panel'} = Wx::Panel->new($frame);                       # right side control panel
+	my %sizer;
+	$sizer{'main'} = Wx::BoxSizer->new(&Wx::wxVERTICAL);
+	my $callback = sub { $app->repaint() };
+	my $sizer_al = &Wx::wxALL|&Wx::wxGROW;
+	$panel->{$_} = Wx::Perl::TextSlider->new
+		($panel, @{$range_defaults{$_}}, $callback) for keys %range_defaults;
+
+	$panel->{'yinvers'} = Wx::CheckBox->new($panel, -1,'Y - Richtung invers');
+	Wx::Event::EVT_CHECKBOX( $panel->{'yinvers'}, -1, $callback );
+
+	$panel->{'rot_richtung'} = Wx::Perl::RadioGroup->new(
+		$panel, 'keine', [qw(keine links rechts)], &Wx::wxHORIZONTAL, $callback
+	);
+
+	 Wx::Perl::Box->new();
+	for my $label (qw(Frequenz Startamplitude Rotation)){
+		my $bname = lc substr($label, 0, 4) . '_box';
+		$sizer{$bname} = Wx::StaticBoxSizer->new
+			( Wx::StaticBox->new($panel, -1, " $label "), &Wx::wxVERTICAL);
+		$sizer{'main'}->Add($sizer{$bname}, 0, $sizer_al, 5);
+	}
+	$sizer{'freq_box'}->Add($panel->{ $_ },       0, $sizer_al, 5) for qw(freqx freqy);
+	$sizer{'freq_box'}->Add($panel->{'yinvers'}, 0, &Wx::wxLEFT, 45 );
+	$sizer{'freq_box'}->AddSpacer(5);
+	$sizer{'star_box'}->Add($panel->{ $_ },       0, $sizer_al, 5) for qw(ampx ampy);
+	$sizer{'rota_box'}->Add($panel->{'rot_richtung'},  0, &Wx::wxLEFT, 10);
+	$sizer{'rota_box'}->Add($panel->{'rota'}, 0, $sizer_al, 5);
+	$sizer{'main'}->Add( $panel->{'friction'},     0, $sizer_al, 5);
+	$sizer{'main'}->Add( $panel->{'length'  },     0, $sizer_al, 5);
+	$sizer{'main'}->Add( $panel->{'zoom'  },       0, $sizer_al, 5);
+	$sizer{'main'}->Add( $panel->{'color' },       0, $sizer_al, 5);
+	$sizer{'main'}->AddSpacer(10);
+	$panel->SetSizerAndFit( $sizer{'main'} );
 
 	my $sizer = Wx::BoxSizer->new( &Wx::wxHORIZONTAL);
-	$sizer->Add( $app{'maltafel'}, 0, &Wx::wxALL|&Wx::wxGROW, 0);
-	$sizer->Add( $app{'panel'},    1, &Wx::wxALL|&Wx::wxGROW, 0);
-	$frame->SetSizer( $sizer );
+	$sizer->Add( $app->{'maltafel'}, 1, &Wx::wxALL|&Wx::wxGROW, 0);
+	$sizer->Add( $app->{'panel'},    1, &Wx::wxALL|&Wx::wxGROW, 0);
+	$frame->SetSizerAndFit( $sizer );
 
-	set_defaults('all');
-	repaint();
+	$app->set_defaults('all');
+	$app->repaint();
 	$frame->Center();
 	$frame->Show(1);
 	$app->SetTopWindow($frame);
 	1;
 }
 
-sub create_knob_panel {
-	my $frame = shift;
-	my $app = $frame->GetParent;
-	my %sizer;
-	my $panel = Wx::Panel->new($frame, -1);                       # right side control panel
-	$sizer{'main'} = Wx::BoxSizer->new(&Wx::wxVERTICAL);
-
-	my $std_al = &Wx::wxALIGN_CENTER_VERTICAL | &Wx::wxALL;
-	my $sizer_al = &Wx::wxALL|&Wx::wxGROW;
-	my $last_al = &Wx::wxLEFT|&Wx::wxRIGHT|&Wx::wxGROW;
-	for my $nr (0 .. $#ranges) {
-		my $name = $name[$nr];
-		my $frontlabel = Wx::StaticText->new($panel, -1, $label[$nr].' : ');
-		my $slider = $panel->{'slider_'.$name} = Wx::Slider->new($panel,-1,0,0,1);
-		my $text = $panel->{'text_'.$name} = 
-			Wx::TextCtrl->new($panel,-1,'',[-1,-1],[40,-1], &Wx::wxTE_READONLY);
-
-		$sizer{$name} = Wx::BoxSizer->new(&Wx::wxHORIZONTAL);
-		$sizer{$name}->Add($frontlabel, 0, $std_al, 5 );
-		$sizer{$name}->Add($slider,     1, $std_al, 5 );
-		$sizer{$name}->Add($text,       0, $std_al, 0 );
-		Wx::Event::EVT_SCROLL( $slider, sub {
-			$text->SetValue( $slider->GetValue) if $text->GetValue ne $slider->GetValue;
-		} );
-		Wx::Event::EVT_SCROLL_THUMBRELEASE( $slider, sub { repaint() } );
-		Wx::Event::EVT_MIDDLE_DOWN( $slider, sub { set_defaults($nr); repaint() } );
-	}
-	$panel->{'invers'} = Wx::CheckBox->new($panel, -1,'Y - Richtung invers');
-	Wx::Event::EVT_CHECKBOX( $panel->{'invers'}, -1, sub { repaint() } );
-
-	$sizer{'rotline'} = Wx::BoxSizer->new(&Wx::wxHORIZONTAL);
-	for my $label (qw(keine links rechts)) {
-		my $name = $label.'rot';
-		$panel->{$name} = Wx::RadioButton->new($panel, -1, $label);
-		$sizer{'rotline'}->Add( $panel->{$name}, 0, $sizer_al, 5);
-		Wx::Event::EVT_RADIOBUTTON($panel->{$name}, -1, sub { repaint() } )
-	}
-	for my $label (qw(Frequenz Startamplitude Rotation)){
-		my $bname = lc substr($label, 0, 4) . '_box';
-		$sizer{$bname} = Wx::StaticBoxSizer->new
-			( Wx::StaticBox->new($panel, -1, " $label "), &Wx::wxVERTICAL);
-		$sizer{'main'}->Add($sizer{$bname}, 0, $sizer_al, 5);
-	}
-	$sizer{'freq_box'}->Add($sizer{ $_ },       0, $sizer_al, 5) for qw(freqx freqy);
-	$sizer{'freq_box'}->Add($panel->{'invers'}, 0, &Wx::wxLEFT, 45 );
-	$sizer{'freq_box'}->AddSpacer(5);
-	$sizer{'star_box'}->Add($sizer{ $_ },       0, $sizer_al, 5) for qw(ampx ampy);
-	$sizer{'rota_box'}->Add($sizer{'rotline'},  0, &Wx::wxLEFT, 10);
-	$sizer{'rota_box'}->Add($sizer{'rotation'}, 0, $sizer_al, 5);
-	$sizer{'main'}->Add($sizer{'friction'},     0, $last_al, 10);
-	$sizer{'main'}->Add($sizer{'length'  },     0, $last_al, 10);
-	$sizer{'main'}->Add($sizer{'zoom'  },       0, $last_al, 10);
-	$sizer{'main'}->Add($sizer{'color' },       0, $last_al|&Wx::wxBOTTOM, 10);
-	$panel->SetSizer( $sizer{'main'} );
-	return $panel;
-}
-
 sub set_defaults {
+	my $app = shift;
 	my $which = shift;
 	if ($which eq 'all'){
-		$app{'panel'}{'keinerot'}->SetValue(1);
-		set_defaults($_) for 0 .. $#ranges;
-	} else {
-		my $name = '_' . $name[$which];
-		$app{'panel'}{'slider'.$name}->SetRange(@{$ranges[$which]}[0,2]);
-		$app{'panel'}{'slider'.$name}->SetValue($ranges[$which][1]);
-		$app{'panel'}{'text' . $name}->SetValue($ranges[$which][1]);
-	}
+		$app->{'panel'}{'rot_richtung'}->ResetValue();
+		$app->set_defaults($_) for keys %range_defaults;
+	} 
+	else { $app->{'panel'}{$which}->ResetValue }
+	$app;
 }
 
 sub repaint {
-	$app{'dc'}->Clear();
-	my $panel = $app{'panel'};
+	my $app = shift;
+	
+	$app->{'dc'}->Clear();
+	my $panel = $app->{'panel'};
 	my $pi = 3.141592654; # deg2rad($degrees);
-	my $dx = .01 / $panel->{'slider_freqx'}->GetValue();
-	my $dy = .01 / $panel->{'slider_freqy'}->GetValue();
-	$dy = $panel->{'invers'}->IsChecked() ? - $dy : $dy;
-	my $x = asin( ($panel->{'slider_ampx'}->GetValue-$amp)/$amp );
-	my $y = asin( ($panel->{'slider_ampy'}->GetValue-$amp)/$amp );
-	my $drot = $panel->{'slider_rotation'}->GetValue / 2000;
-	$drot = - $drot if $panel->{'rechtsrot'}->GetValue();
-	my $friction = 1 - 0.000001 * $panel->{'slider_friction'}->GetValue;
-	my $duration = 4000 * $panel->{'slider_length'}->GetValue;
-	my $zoomfaktor = 1.2**$panel->{'slider_zoom'}->GetValue;
-	my $colormorph = $panel->{'slider_color'}->GetValue/1000;
+	my $dx = .01 / $panel->{'freqx'}->GetValue();
+	my $dy = .01 / $panel->{'freqy'}->GetValue();
+	$dy = $panel->{'yinvers'}->IsChecked() ? - $dy : $dy;
+	my $x = Math::Trig::asin( ($panel->{'ampx'}->GetValue - $amp)/$amp );
+	my $y = Math::Trig::asin( ($panel->{'ampy'}->GetValue - $amp)/$amp );
+	my $drot = $panel->{'rota'}->GetValue / 2000;
+	my $rot_richtung = $panel->{'rot_richtung'}->GetValue();
+	$drot = - $drot if $rot_richtung eq 'rechts';
+	my $friction = 1 - 0.000001 * $panel->{'friction'}->GetValue;
+	my $duration = 4000 * $panel->{'length'}->GetValue;
+	my $zoomfaktor = 1.2**$panel->{'zoom'}->GetValue;
+	my $colormorph = $panel->{'color'}->GetValue/1000;
 	my $color = 0;
 	my $rot = 0;
 	my $cur_amplit = $amp;
 		elsif ($color < 1024){ $b = 255; $g = 1023 - $color;}
 		elsif ($color < 1280){ $b = 255; $r = $color - 1023;}
 		elsif ($color < 1535){ $r = 255; $b = 1534 - $color;}
-		$app{'dc'}->SetPen( Wx::Pen->new( Wx::Colour->new( $r, $g, $b ), 1, &Wx::wxSOLID) );
+		$app->{'dc'}->SetPen( Wx::Pen->new( Wx::Colour->new( $r, $g, $b ), 1, &Wx::wxSOLID) );
 		my $xs = sin($x += $dx);
 		my $ys = sin($y += $dy);
 		$cur_amplit *= $friction;
 		$rot += $drot;
-		($xs, $ys) = rotate($xs, $ys, $rot) unless $panel->{'keinerot'}->GetValue();
-		$app{'dc'}->DrawPoint( ($xs*$cur_amplit*$zoomfaktor)+$midoffset,
+		($xs, $ys) = rotate($xs, $ys, $rot) unless $rot_richtung eq 'keine';
+		$app->{'dc'}->DrawPoint( ($xs*$cur_amplit*$zoomfaktor)+$midoffset,
 		                       ($ys*$cur_amplit*$zoomfaktor)+$midoffset );
 	}
-	$app{'maltafel'}->SetBitmap( $app{'bmp'} );
-	$app{'dc'}->SelectObject( $app{'bmp'} );
-	$app{'maltafel'}->Refresh();
-	$panel->{'text_'.$name[$_]}->SetValue( $panel->{'slider_'.$name[$_]}->GetValue) for 0 .. $#ranges;
+	$app->{'maltafel'}->SetBitmap( $app->{'bmp'} );
+	$app->{'dc'}->SelectObject( $app->{'bmp'} );
+	$app->{'maltafel'}->Refresh();
 }
 
 sub rotate {
 	return ($cosr*$x - $sinr*$y, $sinr*$x + $cosr*$y);
 }
 
+package Wx::Perl::Box;
+use base qw(Wx::Panel);
+sub new {
+	my ($class, $parent, $label, $widgets) = @_;
+	return unless ref $widgets eq 'ARRAY';
+	my ($self) = $class->SUPER::new( $parent );
+	my ($sizer) = Wx::StaticBoxSizer->new 
+			(Wx::StaticBox->new($self, -1, " $label "), &Wx::wxVERTICAL);
+
+	for my $widget (@$widgets) {
+		next unless ref $widget and $widget->isa('Wx::Control');
+		$sizer->Add( $widget, 0, &Wx::wxLEFT, 0); # 10
+		$widget->Reparent( $self );
+	}
+	$self->SetSizerAndFit( $sizer );
+	$self;
+}
+
+
 package main;
 Harmonograph->new->MainLoop;

lib/App/Harmonograph.pm

-package App::Harmonograph;
-
-use 5.006;
-use strict;
-use warnings;
-
-=head1 NAME
-
-App::Harmonograph - The great new App::Harmonograph!
-
-=head1 VERSION
-
-Version 0.01
-
-=cut
-
-our $VERSION = '0.01';
-
-
-=head1 SYNOPSIS
-
-Quick summary of what the module does.
-
-Perhaps a little code snippet.
-
-    use App::Harmonograph;
-
-    my $foo = App::Harmonograph->new();
-    ...
-
-=head1 EXPORT
-
-A list of functions that can be exported.  You can delete this section
-if you don't export anything, such as for a purely object-oriented module.
-
-=head1 SUBROUTINES/METHODS
-
-=head2 function1
-
-=cut
-
-sub function1 {
-}
-
-=head2 function2
-
-=cut
-
-sub function2 {
-}
-
-=head1 AUTHOR
-
-Herbert Breunung, C<< <lichtkind at cpan.org> >>
-
-=head1 BUGS
-
-Please report any bugs or feature requests to C<bug-app-harmonograph at rt.cpan.org>, or through
-the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=App-Harmonograph>.  I will be notified, and then you'll
-automatically be notified of progress on your bug as I make changes.
-
-
-
-
-=head1 SUPPORT
-
-You can find documentation for this module with the perldoc command.
-
-    perldoc App::Harmonograph
-
-
-You can also look for information at:
-
-=over 4
-
-=item * RT: CPAN's request tracker (report bugs here)
-
-L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=App-Harmonograph>
-
-=item * AnnoCPAN: Annotated CPAN documentation
-
-L<http://annocpan.org/dist/App-Harmonograph>
-
-=item * CPAN Ratings
-
-L<http://cpanratings.perl.org/d/App-Harmonograph>
-
-=item * Search CPAN
-
-L<http://search.cpan.org/dist/App-Harmonograph/>
-
-=back
-
-
-=head1 ACKNOWLEDGEMENTS
-
-
-=head1 LICENSE AND COPYRIGHT
-
-Copyright 2011 Herbert Breunung.
-
-This program is free software; you can redistribute it and/or modify it
-under the terms of either: the GNU General Public License as published
-by the Free Software Foundation; or the Artistic License.
-
-See http://dev.perl.org/licenses/ for more information.
-
-
-=cut
-
-1; # End of App::Harmonograph

lib/Colour/Flow.pm

+use v5.12;
+use Wx;
+
+package Colour::Flow;
+
+
+sub new {
+	my ($class, $mode, $steps, $start) = @_;
+	$mode =   1 unless defined $mode;
+	$steps=  10 unless defined $steps;
+	$start=   0 unless defined $start;
+	my ($r, $g, $b);
+	my ($phases, $colours);
+	if    ($mode == 1){ $r = 255; $g =   0; $b = 0; $phases = 6; $colours = 256}
+	elsif ($mode == 2){ $r = 255; $g =   0; $b = 0; $phases = 9; $colours = 128}
+	elsif ($mode == 3){ $r = 255; $g =   0; $b = 0; $phases = 3; $colours = 256}
+	elsif ($mode == 4){ $r = 255; $g = 256; $b = 0; $phases = 3; $colours = 256}
+
+	#$steps = int $steps; 
+	my $self = bless {
+		r => $r,   g => $g,  b => $b,     mode => $mode,
+		steps => $steps,     step_cc => 1,
+		colours => $colours, colour_cc => 1,
+		phases => $phases,   phase_cc => 1,
+	};
+	$self->next_colour() for 1 ..  $start;
+	return $self;
+}
+
+sub get_rgb { my $self = shift; return $self->{'r'}, $self->{'g'}, $self->{'b'} }
+
+sub next_colour {
+	my $self = shift;
+	if ($self->{'mode'} == 1){
+		if    ($self->{'phase_cc'} == 1){$self->{'g'}++}
+		elsif ($self->{'phase_cc'} == 2){$self->{'r'}--}
+		elsif ($self->{'phase_cc'} == 3){$self->{'b'}++}
+		elsif ($self->{'phase_cc'} == 4){$self->{'g'}--}
+		elsif ($self->{'phase_cc'} == 5){$self->{'r'}++}
+		elsif ($self->{'phase_cc'} == 6){$self->{'b'}--}
+	} elsif ($self->{'mode'} == 2){
+		if    ($self->{'phase_cc'} == 1){                $self->{'g'}++}
+		elsif ($self->{'phase_cc'} == 2){$self->{'r'}--; $self->{'g'}++}
+		elsif ($self->{'phase_cc'} == 3){$self->{'r'}--}
+		elsif ($self->{'phase_cc'} == 4){                $self->{'b'}++}
+		elsif ($self->{'phase_cc'} == 5){$self->{'g'}--; $self->{'b'}++}
+		elsif ($self->{'phase_cc'} == 6){$self->{'g'}--}
+		elsif ($self->{'phase_cc'} == 7){                $self->{'r'}++}
+		elsif ($self->{'phase_cc'} == 8){$self->{'b'}--; $self->{'r'}++}
+		elsif ($self->{'phase_cc'} == 9){$self->{'b'}--}
+	} elsif ($self->{'mode'} == 3){
+		if    ($self->{'phase_cc'} == 1){$self->{'r'}--; $self->{'g'}++}
+		elsif ($self->{'phase_cc'} == 2){$self->{'g'}--; $self->{'b'}++}
+		elsif ($self->{'phase_cc'} == 3){$self->{'b'}--; $self->{'r'}++}
+	} elsif ($self->{'mode'} == 4){
+		if    ($self->{'phase_cc'} == 1){$self->{'r'}--; $self->{'b'}++}
+		elsif ($self->{'phase_cc'} == 2){$self->{'g'}--; $self->{'r'}++}
+		elsif ($self->{'phase_cc'} == 3){$self->{'b'}--; $self->{'g'}++}
+	}
+	if (++$self->{'colour_cc'} == $self->{'colours'}){
+		$self->{'colour_cc'} = 1;
+		$self->{'phase_cc'} = 1 if $self->{'phase_cc'}++ == $self->{'phases'};
+	}
+}
+
+sub next_step {
+	my $self = shift;
+	if ($self->{'steps'}){
+		unless ($self->{'did_steps'}++ < $self->{'steps'}){
+			$self->next_colour();
+			$self->{'did_steps'} = 1;
+			return 1;
+		}
+	}
+	return 0;
+}
+
+1;

lib/Wx/Perl/RadioGroup.pm

+use v5.12;
+use warnings;
+
+package Wx::Perl::RadioGroup;
+use base qw(Wx::Panel);
+sub new {
+	my ($class, $parent, $default, $label, $orient, $callback) = @_;
+	return unless ref $label eq 'ARRAY';
+	$orient = &Wx::wxHORIZONTAL unless defined $orient;
+	$callback = sub {} if not defined $callback or ref $callback ne 'CODE';
+	
+	my ($self) = $class->SUPER::new( $parent );
+	my ($sizer) = Wx::BoxSizer->new( $orient );
+	$self->{'default'} = $default;
+	$self->{'label'}   = $label;
+
+	for my $clabel (@$label) {
+		next unless $clabel;
+		my $style = $clabel eq $label->[0] ? &Wx::wxRB_GROUP : 0;
+		$self->{$clabel} = Wx::RadioButton->new($self, -1, $clabel,[-1,-1],[-1,-1], $style);
+		$sizer->Add( $self->{$clabel}, 0, &Wx::wxGROW | &Wx::wxALL, 5);
+		Wx::Event::EVT_RADIOBUTTON($self->{$clabel}, -1, $callback );
+	}
+
+	$self->SetSizerAndFit( $sizer );
+	$self->ResetValue ();
+	$self;
+}
+sub GetValue {
+	my ($self) = shift;
+	for my $label (@{ $self->{'label'} }) {
+		next unless $label;
+		return $label if $self->{$label}->GetValue;
+	}
+}
+sub SetValue {
+	my ($self, $value) = @_;
+	$self->{$value}->SetValue(1) if ref $self->{$value} eq 'Wx::RadioButton';
+}
+sub ResetValue{
+	my ($self) = shift;
+	$self->SetValue( $self->{'default'} );
+}
+
+1;

lib/Wx/Perl/Smart/Frame.pm

+use v5.12;
+use warnings;
+use Wx;
+use Wx::Perl::Smart::Sizer;
+use Wx::Perl::Smart::Panel;
+
+package Wx::Perl::Smart::Frame;
+use base qw(Wx::Frame);
+use Scalar::Util qw(blessed looks_like_number);
+#sub new {
+	#my ($class, $parent, $widgets, $border) = @_;
+	#return unless ref $widgets eq 'ARRAY';
+
+	#my ($self)  = $class->SUPER::new( $parent );
+	#my $callback = sub { $_[0]->Reparent($self) if $_[0]->isa('Wx::Window') };
+	#$self->{'sizer'} = Wx::Perl::Sizer->new($widgets, undef, $border, $callback);
+	#$self->SetSizerAndFit( $self->{'sizer'} );
+	#$self;
+#}
+
+sub SubscribeWidgets {
+	my ($self, $widgets) = @_;
+	#my $widgets = shift
+	#for my $widget
+	#die unless 
+}
+
+sub SetSmartLayout {
+	my ($self) = shift;
+#say $self->GetParent;# 
+
+	$self->SetSizerAndFit( 
+		Wx::Perl::Smart::Sizer->new(undef, [ 
+			Wx::Perl::Smart::Panel->new( $self, [ @_ ] ) 
+		])
+	);
+}
+
+1;

lib/Wx/Perl/Smart/LabeledBox.pm

+use v5.12;
+use warnings;
+use Wx;
+use Wx::Perl::Smart::Sizer;
+
+package Wx::Perl::Smart::LabeledBox;
+use base qw(Wx::Panel);
+sub new {
+	my ($class, $parent, $label, $widgets, $orientation, $init_arg) = @_;
+	return unless ref $widgets eq 'ARRAY';
+	my ($self) = $class->SUPER::new( $parent );
+	my ($sizer) = Wx::StaticBoxSizer->new 
+			(Wx::StaticBox->new($self, -1, " $label "), &Wx::wxVERTICAL);
+	$sizer->Add( Wx::Perl::Smart::Sizer->new
+			($self, $widgets, $orientation, $init_arg), 0, &Wx::wxGROW);
+	$self->SetSizerAndFit( $sizer );
+	$self;
+}
+
+1;

lib/Wx/Perl/Smart/Panel.pm

+use v5.12;
+use warnings;
+use Wx;
+use Wx::Perl::Smart::Sizer;
+
+package Wx::Perl::Smart::Panel;
+use base qw(Wx::Panel);
+use Scalar::Util qw(blessed);
+
+sub new {
+	my ($class, $parent, $widgets, $orientation, $init_arg) = @_;
+	die __PACKAGE__.'::new a widget as first arg'
+		if  defined $parent and not (blessed($parent) and $parent->isa('Wx::Window'));
+	die __PACKAGE__.'::new need a widget list (ARRAY ref) as second arg' unless ref $widgets eq 'ARRAY';
+
+	my ($self)  = $class->SUPER::new( $parent );
+	$self->{'sizer'} = Wx::Perl::Smart::Sizer->new($self, $widgets, $orientation, $init_arg);
+	$self->SetSizerAndFit( $self->{'sizer'} );
+	$self;
+}
+
+1;

lib/Wx/Perl/Smart/Sizer.pm

+use v5.12;
+use warnings;
+use Wx;
+use Wx::Perl::Smart::LabeledBox;
+use Wx::Perl::Smart::TabbedBox;
+use Wx::Perl::RadioGroup;
+use Wx::Perl::TextSlider;
+
+package Wx::Perl::Smart::Sizer;
+use base qw(Wx::BoxSizer);
+use Scalar::Util qw(blessed looks_like_number);
+
+sub new {
+	my ($class, $parent, $widgets, $orientation, $init_arg) = @_;
+	# complain about bad arguments
+	die __PACKAGE__.'::new needs a parent widget as first parameter'
+		if defined $parent and not (blessed($parent) and $parent->isa('Wx::Window') );;
+	die __PACKAGE__.'::new needs a widget array as second parameter' unless ref $widgets eq 'ARRAY';
+	die __PACKAGE__.'::new needs a Wx orientation constant as third parameter'
+		if defined $orientation and $orientation != &Wx::wxVERTICAL and $orientation != &Wx::wxHORIZONTAL;
+	die __PACKAGE__.'::new needs a hash ref as forth parameter' if defined $init_arg and ref $init_arg ne 'HASH';
+
+	my %default_arg = (
+		flags        => &Wx::wxLEFT|&Wx::wxRIGHT|&Wx::wxGROW,
+		border       => 0,
+		add_callback => sub {},
+	);
+	# sanitize arguments
+	$orientation     =  &Wx::wxHORIZONTAL unless defined $orientation;
+	my (%arg, @arg_stack);
+	for (keys %default_arg) { $arg{$_} = defined $init_arg->{$_} ? $init_arg->{$_} : $default_arg{$_} }
+	$arg{'flags'} = ${$arg{'flags'}} | $default_arg{'flags'} if ref $arg{'flags'} eq 'SCALAR';
+	$arg{'add_callback'} = $default_arg{'add_callback'} unless ref $arg{'add_callback'} eq 'CODE';
+
+	my ($self) = $class->SUPER::new( $orientation );
+	for (my $pos = 0; $pos < scalar @$widgets; $pos++) {
+		my $widget = $widgets->[$pos];
+		my ($proportion) = (0);
+		$proportion++, $widget = $$widget while ref $widget eq 'REF' or ref $widget eq 'SCALAR';
+
+		if (ref $widget eq 'HASH'){
+			my %old_arg = %arg;
+			push @arg_stack, \%old_arg;
+			for (keys %default_arg) {$arg{$_} = $widget->{$_} if defined $widget->{$_}}
+			next;
+		}
+		elsif (ref $widget eq 'ARRAY'){
+			my $sizer = Wx::Perl::Smart::Sizer->new(
+				$parent, $widget, toggle_orientation($orientation),
+				{add_callback => $arg{'add_callback'}}
+			);
+			return $sizer if scalar @$widgets == 1;
+			$widget = $sizer;
+		}
+		elsif (not ref $widget) {
+			if (substr($widget, 0, 1) eq '-' ) {
+				($widget, my $name) = split(':', substr($widget, 1));
+				if ($widget eq 'LabeledBox') {
+					next unless exists $widgets->[$pos+1];
+					$widget = $widgets->[$pos+1];
+					next unless ref $widget eq 'ARRAY' and scalar @$widget > 0 and not ref $widget->[0];
+					$pos++;
+					my $label = shift @$widget;
+					$widget = Wx::Perl::Smart::LabeledBox->new($parent, $label, $widget);
+				}
+				elsif ($widget eq 'TabbedBox' ) {
+					next unless exists $widgets->[$pos+1];
+					$widget = $widgets->[$pos+1];
+					next unless ref $widget eq 'ARRAY' and scalar @$widget > 0 and not ref $widget->[0];
+					$pos++;
+					$widget = Wx::Perl::Smart::TabbedBox->new(
+						$parent, $widget, toggle_orientation($orientation),
+						{add_callback => $arg{'add_callback'}}
+					);
+				}
+
+			}
+			elsif (looks_like_number($widget)) {
+				$proportion 
+					? $self->AddStretchSpacer( $proportion )
+					: $self->AddSpacer( $widget );
+				next;
+			}
+			elsif (substr($widget, 0, 1) eq '-' or substr($widget, 0, 1) eq '|') {
+				$widget = $orientation == &Wx::wxVERTICAL
+					? Wx::StaticLine->new($parent,-1,[-1,-1],[-1,2])
+					: Wx::StaticLine->new($parent,-1,[-1,-1],[2,-1]);
+			}
+			else {
+				$widget = substr($widget,1) if substr($widget,0,1) eq '-';
+				$widget = Wx::StaticText->new($parent, -1, $widget);
+			}
+		}
+		next unless blessed($widget) and ($widget->isa('Wx::Window') or $widget->isa('Wx::Sizer'));
+		$arg{'add_callback'}->($widget);
+		$self->Add( $widget, $proportion, $arg{'flags'}, $arg{'border'});
+		$widget->Reparent($parent) if defined $parent and $widget->isa('Wx::Window');
+	}
+	return $self;
+}
+
+sub toggle_orientation {
+	die __PACKAGE__.'::toggle_orientation needs wxVERTICAL or wxHORIZONTAL as input'
+		unless $_[0] == &Wx::wxVERTICAL or $_[0] == &Wx::wxHORIZONTAL;
+	return $_[0] == &Wx::wxHORIZONTAL ? &Wx::wxVERTICAL : &Wx::wxHORIZONTAL;
+}
+
+1;
+
+__DATA__
+
+Syntax Def:
+
+'text'		Label				creates a static text widget
+'---'		Line				creates a static line widget
+								(horizontal or vertical according to sizer orient)
+								(use as many '-' or '|' as you prefer)
+number		n px space			inserts a spacer with that number amount of pixel
+\...		proportion++		all space left gets divided between the hungry according their proportion number
+								(default is 0)
+\n			stretching space	creates a hungry spacer (more backslashes => more proportion)
+[,,]		BoxSizer			member widgets get stacked vertically, in subarray horizontally, and so on
+%box =>[]	StaticBoxSizer		arrange widgets within a labeled box
+%grid=>[]	GridBoxSizer		arrange widgets within a table
+{k=>v,}		set attributes		overwrite selected attributs, become effective with next element

lib/Wx/Perl/Smart/TabbedBox.pm

+use v5.12;
+use warnings;
+use Wx;
+use Wx::Perl::Smart::Panel;
+
+package Wx::Perl::Smart::TabbedBox;
+use base qw(Wx::Notebook);
+use Scalar::Util qw(blessed);
+
+sub new {
+	my ($class, $parent, $widgets, $orientation, $init_arg) = @_;
+	die __PACKAGE__.'::new a widget as first arg'
+		if  defined $parent and not (blessed($parent) and $parent->isa('Wx::Window'));
+	die __PACKAGE__.'::new need a widget list (ARRAY ref) as second arg' unless ref $widgets eq 'ARRAY';
+
+	my ($self) = $class->SUPER::new( $parent, -1, [-1, -1],[-1, -1], &Wx::wxNB_TOP );
+	while (scalar @$widgets > 1){
+		my $label         = shift $widgets;
+		my $panel_widgets = shift $widgets;
+		$self->AddPage( Wx::Perl::Smart::Panel->new($self, $panel_widgets, $orientation, $init_arg), $label, 0);
+	}
+	#$self->ChangeSelection(0);
+	$self;
+}
+
+1;

lib/Wx/Perl/TextSlider.pm

+use v5.12;
+use warnings;
+use Wx;
+
+package Wx::Perl::TextSlider;
+use base qw(Wx::Panel);
+
+sub new {
+	my ($class, $parent, $label, $default, $min, $max, $callback) = @_;
+	$default = $min if $default < $min;
+	$default = $max if $default > $max;
+    
+	my ($self) = $class->SUPER::new( $parent );
+	$self->{'min'} = $min;
+	$self->{'max'} = $max;
+	$self->{'default'} = $default;
+	$callback = sub {} unless ref $callback eq 'CODE'; 
+	my $frontlabel = Wx::StaticText->new($self, -1, "$label : ");
+	my $slider = $self->{'slider'}= Wx::Slider->new($self,-1,$default,$min,$max);
+	$slider->SetMinSize([300,-1]);
+	my $text =   $self->{'text'}  = Wx::TextCtrl->new(
+			$self, -1, $default, [-1,-1], [40, -1], &Wx::wxTE_PROCESS_ENTER| &Wx::wxTE_RIGHT
+	);
+
+	my $sizer = Wx::BoxSizer->new( &Wx::wxHORIZONTAL);
+	my $grow_center_all = &Wx::wxGROW | &Wx::wxALL| &Wx::wxALIGN_CENTER_VERTICAL;
+	$sizer->Add($frontlabel, 0, $grow_center_all, 10 );
+	$sizer->Add($slider,     1, $grow_center_all,  5 );
+	$sizer->Add($text,       0, $grow_center_all,  5 );
+
+	Wx::Event::EVT_SCROLL( $slider, sub { # text always shows current value of slider
+		$text->SetValue( $slider->GetValue) if $text->GetValue != $slider->GetValue;
+	} );
+	Wx::Event::EVT_SCROLL_THUMBRELEASE( $slider, sub {
+		#return if $slider->GetValue == $text->GetValue;
+		$text->SetValue( $slider->GetValue );
+		$callback->(); 
+	} );
+	Wx::Event::EVT_TEXT_ENTER($text, -1, sub {
+		return if $slider->GetValue == $text->GetValue;
+		$slider->SetValue( $text->GetValue );
+		$callback->(); 
+	});
+	Wx::Event::EVT_KILL_FOCUS($text, sub {
+		$_[1]->Skip;
+		if ($slider->GetValue != $text->GetValue){
+			$slider->SetValue( $text->GetValue );
+			$callback->();
+		}
+	});
+	Wx::Event::EVT_MIDDLE_DOWN( $slider, sub { $self->ResetValue; $callback->() } );
+	Wx::Event::EVT_LEFT_DCLICK( $slider, sub { $self->ResetValue; $callback->() } );
+    
+    $self->SetSizerAndFit( $sizer );
+    $self->ResetValue();
+    $self;
+}
+
+sub GetValue {
+	my ($self) = shift;
+	$self->{'text'}->GetValue;
+}
+sub SetValue{
+	my ($self, $value) = @_;
+	$value = 0 unless defined $value;
+	$value = 0 + $value;
+	return if $value < $self->{'min'};
+	return if $value > $self->{'max'};
+	$self->{'text'}->SetValue( $value );
+	$self->{'slider'}->SetValue( $value );
+}
+sub ResetValue {
+	my ($self) = shift;
+	$self->SetValue( $self->{'default'} );
+}
+
+1;
+---
+gelbrad:
+  amplitude_x: 270
+  amplitude_y: 270
+  density: 451
+  flow_colour: 2
+  frequency_x: 4
+  frequency_y: 5
+  friction: 3
+  length: 31
+  rotation: 1
+  rotation_dir: Links
+  scale_colour: 1
+  start_colour: 0
+  thickness: 1
+  y_invers: ''
+  zoom: '-2'
+schwingreihe:
+  amplitude_x: 290
+  amplitude_y: 290
+  density: 589
+  flow_colour: 22
+  frequency_x: 7
+  frequency_y: 9
+  friction: 14
+  length: 12
+  rotation: 1
+  rotation_dir: keine
+  scale_colour: 1
+  start_colour: 0
+  thickness: 2
+  y_invers: ''
+  zoom: 0
+sonne:
+  amplitude_x: 270
+  amplitude_y: 270
+  density: 90
+  flow_colour: 1
+  frequency_x: 5
+  frequency_y: 4
+  friction: 5
+  length: 46
+  rotation: 1
+  rotation_dir: Links
+  scale_colour: 1
+  start_colour: 0
+  thickness: 1
+  y_invers: ''
+  zoom: '-1'
+sonnenrad:
+  amp_x: 265
+  amp_y: 265
+  dense: 9150
+  freq_x: 1
+  freq_y: 1
+  friction: 0
+  length: 27
+  rota: 1
+  rota_dir: links
+  scale_c: 1
+  speed_c: 3
+  y_invers: ''
+  zoom: '-2'
+sr:
+  amplitude_x: 290
+  amplitude_y: 290
+  density: 90
+  flow_colour: 0
+  frequency_x: 1
+  frequency_y: 1
+  friction: 2
+  length: 12
+  rotation: 1
+  rotation_dir: Links
+  scale_colour: 1
+  start_colour: 0
+  thickness: 1
+  y_invers: ''
+  zoom: '-2'

lib/harmonograph.pl

+#!/usr/bin/perl
+use v5.12;
+use warnings;
+use Wx;
+use Wx::Perl::Smart::Frame;
+use Colour::Flow;
+use Math::Trig;
+use File::Spec;
+use YAML::Tiny;
+
+package Harmonograph;
+our $VERSION = '0.88';
+use base qw(Wx::App);
+
+my $maltafelgroesse = 600;
+my (%l18n, %range_defaults, $midoffset, $max_amp, );
+my $fav_file = 'favs.yml'; 
+my $l18n_file = 'localisation.yml';
+my $language = 'german';
+
+sub OnInit {
+	my $app = shift;
+	die "$l18n_file is missing " unless -e $l18n_file;
+	%l18n = %{YAML::Tiny->read( $l18n_file )->[0]{$language}};
+
+	my $frame = $app->{'frame'} = Wx::Perl::Smart::Frame->new(
+		undef, -1, __PACKAGE__." $VERSION", [1,1],[700,-1],
+		&Wx::wxCAPTION | &Wx::wxCLOSE_BOX | &Wx::wxFRAME_TOOL_WINDOW | &Wx::wxSYSTEM_MENU #| &Wx::wxRESIZE_BORDER
+	);
+	Wx::InitAllImageHandlers();
+	$frame->SetIcon( Wx::GetWxPerlIcon() );
+
+	$midoffset = $maltafelgroesse / 2;
+	$max_amp = $midoffset - 10;   # max amplitude
+	%range_defaults = ( # label, min, max, init
+		frequency_x => ['X',               1, 1,  27], frequency_y => ['Y',              1,  1,   27],
+		amplitude_x => ['X', $max_amp, 0, $max_amp*2], amplitude_y => ['Y',       $max_amp,  0,$max_amp*2],
+		rotation    => [$l18n{'amount'},   1, 0,  20], friction    => [$l18n{'friction'},0,  0,   50],
+		length      => [$l18n{'length'},  12, 1, 100], density     => [$l18n{'density'},90,  1, 1000],
+		thickness   => [$l18n{'thickness'},1, 1,  12], zoom        => [$l18n{'zoom'},    0,-10,   10], 
+		start_colour=> [$l18n{'start'},    0, 0,1500], flow_colour => [$l18n{'flow'},    0,  0,   30],
+		scale_colour=> [$l18n{'scale'},    1, 1,   4],
+	);
+
+	$app->{'maltafel'} = Wx::StaticBitmap->new($frame, -1, &Wx::wxNullBitmap);
+	$app->{'maltafel'}->SetMinSize([$maltafelgroesse, $maltafelgroesse]);
+	$app->{'bmp'}   = Wx::Bitmap->new( $maltafelgroesse, $maltafelgroesse);
+	$app->{'dc'}  = Wx::MemoryDC->new();
+	$app->{'dc'}->SelectObject( $app->{'bmp'} );
+
+	my (%button);
+	my $callback = sub { $app->Repaint() };
+	$app->{$_} = Wx::Perl::TextSlider->new
+		($frame, @{$range_defaults{$_}}, $callback) for keys %range_defaults;
+	$app->{'y_invers'} = Wx::CheckBox->new($frame, -1, $l18n{'y_inverse'});
+	Wx::Event::EVT_CHECKBOX( $app->{'y_invers'}, -1, $callback );
+	$app->{'rotation_dir'} = Wx::Perl::RadioGroup->new(
+		$frame, $l18n{'no'}, [$l18n{'no'}, $l18n{'left'}, $l18n{'right'}],
+		&Wx::wxHORIZONTAL, $callback
+	);
+	$button{$_} = Wx::Button->new($frame, -1, $l18n{$_}) for qw(save all remember forget);
+	Wx::Event::EVT_BUTTON( $button{'save'},    $button{'save'},    sub {$app->Save()    } );
+	Wx::Event::EVT_BUTTON( $button{'all'},     $button{'all'},     sub {$app->SaveAll() } );
+	Wx::Event::EVT_BUTTON( $button{'remember'},$button{'remember'},sub {$app->Remember()} );
+	Wx::Event::EVT_BUTTON( $button{'forget'},  $button{'forget'},  sub {$app->Forget()  } );
+
+	my ($fsaved, @saved, ) = ( '', ());
+	$app->{'favorites'} = -e $fav_file ? YAML::Tiny->read( $fav_file ) : YAML::Tiny->new;
+	if (ref $app->{'favorites'}->[0] eq 'HASH'){
+		@saved = keys $app->{'favorites'}->[0];
+		$fsaved = $saved[0];
+	}
+	$app->{'fav_select'} = Wx::ComboBox->new($frame, -1, $fsaved,[-1,-1],[-1,-1], \@saved);
+	Wx::Event::EVT_COMBOBOX($app->{'fav_select'}, -1, sub {
+		$app->SetValues( $app->{'favorites'}[0]{ $app->{'fav_select'}->GetValue() } );
+	});
+
+	$frame->SetSmartLayout(
+		[   # left part
+			$app->{'maltafel'},
+			10,
+			{border => 10, flags => &Wx::wxALL|&Wx::wxGROW},
+			$app->{'fav_select'},
+			[
+				$button{'save'},
+				{border => 20, flags => &Wx::wxLEFT},
+				$button{'all'},
+				\1,
+				{border => 20, flags => &Wx::wxRIGHT},
+				$button{'forget'}, 
+				{border => 0},
+				$button{'remember'}
+			],
+		],[ # right half
+			-TabbedBox =>  [ 
+				$l18n{'oscillators'} => [[
+					{border => 5, flags => &Wx::wxGROW|&Wx::wxALL},
+					-LabeledBox => [ $l18n{'frequency'} => [$app->{'frequency_x'}, $app->{'frequency_y'}, $app->{'y_invers'}]],
+					-LabeledBox => [ $l18n{'start_amp'} => [$app->{'amplitude_x'}, $app->{'amplitude_y'}  ]],
+					-LabeledBox => [ $l18n{'rotation'}  => [$app->{'rotation_dir'},$app->{'rotation'}     ]],
+					{border => 10},
+					$app->{'friction'}, 
+				]],
+				$l18n{'visuals'} => [[
+					{border => 5, flags => &Wx::wxGROW|&Wx::wxALL},
+					-LabeledBox => [ $l18n{'line'}  => [$app->{'length'},      $app->{'density'},    $app->{'thickness'}   ]],
+					-LabeledBox => [ $l18n{'color'} => [$app->{'start_colour'},$app->{'flow_colour'},$app->{'scale_colour'}]],
+					{border => 10},
+					$app->{'zoom'},
+				]]
+			],
+		],
+	);
+	$app->ResetValues('all');
+	$app->Repaint();
+	$frame->Center();
+	$frame->Show(1);
+	$app->SetTopWindow($frame);
+	1;
+}
+sub OnExit {
+	my $app = shift;
+	#$app->{'favorites'}->write( $fav_file );
+	1;
+}
+
+
+sub SubscribeWidget {
+	my $app = shift;
+}
+
+sub GetValues {
+	my $app = shift;
+	my %value;
+	$value{$_} = $app->{$_}->GetValue() for (keys %range_defaults, qw(rotation_dir y_invers));
+	return \%value;
+}
+sub SetValues {
+	my $app = shift;
+	my %value = %{shift;};
+	$app->{$_}->SetValue( $value{$_} ) for (keys %range_defaults, qw(rotation_dir y_invers));
+	$app->Repaint();
+}
+sub ResetValues { shift->ResetValue }     # for naming consistency
+sub ResetValue {
+	my $app = shift;
+	my $which = shift // 'all';
+	if ($which eq 'all'){
+		$app->{'y_invers'}->SetValue(0);
+		$app->ResetValue($_) for (keys %range_defaults, 'rotation_dir');
+	} 
+	else { $app->{$which}->ResetValue }
+	$app;
+}
+
+
+
+sub Remember {
+	my $app = shift;
+	my $name = Wx::GetTextFromUser(
+		$l18n{'ask_for_a_name'}, __PACKAGE__." $VERSION", '', $app->{'frame'}
+	);
+	$name = Wx::GetTextFromUser(
+		$l18n{'ask_another_name'}, __PACKAGE__." $VERSION", '', $app->{'frame'}
+	) while exists $app->{'favorites'}[0]{$name} or not $name;
+	$app->{'favorites'}[0]{$name} = $app->GetValues;
+	$app->{'fav_select'}->SetValue($name);
+	$app->{'fav_select'}->Insert($name, 0);
+	$app->{'favorites'}->write( $fav_file );
+}
+sub Forget {
+	my $app = shift;
+	my $cb = $app->{'fav_select'};
+	return if $cb->IsEmpty;
+	my $name = $cb->GetValue();
+	$cb->Delete( $cb->FindString($name) );
+	delete $app->{'favorites'}[0]{$name};
+	if ($cb->IsEmpty){
+		$cb->SetValue('');
+		$app->ResetValues();
+		$app->{'favorites'} =  YAML::Tiny->new;
+	} else {
+		$cb->SetSelection(0);
+		$app->SetValues( $app->{'favorites'}[0]{ $cb->GetValue() } );
+		delete $app->{'favorites'}[0]{$name};
+	}
+	$app->Repaint();
+}
+
+
+
+sub Save {
+	my $app = shift;
+	my $file = Wx::FileSelector(
+		$l18n{'save_under'}, '.', $app->{'fav_select'}->GetValue().'.jpg',
+		'', '(*)|*', &Wx::wxFD_SAVE, $app->{'frame'}
+	);
+	return unless $file;
+	my $img = Wx::Image->new( $app->{'maltafel'}->GetBitmap() );  # in later versions use: ->ConvertToImage() 
+	$img->SaveFile($file, &Wx::wxBITMAP_TYPE_JPEG ); #wxBITMAP_TYPE_BMP
+}
+sub SaveAll {
+	my $app = shift;
+	my $dir = Wx::DirSelector( $l18n{'save_all_under'}, '.', 0, [-1,-1], $app->{'frame'});
+	return unless -d $dir;
+	my $values = $app->GetValues();
+	for (keys $app->{'favorites'}[0]) {
+		$app->SetValues( $app->{'favorites'}[0]{$_} );
+		my $file = File::Spec->catfile($dir, $_.'.jpg' );
+		my $img = Wx::Image->new( $app->{'maltafel'}->GetBitmap() );  # in later versions use: ->ConvertToImage() 
+		$img->SaveFile($file, &Wx::wxBITMAP_TYPE_JPEG ); #wxBITMAP_TYPE_BMP
+	}
+	$app->SetValues($values);
+	$app->Repaint();
+}
+
+
+
+sub Repaint {
+	my $app = shift;
+	my $value = $app->GetValues;
+	#my $pi = 3.141592654; # deg2rad($degrees);
+	my $line_density =   10000 / (1.02**$value->{'density'});
+	my $dx = $line_density / $value->{'frequency_x'};
+	my $dy = $line_density / $value->{'frequency_y'};
+	$dy = $value->{'y_invers'} ? - $dy : $dy;
+	my $x = Math::Trig::asin( ($value->{'amplitude_x'} - $max_amp)/$max_amp );
+	my $y = Math::Trig::asin( ($value->{'amplitude_y'} - $max_amp)/$max_amp );
+	my $drot = $value->{'rotation'} / 2000;
+	my $rota_dir = $value->{'rotation_dir'};
+	$drot = - $drot if $rota_dir eq $l18n{'right'};
+	my $friction = 1 - 0.000001 * $value->{'friction'};
+	$value->{'zoom'} = 1.2**$value->{'zoom'};
+	my $rot = 0;
+	my $cur_amp = $max_amp;
+	my $colour_flow = Colour::Flow->new(
+		$value->{'scale_colour'} ,
+		($value->{'flow_colour'} ? 1000 / $value->{'flow_colour'} : 0), # change rate
+		$value->{'start_colour'} ,
+	);
+	$app->{'dc'}->Clear();
+	my $colour = Wx::Colour->new($colour_flow->get_rgb(), 0);
+	my $pen = Wx::Pen->new($colour, $value->{'thickness'}, &Wx::wxSOLID);
+	$app->{'dc'}->SetPen( $pen );
+
+	for my $cc (0 .. $value->{'length'} *4000) {
+		my $xs = sin($x += $dx);
+		my $ys = sin($y += $dy);
+		$cur_amp *= $friction;
+		$rot += $drot;
+		($xs, $ys) = rotate($xs, $ys, $rot) unless $rota_dir eq $l18n{'no'};
+		if ($colour_flow->next_step()){
+			$colour->Set( $colour_flow->get_rgb(), 0 );
+			$pen->SetColour( $colour );
+			$app->{'dc'}->SetPen( $pen );
+		}
+		$app->{'dc'}->DrawPoint( ($xs*$cur_amp*$value->{'zoom'})+$midoffset,
+		                         ($ys*$cur_amp*$value->{'zoom'})+$midoffset );
+	}
+	$app->{'maltafel'}->SetBitmap( $app->{'bmp'} );
+	$app->{'dc'}->SelectObject( $app->{'bmp'} );
+	$app->{'maltafel'}->Refresh();
+}
+sub rotate {
+	my ($x, $y, $r) = @_;
+	my ($sinr, $cosr) = (sin($r), cos($r));
+	return ($cosr*$x - $sinr*$y, $sinr*$x + $cosr*$y);
+}
+
+package main;
+Harmonograph->new->MainLoop;

lib/localisation.yml

+---
+english:
+  all: All
+  amount: Amount
+  ask_another_name: 'Please select another name or delete the other memory first.'
+  ask_for_a_name: 'Remember under which name ?'
+  color: Color
+  density: Density
+  line: Line
+  flow: Flow
+  forget: Forget
+  frequency: Frequency
+  friction: Friction
+  left: Left
+  length: Length
+  no: 'No'
+  oscillators: Oscillators
+  remember: Remember
+  right: Right
+  rotation: Rotation
+  save: 'Save'
+  save_all_under: 'Save All Remebered as JPG Files in Directory ...'
+  save_under: 'Save Picture as JPG File under ...'
+  scale: Scale
+  start: Start
+  start_amp: 'Starting Amplitude'
+  thickness: Thickness
+  visuals: Visuals
+  y_inverse: 'Y - Inverse Direction'
+  zoom: Zoom
+german:
+  all: Alle
+  amount: Betrag
+  ask_another_name: 'Bitte anderen Namen waehlen, oder Vorhandenes vorher loeschen.'
+  ask_for_a_name: 'Unter welchem Namen merken ?'
+  color: Farbe
+  density: Dichte
+  line: Linie
+  flow: Verlauf
+  forget: Vergiss
+  frequency: Frequenz
+  friction: Reibung
+  left: Links
+  length: Laenge
+  thickness: Dicke
+  no: Keine
+  oscillators: Oszillatoren
+  remember: Merke
+  right: Rechts
+  rotation: Rotation
+  save: 'Speichern'
+  save_all_under: 'Speicher alle Gemerkten als JPG im Verzeichnis ...'
+  save_under: 'Speichere Bild als JPG Datei unter ...'
+  scale: Skala
+  start: Start
+  start_amp: Startamplitude
+  visuals: Optik
+  y_inverse: 'Y - Richtung invers'
+  zoom: Zoom