Entries by admin

3D Organic Experiments in Houdini

Original Source: http://feedproxy.google.com/~r/abduzeedo/~3/qlJCegjv47g/3d-organic-experiments-houdini

3D Organic Experiments in Houdini
3D Organic Experiments in Houdini

abduzeedo06.07.21

Vinicius Araújo shared a beautiful 3D project on Behance. The compositions feature colorful organic experiments full of what it looks like particles or simulation of vegetation. All of that using the powerful 3D tool Houdini. 

CG color houdini Nature organicImage may contain: outdoorCG color houdini Nature organicImage may contain: tree, plant and reefImage may contain: tree, plant and outdoorImage may contain: abstractImage may contain: fireworks and outdoor objectImage may contain: abstractCG color houdini Nature organicImage may contain: animal and colorfulImage may contain: tree, plant and reefImage may contain: fireworks and outdoor object

For more information make sure to check out Vinicius on 

Behance
Instagram
Website


Trigonometry in CSS and JavaScript: Beyond Triangles

Original Source: http://feedproxy.google.com/~r/tympanus/~3/vR1zyMHn0Ck/

In the previous article we looked at how to clip an equilateral triangle with trigonometry, but what about some even more interesting geometric shapes?

This article is the 3rd part in a series on Trigonometry in CSS and JavaScript:

Introduction to TrigonometryGetting Creative with Trigonometric FunctionsBeyond Triangles (this article)

Plotting regular polygons

A regular polygon is a polygon with all equal sides and all equal angles. An equilateral triangle is one, so too is a pentagon, hexagon, decagon, and any number of others that meet the criteria. We can use trigonometry to plot the points of a regular polygon by visualizing each set of coordinates as points of a triangle.

Polar coordinates

If we visualize a circle on an x/y axis, draw a line from the center to any point on the outer edge, then connect that point to the horizontal axis, we get a triangle.

A circle centrally positioned on an axis, with a line drawn along the radius to form a triangle

If we repeatedly rotated the line at equal intervals six times around the circle, we could plot the points of a hexagon.

A hexagon, made by drawing lines along the radius of the circle

But how do we get the x and y coordinates for each point? These are known as cartesian coordinates, whereas polar coordinates tell us the distance and angle from a particular point. Essentially, the radius of the circle and the angle of the line. Drawing a line from the center to the edge gives us a triangle where hypotenuse is equal to the circle’s radius.

Showing the triangle made by drawing a line from one of the vertices, with the hypotenuse equal to the radius, and the angle as 2pi divided by 6

We can get the angle in degrees by diving 360 by the number of vertices our polygon has, or in radians by diving 2pi radians. For a hexagon with a radius of 100, the polar coordinates of the uppermost point of the triangle in the diagram would be written (100, 1.0472rad) (r, θ).

An infinite number of points would enable us to plot a circle.

Polar to cartesian coordinates

We need to plot the points of our polygon as cartesian coordinates – their position on the x and y axis.

As we know the radius and the angle, we need to calculate the adjacent side length for the x position, and the opposite side length for the y position.

Showing the triangle superimposed on the hexagon, and the equations needed to calculate the opposite and adjacent sides.

Therefore we need Cosine for the former and Sine for the latter:

adjacent = cos(angle) * hypotenuse
opposite = sin(angle) * hypotenuse

We can write a JS function that returns an array of coordinates:

const plotPoints = (radius, numberOfPoints) => {

/* step used to place each point at equal distances */
const angleStep = (Math.PI * 2) / numberOfPoints

const points = []

for (let i = 1; i <= numberOfPoints; i++) {
/* x & y coordinates of the current point */
const x = Math.cos(i * angleStep) * radius
const y = Math.sin(i * angleStep) * radius

/* push the point to the points array */
points.push({ x, y })
}

return points
}

We could then convert each array item into a string with the x and y coordinates in pixels, then use the join() method to join them into a string for use in a clip path:

const polygonCoordinates = plotPoints(100, 6).map(({ x, y }) => {
return `${x}px ${y}px`
}).join(‘,’)

shape.style.clipPath = `polygon(${polygonCoordinates})`

See the Pen Clip-path polygon by Michelle Barker (@michellebarker) on CodePen.dark

This clips a polygon, but you’ll notice we can only see one quarter of it. The clip path is positioned in the top left corner, with the center of the polygon in the corner. This is because at some points, calculating the cartesian coordinates from the polar coordinates is going to result in negative values. The area we’re clipping is outside of the element’s bounding box.

To position the clip path centrally, we need to add half of the width and height respectively to our calculations:

const xPosition = shape.clientWidth / 2
const yPosition = shape.clientHeight / 2

const x = xPosition + Math.cos(i * angleStep) * radius
const y = yPosition + Math.sin(i * angleStep) * radius

Let’s modify our function:

const plotPoints = (radius, numberOfPoints) => {
const xPosition = shape.clientWidth / 2
const yPosition = shape.clientHeight / 2
const angleStep = (Math.PI * 2) / numberOfPoints
const points = []

for (let i = 1; i <= numberOfPoints; i++) {
const x = xPosition + Math.cos(i * angleStep) * radius
const y = yPosition + Math.sin(i * angleStep) * radius

points.push({ x, y })
}

return points
}

Our clip path is now positioned in the center.

See the Pen Clip-path polygon by Michelle Barker (@michellebarker) on CodePen.dark

Star polygons

The types of polygons we’ve plotted so far are known as convex polygons. We can also plot star polygons by modifying our code in the plotPoints() function ever so slightly. For every other point, we could change the radius value to be 50% of the original value:

/* Set every other point’s radius to be 50% */
const radiusAtPoint = i % 2 === 0 ? radius * 0.5 : radius

/* x & y coordinates of the current point */
const x = xPosition + Math.cos(i * angleStep) * radiusAtPoint
const y = yPosition + Math.sin(i * angleStep) * radiusAtPoint

See the Pen Clip-path star polygon by Michelle Barker (@michellebarker) on CodePen.dark

Here’s an interactive example. Try adjusting the values for the number of points and the inner radius to see the different shapes that can be made.

See the Pen Clip-path adjustable polygon by Michelle Barker (@michellebarker) on CodePen.dark

Drawing with the Canvas API

So far we’ve plotted values to use in CSS, but trigonometry has plenty of applications beyond that. For instance, we can plot points in exactly the same way to draw on a <canvas> with Javascript. In this function, we’re using the same function as before (plotPoints()) to create an array of polygon points, then we draw a line from one point to the next:

const canvas = document.getElementById(‘canvas’)
const ctx = canvas.getContext(‘2d’)

