A brief history of Torrents

Today, let’s talk about a decade old technology that drives a third of all the Internet Traffic.

The internet has grown a lot. High-speed data cables, 4G connectivity, wireless routers,etc. have eased our Online life quite a bit. In the midst of all these “connectivity augmentations”, torrents occupy an indelible place. For the average netaholic, a day without uTorrent is a day wasted. We need it for all kinds of software and media, especially the pirated ones. Torrent has become the brand-logo for Piracy in contemporary media industries. It owe this infamous title to torrent sharing websites like Kickass, PiratesBay, etc. While the topic of whether or not Kickass was right to do so is a whole new post on it’s own, we are going to study the origins of this technology and not one specific (but massive) implication.

Origin

File sharing has been around ever since ARPANET and maybe even before. Torrents were not the first of their kind. There were several other predecessors including the USENET most of which were very short-lived. For a detailed history of File Sharing before Torrents you can head over to this TorrentFreak post. Wikipedia also has a timeline of File Sharing and BitTorrent is quite a bit down the list. Files are nothing but streams of data that could be sent and received like any other packet of data over the internet. But what makes File Sharing harder than hosting a web page are these:

  1. Files usually contain more sensitive data than publicly visible web pages
  2. A typical file is usually larger than a typical website and therefore consume more time and data.
  3. The existing internet protocols at that time were suited for web pages with limited markups and variations but not so much for other files.

All these contributed to the need for a new protocol for file sharing. In 2001, programmer Bram Cohen (then, a student at the University at Buffalo, New York) rose to the cause and outlined a new protocol for faster and more reliable sharing of large files over the internet. He called it the BitTorrent Protocol (first released in April, 2001). He went on to make a client for this protocol which goes by the same name as the protocol. What makes it unique is the way in which it handles downloading of large files across the internet.

Predating the BitTorrent protocol, a proprietary file owned by a web admin was served only from the admin’s own server. This meant that whenever N users wanted to download the file, the server had to open N connections and serve individual instances of the file to all the requesters. Also, since only a single packet of data could be sent over a single connection, the download was linear. This posed 2 main problems:

  1. There was a lot of pressure on the central server and so it’s maintenance costs were high. That also meant that if the central server failed, the resource would become unavailable to all.
  2. The file download rate was limited to the server’s ability to handle the open connections simultaneously.

Existing solutions to the aforesaid problems either involved additional hardware or setting up additional paid, trusted servers across the world. The BitTorrent Protocol reformed this completely. Instead of centralizing file downloads, the idea was to decentralize file sharing.

Terminology

In order to understand how it worked, we first need to familiarize ourselves with some basic terminology.

Torrents

A file was divided into several pieces. The records for these pieces were stored in a separate, much smaller file (called a torrent file) along with additional meta-data about the file(like content length, file size, etc.). Optionally, it contained a list of trackers (described below).

Seeders

A seeder was a computer connected to the internet, having the torrent file, the BitTorrent client, and has downloaded the complete file contents. They acted as servers which served the whole file over the internet. A peer could then download the entire file or a piece of it from a connection to the seeder.

The central computer has the entire file and is acting as a seeder in the above figure.

Leechers

A leecher was a computer with the partial file contents. Suppose someone has downloaded 2 of 5 pieces of data over the internet. He could then relay this data to another peer while simultaneously continuing downloading the rest of the 3 pieces (maybe even from another leecher !).

Trackers

As you can already realize, there is a lot of confusion going on over this hypothetical file of ours. Someone is seeding it, someone is leeching it. How will your client know which system to connect to? All this fuss could be avoided if some systems agreed to keep track of the file sharing (i.e, status of the seeders and leechers). They came to be known as Trackers as they kept track of the seeders and leechers and directed new clients to make the appropriate connections for downloading a particular file. In most cases, the server acted as a tracker too.

Peers

The people who have the torrent file and are in the process of downloading it’s contents. The reason for inventing a separate term for them is this – A peer may or may not be a leecher. If someone opts to limit his upload speed to 0 Kb/s , he can still download the file contents but is no longer uploading any content to the swarm.

Swarm

An interconnected network of several seeders, peers and trackers sharing the same torrent is known as a swarm.

The BitTorrent Protocol

It worked like this:

  •  All computers having the BitTorrent Client and the Torrent file could share the file over the internet.
  • A seeder seeded the file or pieces of it to requesting clients
  • A leecher requested the pieces it needs while simultaneously (partially) seeding the pieces it has already downloaded.
  • Trackers kept track of the file transactions and connections. All clients were required to inform the Tracker of their activities and status.

This can be better illustrated by this animated GIF from Wikipedia.

torrentcomp_small

Animation of protocol use: The colored dots beneath each computer in the animation represent different parts of the file being shared. By the time a copy to a destination computer of each of those parts completes, a copy to another destination computer of that part (or other parts) is already taking place between users. The tracker (server) provides only a single copy of the file, and all the users clone its parts from one another.

Now, the file sharing was decentralized and the downloading of files became non-linear or non-sequential which meant somewhat faster downloads. Also, if the central server fails, the connected peers can keep downloading the file from a different source. The potential security loophole was that from the Man-in-the-middle-attack whereby, a malicious user would serve modified version of the file to the requesting client. But this was rectified soon by addition of cryptographic hashes for the file pieces in the Torrent file. Since the torrent file was downloaded from a trusted source, one can be sure that the file contents he fetched from another remote seeder/leecher were legit by just verifying their hashes.

Anatomy of a Torrent File

A torrent file contains the following UTF-8 encoded information:

  • announce—the URL of the tracker
  • info—this maps to a dictionary whose keys are dependent on whether one or more files are being shared:
    • files—a list of dictionaries each corresponding to a file (only when multiple files are being shared). Each dictionary has the following keys:
      • length—size of the file in bytes.
      • path—a list of strings corresponding to subdirectory names, the last of which is the actual file name
    • length—size of the file in bytes (only when one file is being shared)
    • name—suggested filename where the file is to be saved (if one file)/suggested directory name where the files are to be saved (if multiple files)
    • piece length—number of bytes per piece. This is commonly 28 KiB = 256 KiB = 262,144 B.
    • pieces—a hash list, i.e., a concatenation of each piece’s SHA-1 hash. As SHA-1 returns a 160-bit hash, pieces will be a string whose length is a multiple of 160-bits. If the torrent contains multiple files, the pieces are formed by concatenating the files in the order they appear in the files dictionary (i.e. all pieces in the torrent are the full piece length except for the last piece, which may be shorter).

