diff --git a/README.md b/README.md
index 977a3aa..8768d92 100644
--- a/README.md
+++ b/README.md
@@ -11,3 +11,18 @@ Or build with:
```
$ pnpm run build
```
+
+# Individual Projects
+
+## Flow Fields
+
+Inspired by https://youtu.be/M_SUcX66SDA Low Byte Productions Flow Fields video
+
+Uses code from:
+* https://github.com/francisrstokes/vec-la-fp/tree/master
+* https://github.com/francisrstokes/microcan
+
+
+## Konva-ball
+
+Experimenting with konva library
diff --git a/src/flow-fields/index.html b/src/flow-fields/index.html
new file mode 100644
index 0000000..0952e84
--- /dev/null
+++ b/src/flow-fields/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Flow Fields
+
+
+
+
+
+
+
diff --git a/src/flow-fields/index.js b/src/flow-fields/index.js
new file mode 100644
index 0000000..42469e4
--- /dev/null
+++ b/src/flow-fields/index.js
@@ -0,0 +1,104 @@
+// Inspired by https://youtu.be/M_SUcX66SDA Low Byte Productions Flow Fields video
+
+import { vScale, vAdd, vAngle } from "./util.js";
+
+//const vAngle = (a) => [Math.cos(a), Math.sin(a)];
+
+const canvas = document.getElementById("main");
+const ctx = canvas.getContext("2d");
+
+const width = 800;
+const height = 800;
+
+// A 10x10 grid, transformed onto the canvas
+const n = 25;
+const fieldSize = width/n;
+
+canvas.setAttribute("width", width);
+canvas.setAttribute("height", height);
+
+
+
+const fill = (ctx, r = 0, g = 0, b = 0, a = 255) => {
+ ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`;
+}
+
+const stroke = (ctx, r = 0, g = 0, b = 0, a = 255) => {
+ ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${a})`;
+};
+
+const line = (ctx, v, v2) => {
+ ctx.beginPath();
+ ctx.moveTo(...v);
+ ctx.lineTo(...v2);
+ ctx.stroke();
+ ctx.closePath();
+}
+
+const ellipse = (ctx, v, rx, ry = rx, oa = 0, sa = 0, ea = Math.PI * 2) => {
+ ctx.beginPath();
+ ctx.ellipse(...v, rx, ry, oa, sa, ea, false);
+ ctx.stroke();
+ ctx.fill();
+ ctx.closePath();
+}
+
+const background = (ctx, r = 0, g = 0, b = 0, a = 255) => {
+ fill(ctx, r, g, b, a);
+ ctx.fillRect(0, 0, width, height);
+}
+
+fill(ctx, 0,0,0,0);
+stroke(ctx, 255,255,255,1);
+background(ctx, 0, 0, 255, width, height);
+
+const vRandom = () => vAngle(Math.random() * Math.PI * 2);
+
+
+
+/*
+for (let y = 0; y < n; y++) {
+ for (let x = 0; x < n; x++) {
+ const sv = vAdd([fieldSize/2, fieldSize/2], vScale(fieldSize, [x, y]));
+ ellipse(ctx, sv, fieldSize/2);
+ }
+}
+*/
+
+let prevTimestamp;
+
+const draw = (timestamp) => {
+
+ if (prevTimestamp === undefined) {
+ prevTimestamp = timestamp;
+ }
+
+ const elapsed = timestamp - prevTimestamp;
+
+ if (elapsed > 500) {
+ prevTimestamp = timestamp;
+
+ const field = Array.from({length: n}, () => Array.from({length: n}, () => {
+ return vRandom();
+ }));
+
+ fill(ctx, 0,0,0,0);
+ stroke(ctx, 255,255,255,1);
+ background(ctx, 0, 0, 255, width, height);
+
+ field.forEach((row, y) => row.forEach((fv, x) => {
+ const sv = vAdd([fieldSize/2, fieldSize/2], vScale(fieldSize, [x, y]));
+ const upper = vAdd(sv, vScale(fieldSize/2, fv));
+ const lower = vAdd(sv, vScale(-fieldSize/2, fv));
+ line(ctx, upper, lower);
+ fill(ctx, 255,255,255,1);
+ ellipse(ctx, upper, 2);
+
+ }));
+ }
+
+
+ window.requestAnimationFrame(draw);
+};
+
+draw();
diff --git a/src/flow-fields/util.js b/src/flow-fields/util.js
new file mode 100644
index 0000000..c967bcd
--- /dev/null
+++ b/src/flow-fields/util.js
@@ -0,0 +1,8 @@
+
+const vScale = (scale, vec) => vec.map(n => n * scale);
+
+const vAdd = (v0, v1) => [v0[0] + v1[0], v0[1] + v1[1]];
+
+const vAngle = (a) => [Math.cos(a), Math.sin(a)];
+
+export { vAdd, vScale, vAngle };
diff --git a/src/index.html b/src/index.html
index ecd2d7d..83d0abf 100644
--- a/src/index.html
+++ b/src/index.html
@@ -8,6 +8,7 @@
Spazer