const draw = () => {
/* Create the array of points */
const points = plotPoints()

/* Move to starting position and plot the path */
ctx.beginPath()
ctx.moveTo(points[0].x, points[0].y)

points.forEach(({ x, y }) => {
ctx.lineTo(x, y)
})

ctx.closePath()

/* Draw the line */
ctx.stroke()
}

See the Pen Canvas polygon (simple) by Michelle Barker (@michellebarker) on CodePen.dark

Spirals

We don’t even have to stick with polygons. With some small tweaks to our code, we can even create spiral patterns. We need to change two things here: First of all, a spiral requires multiple rotations around the point, not just one. To get the angle for each step, we can multiply pi by 10 (for example), instead of two, and divide that by the number of points. That will result in five rotations of the spiral (as 10pi divided by two is five).

const angleStep = (Math.PI * 10) / numberOfPoints

Secondly, instead of an equal radius for every point, we’ll need to increase this with every step. We can multiply it by a number of our choosing to determine how far apart the lines of our spiral are rendered:

const multiplier = 2
const radius = i * multiplier
const x = xPosition + Math.cos(i * angleStep) * radius
const y = yPosition + Math.sin(i * angleStep) * radius

Putting it all together, our adjusted function to plot the points is as follows:

const plotPoints = (numberOfPoints) => {
const angleStep = (Math.PI * 10) / numberOfPoints
const xPosition = canvas.width / 2
const yPosition = canvas.height / 2

const points = []

for (let i = 1; i <= numberOfPoints; i++) {
const radius = i * 2 // multiply the radius to get the spiral
const x = xPosition + Math.cos(i * angleStep) * radius
const y = yPosition + Math.sin(i * angleStep) * radius

points.push({ x, y })
}

return points
}

See the Pen Canvas spiral – simple by Michelle Barker (@michellebarker) on CodePen.dark

At the moment the lines of our spiral are at equal distance from each other, but we could increase the radius exponentially to get a more pleasing spiral. By using the Math.pow() function, we can increase the radius by a larger number for each iteration. By the golden ratio, for example:

const radius = Math.pow(i, 1.618)
const x = xPosition + Math.cos(i * angleStep) * radius
const y = yPosition + Math.sin(i * angleStep) * radius

See the Pen Canvas spiral by Michelle Barker (@michellebarker) on CodePen.dark

Animation

We could also rotate the spiral, using (using requestAnimationFrame). We’ll set a rotation variable to 0, then on every frame increment or decrement it by a small amount. In this case I’m decrementing the rotation, to rotate the spiral anti-clockwise

let rotation = 0

const draw = () => {
const { width, height } = canvas

/* Create points */
const points = plotPoints(400, rotation)

/* Clear canvas and redraw */
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = ‘#ffffff’
ctx.fillRect(0, 0, width, height)

/* Move to beginning position */
ctx.beginPath()
ctx.moveTo(points[0].x, points[0].y)

/* Plot lines */
points.forEach((point, i) => {
ctx.lineTo(point.x, point.y)
})

/* Draw the stroke */
ctx.strokeStyle = ‘#000000’
ctx.stroke()

/* Decrement the rotation */
rotation -= 0.01

window.requestAnimationFrame(draw)
}

draw()

We’ll also need to modify our plotPoints() function to take the rotation value as an argument. We’ll use this to increment the x and y position of each point on every frame:

const x = xPosition + Math.cos(i * angleStep + rotation) * radius
const y = yPosition + Math.sin(i * angleStep + rotation) * radius

This is how our plotPoints() function looks now:

const plotPoints = (numberOfPoints, rotation) => {
/* 6 rotations of the spiral divided by number of points */
const angleStep = (Math.PI * 12) / numberOfPoints

/* Center the spiral */
const xPosition = canvas.width / 2
const yPosition = canvas.height / 2

const points = []

for (let i = 1; i <= numberOfPoints; i++) {
const r = Math.pow(i, 1.3)
const x = xPosition + Math.cos(i * angleStep + rotation) * r
const y = yPosition + Math.sin(i * angleStep + rotation) * r

points.push({ x, y, r })
}

return points
}

See the Pen Canvas spiral by Michelle Barker (@michellebarker) on CodePen.dark

Wrapping up

I hope this series of articles has given you a few ideas for how to get creative with trigonometry and code. I’ll leave you with one more creative example to delve into, using the spiral method detailed above. Instead of plotting points from an array, I’m drawing circles at a new position on each iteration (using requestAnimationFrame).

See the Pen Canvas spiral IIII by Michelle Barker (@michellebarker) on CodePen.dark

Special thanks to George Francis and Liam Egan, whose wonderful creative work inspired me to delve deeper into this topic!

The post Trigonometry in CSS and JavaScript: Beyond Triangles appeared first on Codrops.

3D Inspiration by Roman Bratschi

Original Source: http://feedproxy.google.com/~r/abduzeedo/~3/ERa9-Wf69o4/3d-inspiration-roman-bratschi

3D Inspiration by Roman Bratschi
3D Inspiration by Roman Bratschi

abduzeedo06.01.21

Roman Bratschi shared a beautiful 3D project using Cinema 4D, X-Particles and Octane render titled N°371-379. In addition to that it seems that Roman also used Photoshop for some post-production. The tools are actually not as important as the compositions and the ideas in general. I am a fan of the idea of glass water drops. 

3D art design inspiration instagram photo photorealistic Realism Still3D art design inspiration instagram photo photorealistic Realism Still3D art design inspiration instagram photo photorealistic Realism Still3D art design inspiration instagram photo photorealistic Realism Still3D art design inspiration instagram photo photorealistic Realism Still3D art design inspiration instagram photo photorealistic Realism Still

For more information make sure to check out Roman on

Behance
Instagram
Website

Or order the prints online — Order here


Passiflora Films Branding and Visual Identity

Original Source: http://feedproxy.google.com/~r/abduzeedo/~3/kQHM05qMZh4/passiflora-films-branding-and-visual-identity

Passiflora Films Branding and Visual Identity
Passiflora Films Branding and Visual Identity

abduzeedo06.02.21

Michał Markiewicz shared a branding and visual identity project for Passiflora Films, a creative production company based out of Warsaw, with over 10 years of production experience in the Polish and Worldwide markets. The scope of my work encompasses comprehensive rebranding including logo, key visual and printed materials.

The Passiflora Films symbol was developed as a result of simplification of the letters ‘P’ and ‘F’. The letters are reflected in basic figures such as the triangle and the circle. 
The whole sign simultaneously discreetly refers to the shape of the camera.
Elements of the symbol have been used in the patterns and are more widely used across the whole visual identity system. Patterns also fulfill the function of a modular grid, in which elements such as a logo, logotype or promotional slogans are placed in various arrangements. 
The logotype was made with a Helvetica Neue typeface, in order not to produce any additional, undesirable associations with the name. 