Magnet Links

A newer, more widely accepted method of downloading torrent is the Magnet URI Scheme whereby, the cryptographic hash values are calculated by the client and not the server and are served via. a plain-text link only. This deprecates the need for a separate file to store the data while maintaining the security of file contents being shared. A typical magnet link is of the format:

magnet:?xt=urn:btih:&dn= and some additional parameters.

Read more about Magnet URI Scheme here.

Analysis of Torrent File health

The analysis of a torrent file’s health is a very good estimation of how good or bad a torrent is. For example, if the number of seeders is very less compared to the number of leechers, then the torrent will surely take a longer time to download. However, there might be faster seeding servers than leechers in a server and hence, the number of seeders is not a very accurate representation of torrent health. Even a smaller but faster group of seeders can guarantee better download speeds than a larger but slower group of seeders in the swarm. Hence, a better representation of a torrents health could be the ratio of the average number of bytes uploaded to the average number of bytes downloaded. This is known as the Seed Ratio of the torrent. As is clear the seed ratio is dynamic. When the seed ratio of a torrent becomes zero, an average of 0 bytes of the torrent is uploaded per second, which means, you can expect virtually no download speed for the torrent. Such torrents are known as Dead Torrents. It is however, possible to resurrect a dead torrent. A person who has the complete file and chooses to manually reseed the torrent is called a reseeder.

A list of the best Torrent Clients

  1. uTorrent
  2. BitTorrent
  3. Vuze
  4. BitComet

Decentralization of Torrent Sharing Platforms

There are several emerging projects in this new domain of File Sharing. A decentralized torrent sharing platform will ensure that there is no one person/community to point to.  Magnet Links was just the first step toward this direction. Some of the most notable projects are-

  1. Open Bay: The Open Bay platform lets you create a local copy of some of the most infamous torrent sharing websites like Kickass and ThePirateBay on your PC. This local copy can also serve the purpose of a tracker and a seeder.
  2.  RIVRAn Open-Source Torrent Search Engine. It lets you scrape torrents off some of Kickass, ThePirateBay, Isohunt, etc. It is a relatively newer project and the author wishes to make it a distributed search engine. Your contributions towards this project could be beneficial to all.
  3. TORWhile this project is not really related to the Torrents, it is an important part of the process. The Onion Router allows us to relay data from a different IP Address belonging to the Tor Network. This lets us stay anonymous while downloading data off the internet. Torrent sites are being blocked by the Government in several countries. Using the TOR Circuit, one can download torrent resources which are otherwise blocked in their respective countries.
  4. eMule ProjectAn offshoot of the earlier eDonkey Project with improved File-Sharing and a nice GUI support. Active since 2002.
  5. Equabit: A new project aimed at decentralization of torrent sharing platforms, which means, you share your part of the torrent database to the world wide web anonymously. This is also one of my own projects. Head over to the website’s Participation page to know more.

Further Reading

For a more technical study of Torrents and Analysis of Torrent as File-Sharing medium, you can refer to the following reads-

  1. BTWorld: towards observing the global BitTorrent file-sharing network 
  2. Daily BitTorrent Statistics ( from IKnowWhatYouDownload* )
  3. LimeWire – Wikipedia
  4. History of P2P Networks and File Sharing – WikiSpaces
  5. eDonkey Network

*This freaky website claims that it can record your download information over BitTorrent Protocol unless you are behind a VPN or other such services. Read more about it here at IFLScience!

If you have any queries, suggestions or comments on this topic please feel free to express the same in the comment section down below. I will surely answer them as soon as possible. Also, please do not forget to leave a rating for this post. If you like it, please share this post. Thank you for reading.

Advertisements

Automated Lamp With Arduino

Hey folks. In this post we are going to see how we can build an automated lamp with Arduino and a bluetooth module. So let’s get started!

Introduction

Arduino is a hobbyist’s right arm. This small piece of wonder comes bundled with just the right tools for any small-to-medium sized hobby project and all that for a reasonable price too. Now we are going to use the Arduino to automate the working of an LED lamp. Here, by “automate” , I do mean “automate” and so it should not require any human input once it is up and running. However, it also should not irrevocably interfere in any way with a human’s lifestyle. Therefore we first have to form a principle of working. For this project, I have chosen the following principles:

  1. The LED should light up when the amount of ambient light is low. It also should be powered by rechargeable batteries and not the mains so it can work even if there is a power outage.
  2. The LED should be capable of being turned off manually with convenience. For that, I have chosen to go with bluetooth to turn it on or off.

Now that we have the principles in place, let’s get down to building it.

Things we need for this project (Apparatus)

We require the following things for this project.

Note: You can use dot board to minify the circuit but it is advisable to use a bread board for realizing the circuit first. Also, I have used Arduino UNO in this project and so the instructions that follow are for Arduino UNO exclusively but just about any of the Arduinos should do.

  1. Bread board
  2. Jumper Wires
  3. HC-05 Bluetooth module
  4. LED light
  5. Resistors (220 ohm)
  6. LDR
  7. Arduino UNO R3 (usually comes with a cable)
  8. USB Charger ( preferable output of 5V 1A )

Once we have gathered all the supplies we can begin assembling the circuit.

The Circuit

The HC-05 bluetooth module looks like this:

hc05 bt module.png

If it does, then look on the back side bottom edge (the ends of the pins). If you find a “5V” written somewhere there then just go ahead and connect the corresponding pins to the Arduino inputs, i.e, 5V to the 5V, GND to the GND, Rx to Rx and Tx to Tx. However, as I have read in some online tutorials, if you find a “Vcc” written there, then the connections are as follows:

  • Vcc of HC-05 to 5V of Arduino
  • GND of HC-05 to GND of Arduino
  • Rx of HC-05 to Tx of Arduino
  • Tx of HC-05 to Rx of Arduino

Since we do have to connect the other components to the Arduino pins too, the circuit could be built like this:

circuit.png

Pardon me if there was some difficulty in understanding the circuit.

The component on the left of the breadboard in the diagram is the LDR and on the right is the LED. Note that you have to try to keep the LED and the LDR as far apart as possible. This is because the LDR (Light Dependent Resistor) roughly tells us about the amount of light it can sense around itself. If the light is low, the LED lights up. But if the LED is too close to the LDR, it will keep lighting up momentarily and turning off or just keep flickering as the LDR values will fluctuate. Also, try to turn the LDR head away from the direction of the LED. For a more visual demo of the circuit see below:

