Skip to content

INFO

基础的展示效果,需要鼠标键盘交互效果

跟随鼠标移动的小球

<script setup>
import { onMounted } from "vue";

const init = () => {
	const container = document.querySelector("#mutual_01");
	const clientRect = container?.getBoundingClientRect();
	container.addEventListener("mousemove", e => {
		const w = clientRect.width - 50;
		const h = clientRect.height - 50;
		// 边界判断, 50 是为了避免鼠标移到边界时,超出容器 正好是球的宽度和高度
		container.style.setProperty("--x", `${e.offsetX >= w ? w : e.offsetX}px`);
		container.style.setProperty("--y", `${e.offsetY >= h ? h : e.offsetY}px`);
	});
};

onMounted(init);
</script>

<template>
	<div class="container" id="mutual_01"></div>
</template>

<style lang="scss" scoped>
#mutual_01 {
	--x: -100px;
	--y: -100px;
	position: relative;
	width: inherit;
	height: 300px;
	background: url("https://file.wangzevw.com/images/default_top_img.webp")
		no-repeat;

	&::before {
		content: "";
		display: block;
		position: absolute;
		inset: 0;
		z-index: 10;
		background-color: rgba(0, 0, 0, 0.5);
		filter: grayscale(0.5);
		cursor: pointer;
	}
	&::after {
		content: "";
		display: block;
		width: 50px;
		height: 50px;
		border-radius: 50px;
		position: absolute;
		box-shadow: 0 0 10px 10px rgba(255, 255, 255, 0.5);
		z-index: 100;
		transform: translate(var(--x), var(--y));
		transition: cubic-bezier(0.215, 0.61, 0.355, 1) 0.5s;
	}
}
</style>

跳动的小球

<script setup>
import { onMounted } from "vue";

const init = () => {
	const container = document.querySelector("#mutual_02");
	const clientRect = container?.getBoundingClientRect();

	const balData = [];

	// 最大限制小球的生成数
	const maxBal = 10;

	// 最大限制小球x/y移动的距离
	const max_m_x = 10;
	const max_m_y = 10;

	const w = clientRect.width;
	const h = clientRect.height;

	for (let i = 0; i < maxBal; i++) {
		const bal = document.createElement("div");
		bal.classList.add("bal");
		let w_h = Math.random() * 50 + 10;
		bal.style.width = `${w_h}px`;
		bal.style.height = `${w_h}px`;

		bal.style.transform = `translate(${Math.random() * 100}}px, ${
			Math.random() * 100
		}}px)`;

		container.appendChild(bal);

		balData.push({
			el: bal,
			x: Math.random() * w,
			y: Math.random() * h,
			w: w_h,
			h: w_h,
			m_x: Math.random() * max_m_x + 1,
			m_y: Math.random() * max_m_y + 1,
		});
	}

	function move() {
		for (let i = 0; i < balData.length; i++) {
			let bal = balData[i];

			const bal_container = document.querySelectorAll("#mutual_02 .bal")[i];

			if (bal_container) {
				const off_w = bal_container.offsetWidth;
				const off_h = bal_container.offsetHeight;

				bal.x += bal.m_x;
				bal.y += bal.m_y;

				if (bal.x < 0) {
					bal.m_x *= -1;
					bal.x = 0;
				} else if (bal.x >= w - off_w) {
					bal.m_x *= -1;
					bal.x = w - off_w;
				} else if (bal.y < 0) {
					bal.m_y *= -1;
					bal.y = 0;
				} else if (bal.y >= h - off_h) {
					bal.m_y *= -1;
					bal.y = h - off_h;
				}
				bal.el.style.transform = `translate(${bal.x}px, ${bal.y}px)`;
				bal.el.style.backgroundColor = `hsl(${bal.x}, 100%, 50%)`;
			}
		}
		requestAnimationFrame(move);
	}
	requestAnimationFrame(move);
};

onMounted(init);
</script>

<template>
	<div class="container" id="mutual_02"></div>
</template>

<style lang="scss" scoped>
#mutual_02 {
	margin: 0;
	padding: 0;
	color: #fff;
	width: 100%;
	height: 300px;
	position: relative;

	:deep(.bal) {
		position: absolute;
		border-radius: 50%;
		background-color: #eeeeee;
		box-shadow: 0 0 3px 10px rgba(255, 255, 255, 0.5);
		transition: transform ease-in-out 10ms;
	}
}
</style>

掉落的卡牌,显示文字

Hello World~

<script setup>
import { onMounted } from "vue";

