├── examples ├── with-node │ ├── .npmignore │ ├── package.json │ ├── index.html │ ├── src │ │ ├── index.js │ │ └── index.ts │ ├── style.css │ ├── package-lock.json │ └── tsconfig.json └── with-browser │ ├── demo.js │ └── index.html ├── assets ├── img │ ├── demo.png │ ├── raindrop.png │ ├── 84765992_p0.jpg │ └── 87747832_p0.jpg └── css │ └── style.css ├── .gitmodules ├── bundle └── index.d.ts ├── .npmignore ├── dist ├── utils.d.ts ├── utils.d.ts.map ├── spawner.d.ts.map ├── blur.d.ts.map ├── index.d.ts.map ├── random.d.ts.map ├── spawner.d.ts ├── random.d.ts ├── index.d.ts ├── blur.d.ts ├── raindrop.d.ts.map ├── raindrop.d.ts ├── simulator.d.ts.map ├── renderer.d.ts.map ├── simulator.d.ts └── renderer.d.ts ├── src ├── utils.ts ├── shader │ ├── droplet.glsl │ ├── 2d-frag.glsl │ ├── raindrop-frag.glsl │ ├── bg-mist.glsl │ ├── erase.glsl │ ├── 2d-vert.glsl │ ├── blur.glsl │ ├── raindrop-vert.glsl │ ├── droplet-vert.glsl │ └── compose.glsl ├── loader.d.ts ├── spawner.ts ├── random.ts ├── blur.ts ├── index.ts ├── raindrop.ts ├── simulator.ts └── renderer.ts ├── .github └── workflows │ ├── build.yml │ └── npm-publish.yml ├── .vscode └── launch.json ├── package.json ├── LICENSE ├── index.html ├── .gitignore ├── tsconfig.json └── README.md /examples/with-node/.npmignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /assets/img/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SardineFish/raindrop-fx/HEAD/assets/img/demo.png -------------------------------------------------------------------------------- /assets/img/raindrop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SardineFish/raindrop-fx/HEAD/assets/img/raindrop.png -------------------------------------------------------------------------------- /assets/img/84765992_p0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SardineFish/raindrop-fx/HEAD/assets/img/84765992_p0.jpg -------------------------------------------------------------------------------- /assets/img/87747832_p0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SardineFish/raindrop-fx/HEAD/assets/img/87747832_p0.jpg -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "zogra-renderer"] 2 | path = zogra-renderer 3 | url = https://github.com/SardineFish/zogra-renderer.git 4 | -------------------------------------------------------------------------------- /bundle/index.d.ts: -------------------------------------------------------------------------------- 1 | import RaindropFXClass from "../dist"; 2 | 3 | declare global 4 | { 5 | const RaindropFX: typeof RaindropFXClass; 6 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | node_modules 3 | /zogra-renderer 4 | /assets/img/84765992_p0.jpg 5 | /assets/img/87747832_p0.jpg 6 | /assets/img/demo.png -------------------------------------------------------------------------------- /dist/utils.d.ts: -------------------------------------------------------------------------------- 1 | export interface Time { 2 | dt: number; 3 | total: number; 4 | } 5 | export declare function clamp(x: number, l: number, h: number): number; 6 | export declare function lerp(a: number, b: number, t: number): number; 7 | //# sourceMappingURL=utils.d.ts.map -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export interface Time 2 | { 3 | dt: number, 4 | total: number, 5 | } 6 | 7 | export function clamp(x: number, l: number, h: number) 8 | { 9 | return Math.min(Math.max(x, l), h); 10 | } 11 | 12 | export function lerp(a: number, b: number, t: number) 13 | { 14 | return a + (b - a) * t; 15 | } -------------------------------------------------------------------------------- /dist/utils.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,IAAI;IAEjB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,UAGpD;AAED,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,UAGnD"} -------------------------------------------------------------------------------- /src/shader/droplet.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision mediump float; 3 | 4 | in vec2 vUV; 5 | 6 | uniform sampler2D uMainTex; 7 | uniform vec4 uColor; 8 | 9 | out vec4 fragColor; 10 | 11 | void main() 12 | { 13 | vec4 color = texture(uMainTex, vUV.xy).rgba; 14 | color.rgb *= color.a; 15 | fragColor = vec4(color.rg, 0.0, color.a); 16 | } -------------------------------------------------------------------------------- /src/shader/2d-frag.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision mediump float; 3 | 4 | in vec4 vColor; 5 | in vec4 vPos; 6 | in vec2 vUV; 7 | 8 | uniform sampler2D uMainTex; 9 | uniform vec4 uColor; 10 | 11 | out vec4 fragColor; 12 | 13 | void main() 14 | { 15 | vec4 color = texture(uMainTex, vUV.xy).rgba; 16 | color.rgba = color * uColor; 17 | fragColor = color.rgba; 18 | } -------------------------------------------------------------------------------- /src/loader.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' { 2 | const contents: Uint8Array 3 | export = contents 4 | } 5 | declare module '*.jpg' { 6 | const contents: Uint8Array 7 | export = contents 8 | } 9 | declare module '*.fbx' { 10 | const contents: string 11 | export = contents 12 | } 13 | declare module '*.glsl' { 14 | const contents: string 15 | export = contents 16 | } 17 | -------------------------------------------------------------------------------- /src/shader/raindrop-frag.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision mediump float; 3 | 4 | in vec4 vColor; 5 | in vec4 vPos; 6 | in vec2 vUV; 7 | in float vSize; 8 | 9 | uniform sampler2D uMainTex; 10 | uniform float uSize; 11 | 12 | out vec4 fragColor; 13 | 14 | void main() 15 | { 16 | vec4 color = texture(uMainTex, vUV.xy).rgba; 17 | 18 | fragColor = vec4(color.rg * color.a, vSize * color.a, color.a); 19 | } -------------------------------------------------------------------------------- /src/shader/bg-mist.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision mediump float; 3 | 4 | in vec2 vUV; 5 | 6 | uniform sampler2D uMainTex; 7 | uniform sampler2D uMistTex; 8 | uniform vec4 uMistColor; 9 | 10 | out vec4 fragColor; 11 | 12 | void main() 13 | { 14 | vec4 color = texture(uMainTex, vUV.xy).rgba; 15 | color.rgb += vec3(uMistColor); 16 | color.a = texture(uMistTex, vUV.xy).r * uMistColor.a; 17 | fragColor = color.rgba; 18 | } -------------------------------------------------------------------------------- /src/shader/erase.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision mediump float; 3 | 4 | in vec4 vColor; 5 | in vec4 vPos; 6 | in vec2 vUV; 7 | 8 | uniform sampler2D uMainTex; 9 | uniform vec4 uColor; 10 | uniform vec2 uEraserSmooth; 11 | 12 | out vec4 fragColor; 13 | 14 | void main() 15 | { 16 | vec4 color = texture(uMainTex, vUV.xy).rgba; 17 | color.a = smoothstep(uEraserSmooth.x, uEraserSmooth.y, color.a); 18 | fragColor = color.rgba; 19 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: '15.x' 16 | registry-url: 'https://registry.npmjs.org' 17 | - run: npm install 18 | - run: npm test -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Attach to Chrome", 9 | "port": 9222, 10 | "request": "attach", 11 | "type": "pwa-chrome", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | 15 | ] 16 | } -------------------------------------------------------------------------------- /examples/with-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raindrop-fx-example", 3 | "version": "1.0.0", 4 | "description": "Example of raindrop-fx using with nodejs.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "npx esbuild --bundle --outdir=./dist --platform=browser ./src/index.js", 8 | "test": "npm run build && npx tsc" 9 | }, 10 | "author": "SardineFish", 11 | "license": "MIT", 12 | "dependencies": { 13 | "raindrop-fx": "file:///../.." 14 | }, 15 | "devDependencies": { 16 | "esbuild": "^0.9.5" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/with-node/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Raindrop Effect 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/with-node/src/index.js: -------------------------------------------------------------------------------- 1 | const RaindropFX = require("raindrop-fx"); 2 | 3 | const canvas = document.querySelector("#canvas"); 4 | const rect = canvas.getBoundingClientRect(); 5 | canvas.width = rect.width; 6 | canvas.height = rect.height; 7 | 8 | const raindropFx = new RaindropFX({ 9 | canvas: canvas, 10 | background: "../../assets/img/84765992_p0.jpg", 11 | }); 12 | 13 | window.onresize = () => 14 | { 15 | const rect = canvas.getBoundingClientRect(); 16 | raindropFx.resize(rect.width, rect.height); 17 | } 18 | 19 | raindropFx.start(); -------------------------------------------------------------------------------- /examples/with-browser/demo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("../../bundle/index")} 3 | */ 4 | 5 | const canvas = document.querySelector("#canvas"); 6 | const rect = canvas.getBoundingClientRect(); 7 | canvas.width = rect.width; 8 | canvas.height = rect.height; 9 | 10 | const raindropFx = new RaindropFX({ 11 | canvas: canvas, 12 | background: "../../assets/img/84765992_p0.jpg", 13 | }); 14 | 15 | window.onresize = () => 16 | { 17 | const rect = canvas.getBoundingClientRect(); 18 | raindropFx.resize(rect.width, rect.height); 19 | } 20 | 21 | raindropFx.start(); -------------------------------------------------------------------------------- /examples/with-node/src/index.ts: -------------------------------------------------------------------------------- 1 | import RaindropFX from "raindrop-fx"; 2 | 3 | const canvas = document.querySelector("#canvas") as HTMLCanvasElement; 4 | const rect = canvas.getBoundingClientRect(); 5 | canvas.width = rect.width; 6 | canvas.height = rect.height; 7 | 8 | const raindropFx = new RaindropFX({ 9 | canvas: canvas, 10 | background: "../../assets/img/84765992_p0.jpg", 11 | }); 12 | 13 | window.onresize = () => 14 | { 15 | const rect = canvas.getBoundingClientRect(); 16 | raindropFx.resize(rect.width, rect.height); 17 | } 18 | 19 | raindropFx.start(); -------------------------------------------------------------------------------- /dist/spawner.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"spawner.d.ts","sourceRoot":"","sources":["../src/spawner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAElE,qBAAa,OAAO;IAIhB,WAAW,SAAK;IAChB,SAAS,SAAK;IAEd,OAAO,CAAC,SAAS,CAAoB;gBAEzB,SAAS,EAAE,iBAAiB,EAAE,OAAO,EAAE,gBAAgB;IAMnE,IAAI,QAAQ,qBAAkD;IAC9D,IAAI,IAAI,qBAA8C;IACtD,IAAI,SAAS,SAA6C;IAE1D,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAKvB,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC;IAe/B,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,SAAI;CAI7C"} -------------------------------------------------------------------------------- /dist/blur.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"blur.d.ts","sourceRoot":"","sources":["../src/blur.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4C,aAAa,EAAsB,OAAO,EAAoC,IAAI,EAAY,aAAa,EAAiB,MAAM,gBAAgB,CAAC;;AAItM,cAAM,YAAa,SAAQ,iBAA0C;IAGjE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAQ;IAG/B,WAAW,EAAE,IAAI,CAAc;IAG/B,YAAY,EAAE,MAAM,CAAK;CAC5B;AAED,qBAAa,YAAY;IAErB,QAAQ,EAAE,aAAa,CAAC;IACxB,KAAK,EAAE,aAAa,EAAE,CAAM;IAC5B,YAAY,eAAsB;gBAEtB,QAAQ,EAAE,aAAa;IAKnC,IAAI,CAAC,OAAO,EAAE,OAAO;IAYrB,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,GAAE,MAAU,EAAE,MAAM,gBAAgB;IAcpE,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM;IAwB5C,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,gBAAgB;CAuB1D"} -------------------------------------------------------------------------------- /examples/with-node/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | 4 | background-position: 0px 0px, 5 | 16px 16px; 6 | background-size: 32px 32px; 7 | background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee 100%), 8 | linear-gradient(45deg, #eee 25%, white 25%, white 75%, #eee 75%, #eee 100%); 9 | } 10 | 11 | #root { 12 | position: absolute; 13 | width: 100vw; 14 | height: 100vh; 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | #canvas { 21 | width: 100%; 22 | height: 100%; 23 | } -------------------------------------------------------------------------------- /examples/with-browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Raindrop Effect 8 | 9 | 10 | 11 | 12 |
13 | 14 |