IMG20171030225651.jpg

Program the Arduino

The next step is to program the Arduino according to our needs. We need to capture the data from the LDR and the bluetooth module simultaneously. If the user instructed the arduino to keep the light off, then it stays off no matter what. Else, if the environmental light is low, turn the LED On. The program looks like this:

#include <SoftwareSerial.h>
#define LDRPin A0
SoftwareSerial btSerial(1,0);
int LDRValue = 0;
int key = 1;
char data = 0;
void setup(){
btSerial.begin(9600);
pinMode(13,OUTPUT);
}
void loop(){
if(btSerial.available()){
data = btSerial.read();
key = (int)data - 48;
}
LDRValue = analogRead(LDRPin);
if(LDRValue < 3 && key > 0){
digitalWrite(13,HIGH);
}else{
digitalWrite(13,LOW);
}
}

In writing the above code, I have assumed that you have arranged the circuit as was given in the diagram above. If not, the code may vary. After uploading the code, the HC-05 should blink twice or thrice as should the Arduino UNO. If you get an error saying that something like “avrdude: stk500_recv(): programmer is not responding” or “avrdude: stk500_getsync() attempt 1 of 10: not in sync: resp=0x0d“, then do not worry. Just disconnect the HC-05 pins and reconnect them and then retry uploading the code. It should compile and upload. After that, we are ready to fire it up.

Turn off your lights, the LED should glow.

IMG20171030225639

Turn the lights back On and the LED should switch off automatically.

Working with the Bluetooth

Now that we are through with this, we have to manually turn the device on or off. For that, you can download any Bluetooth Terminal App on your phone. I am using Android and I have downloaded this app.

Once you have the app, open the app and change to ASCII mode. Also, make sure to turn off returning the carriage return ( \r ) and the new line ( \n ) characters at the end of the serial data. On the prescribed app above, I do it by long pressing the Send ASCII button to bring up the options and then turning the two checkboxes off in the Select ending for Sent data section and choosing ASCII in Select Sent data type section. Now we are good to go.

Try switching off the light, and the LED turns on. Now send 0 to the bluetooth. The LED turns off even in the dark. Send 1 again and the LED turns back on. A general question would be, “When is the extra control helpful ?” To answer that, first we have to take into consideration that this was only a prototype. The real lamp would be powered by DC batteries. The whole point of this lamp, as we can see, is to save energy. Now suppose you want to leave for work. You would turn all light of your house off and only then leave. Now since the lamp is powered by the battery, unless turned manually off it will use up the charge in the battery to a futile cause. This is why we would need the extra bit of control.

Possible Extensions

We have reached the end of this project. Now let us broadly explore how we can extend this concept to achieve better results. Some possible extension could be as follows:

  1. Automated Home Lighting – With a few PIR sensors ( can roughly detect human presence ) we can extend the same circuit to controlling lights of entire rooms. Switch them on only when human movement is detected.
  2. Thermoregulator – Instead of using an LDR, we can use a Thermometer and instead of LED, a heating element. By sensing the current temperature using the Thermometer, we can turn the heating element ON or OFF. We can also take this one step further by using Analog I/O Pins instead of the Digital I/O Pins to vary the temperature of a closed space. This can be helpful in highly controlled environments. A possible application would be miniature farms and/or nurseries.

If you like this post, please rate it. If you have a suggestion, question and/or correction please feel free to leave a comment below. If you came up with a new application for this project please let me know. You can email me, call me or leave a comment down below. And as always, thanks for reading. 🙂

Optimization Algorithms (and their uses)

So I have not written a post in days. Partly because of my workshops and exams and partly because I was indulging myself deeply into studying Machine Learning. One of the ideas that provoked me was this – Which aspect of Machine Learning can we truly call an AI ? Now I know this is not an original question and there must be hundreds of discussion threads on this topic, but I will try and illustrate what I have understood. From one perspective, there is no true ‘best’ Artificial Intelligence technique.

An Optimization Algorithm is one which finds you an optimal solution for a given problem within a particular problem domain. The problem domain may be fixed or might be variable. The same base-algorithm can also be applied to several problem domains. In other words, for a given list of possible ways to solve a problem (or come close to solving it), an Optimization algorithm searches for the way that offers least resistance and most benefit. There are several such algorithms. Which reminds me, there is an entire directory of this over at Wikipedia. You can follow this link

https://en.wikipedia.org/wiki/Category:Optimization_algorithms_and_methods

to read more about them.

Now if you are familiar with these systems, you can argue that the Neural Networks or the Genetic Algorithm are more suited to any problem domain, but, the counter-argument that they are (fundamentally) less suited to solving problems related to one specific domain seems to surpass it. Suppose I have a game of Chess. I can use a GA-based system and train it for days and I know that still, it won’t be anywhere near today’s Chess AIs which are primarily based on MiniMax or some other variant. This is because in them, the heuristic functions are designed for and only for that game (and none other). You cannot expect a Chess AI to play good Poker, but you should expect it to play veteran chess. So it seems that the problem boils down to what our objective in building the autonomous system is. Unless you want to invent a Terminator, that is what you would do. Pick an algorithm best suited for the task at hand. When I attended my first AI Workshop, I saw the lecturer train the Neural Networks with a carefully chosen sample. But he kept calling it a ‘Random Sample’. At first I thought he was cheating on us because it all seemed counter-intuitive to me. So I went straight to him and asked him. He gave me what I believe is a very precise answer –

“Would I teach you the alphabets A, B, C and D today and expect you to write G and H tomorrow ?”

I realised that even a Random sample must follow certain criteria. The better your training set, the better the results. And as a generalisation of the aforesaid hypothesis, the better suited your algorithm and (if applicable) the better your training set, the better the outcome from the testing set.

I have written seven programs in the past 1 month. I have shared 2 of the most interesting of these over at my website. I have provided the links below.

Please note that I have just started learning AI and as such, there might be innumerous faults in my understanding. I would be grateful if you can help me rectify my misconceptions by suggestions, so, if you have any queries or suggestions please do leave it in a comment below. And finally, thank you for reading. Kudos ! 😉

Grapher (A JAVA desktop application to solve mazes or plot paths)

A tic tac toe game using MaxiMin algorithm and heuristic determination

Simple Path Finder Algorithm in JAVA and Python

