Source

App::Harmonograph / lib / harmonograph.pl

Full commit
#!/usr/bin/perl

use v5.12;
use warnings;
use Wx;
use Wx::Perl::DrawMap;
use Wx::Perl::DisplaySlider;
use Wx::Perl::RadioGroup;
use Wx::Perl::Smart::Frame;
use Colour::Flow;
use Math::Trig;
use File::Spec;
use YAML::Tiny;

package Harmonograph;
our $VERSION = '0.94';
use base qw(Wx::App);

my $fav_file = 'favs.yaml'; 
my $l18n_file = 'localisation.yaml';
my $language = 'english';

sub OnInit {
	my $app = shift;

	# load localisation texts in chosen language
	my ($v, $dir, $f) = File::Spec->splitpath(__FILE__);
	$l18n_file = File::Spec->catfile($dir, $l18n_file) if $dir;
	die "localisation file $l18n_file is missing!" unless -e $l18n_file;
	my %l18n = %{YAML::Tiny->read( $l18n_file )->[0]{$language}};
	$app->{'l18n'} = \%l18n;

	# loading the numbers of the remembered favorites
	$app->{'favorites'} = -e $fav_file ? YAML::Tiny->read( $fav_file ) : YAML::Tiny->new;
	my @remembered_pic_names = ref $app->{'favorites'}->[0] eq 'HASH' ? keys $app->{'favorites'}->[0] : ();

	# making UI
	my $frame = $app->{'frame'} = Wx::Perl::Smart::Frame->new(__PACKAGE__." $VERSION", 'fixed_size');
	$frame->SubscribeStrings( $app->{'l18n'} );
	my $remember= sub { $frame->SetValues( $app->{'favorites'}[0]{ $_[0]->GetValue() } ); $app->Repaint()};
	my $repaint = sub { $app->Repaint() };
	my $boardsize = Wx::GetDisplaySize()->GetHeight() - 250;
	my $origin_offset = $app->{'origin_offset'} = $boardsize / 2;
	my $max_amp = $app->{'max_amp'} = $origin_offset - 10;        # max amplitude
	my %range_defaults = (# label, min, max, init
		frequency_x => ['X',               1, 1,  30], frequency_y => ['Y',               1,  1,   30],
		amplitude_x => ['X',               0, 0, 360], amplitude_y => ['Y',               0,  0,  360],
		rotation    => [$l18n{'amount'},   1, 0,  30], friction    => [$l18n{'friction'}, 0,  0,  200],
		length      => [$l18n{'length'},  12, 1, 100], density     => [$l18n{'density'},100,  1,  100],
		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,   60],
		scale_colour=> [$l18n{'scale'},    1, 1,   4],
	);
	$frame->SubscribeWidgets
		({$_         => Wx::Perl::DisplaySlider->new($frame, @{$range_defaults{$_}}, $repaint)}) for keys %range_defaults;
	$frame->SubscribeWidgets({
		drawboard    => Wx::Perl::DrawMap->new($frame, $boardsize),
		fav_select   => [-ComboBox => \@remembered_pic_names, 0, -1, $remember],
		format_select=> [-ComboBox => [qw(PNG JPG TIFF BMP XPM)], 0, 70       ],
		save         => [-Button   => '~save',     sub {$app->Save()    }],
		save_all     => [-Button   => '~all',      sub {$app->SaveAll() }],
		remember     => [-Button   => '~remember', sub {$app->Remember()}],
		forget       => [-Button   => '~forget',   sub {$app->Forget()  }],
		no_phase     => [-Button   => '~no',       sub {$frame->SetValues(amplitude_x =>  0, amplitude_y =>   0);$app->Repaint()}],
		closed_phase => [-Button   => '~closed',   sub {$frame->SetValues(amplitude_x => 90, amplitude_y =>  90);$app->Repaint()}],
		open_phase   => [-Button   => '~open',     sub {$frame->SetValues(amplitude_x => 90, amplitude_y => 180);$app->Repaint()}],
		y_invers     => [-CheckBox => '~y_inverse', 0,  $repaint],
		rotation_dir => Wx::Perl::RadioGroup->new($frame, [@l18n{qw(no left right)}],       0, &Wx::wxHORIZONTAL, $repaint),
		app_mode     => Wx::Perl::RadioGroup->new($frame, [@l18n{qw(lateral rotary free)}], 0, &Wx::wxHORIZONTAL, sub{}),
	});

	$frame->SetSmartLayout(
	{flags => &Wx::wxGROW|&Wx::wxALL},
	[	# left part
		'<drawboard>',
		10,
		{border => 10, flags => &Wx::wxALL|&Wx::wxGROW},
		'<fav_select>',
		['<format_select>', 10,'<save>', 10, '<save_all>', \1, '<forget>', 10, '<remember>'],
	],[ # right half
		-TabbedBox => [ 
			'~oscillators' =>[
				{border => 5, flags => &Wx::wxGROW|&Wx::wxALL},
				['~mode :', '<app_mode>'],
				-LabeledBox =>['~frequency' =>[qw( <frequency_x> <frequency_y>)]], #<y_invers>
				-LabeledBox =>['~start_amp' =>[qw( <amplitude_x> <amplitude_y>),
					{border => 5, flags => &Wx::wxALL|&Wx::wxGROW},
					['~phase :', \1, '<no_phase>', \1,'<closed_phase>', \1, '<open_phase>'],
				]],
				-LabeledBox =>[ '~rotation ' =>[qw( <rotation_dir>  <rotation> )]],
				{border => 10}, '<friction>', 
			],
			'~visuals' => [
				{border => 5, flags => &Wx::wxGROW|&Wx::wxALL},
				-LabeledBox =>['~line'  =>[qw( <length>       <density>                   )]], #<thickness>
				-LabeledBox =>['~color' =>[qw( <start_colour> <flow_colour> <scale_colour>)]],
				{border => 10},'<zoom>',
			],
		],
	]);
	$frame->ResetValues();
	$app->Repaint();
	$app->SetTopWindow($frame);
	1;
}



sub Remember {
	my $app = shift;
	my $frame = $app->{'frame'};
	my $cb = $frame->GetSubscribedWidget('fav_select'); # combo box
	my $name = Wx::GetTextFromUser
		($app->{'l18n'}{'ask_for_a_name'}, __PACKAGE__." $VERSION", '', $frame);
	$name = Wx::GetTextFromUser
		($app->{'l18n'}{'ask_another_name'}, __PACKAGE__." $VERSION", '', $frame)
			while exists $app->{'favorites'}[0]{$name} or not $name;
	my $values = $frame->GetValues;
	delete $values->{'fav_select'};          # prevent bad recursion, restoring state that was when saved this one
	$app->{'favorites'}[0]{$name} = $values;
	$cb->SetValue($name);
	$cb->Insert($name, 0);
	$app->{'favorites'}->write( $fav_file );
}
sub Forget {
	my $app = shift;
	my $frame = $app->{'frame'};
	my $cb = $frame->GetSubscribedWidget('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('');
		$frame->ResetValues();
		$app->{'favorites'} =  YAML::Tiny->new;
	} else {
		$cb->SetSelection(0);
		$frame->SetValues( $app->{'favorites'}[0]{ $cb->GetValue() } );
		delete $app->{'favorites'}[0]{$name};
	}
	$app->Repaint();
	$app->{'favorites'}->write( $fav_file );
}



