Tag: Recreating

Recreating Game Elements for the Web: The Among Us Card Swipe

As a web developer, I pay close attention to the design of video games. From the HUD in Overwatch to the catch screen in Pokemon Go to hunting in Oregon Trail, games often have interesting mechanics and satisfying interactions, many of which inspire my own coding games at Codepip.

Beyond that, implementing small slices of these game designs on a web stack is a fun, effective way to broaden your skills. By focusing on a specific element, your time is spent working on an interesting part without having to build out a whole game with everything that entails. And even in this limited scope, you often get exposed to new technologies and techniques that push on the boundaries of your dev knowledge.

As a case study for this idea, I’ll walk you through my recreation of the card swipe from Among Us. For anyone in the dark, Among Us is a popular multiplayer game. Aboard a spaceship, crewmates have to deduce who among them is an imposter. All the while, they complete mundane maintenance tasks and avoid being offed by the imposter.

The card swipe is the most infamous of the maintenance tasks. Despite being simple, so many players have struggled with it that it’s become the stuff of streams and memes.

Here’s my demo

This is my rendition of the card swipe task:

Next, I’ll walk you through some of the techniques I used to create this demo.

Swiping with mouse and touch events

After quickly wireframing the major components in code, I had to make the card draggable. In the game, when you start dragging the card, it follows your pointer’s position horizontally, but stays aligned with the card reader vertically. The card has a limit in how far past the reader it can be dragged to its left or right. Lastly, when you lift your mouse or finger, the card returns to its original position.

All of this is accomplished by assigning functions to mouse and touch events. Three functions are all that‘s needed to handle mouse down, mouse move, and mouse up (or touch start, touch move, and touch end if you‘re on a touchscreen device). Here’s the skeleton of that JavaScript code:

const card = document.getElementById('card'); const reader = document.getElementById('reader'); let active = false; let initialX;  // set event handlers document.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); document.addEventListener('touchstart', dragStart); document.addEventListener('touchmove', drag); document.addEventListener('touchend', dragEnd);  function dragStart(e) {   // continue only if drag started on card   if (e.target !== card) return;    // get initial pointer position   if (e.type === 'touchstart') {     initialX = e.touches[0].clientX;   } else {     initialX = e.clientX;   }    active = true; }  function drag(e) {   // continue only if drag started on card   if (!active) return;    e.preventDefault();      let x;    // get current pointer position   if (e.type === 'touchmove') {     x = e.touches[0].clientX - initialX;   } else {     x = e.clientX - initialX;   }    // update card position   setTranslate(x); }  function dragEnd(e) {   // continue only if drag started on card   if (!active) return;    e.preventDefault();      let x;    // get final pointer position   if (e.type === 'touchend') {     x = e.touches[0].clientX - initialX;   } else {     x = e.clientX - initialX;   }    active = false;      // reset card position   setTranslate(0); }  function setTranslate(x) {   // don't let card move too far left or right   if (x < 0) {     x = 0;   } else if (x > reader.offsetWidth) {     x = reader.offsetWidth;   }    // set card position on center instead of left edge   x -= (card.offsetWidth / 2);      card.style.transform = 'translateX(' + x + 'px)'; }

Setting status with performance.now()

Next, I had to determine whether the card swipe was valid or invalid. For it to be valid, you must drag the card across the reader at just the right speed. Didn’t drag it far enough? Invalid. Too fast? Invalid. Too slow? Invalid.

To find if the card has been swiped far enough, I checked the card’s position relative to the right edge of the card reader in the function dragEnd:

let status;  // check if card wasn't swiped all the way if (x < reader.offsetWidth) {   status = 'invalid'; }  setStatus(status);

To measure the duration of the card swipe, I set start and end timestamps in dragStart and dragEnd respectively, using performance.now().