The age of AI is upon us and almost in every digital market scenario, we are witnessing the involvement of some form of Artificial Intelligence, either in unremarkable forms or in appraisable forms. Some of the emerging projects in Artificial Intelligence are Google’s DeepMind, IBM’s Watson, Apple Siri (who hasn’t heard that name before?) and Microsoft’s (Halo-based AI) Cortana. Maybe Turing was just a tiny bit off the mark when he said that machines will be able to “answer” like humans by the end of the 20th century. But, only time will judge just how far off the mark he was. This post however, is not about AI, but rather about a specific topic of Rule-Based Expert Systems which are mathematically equipped and programmatically armored to solve certain (and only those certain) problems based on some set rules.

Depth-First Search

For those of you who are still reading this, 😛  if you want to learn about AI, I strongly suggest you subscribe to the MIT OpenCourseWare video lectures on AI over at their YouTube Channel. That is where I learnt all this!

So, we are building a basic Path-Finder, and so we will stick to DFS and not proceed further. What is DFS? DFS is a technique to find a path from one node of an undirected graph to another node of the same graph. How will it help us? It will show us a path that leads from one point on our map to another. We start off at the starting point, navigate through each node until we hit a dead end or the goal. If we hit a dead end, we retrace (called backtracking) our path to the previous node and try another “unvisited” node. If all the nodes of the current node are visited, we backtrack again to the previous node and so on. If we hit the goal, Hurray! We found it!

Show me some codes

For those of you who prefer codes over explanations, here is a class in JAVA that finds the DFS Path for you

 

import java.lang.*;
import java.util.*;

public class DFS{
 private String nodes[][];
 private String visited[];
 private String stack[];
 public DFS(int n, String[][] adj){
     nodes = adj;
     visited = new String[n];
     stack = new String[2*n];
 }
 public boolean isVisited(String node){
    if(Arrays.asList(visited).contains(node)){
        return true;
    }
    return false;
 }
 
 public void setVisited(String node){
     if(!Arrays.asList(visited).contains(node)){
         for(int i = 0; i < visited.length; i++){
             if(visited[i] == null){
                 visited[i] = node;
                 break;
             }
         }
     }
 }
 
 public void addToStack(String node){
     for(int i = 0; i < stack.length; i++){
         if(stack[i] == null){
             stack[i] = node;
             break;
         }
     }
 }
 
 public void popFromStack(){
     for(int i = 0; i < stack.length; i++){
         if(stack[i] == null){
             stack[i-1] = null;
             break;
         }
     }
 }
 
 public String stackTop() throws Exception{
     for(int i = 0; i  0){
                return stack[i-1];
            }else{
                throw new Exception("No DFS Path found");
            }
         }
     }
     return null;
 }
 public void print(String[] array){
     System.out.print(array[0]);
     for(int i = 1; i " + array[i]);
     }
     System.out.print("\n");
    }
     
 public String[] trimmedStack(){
     int l = 0;
     for(int i = 0; i < stack.length; i++){
         if(stack[i] == null){
             break;
         }
         l++;
     }
     String[] stk = new String[l];
     for(int i = 0; i < l; i++){
         stk[i] = stack[i];
     }
     return stk;
 }
 
 public int visitCount(){
     int l = 0;
     for(int i = 0; i < visited.length; i++){
         if(visited[i] == null){
             break;
         }
         l++;
     }
     return l;
 }
 
 public static void getDFSPath(int num_nodes, String[][] nodeMatrix,String start,String end) throws Exception{
     
     long startTime = System.nanoTime();
     
     DFS dfs = new DFS(num_nodes, nodeMatrix);
     String cursor = start;
     String goal = end;
     dfs.setVisited(cursor);
     dfs.addToStack(cursor);
     boolean check = true;
     while(cursor != goal){
         check = true;
         for(int i = 0; i < dfs.nodes.length; i++){
             if(dfs.nodes[i][0] == cursor && !dfs.isVisited(dfs.nodes[i][1])){
                 cursor = dfs.nodes[i][1];
                 check = false;
                 dfs.setVisited(cursor);
                 dfs.addToStack(cursor);
                 break;
             }else if(dfs.nodes[i][1] == cursor && !dfs.isVisited(dfs.nodes[i][0])){
                 cursor = dfs.nodes[i][0];
                 check = false;
                 dfs.setVisited(cursor);
                 dfs.addToStack(cursor);
                 break;
             }else
                continue;
         }
         if(check){
             dfs.popFromStack();
             cursor = dfs.stackTop();
         }
     }
     long elapsedTime = System.nanoTime() - startTime;
     System.out.println("Total Elapsed Time (nanosecs) : " + elapsedTime);
     dfs.print(dfs.trimmedStack());
 }
}

and the same in Python.

import time
class DFS:
     def __init__(self,n,nodes):
         self.nodes = nodes;
         self.n = n;
         self.visited = [];
         self.stack = [];
     def isVisited(self,name):
         if name in self.visited:
             return 1;
         else:
             return 0;

     def setVisited(self,name):
         if name not in self.visited:
             self.visited.append(name);
     def addToStack(self,name):
         self.stack.append(name);
     def popFromStack(self):
         self.stack.pop();
     def stackTop(self):
         return self.stack[len(self.stack)-1];
     def printStack(self):
         print(self.stack);
     def getDFSPath(num_nodes,matrix,start,end):
         start_time = time.time()
         dfs = DFS(num_nodes,matrix);
         cursor = start;
         goal = end;
         dfs.setVisited(cursor);
         dfs.addToStack(cursor);
         check = 1;
         while(cursor != goal):
             check = 1;
             for i in range(len(dfs.nodes)):
                 if(dfs.nodes[i][0] == cursor and not(dfs.isVisited(dfs.nodes[i][1]))):
                     cursor = dfs.nodes[i][1];
                     check = 0;
                     dfs.setVisited(cursor);
                     dfs.addToStack(cursor);
                     break;
                 if(dfs.nodes[i][1] == cursor and not(dfs.isVisited(dfs.nodes[i][0]))):
                     cursor = dfs.nodes[i][0];
                     check = 0;
                     dfs.setVisited(cursor);
                     dfs.addToStack(cursor);
                     break;
                 else:
                     continue;

             if(check > 0):
                dfs.popFromStack();
                cursor = dfs.stackTop();
         print("---Execution Time is %f seconds ---" % (time.time() - start_time))
         dfs.printStack();