The company’s recognition and character are to be built through the entire system, the sum of the elements; logo, complementary marks, colors and layout.

branding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland graybranding  minimal simple Production house logo print pattern poland gray

For more information make sure to check out

Dribbble​​​​​​​
Instagram 
Website 
 

Stationery photos by Axela Frank. Also visit  passiflora-films.co


Happy June Vibes For Your Screen (2021 Desktop Wallpapers Edition)

Original Source: https://smashingmagazine.com/2021/05/desktop-wallpaper-calendars-june-2021/

There’s an artist in everyone. Some bring their creative ideas to life with digital tools, others capture the perfect moment with a camera, or love to grab pen and paper to create little doodles or pieces of lettering. And even if you think you’re far away from being an artist, well, it might just be hidden somewhere deep inside of you. So why not explore it?

Since more than ten years, our monthly wallpapers series is the perfect opportunity to do just that: to challenge your creative skills and break out of your daily routine to do something just for fun, fully immersing yourself in the creative process.

For this post, folks from across the globe once again took on the challenge and designed beautiful and unique wallpapers to cater for some good vibes on your screens. All of them come in versions with and without a calendar for June 2021 and can be downloaded for free. At the end of this post, we also compiled some wallpaper goodies from our archives that are just too good to be forgotten. A big thank-you to everyone who shared their designs with us — this post wouldn’t exist without you. Happy June!

You can click on every image to see a larger preview,
We respect and carefully consider the ideas and motivation behind each and every artist’s work. This is why we give all artists the full freedom to explore their creativity and express emotions and experience through their works. This is also why the themes of the wallpapers weren’t anyhow influenced by us but rather designed from scratch by the artists themselves.

Submit a wallpaper!

Did you know that you could get featured in our next wallpapers post, too? We are always looking for creative talent! Join in! →

Mother Nature

“Rain, fog, and winter jackets have been our companions for the last couple of weeks, but we are challenging the gloomy weather with this colorful, vibrant, picturesque calendar design. Spring is the most wonderful time of the year, intense, powerful, and vivid. We hope to summon sunny June with our desktop wallpaper – join us!” — Designed by PopArt Studio from Novi Sad, Serbia.

preview
with calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440
without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Reef Days

“June brings the start of summer full of bright colors, happy memories, and traveling. What better way to portray the goodness of summer than through an ocean folk art themed wallpaper. This statement wallpaper gives me feelings of summer and I hope to share that same feeling with others.” — Designed by Taylor Davidson from Kentucky.

preview
with calendar: 480×800, 1024×1024, 1242×2208, 1280×1024
without calendar: 480×800, 1024×1024, 1242×2208, 1280×1024

Happy Father’s Day

“Whatever you call them, Pa, Dad, Daddy, Pops, they all have one thing in common: they are our superheroes. So, to honor superhero fathers this Father’s Day, we created this super calendar for you to enjoy.” — Designed by Ever Increasing Circles from the United Kingdom.

preview
with calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440
without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Bikini Season

“June reminds me of growing up on the lake. For me, this month is the official start of bikini season! I wanted to create this wallpaper to get everyone excited to put on their favorite suit and find their favorite body of water.” — Designed by Katie Ulrich from the United States.

preview
with calendar: 640×480, 1024×1024, 1680×1200, 1920×1200, 2560×1440
without calendar: 640×480, 1024×1024, 1680×1200, 1920×1200, 2560×1440

Summer Party

Designed by Ricardo Gimenes from Sweden.

preview
with calendar: 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440, 3840×2160
without calendar: 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440, 3840×2160

Happy Squatch

“I just wanted to capture the atmosphere of late spring/early summer in a fun, quirky way that may be reflective of an adventurous person during this time of year.” — Designed by Nick Arcarese from the United States.

preview
with calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440
without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Dancing In The Summer Moonlight

“If you’re happy and you know it – show some dance moves – because summer is finally here!” — Designed by ActiveCollab from the United States.

preview
with calendar: 1080×1920, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1920×1080, 1920×1200, 1920×1440, 2560×1440
without calendar: 1080×1920, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Love Yourz

“J Cole is one of the most inspiring hip hop artists and is known for his famous song ‘Love Yourz’. With all of the negativity and hate we have been having the past year, this is a message to remind people to love your life (love yourz) because there is no such thing as a life that is better than yours.” — Designed by James from Pennsylvania.

preview
with calendar: 640×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×800, 1280×1024, 1366×768, 1400×1050, 1440×900, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1400
without calendar: 640×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×800, 1280×1024, 1366×768, 1400×1050, 1440×900, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1400

Made For Greatness

“Inspiring to more than mediocrity.” — Designed by Katherine Bollinger from the United States.

preview
with calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440
without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Summertime

Designed by Ricardo Gimenes from Sweden.

preview
with calendar: 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440, 3840×2160
without calendar: 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440, 3840×2160

This Is How You Start The Day

“When I think of June, I think of what summer items make you happy. For the 21+ club, summer means grabbing a drink with your friends on a hot summer day. The preferred drink of the summer is bottomless mimosas! And what better time to start drinking than the beginning of the day (responsibly of course)!” — Designed by Carolyn Choates from the United States.

preview
with calendar: 640×480, 800×480, 800×600, 1024×1024, 1280×800, 1280×960, 1280×1024, 1600×1200, 1680×1050, 1920×1200, 1920×1440, 2560×1440
without calendar: 640×480, 800×480, 800×600, 1024×1024, 1280×800, 1280×960, 1280×1024, 1600×1200, 1680×1050, 1920×1200, 1920×1440, 2560×1440

Under The Starlight

“After being cooped up inside for so long, everyone needs a little nature break! And what’s more calming than a crackling campfire on a cool midsummer night, looking up at the shining stars and full moon!” — Designed by Hannah Basham from the United States.

preview
with calendar: 800×600, 1280×720, 1400×1050, 1600×1200, 1920×1080, 2560×1440
without calendar: 800×600, 1280×720, 1400×1050, 1600×1200, 1920×1080, 2560×1440

Oldies But Goodies

Ready for more? Below you’ll find a little best-of from past June wallpapers editions. Enjoy! (Please note that these designs don’t come with a calendar.)

Summer Coziness

“I’ve waited for this summer more than I waited for any other summer since I was a kid. I dream of watermelon, strawberries, and lots of colors.” — Designed by Kate Jameson from the United States.

preview
without calendar: 320×480, 1024×1024, 1280×720, 1680×1200, 1920×1080, 2560×1440

Wildlife Revival

“In these turbulent times for men, we are witnessing the extraordinary rebirth of nature, especially of the wildlife around the planet. Maybe this situation is a blessing in disguise? Either way, one thing is for sure, this planet is the home that we share with all other forms of life and it is our obligation and sacred duty to protect it.” — Designed by LibraFire from Serbia.

