参考文章:Hexo页脚养鱼效果

  1. 引入必须的jquery文件

  2. 把上述文件保存在\themes\butterfly\source\js并命名为jquery.min.jsfish.js

  3. \themes\butterfly\source\js路径下新建fish.js文件,代码内容如下:

      1fish();
      2function fish() {
      3  return (
      4    $("footer").append(
      5      '<div class="fish_container" id="jsi-flying-fish-container"></div>'
      6    ),
      7    $(".fish_container").css({
      8      "z-index": -1,
      9      width: "100%",
     10      height: "160px",
     11      margin: 0,
     12      padding: 0,
     13    }),
     14    $("#footer-wrap").css({
     15      position: "absolute",
     16      "text-align": "center",
     17      top: 0,
     18      right: 0,
     19      left: 0,
     20      bottom: 0,
     21    }),
     22    this
     23  );
     24}
     25var RENDERER = {
     26	POINT_INTERVAL : 5,
     27	FISH_COUNT : 3,
     28	MAX_INTERVAL_COUNT : 50,
     29	INIT_HEIGHT_RATE : 0.5,
     30	THRESHOLD : 50,
     31
     32	init : function(){
     33		this.setParameters();
     34		this.reconstructMethods();
     35		this.setup();
     36		this.bindEvent();
     37		this.render();
     38	},
     39	setParameters : function(){
     40		this.$window = $(window);
     41		this.$container = $('#jsi-flying-fish-container');
     42		this.$canvas = $('<canvas />');
     43		this.context = this.$canvas.appendTo(this.$container).get(0).getContext('2d');
     44		this.points = [];
     45		this.fishes = [];
     46		this.watchIds = [];
     47	},
     48	createSurfacePoints : function(){
     49		var count = Math.round(this.width / this.POINT_INTERVAL);
     50		this.pointInterval = this.width / (count - 1);
     51		this.points.push(new SURFACE_POINT(this, 0));
     52
     53		for(var i = 1; i < count; i++){
     54			var point = new SURFACE_POINT(this, i * this.pointInterval),
     55				previous = this.points[i - 1];
     56
     57			point.setPreviousPoint(previous);
     58			previous.setNextPoint(point);
     59			this.points.push(point);
     60		}
     61	},
     62	reconstructMethods : function(){
     63		this.watchWindowSize = this.watchWindowSize.bind(this);
     64		this.jdugeToStopResize = this.jdugeToStopResize.bind(this);
     65		this.startEpicenter = this.startEpicenter.bind(this);
     66		this.moveEpicenter = this.moveEpicenter.bind(this);
     67		this.reverseVertical = this.reverseVertical.bind(this);
     68		this.render = this.render.bind(this);
     69	},
     70	setup : function(){
     71		this.points.length = 0;
     72		this.fishes.length = 0;
     73		this.watchIds.length = 0;
     74		this.intervalCount = this.MAX_INTERVAL_COUNT;
     75		this.width = this.$container.width();
     76		this.height = this.$container.height();
     77		this.fishCount = this.FISH_COUNT * this.width / 500 * this.height / 500;
     78		this.$canvas.attr({width : this.width, height : this.height});
     79		this.reverse = false;
     80
     81		this.fishes.push(new FISH(this));
     82		this.createSurfacePoints();
     83	},
     84	watchWindowSize : function(){
     85		this.clearTimer();
     86		this.tmpWidth = this.$window.width();
     87		this.tmpHeight = this.$window.height();
     88		this.watchIds.push(setTimeout(this.jdugeToStopResize, this.WATCH_INTERVAL));
     89	},
     90	clearTimer : function(){
     91		while(this.watchIds.length > 0){
     92			clearTimeout(this.watchIds.pop());
     93		}
     94	},
     95	jdugeToStopResize : function(){
     96		var width = this.$window.width(),
     97			height = this.$window.height(),
     98			stopped = (width == this.tmpWidth && height == this.tmpHeight);
     99
    100		this.tmpWidth = width;
    101		this.tmpHeight = height;
    102
    103		if(stopped){
    104			this.setup();
    105		}
    106	},
    107	bindEvent : function(){
    108		this.$window.on('resize', this.watchWindowSize);
    109		this.$container.on('mouseenter', this.startEpicenter);
    110		this.$container.on('mousemove', this.moveEpicenter);
    111		this.$container.on('click', this.reverseVertical);
    112	},
    113	getAxis : function(event){
    114		var offset = this.$container.offset();
    115
    116		return {
    117			x : event.clientX - offset.left + this.$window.scrollLeft(),
    118			y : event.clientY - offset.top + this.$window.scrollTop()
    119		};
    120	},
    121	startEpicenter : function(event){
    122		this.axis = this.getAxis(event);
    123	},
    124	moveEpicenter : function(event){
    125		var axis = this.getAxis(event);
    126
    127		if(!this.axis){
    128			this.axis = axis;
    129		}
    130		this.generateEpicenter(axis.x, axis.y, axis.y - this.axis.y);
    131		this.axis = axis;
    132	},
    133	generateEpicenter : function(x, y, velocity){
    134		if(y < this.height / 2 - this.THRESHOLD || y > this.height / 2 + this.THRESHOLD){
    135			return;
    136		}
    137		var index = Math.round(x / this.pointInterval);
    138
    139		if(index < 0 || index >= this.points.length){
    140			return;
    141		}
    142		this.points[index].interfere(y, velocity);
    143	},
    144	reverseVertical : function(){
    145		this.reverse = !this.reverse;
    146
    147		for(var i = 0, count = this.fishes.length; i < count; i++){
    148			this.fishes[i].reverseVertical();
    149		}
    150	},
    151	controlStatus : function(){
    152		for(var i = 0, count = this.points.length; i < count; i++){
    153			this.points[i].updateSelf();
    154		}
    155		for(var i = 0, count = this.points.length; i < count; i++){
    156			this.points[i].updateNeighbors();
    157		}
    158		if(this.fishes.length < this.fishCount){
    159			if(--this.intervalCount == 0){
    160				this.intervalCount = this.MAX_INTERVAL_COUNT;
    161				this.fishes.push(new FISH(this));
    162			}
    163		}
    164	},
    165	render : function(){
    166		requestAnimationFrame(this.render);
    167		this.controlStatus();
    168		this.context.clearRect(0, 0, this.width, this.height);
    169		this.context.fillStyle = 'hsl(0, 0%, 95%)';
    170
    171		for(var i = 0, count = this.fishes.length; i < count; i++){
    172			this.fishes[i].render(this.context);
    173		}
    174		this.context.save();
    175		this.context.globalCompositeOperation = 'xor';
    176		this.context.beginPath();
    177		this.context.moveTo(0, this.reverse ? 0 : this.height);
    178
    179		for(var i = 0, count = this.points.length; i < count; i++){
    180			this.points[i].render(this.context);
    181		}
    182		this.context.lineTo(this.width, this.reverse ? 0 : this.height);
    183		this.context.closePath();
    184		this.context.fill();
    185		this.context.restore();
    186	}
    187};
    188var SURFACE_POINT = function(renderer, x){
    189	this.renderer = renderer;
    190	this.x = x;
    191	this.init();
    192};
    193SURFACE_POINT.prototype = {
    194	SPRING_CONSTANT : 0.03,
    195	SPRING_FRICTION : 0.9,
    196	WAVE_SPREAD : 0.3,
    197	ACCELARATION_RATE : 0.01,
    198
    199	init : function(){
    200		this.initHeight = this.renderer.height * this.renderer.INIT_HEIGHT_RATE;
    201		this.height = this.initHeight;
    202		this.fy = 0;
    203		this.force = {previous : 0, next : 0};
    204	},
    205	setPreviousPoint : function(previous){
    206		this.previous = previous;
    207	},
    208	setNextPoint : function(next){
    209		this.next = next;
    210	},
    211	interfere : function(y, velocity){
    212		this.fy = this.renderer.height * this.ACCELARATION_RATE * ((this.renderer.height - this.height - y) >= 0 ? -1 : 1) * Math.abs(velocity);
    213	},
    214	updateSelf : function(){
    215		this.fy += this.SPRING_CONSTANT * (this.initHeight - this.height);
    216		this.fy *= this.SPRING_FRICTION;
    217		this.height += this.fy;
    218	},
    219	updateNeighbors : function(){
    220		if(this.previous){
    221			this.force.previous = this.WAVE_SPREAD * (this.height - this.previous.height);
    222		}
    223		if(this.next){
    224			this.force.next = this.WAVE_SPREAD * (this.height - this.next.height);
    225		}
    226	},
    227	render : function(context){
    228		if(this.previous){
    229			this.previous.height += this.force.previous;
    230			this.previous.fy += this.force.previous;
    231		}
    232		if(this.next){
    233			this.next.height += this.force.next;
    234			this.next.fy += this.force.next;
    235		}
    236		context.lineTo(this.x, this.renderer.height - this.height);
    237	}
    238};
    239var FISH = function(renderer){
    240	this.renderer = renderer;
    241	this.init();
    242};
    243FISH.prototype = {
    244	GRAVITY : 0.4,
    245
    246	init : function(){
    247		this.direction = Math.random() < 0.5;
    248		this.x = this.direction ? (this.renderer.width + this.renderer.THRESHOLD) : -this.renderer.THRESHOLD;
    249		this.previousY = this.y;
    250		this.vx = this.getRandomValue(4, 10) * (this.direction ? -1 : 1);
    251
    252		if(this.renderer.reverse){
    253			this.y = this.getRandomValue(this.renderer.height * 1 / 10, this.renderer.height * 4 / 10);
    254			this.vy = this.getRandomValue(2, 5);
    255			this.ay = this.getRandomValue(0.05, 0.2);
    256		}else{
    257			this.y = this.getRandomValue(this.renderer.height * 6 / 10, this.renderer.height * 9 / 10);
    258			this.vy = this.getRandomValue(-5, -2);
    259			this.ay = this.getRandomValue(-0.2, -0.05);
    260		}
    261		this.isOut = false;
    262		this.theta = 0;
    263		this.phi = 0;
    264	},
    265	getRandomValue : function(min, max){
    266		return min + (max - min) * Math.random();
    267	},
    268	reverseVertical : function(){
    269		this.isOut = !this.isOut;
    270		this.ay *= -1;
    271	},
    272	controlStatus : function(context){
    273		this.previousY = this.y;
    274		this.x += this.vx;
    275		this.y += this.vy;
    276		this.vy += this.ay;
    277
    278		if(this.renderer.reverse){
    279			if(this.y > this.renderer.height * this.renderer.INIT_HEIGHT_RATE){
    280				this.vy -= this.GRAVITY;
    281				this.isOut = true;
    282			}else{
    283				if(this.isOut){
    284					this.ay = this.getRandomValue(0.05, 0.2);
    285				}
    286				this.isOut = false;
    287			}
    288		}else{
    289			if(this.y < this.renderer.height * this.renderer.INIT_HEIGHT_RATE){
    290				this.vy += this.GRAVITY;
    291				this.isOut = true;
    292			}else{
    293				if(this.isOut){
    294					this.ay = this.getRandomValue(-0.2, -0.05);
    295				}
    296				this.isOut = false;
    297			}
    298		}
    299		if(!this.isOut){
    300			this.theta += Math.PI / 20;
    301			this.theta %= Math.PI * 2;
    302			this.phi += Math.PI / 30;
    303			this.phi %= Math.PI * 2;
    304		}
    305		this.renderer.generateEpicenter(this.x + (this.direction ? -1 : 1) * this.renderer.THRESHOLD, this.y, this.y - this.previousY);
    306
    307		if(this.vx > 0 && this.x > this.renderer.width + this.renderer.THRESHOLD || this.vx < 0 && this.x < -this.renderer.THRESHOLD){
    308			this.init();
    309		}
    310	},
    311	render : function(context){
    312		context.save();
    313		context.translate(this.x, this.y);
    314		context.rotate(Math.PI + Math.atan2(this.vy, this.vx));
    315		context.scale(1, this.direction ? 1 : -1);
    316		context.beginPath();
    317		context.moveTo(-30, 0);
    318		context.bezierCurveTo(-20, 15, 15, 10, 40, 0);
    319		context.bezierCurveTo(15, -10, -20, -15, -30, 0);
    320		context.fill();
    321
    322		context.save();
    323		context.translate(40, 0);
    324		context.scale(0.9 + 0.2 * Math.sin(this.theta), 1);
    325		context.beginPath();
    326		context.moveTo(0, 0);
    327		context.quadraticCurveTo(5, 10, 20, 8);
    328		context.quadraticCurveTo(12, 5, 10, 0);
    329		context.quadraticCurveTo(12, -5, 20, -8);
    330		context.quadraticCurveTo(5, -10, 0, 0);
    331		context.fill();
    332		context.restore();
    333
    334		context.save();
    335		context.translate(-3, 0);
    336		context.rotate((Math.PI / 3 + Math.PI / 10 * Math.sin(this.phi)) * (this.renderer.reverse ? -1 : 1));
    337
    338		context.beginPath();
    339
    340		if(this.renderer.reverse){
    341			context.moveTo(5, 0);
    342			context.bezierCurveTo(10, 10, 10, 30, 0, 40);
    343			context.bezierCurveTo(-12, 25, -8, 10, 0, 0);
    344		}else{
    345			context.moveTo(-5, 0);
    346			context.bezierCurveTo(-10, -10, -10, -30, 0, -40);
    347			context.bezierCurveTo(12, -25, 8, -10, 0, 0);
    348		}
    349		context.closePath();
    350		context.fill();
    351		context.restore();
    352		context.restore();
    353		this.controlStatus(context);
    354	}
    355};
    356$(function(){
    357	RENDERER.init();
    358});
    
  4. 在主题配置文件_config.butterfly.ymlinject,在bottom直接引入两个js文件 image-20230304111542538

  5. \themes\butterfly\source\css路径下创建一个footer.css文件

     1.fish_container{
     2  z-index: -1;
     3  width: "100%";
     4  height: "160px";
     5  margin: 0;
     6  padding: 0;
     7}
     8#footer-wrap{
     9  position: absolute;
    10  text-align: center;
    11  padding: 20px 20px;
    12  top: 0;
    13  right: 0;
    14  left: 0;
    15  bottom: 0;
    16}
    
  6. 设置透明度,在\themes\butterfly\source\css路径下创建一个fish_transparent.css文件

     1/* 页脚半透明 */
     2#footer {
     3    background: rgba(255, 255, 255, 0);
     4    color: #000;
     5    border-top-right-radius: 20px;
     6    border-top-left-radius: 20px;
     7    backdrop-filter: saturate(100%) blur(5px)
     8}
     9
    10#footer::before {
    11    background: rgba(255,255,255,0)
    12}
    13
    14#footer #footer-wrap {
    15    color: var(--font-color);
    16}
    17
    18#footer #footer-wrap a {
    19    color: var(--font-color);
    20}
    
    1/* 页脚透明 */
    2#footer {
    3    background: transparent !important;
    4}
    
  7. 在主题配置文件_config.butterfly.ymlinject,在head直接引入css文件

    1- <link rel="stylesheet" href="/css/footer.css">
    2- <link rel="stylesheet" href="/css/fish_transparent.css">
    
  8. 注意 HTML<script> 引入 jQuery 文件的顺序,要把 JQuery 库的引用放到第一个 <script> 引用前面,这样顺序执行后面的 js 文件才能识别。

  9. 修改小鱼的颜色,颜色值可以参考 HSL

    image-20230305111449907