You can see that I put some benchmarking code within the functions. Well, I tested several varieties of DFS and BFS (some other time..) algorithms and compared the execution time. It seems JAVA runs faster than Python does. But I guessed so. Anyways, so now we have a system that can output the path to the goal, given all the nodes and connections. But wait… How do we format the input? Well, it should be pretty clear from the code but just in case, an example function call in JAVA would look like this:

DFS.getDFSPath(5,{{“A”,”B”},{“B”,”C”},{“B”,”D”},{“D”,”E”}},”A”,”E”);

and in Python, it looks like this:

DFS.getDFSPath(5,[[“A”,”B”],[“B”,”C”],[“B”,”D”],[“D”,”E”]],”A”,”E”)

And that concludes our post. Finally, in footnote, try tinkering with it and see if you can make it find the most optimal path to the goal. Paste your code in the comments below so others can see this too. If you feel the post is missing something or something must be changed, feel free to email me or leave a comment below. Thank you for reading. Stay tuned for more of the same..

Optimizing Page Speed by reducing headers

I have not written anything for quite a while now, so I decided to put my cursor down on the screen again. Web pages are becoming increasingly beautiful. All those eye-catching banners and color combinations, matched with animations and 3D transitions make us want more out of the internet that our grandparents could ever have imagined. Considering this, the complexity of the code involved in creating these styles is also accelerating quicker than ever.

We have at our disposal, Media ! Starting from images, videos, audio clips, several fonts – each one more beautiful than the previous, 3D rendering frameworks, etc. Standing where we are, we need to load several files on which our original Hypertext file depends. These files are part of what is known as HEADER. Most of you must already be familiar with HTTP Headers. For those who are not, I shall not be discussing it here, but you can read it here or watch this video.

In this post, I will discuss a technique to reduce HTTP headers in order to decrease DOM Loading time and also exclude unused data. For that, let’s first understand the problem with a classic example.

CSS Fonts

If you are intermediately familiar with CSS Stylesheets, you are also familiar with CSS3 Fonts. Now, for any big website, we may require different fonts for different pages depending on our necessity. But consider this – what if I have say, 5 or more different pages with some similar layout and design that can be defined in just one CSS file, but require usage of different fonts? You might say, the answer is, I include the required fonts using link tag(s) and include the common CSS file on every page. But how about a dynamic Single-Page App? What if you require usage of several fonts on such a dynamic page, where you never know what the user wants and when he wants it?

What I want to do is load whatever is mission-critical at first. Then, depending on whether the user needs a particular file, load it onto the DOM. How do I do this?

Ok, How do I do this?

The answer is easy. Using Javascript. But yes, in such cases, although the codes might vary depending on your needs, the concept remains the same. Load if and when needed.

Let’s look at a sample CSS file:

fonts.css

.font-dreamy{
  font-family: 'Josefin Sans', sans-serif;
}
.font-office{
  font-family: 'Poppins', Roboto Slab, Times, sans-serif;
}
.font-hand{
  font-family: 'Satisfy', cursive;
}
.font-spray{
  font-family: 'Sedgwick Ave Display', cursive;
  font-size: larger;
}
.font-blog{
  font-family: 'Roboto', Helvetica, Arial, sans-serif;
}

Now most of these are custom fonts, i.e, these are generally not packaged with the OS by default. We need to load them from an external source which in this case is the Google Fonts library. To do this, the Google Fonts library defines two methods.

  1. We can add the tag given by Google Fonts in the document head.
  2. We can use an @import () css statement to import the required fonts.

The problem with either of these are that they load all the fonts irrespective of whether we need them. Suppose one page uses only the font-hand class and none of the others and another uses only the font-office class and none of the others. In either cases, although unused, all the fonts are loaded beforehand.

The JS Way

Now to the solution :

For this specific example, the JS code is :

fonts.js

function updateTyp(){
    var imp = '&lt;link title=&quot;TypFonts&quot; href=&quot;https://fonts.googleapis.com/css?family=Open+Sans:300,400|';
    if(document.body.querySelectorAll('.f-dreamy').length &gt; 0){
        imp += 'Josefin+Sans|';
    }
    if(document.body.querySelectorAll('.f-office').length &gt; 0){
        imp += 'Poppins:300|';
    }
    if(document.body.querySelectorAll('.f-hand').length &gt; 0){
        imp += 'Satisfy|';
    }
    if(document.body.querySelectorAll('.f-spray').length &gt; 0){
        imp += 'Sedgwick+Ave+Display|';
    }
    if(document.body.querySelectorAll('.f-blog').length &gt; 0){
        imp += 'Roboto:300|';
    }
    if(document.body.querySelectorAll('.dropcap').length &gt; 0){
        imp += 'Alegreya+SC|';
    }
    if(document.body.querySelectorAll('blockquote').length &gt; 0){
        imp += 'Quattrocento+Sans|';
    }
    if(document.body.querySelectorAll('h1').length &gt; 0){
        imp += 'Raleway|';
    }
    imp += '&quot; rel=&quot;stylesheet&quot;&gt;';
    if(document.querySelector(&quot;link[title=TypFonts]&quot;) !== null){
        document.querySelector(&quot;link[title=TypFonts]&quot;).remove();
    }
    document.getElementsByTagName('head')[0].innerHTML += imp;
}

You get the picture, right? We add the fonts to the link tag whenever we need it.
But how do I make this work for dynamic pages?
For that, we need to detect changes in the DOM and execute the above function whenever a change in the DOM is detected. This code is what helps us do just that and has been proudly and professionally ripped off from this stackoverflow answer.

var observeDOM = (function(){
    var MutationObserver = window.MutationObserver || window.WebKitMutationObserver,
        eventListenerSupported = window.addEventListener;

    return function(obj, callback){
        if( MutationObserver ){
            // define a new observer
            var obs = new MutationObserver(function(mutations, observer){
                if( mutations[0].addedNodes.length || mutations[0].removedNodes.length )
                    callback();
            });
            // have the observer observe foo for changes in children
            obs.observe( obj, { childList:true, subtree:true });
        }
        else if( eventListenerSupported ){
            obj.addEventListener('DOMNodeInserted', callback, false);
            obj.addEventListener('DOMNodeRemoved', callback, false);
        }
    };
})();

observeDOM(document.getElementsByTagName('body')[0], function (){
    updateTyp();
});