const init = () => {
	const max = 1000;
	for (let i = 0; i < max; i++) {
		const div_box = document.createDocumentFragment();
		const div = document.createElement("div");
		div.classList.add("box-item");
		div_box.appendChild(div);
		document.querySelector("#mutual_03 .box").appendChild(div_box);

		document.querySelectorAll("#mutual_03 .box-item")[i].addEventListener(
			"mouseover",
			function () {
				const duration = Math.random() * 2 + 1;
				this.classList.add("active");
				this.style.animationDuration = duration + "s";
			},
			false
		);
	}
};

onMounted(init);
</script>

<template>
	<div class="container" id="mutual_03">
		<h1>Hello World~</h1>
		<div class="box"></div>
	</div>
</template>

<style lang="scss" scoped>
#mutual_03 {
	margin: 0;
	padding: 0;
	background-color: #000;
	color: #fff;
	width: 100%;
	height: 300px;
	position: relative;
	display: flex;
	align-items: center;
	justify-content: center;
	overflow: hidden;
	.box {
		width: 100%;
		height: 100%;
		display: flex;
		flex-wrap: wrap;
		position: absolute;
		z-index: 10;
		:deep(.box-item) {
			width: 50px;
			height: 50px;
			background-color: #222;
			border: 1px solid #111;
			cursor: pointer;

			&.active {
				background-color: cadetblue;
				box-shadow: 0 0 10px cadetblue, 0 0 30px cadetblue, 0 0 50px cadetblue;
				animation: ani 2s linear forwards;
			}
		}
	}
}

@keyframes ani {
	from {
		transform: translateY(0) rotate(0deg);
	}
	to {
		transform: translateY(100vh) rotate(360deg);
	}
}
</style>

图片转阴影-box-shadow

image
点击按钮进行转换
图片转阴影
点击按钮会将左侧的图片转换成用‘box-shadow’渲染的容器,切记:只支持小图片, 大图片会导致系统崩溃
<script setup lang="ts">
import { ref } from "vue";
const image = ref(new URL(`../images/test.png`, import.meta.url).href);

const shadow = ref("");

const handlerResult = () => {
	const _image = new Image();
	_image.src = image.value;
	_image.crossOrigin = "anonymous";
	_image.onload = () => {
		const canvas = document.createElement("canvas");
		const ctx = canvas.getContext("2d");
		if (!ctx) return;
		canvas.width = _image.width;
		canvas.height = _image.height;
		ctx.drawImage(_image, 0, 0);
		const { data, width, height } = ctx.getImageData(
			0,
			0,
			_image.width,
			_image.height
		);
		shadow.value = renderShadow(data, width, height);
	};
};

const renderShadow = (
	data: Uint8ClampedArray,
	width: number,
	height: number
) => {
	const w = 1;
	let shadow: string[] = [];
	for (let i = 0; i < height; i++) {
		for (let j = 0; j < width; j++) {
			const d = (width * i + j) * 4;
			var r = data[0 + d];
			var g = data[1 + d];
			var b = data[2 + d];
			var a = data[3 + d];
			const color = `rgba(${r}, ${g}, ${b}, ${a === 255 ? 1 : a})`;
			shadow.push(`${j + w}px ${i * w}px ${color}`);
		}
	}
	return shadow.join(",");
};
</script>

<template>
	<div class="container">
		<img :src="image" alt="image" crossorigin="anonymous" />
		<button @click="handlerResult">将左侧图片转换</button>

		<div class="result">
			<div class="empty" v-if="!shadow">点击按钮进行转换</div>

			<div v-else></div>
		</div>
	</div>
</template>

<style lang="scss" scoped>
.container {
	display: flex;
	align-items: center;
	justify-content: space-around;
	img,
	.result {
		width: 100px;
		height: 100px;
		border-radius: 4px;
		border: 1px dashed #eee;
	}
	button {
		padding: 4px 8px;
		border-radius: 4px;
		border: 1px solid #ccc;
		cursor: pointer;
		background: #fff;
		&:hover {
			background: #eee;
		}
		&:active {
			background: #ddd;
		}
	}

	.result {
		.empty {
			width: 100%;
			height: 100%;
			font-size: 12px;
			display: flex;
			align-items: center;
			justify-content: center;
			text-decoration: underline;
		}

		div {
			width: 1px;
			height: 1px;
			box-shadow: v-bind(shadow);
		}
	}
}
</style>

引导组件

<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
const stepIndex = ref(0);
const domRect = ref();
const popupLayer = [
	{ content: ".box2", title: "第一步", subTitle: "选中第二个格子" },
	{ content: ".box1", title: "第二步", subTitle: "选中第一个格子" },
	{ content: "span", title: "第三步", subTitle: "选中文本Text" },
	{ content: "p", title: "最后一步", subTitle: "选中这个段落" },
];

