feat(client): svg sparkle effect

Resolve #8088
This commit is contained in:
syuilo 2021-12-29 04:13:45 +09:00
parent 30892ebfa3
commit 5c7181ca21
3 changed files with 106 additions and 131 deletions

View file

@ -11,6 +11,7 @@
### Improvements ### Improvements
- クライアント: ノートプレビューの精度を改善 - クライアント: ノートプレビューの精度を改善
- クライアント: MFM sparkleエフェクトの改善
### Bugfixes ### Bugfixes
- クライアント: 一部のコンポーネントが裏に隠れるのを修正 - クライアント: 一部のコンポーネントが裏に隠れるのを修正

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 B

View file

@ -1,14 +1,69 @@
<template> <template>
<span class="mk-sparkle"> <span class="mk-sparkle">
<span ref="content"> <span ref="el">
<slot></slot> <slot></slot>
</span> </span>
<canvas ref="canvas"></canvas> <!-- なぜか path に対する key が機能しないため
<svg :width="width" :height="height" :viewBox="`0 0 ${width} ${height}`" xmlns="http://www.w3.org/2000/svg">
<path v-for="particle in particles" :key="particle.id" style="transform-origin: center; transform-box: fill-box;"
:transform="`translate(${particle.x} ${particle.y})`"
:fill="particle.color"
d="M29.427,2.011C29.721,0.83 30.782,0 32,0C33.218,0 34.279,0.83 34.573,2.011L39.455,21.646C39.629,22.347 39.991,22.987 40.502,23.498C41.013,24.009 41.653,24.371 42.354,24.545L61.989,29.427C63.17,29.721 64,30.782 64,32C64,33.218 63.17,34.279 61.989,34.573L42.354,39.455C41.653,39.629 41.013,39.991 40.502,40.502C39.991,41.013 39.629,41.653 39.455,42.354L34.573,61.989C34.279,63.17 33.218,64 32,64C30.782,64 29.721,63.17 29.427,61.989L24.545,42.354C24.371,41.653 24.009,41.013 23.498,40.502C22.987,39.991 22.347,39.629 21.646,39.455L2.011,34.573C0.83,34.279 0,33.218 0,32C0,30.782 0.83,29.721 2.011,29.427L21.646,24.545C22.347,24.371 22.987,24.009 23.498,23.498C24.009,22.987 24.371,22.347 24.545,21.646L29.427,2.011Z"
>
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
from="0 0 0"
to="360 0 0"
:dur="`${particle.dur}ms`"
repeatCount="indefinite"
additive="sum"
/>
<animateTransform
attributeName="transform"
attributeType="XML"
type="scale"
:values="`0; ${particle.size}; 0`"
:dur="`${particle.dur}ms`"
repeatCount="indefinite"
additive="sum"
/>
</path>
</svg>
-->
<svg v-for="particle in particles" :key="particle.id" :width="width" :height="height" :viewBox="`0 0 ${width} ${height}`" xmlns="http://www.w3.org/2000/svg">
<path style="transform-origin: center; transform-box: fill-box;"
:transform="`translate(${particle.x} ${particle.y})`"
:fill="particle.color"
d="M29.427,2.011C29.721,0.83 30.782,0 32,0C33.218,0 34.279,0.83 34.573,2.011L39.455,21.646C39.629,22.347 39.991,22.987 40.502,23.498C41.013,24.009 41.653,24.371 42.354,24.545L61.989,29.427C63.17,29.721 64,30.782 64,32C64,33.218 63.17,34.279 61.989,34.573L42.354,39.455C41.653,39.629 41.013,39.991 40.502,40.502C39.991,41.013 39.629,41.653 39.455,42.354L34.573,61.989C34.279,63.17 33.218,64 32,64C30.782,64 29.721,63.17 29.427,61.989L24.545,42.354C24.371,41.653 24.009,41.013 23.498,40.502C22.987,39.991 22.347,39.629 21.646,39.455L2.011,34.573C0.83,34.279 0,33.218 0,32C0,30.782 0.83,29.721 2.011,29.427L21.646,24.545C22.347,24.371 22.987,24.009 23.498,23.498C24.009,22.987 24.371,22.347 24.545,21.646L29.427,2.011Z"
>
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
from="0 0 0"
to="360 0 0"
:dur="`${particle.dur}ms`"
repeatCount="1"
additive="sum"
/>
<animateTransform
attributeName="transform"
attributeType="XML"
type="scale"
:values="`0; ${particle.size}; 0`"
:dur="`${particle.dur}ms`"
repeatCount="1"
additive="sum"
/>
</path>
</svg>
</span> </span>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
const sprite = new Image(); const sprite = new Image();
@ -25,137 +80,56 @@ export default defineComponent({
required: true, required: true,
}, },
}, },
data() {
return {
sprites: [0,6,13,20],
particles: [],
anim: null,
ctx: null,
};
},
unmounted() {
window.cancelAnimationFrame(this.anim);
},
mounted() {
this.ctx = this.$refs.canvas.getContext('2d');
new ResizeObserver(this.resize).observe(this.$refs.content); setup(props) {
const particles = ref([]);
const el = ref<HTMLElement>();
const width = ref(0);
const height = ref(0);
const colors = ['#FF1493', '#00FFFF', '#FFE202', '#FFE202', '#FFE202'];
this.resize(); onMounted(() => {
this.tick(); const ro = new ResizeObserver((entries, observer) => {
}, width.value = el.value?.offsetWidth + 64;
updated() { height.value = el.value?.offsetHeight + 64;
this.resize(); });
}, ro.observe(el.value);
methods: { let stop = false;
createSparkles(w, h, count) { const add = () => {
const holder = []; if (stop) return;
const x = (Math.random() * (width.value - 64));
for (let i = 0; i < count; i++) { const y = (Math.random() * (height.value - 64));
const sizeFactor = Math.random();
const color = '#' + ('000000' + Math.floor(Math.random() * 16777215).toString(16)).slice(-6); const particle = {
id: Math.random().toString(),
holder[i] = { x,
position: { y,
x: Math.floor(Math.random() * w), size: 0.2 + ((sizeFactor / 10) * 3),
y: Math.floor(Math.random() * h) dur: 1000 + (sizeFactor * 1000),
}, color: colors[Math.floor(Math.random() * colors.length)],
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(),
}; };
particles.value.push(particle);
window.setTimeout(() => {
particles.value = particles.value.filter(x => x.id !== particle.id);
}, particle.dur - 100);
} window.setTimeout(() => {
add();
return holder; }, 500 + (Math.random() * 500));
}, };
draw(time) { add();
this.ctx.clearRect(0, 0, this.$refs.canvas.width, this.$refs.canvas.height); onUnmounted(() => {
this.ctx.beginPath(); ro.disconnect();
stop = true;
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) { return {
particle.position.x += (particle.delta.x * this.speed) / 1500; el,
} width,
height,
if (!randY) { particles,
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);
}
},
}, },
}); });
</script> </script>
@ -169,10 +143,10 @@ export default defineComponent({
display: inline-block; display: inline-block;
} }
> canvas { > svg {
position: absolute; position: absolute;
top: -0.1em; top: -32px;
left: -0.1em; left: -32px;
pointer-events: none; pointer-events: none;
} }
} }