I have used fonts in this demonstration as an example. The same concept can be extended to images, videos, stylesheets and other graphics. Infact, any such header is almost similar to any AJAX call.

One must note that in order for the DOM Observer to work, the script should be appended only to the end of the body because otherwise, it will not be able to observe any DOM Element(s) below it’s line of declaration and might lead to unexpected and often annoying results.

If you like the post, give it a 5 star rating below. In case you want to add any suggestions or corrections, please leave a comment below. And do read the other posts of this blog. And thank you for reading.

Some CSS3 features you probably never heard of

Hey folks! With CSS4 on the way to the marketplace and the buzz surrounding it, we naturally have not much more to discuss about CSS3 and it’s predecessors. But let us look back and see what all we might have missed about CSS3.

CSS3 introduced many new features. It was a great improvement over it’s popular predecessor CSS 2.1. It added media queries, namespaces, gradients, animations, etc. In this post I am going to talk only about some of the most scarcely seen features found in CSS 3.

@supports

Browser features differ widely from device to device and browser to browser. Most of the time the browser engines define browser-specific prefixes (like -webkit-, -moz-, etc.) to allow access to incomplete versions of the feature with lesser support. But sometimes, even that is not enough. Sometimes, we just don’t have a way to go around. In such cases, we can use the @supports query to define whether to use a feature or to fallback to another method. For example,

@supports (condition 1) or (condition 2) ... (condition N) {
/*define properties when condition(s) is/are supported*/
}
@supports not ((condition 1) or (condition 2) ... (condition N)){
/*define properties for elements when condition(s) is/are not supported*/
}

Read on at MDN Docs

vw and vh

We all know about responsive typography using media queries. But there is another way to resize font with change in device dimensions. The vw and vh represent these measurement units. As opposed to the traditional px and em, these are dependent on the device width and height. They scale up and down depending on the device width and height. There is also a widely supported variation of these, the vmax and the vmin which takes the maximum or the minimum of the device width and height, whichever applies.

Read more MDN Docs

CSS Speech Module

This feature is still under consideration and most known browsers do not support it as of yet. This module, according to the W3.org documentation is just a wrapper over the SSML (Speech Synthesis Markup Language). The syntax is very simple. Let’s take a simple example here :

.speak
{
voice-family: male;
voice-stress: moderate;
cue-before: url(./pre.wav);
voice-volume: medium 6dB;
}

Read more W3.org Docs

Multiple Backgrounds

CSS3 support for multiple background is becoming increasingly common. Check the list of support here. Multiple background property lets you set several backgrounds to an element, one on top of the other. The syntax is as follows:

background: background1, background2, ... ;

Check it out at the MDN Docs

Background Blend Mode

Now that we already talked about adding several backgrounds, will they always look good over one another? To ensure that they do, we have another property called the background-blend-mode. This lets us define how the backgrounds blend with each other.

Read more at MDN Docs

3D Transforms, Translates and Scales

3D transforms are already quite popular. Most probably you already know or guessed what it is. Anyways, I will just state the obvious just in case. 3D Transforms allow us to make CSS Transformations… in 3D. Just a Z-axis more. But it can create cool effects too! It has a few interesting sub-properties – the translate3D, rotate3D and the scale3D. Although they require MathML support, they can be handy in preserving details.

Read more at MDN Docs – scale3d(), MDN Docs – rotate3d() and MDN Docs – translate3d()

Backface Visibility

Since we already talked about 3D Transformations in CSS3, we might as well talk about this property. This property defines whether an element’s backface should be visible when it is rotated in the 3D plane. Browser support is currently increasing at a steady rate and should be available in most major browsers.

Read more at MDN Docs

Appearance

The appearance property is now supported in webkit. Vendor-specific prefixes for other engines are also available. This property allows us to change the default appearance of  HTML5 element to suit our design needs. There are possibly no restrictions – from inputs to buttons, it can modify any HTML5 native element.

Read more at MDN Docs

Backdrop

The CSS ::backdrop pseudo-element can be used to create a backdrop without any fancy code or CSS hacks. An example usage is as follows:

dialog::backdrop{
background: rgba(255,0,0,.25);
}

Read more at MDN Docs

Basic Shapes

How do you create a triangle using CSS ? Reducing height, setting border backgrounds to transparent, blah blah.. right ? Well, how do you make a hexagon ? That would surely be a lot of work using this technique. Or, you can just switch to the basic shapes that can be set using the clip-path or the set-outside properties. They are cleaner and more interesting.

Read more at MDN Docs

@page

This property is certainly not untapped. But I added it, just in case. This allows styling your document to look as good on printed paper as on the screen.

Read more at MDN Docs

User Zooming

The user-zoom property lets you decide whether or not a user can change the zoom factor defined by the viewport. Changing zooms can break the html styles and can be quite annoying at times. This lets you prevent that from happening.

Read more at MDN Docs

Object Fit

The best way to resize an image or video is using the CSS object-fit property. It can even change the dimensions of the image or video to fit the container element.

Read more at MDN Docs

Cue Change

The CSS ::cue pseudo-selector lets you style the elements especially in a VTT track.

Read more at MDN Docs

Caret Coloring

Ever wanted to change the color of the caret (the blinking pointer showing at what position you are typing) in a text field ? Well, you can use the caret-color property to change it to just about any plain color or even any hex or rgb value.

Read more at MDN Docs

Will Change

Worried about how the browser will optimize your content ? The will-change property allows the developer to have partial control over the web page optimization strategies implemented by the browser by letting it know in advance what is to be expected.

Read more at MDN Docs

Orientation

On handheld devices, orientation is a big issue. A webpage might look good in portrait mode but the same might not be so beautiful in landscape mode. Using the orientation property you can (to some extent) force the user to view the web-page only in one specific mode (i.e, either portrait or landscape);

orientation: auto;
orientation: portrait;
orientation: landscape;

Font Language Override

Anyone speaks Azkabanian here ? Probably not. But hey ! I wanna write in Azkabanian ! Oh, wait ! Hogwartzian is somewhat like Azkabanian and is supported in this browser. But,… I guess there are no provisions to use Hogwartzian dialects for Azkabanians, right? There is. Using the font-language-override property, you can set the default unicode dialect for an unsupported language to a similar supported language. And although, Azkabanian and Hogwartzians are just imaginary languages whose ideas are ripped-off from the world of Harry Potter land by the infamous me, it works just as well in the real world too !

Read more at MDN Docs

Fallback Soldiers !