Loading...

15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /dist/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,WAAW,EAAQ,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGlE,UAAU,OAAQ,SAAQ,gBAAgB,EAAE,aAAa;CAExD;AAED,cAAM,UAAU;IAEL,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,SAAS,EAAE,iBAAiB,CAAC;IAEpC,OAAO,CAAC,UAAU,CAAK;gBAEX,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG;QAAC,MAAM,EAAE,iBAAiB,CAAA;KAAC;IAsD7D,KAAK;IAsBX,IAAI;IAKJ,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAQ9B,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,WAAW;IAMpD,OAAO;IAMP,OAAO,CAAC,MAAM;CAOjB;AAED,SAAS,UAAU,CAAC"} -------------------------------------------------------------------------------- /dist/random.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"random.d.ts","sourceRoot":"","sources":["../src/random.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,IAAI,EAAE,IAAI,EAAiB,MAAM,gBAAgB,CAAC;AAE1E,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAEpE,IAAI,EAAE,CAAC,CAAC;IACR,MAAM,EAAE,CAAC,CAAC;CACb;AAED,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAYlF;AACD,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAG7C;AAQD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,UAG/C;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAIhD;AAED;;GAEG;AACH,wBAAgB,MAAM,WAGrB;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,UAGnD"} -------------------------------------------------------------------------------- /dist/spawner.d.ts: -------------------------------------------------------------------------------- 1 | import { Rect, vec2 } from "zogra-renderer"; 2 | import { RainDrop } from "./raindrop"; 3 | import { RaindropSimulator, SimulatorOptions } from "./simulator"; 4 | export declare class Spawner { 5 | currentTime: number; 6 | nextSpawn: number; 7 | private simulator; 8 | constructor(simulator: RaindropSimulator, options: SimulatorOptions); 9 | get interval(): [number, number]; 10 | get size(): [number, number]; 11 | get spawnRect(): Rect; 12 | update(dt: number): this; 13 | trySpawn(): Iterable; 14 | spawn(pos: vec2, size: number, density?: number): RainDrop; 15 | } 16 | //# sourceMappingURL=spawner.d.ts.map -------------------------------------------------------------------------------- /src/shader/2d-vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision mediump float; 3 | 4 | in vec3 aPos; 5 | in vec4 aColor; 6 | in vec2 aUV; 7 | in vec3 aNormal; 8 | 9 | uniform mat4 uTransformM; 10 | uniform mat4 uTransformVP; 11 | uniform mat4 uTransformMVP; 12 | uniform mat4 uTransformM_IT; 13 | 14 | out vec4 vColor; 15 | out vec4 vPos; 16 | out vec2 vUV; 17 | out vec3 vNormal; 18 | out vec3 vWorldPos; 19 | 20 | void main() 21 | { 22 | gl_Position = uTransformMVP * vec4(aPos, 1); 23 | vPos = gl_Position; 24 | vColor = aColor; 25 | vUV = aUV; 26 | vNormal = (uTransformM_IT * vec4(aNormal, 0)).xyz; 27 | vWorldPos = (uTransformM * vec4(aPos, 1)).xyz; 28 | 29 | } -------------------------------------------------------------------------------- /src/shader/blur.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision mediump float; 3 | 4 | in vec4 vColor; 5 | in vec4 vPos; 6 | in vec2 vUV; 7 | 8 | uniform sampler2D uMainTex; 9 | uniform vec4 uTexSize; // (w, h, 1/w, 1/h) 10 | uniform float uSampleOffset; 11 | 12 | out vec4 fragColor; 13 | 14 | void main() 15 | { 16 | vec2 delta = vec2(-uSampleOffset, uSampleOffset); 17 | vec4 color = 18 | texture(uMainTex, clamp(vUV.xy + uTexSize.zw * delta.xx, vec2(0), vec2(1))) 19 | + texture(uMainTex, clamp(vUV.xy + uTexSize.zw * delta.yx, vec2(0), vec2(1))) 20 | + texture(uMainTex, clamp(vUV.xy + uTexSize.zw * delta.yy, vec2(0), vec2(1))) 21 | + texture(uMainTex, clamp(vUV.xy + uTexSize.zw * delta.xy, vec2(0), vec2(1))); 22 | 23 | color /= vec4(4.0); 24 | 25 | fragColor = color.rgba; 26 | } -------------------------------------------------------------------------------- /assets/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | 4 | background-position: 0px 0px, 5 | 16px 16px; 6 | background-size: 32px 32px; 7 | background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee 100%), 8 | linear-gradient(45deg, #eee 25%, white 25%, white 75%, #eee 75%, #eee 100%); 9 | } 10 | 11 | #root { 12 | position: absolute; 13 | width: 100vw; 14 | height: 100vh; 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | #canvas { 21 | width: 100%; 22 | height: 100%; 23 | } 24 | 25 | #loading { 26 | position: absolute; 27 | z-index: -1; 28 | font-family: 'Segoe UI', Roboto, Tahoma, Geneva, Verdana, sans-serif; 29 | font-size: 2em; 30 | color: gray; 31 | } -------------------------------------------------------------------------------- /dist/random.d.ts: -------------------------------------------------------------------------------- 1 | import { Rect, vec2 } from "zogra-renderer"; 2 | export interface JitterOption { 3 | base: T; 4 | jitter: T; 5 | } 6 | export declare function randomJittered(option: JitterOption): T; 7 | export declare function randomInRect(rect: Rect): vec2; 8 | /** 9 | * Generate noise in [-1, 1] 10 | * @param xy Should be non zero integer 11 | * @param seed Non zero integer 12 | */ 13 | export declare function goldNoise(xy: vec2, seed: number): number; 14 | export declare function tentNoise(t: number, seed: number): void; 15 | /** 16 | * Get random value in (-1, 1) 17 | */ 18 | export declare function random(): number; 19 | export declare function randomRange(min: number, max: number): number; 20 | //# sourceMappingURL=random.d.ts.map -------------------------------------------------------------------------------- /src/shader/raindrop-vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision mediump float; 3 | 4 | in vec3 aPos; 5 | in vec4 aColor; 6 | in vec2 aUV; 7 | in vec3 aNormal; 8 | 9 | in float aSize; 10 | in mat4 aModelMatrix; 11 | 12 | uniform mat4 uTransformM; 13 | uniform mat4 uTransformVP; 14 | uniform mat4 uTransformMVP; 15 | uniform mat4 uTransformM_IT; 16 | 17 | out vec4 vColor; 18 | out vec4 vPos; 19 | out vec2 vUV; 20 | out vec3 vNormal; 21 | out vec3 vWorldPos; 22 | out float vSize; 23 | 24 | void main() 25 | { 26 | mat4 mvp = uTransformVP * aModelMatrix; 27 | gl_Position = mvp * vec4(aPos, 1); 28 | vPos = gl_Position; 29 | vColor = aColor; 30 | vUV = aUV; 31 | vNormal = (uTransformM_IT * vec4(aNormal, 0)).xyz; 32 | vWorldPos = (uTransformM * vec4(aPos, 1)).xyz; 33 | vSize = aSize; 34 | } -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import { TextureData } from "zogra-renderer"; 2 | import { RaindropRenderer, RenderOptions } from "./renderer"; 3 | import { RaindropSimulator, SimulatorOptions } from "./simulator"; 4 | interface Options extends SimulatorOptions, RenderOptions { 5 | } 6 | declare class RaindropFX { 7 | options: Options; 8 | renderer: RaindropRenderer; 9 | simulator: RaindropSimulator; 10 | private animHandle; 11 | constructor(options: Partial & { 12 | canvas: HTMLCanvasElement; 13 | }); 14 | start(): Promise; 15 | stop(): void; 16 | resize(width: number, height: number): void; 17 | setBackground(background: string | TextureData): Promise; 18 | destroy(): void; 19 | private update; 20 | } 21 | export = RaindropFX; 22 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /dist/blur.d.ts: -------------------------------------------------------------------------------- 1 | import { RenderTexture, Texture, vec4, ZograRenderer } from "zogra-renderer"; 2 | declare const MaterialBlur_base: new (gl?: WebGL2RenderingContext | undefined) => import("zogra-renderer").Material; 3 | declare class MaterialBlur extends MaterialBlur_base { 4 | texture: Texture | null; 5 | textureSize: vec4; 6 | sampleOffset: number; 7 | } 8 | export declare class BlurRenderer { 9 | renderer: ZograRenderer; 10 | steps: RenderTexture[]; 11 | mateiralBlur: MaterialBlur; 12 | constructor(renderer: ZograRenderer); 13 | init(texture: Texture): void; 14 | blur(texture: Texture, iteration?: number, output?: RenderTexture): RenderTexture; 15 | downSample(input: Texture, iteration: number): void; 16 | upSample(iteration: number, finalOutput?: RenderTexture): RenderTexture; 17 | } 18 | export {}; 19 | //# sourceMappingURL=blur.d.ts.map -------------------------------------------------------------------------------- /dist/raindrop.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"raindrop.d.ts","sourceRoot":"","sources":["../src/raindrop.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,IAAI,EAAW,MAAM,gBAAgB,CAAC;AAE/D,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAQ,IAAI,EAAE,MAAM,SAAS,CAAC;AAErC,qBAAa,QAAQ;IAEjB,GAAG,EAAE,IAAI,CAAC;IACV,OAAO,EAAE,MAAM,CAAK;IACpB,QAAQ,EAAE,IAAI,CAAe;IAC7B,MAAM,EAAE,IAAI,CAAC;IACb,SAAS,UAAS;IAClB,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,iBAAiB,CAAS;IAElC,OAAO,CAAC,cAAc,CAAK;gBAEf,SAAS,EAAE,iBAAiB,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,SAAI;IAc9E,IAAI,IAAI,IACI,MAAM,CADe;IACjC,IAAI,IAAI,CAAC,CAAC,EAAE,MAAM,EAOjB;IAED,IAAI,IAAI,IAAI,IAAI,CAGf;IAED,IAAI,aAAa,WAGhB;IAED,IAAI,OAAO,2CAAoC;IAE/C,cAAc,CAAC,IAAI,EAAE,IAAI;IA+BzB,KAAK;IAgBL,YAAY;IAOZ,KAAK,CAAC,MAAM,EAAE,QAAQ;CAQzB"} -------------------------------------------------------------------------------- /dist/raindrop.d.ts: -------------------------------------------------------------------------------- 1 | import { vec2 } from "zogra-renderer"; 2 | import { CollisionGrid, RaindropSimulator } from "./simulator"; 3 | import { Time } from "./utils"; 4 | export declare class RainDrop { 5 | pos: vec2; 6 | density: number; 7 | velocity: vec2; 8 | spread: vec2; 9 | destroied: boolean; 10 | parent?: RainDrop; 11 | grid?: CollisionGrid; 12 | gridIdx?: number; 13 | private _mass; 14 | private _size; 15 | private simulator; 16 | private resistance; 17 | private shifting; 18 | private lastTrailPos; 19 | private nextTrailDistance; 20 | private nextRandomTime; 21 | constructor(simulator: RaindropSimulator, pos: vec2, size: number, density?: number); 22 | get mass(): number; 23 | set mass(m: number); 24 | get size(): vec2; 25 | get mergeDistance(): number; 26 | get options(): import("./simulator").SimulatorOptions; 27 | updateRaindrop(time: Time): void; 28 | split(): void; 29 | randomMotion(): void; 30 | merge(target: RainDrop): void; 31 | } 32 | //# sourceMappingURL=raindrop.d.ts.map -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raindrop-fx", 3 | "version": "1.0.8", 4 | "description": "Rain drop effect with WebGL", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "npm run build:ts && npm run build:node && npm run build:browser", 8 | "build:watch": "node ./build/build-browser.js --dev -w", 9 | "build:browser": "node ./build/build-browser.js", 10 | "build:node": "node ./build/build-node.js", 11 | "build:ts": "npx tsc", 12 | "start": "npx esbuild --serve=0.0.0.0:8000 --servedir=./", 13 | "test": "npm run build && npm run test:examples", 14 | "test:examples": "cd examples/with-node && npm i && npm test" 15 | }, 16 | "keywords": [ 17 | "webgl", 18 | "raindrop", 19 | "rain", 20 | "visual", 21 | "effect", 22 | "vfx" 23 | ], 24 | "author": "SardineFish", 25 | "license": "MIT", 26 | "dependencies": { 27 | "zogra-renderer": "^1.3.5" 28 | }, 29 | "devDependencies": { 30 | "esbuild": "^0.8.50", 31 | "typescript": "^4.1.5" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/SardineFish/raindrop-fx.git" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 SardineFish 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Raindrop Effect 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |

Loading...

17 |
18 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/spawner.ts: -------------------------------------------------------------------------------- 1 | import { Rect, vec2 } from "zogra-renderer"; 2 | import { RainDrop } from "./raindrop"; 3 | import { randomJittered, JitterOption, randomInRect, randomRange } from "./random"; 4 | import { RaindropSimulator, SimulatorOptions } from "./simulator"; 5 | 6 | export class Spawner 7 | { 8 | 9 | 10 | currentTime = 0; 11 | nextSpawn = 0; 12 | 13 | private simulator: RaindropSimulator; 14 | 15 | constructor(simulator: RaindropSimulator, options: SimulatorOptions) 16 | { 17 | 18 | this.simulator = simulator; 19 | } 20 | 21 | get interval() { return this.simulator.options.spawnInterval } 22 | get size() { return this.simulator.options.spawnSize } 23 | get spawnRect() { return this.simulator.options.viewport } 24 | 25 | update(dt: number): this 26 | { 27 | this.currentTime += dt; 28 | return this; 29 | } 30 | *trySpawn(): Iterable 31 | { 32 | while (this.currentTime >= this.nextSpawn) 33 | { 34 | const size = randomRange(...this.size); 35 | const pos = randomInRect(this.spawnRect); 36 | this.nextSpawn += randomRange(...this.interval); 37 | yield new RainDrop(this.simulator, pos, size); 38 | } 39 | if (this.currentTime >= this.nextSpawn) 40 | { 41 | 42 | } 43 | return undefined; 44 | } 45 | spawn(pos: vec2, size: number, density = 1) 46 | { 47 | return new RainDrop(this.simulator, pos, size, density); 48 | } 49 | } -------------------------------------------------------------------------------- /src/shader/droplet-vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision mediump float; 3 | 4 | in vec3 aPos; 5 | in vec2 aUV; 6 | 7 | uniform mat4 uTransformVP; 8 | 9 | uniform float uSeed; 10 | uniform vec4 uSpawnRect; // (xmin, ymin, xsize, ysize) 11 | uniform vec2 uSizeRange; 12 | 13 | out vec2 vUV; 14 | 15 | // Gold Noise ©2015 dcerisano@standard3d.com 16 | // - based on the Golden Ratio 17 | // - uniform normalized distribution 18 | // - fastest static noise generator function (also runs at low precision) 19 | // Ref: https://www.shadertoy.com/view/ltB3zD 20 | const float PHI = 1.61803398874989484820459; // Φ = Golden Ratio 21 | 22 | float gold_noise(in vec2 xy, in float seed) 23 | { 24 | return fract(tan(distance(xy*PHI, xy)*seed)*xy.x); 25 | } 26 | 27 | vec2 lerp(vec2 a, vec2 b, vec2 t) 28 | { 29 | return a + (b - a) * t; 30 | } 31 | 32 | void main() 33 | { 34 | int id = gl_InstanceID + 1; 35 | vec2 pos = uSpawnRect.xy + uSpawnRect.zw * vec2( 36 | gold_noise(vec2(1, id), uSeed + 1.0), 37 | gold_noise(vec2(id, 1), uSeed + 2.0)); 38 | 39 | vec2 size = vec2( 40 | gold_noise(vec2(1, id), uSeed + 3.0), 41 | gold_noise(vec2(id, 1), uSeed + 4.0)); 42 | size = lerp(vec2(uSizeRange.x), vec2(uSizeRange.y), size); 43 | 44 | mat4 model = mat4(size.x, 0.0, 0.0, 0.0, 45 | 0.0, size.x, 0.0, 0.0, 46 | 0.0, 0.0, 1, 0.0, 47 | pos.x, pos.y, 0.0, 1.0); 48 | mat4 mvp = uTransformVP * model; 49 | gl_Position = mvp * vec4(aPos, 1); 50 | vUV = aUV; 51 | } -------------------------------------------------------------------------------- /dist/simulator.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"simulator.d.ts","sourceRoot":"","sources":["../src/simulator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,IAAI,EAAQ,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE/B,MAAM,WAAW,gBAAgB;IAE7B,QAAQ,EAAE,IAAI,CAAC;IACf;;OAEG;IACH,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC;;OAEG;IACH,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;OAEG;IACH,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B;;OAEG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB;;OAEG;IACH,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC;;OAEG;IACH,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC;;OAEG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,aAAc,SAAQ,KAAK,CAAC,QAAQ,CAAC;IAE9C,iBAAiB;IACjB,IAAI,CAAC,GAAG,IAAI,EAAE,QAAQ,EAAE;IAIxB,GAAG,CAAC,QAAQ,EAAE,QAAQ;IAMtB,MAAM,CAAC,QAAQ,EAAE,QAAQ;CAQ5B;AAED,qBAAa,iBAAiB;IAE1B,OAAO,EAAE,gBAAgB,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,QAAQ,EAAE,CAAM;IAC3B,IAAI,EAAE,aAAa,EAAE,CAAM;gBACf,OAAO,EAAE,gBAAgB;IAQrC,IAAI,QAAQ,WAA6C;IAEzD,MAAM;IAaN,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAUnC,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM;IAInC,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAMnD,GAAG,CAAC,QAAQ,EAAE,QAAQ;IAYtB,MAAM,CAAC,IAAI,EAAE,IAAI;IAwBjB,cAAc,CAAC,IAAI,EAAE,IAAI;IA4BzB,eAAe;CAgDlB"} -------------------------------------------------------------------------------- /examples/with-node/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raindrop-fx-example", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "raindrop-fx-example", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "raindrop-fx": "file:///../.." 13 | }, 14 | "devDependencies": { 15 | "esbuild": "^0.9.5" 16 | } 17 | }, 18 | "../..": { 19 | "version": "1.0.1", 20 | "license": "MIT", 21 | "dependencies": { 22 | "@sardinefish/zogra-renderer": "^1.2.0" 23 | }, 24 | "devDependencies": { 25 | "esbuild": "^0.8.50", 26 | "typescript": "^4.1.5" 27 | } 28 | }, 29 | "node_modules/esbuild": { 30 | "version": "0.9.5", 31 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.9.5.tgz", 32 | "integrity": "sha512-ZBOtZX9HxgSdwkAroVifEUgylWqbOjqbu1i1ohRvhoaYt6aj81dxbizODx419gwDAupqut44ehFVwUq7WRN/OA==", 33 | "dev": true, 34 | "hasInstallScript": true, 35 | "bin": { 36 | "esbuild": "bin/esbuild" 37 | } 38 | }, 39 | "node_modules/raindrop-fx": { 40 | "resolved": "../..", 41 | "link": true 42 | } 43 | }, 44 | "dependencies": { 45 | "esbuild": { 46 | "version": "0.9.5", 47 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.9.5.tgz", 48 | "integrity": "sha512-ZBOtZX9HxgSdwkAroVifEUgylWqbOjqbu1i1ohRvhoaYt6aj81dxbizODx419gwDAupqut44ehFVwUq7WRN/OA==", 49 | "dev": true 50 | }, 51 | "raindrop-fx": { 52 | "version": "file:../..", 53 | "requires": { 54 | "@sardinefish/zogra-renderer": "^1.2.0", 55 | "esbuild": "^0.8.50", 56 | "typescript": "^4.1.5" 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/random.ts: -------------------------------------------------------------------------------- 1 | import { distance, mul, Rect, vec2, vec3, Vector2 } from "zogra-renderer"; 2 | 3 | export interface JitterOption 4 | { 5 | base: T; 6 | jitter: T; 7 | } 8 | 9 | export function randomJittered(option: JitterOption): T 10 | { 11 | if (typeof (option.base) === "number") 12 | return option.base + (option.jitter as number) * (Math.random() * 2 - 1) as T; 13 | else 14 | { 15 | let opt = option as JitterOption<[number, number]>; 16 | return vec2( 17 | opt.base[0] + (opt.jitter[0]) * (Math.random() * 2 - 1), 18 | opt.base[1] + (opt.jitter[1]) * (Math.random() * 2 - 1) 19 | ) as T; 20 | } 21 | } 22 | export function randomInRect(rect: Rect): vec2 23 | { 24 | return vec2(Math.random(), Math.random()).mul(rect.size).plus(rect.min); 25 | } 26 | 27 | // https://www.shadertoy.com/view/ltB3zD 28 | const PHI = 1.61803398874989484820459; // Φ = Golden Ratio 29 | function fract(x: number) 30 | { 31 | return x - Math.floor(x); 32 | } 33 | /** 34 | * Generate noise in [-1, 1] 35 | * @param xy Should be non zero integer 36 | * @param seed Non zero integer 37 | */ 38 | export function goldNoise(xy: vec2, seed: number) 39 | { 40 | return fract(Math.tan(distance(mul(xy, vec2(PHI)), xy) * seed) * xy.x); 41 | } 42 | 43 | export function tentNoise(t: number, seed: number) 44 | { 45 | let frac = fract(t); 46 | let grid = Math.floor(t += seed); 47 | } 48 | 49 | /** 50 | * Get random value in (-1, 1) 51 | */ 52 | export function random() 53 | { 54 | return Math.random() * 2 - 1; 55 | } 56 | 57 | export function randomRange(min: number, max: number) 58 | { 59 | return Math.random() * (max - min) + min; 60 | } -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npm 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v2 12 | with: 13 | node-version: '15.x' 14 | registry-url: 'https://registry.npmjs.org' 15 | - run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 16 | - run: npm install 17 | - run: npm run build 18 | - run: zip -j raindrop-fx README.md LICENSE bundle/index.js 19 | - name: Create Release 20 | id: create_release 21 | uses: actions/create-release@v1 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | with: 25 | tag_name: ${{ github.ref }} 26 | release_name: Release ${{ github.ref }} 27 | draft: false 28 | prerelease: false 29 | - name: Upload Release Asset 30 | id: upload-release-asset 31 | uses: actions/upload-release-asset@v1 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | with: 35 | upload_url: ${{ steps.create_release.outputs.upload_url }} 36 | asset_path: ./raindrop-fx.zip 37 | asset_name: raindrop-fx-${{ env.RELEASE_VERSION }}.zip 38 | asset_content_type: application/zip 39 | 40 | publish: 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v2 44 | - uses: actions/setup-node@v2 45 | with: 46 | node-version: '15.x' 47 | registry-url: 'https://registry.npmjs.org' 48 | - run: npm install 49 | - run: npm run build 50 | - run: npm publish 51 | env: 52 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /dist/renderer.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA,OACA,EAEkG,WAAW,EAC5C,aAAa,EAC7E,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAWtC,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAmH/B,MAAM,WAAW,aAAa;IAE1B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,WAAW,GAAG,MAAM,CAAC;IACjC;;;;OAIG;IACH,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;OAEG;IACH,kBAAkB,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAClD;;OAEG;IACH,IAAI,EAAE,OAAO,CAAC;IACd;;;;OAIG;IACH,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B;;;;;;;OAOG;IACH,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,eAAe,EAAE,UAAU,GAAG,QAAQ,CAAA;IACtC;;;;;;;OAOG;IACH,gBAAgB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACnD;;;OAGG;IACH,oBAAoB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C;;;OAGG;IACH,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,kBAAkB,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC;;;OAGG;IACH,qBAAqB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD;;;OAGG;IACH,yBAAyB,EAAE,MAAM,CAAC;IAClC;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,CAAC;CAE7B;AAED,qBAAa,gBAAgB;IAEzB,QAAQ,EAAE,aAAa,CAAC;IACxB,OAAO,EAAE,aAAa,CAAC;IAEvB,OAAO,CAAC,WAAW,CAA0B;IAC7C,OAAO,CAAC,kBAAkB,CAA0B;IACpD,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,kBAAkB,CAAgB;IAC1C,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,gBAAgB,CAAgB;IACxC,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,YAAY,CAAe;IAEnC,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,aAAa,CAA0B;IAC/C,OAAO,CAAC,YAAY,CAAyB;IAC7C,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,SAAS,CAAwB;IACzC,OAAO,CAAC,gBAAgB,CAA+B;IAEvD,OAAO,CAAC,gBAAgB,CAAO;IAC/B,OAAO,CAAC,IAAI,CAAO;IAEnB,OAAO,CAAC,cAAc,CAAmD;gBAI7D,OAAO,EAAE,aAAa;IAmB5B,UAAU;IAaV,gBAAgB;IAmCtB,MAAM;IAmBN,MAAM,CAAC,SAAS,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI;IA0BxC,OAAO,CAAC,cAAc;IAgCtB,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,aAAa;IAwCrB,OAAO,CAAC,WAAW;CAStB"} -------------------------------------------------------------------------------- /src/shader/compose.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision mediump float; 3 | 4 | in vec4 vColor; 5 | in vec4 vPos; 6 | in vec2 vUV; 7 | 8 | uniform sampler2D uMainTex; 9 | uniform vec4 uBackgroundSize; // (x, y, 1/x, 1/y) 10 | uniform sampler2D uRaindropTex; 11 | uniform sampler2D uDropletTex; 12 | uniform sampler2D uMistTex; 13 | uniform vec4 uColor; 14 | uniform vec2 uSmoothRaindrop; 15 | uniform vec2 uRefractParams; // (refractBase, refractScale) 16 | uniform vec4 uLightPos; 17 | uniform vec4 uDiffuseColor; // (color.rgb, shadowOffset) 18 | uniform vec4 uSpecularParams; // (color.rgb, exponent) 19 | uniform float uBump; 20 | 21 | out vec4 fragColor; 22 | 23 | void main() 24 | { 25 | // vec3 lightPos = vec3(0.5, 1, 1); 26 | 27 | vec4 raindrop = texture(uRaindropTex, vUV.xy).rgba; 28 | vec4 droplet = texture(uDropletTex, vUV.xy).rgba; 29 | float mist = texture(uMistTex, vUV.xy).r; 30 | 31 | vec4 compose = vec4(raindrop.rgb + droplet.rgb - vec3(2.0) * raindrop.rgb * droplet.rgb, max(droplet.a, raindrop.a)); 32 | 33 | float mask = smoothstep(uSmoothRaindrop.x, uSmoothRaindrop.y, compose.a); 34 | 35 | vec2 uv = vUV.xy + -(compose.xy - vec2(0.5)) * vec2(compose.b * uRefractParams.y + uRefractParams.x); 36 | vec3 normal = normalize(vec3((compose.xy - vec2(0.5)) * vec2(2), 1.0)); 37 | 38 | // vec3 lightDir = lightPos - vec3(vUV, 0); 39 | vec3 lightDir = uLightPos.xyz - uLightPos.w * vec3(vUV.xy, 0.0); 40 | vec3 viewDir = vec3(0, 0, 1); 41 | vec3 halfDir = normalize(lightDir + viewDir); 42 | float lambertian = clamp(dot(normalize(lightDir), normal), 0.0, 1.0); 43 | float blinnPhon = pow(max(dot(normal, halfDir), 0.0), uSpecularParams.a); 44 | 45 | 46 | // offset = pow(offset, vec2(2)); 47 | vec4 color = texture(uMainTex, uv.xy).rgba; 48 | vec3 diffuse = vec3((lambertian - uDiffuseColor.a) * uDiffuseColor.rgb); 49 | 50 | color.rgb += vec3((lambertian - uDiffuseColor.a) * uDiffuseColor.rgb); 51 | color.rgb += vec3(blinnPhon) * uSpecularParams.rgb; 52 | 53 | 54 | // fragColor = vec4(mask, mask, mask, 1); 55 | // color = color * vec3(uColor); 56 | 57 | fragColor = vec4(color.rgb, mask);// vec4(color.rgb, mask); 58 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | # dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | -------------------------------------------------------------------------------- /dist/simulator.d.ts: -------------------------------------------------------------------------------- 1 | import { Rect } from "zogra-renderer"; 2 | import { RainDrop } from "./raindrop"; 3 | import { Spawner } from "./spawner"; 4 | import { Time } from "./utils"; 5 | export interface SimulatorOptions { 6 | viewport: Rect; 7 | /** 8 | * Time range between two raindrop spwan. 9 | */ 10 | spawnInterval: [number, number]; 11 | /** 12 | * Random size range when spawn a new raindrop 13 | */ 14 | spawnSize: [number, number]; 15 | /** 16 | * Maximal amount of spawned raindrops. 17 | * Recommend less than 2000 18 | */ 19 | spawnLimit: number; 20 | /** 21 | * Recommend in range (0..1), other value should be ok. 22 | */ 23 | slipRate: number; 24 | /** 25 | * Describe how often a raindrop change its motion state 26 | */ 27 | motionInterval: [number, number]; 28 | /** 29 | * Random velociy x relative to velocity y. Recommend in range (0..0.1) 30 | */ 31 | xShifting: [number, number]; 32 | /** 33 | * Relative size for collision check. Recommend in range (0.6..1.2) 34 | */ 35 | colliderSize: number; 36 | /** 37 | * Mass density of the slipping trail raindrop. 38 | * Recommend in range (0.1..0.3) 39 | * 40 | * A large value cause a raindrop quickly lose its size during slipping. 41 | */ 42 | trailDropDensity: number; 43 | /** 44 | * Random size range of slipping trail drop. Recommend in range (0.3..0.5) 45 | */ 46 | trailDropSize: [number, number]; 47 | /** 48 | * Random distance range between two slipping trail drop. Recommend in range (20..40) 49 | */ 50 | trailDistance: [number, number]; 51 | /** 52 | * Vertical spread of a new spawned slipping trail drop. Recommend in range (0.4..0.6) 53 | */ 54 | trailSpread: number; 55 | /** 56 | * Spread rate when a new spawned raindrop hit the screen. Recommend in range (0.4..0.7) 57 | */ 58 | initialSpread: number; 59 | /** 60 | * Spread shrink rate per seconds. Recommend in range (0.01..0.02) 61 | */ 62 | shrinkRate: number; 63 | /** 64 | * Spread rate by velocity Y. Recommend in range (0.2..0.4) 65 | * 66 | * Raindrop with higher fall speed looks more narrow. 67 | */ 68 | velocitySpread: number; 69 | /** 70 | * Mass lose per second. Recommend in range (10..30) 71 | */ 72 | evaporate: number; 73 | /** 74 | * Gravity acceleration in pixels/s. Recommend 2400 75 | */ 76 | gravity: number; 77 | } 78 | export declare class CollisionGrid extends Array { 79 | /**@deprecated */ 80 | push(...item: RainDrop[]): number; 81 | add(raindrop: RainDrop): void; 82 | delete(raindrop: RainDrop): void; 83 | } 84 | export declare class RaindropSimulator { 85 | options: SimulatorOptions; 86 | spawner: Spawner; 87 | raindrops: RainDrop[]; 88 | grid: CollisionGrid[]; 89 | constructor(options: SimulatorOptions); 90 | get gridSize(): number; 91 | resize(): void; 92 | gridAt(gridX: number, gridY: number): CollisionGrid | undefined; 93 | gridAtWorldPos(x: number, y: number): CollisionGrid | undefined; 94 | worldToGrid(x: number, y: number): [number, number]; 95 | add(raindrop: RainDrop): void; 96 | update(time: Time): void; 97 | raindropUpdate(time: Time): void; 98 | collisionUpdate(): void; 99 | } 100 | //# sourceMappingURL=simulator.d.ts.map -------------------------------------------------------------------------------- /src/blur.ts: -------------------------------------------------------------------------------- 1 | import { div, FilterMode, MaterialFromShader, mul, RenderTexture, Shader, shaderProp, Texture, Texture2D, TextureResizing, vec2, vec4, WrapMode, ZograRenderer, TextureFormat } from "zogra-renderer"; 2 | import vert from "./shader/2d-vert.glsl"; 3 | import frag from "./shader/blur.glsl"; 4 | 5 | class MaterialBlur extends MaterialFromShader(new Shader(vert, frag)) 6 | { 7 | @shaderProp("uMainTex", "tex2d") 8 | texture: Texture | null = null; 9 | 10 | @shaderProp("uTexSize", "vec4") 11 | textureSize: vec4 = vec4.one(); 12 | 13 | @shaderProp("uSampleOffset", "float") 14 | sampleOffset: number = 1; 15 | } 16 | 17 | export class BlurRenderer 18 | { 19 | renderer: ZograRenderer; 20 | steps: RenderTexture[] = []; 21 | mateiralBlur = new MaterialBlur(); 22 | 23 | constructor(renderer: ZograRenderer) 24 | { 25 | this.renderer = renderer; 26 | } 27 | 28 | init(texture: Texture) 29 | { 30 | if (!this.steps[0]) 31 | { 32 | this.steps[0] = new RenderTexture(texture.width, texture.height, false, texture.format, texture.filterMode); 33 | this.steps[0].wrapMode = WrapMode.Clamp; 34 | this.steps[0].updateParameters(); 35 | } 36 | if (this.steps[0].width !== texture.width || this.steps[0].height !== texture.height) 37 | this.steps[0].resize(texture.width, texture.height, TextureResizing.Discard); 38 | } 39 | 40 | blur(texture: Texture, iteration: number = 4, output = this.steps[0]) 41 | { 42 | if (!this.steps[0]) 43 | this.steps[0] = new RenderTexture(texture.width, texture.height, false, texture.format, texture.filterMode); 44 | output = output || this.steps[0]; 45 | 46 | if (this.steps[0].width !== texture.width || this.steps[0].height !== texture.height) 47 | this.steps[0].resize(texture.width, texture.height, TextureResizing.Discard); 48 | 49 | this.downSample(texture, iteration); 50 | 51 | return this.upSample(iteration, output); 52 | } 53 | 54 | downSample(input: Texture, iteration: number) 55 | { 56 | for (let i = 1; i <= iteration; i++) 57 | { 58 | const downSize = vec2.floor(div(input.size, vec2(2))); 59 | if (!this.steps[i]) 60 | { 61 | this.steps[i] = new RenderTexture(downSize.x, downSize.y, false, TextureFormat.RGBA, FilterMode.Linear); 62 | this.steps[i].wrapMode = WrapMode.Clamp; 63 | this.steps[i].updateParameters(); 64 | } 65 | 66 | const output = this.steps[i]; 67 | if (output.width !== downSize.x || output.height !== downSize.y) 68 | output.resize(downSize.x, downSize.y, TextureResizing.Discard); 69 | 70 | this.mateiralBlur.texture = input; 71 | this.mateiralBlur.textureSize = vec4(input.width, input.height, 1 / input.width, 1 / input.height); 72 | this.mateiralBlur.sampleOffset = 1; 73 | this.renderer.blit(input, output, this.mateiralBlur); 74 | input = output; 75 | } 76 | } 77 | 78 | upSample(iteration: number, finalOutput = this.steps[0]) 79 | { 80 | let input = this.steps[iteration]; 81 | for (let i = iteration - 1; i >= 0; i--) 82 | { 83 | const upSize = mul(input.size, vec2(2)); 84 | if (!this.steps[i]) 85 | { 86 | this.steps[i] = new RenderTexture(upSize.x, upSize.y, false, TextureFormat.RGBA, FilterMode.Linear); 87 | this.steps[i].wrapMode = WrapMode.Clamp; 88 | this.steps[i].updateParameters(); 89 | } 90 | 91 | const output = i === 0 ? finalOutput : this.steps[i]; 92 | 93 | this.mateiralBlur.texture = input; 94 | this.mateiralBlur.textureSize = vec4(input.width, input.height, 1 / input.width, 1 / input.height); 95 | this.mateiralBlur.sampleOffset = 1; 96 | this.renderer.blit(input, output, this.mateiralBlur); 97 | input = output; 98 | } 99 | return input; 100 | } 101 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Rect, TextureData, vec2 } from "zogra-renderer"; 2 | import { RaindropRenderer, RenderOptions } from "./renderer"; 3 | import { RaindropSimulator, SimulatorOptions } from "./simulator"; 4 | import { Time } from "./utils"; 5 | 6 | interface Options extends SimulatorOptions, RenderOptions 7 | { 8 | } 9 | 10 | class RaindropFX 11 | { 12 | public options: Options; 13 | public renderer: RaindropRenderer; 14 | public simulator: RaindropSimulator; 15 | 16 | private animHandle = 0; 17 | 18 | constructor(options: Partial & {canvas: HTMLCanvasElement}) 19 | { 20 | const canvas = options.canvas; 21 | const defaultOptions: Options = { 22 | // Simulator options 23 | spawnInterval: [0.1, 0.1], 24 | spawnSize: [60, 100], 25 | spawnLimit: 2000, 26 | viewport: new Rect(vec2.zero(), vec2(canvas.width, canvas.height)), 27 | canvas: canvas, 28 | width: canvas.width, 29 | height: canvas.height, 30 | background: "", 31 | gravity: 2400, 32 | slipRate: 0, 33 | motionInterval: [0.1, 0.4], 34 | colliderSize: 1, 35 | trailDropDensity: 0.2, 36 | trailDistance: [20, 30], 37 | trailDropSize: [0.3, 0.5], 38 | trailSpread: 0.6, 39 | initialSpread: 0.5, 40 | shrinkRate: 0.01, 41 | velocitySpread: 0.3, 42 | evaporate: 10, 43 | xShifting: [0, 0.1], 44 | 45 | // Rendering options 46 | backgroundBlurSteps: 3, 47 | backgroundWrapMode: "clamp", 48 | mist: true, 49 | mistColor: [0.01, 0.01, 0.01, 1], 50 | mistBlurStep: 4, 51 | mistTime: 10, 52 | dropletsPerSeconds: 500, 53 | dropletSize: [10, 30], 54 | smoothRaindrop: [0.96, 0.99], 55 | refractBase: 0.4, 56 | refractScale: 0.6, 57 | raindropCompose: "smoother", 58 | raindropLightPos: [-1, 1, 2, 0], 59 | raindropDiffuseLight: [0.2, 0.2, 0.2], 60 | raindropShadowOffset: 0.8, 61 | raindropEraserSize: [0.93, 1.0], 62 | raindropSpecularLight: [0, 0, 0], 63 | raindropSpecularShininess: 256, 64 | raindropLightBump: 1, 65 | }; 66 | this.options = { ...defaultOptions, ...options }; 67 | 68 | this.simulator = new RaindropSimulator(this.options); 69 | this.renderer = new RaindropRenderer(this.options); 70 | } 71 | 72 | async start() 73 | { 74 | await this.renderer.loadAssets(); 75 | 76 | let lastFrameTime = 0; 77 | const update = (delay: number) => 78 | { 79 | const dt = (delay - lastFrameTime) / 1000; 80 | lastFrameTime = delay; 81 | const time =