Step 1: Introduce the Necessary jQuery File

First, you need to include the jQuery library. You can use the following CDN link:

1https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js

Step 2: Save the jQuery File

Save the above jQuery file in the \themes\butterfly\source\js directory and name it jquery.min.js.

Step 3: Create the fish.js File

In the same \themes\butterfly\source\js directory, create a new file named fish.js and add the following code:

  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 and 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});

Step 4: Modify the Theme Configuration

In the Butterfly theme configuration file _config.butterfly.yml, add the following lines under the inject section to include the JavaScript files:

1inject:
2  bottom:
3    - <script src="/js/jquery.min.js"></script>
4    - <script src="/js/fish.js"></script>

Step 5: Create the CSS File

Create a new CSS file named footer.css in the \themes\butterfly\source\css directory and add the following styles:

 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}

Create another CSS file named fish_transparent.css in the same directory and add the following styles to make the footer transparent:

 1/* Semi-transparent Footer */
 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/* Transparent Footer */
2#footer {
3    background: transparent !important;
4}

Step 7: Include the CSS Files in the Theme Configuration

In the Butterfly theme configuration file _config.butterfly.yml, add the following lines under the inject section to include the CSS files:

1inject:
2  head:
3    - <link rel="stylesheet" href="/css/footer.css">
4    - <link rel="stylesheet" href="/css/fish_transparent.css">

Step 8: Ensure Correct Order of Scripts

Make sure that the jQuery library is loaded before the fish.js script in your HTML file. The order of script inclusion is crucial for the correct functioning of the fish effect.

Step 9: Customize the Fish Color

You can customize the color of the fish by modifying the fillStyle property in the RENDERER object within the fish.js file. Use HSL color values to achieve the desired color.

1this.context.fillStyle = 'hsl(0, 0%, 95%)'; // Change this value to customize the fish color