When nothing works in the face of features-battle, save yourself ! The last thing anybody wants is bad PR for nothing more than unsupported features. For fallback, we most often have to lean on Javascript. But that should not be considered bad. After all, these features exist only to beautify the looks of your content and not the content itself. But the content carries points too, right?

This is all the features I could add to the list. Try them out if you haven’t and let me know what you got (a link to your works would be awesome). If you wanna add something to this list, let me know in the comments section. I am open to suggestions and corrections. And as always, thanks for reading.

Let’s build a polling system!

Hi there! Today we are going to build an AJAX Polling System using HTML5, CSS and Javascript. During the course of this, we could obviously use a library like JQuery to speed things up a bit but I would just stick to plain old Javascript.

A Polling System

Obviously, a polling system is a system that polls data from users regarding your brand or website or products you offer to them. It provides valuable insights into your target audience’s mindset while also revealing important feedback on your product quality and and how you hold up against your competition.

Polling vs Rating

A polling system and a rating system are almost similar. They both have the same goal – to get feedback from the user. However, a Polling System has a broader base than a rating system. A rating system per se only helps rate the content based on user experience. It does not provide any information on “why the user likes/dislikes your product“, “how does he/she suggest you improve it” and so on. Logically, you can think of Ratings as a subset of Pollings. What we are going to build is a Polling System and not just a Rating System.

img1

Let’s get started !

We will start off by first creating the file structures we will eventually fill with codes. Create a new directory or folder named “poll” and create the files inside it. It’s contents should looks like this:

  • poll.html
  • poll.js
  • poll.css
  • poll-data (folder)
    • form1.html
    • form2.html
    • form3.html
    • form4.html

Ok. So now we are ready to begin building our polling system.

Let’s first fill up the poll.html

poll.html

<!DOCTYPE html>
<html>
<title>Poll Form</title>
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:200" rel="stylesheet">
<link rel="stylesheet" href="./poll.css">
</head>
<body>
    <div class="pollbox">
        <div id="form" class="form"></div>
        <button type="button" id="prevBtn" class="btn">Previous</button>
        <button type="button" id="nextBtn" class="btn">Next</button>
    </div>
<script src="./poll.js"></script>
</body>
</html>

Now onwards to the script file – poll.js

poll.js

var page = 0;
var finalPage = 3;
var response = {};
var XMLHttpObjs = [
    function () {return new XMLHttpRequest()},
    function () {return new ActiveXObject("Msxml3.XMLHTTP")},
    function () {return new ActiveXObject("Msxml2.XMLHTTP.6.0")},
    function () {return new ActiveXObject("Msxml2.XMLHTTP.3.0")},
    function () {return new ActiveXObject("Msxml2.XMLHTTP")},
    function () {return new ActiveXObject("Microsoft.XMLHTTP")}
];

function createXMLHTTPObject() {
    var xmlhttp = false;
    for (var i=0;i < XMLHttpObjs.length;i++) {
        try {
            xmlhttp = XMLHttpObjs[i]();
        }
        catch (e) {
            continue;
        }
        break;
    }
    return xmlhttp;
}

function getForm(n){
        var xhr = createXMLHTTPObject();
        xhr.onreadystatechange = function() {
            if (this.readyState == 4 && this.status == 200) {
                document.getElementById("form").innerHTML = xhr.responseText;
                if(page === finalPage){
                    document.getElementById("nextBtn").innerHTML = 'Finish';
                }else{
                    document.getElementById("nextBtn").innerHTML = 'Next';
                }
            }
        };
        xhr.open("GET", "poll-data/form" + n + ".html", true);
        xhr.send();
}

function next(){
    updateResponses();
    if(page !== finalPage){
        page++;
        getForm(page);
    }else{
        //handle response variable
        finish();
    }
}

function prev(){
    updateResponses();
    if(page !== 1){
        page--;
        getForm(page);
    }
}

document.getElementById("nextBtn").addEventListener("click", function(){
    next();
});
document.getElementById("prevBtn").addEventListener("click", function(){
    prev();
});

function updateResponses(){
    if(page > 0){
        var tempRes = {};
        var inputs = document.getElementsByTagName("input");
        for(i = 0;i < inputs.length;i++){
            if((inputs[i].type !== 'checkbox' && inputs[i].type !== 'radio') || (inputs[i].type == 'checkbox' && inputs[i].checked) || (inputs[i].type == 'radio' && inputs[i].checked)){
                tempRes[inputs[i].name] = inputs[i].value;
            }
        }
        var selects = document.getElementsByTagName("select");
        for(i = 0;i < selects.length;i++){
            tempRes[selects[i].name] = selects[i].value;
        }
        var textareas = document.getElementsByTagName("textarea");
        for(i = 0;i < textareas.length;i++){
            tempRes[textareas[i].name] = textareas[i].value;
        }
            response[page] = tempRes;
    }
}

function finish(){
    var param = 'json=' + JSON.stringify(response);
    console.log(param);
    var xhr = createXMLHTTPObject();
        xhr.onreadystatechange = function() {
            if (this.readyState == 4 && this.status == 200) {
                document.getElementById("form").innerHTML = xhr.responseText;
                document.getElementById("nextBtn").outerHTML = '';
                document.getElementById("prevBtn").outerHTML = '';
            }
        };
        xhr.open("POST", "savetofile.php", true);
        xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        xhr.send(param);
}
next();

We will only concern ourselves with the actual working and not the design of the form. So, let’s just define a few CSS Styles for a basic form layout in our poll.css file.

poll.css

@charset "UTF-8";
body{
    padding: 10px;
    font-family: Helvetica, Aria, sans-serif, Roboto;
}
.pollbox{
    position: relative;
    height: 400px;
    width: 450px;
    margin: auto;
    border-radius: 4px;
    padding: 20px;
    text-align: justify;
    justify-content: space-between;
    -webkit-box-shadow: 0px 2px 10px 3px rgba(0,0,0,0.20);
    -moz-box-shadow: 0px 2px 10px 3px rgba(0,0,0,0.20);
    box-shadow: 0px 2px 10px 3px rgba(0,0,0,0.20);
}
@media only screen and (max-width: 480px){
    .pollbox{
        width: 100%;
    }
}
header{
    font-size: 18pt;
    letter-spacing: 0.4em;
    font-family: Helvetica, Aria, sans-serif, Roboto;
    color: #f4671a;
}
.hr{
    width: 90%;
    margin-bottom: 20px;
    border: 0px;
    border-bottom: 1px solid;
    color: #f4671a;
    -webkit-animation: grow 1s;
    -moz-animation: grow 1s;
    -o-animation: grow 1s;
}
@keyframes grow{
    0%{width: 0;}
    100%{width: 90%;}
}
@-webkit-keyframes grow{
    0%{width: 0;}
    100%{width: 90%;}
}
@-moz-keyframes grow{
    0%{width: 0;}
    100%{width: 90%;}
}
@-o-keyframes grow{
    0%{width: 0;}
    100%{width: 90%;}
}
.pollbox .form{
    height: 360px;
    overflow-y: auto;
    font-size: 14pt;
    letter-spacing: 0.08em;
    padding: 5px;
}