preview
without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Summer Is Coming

“Imagine a sunny beach and an endless blue sea. Imagine yourself right there. Is it somewhere in Greece? The Mediterranean? North Africa? Now turn around and start wandering through those picturesque, narrow streets. See all those authentic white houses with blue doors and blue windows? Feel the fragrance of fresh flowers? Stop for a second. Take a deep breath. Seize the moment. Breathe in. Breathe out. Now slowly open your eyes. Not quite there yet? Don’t worry. You will be soon! Summer is coming…” — Designed by PopArt Studio from Serbia.

preview
without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Solstice Sunset

“June 21 marks the longest day of the year for the Northern Hemisphere — and sunsets like these will be getting earlier and earlier after that!” — Designed by James Mitchell from the United Kingdom.

preview
without calendar: 1280×720, 1280×800, 1366×768, 1440×900, 1680×1050, 1920×1080, 1920×1200, 2560×1440, 2880×1800

Ice Creams Away!

“Summer is taking off with some magical ice cream hot air balloons.” — Designed by Sasha Endoh from Canada.

preview
without calendar: 320×480, 1024×768, 1152×864, 1280×800, 1280×960, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1920×1080, 1920×1200, 2560×1440

Deep Dive

“Summer rains, sunny days and a whole month to enjoy. Dive deep inside your passions and let them guide you.” — Designed by Ana Masnikosa from Belgrade, Serbia.

preview
without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

The Call Of Koel

“The peak of summer is upon us, and June brings scorching heat to most places in India. Summer season in my state also reminds me of the bird songs, especially the Koel bird. A black bird with a red eye, this bird’s elegant voice rings through the trees on hot summer afternoons. This June, I have created a wallpaper to give life to this experience — the birds singing in scorching heat give us some respite!” — Designed by Anuja from India.

preview
without calendar: 640×480, 1024×768, 1280×960, 1366×768, 1440×900, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Oh, The Places You Will Go!

“In celebration of high school and college graduates ready to make their way in the world!” — Designed by Bri Loesch from the United States.

preview
without calendar: 320×480, 1024×768, 1280×1024, 1440×900, 1680×1050, 1680×1200, 1920×1440, 2560×1440

Bauhaus

“I created a screenprint of one of the most famous buildings from the Bauhaus architect Mies van der Rohe for you. So, enjoy the Barcelona Pavillon for your June wallpaper.” — Designed by Anne Korfmacher from Germany.

preview
without calendar: 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Join The Wave

“The month of warmth and nice weather is finally here. We found inspiration in the World Oceans Day which occurs on June 8th and celebrates the wave of change worldwide. Join the wave and dive in!” — Designed by PopArt Studio from Serbia.

preview
without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Flamingood Vibes Only

“I love flamingos! They give me a happy feeling that I want to share with the world.” — Designed by Melissa Bogemans from Belgium.

preview
without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1200, 1920×1440, 2560×1440

Shine Your Light

“Shine your light, Before the fight, Just like the sun, Cause we don’t have to run.” — Designed by Anh Nguyet Tran from Vietnam.

preview
without calendar: 768×1280, 1024×1024, 1280×800, 1280×1024, 1366×768, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Summer Surf

“Summer vibes.” — Designed by Antun Hirsman from Croatia.

preview
without calendar: 640×480, 1152×864, 1280×1024, 1440×900, 1680×1050, 1920×1080, 1920×1440, 2650×1440

Ice Cream June

“For me, June always marks the beginning of summer! The best way to celebrate summer is of course ice cream, what else?” — Designed by Tatiana Anagnostaki from Greece.

previewwithout calendar: 1024×768, 1280×1024, 1440×900, 1680×1050, 1680×1200, 1920×1440, 2560×1440

Lavender Is In The Air!

“June always reminds me of lavender — it just smells wonderful and fresh. For this wallpaper I wanted to create a simple, yet functional design that featured — you guessed it — lavender!” — Designed by Jon Phillips from Canada.

preview
without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Strawberry Fields

Designed by Nathalie Ouederni from France.

preview
without calendar: 320×480, 1024×768, 1280×1024, 1440×900, 1680×1200, 1920×1200, 2560×1440

Start Your Day

Designed by Elise Vanoorbeek from Belgium.

preview
without calendar: 1024×768, 1280×720, 1280×800, 1280×960, 1280×1024, 1440×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1220, 1920×1440, 2560×1440

Pineapple Summer Pop

“I love creating fun and feminine illustrations and designs. I was inspired by juicy tropical pineapples to celebrate the start of summer.” — Designed by Brooke Glaser from Honolulu, Hawaii.

<img loading=”lazy” decoding=”async” src=”https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/16db22ee-c7f8-47a3-856c-992c82cd61f9/june-16-pineapple-summer-pop-preview-opt.png” alt=”Pineapple Summer Pop”

preview
without calendar: 640×480, 800×600, 1024×768, 1152×720, 1280×720, 1280×800, 1280×960, 1366×768, 1440×900, 1680×1050, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Nine Lives!

“I grew up with cats around (and drawing them all the time). They are so funny… one moment they are being funny, the next they are reserved. If you have place in your life for a pet, adopt one today!” — Designed by Karen Frolo from the United States.

preview
without calendar: 1024×768, 1024×1024, 1280×800, 1280×960, 1280×1024, 1366×768, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Adding A Commenting System To A WYSIWYG Editor

Original Source: https://smashingmagazine.com/2021/05/commenting-system-wysiwyg-editor/

In recent years, we’ve seen Collaboration penetrate a lot of digital workflows and use-cases across many professions. Just within the Design and Software Engineering community, we see designers collaborate on design artifacts using tools like Figma, teams doing Sprint and Project Planning using tools like Mural and interviews being conducted using CoderPad. All these tools are constantly aiming to bridge the gap between an online and a physical world experience of executing these workflows and making the collaboration experience as rich and seamless as possible.

For the majority of the Collaboration Tools like these, the ability to share opinions with one another and have discussions about the same content is a must-have. A Commenting System that enables collaborators to annotate parts of a document and have conversations about them is at the heart of this concept. Along with building one for text in a WYSIWYG Editor, the article tries to engage the readers into how we try to weigh the pros and cons and attempt to find a balance between application complexity and user experience when it comes to building features for WYSIWYG Editors or Word Processors in general.

Representing Comments In Document Structure

In order to find a way to represent comments in a rich text document’s data structure, let’s look at a few scenarios under which comments could be created inside an editor.

