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}
Step 6: Set Transparency for the Footer
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