Add projects and tutorials

This commit is contained in:
John Gatward
2026-03-16 18:03:17 +00:00
parent fc54c3bd4e
commit 4e94902f01
132 changed files with 19170 additions and 65 deletions

View File

@@ -0,0 +1,7 @@
class dot{
constructor(x, y){
this.x = x;
this.y = y;
}
}

View File

@@ -0,0 +1,95 @@
<DOCTYPE! html>
<html>
<title>Convex Hull</title>
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Inconsolata" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.2.0/p5.min.js"
integrity="sha512-b/htz6gIyFi3dwSoZ0Uv3cuv3Ony7EeKkacgrcVg8CMzu90n777qveu0PBcbZUA7TzyENGtU+qZRuFAkfqgyoQ=="
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/latest.js?config=AM_CHTML"></script>
<button id="backButton" onclick="window.location.href='/#tutorials'">Back</button>
<head>
<h1>Convex Hull - Jarvis' Marsh</h1>
<p>A convex hull is where a convex polygon is constructed to bound a set of points on a 2D plane. So, given all
points in the set P, what is the smallest convex polygon we can construct that bounds all points in P.</p>
<br>
<p>Pseudo code:</p>
<div class="psudoCode">
<ul>
<li>Randomly place n points on a 2D plane.</li>
<li>Find the leftmost point/the point with the smallest x value. Called `gamma`.</li>
<li>Find the vector which is the <i>most left</i> which connects `gamma` to point q in set P.</li>
<li>Store `gamma` and q in a list and repeat until q = `gamma`.</li>
</div>
<br>
</head>
<body>
<p>This canvas starts off by generating a set amount of points, placing them at random locations around the
screen and adding them to an array. It then finds the index of the point in the array - `vertices[]` - with
the lowest x value. It then loops through the array finding all vector's originating from `gamma`,
<a>`vec(gammaP(i))`</a>.
</p>
<p>We then examine each vertex and evaluate which one is the <i>leftmost</i> from the perspective of point
`gamma`. Say in this case `P(q)` is the leftmost vector so we push q onto the stack.</p><br>
<p>Stack: `((q), (gamma))`</p><br>
<p>We then repeat until we reach our starting point, which in this case means our starting point is the same as
the next point (`q=gamma`)</p>
<img class="examples" src="stepByStep.png">
<p>Now the question of how to calculate the <i>leftness</i> of a vector compared to another. We can use the
<a>cross product</a> in 2D.
</p>
<p><a>`vecu times vecv = u`<sub>`x`</sub>`*v`<sub>`y`</sub>` - u`<sub>`y`</sub>`*v`<sub>`x`</sub></a> If the
cross product is greater than 0, the vector is to the left. If it is equal to 0 then the vectors are
colinear</p>
<p><b>Note:</b> to convert coordinates to a vector, find the difference between your origin point and your
target point</p>
<div class="psudoCode">
<a>for (var i = 0; i < vertices.length; i++){</a><br>
<a>&nbsp;if (i != originPoint){</a><br>
<a>&nbsp;&nbsp;x1 = vertices[recordLeftVector].x - vertices[originPoint].x;</a><br>
<a>&nbsp;&nbsp;y1 = vertices[recordLeftVector].y - vertices[originPoint].y;</a><br><br>
<a>&nbsp;&nbsp;x2 = vertices[i].x - vertices[originPoint].x;</a><br>
<a>&nbsp;&nbsp;y2 = vertices[i].y - vertices[originPoint].y;</a><br><br>
<a>&nbsp;&nbsp;if ((y2 * x1) - (y1 * x2) <= 0){</a><br>
<a>&nbsp;&nbsp;&nbsp;recordLeftVector = i;</a><br>
<a>&nbsp;&nbsp;}</a><br>
<a>&nbsp;}</a><br>
<a>}</a><br>
</div>
<p>Now we just have to add in a little piece of code at the bottom of this function to: increment the iteration
number so we don't recurse until eternity, add our leftmost vector to the route stack and call the function
again with the next point as a parameter.</p>
<div class="psudoCode">
<a>if (originPoint != recordLeftVector && iterations < 75){</a><br>
<a>&nbsp;&nbsp;route[iterations] = recordLeftVector;</a><br>
<a>&nbsp;&nbsp;iterations += 1;</a><br>
<a>&nbsp;&nbsp;findLeftLine(recordLeftVector);</a><br>
<a>}</a><br>
</div><br><br>
</body>
<script src="sketch.js"></script>
<script src="dot.js"></script>
<!--https://www.youtube.com/watch?v=Vu84lmMzP2o<br>
http://jeffe.cs.illinois.edu/teaching/compgeom/notes/01-convexhull.pdf</p>-->
</html>

View File

