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