sub Save {
	my $app = shift;
	my $format = lc $app->{'frame'}->GetSubscribedWidget('format_select')->GetValue();
	my $file = Wx::FileSelector(
		$app->{'l18n'}{'save_under'}.' '.uc($format),            '.', 
		$app->{'frame'}->GetSubscribedWidget('fav_select')->GetValue().".$format",
		'', '(*)|*', &Wx::wxFD_SAVE, $app->{'frame'}
	);
	$app->{'frame'}->GetSubscribedWidget('drawboard')->SaveAsFile($file, $format) if $file;
}
sub SaveAll {
	my $app = shift;
	my $frame = $app->{'frame'};
	my $format = lc $app->{'frame'}->GetSubscribedWidget('format_select')->GetValue();
	my $dir = Wx::DirSelector( $app->{'l18n'}{'save_all_under'}.' '.uc($format), '.', 0, [-1,-1], $frame);
	return unless -d $dir;
	my $values = $frame->GetValues();
	for (keys $app->{'favorites'}[0]) {
		$frame->SetValues( $app->{'favorites'}[0]{$_} );
		$app->Repaint();
		my $file = File::Spec->catfile($dir, "$_.$format" );
		$frame->GetSubscribedWidget('drawboard')->SaveAsFile($file, $format);
	}
	$frame->SetValues($values);
	$app->Repaint();
}



sub Repaint {
	my $app = shift;
	my $board = $app->{'frame'}->GetSubscribedWidget('drawboard');
	my $v = $app->{'frame'}->GetValues; # values

	my $origin_offset   = $app->{'origin_offset'};
	my $max_amp         = $app->{'max_amp'} * (1.2 ** $v->{'zoom'});
	my $win_size_factor = $max_amp / 10;
	my $pi = 3.1415926536; 
	my $xangle = $v->{'amplitude_x'} / 180 * $pi;                               # beginning with start amplitudes
	my $yangle = $v->{'amplitude_y'} / 180 * $pi;
	my $dx = $v->{'frequency_x'};                                               # one step of the oscillator rotation 
	my $dy = $v->{'frequency_y'};
	my $max_freq = $dx > $dy ? $dx : $dy;
	my $step_size = 100 / $v->{'density'} / $max_freq / $win_size_factor;
	$dx *= 0.2 * $step_size;                                                   # one step of the oscillator rotation 
	$dy *= 0.2 * $step_size;
	my $friction_factor =  (1 - (0.00005 * $v->{'friction'})) ** $step_size;
	my $steps = $v->{'length'} * 100 * $v->{'density'};

	$dy = $v->{'y_invers'} ? - $dy : $dy;
	#my $x = Math::Trig::asin( ($v->{'amplitude_x'} - $max_amp)/$max_amp );
	#my $y = Math::Trig::asin( ($v->{'amplitude_y'} - $max_amp)/$max_amp );
	#my $drot = $v->{'rotation'} / 2000;
	#my $rota_dir = $v->{'rotation_dir'};
	#$drot = - $drot if $rota_dir == 2;
	#my $rot = 0;# deg2rad($degrees);
	my $colour_flow = Colour::Flow->new(
		 $v->{'scale_colour'},
		($v->{'flow_colour'} ? 40 / $v->{'flow_colour'} * $v->{'density'} : 0), # change rate
		 $v->{'start_colour'},
	);
	$board->Colour( $colour_flow->get_rgb() );
	$board->NewDrawing();

	for my $cc (0 .. $steps) {
		$xangle += $dx;
		$yangle += $dy;
		$max_amp *= $friction_factor;

		#my $xs = sin($x += $dx);
		#my $ys = sin($y += $dy);
		#$rot += $drot;
		#($xs, $ys) = rotate($xs, $ys, $rot) if $rota_dir;
		$board->Colour( $colour_flow->get_rgb() ) if $colour_flow->next_step(); 

		#$app->{'dc'}->DrawPoint( ($xs*$cur_amp*$value->{'zoom'})+$midoffset,
		#                         ($ys*$cur_amp*$value->{'zoom'})+$midoffset );

		$board->Point(sin($xangle) * $max_amp + $origin_offset, 
					 -sin($yangle) * $max_amp + $origin_offset);
	}
	$board->PublishDrawing();
}
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;