vue3雪花代码
成果图:
视频:
2024-11-06 16-33-32
一,首先引入 JParticles
npm install jparticles --save
二,去阿里巴巴矢量图库下载雪花图片
多下载几种不同的雪花图片,效果会更好看些
下载完成之后我是用的七牛云的对象存储:对象存储 Kodo_云存储_海量安全高可靠云存储_oss - 七牛云
把图片上传到这个里面,然后生成临时访问的链接就行了:雪花 (3).png (200×200) (clouddn.com)也可以直接放到项目本地
三,完整代码
<template>
<div ref="snowfallContainer" class="snowfall-container"></div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import JParticles from "jparticles";
const snowfallContainer = ref(null);
onMounted(() => {
if (snowfallContainer.value) {
new JParticles.Snow(snowfallContainer.value, {
// JParticles 的雪花配置项
count: 100, // 数量
color: "#FFF", // 颜色
minSize: 1, // 最小尺寸
maxSize: 5, // 最大尺寸
shape: [
"http://sle6k7baz.hd-bkt.clouddn.com/ax/other/%E9%9B%AA%E8%8A%B1%20%283%29.png?e=1730965992&token=Fj4JTDTCX_isNmsjhWmTjkWWt-75_zWjjYIAv8Gq:CMOH2B52ah2hiyXSZXuO4gefZBQ=",
"http://sle6k7baz.hd-bkt.clouddn.com/ax/other/%E9%9B%AA%E8%8A%B1%20%282%29.png?e=1730965785&token=Fj4JTDTCX_isNmsjhWmTjkWWt-75_zWjjYIAv8Gq:h1ZJa-5GNWzorFLhI6THAehfQLM=",
"http://sle6k7baz.hd-bkt.clouddn.com/ax/other/%E9%9B%AA%E8%8A%B1%20%281%29.png?e=1730965715&token=Fj4JTDTCX_isNmsjhWmTjkWWt-75_zWjjYIAv8Gq:UdqK5uMD6NR6obkcchTjaCGO0xM=",
"http://sle6k7baz.hd-bkt.clouddn.com/ax/other/%E9%9B%AA%E8%8A%B1.png?e=1730965649&token=Fj4JTDTCX_isNmsjhWmTjkWWt-75_zWjjYIAv8Gq:5wdfCDrRzr3yYJC1TNo0bPxP7Q4=",
],//雪花图片地址,我这个链接一天后失效,请换成自己的图片链接
});
}
});
</script>
<style scoped>
.snowfall-container {
width: 100%;
height: 100%;
background-image: url("http://sle6k7baz.hd-bkt.clouddn.com/ax/other/snow.jpg?e=1730967250&token=Fj4JTDTCX_isNmsjhWmTjkWWt-75_zWjjYIAv8Gq:HQEG7JqMyx7E3K9UnTNercJO6p8=");
background-size: cover; /* 覆盖整个容器,可能会裁剪图片 */
background-position: center; /* 居中背景图片 */
background-repeat: no-repeat; /* 不重复背景图片 */
}
</style>
这样的话,咱们就是能实现上面展示的雪花的一个动态效果,上面我的图片地址一天后会失效,如果没有显示雪花的话请把图片地址换成自己的。
=======================================
后话
代码优化
昨天本来想把这个部署到服务器上,结果七牛云生成的链接是http协议的,我的网站是https协议的,导致图片加载不出来,本来想着那我就去配置一下ssl,结果这玩意竟然不能使用阿里云申请的ssl证书,oh my god,说实话不是很好用,貌似只能申请七牛云自己的ssl,我想着那我申请一个吧,结果这玩意还要填公司信息,还要填一个座机号,太麻烦了。
一怒之下我怒了一下,然后直接不用了,换成ftp把文件存服务器上
昨天那个代码我顺便优化了一下,把抖音上那个很火的爱心也加进去了
这是优化后的代码运行图及在线地址:AX
雪花和爱心都是动态的,视频我就不录制了,大家拿源码跑一下就行:
<template>
<div ref="snowfallContainer" class="snowfall-container" style="position:relative">
<div class="heart-container">
<canvas ref="heartCanvas"></canvas>
</div>
</div>
<div style="position: fixed ; width: 500px; height: 500px; top:0">
<div
style="
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
"
>
<span :style="{ color: textColor }">
{{ text }}
</span>
</div>
<div style="position: absolute; height: 100%; width: 100%; top: 0; left: 0">
<ButtonGroup style="left: 4px; top: 4px">
</ButtonGroup>
<canvas
id="pinkboard"
style="position: relative"
ref="pinkboard"
@click="changeText"
></canvas>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, watch ,reactive } from "vue";
import JParticles from "jparticles";
const snowfallContainer = ref(null);
const text = ref("XJX");
const textColor = ref("#ea80b0");
const defaultTextColors = ref([
"#ea80b0",
"#2D8CF0",
"#FF9900",
"#19C919",
"#9B1DEA",
"#EA4CA3",
]);
const defaultHeartColors = ref([
"#ea80b0",
"#2D8CF0",
"#FF9900",
"#19C919",
"#9B1DEA",
"#EA4CA3",
]);
const drawerValue = ref(false);
const heartColor = ref("#ea80b0");
const pinkboard = ref(null);
let canvasEl;
onMounted(() => {
// canvasEl=document.getElementById('pinkboard')
canvasEl = pinkboard.value;
puttingItAll(canvasEl);
if (snowfallContainer.value) {
new JParticles.Snow(snowfallContainer.value, {
// JParticles 的雪花配置项
count: 100, // 数量
color: "#FFF", // 颜色
minSize: 1, // 最小尺寸
maxSize: 5, // 最大尺寸
shape: [
"https://wqjay.cn/api/axImg/getImgById/26e3657c6fe4ca96346b97aa0a1a3d51",
"https://wqjay.cn/api/axImg/getImgById/2147acf9c77a397ed357ed7d5412ef4f",
"https://wqjay.cn/api/axImg/getImgById/45266fd99bc611e3a3ba260744285052",
"https://wqjay.cn/api/axImg/getImgById/bdc2e68f13260ee24f8fb2c7ba5ebcf2",
],
});
}
});
watch(heartColor, () => {
puttingItAll(canvasEl, heartColor);
});
function getRndInteger(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
const textArr = ["许佳欣", "xjx", "XJX"];
let randomOld = 0;
function changeText() {
let random = getRndInteger(0, textArr.length - 1);
if (random == randomOld && random == textArr.length - 1) {
random = random - 1;
} else if (random == randomOld) {
random = random + 1;
}
randomOld = random;
text.value = textArr[random];
}
/*
* Settings
*/
var settings = {
particles: {
length: 500, // maximum amount of particles
duration: 2, // particle duration in sec
velocity: 100, // particle velocity in pixels/sec
effect: -0.75, // play with this for a nice effect
size: 30, // particle size in pixels
},
};
/*
* RequestAnimationFrame polyfill by Erik Möller
*/
(function () {
var b = 0;
var c = ["ms", "moz", "webkit", "o"];
for (var a = 0; a < c.length && !window.requestAnimationFrame; ++a) {
window.requestAnimationFrame = window[c[a] + "RequestAnimationFrame"];
window.cancelAnimationFrame =
window[c[a] + "CancelAnimationFrame"] ||
window[c[a] + "CancelRequestAnimationFrame"];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function (h, e) {
var d = new Date().getTime();
var f = Math.max(0, 16 - (d - b));
var g = window.setTimeout(function () {
h(d + f);
}, f);
b = d + f;
return g;
};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function (d) {
clearTimeout(d);
};
}
})();
/*
* Point class
*/
var Point = (function () {
function Point(x, y) {
this.x = typeof x !== "undefined" ? x : 0;
this.y = typeof y !== "undefined" ? y : 0;
}
Point.prototype.clone = function () {
return new Point(this.x, this.y);
};
Point.prototype.length = function (length) {
if (typeof length == "undefined") return Math.sqrt(this.x * this.x + this.y * this.y);
this.normalize();
this.x *= length;
this.y *= length;
return this;
};
Point.prototype.normalize = function () {
var length = this.length();
this.x /= length;
this.y /= length;
return this;
};
return Point;
})();
/*
* Particle class
*/
var Particle = (function () {
function Particle() {
this.position = new Point();
this.velocity = new Point();
this.acceleration = new Point();
this.age = 0;
}
Particle.prototype.initialize = function (x, y, dx, dy) {
this.position.x = x;
this.position.y = y;
this.velocity.x = dx;
this.velocity.y = dy;
this.acceleration.x = dx * settings.particles.effect;
this.acceleration.y = dy * settings.particles.effect;
this.age = 0;
};
Particle.prototype.update = function (deltaTime) {
this.position.x += this.velocity.x * deltaTime;
this.position.y += this.velocity.y * deltaTime;
this.velocity.x += this.acceleration.x * deltaTime;
this.velocity.y += this.acceleration.y * deltaTime;
this.age += deltaTime;
};
Particle.prototype.draw = function (context, image) {
function ease(t) {
return --t * t * t + 1;
}
var size = image.width * ease(this.age / settings.particles.duration);
context.globalAlpha = 1 - this.age / settings.particles.duration;
context.drawImage(
image,
this.position.x - size / 2,
this.position.y - size / 2,
size,
size
);
};
return Particle;
})();
/*
* ParticlePool class
*/
var ParticlePool = (function () {
var particles,
firstActive = 0,
firstFree = 0,
duration = settings.particles.duration;
function ParticlePool(length) {
// create and populate particle pool
particles = new Array(length);
for (var i = 0; i < particles.length; i++) particles[i] = new Particle();
}
ParticlePool.prototype.add = function (x, y, dx, dy) {
particles[firstFree].initialize(x, y, dx, dy);
// handle circular queue
firstFree++;
if (firstFree == particles.length) firstFree = 0;
if (firstActive == firstFree) firstActive++;
if (firstActive == particles.length) firstActive = 0;
};
ParticlePool.prototype.update = function (deltaTime) {
let i;
// update active particles
if (firstActive < firstFree) {
for (i = firstActive; i < firstFree; i++) particles[i].update(deltaTime);
}
if (firstFree < firstActive) {
for (i = firstActive; i < particles.length; i++) particles[i].update(deltaTime);
for (i = 0; i < firstFree; i++) particles[i].update(deltaTime);
}
// remove inactive particles
while (particles[firstActive].age >= duration && firstActive != firstFree) {
firstActive++;
if (firstActive == particles.length) firstActive = 0;
}
};
ParticlePool.prototype.draw = function (context, image) {
let i;
// draw active particles
if (firstActive < firstFree) {
for (i = firstActive; i < firstFree; i++) particles[i].draw(context, image);
}
if (firstFree < firstActive) {
for (i = firstActive; i < particles.length; i++) particles[i].draw(context, image);
for (i = 0; i < firstFree; i++) particles[i].draw(context, image);
}
};
return ParticlePool;
})();
/*
* Putting it all together
*/
function puttingItAll(canvas, heartColor) {
var context = canvas.getContext("2d"),
particles = new ParticlePool(settings.particles.length),
particleRate = settings.particles.length / settings.particles.duration, // particles/sec
time;
// get point on heart with -PI <= t <= PI
function pointOnHeart(t) {
return new Point(
160 * Math.pow(Math.sin(t), 3),
130 * Math.cos(t) -
50 * Math.cos(2 * t) -
20 * Math.cos(3 * t) -
10 * Math.cos(4 * t) +
25
);
}
// creating the particle image using a dummy canvas
var image = (function () {
var canvas = document.createElement("canvas"),
context = canvas.getContext("2d");
canvas.width = settings.particles.size;
canvas.height = settings.particles.size;
// helper function to create the path
function to(t) {
var point = pointOnHeart(t);
point.x = settings.particles.size / 2 + (point.x * settings.particles.size) / 350;
point.y = settings.particles.size / 2 - (point.y * settings.particles.size) / 350;
return point;
}
// create the path
context.beginPath();
var t = -Math.PI;
var point = to(t);
context.moveTo(point.x, point.y);
while (t < Math.PI) {
t += 0.01; // baby steps!
point = to(t);
context.lineTo(point.x, point.y);
}
context.closePath();
// create the fill
context.fillStyle = heartColor ? heartColor.value : "#ea80b0";
context.fill();
// create the image
var image = new Image();
image.src = canvas.toDataURL();
return image;
})();
// render that thing!
function render() {
// next animation frame
requestAnimationFrame(render);
// update time
var newTime = new Date().getTime() / 1000,
deltaTime = newTime - (time || newTime);
time = newTime;
// clear canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// create new particles
var amount = particleRate * deltaTime;
for (var i = 0; i < amount; i++) {
var pos = pointOnHeart(Math.PI - 2 * Math.PI * Math.random());
var dir = pos.clone().length(settings.particles.velocity);
particles.add(canvas.width / 2 + pos.x, canvas.height / 2 - pos.y, dir.x, -dir.y);
}
// update and draw particles
particles.update(deltaTime);
particles.draw(context, image);
}
// handle (re-)sizing of the canvas
function onResize() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
window.onresize = onResize;
// delay rendering bootstrap
setTimeout(function () {
onResize();
render();
}, 10);
}
</script>
<style scoped>
html,
body {
height: 100%;
padding: 0;
margin: 0;
background: #000;
}
.snowfall-container {
width: 100%;
height: 100%;
background-image: url("https://wqjay.cn/api/axImg/getImgById/280b3c2eb17061a5f230c061630047e6");
background-size: cover; /* 覆盖整个容器,可能会裁剪图片 */
background-position: center; /* 居中背景图片 */
background-repeat: no-repeat; /* 不重复背景图片 */
}
.heart-container {
position: relative;
width: 100%;
height: 100%;
}
canvas {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
</style>
ftp工具类
顺便把ftp工具类也给到大家:
package com.wq.ax.utils;
import com.wq.ax.common.exception.CustomException;
import com.wq.ax.common.exception.ErrorCode;
import jakarta.servlet.http.HttpServletResponse;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
public class UploadUtil {
//本地测试可以用服务器ip,就填192.168.1.1这种ip地址就行
//假如我的ftp在服务器180.101.50.188上,而且我们的后端代码也部署在180.101.50.188,也就是同一台服务器上
//那我们这里得用localhost,否则连不上
private final static String server = "localhost";//ip地址
private final static Integer port = 21;//端口
private final static String username = "";//用户名
private final static String password = "";//密码
private final static String homePath = "/www/wwwroot/ftp_wq";//ftp文件夹所在目录
/**
* 上传文件到指定路径
*
* @param multipartFile 文件
* @param dirPath 上传地址
* @return
* @throws Exception
*/
public static String upload(MultipartFile multipartFile, String dirPath, Boolean masterDrawing) throws Exception {
// 获取文件名
String fileName = multipartFile.getOriginalFilename();
if (fileName == null || fileName.isEmpty()) {
throw new IllegalArgumentException("文件名不能为空");
}
// 创建FTPClient实例
FTPClient ftpClient = new FTPClient();
try {
// 连接到服务器
ftpClient.connect(server, port);
// 登录
ftpClient.login(username, password); // 使用你的FTP用户名和密码
// 设置文件类型为二进制(对于图片等文件)
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
// 设置编码为UTF-8
ftpClient.setControlEncoding("UTF-8");
// 转到目标目录,如果目录不存在则创建
ftpClient.changeWorkingDirectory(dirPath);
if (!ftpClient.changeWorkingDirectory(dirPath)) {
// 创建目录
if (!ftpClient.makeDirectory(dirPath)) {
throw new CustomException(ErrorCode.SERVER_ERROR, "创建目录失败");
}
// 切换到新创建的目录
ftpClient.changeWorkingDirectory(dirPath);
}
// 检查文件是否存在
// String[] fileNames = ftpClient.listNames();
// boolean fileExists = false;
// for (String name : fileNames) {
// if (name.equals(fileName)) {
// fileExists = true;
// break;
// }
// }
//
// if (fileExists) {
// // 生成随机文件名
// String randomFileName = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + "." + FilenameUtils.getExtension(fileName);
// fileName = randomFileName;
// }
String substring = fileName.substring(fileName.lastIndexOf('.') + 1);
// 生成随机文件名
String randomFileName = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + "." + substring;
fileName = randomFileName;
// 根据是否上传原图去判断要不要调用压缩方法
InputStream inputStream;
if (masterDrawing) {
inputStream = multipartFile.getInputStream();
} else {
inputStream = compressImage(multipartFile);
}
// 将文件保存到服务器
boolean uploaded = ftpClient.storeFile(fileName, inputStream);
inputStream.close();
if (!uploaded) {
throw new CustomException(ErrorCode.SERVER_ERROR, "文件上传失败");
}
// 返回文件上传后的完整路径
return dirPath + "/" + fileName;
} finally {
// 关闭连接
if (ftpClient.isConnected()) {
ftpClient.logout();
ftpClient.disconnect();
}
}
}
/**
* 移动文件到指定路径
* @param fromPath 原路径
* @param toPath 新路径
* @return
* @throws Exception
*/
public static String remove(String fromPath, String toPath) throws Exception {
// 创建 FTPClient 实例
FTPClient ftpClient = new FTPClient();
try {
// 连接到 FTP 服务器
ftpClient.connect(server, port);
// 登录 FTP 服务器
ftpClient.login(username, password);
// 设置文件传输类型为二进制
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
// 设置被动模式
ftpClient.enterLocalPassiveMode();
String lastPathComponent = getLastPathComponent(fromPath);
// 移动文件
boolean success = ftpClient.rename(fromPath, toPath+lastPathComponent);
if (!success) {
throw new IOException("文件移动失败");
} else {
return toPath+lastPathComponent;
}
} finally {
// 关闭连接
if (ftpClient.isConnected()) {
ftpClient.logout();
ftpClient.disconnect();
}
}
}
/**
* 根据id获取文件预览流
* @param response
* @param filename 文件完整地址
* @throws IOException
*/
public static void getFile(HttpServletResponse response, String filename) throws IOException {
// 配置FTP客户端
FTPClient ftpClient = new FTPClient();
ftpClient.setControlEncoding("UTF-8");
ftpClient.connect(server, port);
ftpClient.login(username, password);
// 设置文件类型为二进制
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
try (InputStream inputStream = ftpClient.retrieveFileStream(filename)) {
// 设置输出的格式
response.reset();
// 设置Content-Type为图片类型
response.setContentType("image/jpeg"); // 或者根据文件格式设置为 image/png 等
response.setHeader("Content-Disposition", "inline; filename=\"" + getLastPart(filename) + "\"");
// 循环取出流中的数据
byte[] b = new byte[1024];
int len;
while ((len = inputStream.read(b)) > 0) {
response.getOutputStream().write(b, 0, len);
}
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
// 返回错误状态码和消息
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write("An error occurred while retrieving the file.");
response.getWriter().flush();
} finally {
try {
if (ftpClient.isConnected()) {
ftpClient.logout();
ftpClient.disconnect();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static String getFileUrl(String filename){
return null;
}
/**
* 删除文件
* @param filePath 文件完整地址
* @return
* @throws Exception
*/
public static boolean deleteFile(String filePath) throws Exception {
FTPClient ftpClient = new FTPClient();
try {
// 连接到FTP服务器
ftpClient.connect(server, port);
// 登录到FTP服务器
ftpClient.login(username, password);
// 设置文件类型为二进制,以确保文件内容不会改变
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
// 设置编码为UTF-8,防止中文乱码
ftpClient.setControlEncoding("UTF-8");
// 转换文件路径编码,防止中文乱码
String remoteFilePath = new String(filePath.getBytes("UTF-8"), "ISO-8859-1");
// 删除文件
return ftpClient.deleteFile(remoteFilePath);
} finally {
// 关闭FTP连接
if (ftpClient.isConnected()) {
ftpClient.logout();
ftpClient.disconnect();
}
}
}
public static String getLastPart(String str) {
if (str == null || str.isEmpty()) {
return "";
}
int lastSlashIndex = str.lastIndexOf('/');
if (lastSlashIndex == -1) {
return str; // 如果没有找到'/',返回整个字符串
}
return str.substring(lastSlashIndex + 1); // 返回最后一个'/'后面的内容
}
/**
* 使用Thumbnailator压缩图片
*/
private static ByteArrayInputStream compressImage(MultipartFile file) throws IOException {
// 读取图片文件
BufferedImage image = ImageIO.read(file.getInputStream());
String contentType = file.getContentType();
String outputFormat = "jpg"; // 默认格式
if (contentType != null) {
if (contentType.equalsIgnoreCase("image/png")) {
outputFormat = "png";
} else if (contentType.equalsIgnoreCase("image/gif")) {
outputFormat = "gif";
}
// 可以根据需要添加更多格式
}
// 使用Thumbnailator压缩图片
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Thumbnails.of(image)
.scale(1.0) // 保持原始尺寸
.outputQuality(0.75) // 设置压缩质量
.outputFormat(outputFormat)
.toOutputStream(outputStream);
// 将压缩后的图片转换为InputStream
return new ByteArrayInputStream(outputStream.toByteArray());
}
public static String getLastPathComponent(String path) {
// 找到最后一个'/'的位置
int lastSlashIndex = path.lastIndexOf('/');
// 如果路径以'/'结尾,或者没有找到'/',则返回整个路径
if (lastSlashIndex == path.length() - 1 || lastSlashIndex == -1) {
return path;
}
// 返回最后一个'/'后面的字符串
return path.substring(lastSlashIndex + 1);
}
}