.pollbox .form::-webkit-scrollbar{
    position: fixed;
    z-index: 1000000;
    width: 4px;
}
.pollbox .form::-webkit-scrollbar-track{
    -webkit-box-shadow: none; 
    -webkit-border-radius: 10px;
    border-radius: 10px;
}
.pollbox .form::-webkit-scrollbar-thumb {
    display: block;
    padding-left: 2px;
    -webkit-border-radius: 10px;
    border-radius: 10px;
    background: grey; 
    -webkit-box-shadow: inset 0 0 6px #a3a3a3;
}
.pollbox .form::-webkit-scrollbar-track-piece{
    background: transparent;
}

.pollbox .btn{
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    padding-left: 10px;
    padding-right: 10px;
    border-radius: 18px;
    background: white;
    color: #f4671a;
    border: 1px solid #f4671a;
    cursor: pointer;
    font-size: 1.4em;
    font-family: 'Source Sans Pro', Helvetica, Aria, sans-serif, Roboto;
    letter-spacing: 0.15em;
}
.pollbox .btn:hover{
    background: #f4671a;
    color: white;
}
.pollbox .btn:focus{
    outline: 0;
}

input{
    margin-bottom: 20px;
}

input[type="text"]{
    border: 1px solid #f4671a;
    border-radius: 16px;
    padding: 8px;
}
input[type="text"]:focus{
    outline: 0;
    border: 1.4px solid #f4671a;
}
input[type="text"]::placeholder{
    font-size: 14pt;
    font-family: 'Source Sans Pro', Helvetica, Aria, sans-serif, Roboto;
    font-style: italic;
    color: #f4671a;
}
.radiogroup,.checkgroup{
    color: #f4671a;
}

Ok. So we are halfway there. Now all we have to do is create the actual forms. If you have seen the poll.js file above you probably noticed that we set the final page variable to 3. So we can only have a maximum of 3 form pages. We did this to ensure that the browser does not simply keep sending GET requests whenever the user clicks ‘Next’ button even if no next page exists. If you want to add more, just pick a number and set it to the finalPage variable.

Now I have created a 3 page Poll Form and the 3 pages look like this:

poll-data/form1.html

<header>Some basic Info</header>
<hr class="hr">
<label for="name">Your Name</label>
<input type="text" name="name" placeholder="Type here"><br/>
<label for="name">Your Email</label>
<input type="text" name="email" placeholder="Type here"><br/>
Your Gender: 
<span class="radiogroup">
<input type="radio" name="gender" value="male">Male
<input type="radio" name="gender" value="female">Female
</span>

poll-data/form2.html

<header>About Product</header>
<hr class="hr">
<p>
How did you come to know of this product?<br/>
<span class="radiogroup">
<input type="radio" name="howknow" value="friend">From my friends<br/>
<input type="radio" name="howknow" value="relatives">From my relatives/acquaintances<br/>
<input type="radio" name="howknow" value="ads">From an Ad Campaign<br/>
<input type="radio" name="howknow" value="others">I would rather not tell
</span>
</p>
<p>
On a scale of 1 to 5 how good was this product? <br/>
<input type="range" name="rating" min="1" max="5" step="1" value="1">
</p>

poll-data/form3.html

<header>A few final feedbacks</header>
<hr class="hr">
<p>Select the options that apply to you:<br/>
<span class="checkgroup">
<input type="checkbox" name="specs[]" value="olduser">I have used this product before<br/>
<input type="checkbox" name="specs[]" value="lead">I am going to refer this product to a friend<br/>
<input type="checkbox" name="specs[]" value="survey">I am an employee in this company<br/>
<input type="checkbox" name="specs[]" value="links">My friend(s) and/or family member(s) already use/are planning to use this product
</span>
</p>
<p>
And lastly... Was this Survey good ?<br/>
<span class="radiogroup">
<input type="radio" name="surveyGood" value="yes">Yes
<input type="radio" name="surveyGood" value="no">No
</span>
</p>

And finally. We have our poll form’s GUI ready to rock ! Run it in your favourite browser and it should look somewhat like this –

img3

 

Now that we have parsed data from the user, let’s store this data somewhere where it will be useful. You can either store it in json files or in your database using PHP depending on what your project demands. Pick one and you are ready to go. I will only show you how to save to file in this tutorial. But I have added a link  (in the References section) to a demo showing the same stored in a MySQL database too.

savetofile.php

<?php
error_reporting(E_ALL);
ini_set(“display_errors”,1);
if($_SERVER[‘REQUEST_METHOD’] === ‘POST’ && isset($_POST[‘json’])){
file_put_contents(‘polls/’.time().’.json’, $_POST[‘json’]);
echo ‘<header>Survey Completed</header><hr class=”hr”>Thank you for completing our short survey.’.
‘A link for your Trial Pro Version of the Product will be sent within the next 48 hours.’;
exit;
}else{
echo ‘Oops! Something went wrong with the survey’;
exit;
}
?>

And finally we have our system. 🙂

If you like it and/or have a suggestion, please leave a comment below. Check the links in the next section for references to further readings. Happy coding!

Further Reading

  1. AJAX Polling (W3Schools) – https://www.w3schools.com/php/php_ajax_poll.asp
  2. Google Forms – https://www.google.com/forms/about/
  3. Styling HTML form elements (MDN) – https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Styling_HTML_forms

Live Demo

  1. Save To File Demo – https://mskies.web44.net/blog_live_demos/25-07-17/poll.html
  2. Save To Database Demo – https://mskies.web44.net/blog_live_demos/25-07-17/poll_database.html

On my previous post I talked about multi-threading in Javascript. If you haven’t checked it out yet, push the Previous button below or click on this link.