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 @@ -logo +logo # 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',