├── sparkler ├── components │ ├── Scale.hx │ ├── Speed.hx │ ├── Rotation.hx │ ├── LifeProgress.hx │ ├── AngularVelocity.hx │ ├── Color.hx │ ├── Size.hx │ ├── PrevPos.hx │ ├── Velocity.hx │ ├── ColorRange.hx │ ├── ScaleRange.hx │ ├── ColorPropertyList.hx │ └── ScalePropertyList.hx ├── utils │ ├── Bounds.hx │ ├── Vector2.hx │ ├── Area.hx │ ├── FloatRange.hx │ ├── ColorRange.hx │ ├── PropertyNode.hx │ ├── ColorFloat.hx │ ├── FloatPropertyList.hx │ ├── ColorPropertyList.hx │ ├── Maths.hx │ ├── Vector2PropertyList.hx │ ├── macro │ │ ├── ParticleMacro.hx │ │ ├── ParticleModuleMacro.hx │ │ ├── MacroUtils.hx │ │ └── ParticleEmitterMacro.hx │ └── Color.hx ├── Particle.hx ├── modules │ ├── helpers │ │ ├── UpdatePosFromVelocityModule.hx │ │ ├── UpdateRotationFromAngularVelocityModule.hx │ │ ├── LifeProgressModule.hx │ │ └── UpdateSpeedModule.hx │ ├── life │ │ ├── LifetimeModule.hx │ │ ├── LifetimeMinMaxModule.hx │ │ ├── EmitterLifetimeModule.hx │ │ └── EmitterLifeDistanceModule.hx │ ├── scale │ │ ├── InitialScaleModule.hx │ │ ├── InitialScaleMinMaxModule.hx │ │ ├── ScaleListOverLifetimeModule.hx │ │ ├── ScaleOverLifetimeModule.hx │ │ ├── ScaleBySpeedModule.hx │ │ └── ScaleMinMaxOverLifetimeModule.hx │ ├── color │ │ ├── InitialColorModule.hx │ │ ├── InitialColorMinMaxModule.hx │ │ ├── ColorListOverLifetimeModule.hx │ │ ├── ColorOverLifetimeModule.hx │ │ ├── ColorBySpeedModule.hx │ │ └── ColorMinMaxOverLifetimeModule.hx │ ├── emit │ │ ├── ParticlesPerEmitModule.hx │ │ ├── EmitRateModule.hx │ │ └── EmitDistanceModule.hx │ ├── velocity │ │ ├── DragModule.hx │ │ ├── InitialVelocityModule.hx │ │ ├── ForceModule.hx │ │ ├── DirectionModule.hx │ │ ├── InitialVelocityMinMaxModule.hx │ │ └── DirectionMinMaxModule.hx │ ├── rotate │ │ ├── InitialRotationModule.hx │ │ ├── InitialRotationMinMaxModule.hx │ │ ├── AngularVelocityModule.hx │ │ └── RotateOverLifetimeModule.hx │ ├── spawn │ │ ├── CircleSpawnModule.hx │ │ └── AreaSpawnModule.hx │ ├── size │ │ ├── InitialSizeModule.hx │ │ ├── InitialSizeMinMaxModule.hx │ │ └── SizeOverLifetimeModule.hx │ ├── move │ │ ├── MovementListOverLifetimeModule.hx │ │ └── MovementOverLifetimeModule.hx │ └── render │ │ ├── ClaySpriteRendererModule.hx │ │ ├── NucSpriteRendererModule.hx │ │ └── KhaSpriteRendererModule.hx ├── ParticleModule.hx └── ParticleEmitter.hx ├── haxelib.json ├── .gitattributes ├── .gitignore ├── LICENSE.md └── README.md /sparkler/components/Scale.hx: -------------------------------------------------------------------------------- 1 | package sparkler.components; 2 | 3 | typedef Scale = Float; -------------------------------------------------------------------------------- /sparkler/components/Speed.hx: -------------------------------------------------------------------------------- 1 | package sparkler.components; 2 | 3 | typedef Speed = Float; -------------------------------------------------------------------------------- /sparkler/components/Rotation.hx: -------------------------------------------------------------------------------- 1 | package sparkler.components; 2 | 3 | typedef Rotation = Float; -------------------------------------------------------------------------------- /sparkler/components/LifeProgress.hx: -------------------------------------------------------------------------------- 1 | package sparkler.components; 2 | 3 | typedef LifeProgress = Float; -------------------------------------------------------------------------------- /sparkler/components/AngularVelocity.hx: -------------------------------------------------------------------------------- 1 | package sparkler.components; 2 | 3 | typedef AngularVelocity = Float; -------------------------------------------------------------------------------- /sparkler/components/Color.hx: -------------------------------------------------------------------------------- 1 | package sparkler.components; 2 | 3 | typedef Color = sparkler.utils.Color; 4 | -------------------------------------------------------------------------------- /sparkler/components/Size.hx: -------------------------------------------------------------------------------- 1 | package sparkler.components; 2 | 3 | typedef Size = sparkler.utils.Vector2; 4 | -------------------------------------------------------------------------------- /sparkler/components/PrevPos.hx: -------------------------------------------------------------------------------- 1 | package sparkler.components; 2 | 3 | typedef PrevPos = sparkler.utils.Vector2; 4 | -------------------------------------------------------------------------------- /sparkler/components/Velocity.hx: -------------------------------------------------------------------------------- 1 | package sparkler.components; 2 | 3 | typedef Velocity = sparkler.utils.Vector2; 4 | -------------------------------------------------------------------------------- /sparkler/components/ColorRange.hx: -------------------------------------------------------------------------------- 1 | package sparkler.components; 2 | 3 | typedef ColorRange = sparkler.utils.ColorRange; 4 | -------------------------------------------------------------------------------- /sparkler/components/ScaleRange.hx: -------------------------------------------------------------------------------- 1 | package sparkler.components; 2 | 3 | typedef ScaleRange = sparkler.utils.FloatRange; 4 | -------------------------------------------------------------------------------- /sparkler/components/ColorPropertyList.hx: -------------------------------------------------------------------------------- 1 | package sparkler.components; 2 | 3 | typedef ColorPropertyList = sparkler.utils.ColorPropertyList; 4 | -------------------------------------------------------------------------------- /sparkler/components/ScalePropertyList.hx: -------------------------------------------------------------------------------- 1 | package sparkler.components; 2 | 3 | typedef ScalePropertyList = sparkler.utils.FloatPropertyList; 4 | -------------------------------------------------------------------------------- /sparkler/utils/Bounds.hx: -------------------------------------------------------------------------------- 1 | package sparkler.utils; 2 | 3 | @:structInit 4 | class Bounds { 5 | 6 | public var min:T; 7 | public var max:T; 8 | 9 | public function new(min:T, max:T) { 10 | this.min = min; 11 | this.max = max; 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /sparkler/utils/Vector2.hx: -------------------------------------------------------------------------------- 1 | package sparkler.utils; 2 | 3 | @:structInit 4 | class Vector2 { 5 | 6 | public var x:Float; 7 | public var y:Float; 8 | 9 | public function new(x:Float = 0, y:Float = 0) { 10 | this.x = x; 11 | this.y = y; 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /sparkler/utils/Area.hx: -------------------------------------------------------------------------------- 1 | package sparkler.utils; 2 | 3 | @:structInit 4 | class Area { 5 | 6 | public var width:Float; 7 | public var height:Float; 8 | 9 | public function new(width:Float, height:Float) { 10 | this.width = width; 11 | this.height = height; 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /sparkler/utils/FloatRange.hx: -------------------------------------------------------------------------------- 1 | package sparkler.utils; 2 | 3 | @:structInit 4 | class FloatRange { 5 | 6 | public var start:Float; 7 | public var end:Float; 8 | 9 | public function new(start:Float = 0, end:Float = 0) { 10 | this.start = start; 11 | this.end = end; 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /sparkler/utils/ColorRange.hx: -------------------------------------------------------------------------------- 1 | package sparkler.utils; 2 | 3 | @:structInit 4 | class ColorRange { 5 | 6 | public var start:Color; 7 | public var end:Color; 8 | 9 | public function new(start:Color = 0xFFFFFF, end:Color = 0xFFFFFF) { 10 | this.start = start; 11 | this.end = end; 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /sparkler/utils/PropertyNode.hx: -------------------------------------------------------------------------------- 1 | package sparkler.utils; 2 | 3 | class PropertyNode { 4 | 5 | public var time:Float; 6 | public var value:T; 7 | public var next:PropertyNode; 8 | 9 | public function new(time:Float, value:T) { 10 | this.time = time; 11 | this.value = value; 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sparkler", 3 | "url": "https://github.com/RudenkoArts/sparkler", 4 | "license": "MIT", 5 | "tags": ["modular", "particles", "system", "macro", "cross"], 6 | "description": "modular 2d particle system", 7 | "version": "0.0.1", 8 | "releasenote": "", 9 | "contributors": ["RudenkoArts"], 10 | "dependencies": {} 11 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /sparkler/Particle.hx: -------------------------------------------------------------------------------- 1 | package sparkler; 2 | 3 | #if !macro 4 | @:genericBuild(sparkler.utils.macro.ParticleMacro.build()) 5 | #end 6 | 7 | class Particle {} 8 | 9 | class ParticleBase { 10 | 11 | public var id(default, null):Int; 12 | public var index:Int = 0; 13 | public var x:Float = 0; 14 | public var y:Float = 0; 15 | public var lifetime:Float = -1; 16 | public var age:Float = 0; 17 | 18 | public function new(id:Int) { 19 | this.id = id; 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /sparkler/modules/helpers/UpdatePosFromVelocityModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.helpers; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.components.Velocity; 5 | import sparkler.ParticleModule; 6 | import sparkler.Particle; 7 | 8 | @priority(60) 9 | class UpdatePosFromVelocityModule extends ParticleModule> { 10 | 11 | override function onParticleUpdate(p:Particle, elapsed:Float) { 12 | p.x += p.velocity.x * elapsed; 13 | p.y += p.velocity.y * elapsed; 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /sparkler/utils/ColorFloat.hx: -------------------------------------------------------------------------------- 1 | package sparkler.utils; 2 | 3 | import sparkler.utils.Color; 4 | 5 | class ColorFloat { 6 | 7 | public var r:Float; 8 | public var g:Float; 9 | public var b:Float; 10 | public var a:Float; 11 | 12 | public function new(r:Float = 1, g:Float = 1, b:Float = 1, a:Float = 1) { 13 | this.r = r; 14 | this.g = g; 15 | this.b = b; 16 | this.a = a; 17 | } 18 | 19 | public inline function fromColor(c:Color) { 20 | r = c.r; 21 | g = c.g; 22 | b = c.b; 23 | a = c.a; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /sparkler/modules/life/LifetimeModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.life; 2 | 3 | import sparkler.ParticleModule; 4 | import sparkler.Particle; 5 | 6 | @group('lifetime') 7 | class LifetimeModule extends ParticleModule { 8 | 9 | public var lifetime:Float = 1; 10 | 11 | function new(options:{?lifetime:Float}) { 12 | if(options.lifetime != null) lifetime = options.lifetime; 13 | } 14 | 15 | override function onParticleSpawn(p:Particle) { 16 | p.lifetime = lifetime; 17 | } 18 | 19 | override function onParticleUnspawn(p:Particle) { 20 | p.lifetime = -1; 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /sparkler/modules/helpers/UpdateRotationFromAngularVelocityModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.helpers; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.components.AngularVelocity; 5 | import sparkler.components.Rotation; 6 | import sparkler.ParticleModule; 7 | import sparkler.Particle; 8 | 9 | @priority(110) 10 | class UpdateRotationFromAngularVelocityModule extends ParticleModule> { 11 | 12 | override function onParticleUpdate(p:Particle, elapsed:Float) { 13 | p.rotation += p.angularVelocity * elapsed; 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /sparkler/modules/scale/InitialScaleModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.scale; 2 | 3 | import sparkler.components.Scale; 4 | import sparkler.ParticleModule; 5 | import sparkler.Particle; 6 | 7 | @priority(100) 8 | @group('scale') 9 | class InitialScaleModule extends ParticleModule> { 10 | 11 | public var initialScale:Scale = 1.0; 12 | 13 | function new(options:{?initialScale:Float}) { 14 | if(options.initialScale != null) initialScale = options.initialScale; 15 | } 16 | 17 | override function onParticleSpawn(p:Particle) { 18 | p.scale = initialScale; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /sparkler/modules/color/InitialColorModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.color; 2 | 3 | import sparkler.utils.Color; 4 | import sparkler.ParticleModule; 5 | import sparkler.Particle; 6 | 7 | @priority(80) 8 | @group('color') 9 | class InitialColorModule extends ParticleModule> { 10 | 11 | public var initialColor:Color; 12 | 13 | function new(options:{?initialColor:sparkler.utils.Color}) { 14 | initialColor = options.initialColor != null ? options.initialColor : new Color(); 15 | } 16 | 17 | override function onParticleSpawn(p:Particle) { 18 | p.color = initialColor; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /sparkler/modules/helpers/LifeProgressModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.helpers; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.components.LifeProgress; 5 | import sparkler.components.Speed; 6 | import sparkler.ParticleModule; 7 | import sparkler.Particle; 8 | 9 | @priority(0) 10 | class LifeProgressModule extends ParticleModule> { 11 | 12 | override function onParticleSpawn(p:Particle) { 13 | p.lifeProgress = 0; 14 | } 15 | 16 | override function onParticleUpdate(p:Particle, elapsed:Float) { 17 | p.lifeProgress = p.age / p.lifetime; 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /sparkler/modules/emit/ParticlesPerEmitModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.emit; 2 | 3 | import sparkler.ParticleModule; 4 | import sparkler.Particle; 5 | 6 | @group('particlesPerEmit') 7 | class ParticlesPerEmitModule extends ParticleModule { 8 | 9 | public var particlesPerEmit:Int = 1; 10 | 11 | function new(options:{?particlesPerEmit:Int}) { 12 | if(options.particlesPerEmit != null) particlesPerEmit = options.particlesPerEmit; 13 | } 14 | 15 | override function onEmit() { 16 | var count = this.particlesPerEmit > cacheSize ? cacheSize : this.particlesPerEmit; 17 | while(count > 0) { 18 | spawn(); 19 | count--; 20 | } 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /sparkler/modules/velocity/DragModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.velocity; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.components.Velocity; 5 | import sparkler.ParticleModule; 6 | import sparkler.Particle; 7 | 8 | @priority(30) 9 | @group('drag') 10 | class DragModule extends ParticleModule> { 11 | 12 | public var drag:Float = 0; 13 | 14 | function new(options:{?drag:Float}) { 15 | if(options.drag != null) drag = options.drag; 16 | } 17 | 18 | override function onParticleUpdate(p:Particle, elapsed:Float) { 19 | var d = 1 / (1 + (elapsed * drag)); 20 | p.velocity.x *= d; 21 | p.velocity.y *= d; 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /sparkler/modules/rotate/InitialRotationModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.rotate; 2 | 3 | import sparkler.components.Rotation; 4 | import sparkler.ParticleModule; 5 | import sparkler.Particle; 6 | 7 | @priority(110) 8 | @group('rotate') 9 | class InitialRotationModule extends ParticleModule> { 10 | 11 | public var initialRotation:Float = 1.0; 12 | 13 | function new(options:{?initialRotation:Float}) { 14 | if(options.initialRotation != null) initialRotation = options.initialRotation; 15 | } 16 | 17 | @filter('rotate') // TODO: ? 18 | override function onParticleSpawn(p:Particle) { 19 | p.rotation = initialRotation; 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /sparkler/modules/spawn/CircleSpawnModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.spawn; 2 | 3 | import sparkler.ParticleModule; 4 | import sparkler.Particle; 5 | import sparkler.utils.Area; 6 | 7 | @group('spawn') 8 | class CircleSpawnModule extends ParticleModule { 9 | 10 | public var circleSpawn:Float = 32; 11 | 12 | function new(options:{?circleSpawn:Float}) { 13 | if(options.circleSpawn != null) circleSpawn = options.circleSpawn; 14 | } 15 | 16 | override function onParticleSpawn(p:Particle) { 17 | var a = random() * Math.PI * 2; 18 | var r = random() * circleSpawn; 19 | var px = Math.cos(a) * r; 20 | var py = Math.sin(a) * r; 21 | setParticlePos(p, px, py); 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /sparkler/modules/size/InitialSizeModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.size; 2 | 3 | import sparkler.components.Size; 4 | import sparkler.ParticleModule; 5 | import sparkler.Particle; 6 | import sparkler.utils.Vector2; 7 | 8 | @priority(90) 9 | @group('size') 10 | class InitialSizeModule extends ParticleModule> { 11 | 12 | public var initialSize:Vector2; 13 | 14 | function new(options:{?initialSize:{x:Float, y:Float}}) { 15 | initialSize = new Vector2(32, 32); 16 | if(options.initialSize != null) { 17 | initialSize.x = options.initialSize.x; 18 | initialSize.y = options.initialSize.y; 19 | } 20 | } 21 | 22 | override function onParticleSpawn(p:Particle) { 23 | p.size.x = initialSize.x; 24 | p.size.y = initialSize.y; 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /sparkler/modules/spawn/AreaSpawnModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.spawn; 2 | 3 | import sparkler.ParticleModule; 4 | import sparkler.Particle; 5 | import sparkler.utils.Area; 6 | 7 | @group('spawn') 8 | class AreaSpawnModule extends ParticleModule { 9 | 10 | public var areaSpawn:Area; 11 | 12 | function new(options:{?areaSpawn:{width:Float, height:Float}}) { 13 | areaSpawn = new Area(0, 0); 14 | if(options.areaSpawn != null) { 15 | areaSpawn.width = options.areaSpawn.width; 16 | areaSpawn.height = options.areaSpawn.height; 17 | } 18 | } 19 | 20 | override function onParticleSpawn(p:Particle) { 21 | var px = areaSpawn.width * 0.5 * random1To1(); 22 | var py = areaSpawn.height * 0.5 * random1To1(); 23 | setParticlePos(p, px, py); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /sparkler/modules/life/LifetimeMinMaxModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.life; 2 | 3 | import sparkler.ParticleModule; 4 | import sparkler.Particle; 5 | import sparkler.utils.Bounds; 6 | 7 | @group('lifetime') 8 | class LifetimeMinMaxModule extends ParticleModule { 9 | 10 | public var lifetimeMinMax:Bounds; 11 | 12 | function new(options:{?lifetimeMinMax:{min:Float, max:Float}}) { 13 | lifetimeMinMax = new Bounds(1.0, 1.0); 14 | if(options.lifetimeMinMax != null) { 15 | lifetimeMinMax.min = options.lifetimeMinMax.min; 16 | lifetimeMinMax.max = options.lifetimeMinMax.max; 17 | } 18 | } 19 | 20 | override function onParticleSpawn(p:Particle) { 21 | p.lifetime = randomFloat(lifetimeMinMax.min, lifetimeMinMax.max); 22 | } 23 | 24 | override function onParticleUnspawn(p:Particle) { 25 | p.lifetime = -1; 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | -------------------------------------------------------------------------------- /sparkler/modules/scale/InitialScaleMinMaxModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.scale; 2 | 3 | import sparkler.components.Scale; 4 | import sparkler.ParticleModule; 5 | import sparkler.Particle; 6 | import sparkler.utils.Bounds; 7 | 8 | @priority(100) 9 | @group('scale') 10 | class InitialScaleMinMaxModule extends ParticleModule> { 11 | 12 | public var initialScaleMinMax:Bounds; 13 | 14 | function new(options:{?initialScaleMinMax:{min:Float, max:Float}}) { 15 | initialScaleMinMax = new Bounds(1.0, 1.0); 16 | if(options.initialScaleMinMax != null) { 17 | initialScaleMinMax.min = options.initialScaleMinMax.min; 18 | initialScaleMinMax.max = options.initialScaleMinMax.max; 19 | } 20 | } 21 | 22 | override function onParticleSpawn(p:Particle) { 23 | p.scale = randomFloat(initialScaleMinMax.min, initialScaleMinMax.max); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /sparkler/modules/helpers/UpdateSpeedModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.helpers; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.components.PrevPos; 5 | import sparkler.components.Speed; 6 | import sparkler.ParticleModule; 7 | import sparkler.Particle; 8 | 9 | @priority(70) 10 | class UpdateSpeedModule extends ParticleModule> { 11 | 12 | override function onParticleSpawn(p:Particle) { 13 | p.prevPos.x = p.x; 14 | p.prevPos.y = p.y; 15 | p.speed = 0; 16 | } 17 | 18 | override function onPreParticleUpdate(p:Particle, elapsed:Float) { 19 | p.prevPos.x = p.x; 20 | p.prevPos.y = p.y; 21 | } 22 | 23 | override function onParticleUpdate(p:Particle, elapsed:Float) { 24 | var dx = p.x - p.prevPos.x; 25 | var dy = p.y - p.prevPos.y; 26 | p.speed = Math.sqrt(dx * dx + dy * dy) / elapsed; 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Andrei Rudenko 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. -------------------------------------------------------------------------------- /sparkler/modules/rotate/InitialRotationMinMaxModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.rotate; 2 | 3 | import sparkler.components.Rotation; 4 | import sparkler.ParticleModule; 5 | import sparkler.Particle; 6 | import sparkler.utils.Bounds; 7 | 8 | @priority(110) 9 | @group('rotation') 10 | class InitialRotationMinMaxModule extends ParticleModule> { 11 | 12 | public var initialRotationMinMax:Bounds; 13 | 14 | function new(options:{?initialRotationMinMax:{min:Float, max:Float}}) { 15 | initialRotationMinMax = new Bounds(1.0, 1.0); 16 | if(options.initialRotationMinMax != null) { 17 | initialRotationMinMax.min = options.initialRotationMinMax.min; 18 | initialRotationMinMax.max = options.initialRotationMinMax.max; 19 | } 20 | } 21 | 22 | override function onParticleSpawn(p:Particle) { 23 | p.rotation = randomFloat(initialRotationMinMax.min, initialRotationMinMax.max); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /sparkler/modules/rotate/AngularVelocityModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.rotate; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.components.AngularVelocity; 5 | import sparkler.components.Rotation; 6 | import sparkler.ParticleModule; 7 | import sparkler.Particle; 8 | 9 | @priority(40) 10 | @group('angularVelocity') 11 | @addModules(sparkler.modules.helpers.UpdateRotationFromAngularVelocityModule) 12 | class AngularVelocityModule extends ParticleModule> { 13 | 14 | public var angularVelocity:Float = 90; 15 | 16 | function new(options:{?angularVelocity:Float}) { 17 | if(options.angularVelocity != null) angularVelocity = options.angularVelocity; 18 | } 19 | 20 | override function onParticleSpawn(p:Particle) { 21 | p.angularVelocity = angularVelocity; 22 | } 23 | 24 | override function onParticleUnspawn(p:Particle) { 25 | p.rotation = 0; 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /sparkler/modules/color/InitialColorMinMaxModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.color; 2 | 3 | import sparkler.utils.Color; 4 | import sparkler.ParticleModule; 5 | import sparkler.Particle; 6 | import sparkler.utils.Bounds; 7 | 8 | @priority(80) 9 | @group('color') 10 | class InitialColorMinMaxModule extends ParticleModule> { 11 | 12 | public var initialColorMinMax:Bounds; 13 | 14 | function new(options:{?initialColorMinMax:{min:sparkler.utils.Color, max:sparkler.utils.Color}}) { 15 | initialColorMinMax = new Bounds(new Color(), new Color()); 16 | if(options.initialColorMinMax != null) { 17 | initialColorMinMax.min = options.initialColorMinMax.min; 18 | initialColorMinMax.max = options.initialColorMinMax.max; 19 | } 20 | } 21 | 22 | inline function randomColor(a:Color, b:Color):Color { 23 | return Color.lerp(a, b, random()); 24 | } 25 | 26 | override function onParticleSpawn(p:Particle) { 27 | p.color = randomColor(initialColorMinMax.min, initialColorMinMax.max); 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /sparkler/modules/velocity/InitialVelocityModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.velocity; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.components.Velocity; 5 | import sparkler.ParticleModule; 6 | import sparkler.Particle; 7 | 8 | @priority(10) 9 | @group('velocity') 10 | @addModules(sparkler.modules.helpers.UpdatePosFromVelocityModule) 11 | class InitialVelocityModule extends ParticleModule> { 12 | 13 | public var initialVelocity:Vector2; 14 | 15 | function new(options:{?initialVelocity:{x:Float, y:Float}}) { 16 | initialVelocity = options.initialVelocity != null ? new Vector2(options.initialVelocity.x, options.initialVelocity.y) : new Vector2(0, 0); 17 | } 18 | 19 | override function onParticleSpawn(p:Particle) { 20 | if(localSpace) { 21 | p.velocity.x = initialVelocity.x; 22 | p.velocity.y = initialVelocity.y; 23 | } else { 24 | p.velocity.x = getRotateX(initialVelocity.x, initialVelocity.y); 25 | p.velocity.y = getRotateY(initialVelocity.x, initialVelocity.y); 26 | } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /sparkler/modules/velocity/ForceModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.velocity; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.components.Velocity; 5 | import sparkler.ParticleModule; 6 | import sparkler.Particle; 7 | 8 | @priority(20) 9 | @group('force') 10 | @addModules(sparkler.modules.helpers.UpdatePosFromVelocityModule) 11 | class ForceModule extends ParticleModule> { 12 | 13 | public var force:Vector2; 14 | 15 | function new(options:{?force:{x:Float, y:Float}}) { 16 | force = options.force != null ? new Vector2(options.force.x, options.force.y) : new Vector2(0, 0); 17 | } 18 | 19 | override function onParticleUpdate(p:Particle, elapsed:Float) { 20 | if(localSpace) { 21 | p.velocity.x += force.x * elapsed; 22 | p.velocity.y += force.y * elapsed; 23 | } else { 24 | p.velocity.x += getRotateX(force.x, force.y) * elapsed; 25 | p.velocity.y += getRotateY(force.x, force.y) * elapsed; 26 | } 27 | } 28 | 29 | @filter('reset velocity') 30 | override function onParticleUnspawn(p:Particle) { 31 | p.velocity.x = 0; 32 | p.velocity.y = 0; 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Sparkler 2 | A modular 2d particle system. 3 | 4 | ### Features 5 | * Macro-powered. 6 | * Customizable. 7 | * You can create custom `ParticleModule` to extend functionality. 8 | * Local/World Space rendering. 9 | * Multiple frameworks support: [Kha](https://github.com/Kode/Kha) and [Clay](https://github.com/clay2d/clay) for now. 10 | 11 | ### Sample Usage 12 | 13 | ```haxe 14 | 15 | var emitter = new ParticleEmitter< 16 | ColorListOverLifetimeModule, 17 | InitialVelocityModule, 18 | KhaSpriteRendererModule 19 | >({ 20 | cacheSize: 512, 21 | lifetime: 2, 22 | emitterLifetime: 5, 23 | emitRate: 50, 24 | colorListOverLifetime: { 25 | ease: null, 26 | list: [ 27 | { 28 | time: 0, 29 | value: 0xFFFF0000 30 | }, 31 | { 32 | time: 0.5, 33 | value: 0xFFFF00FF 34 | }, 35 | { 36 | time: 1, 37 | value: 0x00FF00FF 38 | } 39 | ] 40 | }, 41 | initialVelocity: {x: 0, y: -200}, 42 | khaSpriteRenderer: { 43 | image: kha.Assets.images.particle 44 | }, 45 | sortFunc: function(a, b) { 46 | return a.age < b.age ? 1 : -1; 47 | } 48 | }); 49 | 50 | emitter.start(); 51 | 52 | 53 | ``` -------------------------------------------------------------------------------- /sparkler/modules/size/InitialSizeMinMaxModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.size; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.components.Size; 5 | import sparkler.ParticleModule; 6 | import sparkler.Particle; 7 | import sparkler.utils.Bounds; 8 | 9 | @priority(90) 10 | @group('size') 11 | class InitialSizeMinMaxModule extends ParticleModule> { 12 | 13 | public var initialSizeMinMax:Bounds; 14 | 15 | function new(options:{?initialSizeMinMax:{min:{x:Float, y:Float}, max:{x:Float, y:Float}}}) { 16 | initialSizeMinMax = new Bounds(new Vector2(), new Vector2()); 17 | if(options.initialSizeMinMax != null) { 18 | initialSizeMinMax.min.x = options.initialSizeMinMax.min.x; 19 | initialSizeMinMax.min.y = options.initialSizeMinMax.min.y; 20 | initialSizeMinMax.max.x = options.initialSizeMinMax.max.x; 21 | initialSizeMinMax.max.y = options.initialSizeMinMax.max.y; 22 | } 23 | } 24 | 25 | override function onParticleSpawn(p:Particle) { 26 | p.size.x = randomFloat(initialSizeMinMax.min.x, initialSizeMinMax.max.x); 27 | p.size.y = randomFloat(initialSizeMinMax.min.y, initialSizeMinMax.max.y); 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /sparkler/modules/life/EmitterLifetimeModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.life; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.components.Velocity; 5 | import sparkler.ParticleModule; 6 | import sparkler.Particle; 7 | 8 | @group('emitterLife') 9 | class EmitterLifetimeModule extends ParticleModule { 10 | 11 | public var emitterLifetime:Float = 5; 12 | var _emTime:Float = 0; 13 | 14 | function new(options:{?emitterLifetime:Float}) { 15 | if(options.emitterLifetime != null) emitterLifetime = options.emitterLifetime; 16 | } 17 | 18 | override function onStart() { 19 | _emTime = 0; 20 | } 21 | 22 | override function onUpdate(elapsed:Float) { 23 | while(enabled && _emTime + elapsed >= emitterLifetime) { 24 | step(emitterLifetime - _emTime); 25 | elapsed -= (emitterLifetime - _emTime); 26 | _emTime = emitterLifetime; 27 | progress = 1; 28 | if(loops >= 0 && _loopsCounter >= loops) { 29 | stop(); 30 | break; 31 | } 32 | _loopsCounter++; 33 | restart(); 34 | } 35 | 36 | if(elapsed > 0) { 37 | step(elapsed); 38 | if(enabled) { 39 | _emTime += elapsed; 40 | progress = _emTime / emitterLifetime; 41 | } 42 | } 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /sparkler/modules/velocity/DirectionModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.velocity; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.utils.Maths; 5 | import sparkler.components.Velocity; 6 | import sparkler.ParticleModule; 7 | import sparkler.Particle; 8 | import sparkler.modules.velocity.DirectionModule.Direction; 9 | 10 | class Direction { 11 | 12 | public var angle:Float = 0; 13 | public var speed:Float = 128; 14 | 15 | public function new() {} 16 | 17 | } 18 | 19 | @priority(10) 20 | @group('velocity') 21 | @addModules(sparkler.modules.helpers.UpdatePosFromVelocityModule) 22 | class DirectionModule extends ParticleModule> { 23 | 24 | public var direction:Direction; 25 | 26 | function new(options:{?direction:{angle:Float, speed:Float}}) { 27 | direction = new Direction(); 28 | if(options.direction != null) { 29 | direction.angle = options.direction.angle; 30 | direction.speed = options.direction.speed; 31 | } 32 | } 33 | 34 | override function onParticleSpawn(p:Particle) { 35 | var angle:Float = Maths.radians(direction.angle); 36 | 37 | if(!localSpace) angle += this._rotation; 38 | 39 | p.velocity.x = direction.speed * Math.cos(angle); 40 | p.velocity.y = direction.speed * Math.sin(angle); 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /sparkler/modules/scale/ScaleListOverLifetimeModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.scale; 2 | 3 | import sparkler.components.LifeProgress; 4 | import sparkler.components.ScalePropertyList; 5 | import sparkler.components.Scale; 6 | import sparkler.ParticleModule; 7 | import sparkler.Particle; 8 | 9 | @priority(100) 10 | @group('scale') 11 | @addModules(sparkler.modules.helpers.LifeProgressModule) 12 | class ScaleListOverLifetimeModule extends ParticleModule> { 13 | 14 | public var scaleListOverLifetime:ScalePropertyList; 15 | 16 | function new(options:{?scaleListOverLifetime:{?ease:(v:Float)->Float, list:Array<{time:Float, value:Float}>}}) { 17 | if(options.scaleListOverLifetime != null) { 18 | scaleListOverLifetime = ScalePropertyList.create(options.scaleListOverLifetime.list); 19 | scaleListOverLifetime.ease = options.scaleListOverLifetime.ease; 20 | } else { 21 | scaleListOverLifetime = new ScalePropertyList(); 22 | } 23 | } 24 | 25 | override function onParticleUpdate(p:Particle, elapsed:Float) { 26 | p.scalePropertyList.interpolate(p.lifeProgress); 27 | p.scale = p.scalePropertyList.value; 28 | } 29 | override function onParticleSpawn(p:Particle) { 30 | p.scalePropertyList.set(scaleListOverLifetime); 31 | p.scale = p.scalePropertyList.value; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /sparkler/modules/color/ColorListOverLifetimeModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.color; 2 | 3 | import sparkler.components.ColorPropertyList; 4 | import sparkler.components.LifeProgress; 5 | import sparkler.components.Color; 6 | import sparkler.ParticleModule; 7 | import sparkler.Particle; 8 | 9 | @priority(80) 10 | @group('color') 11 | @addModules(sparkler.modules.helpers.LifeProgressModule) 12 | class ColorListOverLifetimeModule extends ParticleModule> { 13 | 14 | public var colorListOverLifetime:ColorPropertyList; 15 | 16 | function new(options:{?colorListOverLifetime:{?ease:(v:Float)->Float, list:Array<{time:Float, value:sparkler.utils.Color}>}}) { 17 | if(options.colorListOverLifetime != null) { 18 | colorListOverLifetime = ColorPropertyList.create(options.colorListOverLifetime.list); 19 | colorListOverLifetime.ease = options.colorListOverLifetime.ease; 20 | } else { 21 | colorListOverLifetime = new ColorPropertyList(); 22 | } 23 | } 24 | 25 | override function onParticleSpawn(p:Particle) { 26 | p.colorPropertyList.set(colorListOverLifetime); 27 | p.color = p.colorPropertyList.value; 28 | } 29 | 30 | override function onParticleUpdate(p:Particle, elapsed:Float) { 31 | p.colorPropertyList.interpolate(p.lifeProgress); 32 | p.color = p.colorPropertyList.value; 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /sparkler/modules/velocity/InitialVelocityMinMaxModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.velocity; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.components.Velocity; 5 | import sparkler.ParticleModule; 6 | import sparkler.Particle; 7 | import sparkler.utils.Bounds; 8 | 9 | @priority(10) 10 | @group('velocity') 11 | @addModules(sparkler.modules.helpers.UpdatePosFromVelocityModule) 12 | class InitialVelocityMinMaxModule extends ParticleModule> { 13 | 14 | public var initialVelocityMinMax:Bounds; 15 | 16 | function new(options:{?initialVelocityMinMax:{min:{x:Float, y:Float}, max:{x:Float, y:Float}}}) { 17 | initialVelocityMinMax = new Bounds(new Vector2(), new Vector2()); 18 | if(options.initialVelocityMinMax != null) { 19 | initialVelocityMinMax.min.x = options.initialVelocityMinMax.min.x; 20 | initialVelocityMinMax.min.y = options.initialVelocityMinMax.min.y; 21 | initialVelocityMinMax.max.x = options.initialVelocityMinMax.max.x; 22 | initialVelocityMinMax.max.y = options.initialVelocityMinMax.max.y; 23 | } 24 | } 25 | 26 | override function onParticleSpawn(p:Particle) { 27 | var rx = randomFloat(initialVelocityMinMax.min.x, initialVelocityMinMax.max.x); 28 | var ry = randomFloat(initialVelocityMinMax.min.y, initialVelocityMinMax.max.y); 29 | 30 | if(localSpace) { 31 | p.velocity.x = rx; 32 | p.velocity.y = ry; 33 | } else { 34 | p.velocity.x = getRotateX(rx, ry); 35 | p.velocity.y = getRotateY(rx, ry); 36 | } 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /sparkler/utils/FloatPropertyList.hx: -------------------------------------------------------------------------------- 1 | package sparkler.utils; 2 | 3 | import sparkler.utils.Color; 4 | 5 | class FloatPropertyList { 6 | 7 | public static function create(items:Array<{time:Float, value:Float}>):FloatPropertyList { 8 | var list = new FloatPropertyList(); 9 | 10 | if(items.length > 0) { 11 | items.sort(function(a, b) { 12 | return a.time - b.time > 0 ? 1 : -1; 13 | }); 14 | 15 | var item = items[0]; 16 | list.head = new PropertyNode(item.time, item.value); 17 | var node = list.head; 18 | var i:Int = 1; 19 | while(i < items.length) { 20 | item = items[i]; 21 | node.next = new PropertyNode(item.time, item.value); 22 | node = node.next; 23 | i++; 24 | } 25 | } 26 | 27 | return list; 28 | } 29 | 30 | var head:PropertyNode; 31 | var current:PropertyNode; 32 | var next:PropertyNode; 33 | 34 | public var value(default, null):Float = 0; 35 | public var ease:(v:Float)->Float; 36 | 37 | public function new() {} 38 | 39 | public function interpolate(lerp:Float) { 40 | if(ease != null) lerp = ease(lerp); 41 | 42 | while (lerp > this.next.time) { 43 | this.current = this.next; 44 | this.next = this.next.next; 45 | } 46 | 47 | lerp = (lerp - this.current.time) / (this.next.time - this.current.time); 48 | 49 | value = this.current.value + (this.next.value - this.current.value) * lerp; 50 | } 51 | 52 | public function set(p:FloatPropertyList) { 53 | current = p.head; 54 | next = current.next; 55 | value = current.value; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /sparkler/utils/ColorPropertyList.hx: -------------------------------------------------------------------------------- 1 | package sparkler.utils; 2 | 3 | import sparkler.utils.Color; 4 | 5 | class ColorPropertyList { 6 | 7 | public static function create(items:Array<{time:Float, value:Color}>):ColorPropertyList { 8 | var list = new ColorPropertyList(); 9 | 10 | if(items.length > 0) { 11 | items.sort(function(a, b) { 12 | return a.time - b.time > 0 ? 1 : -1; 13 | }); 14 | 15 | var item = items[0]; 16 | list.head = new PropertyNode(item.time, item.value); 17 | var node = list.head; 18 | var i:Int = 1; 19 | while(i < items.length) { 20 | item = items[i]; 21 | node.next = new PropertyNode(item.time, item.value); 22 | node = node.next; 23 | i++; 24 | } 25 | } 26 | 27 | return list; 28 | } 29 | 30 | var head:PropertyNode; 31 | var current:PropertyNode; 32 | var next:PropertyNode; 33 | 34 | public var value(default, null):Color; 35 | public var ease:(v:Float)->Float; 36 | 37 | public function new() { 38 | value = new Color(); 39 | } 40 | 41 | public function interpolate(lerp:Float) { 42 | if(ease != null) lerp = ease(lerp); 43 | 44 | while (lerp > this.next.time) { 45 | this.current = this.next; 46 | this.next = this.next.next; 47 | } 48 | 49 | lerp = (lerp - this.current.time) / (this.next.time - this.current.time); 50 | 51 | value = Color.lerp(this.current.value, this.next.value, lerp); 52 | } 53 | 54 | public function set(p:ColorPropertyList) { 55 | current = p.head; 56 | next = current.next; 57 | value = current.value; 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /sparkler/modules/scale/ScaleOverLifetimeModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.scale; 2 | 3 | import sparkler.utils.Maths; 4 | import sparkler.components.Scale; 5 | import sparkler.components.LifeProgress; 6 | import sparkler.ParticleModule; 7 | import sparkler.Particle; 8 | import sparkler.modules.scale.ScaleOverLifetimeModule.ScaleOverLifetime; 9 | 10 | class ScaleOverLifetime { 11 | 12 | public var start:Float = 1; 13 | public var end:Float = 1; 14 | public var ease:(v:Float)->Float; 15 | 16 | public function new() {} 17 | 18 | } 19 | 20 | @priority(100) 21 | @group('scale') 22 | @addModules(sparkler.modules.helpers.LifeProgressModule) 23 | class ScaleOverLifetimeModule extends ParticleModule> { 24 | 25 | public var scaleOverLifetime:ScaleOverLifetime; 26 | 27 | function new(options:{?scaleOverLifetime:{?ease:(v:Float)->Float, start:Float, end:Float}}) { 28 | scaleOverLifetime = new ScaleOverLifetime(); 29 | 30 | if(options.scaleOverLifetime != null) { 31 | scaleOverLifetime.start = options.scaleOverLifetime.start; 32 | scaleOverLifetime.end = options.scaleOverLifetime.end; 33 | scaleOverLifetime.ease = options.scaleOverLifetime.ease; 34 | } 35 | } 36 | 37 | override function onParticleUpdate(p:Particle, elapsed:Float) { 38 | p.scale = Maths.lerp(scaleOverLifetime.start, scaleOverLifetime.end, scaleOverLifetime.ease != null ? scaleOverLifetime.ease(p.lifeProgress) : p.lifeProgress); 39 | } 40 | 41 | override function onParticleSpawn(p:Particle) { 42 | p.scale = scaleOverLifetime.start; 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /sparkler/utils/Maths.hx: -------------------------------------------------------------------------------- 1 | package sparkler.utils; 2 | 3 | class Maths { 4 | 5 | static public inline var TAU = 6.28318530717958648; 6 | static public inline var DEG2RAD:Float = 6.28318530717958648 / 360; 7 | static public inline var RAD2DEG:Float = 360 / 6.28318530717958648; 8 | static public inline var EPSILON:Float = 1e-10; 9 | 10 | static public inline function lerp(start:Float, end:Float, t:Float):Float { 11 | return start + t * (end - start); 12 | } 13 | 14 | static public inline function inverseLerp(start:Float, end:Float, value:Float):Float { 15 | return end == start ? 0.0 : (value - start) / (end - start); 16 | } 17 | 18 | static public inline function clamp(value:Float, a:Float, b:Float):Float { 19 | return ( value < a ) ? a : ( ( value > b ) ? b : value ); 20 | } 21 | 22 | static public inline function map(istart:Float, iend:Float, ostart:Float, oend:Float, value:Float):Float { 23 | return ostart + (oend - ostart) * ((value - istart) / (iend - istart)); 24 | } 25 | 26 | static public inline function imap(istart:Int, iend:Int, ostart:Int, oend:Int, value:Float):Int { 27 | return Std.int(map(istart, iend, ostart, oend, value)); 28 | } 29 | 30 | static public inline function distanceSq(dx:Float, dy:Float) { 31 | return dx * dx + dy * dy; 32 | } 33 | 34 | static public inline function distance(dx:Float, dy:Float) { 35 | return Math.sqrt(distanceSq(dx,dy)); 36 | } 37 | 38 | static public inline function radians(degrees:Float):Float { 39 | return degrees * DEG2RAD; 40 | } 41 | 42 | static public inline function degrees(radians:Float):Float { 43 | return radians * RAD2DEG; 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /sparkler/modules/rotate/RotateOverLifetimeModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.rotate; 2 | 3 | import sparkler.utils.Maths; 4 | import sparkler.components.LifeProgress; 5 | import sparkler.components.Rotation; 6 | import sparkler.ParticleModule; 7 | import sparkler.Particle; 8 | import sparkler.modules.rotate.RotateOverLifetimeModule.RotateOverLifetime; 9 | 10 | class RotateOverLifetime { 11 | 12 | public var start:Float = 0; 13 | public var end:Float = 0; 14 | public var ease:(v:Float)->Float; 15 | 16 | public function new() {} 17 | 18 | } 19 | 20 | @priority(110) 21 | @group('rotate') 22 | @addModules(sparkler.modules.helpers.LifeProgressModule) 23 | class RotateOverLifetimeModule extends ParticleModule> { 24 | 25 | public var rotateOverLifetime:RotateOverLifetime; 26 | 27 | function new(options:{?rotateOverLifetime:{?ease:(v:Float)->Float, start:Float, end:Float}}) { 28 | rotateOverLifetime = new RotateOverLifetime(); 29 | if(options.rotateOverLifetime != null) { 30 | rotateOverLifetime.start = options.rotateOverLifetime.start; 31 | rotateOverLifetime.end = options.rotateOverLifetime.end; 32 | rotateOverLifetime.ease = options.rotateOverLifetime.ease; 33 | } 34 | } 35 | 36 | override function onParticleUpdate(p:Particle, elapsed:Float) { 37 | p.rotation = Maths.lerp(rotateOverLifetime.start, rotateOverLifetime.end, rotateOverLifetime.ease != null ? rotateOverLifetime.ease(p.lifeProgress) : p.lifeProgress); 38 | } 39 | 40 | override function onParticleSpawn(p:Particle) { 41 | p.rotation = rotateOverLifetime.start; 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /sparkler/modules/emit/EmitRateModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.emit; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.utils.Maths; 5 | import sparkler.components.Velocity; 6 | import sparkler.ParticleModule; 7 | import sparkler.Particle; 8 | 9 | @group('emit') 10 | class EmitRateModule extends ParticleModule { 11 | 12 | public var emitRate:Float = 40; 13 | var _frameOffset:Float = 0; 14 | 15 | function new(options:{?emitRate:Float}) { 16 | if(options.emitRate != null) emitRate = options.emitRate; 17 | } 18 | 19 | override function onStart() { 20 | _frameOffset = 0; 21 | } 22 | 23 | override function onStep(elapsed:Float) { 24 | var px:Float = _x; 25 | var py:Float = _y; 26 | 27 | var ft:Float = 0; 28 | var lt:Float = 0; 29 | 30 | if(enabled && emitRate > 0) { 31 | var invRate = 1 / emitRate; 32 | var t:Float = 0; 33 | while (_frameOffset + elapsed >= invRate) { // TODO: test >= 34 | t = invRate - _frameOffset; 35 | ft += t; 36 | if(!localSpace) { 37 | lt = ft / _frameTime; 38 | _x = Maths.lerp(_lastX, px, lt); 39 | _y = Maths.lerp(_lastY, py, lt); 40 | } 41 | updateParticles(t); 42 | elapsed -= t; 43 | _frameOffset = 0; 44 | emit(); 45 | } 46 | } 47 | 48 | if (elapsed > 0) { 49 | ft += elapsed; 50 | if(!localSpace) { 51 | lt = ft / _frameTime; 52 | _x = Maths.lerp(_lastX, px, ft / _frameTime); 53 | _y = Maths.lerp(_lastY, py, ft / _frameTime); 54 | } 55 | updateParticles(elapsed); 56 | if (enabled && emitRate > 0) { 57 | _frameOffset += elapsed; 58 | } 59 | } 60 | 61 | _x = px; 62 | _y = py; 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /sparkler/modules/color/ColorOverLifetimeModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.color; 2 | 3 | import sparkler.components.Color; 4 | import sparkler.components.LifeProgress; 5 | import sparkler.ParticleModule; 6 | import sparkler.Particle; 7 | import sparkler.modules.color.ColorOverLifetimeModule.ColorOverLifetime; 8 | 9 | class ColorOverLifetime { 10 | 11 | public var start:Color; 12 | public var end:Color; 13 | public var ease:(v:Float)->Float; 14 | 15 | public function new() {} 16 | 17 | } 18 | 19 | @priority(80) 20 | @group('color') 21 | @addModules(sparkler.modules.helpers.LifeProgressModule) 22 | class ColorOverLifetimeModule extends ParticleModule> { 23 | 24 | public var colorOverLifetime:ColorOverLifetime; 25 | 26 | function new(options:{?colorOverLifetime:{?ease:(v:Float)->Float, start:sparkler.utils.Color, end:sparkler.utils.Color}}) { 27 | colorOverLifetime = new ColorOverLifetime(); 28 | 29 | if(options.colorOverLifetime != null) { 30 | colorOverLifetime.start = options.colorOverLifetime.start; 31 | colorOverLifetime.end = options.colorOverLifetime.end; 32 | colorOverLifetime.ease = options.colorOverLifetime.ease; 33 | } else { 34 | colorOverLifetime.start = new Color(); 35 | colorOverLifetime.end = new Color(); 36 | } 37 | } 38 | 39 | override function onParticleUpdate(p:Particle, elapsed:Float) { 40 | p.color = Color.lerp(colorOverLifetime.start, colorOverLifetime.end, colorOverLifetime.ease != null ? colorOverLifetime.ease(p.lifeProgress) : p.lifeProgress); 41 | } 42 | 43 | override function onParticleSpawn(p:Particle) { 44 | p.color = colorOverLifetime.start; 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /sparkler/modules/life/EmitterLifeDistanceModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.life; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.components.Velocity; 5 | import sparkler.ParticleModule; 6 | import sparkler.Particle; 7 | 8 | @group('emitterLife') 9 | class EmitterLifeDistanceModule extends ParticleModule { 10 | 11 | public var emitterLifeDistance:Float = 1000; 12 | var _emDist:Float = 0; 13 | 14 | function new(options:{?emitterLifeDistance:Float}) { 15 | if(options.emitterLifeDistance != null) emitterLifeDistance = options.emitterLifeDistance; 16 | } 17 | 18 | override function onStart() { 19 | _emDist = 0; 20 | } 21 | 22 | override function onUpdate(elapsed:Float) { 23 | if(enabled && !localSpace && (_lastX != _x || _lastY != _y)) { 24 | var dx = _x - _lastX; 25 | var dy = _y - _lastY; 26 | var currentDistance:Float = Math.sqrt(dx * dx + dy * dy); 27 | var framePos:Float = elapsed / _frameTime; 28 | var f = elapsed / currentDistance; 29 | var distLeft = currentDistance; 30 | var frameTime:Float = 0; 31 | 32 | var d:Float = 0; 33 | while(_emDist + distLeft >= emitterLifeDistance) { 34 | d = emitterLifeDistance - _emDist; 35 | frameTime = d * f; 36 | step(frameTime); 37 | elapsed -= frameTime; 38 | distLeft -= d; 39 | _emDist = 0; 40 | progress = 1; 41 | if(loops >= 0 && _loopsCounter >= loops) { 42 | stop(); 43 | break; 44 | } 45 | _loopsCounter++; 46 | restart(); 47 | } 48 | 49 | if(elapsed > 0) { 50 | step(elapsed); 51 | _emDist += distLeft; 52 | progress = _emDist / emitterLifeDistance; 53 | } 54 | } else { 55 | step(elapsed); 56 | } 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /sparkler/modules/velocity/DirectionMinMaxModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.velocity; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.utils.Maths; 5 | import sparkler.utils.Bounds; 6 | import sparkler.components.Velocity; 7 | import sparkler.ParticleModule; 8 | import sparkler.Particle; 9 | import sparkler.modules.velocity.DirectionMinMaxModule.DirectionMinMax; 10 | 11 | class DirectionMinMax { 12 | 13 | public var angle:Bounds = new Bounds(0.0, 0.0); 14 | public var speed:Bounds = new Bounds(64.0, 128.0); 15 | 16 | public function new() {} 17 | 18 | } 19 | 20 | @priority(10) 21 | @group('velocity') 22 | @addModules(sparkler.modules.helpers.UpdatePosFromVelocityModule) 23 | class DirectionMinMaxModule extends ParticleModule> { 24 | 25 | public var directionMinMax:DirectionMinMax; 26 | 27 | function new(options:{?directionMinMax:{angle:{min:Float, max:Float}, speed:{min:Float, max:Float}}}) { 28 | directionMinMax = new DirectionMinMax(); 29 | if(options.directionMinMax != null) { 30 | directionMinMax.angle.min = options.directionMinMax.angle.min; 31 | directionMinMax.angle.max = options.directionMinMax.angle.max; 32 | directionMinMax.speed.min = options.directionMinMax.speed.min; 33 | directionMinMax.speed.max = options.directionMinMax.speed.max; 34 | } 35 | } 36 | 37 | override function onParticleSpawn(p:Particle) { 38 | var dAngle:Float = Maths.radians(randomFloat(directionMinMax.angle.min, directionMinMax.angle.max)); 39 | var dSpeed:Float = randomFloat(directionMinMax.speed.min, directionMinMax.speed.max); 40 | if(!localSpace) dAngle += _rotation; 41 | 42 | p.velocity.x = dSpeed * Math.cos(dAngle); 43 | p.velocity.y = dSpeed * Math.sin(dAngle); 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /sparkler/utils/Vector2PropertyList.hx: -------------------------------------------------------------------------------- 1 | package sparkler.utils; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.utils.Maths; 5 | 6 | class Vector2PropertyList { 7 | 8 | public static function create(items:Array<{time:Float, value:Vector2}>):Vector2PropertyList { 9 | var list = new Vector2PropertyList(); 10 | 11 | if(items.length > 0) { 12 | items.sort(function(a, b) { 13 | return a.time - b.time > 0 ? 1 : -1; 14 | }); 15 | 16 | var item = items[0]; 17 | list.head = new PropertyNode(item.time, item.value); 18 | var node = list.head; 19 | var i:Int = 1; 20 | while(i < items.length) { 21 | item = items[i]; 22 | node.next = new PropertyNode(item.time, item.value); 23 | node = node.next; 24 | i++; 25 | } 26 | } 27 | 28 | return list; 29 | } 30 | 31 | var head:PropertyNode; 32 | var current:PropertyNode; 33 | var next:PropertyNode; 34 | 35 | public var value(default, null):Vector2; 36 | public var ease:(v:Float)->Float; 37 | 38 | public function new() { 39 | value = new Vector2(); 40 | } 41 | 42 | public function interpolate(lerp:Float) { 43 | if(ease != null) lerp = ease(lerp); 44 | 45 | while (lerp > this.next.time) { 46 | this.current = this.next; 47 | this.next = this.next.next; 48 | } 49 | 50 | lerp = (lerp - this.current.time) / (this.next.time - this.current.time); 51 | 52 | value.x = Maths.lerp(this.current.value.x, this.next.value.x, lerp); 53 | value.y = Maths.lerp(this.current.value.y, this.next.value.y, lerp); 54 | } 55 | 56 | public function set(p:Vector2PropertyList) { 57 | current = p.head; 58 | next = current.next; 59 | value.x = current.value.x; 60 | value.y = current.value.y; 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /sparkler/modules/move/MovementListOverLifetimeModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.move; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.utils.Maths; 5 | import sparkler.utils.Vector2PropertyList; 6 | import sparkler.components.Velocity; 7 | import sparkler.components.LifeProgress; 8 | import sparkler.ParticleModule; 9 | import sparkler.Particle; 10 | 11 | @priority(65) 12 | @group('move') 13 | @addModules(sparkler.modules.helpers.LifeProgressModule) 14 | class MovementListOverLifetimeModule extends ParticleModule> { 15 | 16 | public var movementListOverLifetime:Vector2PropertyList; 17 | 18 | function new(options:{?movementListOverLifetime:{?ease:(v:Float)->Float, list:Array<{time:Float, value:sparkler.utils.Vector2}>}}) { 19 | if(options.movementListOverLifetime != null) { 20 | movementListOverLifetime = Vector2PropertyList.create(options.movementListOverLifetime.list); 21 | movementListOverLifetime.ease = options.movementListOverLifetime.ease; 22 | } else { 23 | movementListOverLifetime = new Vector2PropertyList(); 24 | } 25 | } 26 | 27 | override function onParticleSpawn(p:Particle) { 28 | p.vector2PropertyList.set(movementListOverLifetime); 29 | } 30 | 31 | override function onParticleUpdate(p:Particle, elapsed:Float) { 32 | p.vector2PropertyList.interpolate(p.lifeProgress); 33 | if(localSpace) { 34 | p.x += p.vector2PropertyList.value.x * elapsed; 35 | p.y += p.vector2PropertyList.value.y * elapsed; 36 | } else { 37 | p.x += getRotateX(p.vector2PropertyList.value.x, p.vector2PropertyList.value.y) * elapsed; 38 | p.y += getRotateY(p.vector2PropertyList.value.x, p.vector2PropertyList.value.y) * elapsed; 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /sparkler/modules/scale/ScaleBySpeedModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.scale; 2 | 3 | import sparkler.components.Speed; 4 | import sparkler.components.Scale; 5 | import sparkler.utils.Maths; 6 | import sparkler.ParticleModule; 7 | import sparkler.Particle; 8 | import sparkler.modules.scale.ScaleBySpeedModule.ScaleBySpeed; 9 | 10 | class ScaleBySpeed { 11 | 12 | public var ease:(v:Float)->Float; 13 | 14 | public var minScale:Float = 1; 15 | public var maxScale:Float = 1; 16 | 17 | public var minSpeed:Float = 0; 18 | public var maxSpeed:Float = 128; 19 | 20 | public function new() {} 21 | 22 | } 23 | 24 | @priority(100) 25 | @group('scale') 26 | @addModules(sparkler.modules.helpers.UpdateSpeedModule) 27 | class ScaleBySpeedModule extends ParticleModule> { 28 | 29 | public var scaleBySpeed:ScaleBySpeed; 30 | 31 | function new(options:{ 32 | ?scaleBySpeed:{ 33 | minScale:Float, 34 | maxScale:Float, 35 | minSpeed:Float, 36 | maxSpeed:Float, 37 | ?ease:(v:Float)->Float 38 | } 39 | }) { 40 | scaleBySpeed = new ScaleBySpeed(); 41 | if(options.scaleBySpeed != null) { 42 | scaleBySpeed.minScale = options.scaleBySpeed.minScale; 43 | scaleBySpeed.maxScale = options.scaleBySpeed.maxScale; 44 | scaleBySpeed.minSpeed = options.scaleBySpeed.minSpeed; 45 | scaleBySpeed.maxSpeed = options.scaleBySpeed.maxSpeed; 46 | scaleBySpeed.ease = options.scaleBySpeed.ease; 47 | } 48 | } 49 | 50 | override function onParticleSpawn(p:Particle) { 51 | p.scale = scaleBySpeed.minScale; 52 | } 53 | 54 | override function onParticleUpdate(p:Particle, elapsed:Float) { 55 | var scaleSpeedInvLerp = Maths.clamp(Maths.inverseLerp(scaleBySpeed.minSpeed, scaleBySpeed.maxSpeed, p.speed), 0, 1); 56 | p.scale = Maths.lerp(scaleBySpeed.minScale, scaleBySpeed.maxScale, scaleBySpeed.ease != null ? scaleBySpeed.ease(scaleSpeedInvLerp) : scaleSpeedInvLerp); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /sparkler/modules/size/SizeOverLifetimeModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.size; 2 | 3 | import sparkler.components.LifeProgress; 4 | import sparkler.components.Size; 5 | import sparkler.utils.Vector2; 6 | import sparkler.utils.Maths; 7 | import sparkler.ParticleModule; 8 | import sparkler.Particle; 9 | import sparkler.modules.size.SizeOverLifetimeModule.SizeOverLifetime; 10 | 11 | class SizeOverLifetime { 12 | 13 | public var start:Vector2 = new Vector2(); 14 | public var end:Vector2 = new Vector2(); 15 | public var ease:(v:Float)->Float; 16 | 17 | public function new() {} 18 | 19 | } 20 | 21 | @priority(90) 22 | @group('size') 23 | @addModules(sparkler.modules.helpers.LifeProgressModule) 24 | class SizeOverLifetimeModule extends ParticleModule> { 25 | 26 | public var sizeOverLifetime:SizeOverLifetime; 27 | 28 | function new(options:{?sizeOverLifetime:{?ease:(v:Float)->Float, start:{x:Float, y:Float}, end:{x:Float, y:Float}}}) { 29 | sizeOverLifetime = new SizeOverLifetime(); 30 | if(options.sizeOverLifetime != null) { 31 | sizeOverLifetime.start.x = options.sizeOverLifetime.start.x; 32 | sizeOverLifetime.start.y = options.sizeOverLifetime.start.y; 33 | sizeOverLifetime.end.x = options.sizeOverLifetime.end.x; 34 | sizeOverLifetime.end.y = options.sizeOverLifetime.end.y; 35 | sizeOverLifetime.ease = options.sizeOverLifetime.ease; 36 | } 37 | } 38 | 39 | override function onParticleUpdate(p:Particle, elapsed:Float) { 40 | var t = sizeOverLifetime.ease != null ? sizeOverLifetime.ease(p.lifeProgress) : p.lifeProgress; 41 | p.size.x = Maths.lerp(sizeOverLifetime.start.x, sizeOverLifetime.end.x, t); 42 | p.size.y = Maths.lerp(sizeOverLifetime.start.y, sizeOverLifetime.end.y, t); 43 | } 44 | 45 | override function onParticleSpawn(p:Particle) { 46 | p.size.x = sizeOverLifetime.start.x; 47 | p.size.y = sizeOverLifetime.start.y; 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /sparkler/modules/color/ColorBySpeedModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.color; 2 | 3 | import sparkler.components.Speed; 4 | import sparkler.utils.Color; 5 | import sparkler.utils.Maths; 6 | import sparkler.ParticleModule; 7 | import sparkler.Particle; 8 | import sparkler.modules.color.ColorBySpeedModule.ColorBySpeed; 9 | 10 | class ColorBySpeed { 11 | 12 | public var ease:(v:Float)->Float; 13 | 14 | public var minColor:Color = new Color(); 15 | public var maxColor:Color = new Color(); 16 | 17 | public var minSpeed:Float = 0; 18 | public var maxSpeed:Float = 128; 19 | 20 | public function new() {} 21 | 22 | } 23 | 24 | @priority(80) 25 | @group('color') 26 | @addModules(sparkler.modules.helpers.UpdateSpeedModule) 27 | class ColorBySpeedModule extends ParticleModule> { 28 | 29 | public var colorBySpeed:ColorBySpeed; 30 | 31 | function new(options:{ 32 | ?colorBySpeed:{ 33 | minColor:sparkler.utils.Color, 34 | maxColor:sparkler.utils.Color, 35 | minSpeed:Float, 36 | maxSpeed:Float, 37 | ?ease:(v:Float)->Float 38 | } 39 | }) { 40 | colorBySpeed = new ColorBySpeed(); 41 | if(options.colorBySpeed != null) { 42 | colorBySpeed.minColor = options.colorBySpeed.minColor; 43 | colorBySpeed.maxColor = options.colorBySpeed.maxColor; 44 | colorBySpeed.minSpeed = options.colorBySpeed.minSpeed; 45 | colorBySpeed.maxSpeed = options.colorBySpeed.maxSpeed; 46 | colorBySpeed.ease = options.colorBySpeed.ease; 47 | } 48 | } 49 | 50 | override function onParticleSpawn(p:Particle) { 51 | p.color = colorBySpeed.minColor; 52 | } 53 | 54 | override function onParticleUpdate(p:Particle, elapsed:Float) { 55 | var colorSpeedInvLerp = Maths.clamp(Maths.inverseLerp(colorBySpeed.minSpeed, colorBySpeed.maxSpeed, p.speed), 0, 1); 56 | p.color = Color.lerp(colorBySpeed.minColor, colorBySpeed.maxColor, colorBySpeed.ease != null ? colorBySpeed.ease(colorSpeedInvLerp) : colorSpeedInvLerp); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /sparkler/modules/emit/EmitDistanceModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.emit; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.utils.Maths; 5 | import sparkler.components.Velocity; 6 | import sparkler.ParticleModule; 7 | import sparkler.Particle; 8 | 9 | @group('emit') 10 | class EmitDistanceModule extends ParticleModule { 11 | 12 | public var emitDistance:Float = 32; 13 | var _dOffset:Float = 0; 14 | 15 | function new(options:{?emitDistance:Float}) { 16 | if(options.emitDistance != null) emitDistance = options.emitDistance; 17 | } 18 | 19 | override function onStart() { 20 | _dOffset = 0; 21 | } 22 | 23 | override function onStep(elapsed:Float) { 24 | if(enabled && !localSpace && emitDistance > 0 && (_lastX != _x || _lastY != _y)) { 25 | var px:Float = _x; 26 | var py:Float = _y; 27 | 28 | var dx = _x - _lastX; 29 | var dy = _y - _lastY; 30 | 31 | var len:Float = Math.sqrt(dx * dx + dy * dy); 32 | 33 | var framePos:Float = elapsed / _frameTime; 34 | var tickDistance = len * framePos; 35 | var distLeft = tickDistance; 36 | 37 | var f = elapsed / tickDistance; 38 | 39 | var frameTime:Float = 0; 40 | var ld:Float = 0; 41 | 42 | var d:Float = 0; 43 | while(_dOffset + distLeft >= emitDistance) { 44 | d = emitDistance - _dOffset; 45 | 46 | ld += d / tickDistance; 47 | _x = Maths.lerp(_lastX, px, ld); 48 | _y = Maths.lerp(_lastY, py, ld); 49 | 50 | frameTime = d * f; 51 | updateParticles(frameTime); 52 | elapsed -= frameTime; 53 | distLeft -= d; 54 | _dOffset = 0; 55 | emit(); 56 | } 57 | 58 | if (elapsed > 0) { 59 | framePos = elapsed / _frameTime; 60 | _x = Maths.lerp(_lastX, px, framePos); 61 | _y = Maths.lerp(_lastY, py, framePos); 62 | updateParticles(elapsed); 63 | _dOffset += distLeft; 64 | } 65 | 66 | _x = px; 67 | _y = py; 68 | } else { 69 | var px:Float = _x; 70 | var py:Float = _y; 71 | var framePos = elapsed / _frameTime; 72 | _x = Maths.lerp(_lastX, px, framePos); 73 | _y = Maths.lerp(_lastY, py, framePos); 74 | updateParticles(elapsed); 75 | _x = px; 76 | _y = py; 77 | } 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /sparkler/modules/move/MovementOverLifetimeModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.move; 2 | 3 | import sparkler.utils.Vector2; 4 | import sparkler.utils.Maths; 5 | import sparkler.components.Velocity; 6 | import sparkler.components.LifeProgress; 7 | import sparkler.ParticleModule; 8 | import sparkler.Particle; 9 | import sparkler.modules.move.MovementOverLifetimeModule.MovementOverLifetime; 10 | 11 | 12 | class MovementOverLifetime { 13 | 14 | public var start:Vector2 = new Vector2(); 15 | public var end:Vector2 = new Vector2(); 16 | public var ease:(v:Float)->Float; 17 | 18 | public function new() {} 19 | 20 | } 21 | 22 | @priority(65) 23 | @group('move') 24 | @addModules(sparkler.modules.helpers.LifeProgressModule) 25 | class MovementOverLifetimeModule extends ParticleModule> { 26 | 27 | public var movementOverLifetime:MovementOverLifetime; 28 | 29 | function new(options:{?movementOverLifetime:{start:{x:Float, y:Float}, end:{x:Float, y:Float}, ease:(v:Float)->Float}}) { 30 | movementOverLifetime = new MovementOverLifetime(); 31 | if(options.movementOverLifetime != null) { 32 | movementOverLifetime.start.x = options.movementOverLifetime.start.x; 33 | movementOverLifetime.start.y = options.movementOverLifetime.start.y; 34 | movementOverLifetime.end.x = options.movementOverLifetime.end.x; 35 | movementOverLifetime.end.y = options.movementOverLifetime.end.y; 36 | movementOverLifetime.ease = options.movementOverLifetime.ease; 37 | } 38 | } 39 | 40 | override function onParticleUpdate(p:Particle, elapsed:Float) { 41 | var t = movementOverLifetime.ease != null ? movementOverLifetime.ease(p.lifeProgress) : p.lifeProgress; 42 | if(localSpace) { 43 | p.x += Maths.lerp(movementOverLifetime.start.x, movementOverLifetime.end.x, t) * elapsed; 44 | p.y += Maths.lerp(movementOverLifetime.start.y, movementOverLifetime.end.y, t) * elapsed; 45 | } else { 46 | var dx = Maths.lerp(movementOverLifetime.start.x, movementOverLifetime.end.x, t); 47 | var dy = Maths.lerp(movementOverLifetime.start.y, movementOverLifetime.end.y, t); 48 | p.x += getRotateX(dx, dy) * elapsed; 49 | p.y += getRotateY(dx, dy) * elapsed; 50 | } 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /sparkler/modules/scale/ScaleMinMaxOverLifetimeModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.scale; 2 | 3 | import sparkler.utils.Maths; 4 | import sparkler.components.Scale; 5 | import sparkler.components.LifeProgress; 6 | import sparkler.components.ScaleRange; 7 | import sparkler.ParticleModule; 8 | import sparkler.Particle; 9 | import sparkler.utils.Bounds; 10 | import sparkler.modules.scale.ScaleMinMaxOverLifetimeModule.ScaleMinMaxOverLifetime; 11 | 12 | class ScaleMinMaxOverLifetime { 13 | 14 | public var start:Bounds; 15 | public var end:Bounds; 16 | public var ease:(v:Float)->Float; 17 | 18 | public function new() { 19 | start = new Bounds(1.0, 1.0); 20 | end = new Bounds(1.0, 1.0); 21 | } 22 | 23 | } 24 | 25 | @priority(100) 26 | @group('scale') 27 | @addModules(sparkler.modules.helpers.LifeProgressModule) 28 | class ScaleMinMaxOverLifetimeModule extends ParticleModule> { 29 | 30 | public var scaleMinMaxOverLifetime:ScaleMinMaxOverLifetime; 31 | 32 | function new(options:{?scaleMinMaxOverLifetime:{?ease:(v:Float)->Float, start:{min:Float, max:Float}, end:{min:Float, max:Float}}}) { 33 | scaleMinMaxOverLifetime = new ScaleMinMaxOverLifetime(); 34 | 35 | if(options.scaleMinMaxOverLifetime != null) { 36 | scaleMinMaxOverLifetime.start.min = options.scaleMinMaxOverLifetime.start.min; 37 | scaleMinMaxOverLifetime.start.max = options.scaleMinMaxOverLifetime.start.max; 38 | scaleMinMaxOverLifetime.end.min = options.scaleMinMaxOverLifetime.end.min; 39 | scaleMinMaxOverLifetime.end.max = options.scaleMinMaxOverLifetime.end.max; 40 | scaleMinMaxOverLifetime.ease = options.scaleMinMaxOverLifetime.ease; 41 | } 42 | } 43 | 44 | override function onParticleUpdate(p:Particle, elapsed:Float) { 45 | p.scale = Maths.lerp(p.scaleRange.start, p.scaleRange.end, scaleMinMaxOverLifetime.ease != null ? scaleMinMaxOverLifetime.ease(p.lifeProgress) : p.lifeProgress); 46 | } 47 | 48 | override function onParticleSpawn(p:Particle) { 49 | p.scaleRange.start = randomFloat(scaleMinMaxOverLifetime.start.min, scaleMinMaxOverLifetime.start.max); 50 | p.scaleRange.end = randomFloat(scaleMinMaxOverLifetime.end.min, scaleMinMaxOverLifetime.end.max); 51 | p.scale = p.scaleRange.start; 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /sparkler/modules/color/ColorMinMaxOverLifetimeModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.color; 2 | 3 | import sparkler.components.Color; 4 | import sparkler.components.ColorRange; 5 | import sparkler.components.LifeProgress; 6 | import sparkler.utils.Bounds; 7 | import sparkler.ParticleModule; 8 | import sparkler.Particle; 9 | import sparkler.modules.color.ColorMinMaxOverLifetimeModule.ColorMinMaxOverLifetime; 10 | 11 | class ColorMinMaxOverLifetime { 12 | 13 | public var start:Bounds; 14 | public var end:Bounds; 15 | public var ease:(v:Float)->Float; 16 | 17 | public function new() { 18 | start = new Bounds(-1,-1); 19 | end = new Bounds(-1,-1); 20 | } 21 | 22 | } 23 | 24 | @priority(80) 25 | @group('color') 26 | @addModules(sparkler.modules.helpers.LifeProgressModule) 27 | class ColorMinMaxOverLifetimeModule extends ParticleModule> { 28 | 29 | public var colorMinMaxOverLifetime:ColorMinMaxOverLifetime; 30 | 31 | function new( 32 | options:{ 33 | ?colorMinMaxOverLifetime:{ 34 | ?ease:(v:Float)->Float, 35 | start:{min:sparkler.utils.Color, max:sparkler.utils.Color}, 36 | end:{min:sparkler.utils.Color, max:sparkler.utils.Color} 37 | } 38 | } 39 | ) { 40 | colorMinMaxOverLifetime = new ColorMinMaxOverLifetime(); 41 | 42 | if(options.colorMinMaxOverLifetime != null) { 43 | colorMinMaxOverLifetime.start.min = options.colorMinMaxOverLifetime.start.min; 44 | colorMinMaxOverLifetime.start.max = options.colorMinMaxOverLifetime.start.max; 45 | colorMinMaxOverLifetime.end.min = options.colorMinMaxOverLifetime.end.min; 46 | colorMinMaxOverLifetime.end.max = options.colorMinMaxOverLifetime.end.max; 47 | colorMinMaxOverLifetime.ease = options.colorMinMaxOverLifetime.ease; 48 | } 49 | } 50 | 51 | override function onParticleUpdate(p:Particle, elapsed:Float) { 52 | p.color = Color.lerp(p.colorRange.start, p.colorRange.end, colorMinMaxOverLifetime.ease != null ? colorMinMaxOverLifetime.ease(p.lifeProgress) : p.lifeProgress); 53 | } 54 | 55 | override function onParticleSpawn(p:Particle) { 56 | p.colorRange.start = randomColor(colorMinMaxOverLifetime.start.min, colorMinMaxOverLifetime.start.max); 57 | p.colorRange.end = randomColor(colorMinMaxOverLifetime.end.min, colorMinMaxOverLifetime.end.max); 58 | p.color = p.colorRange.start; 59 | } 60 | 61 | inline function randomColor(a:Color, b:Color):Color { 62 | return Color.lerp(a, b, random()); 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /sparkler/utils/macro/ParticleMacro.hx: -------------------------------------------------------------------------------- 1 | package sparkler.utils.macro; 2 | 3 | #if (macro || display) 4 | 5 | import haxe.macro.Context; 6 | import haxe.macro.Expr; 7 | import haxe.macro.Type; 8 | import haxe.macro.TypeTools; 9 | import sparkler.utils.macro.MacroUtils; 10 | 11 | class ParticleMacro { 12 | 13 | static public var typeName:String = "Particle"; 14 | static public var particleIds:Map = new Map(); 15 | static public var particleTypes:Map> = new Map(); 16 | 17 | static var idx:Int = 0; 18 | static var particleTypeNames:Array = ['ParticleBase']; 19 | 20 | static function build() { 21 | return switch (Context.getLocalType()) { 22 | case TInst(t, p): 23 | buildParticle(p); 24 | default: 25 | throw false; 26 | } 27 | } 28 | 29 | static public function getParticleName(types:Array):String { 30 | var typesString = MacroUtils.getStringFromTypes(types); 31 | 32 | var name = '${typeName}_${typesString}'; 33 | 34 | var uuid:String = particleIds.get(name); 35 | 36 | if(uuid == null) { 37 | uuid = '${typeName}_${idx++}'; 38 | particleIds.set(name, uuid); 39 | } 40 | 41 | return uuid; 42 | } 43 | 44 | static public function buildParticle(types:Array) { 45 | if(types.length == 0) return Context.getType('sparkler.Particle.ParticleBase'); 46 | 47 | var name = getParticleName(types); 48 | 49 | if(particleTypeNames.indexOf(name) == -1) { 50 | var fields = Context.getBuildFields(); 51 | var pos = Context.currentPos(); 52 | 53 | particleTypes.set(name, types); 54 | for (i in 0...types.length) { 55 | var expr:Expr; 56 | switch (types[i]) { 57 | case TType(t, p): 58 | switch (t.get().type) { 59 | case TAbstract(at, ap): 60 | switch (at.get().name) { 61 | case 'Float' | 'Int': expr = macro 0; 62 | case 'String': expr = macro ''; 63 | default: 64 | } 65 | case TInst(at, ap): 66 | default: 67 | } 68 | default: 69 | } 70 | var info = MacroUtils.getPathInfo(types[i]); 71 | var compType = {pack: info.pack, name: info.module, sub: info.name}; 72 | var compName = MacroUtils.toCamelCase(info.name); 73 | if(expr == null) expr = macro new $compType(); 74 | fields.push(MacroUtils.buildVar(compName, [APublic], TPath(compType), expr)); 75 | } 76 | 77 | Context.defineType({ 78 | pack: ['sparkler'], 79 | name: name, 80 | pos: pos, 81 | meta: [], 82 | kind: TDClass({ 83 | pack: ['sparkler'], 84 | name: "Particle", 85 | sub: "ParticleBase" 86 | }), 87 | fields: fields 88 | }); 89 | 90 | particleTypeNames.push(name); 91 | 92 | } 93 | 94 | return Context.getType('sparkler.${name}'); 95 | } 96 | 97 | } 98 | 99 | #end 100 | -------------------------------------------------------------------------------- /sparkler/ParticleModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler; 2 | 3 | import sparkler.Particle; 4 | import sparkler.ParticleEmitter.IParticleEmitter; 5 | import sparkler.utils.Color; 6 | 7 | #if !macro 8 | @:autoBuild(sparkler.utils.macro.ParticleModuleMacro.build()) 9 | #end 10 | 11 | class ParticleModule implements IParticleEmitter { 12 | 13 | public var cacheSize(default, null):Int; 14 | public var activeCount(default, null):Int; 15 | public var progress(default, null):Float; 16 | public var loops:Int; 17 | public var localSpace:Bool; 18 | public var enabled:Bool; 19 | public var particles:haxe.ds.Vector; 20 | public var random:()->Float; 21 | public var sortFunc:(a:T, b:T)->Int; 22 | 23 | var _rotation:Float; 24 | var _scaleX:Float; 25 | var _scaleY:Float; 26 | var _originX:Float; 27 | var _originY:Float; 28 | 29 | var _x:Float; 30 | var _y:Float; 31 | var _lastX:Float; 32 | var _lastY:Float; 33 | var _frameTime:Float; 34 | var _loopsCounter:Int; 35 | 36 | var _a:Float; 37 | var _b:Float; 38 | var _c:Float; 39 | var _d:Float; 40 | var _tx:Float; 41 | var _ty:Float; 42 | 43 | 44 | public function emit() {} 45 | public function start() {} 46 | public function stop() {} 47 | 48 | function setParticlePos(p:T, x:Float, y:Float) {} 49 | function updateParticles(elapsed:Float) {} 50 | function getSorted():haxe.ds.Vector return null; 51 | function step(elapsed:Float) {} 52 | function restart() {} 53 | function spawn() {} 54 | function unspawn(p:T) {} 55 | 56 | function onEmit() {} 57 | function onStart() {} 58 | function onStop() {} 59 | 60 | function onUpdate(elapsed:Float) {} 61 | function onStepStart(elapsed:Float) {} 62 | function onStep(elapsed:Float) {} 63 | function onStepEnd(elapsed:Float) {} 64 | 65 | function onParticleUpdate(p:T, elapsed:Float) {} 66 | function onParticleSpawn(p:T) {} 67 | function onParticleUnspawn(p:T) {} 68 | 69 | function getRotateX(x:Float, y:Float) return 0.0; 70 | function getRotateY(x:Float, y:Float) return 0.0; 71 | 72 | function getTransformX(x:Float, y:Float) return 0.0; 73 | function getTransformY(x:Float, y:Float) return 0.0; 74 | 75 | function random1To1() return 0; 76 | function randomInt(min:Float, ?max:Null) return 0; 77 | function randomFloat(min:Float, ?max:Null) return 0; 78 | 79 | // additional 80 | function onPreStepStart(elapsed:Float) {} 81 | function onPostStepStart(elapsed:Float) {} 82 | 83 | function onPreStep(elapsed:Float) {} 84 | function onPostStep(elapsed:Float) {} 85 | 86 | function onPreStepEnd(elapsed:Float) {} 87 | function onPostStepEnd(elapsed:Float) {} 88 | 89 | function onPreStart() {} 90 | function onPostStart() {} 91 | 92 | function onPreStop() {} 93 | function onPostStop() {} 94 | 95 | function onPreParticleUpdate(p:T, elapsed:Float) {} 96 | function onPostParticleUpdate(p:T, elapsed:Float) {} 97 | 98 | function onPreParticleSpawn(p:T) {} 99 | function onPostParticleSpawn(p:T) {} 100 | 101 | function onPreParticleUnspawn(p:T) {} 102 | function onPostParticleUnspawn(p:T) {} 103 | 104 | } 105 | 106 | #if !macro 107 | @:autoBuild(sparkler.utils.macro.ParticleModuleMacro.buildInject()) 108 | #end 109 | class ParticleInjectModule {} 110 | -------------------------------------------------------------------------------- /sparkler/utils/macro/ParticleModuleMacro.hx: -------------------------------------------------------------------------------- 1 | package sparkler.utils.macro; 2 | 3 | #if (macro || display) 4 | 5 | import haxe.macro.Context; 6 | import haxe.macro.Expr; 7 | import haxe.macro.Type; 8 | import haxe.macro.ComplexTypeTools; 9 | import haxe.macro.TypeTools; 10 | import sparkler.utils.macro.MacroUtils; 11 | 12 | class ParticleModuleMacro { 13 | 14 | static public var moduleOptions:Map = new Map(); 15 | 16 | static public function build():Array { 17 | var cp:String = Context.getLocalModule(); 18 | var t = Context.getLocalType(); 19 | var fields = Context.getBuildFields(); 20 | var imports = Context.getLocalImports(); 21 | 22 | var fFields:Array = []; 23 | for (f in fields) { 24 | if(!MacroUtils.hasFieldMeta(f, 'virtual')) fFields.push(f); 25 | } 26 | 27 | addModuleOptions(cp, t, fFields, imports, false); 28 | 29 | return fields; 30 | } 31 | 32 | static public function buildInject():Array { 33 | var cp:String = Context.getLocalModule(); 34 | var t = Context.getLocalType(); 35 | var fields = Context.getBuildFields(); 36 | var imports = Context.getLocalImports(); 37 | 38 | addModuleOptions(cp, t, [], imports, true); 39 | 40 | return fields; 41 | } 42 | 43 | static public function addModuleOptions(name:String, type:Type, fields:Array, imports:Array, isInjectType:Bool) { 44 | var opt:ParticleModuleMacroOptions = { 45 | name: name, 46 | type: type, 47 | particleTypeName: null, 48 | isInjectType: isInjectType, 49 | priority: 0, 50 | group: null, 51 | imports: imports, 52 | fields: fields, 53 | addModules: [] 54 | } 55 | 56 | var c = TypeTools.getClass(type); 57 | if(c.superClass.params.length > 0) { 58 | switch (c.superClass.params[0]) { 59 | case TInst(t, p): 60 | var pack = t.toString().split('.'); 61 | var pName = pack[pack.length-1]; 62 | opt.particleTypeName = pack[pack.length-1]; 63 | default: 64 | } 65 | } 66 | 67 | var metas = c.meta.get(); 68 | for (m in metas) { 69 | switch (m.name) { 70 | case 'inject': 71 | opt.isInjectType = true; 72 | case 'priority': 73 | var e = m.params[0].expr; 74 | switch (e) { 75 | case EConst(c): 76 | switch (c) { 77 | case CInt(i): 78 | opt.priority = Std.parseInt(i); 79 | default: 80 | } 81 | default: throw('@priority meta must be a Int'); 82 | } 83 | case 'group': 84 | var e = m.params[0].expr; 85 | switch (e) { 86 | case EConst(c): 87 | switch (c) { 88 | case CString(s): 89 | opt.group = s; 90 | default: 91 | } 92 | default: throw('@group meta must be a String'); 93 | } 94 | case 'addModules': 95 | for (p in m.params) { 96 | var pe = p.expr; 97 | var tPath:Array = []; 98 | while(pe != null) { 99 | switch (pe) { 100 | case EField(e, f): 101 | pe = e.expr; 102 | tPath.insert(0, f); 103 | case EConst(CIdent(s)): 104 | tPath.insert(0, s); 105 | pe = null; 106 | default: break; 107 | } 108 | } 109 | 110 | var name = tPath.join('.'); 111 | var at = Context.getType(name); 112 | opt.addModules.push(at); 113 | } 114 | default: 115 | } 116 | } 117 | 118 | moduleOptions.set(name, opt); 119 | } 120 | 121 | } 122 | 123 | typedef ParticleModuleMacroOptions = { 124 | name:String, 125 | type:Type, 126 | particleTypeName:String, 127 | isInjectType:Bool, 128 | priority:Float, 129 | group:String, 130 | imports:Array, 131 | fields:Array, 132 | addModules:Array 133 | } 134 | 135 | #end 136 | -------------------------------------------------------------------------------- /sparkler/utils/Color.hx: -------------------------------------------------------------------------------- 1 | package sparkler.utils; 2 | 3 | abstract Color(Int) from Int to Int { 4 | 5 | static public inline function fromValue(value:Int):Color { 6 | var c = new Color(); 7 | c.value = value; 8 | return c; 9 | } 10 | 11 | static public inline function lerp(a:Color, b:Color, t:Float):Color { 12 | var c = new Color(); 13 | t = (t > 1) ? 1 : ((t < 0) ? 0 : t); 14 | c.setBytes( 15 | a.rB + Std.int((b.rB - a.rB) * t), 16 | a.gB + Std.int((b.gB - a.gB) * t), 17 | a.bB + Std.int((b.bB - a.bB) * t), 18 | a.aB + Std.int((b.aB - a.aB) * t) 19 | ); 20 | return c; 21 | } 22 | 23 | static inline var invMaxChannelValue: Float = 1 / 255; 24 | 25 | public var r(get, set):Float; 26 | public var g(get, set):Float; 27 | public var b(get, set):Float; 28 | public var a(get, set):Float; 29 | 30 | public var rB(get, set):Int; 31 | public var gB(get, set):Int; 32 | public var bB(get, set):Int; 33 | public var aB(get, set):Int; 34 | 35 | public var value(get, set):Int; 36 | 37 | public inline function new(r:Float = 1, g:Float = 1, b:Float = 1, a:Float = 1) { 38 | this = (Std.int(a * 255) << 24) | (Std.int(r * 255) << 16) | (Std.int(g * 255) << 8) | Std.int(b * 255); 39 | } 40 | 41 | public inline function set(r:Float, g:Float, b:Float, a:Float):Color { 42 | this = (Std.int(a * 255) << 24) | (Std.int(r * 255) << 16) | (Std.int(g * 255) << 8) | Std.int(b * 255); 43 | return this; 44 | } 45 | 46 | public inline function setBytes(rB:Int, gB:Int, bB:Int, aB:Int):Color { 47 | this = (aB << 24) | (rB << 16) | (gB << 8) | bB; 48 | return this; 49 | } 50 | 51 | public inline function clone():Color { 52 | return Color.fromValue(this); 53 | } 54 | 55 | inline function get_r():Float { 56 | return get_rB() * invMaxChannelValue; 57 | } 58 | 59 | inline function get_g():Float { 60 | return get_gB() * invMaxChannelValue; 61 | } 62 | 63 | inline function get_b():Float { 64 | return get_bB() * invMaxChannelValue; 65 | } 66 | 67 | inline function get_a():Float { 68 | return get_aB() * invMaxChannelValue; 69 | } 70 | 71 | inline function set_r(f:Float):Float { 72 | this = (Std.int(a * 255) << 24) | (Std.int(f * 255) << 16) | (Std.int(g * 255) << 8) | Std.int(b * 255); 73 | return f; 74 | } 75 | 76 | inline function set_g(f:Float):Float { 77 | this = (Std.int(a * 255) << 24) | (Std.int(r * 255) << 16) | (Std.int(f * 255) << 8) | Std.int(b * 255); 78 | return f; 79 | } 80 | 81 | inline function set_b(f:Float):Float { 82 | this = (Std.int(a * 255) << 24) | (Std.int(r * 255) << 16) | (Std.int(g * 255) << 8) | Std.int(f * 255); 83 | return f; 84 | } 85 | 86 | inline function set_a(f:Float):Float { 87 | this = (Std.int(f * 255) << 24) | (Std.int(r * 255) << 16) | (Std.int(g * 255) << 8) | Std.int(b * 255); 88 | return f; 89 | } 90 | 91 | inline function get_rB():Int { 92 | return (this & 0x00ff0000) >>> 16; 93 | } 94 | 95 | inline function get_gB():Int { 96 | return (this & 0x0000ff00) >>> 8; 97 | } 98 | 99 | inline function get_bB():Int { 100 | return this & 0x000000ff; 101 | } 102 | 103 | inline function get_aB():Int { 104 | return this >>> 24; 105 | } 106 | 107 | inline function set_rB(i:Int):Int { 108 | this = (aB << 24) | (i << 16) | (gB << 8) | bB; 109 | return i; 110 | } 111 | 112 | inline function set_gB(i:Int):Int { 113 | this = (aB << 24) | (rB << 16) | (i << 8) | bB; 114 | return i; 115 | } 116 | 117 | inline function set_bB(i:Int):Int { 118 | this = (aB << 24) | (rB << 16) | (gB << 8) | i; 119 | return i; 120 | } 121 | 122 | inline function set_aB(i:Int):Int { 123 | this = (i << 24) | (rB << 16) | (gB << 8) | bB; 124 | return i; 125 | } 126 | 127 | inline function get_value():Int { 128 | return this; 129 | } 130 | 131 | inline function set_value(value:Int):Int { 132 | this = value; 133 | return this; 134 | } 135 | 136 | } -------------------------------------------------------------------------------- /sparkler/modules/render/ClaySpriteRendererModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.render; 2 | 3 | import sparkler.utils.macro.ParticleEmitterMacro; 4 | import sparkler.utils.macro.MacroUtils; 5 | import sparkler.ParticleModule; 6 | import sparkler.Particle; 7 | 8 | import haxe.macro.Context; 9 | import haxe.macro.Expr; 10 | 11 | #if !macro 12 | import clay.graphics.Texture; 13 | import clay.math.FastMatrix3; 14 | 15 | class ClaySpriteRenderer { 16 | 17 | public var texture:Texture; 18 | 19 | @:noCompletion public var _transformPrev:FastMatrix3 = new FastMatrix3(); 20 | @:noCompletion public var _transform:FastMatrix3 = new FastMatrix3(); 21 | @:noCompletion public var _drawMatrix:FastMatrix3 = new FastMatrix3(); 22 | 23 | public function new() {} 24 | 25 | } 26 | #end 27 | 28 | @group('renderer') 29 | class ClaySpriteRendererModule extends ParticleInjectModule { 30 | 31 | #if (macro || display) 32 | static public function inject(options:ParticleEmitterBuildOptions) { 33 | var pos = Context.currentPos(); 34 | 35 | var fields = options.fields; 36 | var particleType = options.particleType; 37 | var particleFieldNames = options.particleFieldNames; 38 | var newExprs = options.newExprs; 39 | var optFields = options.optFields; 40 | 41 | var textureOptVar = MacroUtils.buildVar( 42 | 'claySpriteRenderer', 43 | [Access.APublic], 44 | macro: { 45 | ?texture:clay.graphics.Texture 46 | }, 47 | null, 48 | [{name: ':optional', pos: pos}] 49 | ); 50 | optFields.push(textureOptVar); 51 | 52 | var claySpriteRendererVar = MacroUtils.buildVar('claySpriteRenderer', [Access.APublic], macro: sparkler.modules.render.ClaySpriteRendererModule.ClaySpriteRenderer); 53 | fields.push(claySpriteRendererVar); 54 | 55 | newExprs.push(macro { 56 | claySpriteRenderer = new sparkler.modules.render.ClaySpriteRendererModule.ClaySpriteRenderer(); 57 | if(options.claySpriteRenderer != null) { 58 | if(options.claySpriteRenderer.texture != null) claySpriteRenderer.texture = options.claySpriteRenderer.texture; 59 | } 60 | }); 61 | 62 | var preExprs:Array = []; 63 | var exprs:Array = []; 64 | 65 | var sizeXexpr = macro 32.0; 66 | var sizeYexpr = macro 32.0; 67 | 68 | var scaleExpr = macro 1.0; 69 | 70 | var rotationExpr = macro 0.0; 71 | 72 | var originXexpr = macro 16.0; 73 | var originYexpr = macro 16.0; 74 | 75 | var skewXexpr = macro 0.0; 76 | var skewYexpr = macro 0.0; 77 | 78 | var regionXexpr = macro 0; 79 | var regionYexpr = macro 0; 80 | var regionWexpr = macro null; 81 | var regionHexpr = macro null; 82 | 83 | var hasOrigin = particleFieldNames.indexOf('origin') != -1; 84 | 85 | if(hasOrigin) { 86 | originXexpr = macro p.origin.x; 87 | originYexpr = macro p.origin.y; 88 | } 89 | 90 | if(particleFieldNames.indexOf('size') != -1) { 91 | sizeXexpr = macro p.size.x; 92 | sizeYexpr = macro p.size.y; 93 | if(!hasOrigin) { 94 | originXexpr = macro (p.size.x / 2); 95 | originYexpr = macro (p.size.y / 2); 96 | } 97 | } 98 | 99 | if(particleFieldNames.indexOf('scale') != -1) { 100 | scaleExpr = macro p.scale; 101 | } 102 | 103 | if(particleFieldNames.indexOf('rotation') != -1) { 104 | rotationExpr = macro sparkler.utils.Maths.radians(p.rotation); 105 | } 106 | 107 | if(particleFieldNames.indexOf('skew') != -1) { 108 | skewXexpr = macro p.skew.x; 109 | skewYexpr = macro p.skew.y; 110 | } 111 | 112 | if(particleFieldNames.indexOf('region') != -1) { 113 | regionXexpr = macro p.region.x; 114 | regionYexpr = macro p.region.y; 115 | regionWexpr = macro p.region.w; 116 | regionHexpr = macro p.region.h; 117 | } 118 | 119 | if(particleFieldNames.indexOf('color') != -1) { 120 | exprs.push( 121 | macro { 122 | batcher.color = p.color.value; 123 | } 124 | ); 125 | } else { 126 | preExprs.push( 127 | macro { 128 | batcher.color = 0xFFFFFFFF; 129 | } 130 | ); 131 | } 132 | 133 | var drawFunc = MacroUtils.buildFunction( 134 | 'draw', 135 | [Access.APublic], 136 | [{name: 'batcher', type: macro: clay.graphics.batchers.SpriteBatch}], 137 | macro: Void, 138 | [macro { 139 | if(activeCount > 0) { 140 | var sortedParticles = getSorted(); 141 | var texture = claySpriteRenderer.texture; 142 | var drawMatrix = claySpriteRenderer._drawMatrix; 143 | 144 | if(localSpace) { 145 | claySpriteRenderer._transformPrev.copyFrom(batcher.transform); 146 | claySpriteRenderer._transform.set(_a, _b, _c, _d, _tx, _ty); 147 | batcher.transform = claySpriteRenderer._transform; 148 | } 149 | 150 | $b{preExprs} 151 | var p; 152 | var i:Int = 0; 153 | while(i < activeCount) { 154 | p = sortedParticles[i]; 155 | $b{exprs} 156 | 157 | drawMatrix.setTransform( 158 | p.x, p.y, 159 | $rotationExpr, 160 | $scaleExpr, $scaleExpr, 161 | $originXexpr, $originYexpr, 162 | $skewXexpr, $skewYexpr 163 | ); 164 | 165 | batcher.drawImageTransform( 166 | texture, 167 | drawMatrix, 168 | $sizeXexpr, $sizeYexpr, 169 | $regionXexpr, $regionYexpr, $regionWexpr, $regionHexpr 170 | ); 171 | i++; 172 | } 173 | 174 | if(localSpace) { 175 | batcher.transform = claySpriteRenderer._transformPrev; 176 | } 177 | } 178 | }] 179 | ); 180 | fields.push(drawFunc); 181 | 182 | } 183 | 184 | #end 185 | 186 | } 187 | -------------------------------------------------------------------------------- /sparkler/modules/render/NucSpriteRendererModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.render; 2 | 3 | import sparkler.utils.macro.ParticleEmitterMacro; 4 | import sparkler.utils.macro.MacroUtils; 5 | import sparkler.ParticleModule; 6 | import sparkler.Particle; 7 | 8 | import haxe.macro.Context; 9 | import haxe.macro.Expr; 10 | 11 | #if !macro 12 | import nuc.graphics.Texture; 13 | import nuc.math.FastAffine; 14 | 15 | class NucSpriteRenderer { 16 | 17 | public var texture:Texture; 18 | 19 | @:noCompletion public var _transform:FastAffine = new FastAffine(); 20 | 21 | public function new() {} 22 | 23 | } 24 | #end 25 | 26 | @group('renderer') 27 | class NucSpriteRendererModule extends ParticleInjectModule { 28 | 29 | #if (macro || display) 30 | static public function inject(options:ParticleEmitterBuildOptions) { 31 | var pos = Context.currentPos(); 32 | 33 | var fields = options.fields; 34 | var particleType = options.particleType; 35 | var particleFieldNames = options.particleFieldNames; 36 | var newExprs = options.newExprs; 37 | var optFields = options.optFields; 38 | 39 | var textureOptVar = MacroUtils.buildVar( 40 | 'nucSpriteRenderer', 41 | [Access.APublic], 42 | macro: { 43 | ?texture:nuc.graphics.Texture 44 | }, 45 | null, 46 | [{name: ':optional', pos: pos}] 47 | ); 48 | optFields.push(textureOptVar); 49 | 50 | var nucSpriteRendererVar = MacroUtils.buildVar('nucSpriteRenderer', [Access.APublic], macro: sparkler.modules.render.NucSpriteRendererModule.NucSpriteRenderer); 51 | fields.push(nucSpriteRendererVar); 52 | 53 | newExprs.push(macro { 54 | nucSpriteRenderer = new sparkler.modules.render.NucSpriteRendererModule.NucSpriteRenderer(); 55 | if(options.nucSpriteRenderer != null) { 56 | if(options.nucSpriteRenderer.texture != null) nucSpriteRenderer.texture = options.nucSpriteRenderer.texture; 57 | } 58 | }); 59 | 60 | var preExprs:Array = []; 61 | var exprsBeginLocal:Array = []; 62 | var exprsEndLocal:Array = []; 63 | var exprsBegin:Array = []; 64 | var exprsEnd:Array = []; 65 | 66 | var sizeXexpr = macro 32.0; 67 | var sizeYexpr = macro 32.0; 68 | 69 | var scaleExpr = macro 1.0; 70 | 71 | var rotationExpr = macro 0.0; 72 | 73 | var originXexpr = macro 16.0; 74 | var originYexpr = macro 16.0; 75 | 76 | var skewXexpr = macro 0.0; 77 | var skewYexpr = macro 0.0; 78 | 79 | var regionXexpr = macro 0; 80 | var regionYexpr = macro 0; 81 | var regionWexpr = macro null; 82 | var regionHexpr = macro null; 83 | 84 | var hasOrigin = particleFieldNames.indexOf('origin') != -1; 85 | 86 | if(hasOrigin) { 87 | originXexpr = macro p.origin.x; 88 | originYexpr = macro p.origin.y; 89 | } 90 | 91 | if(particleFieldNames.indexOf('size') != -1) { 92 | sizeXexpr = macro p.size.x; 93 | sizeYexpr = macro p.size.y; 94 | if(!hasOrigin) { 95 | originXexpr = macro (p.size.x / 2); 96 | originYexpr = macro (p.size.y / 2); 97 | } 98 | } 99 | 100 | if(particleFieldNames.indexOf('scale') != -1) { 101 | scaleExpr = macro p.scale; 102 | } 103 | 104 | if(particleFieldNames.indexOf('rotation') != -1) { 105 | exprsBegin.push( 106 | macro { 107 | g.pushTransform(); 108 | g.rotate(sparkler.utils.Maths.radians(p.rotation), p.x, p.y); 109 | } 110 | ); 111 | exprsEnd.push( 112 | macro { 113 | g.popTransform(); 114 | } 115 | ); 116 | 117 | exprsBeginLocal.push( 118 | macro { 119 | var rx = t.getTransformX(p.x, p.y); 120 | var ry = t.getTransformY(p.x, p.y); 121 | g.pushTransform(); 122 | g.rotate(sparkler.utils.Maths.radians(p.rotation), rx, ry); 123 | } 124 | ); 125 | exprsEndLocal.push( 126 | macro { 127 | g.popTransform(); 128 | } 129 | ); 130 | } 131 | 132 | if(particleFieldNames.indexOf('skew') != -1) { 133 | skewXexpr = macro p.skew.x; 134 | skewYexpr = macro p.skew.y; 135 | } 136 | 137 | if(particleFieldNames.indexOf('region') != -1) { 138 | regionXexpr = macro p.region.x; 139 | regionYexpr = macro p.region.y; 140 | regionWexpr = macro p.region.w; 141 | regionHexpr = macro p.region.h; 142 | } 143 | 144 | if(particleFieldNames.indexOf('color') != -1) { 145 | exprsBeginLocal.push( 146 | macro { 147 | g.color = p.color.value; 148 | } 149 | ); 150 | exprsBegin.push( 151 | macro { 152 | g.color = p.color.value; 153 | } 154 | ); 155 | } else { 156 | preExprs.push( 157 | macro { 158 | g.color = 0xFFFFFFFF; 159 | } 160 | ); 161 | } 162 | 163 | var drawExpr = macro { 164 | g.drawImage( 165 | texture, p.x - $originXexpr * $scaleExpr, p.y - $originYexpr * $scaleExpr, 166 | $sizeXexpr * $scaleExpr, $sizeYexpr * $scaleExpr, 167 | $regionXexpr, $regionYexpr, $regionWexpr, $regionHexpr 168 | ); 169 | }; 170 | 171 | var drawFunc = MacroUtils.buildFunction( 172 | 'draw', 173 | [Access.APublic], 174 | [{name: 'g', type: macro: nuc.graphics.SpriteBatch}], 175 | macro: Void, 176 | [macro { 177 | if(activeCount > 0) { 178 | var sortedParticles = getSorted(); 179 | var texture = nucSpriteRenderer.texture; 180 | 181 | $b{preExprs} 182 | 183 | if(localSpace) { 184 | var t = nucSpriteRenderer._transform.set(_a, _b, _c, _d, _tx, _ty); 185 | t.set(_a, _b, _c, _d, _tx, _ty); 186 | g.pushTransform(t); 187 | 188 | var p; 189 | var i:Int = 0; 190 | while(i < activeCount) { 191 | p = sortedParticles[i]; 192 | $b{exprsBeginLocal} 193 | $e{drawExpr} 194 | $b{exprsEndLocal} 195 | i++; 196 | } 197 | g.popTransform(); 198 | } else { 199 | var p; 200 | var i:Int = 0; 201 | while(i < activeCount) { 202 | p = sortedParticles[i]; 203 | $b{exprsBegin} 204 | $e{drawExpr} 205 | $b{exprsEnd} 206 | i++; 207 | } 208 | } 209 | } 210 | }] 211 | ); 212 | fields.push(drawFunc); 213 | 214 | } 215 | 216 | #end 217 | 218 | } 219 | -------------------------------------------------------------------------------- /sparkler/modules/render/KhaSpriteRendererModule.hx: -------------------------------------------------------------------------------- 1 | package sparkler.modules.render; 2 | 3 | import sparkler.utils.macro.ParticleEmitterMacro; 4 | import sparkler.utils.macro.MacroUtils; 5 | import sparkler.ParticleModule; 6 | import sparkler.Particle; 7 | 8 | import haxe.macro.Context; 9 | import haxe.macro.Expr; 10 | 11 | #if !macro 12 | import kha.Image; 13 | import kha.math.FastMatrix3; 14 | 15 | class KhaSpriteRenderer { 16 | 17 | public var image:Image; 18 | 19 | @:noCompletion public var _imageDefault:Image; 20 | @:noCompletion public var _transform:FastMatrix3 = FastMatrix3.identity(); 21 | 22 | public function new() { 23 | _imageDefault = Image.create(1,1); 24 | var pixels = _imageDefault.lock(); 25 | pixels.setInt32(0, 0xffffffff); 26 | _imageDefault.unlock(); 27 | } 28 | 29 | } 30 | #end 31 | 32 | @group('renderer') 33 | class KhaSpriteRendererModule extends ParticleInjectModule { 34 | 35 | #if (macro || display) 36 | static public function inject(options:ParticleEmitterBuildOptions) { 37 | var pos = Context.currentPos(); 38 | 39 | var fields = options.fields; 40 | var particleType = options.particleType; 41 | var particleFieldNames = options.particleFieldNames; 42 | var newExprs = options.newExprs; 43 | var optFields = options.optFields; 44 | 45 | var imageOptVar = MacroUtils.buildVar( 46 | 'khaSpriteRenderer', 47 | [Access.APublic], 48 | macro: { 49 | ?image:kha.Image 50 | }, 51 | null, 52 | [{name: ':optional', pos: pos}] 53 | ); 54 | optFields.push(imageOptVar); 55 | 56 | var khaSpriteRendererVar = MacroUtils.buildVar('khaSpriteRenderer', [Access.APublic], macro: sparkler.modules.render.KhaSpriteRendererModule.KhaSpriteRenderer); 57 | fields.push(khaSpriteRendererVar); 58 | 59 | newExprs.push(macro { 60 | khaSpriteRenderer = new sparkler.modules.render.KhaSpriteRendererModule.KhaSpriteRenderer(); 61 | if(options.khaSpriteRenderer != null) { 62 | if(options.khaSpriteRenderer.image != null) khaSpriteRenderer.image = options.khaSpriteRenderer.image; 63 | } 64 | }); 65 | 66 | var preExprs:Array = []; 67 | var exprsBeginLocal:Array = []; 68 | var exprsEndLocal:Array = []; 69 | var exprsBegin:Array = []; 70 | var exprsEnd:Array = []; 71 | 72 | var sizeXexpr = macro 32.0; 73 | var sizeYexpr = macro 32.0; 74 | 75 | var scaleExpr = macro 1.0; 76 | 77 | var originXexpr = macro 16.0; 78 | var originYexpr = macro 16.0; 79 | 80 | var regionXexpr = macro 0; 81 | var regionYexpr = macro 0; 82 | var regionWexpr = macro image.width; 83 | var regionHexpr = macro image.height; 84 | 85 | var hasOrigin = particleFieldNames.indexOf('origin') != -1; 86 | 87 | if(hasOrigin) { 88 | originXexpr = macro p.origin.x; 89 | originYexpr = macro p.origin.y; 90 | } 91 | 92 | if(particleFieldNames.indexOf('size') != -1) { 93 | sizeXexpr = macro p.size.x; 94 | sizeYexpr = macro p.size.y; 95 | if(!hasOrigin) { 96 | originXexpr = macro (p.size.x / 2); 97 | originYexpr = macro (p.size.y / 2); 98 | } 99 | } 100 | 101 | if(particleFieldNames.indexOf('scale') != -1) { 102 | scaleExpr = macro p.scale; 103 | } 104 | 105 | if(particleFieldNames.indexOf('rotation') != -1) { 106 | exprsBegin.push( 107 | macro { 108 | g.pushRotation(sparkler.utils.Maths.radians(p.rotation), p.x, p.y); 109 | } 110 | ); 111 | exprsEnd.push( 112 | macro { 113 | g.popTransformation(); 114 | } 115 | ); 116 | 117 | exprsBeginLocal.push( 118 | macro { 119 | var rx = t._00 * p.x + t._10 * p.y + t._20; 120 | var ry = t._01 * p.x + t._11 * p.y + t._21; 121 | g.pushRotation(sparkler.utils.Maths.radians(p.rotation), rx, ry); 122 | } 123 | ); 124 | exprsEndLocal.push( 125 | macro { 126 | g.popTransformation(); 127 | } 128 | ); 129 | } 130 | 131 | if(particleFieldNames.indexOf('region') != -1) { 132 | regionXexpr = macro p.region.x; 133 | regionYexpr = macro p.region.y; 134 | regionWexpr = macro p.region.w; 135 | regionHexpr = macro p.region.h; 136 | } 137 | 138 | if(particleFieldNames.indexOf('color') != -1) { 139 | exprsBeginLocal.push( 140 | macro { 141 | g.color = p.color.value; 142 | } 143 | ); 144 | exprsBegin.push( 145 | macro { 146 | g.color = p.color.value; 147 | } 148 | ); 149 | } else { 150 | preExprs.push( 151 | macro { 152 | g.color = 0xFFFFFFFF; 153 | } 154 | ); 155 | } 156 | 157 | var drawExpr = macro { 158 | g.drawScaledSubImage( 159 | image, 160 | $regionXexpr, $regionYexpr, $regionWexpr, $regionHexpr, 161 | p.x - $originXexpr * $scaleExpr, p.y - $originYexpr * $scaleExpr, 162 | $sizeXexpr * $scaleExpr, $sizeYexpr * $scaleExpr 163 | ); 164 | }; 165 | 166 | var drawFunc = MacroUtils.buildFunction( 167 | 'draw', 168 | [Access.APublic], 169 | [{name: 'g', type: macro: kha.graphics2.Graphics}], 170 | macro: Void, 171 | [macro { 172 | if(activeCount > 0) { 173 | var sortedParticles = getSorted(); 174 | 175 | var image = khaSpriteRenderer.image; 176 | if(image == null) image = khaSpriteRenderer._imageDefault; 177 | 178 | $b{preExprs} 179 | 180 | if(localSpace) { 181 | var t = khaSpriteRenderer._transform; 182 | t._00 = _a; t._01 = _b; 183 | t._10 = _c; t._11 = _d; 184 | t._20 = _tx; t._21 = _ty; 185 | g.pushTransformation(t); 186 | 187 | var p; 188 | var i:Int = 0; 189 | while(i < activeCount) { 190 | p = sortedParticles[i]; 191 | $b{exprsBeginLocal} 192 | $e{drawExpr} 193 | $b{exprsEndLocal} 194 | i++; 195 | } 196 | g.popTransformation(); 197 | } else { 198 | var p; 199 | var i:Int = 0; 200 | while(i < activeCount) { 201 | p = sortedParticles[i]; 202 | $b{exprsBegin} 203 | $e{drawExpr} 204 | $b{exprsEnd} 205 | i++; 206 | } 207 | } 208 | } 209 | }] 210 | ); 211 | 212 | fields.push(drawFunc); 213 | } 214 | 215 | #end 216 | 217 | } 218 | -------------------------------------------------------------------------------- /sparkler/utils/macro/MacroUtils.hx: -------------------------------------------------------------------------------- 1 | package sparkler.utils.macro; 2 | 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | import haxe.macro.Type; 6 | 7 | class MacroUtils { 8 | 9 | public static function getStringFromTypes(types:Array, delimiter:String = '_', sort:Bool = true):String { 10 | var len = types.length; 11 | var typesStrings = []; 12 | for (i in 0...len) { 13 | switch (types[i]) { 14 | case TInst(t, params): 15 | typesStrings.push(t.toString()); 16 | case TType(t, params): 17 | typesStrings.push(t.toString()); 18 | case TAbstract(t, params): 19 | typesStrings.push(t.toString()); 20 | default: 21 | } 22 | } 23 | 24 | if(sort) { 25 | typesStrings.sort(function(a:String, b:String):Int { 26 | a = a.toUpperCase(); 27 | b = b.toUpperCase(); 28 | 29 | if (a < b) { 30 | return -1; 31 | } else if (a > b) { 32 | return 1; 33 | } else { 34 | return 0; 35 | } 36 | }); 37 | } 38 | 39 | return typesStrings.join(delimiter); 40 | } 41 | 42 | public static function buildTypeExpr(pack:Array, module:String, name:String):Expr { 43 | var pack_module = pack.concat([module, name]); 44 | 45 | var type_expr = macro $i{pack_module[0]}; 46 | for (idx in 1...pack_module.length){ 47 | var field = $i{pack_module[idx]}; 48 | type_expr = macro $type_expr.$field; 49 | } 50 | 51 | return macro $type_expr; 52 | } 53 | 54 | public static inline function toCamelCase(name:String):String { 55 | return name.substr(0, 1).toLowerCase() + name.substr(1); 56 | } 57 | 58 | static public function hasType(types:Array, type:Type):Bool { 59 | for (t in types) { 60 | if(getTypePath(t) == getTypePath(type)) { 61 | return true; 62 | } 63 | } 64 | return false; 65 | } 66 | 67 | static public function hasFieldMeta(field:Field, meta:String):Bool { 68 | return getFieldMeta(field, meta) != null; 69 | } 70 | 71 | static public function getFieldMeta(field:Field, meta:String):MetadataEntry { 72 | for (m in field.meta) { 73 | if(m.name == meta) { 74 | return m; 75 | } 76 | } 77 | return null; 78 | } 79 | 80 | static public function hasField(fields:Array, name:String):Bool { 81 | return getField(fields, name) != null; 82 | } 83 | 84 | static public function getField(fields:Array, name:String):Field { 85 | for (f in fields) { 86 | if(f.name == name) { 87 | return f; 88 | } 89 | } 90 | return null; 91 | } 92 | 93 | static public function hasObjectField(ofields:Array, name:String) { 94 | return getObjectField(ofields, name) != null; 95 | } 96 | 97 | static public function getObjectField(ofields:Array, name:String):ObjectField { 98 | for (f in ofields) { 99 | if(f.field == name) { 100 | return f; 101 | } 102 | } 103 | return null; 104 | } 105 | 106 | #if macro 107 | static public function buildVar(name:String, access:Array, type:ComplexType, e:Expr = null, m:Metadata = null):Field { 108 | return { 109 | pos: Context.currentPos(), 110 | name: name, 111 | access: access, 112 | kind: FVar(type, e), 113 | meta: m == null ? [] : m 114 | }; 115 | } 116 | 117 | static public function buildProp(name:String, access:Array, get:String, set:String, type:ComplexType, e:Expr = null, m:Metadata = null):Field { 118 | return { 119 | pos: Context.currentPos(), 120 | name: name, 121 | access: access, 122 | kind: FProp(get, set, type, e), 123 | meta: m == null ? [] : m 124 | }; 125 | } 126 | 127 | static public function buildFunction(name:String, access:Array, args:Array, ret:ComplexType, exprs:Array, m:Metadata = null):Field { 128 | return { 129 | pos: Context.currentPos(), 130 | name: name, 131 | access: access, 132 | kind: FFun({ 133 | args: args, 134 | ret: ret, 135 | expr: macro $b{exprs} 136 | }), 137 | meta: m == null ? [] : m 138 | }; 139 | } 140 | 141 | static public function buildConstructor(name:String, pack:Array, params:Array, exprs:Array):Expr { 142 | return { 143 | pos: Context.currentPos(), 144 | expr: ENew( 145 | { 146 | name: name, 147 | pack: pack, 148 | params: params 149 | }, 150 | exprs 151 | ) 152 | } 153 | } 154 | #end 155 | 156 | static public function getPathInfo(type:Type):PathInfo { 157 | var data:PathInfo = { 158 | pack: null, 159 | module: null, 160 | name: null 161 | } 162 | 163 | switch (type) { 164 | case TInst(ref, types): 165 | data.pack = ref.get().pack; 166 | data.module = ref.get().module.split('.').pop(); 167 | data.name = ref.get().name; 168 | case TType(ref, types): 169 | data.pack = ref.get().pack; 170 | data.module = ref.get().module.split('.').pop(); 171 | data.name = ref.get().name; 172 | case TAbstract(t, params): 173 | data.pack = t.get().pack; 174 | data.module = t.get().module.split('.').pop(); 175 | data.name = t.get().name; 176 | default: 177 | throw false; 178 | } 179 | 180 | return data; 181 | } 182 | 183 | static public function getTypePath(type:Type):String { 184 | switch (type) { 185 | case TType(ref, types): 186 | return ref.toString(); 187 | case TAbstract(ref, types): 188 | return ref.toString(); 189 | case TInst(ref, types): 190 | return ref.toString(); 191 | default: 192 | throw false; 193 | } 194 | 195 | return null; 196 | } 197 | 198 | static public function getClassTypePath(type:haxe.macro.Type) { 199 | switch (type) { 200 | case TType(ref, types): 201 | var name = ref.get().name; 202 | if(!StringTools.startsWith(name, 'Class')) throw '$name must be Class'; 203 | var pack = name.substring(6, name.length-1).split('.'); 204 | name = pack.pop(); 205 | return {pack: pack, name: name, sub: null, params: []}; 206 | default: 207 | throw 'Invalid type'; 208 | } 209 | } 210 | 211 | static public function subclasses(type:ClassType, root:String):Bool { 212 | var name = type.module + '.' + type.name; 213 | return (name.substr(0, root.length) == root || type.superClass != null && subclasses(type.superClass.t.get(), root)); 214 | } 215 | 216 | } 217 | 218 | typedef PathInfo = { 219 | var pack:Array; 220 | var module:String; 221 | var name:String; 222 | } 223 | 224 | enum abstract OFType(Int){ 225 | var UNKNOWN; 226 | var VECTOR; 227 | var RANGE; 228 | var LIST; 229 | var LIST_RANGE; 230 | var INT; 231 | var FLOAT; 232 | } -------------------------------------------------------------------------------- /sparkler/ParticleEmitter.hx: -------------------------------------------------------------------------------- 1 | package sparkler; 2 | 3 | import sparkler.utils.Color; 4 | import sparkler.Particle; 5 | 6 | 7 | /* 8 | onUpdate order 9 | 10 | 0 p.lifeProgress = p.age / p.life 11 | p.prevPos = p.pos 12 | 13 | // 10 - velocity start 14 | 15 | 10 p.velocity += velocity 16 | 20 p.velocity += force 17 | 30 p.velocity *= drag 18 | 19 | 40 p.angularVelocity += angularVelocity 20 | 50 p.angularVelocity *= angularDrag 21 | 22 | // 60 - velocity end 23 | 24 | // 60 - position start 25 | 26 | 60 p.pos += velocity 27 | 28 | // 70 - position end 29 | 30 | 70 p.speed = p.pos - p.prevPos 31 | 32 | 80 p.color = color 33 | 90 p.size = size 34 | 100 p.scale = scale 35 | 110 p.rotation = rotation or angularVelocity 36 | 37 | */ 38 | 39 | #if !macro 40 | @:genericBuild(sparkler.utils.macro.ParticleEmitterMacro.build()) 41 | #end 42 | 43 | class ParticleEmitter {} 44 | 45 | class ParticleEmitterBase implements IParticleEmitter{ 46 | 47 | public var x(get, set):Float; 48 | var _x:Float = 0; 49 | inline function get_x() return _x; 50 | function set_x(v:Float):Float { 51 | _lastX = _x; 52 | _transformDirty = true; 53 | return _x = v; 54 | } 55 | 56 | public var y(get, set):Float; 57 | var _y:Float = 0; 58 | inline function get_y() return _y; 59 | function set_y(v:Float):Float { 60 | _lastY = _y; 61 | _transformDirty = true; 62 | return _y = v; 63 | } 64 | 65 | public var rotation(get, set):Float; 66 | var _rotation:Float = 0; 67 | inline function get_rotation() return _rotation; 68 | function set_rotation(v:Float):Float { 69 | _rotation = v; 70 | _transformDirty = true; 71 | return _rotation = v; 72 | } 73 | 74 | public var scaleX(get, set):Float; 75 | var _scaleX:Float = 0; 76 | inline function get_scaleX() return _scaleX; 77 | function set_scaleX(v:Float):Float { 78 | _scaleX = v; 79 | _transformDirty = true; 80 | return _scaleX = v; 81 | } 82 | 83 | public var scaleY(get, set):Float; 84 | var _scaleY:Float = 0; 85 | inline function get_scaleY() return _scaleY; 86 | function set_scaleY(v:Float):Float { 87 | _scaleY = v; 88 | _transformDirty = true; 89 | return _scaleY = v; 90 | } 91 | 92 | public var originX(get, set):Float; 93 | var _originX:Float = 0; 94 | inline function get_originX() return _originX; 95 | function set_originX(v:Float):Float { 96 | _originX = v; 97 | _transformDirty = true; 98 | return _originX = v; 99 | } 100 | 101 | public var originY(get, set):Float; 102 | var _originY:Float = 0; 103 | inline function get_originY() return _originY; 104 | function set_originY(v:Float):Float { 105 | _originY = v; 106 | _transformDirty = true; 107 | return _originY = v; 108 | } 109 | 110 | var _lastX:Float = 0; 111 | var _lastY:Float = 0; 112 | 113 | var _transformDirty:Bool = true; 114 | 115 | // emitter name 116 | public var name:String; 117 | // if the emitter is active, it will update 118 | public var active:Bool = true; 119 | // if the emitter is enabled, it's spawn and update modules 120 | public var enabled:Bool = false; 121 | 122 | public var cacheSize(default, null):Int = 512; 123 | public var progress(default, null):Float = 0; 124 | public var cacheWrap:Bool = false; 125 | public var localSpace:Bool = false; 126 | public var preprocess:Float = 0; 127 | public var loops:Int = 0; 128 | 129 | public var activeCount(default, null):Int = 0; 130 | public var particles:haxe.ds.Vector; 131 | 132 | public var random:()->Float; 133 | public var sortFunc:(a:T, b:T)->Int; 134 | 135 | var _loopsCounter:Int = 0; 136 | var _wrapIdx:Int = 0; 137 | var _frameTime:Float = 0; 138 | 139 | // matrix 140 | var _a:Float = 0; 141 | var _b:Float = 0; 142 | var _c:Float = 0; 143 | var _d:Float = 0; 144 | var _tx:Float = 0; 145 | var _ty:Float = 0; 146 | 147 | var _sin:Float = 0; 148 | var _cos:Float = 0; 149 | 150 | // for sorting 151 | var _particlesSorted:haxe.ds.Vector; 152 | var _particlesSortTmp:haxe.ds.Vector; 153 | 154 | public function new(options:ParticleEmitterOptions) { 155 | if(options.x != null) _x = options.x; 156 | if(options.y != null) _y = options.y; 157 | if(options.rotation != null) rotation = options.rotation; 158 | if(options.scaleX != null) scaleX = options.scaleX; 159 | if(options.scaleY != null) scaleY = options.scaleY; 160 | if(options.originX != null) originX = options.originX; 161 | if(options.originY != null) originY = options.originY; 162 | 163 | _lastX = _x; 164 | _lastY = _y; 165 | 166 | if(options.active != null) active = options.active; 167 | if(options.enabled != null) enabled = options.enabled; 168 | if(options.cacheSize != null) cacheSize = options.cacheSize; 169 | if(options.cacheWrap != null) cacheWrap = options.cacheWrap; 170 | if(options.localSpace != null) localSpace = options.localSpace; 171 | if(options.preprocess != null) preprocess = options.preprocess; 172 | if(options.loops != null) loops = options.loops; 173 | 174 | random = options.random != null ? options.random : Math.random; 175 | particles = new haxe.ds.Vector(cacheSize); 176 | _particlesSorted = new haxe.ds.Vector(cacheSize); 177 | _particlesSortTmp = new haxe.ds.Vector(cacheSize); 178 | } 179 | 180 | public final function start() { 181 | _loopsCounter = 0; 182 | startInternal(); 183 | emit(); 184 | if(preprocess > 0) { 185 | update(preprocess); 186 | } 187 | } 188 | 189 | public final function stop() { 190 | stopInternal(); 191 | } 192 | 193 | public final function update(elapsed:Float) { 194 | if(!active) return; 195 | _frameTime = elapsed; 196 | updateTransform(); 197 | 198 | onUpdate(elapsed); 199 | 200 | _lastX = _x; 201 | _lastY = _y; 202 | } 203 | 204 | public final function emit() { 205 | updateTransform(); 206 | onEmit(); 207 | } 208 | 209 | public function unspawnAll() { 210 | for (i in 0...activeCount) { 211 | onParticleUnspawn(particles[i]); 212 | } 213 | activeCount = 0; 214 | } 215 | 216 | function startInternal() { 217 | progress = 0; 218 | _wrapIdx = 0; 219 | enabled = true; 220 | _lastX = _x; 221 | _lastY = _y; 222 | updateTransform(); 223 | onStart(); 224 | } 225 | 226 | function stopInternal() { 227 | enabled = false; 228 | onStop(); 229 | } 230 | 231 | function restart() { 232 | stopInternal(); 233 | startInternal(); 234 | } 235 | 236 | function step(elapsed:Float) { 237 | onStepStart(elapsed); 238 | onStep(elapsed); 239 | onStepEnd(elapsed); 240 | } 241 | 242 | function setParticlePos(p:T, x:Float, y:Float) { 243 | if(localSpace) { 244 | p.x = x; 245 | p.y = y; 246 | } else { 247 | p.x = getTransformX(x, y); 248 | p.y = getTransformY(x, y); 249 | } 250 | } 251 | 252 | function updateParticles(elapsed:Float) { 253 | var p:T; 254 | var i:Int = 0; 255 | var len:Int = activeCount; 256 | var timeLeft:Float = 0; 257 | while(i < len) { 258 | p = particles[i]; 259 | if(p.age + elapsed >= p.lifetime) { 260 | timeLeft = (p.age + elapsed) - p.lifetime; 261 | if(timeLeft > 0) onParticleUpdate(p, timeLeft); 262 | p.age += timeLeft; 263 | unspawn(p); 264 | len = activeCount; 265 | } else { 266 | p.age += elapsed; 267 | onParticleUpdate(p, elapsed); 268 | i++; 269 | } 270 | } 271 | } 272 | 273 | final function spawn() { 274 | if(activeCount < cacheSize) { 275 | var p = particles[activeCount]; 276 | activeCount++; 277 | spawnParticle(p); 278 | } else if(cacheWrap) { 279 | var lastIdx = activeCount-1; 280 | swapParticles(_wrapIdx % lastIdx, lastIdx); 281 | _wrapIdx++; 282 | var p = particles[lastIdx]; 283 | unspawnParticle(p); 284 | spawnParticle(p); 285 | } 286 | } 287 | 288 | final function unspawn(p:T) { 289 | swapParticles(p.index, activeCount-1); 290 | activeCount--; 291 | } 292 | 293 | final function swapParticles(a:Int, b:Int) { 294 | var pA = particles[a]; 295 | var pB = particles[b]; 296 | 297 | particles[a] = pB; 298 | particles[b] = pA; 299 | 300 | pB.index = a; 301 | pA.index = b; 302 | } 303 | 304 | inline function spawnParticle(p:T) { 305 | p.age = 0; 306 | onParticleSpawn(p); 307 | } 308 | 309 | inline function unspawnParticle(p:T) { 310 | onParticleUnspawn(p); 311 | } 312 | 313 | function getSorted():haxe.ds.Vector { 314 | if(sortFunc != null) { 315 | var i:Int = 0; 316 | while(i < activeCount) { 317 | _particlesSorted[i] = particles[i]; 318 | i++; 319 | } 320 | sort(_particlesSorted, _particlesSortTmp, 0, activeCount-1, sortFunc); 321 | return _particlesSorted; 322 | } else { 323 | return particles; 324 | } 325 | } 326 | 327 | function onEmit() {} 328 | function onStart() {} 329 | function onStop() {} 330 | function onUpdate(elapsed:Float) {} 331 | function onStepStart(elapsed:Float) {} 332 | function onStep(elapsed:Float) {} 333 | function onStepEnd(elapsed:Float) {} 334 | function onParticleUpdate(p:T, elapsed:Float) {} 335 | function onParticleSpawn(p:T) {} 336 | function onParticleUnspawn(p:T) {} 337 | 338 | function updateTransform() { 339 | if (_transformDirty) { 340 | setTransform(_x, _y, rotation, scaleX, scaleY, originX, originY, 0, 0); 341 | _transformDirty = false; 342 | } 343 | } 344 | 345 | function getRotateX(x:Float, y:Float):Float { 346 | return _cos * x - _sin * y; 347 | } 348 | 349 | function getRotateY(x:Float, y:Float):Float { 350 | return _sin * x + _cos * y; 351 | } 352 | 353 | function getTransformX(x:Float, y:Float):Float { 354 | return _a * x + _c * y + _tx; 355 | } 356 | 357 | function getTransformY(x:Float, y:Float):Float { 358 | return _b * x + _d * y + _ty; 359 | } 360 | 361 | function setTransform(x:Float, y:Float, angle:Float, sx:Float, sy:Float, ox:Float, oy:Float, kx:Float, ky:Float) { 362 | _sin = Math.sin(angle); 363 | _cos = Math.cos(angle); 364 | 365 | _a = _cos * sx - ky * _sin * sy; 366 | _b = _sin * sx + ky * _cos * sy; 367 | _c = kx * _cos * sx - _sin * sy; 368 | _d = kx * _sin * sx + _cos * sy; 369 | _tx = x - ox * _a - oy * _c; 370 | _ty = y - ox * _b - oy * _d; 371 | } 372 | 373 | final function random1To1(){ 374 | return random() * 2 - 1; 375 | } 376 | 377 | final function randomInt(min:Float, ?max:Null):Int { 378 | return Math.floor(randomFloat(min, max)); 379 | } 380 | 381 | final function randomFloat(min:Float, ?max:Null):Float { 382 | if(max == null) { max = min; min = 0; } 383 | return random() * (max - min) + min; 384 | } 385 | 386 | // merge sort 387 | function sort(a:haxe.ds.Vector, aux:haxe.ds.Vector, l:Int, r:Int, compare:(p1:T, p2:T)->Int) { 388 | if (l < r) { 389 | var m = Std.int(l + (r - l) / 2); 390 | sort(a, aux, l, m, compare); 391 | sort(a, aux, m + 1, r, compare); 392 | merge(a, aux, l, m, r, compare); 393 | } 394 | } 395 | 396 | inline function merge(a:haxe.ds.Vector, aux:haxe.ds.Vector, l:Int, m:Int, r:Int, compare:(p1:T, p2:T)->Int) { 397 | var k = l; 398 | while (k <= r) { 399 | aux[k] = a[k]; 400 | k++; 401 | } 402 | 403 | k = l; 404 | var i = l; 405 | var j = m + 1; 406 | while (k <= r) { 407 | if (i > m) a[k] = aux[j++]; 408 | else if (j > r) a[k] = aux[i++]; 409 | else if (compare(aux[j], aux[i]) < 0) a[k] = aux[j++]; 410 | else a[k] = aux[i++]; 411 | k++; 412 | } 413 | } 414 | 415 | } 416 | 417 | interface IParticleEmitter { 418 | 419 | public var cacheSize(default, null):Int; 420 | public var activeCount(default, null):Int; 421 | public var progress(default, null):Float; 422 | public var localSpace:Bool; 423 | public var enabled:Bool; 424 | public var particles:haxe.ds.Vector; 425 | public var loops:Int; 426 | public var random:()->Float; 427 | public var sortFunc:(a:T, b:T)->Int; 428 | 429 | private var _rotation:Float; 430 | private var _scaleX:Float; 431 | private var _scaleY:Float; 432 | private var _originX:Float; 433 | private var _originY:Float; 434 | 435 | private var _x:Float; 436 | private var _y:Float; 437 | private var _lastX:Float; 438 | private var _lastY:Float; 439 | private var _frameTime:Float; 440 | private var _loopsCounter:Int; 441 | 442 | private var _a:Float; 443 | private var _b:Float; 444 | private var _c:Float; 445 | private var _d:Float; 446 | private var _tx:Float; 447 | private var _ty:Float; 448 | 449 | public function emit():Void; 450 | public function start():Void; 451 | public function stop():Void; 452 | private function setParticlePos(p:T, x:Float, y:Float):Void; 453 | private function updateParticles(elapsed:Float):Void; 454 | private function getSorted():haxe.ds.Vector; 455 | private function restart():Void; 456 | private function spawn():Void; 457 | private function unspawn(p:T):Void; 458 | private function step(elapsed:Float):Void; 459 | 460 | private function onEmit():Void; 461 | private function onUpdate(elapsed:Float):Void; 462 | private function onStepStart(elapsed:Float):Void; 463 | private function onStep(elapsed:Float):Void; 464 | private function onStepEnd(elapsed:Float):Void; 465 | private function onStart():Void; 466 | private function onStop():Void; 467 | 468 | private function onParticleUpdate(p:T, elapsed:Float):Void; 469 | private function onParticleSpawn(p:T):Void; 470 | private function onParticleUnspawn(p:T):Void; 471 | 472 | private function getRotateX(x:Float, y:Float):Float; 473 | private function getRotateY(x:Float, y:Float):Float; 474 | 475 | private function getTransformX(x:Float, y:Float):Float; 476 | private function getTransformY(x:Float, y:Float):Float; 477 | 478 | private function random1To1():Float; 479 | private function randomInt(min:Float, ?max:Null):Int; 480 | private function randomFloat(min:Float, ?max:Null):Float; 481 | 482 | } 483 | 484 | typedef ParticleEmitterOptions = { 485 | ?name:String, 486 | ?x:Float, 487 | ?y:Float, 488 | ?rotation:Float, 489 | ?scaleX:Float, 490 | ?scaleY:Float, 491 | ?originX:Float, 492 | ?originY:Float, 493 | 494 | ?active:Bool, 495 | ?enabled:Bool, 496 | ?cacheSize:Int, 497 | ?cacheWrap:Bool, 498 | ?localSpace:Bool, 499 | ?preprocess:Float, 500 | 501 | ?loops:Int, 502 | 503 | ?random:()->Float, 504 | } 505 | -------------------------------------------------------------------------------- /sparkler/utils/macro/ParticleEmitterMacro.hx: -------------------------------------------------------------------------------- 1 | package sparkler.utils.macro; 2 | 3 | #if (macro || display) 4 | 5 | import haxe.macro.Context; 6 | import haxe.macro.Expr; 7 | import haxe.macro.Type; 8 | import haxe.macro.TypeTools; 9 | import haxe.macro.ComplexTypeTools; 10 | 11 | import sparkler.utils.macro.MacroUtils; 12 | import sparkler.utils.macro.ParticleModuleMacro; 13 | import sparkler.utils.macro.ParticleModuleMacro.ParticleModuleMacroOptions; 14 | 15 | class ParticleEmitterMacro { 16 | 17 | public static inline var particleModulePath:String = 'sparkler.ParticleModule'; 18 | public static inline var particleInjectModulePath:String = 'sparkler.ParticleInjectModule'; 19 | 20 | static public var typeName:String = "ParticleEmitter"; 21 | static public var emitterIds:Map = new Map(); 22 | 23 | static var idx:Int = 0; 24 | static var emitterTypeNames:Array = ['ParticleEmitterBase']; 25 | 26 | static var iModules:Array = [ 27 | {name:'sparkler.modules.render.NucSpriteRendererModule', inject:sparkler.modules.render.NucSpriteRendererModule.inject}, 28 | {name:'sparkler.modules.render.ClaySpriteRendererModule', inject:sparkler.modules.render.ClaySpriteRendererModule.inject}, 29 | {name:'sparkler.modules.render.KhaSpriteRendererModule', inject:sparkler.modules.render.KhaSpriteRendererModule.inject} 30 | ]; 31 | 32 | static var filterMeta:Map> = new Map(); 33 | 34 | static public function build() { 35 | return switch (Context.getLocalType()) { 36 | case TInst(t, p): 37 | buildEmitter(p); 38 | default: 39 | throw false; 40 | } 41 | } 42 | 43 | static function buildEmitter(types:Array) { 44 | var eName = getEmitterName(types); 45 | 46 | if(emitterTypeNames.indexOf(eName) == -1) { 47 | var pos = Context.currentPos(); 48 | 49 | var groupNames:Array = []; 50 | 51 | filterMeta = new Map(); 52 | 53 | var fields:Array = []; 54 | var particleTypes:Array = []; 55 | var optFields:Array = []; 56 | 57 | // get module options 58 | var modulesOpt:Array = []; 59 | var additionalModuleTypes:Array = []; 60 | var moduleName:String; 61 | var moduleOptions:ParticleModuleMacroOptions; 62 | for (t in types) { 63 | moduleName = MacroUtils.getTypePath(t); 64 | moduleOptions = ParticleModuleMacro.moduleOptions.get(moduleName); 65 | 66 | // get additional module types 67 | for (am in moduleOptions.addModules) { 68 | additionalModuleTypes.push(am); 69 | } 70 | 71 | modulesOpt.push(moduleOptions); 72 | } 73 | 74 | // add additional module options 75 | for (t in additionalModuleTypes) { 76 | TypeTools.getClass(t); // build type 77 | moduleName = MacroUtils.getTypePath(t); 78 | moduleOptions = ParticleModuleMacro.moduleOptions.get(moduleName); 79 | if(modulesOpt.indexOf(moduleOptions) == -1) modulesOpt.push(moduleOptions); 80 | } 81 | 82 | // check modules groups 83 | for (o in modulesOpt) { 84 | if(o.group == null) continue; 85 | 86 | if(groupNames.indexOf(o.group) != -1) { 87 | throw('Cant add ${o.name}, more than one module in group "${o.group}" is not allowed'); 88 | } 89 | groupNames.push(o.group); 90 | } 91 | 92 | addDefaultModules(groupNames, modulesOpt); 93 | 94 | // sort modules 95 | modulesOpt.sort(function(a:ParticleModuleMacroOptions, b:ParticleModuleMacroOptions) { 96 | return Std.int(a.priority - b.priority); 97 | }); 98 | 99 | // get particle types from all modules 100 | for (o in modulesOpt) { 101 | // trace('${o.priority}: ${o.name}'); 102 | if(o.isInjectType) continue; 103 | var pTypes = ParticleMacro.particleTypes.get(o.particleTypeName); 104 | if(pTypes != null) { 105 | for (pt in pTypes) { 106 | if(!MacroUtils.hasType(particleTypes, pt)) { 107 | particleTypes.push(pt); 108 | } 109 | } 110 | } 111 | } 112 | 113 | // get particle types names 114 | var particleFieldNames:Array = []; 115 | 116 | for (pt in particleTypes) { 117 | var info = MacroUtils.getPathInfo(pt); 118 | var compName = MacroUtils.toCamelCase(info.name); 119 | particleFieldNames.push(compName); 120 | } 121 | 122 | // build particle type 123 | var pType = ParticleMacro.buildParticle(particleTypes); 124 | var pInfo = MacroUtils.getPathInfo(pType); 125 | var pTInfo = {pack: pInfo.pack, name: pInfo.module, sub: pInfo.name}; 126 | var pCType = Context.toComplexType(pType); 127 | 128 | // push sortFunc option 129 | var sortFuncOpt = MacroUtils.buildVar( 130 | 'sortFunc', 131 | [Access.APublic], 132 | macro: (a:$pCType, b:$pCType)->Int, 133 | null, 134 | [{name: ':optional', pos: pos}] 135 | ); 136 | optFields.push(sortFuncOpt); 137 | 138 | // setup emitter exprs 139 | var newExprs:Array = [ 140 | macro { 141 | super(options); 142 | if(options.sortFunc != null) sortFunc = options.sortFunc; 143 | } 144 | ]; 145 | 146 | var emitExprs:Array = []; 147 | var onStartExprs:Array = []; 148 | var onStopExprs:Array = []; 149 | 150 | var onUpdateExprs:Array = []; 151 | var onStepStartExprs:Array = []; 152 | var onStepExprs:Array = []; 153 | var onStepEndExprs:Array = []; 154 | var onParticleUpdateExprs:Array = []; 155 | var onParticleSpawnExprs:Array = []; 156 | var onParticleUnspawnExprs:Array = []; 157 | 158 | var emitterImports:Array = []; 159 | 160 | var peopt:ParticleEmitterBuildOptions = { 161 | groupNames: groupNames, 162 | particleType: pType, 163 | particleFieldNames: particleFieldNames, 164 | fields: fields, 165 | optFields: optFields, 166 | 167 | newExprs: newExprs, 168 | emitExprs: emitExprs, 169 | onStartExprs: onStartExprs, 170 | onStopExprs: onStopExprs, 171 | onUpdateExprs: onUpdateExprs, 172 | onStepStartExprs: onStepStartExprs, 173 | onStepExprs: onStepExprs, 174 | onStepEndExprs: onStepEndExprs, 175 | onParticleUpdateExprs: onParticleUpdateExprs, 176 | onParticleSpawnExprs: onParticleSpawnExprs, 177 | onParticleUnspawnExprs: onParticleUnspawnExprs, 178 | 179 | emitterImports: emitterImports 180 | }; 181 | 182 | // inject modules 183 | injectModules(peopt, modulesOpt); 184 | 185 | // add default exprs 186 | injectDefaultExprs(peopt); 187 | 188 | // create options type 189 | var optPTInfo = {pack: ['sparkler'], name: 'ParticleEmitter', sub: 'ParticleEmitterOptions'}; 190 | var optName = '${eName}Options'; 191 | 192 | var opDef:TypeDefinition = { 193 | fields: [], 194 | kind: TDAlias(TExtend([optPTInfo], optFields)), 195 | pack: ['sparkler'], 196 | name: optName, 197 | pos: pos 198 | }; 199 | 200 | Context.defineType(opDef); 201 | 202 | var opType = Context.getType('sparkler.$optName'); 203 | var opCType = Context.toComplexType(opType); 204 | 205 | // create emitter fields 206 | newExprs.push(macro { 207 | var p; 208 | for (i in 0...cacheSize) { 209 | p = new $pTInfo(i); 210 | p.index = i; 211 | particles[i] = p; 212 | } 213 | }); 214 | 215 | // TODO: move to function 216 | if(newExprs.length > 0) { 217 | var newField = MacroUtils.buildFunction( 218 | 'new', 219 | [APublic], 220 | [{name: 'options', type: opCType}], 221 | macro: Void, 222 | newExprs 223 | ); 224 | fields.push(newField); 225 | } 226 | 227 | if(onStartExprs.length > 0) { 228 | var onStart = MacroUtils.buildFunction( 229 | 'onStart', 230 | [AOverride], 231 | [], 232 | macro: Void, 233 | onStartExprs 234 | ); 235 | fields.push(onStart); 236 | } 237 | 238 | if(emitExprs.length > 0) { 239 | var emit = MacroUtils.buildFunction( 240 | 'onEmit', 241 | [AOverride], 242 | [], 243 | macro: Void, 244 | emitExprs 245 | ); 246 | fields.push(emit); 247 | } 248 | 249 | if(onStopExprs.length > 0) { 250 | var onStop = MacroUtils.buildFunction( 251 | 'onStop', 252 | [AOverride], 253 | [], 254 | macro: Void, 255 | onStopExprs 256 | ); 257 | fields.push(onStop); 258 | } 259 | 260 | if(onStepExprs.length > 0) { 261 | var onStepField = MacroUtils.buildFunction( 262 | 'onStep', 263 | [AOverride], 264 | [{name: 'elapsed', type: macro:Float}], 265 | macro: Void, 266 | onStepExprs 267 | ); 268 | fields.push(onStepField); 269 | } 270 | 271 | if(onUpdateExprs.length > 0) { 272 | var onUpdateField = MacroUtils.buildFunction( 273 | 'onUpdate', 274 | [AOverride], 275 | [{name: 'elapsed', type: macro:Float}], 276 | macro: Void, 277 | onUpdateExprs 278 | ); 279 | fields.push(onUpdateField); 280 | } 281 | 282 | if(onStepStartExprs.length > 0) { 283 | var onStepStartField = MacroUtils.buildFunction( 284 | 'onStepStart', 285 | [AOverride], 286 | [{name: 'elapsed', type: macro:Float}], 287 | macro: Void, 288 | onStepStartExprs 289 | ); 290 | fields.push(onStepStartField); 291 | } 292 | 293 | if(onStepEndExprs.length > 0) { 294 | var onStepEndField = MacroUtils.buildFunction( 295 | 'onStepEnd', 296 | [AOverride], 297 | [{name: 'elapsed', type: macro:Float}], 298 | macro: Void, 299 | onStepEndExprs 300 | ); 301 | fields.push(onStepEndField); 302 | } 303 | 304 | if(onParticleUpdateExprs.length > 0) { 305 | var onParticleUpdateField = MacroUtils.buildFunction( 306 | 'onParticleUpdate', 307 | [AOverride], 308 | [{name: 'p', type: pCType}, {name: 'elapsed', type: macro:Float}], 309 | macro: Void, 310 | onParticleUpdateExprs 311 | ); 312 | fields.push(onParticleUpdateField); 313 | } 314 | 315 | if(onParticleSpawnExprs.length > 0) { 316 | var onParticleSpawnField = MacroUtils.buildFunction( 317 | 'onParticleSpawn', 318 | [AOverride], 319 | [{name: 'p', type: pCType}], 320 | macro: Void, 321 | onParticleSpawnExprs 322 | ); 323 | fields.push(onParticleSpawnField); 324 | } 325 | 326 | if(onParticleUnspawnExprs.length > 0) { 327 | var onParticleUnspawnField = MacroUtils.buildFunction( 328 | 'onParticleUnspawn', 329 | [AOverride], 330 | [{name: 'p', type: pCType}], 331 | macro: Void, 332 | onParticleUnspawnExprs 333 | ); 334 | fields.push(onParticleUnspawnField); 335 | } 336 | 337 | // filter and check imports 338 | emitterImports = filterDuplicateImport(emitterImports); 339 | checkDuplicateClassImport(emitterImports); 340 | 341 | // define emitter type 342 | var eTDef:TypeDefinition = { 343 | pack: ['sparkler'], 344 | name: eName, 345 | pos: pos, 346 | meta: [], 347 | kind: TDClass({ 348 | pack: ["sparkler"], 349 | name: "ParticleEmitter", 350 | sub: "ParticleEmitterBase", 351 | params : [TPType(TPath(pTInfo))] 352 | }), 353 | fields: fields 354 | } 355 | 356 | emitterTypeNames.push(eName); 357 | 358 | Context.defineModule('sparkler.${eName}', [eTDef], emitterImports); 359 | } 360 | 361 | return Context.getType('sparkler.${eName}'); 362 | } 363 | 364 | static function filterDuplicateImport(importExprs:Array):Array { 365 | var importsNames:Array = []; 366 | var importsClean:Array = []; 367 | 368 | // clear imports 369 | var iPath:String; 370 | for (i in importExprs) { 371 | iPath = getImportPathString(i); 372 | if(importsNames.indexOf(iPath) == -1) { 373 | importsNames.push(iPath); 374 | importsClean.push(i); 375 | } 376 | } 377 | return importsClean; 378 | } 379 | 380 | static function getImportPathString(iExpr:ImportExpr):String { 381 | var iPath = ''; 382 | for (j in 0...iExpr.path.length) { 383 | iPath += iExpr.path[j].name; 384 | if(j < iExpr.path.length-1) { 385 | iPath += '.'; 386 | } 387 | } 388 | return iPath; 389 | } 390 | 391 | static function checkDuplicateClassImport(importExprs:Array) { 392 | var importsClassNames:Array = []; 393 | var importPaths:Array = []; 394 | 395 | var iName:String; 396 | var iPath:String; 397 | for (i in importExprs) { 398 | iPath = getImportPathString(i); 399 | iName = i.path[i.path.length-1].name; 400 | var idx = importsClassNames.indexOf(iName); 401 | if(idx == -1) { 402 | importPaths.push(iPath); 403 | importsClassNames.push(iName); 404 | } else { 405 | var otherIPath = importPaths[idx]; 406 | trace('Warning: import class with different path but same name: "${otherIPath}", and "${iPath}"'); 407 | } 408 | } 409 | } 410 | 411 | // TODO: add from array of default modules 412 | static function addDefaultModules(groupNames:Array, modulesOpt:Array) { 413 | if(groupNames.indexOf('emit') == -1) { 414 | modulesOpt.push(getModuleOptionsFromClassName('sparkler.modules.emit.EmitRateModule')); 415 | } 416 | 417 | if(groupNames.indexOf('lifetime') == -1) { 418 | modulesOpt.push(getModuleOptionsFromClassName('sparkler.modules.life.LifetimeModule')); 419 | } 420 | 421 | if(groupNames.indexOf('emitterLife') == -1) { 422 | modulesOpt.push(getModuleOptionsFromClassName('sparkler.modules.life.EmitterLifetimeModule')); 423 | } 424 | } 425 | 426 | static function injectDefaultExprs(options:ParticleEmitterBuildOptions) { 427 | var groupNames = options.groupNames; 428 | var onParticleSpawnExprs = options.onParticleSpawnExprs; 429 | var emitExprs = options.emitExprs; 430 | 431 | if(groupNames.indexOf('particlesPerEmit') == -1) { 432 | emitExprs.insert(0, macro { 433 | spawn(); 434 | }); 435 | } 436 | 437 | if(groupNames.indexOf('spawn') == -1) { 438 | onParticleSpawnExprs.insert(0, macro { 439 | setParticlePos(p, 0, 0); 440 | }); 441 | } 442 | } 443 | 444 | static function injectModules(options:ParticleEmitterBuildOptions, modulesOpt:Array) { 445 | var fields = options.fields; 446 | var optFields = options.optFields; 447 | 448 | var newExprs = options.newExprs; 449 | var emitExprs = options.emitExprs; 450 | var onUpdateExprs = options.onUpdateExprs; 451 | var onStepStartExprs = options.onStepStartExprs; 452 | var onStepExprs = options.onStepExprs; 453 | var onStepEndExprs = options.onStepEndExprs; 454 | var onParticleSpawnExprs = options.onParticleSpawnExprs; 455 | var onParticleUnspawnExprs = options.onParticleUnspawnExprs; 456 | var onParticleUpdateExprs = options.onParticleUpdateExprs; 457 | var onStartExprs = options.onStartExprs; 458 | var onStopExprs = options.onStopExprs; 459 | var emitterImports = options.emitterImports; 460 | 461 | // inject function exprs 462 | for (o in modulesOpt) { 463 | if(o.isInjectType) { 464 | for (im in iModules) { 465 | if(o.name == im.name) { 466 | im.inject(options); 467 | break; 468 | } 469 | } 470 | continue; 471 | } 472 | 473 | var moduleFields = o.fields; 474 | var moduleImports = o.imports; 475 | for (i in moduleImports) emitterImports.push(i); 476 | 477 | for (mf in moduleFields) { 478 | var filterTag:String = null; 479 | for (m in mf.meta) { 480 | switch (m.name) { 481 | case 'filter': 482 | filterTag = getMetaString(m); 483 | if(filterTag == null) throw('filter must have tag: "@filter("tag")"'); 484 | default: 485 | } 486 | } 487 | 488 | 489 | switch (mf.kind) { 490 | case FVar(t, e): 491 | //TODO: set emitter Particle type if so 492 | pushFilteredField(fields, filterTag, mf); 493 | case FProp(g, s): 494 | //TODO: set emitter Particle type if so 495 | pushFilteredField(fields, filterTag, mf); 496 | case FFun(f): 497 | switch (mf.name) { 498 | case 'new': 499 | switch (mf.kind) { 500 | case FFun(ff): 501 | if(ff.args.length != 1 || ff.args[0].name != 'options') { 502 | throw('number arguments of constructor must be one, and name options'); 503 | } 504 | for (arg in ff.args) { 505 | switch (arg.type) { 506 | case TAnonymous(aFields): 507 | for (af in aFields) { 508 | optFields.push(af); 509 | } 510 | default: throw('options type must be Anonymous structure'); 511 | } 512 | } 513 | default: 514 | } 515 | newExprs.push(f.expr); 516 | 517 | case 'emit': pushFilteredExpr(emitExprs, filterTag, 'emit', f.expr, false); 518 | case 'onStepStart': pushFilteredExpr(onStepStartExprs, filterTag, 'onStepStart', f.expr, false); 519 | case 'onUpdate': pushFilteredExpr(onUpdateExprs, filterTag, 'onUpdate', f.expr, false); 520 | case 'onStep': pushFilteredExpr(onStepExprs, filterTag, 'onStep', f.expr, false); 521 | case 'onStepEnd': pushFilteredExpr(onStepEndExprs, filterTag, 'onStepEnd', f.expr, false); 522 | case 'onParticleSpawn': pushFilteredExpr(onParticleSpawnExprs, filterTag, 'onParticleSpawn', f.expr, false); 523 | case 'onParticleUnspawn': pushFilteredExpr(onParticleUnspawnExprs, filterTag, 'onParticleUnspawn', f.expr, false); 524 | case 'onParticleUpdate': pushFilteredExpr(onParticleUpdateExprs, filterTag, 'onParticleUpdate', f.expr, false); 525 | case 'onStart': pushFilteredExpr(onStartExprs, filterTag, 'onStart', f.expr, false); 526 | case 'onStop': pushFilteredExpr(onStopExprs, filterTag, 'onStop', f.expr, false); 527 | 528 | case 'onPreStepStart': pushFilteredExpr(onStepStartExprs, filterTag, 'onStepStart', f.expr, true); 529 | case 'onPreStep': pushFilteredExpr(onStepExprs, filterTag, 'onStep', f.expr, true); 530 | case 'onPreStepEnd': pushFilteredExpr(onStepEndExprs, filterTag, 'onStepEnd', f.expr, true); 531 | case 'onPreParticleSpawn': pushFilteredExpr(onParticleSpawnExprs, filterTag, 'onParticleSpawn', f.expr, true); 532 | case 'onPreParticleUnspawn': pushFilteredExpr(onParticleUnspawnExprs, filterTag, 'onParticleUnspawn', f.expr, true); 533 | case 'onPreParticleUpdate': pushFilteredExpr(onParticleUpdateExprs, filterTag, 'onParticleUpdate', f.expr, true); 534 | case 'onPreStart': pushFilteredExpr(onStartExprs, filterTag, 'onStart', f.expr, true); 535 | case 'onPreStop': pushFilteredExpr(onStopExprs, filterTag, 'onStop', f.expr, true); 536 | 537 | case 'onPostStepStart' | 'onPostStep' | 'onPostStepEnd' | 538 | 'onPostParticleSpawn' | 'onPostParticleUnspawn' | 539 | 'onPostParticleUpdate' | 'onPostStart' | 'onPostStop': 540 | 541 | default: 542 | setFunctionArgsParticleType(mf, options.particleType); 543 | pushFilteredField(fields, filterTag, mf); 544 | } 545 | default: 546 | } 547 | } 548 | } 549 | for (o in modulesOpt) { 550 | if(o.isInjectType) continue; 551 | 552 | var moduleFields = o.fields; 553 | var moduleImports = o.imports; 554 | 555 | for (mf in moduleFields) { 556 | var filterTag:String = null; 557 | for (m in mf.meta) { 558 | switch (m.name) { 559 | case 'filter': 560 | filterTag = getMetaString(m); 561 | if(filterTag == null) throw('filter must have tag: "@filter("tag")"'); 562 | default: 563 | } 564 | } 565 | 566 | switch (mf.kind) { 567 | case FFun(f): 568 | switch (mf.name) { 569 | case 'onPreStepStart': pushFilteredExpr(onStepStartExprs, filterTag, 'onStepStart', f.expr, false); 570 | case 'onPostStep': pushFilteredExpr(onStepExprs, filterTag, 'onStep', f.expr, false); 571 | case 'onPostStepEnd': pushFilteredExpr(onStepEndExprs, filterTag, 'onStepEnd', f.expr, false); 572 | case 'onPostParticleSpawn': pushFilteredExpr(onParticleSpawnExprs, filterTag, 'onParticleSpawn', f.expr, false); 573 | case 'onPostParticleUnspawn': pushFilteredExpr(onParticleUnspawnExprs, filterTag, 'onParticleUnspawn', f.expr, false); 574 | case 'onPostParticleUpdate': pushFilteredExpr(onParticleUpdateExprs, filterTag, 'onParticleUpdate', f.expr, false); 575 | case 'onPostStart': pushFilteredExpr(onStartExprs, filterTag, 'onStart', f.expr, false); 576 | case 'onPostStop': pushFilteredExpr(onStopExprs, filterTag, 'onStop', f.expr, false); 577 | default: 578 | } 579 | default: 580 | } 581 | } 582 | } 583 | } 584 | 585 | static function setFunctionArgsParticleType(mf:Field, pType:Type) { 586 | switch (mf.kind) { 587 | case FFun(f): 588 | for (a in f.args) { 589 | switch (a.type) { 590 | case TPath(p): 591 | // convert particle type 592 | if(p.name == 'Particle') a.type = Context.toComplexType(pType); 593 | default: 594 | } 595 | } 596 | default: 597 | } 598 | } 599 | 600 | static function pushFilteredExpr(exprs:Array, tag:String, fName:String, e:Expr, first:Bool = false) { 601 | var insert = true; 602 | 603 | if(tag != null) { 604 | var fm = filterMeta.get(fName); 605 | insert = false; 606 | 607 | if(fm == null) { 608 | fm = []; 609 | filterMeta.set(fName, fm); 610 | } 611 | 612 | if(fm.indexOf(tag) == -1) { 613 | fm.push(tag); 614 | insert = true; 615 | } 616 | } 617 | 618 | if(insert) { 619 | if(first) { 620 | exprs.insert(0, e); 621 | } else { 622 | exprs.push(e); 623 | } 624 | } 625 | } 626 | 627 | static function pushFilteredField(fields:Array, tag:String, f:Field) { 628 | if(tag != null) { 629 | var fm = filterMeta.get(f.name); 630 | 631 | if(fm == null) { 632 | fm = []; 633 | filterMeta.set(f.name, fm); 634 | } 635 | 636 | if(fm.indexOf(tag) == -1) { 637 | fm.push(tag); 638 | fields.push(f); 639 | } 640 | } else { 641 | fields.push(f); 642 | } 643 | } 644 | 645 | static function getEmitterName(types:Array):String { 646 | var typesString = MacroUtils.getStringFromTypes(types); 647 | 648 | var name = '${typeName}_${typesString}'; 649 | 650 | var uuid:String = emitterIds.get(name); 651 | 652 | if(uuid == null) { 653 | uuid = '${typeName}_${idx++}'; 654 | emitterIds.set(name, uuid); 655 | } 656 | 657 | return uuid; 658 | } 659 | 660 | static function getMetaString(meta:MetadataEntry):String { 661 | switch (meta.params[0].expr) { 662 | case EConst(c): 663 | switch (c) { 664 | case CString(s): 665 | return s; 666 | default: 667 | } 668 | default: 669 | } 670 | return null; 671 | } 672 | 673 | static function getModuleOptionsFromClassName(name:String):ParticleModuleMacroOptions { 674 | TypeTools.getClass(Context.getType(name)); // build type 675 | return ParticleModuleMacro.moduleOptions.get(name); 676 | } 677 | 678 | } 679 | 680 | typedef InjectModule = { 681 | name:String, 682 | inject:(options:ParticleEmitterBuildOptions)->Void 683 | } 684 | 685 | typedef ParticleEmitterBuildOptions = { 686 | groupNames:Array, 687 | particleType:Type, 688 | particleFieldNames:Array, 689 | fields:Array, 690 | optFields:Array, 691 | 692 | newExprs:Array, 693 | emitExprs:Array, 694 | onStartExprs:Array, 695 | onStopExprs:Array, 696 | onUpdateExprs:Array, 697 | onStepStartExprs:Array, 698 | onStepExprs:Array, 699 | onStepEndExprs:Array, 700 | onParticleUpdateExprs:Array, 701 | onParticleSpawnExprs:Array, 702 | onParticleUnspawnExprs:Array, 703 | 704 | emitterImports:Array, 705 | 706 | } 707 | 708 | #end --------------------------------------------------------------------------------