Comments created over text that has no styles on it (basic scenario);
Comments created over text that may be bold/italic/underlined, and so on;
Comments that overlap each other in some way (partial overlap where two comments share only a few words or fully-contained where one comment’s text is fully contained within text of another comment);
Comments created over text inside a link (special because links are nodes themselves in our document structure);
Comments that span multiple paragraphs (special because paragraphs are nodes in our document structure and comments are applied to text nodes which are paragraph’s children).

Looking at the above use-cases, it seems like comments in the way they can come up in a rich text document are very similar to character styles (bold, italics etc). They can overlap with each other, go over text in other types of nodes like links and even span multiple parent nodes like paragraphs.

For this reason, we use the same method to represent comments as we do for character styles, i.e. “Marks” (as they are so called in SlateJS terminology). Marks are just regular properties on nodes — speciality being that Slate’s API around marks (Editor.addMark and Editor.removeMark) handles changing of the node hierarchy as multiple marks get applied to the same range of text. This is extremely useful to us as we deal with a lot of different combinations of overlapping comments.

Comment Threads As Marks

Whenever a user selects a range of text and tries to insert a comment, technically, they’re starting a new comment thread for that text range. Because we would allow them to insert a comment and later replies to that comment, we treat this event as a new comment thread insertion in the document.

The way we represent comment threads as marks is that each comment thread is represented by a mark named as commentThread_threadID where threadID is a unique ID we assign to each comment thread. So, if the same range of text has two comment threads over it, it would have two properties set to the true — commentThread_thread1 and commentThread_thread2. This is where comment threads are very similar to character styles since if the same text was bold and italic, it would have both the properties set to true — bold and italic.

Before we dive into actually setting this structure up, it’s worth looking at how the text nodes change as comment threads get applied to them. The way this works (as it does with any mark) is that when a mark property is being set on the selected text, Slate’s Editor.addMark API would split the text node(s) if needed such that in the resulting structure, text nodes are set up in a way that each text node has the exact same value of the mark.

To understand this better, take a look at the following three examples that show the before-and-after state of the text nodes once a comment thread is inserted on the selected text:

Highlighting Commented Text

Now that we know how we are going to represent comments in the document structure, let’s go ahead and add a few to the example document from the first article and configure the editor to actually show them as highlighted. Since we will have a lot of utility functions to deal with comments in this article, we create a EditorCommentUtils module that will house all these utils. To start with, we create a function that creates a mark for a given comment thread ID. We then use that to insert a few comment threads in our ExampleDocument.

# src/utils/EditorCommentUtils.js

const COMMENT_THREAD_PREFIX = “commentThread_”;

export function getMarkForCommentThreadID(threadID) {
return `${COMMENT_THREAD_PREFIX}${threadID}`;
}

Below image underlines in red the ranges of text that we have as example comment threads added in the next code snippet. Note that the text ‘Richard McClintock’ has two comment threads that overlap each other. Specifically, this is a case of one comment thread being fully contained inside another.

# src/utils/ExampleDocument.js
import { getMarkForCommentThreadID } from “../utils/EditorCommentUtils”;
import { v4 as uuid } from “uuid”;

const exampleOverlappingCommentThreadID = uuid();

const ExampleDocument = [

{
text: “Lorem ipsum”,
[getMarkForCommentThreadID(uuid())]: true,
},

{
text: “Richard McClintock”,
// note the two comment threads here.
[getMarkForCommentThreadID(uuid())]: true,
[getMarkForCommentThreadID(exampleOverlappingCommentThreadID)]: true,
},
{
text: “, a Latin scholar”,
[getMarkForCommentThreadID(exampleOverlappingCommentThreadID)]: true,
},

];

We focus on the UI side of things of a Commenting System in this article so we assign them IDs in the example document directly using the npm package uuid. Very likely that in a production version of an editor, these IDs are created by a backend service.

We now focus on tweaking the editor to show these text nodes as highlighted. In order to do that, when rendering text nodes, we need a way to tell if it has comment threads on it. We add a util getCommentThreadsOnTextNode for that. We build on the StyledText component that we created in the first article to handle the case where it may be trying to render a text node with comments on. Since we have some more functionality coming that would be added to commented text nodes later, we create a component CommentedText that renders the commented text. StyledText will check if the text node it’s trying to render has any comments on it. If it does, it renders CommentedText. It uses a util getCommentThreadsOnTextNode to deduce that.

# src/utils/EditorCommentUtils.js

export function getCommentThreadsOnTextNode(textNode) {
return new Set(
// Because marks are just properties on nodes,
// we can simply use Object.keys() here.
Object.keys(textNode)
.filter(isCommentThreadIDMark)
.map(getCommentThreadIDFromMark)
);
}

export function getCommentThreadIDFromMark(mark) {
if (!isCommentThreadIDMark(mark)) {
throw new Error(“Expected mark to be of a comment thread”);
}
return mark.replace(COMMENT_THREAD_PREFIX, “”);
}

function isCommentThreadIDMark(mayBeCommentThread) {
return mayBeCommentThread.indexOf(COMMENT_THREAD_PREFIX) === 0;
}

The first article built a component StyledText that renders text nodes (handling character styles and so on). We extend that component to use the above util and render a CommentedText component if the node has comments on it.

# src/components/StyledText.js

import { getCommentThreadsOnTextNode } from “../utils/EditorCommentUtils”;

export default function StyledText({ attributes, children, leaf }) {

const commentThreads = getCommentThreadsOnTextNode(leaf);

if (commentThreads.size > 0) {
return (
<CommentedText
{…attributes}
// We use commentThreads and textNode props later in the article.
commentThreads={commentThreads}
textNode={leaf}
>
{children}
</CommentedText>
);
}

return <span {…attributes}>{children}</span>;
}

Below is the implementation of CommentedText that renders the text node and attaches the CSS that shows it as highlighted.

# src/components/CommentedText.js

import “./CommentedText.css”;

import classNames from “classnames”;

export default function CommentedText(props) {
const { commentThreads, …otherProps } = props;
return (
<span
{…otherProps}
className={classNames({
comment: true,
})}
>
{props.children}
</span>
);
}

# src/components/CommentedText.css

.comment {
background-color: #feeab5;
}

With all of the above code coming together, we now see text nodes with comment threads highlighted in the editor.

Note: The users currently cannot tell if certain text has overlapping comments on it. The entire highlighted text range looks like a single comment thread. We address that later in the article where we introduce the concept of active comment thread which lets users select a specific comment thread and be able to see its range in the editor.

UI Storage For Comments

Before we add the functionality that enables a user to insert new comments, we first setup a UI state to hold our comment threads. In this article, we use RecoilJS as our state management library to store comment threads, comments contained inside the threads and other metadata like creation time, status, comment author etc. Let’s add Recoil to our application:

> yarn add recoil

