diff --git a/examples/tulip.py b/examples/tulip.py
index f7fc23c..f371d98 100644
--- a/examples/tulip.py
+++ b/examples/tulip.py
@@ -200,6 +200,6 @@ def repeated_shader(base, repeat_cond, range_args,
 
 for obj in canvas.order.items:
     if isinstance(obj, Line):
-        obj.noise((5,5))
+        obj.noise((20,20))
         # obj.repeat(random=True)
 canvas.show(inspect=True)
\ No newline at end of file
diff --git a/minimal/noise.py b/minimal/noise.py
new file mode 100644
index 0000000..f126733
--- /dev/null
+++ b/minimal/noise.py
@@ -0,0 +1,357 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2008, Casey Duncan (casey dot duncan at gmail dot com)
+# see LICENSE.txt for details
+
+"""Perlin noise -- pure python implementation"""
+
+# FIXME: Previous implementations of minimal were dependent on the noise
+# python package (https://pypi.org/project/noise/). However, when wrapping
+# minimal into its own environment with a newer version of python, noise
+# could not be successfully pip installed (See https://github.com/caseman/noise/issues/32).
+# Noise seems to be largely unsupported now, so any usage of noise in drawings
+# would be impossible to do. Therefore, we will take their purely-python
+# implementation of noise in their repo and represent that in here.
+# https://github.com/caseman/noise/blob/master/perlin.py
+# If the package were to be maintained again, it would be recommended to switch
+# back to that since use of C setup ext would make calculation more efficient
+
+__version__ = '$Id: perlin.py 521 2008-12-15 03:03:52Z casey.duncan $'
+
+from math import floor, fmod, sqrt
+from random import randint
+
+# 3D Gradient vectors
+_GRAD3 = ((1,1,0),(-1,1,0),(1,-1,0),(-1,-1,0), 
+	(1,0,1),(-1,0,1),(1,0,-1),(-1,0,-1), 
+	(0,1,1),(0,-1,1),(0,1,-1),(0,-1,-1),
+	(1,1,0),(0,-1,1),(-1,1,0),(0,-1,-1),
+) 
+
+# 4D Gradient vectors
+_GRAD4 = ((0,1,1,1), (0,1,1,-1), (0,1,-1,1), (0,1,-1,-1), 
+	(0,-1,1,1), (0,-1,1,-1), (0,-1,-1,1), (0,-1,-1,-1), 
+	(1,0,1,1), (1,0,1,-1), (1,0,-1,1), (1,0,-1,-1), 
+	(-1,0,1,1), (-1,0,1,-1), (-1,0,-1,1), (-1,0,-1,-1), 
+	(1,1,0,1), (1,1,0,-1), (1,-1,0,1), (1,-1,0,-1), 
+	(-1,1,0,1), (-1,1,0,-1), (-1,-1,0,1), (-1,-1,0,-1), 
+	(1,1,1,0), (1,1,-1,0), (1,-1,1,0), (1,-1,-1,0), 
+	(-1,1,1,0), (-1,1,-1,0), (-1,-1,1,0), (-1,-1,-1,0))
+
+# A lookup table to traverse the simplex around a given point in 4D. 
+# Details can be found where this table is used, in the 4D noise method. 
+_SIMPLEX = (
+	(0,1,2,3),(0,1,3,2),(0,0,0,0),(0,2,3,1),(0,0,0,0),(0,0,0,0),(0,0,0,0),(1,2,3,0), 
+	(0,2,1,3),(0,0,0,0),(0,3,1,2),(0,3,2,1),(0,0,0,0),(0,0,0,0),(0,0,0,0),(1,3,2,0), 
+	(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0), 
+	(1,2,0,3),(0,0,0,0),(1,3,0,2),(0,0,0,0),(0,0,0,0),(0,0,0,0),(2,3,0,1),(2,3,1,0), 
+	(1,0,2,3),(1,0,3,2),(0,0,0,0),(0,0,0,0),(0,0,0,0),(2,0,3,1),(0,0,0,0),(2,1,3,0), 
+	(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0), 
+	(2,0,1,3),(0,0,0,0),(0,0,0,0),(0,0,0,0),(3,0,1,2),(3,0,2,1),(0,0,0,0),(3,1,2,0), 
+	(2,1,0,3),(0,0,0,0),(0,0,0,0),(0,0,0,0),(3,1,0,2),(0,0,0,0),(3,2,0,1),(3,2,1,0))
+
+# Simplex skew constants
+_F2 = 0.5 * (sqrt(3.0) - 1.0)
+_G2 = (3.0 - sqrt(3.0)) / 6.0
+_F3 = 1.0 / 3.0
+_G3 = 1.0 / 6.0
+
+
+class BaseNoise:
+	"""Noise abstract base class"""
+
+	permutation = (151,160,137,91,90,15, 
+		131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, 
+		190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, 
+		88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166, 
+		77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 
+		102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196, 
+		135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123, 
+		5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 
+		223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9, 
+		129,22,39,253,9,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228, 
+		251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 
+		49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254, 
+		138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180)
+
+	period = len(permutation)
+
+	# Double permutation array so we don't need to wrap
+	permutation = permutation * 2
+
+	randint_function = randint
+
+	def __init__(self, period=None, permutation_table=None, randint_function=None):
+		"""Initialize the noise generator. With no arguments, the default
+		period and permutation table are used (256). The default permutation
+		table generates the exact same noise pattern each time.
+		
+		An integer period can be specified, to generate a random permutation
+		table with period elements. The period determines the (integer)
+		interval that the noise repeats, which is useful for creating tiled
+		textures.  period should be a power-of-two, though this is not
+		enforced. Note that the speed of the noise algorithm is indpendent of
+		the period size, though larger periods mean a larger table, which
+		consume more memory.
+		A permutation table consisting of an iterable sequence of whole
+		numbers can be specified directly. This should have a power-of-two
+		length. Typical permutation tables are a sequnce of unique integers in
+		the range [0,period) in random order, though other arrangements could
+		prove useful, they will not be "pure" simplex noise. The largest
+		element in the sequence must be no larger than period-1.
+		period and permutation_table may not be specified together.
+		A substitute for the method random.randint(a, b) can be chosen. The
+		method must take two integer parameters a and b and return an integer N
+		such that a <= N <= b.
+		"""
+		if randint_function is not None:  # do this before calling randomize()
+			if not hasattr(randint_function, '__call__'):
+				raise TypeError(
+					'randint_function has to be a function')
+			self.randint_function = randint_function
+			if period is None:
+				period = self.period  # enforce actually calling randomize()
+		if period is not None and permutation_table is not None:
+			raise ValueError(
+				'Can specify either period or permutation_table, not both')
+		if period is not None:
+			self.randomize(period)
+		elif permutation_table is not None:
+			self.permutation = tuple(permutation_table) * 2
+			self.period = len(permutation_table)
+
+	def randomize(self, period=None):
+		"""Randomize the permutation table used by the noise functions. This
+		makes them generate a different noise pattern for the same inputs.
+		"""
+		if period is not None:
+			self.period = period
+		perm = list(range(self.period))
+		perm_right = self.period - 1
+		for i in list(perm):
+			j = self.randint_function(0, perm_right)
+			perm[i], perm[j] = perm[j], perm[i]
+		self.permutation = tuple(perm) * 2
+
+
+class SimplexNoise(BaseNoise):
+	"""Perlin simplex noise generator
+	Adapted from Stefan Gustavson's Java implementation described here:
+	http://staffwww.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf
+	To summarize:
+	"In 2001, Ken Perlin presented 'simplex noise', a replacement for his classic
+	noise algorithm.  Classic 'Perlin noise' won him an academy award and has
+	become an ubiquitous procedural primitive for computer graphics over the
+	years, but in hindsight it has quite a few limitations.  Ken Perlin himself
+	designed simplex noise specifically to overcome those limitations, and he
+	spent a lot of good thinking on it. Therefore, it is a better idea than his
+	original algorithm. A few of the more prominent advantages are: 
+	* Simplex noise has a lower computational complexity and requires fewer
+	  multiplications. 
+	* Simplex noise scales to higher dimensions (4D, 5D and up) with much less
+	  computational cost, the complexity is O(N) for N dimensions instead of 
+	  the O(2^N) of classic Noise. 
+	* Simplex noise has no noticeable directional artifacts.  Simplex noise has 
+	  a well-defined and continuous gradient everywhere that can be computed 
+	  quite cheaply. 
+	* Simplex noise is easy to implement in hardware."
+	"""
+
+	def noise2(self, x, y):
+		"""2D Perlin simplex noise. 
+		
+		Return a floating point value from -1 to 1 for the given x, y coordinate. 
+		The same value is always returned for a given x, y pair unless the
+		permutation table changes (see randomize above). 
+		"""
+		# Skew input space to determine which simplex (triangle) we are in
+		s = (x + y) * _F2
+		i = floor(x + s)
+		j = floor(y + s)
+		t = (i + j) * _G2
+		x0 = x - (i - t) # "Unskewed" distances from cell origin
+		y0 = y - (j - t)
+
+		if x0 > y0:
+			i1 = 1; j1 = 0 # Lower triangle, XY order: (0,0)->(1,0)->(1,1)
+		else:
+			i1 = 0; j1 = 1 # Upper triangle, YX order: (0,0)->(0,1)->(1,1)
+		
+		x1 = x0 - i1 + _G2 # Offsets for middle corner in (x,y) unskewed coords
+		y1 = y0 - j1 + _G2
+		x2 = x0 + _G2 * 2.0 - 1.0 # Offsets for last corner in (x,y) unskewed coords
+		y2 = y0 + _G2 * 2.0 - 1.0
+
+		# Determine hashed gradient indices of the three simplex corners
+		perm = self.permutation
+		ii = int(i) % self.period
+		jj = int(j) % self.period
+		gi0 = perm[ii + perm[jj]] % 12
+		gi1 = perm[ii + i1 + perm[jj + j1]] % 12
+		gi2 = perm[ii + 1 + perm[jj + 1]] % 12
+
+		# Calculate the contribution from the three corners
+		tt = 0.5 - x0**2 - y0**2
+		if tt > 0:
+			g = _GRAD3[gi0]
+			noise = tt**4 * (g[0] * x0 + g[1] * y0)
+		else:
+			noise = 0.0
+		
+		tt = 0.5 - x1**2 - y1**2
+		if tt > 0:
+			g = _GRAD3[gi1]
+			noise += tt**4 * (g[0] * x1 + g[1] * y1)
+		
+		tt = 0.5 - x2**2 - y2**2
+		if tt > 0:
+			g = _GRAD3[gi2]
+			noise += tt**4 * (g[0] * x2 + g[1] * y2)
+
+		return noise * 70.0 # scale noise to [-1, 1]
+
+	def noise3(self, x, y, z):
+		"""3D Perlin simplex noise. 
+		
+		Return a floating point value from -1 to 1 for the given x, y, z coordinate. 
+		The same value is always returned for a given x, y, z pair unless the
+		permutation table changes (see randomize above).
+		"""
+		# Skew the input space to determine which simplex cell we're in
+		s = (x + y + z) * _F3
+		i = floor(x + s)
+		j = floor(y + s)
+		k = floor(z + s)
+		t = (i + j + k) * _G3
+		x0 = x - (i - t) # "Unskewed" distances from cell origin
+		y0 = y - (j - t)
+		z0 = z - (k - t)
+
+		# For the 3D case, the simplex shape is a slightly irregular tetrahedron. 
+		# Determine which simplex we are in. 
+		if x0 >= y0:
+			if y0 >= z0:
+				i1 = 1; j1 = 0; k1 = 0
+				i2 = 1; j2 = 1; k2 = 0
+			elif x0 >= z0:
+				i1 = 1; j1 = 0; k1 = 0
+				i2 = 1; j2 = 0; k2 = 1
+			else:
+				i1 = 0; j1 = 0; k1 = 1
+				i2 = 1; j2 = 0; k2 = 1
+		else: # x0 < y0
+			if y0 < z0:
+				i1 = 0; j1 = 0; k1 = 1
+				i2 = 0; j2 = 1; k2 = 1
+			elif x0 < z0:
+				i1 = 0; j1 = 1; k1 = 0
+				i2 = 0; j2 = 1; k2 = 1
+			else:
+				i1 = 0; j1 = 1; k1 = 0
+				i2 = 1; j2 = 1; k2 = 0
+		
+		# Offsets for remaining corners
+		x1 = x0 - i1 + _G3
+		y1 = y0 - j1 + _G3
+		z1 = z0 - k1 + _G3
+		x2 = x0 - i2 + 2.0 * _G3
+		y2 = y0 - j2 + 2.0 * _G3
+		z2 = z0 - k2 + 2.0 * _G3
+		x3 = x0 - 1.0 + 3.0 * _G3
+		y3 = y0 - 1.0 + 3.0 * _G3
+		z3 = z0 - 1.0 + 3.0 * _G3
+
+		# Calculate the hashed gradient indices of the four simplex corners
+		perm = self.permutation
+		ii = int(i) % self.period
+		jj = int(j) % self.period
+		kk = int(k) % self.period
+		gi0 = perm[ii + perm[jj + perm[kk]]] % 12
+		gi1 = perm[ii + i1 + perm[jj + j1 + perm[kk + k1]]] % 12
+		gi2 = perm[ii + i2 + perm[jj + j2 + perm[kk + k2]]] % 12
+		gi3 = perm[ii + 1 + perm[jj + 1 + perm[kk + 1]]] % 12
+
+		# Calculate the contribution from the four corners
+		noise = 0.0
+		tt = 0.6 - x0**2 - y0**2 - z0**2
+		if tt > 0:
+			g = _GRAD3[gi0]
+			noise = tt**4 * (g[0] * x0 + g[1] * y0 + g[2] * z0)
+		else:
+			noise = 0.0
+		
+		tt = 0.6 - x1**2 - y1**2 - z1**2
+		if tt > 0:
+			g = _GRAD3[gi1]
+			noise += tt**4 * (g[0] * x1 + g[1] * y1 + g[2] * z1)
+		
+		tt = 0.6 - x2**2 - y2**2 - z2**2
+		if tt > 0:
+			g = _GRAD3[gi2]
+			noise += tt**4 * (g[0] * x2 + g[1] * y2 + g[2] * z2)
+		
+		tt = 0.6 - x3**2 - y3**2 - z3**2
+		if tt > 0:
+			g = _GRAD3[gi3]
+			noise += tt**4 * (g[0] * x3 + g[1] * y3 + g[2] * z3)
+		
+		return noise * 32.0
+
+
+def lerp(t, a, b):
+	return a + t * (b - a)
+
+def grad3(hash, x, y, z):
+	g = _GRAD3[hash % 16]
+	return x*g[0] + y*g[1] + z*g[2]
+
+
+class TileableNoise(BaseNoise):
+	"""Tileable implemention of Perlin "improved" noise. This
+	is based on the reference implementation published here:
+	
+	http://mrl.nyu.edu/~perlin/noise/
+	"""
+
+	def noise3(self, x, y, z, repeat, base=0.0):
+		"""Tileable 3D noise.
+		
+		repeat specifies the integer interval in each dimension 
+		when the noise pattern repeats.
+		
+		base allows a different texture to be generated for
+		the same repeat interval.
+		"""
+		i = int(fmod(floor(x), repeat))
+		j = int(fmod(floor(y), repeat))
+		k = int(fmod(floor(z), repeat))
+		ii = (i + 1) % repeat
+		jj = (j + 1) % repeat
+		kk = (k + 1) % repeat
+		if base:
+			i += base; j += base; k += base
+			ii += base; jj += base; kk += base
+
+		x -= floor(x); y -= floor(y); z -= floor(z)
+		fx = x**3 * (x * (x * 6 - 15) + 10)
+		fy = y**3 * (y * (y * 6 - 15) + 10)
+		fz = z**3 * (z * (z * 6 - 15) + 10)
+
+		perm = self.permutation
+		A = perm[i]
+		AA = perm[A + j]
+		AB = perm[A + jj]
+		B = perm[ii]
+		BA = perm[B + j]
+		BB = perm[B + jj]
+		
+		return lerp(fz, lerp(fy, lerp(fx, grad3(perm[AA + k], x, y, z),
+										  grad3(perm[BA + k], x - 1, y, z)),
+								 lerp(fx, grad3(perm[AB + k], x, y - 1, z),
+										  grad3(perm[BB + k], x - 1, y - 1, z))),
+						lerp(fy, lerp(fx, grad3(perm[AA + kk], x, y, z - 1),
+										  grad3(perm[BA + kk], x - 1, y, z - 1)),
+								 lerp(fx, grad3(perm[AB + kk], x, y - 1, z - 1),
+										  grad3(perm[BB + kk], x - 1, y - 1, z - 1))))
+
+
diff --git a/minimal/objects.py b/minimal/objects.py
index e3ff268..b442407 100644
--- a/minimal/objects.py
+++ b/minimal/objects.py
@@ -4,15 +4,15 @@
 """
 
 from datetime import datetime
-from functools import partial
 
 import cairo
 from colour import Color
 import cv2
 import imageio
-import noise
 import numpy as np
 
+from . import noise
+
 #TODO: Can we do 3d rotations via cv2.warpPerspective
 
 #FIXME: WORK IN PROGRESS 
@@ -207,9 +207,7 @@ def couple(self, line):
         self._lead += line.div
         self.pts = np.vstack((self.pts[:-1], line.pts[:]))
 
-    def noise(self, scale=1, method="simplex", z=datetime.now().microsecond,
-              octaves=1, persistence=0.5, lacunarity=2.0,
-              repeatx=1024, repeaty=1024, repeatz=1024):
+    def noise(self, scale=1, z=datetime.now().microsecond, **kwargs):
         """
         Using simplex or perlin noise methods to add noise in constructed line.pts
         
@@ -219,10 +217,12 @@ def noise(self, scale=1, method="simplex", z=datetime.now().microsecond,
                                        to axis 0, 1
             method (str): Determine which noise method to apply to lines
             z (int, float): Variable parameter to ensure random generation of noise in each run
-            ** kwargs as defined by noise.pnoise3 and noise.snoise3
+            ** kwargs as defined by noise.SimplexNoise
         
         """
 
+        snoise = noise.SimplexNoise()
+
         if isinstance(scale, int) or isinstance(scale, float):
             scalex, scaley = scale, scale
         elif isinstance(scale, tuple):
@@ -230,29 +230,13 @@ def noise(self, scale=1, method="simplex", z=datetime.now().microsecond,
         else:
             raise NotImplementedError("Noise scale only accepts int for uniform axis definition or tuple with length 2 for singular axis definition")
 
-        if method == "simplex":
-            _noise = noise.snoise3
-        elif method == "perlin":
-            _noise = partial(noise.pnoise3, 
-                             repeatx=repeatx, repeaty=repeaty, repeatz=repeatz)
-        else:
-            raise NotImplementedError("Noise method only accepts 'perlin' or 'simplex'")
-
         for pt in range(len(self.pts)):
-            self.pts[pt, 0] += scalex*_noise(self.pts[pt, 0]/self.div, 
-                                             self.pts[pt, 1]/self.div,
-                                             z,
-                                             octaves=octaves, 
-                                             persistence=persistence, 
-                                             lacunarity=lacunarity, 
-                                             )
-            self.pts[pt, 1] += scaley*_noise(self.pts[pt, 0]/self.div, 
-                                             self.pts[pt, 1]/self.div,
-                                             z,
-                                             octaves=octaves, 
-                                             persistence=persistence, 
-                                             lacunarity=lacunarity, 
-                                             )
+            self.pts[pt, 0] += scalex*snoise.noise3(x=self.pts[pt, 0]/self.div, 
+                                                    y=self.pts[pt, 1]/self.div,
+                                                    z=z)
+            self.pts[pt, 1] += scaley*snoise.noise3(x=self.pts[pt, 0]/self.div, 
+                                                    y=self.pts[pt, 1]/self.div,
+                                                    z=z)
     
     #TODO: Bezier Curve in Line: https://stackoverflow.com/questions/12643079/b%C3%A9zier-curve-fitting-with-scipy