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
点击按钮进行转换
图片转阴影
点击按钮会将左侧的图片转换成用‘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>
引导组件
第一个格子
第二个格子
第三个格子
第四个格子
第五个格子
这是一个有趣的事情... Text
<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>