Robb Owen Digital
Robb is an independent creative developer from South Wales who loves helping organisations to build ambitious yet accessible web projects. Sometimes, he also writes in the third person.
robbowen.digitalRobb is an independent creative developer from South Wales who loves helping organisations to build ambitious yet accessible web projects. Sometimes, he also writes in the third person.
robbowen.digitalCtrl + Shift + P or Shift + ⌘ + P and choose "Disable Custom CSS and JS". In the name of simplifying the install process and mitigating update-related issues, the new version of Synthwave doesn't use a custom CSS file anymore. If you have been using a custom-modified version of the theme, then you might want to leave it as is for now. ## Start dreaming Firstly, if you are a Windows user, you may need to run VS Code with administrator privileges. For Linux and Mac users, Code must not be installed in a read-only location and you must have write permissions. Once you have admin permissions, open VS Code, and set your active colour theme to Synthwave '84 - the glow is only active when the base theme is selected. press Ctrl + Shift + P or Shift + ⌘ + P and choose "Enable Neon Dreams" You should be prompted for a restart. When the editor reloads, the glow should be active. As with the prior versions, VS Code may warn you that your installation is corrupt. You can suppress this message by choosing 'Don't show this again' from the notification. Also as before, whenever VS code updates, you will need to rerun the enable commandc pipenv install bs4 html5lib Next, create a file called main.py with the following contents: python import requests # we'll need this to make requests to URLs from bs4 import BeautifulSoup, Comment # HTML and comment parsers def read_urls(urls): content_string = '' for page in urls: print(f'Scraping URL: {page}') # Synchronous request to page, and store returned markup scrape = requests.get(page).content # Parse the returned markup with BeautifulSoup's html5 parser soup = BeautifulSoup(scrape, "html5lib") # loop script tags and remove them for script_tag in soup.find_all('script'): script_tag.extract() # loop style tags and remove them for style_tag in soup.find_all('style'): style_tag.extract() # find all comments and remove them for comment in soup(text=lambda text: isinstance(text, Comment)): comment.extract() # Join all the scraped text content text = ''.join(soup.findAll(text=True)) # Append to previously scraped text content_string += text return content_string You don't need to worry too much about the specific code of the function above, but it goes as follows: It read a list of URLs, making a request to each before parsing the content with BeautifulSoup. The scraped markup needs to be tidied up a bit though, so next it removes any comments, script or style tags before pulling out the remaining text content and appending it to a string. When every URL in the list has been scraped, the string is returned as our corpus. Now we have our corpus, we can start to process it. ## Lost in SpaCy SpaCy bills itself as 'Industrial-strength Natural Language Processing in Python'. Whereas packages like NLTK offer immense amounts of flexibility and configuration, the flipside is a steep learning curve. SpaCy on the other hand, can be set up and ready to go in minutes. We need to add SpaCy to our project as a dependency. Open your project directory in your terminal and run: c pipenv install spacy SpaCy doesn't do much by itself though - it needs a pre-trained language model to work with. Fortunately several are available out of the box. We'll also install the standard english model and save it to our Pipfile: c pipenv install https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-2.2.5/en_core_web_sm-2.2.5.tar.gz With our dependencies installed, let's get back into the code. Open up main.py and add SpaCy to our imports at the top: python import requests # we'll need this to make requests to URLs from bs4 import BeautifulSoup, Comment # HTML and comment parsers import en_core_web_sm # SpaCy's english model Next, underneath our read_urls function, we can define a new function to allow SpaCy to parse the corpus. python def get_tokens(corpus): # load the pre-trained english model nlp = en_core_web_sm.load() # parse the corpus and return the processed data return nlp(corpus) Notice the name of our function, get_tokens. What's that all about? ## Parts of Speech and Tokenization Most languages divide their words up into different categories. These usually comprise nouns, verbs, adjectives, adverbs and more. In linguistic terms, these categories are known as 'parts of speech', or POS. This is important to us as, to generate our agency name, we want to single out nouns and adjectives. But how can we get at them? One place to start would be to break each sentence up into its individual parts but this isn't a straightforward as using String split(). We need a way to intelligently divide our text, whilst retaining the individual purpose of each word. Doing this manually would be too time consuming, so this is where machine learning becomes necessary. Most NLP libraries use trained language data to intelligently scan, compare and identify the characteristics of each word. Once identified, these words are separated out into 'tokens'. This process is known as tokenization and is one of the most important core principles of NLP. Our get_tokens function tokenizes our corpus. Or, in simpler terms, SpaCy reads over the corpus, splits out individual words and returns a list of tokens. In creating that list, SpaCy also adds a lot of extra information to each extracted word, such as its relationship to other words in the sentence or which part of speech it is. We can use those attributes to filter the list down to find a specific part of speech. Let's create another function: python def get_word_list(tokens, part_of_speech): # extract a list of words and capitalise them list_of_words = [ word.text.capitalize() for word in tokens if word.pos_ == part_of_speech ] # make the list unique by casting it to a set unique_list = set(list_of_words) # cast it back to a list and return return list(unique_list) This function receives the list of tokens, a part of speech code, and then filters the results using a list comprehension (again, for JS developers, a list comprehension is a bit like a cross between Array.map and Array.filter). To break this list comprehension down, it's best to work from the inside out – For every word in our tokens list, we grab word.text and capitalise it, but only if the part of speech (word.pos_) matches our POS code parameter. You can find a full list of these POS codes in the SpaCy POS documentation. If you're curious about what other attributes might be available on each token, you can read the Token reference. Lastly, we can filter out any duplicated words by casting the results to a set(). We'll need to perform some list actions later though, so it is cast back to a list before being returned. This is all looking great and we can now use this function to create a list of nouns, adjectives, verbs or adverbs. There's a slight problem though - if you were to use this function as-is, you might find that the list of words you get back is a bit of a mess. ## Stop words Most languages have a set of core words that occur frequently. In English, this might be words like "the", "at", "from", "in", "for" and so on. Whilst these words are useful for humans, they can quite quickly get in the way extracting meaning with NLP. As a result, it's common to filter them out and these kinds of words are known as stop words. Our script is no different and the stop words need to go. Whilst we're here, we can also get rid of any rogue punctuation. Fortunately SpaCy tokens have two handy attributes that allow us to do this - the aptly named is_stop and is_punct. Let's update the list comprehension in our get_word_data function: python list_of_words = [ word.text.capitalize() for word in tokens if word.pos_ == part_of_speech and not word.is_stop and not word.is_punct ] Now we can extract a nice sanitised list of words, but there's a tiny bit more optimisation that we can do. ## Lemmatization Let's say you've scraped your website content and in amongst your list of tokens are the words "goes", "going", "went" and "gone". That's quite a few words to express the same base idea. We can make this simpler. These words could all be lumped together under the core concept: "go". In linguistic terms, we call this a word's 'lemma' or the uninflected root form of a word. The simplest way to think of a lemma is to see it as what you might look for in a dictionary: Out of the box, SpaCy provides a convenient means to convert any matched term back to its lemma. This is called lemmatization. Let's hone our list of tokens down into a much tighter list of core ideas by updating our list comprehension. We'll change the first line to call .lemma_ instead of .text. Your final get_word_list function should look like this: python def get_word_list(tokens, part_of_speech): # extract a list of lemmas and capitalise them list_of_words = [ word.lemma_.capitalize() # <-- update .text to .lemma_ for word in tokens if word.pos_ == part_of_speech and not word.is_stop and not word.is_punct ] # make the list unique by casting it to a set unique_list = set(list_of_words) # cast it back to a list and return return list(unique_list) ## Bringing it all together Now we've set up a few functions and we've covered the core concepts, let's pull everything together. To generate the final name, we're going to need to be able to make random selections from our word lists. At the top of main.py, add random to your imports. python import requests # we'll need this to make requests to URLs from bs4 import BeautifulSoup, Comment # HTML and comment parsers import en_core_web_sm # SpaCy's english model import random At the end of the file, underneath your get_word_list function, paste the following: python # create a list of urls to scrape # For illustration, I'm using my website # but replace with any urls of your choice CORPUS = read_urls([ 'https://robbowen.digital/', 'https://robbowen.digital/work', 'https://robbowen.digital/about' ]) # parse the corpus and tokenize TOKENS = get_tokens(CORPUS) # extract a unique list of noun lemmas from the token list nouns = get_word_list(TOKENS, "NOUN") # extract a unique list of adjective lemmas from the token list adjectives = get_word_list(TOKENS, "ADJ") # create a random list of 500 nouns and adjective-noun combos # cast to a set to make sure that we only get unique results list_of_combos = set( [ # String interpolation of an adjective and a noun... f'{random.choice(adjectives)} {random.choice(nouns)}' # 50% of the time... if random.choice([True, False]) is True # else return a random noun else random.choice(nouns) # and repeat this 500 times for x in range(500) ] ) # Write list of combinations to a text file output_file = open("./output.txt", 'w') for item in list_of_combos: output_file.write(f'{item}\r\n') Let's break that code down. First of all, we pass a list of URLs into our read_urls function and store the result in a constant for later. Be sure to change the example URLs to your own choices here, and remember: the more text on your chosen pages, the better your results will be. Next, we pass the corpus into our get_tokens function. This is where SpaCy does its magic, tokenizing the corpus and returning a list of data-rich tokens. To generate our agency name, we want some random nouns. Nouns by themselves aren't much fun, so we'll grab some adjectives too. This uses the get_word_list function we created earlier. The first argument is the list of tokens, and the second filters by a 'part of speech code'. Now we have our nouns and adjectives lists we can prep our final list of names. We want to generate a list of names – let's say 500 of them. We can use another list comprehension to do this. Remember the random library we imported above? It gives us access to the random.choice function. As the name implies, it allows us to randomly choose a single item from a list. random.choice also gives us a handy way to embed a 50/50 chance into our comprehension. Here, an if-else means each iteration of our loop will return either an adjective-noun combo or a single noun. Finally, with the comprehension complete, we wrap the list in set() to ensure that we only have unique results. Ok, everything is ready. At the very end we create a file called output.txt, loop through our list of names and write each to a new line. And...we're done! If you need to, you can double check your code against the final source on github. ## Generate the name It's time to fire up the final script and generate some names. At your terminal, run the following command: c pipenv run python main.py If all goes well, you should have now a file named output.txt chock full of multi-award-winning agency names. ## And the winner is... "Just pick a random word or two and go with that". So, after all this, what name did I go with? Well, I had some real gems: Loaf, Strategic Bear, Rise, Chaotic Rest, Bold Tomorrow. After a few minutes crawling through the output list though, I spotted the one. Exciting, yet confident. Like a life-raft in a sea of comedic nonsense: ## Wrapping up Ok, I get it. Like most clickbait, this might not have been a completely satisfying ending. Hopefully though, you've learned a little about the potential application of even the most superficial Natural Language Processing scripts. To recap, in this introduction, we've used and learned about: - Corpus: The body of text to be processed - Parts of speech: The name given to the categories of words comprising nouns, verbs, adjectives etc. - Tokenization: The act of splitting sentences into individual words, or tokens - Stop words: Small, frequently occurring words that we do not wish to include in our results i.e. "the", "for", "it" etc. - Lemmatization: Changing a word back to its root concept, or dictionary form i.e. the lemma of "best" or "better" is "good" From these core concepts, the possibilities are vast. Imagine going into a pitch having scanned the client's website in advance to profile exactly how they talk about themselves. Or perhaps you could take the jump into sentiment analysis to scan how well received a particular topic is on Twitter. There's still a lot to learn but, with these core building blocks, you're on your way. If you enjoyed this article and have any ideas on how to extend this example, or if you generated a particularly badass name, then please let me know on Mastodon. If you're looking to learn more about NLP, I strongly recommend the Advanced NLP with SpaCy course by Ines Montani - it's free, and really rather good.canvas. Tools like threejs make it easier to harness the power of 3D and GLSL (OpenGL Shading Language) shaders and, with that, a whole new level of visual effects. This got me thinking though; why let JS have all the fun? With mix-blend-mode finally gaining wide browser-support, we now have access to many of most common shading techniques in CSS. With some choice images and a bit of careful layering it's possible to build some surprisingly high-quality effects without the need for introducing any JS dependencies. Let's take a look at an example. As you scroll past the image below, the sunlight blooms a warm orange, before fading to a cool blue. You'll also briefly see some lens bokeh. Ooooh shiny. Let's break it down. ## What is a CSS 'shader'? Shaders in the WebGL world are complex GLSL scripts that determine how each individual pixel is rendered to the screen. We still don't have that level of control in our CSS so, at its most basic level, our CSS 'shader' is just an image with additional background-image layers above it. Yes, I'm taking a few liberties with the name but with careful use of gradients, masking, nesting and mix-blend-mode we can manage how these layers interact with both one-another and the image at the bottom of the stack. For the sake of visualising this core structure, the 'shader' example above is set up with a few nested divs: html <div class="shader"> <img src="tower.jpg" alt="Asakusa at dusk"> <div class="shader__layer specular"> <div class="shader__layer mask"></div> </div> </div> To keep each layer aligned with the image at the base, we keep the nested content positioned with the following CSS: css .shader { position: relative; overflow: hidden; backface-visibility: hidden; /* to force GPU performance. More on that later */ } .shader__layer { background: black; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-size: 100%; } Ok, with the basic layout taken care of, lets take a look at the first layer of our effect - the lighting. ## Simulating specularity First of all we need to think about how light travels from light to dark across the surface of our image. We'll need an area of brightness where the light is at its most intense, falling off gradually into darkness as the light dissipates. We'll do this with a gradient. css .specular { background-image: linear-gradient(180deg, black 20%, #3c5e6d 35%, #f4310e, #f58308 80%, black); } If you imagine looking at a shiny surface, the light that reflects back is known as a specular reflection. How and where that highlight appears is dependant on the light source, but also your viewing angle - the highlight moves with you. Whilst our gradient is looking lovely, it's all a bit static. We'll need to introduce a bit of movement to really sell the effect. Fortunately, there's a vintage CSS Level 1 property that can help with that; setting .specular to background-attachment: fixed means that, as the page scrolls, the gradient remains locked to the browser's viewport. This not only brings some much needed motion to our shader, but also means that we can very roughly simulate the changing view-angle without reaching for JavaScript. css .specular { background-attachment: fixed; background-image: linear-gradient(180deg, black 20%, #3c5e6d 35%, #f4310e, #f58308 80%, black); } Great! Now, let's get to applying this lighting to our base image. ## Know your blend modes As the name implies, mix-blend-mode mixes the colours of each pixel in one element layer together with the layer directly below it. As with GLSL, CSS gives us a nice long list of options to choose from, and creating the right effect means knowing which blend is going to give the results we need. But what do these blend modes actually do? Before we get stuck in with our shader, let's take a quick look at the blend modes we'll be using. Below you can see the images that we'll be using for these examples. On the left is the upper layer to be blended, and the right is the base image we'll be blending onto. First off, let's look at a multiply blend. Multiply takes the colour of each pixel in the current layer and multiplies it with the colour of the pixel directly beneath it. In practice this means that darker colours in the current layer will obscure those in the layer below: Setting the blend to screen takes the inverse of each pixel and multiplies them before inverting the result. This might sound complicated, but you can think of screen as the opposite of multiply - Darker colours become transparent and only the lighter colours will show through to the layer below: Lastly, color-dodge and color-burn are like taking multiply and screen into overdrive. Both modes divide the pixel colour on the base layer with the one in the current layer. For color-dodge this means that highlights and midtones get blown out whilst dark tones have no affect at all. color-burn will boost shadows and darker midtones, whilst lighter tones have no affect at all. In the image below, the left example is color-dodge and the right is color-burn. ## Compositing layers Now we know what we're working with, it might seem like the next step would be to slap a blend mode onto our gradient, place it over our base image and call it a day. That will definitely work, but it won't reach the level of quality we're going for. Blending the gradient directly to the base layer will mean that the lighting will be totally uniform across the image. Aside from chromed surfaces, that doesn't happen too often in nature. To really sell the effect we want to have control over the areas of the image where light can fall and where it can't. To simulate this, we can use a predominately dark image to mask out our gradient. This technique of using a dark image to mask off areas of a lighter one is often known as a specular map. You might be wondering how it will be possible to do this with CSS if mix-blend-mode only affects the pixels in the layer directly below it, and we can only set one blend mode at a time. This is where our HTML structure starts to shine. html <div class="shader"> <img src="tower.jpg" alt="Asakusa at dusk"> <div class="shader__layer specular"> <div class="shader__layer mask"></div> </div> </div> By nesting div layers inside of one another, we can work outwards applying additional mix-blend-modes to each wrapping div. Essentially, this lets us add another mix-blend-mode to the output of the previous blend. This process of layering different blend modes to produce a final output is known as compositing. Let's give it a try. Using a suitably dark background-image we'll set mix-blend-mode: multiply on our .mask layer and throw away the parts of our gradient where we don't want light to show through. css .mask { mix-blend-mode: multiply; background-image: url(/tower_spec.jpg); } .specular { background-attachment: fixed; background-image: linear-gradient(180deg, black 20%, #3c5e6d 35%, #f4310e, #f58308 80%, black); } Now that we have our specular map, we can apply the final lighting to the base image. We need to use one of the blend modes that ignores black and dark tones. This means that we should set our .specular layer to use mix-blend-mode: screen or mix-blend-mode: color-dodge. Either would work in this case, but because we want the highlights to blow out into a nice sunlight-bloom effect, we'll go for color-dodge. Let's take a look: css .specular { mix-blend-mode: color-dodge; background-attachment: fixed; background-image: linear-gradient(180deg, black 20%, #3c5e6d 35%, #f4310e, #f58308 80%, black); } ## The final shader And with that, the effect is complete! Here's the completed HTML and CSS: html <div class="shader"> <img src="tower.jpg" alt="Asakusa at dusk"> <div class="shader__layer specular"> <div class="shader__layer mask"></div> </div> </div> <style> .shader { position: relative; overflow: hidden; backface-visibility: hidden; /* to force GPU performance */ } .shader__layer { background: black; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-size: 100%; background-position: center; } .specular { mix-blend-mode: color-dodge; background-attachment: fixed; background-image: linear-gradient(180deg, black 20%, #3c5e6d 35%, #f4310e, #f58308 80%, black); } .mask { mix-blend-mode: multiply; background-image: url(/tower_spec.jpg); } </style> Let's take another look at the completed effect, but this time with the added ability to isolate each layer of the shader. Change the view mode dropdown to step through the effect and get a better idea of how the layers work together to produce the final image: View mode: No effect Specular only Specular + base image Mask only Specular + mask * Full effect ## Taking it further In the example above, we used a greyscale version of the main image (with added scratches and bokeh) as a mask. That's a great way to add interest to an image, but shader layers can be whatever you want them to be. Let's look at a few more examples. ### Aurora Borealis In this example, repeating the gradient background gradient and reducing background-size-y causes the light effect to move more quickly across the screen. When masked out with a specular map, this creates the illusion of an aurora rippling over the main image. Blown-out highlights with colour-dodge creates the wrong effect here, so swapping our .specular layer to mix-blend-mode: screen maintains the sharp definition of the aurora. View mode: No effect Specular only Specular + base image Mask only Specular + mask * Full effect ### Light-leak Until now the examples have all used a greyscale specular map, but a full colour specular map can introduce new effects. In this example the mask image is created from an inverted and blurred version of the primary image, with a blue-red tone overlaid on top. When it's all layered together with a hot red-orange gradient, the resulting blend of colours results in something that looks a little like the light-leaks you get on vintage film cameras. View mode: No effect Specular only Specular + base image Mask only Specular + mask * Full effect ### Hologram Layering within the mask opens up even more possibilities. What would happen if we add another layer with background-attachment: fixed? In this final example the mask layer has a background image SVG, and another black-white gradient running at the opposite angle to the specular gradient in the .specular layer. Setting the nested mask layer to color-burn causes it to blow out the definition of the SVG and you get this sweet two-way holographic look. CSS is awesome. View mode: No effect Specular only Specular + base image Mask only Specular + mask * Full effect ## Wrapping up I've said it before, but it bears repeating: modern CSS is such an amazing tool to work with and I'm consistently impressed with the level of fidelity you can achieve. That said, It's time to address the potential elephant in the room. Depending on the device or browser you've used to read this article, your scroll performance might be on the floor and your fans may have kicked up a notch. At the time of writing, blend modes in browsers are still pretty resource heavy. For more complex effects, with several layers of compositing, you will see a real performance hit. Add any CSS animations or transitions to the mix and it will really tank - particularly in Safari. After a bit of tweaking I eventually managed to claw back a bit of a performance boost by using backface-visibility: hidden, but my initial impulse was to force GPU rendering by using the trusty transform: translateZ(0); hack. Sadly, adding a transform revealed another quirk to be aware of. Due to the reliance on background-attachment: fixed, applying CSS transforms to the shader can cause some strange side-effects. In Chrome it broadly works, but the gradients may appear offset depending on the transforms applied. Firefox on the other hand, will simply ignore the fixed positioning and your gradients will totally appear static. I'm sure there are ways and means around that, but they'd likely be testing the spirit of doing all of this without using JS. All-in-all this was really fun to explore. Sure, we can't get quite the same level of fidelity as GLSL, but for simpler effects this technique is a great alternative to introducing extra libraries to your project. As great as these effects can look though, I think that for now this is very much a case of: just because you can, doesn't mean you should. Until CSS filters and blend modes become more performant (or until browsers allow linking GLSL filters directly from CSS 🤞) restraint and subtlety might be the best way to go. ## Image credits The demos in this article use a collection of images or composited textures made available by the Unsplash photographers below. Please go check out their beautiful work. - Bokeh lights by Hello I'm Nik - Silhouette of pine trees under stars by Joshua Woroniecki - Aurora by Johannes Groll - Lake under blue sky by Juan Davila - Vintage dashboard by Wassim Chouak - Light leak by Sean Sinclair - Scratches by Dan Cristian Pădureț