Source

App::Harmonograph / 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.89';
use base qw(Wx::App);

my $maltafelgroesse = 560;
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();
	$app->{'favorites'}->write( $fav_file );
}



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;