144 lines
6 KiB
Python
144 lines
6 KiB
Python
# SPDX-License-Identifier: MPL-2.0
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright 2021 Alexander Rowsell. Licenced under MPLv2.0
|
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
# gtk imports
|
|
import gi
|
|
gi.require_version('Gtk', '3.0')
|
|
from gi.repository import Gtk, Gdk, GObject, GLib
|
|
|
|
# math/plot imports
|
|
from matplotlib.backends.backend_gtk3agg import (
|
|
FigureCanvasGTK3Agg as FigureCanvas)
|
|
from matplotlib.figure import Figure
|
|
import numpy as np
|
|
import math
|
|
import sys
|
|
|
|
class spirographs:
|
|
def __init__(self):
|
|
self.bigRadius = 12
|
|
self.smallRadius = 5
|
|
self.distance = 4
|
|
self.highestTheta = (np.lcm(self.smallRadius, self.bigRadius)/self.bigRadius) * 2 * math.pi
|
|
self.stepSize = self.highestTheta / 4096
|
|
# update initial slider positions
|
|
self.bigRadiusAdjustment = builder.get_object('bigRadiusAdjustment')
|
|
self.smallRadiusAdjustment = builder.get_object('smallRadiusAdjustment')
|
|
self.distanceAdjustment = builder.get_object('distanceAdjustment')
|
|
self.bigRadiusEntry = builder.get_object('bigRadiusEntry')
|
|
self.smallRadiusEntry = builder.get_object('smallRadiusEntry')
|
|
self.distanceEntry = builder.get_object('distanceEntry')
|
|
# set the initial values to a neat shape
|
|
self.bigRadiusAdjustment.set_value(self.bigRadius)
|
|
self.smallRadiusAdjustment.set_value(self.smallRadius)
|
|
self.distanceAdjustment.set_value(self.distance)
|
|
self.bigRadiusEntry.set_text(str(self.bigRadius))
|
|
self.smallRadiusEntry.set_text(str(self.smallRadius))
|
|
self.distanceEntry.set_text(str(self.distance))
|
|
# try and set the step size?!
|
|
self.bigRadiusAdjustment.set_step_increment(0.5)
|
|
self.smallRadiusAdjustment.set_step_increment(0.5)
|
|
self.distanceAdjustment.set_step_increment(0.5)
|
|
# create initial plot
|
|
self.recalcPoints()
|
|
self.showPlot()
|
|
|
|
def calcEpiX(self, theta):
|
|
return ((self.bigRadius + self.smallRadius) * math.cos(theta)) - (self.distance * math.cos(((self.bigRadius + self.smallRadius)/(self.smallRadius))*theta))
|
|
|
|
def calcEpiY(self, theta):
|
|
return ((self.bigRadius + self.smallRadius) * math.sin(theta)) - (self.distance * math.sin(((self.bigRadius + self.smallRadius)/(self.smallRadius))*theta))
|
|
|
|
def calcHypoX(self, theta):
|
|
return ((self.bigRadius - self.smallRadius) * math.cos(theta)) + (self.distance * math.cos(((self.bigRadius - self.smallRadius)/(self.smallRadius))*theta))
|
|
|
|
def calcHypoY(self, theta):
|
|
return ((self.bigRadius - self.smallRadius) * math.sin(theta)) - (self.distance * math.sin(((self.bigRadius - self.smallRadius)/(self.smallRadius))*theta))
|
|
|
|
def onDestroy(self, widget):
|
|
Gtk.main_quit()
|
|
return
|
|
|
|
def bigRadiusAdjustment_value_changed_cb(self, widget):
|
|
self.bigRadius = widget.get_value()
|
|
self.bigRadiusEntry.set_text(str(self.bigRadius))
|
|
self.recalcPoints()
|
|
self.updatePlot()
|
|
|
|
def bigRadiusEntry_changed_cb(self, widget):
|
|
try:
|
|
self.bigRadius = float(widget.get_text())
|
|
except ValueError:
|
|
print("Not a float for R, ignoring...\n")
|
|
return
|
|
self.bigRadiusAdjustment.set_value(self.bigRadius)
|
|
|
|
def smallRadiusAdjustment_value_changed_cb(self, widget):
|
|
self.smallRadius = widget.get_value()
|
|
self.smallRadiusEntry.set_text(str(self.smallRadius))
|
|
self.recalcPoints()
|
|
self.updatePlot()
|
|
|
|
def smallRadiusEntry_changed_cb(self, widget):
|
|
try:
|
|
self.smallRadius = float(widget.get_text())
|
|
except ValueError:
|
|
print("Couldn't get a float for r, ignoring...\n")
|
|
return
|
|
self.smallRadiusAdjustment.set_value(self.smallRadius)
|
|
|
|
def distanceAdjustment_value_changed_cb(self, widget):
|
|
self.distance = widget.get_value()
|
|
self.distanceEntry.set_text(str(self.distance))
|
|
self.recalcPoints()
|
|
self.updatePlot()
|
|
|
|
def distanceEntry_changed_cb(self, widget):
|
|
try:
|
|
self.distance = float(widget.get_text())
|
|
except ValueError:
|
|
print("Couldn't get a float for d, ignoring...\n")
|
|
return
|
|
self.distanceAdjustment.set_value(self.distance)
|
|
|
|
def recalcPoints(self):
|
|
self.epiX = np.array([self.calcEpiX(i) for i in np.arange(0, self.highestTheta, self.stepSize)])
|
|
self.epiY = np.array([self.calcEpiY(i) for i in np.arange(0, self.highestTheta, self.stepSize)])
|
|
self.hypoX = np.array([self.calcHypoX(i) for i in np.arange(0, self.highestTheta, self.stepSize)])
|
|
self.hypoY = np.array([self.calcHypoY(i) for i in np.arange(0, self.highestTheta, self.stepSize)])
|
|
|
|
def showPlot(self):
|
|
viewport = builder.get_object('plotViewport')
|
|
self.plotFigure = Figure(figsize=(5, 8), dpi=100)
|
|
self.epiPlot, self.hypoPlot = self.plotFigure.subplots(1, 2)
|
|
self.epiPlot.set_title(f"Epitrochoid of {self.bigRadius}, {self.smallRadius}, {self.distance}")
|
|
self.epiPlot.plot(self.epiX, self.epiY)
|
|
self.hypoPlot.set_title(f"Hypotrochoid of {self.bigRadius}, {self.smallRadius}, {self.distance}")
|
|
self.hypoPlot.plot(self.hypoX, self.hypoY)
|
|
|
|
self.canvas = FigureCanvas(self.plotFigure)
|
|
self.canvas.set_size_request(800, 600)
|
|
viewport.add(self.canvas)
|
|
|
|
def updatePlot(self):
|
|
self.epiPlot.clear()
|
|
self.hypoPlot.clear()
|
|
self.epiPlot.set_title(f"Epitrochoid of {self.bigRadius}, {self.smallRadius}, {self.distance}")
|
|
self.hypoPlot.set_title(f"Hypotrochoid of {self.bigRadius}, {self.smallRadius}, {self.distance}")
|
|
self.epiPlot.plot(self.epiX, self.epiY)
|
|
self.hypoPlot.plot(self.hypoX, self.hypoY)
|
|
self.canvas.draw_idle()
|
|
|
|
builder = Gtk.Builder()
|
|
builder.add_from_file("spirographs.glade")
|
|
sp = spirographs()
|
|
builder.connect_signals(sp)
|
|
window = builder.get_object("spWindow")
|
|
window.show_all()
|
|
|
|
Gtk.main()
|