const getEL = computed(() => {
	return popupLayer[stepIndex.value];
});

const initEl = () => {
	popupLayer.forEach(({ content }) => {
		const dom = document.querySelector(`.popup-box ${content}`);
		if (dom) {
			// @ts-ignore
			dom.style.position = "initial";
			// @ts-ignore
			dom.style.zIndex = 0;
		}
	});
};

const positionPopup = () => {
	// 获取当前元素
	const { content } = getEL.value;
	// 因为是在 vitepress , 所有定位去掉父级的元素信息
	const current = document.querySelector(".popup-box")?.parentElement;
	if (!current) return;
	const parentRect = current.getBoundingClientRect();

	initEl();

	// 当前的元素信息
	const dom = document.querySelector(`.popup-box ${content}`);
	if (!dom) return;
	// @ts-ignore
	dom.style.position = "relative";
	// @ts-ignore
	dom.style.zIndex = 12;
	const child = dom.getBoundingClientRect();

	// 20 是父及的间距
	domRect.value = {
		left: child.left - parentRect.left - 20,
		top: child.top - parentRect.top - 20,
		width: child.width,
		height: child.height,
	};
};

const onStep = (type: "pre" | "next" | "finish") => {
	if (type === "pre") {
		stepIndex.value--;
		positionPopup();
	} else if (type === "next") {
		stepIndex.value++;
		positionPopup();
	} else if (type === "finish") {
		domRect.value = null;
		stepIndex.value = 0;
	}
};

onMounted(positionPopup);
</script>

<template>
	<div class="popup-box">
		<div class="container">
			<div class="box box1">第一个格子</div>
			<div class="box box2">第二个格子</div>
			<div class="box box3">第三个格子</div>
			<div class="box box4">第四个格子</div>
			<div class="box box5">第五个格子</div>
		</div>
		<p>
			这是一个有趣的事情...

			<span>Text</span>
		</p>

		<div class="popup" v-if="domRect && popupLayer.length > 0">
			<div
				class="checked-box"
				:style="{
					left: domRect.left + 'px',
					top: domRect.top + domRect.height + 6 + 'px',
				}"
			>
				<header class="title">{{ getEL.title }}</header>
				<div class="subTitle">{{ getEL.subTitle }}</div>
				<div class="handler">
					<button v-if="stepIndex !== 0" @click="onStep('pre')">上一步</button>
					<button
						v-if="stepIndex === popupLayer.length - 1"
						@click="onStep('finish')"
					>
						结束
					</button>

					<button v-else @click="onStep('next')">下一步</button>
				</div>
			</div>
		</div>
	</div>
</template>

<style lang="scss" scoped>
.popup-box {
	position: relative;
	width: 100%;
	height: 500px;
	.container {
		padding-top: 40px;
		display: flex;
		align-items: center;

		.box {
			width: 100px;
			height: 40px;
			border: 1px solid #ccc;
			margin: 0 10px;
			display: flex;
			align-items: center;
			justify-content: center;
			background-image: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%);
			color: #fff;
		}
	}

	p {
		margin-top: 20px;
		padding: 6px 10px;
		background-image: linear-gradient(120deg, #e0c3fc 0%, #8ec5fc 100%);
		span {
			padding: 4px 10px;
			background-image: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
		}
	}
	.popup {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		&::before {
			content: "";
			display: block;
			position: absolute;
			inset: 0;
			background-color: rgba($color: #000000, $alpha: 0.5);
		}

		.checked-box {
			min-width: 150px;
			position: absolute;
			z-index: 110;
			padding: 10px 14px;
			border-radius: 4px;
			z-index: 1;
			background-color: #fff;
			box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2);
			transition: all 0.3s cubic-bezier(0.19, 1, 0.22, 1);
			&::before {
				content: "";
				display: block;
				position: absolute;
				top: -4px;
				left: 30%;
				transform: translateX(-30%);
				width: 0;
				height: 0;
				border-left: 4px solid transparent;
				border-right: 4px solid transparent;
				border-bottom: 4px solid #fff;
				z-index: 1;
			}
			.title {
				font-weight: bold;
				margin-bottom: 4px;
				border-bottom: 1px dashed #eee;
			}

			.subTitle {
				font-size: 12px;
			}
			.handler {
				border-top: 1px dashed #eee;
				padding: 4px 0;
				display: flex;
				align-items: center;
				justify-content: space-between;
				margin-top: 10x;
				button {
					font-size: 12px;
					padding: 1px 4px;
					background-color: #8ec5fc;
					color: #fff;
					border-radius: 4px;
					border: 1px solid #8ec5fc;
				}
			}
		}
	}
}
</style>

wangxiaoze | MIT License.