From d43cfb880033f08a7e133f1982b61933f93dfbb8 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:35:12 +0200 Subject: [PATCH] commit --- index.html | 4 +- package-lock.json | 137 ++++++++++++++++++++++ package.json | 6 + src/index.js | 36 ++++-- src/main.ts | 64 +++++++++++ src/new.ts | 285 ++++++++++++++++++++++++++++++++++++++++++++++ src/particles.ts | 155 +++++++++++++++++++++++++ 7 files changed, 676 insertions(+), 11 deletions(-) create mode 100644 src/main.ts create mode 100644 src/new.ts create mode 100644 src/particles.ts diff --git a/index.html b/index.html index 430c212..32b0675 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - - +
+ diff --git a/package-lock.json b/package-lock.json index eec7904..9514fc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,12 @@ "": { "name": "particle-surfer", "version": "0.0.0", + "dependencies": { + "pixi.js": "^8.11.0", + "stats.js": "^0.17.0", + "three": "^0.179.1", + "uber-noise": "^0.2.2" + }, "devDependencies": { "typescript": "~5.8.3", "vite": "^7.0.4" @@ -454,6 +460,12 @@ "node": ">=18" } }, + "node_modules/@pixi/colord": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", + "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==", + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.46.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", @@ -734,6 +746,18 @@ "win32" ] }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", + "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==", + "license": "MIT" + }, + "node_modules/@types/earcut": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-3.0.0.tgz", + "integrity": "sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -741,6 +765,33 @@ "dev": true, "license": "MIT" }, + "node_modules/@webgpu/types": { + "version": "0.1.64", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.64.tgz", + "integrity": "sha512-84kRIAGV46LJTlJZWxShiOrNL30A+9KokD7RB3dRCIqODFjodS5tCD5yyiZ8kIReGVZSDfA3XkkwyyOIF6K62A==", + "license": "BSD-3-Clause" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/alea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/alea/-/alea-1.0.1.tgz", + "integrity": "sha512-QU+wv+ziDXaMxRdsQg/aH7sVfWdhKps5YP97IIwFkHCsbDZA3k87JXoZ5/iuemf4ntytzIWeScrRpae8+lDrXA==", + "license": "MIT" + }, + "node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "license": "ISC" + }, "node_modules/esbuild": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", @@ -783,6 +834,12 @@ "@esbuild/win32-x64": "0.25.8" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/fdir": { "version": "6.4.6", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", @@ -813,6 +870,27 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/gifuct-js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz", + "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==", + "license": "MIT", + "dependencies": { + "js-binary-schema-parser": "^2.0.3" + } + }, + "node_modules/ismobilejs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", + "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==", + "license": "MIT" + }, + "node_modules/js-binary-schema-parser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz", + "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -832,6 +910,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -852,6 +936,28 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pixi.js": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.11.0.tgz", + "integrity": "sha512-dyuThzncsgEgJZnvd/A/5x6IkUERbK+phXqUQrI+0C6WE+8xqGH5VChRTLecemhgZF0kQ+gZOM3tJTX9937xpg==", + "license": "MIT", + "dependencies": { + "@pixi/colord": "^2.9.6", + "@types/css-font-loading-module": "^0.0.12", + "@types/earcut": "^3.0.0", + "@webgpu/types": "^0.1.40", + "@xmldom/xmldom": "^0.8.10", + "earcut": "^3.0.1", + "eventemitter3": "^5.0.1", + "gifuct-js": "^2.1.2", + "ismobilejs": "^1.1.1", + "parse-svg-path": "^0.1.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -921,6 +1027,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/simplex-noise": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/simplex-noise/-/simplex-noise-4.0.3.tgz", + "integrity": "sha512-qSE2I4AngLQG7BXqoZj51jokT4WUXe8mOBrvfOXpci8+6Yu44+/dD5zqDpOx3Ux792eamTd2lLcI8jqFntk/lg==", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -931,6 +1043,18 @@ "node": ">=0.10.0" } }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" + }, + "node_modules/three": { + "version": "0.179.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.179.1.tgz", + "integrity": "sha512-5y/elSIQbrvKOISxpwXCR4sQqHtGiOI+MKLc3SsBdDXA2hz3Mdp3X59aUp8DyybMa34aeBwbFTpdoLJaUDEWSw==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -962,6 +1086,19 @@ "node": ">=14.17" } }, + "node_modules/uber-noise": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/uber-noise/-/uber-noise-0.2.2.tgz", + "integrity": "sha512-fOP7KMrxtVdmknaAPuMMQq+7cUNbnnFLkFycTA/c/hfn7/FC9sc5yOIhmYDtporlUOGiXo9Lwi6AV6GxhmKAhg==", + "license": "MIT", + "dependencies": { + "alea": "^1.0.1", + "simplex-noise": "^4.0.3" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/vite": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", diff --git a/package.json b/package.json index 64b739a..8117055 100644 --- a/package.json +++ b/package.json @@ -11,5 +11,11 @@ "devDependencies": { "typescript": "~5.8.3", "vite": "^7.0.4" + }, + "dependencies": { + "pixi.js": "^8.11.0", + "stats.js": "^0.17.0", + "three": "^0.179.1", + "uber-noise": "^0.2.2" } } diff --git a/src/index.js b/src/index.js index c08e5f2..111ee6c 100644 --- a/src/index.js +++ b/src/index.js @@ -3,8 +3,11 @@ import * as THREE from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/110/thre //import { LPSphere, LPPlane } from 'https://1florki.github.io/low_poly_generator/lpshapes.js' import { Noise } from 'https://1florki.github.io/jsutils2/noise.js' -import { Gradient } from 'https://1florki.github.io/threejsutils/gradient.js' +import Stats from "stats.js"; +let stats = new Stats(); +stats.showPanel( 0 ); // 0: fps, 1: ms, 2: mb, 3+: custom +document.body.appendChild( stats.dom ); class Particle { constructor(opt) { @@ -17,12 +20,10 @@ class Particle { if(opt.size) this.reset(opt.size); } reset(size) { - //if(Math.random() < 0.01) console.log(this.speed.length()); this.setPos((Math.random() - 0.5) * 2 * size.x, (Math.random() - 0.5) * 2 * size.y, 0); this.acc.set(0, 0, 0); this.speed.set(0, 0, 0); this.time = Math.random() * 1.8 + 0.3 - //console.log("reset"); } setPos(x, y, z) { this.pos.set(x, y, z); @@ -120,11 +121,14 @@ const levels = [[89842, 74789], [41742, 37680], [78288, 60840], [15693, 83395], let level = 0; function setupScene() { - renderer = new THREE.WebGLRenderer({antialising: true}); + renderer = new THREE.WebGLRenderer({antialising: true, alpha: true, preserveDrawingBuffer: true, alpha: true}); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); + // Enable alpha blending for trail effect + renderer.autoClearColor = false; // Don't auto-clear the canvas + document.body.appendChild(renderer.domElement); window.addEventListener('resize', onResize, false); @@ -139,6 +143,11 @@ function setupScene() { let light2 = new THREE.DirectionalLight(0xffffff, 0.5); light2.position.set(0, - 1, 0.5) scene.add(light2); + + // add black transparent rect + let rect = new THREE.Mesh(new THREE.PlaneGeometry(100, 100), new THREE.MeshBasicMaterial({color: 0x000000, transparent: true, opacity: 0.15})); + rect.position.set(0, 0, 0); + scene.add(rect); document.addEventListener("keydown", (event) => { keys[event.key] = true @@ -166,8 +175,8 @@ function setupScene() { }); let fogColor = new THREE.Color(0x000000); - scene.background = fogColor; - scene.fog = new THREE.Fog(fogColor, 4.5, 4.7); + scene.background = null; // Remove solid background for trail effect + // scene.fog = new THREE.Fog(fogColor, 4.5, 4.7); size = new THREE.Vector3(10, 2, .01); @@ -219,9 +228,9 @@ function nextLevel() { function switchActive() { active++; if(active >= particles.length) active = 0; - for(let i = 0; i < particles.length; i++) { - particles[i].mesh.material.size = 1.7; - particles[i].mesh.position.z = -0.1; + for(const element of particles) { + element.mesh.material.size = 1; + element.mesh.position.z = -0.1; } particles[active].mesh.material.size = 1.7; particles[active].mesh.position.z = 0; @@ -233,6 +242,8 @@ function resetPlayer() { } function animate(now) { requestAnimationFrame(animate); + + stats.begin(); // animation loop here @@ -255,7 +266,14 @@ function animate(now) { camera.position.x = (playerPart.pos.x + camera.position.x * 49) / 50; } //console.log(playerPart.pos); + + // renderer.setClearAlpha(0.0); + // Create trail effect using WebGL + // renderer.setClearColor(0x000000, 0.1); + // renderer.clear(); renderer.render(scene, camera); + + stats.end(); } function onResize() { diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..0f89fb9 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,64 @@ +/* eslint-disable prefer-const */ +/* eslint-disable prettier/prettier */ +import { Application, Graphics, RenderTexture, Sprite } from "pixi.js"; +import { UberNoise } from "uber-noise"; +import ParticleSystem from "./particles"; +import 'pixi.js/math-extras'; +import Stats from "stats.js"; + +(async () => { + const app = new Application(); + await app.init({ background: "#000000", resizeTo: window }); + + const noise = new UberNoise({ scale: 0.01}) + document.getElementById("pixi-container")!.appendChild(app.canvas); + + const renderTexture = RenderTexture.create({ width: 1000, height: 1000 }); + + const sprite = new Sprite(renderTexture); + app.stage.addChild(sprite); + + const particles = new ParticleSystem(); + let stats = new Stats(); + stats.showPanel( 1 ); // 0: fps, 1: ms, 2: mb, 3+: custom + document.body.appendChild( stats.dom ); + + // app.stage.addChild(particles.container); + for(let i = 0; i < 5000; i++) { + particles.spawnParticle(); + } + + // add mostly transparent black to the render texture + + const black = new Graphics(); + black.rect(0, 0, 1000, 1000).fill({ color: 0x000000, alpha: 0.1 }); + particles.container.addChild(black); + + let noiseData: number[][][] = []; + for(let x = 0; x < 1000; x++) { + noiseData.push([]); + for(let y = 0; y < 1000; y++) { + noiseData[x].push([noise.get(x, y), noise.get(x + 5000, y - 1234)]); + } + } + + console.log(noiseData); + + + app.ticker.add(() => { + stats.begin(); + particles.update(app.ticker.deltaTime, noiseData); + + app.renderer.render({ container: particles.container, target: renderTexture, clear: false }); + stats.end(); + + }); + + window.addEventListener("keydown", (e) => { + + }); + + window.addEventListener("keyup", (e) => { + + }); +})(); diff --git a/src/new.ts b/src/new.ts new file mode 100644 index 0000000..20aa0f9 --- /dev/null +++ b/src/new.ts @@ -0,0 +1,285 @@ +import * as THREE from 'three'; + +import { UberNoise } from "uber-noise"; +// @ts-ignore +import Stats from "stats.js"; + +let stats = new Stats(); +stats.showPanel( 0 ); // 0: fps, 1: ms, 2: mb, 3+: custom +document.body.appendChild( stats.dom ); + +class Particle { + constructor(opt) { + opt = opt || {}; + + this.pos = new THREE.Vector3(0, 0, 0); + this.speed = new THREE.Vector3(0, 0, 0); + this.acc = new THREE.Vector3(0, 0, 0); + this.time = opt.time || Math.random() * 1.8 + 0.3; + if(opt.size) this.reset(opt.size); + } + reset(size) { + this.setPos((Math.random() - 0.5) * 2 * size.x, (Math.random() - 0.5) * 2 * size.y, 0); + this.acc.set(0, 0, 0); + this.speed.set(0, 0, 0); + this.time = Math.random() * 1.8 + 0.3 + } + setPos(x, y, z) { + this.pos.set(x, y, z); + } + update(dt, maxSpeed) { + this.speed.add(this.acc); + this.speed.clampLength(0, maxSpeed); + this.pos.add(this.speed); + this.time -= dt; + } + isDead(size) { + return (Math.abs(this.pos.x) > size.x || Math.abs(this.pos.y) > size.y || this.time < 0) + } +} + +class Particles { + constructor(opt) { + opt = opt || {}; + + this.num = opt.num || 10000; + + this.particleSize = opt.particleSize || 1; + this.size = opt.size || new THREE.Vector3(1, 1, 1); + + + this.vertexColors = opt.vertexColors || true; + this.color = opt.color || new THREE.Color(0xffffff); + this.maxSpeed = opt.maxSpeed || 0.02; + + this.minTime = opt.minTime || 0.01; + this.maxTime = opt.maxTime || 1; + this.newNoise(opt.seed || 0); + + this.createParticles(); + } + newNoise(seed) { + this.noise = new UberNoise({min: -0.01, max: 0.01, scale: 0.2, octaves: 2, persistence: 0.5, seed: seed}) + } + applyNoiseForce(p) { + p.acc.x = this.noise.get(p.pos.x, p.pos.y + 23, p.pos.z) + 0.002; + p.acc.y = this.noise.get(p.pos.x + 100, p.pos.y, p.pos.z); + } + + createParticles() { + this.geo = new THREE.BufferGeometry(); + + this.positionData = new Float32Array(this.num * 3); + + this.geo.setAttribute('position', new THREE.BufferAttribute(this.positionData, 3)); + + if(this.vertexColors) { + this.colorData = new Float32Array(this.num * 3); + this.geo.setAttribute('color', new THREE.BufferAttribute(this.colorData, 3)); + } + + this.parts = []; + for(let i = 0; i < this.num; i++) { + this.parts.push(new Particle({size: this.size})); + } + + var mat = new THREE.PointsMaterial({vertexColors: true, size: this.particleSize, sizeAttenuation: false, color: this.color}); + this.mesh = new THREE.Points(this.geo, mat); + } + update(dt: number, num: number) { + for(let i = 0; i < this.num; i++) { + let p = this.parts[i]; + + this.applyNoiseForce(p); + p.update(dt, this.maxSpeed); + if(p.isDead(this.size)) p.reset(this.size) + + this.positionData[i * 3] = p.pos.x; + this.positionData[i * 3 + 1] = p.pos.y; + this.positionData[i * 3 + 2] = p.pos.z; + + this.colorData[i * 3] = num * ((p.acc.x + 0.01) * 80 + 0.2); + this.colorData[i * 3 + 1] = (1 - num) * ((p.acc.x + 0.01) * 80 + 0.2); + this.colorData[i * 3 + 2] = (p.acc.y + 0.01) * 50 + 0.2; + + } + this.geo.attributes.position.needsUpdate = true; + + if(this.vertexColors) this.geo.attributes.color.needsUpdate = true; + + } + +} + + +var renderer, scene, light, camera, keys = {}, mesh, camNode, clock, particles = [], player, playerPart, active = 0, size, stop = false; + +const levels = [[89842, 74789], [41742, 37680], [78288, 60840], [15693, 83395], [54971, 5891], [29338, 42504], [83166, 59559], [14271, 26324], [87125, 3695], [43298, 94833], [12641, 84336], [81706, 92840], [81342, 18215], [68226, 9387], [50415, 61135], [40356, 68917], [21870, 34087], [77604, 4641], [35813, 32668]]; + +let level = 0; + +function setupScene() { + renderer = new THREE.WebGLRenderer({antialising: true, alpha: true, preserveDrawingBuffer: true, alpha: true}); + + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(window.devicePixelRatio); + + // Enable alpha blending for trail effect + renderer.autoClearColor = false; // Don't auto-clear the canvas + + document.body.appendChild(renderer.domElement); + + window.addEventListener('resize', onResize, false); + + scene = new THREE.Scene({background: new THREE.Color(0xff0000)}); + + let ambi = new THREE.AmbientLight(0xffffff, 0.5); // soft white light + scene.add(ambi); + + let light = new THREE.DirectionalLight(0xffffff, 0.5); + scene.add(light); + let light2 = new THREE.DirectionalLight(0xffffff, 0.5); + light2.position.set(0, - 1, 0.5) + scene.add(light2); + + // add black transparent rect + let rect = new THREE.Mesh(new THREE.PlaneGeometry(100, 100), new THREE.MeshBasicMaterial({color: 0x000000, transparent: true, opacity: 0.15})); + rect.position.set(0, 0, 0); + scene.add(rect); + + document.addEventListener("keydown", (event) => { + keys[event.key] = true + if(event.key == " ") { + switchActive(); + } + if(event.key == "t") { + stop = !stop; + } + if(event.key == "l") { + nextLevel(); + } + if(event.key == "r") { + let s1 = Math.floor(Math.random() * 100000); + let s2 = Math.floor(Math.random() * 100000); + particles[0].newNoise(s1); + particles[1].newNoise(s2); + console.log("random level [" + s1 + ", " + s2 + "]"); + resetPlayer(); + } + }, false); + + document.addEventListener("mouseup", (event) => { + switchActive(); + }); + + let fogColor = new THREE.Color(0x000000); + scene.background = null; // Remove solid background for trail effect + // scene.fog = new THREE.Fog(fogColor, 4.5, 4.7); + + size = new THREE.Vector3(10, 2, .01); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.2, 40); + camera.position.z = 4.5 + + clock = new THREE.Clock(); + + let part1 = new Particles({size: size, seed: levels[level][0]}); + let part2 = new Particles({size: size, seed: levels[level][1]}); + + particles.push(part1); + particles.push(part2); + scene.add(part1.mesh); + scene.add(part2.mesh); + switchActive(); + + player = new THREE.Mesh(new THREE.SphereGeometry( 0.07, 8, 8 ), new THREE.MeshBasicMaterial( {color: 0xffffff} )) + playerPart = new Particle(); + playerPart.pos.set(-size.x * 5 / 6, 0, 0); + playerPart.time = 1000000; + scene.add(player); + + let borderColor = 0x444444//0xcf1020 + let goalColor = 0xffffff//0xcf1020 + let border = new THREE.Mesh(new THREE.BoxGeometry( size.x * 2, 0.1, 0.15 ), new THREE.MeshStandardMaterial( {color: borderColor} )); + border.position.y = size.y; + scene.add(border); + let border2 = border.clone(); + border2.position.y = -size.y; + scene.add(border2) + + let border3 = new THREE.Mesh(new THREE.BoxGeometry( 0.1, size.y * 2 + 0.1, 0.15 ), new THREE.MeshStandardMaterial( {color: borderColor} )); + border3.position.x = -size.x; + scene.add(border3) + + let goal = new THREE.Mesh(new THREE.BoxGeometry( 0.1, size.y * 2 + 0.1, 0.15 ), new THREE.MeshStandardMaterial( {color: goalColor} )); + goal.position.x = size.x; + scene.add(goal) +} +function nextLevel() { + level++; + if(level >= levels.length) level = 0; + + particles[0].newNoise(levels[level][0]); + particles[1].newNoise(levels[level][1]); + console.log("level: " + level + " [" + levels[level][0] + ", " + levels[level][1] + "]"); +} +function switchActive() { + active++; + if(active >= particles.length) active = 0; + for(const element of particles) { + element.mesh.material.size = 1; + element.mesh.position.z = -0.1; + } + particles[active].mesh.material.size = 1.7; + particles[active].mesh.position.z = 0; +} +function resetPlayer() { + playerPart.reset(size); + playerPart.pos.set(-size.x * 5 / 6, 0, 0); + playerPart.time = 1000000; +} +function animate(now) { + requestAnimationFrame(animate); + + stats.begin(); + + // animation loop here + + let dt = clock.getDelta(); + //console.log(dt); + for(let i = 0; i < particles.length; i++) { + particles[i].update(dt, i); + } + if(!stop) { + + particles[active].applyNoiseForce(playerPart); + playerPart.update(dt, particles[active].maxSpeed); + player.position.copy(playerPart.pos); + if(playerPart.isDead(size)) { + if(playerPart.pos.x >= size.x) { + nextLevel(); + } + resetPlayer(); + } + camera.position.x = (playerPart.pos.x + camera.position.x * 49) / 50; + } + //console.log(playerPart.pos); + + // renderer.setClearAlpha(0.0); + // Create trail effect using WebGL + // renderer.setClearColor(0x000000, 0.1); + // renderer.clear(); + renderer.render(scene, camera); + + stats.end(); +} + +function onResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +setupScene(); +animate(0); diff --git a/src/particles.ts b/src/particles.ts new file mode 100644 index 0000000..6b3286b --- /dev/null +++ b/src/particles.ts @@ -0,0 +1,155 @@ +import * as PIXI from "pixi.js"; +import type UberNoise from "uber-noise"; + +export type ParticleOptions = { + x: number; + y: number; + size: number; + color: PIXI.ColorSource; + alpha: number; + speedX: number; + speedY: number; + maxAge: number; +}; + +class Particle extends PIXI.Graphics { + speedX: number = 0; + speedY: number = 0; + age: number = 0; + maxAge: number = 100; + initialSize: number = 1; + + maxSpeed: number = 2; + + speed = new PIXI.Point(0, 0); + acc = new PIXI.Point(0, 0); + + constructor() { + super(); + this.reset(new PIXI.Point(Math.random() * 1000, Math.random() * 1000)); + + this.rect(0, 0, 1, 1); + } + + update(dt: number, noise: number[][][]) { + let x = Math.max(0, Math.min(999, Math.floor(this.x))); + let y = Math.max(0, Math.min(999, Math.floor(this.y))); + + this.acc.x = noise[x][y][0]; + this.acc.y = noise[x][y][1]; + + this.speed.x += this.acc.x * dt * 0.1; + this.speed.y += this.acc.y * dt * 0.1; + + let l = this.speed.magnitude(); + if(l > this.maxSpeed) { + this.speed.normalize().multiplyScalar(this.maxSpeed, this.speed); + } + + if(this.x > 1000) { + this.x = 0; + } else if(this.x < 0) { + this.x = 1000; + } + + if(this.y > 1000) { + this.y = 0; + } else if(this.y < 0) { + this.y = 1000; + } + + let lastX = this.x; + let lastY = this.y; + + this.x += this.speed.x * dt * 0.1; + this.y += this.speed.y * dt * 0.1; + this.age += dt; + + this.clear(); + this.beginPath(); + + this.moveTo(lastX, lastY); + this.lineTo(this.x, this.y); + + // get angle between lastX, lastY and this.x, this.y + let angle = Math.atan2(this.y - lastY, this.x - lastX); + // get distance between lastX, lastY and this.x, this.y + // let distance = Math.sqrt((this.x - lastX) ** 2 + (this.y - lastY) ** 2); + + // this.scale.set(1, distance); + // this.rotation = angle; + + // color based on angle (to hue) + let hue = angle / (2 * Math.PI); + + this.stroke({ + color: `hsl(${(hue * 100).toFixed(1)}, 100%, 50%)`, + width: 5, + }); + } + + reset(position: PIXI.Point) { + this.x = position.x; + this.y = position.y; + this.acc.set(0, 0); + this.speed.set(0, 0); + this.age = 0; + this.maxAge = Math.random() * 100 + 10; + } + +} + +export default class ParticleSystem { + private readonly maxParticles: number; + public container: PIXI.Container; + private readonly particles: Particle[]; + private readonly particlePool: Particle[]; + + + constructor(maxParticles: number = 10000) { + this.maxParticles = maxParticles; + this.container = new PIXI.Container(); + this.container.zIndex = 10; + this.particles = []; + this.particlePool = []; + } + + + spawnParticle(): Particle | null { + let particle: Particle; + if (this.particlePool.length > 0) { + particle = this.particlePool.pop()!; + } else if (this.particles.length < this.maxParticles) { + particle = new Particle(); + particle.reset(new PIXI.Point(Math.random() * 1000, Math.random() * 1000)); + + this.container.addChild(particle); + } else { + return null; + } + + this.particles.push(particle); + + return particle; + } + + removeParticle(particle: Particle): void { + const index = this.particles.indexOf(particle); + if (index !== -1) { + particle.visible = false; + + this.particles.splice(index, 1); + this.particlePool.push(particle); + } + } + + update(deltaTime: number, noise: number[][][]): void { + for (let i = this.particles.length - 1; i >= 0; i--) { + this.particles[i].update(deltaTime, noise); + + if(this.particles[i].age > this.particles[i].maxAge) { + this.particles[i].reset(new PIXI.Point(Math.random() * 1000, Math.random() * 1000)); + } + } + } +} -- 2.51.2