We use Recoil atoms to store these two data structures. If you’re not familiar with Recoil, atoms are what hold the application state. For different pieces of application state, you’d usually want to set up different atoms. Atom Family is a collection of atoms — it can be thought to be a Map from a unique key identifying the atom to the atoms themselves. It’s worth going through core concepts of Recoil at this point and familiarizing ourselves with them.

For our use case, we store comment threads as an Atom family and then wrap our application in a RecoilRoot component. RecoilRoot is applied to provide the context in which the atom values are going to be used. We create a separate module CommentState that holds our Recoil atom definitions as we add more atom definitions later in the article.

# src/utils/CommentState.js

import { atom, atomFamily } from “recoil”;

export const commentThreadsState = atomFamily({
key: “commentThreads”,
default: [],
});

export const commentThreadIDsState = atom({
key: “commentThreadIDs”,
default: new Set([]),
});

Worth calling out few things about these atom definitions:

Each atom/atom family is uniquely identified by a key and can be set up with a default value.
As we build further in this article, we are going to need a way to iterate over all the comment threads which would basically mean needing a way to iterate over commentThreadsState atom family. At the time of writing this article, the way to do that with Recoil is to set up another atom that holds all the IDs of the atom family. We do that with commentThreadIDsState above. Both these atoms would have to be kept in sync whenever we add/delete comment threads.

We add a RecoilRoot wrapper in our root App component so we can use these atoms later. Recoil’s documentation also provides a helpful Debugger component that we take as it is and drop into our editor. This component will leave console.debug logs to our Dev console as Recoil atoms are updated in real-time.

# src/components/App.js

import { RecoilRoot } from “recoil”;

export default function App() {

return (
<RecoilRoot>
>

<Editor document={document} onChange={updateDocument} />

</RecoilRoot>
);
}

# src/components/Editor.js

