dopaminejs 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +51 -0
- package/dist/dopamine.mjs +291 -278
- package/dist/dopamine.umd.js +11 -11
- package/package.json +1 -1
- package/src/effects/ParticleSystem.js +201 -131
package/dist/dopamine.umd.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(o,p){typeof exports=="object"&&typeof module<"u"?p(exports):typeof define=="function"&&define.amd?define(["exports"],p):(o=typeof globalThis<"u"?globalThis:o||self,p(o.Dopamine={}))})(this,function(o){"use strict";class p{constructor(){this.listeners=new Map}on(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>this.off(e,t)}off(e,t){this.listeners.has(e)&&this.listeners.get(e).delete(t)}emit(e,t){this.listeners.has(e)&&this.listeners.get(e).forEach(s=>{try{s(t)}catch(i){console.error(`Error in listener for event '${e}':`,i)}})}clear(){this.listeners.clear()}}class m extends p{constructor(e,t={}){super(),this.dataService=e,this.player=null,this.achievements={...this._initAchievements(),...t.achievements||{}}}async init(){return this.player=await this.dataService.load("player",this._getDefaultPlayer()),this._updateDailyStreak(),await this.save(),this.player}_getDefaultPlayer(){return{name:"Player",xp:0,level:1,totalGamesPlayed:0,createdAt:Date.now(),lastPlayedAt:Date.now(),streak:{current:1,longest:1,lastPlayDate:this._getTodayDateString()},achievements:{},stats:{}}}async save(){this.player&&(this.player.lastPlayedAt=Date.now(),await this.dataService.save("player",this.player))}async addXP(e,t=""){const s=this.player.level;this.player.xp+=e;const i=this._calculateLevel(this.player.xp),a=i>s;return a&&(this.player.level=i),await this.save(),this.emit("xp_gained",{amount:e,reason:t,leveledUp:a,newLevel:i}),a&&this.emit("level_up",{oldLevel:s,newLevel:i}),{leveledUp:a,newLevel:i,xpGained:e}}_calculateLevel(e){const t=Math.floor((1+Math.sqrt(1+8*e/50))/2);return Math.max(1,t)}getXPForNextLevel(){const e=this.player.level+1,t=50*e*(e-1),s=50*this.player.level*(this.player.level-1);return{total:t,needed:t-this.player.xp,progress:(this.player.xp-s)/(t-s)}}async recordGame(e,t){this.player.stats[e]||(this.player.stats[e]={totalPlays:0,highScore:0});const s=this.player.stats[e];s.totalPlays++,this.player.totalGamesPlayed++,t.score!==void 0&&t.score>s.highScore&&(s.highScore=t.score,this.emit("new_high_score",{gameName:e,score:t.score})),Object.keys(t).forEach(a=>{a!=="score"&&(s[a]=(s[a]||0)+(typeof t[a]=="number"?t[a]:0))});let i=10;t.score&&(i+=Math.floor(t.score/5)),i=Math.floor(i*this.getStreakMultiplier()),await this.addXP(i,`Played ${e}`),await this.checkAchievements(e,t),await this.save()}async checkAchievements(e,t){const s=[];for(const[i,a]of Object.entries(this.achievements))this.player.achievements[i]||a.check(this.player,e,t)&&(await this.unlockAchievement(i),s.push(a));return s}async unlockAchievement(e){const t=this.achievements[e];return t?(this.player.achievements[e]={unlockedAt:Date.now(),seen:!1},await this.addXP(t.xp,`Achievement: ${t.name}`),this.emit("achievement_unlocked",t),await this.save(),!0):!1}getUnlockedAchievements(){return Object.entries(this.player.achievements).map(([e,t])=>({...this.achievements[e],id:e,...t}))}getUnseenAchievements(){return this.getUnlockedAchievements().filter(e=>!e.seen)}async markAchievementsSeen(e){for(const t of e)this.player.achievements[t]&&(this.player.achievements[t].seen=!0);await this.save()}_updateDailyStreak(){const e=this._getTodayDateString(),t=this.player.streak.lastPlayDate;if(t===e)return;const s=this._getYesterdayDateString();t===s?(this.player.streak.current++,this.player.streak.current>this.player.streak.longest&&(this.player.streak.longest=this.player.streak.current)):this.player.streak.current=1,this.player.streak.lastPlayDate=e}getStreakMultiplier(){const e=this.player.streak.current;return e>=7?2:e>=5?1.5:e>=3?1.25:1}_getTodayDateString(){return new Date().toISOString().split("T")[0]}_getYesterdayDateString(){const e=new Date;return e.setDate(e.getDate()-1),e.toISOString().split("T")[0]}_initAchievements(){return{first_game:{name:"First Steps",description:"Play your first game",icon:"🎮",xp:50,check:e=>e.totalGamesPlayed>=1},level_5:{name:"Rising Star",description:"Reach level 5",icon:"⭐",xp:100,check:e=>e.level>=5},level_10:{name:"Expert Player",description:"Reach level 10",icon:"🏆",xp:250,check:e=>e.level>=10},streak_3:{name:"Dedicated",description:"Play 3 days in a row",icon:"🔥",xp:100,check:e=>e.streak.current>=3},streak_7:{name:"Unstoppable",description:"Play 7 days in a row",icon:"💥",xp:300,check:e=>e.streak.current>=7}}}}class y{constructor(e={}){this.storage=e.storage||window.localStorage,this.prefix=e.prefix||"dopamine_"}async save(e,t){try{const s=JSON.stringify(t);return this.storage.setItem(this.prefix+e,s),!0}catch(s){return console.error("Error saving data:",s),!1}}async load(e,t=null){try{const s=this.storage.getItem(this.prefix+e);return s?JSON.parse(s):t}catch(s){return console.error("Error loading data:",s),t}}async clear(e){this.storage.removeItem(this.prefix+e)}}class f{constructor(e){this.particleSystem=e,this.container=null,this.xpBar=null,this.levelDisplay=null,this.streakDisplay=null,this.init()}init(){this.container=document.createElement("div"),this.container.id="game-ui-overlay",this.container.innerHTML=`
|
|
2
2
|
<div class="game-ui-top-bar">
|
|
3
3
|
<div class="level-badge" id="level-badge">
|
|
4
4
|
<span class="level-label">LVL</span>
|
|
@@ -17,29 +17,29 @@
|
|
|
17
17
|
<span class="streak-number" id="streak-number">1</span>
|
|
18
18
|
</div>
|
|
19
19
|
</div>
|
|
20
|
-
`,document.body.appendChild(this.container),this.xpBar=document.getElementById("xp-bar-fill"),this.xpText=document.getElementById("xp-text"),this.levelDisplay=document.getElementById("level-number"),this.streakDisplay=document.getElementById("streak-number")}updateXP(t,
|
|
20
|
+
`,document.body.appendChild(this.container),this.xpBar=document.getElementById("xp-bar-fill"),this.xpText=document.getElementById("xp-text"),this.levelDisplay=document.getElementById("level-number"),this.streakDisplay=document.getElementById("streak-number")}updateXP(e,t,s){const i=e/s*100;this.xpBar.style.width=`${Math.min(100,i)}%`,this.xpText.textContent=`${e} / ${s} XP`,this.xpBar.style.animation="none",setTimeout(()=>{this.xpBar.style.animation="xp-pulse 0.3s ease-out"},10)}updateLevel(e){this.levelDisplay.textContent=e;const t=document.getElementById("level-badge");t.style.animation="none",setTimeout(()=>{t.style.animation="level-pulse 0.5s ease-out"},10)}updateStreak(e){this.streakDisplay.textContent=e;const t=document.getElementById("streak-badge");e>=7?t.style.background="linear-gradient(135deg, #ff6b00 0%, #ff4400 100%)":e>=3&&(t.style.background="linear-gradient(135deg, #ff8800 0%, #ff6b00 100%)")}showAchievement(e){const t=document.createElement("div");t.className="achievement-popup",t.innerHTML=`
|
|
21
21
|
<div class="achievement-shine"></div>
|
|
22
22
|
<div class="achievement-content">
|
|
23
|
-
<div class="achievement-icon">${
|
|
23
|
+
<div class="achievement-icon">${e.icon}</div>
|
|
24
24
|
<div class="achievement-info">
|
|
25
25
|
<div class="achievement-title">Achievement Unlocked!</div>
|
|
26
|
-
<div class="achievement-name">${
|
|
27
|
-
<div class="achievement-desc">${
|
|
28
|
-
<div class="achievement-xp">+${
|
|
26
|
+
<div class="achievement-name">${e.name}</div>
|
|
27
|
+
<div class="achievement-desc">${e.description}</div>
|
|
28
|
+
<div class="achievement-xp">+${e.xp} XP</div>
|
|
29
29
|
</div>
|
|
30
30
|
</div>
|
|
31
|
-
`,document.body.appendChild(
|
|
31
|
+
`,document.body.appendChild(t),setTimeout(()=>{const s=t.getBoundingClientRect();this.particleSystem.confetti(s.left+s.width/2,s.top+s.height/2,40)},300),setTimeout(()=>{t.style.animation="slideOut 0.3s ease-in forwards",setTimeout(()=>t.remove(),300)},4e3)}showLevelUp(e,t){const s=document.createElement("div");s.className="level-up-overlay",s.innerHTML=`
|
|
32
32
|
<div class="level-up-content">
|
|
33
33
|
<div class="level-up-title">LEVEL UP!</div>
|
|
34
|
-
<div class="level-up-number">${
|
|
34
|
+
<div class="level-up-number">${t}</div>
|
|
35
35
|
<div class="level-up-subtitle">Amazing progress!</div>
|
|
36
36
|
</div>
|
|
37
|
-
`,document.body.appendChild(s),setTimeout(()=>{for(let i=0;i<5;i++)setTimeout(()=>{const a=Math.random()*window.innerWidth,n=Math.random()*window.innerHeight*.6;this.particleSystem.confetti(a,n,30)},i*200)},300),setTimeout(()=>{s.style.opacity="0",setTimeout(()=>s.remove(),500)},2500)}showFloatingText(t,
|
|
37
|
+
`,document.body.appendChild(s),setTimeout(()=>{for(let i=0;i<5;i++)setTimeout(()=>{const a=Math.random()*window.innerWidth,n=Math.random()*window.innerHeight*.6;this.particleSystem.confetti(a,n,30)},i*200)},300),setTimeout(()=>{s.style.opacity="0",setTimeout(()=>s.remove(),500)},2500)}showFloatingText(e,t,s,i="#ffd700",a="24px"){const n=document.createElement("div");n.className="floating-text",n.textContent=e,n.style.left=t+"px",n.style.top=s+"px",n.style.color=i,n.style.fontSize=a,document.body.appendChild(n),setTimeout(()=>n.remove(),2e3)}showCombo(e,t,s){this.showFloatingText(`${e}× COMBO!`,t,s,"#ff6b6b","32px"),this.particleSystem.fire(t,s,20)}showNearMiss(e,t){this.showFloatingText("CLOSE CALL!",e,t,"#45b7d1","20px"),this.particleSystem.sparkle(e,t,15,"#45b7d1")}showLuckyMoment(e,t,s){this.showFloatingText(`LUCKY ${e}×!`,t,s,"#ffd700","36px"),this.particleSystem.starBurst(t,s,12),this.particleSystem.confetti(t,s,25)}showNotification(e,t="normal"){const s=window.innerWidth/2,i=window.innerHeight/3;t==="legendary"?this.showLuckyMoment(e,s,i):t==="rare"?this.showCombo(e,s,i):this.showFloatingText(e,s,i,"#fff","24px")}showSummary(e){const t=document.createElement("div");t.className="message-box",t.style.display="flex",t.style.position="absolute",t.style.top="50%",t.style.left="50%",t.style.transform="translate(-50%, -50%)",t.style.zIndex="1000";let s="";e.metrics&&(s=Object.entries(e.metrics).map(([i,a])=>`<div style="display:flex;justify-content:space-between;width:100%"><span>${i}:</span> <strong>${a}</strong></div>`).join("")),t.innerHTML=`
|
|
38
38
|
<h1>Game Over</h1>
|
|
39
|
-
<div style="font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;">${
|
|
39
|
+
<div style="font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;">${e.score}</div>
|
|
40
40
|
<div style="width: 100%; margin-bottom: 20px;">
|
|
41
41
|
${s}
|
|
42
42
|
</div>
|
|
43
43
|
<button class="btn" onclick="location.reload()">Play Again</button>
|
|
44
44
|
<button class="btn" onclick="window.location.href='index.html'" style="margin-top: 10px; background: #543847;">Exit</button>
|
|
45
|
-
`,document.body.appendChild(
|
|
45
|
+
`,document.body.appendChild(t)}}class v{constructor(e={}){this.container=e.container?typeof e.container=="string"?document.querySelector(e.container):e.container:document.body,this.canvasId=e.canvasId||"dopamine-particle-canvas",this.canvas=document.getElementById(this.canvasId),this.canvas||(this.canvas=document.createElement("canvas"),this.canvas.id=this.canvasId,this.canvas.style.pointerEvents="none",this.canvas.style.zIndex=e.zIndex||"9999",this.container===document.body?(this.canvas.style.position="fixed",this.canvas.style.top="0",this.canvas.style.left="0"):(this.canvas.style.position="absolute",this.canvas.style.top="0",this.canvas.style.left="0",window.getComputedStyle(this.container).position==="static"&&(this.container.style.position="relative")),this.container.appendChild(this.canvas)),this.ctx=this.canvas.getContext("2d"),this.particles=[],this.pool=[],this.sprites=new Map,this.customEffects=new Map,this.isAnimating=!1,this._resize(),window.ResizeObserver&&this.container!==document.body?new ResizeObserver(()=>this._resize()).observe(this.container):window.addEventListener("resize",()=>this._resize())}_resize(){this.container===document.body?(this.canvas.width=window.innerWidth,this.canvas.height=window.innerHeight):(this.canvas.width=this.container.clientWidth,this.canvas.height=this.container.clientHeight)}registerSprite(e,t){const s=new Image;s.src=t,this.sprites.set(e,s)}registerEffect(e,t){this.customEffects.set(e,t)}play(e,t,s,...i){const a=this.customEffects.get(e);a?a(t,s,...i):console.warn(`[DopamineJS] Effect '${e}' not found.`)}emit(e){const{x:t,y:s,count:i=10,speed:a=5,gravity:n=.1,decay:l=.02,life:d=1,color:c="#ffd700",sprite:u=null,spread:M=Math.PI*2,angle:P=0,size:T=5}=e;for(let x=0;x<i;x++){const r=this._getParticle(),w=P+(Math.random()-.5)*M,S=Math.random()*a;r.x=t,r.y=s,r.vx=Math.cos(w)*S,r.vy=Math.sin(w)*S,r.life=d,r.decay=l*(.8+Math.random()*.4),r.gravity=n,r.color=Array.isArray(c)?c[Math.floor(Math.random()*c.length)]:c,r.size=T*(.8+Math.random()*.4),r.sprite=u,r.rotation=Math.random()*Math.PI*2,r.rotationSpeed=(Math.random()-.5)*.2,this.particles.push(r)}this._startAnimation()}_getParticle(){return this.pool.length>0?this.pool.pop():{}}_recycleParticle(e){this.pool.push(e)}confetti(e,t,s=50){const i=["#ff6b6b","#4ecdc4","#45b7d1","#f7dc6f","#c06c84","#6c5ce7"];this.emit({x:e,y:t,count:s,color:i,speed:10,gravity:.3,decay:.015,size:8,spread:Math.PI*2})}coinShower(e,t,s=10){this.emit({x:e,y:t,count:s,color:"#ffd700",speed:4,gravity:.4,decay:.012,size:12,angle:-Math.PI/2,spread:Math.PI/2})}sparkle(e,t,s=20,i="#ffd700"){this.emit({x:e,y:t,count:s,color:i,speed:3,gravity:-.05,decay:.03,size:3})}fire(e,t,s=15){const i=["#ff6b00","#ff8800","#ffaa00","#ff4400"];this.emit({x:e,y:t,count:s,color:i,speed:3,gravity:-.2,decay:.04,size:5,angle:-Math.PI/2,spread:Math.PI/4})}starBurst(e,t,s=8){for(let i=0;i<s;i++){const a=this._getParticle(),n=Math.PI*2/s*i,l=4;a.x=e,a.y=t,a.vx=Math.cos(n)*l,a.vy=Math.sin(n)*l,a.life=1,a.decay=.03,a.gravity=0,a.color="#ffffff",a.size=15,a.sprite=null,a.type="star",a.rotation=n,a.rotationSpeed=0,this.particles.push(a)}this._startAnimation()}_startAnimation(){this.isAnimating||(this.isAnimating=!0,this._animate())}_animate(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);for(let e=this.particles.length-1;e>=0;e--){const t=this.particles[e];if(t.vy+=t.gravity,t.x+=t.vx,t.y+=t.vy,t.life-=t.decay,t.rotationSpeed&&(t.rotation+=t.rotationSpeed),t.life<=0){this._recycleParticle(t),this.particles.splice(e,1);continue}if(this.ctx.save(),this.ctx.globalAlpha=t.life,this.ctx.translate(t.x,t.y),this.ctx.rotate(t.rotation),t.sprite&&this.sprites.has(t.sprite)){const s=this.sprites.get(t.sprite);s.complete&&this.ctx.drawImage(s,-t.size/2,-t.size/2,t.size,t.size)}else t.type==="star"?(this.ctx.fillStyle=t.color,this._drawStarPath(0,0,5,t.size,t.size/2),this.ctx.fill()):(this.ctx.fillStyle=t.color,this.ctx.beginPath(),this.ctx.arc(0,0,t.size,0,Math.PI*2),this.ctx.fill());this.ctx.restore()}this.particles.length>0?requestAnimationFrame(()=>this._animate()):this.isAnimating=!1}_drawStarPath(e,t,s,i,a){let n=Math.PI/2*3,l=e,d=t;const c=Math.PI/s;this.ctx.beginPath(),this.ctx.moveTo(e,t-i);for(let u=0;u<s;u++)l=e+Math.cos(n)*i,d=t+Math.sin(n)*i,this.ctx.lineTo(l,d),n+=c,l=e+Math.cos(n)*a,d=t+Math.sin(n)*a,this.ctx.lineTo(l,d),n+=c;this.ctx.lineTo(e,t-i),this.ctx.closePath()}clear(){this.particles.forEach(e=>this._recycleParticle(e)),this.particles=[],this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height)}}class g{constructor(e={}){this.audioContext=null,this.storageKey=e.storageKey||"dopamineSoundsMuted",this.muted=localStorage.getItem(this.storageKey)==="true",this.assets=new Map,this.soundPack=e.soundPack||"synth",this.customSounds=e.customSounds||{},Object.keys(this.customSounds).length>0&&this.preloadSounds(this.customSounds)}initAudio(){if(!this.audioContext){const e=window.AudioContext||window.webkitAudioContext;this.audioContext=new e}this.audioContext.state==="suspended"&&this.audioContext.resume()}toggleMute(){return this.muted=!this.muted,localStorage.setItem(this.storageKey,this.muted),this.muted}registerSound(e,t){this.customSounds[e]=t,this.audioContext&&this.loadSound(e,t)}async preloadSounds(e){const t=Object.entries(e).map(([s,i])=>this.loadSound(s,i));await Promise.allSettled(t)}async loadSound(e,t){try{if(!this.audioContext)return;const i=await(await fetch(t)).arrayBuffer(),a=await this.audioContext.decodeAudioData(i);this.assets.set(e,a)}catch(s){console.warn(`[DopamineJS] Failed to load sound '${e}':`,s)}}async play(e){if(this.muted)return;if(this.initAudio(),this.assets.has(e)){this._playBuffer(this.assets.get(e));return}if(this.customSounds[e]&&!this.assets.has(e)&&(await this.loadSound(e,this.customSounds[e]),this.assets.has(e))){this._playBuffer(this.assets.get(e));return}const t={jump:()=>this.playJump(!0),score:()=>this.playScore(!0),success:()=>this.playSuccess(!0),click:()=>this.playClick(!0),error:()=>this.playError(!0),gameover:()=>this.playGameOver(!0)};t[e]&&t[e]()}_playBuffer(e){const t=this.audioContext.createBufferSource();t.buffer=e,t.connect(this.audioContext.destination),t.start(0)}playTone(e,t,s="sine",i=.3){if(this.muted)return;this.initAudio();const a=this.audioContext.createOscillator(),n=this.audioContext.createGain();a.connect(n),n.connect(this.audioContext.destination),a.frequency.value=e,a.type=s,n.gain.setValueAtTime(i,this.audioContext.currentTime),n.gain.exponentialRampToValueAtTime(.01,this.audioContext.currentTime+t),a.start(this.audioContext.currentTime),a.stop(this.audioContext.currentTime+t)}playJump(e=!1){if(!e&&this.assets.has("jump"))return this.play("jump");this.playTone(400,.1,"square",.2)}playScore(e=!1){if(!e&&this.assets.has("score"))return this.play("score");this.playTone(800,.15,"sine",.25),setTimeout(()=>this.playTone(1e3,.15,"sine",.25),50)}playGameOver(e=!1){if(!e&&this.assets.has("gameover"))return this.play("gameover");this.playTone(300,.2,"sawtooth",.3),setTimeout(()=>this.playTone(200,.2,"sawtooth",.3),100),setTimeout(()=>this.playTone(150,.3,"sawtooth",.3),200)}playClick(e=!1){if(!e&&this.assets.has("click"))return this.play("click");this.playTone(600,.05,"sine",.1)}playSuccess(e=!1){if(!e&&this.assets.has("success"))return this.play("success");this.playTone(600,.1,"sine",.2),setTimeout(()=>this.playTone(800,.2,"sine",.2),100)}playError(e=!1){if(!e&&this.assets.has("error"))return this.play("error");this.playTone(200,.2,"sawtooth",.3)}}class b{constructor(e={}){this.config=e,this.dataService=new y(e.data||{}),this.rewardSystem=new m(this.dataService,e.rewards||{}),this.soundManager=new g(e.sound||{}),this.particleSystem=new v(e.particles||{}),this.gameUI=new f(this.particleSystem),this._bindEvents()}async init(){return await this.rewardSystem.init(),console.log("DopamineJS Initialized 🚀"),{rewardSystem:this.rewardSystem,gameUI:this.gameUI,particleSystem:this.particleSystem,soundManager:this.soundManager}}_bindEvents(){this.rewardSystem.on("xp_gained",e=>{const{total:t,needed:s}=this.rewardSystem.getXPForNextLevel();this.gameUI.updateXP(this.rewardSystem.player.xp,s,t),e.xpGained>0}),this.rewardSystem.on("level_up",e=>{this.gameUI.updateLevel(e.newLevel),this.gameUI.showLevelUp(e.oldLevel,e.newLevel)}),this.rewardSystem.on("achievement_unlocked",e=>{this.gameUI.showAchievement(e)}),this.rewardSystem.on("new_high_score",e=>{this.gameUI.showNotification(`New High Score: ${e.score}!`,"legendary"),this.soundManager.playSuccess()})}}o.DataService=y,o.GameUI=f,o.ParticleSystem=v,o.RewardSystem=m,o.SoundManager=g,o.default=b,Object.defineProperties(o,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
package/package.json
CHANGED
|
@@ -5,153 +5,227 @@
|
|
|
5
5
|
|
|
6
6
|
export class ParticleSystem {
|
|
7
7
|
constructor(config = {}) {
|
|
8
|
+
this.container = config.container ?
|
|
9
|
+
(typeof config.container === 'string' ? document.querySelector(config.container) : config.container)
|
|
10
|
+
: document.body;
|
|
11
|
+
|
|
8
12
|
this.canvasId = config.canvasId || 'dopamine-particle-canvas';
|
|
9
13
|
this.canvas = document.getElementById(this.canvasId);
|
|
10
14
|
|
|
11
15
|
if (!this.canvas) {
|
|
12
|
-
// Create overlay canvas if it doesn't exist
|
|
13
16
|
this.canvas = document.createElement('canvas');
|
|
14
17
|
this.canvas.id = this.canvasId;
|
|
15
|
-
this.canvas.style.position = 'fixed';
|
|
16
|
-
this.canvas.style.top = '0';
|
|
17
|
-
this.canvas.style.left = '0';
|
|
18
|
-
this.canvas.style.width = '100%';
|
|
19
|
-
this.canvas.style.height = '100%';
|
|
20
18
|
this.canvas.style.pointerEvents = 'none';
|
|
21
19
|
this.canvas.style.zIndex = config.zIndex || '9999';
|
|
22
|
-
|
|
20
|
+
|
|
21
|
+
if (this.container === document.body) {
|
|
22
|
+
this.canvas.style.position = 'fixed';
|
|
23
|
+
this.canvas.style.top = '0';
|
|
24
|
+
this.canvas.style.left = '0';
|
|
25
|
+
} else {
|
|
26
|
+
this.canvas.style.position = 'absolute';
|
|
27
|
+
this.canvas.style.top = '0';
|
|
28
|
+
this.canvas.style.left = '0';
|
|
29
|
+
// Ensure container is positioned
|
|
30
|
+
const style = window.getComputedStyle(this.container);
|
|
31
|
+
if (style.position === 'static') {
|
|
32
|
+
this.container.style.position = 'relative';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.container.appendChild(this.canvas);
|
|
23
37
|
}
|
|
24
38
|
|
|
25
39
|
this.ctx = this.canvas.getContext('2d');
|
|
26
40
|
this.particles = [];
|
|
41
|
+
this.pool = []; // Object pool
|
|
42
|
+
this.sprites = new Map(); // key -> Image
|
|
43
|
+
this.customEffects = new Map(); // key -> callback
|
|
27
44
|
this.isAnimating = false;
|
|
28
45
|
|
|
29
46
|
this._resize();
|
|
30
|
-
|
|
47
|
+
// Use ResizeObserver for container resizing if supported, fallback to window resize
|
|
48
|
+
if (window.ResizeObserver && this.container !== document.body) {
|
|
49
|
+
new ResizeObserver(() => this._resize()).observe(this.container);
|
|
50
|
+
} else {
|
|
51
|
+
window.addEventListener('resize', () => this._resize());
|
|
52
|
+
}
|
|
31
53
|
}
|
|
32
54
|
|
|
33
55
|
_resize() {
|
|
34
|
-
this.
|
|
35
|
-
|
|
56
|
+
if (this.container === document.body) {
|
|
57
|
+
this.canvas.width = window.innerWidth;
|
|
58
|
+
this.canvas.height = window.innerHeight;
|
|
59
|
+
} else {
|
|
60
|
+
this.canvas.width = this.container.clientWidth;
|
|
61
|
+
this.canvas.height = this.container.clientHeight;
|
|
62
|
+
}
|
|
36
63
|
}
|
|
37
64
|
|
|
38
65
|
/**
|
|
39
|
-
*
|
|
66
|
+
* Register a custom sprite image
|
|
67
|
+
* @param {string} key
|
|
68
|
+
* @param {string} url
|
|
40
69
|
*/
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
this.particles.push({
|
|
46
|
-
x, y,
|
|
47
|
-
vx: (Math.random() - 0.5) * 10,
|
|
48
|
-
vy: (Math.random() - 0.5) * 10 - 5,
|
|
49
|
-
rotation: Math.random() * Math.PI * 2,
|
|
50
|
-
rotationSpeed: (Math.random() - 0.5) * 0.2,
|
|
51
|
-
color: colors[Math.floor(Math.random() * colors.length)],
|
|
52
|
-
size: Math.random() * 8 + 4,
|
|
53
|
-
life: 1.0,
|
|
54
|
-
decay: 0.015 + Math.random() * 0.01,
|
|
55
|
-
gravity: 0.3,
|
|
56
|
-
type: 'confetti'
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
this._startAnimation();
|
|
70
|
+
registerSprite(key, url) {
|
|
71
|
+
const img = new Image();
|
|
72
|
+
img.src = url;
|
|
73
|
+
this.sprites.set(key, img);
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
/**
|
|
64
|
-
*
|
|
77
|
+
* Register a custom effect
|
|
78
|
+
* @param {string} name
|
|
79
|
+
* @param {Function} callback - (x, y, ...args) => void
|
|
65
80
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const angle = (Math.PI * 2 / count) * i;
|
|
69
|
-
const speed = 2 + Math.random() * 3;
|
|
70
|
-
|
|
71
|
-
this.particles.push({
|
|
72
|
-
x, y,
|
|
73
|
-
vx: Math.cos(angle) * speed,
|
|
74
|
-
vy: Math.sin(angle) * speed,
|
|
75
|
-
color,
|
|
76
|
-
size: Math.random() * 4 + 2,
|
|
77
|
-
life: 1.0,
|
|
78
|
-
decay: 0.02,
|
|
79
|
-
gravity: -0.1, // Slight upward drift
|
|
80
|
-
type: 'spark'
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
this._startAnimation();
|
|
81
|
+
registerEffect(name, callback) {
|
|
82
|
+
this.customEffects.set(name, callback);
|
|
85
83
|
}
|
|
86
84
|
|
|
87
85
|
/**
|
|
88
|
-
*
|
|
86
|
+
* Play a registered effect
|
|
89
87
|
*/
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
vy: Math.random() * -2 - 3,
|
|
97
|
-
color: '#ffd700',
|
|
98
|
-
size: 12,
|
|
99
|
-
life: 1.0,
|
|
100
|
-
decay: 0.012,
|
|
101
|
-
gravity: 0.4,
|
|
102
|
-
type: 'coin'
|
|
103
|
-
});
|
|
88
|
+
play(name, x, y, ...args) {
|
|
89
|
+
const effect = this.customEffects.get(name);
|
|
90
|
+
if (effect) {
|
|
91
|
+
effect(x, y, ...args);
|
|
92
|
+
} else {
|
|
93
|
+
console.warn(`[DopamineJS] Effect '${name}' not found.`);
|
|
104
94
|
}
|
|
105
|
-
|
|
106
|
-
this._startAnimation();
|
|
107
95
|
}
|
|
108
96
|
|
|
109
97
|
/**
|
|
110
|
-
*
|
|
98
|
+
* Emit particles with custom config
|
|
99
|
+
* @param {Object} config
|
|
111
100
|
*/
|
|
112
|
-
|
|
113
|
-
const
|
|
101
|
+
emit(config) {
|
|
102
|
+
const {
|
|
103
|
+
x, y,
|
|
104
|
+
count = 10,
|
|
105
|
+
speed = 5,
|
|
106
|
+
gravity = 0.1,
|
|
107
|
+
decay = 0.02,
|
|
108
|
+
life = 1.0,
|
|
109
|
+
color = '#ffd700',
|
|
110
|
+
sprite = null,
|
|
111
|
+
spread = Math.PI * 2,
|
|
112
|
+
angle = 0,
|
|
113
|
+
size = 5
|
|
114
|
+
} = config;
|
|
114
115
|
|
|
115
116
|
for (let i = 0; i < count; i++) {
|
|
116
|
-
this.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
117
|
+
const p = this._getParticle();
|
|
118
|
+
const pAngle = angle + (Math.random() - 0.5) * spread;
|
|
119
|
+
const pSpeed = Math.random() * speed;
|
|
120
|
+
|
|
121
|
+
p.x = x;
|
|
122
|
+
p.y = y;
|
|
123
|
+
p.vx = Math.cos(pAngle) * pSpeed;
|
|
124
|
+
p.vy = Math.sin(pAngle) * pSpeed;
|
|
125
|
+
p.life = life;
|
|
126
|
+
p.decay = decay * (0.8 + Math.random() * 0.4); // Randomize decay slightly
|
|
127
|
+
p.gravity = gravity;
|
|
128
|
+
p.color = Array.isArray(color) ? color[Math.floor(Math.random() * color.length)] : color;
|
|
129
|
+
p.size = size * (0.8 + Math.random() * 0.4);
|
|
130
|
+
p.sprite = sprite;
|
|
131
|
+
p.rotation = Math.random() * Math.PI * 2;
|
|
132
|
+
p.rotationSpeed = (Math.random() - 0.5) * 0.2;
|
|
133
|
+
|
|
134
|
+
this.particles.push(p);
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
this._startAnimation();
|
|
131
138
|
}
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
_getParticle() {
|
|
141
|
+
if (this.pool.length > 0) {
|
|
142
|
+
return this.pool.pop();
|
|
143
|
+
}
|
|
144
|
+
return {}; // New particle object
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
_recycleParticle(p) {
|
|
148
|
+
this.pool.push(p);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// --- Built-in Effects ---
|
|
152
|
+
|
|
153
|
+
confetti(x, y, count = 50) {
|
|
154
|
+
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f7dc6f', '#c06c84', '#6c5ce7'];
|
|
155
|
+
this.emit({
|
|
156
|
+
x, y, count,
|
|
157
|
+
color: colors,
|
|
158
|
+
speed: 10,
|
|
159
|
+
gravity: 0.3,
|
|
160
|
+
decay: 0.015,
|
|
161
|
+
size: 8,
|
|
162
|
+
spread: Math.PI * 2
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
coinShower(x, y, count = 10) {
|
|
167
|
+
this.emit({
|
|
168
|
+
x, y, count,
|
|
169
|
+
color: '#ffd700',
|
|
170
|
+
speed: 4,
|
|
171
|
+
gravity: 0.4,
|
|
172
|
+
decay: 0.012,
|
|
173
|
+
size: 12,
|
|
174
|
+
angle: -Math.PI / 2, // Upwards initially
|
|
175
|
+
spread: Math.PI / 2
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
sparkle(x, y, count = 20, color = '#ffd700') {
|
|
180
|
+
this.emit({
|
|
181
|
+
x, y, count,
|
|
182
|
+
color,
|
|
183
|
+
speed: 3,
|
|
184
|
+
gravity: -0.05, // Float up
|
|
185
|
+
decay: 0.03,
|
|
186
|
+
size: 3
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fire(x, y, count = 15) {
|
|
191
|
+
const colors = ['#ff6b00', '#ff8800', '#ffaa00', '#ff4400'];
|
|
192
|
+
this.emit({
|
|
193
|
+
x, y, count,
|
|
194
|
+
color: colors,
|
|
195
|
+
speed: 3,
|
|
196
|
+
gravity: -0.2,
|
|
197
|
+
decay: 0.04,
|
|
198
|
+
size: 5,
|
|
199
|
+
angle: -Math.PI / 2,
|
|
200
|
+
spread: Math.PI / 4
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
136
204
|
starBurst(x, y, count = 8) {
|
|
205
|
+
// Star burst is unique because of fixed angles, so we keep manual loop or use emit carefully
|
|
206
|
+
// For simplicity, let's use emit but we lose the perfect star shape distribution
|
|
207
|
+
// To keep it perfect, we'll manually push particles but use the pool
|
|
137
208
|
for (let i = 0; i < count; i++) {
|
|
209
|
+
const p = this._getParticle();
|
|
138
210
|
const angle = (Math.PI * 2 / count) * i;
|
|
139
211
|
const speed = 4;
|
|
140
212
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
213
|
+
p.x = x;
|
|
214
|
+
p.y = y;
|
|
215
|
+
p.vx = Math.cos(angle) * speed;
|
|
216
|
+
p.vy = Math.sin(angle) * speed;
|
|
217
|
+
p.life = 1.0;
|
|
218
|
+
p.decay = 0.03;
|
|
219
|
+
p.gravity = 0;
|
|
220
|
+
p.color = '#ffffff';
|
|
221
|
+
p.size = 15;
|
|
222
|
+
p.sprite = null;
|
|
223
|
+
p.type = 'star'; // Special type for drawing logic
|
|
224
|
+
p.rotation = angle;
|
|
225
|
+
p.rotationSpeed = 0;
|
|
226
|
+
|
|
227
|
+
this.particles.push(p);
|
|
153
228
|
}
|
|
154
|
-
|
|
155
229
|
this._startAnimation();
|
|
156
230
|
}
|
|
157
231
|
|
|
@@ -165,11 +239,10 @@ export class ParticleSystem {
|
|
|
165
239
|
_animate() {
|
|
166
240
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
167
241
|
|
|
168
|
-
// Update and draw particles
|
|
169
242
|
for (let i = this.particles.length - 1; i >= 0; i--) {
|
|
170
243
|
const p = this.particles[i];
|
|
171
244
|
|
|
172
|
-
//
|
|
245
|
+
// Physics
|
|
173
246
|
p.vy += p.gravity;
|
|
174
247
|
p.x += p.vx;
|
|
175
248
|
p.y += p.vy;
|
|
@@ -179,43 +252,41 @@ export class ParticleSystem {
|
|
|
179
252
|
p.rotation += p.rotationSpeed;
|
|
180
253
|
}
|
|
181
254
|
|
|
182
|
-
//
|
|
255
|
+
// Death
|
|
183
256
|
if (p.life <= 0) {
|
|
257
|
+
this._recycleParticle(p);
|
|
184
258
|
this.particles.splice(i, 1);
|
|
185
259
|
continue;
|
|
186
260
|
}
|
|
187
261
|
|
|
188
|
-
// Draw
|
|
262
|
+
// Draw
|
|
189
263
|
this.ctx.save();
|
|
190
264
|
this.ctx.globalAlpha = p.life;
|
|
191
|
-
this.ctx.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
this.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
this.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
|
200
|
-
this.ctx.fill();
|
|
201
|
-
// Add shine
|
|
202
|
-
this.ctx.fillStyle = '#ffee88';
|
|
203
|
-
this.ctx.beginPath();
|
|
204
|
-
this.ctx.arc(p.x - p.size / 3, p.y - p.size / 3, p.size / 3, 0, Math.PI * 2);
|
|
205
|
-
this.ctx.fill();
|
|
265
|
+
this.ctx.translate(p.x, p.y);
|
|
266
|
+
this.ctx.rotate(p.rotation);
|
|
267
|
+
|
|
268
|
+
if (p.sprite && this.sprites.has(p.sprite)) {
|
|
269
|
+
const img = this.sprites.get(p.sprite);
|
|
270
|
+
if (img.complete) {
|
|
271
|
+
this.ctx.drawImage(img, -p.size / 2, -p.size / 2, p.size, p.size);
|
|
272
|
+
}
|
|
206
273
|
} else if (p.type === 'star') {
|
|
207
|
-
this.
|
|
274
|
+
this.ctx.fillStyle = p.color;
|
|
275
|
+
this._drawStarPath(0, 0, 5, p.size, p.size / 2);
|
|
276
|
+
this.ctx.fill();
|
|
208
277
|
} else {
|
|
209
|
-
|
|
278
|
+
this.ctx.fillStyle = p.color;
|
|
279
|
+
// Default shape is square for performance/confetti, or circle
|
|
280
|
+
// Let's usefillRect for speed unless it's a coin/sparkle which usually implies circle
|
|
281
|
+
// For generic emit, let's assume circle if no sprite
|
|
210
282
|
this.ctx.beginPath();
|
|
211
|
-
this.ctx.arc(
|
|
283
|
+
this.ctx.arc(0, 0, p.size, 0, Math.PI * 2);
|
|
212
284
|
this.ctx.fill();
|
|
213
285
|
}
|
|
214
286
|
|
|
215
287
|
this.ctx.restore();
|
|
216
288
|
}
|
|
217
289
|
|
|
218
|
-
// Continue animation if particles remain
|
|
219
290
|
if (this.particles.length > 0) {
|
|
220
291
|
requestAnimationFrame(() => this._animate());
|
|
221
292
|
} else {
|
|
@@ -223,16 +294,18 @@ export class ParticleSystem {
|
|
|
223
294
|
}
|
|
224
295
|
}
|
|
225
296
|
|
|
226
|
-
|
|
227
|
-
let rot = Math.PI / 2 * 3
|
|
297
|
+
_drawStarPath(cx, cy, spikes, outerRadius, innerRadius) {
|
|
298
|
+
let rot = Math.PI / 2 * 3;
|
|
299
|
+
let x = cx;
|
|
300
|
+
let y = cy;
|
|
228
301
|
const step = Math.PI / spikes;
|
|
229
302
|
|
|
230
303
|
this.ctx.beginPath();
|
|
231
304
|
this.ctx.moveTo(cx, cy - outerRadius);
|
|
232
305
|
|
|
233
306
|
for (let i = 0; i < spikes; i++) {
|
|
234
|
-
|
|
235
|
-
|
|
307
|
+
x = cx + Math.cos(rot) * outerRadius;
|
|
308
|
+
y = cy + Math.sin(rot) * outerRadius;
|
|
236
309
|
this.ctx.lineTo(x, y);
|
|
237
310
|
rot += step;
|
|
238
311
|
|
|
@@ -241,16 +314,13 @@ export class ParticleSystem {
|
|
|
241
314
|
this.ctx.lineTo(x, y);
|
|
242
315
|
rot += step;
|
|
243
316
|
}
|
|
244
|
-
|
|
245
317
|
this.ctx.lineTo(cx, cy - outerRadius);
|
|
246
318
|
this.ctx.closePath();
|
|
247
|
-
this.ctx.fill();
|
|
248
319
|
}
|
|
249
320
|
|
|
250
|
-
/**
|
|
251
|
-
* Clear all particles
|
|
252
|
-
*/
|
|
253
321
|
clear() {
|
|
322
|
+
// Recycle all
|
|
323
|
+
this.particles.forEach(p => this._recycleParticle(p));
|
|
254
324
|
this.particles = [];
|
|
255
325
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
256
326
|
}
|