diff --git a/codepen/arcs.html b/codepen/arcs.html
new file mode 100644
index 0000000..85071ef
--- /dev/null
+++ b/codepen/arcs.html
@@ -0,0 +1,124 @@
+
+
+
\ No newline at end of file
diff --git a/codepen/cubes.html b/codepen/cubes.html
new file mode 100644
index 0000000..b66d64e
--- /dev/null
+++ b/codepen/cubes.html
@@ -0,0 +1,139 @@
+
+
\ No newline at end of file
diff --git a/codepen/drops.html b/codepen/drops.html
new file mode 100644
index 0000000..b624009
--- /dev/null
+++ b/codepen/drops.html
@@ -0,0 +1,165 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/codepen/face.html b/codepen/face.html
new file mode 100644
index 0000000..382260b
--- /dev/null
+++ b/codepen/face.html
@@ -0,0 +1,157 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/codepen/foggy-forest.html b/codepen/foggy-forest.html
new file mode 100644
index 0000000..1e1eab5
--- /dev/null
+++ b/codepen/foggy-forest.html
@@ -0,0 +1,102 @@
+
+
+
\ No newline at end of file
diff --git a/codepen/followers.html b/codepen/followers.html
new file mode 100644
index 0000000..15ced05
--- /dev/null
+++ b/codepen/followers.html
@@ -0,0 +1,101 @@
+
+
\ No newline at end of file
diff --git a/codepen/morbius.html b/codepen/morbius.html
new file mode 100644
index 0000000..ad3cd85
--- /dev/null
+++ b/codepen/morbius.html
@@ -0,0 +1,66 @@
+
\ No newline at end of file
diff --git a/codepen/morph.html b/codepen/morph.html
new file mode 100644
index 0000000..d0fafe5
--- /dev/null
+++ b/codepen/morph.html
@@ -0,0 +1,239 @@
+
+
+
\ No newline at end of file
diff --git a/codepen/mother-box.html b/codepen/mother-box.html
new file mode 100644
index 0000000..9314b21
--- /dev/null
+++ b/codepen/mother-box.html
@@ -0,0 +1,209 @@
+
+
+
\ No newline at end of file
diff --git a/codepen/nebula.html b/codepen/nebula.html
new file mode 100644
index 0000000..1dd40b4
--- /dev/null
+++ b/codepen/nebula.html
@@ -0,0 +1,166 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/codepen/pixeliser.html b/codepen/pixeliser.html
new file mode 100644
index 0000000..14da605
--- /dev/null
+++ b/codepen/pixeliser.html
@@ -0,0 +1,123 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/codepen/rings.html b/codepen/rings.html
new file mode 100644
index 0000000..38eb5c3
--- /dev/null
+++ b/codepen/rings.html
@@ -0,0 +1,137 @@
+
+
+
\ No newline at end of file
diff --git a/codepen/ripples.html b/codepen/ripples.html
new file mode 100644
index 0000000..4a51ef2
--- /dev/null
+++ b/codepen/ripples.html
@@ -0,0 +1,90 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/codepen/solar.html b/codepen/solar.html
new file mode 100644
index 0000000..bc27c53
--- /dev/null
+++ b/codepen/solar.html
@@ -0,0 +1,84 @@
+
+Position of the moon, sun and planets in our solar system, as seen from earth
+
+
+
+
\ No newline at end of file
diff --git a/codepen/tau.html b/codepen/tau.html
new file mode 100644
index 0000000..dba8798
--- /dev/null
+++ b/codepen/tau.html
@@ -0,0 +1,132 @@
+
+
+
\ No newline at end of file
diff --git a/codepen/tree-of-thought.html b/codepen/tree-of-thought.html
new file mode 100644
index 0000000..621360b
--- /dev/null
+++ b/codepen/tree-of-thought.html
@@ -0,0 +1,175 @@
+
+
+
\ No newline at end of file
diff --git a/codepen/trigonometry.html b/codepen/trigonometry.html
new file mode 100644
index 0000000..e4ed479
--- /dev/null
+++ b/codepen/trigonometry.html
@@ -0,0 +1,278 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/codepen/wander-lost.html b/codepen/wander-lost.html
new file mode 100644
index 0000000..0e7d995
--- /dev/null
+++ b/codepen/wander-lost.html
@@ -0,0 +1,289 @@
+
+
+
\ No newline at end of file
diff --git a/codepen/worlds-end.html b/codepen/worlds-end.html
new file mode 100644
index 0000000..ed28b0a
--- /dev/null
+++ b/codepen/worlds-end.html
@@ -0,0 +1,203 @@
+
+
+
\ No newline at end of file
diff --git a/old/src/experiments/polyhedron/1.ts b/old/src/experiments/polyhedron/1.ts
deleted file mode 100644
index a3d2025..0000000
--- a/old/src/experiments/polyhedron/1.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import * as THREE from 'three';
-import * as Base from './base';
-
-const shape = new THREE.TetrahedronGeometry(200);
-export default Base.run(shape, 10, 20);
\ No newline at end of file
diff --git a/old/src/experiments/polyhedron/2.ts b/old/src/experiments/polyhedron/2.ts
deleted file mode 100644
index e3bb52c..0000000
--- a/old/src/experiments/polyhedron/2.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import * as THREE from 'three';
-import * as Base from './base';
-
-const shape = new THREE.DodecahedronGeometry(200);
-export default Base.run(shape, 1, 30);
\ No newline at end of file
diff --git a/old/src/experiments/polyhedron/base.ts b/old/src/experiments/polyhedron/base.ts
deleted file mode 100644
index 50f26da..0000000
--- a/old/src/experiments/polyhedron/base.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { PI2 } from '@/constants';
-import SimplexNoise from 'simplex-noise';
-import * as THREE from 'three';
-
-const FRAME = 640;
-const PARTICLES = 10000;
-const SIZE = 2;
-
-const scene = new THREE.Scene();
-const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
-camera.position.z = 500;
-
-const renderer = new THREE.WebGLRenderer();
-renderer.setSize(FRAME, FRAME);
-
-export function run(shape: THREE.PolyhedronGeometry, layers: number, spread: number) {
- const lines = [];
- for (let i = 0; i < shape.attributes.position.array.length; i += 9) {
- const face = {
- a: {
- x: shape.attributes.position.array[i],
- y: shape.attributes.position.array[i + 1],
- z: shape.attributes.position.array[i + 2],
- },
- b: {
- x: shape.attributes.position.array[i + 3],
- y: shape.attributes.position.array[i + 4],
- z: shape.attributes.position.array[i + 5],
- },
- c: {
- x: shape.attributes.position.array[i + 6],
- y: shape.attributes.position.array[i + 7],
- z: shape.attributes.position.array[i + 8],
- },
- };
-
- lines.push({
- from: face.a,
- to: face.b,
- });
-
- lines.push({
- from: face.b,
- to: face.c,
- });
-
- lines.push({
- from: face.c,
- to: face.a,
- });
- }
-
- const particles = [];
- const vertices = [];
- const group = Math.ceil(PARTICLES / lines.length / layers);
- for (let i = 0; i < PARTICLES; i++) {
- const line = lines[Math.floor(i / group / layers)];
- const index = i % group;
-
- const n = 1.6 * (index / group) - 0.3;
- const dX = line.from.x + (line.to.x - line.from.x) * n;
- const dY = line.from.y + (line.to.y - line.from.y) * n;
- const dZ = line.from.z + (line.to.z - line.from.z) * n;
- const position = new THREE.Vector3(dX, dY, dZ);
- vertices.push(position.clone());
-
- const particle = {
- point: vertices[i],
- group: Math.floor(i / group),
- layer: Math.floor(i / group) % layers,
- position,
- index,
- line,
- };
- particles.push(particle);
- }
-
- const geometry = new THREE.BufferGeometry();
-
- const material = new THREE.PointsMaterial({
- color: 0xFFFFFF,
- opacity: 0.2,
- size: SIZE,
- transparent: true,
- });
-
- const points = new THREE.Points(geometry, material);
- scene.add(points);
-
- const simplex = new SimplexNoise();
-
- return {
- duration: 10,
- element: renderer.domElement,
- size: SIZE,
- onTick: (tick) => {
- const d = (0.5 * Math.sin(PI2 * tick) + 0.5);
- const p = 1 - 0.75 * d;
- const s = spread * d + 5;
-
- scene.rotation.y = PI2 * tick;
-
- for (let i = 0; i < particles.length; i++) {
- const particle = particles[i];
-
- const n = particle.index / group;
- const alpha = PI2 * (n + tick);
- const cos = Math.cos(alpha);
- const sin = Math.sin(alpha);
- const noiseX = simplex.noise3D(cos + particle.group + 1, sin, n) * s;
- const noiseY = simplex.noise3D(cos + particle.group + 2, sin, n) * s;
- const noiseZ = simplex.noise3D(cos + particle.group + 3, sin, n) * s;
- particle.point.x = particle.position.x * p + noiseX;
- particle.point.y = particle.position.y * p + noiseY;
- particle.point.z = particle.position.z * p + noiseZ;
- }
-
- geometry.setFromPoints(vertices);
-
- renderer.render(scene, camera);
- },
- }
-}
diff --git a/package.json b/package.json
index c51b477..873372c 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@types/lodash": "^4.17.7",
"@types/three": "^0.166.0",
"alea": "^1.0.1",
+ "gsap": "^3.12.5",
"lodash": "^4.17.21",
"next": "14.2.5",
"pixi-filters": "^6.0.4",
diff --git a/public/assets/images/duality.png b/public/assets/images/duality.png
new file mode 100644
index 0000000..43699d6
Binary files /dev/null and b/public/assets/images/duality.png differ
diff --git a/public/assets/images/polyhedron-1.png b/public/assets/images/polyhedron-1.png
new file mode 100644
index 0000000..20331e6
Binary files /dev/null and b/public/assets/images/polyhedron-1.png differ
diff --git a/public/assets/images/polyhedron-2.png b/public/assets/images/polyhedron-2.png
new file mode 100644
index 0000000..2d3a056
Binary files /dev/null and b/public/assets/images/polyhedron-2.png differ
diff --git a/src/@types/common.d.ts b/src/@types/common.d.ts
index 9ea1cab..b49a8ff 100644
--- a/src/@types/common.d.ts
+++ b/src/@types/common.d.ts
@@ -1,5 +1,7 @@
interface Experiment {
+ date: string;
disabled?: boolean;
+ featured?: boolean;
image: string;
index: string;
path: string;
diff --git a/src/app/cubic/1/page.tsx b/src/app/cubic/1/page.tsx
index de52e01..fd64410 100644
--- a/src/app/cubic/1/page.tsx
+++ b/src/app/cubic/1/page.tsx
@@ -45,6 +45,9 @@ export default class Cubic1 extends React.Component
}
}
}
+
+ // Trigger state change
+ this.setState({});
}
handleTick = (progress: number) => {
diff --git a/src/app/cubic/2/page.tsx b/src/app/cubic/2/page.tsx
index b195176..da74d6b 100644
--- a/src/app/cubic/2/page.tsx
+++ b/src/app/cubic/2/page.tsx
@@ -38,6 +38,9 @@ export default class Cubic2 extends React.Component
}
}
}
+
+ // Trigger state change
+ this.setState({});
}
handleTick = (progress: number) => {
diff --git a/src/app/cubic/3/page.tsx b/src/app/cubic/3/page.tsx
index 63ec232..6562bb9 100644
--- a/src/app/cubic/3/page.tsx
+++ b/src/app/cubic/3/page.tsx
@@ -57,6 +57,9 @@ export default class Cubic3 extends React.Component
}
}
}
+
+ // Trigger state change
+ this.setState({});
}
handleTick = (progress: number) => {
diff --git a/src/app/duality/Shapes.tsx b/src/app/duality/Shapes.tsx
new file mode 100644
index 0000000..1946144
--- /dev/null
+++ b/src/app/duality/Shapes.tsx
@@ -0,0 +1,260 @@
+import { PI_D2, PI_M2 } from '@/setup';
+import gsap, { Back, Power2 } from 'gsap';
+import * as PIXI from 'pixi.js';
+import { Ticker } from 'pixi.js';
+
+export class ShapeBase extends PIXI.Container {
+
+ mask = new PIXI.Graphics();
+ maskRounded = false;
+ progress = 0;
+ shape = new PIXI.Graphics();
+ speed = 0.00001;
+
+ length: number;
+ maskThickness: number;
+ radius: number;
+ tickElement: PIXI.Graphics | null;
+ velocity: number;
+
+ constructor(radius: number, thickness: number, length = 1, velocity = 0) {
+ super();
+
+ this.radius = radius;
+ this.length = length;
+ this.velocity = velocity;
+
+ this.addChild(this.mask);
+ this.maskThickness = thickness + 5;
+
+ this.addChild(this.shape);
+ this.tickElement = this.shape;
+ }
+
+ intro = (index: number) => {
+ const delay = index * 0.1;
+ this.progress = 0;
+
+ gsap.from(this, {
+ delay,
+ duration: 0.5,
+ ease: Power2.easeInOut,
+ maskThickness: 0,
+ });
+
+ gsap.from(this.scale, {
+ delay,
+ duration: 3,
+ ease: Back.easeInOut,
+ x: 0.9,
+ y: 0.9,
+ });
+
+ gsap.to(this, {
+ delay,
+ duration: 3,
+ ease: Power2.easeInOut,
+ progress: Math.PI * this.length,
+ onUpdate: this.introProgress,
+ });
+
+ if (this.velocity) {
+ PIXI.Ticker.shared.add(this.tick);
+ }
+ }
+
+ introProgress = () => {
+ if (this.mask) {
+ this.mask.clear();
+ this.mask.setStrokeStyle({
+ color: 0,
+ width: this.maskThickness,
+ });
+ this.mask.arc(0, 0, this.radius, -this.progress, this.progress);
+
+ if (this.maskRounded) {
+ this.mask.circle(Math.sin(PI_D2 + this.progress) * this.radius, Math.cos(PI_D2 + this.progress) * this.radius, 1);
+ this.mask.circle(Math.sin(PI_D2 - this.progress) * this.radius, Math.cos(PI_D2 - this.progress) * this.radius, 1);
+ }
+
+ this.mask.stroke();
+ }
+ }
+
+ tick = (ticker: Ticker) => {
+ if (this.tickElement) {
+ this.tickElement.rotation += this.velocity * ticker.deltaMS * this.speed;
+ }
+ }
+}
+
+export class Circle extends ShapeBase {
+
+ constructor(color: number, radius: number, thickness: number, length?: number, velocity?: number) {
+ super(radius, thickness, length, velocity);
+
+ this.shape.setStrokeStyle({
+ color,
+ width: thickness,
+ });
+ this.shape.circle(0, 0, radius);
+ this.shape.stroke();
+
+ this.tickElement = this.mask;
+ }
+
+}
+
+export class Dashed extends ShapeBase {
+
+ color: number;
+ radius: number;
+ thickness: number;
+ dash: number;
+ gap: number;
+ velocity: number;
+
+ constructor(color: number, radius: number, thickness: number, dash: number, gap: number, velocity: number) {
+ super(radius, thickness, 1, velocity);
+
+ this.maskRounded = true;
+
+ const circumference = PI_M2 * radius;
+ const segments = Math.round(circumference / (dash + gap));
+ const radiusInner = radius - thickness / 2;
+ const radiusOuter = radiusInner + thickness;
+
+ this.shape.setStrokeStyle({
+ color,
+ width: dash,
+ });
+ for (let i = 0; i < segments; i++) {
+ const n = (i / segments) * PI_M2;
+ const x = Math.sin(n);
+ const y = Math.cos(n);
+
+ this.shape.moveTo(x * radiusInner, y * radiusInner);
+ this.shape.lineTo(x * radiusOuter, y * radiusOuter);
+ }
+ this.shape.stroke();
+ }
+
+}
+
+export class Iterated extends ShapeBase {
+
+ constructor(color: number, radius: number, thickness: number, segments: number, velocity: number, shapesData: string) {
+ super(radius, thickness, 1, velocity);
+
+ this.maskRounded = true;
+
+ const bounds = {
+ left: Number.MAX_VALUE,
+ right: -Number.MAX_VALUE,
+ };
+
+ const shapes = shapesData.split(';')
+ .map(shapeData => {
+ const shape = shapeData.split(',');
+
+ const data: any = {
+ type: shape.length === 3 ? 'circle' : 'line',
+ };
+
+ if (data.type === 'circle') {
+ data.x = parseInt(shape[0]) + radius;
+ data.y = parseInt(shape[1]);
+ data.radius = parseInt(shape[2]);
+
+ bounds.left = Math.min(bounds.left, -data.radius);
+ bounds.right = Math.max(bounds.right, data.radius);
+
+ } else {
+ data.x = [];
+ data.y = [];
+
+ for (let i = 0; i < shape.length; i++) {
+ const value = parseInt(shape[i]);
+ if (i % 2) {
+ data.x.push(value + radius);
+ } else {
+ data.y.push(value);
+ }
+ }
+
+ bounds.left = Math.min.apply(this, [
+ bounds.left,
+ ...data.x,
+ ]);
+ bounds.right = Math.max.apply(this, [
+ bounds.right,
+ ...data.x,
+ ]);
+ }
+
+ return data;
+ });
+
+ this.maskThickness = Math.abs(bounds.right - bounds.left) + 5;
+
+ this.shape.setStrokeStyle({
+ color,
+ width: thickness,
+ });
+ for (let i = 0; i < segments; i++) {
+ const n = (i / segments) * PI_M2;
+ const cosN = Math.cos(n);
+ const sinN = Math.sin(n);
+
+ for (const shape of shapes) {
+ if (shape.type === 'circle') {
+ this.shape.circle(shape.x * cosN - shape.y * sinN, shape.y * cosN + shape.x * sinN, shape.radius);
+
+ } else {
+ for (let k = 0; k < shape.x.length; k++) {
+ const x = shape.x[k];
+ const y = shape.y[k];
+ if (k) {
+ this.shape.lineTo(x * cosN - y * sinN, y * cosN + x * sinN);
+ } else {
+ this.shape.moveTo(x * cosN - y * sinN, y * cosN + x * sinN);
+ }
+ }
+
+ }
+
+ }
+ }
+ this.shape.stroke();
+ }
+
+}
+
+export class Polygon extends ShapeBase {
+
+ constructor(color: number, radius: number, thickness: number, sides: number, velocity: number) {
+ super(radius, thickness, 1, velocity);
+
+ const angle = PI_M2 / sides;
+
+ this.shape.setStrokeStyle({
+ color,
+ width: thickness,
+ });
+ for (let i = 0; i <= sides; i++) {
+ const a = i * angle;
+ const x = Math.sin(a) * radius;
+ const y = Math.cos(a) * radius;
+
+ if (i) {
+ this.shape.lineTo(x, y);
+ } else {
+ this.shape.moveTo(x, y);
+ }
+ }
+ this.shape.stroke();
+
+ this.maskThickness = thickness + 20;
+ }
+
+}
\ No newline at end of file
diff --git a/src/app/duality/Sides.tsx b/src/app/duality/Sides.tsx
new file mode 100644
index 0000000..153e568
--- /dev/null
+++ b/src/app/duality/Sides.tsx
@@ -0,0 +1,110 @@
+import { BLACK, PI_D2, WHITE } from '@/setup';
+import * as PIXI from 'pixi.js';
+import { Circle, Dashed, Iterated, Polygon, ShapeBase } from './Shapes';
+
+class SideBase extends PIXI.Container {
+
+ background = new PIXI.Graphics();
+ shapes = new PIXI.Container();
+ size = 1920;
+
+ constructor(primary: number, secondary: number, offset: number) {
+ super();
+
+ const center = this.size / 2;
+
+ // Add background
+ this.addChild(this.background);
+ this.background.clear();
+ this.background.rect(offset * (center), 0, center, this.size);
+ this.background.fill(primary);
+
+ // Add shapes container
+ this.addChild(this.shapes);
+ this.shapes.x = Math.round(center);
+ this.shapes.y = Math.round(center);
+
+ // Add shapes mask
+ const mask = new PIXI.Graphics();
+ this.shapes.mask = mask;
+ mask.clear();
+ mask.rect(offset * (center), 0, center, this.size);
+ mask.fill(0);
+
+ // Add shapes
+ this.add(new Dashed(secondary, 130, 0.5, 2, 5, 1));
+ this.add(new Dashed(secondary, 160, 3, 3, 100, -5));
+ this.add(new Dashed(secondary, 260, 0.5, 2, 5, -1));
+ this.add(new Circle(secondary, 280, 1));
+ this.add(new Dashed(secondary, 290, 10, 1, 15, 3));
+ this.add(new Circle(secondary, 300, 1));
+ this.add(new Circle(secondary, 270, 2, 0.3, 20));
+ this.add(new Circle(secondary, 310, 2, 0.4, -30));
+ this.add(new Dashed(secondary, 390, 3, 3, 118, 4));
+ this.add(new Dashed(secondary, 470, 2, 2, 15, 3));
+ this.add(new Dashed(secondary, 480, 2, 2, 7, -2));
+ this.add(new Circle(secondary, 490, 2));
+ this.add(new Circle(secondary, 555, 1));
+ this.add(new Dashed(secondary, 570, 25, 2, 100, -5));
+ this.add(new Circle(secondary, 585, 1));
+ this.add(new Dashed(secondary, 600, 2, 2, 7, 6));
+ this.add(new Circle(secondary, 625, 2, 0.3, -40));
+ this.add(new Circle(secondary, 655, 2, 0.4, 30));
+ this.add(new Circle(secondary, 710, 2));
+ this.add(new Circle(secondary, 871, 1));
+ this.add(new Circle(secondary, 890, 1));
+ }
+
+ add = (shape: ShapeBase) => {
+ this.shapes.addChild(shape);
+ shape.rotation = this.shapes.children.length % 2 ? PI_D2 : -PI_D2;
+ }
+
+ intro = () => {
+ for (const shape of this.shapes.children) {
+ shape.intro(shape.radius / 890 * this.shapes.children.length);
+ }
+ }
+}
+
+export class BlackSide extends SideBase {
+
+ constructor() {
+ super(BLACK, WHITE, 0);
+
+ this.add(new Polygon(WHITE, 80, 0.5, 8, -5));
+ this.add(new Polygon(WHITE, 95, 1, 8, -5));
+ this.add(new Polygon(WHITE, 100, 0.5, 8, -5));
+ this.add(new Iterated(WHITE, 210, 1, 12, 15, '-5,-10,-5,10,8,0,-5,-10'));
+ this.add(new Iterated(WHITE, 390, 1, 14, -3, '-40,0,-20,-35,20,-35,40,0,20,35,-20,35,-40,0'));
+ this.add(new Iterated(WHITE, 520, 1, 50, 5, '-5,-5,5,5;-5,5,5,-5'));
+ this.add(new Iterated(WHITE, 640, 1, 260, 4, '-10,0,0,-10,10,0,0,10,-10,0'));
+ this.add(new Polygon(WHITE, 690, 2, 20, -5));
+ this.add(new Polygon(WHITE, 695, 0.5, 20, -5));
+ this.add(new Iterated(WHITE, 787, 1, 100, -2, '0,50,-3,-50,3,-50,0,50'));
+
+ this.intro();
+ }
+
+}
+
+export class WhiteSide extends SideBase {
+
+ constructor() {
+ super(WHITE, BLACK, 1);
+
+ this.add(new Circle(BLACK, 80, 0.5));
+ this.add(new Circle(BLACK, 95, 1));
+ this.add(new Circle(BLACK, 100, 0.5));
+ this.add(new Iterated(BLACK, 210, 1, 12, 15, '0,0,10'));
+ this.add(new Iterated(BLACK, 390, 1, 14, -3, '0,0,40'));
+ this.add(new Iterated(BLACK, 520, 1, 50, 5, '0,-6,0,6;-6,0,6,0'));
+ this.add(new Iterated(BLACK, 640, 1, 260, 4, '0,0,10'));
+ this.add(new Circle(BLACK, 690, 2));
+ this.add(new Circle(BLACK, 695, 0.5));
+ this.add(new Iterated(BLACK, 787, 1, 100, -2, '0,-50,0,50'));
+
+ this.intro();
+ }
+
+}
\ No newline at end of file
diff --git a/src/app/duality/page.tsx b/src/app/duality/page.tsx
new file mode 100644
index 0000000..12768ac
--- /dev/null
+++ b/src/app/duality/page.tsx
@@ -0,0 +1,28 @@
+'use client';
+import { PixiPlayer } from '@/components';
+import * as PIXI from 'pixi.js';
+import React from 'react';
+import { BlackSide, WhiteSide } from './Sides';
+
+export default class Duality extends React.Component {
+
+ // TODO: Add experiment controls
+ // TODO: Add player controls
+
+ handleInit = (app: PIXI.Application) => {
+ const black = new BlackSide();
+ app.stage.addChild(black);
+
+ const white = new WhiteSide();
+ app.stage.addChild(white);
+ }
+
+ render() {
+ return ;
+ }
+
+}
\ No newline at end of file
diff --git a/src/app/page.tsx b/src/app/page.tsx
index fd1124b..454d3de 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,30 +1,71 @@
'use client';
import { experiments } from '@/setup';
-import { Box, Card, CardActionArea, CardContent, CardHeader, CardMedia, Grid, Typography } from '@mui/material';
+import { Box, Card, CardActionArea, CardContent, CardHeader, CardMedia, Divider, Grid, Typography } from '@mui/material';
export default function Home() {
- return
-
-
-
-
- Just some experimental digital artwork
-
-
-
-
- {experiments.map((experiment, index) => !experiment.disabled && (
-
-
-
-
-
-
-
-
- ))}
-
+ return
+
+
+
+
+
+ Just some experimental digital artwork
+
+
+
+
+
+
+ Featured
+
+
+
+ {experiments.filter(experiment => experiment.featured).map((experiment, index) => (
+
+
+
+
+
+ {experiment.index}
+ |
+ {experiment.date}
+
+ )}
+ title={experiment.title}
+ />
+
+
+
+ ))}
+
+
+
+
+
+ {experiments.filter(experiment => !experiment.disabled && !experiment.featured).map((experiment, index) => (
+
+
+
+
+
+ {experiment.index}
+ |
+ {experiment.date}
+
+ )}
+ title={experiment.title}
+ />
+
+
+
+ ))}
+
+
;
}
diff --git a/src/app/polyhedron/1/page.tsx b/src/app/polyhedron/1/page.tsx
new file mode 100644
index 0000000..e9dc018
--- /dev/null
+++ b/src/app/polyhedron/1/page.tsx
@@ -0,0 +1,15 @@
+'use client';
+import * as THREE from 'three';
+import PolyhedronBase from '../PolyhedronBase';
+
+export default function Polyhedron1() {
+
+ const shape = new THREE.TetrahedronGeometry(200);
+
+ return ;
+
+}
\ No newline at end of file
diff --git a/src/app/polyhedron/2/page.tsx b/src/app/polyhedron/2/page.tsx
new file mode 100644
index 0000000..e4b6016
--- /dev/null
+++ b/src/app/polyhedron/2/page.tsx
@@ -0,0 +1,15 @@
+'use client';
+import * as THREE from 'three';
+import PolyhedronBase from '../PolyhedronBase';
+
+export default function Polyhedron2() {
+
+ const shape = new THREE.DodecahedronGeometry(200);
+
+ return ;
+
+}
\ No newline at end of file
diff --git a/src/app/polyhedron/PolyhedronBase.tsx b/src/app/polyhedron/PolyhedronBase.tsx
new file mode 100644
index 0000000..e72228c
--- /dev/null
+++ b/src/app/polyhedron/PolyhedronBase.tsx
@@ -0,0 +1,168 @@
+'use client';
+import { ExperimentControls, ThreePlayer } from '@/components';
+import { PI_M2 } from '@/setup';
+import React from 'react';
+import { createNoise3D } from 'simplex-noise';
+import * as THREE from 'three';
+
+export default class PolyhedronBase extends React.Component {
+
+ container = new THREE.Group();
+ geometry = new THREE.BufferGeometry();
+ groups = 0;
+ lines: PolyhedronBaseLine[] = [];
+ noise3D = createNoise3D();
+ particles: PolyhedronBaseParticle[] = [];
+ particleSize = 2;
+ vertices: THREE.Vector3[] = [];
+
+ constructor(props: PolyhedronBaseProps) {
+ super(props);
+
+ const { layers, spread } = this.props;
+
+ this.state = {
+ layers: { min: 1, max: 100, step: 1, value: layers },
+ particles: { min: 100, max: 100000, step: 100, value: 50000 },
+ speed: { min: 1, max: 10, step: 1, value: 1 },
+ spread: { min: 1, max: 100, step: 1, value: spread },
+ };
+ }
+
+ handleInit = (scene: THREE.Scene, camera: THREE.PerspectiveCamera) => {
+ const { shape } = this.props;
+
+ // Update camera
+ camera.position.z = 500;
+
+ // Add container
+ scene.add(this.container);
+
+ // Add points
+ const material = new THREE.PointsMaterial({
+ color: 0xFFFFFF,
+ opacity: 0.2,
+ size: this.particleSize,
+ transparent: true,
+ });
+
+ const points = new THREE.Points(this.geometry, material);
+ this.container.add(points);
+
+ // Get shape lines
+ for (let i = 0; i < shape.attributes.position.array.length; i += 9) {
+ const face = {
+ a: {
+ x: shape.attributes.position.array[i],
+ y: shape.attributes.position.array[i + 1],
+ z: shape.attributes.position.array[i + 2],
+ },
+ b: {
+ x: shape.attributes.position.array[i + 3],
+ y: shape.attributes.position.array[i + 4],
+ z: shape.attributes.position.array[i + 5],
+ },
+ c: {
+ x: shape.attributes.position.array[i + 6],
+ y: shape.attributes.position.array[i + 7],
+ z: shape.attributes.position.array[i + 8],
+ },
+ };
+
+ this.lines.push({
+ from: face.a,
+ to: face.b,
+ });
+
+ this.lines.push({
+ from: face.b,
+ to: face.c,
+ });
+
+ this.lines.push({
+ from: face.c,
+ to: face.a,
+ });
+ }
+
+ // Trigger state change
+ this.setState({});
+ }
+
+ componentDidUpdate() {
+ const { layers, particles } = this.state;
+
+ // Remove previous particles and vertices
+ while (this.particles.length) {
+ this.particles.pop();
+ }
+ while (this.vertices.length) {
+ this.vertices.pop();
+ }
+
+ // Get new particles and vertices
+ this.groups = Math.ceil(particles.value / this.lines.length / layers.value);
+ for (let i = 0; i < particles.value; i++) {
+ const line = this.lines[Math.floor(i / this.groups / layers.value)];
+ const index = i % this.groups;
+
+ const n = 1.6 * (index / this.groups) - 0.3;
+ const dX = line.from.x + (line.to.x - line.from.x) * n;
+ const dY = line.from.y + (line.to.y - line.from.y) * n;
+ const dZ = line.from.z + (line.to.z - line.from.z) * n;
+ const position = new THREE.Vector3(dX, dY, dZ);
+ this.vertices.push(position.clone());
+
+ const particle = {
+ point: this.vertices[i],
+ group: Math.floor(i / this.groups),
+ layer: Math.floor(i / this.groups) % layers.value,
+ position,
+ index,
+ line,
+ };
+ this.particles.push(particle);
+ }
+ }
+
+ handleTick = (progress: number) => {
+ const { speed, spread } = this.state;
+ const tick = (progress * speed.value) % 1;
+
+ this.container.rotation.y = PI_M2 * tick;
+
+ const d = (0.5 * Math.sin(PI_M2 * tick) + 0.5);
+ const p = 1 - 0.75 * d;
+ const s = spread.value * d + 5;
+
+ for (const particle of this.particles) {
+ const n = particle.index / this.groups;
+ const alpha = PI_M2 * (n + tick);
+ const cos = Math.cos(alpha);
+ const sin = Math.sin(alpha);
+ const noiseX = this.noise3D(cos + particle.group + 1, sin, n) * s;
+ const noiseY = this.noise3D(cos + particle.group + 2, sin, n) * s;
+ const noiseZ = this.noise3D(cos + particle.group + 3, sin, n) * s;
+ particle.point.x = particle.position.x * p + noiseX;
+ particle.point.y = particle.position.y * p + noiseY;
+ particle.point.z = particle.position.z * p + noiseZ;
+ }
+
+ this.geometry.setFromPoints(this.vertices);
+ }
+
+ render() {
+ return <>
+ this.setState(items)}
+ />
+
+ >;
+ }
+
+}
\ No newline at end of file
diff --git a/src/app/polyhedron/PolyhedronBase.types.d.ts b/src/app/polyhedron/PolyhedronBase.types.d.ts
new file mode 100644
index 0000000..b7c4ce1
--- /dev/null
+++ b/src/app/polyhedron/PolyhedronBase.types.d.ts
@@ -0,0 +1,27 @@
+interface PolyhedronBaseProps {
+ layers: number;
+ spread: number;
+ shape: THREE.PolyhedronGeometry;
+}
+
+interface PolyhedronBaseLine {
+ from: {
+ x: any;
+ y: any;
+ z: any;
+ };
+ to: {
+ x: any;
+ y: any;
+ z: any;
+ };
+}
+
+interface PolyhedronBaseParticle {
+ group: number;
+ index: number;
+ layer: number;
+ line: PolyhedronBaseLine;
+ point: THREE.Vector3;
+ position: THREE.Vector3;
+}
\ No newline at end of file
diff --git a/src/components/PixiPlayer/PixiPlayer.tsx b/src/components/PixiPlayer/PixiPlayer.tsx
index df9a998..035bd65 100644
--- a/src/components/PixiPlayer/PixiPlayer.tsx
+++ b/src/components/PixiPlayer/PixiPlayer.tsx
@@ -85,7 +85,7 @@ export default class PixiPlayer extends Component {
const { duration, startTime } = this.state;
-
+
// Update progress
const progress = (((performance.now() / 1000) - startTime) / duration) % 1;
this.setState({
@@ -93,7 +93,7 @@ export default class PixiPlayer extends Component {
@@ -135,12 +135,14 @@ export default class PixiPlayer extends Component
-
+ {!!this.props.onTick && (
+
+ )}
>;
}
diff --git a/src/components/PixiPlayer/PixiPlayer.types.d.ts b/src/components/PixiPlayer/PixiPlayer.types.d.ts
index 1034adb..4862eaf 100644
--- a/src/components/PixiPlayer/PixiPlayer.types.d.ts
+++ b/src/components/PixiPlayer/PixiPlayer.types.d.ts
@@ -2,7 +2,7 @@ interface PixiPlayerProps {
duration?: number;
size?: number;
onInit(app: PIXI.Application): void;
- onTick(progress: number): void;
+ onTick?(progress: number): void;
}
interface PixiPlayerState {
diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx
index 4b1f2b7..6f03b0f 100644
--- a/src/components/Sidebar/Sidebar.tsx
+++ b/src/components/Sidebar/Sidebar.tsx
@@ -19,7 +19,7 @@ export default function Sidebar() {
Experiments
-
+
diff --git a/src/setup/constants.ts b/src/setup/constants.ts
index e5588ba..8624121 100644
--- a/src/setup/constants.ts
+++ b/src/setup/constants.ts
@@ -1,3 +1,6 @@
+export const BLACK = 0x000000;
+export const WHITE = 0xFFFFFF;
+
export const PI = Math.PI;
export const PI_D2 = Math.PI / 2;
export const PI_D4 = Math.PI / 4;
diff --git a/src/setup/experiments.ts b/src/setup/experiments.ts
index d092d8e..dae0495 100644
--- a/src/setup/experiments.ts
+++ b/src/setup/experiments.ts
@@ -1,82 +1,75 @@
import _ from 'lodash';
-const items = [
- 'simplex-noise/1',
- 'simplex-noise/2',
- 'hexagon/1',
- 'hexagon/2',
- 'hexagon/3',
- 'hexagon/4',
- 'hexagon/5',
- 'sphere/1',
- 'sphere/2',
- 'polyhedron/1',
- 'polyhedron/2',
- 'kaleidoscope/1',
- 'kaleidoscope/2',
- 'kaleidoscope/3',
- 'tunnel/1',
- 'tunnel/2',
- 'cubic/1',
- 'cubic/2',
- 'cubic/3',
- 'wave/1',
- 'flocking/1',
- 'flocking/2',
- 'flocking/3',
- 'eye/1',
- 'eye/2',
- 'eye/3',
- 'plotter/1',
- 'plotter/2',
- 'cubic/4',
- 'tunnel/3',
- 'wave/2',
- 'cubic/5',
- 'cubic/6',
- 'cubic/7',
- 'art/1',
- 'wave/3',
- 'animal/1',
- 'animal/2',
- 'sphere/3',
+const items: [string, string, boolean?, boolean?][] = [
+ ['animal/1', '2019-11-10', false, true],
+ ['animal/2', '2019-12-05', false, true],
+ ['arcs', '2018-04-14', false, true],
+ ['art/1', '2019-10-28', false, true],
+ ['cubes', '2018-06-02', false, true],
+ ['cubic/1', '2019-08-20'],
+ ['cubic/2', '2019-08-20'],
+ ['cubic/3', '2019-08-20'],
+ ['cubic/4', '2019-09-27', false, true],
+ ['cubic/5', '2019-10-07', false, true],
+ ['cubic/6', '2019-10-10', false, true],
+ ['cubic/7', '2019-10-15', false, true],
+ ['drops', '2017-06-02', false, true],
+ ['duality', '2019-05-20', true],
+ ['eye/1', '2019-08-23', false, true],
+ ['eye/2', '2019-08-23', false, true],
+ ['eye/3', '2019-09-10', false, true],
+ ['face', '2018-03-27', false, true],
+ ['flocking/1', '2019-08-21', false, true],
+ ['flocking/2', '2019-08-21', false, true],
+ ['flocking/3', '2019-08-23', false, true],
+ ['foggy-forest', '2018-03-31', false, true],
+ ['followers', '2018-06-03', false, true],
+ ['hexagon/1', '2019-08-11'],
+ ['hexagon/2', '2019-08-11'],
+ ['hexagon/3', '2019-08-11'],
+ ['hexagon/4', '2019-08-11'],
+ ['hexagon/5', '2019-08-11'],
+ ['kaleidoscope/1', '2019-08-18', false, true],
+ ['kaleidoscope/2', '2019-08-18', false, true],
+ ['kaleidoscope/3', '2019-08-18', false, true],
+ ['mobius', '2017-06-01', false, true],
+ ['morph', '2017-10-31', false, true],
+ ['mother-box', '2017-12-04', false, true],
+ ['nebula', '2017-10-30', false, true],
+ ['pixeliser', '2018-04-14', false, true],
+ ['plotter/1', '2019-09-17', false, true],
+ ['plotter/2', '2019-09-18', false, true],
+ ['polyhedron/1', '2019-08-14'],
+ ['polyhedron/2', '2019-08-14'],
+ ['rings', '2018-06-21', false, true],
+ ['ripples', '2017-06-07', false, true],
+ ['simplex-noise/1', '2019-08-09'],
+ ['simplex-noise/2', '2019-08-11'],
+ ['solar', '2018-04-02', false, true],
+ ['sphere/1', '2019-08-13'],
+ ['sphere/2', '2019-08-13'],
+ ['sphere/3', '2020-01-30', false, true],
+ ['tau', '2018-06-15', false, true],
+ ['tree-of-thoughts', '2017-11-01', false, true],
+ ['trigonometry', '2017-08-08', false, true],
+ ['tunnel/1', '2019-08-19', false, true],
+ ['tunnel/2', '2019-08-19', false, true],
+ ['tunnel/3', '2019-09-29', false, true],
+ ['wander-lost', '2019-05-28', false, true],
+ ['wave/1', '2019-08-21', false, true],
+ ['wave/2', '2019-10-04', false, true],
+ ['wave/3', '2019-11-09', false, true],
+ ['worlds-end', '2018-02-22', false, true],
];
-const disabled = [
- 'polyhedron/1',
- 'polyhedron/2',
- 'kaleidoscope/1',
- 'kaleidoscope/2',
- 'kaleidoscope/3',
- 'tunnel/1',
- 'tunnel/2',
- 'wave/1',
- 'flocking/1',
- 'flocking/2',
- 'flocking/3',
- 'eye/1',
- 'eye/2',
- 'eye/3',
- 'plotter/1',
- 'plotter/2',
- 'cubic/4',
- 'tunnel/3',
- 'wave/2',
- 'cubic/5',
- 'cubic/6',
- 'cubic/7',
- 'art/1',
- 'wave/3',
- 'animal/1',
- 'animal/2',
- 'sphere/3',
-];
+items.sort((a, b) => a[1] < b[1] ? 1 : -1);
-items.reverse();
-export const experiments: Experiment[] = items.map((item, index) => ({
- disabled: disabled.includes(item),
- image: `/assets/images/${item.replace('/', '-')}.png`,
+export const experiments: Experiment[] = items.map(([name, date, featured, disabled], index) => ({
+ date,
+ disabled,
+ featured,
+ image: `/assets/images/${name.replace('/', '-')}.png`,
index: (items.length - index).toString().padStart(3, '0'),
- path: `/${item}`,
- title: _.startCase(item),
+ path: `/${name}`,
+ title: _.startCase(name),
}));
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 4ea8cf9..8d0d745 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1749,6 +1749,11 @@ graphemer@^1.4.0:
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
+gsap@^3.12.5:
+ version "3.12.5"
+ resolved "https://registry.yarnpkg.com/gsap/-/gsap-3.12.5.tgz#136c02dad4c673b441bdb1ca00104bfcb4eae7f4"
+ integrity sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ==
+
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"