@@ -0,0 +1,102 @@
var jarvisMarsh = function( p ){
const numPoints = 12;
var vertices = [];
var route = [];
let iterations = 0;
p.setup = function(){
p.createCanvas(750, 500);
p.background(217);
button = p.createButton('Reset Button')
button.mousePressed(p.resetSketch);
p.initValues();
}
p.draw = function(){}
p.drawLine = function(){
let r = p.findLeftPoint();
route[iterations] = r;
p.findLeftLine(r);
p.stroke(215, 75, 75);
p.strokeWeight(1);
for (var i = 0; i < route.length - 1; i++){
p.line(vertices[route[i]].x, vertices[route[i]].y, vertices[route[i+1]].x, vertices[route[i+1]].y)
}
}
p.findLeftLine = function(originPoint){
var recordLeftVector = 1;
let x1, x2, y1, y2;
for (var i = 0; i < vertices.length; i++){
if (i != originPoint){
x1 = vertices[recordLeftVector].x - vertices[originPoint].x;
y1 = vertices[recordLeftVector].y - vertices[originPoint].y;
x2 = vertices[i].x - vertices[originPoint].x;
y2 = vertices[i].y - vertices[originPoint].y;
//if the result if positive then vector u is left of vector v
//where u and v are both vectors from the target point
//If its equal to 0 then they're colinear. This is also good :)
if ((y2 * x1) - (y1 * x2) <= 0){
recordLeftVector = i;
}
}
}
if (originPoint != recordLeftVector && iterations < 75){
route[iterations] = recordLeftVector;
iterations += 1;
p.findLeftLine(recordLeftVector);
}
}
p.findLeftPoint = function(){
let recordLeft = 0;
for (var i = 0; i < vertices.length; i++){
if (vertices[i].x < vertices[recordLeft].x){
recordLeft = i;
}
}
return recordLeft;
}
p.resetSketch = function(){
vertices.length = 0;
p.clear()
p.background(217);
p.initValues();
p.findLeftLine();
p.draw();
}
p.initValues = function(){
p.stroke(0);
p.strokeWeight(7);
var bufferx = p.width * 0.0625;
var buffery = p.height * 0.0625;
for (var i = 0; i < numPoints; i++){
vertices[i] = new dot(p.random(bufferx, p.width - bufferx), p.random(buffery, p.height - buffery));
p.point(vertices[i].x, vertices[i].y);
}
iterations = 0;
p.drawLine();
}
};
var myp5 = new p5(jarvisMarsh, 'c1');

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -0,0 +1,71 @@
h1{
font-family: 'Roboto Condensed', sans-serif;
margin: 0;
padding: 0 0 15px 0;
font-weight: 700;
font-size: 32px;
}
h2{
font-family: 'Roboto Condensed', sans-serif;
margin: 0;
padding: 15px 0 15px 0;
font-weight: 650;
font-size: 20px;
}
p{
margin: 1px;
font-family: 'Roboto', sans-serif;
font-size: 19px;
padding: 5px 0px 5px 0px;
}
.psudoCode{
font-family: 'Inconsolata', monospace;
font-size: 19px;
background-color: #D9D9D9;
border: 1px solid;
padding: 5px;
box-shadow: 5px 5px #888888;
}
a{
font-family: 'Inconsolata', monospace;
font-size: 19px;
background-color: #D9D9D9;
}
button{
background-color: white;
border: 2px solid #555555;
color: black;
padding: 16px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
-webkit-transition-duration: 0.4s; /* Safari */
transition-duration: 0.4s;
cursor: pointer;
float:right;
}
button:hover {
background-color: #555555;
color: white;
}
.examples{
padding: 10px;
}
html{
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+/Edge */
user-select: none; /* Standard */
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,8 @@
class dot{
constructor(x, y){
this.x = x;
this.y = y;
}
}

View File

@@ -0,0 +1,104 @@
<html>
<title>Midpoint Displacement</title>
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Inconsolata" rel="stylesheet">
<link rel"shortcut icon" type"image/ico" href"/Media/iconImage.ico">
<link rel="stylesheet" type="text/css" href="style.css">
<button id="backButton" onclick="window.location.href='/#tutorials'">Back</button>
<head>
<h1>Midpoint Displacement</h1>
<p>Midpoint displacement is a common way to generate terrains in video
games. Instead of manually creating a stage you can make an
algorithm create one based off randomness. This is faster to
calculate than a perlin noise stage which is why it can be found
frequently in older video game titles.</p><br>
<p>Psudo code:</p>
<div class="psudoCode">
<ul>
<li>Create two points.</li>
<li>Find the center x coordinate.</li>
<li>Create a new point in between the two first points with some
offset in the y coorindate.</li>
<li>Repeat. (can be recursive)</li>
</div>
<br>
</head>
<body>
<p>This canvas starts off with two points in an array called
<a>points[]</a>, one at <a>height/2</a> and one at
<a>math.random(-vertical_displacement, vertical_displacement)</a>.
The center point is found and a new point is made and added to the
array. This new point is offset on the y-axis by an amount however
we can't use the same <a>vertical_displacement</a> as we did before
as we'd end up with a very jagged terrain.
</p>
<img class="examples" src="Jagged_Terrain.jpg">
<p>Instead we have to iterate our variable -
<a>vertical_displacement</a> - based on the iteration number of the
sketch, in order to generate a smooth stage/terrain.
</p>
<div class="psudoCode">
<a>vertical_displacement = points[0].y + points[1].y / 2;</a><br>
<a>vertical_displacement *= 2 ** (-roughness);</a>
</div>
<p>The top formula is the inital value. The second is applied very
iteration of the sketch.</p>
<p>Right now we can add one point to the list, time to add several! This
means for loops. But before we can loop through our array how do we
know which point should connect to which? We need to sort the array
based on x value, so that the points read off from left to right.
</p>
<p>I just used a bubble sort for simplicity however it's worth pointing
out that the number of points grows exponetially - <a>O(2^n)</a>
where n = iterations - so after just 10 iterations <a>points.length
= 1024</a>. A merge sort could be used to speed up computation
times.</p>
<p>Now we can use a for loop to go through our array and compare the ith
item to the (i+1)th item, and then update <a>iterations</a> and
<a>vertical_displacement</a>.
</p>
<div class="psudoCode">
<a>var pLength = points.length;</a><br><br>
<a>&nbsp;for (var i = 0; i < pLength - 1; i++){</a><br>
<a>&nbsp;&nbsp;&nbsp;var midX = (points[i].x + points[i +
1].x) / 2;</a><br>
<a>&nbsp;&nbsp;&nbsp;var midY = (points[i].y + points[i +
1].y) / 2;</a><br>
<a>&nbsp;&nbsp;&nbsp;midY += random(-vertical_displacement,
vertical_displacement);</a><br>
<a>&nbsp;&nbsp;&nbsp;points[pLength + i] = new point(midX,
midY);</a><br>
<a>&nbsp;}</a><br><br>
<a>iterations += 1;</a><br>
<a>vertical_displacement *= 2 ** (-roughness);</a><br>
<a>bubbleSort();</a><br>
</div>
<h2>NOTE: Do not use points.length to define your for loop as we are
increasing the length of the array within the loop</h2>
<!--<span id="c1"></span>-->
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.2.0/p5.min.js"
integrity="sha512-b/htz6gIyFi3dwSoZ0Uv3cuv3Ony7EeKkacgrcVg8CMzu90n777qveu0PBcbZUA7TzyENGtU+qZRuFAkfqgyoQ=="
crossorigin="anonymous"></script>
<script src="sketch.js"></script>
<script src="dot.js"></script>
</html>

View File

@@ -0,0 +1,90 @@
var MidDisplacement = function( p ){
var points = [];
var iterations = 1;
const roughness = 1;
var vertical_displacement;
let button;
p.setup = function(){
p.createCanvas(800, 500);
p.frameRate(2);
p.background(217);
button = p.createButton('Reset Button')
//button.position(p.width, p.height);
button.mousePressed(p.resetSketch);
p.initValues();
}
p.draw = function(){}
p.bubbleSort = function() {
var length = points.length;
//Number of passes
for (var i = 0; i < length; i++) {
//Notice that j < (length - i)
for (var j = 0; j < (length - i - 1); j++) {
//Compare the x values
if(points[j].x > points[j+1].x) {
//Swap the numbers
var tmp = points[j]; //Temporary variable to hold the current number
points[j] = points[j+1]; //Replace current number with adjacent number
points[j+1] = tmp; //Replace adjacent number with current number
}
}
}
}
p.mouseClicked = function(){
if (iterations < 11){
var pLength = points.length;
for (var i = 0; i < pLength - 1; i++){
var midX = (points[i].x + points[i + 1].x) / 2;
var midY = (points[i].y + points[i + 1].y) / 2;
midY += p.random(-vertical_displacement, vertical_displacement);
points[pLength + i] = new dot(midX, midY);
}
iterations += 1;
vertical_displacement *= 2 ** (-roughness);
p.bubbleSort();
p.clear();
p.background(215);
for (var i = 0; i < pLength; i++){
p.strokeWeight(4);
p.stroke(0);
p.point(points[i].x * 2, points[i].y);
p.strokeWeight(1);
p.stroke(215, 15, 15);
p.line(points[i].x * 2, points[i].y, points[i+1].x * 2, points[i+1].y);
}
}
}
p.initValues = function(){
points[0] = new dot(0, p.height/2);
points[1] = new dot(p.width, p.height/2);
vertical_displacement = points[0].y + points[1].y / 2;
vertical_displacement *= 2 ** (-roughness);
iterations = 1;
}
p.resetSketch = function(){
points.length = 0;
p.clear()
p.background(217);
p.initValues();
}
};
var myp5 = new p5(MidDisplacement, 'c1');

View File

@@ -0,0 +1,71 @@
h1{
font-family: 'Roboto Condensed', sans-serif;
margin: 0;
padding: 0 0 15px 0;
font-weight: 700;
font-size: 32px;
}
h2{
font-family: 'Roboto Condensed', sans-serif;
margin: 0;
padding: 15px 0 15px 0;
font-weight: 650;
font-size: 20px;
}
p{
margin: 1px;
font-family: 'Roboto', sans-serif;
font-size: 19px;
padding: 5px 0px 5px 0px;
}
.psudoCode{
font-family: 'Inconsolata', monospace;
font-size: 19px;
background-color: #D9D9D9;
border: 1px solid;
padding: 5px;
box-shadow: 5px 5px #888888;
}
a{
font-family: 'Inconsolata', monospace;
font-size: 19px;
background-color: #D9D9D9;
}
button{
background-color: white;
border: 2px solid #555555;
color: black;
padding: 16px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
-webkit-transition-duration: 0.4s; /* Safari */
transition-duration: 0.4s;
cursor: pointer;
float:right;
}
button:hover {
background-color: #555555;
color: white;
}
.examples{
padding: 10px;
}
html{
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+/Edge */
user-select: none; /* Standard */
}

View File

@@ -0,0 +1,83 @@
@import "../nord.scss";
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
$roboto-slab: 'Roboto', sans-serif;
html {
background-color: $nord0;
}
h1 {
color: $nord6;
font-family: $roboto-slab;
font-size: 48pt;
}
p {
color: $nord4;
font-family: $roboto-slab;
font-size: 16pt;
}
code {
font-size: 14pt;
border: 4px solid $nord13;
}
a {
color: $nord4;
font-family: $roboto-slab;
font-size: 24pt;
}
#subsetOutput {
color: $nord8;
}
#wholeOutput {
color: $nord15;
}
#tlOutput{
color: $nord12;
}
#tOutput{
color: $nord13;
}
#lOutput{
color: $nord14;
}
#sumDiv {
text-align: center;
}
button {
display: inline-block;
width: 25%;
padding: 10px 25px;
margin: 0.1em;
border: 4px solid $nord8;
box-sizing: border-box;
text-decoration: none;
font-family: 'Roboto',sans-serif;
font-size: 14pt;
color: $nord2;
background-color: $nord6;
text-align: center;
position: relative;
transition: all 0.15s;
&:hover {
background-color: $nord3;
color: $nord6;
border-color: $nord10;
}
&:focus {
border: 4px solid $nord8;
box-sizing: border-box;
}
}

