From 36f01c67169e8cb77866920674601211f4ae91ca Mon Sep 17 00:00:00 2001 From: schmelczerandras Date: Wed, 16 Sep 2020 16:03:01 +0200 Subject: [PATCH] Copy files --- .eslintignore | 1 + .eslintrc.json | 28 ++++++ .gitattributes | 1 + .gitignore | 4 + .prettierrc | 7 ++ package.json | 65 ++++++++++++++ src/helper/clamp.ts | 4 + src/helper/delta-time-calculator.ts | 23 +++++ src/helper/last.ts | 3 + src/helper/mix.ts | 1 + src/helper/random.ts | 18 ++++ src/helper/to-percent.ts | 1 + src/helper/wait.ts | 3 + src/index.html | 14 +++ src/index.scss | 35 ++++++++ src/index.ts | 76 +++++++++++++++++ src/scenes/rain-scene.ts | 62 ++++++++++++++ src/scenes/scene.ts | 10 +++ static/declared.png | Bin 0 -> 26027 bytes static/declared.psd | 3 + tsconfig.json | 14 +++ webpack.config.js | 127 ++++++++++++++++++++++++++++ 22 files changed, 500 insertions(+) create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 package.json create mode 100644 src/helper/clamp.ts create mode 100644 src/helper/delta-time-calculator.ts create mode 100644 src/helper/last.ts create mode 100644 src/helper/mix.ts create mode 100644 src/helper/random.ts create mode 100644 src/helper/to-percent.ts create mode 100644 src/helper/wait.ts create mode 100644 src/index.html create mode 100644 src/index.scss create mode 100644 src/index.ts create mode 100644 src/scenes/rain-scene.ts create mode 100644 src/scenes/scene.ts create mode 100644 static/declared.png create mode 100644 static/declared.psd create mode 100644 tsconfig.json create mode 100644 webpack.config.js diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..2cb7d2a --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +**/*.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..c259ab0 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "root": true, + "env": { + "browser": true, + "es2020": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "prettier", + "prettier/@typescript-eslint" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 11, + "sourceType": "module" + }, + "plugins": ["unused-imports", "@typescript-eslint", "prettier"], + "rules": { + "prettier/prettier": "error", + "no-unused-vars": "off", + "unused-imports/no-unused-imports-ts": "error", + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/explicit-module-boundary-types": "off" + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ea544b2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.psd filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a9d836 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist +node_modules +package-lock.json +.vscode diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5aae580 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "trailingComma": "es5", + "printWidth": 90, + "tabWidth": 2, + "singleQuote": true, + "endOfLine": "lf" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3141753 --- /dev/null +++ b/package.json @@ -0,0 +1,65 @@ +{ + "name": "decla.red", + "version": "0.0.0", + "description": "![logo](media/declared.png)", + "private": true, + "main": "index.js", + "scripts": { + "start": "webpack-dev-server --mode development", + "lint": "npx eslint --fix \"src/**/*.ts\" && npx prettier --write \"src/**/*.ts\"", + "build": "webpack && find dist -type f -not -name '*.html' | xargs rm" + }, + "keywords": [], + "author": "AndrĂ¡s Schmelczer", + "postcss": { + "plugins": { + "autoprefixer": {} + } + }, + "browserslist": [ + "defaults" + ], + "sideEffects": [ + "*.scss" + ], + "optimization": { + "usedExports": true + }, + "devDependencies": { + "@types/gl-matrix": "^2.4.5", + "@typescript-eslint/eslint-plugin": "^3.10.1", + "@typescript-eslint/parser": "^3.10.1", + "autoprefixer": "^9.8.6", + "clean-webpack-plugin": "^3.0.0", + "css-loader": "^3.5.2", + "cssnano": "^4.1.10", + "eslint": "^7.9.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-prettier": "^3.1.4", + "eslint-plugin-unused-imports": "^0.1.3", + "gl-matrix": "^3.3.0", + "html-webpack-inline-source-plugin": "0.0.10", + "html-webpack-plugin": "^3.2.0", + "image-webpack-loader": "^6.0.0", + "mini-css-extract-plugin": "^0.9.0", + "optimize-css-assets-webpack-plugin": "^5.0.4", + "postcss-loader": "^3.0.0", + "prettier": "^2.1.2", + "raw-loader": "^4.0.1", + "resolve-url-loader": "^3.1.1", + "responsive-loader": "^1.2.0", + "sass": "^1.26.3", + "sass-loader": "^9.0.3", + "sdf-2d": "^0.0.0", + "sharp": "^0.25.4", + "style-loader": "^1.1.4", + "svg-url-loader": "^6.0.0", + "terser-webpack-plugin": "^2.3.8", + "ts-loader": "^8.0.3", + "typescript": "^3.9.7", + "webpack": "^4.44.1", + "webpack-cli": "^3.3.11", + "webpack-dev-server": "^3.10.3" + } +} diff --git a/src/helper/clamp.ts b/src/helper/clamp.ts new file mode 100644 index 0000000..45da555 --- /dev/null +++ b/src/helper/clamp.ts @@ -0,0 +1,4 @@ +export const clamp = (value: number, min: number, max: number): number => + Math.min(max, Math.max(min, value)); + +export const clamp01 = (value: number): number => Math.min(1, Math.max(0, value)); diff --git a/src/helper/delta-time-calculator.ts b/src/helper/delta-time-calculator.ts new file mode 100644 index 0000000..fb7686d --- /dev/null +++ b/src/helper/delta-time-calculator.ts @@ -0,0 +1,23 @@ +export class DeltaTimeCalculator { + private previousTime: DOMHighResTimeStamp | null = null; + + constructor() { + document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); + } + + public getNextDeltaTime(currentTime: DOMHighResTimeStamp): DOMHighResTimeStamp { + if (this.previousTime === null) { + this.previousTime = currentTime; + } + + const delta = currentTime - this.previousTime; + this.previousTime = currentTime; + return delta; + } + + private handleVisibilityChange() { + if (!document.hidden) { + this.previousTime = null; + } + } +} diff --git a/src/helper/last.ts b/src/helper/last.ts new file mode 100644 index 0000000..5c319b0 --- /dev/null +++ b/src/helper/last.ts @@ -0,0 +1,3 @@ +export function last(a: Array): T | null { + return a.length > 0 ? a[a.length - 1] : null; +} diff --git a/src/helper/mix.ts b/src/helper/mix.ts new file mode 100644 index 0000000..16a76ed --- /dev/null +++ b/src/helper/mix.ts @@ -0,0 +1 @@ +export const mix = (from: number, to: number, q: number) => from + (to - from) * q; diff --git a/src/helper/random.ts b/src/helper/random.ts new file mode 100644 index 0000000..6a59c67 --- /dev/null +++ b/src/helper/random.ts @@ -0,0 +1,18 @@ +// source: +// https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript +// Mulberry32 + +export abstract class Random { + private static _seed = 42; + + public static set seed(value: number) { + Random._seed = value; + } + + public static getRandom(): number { + let t = (Random._seed += 0x6d2b79f5); + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + } +} diff --git a/src/helper/to-percent.ts b/src/helper/to-percent.ts new file mode 100644 index 0000000..0047591 --- /dev/null +++ b/src/helper/to-percent.ts @@ -0,0 +1 @@ +export const toPercent = (value: number) => `${Math.round(value * 100)}%`; diff --git a/src/helper/wait.ts b/src/helper/wait.ts new file mode 100644 index 0000000..73dd2c2 --- /dev/null +++ b/src/helper/wait.ts @@ -0,0 +1,3 @@ +export const wait = (ms: number): Promise => { + return new Promise((resolve, _) => setTimeout(resolve, ms)); +}; diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..ad4e325 --- /dev/null +++ b/src/index.html @@ -0,0 +1,14 @@ + + + + + Test + + + +
+ + + diff --git a/src/index.scss b/src/index.scss new file mode 100644 index 0000000..5105afb --- /dev/null +++ b/src/index.scss @@ -0,0 +1,35 @@ +html, +body, +canvas#main { + height: 100%; + width: 100%; +} + +body { + position: relative; + margin: 0; + + #overlay { + font-family: Helvetica, Arial, sans-serif; + font-size: 0.6em; + margin: 0.75em 1em; + position: absolute; + right: 0; + + $outline-width: 0.5px; + text-shadow: -$outline-width -$outline-width 0 #000, + $outline-width -$outline-width 0 #000, -$outline-width $outline-width 0 #000, + $outline-width $outline-width 0 #000; + + color: white; + white-space: pre; + + @media (max-width: 800px) { + font-size: 0.55em; + } + } + + canvas#main { + background-color: hotpink; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..aaeb6db --- /dev/null +++ b/src/index.ts @@ -0,0 +1,76 @@ +import { glMatrix, vec2, vec3 } from 'gl-matrix'; +import { compile, Renderer } from 'sdf-2d'; +import { DeltaTimeCalculator } from './helper/delta-time-calculator'; +import './index.scss'; +import { RainScene } from './scenes/rain-scene'; +import { Scene } from './scenes/scene'; + +glMatrix.setMatrixArrayType(Array); + +const deltaTimeCalculator = new DeltaTimeCalculator(); +const canvas = document.querySelector('canvas') as HTMLCanvasElement; +const overlay = document.querySelector('#overlay') as HTMLDivElement; + +const drawFrame = ( + renderer: Renderer, + scene: Scene, + deltaTime: DOMHighResTimeStamp, + currentTime: DOMHighResTimeStamp +) => { + const { width, height } = canvas.getBoundingClientRect(); + renderer.setViewArea(vec2.fromValues(0, height), vec2.fromValues(width, height)); + renderer.autoscaleQuality(deltaTime); + + overlay.innerText = JSON.stringify( + renderer.insights, + (_, v) => (v.toFixed ? Number(v.toFixed(2)) : v), + ' ' + ); + + scene.animate(currentTime, { width, height }); + + scene.drawables.forEach((d) => renderer.addDrawable(d)); + renderer.renderDrawables(); +}; + +const handleScene = async (scene: Scene) => { + const renderer = compile( + canvas, + scene.descriptors, + [vec3.fromValues(0.4, 0.35, 0.6), vec3.fromValues(0, 1, 0), vec3.fromValues(1, 1, 0)], + { tileMultiplier: 8, enableStopwatch: false } + ); + + let triggerIsOver: () => void; + const isOver = new Promise((resolve) => (triggerIsOver = resolve)); + + let timeSinceStart = 0; + + const handleFrame = (currentTime: DOMHighResTimeStamp) => { + const deltaTime = deltaTimeCalculator.getNextDeltaTime(currentTime); + + drawFrame(renderer, scene, deltaTime, currentTime); + + if ((timeSinceStart += deltaTime) > 200000 * 1000) { + triggerIsOver(); + } else { + requestAnimationFrame(handleFrame); + } + }; + + requestAnimationFrame(handleFrame); + await isOver; + + renderer.destroy(); +}; + +const scenes = [new RainScene()]; + +const main = async () => { + let i = 0; + for (;;) { + await handleScene(scenes[i++ % scenes.length]); + } +}; + +main(); diff --git a/src/scenes/rain-scene.ts b/src/scenes/rain-scene.ts new file mode 100644 index 0000000..616abd0 --- /dev/null +++ b/src/scenes/rain-scene.ts @@ -0,0 +1,62 @@ +import { vec2 } from 'gl-matrix'; +import { CircleLight, Drawable, DrawableDescriptor, Flashlight, Tunnel } from 'sdf-2d'; +import { Random } from '../helper/random'; +import { Scene } from './scene'; + +export class RainScene implements Scene { + public droplets: Array = []; + + public constructor() { + for (let i = 0; i < 100; i++) { + this.droplets.push(new Droplet()); + } + } + + public get descriptors(): Array { + return [Tunnel.descriptor, CircleLight.descriptor, Flashlight.descriptor]; + } + + public animate( + currentTime: number, + viewAreaSize: { width: number; height: number } + ): void { + this.droplets.forEach((d) => d.animate(currentTime, viewAreaSize)); + } + + public get drawables(): Array { + return this.droplets.map((d) => d.drawable); + } +} + +class Droplet { + public readonly drawable: Tunnel; + + private speed = Random.getRandom() * 0.2 + 0.1; + private position = vec2.fromValues(Random.getRandom(), Random.getRandom()); + private length = Random.getRandom() * 30 + 8; + + constructor() { + const size = Random.getRandom() * 10 + 5; + + this.drawable = new Tunnel( + vec2.create(), + vec2.create(), + size + Random.getRandom() * 5 + 5, + size + ); + } + + public animate( + currentTime: number, + { width, height }: { width: number; height: number } + ) { + const heightOffset = 300; + vec2.set( + this.drawable.from, + this.position[0] * width, + height - ((this.speed * currentTime) % (height + heightOffset)) + ); + + vec2.add(this.drawable.to, this.drawable.from, vec2.fromValues(0, this.length)); + } +} diff --git a/src/scenes/scene.ts b/src/scenes/scene.ts new file mode 100644 index 0000000..6c5d2d5 --- /dev/null +++ b/src/scenes/scene.ts @@ -0,0 +1,10 @@ +import { Drawable, DrawableDescriptor } from 'sdf-2d'; + +export interface Scene { + readonly descriptors: Array; + animate( + currentTime: DOMHighResTimeStamp, + viewAreaSize: { width: number; height: number } + ): void; + readonly drawables: Array; +} diff --git a/static/declared.png b/static/declared.png new file mode 100644 index 0000000000000000000000000000000000000000..897278d92a5e349f0a101b77ac6804fdeca52a22 GIT binary patch literal 26027 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 literal 0 HcmV?d00001 diff --git a/static/declared.psd b/static/declared.psd new file mode 100644 index 0000000..e2f814d --- /dev/null +++ b/static/declared.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03ed783e268bc05d40997239f6118767c5d5d8b15d23fa7b329321c0a1529c46 +size 437314 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..13fb34c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "sourceMap": true, + "noImplicitAny": false, + "target": "es5", + "downlevelIteration": true, + "allowJs": true, + "experimentalDecorators": true, + "moduleResolution": "Node", + "module": "commonjs", + "lib": ["es2015", "dom"] + } +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..f91ecb7 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,127 @@ +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const TerserJSPlugin = require('terser-webpack-plugin'); +const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); +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; + +module.exports = { + watchOptions: { + ignored: /node_modules/, + }, + devtool: 'inline-source-map', + devServer: { + host: '0.0.0.0', + disableHostCheck: true, + }, + optimization: { + minimize: true, + minimizer: [ + new TerserJSPlugin({ + sourceMap: isDevelopment, + cache: true, + test: /\.ts$/i, + terserOptions: { + ecma: 5, + warnings: true, + parse: {}, + compress: { defaults: true }, + mangle: true, + module: false, + output: null, + toplevel: true, + nameCache: null, + ie8: false, + keep_classnames: false, + keep_fnames: false, + safari10: false, + }, + }), + new OptimizeCSSAssetsPlugin({}), + ], + }, + plugins: [ + new CleanWebpackPlugin(), + new HtmlWebpackPlugin({ + xhtml: true, + template: './src/index.html', + minify: { + collapseWhitespace: true, + removeComments: true, + removeRedundantAttributes: true, + removeScriptTypeAttributes: true, + removeStyleLinkTypeAttributes: true, + useShortDoctype: true, + }, + inlineSource: '.(js|css)$', + }), + new HtmlWebpackInlineSourcePlugin(), + new MiniCssExtractPlugin({ + filename: '[name].[contenthash].css', + chunkFilename: '[id].[contenthash].css', + }), + ], + entry: { + index: './src/index.ts', + }, + module: { + rules: [ + { + test: /\.(glsl)$/, + use: { + loader: 'raw-loader', + }, + }, + { + test: /\.ico$/i, + use: { + loader: 'file-loader', + query: { + outputPath: '/', + name: '[name].[ext]', + }, + }, + }, + { + test: /\.scss$/i, + use: [ + MiniCssExtractPlugin.loader, + 'css-loader', + 'postcss-loader', + { + loader: 'resolve-url-loader', + options: { + keepQuery: true, + }, + }, + { + loader: 'sass-loader', + options: { + sourceMap: true, + implementation: Sass, + }, + }, + ], + }, + { + test: /\.ts$/, + use: { + loader: 'ts-loader', + }, + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: ['.ts', '.js', '.glsl'], + }, + output: { + filename: '[name].[contenthash].js', + path: path.resolve(__dirname, 'dist'), + }, +};