export default function Editor({ … }): JSX.Element {
…..

return (
<>
<Slate>
…..
</Slate>
<DebugObserver />
</>
);

function DebugObserver(): React.Node {
// see API link above for implementation.
}

We also need to need to add code that initializes our atoms with the comment threads that already exist on the document (the ones we added to our example document in the previous section, for instance). We do that at a later point when we build the Comments Sidebar that needs to read all the comment threads in a document.

At this point, we load our application, make sure there are no errors pointing to our Recoil setup and move forward.

Adding New Comments

In this section, we add a button to the toolbar that lets the user add comments (viz. create a new comment thread) for the selected text range. When the user selects a text range and clicks on this button, we need to do the below:

Assign a unique ID to the new comment thread being inserted.
Add a new mark to Slate document structure with the ID so the user sees that text highlighted.
Add the new comment thread to Recoil atoms we created in the previous section.

Let’s add a util function to EditorCommentUtils that does #1 and #2.

# src/utils/EditorCommentUtils.js

import { Editor } from “slate”;
import { v4 as uuidv4 } from “uuid”;

export function insertCommentThread(editor, addCommentThreadToState) {
const threadID = uuidv4();
const newCommentThread = {
// comments as added would be appended to the thread here.
comments: [],
creationTime: new Date(),
// Newly created comment threads are OPEN. We deal with statuses
// later in the article.
status: “open”,
};
addCommentThreadToState(threadID, newCommentThread);
Editor.addMark(editor, getMarkForCommentThreadID(threadID), true);
return threadID;
}

By using the concept of marks to store each comment thread as its own mark, we’re able to simply use the Editor.addMark API to add a new comment thread on the text range selected. This call alone handles all the different cases of adding comments — some of which we described in the earlier section — partially overlapping comments, comments inside/overlapping links, comments over bold/italic text, comments spanning paragraphs and so on. This API call adjusts the node hierarchy to create as many new text nodes as needed to handle these cases.

addCommentThreadToState is a callback function that handles step #3 — adding the new comment thread to Recoil atom . We implement that next as a custom callback hook so that it’s re-usable. This callback needs to add the new comment thread to both the atoms — commentThreadsState and commentThreadIDsState. To be able to do this, we use the useRecoilCallback hook. This hook can be used to construct a callback which gets a few things that can be used to read/set atom data. The one we’re interested in right now is the set function which can be used to update an atom value as set(atom, newValueOrUpdaterFunction).

# src/hooks/useAddCommentThreadToState.js

import {
commentThreadIDsState,
commentThreadsState,
} from “../utils/CommentState”;

import { useRecoilCallback } from “recoil”;

export default function useAddCommentThreadToState() {
return useRecoilCallback(
({ set }) => (id, threadData) => {
set(commentThreadIDsState, (ids) => new Set([…Array.from(ids), id]));
set(commentThreadsState(id), threadData);
},
[]
);
}

The first call to set adds the new ID to the existing set of comment thread IDs and returns the new Set(which becomes the new value of the atom).

In the second call, we get the atom for the ID from the atom family — commentThreadsState as commentThreadsState(id) and then set the threadData to be its value. atomFamilyName(atomID) is how Recoil lets us access an atom from its atom family using the unique key. Loosely speaking, we could say that if commentThreadsState was a javascript Map, this call is basically — commentThreadsState.set(id, threadData).

Now that we have all this code setup to handle insertion of a new comment thread to the document and Recoil atoms, lets add a button to our toolbar and wire it up with the call to these functions.

# src/components/Toolbar.js

import { insertCommentThread } from “../utils/EditorCommentUtils”;
import useAddCommentThreadToState from “../hooks/useAddCommentThreadToState”;

export default function Toolbar({ selection, previousSelection }) {
const editor = useEditor();

const addCommentThread = useAddCommentThreadToState();

const onInsertComment = useCallback(() => {
const newCommentThreadID = insertCommentThread(editor, addCommentThread);
}, [editor, addCommentThread]);

return (
<div className=”toolbar”>

<ToolBarButton
isActive={false}
label={<i className={bi ${getIconForButton(“comment”)}} />}
onMouseDown={onInsertComment}
/>
</div>
);
}

Note: We use onMouseDown and not onClick which would have made the editor lose focus and selection to become null. We’ve discussed that in a little more detail in the link insertion section of the first article.

In the below example, we see the insertion in action for a simple comment thread and an overlapping comment thread with links. Notice how we get updates from Recoil Debugger confirming our state is getting updated correctly. We also verify that new text nodes are created as threads are being added to the document.

In the above example, the user inserts the following comment threads in that order:

Comment Thread #1 over character ‘B’ (length = 1).
Comment Thread #2 over ‘AB’ (length = 2).
Comment Thread #3 over ‘BC’ (length = 2).

At the end of these insertions, because of the way Slate splits the text nodes with marks, we will have three text nodes — one for each character. Now, if the user clicks on ‘B’, going by the shortest length rule, we select thread #1 as it is the shortest of the three in length. If we don’t do that, we wouldn’t have a way to select Comment Thread #1 ever since it is only one-character in length and also a part of two other threads.

Although this rule makes it easy to surface shorter-length comment threads, we could run into situations where longer comment threads become inaccessible since all the characters contained in them are part of some other shorter comment thread. Let’s look at an example for that.

Let’s assume we have 100 characters (say, character ‘A’ typed 100 times that is) and the user inserts comment threads in the following order:

Comment Thread # 1 of range 20,80
Comment Thread # 2 of range 0,50
Comment Thread # 3 of range 51,100

As you can see in the above example, if we follow the rule we just described here, clicking on any character between #20 and #80, would always select threads #2 or #3 since they are shorter than #1 and hence #1 would not be selectable. Another scenario where this rule can leave us undecided as to which comment thread to select is when there are more than one comment threads of the same shortest length on a text node.

For such combination of overlapping comments and many other such combinations that one could think of where following this rule makes a certain comment thread inaccessible by clicking on text, we build a Comments Sidebar later in this article which gives user a view of all the comment threads present in the document so they can click on those threads in the sidebar and activate them in the editor to see the range of the comment. We still would want to have this rule and implement it as it should cover a lot of overlap scenarios except for the less-likely examples we cited above. We put in all this effort around this rule primarily because seeing highlighted text in the editor and clicking on it to comment is a more intuitive way of accessing a comment on text than merely using a list of comments in the sidebar.

Insertion Rule

The rule is:

“If the text user has selected and is trying to comment on is already fully covered by comment thread(s), don’t allow that insertion.”

This is so because if we did allow this insertion, each character in that range would end up having at least two comment threads (one existing and another the new one we just allowed) making it difficult for us to determine which one to select when the user clicks on that character later.

Looking at this rule, one might wonder why we need it in the first place if we already have the Shortest Comment Range Rule that allows us to select the smallest text range. Why not allow all combinations of overlaps if we can use the first rule to deduce the right comment thread to show? As some of the examples we’ve discussed earlier, the first rule works for a lot of scenarios but not all of them. With the Insertion Rule, we try to minimize the number of scenarios where the first rule cannot help us and we have to fallback on the Sidebar as the only way for the user to access that comment thread. Insertion Rule also prevents exact-overlaps of comment threads. This rule is commonly implemented by a lot of popular editors.

Below is an example where if this rule didn’t exist, we would allow the Comment Thread #3 and then as a result of the first rule, #3 would not be accessible since it would become the longest in length.

In this example, let’s assume we don’t wait for intersection to become 0 and just stop when we reach the edge of a comment thread. Now, if the user clicked on #2 and we start traversal in reverse direction, we’d stop at the start of text node #2 itself since that’s the start of the comment thread A. As a result, we might not compute the comment thread lengths correctly for A & B. With the implementation above traversing the farthest edges (text nodes 1,2, and 3), we should get B as the shortest comment thread as expected.

To see the implementation visually, below is a walkthrough with a slideshow of the iterations. We have two comment threads A and B that overlap each other over text node #3 and the user clicks on the overlapping text node #3.

Now that we have all the code in to make selection of comment threads work, let’s see it in action. To test our traversal code well, we test some straightforward cases of overlap and some edge cases like:

Clicking on a commented text node at the start/end of the editor.
Clicking on a commented text node with comment threads spanning multiple paragraphs.
Clicking on a commented text node right before an image node.
Clicking on a commented text node overlapping links.

Now that our state is correctly initialized, we can start implementing the sidebar. All our comment threads in the UI are stored in the Recoil atom family — commentThreadsState. As highlighted earlier, the way we iterate through all the items in a Recoil atom family is by tracking the atom keys/ids in another atom. We’ve been doing that with commentThreadIDsState. Let’s add the CommentSidebar component that iterates through the set of ids in this atom and renders a CommentThread component for each.

# src/components/CommentsSidebar.js

import “./CommentSidebar.css”;

import {commentThreadIDsState,} from “../utils/CommentState”;
import { useRecoilValue } from “recoil”;

export default function CommentsSidebar(params) {
const allCommentThreadIDs = useRecoilValue(commentThreadIDsState);

return (
<Card className={“comments-sidebar”}>
<Card.Header>Comments</Card.Header>
<Card.Body>
{Array.from(allCommentThreadIDs).map((id) => (
<Row key={id}>
<Col>
<CommentThread id={id} />
</Col>
</Row>
))}
</Card.Body>
</Card>
);
}

Now, we implement the CommentThread component that listens to the Recoil atom in the family corresponding to the comment thread it is rendering. This way, as the user adds more comments on the thread in the editor or changes any other metadata, we can update the sidebar to reflect that.

As the sidebar could grow to be really big for a document with a lot of comments, we hide all comments but the first one when we render the sidebar. The user can use the ‘Show/Hide Replies’ button to show/hide the entire thread of comments.

# src/components/CommentSidebar.js

function CommentThread({ id }) {
const { comments } = useRecoilValue(commentThreadsState(id));

const [shouldShowReplies, setShouldShowReplies] = useState(false);
const onBtnClick = useCallback(() => {
setShouldShowReplies(!shouldShowReplies);
}, [shouldShowReplies, setShouldShowReplies]);

if (comments.length === 0) {
return null;
}

const [firstComment, …otherComments] = comments;
return (
<Card
body={true}
className={classNames({
“comment-thread-container”: true,
})}
>
<CommentRow comment={firstComment} showConnector={false} />
{shouldShowReplies
? otherComments.map((comment, index) => (
<CommentRow key={comment-${index}} comment={comment} showConnector={true} />
))
: null}
{comments.length > 1 ? (
<Button
className={“show-replies-btn”}
size=”sm”
variant=”outline-primary”
onClick={onBtnClick}
>
{shouldShowReplies ? “Hide Replies” : “Show Replies”}
</Button>
) : null}
</Card>
);
}

We’ve reused the CommentRow component from the popover although we added a design treatment using showConnector prop that basically makes all the comments look connected with a thread in the sidebar.

Now, we render the CommentSidebar in the Editor and verify that it shows all the threads we have in the document and correctly updates as we add new threads or new comments to existing threads.

# src/components/Editor.js

return (
<>
<Slate … >
…..
<div className={“sidebar-wrapper”}>
<CommentsSidebar />
</div>
</Slate>
</>
);

We now move on to implementing a popular Comments Sidebar interaction found in editors:

Clicking on a comment thread in the sidebar should select/activate that comment thread. We also add a differential design treatment to highlight a comment thread in the sidebar if it’s active in the editor. To be able to do so, we use the Recoil atom — activeCommentThreadIDAtom. Let’s update the CommentThread component to support this.

# src/components/CommentsSidebar.js

function CommentThread({ id }) {

const [activeCommentThreadID, setActiveCommentThreadID] = useRecoilState(
activeCommentThreadIDAtom
);

const onClick = useCallback(() => {
setActiveCommentThreadID(id);
}, [id, setActiveCommentThreadID]);

return (
<Card
body={true}
className={classNames({
“comment-thread-container”: true,
“is-active”: activeCommentThreadID === id,
})}
onClick={onClick}
>
….
</Card>
);

If we look closely, we have a bug in our implementation of sync-ing the active comment thread with the sidebar. As we click on different comment threads in the sidebar, the correct comment thread is indeed highlighted in the editor. However, the Comment Popover doesn’t actually move to the changed active comment thread. It stays where it was first rendered. If we look at the implementation of the Comment Popover, it renders itself against the first text node in the editor’s selection. At that point in the implementation, the only way to select a comment thread was to click on a text node so we could conveniently rely on the editor’s selection since it was updated by Slate as a result of the click event. In the above onClick event, we don’t update the selection but merely update the Recoil atom value causing Slate’s selection to remain unchanged and hence the Comment Popover doesn’t move.

A solution to this problem is to update the editor’s selection along with updating the Recoil atom when the user clicks on the comment thread in the sidebar. The steps do this are:

Find all text nodes that have this comment thread on them that we are going to set as the new active thread.
Sort these text nodes in the order in which they appear in the document (We use Slate’s Path.compare API for this).
Compute a selection range that spans from the start of the first text node to the end of the last text node.
Set the selection range to be the editor’s new selection (using Slate’s Transforms.select API).

If we just wanted to fix the bug, we could just find the first text node in Step #1 that has the comment thread and set that to be the editor’s selection. However, it feels like a cleaner approach to select the entire comment range as we really are selecting the comment thread.

Let’s update the onClick callback implementation to include the steps above.

const onClick = useCallback(() => {

const textNodesWithThread = Editor.nodes(editor, {
at: [],
mode: “lowest”,
match: (n) => Text.isText(n) && getCommentThreadsOnTextNode(n).has(id),
});

let textNodeEntry = textNodesWithThread.next().value;
const allTextNodePaths = [];

while (textNodeEntry != null) {
allTextNodePaths.push(textNodeEntry[1]);
textNodeEntry = textNodesWithThread.next().value;
}

// sort the text nodes
allTextNodePaths.sort((p1, p2) => Path.compare(p1, p2));

// set the selection on the editor
Transforms.select(editor, {
anchor: Editor.point(editor, allTextNodePaths[0], { edge: “start” }),
focus: Editor.point(
editor,
allTextNodePaths[allTextNodePaths.length – 1],
{ edge: “end” }
),
});

// Update the Recoil atom value.
setActiveCommentThreadID(id);
}, [editor, id, setActiveCommentThreadID]);

Note: allTextNodePaths contains the path to all the text nodes. We use the Editor.point API to get the start and end points at that path. The first article goes through Slate’s Location concepts. They’re also well-documented on Slate’s documentation.

Let’s verify that this implementation does fix the bug and the Comment Popover moves to the active comment thread correctly. This time, we also test with a case of overlapping threads to make sure it doesn’t break there.

With the bug fix, we’ve enabled another sidebar interaction that we haven’t discussed yet. If we have a really long document and the user clicks on a comment thread in the sidebar that’s outside the viewport, we’d want to scroll to that part of the document so the user can focus on the comment thread in the editor. By setting the selection above using Slate’s API, we get that for free. Let’s see it in action below.

With that, we wrap our implementation of the sidebar. Towards the end of the article, we list out some nice feature additions and enhancements we can do to the Comments Sidebar that help elevate the Commenting and Review experience on the editor.

Resolving And Re-Opening Comments

In this section, we focus on enabling users to mark comment threads as ‘Resolved’ or be able to re-open them for discussion if needed. From an implementation detail perspective, this is the status metadata on a comment thread that we change as the user performs this action. From a user’s perspective, this is a very useful feature as it gives them a way to affirm that the discussion about something on the document has concluded or needs to be re-opened because there are some updates/new perspectives, and so on.

To enable toggling the status, we add a button to the CommentPopover that allows the user to toggle between the two statuses: open and resolved.

# src/components/CommentThreadPopover.js

export default function CommentThreadPopover({
editorOffsets,
selection,
threadID,
}) {

const [threadData, setCommentThreadData] = useRecoilState(
commentThreadsState(threadID)
);

const onToggleStatus = useCallback(() => {
const currentStatus = threadData.status;
setCommentThreadData((threadData) => ({
…threadData,
status: currentStatus === “open” ? “resolved” : “open”,
}));
}, [setCommentThreadData, threadData.status]);

return (
<NodePopover

header={
<Header
status={threadData.status}
shouldAllowStatusChange={threadData.comments.length > 0}
onToggleStatus={onToggleStatus}
/>
}
>
<div className={“comment-list”}>

</div>
</NodePopover>
);
}

function Header({ onToggleStatus, shouldAllowStatusChange, status }) {
return (
<div className={“comment-thread-popover-header”}>
{shouldAllowStatusChange && status != null ? (
<Button size=”sm” variant=”primary” onClick={onToggleStatus}>
{status === “open” ? “Resolve” : “Re-Open”}
</Button>
) : null}
</div>
);
}

Before we test this, let’s also give the Comments Sidebar a differential design treatment for resolved comments so that the user can easily detect which comment threads are un-resolved or open and focus on those if they want to.

# src/components/CommentsSidebar.js

function CommentThread({ id }) {

const { comments, status } = useRecoilValue(commentThreadsState(id));


return (
<Card
body={true}
className={classNames({
“comment-thread-container”: true,
“is-resolved”: status === “resolved”,
“is-active”: activeCommentThreadID === id,
})}
onClick={onClick}
>

</Card>
);
}

Conclusion

In this article, we built the core UI infrastructure for a Commenting System on a Rich Text Editor. The set of functionalities we add here act as a foundation to build a richer Collaboration Experience on an editor where collaborators could annotate parts of the document and have conversations about them. Adding a Comments Sidebar gives us a space to have more conversational or review-based functionalities to be enabled on the product.

Along those lines, here are some of features that a Rich Text Editor could consider adding on top of what we built in this article:

Support for @ mentions so collaborators could tag one another in comments;
Support for media types like images and videos to be added to comment threads;
Suggestion Mode at the document level that allows reviewers to make edits to the document that appear as suggestions for changes. One could refer to this feature in Google Docs or Change Tracking in Microsoft Word as examples;
Enhancements to the sidebar to search conversations by keyword, filter threads by status or comment author(s), and so on.