View File

@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/atom-one-dark.min.css" />
<link rel="stylesheet" href="style.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.2.0/p5.min.js"
integrity="sha512-b/htz6gIyFi3dwSoZ0Uv3cuv3Ony7EeKkacgrcVg8CMzu90n777qveu0PBcbZUA7TzyENGtU+qZRuFAkfqgyoQ=="
crossorigin="anonymous"></script>
<script src="script.js"></script>
<script>
fetch("snippets/defineCount.js")
.then(r => r.text())
.then(code => {
document.getElementById("define-count").textContent = code;
if (window.hljs) hljs.highlightElement(document.getElementById("define-count"));
});
fetch("snippets/countSquares.js")
.then(r => r.text())
.then(code => {
document.getElementById("count-squares").textContent = code;
if (window.hljs) hljs.highlightElement(document.getElementById("count-squares"));
});
</script>
<head>
<h1>Summed-area Table</h1>
<p>
A Summed-area table is a data structure and algorithm for quickly and
efficiently generating the sum of values in a rectangular grid. In image
processing it is also known as an integral image.
</p>
</head>
<body>
<p>
To generate a summed-area table, each cell is given a value which is the
sum of all values in a rectangular subset where the top left corner is the
first cell in the grid and where the bottom right cell is the cell you're
calculating the value for (assumuing the array starts top left).
</p>
<pre>
<code id="define-count" class="js">
</code>
</pre>
<p>
Now each cell has its own count value. This isn't much help on it's own,
however we can use it to calculate the sum value of any subset of the grid
by interrogating 4 cells and performing only 4 additions. NOTE: we have to
add the 'top left' region as it as been subtracted twice.
</p>
<pre>
<code id="count-squares" class="js">
</code>
</pre>
<p><b>Click</b> a sqaure on the grid to turn it on or off.</p>
<p><b>Shift+Click</b> two square to define a rectangle.</p>
<p>
Press the <b>show count</b> button to display the sum value of each cell
(in this example a cell's value can only be 0 or 1).
</p>
<p>
Press <b>show rectangles</b> to see the different regions simplified and
see how
it changes the sum below.
</p>
<div id="sumDiv">
<a id="subsetOutput">0</a>
<a> = </a>
<a id="wholeOutput"> 0</a>
<a> + </a>
<a id="tlOutput"> 0</a>
<a> - </a>
<a id="lOutput"> 0</a>
<a> - </a>
<a id="tOutput"> 0</a>
</div>
<button onclick="showCountToggle()">Show count</button>
<button onclick="toggleRectMode()">Show rectangles</button>
</body>
</html>

View File

@@ -0,0 +1,278 @@
let myWidth = 800, myHeight = 800, offset = 5;
let r, n = 10;
let t = [];
let tl = null
let br = null;
let showCount = false;
let drawRectMode = 0;
let sOutput, wOutput, tlOutput, tOutput, lOutput;
function setup()
{
createCanvas(myWidth + 2*offset, myHeight + 2*offset);
r = myWidth / n;
for (y = 0; y < n; y ++)
{
for (x = 0; x < n; x ++)
{
t.push(new cTile(x*r + offset, y*r + offset, r));
}
}
sOutput = document.getElementById('subsetOutput');
wOutput = document.getElementById('wholeOutput');
tlOutput = document.getElementById('tlOutput');
tOutput = document.getElementById('tOutput');
lOutput = document.getElementById('lOutput');
}
function draw()
{
background(46, 52, 64);
t.forEach(e => e.show());
stroke(156,212,228);
noFill();
strokeWeight(5);
if (tl)
{
if (!br)
{
rect(tl.x, tl.y, tl.w);
} else {
rect(tl.x, tl.y, br.x - tl.x + r, br.y - tl.y + r);
drawRect(drawRectMode);
}
}
}
function defineCount(br)
{
let cols = Math.floor(br.x/ r) + 1;
let rows = Math.floor(br.y/ r) + 1;
let c = 0;
// let index = cols + n * rows;
for (y = 0; y < rows; y++) {
for (x = 0; x < cols; x++){
var i = x + n * y;
if (t[i].s){
c++;
}
}
}
return c;
}
function countSquares(tl, br)
{
let topLeft, left, top, whole;
let tlx, tly, brx, bry;
tlx = Math.floor(tl.x / r);
tly = Math.floor(tl.y / r);
brx = Math.floor(br.x / r);
bry = Math.floor(br.y / r);
topLeft = t[(tlx-1) + n * (tly-1)].count;
left = t[(tlx-1) + n * bry ].count;
top = t[brx + n * (tly-1)].count;
whole = t[brx + n * bry ].count;
var s = whole + topLeft - top - left;
if (s < 0)
s = 0;
return s;
}
function drawRect(n)
{
strokeWeight(5);
noFill();
switch(n)
{
case 1:
//whole
stroke(180,142,173);
fill('rgba(180, 142, 173, 0.05)');
rect(offset, offset, br.x - offset + br.w, br.y - offset + br.w);
break;
case 2:
//topleft
stroke(208,135,112);
fill('rgba(208,135,112, 0.05)');
rect(offset, offset, tl.x - offset, tl.y - offset);
break;
case 3:
//left
stroke(163,190,140);
fill('rgba(163,190,140, 0.05)');
rect(offset, offset, tl.x - offset, br.y - offset + br.w);
break;
case 4:
//top
stroke(235,203,139);
fill('rgba(235,203,139, 0.05)');
rect(offset, offset, br.x - offset + br.w, tl.y - offset);
break;
default:
break;
}
strokeWeight(1);
}
function showCountToggle(){
showCount = !showCount;
}
function toggleRectMode(){
if (tl && br)
{
var tlx = Math.floor(tl.x / r);
var tly = Math.floor(tl.y / r);
var brx = Math.floor(br.x / r);
var bry = Math.floor(br.y / r);
var topLeft = t[(tlx-1) + n * (tly-1)].count;
var left = t[(tlx-1) + n * bry ].count;
var top = t[brx + n * (tly-1)].count;
var whole = t[brx + n * bry ].count;
if (drawRectMode == 0){
wOutput.innerHTML = whole.toString(10);
wOutput.style.fontWeight = "bold";
drawRectMode ++;
} else if (drawRectMode == 1){
tlOutput.innerHTML = topLeft.toString(10);
tlOutput.style.fontWeight = "bold";
wOutput.style.fontWeight = "normal";
drawRectMode ++;
} else if (drawRectMode == 2){
lOutput.innerHTML = left.toString(10);
lOutput.style.fontWeight = "bold";
tlOutput.style.fontWeight = "normal";
drawRectMode ++;
} else if (drawRectMode == 3) {
tOutput.innerHTML = top.toString(10);
tOutput.style.fontWeight = "bold";
lOutput.style.fontWeight = "normal";
drawRectMode ++;
} else if (drawRectMode == 4) {
tOutput.style.fontWeight = "normal";
drawRectMode = 0;
}
}
}
function updateSum()
{
var tlx = Math.floor(tl.x / r);
var tly = Math.floor(tl.y / r);
var brx = Math.floor(br.x / r);
var bry = Math.floor(br.y / r);
var topLeft = t[(tlx-1) + n * (tly-1)].count;
var left = t[(tlx-1) + n * bry ].count;
var top = t[brx + n * (tly-1)].count;
var whole = t[brx + n * bry ].count;
wOutput.innerHTML = whole.toString(10);
tlOutput.innerHTML = topLeft.toString(10);
lOutput.innerHTML = left.toString(10);
tOutput.innerHTML = top.toString(10);
sOutput.innerHTML = countSquares(tl,br).toString(10);
}
function mouseClicked(event)
{
if (event.shiftKey)
{
if (mouseX > offset && mouseX < myWidth && mouseY > offset && mouseY < myHeight)
{
let col = Math.floor(mouseX / r);
let row = Math.floor(mouseY / r);
if (tl != null && br != null)
{
tl = null;
br = null;
}
if (!tl)
{
tl = t[col + n * row];
} else {
br = t[col + n * row];
}
if (tl != null && br != null)
{
updateSum();
}
}
} else {
if (mouseX > 0 && mouseX < myWidth && mouseY > 0 && mouseY < myHeight)
{
let col = Math.floor(mouseX / r);
let row = Math.floor(mouseY / r);
t[col + n * row].setState();
t.forEach(tile => tile.setCount(defineCount(tile)));
if (tl != null && br != null)
{
updateSum();
}
}
}
}
class cTile
{
constructor(_x, _y, _w)
{
this.x = _x;
this.y = _y;
this.s = false;
this.w = _w;
this.count = 0;
}
show()
{
if (this.s)
{
fill(191,97,106);
} else {
noFill();
}
stroke(236,239,244);
strokeWeight(1);
rect(this.x, this.y, this.w, this.w);
if (showCount)
{
textSize(18);
stroke(255);
fill(255);
text(this.count.toString(10), this.x + this.w/2 - 3, this.y + this.w/2 + 5);
}
}
setState()
{
this.s = !this.s;
}
setCount(c)
{
this.count = c;
}
};

View File

@@ -0,0 +1,14 @@
// where tl and br are the top left and bottom right cell respectively
function countSquares(tl, br) {
// these are the four cells to interrogate
let topLeft, left, top, whole;
// 1d index = x + #rows * y
topLeft = t[(tl.x - 1) + n * (tl.y - 1)].count;
left = t[(tl.x - 1) + n * bry].count;
top = t[br.x + n * (tl.y - 1)].count;
whole = t[br.x + n * br.y].count;
return whole + topLeft - top - left;
}

View File

@@ -0,0 +1,16 @@
// where br is the cell we're calculating
function defineCount(br) {
let c = 0;
// br.x is the column number
// br.y is the row number
for (y = 0; y <= br.y; y++) { // loop through all rows above br (and the row br is on)
for (x = 0; x <= br.x; x++) { // loop through all columns to the left of br (and br column)
var i = x + n * y; // convert 2d array format to 1d index
if (t[i].s) { // if cell[i].status==true then increase count
c++;
}
}
}
return c;
}

View File

@@ -0,0 +1,68 @@
@import url("https://fonts.googleapis.com/css2?family=Roboto&display=swap");
html {
background-color: #2e3440; }
h1 {
color: #eceff4;
font-family: "Roboto", sans-serif;
font-size: 48pt; }
p {
color: #d8dee9;
font-family: "Roboto", sans-serif;
font-size: 16pt; }
code {
font-size: 14pt;
border: 4px solid #ebcb8b; }
a {
color: #d8dee9;
font-family: "Roboto", sans-serif;
font-size: 24pt; }
#subsetOutput {
color: #88c0d0; }
#wholeOutput {
color: #b48ead; }
#tlOutput {
color: #d08770; }
#tOutput {
color: #ebcb8b; }
#lOutput {
color: #a3be8c; }
#sumDiv {
text-align: center; }
button {
display: inline-block;
width: 25%;
padding: 10px 25px;
margin: 0.1em;
border: 4px solid #88c0d0;
box-sizing: border-box;
text-decoration: none;
font-family: 'Roboto',sans-serif;
font-size: 14pt;
color: #434c5e;
background-color: #eceff4;
text-align: center;
position: relative;
transition: all 0.15s; }
button:hover {
background-color: #4c566a;
color: #eceff4;
border-color: #5e81ac; }
button:focus {
border: 4px solid #88c0d0;
box-sizing: border-box; }
#chillin {
display: none; }
/*# sourceMappingURL=style.css.map */

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"mappings": "AACQ,2EAAmE;AAI3E,IAAK;EACD,gBAAgB,EC+BZ,OAAO;;AD5Bf,EAAG;EACC,KAAK,ECuID,OAAO;EDtIX,WAAW,EARD,oBAAoB;EAS9B,SAAS,EAAE,IAAI;;AAGnB,CAAE;EACE,KAAK,EC2GD,OAAO;ED1GX,WAAW,EAdD,oBAAoB;EAe9B,SAAS,EAAE,IAAI;;AAGnB,IAAK;EACD,SAAS,EAAE,IAAI;EACf,MAAM,EAAE,iBAAiB;;AAG7B,CAAE;EACE,KAAK,ECgGD,OAAO;ED/FX,WAAW,EAzBD,oBAAoB;EA0B9B,SAAS,EAAE,IAAI;;AAGnB,aAAc;EACV,KAAK,ECuID,OAAO;;ADpIf,YAAa;EACT,KAAK,ECqMA,OAAO;;ADlMhB,SAAS;EACL,KAAK,ECoKA,OAAO;;ADjKhB,QAAQ;EACJ,KAAK,EC0KA,OAAO;;ADvKhB,QAAQ;EACJ,KAAK,ECgLA,OAAO;;AD7KhB,OAAQ;EACJ,UAAU,EAAE,MAAM;;AAGtB,MAAO;EACH,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,GAAG;EACV,OAAO,EAAE,SAAS;EAClB,MAAM,EAAE,KAAK;EACb,MAAM,EAAE,iBAAgB;EACxB,UAAU,EAAE,UAAU;EACtB,eAAe,EAAE,IAAI;EACrB,WAAW,EAAE,mBAAmB;EAChC,SAAS,EAAE,IAAI;EACf,KAAK,ECMD,OAAO;EDLX,gBAAgB,EC8EZ,OAAO;ED7EX,UAAU,EAAE,MAAM;EAClB,QAAQ,EAAE,QAAQ;EAClB,UAAU,EAAE,SAAS;EAErB,YAAQ;IACJ,gBAAgB,EC+BhB,OAAO;ID9BP,KAAK,ECuEL,OAAO;IDtEP,YAAY,ECgHX,OAAO;ED7GZ,YAAQ;IACJ,MAAM,EAAE,iBAAgB;IACxB,UAAU,EAAE,UAAU;;AAI9B,QAAS;EACL,OAAO,EAAE,IAAI",
"sources": ["cStyle.scss","../nord.scss"],
"names": [],
"file": "style.css"
}

79
tutorials/tsp/index.html Normal file
View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html>
<link href="style.css" rel="stylesheet" type="text/css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.2.0/p5.min.js"
integrity="sha512-b/htz6gIyFi3dwSoZ0Uv3cuv3Ony7EeKkacgrcVg8CMzu90n777qveu0PBcbZUA7TzyENGtU+qZRuFAkfqgyoQ=="
crossorigin="anonymous"></script>
<title>Travelling Sales Person</title>
<h1>Travelling Sales Person Problem</h1>
<button id="backButton" onclick="window.location.href='/#tutorials'">Back</button>
<head>
<meta charset="UTF-8">
<div class="pictureContainer">
<img src="https://optimization.mccormick.northwestern.edu/images/e/ea/48StatesTSP.png" alt="TSP Problem"
id="Conway">
</div>
<p>The travelling salesman problem (TSP) asks the following question: "Given a list of cities and the distances
between each pair of cities, what is the shortest possible route that visits each city and returns to the origin
city?" </p>
<p>The problem was first formulated in 1930 and is one of the most intensively studied problems in optimization. It
is used as a benchmark for many optimization methods. Even though the problem is computationally difficult, a
large number of heuristics and exact algorithms are known, so that some instances with tens of thousands of
cities can be solved completely and even problems with millions of cities can be approximated within a small
fraction of 1%.</p>
<p>The TSP has several applications even in its purest formulation, such as planning, logistics, and the manufacture
of microchips. Slightly modified, it appears as a sub-problem in many areas, such as DNA sequencing. In these
applications, the concept city represents, for example, customers, soldering points, or DNA fragments, and the
concept distance represents travelling times or cost, or a similarity measure between DNA fragments. The TSP
also appears in astronomy, as astronomers observing many sources will want to minimize the time spent moving the
telescope between the sources. In many applications, additional constraints such as limited resources or time
windows may be imposed.</p>
<h2>These are some of the algorithms I used</h2>
<p>Note the purple route is the best route it's found so far and the thin white lines are the routes it's trying
real time.</p>
</head>
<body>
<div class="canvasBody">
<h3>Random Sort</h3>
<span id="c1"></span>
<p class="canvasText">This canvas sorts through random possiblities. Every frame the program chooses two random
points (cities) and swaps them around. eg say the order was London, Paris, Madrid, the program would swap
London and Paris so that the new order is: Paris, London, Madrid. The program then compares the distance
against the record distance to decide whether the new order is better than the old order. This search method
is the most inefficient way, the worst case scenario is never ending, as the point swaping is random the
program may never reach the optimum route</p><br>
<h3>Lexicographic Order</h3>
<span id="c2"></span>
<p class="canvasText">This canvas sorts through all possible orders sequentially, so after n! (where n is the
number of points) this algorithm is guaranteed to have found the quickest possible route. However it is
highly inefficient always taking n! frames to complete and as n increases, time taken increases
exponentially.</p>
<a target="_blank"
href="https://www.quora.com/How-would-you-explain-an-algorithm-that-generates-permutations-using-lexicographic-ordering">Click
here to learn more about the algorithm</a><br>
<h3>Genetic Algorithm</h3>
<span id="c3"></span>
<p class="canvasText">This canvas is the most efficient at finding the quickest route, it is a mixture of the
two methods above. It starts off by creating a population of orders, a fitness is then generated for each
order in the population. This fitness decides how likely the order is to be picked and is based on the
distance it takes (lower distance is better). When two orders are picked, the algorithm splices the two
together at a random term, it's then mutated and compared against the record distance. This takes the least
amount of time to find the shortest distance as the algorithm doesn't search through permuations that are
obviously longer due to the order.</p><br>
</div>
</body>
<script src="sketch.js"></script>
<footer>
<p>This page was inspired by The Coding Train</p><a
href="https://www.youtube.com/channel/UCvjgXvBlbQiydffZU7m1_aw">Check him out here</a>
</footer>
</html>

382
tutorials/tsp/sketch.js Normal file
View File

@@ -0,0 +1,382 @@
/*var seedSlider = document.getElementById('sliderRange')
seedSlider.onChange = function(){
return this.value;
}
var citiesSlider = document.getElementById('nRange')
citiesSlider.onChange = function(){
return this.value;
}*/
var Seed = 21//seedChange(11);
var totalCities = 25//citiesChange(9);
// Sketch One
var randomSearch = function( p ) { // p could be any variable name
var cities = [];
var recordDistance;
var bestEver;
var localSeed = Seed;
var localCities = totalCities;
p.setup = function() {
p.createCanvas(400, 400);
p.setupPoints(Seed, totalCities);
}
p.setupPoints = function(rngSeed, numCities){
p.randomSeed(rngSeed);
for (var i = 0; i < numCities; i++) {
var v = p.createVector(p.random(p.width), p.random(p.height/2));
cities[i] = v;
}
var d = p.calcDistance(cities);
recordDistance = d;
bestEver = cities.slice();
}
p.draw = function() {
if(localSeed != Seed){
p.setupPoints(Seed, localCities);
localSeed = Seed;
}
if(localCities != totalCities){
setupPoints(localSeed, totalCities);
localCities = totalCities;
}
p.background(0);
p.fill(255);
p.stroke(255);
for (var i = 0; i < cities.length; i++) {
p.ellipse(cities[i].x, cities[i].y, 8, 8);
}
p.stroke(255, 0, 255);
p.strokeWeight(4);
p.noFill();
p.beginShape();
for (var i = 0; i < cities.length; i++) {
p.vertex(bestEver[i].x, bestEver[i].y);
}
p.endShape();
p.stroke(255);
p.translate(0 , p.height/2);
p.strokeWeight(1);
p.noFill();
p.beginShape();
for (var i = 0; i < cities.length; i++) {
p.vertex(cities[i].x, cities[i].y);
}
p.endShape();
var i = p.floor(p.random(cities.length));
var j = p.floor(p.random(cities.length));
p.swap(cities, i, j);
var d = p.calcDistance(cities);
if (d < recordDistance) {
recordDistance = d;
bestEver = cities.slice();
}
}
p.swap = function(a, i, j) {
var temp = a[i];
a[i] = a[j];
a[j] = temp;
}
p.calcDistance = function(points) {
var sum = 0;
for (var i = 0; i < points.length - 1; i++) {
var d = p.dist(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y);
sum += d;
}
return sum;
}
};
var myp5 = new p5(randomSearch, 'c1');
// Sketch Two
var lexicographicOrder = function( q ) {
var cities = [];
var order = [];
var totalPermutations;
var count = 0;
var recordDistance;
var bestEver;
q.setup = function() {
q.randomSeed(Seed);
q.createCanvas(400, 400);
for (var i = 0; i < totalCities; i++) {
var v = q.createVector(q.random(q.width), q.random(q.height / 2));
cities[i] = v;
order[i] = i;
}
var d = q.calcDistance(cities, order);
recordDistance = d;
bestEver = order.slice();
}
q.draw = function() {
q.background(0);
q.fill(255);
for (var i = 0; i < cities.length; i++) {
q.ellipse(cities[i].x, cities[i].y, 8, 8);
}
q.stroke(255, 0, 255);
q.strokeWeight(4);
q.noFill();
q.beginShape();
for (var i = 0; i < order.length; i++) {
var n = bestEver[i];
q.vertex(cities[n].x, cities[n].y);
}
q.endShape();
q.translate(0, q.height / 2);
q.stroke(255);
q.strokeWeight(1);
q.noFill();
q.beginShape();
for (var i = 0; i < order.length; i++) {
var n = order[i];
q.vertex(cities[n].x, cities[n].y);
}
q.endShape();
var d = q.calcDistance(cities, order);
if (d < recordDistance) {
recordDistance = d;
bestEver = order.slice();
}
q.nextOrder();
}
q.swap = function(a, i, j) {
var temp = a[i];
a[i] = a[j];
a[j] = temp;
}
q.calcDistance = function(points, order) {
var sum = 0;
for (var i = 0; i < order.length - 1; i++) {
var cityAIndex = order[i];
var cityA = points[cityAIndex];
var cityBIndex = order[i + 1];
var cityB = points[cityBIndex];
var d = q.dist(cityA.x, cityA.y, cityB.x, cityB.y);
sum += d;
}
return sum;
}
// This is my lexical order algorithm
q.nextOrder = function() {
// STEP 1 of the algorithm
// https://www.quora.com/How-would-you-explain-an-algorithm-that-generates-permutations-using-lexicographic-ordering
var largestI = -1;
for (var i = 0; i < order.length - 1; i++) {
if (order[i] < order[i + 1]) {
largestI = i;
}
}
if (largestI == -1) {
q.noLoop();
}
// STEP 2
var largestJ = -1;
for (var j = 0; j < order.length; j++) {
if (order[largestI] < order[j]) {
largestJ = j;
}
}
// STEP 3
q.swap(order, largestI, largestJ);
// STEP 4: reverse from largestI + 1 to the end
var endArray = order.splice(largestI + 1);
endArray.reverse();
order = order.concat(endArray);
}
};
var myp5 = new p5(lexicographicOrder, 'c2');
var geneticSearch = function( p ) {
var cities = [];
var popSize = 500;
var population = [];
var fitness = [];
var recordDistance = Infinity;
var bestEver;
var currentBest;
var statusP;
p.setup = function(){
p.randomSeed(Seed);
p.createCanvas(400, 400);
var order = [];
for (var i = 0; i < totalCities; i++) {
var v = p.createVector(p.random(p.width), p.random(p.height / 2));
cities[i] = v;
order[i] = i;
}
for (var i = 0; i < popSize; i++) {
population[i] = p.shuffle(order);
}
statusP = p.createP('').style('font-size', '32pt');
}
p.draw = function() {
p.background(0);
p.stroke(255);
p.fill(255);
for (var i = 0; i < cities.length; i++) {
p.ellipse(cities[i].x, cities[i].y, 8, 8);
}
p.calculateFitness();
p.normalizeFitness();
p.nextGeneration();
p.noFill();
p.strokeWeight(4);
p.beginShape();
for (var i = 0; i < bestEver.length; i++) {
var n = bestEver[i];
p.stroke(255, 0, 255);
p.vertex(cities[n].x, cities[n].y);
//p.stroke(255);
//p.ellipse(cities[n].x, cities[n].y, 8, 8);
}
p.endShape();
p.translate(0, p.height / 2);
p.stroke(255);
p.strokeWeight(1);
p.noFill();
p.beginShape();
for (var i = 0; i < currentBest.length; i++) {
var n = currentBest[i];
p.vertex(cities[n].x, cities[n].y);
//p.ellipse(cities[n].x, cities[n].y, 8, 8);
}
p.endShape();
}
p.swap = function(a, i, j) {
var temp = a[i];
a[i] = a[j];
a[j] = temp;
}
p.calcDistance = function(points, order) {
var sum = 0;
for (var i = 0; i < order.length - 1; i++) {
var cityAIndex = order[i];
var cityA = points[cityAIndex];
var cityBIndex = order[i + 1];
var cityB = points[cityBIndex];
var d = p.dist(cityA.x, cityA.y, cityB.x, cityB.y);
sum += d;
}
return sum;
}
p.calculateFitness = function() {
var currentRecord = Infinity;
for (var i = 0; i < population.length; i++) {
var d = p.calcDistance(cities, population[i]);
if (d < recordDistance) {
recordDistance = d;
bestEver = population[i];
}
if (d < currentRecord) {
currentRecord = d;
currentBest = population[i];
}
fitness[i] = 1 / (p.pow(d, 8) + 1);
}
}
p.normalizeFitness = function() {
var sum = 0;
for (var i = 0; i < fitness.length; i++) {
sum += fitness[i];
}
for (var i = 0; i < fitness.length; i++) {
fitness[i] = fitness[i] / sum;;
}
}
p.nextGeneration = function() {
var newPopulation = [];
for (var i = 0; i < population.length; i++) {
var orderA = p.pickOne(population, fitness);
var orderB = p.pickOne(population, fitness);
var order = p.crossOver(orderA, orderB);
p.mutate(order, 0.01);
newPopulation[i] = order;
}
population = newPopulation;
}
p.pickOne = function(list, prob) {
var index = 0;
var r = p.random(1);
while (r > 0) {
r = r - prob[index];
index++;
}
index--;
return list[index].slice();
}
p.crossOver = function(orderA, orderB) {
var start = p.floor(p.random(orderA.length));
var end = p.floor(p.random(start + 1, orderA.length));
var neworder = orderA.slice(start, end);
// var left = totalCities - neworder.length;
for (var i = 0; i < orderB.length; i++) {
var city = orderB[i];
if (!neworder.includes(city)) {
neworder.push(city);
}
}
return neworder;
}
p.mutate = function(order, mutationRate) {
for (var i = 0; i < totalCities; i++) {
if (p.random(1) < mutationRate) {
var indexA = p.floor(p.random(order.length));
var indexB = (indexA + 1) % totalCities;
p.swap(order, indexA, indexB);
}
}
}
}
var myp5 = new p5(geneticSearch, 'c3');

160
tutorials/tsp/style.css Normal file
View File

@@ -0,0 +1,160 @@
@import url('https://fonts.googleapis.com/css?family=Roboto+Condensed');
a{font-family: 'Roboto Condensed', sans-serif; font-size: 18pt;}
h1 {
font-family: 'Roboto Condensed', sans-serif;
margin: 0;
padding: 0 0 15px 0;
}
h2 {
font-family: 'Roboto Condensed', sans-serif;
margin: 0;
padding: 0 0 15px 0;
text-align: center;
text-decoration: bold;
}
@media (min-width: 350px) {
h1 {font-size: 3.25em;}
img{height: 40px;}
p{font-size: 10px;}
h2{font-size: 17px;}
}
@media (min-width: 400px) {
h1 {font-size: 3.25em;}
img{height: 45px;}
p{font-size: 15px;}
h2{font-size: 17px;}
}
@media (min-width: 440px) {
h1 {font-size: 3.5em;}
img {height: 100px;}
p{font-size: 16px;}
h2{font-size: 18px;}
}
@media (min-width: 500px) {
h1 {font-size: 3.75em;}
img{height: 125px;}
p{font-size: 16px;}
h2{font-size: 19px;}
}
@media (min-width: 630px) {
h1 {font-size: 5em;}
img{height: 150px;}
p{font-size: 20px;}
h2{font-size: 24px;}
}
@media (min-width: 768px) {
h1 {
font-size: 5em;
padding-bottom: 30px;
}
img{height: 175px;}
p{font-size: 22px;}
h2{font-size: 26px;}
}
@media (min-width: 1200px) {
h1 {font-size: 8em;}
img{height: 250px;}
p{font-size: 24px;}
h2{font-size: 28px;}
}
p{
font-family: 'Roboto Condensed', sans-serif;
}
h3{
text-align: center;
font-size: 30px;
font-family: 'Roboto Condensed', sans-serif;
}
footer{
padding: 20px;
background-color: #e0e0e0;
font-family: 'Roboto Condensed', sans-serif;
}
@keyframes dimImg{
from {opacity: 1;
filter: alpha(opacity=100);}
to {opacity: 0.4;
filter: alpha(opacity=50);}
}
@keyframes revealText{
from {opacity: 0.4;
filter: alpha(opacity=50);}
to {opacity: 1;
filter: alpha(opacity=100);}
}
.pictureContainer{
float: right;
position: relative;
}
.pictureContainer a{
opacity: 0;
position: absolute;
text-align: center;
top: 5px;
left: 5px;
}
.pictureContainer:hover img{
animation-name: dimImg;
animation-duration: 1s;
opacity: 0.4;
filter: alpha(opacity=50);
}
.pictureContainer:hover a{
animation-name: revealText;
animation-duration: 1s;
opacity: 1;
}
.canvasText{
margin: 0px;
display: inline-block;
text-align: left;
}
#c1, #c2, #c3{
display: block;
margin-left: auto;
margin-right: auto;
width: 50%;
}
.button{
padding: 16px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
-webkit-transition-duration: 0.4s; /* Safari */
transition-duration: 0.4s;
cursor: pointer;
background-color: white;
color: black;
border: 2px solid #555555;
float: right;
}
.button:hover {
background-color: #555555;
color: white;
}