diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
new file mode 100644
index 0000000..c5782ad
--- /dev/null
+++ b/.github/workflows/main.yaml
@@ -0,0 +1,50 @@
+name: Deploy everything
+on:
+ push:
+ branches:
+ - main
+env:
+ CONTAINER_REGISTRY: schmelczera
+ DOMAIN: schmelczer.dev
+ PROJECT_NAME: sdf-2d-demo
+
+jobs:
+ build-frontend:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout current branch with lfs
+ uses: actions/checkout@main
+ with:
+ lfs: true
+
+ - name: Setup auth tokens
+ run: |
+ docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
+
+ - name: Build and push frontend
+ run: |
+ docker build . -t $CONTAINER_REGISTRY/$PROJECT_NAME
+ docker push $CONTAINER_REGISTRY/$PROJECT_NAME
+
+ deploy:
+ runs-on: ubuntu-latest
+ needs:
+ - build
+ steps:
+ - name: Checkout current branch with lfs
+ uses: actions/checkout@main
+ with:
+ lfs: true
+
+ - name: Setup auth tokens
+ run: |
+ # SSH key
+ mkdir ~/.ssh
+ echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
+ chmod 400 ~/.ssh/id_ed25519
+ ssh -o StrictHostKeyChecking=no root@$DOMAIN uptime
+
+ - name: Stack deploy
+ run: |
+ DOCKER_HOST=ssh://root@$DOMAIN docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
+ DOCKER_HOST=ssh://root@$DOMAIN docker stack deploy $PROJECT_NAME -c docker-compose.yml --with-registry-auth
diff --git a/Dockerfile b/Dockerfile
index d7c92e0..b0b745e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,12 +7,11 @@ WORKDIR /home/node
COPY src src
COPY static static
-COPY package.json custom.d.ts tsconfig.json webpack.config.js ./
+COPY package.json tsconfig.json webpack.config.js ./
RUN npm install
RUN npm run build
-
FROM nginx:alpine
HEALTHCHECK --interval=1m --timeout=10s CMD curl --fail http://localhost/ || exit 1
@@ -22,7 +21,7 @@ WORKDIR /usr/share/nginx/html
RUN rm -rf *
COPY --from=build-webpage /home/node/dist .
COPY --from=build-error-pages /home/python/built errors
-RUN find . -type f | xargs gzip -k9 &&\
+RUN find . -type f | xargs gzip -k9 &&\
chmod -R 555 .
COPY nginx-config /etc/nginx/
diff --git a/README.md b/README.md
index 89f655e..e1ff961 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-
+
# SDF-2D demo
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..503b7ee
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,31 @@
+version: "3.8"
+
+services:
+ sdf-2d-demo:
+ init: true
+ image: schmelczera/sdf-2d-demo
+ networks:
+ - network
+ deploy:
+ replicas: 3
+ resources:
+ limits:
+ cpus: "0.1"
+ memory: 16M
+ reservations:
+ cpus: "0.1"
+ memory: 16M
+ placement:
+ max_replicas_per_node: 1
+ update_config:
+ parallelism: 1
+ failure_action: rollback
+ delay: 10s
+ monitor: 10s
+ restart_policy:
+ condition: on-failure
+ window: 30s
+
+networks:
+ network:
+ driver: overlay
diff --git a/package.json b/package.json
index 0a7767a..d569df2 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"scripts": {
"start": "webpack-dev-server --mode development",
"lint": "npx eslint --fix \"src/**/*.ts\" && npx prettier --write \"src/**/*.ts\"",
- "build": "webpack --mode production && find dist -type f -not -regex \"dist\\/.*\\.\\(html\\|png\\|ico\\|svg\\|jpg\\)\" | xargs rm && sed -i 's/^\\/\\/#.*.map//' dist/index.html"
+ "build": "rm -rf dist/* && webpack --mode production && find dist -type f -not -regex \"dist\\/.*\\.\\(html\\|png\\|ico\\|svg\\|jpg\\)\" | xargs rm && sed -i 's/^\\/\\/#.*.map//' dist/index.html"
},
"keywords": [],
"author": "AndrĂ¡s Schmelczer (https://schmelczer.dev/)",
@@ -20,19 +20,12 @@
"browserslist": [
"defaults"
],
- "sideEffects": [
- "*.scss",
- "*.png",
- "*.jpg",
- "*.ico",
- "*.svg"
- ],
- "optimization": {
- "usedExports": true
- },
+ "sideEffects": false,
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^3.10.1",
"@typescript-eslint/parser": "^3.10.1",
+ "@types/gl-matrix": "^2.4.5",
+ "gl-matrix": "^3.3.0",
"autoprefixer": "^9.8.6",
"css-loader": "^3.5.2",
"eslint": "^7.9.0",
diff --git a/src/helper/pretty-print.ts b/src/helper/pretty-print.ts
new file mode 100644
index 0000000..4b21da7
--- /dev/null
+++ b/src/helper/pretty-print.ts
@@ -0,0 +1,2 @@
+export const prettyPrint = (o: any): string =>
+ JSON.stringify(o, (_, v) => (v.toFixed ? Number(v.toFixed(2)) : v), ' ');
diff --git a/src/index.html b/src/index.html
index 8fced4c..b87696b 100644
--- a/src/index.html
+++ b/src/index.html
@@ -2,37 +2,23 @@
-
SDF-2D demo
-
-
+
+
-
+
+
+
+
+
-
-
-
diff --git a/src/index.ts b/src/index.ts
index 174c780..41012d1 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,15 +3,18 @@ import '../static/favicons/apple-touch-icon.png';
import '../static/favicons/favicon-16x16.png';
import '../static/favicons/favicon-32x32.png';
import '../static/favicons/favicon.ico';
-//import '../static/og-image.jpg';
+import '../static/logo-white.svg';
+import '../static/og-image.jpg';
import { DeltaTimeCalculator } from './helper/delta-time-calculator';
import { removeUnnecessaryOutlines } from './helper/remove-unnecessary-outlines';
-import { BlobScene } from './scenes/blob-scene';
+import { BlobScene } from './scenes/blob/blob-scene';
+import { RainScene } from './scenes/rain/rain-scene';
import { Scene } from './scenes/scene';
+import { TunnelScene } from './scenes/tunnel-scene';
import './styles/index.scss';
-const scenes = [/*TunnelScene, RainScene*/ BlobScene];
-const sceneIntervalInSeconds = 80000;
+const scenes = [TunnelScene, RainScene, BlobScene];
+const sceneIntervalInSeconds = 8;
glMatrix.setMatrixArrayType(Array);
removeUnnecessaryOutlines();
diff --git a/src/scenes/blob-scene.ts b/src/scenes/blob/blob-scene.ts
similarity index 51%
rename from src/scenes/blob-scene.ts
rename to src/scenes/blob/blob-scene.ts
index 03e4192..a0bca48 100644
--- a/src/scenes/blob-scene.ts
+++ b/src/scenes/blob/blob-scene.ts
@@ -1,6 +1,8 @@
-import { vec3 } from 'gl-matrix';
+import { vec2, vec3 } from 'gl-matrix';
import { Circle, CircleLight, compile, InvertedTunnel, Renderer } from 'sdf-2d';
-import { Scene } from './scene';
+import { prettyPrint } from '../../helper/pretty-print';
+import { Scene } from '../scene';
+import { Blob } from './blob';
export class BlobScene implements Scene {
private renderer: Renderer;
@@ -19,25 +21,33 @@ export class BlobScene implements Scene {
this.renderer = await compile(
canvas,
[
- {
- ...InvertedTunnel.descriptor,
- shaderCombinationSteps: [0, 1, 2, 4, 8, 12],
- },
Circle.descriptor,
{
...CircleLight.descriptor,
- shaderCombinationSteps: [1, 2, 3, 4, 5, 6, 7],
+ shaderCombinationSteps: [1, 2],
},
+ Blob.descriptor,
],
- [vec3.fromValues(0.4, 1, 0.6), vec3.fromValues(1, 1, 0), vec3.fromValues(0.3, 1, 1)]
+ [
+ vec3.fromValues(0, 0, 0),
+ vec3.fromValues(224 / 255, 96 / 255, 126 / 255),
+ vec3.fromValues(119 / 255, 173 / 255, 120 / 255),
+ ]
);
this.renderer.setRuntimeSettings({
ambientLight: vec3.fromValues(0.35, 0.1, 0.45),
- shadowLength: 550,
+ shadowLength: 800,
});
+
+ const { width, height } = this.canvas.getBoundingClientRect();
+ const length = vec2.length([width, height]);
+
+ this.blob = new Blob([width / 2, -length / 4 + length / 2]);
}
+ private blob: Blob;
+
private deltaSinceStart = 0;
public drawNextFrame(
currentTime: DOMHighResTimeStamp,
@@ -48,20 +58,28 @@ export class BlobScene implements Scene {
this.renderer.setViewArea([0, height], [width, height]);
this.renderer.autoscaleQuality(deltaTime);
- this.overlay.innerText = JSON.stringify(
- this.renderer.insights,
- (_, v) => (v.toFixed ? Number(v.toFixed(2)) : v),
- ' '
- );
+ this.overlay.innerText = prettyPrint(this.renderer.insights);
- const q = (this.deltaSinceStart % 5000) / 2500;
- console.log(q);
+ const length = vec2.length([width, height]);
+ const q = (this.deltaSinceStart % 8000) / 4000;
+ this.blob.animate(this.deltaSinceStart);
[
- new Circle([width / 2, -width / 4], width / 2),
- new CircleLight([q * width, Math.sin(q * Math.PI) * height], [1, 0.8, 0], 1),
+ new Circle([width / 2, -length / 4], length / 2),
+ this.blob,
new CircleLight(
- [(q - 1) * width, Math.sin((q - 1) * Math.PI) * height],
+ [
+ (Math.cos((1 - q) * Math.PI) * length) / 2 + width / 2,
+ (Math.sin((1 - q) * Math.PI) * length) / 2,
+ ],
+ [1, 0.8, 0],
+ 1
+ ),
+ new CircleLight(
+ [
+ (Math.cos(-q * Math.PI) * length) / 2 + width / 2,
+ (Math.sin(-q * Math.PI) * length) / 2,
+ ],
[0, 0.8, 1],
1
),
diff --git a/src/scenes/blob/blob.ts b/src/scenes/blob/blob.ts
new file mode 100644
index 0000000..50409bf
--- /dev/null
+++ b/src/scenes/blob/blob.ts
@@ -0,0 +1,139 @@
+import { mat2d, vec2 } from 'gl-matrix';
+import { Circle, Drawable, DrawableDescriptor } from 'sdf-2d';
+
+export class Blob extends Drawable {
+ public static descriptor: DrawableDescriptor = {
+ sdf: {
+ shader: `
+ uniform struct {
+ vec2 headCenter;
+ vec2 leftFootCenter;
+ vec2 rightFootCenter;
+ float headRadius;
+ float footRadius;
+ float k;
+ }[BLOB_COUNT] blobs;
+
+ float smoothMin(float a, float b)
+ {
+ const float k = 80.0;
+ float res = exp2( -k*a ) + exp2( -k*b );
+ return -log2( res )/k;
+ }
+
+ float circleDistance(vec2 circleCenter, float radius) {
+ return distance(position, circleCenter) - radius;
+ }
+
+ void blobMinDistance(inout float minDistance, inout float color) {
+ for (int i = 0; i < BLOB_COUNT; i++) {
+ float headDistance = circleDistance(blobs[i].headCenter, blobs[i].headRadius);
+ float leftFootDistance = circleDistance(blobs[i].leftFootCenter, blobs[i].footRadius);
+ float rightFootDistance = circleDistance(blobs[i].rightFootCenter, blobs[i].footRadius);
+
+ float res = min(
+ smoothMin(headDistance, leftFootDistance),
+ smoothMin(headDistance, rightFootDistance)
+ );
+
+ minDistance = min(minDistance, res);
+ color = mix(1.0, color, step(distanceNdcPixelSize + SURFACE_OFFSET, res));
+ }
+ }
+ `,
+ distanceFunctionName: 'blobMinDistance',
+ },
+ uniformName: 'blobs',
+ uniformCountMacroName: 'BLOB_COUNT',
+ shaderCombinationSteps: [1],
+ empty: new Blob(vec2.fromValues(0, 0)),
+ };
+
+ public readonly boundingCircleRadius = 200;
+
+ protected readonly headRadius = 70;
+ protected readonly footRadius = 30;
+
+ private readonly headOffset = vec2.fromValues(0, 120);
+ private leftFootOffset = vec2.fromValues(-30, this.footRadius);
+ private rightFootOffset = vec2.fromValues(30, this.footRadius);
+
+ private leftFootOffsetDefault = vec2.fromValues(-30, this.footRadius);
+ private rightFootOffsetDefault = vec2.fromValues(30, this.footRadius);
+
+ protected boundingCircle = new Circle(vec2.create(), this.boundingCircleRadius);
+ protected head = new Circle(vec2.create(), this.headRadius);
+ protected leftFoot = new Circle(vec2.create(), this.footRadius);
+ protected rightFoot = new Circle(vec2.create(), this.footRadius);
+
+ public constructor(private center: vec2) {
+ super();
+ this.position = center;
+ }
+
+ public set position(value: vec2) {
+ vec2.copy(this.boundingCircle.center, value);
+ vec2.add(this.head.center, value, this.headOffset);
+ vec2.add(this.leftFoot.center, value, this.leftFootOffset);
+ vec2.add(this.rightFoot.center, value, this.rightFootOffset);
+ }
+
+ public animate(time: number) {
+ const offset = 20;
+ const q = (time % 1000) / 500;
+
+ vec2.subtract(
+ this.rightFootOffset,
+ this.rightFootOffsetDefault,
+ vec2.fromValues(q * offset, 0)
+ );
+
+ vec2.subtract(
+ this.leftFootOffset,
+ this.leftFootOffsetDefault,
+ vec2.fromValues(q * offset, 0)
+ );
+
+ vec2.add(
+ this.rightFootOffset,
+ this.rightFootOffset,
+ vec2.fromValues(
+ Math.min(1, q) * offset * 2,
+ Math.sin(Math.min(1, q) * Math.PI) * 10
+ )
+ );
+
+ vec2.add(
+ this.leftFootOffset,
+ this.leftFootOffset,
+ vec2.fromValues(
+ (q > 1 ? q - 1 : 0) * offset * 2,
+ Math.sin((q > 1 ? q - 1 : 0) * Math.PI) * 10
+ )
+ );
+
+ this.position = this.center;
+ }
+
+ public minDistance(target: vec2): number {
+ return this.boundingCircle.minDistance(target);
+ }
+
+ protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any {
+ return {
+ headCenter: vec2.transformMat2d(vec2.create(), this.head.center, transform2d),
+ leftFootCenter: vec2.transformMat2d(
+ vec2.create(),
+ this.leftFoot.center,
+ transform2d
+ ),
+ rightFootCenter: vec2.transformMat2d(
+ vec2.create(),
+ this.rightFoot.center,
+ transform2d
+ ),
+ headRadius: this.headRadius * transform1d,
+ footRadius: this.footRadius * transform1d,
+ };
+ }
+}
diff --git a/src/scenes/rain/rain-scene.ts b/src/scenes/rain/rain-scene.ts
index 1b39475..f2e5307 100644
--- a/src/scenes/rain/rain-scene.ts
+++ b/src/scenes/rain/rain-scene.ts
@@ -1,5 +1,6 @@
import { vec2, vec3 } from 'gl-matrix';
import { CircleLight, compile, Renderer, Tunnel } from 'sdf-2d';
+import { prettyPrint } from '../../helper/pretty-print';
import { Scene } from '../scene';
import { Droplet } from './droplet';
@@ -7,7 +8,7 @@ export class RainScene implements Scene {
private droplets: Array = [];
private light1: CircleLight = new CircleLight(
vec2.create(),
- vec3.fromValues(1, 0, 1),
+ vec3.fromValues(0.5, 0, 1),
1
);
private light2: CircleLight = new CircleLight(
@@ -20,12 +21,6 @@ export class RainScene implements Scene {
private canvas: HTMLCanvasElement;
private overlay: HTMLDivElement;
- public constructor() {
- for (let i = 0; i < 40; i++) {
- this.droplets.push(new Droplet());
- }
- }
-
public async initialize(
canvas: HTMLCanvasElement,
overlay: HTMLDivElement
@@ -51,6 +46,10 @@ export class RainScene implements Scene {
ambientLight: vec3.fromValues(0.45, 0.25, 0.45),
tileMultiplier: 10,
});
+
+ for (let i = 0; i < (canvas.getBoundingClientRect().width / 1000) * 20; i++) {
+ this.droplets.push(new Droplet());
+ }
}
public drawNextFrame(
@@ -60,12 +59,7 @@ export class RainScene implements Scene {
const { width, height } = this.canvas.getBoundingClientRect();
this.renderer.setViewArea(vec2.fromValues(0, height), vec2.fromValues(width, height));
this.renderer.autoscaleQuality(deltaTime);
-
- this.overlay.innerText = JSON.stringify(
- this.renderer.insights,
- (_, v) => (v.toFixed ? Number(v.toFixed(2)) : v),
- ' '
- );
+ this.overlay.innerText = prettyPrint(this.renderer.insights);
const viewAreaSize = this.renderer.viewAreaSize;
diff --git a/src/scenes/tunnel-scene.ts b/src/scenes/tunnel-scene.ts
index a687384..488b160 100644
--- a/src/scenes/tunnel-scene.ts
+++ b/src/scenes/tunnel-scene.ts
@@ -2,6 +2,7 @@ import { vec2, vec3 } from 'gl-matrix';
import { CircleLight, compile, InvertedTunnel, Renderer } from 'sdf-2d';
import { clamp } from '../helper/clamp';
import { last } from '../helper/last';
+import { prettyPrint } from '../helper/pretty-print';
import { Random } from '../helper/random';
import { Scene } from './scene';
@@ -101,11 +102,7 @@ export class TunnelScene implements Scene {
);
this.renderer.autoscaleQuality(deltaTime);
- this.overlay.innerText = JSON.stringify(
- this.renderer.insights,
- (_, v) => (v.toFixed ? Number(v.toFixed(2)) : v),
- ' '
- );
+ this.overlay.innerText = prettyPrint(this.renderer.insights);
[
...this.tunnels.filter(
diff --git a/static/logo-white.svg b/static/logo-white.svg
index 3569c14..bb36e19 100644
--- a/static/logo-white.svg
+++ b/static/logo-white.svg
@@ -2,18 +2,18 @@
-
+
-
+
-
+
-
+
@@ -33,7 +33,7 @@
attributeName="transform"
type="rotate"
values="0;90"
- dur="8s"
+ dur="10s"
repeatCount="indefinite"
/>
diff --git a/static/og-image.jpg b/static/og-image.jpg
new file mode 100644
index 0000000..93c8001
Binary files /dev/null and b/static/og-image.jpg differ
diff --git a/tsconfig.json b/tsconfig.json
index 13fb34c..37fe606 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,12 +3,12 @@
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": false,
- "target": "es5",
+ "target": "es6",
"downlevelIteration": true,
"allowJs": true,
"experimentalDecorators": true,
"moduleResolution": "Node",
"module": "commonjs",
- "lib": ["es2015", "dom"]
+ "lib": ["es2017", "dom"]
}
}
diff --git a/webpack.config.js b/webpack.config.js
index 1cbb137..8af85f7 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -6,9 +6,6 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
const Sass = require('sass');
-const isProduction = process.env.NODE_ENV == 'production';
-const isDevelopment = !isProduction;
-
const PATHS = {
entryPoint: path.resolve(__dirname, 'src/index.ts'),
bundles: path.resolve(__dirname, 'dist'),
@@ -23,7 +20,7 @@ module.exports = {
filename: '[name].[contenthash].js',
path: PATHS.bundles,
},
- devtool: isDevelopment ? 'source-map' : 'null',
+ devtool: 'source-map',
watchOptions: {
aggregateTimeout: 600,
ignored: /node_modules/,
@@ -34,10 +31,11 @@ module.exports = {
},
optimization: {
minimize: true,
+ usedExports: true,
minimizer: [
new TerserJSPlugin({
sourceMap: true,
- test: /\.js$/i,
+ test: /\.js$/,
}),
new OptimizeCSSAssetsPlugin({}),
],
@@ -65,7 +63,7 @@ module.exports = {
module: {
rules: [
{
- test: /\.scss$/i,
+ test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
@@ -93,15 +91,7 @@ module.exports = {
exclude: /node_modules/,
},
{
- test: /\.svg$/,
- loader: 'svg-url-loader',
- options: {
- limit: 10 * 1024,
- noquotes: true,
- },
- },
- {
- test: /\.(ico|png|jpg)$/i,
+ test: /\.(ico|png|jpg)$/,
use: {
loader: 'file-loader',
query: {
@@ -110,6 +100,16 @@ module.exports = {
},
},
},
+ {
+ test: /\.(svg)$/,
+ use: {
+ loader: 'file-loader',
+ query: {
+ outputPath: '/static',
+ name: '[name].[ext]',
+ },
+ },
+ },
{
test: /\.js$/,
enforce: 'pre',