function setStatus(status) {    // status is only set for incomplete swipes so far   if (typeof status === 'undefined') {      // timestamps taken at drag start and end using performance.now()     let duration = timeEnd - timeStart;      if (duration > 700) {       status = 'slow';     } else if (duration < 400) {       status = 'fast';     } else {       status = 'valid';     }   }    // set [data-status] attribute on reader   reader.dataset.status = status; } 

Based on each condition, a different value is set on the reader’s data-status attribute. CSS is used to display the relevant message and illuminate either a red or green light.

#message:after {   content: "Please swipe card"; }  [data-status="invalid"] #message:after {   content: "Bad read. Try again."; }  [data-status="slow"] #message:after {   content: "Too slow. Try again."; }  [data-status="fast"] #message:after {   content: "Too fast. Try again."; }  [data-status="valid"] #message:after {   content: "Accepted. Thank you."; }  .red {   background-color: #f52818;   filter: saturate(0.6) brightness(0.7); }  .green {   background-color: #3dd022;   filter: saturate(0.6) brightness(0.7); }  [data-status="invalid"] .red, [data-status="slow"] .red, [data-status="fast"] .red, [data-status="valid"] .green {   filter: none; }

Final touches with fonts, animations, and audio

With the core functionality complete, I added a few more touches to get the project looking even more like Among Us.

First, I used a free custom font called DSEG to imitate the segmented type from old LCDs. All it took was hosting the files and declaring the font face in CSS.

@font-face {   font-family: 'DSEG14Classic';   src: url('../fonts/DSEG14-Classic/DSEG14Classic-Regular.woff2') format('woff2'),        url('../fonts/DSEG14-Classic/DSEG14Classic-Regular.woff') format('woff'),        url('../fonts/DSEG14-Classic/DSEG14Classic-Regular.ttf') format('truetype'); }

Next, I copied the jitter animation of the text in the original. Game developers often add subtle animations to breath life into an element, like making a background drift or a character, well, breathe. To achieve the jitter, I defined a CSS animation:

@keyframes jitter {   from {     transform: translateX(0);   }   to {     transform: translateX(5px);   } }

At this point, the text glides smoothly back and forth. Instead, what I want is for it to jump back and forth five pixels at a time. Enter the steps() function:

#message {   animation: jitter 3s infinite steps(2); }

Finally, I added the same audio feedback as used in Among Us.

let soundAccepted = new Audio('./audio/CardAccepted.mp3'); let soundDenied = new Audio('./audio/CardDenied.mp3');  if (status === 'valid') {   soundAccepted.play(); } else {   soundDenied.play(); }

Sound effects are often frowned upon in the web development world. A project like this an opportunity to run wild with audio.

And with that, the we’re done! Here’s that demo again:

Try your own

Given how standardized the web has become in look and feel, this approach of pulling an element from a game and implementing it for the web is a good way to break out of your comfort zone and try something new.

Take this Among Us card swipe. In a small, simple demo, I tinkered with web fonts and animations in CSS. I monkeyed with input events and audio in JavaScript. I dabbled with an unconventional visual style.

Now it’s time for you to survey interesting mechanics from your favorite games and try your hand at replicating them. You might be surprised what you learn.


The post Recreating Game Elements for the Web: The Among Us Card Swipe appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,

Re-Creating the Porky Pig Animation from Looney Tunes in CSS

You know, Porky Pig coming out of those red rings announcing the end of a Looney Tunes cartoon. We’ll get there, but first we need to cover some CSS concepts.

Everything in CSS is a box, or rectangle. Rectangles stack, and can be displayed on top of, or below, other rectangles. Rectangles can contain other rectangles and you can style them such that the inner rectangle is visible outside the outer rectangle (so they overflow) or that they’re clipped by the outer rectangle (using overflow: hidden). So far, so good.

What if you want a rectangle to be visible outside its surrounding rectangle, but only on one side. That’s not possible, right?

The first rectangle contains an inner element that overflows both the top and bottom edges with the text "Possible" below it. The second rectangle clips the inner element on both sides, with "Also possible" below it. The third rectangle clips the inner element on the bottom, but shows it overflowing at the top, with the text "...Not possible" below it.

Perhaps, when you look at the image above, the wheels start turning: What if I copy the inner rectangle and clip half of it and then position it exactly?. But when it comes down to it, you can’t choose to have an element overflow at the top but clip at the bottom.

Or can you?

3D transforms

Using 3D transforms you can rotate, transform, and translate elements in 3D space. Here’s a group of practical examples I gathered showcasing some possibilities.

For 3D transforms to do their thing, you need two CSS properties:

  • perspective, using a value in pixels, to determine how pronounced the 3D effect is
  • transform-style: preserve-3d, to tell the browser to keep elements positioned in 3D space.

Even with the good support that 3D transforms have, you don’t see 3D transforms ‘in the wild’ all that much, sadly. Websites are still a “2D” thing, a flat page that scrolls. But as I started playing around with 3D transforms and scouting examples, I found one that was by far the most interesting as far as 3D transforms go:

Three planes floating above each other in 3D space

The image clearly shows three planes but this effect is achieved using a single <div>. The two other planes are the ::before and ::after pseudo-elements that are moved up and down respectively, using translate(), to stack on top of each other in 3D space. What is noticeable here is how the ::after element, that normally would be positioned on top of an element, is behind that element. The creator was able to achieve this by adding transform: translateZ(-1px);.

Even though this was one of many 3D transforms I had seen at this point, it was the first one that made me realize that I was actually positioning elements in 3D space. And if I can do that, I can also make elements intersect:

Two planes intersecting each other in 3D space

I couldn’t think of how this sort of thing would be useful, but then I saw the Porky Pig cartoon animation. He emerges from behind the bottom frame, but his face overlaps and stacks on top of the top edge of the same frame — the exact same sort of clipping situation we saw earlier. That’s when my wheels started turning. Could I replicate that effect using just CSS? And for extra credit, could I replicate it using a single <div>?

I started playing around and relatively quickly had this to show for it:

An orange rectangle that intersects through a blue frame. At the top of the image it's above the frame and at the bottom of the image it's below the blue frame.

Here we have a single <div> with its ::before and an ::after pseudo-elements. The div itself is transparent, the ::before has a blue border and the ::after has been rotated along the x-axis. Because the div has perspective, everything is positioned in 3D and, because of that, the ::after pseudo-element is above the border at the top edge of the frame and behind the border at the bottom edge of the frame.

Here’s that in code:

div {   transform: perspective(3000px);   transform-style: preserve-3d;   position: relative;   width: 200px;   height: 200px; }  div::before {   content: "";   width: 100%;   height: 100%;   border:10px solid darkblue; }  div::after {   content: "";   position: absolute;   background: orangered;   width: 80%;   height: 150%;   display: block;   left: 10%;   bottom: -25%;   transform: rotateX(-10deg); }

With perspective, we can determine how far a viewer is from “z=0” which we can consider to be the “horizon” of our CSS 3D space. The larger the perspective, the less pronounced the 3D effect, and vice versa. For most 3D scenes, a perspective value between 500 and 1,000 pixels works best, though you can play around with it to get the exact effect you want. You can compare this with perspective drawing: If you draw two horizon points close together, you get a very strong perspective; but if they’re far apart, then things appear flatter.

From rectangles to cartoons

Rectangles are fun, but what I really wanted to build was something like this:

A film cell of Porky Pig coming out of a circle with the text "That's all folks."

I couldn‘t find or create a nicely cut-out version of Porky Pig from that image, but the Wikipedia page contains a nice alternative, so we’ll use that.

First, we need to split the image up into three parts:

  • <div>: the blue background behind Porky
  • ::after: all the red circles that form a sort of tunnel
  • ::before: Porky Pig himself in all his glory, set as a background image

We’ll start with the <div>. That will be the background as well as the base for the rest of the elements. It’ll also contain the perspective and transform-style properties I called out earlier, along with some sizes and the background color:

div {   transform: perspective(3000px);   transform-style:preserve-3d;   position: relative;   width: 200px;   height: 200px;   background: #4992AD; }

Alright, next up, we‘ll move to the red circles. The element itself has to be transparent because that’s the opening where Porky emerges. So how shall we go about it? We can use a border just like the example earlier in this article, but we only have one border and that can have a solid color. We need a bunch of circles that can accept gradients. We can use box-shadow instead, chaining multiple shadows in the property values. This gets us all of the circles we need, and by using a blur radius value of 0 with a large spread radius, we can create the appearance of multiple “borders.”

box-shadow: <x-offset> <y-offset> <blur-radius> <spread-radius> <color>;

We‘ll use a border-radius that‘s as large as the <div> itself, making the ::before a circle. Then we’ll add the shadows. When we add a few red circles with a large spread and add blurry white, we get an effect that looks very similar to the Porky’s tunnel.

box-shadow: 0 0 20px   0px #fff, 0 0 0  30px #CF331F,             0 0 20px  30px #fff, 0 0 0  60px #CF331F,             0 0 20px  60px #fff, 0 0 0  90px #CF331F,             0 0 20px  90px #fff, 0 0 0 120px #CF331F,             0 0 20px 120px #fff, 0 0 0 150px #CF331F;

Here, we’re adding five circles, where each is 30px wide. Each circle has a solid red background. And, by using white shadows with a blur radius of 20px on top of that, we create the gradient effect.

The background and circles in pure CSS without Porky

With the background and the circles sorted, we’re now going to add Porky. Let’s start with adding him at the spot we want him to end up, for now above the circles.

div::before {   position: absolute;   content: "";   width: 80%;   height: 150%;   display: block;   left: 10%;   bottom: -12%;   background: url("Porky_Pig.svg") no-repeat center/contain; }

You might have noticed that slash in “center/contain” for the background. That’s the syntax to set both the position (center) and size (contain) in the background shorthand CSS property. The slash syntax is also used in the font shorthand CSS property where it’s used to set the font-size and line-height like so: <font-size>/<line-height>.

The slash syntax will be used more in future versions of CSS. For example, the updated rgb() and hsl() color syntax can take a slash followed by a number to indicate the opacity, like so: rgb(0 0 0 / 0.5). That way, there’s not need to switch between rgb() and rgba(). This already works in all browsers, except Internet Explorer 11.

Porky Pig positioned above the circles

Both the size and positioning here is a little arbitrary, so play around with that as you see fit. We’re a lot closer to what we want, but now need to get it so the bottom portion of Porky is behind the red circles and his top half remains visible.

The trick

We need to transpose both the circles as well as Porky in 3D space. If we want to rotate Porky, there are a few requirements we need to meet:

  • He should not clip through the background.
  • We should not rotate him so far that the image distorts.
  • His lower body should be below the red circles and his upper body should be above them.

To make sure Porky doesn‘t clip through the background, we first move the circles in the Z direction to make them appear closer to the viewer. Because preserve-3d is applied it means they also zoom in a bit, but if we only move them a smidge, the zoom effect isn’t noticeable and we end up with enough space between the background and the circles:

transform: translateZ(20px);

Now Porky. We’re going to rotate him around the X-axis, causing his upper body to move closer to us, and the lower part to move away. We can do this with:

transform: rotateX(-10deg);

This looks pretty bad at first. Porky is partially hidden behind the blue background, and he’s also clipping through the circles in a weird way.

Porky Pig partially clipped by the background and the circles

We can solve this by moving Porky “closer” to us (like we did with the circles) using translateZ(), but a better solution is to change the position of our rotation point. Right now it happens from the center of the image, causing the lower half of the image to rotate away from us.

If we move the starting point of the rotation toward the bottom of the image, or even a little bit below that, then the entirety of the image rotates toward us. And because we already moved the circles closer to us, everything ends up looking as it should:

transform: rotateX(-10deg); transform-origin: center 120%;
Porky Pig emerges from the circle, with his legs behind the circles but his head above them.

To get an idea of how everything works in 3D, click “show debug” in the following Pen:

Animation

If we keep things as they are — a static image — then we wouldn’t have needed to go through all this trouble. But when we animate things, we can reveal the layering and enhance the effect.

Here‘s the animation I’m going for: Porky starts out small at the bottom behind the circles, then zooms in, emerging from the blue background over the red circles. He stays there for a bit, then moves back out again.

We’ll use transform for the animation to get the best performance. And because we’re doing that, we need to make sure we keep the rotateX in there as well.

@keyframes zoom {   0% {     transform: rotateX(-10deg) scale(0.66);   }   40% {     transform: rotateX(-10deg) scale(1);   }   60% {     transform: rotateX(-10deg) scale(1);   }   100% {     transform: rotateX(-10deg) scale(0.66);   } }

Soon, we’ll be able to directly set different transforms, as browsers have started implementing them as individual CSS properties. That means that repeating that rotateX(-10deg) will eventually be unnecessary; but for now, we have a little bit of duplication.

We zoom in and out using the scale() function and, because we’ve already set a transform-origin, scaling happens from the center-bottom of the image, which is precisely the effect we want! We’re animating the scale up to 60% of Porky’s actual size, we have the little break at the largest point, where he fully pops out of the circle frame.

The animation goes on the ::before pseudo-element. To make the animation look a little more natural, we’re using an ease-in-out timing function, which slows down the animation at the start and end.

div::before {   animation-name: zoom;   animation-duration: 4s;   animation-iteration-count: infinite;   animation-fill-mode:forwards;   animation-timing-function: ease-in-out; }

What about reduced motion?

Glad you asked! For people who are sensitive to animations and prefer reduced or no motion, we can reach for the prefers-reduced-motion media query. Instead of removing the full animation, we’ll target those who prefer reduced motion and use a more subtle fade effect rather than the full-blown animation.

@media (prefers-reduced-motion: reduce) {    @keyframes zoom {     0% {       opacity:0;     }     100% {       opacity: 1;     }   }    div::before {     animation-iteration-count: 1;   } }

By overwriting the @keyframes inside a media query, the browser will automatically pick it up. This way, we still accentuate the effect of Porky emerging from the circles. And by setting animation-iteration-count to 1, we still let people see the effect, but then stop to prevent continued motion.

Finishing touches

Two more things we can do to make this a bit more fun:

  • We can create more depth in the image by adding a shadow behind Porky that grows as he emerges and appears to zoom in closer to the view.
  • We can turn Porky as he moves, to embellish the pop-out effect even further.

That second part we can implement using rotateZ() in the same animation. Easy breezy.

But the first part requires an additional trick. Because we use an image for Porky, we can’t use box-shadow because that creates a shadow around the box of the ::before pseudo-element instead of around the shape of Porky Pig.

That’s where filter: drop-shadow() comes to the rescue. It looks at the opaque parts of the element and adds a shadow to that instead of around the box.

@keyframes zoom {   0% {     transform: rotateX(-10deg) scale(0.66);     filter: drop-shadow(-5px 5px 5px rgba(0,0,0,0));   }   40% {     transform: rotateZ(-10deg) rotateX(-10deg) scale(1);     filter: drop-shadow(-10px 10px 10px rgba(0,0,0,0.5));   }    60% {     transform: rotateZ(-10deg) rotateX(-10deg) scale(1);     filter: drop-shadow(-10px 10px 10px rgba(0,0,0,0.5));   }    100% {     transform: rotateX(-10deg) scale(0.66);     filter: drop-shadow(-5px 5px 5px rgba(0,0,0,0));   } }

And that‘s how I re-created the Looney Tunes animation of Porky Pig. All I can say now is, “That’s all Folks!”


The post Re-Creating the Porky Pig Animation from Looney Tunes in CSS appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

Recreating the CodePen Gutenberg Embed Block for Sanity.io

Chris recently put out a neat CodePen Embed Block for the Gutenberg editor in WordPress. It allows you to embed a Pen just by dropping in its URL. From there, you get access to control the size, theme, and the default tabs that render on initial load. Super neat!

Having a live preview of the embedded Pen while writing is so handy!

But it got me thinking: How difficult would it be to recreate it with Sanity Studio’s Portable Text editor? (Spoiler: Not that difficult). Since I already knew how to do it, it took me under seven minutes from start to finish. This tutorial takes you through how to get up and running with a studio, and how to add the schemas and the custom preview component for a CodePen embed.

That felt so cool that I want to teach you how to do it as well. Let’s dive right into it.

Getting Sanity Studio up and running locally

First, you’ll need to install Sanity Studio locally on your machine. In this tutorial we will be using the blog studio that you can initiate from the command line, but you can also check out the different starters on sanity.io/create. You should be able to tag along with one of those too.

This tutorial assumes that you have a bit of knowledge of JavaScript. It will use a bit of React, but only a small part. You should have installed node and npm if you haven’t already.

Oh, and you’ll want the Sanity CLI, which you can snag with the command line:

npm install --global @sanity/cli

Once the installation is done, you can initiate a new Sanity Studio with a new project by running the command sanity init. It will let you log in with your Google or GitHub account (or make a new account with an email/password). Give your project a name and follow the instructions. When given the options for a project template, choose the blog one:

? Select project template   Movie project (schema + sample data)   E-commerce (schema + sample data) ❯ Blog (schema)   Clean project with no predefined schemas

After completing the steps, change directory (cd) into the new project folder and open it in your favorite code editor. To start the developer server that will also hot reload your studio when you make changes, run sanity start. To stop this server, you press ctrl + C in most command line tools.

Adding the schemas for a CodePen embed

Schemas define which document types that are available in the Studio, and which input fields they have. These schemas are defined in JavaScript objects that you import into the schemas.js file, where they are exported as a function that the Studio translates into its UI. There’s a lot you can do with these schemas, but in this tutorial, we will keep it reasonably simple.

Start with adding a new file inside /yourproject/schemas called codepen.js. Then type in this code:

export default {   name: "codepen",   type: "object",   title: "CodePen Embed",   fields: [     {       name: "url",       type: "url",       title: "CodePen URL"     }   ] };

Then you can go to /yourproject/schemas/schema.js and add the following two lines of code to it:

import createSchema from "part:@sanity/base/schema-creator"; import schemaTypes from "all:part:@sanity/base/schema-type";  import blockContent from "./blockContent"; import category from "./category"; import post from "./post"; import author from "./author"; import codepen from "/codepen.js"; // <= first import the object  export default createSchema({   name: "default",   types: schemaTypes.concat([     post,     author,     category,     blockContent,     codepen // <= add it to the schema types array   ]) });

So what did we just do? Well, we have now made this CodePen object available as a type in other schemas in the Studio. In other words, you can now add type: 'codepen' to get those fields anywhere else in the schema code where you add fields. Adding this type to the rich text field is also our next step. Hang on!

Adding the CodePen field to the rich text editor

Before diving into the code bit, let us take a step back and look at what is going on in terms of the data formats we operate with, and how WordPress and Sanity differ slightly.

While Gutenberg stores rich text as JSON in its runtime (which is great!), what developers end up dealing with is mostly this content as HTML and JSON objects inside of HTML comments.

Sanity stores and distributes rich text content as Portable Text, which developers then serializes in their frontends. That means that you get fine-grained control over how rich text content is rendered by letting you use custom components for your favorite framework, either it's ReactVueSvelte, or .NETPHP, or even Markdown.

In other words, you store your content as structured data in Sanity’s backend, and then decide how you want to use the data inside your frontend components. But enough exposition, let's get back to the code!

Open /schemas/blockContent.js and notice that it's of the type array. Yes, rich text is an array of different types, where one of them has to be of the block type (in which text paragraphs are stored). So the simplest way of making rich text is the following schema definition:

export default {   name: "body",   type: "array",   title: "Body",   of: [     {       type: "block"     }   ] };

Now, blockContent.js has a bunch of more stuff. You can see styles, lists, marks, and so on. All defining which properties should be available for the author. In the top array, there are two types block and image. We are going to add the third one, codepen:

export default {   title: "Block Content",   name: "blockContent",   type: "array",   of: [     {       type: "block"       // ...     },     {       type: "image",       options: { hotspot: true }     },     {       type: "codepen"     }   ] };

Save the file, and that's it! If you now run sanity start in your command line (assuming you haven't already), and open the Studio on https://localhost:3333, you should be able to find your new field in the rich text editor under the "post" type:

Sanity Studio with a CodePen button in the Rich Text editor.

If you try out the new button, you'll get a modal with the URL field that you defined in the previous section. Feel free to add the URL from a cool CodePen that you have found. We will use this one from the legendary Sara Drasner; it's pretty cool.

Just showing the URL value in the editor isn't especially inspiring, though. So let's go ahead and add the actual CodePen embed so we can interact with it directly in the editor!

Adding the CodePen embed as a preview

Open /yourproject/schemas/codepen.js again. Now we are going to make a small React component for our preview. Start by importing React in the top, and the boilerplate for the React component that we will turn into the embed:

import React from "react";  const CodePenPreview = ({ value }) => {   return <pre>{JSON.stringify(value, null, 2)}</pre>; };  export default {   name: "codepen",   type: "object",   title: "CodePen Embed",   fields: [     {       name: "url",       type: "url",       title: "CodePen URL"     }   ] };

The JSON.stringify stuff is a temporary little way of outputting the incoming data in a readable manner. You could also use console.log(value), but who has time to open the developer console?

Now you must tell Sanity how to use this component for the preview. As well as which of the fields in the object it should select for the value in the preview component.

import React from "react";  const CodePenPreview = ({ value }) => {   return <pre>{JSON.stringify(value, null, 2)}</pre>; };  export default {   name: "codepen",   type: "object",   title: "CodePen Embed",   preview: {     select: {       url: "url"     },     component: CodePenPreview   },   fields: [     {       name: "url",       type: "url",       title: "CodePen URL"     }   ] };

The editor should look something like this after you saved your changes:

Cool! Now we want to take the url value and somehow integrate it with a CodePen embed. The easiest way to go about this is to fit the markup for CodePen’s iFrame embed, and fit into our preview component in React.

The original iFrame element will look like this:

<iframe height="265" style="width: 100%;" scrolling="no" title="React Animated Page Transitions" src="https://codepen.io/sdras/embed/gWWQgb?height=265&theme-id=dark&default-tab=js,result" frameborder="no" allowtransparency="true" allowfullscreen="true">   See the Pen <a href='https://codepen.io/sdras/pen/gWWQgb'>React Animated Page Transitions</a> by Sarah Drasner   (<a href='https://codepen.io/sdras'>@sdras</a>) on <a href='https://codepen.io'>CodePen</a>. </iframe>

If we paste this snippet into our preview component, it will almost work. In order to make it JSX-compatible you'll have to some few changes to some of the HTML-attributes. Make sure that you change:

  • style="width: 100%;" to style={{width: "100%"}}
  • frameborder="no" to frameBorder="no"
  • allow-transparency="true" to allowTransparency
  • allow-fullscreen="true" to allowFullScreen

You can remove the content (links, etc.) inside of the iframe, because it isn't particularly useful inside the studio. What we should end up with is something like this:

import React from "react"; import Codepen from "react-codepen-embed";  const CodePenPreview = ({ value }) => {   return (     <iframe       height="265"       style={{ width: '100%' }}       scrolling="no"       title="React Animated Page Transitions"       src="https://codepen.io/sdras/embed/gWWQgb?height=370&theme-id=dark&default-tab=js,result"       frameBorder="no"       allowTransparency       allowFullScreen     />); };  // ...

When saved, we should be able to see the CodePen embed inside the rich text editor:

Notice that the iFrame has an embed URL with some parameters for how it should be displayed. Of course, we could've asked someone to dive into CodePen to obtain this URL, but it's probably better for to use the regular one. We'll take the effort to reassemble into what we need:

The last part is to take the URL from the field, and get the hash and user out of it.

We split the URL string on forward slashes into an array. Then we use array destructuring to assign the different array elements to a variable. Since we only need the user and the hash we leave the other positions empty. This method isn't bulletproof, as it assumed a specific format for the URL, but it works for this example. Then we reassemble the embedUrl by using template literals.

import React from "react";  const CodePenPreview = ({ value }) => {   const { url } = value;   const splitURL = url.split("/");   // [ 'https:', '', 'codepen.io', 'sdras', 'pen', 'gWWQgb' ]   const [, , , user, , hash] = splitURL;   const embedUrl = `https://codepen.io/$ {user}/embed/$ {hash}?height=370&theme-id=dark&default-tab=result`;   return (     <iframe       height="370"       style={{ width: '100%' }}       scrolling="no"       title="CodePen Embed"       src={embedUrl}       frameBorder="no"       allowTransparency       allowFullScreen     />   ); }; // ...

Save the changes and voilá; we're pretty much done with the custom CodePen block!

Taking it further

Now, you probably noticed that Chris had put more settings into his custom block. Nothing is stopping us from doing the same! If we look up the documentation for the React CodePen embed component that we installed, we'll find a table of properties that it can take. We can add these as fields in the schema definition. For example, if we wanted to add the themeId, we could do it as follows:

import React from "react"; import Codepen from "react-codepen-embed";  const CodePenPreview = ({ value }) => {   const { url, themeId = "dark" } = value; // <= add themeId here, default it to "dark"   const splitURL = url.split("/");   // [ 'https:', '', 'codepen.io', 'sdras', 'pen', 'gWWQgb' ]   const [, , , user, , hash] = splitURL;   const embedUrl = `https://codepen.io/$ {user}/embed/$ {hash}?height=370&theme-id=$ {themeId}&default-tab=result`; // <= add themeId here   return (     <iframe       height="370"       style={{ width: '100%' }}       scrolling="no"       title="CodePen Embed"       src={embedUrl}       frameBorder="no"       allowTransparency       allowFullScreen     />   ); };  export default {   name: "codepen",   type: "object",   title: "CodePen Embed",   preview: {     select: {       url: "url",       themeId: "themeId" // <= add themeId here     },     component: CodePenPreview   },   fields: [     {       name: "url",       type: "url",       title: "CodePen URL"     },     // Add the new field below     {       name: "themeId",       type: "string",       title: "Theme ID",       description: 'You can use "light" and "dark" also.'     }   ] };

Conclusion

We just looked at how schemas for Sanity Studio work, and learned how to make previews for custom components to boot! Hopefully, you now know enough to make pretty much any custom component with a preview using these same principles. If you do, I would love to know about it either on Twitter or in the comments.

The post Recreating the CodePen Gutenberg Embed Block for Sanity.io appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]

Recreating the Facebook Messenger Gradient Effect with CSS

One Sunday morning, I woke up a little earlier than I would’ve liked to, thanks to the persistent buzzing of my phone. I reached out, tapped into Facebook Messenger, and joined the conversation. Pretty soon my attention went from the actual conversations to the funky gradient effect of the message bubbles containing them. Let me show you what I mean:

This is a new feature of Messenger, which allows you to choose a gradient instead of a plain color for the background of the chat messages. It’s currently available on the mobile application as well as Facebook’s site, but not yet on Messenger’s site. The gradient appears “fixed” so that chat bubbles appear to change background color as they scroll vertically.

I thought this looked like something that could be done in CSS, so… challenge accepted!

Let’s walk through my thought process as I attempted to recreate it and explain the CSS features that were used to make it work. Also, we’ll see how Facebook actually implemented it (spoiler alert: not the way I did) and how the two approaches compare.

Getting our hands dirty

First, let’s look at the example again to see what exactly it is that we’re trying to achieve here.

In general, we have a pretty standard messaging layout: messages are divided into bubbles going from top to bottom, ours on the right and the other people in the chat on the left. The ones on the left all have a gray background color, but the ones on the right look like they’re sharing the same fixed background gradient. That’s pretty much it!

Step 1: Set up the layout

This part is pretty simple: let’s arrange the messages in an ordered list and apply some basic CSS to make it look more like an actual messaging application:

<ol class="messages">   <li class="ours">Hi, babe!</li>   <li class="ours">I have something for you.</li>   <li>What is it?</li>   <li class="ours">Just a little something.</li>   <li>Johnny, it’s beautiful. Thank you. Can I try it on now?</li>   <li class="ours">Sure, it’s yours.</li>   <li>Wait right here.</li>   <li>I’ll try it on right now.</li> </ol>

When it comes to dividing the messages to the left and the right, my knee-jerk reaction was to use floats. We could use float: left for messages on the left and float: right for messages on the right to have them stick to different edges. Then, we’d apply clear: both to on each message so they stack. But there’s a much more modern approach — flexbox!

We can use flexbox to stack the list items vertically with flex-direction: column and tell all the children to stick to the left edge (or “align the cross-start margin edges of the flex children with cross-start margin edges of the lines,” if you prefer the technical terms) with align-items: flex-start. Then, we can overwrite the align-items value for individual flex items by setting align-self: flex-end on them.

What, you mean you couldn’t visualize the code based on that? Fine, here’s how that looks:

.messages {   /* Flexbox-specific styles */   display: flex;   flex-direction: column;   align-items: flex-start;    /* General styling */   font: 16px/1.3 sans-serif;   height: 300px;   list-style-type: none;   margin: 0 auto;   padding: 8px;   overflow: auto;   width: 200px; }  /* Default styles for chat bubbles */ .messages li {   background: #eee;   border-radius: 8px;   padding: 8px;   margin: 2px 8px 2px 0; }  /* Styles specific to our chat bubbles */ .messages li.ours {   align-self: flex-end; /* Stick to the right side, please! */   margin: 2px 0 2px 8px; }

Some padding and colors here and there and this already looks similar enough to move on to the fun part.

Step 2: Let’s color things in!

The initial idea for the gradient actually came to me from this tweet by Matthias Ott (that Chris recreated in another post):

The key clue here is mix-blend-mode, which is a CSS property that allows us to control how the content of an element blends in with what’s behind it. It’s a feature that has been present in Photoshop and other similar tools for a while, but is fairly new to the web. There’s an almanac entry for the property that explains all of its many possible values.

One of the values is screen: it takes the values of the pixels of the background and foreground, inverts them, multiplies them, and inverts them once more. This results in a color that is brighter than the original background color.

The description can seem a little confusing, but what it essentially means is that if the background is monochrome, wherever the background is black, the foreground pixels are shown fully and wherever it is white, white remains.

With mix-blend-mode: screen;</code on the foreground, we'll see more of the foreground as the background is darker.</figcaption></figure>

So, for our purposes, the background will be the chat window itself and the foreground will contain an element with the desired gradient set as the background that’s positioned over the background. Then, we apply the appropriate blend mode to the foreground element and restyle the background. We want the background to be black in places where we want the gradient to be shown and white in other places, so we’ll style the bubbles by giving them a plain black background and white text. Oh, and let’s remember to add <code>pointer-events: none to the foreground element so the user can interact with the underlying text.

At this point, I also changed the original HTML a little. The entire chat is a wrapper in an additional container that allows the gradient to stay “fixed” over the scrollable part of the chat:

.messages-container:after {   content: '';   background: linear-gradient(rgb(255, 143, 178) 0%, rgb(167, 151, 255) 50%, rgb(0, 229, 255) 100%);   position: absolute;   left: 0;   top: 0;   height: 100%;   width: 100%;   mix-blend-mode: screen;   pointer-events: none; }  .messages li {   background: black;   color: white;   /* rest of styles */ }

The result looks something like this:

The gradient applied to the chat bubbles

Step 3: Exclude some messages from the gradient

Now the gradient is being shown where the text bubbles are under it! However, we only want it to be shown over our bubbles — the ones along the right edge. A hint to how that can be achieved is hidden in MDN’s description of the mix-blend-mode property:

The mix-blend-mode CSS property sets how an element’s content should blend with the content of the element’s parent and the element’s background.

That’s right! The background. Of course, the effect only takes into account the HTML elements that are behind the current element and have a lower stack order. Fortunately, the stacking order of elements can easily be changed with the z-index property. So all we have to do is to give the chat bubbles on the left a higher z-index than that of the foreground element and they will be raised above it, outside of the influence of mix-blend-mode! Then we can style them however we want.

The gradient applied to the chat bubbles.

Let’s talk browser support

At the time of writing, mix-blend-mode is not supported at all in Internet Explorer and Edge. In those browsers, the gradient is laid over the whole chat and others’ bubbles appear on top of it, which is not an ideal solution.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

Chrome Opera Firefox IE Edge Safari
41 29 32 No No TP

Mobile / Tablet

iOS Safari Opera Mobile Opera Mini Android Android Chrome Android Firefox
12.2 46 No 67 71 64

So, this is what we get in unsupported browsers:

How browsers that don’t support mix-blend-mode render the chat.

Fortunately, all the browsers that support mix-blend-mode also support CSS Feature Queries. Using them allows us to write fallback styles for unsupported browsers first and include the fancy effects for the browsers that support them. This way, even if a user can’t see the full effect, they can still see the whole chat and interact with it:

A simplified UI for older browsers, falling back to a plain cyan background color.

Here’s the final Pen with the full effect and fallback styles:

See the Pen
Facebook Messenger-like gradient coloring in CSS
by Stepan Bolotnikov (@Stopa)
on CodePen.

Now let’s see how Facebook did it

Turns out that Facebook’s solution is almost the opposite of what we’ve covered here. Instead of laying the gradient over the chat and cutting holes in it, they apply the gradient as a fixed background image to the whole chat. The chat itself is filled with a whole bunch of empty elements with white backgrounds and borders, except where the gradient should be visible.

The final HTML rendered by the Facebook Messenger React app is pretty verbose and hard to navigate, so I recreated a minimal example to demonstrate it. A lot of the empty HTML elements can be switched for pseudo-elements instead:

See the Pen
Facebook Messenger-like gradient coloring in CSS: The Facebook Way
by Stepan Bolotnikov (@Stopa)
on CodePen.

As you can see, the end result looks similar to the mix-blend-mode solution, but with a little bit of extra markup. Additionally, their approach provides more flexibility for rich content, like images and emojis . The mix-blend-mode approach doesn’t really work if the background is anything but monochrome and I haven’t been able to come up with a way to “raise” inner content above the gradient or get around this limitation in another way.

Because of this limitation, it’s wiser to use Facebook’s approach in an actual chat application. Still, our solution using mix-blend-mode showcases an interesting way to use one of the most under-appreciated CSS properties in modern web design and hopefully it has given you some ideas on what you could do with it!

The post Recreating the Facebook Messenger Gradient Effect with CSS appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]