Initial commit: 网络验证平台
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface FlickeringGridProps {
|
||||
squareSize?: number
|
||||
gridGap?: number
|
||||
flickerChance?: number
|
||||
color?: string
|
||||
width?: number
|
||||
height?: number
|
||||
class?: string
|
||||
maxOpacity?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<FlickeringGridProps>(), {
|
||||
squareSize: 4,
|
||||
gridGap: 6,
|
||||
flickerChance: 0.3,
|
||||
color: 'rgb(0, 0, 0)',
|
||||
maxOpacity: 0.3,
|
||||
})
|
||||
|
||||
const { squareSize, gridGap, flickerChance, color, maxOpacity, width, height } = toRefs(props)
|
||||
|
||||
const containerRef = useTemplateRef<HTMLDivElement>('containerRef')
|
||||
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef')
|
||||
const context = ref<CanvasRenderingContext2D>()
|
||||
|
||||
const isInView = ref(false)
|
||||
const canvasSize = ref({ width: 0, height: 0 })
|
||||
|
||||
const computedColor = computed(() => {
|
||||
if (!context.value)
|
||||
return 'rgba(255, 0, 0,'
|
||||
|
||||
const hex = color.value.replace(/^#/, '')
|
||||
const bigint = Number.parseInt(hex, 16)
|
||||
const r = (bigint >> 16) & 255
|
||||
const g = (bigint >> 8) & 255
|
||||
const b = bigint & 255
|
||||
return `rgba(${r}, ${g}, ${b},`
|
||||
})
|
||||
|
||||
function setupCanvas(
|
||||
canvas: HTMLCanvasElement,
|
||||
width: number,
|
||||
height: number,
|
||||
): {
|
||||
cols: number
|
||||
rows: number
|
||||
squares: Float32Array
|
||||
dpr: number
|
||||
} {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
|
||||
const cols = Math.floor(width / (squareSize.value + gridGap.value))
|
||||
const rows = Math.floor(height / (squareSize.value + gridGap.value))
|
||||
|
||||
const squares = new Float32Array(cols * rows)
|
||||
for (let i = 0; i < squares.length; i++) {
|
||||
squares[i] = Math.random() * maxOpacity.value
|
||||
}
|
||||
return { cols, rows, squares, dpr }
|
||||
}
|
||||
|
||||
function updateSquares(squares: Float32Array, deltaTime: number) {
|
||||
for (let i = 0; i < squares.length; i++) {
|
||||
if (Math.random() < flickerChance.value * deltaTime) {
|
||||
squares[i] = Math.random() * maxOpacity.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawGrid(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
cols: number,
|
||||
rows: number,
|
||||
squares: Float32Array,
|
||||
dpr: number,
|
||||
) {
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
ctx.fillStyle = 'transparent'
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
for (let i = 0; i < cols; i++) {
|
||||
for (let j = 0; j < rows; j++) {
|
||||
const opacity = squares[i * rows + j]
|
||||
ctx.fillStyle = `${computedColor.value}${opacity})`
|
||||
ctx.fillRect(
|
||||
i * (squareSize.value + gridGap.value) * dpr,
|
||||
j * (squareSize.value + gridGap.value) * dpr,
|
||||
squareSize.value * dpr,
|
||||
squareSize.value * dpr,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const gridParams = ref<ReturnType<typeof setupCanvas>>()
|
||||
|
||||
function updateCanvasSize() {
|
||||
const newWidth = width.value || containerRef.value!.clientWidth
|
||||
const newHeight = height.value || containerRef.value!.clientHeight
|
||||
|
||||
canvasSize.value = { width: newWidth, height: newHeight }
|
||||
gridParams.value = setupCanvas(canvasRef.value!, newWidth, newHeight)
|
||||
}
|
||||
|
||||
let animationFrameId: number | undefined
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
let intersectionObserver: IntersectionObserver | undefined
|
||||
let lastTime = 0
|
||||
|
||||
function animate(time: number) {
|
||||
if (!isInView.value)
|
||||
return
|
||||
|
||||
const deltaTime = (time - lastTime) / 1000
|
||||
lastTime = time
|
||||
|
||||
updateSquares(gridParams.value!.squares, deltaTime)
|
||||
drawGrid(
|
||||
context.value!,
|
||||
canvasRef.value!.width,
|
||||
canvasRef.value!.height,
|
||||
gridParams.value!.cols,
|
||||
gridParams.value!.rows,
|
||||
gridParams.value!.squares,
|
||||
gridParams.value!.dpr,
|
||||
)
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!canvasRef.value || !containerRef.value)
|
||||
return
|
||||
context.value = canvasRef.value.getContext('2d')!
|
||||
if (!context.value)
|
||||
return
|
||||
|
||||
updateCanvasSize()
|
||||
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
updateCanvasSize()
|
||||
})
|
||||
intersectionObserver = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
isInView.value = entry.isIntersecting
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
},
|
||||
{ threshold: 0 },
|
||||
)
|
||||
|
||||
resizeObserver.observe(containerRef.value)
|
||||
intersectionObserver.observe(canvasRef.value)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
}
|
||||
resizeObserver?.disconnect()
|
||||
intersectionObserver?.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
:class="cn('w-full h-full', props.class)"
|
||||
>
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
class="pointer-events-none"
|
||||
:width="canvasSize.width"
|
||||
:height="canvasSize.height"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,197 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { animate } from 'motion-v'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface Props {
|
||||
blur?: number
|
||||
inactiveZone?: number
|
||||
proximity?: number
|
||||
spread?: number
|
||||
variant?: 'default' | 'white'
|
||||
glow?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
disabled?: boolean
|
||||
movementDuration?: number
|
||||
borderWidth?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
blur: 0,
|
||||
inactiveZone: 0.7,
|
||||
proximity: 0,
|
||||
spread: 20,
|
||||
variant: 'default',
|
||||
glow: false,
|
||||
movementDuration: 2,
|
||||
borderWidth: 1,
|
||||
disabled: true,
|
||||
})
|
||||
|
||||
const containerRef = useTemplateRef('containerRef')
|
||||
const lastPosition = ref({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
const animationFrame = ref(0)
|
||||
|
||||
const containerStyles = computed(() => {
|
||||
return {
|
||||
'--blur': `${props.blur}px`,
|
||||
'--spread': props.spread,
|
||||
'--start': '0',
|
||||
'--active': '0',
|
||||
'--glowingeffect-border-width': `${props.borderWidth}px`,
|
||||
'--repeating-conic-gradient-times': '5',
|
||||
'--gradient':
|
||||
props.variant === 'white'
|
||||
? `repeating-conic-gradient(
|
||||
from 236.84deg at 50% 50%,
|
||||
var(--black),
|
||||
var(--black) calc(25% / var(--repeating-conic-gradient-times))
|
||||
)`
|
||||
: `radial-gradient(circle, #dd7bbb 10%, #dd7bbb00 20%),
|
||||
radial-gradient(circle at 40% 40%, #d79f1e 5%, #d79f1e00 15%),
|
||||
radial-gradient(circle at 60% 60%, #5a922c 10%, #5a922c00 20%),
|
||||
radial-gradient(circle at 40% 60%, #4c7894 10%, #4c789400 20%),
|
||||
repeating-conic-gradient(
|
||||
from 236.84deg at 50% 50%,
|
||||
#dd7bbb 0%,
|
||||
#d79f1e calc(25% / var(--repeating-conic-gradient-times)),
|
||||
#5a922c calc(50% / var(--repeating-conic-gradient-times)),
|
||||
#4c7894 calc(75% / var(--repeating-conic-gradient-times)),
|
||||
#dd7bbb calc(100% / var(--repeating-conic-gradient-times))
|
||||
)`,
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (props.disabled)
|
||||
return
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
document.body.addEventListener('pointermove', handlePointerMove, {
|
||||
passive: true,
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationFrame.value) {
|
||||
cancelAnimationFrame(animationFrame.value)
|
||||
}
|
||||
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
document.body.removeEventListener('pointermove', handlePointerMove)
|
||||
})
|
||||
|
||||
function handlePointerMove(e: PointerEvent) {
|
||||
handleMove(e)
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
handleMove()
|
||||
}
|
||||
|
||||
function handleMove(e?: MouseEvent | PointerEvent | { x: number, y: number }) {
|
||||
if (!containerRef.value)
|
||||
return
|
||||
|
||||
if (animationFrame.value) {
|
||||
cancelAnimationFrame(animationFrame.value)
|
||||
}
|
||||
|
||||
animationFrame.value = requestAnimationFrame(() => {
|
||||
const element = containerRef.value
|
||||
|
||||
if (!element)
|
||||
return
|
||||
|
||||
const { left, top, width, height } = element.getBoundingClientRect()
|
||||
|
||||
const mouseX = e?.x ?? lastPosition.value.x
|
||||
const mouseY = e?.y ?? lastPosition.value.y
|
||||
|
||||
if (e) {
|
||||
lastPosition.value = { x: mouseX, y: mouseY }
|
||||
}
|
||||
|
||||
const center = [left + width * 0.5, top + height * 0.5]
|
||||
const distanceFromCenter = Math.hypot(mouseX - center[0], mouseY - center[1])
|
||||
const inactiveRadius = 0.5 * Math.min(width, height) * props.inactiveZone
|
||||
|
||||
if (distanceFromCenter < inactiveRadius) {
|
||||
element.style.setProperty('--active', '0')
|
||||
return
|
||||
}
|
||||
|
||||
const isActive
|
||||
= mouseX > left - props.proximity
|
||||
&& mouseX < left + width + props.proximity
|
||||
&& mouseY > top - props.proximity
|
||||
&& mouseY < top + height + props.proximity
|
||||
|
||||
element.style.setProperty('--active', isActive ? '1' : '0')
|
||||
|
||||
if (!isActive)
|
||||
return
|
||||
|
||||
const currentAngle = Number.parseFloat(element.style.getPropertyValue('--start')) || 0
|
||||
const targetAngle = (180 * Math.atan2(mouseY - center[1], mouseX - center[0])) / Math.PI + 90
|
||||
|
||||
const angleDiff = ((targetAngle - currentAngle + 180) % 360) - 180
|
||||
const newAngle = currentAngle + angleDiff
|
||||
|
||||
animate(currentAngle, newAngle, {
|
||||
duration: props.movementDuration,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
onUpdate: (value) => {
|
||||
element.style.setProperty('--start', String(value))
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'pointer-events-none absolute -inset-px hidden rounded-[inherit] border opacity-0 transition-opacity',
|
||||
glow && 'opacity-100',
|
||||
variant === 'white' && 'border-white',
|
||||
disabled && 'block!',
|
||||
)
|
||||
"
|
||||
/>
|
||||
<div
|
||||
ref="containerRef"
|
||||
:style="containerStyles"
|
||||
:class="
|
||||
cn(
|
||||
'pointer-events-none absolute inset-0 rounded-[inherit] opacity-100 transition-opacity',
|
||||
glow && 'opacity-100',
|
||||
blur > 0 && 'blur-(--blur)',
|
||||
props.class,
|
||||
disabled && 'hidden!',
|
||||
)
|
||||
"
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'glow',
|
||||
'rounded-[inherit]',
|
||||
`after:content-[''] after:rounded-[inherit] after:absolute after:inset-[calc(-1*var(--glowingeffect-border-width))]`,
|
||||
'after:[border:var(--glowingeffect-border-width)_solid_transparent]',
|
||||
'after:[background:var(--gradient)] after:bg-fixed',
|
||||
'after:opacity-(--active) after:transition-opacity after:duration-300',
|
||||
'after:[mask-clip:padding-box,border-box]',
|
||||
'after:mask-intersect',
|
||||
'after:mask-[linear-gradient(#0000,#0000),conic-gradient(from_calc((var(--start)-var(--spread))*1deg),#00000000_0deg,#fff,#00000000_calc(var(--spread)*2deg))]',
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
class?: string
|
||||
reverse?: boolean
|
||||
pauseOnHover?: boolean
|
||||
vertical?: boolean
|
||||
repeat?: number
|
||||
}>(),
|
||||
{
|
||||
pauseOnHover: false,
|
||||
vertical: false,
|
||||
repeat: 4,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'group flex overflow-hidden p-2 [--duration:40s] [--gap:1rem] gap-(--gap)',
|
||||
vertical ? 'flex-col' : 'flex-row',
|
||||
$props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="index in repeat"
|
||||
:key="index"
|
||||
:class="
|
||||
cn(
|
||||
'flex shrink-0 justify-around gap-(--gap)',
|
||||
vertical ? 'animate-marquee-vertical flex-col' : 'animate-marquee flex-row',
|
||||
pauseOnHover ? 'group-hover:paused' : '',
|
||||
)
|
||||
"
|
||||
:style="{
|
||||
animationDirection: reverse ? 'reverse' : 'normal',
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.animate-marquee {
|
||||
animation: marquee var(--duration) linear infinite;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
|
||||
.animate-marquee-vertical {
|
||||
animation: marquee-vertical var(--duration) linear infinite;
|
||||
}
|
||||
|
||||
@keyframes marquee {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(calc(-100% - var(--gap)));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee-vertical {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
transform: translateY(calc(-100% - var(--gap)));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
img: string
|
||||
name: string
|
||||
username: string
|
||||
body: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<figure
|
||||
class="relative w-64 cursor-pointer overflow-hidden rounded-xl border border-gray-950/10 bg-gray-950/1 p-4 hover:bg-gray-950/5 dark:border-gray-50/10 dark:bg-gray-50/10 dark:hover:bg-gray-50/15"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<img :src="img" class="rounded-full" width="32" height="32" alt="">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-medium dark:text-white">
|
||||
{{ name }}
|
||||
</span>
|
||||
<p class="text-xs font-medium dark:text-white/40">
|
||||
{{ username }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<blockquote class="mt-2 text-sm">
|
||||
{{ body }}
|
||||
</blockquote>
|
||||
</figure>
|
||||
</template>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface Props {
|
||||
size?: number
|
||||
class?: string
|
||||
opacity?: number
|
||||
animationDelay?: number
|
||||
borderStyle?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 210,
|
||||
opacity: 0.24,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('absolute shadow-xl', 'animate-ripple-circle', props.class)" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.animate-ripple-circle {
|
||||
animation: ripple-effect var(--duration, 2s) ease-in-out calc(var(--i, 0) * 0.2s) infinite;
|
||||
border-width: 1px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: v-bind('`${props.size}px`');
|
||||
height: v-bind('`${props.size}px`');
|
||||
animation-delay: v-bind('`${props.animationDelay}ms`');
|
||||
opacity: v-bind('props.opacity');
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
border-style: v-bind('props.borderStyle');
|
||||
}
|
||||
|
||||
@keyframes ripple-effect {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import RippleCircle from './circle.vue'
|
||||
|
||||
interface Props {
|
||||
baseCircleSize?: number
|
||||
baseCircleOpacity?: number
|
||||
spaceBetweenCircle?: number
|
||||
circleOpacityDowngradeRatio?: number
|
||||
circleClass?: string
|
||||
waveSpeed?: number
|
||||
numberOfCircles?: number
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
baseCircleSize: 210,
|
||||
baseCircleOpacity: 0.24,
|
||||
circleOpacityDowngradeRatio: 0.03,
|
||||
waveSpeed: 80,
|
||||
spaceBetweenCircle: 70,
|
||||
numberOfCircles: 7,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute inset-0">
|
||||
<RippleCircle
|
||||
v-for="index in numberOfCircles"
|
||||
:key="index"
|
||||
:opacity="baseCircleOpacity - index * circleOpacityDowngradeRatio"
|
||||
:size="baseCircleSize + index * spaceBetweenCircle"
|
||||
:animation-delay="index * waveSpeed"
|
||||
:border-style="index === numberOfCircles - 1 ? 'dashed' : 'solid'"
|
||||
:class="circleClass"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import RippleCircle from './circle.vue'
|
||||
|
||||
interface Props {
|
||||
baseCircleSize?: number
|
||||
baseCircleOpacity?: number
|
||||
spaceBetweenCircle?: number
|
||||
circleOpacityDowngradeRatio?: number
|
||||
circleClass?: string
|
||||
waveSpeed?: number
|
||||
numberOfCircles?: number
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
baseCircleSize: 210,
|
||||
baseCircleOpacity: 0.24,
|
||||
circleOpacityDowngradeRatio: 0.03,
|
||||
waveSpeed: 80,
|
||||
spaceBetweenCircle: 70,
|
||||
numberOfCircles: 7,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute inset-0">
|
||||
<RippleCircle
|
||||
v-for="index in numberOfCircles"
|
||||
:key="index"
|
||||
:opacity="baseCircleOpacity - index * circleOpacityDowngradeRatio"
|
||||
:size="baseCircleSize + index * spaceBetweenCircle"
|
||||
:animation-delay="index * waveSpeed"
|
||||
:border-style="index === numberOfCircles - 1 ? 'dashed' : 'solid'"
|
||||
:class="circleClass"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user