feat: MFM Sparkle animation (#7813)
* Add sparkle mfm animation ✨
* Cleanup sparkle effect
+ spaces -> tabs and other codestyle
+ use proper image
+ listen for resizes
+ use font-size to determine particle size (for fun with x2/3/4 stacking)
			
			
This commit is contained in:
		
							parent
							
								
									76c5dc8999
								
							
						
					
					
						commit
						a499ad6879
					
				
					 5 changed files with 208 additions and 1 deletions
				
			
		
							
								
								
									
										
											BIN
										
									
								
								assets/client/sparkle-spritesheet.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/client/sparkle-spritesheet.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 185 B | 
|  | @ -899,6 +899,8 @@ _mfm: | |||
|   fontDescription: "Sets the font to display contents in." | ||||
|   rainbow: "Rainbow" | ||||
|   rainbowDescription: "Makes the content appear in rainbow colors." | ||||
|   sparkle: "Sparkle" | ||||
|   sparkleDescription: "Infuses a sparkling animation" | ||||
| _reversi: | ||||
|   reversi: "Reversi" | ||||
|   gameSettings: "Game settings" | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ import { concat } from '@client/../prelude/array'; | |||
| import MkFormula from '@client/components/formula.vue'; | ||||
| import MkCode from '@client/components/code.vue'; | ||||
| import MkGoogle from '@client/components/google.vue'; | ||||
| import MkSparkle from '@client/components/sparkle.vue'; | ||||
| import MkA from '@client/components/global/a.vue'; | ||||
| import { host } from '@client/config'; | ||||
| 
 | ||||
|  | @ -169,6 +170,19 @@ export default defineComponent({ | |||
| 							style = this.$store.state.animatedMfm ? 'animation: mfm-rainbow 1s linear infinite;' : ''; | ||||
| 							break; | ||||
| 						} | ||||
| 						case 'sparkle': { | ||||
| 							if (!this.$store.state.animatedMfm) { | ||||
| 								return genEl(token.children); | ||||
| 							} | ||||
| 							let count = token.props.args.count ? parseInt(token.props.args.count) : 10; | ||||
| 							if (count > 100) { | ||||
| 								count = 100; | ||||
| 							} | ||||
| 							const speed = token.props.args.speed ? parseFloat(token.props.args.speed) : 1; | ||||
| 							return h(MkSparkle, { | ||||
| 								count, speed, | ||||
| 							}, genEl(token.children)); | ||||
| 						} | ||||
| 					} | ||||
| 					if (style == null) { | ||||
| 						return h('span', {}, ['[', token.props.name, ...genEl(token.children), ']']); | ||||
|  |  | |||
							
								
								
									
										180
									
								
								src/client/components/sparkle.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								src/client/components/sparkle.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,180 @@ | |||
| <template> | ||||
| <span class="mk-sparkle"> | ||||
| 	<span ref="content"> | ||||
| 		<slot></slot> | ||||
| 	</span> | ||||
| 	<canvas ref="canvas"></canvas> | ||||
| </span> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import * as os from '@client/os'; | ||||
| 
 | ||||
| const sprite = new Image(); | ||||
| sprite.src = "/static-assets/client/sparkle-spritesheet.png"; | ||||
| 
 | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
| 		count: { | ||||
| 			type: Number, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		speed: { | ||||
| 			type: Number, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			sprites: [0,6,13,20], | ||||
| 			particles: [], | ||||
| 			anim: null, | ||||
| 			ctx: null, | ||||
| 		}; | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		createSparkles(w, h, count) { | ||||
| 			var holder = []; | ||||
| 
 | ||||
| 			for (var i = 0; i < count; i++) { | ||||
| 
 | ||||
| 				const color = '#' + ('000000' + Math.floor(Math.random() * 16777215).toString(16)).slice(-6); | ||||
| 
 | ||||
| 				holder[i] = { | ||||
| 					position: { | ||||
| 						x: Math.floor(Math.random() * w), | ||||
| 						y: Math.floor(Math.random() * h) | ||||
| 					}, | ||||
| 					style: this.sprites[ Math.floor(Math.random() * 4) ], | ||||
| 					delta: { | ||||
| 						x: Math.floor(Math.random() * 1000) - 500, | ||||
| 						y: Math.floor(Math.random() * 1000) - 500 | ||||
| 					}, | ||||
| 					color: color, | ||||
| 					opacity: Math.random(), | ||||
| 				}; | ||||
| 
 | ||||
| 			} | ||||
| 
 | ||||
| 			return holder; | ||||
| 		}, | ||||
| 		draw(time) { | ||||
| 			this.ctx.clearRect(0, 0, this.$refs.canvas.width, this.$refs.canvas.height); | ||||
| 			this.ctx.beginPath(); | ||||
| 
 | ||||
| 			const particleSize = Math.floor(this.fontSize / 2); | ||||
| 			this.particles.forEach((particle) => { | ||||
| 				var modulus = Math.floor(Math.random()*7); | ||||
| 
 | ||||
| 				if (Math.floor(time) % modulus === 0) { | ||||
| 					particle.style = this.sprites[ Math.floor(Math.random()*4) ]; | ||||
| 				} | ||||
| 
 | ||||
| 				this.ctx.save(); | ||||
| 				this.ctx.globalAlpha = particle.opacity; | ||||
| 				this.ctx.drawImage(sprite, particle.style, 0, 7, 7, particle.position.x, particle.position.y, particleSize, particleSize); | ||||
| 
 | ||||
| 				this.ctx.globalCompositeOperation = "source-atop"; | ||||
| 				this.ctx.globalAlpha = 0.5; | ||||
| 				this.ctx.fillStyle = particle.color; | ||||
| 				this.ctx.fillRect(particle.position.x, particle.position.y, particleSize, particleSize); | ||||
| 
 | ||||
| 				this.ctx.restore(); | ||||
| 			}); | ||||
| 			this.ctx.stroke(); | ||||
| 		}, | ||||
| 		tick() { | ||||
| 			this.anim = window.requestAnimationFrame((time) => { | ||||
| 				if (!this.$refs.canvas) { | ||||
| 					return; | ||||
| 				} | ||||
| 				this.particles.forEach((particle) => { | ||||
| 					if (!particle) { | ||||
| 						return; | ||||
| 					} | ||||
| 					var randX = Math.random() > Math.random() * 2; | ||||
| 					var randY = Math.random() > Math.random() * 3; | ||||
| 
 | ||||
| 					if (randX) { | ||||
| 						particle.position.x += (particle.delta.x * this.speed) / 1500; | ||||
| 					} | ||||
| 
 | ||||
| 					if (!randY) { | ||||
| 						particle.position.y -= (particle.delta.y * this.speed) / 800; | ||||
| 					} | ||||
| 
 | ||||
| 					if( particle.position.x > this.$refs.canvas.width ) { | ||||
| 						particle.position.x = -7; | ||||
| 					} else if (particle.position.x < -7) { | ||||
| 						particle.position.x = this.$refs.canvas.width; | ||||
| 					} | ||||
| 
 | ||||
| 					if (particle.position.y > this.$refs.canvas.height) { | ||||
| 						particle.position.y = -7; | ||||
| 						particle.position.x = Math.floor(Math.random() * this.$refs.canvas.width); | ||||
| 					} else if (particle.position.y < -7) { | ||||
| 						particle.position.y = this.$refs.canvas.height; | ||||
| 						particle.position.x = Math.floor(Math.random() * this.$refs.canvas.width); | ||||
| 					} | ||||
| 
 | ||||
| 					particle.opacity -= 0.005; | ||||
| 
 | ||||
| 					if (particle.opacity <= 0) { | ||||
| 						particle.opacity = 1; | ||||
| 					} | ||||
| 				}); | ||||
| 
 | ||||
| 				this.draw(time); | ||||
| 
 | ||||
| 				this.tick(); | ||||
| 			}); | ||||
| 		}, | ||||
| 		resize() { | ||||
| 			if (this.$refs.content) { | ||||
| 				const contentRect = this.$refs.content.getBoundingClientRect(); | ||||
| 				this.fontSize = parseFloat(getComputedStyle(this.$refs.content).fontSize); | ||||
| 				const padding = this.fontSize * 0.2; | ||||
| 
 | ||||
| 				this.$refs.canvas.width = parseInt(contentRect.width + padding); | ||||
| 				this.$refs.canvas.height = parseInt(contentRect.height + padding); | ||||
| 
 | ||||
| 				this.particles = this.createSparkles(this.$refs.canvas.width, this.$refs.canvas.height, this.count); | ||||
| 			} | ||||
| 		}, | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.ctx = this.$refs.canvas.getContext('2d'); | ||||
| 
 | ||||
| 		new ResizeObserver(this.resize).observe(this.$refs.content); | ||||
| 
 | ||||
| 		this.resize(); | ||||
| 		this.tick(); | ||||
| 	}, | ||||
| 	updated() { | ||||
| 		this.resize(); | ||||
| 	}, | ||||
| 	destroyed() { | ||||
| 		window.cancelAnimationFrame(this.anim); | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .mk-sparkle { | ||||
| 	position: relative; | ||||
| 	display: inline-block; | ||||
| 
 | ||||
| 	> span { | ||||
| 		display: inline-block; | ||||
| 	} | ||||
| 
 | ||||
| 	> canvas { | ||||
| 		position: absolute; | ||||
| 		top: -0.1em; | ||||
| 		left: -0.1em; | ||||
| 		pointer-events: none; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -271,6 +271,16 @@ | |||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.sparkle }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.sparkleDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_sparkle"/> | ||||
| 				<MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -294,7 +304,7 @@ export default defineComponent({ | |||
| 			preview_hashtag: '#test', | ||||
| 			preview_url: `https://example.com`, | ||||
| 			preview_link: `[${this.$ts._mfm.dummy}](https://example.com)`, | ||||
| 			preview_emoji: `:${this.$instance.emojis[0].name}:`, | ||||
| 			preview_emoji: this.$instance.emojis.length ? `:${this.$instance.emojis[0].name}:` : `:emojiname:`, | ||||
| 			preview_bold: `**${this.$ts._mfm.dummy}**`, | ||||
| 			preview_small: `<small>${this.$ts._mfm.dummy}</small>`, | ||||
| 			preview_center: `<center>${this.$ts._mfm.dummy}</center>`, | ||||
|  | @ -317,6 +327,7 @@ export default defineComponent({ | |||
| 			preview_x4: `$[x4 🍮]`, | ||||
| 			preview_blur: `$[blur ${this.$ts._mfm.dummy}]`, | ||||
| 			preview_rainbow: `$[rainbow 🍮]`, | ||||
| 			preview_sparkle: `$[sparkle 🍮]`, | ||||
| 		} | ||||
| 	}, | ||||
| }); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue