added animations
This commit is contained in:
parent
e995513829
commit
f2dd78dc80
124
components/FlickeringGrid.vue
Normal file
124
components/FlickeringGrid.vue
Normal file
@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div ref="containerRef" :class="['h-full w-full', className]">
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
class="pointer-events-none"
|
||||
:style="{ width: canvasSize.width + 'px', height: canvasSize.height + 'px' }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref, watchEffect } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
squareSize: { type: Number, default: 4 },
|
||||
gridGap: { type: Number, default: 6 },
|
||||
flickerChance: { type: Number, default: 0.3 },
|
||||
color: { type: String, default: 'rgb(0, 0, 0)' },
|
||||
width: Number,
|
||||
height: Number,
|
||||
className: String,
|
||||
maxOpacity: { type: Number, default: 0.3 },
|
||||
});
|
||||
|
||||
const canvasRef = ref(null);
|
||||
const containerRef = ref(null);
|
||||
const canvasSize = ref({ width: 0, height: 0 });
|
||||
const isInView = ref(false);
|
||||
|
||||
function toRGBA(color) {
|
||||
if (process.server) return `rgba(0, 0, 0,`;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = canvas.height = 1;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return `rgba(255, 0, 0,`;
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data);
|
||||
return `rgba(${r}, ${g}, ${b},`;
|
||||
}
|
||||
|
||||
|
||||
const memoizedColor = toRGBA(props.color);
|
||||
|
||||
onMounted(() => {
|
||||
const canvas = canvasRef.value;
|
||||
const container = containerRef.value;
|
||||
if (!canvas || !container) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
let cols, rows, squares, animationFrameId, lastTime = 0;
|
||||
|
||||
const setupCanvas = () => {
|
||||
const w = props.width || container.clientWidth;
|
||||
const h = props.height || container.clientHeight;
|
||||
|
||||
canvasSize.value = { width: w, height: h };
|
||||
canvas.width = w * dpr;
|
||||
canvas.height = h * dpr;
|
||||
canvas.style.width = `${w}px`;
|
||||
canvas.style.height = `${h}px`;
|
||||
|
||||
cols = Math.floor(w / (props.squareSize + props.gridGap));
|
||||
rows = Math.floor(h / (props.squareSize + props.gridGap));
|
||||
squares = new Float32Array(cols * rows).map(() => Math.random() * props.maxOpacity);
|
||||
};
|
||||
|
||||
const updateSquares = (deltaTime) => {
|
||||
for (let i = 0; i < squares.length; i++) {
|
||||
if (Math.random() < props.flickerChance * deltaTime) {
|
||||
squares[i] = Math.random() * props.maxOpacity;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const drawGrid = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
for (let i = 0; i < cols; i++) {
|
||||
for (let j = 0; j < rows; j++) {
|
||||
const opacity = squares[i * rows + j];
|
||||
ctx.fillStyle = `${memoizedColor}${opacity})`;
|
||||
ctx.fillRect(
|
||||
i * (props.squareSize + props.gridGap) * dpr,
|
||||
j * (props.squareSize + props.gridGap) * dpr,
|
||||
props.squareSize * dpr,
|
||||
props.squareSize * dpr
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const animate = (time) => {
|
||||
if (!isInView.value) return;
|
||||
const deltaTime = (time - lastTime) / 1000;
|
||||
lastTime = time;
|
||||
updateSquares(deltaTime);
|
||||
drawGrid();
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => setupCanvas());
|
||||
resizeObserver.observe(container);
|
||||
|
||||
const intersectionObserver = new IntersectionObserver(([entry]) => {
|
||||
isInView.value = entry.isIntersecting;
|
||||
if (isInView.value) animationFrameId = requestAnimationFrame(animate);
|
||||
});
|
||||
intersectionObserver.observe(canvas);
|
||||
|
||||
setupCanvas();
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
resizeObserver.disconnect();
|
||||
intersectionObserver.disconnect();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<article class="md:flex">
|
||||
<article ref="postEl" class="md:flex">
|
||||
<h2 class="content-date h-full mt-px">
|
||||
<a href="#2022-06-23">{{ content.date }}</a>
|
||||
</h2>
|
||||
@ -20,9 +20,18 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useScrollReveal } from '@/composables/useScrollReveal'
|
||||
|
||||
const props = defineProps({
|
||||
content: Object,
|
||||
});
|
||||
})
|
||||
|
||||
const postEl = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
useScrollReveal(postEl)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
21
composables/useScrollReveal.js
Normal file
21
composables/useScrollReveal.js
Normal file
@ -0,0 +1,21 @@
|
||||
// composables/useScrollReveal.js
|
||||
import { useMotion } from '@vueuse/motion'
|
||||
|
||||
export const useScrollReveal = (elRef) => {
|
||||
useMotion(elRef, {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
y: 40,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
stiffness: 250,
|
||||
damping: 20,
|
||||
},
|
||||
},
|
||||
onAppear: true,
|
||||
})
|
||||
}
|
1310
package-lock.json
generated
1310
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,5 +12,8 @@
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"nuxt": "^3.1.1",
|
||||
"nuxt-icon": "^0.2.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/motion": "^3.0.3"
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,27 @@
|
||||
<template>
|
||||
<main class="min-h-screen relative bg-background">
|
||||
<hero />
|
||||
<!-- // Remove this once you clone the template -->
|
||||
<!-- <github /> -->
|
||||
<!-- // Remove this once you clone the template -->
|
||||
<section
|
||||
class="relative mx-auto max-w-5xl px-4 sm:px-6 lg:px-8 overflow-hidden text-primary"
|
||||
>
|
||||
<post v-for="post in data" :content="post" />
|
||||
</section>
|
||||
<main class="min-h-screen relative bg-[#070F11]">
|
||||
<FlickeringGrid
|
||||
class="absolute inset-0 z-0"
|
||||
color="#0CE77E"
|
||||
:squareSize="3"
|
||||
:gridGap="10"
|
||||
:maxOpacity="0.2"
|
||||
/>
|
||||
<div class="relative z-10">
|
||||
<hero />
|
||||
<section
|
||||
class="relative mx-auto max-w-5xl px-4 sm:px-6 lg:px-8 overflow-hidden text-primary"
|
||||
>
|
||||
<post v-for="post in data" :content="post" />
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FlickeringGrid from "@/components/FlickeringGrid.vue";
|
||||
import seoConfig from "../seoConfig/index";
|
||||
|
||||
useHead({
|
||||
title: seoConfig.title,
|
||||
meta: [
|
||||
@ -33,6 +41,7 @@ useHead({
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { data } = await useAsyncData("feed", () =>
|
||||
queryContent("/posts").find()
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user